From cb43f64ec4a81b1ffabc7c20f67f71b8d6c58e98 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 11 Apr 2018 09:30:39 +0200 Subject: [PATCH 0001/1714] have safe_rmpath() on windows retry for 1 sec in case of failure as a workaround for orphaned open handles --- psutil/tests/__init__.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index dcdbd4fa82..80404a3325 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -357,7 +357,7 @@ def create_proc_children_pair(): subp = pyrun(s) child1 = psutil.Process(subp.pid) data = wait_for_file(_TESTFN2, delete=False, empty=False) - os.remove(_TESTFN2) + safe_rmpath(_TESTFN2) child2_pid = int(data) _pids_started.add(child2_pid) child2 = psutil.Process(child2_pid) @@ -663,7 +663,7 @@ def wait_for_file(fname, delete=True, empty=False): if not empty: assert data if delete: - os.remove(fname) + safe_rmpath(fname) return data @@ -685,12 +685,34 @@ def call_until(fun, expr): 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): - os.rmdir(path) + fun = functools.partial(shutil.rmtree, path) + else: + fun = functools.partial(os.remove, path) + if POSIX: + fun() else: - os.remove(path) + retry_fun(fun) except OSError as err: if err.errno != errno.ENOENT: raise From fa54c8ae4a384394efe867681a2c5d40a0d50de0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 12 Apr 2018 07:50:46 +0200 Subject: [PATCH 0002/1714] adjust win deps --- appveyor.yml | 5 +---- scripts/internal/winmake.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 1bacc06517..a0ca6a0edf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -71,14 +71,11 @@ install: # - ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:/get-pip.py') - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user setuptools pip" - - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user unittest2 ipaddress pypiwin32==219 wmi wheel" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py setup-dev-env" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py clean" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build build_ext -i" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py develop" - # 1.0.1 is the latest release supporting python 2.6 - - "%WITH_COMPILER% %PYTHON%/Scripts/pip.exe install mock==1.0.1" build: off diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 5754375088..f12a40cf5e 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -33,20 +33,24 @@ DEPS = [ "coverage", "flake8", - "ipaddress", - "mock", "nose", "pdbpp", "perf", "pip", - "pypiwin32", + "pypiwin32==219" if sys.version_info[:2] <= (3, 4) else "pypiwin32", "pyreadline", "setuptools", - "unittest2", "wheel", "wmi", "requests" ] +if sys.version_info[:2] <= (2, 6): + DEPS.append('unittest2') +if sys.version_info[:2] <= (2, 7): + DEPS.append('mock') +if sys.version_info[:2] <= (3, 2): + DEPS.append('ipaddress') + _cmds = {} if PY3: basestring = str From 540c71af92bb3ca60a2da63e4392f941087962ba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 12 Apr 2018 10:29:02 +0200 Subject: [PATCH 0003/1714] another appveyor adjustment --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a0ca6a0edf..8b73375e80 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -71,11 +71,11 @@ install: # - ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:/get-pip.py') - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user setuptools pip" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py setup-dev-env" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build build_ext -i" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py develop" + - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user unittest2 ipaddress pypiwin32 wmi wheel" build: off From 8a5ad9adaa758598156d1a0c701969dfd3814d36 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 12 Apr 2018 10:30:37 +0200 Subject: [PATCH 0004/1714] and another one --- appveyor.yml | 2 +- scripts/internal/winmake.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 8b73375e80..f39053adea 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -75,7 +75,7 @@ install: - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build build_ext -i" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py develop" - - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user unittest2 ipaddress pypiwin32 wmi wheel" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py setup-dev-env" build: off diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index f12a40cf5e..bd518f0922 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -24,7 +24,11 @@ import tempfile -PYTHON = os.getenv('PYTHON', sys.executable) +APPVEYOR = bool(os.environ.get('APPVEYOR')) +if APPVEYOR: + PYTHON = sys.executable +else: + PYTHON = os.getenv('PYTHON', sys.executable) TSCRIPT = os.getenv('TSCRIPT', 'psutil\\tests\\__main__.py') GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" PY3 = sys.version_info[0] == 3 From b2bbd272f6b183ddc7c119d39ed5c969b399239f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 12 Apr 2018 10:51:44 +0200 Subject: [PATCH 0005/1714] remove incorrect test assumption that proc cpu percent on windows is supposed to be <= 100, see https://ci.appveyor.com/project/giampaolo/psutil/build/1477/job/w1e0u92xrgg91ye3 --- psutil/tests/test_process.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 3411114aff..6f58be6d25 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -238,10 +238,6 @@ def test_cpu_percent(self): percent = p.cpu_percent(interval=None) self.assertIsInstance(percent, float) self.assertGreaterEqual(percent, 0.0) - if not POSIX: - self.assertLessEqual(percent, 100.0) - else: - self.assertGreaterEqual(percent, 0.0) with self.assertRaises(ValueError): p.cpu_percent(interval=-1) From 1ab4b3a1f416eda6c42414a7fbc2dd716e7e42f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Apr 2018 04:16:16 +0200 Subject: [PATCH 0006/1714] adust winmake individual tests --- scripts/internal/winmake.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index bd518f0922..ff3a648914 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -365,7 +365,7 @@ def test_process(): """Run process tests""" install() test_setup() - sh("%s -m unittest -v psutil.tests.test_process" % PYTHON) + sh("%s psutil\\tests\\test_process.py" % PYTHON) @cmd @@ -373,7 +373,7 @@ def test_system(): """Run system tests""" install() test_setup() - sh("%s -m unittest -v psutil.tests.test_system" % PYTHON) + sh("%s psutil\\tests\\test_system.py" % PYTHON) @cmd @@ -381,7 +381,7 @@ def test_platform(): """Run windows only tests""" install() test_setup() - sh("%s -m unittest -v psutil.tests.test_windows" % PYTHON) + sh("%s psutil\\tests\\test_windows.py" % PYTHON) @cmd @@ -389,7 +389,7 @@ def test_misc(): """Run misc tests""" install() test_setup() - sh("%s -m unittest -v psutil.tests.test_misc" % PYTHON) + sh("%s psutil\\tests\\test_misc.py" % PYTHON) @cmd @@ -397,7 +397,7 @@ def test_unicode(): """Run unicode tests""" install() test_setup() - sh("%s -m unittest -v psutil.tests.test_unicode" % PYTHON) + sh("%s psutil\\tests\\test_unicode.py" % PYTHON) @cmd @@ -405,7 +405,7 @@ def test_connections(): """Run connections tests""" install() test_setup() - sh("%s -m unittest -v psutil.tests.test_connections" % PYTHON) + sh("%s psutil\\tests\\test_connections.py" % PYTHON) @cmd @@ -413,7 +413,7 @@ def test_contracts(): """Run contracts tests""" install() test_setup() - sh("%s -m unittest -v psutil.tests.test_contracts" % PYTHON) + sh("%s psutil\\tests\\test_contracts.py" % PYTHON) @cmd From ae7e4b4a9eebd3754cf0ee2a4ef0054a10151310 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Apr 2018 04:26:21 +0200 Subject: [PATCH 0007/1714] fix some compilation warns on win --- psutil/_psutil_windows.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index bb993dbd43..89d192697d 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -855,7 +855,8 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) goto done; } - info_array_size = tmp_size + (entries * sizeof(PSAPI_WORKING_SET_BLOCK)); + info_array_size = tmp_size + \ + ((DWORD)entries * sizeof(PSAPI_WORKING_SET_BLOCK)); info_array = (PSAPI_WORKING_SET_INFORMATION*)malloc(info_array_size); if (!info_array) { PyErr_NoMemory(); @@ -2381,7 +2382,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { int devNum; int i; size_t ioctrlSize; - BOOL WINAPI ret; + BOOL ret; PyObject *py_retdict = PyDict_New(); PyObject *py_tuple = NULL; From 1e63ab355d592fefd77c819fb642fe6d7963f295 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Apr 2018 04:31:51 +0200 Subject: [PATCH 0008/1714] fix some compilation warns on win --- psutil/_psutil_windows.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 89d192697d..d39afb218c 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2958,7 +2958,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess = NULL; PVOID baseAddress; - PVOID previousAllocationBase; + ULONGLONG previousAllocationBase; WCHAR mappedFileName[MAX_PATH]; SYSTEM_INFO system_info; LPVOID maxAddr; From 34e98b6e2a5739f9e633436e8cd61f3246c1091e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Apr 2018 04:52:55 -0400 Subject: [PATCH 0009/1714] 771 Windows CPU count (#1257) * use GetLogicalProcessorInformation() to get logical cpu_count() * return None if cpu_count() is undetermined + add mock test * style * factor out logical CPU num fun * remove unused code * psutil_get_num_cpus(): provide an option to fail on err * add comments * reuse get_num_cpus() function * error out if get_num_cpus() fail * use GetLogicalProcessorInformationEx to get phys CPU num * on win vista/xp just return None for phys CPU count * rename vars * fix C compiler warnings + remove mingw workarounds * return None if phys cpu count cant' be determined; update HISTORY * update HISTORY * update doc * add WMI tests * refactor tests * print debug msg for cpu phys returning None on win < 7 * try to fix win test * appveyor debug * fix typo * adjust appveyor 64 bit versions * debug msg * fix for loop * re-enable python versions * (maybe) finally fix GetLogicalProcessorInformationEx return len --- HISTORY.rst | 11 ++ MANIFEST.in | 1 - appveyor.yml | 18 +--- docs/index.rst | 17 +-- psutil/_psutil_common.c | 2 +- psutil/_psutil_windows.c | 197 +++++++++++++++++++++++------------ psutil/_pswindows.py | 2 +- psutil/arch/windows/glpi.h | 41 -------- psutil/tests/test_system.py | 7 +- psutil/tests/test_windows.py | 48 ++++++--- 10 files changed, 195 insertions(+), 149 deletions(-) delete mode 100644 psutil/arch/windows/glpi.h diff --git a/HISTORY.rst b/HISTORY.rst index 65cad58e01..b5fad59c50 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,12 @@ XXXX-XX-XX - 694_: [SunOS] cmdline() could be truncated at the 15th character when reading it from /proc. An extra effort is made by reading it from process address space first. (patch by Georg Sauthoff) +- 771_: [Windows] cpu_count() (both logical and physical) return a wrong + (smaller) number on systems using process groups (> 64 cores). +- 771_: [Windows] cpu_times(percpu=True) return fewer CPUs on systems using + process groups (> 64 cores). +- 771_: [Windows] cpu_stats() and cpu_freq() may return incorrect results on + systems using process groups (> 64 cores). - 1193_: [SunOS] Return uid/gid from /proc/pid/psinfo if there aren't enough permissions for /proc/pid/cred. (patch by Georg Sauthoff) - 1194_: [SunOS] Return nice value from psinfo as getpriority() doesn't @@ -38,6 +44,11 @@ XXXX-XX-XX - 1255_: [FreeBSD] swap_memory() stats were erroneously represented in KB. (patch by Denis Krienbühl) +**Backward compatibility** + +- 771_: [Windows] cpu_count(logical=False) on Windows XP and Vista is no + longer supported and returns None. + 5.4.3 ===== diff --git a/MANIFEST.in b/MANIFEST.in index 87d9bebb49..85d1f21ece 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -67,7 +67,6 @@ include psutil/arch/solaris/environ.c include psutil/arch/solaris/environ.h include psutil/arch/solaris/v10/ifaddrs.c include psutil/arch/solaris/v10/ifaddrs.h -include psutil/arch/windows/glpi.h include psutil/arch/windows/inet_ntop.c include psutil/arch/windows/inet_ntop.h include psutil/arch/windows/ntextapi.h diff --git a/appveyor.yml b/appveyor.yml index f39053adea..436faadbc4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,20 +45,13 @@ environment: - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" - ARCH: x86_64 - VS_VER: "2015" - INSTANCENAME: "SQL2012SP1" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" - ARCH: x86_64 - VS_VER: "2015" - INSTANCENAME: "SQL2012SP1" # Also build on a Python version not pre-installed by Appveyor. # See: https://github.com/ogrisel/python-appveyor-demo/issues/10 - # - PYTHON: "C:\\Python266" # PYTHON_VERSION: "2.6.6" # PYTHON_ARCH: "32" @@ -71,20 +64,17 @@ install: # - ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:/get-pip.py') - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user setuptools pip" - - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" - - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build" - - "%WITH_COMPILER% %PYTHON%/python.exe setup.py build build_ext -i" - - "%WITH_COMPILER% %PYTHON%/python.exe setup.py develop" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py setup-dev-env" + - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install" build: off test_script: - - "%WITH_COMPILER% %PYTHON%/python -V" - - "set PYTHONWARNINGS=all && set PSUTIL_TESTING=1 && set PSUTIL_DEBUG=1 && %WITH_COMPILER% %PYTHON%/python psutil/tests/__main__.py" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test" after_test: - - "%WITH_COMPILER% %PYTHON%/python setup.py bdist_wheel" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py wheel" artifacts: - path: dist\* diff --git a/docs/index.rst b/docs/index.rst index 395fd688f0..548f361f4c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -164,15 +164,11 @@ CPU Return the number of logical CPUs in the system (same as `os.cpu_count() `__ in Python 3.4) or ``None`` if undetermined. - This number may not be equivalent to the number of CPUs the current process - can actually use in case process CPU affinity has been changed or Linux - cgroups are being used. - The number of usable CPUs can be obtained with - ``len(psutil.Process().cpu_affinity())``. If *logical* is ``False`` return the number of physical cores only (hyper - thread CPUs are excluded). + thread CPUs are excluded) or ``None`` if undetermined. On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return - ``None``. Example on a system having 2 physical hyper-thread CPU cores: + ``None``. + Example on a system having 2 physical hyper-thread CPU cores: >>> import psutil >>> psutil.cpu_count() @@ -180,7 +176,12 @@ CPU >>> psutil.cpu_count(logical=False) 2 - Example returning the number of CPUs usable by the current process: + Note that this number is not equivalent to the number of CPUs the current + process can actually use. + That can vary in case process CPU affinity has been changed, Linux cgroups + are being used or on Windows systems using processor groups or having more + than 64 CPUs. + The number of usable CPUs can be obtained with: >>> len(psutil.Process().cpu_affinity()) 1 diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index e08f011c20..49b91c8269 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -85,7 +85,7 @@ void psutil_debug(const char* format, ...) { va_list argptr; va_start(argptr, format); - fprintf(stderr, "psutil-dubug> "); + fprintf(stderr, "psutil-debug> "); vfprintf(stderr, format, argptr); fprintf(stderr, "\n"); va_end(argptr); diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index d39afb218c..ae2e538a4a 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -38,10 +38,6 @@ #include "arch/windows/inet_ntop.h" #include "arch/windows/services.h" -#ifdef __MINGW32__ -#include "arch/windows/glpi.h" -#endif - /* * ============================================================================ @@ -63,8 +59,13 @@ Py_DECREF(_SOCK_STREAM);\ Py_DECREF(_SOCK_DGRAM); -typedef BOOL (WINAPI *LPFN_GLPI) - (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION, PDWORD); +#if (_WIN32_WINNT >= 0x0601) // Windows 7 +typedef BOOL (WINAPI *PFN_GETLOGICALPROCESSORINFORMATIONEX)( + LOGICAL_PROCESSOR_RELATIONSHIP relationship, + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Buffer, + PDWORD ReturnLength); +static PFN_GETLOGICALPROCESSORINFORMATIONEX _GetLogicalProcessorInformationEx; +#endif // Fix for mingw32, see: // https://github.com/giampaolo/psutil/issues/351#c2 @@ -195,6 +196,45 @@ psutil_get_nic_addresses() { } +/* + * Return the number of logical, active CPUs. Return 0 if undetermined. + * See discussion at: https://bugs.python.org/issue33166#msg314631 + */ +unsigned int +psutil_get_num_cpus(int fail_on_err) { + unsigned int ncpus = 0; + SYSTEM_INFO sysinfo; + static DWORD(CALLBACK *_GetActiveProcessorCount)(WORD) = NULL; + HINSTANCE hKernel32; + + // GetActiveProcessorCount is available only on 64 bit versions + // of Windows from Windows 7 onward. + // Windows Vista 64 bit and Windows XP doesn't have it. + hKernel32 = GetModuleHandleW(L"KERNEL32"); + _GetActiveProcessorCount = (void*)GetProcAddress( + hKernel32, "GetActiveProcessorCount"); + + if (_GetActiveProcessorCount != NULL) { + ncpus = _GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); + if ((ncpus == 0) && (fail_on_err == 1)) { + PyErr_SetFromWindowsErr(0); + } + } + else { + psutil_debug("GetActiveProcessorCount() not available; " + "using GetNativeSystemInfo()"); + GetNativeSystemInfo(&sysinfo); + ncpus = (unsigned int)sysinfo.dwNumberOfProcessors; + if ((ncpus == 0) && (fail_on_err == 1)) { + PyErr_SetString( + PyExc_RuntimeError, + "GetNativeSystemInfo() failed to retrieve CPU count"); + } + } + return ncpus; +} + + /* * ============================================================================ * Public Python API @@ -553,55 +593,77 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { } - /* - * Return the number of logical CPUs. + * Return the number of active, logical CPUs. */ static PyObject * psutil_cpu_count_logical(PyObject *self, PyObject *args) { - SYSTEM_INFO system_info; - system_info.dwNumberOfProcessors = 0; + unsigned int ncpus; - GetSystemInfo(&system_info); - if (system_info.dwNumberOfProcessors == 0) - Py_RETURN_NONE; // mimic os.cpu_count() + ncpus = psutil_get_num_cpus(0); + if (ncpus != 0) + return Py_BuildValue("I", ncpus); else - return Py_BuildValue("I", system_info.dwNumberOfProcessors); + Py_RETURN_NONE; // mimick os.cpu_count() } /* - * Return the number of physical CPU cores. + * Return the number of physical CPU cores (hyper-thread CPUs count + * is excluded). */ +#if (_WIN32_WINNT < 0x0601) // < Windows 7 (namely Vista and XP) +static PyObject * +psutil_cpu_count_phys(PyObject *self, PyObject *args) { + // Note: we may have used GetLogicalProcessorInformation() + // but I don't want to prolong support for Windows XP and Vista. + // On such old systems psutil will compile but this API will + // just return None. + psutil_debug("Win < 7; cpu_count_phys() forced to None"); + Py_RETURN_NONE; +} +#else // Windows >= 7 static PyObject * psutil_cpu_count_phys(PyObject *self, PyObject *args) { - LPFN_GLPI glpi; DWORD rc; - PSYSTEM_LOGICAL_PROCESSOR_INFORMATION buffer = NULL; - PSYSTEM_LOGICAL_PROCESSOR_INFORMATION ptr = NULL; + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX buffer = NULL; + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX ptr = NULL; DWORD length = 0; DWORD offset = 0; - int ncpus = 0; - - glpi = (LPFN_GLPI)GetProcAddress(GetModuleHandle(TEXT("kernel32")), - "GetLogicalProcessorInformation"); - if (glpi == NULL) + DWORD ncpus = 0; + + // GetLogicalProcessorInformationEx() is available from Windows 7 + // onward. Differently from GetLogicalProcessorInformation() + // it supports process groups, meaning this is able to report more + // than 64 CPUs. See: + // https://bugs.python.org/issue33166 + _GetLogicalProcessorInformationEx = \ + (PFN_GETLOGICALPROCESSORINFORMATIONEX)GetProcAddress( + GetModuleHandle(TEXT("kernel32")), + "GetLogicalProcessorInformationEx"); + if (_GetLogicalProcessorInformationEx == NULL) { + psutil_debug("failed loading GetLogicalProcessorInformationEx()"); goto return_none; + } while (1) { - rc = glpi(buffer, &length); + rc = _GetLogicalProcessorInformationEx( + RelationAll, buffer, &length); if (rc == FALSE) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - if (buffer) + if (buffer) { free(buffer); - buffer = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)malloc( - length); + } + buffer = \ + (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)malloc(length); if (NULL == buffer) { PyErr_NoMemory(); return NULL; } } else { + psutil_debug("GetLogicalProcessorInformationEx() returned ", + GetLastError()); goto return_none; } } @@ -611,25 +673,30 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { } ptr = buffer; - while (offset + sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION) <= length) { - if (ptr->Relationship == RelationProcessorCore) + while (ptr->Size > 0 && offset + ptr->Size <= length) { + if (ptr->Relationship == RelationProcessorCore) { ncpus += 1; - offset += sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); - ptr++; + } + offset += ptr->Size; + ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)\ + (((char*)ptr) + ptr->Size); } free(buffer); - if (ncpus == 0) - goto return_none; - else - return Py_BuildValue("i", ncpus); + if (ncpus != 0) { + return Py_BuildValue("I", ncpus); + } + else { + psutil_debug("GetLogicalProcessorInformationEx() count was 0"); + Py_RETURN_NONE; // mimick os.cpu_count() + } return_none: - // mimic os.cpu_count() if (buffer != NULL) free(buffer); Py_RETURN_NONE; } +#endif /* @@ -957,8 +1024,8 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { double idle, kernel, systemt, user, interrupt, dpc; NTSTATUS status; _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; - SYSTEM_INFO si; UINT i; + unsigned int ncpus; PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); @@ -978,14 +1045,15 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { goto error; } - // retrives number of processors - GetSystemInfo(&si); + // retrieves number of processors + ncpus = psutil_get_num_cpus(1); + if (ncpus == 0) + goto error; // allocates an array of _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION // structures, one per processor sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(si.dwNumberOfProcessors * \ - sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); if (sppi == NULL) { PyErr_NoMemory(); goto error; @@ -995,8 +1063,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { status = NtQuerySystemInformation( SystemProcessorPerformanceInformation, sppi, - si.dwNumberOfProcessors * sizeof - (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), + ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), NULL); if (status != 0) { PyErr_SetFromWindowsErr(0); @@ -1006,7 +1073,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // computes system global times summing each // processor value idle = user = kernel = interrupt = dpc = 0; - for (i = 0; i < si.dwNumberOfProcessors; i++) { + for (i = 0; i < ncpus; i++) { py_tuple = NULL; user = (double)((HI_T * sppi[i].UserTime.HighPart) + (LO_T * sppi[i].UserTime.LowPart)); @@ -3403,7 +3470,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { _SYSTEM_PERFORMANCE_INFORMATION *spi = NULL; _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; _SYSTEM_INTERRUPT_INFORMATION *InterruptInformation = NULL; - SYSTEM_INFO si; + unsigned int ncpus; UINT i; ULONG64 dpcs = 0; ULONG interrupts = 0; @@ -3421,13 +3488,14 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { goto error; } - // retrives number of processors - GetSystemInfo(&si); + // retrieves number of processors + ncpus = psutil_get_num_cpus(1); + if (ncpus == 0) + goto error; // get syscalls / ctx switches spi = (_SYSTEM_PERFORMANCE_INFORMATION *) \ - malloc(si.dwNumberOfProcessors * \ - sizeof(_SYSTEM_PERFORMANCE_INFORMATION)); + malloc(ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION)); if (spi == NULL) { PyErr_NoMemory(); goto error; @@ -3435,7 +3503,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { status = NtQuerySystemInformation( SystemPerformanceInformation, spi, - si.dwNumberOfProcessors * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), + ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), NULL); if (status != 0) { PyErr_SetFromWindowsErr(0); @@ -3444,8 +3512,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { // get DPCs InterruptInformation = \ - malloc(sizeof(_SYSTEM_INTERRUPT_INFORMATION) * - si.dwNumberOfProcessors); + malloc(sizeof(_SYSTEM_INTERRUPT_INFORMATION) * ncpus); if (InterruptInformation == NULL) { PyErr_NoMemory(); goto error; @@ -3454,20 +3521,19 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { status = NtQuerySystemInformation( SystemInterruptInformation, InterruptInformation, - si.dwNumberOfProcessors * sizeof(SYSTEM_INTERRUPT_INFORMATION), + ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), NULL); if (status != 0) { PyErr_SetFromWindowsErr(0); goto error; } - for (i = 0; i < si.dwNumberOfProcessors; i++) { + for (i = 0; i < ncpus; i++) { dpcs += InterruptInformation[i].DpcCount; } // get interrupts sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(si.dwNumberOfProcessors * \ - sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); if (sppi == NULL) { PyErr_NoMemory(); goto error; @@ -3476,15 +3542,14 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { status = NtQuerySystemInformation( SystemProcessorPerformanceInformation, sppi, - si.dwNumberOfProcessors * sizeof - (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), + ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), NULL); if (status != 0) { PyErr_SetFromWindowsErr(0); goto error; } - for (i = 0; i < si.dwNumberOfProcessors; i++) { + for (i = 0; i < ncpus; i++) { interrupts += sppi[i].InterruptCount; } @@ -3525,19 +3590,15 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { LPBYTE pBuffer = NULL; ULONG current; ULONG max; - unsigned int num_cpus; - SYSTEM_INFO system_info; - system_info.dwNumberOfProcessors = 0; + unsigned int ncpus; // Get the number of CPUs. - GetSystemInfo(&system_info); - if (system_info.dwNumberOfProcessors == 0) - num_cpus = 1; - else - num_cpus = system_info.dwNumberOfProcessors; + ncpus = psutil_get_num_cpus(1); + if (ncpus == 0) + return NULL; // Allocate size. - size = num_cpus * sizeof(PROCESSOR_POWER_INFORMATION); + size = ncpus * sizeof(PROCESSOR_POWER_INFORMATION); pBuffer = (BYTE*)LocalAlloc(LPTR, size); if (! pBuffer) return PyErr_SetFromWindowsErr(0); diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index ab727cba30..18651d6cf3 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -288,7 +288,7 @@ def cpu_count_logical(): def cpu_count_physical(): - """Return the number of physical CPUs in the system.""" + """Return the number of physical CPU cores in the system.""" return cext.cpu_count_phys() diff --git a/psutil/arch/windows/glpi.h b/psutil/arch/windows/glpi.h deleted file mode 100644 index 6f98483733..0000000000 --- a/psutil/arch/windows/glpi.h +++ /dev/null @@ -1,41 +0,0 @@ -// mingw headers are missing this - -typedef enum _LOGICAL_PROCESSOR_RELATIONSHIP { - RelationProcessorCore, - RelationNumaNode, - RelationCache, - RelationProcessorPackage, - RelationGroup, - RelationAll=0xffff -} LOGICAL_PROCESSOR_RELATIONSHIP; - -typedef enum _PROCESSOR_CACHE_TYPE { - CacheUnified,CacheInstruction,CacheData,CacheTrace -} PROCESSOR_CACHE_TYPE; - -typedef struct _CACHE_DESCRIPTOR { - BYTE Level; - BYTE Associativity; - WORD LineSize; - DWORD Size; - PROCESSOR_CACHE_TYPE Type; -} CACHE_DESCRIPTOR,*PCACHE_DESCRIPTOR; - -typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION { - ULONG_PTR ProcessorMask; - LOGICAL_PROCESSOR_RELATIONSHIP Relationship; - union { - struct { - BYTE Flags; - } ProcessorCore; - struct { - DWORD NodeNumber; - } NumaNode; - CACHE_DESCRIPTOR Cache; - ULONGLONG Reserved[2]; - }; -} SYSTEM_LOGICAL_PROCESSOR_INFORMATION,*PSYSTEM_LOGICAL_PROCESSOR_INFORMATION; - -WINBASEAPI WINBOOL WINAPI -GetLogicalProcessorInformation(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer, - PDWORD ReturnedLength); \ No newline at end of file diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 20b132a91d..6081ea5a2d 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -268,8 +268,11 @@ def test_cpu_count(self): if "physical id" not in cpuinfo_data: raise unittest.SkipTest("cpuinfo doesn't include physical id") physical = psutil.cpu_count(logical=False) - self.assertGreaterEqual(physical, 1) - self.assertGreaterEqual(logical, physical) + 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 diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 32c46f67a3..ffa763d099 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -69,35 +69,57 @@ def wrapper(self, *args, **kwargs): @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) +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(self): + 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_2(self): + 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), From feded95354062d3cc36f71993791b32a1c87098c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Apr 2018 11:09:43 +0200 Subject: [PATCH 0010/1714] pre-release --- HISTORY.rst | 2 +- docs/index.rst | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b5fad59c50..a2913b265d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.4.4 ===== -XXXX-XX-XX +2018-04-13 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 548f361f4c..67a7c56738 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2624,9 +2624,13 @@ take a look at the Timeline ======== +- 2018-04-13: + `5.4.4 `__ - + `what's new `__ - + `diff `__ - 2018-01-01: `5.4.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-12-07: `5.4.2 `__ - From 91b0d9c05d5781d3cf6594f2a3660ee897be0345 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Apr 2018 19:34:06 +0200 Subject: [PATCH 0011/1714] #1268: fix setup.py's extra_require --- HISTORY.rst | 10 ++++++++++ docs/index.rst | 4 ++++ psutil/__init__.py | 2 +- setup.py | 22 ++++++++++++++-------- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a2913b265d..685a8718e2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.4.5 +===== + +2018-04-14 + +**Bug fixes** + +- 1268_: setup.py's extra_require parameter requires latest setuptools version, + breaking quite a lot of installations. + 5.4.4 ===== diff --git a/docs/index.rst b/docs/index.rst index 67a7c56738..c8f7be8f5e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2624,6 +2624,10 @@ take a look at the Timeline ======== +- 2018-04-14: + `5.4.5 `__ - + `what's new `__ - + `diff `__ - 2018-04-13: `5.4.4 `__ - `what's new `__ - diff --git a/psutil/__init__.py b/psutil/__init__.py index 1b0f2c045b..90e8b15e50 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -218,7 +218,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.4.4" +__version__ = "5.4.5" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED diff --git a/setup.py b/setup.py index 61056f5f1d..d8db694ebf 100755 --- a/setup.py +++ b/setup.py @@ -52,6 +52,18 @@ if POSIX: sources.append('psutil/_psutil_posix.c') +tests_require = [] +if sys.version_info[:2] <= (2, 6): + tests_require.append('unittest2') +if sys.version_info[:2] <= (2, 7): + tests_require.append('mock') +if sys.version_info[:2] <= (3, 2): + tests_require.append('ipaddress') + +extras_require = {} +if sys.version_info[:2] <= (3, 3): + extras_require.update(dict(enum='enum34')) + def get_version(): INIT = os.path.join(HERE, 'psutil/__init__.py') @@ -328,14 +340,8 @@ def main(): kwargs.update( python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", test_suite="psutil.tests.get_suite", - tests_require=[ - 'ipaddress; python_version < "3.3"', - 'mock; python_version < "3.3"', - 'unittest2; python_version < "2.7"', - ], - extras_require={ - 'enum': 'enum34; python_version < "3.4"', - }, + tests_require=tests_require, + extras_require=extras_require, zip_safe=False, ) setup(**kwargs) From 70b330221b87ff9c42d5160c57efea8fd3804398 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Apr 2018 11:17:08 +0200 Subject: [PATCH 0012/1714] fix doc --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index c8f7be8f5e..31b53384cf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -449,7 +449,7 @@ Disks Network ------- -.. function:: net_io_counters(pernic=False) +.. function:: net_io_counters(pernic=False, nowrap=True) Return system-wide network I/O statistics as a named tuple including the following attributes: From 489422b9cccf55100a19e96135d05f8e01d48daa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 Apr 2018 06:15:58 +0200 Subject: [PATCH 0013/1714] #1270: migrate URLs to new pypi site --- INSTALL.rst | 6 +- Makefile | 2 +- README.rst | 8 +- docs/index.rst | 140 ++++++++++++++--------------- scripts/internal/print_announce.py | 2 +- scripts/internal/print_timeline.py | 2 +- 6 files changed, 80 insertions(+), 80 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 9e5defb42b..4ceb0c19b9 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -76,15 +76,15 @@ Windows The easiest way to install psutil on Windows is to just use the pre-compiled exe/wheel installers hosted on -`PYPI `__ via pip: +`PYPI `__ via pip: .. code-block:: bat C:\Python27\python.exe -m pip install psutil If you want to compile psutil from sources you'll need **Visual Studio** -(Mingw32 is no longer supported), which really is a mess. -The VS versions are the onle listed below. +(Mingw32 is no longer supported), which really is a mess. +The VS versions are the onle listed below. This `blog post `__ provides numerous info on how to properly set up VS (good luck with that). diff --git a/Makefile b/Makefile index f7b4cd7f5b..5693bb9536 100644 --- a/Makefile +++ b/Makefile @@ -206,7 +206,7 @@ win-download-wheels: ## Download wheels hosted on appveyor. # --- upload -upload-src: ## Upload source tarball on https://pypi.python.org/pypi/psutil. +upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist $(PYTHON) setup.py sdist upload diff --git a/README.rst b/README.rst index 73205270cb..3f2a65203c 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi - :target: https://pypi.python.org/pypi/psutil/ + :target: https://pypi.org/project/psutil :alt: Latest version .. image:: https://img.shields.io/github/stars/giampaolo/psutil.svg @@ -23,7 +23,7 @@ :alt: Github stars .. image:: https://img.shields.io/pypi/l/psutil.svg - :target: https://pypi.python.org/pypi/psutil/ + :target: https://pypi.org/project/psutil :alt: License =========== @@ -33,7 +33,7 @@ Quick links - `Home page `_ - `Install `_ - `Documentation `_ -- `Download `_ +- `Download `_ - `Forum `_ - `StackOverflow `_ - `Blog `_ @@ -86,7 +86,7 @@ Projects using psutil At the time of writing psutil has roughly `2.9 milion downloads `__ per month and there are over -`7000 open source projects `__ +`8000 open source projects `__ on github which depend from psutil. Here's some I find particularly interesting: diff --git a/docs/index.rst b/docs/index.rst index 31b53384cf..866eaf3197 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,7 +12,7 @@ Quick links - `Install `_ - `Blog `__ - `Forum `__ -- `Download `__ +- `Download `__ - `Development guide `_ - `What's new `__ @@ -39,7 +39,7 @@ psutil currently supports the following platforms: ...both **32-bit** and **64-bit** architectures, with Python versions from **2.6 to 3.6** (users of Python 2.4 and 2.5 may use -`2.1.3 `__ version). +`2.1.3 `__ version). `PyPy `__ is also known to work. The psutil documentation you're reading is distributed as a single HTML page. @@ -53,7 +53,7 @@ The easiest way to install psutil is via ``pip``:: On UNIX this requires a C compiler (e.g. gcc) installed. On Windows pip will automatically retrieve a pre-compiled wheel version from -`PYPI repository `__. +`PYPI repository `__. Alternatively, see more detailed `install `_ instructions. @@ -627,7 +627,7 @@ Network .. note:: if you're interested in others families (e.g. AF_BLUETOOTH) you can use - the more powerful `netifaces `__ + the more powerful `netifaces `__ extension. .. note:: @@ -2564,7 +2564,7 @@ FAQs * A: From Windows **Vista** onwards, both 32 and 64 bit versions. Latest binary (wheel / exe) release which supports Windows **2000**, **XP** and **2003 server** is - `psutil 3.4.2 `__. + `psutil 3.4.2 `__. On such old systems psutil is no longer tested or maintained, but it can still be compiled from sources (you'll need `Visual Studio <(https://github.com/giampaolo/psutil/blob/master/INSTALL.rst#windows>`__) and it should "work" (more or less). @@ -2573,7 +2573,7 @@ FAQs * Q: What Python versions are supported? * A: From 2.6 to 3.6, both 32 and 64 bit versions. Last version supporting - Python 2.4 and 2.5 is `psutil 2.1.3 `__. + Python 2.4 and 2.5 is `psutil 2.1.3 `__. PyPy is also known to work. ---- @@ -2625,258 +2625,258 @@ Timeline ======== - 2018-04-14: - `5.4.5 `__ - + `5.4.5 `__ - `what's new `__ - `diff `__ - 2018-04-13: - `5.4.4 `__ - + `5.4.4 `__ - `what's new `__ - `diff `__ - 2018-01-01: - `5.4.3 `__ - + `5.4.3 `__ - `what's new `__ - `diff `__ - 2017-12-07: - `5.4.2 `__ - + `5.4.2 `__ - `what's new `__ - `diff `__ - 2017-11-08: - `5.4.1 `__ - + `5.4.1 `__ - `what's new `__ - `diff `__ - 2017-10-12: - `5.4.0 `__ - + `5.4.0 `__ - `what's new `__ - `diff `__ - 2017-09-10: - `5.3.1 `__ - + `5.3.1 `__ - `what's new `__ - `diff `__ - 2017-09-01: - `5.3.0 `__ - + `5.3.0 `__ - `what's new `__ - `diff `__ - 2017-04-10: - `5.2.2 `__ - + `5.2.2 `__ - `what's new `__ - `diff `__ - 2017-03-24: - `5.2.1 `__ - + `5.2.1 `__ - `what's new `__ - `diff `__ - 2017-03-05: - `5.2.0 `__ - + `5.2.0 `__ - `what's new `__ - `diff `__ - 2017-02-07: - `5.1.3 `__ - + `5.1.3 `__ - `what's new `__ - `diff `__ - 2017-02-03: - `5.1.2 `__ - + `5.1.2 `__ - `what's new `__ - `diff `__ - 2017-02-03: - `5.1.1 `__ - + `5.1.1 `__ - `what's new `__ - `diff `__ - 2017-02-01: - `5.1.0 `__ - + `5.1.0 `__ - `what's new `__ - `diff `__ - 2016-12-21: - `5.0.1 `__ - + `5.0.1 `__ - `what's new `__ - `diff `__ - 2016-11-06: - `5.0.0 `__ - + `5.0.0 `__ - `what's new `__ - `diff `__ - 2016-10-05: - `4.4.2 `__ - + `4.4.2 `__ - `what's new `__ - `diff `__ - 2016-10-25: - `4.4.1 `__ - + `4.4.1 `__ - `what's new `__ - `diff `__ - 2016-10-23: - `4.4.0 `__ - + `4.4.0 `__ - `what's new `__ - `diff `__ - 2016-09-01: - `4.3.1 `__ - + `4.3.1 `__ - `what's new `__ - `diff `__ - 2016-06-18: - `4.3.0 `__ - + `4.3.0 `__ - `what's new `__ - `diff `__ - 2016-05-14: - `4.2.0 `__ - + `4.2.0 `__ - `what's new `__ - `diff `__ - 2016-03-12: - `4.1.0 `__ - + `4.1.0 `__ - `what's new `__ - `diff `__ - 2016-02-17: - `4.0.0 `__ - + `4.0.0 `__ - `what's new `__ - `diff `__ - 2016-01-20: - `3.4.2 `__ - + `3.4.2 `__ - `what's new `__ - `diff `__ - 2016-01-15: - `3.4.1 `__ - + `3.4.1 `__ - `what's new `__ - `diff `__ - 2015-11-25: - `3.3.0 `__ - + `3.3.0 `__ - `what's new `__ - `diff `__ - 2015-10-04: - `3.2.2 `__ - + `3.2.2 `__ - `what's new `__ - `diff `__ - 2015-09-03: - `3.2.1 `__ - + `3.2.1 `__ - `what's new `__ - `diff `__ - 2015-09-02: - `3.2.0 `__ - + `3.2.0 `__ - `what's new `__ - `diff `__ - 2015-07-15: - `3.1.1 `__ - + `3.1.1 `__ - `what's new `__ - `diff `__ - 2015-07-15: - `3.1.0 `__ - + `3.1.0 `__ - `what's new `__ - `diff `__ - 2015-06-18: - `3.0.1 `__ - + `3.0.1 `__ - `what's new `__ - `diff `__ - 2015-06-13: - `3.0.0 `__ - + `3.0.0 `__ - `what's new `__ - `diff `__ - 2015-02-02: - `2.2.1 `__ - + `2.2.1 `__ - `what's new `__ - `diff `__ - 2015-01-06: - `2.2.0 `__ - + `2.2.0 `__ - `what's new `__ - `diff `__ - 2014-09-26: - `2.1.3 `__ - + `2.1.3 `__ - `what's new `__ - `diff `__ - 2014-09-21: - `2.1.2 `__ - + `2.1.2 `__ - `what's new `__ - `diff `__ - 2014-04-30: - `2.1.1 `__ - + `2.1.1 `__ - `what's new `__ - `diff `__ - 2014-04-08: - `2.1.0 `__ - + `2.1.0 `__ - `what's new `__ - `diff `__ - 2014-03-10: - `2.0.0 `__ - + `2.0.0 `__ - `what's new `__ - `diff `__ - 2013-11-25: - `1.2.1 `__ - + `1.2.1 `__ - `what's new `__ - `diff `__ - 2013-11-20: - `1.2.0 `__ - + `1.2.0 `__ - `what's new `__ - `diff `__ - 2013-10-22: - `1.1.2 `__ - + `1.1.2 `__ - `what's new `__ - `diff `__ - 2013-10-08: - `1.1.1 `__ - + `1.1.1 `__ - `what's new `__ - `diff `__ - 2013-09-28: - `1.1.0 `__ - + `1.1.0 `__ - `what's new `__ - `diff `__ - 2013-07-12: - `1.0.1 `__ - + `1.0.1 `__ - `what's new `__ - `diff `__ - 2013-07-10: - `1.0.0 `__ - + `1.0.0 `__ - `what's new `__ - `diff `__ - 2013-05-03: - `0.7.1 `__ - + `0.7.1 `__ - `what's new `__ - `diff `__ - 2013-04-12: - `0.7.0 `__ - + `0.7.0 `__ - `what's new `__ - `diff `__ - 2012-08-16: - `0.6.1 `__ - + `0.6.1 `__ - `what's new `__ - `diff `__ - 2012-08-13: - `0.6.0 `__ - + `0.6.0 `__ - `what's new `__ - `diff `__ - 2012-06-29: - `0.5.1 `__ - + `0.5.1 `__ - `what's new `__ - `diff `__ - 2012-06-27: - `0.5.0 `__ - + `0.5.0 `__ - `what's new `__ - `diff `__ - 2011-12-14: - `0.4.1 `__ - + `0.4.1 `__ - `what's new `__ - `diff `__ - 2011-10-29: - `0.4.0 `__ - + `0.4.0 `__ - `what's new `__ - `diff `__ - 2011-07-08: - `0.3.0 `__ - + `0.3.0 `__ - `what's new `__ - `diff `__ - 2011-03-20: - `0.2.1 `__ - + `0.2.1 `__ - `what's new `__ - `diff `__ - 2010-11-13: - `0.2.0 `__ - + `0.2.0 `__ - `what's new `__ - `diff `__ - 2010-03-02: - `0.1.3 `__ - + `0.1.3 `__ - `what's new `__ - `diff `__ - 2009-05-06: - `0.1.2 `__ - + `0.1.2 `__ - `what's new `__ - `diff `__ - 2009-03-06: - `0.1.1 `__ - + `0.1.1 `__ - `what's new `__ - `diff `__ - 2009-01-27: - `0.1.0 `__ - + `0.1.0 `__ - `what's new `__ - `diff `__ diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 1c2b9e1133..018fb0928b 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -20,7 +20,7 @@ PRJ_NAME = 'psutil' PRJ_URL_HOME = 'https://github.com/giampaolo/psutil' PRJ_URL_DOC = 'http://psutil.readthedocs.io' -PRJ_URL_DOWNLOAD = 'https://pypi.python.org/pypi/psutil' +PRJ_URL_DOWNLOAD = 'https://pypi.org/project/psutil/#files' PRJ_URL_WHATSNEW = \ 'https://github.com/giampaolo/psutil/blob/master/HISTORY.rst' diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index ffcb8fe85f..4bfe76b332 100644 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -13,7 +13,7 @@ entry = """\ - {date}: - `{ver} `__ - + `{ver} `__ - `what's new `__ - `diff `__""" # NOQA From abbe589de7e6109849924bf6172bd40132fdfc5c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 4 May 2018 23:00:33 +0200 Subject: [PATCH 0014/1714] #1273 net_if_addr() namedtuple name has been renamed from "snic" to "snicaddr". --- HISTORY.rst | 10 ++++++++++ Makefile | 2 +- README.rst | 12 ++++++------ docs/index.rst | 12 ++++++------ psutil/__init__.py | 2 +- psutil/_common.py | 5 +++-- psutil/tests/__init__.py | 2 +- scripts/internal/{purge.py => purge_installation.py} | 0 8 files changed, 28 insertions(+), 17 deletions(-) rename scripts/internal/{purge.py => purge_installation.py} (100%) diff --git a/HISTORY.rst b/HISTORY.rst index 685a8718e2..8f49025b78 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.4.6 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 1273_: net_if_addr() namedtuple's name has been renamed from "snic" to + "snicaddr". + 5.4.5 ===== diff --git a/Makefile b/Makefile index 5693bb9536..51047383fa 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ install: ## Install this package as current user in "edit" mode. uninstall: ## Uninstall this package via pip. cd ..; $(PYTHON) -m pip uninstall -y -v psutil || true - $(PYTHON) scripts/internal/purge.py + $(PYTHON) scripts/internal/purge_installation.py install-pip: ## Install pip (no-op if already installed). $(PYTHON) -c \ diff --git a/README.rst b/README.rst index 3f2a65203c..0852b1ce51 100644 --- a/README.rst +++ b/README.rst @@ -201,12 +201,12 @@ Network ...] >>> >>> psutil.net_if_addrs() - {'lo': [snic(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), - snic(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), - snic(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], - 'wlan0': [snic(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), - snic(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), - snic(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} + {'lo': [snicaddr(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), + snicaddr(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), + snicaddr(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], + 'wlan0': [snicaddr(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), + snicaddr(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), + snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> >>> psutil.net_if_stats() {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500), diff --git a/docs/index.rst b/docs/index.rst index 866eaf3197..6680a56201 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -613,12 +613,12 @@ Network >>> import psutil >>> psutil.net_if_addrs() - {'lo': [snic(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), - snic(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), - snic(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], - 'wlan0': [snic(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), - snic(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), - snic(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} + {'lo': [snicaddr(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), + snicaddr(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), + snicaddr(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], + 'wlan0': [snicaddr(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), + snicaddr(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), + snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> See also `nettop.py `__ diff --git a/psutil/__init__.py b/psutil/__init__.py index 90e8b15e50..2ecd3bddf3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2155,7 +2155,7 @@ def net_if_addrs(): separator = ":" if POSIX else "-" while addr.count(separator) < 5: addr += "%s00" % separator - ret[name].append(_common.snic(fam, addr, mask, broadcast, ptp)) + ret[name].append(_common.snicaddr(fam, addr, mask, broadcast, ptp)) return dict(ret) diff --git a/psutil/_common.py b/psutil/_common.py index 05dbb4ce65..e65d39e3b4 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -59,7 +59,7 @@ # named tuples 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile', 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart', - 'sdiskusage', 'snetio', 'snic', 'snicstats', 'sswap', 'suser', + 'sdiskusage', 'snetio', 'snicaddr', 'snicstats', 'sswap', 'suser', # utility functions 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize', 'parse_environ_block', 'path_exists_strict', 'usage_percent', @@ -182,7 +182,8 @@ class BatteryTime(enum.IntEnum): sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr', 'status', 'pid']) # psutil.net_if_addrs() -snic = namedtuple('snic', ['family', 'address', 'netmask', 'broadcast', 'ptp']) +snicaddr = namedtuple('snicaddr', + ['family', 'address', 'netmask', 'broadcast', 'ptp']) # psutil.net_if_stats() snicstats = namedtuple('snicstats', ['isup', 'duplex', 'speed', 'mtu']) # psutil.cpu_stats() diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 80404a3325..f11a6a028e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -125,7 +125,7 @@ # 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 +GLOBAL_TIMEOUT = 3 if TRAVIS or APPVEYOR else 0.5 # test output verbosity VERBOSITY = 1 if os.getenv('SILENT') or TOX else 2 # be more tolerant if we're on travis / appveyor in order to avoid diff --git a/scripts/internal/purge.py b/scripts/internal/purge_installation.py similarity index 100% rename from scripts/internal/purge.py rename to scripts/internal/purge_installation.py From 757f16b744a8b8d8ae442cf69732feff02e83ea8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 4 May 2018 23:03:10 +0200 Subject: [PATCH 0015/1714] remove outdated linux test --- psutil/tests/test_linux.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d4eaf2a146..9ea59b617b 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1408,20 +1408,6 @@ def test_emulate_energy_full_not_avail(self): "/sys/class/power_supply/BAT0/capacity", b"88"): self.assertEqual(psutil.sensors_battery().percent, 88) - def test_emulate_no_ac0_online(self): - # Emulate a case where /AC0/online file does not exist. - def path_exists_mock(name): - if name.startswith("/sys/class/power_supply/AC0/online"): - return False - else: - return orig_path_exists(name) - - orig_path_exists = os.path.exists - with mock.patch("psutil._pslinux.os.path.exists", - side_effect=path_exists_mock) as m: - psutil.sensors_battery() - assert m.called - def test_emulate_no_power(self): # Emulate a case where /AC0/online file nor /BAT0/status exist. with mock_open_exception( From 6ad33123e00383f9ea8a3e336783acec27c30448 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 4 May 2018 23:43:46 +0200 Subject: [PATCH 0016/1714] minor refactoring --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d8db694ebf..fffefd2f7f 100755 --- a/setup.py +++ b/setup.py @@ -43,8 +43,6 @@ macros = [] if POSIX: macros.append(("PSUTIL_POSIX", 1)) -if WINDOWS: - macros.append(("PSUTIL_WINDOWS", 1)) if BSD: macros.append(("PSUTIL_BSD", 1)) @@ -117,6 +115,7 @@ def get_winver(): msg += "Visual Studio and may also (kind of) work though" warnings.warn(msg, UserWarning) + macros.append(("PSUTIL_WINDOWS", 1)) macros.extend([ # be nice to mingw, see: # http://www.mingw.org/wiki/Use_more_recent_defined_functions @@ -252,6 +251,7 @@ def get_ethtool_macro(): 'psutil/arch/aix/ifaddrs.c'], libraries=['perfstat'], define_macros=macros) + else: sys.exit('platform %s is not supported' % sys.platform) From 1b5f8d55d22167d640eeb52d0b82f69dec83d210 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 8 May 2018 18:13:40 +0200 Subject: [PATCH 0017/1714] fix #1274 / Process.children / Linux: do not swallow AccessDenied --- HISTORY.rst | 2 ++ psutil/__init__.py | 10 +++------- psutil/_pslinux.py | 5 ++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8f49025b78..f69c5fb638 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 1273_: net_if_addr() namedtuple's name has been renamed from "snic" to "snicaddr". +- 1274_: [Linux] there was a small chance Process.children() may swallow + AccessDenied exceptions. 5.4.5 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 2ecd3bddf3..5e9a7fb6c0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -218,7 +218,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.4.5" +__version__ = "5.4.6" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED @@ -265,13 +265,9 @@ def _ppid_map(): ret = {} for pid in pids(): try: - proc = _psplatform.Process(pid) - ppid = proc.ppid() - except (NoSuchProcess, AccessDenied): - # Note: AccessDenied is unlikely to happen. + ret[pid] = _psplatform.Process(pid).ppid() + except (NoSuchProcess, ZombieProcess): pass - else: - ret[pid] = ppid return ret diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 78c03d5c5c..b197dba331 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1380,9 +1380,8 @@ def ppid_map(): data = f.read() except EnvironmentError as err: # Note: we should be able to access /stat for all processes - # so we won't bump into EPERM, which is good. - if err.errno not in (errno.ENOENT, errno.ESRCH, - errno.EPERM, errno.EACCES): + # aka it's unlikely we'll bump into EPERM, which is good. + if err.errno not in (errno.ENOENT, errno.ESRCH): raise else: rpar = data.rfind(b')') From f4c6b02461cc89a7f9a98a164d346b4dfb72bee0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 22 May 2018 17:48:38 +0200 Subject: [PATCH 0018/1714] fix freebsd compilation warning --- psutil/arch/freebsd/specific.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index cf0b7df246..177ac8a6bd 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -273,7 +273,6 @@ psutil_proc_exe(PyObject *self, PyObject *args) { int mib[4]; int ret; size_t size; - const char *encoding_errs; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; @@ -540,7 +539,6 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { struct kinfo_file *freep = NULL; struct kinfo_file *kif; struct kinfo_proc kipp; - const char *encoding_errs; PyObject *py_path = NULL; int i, cnt; From ac9dccab6b038835b5d612f92cf4804ec2662c2e Mon Sep 17 00:00:00 2001 From: Jean-Luc Migot Date: Thu, 7 Jun 2018 17:13:40 +0200 Subject: [PATCH 0019/1714] Fix Windows crash on proc_username(), happens when WinAPI calls fail, leading to "goto error" (#1289) This will prevent deallocation of random memory not initialized (by the way you should stop using "goto"...) --- psutil/_psutil_windows.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index ae2e538a4a..ba898f60b0 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1405,9 +1405,9 @@ psutil_proc_username(PyObject *self, PyObject *args) { ULONG nameSize; ULONG domainNameSize; SID_NAME_USE nameUse; - PyObject *py_username; - PyObject *py_domain; - PyObject *py_tuple; + PyObject *py_username = NULL; + PyObject *py_domain = NULL; + PyObject *py_tuple = NULL; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; From e05f07722a994bc5c96c9f11eeaeec244dfd3516 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Jun 2018 17:16:19 +0200 Subject: [PATCH 0020/1714] update HISTORY and CREDITS --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index 41061cdf78..f5f1aa8644 100644 --- a/CREDITS +++ b/CREDITS @@ -535,3 +535,7 @@ I: 1239 N: Denis Krienbühl W: https://github.com/href I: 1260 + +N: Jean-Luc Migot +W: https://github.com/jmigot-tehtris +I: 1258, 1289 diff --git a/HISTORY.rst b/HISTORY.rst index f69c5fb638..6da633ef5a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ XXXX-XX-XX **Bug fixes** +- 1258_: [Windows] Process.username() may cause a segfault (Python interpreter + crash). (patch by Jean-Luc Migot) - 1273_: net_if_addr() namedtuple's name has been renamed from "snic" to "snicaddr". - 1274_: [Linux] there was a small chance Process.children() may swallow From 776d61024f061c47fbff5c765a619c1fd5701f5a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Jun 2018 17:39:07 +0200 Subject: [PATCH 0021/1714] pre-release --- HISTORY.rst | 2 +- MANIFEST.in | 2 +- docs/index.rst | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6da633ef5a..b07a0df81a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.4.6 ===== -XXXX-XX-XX +2018-06-07 **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index 85d1f21ece..26d678fc33 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -109,7 +109,7 @@ include scripts/internal/download_exes.py include scripts/internal/generate_manifest.py include scripts/internal/print_announce.py include scripts/internal/print_timeline.py -include scripts/internal/purge.py +include scripts/internal/purge_installation.py include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py diff --git a/docs/index.rst b/docs/index.rst index 6680a56201..aaf004cee7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2624,6 +2624,10 @@ take a look at the Timeline ======== +- 2018-06-07: + `5.4.6 `__ - + `what's new `__ - + `diff `__ - 2018-04-14: `5.4.5 `__ - `what's new `__ - From c7bd2b6943402805bcaf3ac899c54278be0a75e8 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 10 Jun 2018 08:44:34 -0700 Subject: [PATCH 0022/1714] Update Python 2 docs URLs to Python 3 (#1293) The Python3 docs are more actively maintained and are the future of the Python project. --- docs/index.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index aaf004cee7..bde3d453c3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -993,11 +993,11 @@ Process class When accessing methods of this class always be prepared to catch :class:`NoSuchProcess`, :class:`ZombieProcess` and :class:`AccessDenied` exceptions. - `hash() `__ builtin can + `hash() `__ builtin can be used against instances of this class in order to identify a process univocally over time (the hash is determined by mixing process PID and creation time). As such it can also be used with - `set()s `__. + `set()s `__. .. note:: @@ -1761,7 +1761,7 @@ Process class ``'r+'`` and ``'a+'``. There's no distinction between files opened in bynary or text mode (``"b"`` or ``"t"``). - **flags** (*Linux*): the flags which were passed to the underlying - `os.open `__ C call + `os.open `__ C call when the file was opened (e.g. `os.O_RDONLY `__, `os.O_TRUNC `__, @@ -2600,7 +2600,7 @@ FAQs * Q: What about load average? * A: psutil does not expose any load average function as it's already available in python as - `os.getloadavg `__. + `os.getloadavg `__. Running tests ============= From 4b5745e94b7922df48e89e77d60218708d5baa1f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Jun 2018 15:56:14 +0200 Subject: [PATCH 0023/1714] import ssl only if necessary --- psutil/tests/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 62fe074287..36554a126b 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -13,7 +13,6 @@ import contextlib import optparse import os -import ssl import sys import tempfile try: @@ -38,6 +37,7 @@ 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)) From 792499afbfdbad4b84ec5bc4acf2ed4e85310624 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Jun 2018 15:33:20 -0700 Subject: [PATCH 0024/1714] OSX - wrapper around task_for_pid() (#1296) (OSX) wrapper around task_for_pid() fix #1181, fix #1209, fix #1291 --- HISTORY.rst | 10 +++++ psutil/_psosx.py | 8 ++-- psutil/_psutil_osx.c | 94 +++++++++++++++++++++++++++------------- psutil/tests/__main__.py | 2 +- 4 files changed, 79 insertions(+), 35 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b07a0df81a..5854fa9fdb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.4.7 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 1209_: [OSX] Process.memory_maps() may fail with EINVAL due to poor + task_for_pid() syscall. AccessDenied is now raised instead. + 5.4.6 ===== diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 193f63e002..38f4066e51 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -334,6 +334,8 @@ def wrapper(self, *args, **kwargs): 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 @@ -557,8 +559,7 @@ def status(self): @wrap_exceptions def threads(self): - with catch_zombie(self): - rawlist = cext.proc_threads(self.pid) + rawlist = cext.proc_threads(self.pid) retlist = [] for thread_id, utime, stime in rawlist: ntuple = _common.pthread(thread_id, utime, stime) @@ -567,5 +568,4 @@ def threads(self): @wrap_exceptions def memory_maps(self): - with catch_zombie(self): - return cext.proc_memory_maps(self.pid) + return cext.proc_memory_maps(self.pid) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 55dd64ca5d..19508723af 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -48,6 +48,8 @@ #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) +static PyObject *ZombieProcessError; + /* * A wrapper around host_statistics() invoked with HOST_VM_INFO. @@ -71,6 +73,59 @@ psutil_sys_vminfo(vm_statistics_data_t *vmstat) { } +/* + * Return 1 if pid refers to a zombie process else 0. + */ +int +psutil_is_zombie(long pid) +{ + struct kinfo_proc kp; + + if (psutil_get_kinfo_proc(pid, &kp) == -1) + return 0; + return (kp.kp_proc.p_stat == SZOMB) ? 1 : 0; +} + + +/* + * A wrapper around task_for_pid() which sucks big time: + * - it's not documented + * - errno is set only sometimes + * - sometimes errno is ENOENT (?!?) + * - for PIDs != getpid() or PIDs which are not members of the procmod + * it requires root + * As such we can only guess what the heck went wrong and fail either + * with NoSuchProcess, ZombieProcessError or giveup with AccessDenied. + * Here's some history: + * https://github.com/giampaolo/psutil/issues/1181 + * https://github.com/giampaolo/psutil/issues/1209 + * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 + */ +int +psutil_task_for_pid(long pid, mach_port_t *task) +{ + // See: https://github.com/giampaolo/psutil/issues/1181 + kern_return_t err = KERN_SUCCESS; + + err = task_for_pid(mach_task_self(), (pid_t)pid, task); + if (err != KERN_SUCCESS) { + if (psutil_pid_exists(pid) == 0) + NoSuchProcess("task_for_pid() failed"); + else if (psutil_is_zombie(pid) == 1) + PyErr_SetString(ZombieProcessError, "task_for_pid() failed"); + else { + psutil_debug( + "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " + "setting AccessDenied()", + pid, err, errno, mach_error_string(err)); + AccessDenied("task_for_pid() failed"); + } + return 1; + } + return 0; +} + + /* * Return a Python list of all the PIDs running on the system. */ @@ -336,20 +391,8 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) goto error; - err = task_for_pid(mach_task_self(), (pid_t)pid, &task); - if (err != KERN_SUCCESS) { - if ((err == 5) && (errno == ENOENT)) { - // See: https://github.com/giampaolo/psutil/issues/1181 - psutil_debug("task_for_pid(MACH_PORT_NULL) failed; err=%i, " - "errno=%i, msg='%s'\n", err, errno, - mach_error_string(err)); - AccessDenied(""); - } - else { - psutil_raise_for_pid(pid, "task_for_pid(MACH_PORT_NULL)"); - } + if (psutil_task_for_pid(pid, &task) != 0) goto error; - } while (1) { py_tuple = NULL; @@ -560,7 +603,6 @@ psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { static PyObject * psutil_proc_memory_uss(PyObject *self, PyObject *args) { long pid; - int err; size_t len; cpu_type_t cpu_type; size_t private_pages = 0; @@ -576,14 +618,8 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - err = task_for_pid(mach_task_self(), (pid_t)pid, &task); - if (err != KERN_SUCCESS) { - if (psutil_pid_exists(pid) == 0) - NoSuchProcess(""); - else - AccessDenied(""); + if (psutil_task_for_pid(pid, &task) != 0) return NULL; - } len = sizeof(cpu_type); if (sysctlbyname("sysctl.proc_cputype", &cpu_type, &len, NULL, 0) != 0) @@ -1018,19 +1054,11 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - // the argument passed should be a process id if (! PyArg_ParseTuple(args, "l", &pid)) goto error; - // task_for_pid() requires root privileges - err = task_for_pid(mach_task_self(), (pid_t)pid, &task); - if (err != KERN_SUCCESS) { - if (psutil_pid_exists(pid) == 0) - NoSuchProcess(""); - else - AccessDenied(""); + if (psutil_task_for_pid(pid, &task) != 0) goto error; - } info_count = TASK_BASIC_INFO_COUNT; err = task_info(task, TASK_BASIC_INFO, (task_info_t)&tasks_info, @@ -2036,6 +2064,12 @@ init_psutil_osx(void) PyModule_AddIntConstant(module, "TCPS_TIME_WAIT", TCPS_TIME_WAIT); PyModule_AddIntConstant(module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); + // Exception. + ZombieProcessError = PyErr_NewException( + "_psutil_osx.ZombieProcessError", NULL, NULL); + Py_INCREF(ZombieProcessError); + PyModule_AddObject(module, "ZombieProcessError", ZombieProcessError); + psutil_setup(); if (module == NULL) diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 62fe074287..36554a126b 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -13,7 +13,6 @@ import contextlib import optparse import os -import ssl import sys import tempfile try: @@ -38,6 +37,7 @@ 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)) From fdbc40224b3ce113fcc264a1d3180ab0b62cb736 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Jun 2018 00:54:21 +0200 Subject: [PATCH 0025/1714] OSX / pids(): append() pid 0 in pos 0 --- psutil/_psosx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 38f4066e51..7cbf40328b 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -309,11 +309,11 @@ def pids(): # https://travis-ci.org/giampaolo/psutil/jobs/309619941 try: Process(0).create_time() - ls.append(0) + ls.insert(0, 0) except NoSuchProcess: pass except AccessDenied: - ls.append(0) + ls.insert(0, 0) return ls From 3612b854956742790f58dcb07a6d50cc4f9b81b8 Mon Sep 17 00:00:00 2001 From: Nikhil Marathe Date: Fri, 15 Jun 2018 21:49:30 +0530 Subject: [PATCH 0026/1714] Add seconds to thread run times on MacOS. (#1292) Add seconds to thread run times on MacOS (fix #1278) --- psutil/_psutil_osx.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 19508723af..b8e3b85d25 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1097,8 +1097,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { py_tuple = Py_BuildValue( "Iff", j + 1, - (float)basic_info_th->user_time.microseconds / 1000000.0, - (float)basic_info_th->system_time.microseconds / 1000000.0 + basic_info_th->user_time.seconds + \ + (float)basic_info_th->user_time.microseconds / 1000000.0, + basic_info_th->system_time.seconds + \ + (float)basic_info_th->system_time.microseconds / 1000000.0 ); if (!py_tuple) goto error; From d20b9939b985d871177bee2c1271a04a1810a100 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Jun 2018 18:21:17 +0200 Subject: [PATCH 0027/1714] give CREDITs to @nikhilm for #1278 --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index f5f1aa8644..a797ff2f0c 100644 --- a/CREDITS +++ b/CREDITS @@ -539,3 +539,7 @@ I: 1260 N: Jean-Luc Migot W: https://github.com/jmigot-tehtris I: 1258, 1289 + +N: Nikhil Marathe +W: https://github.com/nikhilm +I: 1278 diff --git a/HISTORY.rst b/HISTORY.rst index 5854fa9fdb..ba2f1ed1a3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 1209_: [OSX] Process.memory_maps() may fail with EINVAL due to poor task_for_pid() syscall. AccessDenied is now raised instead. +- 1278_: [OSX] Process.threads() incorrectly return microseconds instead of + seconds. (patch by Nikhil Marathe) 5.4.6 ===== From 196802dd34b7134c1fff99ad4755d0bc0e04cc98 Mon Sep 17 00:00:00 2001 From: Alex Manuskin Date: Tue, 26 Jun 2018 16:19:54 +0300 Subject: [PATCH 0028/1714] Osx temps (#1284) OSX: add temperatures() and fans() --- psutil/__init__.py | 4 +- psutil/_common.py | 2 +- psutil/_psosx.py | 146 ++++++++++++++++++++ psutil/_psutil_osx.c | 68 ++++++++++ psutil/arch/osx/smc.c | 236 +++++++++++++++++++++++++++++++++ psutil/arch/osx/smc.h | 79 +++++++++++ psutil/tests/test_contracts.py | 4 +- setup.py | 1 + 8 files changed, 535 insertions(+), 5 deletions(-) create mode 100644 psutil/arch/osx/smc.c create mode 100644 psutil/arch/osx/smc.h diff --git a/psutil/__init__.py b/psutil/__init__.py index 5e9a7fb6c0..f385927922 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2175,7 +2175,7 @@ def net_if_stats(): # ===================================================================== -# Linux +# Linux, OSX if hasattr(_psplatform, "sensors_temperatures"): def sensors_temperatures(fahrenheit=False): @@ -2213,7 +2213,7 @@ def convert(n): __all__.append("sensors_temperatures") -# Linux +# Linux, OSX if hasattr(_psplatform, "sensors_fans"): def sensors_fans(): diff --git a/psutil/_common.py b/psutil/_common.py index e65d39e3b4..b721393964 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -196,7 +196,7 @@ class BatteryTime(enum.IntEnum): 'shwtemp', ['label', 'current', 'high', 'critical']) # psutil.sensors_battery() sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged']) -# psutil.sensors_battery() +# psutil.sensors_fans() sfan = namedtuple('sfan', ['label', 'current']) # --- for Process methods diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 7cbf40328b..90c8be438e 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -6,6 +6,7 @@ import contextlib import errno +import collections import functools import os from socket import AF_INET @@ -62,6 +63,110 @@ cext.SZOMB: _common.STATUS_ZOMBIE, } +temperatures = ( + # group, key, label + + # --- CPU + ("CPU", "TCXC", "PECI CPU"), + ("CPU", "TCXc", "PECI CPU"), + ("CPU", "TC0P", "CPU 1 Proximity"), + ("CPU", "TC0H", "CPU 1 Heatsink"), + ("CPU", "TC0D", "CPU 1 Package"), + ("CPU", "TC0E", "CPU 1"), + ("CPU", "TC1C", "CPU Core 1"), + ("CPU", "TC2C", "CPU Core 2"), + ("CPU", "TC3C", "CPU Core 3"), + ("CPU", "TC4C", "CPU Core 4"), + ("CPU", "TC5C", "CPU Core 5"), + ("CPU", "TC6C", "CPU Core 6"), + ("CPU", "TC7C", "CPU Core 7"), + ("CPU", "TC8C", "CPU Core 8"), + ("CPU", "TCAH", "CPU 1 Heatsink Alt."), + ("CPU", "TCAD", "CPU 1 Package Alt."), + ("CPU", "TC1P", "CPU 2 Proximity"), + ("CPU", "TC1H", "CPU 2 Heatsink"), + ("CPU", "TC1D", "CPU 2 Package"), + ("CPU", "TC1E", "CPU 2"), + ("CPU", "TCBH", "CPU 2 Heatsink Alt."), + ("CPU", "TCBD", "CPU 2 Package Alt."), + + ("CPU", "TCSC", "PECI SA"), + ("CPU", "TCSc", "PECI SA"), + ("CPU", "TCSA", "PECI SA"), + + # --- GPU + ("GPU", "TCGC", "PECI GPU"), + ("GPU", "TCGc", "PECI GPU"), + ("GPU", "TG0P", "GPU Proximity"), + ("GPU", "TG0D", "GPU Die"), + ("GPU", "TG1D", "GPU Die"), + ("GPU", "TG0H", "GPU Heatsink"), + ("GPU", "TG1H", "GPU Heatsink"), + + # --- Memory + ("Memory", "Ts0S", "Memory Proximity"), + ("Memory", "TM0P", "Mem Bank A1"), + ("Memory", "TM1P", "Mem Bank A2"), + ("Memory", "TM8P", "Mem Bank B1"), + ("Memory", "TM9P", "Mem Bank B2"), + ("Memory", "TM0S", "Mem Module A1"), + ("Memory", "TM1S", "Mem Module A2"), + ("Memory", "TM8S", "Mem Module B1"), + ("Memory", "TM9S", "Mem Module B2"), + + # --- HDD + ("HDD", "TH0P", "HDD Bay 1"), + ("HDD", "TH1P", "HDD Bay 2"), + ("HDD", "TH2P", "HDD Bay 3"), + ("HDD", "TH3P", "HDD Bay 4"), + + # --- Battery + ("Battery", "TB0T", "Battery TS_MAX"), + ("Battery", "TB1T", "Battery 1"), + ("Battery", "TB2T", "Battery 2"), + ("Battery", "TB3T", "Battery"), + + # --- Others + ("Others", "TN0D", "Northbridge Die"), + ("Others", "TN0P", "Northbridge Proximity 1"), + ("Others", "TN1P", "Northbridge Proximity 2"), + ("Others", "TN0C", "MCH Die"), + ("Others", "TN0H", "MCH Heatsink"), + ("Others", "TP0D", "PCH Die"), + ("Others", "TPCD", "PCH Die"), + ("Others", "TP0P", "PCH Proximity"), + + ("Others", "TA0P", "Airflow 1"), + ("Others", "TA1P", "Airflow 2"), + ("Others", "Th0H", "Heatpipe 1"), + ("Others", "Th1H", "Heatpipe 2"), + ("Others", "Th2H", "Heatpipe 3"), + + ("Others", "Tm0P", "Mainboard Proximity"), + ("Others", "Tp0P", "Powerboard Proximity"), + ("Others", "Ts0P", "Palm Rest"), + ("Others", "Tb0P", "BLC Proximity"), + + ("Others", "TL0P", "LCD Proximity"), + ("Others", "TW0P", "Airport Proximity"), + ("Others", "TO0P", "Optical Drive"), + + ("Others", "Tp0P", "Power Supply 1"), + ("Others", "Tp0C", "Power Supply 1 Alt."), + ("Others", "Tp1P", "Power Supply 2"), + ("Others", "Tp1C", "Power Supply 2 Alt."), + ("Others", "Tp2P", "Power Supply 3"), + ("Others", "Tp3P", "Power Supply 4"), + ("Others", "Tp4P", "Power Supply 5"), + ("Others", "Tp5P", "Power Supply 6"), + + ("Others", "TS0C", "Expansion Slots"), + ("Others", "TA0S", "PCI Slot 1 Pos 1"), + ("Others", "TA1S", "PCI Slot 1 Pos 2"), + ("Others", "TA2S", "PCI Slot 2 Pos 1"), + ("Others", "TA3S", "PCI Slot 2 Pos 2"), +) + kinfo_proc_map = dict( ppid=0, ruid=1, @@ -212,6 +317,35 @@ def disk_partitions(all=False): # ===================================================================== +def sensors_temperatures(): + """Returns a dictionary of regions of temperature sensors: + CPU/GPU/Memory/Others + Each entry contains a list of results of all the successfully polled + SMC keys from the system. + + References for SMC keys and meaning: + + https://stackoverflow.com/questions/28568775/ + description-for-apples-smc-keys/31033665#31033665 + + https://github.com/Chris911/iStats/blob/ + 09b159f85a9481b59f347a37259f6d272f65cc05/lib/iStats/smc.rb + + http://web.archive.org/web/20140714090133/http://www.parhelia.ch:80/ + blog/statics/k3_keys.html + """ + ret = collections.defaultdict(list) + + for group, key, label in temperatures: + result = cext.smc_get_temperature(key) + result = round(result, 1) + if result <= 0: + continue + ret[group].append((label, result, None, None)) + + return dict(ret) + + def sensors_battery(): """Return battery information. """ @@ -230,6 +364,18 @@ def sensors_battery(): return _common.sbattery(percent, secsleft, power_plugged) +def sensors_fans(): + """Return fans speed information. + """ + ret = collections.defaultdict(list) + rawlist = cext.sensors_fans() + if rawlist is not None: + for fan in rawlist: + ret["Fans"].append(_common.sfan(fan[0], fan[1])) + + return dict(ret) + + # ===================================================================== # --- network # ===================================================================== diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index b8e3b85d25..584df3b836 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -44,6 +44,7 @@ #include "_psutil_common.h" #include "_psutil_posix.h" #include "arch/osx/process_info.h" +#include "arch/osx/smc.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -896,6 +897,68 @@ psutil_boot_time(PyObject *self, PyObject *args) { return Py_BuildValue("f", (float)boot_time); } +/* + * Return a Python float indicating the value of the temperature + * measured by an SMC key + */ +static PyObject * +psutil_smc_get_temperature(PyObject *self, PyObject *args) { + char* key; + float temp; + + if (! PyArg_ParseTuple(args, "s", &key)) { + return NULL; + } + temp = SMCGetTemperature(key); + return Py_BuildValue("d", temp); +} + + +/* + * Return a Python list of tuples of fan label and speed + */ +static PyObject * +psutil_sensors_fans(PyObject *self, PyObject *args) { + int key; + int speed; + char fan[7]; + int fan_count; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + fan_count = SMCGetFanNumber(SMC_KEY_FAN_NUM); + if (fan_count < 0) { + fan_count = 0; + } + for (key =0; key < fan_count; key++) { + sprintf(fan, "Fan %d", key); + speed = SMCGetFanSpeed(key); + if (speed < 0) { + continue; + } + py_tuple = Py_BuildValue( + "(si)", + fan, // label + speed // value + ); + if (!py_tuple) + goto error; + + if (PyList_Append(py_retlist, py_tuple)) { + goto error; + } + Py_XDECREF(py_tuple); + } + + return py_retlist; +error: + Py_XDECREF(py_tuple); + Py_XDECREF(py_retlist); + return NULL; +} /* * Return a list of tuples including device, mount point and fs type @@ -1828,6 +1891,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } + /* * Return battery information. */ @@ -1980,6 +2044,10 @@ PsutilMethods[] = { "Return currently connected users as a list of tuples"}, {"cpu_stats", psutil_cpu_stats, METH_VARARGS, "Return CPU statistics"}, + {"smc_get_temperature", psutil_smc_get_temperature, METH_VARARGS, + "Temperature of SMC key as float"}, + {"sensors_fans", psutil_sensors_fans, METH_VARARGS, + "Return the RPM of the fan with SMC key"}, {"sensors_battery", psutil_sensors_battery, METH_VARARGS, "Return battery information."}, diff --git a/psutil/arch/osx/smc.c b/psutil/arch/osx/smc.c new file mode 100644 index 0000000000..fb5668a1e3 --- /dev/null +++ b/psutil/arch/osx/smc.c @@ -0,0 +1,236 @@ +/* + * 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. + * + * Interface to SMC API, needed in order to collect sensors stats. + */ + +#include +#include +#include + +#include "smc.h" + +UInt32 _strtoul(char *str, int size, int base) +{ + UInt32 total = 0; + int i; + + for (i = 0; i < size; i++) { + if (base == 16) + total += str[i] << (size - 1 - i) * 8; + else + total += (unsigned char) (str[i] << (size - 1 - i) * 8); + } + return total; +} + + +float _strtof(unsigned char *str, int size, int e) +{ + float total = 0; + int i; + + for (i = 0; i < size; i++) { + if (i == (size - 1)) + total += (str[i] & 0xff) >> e; + else + total += str[i] << (size - 1 - i) * (8 - e); + } + + total += (str[size-1] & 0x03) * 0.25; + + return total; +} + + +void _ultostr(char *str, UInt32 val) +{ + str[0] = '\0'; + sprintf(str, "%c%c%c%c", + (unsigned int) val >> 24, + (unsigned int) val >> 16, + (unsigned int) val >> 8, + (unsigned int) val); +} + + +kern_return_t SMCOpen(io_connect_t * pconn) +{ + kern_return_t result; + io_iterator_t iterator; + io_object_t device; + + CFMutableDictionaryRef matchingDictionary = IOServiceMatching("AppleSMC"); + result = IOServiceGetMatchingServices( + kIOMasterPortDefault, + matchingDictionary, + &iterator + ); + if (result != kIOReturnSuccess) { + return 1; + } + + device = IOIteratorNext(iterator); + IOObjectRelease(iterator); + if (device == 0) { + return 1; + } + + result = IOServiceOpen(device, mach_task_self(), 0, pconn); + IOObjectRelease(device); + if (result != kIOReturnSuccess) { + return 1; + } + + return kIOReturnSuccess; +} + + +kern_return_t SMCClose(io_connect_t conn) +{ + return IOServiceClose(conn); +} + + +kern_return_t SMCCall(io_connect_t conn, + int index, SMCKeyData_t *inputStructure, SMCKeyData_t *outputStructure) +{ + size_t structureInputSize; + size_t structureOutputSize; + + structureInputSize = sizeof(SMCKeyData_t); + structureOutputSize = sizeof(SMCKeyData_t); + +#if MAC_OS_X_VERSION_10_5 + return IOConnectCallStructMethod( + conn, index, + inputStructure, + structureInputSize, + outputStructure, + &structureOutputSize); +#else + return IOConnectMethodStructureIStructureO( + conn, index, + structureInputSize, + &structureOutputSize, + inputStructure, + outputStructure); +#endif +} + + +kern_return_t SMCReadKey(io_connect_t conn, UInt32Char_t key, SMCVal_t *val) { + kern_return_t result; + SMCKeyData_t inputStructure; + SMCKeyData_t outputStructure; + + memset(&inputStructure, 0, sizeof(SMCKeyData_t)); + memset(&outputStructure, 0, sizeof(SMCKeyData_t)); + memset(val, 0, sizeof(SMCVal_t)); + + inputStructure.key = _strtoul(key, 4, 16); + inputStructure.data8 = SMC_CMD_READ_KEYINFO; + + result = SMCCall( + conn, + KERNEL_INDEX_SMC, + &inputStructure, + &outputStructure + ); + if (result != kIOReturnSuccess) + return result; + + val->dataSize = outputStructure.keyInfo.dataSize; + _ultostr(val->dataType, outputStructure.keyInfo.dataType); + inputStructure.keyInfo.dataSize = val->dataSize; + inputStructure.data8 = SMC_CMD_READ_BYTES; + + result = SMCCall( + conn, + KERNEL_INDEX_SMC, + &inputStructure, + &outputStructure + ); + if (result != kIOReturnSuccess) + return result; + + memcpy(val->bytes, outputStructure.bytes, sizeof(outputStructure.bytes)); + + return kIOReturnSuccess; +} + + +double SMCGetTemperature(char *key) { + io_connect_t conn; + SMCVal_t val; + kern_return_t result; + int intValue; + + result = SMCOpen(&conn); + if (result != kIOReturnSuccess) { + return 0.0; + } + + result = SMCReadKey(conn, key, &val); + if (result == kIOReturnSuccess) { + // read succeeded - check returned value + if (val.dataSize > 0) { + if (strcmp(val.dataType, DATATYPE_SP78) == 0) { + // convert sp78 value to temperature + intValue = val.bytes[0] * 256 + (unsigned char)val.bytes[1]; + SMCClose(conn); + return intValue / 256.0; + } + } + } + // read failed + SMCClose(conn); + return 0.0; +} + + +float SMCGetFanSpeed(int fanNum) +{ + SMCVal_t val; + kern_return_t result; + UInt32Char_t key; + io_connect_t conn; + + result = SMCOpen(&conn); + if (result != kIOReturnSuccess) { + return -1; + } + + sprintf(key, SMC_KEY_FAN_SPEED, fanNum); + result = SMCReadKey(conn, key, &val); + if (result != kIOReturnSuccess) { + SMCClose(conn); + return -1; + } + SMCClose(conn); + return _strtof((unsigned char *)val.bytes, val.dataSize, 2); +} + + + +int SMCGetFanNumber(char *key) +{ + SMCVal_t val; + kern_return_t result; + io_connect_t conn; + + result = SMCOpen(&conn); + if (result != kIOReturnSuccess) { + return 0; + } + + result = SMCReadKey(conn, key, &val); + if (result != kIOReturnSuccess) { + SMCClose(conn); + return 0; + } + SMCClose(conn); + return _strtoul((char *)val.bytes, val.dataSize, 10); +} diff --git a/psutil/arch/osx/smc.h b/psutil/arch/osx/smc.h new file mode 100644 index 0000000000..acd399c2ea --- /dev/null +++ b/psutil/arch/osx/smc.h @@ -0,0 +1,79 @@ +/* + * 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. + */ + +#ifndef __SMC_H__ +#define __SMC_H__ + +#define KERNEL_INDEX_SMC 2 + +#define SMC_CMD_READ_BYTES 5 +#define SMC_CMD_WRITE_BYTES 6 +#define SMC_CMD_READ_INDEX 8 +#define SMC_CMD_READ_KEYINFO 9 +#define SMC_CMD_READ_PLIMIT 11 +#define SMC_CMD_READ_VERS 12 + + +#define DATATYPE_FPE2 "fpe2" +#define DATATYPE_UINT8 "ui8 " +#define DATATYPE_UINT16 "ui16" +#define DATATYPE_UINT32 "ui32" +#define DATATYPE_SP78 "sp78" + +// Fans SMC key values +#define SMC_KEY_FAN_SPEED "F%dAc" +#define SMC_KEY_FAN_NUM "FNum" + +typedef struct { + char major; + char minor; + char build; + char reserved[1]; + UInt16 release; +} SMCKeyData_vers_t; + +typedef struct { + UInt16 version; + UInt16 length; + UInt32 cpuPLimit; + UInt32 gpuPLimit; + UInt32 memPLimit; +} SMCKeyData_pLimitData_t; + +typedef struct { + UInt32 dataSize; + UInt32 dataType; + char dataAttributes; +} SMCKeyData_keyInfo_t; + +typedef char SMCBytes_t[32]; + +typedef struct { + UInt32 key; + SMCKeyData_vers_t vers; + SMCKeyData_pLimitData_t pLimitData; + SMCKeyData_keyInfo_t keyInfo; + char result; + char status; + char data8; + UInt32 data32; + SMCBytes_t bytes; +} SMCKeyData_t; + +typedef char UInt32Char_t[5]; + +typedef struct { + UInt32Char_t key; + UInt32 dataSize; + UInt32Char_t dataType; + SMCBytes_t bytes; +} SMCVal_t; + +double SMCGetTemperature(char *key); +int SMCGetFanNumber(char *key); +float SMCGetFanSpeed(int fanNum); + +#endif diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 0dfb3e2a82..912e811f3b 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -116,10 +116,10 @@ def test_cpu_freq(self): self.assertEqual(hasattr(psutil, "cpu_freq"), linux or OSX or WINDOWS) def test_sensors_temperatures(self): - self.assertEqual(hasattr(psutil, "sensors_temperatures"), LINUX) + self.assertEqual(hasattr(psutil, "sensors_temperatures"), LINUX or OSX) def test_sensors_fans(self): - self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX) + self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX or OSX) def test_battery(self): self.assertEqual(hasattr(psutil, "sensors_battery"), diff --git a/setup.py b/setup.py index fffefd2f7f..b0343212b5 100755 --- a/setup.py +++ b/setup.py @@ -152,6 +152,7 @@ def get_winver(): sources=sources + [ 'psutil/_psutil_osx.c', 'psutil/arch/osx/process_info.c', + 'psutil/arch/osx/smc.c', ], define_macros=macros, extra_link_args=[ From 00c12b954749ce6d771f69ddf29cf4cb87f290ca Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Jun 2018 15:20:38 +0200 Subject: [PATCH 0029/1714] little refactoring --- psutil/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index e65d39e3b4..fdc94b1535 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -80,7 +80,7 @@ OPENBSD = sys.platform.startswith("openbsd") NETBSD = sys.platform.startswith("netbsd") BSD = FREEBSD or OPENBSD or NETBSD -SUNOS = sys.platform.startswith("sunos") or sys.platform.startswith("solaris") +SUNOS = sys.platform.startswith(("sunos", "solaris")) AIX = sys.platform.startswith("aix") From 93b1da90ee48630d57102faa989941ce46636644 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Jun 2018 15:35:46 +0200 Subject: [PATCH 0030/1714] #1284: give credits to @amanusk + some minor adjustments --- CREDITS | 4 ++++ HISTORY.rst | 7 ++++++- docs/index.rst | 8 ++++++-- psutil/_psosx.py | 34 ++++++++++++++-------------------- psutil/_psutil_osx.c | 22 ++++++++-------------- psutil/arch/osx/smc.c | 6 ++++-- 6 files changed, 42 insertions(+), 39 deletions(-) diff --git a/CREDITS b/CREDITS index a797ff2f0c..fd4f53d644 100644 --- a/CREDITS +++ b/CREDITS @@ -543,3 +543,7 @@ I: 1258, 1289 N: Nikhil Marathe W: https://github.com/nikhilm I: 1278 + +N: Alex Manuskin +W: https://github.com/amanusk +I: 1284 diff --git a/HISTORY.rst b/HISTORY.rst index ba2f1ed1a3..3ba7131451 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,10 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.4.7 +5.5.0 ===== XXXX-XX-XX +**Enhancements** + +- 1284_: [OSX] added support for sensors_temperatures() and sensors_fans(). + (patch by Alex Manuskin) + **Bug fixes** - 1209_: [OSX] Process.memory_maps() may fail with EINVAL due to poor diff --git a/docs/index.rst b/docs/index.rst index bde3d453c3..208f59e9c7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -698,10 +698,12 @@ Sensors See also `temperatures.py `__ and `sensors.py `__ for an example application. - Availability: Linux + Availability: Linux, OSX .. versionadded:: 5.1.0 + .. versionchanged:: 5.5.0: added OSX support + .. warning:: this API is experimental. Backward incompatible changes may occur if @@ -722,10 +724,12 @@ Sensors See also `fans.py `__ and `sensors.py `__ for an example application. - Availability: Linux + Availability: Linux, OSX .. versionadded:: 5.2.0 + .. versionchanged:: 5.5.0: added OSX support + .. warning:: this API is experimental. Backward incompatible changes may occur if diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 90c8be438e..a0101df070 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -4,9 +4,9 @@ """OSX platform implementation.""" +import collections import contextlib import errno -import collections import functools import os from socket import AF_INET @@ -63,9 +63,7 @@ cext.SZOMB: _common.STATUS_ZOMBIE, } -temperatures = ( - # group, key, label - +SMC_TEMPERATURES = ( # --- CPU ("CPU", "TCXC", "PECI CPU"), ("CPU", "TCXc", "PECI CPU"), @@ -319,36 +317,34 @@ def disk_partitions(all=False): def sensors_temperatures(): """Returns a dictionary of regions of temperature sensors: - CPU/GPU/Memory/Others - Each entry contains a list of results of all the successfully polled - SMC keys from the system. - + CPU/GPU/Memory/HDD/Battery/Others. + Each entry contains a list of results of all the SMC keys successfully + polled from the system. References for SMC keys and meaning: - https://stackoverflow.com/questions/28568775/ + * https://stackoverflow.com/questions/28568775/ description-for-apples-smc-keys/31033665#31033665 - https://github.com/Chris911/iStats/blob/ + * https://github.com/Chris911/iStats/blob/ 09b159f85a9481b59f347a37259f6d272f65cc05/lib/iStats/smc.rb - http://web.archive.org/web/20140714090133/http://www.parhelia.ch:80/ + * http://web.archive.org/web/20140714090133/http://www.parhelia.ch:80/ blog/statics/k3_keys.html """ + # TODO: this should be rewritten in C: + # https://github.com/giampaolo/psutil/pull/1284#issuecomment-399480983 ret = collections.defaultdict(list) - - for group, key, label in temperatures: + for group, key, label in SMC_TEMPERATURES: result = cext.smc_get_temperature(key) result = round(result, 1) if result <= 0: continue ret[group].append((label, result, None, None)) - return dict(ret) def sensors_battery(): - """Return battery information. - """ + """Return battery information.""" try: percent, minsleft, power_plugged = cext.sensors_battery() except NotImplementedError: @@ -369,10 +365,8 @@ def sensors_fans(): """ ret = collections.defaultdict(list) rawlist = cext.sensors_fans() - if rawlist is not None: - for fan in rawlist: - ret["Fans"].append(_common.sfan(fan[0], fan[1])) - + for fan in rawlist: + ret["Fans"].append(_common.sfan(fan[0], fan[1])) return dict(ret) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 584df3b836..15f4d4f696 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -930,33 +930,27 @@ psutil_sensors_fans(PyObject *self, PyObject *args) { return NULL; fan_count = SMCGetFanNumber(SMC_KEY_FAN_NUM); - if (fan_count < 0) { + if (fan_count < 0) fan_count = 0; - } - for (key =0; key < fan_count; key++) { + + for (key = 0; key < fan_count; key++) { sprintf(fan, "Fan %d", key); speed = SMCGetFanSpeed(key); - if (speed < 0) { + if (speed < 0) continue; - } - py_tuple = Py_BuildValue( - "(si)", - fan, // label - speed // value - ); + py_tuple = Py_BuildValue("(si)", fan, speed); if (!py_tuple) goto error; - - if (PyList_Append(py_retlist, py_tuple)) { + if (PyList_Append(py_retlist, py_tuple)) goto error; - } Py_XDECREF(py_tuple); } return py_retlist; + error: Py_XDECREF(py_tuple); - Py_XDECREF(py_retlist); + Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/osx/smc.c b/psutil/arch/osx/smc.c index fb5668a1e3..00de0a3725 100644 --- a/psutil/arch/osx/smc.c +++ b/psutil/arch/osx/smc.c @@ -105,14 +105,16 @@ kern_return_t SMCCall(io_connect_t conn, #if MAC_OS_X_VERSION_10_5 return IOConnectCallStructMethod( - conn, index, + conn, + index, inputStructure, structureInputSize, outputStructure, &structureOutputSize); #else return IOConnectMethodStructureIStructureO( - conn, index, + conn, + index, structureInputSize, &structureOutputSize, inputStructure, From 44ff3cda273d31abbdd36d51c20efaee61da0622 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Jun 2018 23:53:16 +0200 Subject: [PATCH 0031/1714] appveyor: remove py 3.4 and add 3.7 --- appveyor.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 436faadbc4..dbf19ae449 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,10 +20,6 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.x" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "32" @@ -32,16 +28,16 @@ environment: PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python37" + PYTHON_VERSION: "3.7.x" + PYTHON_ARCH: "32" + # 64 bits - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.x" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" @@ -50,6 +46,10 @@ environment: PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python37-x64" + PYTHON_VERSION: "3.7.x" + PYTHON_ARCH: "64" + # Also build on a Python version not pre-installed by Appveyor. # See: https://github.com/ogrisel/python-appveyor-demo/issues/10 # - PYTHON: "C:\\Python266" From f6def245a832feb6b15f53dfc006ba40cd38c98a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Jun 2018 00:00:46 +0200 Subject: [PATCH 0032/1714] try to fix occasional children() failure on Win: https://ci.appveyor.com/project/giampaolo/psutil/build/job/je3qyldbb86ff66h --- psutil/tests/test_process.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 6f58be6d25..e295c95b1f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1059,6 +1059,7 @@ def test_parent_disappeared(self): self.assertIsNone(p.parent()) def test_children(self): + reap_children(recursive=True) p = psutil.Process() self.assertEqual(p.children(), []) self.assertEqual(p.children(recursive=True), []) From 7a3ed446447ec845a7d03b32233b4ed7b48f3c26 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Jun 2018 00:01:18 +0200 Subject: [PATCH 0033/1714] apveyor: reset py 3.4 and remove 3.7 (not available yet) --- appveyor.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index dbf19ae449..436faadbc4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,6 +20,10 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python34" + PYTHON_VERSION: "3.4.x" + PYTHON_ARCH: "32" + - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "32" @@ -28,16 +32,16 @@ environment: PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python37" - PYTHON_VERSION: "3.7.x" - PYTHON_ARCH: "32" - # 64 bits - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4.x" + PYTHON_ARCH: "64" + - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" @@ -46,10 +50,6 @@ environment: PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python37-x64" - PYTHON_VERSION: "3.7.x" - PYTHON_ARCH: "64" - # Also build on a Python version not pre-installed by Appveyor. # See: https://github.com/ogrisel/python-appveyor-demo/issues/10 # - PYTHON: "C:\\Python266" From 2f60e997fc1c500e0c245979bbecf2761f36e820 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Jun 2018 16:57:36 -0700 Subject: [PATCH 0034/1714] Rename OSX to macOS (#1298) rename OSX to macOS --- .ci/README | 2 +- .ci/travis/run.sh | 2 +- .coveragerc | 2 +- .travis.yml | 2 +- CREDITS | 4 +- DEVGUIDE.rst | 10 +-- HISTORY.rst | 99 +++++++++++---------- IDEAS | 6 +- INSTALL.rst | 6 +- MANIFEST.in | 12 +-- Makefile | 2 +- README.rst | 6 +- docs/index.rst | 57 ++++++------ psutil/__init__.py | 37 ++++---- psutil/_common.py | 9 +- psutil/_exceptions.py | 2 +- psutil/{_psosx.py => _psmacos.py} | 10 +-- psutil/_psutil_bsd.c | 2 +- psutil/{_psutil_osx.c => _psutil_macos.c} | 32 +++---- psutil/_psutil_posix.c | 20 ++--- psutil/arch/{osx => macos}/process_info.c | 6 +- psutil/arch/{osx => macos}/process_info.h | 0 psutil/arch/{osx => macos}/smc.c | 0 psutil/arch/{osx => macos}/smc.h | 0 psutil/tests/README.rst | 2 +- psutil/tests/__init__.py | 8 +- psutil/tests/test_connections.py | 10 +-- psutil/tests/test_contracts.py | 22 ++--- psutil/tests/{test_osx.py => test_macos.py} | 12 +-- psutil/tests/test_memory_leaks.py | 10 +-- psutil/tests/test_posix.py | 10 +-- psutil/tests/test_process.py | 24 ++--- psutil/tests/test_system.py | 12 +-- psutil/tests/test_unicode.py | 6 +- scripts/internal/bench_oneshot.py | 2 +- scripts/internal/print_announce.py | 6 +- scripts/iotop.py | 2 +- scripts/pmap.py | 2 +- scripts/procsmem.py | 2 +- setup.py | 16 ++-- 40 files changed, 245 insertions(+), 229 deletions(-) rename psutil/{_psosx.py => _psmacos.py} (98%) rename psutil/{_psutil_osx.c => _psutil_macos.c} (98%) rename psutil/arch/{osx => macos}/process_info.c (98%) rename psutil/arch/{osx => macos}/process_info.h (100%) rename psutil/arch/{osx => macos}/smc.c (100%) rename psutil/arch/{osx => macos}/smc.h (100%) rename psutil/tests/{test_osx.py => test_macos.py} (97%) diff --git a/.ci/README b/.ci/README index 86b72afb83..810fe63800 100644 --- a/.ci/README +++ b/.ci/README @@ -1,3 +1,3 @@ This directory contains support scripts for Travis and Appveyor continuous integration services. -Travis is used to run tests on Linux and OSX, Appveyor runs tests on Windows. +Travis is used to run tests on Linux and macOS, Appveyor runs tests on Windows. diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 1501387a53..06cc5b7e60 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -5,7 +5,7 @@ set -x PYVER=`python -c 'import sys; print(".".join(map(str, sys.version_info[:2])))'` -# setup OSX +# setup macOS if [[ "$(uname -s)" == 'Darwin' ]]; then if which pyenv > /dev/null; then eval "$(pyenv init -)" diff --git a/.coveragerc b/.coveragerc index 7d3f185f55..c6772e9539 100644 --- a/.coveragerc +++ b/.coveragerc @@ -21,7 +21,7 @@ exclude_lines = if LITTLE_ENDIAN: if NETBSD if OPENBSD - if OSX + if MACOS if ppid_map is None: if PY3: if SUNOS diff --git a/.travis.yml b/.travis.yml index 9289eb6b7a..86cc73ec09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ matrix: - python: 3.4 - python: 3.5 - python: 3.6 - # OSX + # macOS - language: generic os: osx env: PYVER=py27 diff --git a/CREDITS b/CREDITS index fd4f53d644..89a91008e4 100644 --- a/CREDITS +++ b/CREDITS @@ -34,7 +34,7 @@ Github usernames of people to CC on github when in need of help. - glebius, Gleb Smirnoff (#1013) - sunpoet, Po-Chuan Hsieh (pkg maintainer, #1105) - kostikbel, Konstantin Belousov (#1105) -- OSX: +- macOS: - whitlockjc, Jeremy Whitlock - Windows: - mrjefftang, Jeff Tang @@ -65,7 +65,7 @@ I: 340, 529, 616, 653, 654, 648, 641 N: Jeremy Whitlock E: jcscoobyrs@gmail.com -D: great help with OSX C development. +D: great help with macOS C development. I: 125, 150, 174, 206 N: Landry Breuil diff --git a/DEVGUIDE.rst b/DEVGUIDE.rst index 2d48ced27c..f604480e55 100644 --- a/DEVGUIDE.rst +++ b/DEVGUIDE.rst @@ -139,10 +139,10 @@ All of the services listed below are automatically run on ``git push``. Unit tests ---------- -Tests are automatically run for every GIT push on **Linux**, **OSX** and +Tests are automatically run for every GIT push on **Linux**, **macOS** and **Windows** by using: -- `Travis `_ (Linux, OSX) +- `Travis `_ (Linux, macOS) - `Appveyor `_ (Windows) Test files controlling these are @@ -153,15 +153,15 @@ Both services run psutil test suite against all supported python version (2.6 - 3.6). Two icons in the home page (README) always show the build status: -.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20OSX +.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20macOS :target: https://travis-ci.org/giampaolo/psutil - :alt: Linux tests (Travis) + :alt: Linux and macOS tests (Travis) .. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil :alt: Windows tests (Appveyor) -OSX, BSD, AIX and Solaris are currently tested manually (sigh!). +BSD, AIX and Solaris are currently tested manually. Test coverage ------------- diff --git a/HISTORY.rst b/HISTORY.rst index 3ba7131451..67f967eda9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,14 +7,16 @@ XXXX-XX-XX **Enhancements** -- 1284_: [OSX] added support for sensors_temperatures() and sensors_fans(). +- 1284_: [macOS] added support for sensors_temperatures() and sensors_fans(). (patch by Alex Manuskin) +- 1286_: [macOS] psutil.OSX constant is now deprecated in favor of new + psutil.MACOS. **Bug fixes** -- 1209_: [OSX] Process.memory_maps() may fail with EINVAL due to poor +- 1209_: [macOS] Process.memory_maps() may fail with EINVAL due to poor task_for_pid() syscall. AccessDenied is now raised instead. -- 1278_: [OSX] Process.threads() incorrectly return microseconds instead of +- 1278_: [macOS] Process.threads() incorrectly return microseconds instead of seconds. (patch by Nikhil Marathe) 5.4.6 @@ -101,7 +103,7 @@ XXXX-XX-XX **Bug fixes** -- 1193_: pids() may return False on OSX. +- 1193_: pids() may return False on macOS. 5.4.2 ===== @@ -112,7 +114,7 @@ XXXX-XX-XX - 1173_: introduced PSUTIL_DEBUG environment variable which can be set in order to print useful debug messages on stderr (useful in case of nasty errors). -- 1177_: added support for sensors_battery() on OSX. (patch by Arnon Yaari) +- 1177_: added support for sensors_battery() on macOS. (patch by Arnon Yaari) - 1183_: Process.children() is 2x faster on UNIX and 2.4x faster on Linux. - 1188_: deprecated method Process.memory_info_ex() now warns by using FutureWarning instead of DeprecationWarning. @@ -126,8 +128,8 @@ XXXX-XX-XX - 1179_: [Linux] Process.cmdline() is now able to splits cmdline args for misbehaving processes which overwrite /proc/pid/cmdline and use spaces instead of null bytes as args separator. -- 1181_: [OSX] Process.memory_maps() may raise ENOENT. -- 1187_: [OSX] pids() does not return PID 0 on recent OSX versions. +- 1181_: [macOS] Process.memory_maps() may raise ENOENT. +- 1187_: [macOS] pids() does not return PID 0 on recent macOS versions. 5.4.1 ===== @@ -164,7 +166,7 @@ XXXX-XX-XX - 1009_: [Linux] sensors_temperatures() may crash with IOError. - 1012_: [Windows] disk_io_counters()'s read_time and write_time were expressed in tens of micro seconds instead of milliseconds. -- 1127_: [OSX] invalid reference counting in Process.open_files() may lead to +- 1127_: [macOS] invalid reference counting in Process.open_files() may lead to segfault. (patch by Jakub Bacic) - 1129_: [Linux] sensors_fans() may crash with IOError. (patch by Sebastian Saip) @@ -236,14 +238,14 @@ XXXX-XX-XX cards installed. - 1021_: [Linux] open_files() may erroneously raise NoSuchProcess instead of skipping a file which gets deleted while open files are retrieved. -- 1029_: [OSX, FreeBSD] Process.connections('unix') on Python 3 doesn't +- 1029_: [macOS, FreeBSD] Process.connections('unix') on Python 3 doesn't properly handle unicode paths and may raise UnicodeDecodeError. -- 1033_: [OSX, FreeBSD] memory leak for net_connections() and +- 1033_: [macOS, FreeBSD] memory leak for net_connections() and Process.connections() when retrieving UNIX sockets (kind='unix'). - 1040_: fixed many unicode related issues such as UnicodeDecodeError on Python 3 + UNIX and invalid encoded data on Windows. - 1042_: [FreeBSD] psutil won't compile on FreeBSD 12. -- 1044_: [OSX] different Process methods incorrectly raise AccessDenied for +- 1044_: [macOS] different Process methods incorrectly raise AccessDenied for zombie processes. - 1046_: [Windows] disk_partitions() on Windows overrides user's SetErrorMode. - 1047_: [Windows] Process username(): memory leak in case exception is thrown. @@ -345,7 +347,8 @@ XXXX-XX-XX **Bug fixes** - 872_: [Linux] can now compile on Linux by using MUSL C library. -- 985_: [Windows] Fix a crash in `Process.open_files` when the worker thread for `NtQueryObject` times out. +- 985_: [Windows] Fix a crash in `Process.open_files` when the worker thread + for `NtQueryObject` times out. - 986_: [Linux] Process.cwd() may raise NoSuchProcess instead of ZombieProcess. 5.1.3 @@ -485,8 +488,8 @@ XXXX-XX-XX **Bug fixes** -- 514_: [OSX] possibly fix Process.memory_maps() segfault (critical!). -- 783_: [OSX] Process.status() may erroneously return "running" for zombie +- 514_: [macOS] possibly fix Process.memory_maps() segfault (critical!). +- 783_: [macOS] Process.status() may erroneously return "running" for zombie processes. - 798_: [Windows] Process.open_files() returns and empty list on Windows 10. - 825_: [Linux] cpu_affinity; fix possible double close and use of unopened @@ -499,12 +502,12 @@ XXXX-XX-XX - 906_: [BSD] disk_partitions(all=False) returned an empty list. Now the argument is ignored and all partitions are always returned. - 907_: [FreeBSD] Process.exe() may fail with OSError(ENOENT). -- 908_: [OSX, BSD] different process methods could errounesuly mask the real +- 908_: [macOS, BSD] different process methods could errounesuly mask the real error for high-privileged PIDs and raise NoSuchProcess and AccessDenied instead of OSError and RuntimeError. -- 909_: [OSX] Process open_files() and connections() methods may raise +- 909_: [macOS] Process open_files() and connections() methods may raise OSError with no exception set if process is gone. -- 916_: [OSX] fix many compilation warnings. +- 916_: [macOS] fix many compilation warnings. 4.3.1 ===== @@ -585,7 +588,7 @@ XXXX-XX-XX - 777_: [Linux] Process.open_files() on Linux return 3 new fields: position, mode and flags. - 779_: Process.cpu_times() returns two new fields, 'children_user' and - 'children_system' (always set to 0 on OSX and Windows). + 'children_system' (always set to 0 on macOS and Windows). - 789_: [Windows] psutil.cpu_times() return two new fields: "interrupt" and "dpc". Same for psutil.cpu_times_percent(). - 792_: new psutil.cpu_stats() function returning number of CPU ctx switches @@ -597,10 +600,10 @@ XXXX-XX-XX provides it. - 776_: [Linux] Process.cpu_affinity() may erroneously raise NoSuchProcess. (patch by wxwright) -- 780_: [OSX] psutil does not compile with some gcc versions. +- 780_: [macOS] psutil does not compile with some gcc versions. - 786_: net_if_addrs() may report incomplete MAC addresses. - 788_: [NetBSD] virtual_memory()'s buffers and shared values were set to 0. -- 790_: [OSX] psutil won't compile on OSX 10.4. +- 790_: [macOS] psutil won't compile on macOS 10.4. 4.0.0 ===== @@ -613,11 +616,11 @@ XXXX-XX-XX - 660_: [Windows] make.bat is smarter in finding alternative VS install locations. (patch by mpderbec) - 732_: Process.environ(). (patch by Frank Benkstein) -- 753_: [Linux, OSX, Windows] Process USS and PSS (Linux) "real" memory stats. +- 753_: [Linux, macOS, Windows] Process USS and PSS (Linux) "real" memory stats. (patch by Eric Rahm) - 755_: Process.memory_percent() "memtype" parameter. - 758_: tests now live in psutil namespace. -- 760_: expose OS constants (psutil.LINUX, psutil.OSX, etc.) +- 760_: expose OS constants (psutil.LINUX, psutil.macOS, etc.) - 756_: [Linux] disk_io_counters() return 2 new fields: read_merged_count and write_merged_count. - 762_: new scripts/procsmem.py script. @@ -734,7 +737,7 @@ XXXX-XX-XX - 644_: [Windows] added support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals to use with Process.send_signal(). -- 648_: CI test integration for OSX. (patch by Jeff Tang) +- 648_: CI test integration for macOS. (patch by Jeff Tang) - 663_: [UNIX] net_if_addrs() now returns point-to-point (VPNs) addresses. - 655_: [Windows] different issues regarding unicode handling were fixed. On Python 2 all APIs returning a string will now return an encoded version of it @@ -1253,7 +1256,7 @@ DeprecationWarning. - 374_: [Windows] negative memory usage reported if process uses a lot of memory. - 379_: [Linux] Process.get_memory_maps() may raise ValueError. -- 394_: [OSX] Mapped memory regions report incorrect file name. +- 394_: [macOS] Mapped memory regions report incorrect file name. - 404_: [Linux] sched_*affinity() are implicitly declared. (patch by Arfrever) **API changes** @@ -1302,16 +1305,16 @@ DeprecationWarning. certain exotic Linux flavors having an incomplete /proc interface. If that's the case we now set the unretrievable stats to 0 and raise a RuntimeWarning. -- 315_: [OSX] fix some compilation warnings. +- 315_: [macOS] fix some compilation warnings. - 317_: [Windows] cannot set process CPU affinity above 31 cores. - 319_: [Linux] process get_memory_maps() raises KeyError 'Anonymous' on Debian squeeze. - 321_: [UNIX] Process.ppid property is no longer cached as the kernel may set the ppid to 1 in case of a zombie process. -- 323_: [OSX] disk_io_counters()'s read_time and write_time parameters were +- 323_: [macOS] disk_io_counters()'s read_time and write_time parameters were reporting microseconds not milliseconds. (patch by Gregory Szorc) - 331_: Process cmdline is no longer cached after first acces as it may change. -- 333_: [OSX] Leak of Mach ports on OS X (patch by rsesek@google.com) +- 333_: [macOS] Leak of Mach ports on macOS (patch by rsesek@google.com) - 337_: [Linux] process methods not working because of a poor /proc implementation will raise NotImplementedError rather than RuntimeError and Process.as_dict() will not blow up. (patch by Curtin1060) @@ -1324,7 +1327,7 @@ DeprecationWarning. - 338_: [Linux] disk_io_counters() fails to find some disks. - 351_: [Windows] if psutil is compiled with mingw32 (provided installers for py2.4 and py2.5 are) disk_io_counters() will fail. (Patch by m.malycha) -- 353_: [OSX] get_users() returns an empty list on OSX 10.8. +- 353_: [macOS] get_users() returns an empty list on macOS 10.8. - 356_: Process.parent now checks whether parent PID has been reused in which case returns None. - 365_: Process.set_nice() should check PID has not been reused by another @@ -1370,11 +1373,11 @@ DeprecationWarning. - 216_: [POSIX] get_connections() UNIX sockets support. - 220_: [FreeBSD] get_connections() has been rewritten in C and no longer requires lsof. -- 222_: [OSX] add support for process cwd. +- 222_: [macOS] add support for process cwd. - 261_: process extended memory info. -- 295_: [OSX] process executable path is now determined by asking the OS +- 295_: [macOS] process executable path is now determined by asking the OS instead of being guessed from process cmdline. -- 297_: [OSX] the Process methods below were always raising AccessDenied for +- 297_: [macOS] the Process methods below were always raising AccessDenied for any process except the current one. Now this is no longer true. Also they are 2.5x faster. - name @@ -1407,8 +1410,8 @@ DeprecationWarning. - active [POSIX] - inactive [POSIX] - buffers (BSD, Linux) - - cached (BSD, OSX) - - wired (OSX, BSD) + - cached (BSD, macOS) + - wired (macOS, BSD) - shared [FreeBSD] New psutil.swap_memory() provides: - total @@ -1425,7 +1428,7 @@ DeprecationWarning. **Bug fixes** -- 298_: [OSX and BSD] memory leak in get_num_fds(). +- 298_: [macOS and BSD] memory leak in get_num_fds(). - 299_: potential memory leak every time PyList_New(0) is used. - 303_: [Windows] potential heap corruption in get_num_threads() and get_status() Process methods. @@ -1480,7 +1483,7 @@ DeprecationWarning. - 245_: [POSIX] Process.wait() incrementally consumes less CPU cycles. - 257_: [Windows] removed Windows 2000 support. - 258_: [Linux] Process.get_memory_info() is now 0.5x faster. -- 260_: process's mapped memory regions. (Windows patch by wj32.64, OSX patch +- 260_: process's mapped memory regions. (Windows patch by wj32.64, macOS patch by Jeremy Whitlock) - 262_: [Windows] psutil.disk_partitions() was slow due to inspecting the floppy disk drive also when "all" argument was False. @@ -1503,7 +1506,7 @@ DeprecationWarning. - 193_: psutil.Popen constructor can throw an exception if the spawned process terminates quickly. -- 240_: [OSX] incorrect use of free() for Process.get_connections(). +- 240_: [macOS] incorrect use of free() for Process.get_connections(). - 244_: [POSIX] Process.wait() can hog CPU resources if called against a process which is not our children. - 248_: [Linux] psutil.network_io_counters() might return erroneous NIC names. @@ -1511,7 +1514,7 @@ DeprecationWarning. processes owned by another user. It now raises AccessDenied instead. - 266_: [Windows] psutil.get_pid_list() only shows 1024 processes. (patch by Amoser) -- 267_: [OSX] Process.get_connections() - an erroneous remote address was +- 267_: [macOS] Process.get_connections() - an erroneous remote address was returned. (Patch by Amoser) - 272_: [Linux] Porcess.get_open_files() - potential race condition can lead to unexpected NoSuchProcess exception. Also, we can get incorrect reports @@ -1542,7 +1545,7 @@ DeprecationWarning. **Bug fixes** - 228_: some example scripts were not working with python 3. -- 230_: [Windows / OSX] memory leak in Process.get_connections(). +- 230_: [Windows / macOS] memory leak in Process.get_connections(). - 232_: [Linux] psutil.phymem_usage() can report erroneous values which are different than "free" command. - 236_: [Windows] memory/handle leak in Process's get_memory_info(), @@ -1555,12 +1558,12 @@ DeprecationWarning. **Enhancements** -- 150_: network I/O counters. (OSX and Windows patch by Jeremy Whitlock) +- 150_: network I/O counters. (macOS and Windows patch by Jeremy Whitlock) - 154_: [FreeBSD] add support for process getcwd() - 157_: [Windows] provide installer for Python 3.2 64-bit. - 198_: Process.wait(timeout=0) can now be used to make wait() return immediately. -- 206_: disk I/O counters. (OSX and Windows patch by Jeremy Whitlock) +- 206_: disk I/O counters. (macOS and Windows patch by Jeremy Whitlock) - 213_: examples/iotop.py script. - 217_: Process.get_connections() now has a "kind" argument to filter for connections with different criteria. @@ -1571,7 +1574,7 @@ DeprecationWarning. **Bug fixes** -- 135_: [OSX] psutil cannot create Process object. +- 135_: [macOS] psutil cannot create Process object. - 144_: [Linux] no longer support 0 special PID. - 188_: [Linux] psutil import error on Linux ARM architectures. - 194_: [POSIX] psutil.Process.get_cpu_percent() now reports a percentage over @@ -1613,7 +1616,7 @@ DeprecationWarning. - 166_: get_memory_info() leaks handles hogging system resources. - 168_: psutil.cpu_percent() returns erroneous results when used in non-blocking mode. (patch by Philip Roberts) -- 178_: OSX - Process.get_threads() leaks memory +- 178_: macOS - Process.get_threads() leaks memory - 180_: [Windows] Process's get_num_threads() and get_threads() methods can raise NoSuchProcess exception while process still exists. @@ -1638,14 +1641,14 @@ DeprecationWarning. - 147_: per-process I/O nice (priority) - Linux only. - 148_: psutil.Popen class which tidies up subprocess.Popen and psutil.Process in a unique interface. -- 152_: [OSX] get_process_open_files() implementation has been rewritten +- 152_: [macOS] get_process_open_files() implementation has been rewritten in C and no longer relies on lsof resulting in a 3x speedup. -- 153_: [OSX] get_process_connection() implementation has been rewritten +- 153_: [macOS] get_process_connection() implementation has been rewritten in C and no longer relies on lsof resulting in a 3x speedup. **Bug fixes** -- 83_: process cmdline is empty on OSX 64-bit. +- 83_: process cmdline is empty on macOS 64-bit. - 130_: a race condition can cause IOError exception be raised on Linux if process disappears between open() and subsequent read() calls. - 145_: WindowsError was raised instead of psutil.AccessDenied when using @@ -1697,7 +1700,7 @@ DeprecationWarning. left behind every time Process class was instantiated. - 111_: path and name Process properties report truncated or erroneous values on UNIX. -- 120_: cpu_percent() always returning 100% on OS X. +- 120_: cpu_percent() always returning 100% on macOS. - 112_: uid and gid properties don't change if process changes effective user/group id at some point. - 126_: ppid, uid, gid, name, exe, cmdline and create_time properties are @@ -1746,7 +1749,7 @@ DeprecationWarning. available - 58_: is_running() is now called before kill() to make sure we are going to kill the correct process. -- 73_: virtual memory size reported on OS X includes shared library size +- 73_: virtual memory size reported on macOS includes shared library size - 77_: NoSuchProcess wasn't raised on Process.create_time if kill() was used first. @@ -1770,7 +1773,7 @@ DeprecationWarning. **Bug fixes** - 36_: [Windows] NoSuchProcess not raised when accessing timing methods. -- 40_: test_get_cpu_times() failing on FreeBSD and OS X. +- 40_: test_get_cpu_times() failing on FreeBSD and macOS. - 42_: [Windows] get_memory_percent() raises AccessDenied. 0.1.1 diff --git a/IDEAS b/IDEAS index 9037269839..8190d48b98 100644 --- a/IDEAS +++ b/IDEAS @@ -18,7 +18,7 @@ PLATFORMS FEATURES ======== -- #371: sensors_temperatures() at least for OSX. +- #371: sensors_temperatures() at least for macOS. - #669: Windows / net_if_addrs(): return broadcast addr. @@ -27,7 +27,7 @@ FEATURES - #772: extended net_io_counters() metrics. -- #900: wheels for OSX and Linux. +- #900: wheels for macOS and Linux. - #922: extended net_io_stats() info. @@ -51,7 +51,7 @@ FEATURES Linux: yes Others: ? -- Process.threads(): thread names; patch for OSX available at: +- Process.threads(): thread names; patch for macOS available at: https://code.google.com/p/plcrashreporter/issues/detail?id=65 Sample code: https://github.com/janmojzis/pstree/blob/master/proc_kvm.c diff --git a/INSTALL.rst b/INSTALL.rst index 4ceb0c19b9..6644fb37b0 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -10,7 +10,7 @@ On Linux or via wget: wget https://bootstrap.pypa.io/get-pip.py -O - | python -On OSX or via curl: +On macOS or via curl: .. code-block:: bash @@ -61,8 +61,8 @@ RedHat / CentOS: If you're on Python 3 use ``python3-dev`` and ``python3-pip`` instead. -OSX -=== +macOS +===== Install `Xcode `__ first, then: diff --git a/MANIFEST.in b/MANIFEST.in index 26d678fc33..c6d8c550a0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,7 +28,7 @@ include psutil/_exceptions.py include psutil/_psaix.py include psutil/_psbsd.py include psutil/_pslinux.py -include psutil/_psosx.py +include psutil/_psmacos.py include psutil/_psposix.py include psutil/_pssunos.py include psutil/_psutil_aix.c @@ -36,7 +36,7 @@ include psutil/_psutil_bsd.c include psutil/_psutil_common.c include psutil/_psutil_common.h include psutil/_psutil_linux.c -include psutil/_psutil_osx.c +include psutil/_psutil_macos.c include psutil/_psutil_posix.c include psutil/_psutil_posix.h include psutil/_psutil_sunos.c @@ -55,14 +55,16 @@ include psutil/arch/freebsd/specific.c include psutil/arch/freebsd/specific.h include psutil/arch/freebsd/sys_socks.c include psutil/arch/freebsd/sys_socks.h +include psutil/arch/macos/process_info.c +include psutil/arch/macos/process_info.h +include psutil/arch/macos/smc.c +include psutil/arch/macos/smc.h include psutil/arch/netbsd/socks.c include psutil/arch/netbsd/socks.h include psutil/arch/netbsd/specific.c include psutil/arch/netbsd/specific.h include psutil/arch/openbsd/specific.c include psutil/arch/openbsd/specific.h -include psutil/arch/osx/process_info.c -include psutil/arch/osx/process_info.h include psutil/arch/solaris/environ.c include psutil/arch/solaris/environ.h include psutil/arch/solaris/v10/ifaddrs.c @@ -86,9 +88,9 @@ include psutil/tests/test_bsd.py include psutil/tests/test_connections.py include psutil/tests/test_contracts.py include psutil/tests/test_linux.py +include psutil/tests/test_macos.py include psutil/tests/test_memory_leaks.py include psutil/tests/test_misc.py -include psutil/tests/test_osx.py include psutil/tests/test_posix.py include psutil/tests/test_process.py include psutil/tests/test_sunos.py diff --git a/Makefile b/Makefile index 51047383fa..58c0d39612 100644 --- a/Makefile +++ b/Makefile @@ -142,7 +142,7 @@ test-posix: ## POSIX specific tests. test-platform: ## Run specific platform tests only. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(TEST_PREFIX) $(PYTHON) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "MACOS", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} install diff --git a/README.rst b/README.rst index 0852b1ce51..abd5816c67 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20OSX +.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20macOS :target: https://travis-ci.org/giampaolo/psutil :alt: Linux tests (Travis) @@ -56,7 +56,7 @@ psutil currently supports the following platforms: - **Linux** - **Windows** -- **OSX**, +- **macOS**, - **FreeBSD, OpenBSD**, **NetBSD** - **Sun Solaris** - **AIX** @@ -307,7 +307,7 @@ Process management >>> >>> p.memory_info() pmem(rss=10915840, vms=67608576, shared=3313664, text=2310144, lib=0, data=7262208, dirty=0) - >>> p.memory_full_info() # "real" USS memory usage (Linux, OSX, Win only) + >>> p.memory_full_info() # "real" USS memory usage (Linux, macOS, Win only) pfullmem(rss=10199040, vms=52133888, shared=3887104, text=2867200, lib=0, data=5967872, dirty=0, uss=6545408, pss=6872064, swap=0) >>> p.memory_percent() 0.7823 diff --git a/docs/index.rst b/docs/index.rst index 208f59e9c7..47167d7642 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,7 +32,7 @@ psutil currently supports the following platforms: - **Linux** - **Windows** -- **OSX**, +- **macOS**, - **FreeBSD, OpenBSD**, **NetBSD** - **Sun Solaris** - **AIX** @@ -235,7 +235,7 @@ CPU scpufreq(current=1703.609, min=800.0, max=3500.0), scpufreq(current=1754.289, min=800.0, max=3500.0)] - Availability: Linux, OSX, Windows + Availability: Linux, macOS, Windows .. versionadded:: 5.1.0 @@ -272,7 +272,7 @@ Memory - **shared** *(Linux, BSD)*: memory that may be simultaneously accessed by multiple processes. - **slab** *(Linux)*: in-kernel data structures cache. - - **wired** *(BSD, OSX)*: memory that is marked to always stay in RAM. It is + - **wired** *(BSD, macOS)*: memory that is marked to always stay in RAM. It is never moved to disk. The sum of **used** and **available** does not necessarily equal **total**. @@ -345,7 +345,7 @@ Disks On Windows it is determined via `GetDriveType `__ and can be either ``"removable"``, ``"fixed"``, ``"remote"``, ``"cdrom"``, - ``"unmounted"`` or ``"ramdisk"``. On OSX and BSD it is retrieved via + ``"unmounted"`` or ``"ramdisk"``. On macOS and BSD it is retrieved via `getfsstat(2) `__. See `disk_usage.py `__ script providing an example usage. @@ -462,7 +462,7 @@ Network - **errout**: total number of errors while sending - **dropin**: total number of incoming packets which were dropped - **dropout**: total number of outgoing packets which were dropped (always 0 - on OSX and BSD) + on macOS and BSD) If *pernic* is ``True`` return the same information for every network interface installed on the system as a dictionary with network interface @@ -555,7 +555,7 @@ Network | ``"all"`` | the sum of all the possible families and protocols | +----------------+-----------------------------------------------------+ - On OSX and AIX this function requires root privileges. + On macOS and AIX this function requires root privileges. To get per-process connections use :meth:`Process.connections`. Also, see `netstat.py sample script `__. @@ -570,7 +570,7 @@ Network ...] .. note:: - (OSX and AIX) :class:`psutil.AccessDenied` is always raised unless running + (macOS and AIX) :class:`psutil.AccessDenied` is always raised unless running as root. This is a limitation of the OS and ``lsof`` does the same. .. note:: @@ -698,11 +698,11 @@ Sensors See also `temperatures.py `__ and `sensors.py `__ for an example application. - Availability: Linux, OSX + Availability: Linux, macOS .. versionadded:: 5.1.0 - .. versionchanged:: 5.5.0: added OSX support + .. versionchanged:: 5.5.0: added macOS support .. warning:: @@ -724,11 +724,11 @@ Sensors See also `fans.py `__ and `sensors.py `__ for an example application. - Availability: Linux, OSX + Availability: Linux, macOS .. versionadded:: 5.2.0 - .. versionchanged:: 5.5.0: added OSX support + .. versionchanged:: 5.5.0: added macOS support .. warning:: @@ -772,7 +772,7 @@ Sensors .. versionadded:: 5.1.0 - .. versionchanged:: 5.4.2 added OSX support + .. versionchanged:: 5.4.2 added macOS support .. warning:: @@ -1070,7 +1070,7 @@ Process class if you call all the methods together (best case scenario). +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | Linux | Windows | OSX | BSD | SunOS | AIX | + | Linux | Windows | macOS | BSD | SunOS | AIX | +==============================+===============================+==============================+==============================+==========================+==========================+ | :meth:`cpu_num` | :meth:`cpu_percent` | :meth:`cpu_percent` | :meth:`cpu_num` | :meth:`name` | :meth:`name` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ @@ -1158,7 +1158,7 @@ Process class >>> psutil.Process().environ() {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} - Availability: Linux, OSX, Windows, SunOS + Availability: Linux, macOS, Windows, SunOS .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support @@ -1414,7 +1414,7 @@ Process class Return a `(user, system, children_user, children_system)` named tuple representing the accumulated process time, in seconds (see `explanation `__). - On Windows and OSX only *user* and *system* are filled, the others are + On Windows and macOS only *user* and *system* are filled, the others are set to ``0``. This is similar to `os.times() `__ @@ -1522,7 +1522,7 @@ Process class All numbers are expressed in bytes. +---------+---------+-------+---------+-----+------------------------------+ - | Linux | OSX | BSD | Solaris | AIX | Windows | + | Linux | macOS | BSD | Solaris | AIX | Windows | +=========+=========+=======+=========+=====+==============================+ | rss | rss | rss | rss | rss | rss (alias for ``wset``) | +---------+---------+-------+---------+-----+------------------------------+ @@ -1582,9 +1582,9 @@ Process class - **dirty** *(Linux)*: the number of dirty pages. - - **pfaults** *(OSX)*: number of page faults. + - **pfaults** *(macOS)*: number of page faults. - - **pageins** *(OSX)*: number of actual pageins. + - **pageins** *(macOS)*: number of actual pageins. For on explanation of Windows fields rely on `PROCESS_MEMORY_COUNTERS_EX `__ structure doc. @@ -1608,7 +1608,7 @@ Process class .. method:: memory_full_info() This method returns the same information as :meth:`memory_info`, plus, on - some platform (Linux, OSX, Windows), also provides additional metrics + some platform (Linux, macOS, Windows), also provides additional metrics (USS, PSS and swap). The additional metrics provide a better representation of "effective" process memory consumption (in case of USS) as explained in detail in this @@ -1619,7 +1619,7 @@ Process class On platforms where extra fields are not implemented this simply returns the same metrics as :meth:`memory_info`. - - **uss** *(Linux, OSX, Windows)*: + - **uss** *(Linux, macOS, Windows)*: aka "Unique Set Size", this is the memory which is unique to a process and which would be freed if the process was terminated right now. @@ -1678,7 +1678,7 @@ Process class for an example application. +---------------+--------------+---------+-----------+--------------+ - | Linux | OSX | Windows | Solaris | FreeBSD | + | Linux | macOS | Windows | Solaris | FreeBSD | +===============+==============+=========+===========+==============+ | rss | rss | rss | rss | rss | +---------------+--------------+---------+-----------+--------------+ @@ -2118,7 +2118,7 @@ Constants .. data:: POSIX .. data:: WINDOWS .. data:: LINUX -.. data:: OSX +.. data:: MACOS .. data:: FREEBSD .. data:: NETBSD .. data:: OPENBSD @@ -2132,6 +2132,13 @@ Constants .. versionadded:: 4.0.0 .. versionchanged:: 5.4.0 added AIX +.. data:: OSX + + Alias for :const:`MACOS` (deprecated). + + .. warning:: + deprecated in version 5.5.0; use :const:`MACOS` instead. + .. _const-procfs_path: .. data:: PROCFS_PATH @@ -2164,7 +2171,7 @@ Constants .. data:: STATUS_DEAD .. data:: STATUS_WAKE_KILL .. data:: STATUS_WAKING -.. data:: STATUS_IDLE (OSX, FreeBSD) +.. data:: STATUS_IDLE (macOS, FreeBSD) .. data:: STATUS_LOCKED (FreeBSD) .. data:: STATUS_WAITING (FreeBSD) .. data:: STATUS_SUSPENDED (NetBSD) @@ -2320,7 +2327,7 @@ methods such as :meth:`Process.username` or :meth:`WindowsService.description`: * all strings are encoded by using the OS filesystem encoding (``sys.getfilesystemencoding()``) which varies depending on the platform - (e.g. "UTF-8" on OSX, "mbcs" on Win) + (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 @@ -2589,7 +2596,7 @@ FAQs * Q: Why do I get :class:`AccessDenied` for certain processes? * A: This may happen when you query processess owned by another user, - especially on `OSX `__ and + especially on `macOS `__ and Windows. Unfortunately there's not much you can do about this except running the Python process with higher privileges. diff --git a/psutil/__init__.py b/psutil/__init__.py index f385927922..affc5c8c3f 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -10,7 +10,7 @@ - Linux - Windows - - OSX + - macOS - FreeBSD - OpenBSD - NetBSD @@ -78,9 +78,10 @@ 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 OPENBSD # NOQA -from ._common import OSX +from ._common import OSX # deprecated alias from ._common import POSIX # NOQA from ._common import SUNOS from ._common import WINDOWS @@ -151,8 +152,8 @@ from ._psutil_windows import REALTIME_PRIORITY_CLASS # NOQA from ._pswindows import CONN_DELETE_TCB # NOQA -elif OSX: - from . import _psosx as _psplatform +elif MACOS: + from . import _psmacos as _psplatform elif BSD: from . import _psbsd as _psplatform @@ -199,8 +200,8 @@ "POWER_TIME_UNKNOWN", "POWER_TIME_UNLIMITED", - "BSD", "FREEBSD", "LINUX", "NETBSD", "OPENBSD", "OSX", "POSIX", "SUNOS", - "WINDOWS", "AIX", + "BSD", "FREEBSD", "LINUX", "NETBSD", "OPENBSD", "MACOS", "OSX", "POSIX", + "SUNOS", "WINDOWS", "AIX", # classes "Process", "Popen", @@ -823,7 +824,7 @@ def cpu_num(self): """ return self._proc.cpu_num() - # Linux, OSX and Windows only + # Linux, macOS and Windows only if hasattr(_psplatform.Process, "environ"): def environ(self): @@ -1027,7 +1028,7 @@ def cpu_times(self): namedtuple representing the accumulated process time, in seconds. This is similar to os.times() but per-process. - On OSX and Windows children_user and children_system are + On macOS and Windows children_user and children_system are always set to 0. """ return self._proc.cpu_times() @@ -1049,7 +1050,7 @@ def memory_info_ex(self): def memory_full_info(self): """This method returns the same information as memory_info(), - plus, on some platform (Linux, OSX, Windows), also provides + plus, on some platform (Linux, macOS, Windows), also provides additional metrics (USS, PSS and swap). The additional metrics provide a better representation of actual process memory usage. @@ -1898,9 +1899,9 @@ def virtual_memory(): - used: memory used, calculated differently depending on the platform and designed for informational purposes only: - OSX: active + inactive + wired + macOS: active + inactive + wired BSD: active + wired + cached - LINUX: total - free + Linux: total - free - free: memory not being used at all (zeroed) that is readily available; @@ -1918,10 +1919,10 @@ def virtual_memory(): - buffers (BSD, Linux): cache for things like file system metadata. - - cached (BSD, OSX): + - cached (BSD, macOS): cache for various things. - - wired (OSX, BSD): + - wired (macOS, BSD): memory that is marked to always stay in RAM. It is never moved to disk. - shared (BSD): @@ -2046,7 +2047,7 @@ def net_io_counters(pernic=False, nowrap=True): - errout: total number of errors while sending - dropin: total number of incoming packets which were dropped - dropout: total number of outgoing packets which were dropped - (always 0 on OSX and BSD) + (always 0 on macOS and BSD) If *pernic* is True return the same information for every network interface installed on the system as a dictionary @@ -2102,7 +2103,7 @@ def net_connections(kind='inet'): | all | the sum of all the possible families and protocols | +------------+----------------------------------------------------+ - On OSX this function requires root privileges. + On macOS this function requires root privileges. """ return _psplatform.net_connections(kind) @@ -2175,7 +2176,7 @@ def net_if_stats(): # ===================================================================== -# Linux, OSX +# Linux, macOS if hasattr(_psplatform, "sensors_temperatures"): def sensors_temperatures(fahrenheit=False): @@ -2213,7 +2214,7 @@ def convert(n): __all__.append("sensors_temperatures") -# Linux, OSX +# Linux, macOS if hasattr(_psplatform, "sensors_fans"): def sensors_fans(): @@ -2226,7 +2227,7 @@ def sensors_fans(): __all__.append("sensors_fans") -# Linux, Windows, FreeBSD, OSX +# Linux, Windows, FreeBSD, macOS if hasattr(_psplatform, "sensors_battery"): def sensors_battery(): diff --git a/psutil/_common.py b/psutil/_common.py index af1fb563cb..6fcf8fe058 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -42,8 +42,8 @@ __all__ = [ # constants - 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'OSX', 'POSIX', 'SUNOS', - 'WINDOWS', + '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', @@ -75,7 +75,8 @@ POSIX = os.name == "posix" WINDOWS = os.name == "nt" LINUX = sys.platform.startswith("linux") -OSX = sys.platform.startswith("darwin") +MACOS = sys.platform.startswith("darwin") +OSX = MACOS # deprecated alias FREEBSD = sys.platform.startswith("freebsd") OPENBSD = sys.platform.startswith("openbsd") NETBSD = sys.platform.startswith("netbsd") @@ -99,7 +100,7 @@ STATUS_DEAD = "dead" STATUS_WAKE_KILL = "wake-kill" STATUS_WAKING = "waking" -STATUS_IDLE = "idle" # FreeBSD, OSX +STATUS_IDLE = "idle" # FreeBSD, macOS STATUS_LOCKED = "locked" # FreeBSD STATUS_WAITING = "waiting" # FreeBSD STATUS_SUSPENDED = "suspended" # NetBSD diff --git a/psutil/_exceptions.py b/psutil/_exceptions.py index c08e6d83c8..6dbbd28269 100644 --- a/psutil/_exceptions.py +++ b/psutil/_exceptions.py @@ -39,7 +39,7 @@ def __init__(self, pid, name=None, msg=None): class ZombieProcess(NoSuchProcess): """Exception raised when querying a zombie process. This is - raised on OSX, BSD and Solaris only, and not always: depending + 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. diff --git a/psutil/_psosx.py b/psutil/_psmacos.py similarity index 98% rename from psutil/_psosx.py rename to psutil/_psmacos.py index a0101df070..a719bedaa1 100644 --- a/psutil/_psosx.py +++ b/psutil/_psmacos.py @@ -2,7 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""OSX platform implementation.""" +"""macOS platform implementation.""" import collections import contextlib @@ -14,7 +14,7 @@ from . import _common from . import _psposix -from . import _psutil_osx as cext +from . import _psutil_macos as cext from . import _psutil_posix as cext_posix from ._common import AF_INET6 from ._common import conn_tmap @@ -277,7 +277,7 @@ def cpu_stats(): def cpu_freq(): """Return CPU frequency. - On OSX per-cpu frequency is not supported. + 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 """ @@ -381,7 +381,7 @@ def sensors_fans(): def net_connections(kind='inet'): """System-wide network connections.""" - # Note: on OSX this will fail with AccessDenied unless + # Note: on macOS this will fail with AccessDenied unless # the process is owned by root. ret = [] for pid in pids(): @@ -444,7 +444,7 @@ def users(): def pids(): ls = cext.pids() if 0 not in ls: - # On certain OSX versions pids() C doesn't return PID 0 but + # On certain macOS versions pids() C doesn't return PID 0 but # "ps" does and the process is querable via sysctl(): # https://travis-ci.org/giampaolo/psutil/jobs/309619941 try: diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 9a2ed04bc5..4e91c02ede 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -393,7 +393,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { /* * Return the number of logical CPUs in the system. - * XXX this could be shared with OSX + * XXX this could be shared with macOS */ static PyObject * psutil_cpu_count_logical(PyObject *self, PyObject *args) { diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_macos.c similarity index 98% rename from psutil/_psutil_osx.c rename to psutil/_psutil_macos.c index 15f4d4f696..8fc3140f62 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_macos.c @@ -3,7 +3,7 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. * - * OS X platform-specific module methods for _psutil_osx + * macOS platform-specific module methods. */ #include @@ -43,8 +43,8 @@ #include "_psutil_common.h" #include "_psutil_posix.h" -#include "arch/osx/process_info.h" -#include "arch/osx/smc.h" +#include "arch/macos/process_info.h" +#include "arch/macos/smc.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -251,7 +251,7 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { "(ddKKkkkk)", (float)pti.pti_total_user / 1000000000.0, // (float) cpu user time (float)pti.pti_total_system / 1000000000.0, // (float) cpu sys time - // Note about memory: determining other mem stats on OSX is a mess: + // Note about memory: determining other mem stats on macOS is a mess: // http://www.opensource.apple.com/source/top/top-67/libtop.c?txt // I just give up. // struct proc_regioninfo pri; @@ -342,7 +342,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - // get the commandline, defined in arch/osx/process_info.c + // get the commandline, defined in arch/macos/process_info.c py_retlist = psutil_get_cmdline(pid); return py_retlist; } @@ -359,7 +359,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - // get the environment block, defined in arch/osx/process_info.c + // get the environment block, defined in arch/macos/process_info.c py_retdict = psutil_get_environ(pid); return py_retdict; } @@ -1759,7 +1759,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { CFNumberGetValue(number, kCFNumberSInt64Type, &write_time); } - // Read/Write time on OS X comes back in nanoseconds and in psutil + // Read/Write time on macOS comes back in nanoseconds and in psutil // we've standardized on milliseconds so do the conversion. py_disk_info = Py_BuildValue( "(KKKKKK)", @@ -2066,13 +2066,13 @@ struct module_state { #if PY_MAJOR_VERSION >= 3 static int -psutil_osx_traverse(PyObject *m, visitproc visit, void *arg) { +psutil_macos_traverse(PyObject *m, visitproc visit, void *arg) { Py_VISIT(GETSTATE(m)->error); return 0; } static int -psutil_osx_clear(PyObject *m) { +psutil_macos_clear(PyObject *m) { Py_CLEAR(GETSTATE(m)->error); return 0; } @@ -2080,31 +2080,31 @@ psutil_osx_clear(PyObject *m) { static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - "psutil_osx", + "psutil_macos", NULL, sizeof(struct module_state), PsutilMethods, NULL, - psutil_osx_traverse, - psutil_osx_clear, + psutil_macos_traverse, + psutil_macos_clear, NULL }; #define INITERROR return NULL -PyMODINIT_FUNC PyInit__psutil_osx(void) +PyMODINIT_FUNC PyInit__psutil_macos(void) #else #define INITERROR return void -init_psutil_osx(void) +init_psutil_macos(void) #endif { #if PY_MAJOR_VERSION >= 3 PyObject *module = PyModule_Create(&moduledef); #else - PyObject *module = Py_InitModule("_psutil_osx", PsutilMethods); + PyObject *module = Py_InitModule("_psutil_macos", PsutilMethods); #endif PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); // process status constants, defined in: @@ -2130,7 +2130,7 @@ init_psutil_osx(void) // Exception. ZombieProcessError = PyErr_NewException( - "_psutil_osx.ZombieProcessError", NULL, NULL); + "_psutil_macos.ZombieProcessError", NULL, NULL); Py_INCREF(ZombieProcessError); PyModule_AddObject(module, "ZombieProcessError", ZombieProcessError); diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index cc827273ca..e9a8f2d95c 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -28,7 +28,7 @@ #include #include #include -#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#elif defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) #include #include #include @@ -71,7 +71,7 @@ psutil_pid_exists(long pid) { #endif } -#if defined(PSUTIL_OSX) +#if defined(PSUTIL_MACOS) ret = kill((pid_t)pid , 0); #else ret = kill(pid , 0); @@ -143,7 +143,7 @@ psutil_posix_getpriority(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; -#ifdef PSUTIL_OSX +#ifdef PSUTIL_MACOS priority = getpriority(PRIO_PROCESS, (id_t)pid); #else priority = getpriority(PRIO_PROCESS, pid); @@ -166,7 +166,7 @@ psutil_posix_setpriority(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "li", &pid, &priority)) return NULL; -#ifdef PSUTIL_OSX +#ifdef PSUTIL_MACOS retval = setpriority(PRIO_PROCESS, (id_t)pid, priority); #else retval = setpriority(PRIO_PROCESS, pid, priority); @@ -221,7 +221,7 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { len = lladdr->sll_halen; data = (const char *)lladdr->sll_addr; } -#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#elif defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) else if (addr->sa_family == AF_LINK) { // Note: prior to Python 3.4 socket module does not expose // AF_LINK so we'll do. @@ -431,9 +431,9 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { /* - * net_if_stats() OSX/BSD implementation. + * net_if_stats() macOS/BSD implementation. */ -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#if defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) int psutil_get_nic_speed(int ifm_active) { // Determine NIC speed. Taken from: @@ -620,7 +620,7 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { close(sock); return PyErr_SetFromErrno(PyExc_OSError); } -#endif // net_if_stats() OSX/BSD implementation +#endif // net_if_stats() macOS/BSD implementation /* @@ -638,7 +638,7 @@ PsutilMethods[] = { "Retrieve NIC MTU"}, {"net_if_flags", psutil_net_if_flags, METH_VARARGS, "Retrieve NIC flags"}, -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#if defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, "Return NIC stats."}, #endif @@ -698,7 +698,7 @@ void init_psutil_posix(void) PyObject *module = Py_InitModule("_psutil_posix", PsutilMethods); #endif -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) || defined(PSUTIL_SUNOS) || defined(PSUTIL_AIX) +#if defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) || defined(PSUTIL_SUNOS) || defined(PSUTIL_AIX) PyModule_AddIntConstant(module, "AF_LINK", AF_LINK); #endif diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/macos/process_info.c similarity index 98% rename from psutil/arch/osx/process_info.c rename to psutil/arch/macos/process_info.c index 40c79a2cdc..de007de486 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/macos/process_info.c @@ -4,7 +4,7 @@ * found in the LICENSE file. * * Helper functions related to fetching process information. - * Used by _psutil_osx module methods. + * Used by _psutil_macos module methods. */ @@ -142,7 +142,7 @@ psutil_get_cmdline(long pid) { mib[2] = (pid_t)pid; if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psosx.py will translate it to ZP. + // to NSP and _psmacos.py will translate it to ZP. if ((errno == EINVAL) && (psutil_pid_exists(pid))) NoSuchProcess(""); else @@ -236,7 +236,7 @@ psutil_get_environ(long pid) { mib[2] = (pid_t)pid; if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psosx.py will translate it to ZP. + // to NSP and _psmacos.py will translate it to ZP. if ((errno == EINVAL) && (psutil_pid_exists(pid))) NoSuchProcess(""); else diff --git a/psutil/arch/osx/process_info.h b/psutil/arch/macos/process_info.h similarity index 100% rename from psutil/arch/osx/process_info.h rename to psutil/arch/macos/process_info.h diff --git a/psutil/arch/osx/smc.c b/psutil/arch/macos/smc.c similarity index 100% rename from psutil/arch/osx/smc.c rename to psutil/arch/macos/smc.c diff --git a/psutil/arch/osx/smc.h b/psutil/arch/macos/smc.h similarity index 100% rename from psutil/arch/osx/smc.h rename to psutil/arch/macos/smc.h diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index 515abf7729..9cefb77573 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -17,7 +17,7 @@ Instructions for running tests (``pip install tox``) then run ``tox`` from within psutil root directory. * Every time a commit is pushed tests are automatically run on Travis - (Linux, OSX) and appveyor (Windows): + (Linux, MACOS) and appveyor (Windows): * Travis builds: https://travis-ci.org/giampaolo/psutil * AppVeyor builds: https://ci.appveyor.com/project/giampaolo/psutil diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f11a6a028e..2b00d4263f 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -36,7 +36,7 @@ from socket import SOCK_STREAM import psutil -from psutil import OSX +from psutil import MACOS from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS @@ -179,7 +179,7 @@ def attempt(exe): else: return exe - if OSX: + if MACOS: exe = \ attempt(sys.executable) or \ attempt(os.path.realpath(sys.executable)) or \ @@ -367,7 +367,7 @@ def create_proc_children_pair(): def create_zombie_proc(): """Create a zombie process and return its PID.""" assert psutil.POSIX - unix_file = tempfile.mktemp(prefix=TESTFILE_PREFIX) if OSX else TESTFN + unix_file = tempfile.mktemp(prefix=TESTFILE_PREFIX) if MACOS else TESTFN src = textwrap.dedent("""\ import os, sys, time, socket, contextlib child_pid = os.fork() @@ -810,7 +810,7 @@ def get_suite(): 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"))] + "macos", "posix", "linux"))] suite = unittest.TestSuite() for tm in testmods: # ...so that the full test paths are printed on screen diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 176e266481..cba835e14f 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -18,9 +18,9 @@ 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 OSX from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS @@ -124,9 +124,9 @@ def compare_procsys_connections(self, pid, proc_cons, kind='all'): try: sys_cons = psutil.net_connections(kind=kind) except psutil.AccessDenied: - # On OSX, system-wide connections are retrieved by iterating + # On MACOS, system-wide connections are retrieved by iterating # over all processes - if OSX: + if MACOS: return else: raise @@ -257,7 +257,7 @@ def test_unix(self): server.close() client.close() - @skip_on_access_denied(only_if=OSX) + @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", @@ -455,7 +455,7 @@ def test_multi_socks(self): @skip_on_access_denied() # See: https://travis-ci.org/giampaolo/psutil/jobs/237566297 - @unittest.skipIf(OSX and TRAVIS, "unreliable on OSX + TRAVIS") + @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() diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 912e811f3b..ac424b54f2 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -21,9 +21,9 @@ 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 @@ -113,21 +113,23 @@ 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 OSX or WINDOWS) + self.assertEqual(hasattr(psutil, "cpu_freq"), + linux or MACOS or WINDOWS) def test_sensors_temperatures(self): - self.assertEqual(hasattr(psutil, "sensors_temperatures"), LINUX or OSX) + self.assertEqual( + hasattr(psutil, "sensors_temperatures"), LINUX or MACOS) def test_sensors_fans(self): - self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX or OSX) + self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX or MACOS) def test_battery(self): self.assertEqual(hasattr(psutil, "sensors_battery"), - LINUX or WINDOWS or FREEBSD or OSX) + LINUX or WINDOWS or FREEBSD or MACOS) def test_proc_environ(self): self.assertEqual(hasattr(psutil.Process, "environ"), - LINUX or OSX or WINDOWS) + LINUX or MACOS or WINDOWS) def test_proc_uids(self): self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) @@ -146,7 +148,7 @@ def test_proc_rlimit(self): def test_proc_io_counters(self): hasit = hasattr(psutil.Process, "io_counters") - self.assertEqual(hasit, False if OSX or SUNOS else True) + self.assertEqual(hasit, False if MACOS or SUNOS else True) def test_proc_num_fds(self): self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX) @@ -224,7 +226,7 @@ def test_disk_partitions(self): @unittest.skipIf(not POSIX, 'POSIX only') @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") - @skip_on_access_denied(only_if=OSX) + @skip_on_access_denied(only_if=MACOS) def test_net_connections(self): with unix_socket_path() as name: with closing(bind_unix_socket(name)): @@ -385,7 +387,7 @@ def exe(self, ret, proc): # 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 OSX + # XXX may fail on MACOS assert os.access(ret, os.X_OK) def pid(self, ret, proc): @@ -431,7 +433,7 @@ def gids(self, ret, proc): # gid == 30 (nodoby); not sure why. for gid in ret: self.assertIsInstance(gid, int) - if not OSX and not NETBSD: + if not MACOS and not NETBSD: self.assertGreaterEqual(gid, 0) self.assertIn(gid, self.all_gids) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_macos.py similarity index 97% rename from psutil/tests/test_osx.py rename to psutil/tests/test_macos.py index bcb2ba4e1a..557af9f95f 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_macos.py @@ -4,14 +4,14 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""OSX specific tests.""" +"""MACOS specific tests.""" import os import re import time import psutil -from psutil import OSX +from psutil import MACOS from psutil.tests import create_zombie_proc from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY @@ -23,7 +23,7 @@ from psutil.tests import unittest -PAGESIZE = os.sysconf("SC_PAGE_SIZE") if OSX else None +PAGESIZE = os.sysconf("SC_PAGE_SIZE") if MACOS else None def sysctl(cmdline): @@ -76,7 +76,7 @@ def human2bytes(s): return int(num * prefix[letter]) -@unittest.skipIf(not OSX, "OSX only") +@unittest.skipIf(not MACOS, "MACOS only") class TestProcess(unittest.TestCase): @classmethod @@ -101,7 +101,7 @@ def test_process_create_time(self): time.strftime("%Y", time.localtime(start_psutil))) -@unittest.skipIf(not OSX, "OSX only") +@unittest.skipIf(not MACOS, "MACOS only") class TestZombieProcessAPIs(unittest.TestCase): @classmethod @@ -162,7 +162,7 @@ def test_memory_maps(self): self.assertRaises(psutil.ZombieProcess, self.p.memory_maps) -@unittest.skipIf(not OSX, "OSX only") +@unittest.skipIf(not MACOS, "MACOS only") class TestSystemAPIs(unittest.TestCase): # --- disk diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 680fe78036..ce08245968 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -25,8 +25,8 @@ import psutil import psutil._common from psutil import LINUX +from psutil import MACOS from psutil import OPENBSD -from psutil import OSX from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS @@ -176,7 +176,7 @@ def call(): def _get_mem(): # By using USS memory it seems it's less likely to bump # into false positives. - if LINUX or WINDOWS or OSX: + if LINUX or WINDOWS or MACOS: return thisproc.memory_full_info().uss else: return thisproc.memory_info().rss @@ -344,8 +344,8 @@ def test_open_files(self): with open(TESTFN, 'w'): self.execute(self.proc.open_files) - # OSX implementation is unbelievably slow - @unittest.skipIf(OSX, "too slow on OSX") + # MACOS implementation is unbelievably slow + @unittest.skipIf(MACOS, "too slow on MACOS") @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") @skip_if_linux() def test_memory_maps(self): @@ -530,7 +530,7 @@ def test_net_io_counters(self): @unittest.skipIf(LINUX, "worthless on Linux (pure python)") - @unittest.skipIf(OSX and os.getuid() != 0, "need root access") + @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") def test_net_connections(self): with create_sockets(): self.execute(psutil.net_connections) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index e9a6f5f63b..b80128c7f2 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -19,8 +19,8 @@ from psutil import AIX from psutil import BSD from psutil import LINUX +from psutil import MACOS from psutil import OPENBSD -from psutil import OSX from psutil import POSIX from psutil import SUNOS from psutil._compat import PY3 @@ -152,7 +152,7 @@ def test_name(self): # 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 OSX this may + # ...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) @@ -194,7 +194,7 @@ def test_name_long_cmdline_nsp_exc(self): p = psutil.Process() self.assertRaises(psutil.NoSuchProcess, p.name) - @unittest.skipIf(OSX or BSD, 'ps -o start not available') + @unittest.skipIf(MACOS or BSD, 'ps -o start not available') def test_create_time(self): time_ps = ps("ps --no-headers -o start -p %s" % self.pid).split(' ')[0] time_psutil = psutil.Process(self.pid).create_time() @@ -309,8 +309,8 @@ def test_pids(self): pids_ps.sort() pids_psutil.sort() - # on OSX and OPENBSD ps doesn't show pid 0 - if OSX or OPENBSD and 0 not in pids_ps: + # 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) self.assertEqual(pids_ps, pids_psutil) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index e295c95b1f..2308196235 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -25,9 +25,9 @@ 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 @@ -504,7 +504,7 @@ def test_rlimit_infinity_value(self): 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 (OSX, Windows) + # but this does not apply across all platforms (MACOS, Windows) p = psutil.Process() if OPENBSD: try: @@ -548,7 +548,7 @@ def test_threads(self): self.assertEqual(athread.system_time, athread[2]) @retry_before_failing() - @skip_on_access_denied(only_if=OSX) + @skip_on_access_denied(only_if=MACOS) @unittest.skipIf(not HAS_THREADS, 'not supported') def test_threads_2(self): sproc = get_test_subprocess() @@ -603,7 +603,7 @@ def test_memory_full_info(self): value = getattr(mem, name) self.assertGreaterEqual(value, 0, msg=(name, value)) self.assertLessEqual(value, total, msg=(name, value, total)) - if LINUX or WINDOWS or OSX: + if LINUX or WINDOWS or MACOS: self.assertGreaterEqual(mem.uss, 0) if LINUX: self.assertGreaterEqual(mem.pss, 0) @@ -668,7 +668,7 @@ def test_memory_percent(self): assert 0 <= ret <= 100, ret assert 0 <= ret <= 100, ret self.assertRaises(ValueError, p.memory_percent, memtype="?!?") - if LINUX or OSX or WINDOWS: + if LINUX or MACOS or WINDOWS: ret = p.memory_percent(memtype='uss') assert 0 <= ret <= 100, ret assert 0 <= ret <= 100, ret @@ -705,7 +705,7 @@ def test_exe(self): self.assertEqual(exe.replace(ver, ''), PYTHON_EXE.replace(ver, '')) except AssertionError: - # Tipically OSX. Really not sure what to do here. + # Tipically MACOS. Really not sure what to do here. pass out = sh([exe, "-c", "import os; print('hey')"]) @@ -825,8 +825,8 @@ def test_nice(self): self.assertEqual( os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice()) # XXX - going back to previous nice value raises - # AccessDenied on OSX - if not OSX: + # AccessDenied on MACOS + if not MACOS: p.nice(0) self.assertEqual(p.nice(), 0) except psutil.AccessDenied: @@ -1317,7 +1317,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): succeed_or_zombie_p_exc(zproc.kill) # ...its parent should 'see' it - # edit: not true on BSD and OSX + # edit: not true on BSD and MACOS # descendants = [x.pid for x in psutil.Process().children( # recursive=True)] # self.assertIn(zpid, descendants) @@ -1327,9 +1327,9 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): # 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 OSX: + if not TRAVIS and MACOS: # For some reason this started failing all of the sudden. - # Maybe they upgraded OSX version? + # 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()]) @@ -1402,7 +1402,7 @@ def clean_dict(d): d.pop("PSUTIL_TESTING", None) d.pop("PLAT", None) d.pop("HOME", None) - if OSX: + if MACOS: d.pop("__CF_USER_TEXT_ENCODING", None) d.pop("VERSIONER_PYTHON_PREFER_32_BIT", None) d.pop("VERSIONER_PYTHON_VERSION", None) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 6081ea5a2d..694a7a3b6a 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -23,9 +23,9 @@ 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 @@ -514,7 +514,7 @@ def test_disk_partitions(self): try: os.stat(disk.mountpoint) except OSError as err: - if TRAVIS and OSX and err.errno == errno.EIO: + if TRAVIS and MACOS and err.errno == errno.EIO: continue # http://mail.python.org/pipermail/python-dev/ # 2012-June/120787.html @@ -630,7 +630,7 @@ def test_net_if_addrs(self): elif addr.ptp: self.assertIsNone(addr.broadcast) - if BSD or OSX or SUNOS: + if BSD or MACOS or SUNOS: if hasattr(socket, "AF_LINK"): self.assertEqual(psutil.AF_LINK, socket.AF_LINK) elif LINUX: @@ -774,7 +774,7 @@ def check_ls(ls): self.assertEqual(len(ls), psutil.cpu_count()) def test_os_constants(self): - names = ["POSIX", "WINDOWS", "LINUX", "OSX", "FREEBSD", "OPENBSD", + names = ["POSIX", "WINDOWS", "LINUX", "MACOS", "FREEBSD", "OPENBSD", "NETBSD", "BSD", "SUNOS"] for name in names: self.assertIsInstance(getattr(psutil, name), bool, msg=name) @@ -799,8 +799,8 @@ def test_os_constants(self): assert psutil.SUNOS names.remove("SUNOS") elif "darwin" in sys.platform.lower(): - assert psutil.OSX - names.remove("OSX") + assert psutil.MACOS + names.remove("MACOS") else: assert psutil.WINDOWS assert not psutil.POSIX diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 6383c9bec1..4144b5c255 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -58,8 +58,8 @@ from contextlib import closing from psutil import BSD +from psutil import MACOS from psutil import OPENBSD -from psutil import OSX from psutil import POSIX from psutil import WINDOWS from psutil._compat import PY3 @@ -285,7 +285,7 @@ def normpath(p): self.assertIsInstance(path, str) -@unittest.skipIf(OSX and TRAVIS, "unreliable on TRAVIS") # TODO +@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") @@ -306,7 +306,7 @@ def expect_exact_path_match(cls): return cls.funky_name in os.listdir(here) -@unittest.skipIf(OSX and TRAVIS, "unreliable on TRAVIS") # TODO +@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): diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 639e9ad769..28ad4bac82 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -79,7 +79,7 @@ 'terminal', 'uids', ] -elif psutil.OSX: +elif psutil.MACOS: names += [ 'cpu_times', 'create_time', diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 018fb0928b..c92cbcb2ec 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -39,9 +39,9 @@ running processes. It implements many functionalities offered by command \ line tools such as: ps, top, lsof, netstat, ifconfig, who, df, kill, free, \ nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap. It \ -currently supports Linux, Windows, OSX, Sun Solaris, FreeBSD, OpenBSD, NetBSD \ -and AIX, both 32-bit and 64-bit architectures, with Python versions from 2.6 \ -to 3.6. PyPy is also known to work. +currently supports Linux, Windows, macOS, Sun Solaris, FreeBSD, OpenBSD, \ +NetBSD and AIX, both 32-bit and 64-bit architectures, with Python versions \ +from 2.6 to 3.6. PyPy is also known to work. What's new ========== diff --git a/scripts/iotop.py b/scripts/iotop.py index 9f76eb1c95..dabe957b52 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -8,7 +8,7 @@ A clone of iotop (http://guichaz.free.fr/iotop/) showing real time disk I/O statistics. -It works on Linux only (FreeBSD and OSX are missing support for IO +It works on Linux only (FreeBSD and macOS are missing support for IO counters). It doesn't work on Windows as curses module is required. diff --git a/scripts/pmap.py b/scripts/pmap.py index 16eebb6090..a509bd733b 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -5,7 +5,7 @@ # found in the LICENSE file. """ -A clone of 'pmap' utility on Linux, 'vmmap' on OSX and 'procstat -v' on BSD. +A clone of 'pmap' utility on Linux, 'vmmap' on macOS and 'procstat -v' on BSD. Report memory map of a process. $ python scripts/pmap.py 32402 diff --git a/scripts/procsmem.py b/scripts/procsmem.py index a28794b9d8..ab9ad0666c 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -41,7 +41,7 @@ import psutil -if not (psutil.LINUX or psutil.OSX or psutil.WINDOWS): +if not (psutil.LINUX or psutil.MACOS or psutil.WINDOWS): sys.exit("platform not supported") diff --git a/setup.py b/setup.py index b0343212b5..03e315b548 100755 --- a/setup.py +++ b/setup.py @@ -28,16 +28,16 @@ # ...so we can import _common.py sys.path.insert(0, os.path.join(HERE, "psutil")) +from _common import AIX # NOQA from _common import BSD # NOQA from _common import FREEBSD # NOQA from _common import LINUX # NOQA +from _common import MACOS # NOQA from _common import NETBSD # NOQA from _common import OPENBSD # NOQA -from _common import OSX # NOQA from _common import POSIX # NOQA from _common import SUNOS # NOQA from _common import WINDOWS # NOQA -from _common import AIX # NOQA macros = [] @@ -145,14 +145,14 @@ def get_winver(): # extra_link_args=["/DEBUG"] ) -elif OSX: - macros.append(("PSUTIL_OSX", 1)) +elif MACOS: + macros.append(("PSUTIL_MACOS", 1)) ext = Extension( - 'psutil._psutil_osx', + 'psutil._psutil_macos', sources=sources + [ - 'psutil/_psutil_osx.c', - 'psutil/arch/osx/process_info.c', - 'psutil/arch/osx/smc.c', + 'psutil/_psutil_macos.c', + 'psutil/arch/macos/process_info.c', + 'psutil/arch/macos/smc.c', ], define_macros=macros, extra_link_args=[ From 05066d9744f0b981f612c71eecace817a2ba7a08 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 29 Jun 2018 00:50:08 +0200 Subject: [PATCH 0035/1714] winmake: add upload-wheels cmd --- docs/index.rst | 2 +- scripts/internal/winmake.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 47167d7642..af207702af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,7 +32,7 @@ psutil currently supports the following platforms: - **Linux** - **Windows** -- **macOS**, +- **macOS** - **FreeBSD, OpenBSD**, **NetBSD** - **Sun Solaris** - **AIX** diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index ff3a648914..ffdfc291eb 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -227,6 +227,13 @@ def wheel(): sh("%s setup.py bdist_wheel" % PYTHON) +@cmd +def upload_wheels(): + """Upload wheel files on PYPI.""" + build() + sh("%s -m twine upload dist/*.whl" % PYTHON) + + @cmd def install_pip(): """Install pip""" From 1b09b5fff78f705dfb42458726ff9789c26f6f21 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 29 Jun 2018 16:37:47 +0200 Subject: [PATCH 0036/1714] revert file renaming of macos files; get them back to 'osx' prefix --- MANIFEST.in | 14 +++++------ Makefile | 2 +- psutil/__init__.py | 2 +- psutil/{_psmacos.py => _psosx.py} | 2 +- psutil/{_psutil_macos.c => _psutil_osx.c} | 26 ++++++++++----------- psutil/_psutil_posix.c | 16 ++++++------- psutil/arch/{macos => osx}/process_info.c | 6 ++--- psutil/arch/{macos => osx}/process_info.h | 0 psutil/arch/{macos => osx}/smc.c | 0 psutil/arch/{macos => osx}/smc.h | 0 psutil/tests/__init__.py | 2 +- psutil/tests/{test_macos.py => test_osx.py} | 0 setup.py | 10 ++++---- 13 files changed, 40 insertions(+), 40 deletions(-) rename psutil/{_psmacos.py => _psosx.py} (99%) rename psutil/{_psutil_macos.c => _psutil_osx.c} (99%) rename psutil/arch/{macos => osx}/process_info.c (98%) rename psutil/arch/{macos => osx}/process_info.h (100%) rename psutil/arch/{macos => osx}/smc.c (100%) rename psutil/arch/{macos => osx}/smc.h (100%) rename psutil/tests/{test_macos.py => test_osx.py} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index c6d8c550a0..146cd92dfb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,7 +28,7 @@ include psutil/_exceptions.py include psutil/_psaix.py include psutil/_psbsd.py include psutil/_pslinux.py -include psutil/_psmacos.py +include psutil/_psosx.py include psutil/_psposix.py include psutil/_pssunos.py include psutil/_psutil_aix.c @@ -36,7 +36,7 @@ include psutil/_psutil_bsd.c include psutil/_psutil_common.c include psutil/_psutil_common.h include psutil/_psutil_linux.c -include psutil/_psutil_macos.c +include psutil/_psutil_osx.c include psutil/_psutil_posix.c include psutil/_psutil_posix.h include psutil/_psutil_sunos.c @@ -55,16 +55,16 @@ include psutil/arch/freebsd/specific.c include psutil/arch/freebsd/specific.h include psutil/arch/freebsd/sys_socks.c include psutil/arch/freebsd/sys_socks.h -include psutil/arch/macos/process_info.c -include psutil/arch/macos/process_info.h -include psutil/arch/macos/smc.c -include psutil/arch/macos/smc.h include psutil/arch/netbsd/socks.c include psutil/arch/netbsd/socks.h include psutil/arch/netbsd/specific.c include psutil/arch/netbsd/specific.h include psutil/arch/openbsd/specific.c include psutil/arch/openbsd/specific.h +include psutil/arch/osx/process_info.c +include psutil/arch/osx/process_info.h +include psutil/arch/osx/smc.c +include psutil/arch/osx/smc.h include psutil/arch/solaris/environ.c include psutil/arch/solaris/environ.h include psutil/arch/solaris/v10/ifaddrs.c @@ -88,7 +88,7 @@ include psutil/tests/test_bsd.py include psutil/tests/test_connections.py include psutil/tests/test_contracts.py include psutil/tests/test_linux.py -include psutil/tests/test_macos.py +include psutil/tests/test_osx.py include psutil/tests/test_memory_leaks.py include psutil/tests/test_misc.py include psutil/tests/test_posix.py diff --git a/Makefile b/Makefile index 58c0d39612..51047383fa 100644 --- a/Makefile +++ b/Makefile @@ -142,7 +142,7 @@ test-posix: ## POSIX specific tests. test-platform: ## Run specific platform tests only. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "MACOS", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(TEST_PREFIX) $(PYTHON) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} install diff --git a/psutil/__init__.py b/psutil/__init__.py index affc5c8c3f..43e9aa7e9d 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -153,7 +153,7 @@ from ._pswindows import CONN_DELETE_TCB # NOQA elif MACOS: - from . import _psmacos as _psplatform + from . import _psosx as _psplatform elif BSD: from . import _psbsd as _psplatform diff --git a/psutil/_psmacos.py b/psutil/_psosx.py similarity index 99% rename from psutil/_psmacos.py rename to psutil/_psosx.py index a719bedaa1..6c77dc6917 100644 --- a/psutil/_psmacos.py +++ b/psutil/_psosx.py @@ -14,7 +14,7 @@ from . import _common from . import _psposix -from . import _psutil_macos as cext +from . import _psutil_osx as cext from . import _psutil_posix as cext_posix from ._common import AF_INET6 from ._common import conn_tmap diff --git a/psutil/_psutil_macos.c b/psutil/_psutil_osx.c similarity index 99% rename from psutil/_psutil_macos.c rename to psutil/_psutil_osx.c index 8fc3140f62..e3250caba6 100644 --- a/psutil/_psutil_macos.c +++ b/psutil/_psutil_osx.c @@ -43,8 +43,8 @@ #include "_psutil_common.h" #include "_psutil_posix.h" -#include "arch/macos/process_info.h" -#include "arch/macos/smc.h" +#include "arch/osx/process_info.h" +#include "arch/osx/smc.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -342,7 +342,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - // get the commandline, defined in arch/macos/process_info.c + // get the commandline, defined in arch/osx/process_info.c py_retlist = psutil_get_cmdline(pid); return py_retlist; } @@ -359,7 +359,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - // get the environment block, defined in arch/macos/process_info.c + // get the environment block, defined in arch/osx/process_info.c py_retdict = psutil_get_environ(pid); return py_retdict; } @@ -2066,13 +2066,13 @@ struct module_state { #if PY_MAJOR_VERSION >= 3 static int -psutil_macos_traverse(PyObject *m, visitproc visit, void *arg) { +psutil_osx_traverse(PyObject *m, visitproc visit, void *arg) { Py_VISIT(GETSTATE(m)->error); return 0; } static int -psutil_macos_clear(PyObject *m) { +psutil_osx_clear(PyObject *m) { Py_CLEAR(GETSTATE(m)->error); return 0; } @@ -2080,31 +2080,31 @@ psutil_macos_clear(PyObject *m) { static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - "psutil_macos", + "psutil_osx", NULL, sizeof(struct module_state), PsutilMethods, NULL, - psutil_macos_traverse, - psutil_macos_clear, + psutil_osx_traverse, + psutil_osx_clear, NULL }; #define INITERROR return NULL -PyMODINIT_FUNC PyInit__psutil_macos(void) +PyMODINIT_FUNC PyInit__psutil_osx(void) #else #define INITERROR return void -init_psutil_macos(void) +init_psutil_osx(void) #endif { #if PY_MAJOR_VERSION >= 3 PyObject *module = PyModule_Create(&moduledef); #else - PyObject *module = Py_InitModule("_psutil_macos", PsutilMethods); + PyObject *module = Py_InitModule("_psutil_osx", PsutilMethods); #endif PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); // process status constants, defined in: @@ -2130,7 +2130,7 @@ init_psutil_macos(void) // Exception. ZombieProcessError = PyErr_NewException( - "_psutil_macos.ZombieProcessError", NULL, NULL); + "_psutil_osx.ZombieProcessError", NULL, NULL); Py_INCREF(ZombieProcessError); PyModule_AddObject(module, "ZombieProcessError", ZombieProcessError); diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index e9a8f2d95c..c851abbc1a 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -28,7 +28,7 @@ #include #include #include -#elif defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) +#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) #include #include #include @@ -71,7 +71,7 @@ psutil_pid_exists(long pid) { #endif } -#if defined(PSUTIL_MACOS) +#if defined(PSUTIL_OSX) ret = kill((pid_t)pid , 0); #else ret = kill(pid , 0); @@ -143,7 +143,7 @@ psutil_posix_getpriority(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; -#ifdef PSUTIL_MACOS +#ifdef PSUTIL_OSX priority = getpriority(PRIO_PROCESS, (id_t)pid); #else priority = getpriority(PRIO_PROCESS, pid); @@ -166,7 +166,7 @@ psutil_posix_setpriority(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "li", &pid, &priority)) return NULL; -#ifdef PSUTIL_MACOS +#ifdef PSUTIL_OSX retval = setpriority(PRIO_PROCESS, (id_t)pid, priority); #else retval = setpriority(PRIO_PROCESS, pid, priority); @@ -221,7 +221,7 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { len = lladdr->sll_halen; data = (const char *)lladdr->sll_addr; } -#elif defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) +#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) else if (addr->sa_family == AF_LINK) { // Note: prior to Python 3.4 socket module does not expose // AF_LINK so we'll do. @@ -433,7 +433,7 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { /* * net_if_stats() macOS/BSD implementation. */ -#if defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) int psutil_get_nic_speed(int ifm_active) { // Determine NIC speed. Taken from: @@ -638,7 +638,7 @@ PsutilMethods[] = { "Retrieve NIC MTU"}, {"net_if_flags", psutil_net_if_flags, METH_VARARGS, "Retrieve NIC flags"}, -#if defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, "Return NIC stats."}, #endif @@ -698,7 +698,7 @@ void init_psutil_posix(void) PyObject *module = Py_InitModule("_psutil_posix", PsutilMethods); #endif -#if defined(PSUTIL_BSD) || defined(PSUTIL_MACOS) || defined(PSUTIL_SUNOS) || defined(PSUTIL_AIX) +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) || defined(PSUTIL_SUNOS) || defined(PSUTIL_AIX) PyModule_AddIntConstant(module, "AF_LINK", AF_LINK); #endif diff --git a/psutil/arch/macos/process_info.c b/psutil/arch/osx/process_info.c similarity index 98% rename from psutil/arch/macos/process_info.c rename to psutil/arch/osx/process_info.c index de007de486..40c79a2cdc 100644 --- a/psutil/arch/macos/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -4,7 +4,7 @@ * found in the LICENSE file. * * Helper functions related to fetching process information. - * Used by _psutil_macos module methods. + * Used by _psutil_osx module methods. */ @@ -142,7 +142,7 @@ psutil_get_cmdline(long pid) { mib[2] = (pid_t)pid; if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psmacos.py will translate it to ZP. + // to NSP and _psosx.py will translate it to ZP. if ((errno == EINVAL) && (psutil_pid_exists(pid))) NoSuchProcess(""); else @@ -236,7 +236,7 @@ psutil_get_environ(long pid) { mib[2] = (pid_t)pid; if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psmacos.py will translate it to ZP. + // to NSP and _psosx.py will translate it to ZP. if ((errno == EINVAL) && (psutil_pid_exists(pid))) NoSuchProcess(""); else diff --git a/psutil/arch/macos/process_info.h b/psutil/arch/osx/process_info.h similarity index 100% rename from psutil/arch/macos/process_info.h rename to psutil/arch/osx/process_info.h diff --git a/psutil/arch/macos/smc.c b/psutil/arch/osx/smc.c similarity index 100% rename from psutil/arch/macos/smc.c rename to psutil/arch/osx/smc.c diff --git a/psutil/arch/macos/smc.h b/psutil/arch/osx/smc.h similarity index 100% rename from psutil/arch/macos/smc.h rename to psutil/arch/osx/smc.h diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 2b00d4263f..d293498e85 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -810,7 +810,7 @@ def get_suite(): x.startswith('test_memory_leaks')] if "WHEELHOUSE_UPLOADER_USERNAME" in os.environ: testmods = [x for x in testmods if not x.endswith(( - "macos", "posix", "linux"))] + "osx", "posix", "linux"))] suite = unittest.TestSuite() for tm in testmods: # ...so that the full test paths are printed on screen diff --git a/psutil/tests/test_macos.py b/psutil/tests/test_osx.py similarity index 100% rename from psutil/tests/test_macos.py rename to psutil/tests/test_osx.py diff --git a/setup.py b/setup.py index 03e315b548..adc7a0e2dd 100755 --- a/setup.py +++ b/setup.py @@ -146,13 +146,13 @@ def get_winver(): ) elif MACOS: - macros.append(("PSUTIL_MACOS", 1)) + macros.append(("PSUTIL_OSX", 1)) ext = Extension( - 'psutil._psutil_macos', + 'psutil._psutil_osx', sources=sources + [ - 'psutil/_psutil_macos.c', - 'psutil/arch/macos/process_info.c', - 'psutil/arch/macos/smc.c', + 'psutil/_psutil_osx.c', + 'psutil/arch/osx/process_info.c', + 'psutil/arch/osx/smc.c', ], define_macros=macros, extra_link_args=[ From f440b6e3c6e9bbcd3e57f9c0d2f78aef36a249b1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Jul 2018 15:16:47 +0200 Subject: [PATCH 0037/1714] appveyor: retire 3.5, add 3.7 --- appveyor.yml | 25 +++++++------------------ scripts/internal/download_exes.py | 2 +- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 436faadbc4..ee434903ad 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,27 +3,18 @@ os: Visual Studio 2015 environment: - global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script intepreter # See: http://stackoverflow.com/a/13751649/163740 WITH_COMPILER: "cmd /E:ON /V:ON /C .\\.ci\\appveyor\\run_with_compiler.cmd" - matrix: - # Pre-installed Python versions, which Appveyor may upgrade to - # a later point release. - # 32 bits - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.x" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "32" @@ -32,16 +23,16 @@ environment: PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python37" + PYTHON_VERSION: "3.7.x" + PYTHON_ARCH: "32" + # 64 bits - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.x" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" @@ -50,11 +41,9 @@ environment: PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" - # Also build on a Python version not pre-installed by Appveyor. - # See: https://github.com/ogrisel/python-appveyor-demo/issues/10 - # - PYTHON: "C:\\Python266" - # PYTHON_VERSION: "2.6.6" - # PYTHON_ARCH: "32" + - PYTHON: "C:\\Python37-x64" + PYTHON_VERSION: "3.7.x" + PYTHON_ARCH: "64" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index 1b00442883..d138f0d360 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -25,7 +25,7 @@ BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.4', '3.5', '3.6'] +PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7'] TIMEOUT = 30 COLORS = True From cf9d1cd9045e4d73ecd2d7308183d254081bc9b3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 16 Jul 2018 00:10:03 +0200 Subject: [PATCH 0038/1714] fix #1279: catch and skip ENODEV in net_if_stat() --- HISTORY.rst | 1 + docs/index.rst | 8 ++++---- psutil/_psbsd.py | 18 ++++++++++++------ psutil/_pslinux.py | 14 ++++++++++---- psutil/_psosx.py | 18 ++++++++++++------ psutil/tests/test_system.py | 10 ++++++++++ 6 files changed, 49 insertions(+), 20 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 67f967eda9..a5382fbcfe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ XXXX-XX-XX task_for_pid() syscall. AccessDenied is now raised instead. - 1278_: [macOS] Process.threads() incorrectly return microseconds instead of seconds. (patch by Nikhil Marathe) +- 1279_: [Linux, macOS, BSD] net_if_stats() may return ENODEV. 5.4.6 ===== diff --git a/docs/index.rst b/docs/index.rst index af207702af..d17d2c657d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1880,11 +1880,11 @@ Process class (OpenBSD) "laddr" and "raddr" fields for UNIX sockets are always set to "". This is a limitation of the OS. - .. versionchanged:: 5.3.0 : "laddr" and "raddr" are named tuples. + .. note:: + (AIX) :class:`psutil.AccessDenied` is always raised unless running + as root (lsof does the same). - .. note:: - (AIX) :class:`psutil.AccessDenied` is always raised unless running - as root (lsof does the same). + .. versionchanged:: 5.3.0 : "laddr" and "raddr" are named tuples. .. method:: is_running() diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 83f38d55e9..7f4bcb6de6 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -345,12 +345,18 @@ def net_if_stats(): names = net_io_counters().keys() ret = {} for name in names: - mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) - duplex, speed = cext_posix.net_if_duplex_speed(name) - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + try: + mtu = cext_posix.net_if_mtu(name) + isup = 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 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) return ret diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b197dba331..85ffe66a5b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1002,10 +1002,16 @@ def net_if_stats(): names = net_io_counters().keys() ret = {} for name in names: - mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) - duplex, speed = cext.net_if_duplex_speed(name) - ret[name] = _common.snicstats(isup, duplex_map[duplex], speed, mtu) + try: + mtu = cext_posix.net_if_mtu(name) + isup = 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: + ret[name] = _common.snicstats(isup, duplex_map[duplex], speed, mtu) return ret diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 6c77dc6917..d059449af8 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -402,12 +402,18 @@ def net_if_stats(): names = net_io_counters().keys() ret = {} for name in names: - mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) - duplex, speed = cext_posix.net_if_duplex_speed(name) - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + try: + mtu = cext_posix.net_if_mtu(name) + isup = 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 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) return ret diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 694a7a3b6a..18cbb5d8bc 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -671,6 +671,16 @@ def test_net_if_stats(self): 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, From 07b42c153d3d2ad11d49e8876a2128ac00f0cffb Mon Sep 17 00:00:00 2001 From: Ashish Billore Date: Tue, 24 Jul 2018 17:39:19 +0900 Subject: [PATCH 0039/1714] Update index.rst (#1308) Updated Process section --- docs/index.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index d17d2c657d..35f135683e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -848,10 +848,8 @@ Functions Cached :class:`Process` instances are checked for identity so that you're safe in case a PID has been reused by another process, in which case the cached instance is updated. - This is should be preferred over :func:`psutil.pids()` for iterating over - processes. - Sorting order in which processes are returned is - based on their PID. + This is preferred over :func:`psutil.pids()` for iterating over processes. + Sorting order in which processes are returned is based on their PID. *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. If *attrs* is specified :meth:`Process.as_dict()` is called internally and the resulting dict is stored as a ``info`` attribute which is attached to the From e2d94439387d176f5f84144649e661b0cfcb5969 Mon Sep 17 00:00:00 2001 From: sylvainduchesne <35115147+sylvainduchesne@users.noreply.github.com> Date: Tue, 24 Jul 2018 16:09:39 -0400 Subject: [PATCH 0040/1714] retain GIL when querying connections table (#1306) fixes issue #1294 --- psutil/_psutil_windows.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index ba898f60b0..1d44dd0ba0 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1536,7 +1536,6 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, DWORD error = ERROR_INSUFFICIENT_BUFFER; *size = 0; *data = NULL; - Py_BEGIN_ALLOW_THREADS; error = call(NULL, size, FALSE, address_family, TCP_TABLE_OWNER_PID_ALL, 0); while (error == ERROR_INSUFFICIENT_BUFFER) @@ -1553,7 +1552,6 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, *data = NULL; } } - Py_END_ALLOW_THREADS; return error; } @@ -1577,7 +1575,6 @@ static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, DWORD error = ERROR_INSUFFICIENT_BUFFER; *size = 0; *data = NULL; - Py_BEGIN_ALLOW_THREADS; error = call(NULL, size, FALSE, address_family, UDP_TABLE_OWNER_PID, 0); while (error == ERROR_INSUFFICIENT_BUFFER) @@ -1594,7 +1591,6 @@ static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, *data = NULL; } } - Py_END_ALLOW_THREADS; return error; } From 35cb1ab6e7cf2ef0942a135d812eebdce5af958d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 24 Jul 2018 22:11:48 +0200 Subject: [PATCH 0041/1714] give CREDITS to @sylvainduchesne for #1294 --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index 89a91008e4..edf1799dd2 100644 --- a/CREDITS +++ b/CREDITS @@ -547,3 +547,7 @@ I: 1278 N: Alex Manuskin W: https://github.com/amanusk I: 1284 + +N: sylvainduchesne +W: https://github.com/sylvainduchesne +I: 1294 diff --git a/HISTORY.rst b/HISTORY.rst index a5382fbcfe..2a39e1c613 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,8 @@ XXXX-XX-XX - 1278_: [macOS] Process.threads() incorrectly return microseconds instead of seconds. (patch by Nikhil Marathe) - 1279_: [Linux, macOS, BSD] net_if_stats() may return ENODEV. +- 1294_: [Windows] psutil.Process().connections() may sometime fail with + MemoryError. (patch by sylvainduchesne) 5.4.6 ===== From df46de5d4df8da20f6939f121a151a646cf88ed2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 Jul 2018 20:54:17 +0200 Subject: [PATCH 0042/1714] fix #1309: add STATUS_PARKED constant and fix STATUS_IDLE (both on linux) --- HISTORY.rst | 3 +++ docs/index.rst | 4 +++- psutil/__init__.py | 6 ++++-- psutil/_common.py | 5 +++-- psutil/_pslinux.py | 9 +++++++-- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2a39e1c613..602ce3d761 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,7 @@ XXXX-XX-XX (patch by Alex Manuskin) - 1286_: [macOS] psutil.OSX constant is now deprecated in favor of new psutil.MACOS. +- 1309_: [Linux] added psutil.STATUS_PARKED constant for Process.status(). **Bug fixes** @@ -21,6 +22,8 @@ XXXX-XX-XX - 1279_: [Linux, macOS, BSD] net_if_stats() may return ENODEV. - 1294_: [Windows] psutil.Process().connections() may sometime fail with MemoryError. (patch by sylvainduchesne) +- 1309_: [Linux] Process.status() is unable to recognie "idle" and "parked" + statuses (returns '?'). 5.4.6 ===== diff --git a/docs/index.rst b/docs/index.rst index 35f135683e..41b7175f96 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2169,7 +2169,8 @@ Constants .. data:: STATUS_DEAD .. data:: STATUS_WAKE_KILL .. data:: STATUS_WAKING -.. data:: STATUS_IDLE (macOS, FreeBSD) +.. data:: STATUS_PARKED (Linux) +.. data:: STATUS_IDLE (Linux, macOS, FreeBSD) .. data:: STATUS_LOCKED (FreeBSD) .. data:: STATUS_WAITING (FreeBSD) .. data:: STATUS_SUSPENDED (NetBSD) @@ -2178,6 +2179,7 @@ Constants Returned by :meth:`psutil.Process.status()`. .. versionadded:: 3.4.1 STATUS_SUSPENDED (NetBSD) + .. versionadded:: 5.5.0 STATUS_PARKED (Linux) .. _const-conn: .. data:: CONN_ESTABLISHED diff --git a/psutil/__init__.py b/psutil/__init__.py index 43e9aa7e9d..299fc1ea84 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -48,13 +48,14 @@ from ._common import STATUS_DEAD from ._common import STATUS_DISK_SLEEP -from ._common import STATUS_IDLE # bsd +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 # bsd +from ._common import STATUS_WAITING from ._common import STATUS_WAKING from ._common import STATUS_ZOMBIE @@ -189,6 +190,7 @@ "STATUS_RUNNING", "STATUS_IDLE", "STATUS_SLEEPING", "STATUS_DISK_SLEEP", "STATUS_STOPPED", "STATUS_TRACING_STOP", "STATUS_ZOMBIE", "STATUS_DEAD", "STATUS_WAKING", "STATUS_LOCKED", "STATUS_WAITING", "STATUS_LOCKED", + "STATUS_PARKED", "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", diff --git a/psutil/_common.py b/psutil/_common.py index 6fcf8fe058..2cc3939a1c 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -55,7 +55,7 @@ 'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED', 'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED', 'STATUS_TRACING_STOP', 'STATUS_WAITING', 'STATUS_WAKE_KILL', - 'STATUS_WAKING', 'STATUS_ZOMBIE', + 'STATUS_WAKING', 'STATUS_ZOMBIE', 'STATUS_PARKED', # named tuples 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile', 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart', @@ -100,10 +100,11 @@ STATUS_DEAD = "dead" STATUS_WAKE_KILL = "wake-kill" STATUS_WAKING = "waking" -STATUS_IDLE = "idle" # FreeBSD, macOS +STATUS_IDLE = "idle" # Linux, macOS, FreeBSD STATUS_LOCKED = "locked" # FreeBSD STATUS_WAITING = "waiting" # FreeBSD STATUS_SUSPENDED = "suspended" # NetBSD +STATUS_PARKED = "parked" # Linux # Process.connections() and psutil.net_connections() CONN_ESTABLISHED = "ESTABLISHED" diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 85ffe66a5b..ffa3a8a328 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -111,7 +111,10 @@ class IOPriority(enum.IntEnum): globals().update(IOPriority.__members__) -# taken from /fs/proc/array.c +# See: +# https://github.com/torvalds/linux/blame/master/fs/proc/array.c +# ...and (TASK_* constants): +# https://github.com/torvalds/linux/blob/master/include/linux/sched.h PROC_STATUSES = { "R": _common.STATUS_RUNNING, "S": _common.STATUS_SLEEPING, @@ -122,7 +125,9 @@ class IOPriority(enum.IntEnum): "X": _common.STATUS_DEAD, "x": _common.STATUS_DEAD, "K": _common.STATUS_WAKE_KILL, - "W": _common.STATUS_WAKING + "W": _common.STATUS_WAKING, + "I": _common.STATUS_IDLE, + "P": _common.STATUS_PARKED, } # https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h From be1a35ff47fd513f6e808ac032ee7305ded1c27e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 27 Jul 2018 17:24:10 +0200 Subject: [PATCH 0043/1714] disambiguate TESTFN for parallel testing --- psutil/tests/__init__.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index d293498e85..a483ecaadd 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -137,7 +137,16 @@ # --- 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') @@ -206,8 +215,13 @@ def attempt(exe): _testfiles_created = set() +def logstderr(s): + print(s, file=sys.stderr) + + @atexit.register -def _cleanup_files(): +def cleanup_test_files(): + logstderr("executing cleanup_test_files() atexit function") DEVNULL.close() for name in os.listdir(u('.')): if isinstance(name, unicode): @@ -215,11 +229,13 @@ def _cleanup_files(): else: prefix = TESTFILE_PREFIX if name.startswith(prefix): + logstderr("removing temporary test file %r" % name) try: safe_rmpath(name) except Exception: traceback.print_exc() for path in _testfiles_created: + logstderr("removing temporary test file %r" % path) try: safe_rmpath(path) except Exception: @@ -228,7 +244,8 @@ def _cleanup_files(): # this is executed first @atexit.register -def _cleanup_procs(): +def cleanup_test_procs(): + logstderr("executing cleanup_test_procs() atexit function") reap_children(recursive=True) From 196dcb27330c0b93f2b850d20c450216721dc8b7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Jul 2018 22:21:22 +0200 Subject: [PATCH 0044/1714] fix wrong reference link in doc --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 41b7175f96..a64448e3dd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1070,9 +1070,9 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | Linux | Windows | macOS | BSD | SunOS | AIX | +==============================+===============================+==============================+==============================+==========================+==========================+ - | :meth:`cpu_num` | :meth:`cpu_percent` | :meth:`cpu_percent` | :meth:`cpu_num` | :meth:`name` | :meth:`name` | + | :meth:`cpu_num` | :meth:`~Process.cpu_percent` | :meth:`~Process.cpu_percent` | :meth:`cpu_num` | :meth:`name` | :meth:`name` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`cpu_percent` | :meth:`cpu_times` | :meth:`cpu_times` | :meth:`cpu_percent` | :meth:`cmdline` | :meth:`cmdline` | + | :meth:`~Process.cpu_percent` | :meth:`cpu_times` | :meth:`cpu_times` | :meth:`~Process.cpu_percent` | :meth:`cmdline` | :meth:`cmdline` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`cpu_times` | :meth:`io_counters()` | :meth:`memory_info` | :meth:`cpu_times` | :meth:`create_time` | :meth:`create_time` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ From 45c0ebc90e98cfe3a44d27de1a815f1fd8c0fc9c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 29 Jul 2018 00:20:09 +0200 Subject: [PATCH 0045/1714] disk_io_counters() - linux: mimic iostat behavior (#1313) Fix #1244, #1305, #1312. disk_io_counters(perdisk=False) on Linux was counting disk device + partitions(s) twice --- HISTORY.rst | 2 + docs/index.rst | 4 ++ psutil/__init__.py | 3 +- psutil/_pslinux.py | 78 +++++++++++++++++++-------------- psutil/tests/test_linux.py | 89 ++++++++++++++++++++++++++------------ 5 files changed, 115 insertions(+), 61 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 602ce3d761..3dcafa3c2f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,6 +24,8 @@ XXXX-XX-XX MemoryError. (patch by sylvainduchesne) - 1309_: [Linux] Process.status() is unable to recognie "idle" and "parked" statuses (returns '?'). +- 1313_: [Linux] disk_io_counters() can report inflated IO counters due to + erroneously counting base disk device and its partition(s) twice. 5.4.6 ===== diff --git a/docs/index.rst b/docs/index.rst index a64448e3dd..097d828dcb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -421,6 +421,8 @@ Disks cache. On Windows it may be ncessary to issue ``diskperf -y`` command from cmd.exe first in order to enable IO counters. + On diskless machines this function will return ``None`` or ``{}`` if + *perdisk* is ``True``. >>> import psutil >>> psutil.disk_io_counters() @@ -474,6 +476,8 @@ Network numbers will always be increasing or remain the same, but never decrease. ``net_io_counters.cache_clear()`` can be used to invalidate the *nowrap* cache. + On machines with no network iterfaces this function will return ``None`` or + ``{}`` if *pernic* is ``True``. >>> import psutil >>> psutil.net_io_counters() diff --git a/psutil/__init__.py b/psutil/__init__.py index 299fc1ea84..291d0d16a8 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2013,7 +2013,8 @@ def disk_io_counters(perdisk=False, nowrap=True): On recent Windows versions 'diskperf -y' command may need to be executed first otherwise this function won't find any disk. """ - rawdict = _psplatform.disk_io_counters() + kwargs = dict(perdisk=perdisk) if LINUX else {} + rawdict = _psplatform.disk_io_counters(**kwargs) if not rawdict: return {} if perdisk else None if nowrap: diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ffa3a8a328..0aa507bd28 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -252,12 +252,12 @@ def file_flags_to_mode(flags): return mode -def get_sector_size(partition): - """Return the sector size of a partition. - Used by disk_io_counters(). - """ +def get_sector_size(name): + """Return the sector size of a storage device.""" + # Some devices may have a slash in their name (e.g. cciss/c0d0...). + name = name.replace('/', '!') try: - with open("/sys/block/%s/queue/hw_sector_size" % partition, "rt") as f: + with open("/sys/block/%s/queue/hw_sector_size" % name, "rt") as f: return int(f.read()) except (IOError, ValueError): # man iostat states that sectors are equivalent with blocks and @@ -265,6 +265,25 @@ def get_sector_size(partition): return SECTOR_SIZE_FALLBACK +def is_storage_device(name): + """Return True if the given name refers to a physical (e.g. "sda", + "nvme0n1") or virtual (e.g. "loop1", "ram") storage device. + In case name refers to a device's partition (e.g. "sda1", "nvme0n1p1") + this is supposed to return False. + """ + # Readapted 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...). + name = name.replace('/', '!') + including_virtual = True + if including_virtual: + path = "/sys/block/%s" % name + else: + path = "/sys/block/%s/device" % name + return os.access(path, os.F_OK) + + @memoize def set_scputimes_ntuple(procfs_path): """Set a namedtuple of variable fields depending on the CPU times @@ -1028,32 +1047,11 @@ def net_if_stats(): disk_usage = _psposix.disk_usage -def disk_io_counters(): +def disk_io_counters(perdisk=False): """Return disk I/O statistics for every disk installed on the system as a dict of raw tuples. """ - # determine partitions we want to look for - def get_partitions(): - partitions = [] - with open_text("%s/partitions" % get_procfs_path()) as f: - lines = f.readlines()[2:] - for line in reversed(lines): - _, _, _, name = line.split() - if name[-1].isdigit(): - # we're dealing with a partition (e.g. 'sda1'); 'sda' will - # also be around but we want to omit it - partitions.append(name) - else: - if not partitions or not partitions[-1].startswith(name): - # we're dealing with a disk entity for which no - # partitions have been defined (e.g. 'sda' but - # 'sda1' was not around), see: - # https://github.com/giampaolo/psutil/issues/338 - partitions.append(name) - return partitions - retdict = {} - partitions = get_partitions() with open_text("%s/diskstats" % get_procfs_path()) as f: lines = f.readlines() for line in lines: @@ -1091,12 +1089,26 @@ def get_partitions(): else: raise ValueError("not sure how to interpret line %r" % line) - if name in partitions: - ssize = get_sector_size(name) - rbytes *= ssize - wbytes *= ssize - retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, - reads_merged, writes_merged, busy_time) + if not perdisk and not is_storage_device(name): + # perdisk=False means we want to calculate totals so we skip + # partitions (e.g. 'sda1', 'nvme0n1p1') and only include + # base disk devices (e.g. 'sda', 'nvme0n1'). Base disks + # include a total of all their partitions + some extra size + # of their own: + # $ cat /proc/diskstats + # 259 0 sda 10485760 ... + # 259 1 sda1 5186039 ... + # 259 1 sda2 5082039 ... + # See: + # https://github.com/giampaolo/psutil/pull/1313 + continue + + ssize = get_sector_size(name) + rbytes *= ssize + wbytes *= ssize + retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, + reads_merged, writes_merged, busy_time) + return retdict diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 9ea59b617b..761f790474 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -986,15 +986,10 @@ def test_disk_io_counters_kernel_2_4_mocked(self): # Tests /proc/diskstats parsing format for 2.4 kernels, see: # https://github.com/giampaolo/psutil/issues/767 with mock_open_content( - '/proc/partitions', - textwrap.dedent("""\ - major minor #blocks name - - 8 0 488386584 hda - """)): - with mock_open_content( - '/proc/diskstats', - " 3 0 1 hda 2 3 4 5 6 7 8 9 10 11 12"): + '/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) @@ -1011,15 +1006,10 @@ def test_disk_io_counters_kernel_2_6_full_mocked(self): # lines reporting all metrics: # https://github.com/giampaolo/psutil/issues/767 with mock_open_content( - '/proc/partitions', - textwrap.dedent("""\ - major minor #blocks name - - 8 0 488386584 hda - """)): - with mock_open_content( - '/proc/diskstats', - " 3 0 hda 1 2 3 4 5 6 7 8 9 10 11"): + '/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) @@ -1038,15 +1028,10 @@ def test_disk_io_counters_kernel_2_6_limited_mocked(self): # (instead of a disk). See: # https://github.com/giampaolo/psutil/issues/767 with mock_open_content( - '/proc/partitions', - textwrap.dedent("""\ - major minor #blocks name - - 8 0 488386584 hda - """)): - with mock_open_content( - '/proc/diskstats', - " 3 1 hda 1 2 3 4"): + '/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) @@ -1059,6 +1044,56 @@ def test_disk_io_counters_kernel_2_6_limited_mocked(self): self.assertEqual(ret.write_time, 0) self.assertEqual(ret.busy_time, 0) + def test_disk_io_counters_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_disk_io_counters_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) + # ===================================================================== # --- misc From 6b4afea8ed6bb94611ec806f7695bd8d7d5f41e9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 29 Jul 2018 01:30:42 +0200 Subject: [PATCH 0046/1714] #1313 remove test which no longer makes sense --- psutil/tests/test_system.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 18cbb5d8bc..f9006ce815 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -712,12 +712,6 @@ def check_ntuple(nt): for key in ret: assert key, key check_ntuple(ret[key]) - if LINUX and key[-1].isdigit(): - # if 'sda1' is listed 'sda' shouldn't, see: - # https://github.com/giampaolo/psutil/issues/338 - while key[-1].isdigit(): - key = key[:-1] - self.assertNotIn(key, ret.keys()) def test_disk_io_counters_no_disks(self): # Emulate a case where no disks are installed, see: From 96363492d1455ad469277675583383690fbba080 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Aug 2018 20:53:11 +0200 Subject: [PATCH 0047/1714] fix #1305 / disk_io_counters() / Linux: assume SECTOR_SIZE is a fixed 512 --- HISTORY.rst | 1 + psutil/_pslinux.py | 33 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3dcafa3c2f..8026def746 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,7 @@ XXXX-XX-XX - 1279_: [Linux, macOS, BSD] net_if_stats() may return ENODEV. - 1294_: [Windows] psutil.Process().connections() may sometime fail with MemoryError. (patch by sylvainduchesne) +- 1305_: [Linux] disk_io_stats() may report inflated r/w bytes values. - 1309_: [Linux] Process.status() is unable to recognie "idle" and "parked" statuses (returns '?'). - 1313_: [Linux] disk_io_counters() can report inflated IO counters due to diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 0aa507bd28..54fa0d893b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -88,7 +88,20 @@ # speedup, see: https://github.com/giampaolo/psutil/issues/708 BIGFILE_BUFFERING = -1 if PY3 else 8192 LITTLE_ENDIAN = sys.byteorder == 'little' -SECTOR_SIZE_FALLBACK = 512 + +# "man iostat" states that sectors are equivalent with blocks and have +# a size of 512 bytes. Despite this value can be queried at runtime +# via /sys/block/{DISK}/queue/hw_sector_size and results may vary +# between 1k, 2k, or 4k... 512 appears to be a magic constant used +# throughout Linux source code: +# * https://stackoverflow.com/a/38136179/376587 +# * https://lists.gt.net/linux/kernel/2241060 +# * https://github.com/giampaolo/psutil/issues/1305 +# * https://github.com/torvalds/linux/blob/ +# 4f671fe2f9523a1ea206f63fe60a7c7b3a56d5c7/include/linux/bio.h#L99 +# * https://lkml.org/lkml/2015/8/17/234 +DISK_SECTOR_SIZE = 512 + if enum is None: AF_LINK = socket.AF_PACKET else: @@ -252,19 +265,6 @@ def file_flags_to_mode(flags): return mode -def get_sector_size(name): - """Return the sector size of a storage device.""" - # Some devices may have a slash in their name (e.g. cciss/c0d0...). - name = name.replace('/', '!') - try: - with open("/sys/block/%s/queue/hw_sector_size" % name, "rt") as f: - return int(f.read()) - except (IOError, ValueError): - # man iostat states that sectors are equivalent with blocks and - # have a size of 512 bytes since 2.4 kernels. - return SECTOR_SIZE_FALLBACK - - def is_storage_device(name): """Return True if the given name refers to a physical (e.g. "sda", "nvme0n1") or virtual (e.g. "loop1", "ram") storage device. @@ -1103,9 +1103,8 @@ def disk_io_counters(perdisk=False): # https://github.com/giampaolo/psutil/pull/1313 continue - ssize = get_sector_size(name) - rbytes *= ssize - wbytes *= ssize + rbytes *= DISK_SECTOR_SIZE + wbytes *= DISK_SECTOR_SIZE retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, writes_merged, busy_time) From a1f37298204d7dee2881fa0dab4665c243689982 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Aug 2018 21:16:55 +0200 Subject: [PATCH 0048/1714] update is_storage_device() docstring --- psutil/_pslinux.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 54fa0d893b..7e01bb7318 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -266,10 +266,10 @@ def file_flags_to_mode(flags): def is_storage_device(name): - """Return True if the given name refers to a physical (e.g. "sda", - "nvme0n1") or virtual (e.g. "loop1", "ram") storage device. - In case name refers to a device's partition (e.g. "sda1", "nvme0n1p1") - this is supposed to return False. + """Return True if the given name refers to a root device (e.g. + "sda", "nvme0n1") as opposed to a logical partition (e.g. "sda1", + "nvme0n1p1"). If name is a virtual device (e.g. "loop1", "ram") + return True. """ # Readapted from iostat source code, see: # https://github.com/sysstat/sysstat/blob/ From 892e70af56742708cbb38e311c6b4b916f0a0a46 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Aug 2018 21:31:27 +0200 Subject: [PATCH 0049/1714] remove old test --- psutil/tests/test_linux.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 761f790474..18d9e07a07 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1257,25 +1257,6 @@ def test_procfs_path(self): psutil.PROCFS_PATH = "/proc" os.rmdir(tdir) - def test_sector_size_mock(self): - # Test SECTOR_SIZE fallback in case 'hw_sector_size' file - # does not exist. - def open_mock(name, *args, **kwargs): - if PY3 and isinstance(name, bytes): - name = name.decode() - if "hw_sector_size" in name: - flag.append(None) - raise IOError(errno.ENOENT, '') - else: - return orig_open(name, *args, **kwargs) - - flag = [] - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - psutil.disk_io_counters() - assert flag - def test_issue_687(self): # In case of thread ID: # - pid_exists() is supposed to return False From 0352a00e863afd168dfcf5f78f0b7c8eb1a70926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 8 Aug 2018 19:36:31 +0200 Subject: [PATCH 0050/1714] Fix DeprecationWarning: invalid escape sequence (#1318) --- psutil/_psaix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 662f306c3c..b402a188f8 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -269,7 +269,7 @@ def net_if_stats(): stdout, stderr = [x.decode(sys.stdout.encoding) for x in (stdout, stderr)] if p.returncode == 0: - re_result = re.search("Running: (\d+) Mbps.*?(\w+) Duplex", stdout) + re_result = re.search(r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout) if re_result is not None: speed = int(re_result.group(1)) duplex = re_result.group(2) @@ -534,7 +534,7 @@ def open_files(self): for x in (stdout, stderr)] if "no such process" in stderr.lower(): raise NoSuchProcess(self.pid, self._name) - procfiles = re.findall("(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout) + procfiles = re.findall(r"(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout) retlist = [] for fd, path in procfiles: path = path.strip() From b739e13bcf59b88d9b6b4e5e71391d600eed8e4c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 12 Aug 2018 10:44:33 +0200 Subject: [PATCH 0051/1714] fix typo --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8026def746..440392e19f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,7 +23,7 @@ XXXX-XX-XX - 1294_: [Windows] psutil.Process().connections() may sometime fail with MemoryError. (patch by sylvainduchesne) - 1305_: [Linux] disk_io_stats() may report inflated r/w bytes values. -- 1309_: [Linux] Process.status() is unable to recognie "idle" and "parked" +- 1309_: [Linux] Process.status() is unable to recognize "idle" and "parked" statuses (returns '?'). - 1313_: [Linux] disk_io_counters() can report inflated IO counters due to erroneously counting base disk device and its partition(s) twice. From b9e60739aebaf824c692efedeb9cf9437c26ac49 Mon Sep 17 00:00:00 2001 From: Lawrence Ye Date: Mon, 13 Aug 2018 16:27:30 +0800 Subject: [PATCH 0052/1714] make disk_io_counters more robust (#1324) --- psutil/_pslinux.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7e01bb7318..1669c45389 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1052,8 +1052,23 @@ def disk_io_counters(perdisk=False): system as a dict of raw tuples. """ retdict = {} - with open_text("%s/diskstats" % get_procfs_path()) as f: - lines = f.readlines() + if os.path.exists("%s/diskstats" % get_procfs_path()): + with open_text("%s/diskstats" % get_procfs_path()) as f: + lines = f.readlines() + else: + # Try to use /sys/block/*/stat for disk_io_counters + # if /process/diskstats doesn't exist + lines = [] + for block in os.listdir('/sys/block'): + for root, _dirs, files in os.walk( + os.path.join('/sys/block', block)): + if 'stat' in files: + with open_text(os.path.join(root, 'stat')) as f: + line = f.readline() + # Let's just set major and minor device number + # to zero since we don't care about that + lines.append( + "0 0 %s %s" % (os.path.basename(root), line)) for line in lines: # OK, this is a bit confusing. The format of /proc/diskstats can # have 3 variations. From 0806efa9af59e1a0de5828af759b9afc47dfda2e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 13 Aug 2018 11:40:05 +0200 Subject: [PATCH 0053/1714] #1321: refactoring --- CREDITS | 4 ++ HISTORY.rst | 3 ++ psutil/_pslinux.py | 91 ++++++++++++++++++++++---------------- psutil/tests/test_linux.py | 2 +- 4 files changed, 60 insertions(+), 40 deletions(-) diff --git a/CREDITS b/CREDITS index edf1799dd2..27a4ed148d 100644 --- a/CREDITS +++ b/CREDITS @@ -551,3 +551,7 @@ I: 1284 N: sylvainduchesne W: https://github.com/sylvainduchesne I: 1294 + +N: Lawrence Ye +W: https://github.com/LEAFERx +I: 1321 diff --git a/HISTORY.rst b/HISTORY.rst index 440392e19f..e9162de5bc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,9 @@ XXXX-XX-XX - 1286_: [macOS] psutil.OSX constant is now deprecated in favor of new psutil.MACOS. - 1309_: [Linux] added psutil.STATUS_PARKED constant for Process.status(). +- 1321_: [Linux] add disk_io_counters() dual implementation relying on + /sys/block filesystem in case /proc/diskstats is not available. (patch by + Lawrence Ye) **Bug fixes** diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1669c45389..63b4ded1ed 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1051,25 +1051,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. """ - retdict = {} - if os.path.exists("%s/diskstats" % get_procfs_path()): - with open_text("%s/diskstats" % get_procfs_path()) as f: - lines = f.readlines() - else: - # Try to use /sys/block/*/stat for disk_io_counters - # if /process/diskstats doesn't exist - lines = [] - for block in os.listdir('/sys/block'): - for root, _dirs, files in os.walk( - os.path.join('/sys/block', block)): - if 'stat' in files: - with open_text(os.path.join(root, 'stat')) as f: - line = f.readline() - # Let's just set major and minor device number - # to zero since we don't care about that - lines.append( - "0 0 %s %s" % (os.path.basename(root), line)) - for line in lines: + def read_procfs(): # OK, this is a bit confusing. The format of /proc/diskstats can # have 3 variations. # On Linux 2.4 each line has always 15 fields, e.g.: @@ -1083,27 +1065,58 @@ def disk_io_counters(perdisk=False): # See: # https://www.kernel.org/doc/Documentation/iostats.txt # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats - fields = line.split() - fields_len = len(fields) - if fields_len == 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 fields_len == 14: - # Linux 2.6+, line referring to a disk - name = fields[2] - (reads, reads_merged, rbytes, rtime, writes, writes_merged, - wbytes, wtime, _, busy_time, _) = map(int, fields[3:14]) - elif fields_len == 7: - # Linux 2.6+, line referring to a partition - name = fields[2] - reads, rbytes, writes, wbytes = map(int, fields[3:]) - rtime = wtime = reads_merged = writes_merged = busy_time = 0 - else: - raise ValueError("not sure how to interpret line %r" % line) + with open_text("%s/diskstats" % get_procfs_path()) as f: + lines = f.readlines() + for line in lines: + fields = line.split() + flen = len(fields) + 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: + # Linux 2.6+, line referring to a disk + name = fields[2] + (reads, reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields[3:14]) + elif flen == 7: + # Linux 2.6+, line referring to a partition + name = fields[2] + reads, rbytes, writes, wbytes = map(int, fields[3:]) + rtime = wtime = reads_merged = writes_merged = busy_time = 0 + else: + raise ValueError("not sure how to interpret line %r" % line) + yield (name, reads, writes, rbytes, wbytes, rtime, wtime, + reads_merged, writes_merged, busy_time) + def read_sysfs(): + for block in os.listdir('/sys/block'): + for root, _, files in os.walk(os.path.join('/sys/block', block)): + if 'stat' not in files: + continue + with open_text(os.path.join(root, 'stat')) as f: + fields = f.read().strip().split() + name = os.path.basename(root) + (reads, reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields) + yield (name, reads, writes, rbytes, wbytes, rtime, + wtime, reads_merged, writes_merged, busy_time) + + if os.path.exists('%s/diskstats' % get_procfs_path()): + gen = read_procfs() + elif os.path.exists('/sys/block'): + gen = read_sysfs() + else: + raise NotImplementedError( + "%s/diskstats nor /sys/block filesystem are available on this " + "system" % get_procfs_path()) + + retdict = {} + for entry in gen: + (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, + writes_merged, busy_time) = entry if not perdisk and not is_storage_device(name): # perdisk=False means we want to calculate totals so we skip # partitions (e.g. 'sda1', 'nvme0n1p1') and only include diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 18d9e07a07..0216eabb72 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1250,7 +1250,7 @@ def test_procfs_path(self): 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_io_counters) self.assertRaises(IOError, psutil.disk_partitions) self.assertRaises(psutil.NoSuchProcess, psutil.Process) finally: From 67e8874a8bead8637948b770c806dc5c10f80bd9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 13 Aug 2018 13:06:45 +0200 Subject: [PATCH 0054/1714] #1321 add unit tests --- psutil/tests/test_linux.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0216eabb72..4e652a9d10 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1094,6 +1094,26 @@ def is_storage_device(name): self.assertEqual(ret.read_count, 1) self.assertEqual(ret.write_count, 5) + def test_disk_io_counters_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_disk_io_counters_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 From 32cefd018133443c60a280488520b3d096d819b5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 13 Aug 2018 13:53:05 +0200 Subject: [PATCH 0055/1714] fix failing linux tests --- psutil/_psaix.py | 3 ++- psutil/tests/test_contracts.py | 18 ++++-------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index b402a188f8..7ba212dbf5 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -269,7 +269,8 @@ def net_if_stats(): 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) + re_result = re.search( + r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout) if re_result is not None: speed = int(re_result.group(1)) duplex = re_result.group(2) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index ac424b54f2..da1c0c8567 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -289,16 +289,6 @@ class TestFetchAllProcesses(unittest.TestCase): some sanity checks against Process API's returned values. """ - def setUp(self): - if POSIX: - import pwd - import grp - users = pwd.getpwall() - groups = grp.getgrall() - self.all_uids = set([x.pw_uid for x in users]) - self.all_usernames = set([x.pw_name for x in users]) - self.all_gids = set([x.gr_gid for x in groups]) - def test_fetch_all(self): valid_procs = 0 excluded_names = set([ @@ -425,7 +415,6 @@ def uids(self, ret, proc): for uid in ret: self.assertIsInstance(uid, int) self.assertGreaterEqual(uid, 0) - self.assertIn(uid, self.all_uids) def gids(self, ret, proc): assert is_namedtuple(ret) @@ -435,13 +424,10 @@ def gids(self, ret, proc): self.assertIsInstance(gid, int) if not MACOS and not NETBSD: self.assertGreaterEqual(gid, 0) - self.assertIn(gid, self.all_gids) def username(self, ret, proc): self.assertIsInstance(ret, str) assert ret - if POSIX: - self.assertIn(ret, self.all_usernames) def status(self, ret, proc): self.assertIsInstance(ret, str) @@ -526,6 +512,10 @@ def memory_full_info(self, ret, proc): value = getattr(ret, name) self.assertIsInstance(value, (int, long)) self.assertGreaterEqual(value, 0, msg=(name, value)) + if LINUX 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: From 5bd44f8afcecbfb0db479ce230c790fc2c56569a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 13 Aug 2018 14:12:04 +0200 Subject: [PATCH 0056/1714] fix #1323: [Linux] sensors_temperatures() may fail with ValueError --- HISTORY.rst | 1 + psutil/_pslinux.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e9162de5bc..7532112df6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -30,6 +30,7 @@ XXXX-XX-XX statuses (returns '?'). - 1313_: [Linux] disk_io_counters() can report inflated IO counters due to erroneously counting base disk device and its partition(s) twice. +- 1323_: [Linux] sensors_temperatures() may fail with ValueError. 5.4.6 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 63b4ded1ed..df624de32c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -318,7 +318,7 @@ def cat(fname, fallback=_DEFAULT, binary=True): try: with open_binary(fname) if binary else open_text(fname) as f: return f.read().strip() - except IOError: + except (IOError, OSError): if fallback is not _DEFAULT: return fallback else: @@ -1199,13 +1199,15 @@ def sensors_temperatures(): current = float(cat(path)) / 1000.0 path = os.path.join(os.path.dirname(base), 'name') unit_name = cat(path, binary=False) - except (IOError, OSError) as err: + except (IOError, OSError, ValueError) as err: # A lot of things can go wrong here, so let's just skip the - # whole entry. + # whole entry. Sure thing is Linux's /sys/class/hwmon really + # is a stinky broken mess. # https://github.com/giampaolo/psutil/issues/1009 # https://github.com/giampaolo/psutil/issues/1101 # 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 @@ -1215,9 +1217,15 @@ def sensors_temperatures(): label = cat(base + '_label', fallback='', binary=False) if high is not None: - high = float(high) / 1000.0 + try: + high = float(high) / 1000.0 + except ValueError: + high = None if critical is not None: - critical = float(critical) / 1000.0 + try: + critical = float(critical) / 1000.0 + except ValueError: + critical = None ret[unit_name].append((label, current, high, critical)) From 4b71736b72b6b68a81dfb22d2df431c57ef1fde1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 13 Aug 2018 22:02:29 +0200 Subject: [PATCH 0057/1714] setup.py: add py 3.7 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index adc7a0e2dd..9815616dca 100755 --- a/setup.py +++ b/setup.py @@ -322,6 +322,7 @@ def main(): 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python', From 2820f92554cbce52afdab3710413e26975cddd5e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Aug 2018 01:37:17 +0200 Subject: [PATCH 0058/1714] OSX / SMC / sensors: revert #1284 (#1325) * setup.py: add py 3.7 * Revert OSX sensors code due to copyright constraints It turns out code contributed in PR #1284 was using parts of a source code released by Apple [1] which uses GPL license which is incompatible with psutil's license (BSD). [1] https://gist.github.com/edvakf/4049362 --- HISTORY.rst | 2 - docs/index.rst | 4 - psutil/_psosx.py | 141 ------------------- psutil/_psutil_osx.c | 62 --------- psutil/arch/osx/smc.c | 238 --------------------------------- psutil/arch/osx/smc.h | 79 ----------- psutil/tests/test_contracts.py | 4 +- setup.py | 2 +- 8 files changed, 3 insertions(+), 529 deletions(-) delete mode 100644 psutil/arch/osx/smc.c delete mode 100644 psutil/arch/osx/smc.h diff --git a/HISTORY.rst b/HISTORY.rst index 7532112df6..e295436aba 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,8 +7,6 @@ XXXX-XX-XX **Enhancements** -- 1284_: [macOS] added support for sensors_temperatures() and sensors_fans(). - (patch by Alex Manuskin) - 1286_: [macOS] psutil.OSX constant is now deprecated in favor of new psutil.MACOS. - 1309_: [Linux] added psutil.STATUS_PARKED constant for Process.status(). diff --git a/docs/index.rst b/docs/index.rst index 097d828dcb..c70d2f497e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -706,8 +706,6 @@ Sensors .. versionadded:: 5.1.0 - .. versionchanged:: 5.5.0: added macOS support - .. warning:: this API is experimental. Backward incompatible changes may occur if @@ -732,8 +730,6 @@ Sensors .. versionadded:: 5.2.0 - .. versionchanged:: 5.5.0: added macOS support - .. warning:: this API is experimental. Backward incompatible changes may occur if diff --git a/psutil/_psosx.py b/psutil/_psosx.py index d059449af8..fbfedf3ea1 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -4,7 +4,6 @@ """macOS platform implementation.""" -import collections import contextlib import errno import functools @@ -63,108 +62,6 @@ cext.SZOMB: _common.STATUS_ZOMBIE, } -SMC_TEMPERATURES = ( - # --- CPU - ("CPU", "TCXC", "PECI CPU"), - ("CPU", "TCXc", "PECI CPU"), - ("CPU", "TC0P", "CPU 1 Proximity"), - ("CPU", "TC0H", "CPU 1 Heatsink"), - ("CPU", "TC0D", "CPU 1 Package"), - ("CPU", "TC0E", "CPU 1"), - ("CPU", "TC1C", "CPU Core 1"), - ("CPU", "TC2C", "CPU Core 2"), - ("CPU", "TC3C", "CPU Core 3"), - ("CPU", "TC4C", "CPU Core 4"), - ("CPU", "TC5C", "CPU Core 5"), - ("CPU", "TC6C", "CPU Core 6"), - ("CPU", "TC7C", "CPU Core 7"), - ("CPU", "TC8C", "CPU Core 8"), - ("CPU", "TCAH", "CPU 1 Heatsink Alt."), - ("CPU", "TCAD", "CPU 1 Package Alt."), - ("CPU", "TC1P", "CPU 2 Proximity"), - ("CPU", "TC1H", "CPU 2 Heatsink"), - ("CPU", "TC1D", "CPU 2 Package"), - ("CPU", "TC1E", "CPU 2"), - ("CPU", "TCBH", "CPU 2 Heatsink Alt."), - ("CPU", "TCBD", "CPU 2 Package Alt."), - - ("CPU", "TCSC", "PECI SA"), - ("CPU", "TCSc", "PECI SA"), - ("CPU", "TCSA", "PECI SA"), - - # --- GPU - ("GPU", "TCGC", "PECI GPU"), - ("GPU", "TCGc", "PECI GPU"), - ("GPU", "TG0P", "GPU Proximity"), - ("GPU", "TG0D", "GPU Die"), - ("GPU", "TG1D", "GPU Die"), - ("GPU", "TG0H", "GPU Heatsink"), - ("GPU", "TG1H", "GPU Heatsink"), - - # --- Memory - ("Memory", "Ts0S", "Memory Proximity"), - ("Memory", "TM0P", "Mem Bank A1"), - ("Memory", "TM1P", "Mem Bank A2"), - ("Memory", "TM8P", "Mem Bank B1"), - ("Memory", "TM9P", "Mem Bank B2"), - ("Memory", "TM0S", "Mem Module A1"), - ("Memory", "TM1S", "Mem Module A2"), - ("Memory", "TM8S", "Mem Module B1"), - ("Memory", "TM9S", "Mem Module B2"), - - # --- HDD - ("HDD", "TH0P", "HDD Bay 1"), - ("HDD", "TH1P", "HDD Bay 2"), - ("HDD", "TH2P", "HDD Bay 3"), - ("HDD", "TH3P", "HDD Bay 4"), - - # --- Battery - ("Battery", "TB0T", "Battery TS_MAX"), - ("Battery", "TB1T", "Battery 1"), - ("Battery", "TB2T", "Battery 2"), - ("Battery", "TB3T", "Battery"), - - # --- Others - ("Others", "TN0D", "Northbridge Die"), - ("Others", "TN0P", "Northbridge Proximity 1"), - ("Others", "TN1P", "Northbridge Proximity 2"), - ("Others", "TN0C", "MCH Die"), - ("Others", "TN0H", "MCH Heatsink"), - ("Others", "TP0D", "PCH Die"), - ("Others", "TPCD", "PCH Die"), - ("Others", "TP0P", "PCH Proximity"), - - ("Others", "TA0P", "Airflow 1"), - ("Others", "TA1P", "Airflow 2"), - ("Others", "Th0H", "Heatpipe 1"), - ("Others", "Th1H", "Heatpipe 2"), - ("Others", "Th2H", "Heatpipe 3"), - - ("Others", "Tm0P", "Mainboard Proximity"), - ("Others", "Tp0P", "Powerboard Proximity"), - ("Others", "Ts0P", "Palm Rest"), - ("Others", "Tb0P", "BLC Proximity"), - - ("Others", "TL0P", "LCD Proximity"), - ("Others", "TW0P", "Airport Proximity"), - ("Others", "TO0P", "Optical Drive"), - - ("Others", "Tp0P", "Power Supply 1"), - ("Others", "Tp0C", "Power Supply 1 Alt."), - ("Others", "Tp1P", "Power Supply 2"), - ("Others", "Tp1C", "Power Supply 2 Alt."), - ("Others", "Tp2P", "Power Supply 3"), - ("Others", "Tp3P", "Power Supply 4"), - ("Others", "Tp4P", "Power Supply 5"), - ("Others", "Tp5P", "Power Supply 6"), - - ("Others", "TS0C", "Expansion Slots"), - ("Others", "TA0S", "PCI Slot 1 Pos 1"), - ("Others", "TA1S", "PCI Slot 1 Pos 2"), - ("Others", "TA2S", "PCI Slot 2 Pos 1"), - ("Others", "TA3S", "PCI Slot 2 Pos 2"), -) - kinfo_proc_map = dict( ppid=0, ruid=1, @@ -315,34 +212,6 @@ def disk_partitions(all=False): # ===================================================================== -def sensors_temperatures(): - """Returns a dictionary of regions of temperature sensors: - CPU/GPU/Memory/HDD/Battery/Others. - Each entry contains a list of results of all the SMC keys successfully - polled from the system. - References for SMC keys and meaning: - - * https://stackoverflow.com/questions/28568775/ - description-for-apples-smc-keys/31033665#31033665 - - * https://github.com/Chris911/iStats/blob/ - 09b159f85a9481b59f347a37259f6d272f65cc05/lib/iStats/smc.rb - - * http://web.archive.org/web/20140714090133/http://www.parhelia.ch:80/ - blog/statics/k3_keys.html - """ - # TODO: this should be rewritten in C: - # https://github.com/giampaolo/psutil/pull/1284#issuecomment-399480983 - ret = collections.defaultdict(list) - for group, key, label in SMC_TEMPERATURES: - result = cext.smc_get_temperature(key) - result = round(result, 1) - if result <= 0: - continue - ret[group].append((label, result, None, None)) - return dict(ret) - - def sensors_battery(): """Return battery information.""" try: @@ -360,16 +229,6 @@ def sensors_battery(): return _common.sbattery(percent, secsleft, power_plugged) -def sensors_fans(): - """Return fans speed information. - """ - ret = collections.defaultdict(list) - rawlist = cext.sensors_fans() - for fan in rawlist: - ret["Fans"].append(_common.sfan(fan[0], fan[1])) - return dict(ret) - - # ===================================================================== # --- network # ===================================================================== diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index e3250caba6..18dbe89310 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -44,7 +44,6 @@ #include "_psutil_common.h" #include "_psutil_posix.h" #include "arch/osx/process_info.h" -#include "arch/osx/smc.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -897,62 +896,6 @@ psutil_boot_time(PyObject *self, PyObject *args) { return Py_BuildValue("f", (float)boot_time); } -/* - * Return a Python float indicating the value of the temperature - * measured by an SMC key - */ -static PyObject * -psutil_smc_get_temperature(PyObject *self, PyObject *args) { - char* key; - float temp; - - if (! PyArg_ParseTuple(args, "s", &key)) { - return NULL; - } - temp = SMCGetTemperature(key); - return Py_BuildValue("d", temp); -} - - -/* - * Return a Python list of tuples of fan label and speed - */ -static PyObject * -psutil_sensors_fans(PyObject *self, PyObject *args) { - int key; - int speed; - char fan[7]; - int fan_count; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - fan_count = SMCGetFanNumber(SMC_KEY_FAN_NUM); - if (fan_count < 0) - fan_count = 0; - - for (key = 0; key < fan_count; key++) { - sprintf(fan, "Fan %d", key); - speed = SMCGetFanSpeed(key); - if (speed < 0) - continue; - py_tuple = Py_BuildValue("(si)", fan, speed); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_XDECREF(py_tuple); - } - - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - return NULL; -} /* * Return a list of tuples including device, mount point and fs type @@ -1885,7 +1828,6 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } - /* * Return battery information. */ @@ -2038,10 +1980,6 @@ PsutilMethods[] = { "Return currently connected users as a list of tuples"}, {"cpu_stats", psutil_cpu_stats, METH_VARARGS, "Return CPU statistics"}, - {"smc_get_temperature", psutil_smc_get_temperature, METH_VARARGS, - "Temperature of SMC key as float"}, - {"sensors_fans", psutil_sensors_fans, METH_VARARGS, - "Return the RPM of the fan with SMC key"}, {"sensors_battery", psutil_sensors_battery, METH_VARARGS, "Return battery information."}, diff --git a/psutil/arch/osx/smc.c b/psutil/arch/osx/smc.c deleted file mode 100644 index 00de0a3725..0000000000 --- a/psutil/arch/osx/smc.c +++ /dev/null @@ -1,238 +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. - * - * Interface to SMC API, needed in order to collect sensors stats. - */ - -#include -#include -#include - -#include "smc.h" - -UInt32 _strtoul(char *str, int size, int base) -{ - UInt32 total = 0; - int i; - - for (i = 0; i < size; i++) { - if (base == 16) - total += str[i] << (size - 1 - i) * 8; - else - total += (unsigned char) (str[i] << (size - 1 - i) * 8); - } - return total; -} - - -float _strtof(unsigned char *str, int size, int e) -{ - float total = 0; - int i; - - for (i = 0; i < size; i++) { - if (i == (size - 1)) - total += (str[i] & 0xff) >> e; - else - total += str[i] << (size - 1 - i) * (8 - e); - } - - total += (str[size-1] & 0x03) * 0.25; - - return total; -} - - -void _ultostr(char *str, UInt32 val) -{ - str[0] = '\0'; - sprintf(str, "%c%c%c%c", - (unsigned int) val >> 24, - (unsigned int) val >> 16, - (unsigned int) val >> 8, - (unsigned int) val); -} - - -kern_return_t SMCOpen(io_connect_t * pconn) -{ - kern_return_t result; - io_iterator_t iterator; - io_object_t device; - - CFMutableDictionaryRef matchingDictionary = IOServiceMatching("AppleSMC"); - result = IOServiceGetMatchingServices( - kIOMasterPortDefault, - matchingDictionary, - &iterator - ); - if (result != kIOReturnSuccess) { - return 1; - } - - device = IOIteratorNext(iterator); - IOObjectRelease(iterator); - if (device == 0) { - return 1; - } - - result = IOServiceOpen(device, mach_task_self(), 0, pconn); - IOObjectRelease(device); - if (result != kIOReturnSuccess) { - return 1; - } - - return kIOReturnSuccess; -} - - -kern_return_t SMCClose(io_connect_t conn) -{ - return IOServiceClose(conn); -} - - -kern_return_t SMCCall(io_connect_t conn, - int index, SMCKeyData_t *inputStructure, SMCKeyData_t *outputStructure) -{ - size_t structureInputSize; - size_t structureOutputSize; - - structureInputSize = sizeof(SMCKeyData_t); - structureOutputSize = sizeof(SMCKeyData_t); - -#if MAC_OS_X_VERSION_10_5 - return IOConnectCallStructMethod( - conn, - index, - inputStructure, - structureInputSize, - outputStructure, - &structureOutputSize); -#else - return IOConnectMethodStructureIStructureO( - conn, - index, - structureInputSize, - &structureOutputSize, - inputStructure, - outputStructure); -#endif -} - - -kern_return_t SMCReadKey(io_connect_t conn, UInt32Char_t key, SMCVal_t *val) { - kern_return_t result; - SMCKeyData_t inputStructure; - SMCKeyData_t outputStructure; - - memset(&inputStructure, 0, sizeof(SMCKeyData_t)); - memset(&outputStructure, 0, sizeof(SMCKeyData_t)); - memset(val, 0, sizeof(SMCVal_t)); - - inputStructure.key = _strtoul(key, 4, 16); - inputStructure.data8 = SMC_CMD_READ_KEYINFO; - - result = SMCCall( - conn, - KERNEL_INDEX_SMC, - &inputStructure, - &outputStructure - ); - if (result != kIOReturnSuccess) - return result; - - val->dataSize = outputStructure.keyInfo.dataSize; - _ultostr(val->dataType, outputStructure.keyInfo.dataType); - inputStructure.keyInfo.dataSize = val->dataSize; - inputStructure.data8 = SMC_CMD_READ_BYTES; - - result = SMCCall( - conn, - KERNEL_INDEX_SMC, - &inputStructure, - &outputStructure - ); - if (result != kIOReturnSuccess) - return result; - - memcpy(val->bytes, outputStructure.bytes, sizeof(outputStructure.bytes)); - - return kIOReturnSuccess; -} - - -double SMCGetTemperature(char *key) { - io_connect_t conn; - SMCVal_t val; - kern_return_t result; - int intValue; - - result = SMCOpen(&conn); - if (result != kIOReturnSuccess) { - return 0.0; - } - - result = SMCReadKey(conn, key, &val); - if (result == kIOReturnSuccess) { - // read succeeded - check returned value - if (val.dataSize > 0) { - if (strcmp(val.dataType, DATATYPE_SP78) == 0) { - // convert sp78 value to temperature - intValue = val.bytes[0] * 256 + (unsigned char)val.bytes[1]; - SMCClose(conn); - return intValue / 256.0; - } - } - } - // read failed - SMCClose(conn); - return 0.0; -} - - -float SMCGetFanSpeed(int fanNum) -{ - SMCVal_t val; - kern_return_t result; - UInt32Char_t key; - io_connect_t conn; - - result = SMCOpen(&conn); - if (result != kIOReturnSuccess) { - return -1; - } - - sprintf(key, SMC_KEY_FAN_SPEED, fanNum); - result = SMCReadKey(conn, key, &val); - if (result != kIOReturnSuccess) { - SMCClose(conn); - return -1; - } - SMCClose(conn); - return _strtof((unsigned char *)val.bytes, val.dataSize, 2); -} - - - -int SMCGetFanNumber(char *key) -{ - SMCVal_t val; - kern_return_t result; - io_connect_t conn; - - result = SMCOpen(&conn); - if (result != kIOReturnSuccess) { - return 0; - } - - result = SMCReadKey(conn, key, &val); - if (result != kIOReturnSuccess) { - SMCClose(conn); - return 0; - } - SMCClose(conn); - return _strtoul((char *)val.bytes, val.dataSize, 10); -} diff --git a/psutil/arch/osx/smc.h b/psutil/arch/osx/smc.h deleted file mode 100644 index acd399c2ea..0000000000 --- a/psutil/arch/osx/smc.h +++ /dev/null @@ -1,79 +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. - */ - -#ifndef __SMC_H__ -#define __SMC_H__ - -#define KERNEL_INDEX_SMC 2 - -#define SMC_CMD_READ_BYTES 5 -#define SMC_CMD_WRITE_BYTES 6 -#define SMC_CMD_READ_INDEX 8 -#define SMC_CMD_READ_KEYINFO 9 -#define SMC_CMD_READ_PLIMIT 11 -#define SMC_CMD_READ_VERS 12 - - -#define DATATYPE_FPE2 "fpe2" -#define DATATYPE_UINT8 "ui8 " -#define DATATYPE_UINT16 "ui16" -#define DATATYPE_UINT32 "ui32" -#define DATATYPE_SP78 "sp78" - -// Fans SMC key values -#define SMC_KEY_FAN_SPEED "F%dAc" -#define SMC_KEY_FAN_NUM "FNum" - -typedef struct { - char major; - char minor; - char build; - char reserved[1]; - UInt16 release; -} SMCKeyData_vers_t; - -typedef struct { - UInt16 version; - UInt16 length; - UInt32 cpuPLimit; - UInt32 gpuPLimit; - UInt32 memPLimit; -} SMCKeyData_pLimitData_t; - -typedef struct { - UInt32 dataSize; - UInt32 dataType; - char dataAttributes; -} SMCKeyData_keyInfo_t; - -typedef char SMCBytes_t[32]; - -typedef struct { - UInt32 key; - SMCKeyData_vers_t vers; - SMCKeyData_pLimitData_t pLimitData; - SMCKeyData_keyInfo_t keyInfo; - char result; - char status; - char data8; - UInt32 data32; - SMCBytes_t bytes; -} SMCKeyData_t; - -typedef char UInt32Char_t[5]; - -typedef struct { - UInt32Char_t key; - UInt32 dataSize; - UInt32Char_t dataType; - SMCBytes_t bytes; -} SMCVal_t; - -double SMCGetTemperature(char *key); -int SMCGetFanNumber(char *key); -float SMCGetFanSpeed(int fanNum); - -#endif diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index da1c0c8567..877a5c0664 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -118,10 +118,10 @@ def test_cpu_freq(self): def test_sensors_temperatures(self): self.assertEqual( - hasattr(psutil, "sensors_temperatures"), LINUX or MACOS) + hasattr(psutil, "sensors_temperatures"), LINUX) def test_sensors_fans(self): - self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX or MACOS) + self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX) def test_battery(self): self.assertEqual(hasattr(psutil, "sensors_battery"), diff --git a/setup.py b/setup.py index adc7a0e2dd..c4845a8d74 100755 --- a/setup.py +++ b/setup.py @@ -152,7 +152,6 @@ def get_winver(): sources=sources + [ 'psutil/_psutil_osx.c', 'psutil/arch/osx/process_info.c', - 'psutil/arch/osx/smc.c', ], define_macros=macros, extra_link_args=[ @@ -322,6 +321,7 @@ def main(): 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python', From da4e3598fa245fb081bba101fd9cc698c81a8033 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Aug 2018 01:59:34 +0200 Subject: [PATCH 0059/1714] set version to 5.4.7 --- HISTORY.rst | 2 +- MANIFEST.in | 4 +--- docs/index.rst | 4 ++-- psutil/__init__.py | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e295436aba..5ca1813aaa 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.5.0 +5.4.7 ===== XXXX-XX-XX diff --git a/MANIFEST.in b/MANIFEST.in index 146cd92dfb..26d678fc33 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -63,8 +63,6 @@ include psutil/arch/openbsd/specific.c include psutil/arch/openbsd/specific.h include psutil/arch/osx/process_info.c include psutil/arch/osx/process_info.h -include psutil/arch/osx/smc.c -include psutil/arch/osx/smc.h include psutil/arch/solaris/environ.c include psutil/arch/solaris/environ.h include psutil/arch/solaris/v10/ifaddrs.c @@ -88,9 +86,9 @@ include psutil/tests/test_bsd.py include psutil/tests/test_connections.py include psutil/tests/test_contracts.py include psutil/tests/test_linux.py -include psutil/tests/test_osx.py include psutil/tests/test_memory_leaks.py include psutil/tests/test_misc.py +include psutil/tests/test_osx.py include psutil/tests/test_posix.py include psutil/tests/test_process.py include psutil/tests/test_sunos.py diff --git a/docs/index.rst b/docs/index.rst index c70d2f497e..a9122995de 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2135,7 +2135,7 @@ Constants Alias for :const:`MACOS` (deprecated). .. warning:: - deprecated in version 5.5.0; use :const:`MACOS` instead. + deprecated in version 5.4.7; use :const:`MACOS` instead. .. _const-procfs_path: .. data:: PROCFS_PATH @@ -2179,7 +2179,7 @@ Constants Returned by :meth:`psutil.Process.status()`. .. versionadded:: 3.4.1 STATUS_SUSPENDED (NetBSD) - .. versionadded:: 5.5.0 STATUS_PARKED (Linux) + .. versionadded:: 5.4.7 STATUS_PARKED (Linux) .. _const-conn: .. data:: CONN_ESTABLISHED diff --git a/psutil/__init__.py b/psutil/__init__.py index 291d0d16a8..e1299655a1 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -221,7 +221,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.4.6" +__version__ = "5.4.7" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED From feba8be76167c457f678b29a6ef4d00cc7babc82 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Aug 2018 22:57:25 +0200 Subject: [PATCH 0060/1714] pre release --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5ca1813aaa..54ee4795fc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.4.7 ===== -XXXX-XX-XX +2018-08-14 **Enhancements** From 0699c04eb20132dc6f15f69e29565c67d264f00e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Aug 2018 23:00:51 +0200 Subject: [PATCH 0061/1714] pre release --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index a9122995de..59f7681505 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2635,6 +2635,10 @@ take a look at the Timeline ======== +- 2018-08-14: + `5.4.7 `__ - + `what's new `__ - + `diff `__ - 2018-06-07: `5.4.6 `__ - `what's new `__ - From df027542ca60998a6d0d4e1a2f59fb720ffccfc0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Aug 2018 14:48:48 +0200 Subject: [PATCH 0062/1714] make test more robust --- .travis.yml | 2 +- psutil/__init__.py | 2 +- psutil/tests/test_system.py | 11 ++++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 86cc73ec09..6696870f42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: env: PYVER=py27 - language: generic os: osx - env: PYVER=py34 + env: PYVER=py36 install: - ./.ci/travis/install.sh script: diff --git a/psutil/__init__.py b/psutil/__init__.py index e1299655a1..23dc1227e3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -221,7 +221,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.4.7" +__version__ = "5.4.8" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index f9006ce815..8b07caff65 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -318,11 +318,12 @@ def test_cpu_times(self): def test_cpu_times_time_increases(self): # Make sure time increases between calls. t1 = sum(psutil.cpu_times()) - time.sleep(0.1) - t2 = sum(psutil.cpu_times()) - difference = t2 - t1 - if not difference >= 0.05: - self.fail("difference %s" % difference) + 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(). From fc415a28340ff6f9e4c5438a5ce68a61c9a3a602 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Aug 2018 14:50:00 +0200 Subject: [PATCH 0063/1714] remove failing test --- psutil/tests/test_process.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 2308196235..9d786d3404 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -538,9 +538,6 @@ def test_threads(self): with ThreadTask(): step2 = p.threads() self.assertEqual(len(step2), len(step1) + 1) - # on Linux, first thread id is supposed to be this process - if LINUX: - self.assertEqual(step2[0].id, os.getpid()) athread = step2[0] # test named tuple self.assertEqual(athread.id, athread[0]) From 625503d2fa9b5bd8bb30dce775ac8c2e909f8c10 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Aug 2018 14:51:48 +0200 Subject: [PATCH 0064/1714] remove failing test assertions --- psutil/tests/test_process.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 9d786d3404..80f9796ffd 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -659,16 +659,10 @@ def normpath(p): def test_memory_percent(self): p = psutil.Process() - ret = p.memory_percent() - assert 0 <= ret <= 100, ret - ret = p.memory_percent(memtype='vms') - assert 0 <= ret <= 100, ret - assert 0 <= ret <= 100, ret + p.memory_percent() self.assertRaises(ValueError, p.memory_percent, memtype="?!?") if LINUX or MACOS or WINDOWS: - ret = p.memory_percent(memtype='uss') - assert 0 <= ret <= 100, ret - assert 0 <= ret <= 100, ret + p.memory_percent(memtype='uss') def test_is_running(self): sproc = get_test_subprocess() From 601b847e1d970f7c738e771b685295f366a84b3e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Aug 2018 12:41:55 +0200 Subject: [PATCH 0065/1714] add download badge --- .travis.yml | 2 +- README.rst | 4 ++++ psutil/_psutil_osx.c | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6696870f42..86cc73ec09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: env: PYVER=py27 - language: generic os: osx - env: PYVER=py36 + env: PYVER=py34 install: - ./.ci/travis/install.sh script: diff --git a/README.rst b/README.rst index abd5816c67..2fa59be653 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,7 @@ +.. image:: http://pepy.tech/badge/psutil + :target: http://pepy.tech/project/psutil + :alt: Downloads + .. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20macOS :target: https://travis-ci.org/giampaolo/psutil :alt: Linux tests (Travis) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 18dbe89310..b75c78b20d 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -682,8 +682,8 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { /* * Return system virtual memory stats. * See: - * http://opensource.apple.com/source/system_cmds/system_cmds-498.2/ - * vm_stat.tproj/vm_stat.c + * https://opensource.apple.com/source/system_cmds/system_cmds-790/ + * vm_stat.tproj/vm_stat.c.auto.html */ static PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { From 5aa81f2892f6b4c1f5cb7cfbb8d1ec552aebe416 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Aug 2018 12:49:45 +0200 Subject: [PATCH 0066/1714] travis: add python 3.7 build --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 86cc73ec09..e32f87c240 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,9 @@ matrix: - python: 3.4 - python: 3.5 - python: 3.6 + - python: 3.7 + dist: xenial + sudo: true # macOS - language: generic os: osx From fe8140c23f481578689fdf0fb95e2d3e301e73c2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Aug 2018 12:53:23 +0200 Subject: [PATCH 0067/1714] also include PYPY (or try to :P) --- .ci/travis/install.sh | 6 +++--- .travis.yml | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index bb86700ea0..56f6623d8e 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -24,9 +24,9 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then pyenv install 2.7.10 pyenv virtualenv 2.7.10 psutil ;; - py34) - pyenv install 3.4.3 - pyenv virtualenv 3.4.3 psutil + py36) + pyenv install 3.6.6 + pyenv virtualenv 3.6.6 psutil ;; esac pyenv rehash diff --git a/.travis.yml b/.travis.yml index e32f87c240..e522fe002f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,10 @@ matrix: env: PYVER=py27 - language: generic os: osx - env: PYVER=py34 + env: PYVER=py36 + # pypy + - python: pypy + - python: pypy3 install: - ./.ci/travis/install.sh script: From 90cb9d19937e630303d5e87e49a4132260316b53 Mon Sep 17 00:00:00 2001 From: yanok Date: Wed, 29 Aug 2018 23:28:50 +0200 Subject: [PATCH 0068/1714] make psutil_debug() aware of PSUTIL_DEBUG (#1332) Fixes #1331 --- psutil/_psutil_common.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 49b91c8269..db1a046012 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -84,11 +84,13 @@ psutil_set_testing(PyObject *self, PyObject *args) { void psutil_debug(const char* format, ...) { va_list argptr; - va_start(argptr, format); - fprintf(stderr, "psutil-debug> "); - vfprintf(stderr, format, argptr); - fprintf(stderr, "\n"); - va_end(argptr); + if (PSUTIL_DEBUG) { + va_start(argptr, format); + fprintf(stderr, "psutil-debug> "); + vfprintf(stderr, format, argptr); + fprintf(stderr, "\n"); + va_end(argptr); + } } From 5e90b0a7f3fccb177445a186cc4fac62cfadb510 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Aug 2018 23:31:42 +0200 Subject: [PATCH 0069/1714] #1332 - update HISTORY --- CREDITS | 4 ++++ HISTORY.rst | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/CREDITS b/CREDITS index 27a4ed148d..8df9090fcd 100644 --- a/CREDITS +++ b/CREDITS @@ -555,3 +555,7 @@ I: 1294 N: Lawrence Ye W: https://github.com/LEAFERx I: 1321 + +N: yanok +W: https://github.com/yanok +I: 1332 diff --git a/HISTORY.rst b/HISTORY.rst index 54ee4795fc..1cd61ed864 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.4.8 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 1332_: psutil debug messages are erroneously printed all the time. (patch by + yanok) + 5.4.7 ===== From 39811bbb989497b04b4bebd927de6c3e806d0e70 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 26 Sep 2018 12:59:08 +0200 Subject: [PATCH 0070/1714] fix #1343: document Process.as_dict() attrs values --- docs/index.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 59f7681505..a3fca7b5f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1179,11 +1179,14 @@ Process class Utility method retrieving multiple process information as a dictionary. If *attrs* is specified it must be a list of strings reflecting available - :class:`Process` class's attribute names (e.g. ``['cpu_times', 'name']``), - else all public (read only) attributes are assumed. *ad_value* is the - value which gets assigned to a dict key in case :class:`AccessDenied` - or :class:`ZombieProcess` exception is raised when retrieving that - particular process information. + :class:`Process` class's attribute names. Here's a list of possible string + values: + ``'cmdline'``, ``'connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. + If *attrs* argument is not passed all public read only attributes are + assumed. + *ad_value* is the value which gets assigned to a dict key in case + :class:`AccessDenied` or :class:`ZombieProcess` exception is raised when + retrieving that particular process information. Internally, :meth:`as_dict` uses :meth:`oneshot` context manager so there's no need you use it also. From 772c675c39f02d94eba5d727f3dd2861569d8c6c Mon Sep 17 00:00:00 2001 From: "E. M. Bray" Date: Wed, 26 Sep 2018 13:07:47 +0200 Subject: [PATCH 0071/1714] Refactored ps() function in test_posix (#1341) * refactored ps() function that attepts to abstract away more platform-specific differences in the ps command * move open_text an open_binary tests to test_misc --- psutil/tests/test_linux.py | 8 --- psutil/tests/test_misc.py | 10 ++++ psutil/tests/test_posix.py | 105 ++++++++++++++++++++++--------------- 3 files changed, 72 insertions(+), 51 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4e652a9d10..5e3c834ef8 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1978,14 +1978,6 @@ def test_cpu_affinity_eligible_cpus(self): @unittest.skipIf(not LINUX, "LINUX only") class TestUtils(unittest.TestCase): - def test_open_text(self): - with psutil._psplatform.open_text(__file__) as f: - self.assertEqual(f.mode, 'rt') - - def test_open_binary(self): - with psutil._psplatform.open_binary(__file__) as f: - self.assertEqual(f.mode, 'rb') - def test_readlink(self): with mock.patch("os.readlink", return_value="foo (deleted)") as m: self.assertEqual(psutil._psplatform.readlink("bar"), "foo") diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 1d9067e77d..3056abc0a7 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -26,6 +26,8 @@ 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 @@ -896,6 +898,14 @@ def setUp(self): 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) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index b80128c7f2..35f73eb80f 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -12,7 +12,6 @@ import os import re import subprocess -import sys import time import psutil @@ -23,7 +22,6 @@ from psutil import OPENBSD from psutil import POSIX from psutil import SUNOS -from psutil._compat import PY3 from psutil.tests import APPVEYOR from psutil.tests import get_kernel_version from psutil.tests import get_test_subprocess @@ -40,23 +38,54 @@ from psutil.tests import which -def ps(cmd): - """Expects a ps command with a -o argument and parse the result - returning only the value of interest. +def ps(fmt, pid=None): """ - if not LINUX: - cmd = cmd.replace(" --no-headers ", " ") + 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: - cmd = cmd.replace("-o start", "-o stime") - if AIX: - cmd = cmd.replace("-o rss", "-o rssize") + fmt_map = {'command', 'comm', + 'start', 'stime'} + fmt = fmt_map.get(fmt, fmt) + + cmd.extend(['-o', fmt]) + output = sh(cmd) - if not LINUX: - output = output.split('\n')[1].strip() - try: - return int(output) - except ValueError: - return output + + 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. @@ -74,14 +103,14 @@ def ps_name(pid): field = "command" if SUNOS: field = "comm" - return ps("ps --no-headers -o %s -p %s" % (field, pid)).split(' ')[0] + return ps(field, pid).split()[0] def ps_args(pid): field = "command" if AIX or SUNOS: field = "args" - return ps("ps --no-headers -o %s -p %s" % (field, pid)) + return ps(field, pid) @unittest.skipIf(not POSIX, "POSIX only") @@ -99,22 +128,22 @@ def tearDownClass(cls): reap_children() def test_ppid(self): - ppid_ps = ps("ps --no-headers -o ppid -p %s" % self.pid) + 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("ps --no-headers -o uid -p %s" % self.pid) + 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("ps --no-headers -o rgid -p %s" % self.pid) + 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("ps --no-headers -o user -p %s" % self.pid) + username_ps = ps('user', self.pid) username_psutil = psutil.Process(self.pid).username() self.assertEqual(username_ps, username_psutil) @@ -133,7 +162,7 @@ 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("ps --no-headers -o rss -p %s" % self.pid) + rss_ps = ps('rss', self.pid) rss_psutil = psutil.Process(self.pid).memory_info()[0] / 1024 self.assertEqual(rss_ps, rss_psutil) @@ -143,7 +172,7 @@ 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("ps --no-headers -o vsz -p %s" % self.pid) + vsz_ps = ps('vsz', self.pid) vsz_psutil = psutil.Process(self.pid).memory_info()[1] / 1024 self.assertEqual(vsz_ps, vsz_psutil) @@ -196,7 +225,7 @@ def test_name_long_cmdline_nsp_exc(self): @unittest.skipIf(MACOS or BSD, 'ps -o start not available') def test_create_time(self): - time_ps = ps("ps --no-headers -o start -p %s" % self.pid).split(' ')[0] + 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") @@ -235,7 +264,7 @@ def test_cmdline(self): @unittest.skipIf(SUNOS, "not reliable on SUNOS") @unittest.skipIf(AIX, "not reliable on AIX") def test_nice(self): - ps_nice = ps("ps --no-headers -o nice -p %s" % self.pid) + ps_nice = ps('nice', self.pid) psutil_nice = psutil.Process().nice() self.assertEqual(ps_nice, psutil_nice) @@ -289,22 +318,7 @@ class TestSystemAPIs(unittest.TestCase): def test_pids(self): # Note: this test might fail if the OS is starting/killing # other processes in the meantime - if SUNOS or AIX: - cmd = ["ps", "-A", "-o", "pid"] - else: - cmd = ["ps", "ax", "-o", "pid"] - p = get_test_subprocess(cmd, stdout=subprocess.PIPE) - output = p.communicate()[0].strip() - assert p.poll() == 0 - if PY3: - output = str(output, sys.stdout.encoding) - pids_ps = [] - for line in output.split('\n')[1:]: - if line: - pid = int(line.split()[0].strip()) - pids_ps.append(pid) - # remove ps subprocess pid which is supposed to be dead in meantime - pids_ps.remove(p.pid) + pids_ps = ps("pid") pids_psutil = psutil.pids() pids_ps.sort() pids_psutil.sort() @@ -312,7 +326,12 @@ def test_pids(self): # 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) - self.assertEqual(pids_ps, pids_psutil) + + # 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 From ab9a381f5cd9b33b244a4883d6ba55208c049dcf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 26 Sep 2018 13:13:36 +0200 Subject: [PATCH 0072/1714] #1341: move open() utilities/wrappers in _common.py --- psutil/_common.py | 18 ++++++++++++++++++ psutil/_pslinux.py | 20 ++------------------ psutil/tests/test_linux.py | 22 +++++++++++----------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 2cc3939a1c..bee9579276 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -576,3 +576,21 @@ def wrap_numbers(input_dict, name): _wn = _WrapNumbers() wrap_numbers.cache_clear = _wn.cache_clear wrap_numbers.cache_info = _wn.cache_info + + +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) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index df624de32c..1575dad9d2 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -33,6 +33,8 @@ from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN +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 @@ -201,24 +203,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) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 5e3c834ef8..a0527851f5 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -528,7 +528,7 @@ def test_free(self): free_value, psutil_value, delta=MEMORY_TOLERANCE) def test_missing_sin_sout(self): - with mock.patch('psutil._pslinux.open', create=True) as m: + 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() @@ -651,7 +651,7 @@ def test_cpu_count_logical_mocked(self): # Let's have open() return emtpy data and make sure None is # returned ('cause we mimick os.cpu_count()). - with mock.patch('psutil._pslinux.open', create=True) as m: + 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 @@ -662,7 +662,7 @@ def test_cpu_count_logical_mocked(self): with open('/proc/cpuinfo', 'rb') as f: cpuinfo_data = f.read() fake_file = io.BytesIO(cpuinfo_data) - with mock.patch('psutil._pslinux.open', + with mock.patch('psutil._common.open', return_value=fake_file, create=True) as m: self.assertEqual(psutil._pslinux.cpu_count_logical(), original) @@ -675,7 +675,7 @@ def test_cpu_count_logical_mocked(self): def test_cpu_count_physical_mocked(self): # Have open() return emtpy data and make sure None is returned # ('cause we want to mimick os.cpu_count()) - with mock.patch('psutil._pslinux.open', create=True) as m: + with mock.patch('psutil._common.open', create=True) as m: self.assertIsNone(psutil._pslinux.cpu_count_physical()) assert m.called @@ -971,7 +971,7 @@ def test_disk_partitions_mocked(self): else: # No ZFS partitions on this system. Let's fake one. fake_file = io.StringIO(u("nodev\tzfs\n")) - with mock.patch('psutil._pslinux.open', + with mock.patch('psutil._common.open', return_value=fake_file, create=True) as m1: with mock.patch( 'psutil._pslinux.cext.disk_partitions', @@ -1232,7 +1232,7 @@ def test_cpu_steal_decrease(self): self.assertNotEqual(cpu_times_percent.user, 0) def test_boot_time_mocked(self): - with mock.patch('psutil._pslinux.open', create=True) as m: + with mock.patch('psutil._common.open', create=True) as m: self.assertRaises( RuntimeError, psutil._pslinux.boot_time) @@ -1679,7 +1679,7 @@ def test_terminal_mocked(self): # TODO: re-enable this test. # def test_num_ctx_switches_mocked(self): - # with mock.patch('psutil._pslinux.open', create=True) as m: + # with mock.patch('psutil._common.open', create=True) as m: # self.assertRaises( # NotImplementedError, # psutil._pslinux.Process(os.getpid()).num_ctx_switches) @@ -1689,12 +1689,12 @@ 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._pslinux.open', + 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._pslinux.open', + with mock.patch('psutil._common.open', return_value=fake_file, create=True) as m: self.assertEqual(p.cmdline(), ['foo', 'bar', '']) assert m.called @@ -1703,12 +1703,12 @@ 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._pslinux.open', + 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._pslinux.open', + with mock.patch('psutil._common.open', return_value=fake_file, create=True) as m: self.assertEqual(p.cmdline(), ['foo', 'bar', '']) assert m.called From 7be5d9c6a1d829c60855edec6543039db6316631 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 26 Sep 2018 06:14:50 -0700 Subject: [PATCH 0073/1714] Correct capitalization of PyPI (#1337) As spelled on https://pypi.org/. --- HISTORY.rst | 4 ++-- INSTALL.rst | 2 +- Makefile | 4 ++-- docs/index.rst | 2 +- make.bat | 2 +- scripts/internal/winmake.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1cd61ed864..0b6f38522a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -213,7 +213,7 @@ XXXX-XX-XX **Compatibility notes** -- 1120_: .exe files for Windows are no longer uploaded on PYPI as per PEP-527; +- 1120_: .exe files for Windows are no longer uploaded on PyPI as per PEP-527; only wheels are provided. 5.3.0 @@ -1227,7 +1227,7 @@ DeprecationWarning. **Enhancements** -- 410_: host tar.gz and windows binary files are on PYPI. +- 410_: host tar.gz and windows binary files are on PyPI. - 412_: [Linux] get/set process resource limits. - 415_: [Windows] Process.get_children() is an order of magnitude faster. - 426_: [Windows] Process.name is an order of magnitude faster. diff --git a/INSTALL.rst b/INSTALL.rst index 6644fb37b0..aaa1b87f05 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -76,7 +76,7 @@ Windows The easiest way to install psutil on Windows is to just use the pre-compiled exe/wheel installers hosted on -`PYPI `__ via pip: +`PyPI `__ via pip: .. code-block:: bat diff --git a/Makefile b/Makefile index 51047383fa..25928e0aa3 100644 --- a/Makefile +++ b/Makefile @@ -210,7 +210,7 @@ upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist $(PYTHON) setup.py sdist upload -upload-win-wheels: ## Upload wheels in dist/* directory on PYPI. +upload-win-wheels: ## Upload wheels in dist/* directory on PyPI. $(PYTHON) -m twine upload dist/*.whl # --- others @@ -233,7 +233,7 @@ pre-release: ## Check if we're ready to produce a new release. release: ## Create a release (down/uploads tar.gz, wheels, git tag release). ${MAKE} pre-release - $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PYPI + $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release print-announce: ## Print announce of new release. diff --git a/docs/index.rst b/docs/index.rst index a3fca7b5f8..7763b6de0a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,7 +53,7 @@ The easiest way to install psutil is via ``pip``:: On UNIX this requires a C compiler (e.g. gcc) installed. On Windows pip will automatically retrieve a pre-compiled wheel version from -`PYPI repository `__. +`PyPI repository `__. Alternatively, see more detailed `install `_ instructions. diff --git a/make.bat b/make.bat index c7c319026f..43000535da 100644 --- a/make.bat +++ b/make.bat @@ -26,7 +26,7 @@ if "%TSCRIPT%" == "" ( set TSCRIPT=psutil\tests\__main__.py ) -rem Needed to locate the .pypirc file and upload exes on PYPI. +rem Needed to locate the .pypirc file and upload exes on PyPI. set HOME=%USERPROFILE% %PYTHON% scripts\internal\winmake.py %1 %2 %3 %4 %5 %6 diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index ffdfc291eb..c4722a0298 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -229,7 +229,7 @@ def wheel(): @cmd def upload_wheels(): - """Upload wheel files on PYPI.""" + """Upload wheel files on PyPI.""" build() sh("%s -m twine upload dist/*.whl" % PYTHON) From 1d7516c10cc89c60b8b5607ff9656d2f817b22b1 Mon Sep 17 00:00:00 2001 From: sylvainduchesne <35115147+sylvainduchesne@users.noreply.github.com> Date: Wed, 26 Sep 2018 14:35:12 -0400 Subject: [PATCH 0074/1714] Fix random 0xC0000001 errors when querying for Connections (#1335) Fix random 0xC0000001 errors when querying for Connections (#1335) --- CREDITS | 2 +- HISTORY.rst | 4 +++- psutil/_psutil_windows.c | 14 ++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CREDITS b/CREDITS index 8df9090fcd..bfb686270f 100644 --- a/CREDITS +++ b/CREDITS @@ -548,7 +548,7 @@ N: Alex Manuskin W: https://github.com/amanusk I: 1284 -N: sylvainduchesne +N: Sylvain Duchesne W: https://github.com/sylvainduchesne I: 1294 diff --git a/HISTORY.rst b/HISTORY.rst index 0b6f38522a..0c79a30a6d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,7 +9,9 @@ XXXX-XX-XX - 1332_: psutil debug messages are erroneously printed all the time. (patch by yanok) - +- 1294_: [Windows] psutil.Process().connections() may sometimes fail with + intermittent 0xC0000001. (patch by Sylvain Duchesne) + 5.4.7 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 1d44dd0ba0..4acea3600a 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1530,15 +1530,14 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, // that the size of the table increases between the moment where we // query the size and the moment where we query the data. Therefore, it's // important to call this in a loop to retry if that happens. - // - // Also, since we may loop a theoretically unbounded number of times here, - // release the GIL while we're doing this. + // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error + // and https://github.com/giampaolo/psutil/issues/1294 DWORD error = ERROR_INSUFFICIENT_BUFFER; *size = 0; *data = NULL; error = call(NULL, size, FALSE, address_family, TCP_TABLE_OWNER_PID_ALL, 0); - while (error == ERROR_INSUFFICIENT_BUFFER) + while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) { *data = malloc(*size); if (*data == NULL) { @@ -1569,15 +1568,14 @@ static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, // that the size of the table increases between the moment where we // query the size and the moment where we query the data. Therefore, it's // important to call this in a loop to retry if that happens. - // - // Also, since we may loop a theoretically unbounded number of times here, - // release the GIL while we're doing this. + // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error + // and https://github.com/giampaolo/psutil/issues/1294 DWORD error = ERROR_INSUFFICIENT_BUFFER; *size = 0; *data = NULL; error = call(NULL, size, FALSE, address_family, UDP_TABLE_OWNER_PID, 0); - while (error == ERROR_INSUFFICIENT_BUFFER) + while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) { *data = malloc(*size); if (*data == NULL) { From 1a7512f337a61733c0523b4199aa392b48a22ad6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 28 Sep 2018 18:45:46 +0200 Subject: [PATCH 0075/1714] use memory tolerance in occasionally failing test --- CREDITS | 2 +- HISTORY.rst | 6 +++--- psutil/tests/test_linux.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CREDITS b/CREDITS index bfb686270f..a7e2dcbefc 100644 --- a/CREDITS +++ b/CREDITS @@ -556,6 +556,6 @@ N: Lawrence Ye W: https://github.com/LEAFERx I: 1321 -N: yanok +N: Ilya Yanok W: https://github.com/yanok I: 1332 diff --git a/HISTORY.rst b/HISTORY.rst index 0c79a30a6d..b1226489f7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,11 +7,11 @@ XXXX-XX-XX **Bug fixes** -- 1332_: psutil debug messages are erroneously printed all the time. (patch by - yanok) - 1294_: [Windows] psutil.Process().connections() may sometimes fail with intermittent 0xC0000001. (patch by Sylvain Duchesne) - +- 1332_: [OSX] psutil debug messages are erroneously printed all the time. + (patch by Ilya Yanok) + 5.4.7 ===== diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index a0527851f5..a8e7a5e34f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -575,7 +575,7 @@ def test_meminfo_against_sysinfo(self): total *= unit_multiplier free *= unit_multiplier self.assertEqual(swap.total, total) - self.assertEqual(swap.free, free) + self.assertEqual(swap.free, free, delta=MEMORY_TOLERANCE) def test_emulate_meminfo_has_no_metrics(self): # Emulate a case where /proc/meminfo provides no swap metrics From cc888778bf49c6472b22ed7b5541e0c6e294c427 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 28 Sep 2018 19:09:56 +0200 Subject: [PATCH 0076/1714] catch UnicodeEncodeError on print() --- scripts/pmap.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/pmap.py b/scripts/pmap.py index a509bd733b..988b75072c 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -35,6 +35,13 @@ import psutil +def safe_print(s): + try: + print(s) + except UnicodeEncodeError: + print(s.encode('ascii', 'ignore').decode()) + + def main(): if len(sys.argv) != 2: sys.exit('usage: pmap ') @@ -45,7 +52,7 @@ def main(): total_rss = 0 for m in p.memory_maps(grouped=False): total_rss += m.rss - print(templ % ( + safe_print(templ % ( m.addr.split('-')[0].zfill(16), str(m.rss / 1024) + 'K', m.perms, From da17f5e3d0b5fc7d3812fc1556426430a8464624 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 28 Sep 2018 19:13:02 +0200 Subject: [PATCH 0077/1714] Fix decoding error in tests https://travis-ci.org/giampaolo/psutil/jobs/434570177 --- psutil/tests/test_process.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 80f9796ffd..a4adf367f9 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -31,6 +31,7 @@ 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 @@ -626,7 +627,7 @@ def test_memory_maps(self): raise else: # https://github.com/giampaolo/psutil/issues/759 - with open('/proc/self/smaps') as f: + with open_text('/proc/self/smaps') as f: data = f.read() if "%s (deleted)" % nt.path not in data: raise From 13ef73cd5c41155b6cb6cd5b51cd5d6c100b4d47 Mon Sep 17 00:00:00 2001 From: Alex Manuskin Date: Mon, 1 Oct 2018 15:50:04 +0300 Subject: [PATCH 0078/1714] Add parsing for /sys/class/thermal (#1345) Add parsing for /sys/class/thermal --- psutil/_pslinux.py | 44 ++++++++++++++++++++++++++++++++++++++ psutil/tests/test_linux.py | 40 +++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1575dad9d2..a69b1d664c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1213,6 +1213,50 @@ def sensors_temperatures(): ret[unit_name].append((label, current, high, critical)) + # 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(cat(path)) / 1000.0 + path = os.path.join(base, 'type') + unit_name = cat(path, binary=False) + except (IOError, OSError, ValueError) as err: + warnings.warn("ignoring %r for file %r" % (err, path), + RuntimeWarning) + 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='', binary=False) + if trip_type == 'critical': + critical = cat(os.path.join(base, trip_point + "_temp"), + fallback=None) + elif trip_type == 'high': + high = cat(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 ret diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index a0527851f5..c565f6c2f5 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1466,6 +1466,8 @@ 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) @@ -1477,7 +1479,7 @@ def open_mock(name, *args, **kwargs): assert m.called self.assertIn("ignoring", str(ws[0].message)) - def test_emulate_data(self): + def test_emulate_class_hwmon(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): return io.StringIO(u("name")) @@ -1495,6 +1497,7 @@ def open_mock(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] @@ -1503,6 +1506,41 @@ def open_mock(name, *args, **kwargs): 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): From 33b7c7a7574c288c17265661d96e9f557f168aaa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 1 Oct 2018 14:52:50 +0200 Subject: [PATCH 0079/1714] #1284, #1345 - give credits to @amanusk --- CREDITS | 2 +- HISTORY.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index a7e2dcbefc..4affa0f9ed 100644 --- a/CREDITS +++ b/CREDITS @@ -546,7 +546,7 @@ I: 1278 N: Alex Manuskin W: https://github.com/amanusk -I: 1284 +I: 1284, 1345 N: Sylvain Duchesne W: https://github.com/sylvainduchesne diff --git a/HISTORY.rst b/HISTORY.rst index b1226489f7..feb42ad4d6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,12 @@ XXXX-XX-XX +**Enhancements** + +- 1310_: [Linux] psutil.sensors_temperatures() now parses /sys/class/thermal + in case /sys/class/hwmon fs is not available (e.g. Raspberry Pi). (patch + by Alex Manuskin) + **Bug fixes** - 1294_: [Windows] psutil.Process().connections() may sometimes fail with From a7900080aed852598776cd3b6c8408c117160e8d Mon Sep 17 00:00:00 2001 From: alxchk Date: Thu, 11 Oct 2018 13:15:08 +0300 Subject: [PATCH 0080/1714] Fix https://github.com/giampaolo/psutil/issues/1346 (#1347) --- psutil/_psutil_sunos.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 99069f56af..0717f19503 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1173,7 +1173,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { mibhdr.len = 0; #endif memcpy(buf, &tor, sizeof tor); - memcpy(buf + sizeof tor, &mibhdr, sizeof mibhdr); + memcpy(buf + tor.OPT_offset, &mibhdr, sizeof mibhdr); ctlbuf.buf = buf; ctlbuf.len = tor.OPT_offset + tor.OPT_length; @@ -1213,6 +1213,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } + memset(&mibhdr, 0x0, sizeof(mibhdr)); + memcpy(&mibhdr, buf + toa.OPT_offset, toa.OPT_length); + databuf.maxlen = mibhdr.len; databuf.len = 0; databuf.buf = (char *)malloc((int)mibhdr.len); From 15b00d8a57630adc4ad31bd28c455382d00f2d36 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 11 Oct 2018 12:17:41 +0200 Subject: [PATCH 0081/1714] give credits to @alxchk for #1346 (sunOS) --- CREDITS | 2 +- HISTORY.rst | 2 ++ psutil/tests/test_linux.py | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CREDITS b/CREDITS index 4affa0f9ed..fea9143ede 100644 --- a/CREDITS +++ b/CREDITS @@ -486,7 +486,7 @@ I: 1042, 1079 N: Oleksii Shevchuk W: https://github.com/alxchk -I: 1077, 1093, 1091, 1220 +I: 1077, 1093, 1091, 1220, 1346 N: Prodesire W: https://github.com/Prodesire diff --git a/HISTORY.rst b/HISTORY.rst index feb42ad4d6..3a22133aeb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX intermittent 0xC0000001. (patch by Sylvain Duchesne) - 1332_: [OSX] psutil debug messages are erroneously printed all the time. (patch by Ilya Yanok) +- 1346_: [SunOS] net_connections() returns an empty list. (patch by Oleksii + Shevchuk) 5.4.7 ===== diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 299c110fe5..c162666c2c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -575,7 +575,7 @@ def test_meminfo_against_sysinfo(self): total *= unit_multiplier free *= unit_multiplier self.assertEqual(swap.total, total) - self.assertEqual(swap.free, free, delta=MEMORY_TOLERANCE) + 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 @@ -1533,7 +1533,6 @@ def glob_mock(path): 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, '') From 659f3a36626e16405eee030b695136884ea4a220 Mon Sep 17 00:00:00 2001 From: Jaime Fullaondo Date: Thu, 11 Oct 2018 06:25:03 -0400 Subject: [PATCH 0082/1714] [aix] improve compilation on AIX, better support for gcc/g++ + fix cpu metrics (#1320) --- psutil/_psutil_aix.c | 25 +++++++++++++++++++++---- psutil/_psutil_posix.c | 8 ++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 916254d5a4..898da6b26d 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -46,6 +46,7 @@ #include #include #include +#include #include "arch/aix/ifaddrs.h" #include "arch/aix/net_connections.h" @@ -617,6 +618,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { static PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { int ncpu, rc, i; + long ticks; perfstat_cpu_t *cpu = NULL; perfstat_id_t id; PyObject *py_retlist = PyList_New(0); @@ -625,6 +627,13 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; + /* get the number of ticks per second */ + ticks = sysconf(_SC_CLK_TCK); + if (ticks < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + /* get the number of cpus in ncpu */ ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); if (ncpu <= 0){ @@ -650,10 +659,10 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { for (i = 0; i < ncpu; i++) { py_cputime = Py_BuildValue( "(dddd)", - (double)cpu[i].user, - (double)cpu[i].sys, - (double)cpu[i].idle, - (double)cpu[i].wait); + (double)cpu[i].user / ticks, + (double)cpu[i].sys / ticks, + (double)cpu[i].idle / ticks, + (double)cpu[i].wait / ticks); if (!py_cputime) goto error; if (PyList_Append(py_retlist, py_cputime)) @@ -916,6 +925,10 @@ struct module_state { #define GETSTATE(m) (&_state) #endif +#ifdef __cplusplus +extern "C" { +#endif + #if PY_MAJOR_VERSION >= 3 static int @@ -986,3 +999,7 @@ void init_psutil_aix(void) return module; #endif } + +#ifdef __cplusplus +} +#endif diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index c851abbc1a..d9a8f6d1d6 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -655,6 +655,10 @@ struct module_state { #define GETSTATE(m) (&_state) #endif +#ifdef __cplusplus +extern "C" { +#endif + #if PY_MAJOR_VERSION >= 3 static int @@ -708,3 +712,7 @@ void init_psutil_posix(void) return module; #endif } + +#ifdef __cplusplus +} +#endif From 20e65b39ff0c601fad90775612c19aedb9b55713 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 11 Oct 2018 12:26:00 +0200 Subject: [PATCH 0083/1714] give CREDITS for #1320 to @truthbk --- CREDITS | 4 ++++ HISTORY.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CREDITS b/CREDITS index fea9143ede..b3c8164c9a 100644 --- a/CREDITS +++ b/CREDITS @@ -559,3 +559,7 @@ I: 1321 N: Ilya Yanok W: https://github.com/yanok I: 1332 + +N: Jaime Fullaondo +W: https://github.com/truthbk +I: 1320 diff --git a/HISTORY.rst b/HISTORY.rst index 3a22133aeb..a03b5ad252 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,11 +10,15 @@ XXXX-XX-XX - 1310_: [Linux] psutil.sensors_temperatures() now parses /sys/class/thermal in case /sys/class/hwmon fs is not available (e.g. Raspberry Pi). (patch by Alex Manuskin) +- 1320_: [Posix] better compilation support when using g++ instead of gcc. + (patch by Jaime Fullaondo) **Bug fixes** - 1294_: [Windows] psutil.Process().connections() may sometimes fail with intermittent 0xC0000001. (patch by Sylvain Duchesne) +- 1320_: [AIX] system CPU times (psutil.cpu_times()) were being reported with + ticks unit as opposed to seconds. (patch by Jaime Fullaondo) - 1332_: [OSX] psutil debug messages are erroneously printed all the time. (patch by Ilya Yanok) - 1346_: [SunOS] net_connections() returns an empty list. (patch by Oleksii From 1a0520d225053a818850b04e9b70ea8d3cce33a9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 12 Oct 2018 17:48:15 +0200 Subject: [PATCH 0084/1714] fix different travis failures --- .travis.yml | 1 - psutil/tests/__init__.py | 12 +++--------- psutil/tests/test_aix.py | 4 ++-- psutil/tests/test_contracts.py | 3 ++- psutil/tests/test_posix.py | 3 +++ psutil/tests/test_process.py | 3 +++ psutil/tests/test_unicode.py | 3 +++ 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index e522fe002f..2067f8d2f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ cache: pip matrix: include: # Linux - - python: 2.6 - python: 2.7 - python: 3.4 - python: 3.5 diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index a483ecaadd..437588a6f2 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -117,6 +117,7 @@ # 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 @@ -215,13 +216,8 @@ def attempt(exe): _testfiles_created = set() -def logstderr(s): - print(s, file=sys.stderr) - - @atexit.register def cleanup_test_files(): - logstderr("executing cleanup_test_files() atexit function") DEVNULL.close() for name in os.listdir(u('.')): if isinstance(name, unicode): @@ -229,13 +225,11 @@ def cleanup_test_files(): else: prefix = TESTFILE_PREFIX if name.startswith(prefix): - logstderr("removing temporary test file %r" % name) try: safe_rmpath(name) except Exception: traceback.print_exc() for path in _testfiles_created: - logstderr("removing temporary test file %r" % path) try: safe_rmpath(path) except Exception: @@ -245,7 +239,6 @@ def cleanup_test_files(): # this is executed first @atexit.register def cleanup_test_procs(): - logstderr("executing cleanup_test_procs() atexit function") reap_children(recursive=True) @@ -1192,11 +1185,12 @@ def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): 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 - 'python' in x.path.lower()] + exe in x.path.lower()] src = random.choice(libs) shutil.copyfile(src, dst) try: diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 7a8a4c3342..0b29215f0b 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -22,9 +22,9 @@ class AIXSpecificTestCase(unittest.TestCase): def test_virtual_memory(self): out = sh('/usr/bin/svmon -O unit=KB') - re_pattern = "memory\s*" + re_pattern = r"memory\s*" for field in ("size inuse free pin virtual available mmode").split(): - re_pattern += "(?P<%s>\S+)\s+" % (field,) + re_pattern += r"(?P<%s>\S+)\s+" % (field,) matchobj = re.search(re_pattern, out) self.assertIsNotNone( diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 877a5c0664..d936eaf88b 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -24,6 +24,7 @@ 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 @@ -512,7 +513,7 @@ def memory_full_info(self, ret, proc): value = getattr(ret, name) self.assertIsInstance(value, (int, long)) self.assertGreaterEqual(value, 0, msg=(name, value)) - if LINUX and name in ('vms', 'data'): + 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 diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 35f73eb80f..5a8fdc1731 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -185,6 +185,9 @@ def test_name(self): # 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): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a4adf367f9..aba8cdb45f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -28,6 +28,7 @@ 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 @@ -600,6 +601,8 @@ def test_memory_full_info(self): 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) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 4144b5c255..71b068c74f 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -75,6 +75,7 @@ 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 run_test_module_by_name from psutil.tests import safe_mkdir @@ -285,6 +286,8 @@ def normpath(p): 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), From 2e220c8ae1b69299eb07bcbd419cf2f59837853d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 12 Oct 2018 17:56:58 +0200 Subject: [PATCH 0085/1714] fix #715: do not print exception on import time in case cpu_times() fails. --- HISTORY.rst | 1 + psutil/__init__.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a03b5ad252..735b9345f1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,7 @@ XXXX-XX-XX **Bug fixes** +- 715_: do not print exception on import time in case cpu_times() fails. - 1294_: [Windows] psutil.Process().connections() may sometimes fail with intermittent 0xC0000001. (patch by Sylvain Duchesne) - 1320_: [AIX] system CPU times (psutil.cpu_times()) were being reported with diff --git a/psutil/__init__.py b/psutil/__init__.py index 23dc1227e3..f43ef7b6cb 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -32,7 +32,6 @@ import subprocess import sys import time -import traceback try: import pwd except ImportError: @@ -1609,14 +1608,12 @@ def cpu_times(percpu=False): except Exception: # Don't want to crash at import time. _last_cpu_times = None - traceback.print_exc() try: _last_per_cpu_times = cpu_times(percpu=True) except Exception: # Don't want to crash at import time. _last_per_cpu_times = None - traceback.print_exc() def _cpu_tot_time(times): From 5404b4fabb34de20be65638c64c2c275f1580cd4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 12 Oct 2018 19:55:47 +0200 Subject: [PATCH 0086/1714] fix travis --- psutil/tests/test_linux.py | 4 +--- psutil/tests/test_process.py | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index c162666c2c..4e4bd56504 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1128,8 +1128,7 @@ def test_boot_time(self): psutil_value = psutil.boot_time() self.assertEqual(int(vmstat_value), int(psutil_value)) - @mock.patch('psutil.traceback.print_exc') - def test_no_procfs_on_import(self, tb): + def test_no_procfs_on_import(self): my_procfs = tempfile.mkdtemp() with open(os.path.join(my_procfs, 'stat'), 'w') as f: @@ -1148,7 +1147,6 @@ def open_mock(name, *args, **kwargs): patch_point = 'builtins.open' if PY3 else '__builtin__.open' with mock.patch(patch_point, side_effect=open_mock): reload_module(psutil) - assert tb.called self.assertRaises(IOError, psutil.cpu_times) self.assertRaises(IOError, psutil.cpu_times, percpu=True) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index aba8cdb45f..2126e32adc 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -899,8 +899,9 @@ def test_cpu_affinity(self): 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)) + 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): From 8163eb1d797a2f5ab4575534a54b6dda0d027b38 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 12 Oct 2018 20:20:54 +0200 Subject: [PATCH 0087/1714] skip test on PYPY + Travis --- psutil/tests/test_unicode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 71b068c74f..cfa8dd92c2 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -309,6 +309,7 @@ def expect_exact_path_match(cls): 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") From 63aa2e7784de76ab39de8c5235a2333b4fb5c51b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 13 Oct 2018 14:58:54 +0200 Subject: [PATCH 0088/1714] travis: disable pypy; se py 3.7 on osx --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2067f8d2f2..84c340f4db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,10 +17,10 @@ matrix: env: PYVER=py27 - language: generic os: osx - env: PYVER=py36 + env: PYVER=py37 # pypy - - python: pypy - - python: pypy3 + # - python: pypy + # - python: pypy3 install: - ./.ci/travis/install.sh script: From 8c414c48d9db0f32194d8ac31ac7654e4483e024 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 16 Oct 2018 19:57:53 +0200 Subject: [PATCH 0089/1714] travis / osx: set py 3.6 --- .travis.yml | 2 +- psutil/_psutil_osx.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 84c340f4db..fdab64f629 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ matrix: env: PYVER=py27 - language: generic os: osx - env: PYVER=py37 + env: PYVER=py36 # pypy # - python: pypy # - python: pypy3 diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index b75c78b20d..0db93fce34 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -713,10 +713,10 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return Py_BuildValue( "KKKKK", total, - (unsigned long long) vm.active_count * pagesize, - (unsigned long long) vm.inactive_count * pagesize, - (unsigned long long) vm.wire_count * pagesize, - // this is how vm_stat cmd does it + (unsigned long long) vm.active_count * pagesize, // active + (unsigned long long) vm.inactive_count * pagesize, // inactive + (unsigned long long) vm.wire_count * pagesize, // wired + // free mem; this is how vm_stat cmd does it (unsigned long long) (vm.free_count - vm.speculative_count) * pagesize ); } From 2f9dcf3c7b37d4da155df62bc835ddbdfe10b24f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 16 Oct 2018 20:28:34 +0200 Subject: [PATCH 0090/1714] fix #1277 / osx / virtual_memory: 'available' and 'used' memory were not calculated properly --- HISTORY.rst | 2 ++ psutil/_psosx.py | 11 +++++++++-- psutil/_psutil_osx.c | 6 +++--- psutil/tests/test_osx.py | 6 ------ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 735b9345f1..4b2e7ed2fc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,8 @@ XXXX-XX-XX **Bug fixes** - 715_: do not print exception on import time in case cpu_times() fails. +- 1277_: [OSX] available and used memory (psutil.virtual_memory()) metrics are + not accurate. - 1294_: [Windows] psutil.Process().connections() may sometimes fail with intermittent 0xC0000001. (patch by Sylvain Duchesne) - 1320_: [AIX] system CPU times (psutil.cpu_times()) were being reported with diff --git a/psutil/_psosx.py b/psutil/_psosx.py index fbfedf3ea1..94e22bc729 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -119,9 +119,16 @@ 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) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 0db93fce34..be08de552f 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -711,13 +711,13 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return NULL; return Py_BuildValue( - "KKKKK", + "KKKKKK", total, (unsigned long long) vm.active_count * pagesize, // active (unsigned long long) vm.inactive_count * pagesize, // inactive (unsigned long long) vm.wire_count * pagesize, // wired - // free mem; this is how vm_stat cmd does it - (unsigned long long) (vm.free_count - vm.speculative_count) * pagesize + (unsigned long long) vm.free_count * pagesize, // free + (unsigned long long) vm.speculative_count * pagesize // speculative ); } diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 557af9f95f..7aabebe67b 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -225,12 +225,6 @@ def test_vmem_free(self): psutil_val = psutil.virtual_memory().free self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - @retry_before_failing() - def test_vmem_available(self): - vmstat_val = vm_stat("inactive") + vm_stat("free") - psutil_val = psutil.virtual_memory().available - self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - @retry_before_failing() def test_vmem_active(self): vmstat_val = vm_stat("active") From 8b465260638cde5370808e23eb9ace608141393a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 16 Oct 2018 21:17:52 +0200 Subject: [PATCH 0091/1714] #1197 / linux / cpu_freq(): parse /proc/cpuinfo in case /sys/devices/system/cpu fs is not available --- HISTORY.rst | 2 ++ psutil/__init__.py | 16 ++++++++++++++-- psutil/_pslinux.py | 13 +++++++++++++ psutil/tests/test_linux.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4b2e7ed2fc..4483709482 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ XXXX-XX-XX **Enhancements** +- 1197_: [Linux] cpu_freq() is now implemented by parsing /proc/cpuinfo in case + /sys/devices/system/cpu/* filesystem is not available. - 1310_: [Linux] psutil.sensors_temperatures() now parses /sys/class/thermal in case /sys/class/hwmon fs is not available (e.g. Raspberry Pi). (patch by Alex Manuskin) diff --git a/psutil/__init__.py b/psutil/__init__.py index f43ef7b6cb..c2a83fb158 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1861,13 +1861,25 @@ 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") diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index a69b1d664c..1520261c13 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -704,6 +704,19 @@ def cpu_freq(): ret.append(_common.scpufreq(curr, min_, max_)) return ret +elif os.path.exists("/proc/cpuinfo"): + def cpu_freq(): + """Alternate implementation using /proc/cpuinfo. + min and max frequencies are not available and are set to None. + """ + ret = [] + with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + for line in f: + if line.lower().startswith(b'cpu mhz'): + key, value = line.split(b'\t:', 1) + ret.append(_common.scpufreq(float(value), None, None)) + return ret + # ===================================================================== # --- network diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4e4bd56504..115a6af8a1 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -703,6 +703,35 @@ def glob_mock(pattern): assert psutil.cpu_freq() self.assertEqual(len(flags), 2) + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_cpu_freq_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.assertIsNone(ret.min) + self.assertIsNone(ret.max) + for freq in psutil.cpu_freq(percpu=True): + self.assertIsNone(freq.min) + self.assertIsNone(freq.max) + finally: + reload_module(psutil._pslinux) + reload_module(psutil) + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq_emulate_data(self): def open_mock(name, *args, **kwargs): From 6db8d2e41dc67621eae9d8eeab3c39fef7e2ddf4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 16 Oct 2018 22:18:33 +0200 Subject: [PATCH 0092/1714] refactor hasattr() checks as global constants --- psutil/_psbsd.py | 15 ++++++++++----- psutil/_pslinux.py | 4 +++- psutil/_pswindows.py | 3 ++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 7f4bcb6de6..c2896cb7cc 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -103,6 +103,11 @@ PAGESIZE = os.sysconf("SC_PAGE_SIZE") 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, @@ -211,7 +216,7 @@ def 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""" ret = [] @@ -678,7 +683,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: @@ -798,7 +803,7 @@ def cwd(self): elif NETBSD: with wrap_exceptions_procfs(self): return os.readlink("/proc/%s/cwd" % self.pid) - elif hasattr(cext, 'proc_open_files'): + elif 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 @@ -817,7 +822,7 @@ def _not_implemented(self): # 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.""" @@ -828,7 +833,7 @@ def open_files(self): # 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.""" diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1520261c13..236934fc9d 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -73,6 +73,7 @@ POWER_SUPPLY_PATH = "/sys/class/power_supply" HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) HAS_PRLIMIT = hasattr(cext, "linux_prlimit") +HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get") _DEFAULT = object() # RLIMIT_* constants, not guaranteed to be present on all kernels @@ -1928,7 +1929,7 @@ def cpu_affinity_set(self, 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): @@ -1971,6 +1972,7 @@ def ionice_set(self, ioclass, value): return cext.proc_ioprio_set(self.pid, ioclass, value) if HAS_PRLIMIT: + @wrap_exceptions def rlimit(self, resource, limits=None): # If pid is 0 prlimit() applies to the calling process and diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 18651d6cf3..b938d42ff0 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -83,6 +83,7 @@ cext.ERROR_ACCESS_DENIED]) NO_SUCH_SERVICE_ERRSET = frozenset([cext.ERROR_INVALID_NAME, cext.ERROR_SERVICE_DOES_NOT_EXIST]) +HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") if enum is None: @@ -928,7 +929,7 @@ def nice_set(self, value): return cext.proc_priority_set(self.pid, value) # available on Windows >= Vista - if hasattr(cext, "proc_io_priority_get"): + if HAS_PROC_IO_PRIORITY: @wrap_exceptions def ionice_get(self): return cext.proc_io_priority_get(self.pid) From fbece8e45991e8cc070bfd5e9efcd4974227abcd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 19 Oct 2018 14:27:56 +0200 Subject: [PATCH 0093/1714] fix #1307: [Linux] disk_partitions() does not honour PROCFS_PATH --- HISTORY.rst | 1 + psutil/_pslinux.py | 11 +++++++++-- psutil/_psutil_linux.c | 9 ++++++--- psutil/tests/test_connections.py | 1 + psutil/tests/test_linux.py | 12 ++++++++++++ psutil/tests/test_system.py | 6 +----- 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4483709482..a02b8e2c03 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,7 @@ XXXX-XX-XX not accurate. - 1294_: [Windows] psutil.Process().connections() may sometimes fail with intermittent 0xC0000001. (patch by Sylvain Duchesne) +- 1307_: [Linux] disk_partitions() does not honour PROCFS_PATH. - 1320_: [AIX] system CPU times (psutil.cpu_times()) were being reported with ticks unit as opposed to seconds. (patch by Jaime Fullaondo) - 1332_: [OSX] psutil debug messages are erroneously printed all the time. diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 236934fc9d..82e3218996 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1140,7 +1140,8 @@ def read_sysfs(): 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: + procfs_path = get_procfs_path() + with open_text("%s/filesystems" % procfs_path) as f: for line in f: line = line.strip() if not line.startswith("nodev"): @@ -1151,8 +1152,14 @@ def disk_partitions(all=False): if fstype == "zfs": fstypes.add("zfs") + # See: https://github.com/giampaolo/psutil/issues/1307 + if procfs_path == "/proc": + mtab_path = os.path.realpath("/etc/mtab") + else: + mtab_path = os.path.realpath("%s/self/mounts" % procfs_path) + retlist = [] - partitions = cext.disk_partitions() + partitions = cext.disk_partitions(mtab_path) for partition in partitions: device, mountpoint, fstype, opts = partition if device == 'none': diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index d1f0d14551..bd27b5f9c6 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -195,6 +195,7 @@ static PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { FILE *file = NULL; struct mntent *entry; + const char *mtab_path; PyObject *py_dev = NULL; PyObject *py_mountp = NULL; PyObject *py_tuple = NULL; @@ -203,12 +204,14 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - // MOUNTED constant comes from mntent.h and it's == '/etc/mtab' + if (!PyArg_ParseTuple(args, "s", &mtab_path)) + return NULL; + Py_BEGIN_ALLOW_THREADS - file = setmntent(MOUNTED, "r"); + file = setmntent(mtab_path, "r"); Py_END_ALLOW_THREADS if ((file == 0) || (file == NULL)) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, MOUNTED); + PyErr_SetFromErrnoWithFilename(PyExc_OSError, mtab_path); goto error; } diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index cba835e14f..7f59a74cbd 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -53,6 +53,7 @@ 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') diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 115a6af8a1..4b72f72572 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1011,6 +1011,18 @@ def test_disk_partitions_mocked(self): assert ret self.assertEqual(ret[0].fstype, 'zfs') + def test_disk_partitions_procfs(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" + def test_disk_io_counters_kernel_2_4_mocked(self): # Tests /proc/diskstats parsing format for 2.4 kernels, see: # https://github.com/giampaolo/psutil/issues/767 diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 8b07caff65..7cc678f057 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -522,11 +522,7 @@ def test_disk_partitions(self): if err.errno not in (errno.EPERM, errno.EACCES): raise else: - 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 os.path.exists(disk.mountpoint), disk self.assertIsInstance(disk.fstype, str) self.assertIsInstance(disk.opts, str) From 7300bd48b4ea6fd98309cc9da569d6015b1ac24a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 19 Oct 2018 21:49:40 +0200 Subject: [PATCH 0094/1714] fix #1004: Process.io_counters() may raise ValueError --- HISTORY.rst | 1 + IDEAS | 5 +++++ psutil/_pslinux.py | 29 +++++++++++++++++++---------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a02b8e2c03..71cff74eef 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ XXXX-XX-XX **Bug fixes** - 715_: do not print exception on import time in case cpu_times() fails. +- 1004_: [Linux] Process.io_counters() may raise ValueError. - 1277_: [OSX] available and used memory (psutil.virtual_memory()) metrics are not accurate. - 1294_: [Windows] psutil.Process().connections() may sometimes fail with diff --git a/IDEAS b/IDEAS index 8190d48b98..2178333fe9 100644 --- a/IDEAS +++ b/IDEAS @@ -12,12 +12,17 @@ PLATFORMS - #82: Cygwin (PR at #998) - #276: GNU/Hurd - #693: Windows Nano +- #1251: Windows bash - DragonFlyBSD - HP-UX FEATURES ======== +- #1115: users() idle time. + +- #1102: Process.is64bit(). + - #371: sensors_temperatures() at least for macOS. - #669: Windows / net_if_addrs(): return broadcast addr. diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 82e3218996..71f0c98435 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1650,18 +1650,27 @@ 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 - ) + 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[0], fname, fields)) else: def io_counters(self): raise NotImplementedError("couldn't find /proc/%s/io (kernel " From d8b05151e65f9348aff9b58da977abd8cacb2127 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 20 Oct 2018 20:52:40 +0200 Subject: [PATCH 0095/1714] sensors_temperatures() / linux: convert defaultdict to dict --- IDEAS | 1 + psutil/_pslinux.py | 2 +- setup.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/IDEAS b/IDEAS index 2178333fe9..babc0e8be1 100644 --- a/IDEAS +++ b/IDEAS @@ -169,3 +169,4 @@ RESOURCES - libstatgrab: http://www.i-scream.org/libstatgrab/ - top: http://www.unixtop.org/ - oshi: https://github.com/oshi/oshi +- netdata: https://github.com/netdata/netdata diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 71f0c98435..ecc4c703f9 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1278,7 +1278,7 @@ def sensors_temperatures(): ret[unit_name].append(('', current, high, critical)) - return ret + return dict(ret) def sensors_fans(): diff --git a/setup.py b/setup.py index c4845a8d74..28cde6f12c 100755 --- a/setup.py +++ b/setup.py @@ -284,7 +284,8 @@ def main(): 'ps', 'top', 'kill', 'free', 'lsof', 'netstat', 'nice', 'tty', 'ionice', 'uptime', 'taskmgr', 'process', 'df', 'iotop', 'iostat', 'ifconfig', 'taskset', 'who', 'pidof', 'pmap', 'smem', 'pstree', - 'monitoring', 'ulimit', 'prlimit', 'smem', + 'monitoring', 'ulimit', 'prlimit', 'smem', 'performance', + 'metrics', 'agent', 'observability', ], author='Giampaolo Rodola', author_email='g.rodola@gmail.com', From 648d8ba39eff4867d461a45a77d1245e2a909234 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 30 Oct 2018 10:57:16 +0100 Subject: [PATCH 0096/1714] pre release --- HISTORY.rst | 2 +- docs/index.rst | 6 +++++- psutil/_psutil_windows.c | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 71cff74eef..5a270184f3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.4.8 ===== -XXXX-XX-XX +2018-10-30 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 7763b6de0a..91abffe84c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -702,7 +702,7 @@ Sensors See also `temperatures.py `__ and `sensors.py `__ for an example application. - Availability: Linux, macOS + Availability: Linux .. versionadded:: 5.1.0 @@ -2638,6 +2638,10 @@ take a look at the Timeline ======== +- 2018-10-30: + `5.4.8 `__ - + `what's new `__ - + `diff `__ - 2018-08-14: `5.4.7 `__ - `what's new `__ - diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 4acea3600a..edb5996c49 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2431,7 +2431,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { /* - * Return a Python dict of tuples for disk I/O information + * Return a Python dict of tuples for disk I/O information. This may + * require running "diskperf -y" command first. */ static PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { From bb5d032be76980a9e110f03f1203bd35fa85a793 Mon Sep 17 00:00:00 2001 From: Alex Manuskin Date: Sat, 3 Nov 2018 16:06:13 +0200 Subject: [PATCH 0097/1714] FreeBSD adding temperature sensors (WIP) (#1350) FreeBSD: add temperature sensors --- psutil/_psbsd.py | 18 +++++++++++++++++ psutil/_psutil_bsd.c | 2 ++ psutil/arch/freebsd/specific.c | 36 ++++++++++++++++++++++++++++++++++ psutil/arch/freebsd/specific.h | 1 + psutil/tests/test_bsd.py | 17 ++++++++++++++++ psutil/tests/test_contracts.py | 4 ++-- 6 files changed, 76 insertions(+), 2 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index c2896cb7cc..0727dd2e87 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -11,6 +11,7 @@ import xml.etree.ElementTree as ET from collections import namedtuple from socket import AF_INET +from collections import defaultdict from . import _common from . import _psposix @@ -437,6 +438,23 @@ 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 diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 4e91c02ede..6b366f13bf 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -981,6 +981,8 @@ PsutilMethods[] = { #if defined(PSUTIL_FREEBSD) {"sensors_battery", psutil_sensors_battery, METH_VARARGS, "Return battery information."}, + {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS, + "Return temperature information for a given CPU core number."}, #endif // --- others diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 177ac8a6bd..14be26b367 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -31,6 +31,7 @@ #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) #define PSUTIL_BT2MSEC(bt) (bt.sec * 1000 + (((uint64_t) 1000000000 * (uint32_t) \ (bt.frac >> 32) ) >> 32 ) / 1000000) +#define DECIKELVIN_2_CELCIUS(t) (t - 2731) / 10 #ifndef _PATH_DEVNULL #define _PATH_DEVNULL "/dev/null" #endif @@ -1010,3 +1011,38 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } + + +/* + * Return temperature information for a given CPU core number. + */ +PyObject * +psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { + int current; + int tjmax; + int core; + char sensor[26]; + size_t size = sizeof(current); + + if (! PyArg_ParseTuple(args, "i", &core)) + return NULL; + sprintf(sensor, "dev.cpu.%d.temperature", core); + if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) + goto error; + current = DECIKELVIN_2_CELCIUS(current); + + // Return -273 in case of faliure. + sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); + if (sysctlbyname(sensor, &tjmax, &size, NULL, 0)) + tjmax = 0; + tjmax = DECIKELVIN_2_CELCIUS(tjmax); + + return Py_BuildValue("ii", current, tjmax); + +error: + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "no temperature sensors"); + else + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} diff --git a/psutil/arch/freebsd/specific.h b/psutil/arch/freebsd/specific.h index 0df66eccbf..cb71ff612f 100644 --- a/psutil/arch/freebsd/specific.h +++ b/psutil/arch/freebsd/specific.h @@ -29,4 +29,5 @@ PyObject* psutil_virtual_mem(PyObject* self, PyObject* args); PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); #if defined(PSUTIL_FREEBSD) PyObject* psutil_sensors_battery(PyObject* self, PyObject* args); +PyObject* psutil_sensors_cpu_temperature(PyObject* self, PyObject* args); #endif diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 7846c1ca25..df43a023e8 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -427,6 +427,23 @@ def test_sensors_battery_no_battery(self): 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 diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index d936eaf88b..8ff41e5b35 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -119,7 +119,7 @@ def test_cpu_freq(self): def test_sensors_temperatures(self): self.assertEqual( - hasattr(psutil, "sensors_temperatures"), LINUX) + hasattr(psutil, "sensors_temperatures"), LINUX or FREEBSD) def test_sensors_fans(self): self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX) @@ -337,7 +337,7 @@ def test_fetch_all(self): self.assertEqual(err.name, p.name()) assert str(err) assert err.msg - except Exception as err: + except Exception: s = '\n' + '=' * 70 + '\n' s += "FAIL: test_%s (proc=%s" % (name, p) if ret != default: From 7ddfa9a637f56dbe236dde285991569783fe444e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 3 Nov 2018 15:45:50 +0100 Subject: [PATCH 0098/1714] #1350: give credits to @amanusk --- CREDITS | 2 +- HISTORY.rst | 7 +++++++ docs/index.rst | 17 ++--------------- psutil/__init__.py | 2 +- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/CREDITS b/CREDITS index b3c8164c9a..404310f50d 100644 --- a/CREDITS +++ b/CREDITS @@ -546,7 +546,7 @@ I: 1278 N: Alex Manuskin W: https://github.com/amanusk -I: 1284, 1345 +I: 1284, 1345, 1350 N: Sylvain Duchesne W: https://github.com/sylvainduchesne diff --git a/HISTORY.rst b/HISTORY.rst index 5a270184f3..97f7b52275 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,12 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.5.0 +===== + +**Enhancements** + +- 1350_: [FreeBSD] added support for sensors_temperatures(). + 5.4.8 ===== diff --git a/docs/index.rst b/docs/index.rst index 91abffe84c..d54129c248 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -702,14 +702,11 @@ Sensors See also `temperatures.py `__ and `sensors.py `__ for an example application. - Availability: Linux + Availability: Linux, FreeBSD .. versionadded:: 5.1.0 - .. warning:: - - this API is experimental. Backward incompatible changes may occur if - deemed necessary. + .. versionchanged:: 5.5.0 added FreeBSD support .. function:: sensors_fans() @@ -730,11 +727,6 @@ Sensors .. versionadded:: 5.2.0 - .. warning:: - - this API is experimental. Backward incompatible changes may occur if - deemed necessary. - .. function:: sensors_battery() Return battery status information as a named tuple including the following @@ -774,11 +766,6 @@ Sensors .. versionchanged:: 5.4.2 added macOS support - .. warning:: - - this API is experimental. Backward incompatible changes may occur if - deemed necessary. - Other system info ----------------- diff --git a/psutil/__init__.py b/psutil/__init__.py index c2a83fb158..5a5720d7ad 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -220,7 +220,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.4.8" +__version__ = "5.5.0" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED From 8f99f3782663959062ee868bbfdbc336307a3a4d Mon Sep 17 00:00:00 2001 From: Koen Kooi Date: Mon, 5 Nov 2018 15:17:16 +0000 Subject: [PATCH 0099/1714] Fix #1354 [Linux] disk_io_counters() fails on Linux kernel 4.18+ (#1360) Linux kernel 4.18+ added 4 fields, ingore them and parse the rest as usual. --- psutil/_pslinux.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ecc4c703f9..b775d39ae7 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1060,6 +1060,8 @@ 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" # See: # https://www.kernel.org/doc/Documentation/iostats.txt # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats @@ -1074,7 +1076,7 @@ def read_procfs(): 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, From 76682c0cfdf1bfe0db8993f261c7ab761643af93 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 5 Nov 2018 16:20:54 +0100 Subject: [PATCH 0100/1714] give credits to @koenkooi for #1360 --- CREDITS | 4 ++++ HISTORY.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CREDITS b/CREDITS index 404310f50d..ee06df0d73 100644 --- a/CREDITS +++ b/CREDITS @@ -563,3 +563,7 @@ I: 1332 N: Jaime Fullaondo W: https://github.com/truthbk I: 1320 + +N: Koen Kooi +W: https://github.com/koenkooi +I: 1360 diff --git a/HISTORY.rst b/HISTORY.rst index 97f7b52275..56d33296ae 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,10 @@ - 1350_: [FreeBSD] added support for sensors_temperatures(). +**Bug fixes** + +- 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. + 5.4.8 ===== From e56c7ed2990117edccf1803a56b509b2518f8230 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 7 Nov 2018 13:12:45 +0100 Subject: [PATCH 0101/1714] fix PEP8 style mistakes --- psutil/tests/__init__.py | 2 +- psutil/tests/test_aix.py | 14 +++++++------- psutil/tests/test_osx.py | 2 +- scripts/internal/check_broken_links.py | 2 +- scripts/internal/print_announce.py | 2 +- scripts/internal/winmake.py | 2 +- scripts/winservices.py | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 437588a6f2..eb49b9c2b5 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -602,7 +602,7 @@ def __init__(self, timeout=None, retries=None, interval=0.001, - logfun=lambda s: print(s, file=sys.stderr), + logfun=print, ): if timeout and retries: raise ValueError("timeout and retries args are mutually exclusive") diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 0b29215f0b..4f0da4e000 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -56,10 +56,10 @@ def test_swap_memory(self): # 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("(?P\S+)\s+" - "(?P\S+)\s+" - "(?P\S+)\s+" - "(?P\d+)MB", out) + 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") @@ -74,11 +74,11 @@ def test_swap_memory(self): def test_cpu_stats(self): out = sh('/usr/bin/mpstat -a') - re_pattern = "ALL\s*" + 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 += "(?P<%s>\S+)\s+" % (field,) + re_pattern += r"(?P<%s>\S+)\s+" % (field,) matchobj = re.search(re_pattern, out) self.assertIsNotNone( @@ -106,7 +106,7 @@ def test_cpu_stats(self): def test_cpu_count_logical(self): out = sh('/usr/bin/mpstat -a') - mpstat_lcpu = int(re.search("lcpu=(\d+)", out).group(1)) + mpstat_lcpu = int(re.search(r"lcpu=(\d+)", out).group(1)) psutil_lcpu = psutil.cpu_count(logical=True) self.assertEqual(mpstat_lcpu, psutil_lcpu) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 7aabebe67b..cf5c5ea8f2 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -285,7 +285,7 @@ def test_net_if_stats(self): @unittest.skipIf(not HAS_BATTERY, "no battery") def test_sensors_battery(self): out = sh("pmset -g batt") - percent = re.search("(\d+)%", out).group(1) + 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() diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 7cf1e4898d..3d108d810d 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -150,7 +150,7 @@ def parse_c(fname): subidx = i + 1 while True: nextline = lines[subidx].strip() - if re.match('^\* .+', nextline): + if re.match(r'^\* .+', nextline): url += nextline[1:].strip() else: break diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index c92cbcb2ec..a39351d61e 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -81,7 +81,7 @@ def get_changes(): for i, line in enumerate(lines): line = lines.pop(0) line = line.rstrip() - if re.match("^- \d+_: ", line): + if re.match(r"^- \d+_: ", line): num, _, rest = line.partition(': ') num = ''.join([x for x in num if x.isdigit()]) line = "- #%s: %s" % (num, rest) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c4722a0298..b1ce7b8a4d 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -495,7 +495,7 @@ def set_python(s): '26-64', '27-64', '34-64', '35-64', '36-64', '37-64') for v in vers: if s == v: - path = 'C:\\python%s\python.exe' % s + path = r'C:\\python%s\python.exe' % s if os.path.isfile(path): print(path) PYTHON = path diff --git a/scripts/winservices.py b/scripts/winservices.py index 1a65adcefc..b52aaf21dd 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -4,7 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" +r""" List all Windows services installed. $ python scripts/winservices.py From e2596ab17b8ae8321d8d82fb501737747f6cd31a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 7 Nov 2018 13:13:55 +0100 Subject: [PATCH 0102/1714] disable false positive mem test on travis + osx --- psutil/tests/test_memory_leaks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index ce08245968..fc3a0365f1 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -524,6 +524,7 @@ def test_pids(self): # --- net + @unittest.skipIf(TRAVIS and MACOS, "false positive on travis") @skip_if_linux() def test_net_io_counters(self): self.execute(psutil.net_io_counters, nowrap=False) From fbe821ebccb61def22cea953f1df83134fa01d0e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 Nov 2018 18:35:59 +0100 Subject: [PATCH 0103/1714] #1359: add test case for cpu_count(logical=False) against lscpu utility --- psutil/tests/test_linux.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4b72f72572..0f981b6bc5 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -639,6 +639,16 @@ def test_cpu_count_logical_w_lscpu(self): num = len([x for x in out.split('\n') if not x.startswith('#')]) self.assertEqual(psutil.cpu_count(logical=True), num) + @unittest.skipIf(not which("lscpu"), "lscpu utility not available") + def test_cpu_count_physical_w_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_cpu_count_logical_mocked(self): import psutil._pslinux original = psutil._pslinux.cpu_count_logical() From 459556dd1e2979cdee22177339ced0761caf4c83 Mon Sep 17 00:00:00 2001 From: Alex Manuskin Date: Sat, 1 Dec 2018 20:14:53 +0200 Subject: [PATCH 0104/1714] Add CPU frequency support for FreeBSD (#1369) Add CPU frequency support for FreeBSD (patch by @amanusk) --- psutil/_psbsd.py | 26 ++++++++++++++++++++++++++ psutil/_psutil_bsd.c | 2 ++ psutil/arch/freebsd/specific.c | 33 +++++++++++++++++++++++++++++++++ psutil/arch/freebsd/specific.h | 1 + psutil/tests/test_bsd.py | 18 ++++++++++++++++++ psutil/tests/test_contracts.py | 2 +- 6 files changed, 81 insertions(+), 1 deletion(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 0727dd2e87..d3ce7b5ccb 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -455,6 +455,32 @@ def sensors_temperatures(): return ret + def cpu_freq(): + """ + Return frequency metrics for CPUs. Currently, only CPU 0 is supported + by FreeBSD, 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_frequency(cpu) + except NotImplementedError: + continue + min_freq = None + max_freq = None + if available_freq: + try: + min_freq = int(available_freq.split(" ")[-1].split("/")[0]) + except(IndexError, ValueError): + pass + try: + max_freq = int(available_freq.split(" ")[0].split("/")[0]) + except(IndexError, ValueError): + pass + ret.append(_common.scpufreq(current, min_freq, max_freq)) + return ret + # ===================================================================== # --- other system functions diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 6b366f13bf..dce157f559 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -983,6 +983,8 @@ PsutilMethods[] = { "Return battery information."}, {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS, "Return temperature information for a given CPU core number."}, + {"cpu_frequency", psutil_cpu_freq, METH_VARARGS, + "Return frequency of a given CPU"}, #endif // --- others diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 14be26b367..70dc7f2ce4 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -1046,3 +1046,36 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } + + +/* + * Return frequency information of a given CPU + */ +PyObject * +psutil_cpu_freq(PyObject *self, PyObject *args) { + int current; + int core; + char sensor[26]; + char available_freq_levels[1000]; + size_t size = sizeof(current); + + if (! PyArg_ParseTuple(args, "i", &core)) + return NULL; + sprintf(sensor, "dev.cpu.%d.freq", core); + if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) + goto error; + + size = sizeof(available_freq_levels); + // In case of failure, an empty string is returned + sprintf(sensor, "dev.cpu.%d.freq_levels", core); + sysctlbyname(sensor, &available_freq_levels, &size, NULL, 0); + + return Py_BuildValue("is", current, available_freq_levels); + +error: + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "Unable to read frequency"); + else + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} diff --git a/psutil/arch/freebsd/specific.h b/psutil/arch/freebsd/specific.h index cb71ff612f..875c816646 100644 --- a/psutil/arch/freebsd/specific.h +++ b/psutil/arch/freebsd/specific.h @@ -30,4 +30,5 @@ PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); #if defined(PSUTIL_FREEBSD) PyObject* psutil_sensors_battery(PyObject* self, PyObject* args); PyObject* psutil_sensors_cpu_temperature(PyObject* self, PyObject* args); +PyObject* psutil_cpu_freq(PyObject* self, PyObject* args); #endif diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index df43a023e8..87feb14733 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -250,6 +250,24 @@ def test_proc_cpu_times(self): if len(tested) != 2: raise RuntimeError("couldn't find lines match in procstat out") + # --- cpu_freq(); tests against sysctl + 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_before_failing() diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 8ff41e5b35..78e6ba22fa 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -115,7 +115,7 @@ def test_cpu_freq(self): (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) + linux or MACOS or WINDOWS or FREEBSD) def test_sensors_temperatures(self): self.assertEqual( From fd0b6d76a6c3319119f9ae932a965ac06c5855c4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Dec 2018 19:28:02 +0100 Subject: [PATCH 0105/1714] give CREDITS to @amanusk for #1369 / #1352 and update doc --- CREDITS | 2 +- HISTORY.rst | 7 +++++++ docs/index.rst | 3 ++- psutil/__init__.py | 2 +- psutil/_psbsd.py | 12 +++++------- psutil/arch/freebsd/specific.c | 10 +++++++--- psutil/tests/test_bsd.py | 5 ++--- 7 files changed, 25 insertions(+), 16 deletions(-) diff --git a/CREDITS b/CREDITS index ee06df0d73..30d9006f99 100644 --- a/CREDITS +++ b/CREDITS @@ -546,7 +546,7 @@ I: 1278 N: Alex Manuskin W: https://github.com/amanusk -I: 1284, 1345, 1350 +I: 1284, 1345, 1350, 1352 N: Sylvain Duchesne W: https://github.com/sylvainduchesne diff --git a/HISTORY.rst b/HISTORY.rst index 56d33296ae..7cfe0f191d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,12 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.5.1 +===== + +**Enhancements** + +- 1352_: [FreeBSD] added support for CPU frequency. (patch by Alex Manuskin) + 5.5.0 ===== diff --git a/docs/index.rst b/docs/index.rst index d54129c248..85506a2802 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -235,10 +235,11 @@ CPU scpufreq(current=1703.609, min=800.0, max=3500.0), scpufreq(current=1754.289, min=800.0, max=3500.0)] - Availability: Linux, macOS, Windows + Availability: Linux, macOS, Windows, FreeBSD .. versionadded:: 5.1.0 + .. versionchanged:: 5.5.1 added FreeBSD support. Memory ------ diff --git a/psutil/__init__.py b/psutil/__init__.py index 5a5720d7ad..3548cdcce0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -220,7 +220,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.5.0" +__version__ = "5.5.1" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index d3ce7b5ccb..1dc7231284 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -456,9 +456,9 @@ def sensors_temperatures(): return ret def cpu_freq(): - """ - Return frequency metrics for CPUs. Currently, only CPU 0 is supported - by FreeBSD, all other cores match the frequency of CPU 0. + """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() @@ -467,17 +467,15 @@ def cpu_freq(): current, available_freq = cext.cpu_frequency(cpu) except NotImplementedError: continue - min_freq = None - max_freq = None if available_freq: try: min_freq = int(available_freq.split(" ")[-1].split("/")[0]) except(IndexError, ValueError): - pass + min_freq = None try: max_freq = int(available_freq.split(" ")[0].split("/")[0]) except(IndexError, ValueError): - pass + max_freq = None ret.append(_common.scpufreq(current, min_freq, max_freq)) return ret diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 70dc7f2ce4..93bf51ff23 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -1049,7 +1049,9 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { /* - * Return frequency information of a given CPU + * Return frequency information of a given CPU. + * As of Dec 2018 only CPU 0 appears to be supported and all other + * cores match the frequency of CPU 0. */ PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { @@ -1061,12 +1063,14 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "i", &core)) return NULL; + // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ sprintf(sensor, "dev.cpu.%d.freq", core); if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) goto error; size = sizeof(available_freq_levels); - // In case of failure, an empty string is returned + // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ + // In case of failure, an empty string is returned. sprintf(sensor, "dev.cpu.%d.freq_levels", core); sysctlbyname(sensor, &available_freq_levels, &size, NULL, 0); @@ -1074,7 +1078,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { error: if (errno == ENOENT) - PyErr_SetString(PyExc_NotImplementedError, "Unable to read frequency"); + PyErr_SetString(PyExc_NotImplementedError, "unable to read frequency"); else PyErr_SetFromErrno(PyExc_OSError); return NULL; diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 87feb14733..34b66ca6b3 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -250,10 +250,9 @@ def test_proc_cpu_times(self): if len(tested) != 2: raise RuntimeError("couldn't find lines match in procstat out") - # --- cpu_freq(); tests against sysctl def test_cpu_frequency_against_sysctl(self): # Currently only cpu 0 is frequency is supported in FreeBSD - # All other cores use the same frequency + # 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) @@ -262,7 +261,7 @@ def test_cpu_frequency_against_sysctl(self): sysctl_result = sysctl(sensor) # sysctl returns a string of the format: # / /... - # Ordered highest available to lowest available + # 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) From 3ea94c1b8589891a8d1a5781f0445cb5080b7c3e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Dec 2018 19:29:19 +0100 Subject: [PATCH 0106/1714] make flake8 happy --- psutil/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index eb49b9c2b5..c6fc8cc23e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -635,7 +635,7 @@ def wrapper(*args, **kwargs): for _ in self: try: return fun(*args, **kwargs) - except self.exception as _: + except self.exception as _: # NOQA exc = _ if self.logfun is not None: self.logfun(exc) From b2dbcbc407920a39a0e0087ef3f507e6dab747cb Mon Sep 17 00:00:00 2001 From: EccoTheFlintstone <32797240+EccoTheFlintstone@users.noreply.github.com> Date: Mon, 3 Dec 2018 07:51:01 -0500 Subject: [PATCH 0107/1714] fix ionice set not working on windows x64 due to LENGTH_MISMATCH (#1368) Fix Process().ionice(0/1/2) length mismatch on Windows --- psutil/_psutil_windows.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index edb5996c49..29311992be 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2098,7 +2098,7 @@ static PyObject * psutil_proc_io_priority_get(PyObject *self, PyObject *args) { long pid; HANDLE hProcess; - PULONG IoPriority; + DWORD IoPriority; _NtQueryInformationProcess NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress( @@ -2114,7 +2114,7 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { hProcess, ProcessIoPriority, &IoPriority, - sizeof(ULONG), + sizeof(DWORD), NULL ); CloseHandle(hProcess); @@ -2128,8 +2128,9 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { static PyObject * psutil_proc_io_priority_set(PyObject *self, PyObject *args) { long pid; - int prio; + DWORD prio; HANDLE hProcess; + DWORD dwDesiredAccess = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; _NtSetInformationProcess NtSetInformationProcess = (_NtSetInformationProcess)GetProcAddress( @@ -2143,7 +2144,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "li", &pid, &prio)) return NULL; - hProcess = psutil_handle_from_pid_waccess(pid, PROCESS_ALL_ACCESS); + hProcess = psutil_handle_from_pid_waccess(pid, dwDesiredAccess); if (hProcess == NULL) return NULL; @@ -2151,7 +2152,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { hProcess, ProcessIoPriority, (PVOID)&prio, - sizeof((PVOID)prio) + sizeof(DWORD) ); CloseHandle(hProcess); From 9f44b6aa4b759c9c4f41dc9cd7df135a013f27ed Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 3 Dec 2018 13:53:02 +0100 Subject: [PATCH 0108/1714] give CREDITS to @EccoTheFlintstone for #1368 --- CREDITS | 4 ++++ HISTORY.rst | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/CREDITS b/CREDITS index 30d9006f99..f238f85ba0 100644 --- a/CREDITS +++ b/CREDITS @@ -567,3 +567,7 @@ I: 1320 N: Koen Kooi W: https://github.com/koenkooi I: 1360 + +N: EccoTheFlintstone +W: https://github.com/EccoTheFlintstone +I: 1368 diff --git a/HISTORY.rst b/HISTORY.rst index 7cfe0f191d..1adea0962d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,11 @@ - 1352_: [FreeBSD] added support for CPU frequency. (patch by Alex Manuskin) +**Bug fixes** + +- 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by + EccoTheFlintstone) + 5.5.0 ===== From c0f6b1d6514bad029995305e68dd127206e82864 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Dec 2018 11:32:31 +0100 Subject: [PATCH 0109/1714] sort HISTORY --- HISTORY.rst | 18 ++++++------------ psutil/__init__.py | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1adea0962d..7a26f50e3a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,28 +1,22 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.5.1 +5.5.0 ===== +XXXX-XX-XX + **Enhancements** +- 1350_: [FreeBSD] added support for sensors_temperatures(). (patch by Alex + Manuskin) - 1352_: [FreeBSD] added support for CPU frequency. (patch by Alex Manuskin) **Bug fixes** +- 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. - 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by EccoTheFlintstone) -5.5.0 -===== - -**Enhancements** - -- 1350_: [FreeBSD] added support for sensors_temperatures(). - -**Bug fixes** - -- 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. - 5.4.8 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 3548cdcce0..5a5720d7ad 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -220,7 +220,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.5.1" +__version__ = "5.5.0" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED From 5398c48047d424af97644879fb4eaa7aad432f58 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Dec 2018 12:32:31 +0100 Subject: [PATCH 0110/1714] #1111 make Process.oneshot() thread-safe --- HISTORY.rst | 1 + psutil/__init__.py | 69 ++++++++++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7a26f50e3a..e66f5b49c7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,7 @@ XXXX-XX-XX **Bug fixes** +- 1111_: Process.oneshot() is now thread safe. - 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. - 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by EccoTheFlintstone) diff --git a/psutil/__init__.py b/psutil/__init__.py index 5a5720d7ad..0eb1979924 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -31,6 +31,7 @@ import signal import subprocess import sys +import threading import time try: import pwd @@ -360,6 +361,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._lock = threading.RLock() # cache creation time for later use in is_running() method try: self.create_time() @@ -456,40 +458,41 @@ 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 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 - 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: + 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() + 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 def as_dict(self, attrs=None, ad_value=None): """Utility method returning process information as a From fca240a81b78de019d69bfc981f38c7fd859bb6d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Dec 2018 12:40:20 +0100 Subject: [PATCH 0111/1714] revert 5398c48047d424af97644879fb4eaa7aad432f58; let's do it in a separate branch --- psutil/__init__.py | 69 ++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 0eb1979924..5a5720d7ad 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -31,7 +31,6 @@ import signal import subprocess import sys -import threading import time try: import pwd @@ -361,7 +360,6 @@ 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._lock = threading.RLock() # cache creation time for later use in is_running() method try: self.create_time() @@ -458,41 +456,40 @@ def oneshot(self): ... >>> """ - with self._lock: - 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() - # ... + 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() 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() - 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 + 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 def as_dict(self, attrs=None, ad_value=None): """Utility method returning process information as a From 10f780b7c2c0bb63417360891662680a39465140 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Dec 2018 12:42:02 +0100 Subject: [PATCH 0112/1714] update HISTORY --- HISTORY.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index e66f5b49c7..7a26f50e3a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,7 +13,6 @@ XXXX-XX-XX **Bug fixes** -- 1111_: Process.oneshot() is now thread safe. - 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. - 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by EccoTheFlintstone) From 0cc8d7b5e206203541193a8e2120b2689a7a5893 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Dec 2018 08:26:39 -0800 Subject: [PATCH 0113/1714] (Windows) use PROCESS_QUERY_LIMITED_INFORMATION access rights (#1376) #1376 / Windows / OpenProcess - use PROCESS_QUERY_LIMITED_INFORMATION wherever possible. This results in less AccessDenied exceptions being thrown for system processes. --- psutil/_psutil_windows.c | 44 ++++++++++++++++-------------- psutil/arch/windows/process_info.c | 17 ++---------- psutil/arch/windows/process_info.h | 3 +- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 29311992be..f3979de626 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -492,7 +492,8 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) return NULL; if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { @@ -546,7 +547,7 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { if (0 == pid || 4 == pid) return psutil_boot_time(NULL, NULL); - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { @@ -756,7 +757,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid_waccess(pid, PROCESS_QUERY_INFORMATION); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; if (GetProcessImageFileNameW(hProcess, exe, MAX_PATH) == 0) { @@ -824,7 +825,7 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; @@ -892,6 +893,8 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) size_t private_pages; size_t i; DWORD info_array_size; + // needed by QueryWorkingSet + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; PSAPI_WORKING_SET_INFORMATION* info_array; SYSTEM_INFO system_info; PyObject* py_result = NULL; @@ -900,7 +903,8 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - proc = psutil_handle_from_pid(pid); + + proc = psutil_handle_from_pid(pid, access); if (proc == NULL) return NULL; @@ -1350,7 +1354,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - processHandle = psutil_handle_from_pid_waccess(pid, access); + processHandle = psutil_handle_from_pid(pid, access); if (processHandle == NULL) return NULL; py_retlist = psutil_get_open_files(pid, processHandle); @@ -1412,8 +1416,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - processHandle = psutil_handle_from_pid_waccess( - pid, PROCESS_QUERY_INFORMATION); + processHandle = psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION); if (processHandle == NULL) return NULL; @@ -2055,7 +2058,7 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; priority = GetPriorityClass(hProcess); @@ -2079,7 +2082,7 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "li", &pid, &priority)) return NULL; - hProcess = psutil_handle_from_pid_waccess(pid, access); + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; retval = SetPriorityClass(hProcess, priority); @@ -2106,7 +2109,7 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; @@ -2130,7 +2133,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { long pid; DWORD prio; HANDLE hProcess; - DWORD dwDesiredAccess = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; _NtSetInformationProcess NtSetInformationProcess = (_NtSetInformationProcess)GetProcAddress( @@ -2144,7 +2147,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "li", &pid, &prio)) return NULL; - hProcess = psutil_handle_from_pid_waccess(pid, dwDesiredAccess); + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; @@ -2172,7 +2175,7 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; if (! GetProcessIoCounters(hProcess, &IoCounters)) { @@ -2202,7 +2205,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) { return NULL; } @@ -2227,8 +2230,7 @@ static PyObject * psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; - DWORD dwDesiredAccess = \ - PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; DWORD_PTR mask; #ifdef _WIN64 @@ -2239,7 +2241,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { { return NULL; } - hProcess = psutil_handle_from_pid_waccess(pid, dwDesiredAccess); + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; @@ -2877,7 +2879,7 @@ psutil_proc_num_handles(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; if (! GetProcessHandleCount(hProcess, &handleCount)) { @@ -3025,6 +3027,8 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { WCHAR mappedFileName[MAX_PATH]; SYSTEM_INFO system_info; LPVOID maxAddr; + // required by GetMappedFileNameW + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; PyObject *py_str = NULL; @@ -3033,7 +3037,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { return NULL; if (! PyArg_ParseTuple(args, "l", &pid)) goto error; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, access); if (NULL == hProcess) goto error; diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index ffd3c80ef4..628c01abd3 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -272,7 +272,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid) { * Return a process handle or NULL. */ HANDLE -psutil_handle_from_pid_waccess(DWORD pid, DWORD dwDesiredAccess) { +psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess) { HANDLE hProcess; if (pid == 0) { @@ -285,18 +285,6 @@ psutil_handle_from_pid_waccess(DWORD pid, DWORD dwDesiredAccess) { } -/* - * Same as psutil_handle_from_pid_waccess but implicitly uses - * PROCESS_QUERY_INFORMATION | PROCESS_VM_READ as dwDesiredAccess - * parameter for OpenProcess. - */ -HANDLE -psutil_handle_from_pid(DWORD pid) { - DWORD dwDesiredAccess = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; - return psutil_handle_from_pid_waccess(pid, dwDesiredAccess); -} - - DWORD * psutil_get_pids(DWORD *numberOfReturnedPIDs) { // Win32 SDK says the only way to know if our process array @@ -553,8 +541,9 @@ static int psutil_get_process_data(long pid, BOOL weAreWow64; BOOL theyAreWow64; #endif + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; - hProcess = psutil_handle_from_pid(pid); + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return -1; diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index a2f70c2b95..f85c1efdf6 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -17,8 +17,7 @@ DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); -HANDLE psutil_handle_from_pid(DWORD pid); -HANDLE psutil_handle_from_pid_waccess(DWORD pid, DWORD dwDesiredAccess); +HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); int psutil_pid_is_running(DWORD pid); int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); From f88ca356b7fe43efc04e2610cf0190d5aeb22188 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Dec 2018 17:46:14 +0100 Subject: [PATCH 0114/1714] update HISTORY --- HISTORY.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 7a26f50e3a..1b4e1c96c3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,9 @@ XXXX-XX-XX - 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. - 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by EccoTheFlintstone) +- 1376_: [Windows] OpenProcess() now uses PROCESS_QUERY_LIMITED_INFORMATION + access rights wherever possible, resulting in less AccessDenied exceptions + being thrown for system processes. 5.4.8 ===== From c9988242c7138802e48ecd09837e95a06096aae6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Dec 2018 09:18:23 -0800 Subject: [PATCH 0115/1714] fix #1370: improper usage of CloseHandle() may lead to override the original error code resulting in raising a wrong exception --- HISTORY.rst | 2 + psutil/_psutil_windows.c | 76 ++++++++++++++++++++---------- psutil/arch/windows/process_info.c | 2 +- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1b4e1c96c3..366485715d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,8 @@ XXXX-XX-XX - 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. - 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by EccoTheFlintstone) +- 1370_: [Windows] improper usage of CloseHandle() may lead to override the + original error code when raising an exception. - 1376_: [Windows] OpenProcess() now uses PROCESS_QUERY_LIMITED_INFORMATION access rights wherever possible, resulting in less AccessDenied exceptions being thrown for system processes. diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index f3979de626..cbd7ea8cb0 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -399,8 +399,8 @@ psutil_proc_kill(PyObject *self, PyObject *args) { err = GetLastError(); // See: https://github.com/giampaolo/psutil/issues/1099 if (err != ERROR_ACCESS_DENIED) { - CloseHandle(hProcess); PyErr_SetFromWindowsErr(err); + CloseHandle(hProcess); return NULL; } } @@ -445,21 +445,21 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // handle return code if (retVal == WAIT_FAILED) { - CloseHandle(hProcess); PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); return NULL; } if (retVal == WAIT_TIMEOUT) { - CloseHandle(hProcess); PyErr_SetString(TimeoutExpired, "WaitForSingleObject() returned WAIT_TIMEOUT"); + CloseHandle(hProcess); return NULL; } if (retVal == WAIT_ABANDONED) { psutil_debug("WaitForSingleObject() -> WAIT_ABANDONED"); - CloseHandle(hProcess); PyErr_SetString(TimeoutAbandoned, "WaitForSingleObject() returned WAIT_ABANDONED"); + CloseHandle(hProcess); return NULL; } @@ -467,9 +467,11 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // process is gone so we can get its process exit code. The PID // may still stick around though but we'll handle that from Python. if (GetExitCodeProcess(hProcess, &ExitCode) == 0) { + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(GetLastError()); + return NULL; } + CloseHandle(hProcess); #if PY_MAJOR_VERSION >= 3 @@ -497,15 +499,16 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { if (hProcess == NULL) return NULL; if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { - CloseHandle(hProcess); if (GetLastError() == ERROR_ACCESS_DENIED) { // usually means the process has died so we throw a NoSuchProcess // here - return NoSuchProcess(""); + NoSuchProcess(""); } else { - return PyErr_SetFromWindowsErr(0); + PyErr_SetFromWindowsErr(0); } + CloseHandle(hProcess); + return NULL; } CloseHandle(hProcess); @@ -551,15 +554,16 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { if (hProcess == NULL) return NULL; if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { - CloseHandle(hProcess); if (GetLastError() == ERROR_ACCESS_DENIED) { // usually means the process has died so we throw a // NoSuchProcess here - return NoSuchProcess(""); + NoSuchProcess(""); } else { - return PyErr_SetFromWindowsErr(0); + PyErr_SetFromWindowsErr(0); } + CloseHandle(hProcess); + return NULL; } CloseHandle(hProcess); @@ -761,8 +765,9 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (NULL == hProcess) return NULL; if (GetProcessImageFileNameW(hProcess, exe, MAX_PATH) == 0) { + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); + return NULL; } CloseHandle(hProcess); return PyUnicode_FromWideChar(exe, wcslen(exe)); @@ -790,8 +795,9 @@ psutil_proc_name(PyObject *self, PyObject *args) { pentry.dwSize = sizeof(PROCESSENTRY32W); ok = Process32FirstW(hSnapShot, &pentry); if (! ok) { + PyErr_SetFromWindowsErr(0); CloseHandle(hSnapShot); - return PyErr_SetFromWindowsErr(0); + return NULL; } while (ok) { if (pentry.th32ProcessID == pid) { @@ -831,8 +837,9 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { if (! GetProcessMemoryInfo(hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, sizeof(cnt))) { + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); + return NULL; } #if (_WIN32_WINNT >= 0x0501) // Windows XP with SP2 @@ -1357,10 +1364,15 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { processHandle = psutil_handle_from_pid(pid, access); if (processHandle == NULL) return NULL; + py_retlist = psutil_get_open_files(pid, processHandle); + if (py_retlist == NULL) { + PyErr_SetFromWindowsErr(0); + CloseHandle(processHandle); + return NULL; + } + CloseHandle(processHandle); - if (py_retlist == NULL) - return PyErr_SetFromWindowsErr(0); return py_retlist; } @@ -2058,13 +2070,18 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; + priority = GetPriorityClass(hProcess); + if (priority == 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } CloseHandle(hProcess); - if (priority == 0) - return PyErr_SetFromWindowsErr(0); return Py_BuildValue("i", priority); } @@ -2085,10 +2102,15 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; + retval = SetPriorityClass(hProcess, priority); + if (retval == 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + CloseHandle(hProcess); - if (retval == 0) - return PyErr_SetFromWindowsErr(0); Py_RETURN_NONE; } @@ -2178,10 +2200,13 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; + if (! GetProcessIoCounters(hProcess, &IoCounters)) { + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); + return NULL; } + CloseHandle(hProcess); return Py_BuildValue("(KKKKKK)", IoCounters.ReadOperationCount, @@ -2210,8 +2235,9 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { return NULL; } if (GetProcessAffinityMask(hProcess, &proc_mask, &system_mask) == 0) { + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); + return NULL; } CloseHandle(hProcess); @@ -2246,8 +2272,9 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { return NULL; if (SetProcessAffinityMask(hProcess, mask) == 0) { + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); + return NULL; } CloseHandle(hProcess); @@ -2883,8 +2910,9 @@ psutil_proc_num_handles(PyObject *self, PyObject *args) { if (NULL == hProcess) return NULL; if (! GetProcessHandleCount(hProcess, &handleCount)) { + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); + return NULL; } CloseHandle(hProcess); return Py_BuildValue("k", handleCount); diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 628c01abd3..d83b715010 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -422,7 +422,7 @@ psutil_pid_is_running(DWORD pid) { return 1; } else { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromWindowsErr(err); return -1; } } From 62410eb927d20003271fb0c5d66a21bf59f38628 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Dec 2018 18:37:27 +0100 Subject: [PATCH 0116/1714] enforce lack of support for Win XP --- psutil/_pswindows.py | 3 +-- setup.py | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index b938d42ff0..2bc9c9dd01 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -27,8 +27,7 @@ # 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" + msg += "2000, XP and 2003 server" raise RuntimeError(msg) else: raise diff --git a/setup.py b/setup.py index 28cde6f12c..e285323696 100755 --- a/setup.py +++ b/setup.py @@ -109,11 +109,10 @@ def get_winver(): return '0x0%s' % ((maj * 100) + min) if sys.getwindowsversion()[0] < 6: - msg = "warning: Windows versions < Vista are no longer supported or " - msg = "maintained; latest official supported version is psutil 3.4.2; " - msg += "psutil may still be installed from sources if you have " - msg += "Visual Studio and may also (kind of) work though" - warnings.warn(msg, UserWarning) + 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) macros.append(("PSUTIL_WINDOWS", 1)) macros.extend([ From 790292d3902a51800117e9343be918ec1ddecbde Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 10 Dec 2018 14:47:00 +0100 Subject: [PATCH 0117/1714] #1376 Windows: check if variable is NULL before free()ing it --- HISTORY.rst | 2 ++ psutil/arch/windows/process_info.c | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 366485715d..263a381f83 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,8 @@ XXXX-XX-XX - 1376_: [Windows] OpenProcess() now uses PROCESS_QUERY_LIMITED_INFORMATION access rights wherever possible, resulting in less AccessDenied exceptions being thrown for system processes. +- 1376_: [Windows] check if variable is NULL before free()ing it. (patch by + EccoTheFlintstone) 5.4.8 ===== diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index d83b715010..b79aeb3e18 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -302,7 +302,8 @@ psutil_get_pids(DWORD *numberOfReturnedPIDs) { do { procArraySz += 1024; - free(procArray); + if (procArray != NULL) + free(procArray); procArrayByteSz = procArraySz * sizeof(DWORD); procArray = malloc(procArrayByteSz); if (procArray == NULL) { @@ -833,7 +834,8 @@ psutil_get_cmdline(long pid) { out: LocalFree(szArglist); - free(data); + if (data != NULL) + free(data); Py_XDECREF(py_unicode); Py_XDECREF(py_retlist); @@ -852,7 +854,8 @@ PyObject *psutil_get_cwd(long pid) { ret = PyUnicode_FromWideChar(data, wcslen(data)); out: - free(data); + if (data != NULL) + free(data); return ret; } @@ -873,8 +876,8 @@ PyObject *psutil_get_environ(long pid) { ret = PyUnicode_FromWideChar(data, size / 2); out: - free(data); - + if (data != NULL) + free(data); return ret; } From 314ab75295cbcdc6d8a12f89a08d702795593eca Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 10 Dec 2018 15:04:17 +0100 Subject: [PATCH 0118/1714] fix #1357: do not expose Process' memory_maps() and io_counters() methods if not supported by the kernel --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 15 ++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 263a381f83..59bf9f678a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,8 @@ XXXX-XX-XX **Bug fixes** - 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. +- 1357_: [Linux] Process' memory_maps() and io_counters() method are no longer + exposed if not supported by the kernel. - 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by EccoTheFlintstone) - 1370_: [Windows] improper usage of CloseHandle() may lead to override the diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b775d39ae7..51cb200966 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1642,6 +1642,7 @@ def terminal(self): 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): @@ -1673,10 +1674,6 @@ def io_counters(self): except KeyError as err: raise ValueError("%r field was not found in %s; found fields " "are %r" % (err[0], fname, fields)) - else: - def io_counters(self): - raise NotImplementedError("couldn't find /proc/%s/io (kernel " - "too old?)" % self.pid) @wrap_exceptions def cpu_times(self): @@ -1767,6 +1764,9 @@ 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 + + /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 = {} @@ -1827,13 +1827,6 @@ def get_blocks(lines, current_block): )) 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: From d364283df3b7c42808704f3c7ef99536b1df784b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 10 Dec 2018 16:38:24 +0100 Subject: [PATCH 0119/1714] Linux: refactor _parse_stat_file() and return a dict instead of a list (+ maintainability) --- psutil/_pslinux.py | 47 +++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 51cb200966..880be2c82b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1538,11 +1538,11 @@ def __init__(self, pid): @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 substract 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. """ @@ -1553,8 +1553,21 @@ def _parse_stat_file(self): # the first occurrence of "(" and the last occurence of ")". rpar = data.rfind(b')') name = data[data.find(b'(') + 1:rpar] - others = data[rpar + 2:].split() - return [name] + others + 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] + + return ret @memoize_when_activated def _read_status_file(self): @@ -1583,7 +1596,7 @@ def oneshot_exit(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 @@ -1635,7 +1648,7 @@ 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] @@ -1678,16 +1691,16 @@ def io_counters(self): @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 + 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 return _common.pcputimes(utime, stime, children_utime, children_stime) @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): @@ -1695,14 +1708,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. # 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): @@ -2013,7 +2026,7 @@ def rlimit(self, resource, limits=None): @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) @@ -2082,7 +2095,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+)')): From 8351fa4ff642d997cd478a7d216b661e62a5f696 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Dec 2018 22:05:40 +0100 Subject: [PATCH 0120/1714] use PROCESS_QUERY_LIMITED_INFORMATION also for username() --- psutil/_psutil_windows.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index cbd7ea8cb0..ce44258a33 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1428,7 +1428,8 @@ psutil_proc_username(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - processHandle = psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION); + processHandle = psutil_handle_from_pid( + pid, PROCESS_QUERY_LIMITED_INFORMATION); if (processHandle == NULL) return NULL; From 2cdf81db322822ba8fb23ed67523aacb6539da95 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Dec 2018 15:54:37 +0100 Subject: [PATCH 0121/1714] #1373: different approach to oneshot() cache (pass Process instances around - which is faster) --- psutil/__init__.py | 16 ++++++++-------- psutil/_common.py | 25 +++++++++++++------------ psutil/_psaix.py | 14 +++++++------- psutil/_psbsd.py | 6 +++--- psutil/_pslinux.py | 14 +++++++------- psutil/_psosx.py | 10 +++++----- psutil/_pssunos.py | 14 +++++++------- psutil/_pswindows.py | 6 +++--- psutil/tests/test_misc.py | 4 ++-- psutil/tests/test_process.py | 15 +++++++++++++++ 10 files changed, 70 insertions(+), 54 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 5a5720d7ad..78ff985df8 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -471,23 +471,23 @@ def oneshot(self): self._oneshot_inctx = True try: # cached in case cpu_percent() is used - self.cpu_times.cache_activate() + self.cpu_times.cache_activate(self) # cached in case memory_percent() is used - self.memory_info.cache_activate() + self.memory_info.cache_activate(self) # cached in case parent() is used - self.ppid.cache_activate() + self.ppid.cache_activate(self) # cached in case username() is used if POSIX: - self.uids.cache_activate() + self.uids.cache_activate(self) # specific implementation cache self._proc.oneshot_enter() yield finally: - self.cpu_times.cache_deactivate() - self.memory_info.cache_deactivate() - self.ppid.cache_deactivate() + self.cpu_times.cache_deactivate(self) + self.memory_info.cache_deactivate(self) + self.ppid.cache_deactivate(self) if POSIX: - self.uids.cache_deactivate() + self.uids.cache_deactivate(self) self._proc.oneshot_exit() self._oneshot_inctx = False diff --git a/psutil/_common.py b/psutil/_common.py index bee9579276..f498bb9037 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -327,7 +327,7 @@ def memoize_when_activated(fun): 1 >>> >>> # activated - >>> foo.cache_activate() + >>> foo.cache_activate(self) >>> foo() 1 >>> foo() @@ -336,26 +336,27 @@ def memoize_when_activated(fun): """ @functools.wraps(fun) def wrapper(self): - if not wrapper.cache_activated: + if not hasattr(self, "_cache"): return fun(self) else: try: - ret = cache[fun] + ret = self._cache[fun] except KeyError: - ret = cache[fun] = fun(self) + ret = self._cache[fun] = fun(self) 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 diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 7ba212dbf5..9975545aa5 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -354,7 +354,7 @@ def wrapper(self, *args, **kwargs): class Process(object): """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,14 +363,14 @@ 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_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) @memoize_when_activated def _proc_name_and_args(self): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 1dc7231284..6683a20051 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -594,7 +594,7 @@ def wrap_exceptions_procfs(inst): class Process(object): """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["pid", "_name", "_ppid", "_cache"] def __init__(self, pid): self.pid = pid @@ -609,10 +609,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): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 880be2c82b..5c8cc20c4b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1528,7 +1528,7 @@ def wrapper(self, *args, **kwargs): class Process(object): """Linux process implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path"] + __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] def __init__(self, pid): self.pid = pid @@ -1585,14 +1585,14 @@ def _read_smaps_file(self): 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): diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 94e22bc729..015c5b411f 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -380,7 +380,7 @@ def catch_zombie(proc): class Process(object): """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["pid", "_name", "_ppid", "_cache"] def __init__(self, pid): self.pid = pid @@ -403,12 +403,12 @@ def _get_pidtaskinfo(self): 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): diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index e2f33a3ae1..730af39305 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -368,7 +368,7 @@ def wrapper(self, *args, **kwargs): class Process(object): """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 @@ -377,14 +377,14 @@ 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_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) @memoize_when_activated def _proc_name_and_args(self): diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 2bc9c9dd01..bb58824221 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -645,7 +645,7 @@ def wrapper(self, *args, **kwargs): class Process(object): """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["pid", "_name", "_ppid", "_cache"] def __init__(self, pid): self.pid = pid @@ -655,10 +655,10 @@ def __init__(self, pid): # --- oneshot() stuff def oneshot_enter(self): - self.oneshot_info.cache_activate() + self.oneshot_info.cache_activate(self) def oneshot_exit(self): - self.oneshot_info.cache_deactivate() + self.oneshot_info.cache_deactivate(self) @memoize_when_activated def oneshot_info(self): diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 3056abc0a7..93132b556d 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -261,14 +261,14 @@ def foo(self): # activate calls = [] - f.foo.cache_activate() + f.foo.cache_activate(f) f.foo() f.foo() self.assertEqual(len(calls), 1) # deactivate calls = [] - f.foo.cache_deactivate() + f.foo.cache_deactivate(f) f.foo() f.foo() self.assertEqual(len(calls), 2) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 2126e32adc..cd72be8504 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1195,6 +1195,21 @@ def test_oneshot_twice(self): 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. From 495bb454d12f72f00ea86a5cad196425e3ae2a78 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Dec 2018 16:27:44 +0100 Subject: [PATCH 0122/1714] pdate HISTORY --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 59bf9f678a..50c839068f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,8 @@ XXXX-XX-XX EccoTheFlintstone) - 1370_: [Windows] improper usage of CloseHandle() may lead to override the original error code when raising an exception. +- 1373_: incorrect handling of cache in Process.oneshot() context causes + Process instances to return incorrect results. - 1376_: [Windows] OpenProcess() now uses PROCESS_QUERY_LIMITED_INFORMATION access rights wherever possible, resulting in less AccessDenied exceptions being thrown for system processes. From b3b5d4293c60bc4390a5a5a39491e985626c9139 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Dec 2018 16:48:57 +0100 Subject: [PATCH 0123/1714] fix #1111: use a lock to make Process.oneshot() thread safe --- HISTORY.rst | 1 + psutil/__init__.py | 74 +++++++++++++++++++++++++--------------------- psutil/_common.py | 17 ++++++----- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 50c839068f..d704e39d0a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,7 @@ XXXX-XX-XX **Bug fixes** +- 1111_: Process.oneshot() is now thread safe. - 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. - 1357_: [Linux] Process' memory_maps() and io_counters() method are no longer exposed if not supported by the kernel. diff --git a/psutil/__init__.py b/psutil/__init__.py index 78ff985df8..a0258b209d 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -31,6 +31,7 @@ import signal import subprocess import sys +import threading import time try: import pwd @@ -352,7 +353,7 @@ def _init(self, pid, _ignore_nsp=False): self._create_time = None self._gone = 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 @@ -456,40 +457,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(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() + 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) - self.memory_info.cache_deactivate(self) - self.ppid.cache_deactivate(self) - if POSIX: - self.uids.cache_deactivate(self) - 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 diff --git a/psutil/_common.py b/psutil/_common.py index f498bb9037..b809a79f60 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -336,14 +336,17 @@ def memoize_when_activated(fun): """ @functools.wraps(fun) def wrapper(self): - if not hasattr(self, "_cache"): + try: + # case 1: we previously entered oneshot() ctx + ret = self._cache[fun] + except AttributeError: + # case 2: we never entered oneshot() ctx return fun(self) - else: - try: - ret = self._cache[fun] - except KeyError: - ret = self._cache[fun] = fun(self) - return ret + except KeyError: + # case 3: we entered oneshot() ctx but there's no cache + # for this entry yet + ret = self._cache[fun] = fun(self) + return ret def cache_activate(proc): """Activate cache. Expects a Process instance. Cache will be From 4ae4f944c1311f471d8506a92eab8ac52401d623 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 25 Dec 2018 13:02:20 +0100 Subject: [PATCH 0124/1714] update readme --- IDEAS | 4 ++++ README.rst | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/IDEAS b/IDEAS index babc0e8be1..046044a307 100644 --- a/IDEAS +++ b/IDEAS @@ -19,6 +19,8 @@ PLATFORMS FEATURES ======== +- set process name/title + - #1115: users() idle time. - #1102: Process.is64bit(). @@ -151,6 +153,8 @@ FEATURES - #550: number of threads per core. +- cpu_percent() and cpu_times_percent() use global vars so are not thread safe. + BUGFIXES ======== diff --git a/README.rst b/README.rst index 2fa59be653..01fdf00bb3 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -.. image:: http://pepy.tech/badge/psutil - :target: http://pepy.tech/project/psutil +.. image:: https://pepy.tech/badge/psutil/month + :target: https://pepy.tech/project/psutil :alt: Downloads .. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20macOS From 1b2148b7cbc0f7961a2f1f871371c754348f34db Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 23 Jan 2019 19:06:04 +0100 Subject: [PATCH 0125/1714] fix win num_handles() test --- psutil/tests/test_windows.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index ffa763d099..4633f7596c 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -518,15 +518,15 @@ def test_num_handles(self): import ctypes.wintypes PROCESS_QUERY_INFORMATION = 0x400 handle = ctypes.windll.kernel32.OpenProcess( - PROCESS_QUERY_INFORMATION, 0, os.getpid()) + 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().num_handles() - ctypes.windll.kernel32.CloseHandle(handle) - self.assertEqual(psutil_value, sys_value + 1) + psutil_value = psutil.Process(self.pid).num_handles() + self.assertEqual(psutil_value, sys_value) @unittest.skipIf(not WINDOWS, "WINDOWS only") From c6b3e929deb182d4db6007548571ad8ddb32bd87 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 23 Jan 2019 19:23:18 +0100 Subject: [PATCH 0126/1714] pre-release --- HISTORY.rst | 2 +- docs/index.rst | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index d704e39d0a..44afa960ff 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.5.0 ===== -XXXX-XX-XX +2019-0-23 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 85506a2802..643bb571f2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2626,6 +2626,10 @@ take a look at the Timeline ======== +- 2019-01-23: + `5.5.0 `__ - + `what's new `__ - + `diff `__ - 2018-10-30: `5.4.8 `__ - `what's new `__ - From 6f4a6228998df48ee09413377785d13d2eec7998 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 25 Jan 2019 16:36:17 +0100 Subject: [PATCH 0127/1714] #1394 / windows / process exe(): convert errno 0 into ERROR_ACCESS_DENIED; errno 0 occurs when the Python process runs in 'Virtual Secure Mode' --- HISTORY.rst | 10 ++++++++++ psutil/_psutil_linux.c | 5 ++++- psutil/_psutil_windows.c | 6 +++++- psutil/_pswindows.py | 4 +++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 44afa960ff..095c906234 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.5.1 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 1394_: [Windows] Process.exe() returns "[Error 0] The operation completed + successfully" when Python process runs in "Virtual Secure Mode". + 5.5.0 ===== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index bd27b5f9c6..5b7a56ad96 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -211,6 +211,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { file = setmntent(mtab_path, "r"); Py_END_ALLOW_THREADS if ((file == 0) || (file == NULL)) { + psutil_debug("setmntent() failed"); PyErr_SetFromErrnoWithFilename(PyExc_OSError, mtab_path); goto error; } @@ -298,8 +299,10 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { while (1) { setsize = CPU_ALLOC_SIZE(ncpus); mask = CPU_ALLOC(ncpus); - if (mask == NULL) + if (mask == NULL) { + psutil_debug("CPU_ALLOC() failed"); return PyErr_NoMemory(); + } if (sched_getaffinity(pid, setsize, mask) == 0) break; CPU_FREE(mask); diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index ce44258a33..4251e0c71a 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -765,7 +765,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (NULL == hProcess) return NULL; if (GetProcessImageFileNameW(hProcess, exe, MAX_PATH) == 0) { - PyErr_SetFromWindowsErr(0); + // https://github.com/giampaolo/psutil/issues/1394 + if (GetLastError() == 0) + PyErr_SetFromWindowsErr(ERROR_ACCESS_DENIED); + else + PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); return NULL; } diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index bb58824221..664d5b6b11 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -698,7 +698,9 @@ def exe(self): # 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))) + exe = cext.proc_exe(self.pid) + exe = convert_dos_path(exe) + return py2_strencode(exe) @wrap_exceptions def cmdline(self): From af7e4b5fcac3e543664058adfe068ab881048832 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Feb 2019 16:06:36 +0100 Subject: [PATCH 0128/1714] update doc --- README.rst | 3 +-- docs/index.rst | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 01fdf00bb3..00713f4809 100644 --- a/README.rst +++ b/README.rst @@ -65,8 +65,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures, with Python versions **2.6, -2.7, and 3.4+**. `PyPy `__ is also known to work. +...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and 3.4+**. `PyPy `__ is also known to work. ==================== Example applications diff --git a/docs/index.rst b/docs/index.rst index 643bb571f2..7cfa9b8207 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,10 +37,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures, with Python -versions from **2.6 to 3.6** (users of Python 2.4 and 2.5 may use -`2.1.3 `__ version). -`PyPy `__ is also known to work. +...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and 3.4+**. `PyPy `__ is also known to work. The psutil documentation you're reading is distributed as a single HTML page. From a45365980d9ae9237caa61dc71d68ffd60c44acf Mon Sep 17 00:00:00 2001 From: EccoTheFlintstone <32797240+EccoTheFlintstone@users.noreply.github.com> Date: Sun, 3 Feb 2019 07:56:45 -0500 Subject: [PATCH 0129/1714] Starting from windows 8.1, get commandline content using NtQueryInformationProcess (see #1384) (#1398) #1384, #1398: on windows 8.1, get cmdline() using NtQueryInformationProcess in case the original method fails with ACCESS_DENIED --- psutil/arch/windows/process_info.c | 192 ++++++++++++++++++++++++++--- psutil/arch/windows/security.c | 16 +-- 2 files changed, 183 insertions(+), 25 deletions(-) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index b79aeb3e18..64888d60c1 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -162,6 +162,73 @@ const int STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; const int STATUS_BUFFER_TOO_SMALL = 0xC0000023L; + +#define WINDOWS_UNINITIALIZED 0 +#define WINDOWS_XP 51 +#define WINDOWS_VISTA 60 +#define WINDOWS_7 61 +#define WINDOWS_8 62 +#define WINDOWS_81 63 +#define WINDOWS_10 100 + + +int get_windows_version() { + OSVERSIONINFO ver_info; + BOOL result; + DWORD dwMajorVersion; + DWORD dwMinorVersion; + DWORD dwBuildNumber; + static int windows_version = WINDOWS_UNINITIALIZED; + // windows_version is static + // and equal to WINDOWS_UNINITIALIZED only on first call + if (windows_version == WINDOWS_UNINITIALIZED) { + memset(&ver_info, 0, sizeof(ver_info)); + ver_info.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); + result = GetVersionEx(&ver_info); + if (result != FALSE) { + dwMajorVersion = ver_info.dwMajorVersion; + dwMinorVersion = ver_info.dwMinorVersion; + dwBuildNumber = ver_info.dwBuildNumber; + // Windows XP, Windows server 2003 + if (dwMajorVersion == 5 && dwMinorVersion == 1) { + windows_version = WINDOWS_XP; + } + // Windows Vista + else if (dwMajorVersion == 6 && dwMinorVersion == 0) { + windows_version = WINDOWS_VISTA; + } + // Windows 7, Windows Server 2008 R2 + else if (dwMajorVersion == 6 && dwMinorVersion == 1) { + windows_version = WINDOWS_7; + } + // Windows 8, Windows Server 2012 + else if (dwMajorVersion == 6 && dwMinorVersion == 2) { + windows_version = WINDOWS_8; + } + // Windows 8.1, Windows Server 2012 R2 + else if (dwMajorVersion == 6 && dwMinorVersion == 3) + { + windows_version = WINDOWS_81; + } + // Windows 10, Windows Server 2016 + else if (dwMajorVersion == 10) { + windows_version = WINDOWS_10; + } + } + } + return windows_version; +} + +_NtQueryInformationProcess psutil_NtQueryInformationProcess() { + static _NtQueryInformationProcess NtQueryInformationProcess = NULL; + if (NtQueryInformationProcess == NULL) { + NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress( + GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); + } + return NtQueryInformationProcess; +} + + // ==================================================================== // Process and PIDs utiilties. // ==================================================================== @@ -526,7 +593,7 @@ static int psutil_get_process_data(long pid, http://stackoverflow.com/a/14012919 http://www.drdobbs.com/embracing-64-bit-windows/184401966 */ - static _NtQueryInformationProcess NtQueryInformationProcess = NULL; + _NtQueryInformationProcess NtQueryInformationProcess = NULL; #ifndef _WIN64 static _NtQueryInformationProcess NtWow64QueryInformationProcess64 = NULL; static _NtWow64ReadVirtualMemory64 NtWow64ReadVirtualMemory64 = NULL; @@ -548,10 +615,7 @@ static int psutil_get_process_data(long pid, if (hProcess == NULL) return -1; - if (NtQueryInformationProcess == NULL) { - NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); - } + NtQueryInformationProcess = psutil_NtQueryInformationProcess(); #ifdef _WIN64 /* 64 bit case. Check if the target is a 32 bit process running in WoW64 @@ -791,10 +855,73 @@ static int psutil_get_process_data(long pid, return -1; } +int psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { + HANDLE hProcess; + ULONG ret_length = 4096; + NTSTATUS status; + char * cmdline_buffer = NULL; + WCHAR * cmdline_buffer_wchar = NULL; + PUNICODE_STRING tmp = NULL; + DWORD string_size; + _NtQueryInformationProcess NtQueryInformationProcess = NULL; + int ret = -1; + + NtQueryInformationProcess = psutil_NtQueryInformationProcess(); + if (NtQueryInformationProcess == NULL) { + PyErr_SetFromWindowsErr(0); + goto error; + } + + cmdline_buffer = calloc(ret_length, 1); + if (cmdline_buffer == NULL) { + PyErr_NoMemory(); + goto error; + } + + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) { + PyErr_SetFromWindowsErr(0); + goto error; + } + status = NtQueryInformationProcess( + hProcess, + 60, // ProcessCommandLineInformation + cmdline_buffer, + ret_length, + &ret_length + ); + if (!NT_SUCCESS(status)) { + PyErr_SetFromWindowsErr(0); + goto error; + } + + tmp = (PUNICODE_STRING)cmdline_buffer; + string_size = wcslen(tmp->Buffer) + 1; + cmdline_buffer_wchar = (WCHAR *)calloc(string_size, sizeof(WCHAR)); + + if (cmdline_buffer_wchar == NULL) { + PyErr_NoMemory(); + goto error; + } + + wcscpy_s(cmdline_buffer_wchar, string_size, tmp->Buffer); + *pdata = cmdline_buffer_wchar; + *psize = string_size * sizeof(WCHAR); + ret = 0; + +error: + if (cmdline_buffer != NULL) + free(cmdline_buffer); + if (hProcess != NULL) + CloseHandle(hProcess); + + return ret; +} + /* - * returns a Python list representing the arguments for the process - * with given pid or NULL on error. - */ +* returns a Python list representing the arguments for the process +* with given pid or NULL on error. +*/ PyObject * psutil_get_cmdline(long pid) { PyObject *ret = NULL; @@ -804,10 +931,44 @@ psutil_get_cmdline(long pid) { PyObject *py_unicode = NULL; LPWSTR *szArglist = NULL; int nArgs, i; - - if (psutil_get_process_data(pid, KIND_CMDLINE, &data, &size) != 0) - goto out; - + int windows_version; + int func_ret; + + + windows_version = get_windows_version(); + + /* + by defaut, still use PEB (if command line params have been patched in + the PEB, we will get the actual ones) + Reading the PEB to get the command line parameters still seem to be + the best method if somebody has tampered with the parameters after + creating the process. + For instance, create a process as suspended, patch the command line + in its PEB and unfreeze it. + The process will use the "new" parameters whereas the system + (with NtQueryInformationProcess) will give you the "old" ones + (see here : https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/) + */ + func_ret = psutil_get_process_data(pid, KIND_CMDLINE, &data, &size); + if (func_ret != 0) { + if ((GetLastError() == ERROR_ACCESS_DENIED) && + (windows_version >= WINDOWS_81)) + { + // reset that we had an error + // and retry with NtQueryInformationProcess + // (for protected processes) + PyErr_Clear(); + + func_ret = psutil_get_cmdline_data(pid, &data, &size); + if (func_ret != 0) { + goto out; + } + } + else { + goto out; + } + } + // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(data, &nArgs); if (szArglist == NULL) { @@ -822,18 +983,18 @@ psutil_get_cmdline(long pid) { goto out; for (i = 0; i < nArgs; i++) { py_unicode = PyUnicode_FromWideChar(szArglist[i], - wcslen(szArglist[i])); + wcslen(szArglist[i])); if (py_unicode == NULL) goto out; PyList_SET_ITEM(py_retlist, i, py_unicode); py_unicode = NULL; } - ret = py_retlist; py_retlist = NULL; out: - LocalFree(szArglist); + if (szArglist != NULL) + LocalFree(szArglist); if (data != NULL) free(data); Py_XDECREF(py_unicode); @@ -960,3 +1121,4 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, free(buffer); return 0; } + diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index 331d96223c..d5f8f8d3da 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -34,7 +34,7 @@ psutil_token_from_handle(HANDLE hProcess) { * constant, we pass through the TOKEN_PRIVILEGES constant. This value returns * an array of privileges that the account has in the environment. Iterating * through the array, we call the function LookupPrivilegeName looking for the - * string SeTcbPrivilege. If the function returns this string, then this + * string “SeTcbPrivilege. If the function returns this string, then this * account has Local System privileges */ int @@ -131,7 +131,6 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { ); if (GetLastError() != ERROR_SUCCESS) return FALSE; - // second pass. set privilege based on previous setting tpPrevious.PrivilegeCount = 1; tpPrevious.Privileges[0].Luid = luid; @@ -160,9 +159,8 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { int psutil_set_se_debug() { HANDLE hToken; - if (! OpenThreadToken(GetCurrentThread(), + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, &hToken) ) { if (GetLastError() == ERROR_NO_TOKEN) { @@ -170,9 +168,8 @@ psutil_set_se_debug() { CloseHandle(hToken); return 0; } - if (!OpenThreadToken(GetCurrentThread(), + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, &hToken) ) { RevertToSelf(); @@ -198,17 +195,15 @@ psutil_set_se_debug() { int psutil_unset_se_debug() { HANDLE hToken; - if (! OpenThreadToken(GetCurrentThread(), + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, &hToken) ) { if (GetLastError() == ERROR_NO_TOKEN) { if (! ImpersonateSelf(SecurityImpersonation)) return 0; - if (!OpenThreadToken(GetCurrentThread(), + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, &hToken)) { return 0; @@ -223,3 +218,4 @@ psutil_unset_se_debug() { CloseHandle(hToken); return 1; } + From 3d65245b72616c956619d6615607ce21561b10d2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 Feb 2019 06:10:50 -0800 Subject: [PATCH 0130/1714] #1348: give CREDITS to @EccoTheFlinstones + some minor style changes --- CREDITS | 2 +- HISTORY.rst | 6 +++ psutil/arch/windows/process_info.c | 62 +++++++++++++++--------------- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/CREDITS b/CREDITS index f238f85ba0..cb4b793b7f 100644 --- a/CREDITS +++ b/CREDITS @@ -570,4 +570,4 @@ I: 1360 N: EccoTheFlintstone W: https://github.com/EccoTheFlintstone -I: 1368 +I: 1368, 1348 diff --git a/HISTORY.rst b/HISTORY.rst index 095c906234..ed5b8bc8b4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,12 @@ XXXX-XX-XX +**Enhancements** + +- 1348_: [Windows] on Windows >= 8.1 if Process.cmdline() fails due to + ERROR_ACCESS_DENIED attempt using NtQueryInformationProcess + + ProcessCommandLineInformation. (patch by EccoTheFlintstone) + **Bug fixes** - 1394_: [Windows] Process.exe() returns "[Error 0] The operation completed diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 64888d60c1..f897bde509 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -161,8 +161,6 @@ typedef struct { const int STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; const int STATUS_BUFFER_TOO_SMALL = 0xC0000023L; - - #define WINDOWS_UNINITIALIZED 0 #define WINDOWS_XP 51 #define WINDOWS_VISTA 60 @@ -219,6 +217,7 @@ int get_windows_version() { return windows_version; } + _NtQueryInformationProcess psutil_NtQueryInformationProcess() { static _NtQueryInformationProcess NtQueryInformationProcess = NULL; if (NtQueryInformationProcess == NULL) { @@ -499,9 +498,8 @@ psutil_pid_is_running(DWORD pid) { /* Given a pointer into a process's memory, figure out how much data can be * read from it. */ -static int psutil_get_process_region_size(HANDLE hProcess, - LPCVOID src, - SIZE_T *psize) { +static int +psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { MEMORY_BASIC_INFORMATION info; if (!VirtualQueryEx(hProcess, src, &info, sizeof(info))) { @@ -517,9 +515,10 @@ static int psutil_get_process_region_size(HANDLE hProcess, #ifndef _WIN64 /* Given a pointer into a process's memory, figure out how much data can be * read from it. */ -static int psutil_get_process_region_size64(HANDLE hProcess, - const PVOID64 src64, - PULONG64 psize) { +static int +psutil_get_process_region_size64(HANDLE hProcess, + const PVOID64 src64, + PULONG64 psize) { static _NtWow64QueryVirtualMemory64 NtWow64QueryVirtualMemory64 = NULL; MEMORY_BASIC_INFORMATION64 info64; @@ -565,10 +564,11 @@ enum psutil_process_data_kind { On success 0 is returned. On error the output parameter is not touched, -1 is returned, and an appropriate Python exception is set. */ -static int psutil_get_process_data(long pid, - enum psutil_process_data_kind kind, - WCHAR **pdata, - SIZE_T *psize) { +static int +psutil_get_process_data(long pid, + enum psutil_process_data_kind kind, + WCHAR **pdata, + SIZE_T *psize) { /* This function is quite complex because there are several cases to be considered: @@ -855,7 +855,9 @@ static int psutil_get_process_data(long pid, return -1; } -int psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { + +static int +psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { HANDLE hProcess; ULONG ret_length = 4096; NTSTATUS status; @@ -864,7 +866,6 @@ int psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { PUNICODE_STRING tmp = NULL; DWORD string_size; _NtQueryInformationProcess NtQueryInformationProcess = NULL; - int ret = -1; NtQueryInformationProcess = psutil_NtQueryInformationProcess(); if (NtQueryInformationProcess == NULL) { @@ -907,17 +908,17 @@ int psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { wcscpy_s(cmdline_buffer_wchar, string_size, tmp->Buffer); *pdata = cmdline_buffer_wchar; *psize = string_size * sizeof(WCHAR); - ret = 0; + return 0; error: if (cmdline_buffer != NULL) free(cmdline_buffer); if (hProcess != NULL) CloseHandle(hProcess); - - return ret; + return -1; } + /* * returns a Python list representing the arguments for the process * with given pid or NULL on error. @@ -934,20 +935,19 @@ psutil_get_cmdline(long pid) { int windows_version; int func_ret; - windows_version = get_windows_version(); - /* - by defaut, still use PEB (if command line params have been patched in - the PEB, we will get the actual ones) - Reading the PEB to get the command line parameters still seem to be - the best method if somebody has tampered with the parameters after - creating the process. + By defaut, still use PEB (if command line params have been patched in + the PEB, we will get the actual ones). Reading the PEB to get the + command line parameters still seem to be the best method if somebody + has tampered with the parameters after creating the process. For instance, create a process as suspended, patch the command line in its PEB and unfreeze it. The process will use the "new" parameters whereas the system (with NtQueryInformationProcess) will give you the "old" ones - (see here : https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/) + See: + - https://github.com/giampaolo/psutil/pull/1398 + - https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/ */ func_ret = psutil_get_process_data(pid, KIND_CMDLINE, &data, &size); if (func_ret != 0) { @@ -968,7 +968,7 @@ psutil_get_cmdline(long pid) { goto out; } } - + // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(data, &nArgs); if (szArglist == NULL) { @@ -999,11 +999,12 @@ psutil_get_cmdline(long pid) { free(data); Py_XDECREF(py_unicode); Py_XDECREF(py_retlist); - return ret; } -PyObject *psutil_get_cwd(long pid) { + +PyObject * +psutil_get_cwd(long pid) { PyObject *ret = NULL; WCHAR *data = NULL; SIZE_T size; @@ -1021,11 +1022,13 @@ PyObject *psutil_get_cwd(long pid) { return ret; } + /* * returns a Python string containing the environment variable data for the * process with given pid or NULL on error. */ -PyObject *psutil_get_environ(long pid) { +PyObject * +psutil_get_environ(long pid) { PyObject *ret = NULL; WCHAR *data = NULL; SIZE_T size; @@ -1121,4 +1124,3 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, free(buffer); return 0; } - From 584914306e50aec251476014ac3934810039e7b5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 4 Feb 2019 16:04:02 +0100 Subject: [PATCH 0131/1714] fix #1402: move psutil exceptions back into __init__.py --- HISTORY.rst | 3 +- psutil/__init__.py | 115 +++++++++++++++++++++++++++++++++++++++--- psutil/_exceptions.py | 94 ---------------------------------- psutil/_psaix.py | 10 ++-- psutil/_psbsd.py | 10 ++-- psutil/_pslinux.py | 10 ++-- psutil/_psosx.py | 10 ++-- psutil/_psposix.py | 6 ++- psutil/_pssunos.py | 10 ++-- psutil/_pswindows.py | 10 ++-- 10 files changed, 156 insertions(+), 122 deletions(-) delete mode 100644 psutil/_exceptions.py diff --git a/HISTORY.rst b/HISTORY.rst index ed5b8bc8b4..73609c7686 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,11 +15,12 @@ XXXX-XX-XX - 1394_: [Windows] Process.exe() returns "[Error 0] The operation completed successfully" when Python process runs in "Virtual Secure Mode". +- 1402_: psutil exceptions' repr() show the internal private module path. 5.5.0 ===== -2019-0-23 +2019-01-23 **Enhancements** diff --git a/psutil/__init__.py b/psutil/__init__.py index a0258b209d..3259dd0ec5 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -17,7 +17,7 @@ - Sun Solaris - AIX -Works with Python versions from 2.6 to 3.X. +Works with Python versions from 2.6 to 3.4+. """ from __future__ import division @@ -87,12 +87,6 @@ from ._common import SUNOS from ._common import WINDOWS -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 # via sys.modules. @@ -229,7 +223,6 @@ _TOTAL_PHYMEM = None _timer = getattr(time, 'monotonic', time.time) - # Sanity check in case the user messed up with psutil installation # or did something weird with sys.path. In this case we might end # up importing a python module using a C extension module which @@ -252,6 +245,112 @@ raise ImportError(msg) +# ===================================================================== +# --- Exceptions +# ===================================================================== + + +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 + + +# Push exception classes into platform specific module namespace. +_psplatform.NoSuchProcess = NoSuchProcess +_psplatform.ZombieProcess = ZombieProcess +_psplatform.AccessDenied = AccessDenied +_psplatform.TimeoutExpired = TimeoutExpired +if POSIX: + from . import _psposix + _psposix.TimeoutExpired = TimeoutExpired + + # ===================================================================== # --- Utils # ===================================================================== diff --git a/psutil/_exceptions.py b/psutil/_exceptions.py deleted file mode 100644 index 6dbbd28269..0000000000 --- a/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/psutil/_psaix.py b/psutil/_psaix.py index 9975545aa5..58ecf17fc0 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -28,9 +28,6 @@ from ._common import socktype_to_enum from ._common import usage_percent from ._compat import PY3 -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess __extra__all__ = ["PROCFS_PATH"] @@ -79,6 +76,13 @@ status=6, ttynr=7) +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + # ===================================================================== # --- named tuples diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 6683a20051..0581de2997 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -28,9 +28,6 @@ from ._common import socktype_to_enum from ._common import usage_percent from ._compat import which -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess __extra__all__ = [] @@ -137,6 +134,13 @@ name=24, ) +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + # ===================================================================== # --- named tuples diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 5c8cc20c4b..011b354168 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -43,9 +43,6 @@ 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): import enum @@ -161,6 +158,13 @@ class IOPriority(enum.IntEnum): "0B": _common.CONN_CLOSING } +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + # ===================================================================== # --- named tuples diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 015c5b411f..72b343f5bc 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -23,9 +23,6 @@ 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 __extra__all__ = [] @@ -87,6 +84,13 @@ volctxsw=7, ) +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + # ===================================================================== # --- named tuples diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 9c3fac27ef..d362143f69 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -15,12 +15,16 @@ from ._common import usage_percent from ._compat import PY3 from ._compat import unicode -from ._exceptions import TimeoutExpired __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] +# This object gets set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +TimeoutExpired = None + + def pid_exists(pid): """Check whether pid exists in the current process table.""" if pid == 0: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 730af39305..faadecfe65 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -24,9 +24,6 @@ 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 __extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] @@ -85,6 +82,13 @@ gid=10, egid=11) +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + # ===================================================================== # --- named tuples diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 664d5b6b11..1aeb46ef0f 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -46,9 +46,6 @@ from ._compat import PY3 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 @@ -143,6 +140,13 @@ class Priority(enum.IntEnum): mem_private=21, ) +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + # ===================================================================== # --- named tuples From 3f9643f2357e887bff3ab4ecf093b9f28fa53fe9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 4 Feb 2019 16:07:51 +0100 Subject: [PATCH 0132/1714] win / cmdline: add free() and CloseHandle() calls --- psutil/arch/windows/process_info.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index f897bde509..051b1c6db2 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -856,6 +856,11 @@ psutil_get_process_data(long pid, } +/* + * Get process cmdline() by using NtQueryInformationProcess. This is + * useful on Windows 8.1+ in order to get less ERROR_ACCESS_DENIED + * errors when querying privileged PIDs. + */ static int psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { HANDLE hProcess; @@ -908,6 +913,8 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { wcscpy_s(cmdline_buffer_wchar, string_size, tmp->Buffer); *pdata = cmdline_buffer_wchar; *psize = string_size * sizeof(WCHAR); + free(cmdline_buffer); + CloseHandle(hProcess); return 0; error: @@ -920,9 +927,9 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { /* -* returns a Python list representing the arguments for the process -* with given pid or NULL on error. -*/ + * Return a Python list representing the arguments for the process + * with given pid or NULL on error. + */ PyObject * psutil_get_cmdline(long pid) { PyObject *ret = NULL; From 2fad87fbc62fb360c93e85e5e7b8475f74a419f8 Mon Sep 17 00:00:00 2001 From: wiggin15 Date: Thu, 14 Feb 2019 22:50:06 +0200 Subject: [PATCH 0133/1714] Fix #1408: add missing header that defines m_len on AIX 64-bit mode (#1409) --- psutil/arch/aix/net_kernel_structs.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/psutil/arch/aix/net_kernel_structs.h b/psutil/arch/aix/net_kernel_structs.h index 09f320ff57..4e7a088c14 100644 --- a/psutil/arch/aix/net_kernel_structs.h +++ b/psutil/arch/aix/net_kernel_structs.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,7 @@ #define tcpcb64 tcpcb #define unpcb64 unpcb #define mbuf64 mbuf -#else +#else /* __64BIT__ */ struct file64 { int f_flag; int f_count; @@ -107,4 +108,4 @@ struct mbuf64 #define m_len m_hdr.mh_len -#endif \ No newline at end of file +#endif /* __64BIT__ */ \ No newline at end of file From 202657b658877afbfae244447f983cd63b0c938c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 21:51:58 +0100 Subject: [PATCH 0134/1714] give CREDITS to @wiggin15 for #1408 --- CREDITS | 2 +- HISTORY.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index cb4b793b7f..c2712ecfbe 100644 --- a/CREDITS +++ b/CREDITS @@ -57,7 +57,7 @@ W: http://www.jayloden.com N: Arnon Yaari (wiggin15) W: https://github.com/wiggin15 -I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214 +I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408 N: Jeff Tang W: https://github.com/mrjefftang diff --git a/HISTORY.rst b/HISTORY.rst index 73609c7686..770182ed16 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,8 @@ XXXX-XX-XX - 1394_: [Windows] Process.exe() returns "[Error 0] The operation completed successfully" when Python process runs in "Virtual Secure Mode". - 1402_: psutil exceptions' repr() show the internal private module path. +- 1408_: [AIX] psutil won't compile on AIX 7.1 due to missing header. (patch + by Arnon Yaari) 5.5.0 ===== From f1374c36d07abd3aaa15d6f512143189c1748099 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Feb 2019 10:30:08 -0800 Subject: [PATCH 0135/1714] #1398 #1348 / win / cmdline: refactor code so that the 2 cmdline() implementations can be called separately (and tested separately) --- psutil/_psutil_windows.c | 15 ++++++++++----- psutil/_pswindows.py | 8 +++++++- psutil/arch/windows/process_info.c | 27 ++++++++------------------- psutil/tests/test_memory_leaks.py | 10 ++++++++++ psutil/tests/test_windows.py | 16 ++++++++++++++++ 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 4251e0c71a..eb35c5f7d0 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -708,22 +708,28 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { * Return process cmdline as a Python list of cmdline arguments. */ static PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args) { +psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { long pid; int pid_return; + int use_peb; + PyObject *py_usepeb = Py_True; + static char *keywords[] = {"pid", "use_peb", NULL}; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (!PyArg_ParseTupleAndKeywords(args, kwdict, "i|O", + keywords, &pid, &py_usepeb)) { return NULL; + } if ((pid == 0) || (pid == 4)) return Py_BuildValue("[]"); + use_peb = (py_usepeb == Py_True); pid_return = psutil_pid_is_running(pid); if (pid_return == 0) return NoSuchProcess(""); if (pid_return == -1) return NULL; - return psutil_get_cmdline(pid); + return psutil_get_cmdline(pid, use_peb); } @@ -3688,8 +3694,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { static PyMethodDef PsutilMethods[] = { // --- per-process functions - - {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS, + {"proc_cmdline", (PyCFunction)(void(*)(void))psutil_proc_cmdline, METH_VARARGS | METH_KEYWORDS, "Return process cmdline as a list of cmdline arguments"}, {"proc_environ", psutil_proc_environ, METH_VARARGS, "Return process environment data"}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 1aeb46ef0f..e66febe0f2 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -708,7 +708,13 @@ def exe(self): @wrap_exceptions def cmdline(self): - ret = cext.proc_cmdline(self.pid) + try: + ret = cext.proc_cmdline(self.pid, use_peb=True) + except OSError as err: + if err.errno in ACCESS_DENIED_ERRSET: + ret = cext.proc_cmdline(self.pid, use_peb=False) + else: + raise if PY3: return ret else: diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 051b1c6db2..6f877342d7 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -931,7 +931,7 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { * with given pid or NULL on error. */ PyObject * -psutil_get_cmdline(long pid) { +psutil_get_cmdline(long pid, int use_peb) { PyObject *ret = NULL; WCHAR *data = NULL; SIZE_T size; @@ -956,25 +956,14 @@ psutil_get_cmdline(long pid) { - https://github.com/giampaolo/psutil/pull/1398 - https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/ */ - func_ret = psutil_get_process_data(pid, KIND_CMDLINE, &data, &size); - if (func_ret != 0) { - if ((GetLastError() == ERROR_ACCESS_DENIED) && - (windows_version >= WINDOWS_81)) - { - // reset that we had an error - // and retry with NtQueryInformationProcess - // (for protected processes) - PyErr_Clear(); - - func_ret = psutil_get_cmdline_data(pid, &data, &size); - if (func_ret != 0) { - goto out; - } - } - else { - goto out; - } + if (use_peb == 1) { + func_ret = psutil_get_process_data(pid, KIND_CMDLINE, &data, &size); + } + else { + func_ret = psutil_get_cmdline_data(pid, &data, &size); } + if (func_ret != 0) + goto out; // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(data, &nArgs); diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index fc3a0365f1..58ae3312e6 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -384,6 +384,16 @@ 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. diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 4633f7596c..35ea62171f 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -673,6 +673,22 @@ def test_num_handles(self): num_handles) assert fun.called + def test_cmdline(self): + from psutil._pswindows import ACCESS_DENIED_ERRSET + 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: + if err.errno in ACCESS_DENIED_ERRSET: + pass + elif err.errno == errno.ESRCH: + pass # NSP + else: + raise + else: + self.assertEqual(a, b) + @unittest.skipIf(not WINDOWS, "WINDOWS only") class RemoteProcessTestCase(unittest.TestCase): From eda2aea6e60fe61ef31a0ebc61984f4b492ddc0e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Feb 2019 20:11:02 +0100 Subject: [PATCH 0136/1714] pre-release --- HISTORY.rst | 2 +- MANIFEST.in | 1 - docs/index.rst | 4 ++++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 770182ed16..ebac8db799 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.5.1 ===== -XXXX-XX-XX +2019-02-15 **Enhancements** diff --git a/MANIFEST.in b/MANIFEST.in index 26d678fc33..ba7978e2b3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -24,7 +24,6 @@ include psutil/DEVNOTES include psutil/__init__.py include psutil/_common.py include psutil/_compat.py -include psutil/_exceptions.py include psutil/_psaix.py include psutil/_psbsd.py include psutil/_pslinux.py diff --git a/docs/index.rst b/docs/index.rst index 7cfa9b8207..3c87b8c64f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2622,6 +2622,10 @@ take a look at the Timeline ======== +- 2019-02-15: + `5.5.1 `__ - + `what's new `__ - + `diff ` - 2019-01-23: `5.5.0 `__ - From 217283bce454079ee5a7a2e598fd45314b9a7f80 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Feb 2019 20:11:34 +0100 Subject: [PATCH 0137/1714] pre-release --- psutil/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 3259dd0ec5..9e7d3d02b5 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -215,7 +215,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.5.0" +__version__ = "5.5.1" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED From 1c3287754f7b87708c3b81e72d1ab4f7f423e9ea Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Feb 2019 20:14:02 +0100 Subject: [PATCH 0138/1714] force appveyor run --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index ee434903ad..a5fe5d0332 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# Build: 1 (bump this up by 1 to force an appveyor run) +# Build: 2 (bump this up by 1 to force an appveyor run) os: Visual Studio 2015 From 7381d4d15dd7fe108e1bf350c3f76035922c6ed4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Feb 2019 10:15:33 -0800 Subject: [PATCH 0139/1714] #1394 / win / exe: use QueryFullProcessImageNameW to get the exe() (#1413) #1394 / win / exe: use QueryFullProcessImageNameW to get the exe() --- HISTORY.rst | 11 ++++++++++ psutil/__init__.py | 2 +- psutil/_psutil_windows.c | 19 +++++++++++++++++- psutil/_pswindows.py | 43 ++++++++++++++++++++++++++++++---------- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ebac8db799..b1eb8ea492 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,16 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.5.2 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 1394_: [Windows] Process name() and exe() may erronously return "Registry". + QueryFullProcessImageNameW is now used instead of GetProcessImageFileNameW + in order to prevent that. + 5.5.1 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 9e7d3d02b5..241dc5f9f0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -215,7 +215,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.5.1" +__version__ = "5.5.2" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index eb35c5f7d0..b5b5a8f5a4 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -764,14 +764,30 @@ psutil_proc_exe(PyObject *self, PyObject *args) { long pid; HANDLE hProcess; wchar_t exe[MAX_PATH]; +#if (_WIN32_WINNT >= 0x0600) // >= Vista + PDWORD size = MAX_PATH; +#endif if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; + + // Here we differentiate between XP and Vista+ because + // QueryFullProcessImageNameW is better than GetProcessImageFileNameW + // (avoid using QueryDosDevice on the returned path), see: + // https://github.com/giampaolo/psutil/issues/1394 +#if (_WIN32_WINNT >= 0x0600) // Windows >= Vista + memset(exe, 0, MAX_PATH); + if (QueryFullProcessImageNameW(hProcess, 0, exe, &size) == 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } +#else // Windows XP if (GetProcessImageFileNameW(hProcess, exe, MAX_PATH) == 0) { - // https://github.com/giampaolo/psutil/issues/1394 + // see: https://github.com/giampaolo/psutil/issues/1394 if (GetLastError() == 0) PyErr_SetFromWindowsErr(ERROR_ACCESS_DENIED); else @@ -779,6 +795,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { CloseHandle(hProcess); return NULL; } +#endif CloseHandle(hProcess); return PyUnicode_FromWideChar(exe, wcslen(exe)); } diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e66febe0f2..0441b8a17e 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -147,6 +147,28 @@ class Priority(enum.IntEnum): AccessDenied = None TimeoutExpired = None +# More values at: https://stackoverflow.com/a/20804735/376587 +WIN_10 = (10, 0) +WIN_8 = (6, 2) +WIN_7 = (6, 1) +WIN_SERVER_2008 = (6, 0) +WIN_VISTA = (6, 0) +WIN_SERVER_2003 = (5, 2) +WIN_XP = (5, 1) + + +@lru_cache() +def get_winver(): + """Usage: + >>> if get_winver() <= WIN_VISTA: + ... ... + """ + wv = sys.getwindowsversion() + return (wv.major, wv.minor) + + +IS_WIN_XP = get_winver() < WIN_VISTA + # ===================================================================== # --- named tuples @@ -694,16 +716,17 @@ def name(self): @wrap_exceptions 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) - exe = cext.proc_exe(self.pid) - exe = convert_dos_path(exe) + # Dual implementation, see: + # https://github.com/giampaolo/psutil/pull/1413 + if not IS_WIN_XP: + exe = cext.proc_exe(self.pid) + else: + if self.pid in (0, 4): + # https://github.com/giampaolo/psutil/issues/414 + # https://github.com/giampaolo/psutil/issues/528 + raise AccessDenied(self.pid, self._name) + exe = cext.proc_exe(self.pid) + exe = convert_dos_path(exe) return py2_strencode(exe) @wrap_exceptions From f4047035a45f01de2fab774d6b645c7e544c5224 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 19 Feb 2019 08:41:16 -0800 Subject: [PATCH 0140/1714] fix compilation warnings on win --- psutil/_psutil_windows.c | 3 +-- psutil/arch/windows/process_info.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index b5b5a8f5a4..e875b74228 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -765,7 +765,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { HANDLE hProcess; wchar_t exe[MAX_PATH]; #if (_WIN32_WINNT >= 0x0600) // >= Vista - PDWORD size = MAX_PATH; + unsigned int size = sizeof(exe); #endif if (! PyArg_ParseTuple(args, "l", &pid)) @@ -3100,7 +3100,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { GetSystemInfo(&system_info); maxAddr = system_info.lpMaximumApplicationAddress; baseAddress = NULL; - previousAllocationBase = NULL; while (VirtualQueryEx(hProcess, baseAddress, &basicInfo, sizeof(MEMORY_BASIC_INFORMATION))) diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index f85c1efdf6..b8e9e28ba5 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -26,7 +26,7 @@ int psutil_assert_pid_exists(DWORD pid, char *err); int psutil_assert_pid_not_exists(DWORD pid, char *err); -PyObject* psutil_get_cmdline(long pid); +PyObject* psutil_get_cmdline(long pid, int use_peb); PyObject* psutil_get_cwd(long pid); PyObject* psutil_get_environ(long pid); From 097b6e9ec4843acfc5c98c147d7aff418679f1db Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 19 Feb 2019 12:52:48 -0800 Subject: [PATCH 0141/1714] Windows / refactoring: utility functions for LoadLibraryA and GetProcAddress (#1417) Windows / refactoring: add utility functions for LoadLibraryA and GetProcAddress. Centralize logic in one place. --- make.bat | 7 +- psutil/_psutil_windows.c | 157 +++++++++----------------- psutil/arch/windows/inet_ntop.h | 2 +- psutil/arch/windows/process_handles.c | 14 +-- psutil/arch/windows/process_info.c | 148 +++++++++--------------- psutil/arch/windows/process_info.h | 6 +- 6 files changed, 121 insertions(+), 213 deletions(-) diff --git a/make.bat b/make.bat index 43000535da..d47eaecc78 100644 --- a/make.bat +++ b/make.bat @@ -20,8 +20,13 @@ rem set PYTHON=C:\Python34\python.exe & set TSCRIPT=foo.py & make.bat test rem ========================================================================== if "%PYTHON%" == "" ( - set PYTHON=C:\Python27\python.exe + if exist "C:\Python37\python.exe" ( + set PYTHON=C:\Python37\python.exe + ) else ( + set PYTHON=C:\Python27\python.exe + ) ) + if "%TSCRIPT%" == "" ( set TSCRIPT=psutil\tests\__main__.py ) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index b5b5a8f5a4..55a1cb220d 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -205,15 +205,12 @@ psutil_get_num_cpus(int fail_on_err) { unsigned int ncpus = 0; SYSTEM_INFO sysinfo; static DWORD(CALLBACK *_GetActiveProcessorCount)(WORD) = NULL; - HINSTANCE hKernel32; // GetActiveProcessorCount is available only on 64 bit versions // of Windows from Windows 7 onward. - // Windows Vista 64 bit and Windows XP doesn't have it. - hKernel32 = GetModuleHandleW(L"KERNEL32"); - _GetActiveProcessorCount = (void*)GetProcAddress( - hKernel32, "GetActiveProcessorCount"); - + // Windows Vista 64 bit and Windows XP don't have it. + _GetActiveProcessorCount = \ + psutil_GetProcAddress("kernel32", "GetActiveProcessorCount"); if (_GetActiveProcessorCount != NULL) { ncpus = _GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); if ((ncpus == 0) && (fail_on_err == 1)) { @@ -261,11 +258,9 @@ psutil_boot_time(PyObject *self, PyObject *args) { time_t pt; FILETIME fileTime; long long ll; - HINSTANCE hKernel32; - psutil_GetTickCount64 = NULL; + psutil_GetTickCount64; GetSystemTimeAsFileTime(&fileTime); - /* HUGE thanks to: http://johnstewien.spaces.live.com/blog/cns!E6885DB5CEBABBC8!831.entry @@ -288,12 +283,11 @@ psutil_boot_time(PyObject *self, PyObject *args) { pt = (time_t)((ll - 116444736000000000ull) / 10000000ull); // GetTickCount64() is Windows Vista+ only. Dinamically load - // GetTickCount64() at runtime. We may have used + // it at runtime. We may have used // "#if (_WIN32_WINNT >= 0x0600)" pre-processor but that way // the produced exe/wheels cannot be used on Windows XP, see: // https://github.com/giampaolo/psutil/issues/811#issuecomment-230639178 - hKernel32 = GetModuleHandleW(L"KERNEL32"); - psutil_GetTickCount64 = (void*)GetProcAddress(hKernel32, "GetTickCount64"); + psutil_GetTickCount64 = psutil_GetProcAddress("kernel32", "GetTickCount64"); if (psutil_GetTickCount64 != NULL) { // Windows >= Vista uptime = psutil_GetTickCount64() / (ULONGLONG)1000.00f; @@ -642,14 +636,10 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { // it supports process groups, meaning this is able to report more // than 64 CPUs. See: // https://bugs.python.org/issue33166 - _GetLogicalProcessorInformationEx = \ - (PFN_GETLOGICALPROCESSORINFORMATIONEX)GetProcAddress( - GetModuleHandle(TEXT("kernel32")), - "GetLogicalProcessorInformationEx"); - if (_GetLogicalProcessorInformationEx == NULL) { - psutil_debug("failed loading GetLogicalProcessorInformationEx()"); - goto return_none; - } + _GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( + "kernel32", "GetLogicalProcessorInformationEx"); + if (_GetLogicalProcessorInformationEx == NULL) + return NULL; while (1) { rc = _GetLogicalProcessorInformationEx( @@ -765,7 +755,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { HANDLE hProcess; wchar_t exe[MAX_PATH]; #if (_WIN32_WINNT >= 0x0600) // >= Vista - PDWORD size = MAX_PATH; + unsigned int size = sizeof(exe); #endif if (! PyArg_ParseTuple(args, "l", &pid)) @@ -1057,7 +1047,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // NtQuerySystemInformation stuff typedef DWORD (_stdcall * NTQSI_PROC) (int, PVOID, ULONG, PULONG); NTQSI_PROC NtQuerySystemInformation; - HINSTANCE hNtDll; double idle, kernel, systemt, user, interrupt, dpc; NTSTATUS status; @@ -1069,19 +1058,10 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - - // obtain NtQuerySystemInformation - hNtDll = LoadLibrary(TEXT("ntdll.dll")); - if (hNtDll == NULL) { - PyErr_SetFromWindowsErr(0); + NtQuerySystemInformation = \ + psutil_GetProcAddressFromLib("ntdll.dll", "NtQuerySystemInformation"); + if (NtQuerySystemInformation == NULL) goto error; - } - NtQuerySystemInformation = (NTQSI_PROC)GetProcAddress( - hNtDll, "NtQuerySystemInformation"); - if (NtQuerySystemInformation == NULL) { - PyErr_SetFromWindowsErr(0); - goto error; - } // retrieves number of processors ncpus = psutil_get_num_cpus(1); @@ -1144,7 +1124,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } free(sppi); - FreeLibrary(hNtDll); return py_retlist; error: @@ -1152,8 +1131,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { Py_DECREF(py_retlist); if (sppi) free(sppi); - if (hNtDll) - FreeLibrary(hNtDll); return NULL; } @@ -1672,11 +1649,26 @@ psutil_net_connections(PyObject *self, PyObject *args) { PyObject *_SOCK_STREAM = PyLong_FromLong((long)SOCK_STREAM); PyObject *_SOCK_DGRAM = PyLong_FromLong((long)SOCK_DGRAM); + // Import some functions. + rtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv4AddressToStringA"); + if (rtlIpv4AddressToStringA == NULL) + goto error; + rtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv6AddressToStringA"); + if (rtlIpv6AddressToStringA == NULL) + goto error; + getExtendedTcpTable = psutil_GetProcAddressFromLib( + "iphlpapi.dll", "GetExtendedTcpTable"); + if (getExtendedTcpTable == NULL) + goto error; + getExtendedUdpTable = psutil_GetProcAddressFromLib( + "iphlpapi.dll", "GetExtendedUdpTable"); + if (getExtendedUdpTable == NULL) + goto error; + if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) - { - _psutil_conn_decref_objs(); - return NULL; - } + goto error; if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { _psutil_conn_decref_objs(); @@ -1696,27 +1688,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { } } - // Import some functions. - { - HMODULE ntdll; - HMODULE iphlpapi; - - ntdll = LoadLibrary(TEXT("ntdll.dll")); - rtlIpv4AddressToStringA = (_RtlIpv4AddressToStringA)GetProcAddress( - ntdll, "RtlIpv4AddressToStringA"); - rtlIpv6AddressToStringA = (_RtlIpv6AddressToStringA)GetProcAddress( - ntdll, "RtlIpv6AddressToStringA"); - /* TODO: Check these two function pointers */ - - iphlpapi = LoadLibrary(TEXT("iphlpapi.dll")); - getExtendedTcpTable = (_GetExtendedTcpTable)GetProcAddress(iphlpapi, - "GetExtendedTcpTable"); - getExtendedUdpTable = (_GetExtendedUdpTable)GetProcAddress(iphlpapi, - "GetExtendedUdpTable"); - FreeLibrary(ntdll); - FreeLibrary(iphlpapi); - } - if ((getExtendedTcpTable == NULL) || (getExtendedUdpTable == NULL)) { PyErr_SetString(PyExc_NotImplementedError, "feature not supported on this Windows version"); @@ -2152,11 +2123,12 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { long pid; HANDLE hProcess; DWORD IoPriority; + _NtQueryInformationProcess NtQueryInformationProcess; - _NtQueryInformationProcess NtQueryInformationProcess = - (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); - + NtQueryInformationProcess = \ + psutil_GetProcAddress("ntdll.dll", "NtQueryInformationProcess"); + if (NtQueryInformationProcess == NULL) + return NULL; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -2184,17 +2156,12 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { DWORD prio; HANDLE hProcess; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; + _NtSetInformationProcess NtSetInformationProcess; - _NtSetInformationProcess NtSetInformationProcess = - (_NtSetInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtSetInformationProcess"); - - if (NtSetInformationProcess == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "couldn't get NtSetInformationProcess syscall"); + NtSetInformationProcess = \ + psutil_GetProcAddress("ntdll.dll", "NtSetInformationProcess"); + if (NtSetInformationProcess == NULL) return NULL; - } - if (! PyArg_ParseTuple(args, "li", &pid, &prio)) return NULL; hProcess = psutil_handle_from_pid(pid, access); @@ -2797,23 +2764,21 @@ psutil_users(PyObject *self, PyObject *args) { PWTS_CLIENT_ADDRESS address; char address_str[50]; long long unix_time; - PWINSTATIONQUERYINFORMATIONW WinStationQueryInformationW; WINSTATION_INFO station_info; - HINSTANCE hInstWinSta = NULL; ULONG returnLen; - - PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; PyObject *py_address = NULL; PyObject *py_username = NULL; + PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) return NULL; - hInstWinSta = LoadLibraryA("winsta.dll"); - WinStationQueryInformationW = (PWINSTATIONQUERYINFORMATIONW) \ - GetProcAddress(hInstWinSta, "WinStationQueryInformationW"); + WinStationQueryInformationW = psutil_GetProcAddressFromLib( + "winsta.dll", "WinStationQueryInformationW"); + if (WinStationQueryInformationW == NULL) + goto error; if (WTSEnumerateSessions(hServer, 0, 1, &sessions, &count) == 0) { PyErr_SetFromWindowsErr(0); @@ -2902,7 +2867,6 @@ psutil_users(PyObject *self, PyObject *args) { WTSFreeMemory(sessions); WTSFreeMemory(buffer_user); WTSFreeMemory(buffer_addr); - FreeLibrary(hInstWinSta); return py_retlist; error: @@ -2911,8 +2875,6 @@ psutil_users(PyObject *self, PyObject *args) { Py_XDECREF(py_address); Py_DECREF(py_retlist); - if (hInstWinSta != NULL) - FreeLibrary(hInstWinSta); if (sessions != NULL) WTSFreeMemory(sessions); if (buffer_user != NULL) @@ -3100,7 +3062,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { GetSystemInfo(&system_info); maxAddr = system_info.lpMaximumApplicationAddress; baseAddress = NULL; - previousAllocationBase = NULL; while (VirtualQueryEx(hProcess, baseAddress, &basicInfo, sizeof(MEMORY_BASIC_INFORMATION))) @@ -3135,7 +3096,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { Py_DECREF(py_tuple); Py_DECREF(py_str); } - previousAllocationBase = basicInfo.AllocationBase; + previousAllocationBase = (ULONGLONG)basicInfo.AllocationBase; baseAddress = (PCHAR)baseAddress + basicInfo.RegionSize; } @@ -3517,11 +3478,8 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { */ static PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { - // NtQuerySystemInformation stuff typedef DWORD (_stdcall * NTQSI_PROC) (int, PVOID, ULONG, PULONG); NTQSI_PROC NtQuerySystemInformation; - HINSTANCE hNtDll; - NTSTATUS status; _SYSTEM_PERFORMANCE_INFORMATION *spi = NULL; _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; @@ -3531,18 +3489,10 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ULONG64 dpcs = 0; ULONG interrupts = 0; - // obtain NtQuerySystemInformation - hNtDll = LoadLibrary(TEXT("ntdll.dll")); - if (hNtDll == NULL) { - PyErr_SetFromWindowsErr(0); - goto error; - } - NtQuerySystemInformation = (NTQSI_PROC)GetProcAddress( - hNtDll, "NtQuerySystemInformation"); - if (NtQuerySystemInformation == NULL) { - PyErr_SetFromWindowsErr(0); - goto error; - } + NtQuerySystemInformation = \ + psutil_GetProcAddressFromLib("ntdll.dll", "NtQuerySystemInformation"); + if (NtQuerySystemInformation == NULL) + return NULL; // retrieves number of processors ncpus = psutil_get_num_cpus(1); @@ -3613,7 +3563,6 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { free(spi); free(InterruptInformation); free(sppi); - FreeLibrary(hNtDll); return Py_BuildValue( "kkkk", spi->ContextSwitches, @@ -3629,8 +3578,6 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { free(InterruptInformation); if (sppi) free(sppi); - if (hNtDll) - FreeLibrary(hNtDll); return NULL; } diff --git a/psutil/arch/windows/inet_ntop.h b/psutil/arch/windows/inet_ntop.h index 70573a3685..0dbf28bfa4 100644 --- a/psutil/arch/windows/inet_ntop.h +++ b/psutil/arch/windows/inet_ntop.h @@ -9,7 +9,7 @@ PCSTR WSAAPI inet_ntop( __in INT Family, - __in PVOID pAddr, + __in const VOID * pAddr, __out_ecount(StringBufSize) PSTR pStringBuf, __in size_t StringBufSize ); diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 356e236867..4ac02b9109 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -5,6 +5,7 @@ * */ #include "process_handles.h" +#include "process_info.h" #include "../../_psutil_common.h" static _NtQuerySystemInformation __NtQuerySystemInformation = NULL; @@ -22,12 +23,6 @@ ULONG g_dwSize = 0; ULONG g_dwLength = 0; -PVOID -GetLibraryProcAddress(PSTR LibraryName, PSTR ProcName) { - return GetProcAddress(GetModuleHandleA(LibraryName), ProcName); -} - - PyObject * psutil_get_open_files(long dwPid, HANDLE hProcess) { OSVERSIONINFO osvi; @@ -50,9 +45,10 @@ psutil_get_open_files_init(BOOL threaded) { return; // Resolve the Windows API calls - __NtQuerySystemInformation = - GetLibraryProcAddress("ntdll.dll", "NtQuerySystemInformation"); - __NtQueryObject = GetLibraryProcAddress("ntdll.dll", "NtQueryObject"); + __NtQuerySystemInformation = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtQuerySystemInformation"); + __NtQueryObject = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtQueryObject"); // Create events for signalling work between threads if (threaded == TRUE) { diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 6f877342d7..9001ab1435 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -161,70 +161,42 @@ typedef struct { const int STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; const int STATUS_BUFFER_TOO_SMALL = 0xC0000023L; -#define WINDOWS_UNINITIALIZED 0 -#define WINDOWS_XP 51 -#define WINDOWS_VISTA 60 -#define WINDOWS_7 61 -#define WINDOWS_8 62 -#define WINDOWS_81 63 -#define WINDOWS_10 100 - - -int get_windows_version() { - OSVERSIONINFO ver_info; - BOOL result; - DWORD dwMajorVersion; - DWORD dwMinorVersion; - DWORD dwBuildNumber; - static int windows_version = WINDOWS_UNINITIALIZED; - // windows_version is static - // and equal to WINDOWS_UNINITIALIZED only on first call - if (windows_version == WINDOWS_UNINITIALIZED) { - memset(&ver_info, 0, sizeof(ver_info)); - ver_info.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); - result = GetVersionEx(&ver_info); - if (result != FALSE) { - dwMajorVersion = ver_info.dwMajorVersion; - dwMinorVersion = ver_info.dwMinorVersion; - dwBuildNumber = ver_info.dwBuildNumber; - // Windows XP, Windows server 2003 - if (dwMajorVersion == 5 && dwMinorVersion == 1) { - windows_version = WINDOWS_XP; - } - // Windows Vista - else if (dwMajorVersion == 6 && dwMinorVersion == 0) { - windows_version = WINDOWS_VISTA; - } - // Windows 7, Windows Server 2008 R2 - else if (dwMajorVersion == 6 && dwMinorVersion == 1) { - windows_version = WINDOWS_7; - } - // Windows 8, Windows Server 2012 - else if (dwMajorVersion == 6 && dwMinorVersion == 2) { - windows_version = WINDOWS_8; - } - // Windows 8.1, Windows Server 2012 R2 - else if (dwMajorVersion == 6 && dwMinorVersion == 3) - { - windows_version = WINDOWS_81; - } - // Windows 10, Windows Server 2016 - else if (dwMajorVersion == 10) { - windows_version = WINDOWS_10; - } - } + +// A wrapper around GetModuleHandle and GetProcAddress. +PVOID +psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { + HMODULE mod; + FARPROC addr; + + if ((mod = GetModuleHandleA(libname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, procname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, procname); + return NULL; } - return windows_version; + return addr; } -_NtQueryInformationProcess psutil_NtQueryInformationProcess() { - static _NtQueryInformationProcess NtQueryInformationProcess = NULL; - if (NtQueryInformationProcess == NULL) { - NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); +// A wrapper around LoadLibrary and GetProcAddress. +PVOID +psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { + HMODULE mod; + FARPROC addr; + + if ((mod = LoadLibraryA(libname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, procname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, procname); + FreeLibrary(mod); + return NULL; } - return NtQueryInformationProcess; + FreeLibrary(mod); + return addr; } @@ -523,11 +495,8 @@ psutil_get_process_region_size64(HANDLE hProcess, MEMORY_BASIC_INFORMATION64 info64; if (NtWow64QueryVirtualMemory64 == NULL) { - NtWow64QueryVirtualMemory64 = - (_NtWow64QueryVirtualMemory64)GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtWow64QueryVirtualMemory64"); - + NtWow64QueryVirtualMemory64 = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtWow64QueryVirtualMemory64"); if (NtWow64QueryVirtualMemory64 == NULL) { PyErr_SetString(PyExc_NotImplementedError, "NtWow64QueryVirtualMemory64 missing"); @@ -611,12 +580,15 @@ psutil_get_process_data(long pid, #endif DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; + NtQueryInformationProcess = \ + psutil_GetProcAddress("ntdll.dll", "NtQueryInformationProcess"); + if (NtQueryInformationProcess == NULL) + return -1; + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return -1; - NtQueryInformationProcess = psutil_NtQueryInformationProcess(); - #ifdef _WIN64 /* 64 bit case. Check if the target is a 32 bit process running in WoW64 * mode. */ @@ -679,11 +651,9 @@ psutil_get_process_data(long pid, RTL_USER_PROCESS_PARAMETERS64 procParameters64; if (NtWow64QueryInformationProcess64 == NULL) { - NtWow64QueryInformationProcess64 = - (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtWow64QueryInformationProcess64"); - + NtWow64QueryInformationProcess64 = \ + psutil_GetProcAddressFromLib( + "ntdll.dll", "NtWow64QueryInformationProcess64"); if (NtWow64QueryInformationProcess64 == NULL) { PyErr_SetString(PyExc_NotImplementedError, "NtWow64QueryInformationProcess64 missing"); @@ -703,11 +673,9 @@ psutil_get_process_data(long pid, // read peb if (NtWow64ReadVirtualMemory64 == NULL) { - NtWow64ReadVirtualMemory64 = - (_NtWow64ReadVirtualMemory64)GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtWow64ReadVirtualMemory64"); - + NtWow64ReadVirtualMemory64 = \ + psutil_GetProcAddressFromLib( + "ntdll.dll", "NtWow64ReadVirtualMemory64"); if (NtWow64ReadVirtualMemory64 == NULL) { PyErr_SetString(PyExc_NotImplementedError, "NtWow64ReadVirtualMemory64 missing"); @@ -870,13 +838,12 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { WCHAR * cmdline_buffer_wchar = NULL; PUNICODE_STRING tmp = NULL; DWORD string_size; - _NtQueryInformationProcess NtQueryInformationProcess = NULL; + _NtQueryInformationProcess NtQueryInformationProcess; - NtQueryInformationProcess = psutil_NtQueryInformationProcess(); - if (NtQueryInformationProcess == NULL) { - PyErr_SetFromWindowsErr(0); + NtQueryInformationProcess = \ + psutil_GetProcAddress("ntdll.dll", "NtQueryInformationProcess"); + if (NtQueryInformationProcess == NULL) goto error; - } cmdline_buffer = calloc(ret_length, 1); if (cmdline_buffer == NULL) { @@ -885,10 +852,8 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { } hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (hProcess == NULL) { - PyErr_SetFromWindowsErr(0); + if (hProcess == NULL) goto error; - } status = NtQueryInformationProcess( hProcess, 60, // ProcessCommandLineInformation @@ -939,10 +904,8 @@ psutil_get_cmdline(long pid, int use_peb) { PyObject *py_unicode = NULL; LPWSTR *szArglist = NULL; int nArgs, i; - int windows_version; int func_ret; - windows_version = get_windows_version(); /* By defaut, still use PEB (if command line params have been patched in the PEB, we will get the actual ones). Reading the PEB to get the @@ -1058,14 +1021,13 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID buffer; ULONG bufferSize; PSYSTEM_PROCESS_INFORMATION process; - - // get NtQuerySystemInformation typedef DWORD (_stdcall * NTQSI_PROC) (int, PVOID, ULONG, PULONG); NTQSI_PROC NtQuerySystemInformation; - HINSTANCE hNtDll; - hNtDll = LoadLibrary(TEXT("ntdll.dll")); - NtQuerySystemInformation = (NTQSI_PROC)GetProcAddress( - hNtDll, "NtQuerySystemInformation"); + + NtQuerySystemInformation = \ + psutil_GetProcAddressFromLib("ntdll.dll", "NtQuerySystemInformation"); + if (NtQuerySystemInformation == NULL) + goto error; bufferSize = initialBufferSize; buffer = malloc(bufferSize); @@ -1077,7 +1039,6 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, while (TRUE) { status = NtQuerySystemInformation(SystemProcessInformation, buffer, bufferSize, &bufferSize); - if (status == STATUS_BUFFER_TOO_SMALL || status == STATUS_INFO_LENGTH_MISMATCH) { @@ -1115,7 +1076,6 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, goto error; error: - FreeLibrary(hNtDll); if (buffer != NULL) free(buffer); return 0; diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index f85c1efdf6..d2e60b7c5c 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -15,7 +15,6 @@ #define HANDLE_TO_PYNUM(handle) PyLong_FromUnsignedLong((unsigned long) handle) #define PYNUM_TO_HANDLE(obj) ((HANDLE)PyLong_AsUnsignedLong(obj)) - DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); int psutil_pid_is_running(DWORD pid); @@ -24,9 +23,10 @@ int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, int psutil_assert_pid_exists(DWORD pid, char *err); int psutil_assert_pid_not_exists(DWORD pid, char *err); +PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); +PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); - -PyObject* psutil_get_cmdline(long pid); +PyObject* psutil_get_cmdline(long pid, int use_peb); PyObject* psutil_get_cwd(long pid); PyObject* psutil_get_environ(long pid); From b0ad9b6484c9d0b1907b2141664f631f481af73e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 19 Feb 2019 18:36:46 -0800 Subject: [PATCH 0142/1714] fix #1419: Process.environ() raise NotImplementedError for 32-bit-WoW process --- HISTORY.rst | 2 + psutil/arch/windows/process_info.c | 78 ++++++------------------------ psutil/tests/test_windows.py | 6 +-- 3 files changed, 19 insertions(+), 67 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b1eb8ea492..be3787e7a1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,8 @@ XXXX-XX-XX - 1394_: [Windows] Process name() and exe() may erronously return "Registry". QueryFullProcessImageNameW is now used instead of GetProcessImageFileNameW in order to prevent that. +- 1419_: [Windows] Process.environ() raises NotImplementedError when querying + a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. 5.5.1 ===== diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 9001ab1435..a2edca58d2 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -107,14 +107,6 @@ typedef enum { MemoryInformationBasic } MEMORY_INFORMATION_CLASS; -typedef NTSTATUS (NTAPI *_NtWow64QueryVirtualMemory64)( - IN HANDLE ProcessHandle, - IN PVOID64 BaseAddress, - IN MEMORY_INFORMATION_CLASS MemoryInformationClass, - OUT PMEMORY_BASIC_INFORMATION64 MemoryInformation, - IN ULONG64 Size, - OUT PULONG64 ReturnLength OPTIONAL); - typedef struct { PVOID Reserved1[2]; PVOID64 PebBaseAddress; @@ -484,43 +476,6 @@ psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { } -#ifndef _WIN64 -/* Given a pointer into a process's memory, figure out how much data can be - * read from it. */ -static int -psutil_get_process_region_size64(HANDLE hProcess, - const PVOID64 src64, - PULONG64 psize) { - static _NtWow64QueryVirtualMemory64 NtWow64QueryVirtualMemory64 = NULL; - MEMORY_BASIC_INFORMATION64 info64; - - if (NtWow64QueryVirtualMemory64 == NULL) { - NtWow64QueryVirtualMemory64 = psutil_GetProcAddressFromLib( - "ntdll.dll", "NtWow64QueryVirtualMemory64"); - if (NtWow64QueryVirtualMemory64 == NULL) { - PyErr_SetString(PyExc_NotImplementedError, - "NtWow64QueryVirtualMemory64 missing"); - return -1; - } - } - - if (!NT_SUCCESS(NtWow64QueryVirtualMemory64( - hProcess, - src64, - 0, - &info64, - sizeof(info64), - NULL))) { - PyErr_SetFromWindowsErr(0); - return -1; - } - - *psize = info64.RegionSize - ((char*)src64 - (char*)info64.BaseAddress); - return 0; -} -#endif - - enum psutil_process_data_kind { KIND_CMDLINE, KIND_CWD, @@ -655,8 +610,18 @@ psutil_get_process_data(long pid, psutil_GetProcAddressFromLib( "ntdll.dll", "NtWow64QueryInformationProcess64"); if (NtWow64QueryInformationProcess64 == NULL) { - PyErr_SetString(PyExc_NotImplementedError, - "NtWow64QueryInformationProcess64 missing"); + // Too complicated. Give up. + AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); + goto error; + } + } + if (NtWow64ReadVirtualMemory64 == NULL) { + NtWow64ReadVirtualMemory64 = \ + psutil_GetProcAddressFromLib( + "ntdll.dll", "NtWow64ReadVirtualMemory64"); + if (NtWow64ReadVirtualMemory64 == NULL) { + // Too complicated. Give up. + AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); goto error; } } @@ -672,17 +637,6 @@ psutil_get_process_data(long pid, } // read peb - if (NtWow64ReadVirtualMemory64 == NULL) { - NtWow64ReadVirtualMemory64 = \ - psutil_GetProcAddressFromLib( - "ntdll.dll", "NtWow64ReadVirtualMemory64"); - if (NtWow64ReadVirtualMemory64 == NULL) { - PyErr_SetString(PyExc_NotImplementedError, - "NtWow64ReadVirtualMemory64 missing"); - goto error; - } - } - if (!NT_SUCCESS(NtWow64ReadVirtualMemory64(hProcess, pbi64.PebBaseAddress, &peb64, @@ -771,12 +725,8 @@ psutil_get_process_data(long pid, if (kind == KIND_ENVIRON) { #ifndef _WIN64 if (weAreWow64 && !theyAreWow64) { - ULONG64 size64; - - if (psutil_get_process_region_size64(hProcess, src64, &size64) != 0) - goto error; - - size = (SIZE_T)size64; + AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); + goto error; } else #endif diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 35ea62171f..9cb2624a60 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -774,10 +774,10 @@ def test_environ_32(self): self.assertEquals(e["THINK_OF_A_NUMBER"], str(os.getpid())) def test_environ_64(self): + # Environ 32 is not supported. p = psutil.Process(self.proc64.pid) - e = p.environ() - self.assertIn("THINK_OF_A_NUMBER", e) - self.assertEquals(e["THINK_OF_A_NUMBER"], str(os.getpid())) + with self.assertRaises(psutil.AccessDenied): + p.environ() # =================================================================== From 76ab63bd63092c432c4cb2397a1cc45602e7e5a7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 19 Feb 2019 18:46:32 -0800 Subject: [PATCH 0143/1714] fix #1420: use PyErr_SetFromWindowsErrWithFilename for disk_usage() in case of error --- HISTORY.rst | 5 +++++ psutil/_psutil_windows.c | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index be3787e7a1..b4f4888d52 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,11 @@ XXXX-XX-XX - 1419_: [Windows] Process.environ() raises NotImplementedError when querying a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. +**Enhancements** + +- 1420_: [Windows] in case of exception disk_usage() now also shows the path + name. + 5.5.1 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 55a1cb220d..d6058b8bc6 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2338,7 +2338,7 @@ psutil_disk_usage(PyObject *self, PyObject *args) { return_: if (retval == 0) - return PyErr_SetFromWindowsErr(0); + return PyErr_SetFromWindowsErrWithFilename(0, path); else return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); } From e118e40b964570b586aba58e8a413f8dfab2c509 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Feb 2019 05:02:14 -0800 Subject: [PATCH 0144/1714] win test: skip cd-dorm for disk_usage() (cuase device busy error) --- psutil/tests/test_windows.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 9cb2624a60..7be12d9c52 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -157,6 +157,8 @@ def test_disks(self): 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: @@ -178,6 +180,8 @@ def test_disks(self): 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, From ba649e832140fd69e0adb3958ad298b8f7434e69 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 21 Feb 2019 16:22:02 +0100 Subject: [PATCH 0145/1714] update doc --- README.rst | 12 +++++--- docs/index.rst | 45 +++++++++++++++--------------- scripts/internal/print_announce.py | 4 +-- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/README.rst b/README.rst index 00713f4809..03be872d8c 100644 --- a/README.rst +++ b/README.rst @@ -86,10 +86,14 @@ and `doc recipes `__. Projects using psutil ===================== -At the time of writing psutil has roughly -`2.9 milion downloads `__ -per month and there are over -`8000 open source projects `__ +psutil has roughly the following monthly downloads: + +.. image:: https://pepy.tech/badge/psutil/month + :target: https://pepy.tech/project/psutil + :alt: Downloads + +There are over +`10000 open source projects `__ on github which depend from psutil. Here's some I find particularly interesting: diff --git a/docs/index.rst b/docs/index.rst index 3c87b8c64f..034795f975 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2556,32 +2556,31 @@ Bytes conversion 100399730688 93.5G +Supported platforms +=================== + +These are the platforms I develop and test on: + +* Linux Ubuntu 16.04 +* MacOS 10.11 El Captain +* Windows 10 +* Solaris 10 +* FreeBSD 11 +* OpenBSD 6.4 +* NetBSD 8.0 +* AIX 6.1 TL8 (maintainer `Arnon Yaari `__) + +Earlier versions are supposed to work but are not tested. +For Linux, Windows and MacOS we have continuos integration. Other platforms +are tested manually from time to time. +Oldest supported Windows version is Windows XP, which can be compiled from +sources. Latest wheel supporting Windows XP is +`psutil 2.1.3 `__. +Supported Python versions are 3.4+, 2.7 and 2.6. + FAQs ==== -* Q: What Windows versions are supported? -* A: From Windows **Vista** onwards, both 32 and 64 bit versions. - Latest binary (wheel / exe) release which supports Windows **2000**, **XP** - and **2003 server** is - `psutil 3.4.2 `__. - On such old systems psutil is no longer tested or maintained, but it can - still be compiled from sources (you'll need `Visual Studio <(https://github.com/giampaolo/psutil/blob/master/INSTALL.rst#windows>`__) - and it should "work" (more or less). - ----- - -* Q: What Python versions are supported? -* A: From 2.6 to 3.6, both 32 and 64 bit versions. Last version supporting - Python 2.4 and 2.5 is `psutil 2.1.3 `__. - PyPy is also known to work. - ----- - -* Q: What SunOS versions are supported? -* A: From Solaris 10 onwards. - ----- - * Q: Why do I get :class:`AccessDenied` for certain processes? * A: This may happen when you query processess owned by another user, especially on `macOS `__ and diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index a39351d61e..7fbe74f3d6 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -40,8 +40,8 @@ line tools such as: ps, top, lsof, netstat, ifconfig, who, df, kill, free, \ nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap. It \ currently supports Linux, Windows, macOS, Sun Solaris, FreeBSD, OpenBSD, \ -NetBSD and AIX, both 32-bit and 64-bit architectures, with Python versions \ -from 2.6 to 3.6. PyPy is also known to work. +NetBSD and AIX, both 32-bit and 64-bit architectures. Supported Python \ +versions are 2.6, 2.7 and 3.4+. PyPy is also known to work. What's new ========== From 0a30dc9dc2693ffe9aa54126eb684711d950c3f1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 21 Feb 2019 07:39:54 -0800 Subject: [PATCH 0146/1714] Windows / speeup: dynamically load libraries on startup and never again (#1422) Windows / speeup: dynamically load libraries on startup and never again. --- psutil/_psutil_windows.c | 289 +++++-------------------- psutil/arch/windows/global.c | 129 ++++++++++++ psutil/arch/windows/global.h | 11 + psutil/arch/windows/ntextapi.h | 293 ++++++++++++++++++++------ psutil/arch/windows/process_handles.c | 198 +++++++++-------- psutil/arch/windows/process_handles.h | 101 --------- psutil/arch/windows/process_info.c | 183 ++++++---------- psutil/arch/windows/process_info.h | 2 - psutil/tests/test_windows.py | 5 +- setup.py | 1 + 10 files changed, 581 insertions(+), 631 deletions(-) create mode 100644 psutil/arch/windows/global.c create mode 100644 psutil/arch/windows/global.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index d6058b8bc6..3908884e18 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -22,6 +22,8 @@ #include #endif #include +#include +#include #include #include #include @@ -30,13 +32,14 @@ // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") -#include "_psutil_common.h" +#include "arch/windows/ntextapi.h" +#include "arch/windows/global.h" #include "arch/windows/security.h" #include "arch/windows/process_info.h" #include "arch/windows/process_handles.h" -#include "arch/windows/ntextapi.h" #include "arch/windows/inet_ntop.h" #include "arch/windows/services.h" +#include "_psutil_common.h" /* @@ -53,109 +56,6 @@ #ifndef AF_INET6 #define AF_INET6 23 #endif -#define _psutil_conn_decref_objs() \ - Py_DECREF(_AF_INET); \ - Py_DECREF(_AF_INET6);\ - Py_DECREF(_SOCK_STREAM);\ - Py_DECREF(_SOCK_DGRAM); - -#if (_WIN32_WINNT >= 0x0601) // Windows 7 -typedef BOOL (WINAPI *PFN_GETLOGICALPROCESSORINFORMATIONEX)( - LOGICAL_PROCESSOR_RELATIONSHIP relationship, - PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Buffer, - PDWORD ReturnLength); -static PFN_GETLOGICALPROCESSORINFORMATIONEX _GetLogicalProcessorInformationEx; -#endif - -// Fix for mingw32, see: -// https://github.com/giampaolo/psutil/issues/351#c2 -// This is actually a DISK_PERFORMANCE struct: -// https://msdn.microsoft.com/en-us/library/windows/desktop/ -// aa363991(v=vs.85).aspx -typedef struct _DISK_PERFORMANCE_WIN_2008 { - LARGE_INTEGER BytesRead; - LARGE_INTEGER BytesWritten; - LARGE_INTEGER ReadTime; - LARGE_INTEGER WriteTime; - LARGE_INTEGER IdleTime; - DWORD ReadCount; - DWORD WriteCount; - DWORD QueueDepth; - DWORD SplitCount; - LARGE_INTEGER QueryTime; - DWORD StorageDeviceNumber; - WCHAR StorageManagerName[8]; -} DISK_PERFORMANCE_WIN_2008; - -// --- network connections mingw32 support -#ifndef _IPRTRMIB_H -#if (_WIN32_WINNT < 0x0600) // Windows XP -typedef struct _MIB_TCP6ROW_OWNER_PID { - UCHAR ucLocalAddr[16]; - DWORD dwLocalScopeId; - DWORD dwLocalPort; - UCHAR ucRemoteAddr[16]; - DWORD dwRemoteScopeId; - DWORD dwRemotePort; - DWORD dwState; - DWORD dwOwningPid; -} MIB_TCP6ROW_OWNER_PID, *PMIB_TCP6ROW_OWNER_PID; - -typedef struct _MIB_TCP6TABLE_OWNER_PID { - DWORD dwNumEntries; - MIB_TCP6ROW_OWNER_PID table[ANY_SIZE]; -} MIB_TCP6TABLE_OWNER_PID, *PMIB_TCP6TABLE_OWNER_PID; -#endif -#endif - -#ifndef __IPHLPAPI_H__ -typedef struct in6_addr { - union { - UCHAR Byte[16]; - USHORT Word[8]; - } u; -} IN6_ADDR, *PIN6_ADDR, FAR *LPIN6_ADDR; - -typedef enum _UDP_TABLE_CLASS { - UDP_TABLE_BASIC, - UDP_TABLE_OWNER_PID, - UDP_TABLE_OWNER_MODULE -} UDP_TABLE_CLASS, *PUDP_TABLE_CLASS; - -typedef struct _MIB_UDPROW_OWNER_PID { - DWORD dwLocalAddr; - DWORD dwLocalPort; - DWORD dwOwningPid; -} MIB_UDPROW_OWNER_PID, *PMIB_UDPROW_OWNER_PID; - -typedef struct _MIB_UDPTABLE_OWNER_PID { - DWORD dwNumEntries; - MIB_UDPROW_OWNER_PID table[ANY_SIZE]; -} MIB_UDPTABLE_OWNER_PID, *PMIB_UDPTABLE_OWNER_PID; -#endif - -#if (_WIN32_WINNT < 0x0600) // Windows XP -typedef struct _MIB_UDP6ROW_OWNER_PID { - UCHAR ucLocalAddr[16]; - DWORD dwLocalScopeId; - DWORD dwLocalPort; - DWORD dwOwningPid; -} MIB_UDP6ROW_OWNER_PID, *PMIB_UDP6ROW_OWNER_PID; - -typedef struct _MIB_UDP6TABLE_OWNER_PID { - DWORD dwNumEntries; - MIB_UDP6ROW_OWNER_PID table[ANY_SIZE]; -} MIB_UDP6TABLE_OWNER_PID, *PMIB_UDP6TABLE_OWNER_PID; -#endif - -typedef struct _PROCESSOR_POWER_INFORMATION { - ULONG Number; - ULONG MaxMhz; - ULONG CurrentMhz; - ULONG MhzLimit; - ULONG MaxIdleState; - ULONG CurrentIdleState; -} PROCESSOR_POWER_INFORMATION, *PPROCESSOR_POWER_INFORMATION; PIP_ADAPTER_ADDRESSES @@ -204,15 +104,10 @@ unsigned int psutil_get_num_cpus(int fail_on_err) { unsigned int ncpus = 0; SYSTEM_INFO sysinfo; - static DWORD(CALLBACK *_GetActiveProcessorCount)(WORD) = NULL; - - // GetActiveProcessorCount is available only on 64 bit versions - // of Windows from Windows 7 onward. - // Windows Vista 64 bit and Windows XP don't have it. - _GetActiveProcessorCount = \ - psutil_GetProcAddress("kernel32", "GetActiveProcessorCount"); - if (_GetActiveProcessorCount != NULL) { - ncpus = _GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); + + // Minimum requirement: Windows 7 + if (psutil_GetActiveProcessorCount != NULL) { + ncpus = psutil_GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); if ((ncpus == 0) && (fail_on_err == 1)) { PyErr_SetFromWindowsErr(0); } @@ -242,8 +137,6 @@ psutil_get_num_cpus(int fail_on_err) { static PyObject *TimeoutExpired; static PyObject *TimeoutAbandoned; -static ULONGLONG (*psutil_GetTickCount64)(void) = NULL; - /* * Return a Python float representing the system uptime expressed in seconds * since the epoch. @@ -258,7 +151,6 @@ psutil_boot_time(PyObject *self, PyObject *args) { time_t pt; FILETIME fileTime; long long ll; - psutil_GetTickCount64; GetSystemTimeAsFileTime(&fileTime); /* @@ -287,7 +179,6 @@ psutil_boot_time(PyObject *self, PyObject *args) { // "#if (_WIN32_WINNT >= 0x0600)" pre-processor but that way // the produced exe/wheels cannot be used on Windows XP, see: // https://github.com/giampaolo/psutil/issues/811#issuecomment-230639178 - psutil_GetTickCount64 = psutil_GetProcAddress("kernel32", "GetTickCount64"); if (psutil_GetTickCount64 != NULL) { // Windows >= Vista uptime = psutil_GetTickCount64() / (ULONGLONG)1000.00f; @@ -297,6 +188,8 @@ psutil_boot_time(PyObject *self, PyObject *args) { // Windows XP. // GetTickCount() time will wrap around to zero if the // system is run continuously for 49.7 days. + psutil_debug("Windows < Vista; using GetTickCount() instead of " + "GetTickCount64()"); uptime = GetTickCount() / (LONGLONG)1000.00f; return Py_BuildValue("L", pt - uptime); } @@ -611,17 +504,6 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { * Return the number of physical CPU cores (hyper-thread CPUs count * is excluded). */ -#if (_WIN32_WINNT < 0x0601) // < Windows 7 (namely Vista and XP) -static PyObject * -psutil_cpu_count_phys(PyObject *self, PyObject *args) { - // Note: we may have used GetLogicalProcessorInformation() - // but I don't want to prolong support for Windows XP and Vista. - // On such old systems psutil will compile but this API will - // just return None. - psutil_debug("Win < 7; cpu_count_phys() forced to None"); - Py_RETURN_NONE; -} -#else // Windows >= 7 static PyObject * psutil_cpu_count_phys(PyObject *self, PyObject *args) { DWORD rc; @@ -636,13 +518,13 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { // it supports process groups, meaning this is able to report more // than 64 CPUs. See: // https://bugs.python.org/issue33166 - _GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( - "kernel32", "GetLogicalProcessorInformationEx"); - if (_GetLogicalProcessorInformationEx == NULL) - return NULL; + if (psutil_GetLogicalProcessorInformationEx == NULL) { + psutil_debug("Win < 7; cpu_count_phys() forced to None"); + Py_RETURN_NONE; + } while (1) { - rc = _GetLogicalProcessorInformationEx( + rc = psutil_GetLogicalProcessorInformationEx( RelationAll, buffer, &length); if (rc == FALSE) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { @@ -691,7 +573,6 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { free(buffer); Py_RETURN_NONE; } -#endif /* @@ -1044,10 +925,6 @@ psutil_cpu_times(PyObject *self, PyObject *args) { */ static PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { - // NtQuerySystemInformation stuff - typedef DWORD (_stdcall * NTQSI_PROC) (int, PVOID, ULONG, PULONG); - NTQSI_PROC NtQuerySystemInformation; - double idle, kernel, systemt, user, interrupt, dpc; NTSTATUS status; _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; @@ -1058,10 +935,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - NtQuerySystemInformation = \ - psutil_GetProcAddressFromLib("ntdll.dll", "NtQuerySystemInformation"); - if (NtQuerySystemInformation == NULL) - goto error; // retrieves number of processors ncpus = psutil_get_num_cpus(1); @@ -1078,7 +951,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } // gets cpu time informations - status = NtQuerySystemInformation( + status = psutil_NtQuerySystemInformation( SystemProcessorPerformanceInformation, sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), @@ -1537,10 +1410,6 @@ psutil_proc_username(PyObject *self, PyObject *args) { } -typedef DWORD (WINAPI * _GetExtendedTcpTable)(PVOID, PDWORD, BOOL, ULONG, - TCP_TABLE_CLASS, ULONG); - - // https://msdn.microsoft.com/library/aa365928.aspx static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, ULONG address_family, @@ -1575,10 +1444,6 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, } -typedef DWORD (WINAPI * _GetExtendedUdpTable)(PVOID, PDWORD, BOOL, ULONG, - UDP_TABLE_CLASS, ULONG); - - // https://msdn.microsoft.com/library/aa365930.aspx static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, ULONG address_family, @@ -1613,6 +1478,13 @@ static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, } +#define psutil_conn_decref_objs() \ + Py_DECREF(_AF_INET); \ + Py_DECREF(_AF_INET6);\ + Py_DECREF(_SOCK_STREAM);\ + Py_DECREF(_SOCK_DGRAM); + + /* * Return a list of network connections opened by a process */ @@ -1621,12 +1493,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { static long null_address[4] = { 0, 0, 0, 0 }; unsigned long pid; int pid_return; - typedef PSTR (NTAPI * _RtlIpv4AddressToStringA)(struct in_addr *, PSTR); - _RtlIpv4AddressToStringA rtlIpv4AddressToStringA; - typedef PSTR (NTAPI * _RtlIpv6AddressToStringA)(struct in6_addr *, PSTR); - _RtlIpv6AddressToStringA rtlIpv6AddressToStringA; - _GetExtendedTcpTable getExtendedTcpTable; - _GetExtendedUdpTable getExtendedUdpTable; PVOID table = NULL; DWORD tableSize; DWORD error; @@ -1650,28 +1516,11 @@ psutil_net_connections(PyObject *self, PyObject *args) { PyObject *_SOCK_DGRAM = PyLong_FromLong((long)SOCK_DGRAM); // Import some functions. - rtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv4AddressToStringA"); - if (rtlIpv4AddressToStringA == NULL) - goto error; - rtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv6AddressToStringA"); - if (rtlIpv6AddressToStringA == NULL) - goto error; - getExtendedTcpTable = psutil_GetProcAddressFromLib( - "iphlpapi.dll", "GetExtendedTcpTable"); - if (getExtendedTcpTable == NULL) - goto error; - getExtendedUdpTable = psutil_GetProcAddressFromLib( - "iphlpapi.dll", "GetExtendedUdpTable"); - if (getExtendedUdpTable == NULL) - goto error; - if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) goto error; if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { - _psutil_conn_decref_objs(); + psutil_conn_decref_objs(); PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); return NULL; } @@ -1679,25 +1528,18 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (pid != -1) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) { - _psutil_conn_decref_objs(); + psutil_conn_decref_objs(); return NoSuchProcess(""); } else if (pid_return == -1) { - _psutil_conn_decref_objs(); + psutil_conn_decref_objs(); return NULL; } } - if ((getExtendedTcpTable == NULL) || (getExtendedUdpTable == NULL)) { - PyErr_SetString(PyExc_NotImplementedError, - "feature not supported on this Windows version"); - _psutil_conn_decref_objs(); - return NULL; - } - py_retlist = PyList_New(0); if (py_retlist == NULL) { - _psutil_conn_decref_objs(); + psutil_conn_decref_objs(); return NULL; } @@ -1712,7 +1554,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedTcpTable(getExtendedTcpTable, + error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, AF_INET, &table, &tableSize); if (error == ERROR_NOT_ENOUGH_MEMORY) { PyErr_NoMemory(); @@ -1737,7 +1579,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in_addr addr; addr.S_un.S_addr = tcp4Table->table[i].dwLocalAddr; - rtlIpv4AddressToStringA(&addr, addressBufferLocal); + psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, @@ -1759,7 +1601,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in_addr addr; addr.S_un.S_addr = tcp4Table->table[i].dwRemoteAddr; - rtlIpv4AddressToStringA(&addr, addressBufferRemote); + psutil_rtlIpv4AddressToStringA(&addr, addressBufferRemote); py_addr_tuple_remote = Py_BuildValue( "(si)", addressBufferRemote, @@ -1809,7 +1651,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedTcpTable(getExtendedTcpTable, + error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, AF_INET6, &table, &tableSize); if (error == ERROR_NOT_ENOUGH_MEMORY) { PyErr_NoMemory(); @@ -1834,7 +1676,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in6_addr addr; memcpy(&addr, tcp6Table->table[i].ucLocalAddr, 16); - rtlIpv6AddressToStringA(&addr, addressBufferLocal); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, @@ -1857,7 +1699,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in6_addr addr; memcpy(&addr, tcp6Table->table[i].ucRemoteAddr, 16); - rtlIpv6AddressToStringA(&addr, addressBufferRemote); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferRemote); py_addr_tuple_remote = Py_BuildValue( "(si)", addressBufferRemote, @@ -1906,7 +1748,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedUdpTable(getExtendedUdpTable, + error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, AF_INET, &table, &tableSize); if (error == ERROR_NOT_ENOUGH_MEMORY) { PyErr_NoMemory(); @@ -1931,7 +1773,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in_addr addr; addr.S_un.S_addr = udp4Table->table[i].dwLocalAddr; - rtlIpv4AddressToStringA(&addr, addressBufferLocal); + psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, @@ -1980,7 +1822,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedUdpTable(getExtendedUdpTable, + error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, AF_INET6, &table, &tableSize); if (error == ERROR_NOT_ENOUGH_MEMORY) { PyErr_NoMemory(); @@ -2004,7 +1846,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in6_addr addr; memcpy(&addr, udp6Table->table[i].ucLocalAddr, 16); - rtlIpv6AddressToStringA(&addr, addressBufferLocal); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, @@ -2043,11 +1885,11 @@ psutil_net_connections(PyObject *self, PyObject *args) { tableSize = 0; } - _psutil_conn_decref_objs(); + psutil_conn_decref_objs(); return py_retlist; error: - _psutil_conn_decref_objs(); + psutil_conn_decref_objs(); Py_XDECREF(py_conn_tuple); Py_XDECREF(py_addr_tuple_local); Py_XDECREF(py_addr_tuple_remote); @@ -2123,19 +1965,13 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { long pid; HANDLE hProcess; DWORD IoPriority; - _NtQueryInformationProcess NtQueryInformationProcess; - NtQueryInformationProcess = \ - psutil_GetProcAddress("ntdll.dll", "NtQueryInformationProcess"); - if (NtQueryInformationProcess == NULL) - return NULL; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; - - NtQueryInformationProcess( + psutil_NtQueryInformationProcess( hProcess, ProcessIoPriority, &IoPriority, @@ -2156,19 +1992,14 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { DWORD prio; HANDLE hProcess; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - _NtSetInformationProcess NtSetInformationProcess; - NtSetInformationProcess = \ - psutil_GetProcAddress("ntdll.dll", "NtSetInformationProcess"); - if (NtSetInformationProcess == NULL) - return NULL; if (! PyArg_ParseTuple(args, "li", &pid, &prio)) return NULL; hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; - NtSetInformationProcess( + psutil_NtSetInformationProcess( hProcess, ProcessIoPriority, (PVOID)&prio, @@ -2461,7 +2292,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { */ static PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { - DISK_PERFORMANCE_WIN_2008 diskPerformance; + DISK_PERFORMANCE diskPerformance; DWORD dwSize; HANDLE hDevice = NULL; char szDevice[MAX_PATH]; @@ -2764,7 +2595,6 @@ psutil_users(PyObject *self, PyObject *args) { PWTS_CLIENT_ADDRESS address; char address_str[50]; long long unix_time; - PWINSTATIONQUERYINFORMATIONW WinStationQueryInformationW; WINSTATION_INFO station_info; ULONG returnLen; PyObject *py_tuple = NULL; @@ -2775,11 +2605,6 @@ psutil_users(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - WinStationQueryInformationW = psutil_GetProcAddressFromLib( - "winsta.dll", "WinStationQueryInformationW"); - if (WinStationQueryInformationW == NULL) - goto error; - if (WTSEnumerateSessions(hServer, 0, 1, &sessions, &count) == 0) { PyErr_SetFromWindowsErr(0); goto error; @@ -2833,12 +2658,13 @@ psutil_users(PyObject *self, PyObject *args) { } // login time - if (!WinStationQueryInformationW(hServer, - sessionId, - WinStationInformation, - &station_info, - sizeof(station_info), - &returnLen)) + if (! psutil_WinStationQueryInformationW( + hServer, + sessionId, + WinStationInformation, + &station_info, + sizeof(station_info), + &returnLen)) { goto error; } @@ -3478,8 +3304,6 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { */ static PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { - typedef DWORD (_stdcall * NTQSI_PROC) (int, PVOID, ULONG, PULONG); - NTQSI_PROC NtQuerySystemInformation; NTSTATUS status; _SYSTEM_PERFORMANCE_INFORMATION *spi = NULL; _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; @@ -3489,11 +3313,6 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ULONG64 dpcs = 0; ULONG interrupts = 0; - NtQuerySystemInformation = \ - psutil_GetProcAddressFromLib("ntdll.dll", "NtQuerySystemInformation"); - if (NtQuerySystemInformation == NULL) - return NULL; - // retrieves number of processors ncpus = psutil_get_num_cpus(1); if (ncpus == 0) @@ -3506,7 +3325,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { PyErr_NoMemory(); goto error; } - status = NtQuerySystemInformation( + status = psutil_NtQuerySystemInformation( SystemPerformanceInformation, spi, ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), @@ -3524,7 +3343,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { goto error; } - status = NtQuerySystemInformation( + status = psutil_NtQuerySystemInformation( SystemInterruptInformation, InterruptInformation, ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), @@ -3545,7 +3364,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { goto error; } - status = NtQuerySystemInformation( + status = psutil_NtQuerySystemInformation( SystemProcessorPerformanceInformation, sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), @@ -3939,6 +3758,8 @@ void init_psutil_windows(void) // set SeDebug for the current process psutil_set_se_debug(); psutil_setup(); + if (psutil_loadlibs() != 0) + return NULL; #if PY_MAJOR_VERSION >= 3 return module; diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c new file mode 100644 index 0000000000..bb9970fea8 --- /dev/null +++ b/psutil/arch/windows/global.c @@ -0,0 +1,129 @@ +/* + * 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. + */ + +#include +#include +#include "ntextapi.h" +#include "global.h" + + +// A wrapper around GetModuleHandle and GetProcAddress. +PVOID +psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { + HMODULE mod; + FARPROC addr; + + if ((mod = GetModuleHandleA(libname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, procname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, procname); + return NULL; + } + return addr; +} + + +// A wrapper around LoadLibrary and GetProcAddress. +PVOID +psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { + HMODULE mod; + FARPROC addr; + + Py_BEGIN_ALLOW_THREADS + mod = LoadLibraryA(libname); + Py_END_ALLOW_THREADS + if (mod == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, procname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, procname); + FreeLibrary(mod); + return NULL; + } + // Causes crash. + // FreeLibrary(mod); + return addr; +} + + +/* + * This is executed on import and loads Windows APIs so that they + * are available globally. + */ +psutil_loadlibs() { + /* + * Mandatory. + */ + + psutil_NtQuerySystemInformation = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtQuerySystemInformation"); + if (psutil_NtQuerySystemInformation == NULL) + return 1; + + psutil_NtQueryInformationProcess = psutil_GetProcAddress( + "ntdll.dll", "NtQueryInformationProcess"); + if (! psutil_NtQueryInformationProcess) + return 1; + + psutil_NtSetInformationProcess = psutil_GetProcAddress( + "ntdll.dll", "NtSetInformationProcess"); + if (! psutil_NtSetInformationProcess) + return 1; + + psutil_WinStationQueryInformationW = psutil_GetProcAddressFromLib( + "winsta.dll", "WinStationQueryInformationW"); + if (! psutil_WinStationQueryInformationW) + return 1; + + psutil_NtQueryObject = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtQueryObject"); + if (! psutil_NtQueryObject) + return 1; + + psutil_rtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv4AddressToStringA"); + if (! psutil_rtlIpv4AddressToStringA) + return 1; + + psutil_rtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv6AddressToStringA"); + if (! psutil_rtlIpv6AddressToStringA) + return 1; + + // minimum requirement: Win XP SP3 + psutil_GetExtendedTcpTable = psutil_GetProcAddressFromLib( + "iphlpapi.dll", "GetExtendedTcpTable"); + if (! psutil_GetExtendedTcpTable) + return 1; + + // minimum requirement: Win XP SP3 + psutil_GetExtendedUdpTable = psutil_GetProcAddressFromLib( + "iphlpapi.dll", "GetExtendedUdpTable"); + if (! psutil_GetExtendedUdpTable) + return 1; + + /* + * Optional. + */ + + // minimum requirement: Win Vista + psutil_GetTickCount64 = psutil_GetProcAddress( + "kernel32", "GetTickCount64"); + + // minimum requirement: Win 7 + psutil_GetActiveProcessorCount = psutil_GetProcAddress( + "kernel32", "GetActiveProcessorCount"); + + // minumum requirement: Win 7 + psutil_GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( + "kernel32", "GetLogicalProcessorInformationEx"); + + PyErr_Clear(); + return 0; +} diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h new file mode 100644 index 0000000000..8d8287765f --- /dev/null +++ b/psutil/arch/windows/global.h @@ -0,0 +1,11 @@ +/* + * 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. + */ + +#include + +int psutil_loadlibs(); +PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); +PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index ea23ddb729..e0006c7b91 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -6,7 +6,75 @@ #if !defined(__NTEXTAPI_H__) #define __NTEXTAPI_H__ #include +#include +typedef LONG NTSTATUS; + +#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004 +#define STATUS_BUFFER_TOO_SMALL 0xC0000023L +#define SystemExtendedHandleInformation 64 + +/* + * ================================================================ + * Enums. + * ================================================================ + */ + +typedef enum _PROCESSINFOCLASS2 { + _ProcessBasicInformation, + ProcessQuotaLimits, + ProcessIoCounters, + ProcessVmCounters, + ProcessTimes, + ProcessBasePriority, + ProcessRaisePriority, + _ProcessDebugPort, + ProcessExceptionPort, + ProcessAccessToken, + ProcessLdtInformation, + ProcessLdtSize, + ProcessDefaultHardErrorMode, + ProcessIoPortHandlers, + ProcessPooledUsageAndLimits, + ProcessWorkingSetWatch, + ProcessUserModeIOPL, + ProcessEnableAlignmentFaultFixup, + ProcessPriorityClass, + ProcessWx86Information, + ProcessHandleCount, + ProcessAffinityMask, + ProcessPriorityBoost, + ProcessDeviceMap, + ProcessSessionInformation, + ProcessForegroundInformation, + _ProcessWow64Information, + /* added after XP+ */ + _ProcessImageFileName, + ProcessLUIDDeviceMapsEnabled, + _ProcessBreakOnTermination, + ProcessDebugObjectHandle, + ProcessDebugFlags, + ProcessHandleTracing, + ProcessIoPriority, + ProcessExecuteFlags, + ProcessResourceManagement, + ProcessCookie, + ProcessImageInformation, + MaxProcessInfoClass +} PROCESSINFOCLASS2; + +#define PROCESSINFOCLASS PROCESSINFOCLASS2 +#define ProcessBasicInformation _ProcessBasicInformation +#define ProcessWow64Information _ProcessWow64Information +#define ProcessDebugPort _ProcessDebugPort +#define ProcessImageFileName _ProcessImageFileName +#define ProcessBreakOnTermination _ProcessBreakOnTermination + +/* + * ================================================================ + * Structs. + * ================================================================ + */ typedef struct { LARGE_INTEGER IdleTime; @@ -17,7 +85,6 @@ typedef struct { ULONG InterruptCount; } _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION; - typedef struct { LARGE_INTEGER IdleProcessTime; LARGE_INTEGER IoReadTransferCount; @@ -93,10 +160,8 @@ typedef struct { ULONG FirstLevelTbFills; ULONG SecondLevelTbFills; ULONG SystemCalls; - } _SYSTEM_PERFORMANCE_INFORMATION; - typedef struct { ULONG ContextSwitches; ULONG DpcCount; @@ -106,7 +171,6 @@ typedef struct { ULONG ApcBypassCount; } _SYSTEM_INTERRUPT_INFORMATION; - typedef enum _KTHREAD_STATE { Initialized, Ready, @@ -120,7 +184,6 @@ typedef enum _KTHREAD_STATE { MaximumThreadState } KTHREAD_STATE, *PKTHREAD_STATE; - typedef enum _KWAIT_REASON { Executive = 0, FreePage = 1, @@ -162,6 +225,22 @@ typedef enum _KWAIT_REASON { MaximumWaitReason = 37 } KWAIT_REASON, *PKWAIT_REASON; +typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { + PVOID Object; + HANDLE UniqueProcessId; + HANDLE HandleValue; + ULONG GrantedAccess; + USHORT CreatorBackTraceIndex; + USHORT ObjectTypeIndex; + ULONG HandleAttributes; + ULONG Reserved; +} SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX; + +typedef struct _SYSTEM_HANDLE_INFORMATION_EX { + ULONG_PTR NumberOfHandles; + ULONG_PTR Reserved; + SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1]; +} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX; typedef struct _CLIENT_ID2 { HANDLE UniqueProcess; @@ -190,8 +269,6 @@ typedef struct _SYSTEM_THREAD_INFORMATION2 { typedef struct _TEB *PTEB; - -// private typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION { SYSTEM_THREAD_INFORMATION ThreadInfo; PVOID StackBase; @@ -203,7 +280,6 @@ typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION { ULONG_PTR Reserved4; } SYSTEM_EXTENDED_THREAD_INFORMATION, *PSYSTEM_EXTENDED_THREAD_INFORMATION; - typedef struct _SYSTEM_PROCESS_INFORMATION2 { ULONG NextEntryOffset; ULONG NumberOfThreads; @@ -244,10 +320,35 @@ typedef struct _SYSTEM_PROCESS_INFORMATION2 { #define SYSTEM_PROCESS_INFORMATION SYSTEM_PROCESS_INFORMATION2 #define PSYSTEM_PROCESS_INFORMATION PSYSTEM_PROCESS_INFORMATION2 - -// ================================================ -// psutil.users() support -// ================================================ +typedef struct _PROCESSOR_POWER_INFORMATION { + ULONG Number; + ULONG MaxMhz; + ULONG CurrentMhz; + ULONG MhzLimit; + ULONG MaxIdleState; + ULONG CurrentIdleState; +} PROCESSOR_POWER_INFORMATION, *PPROCESSOR_POWER_INFORMATION; + +#ifndef __IPHLPAPI_H__ +typedef struct in6_addr { + union { + UCHAR Byte[16]; + USHORT Word[8]; + } u; +} IN6_ADDR, *PIN6_ADDR, FAR *LPIN6_ADDR; +#endif + +// http://msdn.microsoft.com/en-us/library/aa813741(VS.85).aspx +typedef struct { + BYTE Reserved1[16]; + PVOID Reserved2[5]; + UNICODE_STRING CurrentDirectoryPath; + PVOID CurrentDirectoryHandle; + UNICODE_STRING DllPath; + UNICODE_STRING ImagePathName; + UNICODE_STRING CommandLine; + LPCWSTR env; +} RTL_USER_PROCESS_PARAMETERS_, *PRTL_USER_PROCESS_PARAMETERS_; typedef struct _WINSTATION_INFO { BYTE Reserved1[72]; @@ -261,18 +362,38 @@ typedef struct _WINSTATION_INFO { FILETIME CurrentTime; } WINSTATION_INFO, *PWINSTATION_INFO; - -typedef BOOLEAN (WINAPI * PWINSTATIONQUERYINFORMATIONW) - (HANDLE,ULONG,WINSTATIONINFOCLASS,PVOID,ULONG,PULONG); - +#if (_WIN32_WINNT < 0x0601) // Windows < 7 (Vista and XP) +typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX { + LOGICAL_PROCESSOR_RELATIONSHIP Relationship; + DWORD Size; + _ANONYMOUS_UNION + union { + PROCESSOR_RELATIONSHIP Processor; + NUMA_NODE_RELATIONSHIP NumaNode; + CACHE_RELATIONSHIP Cache; + GROUP_RELATIONSHIP Group; + } DUMMYUNIONNAME; +} SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX; +#endif /* - * NtQueryInformationProcess code taken from - * http://wj32.wordpress.com/2009/01/24/howto-get-the-command-line-of-processes/ - * typedefs needed to compile against ntdll functions not exposted in the API + * ================================================================ + * Type defs for modules loaded at runtime. + * ================================================================ */ -typedef LONG NTSTATUS; +typedef BOOL (WINAPI *_GetLogicalProcessorInformationEx)( + LOGICAL_PROCESSOR_RELATIONSHIP relationship, + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Buffer, + PDWORD ReturnLength); + +typedef BOOLEAN (WINAPI * _WinStationQueryInformationW)( + HANDLE ServerHandle, + ULONG SessionId, + WINSTATIONINFOCLASS WinStationInformationClass, + PVOID pWinStationInformation, + ULONG WinStationInformationLength, + PULONG pReturnLength); typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)( HANDLE ProcessHandle, @@ -282,6 +403,12 @@ typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)( PDWORD ReturnLength ); +typedef NTSTATUS (NTAPI *_NtQuerySystemInformation)( + ULONG SystemInformationClass, + PVOID SystemInformation, + ULONG SystemInformationLength, + PULONG ReturnLength +); typedef NTSTATUS (NTAPI *_NtSetInformationProcess)( HANDLE ProcessHandle, @@ -290,56 +417,86 @@ typedef NTSTATUS (NTAPI *_NtSetInformationProcess)( DWORD ProcessInformationLength ); +typedef PSTR (NTAPI * _RtlIpv4AddressToStringA)( + struct in_addr *Addr, + PSTR S); + +typedef PSTR (NTAPI * _RtlIpv6AddressToStringA)( + struct in6_addr *Addr, + PSTR P); + +typedef DWORD (WINAPI * _GetExtendedTcpTable)( + PVOID pTcpTable, + PDWORD pdwSize, + BOOL bOrder, + ULONG ulAf, + TCP_TABLE_CLASS TableClass, + ULONG Reserved +); -typedef enum _PROCESSINFOCLASS2 { - _ProcessBasicInformation, - ProcessQuotaLimits, - ProcessIoCounters, - ProcessVmCounters, - ProcessTimes, - ProcessBasePriority, - ProcessRaisePriority, - _ProcessDebugPort, - ProcessExceptionPort, - ProcessAccessToken, - ProcessLdtInformation, - ProcessLdtSize, - ProcessDefaultHardErrorMode, - ProcessIoPortHandlers, - ProcessPooledUsageAndLimits, - ProcessWorkingSetWatch, - ProcessUserModeIOPL, - ProcessEnableAlignmentFaultFixup, - ProcessPriorityClass, - ProcessWx86Information, - ProcessHandleCount, - ProcessAffinityMask, - ProcessPriorityBoost, - ProcessDeviceMap, - ProcessSessionInformation, - ProcessForegroundInformation, - _ProcessWow64Information, - /* added after XP+ */ - _ProcessImageFileName, - ProcessLUIDDeviceMapsEnabled, - _ProcessBreakOnTermination, - ProcessDebugObjectHandle, - ProcessDebugFlags, - ProcessHandleTracing, - ProcessIoPriority, - ProcessExecuteFlags, - ProcessResourceManagement, - ProcessCookie, - ProcessImageInformation, - MaxProcessInfoClass -} PROCESSINFOCLASS2; +typedef DWORD (WINAPI * _GetExtendedUdpTable)( + PVOID pUdpTable, + PDWORD pdwSize, + BOOL bOrder, + ULONG ulAf, + UDP_TABLE_CLASS TableClass, + ULONG Reserved +); +typedef DWORD (CALLBACK *_GetActiveProcessorCount)( + WORD GroupNumber); -#define PROCESSINFOCLASS PROCESSINFOCLASS2 -#define ProcessBasicInformation _ProcessBasicInformation -#define ProcessWow64Information _ProcessWow64Information -#define ProcessDebugPort _ProcessDebugPort -#define ProcessImageFileName _ProcessImageFileName -#define ProcessBreakOnTermination _ProcessBreakOnTermination +typedef ULONGLONG (CALLBACK *_GetTickCount64)( + void); + +typedef NTSTATUS (NTAPI *_NtQueryObject)( + HANDLE Handle, + OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, + ULONG ObjectInformationLength, + PULONG ReturnLength +); + +/* + * ================================================================ + * Custom psutil definitions for modules loaded at runtime. + * ================================================================ + */ + +_NtQuerySystemInformation \ + psutil_NtQuerySystemInformation; + +_NtQueryInformationProcess \ + psutil_NtQueryInformationProcess; + +_NtSetInformationProcess + psutil_NtSetInformationProcess; + +_WinStationQueryInformationW \ + psutil_WinStationQueryInformationW; + +_RtlIpv4AddressToStringA \ + psutil_rtlIpv4AddressToStringA; + +_RtlIpv6AddressToStringA \ + psutil_rtlIpv6AddressToStringA; + +_GetExtendedTcpTable \ + psutil_GetExtendedTcpTable; + +_GetExtendedUdpTable \ + psutil_GetExtendedUdpTable; + +_GetActiveProcessorCount \ + psutil_GetActiveProcessorCount; + +_GetTickCount64 \ + psutil_GetTickCount64; + +_NtQueryObject \ + psutil_NtQueryObject; + +_GetLogicalProcessorInformationEx \ + psutil_GetLogicalProcessorInformationEx; #endif // __NTEXTAPI_H__ diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 4ac02b9109..f295f18d16 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -4,13 +4,16 @@ * found in the LICENSE file. * */ + +#include +#include +#include +#include "ntextapi.h" +#include "global.h" #include "process_handles.h" #include "process_info.h" #include "../../_psutil_common.h" -static _NtQuerySystemInformation __NtQuerySystemInformation = NULL; -static _NtQueryObject __NtQueryObject = NULL; - CRITICAL_SECTION g_cs; BOOL g_initialized = FALSE; NTSTATUS g_status; @@ -23,33 +26,15 @@ ULONG g_dwSize = 0; ULONG g_dwLength = 0; -PyObject * -psutil_get_open_files(long dwPid, HANDLE hProcess) { - OSVERSIONINFO osvi; - - ZeroMemory(&osvi, sizeof(OSVERSIONINFO)); - osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); - GetVersionEx(&osvi); - - // Threaded version only works for Vista+ - if (osvi.dwMajorVersion >= 6) - return psutil_get_open_files_ntqueryobject(dwPid, hProcess); - else - return psutil_get_open_files_getmappedfilename(dwPid, hProcess); -} +#define ObjectNameInformation 1 +#define NTQO_TIMEOUT 100 -VOID +static VOID psutil_get_open_files_init(BOOL threaded) { if (g_initialized == TRUE) return; - // Resolve the Windows API calls - __NtQuerySystemInformation = psutil_GetProcAddressFromLib( - "ntdll.dll", "NtQuerySystemInformation"); - __NtQueryObject = psutil_GetProcAddressFromLib( - "ntdll.dll", "NtQueryObject"); - // Create events for signalling work between threads if (threaded == TRUE) { g_hEvtStart = CreateEvent(NULL, FALSE, FALSE, NULL); @@ -61,7 +46,59 @@ psutil_get_open_files_init(BOOL threaded) { } -PyObject * +static DWORD WINAPI +psutil_wait_thread(LPVOID lpvParam) { + // Loop infinitely waiting for work + while (TRUE) { + WaitForSingleObject(g_hEvtStart, INFINITE); + + g_status = psutil_NtQueryObject( + g_hFile, + ObjectNameInformation, + g_pNameBuffer, + g_dwSize, + &g_dwLength); + SetEvent(g_hEvtFinish); + } +} + + +static DWORD +psutil_create_thread() { + DWORD dwWait = 0; + + if (g_hThread == NULL) + g_hThread = CreateThread( + NULL, + 0, + psutil_wait_thread, + NULL, + 0, + NULL); + if (g_hThread == NULL) + return GetLastError(); + + // Signal the worker thread to start + SetEvent(g_hEvtStart); + + // Wait for the worker thread to finish + dwWait = WaitForSingleObject(g_hEvtFinish, NTQO_TIMEOUT); + + // If the thread hangs, kill it and cleanup + if (dwWait == WAIT_TIMEOUT) { + SuspendThread(g_hThread); + TerminateThread(g_hThread, 1); + WaitForSingleObject(g_hThread, INFINITE); + CloseHandle(g_hThread); + + g_hThread = NULL; + } + + return dwWait; +} + + +static PyObject * psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { NTSTATUS status; PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL; @@ -81,9 +118,7 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { // to psutil_get_open_files() is running EnterCriticalSection(&g_cs); - if (__NtQuerySystemInformation == NULL || - __NtQueryObject == NULL || - g_hEvtStart == NULL || + if (g_hEvtStart == NULL || g_hEvtFinish == NULL) { @@ -117,7 +152,7 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { error = TRUE; goto cleanup; } - } while ((status = __NtQuerySystemInformation( + } while ((status = psutil_NtQuerySystemInformation( SystemExtendedHandleInformation, pHandleInfo, dwInfoSize, @@ -180,7 +215,7 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { goto loop_cleanup; } - dwWait = psutil_NtQueryObject(); + dwWait = psutil_create_thread(); // If the call does not return, skip this handle if (dwWait != WAIT_OBJECT_0) @@ -228,19 +263,19 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { } loop_cleanup: - Py_XDECREF(py_path); - py_path = NULL; + Py_XDECREF(py_path); + py_path = NULL; - if (g_pNameBuffer != NULL) - HeapFree(GetProcessHeap(), 0, g_pNameBuffer); - g_pNameBuffer = NULL; - g_dwSize = 0; - g_dwLength = 0; + if (g_pNameBuffer != NULL) + HeapFree(GetProcessHeap(), 0, g_pNameBuffer); + g_pNameBuffer = NULL; + g_dwSize = 0; + g_dwLength = 0; - if (g_hFile != NULL) - CloseHandle(g_hFile); - g_hFile = NULL; - } + if (g_hFile != NULL) + CloseHandle(g_hFile); + g_hFile = NULL; +} cleanup: if (g_pNameBuffer != NULL) @@ -268,58 +303,7 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { } -DWORD -psutil_NtQueryObject() { - DWORD dwWait = 0; - - if (g_hThread == NULL) - g_hThread = CreateThread( - NULL, - 0, - psutil_NtQueryObjectThread, - NULL, - 0, - NULL); - if (g_hThread == NULL) - return GetLastError(); - - // Signal the worker thread to start - SetEvent(g_hEvtStart); - - // Wait for the worker thread to finish - dwWait = WaitForSingleObject(g_hEvtFinish, NTQO_TIMEOUT); - - // If the thread hangs, kill it and cleanup - if (dwWait == WAIT_TIMEOUT) { - SuspendThread(g_hThread); - TerminateThread(g_hThread, 1); - WaitForSingleObject(g_hThread, INFINITE); - CloseHandle(g_hThread); - - g_hThread = NULL; - } - - return dwWait; -} - - -DWORD WINAPI -psutil_NtQueryObjectThread(LPVOID lpvParam) { - // Loop infinitely waiting for work - while (TRUE) { - WaitForSingleObject(g_hEvtStart, INFINITE); - - g_status = __NtQueryObject(g_hFile, - ObjectNameInformation, - g_pNameBuffer, - g_dwSize, - &g_dwLength); - SetEvent(g_hEvtFinish); - } -} - - -PyObject * +static PyObject * psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { NTSTATUS status; PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL; @@ -339,12 +323,6 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { if (g_initialized == FALSE) psutil_get_open_files_init(FALSE); - if (__NtQuerySystemInformation == NULL || __NtQueryObject == NULL) { - PyErr_SetFromWindowsErr(0); - error = TRUE; - goto cleanup; - } - // Py_BuildValue raises an exception if NULL is returned py_retlist = PyList_New(0); if (py_retlist == NULL) { @@ -370,7 +348,7 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { error = TRUE; goto cleanup; } - } while ((status = __NtQuerySystemInformation( + } while ((status = psutil_NtQuerySystemInformation( SystemExtendedHandleInformation, pHandleInfo, dwInfoSize, @@ -517,3 +495,23 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { return py_retlist; } + + +/* + * The public function. + */ +PyObject * +psutil_get_open_files(long dwPid, HANDLE hProcess) { + OSVERSIONINFO osvi; + + ZeroMemory(&osvi, sizeof(OSVERSIONINFO)); + osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); + GetVersionEx(&osvi); + + // Threaded version only works for Vista+ + if (osvi.dwMajorVersion >= 6) + return psutil_get_open_files_ntqueryobject(dwPid, hProcess); + else + return psutil_get_open_files_getmappedfilename(dwPid, hProcess); +} + diff --git a/psutil/arch/windows/process_handles.h b/psutil/arch/windows/process_handles.h index 4a022c1c12..342ce8fd26 100644 --- a/psutil/arch/windows/process_handles.h +++ b/psutil/arch/windows/process_handles.h @@ -4,108 +4,7 @@ * found in the LICENSE file. */ -#ifndef __PROCESS_HANDLES_H__ -#define __PROCESS_HANDLES_H__ - -#ifndef UNICODE -#define UNICODE -#endif - #include -#include #include -#include -#include -#include - - -#ifndef NT_SUCCESS -#define NT_SUCCESS(x) ((x) >= 0) -#endif - -#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004 -#define ObjectBasicInformation 0 -#define ObjectNameInformation 1 -#define ObjectTypeInformation 2 -#define HANDLE_TYPE_FILE 28 -#define NTQO_TIMEOUT 100 - -typedef NTSTATUS (NTAPI *_NtQuerySystemInformation)( - ULONG SystemInformationClass, - PVOID SystemInformation, - ULONG SystemInformationLength, - PULONG ReturnLength -); - -typedef NTSTATUS (NTAPI *_NtQueryObject)( - HANDLE ObjectHandle, - ULONG ObjectInformationClass, - PVOID ObjectInformation, - ULONG ObjectInformationLength, - PULONG ReturnLength -); -// Undocumented FILE_INFORMATION_CLASS: FileNameInformation -static const SYSTEM_INFORMATION_CLASS SystemExtendedHandleInformation = (SYSTEM_INFORMATION_CLASS)64; - -typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { - PVOID Object; - HANDLE UniqueProcessId; - HANDLE HandleValue; - ULONG GrantedAccess; - USHORT CreatorBackTraceIndex; - USHORT ObjectTypeIndex; - ULONG HandleAttributes; - ULONG Reserved; -} SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX; - -typedef struct _SYSTEM_HANDLE_INFORMATION_EX { - ULONG_PTR NumberOfHandles; - ULONG_PTR Reserved; - SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1]; -} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX; - -typedef enum _POOL_TYPE { - NonPagedPool, - PagedPool, - NonPagedPoolMustSucceed, - DontUseThisType, - NonPagedPoolCacheAligned, - PagedPoolCacheAligned, - NonPagedPoolCacheAlignedMustS -} POOL_TYPE, *PPOOL_TYPE; - -typedef struct _OBJECT_TYPE_INFORMATION { - UNICODE_STRING Name; - ULONG TotalNumberOfObjects; - ULONG TotalNumberOfHandles; - ULONG TotalPagedPoolUsage; - ULONG TotalNonPagedPoolUsage; - ULONG TotalNamePoolUsage; - ULONG TotalHandleTableUsage; - ULONG HighWaterNumberOfObjects; - ULONG HighWaterNumberOfHandles; - ULONG HighWaterPagedPoolUsage; - ULONG HighWaterNonPagedPoolUsage; - ULONG HighWaterNamePoolUsage; - ULONG HighWaterHandleTableUsage; - ULONG InvalidAttributes; - GENERIC_MAPPING GenericMapping; - ULONG ValidAccess; - BOOLEAN SecurityRequired; - BOOLEAN MaintainHandleCount; - USHORT MaintainTypeList; - POOL_TYPE PoolType; - ULONG PagedPoolUsage; - ULONG NonPagedPoolUsage; -} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION; - -PVOID GetLibraryProcAddress(PSTR LibraryName, PSTR ProcName); -VOID psutil_get_open_files_init(BOOL threaded); PyObject* psutil_get_open_files(long pid, HANDLE processHandle); -PyObject* psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess); -PyObject* psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess); -DWORD psutil_NtQueryObject(void); -DWORD WINAPI psutil_NtQueryObjectThread(LPVOID lpvParam); - -#endif // __PROCESS_HANDLES_H__ diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index a2edca58d2..9a698e4245 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -12,9 +12,10 @@ #include #include +#include "ntextapi.h" +#include "global.h" #include "security.h" #include "process_info.h" -#include "ntextapi.h" #include "../../_psutil_common.h" @@ -24,23 +25,6 @@ // but unfortunately not in a usable way. // ==================================================================== -// see http://msdn2.microsoft.com/en-us/library/aa489609.aspx -#ifndef NT_SUCCESS -#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0) -#endif - -// http://msdn.microsoft.com/en-us/library/aa813741(VS.85).aspx -typedef struct { - BYTE Reserved1[16]; - PVOID Reserved2[5]; - UNICODE_STRING CurrentDirectoryPath; - PVOID CurrentDirectoryHandle; - UNICODE_STRING DllPath; - UNICODE_STRING ImagePathName; - UNICODE_STRING CommandLine; - LPCWSTR env; -} RTL_USER_PROCESS_PARAMETERS_, *PRTL_USER_PROCESS_PARAMETERS_; - // https://msdn.microsoft.com/en-us/library/aa813706(v=vs.85).aspx #ifdef _WIN64 typedef struct { @@ -97,15 +81,11 @@ typedef struct { use the 64 bit structure layout and a special function to read its memory. */ typedef NTSTATUS (NTAPI *_NtWow64ReadVirtualMemory64)( - IN HANDLE ProcessHandle, - IN PVOID64 BaseAddress, - OUT PVOID Buffer, - IN ULONG64 Size, - OUT PULONG64 NumberOfBytesRead); - -typedef enum { - MemoryInformationBasic -} MEMORY_INFORMATION_CLASS; + HANDLE ProcessHandle, + PVOID64 BaseAddress, + PVOID Buffer, + ULONG64 Size, + PULONG64 NumberOfBytesRead); typedef struct { PVOID Reserved1[2]; @@ -150,47 +130,6 @@ typedef struct { (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) -const int STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; -const int STATUS_BUFFER_TOO_SMALL = 0xC0000023L; - - -// A wrapper around GetModuleHandle and GetProcAddress. -PVOID -psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { - HMODULE mod; - FARPROC addr; - - if ((mod = GetModuleHandleA(libname)) == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, libname); - return NULL; - } - if ((addr = GetProcAddress(mod, procname)) == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, procname); - return NULL; - } - return addr; -} - - -// A wrapper around LoadLibrary and GetProcAddress. -PVOID -psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { - HMODULE mod; - FARPROC addr; - - if ((mod = LoadLibraryA(libname)) == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, libname); - return NULL; - } - if ((addr = GetProcAddress(mod, procname)) == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, procname); - FreeLibrary(mod); - return NULL; - } - FreeLibrary(mod); - return addr; -} - // ==================================================================== // Process and PIDs utiilties. @@ -535,11 +474,6 @@ psutil_get_process_data(long pid, #endif DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; - NtQueryInformationProcess = \ - psutil_GetProcAddress("ntdll.dll", "NtQueryInformationProcess"); - if (NtQueryInformationProcess == NULL) - return -1; - hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return -1; @@ -547,11 +481,13 @@ psutil_get_process_data(long pid, #ifdef _WIN64 /* 64 bit case. Check if the target is a 32 bit process running in WoW64 * mode. */ - if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, - ProcessWow64Information, - &ppeb32, - sizeof(LPVOID), - NULL))) { + if (! NT_SUCCESS(psutil_NtQueryInformationProcess( + hProcess, + ProcessWow64Information, + &ppeb32, + sizeof(LPVOID), + NULL))) + { PyErr_SetFromWindowsErr(0); goto error; } @@ -610,7 +546,7 @@ psutil_get_process_data(long pid, psutil_GetProcAddressFromLib( "ntdll.dll", "NtWow64QueryInformationProcess64"); if (NtWow64QueryInformationProcess64 == NULL) { - // Too complicated. Give up. + PyErr_Clear(); AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); goto error; } @@ -620,38 +556,44 @@ psutil_get_process_data(long pid, psutil_GetProcAddressFromLib( "ntdll.dll", "NtWow64ReadVirtualMemory64"); if (NtWow64ReadVirtualMemory64 == NULL) { - // Too complicated. Give up. + PyErr_Clear(); AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); goto error; } } - if (!NT_SUCCESS(NtWow64QueryInformationProcess64( - hProcess, - ProcessBasicInformation, - &pbi64, - sizeof(pbi64), - NULL))) { + if (! NT_SUCCESS( + NtWow64QueryInformationProcess64( + hProcess, + ProcessBasicInformation, + &pbi64, + sizeof(pbi64), + NULL))) + { PyErr_SetFromWindowsErr(0); goto error; } // read peb - if (!NT_SUCCESS(NtWow64ReadVirtualMemory64(hProcess, - pbi64.PebBaseAddress, - &peb64, - sizeof(peb64), - NULL))) { + if (! NT_SUCCESS(NtWow64ReadVirtualMemory64( + hProcess, + pbi64.PebBaseAddress, + &peb64, + sizeof(peb64), + NULL))) + { PyErr_SetFromWindowsErr(0); goto error; } // read process parameters - if (!NT_SUCCESS(NtWow64ReadVirtualMemory64(hProcess, - peb64.ProcessParameters, - &procParameters64, - sizeof(procParameters64), - NULL))) { + if (! NT_SUCCESS(NtWow64ReadVirtualMemory64( + hProcess, + peb64.ProcessParameters, + &procParameters64, + sizeof(procParameters64), + NULL))) + { PyErr_SetFromWindowsErr(0); goto error; } @@ -671,18 +613,19 @@ psutil_get_process_data(long pid, } } else #endif - /* Target process is of the same bitness as us. */ { PROCESS_BASIC_INFORMATION pbi; PEB_ peb; RTL_USER_PROCESS_PARAMETERS_ procParameters; - if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, - ProcessBasicInformation, - &pbi, - sizeof(pbi), - NULL))) { + if (! NT_SUCCESS(psutil_NtQueryInformationProcess( + hProcess, + ProcessBasicInformation, + &pbi, + sizeof(pbi), + NULL))) + { PyErr_SetFromWindowsErr(0); goto error; } @@ -743,11 +686,13 @@ psutil_get_process_data(long pid, #ifndef _WIN64 if (weAreWow64 && !theyAreWow64) { - if (!NT_SUCCESS(NtWow64ReadVirtualMemory64(hProcess, - src64, - buffer, - size, - NULL))) { + if (! NT_SUCCESS(NtWow64ReadVirtualMemory64( + hProcess, + src64, + buffer, + size, + NULL))) + { PyErr_SetFromWindowsErr(0); goto error; } @@ -788,12 +733,6 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { WCHAR * cmdline_buffer_wchar = NULL; PUNICODE_STRING tmp = NULL; DWORD string_size; - _NtQueryInformationProcess NtQueryInformationProcess; - - NtQueryInformationProcess = \ - psutil_GetProcAddress("ntdll.dll", "NtQueryInformationProcess"); - if (NtQueryInformationProcess == NULL) - goto error; cmdline_buffer = calloc(ret_length, 1); if (cmdline_buffer == NULL) { @@ -804,14 +743,14 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) goto error; - status = NtQueryInformationProcess( + status = psutil_NtQueryInformationProcess( hProcess, 60, // ProcessCommandLineInformation cmdline_buffer, ret_length, &ret_length ); - if (!NT_SUCCESS(status)) { + if (! NT_SUCCESS(status)) { PyErr_SetFromWindowsErr(0); goto error; } @@ -971,13 +910,6 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID buffer; ULONG bufferSize; PSYSTEM_PROCESS_INFORMATION process; - typedef DWORD (_stdcall * NTQSI_PROC) (int, PVOID, ULONG, PULONG); - NTQSI_PROC NtQuerySystemInformation; - - NtQuerySystemInformation = \ - psutil_GetProcAddressFromLib("ntdll.dll", "NtQuerySystemInformation"); - if (NtQuerySystemInformation == NULL) - goto error; bufferSize = initialBufferSize; buffer = malloc(bufferSize); @@ -987,8 +919,11 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, } while (TRUE) { - status = NtQuerySystemInformation(SystemProcessInformation, buffer, - bufferSize, &bufferSize); + status = psutil_NtQuerySystemInformation( + SystemProcessInformation, + buffer, + bufferSize, + &bufferSize); if (status == STATUS_BUFFER_TOO_SMALL || status == STATUS_INFO_LENGTH_MISMATCH) { diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index d2e60b7c5c..4278c4df9e 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -23,8 +23,6 @@ int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, int psutil_assert_pid_exists(DWORD pid, char *err); int psutil_assert_pid_not_exists(DWORD pid, char *err); -PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); -PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); PyObject* psutil_get_cmdline(long pid, int use_peb); PyObject* psutil_get_cwd(long pid); diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 7be12d9c52..c98d892cfa 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -778,10 +778,11 @@ def test_environ_32(self): self.assertEquals(e["THINK_OF_A_NUMBER"], str(os.getpid())) def test_environ_64(self): - # Environ 32 is not supported. p = psutil.Process(self.proc64.pid) - with self.assertRaises(psutil.AccessDenied): + try: p.environ() + except psutil.AccessDenied: + pass # =================================================================== diff --git a/setup.py b/setup.py index e285323696..764f8b31d3 100755 --- a/setup.py +++ b/setup.py @@ -134,6 +134,7 @@ def get_winver(): 'psutil/arch/windows/security.c', 'psutil/arch/windows/inet_ntop.c', 'psutil/arch/windows/services.c', + 'psutil/arch/windows/global.c', ], define_macros=macros, libraries=[ From b29f0620f14ecfc1419387de04e283015d96ae31 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 21 Feb 2019 10:41:17 -0800 Subject: [PATCH 0147/1714] set Windows version constants globally; get rid of deprecated GetVersionEx --- psutil/_psutil_windows.c | 2 +- psutil/arch/windows/global.c | 60 ++++++++++++++++++++++++--- psutil/arch/windows/global.h | 12 +++++- psutil/arch/windows/ntextapi.h | 23 +++++----- psutil/arch/windows/process_handles.c | 9 +--- 5 files changed, 79 insertions(+), 27 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 3908884e18..c0d736457b 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3758,7 +3758,7 @@ void init_psutil_windows(void) // set SeDebug for the current process psutil_set_se_debug(); psutil_setup(); - if (psutil_loadlibs() != 0) + if (psutil_load_globals() != 0) return NULL; #if PY_MAJOR_VERSION >= 3 diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index bb9970fea8..da3bdc6e4f 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -2,6 +2,10 @@ * 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. + * + * This code is executed on import. It loads private/undocumented + * Windows APIs and sets Windows version constants so that they are + * available globally. */ #include @@ -10,6 +14,9 @@ #include "global.h" +// Needed to make this globally visible. +int PSUTIL_WINVER; + // A wrapper around GetModuleHandle and GetProcAddress. PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { @@ -52,15 +59,11 @@ psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { } -/* - * This is executed on import and loads Windows APIs so that they - * are available globally. - */ +static int psutil_loadlibs() { /* * Mandatory. */ - psutil_NtQuerySystemInformation = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQuerySystemInformation"); if (psutil_NtQuerySystemInformation == NULL) @@ -108,10 +111,14 @@ psutil_loadlibs() { if (! psutil_GetExtendedUdpTable) return 1; + psutil_RtlGetVersion = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlGetVersion"); + if (! psutil_RtlGetVersion) + return 1; + /* * Optional. */ - // minimum requirement: Win Vista psutil_GetTickCount64 = psutil_GetProcAddress( "kernel32", "GetTickCount64"); @@ -127,3 +134,44 @@ psutil_loadlibs() { PyErr_Clear(); return 0; } + + +static int +psutil_set_winver() { + RTL_OSVERSIONINFOEXW versionInfo; + ULONG maj; + ULONG min; + + versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW); + memset(&versionInfo, 0, sizeof(RTL_OSVERSIONINFOEXW)); + psutil_RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); + maj = versionInfo.dwMajorVersion; + min = versionInfo.dwMinorVersion; + if (maj == 5 && min == 1) + PSUTIL_WINVER = PSUTIL_WINDOWS_XP; + else if (maj == 5 && min == 2) + PSUTIL_WINVER = PSUTIL_WINDOWS_SERVER_2003; + else if (maj == 6 && min == 0) + PSUTIL_WINVER = PSUTIL_WINDOWS_VISTA; // or Server 2008 + else if (maj == 6 && min == 1) + PSUTIL_WINVER = PSUTIL_WINDOWS_7; + else if (maj == 6 && min == 2) + PSUTIL_WINVER = PSUTIL_WINDOWS_8; + else if (maj == 6 && min == 3) + PSUTIL_WINVER = PSUTIL_WINDOWS_8_1; + else if (maj == 10 && min == 0) + PSUTIL_WINVER = PSUTIL_WINDOWS_10; + else + PSUTIL_WINVER = PSUTIL_WINDOWS_NEW; + return 0; +} + + +int +psutil_load_globals() { + if (psutil_loadlibs() != 0) + return 1; + if (psutil_set_winver() != 0) + return 1; + return 0; +} diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 8d8287765f..378026dbae 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -6,6 +6,16 @@ #include -int psutil_loadlibs(); +extern int PSUTIL_WINVER; +#define PSUTIL_WINDOWS_XP 51 +#define PSUTIL_WINDOWS_SERVER_2003 52 +#define PSUTIL_WINDOWS_VISTA 60 +#define PSUTIL_WINDOWS_7 61 +#define PSUTIL_WINDOWS_8 62 +#define PSUTIL_WINDOWS_8_1 63 +#define PSUTIL_WINDOWS_10 100 +#define PSUTIL_WINDOWS_NEW MAXLONG + +int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index e0006c7b91..84c5a2e1b4 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -400,22 +400,19 @@ typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)( DWORD ProcessInformationClass, PVOID ProcessInformation, DWORD ProcessInformationLength, - PDWORD ReturnLength -); + PDWORD ReturnLength); typedef NTSTATUS (NTAPI *_NtQuerySystemInformation)( ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, - PULONG ReturnLength -); + PULONG ReturnLength); typedef NTSTATUS (NTAPI *_NtSetInformationProcess)( HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, - DWORD ProcessInformationLength -); + DWORD ProcessInformationLength); typedef PSTR (NTAPI * _RtlIpv4AddressToStringA)( struct in_addr *Addr, @@ -431,8 +428,7 @@ typedef DWORD (WINAPI * _GetExtendedTcpTable)( BOOL bOrder, ULONG ulAf, TCP_TABLE_CLASS TableClass, - ULONG Reserved -); + ULONG Reserved); typedef DWORD (WINAPI * _GetExtendedUdpTable)( PVOID pUdpTable, @@ -440,8 +436,7 @@ typedef DWORD (WINAPI * _GetExtendedUdpTable)( BOOL bOrder, ULONG ulAf, UDP_TABLE_CLASS TableClass, - ULONG Reserved -); + ULONG Reserved); typedef DWORD (CALLBACK *_GetActiveProcessorCount)( WORD GroupNumber); @@ -454,7 +449,10 @@ typedef NTSTATUS (NTAPI *_NtQueryObject)( OBJECT_INFORMATION_CLASS ObjectInformationClass, PVOID ObjectInformation, ULONG ObjectInformationLength, - PULONG ReturnLength + PULONG ReturnLength); + +typedef NTSTATUS (WINAPI *_RtlGetVersion) ( + PRTL_OSVERSIONINFOW lpVersionInformation ); /* @@ -499,4 +497,7 @@ _NtQueryObject \ _GetLogicalProcessorInformationEx \ psutil_GetLogicalProcessorInformationEx; +_RtlGetVersion \ + psutil_RtlGetVersion; + #endif // __NTEXTAPI_H__ diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index f295f18d16..6f133ef978 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -502,16 +502,9 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { */ PyObject * psutil_get_open_files(long dwPid, HANDLE hProcess) { - OSVERSIONINFO osvi; - - ZeroMemory(&osvi, sizeof(OSVERSIONINFO)); - osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); - GetVersionEx(&osvi); - // Threaded version only works for Vista+ - if (osvi.dwMajorVersion >= 6) + if (PSUTIL_WINVER >= PSUTIL_WINDOWS_VISTA) return psutil_get_open_files_ntqueryobject(dwPid, hProcess); else return psutil_get_open_files_getmappedfilename(dwPid, hProcess); } - From 4beeca825cf6c2a796088c8a61a3c8378d7985c4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 21 Feb 2019 10:48:15 -0800 Subject: [PATCH 0148/1714] move custom object defs from ntextapi.h to global.h --- psutil/arch/windows/global.h | 42 +++++++++++++++++++++++++++++++ psutil/arch/windows/ntextapi.h | 45 ---------------------------------- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 378026dbae..7bf8b5cad6 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -2,9 +2,12 @@ * 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. + + * List of constants and objects that are globally available. */ #include +#include "ntextapi.h" extern int PSUTIL_WINVER; #define PSUTIL_WINDOWS_XP 51 @@ -19,3 +22,42 @@ extern int PSUTIL_WINVER; int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); + +_NtQuerySystemInformation \ + psutil_NtQuerySystemInformation; + +_NtQueryInformationProcess \ + psutil_NtQueryInformationProcess; + +_NtSetInformationProcess + psutil_NtSetInformationProcess; + +_WinStationQueryInformationW \ + psutil_WinStationQueryInformationW; + +_RtlIpv4AddressToStringA \ + psutil_rtlIpv4AddressToStringA; + +_RtlIpv6AddressToStringA \ + psutil_rtlIpv6AddressToStringA; + +_GetExtendedTcpTable \ + psutil_GetExtendedTcpTable; + +_GetExtendedUdpTable \ + psutil_GetExtendedUdpTable; + +_GetActiveProcessorCount \ + psutil_GetActiveProcessorCount; + +_GetTickCount64 \ + psutil_GetTickCount64; + +_NtQueryObject \ + psutil_NtQueryObject; + +_GetLogicalProcessorInformationEx \ + psutil_GetLogicalProcessorInformationEx; + +_RtlGetVersion \ + psutil_RtlGetVersion; diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 84c5a2e1b4..d6030caa26 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -455,49 +455,4 @@ typedef NTSTATUS (WINAPI *_RtlGetVersion) ( PRTL_OSVERSIONINFOW lpVersionInformation ); -/* - * ================================================================ - * Custom psutil definitions for modules loaded at runtime. - * ================================================================ - */ - -_NtQuerySystemInformation \ - psutil_NtQuerySystemInformation; - -_NtQueryInformationProcess \ - psutil_NtQueryInformationProcess; - -_NtSetInformationProcess - psutil_NtSetInformationProcess; - -_WinStationQueryInformationW \ - psutil_WinStationQueryInformationW; - -_RtlIpv4AddressToStringA \ - psutil_rtlIpv4AddressToStringA; - -_RtlIpv6AddressToStringA \ - psutil_rtlIpv6AddressToStringA; - -_GetExtendedTcpTable \ - psutil_GetExtendedTcpTable; - -_GetExtendedUdpTable \ - psutil_GetExtendedUdpTable; - -_GetActiveProcessorCount \ - psutil_GetActiveProcessorCount; - -_GetTickCount64 \ - psutil_GetTickCount64; - -_NtQueryObject \ - psutil_NtQueryObject; - -_GetLogicalProcessorInformationEx \ - psutil_GetLogicalProcessorInformationEx; - -_RtlGetVersion \ - psutil_RtlGetVersion; - #endif // __NTEXTAPI_H__ From 9eb7498412289e2f2a3ed7b9326d17ab74d4ee12 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 21 Feb 2019 11:21:56 -0800 Subject: [PATCH 0149/1714] fix windows test --- psutil/tests/__init__.py | 7 +++++-- psutil/tests/test_connections.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index c6fc8cc23e..b1c3c7b397 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -321,8 +321,11 @@ def get_test_subprocess(cmd=None, **kwds): kwds.setdefault("cwd", os.getcwd()) kwds.setdefault("env", os.environ) if WINDOWS: - # Prevents the subprocess to open error dialogs. - kwds.setdefault("creationflags", 0x8000000) # CREATE_NO_WINDOW + # 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;" \ diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 7f59a74cbd..c97fc409ec 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -474,7 +474,7 @@ def test_multi_sockets_procs(self): import time, os from psutil.tests import create_sockets with create_sockets(): - with open('%s', 'w') as f: + with open(r'%s', 'w') as f: f.write(str(os.getpid())) time.sleep(60) """ % fname) From 3e61671deb33d0ad8bdcd6c162921c58a40ff759 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 21 Feb 2019 11:42:49 -0800 Subject: [PATCH 0150/1714] remove deprecated WSAAddressToStringA causing a compiler warning and use UNICODE version instead --- psutil/arch/windows/inet_ntop.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/psutil/arch/windows/inet_ntop.c b/psutil/arch/windows/inet_ntop.c index 4b6c1dfec2..f36bac2d99 100644 --- a/psutil/arch/windows/inet_ntop.c +++ b/psutil/arch/windows/inet_ntop.c @@ -9,10 +9,7 @@ // From: https://memset.wordpress.com/2010/10/09/inet_ntop-for-win32/ PCSTR WSAAPI -inet_ntop(__in INT family, - __in PVOID pAddr, - __out_ecount(StringBufSize) PSTR pStringBuf, - __in size_t StringBufSize) { +inet_ntop(INT family, PVOID pAddr, PSTR stringBuf, size_t strBufSize) { DWORD dwAddressLength = 0; struct sockaddr_storage srcaddr; struct sockaddr_in *srcaddr4 = (struct sockaddr_in*) &srcaddr; @@ -34,13 +31,15 @@ inet_ntop(__in INT family, return NULL; } - if (WSAAddressToString((LPSOCKADDR) &srcaddr, - dwAddressLength, - 0, - pStringBuf, - (LPDWORD) &StringBufSize) != 0) { + if (WSAAddressToStringW( + (LPSOCKADDR) &srcaddr, + dwAddressLength, + 0, + (LPWSTR) stringBuf, + (LPDWORD) &strBufSize) != 0) + { PyErr_SetExcFromWindowsErr(PyExc_OSError, WSAGetLastError()); return NULL; } - return pStringBuf; + return stringBuf; } From a828c38532b146c1f62fee2b05a6f89c85314fdb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 21 Feb 2019 12:33:55 -0800 Subject: [PATCH 0151/1714] revert previous change --- psutil/arch/windows/inet_ntop.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/arch/windows/inet_ntop.c b/psutil/arch/windows/inet_ntop.c index f36bac2d99..3db507b9ba 100644 --- a/psutil/arch/windows/inet_ntop.c +++ b/psutil/arch/windows/inet_ntop.c @@ -31,11 +31,11 @@ inet_ntop(INT family, PVOID pAddr, PSTR stringBuf, size_t strBufSize) { return NULL; } - if (WSAAddressToStringW( + if (WSAAddressToStringA( (LPSOCKADDR) &srcaddr, dwAddressLength, 0, - (LPWSTR) stringBuf, + stringBuf, (LPDWORD) &strBufSize) != 0) { PyErr_SetExcFromWindowsErr(PyExc_OSError, WSAGetLastError()); From 05a7ad481f56ec280729d4f403ebee261097e824 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Feb 2019 01:01:06 -0800 Subject: [PATCH 0152/1714] fix compilation warning --- psutil/arch/windows/inet_ntop.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/arch/windows/inet_ntop.h b/psutil/arch/windows/inet_ntop.h index 0dbf28bfa4..2d86c26cf1 100644 --- a/psutil/arch/windows/inet_ntop.h +++ b/psutil/arch/windows/inet_ntop.h @@ -4,6 +4,8 @@ * found in the LICENSE file. */ +// because of WSAAddressToStringA +#define _WINSOCK_DEPRECATED_NO_WARNINGS #include PCSTR WSAAPI From 2e239aa325aa25999be95b8bf2c59614aefe1bf9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Feb 2019 14:06:47 +0100 Subject: [PATCH 0153/1714] fix #1427: [OSX] Process cmdline() and environ() may erroneously raise OSError on failed malloc(). --- HISTORY.rst | 2 ++ psutil/arch/osx/process_info.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b4f4888d52..1f7847061e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,8 @@ XXXX-XX-XX in order to prevent that. - 1419_: [Windows] Process.environ() raises NotImplementedError when querying a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. +- 1427_: [OSX] Process cmdline() and environ() may erroneously raise OSError + on failed malloc(). **Enhancements** diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 40c79a2cdc..e5ecbaed8d 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -132,7 +132,7 @@ psutil_get_cmdline(long pid) { procargs = (char *)malloc(argmax); if (NULL == procargs) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_NoMemory(); goto error; } @@ -226,7 +226,7 @@ psutil_get_environ(long pid) { procargs = (char *)malloc(argmax); if (NULL == procargs) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_NoMemory(); goto error; } From 8c4d4c21d08a573a25cbbe3d6c3e2ec20008e215 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Feb 2019 07:21:21 -0800 Subject: [PATCH 0154/1714] win connections refactoring --- psutil/_psutil_windows.c | 428 +++++++++++++++++++-------------------- 1 file changed, 207 insertions(+), 221 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index c0d736457b..82be938b0d 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1561,79 +1561,75 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } - if (error == NO_ERROR) - { - tcp4Table = table; + if (error != NO_ERROR) { + PyErr_SetFromWindowsErr(error); + goto error; + } - for (i = 0; i < tcp4Table->dwNumEntries; i++) - { - if (pid != -1) { - if (tcp4Table->table[i].dwOwningPid != pid) { - continue; - } + tcp4Table = table; + for (i = 0; i < tcp4Table->dwNumEntries; i++) { + if (pid != -1) { + if (tcp4Table->table[i].dwOwningPid != pid) { + continue; } + } - if (tcp4Table->table[i].dwLocalAddr != 0 || - tcp4Table->table[i].dwLocalPort != 0) - { - struct in_addr addr; - - addr.S_un.S_addr = tcp4Table->table[i].dwLocalAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(tcp4Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); - } + if (tcp4Table->table[i].dwLocalAddr != 0 || + tcp4Table->table[i].dwLocalPort != 0) + { + struct in_addr addr; + + addr.S_un.S_addr = tcp4Table->table[i].dwLocalAddr; + psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(tcp4Table->table[i].dwLocalPort)); + } + else { + py_addr_tuple_local = PyTuple_New(0); + } - if (py_addr_tuple_local == NULL) - goto error; + if (py_addr_tuple_local == NULL) + goto error; - // On Windows <= XP, remote addr is filled even if socket - // is in LISTEN mode in which case we just ignore it. - if ((tcp4Table->table[i].dwRemoteAddr != 0 || - tcp4Table->table[i].dwRemotePort != 0) && - (tcp4Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) - { - struct in_addr addr; - - addr.S_un.S_addr = tcp4Table->table[i].dwRemoteAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferRemote); - py_addr_tuple_remote = Py_BuildValue( - "(si)", - addressBufferRemote, - BYTESWAP_USHORT(tcp4Table->table[i].dwRemotePort)); - } - else - { - py_addr_tuple_remote = PyTuple_New(0); - } + // On Windows <= XP, remote addr is filled even if socket + // is in LISTEN mode in which case we just ignore it. + if ((tcp4Table->table[i].dwRemoteAddr != 0 || + tcp4Table->table[i].dwRemotePort != 0) && + (tcp4Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) + { + struct in_addr addr; + + addr.S_un.S_addr = tcp4Table->table[i].dwRemoteAddr; + psutil_rtlIpv4AddressToStringA(&addr, addressBufferRemote); + py_addr_tuple_remote = Py_BuildValue( + "(si)", + addressBufferRemote, + BYTESWAP_USHORT(tcp4Table->table[i].dwRemotePort)); + } + else + { + py_addr_tuple_remote = PyTuple_New(0); + } - if (py_addr_tuple_remote == NULL) - goto error; + if (py_addr_tuple_remote == NULL) + goto error; - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_STREAM, - py_addr_tuple_local, - py_addr_tuple_remote, - tcp4Table->table[i].dwState, - tcp4Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_DECREF(py_conn_tuple); - } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET, + SOCK_STREAM, + py_addr_tuple_local, + py_addr_tuple_remote, + tcp4Table->table[i].dwState, + tcp4Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_DECREF(py_conn_tuple); } free(table); @@ -1657,80 +1653,76 @@ psutil_net_connections(PyObject *self, PyObject *args) { PyErr_NoMemory(); goto error; } + if (error != NO_ERROR) { + PyErr_SetFromWindowsErr(error); + goto error; + } - if (error == NO_ERROR) + tcp6Table = table; + for (i = 0; i < tcp6Table->dwNumEntries; i++) { - tcp6Table = table; - - for (i = 0; i < tcp6Table->dwNumEntries; i++) - { - if (pid != -1) { - if (tcp6Table->table[i].dwOwningPid != pid) { - continue; - } + if (pid != -1) { + if (tcp6Table->table[i].dwOwningPid != pid) { + continue; } + } - if (memcmp(tcp6Table->table[i].ucLocalAddr, null_address, 16) - != 0 || tcp6Table->table[i].dwLocalPort != 0) - { - struct in6_addr addr; - - memcpy(&addr, tcp6Table->table[i].ucLocalAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(tcp6Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); - } + if (memcmp(tcp6Table->table[i].ucLocalAddr, null_address, 16) + != 0 || tcp6Table->table[i].dwLocalPort != 0) + { + struct in6_addr addr; + + memcpy(&addr, tcp6Table->table[i].ucLocalAddr, 16); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(tcp6Table->table[i].dwLocalPort)); + } + else { + py_addr_tuple_local = PyTuple_New(0); + } - if (py_addr_tuple_local == NULL) - goto error; + if (py_addr_tuple_local == NULL) + goto error; - // On Windows <= XP, remote addr is filled even if socket - // is in LISTEN mode in which case we just ignore it. - if ((memcmp(tcp6Table->table[i].ucRemoteAddr, null_address, 16) - != 0 || - tcp6Table->table[i].dwRemotePort != 0) && - (tcp6Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) - { - struct in6_addr addr; - - memcpy(&addr, tcp6Table->table[i].ucRemoteAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferRemote); - py_addr_tuple_remote = Py_BuildValue( - "(si)", - addressBufferRemote, - BYTESWAP_USHORT(tcp6Table->table[i].dwRemotePort)); - } - else { - py_addr_tuple_remote = PyTuple_New(0); - } + // On Windows <= XP, remote addr is filled even if socket + // is in LISTEN mode in which case we just ignore it. + if ((memcmp(tcp6Table->table[i].ucRemoteAddr, null_address, 16) + != 0 || + tcp6Table->table[i].dwRemotePort != 0) && + (tcp6Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) + { + struct in6_addr addr; + + memcpy(&addr, tcp6Table->table[i].ucRemoteAddr, 16); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferRemote); + py_addr_tuple_remote = Py_BuildValue( + "(si)", + addressBufferRemote, + BYTESWAP_USHORT(tcp6Table->table[i].dwRemotePort)); + } + else { + py_addr_tuple_remote = PyTuple_New(0); + } - if (py_addr_tuple_remote == NULL) - goto error; + if (py_addr_tuple_remote == NULL) + goto error; - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_STREAM, - py_addr_tuple_local, - py_addr_tuple_remote, - tcp6Table->table[i].dwState, - tcp6Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_DECREF(py_conn_tuple); - } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_STREAM, + py_addr_tuple_local, + py_addr_tuple_remote, + tcp6Table->table[i].dwState, + tcp6Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_DECREF(py_conn_tuple); } free(table); @@ -1754,57 +1746,53 @@ psutil_net_connections(PyObject *self, PyObject *args) { PyErr_NoMemory(); goto error; } + if (error != NO_ERROR) { + PyErr_SetFromWindowsErr(error); + goto error; + } - if (error == NO_ERROR) + udp4Table = table; + for (i = 0; i < udp4Table->dwNumEntries; i++) { - udp4Table = table; - - for (i = 0; i < udp4Table->dwNumEntries; i++) - { - if (pid != -1) { - if (udp4Table->table[i].dwOwningPid != pid) { - continue; - } + if (pid != -1) { + if (udp4Table->table[i].dwOwningPid != pid) { + continue; } + } - if (udp4Table->table[i].dwLocalAddr != 0 || - udp4Table->table[i].dwLocalPort != 0) - { - struct in_addr addr; - - addr.S_un.S_addr = udp4Table->table[i].dwLocalAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(udp4Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); - } + if (udp4Table->table[i].dwLocalAddr != 0 || + udp4Table->table[i].dwLocalPort != 0) + { + struct in_addr addr; + + addr.S_un.S_addr = udp4Table->table[i].dwLocalAddr; + psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(udp4Table->table[i].dwLocalPort)); + } + else { + py_addr_tuple_local = PyTuple_New(0); + } - if (py_addr_tuple_local == NULL) - goto error; + if (py_addr_tuple_local == NULL) + goto error; - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_DGRAM, - py_addr_tuple_local, - PyTuple_New(0), - PSUTIL_CONN_NONE, - udp4Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_DECREF(py_conn_tuple); - } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET, + SOCK_DGRAM, + py_addr_tuple_local, + PyTuple_New(0), + PSUTIL_CONN_NONE, + udp4Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_DECREF(py_conn_tuple); } free(table); @@ -1829,55 +1817,53 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } - if (error == NO_ERROR) - { - udp6Table = table; + if (error != NO_ERROR) { + PyErr_SetFromWindowsErr(error); + goto error; + } - for (i = 0; i < udp6Table->dwNumEntries; i++) { - if (pid != -1) { - if (udp6Table->table[i].dwOwningPid != pid) { - continue; - } - } + udp6Table = table; - if (memcmp(udp6Table->table[i].ucLocalAddr, null_address, 16) - != 0 || udp6Table->table[i].dwLocalPort != 0) - { - struct in6_addr addr; - - memcpy(&addr, udp6Table->table[i].ucLocalAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(udp6Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); + for (i = 0; i < udp6Table->dwNumEntries; i++) { + if (pid != -1) { + if (udp6Table->table[i].dwOwningPid != pid) { + continue; } + } - if (py_addr_tuple_local == NULL) - goto error; - - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_DGRAM, - py_addr_tuple_local, - PyTuple_New(0), - PSUTIL_CONN_NONE, - udp6Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_DECREF(py_conn_tuple); + if (memcmp(udp6Table->table[i].ucLocalAddr, null_address, 16) + != 0 || udp6Table->table[i].dwLocalPort != 0) + { + struct in6_addr addr; + + memcpy(&addr, udp6Table->table[i].ucLocalAddr, 16); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(udp6Table->table[i].dwLocalPort)); } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; + else { + py_addr_tuple_local = PyTuple_New(0); + } + + if (py_addr_tuple_local == NULL) + goto error; + + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_DGRAM, + py_addr_tuple_local, + PyTuple_New(0), + PSUTIL_CONN_NONE, + udp6Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_DECREF(py_conn_tuple); } free(table); From a96bbdf2e84c737f2b80f5a9af9ac0ca46b93526 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Feb 2019 07:25:54 -0800 Subject: [PATCH 0155/1714] win connections refactoring --- psutil/_psutil_windows.c | 50 +++++++++++----------------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 82be938b0d..bcdc3e0b5a 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1474,7 +1474,16 @@ static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, *data = NULL; } } - return error; + + if (error == ERROR_NOT_ENOUGH_MEMORY) { + PyErr_NoMemory(); + return 1; + } + if (error != NO_ERROR) { + PyErr_SetFromWindowsErr(error); + return 1; + } + return 0; } @@ -1556,16 +1565,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, AF_INET, &table, &tableSize); - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); + if (error != 0) goto error; - } - - if (error != NO_ERROR) { - PyErr_SetFromWindowsErr(error); - goto error; - } - tcp4Table = table; for (i = 0; i < tcp4Table->dwNumEntries; i++) { if (pid != -1) { @@ -1649,15 +1650,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, AF_INET6, &table, &tableSize); - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - goto error; - } - if (error != NO_ERROR) { - PyErr_SetFromWindowsErr(error); + if (error != 0) goto error; - } - tcp6Table = table; for (i = 0; i < tcp6Table->dwNumEntries; i++) { @@ -1742,15 +1736,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { tableSize = 0; error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, AF_INET, &table, &tableSize); - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - goto error; - } - if (error != NO_ERROR) { - PyErr_SetFromWindowsErr(error); + if (error != 0) goto error; - } - udp4Table = table; for (i = 0; i < udp4Table->dwNumEntries; i++) { @@ -1812,18 +1799,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { tableSize = 0; error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, AF_INET6, &table, &tableSize); - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - goto error; - } - - if (error != NO_ERROR) { - PyErr_SetFromWindowsErr(error); + if (error != 0) goto error; - } - udp6Table = table; - for (i = 0; i < udp6Table->dwNumEntries; i++) { if (pid != -1) { if (udp6Table->table[i].dwOwningPid != pid) { From 88beee9e0d169e979a3027bf9e61c59ccc1ad6e3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Feb 2019 17:04:02 +0100 Subject: [PATCH 0156/1714] fix #1424: workaround for subprocess bug on Windows returnin ERROR_INVALID_HANDLE on terminate() --- HISTORY.rst | 10 +++++----- psutil/tests/__init__.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1f7847061e..c34bc78067 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,11 @@ XXXX-XX-XX +**Enhancements** + +- 1420_: [Windows] in case of exception disk_usage() now also shows the path + name. + **Bug fixes** - 1394_: [Windows] Process name() and exe() may erronously return "Registry". @@ -15,11 +20,6 @@ XXXX-XX-XX - 1427_: [OSX] Process cmdline() and environ() may erroneously raise OSError on failed malloc(). -**Enhancements** - -- 1420_: [Windows] in case of exception disk_usage() now also shows the path - name. - 5.5.1 ===== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b1c3c7b397..fac9083beb 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -495,7 +495,7 @@ def assert_gone(pid): try: subp.terminate() except OSError as err: - if WINDOWS and err.errno == 6: # "invalid handle" + if WINDOWS and err.winerror == 6: # "invalid handle" pass elif err.errno != errno.ESRCH: raise From 7a2572268168e96c8841ca83ab1a89735ec02c3a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Feb 2019 09:55:52 -0800 Subject: [PATCH 0157/1714] #1428 in case of error, show the C syscall which caused it --- psutil/_psutil_bsd.c | 2 +- psutil/_psutil_common.c | 27 +++++- psutil/_psutil_common.h | 1 + psutil/_psutil_linux.c | 7 +- psutil/_psutil_osx.c | 61 +++++++------ psutil/_psutil_windows.c | 54 +++++------ psutil/arch/freebsd/specific.c | 138 ++++++++++++++++++----------- psutil/arch/osx/process_info.c | 37 +++++--- psutil/arch/windows/process_info.c | 48 +++++----- psutil/arch/windows/services.c | 22 ++--- psutil/tests/test_system.py | 4 +- 11 files changed, 239 insertions(+), 162 deletions(-) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index dce157f559..073ff02d94 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -804,7 +804,7 @@ psutil_users(PyObject *self, PyObject *args) { fp = fopen(_PATH_UTMP, "r"); if (fp == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromErrnoWithFilename(PyExc_OSError, _PATH_UTMP); goto error; } diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index db1a046012..a4899487b8 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -7,8 +7,9 @@ */ #include -#include - +#ifdef _WIN32 +#include +#endif // Global vars. int PSUTIL_DEBUG = 0; @@ -50,6 +51,28 @@ NoSuchProcess(const char *msg) { } +/* + * Same as PyErr_SetFromErrno(0) but adds the syscall to the exception + * message. + */ +PyObject * +PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { + char fullmsg[1024]; + +#ifdef _WIN32 + sprintf(fullmsg, "originated from %s", syscall); + PyErr_SetFromWindowsErrWithFilename(GetLastError(), fullmsg); +#else + PyObject *exc; + sprintf(fullmsg, "%s (originated from %s)", strerror(errno), syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", errno, fullmsg); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); +#endif + return NULL; +} + + /* * Set OSError(errno=EACCES, strerror="Permission denied") Python exception. * If msg != "" the exception message will change in accordance. diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index e107166a12..70698107a1 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -22,6 +22,7 @@ PyObject* PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size); PyObject* AccessDenied(const char *msg); PyObject* NoSuchProcess(const char *msg); +PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 5b7a56ad96..be808633d3 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -535,7 +535,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) - goto error; + return PyErr_SetFromOSErrnoWithSyscall("socket()"); strncpy(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // duplex and speed @@ -558,20 +558,21 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { speed = 0; } else { + PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCETHTOOL)"); goto error; } } - close(sock); py_retlist = Py_BuildValue("[ii]", duplex, speed); if (!py_retlist) goto error; + close(sock); return py_retlist; error: if (sock != -1) close(sock); - return PyErr_SetFromErrno(PyExc_OSError); + return NULL; } diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index be08de552f..1b98cc5860 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -141,31 +141,22 @@ psutil_pids(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (psutil_get_proc_list(&proclist, &num_processes) != 0) { - if (errno != 0) { - PyErr_SetFromErrno(PyExc_OSError); - } - else { - PyErr_SetString(PyExc_RuntimeError, - "failed to retrieve process list"); - } + if (psutil_get_proc_list(&proclist, &num_processes) != 0) goto error; - } - if (num_processes > 0) { - // save the address of proclist so we can free it later - orig_address = proclist; - for (idx = 0; idx < num_processes; idx++) { - py_pid = Py_BuildValue("i", proclist->kp_proc.p_pid); - if (! py_pid) - goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_DECREF(py_pid); - proclist++; - } - free(orig_address); + // save the address of proclist so we can free it later + orig_address = proclist; + for (idx = 0; idx < num_processes; idx++) { + py_pid = Py_BuildValue("i", proclist->kp_proc.p_pid); + if (! py_pid) + goto error; + if (PyList_Append(py_retlist, py_pid)) + goto error; + Py_DECREF(py_pid); + proclist++; } + free(orig_address); + return py_retlist; error: @@ -622,8 +613,10 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { return NULL; len = sizeof(cpu_type); - if (sysctlbyname("sysctl.proc_cputype", &cpu_type, &len, NULL, 0) != 0) - return PyErr_SetFromErrno(PyExc_OSError); + if (sysctlbyname("sysctl.proc_cputype", &cpu_type, &len, NULL, 0) != 0) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('sysctl.proc_cputype')"); + } // Roughly based on libtop_update_vm_regions in // http://www.opensource.apple.com/source/top/top-100.1.2/libtop.c @@ -863,12 +856,18 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { int64_t max; size_t size = sizeof(int64_t); - if (sysctlbyname("hw.cpufrequency", &curr, &size, NULL, 0)) - return PyErr_SetFromErrno(PyExc_OSError); - if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) - return PyErr_SetFromErrno(PyExc_OSError); - if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) - return PyErr_SetFromErrno(PyExc_OSError); + if (sysctlbyname("hw.cpufrequency", &curr, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('hw.cpufrequency')"); + } + if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('hw.cpufrequency_min')"); + } + if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('hw.cpufrequency_max')"); + } return Py_BuildValue( "KKK", @@ -1370,7 +1369,7 @@ psutil_proc_connections(PyObject *self, PyObject *args) { // check for inet_ntop failures if (errno != 0) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); goto error; } diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index bcdc3e0b5a..7dc37ee171 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -286,7 +286,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { err = GetLastError(); // See: https://github.com/giampaolo/psutil/issues/1099 if (err != ERROR_ACCESS_DENIED) { - PyErr_SetFromWindowsErr(err); + PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); CloseHandle(hProcess); return NULL; } @@ -332,7 +332,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // handle return code if (retVal == WAIT_FAILED) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); CloseHandle(hProcess); return NULL; } @@ -354,7 +354,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // process is gone so we can get its process exit code. The PID // may still stick around though but we'll handle that from Python. if (GetExitCodeProcess(hProcess, &ExitCode) == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); CloseHandle(hProcess); return NULL; } @@ -652,7 +652,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { #if (_WIN32_WINNT >= 0x0600) // Windows >= Vista memset(exe, 0, MAX_PATH); if (QueryFullProcessImageNameW(hProcess, 0, exe, &size) == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("QueryFullProcessImageNameW"); CloseHandle(hProcess); return NULL; } @@ -662,7 +662,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (GetLastError() == 0) PyErr_SetFromWindowsErr(ERROR_ACCESS_DENIED); else - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("GetProcessImageFileNameW"); CloseHandle(hProcess); return NULL; } @@ -689,11 +689,11 @@ psutil_proc_name(PyObject *self, PyObject *args) { return NULL; hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, pid); if (hSnapShot == INVALID_HANDLE_VALUE) - return PyErr_SetFromWindowsErr(0); + return PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); pentry.dwSize = sizeof(PROCESSENTRY32W); ok = Process32FirstW(hSnapShot, &pentry); if (! ok) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("Process32FirstW"); CloseHandle(hSnapShot); return NULL; } @@ -944,7 +944,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // allocates an array of _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION // structures, one per processor sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); if (sppi == NULL) { PyErr_NoMemory(); goto error; @@ -1046,7 +1046,7 @@ psutil_proc_suspend_or_resume(DWORD pid, int suspend) { hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThreadSnap == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); return FALSE; } @@ -1054,7 +1054,7 @@ psutil_proc_suspend_or_resume(DWORD pid, int suspend) { te32.dwSize = sizeof(THREADENTRY32); if (! Thread32First(hThreadSnap, &te32)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("Thread32First"); CloseHandle(hThreadSnap); return FALSE; } @@ -1067,14 +1067,14 @@ psutil_proc_suspend_or_resume(DWORD pid, int suspend) { hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te32.th32ThreadID); if (hThread == NULL) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("OpenThread"); CloseHandle(hThread); CloseHandle(hThreadSnap); return FALSE; } if (suspend == 1) { if (SuspendThread(hThread) == (DWORD) - 1) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("SuspendThread"); CloseHandle(hThread); CloseHandle(hThreadSnap); return FALSE; @@ -1082,7 +1082,7 @@ psutil_proc_suspend_or_resume(DWORD pid, int suspend) { } else { if (ResumeThread(hThread) == (DWORD) - 1) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("ResumeThread"); CloseHandle(hThread); CloseHandle(hThreadSnap); return FALSE; @@ -1156,7 +1156,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThreadSnap == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); goto error; } @@ -1164,7 +1164,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { te32.dwSize = sizeof(THREADENTRY32); if (! Thread32First(hThreadSnap, &te32)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("Thread32First"); goto error; } @@ -1184,7 +1184,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { rc = GetThreadTimes(hThread, &ftDummy, &ftDummy, &ftKernel, &ftUser); if (rc == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("GetThreadTimes"); goto error; } @@ -1311,7 +1311,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { return NULL; if (!OpenProcessToken(processHandle, TOKEN_QUERY, &tokenHandle)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); goto error; } @@ -1334,7 +1334,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { continue; } else { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("GetTokenInformation"); goto error; } } @@ -1367,7 +1367,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { continue; } else { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("LookupAccountSidW"); goto error; } } @@ -2570,7 +2570,7 @@ psutil_users(PyObject *self, PyObject *args) { return NULL; if (WTSEnumerateSessions(hServer, 0, 1, &sessions, &count) == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessions"); goto error; } @@ -2590,7 +2590,7 @@ psutil_users(PyObject *self, PyObject *args) { bytes = 0; if (WTSQuerySessionInformationW(hServer, sessionId, WTSUserName, &buffer_user, &bytes) == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); goto error; } if (bytes <= 2) @@ -2600,7 +2600,7 @@ psutil_users(PyObject *self, PyObject *args) { bytes = 0; if (WTSQuerySessionInformation(hServer, sessionId, WTSClientAddress, &buffer_addr, &bytes) == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformation"); goto error; } @@ -2630,6 +2630,7 @@ psutil_users(PyObject *self, PyObject *args) { sizeof(station_info), &returnLen)) { + PyErr_SetFromOSErrnoWithSyscall("WinStationQueryInformationW"); goto error; } @@ -3295,7 +3296,8 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), NULL); if (status != 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "NtQuerySystemInformation(SYSTEM_PERFORMANCE_INFORMATION)"); goto error; } @@ -3313,7 +3315,8 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), NULL); if (status != 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "NtQuerySystemInformation(SYSTEM_INTERRUPT_INFORMATION)"); goto error; } for (i = 0; i < ncpus; i++) { @@ -3334,7 +3337,8 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), NULL); if (status != 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "NtQuerySystemInformation(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)"); goto error; } diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 93bf51ff23..26b802422d 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -54,7 +54,7 @@ psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc) { size = sizeof(struct kinfo_proc); if (sysctl((int *)mib, 4, proc, &size, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_PID)"); return -1; } @@ -209,7 +209,7 @@ static char size = argmax; if (sysctl(mib, 4, procargs, &size, NULL, 0) == -1) { free(procargs); - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGS)"); return NULL; } @@ -287,10 +287,13 @@ psutil_proc_exe(PyObject *self, PyObject *args) { error = sysctl(mib, 4, pathname, &size, NULL, 0); if (error == -1) { // see: https://github.com/giampaolo/psutil/issues/907 - if (errno == ENOENT) + if (errno == ENOENT) { return PyUnicode_DecodeFSDefault(""); - else - return PyErr_SetFromErrno(PyExc_OSError); + } + else { + return \ + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_PATHNAME)"); + } } if (size == 0 || strlen(pathname) == 0) { ret = psutil_pid_exists(pid); @@ -350,7 +353,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { size = 0; error = sysctl(mib, 4, NULL, &size, NULL, 0); if (error == -1) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); goto error; } if (size == 0) { @@ -366,7 +369,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { error = sysctl(mib, 4, kip, &size, NULL, 0); if (error == -1) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); goto error; } if (size == 0) { @@ -447,26 +450,38 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { #endif size_t buffers_size = sizeof(buffers); - if (sysctlbyname("hw.physmem", &total, &size, NULL, 0)) - goto error; - if (sysctlbyname("vm.stats.vm.v_active_count", &active, &size, NULL, 0)) - goto error; - if (sysctlbyname("vm.stats.vm.v_inactive_count", - &inactive, &size, NULL, 0)) - goto error; - if (sysctlbyname("vm.stats.vm.v_wire_count", &wired, &size, NULL, 0)) - goto error; + if (sysctlbyname("hw.physmem", &total, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('hw.physmem')"); + } + if (sysctlbyname("vm.stats.vm.v_active_count", &active, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_active_count')"); + } + if (sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, &size, NULL, 0)) + { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_inactive_count')"); + } + if (sysctlbyname("vm.stats.vm.v_wire_count", &wired, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_wire_count')"); + } // https://github.com/giampaolo/psutil/issues/997 - if (sysctlbyname("vm.stats.vm.v_cache_count", &cached, &size, NULL, 0)) + if (sysctlbyname("vm.stats.vm.v_cache_count", &cached, &size, NULL, 0)) { cached = 0; - if (sysctlbyname("vm.stats.vm.v_free_count", &free, &size, NULL, 0)) - goto error; - if (sysctlbyname("vfs.bufspace", &buffers, &buffers_size, NULL, 0)) - goto error; + } + if (sysctlbyname("vm.stats.vm.v_free_count", &free, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_free_count')"); + } + if (sysctlbyname("vfs.bufspace", &buffers, &buffers_size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('vfs.bufspace')"); + } size = sizeof(vm); - if (sysctl(mib, 2, &vm, &size, NULL, 0) != 0) - goto error; + if (sysctl(mib, 2, &vm, &size, NULL, 0) != 0) { + return PyErr_SetFromOSErrnoWithSyscall("sysctl(CTL_VM | VM_METER)"); + } return Py_BuildValue("KKKKKKKK", (unsigned long long) total, @@ -478,9 +493,6 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { (unsigned long long) buffers, (unsigned long long) (vm.t_vmshr + vm.t_rmshr) * pagesize // shared ); - -error: - return PyErr_SetFromErrno(PyExc_OSError); } @@ -491,6 +503,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { struct kvm_swap kvmsw[1]; unsigned int swapin, swapout, nodein, nodeout; size_t size = sizeof(unsigned int); + int pagesize; kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open failed"); if (kd == NULL) { @@ -507,16 +520,28 @@ psutil_swap_mem(PyObject *self, PyObject *args) { kvm_close(kd); - if (sysctlbyname("vm.stats.vm.v_swapin", &swapin, &size, NULL, 0) == -1) - goto error; - if (sysctlbyname("vm.stats.vm.v_swapout", &swapout, &size, NULL, 0) == -1) - goto error; - if (sysctlbyname("vm.stats.vm.v_vnodein", &nodein, &size, NULL, 0) == -1) - goto error; - if (sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, &size, NULL, 0) == -1) - goto error; + if (sysctlbyname("vm.stats.vm.v_swapin", &swapin, &size, NULL, 0) == -1) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_swapin)'"); + } + if (sysctlbyname("vm.stats.vm.v_swapout", &swapout, &size, NULL, 0) == -1){ + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_swapout)'"); + } + if (sysctlbyname("vm.stats.vm.v_vnodein", &nodein, &size, NULL, 0) == -1) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_vnodein)'"); + } + if (sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, &size, NULL, 0) == -1) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_vnodeout)'"); + } - int pagesize = getpagesize(); + pagesize = getpagesize(); + if (pagesize <= 0) { + PyErr_SetString(PyExc_ValueError, "invalid getpagesize()"); + return NULL; + } return Py_BuildValue( "(KKKII)", @@ -527,9 +552,6 @@ psutil_swap_mem(PyObject *self, PyObject *args) { swapin + swapout, // swap in nodein + nodeout // swap out ); - -error: - return PyErr_SetFromErrno(PyExc_OSError); } @@ -629,7 +651,8 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { size = sizeof(maxcpus); if (sysctlbyname("kern.smp.maxcpus", &maxcpus, &size, NULL, 0) < 0) { Py_DECREF(py_retlist); - return PyErr_SetFromErrno(PyExc_OSError); + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('kern.smp.maxcpus')"); } long cpu_time[maxcpus][CPUSTATES]; @@ -638,14 +661,14 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { mib[1] = HW_NCPU; len = sizeof(ncpu); if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_NCPU)"); goto error; } // per-cpu info size = sizeof(cpu_time); if (sysctlbyname("kern.cp_times", &cpu_time, &size, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('kern.smp.maxcpus')"); goto error; } @@ -960,16 +983,26 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { unsigned int v_swtch; size_t size = sizeof(v_soft); - if (sysctlbyname("vm.stats.sys.v_soft", &v_soft, &size, NULL, 0)) - goto error; - if (sysctlbyname("vm.stats.sys.v_intr", &v_intr, &size, NULL, 0)) - goto error; - if (sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, &size, NULL, 0)) - goto error; - if (sysctlbyname("vm.stats.sys.v_trap", &v_trap, &size, NULL, 0)) - goto error; - if (sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, &size, NULL, 0)) - goto error; + if (sysctlbyname("vm.stats.sys.v_soft", &v_soft, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_soft')"); + } + if (sysctlbyname("vm.stats.sys.v_intr", &v_intr, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_intr')"); + } + if (sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_syscall')"); + } + if (sysctlbyname("vm.stats.sys.v_trap", &v_trap, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_trap')"); + } + if (sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_swtch')"); + } return Py_BuildValue( "IIIII", @@ -979,9 +1012,6 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { v_syscall, // syscalls v_trap // traps ); - -error: - return PyErr_SetFromErrno(PyExc_OSError); } diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index e5ecbaed8d..484254d549 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -59,8 +59,10 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { */ while (lim-- > 0) { size = 0; - if (sysctl((int *)mib3, 3, NULL, &size, NULL, 0) == -1) - return errno; + if (sysctl((int *)mib3, 3, NULL, &size, NULL, 0) == -1) { + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); + return 1; + } size2 = size + (size >> 3); // add some if (size2 > size) { ptr = malloc(size2); @@ -72,22 +74,32 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { else { ptr = malloc(size); } - if (ptr == NULL) - return ENOMEM; + if (ptr == NULL) { + PyErr_NoMemory(); + return 1; + } if (sysctl((int *)mib3, 3, ptr, &size, NULL, 0) == -1) { err = errno; free(ptr); - if (err != ENOMEM) - return err; + if (err != ENOMEM) { + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); + return 1; + } } else { *procList = (kinfo_proc *)ptr; *procCount = size / sizeof(kinfo_proc); - return 0; + if (procCount <= 0) { + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + return 1; + } + return 0; // success } } - return ENOMEM; + + PyErr_Format(PyExc_RuntimeError, "couldn't collect PIDs list"); + return 1; } @@ -100,6 +112,7 @@ psutil_get_argmax() { if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) return argmax; + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); return 0; } @@ -125,10 +138,8 @@ psutil_get_cmdline(long pid) { // read argmax and allocate memory for argument space. argmax = psutil_get_argmax(); - if (! argmax) { - PyErr_SetFromErrno(PyExc_OSError); + if (! argmax) goto error; - } procargs = (char *)malloc(argmax); if (NULL == procargs) { @@ -219,10 +230,8 @@ psutil_get_environ(long pid) { // read argmax and allocate memory for argument space. argmax = psutil_get_argmax(); - if (! argmax) { - PyErr_SetFromErrno(PyExc_OSError); + if (! argmax) goto error; - } procargs = (char *)malloc(argmax); if (NULL == procargs) { diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 9a698e4245..5e0b94217b 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -227,7 +227,10 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid) { else if (ret == 0) return NoSuchProcess(""); else if (ret == -1) - return PyErr_SetFromWindowsErr(0); + if (GetLastError() == ERROR_ACCESS_DENIED) + return PyErr_SetFromWindowsErr(0); + else + return PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); else // -2 return NULL; } @@ -358,7 +361,7 @@ psutil_pid_is_running(DWORD pid) { // Be strict and raise an exception; the caller is supposed // to take -1 into account. else { - PyErr_SetFromWindowsErr(err); + PyErr_SetFromOSErrnoWithSyscall("OpenProcess(PROCESS_VM_READ)"); return -1; } } @@ -392,7 +395,7 @@ psutil_pid_is_running(DWORD pid) { return 1; } else { - PyErr_SetFromWindowsErr(err); + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); return -1; } } @@ -406,7 +409,7 @@ psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { MEMORY_BASIC_INFORMATION info; if (!VirtualQueryEx(hProcess, src, &info, sizeof(info))) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("VirtualQueryEx"); return -1; } @@ -488,7 +491,8 @@ psutil_get_process_data(long pid, sizeof(LPVOID), NULL))) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "NtQueryInformationProcess(ProcessWow64Information)"); goto error; } @@ -499,7 +503,7 @@ psutil_get_process_data(long pid, // read PEB if (!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("ReadProcessMemory"); goto error; } @@ -509,7 +513,8 @@ psutil_get_process_data(long pid, &procParameters32, sizeof(procParameters32), NULL)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "ReadProcessMemory(ProcessParameters)"); goto error; } @@ -530,8 +535,8 @@ psutil_get_process_data(long pid, #else /* 32 bit case. Check if the target is also 32 bit. */ if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || - !IsWow64Process(hProcess, &theyAreWow64)) { - PyErr_SetFromWindowsErr(0); + !IsWow64Process(hProcess, &theyAreWow64)) { + PyErr_SetFromOSErrnoWithSyscall("IsWow64Process"); goto error; } @@ -570,7 +575,8 @@ psutil_get_process_data(long pid, sizeof(pbi64), NULL))) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); goto error; } @@ -582,7 +588,7 @@ psutil_get_process_data(long pid, sizeof(peb64), NULL))) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("NtWow64ReadVirtualMemory64"); goto error; } @@ -594,7 +600,8 @@ psutil_get_process_data(long pid, sizeof(procParameters64), NULL))) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "NtWow64ReadVirtualMemory64(ProcessParameters)"); goto error; } @@ -626,7 +633,8 @@ psutil_get_process_data(long pid, sizeof(pbi), NULL))) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "NtQueryInformationProcess(ProcessBasicInformation)"); goto error; } @@ -636,7 +644,7 @@ psutil_get_process_data(long pid, &peb, sizeof(peb), NULL)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("ReadProcessMemory"); goto error; } @@ -646,7 +654,8 @@ psutil_get_process_data(long pid, &procParameters, sizeof(procParameters), NULL)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall( + "ReadProcessMemory(ProcessParameters)"); goto error; } @@ -678,7 +687,6 @@ psutil_get_process_data(long pid, } buffer = calloc(size + 2, 1); - if (buffer == NULL) { PyErr_NoMemory(); goto error; @@ -693,13 +701,13 @@ psutil_get_process_data(long pid, size, NULL))) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("NtWow64ReadVirtualMemory64"); goto error; } } else #endif if (!ReadProcessMemory(hProcess, src, buffer, size, NULL)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("ReadProcessMemory"); goto error; } @@ -751,7 +759,7 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { &ret_length ); if (! NT_SUCCESS(status)) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess"); goto error; } @@ -820,7 +828,7 @@ psutil_get_cmdline(long pid, int use_peb) { // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(data, &nArgs); if (szArglist == NULL) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("CommandLineToArgvW"); goto out; } diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 62a12861f3..92458494b4 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -24,13 +24,13 @@ psutil_get_service_handler(char *service_name, DWORD scm_access, DWORD access) sc = OpenSCManager(NULL, NULL, scm_access); if (sc == NULL) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); return NULL; } hService = OpenService(sc, service_name, access); if (hService == NULL) { + PyErr_SetFromOSErrnoWithSyscall("OpenService"); CloseServiceHandle(sc); - PyErr_SetFromWindowsErr(0); return NULL; } CloseServiceHandle(sc); @@ -113,7 +113,7 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { sc = OpenSCManager(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE); if (sc == NULL) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); return NULL; } @@ -213,13 +213,13 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { bytesNeeded = 0; QueryServiceConfigW(hService, NULL, 0, &bytesNeeded); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); goto error; } qsc = (QUERY_SERVICE_CONFIGW *)malloc(bytesNeeded); ok = QueryServiceConfigW(hService, qsc, bytesNeeded, &bytesNeeded); if (ok == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); goto error; } @@ -307,7 +307,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { return Py_BuildValue("s", ""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); goto error; } ssp = (SERVICE_STATUS_PROCESS *)HeapAlloc( @@ -321,7 +321,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { ok = QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)ssp, bytesNeeded, &bytesNeeded); if (ok == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); goto error; } @@ -381,7 +381,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { return Py_BuildValue("s", ""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); goto error; } @@ -389,7 +389,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { ok = QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, (LPBYTE)scd, bytesNeeded, &bytesNeeded); if (ok == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); goto error; } @@ -435,7 +435,7 @@ psutil_winservice_start(PyObject *self, PyObject *args) { } ok = StartService(hService, 0, NULL); if (ok == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("StartService"); goto error; } @@ -471,7 +471,7 @@ psutil_winservice_stop(PyObject *self, PyObject *args) { ok = ControlService(hService, SERVICE_CONTROL_STOP, &ssp); Py_END_ALLOW_THREADS if (ok == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("ControlService"); goto error; } diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 7cc678f057..b0b7e4f098 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -766,7 +766,9 @@ def check_ls(ls): ls = psutil.cpu_freq(percpu=True) if TRAVIS and not ls: - return + 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)]) From f326539daa5bbd9153db79b508b6aaede731a154 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 02:35:07 -0800 Subject: [PATCH 0158/1714] Process SE DEBUG mode was not set on Windows (#1429) --- Makefile | 4 + psutil/_psutil_windows.c | 4 +- psutil/arch/windows/security.c | 253 ++++++++---------------- psutil/arch/windows/security.h | 4 - scripts/internal/procs_access_denied.py | 80 ++++++++ scripts/internal/winmake.py | 7 + 6 files changed, 179 insertions(+), 173 deletions(-) create mode 100644 scripts/internal/procs_access_denied.py diff --git a/Makefile b/Makefile index 25928e0aa3..744805187f 100644 --- a/Makefile +++ b/Makefile @@ -266,5 +266,9 @@ bench-oneshot-2: ## Same as above but using perf module (supposed to be more pr check-broken-links: ## Look for broken links in source files. git ls-files | xargs $(PYTHON) -Wa scripts/internal/check_broken_links.py +print-access-denied: +# ${MAKE} install + $(TEST_PREFIX) $(PYTHON) scripts/internal/procs_access_denied.py + help: ## Display callable targets. @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 7dc37ee171..111cd71a51 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3724,7 +3724,9 @@ void init_psutil_windows(void) module, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST); // set SeDebug for the current process - psutil_set_se_debug(); + if (psutil_set_se_debug() != 0) + return NULL; + psutil_setup(); if (psutil_load_globals() != 0) return NULL; diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index d5f8f8d3da..4e2c7435b2 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -4,134 +4,44 @@ * found in the LICENSE file. * * Security related functions for Windows platform (Set privileges such as - * SeDebug), as well as security helper functions. + * SE DEBUG). */ #include #include +#include "../../_psutil_common.h" -/* - * Convert a process handle to a process token handle. - */ -HANDLE -psutil_token_from_handle(HANDLE hProcess) { - HANDLE hToken = NULL; - - if (! OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) - return PyErr_SetFromWindowsErr(0); - return hToken; -} - - -/* - * http://www.ddj.com/windows/184405986 - * - * There's a way to determine whether we're running under the Local System - * account. However (you guessed it), we have to call more Win32 functions to - * determine this. Backing up through the code listing, we need to make another - * call to GetTokenInformation, but instead of passing through the TOKEN_USER - * constant, we pass through the TOKEN_PRIVILEGES constant. This value returns - * an array of privileges that the account has in the environment. Iterating - * through the array, we call the function LookupPrivilegeName looking for the - * string “SeTcbPrivilege. If the function returns this string, then this - * account has Local System privileges - */ -int -psutil_has_system_privilege(HANDLE hProcess) { - DWORD i; - DWORD dwSize = 0; - DWORD dwRetval = 0; - TCHAR privName[256]; - DWORD dwNameSize = 256; - // PTOKEN_PRIVILEGES tp = NULL; - BYTE *pBuffer = NULL; - TOKEN_PRIVILEGES *tp = NULL; - HANDLE hToken = psutil_token_from_handle(hProcess); - - if (NULL == hToken) - return -1; - // call GetTokenInformation first to get the buffer size - if (! GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &dwSize)) { - dwRetval = GetLastError(); - // if it failed for a reason other than the buffer, bail out - if (dwRetval != ERROR_INSUFFICIENT_BUFFER ) { - PyErr_SetFromWindowsErr(dwRetval); - return 0; - } - } - - // allocate buffer and call GetTokenInformation again - // tp = (PTOKEN_PRIVILEGES) GlobalAlloc(GPTR, dwSize); - pBuffer = (BYTE *) malloc(dwSize); - if (pBuffer == NULL) { - PyErr_NoMemory(); - return -1; - } - - if (! GetTokenInformation(hToken, TokenPrivileges, pBuffer, - dwSize, &dwSize)) - { - PyErr_SetFromWindowsErr(0); - free(pBuffer); - return -1; - } - - // convert the BYTE buffer to a TOKEN_PRIVILEGES struct pointer - tp = (TOKEN_PRIVILEGES *)pBuffer; - - // check all the privileges looking for SeTcbPrivilege - for (i = 0; i < tp->PrivilegeCount; i++) { - // reset the buffer contents and the buffer size - strcpy(privName, ""); - dwNameSize = sizeof(privName) / sizeof(TCHAR); - if (! LookupPrivilegeName(NULL, - &tp->Privileges[i].Luid, - (LPTSTR)privName, - &dwNameSize)) - { - PyErr_SetFromWindowsErr(0); - free(pBuffer); - return -1; - } - - // if we find the SeTcbPrivilege then it's a LocalSystem process - if (! lstrcmpi(privName, TEXT("SeTcbPrivilege"))) { - free(pBuffer); - return 1; - } - } - - free(pBuffer); - return 0; -} - - -BOOL +static BOOL psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { TOKEN_PRIVILEGES tp; LUID luid; TOKEN_PRIVILEGES tpPrevious; DWORD cbPrevious = sizeof(TOKEN_PRIVILEGES); - if (!LookupPrivilegeValue( NULL, Privilege, &luid )) return FALSE; + if (! LookupPrivilegeValue(NULL, Privilege, &luid)) { + PyErr_SetFromOSErrnoWithSyscall("LookupPrivilegeValue"); + return 1; + } // first pass. get current privilege setting tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = 0; - AdjustTokenPrivileges( - hToken, - FALSE, - &tp, - sizeof(TOKEN_PRIVILEGES), - &tpPrevious, - &cbPrevious - ); - - if (GetLastError() != ERROR_SUCCESS) return FALSE; - // second pass. set privilege based on previous setting + if (! AdjustTokenPrivileges( + hToken, + FALSE, + &tp, + sizeof(TOKEN_PRIVILEGES), + &tpPrevious, + &cbPrevious)) + { + PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); + return 1; + } + + // Second pass. Set privilege based on previous setting. tpPrevious.PrivilegeCount = 1; tpPrevious.Privileges[0].Luid = luid; @@ -141,81 +51,88 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { tpPrevious.Privileges[0].Attributes ^= (SE_PRIVILEGE_ENABLED & tpPrevious.Privileges[0].Attributes); - AdjustTokenPrivileges( - hToken, - FALSE, - &tpPrevious, - cbPrevious, - NULL, - NULL - ); - - if (GetLastError() != ERROR_SUCCESS) return FALSE; + if (! AdjustTokenPrivileges( + hToken, + FALSE, + &tpPrevious, + cbPrevious, + NULL, + NULL)) + { + PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); + return 1; + } - return TRUE; + return 0; } -int -psutil_set_se_debug() { - HANDLE hToken; - if (!OpenProcessToken(GetCurrentProcess(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - &hToken) - ) { - if (GetLastError() == ERROR_NO_TOKEN) { - if (!ImpersonateSelf(SecurityImpersonation)) { - CloseHandle(hToken); - return 0; +static HANDLE +psutil_get_thisproc_token() { + HANDLE hToken = NULL; + HANDLE me = GetCurrentProcess(); + + if (! OpenProcessToken( + me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) + { + if (GetLastError() == ERROR_NO_TOKEN) + { + if (! ImpersonateSelf(SecurityImpersonation)) { + PyErr_SetFromOSErrnoWithSyscall("ImpersonateSelf"); + return NULL; } - if (!OpenProcessToken(GetCurrentProcess(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - &hToken) - ) { - RevertToSelf(); - CloseHandle(hToken); - return 0; + if (! OpenProcessToken( + me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) + { + PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + return NULL; } } + else { + PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + return NULL; + } } - // enable SeDebugPrivilege (open any process) - if (! psutil_set_privilege(hToken, SE_DEBUG_NAME, TRUE)) { - RevertToSelf(); - CloseHandle(hToken); - return 0; - } + return hToken; +} - RevertToSelf(); - CloseHandle(hToken); - return 1; + +static void +psutil_print_err() { + char *msg = "psutil module couldn't set SE DEBUG mode for this process; " \ + "please file an issue against psutil bug tracker"; + psutil_debug(msg); + if (GetLastError() != ERROR_ACCESS_DENIED) + PyErr_WarnEx(PyExc_RuntimeWarning, msg, 1); + PyErr_Clear(); } +/* + * Set this process in SE DEBUG mode so that we have more chances of + * querying processes owned by other users, including many owned by + * Administrator and Local System. + * https://docs.microsoft.com/windows-hardware/drivers/debugger/debug-privilege + * This is executed on module import and we don't crash on error. + */ int -psutil_unset_se_debug() { +psutil_set_se_debug() { HANDLE hToken; - if (!OpenProcessToken(GetCurrentProcess(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - &hToken) - ) { - if (GetLastError() == ERROR_NO_TOKEN) { - if (! ImpersonateSelf(SecurityImpersonation)) - return 0; - if (!OpenProcessToken(GetCurrentProcess(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - &hToken)) - { - return 0; - } - } - } + int err = 1; - // now disable SeDebug - if (! psutil_set_privilege(hToken, SE_DEBUG_NAME, FALSE)) + if ((hToken = psutil_get_thisproc_token()) == NULL) { + // "return 1;" to get an exception + psutil_print_err(); return 0; + } + + if (psutil_set_privilege(hToken, SE_DEBUG_NAME, TRUE) != 0) { + // "return 1;" to get an exception + psutil_print_err(); + } + RevertToSelf(); CloseHandle(hToken); - return 1; + return 0; } - diff --git a/psutil/arch/windows/security.h b/psutil/arch/windows/security.h index aa8a22ad1a..8d4ddb00d4 100644 --- a/psutil/arch/windows/security.h +++ b/psutil/arch/windows/security.h @@ -9,9 +9,5 @@ #include -BOOL psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege); -HANDLE psutil_token_from_handle(HANDLE hProcess); -int psutil_has_system_privilege(HANDLE hProcess); int psutil_set_se_debug(); -int psutil_unset_se_debug(); diff --git a/scripts/internal/procs_access_denied.py b/scripts/internal/procs_access_denied.py new file mode 100644 index 0000000000..9f792480bc --- /dev/null +++ b/scripts/internal/procs_access_denied.py @@ -0,0 +1,80 @@ +#!/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. + +""" +Helper script which tries to access all info of all running processes. +It prints how many AccessDenied exceptions are raised in total and +for each Process method. +""" + +from __future__ import print_function, division +from collections import defaultdict +import sys + +import psutil + + +def term_supports_colors(file=sys.stdout): + try: + import curses + assert file.isatty() + curses.setupterm() + assert curses.tigetnum("colors") > 0 + except Exception: + return False + else: + return True + + +COLORS = term_supports_colors() + + +def hilite(s, ok=True, bold=False): + """Return an highlighted version of 'string'.""" + if not COLORS: + return s + attr = [] + if ok is None: # no color + pass + elif ok: # green + attr.append('32') + else: # red + attr.append('31') + if bold: + attr.append('1') + return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) + + +def main(): + tot_procs = 0 + tot_ads = 0 + signaler = object() + d = defaultdict(int) + for p in psutil.process_iter(attrs=[], ad_value=signaler): + tot_procs += 1 + for methname, value in p.info.items(): + if value is signaler: + tot_ads += 1 + d[methname] += 1 + else: + d[methname] += 0 + + for methname, ads in sorted(d.items(), key=lambda x: x[1]): + perc = (ads / tot_procs) * 100 + s = "%-20s %-3s %5.1f%% " % (methname, ads, perc) + if not ads: + s += "SUCCESS" + s = hilite(s, ok=True) + else: + s += "ACCESS DENIED" + s = hilite(s, ok=False) + print(s) + print("--------------------------") + print("total: %19s (%s total processes)" % (tot_ads, tot_procs)) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index b1ce7b8a4d..49aae699f9 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -483,6 +483,13 @@ def bench_oneshot_2(): sh("%s -Wa scripts\\internal\\bench_oneshot_2.py" % PYTHON) +@cmd +def print_access_denied(): + """Benchmarks for oneshot() ctx manager (see #799).""" + install() + sh("%s -Wa scripts\\internal\\procs_access_denied.py" % PYTHON) + + def set_python(s): global PYTHON if os.path.isabs(s): From e5f7f556f8f91cf272acfd536e82293d450c5ec7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 03:25:27 -0800 Subject: [PATCH 0159/1714] fix #1431: use GetNativeSystemInfo instead of GetSystemInfo in order to support WoW64 processes --- HISTORY.rst | 3 +++ psutil/_psutil_windows.c | 4 +--- psutil/tests/test_windows.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c34bc78067..7de87584ea 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,9 @@ XXXX-XX-XX a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. - 1427_: [OSX] Process cmdline() and environ() may erroneously raise OSError on failed malloc(). +- 1431_: [Windows] GetNativeSystemInfo is not used instead of GetSystemInfo in + order to support WoW64 processes. Affected APIs are psutil.cpu_count(), + and Process memory_maps() and memory_info_exe() ("uss" field). 5.5.1 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 111cd71a51..df52d43628 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -854,8 +854,6 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) } } - // GetSystemInfo has no return value. - GetSystemInfo(&system_info); total = private_pages * system_info.dwPageSize; py_result = Py_BuildValue("K", total); @@ -2850,7 +2848,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (NULL == hProcess) goto error; - GetSystemInfo(&system_info); + GetNativeSystemInfo(&system_info); maxAddr = system_info.lpMaximumApplicationAddress; baseAddress = NULL; diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index c98d892cfa..ff63cd79cc 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -82,7 +82,7 @@ def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): def test_cpu_count_vs_GetSystemInfo(self): # Will likely fail on many-cores systems: # https://stackoverflow.com/questions/31209256 - sys_value = win32api.GetSystemInfo()[5] + sys_value = win32api.GetNativeSystemInfo()[5] psutil_value = psutil.cpu_count() self.assertEqual(sys_value, psutil_value) From bc7b5982ca9cb12c91b44aac6cf3f4698a61c369 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 03:28:38 -0800 Subject: [PATCH 0160/1714] fix #1432: use the actual system PAGESIZE when calculating USS memory --- HISTORY.rst | 2 ++ psutil/_psutil_windows.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7de87584ea..aa2b2fed2c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,8 @@ XXXX-XX-XX - 1431_: [Windows] GetNativeSystemInfo is not used instead of GetSystemInfo in order to support WoW64 processes. Affected APIs are psutil.cpu_count(), and Process memory_maps() and memory_info_exe() ("uss" field). +- 1432_: [Windows] Process.memory_info_ex()'s USS memory is miscalculated + because we're not using the actual system PAGESIZE. 5.5.1 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index df52d43628..8ab21aa7e3 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -808,7 +808,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - + GetNativeSystemInfo(&system_info); proc = psutil_handle_from_pid(pid, access); if (proc == NULL) return NULL; From b7c562e29c3b172af46afba378759a486f8fbb4d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 03:45:23 -0800 Subject: [PATCH 0161/1714] fix #1426: load PAGESIZE and num of processors on startup --- HISTORY.rst | 5 +++++ psutil/_psutil_windows.c | 12 +++--------- psutil/arch/windows/global.c | 12 +++++++++++- psutil/arch/windows/global.h | 1 + 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index aa2b2fed2c..7d77569d2c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,11 @@ XXXX-XX-XX - 1420_: [Windows] in case of exception disk_usage() now also shows the path name. +- 1422_: [Windows] Windows APIs requiring to be dynamically loaded from DLL + libraries are now loaded only once on startup (instead of on per function + call) significantly speeding up different functions and methods. +- 1426_: [Windows] PAGESIZE and number of processors is now calculated on + startup. **Bug fixes** diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 8ab21aa7e3..48e7586372 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -103,7 +103,6 @@ psutil_get_nic_addresses() { unsigned int psutil_get_num_cpus(int fail_on_err) { unsigned int ncpus = 0; - SYSTEM_INFO sysinfo; // Minimum requirement: Windows 7 if (psutil_GetActiveProcessorCount != NULL) { @@ -115,8 +114,7 @@ psutil_get_num_cpus(int fail_on_err) { else { psutil_debug("GetActiveProcessorCount() not available; " "using GetNativeSystemInfo()"); - GetNativeSystemInfo(&sysinfo); - ncpus = (unsigned int)sysinfo.dwNumberOfProcessors; + ncpus = (unsigned int)PSUTIL_SYSTEM_INFO.dwNumberOfProcessors; if ((ncpus == 0) && (fail_on_err == 1)) { PyErr_SetString( PyExc_RuntimeError, @@ -801,14 +799,12 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) // needed by QueryWorkingSet DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; PSAPI_WORKING_SET_INFORMATION* info_array; - SYSTEM_INFO system_info; PyObject* py_result = NULL; unsigned long long total = 0; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - GetNativeSystemInfo(&system_info); proc = psutil_handle_from_pid(pid, access); if (proc == NULL) return NULL; @@ -854,7 +850,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) } } - total = private_pages * system_info.dwPageSize; + total = private_pages * PSUTIL_SYSTEM_INFO.dwPageSize; py_result = Py_BuildValue("K", total); done: @@ -2832,7 +2828,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { PVOID baseAddress; ULONGLONG previousAllocationBase; WCHAR mappedFileName[MAX_PATH]; - SYSTEM_INFO system_info; LPVOID maxAddr; // required by GetMappedFileNameW DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; @@ -2848,8 +2843,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (NULL == hProcess) goto error; - GetNativeSystemInfo(&system_info); - maxAddr = system_info.lpMaximumApplicationAddress; + maxAddr = PSUTIL_SYSTEM_INFO.lpMaximumApplicationAddress; baseAddress = NULL; while (VirtualQueryEx(hProcess, baseAddress, &basicInfo, diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index da3bdc6e4f..df1c752894 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -14,8 +14,9 @@ #include "global.h" -// Needed to make this globally visible. +// Needed to make these globally visible. int PSUTIL_WINVER; +SYSTEM_INFO PSUTIL_SYSTEM_INFO; // A wrapper around GetModuleHandle and GetProcAddress. PVOID @@ -167,11 +168,20 @@ psutil_set_winver() { } +static int +psutil_load_sysinfo() { + GetNativeSystemInfo(&PSUTIL_SYSTEM_INFO); + return 0; +} + + int psutil_load_globals() { if (psutil_loadlibs() != 0) return 1; if (psutil_set_winver() != 0) return 1; + if (psutil_load_sysinfo() != 0) + return 1; return 0; } diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 7bf8b5cad6..1f4951faa7 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -10,6 +10,7 @@ #include "ntextapi.h" extern int PSUTIL_WINVER; +extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; #define PSUTIL_WINDOWS_XP 51 #define PSUTIL_WINDOWS_SERVER_2003 52 #define PSUTIL_WINDOWS_VISTA 60 From 727118dcf48e89fce6796d7b721fd11a8dd511db Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 12:52:00 +0100 Subject: [PATCH 0162/1714] OSX move is_zombie() function in process_info.c --- psutil/_psutil_osx.c | 14 -------------- psutil/arch/osx/process_info.c | 12 ++++++++++++ psutil/arch/osx/process_info.h | 1 + 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 1b98cc5860..99c073e8d8 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -73,20 +73,6 @@ psutil_sys_vminfo(vm_statistics_data_t *vmstat) { } -/* - * Return 1 if pid refers to a zombie process else 0. - */ -int -psutil_is_zombie(long pid) -{ - struct kinfo_proc kp; - - if (psutil_get_kinfo_proc(pid, &kp) == -1) - return 0; - return (kp.kp_proc.p_stat == SZOMB) ? 1 : 0; -} - - /* * A wrapper around task_for_pid() which sucks big time: * - it's not documented diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 484254d549..d21c048edb 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -117,6 +117,18 @@ psutil_get_argmax() { } +// Return 1 if pid refers to a zombie process else 0. +int +psutil_is_zombie(long pid) { + struct kinfo_proc kp; + + if (psutil_get_kinfo_proc(pid, &kp) == -1) + return 0; + return (kp.kp_proc.p_stat == SZOMB) ? 1 : 0; +} + + + // return process args as a python list PyObject * psutil_get_cmdline(long pid) { diff --git a/psutil/arch/osx/process_info.h b/psutil/arch/osx/process_info.h index bd2eef868a..bd7ffa89ca 100644 --- a/psutil/arch/osx/process_info.h +++ b/psutil/arch/osx/process_info.h @@ -9,6 +9,7 @@ typedef struct kinfo_proc kinfo_proc; int psutil_get_argmax(void); +int psutil_is_zombie(long pid); int psutil_get_kinfo_proc(long pid, struct kinfo_proc *kp); int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); int psutil_proc_pidinfo( From c5f616d78d44a5682258b68b57cfdffd9854ad15 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 13:01:22 +0100 Subject: [PATCH 0163/1714] OSX small refactoring --- psutil/_psutil_osx.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 99c073e8d8..08be754bea 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -546,7 +546,7 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { * Indicates if the given virtual address on the given architecture is in the * shared VM region. */ -bool +static bool psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { mach_vm_address_t base; mach_vm_address_t size; @@ -1468,19 +1468,18 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { char *buf = NULL, *lim, *next; struct if_msghdr *ifm; int mib[6]; - size_t len; - PyObject *py_retdict = PyDict_New(); - PyObject *py_ifc_info = NULL; - - if (py_retdict == NULL) - return NULL; - mib[0] = CTL_NET; // networking subsystem mib[1] = PF_ROUTE; // type of information mib[2] = 0; // protocol (IPPROTO_xxx) mib[3] = 0; // address family mib[4] = NET_RT_IFLIST2; // operation mib[5] = 0; + size_t len; + PyObject *py_ifc_info = NULL; + PyObject *py_retdict = PyDict_New(); + + if (py_retdict == NULL) + return NULL; if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { PyErr_SetFromErrno(PyExc_OSError); @@ -1558,8 +1557,8 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { io_registry_entry_t parent; io_registry_entry_t disk; io_iterator_t disk_list; - PyObject *py_retdict = PyDict_New(); PyObject *py_disk_info = NULL; + PyObject *py_retdict = PyDict_New(); if (py_retdict == NULL) return NULL; From cbf2bafbd33ad21ef63400d94cb313c299e78a45 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 04:57:33 -0800 Subject: [PATCH 0164/1714] expose WINVER constants in the cext module --- psutil/_psutil_common.c | 5 +++-- psutil/_psutil_common.h | 2 +- psutil/_psutil_linux.c | 4 ++-- psutil/_psutil_osx.c | 8 ++++++-- psutil/_psutil_sunos.c | 8 ++++++-- psutil/_psutil_windows.c | 32 +++++++++++++++++++++++--------- 6 files changed, 41 insertions(+), 18 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index a4899487b8..6d13ec4a1e 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -120,10 +120,11 @@ psutil_debug(const char* format, ...) { /* * Called on module import on all platforms. */ -void -psutil_setup(void) { +int +psutil_setup() { if (getenv("PSUTIL_DEBUG") != NULL) PSUTIL_DEBUG = 1; if (getenv("PSUTIL_TESTING") != NULL) PSUTIL_TESTING = 1; + return 0; } diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 70698107a1..5e6640c25e 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -26,6 +26,6 @@ PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); -void psutil_setup(void); +int psutil_setup(); #endif // PSUTIL_PSUTIL_COMMON_H diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index be808633d3..4bf53b8579 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -673,6 +673,8 @@ void init_psutil_linux(void) #else PyObject *module = Py_InitModule("_psutil_linux", PsutilMethods); #endif + if (module == NULL) + INITERROR; PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); #if PSUTIL_HAVE_PRLIMIT @@ -720,8 +722,6 @@ void init_psutil_linux(void) PyModule_AddIntConstant(module, "DUPLEX_FULL", DUPLEX_FULL); PyModule_AddIntConstant(module, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN); - psutil_setup(); - if (module == NULL) INITERROR; #if PY_MAJOR_VERSION >= 3 diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 1b98cc5860..2be0b3f410 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -2043,6 +2043,12 @@ init_psutil_osx(void) #else PyObject *module = Py_InitModule("_psutil_osx", PsutilMethods); #endif + if (module == NULL) + INITERROR; + + if (psutil_setup() != 0) + INITERROR; + PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); // process status constants, defined in: // http://fxr.watson.org/fxr/source/bsd/sys/proc.h?v=xnu-792.6.70#L149 @@ -2071,8 +2077,6 @@ init_psutil_osx(void) Py_INCREF(ZombieProcessError); PyModule_AddObject(module, "ZombieProcessError", ZombieProcessError); - psutil_setup(); - if (module == NULL) INITERROR; #if PY_MAJOR_VERSION >= 3 diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 0717f19503..99423f7ae0 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1732,6 +1732,12 @@ void init_psutil_sunos(void) #else PyObject *module = Py_InitModule("_psutil_sunos", PsutilMethods); #endif + if (module == NULL) + INITERROR; + + if (psutil_setup() != 0) + INITERROR; + PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); PyModule_AddIntConstant(module, "SSLEEP", SSLEEP); @@ -1761,8 +1767,6 @@ void init_psutil_sunos(void) PyModule_AddIntConstant(module, "TCPS_BOUND", TCPS_BOUND); PyModule_AddIntConstant(module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); - psutil_setup(); - if (module == NULL) INITERROR; #if PY_MAJOR_VERSION >= 3 diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 48e7586372..62ba73e51b 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3615,10 +3615,15 @@ void init_psutil_windows(void) #else PyObject *module = Py_InitModule("_psutil_windows", PsutilMethods); #endif + if (module == NULL) + INITERROR; - if (module == NULL) { + if (psutil_setup() != 0) + INITERROR; + if (psutil_load_globals() != 0) + INITERROR; + if (psutil_set_se_debug() != 0) INITERROR; - } st = GETSTATE(module); st->error = PyErr_NewException("_psutil_windows.Error", NULL, NULL); @@ -3715,13 +3720,22 @@ void init_psutil_windows(void) PyModule_AddIntConstant( module, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST); - // set SeDebug for the current process - if (psutil_set_se_debug() != 0) - return NULL; - - psutil_setup(); - if (psutil_load_globals() != 0) - return NULL; + PyModule_AddIntConstant( + module, "WINVER", PSUTIL_WINVER); + PyModule_AddIntConstant( + module, "WINDOWS_XP", PSUTIL_WINDOWS_XP); + PyModule_AddIntConstant( + module, "WINDOWS_SERVER_2003", PSUTIL_WINDOWS_SERVER_2003); + PyModule_AddIntConstant( + module, "WINDOWS_VISTA", PSUTIL_WINDOWS_VISTA); + PyModule_AddIntConstant( + module, "WINDOWS_7", PSUTIL_WINDOWS_7); + PyModule_AddIntConstant( + module, "WINDOWS_8", PSUTIL_WINDOWS_8); + PyModule_AddIntConstant( + module, "WINDOWS_8_1", PSUTIL_WINDOWS_8_1); + PyModule_AddIntConstant( + module, "WINDOWS_10", PSUTIL_WINDOWS_10); #if PY_MAJOR_VERSION >= 3 return module; From 1be0343556f6a52ecf6fe3ae7e4a2ad2ff774ff9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 06:45:08 -0800 Subject: [PATCH 0165/1714] win: remove unused header files --- psutil/_psutil_windows.c | 19 ++++++------------- setup.py | 3 ++- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 62ba73e51b..54b401d31d 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -12,22 +12,15 @@ #include #include #include -#include -#include -#include +#include +#include // disk_io_counters() #include #include -#include -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above -#include +#include // users() +#include // cpu_freq() +#if (_WIN32_WINNT >= 0x0600) // Windows >= Vista +#include // net_connections() #endif -#include -#include -#include -#include -#include -#include -#include // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") diff --git a/setup.py b/setup.py index 764f8b31d3..465a9b9e33 100755 --- a/setup.py +++ b/setup.py @@ -135,11 +135,12 @@ def get_winver(): 'psutil/arch/windows/inet_ntop.c', 'psutil/arch/windows/services.c', 'psutil/arch/windows/global.c', + # 'psutil/arch/windows/connections.c', ], define_macros=macros, libraries=[ "psapi", "kernel32", "advapi32", "shell32", "netapi32", - "iphlpapi", "wtsapi32", "ws2_32", "PowrProf", + "wtsapi32", "ws2_32", "PowrProf", ], # extra_compile_args=["/Z7"], # extra_link_args=["/DEBUG"] From 8e2cb1d1eddb4668cb1be25066a1dafa6bdcf67a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 16:58:10 +0100 Subject: [PATCH 0166/1714] fix compiler warnings --- psutil/_psutil_common.c | 4 +++- psutil/_psutil_common.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 6d13ec4a1e..4b6ab39968 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -11,6 +11,8 @@ #include #endif +#include "_psutil_common.h" + // Global vars. int PSUTIL_DEBUG = 0; int PSUTIL_TESTING = 0; @@ -121,7 +123,7 @@ psutil_debug(const char* format, ...) { * Called on module import on all platforms. */ int -psutil_setup() { +psutil_setup(void) { if (getenv("PSUTIL_DEBUG") != NULL) PSUTIL_DEBUG = 1; if (getenv("PSUTIL_TESTING") != NULL) diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 5e6640c25e..7f58ad1738 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -26,6 +26,6 @@ PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); -int psutil_setup(); +int psutil_setup(void); #endif // PSUTIL_PSUTIL_COMMON_H From 1440b52f875bab4f11f63c97147301f6c37a6587 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 08:25:50 -0800 Subject: [PATCH 0167/1714] Add Process.parents() method (#1433) --- CREDITS | 5 +++++ HISTORY.rst | 3 ++- docs/index.rst | 7 +++++++ psutil/__init__.py | 15 +++++++++++++-- psutil/_psutil_common.c | 4 +++- psutil/_psutil_common.h | 2 +- psutil/_psutil_osx.c | 31 ++++++++----------------------- psutil/arch/osx/process_info.c | 12 ++++++++++++ psutil/arch/osx/process_info.h | 1 + psutil/tests/test_contracts.py | 3 ++- psutil/tests/test_process.py | 11 +++++++++++ 11 files changed, 65 insertions(+), 29 deletions(-) diff --git a/CREDITS b/CREDITS index c2712ecfbe..6cdeb1e9f0 100644 --- a/CREDITS +++ b/CREDITS @@ -571,3 +571,8 @@ I: 1360 N: EccoTheFlintstone W: https://github.com/EccoTheFlintstone I: 1368, 1348 + +N: Ghislain Le Meur +W: https://github.com/gigi206 +D: Process.parents() method +I: 1379 diff --git a/HISTORY.rst b/HISTORY.rst index 7d77569d2c..e0ea1f1d85 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.5.2 +5.6.0 ===== XXXX-XX-XX @@ -14,6 +14,7 @@ XXXX-XX-XX call) significantly speeding up different functions and methods. - 1426_: [Windows] PAGESIZE and number of processors is now calculated on startup. +- 1433_: new Process.parents() method. (idea by Ghislain Le Meur) **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 034795f975..8983e88039 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1194,6 +1194,13 @@ Process class PID is known return ``None``. See also :meth:`ppid` method. + .. method:: parents() + + Utility method which return the parents of this process as a list of + :class:`Process` instances. If no parents are known return an empty list. + + .. versionadded:: 5.6.0 + .. method:: status() The current process status as a string. The returned string is one of the diff --git a/psutil/__init__.py b/psutil/__init__.py index 241dc5f9f0..0aa13c7559 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -215,7 +215,7 @@ ] __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.5.2" +__version__ = "5.6.0" version_info = tuple([int(num) for num in __version__.split('.')]) AF_LINK = _psplatform.AF_LINK POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED @@ -656,6 +656,17 @@ 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 @@ -1459,7 +1470,7 @@ def wait(self, timeout=None): _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', + 'is_running', 'as_dict', 'parent', 'parents', 'children', 'rlimit', 'memory_info_ex', 'oneshot']]) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 6d13ec4a1e..4b6ab39968 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -11,6 +11,8 @@ #include #endif +#include "_psutil_common.h" + // Global vars. int PSUTIL_DEBUG = 0; int PSUTIL_TESTING = 0; @@ -121,7 +123,7 @@ psutil_debug(const char* format, ...) { * Called on module import on all platforms. */ int -psutil_setup() { +psutil_setup(void) { if (getenv("PSUTIL_DEBUG") != NULL) PSUTIL_DEBUG = 1; if (getenv("PSUTIL_TESTING") != NULL) diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 5e6640c25e..7f58ad1738 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -26,6 +26,6 @@ PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); -int psutil_setup(); +int psutil_setup(void); #endif // PSUTIL_PSUTIL_COMMON_H diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 2be0b3f410..ed4e60d374 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -73,20 +73,6 @@ psutil_sys_vminfo(vm_statistics_data_t *vmstat) { } -/* - * Return 1 if pid refers to a zombie process else 0. - */ -int -psutil_is_zombie(long pid) -{ - struct kinfo_proc kp; - - if (psutil_get_kinfo_proc(pid, &kp) == -1) - return 0; - return (kp.kp_proc.p_stat == SZOMB) ? 1 : 0; -} - - /* * A wrapper around task_for_pid() which sucks big time: * - it's not documented @@ -560,7 +546,7 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { * Indicates if the given virtual address on the given architecture is in the * shared VM region. */ -bool +static bool psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { mach_vm_address_t base; mach_vm_address_t size; @@ -1482,19 +1468,18 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { char *buf = NULL, *lim, *next; struct if_msghdr *ifm; int mib[6]; - size_t len; - PyObject *py_retdict = PyDict_New(); - PyObject *py_ifc_info = NULL; - - if (py_retdict == NULL) - return NULL; - mib[0] = CTL_NET; // networking subsystem mib[1] = PF_ROUTE; // type of information mib[2] = 0; // protocol (IPPROTO_xxx) mib[3] = 0; // address family mib[4] = NET_RT_IFLIST2; // operation mib[5] = 0; + size_t len; + PyObject *py_ifc_info = NULL; + PyObject *py_retdict = PyDict_New(); + + if (py_retdict == NULL) + return NULL; if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { PyErr_SetFromErrno(PyExc_OSError); @@ -1572,8 +1557,8 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { io_registry_entry_t parent; io_registry_entry_t disk; io_iterator_t disk_list; - PyObject *py_retdict = PyDict_New(); PyObject *py_disk_info = NULL; + PyObject *py_retdict = PyDict_New(); if (py_retdict == NULL) return NULL; diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 484254d549..d21c048edb 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -117,6 +117,18 @@ psutil_get_argmax() { } +// Return 1 if pid refers to a zombie process else 0. +int +psutil_is_zombie(long pid) { + struct kinfo_proc kp; + + if (psutil_get_kinfo_proc(pid, &kp) == -1) + return 0; + return (kp.kp_proc.p_stat == SZOMB) ? 1 : 0; +} + + + // return process args as a python list PyObject * psutil_get_cmdline(long pid) { diff --git a/psutil/arch/osx/process_info.h b/psutil/arch/osx/process_info.h index bd2eef868a..bd7ffa89ca 100644 --- a/psutil/arch/osx/process_info.h +++ b/psutil/arch/osx/process_info.h @@ -9,6 +9,7 @@ typedef struct kinfo_proc kinfo_proc; int psutil_get_argmax(void); +int psutil_is_zombie(long pid); int psutil_get_kinfo_proc(long pid, struct kinfo_proc *kp); int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); int psutil_proc_pidinfo( diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 78e6ba22fa..5ea4ba1c0c 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -294,7 +294,8 @@ def test_fetch_all(self): valid_procs = 0 excluded_names = set([ 'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', - 'as_dict', 'parent', 'children', 'memory_info_ex', 'oneshot', + 'as_dict', 'parent', 'parents', 'children', 'memory_info_ex', + 'oneshot', ]) if LINUX and not HAS_RLIMIT: excluded_names.add('rlimit') diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index cd72be8504..bfbe9bc042 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1046,6 +1046,11 @@ def test_parent(self): p = psutil.Process(sproc.pid) self.assertEqual(p.parent().pid, this_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() @@ -1054,6 +1059,12 @@ def test_parent_disappeared(self): 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(p2.parents()[0], p1) + self.assertEqual(p2.parents()[1], psutil.Process()) + def test_children(self): reap_children(recursive=True) p = psutil.Process() From a740a03e9de7492e20d98426b7e78eb805691890 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 17:53:29 +0100 Subject: [PATCH 0168/1714] refactor CREDITS --- CREDITS | 92 ++++++++++++++++++------------- psutil/tests/test_memory_leaks.py | 4 +- 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/CREDITS b/CREDITS index 6cdeb1e9f0..ea21c24ac9 100644 --- a/CREDITS +++ b/CREDITS @@ -2,24 +2,24 @@ Intro ===== I would like to recognize some of the people who have been instrumental in the -development of psutil. -I'm sure I'm forgetting some people (feel free to email me), but here is a -short list. -It's modeled after the Linux CREDITS file where the fields are: -name (N), e-mail (E), web-address (W), country (C), description (D), (I) issues -(issue tracker is at https://github.com/giampaolo/psutil/issues). -Really thanks to all of you. +development of psutil. I'm sure I'm forgetting somebody (feel free to email me) +but here is a short list. It's modeled after the Linux CREDITS file where the +fields are: name (N), e-mail (E), website (W), country (C), description (D), +(I) issues. Issue tracker is at https://github.com/giampaolo/psutil/issues). +A big thanks to all of you. - Giampaolo + Author ====== -N: Giampaolo Rodola' +N: Giampaolo Rodola C: Italy E: g.rodola@gmail.com W: http://grodola.blogspot.com/ + Experts ======= @@ -46,8 +46,9 @@ Github usernames of people to CC on github when in need of help. - AIX: - wiggin15, Arnon Yaari (maintainer) -Contributors -============ + +Top contributors +================ N: Jay Loden C: NJ, USA @@ -57,8 +58,14 @@ W: http://www.jayloden.com N: Arnon Yaari (wiggin15) W: https://github.com/wiggin15 +D: AIX implementation, expert on multiple fronts I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408 +N: Alex Manuskin +W: https://github.com/amanusk +D: FreeBSD cpu_freq(), OSX temperatures, support for Linux temperatures +I: 1284, 1345, 1350, 1352 + N: Jeff Tang W: https://github.com/mrjefftang I: 340, 529, 616, 653, 654, 648, 641 @@ -73,6 +80,25 @@ W: https://github.com/landryb D: OpenBSD implementation. I: 615 +N: Justin Venus +E: justin.venus@gmail.com +D: Solaris support +I: 18 + +N: Thomas Klausner +W: https://github.com/0-wiz-0 +D: NetBSD implementation (co-author). +I: 557 + +N: Ryo Onodera +W: https://github.com/ryoon +D: NetBSD implementation (co-author). +I: 557 + + +Contributors +============ + N: wj32 E: wj32.64@gmail.com D: process username() and get_connections() on Windows @@ -83,11 +109,6 @@ C: Bologna, Italy E: yanraber@gmail.com D: help on Windows development (initial version of Process.username()) -N: Justin Venus -E: justin.venus@gmail.com -D: Solaris support -I: 18 - N: Dave Daeschler C: USA E: david.daeschler@gmail.com @@ -95,19 +116,25 @@ W: http://daviddaeschler.com D: some contributions to initial design/bootstrap plus occasional bug fixing I: 522, 536 -N: Thomas Klausner -W: https://github.com/0-wiz-0 -I: #557 - -N: Ryo Onodera -W: https://github.com/ryoon -I: #557 - N: cjgohlke E: cjgohlke@gmail.com D: Windows 64 bit support I: 107 +N: Frank Benkstein +D: process environ() +W: https://github.com/fbenkstein +I: 732, 733 + +N: Mozilla Foundation +D: sample code for process USS memory. + +N: EccoTheFlintstone +W: https://github.com/EccoTheFlintstone +I: 1368, 1348 + +---- + N: Jeffery Kline E: jeffery.kline@gmail.com I: 130 @@ -381,10 +408,6 @@ N: Syohei YOSHIDA W: https://github.com/syohex I: 730 -N: Frank Benkstein -W: https://github.com/fbenkstein -I: 732, 733 - N: Visa Hankala E: visa@openbsd.org I: 741 @@ -402,9 +425,6 @@ N: mpderbec W: https://github.com/mpderbec I: 660 -N: Mozilla Foundation -D: sample code for process USS memory. - N: wxwright W: https://github.com/wxwright I: 776 @@ -482,7 +502,8 @@ I: 1057 N: Gleb Smirnoff W: https://github.com/glebius -I: 1042, 1079 +D: good help with FreeBSD +I: 1042, 1079, 1070 N: Oleksii Shevchuk W: https://github.com/alxchk @@ -544,10 +565,6 @@ N: Nikhil Marathe W: https://github.com/nikhilm I: 1278 -N: Alex Manuskin -W: https://github.com/amanusk -I: 1284, 1345, 1350, 1352 - N: Sylvain Duchesne W: https://github.com/sylvainduchesne I: 1294 @@ -562,16 +579,13 @@ I: 1332 N: Jaime Fullaondo W: https://github.com/truthbk +D: AIX support I: 1320 N: Koen Kooi W: https://github.com/koenkooi I: 1360 -N: EccoTheFlintstone -W: https://github.com/EccoTheFlintstone -I: 1368, 1348 - N: Ghislain Le Meur W: https://github.com/gigi206 D: Process.parents() method diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 58ae3312e6..1612857162 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -200,8 +200,8 @@ 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", "rlimit", "send_signal", "suspend", - "terminate", "wait")) + "nice", "oneshot", "parent", "parents", "rlimit", "send_signal", + "suspend", "terminate", "wait")) for name in dir(psutil.Process): if name.startswith('_'): continue From 842a50538fb518291079ab5f245cca4c63813cb2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 18:35:57 +0100 Subject: [PATCH 0169/1714] #1411 / BSD / Process() init: use Py_INCREF() around process name pyobj and avoid segfault --- HISTORY.rst | 2 ++ psutil/_psutil_bsd.c | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e0ea1f1d85..013645bdc5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,8 @@ XXXX-XX-XX - 1394_: [Windows] Process name() and exe() may erronously return "Registry". QueryFullProcessImageNameW is now used instead of GetProcessImageFileNameW in order to prevent that. +- 1411_: [BSD] lack of Py_DECREF could cause segmentation fault on process + instantiation. - 1419_: [Windows] Process.environ() raises NotImplementedError when querying a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. - 1427_: [OSX] Process cmdline() and environ() may erroneously raise OSError diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 073ff02d94..efb933fb2f 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -223,6 +223,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { PyErr_Clear(); py_name = Py_None; } + // Py_INCREF(py_name); // Calculate memory. #ifdef PSUTIL_FREEBSD @@ -343,10 +344,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { py_name // (pystr) name ); - if (py_retlist != NULL) { - // XXX shall we decref() also in case of Py_BuildValue() error? - Py_DECREF(py_name); - } + Py_DECREF(py_name); return py_retlist; } From 32c1a0d92f657487b0bd61c08c1a107e5d13bc51 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Feb 2019 21:19:00 +0100 Subject: [PATCH 0170/1714] #1379 - Windows: suspend / resume process by using native APIs (#1435) --- HISTORY.rst | 3 + psutil/_psutil_windows.c | 111 ++++++----------------------- psutil/_pswindows.py | 4 +- psutil/arch/windows/global.c | 10 +++ psutil/arch/windows/global.h | 6 ++ psutil/arch/windows/ntextapi.h | 8 +++ psutil/arch/windows/process_info.c | 19 +++-- 7 files changed, 64 insertions(+), 97 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 013645bdc5..3addc0270e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,9 @@ XXXX-XX-XX **Enhancements** +- 1379_: [Windows] Process suspend() and resume() now use NtSuspendProcess + and NtResumeProcess instead of stopping/resuming all threads of a process. + This is faster and more reliable (aka this is what ProcessHacker does). - 1420_: [Windows] in case of exception disk_usage() now also shows the path name. - 1422_: [Windows] Windows APIs requiring to be dynamically loaded from DLL diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 54b401d31d..e9db62a5a5 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1019,93 +1019,31 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { /* * Resume or suspends a process */ -int -psutil_proc_suspend_or_resume(DWORD pid, int suspend) { - // a huge thanks to http://www.codeproject.com/KB/threads/pausep.aspx - HANDLE hThreadSnap = NULL; - HANDLE hThread; - THREADENTRY32 te32 = {0}; - - if (pid == 0) { - AccessDenied(""); - return FALSE; - } - - hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); - if (hThreadSnap == INVALID_HANDLE_VALUE) { - PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); - return FALSE; - } - - // Fill in the size of the structure before using it - te32.dwSize = sizeof(THREADENTRY32); - - if (! Thread32First(hThreadSnap, &te32)) { - PyErr_SetFromOSErrnoWithSyscall("Thread32First"); - CloseHandle(hThreadSnap); - return FALSE; - } - - // Walk the thread snapshot to find all threads of the process. - // If the thread belongs to the process, add its information - // to the display list. - do { - if (te32.th32OwnerProcessID == pid) { - hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, - te32.th32ThreadID); - if (hThread == NULL) { - PyErr_SetFromOSErrnoWithSyscall("OpenThread"); - CloseHandle(hThread); - CloseHandle(hThreadSnap); - return FALSE; - } - if (suspend == 1) { - if (SuspendThread(hThread) == (DWORD) - 1) { - PyErr_SetFromOSErrnoWithSyscall("SuspendThread"); - CloseHandle(hThread); - CloseHandle(hThreadSnap); - return FALSE; - } - } - else { - if (ResumeThread(hThread) == (DWORD) - 1) { - PyErr_SetFromOSErrnoWithSyscall("ResumeThread"); - CloseHandle(hThread); - CloseHandle(hThreadSnap); - return FALSE; - } - } - CloseHandle(hThread); - } - } while (Thread32Next(hThreadSnap, &te32)); - - CloseHandle(hThreadSnap); - return TRUE; -} - - static PyObject * -psutil_proc_suspend(PyObject *self, PyObject *args) { +psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { long pid; - int suspend = 1; + int ret; + HANDLE hProcess; + PyObject* suspend; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, "lO", &pid, &suspend)) return NULL; - if (! psutil_proc_suspend_or_resume(pid, suspend)) - return NULL; - Py_RETURN_NONE; -} + hProcess = psutil_handle_from_pid(pid, PROCESS_SUSPEND_RESUME); + if (hProcess == NULL) + return NULL; -static PyObject * -psutil_proc_resume(PyObject *self, PyObject *args) { - long pid; - int suspend = 0; + if (PyObject_IsTrue(suspend)) + ret = psutil_NtSuspendProcess(hProcess); + else + ret = psutil_NtResumeProcess(hProcess); - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (! psutil_proc_suspend_or_resume(pid, suspend)) + if (ret != 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); return NULL; + } + CloseHandle(hProcess); Py_RETURN_NONE; } @@ -2060,8 +1998,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { /* - * Return True if one of the process threads is in a waiting or - * suspended status. + * Return True if all process threads are in waiting/suspended state. */ static PyObject * psutil_proc_is_suspended(PyObject *self, PyObject *args) { @@ -2072,9 +2009,8 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - if (! psutil_get_proc_info(pid, &process, &buffer)) { + if (! psutil_get_proc_info(pid, &process, &buffer)) return NULL; - } for (i = 0; i < process->NumberOfThreads; i++) { if (process->Threads[i].ThreadState != Waiting || process->Threads[i].WaitReason != Suspended) @@ -3430,7 +3366,8 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { static PyMethodDef PsutilMethods[] = { // --- per-process functions - {"proc_cmdline", (PyCFunction)(void(*)(void))psutil_proc_cmdline, METH_VARARGS | METH_KEYWORDS, + {"proc_cmdline", (PyCFunction)(void(*)(void))psutil_proc_cmdline, + METH_VARARGS | METH_KEYWORDS, "Return process cmdline as a list of cmdline arguments"}, {"proc_environ", psutil_proc_environ, METH_VARARGS, "Return process environment data"}, @@ -3451,10 +3388,8 @@ PsutilMethods[] = { "Return the USS of the process"}, {"proc_cwd", psutil_proc_cwd, METH_VARARGS, "Return process current working directory"}, - {"proc_suspend", psutil_proc_suspend, METH_VARARGS, - "Suspend a process"}, - {"proc_resume", psutil_proc_resume, METH_VARARGS, - "Resume a process"}, + {"proc_suspend_or_resume", psutil_proc_suspend_or_resume, METH_VARARGS, + "Suspend or resume a process"}, {"proc_open_files", psutil_proc_open_files, METH_VARARGS, "Return files opened by process"}, {"proc_username", psutil_proc_username, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 0441b8a17e..0ab8afe4b9 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -913,11 +913,11 @@ def cpu_times(self): @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 def cwd(self): diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index df1c752894..6134687de3 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -117,6 +117,16 @@ psutil_loadlibs() { if (! psutil_RtlGetVersion) return 1; + psutil_NtSuspendProcess = psutil_GetProcAddressFromLib( + "ntdll", "NtSuspendProcess"); + if (! psutil_NtSuspendProcess) + return 1; + + psutil_NtResumeProcess = psutil_GetProcAddressFromLib( + "ntdll", "NtResumeProcess"); + if (! psutil_NtResumeProcess) + return 1; + /* * Optional. */ diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 1f4951faa7..1b1d00f818 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -62,3 +62,9 @@ _GetLogicalProcessorInformationEx \ _RtlGetVersion \ psutil_RtlGetVersion; + +_NtSuspendProcess \ + psutil_NtSuspendProcess; + +_NtResumeProcess \ + psutil_NtResumeProcess; diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index d6030caa26..8007ec01c5 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -455,4 +455,12 @@ typedef NTSTATUS (WINAPI *_RtlGetVersion) ( PRTL_OSVERSIONINFOW lpVersionInformation ); +typedef NTSTATUS (WINAPI *_NtResumeProcess) ( + HANDLE hProcess +); + +typedef NTSTATUS (WINAPI *_NtSuspendProcess) ( + HANDLE hProcess +); + #endif // __NTEXTAPI_H__ diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 5e0b94217b..1cfd3b5cd7 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -222,17 +222,21 @@ psutil_is_phandle_running(HANDLE hProcess, DWORD pid) { HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid) { int ret = psutil_is_phandle_running(hProcess, pid); - if (ret == 1) + if (ret == 1) { return hProcess; - else if (ret == 0) + } + else if (ret == 0) { return NoSuchProcess(""); - else if (ret == -1) + } + else if (ret == -1) { if (GetLastError() == ERROR_ACCESS_DENIED) return PyErr_SetFromWindowsErr(0); else return PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); - else // -2 + } + else { return NULL; + } } @@ -244,15 +248,16 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid) { * Return a process handle or NULL. */ HANDLE -psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess) { +psutil_handle_from_pid(DWORD pid, DWORD access) { HANDLE hProcess; if (pid == 0) { // otherwise we'd get NoSuchProcess return AccessDenied(""); } - - hProcess = OpenProcess(dwDesiredAccess, FALSE, pid); + // needed for GetExitCodeProcess + access |= PROCESS_QUERY_LIMITED_INFORMATION; + hProcess = OpenProcess(access, FALSE, pid); return psutil_check_phandle(hProcess, pid); } From af4e8353def6eba3dc1d6920d1238765ceaec317 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 00:54:00 +0100 Subject: [PATCH 0171/1714] make README a bit more terse --- README.rst | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 03be872d8c..95b1e1d7a9 100644 --- a/README.rst +++ b/README.rst @@ -125,6 +125,7 @@ CPU .. code-block:: python >>> import psutil + >>> >>> psutil.cpu_times() scputimes(user=3961.46, nice=169.729, system=2150.659, idle=16900.540, iowait=629.59, irq=0.0, softirq=19.42, steal=0.0, guest=0, nice=0.0) >>> @@ -166,7 +167,6 @@ Memory .. code-block:: python - >>> import psutil >>> psutil.virtual_memory() svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304) >>> psutil.swap_memory() @@ -178,7 +178,6 @@ Disks .. code-block:: python - >>> import psutil >>> psutil.disk_partitions() [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext, opts='rw')] @@ -195,7 +194,6 @@ Network .. code-block:: python - >>> import psutil >>> psutil.net_io_counters(pernic=True) {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0), 'lo': netio(bytes_sent=2838627, bytes_recv=2838627, packets_sent=30567, packets_recv=30567, errin=0, errout=0, dropin=0, dropout=0)} @@ -203,8 +201,6 @@ Network >>> psutil.net_connections() [sconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254), sconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING', pid=2987), - sconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status='ESTABLISHED', pid=None), - sconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status='SYN_SENT', pid=None) ...] >>> >>> psutil.net_if_addrs() @@ -216,8 +212,8 @@ Network snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> >>> psutil.net_if_stats() - {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500), - 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536)} + {'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536), + 'wlan0': snicstats(isup=True, duplex=, speed=100, mtu=1500)} >>> Sensors @@ -230,10 +226,7 @@ Sensors {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], 'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), - shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0), - shwtemp(label='Core 1', current=52.0, high=100.0, critical=100.0), - shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), - shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} + shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0)]} >>> >>> psutil.sensors_fans() {'asus': [sfan(label='cpu_fan', current=3200)]} @@ -282,11 +275,16 @@ Process management 7055 >>> p.ppid() 7054 + >>> p.children(recursive=True) + [psutil.Process(pid=29835, name='python2.7', started='11:45:38'), + psutil.Process(pid=29836, name='python2.7', started='11:43:39')] + >>> >>> p.parent() - - >>> p.children() - [, - ] + psutil.Process(pid=4699, name='bash', started='09:06:44') + >>> p.parents() + [psutil.Process(pid=4699, name='bash', started='09:06:44'), + psutil.Process(pid=4689, name='gnome-terminal-server', started='0:06:44'), + psutil.Process(pid=1, name='systemd', started='05:56:55')] >>> >>> p.status() 'running' @@ -321,7 +319,6 @@ Process management >>> p.memory_maps() [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), - pmmap_grouped(path='/lib/x8664-linux-gnu/libcrypto.so.0.1', rss=34124, rss=32768, size=2134016, pss=15360, shared_clean=24576, shared_dirty=0, private_clean=0, private_dirty=8192, referenced=24576, anonymous=8192, swap=0), pmmap_grouped(path='[heap]', rss=32768, size=139264, pss=32768, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=32768, referenced=32768, anonymous=32768, swap=0), pmmap_grouped(path='[stack]', rss=2465792, size=2494464, pss=2465792, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=2465792, referenced=2277376, anonymous=2465792, swap=0), ...] @@ -335,9 +332,7 @@ Process management >>> >>> p.connections() [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING'), - pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status='ESTABLISHED'), - pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status='SYN_SENT')] + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')] >>> >>> p.num_threads() 4 @@ -345,8 +340,6 @@ Process management 8 >>> p.threads() [pthread(id=5234, user_time=22.5, system_time=9.2891), - pthread(id=5235, user_time=0.0, system_time=0.0), - pthread(id=5236, user_time=0.0, system_time=0.0), pthread(id=5237, user_time=0.0707, system_time=1.1)] >>> >>> p.num_ctx_switches() @@ -366,7 +359,7 @@ Process management >>> >>> p.environ() {'LC_PAPER': 'it_IT.UTF-8', 'SHELL': '/bin/bash', 'GREP_OPTIONS': '--color=auto', - 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'COLORTERM': 'gnome-terminal', + 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', ...} >>> >>> p.as_dict() @@ -384,7 +377,6 @@ Process management USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND root 1 0.0 0.0 24584 2240 Jun17 00:00 init root 2 0.0 0.0 0 0 Jun17 00:00 kthreadd - root 3 0.0 0.0 0 0 Jun17 00:05 ksoftirqd/0 ... giampaolo 31475 0.0 0.0 20760 3024 /dev/pts/0 Jun19 00:00 python2.4 giampaolo 31721 0.0 2.2 773060 181896 00:04 10:30 chrome From d912cd584f354c2e660c3331ab63fd8e53c1ecf0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 01:37:17 +0100 Subject: [PATCH 0172/1714] fix #1414: linux test failure because of sensors_batter() exc on import --- psutil/tests/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index fac9083beb..b6f76a2950 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -172,7 +172,10 @@ HAS_RLIMIT = hasattr(psutil.Process, "rlimit") HAS_THREADS = hasattr(psutil.Process, "threads") HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") -HAS_BATTERY = HAS_SENSORS_BATTERY and bool(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") From 96091c266e9ab09995ad027c069cb8ade771e6c7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 02:04:52 +0100 Subject: [PATCH 0173/1714] #1353: make process_iter() thread-safe --- HISTORY.rst | 1 + psutil/__init__.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3addc0270e..c553086b19 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,7 @@ XXXX-XX-XX **Bug fixes** +- 1353_: process_iter() is now thread safe (it rarely raised TypeError). - 1394_: [Windows] Process name() and exe() may erronously return "Registry". QueryFullProcessImageNameW is now used instead of GetProcessImageFileNameW in order to prevent that. diff --git a/psutil/__init__.py b/psutil/__init__.py index 0aa13c7559..70efc5af08 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1503,6 +1503,7 @@ def pid_exists(pid): _pmap = {} +_lock = threading.Lock() def process_iter(attrs=None, ad_value=None): @@ -1530,21 +1531,26 @@ 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 + with _lock: + _pmap[proc.pid] = proc return proc def remove(pid): - _pmap.pop(pid, None) + with _lock: + _pmap.pop(pid, None) a = set(pids()) 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())): + + with _lock: + 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) From f240d984c8428dfad417ef516b837e1e7bb2769f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 15:35:41 +0100 Subject: [PATCH 0174/1714] #1433, #1379: fix parents() method (infinite loop) --- psutil/__init__.py | 27 +++++++++++++++++++++++---- psutil/tests/test_posix.py | 2 +- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 70efc5af08..2916764a4e 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -45,6 +45,7 @@ from ._common import wrap_numbers as _wrap_numbers from ._compat import long from ._compat import PY3 as _PY3 +from ._compat import lru_cache from ._common import STATUS_DEAD from ._common import STATUS_DISK_SLEEP @@ -396,6 +397,11 @@ def _pprint_secs(secs): return datetime.datetime.fromtimestamp(secs).strftime(fmt) +@lru_cache() +def _first_pid(): + return sorted(pids())[0] + + # ===================================================================== # --- Process class # ===================================================================== @@ -660,11 +666,24 @@ def parents(self): """Return the parents of this process as a list of Process instances. If no parents are known return an empty list. """ + first_pid = _first_pid() parents = [] proc = self.parent() - while proc is not None: - parents.append(proc) - proc = proc.parent() + while True: + if proc is None: + break + elif proc.pid == first_pid: + # Needed because on certain systems such as macOS + # Process(0).ppid() returns 0. + parents.append(proc) + break + else: + par = proc.parent() + if par is None: + break + assert par.pid <= proc.pid, (par.pid, proc.pid) + parents.append(proc) + proc = par return parents def is_running(self): @@ -2475,7 +2494,7 @@ def test(): # pragma: no cover p.info['name'].strip() or '?')) -del memoize, memoize_when_activated, division, deprecated_method +del memoize, memoize_when_activated, division, deprecated_method, lru_cache if sys.version_info[0] < 3: del num, x diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 5a8fdc1731..955a483485 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -288,7 +288,7 @@ def call(p, attr): failures = [] ignored_names = ['terminate', 'kill', 'suspend', 'resume', 'nice', 'send_signal', 'wait', 'children', 'as_dict', - 'memory_info_ex'] + '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): From 9f14dd4d7c0cce154c5b13728d69a77c58103c44 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 15:36:52 +0100 Subject: [PATCH 0175/1714] fix <= logic --- psutil/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 2916764a4e..b5a584f2d3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -681,7 +681,7 @@ def parents(self): par = proc.parent() if par is None: break - assert par.pid <= proc.pid, (par.pid, proc.pid) + assert par.pid < proc.pid, (par.pid, proc.pid) parents.append(proc) proc = par return parents From 31324467736c9a0fc63f47f11c6810dee23f05e2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 15:56:50 +0100 Subject: [PATCH 0176/1714] fix #1437: return pids() in sorted order --- HISTORY.rst | 1 + docs/index.rst | 8 ++++++-- psutil/__init__.py | 4 ++-- psutil/tests/test_posix.py | 4 +--- psutil/tests/test_system.py | 8 ++++---- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c553086b19..9fcc72f862 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ XXXX-XX-XX - 1426_: [Windows] PAGESIZE and number of processors is now calculated on startup. - 1433_: new Process.parents() method. (idea by Ghislain Le Meur) +- 1437_: pids() are returned in sorted order. **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 8983e88039..a6f2ba731f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -817,13 +817,17 @@ Functions .. function:: pids() - Return a list of current running PIDs. To iterate over all processes - and avoid race conditions :func:`process_iter()` should be preferred. + Return a sorted list of current running PIDs. + To iterate over all processes and avoid race conditions :func:`process_iter()` + should be preferred. >>> import psutil >>> psutil.pids() [1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, ..., 32498] + .. versionchanged:: + 5.6.0 PIDs are returned in sorted order + .. function:: process_iter(attrs=None, ad_value=None) Return an iterator yielding a :class:`Process` class instance for all running diff --git a/psutil/__init__.py b/psutil/__init__.py index b5a584f2d3..1893298b05 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -399,7 +399,7 @@ def _pprint_secs(secs): @lru_cache() def _first_pid(): - return sorted(pids())[0] + return pids()[0] # ===================================================================== @@ -1500,7 +1500,7 @@ def wait(self, timeout=None): def pids(): """Return a list of current running PIDs.""" - return _psplatform.pids() + return sorted(_psplatform.pids()) def pid_exists(pid): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 955a483485..15d2f12783 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -321,10 +321,8 @@ class TestSystemAPIs(unittest.TestCase): def test_pids(self): # Note: this test might fail if the OS is starting/killing # other processes in the meantime - pids_ps = ps("pid") + pids_ps = sorted(ps("pid")) pids_psutil = psutil.pids() - pids_ps.sort() - pids_psutil.sort() # on MACOS and OPENBSD ps doesn't show pid 0 if MACOS or OPENBSD and 0 not in pids_ps: diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index b0b7e4f098..1236af3b64 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -242,11 +242,11 @@ def test_pid_exists_2(self): self.assertFalse(psutil.pid_exists(pid), msg=pid) def test_pids(self): - plist = [x.pid for x in psutil.process_iter()] - pidlist = psutil.pids() - self.assertEqual(plist.sort(), pidlist.sort()) + pidslist = psutil.pids() + procslist = [x.pid for x in psutil.process_iter()] # make sure every pid is unique - self.assertEqual(len(pidlist), len(set(pidlist))) + self.assertEqual(sorted(set(pidslist)), pidslist) + self.assertEqual(pidslist, procslist) def test_test(self): # test for psutil.test() function From 62e5b208938bb0ce1b533dced5cf566ded754452 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 17:22:15 +0100 Subject: [PATCH 0177/1714] remove assertion --- docs/index.rst | 3 +++ psutil/__init__.py | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index a6f2ba731f..012f18aeda 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1205,6 +1205,9 @@ Process class .. versionadded:: 5.6.0 + .. warning:: + this API is experimental and may be removed if deemed necessary. + .. method:: status() The current process status as a string. The returned string is one of the diff --git a/psutil/__init__.py b/psutil/__init__.py index 1893298b05..513b2a7a75 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -398,7 +398,7 @@ def _pprint_secs(secs): @lru_cache() -def _first_pid(): +def _lowest_pid(): return pids()[0] @@ -666,13 +666,13 @@ def parents(self): """Return the parents of this process as a list of Process instances. If no parents are known return an empty list. """ - first_pid = _first_pid() + lowest = _lowest_pid() # 1 if LINUX else 0 parents = [] proc = self.parent() while True: if proc is None: break - elif proc.pid == first_pid: + elif proc.pid == lowest: # Needed because on certain systems such as macOS # Process(0).ppid() returns 0. parents.append(proc) @@ -681,7 +681,6 @@ def parents(self): par = proc.parent() if par is None: break - assert par.pid < proc.pid, (par.pid, proc.pid) parents.append(proc) proc = par return parents From 682fae3600c830905c4c57341670c7c097ec6a33 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 18:57:23 +0100 Subject: [PATCH 0178/1714] #fix 1438: do not return any parent() for PID 0 + update doc --- CREDITS | 2 +- docs/index.rst | 20 ++++++++---------- psutil/__init__.py | 40 +++++++++++++++--------------------- psutil/tests/test_process.py | 21 +++++++++++++++++++ 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/CREDITS b/CREDITS index ea21c24ac9..d2477d42b9 100644 --- a/CREDITS +++ b/CREDITS @@ -588,5 +588,5 @@ I: 1360 N: Ghislain Le Meur W: https://github.com/gigi206 -D: Process.parents() method +D: idea for Process.parents() I: 1379 diff --git a/docs/index.rst b/docs/index.rst index 012f18aeda..78a88f970c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -982,19 +982,18 @@ Process class On Linux *pid* can also refer to a thread ID (the *id* field returned by :meth:`threads` method). When accessing methods of this class always be prepared to catch - :class:`NoSuchProcess`, :class:`ZombieProcess` and :class:`AccessDenied` - exceptions. + :class:`NoSuchProcess` and :class:`AccessDenied` exceptions. `hash() `__ builtin can be used against instances of this class in order to identify a process univocally over time (the hash is determined by mixing process PID - and creation time). As such it can also be used with + + creation time). As such it can also be used with `set()s `__. .. note:: In order to efficiently fetch more than one information about the process - at the same time, make sure to use either :meth:`as_dict` or - :meth:`oneshot` context manager. + at the same time, make sure to use either :meth:`oneshot` context manager + or :meth:`as_dict` utility method. .. note:: @@ -1009,6 +1008,7 @@ Process class :meth:`rlimit` (set), :meth:`children`, :meth:`parent`, + :meth:`parents`, :meth:`suspend` :meth:`resume`, :meth:`send_signal`, @@ -1108,7 +1108,7 @@ Process class call. Not on POSIX because `ppid may change `__ if process becomes a zombie. - See also :meth:`parent` method. + See also :meth:`parent` and :meth:`parents` methods. .. method:: name() @@ -1194,20 +1194,18 @@ Process class .. method:: parent() Utility method which returns the parent process as a :class:`Process` - object preemptively checking whether PID has been reused. If no parent + object, preemptively checking whether PID has been reused. If no parent PID is known return ``None``. - See also :meth:`ppid` method. + See also :meth:`ppid` and :meth:`parents` methods. .. method:: parents() Utility method which return the parents of this process as a list of :class:`Process` instances. If no parents are known return an empty list. + See also :meth:`ppid` and :meth:`parent` methods. .. versionadded:: 5.6.0 - .. warning:: - this API is experimental and may be removed if deemed necessary. - .. method:: status() The current process status as a string. The returned string is one of the diff --git a/psutil/__init__.py b/psutil/__init__.py index 513b2a7a75..447e507b79 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -45,7 +45,6 @@ from ._common import wrap_numbers as _wrap_numbers from ._compat import long from ._compat import PY3 as _PY3 -from ._compat import lru_cache from ._common import STATUS_DEAD from ._common import STATUS_DISK_SLEEP @@ -214,15 +213,19 @@ # "sensors_temperatures", "sensors_battery", "sensors_fans" # sensors "users", "boot_time", # others ] + + __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" __version__ = "5.6.0" version_info = tuple([int(num) for num in __version__.split('.')]) + +_timer = getattr(time, 'monotonic', time.time) 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) +_LOWEST_PID = None # Sanity check in case the user messed up with psutil installation # or did something weird with sys.path. In this case we might end @@ -397,11 +400,6 @@ def _pprint_secs(secs): return datetime.datetime.fromtimestamp(secs).strftime(fmt) -@lru_cache() -def _lowest_pid(): - return pids()[0] - - # ===================================================================== # --- Process class # ===================================================================== @@ -651,6 +649,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() @@ -666,23 +667,11 @@ def parents(self): """Return the parents of this process as a list of Process instances. If no parents are known return an empty list. """ - lowest = _lowest_pid() # 1 if LINUX else 0 parents = [] proc = self.parent() - while True: - if proc is None: - break - elif proc.pid == lowest: - # Needed because on certain systems such as macOS - # Process(0).ppid() returns 0. - parents.append(proc) - break - else: - par = proc.parent() - if par is None: - break - parents.append(proc) - proc = par + while proc is not None: + parents.append(proc) + proc = proc.parent() return parents def is_running(self): @@ -1499,7 +1488,10 @@ def wait(self, timeout=None): def pids(): """Return a list of current running PIDs.""" - return sorted(_psplatform.pids()) + global _LOWEST_PID + ret = sorted(_psplatform.pids()) + _LOWEST_PID = ret[0] + return ret def pid_exists(pid): @@ -2493,7 +2485,7 @@ def test(): # pragma: no cover p.info['name'].strip() or '?')) -del memoize, memoize_when_activated, division, deprecated_method, lru_cache +del memoize, memoize_when_activated, division, deprecated_method if sys.version_info[0] < 3: del num, x diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index bfbe9bc042..f9a91e3666 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1046,6 +1046,9 @@ def test_parent(self): 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) @@ -1062,9 +1065,14 @@ def test_parent_disappeared(self): 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()) + 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() @@ -1112,6 +1120,19 @@ def test_children_duplicates(self): 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) From afa8df7aa694f067c2979fda8933a2b589580fa8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 19:13:38 +0100 Subject: [PATCH 0179/1714] refactor README a bit --- CREDITS | 4 ---- README.rst | 57 +++++++++++++++++++++++++++++------------------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/CREDITS b/CREDITS index d2477d42b9..77d2c03a59 100644 --- a/CREDITS +++ b/CREDITS @@ -10,7 +10,6 @@ A big thanks to all of you. - Giampaolo - Author ====== @@ -19,7 +18,6 @@ C: Italy E: g.rodola@gmail.com W: http://grodola.blogspot.com/ - Experts ======= @@ -46,7 +44,6 @@ Github usernames of people to CC on github when in need of help. - AIX: - wiggin15, Arnon Yaari (maintainer) - Top contributors ================ @@ -95,7 +92,6 @@ W: https://github.com/ryoon D: NetBSD implementation (co-author). I: 557 - Contributors ============ diff --git a/README.rst b/README.rst index 95b1e1d7a9..0f26fbcf4c 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ :target: https://pypi.org/project/psutil :alt: License -=========== + Quick links =========== @@ -44,7 +44,7 @@ Quick links - `Development guide `_ - `What's new `_ -======= + Summary ======= @@ -67,7 +67,25 @@ psutil currently supports the following platforms: ...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and 3.4+**. `PyPy `__ is also known to work. -==================== + +Author +====== + +psutil was created and is maintained by +`Giampaolo Rodola `__ and it +received many useful `contributions `__ +over the years. +A lot of time and effort went into making psutil as it is right now. +If you feel psutil is useful to you or your business and want to support its +future development consider making a small donation: + +.. image:: http://www.paypal.com/en_US/i/btn/x-click-but04.gif + :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 + :alt: Donate via PayPal + +Don't want to donate money? Then maybe you could `write me a recommendation on Linkedin `_. + + Example applications ==================== @@ -82,7 +100,6 @@ Example applications Also see `scripts directory `__ and `doc recipes `__. -===================== Projects using psutil ===================== @@ -93,18 +110,18 @@ psutil has roughly the following monthly downloads: :alt: Downloads There are over -`10000 open source projects `__ +`10.000 open source projects `__ on github which depend from psutil. Here's some I find particularly interesting: +- https://github.com/google/grr - https://github.com/facebook/osquery/ - https://github.com/nicolargo/glances -- https://github.com/google/grr - https://github.com/Jahaja/psdash - https://github.com/ajenti/ajenti - https://github.com/home-assistant/home-assistant/ -======== + Portings ======== @@ -115,10 +132,12 @@ Portings - Ruby: https://github.com/spacewander/posixpsutil - Nim: https://github.com/johnscillieri/psutil-nim -============== + Example usages ============== +This represents pretty much the whole psutil API. + CPU === @@ -262,6 +281,8 @@ Process management 5167, 5234, 5235, 5252, 5318, 5424, 5644, 6987, 7054, 7055, 7071] >>> >>> p = psutil.Process(7055) + >>> p + psutil.Process(pid=7055, name='python', started='09:04:44') >>> p.name() 'python' >>> p.exe() @@ -449,21 +470,5 @@ Windows services Other samples ============= -See `doc recipes `__. - -====== -Author -====== - -psutil was created and is maintained by -`Giampaolo Rodola' `__. -A lot of time and effort went into making psutil as it is right now. -If you feel psutil is useful to you or your business and want to support its -future development please consider donating me -(`Giampaolo `__) some money. - -.. image:: http://www.paypal.com/en_US/i/btn/x-click-but04.gif - :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 - :alt: Donate via PayPal - -Don't want to donate money? Then maybe you could `write me a recommendation on Linkedin `_. +See `doc recipes `__ and +`demo scritps `__. From 9fd71777ba6aa93450a7be64f0d743d27445fdba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 10:35:38 -0800 Subject: [PATCH 0180/1714] fix backslash warnings --- psutil/tests/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b6f76a2950..545f5f2519 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -203,6 +203,9 @@ def attempt(exe): 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 From 63235743d51f5cdef5315c430a87c6bf8da9dbf3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 11:02:19 -0800 Subject: [PATCH 0181/1714] fix win tests --- psutil/tests/test_process.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index f9a91e3666..e8f03f7aaf 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1068,10 +1068,13 @@ def test_parents(self): self.assertEqual(p1.parents()[0], psutil.Process()) self.assertEqual(p2.parents()[0], p1) self.assertEqual(p2.parents()[1], psutil.Process()) - - lowest_pid = psutil.pids()[0] - self.assertEqual(p1.parents()[-1].pid, lowest_pid) - self.assertEqual(p2.parents()[-1].pid, lowest_pid) + if POSIX: + lowest_pid = psutil.pids()[0] + self.assertEqual(p1.parents()[-1].pid, lowest_pid) + self.assertEqual(p2.parents()[-1].pid, lowest_pid) + else: + self.assertEqual(p1.parents()[-1].name(), "explorer.exe") + self.assertEqual(p2.parents()[-1].name(), "explorer.exe") def test_children(self): reap_children(recursive=True) From ed9653aa4835289f3bf64b7956dfcc739f2a216b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 11:35:16 -0800 Subject: [PATCH 0182/1714] fix win test --- psutil/tests/test_process.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index e8f03f7aaf..7380b60c73 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1072,9 +1072,6 @@ def test_parents(self): lowest_pid = psutil.pids()[0] self.assertEqual(p1.parents()[-1].pid, lowest_pid) self.assertEqual(p2.parents()[-1].pid, lowest_pid) - else: - self.assertEqual(p1.parents()[-1].name(), "explorer.exe") - self.assertEqual(p2.parents()[-1].name(), "explorer.exe") def test_children(self): reap_children(recursive=True) From 4faef5bc430080e66a8254e0afb8951dc1931ebf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Feb 2019 16:00:13 -0800 Subject: [PATCH 0183/1714] add win tests related to send_signal(CTRL_C_EVENT) #1227 --- psutil/tests/test_process.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 7380b60c73..16c0ba4ace 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1256,12 +1256,16 @@ def test_halfway_terminated_process(self): p.wait() if WINDOWS: call_until(psutil.pids, "%s not in ret" % p.pid) - self.assertFalse(p.is_running()) - # self.assertFalse(p.pid in psutil.pids(), msg="retcode = %s" % - # retcode) + 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'] + 'oneshot'] if LINUX and not HAS_RLIMIT: excluded_names.append('rlimit') for name in dir(p): From d180e25b871c314614aa96eccadb0bcc0a5df676 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 01:39:48 +0100 Subject: [PATCH 0184/1714] fix NetBSD: Process.connections() may return incomplete results if using oneshot() #1439 --- HISTORY.rst | 2 ++ psutil/_psbsd.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9fcc72f862..26eff418b3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -37,6 +37,8 @@ XXXX-XX-XX and Process memory_maps() and memory_info_exe() ("uss" field). - 1432_: [Windows] Process.memory_info_ex()'s USS memory is miscalculated because we're not using the actual system PAGESIZE. +- 1439_: [NetBSD] Process.connections() may return incomplete results if using + oneshot() 5.5.1 ===== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 0581de2997..a5c5a14399 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -807,9 +807,9 @@ def connections(self, kind='inet'): 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 + # in case the process is gone (and the returned list may be + # incomplete). Raise NSP if the process disappeared on us. + cext.proc_name(self.pid) return ret @wrap_exceptions From d4d896512d01c7c14a4c145232e1c6bb6f4c3631 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 01:50:59 +0100 Subject: [PATCH 0185/1714] refactor --- psutil/_psbsd.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index a5c5a14399..92864cea6e 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -605,6 +605,12 @@ def __init__(self, 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) + @memoize_when_activated def oneshot(self): """Retrieves multiple process info in one shot as a raw tuple.""" @@ -751,10 +757,7 @@ 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 @@ -784,10 +787,7 @@ def connections(self, kind='inet'): 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 + self._assert_alive() return list(ret) families, types = conn_tmap[kind] @@ -806,10 +806,7 @@ def connections(self, kind='inet'): nt = _common.pconn(fd, fam, type, laddr, raddr, status) 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 be - # incomplete). Raise NSP if the process disappeared on us. - cext.proc_name(self.pid) + self._assert_alive() return ret @wrap_exceptions @@ -885,9 +882,7 @@ 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 From a5d179e0d97eeeb8112ca2bf72f846bd1ca2b161 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 02:01:10 +0100 Subject: [PATCH 0186/1714] _assert_alive() refactor (linux) --- psutil/_pslinux.py | 20 +++++++++++--------- psutil/_pssunos.py | 18 ++++++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 011b354168..2eadc4c7dc 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1540,6 +1540,12 @@ 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)) + @memoize_when_activated def _parse_stat_file(self): """Parse /proc/{pid}/stat file and return a dict with various @@ -1906,8 +1912,7 @@ def threads(self): 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._assert_alive() return retlist @wrap_exceptions @@ -2070,9 +2075,8 @@ def open_files(self): 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. + # fd gone in the meantime; process may + # still be alive hit_enoent = True else: raise @@ -2082,15 +2086,13 @@ def open_files(self): 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._assert_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._assert_alive() return ret @wrap_exceptions diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index faadecfe65..17469eacc6 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -380,6 +380,12 @@ 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) self._proc_basic_info.cache_activate(self) @@ -522,8 +528,7 @@ def terminal(self): continue raise 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): @@ -585,8 +590,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 @@ -610,8 +614,7 @@ def open_files(self): 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): @@ -711,8 +714,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 From d6f5cc7eca2dad63acaba5db6ac846b7b986443f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 02:06:58 +0100 Subject: [PATCH 0187/1714] move access_denied script --- Makefile | 22 ++++++----- ...ccess_denied.py => print_access_denied.py} | 38 ++++++++++++++++++- scripts/internal/winmake.py | 2 +- 3 files changed, 50 insertions(+), 12 deletions(-) rename scripts/internal/{procs_access_denied.py => print_access_denied.py} (56%) diff --git a/Makefile b/Makefile index 744805187f..314415a4e6 100644 --- a/Makefile +++ b/Makefile @@ -236,17 +236,25 @@ release: ## Create a release (down/uploads tar.gz, wheels, git tag release). $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release +check-manifest: ## Inspect MANIFEST.in file. + $(PYTHON) -m check_manifest -v $(ARGS) + +generate-manifest: ## Generates MANIFEST.in file. + $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in + +# =================================================================== +# Printers +# =================================================================== + print-announce: ## Print announce of new release. @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_announce.py print-timeline: ## Print releases' timeline. @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_timeline.py -check-manifest: ## Inspect MANIFEST.in file. - $(PYTHON) -m check_manifest -v $(ARGS) - -generate-manifest: ## Generates MANIFEST.in file. - $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in +print-access-denied: +# ${MAKE} install + $(TEST_PREFIX) $(PYTHON) scripts/internal/procs_access_denied.py # =================================================================== # Misc @@ -266,9 +274,5 @@ bench-oneshot-2: ## Same as above but using perf module (supposed to be more pr check-broken-links: ## Look for broken links in source files. git ls-files | xargs $(PYTHON) -Wa scripts/internal/check_broken_links.py -print-access-denied: -# ${MAKE} install - $(TEST_PREFIX) $(PYTHON) scripts/internal/procs_access_denied.py - help: ## Display callable targets. @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/scripts/internal/procs_access_denied.py b/scripts/internal/print_access_denied.py similarity index 56% rename from scripts/internal/procs_access_denied.py rename to scripts/internal/print_access_denied.py index 9f792480bc..8381928d3e 100644 --- a/scripts/internal/procs_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -5,9 +5,43 @@ # found in the LICENSE file. """ -Helper script which tries to access all info of all running processes. +Helper script iterates over all processes and . It prints how many AccessDenied exceptions are raised in total and -for each Process method. +for what Process method. + +$ make print-access-denied +username 0 0.0% SUCCESS +cpu_num 0 0.0% SUCCESS +num_ctx_switches 0 0.0% SUCCESS +pid 0 0.0% SUCCESS +cmdline 0 0.0% SUCCESS +create_time 0 0.0% SUCCESS +ionice 0 0.0% SUCCESS +cpu_percent 0 0.0% SUCCESS +terminal 0 0.0% SUCCESS +ppid 0 0.0% SUCCESS +nice 0 0.0% SUCCESS +status 0 0.0% SUCCESS +cpu_times 0 0.0% SUCCESS +memory_info 0 0.0% SUCCESS +threads 0 0.0% SUCCESS +uids 0 0.0% SUCCESS +num_threads 0 0.0% SUCCESS +name 0 0.0% SUCCESS +gids 0 0.0% SUCCESS +cpu_affinity 0 0.0% SUCCESS +memory_percent 0 0.0% SUCCESS +memory_full_info 70 20.8% ACCESS DENIED +memory_maps 70 20.8% ACCESS DENIED +exe 174 51.8% ACCESS DENIED +connections 237 70.5% ACCESS DENIED +num_fds 237 70.5% ACCESS DENIED +cwd 237 70.5% ACCESS DENIED +io_counters 237 70.5% ACCESS DENIED +open_files 237 70.5% ACCESS DENIED +environ 237 70.5% ACCESS DENIED +-------------------------- +total: 1736 (336 total processes) """ from __future__ import print_function, division diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 49aae699f9..1f4fa8f21d 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -487,7 +487,7 @@ def bench_oneshot_2(): def print_access_denied(): """Benchmarks for oneshot() ctx manager (see #799).""" install() - sh("%s -Wa scripts\\internal\\procs_access_denied.py" % PYTHON) + sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) def set_python(s): From 0a28fa0cb19e7af2cf03325a22340fa0d2d0ad18 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 03:13:15 +0100 Subject: [PATCH 0188/1714] add script for to benchmark API calls --- Makefile | 8 ++- scripts/internal/print_api_speed.py | 92 +++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 scripts/internal/print_api_speed.py diff --git a/Makefile b/Makefile index 314415a4e6..6c64a0b5c0 100644 --- a/Makefile +++ b/Makefile @@ -252,9 +252,11 @@ print-announce: ## Print announce of new release. print-timeline: ## Print releases' timeline. @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_timeline.py -print-access-denied: -# ${MAKE} install - $(TEST_PREFIX) $(PYTHON) scripts/internal/procs_access_denied.py +print-access-denied: ## Print AD exceptions + @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_access_denied.py + +print-api-speed: ## Benchmark all API calls + @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py # =================================================================== # Misc diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py new file mode 100644 index 0000000000..24b98f1197 --- /dev/null +++ b/scripts/internal/print_api_speed.py @@ -0,0 +1,92 @@ +#!/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. + +"""Benchmark all API calls. + +$ python scripts/internal/print_api_speed.py +SYSTEM APIS + boot_time 0.000127 secs + cpu_count 0.000034 secs + cpu_count (cores) 0.000279 secs + cpu_freq 0.000438 secs + ... + +PROCESS APIS + cmdline 0.000027 secs + connections 0.000056 secs + cpu_affinity 0.000014 secs + cpu_num 0.000054 secs + cpu_percent 0.000077 secs + ... +""" + +from __future__ import print_function, division +from timeit import default_timer as timer +import os + +import psutil + + +SORT_BY_NAME = 0 # <- toggle this +timings = [] + + +def print_timings(): + timings.sort(key=lambda x: x[0 if SORT_BY_NAME else 1]) + while timings[:]: + title, elapsed = timings.pop(0) + print(" %-30s %f secs" % (title, elapsed)) + + +def timecall(title, fun, *args, **kw): + t = timer() + fun(*args, **kw) + elapsed = timer() - t + timings.append((title, elapsed)) + + +def main(): + print("SYSTEM APIS") + timecall('cpu_count', psutil.cpu_count) + timecall('cpu_count (cores)', psutil.cpu_count, logical=False) + timecall('cpu_times', psutil.cpu_times) + timecall('cpu_percent', psutil.cpu_percent, interval=0) + timecall('cpu_times_percent', psutil.cpu_times_percent, interval=0) + timecall('cpu_stats', psutil.cpu_stats) + timecall('cpu_freq', psutil.cpu_freq) + timecall('virtual_memory', psutil.virtual_memory) + timecall('swap_memory', psutil.swap_memory) + timecall('disk_partitions', psutil.disk_partitions) + timecall('disk_usage', psutil.disk_usage, os.getcwd()) + timecall('disk_io_counters', psutil.disk_io_counters) + timecall('net_io_counters', psutil.net_io_counters) + timecall('net_connections', psutil.net_connections) + timecall('net_if_addrs', psutil.net_if_addrs) + timecall('net_if_stats', psutil.net_if_stats) + timecall('sensors_temperatures', psutil.sensors_temperatures) + timecall('sensors_fans', psutil.sensors_fans) + timecall('sensors_battery', psutil.sensors_battery) + timecall('boot_time', psutil.boot_time) + timecall('users', psutil.users) + timecall('pids', psutil.pids) + timecall('pid_exists', psutil.pid_exists, os.getpid()) + timecall('process_iter (all)', lambda: list(psutil.process_iter())) + print_timings() + + print("\nPROCESS APIS") + ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', + 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', + 'pid', 'rlimit'] + p = psutil.Process() + for name in sorted(dir(p)): + if not name.startswith('_') and name not in ignore: + fun = getattr(p, name) + timecall(name, fun) + print_timings() + + +if __name__ == '__main__': + main() From e827bf46381d3d996d63e064834aa421e502d8b8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 12:18:41 +0100 Subject: [PATCH 0189/1714] OSX memory_maps() - add error handling --- psutil/_psutil_osx.c | 49 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index ed4e60d374..02b2172dac 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -344,6 +344,8 @@ psutil_proc_environ(PyObject *self, PyObject *args) { /* * Return a list of tuples for every process memory maps. * 'procstat' cmdline utility has been used as an example. + * This will fail for all processes except os.getpid(), + * even as root. */ static PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { @@ -351,40 +353,40 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { char addr_str[34]; char perms[8]; int pagesize = getpagesize(); + int iteration = 0; long pid; - kern_return_t err = KERN_SUCCESS; + kern_return_t kr; mach_port_t task = MACH_PORT_NULL; uint32_t depth = 1; vm_address_t address = 0; vm_size_t size = 0; - + struct vm_region_submap_info_64 info; + mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64; PyObject *py_tuple = NULL; PyObject *py_path = NULL; PyObject *py_list = PyList_New(0); if (py_list == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) goto error; - if (psutil_task_for_pid(pid, &task) != 0) goto error; while (1) { + iteration += 1; py_tuple = NULL; - struct vm_region_submap_info_64 info; - mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64; - - err = vm_region_recurse_64(task, &address, &size, &depth, + kr = vm_region_recurse_64(task, &address, &size, &depth, (vm_region_info_64_t)&info, &count); - if (err == KERN_INVALID_ADDRESS) { - // TODO temporary - psutil_debug("vm_region_recurse_64 returned KERN_INVALID_ADDRESS"); - break; - } - if (err != KERN_SUCCESS) { - psutil_debug("vm_region_recurse_64 returned != KERN_SUCCESS"); + if (kr != KERN_SUCCESS) { + if ((kr == KERN_INVALID_ADDRESS) && (iteration > 1)) { + break; // no more regions to read + } + PyErr_Format( + PyExc_RuntimeError, + "vm_region_recurse_64() failed: %s", + mach_error_string(kr)); + goto error; } if (info.is_submap) { @@ -408,14 +410,13 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-'); - // proc_regionfilename() return value seems meaningless - // so we do what we can in order to not continue in case - // of error. + // proc_regionfilename() is undocumented but from its source + // code we can determine that it sets errno on error and + // return length of path. errno = 0; proc_regionfilename((pid_t)pid, address, buf, sizeof(buf)); - if ((errno != 0) || ((sizeof(buf)) <= 0)) { - // TODO temporary - psutil_debug("proc_regionfilename() failed"); + if (errno != 0) { + psutil_debug("proc_regionfilename() failed errno=%i", errno); psutil_raise_for_pid(pid, "proc_regionfilename()"); goto error; } @@ -477,10 +478,10 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { goto error; Py_DECREF(py_tuple); Py_DECREF(py_path); - } - // increment address for the next map/file - address += size; + // increment address for the next map/file + address += size; + } } if (task != MACH_PORT_NULL) From b31f3bf778004f1eae6ac552dd8eb8419664ac73 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 14:32:21 +0100 Subject: [PATCH 0190/1714] #1291 / OSX: mark memory_maps() as deprecated and make it alwats raise AccessDenied --- HISTORY.rst | 5 ++ docs/index.rst | 67 +++++++------- psutil/__init__.py | 1 - psutil/_common.py | 2 +- psutil/_psaix.py | 5 -- psutil/_psosx.py | 13 +-- psutil/_psutil_osx.c | 160 --------------------------------- psutil/tests/__init__.py | 2 +- psutil/tests/test_contracts.py | 10 ++- psutil/tests/test_osx.py | 3 - psutil/tests/test_process.py | 4 +- 11 files changed, 56 insertions(+), 216 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 26eff418b3..5313a091fa 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -40,6 +40,11 @@ XXXX-XX-XX - 1439_: [NetBSD] Process.connections() may return incomplete results if using oneshot() +**API changes** + +- 1291_: [OSX] Process.memory_maps() is deprecated and will always raise + AccessDenied. It will be removed in psutil 6.0.0. + 5.5.1 ===== diff --git a/docs/index.rst b/docs/index.rst index 78a88f970c..6d464c8545 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1675,48 +1675,47 @@ Process class See `pmap.py `__ for an example application. - +---------------+--------------+---------+-----------+--------------+ - | Linux | macOS | Windows | Solaris | FreeBSD | - +===============+==============+=========+===========+==============+ - | rss | rss | rss | rss | rss | - +---------------+--------------+---------+-----------+--------------+ - | size | private | | anonymous | private | - +---------------+--------------+---------+-----------+--------------+ - | pss | swapped | | locked | ref_count | - +---------------+--------------+---------+-----------+--------------+ - | shared_clean | dirtied | | | shadow_count | - +---------------+--------------+---------+-----------+--------------+ - | shared_dirty | ref_count | | | | - +---------------+--------------+---------+-----------+--------------+ - | private_clean | shadow_depth | | | | - +---------------+--------------+---------+-----------+--------------+ - | private_dirty | | | | | - +---------------+--------------+---------+-----------+--------------+ - | referenced | | | | | - +---------------+--------------+---------+-----------+--------------+ - | anonymous | | | | | - +---------------+--------------+---------+-----------+--------------+ - | swap | | | | | - +---------------+--------------+---------+-----------+--------------+ + +---------------+---------+--------------+-----------+ + | Linux | Windows | FreeBSD | Solaris | + +===============+=========+==============+===========+ + | rss | rss | rss | rss | + +---------------+---------+--------------+-----------+ + | size | | private | anonymous | + +---------------+---------+--------------+-----------+ + | pss | | ref_count | locked | + +---------------+---------+--------------+-----------+ + | shared_clean | | shadow_count | | + +---------------+---------+--------------+-----------+ + | shared_dirty | | | | + +---------------+---------+--------------+-----------+ + | private_clean | | | | + +---------------+---------+--------------+-----------+ + | private_dirty | | | | + +---------------+---------+--------------+-----------+ + | referenced | | | | + +---------------+---------+--------------+-----------+ + | anonymous | | | | + +---------------+---------+--------------+-----------+ + | swap | | | | + +---------------+---------+--------------+-----------+ >>> import psutil >>> p = psutil.Process() >>> p.memory_maps() [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), - pmmap_grouped(path='/lib/x8664-linux-gnu/libcrypto.so.0.1', rss=34124, rss=32768, size=2134016, pss=15360, shared_clean=24576, shared_dirty=0, private_clean=0, private_dirty=8192, referenced=24576, anonymous=8192, swap=0), - pmmap_grouped(path='[heap]', rss=32768, size=139264, pss=32768, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=32768, referenced=32768, anonymous=32768, swap=0), - pmmap_grouped(path='[stack]', rss=2465792, size=2494464, pss=2465792, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=2465792, referenced=2277376, anonymous=2465792, swap=0), - ...] - >>> p.memory_maps(grouped=False) - [pmmap_ext(addr='00400000-006ea000', perms='r-xp', path='/usr/bin/python2.7', rss=2293760, size=3055616, pss=1157120, shared_clean=2273280, shared_dirty=0, private_clean=20480, private_dirty=0, referenced=2293760, anonymous=0, swap=0), - pmmap_ext(addr='008e9000-008eb000', perms='r--p', path='/usr/bin/python2.7', rss=8192, size=8192, pss=6144, shared_clean=4096, shared_dirty=0, private_clean=0, private_dirty=4096, referenced=8192, anonymous=4096, swap=0), - pmmap_ext(addr='008eb000-00962000', perms='rw-p', path='/usr/bin/python2.7', rss=417792, size=487424, pss=317440, shared_clean=200704, shared_dirty=0, private_clean=16384, private_dirty=200704, referenced=417792, anonymous=200704, swap=0), - pmmap_ext(addr='00962000-00985000', perms='rw-p', path='[anon]', rss=139264, size=143360, pss=139264, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=139264, referenced=139264, anonymous=139264, swap=0), - pmmap_ext(addr='02829000-02ccf000', perms='rw-p', path='[heap]', rss=4743168, size=4874240, pss=4743168, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=4743168, referenced=4718592, anonymous=4743168, swap=0), ...] - Availability: All platforms except OpenBSD, NetBSD and AIX. + Availability: Linux, Windows, FreeBSD, SunOS + + .. warning:: + on macOS, starting from version 5.6.0, this function is deprecated and + will always raise :class:`psutil.AccessDenied`. It is scheduled for + removal in 6.0.0 + (see `issue 1020 `_). + + .. versionchanged:: + 5.6.0 deprecated on macOS, always raise AccessDenied .. method:: children(recursive=False) diff --git a/psutil/__init__.py b/psutil/__init__.py index 447e507b79..d8ec701cd6 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1218,7 +1218,6 @@ def memory_percent(self, memtype="rss"): 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. diff --git a/psutil/_common.py b/psutil/_common.py index b809a79f60..6d43038898 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -475,7 +475,7 @@ def outer(fun): @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 diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 58ecf17fc0..96ed947608 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -97,11 +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)) # ===================================================================== diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 72b343f5bc..78f710aa33 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -8,6 +8,7 @@ import errno import functools import os +import warnings from socket import AF_INET from collections import namedtuple @@ -107,13 +108,6 @@ 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)) # ===================================================================== @@ -582,6 +576,7 @@ def threads(self): retlist.append(ntuple) return retlist - @wrap_exceptions def memory_maps(self): - return cext.proc_memory_maps(self.pid) + msg = "memory_maps() on OSX is deprecated and will be removed in 6.0.0" + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + raise AccessDenied(self.pid, msg=msg) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 02b2172dac..eaf4e514a1 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -341,164 +341,6 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } -/* - * Return a list of tuples for every process memory maps. - * 'procstat' cmdline utility has been used as an example. - * This will fail for all processes except os.getpid(), - * even as root. - */ -static PyObject * -psutil_proc_memory_maps(PyObject *self, PyObject *args) { - char buf[PATH_MAX]; - char addr_str[34]; - char perms[8]; - int pagesize = getpagesize(); - int iteration = 0; - long pid; - kern_return_t kr; - mach_port_t task = MACH_PORT_NULL; - uint32_t depth = 1; - vm_address_t address = 0; - vm_size_t size = 0; - struct vm_region_submap_info_64 info; - mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64; - PyObject *py_tuple = NULL; - PyObject *py_path = NULL; - PyObject *py_list = PyList_New(0); - - if (py_list == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - if (psutil_task_for_pid(pid, &task) != 0) - goto error; - - while (1) { - iteration += 1; - py_tuple = NULL; - kr = vm_region_recurse_64(task, &address, &size, &depth, - (vm_region_info_64_t)&info, &count); - if (kr != KERN_SUCCESS) { - if ((kr == KERN_INVALID_ADDRESS) && (iteration > 1)) { - break; // no more regions to read - } - PyErr_Format( - PyExc_RuntimeError, - "vm_region_recurse_64() failed: %s", - mach_error_string(kr)); - goto error; - } - - if (info.is_submap) { - depth++; - } - else { - // Free/Reset the char[]s to avoid weird paths - memset(buf, 0, sizeof(buf)); - memset(addr_str, 0, sizeof(addr_str)); - memset(perms, 0, sizeof(perms)); - - sprintf(addr_str, - "%016lx-%016lx", - (long unsigned int)address, - (long unsigned int)address + size); - sprintf(perms, "%c%c%c/%c%c%c", - (info.protection & VM_PROT_READ) ? 'r' : '-', - (info.protection & VM_PROT_WRITE) ? 'w' : '-', - (info.protection & VM_PROT_EXECUTE) ? 'x' : '-', - (info.max_protection & VM_PROT_READ) ? 'r' : '-', - (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', - (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-'); - - // proc_regionfilename() is undocumented but from its source - // code we can determine that it sets errno on error and - // return length of path. - errno = 0; - proc_regionfilename((pid_t)pid, address, buf, sizeof(buf)); - if (errno != 0) { - psutil_debug("proc_regionfilename() failed errno=%i", errno); - psutil_raise_for_pid(pid, "proc_regionfilename()"); - goto error; - } - - if (info.share_mode == SM_COW && info.ref_count == 1) { - // Treat single reference SM_COW as SM_PRIVATE - info.share_mode = SM_PRIVATE; - } - - if (strlen(buf) == 0) { - switch (info.share_mode) { -// #ifdef SM_LARGE_PAGE - // case SM_LARGE_PAGE: - // Treat SM_LARGE_PAGE the same as SM_PRIVATE - // since they are not shareable and are wired. -// #endif - case SM_COW: - strcpy(buf, "[cow]"); - break; - case SM_PRIVATE: - strcpy(buf, "[prv]"); - break; - case SM_EMPTY: - strcpy(buf, "[nul]"); - break; - case SM_SHARED: - case SM_TRUESHARED: - strcpy(buf, "[shm]"); - break; - case SM_PRIVATE_ALIASED: - strcpy(buf, "[ali]"); - break; - case SM_SHARED_ALIASED: - strcpy(buf, "[s/a]"); - break; - default: - strcpy(buf, "[???]"); - } - } - - py_path = PyUnicode_DecodeFSDefault(buf); - if (! py_path) - goto error; - py_tuple = Py_BuildValue( - "ssOIIIIIH", - addr_str, // "start-end"address - perms, // "rwx" permissions - py_path, // path - info.pages_resident * pagesize, // rss - info.pages_shared_now_private * pagesize, // private - info.pages_swapped_out * pagesize, // swapped - info.pages_dirtied * pagesize, // dirtied - info.ref_count, // ref count - info.shadow_depth // shadow depth - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_list, py_tuple)) - goto error; - Py_DECREF(py_tuple); - Py_DECREF(py_path); - - // increment address for the next map/file - address += size; - } - } - - if (task != MACH_PORT_NULL) - mach_port_deallocate(mach_task_self(), task); - - return py_list; - -error: - if (task != MACH_PORT_NULL) - mach_port_deallocate(mach_task_self(), task); - Py_XDECREF(py_tuple); - Py_XDECREF(py_path); - Py_DECREF(py_list); - return NULL; -} - - /* * Return the number of logical CPUs in the system. * XXX this could be shared with BSD. @@ -1931,8 +1773,6 @@ PsutilMethods[] = { "Return the number of fds opened by process."}, {"proc_connections", psutil_proc_connections, METH_VARARGS, "Get process TCP and UDP connections as a list of tuples"}, - {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS, - "Return a list of tuples for every process's memory map"}, // --- system-related functions diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 545f5f2519..86af2cfb50 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -167,7 +167,7 @@ HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") HAS_IONICE = hasattr(psutil.Process, "ionice") HAS_MEMORY_FULL_INFO = 'uss' in psutil.Process().memory_full_info()._fields -HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") +HAS_MEMORY_MAPS = not MACOS and hasattr(psutil.Process, "memory_maps") HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") HAS_RLIMIT = hasattr(psutil.Process, "rlimit") HAS_THREADS = hasattr(psutil.Process, "threads") diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 5ea4ba1c0c..97a1a0f069 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -181,10 +181,18 @@ 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(), FutureWarning) + self.assertIsInstance(w.category(), DeprecationWarning) self.assertIn("memory_info_ex() is deprecated", str(w.message)) self.assertIn("use memory_info() instead", str(w.message)) + @unittest.skipIf(not MACOS, "deprecated on macOS") + def test_memory_maps_osx(self): + with warnings.catch_warnings(record=True) as ws: + with self.assertRaises(psutil.AccessDenied): + psutil.Process().memory_maps() + w = ws[0] + self.assertIsInstance(w.category(), DeprecationWarning) + # =================================================================== # --- System API types diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index cf5c5ea8f2..4025719093 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -158,9 +158,6 @@ def test_threads(self): self.assertRaises((psutil.ZombieProcess, psutil.AccessDenied), self.p.threads) - def test_memory_maps(self): - self.assertRaises(psutil.ZombieProcess, self.p.memory_maps) - @unittest.skipIf(not MACOS, "MACOS only") class TestSystemAPIs(unittest.TestCase): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 16c0ba4ace..31f42ebaad 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1265,7 +1265,7 @@ def test_halfway_terminated_process(self): p.send_signal(signal.CTRL_BREAK_EVENT) excluded_names = ['pid', 'is_running', 'wait', 'create_time', - 'oneshot'] + 'oneshot', 'memory_info_ex'] if LINUX and not HAS_RLIMIT: excluded_names.append('rlimit') for name in dir(p): @@ -1291,6 +1291,8 @@ def test_halfway_terminated_process(self): ret = meth([0]) elif name == 'send_signal': ret = meth(signal.SIGTERM) + elif MACOS and name == 'memory_maps': + continue # XXX else: ret = meth() except psutil.ZombieProcess: From 665f0c1c275a3575da4b01ea859a4c41a82a7321 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 15:14:39 +0100 Subject: [PATCH 0191/1714] introduce a new scriptsutils.py private module shared by all internal utils + refactor print_access_speed.py script --- scripts/internal/download_exes.py | 32 +----------------- scripts/internal/print_access_denied.py | 33 +------------------ scripts/internal/print_api_speed.py | 44 ++++++++++++------------- scripts/internal/scriptutils.py | 42 +++++++++++++++++++++++ 4 files changed, 65 insertions(+), 86 deletions(-) create mode 100644 scripts/internal/scriptutils.py diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index d138f0d360..e3e6a46d20 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -22,6 +22,7 @@ import sys from psutil import __version__ as PSUTIL_VERSION +from scriptutils import hilite BASE_URL = 'https://ci.appveyor.com/api' @@ -36,37 +37,6 @@ def exit(msg=""): sys.exit(1) -def term_supports_colors(file=sys.stdout): - try: - import curses - assert file.isatty() - curses.setupterm() - assert curses.tigetnum("colors") > 0 - except Exception: - return False - else: - return True - - -COLORS = term_supports_colors() - - -def hilite(s, ok=True, bold=False): - """Return an highlighted version of 'string'.""" - if not COLORS: - return s - attr = [] - if ok is None: # no color - pass - elif ok: # green - attr.append('32') - else: # red - attr.append('31') - if bold: - attr.append('1') - return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) - - def safe_makedirs(path): try: os.makedirs(path) diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 8381928d3e..7e46f48e7e 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -46,40 +46,9 @@ from __future__ import print_function, division from collections import defaultdict -import sys import psutil - - -def term_supports_colors(file=sys.stdout): - try: - import curses - assert file.isatty() - curses.setupterm() - assert curses.tigetnum("colors") > 0 - except Exception: - return False - else: - return True - - -COLORS = term_supports_colors() - - -def hilite(s, ok=True, bold=False): - """Return an highlighted version of 'string'.""" - if not COLORS: - return s - attr = [] - if ok is None: # no color - pass - elif ok: # green - attr.append('32') - else: # red - attr.append('31') - if bold: - attr.append('1') - return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) +from scriptutils import hilite def main(): diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 24b98f1197..c0e0e0e053 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -25,6 +25,7 @@ from __future__ import print_function, division from timeit import default_timer as timer +import inspect import os import psutil @@ -38,7 +39,7 @@ def print_timings(): timings.sort(key=lambda x: x[0 if SORT_BY_NAME else 1]) while timings[:]: title, elapsed = timings.pop(0) - print(" %-30s %f secs" % (title, elapsed)) + print("%-30s %f secs" % (title, elapsed)) def timecall(title, fun, *args, **kw): @@ -49,33 +50,30 @@ def timecall(title, fun, *args, **kw): def main(): + # --- system + + public_apis = [] + for name in psutil.__all__: + obj = getattr(psutil, name, None) + if inspect.isfunction(obj): + if name not in ('wait_procs', 'process_iter'): + public_apis.append(name) + print("SYSTEM APIS") - timecall('cpu_count', psutil.cpu_count) + for name in public_apis: + fun = getattr(psutil, name) + args = () + if name == 'pid_exists': + args = (os.getpid(), ) + elif name == 'disk_usage': + args = (os.getcwd(), ) + timecall(name, fun, *args) timecall('cpu_count (cores)', psutil.cpu_count, logical=False) - timecall('cpu_times', psutil.cpu_times) - timecall('cpu_percent', psutil.cpu_percent, interval=0) - timecall('cpu_times_percent', psutil.cpu_times_percent, interval=0) - timecall('cpu_stats', psutil.cpu_stats) - timecall('cpu_freq', psutil.cpu_freq) - timecall('virtual_memory', psutil.virtual_memory) - timecall('swap_memory', psutil.swap_memory) - timecall('disk_partitions', psutil.disk_partitions) - timecall('disk_usage', psutil.disk_usage, os.getcwd()) - timecall('disk_io_counters', psutil.disk_io_counters) - timecall('net_io_counters', psutil.net_io_counters) - timecall('net_connections', psutil.net_connections) - timecall('net_if_addrs', psutil.net_if_addrs) - timecall('net_if_stats', psutil.net_if_stats) - timecall('sensors_temperatures', psutil.sensors_temperatures) - timecall('sensors_fans', psutil.sensors_fans) - timecall('sensors_battery', psutil.sensors_battery) - timecall('boot_time', psutil.boot_time) - timecall('users', psutil.users) - timecall('pids', psutil.pids) - timecall('pid_exists', psutil.pid_exists, os.getpid()) timecall('process_iter (all)', lambda: list(psutil.process_iter())) print_timings() + # --- process + print("\nPROCESS APIS") ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', diff --git a/scripts/internal/scriptutils.py b/scripts/internal/scriptutils.py new file mode 100644 index 0000000000..7369804c25 --- /dev/null +++ b/scripts/internal/scriptutils.py @@ -0,0 +1,42 @@ +#!/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. + +"""Utils shared by all files in scripts/internal.""" + +import sys + +from psutil._compat import lru_cache + +__all__ = ['hilite'] + + +@lru_cache() +def _term_supports_colors(file=sys.stdout): + try: + import curses + assert file.isatty() + curses.setupterm() + assert curses.tigetnum("colors") > 0 + except Exception: + return False + else: + return True + + +def hilite(s, ok=True, bold=False): + """Return an highlighted version of 'string'.""" + if not _term_supports_colors(): + return s + attr = [] + if ok is None: # no color + pass + elif ok: # green + attr.append('32') + else: # red + attr.append('31') + if bold: + attr.append('1') + return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) From 52253a519609d6d8990d9d6812ea9bed0204c81c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 15:29:32 +0100 Subject: [PATCH 0192/1714] add arg parser for ad script --- .ci/travis/run.sh | 3 +++ Makefile | 2 +- scripts/internal/download_exes.py | 10 +++++++--- scripts/internal/print_api_speed.py | 17 ++++++++++++++--- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 06cc5b7e60..8183863366 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -33,3 +33,6 @@ if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then python -m flake8 fi fi + +PSUTIL_TESTING=1 python -Wa scripts/internal/print_access_denied.py +PSUTIL_TESTING=1 python -Wa scripts/internal/print_api_speed.py diff --git a/Makefile b/Makefile index 6c64a0b5c0..839a602674 100644 --- a/Makefile +++ b/Makefile @@ -256,7 +256,7 @@ print-access-denied: ## Print AD exceptions @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls - @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py + @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) # =================================================================== # Misc diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index e3e6a46d20..efe4ef18ed 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -122,7 +122,7 @@ def rename_27_wheels(): os.rename(src, dst) -def main(options): +def run(options): safe_rmtree('dist') urls = get_file_urls(options) completed = 0 @@ -149,10 +149,14 @@ def main(options): rename_27_wheels() -if __name__ == '__main__': +def main(): parser = argparse.ArgumentParser( description='AppVeyor artifact downloader') parser.add_argument('--user', required=True) parser.add_argument('--project', required=True) args = parser.parse_args() - main(args) + run(args) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index c0e0e0e053..b88ab3c5b3 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -25,18 +25,19 @@ from __future__ import print_function, division from timeit import default_timer as timer +import argparse import inspect import os import psutil -SORT_BY_NAME = 0 # <- toggle this +SORT_BY_TIME = False timings = [] def print_timings(): - timings.sort(key=lambda x: x[0 if SORT_BY_NAME else 1]) + timings.sort(key=lambda x: x[1 if SORT_BY_TIME else 0]) while timings[:]: title, elapsed = timings.pop(0) print("%-30s %f secs" % (title, elapsed)) @@ -49,7 +50,7 @@ def timecall(title, fun, *args, **kw): timings.append((title, elapsed)) -def main(): +def run(): # --- system public_apis = [] @@ -86,5 +87,15 @@ def main(): print_timings() +def main(): + global SORT_BY_TIME + parser = argparse.ArgumentParser(description='Benchmark all API calls') + parser.add_argument('-t', '--time', required=False, default=False, + action='store_true', help="sort by timings") + args = parser.parse_args() + SORT_BY_TIME = bool(args.time) + run() + + if __name__ == '__main__': main() From 25beb1467448e68cd88803f8289e20c8423d760e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 15:42:00 +0100 Subject: [PATCH 0193/1714] add printerr() and exit() to shared utils module --- scripts/internal/download_exes.py | 20 +++++++------------- scripts/internal/scriptutils.py | 14 +++++++++++++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index efe4ef18ed..3d84b500d7 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -19,10 +19,9 @@ import os import requests import shutil -import sys from psutil import __version__ as PSUTIL_VERSION -from scriptutils import hilite +from scriptutils import printerr, exit BASE_URL = 'https://ci.appveyor.com/api' @@ -31,12 +30,6 @@ COLORS = True -def exit(msg=""): - if msg: - print(hilite(msg, ok=False), file=sys.stderr) - sys.exit(1) - - def safe_makedirs(path): try: os.makedirs(path) @@ -106,8 +99,9 @@ def get_file_urls(options): urls.append(file_url) if not urls: exit("no artifacts found") - for url in sorted(urls, key=lambda x: os.path.basename(x)): - yield url + else: + for url in sorted(urls, key=lambda x: os.path.basename(x)): + yield url def rename_27_wheels(): @@ -133,9 +127,9 @@ def run(options): url = fut_to_url[fut] try: local_fname = fut.result() - except Exception as _: - exc = _ - print("error while downloading %s: %s" % (url, exc)) + except Exception: + printerr("error while downloading %s" % (url)) + raise else: completed += 1 print("downloaded %-45s %s" % ( diff --git a/scripts/internal/scriptutils.py b/scripts/internal/scriptutils.py index 7369804c25..3ee416f825 100644 --- a/scripts/internal/scriptutils.py +++ b/scripts/internal/scriptutils.py @@ -6,11 +6,13 @@ """Utils shared by all files in scripts/internal.""" +from __future__ import print_function import sys from psutil._compat import lru_cache -__all__ = ['hilite'] + +__all__ = ['hilite', 'printerr', 'exit'] @lru_cache() @@ -40,3 +42,13 @@ def hilite(s, ok=True, bold=False): if bold: attr.append('1') return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) + + +def printerr(s): + print(hilite(s, ok=False), file=sys.stderr) + + +def exit(msg=""): + if msg: + printerr(msg) + sys.exit(1) From 7d0810ef4fc3e17b59207f9d5b7aabe131f4c6f1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 16:02:07 +0100 Subject: [PATCH 0194/1714] highlight top 6 slowest calls --- scripts/internal/print_access_denied.py | 4 ++-- scripts/internal/print_api_speed.py | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 7e46f48e7e..d903f8b95c 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -40,7 +40,7 @@ io_counters 237 70.5% ACCESS DENIED open_files 237 70.5% ACCESS DENIED environ 237 70.5% ACCESS DENIED --------------------------- +----------------------------------------------- total: 1736 (336 total processes) """ @@ -75,7 +75,7 @@ def main(): s += "ACCESS DENIED" s = hilite(s, ok=False) print(s) - print("--------------------------") + print("-----------------------------------------------") print("total: %19s (%s total processes)" % (tot_ads, tot_procs)) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index b88ab3c5b3..2266caa28d 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -30,17 +30,25 @@ import os import psutil +from scriptutils import hilite SORT_BY_TIME = False +TOP_SLOWEST = 7 timings = [] def print_timings(): + slower = [] timings.sort(key=lambda x: x[1 if SORT_BY_TIME else 0]) + for x in sorted(timings, key=lambda x: x[1], reverse=1)[:TOP_SLOWEST]: + slower.append(x[0]) while timings[:]: title, elapsed = timings.pop(0) - print("%-30s %f secs" % (title, elapsed)) + s = " %-30s %f secs" % (title, elapsed) + if title in slower: + s = hilite(s, ok=False) + print(s) def timecall(title, fun, *args, **kw): @@ -50,6 +58,10 @@ def timecall(title, fun, *args, **kw): timings.append((title, elapsed)) +def titlestr(s): + return hilite(s, ok=None, bold=True) + + def run(): # --- system @@ -60,7 +72,7 @@ def run(): if name not in ('wait_procs', 'process_iter'): public_apis.append(name) - print("SYSTEM APIS") + print(titlestr("SYSTEM APIS")) for name in public_apis: fun = getattr(psutil, name) args = () @@ -75,7 +87,7 @@ def run(): # --- process - print("\nPROCESS APIS") + print(titlestr("\nPROCESS APIS")) ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', 'pid', 'rlimit'] @@ -88,12 +100,15 @@ def run(): def main(): - global SORT_BY_TIME + global SORT_BY_TIME, TOP_SLOWEST parser = argparse.ArgumentParser(description='Benchmark all API calls') parser.add_argument('-t', '--time', required=False, default=False, action='store_true', help="sort by timings") + parser.add_argument('-s', '--slowest', required=False, default=TOP_SLOWEST, + help="highlight the top N slowest APIs") args = parser.parse_args() SORT_BY_TIME = bool(args.time) + TOP_SLOWEST = int(args.slowest) run() From eb05ea9bd7464232bbc81a8a9bfb902717d77b43 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 07:26:22 -0800 Subject: [PATCH 0195/1714] appveyor: run print scripts after tests --- appveyor.yml | 2 ++ scripts/internal/print_api_speed.py | 8 +++++--- scripts/internal/winmake.py | 11 +++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a5fe5d0332..38a6a8e0a8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -64,6 +64,8 @@ test_script: after_test: - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py wheel" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_access_denied.py" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_api_speed.py" artifacts: - path: dist\* diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 2266caa28d..e635810f60 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -33,7 +33,7 @@ from scriptutils import hilite -SORT_BY_TIME = False +SORT_BY_TIME = False if psutil.POSIX else True TOP_SLOWEST = 7 timings = [] @@ -66,10 +66,12 @@ def run(): # --- system public_apis = [] + ignore = ('wait_procs', 'process_iter', 'win_service_get', + 'win_service_iter') for name in psutil.__all__: obj = getattr(psutil, name, None) if inspect.isfunction(obj): - if name not in ('wait_procs', 'process_iter'): + if name not in ignore: public_apis.append(name) print(titlestr("SYSTEM APIS")) @@ -102,7 +104,7 @@ def run(): def main(): global SORT_BY_TIME, TOP_SLOWEST parser = argparse.ArgumentParser(description='Benchmark all API calls') - parser.add_argument('-t', '--time', required=False, default=False, + parser.add_argument('-t', '--time', required=False, default=SORT_BY_TIME, action='store_true', help="sort by timings") parser.add_argument('-s', '--slowest', required=False, default=TOP_SLOWEST, help="highlight the top N slowest APIs") diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 1f4fa8f21d..cd26c67edb 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -472,24 +472,27 @@ def install_git_hooks(): @cmd def bench_oneshot(): """Benchmarks for oneshot() ctx manager (see #799).""" - install() sh("%s -Wa scripts\\internal\\bench_oneshot.py" % PYTHON) @cmd def bench_oneshot_2(): """Same as above but using perf module (supposed to be more precise).""" - install() sh("%s -Wa scripts\\internal\\bench_oneshot_2.py" % PYTHON) @cmd def print_access_denied(): - """Benchmarks for oneshot() ctx manager (see #799).""" - install() + """Print AD exceptions raised by all Process methods.""" sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) +@cmd +def print_api_speed(): + """Benchmark all API calls.""" + sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) + + def set_python(s): global PYTHON if os.path.isabs(s): From bcff9ae55cb324ffe499347e1925c29f1d6800c9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 17:13:52 +0100 Subject: [PATCH 0196/1714] fix #1442: use python3 as Makefile default --- .ci/travis/install.sh | 2 +- DEVGUIDE.rst | 2 +- HISTORY.rst | 1 + Makefile | 19 +++++-------------- scripts/internal/print_api_speed.py | 9 ++++++--- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index 56f6623d8e..b0f28c66d7 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -39,4 +39,4 @@ elif [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] || [[ $PYVER == 'py27' ]]; then pip install -U ipaddress mock fi -pip install -U coverage coveralls flake8 pep8 setuptools +pip install -U coverage coveralls flake8 setuptools diff --git a/DEVGUIDE.rst b/DEVGUIDE.rst index f604480e55..1b056198c6 100644 --- a/DEVGUIDE.rst +++ b/DEVGUIDE.rst @@ -49,7 +49,7 @@ Some useful make commands: .. code-block:: bash make install # install - make setup-dev-env # install useful dev libs (pyflakes, unittest2, etc.) + make setup-dev-env # install useful dev libs (fkale8, unittest2, etc.) make test # run unit tests make test-memleaks # run memory leak tests make test-coverage # run test coverage diff --git a/HISTORY.rst b/HISTORY.rst index 5313a091fa..2428f033a8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,7 @@ XXXX-XX-XX startup. - 1433_: new Process.parents() method. (idea by Ghislain Le Meur) - 1437_: pids() are returned in sorted order. +- 1442_: python3 is now the default interpreter used by Makefile. **Bug fixes** diff --git a/Makefile b/Makefile index 839a602674..24ee6db6c5 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # To use a specific Python version run: "make install PYTHON=python3.3" # You can set the variables below from the command line. -PYTHON = python +PYTHON = python3 TSCRIPT = psutil/tests/__main__.py ARGS = # List of nice-to-have dev libs. @@ -14,9 +14,7 @@ DEPS = \ futures \ ipaddress \ mock==1.0.1 \ - pep8 \ perf \ - pyflakes \ requests \ setuptools \ sphinx \ @@ -166,13 +164,6 @@ test-coverage: ## Run test coverage. # Linters # =================================================================== -pep8: ## PEP8 linter. - @git ls-files | grep \\.py$ | xargs $(PYTHON) -m pep8 - -pyflakes: ## Pyflakes linter. - @export PYFLAKES_NODOCTEST=1 && \ - git ls-files | grep \\.py$ | xargs $(PYTHON) -m pyflakes - flake8: ## flake8 linter. @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 @@ -247,16 +238,16 @@ generate-manifest: ## Generates MANIFEST.in file. # =================================================================== print-announce: ## Print announce of new release. - @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_announce.py + @$(PYTHON) scripts/internal/print_announce.py print-timeline: ## Print releases' timeline. - @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_timeline.py + @$(PYTHON) scripts/internal/print_timeline.py print-access-denied: ## Print AD exceptions - @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_access_denied.py + @$(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls - @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) + @$(PYTHON) scripts/internal/print_api_speed.py $(ARGS) # =================================================================== # Misc diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index e635810f60..188ae737e9 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -66,8 +66,10 @@ def run(): # --- system public_apis = [] - ignore = ('wait_procs', 'process_iter', 'win_service_get', - 'win_service_iter') + ignore = ['wait_procs', 'process_iter', 'win_service_get', + 'win_service_iter'] + if psutil.MACOS: + ignore.append('net_connections') # raises AD for name in psutil.__all__: obj = getattr(psutil, name, None) if inspect.isfunction(obj): @@ -88,11 +90,12 @@ def run(): print_timings() # --- process - print(titlestr("\nPROCESS APIS")) ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', 'pid', 'rlimit'] + if psutil.MACOS: + ignore.append('memory_maps') # XXX p = psutil.Process() for name in sorted(dir(p)): if not name.startswith('_') and name not in ignore: From 84f0d95bf6672d2426caeb1ac66d6dae119a2f5f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 17:40:43 +0100 Subject: [PATCH 0197/1714] better print formatting for print scripts --- scripts/internal/print_access_denied.py | 89 +++++++++++++------------ scripts/internal/print_api_speed.py | 44 +++++++----- 2 files changed, 75 insertions(+), 58 deletions(-) diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index d903f8b95c..a098a72b1e 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -10,38 +10,39 @@ for what Process method. $ make print-access-denied -username 0 0.0% SUCCESS -cpu_num 0 0.0% SUCCESS -num_ctx_switches 0 0.0% SUCCESS -pid 0 0.0% SUCCESS -cmdline 0 0.0% SUCCESS -create_time 0 0.0% SUCCESS -ionice 0 0.0% SUCCESS -cpu_percent 0 0.0% SUCCESS -terminal 0 0.0% SUCCESS -ppid 0 0.0% SUCCESS -nice 0 0.0% SUCCESS -status 0 0.0% SUCCESS -cpu_times 0 0.0% SUCCESS -memory_info 0 0.0% SUCCESS -threads 0 0.0% SUCCESS -uids 0 0.0% SUCCESS -num_threads 0 0.0% SUCCESS -name 0 0.0% SUCCESS -gids 0 0.0% SUCCESS -cpu_affinity 0 0.0% SUCCESS -memory_percent 0 0.0% SUCCESS -memory_full_info 70 20.8% ACCESS DENIED -memory_maps 70 20.8% ACCESS DENIED -exe 174 51.8% ACCESS DENIED -connections 237 70.5% ACCESS DENIED -num_fds 237 70.5% ACCESS DENIED -cwd 237 70.5% ACCESS DENIED -io_counters 237 70.5% ACCESS DENIED -open_files 237 70.5% ACCESS DENIED -environ 237 70.5% ACCESS DENIED ------------------------------------------------ -total: 1736 (336 total processes) +API AD Percent Outcome +memory_info 0 0.0% SUCCESS +uids 0 0.0% SUCCESS +cmdline 0 0.0% SUCCESS +create_time 0 0.0% SUCCESS +status 0 0.0% SUCCESS +num_ctx_switches 0 0.0% SUCCESS +username 0 0.0% SUCCESS +ionice 0 0.0% SUCCESS +memory_percent 0 0.0% SUCCESS +gids 0 0.0% SUCCESS +cpu_times 0 0.0% SUCCESS +nice 0 0.0% SUCCESS +pid 0 0.0% SUCCESS +cpu_percent 0 0.0% SUCCESS +num_threads 0 0.0% SUCCESS +cpu_num 0 0.0% SUCCESS +ppid 0 0.0% SUCCESS +terminal 0 0.0% SUCCESS +name 0 0.0% SUCCESS +threads 0 0.0% SUCCESS +cpu_affinity 0 0.0% SUCCESS +memory_maps 71 21.3% ACCESS DENIED +memory_full_info 71 21.3% ACCESS DENIED +exe 174 52.1% ACCESS DENIED +environ 238 71.3% ACCESS DENIED +num_fds 238 71.3% ACCESS DENIED +io_counters 238 71.3% ACCESS DENIED +cwd 238 71.3% ACCESS DENIED +connections 238 71.3% ACCESS DENIED +open_files 238 71.3% ACCESS DENIED +-------------------------------------------------- +Totals: access-denied=1744, calls=10020, processes=334 """ from __future__ import print_function, division @@ -52,31 +53,37 @@ def main(): + # collect tot_procs = 0 tot_ads = 0 + tot_calls = 0 signaler = object() d = defaultdict(int) for p in psutil.process_iter(attrs=[], ad_value=signaler): tot_procs += 1 for methname, value in p.info.items(): + tot_calls += 1 if value is signaler: tot_ads += 1 d[methname] += 1 else: d[methname] += 0 + # print + templ = "%-20s %-5s %-9s %s" + s = templ % ("API", "AD", "Percent", "Outcome") + print(hilite(s, ok=None, bold=True)) for methname, ads in sorted(d.items(), key=lambda x: x[1]): perc = (ads / tot_procs) * 100 - s = "%-20s %-3s %5.1f%% " % (methname, ads, perc) - if not ads: - s += "SUCCESS" - s = hilite(s, ok=True) - else: - s += "ACCESS DENIED" - s = hilite(s, ok=False) + outcome = "SUCCESS" if not ads else "ACCESS DENIED" + s = templ % (methname, ads, "%6.1f%%" % perc, outcome) + s = hilite(s, ok=not ads) print(s) - print("-----------------------------------------------") - print("total: %19s (%s total processes)" % (tot_ads, tot_procs)) + print("-" * 50) + # print("total: %s AccessDenied errors (%s total processes)" % ( + # tot_ads, tot_procs)) + print("Totals: access-denied=%s, calls=%s, processes=%s" % ( + tot_ads, tot_calls, tot_procs)) if __name__ == '__main__': diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 188ae737e9..12040dcf43 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -7,20 +7,26 @@ """Benchmark all API calls. $ python scripts/internal/print_api_speed.py -SYSTEM APIS - boot_time 0.000127 secs - cpu_count 0.000034 secs - cpu_count (cores) 0.000279 secs - cpu_freq 0.000438 secs - ... - -PROCESS APIS - cmdline 0.000027 secs - connections 0.000056 secs - cpu_affinity 0.000014 secs - cpu_num 0.000054 secs - cpu_percent 0.000077 secs - ... +SYSTEM APIS SECONDS +---------------------------------- +boot_time 0.000140 +cpu_count 0.000016 +cpu_count (cores) 0.000312 +cpu_freq 0.000811 +cpu_percent 0.000138 +cpu_stats 0.000165 +cpu_times 0.000140 +... + +PROCESS APIS SECONDS +---------------------------------- +children 0.007246 +cmdline 0.000069 +connections 0.000072 +cpu_affinity 0.000012 +cpu_num 0.000035 +cpu_percent 0.000042 +cpu_times 0.000031 """ from __future__ import print_function, division @@ -36,6 +42,7 @@ SORT_BY_TIME = False if psutil.POSIX else True TOP_SLOWEST = 7 timings = [] +templ = "%-25s %s" def print_timings(): @@ -45,7 +52,7 @@ def print_timings(): slower.append(x[0]) while timings[:]: title, elapsed = timings.pop(0) - s = " %-30s %f secs" % (title, elapsed) + s = templ % (title, "%f" % elapsed) if title in slower: s = hilite(s, ok=False) print(s) @@ -76,7 +83,8 @@ def run(): if name not in ignore: public_apis.append(name) - print(titlestr("SYSTEM APIS")) + print(titlestr(templ % ("SYSTEM APIS", "SECONDS"))) + print("-" * 34) for name in public_apis: fun = getattr(psutil, name) args = () @@ -90,7 +98,9 @@ def run(): print_timings() # --- process - print(titlestr("\nPROCESS APIS")) + print("") + print(titlestr(templ % ("PROCESS APIS", "SECONDS"))) + print("-" * 34) ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', 'pid', 'rlimit'] From e8bd07f81bc31569a6e1703ed95f3686361017ed Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 27 Feb 2019 18:48:35 +0100 Subject: [PATCH 0198/1714] update doc --- DEVGUIDE.rst | 21 ---------------- INSTALL.rst | 33 ++++++++++++------------- README.rst | 26 ++++++++----------- scripts/internal/print_access_denied.py | 2 -- scripts/internal/print_api_speed.py | 2 +- 5 files changed, 27 insertions(+), 57 deletions(-) diff --git a/DEVGUIDE.rst b/DEVGUIDE.rst index 1b056198c6..1d1baf1f7e 100644 --- a/DEVGUIDE.rst +++ b/DEVGUIDE.rst @@ -1,4 +1,3 @@ -======================= Setup and running tests ======================= @@ -31,7 +30,6 @@ If you plan on hacking on psutil this is what you're supposed to do first: "edit" mode; also ``make setup-dev-env`` installs deps as a limited user. - use `make help` to see the list of available commands. -============ Coding style ============ @@ -40,7 +38,6 @@ Coding style - C code strictly follows `PEP 7 `_ styling guides. -======== Makefile ======== @@ -87,7 +84,6 @@ On Windows: set TSCRIPT=foo.py && make test -==================== Adding a new feature ==================== @@ -120,7 +116,6 @@ Typical process occurring when adding a new functionality (API): - update ``README.rst`` (if necessary). - make a pull request. -=================== Make a pull request =================== @@ -130,7 +125,6 @@ Make a pull request - push to the branch (``git push origin my-new-feature``) - create a new pull request -====================== Continuous integration ====================== @@ -175,7 +169,6 @@ An icon in the home page (README) always shows the last coverage percentage: :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) -============= Documentation ============= @@ -185,7 +178,6 @@ Documentation - doc can be built with ``make setup-dev-env; cd docs; make html``. - public doc is hosted on http://psutil.readthedocs.io/ -======================= Releasing a new version ======================= @@ -194,16 +186,3 @@ These are notes for myself (Giampaolo): - ``make release`` - post announce (``make print-announce``) on psutil and python-announce mailing lists, twitter, g+, blog. - -============= -FreeBSD notes -============= - -- setup: - -.. code-block:: bash - - pkg install python python3 gcc git vim screen bash - chsh -s /usr/local/bin/bash user # set bash as default shell - -- ``/usr/src`` contains the source codes for all installed CLI tools (grep in it). diff --git a/INSTALL.rst b/INSTALL.rst index aaa1b87f05..b2e8941493 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,9 +1,8 @@ Install pip =========== -pip is the easiest way to install psutil. -It is shipped by default with Python 2.7.9+ and 3.4+. For other Python versions -you can install it manually. +pip is the easiest way to install psutil. It is shipped by default with Python +2.7.9+ and 3.4+. For other Python versions you can install it manually. On Linux or via wget: .. code-block:: bash @@ -74,19 +73,19 @@ first, then: Windows ======= -The easiest way to install psutil on Windows is to just use the pre-compiled -exe/wheel installers hosted on -`PyPI `__ via pip: +Open a cmd.exe shell and run: -.. code-block:: bat +.. code-block:: + + python -m pip install psutil - C:\Python27\python.exe -m pip install psutil +This assumes "python" is in your PATH. If not, specify the full python.exe +path. -If you want to compile psutil from sources you'll need **Visual Studio** -(Mingw32 is no longer supported), which really is a mess. -The VS versions are the onle listed below. +In order to compile psutil from sources you'll need **Visual Studio** (Mingw32 +is not supported). This `blog post `__ -provides numerous info on how to properly set up VS (good luck with that). +provides numerous info on how to properly set up VS. The needed VS versions are: * Python 2.6, 2.7: `VS-2008 `__ * Python 3.4: `VS-2010 `__ @@ -94,14 +93,14 @@ provides numerous info on how to properly set up VS (good luck with that). Compiling 64 bit versions of Python 2.6 and 2.7 with VS 2008 requires `Windows SDK and .NET Framework 3.5 SP1 `__. -Once installed run vcvars64.bat, then you can finally compile (see -`here `__). -To compile / install psutil from sources on Windows run: +Once installed run `vcvars64.bat` +(see `here `__). +Once VS is setup open a cmd.exe shell, cd into psutil directory and run: .. code-block:: bat - make.bat build - make.bat install + python setup.py build + python setup.py install FreeBSD ======= diff --git a/README.rst b/README.rst index 0f26fbcf4c..6c16373984 100644 --- a/README.rst +++ b/README.rst @@ -60,7 +60,7 @@ psutil currently supports the following platforms: - **Linux** - **Windows** -- **macOS**, +- **macOS** - **FreeBSD, OpenBSD**, **NetBSD** - **Sun Solaris** - **AIX** @@ -139,7 +139,7 @@ Example usages This represents pretty much the whole psutil API. CPU -=== +--- .. code-block:: python @@ -182,7 +182,7 @@ CPU >>> Memory -====== +------ .. code-block:: python @@ -193,7 +193,7 @@ Memory >>> Disks -===== +----- .. code-block:: python @@ -209,7 +209,7 @@ Disks >>> Network -======= +------- .. code-block:: python @@ -236,7 +236,7 @@ Network >>> Sensors -======= +------- .. code-block:: python @@ -255,7 +255,7 @@ Sensors >>> Other system info -================= +----------------- .. code-block:: python @@ -269,7 +269,7 @@ Other system info >>> Process management -================== +------------------ .. code-block:: python @@ -405,7 +405,7 @@ Process management >>> Further process APIs -==================== +-------------------- .. code-block:: python @@ -446,7 +446,7 @@ Popen wrapper: >>> Windows services -================ +---------------- .. code-block:: python @@ -466,9 +466,3 @@ Windows services 'start_type': 'manual', 'status': 'stopped', 'username': 'NT AUTHORITY\\LocalService'} - -Other samples -============= - -See `doc recipes `__ and -`demo scritps `__. diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index a098a72b1e..1519c94b17 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -80,8 +80,6 @@ def main(): s = hilite(s, ok=not ads) print(s) print("-" * 50) - # print("total: %s AccessDenied errors (%s total processes)" % ( - # tot_ads, tot_procs)) print("Totals: access-denied=%s, calls=%s, processes=%s" % ( tot_ads, tot_calls, tot_procs)) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 12040dcf43..a99293c458 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -6,7 +6,7 @@ """Benchmark all API calls. -$ python scripts/internal/print_api_speed.py +$ make print_api_speed SYSTEM APIS SECONDS ---------------------------------- boot_time 0.000140 From 59e3c5e2aa889d443f5f0e44beb52f654fc6e23e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 04:25:14 -0800 Subject: [PATCH 0199/1714] #1398 / win / cmdline: call NtQueryInformationProcess twice, the first time to get the right buf size (ProcessHacker does this) --- psutil/arch/windows/process_info.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 1cfd3b5cd7..3966bc7105 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -136,6 +136,8 @@ typedef struct { // ==================================================================== +#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) + /* * Return 1 if PID exists, 0 if not, -1 on error. */ @@ -746,6 +748,7 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { WCHAR * cmdline_buffer_wchar = NULL; PUNICODE_STRING tmp = NULL; DWORD string_size; + int ProcessCommandLineInformation = 60; cmdline_buffer = calloc(ret_length, 1); if (cmdline_buffer == NULL) { @@ -756,27 +759,42 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) goto error; + + // get the right buf size status = psutil_NtQueryInformationProcess( hProcess, - 60, // ProcessCommandLineInformation + ProcessCommandLineInformation, + NULL, + 0, + &ret_length); + if (status != STATUS_BUFFER_OVERFLOW && \ + status != STATUS_BUFFER_TOO_SMALL && \ + status != STATUS_INFO_LENGTH_MISMATCH) { + PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess(0)"); + goto error; + } + + // get the cmdline + status = psutil_NtQueryInformationProcess( + hProcess, + ProcessCommandLineInformation, cmdline_buffer, ret_length, &ret_length ); if (! NT_SUCCESS(status)) { - PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess"); + PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess(withlen)"); goto error; } + // build the string tmp = (PUNICODE_STRING)cmdline_buffer; string_size = wcslen(tmp->Buffer) + 1; cmdline_buffer_wchar = (WCHAR *)calloc(string_size, sizeof(WCHAR)); - if (cmdline_buffer_wchar == NULL) { PyErr_NoMemory(); goto error; } - wcscpy_s(cmdline_buffer_wchar, string_size, tmp->Buffer); *pdata = cmdline_buffer_wchar; *psize = string_size * sizeof(WCHAR); From 5964d6c2fc2a879ad1506085c9631ee0b1cd1fa5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 06:07:36 -0800 Subject: [PATCH 0200/1714] fix compilation warnings --- psutil/_psutil_windows.c | 4 ---- psutil/arch/windows/ntextapi.h | 4 ++-- psutil/arch/windows/process_handles.c | 6 +++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index e9db62a5a5..343f887803 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2747,11 +2747,7 @@ static char *get_region_protection_string(ULONG protection) { */ static PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { -#ifdef _WIN64 - MEMORY_BASIC_INFORMATION64 basicInfo; -#else MEMORY_BASIC_INFORMATION basicInfo; -#endif DWORD pid; HANDLE hProcess = NULL; PVOID baseAddress; diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 8007ec01c5..4a351b4143 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -227,8 +227,8 @@ typedef enum _KWAIT_REASON { typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { PVOID Object; - HANDLE UniqueProcessId; - HANDLE HandleValue; + ULONG_PTR UniqueProcessId; + ULONG_PTR HandleValue; ULONG GrantedAccess; USHORT CreatorBackTraceIndex; USHORT ObjectTypeIndex; diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 6f133ef978..b639d3819a 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -169,11 +169,11 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { hHandle = &pHandleInfo->Handles[i]; // Check if this hHandle belongs to the PID the user specified. - if (hHandle->UniqueProcessId != (HANDLE)dwPid) + if (hHandle->UniqueProcessId != (ULONG_PTR)dwPid) goto loop_cleanup; if (!DuplicateHandle(hProcess, - hHandle->HandleValue, + (HANDLE)hHandle->HandleValue, GetCurrentProcess(), &g_hFile, 0, @@ -365,7 +365,7 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { hHandle = &pHandleInfo->Handles[i]; // Check if this hHandle belongs to the PID the user specified. - if (hHandle->UniqueProcessId != (HANDLE)dwPid) + if (hHandle->UniqueProcessId != (ULONG_PTR)dwPid) goto loop_cleanup; if (!DuplicateHandle(hProcess, From 4fbc1b4e453312ec7d8de7a0393c76df3a2feb2e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 06:19:55 -0800 Subject: [PATCH 0201/1714] set proper SYSTEM_PROCESS_INFORMATION struct from PH --- psutil/arch/windows/ntextapi.h | 16 +++++++++------- psutil/arch/windows/process_info.c | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 4a351b4143..df5cfe114e 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -283,22 +283,24 @@ typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION { typedef struct _SYSTEM_PROCESS_INFORMATION2 { ULONG NextEntryOffset; ULONG NumberOfThreads; - LARGE_INTEGER SpareLi1; - LARGE_INTEGER SpareLi2; - LARGE_INTEGER SpareLi3; + LARGE_INTEGER WorkingSetPrivateSize; // since Vista + ULONG HardFaultCount; // since Win7 + ULONG NumberOfThreadsHighWatermark; // since Win7 + ULONGLONG CycleTime; // since Win7 LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ImageName; - LONG BasePriority; + KPRIORITY BasePriority; HANDLE UniqueProcessId; HANDLE InheritedFromUniqueProcessId; ULONG HandleCount; ULONG SessionId; - ULONG_PTR PageDirectoryBase; + // since VISTA (requires SystemExtendedProcessInformation) + ULONG_PTR UniqueProcessKey; SIZE_T PeakVirtualSize; SIZE_T VirtualSize; - DWORD PageFaultCount; + ULONG PageFaultCount; SIZE_T PeakWorkingSetSize; SIZE_T WorkingSetSize; SIZE_T QuotaPeakPagedPoolUsage; @@ -314,7 +316,7 @@ typedef struct _SYSTEM_PROCESS_INFORMATION2 { LARGE_INTEGER ReadTransferCount; LARGE_INTEGER WriteTransferCount; LARGE_INTEGER OtherTransferCount; - SYSTEM_THREAD_INFORMATION Threads[1]; + SYSTEM_THREAD_INFORMATION Threads[1]; // SystemProcessInformation } SYSTEM_PROCESS_INFORMATION2, *PSYSTEM_PROCESS_INFORMATION2; #define SYSTEM_PROCESS_INFORMATION SYSTEM_PROCESS_INFORMATION2 diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 3966bc7105..22b4ee911b 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -747,7 +747,7 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { char * cmdline_buffer = NULL; WCHAR * cmdline_buffer_wchar = NULL; PUNICODE_STRING tmp = NULL; - DWORD string_size; + size_t string_size; int ProcessCommandLineInformation = 60; cmdline_buffer = calloc(ret_length, 1); From ef792a35a2f8a94a5f30611d34443e589f9e2308 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 06:32:38 -0800 Subject: [PATCH 0202/1714] take defs from PH --- psutil/arch/windows/ntextapi.h | 89 ++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index df5cfe114e..e746541e5a 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -160,6 +160,10 @@ typedef struct { ULONG FirstLevelTbFills; ULONG SecondLevelTbFills; ULONG SystemCalls; + ULONGLONG CcTotalDirtyPages; // since THRESHOLD + ULONGLONG CcDirtyPageThreshold; // since THRESHOLD + LONGLONG ResidentAvailablePages; // since THRESHOLD + ULONGLONG SharedCommittedPages; // since THRESHOLD } _SYSTEM_PERFORMANCE_INFORMATION; typedef struct { @@ -180,49 +184,52 @@ typedef enum _KTHREAD_STATE { Waiting, Transition, DeferredReady, - GateWait, + GateWaitObsolete, + WaitingForProcessInSwap, MaximumThreadState } KTHREAD_STATE, *PKTHREAD_STATE; typedef enum _KWAIT_REASON { - Executive = 0, - FreePage = 1, - PageIn = 2, - PoolAllocation = 3, - DelayExecution = 4, - Suspended = 5, - UserRequest = 6, - WrExecutive = 7, - WrFreePage = 8, - WrPageIn = 9, - WrPoolAllocation = 10, - WrDelayExecution = 11, - WrSuspended = 12, - WrUserRequest = 13, - WrEventPair = 14, - WrQueue = 15, - WrLpcReceive = 16, - WrLpcReply = 17, - WrVirtualMemory = 18, - WrPageOut = 19, - WrRendezvous = 20, - Spare2 = 21, - Spare3 = 22, - Spare4 = 23, - Spare5 = 24, - WrCalloutStack = 25, - WrKernel = 26, - WrResource = 27, - WrPushLock = 28, - WrMutex = 29, - WrQuantumEnd = 30, - WrDispatchInt = 31, - WrPreempted = 32, - WrYieldExecution = 33, - WrFastMutex = 34, - WrGuardedMutex = 35, - WrRundown = 36, - MaximumWaitReason = 37 + Executive, + FreePage, + PageIn, + PoolAllocation, + DelayExecution, + Suspended, + UserRequest, + WrExecutive, + WrFreePage, + WrPageIn, + WrPoolAllocation, + WrDelayExecution, + WrSuspended, + WrUserRequest, + WrEventPair, + WrQueue, + WrLpcReceive, + WrLpcReply, + WrVirtualMemory, + WrPageOut, + WrRendezvous, + WrKeyedEvent, + WrTerminated, + WrProcessInSwap, + WrCpuRateControl, + WrCalloutStack, + WrKernel, + WrResource, + WrPushLock, + WrMutex, + WrQuantumEnd, + WrDispatchInt, + WrPreempted, + WrYieldExecution, + WrFastMutex, + WrGuardedMutex, + WrRundown, + WrAlertByThreadId, + WrDeferredPreempt, + MaximumWaitReason } KWAIT_REASON, *PKWAIT_REASON; typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { @@ -257,7 +264,7 @@ typedef struct _SYSTEM_THREAD_INFORMATION2 { ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientId; - LONG Priority; + KPRIORITY Priority; LONG BasePriority; ULONG ContextSwitches; ULONG ThreadState; @@ -274,7 +281,7 @@ typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION { PVOID StackBase; PVOID StackLimit; PVOID Win32StartAddress; - PTEB TebBase; + PTEB TebBase; // since VISTA ULONG_PTR Reserved2; ULONG_PTR Reserved3; ULONG_PTR Reserved4; From 60c62d11f03475a177f26f3eaddd6fdf0c5e0139 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 06:39:01 -0800 Subject: [PATCH 0203/1714] fix compiler warning --- psutil/_psutil_windows.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 343f887803..3de400df4c 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2186,7 +2186,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { char szDeviceDisplay[MAX_PATH]; int devNum; int i; - size_t ioctrlSize; + DWORD ioctrlSize; BOOL ret; PyObject *py_retdict = PyDict_New(); PyObject *py_tuple = NULL; From e105d6c0c509fb2a568f3985120257c5bf97740d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 06:42:48 -0800 Subject: [PATCH 0204/1714] fix compiler warning --- psutil/_psutil_windows.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 3de400df4c..baf197b980 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3293,7 +3293,7 @@ static PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { PROCESSOR_POWER_INFORMATION *ppi; NTSTATUS ret; - size_t size; + ULONG size; LPBYTE pBuffer = NULL; ULONG current; ULONG max; From edd4bd3fec71e65ce2006e15b3cf872ce2c11626 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 06:45:23 -0800 Subject: [PATCH 0205/1714] fix compiler warning --- psutil/arch/windows/process_handles.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index b639d3819a..997278d305 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -369,7 +369,7 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { goto loop_cleanup; if (!DuplicateHandle(hProcess, - hHandle->HandleValue, + (HANDLE)hHandle->HandleValue, GetCurrentProcess(), &hFile, 0, From 280a6caef5b24dbdff2be1ccc888aceb7a9324ea Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 06:49:24 -0800 Subject: [PATCH 0206/1714] fix compiler warning --- psutil/arch/windows/process_handles.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 997278d305..8b899972ed 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -318,7 +318,7 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { PyObject* py_path = NULL; ULONG dwSize = 0; LPVOID pMem = NULL; - TCHAR pszFilename[MAX_PATH+1]; + wchar_t pszFilename[MAX_PATH+1]; if (g_initialized == FALSE) psutil_get_open_files_init(FALSE); @@ -409,7 +409,7 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { } dwSize = GetMappedFileName( - GetCurrentProcess(), pMem, pszFilename, MAX_PATH); + GetCurrentProcess(), pMem, (LPSTR)pszFilename, MAX_PATH); if (dwSize == 0) { /* printf("[%d] GetMappedFileName (%#x): %#x \n", From 6148b137122a6893903bc9462f03e8edf333f5df Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 07:09:03 -0800 Subject: [PATCH 0207/1714] restore previous def --- psutil/arch/windows/ntextapi.h | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index e746541e5a..9530926bf7 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -290,24 +290,22 @@ typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION { typedef struct _SYSTEM_PROCESS_INFORMATION2 { ULONG NextEntryOffset; ULONG NumberOfThreads; - LARGE_INTEGER WorkingSetPrivateSize; // since Vista - ULONG HardFaultCount; // since Win7 - ULONG NumberOfThreadsHighWatermark; // since Win7 - ULONGLONG CycleTime; // since Win7 + LARGE_INTEGER SpareLi1; + LARGE_INTEGER SpareLi2; + LARGE_INTEGER SpareLi3; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ImageName; - KPRIORITY BasePriority; + LONG BasePriority; HANDLE UniqueProcessId; HANDLE InheritedFromUniqueProcessId; ULONG HandleCount; ULONG SessionId; - // since VISTA (requires SystemExtendedProcessInformation) - ULONG_PTR UniqueProcessKey; + ULONG_PTR PageDirectoryBase; SIZE_T PeakVirtualSize; SIZE_T VirtualSize; - ULONG PageFaultCount; + DWORD PageFaultCount; SIZE_T PeakWorkingSetSize; SIZE_T WorkingSetSize; SIZE_T QuotaPeakPagedPoolUsage; @@ -323,7 +321,7 @@ typedef struct _SYSTEM_PROCESS_INFORMATION2 { LARGE_INTEGER ReadTransferCount; LARGE_INTEGER WriteTransferCount; LARGE_INTEGER OtherTransferCount; - SYSTEM_THREAD_INFORMATION Threads[1]; // SystemProcessInformation +SYSTEM_THREAD_INFORMATION Threads[1]; } SYSTEM_PROCESS_INFORMATION2, *PSYSTEM_PROCESS_INFORMATION2; #define SYSTEM_PROCESS_INFORMATION SYSTEM_PROCESS_INFORMATION2 From 17b9727300cb7dd226d88654315c67f55b172e0c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 08:47:17 -0800 Subject: [PATCH 0208/1714] try to fix ntext.h --- psutil/arch/windows/ntextapi.h | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 9530926bf7..3927edf39d 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -160,10 +160,6 @@ typedef struct { ULONG FirstLevelTbFills; ULONG SecondLevelTbFills; ULONG SystemCalls; - ULONGLONG CcTotalDirtyPages; // since THRESHOLD - ULONGLONG CcDirtyPageThreshold; // since THRESHOLD - LONGLONG ResidentAvailablePages; // since THRESHOLD - ULONGLONG SharedCommittedPages; // since THRESHOLD } _SYSTEM_PERFORMANCE_INFORMATION; typedef struct { @@ -184,8 +180,7 @@ typedef enum _KTHREAD_STATE { Waiting, Transition, DeferredReady, - GateWaitObsolete, - WaitingForProcessInSwap, + GateWait, MaximumThreadState } KTHREAD_STATE, *PKTHREAD_STATE; @@ -234,8 +229,8 @@ typedef enum _KWAIT_REASON { typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { PVOID Object; - ULONG_PTR UniqueProcessId; - ULONG_PTR HandleValue; + HANDLE UniqueProcessId; + HANDLE HandleValue; ULONG GrantedAccess; USHORT CreatorBackTraceIndex; USHORT ObjectTypeIndex; @@ -264,7 +259,7 @@ typedef struct _SYSTEM_THREAD_INFORMATION2 { ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientId; - KPRIORITY Priority; + LONG Priority; LONG BasePriority; ULONG ContextSwitches; ULONG ThreadState; @@ -281,7 +276,7 @@ typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION { PVOID StackBase; PVOID StackLimit; PVOID Win32StartAddress; - PTEB TebBase; // since VISTA + PTEB TebBase; ULONG_PTR Reserved2; ULONG_PTR Reserved3; ULONG_PTR Reserved4; @@ -321,7 +316,7 @@ typedef struct _SYSTEM_PROCESS_INFORMATION2 { LARGE_INTEGER ReadTransferCount; LARGE_INTEGER WriteTransferCount; LARGE_INTEGER OtherTransferCount; -SYSTEM_THREAD_INFORMATION Threads[1]; + SYSTEM_THREAD_INFORMATION Threads[1]; } SYSTEM_PROCESS_INFORMATION2, *PSYSTEM_PROCESS_INFORMATION2; #define SYSTEM_PROCESS_INFORMATION SYSTEM_PROCESS_INFORMATION2 From e414895dc27a84482a2cb35f0c20f2c69530990d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 13:43:58 -0800 Subject: [PATCH 0209/1714] Restore Win-7 support on GIT master (5.5.1 was OK) (#1446) --- DEVGUIDE.rst | 2 +- HISTORY.rst | 3 -- psutil/_psutil_windows.c | 8 ++-- psutil/_pswindows.py | 17 ++++--- psutil/arch/windows/global.c | 2 +- psutil/arch/windows/process_info.c | 77 +++++++++++++++--------------- psutil/tests/test_windows.py | 2 +- scripts/internal/winmake.py | 19 ++++---- 8 files changed, 68 insertions(+), 62 deletions(-) diff --git a/DEVGUIDE.rst b/DEVGUIDE.rst index 1d1baf1f7e..df031bdeab 100644 --- a/DEVGUIDE.rst +++ b/DEVGUIDE.rst @@ -82,7 +82,7 @@ On Windows: .. code-block:: bat - set TSCRIPT=foo.py && make test + make test foo.py Adding a new feature ==================== diff --git a/HISTORY.rst b/HISTORY.rst index 2428f033a8..e293626834 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -33,9 +33,6 @@ XXXX-XX-XX a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. - 1427_: [OSX] Process cmdline() and environ() may erroneously raise OSError on failed malloc(). -- 1431_: [Windows] GetNativeSystemInfo is not used instead of GetSystemInfo in - order to support WoW64 processes. Affected APIs are psutil.cpu_count(), - and Process memory_maps() and memory_info_exe() ("uss" field). - 1432_: [Windows] Process.memory_info_ex()'s USS memory is miscalculated because we're not using the actual system PAGESIZE. - 1439_: [NetBSD] Process.connections() may return incomplete results if using diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index baf197b980..5c05ac388e 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -106,12 +106,12 @@ psutil_get_num_cpus(int fail_on_err) { } else { psutil_debug("GetActiveProcessorCount() not available; " - "using GetNativeSystemInfo()"); + "using GetSystemInfo()"); ncpus = (unsigned int)PSUTIL_SYSTEM_INFO.dwNumberOfProcessors; - if ((ncpus == 0) && (fail_on_err == 1)) { + if ((ncpus <= 0) && (fail_on_err == 1)) { PyErr_SetString( PyExc_RuntimeError, - "GetNativeSystemInfo() failed to retrieve CPU count"); + "GetSystemInfo() failed to retrieve CPU count"); } } return ncpus; @@ -584,13 +584,13 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { if ((pid == 0) || (pid == 4)) return Py_BuildValue("[]"); - use_peb = (py_usepeb == Py_True); pid_return = psutil_pid_is_running(pid); if (pid_return == 0) return NoSuchProcess(""); if (pid_return == -1) return NULL; + use_peb = (py_usepeb == Py_True) ? 1 : 0; return psutil_get_cmdline(pid, use_peb); } diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 0ab8afe4b9..3fcee4500f 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -731,13 +731,18 @@ def exe(self): @wrap_exceptions def cmdline(self): - try: + 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 err.errno in ACCESS_DENIED_ERRSET: + ret = cext.proc_cmdline(self.pid, use_peb=False) + else: + raise + else: ret = cext.proc_cmdline(self.pid, use_peb=True) - except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - ret = cext.proc_cmdline(self.pid, use_peb=False) - else: - raise if PY3: return ret else: diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index 6134687de3..a622b63563 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -180,7 +180,7 @@ psutil_set_winver() { static int psutil_load_sysinfo() { - GetNativeSystemInfo(&PSUTIL_SYSTEM_INFO); + GetSystemInfo(&PSUTIL_SYSTEM_INFO); return 0; } diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 22b4ee911b..946a01cbca 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -735,24 +735,24 @@ psutil_get_process_data(long pid, /* - * Get process cmdline() by using NtQueryInformationProcess. This is - * useful on Windows 8.1+ in order to get less ERROR_ACCESS_DENIED - * errors when querying privileged PIDs. + * Get process cmdline by using NtQueryInformationProcess. This is a + * method alternative to PEB which is less likely to result in + * AccessDenied. Requires Windows 8.1+. */ static int -psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { +psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { HANDLE hProcess; - ULONG ret_length = 4096; + ULONG bufLen = 0; NTSTATUS status; - char * cmdline_buffer = NULL; - WCHAR * cmdline_buffer_wchar = NULL; + char * buffer = NULL; + WCHAR * bufWchar = NULL; PUNICODE_STRING tmp = NULL; - size_t string_size; + size_t size; int ProcessCommandLineInformation = 60; - cmdline_buffer = calloc(ret_length, 1); - if (cmdline_buffer == NULL) { - PyErr_NoMemory(); + if (PSUTIL_WINVER < PSUTIL_WINDOWS_8_1) { + PyErr_SetString( + PyExc_RuntimeError, "requires Windows 8.1+"); goto error; } @@ -766,7 +766,7 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { ProcessCommandLineInformation, NULL, 0, - &ret_length); + &bufLen); if (status != STATUS_BUFFER_OVERFLOW && \ status != STATUS_BUFFER_TOO_SMALL && \ status != STATUS_INFO_LENGTH_MISMATCH) { @@ -774,13 +774,20 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { goto error; } + // allocate memory + buffer = calloc(bufLen, 1); + if (buffer == NULL) { + PyErr_NoMemory(); + goto error; + } + // get the cmdline status = psutil_NtQueryInformationProcess( hProcess, ProcessCommandLineInformation, - cmdline_buffer, - ret_length, - &ret_length + buffer, + bufLen, + &bufLen ); if (! NT_SUCCESS(status)) { PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess(withlen)"); @@ -788,23 +795,23 @@ psutil_get_cmdline_data(long pid, WCHAR **pdata, SIZE_T *psize) { } // build the string - tmp = (PUNICODE_STRING)cmdline_buffer; - string_size = wcslen(tmp->Buffer) + 1; - cmdline_buffer_wchar = (WCHAR *)calloc(string_size, sizeof(WCHAR)); - if (cmdline_buffer_wchar == NULL) { + tmp = (PUNICODE_STRING)buffer; + size = wcslen(tmp->Buffer) + 1; + bufWchar = (WCHAR *)calloc(size, sizeof(WCHAR)); + if (bufWchar == NULL) { PyErr_NoMemory(); goto error; } - wcscpy_s(cmdline_buffer_wchar, string_size, tmp->Buffer); - *pdata = cmdline_buffer_wchar; - *psize = string_size * sizeof(WCHAR); - free(cmdline_buffer); + wcscpy_s(bufWchar, size, tmp->Buffer); + *pdata = bufWchar; + *psize = size * sizeof(WCHAR); + free(buffer); CloseHandle(hProcess); return 0; error: - if (cmdline_buffer != NULL) - free(cmdline_buffer); + if (buffer != NULL) + free(buffer); if (hProcess != NULL) CloseHandle(hProcess); return -1; @@ -827,24 +834,18 @@ psutil_get_cmdline(long pid, int use_peb) { int func_ret; /* - By defaut, still use PEB (if command line params have been patched in - the PEB, we will get the actual ones). Reading the PEB to get the - command line parameters still seem to be the best method if somebody - has tampered with the parameters after creating the process. + Reading the PEB to get the cmdline seem to be the best method if + somebody has tampered with the parameters after creating the process. For instance, create a process as suspended, patch the command line - in its PEB and unfreeze it. - The process will use the "new" parameters whereas the system - (with NtQueryInformationProcess) will give you the "old" ones - See: + in its PEB and unfreeze it. It requires more privileges than + NtQueryInformationProcess though (the fallback): - https://github.com/giampaolo/psutil/pull/1398 - https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/ */ - if (use_peb == 1) { + if (use_peb == 1) func_ret = psutil_get_process_data(pid, KIND_CMDLINE, &data, &size); - } - else { - func_ret = psutil_get_cmdline_data(pid, &data, &size); - } + else + func_ret = psutil_cmdline_query_proc(pid, &data, &size); if (func_ret != 0) goto out; diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index ff63cd79cc..c98d892cfa 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -82,7 +82,7 @@ def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): def test_cpu_count_vs_GetSystemInfo(self): # Will likely fail on many-cores systems: # https://stackoverflow.com/questions/31209256 - sys_value = win32api.GetNativeSystemInfo()[5] + sys_value = win32api.GetSystemInfo()[5] psutil_value = psutil.cpu_count() self.assertEqual(sys_value, psutil_value) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index cd26c67edb..c35853c5ee 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -29,7 +29,7 @@ PYTHON = sys.executable else: PYTHON = os.getenv('PYTHON', sys.executable) -TSCRIPT = os.getenv('TSCRIPT', 'psutil\\tests\\__main__.py') +TEST_SCRIPT = 'psutil\\tests\\__main__.py' GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" PY3 = sys.version_info[0] == 3 HERE = os.path.abspath(os.path.dirname(__file__)) @@ -350,9 +350,16 @@ def flake8(): @cmd def test(): """Run tests""" + try: + arg = sys.argv[2] + except IndexError: + arg = TEST_SCRIPT + install() test_setup() - sh("%s %s" % (PYTHON, TSCRIPT)) + cmdline = "%s %s" % (PYTHON, arg) + safe_print(cmdline) + sh(cmdline) @cmd @@ -361,7 +368,7 @@ def coverage(): # Note: coverage options are controlled by .coveragerc file install() test_setup() - sh("%s -m coverage run %s" % (PYTHON, TSCRIPT)) + sh("%s -m coverage run %s" % (PYTHON, TEST_SCRIPT)) sh("%s -m coverage report" % PYTHON) sh("%s -m coverage html" % PYTHON) sh("%s -m webbrowser -t htmlcov/index.html" % PYTHON) @@ -426,11 +433,7 @@ def test_contracts(): @cmd def test_by_name(): """Run test by name""" - try: - safe_print(sys.argv) - name = sys.argv[2] - except IndexError: - sys.exit('second arg missing') + name = sys.argv[2] install() test_setup() sh("%s -m unittest -v %s" % (PYTHON, name)) From 4a283d62b687b849b89991a03a4099c53fd9f125 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Feb 2019 23:49:29 +0100 Subject: [PATCH 0210/1714] #1291: (BACKWARD-INCOMPATIBLE) remove memory_maps() on OSX --- HISTORY.rst | 6 +++--- docs/index.rst | 9 ++------- psutil/_psosx.py | 6 ------ psutil/tests/__init__.py | 2 +- psutil/tests/test_contracts.py | 11 ++--------- psutil/tests/test_memory_leaks.py | 2 -- psutil/tests/test_process.py | 2 -- 7 files changed, 8 insertions(+), 30 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2428f033a8..6b2a23f799 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -41,10 +41,10 @@ XXXX-XX-XX - 1439_: [NetBSD] Process.connections() may return incomplete results if using oneshot() -**API changes** +**Incompatible API changes** -- 1291_: [OSX] Process.memory_maps() is deprecated and will always raise - AccessDenied. It will be removed in psutil 6.0.0. +- 1291_: [OSX] Process.memory_maps() was removed because inherently broken + (segfault) for years. 5.5.1 ===== diff --git a/docs/index.rst b/docs/index.rst index 6d464c8545..73a81cdbf3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1708,14 +1708,9 @@ Process class Availability: Linux, Windows, FreeBSD, SunOS - .. warning:: - on macOS, starting from version 5.6.0, this function is deprecated and - will always raise :class:`psutil.AccessDenied`. It is scheduled for - removal in 6.0.0 - (see `issue 1020 `_). - .. versionchanged:: - 5.6.0 deprecated on macOS, always raise AccessDenied + 5.6.0 removed macOS support because inherently broken (see + issue `#1291 `__) .. method:: children(recursive=False) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 78f710aa33..20c05612a1 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -8,7 +8,6 @@ import errno import functools import os -import warnings from socket import AF_INET from collections import namedtuple @@ -575,8 +574,3 @@ def threads(self): ntuple = _common.pthread(thread_id, utime, stime) retlist.append(ntuple) return retlist - - def memory_maps(self): - msg = "memory_maps() on OSX is deprecated and will be removed in 6.0.0" - warnings.warn(msg, category=DeprecationWarning, stacklevel=2) - raise AccessDenied(self.pid, msg=msg) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 86af2cfb50..545f5f2519 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -167,7 +167,7 @@ HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") HAS_IONICE = hasattr(psutil.Process, "ionice") HAS_MEMORY_FULL_INFO = 'uss' in psutil.Process().memory_full_info()._fields -HAS_MEMORY_MAPS = not MACOS and hasattr(psutil.Process, "memory_maps") +HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") HAS_RLIMIT = hasattr(psutil.Process, "rlimit") HAS_THREADS = hasattr(psutil.Process, "threads") diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 97a1a0f069..1f02156f34 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -167,7 +167,8 @@ def test_proc_cpu_num(self): def test_proc_memory_maps(self): hasit = hasattr(psutil.Process, "memory_maps") - self.assertEqual(hasit, False if OPENBSD or NETBSD or AIX else True) + self.assertEqual( + hasit, False if OPENBSD or NETBSD or AIX or MACOS else True) # =================================================================== @@ -185,14 +186,6 @@ def test_memory_info_ex(self): self.assertIn("memory_info_ex() is deprecated", str(w.message)) self.assertIn("use memory_info() instead", str(w.message)) - @unittest.skipIf(not MACOS, "deprecated on macOS") - def test_memory_maps_osx(self): - with warnings.catch_warnings(record=True) as ws: - with self.assertRaises(psutil.AccessDenied): - psutil.Process().memory_maps() - w = ws[0] - self.assertIsInstance(w.category(), DeprecationWarning) - # =================================================================== # --- System API types diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 1612857162..07d7f06807 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -344,8 +344,6 @@ def test_open_files(self): with open(TESTFN, 'w'): self.execute(self.proc.open_files) - # MACOS implementation is unbelievably slow - @unittest.skipIf(MACOS, "too slow on MACOS") @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") @skip_if_linux() def test_memory_maps(self): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 31f42ebaad..2af676ba9f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1291,8 +1291,6 @@ def test_halfway_terminated_process(self): ret = meth([0]) elif name == 'send_signal': ret = meth(signal.SIGTERM) - elif MACOS and name == 'memory_maps': - continue # XXX else: ret = meth() except psutil.ZombieProcess: From 17832436049d10265e6c1fb9ba8dcc6e9572da58 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Mar 2019 00:44:21 +0100 Subject: [PATCH 0211/1714] Update issue templates --- .../ISSUE_TEMPLATE/-os--bug-description.md | 11 ++++++ .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/-os--bug-description.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/-os--bug-description.md b/.github/ISSUE_TEMPLATE/-os--bug-description.md new file mode 100644 index 0000000000..4661e1121d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-os--bug-description.md @@ -0,0 +1,11 @@ +--- +name: "[OS] bug description" +about: "[OS] bug description" +title: "[OS] bug description" +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. If you can include OS, kernel and psutil version diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..dd84ea7824 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. From b5d68fb32a58cc38f8860dc8cf6b22060640979c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Mar 2019 00:58:43 +0100 Subject: [PATCH 0212/1714] remove issue template commited by accident --- .../ISSUE_TEMPLATE/-os--bug-description.md | 11 ------ .github/ISSUE_TEMPLATE/bug_report.md | 38 ------------------- 2 files changed, 49 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/-os--bug-description.md delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/-os--bug-description.md b/.github/ISSUE_TEMPLATE/-os--bug-description.md deleted file mode 100644 index 4661e1121d..0000000000 --- a/.github/ISSUE_TEMPLATE/-os--bug-description.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: "[OS] bug description" -about: "[OS] bug description" -title: "[OS] bug description" -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. If you can include OS, kernel and psutil version diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea7824..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. From 9e501a4b23c2ce09fba6cf0836a6c04d5dbcb605 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Mar 2019 02:50:09 +0100 Subject: [PATCH 0213/1714] add issue templates for 'bug' and 'enhancement' types --- .github/ISSUE_TEMPLATE/bug.md | 22 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/enhancement.md | 11 +++++++++++ .github/placeholder | 0 3 files changed, 33 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/enhancement.md create mode 100644 .github/placeholder diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000000..ca9483d30d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,22 @@ +--- +name: Bug +about: Report a bug +title: "[OS] title" +labels: 'bug' +assignees: 'giampaolo' + +--- + +**Platform** + +* { OS version } +* { psutil version (pip show psutil) } + +**Bug description** + +{ a clear and concise description of what the bug is } + +``` +traceback message (if any) +``` + diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 0000000000..408728ee37 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,11 @@ +--- +name: Enhancement +about: Propose an enhancement +title: "title" +labels: 'enhancement' +assignees: 'giampaolo' + +--- + +{ describe the proposal } + diff --git a/.github/placeholder b/.github/placeholder new file mode 100644 index 0000000000..e69de29bb2 From 611a12323c73e063d4b33eea363dc073388d4686 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Mar 2019 03:13:37 +0100 Subject: [PATCH 0214/1714] move doc; rephrase it a bit --- .github/ISSUE_TEMPLATE/bug.md | 4 -- HISTORY.rst | 2 +- INSTALL.rst | 67 ++++++++++--------------------- README.rst | 2 +- DEVGUIDE.rst => docs/DEVGUIDE.rst | 0 IDEAS => docs/DEVNOTES | 0 tox.ini | 2 +- 7 files changed, 24 insertions(+), 53 deletions(-) rename DEVGUIDE.rst => docs/DEVGUIDE.rst (100%) rename IDEAS => docs/DEVNOTES (100%) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index ca9483d30d..9f377b2f7b 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -8,15 +8,11 @@ assignees: 'giampaolo' --- **Platform** - * { OS version } * { psutil version (pip show psutil) } **Bug description** - { a clear and concise description of what the bug is } - ``` traceback message (if any) ``` - diff --git a/HISTORY.rst b/HISTORY.rst index 537b8b29c9..35a9ae6039 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -922,7 +922,7 @@ XXXX-XX-XX - 646_: continuous tests integration for Windows with https://ci.appveyor.com/project/giampaolo/psutil. - 647_: new dev guide: - https://github.com/giampaolo/psutil/blob/master/DEVGUIDE.rst + https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst - 651_: continuous code quality test integration with scrutinizer-ci.com **Bug fixes** diff --git a/INSTALL.rst b/INSTALL.rst index b2e8941493..be9f94fa3a 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -3,22 +3,16 @@ Install pip pip is the easiest way to install psutil. It is shipped by default with Python 2.7.9+ and 3.4+. For other Python versions you can install it manually. -On Linux or via wget: - -.. code-block:: bash +On Linux or via wget:: wget https://bootstrap.pypa.io/get-pip.py -O - | python -On macOS or via curl: - -.. code-block:: bash +On macOS or via curl:: python < <(curl -s https://bootstrap.pypa.io/get-pip.py) On Windows, `download pip `__, open -cmd.exe and install it: - -.. code-block:: bat +cmd.exe and install it:: C:\Python27\python.exe get-pip.py @@ -28,32 +22,25 @@ Permission issues (UNIX) The commands below assume you're running as root. If you're not or you bump into permission errors you can either: -* prepend ``sudo``, e.g.: +* install psutil for your user only:: -.. code-block:: bash + pip install --user psutil - sudo pip install psutil -* install psutil for your user only (not at system level): +* prepend ``sudo``, e.g.:: -.. code-block:: bash + sudo pip install psutil - pip install --user psutil Linux ===== -Ubuntu / Debian: - -.. code-block:: bash +Ubuntu / Debian:: sudo apt-get install gcc python-dev python-pip pip install psutil -RedHat / CentOS: - - -.. code-block:: bash +RedHat / CentOS:: sudo yum install gcc python-devel python-pip pip install psutil @@ -63,19 +50,16 @@ If you're on Python 3 use ``python3-dev`` and ``python3-pip`` instead. macOS ===== -Install `Xcode `__ -first, then: +* install Xcode: https://developer.apple.com/downloads/?name=Xcode -.. code-block:: bash +* run:: pip install psutil Windows ======= -Open a cmd.exe shell and run: - -.. code-block:: +Open a cmd.exe shell and run:: python -m pip install psutil @@ -95,9 +79,7 @@ Compiling 64 bit versions of Python 2.6 and 2.7 with VS 2008 requires `Windows SDK and .NET Framework 3.5 SP1 `__. Once installed run `vcvars64.bat` (see `here `__). -Once VS is setup open a cmd.exe shell, cd into psutil directory and run: - -.. code-block:: bat +Once VS is setup open a cmd.exe shell, cd into psutil directory and run:: python setup.py build python setup.py install @@ -105,16 +87,14 @@ Once VS is setup open a cmd.exe shell, cd into psutil directory and run: FreeBSD ======= -.. code-block:: bash - +:: pkg install python gcc python -m pip install psutil OpenBSD ======= -.. code-block:: bash - +:: export PKG_PATH="http://ftp.openbsd.org/pub/OpenBSD/`uname -r`/packages/`arch -s`/" pkg_add -v python gcc python -m pip install psutil @@ -122,8 +102,7 @@ OpenBSD NetBSD ====== -.. code-block:: bash - +:: export PKG_PATH="ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" pkg_add -v pkgin pkgin install python gcc @@ -134,29 +113,25 @@ Solaris If ``cc`` compiler is not installed create a symlink to ``gcc``: -.. code-block:: bash - +:: sudo ln -s /usr/bin/gcc /usr/local/bin/cc Install: -.. code-block:: bash - +:: pkg install gcc python -m pip install psutil Install from sources ==================== -.. code-block:: bash - +:: git clone https://github.com/giampaolo/psutil.git cd psutil python setup.py install - Dev Guide ========= -If you plan on hacking on psutil you may want to take a look at the -`dev guide `__. +If you plan on hacking on psutil you may want to take a look at the dev guide: +https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst diff --git a/README.rst b/README.rst index 6c16373984..18a70a7168 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,7 @@ Quick links - `Forum `_ - `StackOverflow `_ - `Blog `_ -- `Development guide `_ +- `Development guide `_ - `What's new `_ diff --git a/DEVGUIDE.rst b/docs/DEVGUIDE.rst similarity index 100% rename from DEVGUIDE.rst rename to docs/DEVGUIDE.rst diff --git a/IDEAS b/docs/DEVNOTES similarity index 100% rename from IDEAS rename to docs/DEVNOTES diff --git a/tox.ini b/tox.ini index 61d63eee73..947fbeda7b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ # directory. [tox] -envlist = py26, py27, py34, py35, py36, lint +envlist = py26, py27, py34, py35, py36, py37, lint [testenv] deps = From d8659cfa35a0da589cb0a303f0e856c0f0d1b087 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Mar 2019 03:29:28 +0100 Subject: [PATCH 0215/1714] add new make command to check tar.gz sanity --- Makefile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 24ee6db6c5..592cda44ea 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ DEPS = \ sphinx \ twine \ unittest2 \ + virtualenv \ wheel # In not in a virtualenv, add --user options for install commands. @@ -206,9 +207,16 @@ upload-win-wheels: ## Upload wheels in dist/* directory on PyPI. # --- others +check-src-dist: ## Make sure we can install from the (MANIFEST-based) tar.gz + rm -rf dist + $(PYTHON) -m virtualenv build/venv + PYTHONWARNINGS=all $(PYTHON) setup.py sdist + build/venv/bin/python -m pip install dist/*.tar.gz + build/venv/bin/python -c "import psutil" + pre-release: ## Check if we're ready to produce a new release. rm -rf dist - ${MAKE} install + ${MAKE} check-src-dist ${MAKE} generate-manifest git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain ${MAKE} win-download-wheels From d01a9eaa35a8aadf6c519839e987a49d8be2d891 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Mar 2019 04:12:27 +0100 Subject: [PATCH 0216/1714] make pre-release checks/install src dist in a venv --- MANIFEST.in | 9 +++++++-- Makefile | 17 +++++++---------- scripts/internal/generate_manifest.py | 11 ++++++----- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index ba7978e2b3..321684c10c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,14 +2,14 @@ include .coveragerc include .git-pre-commit include .gitignore include CREDITS -include DEVGUIDE.rst include HISTORY.rst -include IDEAS include INSTALL.rst include LICENSE include MANIFEST.in include Makefile include README.rst +include docs/DEVGUIDE.rst +include docs/DEVNOTES include docs/Makefile include docs/README include docs/_static/copybutton.js @@ -66,6 +66,8 @@ include psutil/arch/solaris/environ.c include psutil/arch/solaris/environ.h include psutil/arch/solaris/v10/ifaddrs.c include psutil/arch/solaris/v10/ifaddrs.h +include psutil/arch/windows/global.c +include psutil/arch/windows/global.h include psutil/arch/windows/inet_ntop.c include psutil/arch/windows/inet_ntop.h include psutil/arch/windows/ntextapi.h @@ -106,9 +108,12 @@ include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py include scripts/internal/download_exes.py include scripts/internal/generate_manifest.py +include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py +include scripts/internal/print_api_speed.py include scripts/internal/print_timeline.py include scripts/internal/purge_installation.py +include scripts/internal/scriptutils.py include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py diff --git a/Makefile b/Makefile index 592cda44ea..23dc59338a 100644 --- a/Makefile +++ b/Makefile @@ -184,8 +184,6 @@ install-git-hooks: ## Install GIT pre-commit hook. # Distribution # =================================================================== -# --- create - sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest $(PYTHON) setup.py sdist @@ -196,8 +194,6 @@ wheel: ## Generate wheel. win-download-wheels: ## Download wheels hosted on appveyor. $(TEST_PREFIX) $(PYTHON) scripts/internal/download_exes.py --user giampaolo --project psutil -# --- upload - upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist $(PYTHON) setup.py sdist upload @@ -207,16 +203,17 @@ upload-win-wheels: ## Upload wheels in dist/* directory on PyPI. # --- others -check-src-dist: ## Make sure we can install from the (MANIFEST-based) tar.gz +check-sdist: ## Create source distribution and checks its sanity (MANIFEST) rm -rf dist - $(PYTHON) -m virtualenv build/venv + ${MAKE} clean + $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv PYTHONWARNINGS=all $(PYTHON) setup.py sdist - build/venv/bin/python -m pip install dist/*.tar.gz - build/venv/bin/python -c "import psutil" + build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz + build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" pre-release: ## Check if we're ready to produce a new release. - rm -rf dist - ${MAKE} check-src-dist + ${MAKE} check-sdist + ${MAKE} install ${MAKE} generate-manifest git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain ${MAKE} win-download-wheels diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 3511b74926..f6cf0eaa05 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -12,8 +12,9 @@ import subprocess -IGNORED_EXTS = ('.png', '.jpg', '.jpeg') -IGNORED_FILES = ('.travis.yml', 'appveyor.yml') +SKIP_EXTS = ('.png', '.jpg', '.jpeg') +SKIP_FILES = ('.travis.yml', 'appveyor.yml') +SKIP_PREFIXES = ('.ci/', '.github/') def sh(cmd): @@ -24,9 +25,9 @@ def sh(cmd): def main(): files = sh("git ls-files").split('\n') for file in files: - if file.startswith('.ci/') or \ - os.path.splitext(file)[1].lower() in IGNORED_EXTS or \ - file in IGNORED_FILES: + if file.startswith(SKIP_PREFIXES) or \ + os.path.splitext(file)[1].lower() in SKIP_EXTS or \ + file in SKIP_FILES: continue print("include " + file) From 784fac235b6305065eda1ff47a5df86c16eb866c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Mar 2019 16:24:48 +0100 Subject: [PATCH 0217/1714] update doc --- INSTALL.rst | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index be9f94fa3a..567758c959 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -20,18 +20,15 @@ Permission issues (UNIX) ======================== The commands below assume you're running as root. -If you're not or you bump into permission errors you can either: - -* install psutil for your user only:: +If you're not or you bump into permission errors you can either install psutil +for your user only:: pip install --user psutil - -* prepend ``sudo``, e.g.:: +...or prepend ``sudo``, e.g.:: sudo pip install psutil - Linux ===== @@ -50,9 +47,7 @@ If you're on Python 3 use ``python3-dev`` and ``python3-pip`` instead. macOS ===== -* install Xcode: https://developer.apple.com/downloads/?name=Xcode - -* run:: +Install `Xcode `__ then run:: pip install psutil @@ -88,17 +83,21 @@ FreeBSD ======= :: + pkg install python gcc python -m pip install psutil + OpenBSD ======= :: + export PKG_PATH="http://ftp.openbsd.org/pub/OpenBSD/`uname -r`/packages/`arch -s`/" pkg_add -v python gcc python -m pip install psutil + NetBSD ====== @@ -108,30 +107,36 @@ NetBSD pkgin install python gcc python -m pip install psutil + Solaris ======= If ``cc`` compiler is not installed create a symlink to ``gcc``: :: + sudo ln -s /usr/bin/gcc /usr/local/bin/cc Install: :: + pkg install gcc python -m pip install psutil + Install from sources ==================== :: + git clone https://github.com/giampaolo/psutil.git cd psutil python setup.py install + Dev Guide ========= -If you plan on hacking on psutil you may want to take a look at the dev guide: -https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst +If you wan to hacking or contribute on psutil continue reading the +`dev guide `__ From 7a9e7969f172c80507416a0fb1df98bf72e71139 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 2 Mar 2019 21:10:40 +0100 Subject: [PATCH 0218/1714] update doc + change git hook location --- Makefile | 2 +- docs/index.rst | 18 ++++++++++-------- psutil/DEVNOTES | 5 ----- psutil/__init__.py | 3 --- .../internal/.git-pre-commit | 0 scripts/internal/winmake.py | 2 +- 6 files changed, 12 insertions(+), 18 deletions(-) delete mode 100644 psutil/DEVNOTES rename .git-pre-commit => scripts/internal/.git-pre-commit (100%) diff --git a/Makefile b/Makefile index 23dc59338a..59ac150683 100644 --- a/Makefile +++ b/Makefile @@ -177,7 +177,7 @@ git-tag-release: ## Git-tag a new release. git push --follow-tags install-git-hooks: ## Install GIT pre-commit hook. - ln -sf ../../.git-pre-commit .git/hooks/pre-commit + ln -sf ../../scripts/internal/.git-pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit # =================================================================== diff --git a/docs/index.rst b/docs/index.rst index 73a81cdbf3..23a7254343 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2313,7 +2313,7 @@ Constants Unicode ======= -Starting from version 5.3.0 psutil fully supports unicode, see +Starting from version 5.3.0 psutil adds unicode support, see `issue #1040 `__. The notes below apply to *any* API returning a string such as :meth:`Process.exe` or :meth:`Process.cwd`, including non-filesystem related @@ -2352,9 +2352,6 @@ and 3:: Recipes ======= -Follows a collection of utilities and examples which are common but not generic -enough to be part of the public API. - Find process by name -------------------- @@ -2406,8 +2403,7 @@ Kill process tree "on_terminate", if specified, is a callabck function which is called as soon as a child terminates. """ - if pid == os.getpid(): - raise RuntimeError("I refuse to kill myself") + assert pid != os.getpid(), "won't kill myself" parent = psutil.Process(pid) children = parent.children(recursive=True) if include_parent: @@ -2437,13 +2433,19 @@ resources. procs = psutil.Process().children() # send SIGTERM for p in procs: - p.terminate() + try: + p.terminate() + except psutil.NoSuchProcess: + pass gone, alive = psutil.wait_procs(procs, timeout=timeout, callback=on_terminate) if alive: # send SIGKILL for p in alive: print("process {} survived SIGTERM; trying SIGKILL" % p) - p.kill() + try: + p.kill() + except psutil.NoSuchProcess: + pass gone, alive = psutil.wait_procs(alive, timeout=timeout, callback=on_terminate) if alive: # give up diff --git a/psutil/DEVNOTES b/psutil/DEVNOTES deleted file mode 100644 index 4fd15ea311..0000000000 --- a/psutil/DEVNOTES +++ /dev/null @@ -1,5 +0,0 @@ -API REFERENCES -============== - -- psutil.sensors_battery: - https://github.com/Kentzo/Power/ diff --git a/psutil/__init__.py b/psutil/__init__.py index d8ec701cd6..449a27ca79 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -922,9 +922,6 @@ 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())) else: diff --git a/.git-pre-commit b/scripts/internal/.git-pre-commit similarity index 100% rename from .git-pre-commit rename to scripts/internal/.git-pre-commit diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c35853c5ee..cbdeebdc9c 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -464,7 +464,7 @@ def test_memleaks(): def install_git_hooks(): """Install GIT pre-commit hook.""" if os.path.isdir('.git'): - src = os.path.join(ROOT_DIR, ".git-pre-commit") + src = os.path.join(ROOT_DIR, "scripts", "internal", ".git-pre-commit") dst = os.path.realpath( os.path.join(ROOT_DIR, ".git", "hooks", "pre-commit")) with open(src, "rt") as s: From 95244de84f59f4793bc806196b383fbd217aa639 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 Mar 2019 22:40:12 +0100 Subject: [PATCH 0219/1714] fix #1447: we weren't use @wrap_exceptions around oneshot() (doh\!) --- HISTORY.rst | 4 +++- psutil/_psaix.py | 3 +++ psutil/_psbsd.py | 1 + psutil/_pslinux.py | 3 +++ psutil/_psosx.py | 2 ++ psutil/_pssunos.py | 8 ++++---- psutil/_pswindows.py | 1 + 7 files changed, 17 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 35a9ae6039..5f3c38f2e7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -36,7 +36,9 @@ XXXX-XX-XX - 1432_: [Windows] Process.memory_info_ex()'s USS memory is miscalculated because we're not using the actual system PAGESIZE. - 1439_: [NetBSD] Process.connections() may return incomplete results if using - oneshot() + oneshot(). +- 1447_: original exception wasn't turned into NSP/AD exceptions when using + Process.oneshot() ctx manager. **Incompatible API changes** diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 96ed947608..4fc87827b1 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -371,14 +371,17 @@ def oneshot_exit(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): 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) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 92864cea6e..3d9dfdab69 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -611,6 +611,7 @@ def _assert_alive(self): # 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.""" diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 2eadc4c7dc..41be6665fb 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1546,6 +1546,7 @@ def _assert_alive(self): # 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 and return a dict with various @@ -1579,6 +1580,7 @@ def _parse_stat_file(self): return ret + @wrap_exceptions @memoize_when_activated def _read_status_file(self): """Read /proc/{pid}/stat file and return its content. @@ -1588,6 +1590,7 @@ 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), diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 20c05612a1..7459a0f3d8 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -384,6 +384,7 @@ def __init__(self, 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. @@ -391,6 +392,7 @@ 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. diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 17469eacc6..67166e464a 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -396,22 +396,22 @@ def oneshot_exit(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): 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): diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 3fcee4500f..9fa14af449 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -686,6 +686,7 @@ def oneshot_enter(self): def oneshot_exit(self): self.oneshot_info.cache_deactivate(self) + @wrap_exceptions @memoize_when_activated def oneshot_info(self): """Return multiple information about this process as a From 5357f94010f49bed052046ff07bb534d569bff64 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Mar 2019 12:36:11 +0100 Subject: [PATCH 0220/1714] pre-release --- HISTORY.rst | 7 ++-- INSTALL.rst | 54 ++++++++++++++----------------- MANIFEST.in | 3 +- docs/index.rst | 6 +++- scripts/internal/download_exes.py | 36 ++++++++++----------- 5 files changed, 54 insertions(+), 52 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5f3c38f2e7..18bd4833b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.6.0 ===== -XXXX-XX-XX +2019-03-05 **Enhancements** @@ -24,7 +24,7 @@ XXXX-XX-XX **Bug fixes** - 1353_: process_iter() is now thread safe (it rarely raised TypeError). -- 1394_: [Windows] Process name() and exe() may erronously return "Registry". +- 1394_: [Windows] Process name() and exe() may erroneously return "Registry". QueryFullProcessImageNameW is now used instead of GetProcessImageFileNameW in order to prevent that. - 1411_: [BSD] lack of Py_DECREF could cause segmentation fault on process @@ -33,6 +33,9 @@ XXXX-XX-XX a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. - 1427_: [OSX] Process cmdline() and environ() may erroneously raise OSError on failed malloc(). +- 1429_: [Windows] SE DEBUG was not properly set for current process. It is + now, and it should result in less AccessDenied exceptions for low-pid + processes. - 1432_: [Windows] Process.memory_info_ex()'s USS memory is miscalculated because we're not using the actual system PAGESIZE. - 1439_: [NetBSD] Process.connections() may return incomplete results if using diff --git a/INSTALL.rst b/INSTALL.rst index 567758c959..68c63917ab 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -20,43 +20,43 @@ Permission issues (UNIX) ======================== The commands below assume you're running as root. -If you're not or you bump into permission errors you can either install psutil +If you aren't or you bump into permission errors you can either install psutil for your user only:: - pip install --user psutil + pip3 install --user psutil -...or prepend ``sudo``, e.g.:: +...or prepend ``sudo`` and install it globally, e.g.:: - sudo pip install psutil + sudo pip3 install psutil Linux ===== Ubuntu / Debian:: - sudo apt-get install gcc python-dev python-pip - pip install psutil + sudo apt-get install gcc python3-dev + pip3 install psutil RedHat / CentOS:: - sudo yum install gcc python-devel python-pip - pip install psutil + sudo yum install gcc python3-devel + pip3 install psutil -If you're on Python 3 use ``python3-dev`` and ``python3-pip`` instead. +If you're on Python 2 use ``python-dev`` instead. macOS ===== Install `Xcode `__ then run:: - pip install psutil + pip3 install psutil Windows ======= Open a cmd.exe shell and run:: - python -m pip install psutil + python3 -m pip install psutil This assumes "python" is in your PATH. If not, specify the full python.exe path. @@ -76,16 +76,16 @@ Once installed run `vcvars64.bat` (see `here `__). Once VS is setup open a cmd.exe shell, cd into psutil directory and run:: - python setup.py build - python setup.py install + python3 setup.py build + python3 setup.py install FreeBSD ======= :: - pkg install python gcc - python -m pip install psutil + pkg install python3 gcc + python -m pip3 install psutil OpenBSD @@ -94,35 +94,32 @@ OpenBSD :: export PKG_PATH="http://ftp.openbsd.org/pub/OpenBSD/`uname -r`/packages/`arch -s`/" - pkg_add -v python gcc - python -m pip install psutil + pkg_add -v python3 gcc + python3 -m pip install psutil NetBSD ====== :: + export PKG_PATH="ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" pkg_add -v pkgin - pkgin install python gcc - python -m pip install psutil + pkgin install python3 gcc + python3 -m pip install psutil Solaris ======= -If ``cc`` compiler is not installed create a symlink to ``gcc``: - -:: +If ``cc`` compiler is not installed create a symlink to ``gcc``:: sudo ln -s /usr/bin/gcc /usr/local/bin/cc -Install: - -:: +Install:: pkg install gcc - python -m pip install psutil + python3 -m pip install psutil Install from sources @@ -132,11 +129,10 @@ Install from sources git clone https://github.com/giampaolo/psutil.git cd psutil - python setup.py install + python3 setup.py install Dev Guide ========= -If you wan to hacking or contribute on psutil continue reading the -`dev guide `__ +See: `dev guide `__. diff --git a/MANIFEST.in b/MANIFEST.in index 321684c10c..3e7b5e7d52 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include .coveragerc -include .git-pre-commit include .gitignore include CREDITS include HISTORY.rst @@ -20,7 +19,6 @@ include docs/conf.py include docs/index.rst include docs/make.bat include make.bat -include psutil/DEVNOTES include psutil/__init__.py include psutil/_common.py include psutil/_compat.py @@ -102,6 +100,7 @@ include scripts/disk_usage.py include scripts/fans.py include scripts/free.py include scripts/ifconfig.py +include scripts/internal/.git-pre-commit include scripts/internal/README include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py diff --git a/docs/index.rst b/docs/index.rst index 23a7254343..6be1dc7bb1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2629,11 +2629,15 @@ take a look at the Timeline ======== + +- 2019-03-05: + `5.6.0 `__ - + `what's new `__ - + `diff ` - 2019-02-15: `5.5.1 `__ - `what's new `__ - `diff ` - - 2019-01-23: `5.5.0 `__ - `what's new `__ - diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index 3d84b500d7..41e303f8c1 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -83,25 +83,25 @@ def download_file(url): def get_file_urls(options): - session = requests.Session() - data = session.get( - BASE_URL + '/projects/' + options.user + '/' + options.project, - timeout=TIMEOUT) - data = data.json() - - urls = [] - for job in (job['jobId'] for job in data['build']['jobs']): - job_url = BASE_URL + '/buildjobs/' + job + '/artifacts' - data = session.get(job_url, timeout=TIMEOUT) + with requests.Session() as session: + data = session.get( + BASE_URL + '/projects/' + options.user + '/' + options.project, + timeout=TIMEOUT) data = data.json() - for item in data: - file_url = job_url + '/' + item['fileName'] - urls.append(file_url) - if not urls: - exit("no artifacts found") - else: - for url in sorted(urls, key=lambda x: os.path.basename(x)): - yield url + + urls = [] + for job in (job['jobId'] for job in data['build']['jobs']): + job_url = BASE_URL + '/buildjobs/' + job + '/artifacts' + data = session.get(job_url, timeout=TIMEOUT) + data = data.json() + for item in data: + file_url = job_url + '/' + item['fileName'] + urls.append(file_url) + if not urls: + exit("no artifacts found") + else: + for url in sorted(urls, key=lambda x: os.path.basename(x)): + yield url def rename_27_wheels(): From 74bdb6487cece4b2acb8ddf716ecd3a645d287d8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Mar 2019 12:38:06 +0100 Subject: [PATCH 0221/1714] fix ResourceWarning --- psutil/tests/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 545f5f2519..37df580e51 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -364,8 +364,8 @@ def create_proc_children_pair(): s += "f.write(str(os.getpid()));" s += "f.close();" s += "time.sleep(60);" - subprocess.Popen(['%s', '-c', 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 From 888f138a45f42f1b9e12cfca2109307a32314d2c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Mar 2019 13:29:59 +0100 Subject: [PATCH 0222/1714] bump up version, fix some doc issues --- HISTORY.rst | 2 ++ docs/DEVGUIDE.rst | 2 +- docs/index.rst | 4 ++-- psutil/__init__.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 18bd4833b9..4e3c79cca3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ call) significantly speeding up different functions and methods. - 1426_: [Windows] PAGESIZE and number of processors is now calculated on startup. +- 1428_: in case of error, the traceback message now shows the underlying C + function called which failed. - 1433_: new Process.parents() method. (idea by Ghislain Le Meur) - 1437_: pids() are returned in sorted order. - 1442_: python3 is now the default interpreter used by Makefile. diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index df031bdeab..f587ebcc0a 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -46,7 +46,7 @@ Some useful make commands: .. code-block:: bash make install # install - make setup-dev-env # install useful dev libs (fkale8, unittest2, etc.) + make setup-dev-env # install useful dev libs (flake8, unittest2, etc.) make test # run unit tests make test-memleaks # run memory leak tests make test-coverage # run test coverage diff --git a/docs/index.rst b/docs/index.rst index 6be1dc7bb1..c25384c2cc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2633,11 +2633,11 @@ Timeline - 2019-03-05: `5.6.0 `__ - `what's new `__ - - `diff ` + `diff `__ - 2019-02-15: `5.5.1 `__ - `what's new `__ - - `diff ` + `diff `__ - 2019-01-23: `5.5.0 `__ - `what's new `__ - diff --git a/psutil/__init__.py b/psutil/__init__.py index 449a27ca79..bd0d5e4dd1 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -217,7 +217,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.0" +__version__ = "5.6.1" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From 7a3037e7c0602f459cb7b4f0c0f887e7bb3a3d39 Mon Sep 17 00:00:00 2001 From: wiggin15 Date: Tue, 5 Mar 2019 14:33:53 +0200 Subject: [PATCH 0223/1714] Fix #1329: [AIX] disable some functions based on availability in libperfstat (#1349) --- psutil/_psaix.py | 29 +++++++++++++++++------------ psutil/_psutil_aix.c | 16 ++++++++++++++++ psutil/tests/__init__.py | 7 ++++--- psutil/tests/test_contracts.py | 2 ++ psutil/tests/test_memory_leaks.py | 2 ++ psutil/tests/test_misc.py | 7 ++++--- psutil/tests/test_posix.py | 2 ++ psutil/tests/test_system.py | 3 +++ 8 files changed, 50 insertions(+), 18 deletions(-) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 4fc87827b1..ff086b9070 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -39,6 +39,8 @@ 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') AF_LINK = cext_posix.AF_LINK @@ -211,7 +213,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): @@ -563,14 +567,15 @@ def num_ctx_switches(self): 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/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 898da6b26d..adb65b170b 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -13,7 +13,9 @@ * * Known limitations: * - psutil.Process.io_counters read count is always 0 + * - psutil.Process.io_counters may not be available on older AIX versions * - psutil.Process.threads may not be available on older AIX versions + # - psutil.net_io_counters may not be available on older AIX versions * - reading basic process info may fail or return incorrect values when * process is starting (see IBM APAR IV58499 - fixed in newer AIX versions) * - sockets and pipes may not be counted in num_fds (fixed in newer AIX @@ -172,6 +174,7 @@ psutil_proc_name_and_args(PyObject *self, PyObject *args) { #ifdef CURR_VERSION_THREAD + /* * Retrieves all threads used by process returning a list of tuples * including thread id, user time and system time. @@ -237,9 +240,12 @@ psutil_proc_threads(PyObject *self, PyObject *args) { free(threadt); return NULL; } + #endif +#ifdef CURR_VERSION_PROCESS + static PyObject * psutil_proc_io_counters(PyObject *self, PyObject *args) { long pid; @@ -263,6 +269,8 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { procinfo.outBytes); } +#endif + /* * Return process user and system CPU times as a Python tuple. @@ -469,6 +477,8 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } +#if defined(CURR_VERSION_NETINTERFACE) && CURR_VERSION_NETINTERFACE >= 3 + /* * Return a list of tuples for network I/O statistics. */ @@ -538,6 +548,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { return NULL; } +#endif + static PyObject* psutil_net_if_stats(PyObject* self, PyObject* args) { @@ -878,8 +890,10 @@ PsutilMethods[] = {"proc_threads", psutil_proc_threads, METH_VARARGS, "Return process threads"}, #endif +#ifdef CURR_VERSION_PROCESS {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS, "Get process I/O counters."}, +#endif {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS, "Get process I/O counters."}, @@ -898,8 +912,10 @@ PsutilMethods[] = "Return system virtual memory usage statistics"}, {"swap_mem", psutil_swap_mem, METH_VARARGS, "Return stats about swap memory, in bytes"}, +#if defined(CURR_VERSION_NETINTERFACE) && CURR_VERSION_NETINTERFACE >= 3 {"net_io_counters", psutil_net_io_counters, METH_VARARGS, "Return a Python dict of tuples for network I/O statistics."}, +#endif {"net_connections", psutil_net_connections, METH_VARARGS, "Return system-wide connections"}, {"net_if_stats", psutil_net_if_stats, METH_VARARGS, diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 37df580e51..65c5aa4edb 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -160,17 +160,17 @@ # --- support +HAS_CONNECTIONS_UNIX = POSIX and not SUNOS HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") -HAS_CONNECTIONS_UNIX = POSIX and not SUNOS HAS_ENVIRON = hasattr(psutil.Process, "environ") -HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") HAS_IONICE = hasattr(psutil.Process, "ionice") HAS_MEMORY_FULL_INFO = 'uss' in psutil.Process().memory_full_info()._fields 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_THREADS = hasattr(psutil.Process, "threads") HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") try: HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) @@ -178,6 +178,7 @@ HAS_BATTERY = True HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") +HAS_THREADS = hasattr(psutil.Process, "threads") # --- misc diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 1f02156f34..87e200bea6 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -33,6 +33,7 @@ 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 @@ -251,6 +252,7 @@ def test_net_if_stats(self): 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(): diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 07d7f06807..cbe37d3e69 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -38,6 +38,7 @@ 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 @@ -534,6 +535,7 @@ def test_pids(self): @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) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 93132b556d..721751a87f 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -44,6 +44,7 @@ from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import HAS_MEMORY_FULL_INFO 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 @@ -620,10 +621,10 @@ def test_cache_clear(self): wrap_numbers.cache_clear('disk_io') wrap_numbers.cache_clear('?!?') - @unittest.skipIf( - not psutil.disk_io_counters() or not psutil.net_io_counters(), - "no disks or NICs available") + @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() diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 15d2f12783..79931283d4 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -25,6 +25,7 @@ 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 @@ -339,6 +340,7 @@ def test_pids(self): @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(): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 1236af3b64..51223fb008 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -38,6 +38,7 @@ 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_NET_IO_COUNTERS from psutil.tests import HAS_SENSORS_BATTERY from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES @@ -538,6 +539,7 @@ def find_mount_point(path): 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) @@ -566,6 +568,7 @@ def check_ntuple(nt): 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 From 3621689c94fb85d3431f5437ff5a8da13cdbeb56 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Mar 2019 13:36:12 +0100 Subject: [PATCH 0224/1714] update HISTORY --- CREDITS | 3 ++- HISTORY.rst | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 77d2c03a59..d94247254e 100644 --- a/CREDITS +++ b/CREDITS @@ -56,7 +56,8 @@ W: http://www.jayloden.com N: Arnon Yaari (wiggin15) W: https://github.com/wiggin15 D: AIX implementation, expert on multiple fronts -I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408 +I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408, + 1329. N: Alex Manuskin W: https://github.com/amanusk diff --git a/HISTORY.rst b/HISTORY.rst index 4e3c79cca3..d2229138a5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,13 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* + +5.6.1 +===== + +**Bug fixes** + +- 1329_: [AIX] psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) + 5.6.0 ===== From f832aa7f008dfeabca61599c539b7b28dd5d4aab Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Mar 2019 22:11:01 +0100 Subject: [PATCH 0225/1714] #1448: fix Wine support due to missing rtlIpv6AddressToStringA --- HISTORY.rst | 2 ++ psutil/_psutil_windows.c | 6 ++++-- psutil/arch/windows/global.c | 9 ++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d2229138a5..2c9b621578 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ **Bug fixes** - 1329_: [AIX] psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) +- 1448_: [Windows] crash on import due to rtlIpv6AddressToStringA not available + on Wine. 5.6.0 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 5c05ac388e..2a498bb18f 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1565,7 +1565,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { // TCP IPv6 if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1)) + (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1) && + (psutil_rtlIpv6AddressToStringA != NULL)) { table = NULL; py_conn_tuple = NULL; @@ -1715,7 +1716,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { // UDP IPv6 if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1)) + (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1) && + (psutil_rtlIpv6AddressToStringA != NULL)) { table = NULL; py_conn_tuple = NULL; diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index a622b63563..a6a59abbd8 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -95,11 +95,6 @@ psutil_loadlibs() { if (! psutil_rtlIpv4AddressToStringA) return 1; - psutil_rtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv6AddressToStringA"); - if (! psutil_rtlIpv6AddressToStringA) - return 1; - // minimum requirement: Win XP SP3 psutil_GetExtendedTcpTable = psutil_GetProcAddressFromLib( "iphlpapi.dll", "GetExtendedTcpTable"); @@ -130,6 +125,10 @@ psutil_loadlibs() { /* * Optional. */ + // not available on Wine + psutil_rtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv6AddressToStringA"); + // minimum requirement: Win Vista psutil_GetTickCount64 = psutil_GetProcAddress( "kernel32", "GetTickCount64"); From 52501180c85fd3cde4101f9b1b40015e81649d34 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Mar 2019 17:18:22 +0100 Subject: [PATCH 0226/1714] mention how to run tests in INSTALL guide --- INSTALL.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 68c63917ab..f11cbaf4ab 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -87,7 +87,6 @@ FreeBSD pkg install python3 gcc python -m pip3 install psutil - OpenBSD ======= @@ -97,7 +96,6 @@ OpenBSD pkg_add -v python3 gcc python3 -m pip install psutil - NetBSD ====== @@ -108,7 +106,6 @@ NetBSD pkgin install python3 gcc python3 -m pip install psutil - Solaris ======= @@ -121,7 +118,6 @@ Install:: pkg install gcc python3 -m pip install psutil - Install from sources ==================== @@ -131,6 +127,12 @@ Install from sources cd psutil python3 setup.py install +Testing installation +==================== + +:: + + python3 -m psutil.tests Dev Guide ========= From ffe8a9d280c397e8fd46eb1422c2838179cfb5d9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Mar 2019 17:50:16 +0100 Subject: [PATCH 0227/1714] test: avoid failing at import time --- psutil/tests/__init__.py | 1 - psutil/tests/test_misc.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 65c5aa4edb..74b0c969a2 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -165,7 +165,6 @@ HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") HAS_ENVIRON = hasattr(psutil.Process, "environ") HAS_IONICE = hasattr(psutil.Process, "ionice") -HAS_MEMORY_FULL_INFO = 'uss' in psutil.Process().memory_full_info()._fields 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") diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 721751a87f..04b45948e1 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -42,7 +42,6 @@ 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_FULL_INFO from psutil.tests import HAS_MEMORY_MAPS from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import HAS_SENSORS_BATTERY @@ -736,8 +735,9 @@ def test_ifconfig(self): def test_pmap(self): self.assert_stdout('pmap.py', str(os.getpid())) - @unittest.skipIf(not HAS_MEMORY_FULL_INFO, "not supported") 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): From 00c28191c32ff96dc4185e02f80ac65ff12856fc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Mar 2019 03:01:02 +0100 Subject: [PATCH 0228/1714] test refactoring --- psutil/tests/test_contracts.py | 104 ++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 87e200bea6..90fa8a1558 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -293,8 +293,7 @@ class TestFetchAllProcesses(unittest.TestCase): some sanity checks against Process API's returned values. """ - def test_fetch_all(self): - valid_procs = 0 + def get_attr_names(self): excluded_names = set([ 'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'as_dict', 'parent', 'parents', 'children', 'memory_info_ex', @@ -309,55 +308,66 @@ def test_fetch_all(self): if name in excluded_names: continue attrs.append(name) + return attrs - default = object() - failures = [] + def iter_procs(self): + attrs = self.get_attr_names() for p in psutil.process_iter(): with p.oneshot(): for name in attrs: - ret = default - try: - 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} - ret = attr(*args, **kwargs) - else: - ret = attr - valid_procs += 1 - 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: - if ret not in (0, 0.0, [], None, '', {}): - assert ret, ret - meth = getattr(self, name) - meth(ret, p) + 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)) From a197c612d9917ef8ee81dc26d9f526d257e43eee Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 9 Mar 2019 18:37:45 -0800 Subject: [PATCH 0229/1714] run win specific tests twice as fast --- psutil/tests/test_windows.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index c98d892cfa..222d8636f6 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -332,12 +332,19 @@ def test_send_signal(self): p = psutil.Process(self.pid) self.assertRaises(ValueError, p.send_signal, signal.SIGINT) - def test_exe(self): + 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: - self.assertEqual(os.path.basename(p.exe()), p.name()) - except psutil.Error: + 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()) @@ -385,15 +392,6 @@ def call(p, attr): if failures: self.fail('\n' + '\n'.join(failures)) - def test_name_always_available(self): - # On Windows name() is never supposed to raise AccessDenied, - # see https://github.com/giampaolo/psutil/issues/627 - for p in psutil.process_iter(): - try: - p.name() - except psutil.NoSuchProcess: - pass - @unittest.skipIf(not sys.version_info >= (2, 7), "CTRL_* signals not supported") def test_ctrl_signals(self): @@ -407,16 +405,6 @@ def test_ctrl_signals(self): self.assertRaises(psutil.NoSuchProcess, p.send_signal, signal.CTRL_BREAK_EVENT) - def test_compare_name_exe(self): - for p in psutil.process_iter(): - try: - a = os.path.basename(p.exe()) - b = p.name() - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - else: - self.assertEqual(a, b) - def test_username(self): self.assertEqual(psutil.Process().username(), win32api.GetUserNameEx(win32con.NameSamCompatible)) From 1b494710631ef6a444f36434f9142b6c443034a8 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Mon, 11 Mar 2019 12:15:15 +0100 Subject: [PATCH 0230/1714] fix version highlighting in README (#1454) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 18a70a7168..f834eb1315 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and 3.4+**. `PyPy `__ is also known to work. +...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy `__ is also known to work. Author From 1ddd673d6231eb79c979034a793c0f5d77503fda Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Mon, 11 Mar 2019 12:15:48 +0100 Subject: [PATCH 0231/1714] fix version highlighting in docs/index (#1455) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index c25384c2cc..2340861375 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,7 +37,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and 3.4+**. `PyPy `__ is also known to work. +...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy `__ is also known to work. The psutil documentation you're reading is distributed as a single HTML page. From a1ff005a5dc6712e284ea35fb3539187e67983cd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 11 Mar 2019 12:16:16 +0100 Subject: [PATCH 0232/1714] [Windows] calculate USS memory by using NtQueryVirtualMemory (#1453) --- psutil/_psutil_windows.c | 166 ++++++++++++++++++++------------- psutil/_pswindows.py | 7 ++ psutil/arch/windows/global.c | 5 + psutil/arch/windows/global.h | 3 + psutil/arch/windows/ntextapi.h | 35 +++++++ scripts/procsmem.py | 2 +- 6 files changed, 150 insertions(+), 68 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 2a498bb18f..092b5af2f0 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -771,91 +771,110 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { } +static int +psutil_GetProcWsetInformation( + DWORD pid, + HANDLE hProcess, + PMEMORY_WORKING_SET_INFORMATION *wSetInfo) +{ + NTSTATUS status; + PVOID buffer; + SIZE_T bufferSize; + + bufferSize = 0x8000; + buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize); + + while ((status = psutil_NtQueryVirtualMemory( + hProcess, + NULL, + MemoryWorkingSetInformation, + buffer, + bufferSize, + NULL)) == STATUS_INFO_LENGTH_MISMATCH) + { + HeapFree(GetProcessHeap(), 0, buffer); + bufferSize *= 2; + psutil_debug("NtQueryVirtualMemory increase bufsize %zd", bufferSize); + // Fail if we're resizing the buffer to something very large. + if (bufferSize > 256 * 1024 * 1024) { + PyErr_SetString(PyExc_RuntimeError, + "NtQueryVirtualMemory bufsize is too large"); + return 1; + } + buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize); + } + + if (!NT_SUCCESS(status)) { + if (status == STATUS_ACCESS_DENIED) { + AccessDenied(""); + } + else if (psutil_pid_is_running(pid) == 0) { + NoSuchProcess(""); + } + else { + PyErr_Clear(); + psutil_debug("NtQueryVirtualMemory failed with %i", status); + PyErr_SetString(PyExc_RuntimeError, "NtQueryVirtualMemory failed"); + } + HeapFree(GetProcessHeap(), 0, buffer); + return 1; + } -/** + *wSetInfo = (PMEMORY_WORKING_SET_INFORMATION)buffer; + return 0; +} + + +/* * Returns the USS of the process. * Reference: * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ * nsMemoryReporterManager.cpp */ static PyObject * -psutil_proc_memory_uss(PyObject *self, PyObject *args) -{ +psutil_proc_memory_uss(PyObject *self, PyObject *args) { DWORD pid; - HANDLE proc; - PSAPI_WORKING_SET_INFORMATION tmp; - DWORD tmp_size = sizeof(tmp); - size_t entries; - size_t private_pages; - size_t i; - DWORD info_array_size; - // needed by QueryWorkingSet - DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; - PSAPI_WORKING_SET_INFORMATION* info_array; - PyObject* py_result = NULL; - unsigned long long total = 0; + HANDLE hProcess; + PSUTIL_PROCESS_WS_COUNTERS wsCounters; + PMEMORY_WORKING_SET_INFORMATION wsInfo; + ULONG_PTR i; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - - proc = psutil_handle_from_pid(pid, access); - if (proc == NULL) + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) return NULL; - // Determine how many entries we need. - memset(&tmp, 0, tmp_size); - if (!QueryWorkingSet(proc, &tmp, tmp_size)) { - // NB: QueryWorkingSet is expected to fail here due to the - // buffer being too small. - if (tmp.NumberOfEntries == 0) { - PyErr_SetFromWindowsErr(0); - goto done; - } - } - - // Fudge the size in case new entries are added between calls. - entries = tmp.NumberOfEntries * 2; - - if (!entries) { - goto done; - } - - info_array_size = tmp_size + \ - ((DWORD)entries * sizeof(PSAPI_WORKING_SET_BLOCK)); - info_array = (PSAPI_WORKING_SET_INFORMATION*)malloc(info_array_size); - if (!info_array) { - PyErr_NoMemory(); - goto done; - } - - if (!QueryWorkingSet(proc, info_array, info_array_size)) { - PyErr_SetFromWindowsErr(0); - goto done; + if (psutil_GetProcWsetInformation(pid, hProcess, &wsInfo) != 0) { + CloseHandle(hProcess); + return NULL; } - - entries = (size_t)info_array->NumberOfEntries; - private_pages = 0; - for (i = 0; i < entries; i++) { - // Count shared pages that only one process is using as private. - if (!info_array->WorkingSetInfo[i].Shared || - info_array->WorkingSetInfo[i].ShareCount <= 1) { - private_pages++; + memset(&wsCounters, 0, sizeof(PSUTIL_PROCESS_WS_COUNTERS)); + + for (i = 0; i < wsInfo->NumberOfEntries; i++) { + // This is what ProcessHacker does. + /* + wsCounters.NumberOfPages++; + if (wsInfo->WorkingSetInfo[i].ShareCount > 1) + wsCounters.NumberOfSharedPages++; + if (wsInfo->WorkingSetInfo[i].ShareCount == 0) + wsCounters.NumberOfPrivatePages++; + if (wsInfo->WorkingSetInfo[i].Shared) + wsCounters.NumberOfShareablePages++; + */ + + // This is what we do: count shared pages that only one process + // is using as private (USS). + if (!wsInfo->WorkingSetInfo[i].Shared || + wsInfo->WorkingSetInfo[i].ShareCount <= 1) { + wsCounters.NumberOfPrivatePages++; } } - total = private_pages * PSUTIL_SYSTEM_INFO.dwPageSize; - py_result = Py_BuildValue("K", total); - -done: - if (proc) { - CloseHandle(proc); - } - - if (info_array) { - free(info_array); - } + HeapFree(GetProcessHeap(), 0, wsInfo); + CloseHandle(hProcess); - return py_result; + return Py_BuildValue("I", wsCounters.NumberOfPrivatePages); } @@ -3359,6 +3378,17 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { } +/* + * System memory page size as an int. + */ +static PyObject * +psutil_getpagesize(PyObject *self, PyObject *args) { + // XXX: we may want to use GetNativeSystemInfo to differentiate + // page size for WoW64 processes (but am not sure). + return Py_BuildValue("I", PSUTIL_SYSTEM_INFO.dwPageSize); +} + + // ------------------------ Python init --------------------------- static PyMethodDef @@ -3464,6 +3494,8 @@ PsutilMethods[] = { "Return CPU frequency."}, {"sensors_battery", psutil_sensors_battery, METH_VARARGS, "Return battery metrics usage."}, + {"getpagesize", psutil_getpagesize, METH_VARARGS, + "Return system memory page size."}, // --- windows services {"winservice_enumerate", psutil_winservice_enumerate, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 9fa14af449..6687770cbb 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -36,6 +36,7 @@ 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 parse_environ_block from ._common import sockfam_to_enum @@ -229,6 +230,11 @@ def py2_strencode(s): return s.encode(ENCODING, ENCODING_ERRS) +@memoize +def getpagesize(): + return cext.getpagesize() + + # ===================================================================== # --- memory # ===================================================================== @@ -798,6 +804,7 @@ def memory_info(self): def memory_full_info(self): basic_mem = self.memory_info() uss = cext.proc_memory_uss(self.pid) + uss *= getpagesize() return pfullmem(*basic_mem + (uss, )) def memory_maps(self): diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index a6a59abbd8..9ef9209276 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -122,6 +122,11 @@ psutil_loadlibs() { if (! psutil_NtResumeProcess) return 1; + psutil_NtQueryVirtualMemory = psutil_GetProcAddressFromLib( + "ntdll", "NtQueryVirtualMemory"); + if (! psutil_NtQueryVirtualMemory) + return 1; + /* * Optional. */ diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 1b1d00f818..fb24bac9a4 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -68,3 +68,6 @@ _NtSuspendProcess \ _NtResumeProcess \ psutil_NtResumeProcess; + +_NtQueryVirtualMemory \ + psutil_NtQueryVirtualMemory; diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 3927edf39d..178f9866f4 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -13,6 +13,8 @@ typedef LONG NTSTATUS; #define STATUS_INFO_LENGTH_MISMATCH 0xc0000004 #define STATUS_BUFFER_TOO_SMALL 0xC0000023L #define SystemExtendedHandleInformation 64 +#define MemoryWorkingSetInformation 0x1 +#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L) /* * ================================================================ @@ -378,6 +380,30 @@ typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX { } SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX; #endif +typedef struct _MEMORY_WORKING_SET_BLOCK { + ULONG_PTR Protection : 5; + ULONG_PTR ShareCount : 3; + ULONG_PTR Shared : 1; + ULONG_PTR Node : 3; +#ifdef _WIN64 + ULONG_PTR VirtualPage : 52; +#else + ULONG VirtualPage : 20; +#endif +} MEMORY_WORKING_SET_BLOCK, *PMEMORY_WORKING_SET_BLOCK; + +typedef struct _MEMORY_WORKING_SET_INFORMATION { + ULONG_PTR NumberOfEntries; + MEMORY_WORKING_SET_BLOCK WorkingSetInfo[1]; +} MEMORY_WORKING_SET_INFORMATION, *PMEMORY_WORKING_SET_INFORMATION; + +typedef struct _PSUTIL_PROCESS_WS_COUNTERS { + SIZE_T NumberOfPages; + SIZE_T NumberOfPrivatePages; + SIZE_T NumberOfSharedPages; + SIZE_T NumberOfShareablePages; +} PSUTIL_PROCESS_WS_COUNTERS, *PPSUTIL_PROCESS_WS_COUNTERS; + /* * ================================================================ * Type defs for modules loaded at runtime. @@ -465,4 +491,13 @@ typedef NTSTATUS (WINAPI *_NtSuspendProcess) ( HANDLE hProcess ); +typedef NTSTATUS (NTAPI *_NtQueryVirtualMemory) ( + HANDLE ProcessHandle, + PVOID BaseAddress, + int MemoryInformationClass, + PVOID MemoryInformation, + SIZE_T MemoryInformationLength, + PSIZE_T ReturnLength +); + #endif // __NTEXTAPI_H__ diff --git a/scripts/procsmem.py b/scripts/procsmem.py index ab9ad0666c..f660c085fc 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -86,7 +86,7 @@ def main(): for p in procs[:86]: line = templ % ( p.pid, - p._info["username"][:7], + p._info["username"][:7] if p._info["username"] else "", " ".join(p._info["cmdline"])[:30], convert_bytes(p._uss), convert_bytes(p._pss) if p._pss != "" else "", From da2bdd6d7356b40baca5f2869a5d20e352e5356a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 11 Mar 2019 18:24:26 +0100 Subject: [PATCH 0233/1714] pre-release --- HISTORY.rst | 4 ++++ docs/index.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 2c9b621578..2d1fbee508 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,11 +4,15 @@ 5.6.1 ===== +2019-03-11 + **Bug fixes** - 1329_: [AIX] psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) - 1448_: [Windows] crash on import due to rtlIpv6AddressToStringA not available on Wine. +- 1451_: [Windows] Process.memory_full_info() segfaults. NtQueryVirtualMemory + is now used instead of QueryWorkingSet to calculate USS memory. 5.6.0 ===== diff --git a/docs/index.rst b/docs/index.rst index 2340861375..f35b23c3fc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2630,6 +2630,10 @@ take a look at the Timeline ======== +- 2019-03-11: + `5.6.1 `__ - + `what's new `__ - + `diff `__ - 2019-03-05: `5.6.0 `__ - `what's new `__ - From c4467c90ef53cd9eba8ab8be620d71b8477d6ce9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Mar 2019 07:30:52 +0100 Subject: [PATCH 0234/1714] Coloured tests (#1459) Fixes #1458. --- MANIFEST.in | 1 + Makefile | 4 +- psutil/tests/__init__.py | 50 +----------- psutil/tests/__main__.py | 2 +- psutil/tests/runner.py | 122 ++++++++++++++++++++++++++++++ psutil/tests/test_aix.py | 2 +- psutil/tests/test_bsd.py | 2 +- psutil/tests/test_connections.py | 2 +- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 2 +- psutil/tests/test_memory_leaks.py | 2 +- psutil/tests/test_misc.py | 2 +- psutil/tests/test_osx.py | 2 +- psutil/tests/test_posix.py | 2 +- psutil/tests/test_process.py | 2 +- psutil/tests/test_sunos.py | 2 +- psutil/tests/test_system.py | 2 +- psutil/tests/test_unicode.py | 2 +- psutil/tests/test_windows.py | 2 +- 19 files changed, 144 insertions(+), 63 deletions(-) create mode 100755 psutil/tests/runner.py diff --git a/MANIFEST.in b/MANIFEST.in index 3e7b5e7d52..6ab7c6483f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -80,6 +80,7 @@ include psutil/arch/windows/services.h include psutil/tests/README.rst include psutil/tests/__init__.py include psutil/tests/__main__.py +include psutil/tests/runner.py include psutil/tests/test_aix.py include psutil/tests/test_bsd.py include psutil/tests/test_connections.py diff --git a/Makefile b/Makefile index 59ac150683..7d838e876c 100644 --- a/Makefile +++ b/Makefile @@ -113,11 +113,11 @@ test: ## Run all tests. test-process: ## Run process-related API tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) -m unittest -v psutil.tests.test_process + $(TEST_PREFIX) $(PYTHON) psutil/tests/test_process.py test-system: ## Run system-related API tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) -m unittest -v psutil.tests.test_system + $(TEST_PREFIX) $(PYTHON) psutil/tests/test_system.py test-misc: ## Run miscellaneous tests. ${MAKE} install diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 74b0c969a2..0a29c59d9e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -69,7 +69,6 @@ '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', - 'VERBOSITY', "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", @@ -127,8 +126,6 @@ MEMORY_TOLERANCE = 500 * 1024 # 500KB # the timeout used in functions which have to wait GLOBAL_TIMEOUT = 3 if TRAVIS or APPVEYOR else 0.5 -# test output verbosity -VERBOSITY = 1 if os.getenv('SILENT') or TOX else 2 # be more tolerant if we're on travis / appveyor in order to avoid # false positives if TRAVIS or APPVEYOR: @@ -802,9 +799,11 @@ 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" % ( - self.__class__.__module__, self.__class__.__name__, - self._testMethodName) + fqmod, self.__class__.__name__, self._testMethodName) # assertRaisesRegexp renamed to assertRaisesRegex in 3.3; # add support for the new name. @@ -816,47 +815,6 @@ def __str__(self): unittest.TestCase = TestCase -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(): - 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"))] - suite = unittest.TestSuite() - 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)) - return suite - - -def run_suite(): - _setup_tests() - result = unittest.TextTestRunner(verbosity=VERBOSITY).run(get_suite()) - success = result.wasSuccessful() - sys.exit(0 if success else 1) - - -def run_test_module_by_name(name): - # testmodules = [os.path.splitext(x)[0] for x in os.listdir(HERE) - # if x.endswith('.py') and x.startswith('test_')] - _setup_tests() - name = os.path.splitext(os.path.basename(name))[0] - suite = unittest.TestSuite() - suite.addTest(unittest.defaultTestLoader.loadTestsFromName(name)) - result = unittest.TextTestRunner(verbosity=VERBOSITY).run(suite) - success = result.wasSuccessful() - sys.exit(0 if success else 1) - - def retry_before_failing(retries=NO_RETRIES): """Decorator which runs a test function and retries N times before actually failing. diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 36554a126b..3180c279c4 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -21,7 +21,7 @@ from urllib2 import urlopen from psutil.tests import PYTHON_EXE -from psutil.tests import run_suite +from psutil.tests.runner import run_suite HERE = os.path.abspath(os.path.dirname(__file__)) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py new file mode 100755 index 0000000000..33f9600027 --- /dev/null +++ b/psutil/tests/runner.py @@ -0,0 +1,122 @@ +#!/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. +""" + +import os +import sys +import unittest +from unittest import TestResult +from unittest import TextTestResult +from unittest import TextTestRunner + +import psutil +from psutil.tests import TOX + + +HERE = os.path.abspath(os.path.dirname(__file__)) +VERBOSITY = 1 if TOX else 2 +GREEN = 1 +RED = 2 +BROWN = 94 + + +def term_supports_colors(file=sys.stdout): + 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) + + +class ColouredResult(unittest.TextTestResult): + + def addSuccess(self, test): + TestResult.addSuccess(self, test) + self.stream.writeln(hilite("OK", GREEN)) + + def addError(self, test, err): + TestResult.addError(self, test, err) + self.stream.writeln(hilite("ERROR", RED, bold=True)) + + def addFailure(self, test, err): + TestResult.addFailure(self, test, err) + self.stream.writeln(hilite("FAIL", RED)) + + def addSkip(self, test, reason): + TestResult.addSkip(self, test, reason) + self.stream.writeln(hilite("skipped: %s" % reason, BROWN)) + + def printErrorList(self, flavour, errors): + flavour = hilite(flavour, RED) + TextTestResult.printErrorList(self, flavour, errors) + + +class ColouredRunner(TextTestRunner): + resultclass = ColouredResult if term_supports_colors() else TextTestResult + + +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(): + 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"))] + suite = unittest.TestSuite() + 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)) + return suite + + +def run_test_module_by_name(name): + # testmodules = [os.path.splitext(x)[0] for x in os.listdir(HERE) + # if x.endswith('.py') and x.startswith('test_')] + setup_tests() + name = os.path.splitext(os.path.basename(name))[0] + suite = unittest.TestSuite() + suite.addTest(unittest.defaultTestLoader.loadTestsFromName(name)) + result = ColouredRunner(verbosity=VERBOSITY).run(suite) + success = result.wasSuccessful() + sys.exit(0 if success else 1) + + +def run_suite(): + setup_tests() + result = ColouredRunner(verbosity=VERBOSITY).run(get_suite()) + success = result.wasSuccessful() + sys.exit(0 if success else 1) diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 4f0da4e000..5294493b2e 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -11,7 +11,6 @@ import re from psutil import AIX -from psutil.tests import run_test_module_by_name from psutil.tests import sh from psutil.tests import unittest import psutil @@ -118,4 +117,5 @@ def test_net_if_addrs_names(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 34b66ca6b3..d06bfef91e 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -25,7 +25,6 @@ from psutil.tests import MEMORY_TOLERANCE from psutil.tests import reap_children from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name from psutil.tests import sh from psutil.tests import unittest from psutil.tests import which @@ -550,4 +549,5 @@ def test_cpu_stats_ctx_switches(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index c97fc409ec..2a79338eac 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -35,7 +35,6 @@ from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import pyrun from psutil.tests import reap_children -from psutil.tests import run_test_module_by_name from psutil.tests import safe_rmpath from psutil.tests import skip_on_access_denied from psutil.tests import tcp_socketpair @@ -523,4 +522,5 @@ def test_connection_constants(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 90fa8a1558..892ffb9b30 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -38,7 +38,6 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import is_namedtuple -from psutil.tests import run_test_module_by_name from psutil.tests import safe_rmpath from psutil.tests import skip_on_access_denied from psutil.tests import TESTFN @@ -654,4 +653,5 @@ def environ(self, ret, proc): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0f981b6bc5..de96f049c9 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -38,7 +38,6 @@ from psutil.tests import reap_children from psutil.tests import reload_module from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name from psutil.tests import safe_rmpath from psutil.tests import sh from psutil.tests import skip_on_not_implemented @@ -2080,4 +2079,5 @@ def test_cat(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index cbe37d3e69..f90e8377cf 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -46,7 +46,6 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import reap_children -from psutil.tests import run_test_module_by_name from psutil.tests import safe_rmpath from psutil.tests import skip_on_access_denied from psutil.tests import TESTFN @@ -607,4 +606,5 @@ def test_win_service_get_description(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 04b45948e1..36f8bf06fb 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -55,7 +55,6 @@ from psutil.tests import reload_module from psutil.tests import retry from psutil.tests import ROOT_DIR -from psutil.tests import run_test_module_by_name from psutil.tests import safe_mkdir from psutil.tests import safe_rmpath from psutil.tests import SCRIPTS_DIR @@ -1054,4 +1053,5 @@ def test_is_namedtuple(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 4025719093..232b5a7168 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -18,7 +18,6 @@ from psutil.tests import MEMORY_TOLERANCE from psutil.tests import reap_children from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name from psutil.tests import sh from psutil.tests import unittest @@ -291,4 +290,5 @@ def test_sensors_battery(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 79931283d4..88506b9af6 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -30,7 +30,6 @@ from psutil.tests import PYTHON_EXE from psutil.tests import reap_children from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name from psutil.tests import sh from psutil.tests import skip_on_access_denied from psutil.tests import TRAVIS @@ -437,4 +436,5 @@ def df(device): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 2af676ba9f..a4d49ed4c8 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -57,7 +57,6 @@ from psutil.tests import PYTHON_EXE from psutil.tests import reap_children from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name from psutil.tests import safe_rmpath from psutil.tests import sh from psutil.tests import skip_on_access_denied @@ -1608,4 +1607,5 @@ def test_kill_terminate(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index ea9afcde0e..75b99feec9 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -10,7 +10,6 @@ import psutil from psutil import SUNOS -from psutil.tests import run_test_module_by_name from psutil.tests import sh from psutil.tests import unittest @@ -42,4 +41,5 @@ def test_cpu_count(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 51223fb008..43de8b99f1 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -45,7 +45,6 @@ from psutil.tests import mock from psutil.tests import reap_children from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name from psutil.tests import safe_rmpath from psutil.tests import TESTFN from psutil.tests import TESTFN_UNICODE @@ -868,4 +867,5 @@ def test_sensors_fans(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index cfa8dd92c2..a015c8961a 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -77,7 +77,6 @@ from psutil.tests import mock from psutil.tests import PYPY from psutil.tests import reap_children -from psutil.tests import run_test_module_by_name from psutil.tests import safe_mkdir from psutil.tests import safe_rmpath as _safe_rmpath from psutil.tests import skip_on_access_denied @@ -367,4 +366,5 @@ def test_proc_environ(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 222d8636f6..00d0fece63 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -27,7 +27,6 @@ from psutil.tests import mock from psutil.tests import reap_children from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name from psutil.tests import sh from psutil.tests import unittest @@ -865,4 +864,5 @@ def test_win_service_get(self): if __name__ == '__main__': + from psutil.tests.runner import run_test_module_by_name run_test_module_by_name(__file__) From d8066a0305616d6981f26c00366d07ae3e6588a7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Mar 2019 13:34:13 +0100 Subject: [PATCH 0235/1714] test runner refactoring (avoid code duplication) --- psutil/tests/__init__.py | 3 +-- psutil/tests/__main__.py | 4 +-- psutil/tests/runner.py | 42 +++++++++++++------------------ psutil/tests/test_aix.py | 4 +-- psutil/tests/test_bsd.py | 4 +-- psutil/tests/test_connections.py | 4 +-- psutil/tests/test_contracts.py | 4 +-- psutil/tests/test_linux.py | 4 +-- psutil/tests/test_memory_leaks.py | 4 +-- psutil/tests/test_misc.py | 4 +-- psutil/tests/test_osx.py | 4 +-- psutil/tests/test_posix.py | 4 +-- psutil/tests/test_process.py | 4 +-- psutil/tests/test_sunos.py | 4 +-- psutil/tests/test_system.py | 4 +-- psutil/tests/test_unicode.py | 4 +-- psutil/tests/test_windows.py | 4 +-- 17 files changed, 48 insertions(+), 57 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 0a29c59d9e..13c87c65b9 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -80,8 +80,7 @@ 'ThreadTask' # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_before_failing', 'run_test_module_by_name', 'get_suite', - 'run_suite', + 'retry_before_failing', # install utils 'install_pip', 'install_test_deps', # fs utils diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 3180c279c4..68710d44f5 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -21,7 +21,7 @@ from urllib2 import urlopen from psutil.tests import PYTHON_EXE -from psutil.tests.runner import run_suite +from psutil.tests.runner import run HERE = os.path.abspath(os.path.dirname(__file__)) @@ -88,7 +88,7 @@ def main(): except ImportError: sys.exit("%r lib is not installed; run %s -m psutil.tests " "--install-deps" % (dep, PYTHON_EXE)) - run_suite() + run() main() diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 33f9600027..f555e4b0d5 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -88,35 +88,27 @@ def setup_tests(): psutil._psplatform.cext.set_testing() -def get_suite(): - 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"))] +def get_suite(name=None): suite = unittest.TestSuite() - 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)) + 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 run_test_module_by_name(name): - # testmodules = [os.path.splitext(x)[0] for x in os.listdir(HERE) - # if x.endswith('.py') and x.startswith('test_')] - setup_tests() - name = os.path.splitext(os.path.basename(name))[0] - suite = unittest.TestSuite() - suite.addTest(unittest.defaultTestLoader.loadTestsFromName(name)) - result = ColouredRunner(verbosity=VERBOSITY).run(suite) - success = result.wasSuccessful() - sys.exit(0 if success else 1) - - -def run_suite(): +def run(name=None): setup_tests() - result = ColouredRunner(verbosity=VERBOSITY).run(get_suite()) + result = ColouredRunner(verbosity=VERBOSITY).run(get_suite(name)) success = result.wasSuccessful() sys.exit(0 if success else 1) diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 5294493b2e..1757e3e5d3 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -117,5 +117,5 @@ def test_net_if_addrs_names(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index d06bfef91e..f1b5775a99 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -549,5 +549,5 @@ def test_cpu_stats_ctx_switches(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 2a79338eac..68eea7845d 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -522,5 +522,5 @@ def test_connection_constants(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 892ffb9b30..08e9e9b824 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -653,5 +653,5 @@ def environ(self, ret, proc): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index de96f049c9..eb3e560da6 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -2079,5 +2079,5 @@ def test_cat(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index f90e8377cf..ed3a77fe71 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -606,5 +606,5 @@ def test_win_service_get_description(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 36f8bf06fb..29e1e41e60 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -1053,5 +1053,5 @@ def test_is_namedtuple(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 232b5a7168..0e6ed67c7b 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -290,5 +290,5 @@ def test_sensors_battery(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 88506b9af6..8f7fbf51b4 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -436,5 +436,5 @@ def df(device): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a4d49ed4c8..baa3e3461b 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1607,5 +1607,5 @@ def test_kill_terminate(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index 75b99feec9..94405d41be 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -41,5 +41,5 @@ def test_cpu_count(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 43de8b99f1..00a385861c 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -867,5 +867,5 @@ def test_sensors_fans(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index a015c8961a..f7115e3d4c 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -366,5 +366,5 @@ def test_proc_environ(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 00d0fece63..a9aeba8e6b 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -864,5 +864,5 @@ def test_win_service_get(self): if __name__ == '__main__': - from psutil.tests.runner import run_test_module_by_name - run_test_module_by_name(__file__) + from psutil.tests.runner import run + run(__file__) From 6c71973462f0c1768f99660a112ba14f3714e082 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Mar 2019 13:58:10 +0100 Subject: [PATCH 0236/1714] test runner: show errors on KeyboardInterrupt --- HISTORY.rst | 6 ++++++ psutil/__init__.py | 2 +- psutil/tests/runner.py | 21 ++++++++++++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2d1fbee508..cd71108d1c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,11 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.6.2 +===== + +**Enhancements** + +- 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. 5.6.1 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index bd0d5e4dd1..9c6bf2bd66 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -217,7 +217,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.1" +__version__ = "5.6.2" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index f555e4b0d5..c009e258ae 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -8,6 +8,7 @@ Unit test runner, providing colourized output. """ +from __future__ import print_function import os import sys import unittest @@ -80,6 +81,13 @@ def 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 = self.resultclass( + self.stream, self.descriptions, self.verbosity) + return self.result + def setup_tests(): if 'PSUTIL_TESTING' not in os.environ: @@ -109,6 +117,13 @@ def get_suite(name=None): def run(name=None): setup_tests() - result = ColouredRunner(verbosity=VERBOSITY).run(get_suite(name)) - success = result.wasSuccessful() - sys.exit(0 if success else 1) + runner = ColouredRunner(verbosity=VERBOSITY) + try: + result = runner.run(get_suite(name)) + except (SystemExit, KeyboardInterrupt): + print("received KeyboardInterrupt", file=sys.stderr) + runner.result.printErrors() + sys.exit(1) + else: + success = result.wasSuccessful() + sys.exit(0 if success else 1) From 3762d0f8d6206bd70b16647d0f2c11172ef958db Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Wed, 13 Mar 2019 14:12:24 +0100 Subject: [PATCH 0237/1714] Make tests invariant to LANG setting (#1462) When LANG is set to a non English locale (e.g. de_DE.UTF-8), free and vmstat will output the text translated and cause the test cases to fail. Therefore set LANG=C.UTF-8 when calling free or vmstat to always get the English output. --- psutil/tests/test_linux.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index eb3e560da6..6e260b994c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -96,7 +96,7 @@ def free_swap(): """Parse 'free' cmd and return swap memory's s total, used and free values. """ - out = sh('free -b') + out = sh('free -b', env={"LANG": "C.UTF-8"}) lines = out.split('\n') for line in lines: if line.startswith('Swap'): @@ -115,7 +115,7 @@ def free_physmem(): # 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') + out = sh('free -b', env={"LANG": "C.UTF-8"}) lines = out.split('\n') for line in lines: if line.startswith('Mem'): @@ -129,7 +129,7 @@ def free_physmem(): def vmstat(stat): - out = sh("vmstat -s") + out = sh("vmstat -s", env={"LANG": "C.UTF-8"}) for line in out.split("\n"): line = line.strip() if stat in line: From 69b44d14ce0c64d30a3bda2c82e67d009dc288fa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Mar 2019 16:27:07 +0100 Subject: [PATCH 0238/1714] Big docfix (#1464) * use https wherever possible * always point to python 3 doc * point to new MSDN urls * use RST references and avoid repetitions of URLs --- CREDITS | 5 + HISTORY.rst | 6 + docs/DEVGUIDE.rst | 80 ++++++---- docs/index.rst | 331 +++++++++++++++++++---------------------- psutil/__init__.py | 2 +- psutil/tests/runner.py | 11 +- 6 files changed, 217 insertions(+), 218 deletions(-) diff --git a/CREDITS b/CREDITS index d94247254e..38cd693970 100644 --- a/CREDITS +++ b/CREDITS @@ -587,3 +587,8 @@ N: Ghislain Le Meur W: https://github.com/gigi206 D: idea for Process.parents() I: 1379 + +N: Benjamin Drung +D: make tests invariant to LANG setting +W: https://github.com/bdrung +I: 1462 diff --git a/HISTORY.rst b/HISTORY.rst index cd71108d1c..9e2b547765 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,12 @@ **Enhancements** - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. +- 1464_: various docfixes (always point to python3 doc, fix links, etc.). + +**Bug fixes** + +- 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by + Benjamin Drung) 5.6.1 ===== diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index f587ebcc0a..d9eb6d7ab0 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -21,11 +21,9 @@ If you plan on hacking on psutil this is what you're supposed to do first: make test -- bear in mind that ``make`` - (see `Makefile `_) - is the designated tool to run tests, build, install etc. and that it is also - available on Windows - (see `make.bat `_). +- bear in mind that ``make``(see `Makefile`_) is the designated tool to run + tests, build, install etc. and that it is also available on Windows (see + `make.bat`_ ). - do not use ``sudo``; ``make install`` installs psutil as a limited user in "edit" mode; also ``make setup-dev-env`` installs deps as a limited user. - use `make help` to see the list of available commands. @@ -33,10 +31,9 @@ If you plan on hacking on psutil this is what you're supposed to do first: Coding style ============ -- python code strictly follows `PEP 8 `_ - styling guides and this is enforced by ``make install-git-hooks``. -- C code strictly follows `PEP 7 `_ - styling guides. +- python code strictly follows `PEP-8`_ styling guides and this is enforced by + a commit GIT hook installed via ``make install-git-hooks``. +- C code follows `PEP-7`_ styling guides. Makefile ======== @@ -100,30 +97,30 @@ Usually the files involved when adding a new functionality are: Typical process occurring when adding a new functionality (API): -- define the new function in ``psutil/__init__.py``. +- define the new function in `psutil/__init__.py`_. - write the platform specific implementation in ``psutil/_ps{platform}.py`` - (e.g. ``psutil/_pslinux.py``). + (e.g. `psutil/_pslinux.py`_). - if the change requires C, write the C implementation in - ``psutil/_psutil_{platform}.c`` (e.g. ``psutil/_psutil_linux.c``). -- write a generic test in ``psutil/tests/test_system.py`` or - ``psutil/tests/test_process.py``. + ``psutil/_psutil_{platform}.c`` (e.g. `psutil/_psutil_linux.c`_). +- write a generic test in `psutil/tests/test_system.py`_ or + `psutil/tests/test_process.py`_. - if possible, write a platform specific test in - ``psutil/tests/test_{platform}.py`` (e.g. ``test_linux.py``). + ``psutil/tests/test_{platform}.py`` (e.g. `psutil/tests/test_linux.py`_). This usually means testing the return value of the new feature against a system CLI tool. - update doc in ``doc/index.py``. - update ``HISTORY.rst``. -- update ``README.rst`` (if necessary). - make a pull request. Make a pull request =================== -- fork psutil -- create your feature branch (``git checkout -b my-new-feature``) -- commit your changes (``git commit -am 'add some feature'``) -- push to the branch (``git push origin my-new-feature``) -- create a new pull request +- fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") +- git clone your fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git``) +- create your feature branch:``git checkout -b new-feature`` +- commit your changes: ``git commit -am 'add some feature'`` +- push to the branch: ``git push origin new-feature`` +- create a new pull request by via github web interface Continuous integration ====================== @@ -136,13 +133,10 @@ Unit tests Tests are automatically run for every GIT push on **Linux**, **macOS** and **Windows** by using: -- `Travis `_ (Linux, macOS) -- `Appveyor `_ (Windows) +- `Travis`_ (Linux, macOS) +- `Appveyor`_ (Windows) -Test files controlling these are -`.travis.yml `_ -and -`appveyor.yml `_. +Test files controlling these are `.travis.yml`_ and `appveyor.yml`_. Both services run psutil test suite against all supported python version (2.6 - 3.6). Two icons in the home page (README) always show the build status: @@ -160,9 +154,8 @@ BSD, AIX and Solaris are currently tested manually. Test coverage ------------- -Test coverage is provided by `coveralls.io `_, -it is controlled via `.travis.yml `_ -and it is updated on every git push. +Test coverage is provided by `coveralls.io`_ and it is controlled via +`.travis.yml`_. An icon in the home page (README) always shows the last coverage percentage: .. image:: https://coveralls.io/repos/giampaolo/psutil/badge.svg?branch=master&service=github @@ -172,9 +165,9 @@ An icon in the home page (README) always shows the last coverage percentage: Documentation ============= -- doc source code is written in a single file: `/docs/index.rst `_. -- it uses `RsT syntax `_ - and it's built with `sphinx `_. +- doc source code is written in a single file: `/docs/index.rst`_. +- it uses `RsT syntax`_ + and it's built with `sphinx`_. - doc can be built with ``make setup-dev-env; cd docs; make html``. - public doc is hosted on http://psutil.readthedocs.io/ @@ -186,3 +179,24 @@ These are notes for myself (Giampaolo): - ``make release`` - post announce (``make print-announce``) on psutil and python-announce mailing lists, twitter, g+, blog. + + +.. _`.travis.yml`: https://github.com/giampaolo/psutil/blob/master/.travis.ym +.. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.ym +.. _`Appveyor`: https://ci.appveyor.com/project/giampaolo/psuti +.. _`coveralls.io`: https://coveralls.io/github/giampaolo/psuti +.. _`doc/index.rst`: https://github.com/giampaolo/psutil/blob/master/doc/index.rst +.. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst +.. _`make.bat`: https://github.com/giampaolo/psutil/blob/master/make.bat +.. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile +.. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ +.. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ +.. _`psutil/__init__.py`: https://github.com/giampaolo/psutil/blob/master/psutil/__init__.py +.. _`psutil/_pslinux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_pslinux.py +.. _`psutil/_psutil_linux.c`: https://github.com/giampaolo/psutil/blob/master/psutil/_psutil_linux.c +.. _`psutil/tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_linux.py +.. _`psutil/tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_process.py +.. _`psutil/tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_system.py +.. _`RsT syntax`: http://docutils.sourceforge.net/docs/user/rst/quickref.htm +.. _`sphinx`: http://sphinx-doc.org +.. _`Travis`: https://travis-ci.org/giampaolo/psuti diff --git a/docs/index.rst b/docs/index.rst index f35b23c3fc..6ff1e22c5a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,7 +37,8 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy `__ is also known to work. +Supported Python versions are **2.6**, **2.7** and **3.4+**. +`PyPy `__ is also known to work. The psutil documentation you're reading is distributed as a single HTML page. @@ -158,8 +159,7 @@ CPU .. function:: cpu_count(logical=True) - Return the number of logical CPUs in the system (same as - `os.cpu_count() `__ + Return the number of logical CPUs in the system (same as `os.cpu_count`_ in Python 3.4) or ``None`` if undetermined. If *logical* is ``False`` return the number of physical cores only (hyper thread CPUs are excluded) or ``None`` if undetermined. @@ -275,8 +275,8 @@ Memory The sum of **used** and **available** does not necessarily equal **total**. On Windows **available** and **free** are the same. - See `meminfo.py `__ - script providing an example on how to convert bytes in a human readable form. + See `meminfo.py`_ script providing an example on how to convert bytes in a + human readable form. .. note:: if you just want to know how much physical memory is left in a cross platform fashion simply rely on the **available** field. @@ -311,8 +311,8 @@ Memory (cumulative) **sin** and **sout** on Windows are always set to ``0``. - See `meminfo.py `__ - script providing an example on how to convert bytes in a human readable form. + See `meminfo.py`_ script providing an example on how to convert bytes in a + human readable form. >>> import psutil >>> psutil.swap_memory() @@ -332,21 +332,17 @@ Disks mount point and filesystem type, similarly to "df" command on UNIX. If *all* parameter is ``False`` it tries to distinguish and return physical devices only (e.g. hard disks, cd-rom drives, USB keys) and ignore all others - (e.g. memory partitions such as - `/dev/shm `__). + (e.g. memory partitions such as /dev/shm). Note that this may not be fully reliable on all systems (e.g. on BSD this parameter is ignored). Named tuple's **fstype** field is a string which varies depending on the platform. On Linux it can be one of the values found in /proc/filesystems (e.g. ``'ext3'`` for an ext3 hard drive o ``'iso9660'`` for the CD-ROM drive). - On Windows it is determined via - `GetDriveType `__ - and can be either ``"removable"``, ``"fixed"``, ``"remote"``, ``"cdrom"``, - ``"unmounted"`` or ``"ramdisk"``. On macOS and BSD it is retrieved via - `getfsstat(2) `__. See - `disk_usage.py `__ - script providing an example usage. + On Windows it is determined via `GetDriveType`_ and can be either + ``"removable"``, ``"fixed"``, ``"remote"``, ``"cdrom"``, ``"unmounted"`` or + ``"ramdisk"``. On macOS and BSD it is retrieved via `getfsstat`_ syscall. + See `disk_usage.py`_ script providing an example usage. >>> import psutil >>> psutil.disk_partitions() @@ -358,12 +354,10 @@ Disks Return disk usage statistics about the partition which contains the given *path* as a named tuple including **total**, **used** and **free** space expressed in bytes, plus the **percentage** usage. - `OSError `__ is - raised if *path* does not exist. - Starting from `Python 3.3 `__ this is - also available as - `shutil.disk_usage() `__. - See `disk_usage.py `__ script providing an example usage. + ``OSError`` is raised if *path* does not exist. + Starting from Python 3.3 this is also available as `shutil.disk_usage`_ + (see `BPO-12442`_). + See `disk_usage.py`_ script providing an example usage. >>> import psutil >>> psutil.disk_usage('/') @@ -400,16 +394,13 @@ Disks (in milliseconds) - **busy_time**: (*Linux*, *FreeBSD*) time spent doing actual I/Os (in milliseconds) - - **read_merged_count** (*Linux*): number of merged reads - (see `iostat doc `__) - - **write_merged_count** (*Linux*): number of merged writes - (see `iostats doc `__) + - **read_merged_count** (*Linux*): number of merged reads (see `iostats doc`_) + - **write_merged_count** (*Linux*): number of merged writes (see `iostats doc`_) If *perdisk* is ``True`` return the same information for every physical disk installed on the system as a dictionary with partition names as the keys and the named tuple described above as the values. - See `iotop.py `__ - for an example application. + See `iotop.py`_ for an example application. On some systems such as Linux, on a very busy or long-lived system, the numbers returned by the kernel may overflow and wrap (restart from zero). If *nowrap* is ``True`` psutil will detect and adjust those numbers across @@ -485,9 +476,7 @@ Network {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} - Also see `nettop.py `__ - and `ifconfig.py `__ - for an example application. + Also see `nettop.py`_ and `ifconfig.py`_ for an example application. .. versionchanged:: 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new @@ -499,18 +488,11 @@ Network Every named tuple provides 7 attributes: - **fd**: the socket file descriptor. If the connection refers to the current - process this may be passed to - `socket.fromfd() `__ + process this may be passed to `socket.fromfd`_ to obtain a usable socket object. On Windows and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET - `__, - `AF_INET6 `__ - or `AF_UNIX `__. - - **type**: the address type, either `SOCK_STREAM - `__ or - `SOCK_DGRAM - `__. + - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. + - **type**: the address type, either `SOCK_STREAM`_ or `SOCK_DGRAM`_. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -559,8 +541,7 @@ Network On macOS and AIX this function requires root privileges. To get per-process connections use :meth:`Process.connections`. - Also, see - `netstat.py sample script `__. + Also, see `netstat.py`_ example script. Example: >>> import psutil @@ -600,9 +581,7 @@ Network value is a list of named tuples for each address assigned to the NIC. Each named tuple includes 5 fields: - - **family**: the address family, either - `AF_INET `__, - `AF_INET6 `__ + - **family**: the address family, either `AF_INET`_ or `AF_INET6`_ or :const:`psutil.AF_LINK`, which refers to a MAC address. - **address**: the primary NIC address (always set). - **netmask**: the netmask address (may be ``None``). @@ -623,9 +602,7 @@ Network snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> - See also `nettop.py `__ - and `ifconfig.py `__ - for an example application. + See also `nettop.py`_ and `ifconfig.py`_ for an example application. .. note:: if you're interested in others families (e.g. AF_BLUETOOTH) you can use @@ -667,9 +644,7 @@ Network {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500), 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536)} - Also see `nettop.py `__ - and `ifconfig.py `__ - for an example application. + Also see `nettop.py`_ and `ifconfig.py`_ for an example application. .. versionadded:: 3.0.0 @@ -697,8 +672,7 @@ Sensors shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} - See also `temperatures.py `__ and `sensors.py `__ - for an example application. + See also `temperatures.py`_ and `sensors.py`_ for an example application. Availability: Linux, FreeBSD @@ -718,8 +692,7 @@ Sensors >>> psutil.sensors_fans() {'asus': [sfan(label='cpu_fan', current=3200)]} - See also `fans.py `__ and `sensors.py `__ - for an example application. + See also `fans.py`_ and `sensors.py`_ for an example application. Availability: Linux, macOS @@ -756,7 +729,7 @@ Sensors >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) charge = 93%, time left = 4:37:08 - See also `battery.py `__ and `sensors.py `__ for an example application. + See also `battery.py`_ and `sensors.py`_ for an example application. Availability: Linux, Windows, FreeBSD @@ -782,8 +755,7 @@ Other system info .. note:: on Windows this function may return a time which is off by 1 second if it's - used across different processes (see - `issue #1007 `__). + used across different processes (see `issue #1007`_). .. function:: users() @@ -976,18 +948,15 @@ Process class .. class:: Process(pid=None) Represents an OS process with the given *pid*. - If *pid* is omitted current process *pid* - (`os.getpid() `__) is used. + If *pid* is omitted current process *pid* (`os.getpid`_) is used. Raise :class:`NoSuchProcess` if *pid* does not exist. On Linux *pid* can also refer to a thread ID (the *id* field returned by :meth:`threads` method). When accessing methods of this class always be prepared to catch :class:`NoSuchProcess` and :class:`AccessDenied` exceptions. - `hash() `__ builtin can - be used against instances of this class in order to identify a process - univocally over time (the hash is determined by mixing process PID - + creation time). As such it can also be used with - `set()s `__. + `hash`_ builtin can be used against instances of this class in order to + identify a process univocally over time (the hash is determined by mixing + process PID + creation time). As such it can also be used with `set`_. .. note:: @@ -1105,16 +1074,13 @@ Process class .. method:: ppid() The process parent PID. On Windows the return value is cached after first - call. Not on POSIX because - `ppid may change `__ - if process becomes a zombie. + call. Not on POSIX because ppid may change if process becomes a zombie See also :meth:`parent` and :meth:`parents` methods. .. method:: name() The process name. On Windows the return value is cached after first - call. Not on POSIX because the process name - `may change `__. + call. Not on POSIX because the process name may change. See also how to `find a process by name <#find-process-by-name>`__. .. method:: exe() @@ -1153,9 +1119,7 @@ Process class .. method:: create_time() The process creation time as a floating point number expressed in seconds - since the epoch, in - `UTC `__. - The return value is cached after first call. + since the epoch, in UTC. The return value is cached after first call. >>> import psutil, datetime >>> p = psutil.Process() @@ -1222,19 +1186,15 @@ Process class .. method:: uids() - The real, effective and saved user ids of this process as a - named tuple. This is the same as - `os.getresuid() `__ - but can be used for any process PID. + The real, effective and saved user ids of this process as a named tuple. + This is the same as `os.getresuid`_ but can be used for any process PID. Availability: UNIX .. method:: gids() - The real, effective and saved group ids of this process as a - named tuple. This is the same as - `os.getresgid() `__ - but can be used for any process PID. + The real, effective and saved group ids of this process as a named tuple. + This is the same as `os.getresgid`_ but can be used for any process PID. Availability: UNIX @@ -1247,8 +1207,7 @@ Process class .. method:: nice(value=None) - Get or set process - `niceness `__ (priority). + Get or set process niceness (priority). On UNIX this is a number which usually goes from ``-20`` to ``20``. The higher the nice value, the lower the priority of the process. @@ -1259,16 +1218,10 @@ Process class 10 >>> - Starting from `Python 3.3 `__ this - functionality is also available as - `os.getpriority() `__ - and - `os.setpriority() `__ - (UNIX only). - On Windows this is implemented via - `GetPriorityClass `__ - and `SetPriorityClass `__ - Windows APIs and *value* is one of the + Starting from Python 3.3 this functionality is also available as + `os.getpriority`_ and `os.setpriority`_ (see `BPO-10784`_). + On Windows this is implemented via `GetPriorityClass`_ and + `SetPriorityClass`_ Windows APIs and *value* is one of the :data:`psutil.*_PRIORITY_CLASS ` constants reflecting the MSDN documentation. Example which increases process priority on Windows: @@ -1277,9 +1230,7 @@ Process class .. method:: ionice(ioclass=None, value=None) - Get or set - `process I/O niceness `__ (priority). - On Linux *ioclass* is one of the + Get or set process I/O niceness (priority). On Linux *ioclass* is one of the :data:`psutil.IOPRIO_CLASS_*` constants. *value* is a number which goes from ``0`` to ``7``. The higher the value, the lower the I/O priority of the process. On Windows only *ioclass* is @@ -1307,14 +1258,11 @@ Process class .. method:: rlimit(resource, limits=None) - Get or set process resource limits (see - `man prlimit `__). *resource* is one + Get or set process resource limits (see `man prlimit`_). *resource* is one of the :data:`psutil.RLIMIT_* ` constants. *limits* is a ``(soft, hard)`` tuple. - This is the same as `resource.getrlimit() `__ - and `resource.setrlimit() `__ - but can be used for any process PID, not only - `os.getpid() `__. + This is the same as `resource.getrlimit`_ and `resource.setrlimit`_ + but can be used for any process PID, not only `os.getpid`_. For get, return value is a ``(soft, hard)`` tuple. Each value may be either and integer or :data:`psutil.RLIMIT_* `. Example: @@ -1336,7 +1284,7 @@ Process class Return process I/O statistics as a named tuple. For Linux you can refer to - `/proc filesysem documentation `__. + `/proc filesysem documentation `__. - **read_count**: the number of read operations performed (cumulative). This is supposed to count the number of read-related syscalls such as @@ -1414,9 +1362,7 @@ Process class `explanation `__). On Windows and macOS only *user* and *system* are filled, the others are set to ``0``. - This is similar to - `os.times() `__ - but can be used for any process PID. + This is similar to `os.times`_ but can be used for any process PID. .. versionchanged:: 4.1.0 return two extra fields: *children_user* and *children_system*. @@ -1506,7 +1452,7 @@ Process class On FreeBSD certain kernel process may return ``-1``. It may be used in conjunction with ``psutil.cpu_percent(percpu=True)`` to observe the system workload distributed across multiple CPUs as shown by - `cpu_distribution.py `__ example script. + `cpu_distribution.py`_ example script. Availability: Linux, FreeBSD, SunOS @@ -1549,32 +1495,27 @@ Process class - **rss**: aka "Resident Set Size", this is the non-swapped physical memory a process has used. - On UNIX it matches "top"'s RES column - (see `doc `__). + On UNIX it matches "top"'s RES column). On Windows this is an alias for `wset` field and it matches "Mem Usage" column of taskmgr.exe. - **vms**: aka "Virtual Memory Size", this is the total amount of virtual memory used by the process. - On UNIX it matches "top"'s VIRT column - (see `doc `__). + On UNIX it matches "top"'s VIRT column. On Windows this is an alias for `pagefile` field and it matches "Mem Usage" "VM Size" column of taskmgr.exe. - **shared**: *(Linux)* memory that could be potentially shared with other processes. - This matches "top"'s SHR column - (see `doc `__). + This matches "top"'s SHR column). - **text** *(Linux, BSD)*: aka TRS (text resident set) the amount of memory devoted to - executable code. This matches "top"'s CODE column - (see `doc `__). + executable code. This matches "top"'s CODE column). - **data** *(Linux, BSD)*: aka DRS (data resident set) the amount of physical memory devoted to - other than executable code. It matches "top"'s DATA column - (see `doc `__). + other than executable code. It matches "top"'s DATA column). - **lib** *(Linux)*: the memory used by shared libraries. @@ -1584,9 +1525,8 @@ Process class - **pageins** *(macOS)*: number of actual pageins. - For on explanation of Windows fields rely on - `PROCESS_MEMORY_COUNTERS_EX `__ structure doc. - Example on Linux: + For on explanation of Windows fields rely on `PROCESS_MEMORY_COUNTERS_EX`_ + structure doc. Example on Linux: >>> import psutil >>> p = psutil.Process() @@ -1643,8 +1583,7 @@ Process class pfullmem(rss=10199040, vms=52133888, shared=3887104, text=2867200, lib=0, data=5967872, dirty=0, uss=6545408, pss=6872064, swap=0) >>> - See also `procsmem.py `__ - for an example application. + See also `procsmem.py`_ for an example application. .. versionadded:: 4.0.0 @@ -1672,8 +1611,7 @@ Process class is ``False`` each mapped region is shown as a single entity and the named tuple will also include the mapped region's address space (*addr*) and permission set (*perms*). - See `pmap.py `__ - for an example application. + See `pmap.py`_ for an example application. +---------------+---------+--------------+-----------+ | Linux | Windows | FreeBSD | Solaris | @@ -1752,16 +1690,13 @@ Process class - **position** (*Linux*): the file (offset) position. - **mode** (*Linux*): a string indicating how the file was opened, similarly - `open `__'s - ``mode`` argument. Possible values are ``'r'``, ``'w'``, ``'a'``, - ``'r+'`` and ``'a+'``. There's no distinction between files opened in - bynary or text mode (``"b"`` or ``"t"``). + to `open`_ builtin ``mode`` argument. + Possible values are ``'r'``, ``'w'``, ``'a'``, ``'r+'`` and ``'a+'``. + There's no distinction between files opened in binary or text mode + (``"b"`` or ``"t"``). - **flags** (*Linux*): the flags which were passed to the underlying - `os.open `__ C call - when the file was opened (e.g. - `os.O_RDONLY `__, - `os.O_TRUNC `__, - etc). + `os.open`_ C call when the file was opened (e.g. `os.O_RDONLY`_, + `os.O_TRUNC`_, etc). >>> import psutil >>> f = open('file.ext', 'w') @@ -1798,17 +1733,12 @@ Process class To get system-wide connections use :func:`psutil.net_connections()`. Every named tuple provides 6 attributes: - - **fd**: the socket file descriptor. This can be passed to - `socket.fromfd() `__ - to obtain a usable socket object. - On Windows, FreeBSD and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET - `__, - `AF_INET6 `__ - or `AF_UNIX `__. - - **type**: the address type, either - `SOCK_STREAM `__ or - `SOCK_DGRAM `__. + - **fd**: the socket file descriptor. This can be passed to `socket.fromfd`_ + to obtain a usable socket object. On Windows, FreeBSD and SunOS this is + always set to ``-1``. + - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or + `AF_UNIX`_. + - **type**: the address type, either `SOCK_STREAM`_ or `SOCK_DGRAM`_. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -1891,9 +1821,8 @@ Process class .. method:: send_signal(signal) - Send a signal to process (see - `signal module `__ - constants) preemptively checking whether PID has been reused. + Send a signal to process (see `signal module`_ constants) preemptively + checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals are supported and *SIGTERM* is treated as an alias for :meth:`kill()`. @@ -1932,8 +1861,7 @@ Process class Kill the current process by using *SIGKILL* signal preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. - On Windows this is done by using - `TerminateProcess `__. + On Windows this is done by using `TerminateProcess`_. See also how to `kill a process tree <#kill-process-tree>`__ and `terminate my children <#terminate-my-children>`__. @@ -1960,10 +1888,9 @@ Popen class .. class:: Popen(*args, **kwargs) - A more convenient interface to stdlib - `subprocess.Popen `__. + A more convenient interface to stdlib `subprocess.Popen`_. It starts a sub process and you deal with it exactly as when using - `subprocess.Popen `__ + `subprocess.Popen`_. but in addition it also provides all the methods of :class:`psutil.Process` class. For method names common to both classes such as @@ -1971,18 +1898,16 @@ Popen class :meth:`terminate() ` and :meth:`kill() ` :class:`psutil.Process` implementation takes precedence. - For a complete documentation refer to - `subprocess module documentation `__. + For a complete documentation refer to subprocess module documentation. .. note:: - Unlike `subprocess.Popen `__ - this class preemptively checks whether PID has been reused on + Unlike `subprocess.Popen`_ this class preemptively checks whether PID has + been reused on :meth:`send_signal() `, :meth:`terminate() ` and :meth:`kill() ` - so that you can't accidentally terminate another process, fixing - http://bugs.python.org/issue6973. + so that you can't accidentally terminate another process, fixing `BPO-6973`_. >>> import psutil >>> from subprocess import PIPE @@ -2138,10 +2063,7 @@ Constants ``"/proc"``). You may want to re-set this constant right after importing psutil in case your /proc filesystem is mounted elsewhere or if you want to retrieve - information about Linux containers such as - `Docker `__, - `Heroku `__ or - `LXC `__ (see + information about Linux containers such as Docker, Heroku or LXC (see `here `__ for more info). It must be noted that this trick works only for APIs which rely on /proc @@ -2204,8 +2126,7 @@ Constants .. data:: REALTIME_PRIORITY_CLASS A set of integers representing the priority of a process on Windows (see - `MSDN documentation `__). - They can be used in conjunction with + `SetPriorityClass`_). They can be used in conjunction with :meth:`psutil.Process.nice()` to get or set process priority. Availability: Windows @@ -2231,10 +2152,8 @@ Constants *IOPRIO_CLASS_IDLE* means the process will get I/O time when no-one else needs the disk. For further information refer to manuals of - `ionice `__ - command line utility or - `ioprio_get `__ - system call. + `ionice `__ command line utility or + `ioprio_get`_ system call. Availability: Linux @@ -2263,8 +2182,8 @@ Constants .. data:: RLIMIT_STACK Constants used for getting and setting process resource limits to be used in - conjunction with :meth:`psutil.Process.rlimit()`. See - `man prlimit `__ for further information. + conjunction with :meth:`psutil.Process.rlimit()`. See `man prlimit`_ for + further information. Availability: Linux @@ -2313,8 +2232,7 @@ Constants Unicode ======= -Starting from version 5.3.0 psutil adds unicode support, see -`issue #1040 `__. +Starting from version 5.3.0 psutil adds unicode support, see `issue #1040`_. The notes below apply to *any* API returning a string such as :meth:`Process.exe` or :meth:`Process.cwd`, including non-filesystem related methods such as :meth:`Process.username` or :meth:`WindowsService.description`: @@ -2591,8 +2509,7 @@ FAQs * Q: Why do I get :class:`AccessDenied` for certain processes? * A: This may happen when you query processess owned by another user, - especially on `macOS `__ and - Windows. + especially on macOS (see `issue #883`_) and Windows. Unfortunately there's not much you can do about this except running the Python process with higher privileges. On Unix you may run the the Python process as root or use the SUID bit @@ -2605,8 +2522,7 @@ FAQs * Q: What about load average? * A: psutil does not expose any load average function as it's already available - in python as - `os.getloadavg `__. + in python as `os.getloadavg`_. Running tests ============= @@ -2624,8 +2540,7 @@ Development guide ================= If you plan on hacking on psutil (e.g. want to add a new feature or fix a bug) -take a look at the -`development guide `_. +take a look at the `development guide`_. Timeline ======== @@ -2914,3 +2829,61 @@ Timeline `0.1.0 `__ - `what's new `__ - `diff `__ + + +.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 +.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET +.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX +.. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`BPO-10784`: https://bugs.python.org/issue10784 +.. _`BPO-12442`: https://bugs.python.org/issue12442 +.. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 +.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`development guide`: https://github.com/giampaolo/psutil/blob/master/DEVGUIDE.rst +.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`enums`: https://docs.python.org/3/library/enum.html#module-enum +.. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`GetDriveType`: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypea +.. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ +.. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +.. _`hash`: https://docs.python.org/3/library/functions.html#hash +.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get +.. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt +.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`issue #1007`: https://github.com/giampaolo/psutil/issues/1007 +.. _`issue #1040`: https://github.com/giampaolo/psutil/issues/1040 +.. _`issue #883`: https://github.com/giampaolo/psutil/issues/883 +.. _`man prlimit`: https://linux.die.net/man/2/prlimit +.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py. +.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`open`: https://docs.python.org/3/library/functions.html#open +.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count +.. _`os.getloadavg`: https://docs.python.org/3/library/os.html#os.getloadavg +.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid +.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority +.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid +.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid +.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY +.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC +.. _`os.open`: https://docs.python.org/3/library/os.html#os.open +.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority +.. _`os.times`: https://docs.python.org//library/os.html#os.times +.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`PROCESS_MEMORY_COUNTERS_EX`: https://docs.microsoft.com/en-us/windows/desktop/api/psapi/ns-psapi-_process_memory_counters_ex +.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit +.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit +.. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. +.. _`SetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-setpriorityclass +.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. +.. _`signal module`: https://docs.python.org//library/signal.html +.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM +.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM +.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd +.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen +.. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py +.. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess diff --git a/psutil/__init__.py b/psutil/__init__.py index 9c6bf2bd66..ab2ed3490a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1417,7 +1417,7 @@ class Popen(Process): http://bugs.python.org/issue6973. For a complete documentation refer to: - http://docs.python.org/library/subprocess.html + http://docs.python.org/3/library/subprocess.html """ def __init__(self, *args, **kwargs): diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index c009e258ae..bf7841c17d 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -5,7 +5,8 @@ # found in the LICENSE file. """ -Unit test runner, providing colourized output. +Unit test runner, providing colourized output and printing failures +on KeyboardInterrupt. """ from __future__ import print_function @@ -55,7 +56,7 @@ def hilite(s, color, bold=False): return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) -class ColouredResult(unittest.TextTestResult): +class ColouredResult(TextTestResult): def addSuccess(self, test): TestResult.addSuccess(self, test) @@ -74,7 +75,7 @@ def addSkip(self, test, reason): self.stream.writeln(hilite("skipped: %s" % reason, BROWN)) def printErrorList(self, flavour, errors): - flavour = hilite(flavour, RED) + flavour = hilite(flavour, RED, bold=flavour == 'ERROR') TextTestResult.printErrorList(self, flavour, errors) @@ -120,8 +121,8 @@ def run(name=None): runner = ColouredRunner(verbosity=VERBOSITY) try: result = runner.run(get_suite(name)) - except (SystemExit, KeyboardInterrupt): - print("received KeyboardInterrupt", file=sys.stderr) + except (KeyboardInterrupt, SystemExit) as err: + print("received %s" % err.__class__.__name__, file=sys.stderr) runner.result.printErrors() sys.exit(1) else: From 3325a8ecd486100659f4017f2544202e7f6d3c29 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Mar 2019 16:27:07 +0100 Subject: [PATCH 0239/1714] Big docfix (#1464) * use https wherever possible * always point to python 3 doc * point to new MSDN urls * use RST references and avoid repetitions of URLs --- .github/ISSUE_TEMPLATE/bug.md | 10 +- .github/ISSUE_TEMPLATE/enhancement.md | 3 - CREDITS | 5 + HISTORY.rst | 6 + docs/DEVGUIDE.rst | 80 ++++--- docs/index.rst | 331 ++++++++++++-------------- psutil/__init__.py | 2 +- psutil/tests/__init__.py | 14 +- psutil/tests/runner.py | 14 +- psutil/tests/test_bsd.py | 32 +-- psutil/tests/test_linux.py | 177 +++++++------- psutil/tests/test_osx.py | 14 +- psutil/tests/test_posix.py | 10 +- psutil/tests/test_process.py | 4 +- psutil/tests/test_system.py | 6 +- psutil/tests/test_windows.py | 6 +- 16 files changed, 369 insertions(+), 345 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 9f377b2f7b..f72fe7792e 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -9,10 +9,18 @@ assignees: 'giampaolo' **Platform** * { OS version } -* { psutil version (pip show psutil) } +* { psutil version (use `pip show psutil`) } **Bug description** { a clear and concise description of what the bug is } ``` traceback message (if any) ``` + +**Test results** +{ This is an extra: you may want to run `python -c psutil.tests`. } +{ If you have failures paste those only (not the full test output). } +{ If the failures look unrelated with the issue at hand open another ticket } +``` +test failure output +``` diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 408728ee37..acf323a6ca 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -1,11 +1,8 @@ --- name: Enhancement about: Propose an enhancement -title: "title" labels: 'enhancement' assignees: 'giampaolo' --- -{ describe the proposal } - diff --git a/CREDITS b/CREDITS index d94247254e..38cd693970 100644 --- a/CREDITS +++ b/CREDITS @@ -587,3 +587,8 @@ N: Ghislain Le Meur W: https://github.com/gigi206 D: idea for Process.parents() I: 1379 + +N: Benjamin Drung +D: make tests invariant to LANG setting +W: https://github.com/bdrung +I: 1462 diff --git a/HISTORY.rst b/HISTORY.rst index cd71108d1c..9e2b547765 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,12 @@ **Enhancements** - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. +- 1464_: various docfixes (always point to python3 doc, fix links, etc.). + +**Bug fixes** + +- 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by + Benjamin Drung) 5.6.1 ===== diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index f587ebcc0a..d9eb6d7ab0 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -21,11 +21,9 @@ If you plan on hacking on psutil this is what you're supposed to do first: make test -- bear in mind that ``make`` - (see `Makefile `_) - is the designated tool to run tests, build, install etc. and that it is also - available on Windows - (see `make.bat `_). +- bear in mind that ``make``(see `Makefile`_) is the designated tool to run + tests, build, install etc. and that it is also available on Windows (see + `make.bat`_ ). - do not use ``sudo``; ``make install`` installs psutil as a limited user in "edit" mode; also ``make setup-dev-env`` installs deps as a limited user. - use `make help` to see the list of available commands. @@ -33,10 +31,9 @@ If you plan on hacking on psutil this is what you're supposed to do first: Coding style ============ -- python code strictly follows `PEP 8 `_ - styling guides and this is enforced by ``make install-git-hooks``. -- C code strictly follows `PEP 7 `_ - styling guides. +- python code strictly follows `PEP-8`_ styling guides and this is enforced by + a commit GIT hook installed via ``make install-git-hooks``. +- C code follows `PEP-7`_ styling guides. Makefile ======== @@ -100,30 +97,30 @@ Usually the files involved when adding a new functionality are: Typical process occurring when adding a new functionality (API): -- define the new function in ``psutil/__init__.py``. +- define the new function in `psutil/__init__.py`_. - write the platform specific implementation in ``psutil/_ps{platform}.py`` - (e.g. ``psutil/_pslinux.py``). + (e.g. `psutil/_pslinux.py`_). - if the change requires C, write the C implementation in - ``psutil/_psutil_{platform}.c`` (e.g. ``psutil/_psutil_linux.c``). -- write a generic test in ``psutil/tests/test_system.py`` or - ``psutil/tests/test_process.py``. + ``psutil/_psutil_{platform}.c`` (e.g. `psutil/_psutil_linux.c`_). +- write a generic test in `psutil/tests/test_system.py`_ or + `psutil/tests/test_process.py`_. - if possible, write a platform specific test in - ``psutil/tests/test_{platform}.py`` (e.g. ``test_linux.py``). + ``psutil/tests/test_{platform}.py`` (e.g. `psutil/tests/test_linux.py`_). This usually means testing the return value of the new feature against a system CLI tool. - update doc in ``doc/index.py``. - update ``HISTORY.rst``. -- update ``README.rst`` (if necessary). - make a pull request. Make a pull request =================== -- fork psutil -- create your feature branch (``git checkout -b my-new-feature``) -- commit your changes (``git commit -am 'add some feature'``) -- push to the branch (``git push origin my-new-feature``) -- create a new pull request +- fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") +- git clone your fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git``) +- create your feature branch:``git checkout -b new-feature`` +- commit your changes: ``git commit -am 'add some feature'`` +- push to the branch: ``git push origin new-feature`` +- create a new pull request by via github web interface Continuous integration ====================== @@ -136,13 +133,10 @@ Unit tests Tests are automatically run for every GIT push on **Linux**, **macOS** and **Windows** by using: -- `Travis `_ (Linux, macOS) -- `Appveyor `_ (Windows) +- `Travis`_ (Linux, macOS) +- `Appveyor`_ (Windows) -Test files controlling these are -`.travis.yml `_ -and -`appveyor.yml `_. +Test files controlling these are `.travis.yml`_ and `appveyor.yml`_. Both services run psutil test suite against all supported python version (2.6 - 3.6). Two icons in the home page (README) always show the build status: @@ -160,9 +154,8 @@ BSD, AIX and Solaris are currently tested manually. Test coverage ------------- -Test coverage is provided by `coveralls.io `_, -it is controlled via `.travis.yml `_ -and it is updated on every git push. +Test coverage is provided by `coveralls.io`_ and it is controlled via +`.travis.yml`_. An icon in the home page (README) always shows the last coverage percentage: .. image:: https://coveralls.io/repos/giampaolo/psutil/badge.svg?branch=master&service=github @@ -172,9 +165,9 @@ An icon in the home page (README) always shows the last coverage percentage: Documentation ============= -- doc source code is written in a single file: `/docs/index.rst `_. -- it uses `RsT syntax `_ - and it's built with `sphinx `_. +- doc source code is written in a single file: `/docs/index.rst`_. +- it uses `RsT syntax`_ + and it's built with `sphinx`_. - doc can be built with ``make setup-dev-env; cd docs; make html``. - public doc is hosted on http://psutil.readthedocs.io/ @@ -186,3 +179,24 @@ These are notes for myself (Giampaolo): - ``make release`` - post announce (``make print-announce``) on psutil and python-announce mailing lists, twitter, g+, blog. + + +.. _`.travis.yml`: https://github.com/giampaolo/psutil/blob/master/.travis.ym +.. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.ym +.. _`Appveyor`: https://ci.appveyor.com/project/giampaolo/psuti +.. _`coveralls.io`: https://coveralls.io/github/giampaolo/psuti +.. _`doc/index.rst`: https://github.com/giampaolo/psutil/blob/master/doc/index.rst +.. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst +.. _`make.bat`: https://github.com/giampaolo/psutil/blob/master/make.bat +.. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile +.. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ +.. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ +.. _`psutil/__init__.py`: https://github.com/giampaolo/psutil/blob/master/psutil/__init__.py +.. _`psutil/_pslinux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_pslinux.py +.. _`psutil/_psutil_linux.c`: https://github.com/giampaolo/psutil/blob/master/psutil/_psutil_linux.c +.. _`psutil/tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_linux.py +.. _`psutil/tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_process.py +.. _`psutil/tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_system.py +.. _`RsT syntax`: http://docutils.sourceforge.net/docs/user/rst/quickref.htm +.. _`sphinx`: http://sphinx-doc.org +.. _`Travis`: https://travis-ci.org/giampaolo/psuti diff --git a/docs/index.rst b/docs/index.rst index f35b23c3fc..6ff1e22c5a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,7 +37,8 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy `__ is also known to work. +Supported Python versions are **2.6**, **2.7** and **3.4+**. +`PyPy `__ is also known to work. The psutil documentation you're reading is distributed as a single HTML page. @@ -158,8 +159,7 @@ CPU .. function:: cpu_count(logical=True) - Return the number of logical CPUs in the system (same as - `os.cpu_count() `__ + Return the number of logical CPUs in the system (same as `os.cpu_count`_ in Python 3.4) or ``None`` if undetermined. If *logical* is ``False`` return the number of physical cores only (hyper thread CPUs are excluded) or ``None`` if undetermined. @@ -275,8 +275,8 @@ Memory The sum of **used** and **available** does not necessarily equal **total**. On Windows **available** and **free** are the same. - See `meminfo.py `__ - script providing an example on how to convert bytes in a human readable form. + See `meminfo.py`_ script providing an example on how to convert bytes in a + human readable form. .. note:: if you just want to know how much physical memory is left in a cross platform fashion simply rely on the **available** field. @@ -311,8 +311,8 @@ Memory (cumulative) **sin** and **sout** on Windows are always set to ``0``. - See `meminfo.py `__ - script providing an example on how to convert bytes in a human readable form. + See `meminfo.py`_ script providing an example on how to convert bytes in a + human readable form. >>> import psutil >>> psutil.swap_memory() @@ -332,21 +332,17 @@ Disks mount point and filesystem type, similarly to "df" command on UNIX. If *all* parameter is ``False`` it tries to distinguish and return physical devices only (e.g. hard disks, cd-rom drives, USB keys) and ignore all others - (e.g. memory partitions such as - `/dev/shm `__). + (e.g. memory partitions such as /dev/shm). Note that this may not be fully reliable on all systems (e.g. on BSD this parameter is ignored). Named tuple's **fstype** field is a string which varies depending on the platform. On Linux it can be one of the values found in /proc/filesystems (e.g. ``'ext3'`` for an ext3 hard drive o ``'iso9660'`` for the CD-ROM drive). - On Windows it is determined via - `GetDriveType `__ - and can be either ``"removable"``, ``"fixed"``, ``"remote"``, ``"cdrom"``, - ``"unmounted"`` or ``"ramdisk"``. On macOS and BSD it is retrieved via - `getfsstat(2) `__. See - `disk_usage.py `__ - script providing an example usage. + On Windows it is determined via `GetDriveType`_ and can be either + ``"removable"``, ``"fixed"``, ``"remote"``, ``"cdrom"``, ``"unmounted"`` or + ``"ramdisk"``. On macOS and BSD it is retrieved via `getfsstat`_ syscall. + See `disk_usage.py`_ script providing an example usage. >>> import psutil >>> psutil.disk_partitions() @@ -358,12 +354,10 @@ Disks Return disk usage statistics about the partition which contains the given *path* as a named tuple including **total**, **used** and **free** space expressed in bytes, plus the **percentage** usage. - `OSError `__ is - raised if *path* does not exist. - Starting from `Python 3.3 `__ this is - also available as - `shutil.disk_usage() `__. - See `disk_usage.py `__ script providing an example usage. + ``OSError`` is raised if *path* does not exist. + Starting from Python 3.3 this is also available as `shutil.disk_usage`_ + (see `BPO-12442`_). + See `disk_usage.py`_ script providing an example usage. >>> import psutil >>> psutil.disk_usage('/') @@ -400,16 +394,13 @@ Disks (in milliseconds) - **busy_time**: (*Linux*, *FreeBSD*) time spent doing actual I/Os (in milliseconds) - - **read_merged_count** (*Linux*): number of merged reads - (see `iostat doc `__) - - **write_merged_count** (*Linux*): number of merged writes - (see `iostats doc `__) + - **read_merged_count** (*Linux*): number of merged reads (see `iostats doc`_) + - **write_merged_count** (*Linux*): number of merged writes (see `iostats doc`_) If *perdisk* is ``True`` return the same information for every physical disk installed on the system as a dictionary with partition names as the keys and the named tuple described above as the values. - See `iotop.py `__ - for an example application. + See `iotop.py`_ for an example application. On some systems such as Linux, on a very busy or long-lived system, the numbers returned by the kernel may overflow and wrap (restart from zero). If *nowrap* is ``True`` psutil will detect and adjust those numbers across @@ -485,9 +476,7 @@ Network {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} - Also see `nettop.py `__ - and `ifconfig.py `__ - for an example application. + Also see `nettop.py`_ and `ifconfig.py`_ for an example application. .. versionchanged:: 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new @@ -499,18 +488,11 @@ Network Every named tuple provides 7 attributes: - **fd**: the socket file descriptor. If the connection refers to the current - process this may be passed to - `socket.fromfd() `__ + process this may be passed to `socket.fromfd`_ to obtain a usable socket object. On Windows and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET - `__, - `AF_INET6 `__ - or `AF_UNIX `__. - - **type**: the address type, either `SOCK_STREAM - `__ or - `SOCK_DGRAM - `__. + - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. + - **type**: the address type, either `SOCK_STREAM`_ or `SOCK_DGRAM`_. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -559,8 +541,7 @@ Network On macOS and AIX this function requires root privileges. To get per-process connections use :meth:`Process.connections`. - Also, see - `netstat.py sample script `__. + Also, see `netstat.py`_ example script. Example: >>> import psutil @@ -600,9 +581,7 @@ Network value is a list of named tuples for each address assigned to the NIC. Each named tuple includes 5 fields: - - **family**: the address family, either - `AF_INET `__, - `AF_INET6 `__ + - **family**: the address family, either `AF_INET`_ or `AF_INET6`_ or :const:`psutil.AF_LINK`, which refers to a MAC address. - **address**: the primary NIC address (always set). - **netmask**: the netmask address (may be ``None``). @@ -623,9 +602,7 @@ Network snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> - See also `nettop.py `__ - and `ifconfig.py `__ - for an example application. + See also `nettop.py`_ and `ifconfig.py`_ for an example application. .. note:: if you're interested in others families (e.g. AF_BLUETOOTH) you can use @@ -667,9 +644,7 @@ Network {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500), 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536)} - Also see `nettop.py `__ - and `ifconfig.py `__ - for an example application. + Also see `nettop.py`_ and `ifconfig.py`_ for an example application. .. versionadded:: 3.0.0 @@ -697,8 +672,7 @@ Sensors shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} - See also `temperatures.py `__ and `sensors.py `__ - for an example application. + See also `temperatures.py`_ and `sensors.py`_ for an example application. Availability: Linux, FreeBSD @@ -718,8 +692,7 @@ Sensors >>> psutil.sensors_fans() {'asus': [sfan(label='cpu_fan', current=3200)]} - See also `fans.py `__ and `sensors.py `__ - for an example application. + See also `fans.py`_ and `sensors.py`_ for an example application. Availability: Linux, macOS @@ -756,7 +729,7 @@ Sensors >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) charge = 93%, time left = 4:37:08 - See also `battery.py `__ and `sensors.py `__ for an example application. + See also `battery.py`_ and `sensors.py`_ for an example application. Availability: Linux, Windows, FreeBSD @@ -782,8 +755,7 @@ Other system info .. note:: on Windows this function may return a time which is off by 1 second if it's - used across different processes (see - `issue #1007 `__). + used across different processes (see `issue #1007`_). .. function:: users() @@ -976,18 +948,15 @@ Process class .. class:: Process(pid=None) Represents an OS process with the given *pid*. - If *pid* is omitted current process *pid* - (`os.getpid() `__) is used. + If *pid* is omitted current process *pid* (`os.getpid`_) is used. Raise :class:`NoSuchProcess` if *pid* does not exist. On Linux *pid* can also refer to a thread ID (the *id* field returned by :meth:`threads` method). When accessing methods of this class always be prepared to catch :class:`NoSuchProcess` and :class:`AccessDenied` exceptions. - `hash() `__ builtin can - be used against instances of this class in order to identify a process - univocally over time (the hash is determined by mixing process PID - + creation time). As such it can also be used with - `set()s `__. + `hash`_ builtin can be used against instances of this class in order to + identify a process univocally over time (the hash is determined by mixing + process PID + creation time). As such it can also be used with `set`_. .. note:: @@ -1105,16 +1074,13 @@ Process class .. method:: ppid() The process parent PID. On Windows the return value is cached after first - call. Not on POSIX because - `ppid may change `__ - if process becomes a zombie. + call. Not on POSIX because ppid may change if process becomes a zombie See also :meth:`parent` and :meth:`parents` methods. .. method:: name() The process name. On Windows the return value is cached after first - call. Not on POSIX because the process name - `may change `__. + call. Not on POSIX because the process name may change. See also how to `find a process by name <#find-process-by-name>`__. .. method:: exe() @@ -1153,9 +1119,7 @@ Process class .. method:: create_time() The process creation time as a floating point number expressed in seconds - since the epoch, in - `UTC `__. - The return value is cached after first call. + since the epoch, in UTC. The return value is cached after first call. >>> import psutil, datetime >>> p = psutil.Process() @@ -1222,19 +1186,15 @@ Process class .. method:: uids() - The real, effective and saved user ids of this process as a - named tuple. This is the same as - `os.getresuid() `__ - but can be used for any process PID. + The real, effective and saved user ids of this process as a named tuple. + This is the same as `os.getresuid`_ but can be used for any process PID. Availability: UNIX .. method:: gids() - The real, effective and saved group ids of this process as a - named tuple. This is the same as - `os.getresgid() `__ - but can be used for any process PID. + The real, effective and saved group ids of this process as a named tuple. + This is the same as `os.getresgid`_ but can be used for any process PID. Availability: UNIX @@ -1247,8 +1207,7 @@ Process class .. method:: nice(value=None) - Get or set process - `niceness `__ (priority). + Get or set process niceness (priority). On UNIX this is a number which usually goes from ``-20`` to ``20``. The higher the nice value, the lower the priority of the process. @@ -1259,16 +1218,10 @@ Process class 10 >>> - Starting from `Python 3.3 `__ this - functionality is also available as - `os.getpriority() `__ - and - `os.setpriority() `__ - (UNIX only). - On Windows this is implemented via - `GetPriorityClass `__ - and `SetPriorityClass `__ - Windows APIs and *value* is one of the + Starting from Python 3.3 this functionality is also available as + `os.getpriority`_ and `os.setpriority`_ (see `BPO-10784`_). + On Windows this is implemented via `GetPriorityClass`_ and + `SetPriorityClass`_ Windows APIs and *value* is one of the :data:`psutil.*_PRIORITY_CLASS ` constants reflecting the MSDN documentation. Example which increases process priority on Windows: @@ -1277,9 +1230,7 @@ Process class .. method:: ionice(ioclass=None, value=None) - Get or set - `process I/O niceness `__ (priority). - On Linux *ioclass* is one of the + Get or set process I/O niceness (priority). On Linux *ioclass* is one of the :data:`psutil.IOPRIO_CLASS_*` constants. *value* is a number which goes from ``0`` to ``7``. The higher the value, the lower the I/O priority of the process. On Windows only *ioclass* is @@ -1307,14 +1258,11 @@ Process class .. method:: rlimit(resource, limits=None) - Get or set process resource limits (see - `man prlimit `__). *resource* is one + Get or set process resource limits (see `man prlimit`_). *resource* is one of the :data:`psutil.RLIMIT_* ` constants. *limits* is a ``(soft, hard)`` tuple. - This is the same as `resource.getrlimit() `__ - and `resource.setrlimit() `__ - but can be used for any process PID, not only - `os.getpid() `__. + This is the same as `resource.getrlimit`_ and `resource.setrlimit`_ + but can be used for any process PID, not only `os.getpid`_. For get, return value is a ``(soft, hard)`` tuple. Each value may be either and integer or :data:`psutil.RLIMIT_* `. Example: @@ -1336,7 +1284,7 @@ Process class Return process I/O statistics as a named tuple. For Linux you can refer to - `/proc filesysem documentation `__. + `/proc filesysem documentation `__. - **read_count**: the number of read operations performed (cumulative). This is supposed to count the number of read-related syscalls such as @@ -1414,9 +1362,7 @@ Process class `explanation `__). On Windows and macOS only *user* and *system* are filled, the others are set to ``0``. - This is similar to - `os.times() `__ - but can be used for any process PID. + This is similar to `os.times`_ but can be used for any process PID. .. versionchanged:: 4.1.0 return two extra fields: *children_user* and *children_system*. @@ -1506,7 +1452,7 @@ Process class On FreeBSD certain kernel process may return ``-1``. It may be used in conjunction with ``psutil.cpu_percent(percpu=True)`` to observe the system workload distributed across multiple CPUs as shown by - `cpu_distribution.py `__ example script. + `cpu_distribution.py`_ example script. Availability: Linux, FreeBSD, SunOS @@ -1549,32 +1495,27 @@ Process class - **rss**: aka "Resident Set Size", this is the non-swapped physical memory a process has used. - On UNIX it matches "top"'s RES column - (see `doc `__). + On UNIX it matches "top"'s RES column). On Windows this is an alias for `wset` field and it matches "Mem Usage" column of taskmgr.exe. - **vms**: aka "Virtual Memory Size", this is the total amount of virtual memory used by the process. - On UNIX it matches "top"'s VIRT column - (see `doc `__). + On UNIX it matches "top"'s VIRT column. On Windows this is an alias for `pagefile` field and it matches "Mem Usage" "VM Size" column of taskmgr.exe. - **shared**: *(Linux)* memory that could be potentially shared with other processes. - This matches "top"'s SHR column - (see `doc `__). + This matches "top"'s SHR column). - **text** *(Linux, BSD)*: aka TRS (text resident set) the amount of memory devoted to - executable code. This matches "top"'s CODE column - (see `doc `__). + executable code. This matches "top"'s CODE column). - **data** *(Linux, BSD)*: aka DRS (data resident set) the amount of physical memory devoted to - other than executable code. It matches "top"'s DATA column - (see `doc `__). + other than executable code. It matches "top"'s DATA column). - **lib** *(Linux)*: the memory used by shared libraries. @@ -1584,9 +1525,8 @@ Process class - **pageins** *(macOS)*: number of actual pageins. - For on explanation of Windows fields rely on - `PROCESS_MEMORY_COUNTERS_EX `__ structure doc. - Example on Linux: + For on explanation of Windows fields rely on `PROCESS_MEMORY_COUNTERS_EX`_ + structure doc. Example on Linux: >>> import psutil >>> p = psutil.Process() @@ -1643,8 +1583,7 @@ Process class pfullmem(rss=10199040, vms=52133888, shared=3887104, text=2867200, lib=0, data=5967872, dirty=0, uss=6545408, pss=6872064, swap=0) >>> - See also `procsmem.py `__ - for an example application. + See also `procsmem.py`_ for an example application. .. versionadded:: 4.0.0 @@ -1672,8 +1611,7 @@ Process class is ``False`` each mapped region is shown as a single entity and the named tuple will also include the mapped region's address space (*addr*) and permission set (*perms*). - See `pmap.py `__ - for an example application. + See `pmap.py`_ for an example application. +---------------+---------+--------------+-----------+ | Linux | Windows | FreeBSD | Solaris | @@ -1752,16 +1690,13 @@ Process class - **position** (*Linux*): the file (offset) position. - **mode** (*Linux*): a string indicating how the file was opened, similarly - `open `__'s - ``mode`` argument. Possible values are ``'r'``, ``'w'``, ``'a'``, - ``'r+'`` and ``'a+'``. There's no distinction between files opened in - bynary or text mode (``"b"`` or ``"t"``). + to `open`_ builtin ``mode`` argument. + Possible values are ``'r'``, ``'w'``, ``'a'``, ``'r+'`` and ``'a+'``. + There's no distinction between files opened in binary or text mode + (``"b"`` or ``"t"``). - **flags** (*Linux*): the flags which were passed to the underlying - `os.open `__ C call - when the file was opened (e.g. - `os.O_RDONLY `__, - `os.O_TRUNC `__, - etc). + `os.open`_ C call when the file was opened (e.g. `os.O_RDONLY`_, + `os.O_TRUNC`_, etc). >>> import psutil >>> f = open('file.ext', 'w') @@ -1798,17 +1733,12 @@ Process class To get system-wide connections use :func:`psutil.net_connections()`. Every named tuple provides 6 attributes: - - **fd**: the socket file descriptor. This can be passed to - `socket.fromfd() `__ - to obtain a usable socket object. - On Windows, FreeBSD and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET - `__, - `AF_INET6 `__ - or `AF_UNIX `__. - - **type**: the address type, either - `SOCK_STREAM `__ or - `SOCK_DGRAM `__. + - **fd**: the socket file descriptor. This can be passed to `socket.fromfd`_ + to obtain a usable socket object. On Windows, FreeBSD and SunOS this is + always set to ``-1``. + - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or + `AF_UNIX`_. + - **type**: the address type, either `SOCK_STREAM`_ or `SOCK_DGRAM`_. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -1891,9 +1821,8 @@ Process class .. method:: send_signal(signal) - Send a signal to process (see - `signal module `__ - constants) preemptively checking whether PID has been reused. + Send a signal to process (see `signal module`_ constants) preemptively + checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals are supported and *SIGTERM* is treated as an alias for :meth:`kill()`. @@ -1932,8 +1861,7 @@ Process class Kill the current process by using *SIGKILL* signal preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. - On Windows this is done by using - `TerminateProcess `__. + On Windows this is done by using `TerminateProcess`_. See also how to `kill a process tree <#kill-process-tree>`__ and `terminate my children <#terminate-my-children>`__. @@ -1960,10 +1888,9 @@ Popen class .. class:: Popen(*args, **kwargs) - A more convenient interface to stdlib - `subprocess.Popen `__. + A more convenient interface to stdlib `subprocess.Popen`_. It starts a sub process and you deal with it exactly as when using - `subprocess.Popen `__ + `subprocess.Popen`_. but in addition it also provides all the methods of :class:`psutil.Process` class. For method names common to both classes such as @@ -1971,18 +1898,16 @@ Popen class :meth:`terminate() ` and :meth:`kill() ` :class:`psutil.Process` implementation takes precedence. - For a complete documentation refer to - `subprocess module documentation `__. + For a complete documentation refer to subprocess module documentation. .. note:: - Unlike `subprocess.Popen `__ - this class preemptively checks whether PID has been reused on + Unlike `subprocess.Popen`_ this class preemptively checks whether PID has + been reused on :meth:`send_signal() `, :meth:`terminate() ` and :meth:`kill() ` - so that you can't accidentally terminate another process, fixing - http://bugs.python.org/issue6973. + so that you can't accidentally terminate another process, fixing `BPO-6973`_. >>> import psutil >>> from subprocess import PIPE @@ -2138,10 +2063,7 @@ Constants ``"/proc"``). You may want to re-set this constant right after importing psutil in case your /proc filesystem is mounted elsewhere or if you want to retrieve - information about Linux containers such as - `Docker `__, - `Heroku `__ or - `LXC `__ (see + information about Linux containers such as Docker, Heroku or LXC (see `here `__ for more info). It must be noted that this trick works only for APIs which rely on /proc @@ -2204,8 +2126,7 @@ Constants .. data:: REALTIME_PRIORITY_CLASS A set of integers representing the priority of a process on Windows (see - `MSDN documentation `__). - They can be used in conjunction with + `SetPriorityClass`_). They can be used in conjunction with :meth:`psutil.Process.nice()` to get or set process priority. Availability: Windows @@ -2231,10 +2152,8 @@ Constants *IOPRIO_CLASS_IDLE* means the process will get I/O time when no-one else needs the disk. For further information refer to manuals of - `ionice `__ - command line utility or - `ioprio_get `__ - system call. + `ionice `__ command line utility or + `ioprio_get`_ system call. Availability: Linux @@ -2263,8 +2182,8 @@ Constants .. data:: RLIMIT_STACK Constants used for getting and setting process resource limits to be used in - conjunction with :meth:`psutil.Process.rlimit()`. See - `man prlimit `__ for further information. + conjunction with :meth:`psutil.Process.rlimit()`. See `man prlimit`_ for + further information. Availability: Linux @@ -2313,8 +2232,7 @@ Constants Unicode ======= -Starting from version 5.3.0 psutil adds unicode support, see -`issue #1040 `__. +Starting from version 5.3.0 psutil adds unicode support, see `issue #1040`_. The notes below apply to *any* API returning a string such as :meth:`Process.exe` or :meth:`Process.cwd`, including non-filesystem related methods such as :meth:`Process.username` or :meth:`WindowsService.description`: @@ -2591,8 +2509,7 @@ FAQs * Q: Why do I get :class:`AccessDenied` for certain processes? * A: This may happen when you query processess owned by another user, - especially on `macOS `__ and - Windows. + especially on macOS (see `issue #883`_) and Windows. Unfortunately there's not much you can do about this except running the Python process with higher privileges. On Unix you may run the the Python process as root or use the SUID bit @@ -2605,8 +2522,7 @@ FAQs * Q: What about load average? * A: psutil does not expose any load average function as it's already available - in python as - `os.getloadavg `__. + in python as `os.getloadavg`_. Running tests ============= @@ -2624,8 +2540,7 @@ Development guide ================= If you plan on hacking on psutil (e.g. want to add a new feature or fix a bug) -take a look at the -`development guide `_. +take a look at the `development guide`_. Timeline ======== @@ -2914,3 +2829,61 @@ Timeline `0.1.0 `__ - `what's new `__ - `diff `__ + + +.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 +.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET +.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX +.. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`BPO-10784`: https://bugs.python.org/issue10784 +.. _`BPO-12442`: https://bugs.python.org/issue12442 +.. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 +.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`development guide`: https://github.com/giampaolo/psutil/blob/master/DEVGUIDE.rst +.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`enums`: https://docs.python.org/3/library/enum.html#module-enum +.. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`GetDriveType`: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypea +.. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ +.. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +.. _`hash`: https://docs.python.org/3/library/functions.html#hash +.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get +.. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt +.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`issue #1007`: https://github.com/giampaolo/psutil/issues/1007 +.. _`issue #1040`: https://github.com/giampaolo/psutil/issues/1040 +.. _`issue #883`: https://github.com/giampaolo/psutil/issues/883 +.. _`man prlimit`: https://linux.die.net/man/2/prlimit +.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py. +.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`open`: https://docs.python.org/3/library/functions.html#open +.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count +.. _`os.getloadavg`: https://docs.python.org/3/library/os.html#os.getloadavg +.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid +.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority +.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid +.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid +.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY +.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC +.. _`os.open`: https://docs.python.org/3/library/os.html#os.open +.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority +.. _`os.times`: https://docs.python.org//library/os.html#os.times +.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`PROCESS_MEMORY_COUNTERS_EX`: https://docs.microsoft.com/en-us/windows/desktop/api/psapi/ns-psapi-_process_memory_counters_ex +.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit +.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit +.. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. +.. _`SetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-setpriorityclass +.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. +.. _`signal module`: https://docs.python.org//library/signal.html +.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM +.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM +.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd +.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen +.. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py +.. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess diff --git a/psutil/__init__.py b/psutil/__init__.py index 9c6bf2bd66..ab2ed3490a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1417,7 +1417,7 @@ class Popen(Process): http://bugs.python.org/issue6973. For a complete documentation refer to: - http://docs.python.org/library/subprocess.html + http://docs.python.org/3/library/subprocess.html """ def __init__(self, *args, **kwargs): diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 13c87c65b9..3aebbcbd78 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -80,7 +80,7 @@ 'ThreadTask' # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_before_failing', + 'retry_on_failure', # install utils 'install_pip', 'install_test_deps', # fs utils @@ -119,7 +119,7 @@ # --- configurable defaults -# how many times retry_before_failing() decorator will retry +# 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 @@ -607,7 +607,7 @@ def __init__(self, timeout=None, retries=None, interval=0.001, - logfun=print, + logfun=None, ): if timeout and retries: raise ValueError("timeout and retries args are mutually exclusive") @@ -814,11 +814,15 @@ def __str__(self): unittest.TestCase = TestCase -def retry_before_failing(retries=NO_RETRIES): +def retry_on_failure(retries=NO_RETRIES): """Decorator which runs a test function and retries N times before actually failing. """ - return retry(exception=AssertionError, timeout=None, retries=retries) + 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): diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index c009e258ae..3fc5e05f14 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -5,7 +5,8 @@ # found in the LICENSE file. """ -Unit test runner, providing colourized output. +Unit test runner, providing colourized output and printing failures +on KeyboardInterrupt. """ from __future__ import print_function @@ -55,7 +56,7 @@ def hilite(s, color, bold=False): return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) -class ColouredResult(unittest.TextTestResult): +class ColouredResult(TextTestResult): def addSuccess(self, test): TestResult.addSuccess(self, test) @@ -74,7 +75,7 @@ def addSkip(self, test, reason): self.stream.writeln(hilite("skipped: %s" % reason, BROWN)) def printErrorList(self, flavour, errors): - flavour = hilite(flavour, RED) + flavour = hilite(flavour, RED, bold=flavour == 'ERROR') TextTestResult.printErrorList(self, flavour, errors) @@ -84,8 +85,7 @@ class ColouredRunner(TextTestRunner): def _makeResult(self): # Store result instance so that it can be accessed on # KeyboardInterrupt. - self.result = self.resultclass( - self.stream, self.descriptions, self.verbosity) + self.result = TextTestRunner._makeResult(self) return self.result @@ -120,8 +120,8 @@ def run(name=None): runner = ColouredRunner(verbosity=VERBOSITY) try: result = runner.run(get_suite(name)) - except (SystemExit, KeyboardInterrupt): - print("received KeyboardInterrupt", file=sys.stderr) + except (KeyboardInterrupt, SystemExit) as err: + print("received %s" % err.__class__.__name__, file=sys.stderr) runner.result.printErrors() sys.exit(1) else: diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index f1b5775a99..5df8ad2982 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -24,7 +24,7 @@ from psutil.tests import HAS_BATTERY from psutil.tests import MEMORY_TOLERANCE from psutil.tests import reap_children -from psutil.tests import retry_before_failing +from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import unittest from psutil.tests import which @@ -171,7 +171,7 @@ def parse_swapinfo(): total, used, free = (int(p) * 1024 for p in parts[1:4]) return total, used, free - @retry_before_failing() + @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) @@ -209,7 +209,7 @@ def test_proc_uids_gids(self): self.assertEqual(gids.effective, int(egid)) self.assertEqual(gids.saved, int(sgid)) - @retry_before_failing() + @retry_on_failure() def test_proc_ctx_switches(self): tested = [] out = sh('procstat -r %s' % self.pid) @@ -229,7 +229,7 @@ def test_proc_ctx_switches(self): if len(tested) != 2: raise RuntimeError("couldn't find lines match in procstat out") - @retry_before_failing() + @retry_on_failure() def test_proc_cpu_times(self): tested = [] out = sh('procstat -r %s' % self.pid) @@ -268,37 +268,37 @@ def test_cpu_frequency_against_sysctl(self): # --- virtual_memory(); tests against sysctl - @retry_before_failing() + @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_before_failing() + @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_before_failing() + @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_before_failing() + @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_before_failing() + @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_before_failing() + @retry_on_failure() def test_vmem_buffers(self): syst = sysctl("vfs.bufspace") self.assertAlmostEqual(psutil.virtual_memory().buffers, syst, @@ -312,42 +312,42 @@ def test_muse_vmem_total(self): self.assertEqual(psutil.virtual_memory().total, num) @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_before_failing() + @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_before_failing() + @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_before_failing() + @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_before_failing() + @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_before_failing() + @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_before_failing() + @retry_on_failure() def test_muse_vmem_buffers(self): num = muse('Buffer') self.assertAlmostEqual(psutil.virtual_memory().buffers, num, diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 6e260b994c..394ce654e0 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -37,7 +37,7 @@ from psutil.tests import pyrun from psutil.tests import reap_children from psutil.tests import reload_module -from psutil.tests import retry_before_failing +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 @@ -204,7 +204,7 @@ def test_total(self): # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e @unittest.skipIf(LINUX and get_free_version_info() < (3, 3, 12), "old free version") - @retry_before_failing() + @retry_on_failure() def test_used(self): free = free_physmem() free_value = free.used @@ -214,18 +214,14 @@ def test_used(self): msg='%s %s \n%s' % (free_value, psutil_value, free.output)) @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_before_failing() + @retry_on_failure() def test_free(self): - # _, _, free_value, _ = free_physmem() - # psutil_value = psutil.virtual_memory().free - # self.assertAlmostEqual( - # free_value, psutil_value, delta=MEMORY_TOLERANCE) vmstat_value = vmstat('free memory') * 1024 psutil_value = psutil.virtual_memory().free self.assertAlmostEqual( vmstat_value, psutil_value, delta=MEMORY_TOLERANCE) - @retry_before_failing() + @retry_on_failure() def test_buffers(self): vmstat_value = vmstat('buffer memory') * 1024 psutil_value = psutil.virtual_memory().buffers @@ -234,7 +230,7 @@ def test_buffers(self): # https://travis-ci.org/giampaolo/psutil/jobs/226719664 @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_before_failing() + @retry_on_failure() def test_active(self): vmstat_value = vmstat('active memory') * 1024 psutil_value = psutil.virtual_memory().active @@ -243,14 +239,14 @@ def test_active(self): # https://travis-ci.org/giampaolo/psutil/jobs/227242952 @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_before_failing() + @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_before_failing() + @retry_on_failure() def test_shared(self): free = free_physmem() free_value = free.shared @@ -261,7 +257,7 @@ def test_shared(self): free_value, psutil_value, delta=MEMORY_TOLERANCE, msg='%s %s \n%s' % (free_value, psutil_value, free.output)) - @retry_before_failing() + @retry_on_failure() def test_available(self): # "free" output format has changed at some point: # https://github.com/giampaolo/psutil/issues/538#issuecomment-147192098 @@ -512,14 +508,14 @@ def test_total(self): return self.assertAlmostEqual( free_value, psutil_value, delta=MEMORY_TOLERANCE) - @retry_before_failing() + @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_before_failing() + @retry_on_failure() def test_free(self): free_value = free_swap().free psutil_value = psutil.swap_memory().free @@ -591,10 +587,10 @@ def test_emulate_meminfo_has_no_metrics(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPU(unittest.TestCase): +class TestSystemCPUTimes(unittest.TestCase): @unittest.skipIf(TRAVIS, "unknown failure on travis") - def test_cpu_times(self): + 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('.'))) @@ -611,9 +607,13 @@ def test_cpu_times(self): 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_cpu_count_logical_w_sysdev_cpu_online(self): + def test_against_sysdev_cpu_online(self): with open("/sys/devices/system/cpu/online") as f: value = f.read().strip() if "-" in str(value): @@ -622,33 +622,23 @@ def test_cpu_count_logical_w_sysdev_cpu_online(self): @unittest.skipIf(not os.path.exists("/sys/devices/system/cpu"), "/sys/devices/system/cpu does not exist") - def test_cpu_count_logical_w_sysdev_cpu_num(self): + 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_cpu_count_logical_w_nproc(self): + 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_cpu_count_logical_w_lscpu(self): + 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) - @unittest.skipIf(not which("lscpu"), "lscpu utility not available") - def test_cpu_count_physical_w_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_cpu_count_logical_mocked(self): + 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 @@ -681,21 +671,37 @@ def test_cpu_count_logical_mocked(self): self.assertEqual(psutil._pslinux.cpu_count_logical(), original) m.called - def test_cpu_count_physical_mocked(self): - # Have open() return emtpy data and make sure None is returned - # ('cause we want to mimick os.cpu_count()) + +@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_empty_cpuinfo(self): with mock.patch('psutil._common.open', create=True) as m: self.assertIsNone(psutil._pslinux.cpu_count_physical()) assert m.called + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemCPUFrequency(unittest.TestCase): + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_no_result(self): + def test_emulate_no_files(self): with mock.patch("psutil._pslinux.glob.glob", return_value=[]): self.assertIsNone(psutil.cpu_freq()) @unittest.skipIf(TRAVIS, "fails on Travis") @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_use_second_file(self): + def test_emulate_use_second_file(self): # https://github.com/giampaolo/psutil/issues/981 def glob_mock(pattern): if pattern.startswith("/sys/devices/system/cpu/cpufreq/policy"): @@ -713,7 +719,7 @@ def glob_mock(pattern): self.assertEqual(len(flags), 2) @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_use_cpuinfo(self): + 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): @@ -742,7 +748,7 @@ def path_exists_mock(path): reload_module(psutil) @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_emulate_data(self): + def test_emulate_data(self): def open_mock(name, *args, **kwargs): if name.endswith('/scaling_cur_freq'): return io.BytesIO(b"500000") @@ -765,7 +771,7 @@ def open_mock(name, *args, **kwargs): self.assertEqual(freq.max, 700.0) @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_emulate_multi_cpu(self): + def test_emulate_multi_cpu(self): def open_mock(name, *args, **kwargs): if name.endswith('/scaling_cur_freq'): return io.BytesIO(b"100000") @@ -790,7 +796,7 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(TRAVIS, "fails on Travis") @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_no_scaling_cur_freq_file(self): + 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'): @@ -829,11 +835,6 @@ def open_mock(name, *args, **kwargs): self.assertRaises(NotImplementedError, psutil.cpu_freq) -# ===================================================================== -# --- system CPU stats -# ===================================================================== - - @unittest.skipIf(not LINUX, "LINUX only") class TestSystemCPUStats(unittest.TestCase): @@ -856,9 +857,9 @@ def test_interrupts(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetwork(unittest.TestCase): +class TestSystemNetIfAddrs(unittest.TestCase): - def test_net_if_addrs_ips(self): + def test_ips(self): for name, addrs in psutil.net_if_addrs().items(): for addr in addrs: if addr.family == psutil.AF_LINK: @@ -867,7 +868,27 @@ def test_net_if_addrs_ips(self): self.assertEqual(addr.address, get_ipv4_address(name)) # TODO: test for AF_INET6 family - def test_net_if_stats(self): + # 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) @@ -879,8 +900,12 @@ def test_net_if_stats(self): self.assertEqual(stats.mtu, int(re.findall(r'(?i)MTU[: ](\d+)', out)[0])) - @retry_before_failing() - def test_net_io_counters(self): + +@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) @@ -921,25 +946,13 @@ def ifconfig(nic): self.assertAlmostEqual( stats.dropout, ifconfig_ret['dropout'], delta=10) - # 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 TestSystemNetConnections(unittest.TestCase): @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) - def test_net_connections_ipv6_unsupported(self, supports_ipv6, inet_ntop): + 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) @@ -949,7 +962,7 @@ def test_net_connections_ipv6_unsupported(self, supports_ipv6, inet_ntop): pass psutil.net_connections(kind='inet6') - def test_net_connections_mocked(self): + def test_emulate_unix(self): with mock_open_content( '/proc/net/unix', textwrap.dedent("""\ @@ -963,16 +976,16 @@ def test_net_connections_mocked(self): # ===================================================================== -# --- system disk +# --- system disks # ===================================================================== @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemDisks(unittest.TestCase): +class TestSystemDiskPartitions(unittest.TestCase): @unittest.skipIf(not hasattr(os, 'statvfs'), "os.statvfs() not available") @skip_on_not_implemented() - def test_disk_partitions_and_usage(self): + def test_against_df(self): # test psutil.disk_usage() and psutil.disk_partitions() # against "df -a" def df(path): @@ -996,7 +1009,7 @@ def df(path): if abs(usage.used - used) > 10 * 1024 * 1024: self.fail("psutil=%s, df=%s" % (usage.used, used)) - def test_disk_partitions_mocked(self): + def test_zfs_fs(self): # Test that ZFS partitions are returned. with open("/proc/filesystems", "r") as f: data = f.read() @@ -1020,7 +1033,7 @@ def test_disk_partitions_mocked(self): assert ret self.assertEqual(ret[0].fstype, 'zfs') - def test_disk_partitions_procfs(self): + def test_emulate_realpath_fail(self): # See: https://github.com/giampaolo/psutil/issues/1307 try: with mock.patch('os.path.realpath', @@ -1032,7 +1045,11 @@ def test_disk_partitions_procfs(self): finally: psutil.PROCFS_PATH = "/proc" - def test_disk_io_counters_kernel_2_4_mocked(self): + +@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( @@ -1051,7 +1068,7 @@ def test_disk_io_counters_kernel_2_4_mocked(self): self.assertEqual(ret.write_time, 8) self.assertEqual(ret.busy_time, 10) - def test_disk_io_counters_kernel_2_6_full_mocked(self): + 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 @@ -1071,7 +1088,7 @@ def test_disk_io_counters_kernel_2_6_full_mocked(self): self.assertEqual(ret.write_time, 8) self.assertEqual(ret.busy_time, 10) - def test_disk_io_counters_kernel_2_6_limited_mocked(self): + 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 @@ -1094,7 +1111,7 @@ def test_disk_io_counters_kernel_2_6_limited_mocked(self): self.assertEqual(ret.write_time, 0) self.assertEqual(ret.busy_time, 0) - def test_disk_io_counters_include_partitions(self): + 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 @@ -1113,7 +1130,7 @@ def test_disk_io_counters_include_partitions(self): self.assertEqual(ret['nvme0n1'].write_count, 5) self.assertEqual(ret['nvme0n1p1'].write_count, 5) - def test_disk_io_counters_exclude_partitions(self): + 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 @@ -1144,7 +1161,7 @@ def is_storage_device(name): self.assertEqual(ret.read_count, 1) self.assertEqual(ret.write_count, 5) - def test_disk_io_counters_sysfs(self): + def test_emulate_use_sysfs(self): def exists(path): if path == '/proc/diskstats': return False @@ -1156,7 +1173,7 @@ def exists(path): wsysfs = psutil.disk_io_counters(perdisk=True) self.assertEqual(len(wprocfs), len(wsysfs)) - def test_disk_io_counters_not_impl(self): + def test_emulate_not_impl(self): def exists(path): return False @@ -2031,7 +2048,7 @@ def test_gids(self): value = tuple(map(int, value.split()[1:4])) self.assertEqual(self.proc.gids(), value) - @retry_before_failing() + @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) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 0e6ed67c7b..723b255eea 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -17,7 +17,7 @@ from psutil.tests import HAS_BATTERY from psutil.tests import MEMORY_TOLERANCE from psutil.tests import reap_children -from psutil.tests import retry_before_failing +from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import unittest @@ -215,25 +215,25 @@ def test_vmem_total(self): sysctl_hwphymem = sysctl('sysctl hw.memsize') self.assertEqual(sysctl_hwphymem, psutil.virtual_memory().total) - @retry_before_failing() + @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_before_failing() + @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_before_failing() + @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_before_failing() + @retry_on_failure() def test_vmem_wired(self): vmstat_val = vm_stat("wired") psutil_val = psutil.virtual_memory().wired @@ -241,13 +241,13 @@ def test_vmem_wired(self): # --- swap mem - @retry_before_failing() + @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_before_failing() + @retry_on_failure() def test_swapmem_sout(self): vmstat_val = vm_stat("Pageout") psutil_val = psutil.swap_memory().sout diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 8f7fbf51b4..d24abad38d 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -29,7 +29,7 @@ from psutil.tests import mock from psutil.tests import PYTHON_EXE from psutil.tests import reap_children -from psutil.tests import retry_before_failing +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 @@ -157,7 +157,7 @@ def test_username_no_resolution(self): assert fun.called @skip_on_access_denied() - @retry_before_failing() + @retry_on_failure() def test_rss_memory(self): # give python interpreter some time to properly initialize # so that the results are the same @@ -167,7 +167,7 @@ def test_rss_memory(self): self.assertEqual(rss_ps, rss_psutil) @skip_on_access_denied() - @retry_before_failing() + @retry_on_failure() def test_vsz_memory(self): # give python interpreter some time to properly initialize # so that the results are the same @@ -317,7 +317,7 @@ def call(p, attr): class TestSystemAPIs(unittest.TestCase): """Test some system APIs.""" - @retry_before_failing() + @retry_on_failure() def test_pids(self): # Note: this test might fail if the OS is starting/killing # other processes in the meantime @@ -354,7 +354,7 @@ def test_nic_names(self): # can't find users on APPVEYOR or TRAVIS @unittest.skipIf(APPVEYOR or TRAVIS and not psutil.users(), "unreliable on APPVEYOR or TRAVIS") - @retry_before_failing() + @retry_on_failure() def test_users(self): out = sh("who") lines = out.split('\n') diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index baa3e3461b..a550809ff4 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -56,7 +56,7 @@ from psutil.tests import PYPY from psutil.tests import PYTHON_EXE from psutil.tests import reap_children -from psutil.tests import retry_before_failing +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 @@ -545,7 +545,7 @@ def test_threads(self): self.assertEqual(athread.user_time, athread[1]) self.assertEqual(athread.system_time, athread[2]) - @retry_before_failing() + @retry_on_failure() @skip_on_access_denied(only_if=MACOS) @unittest.skipIf(not HAS_THREADS, 'not supported') def test_threads_2(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 00a385861c..75e65b0fbd 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -44,7 +44,7 @@ from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import mock from psutil.tests import reap_children -from psutil.tests import retry_before_failing +from psutil.tests import retry_on_failure from psutil.tests import safe_rmpath from psutil.tests import TESTFN from psutil.tests import TESTFN_UNICODE @@ -124,7 +124,7 @@ def callback(p): for p in alive: self.assertFalse(hasattr(p, 'returncode')) - @retry_before_failing(30) + @retry_on_failure(30) def test(procs, callback): gone, alive = psutil.wait_procs(procs, timeout=0.03, callback=callback) @@ -143,7 +143,7 @@ def test(procs, callback): for p in alive: self.assertFalse(hasattr(p, 'returncode')) - @retry_before_failing(30) + @retry_on_failure(30) def test(procs, callback): gone, alive = psutil.wait_procs(procs, timeout=0.03, callback=callback) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index a9aeba8e6b..e0bfdbb837 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -26,7 +26,7 @@ from psutil.tests import HAS_BATTERY from psutil.tests import mock from psutil.tests import reap_children -from psutil.tests import retry_before_failing +from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import unittest @@ -137,7 +137,7 @@ def test_total_phymem(self): # Note: this test is not very reliable @unittest.skipIf(APPVEYOR, "test not relieable on appveyor") - @retry_before_failing() + @retry_on_failure() def test_pids(self): # Note: this test might fail if the OS is starting/killing # other processes in the meantime @@ -146,7 +146,7 @@ def test_pids(self): psutil_pids = set(psutil.pids()) self.assertEqual(wmi_pids, psutil_pids) - @retry_before_failing() + @retry_on_failure() def test_disks(self): ps_parts = psutil.disk_partitions(all=True) wmi_parts = wmi.WMI().Win32_LogicalDisk() From b583b8b10d744412f0ecd0f8265f6cf73b147d77 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Mar 2019 20:07:11 +0100 Subject: [PATCH 0240/1714] issue #1404 / linux / phys CPUs count determine CPUs from /sys/devices/system/cpu/cpu[0-9]/topology/core_id in case /proc/cpuinfo does not provide this info --- HISTORY.rst | 3 +++ psutil/_pslinux.py | 14 ++++++++++++-- psutil/tests/test_linux.py | 10 ++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9e2b547765..217effbd9a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,9 @@ **Enhancements** +- 1404_: [Linux] cpu_count(logical=False) uses a second method (read from + `/sys/devices/system/cpu/cpu[0-9]/topology/core_id`) in order to determine + the number of physical CPUs in case /proc/cpuinfo does not provide this info. - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. - 1464_: various docfixes (always point to python3 doc, fix links, etc.). diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 41be6665fb..3502f1b140 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -627,6 +627,16 @@ def cpu_count_logical(): def cpu_count_physical(): """Return the number of physical cores in the system.""" + # Method #1 + core_ids = set() + for path in glob.glob("/sys/devices/system/cpu/cpu[0-9]/topology/core_id"): + with open_binary(path) as f: + core_ids.add(int(f.read())) + result = len(core_ids) + if result != 0: + return result + + # Method #2 mapping = {} current_info = {} with open_binary('%s/cpuinfo' % get_procfs_path()) as f: @@ -646,8 +656,8 @@ def cpu_count_physical(): 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(): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 394ce654e0..66c50aa685 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -685,10 +685,12 @@ def test_against_lscpu(self): core_ids.add(fields[1]) self.assertEqual(psutil.cpu_count(logical=False), len(core_ids)) - def test_emulate_empty_cpuinfo(self): - with mock.patch('psutil._common.open', create=True) as m: - self.assertIsNone(psutil._pslinux.cpu_count_physical()) - assert m.called + 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") From 672196a13115a6cfaf798d15b92b469c3f71b35f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Mar 2019 20:39:27 +0100 Subject: [PATCH 0241/1714] update issue template --- .github/ISSUE_TEMPLATE/bug.md | 17 ++++++++++------- .github/ISSUE_TEMPLATE/enhancement.md | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index f72fe7792e..283d8dc672 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -6,21 +6,24 @@ labels: 'bug' assignees: 'giampaolo' --- - **Platform** -* { OS version } -* { psutil version (use `pip show psutil`) } +* { OS version } (also add appropriate OS issue label (linux, windows, ...)) +* { psutil version } (use "pip show psutil") **Bug description** { a clear and concise description of what the bug is } + ``` traceback message (if any) ``` +```python +code to reproduce the problem (if any) +``` + **Test results** -{ This is an extra: you may want to run `python -c psutil.tests`. } -{ If you have failures paste those only (not the full test output). } -{ If the failures look unrelated with the issue at hand open another ticket } ``` -test failure output +output of `python -c psutil.tests` (failures only, not full result) ``` +{ you may want to do this in order to discover other issues affecting your platform } +{ if failures look unrelated with the issue at hand open another ticket } diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index acf323a6ca..1a5e14e590 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -3,6 +3,8 @@ name: Enhancement about: Propose an enhancement labels: 'enhancement' assignees: 'giampaolo' +title: "[OS] title" --- +{ a clear and concise description of what the enhancment is about } From 01ca8967d5d875dcb1747875bb76149d70835452 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Mar 2019 18:27:23 +0100 Subject: [PATCH 0242/1714] fix #1463: cpu_distribution.py script is broken --- HISTORY.rst | 1 + scripts/cpu_distribution.py | 42 ++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 217effbd9a..a5a155d3a7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,7 @@ - 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by Benjamin Drung) +- 1463_: cpu_distribution.py script was broken. 5.6.1 ===== diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index a9f76b4e37..67f25b2d9f 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -59,17 +59,44 @@ def clean_screen(): os.system('cls') +def get_terminal_size(fallback=(80, 24)): + try: + # Added in Python 3.3 + from shutil import get_terminal_size as gts + return gts(fallback=fallback) + except ImportError: + try: + # This should work on Linux. + import fcntl + import termios + import struct + res = struct.unpack( + 'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234')) + return (res[1], res[0]) + except Exception: + return fallback + + def main(): - total = psutil.cpu_count() + num_cpus = psutil.cpu_count() + if num_cpus > 8: + num_cpus = 8 # try to fit into screen + cpus_hidden = True + else: + cpus_hidden = False + while True: # header clean_screen() cpus_percent = psutil.cpu_percent(percpu=True) - for i in range(total): + for i in range(num_cpus): print("CPU %-6i" % i, end="") + if cpus_hidden: + print(" (+ hidden)", end="") + print() - for percent in cpus_percent: - print("%-10s" % percent, end="") + for _ in range(num_cpus): + print("%-10s" % cpus_percent.pop(0), end="") print() # processes @@ -77,16 +104,17 @@ def main(): for p in psutil.process_iter(attrs=['name', 'cpu_num']): procs[p.info['cpu_num']].append(p.info['name'][:5]) - end_marker = [[] for x in range(total)] + curr_line = 3 while True: - for num in range(total): + for num in range(num_cpus): try: pname = procs[num].pop() except IndexError: pname = "" print("%-10s" % pname[:10], end="") print() - if procs.values() == end_marker: + curr_line += 1 + if curr_line >= get_terminal_size()[1]: break time.sleep(1) From 7b70d288648aa1117c2f7043092269de35105bec Mon Sep 17 00:00:00 2001 From: Xiaoling Bao Date: Sun, 17 Mar 2019 10:50:08 -0700 Subject: [PATCH 0243/1714] Make uptime type consitent to fix boot time error. (#1225) --- psutil/_psutil_windows.c | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 092b5af2f0..a11d8c5c06 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -134,14 +134,12 @@ static PyObject *TimeoutAbandoned; */ static PyObject * psutil_boot_time(PyObject *self, PyObject *args) { -#if (_WIN32_WINNT >= 0x0600) // Windows Vista ULONGLONG uptime; -#else - double uptime; -#endif time_t pt; FILETIME fileTime; - long long ll; + ULONGLONG ll; + HINSTANCE hKernel32; + psutil_GetTickCount64 = NULL; GetSystemTimeAsFileTime(&fileTime); /* @@ -157,33 +155,26 @@ psutil_boot_time(PyObject *self, PyObject *args) { and 01-01-1601, from time_t the divide by 1e+7 to get to the same base granularity. */ -#if (_WIN32_WINNT >= 0x0600) // Windows Vista ll = (((ULONGLONG) -#else - ll = (((LONGLONG) -#endif (fileTime.dwHighDateTime)) << 32) + fileTime.dwLowDateTime; pt = (time_t)((ll - 116444736000000000ull) / 10000000ull); - // GetTickCount64() is Windows Vista+ only. Dinamically load - // it at runtime. We may have used + // GetTickCount64() is Windows Vista+ only. Dynamically load + // GetTickCount64() at runtime. We may have used // "#if (_WIN32_WINNT >= 0x0600)" pre-processor but that way // the produced exe/wheels cannot be used on Windows XP, see: // https://github.com/giampaolo/psutil/issues/811#issuecomment-230639178 if (psutil_GetTickCount64 != NULL) { // Windows >= Vista - uptime = psutil_GetTickCount64() / (ULONGLONG)1000.00f; - return Py_BuildValue("K", pt - uptime); + uptime = psutil_GetTickCount64() / 1000ull; } else { // Windows XP. // GetTickCount() time will wrap around to zero if the // system is run continuously for 49.7 days. - psutil_debug("Windows < Vista; using GetTickCount() instead of " - "GetTickCount64()"); - uptime = GetTickCount() / (LONGLONG)1000.00f; - return Py_BuildValue("L", pt - uptime); + uptime = (ULONGLONG)GetTickCount() / 1000ull; } + return Py_BuildValue("K", pt - uptime); } From 2e0016faa875d83665bb596e0b2d851d12337fb3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Mar 2019 10:54:57 -0700 Subject: [PATCH 0244/1714] update HISTORY / CREDITS, fix some C warnings --- CREDITS | 3 +++ HISTORY.rst | 1 + psutil/_psutil_windows.c | 9 +-------- psutil/tests/test_windows.py | 3 +-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CREDITS b/CREDITS index 38cd693970..ee2682eb5a 100644 --- a/CREDITS +++ b/CREDITS @@ -592,3 +592,6 @@ N: Benjamin Drung D: make tests invariant to LANG setting W: https://github.com/bdrung I: 1462 + +N: Xiaoling Bao +I: 1223 diff --git a/HISTORY.rst b/HISTORY.rst index a5a155d3a7..f48229d4fe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,7 @@ - 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by Benjamin Drung) - 1463_: cpu_distribution.py script was broken. +- 1223_: [Windows] boot_time() may return value on Windows XP. 5.6.1 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index a11d8c5c06..9fd8d9c706 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -138,8 +138,6 @@ psutil_boot_time(PyObject *self, PyObject *args) { time_t pt; FILETIME fileTime; ULONGLONG ll; - HINSTANCE hKernel32; - psutil_GetTickCount64 = NULL; GetSystemTimeAsFileTime(&fileTime); /* @@ -151,7 +149,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { The time_t is a 32-bit value for the number of seconds since January 1, 1970. A FILETIME is a 64-bit for the number of 100-nanosecond periods since January 1, 1601. Convert by - subtracting the number of 100-nanosecond period betwee 01-01-1970 + subtracting the number of 100-nanosecond period between 01-01-1970 and 01-01-1601, from time_t the divide by 1e+7 to get to the same base granularity. */ @@ -159,11 +157,6 @@ psutil_boot_time(PyObject *self, PyObject *args) { (fileTime.dwHighDateTime)) << 32) + fileTime.dwLowDateTime; pt = (time_t)((ll - 116444736000000000ull) / 10000000ull); - // GetTickCount64() is Windows Vista+ only. Dynamically load - // GetTickCount64() at runtime. We may have used - // "#if (_WIN32_WINNT >= 0x0600)" pre-processor but that way - // the produced exe/wheels cannot be used on Windows XP, see: - // https://github.com/giampaolo/psutil/issues/811#issuecomment-230639178 if (psutil_GetTickCount64 != NULL) { // Windows >= Vista uptime = psutil_GetTickCount64() / 1000ull; diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index e0bfdbb837..a3a6b61d76 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -214,8 +214,7 @@ def test_boot_time(self): 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()) - # Wmic time is 2-3 secs lower for some reason; that's OK. - self.assertLessEqual(diff, 3) + self.assertLessEqual(diff, 1) def test_boot_time_fluctuation(self): # https://github.com/giampaolo/psutil/issues/1007 From 73337e03e8eb6f78dba791a91da34cd244172159 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Mar 2019 07:09:29 -0700 Subject: [PATCH 0245/1714] #1458: implement colors on Windows --- psutil/tests/runner.py | 56 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 3fc5e05f14..9e19d19846 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -10,25 +10,39 @@ """ 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 TOX HERE = os.path.abspath(os.path.dirname(__file__)) VERBOSITY = 1 if TOX else 2 -GREEN = 1 -RED = 2 -BROWN = 94 +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() @@ -56,23 +70,51 @@ def hilite(s, color, bold=False): 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.stream.writeln(hilite("OK", GREEN)) + self._color_print("OK", GREEN) def addError(self, test, err): TestResult.addError(self, test, err) - self.stream.writeln(hilite("ERROR", RED, bold=True)) + self._color_print("ERROR", RED, bold=True) def addFailure(self, test, err): TestResult.addFailure(self, test, err) - self.stream.writeln(hilite("FAIL", RED)) + self._color_print("FAIL", RED) def addSkip(self, test, reason): TestResult.addSkip(self, test, reason) - self.stream.writeln(hilite("skipped: %s" % reason, BROWN)) + self._color_print("skipped: %s" % reason, BROWN) def printErrorList(self, flavour, errors): flavour = hilite(flavour, RED, bold=flavour == 'ERROR') From 05338fb0e67cb087844fdc1a10e0d4bb0617aba2 Mon Sep 17 00:00:00 2001 From: ABDUL NIYAS P M Date: Wed, 27 Mar 2019 14:23:45 +0530 Subject: [PATCH 0246/1714] Typo fixed (#1469) --- psutil/_psutil_windows.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 9fd8d9c706..4dfae2d5f0 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -377,7 +377,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { /* * User and kernel times are represented as a FILETIME structure - * wich contains a 64-bit value representing the number of + * which contains a 64-bit value representing the number of * 100-nanosecond intervals since January 1, 1601 (UTC): * http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx * To convert it into a float representing the seconds that the @@ -1118,7 +1118,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { /* * User and kernel times are represented as a FILETIME structure - * wich contains a 64-bit value representing the number of + * which contains a 64-bit value representing the number of * 100-nanosecond intervals since January 1, 1601 (UTC): * http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx * To convert it into a float representing the seconds that the From 36d9201a1e1e0efc20f44d48797e1e7f1035449b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Mar 2019 10:30:52 +0100 Subject: [PATCH 0247/1714] update DEVNOTES --- docs/DEVNOTES | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 046044a307..c54fa9388f 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -165,6 +165,17 @@ REJECTED - #550: threads per core +INCONSISTENCIES +=============== + +- disk_partitions(all=False) should have been "perdisk=False" instead: + * disk_io_counters(perdisk=False) + * net_io_counters(pernic=False) + * cpu_times_percent(percpu=False) + * cpu_times(percpu=False) + * cpu_freq(percpu=False) +- PROCFS_PATH should have been set_procfs_path() + RESOURCES ========= From 9691d791d7a00c7e4055156545941f4ccfa2ee12 Mon Sep 17 00:00:00 2001 From: Cedric Lamoriniere Date: Thu, 28 Mar 2019 16:46:18 +0100 Subject: [PATCH 0248/1714] Fix corner case when /etc/mtab doesn't exist and procfs=/proc (#1470) In some Linux configurations the `/etc/mtab` does not exist but the procfs_path is equal to `/proc`. With the fix done for the issue #1307, the described configuration didn't work. This Commit introduce an additional check that verifies if the `/etc/mtab` file exists before using it, else it defaults to `/self/mounts` Signed-off-by: cedric lamoriniere --- psutil/_pslinux.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 3502f1b140..fac9e781de 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1169,13 +1169,13 @@ def disk_partitions(all=False): fstypes.add("zfs") # See: https://github.com/giampaolo/psutil/issues/1307 - if procfs_path == "/proc": - mtab_path = os.path.realpath("/etc/mtab") + if procfs_path == "/proc" and os.path.isfile('/etc/mtab'): + mounts_path = os.path.realpath("/etc/mtab") else: - mtab_path = os.path.realpath("%s/self/mounts" % procfs_path) + mounts_path = os.path.realpath("%s/self/mounts" % procfs_path) retlist = [] - partitions = cext.disk_partitions(mtab_path) + partitions = cext.disk_partitions(mounts_path) for partition in partitions: device, mountpoint, fstype, opts = partition if device == 'none': From 0ef3a9672fa87bc2ef55133a5b54c309aec521b6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Mar 2019 16:48:13 +0100 Subject: [PATCH 0249/1714] give CREDITS for #1470 --- CREDITS | 4 ++++ HISTORY.rst | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index ee2682eb5a..656c43d161 100644 --- a/CREDITS +++ b/CREDITS @@ -595,3 +595,7 @@ I: 1462 N: Xiaoling Bao I: 1223 + +N: Cedric Lamoriniere +W: https://github.com/clamoriniere +I: 1470 diff --git a/HISTORY.rst b/HISTORY.rst index f48229d4fe..c793e8edad 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,9 +13,11 @@ **Bug fixes** +- 1470_: [Linux] disk_partitions(): fix corner case when /etc/mtab doesn't + exist. (patch by Cedric Lamoriniere) +- 1463_: cpu_distribution.py script was broken. - 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by Benjamin Drung) -- 1463_: cpu_distribution.py script was broken. - 1223_: [Windows] boot_time() may return value on Windows XP. 5.6.1 From e91c041d84aa34790ebd6396dcbbc2e3391b0a73 Mon Sep 17 00:00:00 2001 From: Daniel Beer Date: Tue, 2 Apr 2019 10:28:49 -0400 Subject: [PATCH 0250/1714] Fix spurious exception when iterating processes on Solaris (#1471) --- psutil/_psutil_sunos.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 99423f7ae0..49100ec667 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -218,8 +218,10 @@ psutil_proc_name_and_args(PyObject *self, PyObject *args) { /* If we can't read process memory or can't decode the result * then return args from /proc. */ - if (!py_args) + if (!py_args) { + PyErr_Clear(); py_args = PyUnicode_DecodeFSDefault(info.pr_psargs); + } /* Both methods has been failed. */ if (!py_args) From d9e69ccfc38702630d5f514daf7ba53fca543469 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 2 Apr 2019 16:31:47 +0200 Subject: [PATCH 0251/1714] give CREDITS to Daniel Beer for #1471 --- CREDITS | 4 ++++ HISTORY.rst | 10 ++++++---- INSTALL.rst | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CREDITS b/CREDITS index 656c43d161..9b7702f9f7 100644 --- a/CREDITS +++ b/CREDITS @@ -599,3 +599,7 @@ I: 1223 N: Cedric Lamoriniere W: https://github.com/clamoriniere I: 1470 + +N: Daniel Beer +W: https://github.com/dbeer1 +I: 1471 diff --git a/HISTORY.rst b/HISTORY.rst index c793e8edad..79963855a4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,12 +13,14 @@ **Bug fixes** -- 1470_: [Linux] disk_partitions(): fix corner case when /etc/mtab doesn't - exist. (patch by Cedric Lamoriniere) -- 1463_: cpu_distribution.py script was broken. +- 1223_: [Windows] boot_time() may return value on Windows XP. - 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by +- 1463_: cpu_distribution.py script was broken. Benjamin Drung) -- 1223_: [Windows] boot_time() may return value on Windows XP. +- 1470_: [Linux] disk_partitions(): fix corner case when /etc/mtab doesn't + exist. (patch by Cedric Lamoriniere) +- 1471_: [SunOS] Process name() and cmdline() can return SystemError. (patch + by Daniel Beer) 5.6.1 ===== diff --git a/INSTALL.rst b/INSTALL.rst index f11cbaf4ab..2b1ea3292a 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -92,7 +92,7 @@ OpenBSD :: - export PKG_PATH="http://ftp.openbsd.org/pub/OpenBSD/`uname -r`/packages/`arch -s`/" + export PKG_PATH=http://ftp.eu.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ pkg_add -v python3 gcc python3 -m pip install psutil From b23fb2910c851ae5497ff865a103af595e18e3f2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 2 Apr 2019 20:37:02 +0200 Subject: [PATCH 0252/1714] #1404: fix regression not returning CPUs > 9 --- psutil/_pslinux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index fac9e781de..ecba4139a7 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -629,7 +629,8 @@ def cpu_count_physical(): """Return the number of physical cores in the system.""" # Method #1 core_ids = set() - for path in glob.glob("/sys/devices/system/cpu/cpu[0-9]/topology/core_id"): + for path in glob.glob( + "/sys/devices/system/cpu/cpu[0-9]*/topology/core_id"): with open_binary(path) as f: core_ids.add(int(f.read())) result = len(core_ids) From 9295df58c0cd9edf1ec2fed82dc5794cb1acc5e0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Apr 2019 15:34:39 +0200 Subject: [PATCH 0253/1714] ionice test refactoring --- psutil/tests/test_process.py | 98 ++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a550809ff4..ae231e3562 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -43,7 +43,6 @@ from psutil.tests import create_zombie_proc from psutil.tests import enum from psutil.tests import get_test_subprocess -from psutil.tests import get_winver from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_ENVIRON from psutil.tests import HAS_IONICE @@ -67,7 +66,6 @@ from psutil.tests import TRAVIS from psutil.tests import unittest from psutil.tests import wait_for_pid -from psutil.tests import WIN_VISTA # =================================================================== @@ -351,54 +349,58 @@ def test_io_counters(self): self.assertGreaterEqual(io2[i], 0) @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(WINDOWS and get_winver() < WIN_VISTA, 'not supported') - def test_ionice(self): - if LINUX: - from psutil import (IOPRIO_CLASS_NONE, IOPRIO_CLASS_RT, - IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE) - self.assertEqual(IOPRIO_CLASS_NONE, 0) - self.assertEqual(IOPRIO_CLASS_RT, 1) - self.assertEqual(IOPRIO_CLASS_BE, 2) - self.assertEqual(IOPRIO_CLASS_IDLE, 3) - p = psutil.Process() - try: - p.ionice(2) - ioclass, value = p.ionice() - if enum is not None: - self.assertIsInstance(ioclass, enum.IntEnum) - self.assertEqual(ioclass, 2) - self.assertEqual(value, 4) - # - p.ionice(3) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 3) - self.assertEqual(value, 0) - # - p.ionice(2, 0) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 2) - self.assertEqual(value, 0) - p.ionice(2, 7) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 2) - self.assertEqual(value, 7) - finally: - p.ionice(IOPRIO_CLASS_NONE) - else: - p = psutil.Process() - original = p.ionice() - self.assertIsInstance(original, int) - try: - value = 0 # very low - if original == value: - value = 1 # low - p.ionice(value) - self.assertEqual(p.ionice(), value) - finally: - p.ionice(original) + @unittest.skipIf(not LINUX, "linux only") + def test_ionice_linux(self): + p = psutil.Process() + self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_NONE, 0)) + + self.assertEqual(psutil.IOPRIO_CLASS_NONE, 0) + self.assertEqual(psutil.IOPRIO_CLASS_RT, 1) + self.assertEqual(psutil.IOPRIO_CLASS_BE, 2) + self.assertEqual(psutil.IOPRIO_CLASS_IDLE, 3) + if enum is not None: + self.assertIsInstance(p.ionice()[0], enum.IntEnum) + + try: + p.ionice(2) + ioclass, value = p.ionice() + if enum is not None: + self.assertIsInstance(ioclass, enum.IntEnum) + self.assertEqual(ioclass, 2) + self.assertEqual(value, 4) + # + p.ionice(3) + ioclass, value = p.ionice() + self.assertEqual(ioclass, 3) + self.assertEqual(value, 0) + # + p.ionice(2, 0) + ioclass, value = p.ionice() + self.assertEqual(ioclass, 2) + self.assertEqual(value, 0) + p.ionice(2, 7) + ioclass, value = p.ionice() + self.assertEqual(ioclass, 2) + self.assertEqual(value, 7) + finally: + p.ionice(psutil.IOPRIO_CLASS_NONE, value=0) + + @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() + original = p.ionice() + self.assertIsInstance(original, int) + try: + value = 0 # very low + if original == value: + value = 1 # low + p.ionice(value) + self.assertEqual(p.ionice(), value) + finally: + p.ionice(original) @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(WINDOWS and get_winver() < WIN_VISTA, 'not supported') def test_ionice_errs(self): sproc = get_test_subprocess() p = psutil.Process(sproc.pid) From 5ad1845d15494cadae069728e3d46dfdf64c6684 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Apr 2019 16:02:49 +0200 Subject: [PATCH 0254/1714] refactor ionice() on Linux --- psutil/_pslinux.py | 39 ++++++------------------- psutil/tests/test_process.py | 56 +++++++++++++++++------------------- 2 files changed, 36 insertions(+), 59 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ecba4139a7..7c9c669f0d 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -41,7 +41,6 @@ from ._common import usage_percent from ._compat import b from ._compat import basestring -from ._compat import long from ._compat import PY3 if sys.version_info >= (3, 4): @@ -1987,36 +1986,16 @@ 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 + if value is 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) - 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) - - return cext.proc_ioprio_set(self.pid, ioclass, value) + if value and ioclass == IOPRIO_CLASS_IDLE: + raise ValueError("IOPRIO_CLASS_IDLE accepts no value") + try: + return cext.proc_ioprio_set(self.pid, ioclass, value) + except OSError as err: + if err.errno == errno.EINVAL and value > 7: + raise ValueError("value not in 0-7 range") + raise if HAS_PRLIMIT: diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ae231e3562..d264ce6d84 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -352,38 +352,36 @@ def test_io_counters(self): @unittest.skipIf(not LINUX, "linux only") def test_ionice_linux(self): p = psutil.Process() - self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_NONE, 0)) - + self.assertEqual(p.ionice()[0], psutil.IOPRIO_CLASS_NONE) self.assertEqual(psutil.IOPRIO_CLASS_NONE, 0) - self.assertEqual(psutil.IOPRIO_CLASS_RT, 1) - self.assertEqual(psutil.IOPRIO_CLASS_BE, 2) - self.assertEqual(psutil.IOPRIO_CLASS_IDLE, 3) - if enum is not None: - self.assertIsInstance(p.ionice()[0], enum.IntEnum) - + 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: - p.ionice(2) - ioclass, value = p.ionice() - if enum is not None: - self.assertIsInstance(ioclass, enum.IntEnum) - self.assertEqual(ioclass, 2) - self.assertEqual(value, 4) - # - p.ionice(3) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 3) - self.assertEqual(value, 0) - # - p.ionice(2, 0) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 2) - self.assertEqual(value, 0) - p.ionice(2, 7) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 2) - self.assertEqual(value, 7) + # 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) finally: - p.ionice(psutil.IOPRIO_CLASS_NONE, value=0) + p.ionice(psutil.IOPRIO_CLASS_BE) @unittest.skipIf(not HAS_IONICE, "not supported") @unittest.skipIf(not WINDOWS, 'not supported on this win version') From 7b8c8f522ca8db3d8bf6fb7a8d627f904ab4dbba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Apr 2019 16:04:42 +0200 Subject: [PATCH 0255/1714] refactor ionice() on Linux --- psutil/_pslinux.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7c9c669f0d..6c58cf2c25 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1990,12 +1990,9 @@ def ionice_set(self, ioclass, value): value = 0 if value and ioclass == IOPRIO_CLASS_IDLE: raise ValueError("IOPRIO_CLASS_IDLE accepts no value") - try: - return cext.proc_ioprio_set(self.pid, ioclass, value) - except OSError as err: - if err.errno == errno.EINVAL and value > 7: - raise ValueError("value not in 0-7 range") - raise + if value < 0 or value > 7: + raise ValueError("value not in 0-7 range") + return cext.proc_ioprio_set(self.pid, ioclass, value) if HAS_PRLIMIT: From c9162ae0f76fd4ba8d0e8c80808df23a2b9c23c4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Apr 2019 09:55:03 -0700 Subject: [PATCH 0256/1714] properly check OSError.winerror --- psutil/_pswindows.py | 83 ++++++++++++++++++------------------ psutil/tests/test_windows.py | 16 +++---- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 6687770cbb..260651d1a6 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -76,10 +76,6 @@ # ===================================================================== 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]) HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") @@ -533,14 +529,14 @@ def _wrap_exceptions(self): """ try: yield - except WindowsError as err: - if err.errno in ACCESS_DENIED_ERRSET: + except OSError as err: + if is_permission_err(err): 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: + elif err.winerror in (cext.ERROR_INVALID_NAME, + cext.ERROR_SERVICE_DOES_NOT_EXIST): raise NoSuchProcess( pid=None, name=self._name, msg="service %r does not exist)" % self._name) @@ -657,20 +653,31 @@ 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 + return exc.errno in (errno.EPERM, errno.EACCES) or \ + exc.winerror == cext.ERROR_ACCESS_DENIED + + +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 @@ -744,7 +751,7 @@ def cmdline(self): try: ret = cext.proc_cmdline(self.pid, use_peb=True) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): ret = cext.proc_cmdline(self.pid, use_peb=False) else: raise @@ -772,7 +779,7 @@ 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() @@ -813,11 +820,7 @@ 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) @@ -893,7 +896,7 @@ def create_time(self): try: return cext.proc_create_time(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): return self.oneshot_info()[pinfo_map['create_time']] raise @@ -915,12 +918,11 @@ def cpu_times(self): try: user, system = cext.proc_cpu_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.oneshot_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) @@ -996,18 +998,17 @@ 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.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']], + ) return pio(*ret) @wrap_exceptions @@ -1055,7 +1056,7 @@ def num_handles(self): try: return cext.proc_num_handles(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): return self.oneshot_info()[pinfo_map['num_handles']] raise diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index a3a6b61d76..70c99b4b3a 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -664,17 +664,15 @@ def test_num_handles(self): assert fun.called def test_cmdline(self): - from psutil._pswindows import ACCESS_DENIED_ERRSET + 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: - if err.errno in ACCESS_DENIED_ERRSET: - pass - elif err.errno == errno.ESRCH: - pass # NSP - else: + err = convert_oserror(err) + if not isinstance(err, (psutil.AccessDenied, + psutil.NoSuchProcess)): raise else: self.assertEqual(a, b) @@ -837,7 +835,8 @@ def test_win_service_get(self): # test NoSuchProcess service = psutil.win_service_get(name) exc = WindowsError( - psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST, "") + 0, "", 0, + psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST) with mock.patch("psutil._psplatform.cext.winservice_query_status", side_effect=exc): self.assertRaises(psutil.NoSuchProcess, service.status) @@ -847,7 +846,8 @@ def test_win_service_get(self): # test AccessDenied exc = WindowsError( - psutil._psplatform.cext.ERROR_ACCESS_DENIED, "") + 0, "", 0, + psutil._psplatform.cext.ERROR_ACCESS_DENIED) with mock.patch("psutil._psplatform.cext.winservice_query_status", side_effect=exc): self.assertRaises(psutil.AccessDenied, service.status) From f6d6fe1f959ebc93fe0284311dc1441f0a47aef5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Apr 2019 10:02:39 -0700 Subject: [PATCH 0257/1714] update HISTORY for #1475 --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 79963855a4..7b0d889c97 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,8 @@ exist. (patch by Cedric Lamoriniere) - 1471_: [SunOS] Process name() and cmdline() can return SystemError. (patch by Daniel Beer) +- 1475_: [Windows] OSError.winerror attribute wasn't properly checked resuling + in WindowsError being raised instead of AccessDenied. 5.6.1 ===== From e8a7c6da1a97921bd44793b7af9ce6d9752d24ea Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Apr 2019 10:30:49 -0700 Subject: [PATCH 0258/1714] fix error on py 2.7 where OSError doesn't always have winerror attribute --- psutil/_pswindows.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 260651d1a6..32b9f5765e 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -656,8 +656,11 @@ def as_dict(self): def is_permission_err(exc): """Return True if this is a permission error.""" assert isinstance(exc, OSError), exc + # 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). return exc.errno in (errno.EPERM, errno.EACCES) or \ - exc.winerror == cext.ERROR_ACCESS_DENIED + getattr(exc, "winerror", -1) == cext.ERROR_ACCESS_DENIED def convert_oserror(exc, pid=None, name=None): From c367b51a70819c6f7328ef2f435d8536067f1199 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Apr 2019 23:34:26 +0200 Subject: [PATCH 0259/1714] [Win] return value is not properly handled for undocumented NT* Windows APIs. (#1477) --- HISTORY.rst | 2 + psutil/_psutil_common.c | 2 +- psutil/_psutil_windows.c | 68 ++++++++++++++++++--------- psutil/arch/windows/global.c | 33 +++++++++++++ psutil/arch/windows/global.h | 4 ++ psutil/arch/windows/ntextapi.h | 4 ++ psutil/arch/windows/process_handles.c | 11 +++-- psutil/arch/windows/process_info.c | 55 ++++++++++++---------- psutil/tests/test_contracts.py | 7 ++- 9 files changed, 133 insertions(+), 53 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7b0d889c97..b5e1e6a082 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,6 +23,8 @@ by Daniel Beer) - 1475_: [Windows] OSError.winerror attribute wasn't properly checked resuling in WindowsError being raised instead of AccessDenied. +- 1477_: [Windows] wrong or absent error handling for private NTSTATUS Windows + APIs. Different process methods were affected by this. 5.6.1 ===== diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 4b6ab39968..c6e37bc22e 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -62,7 +62,7 @@ PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { char fullmsg[1024]; #ifdef _WIN32 - sprintf(fullmsg, "originated from %s", syscall); + sprintf(fullmsg, "(originated from %s)", syscall); PyErr_SetFromWindowsErrWithFilename(GetLastError(), fullmsg); #else PyObject *exc; diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 4dfae2d5f0..b1f8d65070 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3,7 +3,15 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. * - * Windows platform-specific module methods for _psutil_windows + * Windows platform-specific module methods for _psutil_windows. + * + * List of undocumented Windows NT APIs which are used in here and in + * other modules: + * - NtQuerySystemInformation + * - NtQueryInformationProcess + * - NtQueryObject + * - NtSuspendProcess + * - NtResumeProcess */ // Fixes clash between winsock2.h and windows.h @@ -797,8 +805,8 @@ psutil_GetProcWsetInformation( } else { PyErr_Clear(); - psutil_debug("NtQueryVirtualMemory failed with %i", status); - PyErr_SetString(PyExc_RuntimeError, "NtQueryVirtualMemory failed"); + psutil_SetFromNTStatusErr( + status, "NtQueryVirtualMemory(MemoryWorkingSetInformation)"); } HeapFree(GetProcessHeap(), 0, buffer); return 1; @@ -946,8 +954,11 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromWindowsErr(0); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtQuerySystemInformation(SystemProcessorPerformanceInformation)" + ); goto error; } @@ -1025,7 +1036,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { static PyObject * psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { long pid; - int ret; + NTSTATUS status; HANDLE hProcess; PyObject* suspend; @@ -1037,15 +1048,15 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { return NULL; if (PyObject_IsTrue(suspend)) - ret = psutil_NtSuspendProcess(hProcess); + status = psutil_NtSuspendProcess(hProcess); else - ret = psutil_NtResumeProcess(hProcess); + status = psutil_NtResumeProcess(hProcess); - if (ret != 0) { - PyErr_SetFromWindowsErr(0); + if (! NT_SUCCESS(status)) { CloseHandle(hProcess); - return NULL; + return psutil_SetFromNTStatusErr(status, "NtSuspend|ResumeProcess"); } + CloseHandle(hProcess); Py_RETURN_NONE; } @@ -1339,6 +1350,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { // https://msdn.microsoft.com/library/aa365928.aspx +// TODO properly handle return code static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, ULONG address_family, PVOID * data, DWORD * size) @@ -1373,6 +1385,7 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, // https://msdn.microsoft.com/library/aa365930.aspx +// TODO properly check return value static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, ULONG address_family, PVOID * data, DWORD * size) @@ -1859,20 +1872,26 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { long pid; HANDLE hProcess; DWORD IoPriority; + NTSTATUS status; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; - psutil_NtQueryInformationProcess( + + status = psutil_NtQueryInformationProcess( hProcess, ProcessIoPriority, &IoPriority, sizeof(DWORD), NULL ); + CloseHandle(hProcess); + if (! NT_SUCCESS(status)) + return psutil_SetFromNTStatusErr(status, "NtQueryInformationProcess"); return Py_BuildValue("i", IoPriority); } @@ -1885,15 +1904,17 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { long pid; DWORD prio; HANDLE hProcess; + NTSTATUS status; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; if (! PyArg_ParseTuple(args, "li", &pid, &prio)) return NULL; + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; - psutil_NtSetInformationProcess( + status = psutil_NtSetInformationProcess( hProcess, ProcessIoPriority, (PVOID)&prio, @@ -1901,6 +1922,8 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { ); CloseHandle(hProcess); + if (! NT_SUCCESS(status)) + return psutil_SetFromNTStatusErr(status, "NtSetInformationProcess"); Py_RETURN_NONE; } #endif @@ -3217,9 +3240,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { spi, ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromOSErrnoWithSyscall( - "NtQuerySystemInformation(SYSTEM_PERFORMANCE_INFORMATION)"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemPerformanceInformation)"); goto error; } @@ -3236,9 +3259,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { InterruptInformation, ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromOSErrnoWithSyscall( - "NtQuerySystemInformation(SYSTEM_INTERRUPT_INFORMATION)"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemInterruptInformation)"); goto error; } for (i = 0; i < ncpus; i++) { @@ -3258,9 +3281,10 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromOSErrnoWithSyscall( - "NtQuerySystemInformation(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtQuerySystemInformation(SystemProcessorPerformanceInformation)"); goto error; } diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index 9ef9209276..4d8526e31d 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -18,6 +18,14 @@ int PSUTIL_WINVER; SYSTEM_INFO PSUTIL_SYSTEM_INFO; +#define NT_FACILITY_MASK 0xfff +#define NT_FACILITY_SHIFT 16 +#define NT_FACILITY(Status) \ + ((((ULONG)(Status)) >> NT_FACILITY_SHIFT) & NT_FACILITY_MASK) +#define NT_NTWIN32(status) (NT_FACILITY(Status) == FACILITY_WIN32) +#define WIN32_FROM_NTSTATUS(Status) (((ULONG)(Status)) & 0xffff) + + // A wrapper around GetModuleHandle and GetProcAddress. PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { @@ -60,6 +68,26 @@ psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { } +/* + * Convert a NTSTATUS value to a Win32 error code and set the proper + * Python exception. + */ +PVOID +psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall) { + ULONG err; + char fullmsg[1024]; + + if (NT_NTWIN32(Status)) + err = WIN32_FROM_NTSTATUS(Status); + else + err = psutil_RtlNtStatusToDosErrorNoTeb(Status); + // if (GetLastError() != 0) + // err = GetLastError(); + sprintf(fullmsg, "(originated from %s)", syscall); + return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); +} + + static int psutil_loadlibs() { /* @@ -127,6 +155,11 @@ psutil_loadlibs() { if (! psutil_NtQueryVirtualMemory) return 1; + psutil_RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( + "ntdll", "RtlNtStatusToDosErrorNoTeb"); + if (! psutil_RtlNtStatusToDosErrorNoTeb) + return 1; + /* * Optional. */ diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index fb24bac9a4..10ae640595 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -23,6 +23,7 @@ extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); +PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); _NtQuerySystemInformation \ psutil_NtQuerySystemInformation; @@ -71,3 +72,6 @@ _NtResumeProcess \ _NtQueryVirtualMemory \ psutil_NtQueryVirtualMemory; + +_RtlNtStatusToDosErrorNoTeb \ + psutil_RtlNtStatusToDosErrorNoTeb; diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 178f9866f4..b6f23d9967 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -500,4 +500,8 @@ typedef NTSTATUS (NTAPI *_NtQueryVirtualMemory) ( PSIZE_T ReturnLength ); +typedef ULONG (WINAPI *_RtlNtStatusToDosErrorNoTeb) ( + NTSTATUS status +); + #endif // __NTEXTAPI_H__ diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 8b899972ed..5966669e28 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -52,6 +52,7 @@ psutil_wait_thread(LPVOID lpvParam) { while (TRUE) { WaitForSingleObject(g_hEvtStart, INFINITE); + // TODO: return code not checked g_status = psutil_NtQueryObject( g_hFile, ObjectNameInformation, @@ -159,8 +160,9 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { &dwRet)) == STATUS_INFO_LENGTH_MISMATCH); // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH - if (!NT_SUCCESS(status)) { - PyErr_SetFromWindowsErr(HRESULT_FROM_NT(status)); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemExtendedHandleInformation)"); error = TRUE; goto cleanup; } @@ -355,8 +357,9 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { &dwRet)) == STATUS_INFO_LENGTH_MISMATCH); // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH - if (!NT_SUCCESS(status)) { - PyErr_SetFromWindowsErr(HRESULT_FROM_NT(status)); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemExtendedHandleInformation)"); error = TRUE; goto cleanup; } diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 946a01cbca..3b3c677edc 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -483,6 +483,7 @@ psutil_get_process_data(long pid, BOOL theyAreWow64; #endif DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; + NTSTATUS status; hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) @@ -491,15 +492,16 @@ psutil_get_process_data(long pid, #ifdef _WIN64 /* 64 bit case. Check if the target is a 32 bit process running in WoW64 * mode. */ - if (! NT_SUCCESS(psutil_NtQueryInformationProcess( - hProcess, - ProcessWow64Information, - &ppeb32, - sizeof(LPVOID), - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall( - "NtQueryInformationProcess(ProcessWow64Information)"); + status = psutil_NtQueryInformationProcess( + hProcess, + ProcessWow64Information, + &ppeb32, + sizeof(LPVOID), + NULL); + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessWow64Information)"); goto error; } @@ -633,18 +635,20 @@ psutil_get_process_data(long pid, PEB_ peb; RTL_USER_PROCESS_PARAMETERS_ procParameters; - if (! NT_SUCCESS(psutil_NtQueryInformationProcess( - hProcess, - ProcessBasicInformation, - &pbi, - sizeof(pbi), - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall( - "NtQueryInformationProcess(ProcessBasicInformation)"); + status = psutil_NtQueryInformationProcess( + hProcess, + ProcessBasicInformation, + &pbi, + sizeof(pbi), + NULL); + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessBasicInformation)"); goto error; } + // read peb if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, @@ -767,10 +771,12 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { NULL, 0, &bufLen); + if (status != STATUS_BUFFER_OVERFLOW && \ status != STATUS_BUFFER_TOO_SMALL && \ status != STATUS_INFO_LENGTH_MISMATCH) { - PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess(0)"); + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessBasicInformation)"); goto error; } @@ -789,8 +795,9 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { bufLen, &bufLen ); - if (! NT_SUCCESS(status)) { - PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess(withlen)"); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessCommandLineInformation)"); goto error; } @@ -971,9 +978,9 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, } } - if (status != 0) { - PyErr_Format( - PyExc_RuntimeError, "NtQuerySystemInformation() syscall failed"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemProcessInformation)"); goto error; } diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 08e9e9b824..adf7b680a6 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -614,8 +614,11 @@ def memory_maps(self, ret, proc): # commented as on Linux we might get # '/foo/bar (deleted)' # assert os.path.exists(nt.path), nt.path - elif fname in ('addr', 'perms'): - assert value + 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) From a5360cc01f1418571d15d320431ade025265ca58 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 00:56:36 +0200 Subject: [PATCH 0260/1714] fix #1478: add make command to re-run tests failed on last run --- HISTORY.rst | 1 + Makefile | 5 +++++ psutil/tests/runner.py | 27 +++++++++++++++++++++++++-- scripts/internal/winmake.py | 10 ++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b5e1e6a082..b01d8fea39 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,7 @@ the number of physical CPUs in case /proc/cpuinfo does not provide this info. - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. - 1464_: various docfixes (always point to python3 doc, fix links, etc.). +- 1478_: add make command to re-run tests failed on last run. **Bug fixes** diff --git a/Makefile b/Makefile index 7d838e876c..e91ae34244 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ clean: ## Remove all build files. *.egg-info \ *\$testfn* \ .coverage \ + .failed-tests.txt \ .tox \ build/ \ dist/ \ @@ -151,6 +152,10 @@ test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSyste ${MAKE} install @$(TEST_PREFIX) $(PYTHON) -m unittest -v $(ARGS) +test-failed: ## Re-run tests which failed on last run + ${MAKE} install + $(TEST_PREFIX) $(PYTHON) -c "import psutil.tests.runner as r; r.run(last_failed=True)" + test-coverage: ## Run test coverage. ${MAKE} install # Note: coverage options are controlled by .coveragerc file diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 9e19d19846..7d3a68b92a 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -24,11 +24,13 @@ 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 @@ -157,15 +159,36 @@ def get_suite(name=None): return suite -def run(name=None): +def get_suite_from_failed(): + # ...from previously failed test run + with open(FAILED_TESTS_FNAME, 'rt') as f: + names = f.read().split() + suite = unittest.TestSuite() + 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(get_suite(name)) + 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) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index cbdeebdc9c..75b4c348ae 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -318,6 +318,7 @@ def clean(): "*.~", "*__pycache__", ".coverage", + ".failed-tests.txt", ".tox", ) safe_rmtree("build") @@ -439,6 +440,15 @@ def test_by_name(): sh("%s -m unittest -v %s" % (PYTHON, name)) +@cmd +def test_failed(): + """Re-run tests which failed on last run.""" + install() + test_setup() + sh('%s -c "import psutil.tests.runner as r; r.run(last_failed=True)"' % ( + PYTHON)) + + @cmd def test_script(): """Quick way to test a script""" From df45572111dbce4a576d23648ef8b5b88e1df899 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 00:57:52 +0200 Subject: [PATCH 0261/1714] don't fail if there are not prev failed tests --- psutil/tests/runner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 7d3a68b92a..1a28aa4352 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -161,9 +161,11 @@ def get_suite(name=None): 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() - suite = unittest.TestSuite() for n in names: suite.addTest(unittest.defaultTestLoader.loadTestsFromName(n)) return suite From e471e7cbad9e2d84f9fb114da86df78755836852 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 01:33:35 +0200 Subject: [PATCH 0262/1714] [Win] Process IO priority constants + high priority (#1479 / #1476) --- HISTORY.rst | 3 ++ docs/index.rst | 26 +++++++++++----- psutil/__init__.py | 4 +++ psutil/_psutil_windows.c | 3 +- psutil/_pswindows.py | 50 ++++++++++++++++++++++--------- psutil/tests/test_process.py | 57 ++++++++++++++++++++---------------- 6 files changed, 95 insertions(+), 48 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b01d8fea39..1c745b8bb8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,9 @@ the number of physical CPUs in case /proc/cpuinfo does not provide this info. - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. - 1464_: various docfixes (always point to python3 doc, fix links, etc.). +- 1473_: [Windows] process IO priority (ionice()) values are now exposed as 4 + new constants: IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH. + Also it was not possible to set high I/O priority (not it is). - 1478_: add make command to re-run tests failed on last run. **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 6ff1e22c5a..94713f5402 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1245,12 +1245,15 @@ Process class pionice(ioclass=, value=0) >>> - On Windows only *ioclass* is used and it can be set to ``2`` (normal), - ``1`` (low) or ``0`` (very low). Also it returns an integer instead of a - named tuple. + On Windows only *ioclass* is used and it can be set to ``3`` (high), + ``2`` (normal), ``1`` (low) or ``0`` (very low). + Also it returns an integer instead of a named tuple. Availability: Linux and Windows > Vista + .. versionchanged:: + Windows accepts ``3`` (high) value. + .. versionchanged:: 3.0.0 on Python >= 3.4 the returned ``ioclass`` constant is an `enum `__ @@ -2157,10 +2160,19 @@ Constants Availability: Linux - .. versionchanged:: - 3.0.0 on Python >= 3.4 these constants are - `enums `__ - instead of a plain integer. +.. _const-ioprio: +.. data:: IOPRIO_VERYLOW +.. data:: IOPRIO_LOW +.. data:: IOPRIO_NORMAL +.. data:: IOPRIO_HIGH + + A set of integers representing the I/O priority of a process on Linux. + They can be used in conjunction with :meth:`psutil.Process.ionice()` to get + or set process I/O priority. + + Availability: Windows + + .. versionadded:: 5.6.2 .. _const-rlimit: .. data:: RLIM_INFINITY diff --git a/psutil/__init__.py b/psutil/__init__.py index ab2ed3490a..2f33436cec 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -146,6 +146,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_VERYLOW # NOQA + from ._pswindows import IOPRIO_LOW # NOQA + from ._pswindows import IOPRIO_NORMAL # NOQA + from ._pswindows import IOPRIO_HIGH # NOQA elif MACOS: from . import _psosx as _psplatform diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index b1f8d65070..a1a6885729 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3685,7 +3685,8 @@ void init_psutil_windows(void) module, "ERROR_INVALID_NAME", ERROR_INVALID_NAME); PyModule_AddIntConstant( module, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST); - + PyModule_AddIntConstant( + module, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD); PyModule_AddIntConstant( module, "WINVER", PSUTIL_WINVER); PyModule_AddIntConstant( diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 260651d1a6..929e27d7d4 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -63,11 +63,14 @@ # http://msdn.microsoft.com/en-us/library/ms686219(v=vs.85).aspx __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", ] @@ -112,6 +115,19 @@ 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, @@ -656,8 +672,12 @@ def as_dict(self): def is_permission_err(exc): """Return True if this is a permission error.""" assert isinstance(exc, OSError), exc + # 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). return exc.errno in (errno.EPERM, errno.EACCES) or \ - exc.winerror == cext.ERROR_ACCESS_DENIED + getattr(exc, "winerror", -1) in (cext.ERROR_ACCESS_DENIED, + cext.ERROR_PRIVILEGE_NOT_HELD) def convert_oserror(exc, pid=None, name=None): @@ -981,17 +1001,19 @@ def nice_set(self, value): if HAS_PROC_IO_PRIORITY: @wrap_exceptions def ionice_get(self): - return cext.proc_io_priority_get(self.pid) + ret = cext.proc_io_priority_get(self.pid) + if enum is not None: + ret = IOPriority(ret) + return ret @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) + def ionice_set(self, ioclass, value): + if value: + raise TypeError("value argument not accepted on Windows") + 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): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index d264ce6d84..753bf612e2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -380,29 +380,7 @@ def test_ionice_linux(self): (psutil.IOPRIO_CLASS_RT, 7)) with self.assertRaises(ValueError): p.ionice(psutil.IOPRIO_CLASS_IDLE, value=8) - 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() - original = p.ionice() - self.assertIsInstance(original, int) - try: - value = 0 # very low - if original == value: - value = 1 # low - p.ionice(value) - self.assertEqual(p.ionice(), value) - finally: - p.ionice(original) - - @unittest.skipIf(not HAS_IONICE, "not supported") - def test_ionice_errs(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - if LINUX: + # errs self.assertRaises(ValueError, p.ionice, 2, 10) self.assertRaises(ValueError, p.ionice, 2, -1) self.assertRaises(ValueError, p.ionice, 4) @@ -416,9 +394,36 @@ def test_ionice_errs(self): self.assertRaisesRegex( ValueError, "'ioclass' argument must be specified", p.ionice, value=1) - else: - self.assertRaises(ValueError, p.ionice, 3) - self.assertRaises(TypeError, p.ionice, 2, 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): From 9d2f9bf98f1d8d3a23228931ef13792c2d3bc007 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 12:20:52 +0200 Subject: [PATCH 0263/1714] update doc --- docs/conf.py | 2 +- docs/index.rst | 141 ++++++++++++++++++++++++++++++------------------- 2 files changed, 87 insertions(+), 56 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index df825cbd55..b056d20fee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -110,7 +110,7 @@ def get_version(): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'DEVGUIDE.rst'] # The reST default role (used for this markup: `text`) to use for all # documents. diff --git a/docs/index.rst b/docs/index.rst index 94713f5402..45fd375879 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1230,34 +1230,49 @@ Process class .. method:: ionice(ioclass=None, value=None) - Get or set process I/O niceness (priority). On Linux *ioclass* is one of the - :data:`psutil.IOPRIO_CLASS_*` constants. - *value* is a number which goes from ``0`` to ``7``. The higher the value, - the lower the I/O priority of the process. On Windows only *ioclass* is - used and it can be set to ``2`` (normal), ``1`` (low) or ``0`` (very low). - The example below sets IDLE priority class for the current process, - meaning it will only get I/O time when no other process needs the disk: - - >>> import psutil - >>> p = psutil.Process() - >>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # set - >>> p.ionice() # get - pionice(ioclass=, value=0) - >>> - - On Windows only *ioclass* is used and it can be set to ``3`` (high), - ``2`` (normal), ``1`` (low) or ``0`` (very low). - Also it returns an integer instead of a named tuple. - - Availability: Linux and Windows > Vista - - .. versionchanged:: - Windows accepts ``3`` (high) value. - - .. versionchanged:: - 3.0.0 on Python >= 3.4 the returned ``ioclass`` constant is an - `enum `__ - instead of a plain integer. + Get or set process I/O niceness (priority). + If no argument is provided it acts as a get, returning a ``(ioclass, value)`` + tuple on Linux and a *ioclass* integer on Windows. + If *ioclass* is provided it acts as a set. In this case an additional + *value* can be specified on Linux only in order to increase or decrease the + I/O priority even further. + Here's the possible platform-dependent *ioclass* values. + + Linux (see `ioprio_get`_ manual): + + * ``IOPRIO_CLASS_RT``: (high) the process gets first access to the disk + every time. Use it with care as it can starve the entire + system. Additional priority *level* can be specified and ranges from + ``0`` (highest) to ``7`` (lowest). + * ``IOPRIO_CLASS_BE``: (normal) the default for any process that hasn't set + a specific I/O priority. Additional priority *level* ranges from + ``0`` (highest) to ``7`` (lowest). + * ``IOPRIO_CLASS_IDLE``: (low) get I/O time when no-one else needs the disk. + No additional *value* is accepted. + * ``IOPRIO_CLASS_NONE``: returned when no priority was previously set. + + Windows: + + * ``IOPRIO_HIGH``: highest priority. + * ``IOPRIO_NORMAL``: default priority. + * ``IOPRIO_LOW``: low priority. + * ``IOPRIO_VERYLOW``: lowest priority. + + Here's an example on how to set the highest I/O priority depending on what + platform you're on:: + + import psutil + p = psutil.Process() + if psutil.LINUX + p.ionice(psutil.IOPRIO_CLASS_RT, level=7) + else: # Windows + p.ionice(psutil.IOPRIO_HIGH) + p.ionice() # get + + Availability: Linux, Windows Vista+ + + .. versionchanged:: 5.6.2 Windows accepts mew ``IOPRIO_*`` constants + including new ``IOPRIO_HIGH``. .. method:: rlimit(resource, limits=None) @@ -2034,6 +2049,9 @@ Example code: Constants ========= +Operating system constants +-------------------------- + .. _const-oses: .. data:: POSIX .. data:: WINDOWS @@ -2054,7 +2072,7 @@ Constants .. data:: OSX - Alias for :const:`MACOS` (deprecated). + Alias for :const:`MACOS`. .. warning:: deprecated in version 5.4.7; use :const:`MACOS` instead. @@ -2078,6 +2096,9 @@ Constants .. versionchanged:: 3.4.2 also available on Solaris. .. versionchanged:: 5.4.0 also available on AIX. +Process status constants +------------------------ + .. _const-pstatus: .. data:: STATUS_RUNNING .. data:: STATUS_SLEEPING @@ -2094,39 +2115,21 @@ Constants .. data:: STATUS_WAITING (FreeBSD) .. data:: STATUS_SUSPENDED (NetBSD) - A set of strings representing the status of a process. Returned by :meth:`psutil.Process.status()`. .. versionadded:: 3.4.1 STATUS_SUSPENDED (NetBSD) .. versionadded:: 5.4.7 STATUS_PARKED (Linux) -.. _const-conn: -.. data:: CONN_ESTABLISHED -.. data:: CONN_SYN_SENT -.. data:: CONN_SYN_RECV -.. data:: CONN_FIN_WAIT1 -.. data:: CONN_FIN_WAIT2 -.. data:: CONN_TIME_WAIT -.. data:: CONN_CLOSE -.. data:: CONN_CLOSE_WAIT -.. data:: CONN_LAST_ACK -.. data:: CONN_LISTEN -.. data:: CONN_CLOSING -.. data:: CONN_NONE -.. data:: CONN_DELETE_TCB (Windows) -.. data:: CONN_IDLE (Solaris) -.. data:: CONN_BOUND (Solaris) - - A set of strings representing the status of a TCP connection. - Returned by :meth:`psutil.Process.connections()` (`status` field). +Process priority constants +-------------------------- .. _const-prio: -.. data:: ABOVE_NORMAL_PRIORITY_CLASS -.. data:: BELOW_NORMAL_PRIORITY_CLASS +.. data:: REALTIME_PRIORITY_CLASS .. data:: HIGH_PRIORITY_CLASS -.. data:: IDLE_PRIORITY_CLASS +.. data:: ABOVE_NORMAL_PRIORITY_CLASS .. data:: NORMAL_PRIORITY_CLASS -.. data:: REALTIME_PRIORITY_CLASS +.. data:: IDLE_PRIORITY_CLASS +.. data:: BELOW_NORMAL_PRIORITY_CLASS A set of integers representing the priority of a process on Windows (see `SetPriorityClass`_). They can be used in conjunction with @@ -2160,7 +2163,6 @@ Constants Availability: Linux -.. _const-ioprio: .. data:: IOPRIO_VERYLOW .. data:: IOPRIO_LOW .. data:: IOPRIO_NORMAL @@ -2174,7 +2176,9 @@ Constants .. versionadded:: 5.6.2 -.. _const-rlimit: +Process resources constants +--------------------------- + .. data:: RLIM_INFINITY .. data:: RLIMIT_AS .. data:: RLIMIT_CORE @@ -2199,6 +2203,33 @@ Constants Availability: Linux +Connections constants +--------------------- + +.. _const-conn: +.. data:: CONN_ESTABLISHED +.. data:: CONN_SYN_SENT +.. data:: CONN_SYN_RECV +.. data:: CONN_FIN_WAIT1 +.. data:: CONN_FIN_WAIT2 +.. data:: CONN_TIME_WAIT +.. data:: CONN_CLOSE +.. data:: CONN_CLOSE_WAIT +.. data:: CONN_LAST_ACK +.. data:: CONN_LISTEN +.. data:: CONN_CLOSING +.. data:: CONN_NONE +.. data:: CONN_DELETE_TCB (Windows) +.. data:: CONN_IDLE (Solaris) +.. data:: CONN_BOUND (Solaris) + + A set of strings representing the status of a TCP connection. + Returned by :meth:`psutil.Process.connections()` and + :func:`psutil.net_connections` (`status` field). + +Hardware constants +------------------ + .. _const-aflink: .. data:: AF_LINK From 511ddb9142df4ada9a3fce6de5c05bd99fac0393 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 12:37:53 +0200 Subject: [PATCH 0264/1714] update doc --- docs/index.rst | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 45fd375879..85429eb6ab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -500,7 +500,7 @@ Network When the remote endpoint is not connected you'll get an empty tuple (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. - **status**: represents the status of a TCP connection. The return value - is one of the :data:`psutil.CONN_* ` constants + is one of the `psutil.CONN_* <#connections-constants>`_ constants (a string). For UDP and UNIX sockets this is always going to be :const:`psutil.CONN_NONE`. @@ -1173,7 +1173,7 @@ Process class .. method:: status() The current process status as a string. The returned string is one of the - :data:`psutil.STATUS_*` constants. + `psutil.STATUS_* <#process-status-constants>`_ constants. .. method:: cwd() @@ -1277,7 +1277,7 @@ Process class .. method:: rlimit(resource, limits=None) Get or set process resource limits (see `man prlimit`_). *resource* is one - of the :data:`psutil.RLIMIT_* ` constants. + of the `psutil.RLIMIT_* <#process-resources-constants>`_ constants. *limits* is a ``(soft, hard)`` tuple. This is the same as `resource.getrlimit`_ and `resource.setrlimit`_ but can be used for any process PID, not only `os.getpid`_. @@ -2054,8 +2054,8 @@ Operating system constants .. _const-oses: .. data:: POSIX -.. data:: WINDOWS .. data:: LINUX +.. data:: WINDOWS .. data:: MACOS .. data:: FREEBSD .. data:: NETBSD @@ -2115,10 +2115,10 @@ Process status constants .. data:: STATUS_WAITING (FreeBSD) .. data:: STATUS_SUSPENDED (NetBSD) - Returned by :meth:`psutil.Process.status()`. + Represent a process status. Returned by :meth:`psutil.Process.status()`. - .. versionadded:: 3.4.1 STATUS_SUSPENDED (NetBSD) - .. versionadded:: 5.4.7 STATUS_PARKED (Linux) + .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) + .. versionadded:: 5.4.7 ``STATUS_PARKED`` (Linux) Process priority constants -------------------------- @@ -2131,17 +2131,12 @@ Process priority constants .. data:: IDLE_PRIORITY_CLASS .. data:: BELOW_NORMAL_PRIORITY_CLASS - A set of integers representing the priority of a process on Windows (see - `SetPriorityClass`_). They can be used in conjunction with - :meth:`psutil.Process.nice()` to get or set process priority. + Represent the priority of a process on Windows (see `SetPriorityClass`_). + They can be used in conjunction with :meth:`psutil.Process.nice()` to get or + set process priority. Availability: Windows - .. versionchanged:: - 3.0.0 on Python >= 3.4 these constants are - `enums `__ - instead of a plain integer. - .. _const-ioprio: .. data:: IOPRIO_CLASS_NONE .. data:: IOPRIO_CLASS_RT From 0056ffe061639184faf72432e5aa27b0915de65b Mon Sep 17 00:00:00 2001 From: Samer Masterson Date: Fri, 5 Apr 2019 03:43:00 -0700 Subject: [PATCH 0265/1714] Fix read access violation in psutil.cpu_count(logical=False) (#1480) Fix read access violation in psutil.cpu_count(logical=False) by stopping the iteration through the SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX buffer when we hit the last item. --- psutil/_psutil_windows.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index a1a6885729..53617c5803 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -495,6 +495,7 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { DWORD length = 0; DWORD offset = 0; DWORD ncpus = 0; + DWORD prev_processor_info_size = 0; // GetLogicalProcessorInformationEx() is available from Windows 7 // onward. Differently from GetLogicalProcessorInformation() @@ -533,13 +534,20 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { } ptr = buffer; - while (ptr->Size > 0 && offset + ptr->Size <= length) { + while (offset < length) { + // Advance ptr by the size of the previous + // SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX struct. + ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)\ + (((char*)ptr) + prev_processor_info_size); + if (ptr->Relationship == RelationProcessorCore) { ncpus += 1; } + + // When offset == length, we've reached the last processor + // info struct in the buffer. offset += ptr->Size; - ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)\ - (((char*)ptr) + ptr->Size); + prev_processor_info_size = ptr->Size; } free(buffer); From e12bec15829441ced304102d66f3b0db0fb17c72 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 12:43:23 +0200 Subject: [PATCH 0266/1714] remove outdated tests --- psutil/tests/test_process.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 753bf612e2..25c70a259a 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -381,10 +381,6 @@ def test_ionice_linux(self): with self.assertRaises(ValueError): p.ionice(psutil.IOPRIO_CLASS_IDLE, value=8) # errs - self.assertRaises(ValueError, p.ionice, 2, 10) - self.assertRaises(ValueError, p.ionice, 2, -1) - self.assertRaises(ValueError, p.ionice, 4) - self.assertRaises(TypeError, p.ionice, 2, "foo") self.assertRaisesRegex( ValueError, "can't specify value with IOPRIO_CLASS_NONE", p.ionice, psutil.IOPRIO_CLASS_NONE, 1) From d4cbce0b3aa7e72deb7409a25850cdd5c3c4d323 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 12:44:48 +0200 Subject: [PATCH 0267/1714] give CREDITS for #1480 --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index 9b7702f9f7..33c62ea6d5 100644 --- a/CREDITS +++ b/CREDITS @@ -603,3 +603,7 @@ I: 1470 N: Daniel Beer W: https://github.com/dbeer1 I: 1471 + +N: Samer Masterson +W: https://github.com/samertm +I: 1480 diff --git a/HISTORY.rst b/HISTORY.rst index 1c745b8bb8..65629f8420 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,8 @@ in WindowsError being raised instead of AccessDenied. - 1477_: [Windows] wrong or absent error handling for private NTSTATUS Windows APIs. Different process methods were affected by this. +- 1480_: [Windows] psutil.cpu_count(logical=False) could cause a crash due to + fixed read violation. (patch by Samer Masterson) 5.6.1 ===== From 275df44b5798ddf09485d7ad503a212636ddf340 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 13:00:06 +0200 Subject: [PATCH 0268/1714] fix #1474: fix formatting of psutil.tests() which mimicks 'ps aux' output --- HISTORY.rst | 1 + psutil/__init__.py | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 65629f8420..5741e2478e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,6 +25,7 @@ exist. (patch by Cedric Lamoriniere) - 1471_: [SunOS] Process name() and cmdline() can return SystemError. (patch by Daniel Beer) +- 1474_: fix formatting of psutil.tests() which mimicks 'ps aux' output. - 1475_: [Windows] OSError.winerror attribute wasn't properly checked resuling in WindowsError being raised instead of AccessDenied. - 1477_: [Windows] wrong or absent error handling for private NTSTATUS Windows diff --git a/psutil/__init__.py b/psutil/__init__.py index 2f33436cec..a217a9e708 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2441,8 +2441,25 @@ def test(): # pragma: no cover """List info of all currently running processes emulating ps aux output. """ + def bytes2human(n): + """ + >>> bytes2human(10000) + '9.8 K' + >>> bytes2human(100001221) + '95.4 M' + """ + symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + prefix = {} + for i, s in enumerate(symbols): + prefix[s] = 1 << (i + 1) * 10 + for s in reversed(symbols): + if n >= prefix[s]: + value = float(n) / prefix[s] + return '%.1f%s' % (value, s) + return '%.1fB' % (n) + today_day = datetime.date.today() - templ = "%-10s %5s %4s %7s %7s %-13s %5s %7s %s" + templ = "%-10s %6s %5s %8s %8s %12s %5s %7s %s" attrs = ['pid', 'memory_percent', 'name', 'cpu_times', 'create_time', 'memory_info'] if POSIX: @@ -2450,7 +2467,7 @@ def test(): # pragma: no cover attrs.append('terminal') print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "TTY", "START", "TIME", "COMMAND")) - for p in process_iter(attrs=attrs, ad_value=''): + for p in process_iter(attrs=attrs, ad_value=None): if p.info['create_time']: ctime = datetime.datetime.fromtimestamp(p.info['create_time']) if ctime.date() == today_day: @@ -2462,24 +2479,26 @@ def test(): # pragma: no cover cputime = time.strftime("%M:%S", time.localtime(sum(p.info['cpu_times']))) try: - user = p.username() + user = p.username()[:9] except Error: user = '' if 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 '?' + + 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']) if \ + p.info['memory_percent'] is not None else '' + print(templ % ( user[:10], p.info['pid'], memp, vms, rss, - p.info.get('terminal', '') or '?', + p.info.get('terminal', '') or '', ctime, cputime, p.info['name'].strip() or '?')) From 3f67688df6c47eec3427c001ef1f11b14e6fcaf2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 16:26:07 +0200 Subject: [PATCH 0269/1714] fix linux tests --- psutil/__init__.py | 2 +- psutil/_pslinux.py | 4 ++-- psutil/tests/test_process.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index a217a9e708..90066f399a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2489,7 +2489,7 @@ def bytes2human(n): 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']) if \ + memp = round(p.info['memory_percent'], 1) if \ p.info['memory_percent'] is not None else '' print(templ % ( diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 6c58cf2c25..bf815e87a2 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1988,8 +1988,8 @@ def ionice_get(self): def ionice_set(self, ioclass, value): if value is None: value = 0 - if value and ioclass == IOPRIO_CLASS_IDLE: - raise ValueError("IOPRIO_CLASS_IDLE accepts no value") + 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: raise ValueError("value not in 0-7 range") return cext.proc_ioprio_set(self.pid, ioclass, value) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 25c70a259a..a4b738eeb2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -382,10 +382,10 @@ def test_ionice_linux(self): p.ionice(psutil.IOPRIO_CLASS_IDLE, value=8) # errs self.assertRaisesRegex( - ValueError, "can't specify value with IOPRIO_CLASS_NONE", + ValueError, "ioclass accepts no value", p.ionice, psutil.IOPRIO_CLASS_NONE, 1) self.assertRaisesRegex( - ValueError, "can't specify value with IOPRIO_CLASS_IDLE", + ValueError, "ioclass accepts no value", p.ionice, psutil.IOPRIO_CLASS_IDLE, 1) self.assertRaisesRegex( ValueError, "'ioclass' argument must be specified", From 2eb7b3bc2982f94afee4f4cbd93764d9708e206d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 07:52:39 -0700 Subject: [PATCH 0270/1714] fix windows failure re. py 2 vs. 3 --- psutil/tests/test_windows.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 70c99b4b3a..5a998dd4ff 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -25,6 +25,7 @@ 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 @@ -826,17 +827,22 @@ def test_win_service_iter(self): self.assertEqual(serv, s) def test_win_service_get(self): - name = next(psutil.win_service_iter()).name() + 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) - exc = WindowsError( - 0, "", 0, - psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST) + 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) @@ -845,9 +851,11 @@ def test_win_service_get(self): self.assertRaises(psutil.NoSuchProcess, service.username) # test AccessDenied - exc = WindowsError( - 0, "", 0, - psutil._psplatform.cext.ERROR_ACCESS_DENIED) + 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) From 90135fa7fa63a0eadd318374d455c04565ce1d8e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 17:44:45 +0200 Subject: [PATCH 0271/1714] move bytes2human() into psutil._common and reused it from scripts dir --- psutil/__init__.py | 7 ++- psutil/_common.py | 21 ++++++++ psutil/tests/test_memory_leaks.py | 20 +------ scripts/disk_usage.py | 18 +------ scripts/ifconfig.py | 19 +------ scripts/internal/download_exes.py | 19 +------ scripts/iotop.py | 21 +------- scripts/meminfo.py | 18 +------ scripts/nettop.py | 21 +------- scripts/procinfo.py | 25 +++------ scripts/ps.py | 90 +++++++++++++++---------------- scripts/top.py | 62 ++++++++------------- 12 files changed, 106 insertions(+), 235 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 90066f399a..3726079e5b 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2476,8 +2476,11 @@ def bytes2human(n): ctime = ctime.strftime("%b%d") else: ctime = '' - cputime = time.strftime("%M:%S", - time.localtime(sum(p.info['cpu_times']))) + if p.info['cpu_times'] is not None: + cputime = time.strftime("%M:%S", + time.localtime(sum(p.info['cpu_times']))) + else: + cputime = '' try: user = p.username()[:9] except Error: diff --git a/psutil/_common.py b/psutil/_common.py index 6d43038898..0b4f5308c0 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -64,6 +64,7 @@ '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", + 'bytes2human', ] @@ -598,3 +599,23 @@ def open_text(fname, **kwargs): kwargs.setdefault('encoding', ENCODING) kwargs.setdefault('errors', ENCODING_ERRS) return open(fname, "rt", **kwargs) + + +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 n >= prefix[symbol]: + value = float(n) / prefix[symbol] + return format % locals() + return format % dict(symbol=symbols[0], value=n) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index ed3a77fe71..170a1d0cd7 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -30,6 +30,7 @@ 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 @@ -73,25 +74,6 @@ def skip_if_linux(): "worthless on LINUX (pure python)") -def bytes2human(n): - """ - http://code.activestate.com/recipes/578019 - >>> bytes2human(10000) - '9.8K' - >>> bytes2human(100001221) - '95.4M' - """ - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.2f%s' % (value, s) - return "%sB" % n - - class TestMemLeak(unittest.TestCase): """Base framework class which calls a function many times and produces a failure if process memory usage keeps increasing diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 37f4da0c96..1860401fda 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -18,23 +18,7 @@ import sys import os import psutil - - -def bytes2human(n): - # http://code.activestate.com/recipes/578019 - # >>> bytes2human(10000) - # '9.8K' - # >>> bytes2human(100001221) - # '95.4M' - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.1f%s' % (value, s) - return "%sB" % n +from psutil._common import bytes2human def main(): diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index e2a9ce5362..ad62a44f79 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -47,6 +47,7 @@ import socket import psutil +from psutil._common import bytes2human af_map = { @@ -62,24 +63,6 @@ } -def bytes2human(n): - """ - >>> bytes2human(10000) - '9.8 K' - >>> bytes2human(100001221) - '95.4 M' - """ - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.2f%s' % (value, s) - return '%.2fB' % (n) - - def main(): stats = psutil.net_if_stats() io_counters = psutil.net_io_counters(pernic=True) diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index 41e303f8c1..1b72a17775 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -21,6 +21,7 @@ import shutil from psutil import __version__ as PSUTIL_VERSION +from psutil._common import bytes2human from scriptutils import printerr, exit @@ -50,24 +51,6 @@ def onerror(fun, path, excinfo): shutil.rmtree(path, onerror=onerror) -def bytes2human(n): - """ - >>> bytes2human(10000) - '9.8 K' - >>> bytes2human(100001221) - '95.4 M' - """ - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.2f %s' % (value, s) - return '%.2f B' % (n) - - def download_file(url): local_fname = url.split('/')[-1] local_fname = os.path.join('dist', local_fname) diff --git a/scripts/iotop.py b/scripts/iotop.py index dabe957b52..6a5d3fa96e 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -39,9 +39,9 @@ sys.exit('platform not supported') import psutil +from psutil._common import bytes2human -# --- curses stuff def tear_down(): win.keypad(0) curses.nocbreak() @@ -70,25 +70,6 @@ def print_line(line, highlight=False): raise else: lineno += 1 -# --- /curses stuff - - -def bytes2human(n): - """ - >>> bytes2human(10000) - '9.8 K/s' - >>> bytes2human(100001221) - '95.4 M/s' - """ - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.2f %s/s' % (value, s) - return '%.2f B/s' % (n) def poll(interval): diff --git a/scripts/meminfo.py b/scripts/meminfo.py index 88c3a9378f..0b15ff2b75 100755 --- a/scripts/meminfo.py +++ b/scripts/meminfo.py @@ -31,23 +31,7 @@ """ import psutil - - -def bytes2human(n): - # http://code.activestate.com/recipes/578019 - # >>> bytes2human(10000) - # '9.8K' - # >>> bytes2human(100001221) - # '95.4M' - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.1f%s' % (value, s) - return "%sB" % n +from psutil._common import bytes2human def pprint_ntuple(nt): diff --git a/scripts/nettop.py b/scripts/nettop.py index e13903c11f..45b8879fac 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -40,9 +40,9 @@ sys.exit('platform not supported') import psutil +from psutil._common import bytes2human -# --- curses stuff def tear_down(): win.keypad(0) curses.nocbreak() @@ -71,25 +71,6 @@ def print_line(line, highlight=False): raise else: lineno += 1 -# --- curses stuff - - -def bytes2human(n): - """ - >>> bytes2human(10000) - '9.8 K' - >>> bytes2human(100001221) - '95.4 M' - """ - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.2f %s' % (value, s) - return '%.2f B' % (n) def poll(interval): diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 54205de36b..161b50575f 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -91,6 +91,7 @@ import sys import psutil +from psutil._common import bytes2human ACCESS_DENIED = '' @@ -115,18 +116,6 @@ } -def convert_bytes(n): - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.1f%s' % (value, s) - return "%sB" % n - - def print_(a, b): if sys.stdout.isatty() and psutil.POSIX: fmt = '\x1b[1;32m%-13s\x1b[0m %s' % (a, b) @@ -135,13 +124,13 @@ def print_(a, b): print(fmt) -def str_ntuple(nt, bytes2human=False): +def str_ntuple(nt, convert_bytes=False): if nt == ACCESS_DENIED: return "" - if not bytes2human: + if not convert_bytes: return ", ".join(["%s=%s" % (x, getattr(nt, x)) for x in nt._fields]) else: - return ", ".join(["%s=%s" % (x, convert_bytes(getattr(nt, x))) + return ", ".join(["%s=%s" % (x, bytes2human(getattr(nt, x))) for x in nt._fields]) @@ -193,7 +182,7 @@ def run(pid, verbose=False): if hasattr(proc, "cpu_num"): print_("cpu-num", pinfo["cpu_num"]) - print_('memory', str_ntuple(pinfo['memory_info'], bytes2human=True)) + print_('memory', str_ntuple(pinfo['memory_info'], convert_bytes=True)) print_('memory %', round(pinfo['memory_percent'], 2)) print_('user', pinfo['username']) if psutil.POSIX: @@ -224,7 +213,7 @@ def run(pid, verbose=False): print_('num-handles', pinfo['num_handles']) if 'io_counters' in pinfo: - print_('I/O', str_ntuple(pinfo['io_counters'], bytes2human=True)) + print_('I/O', str_ntuple(pinfo['io_counters'], convert_bytes=True)) if 'num_ctx_switches' in pinfo: print_("ctx-switches", str_ntuple(pinfo['num_ctx_switches'])) if pinfo['children']: @@ -322,7 +311,7 @@ def run(pid, verbose=False): if not verbose and i >= NON_VERBOSE_ITERATIONS: print_("", "[...]") break - print_("", template % (convert_bytes(region.rss), region.path)) + print_("", template % (bytes2human(region.rss), region.path)) def main(argv=None): diff --git a/scripts/ps.py b/scripts/ps.py index b85790d6df..bf28a4625c 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -16,6 +16,7 @@ import time import psutil +from psutil._common import bytes2human PROC_STATUSES_RAW = { @@ -49,55 +50,52 @@ def main(): attrs.append('terminal') print(templ % ("USER", "PID", "%CPU", "%MEM", "VSZ", "RSS", "TTY", "STAT", "START", "TIME", "COMMAND")) - for p in psutil.process_iter(): - try: - pinfo = p.as_dict(attrs, ad_value='') - except psutil.NoSuchProcess: - pass + for p in psutil.process_iter(attrs, ad_value=''): + pinfo = p.info + if pinfo['create_time']: + ctime = datetime.datetime.fromtimestamp(pinfo['create_time']) + if ctime.date() == today_day: + ctime = ctime.strftime("%H:%M") + else: + ctime = ctime.strftime("%b%d") else: - if pinfo['create_time']: - ctime = datetime.datetime.fromtimestamp(pinfo['create_time']) - if ctime.date() == today_day: - ctime = ctime.strftime("%H:%M") + ctime = '' + cputime = time.strftime("%M:%S", + time.localtime(sum(pinfo['cpu_times']))) + try: + user = p.username() + except KeyError: + if psutil.POSIX: + if pinfo['uids']: + user = str(pinfo['uids'].real) else: - ctime = ctime.strftime("%b%d") + user = '' else: - ctime = '' - cputime = time.strftime("%M:%S", - time.localtime(sum(pinfo['cpu_times']))) - try: - user = p.username() - except KeyError: - if os.name == 'posix': - if pinfo['uids']: - user = str(pinfo['uids'].real) - else: - user = '' - else: - raise - except psutil.Error: - user = '' - if os.name == 'nt' and '\\' in user: - user = user.split('\\')[1] - vms = pinfo['memory_info'] and \ - int(pinfo['memory_info'].vms / 1024) or '?' - rss = pinfo['memory_info'] and \ - int(pinfo['memory_info'].rss / 1024) or '?' - memp = pinfo['memory_percent'] and \ - round(pinfo['memory_percent'], 1) or '?' - status = PROC_STATUSES_RAW.get(pinfo['status'], pinfo['status']) - print(templ % ( - user[:10], - pinfo['pid'], - pinfo['cpu_percent'], - memp, - vms, - rss, - pinfo.get('terminal', '') or '?', - status, - ctime, - cputime, - pinfo['name'].strip() or '?')) + raise + except psutil.Error: + user = '' + if psutil.WINDOWS and '\\' in user: + user = user.split('\\')[1] + vms = bytes2human(pinfo['memory_info'].vms) if \ + pinfo['memory_info'] is not None else '' + rss = bytes2human(pinfo['memory_info'].rss) if \ + pinfo['memory_info'] is not None else '' + memp = round(pinfo['memory_percent'], 1) if \ + pinfo['memory_percent'] is not None else '' + + status = PROC_STATUSES_RAW.get(pinfo['status'], pinfo['status']) + print(templ % ( + user[:10], + pinfo['pid'], + pinfo['cpu_percent'], + memp, + vms, + rss, + pinfo.get('terminal', '') or '', + status, + ctime, + cputime, + pinfo['name'].strip() or '?')) if __name__ == '__main__': diff --git a/scripts/top.py b/scripts/top.py index 70dbf6c9c3..16f6da3b0b 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -10,27 +10,26 @@ Author: Giampaolo Rodola' $ python scripts/top.py - CPU0 [| ] 4.9% - CPU1 [||| ] 7.8% - CPU2 [ ] 2.0% - CPU3 [||||| ] 13.9% - Mem [||||||||||||||||||| ] 49.8% 4920M / 9888M - Swap [ ] 0.0% 0M / 0M - Processes: 287 (running=1, sleeping=286, zombie=1) - Load average: 0.34 0.54 0.46 Uptime: 3 days, 10:16:37 - -PID USER NI VIRT RES CPU% MEM% TIME+ NAME ------------------------------------------------------------- -989 giampaol 0 66M 12M 7.4 0.1 0:00.61 python -2083 root 0 506M 159M 6.5 1.6 0:29.26 Xorg -4503 giampaol 0 599M 25M 6.5 0.3 3:32.60 gnome-terminal -3868 giampaol 0 358M 8M 2.8 0.1 23:12.60 pulseaudio -3936 giampaol 0 1G 111M 2.8 1.1 33:41.67 compiz -4401 giampaol 0 536M 141M 2.8 1.4 35:42.73 skype -4047 giampaol 0 743M 76M 1.8 0.8 42:03.33 unity-panel-service -13155 giampaol 0 1G 280M 1.8 2.8 41:57.34 chrome -10 root 0 0B 0B 0.9 0.0 4:01.81 rcu_sched -339 giampaol 0 1G 113M 0.9 1.1 8:15.73 chrome + CPU0 [|||| ] 10.9% + CPU1 [||||| ] 13.1% + CPU2 [||||| ] 12.8% + CPU3 [|||| ] 11.5% + Mem [||||||||||||||||||||||||||||| ] 73.0% 11017M / 15936M + Swap [ ] 1.3% 276M / 20467M + Processes: 347 (sleeping=273, running=1, idle=73) + Load average: 1.10 1.28 1.34 Uptime: 8 days, 21:15:40 + +PID USER NI VIRT RES CPU% MEM% TIME+ NAME +5368 giampaol 0 7.2G 4.3G 41.8 27.7 56:34.18 VirtualBox +24976 giampaol 0 2.1G 487.2M 18.7 3.1 22:05.16 Web Content +22731 giampaol 0 3.2G 596.2M 11.6 3.7 35:04.90 firefox +1202 root 0 807.4M 288.5M 10.6 1.8 12:22.12 Xorg +22811 giampaol 0 2.8G 741.8M 9.0 4.7 2:26.61 Web Content +2590 giampaol 0 2.3G 579.4M 5.5 3.6 28:02.70 compiz +22990 giampaol 0 3.0G 1.2G 4.2 7.6 4:30.32 Web Content +18412 giampaol 0 90.1M 14.5M 3.5 0.1 0:00.26 python3 +26971 netdata 0 20.8M 3.9M 2.9 0.0 3:17.14 apps.plugin +2421 giampaol 0 3.3G 36.9M 2.3 0.2 57:14.21 pulseaudio ... """ @@ -45,6 +44,7 @@ sys.exit('platform not supported') import psutil +from psutil._common import bytes2human # --- curses stuff @@ -81,24 +81,6 @@ def print_line(line, highlight=False): # --- /curses stuff -def bytes2human(n): - """ - >>> bytes2human(10000) - '9K' - >>> bytes2human(100001221) - '95M' - """ - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = int(float(n) / prefix[s]) - return '%s%s' % (value, s) - return "%sB" % n - - def poll(interval): # sleep some time time.sleep(interval) @@ -178,7 +160,7 @@ def get_dashes(perc): def refresh_window(procs, procs_status): """Print results on screen by using curses.""" curses.endwin() - templ = "%-6s %-8s %4s %5s %5s %6s %4s %9s %2s" + templ = "%-6s %-8s %4s %6s %6s %5s %5s %9s %2s" win.erase() header = templ % ("PID", "USER", "NI", "VIRT", "RES", "CPU%", "MEM%", "TIME+", "NAME") From 279f6e4219eda6af2e1d05c762b022c5d7e10456 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 19:25:05 +0200 Subject: [PATCH 0272/1714] improve ps.py script --- scripts/ps.py | 51 ++++++++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/scripts/ps.py b/scripts/ps.py index bf28a4625c..72c315a800 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -12,7 +12,6 @@ """ import datetime -import os import time import psutil @@ -32,25 +31,20 @@ psutil.STATUS_LOCKED: "L", psutil.STATUS_WAITING: "W", } - if hasattr(psutil, 'STATUS_WAKE_KILL'): PROC_STATUSES_RAW[psutil.STATUS_WAKE_KILL] = "WK" - if hasattr(psutil, 'STATUS_SUSPENDED'): PROC_STATUSES_RAW[psutil.STATUS_SUSPENDED] = "V" def main(): today_day = datetime.date.today() - templ = "%-10s %5s %4s %4s %7s %7s %-13s %-5s %5s %7s %s" + templ = "%-10s %5s %5s %5s %7s %7s %5s %-5s %5s %7s %s" attrs = ['pid', 'cpu_percent', 'memory_percent', 'name', 'cpu_times', - 'create_time', 'memory_info', 'status'] - if os.name == 'posix': - attrs.append('uids') - attrs.append('terminal') - print(templ % ("USER", "PID", "%CPU", "%MEM", "VSZ", "RSS", "TTY", + 'create_time', 'memory_info', 'status', 'nice', 'username'] + print(templ % ("USER", "PID", "%CPU", "%MEM", "VSZ", "RSS", "NICE", "STAT", "START", "TIME", "COMMAND")) - for p in psutil.process_iter(attrs, ad_value=''): + for p in psutil.process_iter(attrs, ad_value=None): pinfo = p.info if pinfo['create_time']: ctime = datetime.datetime.fromtimestamp(pinfo['create_time']) @@ -60,28 +54,30 @@ def main(): ctime = ctime.strftime("%b%d") else: ctime = '' - cputime = time.strftime("%M:%S", - time.localtime(sum(pinfo['cpu_times']))) - try: - user = p.username() - except KeyError: - if psutil.POSIX: - if pinfo['uids']: - user = str(pinfo['uids'].real) - else: - user = '' - else: - raise - except psutil.Error: - user = '' - if psutil.WINDOWS and '\\' in user: + if pinfo['cpu_times']: + cputime = time.strftime("%M:%S", + time.localtime(sum(pinfo['cpu_times']))) + else: + cputime = '' + + user = pinfo['username'] + if not user and psutil.POSIX: + try: + user = p.uids()[0] + except psutil.Error: + pass + if user and psutil.WINDOWS and '\\' in user: user = user.split('\\')[1] + user = user[:9] + vms = bytes2human(pinfo['memory_info'].vms) if \ pinfo['memory_info'] is not None else '' rss = bytes2human(pinfo['memory_info'].rss) if \ pinfo['memory_info'] is not None else '' memp = round(pinfo['memory_percent'], 1) if \ pinfo['memory_percent'] is not None else '' + nice = int(pinfo['nice']) if pinfo['nice'] else '' + name = pinfo['name'] if pinfo['name'] else '' status = PROC_STATUSES_RAW.get(pinfo['status'], pinfo['status']) print(templ % ( @@ -91,11 +87,12 @@ def main(): memp, vms, rss, - pinfo.get('terminal', '') or '', + nice, status, ctime, cputime, - pinfo['name'].strip() or '?')) + name + )) if __name__ == '__main__': From 3204eaf4d244f1a76d1c20b2fb9773ad36db4e22 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 19:34:21 +0200 Subject: [PATCH 0273/1714] improve ps.py script --- scripts/ps.py | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/scripts/ps.py b/scripts/ps.py index 72c315a800..f7857f21f8 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -18,32 +18,13 @@ from psutil._common import bytes2human -PROC_STATUSES_RAW = { - psutil.STATUS_RUNNING: "R", - psutil.STATUS_SLEEPING: "S", - psutil.STATUS_DISK_SLEEP: "D", - psutil.STATUS_STOPPED: "T", - psutil.STATUS_TRACING_STOP: "t", - psutil.STATUS_ZOMBIE: "Z", - psutil.STATUS_DEAD: "X", - psutil.STATUS_WAKING: "WA", - psutil.STATUS_IDLE: "I", - psutil.STATUS_LOCKED: "L", - psutil.STATUS_WAITING: "W", -} -if hasattr(psutil, 'STATUS_WAKE_KILL'): - PROC_STATUSES_RAW[psutil.STATUS_WAKE_KILL] = "WK" -if hasattr(psutil, 'STATUS_SUSPENDED'): - PROC_STATUSES_RAW[psutil.STATUS_SUSPENDED] = "V" - - def main(): today_day = datetime.date.today() - templ = "%-10s %5s %5s %5s %7s %7s %5s %-5s %5s %7s %s" - attrs = ['pid', 'cpu_percent', 'memory_percent', 'name', 'cpu_times', + templ = "%-10s %5s %5s %7s %7s %5s %6s %6s %6s %s" + attrs = ['pid', 'memory_percent', 'name', 'cpu_times', 'create_time', 'memory_info', 'status', 'nice', 'username'] - print(templ % ("USER", "PID", "%CPU", "%MEM", "VSZ", "RSS", "NICE", - "STAT", "START", "TIME", "COMMAND")) + print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "NICE", + "STAT", "START", "TIME", "NAME")) for p in psutil.process_iter(attrs, ad_value=None): pinfo = p.info if pinfo['create_time']: @@ -79,11 +60,11 @@ def main(): nice = int(pinfo['nice']) if pinfo['nice'] else '' name = pinfo['name'] if pinfo['name'] else '' - status = PROC_STATUSES_RAW.get(pinfo['status'], pinfo['status']) +# status = PROC_STATUSES_RAW.get(pinfo['status'], pinfo['status']) + status = pinfo['status'][:5] if pinfo['status'] else '' print(templ % ( user[:10], pinfo['pid'], - pinfo['cpu_percent'], memp, vms, rss, From 83ed63b168e4c05bf8036e0c72a2460b9e6fa29b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 19:55:46 +0200 Subject: [PATCH 0274/1714] move get_terminal_size() in _compat.py --- psutil/_compat.py | 23 +++++++++++++++++- scripts/cpu_distribution.py | 19 +-------------- scripts/ps.py | 48 +++++++++++++++++++------------------ 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/psutil/_compat.py b/psutil/_compat.py index 08aefe4b76..c772f61d60 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -10,7 +10,7 @@ import sys __all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", - "lru_cache", "which"] + "lru_cache", "which", "get_terminal_size"] PY3 = sys.version_info[0] == 3 @@ -239,3 +239,24 @@ 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 termios + import struct + 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: + return fallback diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index 67f25b2d9f..c509c73288 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -46,6 +46,7 @@ import time import psutil +from psutil._compat import get_terminal_size if not hasattr(psutil.Process, "cpu_num"): @@ -59,24 +60,6 @@ def clean_screen(): os.system('cls') -def get_terminal_size(fallback=(80, 24)): - try: - # Added in Python 3.3 - from shutil import get_terminal_size as gts - return gts(fallback=fallback) - except ImportError: - try: - # This should work on Linux. - import fcntl - import termios - import struct - res = struct.unpack( - 'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234')) - return (res[1], res[0]) - except Exception: - return fallback - - def main(): num_cpus = psutil.cpu_count() if num_cpus > 8: diff --git a/scripts/ps.py b/scripts/ps.py index f7857f21f8..fd2a8ec446 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -5,7 +5,7 @@ # found in the LICENSE file. """ -A clone of 'ps -aux' on UNIX. +A clone of 'ps aux'. $ python scripts/ps.py ... @@ -16,32 +16,32 @@ import psutil from psutil._common import bytes2human +from psutil._compat import get_terminal_size def main(): today_day = datetime.date.today() templ = "%-10s %5s %5s %7s %7s %5s %6s %6s %6s %s" - attrs = ['pid', 'memory_percent', 'name', 'cpu_times', + attrs = ['pid', 'memory_percent', 'name', 'cmdline', 'cpu_times', 'create_time', 'memory_info', 'status', 'nice', 'username'] print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "NICE", - "STAT", "START", "TIME", "NAME")) + "STATUS", "START", "TIME", "CMDLINE")) for p in psutil.process_iter(attrs, ad_value=None): - pinfo = p.info - if pinfo['create_time']: - ctime = datetime.datetime.fromtimestamp(pinfo['create_time']) + if p.info['create_time']: + ctime = datetime.datetime.fromtimestamp(p.info['create_time']) if ctime.date() == today_day: ctime = ctime.strftime("%H:%M") else: ctime = ctime.strftime("%b%d") else: ctime = '' - if pinfo['cpu_times']: + if p.info['cpu_times']: cputime = time.strftime("%M:%S", - time.localtime(sum(pinfo['cpu_times']))) + time.localtime(sum(p.info['cpu_times']))) else: cputime = '' - user = pinfo['username'] + user = p.info['username'] if not user and psutil.POSIX: try: user = p.uids()[0] @@ -51,20 +51,22 @@ def main(): user = user.split('\\')[1] user = user[:9] - vms = bytes2human(pinfo['memory_info'].vms) if \ - pinfo['memory_info'] is not None else '' - rss = bytes2human(pinfo['memory_info'].rss) if \ - pinfo['memory_info'] is not None else '' - memp = round(pinfo['memory_percent'], 1) if \ - pinfo['memory_percent'] is not None else '' - nice = int(pinfo['nice']) if pinfo['nice'] else '' - name = pinfo['name'] if pinfo['name'] else '' + 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 '' -# status = PROC_STATUSES_RAW.get(pinfo['status'], pinfo['status']) - status = pinfo['status'][:5] if pinfo['status'] else '' - print(templ % ( + line = templ % ( user[:10], - pinfo['pid'], + p.info['pid'], memp, vms, rss, @@ -72,8 +74,8 @@ def main(): status, ctime, cputime, - name - )) + cmdline) + print(line[:get_terminal_size()[0]]) if __name__ == '__main__': From d286fea91fa93c6b5723e25ed89a803036e7c72f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 20:08:44 +0200 Subject: [PATCH 0275/1714] reuse ps.py script in psutil.test() --- psutil/__init__.py | 67 ++++++++++++++++++++-------------------------- scripts/ps.py | 24 +++++++++++++++-- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 3726079e5b..5d2b8d3c63 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2438,36 +2438,16 @@ def win_service_get(name): def test(): # pragma: no cover - """List info of all currently running processes emulating ps aux - output. - """ - def bytes2human(n): - """ - >>> bytes2human(10000) - '9.8 K' - >>> bytes2human(100001221) - '95.4 M' - """ - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return '%.1f%s' % (value, s) - return '%.1fB' % (n) + from ._common import bytes2human + from ._compat import get_terminal_size today_day = datetime.date.today() - templ = "%-10s %6s %5s %8s %8s %12s %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=None): + 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", + "STATUS", "START", "TIME", "CMDLINE")) + 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: @@ -2476,35 +2456,46 @@ def bytes2human(n): ctime = ctime.strftime("%b%d") else: ctime = '' - if p.info['cpu_times'] is not None: + if p.info['cpu_times']: cputime = time.strftime("%M:%S", time.localtime(sum(p.info['cpu_times']))) else: cputime = '' - try: - user = p.username()[:9] - except Error: - user = '' - if WINDOWS and '\\' in user: - user = user.split('\\')[1] + user = p.info['username'] + if not user and POSIX: + try: + user = p.uids()[0] + except Error: + pass + if user and WINDOWS and '\\' in user: + user = user.split('\\')[1] + 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 '' - print(templ % ( + 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]]) del memoize, memoize_when_activated, division, deprecated_method diff --git a/scripts/ps.py b/scripts/ps.py index fd2a8ec446..40dcce20a3 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -8,7 +8,28 @@ A clone of 'ps aux'. $ python scripts/ps.py -... +USER PID %MEM VSZ RSS NICE STATUS START TIME CMDLINE +root 1 0.0 220.9M 6.5M sleep Mar27 09:10 /lib/systemd +root 2 0.0 0.0B 0.0B sleep Mar27 00:00 kthreadd +root 4 0.0 0.0B 0.0B -20 idle Mar27 00:00 kworker/0:0H +root 6 0.0 0.0B 0.0B -20 idle Mar27 00:00 mm_percpu_wq +root 7 0.0 0.0B 0.0B sleep Mar27 00:06 ksoftirqd/0 +root 8 0.0 0.0B 0.0B idle Mar27 03:32 rcu_sched +root 9 0.0 0.0B 0.0B idle Mar27 00:00 rcu_bh +root 10 0.0 0.0B 0.0B sleep Mar27 00:00 migration/0 +root 11 0.0 0.0B 0.0B sleep Mar27 00:00 watchdog/0 +root 12 0.0 0.0B 0.0B sleep Mar27 00:00 cpuhp/0 +root 13 0.0 0.0B 0.0B sleep Mar27 00:00 cpuhp/1 +root 14 0.0 0.0B 0.0B sleep Mar27 00:01 watchdog/1 +root 15 0.0 0.0B 0.0B sleep Mar27 00:00 migration/1 +[...] +giampaolo 19704 1.5 1.9G 235.6M sleep 17:39 01:11 firefox +root 20414 0.0 0.0B 0.0B idle Apr04 00:00 kworker/4:2 +giampaolo 20952 0.0 10.7M 100.0K sleep Mar28 00:00 sh -c /usr +giampaolo 20953 0.0 269.0M 528.0K sleep Mar28 00:00 /usr/lib/ +giampaolo 22150 3.3 2.4G 525.5M sleep Apr02 49:09 /usr/lib/ +root 22338 0.0 0.0B 0.0B idle 02:04 00:00 kworker/1:2 +giampaolo 24123 0.0 35.0M 7.0M sleep 02:12 00:02 bash """ import datetime @@ -50,7 +71,6 @@ def main(): if user and psutil.WINDOWS and '\\' in user: user = user.split('\\')[1] 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 \ From 36b5ab0c83ad76a6a6b9ea4e97380a0f05ae332a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 20:26:29 +0200 Subject: [PATCH 0276/1714] improve pmap.py script --- scripts/pmap.py | 14 ++++++++------ scripts/winservices.py | 1 - 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/pmap.py b/scripts/pmap.py index 988b75072c..300f23e9d2 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -9,7 +9,6 @@ Report memory map of a process. $ python scripts/pmap.py 32402 -pid=32402, name=hg Address RSS Mode Mapping 0000000000400000 1200K r-xp /usr/bin/python2.7 0000000000838000 4K r--p /usr/bin/python2.7 @@ -33,9 +32,12 @@ import sys import psutil +from psutil._common import bytes2human +from psutil._compat import get_terminal_size def safe_print(s): + s = s[:get_terminal_size()[0]] try: print(s) except UnicodeEncodeError: @@ -46,19 +48,19 @@ def main(): if len(sys.argv) != 2: sys.exit('usage: pmap ') p = psutil.Process(int(sys.argv[1])) - print("pid=%s, name=%s" % (p.pid, p.name())) - templ = "%-16s %10s %-7s %s" + templ = "%-20s %10s %-7s %s" print(templ % ("Address", "RSS", "Mode", "Mapping")) total_rss = 0 for m in p.memory_maps(grouped=False): total_rss += m.rss safe_print(templ % ( m.addr.split('-')[0].zfill(16), - str(m.rss / 1024) + 'K', + bytes2human(m.rss), m.perms, m.path)) - print("-" * 33) - print(templ % ("Total", str(total_rss / 1024) + 'K', '', '')) + print("-" * 31) + print(templ % ("Total", bytes2human(total_rss), '', '')) + safe_print("PID = %s, name = %s" % (p.pid, p.name())) if __name__ == '__main__': diff --git a/scripts/winservices.py b/scripts/winservices.py index b52aaf21dd..677248359a 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -27,7 +27,6 @@ Appinfo (Application Information) status: stopped, start: manual, username: LocalSystem, pid: None binpath: C:\Windows\system32\svchost.exe -k netsvcs - ... """ From 05d51649ca709c6626d84cc710c2470d64829848 Mon Sep 17 00:00:00 2001 From: Alex Manuskin Date: Fri, 5 Apr 2019 21:32:37 +0300 Subject: [PATCH 0277/1714] Linux / CPU freq, fixes #1481 --- .gitignore | 1 + psutil/_pslinux.py | 26 ++++++------ psutil/tests/test_linux.py | 81 ++++++++++++++++++++++---------------- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 99d0d54571..3d22b0b355 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ syntax: glob *.rej *.so *.swp +.failed-tests.txt .cache/ .idea/ .tox/ diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index bf815e87a2..1f8ddb93ca 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -688,23 +688,19 @@ def cpu_freq(): 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 + def get_path(num): + for p in ("/sys/devices/system/cpu/cpufreq/policy%s" % num, + "/sys/devices/system/cpu/cpu%s/cpufreq" % num): + if os.path.exists(p): + return p + 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))) + for n in range(cpu_count_logical()): + path = get_path(n) + if not path: + continue - pjoin = os.path.join - for path in ls: + pjoin = os.path.join curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) if curr is None: # Likely an old RedHat, see: diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 66c50aa685..85cf33c44a 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -10,7 +10,6 @@ import collections import contextlib import errno -import glob import io import os import re @@ -698,27 +697,26 @@ class TestSystemCPUFrequency(unittest.TestCase): @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_emulate_no_files(self): - with mock.patch("psutil._pslinux.glob.glob", return_value=[]): + 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 glob_mock(pattern): - if pattern.startswith("/sys/devices/system/cpu/cpufreq/policy"): - flags.append(None) - return [] + def path_exists_mock(path): + if path.startswith("/sys/devices/system/cpu/cpufreq/policy"): + return False else: flags.append(None) - return orig_glob(pattern) + return orig_exists(path) flags = [] - orig_glob = glob.glob - with mock.patch("psutil._pslinux.glob.glob", side_effect=glob_mock, + 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), 2) + self.assertEqual(len(flags), psutil.cpu_count(logical=True)) @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_emulate_use_cpuinfo(self): @@ -752,11 +750,14 @@ def path_exists_mock(path): @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'): + 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'): + 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'): + 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) @@ -765,8 +766,7 @@ def open_mock(name, *args, **kwargs): 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/devices/system/cpu/cpufreq/policy0']): + 'os.path.exists', return_value=True): freq = psutil.cpu_freq() self.assertEqual(freq.current, 500.0) self.assertEqual(freq.min, 600.0) @@ -775,26 +775,41 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_emulate_multi_cpu(self): def open_mock(name, *args, **kwargs): - if name.endswith('/scaling_cur_freq'): + n = name + if (n.endswith('/scaling_cur_freq') and + n.startswith("/sys/devices/system/cpu/cpufreq/policy0")): return io.BytesIO(b"100000") - elif name.endswith('/scaling_min_freq'): + elif (n.endswith('/scaling_min_freq') and + n.startswith("/sys/devices/system/cpu/cpufreq/policy0")): return io.BytesIO(b"200000") - elif name.endswith('/scaling_max_freq'): + 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' - policies = ['/sys/devices/system/cpu/cpufreq/policy0', - '/sys/devices/system/cpu/cpufreq/policy1', - '/sys/devices/system/cpu/cpufreq/policy2'] with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('glob.glob', return_value=policies): - freq = psutil.cpu_freq() - self.assertEqual(freq.current, 100.0) - self.assertEqual(freq.min, 200.0) - self.assertEqual(freq.max, 300.0) + 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") @@ -810,14 +825,12 @@ def open_mock(name, *args, **kwargs): orig_open = open patch_point = 'builtins.open' if PY3 else '__builtin__.open' - policies = ['/sys/devices/system/cpu/cpufreq/policy0', - '/sys/devices/system/cpu/cpufreq/policy1', - '/sys/devices/system/cpu/cpufreq/policy2'] - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('glob.glob', return_value=policies): - freq = psutil.cpu_freq() - self.assertEqual(freq.current, 200) + 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. @@ -833,7 +846,7 @@ def open_mock(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=policies): + with mock.patch('os.path.exists', return_value=True): self.assertRaises(NotImplementedError, psutil.cpu_freq) From 97f0c7adf18ac02fa7e610fca19bf2a91455a06f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2019 21:27:01 +0200 Subject: [PATCH 0278/1714] give credits to @amanusk for #1472 --- CREDITS | 4 ++-- HISTORY.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index 33c62ea6d5..36ab49be2b 100644 --- a/CREDITS +++ b/CREDITS @@ -61,8 +61,8 @@ I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408, N: Alex Manuskin W: https://github.com/amanusk -D: FreeBSD cpu_freq(), OSX temperatures, support for Linux temperatures -I: 1284, 1345, 1350, 1352 +D: FreeBSD cpu_freq(), OSX temperatures, support for Linux temperatures. +I: 1284, 1345, 1350, 1352, 1472, 1481. N: Jeff Tang W: https://github.com/mrjefftang diff --git a/HISTORY.rst b/HISTORY.rst index 5741e2478e..afd63f01e7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,6 +25,7 @@ exist. (patch by Cedric Lamoriniere) - 1471_: [SunOS] Process name() and cmdline() can return SystemError. (patch by Daniel Beer) +- 1472_: [Linux] cpu_freq() does not return all CPUs on Rasbperry pi 3. - 1474_: fix formatting of psutil.tests() which mimicks 'ps aux' output. - 1475_: [Windows] OSError.winerror attribute wasn't properly checked resuling in WindowsError being raised instead of AccessDenied. From 921870d54091f399cd2b129db19530cc486b5700 Mon Sep 17 00:00:00 2001 From: Ammar Askar Date: Thu, 11 Apr 2019 05:32:39 -0400 Subject: [PATCH 0279/1714] Implement getloadavg on Windows. Fixes #604 and #1484 (#1485) (patch by Ammar Askar) --- .ci/travis/install.sh | 4 +- CREDITS | 5 ++ HISTORY.rst | 2 + docs/DEVNOTES | 2 - docs/index.rst | 30 ++++++-- psutil/__init__.py | 11 +++ psutil/_psutil_windows.c | 7 ++ psutil/_pswindows.py | 18 +++++ psutil/arch/windows/wmi.c | 115 ++++++++++++++++++++++++++++++ psutil/arch/windows/wmi.h | 12 ++++ psutil/tests/__init__.py | 1 + psutil/tests/test_linux.py | 15 ++++ psutil/tests/test_memory_leaks.py | 5 ++ psutil/tests/test_system.py | 10 +++ setup.py | 4 +- 15 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 psutil/arch/windows/wmi.c create mode 100644 psutil/arch/windows/wmi.h diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index b0f28c66d7..1e37c39b5c 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -21,8 +21,8 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then # pyenv virtualenv 2.6.9 psutil # ;; py27) - pyenv install 2.7.10 - pyenv virtualenv 2.7.10 psutil + pyenv install 2.7.16 + pyenv virtualenv 2.7.16 psutil ;; py36) pyenv install 3.6.6 diff --git a/CREDITS b/CREDITS index 33c62ea6d5..488ce3e36e 100644 --- a/CREDITS +++ b/CREDITS @@ -607,3 +607,8 @@ I: 1471 N: Samer Masterson W: https://github.com/samertm I: 1480 + +N: Ammar Askar +E: ammar@ammaraskar.com +W: http://ammaraskar.com/ +I: 604, 1484 \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst index 5741e2478e..5427ef4e40 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,8 @@ new constants: IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH. Also it was not possible to set high I/O priority (not it is). - 1478_: add make command to re-run tests failed on last run. +- 604: [UNIX, Windows] add new psutil.getloadavg() returning system load + average calculation. (patch by Ammar Askar) **Bug fixes** diff --git a/docs/DEVNOTES b/docs/DEVNOTES index c54fa9388f..abd2e3688d 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -69,8 +69,6 @@ FEATURES - #613: thread names. -- #604: emulate os.getloadavg() on Windows - - scripts/taskmgr-gui.py (using tk). - system-wide number of open file descriptors: diff --git a/docs/index.rst b/docs/index.rst index 85429eb6ab..1469e61b47 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -238,6 +238,30 @@ CPU .. versionchanged:: 5.5.1 added FreeBSD support. +.. function:: getloadavg() + + Returns the average load on the system over the last 1, 5 and 15 minutes + respectively as a tuple. The load represents how many processes are waiting + to be run by the operating system. + + On UNIX systems this relies on `os.getloadavg`_. On Windows, this is + emulated by using a Windows API call that spawns a thread which updates the + average every 5 seconds mimicking the UNIX behavior. Thus, the first time + this is called and up until 5 seconds it returns a meaningless + ``(0.0, 0.0, 0.0)`` tuple. + + Example: + + .. code-block:: python + + >>> import psutil + >>> psutil.getloadavg() + (3.14, 3.89, 4.67) + + Availability: Unix, Windows + + .. versionadded:: 5.6.2 + Memory ------ @@ -2556,12 +2580,6 @@ FAQs the Python script as a Windows service (this is the trick used by tools such as ProcessHacker). ----- - -* Q: What about load average? -* A: psutil does not expose any load average function as it's already available - in python as `os.getloadavg`_. - Running tests ============= diff --git a/psutil/__init__.py b/psutil/__init__.py index 5d2b8d3c63..07f1104b33 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2017,6 +2017,17 @@ def cpu_freq(percpu=False): __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 # ===================================================================== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 53617c5803..f071648f18 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -40,6 +40,7 @@ #include "arch/windows/process_handles.h" #include "arch/windows/inet_ntop.h" #include "arch/windows/services.h" +#include "arch/windows/wmi.h" #include "_psutil_common.h" @@ -3508,6 +3509,12 @@ PsutilMethods[] = { "Return NICs stats."}, {"cpu_freq", psutil_cpu_freq, METH_VARARGS, "Return CPU frequency."}, +#if (_WIN32_WINNT >= 0x0600) // Windows Vista + {"init_loadavg_counter", psutil_init_loadavg_counter, METH_VARARGS, + "Initializes the emulated load average calculator."}, + {"getloadavg", psutil_get_loadavg, METH_VARARGS, + "Returns the emulated POSIX-like load average."}, +#endif {"sensors_battery", psutil_sensors_battery, METH_VARARGS, "Return battery metrics usage."}, {"getpagesize", psutil_getpagesize, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 929e27d7d4..3f1319806f 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -80,6 +80,7 @@ CONN_DELETE_TCB = "DELETE_TCB" HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") +HAS_GETLOADAVG = hasattr(cext, "getloadavg") if enum is None: @@ -353,6 +354,23 @@ def cpu_freq(): return [_common.scpufreq(float(curr), min_, float(max_))] +if HAS_GETLOADAVG: + _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 # ===================================================================== diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c new file mode 100644 index 0000000000..5858a9e083 --- /dev/null +++ b/psutil/arch/windows/wmi.c @@ -0,0 +1,115 @@ +/* + * 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. + * + * Functions related to the Windows Management Instrumentation API. + */ + +#include +#include +#include + +#include "../../_psutil_common.h" + + +// We use an exponentially weighted moving average, just like Unix systems do +// https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation +// +// These constants serve as the damping factor and are calculated with +// 1 / exp(sampling interval in seconds / window size in seconds) +// +// This formula comes from linux's include/linux/sched/loadavg.h +// https://github.com/torvalds/linux/blob/345671ea0f9258f410eb057b9ced9cefbbe5dc78/include/linux/sched/loadavg.h#L20-L23 +#define LOADAVG_FACTOR_1F 0.9200444146293232478931553241 +#define LOADAVG_FACTOR_5F 0.6592406302004437462547604110 +#define LOADAVG_FACTOR_15F 0.2865047968601901003248854266 +// The time interval in seconds between taking load counts, same as Linux +#define SAMPLING_INTERVAL 5 + +double load_avg_1m = 0; +double load_avg_5m = 0; +double load_avg_15m = 0; + + +VOID CALLBACK LoadAvgCallback(PVOID hCounter) { + PDH_FMT_COUNTERVALUE displayValue; + double currentLoad; + PDH_STATUS err; + + err = PdhGetFormattedCounterValue( + (PDH_HCOUNTER)hCounter, PDH_FMT_DOUBLE, 0, &displayValue); + // Skip updating the load if we can't get the value successfully + if (err != ERROR_SUCCESS) { + return; + } + currentLoad = displayValue.doubleValue; + + load_avg_1m = load_avg_1m * LOADAVG_FACTOR_1F + currentLoad * \ + (1.0 - LOADAVG_FACTOR_1F); + load_avg_5m = load_avg_5m * LOADAVG_FACTOR_5F + currentLoad * \ + (1.0 - LOADAVG_FACTOR_5F); + load_avg_15m = load_avg_15m * LOADAVG_FACTOR_15F + currentLoad * \ + (1.0 - LOADAVG_FACTOR_15F); +} + + +PyObject * +psutil_init_loadavg_counter(PyObject *self, PyObject *args) { + WCHAR *szCounterPath = L"\\System\\Processor Queue Length"; + PDH_STATUS s; + BOOL ret; + HQUERY hQuery; + HCOUNTER hCounter; + HANDLE event; + HANDLE waitHandle; + + if ((PdhOpenQueryW(NULL, 0, &hQuery)) != ERROR_SUCCESS) + goto error; + + s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); + if (s != ERROR_SUCCESS) + goto error; + + event = CreateEventW(NULL, FALSE, FALSE, L"LoadUpdateEvent"); + if (event == NULL) { + PyErr_SetFromWindowsErr(GetLastError()); + return NULL; + } + + s = PdhCollectQueryDataEx(hQuery, SAMPLING_INTERVAL, event); + if (s != ERROR_SUCCESS) + goto error; + + ret = RegisterWaitForSingleObject( + &waitHandle, + event, + (WAITORTIMERCALLBACK)LoadAvgCallback, + (PVOID) + hCounter, + INFINITE, + WT_EXECUTEDEFAULT); + + if (ret == 0) { + PyErr_SetFromWindowsErr(GetLastError()); + return NULL; + } + + Py_RETURN_NONE; + +error: + PyErr_SetExcFromWindowsErr(PyExc_OSError, 0); + return NULL; +} + + +/* + * Gets the emulated 1 minute, 5 minute and 15 minute load averages + * (processor queue length) for the system. + * `init_loadavg_counter` must be called before this function to engage the + * mechanism that records load values. + */ +PyObject * +psutil_get_loadavg(PyObject *self, PyObject *args) { + return Py_BuildValue("(ddd)", load_avg_1m, load_avg_5m, load_avg_15m); +} \ No newline at end of file diff --git a/psutil/arch/windows/wmi.h b/psutil/arch/windows/wmi.h new file mode 100644 index 0000000000..0210f2d699 --- /dev/null +++ b/psutil/arch/windows/wmi.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + + + +#include + +PyObject* psutil_init_loadavg_counter(); +PyObject* psutil_get_loadavg(); diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 3aebbcbd78..796817195f 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -159,6 +159,7 @@ 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") diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 85cf33c44a..e8745a75ba 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -29,6 +29,7 @@ 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 @@ -866,6 +867,20 @@ def test_interrupts(self): 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 # ===================================================================== diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 170a1d0cd7..dde50a57f1 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -36,6 +36,7 @@ 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 @@ -474,6 +475,10 @@ def test_cpu_stats(self): 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): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 75e65b0fbd..c4c503a217 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -38,6 +38,7 @@ 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 @@ -778,6 +779,15 @@ def check_ls(ls): 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"] diff --git a/setup.py b/setup.py index 465a9b9e33..2c3d9b36e5 100755 --- a/setup.py +++ b/setup.py @@ -135,12 +135,12 @@ def get_winver(): 'psutil/arch/windows/inet_ntop.c', 'psutil/arch/windows/services.c', 'psutil/arch/windows/global.c', - # 'psutil/arch/windows/connections.c', + 'psutil/arch/windows/wmi.c', ], define_macros=macros, libraries=[ "psapi", "kernel32", "advapi32", "shell32", "netapi32", - "wtsapi32", "ws2_32", "PowrProf", + "wtsapi32", "ws2_32", "PowrProf", "pdh", ], # extra_compile_args=["/Z7"], # extra_link_args=["/DEBUG"] From 50defe14d1dabba3c048eaa5d8789239b0d7671f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 11 Apr 2019 23:09:41 +0200 Subject: [PATCH 0280/1714] update HISTORY --- HISTORY.rst | 2 +- README.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2d24ef95b4..b9393d1776 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ **Enhancements** +- 604_: [UNIX, Windows] add new psutil.getloadavg() returning system load - 1404_: [Linux] cpu_count(logical=False) uses a second method (read from `/sys/devices/system/cpu/cpu[0-9]/topology/core_id`) in order to determine the number of physical CPUs in case /proc/cpuinfo does not provide this info. @@ -14,7 +15,6 @@ new constants: IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH. Also it was not possible to set high I/O priority (not it is). - 1478_: add make command to re-run tests failed on last run. -- 604: [UNIX, Windows] add new psutil.getloadavg() returning system load average calculation. (patch by Ammar Askar) **Bug fixes** diff --git a/README.rst b/README.rst index f834eb1315..c7003547e4 100644 --- a/README.rst +++ b/README.rst @@ -180,6 +180,8 @@ CPU >>> psutil.cpu_freq() scpufreq(current=931.42925, min=800.0, max=3500.0) >>> + >>> psutil.getloadavg() # also on Windows (emulated) + (3.14, 3.89, 4.67) Memory ------ From 55f4b24642b53c747540a02b4161753d1ea7e1f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 12 Apr 2019 00:47:03 +0200 Subject: [PATCH 0281/1714] update doc --- docs/index.rst | 19 ++++++++----------- psutil/__init__.py | 2 +- scripts/top.py | 3 +-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1469e61b47..95a5e2ce8f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -240,17 +240,14 @@ CPU .. function:: getloadavg() - Returns the average load on the system over the last 1, 5 and 15 minutes - respectively as a tuple. The load represents how many processes are waiting - to be run by the operating system. - - On UNIX systems this relies on `os.getloadavg`_. On Windows, this is - emulated by using a Windows API call that spawns a thread which updates the - average every 5 seconds mimicking the UNIX behavior. Thus, the first time - this is called and up until 5 seconds it returns a meaningless - ``(0.0, 0.0, 0.0)`` tuple. - - Example: + Return the average system load over the last 1, 5 and 15 minutes as a tuple. + The load represents how many processes are waiting to be run by the + operating system. + On UNIX systems this relies on `os.getloadavg`_. On Windows this is + emulated by using a Windows API that spawns a thread which updates the + average every 5 seconds, mimicking the UNIX behavior. Thus, the first time + this is called and for the next 5 seconds it will return a meaningless + ``(0.0, 0.0, 0.0)`` tuple. Example: .. code-block:: python diff --git a/psutil/__init__.py b/psutil/__init__.py index 07f1104b33..4adb450997 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -210,7 +210,7 @@ "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 diff --git a/scripts/top.py b/scripts/top.py index 16f6da3b0b..69890e27ce 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -35,7 +35,6 @@ import atexit import datetime -import os import sys import time try: @@ -151,7 +150,7 @@ def get_dashes(perc): # load average, uptime uptime = datetime.datetime.now() - \ datetime.datetime.fromtimestamp(psutil.boot_time()) - av1, av2, av3 = os.getloadavg() + av1, av2, av3 = psutil.getloadavg() line = " Load average: %.2f %.2f %.2f Uptime: %s" \ % (av1, av2, av3, str(uptime).split('.')[0]) print_line(line) From c0aba35a78649c453f0c89ab163a58a8efb4639e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 12 Apr 2019 00:54:11 +0200 Subject: [PATCH 0282/1714] refactor/move some utilities into _common.py --- psutil/__init__.py | 2 +- psutil/_common.py | 13 +++++++++++++ psutil/_psaix.py | 11 +---------- psutil/_pslinux.py | 17 ++--------------- psutil/_pssunos.py | 11 +---------- 5 files changed, 18 insertions(+), 36 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 4adb450997..bd968f5d40 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2473,7 +2473,7 @@ def test(): # pragma: no cover else: cputime = '' - user = p.info['username'] + user = p.info['username'] or '' if not user and POSIX: try: user = p.uids()[0] diff --git a/psutil/_common.py b/psutil/_common.py index 0b4f5308c0..e3b45417ae 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -619,3 +619,16 @@ def bytes2human(n, format="%(value).1f%(symbol)s"): 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 diff --git a/psutil/_psaix.py b/psutil/_psaix.py index ff086b9070..b0aefa9990 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -20,6 +20,7 @@ from . import _psutil_aix as cext from . import _psutil_posix as cext_posix from ._common import AF_INET6 +from ._common import get_procfs_path from ._common import memoize_when_activated from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF @@ -101,16 +102,6 @@ svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) -# ===================================================================== -# --- utils -# ===================================================================== - - -def get_procfs_path(): - """Return updated psutil.PROCFS_PATH constant.""" - return sys.modules['psutil'].PROCFS_PATH - - # ===================================================================== # --- memory # ===================================================================== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1f8ddb93ca..eccfeed2c0 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -25,8 +25,8 @@ 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 decode +from ._common import get_procfs_path from ._common import isfile_strict from ._common import memoize from ._common import memoize_when_activated @@ -207,19 +207,6 @@ class IOPriority(enum.IntEnum): # ===================================================================== -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 diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 67166e464a..47a18181c8 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -17,6 +17,7 @@ from . import _psutil_posix as cext_posix from . import _psutil_sunos as cext from ._common import AF_INET6 +from ._common import get_procfs_path from ._common import isfile_strict from ._common import memoize_when_activated from ._common import sockfam_to_enum @@ -113,16 +114,6 @@ '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 - - # ===================================================================== # --- memory # ===================================================================== From 3d6b084a281ed72ae4d31218ed949d7046e7e841 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 25 Apr 2019 05:05:06 +0200 Subject: [PATCH 0283/1714] fix #1486: add wraps() decorator around wrap_exceptions --- HISTORY.rst | 2 ++ psutil/_psaix.py | 3 ++- psutil/_pssunos.py | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b9393d1776..f2a0bd136f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -35,6 +35,8 @@ APIs. Different process methods were affected by this. - 1480_: [Windows] psutil.cpu_count(logical=False) could cause a crash due to fixed read violation. (patch by Samer Masterson) +- 1486_: [AIX, SunOS] AttributeError when interacting with Process methods + involved into oneshot() context. 5.6.1 ===== diff --git a/psutil/_psaix.py b/psutil/_psaix.py index b0aefa9990..174dac1ec7 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -7,6 +7,7 @@ """AIX platform implementation.""" import errno +import functools import glob import os import re @@ -322,7 +323,7 @@ def wrap_exceptions(fun): """Call callable into a try/except clause and translate ENOENT, EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ - + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 47a18181c8..6d7fda8555 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -5,6 +5,7 @@ """Sun OS Solaris platform implementation.""" import errno +import functools import os import socket import subprocess @@ -336,7 +337,7 @@ def wrap_exceptions(fun): """Call callable into a try/except clause and translate ENOENT, EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ - + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) From 611b44ec6e830d98c768038c66683c3fa1a7b491 Mon Sep 17 00:00:00 2001 From: agnewee Date: Thu, 25 Apr 2019 11:12:56 +0800 Subject: [PATCH 0284/1714] SunOS / net_if_addrs(): free() ifap struct on error (#1491) --- psutil/arch/solaris/v10/ifaddrs.c | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/arch/solaris/v10/ifaddrs.c b/psutil/arch/solaris/v10/ifaddrs.c index aedba84e95..b741a6b92b 100644 --- a/psutil/arch/solaris/v10/ifaddrs.c +++ b/psutil/arch/solaris/v10/ifaddrs.c @@ -120,5 +120,6 @@ int getifaddrs (struct ifaddrs **ifap) free(ifc.lifc_buf); if (sd != -1) close(sd); + freeifaddrs(*ifap); return (-1); } From 398ae043991acf78f96299477232e763bf71b8ee Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 25 Apr 2019 05:15:56 +0200 Subject: [PATCH 0285/1714] give CREDITS to @agnewee for #1491 --- CREDITS | 7 ++++++- HISTORY.rst | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 626a79a896..8d0ce8fb81 100644 --- a/CREDITS +++ b/CREDITS @@ -611,4 +611,9 @@ I: 1480 N: Ammar Askar E: ammar@ammaraskar.com W: http://ammaraskar.com/ -I: 604, 1484 \ No newline at end of file +I: 604, 1484 + +N: agnewee +W: https://github.com/Agnewee +C: China +I: 1491 diff --git a/HISTORY.rst b/HISTORY.rst index f2a0bd136f..edc0e0e6ac 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -37,6 +37,8 @@ fixed read violation. (patch by Samer Masterson) - 1486_: [AIX, SunOS] AttributeError when interacting with Process methods involved into oneshot() context. +- 1491_: [SunOS] net_if_addrs(): free() ifap struct on error. (patch by + Agnewee) 5.6.1 ===== From 7ee88afd9f6730289cf9e34f70210fdb387b8a71 Mon Sep 17 00:00:00 2001 From: Alex Manuskin Date: Thu, 25 Apr 2019 06:17:11 +0300 Subject: [PATCH 0286/1714] Update cpu_freq to return 0 for max/min if not available (#1487) --- CREDITS | 4 ++-- HISTORY.rst | 2 ++ psutil/_pslinux.py | 2 +- psutil/tests/test_linux.py | 8 ++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CREDITS b/CREDITS index 626a79a896..a2fca99b6b 100644 --- a/CREDITS +++ b/CREDITS @@ -62,7 +62,7 @@ I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408, N: Alex Manuskin W: https://github.com/amanusk D: FreeBSD cpu_freq(), OSX temperatures, support for Linux temperatures. -I: 1284, 1345, 1350, 1352, 1472, 1481. +I: 1284, 1345, 1350, 1352, 1472, 1481, 1487. N: Jeff Tang W: https://github.com/mrjefftang @@ -611,4 +611,4 @@ I: 1480 N: Ammar Askar E: ammar@ammaraskar.com W: http://ammaraskar.com/ -I: 604, 1484 \ No newline at end of file +I: 604, 1484 diff --git a/HISTORY.rst b/HISTORY.rst index f2a0bd136f..9a9df63c93 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,8 @@ **Bug fixes** - 1223_: [Windows] boot_time() may return value on Windows XP. +- 1456_: [Linux] cpu_freq() returns None instead of 0.0 when min/max not + available (patch by Alex Manuskin) - 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by - 1463_: cpu_distribution.py script was broken. Benjamin Drung) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index eccfeed2c0..4c973c2d3f 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -712,7 +712,7 @@ def cpu_freq(): for line in f: if line.lower().startswith(b'cpu mhz'): key, value = line.split(b'\t:', 1) - ret.append(_common.scpufreq(float(value), None, None)) + ret.append(_common.scpufreq(float(value), 0.0, 0.0)) return ret diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index e8745a75ba..4cdc9cbf7e 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -739,11 +739,11 @@ def path_exists_mock(path): ret = psutil.cpu_freq() assert ret assert flags - self.assertIsNone(ret.min) - self.assertIsNone(ret.max) + self.assertEqual(ret.max, 0.0) + self.assertEqual(ret.min, 0.0) for freq in psutil.cpu_freq(percpu=True): - self.assertIsNone(freq.min) - self.assertIsNone(freq.max) + self.assertEqual(ret.max, 0.0) + self.assertEqual(ret.min, 0.0) finally: reload_module(psutil._pslinux) reload_module(psutil) From 01e00a6629a78a7277aac627c8b025cb753d6fee Mon Sep 17 00:00:00 2001 From: David Brochart Date: Thu, 25 Apr 2019 06:29:25 +0200 Subject: [PATCH 0287/1714] Fix cpu_freq (#1493) --- psutil/_pslinux.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4c973c2d3f..ac45b54b3f 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -668,7 +668,7 @@ def cpu_stats(): ctx_switches, interrupts, soft_interrupts, syscalls) -if os.path.exists("/sys/devices/system/cpu/cpufreq") or \ +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. @@ -715,6 +715,12 @@ def cpu_freq(): ret.append(_common.scpufreq(float(value), 0.0, 0.0)) return ret +else: + def cpu_freq(): + """Dummy implementation when none of the above files are present. + """ + return [] + # ===================================================================== # --- network From d3d13e2b8c23fb8d3da11d15319732966fa41178 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 25 Apr 2019 14:40:32 +0800 Subject: [PATCH 0288/1714] Revert "Fix cpu_freq (#1493)" (#1495) This reverts commit 01e00a6629a78a7277aac627c8b025cb753d6fee. --- psutil/_pslinux.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ac45b54b3f..4c973c2d3f 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -668,7 +668,7 @@ def cpu_stats(): ctx_switches, interrupts, soft_interrupts, syscalls) -if os.path.exists("/sys/devices/system/cpu/cpufreq/policy0") or \ +if os.path.exists("/sys/devices/system/cpu/cpufreq") or \ os.path.exists("/sys/devices/system/cpu/cpu0/cpufreq"): def cpu_freq(): """Return frequency metrics for all CPUs. @@ -715,12 +715,6 @@ def cpu_freq(): ret.append(_common.scpufreq(float(value), 0.0, 0.0)) return ret -else: - def cpu_freq(): - """Dummy implementation when none of the above files are present. - """ - return [] - # ===================================================================== # --- network From c2b333b7dc051e9c0b62b619a35c6f4a321d0b5b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 26 Apr 2019 10:36:48 +0800 Subject: [PATCH 0289/1714] fix #1493: [Linux] cpu_freq(): handle the case where /sys/devices/system/cpu/cpufreq/ exists but is empty. --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9c3fa963c4..f137424155 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -41,6 +41,8 @@ involved into oneshot() context. - 1491_: [SunOS] net_if_addrs(): free() ifap struct on error. (patch by Agnewee) +- 1493_: [Linux] cpu_freq(): handle the case where + /sys/devices/system/cpu/cpufreq/ exists but is empty. 5.6.1 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4c973c2d3f..c8733e6fe8 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -668,7 +668,7 @@ def cpu_stats(): ctx_switches, interrupts, soft_interrupts, syscalls) -if os.path.exists("/sys/devices/system/cpu/cpufreq") or \ +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. From e74f59b86bd3d98be915e42c8f470a52667280cc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 26 Apr 2019 10:42:40 +0800 Subject: [PATCH 0290/1714] pre release --- HISTORY.rst | 2 ++ MANIFEST.in | 2 ++ docs/index.rst | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index f137424155..3568c4c8d7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,8 @@ 5.6.2 ===== +2019-04-26 + **Enhancements** - 604_: [UNIX, Windows] add new psutil.getloadavg() returning system load diff --git a/MANIFEST.in b/MANIFEST.in index 6ab7c6483f..027e4e94bf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -77,6 +77,8 @@ include psutil/arch/windows/security.c include psutil/arch/windows/security.h include psutil/arch/windows/services.c include psutil/arch/windows/services.h +include psutil/arch/windows/wmi.c +include psutil/arch/windows/wmi.h include psutil/tests/README.rst include psutil/tests/__init__.py include psutil/tests/__main__.py diff --git a/docs/index.rst b/docs/index.rst index 95a5e2ce8f..09d4c3ed25 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2598,6 +2598,10 @@ take a look at the `development guide`_. Timeline ======== +- 2019-0426: + `5.6.2 `__ - + `what's new `__ - + `diff `__ - 2019-03-11: `5.6.1 `__ - `what's new `__ - From d6f268f6059fde4283276b0501ad145d85106313 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 26 Apr 2019 05:06:23 +0200 Subject: [PATCH 0291/1714] Fix cpu freq (#1496) --- psutil/_pslinux.py | 22 +++++++++++++++++----- psutil/tests/test_linux.py | 29 +++++++++++++++++++++++------ psutil/tests/test_system.py | 3 ++- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index c8733e6fe8..027846027e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -708,13 +708,25 @@ def cpu_freq(): min and max frequencies are not available and are set to None. """ ret = [] - with open_binary('%s/cpuinfo' % get_procfs_path()) as f: - for line in f: - if line.lower().startswith(b'cpu mhz'): - key, value = line.split(b'\t:', 1) - ret.append(_common.scpufreq(float(value), 0.0, 0.0)) + path = '%s/cpuinfo' % get_procfs_path() + if os.path.exists(path): + try: + with open_binary(path) as f: + for line in f: + if line.lower().startswith(b'cpu mhz'): + key, value = line.split(b'\t:', 1) + ret.append(_common.scpufreq(float(value), 0., 0.)) + except IOError as err: + raise NotImplementedError( + "%r for file %r" % (err, path)) return ret +else: + def cpu_freq(): + """Dummy implementation when none of the above files are present. + """ + return [] + # ===================================================================== # --- network diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4cdc9cbf7e..a4b79e4fc5 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -760,6 +760,8 @@ def open_mock(name, *args, **kwargs): 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) @@ -770,8 +772,12 @@ def open_mock(name, *args, **kwargs): '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) + # 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): @@ -795,6 +801,9 @@ def open_mock(name, *args, **kwargs): 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\n" + b"cpu MHz : 400") else: return orig_open(name, *args, **kwargs) @@ -806,11 +815,15 @@ def open_mock(name, *args, **kwargs): 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) + 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) - self.assertEqual(freq[1].min, 500.0) - self.assertEqual(freq[1].max, 600.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(TRAVIS, "fails on Travis") @unittest.skipIf(not HAS_CPU_FREQ, "not supported") @@ -821,6 +834,8 @@ def open_mock(name, *args, **kwargs): 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) @@ -841,6 +856,8 @@ def open_mock(name, *args, **kwargs): raise IOError(errno.ENOENT, "") elif name.endswith('/cpuinfo_cur_freq'): raise IOError(errno.ENOENT, "") + elif name == '/proc/cpuinfo': + raise IOError(errno.ENOENT, "") else: return orig_open(name, *args, **kwargs) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index c4c503a217..eb9016f08a 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -761,7 +761,8 @@ 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) + 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)) From c78a850358af883dcaa63a3f9204f15cc129e518 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 26 Apr 2019 11:07:40 +0800 Subject: [PATCH 0292/1714] remove catching IOError; let the test fail and adjust it later --- psutil/_pslinux.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 027846027e..33bafd2872 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -708,17 +708,11 @@ def cpu_freq(): min and max frequencies are not available and are set to None. """ ret = [] - path = '%s/cpuinfo' % get_procfs_path() - if os.path.exists(path): - try: - with open_binary(path) as f: - for line in f: - if line.lower().startswith(b'cpu mhz'): - key, value = line.split(b'\t:', 1) - ret.append(_common.scpufreq(float(value), 0., 0.)) - except IOError as err: - raise NotImplementedError( - "%r for file %r" % (err, path)) + with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + for line in f: + if line.lower().startswith(b'cpu mhz'): + key, value = line.split(b'\t:', 1) + ret.append(_common.scpufreq(float(value), 0., 0.)) return ret else: From 0ba30fdccb57733a697d422946700175793f155e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 29 Apr 2019 11:13:29 +0800 Subject: [PATCH 0293/1714] fix history syntax --- HISTORY.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3568c4c8d7..26fc5b0efb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,31 +7,31 @@ **Enhancements** -- 604_: [UNIX, Windows] add new psutil.getloadavg() returning system load +- 604_: [Windows, Windows] add new psutil.getloadavg(), returning system load + average calculation, including on Windows (emulated). (patch by Ammar Askar) - 1404_: [Linux] cpu_count(logical=False) uses a second method (read from `/sys/devices/system/cpu/cpu[0-9]/topology/core_id`) in order to determine the number of physical CPUs in case /proc/cpuinfo does not provide this info. - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. - 1464_: various docfixes (always point to python3 doc, fix links, etc.). -- 1473_: [Windows] process IO priority (ionice()) values are now exposed as 4 - new constants: IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH. - Also it was not possible to set high I/O priority (not it is). +- 1476_: [Windows] it is now possible to set process high I/O priority + (ionice()).Also, I/O priority values are now exposed as 4 new constants: + IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH. - 1478_: add make command to re-run tests failed on last run. - average calculation. (patch by Ammar Askar) **Bug fixes** - 1223_: [Windows] boot_time() may return value on Windows XP. - 1456_: [Linux] cpu_freq() returns None instead of 0.0 when min/max not available (patch by Alex Manuskin) -- 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by -- 1463_: cpu_distribution.py script was broken. +- 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by Benjamin Drung) +- 1463_: cpu_distribution.py script was broken. - 1470_: [Linux] disk_partitions(): fix corner case when /etc/mtab doesn't exist. (patch by Cedric Lamoriniere) - 1471_: [SunOS] Process name() and cmdline() can return SystemError. (patch by Daniel Beer) -- 1472_: [Linux] cpu_freq() does not return all CPUs on Rasbperry pi 3. +- 1472_: [Linux] cpu_freq() does not return all CPUs on Rasbperry-pi 3. - 1474_: fix formatting of psutil.tests() which mimicks 'ps aux' output. - 1475_: [Windows] OSError.winerror attribute wasn't properly checked resuling in WindowsError being raised instead of AccessDenied. From 23832cdebf3105c4fb0b0a976e9d1204e559d8a8 Mon Sep 17 00:00:00 2001 From: Tongzhou Wang Date: Mon, 6 May 2019 03:12:41 -0400 Subject: [PATCH 0294/1714] Fix Process.ionice example using wrong keyword arg (#1504) --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 09d4c3ed25..967d7bdfc9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1285,14 +1285,14 @@ Process class import psutil p = psutil.Process() if psutil.LINUX - p.ionice(psutil.IOPRIO_CLASS_RT, level=7) + p.ionice(psutil.IOPRIO_CLASS_RT, value=7) else: # Windows p.ionice(psutil.IOPRIO_HIGH) p.ionice() # get Availability: Linux, Windows Vista+ - .. versionchanged:: 5.6.2 Windows accepts mew ``IOPRIO_*`` constants + .. versionchanged:: 5.6.2 Windows accepts new ``IOPRIO_*`` constants including new ``IOPRIO_HIGH``. .. method:: rlimit(resource, limits=None) From 7d8c23d5ce46eaeb13f1ca1910eabf76bdcda6a5 Mon Sep 17 00:00:00 2001 From: wiggin15 Date: Mon, 6 May 2019 01:13:56 -0600 Subject: [PATCH 0295/1714] Fix #1276: [AIX] use getargs to get process cmdline (#1500) (patch by @wiggin15) --- psutil/_psaix.py | 18 +++----- psutil/_psutil_aix.c | 86 +++++++++++++++++++++++++++--------- psutil/tests/test_process.py | 16 +++++++ 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 174dac1ec7..d0aa0e94e4 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -358,20 +358,13 @@ def __init__(self, pid): self._procfs_path = get_procfs_path() def oneshot_enter(self): - 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) 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): @@ -386,14 +379,17 @@ 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): @@ -415,7 +411,7 @@ def exe(self): @wrap_exceptions def cmdline(self): - return self._proc_name_and_args()[1].split(' ') + return cext.proc_args(self.pid) @wrap_exceptions def create_time(self): diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index adb65b170b..1a83ac24e9 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -23,20 +23,21 @@ * * Useful resources: * - proc filesystem: http://www-01.ibm.com/support/knowledgecenter/ - * ssw_aix_61/com.ibm.aix.files/proc.htm + * ssw_aix_72/com.ibm.aix.files/proc.htm * - libperfstat: http://www-01.ibm.com/support/knowledgecenter/ - * ssw_aix_61/com.ibm.aix.files/libperfstat.h.htm + * ssw_aix_72/com.ibm.aix.files/libperfstat.h.htm */ #include -#include -#include +#include #include -#include #include #include +#include +#include #include +#include #include #include #include @@ -134,17 +135,14 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { /* - * Return process name and args as a Python tuple. + * Return process name as a Python string. */ static PyObject * -psutil_proc_name_and_args(PyObject *self, PyObject *args) { +psutil_proc_name(PyObject *self, PyObject *args) { int pid; char path[100]; psinfo_t info; const char *procfs_path; - PyObject *py_name = NULL; - PyObject *py_args = NULL; - PyObject *py_retlist = NULL; if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; @@ -152,23 +150,65 @@ psutil_proc_name_and_args(PyObject *self, PyObject *args) { if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; - py_name = PyUnicode_DecodeFSDefault(info.pr_fname); - if (!py_name) + return PyUnicode_DecodeFSDefaultAndSize(info.pr_fname, PRFNSZ); +} + + +/* + * Return process command line arguments as a Python list + */ +static PyObject * +psutil_proc_args(PyObject *self, PyObject *args) { + int pid; + PyObject *py_retlist = PyList_New(0); + PyObject *py_arg = NULL; + struct procsinfo procbuf; + long arg_max; + char *argbuf = NULL; + char *curarg = NULL; + int ret; + + if (py_retlist == NULL) + return NULL; + if (!PyArg_ParseTuple(args, "i", &pid)) goto error; - py_args = PyUnicode_DecodeFSDefault(info.pr_psargs); - if (!py_args) + arg_max = sysconf(_SC_ARG_MAX); + argbuf = malloc(arg_max); + if (argbuf == NULL) { + PyErr_NoMemory(); goto error; - py_retlist = Py_BuildValue("OO", py_name, py_args); - if (!py_retlist) + } + + procbuf.pi_pid = pid; + ret = getargs(&procbuf, sizeof(struct procinfo), argbuf, ARG_MAX); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); goto error; - Py_DECREF(py_name); - Py_DECREF(py_args); + } + + curarg = argbuf; + /* getargs will always append an extra NULL to end the arg list, + * even if the buffer is not big enough (even though it is supposed + * to be) so the following 'while' is safe */ + while (*curarg != '\0') { + py_arg = PyUnicode_DecodeFSDefault(curarg); + if (!py_arg) + goto error; + if (PyList_Append(py_retlist, py_arg)) + goto error; + Py_DECREF(py_arg); + curarg = strchr(curarg, '\0') + 1; + } + + free(argbuf); + return py_retlist; error: - Py_XDECREF(py_name); - Py_XDECREF(py_args); + if (argbuf != NULL) + free(argbuf); Py_XDECREF(py_retlist); + Py_XDECREF(py_arg); return NULL; } @@ -880,8 +920,10 @@ PsutilMethods[] = // --- process-related functions {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS, "Return process ppid, rss, vms, ctime, nice, nthreads, status and tty"}, - {"proc_name_and_args", psutil_proc_name_and_args, METH_VARARGS, - "Return process name and args."}, + {"proc_name", psutil_proc_name, METH_VARARGS, + "Return process name."}, + {"proc_args", psutil_proc_args, METH_VARARGS, + "Return process command line arguments."}, {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, "Return process user and system CPU times."}, {"proc_cred", psutil_proc_cred, METH_VARARGS, diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a4b738eeb2..512a84371f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -724,12 +724,28 @@ def test_cmdline(self): else: raise + def test_long_cmdline(self): + create_exe(TESTFN) + self.addCleanup(safe_rmpath, TESTFN) + cmdline = [TESTFN] + (["0123456789"] * 20) + sproc = get_test_subprocess(cmdline) + p = psutil.Process(sproc.pid) + self.assertEqual(p.cmdline(), cmdline) + 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) + def test_long_name(self): + long_name = TESTFN + ("0123456789" * 2) + create_exe(long_name) + self.addCleanup(safe_rmpath, long_name) + sproc = get_test_subprocess(long_name) + p = psutil.Process(sproc.pid) + self.assertEqual(p.name(), os.path.basename(long_name)) + # XXX @unittest.skipIf(SUNOS, "broken on SUNOS") @unittest.skipIf(AIX, "broken on AIX") From 904f5ec2b416620302df78de1ef8d73c00401a32 Mon Sep 17 00:00:00 2001 From: wiggin15 Date: Mon, 6 May 2019 21:06:13 -0600 Subject: [PATCH 0296/1714] Fix #1494: [AIX] implement Process.environ() (#1505) (patch by Arnon Yaari) --- psutil/__init__.py | 2 +- psutil/_psaix.py | 4 ++ psutil/_psutil_aix.c | 74 ++++++++++++++++++++++++++++++++++ psutil/tests/test_contracts.py | 2 +- 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index bd968f5d40..5d37c5c0cf 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -949,7 +949,7 @@ def cpu_num(self): """ return self._proc.cpu_num() - # Linux, macOS and Windows only + # Linux, macOS, Windows, Solaris, AIX if hasattr(_psplatform.Process, "environ"): def environ(self): diff --git a/psutil/_psaix.py b/psutil/_psaix.py index d0aa0e94e4..5810e3b49f 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -413,6 +413,10 @@ def exe(self): def cmdline(self): return cext.proc_args(self.pid) + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) + @wrap_exceptions def create_time(self): return self._proc_basic_info()[proc_info_map['create_time']] diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 1a83ac24e9..723d159d4e 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -213,6 +213,78 @@ psutil_proc_args(PyObject *self, PyObject *args) { } +/* + * Return process environment variables as a Python dict + */ +static PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + int pid; + PyObject *py_retdict = PyDict_New(); + PyObject *py_key = NULL; + PyObject *py_val = NULL; + struct procsinfo procbuf; + long env_max; + char *envbuf = NULL; + char *curvar = NULL; + char *separator = NULL; + int ret; + + if (py_retdict == NULL) + return NULL; + if (!PyArg_ParseTuple(args, "i", &pid)) + goto error; + env_max = sysconf(_SC_ARG_MAX); + envbuf = malloc(env_max); + if (envbuf == NULL) { + PyErr_NoMemory(); + goto error; + } + + procbuf.pi_pid = pid; + ret = getevars(&procbuf, sizeof(struct procinfo), envbuf, ARG_MAX); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + curvar = envbuf; + /* getevars will always append an extra NULL to end the arg list, + * even if the buffer is not big enough (even though it is supposed + * to be) so the following 'while' is safe */ + while (*curvar != '\0') { + separator = strchr(curvar, '='); + if (separator != NULL) { + py_key = PyUnicode_DecodeFSDefaultAndSize( + curvar, + (Py_ssize_t)(separator - curvar) + ); + if (!py_key) + goto error; + py_val = PyUnicode_DecodeFSDefault(separator + 1); + if (!py_val) + goto error; + if (PyDict_SetItem(py_retdict, py_key, py_val)) + goto error; + Py_DECREF(py_key); + Py_DECREF(py_val); + } + curvar = strchr(curvar, '\0') + 1; + } + + free(envbuf); + + return py_retdict; + +error: + if (envbuf != NULL) + free(envbuf); + Py_XDECREF(py_retdict); + Py_XDECREF(py_key); + Py_XDECREF(py_val); + return NULL; +} + + #ifdef CURR_VERSION_THREAD /* @@ -924,6 +996,8 @@ PsutilMethods[] = "Return process name."}, {"proc_args", psutil_proc_args, METH_VARARGS, "Return process command line arguments."}, + {"proc_environ", psutil_proc_environ, METH_VARARGS, + "Return process environment variables."}, {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, "Return process user and system CPU times."}, {"proc_cred", psutil_proc_cred, METH_VARARGS, diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index adf7b680a6..20da8241f3 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -130,7 +130,7 @@ def test_battery(self): def test_proc_environ(self): self.assertEqual(hasattr(psutil.Process, "environ"), - LINUX or MACOS or WINDOWS) + LINUX or MACOS or WINDOWS or AIX or SUNOS) def test_proc_uids(self): self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) From 43a22b81832f7ba546636c26006190bc2bb8dfc2 Mon Sep 17 00:00:00 2001 From: wiggin15 Date: Wed, 8 May 2019 11:20:49 +0300 Subject: [PATCH 0297/1714] Fix some tests on AIX (#1507) --- psutil/tests/test_posix.py | 20 +++++++++++++++++--- scripts/ps.py | 4 +++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index d24abad38d..348366ad7b 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -52,7 +52,7 @@ def ps(fmt, pid=None): if pid is not None: cmd.extend(['-p', str(pid)]) else: - if SUNOS: + if SUNOS or AIX: cmd.append('-A') else: cmd.append('ax') @@ -113,6 +113,20 @@ def ps_args(pid): return ps(field, pid) +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(unittest.TestCase): """Compare psutil results against 'ps' command line utility (mainly).""" @@ -162,7 +176,7 @@ 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_ps = ps_rss(self.pid) rss_psutil = psutil.Process(self.pid).memory_info()[0] / 1024 self.assertEqual(rss_ps, rss_psutil) @@ -172,7 +186,7 @@ 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_ps = ps_vsz(self.pid) vsz_psutil = psutil.Process(self.pid).memory_info()[1] / 1024 self.assertEqual(vsz_ps, vsz_psutil) diff --git a/scripts/ps.py b/scripts/ps.py index 40dcce20a3..8467cca6f3 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -70,6 +70,8 @@ def main(): pass if user and psutil.WINDOWS and '\\' in user: user = user.split('\\')[1] + if not user: + user = '' user = user[:9] vms = bytes2human(p.info['memory_info'].vms) if \ p.info['memory_info'] is not None else '' @@ -85,7 +87,7 @@ def main(): status = p.info['status'][:5] if p.info['status'] else '' line = templ % ( - user[:10], + user, p.info['pid'], memp, vms, From 8b6ffeec13306701c788bc0442bc77cd05651974 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 8 May 2019 20:29:00 +0800 Subject: [PATCH 0298/1714] update CREDITS + small style fix --- CREDITS | 2 +- HISTORY.rst | 13 +++++++++++++ docs/index.rst | 1 + psutil/__init__.py | 2 +- psutil/_psaix.py | 6 +++--- psutil/tests/test_linux.py | 5 ----- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/CREDITS b/CREDITS index 20fe89627d..ca7895c5eb 100644 --- a/CREDITS +++ b/CREDITS @@ -57,7 +57,7 @@ N: Arnon Yaari (wiggin15) W: https://github.com/wiggin15 D: AIX implementation, expert on multiple fronts I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408, - 1329. + 1329, 1276, 1494. N: Alex Manuskin W: https://github.com/amanusk diff --git a/HISTORY.rst b/HISTORY.rst index 26fc5b0efb..36977cbc53 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,18 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.6.3 +===== + +XXXX-XX-XX + +**Enhancements** + +- 1494_: [AIX] added support for Process.environ(). (patch by Arnon Yaari) + +**Bug fixes** + +- 1276_: [AIX] can't get whole cmdline(). (patch by Arnon Yaari) + 5.6.2 ===== diff --git a/docs/index.rst b/docs/index.rst index 967d7bdfc9..ede2b3f997 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1136,6 +1136,7 @@ Process class .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support + .. versionchanged:: 5.6.3 added AIX suport .. method:: create_time() diff --git a/psutil/__init__.py b/psutil/__init__.py index 5d37c5c0cf..0e4a8de9cd 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -221,7 +221,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.2" +__version__ = "5.6.3" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 5810e3b49f..b24325d19a 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -396,8 +396,8 @@ def exe(self): # 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)): + 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) @@ -405,7 +405,7 @@ def exe(self): 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)): + os.access(possible_exe, os.X_OK)): return possible_exe return '' diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index a4b79e4fc5..d732e90d7e 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -696,11 +696,6 @@ def test_emulate_none(self): @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): From 2e90cdfaf52c6b245db926fffbb728d2ae1c856f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 8 May 2019 20:53:36 +0800 Subject: [PATCH 0299/1714] README: add some badges --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index c7003547e4..34a1c894bd 100644 --- a/README.rst +++ b/README.rst @@ -26,10 +26,23 @@ :target: https://github.com/giampaolo/psutil/ :alt: Github stars +.. image:: https://img.shields.io/github/forks/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/ + :alt: Github forks + .. image:: https://img.shields.io/pypi/l/psutil.svg :target: https://pypi.org/project/psutil :alt: License +.. image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg + :target: https://pypi.org/project/psutil + :alt: Contributors + +.. image:: https://img.shields.io/pypi/pyversions/psutil.svg + :target: https://pypi.org/project/psutil + :alt: Supported Python versions + +----- Quick links =========== From 5a398984d709d750da1fc0e450d72c771e18f393 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 8 May 2019 20:59:19 +0800 Subject: [PATCH 0300/1714] refactor shields --- README.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 34a1c894bd..dba93a06f7 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,18 @@ :target: https://pepy.tech/project/psutil :alt: Downloads +.. image:: https://img.shields.io/github/stars/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/stargazers + :alt: Github stars + +.. image:: https://img.shields.io/github/forks/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/network/members + :alt: Github forks + +.. image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/graphs/contributors + :alt: Contributors + .. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20macOS :target: https://travis-ci.org/giampaolo/psutil :alt: Linux tests (Travis) @@ -22,26 +34,14 @@ :target: https://pypi.org/project/psutil :alt: Latest version -.. image:: https://img.shields.io/github/stars/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/ - :alt: Github stars - -.. image:: https://img.shields.io/github/forks/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/ - :alt: Github forks +.. image:: https://img.shields.io/pypi/pyversions/psutil.svg + :target: https://pypi.org/project/psutil + :alt: Supported Python versions .. image:: https://img.shields.io/pypi/l/psutil.svg :target: https://pypi.org/project/psutil :alt: License -.. image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg - :target: https://pypi.org/project/psutil - :alt: Contributors - -.. image:: https://img.shields.io/pypi/pyversions/psutil.svg - :target: https://pypi.org/project/psutil - :alt: Supported Python versions - ----- Quick links From 6dc8c8d6ee3da0b4979520b7c1a3dc4a269d44f0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 13 May 2019 23:31:29 -0700 Subject: [PATCH 0301/1714] fix #1501: guard against NtQueryInformationProcess failing for 'Registry' pseudo process on win 10 --- HISTORY.rst | 3 +++ psutil/arch/windows/process_info.c | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 36977cbc53..6b517766dc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,9 @@ XXXX-XX-XX **Bug fixes** - 1276_: [AIX] can't get whole cmdline(). (patch by Arnon Yaari) +- 1501_: [Windows] Process cmdline() and exe() raise unhandled "WinError 1168 + element not found" exceptions for "Registry" and "Memory Compression" psuedo + processes on Windows 10. 5.6.2 ===== diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 3b3c677edc..ba9966bb39 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -772,6 +772,14 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { 0, &bufLen); + // 0xC0000225 == STATUS_NOT_FOUND, see: + // https://github.com/giampaolo/psutil/issues/1501 + if (status == 0xC0000225) { + AccessDenied("NtQueryInformationProcess(ProcessBasicInformation) -> " + "STATUS_NOT_FOUND translated into PermissionError"); + goto error; + } + if (status != STATUS_BUFFER_OVERFLOW && \ status != STATUS_BUFFER_TOO_SMALL && \ status != STATUS_INFO_LENGTH_MISMATCH) { From f98d627277445282954413cb0e20116e26ff8183 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 May 2019 14:35:06 +0800 Subject: [PATCH 0302/1714] small refactoring --- README.rst | 2 +- psutil/__init__.py | 2 +- psutil/_pslinux.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index dba93a06f7..ba6d7f7fb5 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/l/psutil.svg - :target: https://pypi.org/project/psutil + :target: https://github.com/giampaolo/psutil/blob/master/LICENSE :alt: License ----- diff --git a/psutil/__init__.py b/psutil/__init__.py index 0e4a8de9cd..b400ec8523 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2053,7 +2053,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 diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 33bafd2872..e4bc7d75c1 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1650,7 +1650,7 @@ 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)] + return data.split(sep) @wrap_exceptions def environ(self): From 6d4ad494e671dd00c8f95067a6ec734c92437d25 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 May 2019 14:35:43 +0800 Subject: [PATCH 0303/1714] fix #1501: handle the case where NtQueryInformationProcess fails when dealing with 'Registry' win 10 pseudo process --- HISTORY.rst | 3 +++ psutil/arch/windows/process_info.c | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 36977cbc53..6b517766dc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,9 @@ XXXX-XX-XX **Bug fixes** - 1276_: [AIX] can't get whole cmdline(). (patch by Arnon Yaari) +- 1501_: [Windows] Process cmdline() and exe() raise unhandled "WinError 1168 + element not found" exceptions for "Registry" and "Memory Compression" psuedo + processes on Windows 10. 5.6.2 ===== diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 3b3c677edc..ba9966bb39 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -772,6 +772,14 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { 0, &bufLen); + // 0xC0000225 == STATUS_NOT_FOUND, see: + // https://github.com/giampaolo/psutil/issues/1501 + if (status == 0xC0000225) { + AccessDenied("NtQueryInformationProcess(ProcessBasicInformation) -> " + "STATUS_NOT_FOUND translated into PermissionError"); + goto error; + } + if (status != STATUS_BUFFER_OVERFLOW && \ status != STATUS_BUFFER_TOO_SMALL && \ status != STATUS_INFO_LENGTH_MISMATCH) { From 9027714bfc1b1af508db8938429a6a8061a92791 Mon Sep 17 00:00:00 2001 From: Michael Yoo Date: Tue, 28 May 2019 11:52:01 +1000 Subject: [PATCH 0304/1714] Update index.rst (#1516) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index ede2b3f997..d693b9f75f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1324,7 +1324,7 @@ Process class Return process I/O statistics as a named tuple. For Linux you can refer to - `/proc filesysem documentation `__. + `/proc filesystem documentation `__. - **read_count**: the number of read operations performed (cumulative). This is supposed to count the number of read-related syscalls such as From e62882adc1dc0896b3ee458b7e2ede0f3337b991 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 May 2019 17:22:00 +0800 Subject: [PATCH 0305/1714] add more badges --- HISTORY.rst | 4 ++-- README.rst | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6b517766dc..fdf8a24774 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,7 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.6.3 -===== +In development +============== XXXX-XX-XX diff --git a/README.rst b/README.rst index ba6d7f7fb5..0b633206f0 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://pepy.tech/badge/psutil/month +.. image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil :alt: Downloads @@ -26,6 +26,10 @@ :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) +.. image:: https://api.codacy.com/project/badge/Grade/ce63e7f7f69d44b5b59682196e6fbfca + :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade + :alt: Code quality + .. image:: https://readthedocs.org/projects/psutil/badge/?version=latest :target: http://psutil.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status @@ -38,6 +42,10 @@ :target: https://pypi.org/project/psutil :alt: Supported Python versions +.. image:: https://repology.org/badge/tiny-repos/python:psutil.svg + :target: https://repology.org/metapackage/python:psutil/versions + :alt: Binary packages + .. image:: https://img.shields.io/pypi/l/psutil.svg :target: https://github.com/giampaolo/psutil/blob/master/LICENSE :alt: License From 41a20b4736a67a21a898787cb6299ba3eafeabcd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 May 2019 17:35:51 +0800 Subject: [PATCH 0306/1714] add more badges --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0b633206f0..213a484804 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) -.. image:: https://api.codacy.com/project/badge/Grade/ce63e7f7f69d44b5b59682196e6fbfca +.. image:: https://img.shields.io/codacy/grade/ce63e7f7f69d44b5b59682196e6fbfca.svg :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade :alt: Code quality @@ -50,6 +50,9 @@ :target: https://github.com/giampaolo/psutil/blob/master/LICENSE :alt: License +.. image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=social + :alt: Twitter Follow + ----- Quick links From 9d5ee03b5a0a088ce0e1426ad59137466116a895 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 May 2019 10:59:48 +0800 Subject: [PATCH 0307/1714] #1515: document how to get a list of valid attrs names for as_dict() --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index d693b9f75f..7a2348726c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1169,6 +1169,10 @@ Process class >>> p = psutil.Process() >>> p.as_dict(attrs=['pid', 'name', 'username']) {'username': 'giampaolo', 'pid': 12366, 'name': 'python'} + >>> + >>> # get a list of valid attrs names + >>> list(psutil.Process().as_dict().keys()) + ['status', 'cpu_num', 'num_ctx_switches', 'pid', 'memory_full_info', 'connections', 'cmdline', 'create_time', 'ionice', 'num_fds', 'memory_maps', 'cpu_percent', 'terminal', 'ppid', 'cwd', 'nice', 'username', 'cpu_times', 'io_counters', 'memory_info', 'threads', 'open_files', 'name', 'num_threads', 'exe', 'uids', 'gids', 'cpu_affinity', 'memory_percent', 'environ'] .. versionchanged:: 3.0.0 *ad_value* is used also when incurring into From 71a7bb56dbbfb5a70b575a939a69ba7f815745c5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 May 2019 15:26:30 +0800 Subject: [PATCH 0308/1714] add more badges --- README.rst | 39 ++++++++++++++++++++++----------------- setup.py | 4 ---- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 213a484804..7ee41fad51 100644 --- a/README.rst +++ b/README.rst @@ -1,56 +1,61 @@ -.. image:: https://img.shields.io/pypi/dm/psutil.svg +| |downloads| |stars| |forks| |contributors| |coverage| |quality| +| |version| |py-versions| |packages| |license| +| |travis| |appveyor| |doc| |twitter| + +.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil :alt: Downloads -.. image:: https://img.shields.io/github/stars/giampaolo/psutil.svg +.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg :target: https://github.com/giampaolo/psutil/stargazers :alt: Github stars -.. image:: https://img.shields.io/github/forks/giampaolo/psutil.svg +.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg :target: https://github.com/giampaolo/psutil/network/members :alt: Github forks -.. image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg +.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg :target: https://github.com/giampaolo/psutil/graphs/contributors :alt: Contributors -.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20macOS +.. |quality| image:: https://img.shields.io/codacy/grade/ce63e7f7f69d44b5b59682196e6fbfca.svg + :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade + :alt: Code quality + +.. |travis| image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=linux%20/%20osx :target: https://travis-ci.org/giampaolo/psutil :alt: Linux tests (Travis) -.. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows +.. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=windows :target: https://ci.appveyor.com/project/giampaolo/psutil :alt: Windows tests (Appveyor) -.. image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master +.. |coverage| image:: https://img.shields.io/coveralls/github/giampaolo/psutil.svg?label=test%20coverage :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) -.. image:: https://img.shields.io/codacy/grade/ce63e7f7f69d44b5b59682196e6fbfca.svg - :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade - :alt: Code quality - -.. image:: https://readthedocs.org/projects/psutil/badge/?version=latest +.. |doc| image:: https://readthedocs.org/projects/psutil/badge/?version=latest :target: http://psutil.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi +.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi :target: https://pypi.org/project/psutil :alt: Latest version -.. image:: https://img.shields.io/pypi/pyversions/psutil.svg +.. |py-versions| image:: https://img.shields.io/pypi/pyversions/psutil.svg :target: https://pypi.org/project/psutil :alt: Supported Python versions -.. image:: https://repology.org/badge/tiny-repos/python:psutil.svg +.. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg :target: https://repology.org/metapackage/python:psutil/versions :alt: Binary packages -.. image:: https://img.shields.io/pypi/l/psutil.svg +.. |license| image:: https://img.shields.io/pypi/l/psutil.svg :target: https://github.com/giampaolo/psutil/blob/master/LICENSE :alt: License -.. image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=social +.. |twitter| image:: https://img.shields.io/twitter/url/https/grodola.svg?label=follow + :target: https://twitter.com/grodola :alt: Twitter Follow ----- diff --git a/setup.py b/setup.py index 2c3d9b36e5..693bd89d87 100755 --- a/setup.py +++ b/setup.py @@ -320,10 +320,6 @@ def main(): 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python', From a81c4e3991e3670bf5affba8bbbbdd0a00f7cabe Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 May 2019 20:33:50 +0800 Subject: [PATCH 0309/1714] skip pure-python mem test on linux --- psutil/tests/test_memory_leaks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index dde50a57f1..543dbf7100 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -467,6 +467,7 @@ def test_cpu_times(self): def test_per_cpu_times(self): self.execute(psutil.cpu_times, percpu=True) + @skip_if_linux() def test_cpu_stats(self): self.execute(psutil.cpu_stats) From 2c8e21a81713b2d3ab843543751c2fe9591d88f6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 May 2019 21:37:31 +0800 Subject: [PATCH 0310/1714] update LICENSE so that it's recognized as BSD-3 by github --- LICENSE | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index e91b1359a2..502b1d4614 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -psutil is distributed under BSD license reproduced below. +psutil is distributed under BSD 3-Clause license reproduced below. Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola' All rights reserved. @@ -8,9 +8,11 @@ are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the psutil authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. From 7069a81a7f6da8192e12f1bbd47c4d928c99be93 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 May 2019 21:38:38 +0800 Subject: [PATCH 0311/1714] add tidelift sponsorship --- .github/FUNDING.yml | 9 ++++++++ README.rst | 52 +++++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..c39b2b6145 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,9 @@ +# These are supported funding model platforms + +tidelift: "pypi/psutil" +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +custom: # Replace with a single custom sponsorship URL diff --git a/README.rst b/README.rst index 7ee41fad51..4f1a71c348 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ | |downloads| |stars| |forks| |contributors| |coverage| |quality| | |version| |py-versions| |packages| |license| -| |travis| |appveyor| |doc| |twitter| +| |travis| |appveyor| |doc| |twitter| |tidelift| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil @@ -54,10 +54,14 @@ :target: https://github.com/giampaolo/psutil/blob/master/LICENSE :alt: License -.. |twitter| image:: https://img.shields.io/twitter/url/https/grodola.svg?label=follow +.. |twitter| image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=flat&logo=twitter&logoColor=4FADFF :target: https://twitter.com/grodola :alt: Twitter Follow +.. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme + :alt: Tidelift + ----- Quick links @@ -73,7 +77,6 @@ Quick links - `Development guide `_ - `What's new `_ - Summary ======= @@ -82,9 +85,8 @@ retrieving information on **running processes** and **system utilization** (CPU, memory, disks, network, sensors) in Python. It is useful mainly for **system monitoring**, **profiling and limiting process resources** and **management of running processes**. -It implements many functionalities offered by UNIX command line tools such as: -ps, top, lsof, netstat, ifconfig, who, df, kill, free, nice, ionice, iostat, -iotop, uptime, pidof, tty, taskset, pmap. +It implements many functionalities offered by classic UNIX command line tools +such as *ps, top, iotop, lsof, netstat, ifconfig, free* and others. psutil currently supports the following platforms: - **Linux** @@ -96,24 +98,28 @@ psutil currently supports the following platforms: ...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy `__ is also known to work. +Professional support +==================== -Author -====== - -psutil was created and is maintained by -`Giampaolo Rodola `__ and it -received many useful `contributions `__ -over the years. -A lot of time and effort went into making psutil as it is right now. -If you feel psutil is useful to you or your business and want to support its -future development consider making a small donation: +.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png + :width: 100 + :alt: Tidelift + :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme -.. image:: http://www.paypal.com/en_US/i/btn/x-click-but04.gif - :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 - :alt: Donate via PayPal +.. list-table:: + :widths: 10 100 -Don't want to donate money? Then maybe you could `write me a recommendation on Linkedin `_. + * - |tideliftlogo| + - Professional support for psutil is available as part of the + `Tidelift Subscription`_. + Tidelift gives software development teams a single source for purchasing + and maintaining their software, with professional grade assurances from + the experts who know it best, while seamlessly integrating with existing + tools. + By subscribing you will help me (`Giampaolo Rodola`_) support psutil + future development. Alternatively consider making a small `donation`_. +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme Example applications ==================== @@ -134,7 +140,7 @@ Projects using psutil psutil has roughly the following monthly downloads: -.. image:: https://pepy.tech/badge/psutil/month +.. image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil :alt: Downloads @@ -497,3 +503,7 @@ Windows services 'start_type': 'manual', 'status': 'stopped', 'username': 'NT AUTHORITY\\LocalService'} + + +.. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html +.. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 From f5df901843097d20246a3171dac3e0ea670f974e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 May 2019 21:43:43 +0800 Subject: [PATCH 0312/1714] update LICENSE so that it's recognized as BSD-3 by github --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 502b1d4614..0bf4a7fc04 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -psutil is distributed under BSD 3-Clause license reproduced below. +BSD 3-Clause License Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola' All rights reserved. From 1087e2418a35cccf6cdad6c2207308a915cf8a1a Mon Sep 17 00:00:00 2001 From: Kamil Rytarowski Date: Tue, 11 Jun 2019 06:04:16 +0200 Subject: [PATCH 0313/1714] NetBSD fixes (#1526) * Fix psutil_get_cmd_args() for NetBSD Do not overallocate the buffer prompting for KERN_ARGMAX. It also fixes the code as KERN_ARGMAX was received into a size_t type, while kernel returned int. It caused argmax to contain garbage and allocation was randomly crashing with new jemalloc in the basesystem. New code prompts for exact buffer size before allocation and stores argv[] inside it. Bug investigated by Leonardo Taccari. * Stop including sys/user.h for NetBSD This header was removed from the OS as it was empty. --- psutil/_psutil_bsd.c | 2 ++ psutil/arch/netbsd/specific.c | 28 +++++++++++----------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index efb933fb2f..0f899ef51b 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -30,7 +30,9 @@ #include #include #include +#if !defined(__NetBSD__) #include +#endif #include #include #include diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index cab60d6082..195896f2fb 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -22,7 +22,6 @@ #include #include #include -#include #include #include // for swap_mem #include @@ -313,40 +312,35 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { char * psutil_get_cmd_args(pid_t pid, size_t *argsize) { int mib[4]; - ssize_t st; - size_t argmax; - size_t size; - char *procargs = NULL; + int st; + size_t len; + char *procargs; mib[0] = CTL_KERN; - mib[1] = KERN_ARGMAX; + mib[1] = KERN_PROC_ARGS; + mib[2] = pid; + mib[3] = KERN_PROC_ARGV; + len = 0; - size = sizeof(argmax); - st = sysctl(mib, 2, &argmax, &size, NULL, 0); + st = sysctl(mib, __arraycount(mib), NULL, &len, NULL, 0); if (st == -1) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } - procargs = (char *)malloc(argmax); + procargs = (char *)malloc(len); if (procargs == NULL) { PyErr_NoMemory(); return NULL; } - - mib[0] = CTL_KERN; - mib[1] = KERN_PROC_ARGS; - mib[2] = pid; - mib[3] = KERN_PROC_ARGV; - - st = sysctl(mib, 4, procargs, &argmax, NULL, 0); + st = sysctl(mib, __arraycount(mib), procargs, &len, NULL, 0); if (st == -1) { free(procargs); PyErr_SetFromErrno(PyExc_OSError); return NULL; } - *argsize = argmax; + *argsize = len; return procargs; } From f50df9657591c52a0a46c5af3d14998c6471065f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Jun 2019 12:23:00 +0800 Subject: [PATCH 0314/1714] update HISTORY - pre release --- CREDITS | 5 +++++ HISTORY.rst | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CREDITS b/CREDITS index ca7895c5eb..5469b0b1c9 100644 --- a/CREDITS +++ b/CREDITS @@ -617,3 +617,8 @@ N: agnewee W: https://github.com/Agnewee C: China I: 1491 + +N: Kamil Rytarowski +W: https://github.com/krytarowski +C: Poland +I: 1526 diff --git a/HISTORY.rst b/HISTORY.rst index fdf8a24774..f8147bfed1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -In development -============== +5.6.3 +===== -XXXX-XX-XX +2019-06-11 **Enhancements** @@ -15,6 +15,8 @@ XXXX-XX-XX - 1501_: [Windows] Process cmdline() and exe() raise unhandled "WinError 1168 element not found" exceptions for "Registry" and "Memory Compression" psuedo processes on Windows 10. +- 1526_: [NetBSD] process cmdline() could raise MemoryError. (patch by + Kamil Rytarowski) 5.6.2 ===== From c2f30702a4db0f4dfbf88842c07532cc187daf5a Mon Sep 17 00:00:00 2001 From: Kamil Rytarowski Date: Wed, 12 Jun 2019 06:38:40 +0200 Subject: [PATCH 0315/1714] Implement psutil_proc_cwd for NetBSD (#1530) Pick KERN_PROC_CWD that is available in 8.99.43 and fallback for older versions to readlink("/proc/$PID/cwd"). --- psutil/_psbsd.py | 5 +---- psutil/_psutil_bsd.c | 2 +- psutil/arch/netbsd/specific.c | 35 +++++++++++++++++++++++++++++++++++ psutil/arch/netbsd/specific.h | 1 + 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 3d9dfdab69..e90bdc8419 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -844,10 +844,7 @@ def cwd(self): # 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 HAS_PROC_OPEN_FILES: + 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 diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 0f899ef51b..74fe5922e2 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -921,9 +921,9 @@ PsutilMethods[] = { #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) {"proc_connections", psutil_proc_connections, METH_VARARGS, "Return connections opened by process"}, +#endif {"proc_cwd", psutil_proc_cwd, METH_VARARGS, "Return process current working directory."}, -#endif #if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS, "Return the number of file descriptors opened by this process"}, diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 195896f2fb..050652c3fa 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -111,6 +111,41 @@ kinfo_getfile(pid_t pid, int* cnt) { return kf; } +PyObject * +psutil_proc_cwd(PyObject *self, PyObject *args) { + long pid; + + char path[MAXPATHLEN]; + size_t pathlen = sizeof path; + + if (! PyArg_ParseTuple(args, "l", &pid)) + return NULL; + +#ifdef KERN_PROC_CWD + int name[] = { CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; + if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } +#else + char *buf; + if (asprintf(&buf, "/proc/%d/cwd", (int)pid) < 0) { + PyErr_NoMemory(); + return NULL; + } + + ssize_t len = readlink(buf, path, sizeof(path) - 1); + free(buf); + if (len == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + path[len] = '\0'; +#endif + + return PyUnicode_DecodeFSDefault(path); +} + // XXX: This is no longer used as per // https://github.com/giampaolo/psutil/pull/557#issuecomment-171912820 diff --git a/psutil/arch/netbsd/specific.h b/psutil/arch/netbsd/specific.h index 96ad9f7d26..391ed164a4 100644 --- a/psutil/arch/netbsd/specific.h +++ b/psutil/arch/netbsd/specific.h @@ -26,3 +26,4 @@ PyObject* psutil_disk_io_counters(PyObject* self, PyObject* args); PyObject* psutil_proc_exe(PyObject* self, PyObject* args); PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); From 48cb36cdff2c83be6c3d265f8a10040ec74d10a0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Jun 2019 12:41:30 +0800 Subject: [PATCH 0316/1714] update docs / HISTORY / CREDITS / @krytarowski for #1530 --- CREDITS | 2 +- HISTORY.rst | 9 +++++++++ docs/index.rst | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 5469b0b1c9..4e30aba322 100644 --- a/CREDITS +++ b/CREDITS @@ -621,4 +621,4 @@ I: 1491 N: Kamil Rytarowski W: https://github.com/krytarowski C: Poland -I: 1526 +I: 1526, 1530 diff --git a/HISTORY.rst b/HISTORY.rst index f8147bfed1..c3d2960e55 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,14 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.6.4 +===== + +XXXX-XX-XX + +**Enhancements** + +- 1530_: [NetBSD] add process cwd() support. (patch by Kamil Rytarowski) + 5.6.3 ===== diff --git a/docs/index.rst b/docs/index.rst index 7a2348726c..f068967e59 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1205,6 +1205,8 @@ Process class The process current working directory as an absolute path. + .. versionchanged:: 5.6.4 added support for NetBSD + .. method:: username() The name of the user that owns the process. On UNIX this is calculated by From 1a835ea713154332f4d4db062e3b38b8a0460d17 Mon Sep 17 00:00:00 2001 From: cclauss Date: Wed, 12 Jun 2019 05:42:01 +0100 Subject: [PATCH 0317/1714] Travis CI: The sudo tag is now deprecated in Travis (#1521) --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fdab64f629..b3b0102dc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -sudo: false language: python cache: pip matrix: @@ -10,7 +9,6 @@ matrix: - python: 3.6 - python: 3.7 dist: xenial - sudo: true # macOS - language: generic os: osx From 6b994c625db2abaaadf58a0425f9daaf3e4ad9e5 Mon Sep 17 00:00:00 2001 From: xiaclo Date: Wed, 12 Jun 2019 14:42:55 +1000 Subject: [PATCH 0318/1714] Add handling of missing space in meminfo output (#1517) --- psutil/_pslinux.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e4bc7d75c1..8f3ff7d6dd 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -386,8 +386,9 @@ def virtual_memory(): mems = {} with open_binary('%s/meminfo' % get_procfs_path()) as f: for line in f: - fields = line.split() - mems[fields[0]] = int(fields[1]) * 1024 + name, _, value = line.partition(':') + value_num, _, units = value.partition(' ') + mems[name + ':'] = int(value_num) * 1024 # /proc doc states that the available fields in /proc/meminfo vary # by architecture and compile options, but these 3 values are also From 4b2acfe192e3baf3a10701e132769650e3070f64 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Jun 2019 12:46:29 +0800 Subject: [PATCH 0319/1714] Revert "Add handling of missing space in meminfo output (#1517)" (#1531) This reverts commit 6b994c625db2abaaadf58a0425f9daaf3e4ad9e5. --- psutil/_pslinux.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 8f3ff7d6dd..e4bc7d75c1 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -386,9 +386,8 @@ def virtual_memory(): mems = {} with open_binary('%s/meminfo' % get_procfs_path()) as f: for line in f: - name, _, value = line.partition(':') - value_num, _, units = value.partition(' ') - mems[name + ':'] = int(value_num) * 1024 + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 # /proc doc states that the available fields in /proc/meminfo vary # by architecture and compile options, but these 3 values are also From 5ebd0bad5bc503d7a0c6182b3b0e672510dabccf Mon Sep 17 00:00:00 2001 From: wiggin15 Date: Wed, 12 Jun 2019 11:02:16 +0300 Subject: [PATCH 0320/1714] Fix #1528: [AIX] use correct definition of size of procbuf (#1533) --- psutil/_psutil_aix.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 723d159d4e..8c055a4320 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -180,7 +180,7 @@ psutil_proc_args(PyObject *self, PyObject *args) { } procbuf.pi_pid = pid; - ret = getargs(&procbuf, sizeof(struct procinfo), argbuf, ARG_MAX); + ret = getargs(&procbuf, sizeof(procbuf), argbuf, ARG_MAX); if (ret == -1) { PyErr_SetFromErrno(PyExc_OSError); goto error; @@ -241,7 +241,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } procbuf.pi_pid = pid; - ret = getevars(&procbuf, sizeof(struct procinfo), envbuf, ARG_MAX); + ret = getevars(&procbuf, sizeof(procbuf), envbuf, ARG_MAX); if (ret == -1) { PyErr_SetFromErrno(PyExc_OSError); goto error; From 9be2120e4b6f43a2450c6ddb86fbc1723dd081e5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Jun 2019 17:03:18 +0800 Subject: [PATCH 0321/1714] credits to @wiggin15 for #1528 --- CREDITS | 2 +- HISTORY.rst | 5 +++++ scripts/internal/check_broken_links.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CREDITS b/CREDITS index 4e30aba322..3b1de50dc9 100644 --- a/CREDITS +++ b/CREDITS @@ -57,7 +57,7 @@ N: Arnon Yaari (wiggin15) W: https://github.com/wiggin15 D: AIX implementation, expert on multiple fronts I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408, - 1329, 1276, 1494. + 1329, 1276, 1494, 1528. N: Alex Manuskin W: https://github.com/amanusk diff --git a/HISTORY.rst b/HISTORY.rst index c3d2960e55..b9e08da17a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,11 @@ XXXX-XX-XX - 1530_: [NetBSD] add process cwd() support. (patch by Kamil Rytarowski) +**Bug fixes** + +- 1528_: [AIX] compilation error on AIX 7.2 due to 32 vs 64 bit differences. + (patch by Arnon Yaari) + 5.6.3 ===== diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 3d108d810d..7313481864 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -160,7 +160,7 @@ def parse_c(fname): def parse_generic(fname): - with open(fname) as f: + with open(fname, 'rt', errors='ignore') as f: text = f.read() return find_urls(text) @@ -174,7 +174,7 @@ def get_urls(fname): elif fname.endswith('.c') or fname.endswith('.h'): return parse_c(fname) else: - with open(fname) as f: + with open(fname, 'rt', errors='ignore') as f: if f.readline().strip().startswith('#!/usr/bin/env python'): return parse_py(fname) return parse_generic(fname) From 8e4aab1482471698644d8d1d1541850741530e2e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Jun 2019 13:52:06 +0800 Subject: [PATCH 0322/1714] add pip install --trusted-host opt --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e91ae34244..b14978b216 100644 --- a/Makefile +++ b/Makefile @@ -101,8 +101,8 @@ install-pip: ## Install pip (no-op if already installed). setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade $(DEPS) + $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade --trusted-host files.pythonhosted.org pip + $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade --trusted-host files.pythonhosted.org $(DEPS) # =================================================================== # Tests From 412cb7f845462b0a67b1d2ce9b3aa61c555aa262 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Jun 2019 15:22:49 +0800 Subject: [PATCH 0323/1714] fix bsd test --- psutil/tests/test_bsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 5df8ad2982..e769b72785 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -486,7 +486,7 @@ class NetBSDSpecificTestCase(unittest.TestCase): @staticmethod def parse_meminfo(look_for): - with open('/proc/meminfo', 'rb') as f: + with open('/proc/meminfo', 'rt') as f: for line in f: if line.startswith(look_for): return int(line.split()[1]) * 1024 From 5f4287d17fc6aa2643c4c6e3589c12abd0f1ded9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Jun 2019 16:11:14 +0800 Subject: [PATCH 0324/1714] Connection family/type are not converted to enums (#1535) --- HISTORY.rst | 6 +- docs/index.rst | 13 +- psutil/_common.py | 29 +++- psutil/_psaix.py | 25 +-- psutil/_psbsd.py | 53 ++----- psutil/_pslinux.py | 2 +- psutil/_psosx.py | 16 +- psutil/_pssunos.py | 1 + psutil/_pswindows.py | 16 +- psutil/tests/__init__.py | 85 +--------- psutil/tests/test_connections.py | 257 ++++++++++++++++++++++--------- psutil/tests/test_contracts.py | 30 ++-- psutil/tests/test_misc.py | 2 + 13 files changed, 263 insertions(+), 272 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b9e08da17a..044b7bc04d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,14 +5,12 @@ XXXX-XX-XX -**Enhancements** - -- 1530_: [NetBSD] add process cwd() support. (patch by Kamil Rytarowski) - **Bug fixes** - 1528_: [AIX] compilation error on AIX 7.2 due to 32 vs 64 bit differences. (patch by Arnon Yaari) +- 1535_: 'type' and 'family' fields returned by net_connections() are not + always turned into enums. 5.6.3 ===== diff --git a/docs/index.rst b/docs/index.rst index f068967e59..8c0ee74a51 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -513,7 +513,8 @@ Network to obtain a usable socket object. On Windows and SunOS this is always set to ``-1``. - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. - - **type**: the address type, either `SOCK_STREAM`_ or `SOCK_DGRAM`_. + - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or + `SOCK_SEQPACKET`_. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -1784,7 +1785,8 @@ Process class always set to ``-1``. - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. - - **type**: the address type, either `SOCK_STREAM`_ or `SOCK_DGRAM`_. + - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or + `SOCK_SEQPACKET`_. . - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -2605,7 +2607,11 @@ take a look at the `development guide`_. Timeline ======== -- 2019-0426: +- 2019-06-11: + `5.6.3 `__ - + `what's new `__ - + `diff `__ +- 2019-04-26: `5.6.2 `__ - `what's new `__ - `diff `__ @@ -2946,6 +2952,7 @@ Timeline .. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. .. _`signal module`: https://docs.python.org//library/signal.html .. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM +.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET .. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM .. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd .. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen diff --git a/psutil/_common.py b/psutil/_common.py index e3b45417ae..4a006c1dcf 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -64,7 +64,7 @@ '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", - 'bytes2human', + 'bytes2human', 'conn_to_ntuple', ] @@ -257,8 +257,6 @@ class BatteryTime(enum.IntEnum): "unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]), }) -del AF_INET, AF_UNIX, SOCK_STREAM, SOCK_DGRAM - # =================================================================== # --- utils @@ -447,7 +445,7 @@ def sockfam_to_enum(num): else: # pragma: no cover try: return socket.AddressFamily(num) - except (ValueError, AttributeError): + except ValueError: return num @@ -459,11 +457,30 @@ 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. diff --git a/psutil/_psaix.py b/psutil/_psaix.py index b24325d19a..3a949d2572 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -14,20 +14,17 @@ 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 conn_to_ntuple from ._common import get_procfs_path 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 usage_percent from ._compat import PY3 @@ -220,27 +217,17 @@ def net_connections(kind, _pid=-1): % (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(): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index e90bdc8419..9964f5ff42 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -10,22 +10,19 @@ import os import xml.etree.ElementTree as ET from collections import namedtuple -from socket import AF_INET from collections import defaultdict 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 conn_to_ntuple 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 usage_percent from ._compat import which @@ -399,22 +396,8 @@ def net_connections(kind): 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) + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES, pid) ret.add(nt) return list(ret) @@ -769,25 +752,15 @@ def connections(self, kind='inet'): if NETBSD: families, types = conn_tmap[kind] - ret = set() + ret = [] 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) + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES) + ret.append(nt) self._assert_alive() return list(ret) @@ -796,18 +769,14 @@ def connections(self, kind='inet'): 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) + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES) ret.append(nt) + if OPENBSD: self._assert_alive() + return ret @wrap_exceptions diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e4bc7d75c1..50094b40e2 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -933,7 +933,7 @@ def process_unix(file, family, inodes, filter_pid=None): path = tokens[-1] else: path = "" - type_ = int(type_) + 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/ diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 7459a0f3d8..54e5720274 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -8,20 +8,17 @@ 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 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 @@ -529,15 +526,8 @@ def connections(self, kind='inet'): 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 diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 6d7fda8555..07298a7387 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -262,6 +262,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) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 3f1319806f..cf938a6f19 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -33,14 +33,13 @@ raise from ._common import conn_tmap +from ._common import conn_to_ntuple 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 parse_environ_block -from ._common import sockfam_to_enum -from ._common import socktype_to_enum from ._common import usage_percent from ._compat import long from ._compat import lru_cache @@ -388,17 +387,8 @@ def net_connections(kind, _pid=-1): 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) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 796817195f..5e4f37b39d 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -32,10 +32,10 @@ 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 AIX from psutil import MACOS from psutil import POSIX from psutil import SUNOS @@ -91,7 +91,7 @@ # sync primitives 'call_until', 'wait_for_pid', 'wait_for_file', # network - 'check_connection_ntuple', 'check_net_address', + 'check_net_address', 'get_free_port', 'unix_socket_path', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', 'unix_socketpair', 'create_sockets', # compat @@ -175,6 +175,7 @@ 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 @@ -212,7 +213,6 @@ def attempt(exe): 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() @@ -403,7 +403,7 @@ def create_zombie_proc(): with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock: sock.settimeout(GLOBAL_TIMEOUT) sock.bind(unix_file) - sock.listen(1) + sock.listen(5) pyrun(src) conn, _ = sock.accept() try: @@ -898,7 +898,7 @@ def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(addr) if type == socket.SOCK_STREAM: - sock.listen(10) + sock.listen(5) return sock except Exception: sock.close() @@ -913,7 +913,7 @@ def bind_unix_socket(name, type=socket.SOCK_STREAM): try: sock.bind(name) if type == socket.SOCK_STREAM: - sock.listen(10) + sock.listen(5) except Exception: sock.close() raise @@ -926,7 +926,7 @@ def tcp_socketpair(family, addr=("", 0)): """ with contextlib.closing(socket.socket(family, SOCK_STREAM)) as ll: ll.bind(addr) - ll.listen(10) + ll.listen(5) addr = ll.getsockname() c = socket.socket(family, SOCK_STREAM) try: @@ -1022,77 +1022,6 @@ def check_net_address(addr, family): 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 # =================================================================== diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 68eea7845d..5fda78cbf1 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -6,6 +6,8 @@ """Tests for net_connections() and Process.connections() APIs.""" +import contextlib +import errno import os import socket import textwrap @@ -29,14 +31,16 @@ 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 check_net_address from psutil.tests import create_sockets +from psutil.tests import enum 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 SKIP_SYSCONS from psutil.tests import tcp_socketpair from psutil.tests import TESTFN from psutil.tests import TRAVIS @@ -47,6 +51,7 @@ thisproc = psutil.Process() +SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) class Base(object): @@ -67,6 +72,122 @@ def tearDown(self): cons = thisproc.connections(kind='all') assert not cons, cons + 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) + + def check_connection_ntuple(self, conn): + """Check validity of a connection namedtuple.""" + def check_ntuple(conn): + has_pid = len(conn) == 7 + self.assertIn(len(conn), (6, 7)) + self.assertEqual(conn[0], conn.fd) + self.assertEqual(conn[1], conn.family) + self.assertEqual(conn[2], conn.type) + self.assertEqual(conn[3], conn.laddr) + self.assertEqual(conn[4], conn.raddr) + self.assertEqual(conn[5], conn.status) + if has_pid: + self.assertEqual(conn[6], conn.pid) + + def check_family(conn): + self.assertIn(conn.family, (AF_INET, AF_INET6, AF_UNIX)) + 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: + self.assertEqual(conn.status, psutil.CONN_NONE) + + def check_type(conn): + # SOCK_SEQPACKET may happen in case of AF_UNIX socks + self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET)) + if enum is not None: + assert isinstance(conn.type, enum.IntEnum), conn + else: + assert isinstance(conn.type, int), conn + if conn.type == SOCK_DGRAM: + self.assertEqual(conn.status, psutil.CONN_NONE) + + 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): + self.assertIsInstance(addr, tuple) + if not addr: + continue + self.assertIsInstance(addr.port, int) + assert 0 <= addr.port <= 65535, addr.port + check_net_address(addr.ip, conn.family) + elif conn.family == AF_UNIX: + self.assertIsInstance(addr, str) + + def check_status(conn): + self.assertIsInstance(conn.status, str) + valids = [getattr(psutil, x) for x in dir(psutil) + if x.startswith('CONN_')] + self.assertIn(conn.status, valids) + if conn.family in (AF_INET, AF_INET6) and conn.type == SOCK_STREAM: + self.assertNotEqual(conn.status, psutil.CONN_NONE) + else: + self.assertEqual(conn.status, psutil.CONN_NONE) + + check_ntuple(conn) + check_family(conn) + check_type(conn) + check_addrs(conn) + check_status(conn) + + +class TestBase(Base, unittest.TestCase): + + @unittest.skipIf(SKIP_SYSCONS, "requires root") + def test_system(self): + with create_sockets(): + for conn in psutil.net_connections(kind='all'): + self.check_connection_ntuple(conn) + + def test_process(self): + with create_sockets(): + for conn in psutil.Process().connections(kind='all'): + self.check_connection_ntuple(conn) + + def test_invalid_kind(self): + self.assertRaises(ValueError, thisproc.connections, kind='???') + self.assertRaises(ValueError, psutil.net_connections, kind='???') + + +class TestUnconnectedSockets(Base, unittest.TestCase): + """Tests sockets which are open but not connected to anything.""" + def get_conn_from_sock(self, sock): cons = thisproc.connections(kind='all') smap = dict([(c.fd, c) for c in cons]) @@ -80,14 +201,13 @@ def get_conn_from_sock(self, sock): self.assertEqual(smap[sock.fileno()].fd, sock.fileno()) return cons[0] - def check_socket(self, sock, conn=None): + 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). """ - if conn is None: - conn = self.get_conn_from_sock(sock) - check_connection_ntuple(conn) + conn = self.get_conn_from_sock(sock) + self.check_connection_ntuple(conn) # fd, family, type if conn.fd != -1: @@ -113,38 +233,9 @@ def check_socket(self, sock, conn=None): # 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) + self.compare_procsys_connections(os.getpid(), cons, kind='all') 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: @@ -192,12 +283,7 @@ def test_unix_udp(self): self.assertEqual(conn.status, psutil.CONN_NONE) -# ===================================================================== -# --- Test connected sockets -# ===================================================================== - - -class TestConnectedSocketPairs(Base, unittest.TestCase): +class TestConnectedSocket(Base, unittest.TestCase): """Test socket pairs which are are actually connected to each other. """ @@ -257,12 +343,58 @@ def test_unix(self): server.close() client.close() + +class TestFilters(Base, unittest.TestCase): + + def test_filters(self): + def check(kind, families, types): + for conn in thisproc.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): 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.check_connection_ntuple(conn) self.assertEqual(conn.family, family) self.assertEqual(conn.type, type) self.assertEqual(conn.laddr, laddr) @@ -284,7 +416,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): import socket, time s = socket.socket($family, socket.SOCK_STREAM) s.bind(('$addr', 0)) - s.listen(1) + s.listen(5) with open('$testfn', 'w') as f: f.write(str(s.getsockname()[:2])) time.sleep(60) @@ -352,13 +484,8 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): 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)) + def test_count(self): + with create_sockets(): # tcp cons = thisproc.connections(kind='tcp') self.assertEqual(len(cons), 2 if supports_ipv6() else 1) @@ -406,8 +533,9 @@ def test_multi_sockets_filtering(self): for conn in cons: self.assertEqual(conn.family, AF_INET6) self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - # unix - if HAS_CONNECTIONS_UNIX: + # unix (skipped on NetBSD because by default the Python process + # creates a connection to '/var/run/log' UNIX socket) + if HAS_CONNECTIONS_UNIX and not NETBSD: cons = thisproc.connections(kind='unix') self.assertEqual(len(cons), 3) for conn in cons: @@ -415,23 +543,17 @@ def test_multi_sockets_filtering(self): self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) -# ===================================================================== -# --- Miscellaneous tests -# ===================================================================== - - +@unittest.skipIf(SKIP_SYSCONS, "requires root") 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) + self.check_connection_ntuple(conn) with create_sockets(): from psutil._common import conn_tmap @@ -444,16 +566,6 @@ def check(cons, families, types_): 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): @@ -495,11 +607,6 @@ def test_multi_sockets_procs(self): self.assertEqual(len(p.connections('all')), expected) -# ===================================================================== -# --- Miscellaneous tests -# ===================================================================== - - class TestMisc(unittest.TestCase): def test_connection_constants(self): diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 20da8241f3..cb4a2b963b 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -15,7 +15,6 @@ import time import traceback import warnings -from contextlib import closing from psutil import AIX from psutil import BSD @@ -29,20 +28,17 @@ 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 create_sockets 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 SKIP_SYSCONS 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 @@ -226,16 +222,13 @@ def test_disk_partitions(self): 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) + @unittest.skipIf(SKIP_SYSCONS, "requires root") 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) + 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. @@ -560,9 +553,10 @@ def num_fds(self, ret, proc): self.assertGreaterEqual(ret, 0) def connections(self, ret, proc): - self.assertEqual(len(ret), len(set(ret))) - for conn in ret: - check_connection_ntuple(conn) + with create_sockets(): + self.assertEqual(len(ret), len(set(ret))) + for conn in ret: + assert is_namedtuple(conn) def cwd(self, ret, proc): if ret: # 'ret' can be None or empty diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 29e1e41e60..6f923bed77 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -20,6 +20,7 @@ import stat from psutil import LINUX +from psutil import NETBSD from psutil import POSIX from psutil import WINDOWS from psutil._common import memoize @@ -1011,6 +1012,7 @@ def tcp_tcp_socketpair(self): self.assertNotEqual(client.getsockname(), addr) @unittest.skipIf(not POSIX, "POSIX only") + @unittest.skipIf(NETBSD, "/var/run/log UNIX socket opened by default") def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() From 6bb5a30dbf8335db8d9b8c4e81d652f055deea6a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Jun 2019 16:53:17 +0800 Subject: [PATCH 0325/1714] [NetBSD] cmdline() raise ZombieProcess when unable to decode chars (#1536) * bug #1536 / NetBSD / cmdline: treat EINVAL as 'return []' This happens with unicode test, meaning the C routine it's unable to decode the unicode chars. Also, fix a bug introduced in 1530 (C impl of cwd()) which does not take ENOENT into account. --- HISTORY.rst | 2 ++ psutil/_psbsd.py | 7 +++---- psutil/arch/netbsd/specific.c | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 044b7bc04d..1f83b02cda 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,8 @@ XXXX-XX-XX (patch by Arnon Yaari) - 1535_: 'type' and 'family' fields returned by net_connections() are not always turned into enums. +- 1536_: [NetBSD] process cmdline() erroneously raise ZombieProcess error if + cmdline has non encodable chars. 5.6.3 ===== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 9964f5ff42..978ad6c8f3 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -648,10 +648,9 @@ def cmdline(self): 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: - raise ZombieProcess(self.pid, self._name, self._ppid) + # XXX: this happens with unicode tests. It means the C + # routine is unable to decode invalid unicode chars. + return [] else: raise else: diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 050652c3fa..25adffcd3b 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -137,7 +137,10 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { ssize_t len = readlink(buf, path, sizeof(path) - 1); free(buf); if (len == -1) { - PyErr_SetFromErrno(PyExc_OSError); + if (errno == ENOENT) + NoSuchProcess(""); + else + PyErr_SetFromErrno(PyExc_OSError); return NULL; } path[len] = '\0'; From 0301cb44137a358b37101a254fb0532a276988a2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Jun 2019 18:27:45 +0800 Subject: [PATCH 0326/1714] #1536: better detection of zombie proc --- psutil/_psbsd.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 978ad6c8f3..bba3a2ba12 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -534,6 +534,14 @@ def pid_exists(pid): pid_exists = _psposix.pid_exists +def is_zombie(pid): + try: + st = cext.proc_oneshot_info(pid)[kinfo_proc_map['status']] + return st == cext.SZOMB + except Exception: + return False + + def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. @@ -648,9 +656,14 @@ def cmdline(self): return cext.proc_cmdline(self.pid) except OSError as err: if err.errno == errno.EINVAL: - # XXX: this happens with unicode tests. It means the C - # routine is unable to decode invalid unicode chars. - return [] + 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. + return [] else: raise else: From e0bdcd75894b2d7c117c747197785b6e9ae17bfc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Jun 2019 19:04:46 +0800 Subject: [PATCH 0327/1714] fix various tests --- Makefile | 2 +- psutil/_psbsd.py | 1 - psutil/tests/test_bsd.py | 10 ++++++++-- psutil/tests/test_misc.py | 4 ++++ psutil/tests/test_process.py | 4 +++- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index b14978b216..4baa23b246 100644 --- a/Makefile +++ b/Makefile @@ -142,7 +142,7 @@ test-posix: ## POSIX specific tests. test-platform: ## Run specific platform tests only. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(TEST_PREFIX) $(PYTHON) psutil.tests.test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} install diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index bba3a2ba12..eccc96a15e 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -781,7 +781,6 @@ def connections(self, kind='inet'): ret = [] for item in rawlist: fd, fam, type, laddr, raddr, status = item - status = TCP_STATUSES[status] nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, TCP_STATUSES) ret.append(nt) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index e769b72785..6c3f6e5220 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -253,7 +253,10 @@ 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)) + 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" @@ -450,7 +453,10 @@ def test_sensors_temperatures_against_sysctl(self): 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])) + 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) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 6f923bed77..4b688288df 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -766,11 +766,15 @@ def test_cpu_distribution(self): @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") 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") @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") 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") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 512a84371f..ae3388a1e1 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -24,6 +24,7 @@ from psutil import AIX from psutil import BSD +from psutil import FREEBSD from psutil import LINUX from psutil import MACOS from psutil import NETBSD @@ -1084,7 +1085,8 @@ def test_parents(self): self.assertEqual(p1.parents()[0], psutil.Process()) self.assertEqual(p2.parents()[0], p1) self.assertEqual(p2.parents()[1], psutil.Process()) - if POSIX: + if POSIX and not FREEBSD: + # On FreeBSD PID 1 has an older/smaller time than PID 0 (?) lowest_pid = psutil.pids()[0] self.assertEqual(p1.parents()[-1].pid, lowest_pid) self.assertEqual(p2.parents()[-1].pid, lowest_pid) From ca31a0feb39f2e2b21459d398c0f629ca63886aa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Jun 2019 19:11:46 +0800 Subject: [PATCH 0328/1714] fix FreeBSD tests --- Makefile | 2 +- psutil/tests/test_bsd.py | 44 ++++++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 4baa23b246..b14978b216 100644 --- a/Makefile +++ b/Makefile @@ -142,7 +142,7 @@ test-posix: ## POSIX specific tests. test-platform: ## Run specific platform tests only. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil.tests.test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(TEST_PREFIX) $(PYTHON) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} install diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 6c3f6e5220..dd521c0f38 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -148,7 +148,7 @@ def test_net_if_stats(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") -class FreeBSDSpecificTestCase(unittest.TestCase): +class FreeBSDSpecificProcessTestCase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -158,21 +158,8 @@ def setUpClass(cls): 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): + 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:] @@ -186,17 +173,17 @@ def test_proc_memory_maps(self): if not map.path.startswith('['): self.assertEqual(fields[10], map.path) - def test_proc_exe(self): + 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_proc_cmdline(self): + 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_proc_uids_gids(self): + 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) @@ -210,7 +197,7 @@ def test_proc_uids_gids(self): self.assertEqual(gids.saved, int(sgid)) @retry_on_failure() - def test_proc_ctx_switches(self): + def test_ctx_switches(self): tested = [] out = sh('procstat -r %s' % self.pid) p = psutil.Process(self.pid) @@ -230,7 +217,7 @@ def test_proc_ctx_switches(self): raise RuntimeError("couldn't find lines match in procstat out") @retry_on_failure() - def test_proc_cpu_times(self): + def test_cpu_times(self): tested = [] out = sh('procstat -r %s' % self.pid) p = psutil.Process(self.pid) @@ -249,6 +236,23 @@ def test_proc_cpu_times(self): if len(tested) != 2: raise RuntimeError("couldn't find lines match in procstat out") + +@unittest.skipIf(not FREEBSD, "FREEBSD only") +class FreeBSDSpecificSystemTestCase(unittest.TestCase): + + @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. From adc28e4bd6534641f2c85ccc42529e6f25e97867 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Jun 2019 19:24:03 +0800 Subject: [PATCH 0329/1714] rename class names --- psutil/tests/test_bsd.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index dd521c0f38..f994bc5dc3 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -7,7 +7,7 @@ # TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. -"""Tests specific to all BSD platforms.""" +"""Tests sprcific to all BSD platforms.""" import datetime @@ -72,7 +72,7 @@ def muse(field): @unittest.skipIf(not BSD, "BSD only") -class BSDSpecificTestCase(unittest.TestCase): +class BSDTestCase(unittest.TestCase): """Generic tests common to all BSD variants.""" @classmethod @@ -148,7 +148,7 @@ def test_net_if_stats(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") -class FreeBSDSpecificProcessTestCase(unittest.TestCase): +class FreeBSDProcessTestCase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -238,7 +238,7 @@ def test_cpu_times(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") -class FreeBSDSpecificSystemTestCase(unittest.TestCase): +class FreeBSDSystemTestCase(unittest.TestCase): @staticmethod def parse_swapinfo(): @@ -373,8 +373,9 @@ def test_cpu_stats_soft_interrupts(self): sysctl('vm.stats.sys.v_soft'), delta=1000) 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=1000) + sysctl('vm.stats.sys.v_syscall'), delta=100000) # def test_cpu_stats_traps(self): # self.assertAlmostEqual(psutil.cpu_stats().traps, @@ -477,7 +478,7 @@ def test_sensors_temperatures_against_sysctl(self): @unittest.skipIf(not OPENBSD, "OPENBSD only") -class OpenBSDSpecificTestCase(unittest.TestCase): +class OpenBSDTestCase(unittest.TestCase): def test_boot_time(self): s = sysctl('kern.boottime') @@ -492,7 +493,7 @@ def test_boot_time(self): @unittest.skipIf(not NETBSD, "NETBSD only") -class NetBSDSpecificTestCase(unittest.TestCase): +class NetBSDTestCase(unittest.TestCase): @staticmethod def parse_meminfo(look_for): From bf8420af0851c6256a9fca322b07864c93a8f8b9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Jun 2019 17:46:09 +0800 Subject: [PATCH 0330/1714] version bump + fix typo --- psutil/__init__.py | 2 +- psutil/tests/test_bsd.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index b400ec8523..118fd9fd17 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -221,7 +221,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.3" +__version__ = "5.6.4" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index f994bc5dc3..7cba4b78a4 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -7,7 +7,7 @@ # TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. -"""Tests sprcific to all BSD platforms.""" +"""Tests specific to all BSD platforms.""" import datetime From 7aea16fd9bd3fbf9a00ae144ebd280903d3b3254 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 26 Jun 2019 20:45:07 +0200 Subject: [PATCH 0331/1714] use SO_REUSEADDR on UNIX only --- Makefile | 1 + psutil/tests/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b14978b216..a376384836 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ DEPS = \ ipaddress \ mock==1.0.1 \ perf \ + readline \ requests \ setuptools \ sphinx \ diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5e4f37b39d..fc52f6ad42 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -868,7 +868,6 @@ def wrapper(*args, **kwargs): 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] @@ -895,7 +894,8 @@ def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): addr = ("", 0) sock = socket.socket(family, type) try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + 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) From c10df5aa04e1ced58d19501fa42f08c1b909b83d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 28 Jun 2019 15:12:56 +0200 Subject: [PATCH 0332/1714] PEP-3151: backport FS exceptions to Python 2 (#1544) --- Makefile | 1 + psutil/__init__.py | 27 ++++--- psutil/_compat.py | 72 ++++++++++++++++++- psutil/_psaix.py | 32 ++++----- psutil/_psbsd.py | 36 +++++----- psutil/_pslinux.py | 115 +++++++++++++----------------- psutil/_psosx.py | 12 ++-- psutil/_psposix.py | 63 ++++++++-------- psutil/_pssunos.py | 50 ++++++------- psutil/tests/__init__.py | 31 ++++---- psutil/tests/test_bsd.py | 2 +- psutil/tests/test_linux.py | 7 +- psutil/tests/test_memory_leaks.py | 7 +- psutil/tests/test_misc.py | 3 +- psutil/tests/test_system.py | 4 +- psutil/tests/test_windows.py | 10 ++- 16 files changed, 254 insertions(+), 218 deletions(-) diff --git a/Makefile b/Makefile index b14978b216..a376384836 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ DEPS = \ ipaddress \ mock==1.0.1 \ perf \ + readline \ requests \ setuptools \ sphinx \ diff --git a/psutil/__init__.py b/psutil/__init__.py index b400ec8523..62dadc90c9 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -25,7 +25,6 @@ import collections import contextlib import datetime -import errno import functools import os import signal @@ -44,6 +43,8 @@ from ._common import memoize_when_activated from ._common import wrap_numbers as _wrap_numbers from ._compat import long +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import PY3 as _PY3 from ._common import STATUS_DEAD @@ -221,7 +222,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.3" +__version__ = "5.6.4" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -1290,18 +1291,16 @@ def _send_signal(self, sig): "calling process (os.getpid()) instead of PID 0") 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): diff --git a/psutil/_compat.py b/psutil/_compat.py index c772f61d60..07ab909a84 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -5,12 +5,15 @@ """Module which provides compatibility with older Python versions.""" import collections +import errno import functools import os import sys __all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", - "lru_cache", "which", "get_terminal_size"] + "lru_cache", "which", "get_terminal_size", + "FileNotFoundError", "PermissionError", "ProcessLookupError", + "InterruptedError", "ChildProcessError", "FileExistsError"] PY3 = sys.version_info[0] == 3 @@ -38,6 +41,73 @@ def b(s): return s +# --- 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 + + 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__(*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', object()) == errno.ENOENT + + @instance_checking_exception(EnvironmentError) + def ProcessLookupError(inst): + return getattr(inst, 'errno', object()) == errno.ESRCH + + @instance_checking_exception(EnvironmentError) + def PermissionError(inst): + return getattr(inst, 'errno', object()) in ( + errno.EACCES, errno.EPERM) + + @instance_checking_exception(EnvironmentError) + def InterruptedError(inst): + return getattr(inst, 'errno', object()) == errno.EINTR + + @instance_checking_exception(EnvironmentError) + def ChildProcessError(inst): + return getattr(inst, 'errno', object()) == errno.ECHILD + + @instance_checking_exception(EnvironmentError) + def FileExistsError(inst): + return getattr(inst, 'errno', object()) == errno.EEXIST + + # --- stdlib additions diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 3a949d2572..79e3be15a1 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -6,7 +6,6 @@ """AIX platform implementation.""" -import errno import functools import glob import os @@ -26,6 +25,9 @@ from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN from ._common import usage_percent +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import PY3 @@ -314,22 +316,16 @@ def wrap_exceptions(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 @@ -488,11 +484,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 None @wrap_exceptions def memory_info(self): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index eccc96a15e..2f41dc0be9 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -24,8 +24,12 @@ from ._common import NETBSD from ._common import OPENBSD from ._common import usage_percent +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import which + __extra__all__ = [] @@ -550,19 +554,19 @@ def wrap_exceptions(fun): def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) - except OSError as err: + except ProcessLookupError: + 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 - 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 @@ -572,18 +576,16 @@ 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 not pid_exists(inst.pid): + raise NoSuchProcess(inst.pid, inst._name) + else: + raise ZombieProcess(inst.pid, inst._name, inst._ppid) + except PermissionError: + raise AccessDenied(inst.pid, inst._name) class Process(object): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 50094b40e2..7a3a164b07 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -41,6 +41,9 @@ from ._common import usage_percent from ._compat import b from ._compat import basestring +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import PY3 if sys.version_info >= (3, 4): @@ -772,17 +775,16 @@ 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 + raise else: if inode.startswith('socket:['): # the process is using a socket @@ -795,7 +797,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 @@ -803,9 +805,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 @@ -1490,11 +1490,10 @@ 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() @@ -1511,16 +1510,12 @@ def wrap_exceptions(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: + raise NoSuchProcess(self.pid, self._name) + except FileNotFoundError: + 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. @@ -1617,21 +1612,19 @@ def name(self): 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 "" + except (FileNotFoundError, ProcessLookupError): + # 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: - 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 + raise ZombieProcess(self.pid, self._name, self._ppid) + except PermissionError: + raise AccessDenied(self.pid, self._name) @wrap_exceptions def cmdline(self): @@ -1856,14 +1849,12 @@ def get_blocks(lines, current_block): def cwd(self): try: return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) - except OSError as err: + except (FileNotFoundError, ProcessLookupError): # 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 + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) @wrap_exceptions def num_ctx_switches(self, @@ -1899,13 +1890,11 @@ def threads(self): 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: + # no such file or directory; it means thread + # disappeared on us + hit_enoent = True + continue # ignore the first two values ("pid (exe)") st = st[st.find(b')') + 2:] values = st.split(b' ') @@ -2029,16 +2018,15 @@ 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 + 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. @@ -2052,13 +2040,10 @@ def open_files(self): 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; process may - # still be alive - hit_enoent = True - else: - raise + except FileNotFoundError: + # fd gone in the meantime; process may + # still be alive + hit_enoent = True else: mode = file_flags_to_mode(flags) ntuple = popenfile( diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 54e5720274..7f28447bb1 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -19,6 +19,8 @@ from ._common import isfile_strict from ._common import memoize_when_activated from ._common import parse_environ_block +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._common import usage_percent @@ -334,12 +336,10 @@ def wrap_exceptions(fun): def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) - except OSError as err: - if err.errno == errno.ESRCH: - raise NoSuchProcess(self.pid, self._name) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) - raise + except ProcessLookupError: + raise NoSuchProcess(self.pid, self._name) + except PermissionError: + raise AccessDenied(self.pid, self._name) except cext.ZombieProcessError: raise ZombieProcess(self.pid, self._name, self._ppid) return wrapper diff --git a/psutil/_psposix.py b/psutil/_psposix.py index d362143f69..24570224fe 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -4,7 +4,6 @@ """Routines common to all posix systems.""" -import errno import glob import os import sys @@ -13,6 +12,11 @@ from ._common import memoize from ._common import sdiskusage from ._common import usage_percent +from ._compat import ChildProcessError +from ._compat import FileNotFoundError +from ._compat import InterruptedError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import PY3 from ._compat import unicode @@ -36,19 +40,13 @@ 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 @@ -84,24 +82,20 @@ def waitcall(): 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 + except InterruptedError: + delay = check_timeout(delay) + 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 True: + if pid_exists(pid): + delay = check_timeout(delay) + else: + return else: if retpid == 0: # WNOHANG was used, pid is still running @@ -180,7 +174,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/psutil/_pssunos.py b/psutil/_pssunos.py index 07298a7387..2aa2a86615 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -25,6 +25,9 @@ from ._common import socktype_to_enum from ._common import usage_percent from ._compat import b +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import PY3 @@ -342,22 +345,22 @@ def wrap_exceptions(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 @@ -515,11 +518,9 @@ def terminal(self): 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 + except FileNotFoundError: + hit_enoent = True + continue if hit_enoent: self._assert_alive() @@ -532,11 +533,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 None @wrap_exceptions def memory_info(self): @@ -597,12 +596,9 @@ 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))) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5e4f37b39d..6c62936877 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -41,6 +41,9 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import supports_ipv6 +from psutil._compat import ChildProcessError +from psutil._compat import FileExistsError +from psutil._compat import FileNotFoundError from psutil._compat import PY3 from psutil._compat import u from psutil._compat import unicode @@ -514,9 +517,8 @@ def assert_gone(pid): # Wait for the process to terminate, to avoid zombies. try: subp.wait() - except OSError as err: - if err.errno != errno.ECHILD: - raise + except ChildProcessError: + pass # Terminate started pids. while _pids_started: @@ -710,13 +712,12 @@ def retry_fun(fun): while time.time() < stop_at: try: return fun() + except FileNotFoundError: + pass except WindowsError as _: err = _ - if err.errno != errno.ENOENT: - raise - else: - warn("ignoring %s" % (str(err))) - time.sleep(0.01) + warn("ignoring %s" % (str(err))) + time.sleep(0.01) raise err try: @@ -729,18 +730,16 @@ def retry_fun(fun): fun() else: retry_fun(fun) - except OSError as err: - if err.errno != errno.ENOENT: - raise + except FileNotFoundError: + pass def safe_mkdir(dir): "Convenience function for creating a directory" try: os.mkdir(dir) - except OSError as err: - if err.errno != errno.EEXIST: - raise + except FileExistsError: + pass @contextlib.contextmanager @@ -868,7 +867,6 @@ def wrapper(*args, **kwargs): 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] @@ -895,7 +893,8 @@ def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): addr = ("", 0) sock = socket.socket(family, type) try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + 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) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index f994bc5dc3..7cba4b78a4 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -7,7 +7,7 @@ # TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. -"""Tests sprcific to all BSD platforms.""" +"""Tests specific to all BSD platforms.""" import datetime diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d732e90d7e..bb74ab3131 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -24,6 +24,7 @@ import psutil from psutil import LINUX from psutil._compat import basestring +from psutil._compat import FileNotFoundError from psutil._compat import PY3 from psutil._compat import u from psutil.tests import call_until @@ -1080,10 +1081,9 @@ def test_emulate_realpath_fail(self): try: with mock.patch('os.path.realpath', return_value='/non/existent') as m: - with self.assertRaises(OSError) as cm: + with self.assertRaises(FileNotFoundError): psutil.disk_partitions() assert m.called - self.assertEqual(cm.exception.errno, errno.ENOENT) finally: psutil.PROCFS_PATH = "/proc" @@ -1920,9 +1920,8 @@ def test_issue_1014(self): '/proc/%s/smaps' % os.getpid(), IOError(errno.ENOENT, "")) as m: p = psutil.Process() - with self.assertRaises(IOError) as err: + with self.assertRaises(FileNotFoundError): p.memory_maps() - self.assertEqual(err.exception.errno, errno.ENOENT) assert m.called @unittest.skipIf(not HAS_RLIMIT, "not supported") diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 543dbf7100..ba75eef00b 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -14,7 +14,6 @@ """ from __future__ import print_function -import errno import functools import gc import os @@ -31,6 +30,7 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import bytes2human +from psutil._compat import ProcessLookupError from psutil._compat import xrange from psutil.tests import create_sockets from psutil.tests import get_test_subprocess @@ -423,9 +423,8 @@ def test_proc_info(self): def call(): try: return cext.proc_info(self.proc.pid) - except OSError as err: - if err.errno != errno.ESRCH: - raise + except ProcessLookupError: + pass self.execute(call) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 4b688288df..4e476871da 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -180,7 +180,8 @@ def test__all__(self): for name in dir_psutil: if name in ('callable', 'error', 'namedtuple', 'tests', 'long', 'test', 'NUM_CPUS', 'BOOT_TIME', - 'TOTAL_PHYMEM'): + 'TOTAL_PHYMEM', 'PermissionError', + 'ProcessLookupError'): continue if not name.startswith('_'): try: diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index eb9016f08a..38843b0eee 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -29,6 +29,7 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil._compat import FileNotFoundError from psutil._compat import long from psutil.tests import APPVEYOR from psutil.tests import ASCII_FS @@ -468,9 +469,8 @@ def test_disk_usage(self): # if path does not exist OSError ENOENT is expected across # all platforms fname = tempfile.mktemp() - with self.assertRaises(OSError) as exc: + with self.assertRaises(FileNotFoundError): 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 diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 5a998dd4ff..51b53f0eb1 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -21,6 +21,7 @@ import psutil from psutil import WINDOWS +from psutil._compat import FileNotFoundError from psutil.tests import APPVEYOR from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY @@ -161,12 +162,9 @@ def test_disks(self): 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 + 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) From a4fcd0916aa079fd66880d89dff489a7d4a5b7fb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 28 Jun 2019 15:22:21 +0200 Subject: [PATCH 0333/1714] fix #1546: usage percent may be rounded to 0 on Python 2. --- HISTORY.rst | 1 + psutil/_common.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1f83b02cda..733ab03834 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,7 @@ XXXX-XX-XX always turned into enums. - 1536_: [NetBSD] process cmdline() erroneously raise ZombieProcess error if cmdline has non encodable chars. +- 1546_: usage percent may be rounded to 0 on Python 2. 5.6.3 ===== diff --git a/psutil/_common.py b/psutil/_common.py index 4a006c1dcf..126d9d6fb7 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -266,12 +266,12 @@ class BatteryTime(enum.IntEnum): 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 From 03a02fe6af19bb875a07a052656544a9b133e10b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 29 Jun 2019 15:39:23 +0200 Subject: [PATCH 0334/1714] add script to help fix flake8 issues --- Makefile | 3 + scripts/internal/fix_flake8.py | 185 +++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 scripts/internal/fix_flake8.py diff --git a/Makefile b/Makefile index a376384836..3f8f4c2d5b 100644 --- a/Makefile +++ b/Makefile @@ -174,6 +174,9 @@ test-coverage: ## Run test coverage. flake8: ## flake8 linter. @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 +fix-flake8: ## Attempt to automaticall fix some flake8 issues. + @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 --exit-zero | $(PYTHON) scripts/internal/fix_flake8.py + # =================================================================== # GIT # =================================================================== diff --git a/scripts/internal/fix_flake8.py b/scripts/internal/fix_flake8.py new file mode 100644 index 0000000000..5aa84db5c2 --- /dev/null +++ b/scripts/internal/fix_flake8.py @@ -0,0 +1,185 @@ +#!/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. + +""" +Fix some flake8 errors by rewriting files for which flake8 emitted +an error/warning. Usage (from the root dir): + + $ python3 -m flake8 --exit-zero | python3 scripts/fix_flake8.py +""" + +import sys +import tempfile +import shutil +from collections import defaultdict +from collections import namedtuple +from pprint import pprint as pp # NOQA + + +ntentry = namedtuple('ntentry', 'msg, issue, lineno, pos, descr') + + +# ===================================================================== +# utils +# ===================================================================== + + +def enter_pdb(): + from pdb import set_trace as st # trick GIT commit hook + sys.stdin = open('/dev/tty') + st() + + +def read_lines(fname): + with open(fname, 'rt') as f: + return f.readlines() + + +def read_line(fname, lineno): + with open(fname, 'rt') as f: + for i, line in enumerate(f, 1): + if i == lineno: + return line + raise ValueError("lineno too big") + + +def write_file(fname, newlines): + with tempfile.NamedTemporaryFile('wt', delete=False) as f: + for line in newlines: + f.write(line) + shutil.move(f.name, fname) + + +# ===================================================================== +# handlers +# ===================================================================== + + +def handle_f401(fname, lineno): + """ This is 'module imported but not used' + Able to handle this case: + import os + + ...but not this: + from os import listdir + """ + line = read_line(fname, lineno).strip() + if line.startswith('import '): + return True + else: + mods = line.partition(' import ')[2] # everything after import + return ',' not in mods + + +# ===================================================================== +# converters +# ===================================================================== + + +def remove_lines(fname, entries): + """Check if we should remove lines, then do it. + Return the numner of lines removed. + """ + to_remove = [] + for entry in entries: + msg, issue, lineno, pos, descr = entry + # 'module imported but not used' + if issue == 'F401' and handle_f401(fname, lineno): + to_remove.append(lineno) + # 'blank line(s) at end of file' + elif issue == 'W391': + lines = read_lines(fname) + i = len(lines) - 1 + while lines[i] == '\n': + to_remove.append(i + 1) + i -= 1 + # 'too many blank lines' + elif issue == 'E303': + howmany = descr.replace('(', '').replace(')', '') + howmany = int(howmany[-1]) + for x in range(lineno - howmany, lineno): + to_remove.append(x) + + if to_remove: + newlines = [] + for i, line in enumerate(read_lines(fname), 1): + if i not in to_remove: + newlines.append(line) + print("removing line(s) from %s" % fname) + write_file(fname, newlines) + + return len(to_remove) + + +def add_lines(fname, entries): + """Check if we should remove lines, then do it. + Return the numner of lines removed. + """ + EXPECTED_BLANK_LINES = ( + 'E302', # 'expected 2 blank limes, found 1' + 'E305') # ìexpected 2 blank lines after class or fun definition' + to_add = {} + for entry in entries: + msg, issue, lineno, pos, descr = entry + if issue in EXPECTED_BLANK_LINES: + howmany = 2 if descr.endswith('0') else 1 + to_add[lineno] = howmany + + if to_add: + newlines = [] + for i, line in enumerate(read_lines(fname), 1): + if i in to_add: + newlines.append('\n' * to_add[i]) + newlines.append(line) + print("adding line(s) to %s" % fname) + write_file(fname, newlines) + + return len(to_add) + + +# ===================================================================== +# main +# ===================================================================== + + +def build_table(): + table = defaultdict(list) + for line in sys.stdin: + line = line.strip() + if not line: + break + fields = line.split(':') + fname, lineno, pos = fields[:3] + issue = fields[3].split()[0] + descr = fields[3].strip().partition(' ')[2] + lineno, pos = int(lineno), int(pos) + table[fname].append(ntentry(line, issue, lineno, pos, descr)) + return table + + +def main(): + table = build_table() + + # remove lines (unused imports) + removed = 0 + for fname, entries in table.items(): + removed += remove_lines(fname, entries) + if removed: + print("%s lines were removed from some file(s); please re-run" % + removed) + return + + # add lines (missing \n between functions/classes) + added = 0 + for fname, entries in table.items(): + added += add_lines(fname, entries) + if added: + print("%s lines were added from some file(s); please re-run" % + added) + return + + +main() From 0609217bf6c153bea589f060614baed4ad9307e9 Mon Sep 17 00:00:00 2001 From: SteveMoto Date: Tue, 27 Aug 2019 21:34:08 +0200 Subject: [PATCH 0335/1714] typo fix scripts/internal/print_timeline.py (#1556) --- scripts/internal/print_timeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index 4bfe76b332..9bf6e9d132 100644 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -30,7 +30,7 @@ def get_tag_date(tag): def main(): releases = [] - out = sh("git tags") + out = sh("git tag") for line in out.split('\n'): tag = line.split(' ')[0] ver = tag.replace('release-', '') From 1487a3f46daf1cfe1238057d15da7c4f78fc3721 Mon Sep 17 00:00:00 2001 From: SteveMoto Date: Tue, 27 Aug 2019 21:34:34 +0200 Subject: [PATCH 0336/1714] typo fix docs/index.rst (#1553) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 8c0ee74a51..c9534470e4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2580,7 +2580,7 @@ FAQs especially on macOS (see `issue #883`_) and Windows. Unfortunately there's not much you can do about this except running the Python process with higher privileges. - On Unix you may run the the Python process as root or use the SUID bit + On Unix you may run the Python process as root or use the SUID bit (this is the trick used by tools such as ``ps`` and ``netstat``). On Windows you may run the Python process as NT AUTHORITY\\SYSTEM or install the Python script as a Windows service (this is the trick used by tools From 3aac9483767e38995f6e5bd973e822ab0598f27a Mon Sep 17 00:00:00 2001 From: SteveMoto Date: Tue, 27 Aug 2019 21:34:56 +0200 Subject: [PATCH 0337/1714] broken links fix docs/index.rst (#1555) --- docs/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index c9534470e4..0e3428d63a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ Quick links - `Blog `__ - `Forum `__ - `Download `__ -- `Development guide `_ +- `Development guide `_ - `What's new `__ About @@ -2910,7 +2910,7 @@ Timeline .. _`BPO-6973`: https://bugs.python.org/issue6973 .. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`development guide`: https://github.com/giampaolo/psutil/blob/master/DEVGUIDE.rst +.. _`development guide`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py .. _`enums`: https://docs.python.org/3/library/enum.html#module-enum .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py @@ -2927,7 +2927,7 @@ Timeline .. _`issue #883`: https://github.com/giampaolo/psutil/issues/883 .. _`man prlimit`: https://linux.die.net/man/2/prlimit .. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py. +.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py .. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py .. _`open`: https://docs.python.org/3/library/functions.html#open .. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count From e6bff45281de1a3e55978812468424086b0edac4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 7 Sep 2019 23:38:05 +0200 Subject: [PATCH 0338/1714] fix #1527: Linux process CPU iowait time --- HISTORY.rst | 5 +++++ docs/index.rst | 30 ++++++++++++++++++++++++------ psutil/_pslinux.py | 8 +++++++- psutil/tests/test_linux.py | 4 ++++ psutil/tests/test_process.py | 2 ++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 733ab03834..aa9909292c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,11 @@ XXXX-XX-XX +**Enhancements** + +- 1527_: [Linux] added Process.cpu_times().iowait counter, which is the time + spent waiting for blocking I/O to complete. + **Bug fixes** - 1528_: [AIX] compilation error on AIX 7.2 due to 32 vs 64 bit differences. diff --git a/docs/index.rst b/docs/index.rst index 8c0ee74a51..c9f5068fef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -78,7 +78,8 @@ CPU - **nice** *(UNIX)*: time spent by niced (prioritized) processes executing in user mode; on Linux this also includes **guest_nice** time - - **iowait** *(Linux)*: time spent waiting for I/O to complete + - **iowait** *(Linux)*: time spent waiting for I/O to complete. This is *not* + accounted in **idle** time counter. - **irq** *(Linux, BSD)*: time spent for servicing hardware interrupts - **softirq** *(Linux)*: time spent for servicing software interrupts - **steal** *(Linux 2.6.11+)*: time spent by other operating systems running @@ -1404,16 +1405,33 @@ Process class .. method:: cpu_times() - Return a `(user, system, children_user, children_system)` named tuple - representing the accumulated process time, in seconds (see - `explanation `__). - On Windows and macOS only *user* and *system* are filled, the others are - set to ``0``. + Return a named tuple representing the accumulated process times, in seconds + (see `explanation `__). This is similar to `os.times`_ but can be used for any process PID. + - **user**: time spent in user mode. + - **system**: time spent in kernel mode. + - **children_user**: user time of all child processes (always ``0`` on + Windows and macOS). + - **system_user**: user time of all child processes (always ``0`` on + Windows and macOS). + - **iowait**: (Linux) time spent waiting for blocking I/O to complete. + This value is excluded from `user` and `system` times count (because the + CPU is not doing any work). + + >>> import psutil + >>> p = psutil.Process() + >>> p.cpu_times() + pcputimes(user=0.03, system=0.67, children_user=0.0, children_system=0.0, iowait=0.08) + >>> sum(p.cpu_times()[:2]) # cumulative, excluding children and iowait + 0.70 + .. versionchanged:: 4.1.0 return two extra fields: *children_user* and *children_system*. + .. versionchanged:: + 5.6.4 added *iowait* on Linux. + .. method:: cpu_percent(interval=None) Return a float representing the process CPU utilization as a percentage diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7a3a164b07..e942c1f702 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -203,6 +203,10 @@ 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']) # ===================================================================== @@ -1571,6 +1575,7 @@ def _parse_stat_file(self): 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 @@ -1700,7 +1705,8 @@ def cpu_times(self): stime = float(values['stime']) / CLOCK_TICKS children_utime = float(values['children_utime']) / CLOCK_TICKS children_stime = float(values['children_stime']) / CLOCK_TICKS - return _common.pcputimes(utime, stime, children_utime, children_stime) + iowait = float(values['blkio_ticks']) / CLOCK_TICKS + return pcputimes(utime, stime, children_utime, children_stime, iowait) @wrap_exceptions def cpu_num(self): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index bb74ab3131..ad3ed93695 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1993,6 +1993,9 @@ def test_stat_file_parsing(self): "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): @@ -2007,6 +2010,7 @@ def test_stat_file_parsing(self): 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): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ae3388a1e1..da0f450b58 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -252,6 +252,8 @@ def test_cpu_times(self): 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))) From 25adbb604df24ae91a985c28578adca6c759f0f8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 14 Sep 2019 14:49:22 +0200 Subject: [PATCH 0339/1714] update README and issue templates --- .github/ISSUE_TEMPLATE/bug.md | 1 - .github/ISSUE_TEMPLATE/enhancement.md | 1 - README.rst | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 283d8dc672..ba4a026c78 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -3,7 +3,6 @@ name: Bug about: Report a bug title: "[OS] title" labels: 'bug' -assignees: 'giampaolo' --- **Platform** diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 1a5e14e590..7e7159f272 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -2,7 +2,6 @@ name: Enhancement about: Propose an enhancement labels: 'enhancement' -assignees: 'giampaolo' title: "[OS] title" --- diff --git a/README.rst b/README.rst index 4f1a71c348..471409ec00 100644 --- a/README.rst +++ b/README.rst @@ -359,7 +359,7 @@ Process management pgids(real=1000, effective=1000, saved=1000) >>> >>> p.cpu_times() - pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1) + pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1, iowait=0.0) >>> p.cpu_percent(interval=1.0) 12.1 >>> p.cpu_affinity() From 73940d9a25612db89b2f72268e614d95bfac2b89 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Sep 2019 11:54:18 +0200 Subject: [PATCH 0340/1714] update getloadavg doc --- README.rst | 5 +++-- docs/index.rst | 22 +++++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 471409ec00..0f25238e8d 100644 --- a/README.rst +++ b/README.rst @@ -385,8 +385,8 @@ Process management pio(read_count=478001, write_count=59371, read_bytes=700416, write_bytes=69632, read_chars=456232, write_chars=517543) >>> >>> p.open_files() - [popenfile(path='/home/giampaolo/svn/psutil/setup.py', fd=3, position=0, mode='r', flags=32768), - popenfile(path='/var/log/monitd', fd=4, position=235542, mode='a', flags=33793)] + [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768), + popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] >>> >>> p.connections() [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), @@ -428,6 +428,7 @@ Process management >>> p.resume() >>> >>> p.terminate() + >>> p.kill() >>> p.wait(timeout=3) 0 >>> diff --git a/docs/index.rst b/docs/index.rst index c9f5068fef..3b9a166f1a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -242,19 +242,27 @@ CPU .. function:: getloadavg() Return the average system load over the last 1, 5 and 15 minutes as a tuple. - The load represents how many processes are waiting to be run by the - operating system. - On UNIX systems this relies on `os.getloadavg`_. On Windows this is - emulated by using a Windows API that spawns a thread which updates the - average every 5 seconds, mimicking the UNIX behavior. Thus, the first time - this is called and for the next 5 seconds it will return a meaningless - ``(0.0, 0.0, 0.0)`` tuple. Example: + The load represents the processes which are in a runnable state, either + using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). + On UNIX systems this relies on `os.getloadavg`_. On Windows this is emulated + by using a Windows API that spawns a thread which keeps running in + background and updates the load average every 5 seconds, mimicking the UNIX + behavior. Thus, the first time this is called and for the next 5 seconds + it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. + The numbers returned only make sense if related to the number of CPU cores + installed on the system. So, for instance, `3.14` on a system with 10 CPU + cores means that the system load was 31.4% percent over the last N minutes. .. code-block:: python >>> import psutil >>> psutil.getloadavg() (3.14, 3.89, 4.67) + >>> psutil.cpu_count() + 10 + >>> # percentage representation + >>> [x / psutil.cpu_count() * 100 for x in psutil.getloadavg()] + [31.4, 38.9, 46.7] Availability: Unix, Windows From 3c7ef4e20e284431c2471f89ac6afc174d4e2af8 Mon Sep 17 00:00:00 2001 From: Ammar Askar Date: Sun, 15 Sep 2019 07:46:39 -0400 Subject: [PATCH 0341/1714] Correct loadavg constants for 5 and 15 minutes. Fixes #1552 (#1583) --- psutil/arch/windows/wmi.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 5858a9e083..f43d790c03 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -22,8 +22,8 @@ // This formula comes from linux's include/linux/sched/loadavg.h // https://github.com/torvalds/linux/blob/345671ea0f9258f410eb057b9ced9cefbbe5dc78/include/linux/sched/loadavg.h#L20-L23 #define LOADAVG_FACTOR_1F 0.9200444146293232478931553241 -#define LOADAVG_FACTOR_5F 0.6592406302004437462547604110 -#define LOADAVG_FACTOR_15F 0.2865047968601901003248854266 +#define LOADAVG_FACTOR_5F 0.9834714538216174894737477501 +#define LOADAVG_FACTOR_15F 0.9944598480048967508795473394 // The time interval in seconds between taking load counts, same as Linux #define SAMPLING_INTERVAL 5 @@ -112,4 +112,4 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { PyObject * psutil_get_loadavg(PyObject *self, PyObject *args) { return Py_BuildValue("(ddd)", load_avg_1m, load_avg_5m, load_avg_15m); -} \ No newline at end of file +} From a4728384564724b071afeb7d4c140ad9356c45fe Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Sep 2019 13:51:24 +0200 Subject: [PATCH 0342/1714] update HISTORY --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index aa9909292c..4e1f2103af 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,8 @@ XXXX-XX-XX - 1536_: [NetBSD] process cmdline() erroneously raise ZombieProcess error if cmdline has non encodable chars. - 1546_: usage percent may be rounded to 0 on Python 2. +- 1552_: [Windows] getloadavg() math for calculating 5 and 15 mins values is + incorrect. 5.6.3 ===== From 55301bddea77bd8fa2415586af93b7d8c3d5bdd1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Sep 2019 09:59:32 +0200 Subject: [PATCH 0343/1714] fix #1578: 'perf' lib was renamed to 'pyperf' --- Makefile | 2 +- scripts/internal/bench_oneshot_2.py | 4 ++-- scripts/internal/winmake.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 3f8f4c2d5b..e3f17f2286 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ DEPS = \ futures \ ipaddress \ mock==1.0.1 \ - perf \ + pyperf \ readline \ requests \ setuptools \ diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index a25d1806e4..becf930c8b 100644 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -11,7 +11,7 @@ import sys -import perf # requires "pip install perf" +import pyperf # requires "pip install pyperf" import psutil from bench_oneshot import names @@ -37,7 +37,7 @@ def add_cmdline_args(cmd, args): def main(): - runner = perf.Runner() + runner = pyperf.Runner() args = runner.parse_args() if not args.worker: diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 75b4c348ae..116809cac1 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -39,8 +39,8 @@ "flake8", "nose", "pdbpp", - "perf", "pip", + "pyperf", "pypiwin32==219" if sys.version_info[:2] <= (3, 4) else "pypiwin32", "pyreadline", "setuptools", From 1075c23d08f26225a84440e44aa8baeb3d047ddd Mon Sep 17 00:00:00 2001 From: Bastian Ebeling Date: Wed, 18 Sep 2019 10:02:04 +0200 Subject: [PATCH 0344/1714] Removed a typo/copy'n'paste mistake Linux->Windows (#1588) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 6429e15af0..e5e407376c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2219,7 +2219,7 @@ Process priority constants .. data:: IOPRIO_NORMAL .. data:: IOPRIO_HIGH - A set of integers representing the I/O priority of a process on Linux. + A set of integers representing the I/O priority of a process on Windows. They can be used in conjunction with :meth:`psutil.Process.ionice()` to get or set process I/O priority. From 2963b5f264b451a67dd26e19d38ab890a5bafa41 Mon Sep 17 00:00:00 2001 From: Athos Ribeiro Date: Thu, 26 Sep 2019 11:29:53 +0200 Subject: [PATCH 0345/1714] Fix #1563 - do not try to close invalid socket file descriptor (#1585) --- psutil/_psutil_posix.c | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index d9a8f6d1d6..5d20b21ec8 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -354,7 +354,7 @@ psutil_net_if_addrs(PyObject* self, PyObject* args) { static PyObject * psutil_net_if_mtu(PyObject *self, PyObject *args) { char *nic_name; - int sock = 0; + int sock = -1; int ret; #ifdef PSUTIL_SUNOS10 struct lifreq lifr; @@ -387,7 +387,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { #endif error: - if (sock != 0) + if (sock != -1) close(sock); return PyErr_SetFromErrno(PyExc_OSError); } @@ -401,7 +401,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { static PyObject * psutil_net_if_flags(PyObject *self, PyObject *args) { char *nic_name; - int sock = 0; + int sock = -1; int ret; struct ifreq ifr; @@ -424,7 +424,7 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { return Py_BuildValue("O", Py_False); error: - if (sock != 0) + if (sock != -1) close(sock); return PyErr_SetFromErrno(PyExc_OSError); } @@ -579,7 +579,7 @@ int psutil_get_nic_speed(int ifm_active) { static PyObject * psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { char *nic_name; - int sock = 0; + int sock = -1; int ret; int duplex; int speed; @@ -591,7 +591,7 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) - goto error; + return PyErr_SetFromErrno(PyExc_OSError); strncpy(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // speed / duplex @@ -614,11 +614,6 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { close(sock); return Py_BuildValue("[ii]", duplex, speed); - -error: - if (sock != 0) - close(sock); - return PyErr_SetFromErrno(PyExc_OSError); } #endif // net_if_stats() macOS/BSD implementation From 5a4c9a27ba266fa446802426ba1d243ff04723cf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Sep 2019 18:03:36 +0800 Subject: [PATCH 0346/1714] update HISTORY --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index 3b1de50dc9..4350eaf796 100644 --- a/CREDITS +++ b/CREDITS @@ -622,3 +622,7 @@ N: Kamil Rytarowski W: https://github.com/krytarowski C: Poland I: 1526, 1530 + +N: Athos Ribeiro +W: https://github.com/athos-ribeiro +I: 1585 diff --git a/HISTORY.rst b/HISTORY.rst index 4e1f2103af..6ad56e11d5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,8 @@ XXXX-XX-XX - 1546_: usage percent may be rounded to 0 on Python 2. - 1552_: [Windows] getloadavg() math for calculating 5 and 15 mins values is incorrect. +- 1585_: [OSX] calling close() (in C) on possible negative integers. (patch + by Athos Ribeiro) 5.6.3 ===== From 0c5e704bf9ad22fe4c36a73aba423ab46f41ba22 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Sep 2019 18:15:31 +0800 Subject: [PATCH 0347/1714] #1594: set a limit for eligible CPUs combinations --- psutil/tests/test_process.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index da0f450b58..24a29b5a03 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -940,6 +940,8 @@ def test_cpu_affinity_all_combinations(self): 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 l in range(0, len(initial) + 1): for subset in itertools.combinations(initial, l): From ff1bc8cdb2e585f717d121ef61dc7f0931a64dd8 Mon Sep 17 00:00:00 2001 From: Anthony Ramine <123095+nox@users.noreply.github.com> Date: Mon, 30 Sep 2019 05:05:28 +0200 Subject: [PATCH 0348/1714] Don't pollute tree with temporary configure test files (#1597) The docs for CCompiler.compile say: > If output_dir is given, object files will be put under it, while retaining their > original path component. That is, foo/bar.c normally compiles to foo/bar.o (for a > Unix implementation); if output_dir is build, then it would compile to build/foo/bar.o. What they forget to say is that path components are also retained if output_dir is not specified, it just means it will do so in the current directory. So if you compile a temporary C file /tmp/foo.c, it will produce a ./tmp/foo.o file relative to the current directory. This commit fixes that issue by passing an explicit output_dir itself located in a temporary directory. --- Makefile | 5 +---- setup.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index e3f17f2286..71f9300107 100644 --- a/Makefile +++ b/Makefile @@ -55,8 +55,7 @@ clean: ## Remove all build files. build/ \ dist/ \ docs/_build/ \ - htmlcov/ \ - tmp/ + htmlcov/ _: @@ -68,13 +67,11 @@ build: _ ## Compile without installing. @# "import psutil" when using the interactive interpreter from within @# this directory. PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i - rm -rf tmp $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. ${MAKE} build PYTHONWARNINGS=all $(PYTHON) setup.py develop $(INSTALL_OPTS) - rm -rf tmp uninstall: ## Uninstall this package via pip. cd ..; $(PYTHON) -m pip uninstall -y -v psutil || true diff --git a/setup.py b/setup.py index 693bd89d87..9f664293f4 100755 --- a/setup.py +++ b/setup.py @@ -205,11 +205,13 @@ def get_ethtool_macro(): suffix='.c', delete=False, mode="wt") as f: f.write("#include ") + output_dir = tempfile.mkdtemp() + try: compiler = UnixCCompiler() with silenced_output('stderr'): with silenced_output('stdout'): - compiler.compile([f.name]) + compiler.compile([f.name], output_dir) except CompileError: return ("PSUTIL_ETHTOOL_MISSING_TYPES", 1) else: From 2210b538b9d0de78ec1f9076fb29fbc17febf99b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Sep 2019 12:17:23 +0800 Subject: [PATCH 0349/1714] setup.py; rm temp dir --- setup.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 9f664293f4..8982d6a57a 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ import io import os import platform +import shutil import sys import tempfile import warnings @@ -206,21 +207,18 @@ def get_ethtool_macro(): f.write("#include ") output_dir = tempfile.mkdtemp() - try: compiler = UnixCCompiler() with silenced_output('stderr'): with silenced_output('stdout'): - compiler.compile([f.name], output_dir) + compiler.compile([f.name], output_dir=output_dir) except CompileError: return ("PSUTIL_ETHTOOL_MISSING_TYPES", 1) else: return None finally: - try: - os.remove(f.name) - except OSError: - pass + os.remove(f.name) + shutil.rmtree(output_dir) macros.append(("PSUTIL_LINUX", 1)) ETHTOOL_MACRO = get_ethtool_macro() From d4613497d8a0f32b6b5864461f9ca26ff623ea80 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 8 Oct 2019 17:19:31 +0200 Subject: [PATCH 0350/1714] test_system: relax test_disk_partitions asserts on mountpoints (#1600) As on at least Solaris and modern Linux systems they can be files too. On linux bind mounts may commonly be used with containers. Fix #1573 --- psutil/tests/test_system.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 38843b0eee..83d7ef3045 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -501,11 +501,8 @@ def test_disk_partitions(self): # 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 + # on modern systems mount points can also be files + assert os.path.exists(disk.mountpoint), disk assert disk.fstype, disk # all = True From af4b6c060e383a8c2f4bee478de62a294d4f7c5f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 9 Oct 2019 13:51:29 +0000 Subject: [PATCH 0351/1714] fix some failing tests on CentOS --- psutil/tests/test_linux.py | 7 +++---- setup.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index ad3ed93695..77958ada85 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -10,6 +10,7 @@ import collections import contextlib import errno +import glob import io import os import re @@ -55,7 +56,7 @@ SIOCGIFHWADDR = 0x8927 if LINUX: SECTOR_SIZE = 512 - +EMPTY_TEMPERATURES = not glob.glob('/sys/class/hwmon/hwmon*') # ===================================================================== # --- utils @@ -705,15 +706,12 @@ 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): @@ -1569,6 +1567,7 @@ def test_emulate_no_power(self): class TestSensorsTemperatures(unittest.TestCase): @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") + @unittest.skipIf(LINUX and EMPTY_TEMPERATURES, "no temperatures") def test_emulate_eio_error(self): def open_mock(name, *args, **kwargs): if name.endswith("_input"): diff --git a/setup.py b/setup.py index 8982d6a57a..04a41d99a6 100755 --- a/setup.py +++ b/setup.py @@ -240,7 +240,7 @@ def get_ethtool_macro(): ], define_macros=macros, libraries=['kstat', 'nsl', 'socket']) -# AIX + elif AIX: macros.append(("PSUTIL_AIX", 1)) ext = Extension( From 63fcc0e3a5c5bc155970d40df99acfac4a3aa0de Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Oct 2019 06:37:32 +0000 Subject: [PATCH 0352/1714] fix more CentOS failures --- psutil/tests/test_linux.py | 19 ------------------- psutil/tests/test_system.py | 19 +++++++++---------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 77958ada85..09fed4e46d 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -842,25 +842,6 @@ def open_mock(name, *args, **kwargs): 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, "") - elif name == '/proc/cpuinfo': - 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): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 83d7ef3045..e04e120b33 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -364,17 +364,16 @@ 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 + giveup_at = time.time() + 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() + 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 def test_cpu_times_comparison(self): # Make sure the sum of all per cpu times is almost equal to From 79fa2621ff8f7cd2eac9e0ad9b6e49e5ea029ce4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Oct 2019 09:04:18 +0000 Subject: [PATCH 0353/1714] fix #1126: cpu_affinity() segfaults on CentOS 5 Remove cpu_affinity() support for CentOS 5 (it's 8 years old anyway); remove the dual implementation. Recent manylinux versions should use CentOS 6. --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 64 ++++++++++++++++++++++-------------------- psutil/_psutil_linux.c | 58 +++++++------------------------------- 3 files changed, 46 insertions(+), 78 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6ad56e11d5..75100fa7f3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,8 @@ XXXX-XX-XX **Bug fixes** +- 1126_: [Linux] cpu_affinity() segfaults on CentOS 5 / manylinux. + cpu_affinity() support for CentOS 5 was removed. - 1528_: [AIX] compilation error on AIX 7.2 due to 32 vs 64 bit differences. (patch by Arnon Yaari) - 1535_: 'type' and 'family' fields returned by net_connections() are not diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e942c1f702..d29ccc8507 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -73,6 +73,7 @@ HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) HAS_PRLIMIT = hasattr(cext, "linux_prlimit") HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get") +HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get") _DEFAULT = object() # RLIMIT_* constants, not guaranteed to be present on all kernels @@ -1925,38 +1926,41 @@ 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 HAS_PROC_IO_PRIORITY: diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 4bf53b8579..8151b75d11 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -54,6 +54,11 @@ static const int NCPUS_START = sizeof(unsigned long) * CHAR_BIT; #include #endif +// Should exist starting from CentOS 6 (year 2011). +#ifdef CPU_ALLOC + #define PSUTIL_HAVE_CPU_AFFINITY +#endif + #include "_psutil_common.h" #include "_psutil_posix.h" @@ -279,11 +284,8 @@ psutil_linux_sysinfo(PyObject *self, PyObject *args) { /* * Return process CPU affinity as a Python list - * The dual implementation exists because of: - * https://github.com/giampaolo/psutil/issues/536 */ - -#ifdef CPU_ALLOC +#ifdef PSUTIL_HAVE_CPU_AFFINITY static PyObject * psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { @@ -347,50 +349,8 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { Py_XDECREF(py_list); return NULL; } -#else -/* - * Alternative implementation in case CPU_ALLOC is not defined. - */ -static PyObject * -psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { - cpu_set_t cpuset; - unsigned int len = sizeof(cpu_set_t); - long pid; - int i; - PyObject* py_retlist = NULL; - PyObject *py_cpu_num = NULL; - - if (!PyArg_ParseTuple(args, "l", &pid)) - return NULL; - CPU_ZERO(&cpuset); - if (sched_getaffinity(pid, len, &cpuset) < 0) - return PyErr_SetFromErrno(PyExc_OSError); - - py_retlist = PyList_New(0); - if (py_retlist == NULL) - goto error; - for (i = 0; i < CPU_SETSIZE; ++i) { - if (CPU_ISSET(i, &cpuset)) { - py_cpu_num = Py_BuildValue("i", i); - if (py_cpu_num == NULL) - goto error; - if (PyList_Append(py_retlist, py_cpu_num)) - goto error; - Py_DECREF(py_cpu_num); - } - } - - return py_retlist; - -error: - Py_XDECREF(py_cpu_num); - Py_XDECREF(py_retlist); - return NULL; -} -#endif - /* * Set process CPU affinity; expects a bitmask */ @@ -432,7 +392,6 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { CPU_SET(value, &cpu_set); } - len = sizeof(cpu_set); if (sched_setaffinity(pid, len, &cpu_set)) { PyErr_SetFromErrno(PyExc_OSError); @@ -447,6 +406,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { Py_DECREF(py_cpu_seq); return NULL; } +#endif /* PSUTIL_HAVE_CPU_AFFINITY */ /* @@ -583,16 +543,18 @@ static PyMethodDef PsutilMethods[] = { // --- per-process functions -#if PSUTIL_HAVE_IOPRIO +#ifdef PSUTIL_HAVE_IOPRIO {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS, "Get process I/O priority"}, {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS, "Set process I/O priority"}, #endif +#ifdef PSUTIL_HAVE_CPU_AFFINITY {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS, "Return process CPU affinity as a Python long (the bitmask)."}, {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS, "Set process CPU affinity; expects a bitmask."}, +#endif // --- system related functions From c20d734ed387476cd79b711684b5a295baf8b8b0 Mon Sep 17 00:00:00 2001 From: Erwan Le Pape Date: Fri, 11 Oct 2019 09:43:59 +0200 Subject: [PATCH 0354/1714] Fixes #1570: raise the NTSTATUS returned by NtWow64* syscalls (#1602) --- CREDITS | 4 +++ HISTORY.rst | 1 + psutil/arch/windows/process_info.c | 49 ++++++++++++++++-------------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/CREDITS b/CREDITS index 4350eaf796..9a70238e76 100644 --- a/CREDITS +++ b/CREDITS @@ -626,3 +626,7 @@ I: 1526, 1530 N: Athos Ribeiro W: https://github.com/athos-ribeiro I: 1585 + +N: Erwan Le Pape +W: https://github.com/erwan-le-pape +I: 1570 diff --git a/HISTORY.rst b/HISTORY.rst index 75100fa7f3..1e8cd0ba82 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,6 +23,7 @@ XXXX-XX-XX - 1546_: usage percent may be rounded to 0 on Python 2. - 1552_: [Windows] getloadavg() math for calculating 5 and 15 mins values is incorrect. +- 1570_: [Windows] NtWow64* syscalls fail to raise the proper error code - 1585_: [OSX] calling close() (in C) on possible negative integers. (patch by Athos Ribeiro) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index ba9966bb39..69da28bd3d 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -576,41 +576,44 @@ psutil_get_process_data(long pid, } } - if (! NT_SUCCESS( - NtWow64QueryInformationProcess64( + status = NtWow64QueryInformationProcess64( hProcess, ProcessBasicInformation, &pbi64, sizeof(pbi64), - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall( - "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); + NULL); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtWow64QueryInformationProcess64(ProcessBasicInformation)" + ); goto error; } // read peb - if (! NT_SUCCESS(NtWow64ReadVirtualMemory64( - hProcess, - pbi64.PebBaseAddress, - &peb64, - sizeof(peb64), - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall("NtWow64ReadVirtualMemory64"); + status = NtWow64ReadVirtualMemory64( + hProcess, + pbi64.PebBaseAddress, + &peb64, + sizeof(peb64), + NULL); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr(status, "NtWow64ReadVirtualMemory64"); goto error; } // read process parameters - if (! NT_SUCCESS(NtWow64ReadVirtualMemory64( + status = NtWow64ReadVirtualMemory64( hProcess, peb64.ProcessParameters, &procParameters64, sizeof(procParameters64), - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall( - "NtWow64ReadVirtualMemory64(ProcessParameters)"); + NULL); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtWow64ReadVirtualMemory64(ProcessParameters)" + ); goto error; } @@ -705,14 +708,14 @@ psutil_get_process_data(long pid, #ifndef _WIN64 if (weAreWow64 && !theyAreWow64) { - if (! NT_SUCCESS(NtWow64ReadVirtualMemory64( + status = NtWow64ReadVirtualMemory64( hProcess, src64, buffer, size, - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall("NtWow64ReadVirtualMemory64"); + NULL); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr(status, "NtWow64ReadVirtualMemory64"); goto error; } } else From ad0f2d200e2d0bb061bf20045126bf8aa87d289e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Oct 2019 20:08:06 +0800 Subject: [PATCH 0355/1714] update doc --- docs/index.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e5e407376c..dd83c6b9a9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1299,13 +1299,15 @@ Process class Here's an example on how to set the highest I/O priority depending on what platform you're on:: - import psutil - p = psutil.Process() - if psutil.LINUX - p.ionice(psutil.IOPRIO_CLASS_RT, value=7) - else: # Windows - p.ionice(psutil.IOPRIO_HIGH) - p.ionice() # get + >>> import psutil + >>> p = psutil.Process() + >>> if psutil.LINUX: + ... p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + ... else: + ... p.ionice(psutil.IOPRIO_HIGH) + ... + >>> p.ionice() # get + pionice(ioclass=, value=7) Availability: Linux, Windows Vista+ From 2ac1cd40168677bfc550bf34a6f04972ed745374 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Oct 2019 14:43:06 +0800 Subject: [PATCH 0356/1714] Refactor C modules init (#1603) --- docs/index.rst | 16 ++-- psutil/_psutil_bsd.c | 163 +++++++++++++++++++---------------------- psutil/_psutil_linux.c | 128 +++++++++++++------------------- psutil/_psutil_osx.c | 147 ++++++++++++++++++------------------- psutil/_psutil_posix.c | 90 +++++++++-------------- 5 files changed, 243 insertions(+), 301 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e5e407376c..dd83c6b9a9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1299,13 +1299,15 @@ Process class Here's an example on how to set the highest I/O priority depending on what platform you're on:: - import psutil - p = psutil.Process() - if psutil.LINUX - p.ionice(psutil.IOPRIO_CLASS_RT, value=7) - else: # Windows - p.ionice(psutil.IOPRIO_HIGH) - p.ionice() # get + >>> import psutil + >>> p = psutil.Process() + >>> if psutil.LINUX: + ... p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + ... else: + ... p.ionice(psutil.IOPRIO_HIGH) + ... + >>> p.ionice() # get + pionice(ioclass=, value=7) Availability: Linux, Windows Vista+ diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 74fe5922e2..b4264ceb76 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -906,8 +906,7 @@ psutil_users(PyObject *self, PyObject *args) { /* * define the psutil C module methods and initialize the module. */ -static PyMethodDef -PsutilMethods[] = { +static PyMethodDef mod_methods[] = { // --- per-process functions {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS, @@ -994,109 +993,99 @@ PsutilMethods[] = { {NULL, NULL, 0, NULL} }; -struct module_state { - PyObject *error; -}; - -#if PY_MAJOR_VERSION >= 3 -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -#endif - #if PY_MAJOR_VERSION >= 3 - -static int -psutil_bsd_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - -static int -psutil_bsd_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - -static struct PyModuleDef - moduledef = { - PyModuleDef_HEAD_INIT, - "psutil_bsd", - NULL, - sizeof(struct module_state), - PsutilMethods, - NULL, - psutil_bsd_traverse, - psutil_bsd_clear, - NULL -}; - -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_bsd(void) - -#else -#define INITERROR return - -void init_psutil_bsd(void) -#endif + #define INITERR return NULL + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_bsd", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL + }; + + PyObject *PyInit__psutil_bsd(void) +#else /* PY_MAJOR_VERSION */ + #define INITERR return + + void init_psutil_bsd(void) +#endif /* PY_MAJOR_VERSION */ { + PyObject *v; #if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); + PyObject *mod = PyModule_Create(&moduledef); #else - PyObject *module = Py_InitModule("_psutil_bsd", PsutilMethods); + PyObject *mod = Py_InitModule("_psutil_bsd", mod_methods); #endif - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); + if (mod == NULL) + INITERR; + + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) INITERR; // process status constants #ifdef PSUTIL_FREEBSD - PyModule_AddIntConstant(module, "SIDL", SIDL); - PyModule_AddIntConstant(module, "SRUN", SRUN); - PyModule_AddIntConstant(module, "SSLEEP", SSLEEP); - PyModule_AddIntConstant(module, "SSTOP", SSTOP); - PyModule_AddIntConstant(module, "SZOMB", SZOMB); - PyModule_AddIntConstant(module, "SWAIT", SWAIT); - PyModule_AddIntConstant(module, "SLOCK", SLOCK); + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) INITERR; + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) INITERR; + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) INITERR; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) INITERR; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) INITERR; + if (PyModule_AddIntConstant(mod, "SWAIT", SWAIT)) INITERR; + if (PyModule_AddIntConstant(mod, "SLOCK", SLOCK)) INITERR; #elif PSUTIL_OPENBSD - PyModule_AddIntConstant(module, "SIDL", SIDL); - PyModule_AddIntConstant(module, "SRUN", SRUN); - PyModule_AddIntConstant(module, "SSLEEP", SSLEEP); - PyModule_AddIntConstant(module, "SSTOP", SSTOP); - PyModule_AddIntConstant(module, "SZOMB", SZOMB); // unused - PyModule_AddIntConstant(module, "SDEAD", SDEAD); - PyModule_AddIntConstant(module, "SONPROC", SONPROC); + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) INITERR; + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) INITERR; + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) INITERR; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) INITERR; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB); INITERR; // unused + if (PyModule_AddIntConstant(mod, "SDEAD", SDEAD)) INITERR; + if (PyModule_AddIntConstant(mod, "SONPROC", SONPROC)) INITERR; #elif defined(PSUTIL_NETBSD) - PyModule_AddIntConstant(module, "SIDL", LSIDL); - PyModule_AddIntConstant(module, "SRUN", LSRUN); - PyModule_AddIntConstant(module, "SSLEEP", LSSLEEP); - PyModule_AddIntConstant(module, "SSTOP", LSSTOP); - PyModule_AddIntConstant(module, "SZOMB", LSZOMB); - PyModule_AddIntConstant(module, "SDEAD", LSDEAD); - PyModule_AddIntConstant(module, "SONPROC", LSONPROC); + if (PyModule_AddIntConstant(mod, "SIDL", LSIDL)) INITERR; + if (PyModule_AddIntConstant(mod, "SRUN", LSRUN)) INITERR; + if (PyModule_AddIntConstant(mod, "SSLEEP", LSSLEEP)) INITERR; + if (PyModule_AddIntConstant(mod, "SSTOP", LSSTOP)) INITERR; + if (PyModule_AddIntConstant(mod, "SZOMB", LSZOMB)) INITERR; + if (PyModule_AddIntConstant(mod, "SDEAD", LSDEAD)) INITERR; + if (PyModule_AddIntConstant(mod, "SONPROC", LSONPROC)) INITERR; // unique to NetBSD - PyModule_AddIntConstant(module, "SSUSPENDED", LSSUSPENDED); + if (PyModule_AddIntConstant(mod, "SSUSPENDED", LSSUSPENDED)) INITERR; #endif // connection status constants - PyModule_AddIntConstant(module, "TCPS_CLOSED", TCPS_CLOSED); - PyModule_AddIntConstant(module, "TCPS_CLOSING", TCPS_CLOSING); - PyModule_AddIntConstant(module, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT); - PyModule_AddIntConstant(module, "TCPS_LISTEN", TCPS_LISTEN); - PyModule_AddIntConstant(module, "TCPS_ESTABLISHED", TCPS_ESTABLISHED); - PyModule_AddIntConstant(module, "TCPS_SYN_SENT", TCPS_SYN_SENT); - PyModule_AddIntConstant(module, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2); - PyModule_AddIntConstant(module, "TCPS_LAST_ACK", TCPS_LAST_ACK); - PyModule_AddIntConstant(module, "TCPS_TIME_WAIT", TCPS_TIME_WAIT); + if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) + INITERR; // PSUTIL_CONN_NONE - PyModule_AddIntConstant(module, "PSUTIL_CONN_NONE", 128); + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", 128)) INITERR; psutil_setup(); - if (module == NULL) - INITERROR; + if (mod == NULL) + INITERR; #if PY_MAJOR_VERSION >= 3 - return module; + return mod; #endif } diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 8151b75d11..717723d088 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -537,10 +537,10 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { /* - * Define the psutil C module methods and initialize the module. + * Module init. */ -static PyMethodDef -PsutilMethods[] = { + +static PyMethodDef mod_methods[] = { // --- per-process functions #ifdef PSUTIL_HAVE_IOPRIO @@ -574,7 +574,6 @@ PsutilMethods[] = { {"linux_prlimit", psutil_linux_prlimit, METH_VARARGS, "Get or set process resource limits."}, #endif - // --- others {"set_testing", psutil_set_testing, METH_NOARGS, "Set psutil in testing mode"}, @@ -582,75 +581,51 @@ PsutilMethods[] = { {NULL, NULL, 0, NULL} }; -struct module_state { - PyObject *error; -}; - -#if PY_MAJOR_VERSION >= 3 -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -#endif #if PY_MAJOR_VERSION >= 3 - -static int -psutil_linux_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - -static int -psutil_linux_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - -static struct PyModuleDef - moduledef = { - PyModuleDef_HEAD_INIT, - "psutil_linux", - NULL, - sizeof(struct module_state), - PsutilMethods, - NULL, - psutil_linux_traverse, - psutil_linux_clear, - NULL -}; - -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_linux(void) - -#else -#define INITERROR return - -void init_psutil_linux(void) -#endif + #define INITERR return NULL + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_linux", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL + }; + + PyObject *PyInit__psutil_linux(void) +#else /* PY_MAJOR_VERSION */ + #define INITERR return + + void init_psutil_linux(void) +#endif /* PY_MAJOR_VERSION */ { PyObject *v; #if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); + PyObject *mod = PyModule_Create(&moduledef); #else - PyObject *module = Py_InitModule("_psutil_linux", PsutilMethods); + PyObject *mod = Py_InitModule("_psutil_linux", mod_methods); #endif - if (module == NULL) - INITERROR; + if (mod == NULL) + INITERR; - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) INITERR; #if PSUTIL_HAVE_PRLIMIT - PyModule_AddIntConstant(module, "RLIMIT_AS", RLIMIT_AS); - PyModule_AddIntConstant(module, "RLIMIT_CORE", RLIMIT_CORE); - PyModule_AddIntConstant(module, "RLIMIT_CPU", RLIMIT_CPU); - PyModule_AddIntConstant(module, "RLIMIT_DATA", RLIMIT_DATA); - PyModule_AddIntConstant(module, "RLIMIT_FSIZE", RLIMIT_FSIZE); - PyModule_AddIntConstant(module, "RLIMIT_LOCKS", RLIMIT_LOCKS); - PyModule_AddIntConstant(module, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK); - PyModule_AddIntConstant(module, "RLIMIT_NOFILE", RLIMIT_NOFILE); - PyModule_AddIntConstant(module, "RLIMIT_NPROC", RLIMIT_NPROC); - PyModule_AddIntConstant(module, "RLIMIT_RSS", RLIMIT_RSS); - PyModule_AddIntConstant(module, "RLIMIT_STACK", RLIMIT_STACK); + if (PyModule_AddIntConstant(mod, "RLIMIT_AS", RLIMIT_AS)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_CORE", RLIMIT_CORE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_CPU", RLIMIT_CPU)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_DATA", RLIMIT_DATA)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_FSIZE", RLIMIT_FSIZE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_NOFILE", RLIMIT_NOFILE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_NPROC", RLIMIT_NPROC)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_RSS", RLIMIT_RSS)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_STACK", RLIMIT_STACK)) INITERR; #if defined(HAVE_LONG_LONG) if (sizeof(RLIM_INFINITY) > sizeof(long)) { @@ -661,32 +636,33 @@ void init_psutil_linux(void) v = PyLong_FromLong((long) RLIM_INFINITY); } if (v) { - PyModule_AddObject(module, "RLIM_INFINITY", v); + PyModule_AddObject(mod, "RLIM_INFINITY", v); } #ifdef RLIMIT_MSGQUEUE - PyModule_AddIntConstant(module, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE); + if (PyModule_AddIntConstant(mod, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE)) INITERR; #endif #ifdef RLIMIT_NICE - PyModule_AddIntConstant(module, "RLIMIT_NICE", RLIMIT_NICE); + if (PyModule_AddIntConstant(mod, "RLIMIT_NICE", RLIMIT_NICE)) INITERR; #endif #ifdef RLIMIT_RTPRIO - PyModule_AddIntConstant(module, "RLIMIT_RTPRIO", RLIMIT_RTPRIO); + if (PyModule_AddIntConstant(mod, "RLIMIT_RTPRIO", RLIMIT_RTPRIO)) INITERR; #endif #ifdef RLIMIT_RTTIME - PyModule_AddIntConstant(module, "RLIMIT_RTTIME", RLIMIT_RTTIME); + if (PyModule_AddIntConstant(mod, "RLIMIT_RTTIME", RLIMIT_RTTIME)) INITERR; #endif #ifdef RLIMIT_SIGPENDING - PyModule_AddIntConstant(module, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING); + if (PyModule_AddIntConstant(mod, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING)) + INITERR; #endif #endif - PyModule_AddIntConstant(module, "DUPLEX_HALF", DUPLEX_HALF); - PyModule_AddIntConstant(module, "DUPLEX_FULL", DUPLEX_FULL); - PyModule_AddIntConstant(module, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN); + if (PyModule_AddIntConstant(mod, "DUPLEX_HALF", DUPLEX_HALF)) INITERR; + if (PyModule_AddIntConstant(mod, "DUPLEX_FULL", DUPLEX_FULL)) INITERR; + if (PyModule_AddIntConstant(mod, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN)) INITERR; - if (module == NULL) - INITERROR; + if (mod == NULL) + INITERR; #if PY_MAJOR_VERSION >= 3 - return module; + return mod; #endif } diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index eaf4e514a1..d2ca94b586 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1745,8 +1745,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { /* * define the psutil C module methods and initialize the module. */ -static PyMethodDef -PsutilMethods[] = { +static PyMethodDef mod_methods[] = { // --- per-process functions {"proc_kinfo_oneshot", psutil_proc_kinfo_oneshot, METH_VARARGS, @@ -1816,96 +1815,94 @@ PsutilMethods[] = { }; -struct module_state { - PyObject *error; -}; - -#if PY_MAJOR_VERSION >= 3 -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -#endif - #if PY_MAJOR_VERSION >= 3 - -static int -psutil_osx_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - -static int -psutil_osx_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "psutil_osx", - NULL, - sizeof(struct module_state), - PsutilMethods, - NULL, - psutil_osx_traverse, - psutil_osx_clear, - NULL -}; - -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_osx(void) - -#else -#define INITERROR return - -void -init_psutil_osx(void) -#endif + #define INITERR return NULL + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_osx", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL + }; + + PyObject *PyInit__psutil_osx(void) +#else /* PY_MAJOR_VERSION */ + #define INITERR return + + void init_psutil_osx(void) +#endif /* PY_MAJOR_VERSION */ { + PyObject *v; #if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); + PyObject *mod = PyModule_Create(&moduledef); #else - PyObject *module = Py_InitModule("_psutil_osx", PsutilMethods); + PyObject *mod = Py_InitModule("_psutil_osx", mod_methods); #endif - if (module == NULL) - INITERROR; + if (mod == NULL) + INITERR; if (psutil_setup() != 0) - INITERROR; + INITERR; - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + INITERR; // process status constants, defined in: // http://fxr.watson.org/fxr/source/bsd/sys/proc.h?v=xnu-792.6.70#L149 - PyModule_AddIntConstant(module, "SIDL", SIDL); - PyModule_AddIntConstant(module, "SRUN", SRUN); - PyModule_AddIntConstant(module, "SSLEEP", SSLEEP); - PyModule_AddIntConstant(module, "SSTOP", SSTOP); - PyModule_AddIntConstant(module, "SZOMB", SZOMB); + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) + INITERR; + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) + INITERR; + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) + INITERR; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) + INITERR; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) + INITERR; // connection status constants - PyModule_AddIntConstant(module, "TCPS_CLOSED", TCPS_CLOSED); - PyModule_AddIntConstant(module, "TCPS_CLOSING", TCPS_CLOSING); - PyModule_AddIntConstant(module, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT); - PyModule_AddIntConstant(module, "TCPS_LISTEN", TCPS_LISTEN); - PyModule_AddIntConstant(module, "TCPS_ESTABLISHED", TCPS_ESTABLISHED); - PyModule_AddIntConstant(module, "TCPS_SYN_SENT", TCPS_SYN_SENT); - PyModule_AddIntConstant(module, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2); - PyModule_AddIntConstant(module, "TCPS_LAST_ACK", TCPS_LAST_ACK); - PyModule_AddIntConstant(module, "TCPS_TIME_WAIT", TCPS_TIME_WAIT); - PyModule_AddIntConstant(module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); + if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) + INITERR; + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) + INITERR; // Exception. ZombieProcessError = PyErr_NewException( "_psutil_osx.ZombieProcessError", NULL, NULL); + if (ZombieProcessError == NULL) + INITERR; Py_INCREF(ZombieProcessError); - PyModule_AddObject(module, "ZombieProcessError", ZombieProcessError); + if (PyModule_AddObject(mod, "ZombieProcessError", ZombieProcessError)) { + Py_DECREF(ZombieProcessError); + INITERR; + } - if (module == NULL) - INITERROR; + if (mod == NULL) + INITERR; #if PY_MAJOR_VERSION >= 3 - return module; + return mod; #endif } diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 5d20b21ec8..209e787d5b 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -618,11 +618,15 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { #endif // net_if_stats() macOS/BSD implementation +#ifdef __cplusplus +extern "C" { +#endif + + /* * define the psutil C module methods and initialize the module. */ -static PyMethodDef -PsutilMethods[] = { +static PyMethodDef mod_methods[] = { {"getpriority", psutil_posix_getpriority, METH_VARARGS, "Return process priority"}, {"setpriority", psutil_posix_setpriority, METH_VARARGS, @@ -640,71 +644,45 @@ PsutilMethods[] = { {NULL, NULL, 0, NULL} }; -struct module_state { - PyObject *error; -}; #if PY_MAJOR_VERSION >= 3 -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -#if PY_MAJOR_VERSION >= 3 - -static int -psutil_posix_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - - -static int -psutil_posix_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "psutil_posix", - NULL, - sizeof(struct module_state), - PsutilMethods, - NULL, - psutil_posix_traverse, - psutil_posix_clear, - NULL -}; - -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_posix(void) - -#else -#define INITERROR return - -void init_psutil_posix(void) -#endif + #define INITERR return NULL + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_posix", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL + }; + + PyObject *PyInit__psutil_posix(void) +#else /* PY_MAJOR_VERSION */ + #define INITERR return + + void init_psutil_posix(void) +#endif /* PY_MAJOR_VERSION */ { #if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); + PyObject *mod = PyModule_Create(&moduledef); #else - PyObject *module = Py_InitModule("_psutil_posix", PsutilMethods); + PyObject *mod = Py_InitModule("_psutil_posix", mod_methods); #endif + if (mod == NULL) + INITERR; #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) || defined(PSUTIL_SUNOS) || defined(PSUTIL_AIX) - PyModule_AddIntConstant(module, "AF_LINK", AF_LINK); + if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) INITERR; #endif - if (module == NULL) - INITERROR; + if (mod == NULL) + INITERR; #if PY_MAJOR_VERSION >= 3 - return module; + return mod; #endif } From abdfe0a6de7d4539f0475e977b5da8b05319731f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Oct 2019 14:45:42 +0800 Subject: [PATCH 0357/1714] appveyor: add python 3.8; drop 3.5 --- appveyor.yml | 16 ++++++++-------- scripts/internal/download_exes.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 38a6a8e0a8..f58fa51c9b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,10 +15,6 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" @@ -27,16 +23,16 @@ environment: PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python38" + PYTHON_VERSION: "3.8.x" + PYTHON_ARCH: "32" + # 64 bits - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" @@ -45,6 +41,10 @@ environment: PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python38-x64" + PYTHON_VERSION: "3.8.x" + PYTHON_ARCH: "64" + init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index 1b72a17775..3f0be8ead0 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -26,7 +26,7 @@ BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7'] +PY_VERSIONS = ['2.7', '3.6', '3.7', '3.8'] TIMEOUT = 30 COLORS = True From 867572cc54af7142b577ff02e22f05541e4008ad Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 24 Oct 2019 02:49:43 -0700 Subject: [PATCH 0358/1714] fix compiler warnings --- make.bat | 4 ++-- psutil/_psutil_windows.c | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/make.bat b/make.bat index d47eaecc78..ae897aa5f6 100644 --- a/make.bat +++ b/make.bat @@ -20,8 +20,8 @@ rem set PYTHON=C:\Python34\python.exe & set TSCRIPT=foo.py & make.bat test rem ========================================================================== if "%PYTHON%" == "" ( - if exist "C:\Python37\python.exe" ( - set PYTHON=C:\Python37\python.exe + if exist "C:\Python38-64\python.exe" ( + set PYTHON=C:\Python38-64\python.exe ) else ( set PYTHON=C:\Python27\python.exe ) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index f071648f18..66a8786d4c 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3510,9 +3510,10 @@ PsutilMethods[] = { {"cpu_freq", psutil_cpu_freq, METH_VARARGS, "Return CPU frequency."}, #if (_WIN32_WINNT >= 0x0600) // Windows Vista - {"init_loadavg_counter", psutil_init_loadavg_counter, METH_VARARGS, + {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, + METH_VARARGS, "Initializes the emulated load average calculator."}, - {"getloadavg", psutil_get_loadavg, METH_VARARGS, + {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS, "Returns the emulated POSIX-like load average."}, #endif {"sensors_battery", psutil_sensors_battery, METH_VARARGS, From 386a9288fc854626c96eb32d1a5bdd3f7f260b12 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 24 Oct 2019 03:08:35 -0700 Subject: [PATCH 0359/1714] revert last appveyor change for 3.8 --- appveyor.yml | 16 ++++++++-------- scripts/internal/download_exes.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f58fa51c9b..38a6a8e0a8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,6 +15,10 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python35" + PYTHON_VERSION: "3.5.x" + PYTHON_ARCH: "32" + - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" @@ -23,16 +27,16 @@ environment: PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python38" - PYTHON_VERSION: "3.8.x" - PYTHON_ARCH: "32" - # 64 bits - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5.x" + PYTHON_ARCH: "64" + - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" @@ -41,10 +45,6 @@ environment: PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python38-x64" - PYTHON_VERSION: "3.8.x" - PYTHON_ARCH: "64" - init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index 3f0be8ead0..1b72a17775 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -26,7 +26,7 @@ BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.6', '3.7', '3.8'] +PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7'] TIMEOUT = 30 COLORS = True From ab9f2803d64aad7426cefc1730b48569ba67eeef Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 31 Oct 2019 16:40:58 +0800 Subject: [PATCH 0360/1714] add Tidelift security contact --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 0f25238e8d..44b88e7a6b 100644 --- a/README.rst +++ b/README.rst @@ -121,6 +121,12 @@ Professional support .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme +Security +======== + +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. + Example applications ==================== @@ -508,3 +514,4 @@ Windows services .. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html .. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 +.. _Tidelift security contact: https://tidelift.com/security From 4739c077432c94b73a57de9ecae4795ffa66cfd7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 Nov 2019 03:34:20 -0700 Subject: [PATCH 0361/1714] fix #875: convert ERROR_PARTIAL_COPY from ReadProcessMemory to AccessDenied --- HISTORY.rst | 2 ++ psutil/_psutil_windows.c | 2 +- psutil/arch/windows/process_info.c | 44 ++++++++++++++++++------------ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1e8cd0ba82..35ae6a18e9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,8 @@ XXXX-XX-XX **Bug fixes** +- 875_: [Windows] Process' cmdline(), environ() or cwd() may occasionally fail + with ERROR_PARTIAL_COPY which now gets translated to AccessDenied. - 1126_: [Linux] cpu_affinity() segfaults on CentOS 5 / manylinux. cpu_affinity() support for CentOS 5 was removed. - 1528_: [AIX] compilation error on AIX 7.2 due to 32 vs 64 bit differences. diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 66a8786d4c..beaba18307 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -807,7 +807,7 @@ psutil_GetProcWsetInformation( if (!NT_SUCCESS(status)) { if (status == STATUS_ACCESS_DENIED) { - AccessDenied(""); + AccessDenied("originated from NtQueryVirtualMemory"); } else if (psutil_pid_is_running(pid) == 0) { NoSuchProcess(""); diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 69da28bd3d..bd568d56a8 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -512,19 +512,17 @@ psutil_get_process_data(long pid, // read PEB if (!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) { - PyErr_SetFromOSErrnoWithSyscall("ReadProcessMemory"); - goto error; + goto read_process_memory_error; } // read process parameters if (!ReadProcessMemory(hProcess, - UlongToPtr(peb32.ProcessParameters), - &procParameters32, - sizeof(procParameters32), - NULL)) { - PyErr_SetFromOSErrnoWithSyscall( - "ReadProcessMemory(ProcessParameters)"); - goto error; + UlongToPtr(peb32.ProcessParameters), + &procParameters32, + sizeof(procParameters32), + NULL)) + { + goto read_process_memory_error; } switch (kind) { @@ -657,9 +655,9 @@ psutil_get_process_data(long pid, pbi.PebBaseAddress, &peb, sizeof(peb), - NULL)) { - PyErr_SetFromOSErrnoWithSyscall("ReadProcessMemory"); - goto error; + NULL)) + { + goto read_process_memory_error; } // read process parameters @@ -667,10 +665,9 @@ psutil_get_process_data(long pid, peb.ProcessParameters, &procParameters, sizeof(procParameters), - NULL)) { - PyErr_SetFromOSErrnoWithSyscall( - "ReadProcessMemory(ProcessParameters)"); - goto error; + NULL)) + { + goto read_process_memory_error; } switch (kind) { @@ -721,8 +718,7 @@ psutil_get_process_data(long pid, } else #endif if (!ReadProcessMemory(hProcess, src, buffer, size, NULL)) { - PyErr_SetFromOSErrnoWithSyscall("ReadProcessMemory"); - goto error; + goto read_process_memory_error; } CloseHandle(hProcess); @@ -732,6 +728,18 @@ psutil_get_process_data(long pid, return 0; +read_process_memory_error: + // see: https://github.com/giampaolo/psutil/issues/875 + if (GetLastError() == ERROR_PARTIAL_COPY) { + psutil_debug("ReadProcessMemory() failed with ERROR_PARTIAL_COPY; " + "converting to EACCES"); + AccessDenied("ReadProcessMemory() failed with ERROR_PARTIAL_COPY"); + } + else { + PyErr_SetFromWindowsErr(0); + } + goto error; + error: if (hProcess != NULL) CloseHandle(hProcess); From 8b91eeb1c599a582c94967e9fe7c080712d9833e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 Nov 2019 22:51:18 -0800 Subject: [PATCH 0362/1714] fix #875, win, cwd/environ/cmdline(): retry with incremental timeout in case of ERROR_PARTIAL_COPY --- psutil/_pswindows.py | 30 ++++++++++++++++++++++++ psutil/arch/windows/process_info.c | 37 ++++++++++++++++-------------- psutil/tests/test_windows.py | 10 ++++++++ 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index cf938a6f19..636b0af905 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -80,6 +80,7 @@ CONN_DELETE_TCB = "DELETE_TCB" HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") HAS_GETLOADAVG = hasattr(cext, "getloadavg") +ERROR_PARTIAL_COPY = 299 if enum is None: @@ -709,6 +710,32 @@ def wrapper(self, *args, **kwargs): return wrapper +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 x 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 + else: + raise + else: + msg = "%s retried %s times, converted to AccessDenied as it's " \ + "still returning %r" % (fun, times, err) + raise AccessDenied(pid=self.pid, name=self._name, msg=msg) + return wrapper + + class Process(object): """Wrapper class around underlying C implementation.""" @@ -772,6 +799,7 @@ def exe(self): return py2_strencode(exe) @wrap_exceptions + @retry_error_partial_copy def cmdline(self): if cext.WINVER >= cext.WINDOWS_8_1: # PEB method detects cmdline changes but requires more @@ -791,6 +819,7 @@ def cmdline(self): 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: @@ -963,6 +992,7 @@ def resume(self): 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) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index bd568d56a8..62735728d5 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -512,7 +512,10 @@ psutil_get_process_data(long pid, // read PEB if (!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) { - goto read_process_memory_error; + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + PyErr_SetFromWindowsErr(0); + goto error; } // read process parameters @@ -522,7 +525,10 @@ psutil_get_process_data(long pid, sizeof(procParameters32), NULL)) { - goto read_process_memory_error; + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + PyErr_SetFromWindowsErr(0); + goto error; } switch (kind) { @@ -657,7 +663,10 @@ psutil_get_process_data(long pid, sizeof(peb), NULL)) { - goto read_process_memory_error; + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + PyErr_SetFromWindowsErr(0); + goto error; } // read process parameters @@ -667,7 +676,10 @@ psutil_get_process_data(long pid, sizeof(procParameters), NULL)) { - goto read_process_memory_error; + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + PyErr_SetFromWindowsErr(0); + goto error; } switch (kind) { @@ -718,7 +730,10 @@ psutil_get_process_data(long pid, } else #endif if (!ReadProcessMemory(hProcess, src, buffer, size, NULL)) { - goto read_process_memory_error; + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + PyErr_SetFromWindowsErr(0); + goto error; } CloseHandle(hProcess); @@ -728,18 +743,6 @@ psutil_get_process_data(long pid, return 0; -read_process_memory_error: - // see: https://github.com/giampaolo/psutil/issues/875 - if (GetLastError() == ERROR_PARTIAL_COPY) { - psutil_debug("ReadProcessMemory() failed with ERROR_PARTIAL_COPY; " - "converting to EACCES"); - AccessDenied("ReadProcessMemory() failed with ERROR_PARTIAL_COPY"); - } - else { - PyErr_SetFromWindowsErr(0); - } - goto error; - error: if (hProcess != NULL) CloseHandle(hProcess); diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 51b53f0eb1..1075efddfd 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -517,6 +517,16 @@ def test_num_handles(self): 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) + @unittest.skipIf(not WINDOWS, "WINDOWS only") class TestProcessWMI(unittest.TestCase): From b57a644c03bf9e1edf1ede2ea46235d5dd119b84 Mon Sep 17 00:00:00 2001 From: vser1 Date: Mon, 4 Nov 2019 08:33:11 +0100 Subject: [PATCH 0363/1714] Fix variable declaration (#1607) In plain old C, variables shall be declared above Fix #1606 --- psutil/_psutil_sunos.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 49100ec667..919e76d6ec 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1120,7 +1120,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { mib2_udp6Entry_t ude6; #endif char buf[512]; - int i, flags, getcode, num_ent, state; + int i, flags, getcode, num_ent, state, ret; char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; int lport, rport; int processed_pid; @@ -1147,7 +1147,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } - int ret = ioctl(sd, I_PUSH, "tcp"); + ret = ioctl(sd, I_PUSH, "tcp"); if (ret == -1) { PyErr_SetFromErrno(PyExc_OSError); goto error; From ff5a4127f48143bb5c14a5340584c0ddb3c121ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Perrin?= Date: Mon, 4 Nov 2019 16:36:00 +0900 Subject: [PATCH 0364/1714] docs: fix TypeError in example (#1580) Terminate my children example had some error: log("process {} survived SIGTERM; trying SIGKILL" % p) TypeError: not all arguments converted during string formatting --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index dd83c6b9a9..c853abae18 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2455,7 +2455,7 @@ resources. if alive: # send SIGKILL for p in alive: - print("process {} survived SIGTERM; trying SIGKILL" % p) + print("process {} survived SIGTERM; trying SIGKILL".format(p)) try: p.kill() except psutil.NoSuchProcess: @@ -2464,7 +2464,7 @@ resources. if alive: # give up for p in alive: - print("process {} survived SIGKILL; giving up" % p) + print("process {} survived SIGKILL; giving up".format(p)) Filtering and sorting processes ------------------------------- From f551896c128bb1719d9347b09c6b66bfe6ce0838 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 4 Nov 2019 15:47:38 +0800 Subject: [PATCH 0365/1714] fix #1568: [Linux] use CC compiler env var if defined --- CREDITS | 4 ++++ HISTORY.rst | 4 +++- setup.py | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 9a70238e76..b275ce8292 100644 --- a/CREDITS +++ b/CREDITS @@ -630,3 +630,7 @@ I: 1585 N: Erwan Le Pape W: https://github.com/erwan-le-pape I: 1570 + +N: vser1 +W: https://github.com/vser1 +I: 1607 diff --git a/HISTORY.rst b/HISTORY.rst index 35ae6a18e9..a5302b8a47 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,9 +25,11 @@ XXXX-XX-XX - 1546_: usage percent may be rounded to 0 on Python 2. - 1552_: [Windows] getloadavg() math for calculating 5 and 15 mins values is incorrect. -- 1570_: [Windows] NtWow64* syscalls fail to raise the proper error code +- 1568_: [Linux] use CC compiler env var if defined. +- 1570_: [Windows] `NtWow64*` syscalls fail to raise the proper error code - 1585_: [OSX] calling close() (in C) on possible negative integers. (patch by Athos Ribeiro) +- 1606_: [SunOS] compilation fails on SunOS 5.10. (patch by vser1) 5.6.3 ===== diff --git a/setup.py b/setup.py index 04a41d99a6..99818ad504 100755 --- a/setup.py +++ b/setup.py @@ -209,6 +209,9 @@ def get_ethtool_macro(): output_dir = tempfile.mkdtemp() try: compiler = UnixCCompiler() + # https://github.com/giampaolo/psutil/pull/1568 + if os.getenv('CC'): + compiler.set_executable('compiler_so', os.getenv('CC')) with silenced_output('stderr'): with silenced_output('stdout'): compiler.compile([f.name], output_dir=output_dir) From fc204a928fb08101961a4295cad6048bd45e6829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 4 Nov 2019 07:47:54 +0000 Subject: [PATCH 0366/1714] add PEP 517/8 build backend and requirements specification (#1565) --- MANIFEST.in | 1 + pyproject.toml | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 pyproject.toml diff --git a/MANIFEST.in b/MANIFEST.in index 027e4e94bf..5532ec5d11 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -135,3 +135,4 @@ include scripts/who.py include scripts/winservices.py include setup.py include tox.ini +include pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..7f59610c6f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools >= 41.0.0", + "wheel >= 0.29.0", +] +build-backend = 'setuptools.build_meta' From 70abf32eb3c803a0a527c3784c3baca3ae041bcb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 4 Nov 2019 16:34:02 +0800 Subject: [PATCH 0367/1714] pre-release --- CREDITS | 4 + HISTORY.rst | 504 ++++++++++++++++++++++++++++++++++++++++++++++++- MANIFEST.in | 3 +- docs/index.rst | 4 + 4 files changed, 513 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index b275ce8292..71b92e1606 100644 --- a/CREDITS +++ b/CREDITS @@ -634,3 +634,7 @@ I: 1570 N: vser1 W: https://github.com/vser1 I: 1607 + +N: Bernát Gábor +W: https://github.com/gaborbernat +I: 1565 diff --git a/HISTORY.rst b/HISTORY.rst index a5302b8a47..04157cbf13 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,12 +3,14 @@ 5.6.4 ===== -XXXX-XX-XX +2019-11-04 **Enhancements** - 1527_: [Linux] added Process.cpu_times().iowait counter, which is the time spent waiting for blocking I/O to complete. +- 1565_: add PEP 517/8 build backend and requirements specification for better + pip integration. (patch by Bernát Gábor) **Bug fixes** @@ -3559,3 +3561,503 @@ DeprecationWarning. .. _1498: https://github.com/giampaolo/psutil/issues/1498 .. _1499: https://github.com/giampaolo/psutil/issues/1499 .. _1500: https://github.com/giampaolo/psutil/issues/1500 +.. _1501: https://github.com/giampaolo/psutil/issues/1501 +.. _1502: https://github.com/giampaolo/psutil/issues/1502 +.. _1503: https://github.com/giampaolo/psutil/issues/1503 +.. _1504: https://github.com/giampaolo/psutil/issues/1504 +.. _1505: https://github.com/giampaolo/psutil/issues/1505 +.. _1506: https://github.com/giampaolo/psutil/issues/1506 +.. _1507: https://github.com/giampaolo/psutil/issues/1507 +.. _1508: https://github.com/giampaolo/psutil/issues/1508 +.. _1509: https://github.com/giampaolo/psutil/issues/1509 +.. _1510: https://github.com/giampaolo/psutil/issues/1510 +.. _1511: https://github.com/giampaolo/psutil/issues/1511 +.. _1512: https://github.com/giampaolo/psutil/issues/1512 +.. _1513: https://github.com/giampaolo/psutil/issues/1513 +.. _1514: https://github.com/giampaolo/psutil/issues/1514 +.. _1515: https://github.com/giampaolo/psutil/issues/1515 +.. _1516: https://github.com/giampaolo/psutil/issues/1516 +.. _1517: https://github.com/giampaolo/psutil/issues/1517 +.. _1518: https://github.com/giampaolo/psutil/issues/1518 +.. _1519: https://github.com/giampaolo/psutil/issues/1519 +.. _1520: https://github.com/giampaolo/psutil/issues/1520 +.. _1521: https://github.com/giampaolo/psutil/issues/1521 +.. _1522: https://github.com/giampaolo/psutil/issues/1522 +.. _1523: https://github.com/giampaolo/psutil/issues/1523 +.. _1524: https://github.com/giampaolo/psutil/issues/1524 +.. _1525: https://github.com/giampaolo/psutil/issues/1525 +.. _1526: https://github.com/giampaolo/psutil/issues/1526 +.. _1527: https://github.com/giampaolo/psutil/issues/1527 +.. _1528: https://github.com/giampaolo/psutil/issues/1528 +.. _1529: https://github.com/giampaolo/psutil/issues/1529 +.. _1530: https://github.com/giampaolo/psutil/issues/1530 +.. _1531: https://github.com/giampaolo/psutil/issues/1531 +.. _1532: https://github.com/giampaolo/psutil/issues/1532 +.. _1533: https://github.com/giampaolo/psutil/issues/1533 +.. _1534: https://github.com/giampaolo/psutil/issues/1534 +.. _1535: https://github.com/giampaolo/psutil/issues/1535 +.. _1536: https://github.com/giampaolo/psutil/issues/1536 +.. _1537: https://github.com/giampaolo/psutil/issues/1537 +.. _1538: https://github.com/giampaolo/psutil/issues/1538 +.. _1539: https://github.com/giampaolo/psutil/issues/1539 +.. _1540: https://github.com/giampaolo/psutil/issues/1540 +.. _1541: https://github.com/giampaolo/psutil/issues/1541 +.. _1542: https://github.com/giampaolo/psutil/issues/1542 +.. _1543: https://github.com/giampaolo/psutil/issues/1543 +.. _1544: https://github.com/giampaolo/psutil/issues/1544 +.. _1545: https://github.com/giampaolo/psutil/issues/1545 +.. _1546: https://github.com/giampaolo/psutil/issues/1546 +.. _1547: https://github.com/giampaolo/psutil/issues/1547 +.. _1548: https://github.com/giampaolo/psutil/issues/1548 +.. _1549: https://github.com/giampaolo/psutil/issues/1549 +.. _1550: https://github.com/giampaolo/psutil/issues/1550 +.. _1551: https://github.com/giampaolo/psutil/issues/1551 +.. _1552: https://github.com/giampaolo/psutil/issues/1552 +.. _1553: https://github.com/giampaolo/psutil/issues/1553 +.. _1554: https://github.com/giampaolo/psutil/issues/1554 +.. _1555: https://github.com/giampaolo/psutil/issues/1555 +.. _1556: https://github.com/giampaolo/psutil/issues/1556 +.. _1557: https://github.com/giampaolo/psutil/issues/1557 +.. _1558: https://github.com/giampaolo/psutil/issues/1558 +.. _1559: https://github.com/giampaolo/psutil/issues/1559 +.. _1560: https://github.com/giampaolo/psutil/issues/1560 +.. _1561: https://github.com/giampaolo/psutil/issues/1561 +.. _1562: https://github.com/giampaolo/psutil/issues/1562 +.. _1563: https://github.com/giampaolo/psutil/issues/1563 +.. _1564: https://github.com/giampaolo/psutil/issues/1564 +.. _1565: https://github.com/giampaolo/psutil/issues/1565 +.. _1566: https://github.com/giampaolo/psutil/issues/1566 +.. _1567: https://github.com/giampaolo/psutil/issues/1567 +.. _1568: https://github.com/giampaolo/psutil/issues/1568 +.. _1569: https://github.com/giampaolo/psutil/issues/1569 +.. _1570: https://github.com/giampaolo/psutil/issues/1570 +.. _1571: https://github.com/giampaolo/psutil/issues/1571 +.. _1572: https://github.com/giampaolo/psutil/issues/1572 +.. _1573: https://github.com/giampaolo/psutil/issues/1573 +.. _1574: https://github.com/giampaolo/psutil/issues/1574 +.. _1575: https://github.com/giampaolo/psutil/issues/1575 +.. _1576: https://github.com/giampaolo/psutil/issues/1576 +.. _1577: https://github.com/giampaolo/psutil/issues/1577 +.. _1578: https://github.com/giampaolo/psutil/issues/1578 +.. _1579: https://github.com/giampaolo/psutil/issues/1579 +.. _1580: https://github.com/giampaolo/psutil/issues/1580 +.. _1581: https://github.com/giampaolo/psutil/issues/1581 +.. _1582: https://github.com/giampaolo/psutil/issues/1582 +.. _1583: https://github.com/giampaolo/psutil/issues/1583 +.. _1584: https://github.com/giampaolo/psutil/issues/1584 +.. _1585: https://github.com/giampaolo/psutil/issues/1585 +.. _1586: https://github.com/giampaolo/psutil/issues/1586 +.. _1587: https://github.com/giampaolo/psutil/issues/1587 +.. _1588: https://github.com/giampaolo/psutil/issues/1588 +.. _1589: https://github.com/giampaolo/psutil/issues/1589 +.. _1590: https://github.com/giampaolo/psutil/issues/1590 +.. _1591: https://github.com/giampaolo/psutil/issues/1591 +.. _1592: https://github.com/giampaolo/psutil/issues/1592 +.. _1593: https://github.com/giampaolo/psutil/issues/1593 +.. _1594: https://github.com/giampaolo/psutil/issues/1594 +.. _1595: https://github.com/giampaolo/psutil/issues/1595 +.. _1596: https://github.com/giampaolo/psutil/issues/1596 +.. _1597: https://github.com/giampaolo/psutil/issues/1597 +.. _1598: https://github.com/giampaolo/psutil/issues/1598 +.. _1599: https://github.com/giampaolo/psutil/issues/1599 +.. _1600: https://github.com/giampaolo/psutil/issues/1600 +.. _1601: https://github.com/giampaolo/psutil/issues/1601 +.. _1602: https://github.com/giampaolo/psutil/issues/1602 +.. _1603: https://github.com/giampaolo/psutil/issues/1603 +.. _1604: https://github.com/giampaolo/psutil/issues/1604 +.. _1605: https://github.com/giampaolo/psutil/issues/1605 +.. _1606: https://github.com/giampaolo/psutil/issues/1606 +.. _1607: https://github.com/giampaolo/psutil/issues/1607 +.. _1608: https://github.com/giampaolo/psutil/issues/1608 +.. _1609: https://github.com/giampaolo/psutil/issues/1609 +.. _1610: https://github.com/giampaolo/psutil/issues/1610 +.. _1611: https://github.com/giampaolo/psutil/issues/1611 +.. _1612: https://github.com/giampaolo/psutil/issues/1612 +.. _1613: https://github.com/giampaolo/psutil/issues/1613 +.. _1614: https://github.com/giampaolo/psutil/issues/1614 +.. _1615: https://github.com/giampaolo/psutil/issues/1615 +.. _1616: https://github.com/giampaolo/psutil/issues/1616 +.. _1617: https://github.com/giampaolo/psutil/issues/1617 +.. _1618: https://github.com/giampaolo/psutil/issues/1618 +.. _1619: https://github.com/giampaolo/psutil/issues/1619 +.. _1620: https://github.com/giampaolo/psutil/issues/1620 +.. _1621: https://github.com/giampaolo/psutil/issues/1621 +.. _1622: https://github.com/giampaolo/psutil/issues/1622 +.. _1623: https://github.com/giampaolo/psutil/issues/1623 +.. _1624: https://github.com/giampaolo/psutil/issues/1624 +.. _1625: https://github.com/giampaolo/psutil/issues/1625 +.. _1626: https://github.com/giampaolo/psutil/issues/1626 +.. _1627: https://github.com/giampaolo/psutil/issues/1627 +.. _1628: https://github.com/giampaolo/psutil/issues/1628 +.. _1629: https://github.com/giampaolo/psutil/issues/1629 +.. _1630: https://github.com/giampaolo/psutil/issues/1630 +.. _1631: https://github.com/giampaolo/psutil/issues/1631 +.. _1632: https://github.com/giampaolo/psutil/issues/1632 +.. _1633: https://github.com/giampaolo/psutil/issues/1633 +.. _1634: https://github.com/giampaolo/psutil/issues/1634 +.. _1635: https://github.com/giampaolo/psutil/issues/1635 +.. _1636: https://github.com/giampaolo/psutil/issues/1636 +.. _1637: https://github.com/giampaolo/psutil/issues/1637 +.. _1638: https://github.com/giampaolo/psutil/issues/1638 +.. _1639: https://github.com/giampaolo/psutil/issues/1639 +.. _1640: https://github.com/giampaolo/psutil/issues/1640 +.. _1641: https://github.com/giampaolo/psutil/issues/1641 +.. _1642: https://github.com/giampaolo/psutil/issues/1642 +.. _1643: https://github.com/giampaolo/psutil/issues/1643 +.. _1644: https://github.com/giampaolo/psutil/issues/1644 +.. _1645: https://github.com/giampaolo/psutil/issues/1645 +.. _1646: https://github.com/giampaolo/psutil/issues/1646 +.. _1647: https://github.com/giampaolo/psutil/issues/1647 +.. _1648: https://github.com/giampaolo/psutil/issues/1648 +.. _1649: https://github.com/giampaolo/psutil/issues/1649 +.. _1650: https://github.com/giampaolo/psutil/issues/1650 +.. _1651: https://github.com/giampaolo/psutil/issues/1651 +.. _1652: https://github.com/giampaolo/psutil/issues/1652 +.. _1653: https://github.com/giampaolo/psutil/issues/1653 +.. _1654: https://github.com/giampaolo/psutil/issues/1654 +.. _1655: https://github.com/giampaolo/psutil/issues/1655 +.. _1656: https://github.com/giampaolo/psutil/issues/1656 +.. _1657: https://github.com/giampaolo/psutil/issues/1657 +.. _1658: https://github.com/giampaolo/psutil/issues/1658 +.. _1659: https://github.com/giampaolo/psutil/issues/1659 +.. _1660: https://github.com/giampaolo/psutil/issues/1660 +.. _1661: https://github.com/giampaolo/psutil/issues/1661 +.. _1662: https://github.com/giampaolo/psutil/issues/1662 +.. _1663: https://github.com/giampaolo/psutil/issues/1663 +.. _1664: https://github.com/giampaolo/psutil/issues/1664 +.. _1665: https://github.com/giampaolo/psutil/issues/1665 +.. _1666: https://github.com/giampaolo/psutil/issues/1666 +.. _1667: https://github.com/giampaolo/psutil/issues/1667 +.. _1668: https://github.com/giampaolo/psutil/issues/1668 +.. _1669: https://github.com/giampaolo/psutil/issues/1669 +.. _1670: https://github.com/giampaolo/psutil/issues/1670 +.. _1671: https://github.com/giampaolo/psutil/issues/1671 +.. _1672: https://github.com/giampaolo/psutil/issues/1672 +.. _1673: https://github.com/giampaolo/psutil/issues/1673 +.. _1674: https://github.com/giampaolo/psutil/issues/1674 +.. _1675: https://github.com/giampaolo/psutil/issues/1675 +.. _1676: https://github.com/giampaolo/psutil/issues/1676 +.. _1677: https://github.com/giampaolo/psutil/issues/1677 +.. _1678: https://github.com/giampaolo/psutil/issues/1678 +.. _1679: https://github.com/giampaolo/psutil/issues/1679 +.. _1680: https://github.com/giampaolo/psutil/issues/1680 +.. _1681: https://github.com/giampaolo/psutil/issues/1681 +.. _1682: https://github.com/giampaolo/psutil/issues/1682 +.. _1683: https://github.com/giampaolo/psutil/issues/1683 +.. _1684: https://github.com/giampaolo/psutil/issues/1684 +.. _1685: https://github.com/giampaolo/psutil/issues/1685 +.. _1686: https://github.com/giampaolo/psutil/issues/1686 +.. _1687: https://github.com/giampaolo/psutil/issues/1687 +.. _1688: https://github.com/giampaolo/psutil/issues/1688 +.. _1689: https://github.com/giampaolo/psutil/issues/1689 +.. _1690: https://github.com/giampaolo/psutil/issues/1690 +.. _1691: https://github.com/giampaolo/psutil/issues/1691 +.. _1692: https://github.com/giampaolo/psutil/issues/1692 +.. _1693: https://github.com/giampaolo/psutil/issues/1693 +.. _1694: https://github.com/giampaolo/psutil/issues/1694 +.. _1695: https://github.com/giampaolo/psutil/issues/1695 +.. _1696: https://github.com/giampaolo/psutil/issues/1696 +.. _1697: https://github.com/giampaolo/psutil/issues/1697 +.. _1698: https://github.com/giampaolo/psutil/issues/1698 +.. _1699: https://github.com/giampaolo/psutil/issues/1699 +.. _1700: https://github.com/giampaolo/psutil/issues/1700 +.. _1701: https://github.com/giampaolo/psutil/issues/1701 +.. _1702: https://github.com/giampaolo/psutil/issues/1702 +.. _1703: https://github.com/giampaolo/psutil/issues/1703 +.. _1704: https://github.com/giampaolo/psutil/issues/1704 +.. _1705: https://github.com/giampaolo/psutil/issues/1705 +.. _1706: https://github.com/giampaolo/psutil/issues/1706 +.. _1707: https://github.com/giampaolo/psutil/issues/1707 +.. _1708: https://github.com/giampaolo/psutil/issues/1708 +.. _1709: https://github.com/giampaolo/psutil/issues/1709 +.. _1710: https://github.com/giampaolo/psutil/issues/1710 +.. _1711: https://github.com/giampaolo/psutil/issues/1711 +.. _1712: https://github.com/giampaolo/psutil/issues/1712 +.. _1713: https://github.com/giampaolo/psutil/issues/1713 +.. _1714: https://github.com/giampaolo/psutil/issues/1714 +.. _1715: https://github.com/giampaolo/psutil/issues/1715 +.. _1716: https://github.com/giampaolo/psutil/issues/1716 +.. _1717: https://github.com/giampaolo/psutil/issues/1717 +.. _1718: https://github.com/giampaolo/psutil/issues/1718 +.. _1719: https://github.com/giampaolo/psutil/issues/1719 +.. _1720: https://github.com/giampaolo/psutil/issues/1720 +.. _1721: https://github.com/giampaolo/psutil/issues/1721 +.. _1722: https://github.com/giampaolo/psutil/issues/1722 +.. _1723: https://github.com/giampaolo/psutil/issues/1723 +.. _1724: https://github.com/giampaolo/psutil/issues/1724 +.. _1725: https://github.com/giampaolo/psutil/issues/1725 +.. _1726: https://github.com/giampaolo/psutil/issues/1726 +.. _1727: https://github.com/giampaolo/psutil/issues/1727 +.. _1728: https://github.com/giampaolo/psutil/issues/1728 +.. _1729: https://github.com/giampaolo/psutil/issues/1729 +.. _1730: https://github.com/giampaolo/psutil/issues/1730 +.. _1731: https://github.com/giampaolo/psutil/issues/1731 +.. _1732: https://github.com/giampaolo/psutil/issues/1732 +.. _1733: https://github.com/giampaolo/psutil/issues/1733 +.. _1734: https://github.com/giampaolo/psutil/issues/1734 +.. _1735: https://github.com/giampaolo/psutil/issues/1735 +.. _1736: https://github.com/giampaolo/psutil/issues/1736 +.. _1737: https://github.com/giampaolo/psutil/issues/1737 +.. _1738: https://github.com/giampaolo/psutil/issues/1738 +.. _1739: https://github.com/giampaolo/psutil/issues/1739 +.. _1740: https://github.com/giampaolo/psutil/issues/1740 +.. _1741: https://github.com/giampaolo/psutil/issues/1741 +.. _1742: https://github.com/giampaolo/psutil/issues/1742 +.. _1743: https://github.com/giampaolo/psutil/issues/1743 +.. _1744: https://github.com/giampaolo/psutil/issues/1744 +.. _1745: https://github.com/giampaolo/psutil/issues/1745 +.. _1746: https://github.com/giampaolo/psutil/issues/1746 +.. _1747: https://github.com/giampaolo/psutil/issues/1747 +.. _1748: https://github.com/giampaolo/psutil/issues/1748 +.. _1749: https://github.com/giampaolo/psutil/issues/1749 +.. _1750: https://github.com/giampaolo/psutil/issues/1750 +.. _1751: https://github.com/giampaolo/psutil/issues/1751 +.. _1752: https://github.com/giampaolo/psutil/issues/1752 +.. _1753: https://github.com/giampaolo/psutil/issues/1753 +.. _1754: https://github.com/giampaolo/psutil/issues/1754 +.. _1755: https://github.com/giampaolo/psutil/issues/1755 +.. _1756: https://github.com/giampaolo/psutil/issues/1756 +.. _1757: https://github.com/giampaolo/psutil/issues/1757 +.. _1758: https://github.com/giampaolo/psutil/issues/1758 +.. _1759: https://github.com/giampaolo/psutil/issues/1759 +.. _1760: https://github.com/giampaolo/psutil/issues/1760 +.. _1761: https://github.com/giampaolo/psutil/issues/1761 +.. _1762: https://github.com/giampaolo/psutil/issues/1762 +.. _1763: https://github.com/giampaolo/psutil/issues/1763 +.. _1764: https://github.com/giampaolo/psutil/issues/1764 +.. _1765: https://github.com/giampaolo/psutil/issues/1765 +.. _1766: https://github.com/giampaolo/psutil/issues/1766 +.. _1767: https://github.com/giampaolo/psutil/issues/1767 +.. _1768: https://github.com/giampaolo/psutil/issues/1768 +.. _1769: https://github.com/giampaolo/psutil/issues/1769 +.. _1770: https://github.com/giampaolo/psutil/issues/1770 +.. _1771: https://github.com/giampaolo/psutil/issues/1771 +.. _1772: https://github.com/giampaolo/psutil/issues/1772 +.. _1773: https://github.com/giampaolo/psutil/issues/1773 +.. _1774: https://github.com/giampaolo/psutil/issues/1774 +.. _1775: https://github.com/giampaolo/psutil/issues/1775 +.. _1776: https://github.com/giampaolo/psutil/issues/1776 +.. _1777: https://github.com/giampaolo/psutil/issues/1777 +.. _1778: https://github.com/giampaolo/psutil/issues/1778 +.. _1779: https://github.com/giampaolo/psutil/issues/1779 +.. _1780: https://github.com/giampaolo/psutil/issues/1780 +.. _1781: https://github.com/giampaolo/psutil/issues/1781 +.. _1782: https://github.com/giampaolo/psutil/issues/1782 +.. _1783: https://github.com/giampaolo/psutil/issues/1783 +.. _1784: https://github.com/giampaolo/psutil/issues/1784 +.. _1785: https://github.com/giampaolo/psutil/issues/1785 +.. _1786: https://github.com/giampaolo/psutil/issues/1786 +.. _1787: https://github.com/giampaolo/psutil/issues/1787 +.. _1788: https://github.com/giampaolo/psutil/issues/1788 +.. _1789: https://github.com/giampaolo/psutil/issues/1789 +.. _1790: https://github.com/giampaolo/psutil/issues/1790 +.. _1791: https://github.com/giampaolo/psutil/issues/1791 +.. _1792: https://github.com/giampaolo/psutil/issues/1792 +.. _1793: https://github.com/giampaolo/psutil/issues/1793 +.. _1794: https://github.com/giampaolo/psutil/issues/1794 +.. _1795: https://github.com/giampaolo/psutil/issues/1795 +.. _1796: https://github.com/giampaolo/psutil/issues/1796 +.. _1797: https://github.com/giampaolo/psutil/issues/1797 +.. _1798: https://github.com/giampaolo/psutil/issues/1798 +.. _1799: https://github.com/giampaolo/psutil/issues/1799 +.. _1800: https://github.com/giampaolo/psutil/issues/1800 +.. _1801: https://github.com/giampaolo/psutil/issues/1801 +.. _1802: https://github.com/giampaolo/psutil/issues/1802 +.. _1803: https://github.com/giampaolo/psutil/issues/1803 +.. _1804: https://github.com/giampaolo/psutil/issues/1804 +.. _1805: https://github.com/giampaolo/psutil/issues/1805 +.. _1806: https://github.com/giampaolo/psutil/issues/1806 +.. _1807: https://github.com/giampaolo/psutil/issues/1807 +.. _1808: https://github.com/giampaolo/psutil/issues/1808 +.. _1809: https://github.com/giampaolo/psutil/issues/1809 +.. _1810: https://github.com/giampaolo/psutil/issues/1810 +.. _1811: https://github.com/giampaolo/psutil/issues/1811 +.. _1812: https://github.com/giampaolo/psutil/issues/1812 +.. _1813: https://github.com/giampaolo/psutil/issues/1813 +.. _1814: https://github.com/giampaolo/psutil/issues/1814 +.. _1815: https://github.com/giampaolo/psutil/issues/1815 +.. _1816: https://github.com/giampaolo/psutil/issues/1816 +.. _1817: https://github.com/giampaolo/psutil/issues/1817 +.. _1818: https://github.com/giampaolo/psutil/issues/1818 +.. _1819: https://github.com/giampaolo/psutil/issues/1819 +.. _1820: https://github.com/giampaolo/psutil/issues/1820 +.. _1821: https://github.com/giampaolo/psutil/issues/1821 +.. _1822: https://github.com/giampaolo/psutil/issues/1822 +.. _1823: https://github.com/giampaolo/psutil/issues/1823 +.. _1824: https://github.com/giampaolo/psutil/issues/1824 +.. _1825: https://github.com/giampaolo/psutil/issues/1825 +.. _1826: https://github.com/giampaolo/psutil/issues/1826 +.. _1827: https://github.com/giampaolo/psutil/issues/1827 +.. _1828: https://github.com/giampaolo/psutil/issues/1828 +.. _1829: https://github.com/giampaolo/psutil/issues/1829 +.. _1830: https://github.com/giampaolo/psutil/issues/1830 +.. _1831: https://github.com/giampaolo/psutil/issues/1831 +.. _1832: https://github.com/giampaolo/psutil/issues/1832 +.. _1833: https://github.com/giampaolo/psutil/issues/1833 +.. _1834: https://github.com/giampaolo/psutil/issues/1834 +.. _1835: https://github.com/giampaolo/psutil/issues/1835 +.. _1836: https://github.com/giampaolo/psutil/issues/1836 +.. _1837: https://github.com/giampaolo/psutil/issues/1837 +.. _1838: https://github.com/giampaolo/psutil/issues/1838 +.. _1839: https://github.com/giampaolo/psutil/issues/1839 +.. _1840: https://github.com/giampaolo/psutil/issues/1840 +.. _1841: https://github.com/giampaolo/psutil/issues/1841 +.. _1842: https://github.com/giampaolo/psutil/issues/1842 +.. _1843: https://github.com/giampaolo/psutil/issues/1843 +.. _1844: https://github.com/giampaolo/psutil/issues/1844 +.. _1845: https://github.com/giampaolo/psutil/issues/1845 +.. _1846: https://github.com/giampaolo/psutil/issues/1846 +.. _1847: https://github.com/giampaolo/psutil/issues/1847 +.. _1848: https://github.com/giampaolo/psutil/issues/1848 +.. _1849: https://github.com/giampaolo/psutil/issues/1849 +.. _1850: https://github.com/giampaolo/psutil/issues/1850 +.. _1851: https://github.com/giampaolo/psutil/issues/1851 +.. _1852: https://github.com/giampaolo/psutil/issues/1852 +.. _1853: https://github.com/giampaolo/psutil/issues/1853 +.. _1854: https://github.com/giampaolo/psutil/issues/1854 +.. _1855: https://github.com/giampaolo/psutil/issues/1855 +.. _1856: https://github.com/giampaolo/psutil/issues/1856 +.. _1857: https://github.com/giampaolo/psutil/issues/1857 +.. _1858: https://github.com/giampaolo/psutil/issues/1858 +.. _1859: https://github.com/giampaolo/psutil/issues/1859 +.. _1860: https://github.com/giampaolo/psutil/issues/1860 +.. _1861: https://github.com/giampaolo/psutil/issues/1861 +.. _1862: https://github.com/giampaolo/psutil/issues/1862 +.. _1863: https://github.com/giampaolo/psutil/issues/1863 +.. _1864: https://github.com/giampaolo/psutil/issues/1864 +.. _1865: https://github.com/giampaolo/psutil/issues/1865 +.. _1866: https://github.com/giampaolo/psutil/issues/1866 +.. _1867: https://github.com/giampaolo/psutil/issues/1867 +.. _1868: https://github.com/giampaolo/psutil/issues/1868 +.. _1869: https://github.com/giampaolo/psutil/issues/1869 +.. _1870: https://github.com/giampaolo/psutil/issues/1870 +.. _1871: https://github.com/giampaolo/psutil/issues/1871 +.. _1872: https://github.com/giampaolo/psutil/issues/1872 +.. _1873: https://github.com/giampaolo/psutil/issues/1873 +.. _1874: https://github.com/giampaolo/psutil/issues/1874 +.. _1875: https://github.com/giampaolo/psutil/issues/1875 +.. _1876: https://github.com/giampaolo/psutil/issues/1876 +.. _1877: https://github.com/giampaolo/psutil/issues/1877 +.. _1878: https://github.com/giampaolo/psutil/issues/1878 +.. _1879: https://github.com/giampaolo/psutil/issues/1879 +.. _1880: https://github.com/giampaolo/psutil/issues/1880 +.. _1881: https://github.com/giampaolo/psutil/issues/1881 +.. _1882: https://github.com/giampaolo/psutil/issues/1882 +.. _1883: https://github.com/giampaolo/psutil/issues/1883 +.. _1884: https://github.com/giampaolo/psutil/issues/1884 +.. _1885: https://github.com/giampaolo/psutil/issues/1885 +.. _1886: https://github.com/giampaolo/psutil/issues/1886 +.. _1887: https://github.com/giampaolo/psutil/issues/1887 +.. _1888: https://github.com/giampaolo/psutil/issues/1888 +.. _1889: https://github.com/giampaolo/psutil/issues/1889 +.. _1890: https://github.com/giampaolo/psutil/issues/1890 +.. _1891: https://github.com/giampaolo/psutil/issues/1891 +.. _1892: https://github.com/giampaolo/psutil/issues/1892 +.. _1893: https://github.com/giampaolo/psutil/issues/1893 +.. _1894: https://github.com/giampaolo/psutil/issues/1894 +.. _1895: https://github.com/giampaolo/psutil/issues/1895 +.. _1896: https://github.com/giampaolo/psutil/issues/1896 +.. _1897: https://github.com/giampaolo/psutil/issues/1897 +.. _1898: https://github.com/giampaolo/psutil/issues/1898 +.. _1899: https://github.com/giampaolo/psutil/issues/1899 +.. _1900: https://github.com/giampaolo/psutil/issues/1900 +.. _1901: https://github.com/giampaolo/psutil/issues/1901 +.. _1902: https://github.com/giampaolo/psutil/issues/1902 +.. _1903: https://github.com/giampaolo/psutil/issues/1903 +.. _1904: https://github.com/giampaolo/psutil/issues/1904 +.. _1905: https://github.com/giampaolo/psutil/issues/1905 +.. _1906: https://github.com/giampaolo/psutil/issues/1906 +.. _1907: https://github.com/giampaolo/psutil/issues/1907 +.. _1908: https://github.com/giampaolo/psutil/issues/1908 +.. _1909: https://github.com/giampaolo/psutil/issues/1909 +.. _1910: https://github.com/giampaolo/psutil/issues/1910 +.. _1911: https://github.com/giampaolo/psutil/issues/1911 +.. _1912: https://github.com/giampaolo/psutil/issues/1912 +.. _1913: https://github.com/giampaolo/psutil/issues/1913 +.. _1914: https://github.com/giampaolo/psutil/issues/1914 +.. _1915: https://github.com/giampaolo/psutil/issues/1915 +.. _1916: https://github.com/giampaolo/psutil/issues/1916 +.. _1917: https://github.com/giampaolo/psutil/issues/1917 +.. _1918: https://github.com/giampaolo/psutil/issues/1918 +.. _1919: https://github.com/giampaolo/psutil/issues/1919 +.. _1920: https://github.com/giampaolo/psutil/issues/1920 +.. _1921: https://github.com/giampaolo/psutil/issues/1921 +.. _1922: https://github.com/giampaolo/psutil/issues/1922 +.. _1923: https://github.com/giampaolo/psutil/issues/1923 +.. _1924: https://github.com/giampaolo/psutil/issues/1924 +.. _1925: https://github.com/giampaolo/psutil/issues/1925 +.. _1926: https://github.com/giampaolo/psutil/issues/1926 +.. _1927: https://github.com/giampaolo/psutil/issues/1927 +.. _1928: https://github.com/giampaolo/psutil/issues/1928 +.. _1929: https://github.com/giampaolo/psutil/issues/1929 +.. _1930: https://github.com/giampaolo/psutil/issues/1930 +.. _1931: https://github.com/giampaolo/psutil/issues/1931 +.. _1932: https://github.com/giampaolo/psutil/issues/1932 +.. _1933: https://github.com/giampaolo/psutil/issues/1933 +.. _1934: https://github.com/giampaolo/psutil/issues/1934 +.. _1935: https://github.com/giampaolo/psutil/issues/1935 +.. _1936: https://github.com/giampaolo/psutil/issues/1936 +.. _1937: https://github.com/giampaolo/psutil/issues/1937 +.. _1938: https://github.com/giampaolo/psutil/issues/1938 +.. _1939: https://github.com/giampaolo/psutil/issues/1939 +.. _1940: https://github.com/giampaolo/psutil/issues/1940 +.. _1941: https://github.com/giampaolo/psutil/issues/1941 +.. _1942: https://github.com/giampaolo/psutil/issues/1942 +.. _1943: https://github.com/giampaolo/psutil/issues/1943 +.. _1944: https://github.com/giampaolo/psutil/issues/1944 +.. _1945: https://github.com/giampaolo/psutil/issues/1945 +.. _1946: https://github.com/giampaolo/psutil/issues/1946 +.. _1947: https://github.com/giampaolo/psutil/issues/1947 +.. _1948: https://github.com/giampaolo/psutil/issues/1948 +.. _1949: https://github.com/giampaolo/psutil/issues/1949 +.. _1950: https://github.com/giampaolo/psutil/issues/1950 +.. _1951: https://github.com/giampaolo/psutil/issues/1951 +.. _1952: https://github.com/giampaolo/psutil/issues/1952 +.. _1953: https://github.com/giampaolo/psutil/issues/1953 +.. _1954: https://github.com/giampaolo/psutil/issues/1954 +.. _1955: https://github.com/giampaolo/psutil/issues/1955 +.. _1956: https://github.com/giampaolo/psutil/issues/1956 +.. _1957: https://github.com/giampaolo/psutil/issues/1957 +.. _1958: https://github.com/giampaolo/psutil/issues/1958 +.. _1959: https://github.com/giampaolo/psutil/issues/1959 +.. _1960: https://github.com/giampaolo/psutil/issues/1960 +.. _1961: https://github.com/giampaolo/psutil/issues/1961 +.. _1962: https://github.com/giampaolo/psutil/issues/1962 +.. _1963: https://github.com/giampaolo/psutil/issues/1963 +.. _1964: https://github.com/giampaolo/psutil/issues/1964 +.. _1965: https://github.com/giampaolo/psutil/issues/1965 +.. _1966: https://github.com/giampaolo/psutil/issues/1966 +.. _1967: https://github.com/giampaolo/psutil/issues/1967 +.. _1968: https://github.com/giampaolo/psutil/issues/1968 +.. _1969: https://github.com/giampaolo/psutil/issues/1969 +.. _1970: https://github.com/giampaolo/psutil/issues/1970 +.. _1971: https://github.com/giampaolo/psutil/issues/1971 +.. _1972: https://github.com/giampaolo/psutil/issues/1972 +.. _1973: https://github.com/giampaolo/psutil/issues/1973 +.. _1974: https://github.com/giampaolo/psutil/issues/1974 +.. _1975: https://github.com/giampaolo/psutil/issues/1975 +.. _1976: https://github.com/giampaolo/psutil/issues/1976 +.. _1977: https://github.com/giampaolo/psutil/issues/1977 +.. _1978: https://github.com/giampaolo/psutil/issues/1978 +.. _1979: https://github.com/giampaolo/psutil/issues/1979 +.. _1980: https://github.com/giampaolo/psutil/issues/1980 +.. _1981: https://github.com/giampaolo/psutil/issues/1981 +.. _1982: https://github.com/giampaolo/psutil/issues/1982 +.. _1983: https://github.com/giampaolo/psutil/issues/1983 +.. _1984: https://github.com/giampaolo/psutil/issues/1984 +.. _1985: https://github.com/giampaolo/psutil/issues/1985 +.. _1986: https://github.com/giampaolo/psutil/issues/1986 +.. _1987: https://github.com/giampaolo/psutil/issues/1987 +.. _1988: https://github.com/giampaolo/psutil/issues/1988 +.. _1989: https://github.com/giampaolo/psutil/issues/1989 +.. _1990: https://github.com/giampaolo/psutil/issues/1990 +.. _1991: https://github.com/giampaolo/psutil/issues/1991 +.. _1992: https://github.com/giampaolo/psutil/issues/1992 +.. _1993: https://github.com/giampaolo/psutil/issues/1993 +.. _1994: https://github.com/giampaolo/psutil/issues/1994 +.. _1995: https://github.com/giampaolo/psutil/issues/1995 +.. _1996: https://github.com/giampaolo/psutil/issues/1996 +.. _1997: https://github.com/giampaolo/psutil/issues/1997 +.. _1998: https://github.com/giampaolo/psutil/issues/1998 +.. _1999: https://github.com/giampaolo/psutil/issues/1999 +.. _2000: https://github.com/giampaolo/psutil/issues/2000 diff --git a/MANIFEST.in b/MANIFEST.in index 5532ec5d11..d876552a09 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -97,6 +97,7 @@ include psutil/tests/test_sunos.py include psutil/tests/test_system.py include psutil/tests/test_unicode.py include psutil/tests/test_windows.py +include pyproject.toml include scripts/battery.py include scripts/cpu_distribution.py include scripts/disk_usage.py @@ -109,6 +110,7 @@ include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py include scripts/internal/download_exes.py +include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py @@ -135,4 +137,3 @@ include scripts/who.py include scripts/winservices.py include setup.py include tox.ini -include pyproject.toml diff --git a/docs/index.rst b/docs/index.rst index c853abae18..e86fd419fd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2635,6 +2635,10 @@ take a look at the `development guide`_. Timeline ======== +- 2019-11-04: + `5.6.4 `__ - + `what's new `__ - + `diff `__ - 2019-06-11: `5.6.3 `__ - `what's new `__ - From 35400e8f17087a69fc7ce672f6eb172698b04f88 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Nov 2019 16:53:02 +0800 Subject: [PATCH 0368/1714] #1615, 1614: remove pyproject.toml --- HISTORY.rst | 9 +++++++++ docs/index.rst | 4 ++++ psutil/__init__.py | 2 +- pyproject.toml | 6 ------ 4 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 pyproject.toml diff --git a/HISTORY.rst b/HISTORY.rst index 04157cbf13..698db920c4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,14 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.6.5 +===== + +2019-11-06 + +**Bug fixes** + +- 1615_: remove pyproject.toml as it was causing installation issues. + 5.6.4 ===== diff --git a/docs/index.rst b/docs/index.rst index e86fd419fd..74a4c36e05 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2635,6 +2635,10 @@ take a look at the `development guide`_. Timeline ======== +- 2019-11-06: + `5.6.5 `__ - + `what's new `__ - + `diff `__ - 2019-11-04: `5.6.4 `__ - `what's new `__ - diff --git a/psutil/__init__.py b/psutil/__init__.py index 62dadc90c9..3f380589a4 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -222,7 +222,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.4" +__version__ = "5.6.5" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 7f59610c6f..0000000000 --- a/pyproject.toml +++ /dev/null @@ -1,6 +0,0 @@ -[build-system] -requires = [ - "setuptools >= 41.0.0", - "wheel >= 0.29.0", -] -build-backend = 'setuptools.build_meta' From b17eda56735b1e93a907355b4436f118d1333a37 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Nov 2019 16:53:37 +0800 Subject: [PATCH 0369/1714] #1615, 1614: remove pyproject.toml --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index d876552a09..d26c2d74a6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -97,7 +97,6 @@ include psutil/tests/test_sunos.py include psutil/tests/test_system.py include psutil/tests/test_unicode.py include psutil/tests/test_windows.py -include pyproject.toml include scripts/battery.py include scripts/cpu_distribution.py include scripts/disk_usage.py From ef032158fd72790ab2be14e99c45c32d66d96291 Mon Sep 17 00:00:00 2001 From: Nathan Houghton Date: Tue, 12 Nov 2019 03:59:49 -0800 Subject: [PATCH 0370/1714] Fix _psutil_bsd.c compile error on OpenBSD (#1619) --- psutil/_psutil_bsd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index b4264ceb76..33bb0c2df2 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1040,7 +1040,7 @@ static PyMethodDef mod_methods[] = { if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) INITERR; if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) INITERR; if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) INITERR; - if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB); INITERR; // unused + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) INITERR; // unused if (PyModule_AddIntConstant(mod, "SDEAD", SDEAD)) INITERR; if (PyModule_AddIntConstant(mod, "SONPROC", SONPROC)) INITERR; #elif defined(PSUTIL_NETBSD) From 8e18da55f39622378dfa579ea9cb6d909ab69576 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Nov 2019 21:25:49 +0800 Subject: [PATCH 0371/1714] update HISTORY --- CREDITS | 4 ++++ HISTORY.rst | 10 ++++++++++ psutil/__init__.py | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 71b92e1606..3af651fb72 100644 --- a/CREDITS +++ b/CREDITS @@ -638,3 +638,7 @@ I: 1607 N: Bernát Gábor W: https://github.com/gaborbernat I: 1565 + +N: Nathan Houghton +W: https://github.com/n1000 +I: 1619 diff --git a/HISTORY.rst b/HISTORY.rst index 698db920c4..f5c5e2e932 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.6.6 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 1619_: [OpenBSD] compilation fails due to C syntax error. (patch by Nathan + Houghton) + 5.6.5 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 3f380589a4..4df2fec0ca 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -222,7 +222,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.5" +__version__ = "5.6.6" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From 7d512c8e4442a896d56505be3e78f1156f443465 Mon Sep 17 00:00:00 2001 From: Riccardo Schirone Date: Wed, 13 Nov 2019 14:54:21 +0100 Subject: [PATCH 0372/1714] Use Py_CLEAR instead of Py_DECREF to also set the variable to NULL (#1616) These files contain loops that convert system data into python objects and during the process they create objects and dereference their refcounts after they have been added to the resulting list. However, in case of errors during the creation of those python objects, the refcount to previously allocated objects is dropped again with Py_XDECREF, which should be a no-op in case the paramater is NULL. Even so, in most of these loops the variables pointing to the objects are never set to NULL, even after Py_DECREF is called at the end of the loop iteration. This means, after the first iteration, if an error occurs those python objects will get their refcount dropped two times, resulting in a possible double-free. --- psutil/_psutil_aix.c | 18 +++++++------- psutil/_psutil_bsd.c | 30 +++++++++++----------- psutil/_psutil_linux.c | 14 +++++------ psutil/_psutil_osx.c | 39 ++++++++++++++--------------- psutil/_psutil_sunos.c | 43 ++++++++++++++++---------------- psutil/_psutil_windows.c | 54 ++++++++++++++++++++-------------------- 6 files changed, 97 insertions(+), 101 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 8c055a4320..9f58f606b0 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -265,8 +265,8 @@ psutil_proc_environ(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItem(py_retdict, py_key, py_val)) goto error; - Py_DECREF(py_key); - Py_DECREF(py_val); + Py_CLEAR(py_key); + Py_CLEAR(py_val); } curvar = strchr(curvar, '\0') + 1; } @@ -510,10 +510,10 @@ psutil_users(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(py_tuple); + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); } endutxent(); @@ -570,9 +570,9 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(py_tuple); + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); mt = getmntent(file); } endmntent(file); diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 33bb0c2df2..e07ddcc1a2 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -154,7 +154,7 @@ psutil_pids(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_pid)) goto error; - Py_DECREF(py_pid); + Py_CLEAR(py_pid); proclist++; } free(orig_address); @@ -507,8 +507,8 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_path); - Py_DECREF(py_tuple); + Py_CLEAR(py_path); + Py_CLEAR(py_tuple); } } free(freep); @@ -670,9 +670,9 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(py_tuple); + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); } free(fs); @@ -765,7 +765,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) goto error; - Py_DECREF(py_ifc_info); + Py_CLEAR(py_ifc_info); } else { continue; @@ -840,10 +840,10 @@ psutil_users(PyObject *self, PyObject *args) { fclose(fp); goto error; } - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(py_tuple); + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); } fclose(fp); @@ -883,10 +883,10 @@ psutil_users(PyObject *self, PyObject *args) { endutxent(); goto error; } - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(py_tuple); + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); } endutxent(); diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 717723d088..0d16eb4276 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -241,9 +241,9 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(py_tuple); + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); } endmntent(file); return py_retlist; @@ -454,10 +454,10 @@ psutil_users(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(py_tuple); + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); } endutent(); return py_retlist; diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index d2ca94b586..76ec0ee850 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -138,7 +138,7 @@ psutil_pids(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_pid)) goto error; - Py_DECREF(py_pid); + Py_CLEAR(py_pid); proclist++; } free(orig_address); @@ -653,7 +653,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_cputime)) goto error; - Py_DECREF(py_cputime); + Py_CLEAR(py_cputime); } ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, @@ -841,9 +841,9 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(py_tuple); + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); } free(fs); @@ -911,7 +911,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } for (j = 0; j < thread_count; j++) { - py_tuple = NULL; thread_info_count = THREAD_INFO_MAX; kr = thread_info(thread_list[j], THREAD_BASIC_INFO, (thread_info_t)thinfo_basic, &thread_info_count); @@ -934,7 +933,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } ret = vm_deallocate(task, (vm_address_t)thread_list, @@ -1043,10 +1042,8 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); - py_tuple = NULL; - Py_DECREF(py_path); - py_path = NULL; + Py_CLEAR(py_tuple); + Py_CLEAR(py_path); // --- /construct python list } } @@ -1226,7 +1223,7 @@ psutil_proc_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } else if (family == AF_UNIX) { py_laddr = PyUnicode_DecodeFSDefault( @@ -1248,9 +1245,9 @@ psutil_proc_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); - Py_DECREF(py_laddr); - Py_DECREF(py_raddr); + Py_CLEAR(py_tuple); + Py_CLEAR(py_laddr); + Py_CLEAR(py_raddr); } } } @@ -1370,7 +1367,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) goto error; - Py_DECREF(py_ifc_info); + Py_CLEAR(py_ifc_info); } else { continue; @@ -1543,7 +1540,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) goto error; - Py_DECREF(py_disk_info); + Py_CLEAR(py_disk_info); CFRelease(parent_dict); IOObjectRelease(parent); @@ -1605,10 +1602,10 @@ psutil_users(PyObject *self, PyObject *args) { endutxent(); goto error; } - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(py_tuple); + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); } endutxent(); diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 919e76d6ec..31d6f364fb 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -300,8 +300,8 @@ psutil_proc_environ(PyObject *self, PyObject *args) { if (PyDict_SetItem(py_retdict, py_envname, py_envval) < 0) goto error; - Py_DECREF(py_envname); - Py_DECREF(py_envval); + Py_CLEAR(py_envname); + Py_CLEAR(py_envval); } psutil_free_cstrings_array(env, env_count); @@ -655,10 +655,10 @@ psutil_users(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(py_tuple); + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); } endutxent(); @@ -714,9 +714,9 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(py_tuple); + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); } fclose(file); return py_retlist; @@ -767,8 +767,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_cputime)) goto error; - Py_DECREF(py_cputime); - py_cputime = NULL; + Py_CLEAR(py_cputime); } } @@ -824,7 +823,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_disk_info)) goto error; - Py_DECREF(py_disk_info); + Py_CLEAR(py_disk_info); } } ksp = ksp->ks_next; @@ -959,8 +958,8 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_path); - Py_DECREF(py_tuple); + Py_CLEAR(py_path); + Py_CLEAR(py_tuple); // increment pointer p += 1; @@ -1075,7 +1074,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_ifc_info)) goto error; - Py_DECREF(py_ifc_info); + Py_CLEAR(py_ifc_info); goto next; next: @@ -1273,7 +1272,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } } #if defined(AF_INET6) @@ -1287,7 +1286,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { #ifdef NEW_MIB_COMPLIANT processed_pid = tp6.tcp6ConnCreationProcess; #else - processed_pid = 0; + processed_pid = 0; #endif if (pid != -1 && processed_pid != pid) continue; @@ -1316,14 +1315,14 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } } #endif // UDPv4 else if (mibhdr.level == MIB2_UDP || mibhdr.level == MIB2_UDP_ENTRY) { num_ent = mibhdr.len / sizeof(mib2_udpEntry_t); - assert(num_ent * sizeof(mib2_udpEntry_t) == mibhdr.len); + assert(num_ent * sizeof(mib2_udpEntry_t) == mibhdr.len); for (i = 0; i < num_ent; i++) { memcpy(&ude, databuf.buf + i * sizeof ude, sizeof ude); #ifdef NEW_MIB_COMPLIANT @@ -1355,7 +1354,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } } #if defined(AF_INET6) @@ -1388,7 +1387,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } } #endif @@ -1561,7 +1560,7 @@ psutil_net_if_stats(PyObject* self, PyObject* args) { goto error; if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_ifc_info)) goto error; - Py_DECREF(py_ifc_info); + Py_CLEAR(py_ifc_info); } } diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index beaba18307..08b208dc0b 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -221,7 +221,7 @@ psutil_pids(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_pid)) goto error; - Py_DECREF(py_pid); + Py_CLEAR(py_pid); } // free C array allocated for PIDs @@ -1003,7 +1003,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } free(sppi); @@ -1156,7 +1156,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); CloseHandle(hThread); } @@ -1580,7 +1580,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_conn_tuple)) goto error; - Py_DECREF(py_conn_tuple); + Py_CLEAR(py_conn_tuple); } free(table); @@ -1667,7 +1667,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_conn_tuple)) goto error; - Py_DECREF(py_conn_tuple); + Py_CLEAR(py_conn_tuple); } free(table); @@ -1730,7 +1730,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_conn_tuple)) goto error; - Py_DECREF(py_conn_tuple); + Py_CLEAR(py_conn_tuple); } free(table); @@ -1793,7 +1793,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_conn_tuple)) goto error; - Py_DECREF(py_conn_tuple); + Py_CLEAR(py_conn_tuple); } free(table); @@ -2188,8 +2188,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItem(py_retdict, py_nic_name, py_nic_info)) goto error; - Py_XDECREF(py_nic_name); - Py_XDECREF(py_nic_info); + Py_CLEAR(py_nic_name); + Py_CLEAR(py_nic_info); free(pIfRow); pCurrAddresses = pCurrAddresses->Next; @@ -2304,7 +2304,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItemString(py_retdict, szDeviceDisplay, py_tuple)) goto error; - Py_XDECREF(py_tuple); + Py_CLEAR(py_tuple); next: CloseHandle(hDevice); @@ -2461,7 +2461,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; } - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); // Continue looking for more mount points mp_flag = FindNextVolumeMountPoint(mp_h, mp_buf, MAX_PATH); @@ -2486,7 +2486,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); goto next; next: @@ -2610,9 +2610,9 @@ psutil_users(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_XDECREF(py_username); - Py_XDECREF(py_address); - Py_XDECREF(py_tuple); + Py_CLEAR(py_username); + Py_CLEAR(py_address); + Py_CLEAR(py_tuple); } WTSFreeMemory(sessions); @@ -2838,8 +2838,8 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); - Py_DECREF(py_str); + Py_CLEAR(py_tuple); + Py_CLEAR(py_str); } previousAllocationBase = (ULONGLONG)basicInfo.AllocationBase; baseAddress = (PCHAR)baseAddress + basicInfo.RegionSize; @@ -2889,8 +2889,8 @@ psutil_ppid_map(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItem(py_retdict, py_pid, py_ppid)) goto error; - Py_DECREF(py_pid); - Py_DECREF(py_ppid); + Py_CLEAR(py_pid); + Py_CLEAR(py_ppid); } while (Process32Next(handle, &pe)); } @@ -2993,8 +2993,8 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); - Py_DECREF(py_mac_address); + Py_CLEAR(py_tuple); + Py_CLEAR(py_mac_address); } // find out the IP address associated with the NIC @@ -3070,14 +3070,14 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); - Py_DECREF(py_address); - Py_DECREF(py_netmask); + Py_CLEAR(py_tuple); + Py_CLEAR(py_address); + Py_CLEAR(py_netmask); pUnicast = pUnicast->Next; } } - Py_DECREF(py_nic_name); + Py_CLEAR(py_nic_name); pCurrAddresses = pCurrAddresses->Next; } @@ -3197,8 +3197,8 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { goto error; if (PyDict_SetItem(py_retdict, py_nic_name, py_ifc_info)) goto error; - Py_DECREF(py_nic_name); - Py_DECREF(py_ifc_info); + Py_CLEAR(py_nic_name); + Py_CLEAR(py_ifc_info); } free(pIfTable); From 3a9bccfd2c6d2e6538298cd3892058b1204056e0 Mon Sep 17 00:00:00 2001 From: Riccardo Schirone Date: Mon, 18 Nov 2019 15:51:39 +0100 Subject: [PATCH 0373/1714] psutil/_psutil_posix.c: better clear variables to ensure they are NULL (#1624) --- psutil/_psutil_posix.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 209e787d5b..aa60084917 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -324,11 +324,11 @@ psutil_net_if_addrs(PyObject* self, PyObject* args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); - Py_DECREF(py_address); - Py_DECREF(py_netmask); - Py_DECREF(py_broadcast); - Py_DECREF(py_ptp); + Py_CLEAR(py_tuple); + Py_CLEAR(py_address); + Py_CLEAR(py_netmask); + Py_CLEAR(py_broadcast); + Py_CLEAR(py_ptp); } freeifaddrs(ifaddr); From d84fb073fda024d9320e965257f22fb73a4497b6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 20 Nov 2019 06:36:04 +0200 Subject: [PATCH 0374/1714] Run slower jobs first so they don't hold up the CI (#1626) --- .travis.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index b3b0102dc4..9753314474 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,6 @@ language: python cache: pip matrix: include: - # Linux - - python: 2.7 - - python: 3.4 - - python: 3.5 - - python: 3.6 - - python: 3.7 - dist: xenial # macOS - language: generic os: osx @@ -16,6 +9,13 @@ matrix: - language: generic os: osx env: PYVER=py36 + # Linux + - python: 2.7 + - python: 3.4 + - python: 3.5 + - python: 3.6 + - python: 3.7 + dist: xenial # pypy # - python: pypy # - python: pypy3 From 0aeb7697be98017c23736328a5debc38d172e1c7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Nov 2019 13:02:03 +0800 Subject: [PATCH 0375/1714] update CREDITS --- CREDITS | 5 +++++ HISTORY.rst | 2 ++ 2 files changed, 7 insertions(+) diff --git a/CREDITS b/CREDITS index 3af651fb72..330f4f995c 100644 --- a/CREDITS +++ b/CREDITS @@ -642,3 +642,8 @@ I: 1565 N: Nathan Houghton W: https://github.com/n1000 I: 1619 + +N: Riccardo Schirone +W: https://github.com/ret2libc +C: Milano, Italy +I: 1616 diff --git a/HISTORY.rst b/HISTORY.rst index f5c5e2e932..5aaf9da123 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ XXXX-XX-XX **Bug fixes** +- 1616_: use of Py_DECREF instead of Py_CLEAR will result in double free and + segfault (CVE). (patch by Riccardo Schirone) - 1619_: [OpenBSD] compilation fails due to C syntax error. (patch by Nathan Houghton) From b316e6ae32870e3c69ff06ff13097788ee4d250c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 20 Nov 2019 08:13:02 +0200 Subject: [PATCH 0376/1714] travis/appveyor: add support for Python 3.8 (#1625) --- .travis.yml | 2 +- appveyor.yml | 8 ++++++++ tox.ini | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9753314474..ced00a0918 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: - python: 3.5 - python: 3.6 - python: 3.7 - dist: xenial + - python: 3.8 # pypy # - python: pypy # - python: pypy3 diff --git a/appveyor.yml b/appveyor.yml index 38a6a8e0a8..a99670e30d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,6 +27,10 @@ environment: PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python38" + PYTHON_VERSION: "3.8.x" + PYTHON_ARCH: "32" + # 64 bits - PYTHON: "C:\\Python27-x64" @@ -45,6 +49,10 @@ environment: PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python38-x64" + PYTHON_VERSION: "3.8.x" + PYTHON_ARCH: "64" + init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" diff --git a/tox.ini b/tox.ini index 947fbeda7b..2698eb6a87 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ # directory. [tox] -envlist = py26, py27, py34, py35, py36, py37, lint +envlist = py26, py27, py34, py35, py36, py37, py38, lint [testenv] deps = From e6faebcd7adaa327d1ce57385cbebe7724d02350 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Nov 2019 14:38:07 +0800 Subject: [PATCH 0377/1714] release gil around users()/BSD (#1425) --- psutil/_psutil_bsd.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index e07ddcc1a2..723d6198c3 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -802,7 +802,9 @@ psutil_users(PyObject *self, PyObject *args) { struct utmp ut; FILE *fp; + Py_BEGIN_ALLOW_THREADS fp = fopen(_PATH_UTMP, "r"); + Py_END_ALLOW_THREADS if (fp == NULL) { PyErr_SetFromErrnoWithFilename(PyExc_OSError, _PATH_UTMP); goto error; From 72c84cb4edb5c0968a83c1f45ad5cc51235e0af3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Nov 2019 00:46:35 -0800 Subject: [PATCH 0378/1714] #fix #1595 / windows: kill() may not raise AccessDenied --- HISTORY.rst | 1 + psutil/_psutil_windows.c | 30 +++++++++++++++++++++++------- scripts/internal/download_exes.py | 2 +- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5aaf9da123..045bb5d5f0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ XXXX-XX-XX **Bug fixes** +- 1595_: [Windows] Process.kill() may not throw AccessDenied. - 1616_: use of Py_DECREF instead of Py_CLEAR will result in double free and segfault (CVE). (patch by Riccardo Schirone) - 1619_: [OpenBSD] compilation fails due to C syntax error. (patch by Nathan diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 08b208dc0b..e01fa50d21 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -243,8 +243,8 @@ psutil_pids(PyObject *self, PyObject *args) { static PyObject * psutil_proc_kill(PyObject *self, PyObject *args) { HANDLE hProcess; - DWORD err; long pid; + DWORD exitCode; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; @@ -265,19 +265,35 @@ psutil_proc_kill(PyObject *self, PyObject *args) { return NULL; } - // kill the process if (! TerminateProcess(hProcess, SIGTERM)) { - err = GetLastError(); - // See: https://github.com/giampaolo/psutil/issues/1099 - if (err != ERROR_ACCESS_DENIED) { - PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); + if (GetLastError() == ERROR_ACCESS_DENIED) { + // ERROR_ACCESS_DENIED (winerror 5) may happen if the + // process already died. See: + // https://github.com/giampaolo/psutil/issues/1099 + // https://github.com/giampaolo/psutil/issues/1595 + if (GetExitCodeProcess(hProcess, &exitCode) == 0) { + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + goto error; + } + if (exitCode == STILL_ACTIVE) { + PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); + goto error; + } CloseHandle(hProcess); - return NULL; + Py_RETURN_NONE; + } + else { + PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); + goto error; } } CloseHandle(hProcess); Py_RETURN_NONE; + +error: + CloseHandle(hProcess); + return NULL; } diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py index 1b72a17775..4a559bb0be 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/download_exes.py @@ -26,7 +26,7 @@ BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7'] +PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7', '3.8'] TIMEOUT = 30 COLORS = True From f7e898b0987f97352c7551bdd9b29b594e1236f6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Nov 2019 02:09:02 -0800 Subject: [PATCH 0379/1714] #1595: use psutil_pid_is_running() instead of GetExitCodeProcess --- psutil/_psutil_windows.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index e01fa50d21..be0b2337fc 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -244,7 +244,6 @@ static PyObject * psutil_proc_kill(PyObject *self, PyObject *args) { HANDLE hProcess; long pid; - DWORD exitCode; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; @@ -266,19 +265,12 @@ psutil_proc_kill(PyObject *self, PyObject *args) { } if (! TerminateProcess(hProcess, SIGTERM)) { - if (GetLastError() == ERROR_ACCESS_DENIED) { - // ERROR_ACCESS_DENIED (winerror 5) may happen if the - // process already died. See: - // https://github.com/giampaolo/psutil/issues/1099 - // https://github.com/giampaolo/psutil/issues/1595 - if (GetExitCodeProcess(hProcess, &exitCode) == 0) { - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); - goto error; - } - if (exitCode == STILL_ACTIVE) { - PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); - goto error; - } + // ERROR_ACCESS_DENIED may happen if the process already died. See: + // https://github.com/giampaolo/psutil/issues/1099 + // https://github.com/giampaolo/psutil/issues/1595 + if ((GetLastError() == ERROR_ACCESS_DENIED) && \ + (psutil_pid_is_running(pid) == 0)) + { CloseHandle(hProcess); Py_RETURN_NONE; } From d739cbb1a5b207212d467b219dfc25b017911530 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 22 Nov 2019 00:10:33 +0800 Subject: [PATCH 0380/1714] use PROCESS_QUERY_LIMITED_INFORMATION --- psutil/arch/windows/process_info.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 62735728d5..5ea5f765c0 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -345,8 +345,7 @@ psutil_pid_is_running(DWORD pid) { return 1; if (pid < 0) return 0; - hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, - FALSE, pid); + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (NULL == hProcess) { err = GetLastError(); // Yeah, this is the actual error code in case of "no such process". From edb20f664f28653dcdd24f0bf0191984738dca6e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 22 Nov 2019 00:19:43 +0800 Subject: [PATCH 0381/1714] linux, cmdline(), fix for #1179, comment 552984549: sometimes string ends with null byte but args are separated by spaces --- psutil/_pslinux.py | 9 ++++++++- psutil/tests/test_linux.py | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index d29ccc8507..80fbf8bfd3 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1649,7 +1649,14 @@ def cmdline(self): sep = '\x00' if data.endswith('\x00') else ' ' if data.endswith(sep): data = data[:-1] - return 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): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 09fed4e46d..ccde735d8d 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1837,6 +1837,14 @@ def test_cmdline_spaces_mocked(self): self.assertEqual(p.cmdline(), ['foo', 'bar', '']) assert m.called + def test_cmdline_mixed_separators(self): + 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)'): From c63369e999b458ecbd559bdde895c344b4db2841 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 22 Nov 2019 14:08:35 +0800 Subject: [PATCH 0382/1714] updat HISTORY --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 5 ++--- psutil/tests/test_linux.py | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 045bb5d5f0..1d3b34a24f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ XXXX-XX-XX **Bug fixes** +- 1179_: [Linux] Process cmdline() now takes into account misbehaving processes + renaming the command line and using inappropriate chars to separate args. - 1595_: [Windows] Process.kill() may not throw AccessDenied. - 1616_: use of Py_DECREF instead of Py_CLEAR will result in double free and segfault (CVE). (patch by Riccardo Schirone) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 80fbf8bfd3..a56ead3698 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1651,9 +1651,8 @@ def cmdline(self): data = data[:-1] 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 + # 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 diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index ccde735d8d..4d9cea9240 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1838,6 +1838,8 @@ def test_cmdline_spaces_mocked(self): 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', From b2414b83d3d728ec34ea0e35bfb21517ee231401 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 22 Nov 2019 14:19:14 +0800 Subject: [PATCH 0383/1714] revert #1595 --- HISTORY.rst | 1 - psutil/_psutil_windows.c | 13 +------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1d3b34a24f..5cef298c01 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,7 +9,6 @@ XXXX-XX-XX - 1179_: [Linux] Process cmdline() now takes into account misbehaving processes renaming the command line and using inappropriate chars to separate args. -- 1595_: [Windows] Process.kill() may not throw AccessDenied. - 1616_: use of Py_DECREF instead of Py_CLEAR will result in double free and segfault (CVE). (patch by Riccardo Schirone) - 1619_: [OpenBSD] compilation fails due to C syntax error. (patch by Nathan diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index be0b2337fc..9599193421 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -267,14 +267,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { if (! TerminateProcess(hProcess, SIGTERM)) { // ERROR_ACCESS_DENIED may happen if the process already died. See: // https://github.com/giampaolo/psutil/issues/1099 - // https://github.com/giampaolo/psutil/issues/1595 - if ((GetLastError() == ERROR_ACCESS_DENIED) && \ - (psutil_pid_is_running(pid) == 0)) - { - CloseHandle(hProcess); - Py_RETURN_NONE; - } - else { + if (GetLastError() != ERROR_ACCESS_DENIED) { PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); goto error; } @@ -282,10 +275,6 @@ psutil_proc_kill(PyObject *self, PyObject *args) { CloseHandle(hProcess); Py_RETURN_NONE; - -error: - CloseHandle(hProcess); - return NULL; } From 70a141cf61bab7c40662ae243d2bff06f6d8bdd7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 22 Nov 2019 03:40:16 -0800 Subject: [PATCH 0384/1714] fix compilation err on win --- psutil/_psutil_windows.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 9599193421..f35d0076d2 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -269,7 +269,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { // https://github.com/giampaolo/psutil/issues/1099 if (GetLastError() != ERROR_ACCESS_DENIED) { PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); - goto error; + return NULL; } } From c6cd256da95ffe9599792759b1c2586ba24fa047 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 Nov 2019 19:35:11 +0800 Subject: [PATCH 0385/1714] pre release --- HISTORY.rst | 2 +- docs/index.rst | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5cef298c01..8d0878d264 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.6.6 ===== -XXXX-XX-XX +2019-11-25 **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 74a4c36e05..aac153aada 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2635,6 +2635,10 @@ take a look at the `development guide`_. Timeline ======== +- 2019-11-25: + `5.6.6 `__ - + `what's new `__ - + `diff `__ - 2019-11-06: `5.6.5 `__ - `what's new `__ - From e15fbda458b061e4442a6a8419c34f3067ce6e27 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Nov 2019 13:58:48 +0800 Subject: [PATCH 0386/1714] fix #1630: can't compile source distribution on Windows --- HISTORY.rst | 10 ++++++++++ psutil/__init__.py | 2 +- psutil/_psutil_windows.c | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8d0878d264..c81ed6f499 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* + +5.6.7 +===== + +2019-11-26 + +**Bug fixes** + +- 1630_: [Windows] can't compile source distribution due to C syntax error. + 5.6.6 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 4df2fec0ca..b267239e28 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -222,7 +222,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.6" +__version__ = "5.6.7" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 9599193421..f35d0076d2 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -269,7 +269,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { // https://github.com/giampaolo/psutil/issues/1099 if (GetLastError() != ERROR_ACCESS_DENIED) { PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); - goto error; + return NULL; } } From 78c8669f9c11fc4d4105925e2fb5e13963abd4a5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Nov 2019 15:25:07 +0800 Subject: [PATCH 0387/1714] update doc --- README.rst | 4 ++-- docs/index.rst | 48 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 44b88e7a6b..d9901b97b2 100644 --- a/README.rst +++ b/README.rst @@ -119,8 +119,6 @@ Professional support By subscribing you will help me (`Giampaolo Rodola`_) support psutil future development. Alternatively consider making a small `donation`_. -.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme - Security ======== @@ -515,3 +513,5 @@ Windows services .. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html .. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 .. _Tidelift security contact: https://tidelift.com/security +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme + diff --git a/docs/index.rst b/docs/index.rst index aac153aada..951bc23c53 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,21 +42,45 @@ Supported Python versions are **2.6**, **2.7** and **3.4+**. The psutil documentation you're reading is distributed as a single HTML page. + +Professional support +==================== + +.. image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png + :width: 80px + :align: left + +Professional support for psutil is available as part of the `Tidelift Subscription`_. +Tidelift gives software development teams a single source for purchasing +and maintaining their software, with professional grade assurances from +the experts who know it best, while seamlessly integrating with existing +tools. +By subscribing you will help me (`Giampaolo Rodola`_) support psutil +future development. Alternatively consider making a small `donation`_. +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. + Install -------- +======= + +Linux Ubuntu / Debian:: + + sudo apt-get install gcc python3-dev + sudo pip3 install psutil -The easiest way to install psutil is via ``pip``:: +Linux Redhat:: - pip install psutil + sudo yum install gcc python3-devel + sudo pip3 install psutil -On UNIX this requires a C compiler (e.g. gcc) installed. On Windows pip will -automatically retrieve a pre-compiled wheel version from -`PyPI repository `__. -Alternatively, see more detailed +Windows:: + + pip3 install psutil + +For other platforms see more detailed `install `_ instructions. - System related functions ======================== @@ -2635,6 +2659,10 @@ take a look at the `development guide`_. Timeline ======== +- 2019-11-26: + `5.6.7 `__ - + `what's new `__ - + `diff `__ - 2019-11-25: `5.6.6 `__ - `what's new `__ - @@ -2952,11 +2980,13 @@ Timeline .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py .. _`development guide`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 .. _`enums`: https://docs.python.org/3/library/enum.html#module-enum .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py .. _`GetDriveType`: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypea .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ .. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +.. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html .. _`hash`: https://docs.python.org/3/library/functions.html#hash .. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get @@ -2998,3 +3028,5 @@ Timeline .. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen .. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py .. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess +.. _Tidelift security contact: https://tidelift.com/security +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme From cb3133fca680fc5a39b2fb5e3569ee343c26d57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 5 Dec 2019 12:38:04 +0100 Subject: [PATCH 0388/1714] total physical memory (exclusive swap). (#1634) fix #1633 --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 951bc23c53..cc1ef24682 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -300,7 +300,7 @@ Memory Return statistics about system memory usage as a named tuple including the following fields, expressed in bytes. Main metrics: - - **total**: total physical memory. + - **total**: total physical memory (exclusive swap). - **available**: the memory that can be given instantly to processes without the system going into swap. This is calculated by summing different memory values depending on the From 0e1c78671b0ea0328bc87ea77081ef52e30b1d06 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Dec 2019 20:57:56 +0800 Subject: [PATCH 0389/1714] Readme tidelift update (#1636) --- README.rst | 26 ++++++++++++++------------ docs/index.rst | 4 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index d9901b97b2..639dfb068a 100644 --- a/README.rst +++ b/README.rst @@ -98,26 +98,28 @@ psutil currently supports the following platforms: ...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy `__ is also known to work. -Professional support -==================== +psutil for enterprise +===================== .. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png - :width: 100 + :width: 150 :alt: Tidelift :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme .. list-table:: - :widths: 10 100 + :widths: 10 150 * - |tideliftlogo| - - Professional support for psutil is available as part of the - `Tidelift Subscription`_. - Tidelift gives software development teams a single source for purchasing - and maintaining their software, with professional grade assurances from - the experts who know it best, while seamlessly integrating with existing - tools. - By subscribing you will help me (`Giampaolo Rodola`_) support psutil - future development. Alternatively consider making a small `donation`_. + - The maintainer of psutil and thousands of other packages are working + with Tidelift to deliver commercial support and maintenance for the open + source dependencies you use to build your applications. Save time, + reduce risk, and improve code health, while paying the maintainers of + the exact dependencies you use. + `Learn more `__. + + By subscribing to Tidelift you will help me (`Giampaolo Rodola`_) support + psutil future development. Alternatively consider making a small + `donation`_. Security ======== diff --git a/docs/index.rst b/docs/index.rst index cc1ef24682..5ed3bb7c78 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,7 +44,7 @@ The psutil documentation you're reading is distributed as a single HTML page. Professional support -==================== +-------------------- .. image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png :width: 80px @@ -61,7 +61,7 @@ To report a security vulnerability, please use the `Tidelift security contact`_. Tidelift will coordinate the fix and disclosure. Install -======= +------- Linux Ubuntu / Debian:: From 5deeca9e7a104105e17cc16ad4f79b1c5cdca29e Mon Sep 17 00:00:00 2001 From: vser1 Date: Tue, 17 Dec 2019 14:54:55 +0100 Subject: [PATCH 0390/1714] [Solaris] Fix #1637 (#1638) --- CREDITS | 4 ++-- HISTORY.rst | 5 +++++ psutil/_psutil_sunos.c | 19 +++++++++++++++---- psutil/arch/solaris/environ.c | 1 - setup.py | 19 +++++++++++++++++++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/CREDITS b/CREDITS index 330f4f995c..40ef4e34c4 100644 --- a/CREDITS +++ b/CREDITS @@ -631,9 +631,9 @@ N: Erwan Le Pape W: https://github.com/erwan-le-pape I: 1570 -N: vser1 +N: Étienne Servais W: https://github.com/vser1 -I: 1607 +I: 1607, 1637 N: Bernát Gábor W: https://github.com/gaborbernat diff --git a/HISTORY.rst b/HISTORY.rst index c81ed6f499..bdd681a4aa 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,11 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +**Bug fixes** + +- 1637_: [SunOS] Add partial support for old SunOS 5.10 Update 0 to 3. + (patch by Étienne Servais) + 5.6.7 ===== diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 31d6f364fb..bcfb448f5e 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -12,12 +12,8 @@ /* fix compilation issue on SunOS 5.10, see: * https://github.com/giampaolo/psutil/issues/421 * https://github.com/giampaolo/psutil/issues/1077 - * http://us-east.manta.joyent.com/jmc/public/opensolaris/ARChive/PSARC/2010/111/materials/s10ceval.txt - * - * Because LEGACY_MIB_SIZE defined in the same file there is no way to make autoconfiguration =\ */ -#define NEW_MIB_COMPLIANT 1 #define _STRUCTURED_PROC 1 #include @@ -44,6 +40,14 @@ #include #include #include +#ifndef NEW_MIB_COMPLIANT +/* + * Solaris introduced NEW_MIB_COMPLIANT macro with Update 4. + * See https://github.com/giampaolo/psutil/issues/421 + * Prior to Update 4, one has to include mib2 by hand. + */ +#include +#endif #include #include #include // fabs() @@ -1747,7 +1751,14 @@ void init_psutil_sunos(void) PyModule_AddIntConstant(module, "SSTOP", SSTOP); PyModule_AddIntConstant(module, "SIDL", SIDL); PyModule_AddIntConstant(module, "SONPROC", SONPROC); +#ifdef SWAIT PyModule_AddIntConstant(module, "SWAIT", SWAIT); +#else + /* sys/proc.h started defining SWAIT somewhere + * after Update 3 and prior to Update 5 included. + */ + PyModule_AddIntConstant(module, "SWAIT", 0); +#endif PyModule_AddIntConstant(module, "PRNODEV", PRNODEV); // for process tty diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/solaris/environ.c index 1af4c12934..eb0fa2abd3 100644 --- a/psutil/arch/solaris/environ.c +++ b/psutil/arch/solaris/environ.c @@ -6,7 +6,6 @@ * Functions specific for Process.environ(). */ -#define NEW_MIB_COMPLIANT 1 #define _STRUCTURED_PROC 1 #include diff --git a/setup.py b/setup.py index 99818ad504..d2f2d75fe6 100755 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ import sys import tempfile import warnings +import re with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -266,10 +267,28 @@ def get_ethtool_macro(): define_macros=macros, sources=sources) if SUNOS: + def get_sunos_update(): + # See https://serverfault.com/q/524883 + # for an explanation of Solaris /etc/release + with open('/etc/release') as f: + update = re.search(r'(?<=s10s_u)[0-9]{1,2}', f.readline()) + if update is None: + return 0 + else: + return int(update.group(0)) + posix_extension.libraries.append('socket') if platform.release() == '5.10': + # Detect Solaris 5.10, update >= 4, see: + # https://github.com/giampaolo/psutil/pull/1638 + if get_sunos_update() >= 4: + # MIB compliancy starts with SunOS 5.10 Update 4: + posix_extension.define_macros.append(('NEW_MIB_COMPLIANT', 1)) posix_extension.sources.append('psutil/arch/solaris/v10/ifaddrs.c') posix_extension.define_macros.append(('PSUTIL_SUNOS10', 1)) + else: + # Other releases are by default considered to be new mib compliant. + posix_extension.define_macros.append(('NEW_MIB_COMPLIANT', 1)) elif AIX: posix_extension.sources.append('psutil/arch/aix/ifaddrs.c') From 58c8bd1abc5565e003d7acb92066ee08b634966b Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 18 Dec 2019 05:02:39 +1100 Subject: [PATCH 0391/1714] Fix simple typo: whish -> wish (#1640) Closes #1639 --- psutil/tests/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index 9cefb77573..b647d5138f 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -7,7 +7,7 @@ Instructions for running tests python -m psutil.tests --install-deps # install test deps python -m psutil.tests - As a "developer", if you have a copy of the source code and you whish to hack + As a "developer", if you have a copy of the source code and you wish to hack on psutil:: make setup-dev-env # install test deps (+ other things) From c1202c28f547d2a1e81749cc6adabfa361c51d6b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Dec 2019 14:50:09 +0100 Subject: [PATCH 0392/1714] SunOS, fix #1642: PID 0 raises FileNotFoundError --- HISTORY.rst | 2 +- psutil/_pssunos.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index bdd681a4aa..1135a2d4f4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,7 +4,7 @@ **Bug fixes** - 1637_: [SunOS] Add partial support for old SunOS 5.10 Update 0 to 3. - (patch by Étienne Servais) +- 1642_: [SunOS] querying basic info for PID 0 results in FileNotFoundError. 5.6.7 ===== diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 2aa2a86615..9f0cac260f 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -400,6 +400,9 @@ def _proc_name_and_args(self): @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 From 04bcd8d9a16f0c053da916d8d57788f8851600a8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Dec 2019 16:12:23 +0100 Subject: [PATCH 0393/1714] HERE: use realpath() instead of abspath() because of failures seen in https://github.com/giampaolo/psutil/pull/1638#issuecomment-567013054 --- HISTORY.rst | 4 ++++ psutil/tests/__init__.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1135a2d4f4..8afc8f2ed9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.6.8 (unreleased) +================== + +XXXX-XX-XX **Bug fixes** diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 6c62936877..767524af5a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -153,9 +153,10 @@ # --- paths -ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +ROOT_DIR = os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..')) SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') -HERE = os.path.abspath(os.path.dirname(__file__)) +HERE = os.path.realpath(os.path.dirname(__file__)) # --- support From a7019f06e1accf6b96ba994dab264b8743f20edd Mon Sep 17 00:00:00 2001 From: Po-Chuan Hsieh Date: Sat, 21 Dec 2019 20:54:54 +0800 Subject: [PATCH 0394/1714] Fix Process on FreeBSD 12.0+ i386 (#1646) FreeBSD 12.0+ change ki_tdev from 32 bits to 64 bits. Reference: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=242543 --- psutil/_psutil_bsd.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 723d6198c3..94088d73af 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -271,7 +271,11 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { // Return a single big tuple with all process info. py_retlist = Py_BuildValue( +#if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031 + "(lillllllLdllllddddlllllbO)", +#else "(lillllllidllllddddlllllbO)", +#endif #ifdef PSUTIL_FREEBSD // (long)kp.ki_ppid, // (long) ppid @@ -285,7 +289,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { (long)kp.ki_groups[0], // (long) effective gid (long)kp.ki_svuid, // (long) saved gid // - kp.ki_tdev, // (int) tty nr + kp.ki_tdev, // (int or long long) tty nr PSUTIL_TV2DOUBLE(kp.ki_start), // (double) create time // ctx switches kp.ki_rusage.ru_nvcsw, // (long) ctx switches (voluntary) From 343cf0229de9dd6a22069e24286d1dbbcf6b6d99 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Dec 2019 13:58:39 +0100 Subject: [PATCH 0395/1714] update HISTORY for #1646 --- CREDITS | 5 +++++ HISTORY.rst | 2 ++ 2 files changed, 7 insertions(+) diff --git a/CREDITS b/CREDITS index 40ef4e34c4..49f255cc53 100644 --- a/CREDITS +++ b/CREDITS @@ -647,3 +647,8 @@ N: Riccardo Schirone W: https://github.com/ret2libc C: Milano, Italy I: 1616 + +N: Po-Chuan Hsieh +W: https://github.com/sunpoet +C: Taiwan +I: 1646 diff --git a/HISTORY.rst b/HISTORY.rst index 8afc8f2ed9..5a56542d3d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 1637_: [SunOS] Add partial support for old SunOS 5.10 Update 0 to 3. - 1642_: [SunOS] querying basic info for PID 0 results in FileNotFoundError. +- 1646_: [FreeBSD] many Process methods may cause a segfault on FreeBSD 12.0 + due to a backward incompatible change in a C type introduced in 12.0. 5.6.7 ===== From 348304dd374cca918d194644b7e678500145a1df Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2019 21:06:52 +0100 Subject: [PATCH 0396/1714] setup.py: print instructions if C compiler is not installed --- psutil/_common.py | 36 +++++++++++++++++++++++++++++++++++- setup.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 126d9d6fb7..2f74460bb2 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -23,6 +23,7 @@ from socket import AF_INET from socket import SOCK_DGRAM from socket import SOCK_STREAM + try: from socket import AF_INET6 except ImportError: @@ -37,6 +38,7 @@ else: enum = None + # can't take it from _common.py as this script is imported by setup.py PY3 = sys.version_info[0] == 3 @@ -64,7 +66,7 @@ '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", - 'bytes2human', 'conn_to_ntuple', + 'bytes2human', 'conn_to_ntuple', 'hilite', ] @@ -649,3 +651,35 @@ def decode(s): else: def decode(s): return s + + +def _term_supports_colors(file=sys.stdout): + if hasattr(_term_supports_colors, "ret"): + return _term_supports_colors.ret + try: + import curses + assert file.isatty() + curses.setupterm() + assert curses.tigetnum("colors") > 0 + except Exception: + _term_supports_colors.ret = False + return False + else: + _term_supports_colors.ret = True + return _term_supports_colors.ret + + +def hilite(s, ok=True, bold=False): + """Return an highlighted version of 'string'.""" + if not _term_supports_colors(): + return s + attr = [] + if ok is None: # no color + pass + elif ok: # green + attr.append('32') + else: # red + attr.append('31') + if bold: + attr.append('1') + return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) diff --git a/setup.py b/setup.py index d2f2d75fe6..a482e42784 100755 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ """Cross-platform lib for process and system monitoring in Python.""" +from __future__ import print_function import contextlib import io import os @@ -27,12 +28,13 @@ HERE = os.path.abspath(os.path.dirname(__file__)) -# ...so we can import _common.py +# ...so we can import _common.py and _compat.py sys.path.insert(0, os.path.join(HERE, "psutil")) from _common import AIX # NOQA from _common import BSD # NOQA from _common import FREEBSD # NOQA +from _common import hilite # NOQA from _common import LINUX # NOQA from _common import MACOS # NOQA from _common import NETBSD # NOQA @@ -40,6 +42,8 @@ from _common import POSIX # NOQA from _common import SUNOS # NOQA from _common import WINDOWS # NOQA +from _compat import PY3 # NOQA +from _compat import which # NOQA macros = [] @@ -105,6 +109,13 @@ def write(self, s): setattr(sys, stream_name, orig) +def missdeps(msg): + s = hilite("C compiler or Python headers are not installed ", ok=False) + s += hilite("on this system. Try to run:\n", ok=False) + s += hilite(msg, ok=False, bold=True) + print(s, file=sys.stderr) + + if WINDOWS: def get_winver(): maj, min = sys.getwindowsversion()[0:2] @@ -365,7 +376,38 @@ def main(): extras_require=extras_require, zip_safe=False, ) - setup(**kwargs) + success = False + try: + setup(**kwargs) + success = True + finally: + if not success and POSIX and not which('gcc'): + py3 = "3" if PY3 else "" + if LINUX: + if which('dpkg'): + missdeps("sudo apt-get install gcc python%s-dev" % py3) + elif which('rpm'): + missdeps("sudo yum install gcc python%s-devel" % py3) + elif MACOS: + print(hilite("XCode (https://developer.apple.com/xcode/) " + "is not installed"), ok=False, file=sys.stderr) + elif FREEBSD: + missdeps("pkg install gcc python%s" % py3) + elif OPENBSD: + missdeps("pkg_add -v gcc python%s" % py3) + elif NETBSD: + missdeps("pkgin install gcc python%s" % py3) + elif SUNOS: + missdeps("sudo ln -s /usr/bin/gcc /usr/local/bin/cc && " + "pkg install gcc") + elif not success and WINDOWS: + if PY3: + ur = "http://www.visualstudio.com/en-au/news/vs2015-preview-vs" + else: + ur = "http://www.microsoft.com/en-us/download/" + ur += "details.aspx?id=44266" + print(hilite("VisualStudio is not installed; get it from %s" % ur), + ok=False, file=sys.stderr) if __name__ == '__main__': From 334619d9583c945fce604839cf6422d1b1377e0f Mon Sep 17 00:00:00 2001 From: Javad Karabi Date: Sat, 28 Dec 2019 15:29:11 -0600 Subject: [PATCH 0397/1714] sensors_temperatures: also search /sys/devices/platform/coretemp.* for temperatures (#1648) --- psutil/_pslinux.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index a56ead3698..e95581cc41 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1206,6 +1206,7 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/971 # https://github.com/nicolargo/glances/issues/1060 basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*')) + basenames.extend(glob.glob('/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*')) basenames = sorted(set([x.split('_')[0] for x in basenames])) for base in basenames: From 283faf2eef04b0ed6dd05f53c84c37bee017ea91 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2019 22:32:36 +0100 Subject: [PATCH 0398/1714] credits for #1648 --- CREDITS | 4 ++++ HISTORY.rst | 5 +++++ psutil/tests/test_linux.py | 1 + 3 files changed, 10 insertions(+) diff --git a/CREDITS b/CREDITS index 49f255cc53..988af1fe25 100644 --- a/CREDITS +++ b/CREDITS @@ -652,3 +652,7 @@ N: Po-Chuan Hsieh W: https://github.com/sunpoet C: Taiwan I: 1646 + +N: Javad Karabi +W: https://github.com/karabijavad +I: 1648 diff --git a/HISTORY.rst b/HISTORY.rst index 5a56542d3d..16d18c082e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,11 @@ XXXX-XX-XX +**Enhancements** + +- 1648_: [Linux] sensors_temperatures() looks into an additional /sys/device/ + directory for additional data. (patch by Javad Karabi) + **Bug fixes** - 1637_: [SunOS] Add partial support for old SunOS 5.10 Update 0 to 3. diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4d9cea9240..f503b384f9 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1616,6 +1616,7 @@ def glob_mock(path): 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' From 03b29d040881f55eb098e78e71c96164edfb73ec Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2019 23:03:07 +0100 Subject: [PATCH 0399/1714] lint --- psutil/__init__.py | 2 ++ psutil/_pslinux.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index b267239e28..042af7ef44 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1369,6 +1369,8 @@ def kill(self): 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. diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e95581cc41..694e307b1c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1206,7 +1206,8 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/971 # https://github.com/nicolargo/glances/issues/1060 basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*')) - basenames.extend(glob.glob('/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*')) + basenames.extend(glob.glob( + '/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*')) basenames = sorted(set([x.split('_')[0] for x in basenames])) for base in basenames: From f170fb8a9ad07edda6ed12c150570a0ad33ee2f0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 29 Dec 2019 00:11:04 +0100 Subject: [PATCH 0400/1714] update/fix wait_procs() doc --- docs/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5ed3bb7c78..6df32819a8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -932,11 +932,11 @@ Functions Convenience function which waits for a list of :class:`Process` instances to terminate. Return a ``(gone, alive)`` tuple indicating which processes are gone and which ones are still alive. The *gone* ones will have a new - *returncode* attribute indicating process exit status (will be ``None`` for - processes which are not our children). + *returncode* attribute indicating process exit status as returned by + :meth:`Process.wait`. ``callback`` is a function which gets called when one of the processes being waited on is terminated and a :class:`Process` instance is passed as callback - argument). + argument (the instance will also have a *returncode* attribute set). This function will return as soon as all processes terminate or when *timeout* (seconds) occurs. Differently from :meth:`Process.wait` it will not raise From 8d184ed7d1c5c10cc38c577b0d7f681e69c2d8d4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 29 Dec 2019 14:59:09 +0100 Subject: [PATCH 0401/1714] bump version --- psutil/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 042af7ef44..093acf0cf0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -222,7 +222,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.7" +__version__ = "5.6.8" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -1635,6 +1635,7 @@ def check_gone(proc, timeout): 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: From 699a3826398b8d6c19199ea0462f93b46dae3e69 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Dec 2019 01:59:57 -0800 Subject: [PATCH 0402/1714] windows: move net_connections() in socks.c --- psutil/_psutil_windows.c | 462 +--------------------------------- psutil/arch/windows/socks.c | 484 ++++++++++++++++++++++++++++++++++++ psutil/arch/windows/socks.h | 9 + setup.py | 5 +- 4 files changed, 498 insertions(+), 462 deletions(-) create mode 100644 psutil/arch/windows/socks.c create mode 100644 psutil/arch/windows/socks.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index f35d0076d2..7eaa12745c 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -27,7 +27,7 @@ #include // users() #include // cpu_freq() #if (_WIN32_WINNT >= 0x0600) // Windows >= Vista -#include // net_connections() +#include // net_io_counters() #endif // Link with Iphlpapi.lib @@ -40,6 +40,7 @@ #include "arch/windows/process_handles.h" #include "arch/windows/inet_ntop.h" #include "arch/windows/services.h" +#include "arch/windows/socks.h" #include "arch/windows/wmi.h" #include "_psutil_common.h" @@ -54,7 +55,6 @@ #define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) #define LO_T 1e-7 #define HI_T 429.4967296 -#define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) #ifndef AF_INET6 #define AF_INET6 23 #endif @@ -1355,464 +1355,6 @@ psutil_proc_username(PyObject *self, PyObject *args) { } -// https://msdn.microsoft.com/library/aa365928.aspx -// TODO properly handle return code -static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, - ULONG address_family, - PVOID * data, DWORD * size) -{ - // Due to other processes being active on the machine, it's possible - // that the size of the table increases between the moment where we - // query the size and the moment where we query the data. Therefore, it's - // important to call this in a loop to retry if that happens. - // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error - // and https://github.com/giampaolo/psutil/issues/1294 - DWORD error = ERROR_INSUFFICIENT_BUFFER; - *size = 0; - *data = NULL; - error = call(NULL, size, FALSE, address_family, - TCP_TABLE_OWNER_PID_ALL, 0); - while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) - { - *data = malloc(*size); - if (*data == NULL) { - error = ERROR_NOT_ENOUGH_MEMORY; - continue; - } - error = call(*data, size, FALSE, address_family, - TCP_TABLE_OWNER_PID_ALL, 0); - if (error != NO_ERROR) { - free(*data); - *data = NULL; - } - } - return error; -} - - -// https://msdn.microsoft.com/library/aa365930.aspx -// TODO properly check return value -static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, - ULONG address_family, - PVOID * data, DWORD * size) -{ - // Due to other processes being active on the machine, it's possible - // that the size of the table increases between the moment where we - // query the size and the moment where we query the data. Therefore, it's - // important to call this in a loop to retry if that happens. - // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error - // and https://github.com/giampaolo/psutil/issues/1294 - DWORD error = ERROR_INSUFFICIENT_BUFFER; - *size = 0; - *data = NULL; - error = call(NULL, size, FALSE, address_family, - UDP_TABLE_OWNER_PID, 0); - while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) - { - *data = malloc(*size); - if (*data == NULL) { - error = ERROR_NOT_ENOUGH_MEMORY; - continue; - } - error = call(*data, size, FALSE, address_family, - UDP_TABLE_OWNER_PID, 0); - if (error != NO_ERROR) { - free(*data); - *data = NULL; - } - } - - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - return 1; - } - if (error != NO_ERROR) { - PyErr_SetFromWindowsErr(error); - return 1; - } - return 0; -} - - -#define psutil_conn_decref_objs() \ - Py_DECREF(_AF_INET); \ - Py_DECREF(_AF_INET6);\ - Py_DECREF(_SOCK_STREAM);\ - Py_DECREF(_SOCK_DGRAM); - - -/* - * Return a list of network connections opened by a process - */ -static PyObject * -psutil_net_connections(PyObject *self, PyObject *args) { - static long null_address[4] = { 0, 0, 0, 0 }; - unsigned long pid; - int pid_return; - PVOID table = NULL; - DWORD tableSize; - DWORD error; - PMIB_TCPTABLE_OWNER_PID tcp4Table; - PMIB_UDPTABLE_OWNER_PID udp4Table; - PMIB_TCP6TABLE_OWNER_PID tcp6Table; - PMIB_UDP6TABLE_OWNER_PID udp6Table; - ULONG i; - CHAR addressBufferLocal[65]; - CHAR addressBufferRemote[65]; - - PyObject *py_retlist; - PyObject *py_conn_tuple = NULL; - PyObject *py_af_filter = NULL; - PyObject *py_type_filter = NULL; - PyObject *py_addr_tuple_local = NULL; - PyObject *py_addr_tuple_remote = NULL; - PyObject *_AF_INET = PyLong_FromLong((long)AF_INET); - PyObject *_AF_INET6 = PyLong_FromLong((long)AF_INET6); - PyObject *_SOCK_STREAM = PyLong_FromLong((long)SOCK_STREAM); - PyObject *_SOCK_DGRAM = PyLong_FromLong((long)SOCK_DGRAM); - - // Import some functions. - if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) - goto error; - - if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { - psutil_conn_decref_objs(); - PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); - return NULL; - } - - if (pid != -1) { - pid_return = psutil_pid_is_running(pid); - if (pid_return == 0) { - psutil_conn_decref_objs(); - return NoSuchProcess(""); - } - else if (pid_return == -1) { - psutil_conn_decref_objs(); - return NULL; - } - } - - py_retlist = PyList_New(0); - if (py_retlist == NULL) { - psutil_conn_decref_objs(); - return NULL; - } - - // TCP IPv4 - - if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1)) - { - table = NULL; - py_conn_tuple = NULL; - py_addr_tuple_local = NULL; - py_addr_tuple_remote = NULL; - tableSize = 0; - - error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, - AF_INET, &table, &tableSize); - if (error != 0) - goto error; - tcp4Table = table; - for (i = 0; i < tcp4Table->dwNumEntries; i++) { - if (pid != -1) { - if (tcp4Table->table[i].dwOwningPid != pid) { - continue; - } - } - - if (tcp4Table->table[i].dwLocalAddr != 0 || - tcp4Table->table[i].dwLocalPort != 0) - { - struct in_addr addr; - - addr.S_un.S_addr = tcp4Table->table[i].dwLocalAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(tcp4Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); - } - - if (py_addr_tuple_local == NULL) - goto error; - - // On Windows <= XP, remote addr is filled even if socket - // is in LISTEN mode in which case we just ignore it. - if ((tcp4Table->table[i].dwRemoteAddr != 0 || - tcp4Table->table[i].dwRemotePort != 0) && - (tcp4Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) - { - struct in_addr addr; - - addr.S_un.S_addr = tcp4Table->table[i].dwRemoteAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferRemote); - py_addr_tuple_remote = Py_BuildValue( - "(si)", - addressBufferRemote, - BYTESWAP_USHORT(tcp4Table->table[i].dwRemotePort)); - } - else - { - py_addr_tuple_remote = PyTuple_New(0); - } - - if (py_addr_tuple_remote == NULL) - goto error; - - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_STREAM, - py_addr_tuple_local, - py_addr_tuple_remote, - tcp4Table->table[i].dwState, - tcp4Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_CLEAR(py_conn_tuple); - } - - free(table); - table = NULL; - tableSize = 0; - } - - // TCP IPv6 - if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1) && - (psutil_rtlIpv6AddressToStringA != NULL)) - { - table = NULL; - py_conn_tuple = NULL; - py_addr_tuple_local = NULL; - py_addr_tuple_remote = NULL; - tableSize = 0; - - error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, - AF_INET6, &table, &tableSize); - if (error != 0) - goto error; - tcp6Table = table; - for (i = 0; i < tcp6Table->dwNumEntries; i++) - { - if (pid != -1) { - if (tcp6Table->table[i].dwOwningPid != pid) { - continue; - } - } - - if (memcmp(tcp6Table->table[i].ucLocalAddr, null_address, 16) - != 0 || tcp6Table->table[i].dwLocalPort != 0) - { - struct in6_addr addr; - - memcpy(&addr, tcp6Table->table[i].ucLocalAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(tcp6Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); - } - - if (py_addr_tuple_local == NULL) - goto error; - - // On Windows <= XP, remote addr is filled even if socket - // is in LISTEN mode in which case we just ignore it. - if ((memcmp(tcp6Table->table[i].ucRemoteAddr, null_address, 16) - != 0 || - tcp6Table->table[i].dwRemotePort != 0) && - (tcp6Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) - { - struct in6_addr addr; - - memcpy(&addr, tcp6Table->table[i].ucRemoteAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferRemote); - py_addr_tuple_remote = Py_BuildValue( - "(si)", - addressBufferRemote, - BYTESWAP_USHORT(tcp6Table->table[i].dwRemotePort)); - } - else { - py_addr_tuple_remote = PyTuple_New(0); - } - - if (py_addr_tuple_remote == NULL) - goto error; - - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_STREAM, - py_addr_tuple_local, - py_addr_tuple_remote, - tcp6Table->table[i].dwState, - tcp6Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_CLEAR(py_conn_tuple); - } - - free(table); - table = NULL; - tableSize = 0; - } - - // UDP IPv4 - - if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1)) - { - table = NULL; - py_conn_tuple = NULL; - py_addr_tuple_local = NULL; - py_addr_tuple_remote = NULL; - tableSize = 0; - error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, - AF_INET, &table, &tableSize); - if (error != 0) - goto error; - udp4Table = table; - for (i = 0; i < udp4Table->dwNumEntries; i++) - { - if (pid != -1) { - if (udp4Table->table[i].dwOwningPid != pid) { - continue; - } - } - - if (udp4Table->table[i].dwLocalAddr != 0 || - udp4Table->table[i].dwLocalPort != 0) - { - struct in_addr addr; - - addr.S_un.S_addr = udp4Table->table[i].dwLocalAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(udp4Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); - } - - if (py_addr_tuple_local == NULL) - goto error; - - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_DGRAM, - py_addr_tuple_local, - PyTuple_New(0), - PSUTIL_CONN_NONE, - udp4Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_CLEAR(py_conn_tuple); - } - - free(table); - table = NULL; - tableSize = 0; - } - - // UDP IPv6 - - if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1) && - (psutil_rtlIpv6AddressToStringA != NULL)) - { - table = NULL; - py_conn_tuple = NULL; - py_addr_tuple_local = NULL; - py_addr_tuple_remote = NULL; - tableSize = 0; - error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, - AF_INET6, &table, &tableSize); - if (error != 0) - goto error; - udp6Table = table; - for (i = 0; i < udp6Table->dwNumEntries; i++) { - if (pid != -1) { - if (udp6Table->table[i].dwOwningPid != pid) { - continue; - } - } - - if (memcmp(udp6Table->table[i].ucLocalAddr, null_address, 16) - != 0 || udp6Table->table[i].dwLocalPort != 0) - { - struct in6_addr addr; - - memcpy(&addr, udp6Table->table[i].ucLocalAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); - py_addr_tuple_local = Py_BuildValue( - "(si)", - addressBufferLocal, - BYTESWAP_USHORT(udp6Table->table[i].dwLocalPort)); - } - else { - py_addr_tuple_local = PyTuple_New(0); - } - - if (py_addr_tuple_local == NULL) - goto error; - - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_DGRAM, - py_addr_tuple_local, - PyTuple_New(0), - PSUTIL_CONN_NONE, - udp6Table->table[i].dwOwningPid); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) - goto error; - Py_CLEAR(py_conn_tuple); - } - - free(table); - table = NULL; - tableSize = 0; - } - - psutil_conn_decref_objs(); - return py_retlist; - -error: - psutil_conn_decref_objs(); - Py_XDECREF(py_conn_tuple); - Py_XDECREF(py_addr_tuple_local); - Py_XDECREF(py_addr_tuple_remote); - Py_DECREF(py_retlist); - if (table != NULL) - free(table); - return NULL; -} - - /* * Get process priority as a Python integer. */ diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c new file mode 100644 index 0000000000..dfa19ba9cd --- /dev/null +++ b/psutil/arch/windows/socks.c @@ -0,0 +1,484 @@ +/* + * 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. + */ + +// Fixes clash between winsock2.h and windows.h +#define WIN32_LEAN_AND_MEAN + +#include +#include +#if (_WIN32_WINNT >= 0x0600) // Windows >= Vista +#include +#endif + +#include "ntextapi.h" +#include "global.h" +#include "process_info.h" +#include "../../_psutil_common.h" + + +#define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) + +#ifndef AF_INET6 +#define AF_INET6 23 +#endif + + +// https://msdn.microsoft.com/library/aa365928.aspx +// TODO properly handle return code +static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, + ULONG address_family, + PVOID * data, DWORD * size) +{ + // Due to other processes being active on the machine, it's possible + // that the size of the table increases between the moment where we + // query the size and the moment where we query the data. Therefore, it's + // important to call this in a loop to retry if that happens. + // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error + // and https://github.com/giampaolo/psutil/issues/1294 + DWORD error = ERROR_INSUFFICIENT_BUFFER; + *size = 0; + *data = NULL; + error = call(NULL, size, FALSE, address_family, + TCP_TABLE_OWNER_PID_ALL, 0); + while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) + { + *data = malloc(*size); + if (*data == NULL) { + error = ERROR_NOT_ENOUGH_MEMORY; + continue; + } + error = call(*data, size, FALSE, address_family, + TCP_TABLE_OWNER_PID_ALL, 0); + if (error != NO_ERROR) { + free(*data); + *data = NULL; + } + } + return error; +} + + +// https://msdn.microsoft.com/library/aa365930.aspx +// TODO properly check return value +static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, + ULONG address_family, + PVOID * data, DWORD * size) +{ + // Due to other processes being active on the machine, it's possible + // that the size of the table increases between the moment where we + // query the size and the moment where we query the data. Therefore, it's + // important to call this in a loop to retry if that happens. + // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error + // and https://github.com/giampaolo/psutil/issues/1294 + DWORD error = ERROR_INSUFFICIENT_BUFFER; + *size = 0; + *data = NULL; + error = call(NULL, size, FALSE, address_family, + UDP_TABLE_OWNER_PID, 0); + while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) + { + *data = malloc(*size); + if (*data == NULL) { + error = ERROR_NOT_ENOUGH_MEMORY; + continue; + } + error = call(*data, size, FALSE, address_family, + UDP_TABLE_OWNER_PID, 0); + if (error != NO_ERROR) { + free(*data); + *data = NULL; + } + } + + if (error == ERROR_NOT_ENOUGH_MEMORY) { + PyErr_NoMemory(); + return 1; + } + if (error != NO_ERROR) { + PyErr_SetFromWindowsErr(error); + return 1; + } + return 0; +} + + +#define psutil_conn_decref_objs() \ + Py_DECREF(_AF_INET); \ + Py_DECREF(_AF_INET6);\ + Py_DECREF(_SOCK_STREAM);\ + Py_DECREF(_SOCK_DGRAM); + + +/* + * Return a list of network connections opened by a process + */ +PyObject * +psutil_net_connections(PyObject *self, PyObject *args) { + static long null_address[4] = { 0, 0, 0, 0 }; + unsigned long pid; + int pid_return; + PVOID table = NULL; + DWORD tableSize; + DWORD error; + PMIB_TCPTABLE_OWNER_PID tcp4Table; + PMIB_UDPTABLE_OWNER_PID udp4Table; + PMIB_TCP6TABLE_OWNER_PID tcp6Table; + PMIB_UDP6TABLE_OWNER_PID udp6Table; + ULONG i; + CHAR addressBufferLocal[65]; + CHAR addressBufferRemote[65]; + + PyObject *py_retlist; + PyObject *py_conn_tuple = NULL; + PyObject *py_af_filter = NULL; + PyObject *py_type_filter = NULL; + PyObject *py_addr_tuple_local = NULL; + PyObject *py_addr_tuple_remote = NULL; + PyObject *_AF_INET = PyLong_FromLong((long)AF_INET); + PyObject *_AF_INET6 = PyLong_FromLong((long)AF_INET6); + PyObject *_SOCK_STREAM = PyLong_FromLong((long)SOCK_STREAM); + PyObject *_SOCK_DGRAM = PyLong_FromLong((long)SOCK_DGRAM); + + // Import some functions. + if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) + goto error; + + if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { + psutil_conn_decref_objs(); + PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); + return NULL; + } + + if (pid != -1) { + pid_return = psutil_pid_is_running(pid); + if (pid_return == 0) { + psutil_conn_decref_objs(); + return NoSuchProcess(""); + } + else if (pid_return == -1) { + psutil_conn_decref_objs(); + return NULL; + } + } + + py_retlist = PyList_New(0); + if (py_retlist == NULL) { + psutil_conn_decref_objs(); + return NULL; + } + + // TCP IPv4 + + if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) && + (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1)) + { + table = NULL; + py_conn_tuple = NULL; + py_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + tableSize = 0; + + error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, + AF_INET, &table, &tableSize); + if (error != 0) + goto error; + tcp4Table = table; + for (i = 0; i < tcp4Table->dwNumEntries; i++) { + if (pid != -1) { + if (tcp4Table->table[i].dwOwningPid != pid) { + continue; + } + } + + if (tcp4Table->table[i].dwLocalAddr != 0 || + tcp4Table->table[i].dwLocalPort != 0) + { + struct in_addr addr; + + addr.S_un.S_addr = tcp4Table->table[i].dwLocalAddr; + psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(tcp4Table->table[i].dwLocalPort)); + } + else { + py_addr_tuple_local = PyTuple_New(0); + } + + if (py_addr_tuple_local == NULL) + goto error; + + // On Windows <= XP, remote addr is filled even if socket + // is in LISTEN mode in which case we just ignore it. + if ((tcp4Table->table[i].dwRemoteAddr != 0 || + tcp4Table->table[i].dwRemotePort != 0) && + (tcp4Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) + { + struct in_addr addr; + + addr.S_un.S_addr = tcp4Table->table[i].dwRemoteAddr; + psutil_rtlIpv4AddressToStringA(&addr, addressBufferRemote); + py_addr_tuple_remote = Py_BuildValue( + "(si)", + addressBufferRemote, + BYTESWAP_USHORT(tcp4Table->table[i].dwRemotePort)); + } + else + { + py_addr_tuple_remote = PyTuple_New(0); + } + + if (py_addr_tuple_remote == NULL) + goto error; + + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET, + SOCK_STREAM, + py_addr_tuple_local, + py_addr_tuple_remote, + tcp4Table->table[i].dwState, + tcp4Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_CLEAR(py_conn_tuple); + } + + free(table); + table = NULL; + tableSize = 0; + } + + // TCP IPv6 + if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && + (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1) && + (psutil_rtlIpv6AddressToStringA != NULL)) + { + table = NULL; + py_conn_tuple = NULL; + py_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + tableSize = 0; + + error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, + AF_INET6, &table, &tableSize); + if (error != 0) + goto error; + tcp6Table = table; + for (i = 0; i < tcp6Table->dwNumEntries; i++) + { + if (pid != -1) { + if (tcp6Table->table[i].dwOwningPid != pid) { + continue; + } + } + + if (memcmp(tcp6Table->table[i].ucLocalAddr, null_address, 16) + != 0 || tcp6Table->table[i].dwLocalPort != 0) + { + struct in6_addr addr; + + memcpy(&addr, tcp6Table->table[i].ucLocalAddr, 16); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(tcp6Table->table[i].dwLocalPort)); + } + else { + py_addr_tuple_local = PyTuple_New(0); + } + + if (py_addr_tuple_local == NULL) + goto error; + + // On Windows <= XP, remote addr is filled even if socket + // is in LISTEN mode in which case we just ignore it. + if ((memcmp(tcp6Table->table[i].ucRemoteAddr, null_address, 16) + != 0 || + tcp6Table->table[i].dwRemotePort != 0) && + (tcp6Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) + { + struct in6_addr addr; + + memcpy(&addr, tcp6Table->table[i].ucRemoteAddr, 16); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferRemote); + py_addr_tuple_remote = Py_BuildValue( + "(si)", + addressBufferRemote, + BYTESWAP_USHORT(tcp6Table->table[i].dwRemotePort)); + } + else { + py_addr_tuple_remote = PyTuple_New(0); + } + + if (py_addr_tuple_remote == NULL) + goto error; + + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_STREAM, + py_addr_tuple_local, + py_addr_tuple_remote, + tcp6Table->table[i].dwState, + tcp6Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_CLEAR(py_conn_tuple); + } + + free(table); + table = NULL; + tableSize = 0; + } + + // UDP IPv4 + + if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) && + (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1)) + { + table = NULL; + py_conn_tuple = NULL; + py_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + tableSize = 0; + error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, + AF_INET, &table, &tableSize); + if (error != 0) + goto error; + udp4Table = table; + for (i = 0; i < udp4Table->dwNumEntries; i++) + { + if (pid != -1) { + if (udp4Table->table[i].dwOwningPid != pid) { + continue; + } + } + + if (udp4Table->table[i].dwLocalAddr != 0 || + udp4Table->table[i].dwLocalPort != 0) + { + struct in_addr addr; + + addr.S_un.S_addr = udp4Table->table[i].dwLocalAddr; + psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(udp4Table->table[i].dwLocalPort)); + } + else { + py_addr_tuple_local = PyTuple_New(0); + } + + if (py_addr_tuple_local == NULL) + goto error; + + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET, + SOCK_DGRAM, + py_addr_tuple_local, + PyTuple_New(0), + PSUTIL_CONN_NONE, + udp4Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_CLEAR(py_conn_tuple); + } + + free(table); + table = NULL; + tableSize = 0; + } + + // UDP IPv6 + + if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && + (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1) && + (psutil_rtlIpv6AddressToStringA != NULL)) + { + table = NULL; + py_conn_tuple = NULL; + py_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + tableSize = 0; + error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, + AF_INET6, &table, &tableSize); + if (error != 0) + goto error; + udp6Table = table; + for (i = 0; i < udp6Table->dwNumEntries; i++) { + if (pid != -1) { + if (udp6Table->table[i].dwOwningPid != pid) { + continue; + } + } + + if (memcmp(udp6Table->table[i].ucLocalAddr, null_address, 16) + != 0 || udp6Table->table[i].dwLocalPort != 0) + { + struct in6_addr addr; + + memcpy(&addr, udp6Table->table[i].ucLocalAddr, 16); + psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); + py_addr_tuple_local = Py_BuildValue( + "(si)", + addressBufferLocal, + BYTESWAP_USHORT(udp6Table->table[i].dwLocalPort)); + } + else { + py_addr_tuple_local = PyTuple_New(0); + } + + if (py_addr_tuple_local == NULL) + goto error; + + py_conn_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_DGRAM, + py_addr_tuple_local, + PyTuple_New(0), + PSUTIL_CONN_NONE, + udp6Table->table[i].dwOwningPid); + if (!py_conn_tuple) + goto error; + if (PyList_Append(py_retlist, py_conn_tuple)) + goto error; + Py_CLEAR(py_conn_tuple); + } + + free(table); + table = NULL; + tableSize = 0; + } + + psutil_conn_decref_objs(); + return py_retlist; + +error: + psutil_conn_decref_objs(); + Py_XDECREF(py_conn_tuple); + Py_XDECREF(py_addr_tuple_local); + Py_XDECREF(py_addr_tuple_remote); + Py_DECREF(py_retlist); + if (table != NULL) + free(table); + return NULL; +} diff --git a/psutil/arch/windows/socks.h b/psutil/arch/windows/socks.h new file mode 100644 index 0000000000..cd9ba58dcb --- /dev/null +++ b/psutil/arch/windows/socks.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Jeff Tang. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +PyObject *psutil_net_connections(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index a482e42784..eb9b0f8cd4 100755 --- a/setup.py +++ b/setup.py @@ -148,6 +148,7 @@ def get_winver(): 'psutil/arch/windows/inet_ntop.c', 'psutil/arch/windows/services.c', 'psutil/arch/windows/global.c', + 'psutil/arch/windows/socks.c', 'psutil/arch/windows/wmi.c', ], define_macros=macros, @@ -406,8 +407,8 @@ def main(): else: ur = "http://www.microsoft.com/en-us/download/" ur += "details.aspx?id=44266" - print(hilite("VisualStudio is not installed; get it from %s" % ur), - ok=False, file=sys.stderr) + s = "VisualStudio is not installed; get it from %s" % ur + print(hilite(s, ok=False), file=sys.stderr) if __name__ == '__main__': From 253abf3fc61ccc3ab1031a9b430a7e99e2194107 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Dec 2019 02:39:22 -0800 Subject: [PATCH 0403/1714] win: split code into new process_utils.c --- psutil/_psutil_windows.c | 1 + psutil/arch/windows/process_handles.c | 3 +- psutil/arch/windows/process_info.c | 271 +----------------------- psutil/arch/windows/process_info.h | 7 - psutil/arch/windows/process_utils.c | 286 ++++++++++++++++++++++++++ psutil/arch/windows/process_utils.h | 11 + psutil/arch/windows/socks.c | 2 +- setup.py | 1 + 8 files changed, 303 insertions(+), 279 deletions(-) create mode 100644 psutil/arch/windows/process_utils.c create mode 100644 psutil/arch/windows/process_utils.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 7eaa12745c..660e08afac 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -36,6 +36,7 @@ #include "arch/windows/ntextapi.h" #include "arch/windows/global.h" #include "arch/windows/security.h" +#include "arch/windows/process_utils.h" #include "arch/windows/process_info.h" #include "arch/windows/process_handles.h" #include "arch/windows/inet_ntop.h" diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 5966669e28..40b209ee1f 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -8,10 +8,11 @@ #include #include #include + #include "ntextapi.h" #include "global.h" #include "process_handles.h" -#include "process_info.h" +#include "process_utils.h" #include "../../_psutil_common.h" CRITICAL_SECTION g_cs; diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 5ea5f765c0..a9c2c1316e 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -16,6 +16,7 @@ #include "global.h" #include "security.h" #include "process_info.h" +#include "process_utils.h" #include "../../_psutil_common.h" @@ -138,276 +139,6 @@ typedef struct { #define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) -/* - * Return 1 if PID exists, 0 if not, -1 on error. - */ -int -psutil_pid_in_pids(DWORD pid) { - DWORD *proclist = NULL; - DWORD numberOfReturnedPIDs; - DWORD i; - - proclist = psutil_get_pids(&numberOfReturnedPIDs); - if (proclist == NULL) - return -1; - for (i = 0; i < numberOfReturnedPIDs; i++) { - if (proclist[i] == pid) { - free(proclist); - return 1; - } - } - free(proclist); - return 0; -} - - -/* - * Given a process HANDLE checks whether it's actually running. - * Returns: - * - 1: running - * - 0: not running - * - -1: WindowsError - * - -2: AssertionError - */ -int -psutil_is_phandle_running(HANDLE hProcess, DWORD pid) { - DWORD processExitCode = 0; - - if (hProcess == NULL) { - if (GetLastError() == ERROR_INVALID_PARAMETER) { - // Yeah, this is the actual error code in case of - // "no such process". - if (! psutil_assert_pid_not_exists( - pid, "iphr: OpenProcess() -> ERROR_INVALID_PARAMETER")) { - return -2; - } - return 0; - } - return -1; - } - - if (GetExitCodeProcess(hProcess, &processExitCode)) { - // XXX - maybe STILL_ACTIVE is not fully reliable as per: - // http://stackoverflow.com/questions/1591342/#comment47830782_1591379 - if (processExitCode == STILL_ACTIVE) { - if (! psutil_assert_pid_exists( - pid, "iphr: GetExitCodeProcess() -> STILL_ACTIVE")) { - return -2; - } - return 1; - } - else { - // We can't be sure so we look into pids. - if (psutil_pid_in_pids(pid) == 1) { - return 1; - } - else { - CloseHandle(hProcess); - return 0; - } - } - } - - CloseHandle(hProcess); - if (! psutil_assert_pid_not_exists( pid, "iphr: exit fun")) { - return -2; - } - return -1; -} - - -/* - * Given a process HANDLE checks whether it's actually running and if - * it does return it, else return NULL with the proper Python exception - * set. - */ -HANDLE -psutil_check_phandle(HANDLE hProcess, DWORD pid) { - int ret = psutil_is_phandle_running(hProcess, pid); - if (ret == 1) { - return hProcess; - } - else if (ret == 0) { - return NoSuchProcess(""); - } - else if (ret == -1) { - if (GetLastError() == ERROR_ACCESS_DENIED) - return PyErr_SetFromWindowsErr(0); - else - return PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); - } - else { - return NULL; - } -} - - -/* - * A wrapper around OpenProcess setting NSP exception if process - * no longer exists. - * "pid" is the process pid, "dwDesiredAccess" is the first argument - * exptected by OpenProcess. - * Return a process handle or NULL. - */ -HANDLE -psutil_handle_from_pid(DWORD pid, DWORD access) { - HANDLE hProcess; - - if (pid == 0) { - // otherwise we'd get NoSuchProcess - return AccessDenied(""); - } - // needed for GetExitCodeProcess - access |= PROCESS_QUERY_LIMITED_INFORMATION; - hProcess = OpenProcess(access, FALSE, pid); - return psutil_check_phandle(hProcess, pid); -} - - -DWORD * -psutil_get_pids(DWORD *numberOfReturnedPIDs) { - // Win32 SDK says the only way to know if our process array - // wasn't large enough is to check the returned size and make - // sure that it doesn't match the size of the array. - // If it does we allocate a larger array and try again - - // Stores the actual array - DWORD *procArray = NULL; - DWORD procArrayByteSz; - int procArraySz = 0; - - // Stores the byte size of the returned array from enumprocesses - DWORD enumReturnSz = 0; - - do { - procArraySz += 1024; - if (procArray != NULL) - free(procArray); - procArrayByteSz = procArraySz * sizeof(DWORD); - procArray = malloc(procArrayByteSz); - if (procArray == NULL) { - PyErr_NoMemory(); - return NULL; - } - if (! EnumProcesses(procArray, procArrayByteSz, &enumReturnSz)) { - free(procArray); - PyErr_SetFromWindowsErr(0); - return NULL; - } - } while (enumReturnSz == procArraySz * sizeof(DWORD)); - - // The number of elements is the returned size / size of each element - *numberOfReturnedPIDs = enumReturnSz / sizeof(DWORD); - - return procArray; -} - - -int -psutil_assert_pid_exists(DWORD pid, char *err) { - if (PSUTIL_TESTING) { - if (psutil_pid_in_pids(pid) == 0) { - PyErr_SetString(PyExc_AssertionError, err); - return 0; - } - } - return 1; -} - - -int -psutil_assert_pid_not_exists(DWORD pid, char *err) { - if (PSUTIL_TESTING) { - if (psutil_pid_in_pids(pid) == 1) { - PyErr_SetString(PyExc_AssertionError, err); - return 0; - } - } - return 1; -} - - -/* -/* Check for PID existance by using OpenProcess() + GetExitCodeProcess. -/* Returns: - * 1: pid exists - * 0: it doesn't - * -1: error - */ -int -psutil_pid_is_running(DWORD pid) { - HANDLE hProcess; - DWORD exitCode; - DWORD err; - - // Special case for PID 0 System Idle Process - if (pid == 0) - return 1; - if (pid < 0) - return 0; - hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - if (NULL == hProcess) { - err = GetLastError(); - // Yeah, this is the actual error code in case of "no such process". - if (err == ERROR_INVALID_PARAMETER) { - if (! psutil_assert_pid_not_exists( - pid, "pir: OpenProcess() -> INVALID_PARAMETER")) { - return -1; - } - return 0; - } - // Access denied obviously means there's a process to deny access to. - else if (err == ERROR_ACCESS_DENIED) { - if (! psutil_assert_pid_exists( - pid, "pir: OpenProcess() ACCESS_DENIED")) { - return -1; - } - return 1; - } - // Be strict and raise an exception; the caller is supposed - // to take -1 into account. - else { - PyErr_SetFromOSErrnoWithSyscall("OpenProcess(PROCESS_VM_READ)"); - return -1; - } - } - - if (GetExitCodeProcess(hProcess, &exitCode)) { - CloseHandle(hProcess); - // XXX - maybe STILL_ACTIVE is not fully reliable as per: - // http://stackoverflow.com/questions/1591342/#comment47830782_1591379 - if (exitCode == STILL_ACTIVE) { - if (! psutil_assert_pid_exists( - pid, "pir: GetExitCodeProcess() -> STILL_ACTIVE")) { - return -1; - } - return 1; - } - // We can't be sure so we look into pids. - else { - return psutil_pid_in_pids(pid); - } - } - else { - err = GetLastError(); - CloseHandle(hProcess); - // Same as for OpenProcess, assume access denied means there's - // a process to deny access to. - if (err == ERROR_ACCESS_DENIED) { - if (! psutil_assert_pid_exists( - pid, "pir: GetExitCodeProcess() -> ERROR_ACCESS_DENIED")) { - return -1; - } - return 1; - } - else { - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); - return -1; - } - } -} - - /* Given a pointer into a process's memory, figure out how much data can be * read from it. */ static int diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index 4278c4df9e..afbbb72d55 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -15,15 +15,8 @@ #define HANDLE_TO_PYNUM(handle) PyLong_FromUnsignedLong((unsigned long) handle) #define PYNUM_TO_HANDLE(obj) ((HANDLE)PyLong_AsUnsignedLong(obj)) -DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); -HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); -int psutil_pid_is_running(DWORD pid); int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); - -int psutil_assert_pid_exists(DWORD pid, char *err); -int psutil_assert_pid_not_exists(DWORD pid, char *err); - PyObject* psutil_get_cmdline(long pid, int use_peb); PyObject* psutil_get_cwd(long pid); PyObject* psutil_get_environ(long pid); diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c new file mode 100644 index 0000000000..a81a32531e --- /dev/null +++ b/psutil/arch/windows/process_utils.c @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + * + * Helper process functions. + */ + +#include +#include +#include +#include + +#include "ntextapi.h" +#include "global.h" +#include "process_utils.h" +#include "../../_psutil_common.h" + + +DWORD * +psutil_get_pids(DWORD *numberOfReturnedPIDs) { + // Win32 SDK says the only way to know if our process array + // wasn't large enough is to check the returned size and make + // sure that it doesn't match the size of the array. + // If it does we allocate a larger array and try again + + // Stores the actual array + DWORD *procArray = NULL; + DWORD procArrayByteSz; + int procArraySz = 0; + + // Stores the byte size of the returned array from enumprocesses + DWORD enumReturnSz = 0; + + do { + procArraySz += 1024; + if (procArray != NULL) + free(procArray); + procArrayByteSz = procArraySz * sizeof(DWORD); + procArray = malloc(procArrayByteSz); + if (procArray == NULL) { + PyErr_NoMemory(); + return NULL; + } + if (! EnumProcesses(procArray, procArrayByteSz, &enumReturnSz)) { + free(procArray); + PyErr_SetFromWindowsErr(0); + return NULL; + } + } while (enumReturnSz == procArraySz * sizeof(DWORD)); + + // The number of elements is the returned size / size of each element + *numberOfReturnedPIDs = enumReturnSz / sizeof(DWORD); + + return procArray; +} + +/* + * Return 1 if PID exists, 0 if not, -1 on error. + */ +int +psutil_pid_in_pids(DWORD pid) { + DWORD *proclist = NULL; + DWORD numberOfReturnedPIDs; + DWORD i; + + proclist = psutil_get_pids(&numberOfReturnedPIDs); + if (proclist == NULL) + return -1; + for (i = 0; i < numberOfReturnedPIDs; i++) { + if (proclist[i] == pid) { + free(proclist); + return 1; + } + } + free(proclist); + return 0; +} + + +/* + * Given a process HANDLE checks whether it's actually running. + * Returns: + * - 1: running + * - 0: not running + * - -1: WindowsError + * - -2: AssertionError + */ +int +psutil_is_phandle_running(HANDLE hProcess, DWORD pid) { + DWORD processExitCode = 0; + + if (hProcess == NULL) { + if (GetLastError() == ERROR_INVALID_PARAMETER) { + // Yeah, this is the actual error code in case of + // "no such process". + if (! psutil_assert_pid_not_exists( + pid, "iphr: OpenProcess() -> ERROR_INVALID_PARAMETER")) { + return -2; + } + return 0; + } + return -1; + } + + if (GetExitCodeProcess(hProcess, &processExitCode)) { + // XXX - maybe STILL_ACTIVE is not fully reliable as per: + // http://stackoverflow.com/questions/1591342/#comment47830782_1591379 + if (processExitCode == STILL_ACTIVE) { + if (! psutil_assert_pid_exists( + pid, "iphr: GetExitCodeProcess() -> STILL_ACTIVE")) { + return -2; + } + return 1; + } + else { + // We can't be sure so we look into pids. + if (psutil_pid_in_pids(pid) == 1) { + return 1; + } + else { + CloseHandle(hProcess); + return 0; + } + } + } + + CloseHandle(hProcess); + if (! psutil_assert_pid_not_exists( pid, "iphr: exit fun")) { + return -2; + } + return -1; +} + + +/* + * Given a process HANDLE checks whether it's actually running and if + * it does return it, else return NULL with the proper Python exception + * set. + */ +HANDLE +psutil_check_phandle(HANDLE hProcess, DWORD pid) { + int ret = psutil_is_phandle_running(hProcess, pid); + if (ret == 1) { + return hProcess; + } + else if (ret == 0) { + return NoSuchProcess(""); + } + else if (ret == -1) { + if (GetLastError() == ERROR_ACCESS_DENIED) + return PyErr_SetFromWindowsErr(0); + else + return PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + } + else { + return NULL; + } +} + + +/* + * A wrapper around OpenProcess setting NSP exception if process + * no longer exists. + * "pid" is the process pid, "dwDesiredAccess" is the first argument + * exptected by OpenProcess. + * Return a process handle or NULL. + */ +HANDLE +psutil_handle_from_pid(DWORD pid, DWORD access) { + HANDLE hProcess; + + if (pid == 0) { + // otherwise we'd get NoSuchProcess + return AccessDenied(""); + } + // needed for GetExitCodeProcess + access |= PROCESS_QUERY_LIMITED_INFORMATION; + hProcess = OpenProcess(access, FALSE, pid); + return psutil_check_phandle(hProcess, pid); +} + + +int +psutil_assert_pid_exists(DWORD pid, char *err) { + if (PSUTIL_TESTING) { + if (psutil_pid_in_pids(pid) == 0) { + PyErr_SetString(PyExc_AssertionError, err); + return 0; + } + } + return 1; +} + + +int +psutil_assert_pid_not_exists(DWORD pid, char *err) { + if (PSUTIL_TESTING) { + if (psutil_pid_in_pids(pid) == 1) { + PyErr_SetString(PyExc_AssertionError, err); + return 0; + } + } + return 1; +} + + +/* +/* Check for PID existance by using OpenProcess() + GetExitCodeProcess. +/* Returns: + * 1: pid exists + * 0: it doesn't + * -1: error + */ +int +psutil_pid_is_running(DWORD pid) { + HANDLE hProcess; + DWORD exitCode; + DWORD err; + + // Special case for PID 0 System Idle Process + if (pid == 0) + return 1; + if (pid < 0) + return 0; + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (NULL == hProcess) { + err = GetLastError(); + // Yeah, this is the actual error code in case of "no such process". + if (err == ERROR_INVALID_PARAMETER) { + if (! psutil_assert_pid_not_exists( + pid, "pir: OpenProcess() -> INVALID_PARAMETER")) { + return -1; + } + return 0; + } + // Access denied obviously means there's a process to deny access to. + else if (err == ERROR_ACCESS_DENIED) { + if (! psutil_assert_pid_exists( + pid, "pir: OpenProcess() ACCESS_DENIED")) { + return -1; + } + return 1; + } + // Be strict and raise an exception; the caller is supposed + // to take -1 into account. + else { + PyErr_SetFromOSErrnoWithSyscall("OpenProcess(PROCESS_VM_READ)"); + return -1; + } + } + + if (GetExitCodeProcess(hProcess, &exitCode)) { + CloseHandle(hProcess); + // XXX - maybe STILL_ACTIVE is not fully reliable as per: + // http://stackoverflow.com/questions/1591342/#comment47830782_1591379 + if (exitCode == STILL_ACTIVE) { + if (! psutil_assert_pid_exists( + pid, "pir: GetExitCodeProcess() -> STILL_ACTIVE")) { + return -1; + } + return 1; + } + // We can't be sure so we look into pids. + else { + return psutil_pid_in_pids(pid); + } + } + else { + err = GetLastError(); + CloseHandle(hProcess); + // Same as for OpenProcess, assume access denied means there's + // a process to deny access to. + if (err == ERROR_ACCESS_DENIED) { + if (! psutil_assert_pid_exists( + pid, "pir: GetExitCodeProcess() -> ERROR_ACCESS_DENIED")) { + return -1; + } + return 1; + } + else { + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + return -1; + } + } +} diff --git a/psutil/arch/windows/process_utils.h b/psutil/arch/windows/process_utils.h new file mode 100644 index 0000000000..a7171c5ca4 --- /dev/null +++ b/psutil/arch/windows/process_utils.h @@ -0,0 +1,11 @@ +/* + * 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. + */ + +DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); +HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); +int psutil_pid_is_running(DWORD pid); +int psutil_assert_pid_exists(DWORD pid, char *err); +int psutil_assert_pid_not_exists(DWORD pid, char *err); diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index dfa19ba9cd..b44a247b75 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -15,7 +15,7 @@ #include "ntextapi.h" #include "global.h" -#include "process_info.h" +#include "process_utils.h" #include "../../_psutil_common.h" diff --git a/setup.py b/setup.py index eb9b0f8cd4..1ebad30ad9 100755 --- a/setup.py +++ b/setup.py @@ -142,6 +142,7 @@ def get_winver(): 'psutil._psutil_windows', sources=sources + [ 'psutil/_psutil_windows.c', + 'psutil/arch/windows/process_utils.c', 'psutil/arch/windows/process_info.c', 'psutil/arch/windows/process_handles.c', 'psutil/arch/windows/security.c', From 0cbab098df8f8d6e242917d44c2231e0a35c8c11 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Dec 2019 15:59:43 -0800 Subject: [PATCH 0404/1714] highlight cmd.exe warnings/errs from VS --- scripts/internal/winmake.py | 54 ++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 116809cac1..c471ad0b81 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -12,6 +12,8 @@ """ from __future__ import print_function +import atexit +import ctypes import errno import fnmatch import functools @@ -59,6 +61,12 @@ if PY3: basestring = str +GREEN = 2 +YELLOW = 6 +RED = 4 +DEFAULT_COLOR = 7 + + # =================================================================== # utils # =================================================================== @@ -84,6 +92,26 @@ def safe_print(text, file=sys.stdout, flush=False): file.write("\n") +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(s, color=3): + color += 8 # bold + handle = stderr_handle() + SetConsoleTextAttribute = ctypes.windll.Kernel32.SetConsoleTextAttribute + SetConsoleTextAttribute(handle, color) + try: + print(s) + finally: + SetConsoleTextAttribute(handle, DEFAULT_COLOR) + + def sh(cmd, nolog=False): if not nolog: safe_print("cmd: " + cmd) @@ -211,13 +239,37 @@ def build(): # Make sure setuptools is installed (needed for 'develop' / # edit mode). sh('%s -c "import setuptools"' % PYTHON) - sh("%s setup.py build" % PYTHON) + + # Print coloured warnings in real time. + cmd = [PYTHON, "setup.py", "build"] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + try: + for line in iter(p.stdout.readline, b''): + if PY3: + line = line.decode() + line = line.strip() + if 'warning' in line: + win_colorprint(line, YELLOW) + elif 'error' in line: + win_colorprint(line, RED) + else: + print(line) + # retcode = p.poll() + p.communicate() + if p.returncode: + win_colorprint("failure", RED) + sys.exit(p.returncode) + finally: + p.terminate() + p.wait() + # Copies compiled *.pyd files in ./psutil directory in order to # allow "import psutil" when using the interactive interpreter # from within this directory. sh("%s setup.py build_ext -i" % PYTHON) # Make sure it actually worked. sh('%s -c "import psutil"' % PYTHON) + win_colorprint("success", GREEN) @cmd From 9e04312d615413c78a7c87d70e9de125429c9d33 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Jan 2020 23:18:59 -0800 Subject: [PATCH 0405/1714] Drop windows XP support (#1652) minimum supported now is Windows Vista --- HISTORY.rst | 2 + docs/index.rst | 4 +- psutil/__init__.py | 2 +- psutil/_pslinux.py | 3 +- psutil/_psutil_windows.c | 79 ++---------------------------------- psutil/_pswindows.py | 26 +----------- psutil/arch/windows/global.c | 2 - scripts/internal/winmake.py | 1 - 8 files changed, 11 insertions(+), 108 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 16d18c082e..f367db3ebe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 1648_: [Linux] sensors_temperatures() looks into an additional /sys/device/ directory for additional data. (patch by Javad Karabi) +- 1652_: [Windows] dropped Windows XP support. Minumum supported client now is + Windows Vista. **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 5ed3bb7c78..a7d86dd473 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2607,9 +2607,9 @@ Supported platforms These are the platforms I develop and test on: -* Linux Ubuntu 16.04 +* Linux Ubuntu 18.04 +* Windows 10 (support back to Windows Vista) * MacOS 10.11 El Captain -* Windows 10 * Solaris 10 * FreeBSD 11 * OpenBSD 6.4 diff --git a/psutil/__init__.py b/psutil/__init__.py index b267239e28..3eaa88ce6b 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -222,7 +222,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.7" +__version__ = "5.6.8" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e95581cc41..694e307b1c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1206,7 +1206,8 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/971 # https://github.com/nicolargo/glances/issues/1060 basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*')) - basenames.extend(glob.glob('/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*')) + basenames.extend(glob.glob( + '/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*')) basenames = sorted(set([x.split('_')[0] for x in basenames])) for base in basenames: diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 660e08afac..b572f0e1ad 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -26,9 +26,7 @@ #include #include // users() #include // cpu_freq() -#if (_WIN32_WINNT >= 0x0600) // Windows >= Vista #include // net_io_counters() -#endif // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") @@ -625,9 +623,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { long pid; HANDLE hProcess; wchar_t exe[MAX_PATH]; -#if (_WIN32_WINNT >= 0x0600) // >= Vista unsigned int size = sizeof(exe); -#endif if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; @@ -639,24 +635,12 @@ psutil_proc_exe(PyObject *self, PyObject *args) { // QueryFullProcessImageNameW is better than GetProcessImageFileNameW // (avoid using QueryDosDevice on the returned path), see: // https://github.com/giampaolo/psutil/issues/1394 -#if (_WIN32_WINNT >= 0x0600) // Windows >= Vista memset(exe, 0, MAX_PATH); if (QueryFullProcessImageNameW(hProcess, 0, exe, &size) == 0) { PyErr_SetFromOSErrnoWithSyscall("QueryFullProcessImageNameW"); CloseHandle(hProcess); return NULL; } -#else // Windows XP - if (GetProcessImageFileNameW(hProcess, exe, MAX_PATH) == 0) { - // see: https://github.com/giampaolo/psutil/issues/1394 - if (GetLastError() == 0) - PyErr_SetFromWindowsErr(ERROR_ACCESS_DENIED); - else - PyErr_SetFromOSErrnoWithSyscall("GetProcessImageFileNameW"); - CloseHandle(hProcess); - return NULL; - } -#endif CloseHandle(hProcess); return PyUnicode_FromWideChar(exe, wcslen(exe)); } @@ -709,12 +693,7 @@ static PyObject * psutil_proc_memory_info(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD pid; -#if (_WIN32_WINNT >= 0x0501) // Windows XP with SP2 PROCESS_MEMORY_COUNTERS_EX cnt; -#else - PROCESS_MEMORY_COUNTERS cnt; -#endif - SIZE_T private = 0; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; @@ -729,11 +708,6 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { CloseHandle(hProcess); return NULL; } - -#if (_WIN32_WINNT >= 0x0501) // Windows XP with SP2 - private = cnt.PrivateUsage; -#endif - CloseHandle(hProcess); // PROCESS_MEMORY_COUNTERS values are defined as SIZE_T which on 64bits @@ -752,7 +726,7 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { (unsigned long long)cnt.QuotaNonPagedPoolUsage, (unsigned long long)cnt.PagefileUsage, (unsigned long long)cnt.PeakPagefileUsage, - (unsigned long long)private); + (unsigned long long)cnt.PrivateUsage); #else return Py_BuildValue( "(kIIIIIIIII)", @@ -765,7 +739,7 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { (unsigned int)cnt.QuotaNonPagedPoolUsage, (unsigned int)cnt.PagefileUsage, (unsigned int)cnt.PeakPagefileUsage, - (unsigned int)private); + (unsigned int)cnt.PrivateUsage); #endif } @@ -1412,7 +1386,6 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { } -#if (_WIN32_WINNT >= 0x0600) // Windows Vista /* * Get process IO priority as a Python integer. */ @@ -1475,7 +1448,6 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { return psutil_SetFromNTStatusErr(status, "NtSetInformationProcess"); Py_RETURN_NONE; } -#endif /* @@ -1645,13 +1617,7 @@ psutil_disk_usage(PyObject *self, PyObject *args) { static PyObject * psutil_net_io_counters(PyObject *self, PyObject *args) { DWORD dwRetVal = 0; - -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above MIB_IF_ROW2 *pIfRow = NULL; -#else // Windows XP - MIB_IFROW *pIfRow = NULL; -#endif - PIP_ADAPTER_ADDRESSES pAddresses = NULL; PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; PyObject *py_retdict = PyDict_New(); @@ -1669,33 +1635,21 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { py_nic_name = NULL; py_nic_info = NULL; -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above pIfRow = (MIB_IF_ROW2 *) malloc(sizeof(MIB_IF_ROW2)); -#else // Windows XP - pIfRow = (MIB_IFROW *) malloc(sizeof(MIB_IFROW)); -#endif - if (pIfRow == NULL) { PyErr_NoMemory(); goto error; } -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above SecureZeroMemory((PVOID)pIfRow, sizeof(MIB_IF_ROW2)); pIfRow->InterfaceIndex = pCurrAddresses->IfIndex; dwRetVal = GetIfEntry2(pIfRow); -#else // Windows XP - pIfRow->dwIndex = pCurrAddresses->IfIndex; - dwRetVal = GetIfEntry(pIfRow); -#endif - if (dwRetVal != NO_ERROR) { PyErr_SetString(PyExc_RuntimeError, "GetIfEntry() or GetIfEntry2() syscalls failed."); goto error; } -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above py_nic_info = Py_BuildValue("(KKKKKKKK)", pIfRow->OutOctets, pIfRow->InOctets, @@ -1705,18 +1659,6 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { pIfRow->OutErrors, pIfRow->InDiscards, pIfRow->OutDiscards); -#else // Windows XP - py_nic_info = Py_BuildValue("(kkkkkkkk)", - pIfRow->dwOutOctets, - pIfRow->dwInOctets, - (pIfRow->dwOutUcastPkts + pIfRow->dwOutNUcastPkts), - (pIfRow->dwInUcastPkts + pIfRow->dwInNUcastPkts), - pIfRow->dwInErrors, - pIfRow->dwOutErrors, - pIfRow->dwInDiscards, - pIfRow->dwOutDiscards); -#endif - if (!py_nic_info) goto error; @@ -2224,7 +2166,6 @@ psutil_proc_info(PyObject *self, PyObject *args) { double user_time; double kernel_time; long long create_time; - SIZE_T mem_private; PyObject *py_retlist; if (! PyArg_ParseTuple(args, "l", &pid)) @@ -2252,12 +2193,6 @@ psutil_proc_info(PyObject *self, PyObject *args) { create_time /= 10000000; } -#if (_WIN32_WINNT >= 0x0501) // Windows XP with SP2 - mem_private = process->PrivatePageCount; -#else - mem_private = 0; -#endif - py_retlist = Py_BuildValue( #if defined(_WIN64) "kkdddiKKKKKK" "kKKKKKKKKK", @@ -2287,7 +2222,7 @@ psutil_proc_info(PyObject *self, PyObject *args) { process->QuotaNonPagedPoolUsage, // non paged pool process->PagefileUsage, // pagefile process->PeakPagefileUsage, // peak pagefile - mem_private // private + process->PrivatePageCount // private ); free(buffer); @@ -2461,11 +2396,9 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { char buff_macaddr[1024]; char buff_netmask[1024]; DWORD dwRetVal = 0; -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above ULONG converted_netmask; UINT netmask_bits; struct in_addr in_netmask; -#endif PIP_ADAPTER_ADDRESSES pAddresses = NULL; PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL; @@ -2548,7 +2481,6 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { sizeof(buff_addr)); if (!intRet) goto error; -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above netmask_bits = pUnicast->OnLinkPrefixLength; dwRetVal = ConvertLengthToIpv4Mask(netmask_bits, &converted_netmask); if (dwRetVal == NO_ERROR) { @@ -2559,7 +2491,6 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { if (!netmaskIntRet) goto error; } -#endif } else if (family == AF_INET6) { struct sockaddr_in6 *sa_in6 = (struct sockaddr_in6 *) @@ -2987,12 +2918,10 @@ PsutilMethods[] = { "Return process priority."}, {"proc_priority_set", psutil_proc_priority_set, METH_VARARGS, "Set process priority."}, -#if (_WIN32_WINNT >= 0x0600) // Windows Vista {"proc_io_priority_get", psutil_proc_io_priority_get, METH_VARARGS, "Return process IO priority."}, {"proc_io_priority_set", psutil_proc_io_priority_set, METH_VARARGS, "Set process IO priority."}, -#endif {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS, "Return process CPU affinity as a bitmask."}, {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS, @@ -3049,13 +2978,11 @@ PsutilMethods[] = { "Return NICs stats."}, {"cpu_freq", psutil_cpu_freq, METH_VARARGS, "Return CPU frequency."}, -#if (_WIN32_WINNT >= 0x0600) // Windows Vista {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, METH_VARARGS, "Initializes the emulated load average calculator."}, {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS, "Returns the emulated POSIX-like load average."}, -#endif {"sensors_battery", psutil_sensors_battery, METH_VARARGS, "Return battery metrics usage."}, {"getpagesize", psutil_getpagesize, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 636b0af905..7048579005 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -168,20 +168,6 @@ class IOPriority(enum.IntEnum): WIN_SERVER_2008 = (6, 0) WIN_VISTA = (6, 0) WIN_SERVER_2003 = (5, 2) -WIN_XP = (5, 1) - - -@lru_cache() -def get_winver(): - """Usage: - >>> if get_winver() <= WIN_VISTA: - ... ... - """ - wv = sys.getwindowsversion() - return (wv.major, wv.minor) - - -IS_WIN_XP = get_winver() < WIN_VISTA # ===================================================================== @@ -785,17 +771,7 @@ def name(self): @wrap_exceptions def exe(self): - # Dual implementation, see: - # https://github.com/giampaolo/psutil/pull/1413 - if not IS_WIN_XP: - exe = cext.proc_exe(self.pid) - else: - if self.pid in (0, 4): - # https://github.com/giampaolo/psutil/issues/414 - # https://github.com/giampaolo/psutil/issues/528 - raise AccessDenied(self.pid, self._name) - exe = cext.proc_exe(self.pid) - exe = convert_dos_path(exe) + exe = cext.proc_exe(self.pid) return py2_strencode(exe) @wrap_exceptions diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index 4d8526e31d..a09bb33745 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -123,13 +123,11 @@ psutil_loadlibs() { if (! psutil_rtlIpv4AddressToStringA) return 1; - // minimum requirement: Win XP SP3 psutil_GetExtendedTcpTable = psutil_GetProcAddressFromLib( "iphlpapi.dll", "GetExtendedTcpTable"); if (! psutil_GetExtendedTcpTable) return 1; - // minimum requirement: Win XP SP3 psutil_GetExtendedUdpTable = psutil_GetProcAddressFromLib( "iphlpapi.dll", "GetExtendedUdpTable"); if (! psutil_GetExtendedUdpTable) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c471ad0b81..ce6b5a8347 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -319,7 +319,6 @@ def install_pip(): @cmd def install(): """Install in develop / edit mode""" - install_git_hooks() build() sh("%s setup.py develop" % PYTHON) From 1cff4c060559242beb908f0f53e8d511964c47f7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Jan 2020 18:13:59 +0100 Subject: [PATCH 0406/1714] small refactoring --- psutil/arch/windows/global.h | 1 - psutil/tests/test_windows.py | 9 +++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 10ae640595..d7a35b319a 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -11,7 +11,6 @@ extern int PSUTIL_WINVER; extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; -#define PSUTIL_WINDOWS_XP 51 #define PSUTIL_WINDOWS_SERVER_2003 52 #define PSUTIL_WINDOWS_VISTA 60 #define PSUTIL_WINDOWS_7 61 diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 1075efddfd..de12ff50f6 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -32,16 +32,13 @@ from psutil.tests import sh from psutil.tests import unittest -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - try: +if WINDOWS: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") 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 From 19f87b14f83669fd450e032e35d5b328b9acc118 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Jan 2020 18:45:29 +0100 Subject: [PATCH 0407/1714] move custom exceptions in _common.py --- psutil/__init__.py | 111 ++----------------------------------------- psutil/_common.py | 104 ++++++++++++++++++++++++++++++++++++++++ psutil/_psaix.py | 10 ++-- psutil/_psbsd.py | 10 ++-- psutil/_pslinux.py | 10 ++-- psutil/_psosx.py | 12 ++--- psutil/_psposix.py | 6 +-- psutil/_pssunos.py | 10 ++-- psutil/_pswindows.py | 10 ++-- 9 files changed, 129 insertions(+), 154 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 093acf0cf0..bf34b429ed 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -38,10 +38,15 @@ pwd = None from . import _common +from ._common import AccessDenied from ._common import deprecated_method +from ._common import Error from ._common import memoize from ._common import memoize_when_activated +from ._common import NoSuchProcess +from ._common import TimeoutExpired from ._common import wrap_numbers as _wrap_numbers +from ._common import ZombieProcess from ._compat import long from ._compat import PermissionError from ._compat import ProcessLookupError @@ -254,112 +259,6 @@ raise ImportError(msg) -# ===================================================================== -# --- Exceptions -# ===================================================================== - - -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 - - -# Push exception classes into platform specific module namespace. -_psplatform.NoSuchProcess = NoSuchProcess -_psplatform.ZombieProcess = ZombieProcess -_psplatform.AccessDenied = AccessDenied -_psplatform.TimeoutExpired = TimeoutExpired -if POSIX: - from . import _psposix - _psposix.TimeoutExpired = TimeoutExpired - - # ===================================================================== # --- Utils # ===================================================================== diff --git a/psutil/_common.py b/psutil/_common.py index 2f74460bb2..729b198350 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -260,6 +260,110 @@ class BatteryTime(enum.IntEnum): }) +# ===================================================================== +# --- Exceptions +# ===================================================================== + + +class Error(Exception): + """Base exception class. All other psutil exceptions inherit + from this one. + """ + __module__ = 'psutil' + + 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. + """ + __module__ = 'psutil' + + 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 + + def __path__(self): + return 'xxx' + + +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, 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.""" + __module__ = 'psutil' + + 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. + """ + __module__ = 'psutil' + + 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 + + # =================================================================== # --- utils # =================================================================== diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 79e3be15a1..994366aaa1 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -18,13 +18,16 @@ from . import _psposix from . import _psutil_aix as cext from . import _psutil_posix as cext_posix +from ._common import AccessDenied from ._common import conn_to_ntuple from ._common import get_procfs_path 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 NoSuchProcess from ._common import usage_percent +from ._common import ZombieProcess from ._compat import FileNotFoundError from ._compat import PermissionError from ._compat import ProcessLookupError @@ -79,13 +82,6 @@ status=6, ttynr=7) -# These objects get set on "import psutil" from the __init__.py -# file, see: https://github.com/giampaolo/psutil/issues/1402 -NoSuchProcess = None -ZombieProcess = None -AccessDenied = None -TimeoutExpired = None - # ===================================================================== # --- named tuples diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 2f41dc0be9..78e436f7e0 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -16,14 +16,17 @@ from . import _psposix from . import _psutil_bsd as cext from . import _psutil_posix as cext_posix +from ._common import AccessDenied from ._common import conn_tmap from ._common import conn_to_ntuple from ._common import FREEBSD from ._common import memoize from ._common import memoize_when_activated from ._common import NETBSD +from ._common import NoSuchProcess from ._common import OPENBSD from ._common import usage_percent +from ._common import ZombieProcess from ._compat import FileNotFoundError from ._compat import PermissionError from ._compat import ProcessLookupError @@ -135,13 +138,6 @@ name=24, ) -# These objects get set on "import psutil" from the __init__.py -# file, see: https://github.com/giampaolo/psutil/issues/1402 -NoSuchProcess = None -ZombieProcess = None -AccessDenied = None -TimeoutExpired = None - # ===================================================================== # --- named tuples diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 694e307b1c..c681439d4b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -25,6 +25,7 @@ from . import _psposix from . import _psutil_linux as cext from . import _psutil_posix as cext_posix +from ._common import AccessDenied from ._common import decode from ._common import get_procfs_path from ._common import isfile_strict @@ -33,12 +34,14 @@ from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN +from ._common import NoSuchProcess 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 ._common import ZombieProcess from ._compat import b from ._compat import basestring from ._compat import FileNotFoundError @@ -161,13 +164,6 @@ class IOPriority(enum.IntEnum): "0B": _common.CONN_CLOSING } -# These objects get set on "import psutil" from the __init__.py -# file, see: https://github.com/giampaolo/psutil/issues/1402 -NoSuchProcess = None -ZombieProcess = None -AccessDenied = None -TimeoutExpired = None - # ===================================================================== # --- named tuples diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 7f28447bb1..e4296495c4 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -14,14 +14,17 @@ from . import _psposix from . import _psutil_osx as cext from . import _psutil_posix as cext_posix +from ._common import AccessDenied 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 NoSuchProcess from ._common import parse_environ_block +from ._common import usage_percent +from ._common import ZombieProcess from ._compat import PermissionError from ._compat import ProcessLookupError -from ._common import usage_percent __extra__all__ = [] @@ -83,13 +86,6 @@ volctxsw=7, ) -# These objects get set on "import psutil" from the __init__.py -# file, see: https://github.com/giampaolo/psutil/issues/1402 -NoSuchProcess = None -ZombieProcess = None -AccessDenied = None -TimeoutExpired = None - # ===================================================================== # --- named tuples diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 24570224fe..88213ef8b6 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -11,6 +11,7 @@ from ._common import memoize from ._common import sdiskusage +from ._common import TimeoutExpired from ._common import usage_percent from ._compat import ChildProcessError from ._compat import FileNotFoundError @@ -24,11 +25,6 @@ __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] -# This object gets set on "import psutil" from the __init__.py -# file, see: https://github.com/giampaolo/psutil/issues/1402 -TimeoutExpired = None - - def pid_exists(pid): """Check whether pid exists in the current process table.""" if pid == 0: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 9f0cac260f..b1e2d1c94d 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -17,13 +17,16 @@ from . import _psposix from . import _psutil_posix as cext_posix from . import _psutil_sunos as cext +from ._common import AccessDenied from ._common import AF_INET6 from ._common import get_procfs_path from ._common import isfile_strict from ._common import memoize_when_activated +from ._common import NoSuchProcess from ._common import sockfam_to_enum from ._common import socktype_to_enum from ._common import usage_percent +from ._common import ZombieProcess from ._compat import b from ._compat import FileNotFoundError from ._compat import PermissionError @@ -87,13 +90,6 @@ gid=10, egid=11) -# These objects get set on "import psutil" from the __init__.py -# file, see: https://github.com/giampaolo/psutil/issues/1402 -NoSuchProcess = None -ZombieProcess = None -AccessDenied = None -TimeoutExpired = None - # ===================================================================== # --- named tuples diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 636b0af905..4ad511e5db 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -32,6 +32,7 @@ else: raise +from ._common import AccessDenied from ._common import conn_tmap from ._common import conn_to_ntuple from ._common import ENCODING @@ -39,7 +40,9 @@ from ._common import isfile_strict from ._common import memoize from ._common import memoize_when_activated +from ._common import NoSuchProcess from ._common import parse_environ_block +from ._common import TimeoutExpired from ._common import usage_percent from ._compat import long from ._compat import lru_cache @@ -154,13 +157,6 @@ class IOPriority(enum.IntEnum): mem_private=21, ) -# These objects get set on "import psutil" from the __init__.py -# file, see: https://github.com/giampaolo/psutil/issues/1402 -NoSuchProcess = None -ZombieProcess = None -AccessDenied = None -TimeoutExpired = None - # More values at: https://stackoverflow.com/a/20804735/376587 WIN_10 = (10, 0) WIN_8 = (6, 2) From ac61e733137dbd392635a8573db58c58207eb671 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Jan 2020 18:56:02 +0100 Subject: [PATCH 0408/1714] #1652: also drop support for Windows Server 2003 --- HISTORY.rst | 4 ++-- docs/index.rst | 5 ++--- psutil/_psutil_windows.c | 4 ---- psutil/arch/windows/global.c | 6 +----- psutil/arch/windows/global.h | 1 - 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f367db3ebe..d112ccc215 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,8 +9,8 @@ XXXX-XX-XX - 1648_: [Linux] sensors_temperatures() looks into an additional /sys/device/ directory for additional data. (patch by Javad Karabi) -- 1652_: [Windows] dropped Windows XP support. Minumum supported client now is - Windows Vista. +- 1652_: [Windows] dropped support for Windows XP and Windows Server 2003. + Minimum supported Windows version now is Windows Vista. **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 877ffcbf4b..dc025642ae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2619,9 +2619,8 @@ These are the platforms I develop and test on: Earlier versions are supposed to work but are not tested. For Linux, Windows and MacOS we have continuos integration. Other platforms are tested manually from time to time. -Oldest supported Windows version is Windows XP, which can be compiled from -sources. Latest wheel supporting Windows XP is -`psutil 2.1.3 `__. +Minimum supported Windows version is Windows Vista (Windows XP and Windows +Server 2003 are not supported). Supported Python versions are 3.4+, 2.7 and 2.6. FAQs diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index b572f0e1ad..5ab4169503 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3172,10 +3172,6 @@ void init_psutil_windows(void) module, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD); PyModule_AddIntConstant( module, "WINVER", PSUTIL_WINVER); - PyModule_AddIntConstant( - module, "WINDOWS_XP", PSUTIL_WINDOWS_XP); - PyModule_AddIntConstant( - module, "WINDOWS_SERVER_2003", PSUTIL_WINDOWS_SERVER_2003); PyModule_AddIntConstant( module, "WINDOWS_VISTA", PSUTIL_WINDOWS_VISTA); PyModule_AddIntConstant( diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index a09bb33745..1aafcdd14d 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -193,11 +193,7 @@ psutil_set_winver() { psutil_RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); maj = versionInfo.dwMajorVersion; min = versionInfo.dwMinorVersion; - if (maj == 5 && min == 1) - PSUTIL_WINVER = PSUTIL_WINDOWS_XP; - else if (maj == 5 && min == 2) - PSUTIL_WINVER = PSUTIL_WINDOWS_SERVER_2003; - else if (maj == 6 && min == 0) + if (maj == 6 && min == 0) PSUTIL_WINVER = PSUTIL_WINDOWS_VISTA; // or Server 2008 else if (maj == 6 && min == 1) PSUTIL_WINVER = PSUTIL_WINDOWS_7; diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index d7a35b319a..4cf77cdeb6 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -11,7 +11,6 @@ extern int PSUTIL_WINVER; extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; -#define PSUTIL_WINDOWS_SERVER_2003 52 #define PSUTIL_WINDOWS_VISTA 60 #define PSUTIL_WINDOWS_7 61 #define PSUTIL_WINDOWS_8 62 From 1358fb7d734878b7e238c39af93ec19dfbe7c77e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Jan 2020 13:05:10 -0800 Subject: [PATCH 0409/1714] Windows: split C modules (#1655) --- psutil/_psutil_windows.c | 1077 +------------------------ psutil/arch/windows/cpu.c | 413 ++++++++++ psutil/arch/windows/cpu.h | 14 + psutil/arch/windows/disk.c | 375 +++++++++ psutil/arch/windows/disk.h | 12 + psutil/arch/windows/global.h | 3 + psutil/arch/windows/net.c | 449 +++++++++++ psutil/arch/windows/net.h | 11 + psutil/arch/windows/ntextapi.h | 10 +- psutil/arch/windows/process_handles.c | 4 +- psutil/arch/windows/process_info.c | 24 +- psutil/arch/windows/process_info.h | 11 - psutil/arch/windows/process_utils.c | 3 +- psutil/arch/windows/services.h | 2 +- psutil/arch/windows/socks.c | 2 - psutil/arch/windows/wmi.c | 4 +- psutil/arch/windows/wmi.h | 4 +- setup.py | 3 + 18 files changed, 1309 insertions(+), 1112 deletions(-) create mode 100644 psutil/arch/windows/cpu.c create mode 100644 psutil/arch/windows/cpu.h create mode 100644 psutil/arch/windows/disk.c create mode 100644 psutil/arch/windows/disk.h create mode 100644 psutil/arch/windows/net.c create mode 100644 psutil/arch/windows/net.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 5ab4169503..9c430c5f41 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -19,14 +19,10 @@ #include #include -#include +#include // memory_info(), memory_maps() #include -#include // disk_io_counters() -#include -#include +#include // threads(), PROCESSENTRY32 #include // users() -#include // cpu_freq() -#include // net_io_counters() // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") @@ -37,64 +33,18 @@ #include "arch/windows/process_utils.h" #include "arch/windows/process_info.h" #include "arch/windows/process_handles.h" +#include "arch/windows/disk.h" +#include "arch/windows/cpu.h" +#include "arch/windows/net.h" #include "arch/windows/inet_ntop.h" #include "arch/windows/services.h" #include "arch/windows/socks.h" #include "arch/windows/wmi.h" #include "_psutil_common.h" - -/* - * ============================================================================ - * Utilities - * ============================================================================ - */ - -#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) -#define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) -#define LO_T 1e-7 -#define HI_T 429.4967296 -#ifndef AF_INET6 -#define AF_INET6 23 -#endif - - -PIP_ADAPTER_ADDRESSES -psutil_get_nic_addresses() { - // allocate a 15 KB buffer to start with - int outBufLen = 15000; - DWORD dwRetVal = 0; - ULONG attempts = 0; - PIP_ADAPTER_ADDRESSES pAddresses = NULL; - - do { - pAddresses = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen); - if (pAddresses == NULL) { - PyErr_NoMemory(); - return NULL; - } - - dwRetVal = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, pAddresses, - &outBufLen); - if (dwRetVal == ERROR_BUFFER_OVERFLOW) { - free(pAddresses); - pAddresses = NULL; - } - else { - break; - } - - attempts++; - } while ((dwRetVal == ERROR_BUFFER_OVERFLOW) && (attempts < 3)); - - if (dwRetVal != NO_ERROR) { - PyErr_SetString( - PyExc_RuntimeError, "GetAdaptersAddresses() syscall failed."); - return NULL; - } - - return pAddresses; -} +// Raised by Process.wait(). +static PyObject *TimeoutExpired; +static PyObject *TimeoutAbandoned; /* @@ -126,16 +76,6 @@ psutil_get_num_cpus(int fail_on_err) { } -/* - * ============================================================================ - * Public Python API - * ============================================================================ - */ - -// Raised by Process.wait(). -static PyObject *TimeoutExpired; -static PyObject *TimeoutAbandoned; - /* * Return a Python float representing the system uptime expressed in seconds * since the epoch. @@ -465,104 +405,6 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { } -/* - * Return the number of active, logical CPUs. - */ -static PyObject * -psutil_cpu_count_logical(PyObject *self, PyObject *args) { - unsigned int ncpus; - - ncpus = psutil_get_num_cpus(0); - if (ncpus != 0) - return Py_BuildValue("I", ncpus); - else - Py_RETURN_NONE; // mimick os.cpu_count() -} - - -/* - * Return the number of physical CPU cores (hyper-thread CPUs count - * is excluded). - */ -static PyObject * -psutil_cpu_count_phys(PyObject *self, PyObject *args) { - DWORD rc; - PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX buffer = NULL; - PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX ptr = NULL; - DWORD length = 0; - DWORD offset = 0; - DWORD ncpus = 0; - DWORD prev_processor_info_size = 0; - - // GetLogicalProcessorInformationEx() is available from Windows 7 - // onward. Differently from GetLogicalProcessorInformation() - // it supports process groups, meaning this is able to report more - // than 64 CPUs. See: - // https://bugs.python.org/issue33166 - if (psutil_GetLogicalProcessorInformationEx == NULL) { - psutil_debug("Win < 7; cpu_count_phys() forced to None"); - Py_RETURN_NONE; - } - - while (1) { - rc = psutil_GetLogicalProcessorInformationEx( - RelationAll, buffer, &length); - if (rc == FALSE) { - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - if (buffer) { - free(buffer); - } - buffer = \ - (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)malloc(length); - if (NULL == buffer) { - PyErr_NoMemory(); - return NULL; - } - } - else { - psutil_debug("GetLogicalProcessorInformationEx() returned ", - GetLastError()); - goto return_none; - } - } - else { - break; - } - } - - ptr = buffer; - while (offset < length) { - // Advance ptr by the size of the previous - // SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX struct. - ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)\ - (((char*)ptr) + prev_processor_info_size); - - if (ptr->Relationship == RelationProcessorCore) { - ncpus += 1; - } - - // When offset == length, we've reached the last processor - // info struct in the buffer. - offset += ptr->Size; - prev_processor_info_size = ptr->Size; - } - - free(buffer); - if (ncpus != 0) { - return Py_BuildValue("I", ncpus); - } - else { - psutil_debug("GetLogicalProcessorInformationEx() count was 0"); - Py_RETURN_NONE; // mimick os.cpu_count() - } - -return_none: - if (buffer != NULL) - free(buffer); - Py_RETURN_NONE; -} - - /* * Return process cmdline as a Python list of cmdline arguments. */ @@ -1174,35 +1016,6 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { } -/* - Accept a filename's drive in native format like "\Device\HarddiskVolume1\" - and return the corresponding drive letter (e.g. "C:\\"). - If no match is found return an empty string. -*/ -static PyObject * -psutil_win32_QueryDosDevice(PyObject *self, PyObject *args) { - LPCTSTR lpDevicePath; - TCHAR d = TEXT('A'); - TCHAR szBuff[5]; - - if (!PyArg_ParseTuple(args, "s", &lpDevicePath)) - return NULL; - - while (d <= TEXT('Z')) { - TCHAR szDeviceName[3] = {d, TEXT(':'), TEXT('\0')}; - TCHAR szTarget[512] = {0}; - if (QueryDosDevice(szDeviceName, szTarget, 511) != 0) { - if (_tcscmp(lpDevicePath, szTarget) == 0) { - _stprintf_s(szBuff, _countof(szBuff), TEXT("%c:"), d); - return Py_BuildValue("s", szBuff); - } - } - d++; - } - return Py_BuildValue("s", ""); -} - - /* * Return process username as a "DOMAIN//USERNAME" string. */ @@ -1573,418 +1386,6 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { } -/* - * Return path's disk total and free as a Python tuple. - */ -static PyObject * -psutil_disk_usage(PyObject *self, PyObject *args) { - BOOL retval; - ULARGE_INTEGER _, total, free; - char *path; - - if (PyArg_ParseTuple(args, "u", &path)) { - Py_BEGIN_ALLOW_THREADS - retval = GetDiskFreeSpaceExW((LPCWSTR)path, &_, &total, &free); - Py_END_ALLOW_THREADS - goto return_; - } - - // on Python 2 we also want to accept plain strings other - // than Unicode -#if PY_MAJOR_VERSION <= 2 - PyErr_Clear(); // drop the argument parsing error - if (PyArg_ParseTuple(args, "s", &path)) { - Py_BEGIN_ALLOW_THREADS - retval = GetDiskFreeSpaceEx(path, &_, &total, &free); - Py_END_ALLOW_THREADS - goto return_; - } -#endif - - return NULL; - -return_: - if (retval == 0) - return PyErr_SetFromWindowsErrWithFilename(0, path); - else - return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); -} - - -/* - * Return a Python list of named tuples with overall network I/O information - */ -static PyObject * -psutil_net_io_counters(PyObject *self, PyObject *args) { - DWORD dwRetVal = 0; - MIB_IF_ROW2 *pIfRow = NULL; - PIP_ADAPTER_ADDRESSES pAddresses = NULL; - PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; - PyObject *py_retdict = PyDict_New(); - PyObject *py_nic_info = NULL; - PyObject *py_nic_name = NULL; - - if (py_retdict == NULL) - return NULL; - pAddresses = psutil_get_nic_addresses(); - if (pAddresses == NULL) - goto error; - pCurrAddresses = pAddresses; - - while (pCurrAddresses) { - py_nic_name = NULL; - py_nic_info = NULL; - - pIfRow = (MIB_IF_ROW2 *) malloc(sizeof(MIB_IF_ROW2)); - if (pIfRow == NULL) { - PyErr_NoMemory(); - goto error; - } - - SecureZeroMemory((PVOID)pIfRow, sizeof(MIB_IF_ROW2)); - pIfRow->InterfaceIndex = pCurrAddresses->IfIndex; - dwRetVal = GetIfEntry2(pIfRow); - if (dwRetVal != NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, - "GetIfEntry() or GetIfEntry2() syscalls failed."); - goto error; - } - - py_nic_info = Py_BuildValue("(KKKKKKKK)", - pIfRow->OutOctets, - pIfRow->InOctets, - (pIfRow->OutUcastPkts + pIfRow->OutNUcastPkts), - (pIfRow->InUcastPkts + pIfRow->InNUcastPkts), - pIfRow->InErrors, - pIfRow->OutErrors, - pIfRow->InDiscards, - pIfRow->OutDiscards); - if (!py_nic_info) - goto error; - - py_nic_name = PyUnicode_FromWideChar( - pCurrAddresses->FriendlyName, - wcslen(pCurrAddresses->FriendlyName)); - - if (py_nic_name == NULL) - goto error; - if (PyDict_SetItem(py_retdict, py_nic_name, py_nic_info)) - goto error; - Py_CLEAR(py_nic_name); - Py_CLEAR(py_nic_info); - - free(pIfRow); - pCurrAddresses = pCurrAddresses->Next; - } - - free(pAddresses); - return py_retdict; - -error: - Py_XDECREF(py_nic_name); - Py_XDECREF(py_nic_info); - Py_DECREF(py_retdict); - if (pAddresses != NULL) - free(pAddresses); - if (pIfRow != NULL) - free(pIfRow); - return NULL; -} - - -/* - * Return a Python dict of tuples for disk I/O information. This may - * require running "diskperf -y" command first. - */ -static PyObject * -psutil_disk_io_counters(PyObject *self, PyObject *args) { - DISK_PERFORMANCE diskPerformance; - DWORD dwSize; - HANDLE hDevice = NULL; - char szDevice[MAX_PATH]; - char szDeviceDisplay[MAX_PATH]; - int devNum; - int i; - DWORD ioctrlSize; - BOOL ret; - PyObject *py_retdict = PyDict_New(); - PyObject *py_tuple = NULL; - - if (py_retdict == NULL) - return NULL; - // Apparently there's no way to figure out how many times we have - // to iterate in order to find valid drives. - // Let's assume 32, which is higher than 26, the number of letters - // in the alphabet (from A:\ to Z:\). - for (devNum = 0; devNum <= 32; ++devNum) { - py_tuple = NULL; - sprintf_s(szDevice, MAX_PATH, "\\\\.\\PhysicalDrive%d", devNum); - hDevice = CreateFile(szDevice, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, OPEN_EXISTING, 0, NULL); - if (hDevice == INVALID_HANDLE_VALUE) - continue; - - // DeviceIoControl() sucks! - i = 0; - ioctrlSize = sizeof(diskPerformance); - while (1) { - i += 1; - ret = DeviceIoControl( - hDevice, IOCTL_DISK_PERFORMANCE, NULL, 0, &diskPerformance, - ioctrlSize, &dwSize, NULL); - if (ret != 0) - break; // OK! - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - // Retry with a bigger buffer (+ limit for retries). - if (i <= 1024) { - ioctrlSize *= 2; - continue; - } - } - else if (GetLastError() == ERROR_INVALID_FUNCTION) { - // This happens on AppVeyor: - // https://ci.appveyor.com/project/giampaolo/psutil/build/ - // 1364/job/ascpdi271b06jle3 - // Assume it means we're dealing with some exotic disk - // and go on. - psutil_debug("DeviceIoControl -> ERROR_INVALID_FUNCTION; " - "ignore PhysicalDrive%i", devNum); - goto next; - } - else if (GetLastError() == ERROR_NOT_SUPPORTED) { - // Again, let's assume we're dealing with some exotic disk. - psutil_debug("DeviceIoControl -> ERROR_NOT_SUPPORTED; " - "ignore PhysicalDrive%i", devNum); - goto next; - } - // XXX: it seems we should also catch ERROR_INVALID_PARAMETER: - // https://sites.ualberta.ca/dept/aict/uts/software/openbsd/ - // ports/4.1/i386/openafs/w-openafs-1.4.14-transarc/ - // openafs-1.4.14/src/usd/usd_nt.c - - // XXX: we can also bump into ERROR_MORE_DATA in which case - // (quoting doc) we're supposed to retry with a bigger buffer - // and specify a new "starting point", whatever it means. - PyErr_SetFromWindowsErr(0); - goto error; - } - - sprintf_s(szDeviceDisplay, MAX_PATH, "PhysicalDrive%i", devNum); - py_tuple = Py_BuildValue( - "(IILLKK)", - diskPerformance.ReadCount, - diskPerformance.WriteCount, - diskPerformance.BytesRead, - diskPerformance.BytesWritten, - // convert to ms: - // https://github.com/giampaolo/psutil/issues/1012 - (unsigned long long) - (diskPerformance.ReadTime.QuadPart) / 10000000, - (unsigned long long) - (diskPerformance.WriteTime.QuadPart) / 10000000); - if (!py_tuple) - goto error; - if (PyDict_SetItemString(py_retdict, szDeviceDisplay, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - -next: - CloseHandle(hDevice); - } - - return py_retdict; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retdict); - if (hDevice != NULL) - CloseHandle(hDevice); - return NULL; -} - - -static char *psutil_get_drive_type(int type) { - switch (type) { - case DRIVE_FIXED: - return "fixed"; - case DRIVE_CDROM: - return "cdrom"; - case DRIVE_REMOVABLE: - return "removable"; - case DRIVE_UNKNOWN: - return "unknown"; - case DRIVE_NO_ROOT_DIR: - return "unmounted"; - case DRIVE_REMOTE: - return "remote"; - case DRIVE_RAMDISK: - return "ramdisk"; - default: - return "?"; - } -} - - -#ifndef _ARRAYSIZE -#define _ARRAYSIZE(a) (sizeof(a)/sizeof(a[0])) -#endif - - -/* - * Return disk partitions as a list of tuples such as - * (drive_letter, drive_letter, type, "") - */ -static PyObject * -psutil_disk_partitions(PyObject *self, PyObject *args) { - DWORD num_bytes; - char drive_strings[255]; - char *drive_letter = drive_strings; - char mp_buf[MAX_PATH]; - char mp_path[MAX_PATH]; - int all; - int type; - int ret; - unsigned int old_mode = 0; - char opts[20]; - HANDLE mp_h; - BOOL mp_flag= TRUE; - LPTSTR fs_type[MAX_PATH + 1] = { 0 }; - DWORD pflags = 0; - PyObject *py_all; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - - if (py_retlist == NULL) { - return NULL; - } - - // avoid to visualize a message box in case something goes wrong - // see https://github.com/giampaolo/psutil/issues/264 - old_mode = SetErrorMode(SEM_FAILCRITICALERRORS); - - if (! PyArg_ParseTuple(args, "O", &py_all)) - goto error; - all = PyObject_IsTrue(py_all); - - Py_BEGIN_ALLOW_THREADS - num_bytes = GetLogicalDriveStrings(254, drive_letter); - Py_END_ALLOW_THREADS - - if (num_bytes == 0) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - while (*drive_letter != 0) { - py_tuple = NULL; - opts[0] = 0; - fs_type[0] = 0; - - Py_BEGIN_ALLOW_THREADS - type = GetDriveType(drive_letter); - Py_END_ALLOW_THREADS - - // by default we only show hard drives and cd-roms - if (all == 0) { - if ((type == DRIVE_UNKNOWN) || - (type == DRIVE_NO_ROOT_DIR) || - (type == DRIVE_REMOTE) || - (type == DRIVE_RAMDISK)) { - goto next; - } - // floppy disk: skip it by default as it introduces a - // considerable slowdown. - if ((type == DRIVE_REMOVABLE) && - (strcmp(drive_letter, "A:\\") == 0)) { - goto next; - } - } - - ret = GetVolumeInformation( - (LPCTSTR)drive_letter, NULL, _ARRAYSIZE(drive_letter), - NULL, NULL, &pflags, (LPTSTR)fs_type, _ARRAYSIZE(fs_type)); - if (ret == 0) { - // We might get here in case of a floppy hard drive, in - // which case the error is (21, "device not ready"). - // Let's pretend it didn't happen as we already have - // the drive name and type ('removable'). - strcat_s(opts, _countof(opts), ""); - SetLastError(0); - } - else { - if (pflags & FILE_READ_ONLY_VOLUME) - strcat_s(opts, _countof(opts), "ro"); - else - strcat_s(opts, _countof(opts), "rw"); - if (pflags & FILE_VOLUME_IS_COMPRESSED) - strcat_s(opts, _countof(opts), ",compressed"); - - // Check for mount points on this volume and add/get info - // (checks first to know if we can even have mount points) - if (pflags & FILE_SUPPORTS_REPARSE_POINTS) { - - mp_h = FindFirstVolumeMountPoint(drive_letter, mp_buf, MAX_PATH); - if (mp_h != INVALID_HANDLE_VALUE) { - while (mp_flag) { - - // Append full mount path with drive letter - strcpy_s(mp_path, _countof(mp_path), drive_letter); - strcat_s(mp_path, _countof(mp_path), mp_buf); - - py_tuple = Py_BuildValue( - "(ssss)", - drive_letter, - mp_path, - fs_type, // Typically NTFS - opts); - - if (!py_tuple || PyList_Append(py_retlist, py_tuple) == -1) { - FindVolumeMountPointClose(mp_h); - goto error; - } - - Py_CLEAR(py_tuple); - - // Continue looking for more mount points - mp_flag = FindNextVolumeMountPoint(mp_h, mp_buf, MAX_PATH); - } - FindVolumeMountPointClose(mp_h); - } - - } - } - - if (strlen(opts) > 0) - strcat_s(opts, _countof(opts), ","); - strcat_s(opts, _countof(opts), psutil_get_drive_type(type)); - - py_tuple = Py_BuildValue( - "(ssss)", - drive_letter, - drive_letter, - fs_type, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS - opts); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - goto next; - -next: - drive_letter = strchr(drive_letter, 0) + 1; - } - - SetErrorMode(old_mode); - return py_retlist; - -error: - SetErrorMode(old_mode); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - return NULL; -} - /* * Return a Python dict of tuples for disk I/O information */ @@ -2381,468 +1782,6 @@ psutil_ppid_map(PyObject *self, PyObject *args) { } -/* - * Return NICs addresses. - */ - -static PyObject * -psutil_net_if_addrs(PyObject *self, PyObject *args) { - unsigned int i = 0; - ULONG family; - PCTSTR intRet; - PCTSTR netmaskIntRet; - char *ptr; - char buff_addr[1024]; - char buff_macaddr[1024]; - char buff_netmask[1024]; - DWORD dwRetVal = 0; - ULONG converted_netmask; - UINT netmask_bits; - struct in_addr in_netmask; - PIP_ADAPTER_ADDRESSES pAddresses = NULL; - PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; - PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL; - - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_address = NULL; - PyObject *py_mac_address = NULL; - PyObject *py_nic_name = NULL; - PyObject *py_netmask = NULL; - - if (py_retlist == NULL) - return NULL; - - pAddresses = psutil_get_nic_addresses(); - if (pAddresses == NULL) - goto error; - pCurrAddresses = pAddresses; - - while (pCurrAddresses) { - pUnicast = pCurrAddresses->FirstUnicastAddress; - - netmaskIntRet = NULL; - py_nic_name = NULL; - py_nic_name = PyUnicode_FromWideChar( - pCurrAddresses->FriendlyName, - wcslen(pCurrAddresses->FriendlyName)); - if (py_nic_name == NULL) - goto error; - - // MAC address - if (pCurrAddresses->PhysicalAddressLength != 0) { - ptr = buff_macaddr; - *ptr = '\0'; - for (i = 0; i < (int) pCurrAddresses->PhysicalAddressLength; i++) { - if (i == (pCurrAddresses->PhysicalAddressLength - 1)) { - sprintf_s(ptr, _countof(buff_macaddr), "%.2X\n", - (int)pCurrAddresses->PhysicalAddress[i]); - } - else { - sprintf_s(ptr, _countof(buff_macaddr), "%.2X-", - (int)pCurrAddresses->PhysicalAddress[i]); - } - ptr += 3; - } - *--ptr = '\0'; - - py_mac_address = Py_BuildValue("s", buff_macaddr); - if (py_mac_address == NULL) - goto error; - - Py_INCREF(Py_None); - Py_INCREF(Py_None); - Py_INCREF(Py_None); - py_tuple = Py_BuildValue( - "(OiOOOO)", - py_nic_name, - -1, // this will be converted later to AF_LINK - py_mac_address, - Py_None, // netmask (not supported) - Py_None, // broadcast (not supported) - Py_None // ptp (not supported on Windows) - ); - if (! py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - Py_CLEAR(py_mac_address); - } - - // find out the IP address associated with the NIC - if (pUnicast != NULL) { - for (i = 0; pUnicast != NULL; i++) { - family = pUnicast->Address.lpSockaddr->sa_family; - if (family == AF_INET) { - struct sockaddr_in *sa_in = (struct sockaddr_in *) - pUnicast->Address.lpSockaddr; - intRet = inet_ntop(AF_INET, &(sa_in->sin_addr), buff_addr, - sizeof(buff_addr)); - if (!intRet) - goto error; - netmask_bits = pUnicast->OnLinkPrefixLength; - dwRetVal = ConvertLengthToIpv4Mask(netmask_bits, &converted_netmask); - if (dwRetVal == NO_ERROR) { - in_netmask.s_addr = converted_netmask; - netmaskIntRet = inet_ntop( - AF_INET, &in_netmask, buff_netmask, - sizeof(buff_netmask)); - if (!netmaskIntRet) - goto error; - } - } - else if (family == AF_INET6) { - struct sockaddr_in6 *sa_in6 = (struct sockaddr_in6 *) - pUnicast->Address.lpSockaddr; - intRet = inet_ntop(AF_INET6, &(sa_in6->sin6_addr), - buff_addr, sizeof(buff_addr)); - if (!intRet) - goto error; - } - else { - // we should never get here - pUnicast = pUnicast->Next; - continue; - } - -#if PY_MAJOR_VERSION >= 3 - py_address = PyUnicode_FromString(buff_addr); -#else - py_address = PyString_FromString(buff_addr); -#endif - if (py_address == NULL) - goto error; - - if (netmaskIntRet != NULL) { -#if PY_MAJOR_VERSION >= 3 - py_netmask = PyUnicode_FromString(buff_netmask); -#else - py_netmask = PyString_FromString(buff_netmask); -#endif - } else { - Py_INCREF(Py_None); - py_netmask = Py_None; - } - - Py_INCREF(Py_None); - Py_INCREF(Py_None); - py_tuple = Py_BuildValue( - "(OiOOOO)", - py_nic_name, - family, - py_address, - py_netmask, - Py_None, // broadcast (not supported) - Py_None // ptp (not supported on Windows) - ); - - if (! py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - Py_CLEAR(py_address); - Py_CLEAR(py_netmask); - - pUnicast = pUnicast->Next; - } - } - Py_CLEAR(py_nic_name); - pCurrAddresses = pCurrAddresses->Next; - } - - free(pAddresses); - return py_retlist; - -error: - if (pAddresses) - free(pAddresses); - Py_DECREF(py_retlist); - Py_XDECREF(py_tuple); - Py_XDECREF(py_address); - Py_XDECREF(py_nic_name); - Py_XDECREF(py_netmask); - return NULL; -} - - -/* - * Provides stats about NIC interfaces installed on the system. - * TODO: get 'duplex' (currently it's hard coded to '2', aka - 'full duplex') - */ -static PyObject * -psutil_net_if_stats(PyObject *self, PyObject *args) { - int i; - DWORD dwSize = 0; - DWORD dwRetVal = 0; - MIB_IFTABLE *pIfTable; - MIB_IFROW *pIfRow; - PIP_ADAPTER_ADDRESSES pAddresses = NULL; - PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; - char descr[MAX_PATH]; - int ifname_found; - - PyObject *py_nic_name = NULL; - PyObject *py_retdict = PyDict_New(); - PyObject *py_ifc_info = NULL; - PyObject *py_is_up = NULL; - - if (py_retdict == NULL) - return NULL; - - pAddresses = psutil_get_nic_addresses(); - if (pAddresses == NULL) - goto error; - - pIfTable = (MIB_IFTABLE *) malloc(sizeof (MIB_IFTABLE)); - if (pIfTable == NULL) { - PyErr_NoMemory(); - goto error; - } - dwSize = sizeof(MIB_IFTABLE); - if (GetIfTable(pIfTable, &dwSize, FALSE) == ERROR_INSUFFICIENT_BUFFER) { - free(pIfTable); - pIfTable = (MIB_IFTABLE *) malloc(dwSize); - if (pIfTable == NULL) { - PyErr_NoMemory(); - goto error; - } - } - // Make a second call to GetIfTable to get the actual - // data we want. - if ((dwRetVal = GetIfTable(pIfTable, &dwSize, FALSE)) != NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "GetIfTable() syscall failed"); - goto error; - } - - for (i = 0; i < (int) pIfTable->dwNumEntries; i++) { - pIfRow = (MIB_IFROW *) & pIfTable->table[i]; - - // GetIfTable is not able to give us NIC with "friendly names" - // so we determine them via GetAdapterAddresses() which - // provides friendly names *and* descriptions and find the - // ones that match. - ifname_found = 0; - pCurrAddresses = pAddresses; - while (pCurrAddresses) { - sprintf_s(descr, MAX_PATH, "%wS", pCurrAddresses->Description); - if (lstrcmp(descr, pIfRow->bDescr) == 0) { - py_nic_name = PyUnicode_FromWideChar( - pCurrAddresses->FriendlyName, - wcslen(pCurrAddresses->FriendlyName)); - if (py_nic_name == NULL) - goto error; - ifname_found = 1; - break; - } - pCurrAddresses = pCurrAddresses->Next; - } - if (ifname_found == 0) { - // Name not found means GetAdapterAddresses() doesn't list - // this NIC, only GetIfTable, meaning it's not really a NIC - // interface so we skip it. - continue; - } - - // is up? - if((pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_CONNECTED || - pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_OPERATIONAL) && - pIfRow->dwAdminStatus == 1 ) { - py_is_up = Py_True; - } - else { - py_is_up = Py_False; - } - Py_INCREF(py_is_up); - - py_ifc_info = Py_BuildValue( - "(Oikk)", - py_is_up, - 2, // there's no way to know duplex so let's assume 'full' - pIfRow->dwSpeed / 1000000, // expressed in bytes, we want Mb - pIfRow->dwMtu - ); - if (!py_ifc_info) - goto error; - if (PyDict_SetItem(py_retdict, py_nic_name, py_ifc_info)) - goto error; - Py_CLEAR(py_nic_name); - Py_CLEAR(py_ifc_info); - } - - free(pIfTable); - free(pAddresses); - return py_retdict; - -error: - Py_XDECREF(py_is_up); - Py_XDECREF(py_ifc_info); - Py_XDECREF(py_nic_name); - Py_DECREF(py_retdict); - if (pIfTable != NULL) - free(pIfTable); - if (pAddresses != NULL) - free(pAddresses); - return NULL; -} - - -/* - * Return CPU statistics. - */ -static PyObject * -psutil_cpu_stats(PyObject *self, PyObject *args) { - NTSTATUS status; - _SYSTEM_PERFORMANCE_INFORMATION *spi = NULL; - _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; - _SYSTEM_INTERRUPT_INFORMATION *InterruptInformation = NULL; - unsigned int ncpus; - UINT i; - ULONG64 dpcs = 0; - ULONG interrupts = 0; - - // retrieves number of processors - ncpus = psutil_get_num_cpus(1); - if (ncpus == 0) - goto error; - - // get syscalls / ctx switches - spi = (_SYSTEM_PERFORMANCE_INFORMATION *) \ - malloc(ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION)); - if (spi == NULL) { - PyErr_NoMemory(); - goto error; - } - status = psutil_NtQuerySystemInformation( - SystemPerformanceInformation, - spi, - ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, "NtQuerySystemInformation(SystemPerformanceInformation)"); - goto error; - } - - // get DPCs - InterruptInformation = \ - malloc(sizeof(_SYSTEM_INTERRUPT_INFORMATION) * ncpus); - if (InterruptInformation == NULL) { - PyErr_NoMemory(); - goto error; - } - - status = psutil_NtQuerySystemInformation( - SystemInterruptInformation, - InterruptInformation, - ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, "NtQuerySystemInformation(SystemInterruptInformation)"); - goto error; - } - for (i = 0; i < ncpus; i++) { - dpcs += InterruptInformation[i].DpcCount; - } - - // get interrupts - sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); - if (sppi == NULL) { - PyErr_NoMemory(); - goto error; - } - - status = psutil_NtQuerySystemInformation( - SystemProcessorPerformanceInformation, - sppi, - ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, - "NtQuerySystemInformation(SystemProcessorPerformanceInformation)"); - goto error; - } - - for (i = 0; i < ncpus; i++) { - interrupts += sppi[i].InterruptCount; - } - - // done - free(spi); - free(InterruptInformation); - free(sppi); - return Py_BuildValue( - "kkkk", - spi->ContextSwitches, - interrupts, - (unsigned long)dpcs, - spi->SystemCalls - ); - -error: - if (spi) - free(spi); - if (InterruptInformation) - free(InterruptInformation); - if (sppi) - free(sppi); - return NULL; -} - - -/* - * Return CPU frequency. - */ -static PyObject * -psutil_cpu_freq(PyObject *self, PyObject *args) { - PROCESSOR_POWER_INFORMATION *ppi; - NTSTATUS ret; - ULONG size; - LPBYTE pBuffer = NULL; - ULONG current; - ULONG max; - unsigned int ncpus; - - // Get the number of CPUs. - ncpus = psutil_get_num_cpus(1); - if (ncpus == 0) - return NULL; - - // Allocate size. - size = ncpus * sizeof(PROCESSOR_POWER_INFORMATION); - pBuffer = (BYTE*)LocalAlloc(LPTR, size); - if (! pBuffer) - return PyErr_SetFromWindowsErr(0); - - // Syscall. - ret = CallNtPowerInformation( - ProcessorInformation, NULL, 0, pBuffer, size); - if (ret != 0) { - PyErr_SetString(PyExc_RuntimeError, - "CallNtPowerInformation syscall failed"); - goto error; - } - - // Results. - ppi = (PROCESSOR_POWER_INFORMATION *)pBuffer; - max = ppi->MaxMhz; - current = ppi->CurrentMhz; - LocalFree(pBuffer); - - return Py_BuildValue("kk", current, max); - -error: - if (pBuffer != NULL) - LocalFree(pBuffer); - return NULL; -} - - /* * Return battery usage stats. */ diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c new file mode 100644 index 0000000000..e891dcc849 --- /dev/null +++ b/psutil/arch/windows/cpu.c @@ -0,0 +1,413 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "ntextapi.h" +#include "global.h" +#include "../../_psutil_common.h" + + +/* + * Return the number of logical, active CPUs. Return 0 if undetermined. + * See discussion at: https://bugs.python.org/issue33166#msg314631 + */ +static unsigned int +psutil_get_num_cpus(int fail_on_err) { + unsigned int ncpus = 0; + + // Minimum requirement: Windows 7 + if (psutil_GetActiveProcessorCount != NULL) { + ncpus = psutil_GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); + if ((ncpus == 0) && (fail_on_err == 1)) { + PyErr_SetFromWindowsErr(0); + } + } + else { + psutil_debug("GetActiveProcessorCount() not available; " + "using GetSystemInfo()"); + ncpus = (unsigned int)PSUTIL_SYSTEM_INFO.dwNumberOfProcessors; + if ((ncpus <= 0) && (fail_on_err == 1)) { + PyErr_SetString( + PyExc_RuntimeError, + "GetSystemInfo() failed to retrieve CPU count"); + } + } + return ncpus; +} + + +/* + * Retrieves system CPU timing information as a (user, system, idle) + * tuple. On a multiprocessor system, the values returned are the + * sum of the designated times across all processors. + */ +PyObject * +psutil_cpu_times(PyObject *self, PyObject *args) { + double idle, kernel, user, system; + FILETIME idle_time, kernel_time, user_time; + + if (!GetSystemTimes(&idle_time, &kernel_time, &user_time)) + return PyErr_SetFromWindowsErr(0); + + idle = (double)((HI_T * idle_time.dwHighDateTime) + \ + (LO_T * idle_time.dwLowDateTime)); + user = (double)((HI_T * user_time.dwHighDateTime) + \ + (LO_T * user_time.dwLowDateTime)); + kernel = (double)((HI_T * kernel_time.dwHighDateTime) + \ + (LO_T * kernel_time.dwLowDateTime)); + + // Kernel time includes idle time. + // We return only busy kernel time subtracting idle time from + // kernel time. + system = (kernel - idle); + return Py_BuildValue("(ddd)", user, system, idle); +} + + +/* + * Same as above but for all system CPUs. + */ +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + double idle, kernel, systemt, user, interrupt, dpc; + NTSTATUS status; + _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; + UINT i; + unsigned int ncpus; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + // retrieves number of processors + ncpus = psutil_get_num_cpus(1); + if (ncpus == 0) + goto error; + + // allocates an array of _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION + // structures, one per processor + sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ + malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + if (sppi == NULL) { + PyErr_NoMemory(); + goto error; + } + + // gets cpu time informations + status = psutil_NtQuerySystemInformation( + SystemProcessorPerformanceInformation, + sppi, + ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), + NULL); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtQuerySystemInformation(SystemProcessorPerformanceInformation)" + ); + goto error; + } + + // computes system global times summing each + // processor value + idle = user = kernel = interrupt = dpc = 0; + for (i = 0; i < ncpus; i++) { + py_tuple = NULL; + user = (double)((HI_T * sppi[i].UserTime.HighPart) + + (LO_T * sppi[i].UserTime.LowPart)); + idle = (double)((HI_T * sppi[i].IdleTime.HighPart) + + (LO_T * sppi[i].IdleTime.LowPart)); + kernel = (double)((HI_T * sppi[i].KernelTime.HighPart) + + (LO_T * sppi[i].KernelTime.LowPart)); + interrupt = (double)((HI_T * sppi[i].InterruptTime.HighPart) + + (LO_T * sppi[i].InterruptTime.LowPart)); + dpc = (double)((HI_T * sppi[i].DpcTime.HighPart) + + (LO_T * sppi[i].DpcTime.LowPart)); + + // kernel time includes idle time on windows + // we return only busy kernel time subtracting + // idle time from kernel time + systemt = kernel - idle; + py_tuple = Py_BuildValue( + "(ddddd)", + user, + systemt, + idle, + interrupt, + dpc + ); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + } + + free(sppi); + return py_retlist; + +error: + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + if (sppi) + free(sppi); + return NULL; +} + + +/* + * Return the number of active, logical CPUs. + */ +PyObject * +psutil_cpu_count_logical(PyObject *self, PyObject *args) { + unsigned int ncpus; + + ncpus = psutil_get_num_cpus(0); + if (ncpus != 0) + return Py_BuildValue("I", ncpus); + else + Py_RETURN_NONE; // mimick os.cpu_count() +} + + +/* + * Return the number of physical CPU cores (hyper-thread CPUs count + * is excluded). + */ +PyObject * +psutil_cpu_count_phys(PyObject *self, PyObject *args) { + DWORD rc; + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX buffer = NULL; + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX ptr = NULL; + DWORD length = 0; + DWORD offset = 0; + DWORD ncpus = 0; + DWORD prev_processor_info_size = 0; + + // GetLogicalProcessorInformationEx() is available from Windows 7 + // onward. Differently from GetLogicalProcessorInformation() + // it supports process groups, meaning this is able to report more + // than 64 CPUs. See: + // https://bugs.python.org/issue33166 + if (psutil_GetLogicalProcessorInformationEx == NULL) { + psutil_debug("Win < 7; cpu_count_phys() forced to None"); + Py_RETURN_NONE; + } + + while (1) { + rc = psutil_GetLogicalProcessorInformationEx( + RelationAll, buffer, &length); + if (rc == FALSE) { + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + if (buffer) { + free(buffer); + } + buffer = \ + (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)malloc(length); + if (NULL == buffer) { + PyErr_NoMemory(); + return NULL; + } + } + else { + psutil_debug("GetLogicalProcessorInformationEx() returned ", + GetLastError()); + goto return_none; + } + } + else { + break; + } + } + + ptr = buffer; + while (offset < length) { + // Advance ptr by the size of the previous + // SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX struct. + ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)\ + (((char*)ptr) + prev_processor_info_size); + + if (ptr->Relationship == RelationProcessorCore) { + ncpus += 1; + } + + // When offset == length, we've reached the last processor + // info struct in the buffer. + offset += ptr->Size; + prev_processor_info_size = ptr->Size; + } + + free(buffer); + if (ncpus != 0) { + return Py_BuildValue("I", ncpus); + } + else { + psutil_debug("GetLogicalProcessorInformationEx() count was 0"); + Py_RETURN_NONE; // mimick os.cpu_count() + } + +return_none: + if (buffer != NULL) + free(buffer); + Py_RETURN_NONE; +} + + +/* + * Return CPU statistics. + */ +PyObject * +psutil_cpu_stats(PyObject *self, PyObject *args) { + NTSTATUS status; + _SYSTEM_PERFORMANCE_INFORMATION *spi = NULL; + _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; + _SYSTEM_INTERRUPT_INFORMATION *InterruptInformation = NULL; + unsigned int ncpus; + UINT i; + ULONG64 dpcs = 0; + ULONG interrupts = 0; + + // retrieves number of processors + ncpus = psutil_get_num_cpus(1); + if (ncpus == 0) + goto error; + + // get syscalls / ctx switches + spi = (_SYSTEM_PERFORMANCE_INFORMATION *) \ + malloc(ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION)); + if (spi == NULL) { + PyErr_NoMemory(); + goto error; + } + status = psutil_NtQuerySystemInformation( + SystemPerformanceInformation, + spi, + ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), + NULL); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemPerformanceInformation)"); + goto error; + } + + // get DPCs + InterruptInformation = \ + malloc(sizeof(_SYSTEM_INTERRUPT_INFORMATION) * ncpus); + if (InterruptInformation == NULL) { + PyErr_NoMemory(); + goto error; + } + + status = psutil_NtQuerySystemInformation( + SystemInterruptInformation, + InterruptInformation, + ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), + NULL); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemInterruptInformation)"); + goto error; + } + for (i = 0; i < ncpus; i++) { + dpcs += InterruptInformation[i].DpcCount; + } + + // get interrupts + sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ + malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + if (sppi == NULL) { + PyErr_NoMemory(); + goto error; + } + + status = psutil_NtQuerySystemInformation( + SystemProcessorPerformanceInformation, + sppi, + ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), + NULL); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtQuerySystemInformation(SystemProcessorPerformanceInformation)"); + goto error; + } + + for (i = 0; i < ncpus; i++) { + interrupts += sppi[i].InterruptCount; + } + + // done + free(spi); + free(InterruptInformation); + free(sppi); + return Py_BuildValue( + "kkkk", + spi->ContextSwitches, + interrupts, + (unsigned long)dpcs, + spi->SystemCalls + ); + +error: + if (spi) + free(spi); + if (InterruptInformation) + free(InterruptInformation); + if (sppi) + free(sppi); + return NULL; +} + + +/* + * Return CPU frequency. + */ +PyObject * +psutil_cpu_freq(PyObject *self, PyObject *args) { + PROCESSOR_POWER_INFORMATION *ppi; + NTSTATUS ret; + ULONG size; + LPBYTE pBuffer = NULL; + ULONG current; + ULONG max; + unsigned int ncpus; + + // Get the number of CPUs. + ncpus = psutil_get_num_cpus(1); + if (ncpus == 0) + return NULL; + + // Allocate size. + size = ncpus * sizeof(PROCESSOR_POWER_INFORMATION); + pBuffer = (BYTE*)LocalAlloc(LPTR, size); + if (! pBuffer) + return PyErr_SetFromWindowsErr(0); + + // Syscall. + ret = CallNtPowerInformation( + ProcessorInformation, NULL, 0, pBuffer, size); + if (ret != 0) { + PyErr_SetString(PyExc_RuntimeError, + "CallNtPowerInformation syscall failed"); + goto error; + } + + // Results. + ppi = (PROCESSOR_POWER_INFORMATION *)pBuffer; + max = ppi->MaxMhz; + current = ppi->CurrentMhz; + LocalFree(pBuffer); + + return Py_BuildValue("kk", current, max); + +error: + if (pBuffer != NULL) + LocalFree(pBuffer); + return NULL; +} diff --git a/psutil/arch/windows/cpu.h b/psutil/arch/windows/cpu.h new file mode 100644 index 0000000000..d88c221210 --- /dev/null +++ b/psutil/arch/windows/cpu.h @@ -0,0 +1,14 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_phys(PyObject *self, PyObject *args); +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c new file mode 100644 index 0000000000..821f687d8c --- /dev/null +++ b/psutil/arch/windows/disk.c @@ -0,0 +1,375 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "ntextapi.h" +#include "global.h" +#include "../../_psutil_common.h" + + +#ifndef _ARRAYSIZE +#define _ARRAYSIZE(a) (sizeof(a)/sizeof(a[0])) +#endif + +static char *psutil_get_drive_type(int type) { + switch (type) { + case DRIVE_FIXED: + return "fixed"; + case DRIVE_CDROM: + return "cdrom"; + case DRIVE_REMOVABLE: + return "removable"; + case DRIVE_UNKNOWN: + return "unknown"; + case DRIVE_NO_ROOT_DIR: + return "unmounted"; + case DRIVE_REMOTE: + return "remote"; + case DRIVE_RAMDISK: + return "ramdisk"; + default: + return "?"; + } +} + + +/* + * Return path's disk total and free as a Python tuple. + */ +PyObject * +psutil_disk_usage(PyObject *self, PyObject *args) { + BOOL retval; + ULARGE_INTEGER _, total, free; + char *path; + + if (PyArg_ParseTuple(args, "u", &path)) { + Py_BEGIN_ALLOW_THREADS + retval = GetDiskFreeSpaceExW((LPCWSTR)path, &_, &total, &free); + Py_END_ALLOW_THREADS + goto return_; + } + + // on Python 2 we also want to accept plain strings other + // than Unicode +#if PY_MAJOR_VERSION <= 2 + PyErr_Clear(); // drop the argument parsing error + if (PyArg_ParseTuple(args, "s", &path)) { + Py_BEGIN_ALLOW_THREADS + retval = GetDiskFreeSpaceEx(path, &_, &total, &free); + Py_END_ALLOW_THREADS + goto return_; + } +#endif + + return NULL; + +return_: + if (retval == 0) + return PyErr_SetFromWindowsErrWithFilename(0, path); + else + return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); +} + + +/* + * Return a Python dict of tuples for disk I/O information. This may + * require running "diskperf -y" command first. + */ +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + DISK_PERFORMANCE diskPerformance; + DWORD dwSize; + HANDLE hDevice = NULL; + char szDevice[MAX_PATH]; + char szDeviceDisplay[MAX_PATH]; + int devNum; + int i; + DWORD ioctrlSize; + BOOL ret; + PyObject *py_retdict = PyDict_New(); + PyObject *py_tuple = NULL; + + if (py_retdict == NULL) + return NULL; + // Apparently there's no way to figure out how many times we have + // to iterate in order to find valid drives. + // Let's assume 32, which is higher than 26, the number of letters + // in the alphabet (from A:\ to Z:\). + for (devNum = 0; devNum <= 32; ++devNum) { + py_tuple = NULL; + sprintf_s(szDevice, MAX_PATH, "\\\\.\\PhysicalDrive%d", devNum); + hDevice = CreateFile(szDevice, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, 0, NULL); + if (hDevice == INVALID_HANDLE_VALUE) + continue; + + // DeviceIoControl() sucks! + i = 0; + ioctrlSize = sizeof(diskPerformance); + while (1) { + i += 1; + ret = DeviceIoControl( + hDevice, IOCTL_DISK_PERFORMANCE, NULL, 0, &diskPerformance, + ioctrlSize, &dwSize, NULL); + if (ret != 0) + break; // OK! + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + // Retry with a bigger buffer (+ limit for retries). + if (i <= 1024) { + ioctrlSize *= 2; + continue; + } + } + else if (GetLastError() == ERROR_INVALID_FUNCTION) { + // This happens on AppVeyor: + // https://ci.appveyor.com/project/giampaolo/psutil/build/ + // 1364/job/ascpdi271b06jle3 + // Assume it means we're dealing with some exotic disk + // and go on. + psutil_debug("DeviceIoControl -> ERROR_INVALID_FUNCTION; " + "ignore PhysicalDrive%i", devNum); + goto next; + } + else if (GetLastError() == ERROR_NOT_SUPPORTED) { + // Again, let's assume we're dealing with some exotic disk. + psutil_debug("DeviceIoControl -> ERROR_NOT_SUPPORTED; " + "ignore PhysicalDrive%i", devNum); + goto next; + } + // XXX: it seems we should also catch ERROR_INVALID_PARAMETER: + // https://sites.ualberta.ca/dept/aict/uts/software/openbsd/ + // ports/4.1/i386/openafs/w-openafs-1.4.14-transarc/ + // openafs-1.4.14/src/usd/usd_nt.c + + // XXX: we can also bump into ERROR_MORE_DATA in which case + // (quoting doc) we're supposed to retry with a bigger buffer + // and specify a new "starting point", whatever it means. + PyErr_SetFromWindowsErr(0); + goto error; + } + + sprintf_s(szDeviceDisplay, MAX_PATH, "PhysicalDrive%i", devNum); + py_tuple = Py_BuildValue( + "(IILLKK)", + diskPerformance.ReadCount, + diskPerformance.WriteCount, + diskPerformance.BytesRead, + diskPerformance.BytesWritten, + // convert to ms: + // https://github.com/giampaolo/psutil/issues/1012 + (unsigned long long) + (diskPerformance.ReadTime.QuadPart) / 10000000, + (unsigned long long) + (diskPerformance.WriteTime.QuadPart) / 10000000); + if (!py_tuple) + goto error; + if (PyDict_SetItemString(py_retdict, szDeviceDisplay, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + +next: + CloseHandle(hDevice); + } + + return py_retdict; + +error: + Py_XDECREF(py_tuple); + Py_DECREF(py_retdict); + if (hDevice != NULL) + CloseHandle(hDevice); + return NULL; +} + + +/* + * Return disk partitions as a list of tuples such as + * (drive_letter, drive_letter, type, "") + */ +PyObject * +psutil_disk_partitions(PyObject *self, PyObject *args) { + DWORD num_bytes; + char drive_strings[255]; + char *drive_letter = drive_strings; + char mp_buf[MAX_PATH]; + char mp_path[MAX_PATH]; + int all; + int type; + int ret; + unsigned int old_mode = 0; + char opts[20]; + HANDLE mp_h; + BOOL mp_flag= TRUE; + LPTSTR fs_type[MAX_PATH + 1] = { 0 }; + DWORD pflags = 0; + PyObject *py_all; + PyObject *py_retlist = PyList_New(0); + PyObject *py_tuple = NULL; + + if (py_retlist == NULL) { + return NULL; + } + + // avoid to visualize a message box in case something goes wrong + // see https://github.com/giampaolo/psutil/issues/264 + old_mode = SetErrorMode(SEM_FAILCRITICALERRORS); + + if (! PyArg_ParseTuple(args, "O", &py_all)) + goto error; + all = PyObject_IsTrue(py_all); + + Py_BEGIN_ALLOW_THREADS + num_bytes = GetLogicalDriveStrings(254, drive_letter); + Py_END_ALLOW_THREADS + + if (num_bytes == 0) { + PyErr_SetFromWindowsErr(0); + goto error; + } + + while (*drive_letter != 0) { + py_tuple = NULL; + opts[0] = 0; + fs_type[0] = 0; + + Py_BEGIN_ALLOW_THREADS + type = GetDriveType(drive_letter); + Py_END_ALLOW_THREADS + + // by default we only show hard drives and cd-roms + if (all == 0) { + if ((type == DRIVE_UNKNOWN) || + (type == DRIVE_NO_ROOT_DIR) || + (type == DRIVE_REMOTE) || + (type == DRIVE_RAMDISK)) { + goto next; + } + // floppy disk: skip it by default as it introduces a + // considerable slowdown. + if ((type == DRIVE_REMOVABLE) && + (strcmp(drive_letter, "A:\\") == 0)) { + goto next; + } + } + + ret = GetVolumeInformation( + (LPCTSTR)drive_letter, NULL, _ARRAYSIZE(drive_letter), + NULL, NULL, &pflags, (LPTSTR)fs_type, _ARRAYSIZE(fs_type)); + if (ret == 0) { + // We might get here in case of a floppy hard drive, in + // which case the error is (21, "device not ready"). + // Let's pretend it didn't happen as we already have + // the drive name and type ('removable'). + strcat_s(opts, _countof(opts), ""); + SetLastError(0); + } + else { + if (pflags & FILE_READ_ONLY_VOLUME) + strcat_s(opts, _countof(opts), "ro"); + else + strcat_s(opts, _countof(opts), "rw"); + if (pflags & FILE_VOLUME_IS_COMPRESSED) + strcat_s(opts, _countof(opts), ",compressed"); + + // Check for mount points on this volume and add/get info + // (checks first to know if we can even have mount points) + if (pflags & FILE_SUPPORTS_REPARSE_POINTS) { + mp_h = FindFirstVolumeMountPoint( + drive_letter, mp_buf, MAX_PATH); + if (mp_h != INVALID_HANDLE_VALUE) { + while (mp_flag) { + + // Append full mount path with drive letter + strcpy_s(mp_path, _countof(mp_path), drive_letter); + strcat_s(mp_path, _countof(mp_path), mp_buf); + + py_tuple = Py_BuildValue( + "(ssss)", + drive_letter, + mp_path, + fs_type, // Typically NTFS + opts); + + if (!py_tuple || + PyList_Append(py_retlist, py_tuple) == -1) { + FindVolumeMountPointClose(mp_h); + goto error; + } + + Py_CLEAR(py_tuple); + + // Continue looking for more mount points + mp_flag = FindNextVolumeMountPoint( + mp_h, mp_buf, MAX_PATH); + } + FindVolumeMountPointClose(mp_h); + } + + } + } + + if (strlen(opts) > 0) + strcat_s(opts, _countof(opts), ","); + strcat_s(opts, _countof(opts), psutil_get_drive_type(type)); + + py_tuple = Py_BuildValue( + "(ssss)", + drive_letter, + drive_letter, + fs_type, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS + opts); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + goto next; + +next: + drive_letter = strchr(drive_letter, 0) + 1; + } + + SetErrorMode(old_mode); + return py_retlist; + +error: + SetErrorMode(old_mode); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + return NULL; +} + + +/* + Accept a filename's drive in native format like "\Device\HarddiskVolume1\" + and return the corresponding drive letter (e.g. "C:\\"). + If no match is found return an empty string. +*/ +PyObject * +psutil_win32_QueryDosDevice(PyObject *self, PyObject *args) { + LPCTSTR lpDevicePath; + TCHAR d = TEXT('A'); + TCHAR szBuff[5]; + + if (!PyArg_ParseTuple(args, "s", &lpDevicePath)) + return NULL; + + while (d <= TEXT('Z')) { + TCHAR szDeviceName[3] = {d, TEXT(':'), TEXT('\0')}; + TCHAR szTarget[512] = {0}; + if (QueryDosDevice(szDeviceName, szTarget, 511) != 0) { + if (_tcscmp(lpDevicePath, szTarget) == 0) { + _stprintf_s(szBuff, _countof(szBuff), TEXT("%c:"), d); + return Py_BuildValue("s", szBuff); + } + } + d++; + } + return Py_BuildValue("s", ""); +} diff --git a/psutil/arch/windows/disk.h b/psutil/arch/windows/disk.h new file mode 100644 index 0000000000..298fb6ba0e --- /dev/null +++ b/psutil/arch/windows/disk.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_disk_usage(PyObject *self, PyObject *args); +PyObject *psutil_win32_QueryDosDevice(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 4cf77cdeb6..18d9a640ee 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -18,6 +18,9 @@ extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; #define PSUTIL_WINDOWS_10 100 #define PSUTIL_WINDOWS_NEW MAXLONG +#define LO_T 1e-7 +#define HI_T 429.4967296 + int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c new file mode 100644 index 0000000000..565e844c3f --- /dev/null +++ b/psutil/arch/windows/net.c @@ -0,0 +1,449 @@ +/* + * 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. + */ + +// Fixes clash between winsock2.h and windows.h +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include + +#include "ntextapi.h" +#include "global.h" + + +#ifndef AF_INET6 +#define AF_INET6 23 +#endif + + +static PIP_ADAPTER_ADDRESSES +psutil_get_nic_addresses() { + // allocate a 15 KB buffer to start with + int outBufLen = 15000; + DWORD dwRetVal = 0; + ULONG attempts = 0; + PIP_ADAPTER_ADDRESSES pAddresses = NULL; + + do { + pAddresses = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen); + if (pAddresses == NULL) { + PyErr_NoMemory(); + return NULL; + } + + dwRetVal = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, pAddresses, + &outBufLen); + if (dwRetVal == ERROR_BUFFER_OVERFLOW) { + free(pAddresses); + pAddresses = NULL; + } + else { + break; + } + + attempts++; + } while ((dwRetVal == ERROR_BUFFER_OVERFLOW) && (attempts < 3)); + + if (dwRetVal != NO_ERROR) { + PyErr_SetString( + PyExc_RuntimeError, "GetAdaptersAddresses() syscall failed."); + return NULL; + } + + return pAddresses; +} + + +/* + * Return a Python list of named tuples with overall network I/O information + */ +PyObject * +psutil_net_io_counters(PyObject *self, PyObject *args) { + DWORD dwRetVal = 0; + MIB_IF_ROW2 *pIfRow = NULL; + PIP_ADAPTER_ADDRESSES pAddresses = NULL; + PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; + PyObject *py_retdict = PyDict_New(); + PyObject *py_nic_info = NULL; + PyObject *py_nic_name = NULL; + + if (py_retdict == NULL) + return NULL; + pAddresses = psutil_get_nic_addresses(); + if (pAddresses == NULL) + goto error; + pCurrAddresses = pAddresses; + + while (pCurrAddresses) { + py_nic_name = NULL; + py_nic_info = NULL; + + pIfRow = (MIB_IF_ROW2 *) malloc(sizeof(MIB_IF_ROW2)); + if (pIfRow == NULL) { + PyErr_NoMemory(); + goto error; + } + + SecureZeroMemory((PVOID)pIfRow, sizeof(MIB_IF_ROW2)); + pIfRow->InterfaceIndex = pCurrAddresses->IfIndex; + dwRetVal = GetIfEntry2(pIfRow); + if (dwRetVal != NO_ERROR) { + PyErr_SetString(PyExc_RuntimeError, + "GetIfEntry() or GetIfEntry2() syscalls failed."); + goto error; + } + + py_nic_info = Py_BuildValue( + "(KKKKKKKK)", + pIfRow->OutOctets, + pIfRow->InOctets, + (pIfRow->OutUcastPkts + pIfRow->OutNUcastPkts), + (pIfRow->InUcastPkts + pIfRow->InNUcastPkts), + pIfRow->InErrors, + pIfRow->OutErrors, + pIfRow->InDiscards, + pIfRow->OutDiscards); + if (!py_nic_info) + goto error; + + py_nic_name = PyUnicode_FromWideChar( + pCurrAddresses->FriendlyName, + wcslen(pCurrAddresses->FriendlyName)); + + if (py_nic_name == NULL) + goto error; + if (PyDict_SetItem(py_retdict, py_nic_name, py_nic_info)) + goto error; + Py_CLEAR(py_nic_name); + Py_CLEAR(py_nic_info); + + free(pIfRow); + pCurrAddresses = pCurrAddresses->Next; + } + + free(pAddresses); + return py_retdict; + +error: + Py_XDECREF(py_nic_name); + Py_XDECREF(py_nic_info); + Py_DECREF(py_retdict); + if (pAddresses != NULL) + free(pAddresses); + if (pIfRow != NULL) + free(pIfRow); + return NULL; +} + + +/* + * Return NICs addresses. + */ +PyObject * +psutil_net_if_addrs(PyObject *self, PyObject *args) { + unsigned int i = 0; + ULONG family; + PCTSTR intRet; + PCTSTR netmaskIntRet; + char *ptr; + char buff_addr[1024]; + char buff_macaddr[1024]; + char buff_netmask[1024]; + DWORD dwRetVal = 0; + ULONG converted_netmask; + UINT netmask_bits; + struct in_addr in_netmask; + PIP_ADAPTER_ADDRESSES pAddresses = NULL; + PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; + PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL; + + PyObject *py_retlist = PyList_New(0); + PyObject *py_tuple = NULL; + PyObject *py_address = NULL; + PyObject *py_mac_address = NULL; + PyObject *py_nic_name = NULL; + PyObject *py_netmask = NULL; + + if (py_retlist == NULL) + return NULL; + + pAddresses = psutil_get_nic_addresses(); + if (pAddresses == NULL) + goto error; + pCurrAddresses = pAddresses; + + while (pCurrAddresses) { + pUnicast = pCurrAddresses->FirstUnicastAddress; + + netmaskIntRet = NULL; + py_nic_name = NULL; + py_nic_name = PyUnicode_FromWideChar( + pCurrAddresses->FriendlyName, + wcslen(pCurrAddresses->FriendlyName)); + if (py_nic_name == NULL) + goto error; + + // MAC address + if (pCurrAddresses->PhysicalAddressLength != 0) { + ptr = buff_macaddr; + *ptr = '\0'; + for (i = 0; i < (int) pCurrAddresses->PhysicalAddressLength; i++) { + if (i == (pCurrAddresses->PhysicalAddressLength - 1)) { + sprintf_s(ptr, _countof(buff_macaddr), "%.2X\n", + (int)pCurrAddresses->PhysicalAddress[i]); + } + else { + sprintf_s(ptr, _countof(buff_macaddr), "%.2X-", + (int)pCurrAddresses->PhysicalAddress[i]); + } + ptr += 3; + } + *--ptr = '\0'; + + py_mac_address = Py_BuildValue("s", buff_macaddr); + if (py_mac_address == NULL) + goto error; + + Py_INCREF(Py_None); + Py_INCREF(Py_None); + Py_INCREF(Py_None); + py_tuple = Py_BuildValue( + "(OiOOOO)", + py_nic_name, + -1, // this will be converted later to AF_LINK + py_mac_address, + Py_None, // netmask (not supported) + Py_None, // broadcast (not supported) + Py_None // ptp (not supported on Windows) + ); + if (! py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + Py_CLEAR(py_mac_address); + } + + // find out the IP address associated with the NIC + if (pUnicast != NULL) { + for (i = 0; pUnicast != NULL; i++) { + family = pUnicast->Address.lpSockaddr->sa_family; + if (family == AF_INET) { + struct sockaddr_in *sa_in = (struct sockaddr_in *) + pUnicast->Address.lpSockaddr; + intRet = inet_ntop(AF_INET, &(sa_in->sin_addr), buff_addr, + sizeof(buff_addr)); + if (!intRet) + goto error; + netmask_bits = pUnicast->OnLinkPrefixLength; + dwRetVal = ConvertLengthToIpv4Mask( + netmask_bits, &converted_netmask); + if (dwRetVal == NO_ERROR) { + in_netmask.s_addr = converted_netmask; + netmaskIntRet = inet_ntop( + AF_INET, &in_netmask, buff_netmask, + sizeof(buff_netmask)); + if (!netmaskIntRet) + goto error; + } + } + else if (family == AF_INET6) { + struct sockaddr_in6 *sa_in6 = (struct sockaddr_in6 *) + pUnicast->Address.lpSockaddr; + intRet = inet_ntop(AF_INET6, &(sa_in6->sin6_addr), + buff_addr, sizeof(buff_addr)); + if (!intRet) + goto error; + } + else { + // we should never get here + pUnicast = pUnicast->Next; + continue; + } + +#if PY_MAJOR_VERSION >= 3 + py_address = PyUnicode_FromString(buff_addr); +#else + py_address = PyString_FromString(buff_addr); +#endif + if (py_address == NULL) + goto error; + + if (netmaskIntRet != NULL) { +#if PY_MAJOR_VERSION >= 3 + py_netmask = PyUnicode_FromString(buff_netmask); +#else + py_netmask = PyString_FromString(buff_netmask); +#endif + } else { + Py_INCREF(Py_None); + py_netmask = Py_None; + } + + Py_INCREF(Py_None); + Py_INCREF(Py_None); + py_tuple = Py_BuildValue( + "(OiOOOO)", + py_nic_name, + family, + py_address, + py_netmask, + Py_None, // broadcast (not supported) + Py_None // ptp (not supported on Windows) + ); + + if (! py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + Py_CLEAR(py_address); + Py_CLEAR(py_netmask); + + pUnicast = pUnicast->Next; + } + } + Py_CLEAR(py_nic_name); + pCurrAddresses = pCurrAddresses->Next; + } + + free(pAddresses); + return py_retlist; + +error: + if (pAddresses) + free(pAddresses); + Py_DECREF(py_retlist); + Py_XDECREF(py_tuple); + Py_XDECREF(py_address); + Py_XDECREF(py_nic_name); + Py_XDECREF(py_netmask); + return NULL; +} + + +/* + * Provides stats about NIC interfaces installed on the system. + * TODO: get 'duplex' (currently it's hard coded to '2', aka + 'full duplex') + */ +PyObject * +psutil_net_if_stats(PyObject *self, PyObject *args) { + int i; + DWORD dwSize = 0; + DWORD dwRetVal = 0; + MIB_IFTABLE *pIfTable; + MIB_IFROW *pIfRow; + PIP_ADAPTER_ADDRESSES pAddresses = NULL; + PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; + char descr[MAX_PATH]; + int ifname_found; + + PyObject *py_nic_name = NULL; + PyObject *py_retdict = PyDict_New(); + PyObject *py_ifc_info = NULL; + PyObject *py_is_up = NULL; + + if (py_retdict == NULL) + return NULL; + + pAddresses = psutil_get_nic_addresses(); + if (pAddresses == NULL) + goto error; + + pIfTable = (MIB_IFTABLE *) malloc(sizeof (MIB_IFTABLE)); + if (pIfTable == NULL) { + PyErr_NoMemory(); + goto error; + } + dwSize = sizeof(MIB_IFTABLE); + if (GetIfTable(pIfTable, &dwSize, FALSE) == ERROR_INSUFFICIENT_BUFFER) { + free(pIfTable); + pIfTable = (MIB_IFTABLE *) malloc(dwSize); + if (pIfTable == NULL) { + PyErr_NoMemory(); + goto error; + } + } + // Make a second call to GetIfTable to get the actual + // data we want. + if ((dwRetVal = GetIfTable(pIfTable, &dwSize, FALSE)) != NO_ERROR) { + PyErr_SetString(PyExc_RuntimeError, "GetIfTable() syscall failed"); + goto error; + } + + for (i = 0; i < (int) pIfTable->dwNumEntries; i++) { + pIfRow = (MIB_IFROW *) & pIfTable->table[i]; + + // GetIfTable is not able to give us NIC with "friendly names" + // so we determine them via GetAdapterAddresses() which + // provides friendly names *and* descriptions and find the + // ones that match. + ifname_found = 0; + pCurrAddresses = pAddresses; + while (pCurrAddresses) { + sprintf_s(descr, MAX_PATH, "%wS", pCurrAddresses->Description); + if (lstrcmp(descr, pIfRow->bDescr) == 0) { + py_nic_name = PyUnicode_FromWideChar( + pCurrAddresses->FriendlyName, + wcslen(pCurrAddresses->FriendlyName)); + if (py_nic_name == NULL) + goto error; + ifname_found = 1; + break; + } + pCurrAddresses = pCurrAddresses->Next; + } + if (ifname_found == 0) { + // Name not found means GetAdapterAddresses() doesn't list + // this NIC, only GetIfTable, meaning it's not really a NIC + // interface so we skip it. + continue; + } + + // is up? + if((pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_CONNECTED || + pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_OPERATIONAL) && + pIfRow->dwAdminStatus == 1 ) { + py_is_up = Py_True; + } + else { + py_is_up = Py_False; + } + Py_INCREF(py_is_up); + + py_ifc_info = Py_BuildValue( + "(Oikk)", + py_is_up, + 2, // there's no way to know duplex so let's assume 'full' + pIfRow->dwSpeed / 1000000, // expressed in bytes, we want Mb + pIfRow->dwMtu + ); + if (!py_ifc_info) + goto error; + if (PyDict_SetItem(py_retdict, py_nic_name, py_ifc_info)) + goto error; + Py_CLEAR(py_nic_name); + Py_CLEAR(py_ifc_info); + } + + free(pIfTable); + free(pAddresses); + return py_retdict; + +error: + Py_XDECREF(py_is_up); + Py_XDECREF(py_ifc_info); + Py_XDECREF(py_nic_name); + Py_DECREF(py_retdict); + if (pIfTable != NULL) + free(pIfTable); + if (pAddresses != NULL) + free(pAddresses); + return NULL; +} diff --git a/psutil/arch/windows/net.h b/psutil/arch/windows/net.h new file mode 100644 index 0000000000..7a6158d13b --- /dev/null +++ b/psutil/arch/windows/net.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); +PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index b6f23d9967..6fa38edb10 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -10,11 +10,15 @@ typedef LONG NTSTATUS; -#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004 -#define STATUS_BUFFER_TOO_SMALL 0xC0000023L +// https://github.com/ajkhoury/TestDll/blob/master/nt_ddk.h +#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) +#define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023L) +#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L) +#define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225L) +#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) + #define SystemExtendedHandleInformation 64 #define MemoryWorkingSetInformation 0x1 -#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L) /* * ================================================================ diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 40b209ee1f..04b30f11ae 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -6,14 +6,12 @@ */ #include -#include +#include // GetMappedFileName() #include #include "ntextapi.h" #include "global.h" -#include "process_handles.h" #include "process_utils.h" -#include "../../_psutil_common.h" CRITICAL_SECTION g_cs; BOOL g_initialized = FALSE; diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index a9c2c1316e..757cabbeb0 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -9,17 +9,21 @@ #include #include -#include -#include #include "ntextapi.h" #include "global.h" -#include "security.h" -#include "process_info.h" #include "process_utils.h" #include "../../_psutil_common.h" +#define PSUTIL_FIRST_PROCESS(Processes) ( \ + (PSYSTEM_PROCESS_INFORMATION)(Processes)) +#define PSUTIL_NEXT_PROCESS(Process) ( \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ + (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) + + // ==================================================================== // Helper structures to access the memory correctly. // Some of these might also be defined in the winternl.h header file @@ -124,21 +128,11 @@ typedef struct { #endif -#define PSUTIL_FIRST_PROCESS(Processes) ( \ - (PSYSTEM_PROCESS_INFORMATION)(Processes)) -#define PSUTIL_NEXT_PROCESS(Process) ( \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ - (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) - - // ==================================================================== -// Process and PIDs utiilties. +// Process / PEB functions. // ==================================================================== -#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) - /* Given a pointer into a process's memory, figure out how much data can be * read from it. */ static int diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index afbbb72d55..110d01df4d 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -4,21 +4,10 @@ * found in the LICENSE file. */ -#if !defined(__PROCESS_INFO_H) -#define __PROCESS_INFO_H - #include -#include -#include "security.h" -#include "ntextapi.h" - -#define HANDLE_TO_PYNUM(handle) PyLong_FromUnsignedLong((unsigned long) handle) -#define PYNUM_TO_HANDLE(obj) ((HANDLE)PyLong_AsUnsignedLong(obj)) int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); PyObject* psutil_get_cmdline(long pid, int use_peb); PyObject* psutil_get_cwd(long pid); PyObject* psutil_get_environ(long pid); - -#endif diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index a81a32531e..1a721ba535 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -8,8 +8,7 @@ #include #include -#include -#include +#include // EnumProcesses #include "ntextapi.h" #include "global.h" diff --git a/psutil/arch/windows/services.h b/psutil/arch/windows/services.h index 286ed232c9..ebcfa5ef59 100644 --- a/psutil/arch/windows/services.h +++ b/psutil/arch/windows/services.h @@ -8,7 +8,7 @@ #include SC_HANDLE psutil_get_service_handle( -char service_name, DWORD scm_access, DWORD access); + char service_name, DWORD scm_access, DWORD access); PyObject *psutil_winservice_enumerate(PyObject *self, PyObject *args); PyObject *psutil_winservice_query_config(PyObject *self, PyObject *args); PyObject *psutil_winservice_query_status(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index b44a247b75..5bf34151c7 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -9,9 +9,7 @@ #include #include -#if (_WIN32_WINNT >= 0x0600) // Windows >= Vista #include -#endif #include "ntextapi.h" #include "global.h" diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index f43d790c03..b790c08e98 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -10,8 +10,6 @@ #include #include -#include "../../_psutil_common.h" - // We use an exponentially weighted moving average, just like Unix systems do // https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation @@ -104,7 +102,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { /* - * Gets the emulated 1 minute, 5 minute and 15 minute load averages + * Gets the emulated 1 minute, 5 minute and 15 minute load averages * (processor queue length) for the system. * `init_loadavg_counter` must be called before this function to engage the * mechanism that records load values. diff --git a/psutil/arch/windows/wmi.h b/psutil/arch/windows/wmi.h index 0210f2d699..311242a393 100644 --- a/psutil/arch/windows/wmi.h +++ b/psutil/arch/windows/wmi.h @@ -1,11 +1,9 @@ /* - * Copyright (c) 2009, Jay Loden, Giampaolo Rodola'. All rights reserved. + * 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. */ - - #include PyObject* psutil_init_loadavg_counter(); diff --git a/setup.py b/setup.py index 1ebad30ad9..51e9516d31 100755 --- a/setup.py +++ b/setup.py @@ -145,6 +145,9 @@ def get_winver(): 'psutil/arch/windows/process_utils.c', 'psutil/arch/windows/process_info.c', 'psutil/arch/windows/process_handles.c', + 'psutil/arch/windows/disk.c', + 'psutil/arch/windows/net.c', + 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/security.c', 'psutil/arch/windows/inet_ntop.c', 'psutil/arch/windows/services.c', From ad620104e778642e8a1fa5a5d725283baf376b55 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Jan 2020 15:40:51 -0800 Subject: [PATCH 0410/1714] #1652: remove inet_ntop.c --- psutil/_psutil_windows.c | 1 - psutil/arch/windows/inet_ntop.c | 45 --------------------------------- psutil/arch/windows/inet_ntop.h | 17 ------------- setup.py | 1 - 4 files changed, 64 deletions(-) delete mode 100644 psutil/arch/windows/inet_ntop.c delete mode 100644 psutil/arch/windows/inet_ntop.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 9c430c5f41..ec8b0b2a24 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -36,7 +36,6 @@ #include "arch/windows/disk.h" #include "arch/windows/cpu.h" #include "arch/windows/net.h" -#include "arch/windows/inet_ntop.h" #include "arch/windows/services.h" #include "arch/windows/socks.h" #include "arch/windows/wmi.h" diff --git a/psutil/arch/windows/inet_ntop.c b/psutil/arch/windows/inet_ntop.c deleted file mode 100644 index 3db507b9ba..0000000000 --- a/psutil/arch/windows/inet_ntop.c +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Jeff Tang. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include -#include "inet_ntop.h" - -// From: https://memset.wordpress.com/2010/10/09/inet_ntop-for-win32/ -PCSTR WSAAPI -inet_ntop(INT family, PVOID pAddr, PSTR stringBuf, size_t strBufSize) { - DWORD dwAddressLength = 0; - struct sockaddr_storage srcaddr; - struct sockaddr_in *srcaddr4 = (struct sockaddr_in*) &srcaddr; - struct sockaddr_in6 *srcaddr6 = (struct sockaddr_in6*) &srcaddr; - - memset(&srcaddr, 0, sizeof(struct sockaddr_storage)); - srcaddr.ss_family = family; - - if (family == AF_INET) { - dwAddressLength = sizeof(struct sockaddr_in); - memcpy(&(srcaddr4->sin_addr), pAddr, sizeof(struct in_addr)); - } - else if (family == AF_INET6) { - dwAddressLength = sizeof(struct sockaddr_in6); - memcpy(&(srcaddr6->sin6_addr), pAddr, sizeof(struct in6_addr)); - } - else { - PyErr_SetString(PyExc_ValueError, "invalid family"); - return NULL; - } - - if (WSAAddressToStringA( - (LPSOCKADDR) &srcaddr, - dwAddressLength, - 0, - stringBuf, - (LPDWORD) &strBufSize) != 0) - { - PyErr_SetExcFromWindowsErr(PyExc_OSError, WSAGetLastError()); - return NULL; - } - return stringBuf; -} diff --git a/psutil/arch/windows/inet_ntop.h b/psutil/arch/windows/inet_ntop.h deleted file mode 100644 index 2d86c26cf1..0000000000 --- a/psutil/arch/windows/inet_ntop.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Jeff Tang. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -// because of WSAAddressToStringA -#define _WINSOCK_DEPRECATED_NO_WARNINGS -#include - -PCSTR WSAAPI -inet_ntop( - __in INT Family, - __in const VOID * pAddr, - __out_ecount(StringBufSize) PSTR pStringBuf, - __in size_t StringBufSize -); diff --git a/setup.py b/setup.py index 51e9516d31..9e63db68d4 100755 --- a/setup.py +++ b/setup.py @@ -149,7 +149,6 @@ def get_winver(): 'psutil/arch/windows/net.c', 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/security.c', - 'psutil/arch/windows/inet_ntop.c', 'psutil/arch/windows/services.c', 'psutil/arch/windows/global.c', 'psutil/arch/windows/socks.c', From 004f330aedaad5739457322487cdab0323ba7af1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Jan 2020 17:25:19 -0800 Subject: [PATCH 0411/1714] winmake / uninstall: remove installation path from easy-install.pth file --- scripts/internal/winmake.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index ce6b5a8347..2ddc170703 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -350,6 +350,24 @@ def uninstall(): for name in os.listdir(dir): if name.startswith('psutil'): rm(os.path.join(dir, name)) + elif name == 'easy-install.pth': + # easy_install can add a line (installation path) into + # easy-install.pth; that line alters sys.path. + path = os.path.join(dir, name) + with open(path, 'rt') as f: + lines = f.readlines() + hasit = False + for line in lines: + if 'psutil' in line: + hasit = True + break + if hasit: + with open(path, 'wt') as f: + for line in lines: + if 'psutil' not in line: + f.write(line) + else: + print("removed line %r from %r" % (line, path)) @cmd From 9083ea9ad5f6b72eccd94faffcd12ae287b93cce Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 03:00:57 +0100 Subject: [PATCH 0412/1714] move AF_INET6 def in global.h --- psutil/arch/windows/global.h | 5 +++++ psutil/arch/windows/net.c | 5 ----- psutil/arch/windows/socks.c | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 18d9a640ee..2773f17aab 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -11,6 +11,7 @@ extern int PSUTIL_WINVER; extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; + #define PSUTIL_WINDOWS_VISTA 60 #define PSUTIL_WINDOWS_7 61 #define PSUTIL_WINDOWS_8 62 @@ -21,6 +22,10 @@ extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; #define LO_T 1e-7 #define HI_T 429.4967296 +#ifndef AF_INET6 +#define AF_INET6 23 +#endif + int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 565e844c3f..550088801c 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -16,11 +16,6 @@ #include "global.h" -#ifndef AF_INET6 -#define AF_INET6 23 -#endif - - static PIP_ADAPTER_ADDRESSES psutil_get_nic_addresses() { // allocate a 15 KB buffer to start with diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 5bf34151c7..854ca9550c 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -19,10 +19,6 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) -#ifndef AF_INET6 -#define AF_INET6 23 -#endif - // https://msdn.microsoft.com/library/aa365928.aspx // TODO properly handle return code From 2559600d57c06c133f6b48c4907823c3bc8378aa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 03:15:32 +0100 Subject: [PATCH 0413/1714] use HeapAlloc() instead of malloc() around GetAdaptersAddresses --- psutil/arch/windows/global.h | 2 ++ psutil/arch/windows/net.c | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index 2773f17aab..6764dde057 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -19,6 +19,8 @@ extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; #define PSUTIL_WINDOWS_10 100 #define PSUTIL_WINDOWS_NEW MAXLONG +#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) +#define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) #define LO_T 1e-7 #define HI_T 429.4967296 diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 550088801c..92887f42b2 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -19,13 +19,13 @@ static PIP_ADAPTER_ADDRESSES psutil_get_nic_addresses() { // allocate a 15 KB buffer to start with - int outBufLen = 15000; + ULONG outBufLen = 15000; DWORD dwRetVal = 0; ULONG attempts = 0; PIP_ADAPTER_ADDRESSES pAddresses = NULL; do { - pAddresses = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen); + pAddresses = (IP_ADAPTER_ADDRESSES *) MALLOC(outBufLen); if (pAddresses == NULL) { PyErr_NoMemory(); return NULL; @@ -34,7 +34,7 @@ psutil_get_nic_addresses() { dwRetVal = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, pAddresses, &outBufLen); if (dwRetVal == ERROR_BUFFER_OVERFLOW) { - free(pAddresses); + FREE(pAddresses); pAddresses = NULL; } else { From 1544ef413ed36a15757a73c04c4b440d04246bfc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 18:30:31 +0100 Subject: [PATCH 0414/1714] rename global.c -> globals.c --- MANIFEST.in | 16 ++++++++++++---- psutil/_psutil_windows.c | 2 +- psutil/arch/windows/cpu.c | 2 +- psutil/arch/windows/disk.c | 2 +- psutil/arch/windows/{global.c => globals.c} | 2 +- psutil/arch/windows/{global.h => globals.h} | 0 psutil/arch/windows/net.c | 2 +- psutil/arch/windows/process_handles.c | 2 +- psutil/arch/windows/process_info.c | 2 +- psutil/arch/windows/process_utils.c | 2 +- psutil/arch/windows/socks.c | 2 +- setup.py | 2 +- 12 files changed, 22 insertions(+), 14 deletions(-) rename psutil/arch/windows/{global.c => globals.c} (99%) rename psutil/arch/windows/{global.h => globals.h} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index d26c2d74a6..75ad5f15d9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -64,19 +64,27 @@ include psutil/arch/solaris/environ.c include psutil/arch/solaris/environ.h include psutil/arch/solaris/v10/ifaddrs.c include psutil/arch/solaris/v10/ifaddrs.h -include psutil/arch/windows/global.c -include psutil/arch/windows/global.h -include psutil/arch/windows/inet_ntop.c -include psutil/arch/windows/inet_ntop.h +include psutil/arch/windows/cpu.c +include psutil/arch/windows/cpu.h +include psutil/arch/windows/disk.c +include psutil/arch/windows/disk.h +include psutil/arch/windows/globals.c +include psutil/arch/windows/globals.h +include psutil/arch/windows/net.c +include psutil/arch/windows/net.h include psutil/arch/windows/ntextapi.h include psutil/arch/windows/process_handles.c include psutil/arch/windows/process_handles.h include psutil/arch/windows/process_info.c include psutil/arch/windows/process_info.h +include psutil/arch/windows/process_utils.c +include psutil/arch/windows/process_utils.h include psutil/arch/windows/security.c include psutil/arch/windows/security.h include psutil/arch/windows/services.c include psutil/arch/windows/services.h +include psutil/arch/windows/socks.c +include psutil/arch/windows/socks.h include psutil/arch/windows/wmi.c include psutil/arch/windows/wmi.h include psutil/tests/README.rst diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index ec8b0b2a24..b5d1b4ec48 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -28,7 +28,7 @@ #pragma comment(lib, "IPHLPAPI.lib") #include "arch/windows/ntextapi.h" -#include "arch/windows/global.h" +#include "arch/windows/globals.h" #include "arch/windows/security.h" #include "arch/windows/process_utils.h" #include "arch/windows/process_info.h" diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index e891dcc849..b44f6c0c4b 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -9,7 +9,7 @@ #include #include "ntextapi.h" -#include "global.h" +#include "globals.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 821f687d8c..cb36c0b3a4 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -9,7 +9,7 @@ #include #include "ntextapi.h" -#include "global.h" +#include "globals.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/globals.c similarity index 99% rename from psutil/arch/windows/global.c rename to psutil/arch/windows/globals.c index 1aafcdd14d..50505102f5 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/globals.c @@ -11,7 +11,7 @@ #include #include #include "ntextapi.h" -#include "global.h" +#include "globals.h" // Needed to make these globally visible. diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/globals.h similarity index 100% rename from psutil/arch/windows/global.h rename to psutil/arch/windows/globals.h diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 92887f42b2..271ac06a81 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -13,7 +13,7 @@ #include #include "ntextapi.h" -#include "global.h" +#include "globals.h" static PIP_ADAPTER_ADDRESSES diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 04b30f11ae..1b4b3db742 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -10,7 +10,7 @@ #include #include "ntextapi.h" -#include "global.h" +#include "globals.h" #include "process_utils.h" CRITICAL_SECTION g_cs; diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 757cabbeb0..1c7174f585 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -11,7 +11,7 @@ #include #include "ntextapi.h" -#include "global.h" +#include "globals.h" #include "process_utils.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index 1a721ba535..e10c7f5cb2 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -11,7 +11,7 @@ #include // EnumProcesses #include "ntextapi.h" -#include "global.h" +#include "globals.h" #include "process_utils.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 854ca9550c..50d29687e2 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -12,7 +12,7 @@ #include #include "ntextapi.h" -#include "global.h" +#include "globals.h" #include "process_utils.h" #include "../../_psutil_common.h" diff --git a/setup.py b/setup.py index 9e63db68d4..8c82ad0cbb 100755 --- a/setup.py +++ b/setup.py @@ -150,7 +150,7 @@ def get_winver(): 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/security.c', 'psutil/arch/windows/services.c', - 'psutil/arch/windows/global.c', + 'psutil/arch/windows/globals.c', 'psutil/arch/windows/socks.c', 'psutil/arch/windows/wmi.c', ], From 85af1eef206c8025cfeaef852a22800e8d6c6ac2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 09:40:03 -0800 Subject: [PATCH 0415/1714] fix compiler warning --- psutil/arch/windows/process_info.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 1c7174f585..a3e66a0236 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -735,12 +735,12 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, process = PSUTIL_FIRST_PROCESS(buffer); do { - if (process->UniqueProcessId == (HANDLE)pid) { + if ((ULONG_PTR)process->UniqueProcessId == pid) { *retProcess = process; *retBuffer = buffer; return 1; } - } while ( (process = PSUTIL_NEXT_PROCESS(process)) ); + } while ((process = PSUTIL_NEXT_PROCESS(process))); NoSuchProcess(""); goto error; From d9c84710c25255619ceb2f45bf527e1718c9517e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 09:52:29 -0800 Subject: [PATCH 0416/1714] fix compiler warning + remove cruft --- psutil/_psutil_windows.c | 8 +- psutil/arch/windows/process_handles.c | 104 +++----------------------- psutil/arch/windows/process_handles.h | 2 +- 3 files changed, 14 insertions(+), 100 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index b5d1b4ec48..ba86040b7e 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -991,10 +991,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { static PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { - long pid; - HANDLE processHandle; - DWORD access = PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION; - PyObject *py_retlist; + DWORD pid; + HANDLE processHandle; + DWORD access = PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION; + PyObject *py_retlist; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 1b4b3db742..922631e85c 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -24,7 +24,6 @@ PUNICODE_STRING g_pNameBuffer = NULL; ULONG g_dwSize = 0; ULONG g_dwLength = 0; - #define ObjectNameInformation 1 #define NTQO_TIMEOUT 100 @@ -99,7 +98,7 @@ psutil_create_thread() { static PyObject * -psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { +psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { NTSTATUS status; PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL; DWORD dwInfoSize = 0x10000; @@ -118,10 +117,7 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { // to psutil_get_open_files() is running EnterCriticalSection(&g_cs); - if (g_hEvtStart == NULL || - g_hEvtFinish == NULL) - - { + if (g_hEvtStart == NULL || g_hEvtFinish == NULL) { PyErr_SetFromWindowsErr(0); error = TRUE; goto cleanup; @@ -170,7 +166,7 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { hHandle = &pHandleInfo->Handles[i]; // Check if this hHandle belongs to the PID the user specified. - if (hHandle->UniqueProcessId != (ULONG_PTR)dwPid) + if ((ULONG_PTR)hHandle->UniqueProcessId != dwPid) goto loop_cleanup; if (!DuplicateHandle(hProcess, @@ -181,12 +177,6 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { TRUE, DUPLICATE_SAME_ACCESS)) { - /* - printf("[%d] DuplicateHandle (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ goto loop_cleanup; } @@ -230,34 +220,14 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { // Convert to PyUnicode and append it to the return list if (g_pNameBuffer->Length > 0) { - /* - printf("[%d] Filename (%#x) %#d bytes: %S\n", - dwPid, - hHandle->HandleValue, - g_pNameBuffer->Length, - g_pNameBuffer->Buffer); - */ - py_path = PyUnicode_FromWideChar(g_pNameBuffer->Buffer, - g_pNameBuffer->Length/2); + g_pNameBuffer->Length / 2); if (py_path == NULL) { - /* - printf("[%d] PyUnicode_FromWideChar (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ error = TRUE; goto loop_cleanup; } if (PyList_Append(py_retlist, py_path)) { - /* - printf("[%d] PyList_Append (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ error = TRUE; goto loop_cleanup; } @@ -266,13 +236,11 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { loop_cleanup: Py_XDECREF(py_path); py_path = NULL; - if (g_pNameBuffer != NULL) HeapFree(GetProcessHeap(), 0, g_pNameBuffer); g_pNameBuffer = NULL; g_dwSize = 0; g_dwLength = 0; - if (g_hFile != NULL) CloseHandle(g_hFile); g_hFile = NULL; @@ -299,13 +267,12 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { } LeaveCriticalSection(&g_cs); - return py_retlist; } static PyObject * -psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { +psutil_get_open_files_getmappedfilename(DWORD dwPid, HANDLE hProcess) { NTSTATUS status; PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL; DWORD dwInfoSize = 0x10000; @@ -367,7 +334,7 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { hHandle = &pHandleInfo->Handles[i]; // Check if this hHandle belongs to the PID the user specified. - if (hHandle->UniqueProcessId != (ULONG_PTR)dwPid) + if ((ULONG_PTR)hHandle->UniqueProcessId != dwPid) goto loop_cleanup; if (!DuplicateHandle(hProcess, @@ -378,78 +345,34 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { TRUE, DUPLICATE_SAME_ACCESS)) { - /* - printf("[%d] DuplicateHandle (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ goto loop_cleanup; } hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (hMap == NULL) { - /* - printf("[%d] CreateFileMapping (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ goto loop_cleanup; } pMem = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 1); if (pMem == NULL) { - /* - printf("[%d] MapViewOfFile (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ goto loop_cleanup; } dwSize = GetMappedFileName( GetCurrentProcess(), pMem, (LPSTR)pszFilename, MAX_PATH); if (dwSize == 0) { - /* - printf("[%d] GetMappedFileName (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ goto loop_cleanup; } pszFilename[dwSize] = '\0'; - /* - printf("[%d] Filename (%#x) %#d bytes: %S\n", - dwPid, - hHandle->HandleValue, - dwSize, - pszFilename); - */ - py_path = PyUnicode_FromWideChar(pszFilename, dwSize); if (py_path == NULL) { - /* - printf("[%d] PyUnicode_FromStringAndSize (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ error = TRUE; goto loop_cleanup; } if (PyList_Append(py_retlist, py_path)) { - /* - printf("[%d] PyList_Append (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ error = TRUE; goto loop_cleanup; } @@ -457,19 +380,15 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { loop_cleanup: Py_XDECREF(py_path); py_path = NULL; - if (pMem != NULL) UnmapViewOfFile(pMem); pMem = NULL; - if (hMap != NULL) CloseHandle(hMap); hMap = NULL; - if (hFile != NULL) CloseHandle(hFile); hFile = NULL; - dwSize = 0; } @@ -477,24 +396,19 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { if (pMem != NULL) UnmapViewOfFile(pMem); pMem = NULL; - if (hMap != NULL) CloseHandle(hMap); hMap = NULL; - if (hFile != NULL) CloseHandle(hFile); hFile = NULL; - if (pHandleInfo != NULL) HeapFree(GetProcessHeap(), 0, pHandleInfo); pHandleInfo = NULL; - if (error) { Py_XDECREF(py_retlist); py_retlist = NULL; } - return py_retlist; } @@ -503,10 +417,10 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { * The public function. */ PyObject * -psutil_get_open_files(long dwPid, HANDLE hProcess) { +psutil_get_open_files(DWORD pid, HANDLE hProcess) { // Threaded version only works for Vista+ if (PSUTIL_WINVER >= PSUTIL_WINDOWS_VISTA) - return psutil_get_open_files_ntqueryobject(dwPid, hProcess); + return psutil_get_open_files_ntqueryobject(pid, hProcess); else - return psutil_get_open_files_getmappedfilename(dwPid, hProcess); + return psutil_get_open_files_getmappedfilename(pid, hProcess); } diff --git a/psutil/arch/windows/process_handles.h b/psutil/arch/windows/process_handles.h index 342ce8fd26..d1be3152d5 100644 --- a/psutil/arch/windows/process_handles.h +++ b/psutil/arch/windows/process_handles.h @@ -7,4 +7,4 @@ #include #include -PyObject* psutil_get_open_files(long pid, HANDLE processHandle); +PyObject* psutil_get_open_files(DWORD pid, HANDLE hProcess); From 11ba1c8e6e92e0fd5a04d03873c839766ec49401 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 09:56:22 -0800 Subject: [PATCH 0417/1714] win: move ObjectNameInformation in ntextapi.h --- psutil/arch/windows/ntextapi.h | 1 + psutil/arch/windows/process_handles.c | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 6fa38edb10..8db79d40f1 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -19,6 +19,7 @@ typedef LONG NTSTATUS; #define SystemExtendedHandleInformation 64 #define MemoryWorkingSetInformation 0x1 +#define ObjectNameInformation 1 /* * ================================================================ diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 922631e85c..8d90912859 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -24,7 +24,6 @@ PUNICODE_STRING g_pNameBuffer = NULL; ULONG g_dwSize = 0; ULONG g_dwLength = 0; -#define ObjectNameInformation 1 #define NTQO_TIMEOUT 100 From 0da43ac0a33c71329c6116aa5aee405d0298fd55 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 10:01:38 -0800 Subject: [PATCH 0418/1714] win: provide alias for HeapAlloc() --- psutil/arch/windows/process_handles.c | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 8d90912859..5877715e33 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -25,6 +25,7 @@ ULONG g_dwSize = 0; ULONG g_dwLength = 0; #define NTQO_TIMEOUT 100 +#define MALLOC_ZERO(x) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (x)) static VOID @@ -131,16 +132,14 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { do { if (pHandleInfo != NULL) { - HeapFree(GetProcessHeap(), 0, pHandleInfo); + FREE(pHandleInfo); pHandleInfo = NULL; } // NtQuerySystemInformation won't give us the correct buffer size, // so we guess by doubling the buffer size. dwInfoSize *= 2; - pHandleInfo = HeapAlloc(GetProcessHeap(), - HEAP_ZERO_MEMORY, - dwInfoSize); + pHandleInfo = MALLOC_ZERO(dwInfoSize); if (pHandleInfo == NULL) { PyErr_NoMemory(); @@ -185,7 +184,7 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { do { // Release any previously allocated buffer if (g_pNameBuffer != NULL) { - HeapFree(GetProcessHeap(), 0, g_pNameBuffer); + FREE(g_pNameBuffer); g_pNameBuffer = NULL; g_dwSize = 0; } @@ -197,9 +196,7 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { g_dwSize = g_dwLength; if (g_dwSize > 0) { - g_pNameBuffer = HeapAlloc(GetProcessHeap(), - HEAP_ZERO_MEMORY, - g_dwSize); + g_pNameBuffer = MALLOC_ZERO(g_dwSize); if (g_pNameBuffer == NULL) goto loop_cleanup; @@ -236,7 +233,7 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { Py_XDECREF(py_path); py_path = NULL; if (g_pNameBuffer != NULL) - HeapFree(GetProcessHeap(), 0, g_pNameBuffer); + FREE(g_pNameBuffer); g_pNameBuffer = NULL; g_dwSize = 0; g_dwLength = 0; @@ -247,7 +244,7 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { cleanup: if (g_pNameBuffer != NULL) - HeapFree(GetProcessHeap(), 0, g_pNameBuffer); + FREE(g_pNameBuffer); g_pNameBuffer = NULL; g_dwSize = 0; g_dwLength = 0; @@ -257,7 +254,7 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { g_hFile = NULL; if (pHandleInfo != NULL) - HeapFree(GetProcessHeap(), 0, pHandleInfo); + FREE(pHandleInfo); pHandleInfo = NULL; if (error) { @@ -299,16 +296,14 @@ psutil_get_open_files_getmappedfilename(DWORD dwPid, HANDLE hProcess) { do { if (pHandleInfo != NULL) { - HeapFree(GetProcessHeap(), 0, pHandleInfo); + FREE(pHandleInfo); pHandleInfo = NULL; } // NtQuerySystemInformation won't give us the correct buffer size, // so we guess by doubling the buffer size. dwInfoSize *= 2; - pHandleInfo = HeapAlloc(GetProcessHeap(), - HEAP_ZERO_MEMORY, - dwInfoSize); + pHandleInfo = MALLOC_ZERO(dwInfoSize); if (pHandleInfo == NULL) { PyErr_NoMemory(); @@ -402,7 +397,7 @@ psutil_get_open_files_getmappedfilename(DWORD dwPid, HANDLE hProcess) { CloseHandle(hFile); hFile = NULL; if (pHandleInfo != NULL) - HeapFree(GetProcessHeap(), 0, pHandleInfo); + FREE(pHandleInfo); pHandleInfo = NULL; if (error) { Py_XDECREF(py_retlist); From 220a1948770eda6cd208c3fb0b6ed017d1772288 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 11:57:14 -0800 Subject: [PATCH 0419/1714] cleanup ntextapi.h --- psutil/arch/windows/ntextapi.h | 218 +++++++++++++-------------------- 1 file changed, 84 insertions(+), 134 deletions(-) diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 8db79d40f1..3c6b76cf54 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -2,7 +2,9 @@ * Copyright (c) 2009, Jay Loden, 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. + * Define Windows structs and constants which are considered private. */ + #if !defined(__NTEXTAPI_H__) #define __NTEXTAPI_H__ #include @@ -17,72 +19,83 @@ typedef LONG NTSTATUS; #define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225L) #define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) +// ================================================================ +// Enums +// ================================================================ + +#undef SystemExtendedHandleInformation #define SystemExtendedHandleInformation 64 +#undef MemoryWorkingSetInformation #define MemoryWorkingSetInformation 0x1 +#undef ObjectNameInformation #define ObjectNameInformation 1 +#undef ProcessIoPriority +#define ProcessIoPriority 33 +#undef ProcessWow64Information +#define ProcessWow64Information 26 -/* - * ================================================================ - * Enums. - * ================================================================ - */ +// process suspend() / resume() +typedef enum _KTHREAD_STATE { + Initialized, + Ready, + Running, + Standby, + Terminated, + Waiting, + Transition, + DeferredReady, + GateWait, + MaximumThreadState +} KTHREAD_STATE, *PKTHREAD_STATE; -typedef enum _PROCESSINFOCLASS2 { - _ProcessBasicInformation, - ProcessQuotaLimits, - ProcessIoCounters, - ProcessVmCounters, - ProcessTimes, - ProcessBasePriority, - ProcessRaisePriority, - _ProcessDebugPort, - ProcessExceptionPort, - ProcessAccessToken, - ProcessLdtInformation, - ProcessLdtSize, - ProcessDefaultHardErrorMode, - ProcessIoPortHandlers, - ProcessPooledUsageAndLimits, - ProcessWorkingSetWatch, - ProcessUserModeIOPL, - ProcessEnableAlignmentFaultFixup, - ProcessPriorityClass, - ProcessWx86Information, - ProcessHandleCount, - ProcessAffinityMask, - ProcessPriorityBoost, - ProcessDeviceMap, - ProcessSessionInformation, - ProcessForegroundInformation, - _ProcessWow64Information, - /* added after XP+ */ - _ProcessImageFileName, - ProcessLUIDDeviceMapsEnabled, - _ProcessBreakOnTermination, - ProcessDebugObjectHandle, - ProcessDebugFlags, - ProcessHandleTracing, - ProcessIoPriority, - ProcessExecuteFlags, - ProcessResourceManagement, - ProcessCookie, - ProcessImageInformation, - MaxProcessInfoClass -} PROCESSINFOCLASS2; - -#define PROCESSINFOCLASS PROCESSINFOCLASS2 -#define ProcessBasicInformation _ProcessBasicInformation -#define ProcessWow64Information _ProcessWow64Information -#define ProcessDebugPort _ProcessDebugPort -#define ProcessImageFileName _ProcessImageFileName -#define ProcessBreakOnTermination _ProcessBreakOnTermination +typedef enum _KWAIT_REASON { + Executive, + FreePage, + PageIn, + PoolAllocation, + DelayExecution, + Suspended, + UserRequest, + WrExecutive, + WrFreePage, + WrPageIn, + WrPoolAllocation, + WrDelayExecution, + WrSuspended, + WrUserRequest, + WrEventPair, + WrQueue, + WrLpcReceive, + WrLpcReply, + WrVirtualMemory, + WrPageOut, + WrRendezvous, + WrKeyedEvent, + WrTerminated, + WrProcessInSwap, + WrCpuRateControl, + WrCalloutStack, + WrKernel, + WrResource, + WrPushLock, + WrMutex, + WrQuantumEnd, + WrDispatchInt, + WrPreempted, + WrYieldExecution, + WrFastMutex, + WrGuardedMutex, + WrRundown, + WrAlertByThreadId, + WrDeferredPreempt, + MaximumWaitReason +} KWAIT_REASON, *PKWAIT_REASON; -/* - * ================================================================ - * Structs. - * ================================================================ - */ +// ================================================================ +// Structs. +// ================================================================ +// cpu_stats(), per_cpu_times() typedef struct { LARGE_INTEGER IdleTime; LARGE_INTEGER KernelTime; @@ -92,6 +105,7 @@ typedef struct { ULONG InterruptCount; } _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION; +// cpu_stats() typedef struct { LARGE_INTEGER IdleProcessTime; LARGE_INTEGER IoReadTransferCount; @@ -169,6 +183,7 @@ typedef struct { ULONG SystemCalls; } _SYSTEM_PERFORMANCE_INFORMATION; +// cpu_stats() typedef struct { ULONG ContextSwitches; ULONG DpcCount; @@ -178,62 +193,6 @@ typedef struct { ULONG ApcBypassCount; } _SYSTEM_INTERRUPT_INFORMATION; -typedef enum _KTHREAD_STATE { - Initialized, - Ready, - Running, - Standby, - Terminated, - Waiting, - Transition, - DeferredReady, - GateWait, - MaximumThreadState -} KTHREAD_STATE, *PKTHREAD_STATE; - -typedef enum _KWAIT_REASON { - Executive, - FreePage, - PageIn, - PoolAllocation, - DelayExecution, - Suspended, - UserRequest, - WrExecutive, - WrFreePage, - WrPageIn, - WrPoolAllocation, - WrDelayExecution, - WrSuspended, - WrUserRequest, - WrEventPair, - WrQueue, - WrLpcReceive, - WrLpcReply, - WrVirtualMemory, - WrPageOut, - WrRendezvous, - WrKeyedEvent, - WrTerminated, - WrProcessInSwap, - WrCpuRateControl, - WrCalloutStack, - WrKernel, - WrResource, - WrPushLock, - WrMutex, - WrQuantumEnd, - WrDispatchInt, - WrPreempted, - WrYieldExecution, - WrFastMutex, - WrGuardedMutex, - WrRundown, - WrAlertByThreadId, - WrDeferredPreempt, - MaximumWaitReason -} KWAIT_REASON, *PKWAIT_REASON; - typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { PVOID Object; HANDLE UniqueProcessId; @@ -276,19 +235,6 @@ typedef struct _SYSTEM_THREAD_INFORMATION2 { #define SYSTEM_THREAD_INFORMATION SYSTEM_THREAD_INFORMATION2 #define PSYSTEM_THREAD_INFORMATION PSYSTEM_THREAD_INFORMATION2 -typedef struct _TEB *PTEB; - -typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION { - SYSTEM_THREAD_INFORMATION ThreadInfo; - PVOID StackBase; - PVOID StackLimit; - PVOID Win32StartAddress; - PTEB TebBase; - ULONG_PTR Reserved2; - ULONG_PTR Reserved3; - ULONG_PTR Reserved4; -} SYSTEM_EXTENDED_THREAD_INFORMATION, *PSYSTEM_EXTENDED_THREAD_INFORMATION; - typedef struct _SYSTEM_PROCESS_INFORMATION2 { ULONG NextEntryOffset; ULONG NumberOfThreads; @@ -329,6 +275,7 @@ typedef struct _SYSTEM_PROCESS_INFORMATION2 { #define SYSTEM_PROCESS_INFORMATION SYSTEM_PROCESS_INFORMATION2 #define PSYSTEM_PROCESS_INFORMATION PSYSTEM_PROCESS_INFORMATION2 +// cpu_freq() typedef struct _PROCESSOR_POWER_INFORMATION { ULONG Number; ULONG MaxMhz; @@ -347,7 +294,7 @@ typedef struct in6_addr { } IN6_ADDR, *PIN6_ADDR, FAR *LPIN6_ADDR; #endif -// http://msdn.microsoft.com/en-us/library/aa813741(VS.85).aspx +// PEB / cmdline(), cwd(), environ() typedef struct { BYTE Reserved1[16]; PVOID Reserved2[5]; @@ -359,6 +306,7 @@ typedef struct { LPCWSTR env; } RTL_USER_PROCESS_PARAMETERS_, *PRTL_USER_PROCESS_PARAMETERS_; +// users() typedef struct _WINSTATION_INFO { BYTE Reserved1[72]; ULONG SessionId; @@ -371,6 +319,7 @@ typedef struct _WINSTATION_INFO { FILETIME CurrentTime; } WINSTATION_INFO, *PWINSTATION_INFO; +// cpu_count_phys() #if (_WIN32_WINNT < 0x0601) // Windows < 7 (Vista and XP) typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX { LOGICAL_PROCESSOR_RELATIONSHIP Relationship; @@ -385,6 +334,7 @@ typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX { } SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX; #endif +// memory_uss() typedef struct _MEMORY_WORKING_SET_BLOCK { ULONG_PTR Protection : 5; ULONG_PTR ShareCount : 3; @@ -397,11 +347,13 @@ typedef struct _MEMORY_WORKING_SET_BLOCK { #endif } MEMORY_WORKING_SET_BLOCK, *PMEMORY_WORKING_SET_BLOCK; +// memory_uss() typedef struct _MEMORY_WORKING_SET_INFORMATION { ULONG_PTR NumberOfEntries; MEMORY_WORKING_SET_BLOCK WorkingSetInfo[1]; } MEMORY_WORKING_SET_INFORMATION, *PMEMORY_WORKING_SET_INFORMATION; +// memory_uss() typedef struct _PSUTIL_PROCESS_WS_COUNTERS { SIZE_T NumberOfPages; SIZE_T NumberOfPrivatePages; @@ -409,11 +361,9 @@ typedef struct _PSUTIL_PROCESS_WS_COUNTERS { SIZE_T NumberOfShareablePages; } PSUTIL_PROCESS_WS_COUNTERS, *PPSUTIL_PROCESS_WS_COUNTERS; -/* - * ================================================================ - * Type defs for modules loaded at runtime. - * ================================================================ - */ +// ================================================================ +// Type defs for modules loaded at runtime. +// ================================================================ typedef BOOL (WINAPI *_GetLogicalProcessorInformationEx)( LOGICAL_PROCESSOR_RELATIONSHIP relationship, From ef8b682027d8adda3c88c393aa42d3f1d3ad95b5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 12:21:52 -0800 Subject: [PATCH 0420/1714] fix open_files() tests broken on windows due to case sensitiveness --- psutil/tests/test_process.py | 21 +++++++++++---------- psutil/tests/test_unicode.py | 3 ++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 24a29b5a03..37eaecf28d 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -966,13 +966,12 @@ def test_open_files(self): 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: + 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) - break - else: - self.fail("no file found; files=%s" % repr(files)) for file in files: assert os.path.isfile(file.path), file @@ -982,12 +981,12 @@ def test_open_files(self): p = psutil.Process(sproc.pid) for x in range(100): - filenames = [x.path for x in p.open_files()] + filenames = [os.path.normcase(x.path) for x in p.open_files()] if TESTFN in filenames: break time.sleep(.01) else: - self.assertIn(TESTFN, filenames) + self.assertIn(os.path.normcase(TESTFN), filenames) for file in filenames: assert os.path.isfile(file), file @@ -997,14 +996,16 @@ def test_open_files(self): @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") def test_open_files_2(self): # test fd and path fields + normcase = os.path.normcase 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(): + if normcase(file.path) == normcase(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) + self.assertEqual(normcase(file.path), normcase(fileobj.name)) if WINDOWS: self.assertEqual(file.fd, -1) else: diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index f7115e3d4c..05c94613ad 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -165,7 +165,8 @@ def test_proc_exe(self): exe = p.exe() self.assertIsInstance(exe, str) if self.expect_exact_path_match(): - self.assertEqual(exe, self.funky_name) + self.assertEqual(os.path.normcase(exe), + os.path.normcase(self.funky_name)) def test_proc_name(self): subp = get_test_subprocess(cmd=[self.funky_name]) From 82f72ff959206ed49a96afad707790bfdbed5c99 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 14:11:45 -0800 Subject: [PATCH 0421/1714] winmake.py: accept builtiple targets/args --- scripts/internal/winmake.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 2ddc170703..5de3102c84 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -606,19 +606,22 @@ def parse_cmdline(): except IndexError: return help() set_python(py) + cmds = sys.argv[1:] + if not cmds: + return help() + funcs = [] + for cmd in cmds: + cmd = cmd.replace('-', '_') + fun = getattr(sys.modules[__name__], cmd, None) + if fun is None: + return help() + funcs.append(fun) + return funcs def main(): - parse_cmdline() - try: - cmd = sys.argv[1].replace('-', '_') - except IndexError: - return help() - if cmd in _cmds: - fun = getattr(sys.modules[__name__], cmd) + for fun in parse_cmdline(): fun() - else: - help() if __name__ == '__main__': From 089dcff0067beb35e699f9da57d04b8fb6824132 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 14:22:32 -0800 Subject: [PATCH 0422/1714] include ntextapi.h from globals.h so that we won't have to import it ever --- psutil/arch/windows/cpu.c | 1 - psutil/arch/windows/disk.c | 1 - psutil/arch/windows/globals.c | 1 - psutil/arch/windows/globals.h | 2 +- psutil/arch/windows/net.c | 1 - psutil/arch/windows/process_handles.c | 1 - psutil/arch/windows/process_info.c | 1 - psutil/arch/windows/process_utils.c | 1 - psutil/arch/windows/socks.c | 1 - 9 files changed, 1 insertion(+), 9 deletions(-) diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index b44f6c0c4b..59bedb44f3 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -8,7 +8,6 @@ #include #include -#include "ntextapi.h" #include "globals.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index cb36c0b3a4..e927bf2e62 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -8,7 +8,6 @@ #include #include -#include "ntextapi.h" #include "globals.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/globals.c b/psutil/arch/windows/globals.c index 50505102f5..e600c3bde5 100644 --- a/psutil/arch/windows/globals.c +++ b/psutil/arch/windows/globals.c @@ -10,7 +10,6 @@ #include #include -#include "ntextapi.h" #include "globals.h" diff --git a/psutil/arch/windows/globals.h b/psutil/arch/windows/globals.h index 6764dde057..edae427132 100644 --- a/psutil/arch/windows/globals.h +++ b/psutil/arch/windows/globals.h @@ -7,7 +7,7 @@ */ #include -#include "ntextapi.h" +#include "ntextapi.h" // make it available to any file including us extern int PSUTIL_WINVER; extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 271ac06a81..cb9c1564a7 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -12,7 +12,6 @@ #include #include -#include "ntextapi.h" #include "globals.h" diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 5877715e33..024d00a31c 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -9,7 +9,6 @@ #include // GetMappedFileName() #include -#include "ntextapi.h" #include "globals.h" #include "process_utils.h" diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index a3e66a0236..e8fe8b7d4b 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -10,7 +10,6 @@ #include #include -#include "ntextapi.h" #include "globals.h" #include "process_utils.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index e10c7f5cb2..d9997ad07a 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -10,7 +10,6 @@ #include #include // EnumProcesses -#include "ntextapi.h" #include "globals.h" #include "process_utils.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 50d29687e2..04ea628533 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -11,7 +11,6 @@ #include #include -#include "ntextapi.h" #include "globals.h" #include "process_utils.h" #include "../../_psutil_common.h" From eb4ffe954252ce080118a9a1322b133f456163c7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 15:55:39 -0800 Subject: [PATCH 0423/1714] windows: move get_process_info() into process_info.c to make room for Cygwin --- psutil/_psutil_windows.c | 88 ---------------------------- psutil/arch/windows/process_info.c | 92 +++++++++++++++++++++++++++++- psutil/arch/windows/process_info.h | 1 + 3 files changed, 91 insertions(+), 90 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index ba86040b7e..a98e1c4d14 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1542,94 +1542,6 @@ psutil_proc_num_handles(PyObject *self, PyObject *args) { } -/* - * Get various process information by using NtQuerySystemInformation. - * We use this as a fallback when faster functions fail with access - * denied. This is slower because it iterates over all processes. - * Returned tuple includes the following process info: - * - * - num_threads() - * - ctx_switches() - * - num_handles() (fallback) - * - cpu_times() (fallback) - * - create_time() (fallback) - * - io_counters() (fallback) - * - memory_info() (fallback) - */ -static PyObject * -psutil_proc_info(PyObject *self, PyObject *args) { - DWORD pid; - PSYSTEM_PROCESS_INFORMATION process; - PVOID buffer; - ULONG i; - ULONG ctx_switches = 0; - double user_time; - double kernel_time; - long long create_time; - PyObject *py_retlist; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (! psutil_get_proc_info(pid, &process, &buffer)) - return NULL; - - for (i = 0; i < process->NumberOfThreads; i++) - ctx_switches += process->Threads[i].ContextSwitches; - user_time = (double)process->UserTime.HighPart * HI_T + \ - (double)process->UserTime.LowPart * LO_T; - kernel_time = (double)process->KernelTime.HighPart * HI_T + \ - (double)process->KernelTime.LowPart * LO_T; - - // Convert the LARGE_INTEGER union to a Unix time. - // It's the best I could find by googling and borrowing code here - // and there. The time returned has a precision of 1 second. - if (0 == pid || 4 == pid) { - // the python module will translate this into BOOT_TIME later - create_time = 0; - } - else { - create_time = ((LONGLONG)process->CreateTime.HighPart) << 32; - create_time += process->CreateTime.LowPart - 116444736000000000LL; - create_time /= 10000000; - } - - py_retlist = Py_BuildValue( -#if defined(_WIN64) - "kkdddiKKKKKK" "kKKKKKKKKK", -#else - "kkdddiKKKKKK" "kIIIIIIIII", -#endif - process->HandleCount, // num handles - ctx_switches, // num ctx switches - user_time, // cpu user time - kernel_time, // cpu kernel time - (double)create_time, // create time - (int)process->NumberOfThreads, // num threads - // IO counters - process->ReadOperationCount.QuadPart, // io rcount - process->WriteOperationCount.QuadPart, // io wcount - process->ReadTransferCount.QuadPart, // io rbytes - process->WriteTransferCount.QuadPart, // io wbytes - process->OtherOperationCount.QuadPart, // io others count - process->OtherTransferCount.QuadPart, // io others bytes - // memory - process->PageFaultCount, // num page faults - process->PeakWorkingSetSize, // peak wset - process->WorkingSetSize, // wset - process->QuotaPeakPagedPoolUsage, // peak paged pool - process->QuotaPagedPoolUsage, // paged pool - process->QuotaPeakNonPagedPoolUsage, // peak non paged pool - process->QuotaNonPagedPoolUsage, // non paged pool - process->PagefileUsage, // pagefile - process->PeakPagefileUsage, // peak pagefile - process->PrivatePageCount // private - ); - - free(buffer); - return py_retlist; -} - - static char *get_region_protection_string(ULONG protection) { switch (protection & 0xff) { case PAGE_NOACCESS: diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index e8fe8b7d4b..46b5914924 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -680,8 +680,8 @@ psutil_get_environ(long pid) { /* * Given a process PID and a PSYSTEM_PROCESS_INFORMATION structure - * fills the structure with various process information by using - * NtQuerySystemInformation. + * fills the structure with various process information in one shot + * by using NtQuerySystemInformation. * We use this as a fallback when faster functions fail with access * denied. This is slower because it iterates over all processes. * On success return 1, else 0 with Python exception already set. @@ -749,3 +749,91 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, free(buffer); return 0; } + + +/* + * Get various process information by using NtQuerySystemInformation. + * We use this as a fallback when faster functions fail with access + * denied. This is slower because it iterates over all processes. + * Returned tuple includes the following process info: + * + * - num_threads() + * - ctx_switches() + * - num_handles() (fallback) + * - cpu_times() (fallback) + * - create_time() (fallback) + * - io_counters() (fallback) + * - memory_info() (fallback) + */ +PyObject * +psutil_proc_info(PyObject *self, PyObject *args) { + DWORD pid; + PSYSTEM_PROCESS_INFORMATION process; + PVOID buffer; + ULONG i; + ULONG ctx_switches = 0; + double user_time; + double kernel_time; + long long create_time; + PyObject *py_retlist; + + if (! PyArg_ParseTuple(args, "l", &pid)) + return NULL; + if (! psutil_get_proc_info(pid, &process, &buffer)) + return NULL; + + for (i = 0; i < process->NumberOfThreads; i++) + ctx_switches += process->Threads[i].ContextSwitches; + user_time = (double)process->UserTime.HighPart * HI_T + \ + (double)process->UserTime.LowPart * LO_T; + kernel_time = (double)process->KernelTime.HighPart * HI_T + \ + (double)process->KernelTime.LowPart * LO_T; + + // Convert the LARGE_INTEGER union to a Unix time. + // It's the best I could find by googling and borrowing code here + // and there. The time returned has a precision of 1 second. + if (0 == pid || 4 == pid) { + // the python module will translate this into BOOT_TIME later + create_time = 0; + } + else { + create_time = ((LONGLONG)process->CreateTime.HighPart) << 32; + create_time += process->CreateTime.LowPart - 116444736000000000LL; + create_time /= 10000000; + } + + py_retlist = Py_BuildValue( +#if defined(_WIN64) + "kkdddiKKKKKK" "kKKKKKKKKK", +#else + "kkdddiKKKKKK" "kIIIIIIIII", +#endif + process->HandleCount, // num handles + ctx_switches, // num ctx switches + user_time, // cpu user time + kernel_time, // cpu kernel time + (double)create_time, // create time + (int)process->NumberOfThreads, // num threads + // IO counters + process->ReadOperationCount.QuadPart, // io rcount + process->WriteOperationCount.QuadPart, // io wcount + process->ReadTransferCount.QuadPart, // io rbytes + process->WriteTransferCount.QuadPart, // io wbytes + process->OtherOperationCount.QuadPart, // io others count + process->OtherTransferCount.QuadPart, // io others bytes + // memory + process->PageFaultCount, // num page faults + process->PeakWorkingSetSize, // peak wset + process->WorkingSetSize, // wset + process->QuotaPeakPagedPoolUsage, // peak paged pool + process->QuotaPagedPoolUsage, // paged pool + process->QuotaPeakNonPagedPoolUsage, // peak non paged pool + process->QuotaNonPagedPoolUsage, // non paged pool + process->PagefileUsage, // pagefile + process->PeakPagefileUsage, // peak pagefile + process->PrivatePageCount // private + ); + + free(buffer); + return py_retlist; +} diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index 110d01df4d..2053566924 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -11,3 +11,4 @@ int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PyObject* psutil_get_cmdline(long pid, int use_peb); PyObject* psutil_get_cwd(long pid); PyObject* psutil_get_environ(long pid); +PyObject* psutil_proc_info(PyObject *self, PyObject *args); From b16778a759124796845d9c3ca2ada147074fda4c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 16:24:09 -0800 Subject: [PATCH 0424/1714] fix compiler warnings; move some defss into process_info.h --- psutil/arch/windows/process_info.c | 17 ++++------------- psutil/arch/windows/process_info.h | 7 +++++++ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 46b5914924..d33008bb0b 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -11,18 +11,11 @@ #include #include "globals.h" +#include "process_info.h" #include "process_utils.h" #include "../../_psutil_common.h" -#define PSUTIL_FIRST_PROCESS(Processes) ( \ - (PSYSTEM_PROCESS_INFORMATION)(Processes)) -#define PSUTIL_NEXT_PROCESS(Process) ( \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ - (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) - - // ==================================================================== // Helper structures to access the memory correctly. // Some of these might also be defined in the winternl.h header file @@ -189,14 +182,13 @@ psutil_get_process_data(long pid, http://stackoverflow.com/a/14012919 http://www.drdobbs.com/embracing-64-bit-windows/184401966 */ - _NtQueryInformationProcess NtQueryInformationProcess = NULL; + SIZE_T size = 0; #ifndef _WIN64 static _NtQueryInformationProcess NtWow64QueryInformationProcess64 = NULL; static _NtWow64ReadVirtualMemory64 NtWow64ReadVirtualMemory64 = NULL; #endif HANDLE hProcess = NULL; LPCVOID src; - SIZE_T size; WCHAR *buffer = NULL; #ifdef _WIN64 LPVOID ppeb32 = NULL; @@ -482,7 +474,7 @@ psutil_get_process_data(long pid, */ static int psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { - HANDLE hProcess; + HANDLE hProcess = NULL; ULONG bufLen = 0; NTSTATUS status; char * buffer = NULL; @@ -509,9 +501,8 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { 0, &bufLen); - // 0xC0000225 == STATUS_NOT_FOUND, see: // https://github.com/giampaolo/psutil/issues/1501 - if (status == 0xC0000225) { + if (status == STATUS_NOT_FOUND) { AccessDenied("NtQueryInformationProcess(ProcessBasicInformation) -> " "STATUS_NOT_FOUND translated into PermissionError"); goto error; diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index 2053566924..5fe342b3d5 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -6,6 +6,13 @@ #include +#define PSUTIL_FIRST_PROCESS(Processes) ( \ + (PSYSTEM_PROCESS_INFORMATION)(Processes)) +#define PSUTIL_NEXT_PROCESS(Process) ( \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ + (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) + int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); PyObject* psutil_get_cmdline(long pid, int use_peb); From f07973461f89987aa827a81d2644498e6abbe0a9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 16:48:18 -0800 Subject: [PATCH 0425/1714] move PEB structs into ntextapi.h --- psutil/arch/windows/cpu.c | 2 +- psutil/arch/windows/ntextapi.h | 101 ++++++++++++++++++++++- psutil/arch/windows/process_info.c | 127 +++-------------------------- 3 files changed, 112 insertions(+), 118 deletions(-) diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 59bedb44f3..479adfebb5 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -229,7 +229,7 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { while (offset < length) { // Advance ptr by the size of the previous // SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX struct. - ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)\ + ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) \ (((char*)ptr) + prev_processor_info_size); if (ptr->Relationship == RelationProcessorCore) { diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 3c6b76cf54..3e7147d1d1 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -331,7 +331,8 @@ typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX { CACHE_RELATIONSHIP Cache; GROUP_RELATIONSHIP Group; } DUMMYUNIONNAME; -} SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX; +} SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, \ + *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX; #endif // memory_uss() @@ -361,6 +362,104 @@ typedef struct _PSUTIL_PROCESS_WS_COUNTERS { SIZE_T NumberOfShareablePages; } PSUTIL_PROCESS_WS_COUNTERS, *PPSUTIL_PROCESS_WS_COUNTERS; +// ==================================================================== +// PEB structs for cmdline(), cwd(), environ() +// ==================================================================== + +#ifdef _WIN64 +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[21]; + PVOID LoaderData; + PRTL_USER_PROCESS_PARAMETERS_ ProcessParameters; + // more fields... +} PEB_; + +// When we are a 64 bit process accessing a 32 bit (WoW64) +// process we need to use the 32 bit structure layout. +typedef struct { + USHORT Length; + USHORT MaxLength; + DWORD Buffer; +} UNICODE_STRING32; + +typedef struct { + BYTE Reserved1[16]; + DWORD Reserved2[5]; + UNICODE_STRING32 CurrentDirectoryPath; + DWORD CurrentDirectoryHandle; + UNICODE_STRING32 DllPath; + UNICODE_STRING32 ImagePathName; + UNICODE_STRING32 CommandLine; + DWORD env; +} RTL_USER_PROCESS_PARAMETERS32; + +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[1]; + DWORD Reserved3[2]; + DWORD Ldr; + DWORD ProcessParameters; + // more fields... +} PEB32; +#else // ! _WIN64 +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[1]; + PVOID Reserved3[2]; + PVOID Ldr; + PRTL_USER_PROCESS_PARAMETERS_ ProcessParameters; + // more fields... +} PEB_; + +// When we are a 32 bit (WoW64) process accessing a 64 bit process +// we need to use the 64 bit structure layout and a special function +// to read its memory. +typedef NTSTATUS (NTAPI *_NtWow64ReadVirtualMemory64)( + HANDLE ProcessHandle, + PVOID64 BaseAddress, + PVOID Buffer, + ULONG64 Size, + PULONG64 NumberOfBytesRead); + +typedef struct { + PVOID Reserved1[2]; + PVOID64 PebBaseAddress; + PVOID Reserved2[4]; + PVOID UniqueProcessId[2]; + PVOID Reserved3[2]; +} PROCESS_BASIC_INFORMATION64; + +typedef struct { + USHORT Length; + USHORT MaxLength; + PVOID64 Buffer; +} UNICODE_STRING64; + +typedef struct { + BYTE Reserved1[16]; + PVOID64 Reserved2[5]; + UNICODE_STRING64 CurrentDirectoryPath; + PVOID64 CurrentDirectoryHandle; + UNICODE_STRING64 DllPath; + UNICODE_STRING64 ImagePathName; + UNICODE_STRING64 CommandLine; + PVOID64 env; +} RTL_USER_PROCESS_PARAMETERS64; + +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[21]; + PVOID64 LoaderData; + PVOID64 ProcessParameters; + // more fields... +} PEB64; +#endif // _WIN64 + // ================================================================ // Type defs for modules loaded at runtime. // ================================================================ diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index d33008bb0b..1ee85e082c 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -16,117 +16,10 @@ #include "../../_psutil_common.h" -// ==================================================================== -// Helper structures to access the memory correctly. -// Some of these might also be defined in the winternl.h header file -// but unfortunately not in a usable way. -// ==================================================================== - -// https://msdn.microsoft.com/en-us/library/aa813706(v=vs.85).aspx -#ifdef _WIN64 -typedef struct { - BYTE Reserved1[2]; - BYTE BeingDebugged; - BYTE Reserved2[21]; - PVOID LoaderData; - PRTL_USER_PROCESS_PARAMETERS_ ProcessParameters; - /* More fields ... */ -} PEB_; -#else -typedef struct { - BYTE Reserved1[2]; - BYTE BeingDebugged; - BYTE Reserved2[1]; - PVOID Reserved3[2]; - PVOID Ldr; - PRTL_USER_PROCESS_PARAMETERS_ ProcessParameters; - /* More fields ... */ -} PEB_; -#endif - -#ifdef _WIN64 -/* When we are a 64 bit process accessing a 32 bit (WoW64) process we need to - use the 32 bit structure layout. */ -typedef struct { - USHORT Length; - USHORT MaxLength; - DWORD Buffer; -} UNICODE_STRING32; - -typedef struct { - BYTE Reserved1[16]; - DWORD Reserved2[5]; - UNICODE_STRING32 CurrentDirectoryPath; - DWORD CurrentDirectoryHandle; - UNICODE_STRING32 DllPath; - UNICODE_STRING32 ImagePathName; - UNICODE_STRING32 CommandLine; - DWORD env; -} RTL_USER_PROCESS_PARAMETERS32; - -typedef struct { - BYTE Reserved1[2]; - BYTE BeingDebugged; - BYTE Reserved2[1]; - DWORD Reserved3[2]; - DWORD Ldr; - DWORD ProcessParameters; - /* More fields ... */ -} PEB32; -#else -/* When we are a 32 bit (WoW64) process accessing a 64 bit process we need to - use the 64 bit structure layout and a special function to read its memory. - */ -typedef NTSTATUS (NTAPI *_NtWow64ReadVirtualMemory64)( - HANDLE ProcessHandle, - PVOID64 BaseAddress, - PVOID Buffer, - ULONG64 Size, - PULONG64 NumberOfBytesRead); - -typedef struct { - PVOID Reserved1[2]; - PVOID64 PebBaseAddress; - PVOID Reserved2[4]; - PVOID UniqueProcessId[2]; - PVOID Reserved3[2]; -} PROCESS_BASIC_INFORMATION64; - -typedef struct { - USHORT Length; - USHORT MaxLength; - PVOID64 Buffer; -} UNICODE_STRING64; - -typedef struct { - BYTE Reserved1[16]; - PVOID64 Reserved2[5]; - UNICODE_STRING64 CurrentDirectoryPath; - PVOID64 CurrentDirectoryHandle; - UNICODE_STRING64 DllPath; - UNICODE_STRING64 ImagePathName; - UNICODE_STRING64 CommandLine; - PVOID64 env; -} RTL_USER_PROCESS_PARAMETERS64; - -typedef struct { - BYTE Reserved1[2]; - BYTE BeingDebugged; - BYTE Reserved2[21]; - PVOID64 LoaderData; - PVOID64 ProcessParameters; - /* More fields ... */ -} PEB64; -#endif - - -// ==================================================================== -// Process / PEB functions. -// ==================================================================== - - -/* Given a pointer into a process's memory, figure out how much data can be - * read from it. */ +/* + * Given a pointer into a process's memory, figure out how much + * data can be read from it. + */ static int psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { MEMORY_BASIC_INFORMATION info; @@ -147,12 +40,14 @@ enum psutil_process_data_kind { KIND_ENVIRON, }; -/* Get data from the process with the given pid. The data is returned in the - pdata output member as a nul terminated string which must be freed on - success. - On success 0 is returned. On error the output parameter is not touched, -1 - is returned, and an appropriate Python exception is set. */ +/* + * Get data from the process with the given pid. The data is returned + * in the pdata output member as a nul terminated string which must be + * freed on success. + * On success 0 is returned. On error the output parameter is not touched, + * -1 is returned, and an appropriate Python exception is set. + */ static int psutil_get_process_data(long pid, enum psutil_process_data_kind kind, From 161accb06f7d0eae089d2844ac43720f889a4e8d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 3 Jan 2020 17:21:56 -0800 Subject: [PATCH 0426/1714] rm duplicated C code --- psutil/_psutil_windows.c | 120 --------------------------------- psutil/arch/windows/security.c | 1 - psutil/arch/windows/services.c | 7 -- 3 files changed, 128 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index a98e1c4d14..69e129ebc2 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -712,124 +712,6 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { memInfo.ullAvailVirtual); // avail virtual } -/* - * Retrieves system CPU timing information as a (user, system, idle) - * tuple. On a multiprocessor system, the values returned are the - * sum of the designated times across all processors. - */ -static PyObject * -psutil_cpu_times(PyObject *self, PyObject *args) { - double idle, kernel, user, system; - FILETIME idle_time, kernel_time, user_time; - - if (!GetSystemTimes(&idle_time, &kernel_time, &user_time)) - return PyErr_SetFromWindowsErr(0); - - idle = (double)((HI_T * idle_time.dwHighDateTime) + \ - (LO_T * idle_time.dwLowDateTime)); - user = (double)((HI_T * user_time.dwHighDateTime) + \ - (LO_T * user_time.dwLowDateTime)); - kernel = (double)((HI_T * kernel_time.dwHighDateTime) + \ - (LO_T * kernel_time.dwLowDateTime)); - - // Kernel time includes idle time. - // We return only busy kernel time subtracting idle time from - // kernel time. - system = (kernel - idle); - return Py_BuildValue("(ddd)", user, system, idle); -} - - -/* - * Same as above but for all system CPUs. - */ -static PyObject * -psutil_per_cpu_times(PyObject *self, PyObject *args) { - double idle, kernel, systemt, user, interrupt, dpc; - NTSTATUS status; - _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; - UINT i; - unsigned int ncpus; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - // retrieves number of processors - ncpus = psutil_get_num_cpus(1); - if (ncpus == 0) - goto error; - - // allocates an array of _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION - // structures, one per processor - sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); - if (sppi == NULL) { - PyErr_NoMemory(); - goto error; - } - - // gets cpu time informations - status = psutil_NtQuerySystemInformation( - SystemProcessorPerformanceInformation, - sppi, - ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, - "NtQuerySystemInformation(SystemProcessorPerformanceInformation)" - ); - goto error; - } - - // computes system global times summing each - // processor value - idle = user = kernel = interrupt = dpc = 0; - for (i = 0; i < ncpus; i++) { - py_tuple = NULL; - user = (double)((HI_T * sppi[i].UserTime.HighPart) + - (LO_T * sppi[i].UserTime.LowPart)); - idle = (double)((HI_T * sppi[i].IdleTime.HighPart) + - (LO_T * sppi[i].IdleTime.LowPart)); - kernel = (double)((HI_T * sppi[i].KernelTime.HighPart) + - (LO_T * sppi[i].KernelTime.LowPart)); - interrupt = (double)((HI_T * sppi[i].InterruptTime.HighPart) + - (LO_T * sppi[i].InterruptTime.LowPart)); - dpc = (double)((HI_T * sppi[i].DpcTime.HighPart) + - (LO_T * sppi[i].DpcTime.LowPart)); - - // kernel time includes idle time on windows - // we return only busy kernel time subtracting - // idle time from kernel time - systemt = kernel - idle; - py_tuple = Py_BuildValue( - "(ddddd)", - user, - systemt, - idle, - interrupt, - dpc - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - } - - free(sppi); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (sppi) - free(sppi); - return NULL; -} - /* * Return process current working directory as a Python string. @@ -1575,7 +1457,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess = NULL; PVOID baseAddress; - ULONGLONG previousAllocationBase; WCHAR mappedFileName[MAX_PATH]; LPVOID maxAddr; // required by GetMappedFileNameW @@ -1628,7 +1509,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { Py_CLEAR(py_tuple); Py_CLEAR(py_str); } - previousAllocationBase = (ULONGLONG)basicInfo.AllocationBase; baseAddress = (PCHAR)baseAddress + basicInfo.RegionSize; } diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index 4e2c7435b2..fa83800449 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -119,7 +119,6 @@ psutil_print_err() { int psutil_set_se_debug() { HANDLE hToken; - int err = 1; if ((hToken = psutil_get_thisproc_token()) == NULL) { // "return 1;" to get an exception diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 92458494b4..839cf79e51 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -18,7 +18,6 @@ SC_HANDLE psutil_get_service_handler(char *service_name, DWORD scm_access, DWORD access) { - ENUM_SERVICE_STATUS_PROCESSW *lpService = NULL; SC_HANDLE sc = NULL; SC_HANDLE hService = NULL; @@ -193,8 +192,6 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { SC_HANDLE hService = NULL; BOOL ok; DWORD bytesNeeded = 0; - DWORD resumeHandle = 0; - DWORD dwBytes = 0; QUERY_SERVICE_CONFIGW *qsc = NULL; PyObject *py_tuple = NULL; PyObject *py_unicode_display_name = NULL; @@ -284,8 +281,6 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { SC_HANDLE hService = NULL; BOOL ok; DWORD bytesNeeded = 0; - DWORD resumeHandle = 0; - DWORD dwBytes = 0; SERVICE_STATUS_PROCESS *ssp = NULL; PyObject *py_tuple = NULL; @@ -355,8 +350,6 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { ENUM_SERVICE_STATUS_PROCESSW *lpService = NULL; BOOL ok; DWORD bytesNeeded = 0; - DWORD resumeHandle = 0; - DWORD dwBytes = 0; SC_HANDLE hService = NULL; SERVICE_DESCRIPTIONW *scd = NULL; char *service_name; From 18259d54fc86cc2eb031a3dfb5ca2432574eb266 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 5 Jan 2020 12:33:23 -0800 Subject: [PATCH 0427/1714] refactor win C code: use original WinAPI functions and remove psuil_ prefix --- psutil/_psutil_windows.c | 20 ++++---- psutil/arch/windows/cpu.c | 16 +++---- psutil/arch/windows/globals.c | 64 ++++++++++++------------- psutil/arch/windows/globals.h | 51 -------------------- psutil/arch/windows/ntextapi.h | 68 ++++++++++++++++++++------- psutil/arch/windows/process_handles.c | 6 +-- psutil/arch/windows/process_info.c | 10 ++-- psutil/arch/windows/socks.c | 67 +++++++++++++------------- 8 files changed, 144 insertions(+), 158 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 69e129ebc2..0e780d9ba5 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -55,8 +55,8 @@ psutil_get_num_cpus(int fail_on_err) { unsigned int ncpus = 0; // Minimum requirement: Windows 7 - if (psutil_GetActiveProcessorCount != NULL) { - ncpus = psutil_GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); + if (GetActiveProcessorCount != NULL) { + ncpus = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); if ((ncpus == 0) && (fail_on_err == 1)) { PyErr_SetFromWindowsErr(0); } @@ -104,9 +104,9 @@ psutil_boot_time(PyObject *self, PyObject *args) { (fileTime.dwHighDateTime)) << 32) + fileTime.dwLowDateTime; pt = (time_t)((ll - 116444736000000000ull) / 10000000ull); - if (psutil_GetTickCount64 != NULL) { + if (GetTickCount64 != NULL) { // Windows >= Vista - uptime = psutil_GetTickCount64() / 1000ull; + uptime = GetTickCount64() / 1000ull; } else { // Windows XP. @@ -598,7 +598,7 @@ psutil_GetProcWsetInformation( bufferSize = 0x8000; buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize); - while ((status = psutil_NtQueryVirtualMemory( + while ((status = NtQueryVirtualMemory( hProcess, NULL, MemoryWorkingSetInformation, @@ -752,9 +752,9 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { return NULL; if (PyObject_IsTrue(suspend)) - status = psutil_NtSuspendProcess(hProcess); + status = NtSuspendProcess(hProcess); else - status = psutil_NtResumeProcess(hProcess); + status = NtResumeProcess(hProcess); if (! NT_SUCCESS(status)) { CloseHandle(hProcess); @@ -1097,7 +1097,7 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { if (hProcess == NULL) return NULL; - status = psutil_NtQueryInformationProcess( + status = NtQueryInformationProcess( hProcess, ProcessIoPriority, &IoPriority, @@ -1130,7 +1130,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { if (hProcess == NULL) return NULL; - status = psutil_NtSetInformationProcess( + status = NtSetInformationProcess( hProcess, ProcessIoPriority, (PVOID)&prio, @@ -1346,7 +1346,7 @@ psutil_users(PyObject *self, PyObject *args) { } // login time - if (! psutil_WinStationQueryInformationW( + if (! WinStationQueryInformationW( hServer, sessionId, WinStationInformation, diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 479adfebb5..6966aa18da 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -21,8 +21,8 @@ psutil_get_num_cpus(int fail_on_err) { unsigned int ncpus = 0; // Minimum requirement: Windows 7 - if (psutil_GetActiveProcessorCount != NULL) { - ncpus = psutil_GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); + if (GetActiveProcessorCount != NULL) { + ncpus = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); if ((ncpus == 0) && (fail_on_err == 1)) { PyErr_SetFromWindowsErr(0); } @@ -100,7 +100,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } // gets cpu time informations - status = psutil_NtQuerySystemInformation( + status = NtQuerySystemInformation( SystemProcessorPerformanceInformation, sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), @@ -194,13 +194,13 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { // it supports process groups, meaning this is able to report more // than 64 CPUs. See: // https://bugs.python.org/issue33166 - if (psutil_GetLogicalProcessorInformationEx == NULL) { + if (GetLogicalProcessorInformationEx == NULL) { psutil_debug("Win < 7; cpu_count_phys() forced to None"); Py_RETURN_NONE; } while (1) { - rc = psutil_GetLogicalProcessorInformationEx( + rc = GetLogicalProcessorInformationEx( RelationAll, buffer, &length); if (rc == FALSE) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { @@ -284,7 +284,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { PyErr_NoMemory(); goto error; } - status = psutil_NtQuerySystemInformation( + status = NtQuerySystemInformation( SystemPerformanceInformation, spi, ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), @@ -303,7 +303,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { goto error; } - status = psutil_NtQuerySystemInformation( + status = NtQuerySystemInformation( SystemInterruptInformation, InterruptInformation, ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), @@ -325,7 +325,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { goto error; } - status = psutil_NtQuerySystemInformation( + status = NtQuerySystemInformation( SystemProcessorPerformanceInformation, sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), diff --git a/psutil/arch/windows/globals.c b/psutil/arch/windows/globals.c index e600c3bde5..27d9020cfd 100644 --- a/psutil/arch/windows/globals.c +++ b/psutil/arch/windows/globals.c @@ -79,7 +79,7 @@ psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall) { if (NT_NTWIN32(Status)) err = WIN32_FROM_NTSTATUS(Status); else - err = psutil_RtlNtStatusToDosErrorNoTeb(Status); + err = RtlNtStatusToDosErrorNoTeb(Status); // if (GetLastError() != 0) // err = GetLastError(); sprintf(fullmsg, "(originated from %s)", syscall); @@ -92,88 +92,88 @@ psutil_loadlibs() { /* * Mandatory. */ - psutil_NtQuerySystemInformation = psutil_GetProcAddressFromLib( + NtQuerySystemInformation = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQuerySystemInformation"); - if (psutil_NtQuerySystemInformation == NULL) + if (NtQuerySystemInformation == NULL) return 1; - psutil_NtQueryInformationProcess = psutil_GetProcAddress( + NtQueryInformationProcess = psutil_GetProcAddress( "ntdll.dll", "NtQueryInformationProcess"); - if (! psutil_NtQueryInformationProcess) + if (! NtQueryInformationProcess) return 1; - psutil_NtSetInformationProcess = psutil_GetProcAddress( + NtSetInformationProcess = psutil_GetProcAddress( "ntdll.dll", "NtSetInformationProcess"); - if (! psutil_NtSetInformationProcess) + if (! NtSetInformationProcess) return 1; - psutil_WinStationQueryInformationW = psutil_GetProcAddressFromLib( + WinStationQueryInformationW = psutil_GetProcAddressFromLib( "winsta.dll", "WinStationQueryInformationW"); - if (! psutil_WinStationQueryInformationW) + if (! WinStationQueryInformationW) return 1; - psutil_NtQueryObject = psutil_GetProcAddressFromLib( + NtQueryObject = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQueryObject"); - if (! psutil_NtQueryObject) + if (! NtQueryObject) return 1; - psutil_rtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( + RtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( "ntdll.dll", "RtlIpv4AddressToStringA"); - if (! psutil_rtlIpv4AddressToStringA) + if (! RtlIpv4AddressToStringA) return 1; - psutil_GetExtendedTcpTable = psutil_GetProcAddressFromLib( + GetExtendedTcpTable = psutil_GetProcAddressFromLib( "iphlpapi.dll", "GetExtendedTcpTable"); - if (! psutil_GetExtendedTcpTable) + if (! GetExtendedTcpTable) return 1; - psutil_GetExtendedUdpTable = psutil_GetProcAddressFromLib( + GetExtendedUdpTable = psutil_GetProcAddressFromLib( "iphlpapi.dll", "GetExtendedUdpTable"); - if (! psutil_GetExtendedUdpTable) + if (! GetExtendedUdpTable) return 1; - psutil_RtlGetVersion = psutil_GetProcAddressFromLib( + RtlGetVersion = psutil_GetProcAddressFromLib( "ntdll.dll", "RtlGetVersion"); - if (! psutil_RtlGetVersion) + if (! RtlGetVersion) return 1; - psutil_NtSuspendProcess = psutil_GetProcAddressFromLib( + NtSuspendProcess = psutil_GetProcAddressFromLib( "ntdll", "NtSuspendProcess"); - if (! psutil_NtSuspendProcess) + if (! NtSuspendProcess) return 1; - psutil_NtResumeProcess = psutil_GetProcAddressFromLib( + NtResumeProcess = psutil_GetProcAddressFromLib( "ntdll", "NtResumeProcess"); - if (! psutil_NtResumeProcess) + if (! NtResumeProcess) return 1; - psutil_NtQueryVirtualMemory = psutil_GetProcAddressFromLib( + NtQueryVirtualMemory = psutil_GetProcAddressFromLib( "ntdll", "NtQueryVirtualMemory"); - if (! psutil_NtQueryVirtualMemory) + if (! NtQueryVirtualMemory) return 1; - psutil_RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( + RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( "ntdll", "RtlNtStatusToDosErrorNoTeb"); - if (! psutil_RtlNtStatusToDosErrorNoTeb) + if (! RtlNtStatusToDosErrorNoTeb) return 1; /* * Optional. */ // not available on Wine - psutil_rtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( + RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( "ntdll.dll", "RtlIpv6AddressToStringA"); // minimum requirement: Win Vista - psutil_GetTickCount64 = psutil_GetProcAddress( + GetTickCount64 = psutil_GetProcAddress( "kernel32", "GetTickCount64"); // minimum requirement: Win 7 - psutil_GetActiveProcessorCount = psutil_GetProcAddress( + GetActiveProcessorCount = psutil_GetProcAddress( "kernel32", "GetActiveProcessorCount"); // minumum requirement: Win 7 - psutil_GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( + GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( "kernel32", "GetLogicalProcessorInformationEx"); PyErr_Clear(); @@ -189,7 +189,7 @@ psutil_set_winver() { versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW); memset(&versionInfo, 0, sizeof(RTL_OSVERSIONINFOEXW)); - psutil_RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); + RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); maj = versionInfo.dwMajorVersion; min = versionInfo.dwMinorVersion; if (maj == 6 && min == 0) diff --git a/psutil/arch/windows/globals.h b/psutil/arch/windows/globals.h index edae427132..f87a4f8b9b 100644 --- a/psutil/arch/windows/globals.h +++ b/psutil/arch/windows/globals.h @@ -32,54 +32,3 @@ int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); - -_NtQuerySystemInformation \ - psutil_NtQuerySystemInformation; - -_NtQueryInformationProcess \ - psutil_NtQueryInformationProcess; - -_NtSetInformationProcess - psutil_NtSetInformationProcess; - -_WinStationQueryInformationW \ - psutil_WinStationQueryInformationW; - -_RtlIpv4AddressToStringA \ - psutil_rtlIpv4AddressToStringA; - -_RtlIpv6AddressToStringA \ - psutil_rtlIpv6AddressToStringA; - -_GetExtendedTcpTable \ - psutil_GetExtendedTcpTable; - -_GetExtendedUdpTable \ - psutil_GetExtendedUdpTable; - -_GetActiveProcessorCount \ - psutil_GetActiveProcessorCount; - -_GetTickCount64 \ - psutil_GetTickCount64; - -_NtQueryObject \ - psutil_NtQueryObject; - -_GetLogicalProcessorInformationEx \ - psutil_GetLogicalProcessorInformationEx; - -_RtlGetVersion \ - psutil_RtlGetVersion; - -_NtSuspendProcess \ - psutil_NtSuspendProcess; - -_NtResumeProcess \ - psutil_NtResumeProcess; - -_NtQueryVirtualMemory \ - psutil_NtQueryVirtualMemory; - -_RtlNtStatusToDosErrorNoTeb \ - psutil_RtlNtStatusToDosErrorNoTeb; diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 3e7147d1d1..b7b1c9765d 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -464,12 +464,14 @@ typedef struct { // Type defs for modules loaded at runtime. // ================================================================ -typedef BOOL (WINAPI *_GetLogicalProcessorInformationEx)( +BOOL (WINAPI *_GetLogicalProcessorInformationEx) ( LOGICAL_PROCESSOR_RELATIONSHIP relationship, PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Buffer, PDWORD ReturnLength); -typedef BOOLEAN (WINAPI * _WinStationQueryInformationW)( +#define GetLogicalProcessorInformationEx _GetLogicalProcessorInformationEx + +BOOLEAN (WINAPI * _WinStationQueryInformationW) ( HANDLE ServerHandle, ULONG SessionId, WINSTATIONINFOCLASS WinStationInformationClass, @@ -477,34 +479,46 @@ typedef BOOLEAN (WINAPI * _WinStationQueryInformationW)( ULONG WinStationInformationLength, PULONG pReturnLength); -typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)( +#define WinStationQueryInformationW _WinStationQueryInformationW + +NTSTATUS (NTAPI *_NtQueryInformationProcess) ( HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, DWORD ProcessInformationLength, PDWORD ReturnLength); -typedef NTSTATUS (NTAPI *_NtQuerySystemInformation)( +#define NtQueryInformationProcess _NtQueryInformationProcess + +NTSTATUS (NTAPI *_NtQuerySystemInformation) ( ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength); -typedef NTSTATUS (NTAPI *_NtSetInformationProcess)( +#define NtQuerySystemInformation _NtQuerySystemInformation + +NTSTATUS (NTAPI *_NtSetInformationProcess) ( HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, DWORD ProcessInformationLength); -typedef PSTR (NTAPI * _RtlIpv4AddressToStringA)( +#define NtSetInformationProcess _NtSetInformationProcess + +PSTR (NTAPI * _RtlIpv4AddressToStringA) ( struct in_addr *Addr, PSTR S); -typedef PSTR (NTAPI * _RtlIpv6AddressToStringA)( +#define RtlIpv4AddressToStringA _RtlIpv4AddressToStringA + +PSTR (NTAPI * _RtlIpv6AddressToStringA) ( struct in6_addr *Addr, PSTR P); -typedef DWORD (WINAPI * _GetExtendedTcpTable)( +#define RtlIpv6AddressToStringA _RtlIpv6AddressToStringA + +DWORD (WINAPI * _GetExtendedTcpTable) ( PVOID pTcpTable, PDWORD pdwSize, BOOL bOrder, @@ -512,7 +526,9 @@ typedef DWORD (WINAPI * _GetExtendedTcpTable)( TCP_TABLE_CLASS TableClass, ULONG Reserved); -typedef DWORD (WINAPI * _GetExtendedUdpTable)( +#define GetExtendedTcpTable _GetExtendedTcpTable + +DWORD (WINAPI * _GetExtendedUdpTable) ( PVOID pUdpTable, PDWORD pdwSize, BOOL bOrder, @@ -520,32 +536,46 @@ typedef DWORD (WINAPI * _GetExtendedUdpTable)( UDP_TABLE_CLASS TableClass, ULONG Reserved); -typedef DWORD (CALLBACK *_GetActiveProcessorCount)( +#define GetExtendedUdpTable _GetExtendedUdpTable + +DWORD (CALLBACK *_GetActiveProcessorCount) ( WORD GroupNumber); -typedef ULONGLONG (CALLBACK *_GetTickCount64)( +#define GetActiveProcessorCount _GetActiveProcessorCount + +ULONGLONG (CALLBACK *_GetTickCount64) ( void); -typedef NTSTATUS (NTAPI *_NtQueryObject)( +#define GetTickCount64 _GetTickCount64 + +NTSTATUS (NTAPI *_NtQueryObject) ( HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength); -typedef NTSTATUS (WINAPI *_RtlGetVersion) ( +#define NtQueryObject _NtQueryObject + +NTSTATUS (WINAPI *_RtlGetVersion) ( PRTL_OSVERSIONINFOW lpVersionInformation ); -typedef NTSTATUS (WINAPI *_NtResumeProcess) ( +#define RtlGetVersion _RtlGetVersion + +NTSTATUS (WINAPI *_NtResumeProcess) ( HANDLE hProcess ); -typedef NTSTATUS (WINAPI *_NtSuspendProcess) ( +#define NtResumeProcess _NtResumeProcess + +NTSTATUS (WINAPI *_NtSuspendProcess) ( HANDLE hProcess ); -typedef NTSTATUS (NTAPI *_NtQueryVirtualMemory) ( +#define NtSuspendProcess _NtSuspendProcess + +NTSTATUS (NTAPI *_NtQueryVirtualMemory) ( HANDLE ProcessHandle, PVOID BaseAddress, int MemoryInformationClass, @@ -554,8 +584,12 @@ typedef NTSTATUS (NTAPI *_NtQueryVirtualMemory) ( PSIZE_T ReturnLength ); -typedef ULONG (WINAPI *_RtlNtStatusToDosErrorNoTeb) ( +#define NtQueryVirtualMemory _NtQueryVirtualMemory + +ULONG (WINAPI *_RtlNtStatusToDosErrorNoTeb) ( NTSTATUS status ); +#define RtlNtStatusToDosErrorNoTeb _RtlNtStatusToDosErrorNoTeb + #endif // __NTEXTAPI_H__ diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 024d00a31c..bb1ab42c8d 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -50,7 +50,7 @@ psutil_wait_thread(LPVOID lpvParam) { WaitForSingleObject(g_hEvtStart, INFINITE); // TODO: return code not checked - g_status = psutil_NtQueryObject( + g_status = NtQueryObject( g_hFile, ObjectNameInformation, g_pNameBuffer, @@ -145,7 +145,7 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { error = TRUE; goto cleanup; } - } while ((status = psutil_NtQuerySystemInformation( + } while ((status = NtQuerySystemInformation( SystemExtendedHandleInformation, pHandleInfo, dwInfoSize, @@ -309,7 +309,7 @@ psutil_get_open_files_getmappedfilename(DWORD dwPid, HANDLE hProcess) { error = TRUE; goto cleanup; } - } while ((status = psutil_NtQuerySystemInformation( + } while ((status = NtQuerySystemInformation( SystemExtendedHandleInformation, pHandleInfo, dwInfoSize, diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 1ee85e082c..5daf0b696c 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -102,7 +102,7 @@ psutil_get_process_data(long pid, #ifdef _WIN64 /* 64 bit case. Check if the target is a 32 bit process running in WoW64 * mode. */ - status = psutil_NtQueryInformationProcess( + status = NtQueryInformationProcess( hProcess, ProcessWow64Information, &ppeb32, @@ -252,7 +252,7 @@ psutil_get_process_data(long pid, PEB_ peb; RTL_USER_PROCESS_PARAMETERS_ procParameters; - status = psutil_NtQueryInformationProcess( + status = NtQueryInformationProcess( hProcess, ProcessBasicInformation, &pbi, @@ -389,7 +389,7 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { goto error; // get the right buf size - status = psutil_NtQueryInformationProcess( + status = NtQueryInformationProcess( hProcess, ProcessCommandLineInformation, NULL, @@ -419,7 +419,7 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { } // get the cmdline - status = psutil_NtQueryInformationProcess( + status = NtQueryInformationProcess( hProcess, ProcessCommandLineInformation, buffer, @@ -589,7 +589,7 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, } while (TRUE) { - status = psutil_NtQuerySystemInformation( + status = NtQuerySystemInformation( SystemProcessInformation, buffer, bufferSize, diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 04ea628533..f46ffaee61 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -17,13 +17,14 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) +typedef DWORD (WINAPI * TYPE_GetExtendedTcpTable)(); +typedef DWORD (WINAPI * TYPE_GetExtendedUdpTable)(); -// https://msdn.microsoft.com/library/aa365928.aspx -// TODO properly handle return code -static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, - ULONG address_family, - PVOID * data, DWORD * size) +static DWORD __GetExtendedTcpTable(TYPE_GetExtendedTcpTable call, + ULONG family, + PVOID *data, + DWORD *size) { // Due to other processes being active on the machine, it's possible // that the size of the table increases between the moment where we @@ -34,8 +35,7 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, DWORD error = ERROR_INSUFFICIENT_BUFFER; *size = 0; *data = NULL; - error = call(NULL, size, FALSE, address_family, - TCP_TABLE_OWNER_PID_ALL, 0); + error = call(NULL, size, FALSE, family, TCP_TABLE_OWNER_PID_ALL, 0); while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) { *data = malloc(*size); @@ -43,22 +43,28 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, error = ERROR_NOT_ENOUGH_MEMORY; continue; } - error = call(*data, size, FALSE, address_family, - TCP_TABLE_OWNER_PID_ALL, 0); + error = call(*data, size, FALSE, family, TCP_TABLE_OWNER_PID_ALL, 0); if (error != NO_ERROR) { free(*data); *data = NULL; } } + if (error == ERROR_NOT_ENOUGH_MEMORY) { + PyErr_NoMemory(); + return 1; + } + if (error != NO_ERROR) { + PyErr_SetString(PyExc_RuntimeError, "GetExtendedTcpTable failed"); + return 1; + } return error; } -// https://msdn.microsoft.com/library/aa365930.aspx -// TODO properly check return value -static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, - ULONG address_family, - PVOID * data, DWORD * size) +static DWORD __GetExtendedUdpTable(TYPE_GetExtendedUdpTable call, + ULONG family, + PVOID * data, + DWORD * size) { // Due to other processes being active on the machine, it's possible // that the size of the table increases between the moment where we @@ -69,8 +75,7 @@ static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, DWORD error = ERROR_INSUFFICIENT_BUFFER; *size = 0; *data = NULL; - error = call(NULL, size, FALSE, address_family, - UDP_TABLE_OWNER_PID, 0); + error = call(NULL, size, FALSE, family, UDP_TABLE_OWNER_PID, 0); while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) { *data = malloc(*size); @@ -78,20 +83,18 @@ static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, error = ERROR_NOT_ENOUGH_MEMORY; continue; } - error = call(*data, size, FALSE, address_family, - UDP_TABLE_OWNER_PID, 0); + error = call(*data, size, FALSE, family, UDP_TABLE_OWNER_PID, 0); if (error != NO_ERROR) { free(*data); *data = NULL; } } - if (error == ERROR_NOT_ENOUGH_MEMORY) { PyErr_NoMemory(); return 1; } if (error != NO_ERROR) { - PyErr_SetFromWindowsErr(error); + PyErr_SetString(PyExc_RuntimeError, "GetExtendedUdpTable failed"); return 1; } return 0; @@ -174,7 +177,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, + error = __GetExtendedTcpTable(GetExtendedTcpTable, AF_INET, &table, &tableSize); if (error != 0) goto error; @@ -192,7 +195,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in_addr addr; addr.S_un.S_addr = tcp4Table->table[i].dwLocalAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); + RtlIpv4AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, @@ -214,7 +217,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in_addr addr; addr.S_un.S_addr = tcp4Table->table[i].dwRemoteAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferRemote); + RtlIpv4AddressToStringA(&addr, addressBufferRemote); py_addr_tuple_remote = Py_BuildValue( "(si)", addressBufferRemote, @@ -252,7 +255,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { // TCP IPv6 if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1) && - (psutil_rtlIpv6AddressToStringA != NULL)) + (RtlIpv6AddressToStringA != NULL)) { table = NULL; py_conn_tuple = NULL; @@ -260,7 +263,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedTcpTable(psutil_GetExtendedTcpTable, + error = __GetExtendedTcpTable(GetExtendedTcpTable, AF_INET6, &table, &tableSize); if (error != 0) goto error; @@ -279,7 +282,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in6_addr addr; memcpy(&addr, tcp6Table->table[i].ucLocalAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); + RtlIpv6AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, @@ -302,7 +305,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in6_addr addr; memcpy(&addr, tcp6Table->table[i].ucRemoteAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferRemote); + RtlIpv6AddressToStringA(&addr, addressBufferRemote); py_addr_tuple_remote = Py_BuildValue( "(si)", addressBufferRemote, @@ -346,7 +349,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, + error = __GetExtendedUdpTable(GetExtendedUdpTable, AF_INET, &table, &tableSize); if (error != 0) goto error; @@ -365,7 +368,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in_addr addr; addr.S_un.S_addr = udp4Table->table[i].dwLocalAddr; - psutil_rtlIpv4AddressToStringA(&addr, addressBufferLocal); + RtlIpv4AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, @@ -403,14 +406,14 @@ psutil_net_connections(PyObject *self, PyObject *args) { if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1) && - (psutil_rtlIpv6AddressToStringA != NULL)) + (RtlIpv6AddressToStringA != NULL)) { table = NULL; py_conn_tuple = NULL; py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; tableSize = 0; - error = __GetExtendedUdpTable(psutil_GetExtendedUdpTable, + error = __GetExtendedUdpTable(GetExtendedUdpTable, AF_INET6, &table, &tableSize); if (error != 0) goto error; @@ -428,7 +431,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in6_addr addr; memcpy(&addr, udp6Table->table[i].ucLocalAddr, 16); - psutil_rtlIpv6AddressToStringA(&addr, addressBufferLocal); + RtlIpv6AddressToStringA(&addr, addressBufferLocal); py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, From 9eb72ab5fd56d422d067eb1ce1bfa59b59b29d03 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 5 Jan 2020 15:37:38 -0800 Subject: [PATCH 0428/1714] move send_signal() code into windows module --- psutil/__init__.py | 11 +---------- psutil/_pswindows.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index bf34b429ed..d33d55e8f8 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1211,16 +1211,7 @@ 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._proc.send_signal(sig) @_assert_pid_not_reused def suspend(self): diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index b4dd9bc178..58c795bd70 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -8,6 +8,7 @@ import errno import functools import os +import signal import sys import time from collections import namedtuple @@ -865,7 +866,16 @@ 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)(sig) + else: + raise ValueError( + "only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals " + "are supported on Windows") @wrap_exceptions def wait(self, timeout=None): From 2a297b455e09cd9724274164b74d8cef54617fc2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 17:11:00 +0000 Subject: [PATCH 0429/1714] refactoring + remove dead code --- psutil/_pswindows.py | 70 ++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 58c795bd70..afef111069 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -14,25 +14,6 @@ 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" - raise RuntimeError(msg) - else: - raise - from ._common import AccessDenied from ._common import conn_tmap from ._common import conn_to_ntuple @@ -57,6 +38,22 @@ from ._psutil_windows import NORMAL_PRIORITY_CLASS from ._psutil_windows import REALTIME_PRIORITY_CLASS +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 sys.version_info >= (3, 4): import enum else: @@ -83,7 +80,6 @@ CONN_DELETE_TCB = "DELETE_TCB" HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") -HAS_GETLOADAVG = hasattr(cext, "getloadavg") ERROR_PARTIAL_COPY = 299 @@ -158,14 +154,6 @@ class IOPriority(enum.IntEnum): mem_private=21, ) -# More values at: https://stackoverflow.com/a/20804735/376587 -WIN_10 = (10, 0) -WIN_8 = (6, 2) -WIN_7 = (6, 1) -WIN_SERVER_2008 = (6, 0) -WIN_VISTA = (6, 0) -WIN_SERVER_2003 = (5, 2) - # ===================================================================== # --- named tuples @@ -210,7 +198,8 @@ def convert_dos_path(s): """ rawdrive = '\\'.join(s.split('\\')[:3]) driveletter = cext.win32_QueryDosDevice(rawdrive) - return os.path.join(driveletter, s[len(rawdrive):]) + remainder = s[len(rawdrive):] + return os.path.join(driveletter, remainder) def py2_strencode(s): @@ -337,21 +326,20 @@ def cpu_freq(): return [_common.scpufreq(float(curr), min_, float(max_))] -if HAS_GETLOADAVG: - _loadavg_inititialized = False +_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 +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 + 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]) + # Drop to 2 decimal points which is what Linux does + raw_loads = cext.getloadavg() + return tuple([round(load, 2) for load in raw_loads]) # ===================================================================== From 9d029a0a19a8dbcfb8fd5913a02eb7755836523e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 11:15:47 -0800 Subject: [PATCH 0430/1714] Win: fix compilation err on python 32 bit --- psutil/arch/windows/process_info.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 5daf0b696c..14e8cecf59 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -16,6 +16,16 @@ #include "../../_psutil_common.h" +#ifndef _WIN64 +typedef NTSTATUS (NTAPI *__NtQueryInformationProcess)( + HANDLE ProcessHandle, + DWORD ProcessInformationClass, + PVOID ProcessInformation, + DWORD ProcessInformationLength, + PDWORD ReturnLength); +#endif + + /* * Given a pointer into a process's memory, figure out how much * data can be read from it. @@ -79,7 +89,7 @@ psutil_get_process_data(long pid, */ SIZE_T size = 0; #ifndef _WIN64 - static _NtQueryInformationProcess NtWow64QueryInformationProcess64 = NULL; + static __NtQueryInformationProcess NtWow64QueryInformationProcess64 = NULL; static _NtWow64ReadVirtualMemory64 NtWow64ReadVirtualMemory64 = NULL; #endif HANDLE hProcess = NULL; From 2c9b0be14ee2228315b3a79663912d58c2aa5527 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 11:34:08 -0800 Subject: [PATCH 0431/1714] Win: fix segfault cause by FREE/MALLOC macros --- psutil/_pswindows.py | 1 + psutil/arch/windows/net.c | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index afef111069..772c38c155 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -328,6 +328,7 @@ def cpu_freq(): _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""" diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index cb9c1564a7..fc08354078 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -18,13 +18,13 @@ static PIP_ADAPTER_ADDRESSES psutil_get_nic_addresses() { // allocate a 15 KB buffer to start with - ULONG outBufLen = 15000; + int outBufLen = 15000; DWORD dwRetVal = 0; ULONG attempts = 0; PIP_ADAPTER_ADDRESSES pAddresses = NULL; do { - pAddresses = (IP_ADAPTER_ADDRESSES *) MALLOC(outBufLen); + pAddresses = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen); if (pAddresses == NULL) { PyErr_NoMemory(); return NULL; @@ -33,7 +33,7 @@ psutil_get_nic_addresses() { dwRetVal = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, pAddresses, &outBufLen); if (dwRetVal == ERROR_BUFFER_OVERFLOW) { - FREE(pAddresses); + free(pAddresses); pAddresses = NULL; } else { From 5aedd552404c5c19cd0412c02f3292d8b952e8d4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 13:14:55 -0800 Subject: [PATCH 0432/1714] winmake.py: use argparse --- scripts/internal/winmake.py | 198 +++++++++++++++--------------------- 1 file changed, 84 insertions(+), 114 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 5de3102c84..fd9e773954 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -12,11 +12,11 @@ """ from __future__ import print_function +import argparse import atexit import ctypes import errno import fnmatch -import functools import os import shutil import site @@ -62,6 +62,7 @@ basestring = str GREEN = 2 +LIGHTBLUE = 3 YELLOW = 6 RED = 4 DEFAULT_COLOR = 7 @@ -101,7 +102,7 @@ def stderr_handle(): return handle -def win_colorprint(s, color=3): +def win_colorprint(s, color=LIGHTBLUE): color += 8 # bold handle = stderr_handle() SetConsoleTextAttribute = ctypes.windll.Kernel32.SetConsoleTextAttribute @@ -121,15 +122,6 @@ def sh(cmd, nolog=False): sys.exit(p.returncode) -def cmd(fun): - @functools.wraps(fun) - def wrapper(*args, **kwds): - return fun(*args, **kwds) - - _cmds[fun.__name__] = fun.__doc__ - return wrapper - - def rm(pattern, directory=False): """Recursively remove a file or dir by pattern.""" def safe_remove(path): @@ -223,17 +215,6 @@ def test_setup(): # =================================================================== -@cmd -def help(): - """Print this help""" - safe_print('Run "make [-p ] " where is one of:') - for name in sorted(_cmds): - safe_print( - " %-20s %s" % (name.replace('_', '-'), _cmds[name] or '')) - sys.exit(1) - - -@cmd def build(): """Build / compile""" # Make sure setuptools is installed (needed for 'develop' / @@ -269,24 +250,21 @@ def build(): sh("%s setup.py build_ext -i" % PYTHON) # Make sure it actually worked. sh('%s -c "import psutil"' % PYTHON) - win_colorprint("success", GREEN) + win_colorprint("build + import successful", GREEN) -@cmd def wheel(): """Create wheel file.""" build() sh("%s setup.py bdist_wheel" % PYTHON) -@cmd def upload_wheels(): """Upload wheel files on PyPI.""" build() sh("%s -m twine upload dist/*.whl" % PYTHON) -@cmd def install_pip(): """Install pip""" try: @@ -316,14 +294,12 @@ def install_pip(): os.remove(tfile) -@cmd def install(): """Install in develop / edit mode""" build() sh("%s setup.py develop" % PYTHON) -@cmd def uninstall(): """Uninstall psutil""" # Uninstalling psutil on Windows seems to be tricky. @@ -370,7 +346,6 @@ def uninstall(): print("removed line %r from %r" % (line, path)) -@cmd def clean(): """Deletes dev files""" recursive_rm( @@ -398,7 +373,6 @@ def clean(): safe_rmtree("tmp") -@cmd def setup_dev_env(): """Install useful deps""" install_pip() @@ -406,7 +380,6 @@ def setup_dev_env(): sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEPS))) -@cmd def flake8(): """Run flake8 against all py files""" py_files = subprocess.check_output("git ls-files") @@ -417,22 +390,15 @@ def flake8(): sh("%s -m flake8 %s" % (PYTHON, py_files), nolog=True) -@cmd -def test(): +def test(script=TEST_SCRIPT): """Run tests""" - try: - arg = sys.argv[2] - except IndexError: - arg = TEST_SCRIPT - install() test_setup() - cmdline = "%s %s" % (PYTHON, arg) + cmdline = "%s %s" % (PYTHON, script) safe_print(cmdline) sh(cmdline) -@cmd def coverage(): """Run coverage tests.""" # Note: coverage options are controlled by .coveragerc file @@ -444,7 +410,6 @@ def coverage(): sh("%s -m webbrowser -t htmlcov/index.html" % PYTHON) -@cmd def test_process(): """Run process tests""" install() @@ -452,7 +417,6 @@ def test_process(): sh("%s psutil\\tests\\test_process.py" % PYTHON) -@cmd def test_system(): """Run system tests""" install() @@ -460,7 +424,6 @@ def test_system(): sh("%s psutil\\tests\\test_system.py" % PYTHON) -@cmd def test_platform(): """Run windows only tests""" install() @@ -468,7 +431,6 @@ def test_platform(): sh("%s psutil\\tests\\test_windows.py" % PYTHON) -@cmd def test_misc(): """Run misc tests""" install() @@ -476,7 +438,6 @@ def test_misc(): sh("%s psutil\\tests\\test_misc.py" % PYTHON) -@cmd def test_unicode(): """Run unicode tests""" install() @@ -484,7 +445,6 @@ def test_unicode(): sh("%s psutil\\tests\\test_unicode.py" % PYTHON) -@cmd def test_connections(): """Run connections tests""" install() @@ -492,7 +452,6 @@ def test_connections(): sh("%s psutil\\tests\\test_connections.py" % PYTHON) -@cmd def test_contracts(): """Run contracts tests""" install() @@ -500,16 +459,13 @@ def test_contracts(): sh("%s psutil\\tests\\test_contracts.py" % PYTHON) -@cmd -def test_by_name(): +def test_by_name(name): """Run test by name""" - name = sys.argv[2] install() test_setup() sh("%s -m unittest -v %s" % (PYTHON, name)) -@cmd def test_failed(): """Re-run tests which failed on last run.""" install() @@ -518,20 +474,6 @@ def test_failed(): PYTHON)) -@cmd -def test_script(): - """Quick way to test a script""" - try: - safe_print(sys.argv) - name = sys.argv[2] - except IndexError: - sys.exit('second arg missing') - install() - test_setup() - sh("%s %s" % (PYTHON, name)) - - -@cmd def test_memleaks(): """Run memory leaks tests""" install() @@ -539,7 +481,6 @@ def test_memleaks(): sh("%s psutil\\tests\\test_memory_leaks.py" % PYTHON) -@cmd def install_git_hooks(): """Install GIT pre-commit hook.""" if os.path.isdir('.git'): @@ -551,77 +492,106 @@ def install_git_hooks(): d.write(s.read()) -@cmd def bench_oneshot(): """Benchmarks for oneshot() ctx manager (see #799).""" sh("%s -Wa scripts\\internal\\bench_oneshot.py" % PYTHON) -@cmd def bench_oneshot_2(): """Same as above but using perf module (supposed to be more precise).""" sh("%s -Wa scripts\\internal\\bench_oneshot_2.py" % PYTHON) -@cmd def print_access_denied(): """Print AD exceptions raised by all Process methods.""" sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) -@cmd def print_api_speed(): """Benchmark all API calls.""" sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) -def set_python(s): - global PYTHON - if os.path.isabs(s): - PYTHON = s - else: - # try to look for a python installation - orig = s - s = s.replace('.', '') - vers = ('26', '27', '34', '35', '36', '37', - '26-64', '27-64', '34-64', '35-64', '36-64', '37-64') - for v in vers: - if s == v: - path = r'C:\\python%s\python.exe' % s - if os.path.isfile(path): - print(path) - PYTHON = path - os.putenv('PYTHON', path) - return - return sys.exit( - "can't find any python installation matching %r" % orig) - - -def parse_cmdline(): - if '-p' in sys.argv: - try: - pos = sys.argv.index('-p') - sys.argv.pop(pos) - py = sys.argv.pop(pos) - except IndexError: - return help() - set_python(py) - cmds = sys.argv[1:] - if not cmds: - return help() - funcs = [] - for cmd in cmds: - cmd = cmd.replace('-', '_') - fun = getattr(sys.modules[__name__], cmd, None) - if fun is None: - return help() - funcs.append(fun) - return funcs +def get_python(path): + if not path: + return sys.executable + if os.path.isabs(path): + return path + # try to look for a python installation given a shortcut name + path = path.replace('.', '') + vers = ('26', '27', '36', '37', '38', + '26-64', '27-64', '36-64', '37-64', '38-64' + '26-32', '27-32', '36-32', '37-32', '38-32') + for v in vers: + pypath = r'C:\\python%s\python.exe' % v + if path in pypath and os.path.isfile(pypath): + return pypath def main(): - for fun in parse_cmdline(): - fun() + global PYYHON + parser = argparse.ArgumentParser() + # option shared by all commands + parser.add_argument( + '-p', '--python', + help="use python executable path") + sp = parser.add_subparsers(dest='command', title='targets') + sp.add_parser('bench-oneshot', help="benchmarks for oneshot()") + sp.add_parser('bench-oneshot_2', help="benchmarks for oneshot() (perf)") + sp.add_parser('build', help="build") + sp.add_parser('clean', help="deletes dev files") + sp.add_parser('coverage', help="run coverage tests.") + sp.add_parser('flake8', help="run flake8 against all py files") + sp.add_parser('help', help="print this help") + sp.add_parser('install', help="build + install in develop/edit mode") + sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") + sp.add_parser('install-pip', help="install pip") + sp.add_parser('print-access-denied', help="print AD exceptions") + sp.add_parser('print-api-speed', help="benchmark all API calls") + sp.add_parser('setup-dev-env', help="install deps") + test = sp.add_parser('test', help="[ARG] run tests") + test_by_name = sp.add_parser('test-by-name', help=" run test by name") + sp.add_parser('test-connections', help="run connections tests") + sp.add_parser('test-contracts', help="run contracts tests") + sp.add_parser('test-failed', help="re-run tests which failed on last run") + sp.add_parser('test-memleaks', help="run memory leaks tests") + sp.add_parser('test-misc', help="run misc tests") + sp.add_parser('test-platform', help="run windows only tests") + sp.add_parser('test-process', help="run process tests") + sp.add_parser('test-system', help="run system tests") + sp.add_parser('test-unicode', help="run unicode tests") + sp.add_parser('uninstall', help="uninstall psutil") + sp.add_parser('upload-wheels', help="upload wheel files on PyPI") + sp.add_parser('wheel', help="create wheel file") + + for p in (test, test_by_name): + p.add_argument('arg', type=str, nargs='?', default="", help="arg") + args = parser.parse_args() + + # set python exe + PYTHON = get_python(args.python) + if not PYTHON: + return sys.exit( + "can't find any python installation matching %r" % args.python) + os.putenv('PYTHON', PYTHON) + win_colorprint("using " + PYTHON) + + if not args.command or args.command == 'help': + parser.print_help(sys.stderr) + sys.exit(1) + + fname = args.command.replace('-', '_') + fun = getattr(sys.modules[__name__], fname) # err if fun not defined + funargs = [] + # mandatory args + if args.command in ('test-by-name', 'test-script'): + if not args.arg: + sys.exit('command needs an argument') + funargs = [args.arg] + # optional args + if args.command == 'test' and args.arg: + funargs = [args.arg] + fun(*funargs) if __name__ == '__main__': From 56525b5299d3fd2830cf479df52cb5f527aeed66 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 14:08:03 -0800 Subject: [PATCH 0433/1714] fix #1656: [Windows] Process.memory_full_info() raises AccessDenied even for the current user and os.getpid() --- HISTORY.rst | 2 ++ psutil/_psutil_windows.c | 2 +- psutil/_pswindows.py | 2 +- psutil/tests/__init__.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d112ccc215..a64ad57a55 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,8 @@ XXXX-XX-XX - 1642_: [SunOS] querying basic info for PID 0 results in FileNotFoundError. - 1646_: [FreeBSD] many Process methods may cause a segfault on FreeBSD 12.0 due to a backward incompatible change in a C type introduced in 12.0. +- 1656_: [Windows] Process.memory_full_info() raises AccessDenied even for the + current user and os.getpid(). 5.6.7 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 0e780d9ba5..228566acfd 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -655,7 +655,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION); if (hProcess == NULL) return NULL; diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 772c38c155..f5c81c6d25 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -860,7 +860,7 @@ def send_signal(self, sig): # py >= 2.7 elif sig in (getattr(signal, "CTRL_C_EVENT", object()), getattr(signal, "CTRL_BREAK_EVENT", object())): - os.kill(self.pid, sig)(sig) + os.kill(self.pid, sig) else: raise ValueError( "only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals " diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 767524af5a..a4dc499ab7 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -127,7 +127,7 @@ # 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 +GLOBAL_TIMEOUT = 3 # be more tolerant if we're on travis / appveyor in order to avoid # false positives if TRAVIS or APPVEYOR: From 6c07f48a874754f472f6c3beb7e1184ab8dab39a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 14:28:24 -0800 Subject: [PATCH 0434/1714] fix some win tests --- psutil/_psutil_windows.c | 2 +- psutil/tests/__init__.py | 5 ++++- psutil/tests/test_windows.py | 2 +- scripts/internal/winmake.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 228566acfd..c03324f803 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -608,7 +608,7 @@ psutil_GetProcWsetInformation( { HeapFree(GetProcessHeap(), 0, buffer); bufferSize *= 2; - psutil_debug("NtQueryVirtualMemory increase bufsize %zd", bufferSize); + psutil_debug("NtQueryVirtualMemory increase bufsize %i", bufferSize); // Fail if we're resizing the buffer to something very large. if (bufferSize > 256 * 1024 * 1024) { PyErr_SetString(PyExc_RuntimeError, diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index a4dc499ab7..3931f1c680 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -453,7 +453,10 @@ def sh(cmd, **kwds): kwds.setdefault("creationflags", flags) p = subprocess.Popen(cmd, **kwds) _subprocesses_started.add(p) - stdout, stderr = p.communicate() + if PY3: + stdout, stderr = p.communicate(timeout=GLOBAL_TIMEOUT) + else: + stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) if stderr: diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index de12ff50f6..de41aed8dd 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -210,7 +210,7 @@ def test_boot_time(self): 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) + self.assertLessEqual(diff, 3) def test_boot_time_fluctuation(self): # https://github.com/giampaolo/psutil/issues/1007 diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index fd9e773954..69a1bfbe27 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -529,7 +529,7 @@ def get_python(path): def main(): - global PYYHON + global PYTHON parser = argparse.ArgumentParser() # option shared by all commands parser.add_argument( From 4b12537e250e6b46c0cd2f48e6d178bc1aae6532 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 14:54:45 -0800 Subject: [PATCH 0435/1714] just move stuff around --- psutil/_psutil_common.c | 55 +++++++++++++++++++++++++++-------------- psutil/_psutil_common.h | 24 ++++++++++++------ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index c6e37bc22e..30633c595f 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -7,17 +7,27 @@ */ #include -#ifdef _WIN32 +#ifdef PSUTIL_WINDOWS #include -#endif +#endif // !PSUTIL_WINDOWS #include "_psutil_common.h" -// Global vars. + +// ==================================================================== +// --- Global vars / constants +// ==================================================================== + + int PSUTIL_DEBUG = 0; int PSUTIL_TESTING = 0; +// PSUTIL_CONN_NONE +// ==================================================================== +// --- Python functions and backward compatibility +// ==================================================================== + /* * Backport of unicode FS APIs from Python 3. * On Python 2 we just return a plain byte string @@ -37,22 +47,6 @@ PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size) { } #endif - -/* - * Set OSError(errno=ESRCH, strerror="No such process") Python exception. - * If msg != "" the exception message will change in accordance. - */ -PyObject * -NoSuchProcess(const char *msg) { - PyObject *exc; - exc = PyObject_CallFunction( - PyExc_OSError, "(is)", ESRCH, strlen(msg) ? msg : strerror(ESRCH)); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); - return NULL; -} - - /* * Same as PyErr_SetFromErrno(0) but adds the syscall to the exception * message. @@ -75,6 +69,25 @@ PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { } +// ==================================================================== +// --- Custom exceptions +// ==================================================================== + +/* + * Set OSError(errno=ESRCH, strerror="No such process") Python exception. + * If msg != "" the exception message will change in accordance. + */ +PyObject * +NoSuchProcess(const char *msg) { + PyObject *exc; + exc = PyObject_CallFunction( + PyExc_OSError, "(is)", ESRCH, strlen(msg) ? msg : strerror(ESRCH)); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); + return NULL; +} + + /* * Set OSError(errno=EACCES, strerror="Permission denied") Python exception. * If msg != "" the exception message will change in accordance. @@ -90,6 +103,10 @@ AccessDenied(const char *msg) { } +// ==================================================================== +// --- Global utils +// ==================================================================== + /* * Enable testing mode. This has the same effect as setting PSUTIL_TESTING * env var. This dual method exists because updating os.environ on diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 7f58ad1738..8b069d9fa6 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -4,28 +4,38 @@ * found in the LICENSE file. */ -#ifndef PSUTIL_PSUTIL_COMMON_H -#define PSUTIL_PSUTIL_COMMON_H - #include +// ==================================================================== +// --- Global vars / constants +// ==================================================================== + extern int PSUTIL_TESTING; extern int PSUTIL_DEBUG; - // a signaler for connections without an actual status static const int PSUTIL_CONN_NONE = 128; +// ==================================================================== +// --- Python functions and backward compatibility +// ==================================================================== + #if PY_MAJOR_VERSION < 3 PyObject* PyUnicode_DecodeFSDefault(char *s); PyObject* PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size); #endif +PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); + +// ==================================================================== +// --- Custom exceptions +// ==================================================================== PyObject* AccessDenied(const char *msg); PyObject* NoSuchProcess(const char *msg); -PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); + +// ==================================================================== +// --- Global utils +// ==================================================================== PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); int psutil_setup(void); - -#endif // PSUTIL_PSUTIL_COMMON_H From d7476f05a73a25faf4a4aea1ac9ad7bec9879c33 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 15:28:58 -0800 Subject: [PATCH 0436/1714] get rid of globals.c; move stuff in _psutil_common.c --- psutil/_psutil_aix.c | 5 +- psutil/_psutil_bsd.c | 6 +- psutil/_psutil_common.c | 208 ++++++++++++++++++++++- psutil/_psutil_common.h | 38 ++++- psutil/_psutil_windows.c | 4 +- psutil/arch/freebsd/specific.c | 1 + psutil/arch/netbsd/specific.c | 5 +- psutil/arch/osx/process_info.c | 2 +- psutil/arch/windows/cpu.c | 1 - psutil/arch/windows/disk.c | 1 - psutil/arch/windows/globals.c | 227 -------------------------- psutil/arch/windows/globals.h | 34 ---- psutil/arch/windows/net.c | 2 +- psutil/arch/windows/process_handles.c | 2 +- psutil/arch/windows/process_info.c | 3 +- psutil/arch/windows/process_utils.c | 3 +- psutil/arch/windows/security.c | 1 + psutil/arch/windows/services.c | 3 +- psutil/arch/windows/socks.c | 3 +- setup.py | 1 - 20 files changed, 256 insertions(+), 294 deletions(-) delete mode 100644 psutil/arch/windows/globals.c delete mode 100644 psutil/arch/windows/globals.h diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 9f58f606b0..d7fcaefcbb 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -29,7 +29,6 @@ */ #include - #include #include #include @@ -51,11 +50,11 @@ #include #include +#include "_psutil_common.h" +#include "_psutil_posix.h" #include "arch/aix/ifaddrs.h" #include "arch/aix/net_connections.h" #include "arch/aix/common.h" -#include "_psutil_common.h" -#include "_psutil_posix.h" #define TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 94088d73af..f58cc72f84 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -31,7 +31,7 @@ #include #include #if !defined(__NetBSD__) -#include + #include #endif #include #include @@ -51,13 +51,10 @@ #include // for struct xtcpcb #include // for TCP connection states #include // for inet_ntop() - #include - #include // net io counters #include #include - #include // process open files/connections #include @@ -99,7 +96,6 @@ #endif - // convert a timeval struct to a double #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 30633c595f..f67088b598 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -7,13 +7,8 @@ */ #include -#ifdef PSUTIL_WINDOWS -#include -#endif // !PSUTIL_WINDOWS - #include "_psutil_common.h" - // ==================================================================== // --- Global vars / constants // ==================================================================== @@ -147,3 +142,206 @@ psutil_setup(void) { PSUTIL_TESTING = 1; return 0; } + + +// ==================================================================== +// --- Windows +// ==================================================================== + +#ifdef PSUTIL_WINDOWS +#include + +// Needed to make these globally visible. +int PSUTIL_WINVER; +SYSTEM_INFO PSUTIL_SYSTEM_INFO; + +#define NT_FACILITY_MASK 0xfff +#define NT_FACILITY_SHIFT 16 +#define NT_FACILITY(Status) \ + ((((ULONG)(Status)) >> NT_FACILITY_SHIFT) & NT_FACILITY_MASK) +#define NT_NTWIN32(status) (NT_FACILITY(Status) == FACILITY_WIN32) +#define WIN32_FROM_NTSTATUS(Status) (((ULONG)(Status)) & 0xffff) + + +// A wrapper around GetModuleHandle and GetProcAddress. +PVOID +psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { + HMODULE mod; + FARPROC addr; + + if ((mod = GetModuleHandleA(libname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, procname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, procname); + return NULL; + } + return addr; +} + + +// A wrapper around LoadLibrary and GetProcAddress. +PVOID +psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { + HMODULE mod; + FARPROC addr; + + Py_BEGIN_ALLOW_THREADS + mod = LoadLibraryA(libname); + Py_END_ALLOW_THREADS + if (mod == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, procname)) == NULL) { + PyErr_SetFromWindowsErrWithFilename(0, procname); + FreeLibrary(mod); + return NULL; + } + // Causes crash. + // FreeLibrary(mod); + return addr; +} + + +/* + * Convert a NTSTATUS value to a Win32 error code and set the proper + * Python exception. + */ +PVOID +psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall) { + ULONG err; + char fullmsg[1024]; + + if (NT_NTWIN32(Status)) + err = WIN32_FROM_NTSTATUS(Status); + else + err = RtlNtStatusToDosErrorNoTeb(Status); + // if (GetLastError() != 0) + // err = GetLastError(); + sprintf(fullmsg, "(originated from %s)", syscall); + return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); +} + + +static int +psutil_loadlibs() { + // --- Mandatory + NtQuerySystemInformation = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtQuerySystemInformation"); + if (NtQuerySystemInformation == NULL) + return 1; + NtQueryInformationProcess = psutil_GetProcAddress( + "ntdll.dll", "NtQueryInformationProcess"); + if (! NtQueryInformationProcess) + return 1; + NtSetInformationProcess = psutil_GetProcAddress( + "ntdll.dll", "NtSetInformationProcess"); + if (! NtSetInformationProcess) + return 1; + WinStationQueryInformationW = psutil_GetProcAddressFromLib( + "winsta.dll", "WinStationQueryInformationW"); + if (! WinStationQueryInformationW) + return 1; + NtQueryObject = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtQueryObject"); + if (! NtQueryObject) + return 1; + RtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv4AddressToStringA"); + if (! RtlIpv4AddressToStringA) + return 1; + GetExtendedTcpTable = psutil_GetProcAddressFromLib( + "iphlpapi.dll", "GetExtendedTcpTable"); + if (! GetExtendedTcpTable) + return 1; + GetExtendedUdpTable = psutil_GetProcAddressFromLib( + "iphlpapi.dll", "GetExtendedUdpTable"); + if (! GetExtendedUdpTable) + return 1; + RtlGetVersion = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlGetVersion"); + if (! RtlGetVersion) + return 1; + NtSuspendProcess = psutil_GetProcAddressFromLib( + "ntdll", "NtSuspendProcess"); + if (! NtSuspendProcess) + return 1; + NtResumeProcess = psutil_GetProcAddressFromLib( + "ntdll", "NtResumeProcess"); + if (! NtResumeProcess) + return 1; + NtQueryVirtualMemory = psutil_GetProcAddressFromLib( + "ntdll", "NtQueryVirtualMemory"); + if (! NtQueryVirtualMemory) + return 1; + RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( + "ntdll", "RtlNtStatusToDosErrorNoTeb"); + if (! RtlNtStatusToDosErrorNoTeb) + return 1; + + // --- Optional + // not available on Wine + RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv6AddressToStringA"); + // minimum requirement: Win Vista + GetTickCount64 = psutil_GetProcAddress( + "kernel32", "GetTickCount64"); + // minimum requirement: Win 7 + GetActiveProcessorCount = psutil_GetProcAddress( + "kernel32", "GetActiveProcessorCount"); + // minumum requirement: Win 7 + GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( + "kernel32", "GetLogicalProcessorInformationEx"); + + PyErr_Clear(); + return 0; +} + + +static int +psutil_set_winver() { + RTL_OSVERSIONINFOEXW versionInfo; + ULONG maj; + ULONG min; + + versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW); + memset(&versionInfo, 0, sizeof(RTL_OSVERSIONINFOEXW)); + RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); + maj = versionInfo.dwMajorVersion; + min = versionInfo.dwMinorVersion; + if (maj == 6 && min == 0) + PSUTIL_WINVER = PSUTIL_WINDOWS_VISTA; // or Server 2008 + else if (maj == 6 && min == 1) + PSUTIL_WINVER = PSUTIL_WINDOWS_7; + else if (maj == 6 && min == 2) + PSUTIL_WINVER = PSUTIL_WINDOWS_8; + else if (maj == 6 && min == 3) + PSUTIL_WINVER = PSUTIL_WINDOWS_8_1; + else if (maj == 10 && min == 0) + PSUTIL_WINVER = PSUTIL_WINDOWS_10; + else + PSUTIL_WINVER = PSUTIL_WINDOWS_NEW; + return 0; +} + + +static int +psutil_load_sysinfo() { + GetSystemInfo(&PSUTIL_SYSTEM_INFO); + return 0; +} + + +int +psutil_load_globals() { + if (psutil_loadlibs() != 0) + return 1; + if (psutil_set_winver() != 0) + return 1; + if (psutil_load_sysinfo() != 0) + return 1; + return 0; +} +#endif // PSUTIL_WINDOWS diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 8b069d9fa6..f36e132168 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -20,8 +20,8 @@ static const int PSUTIL_CONN_NONE = 128; // ==================================================================== #if PY_MAJOR_VERSION < 3 -PyObject* PyUnicode_DecodeFSDefault(char *s); -PyObject* PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size); + PyObject* PyUnicode_DecodeFSDefault(char *s); + PyObject* PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size); #endif PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); @@ -39,3 +39,37 @@ PyObject* NoSuchProcess(const char *msg); PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); int psutil_setup(void); + +// ==================================================================== +// --- Windows +// ==================================================================== + +#ifdef PSUTIL_WINDOWS + #include + // make it available to any file which includes this module + #include "arch/windows/ntextapi.h" + + extern int PSUTIL_WINVER; + extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; + + #define PSUTIL_WINDOWS_VISTA 60 + #define PSUTIL_WINDOWS_7 61 + #define PSUTIL_WINDOWS_8 62 + #define PSUTIL_WINDOWS_8_1 63 + #define PSUTIL_WINDOWS_10 100 + #define PSUTIL_WINDOWS_NEW MAXLONG + + #define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) + #define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) + #define LO_T 1e-7 + #define HI_T 429.4967296 + + #ifndef AF_INET6 + #define AF_INET6 23 + #endif + + int psutil_load_globals(); + PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); + PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); + PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); +#endif diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index c03324f803..dd336489ee 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -27,8 +27,7 @@ // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") -#include "arch/windows/ntextapi.h" -#include "arch/windows/globals.h" +#include "_psutil_common.h" #include "arch/windows/security.h" #include "arch/windows/process_utils.h" #include "arch/windows/process_info.h" @@ -39,7 +38,6 @@ #include "arch/windows/services.h" #include "arch/windows/socks.h" #include "arch/windows/wmi.h" -#include "_psutil_common.h" // Raised by Process.wait(). static PyObject *TimeoutExpired; diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 26b802422d..bc267acd97 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -28,6 +28,7 @@ #include "../../_psutil_common.h" #include "../../_psutil_posix.h" + #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) #define PSUTIL_BT2MSEC(bt) (bt.sec * 1000 + (((uint64_t) 1000000000 * (uint32_t) \ (bt.frac >> 32) ) >> 32 ) / 1000000) diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 25adffcd3b..681b237751 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -31,15 +31,16 @@ #include #include // for CPUSTATES & CP_* #define _KERNEL // for DTYPE_* -#include + #include #undef _KERNEL #include // struct diskstats #include #include -#include "specific.h" #include "../../_psutil_common.h" #include "../../_psutil_posix.h" +#include "specific.h" + #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index d21c048edb..9877fd6b13 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -19,9 +19,9 @@ #include #include -#include "process_info.h" #include "../../_psutil_common.h" #include "../../_psutil_posix.h" +#include "process_info.h" /* * Returns a list of all BSD processes on the system. This routine diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 6966aa18da..9a22e1499c 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -8,7 +8,6 @@ #include #include -#include "globals.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index e927bf2e62..45e0ee1e62 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -8,7 +8,6 @@ #include #include -#include "globals.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/globals.c b/psutil/arch/windows/globals.c deleted file mode 100644 index 27d9020cfd..0000000000 --- a/psutil/arch/windows/globals.c +++ /dev/null @@ -1,227 +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. - * - * This code is executed on import. It loads private/undocumented - * Windows APIs and sets Windows version constants so that they are - * available globally. - */ - -#include -#include -#include "globals.h" - - -// Needed to make these globally visible. -int PSUTIL_WINVER; -SYSTEM_INFO PSUTIL_SYSTEM_INFO; - -#define NT_FACILITY_MASK 0xfff -#define NT_FACILITY_SHIFT 16 -#define NT_FACILITY(Status) \ - ((((ULONG)(Status)) >> NT_FACILITY_SHIFT) & NT_FACILITY_MASK) -#define NT_NTWIN32(status) (NT_FACILITY(Status) == FACILITY_WIN32) -#define WIN32_FROM_NTSTATUS(Status) (((ULONG)(Status)) & 0xffff) - - -// A wrapper around GetModuleHandle and GetProcAddress. -PVOID -psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { - HMODULE mod; - FARPROC addr; - - if ((mod = GetModuleHandleA(libname)) == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, libname); - return NULL; - } - if ((addr = GetProcAddress(mod, procname)) == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, procname); - return NULL; - } - return addr; -} - - -// A wrapper around LoadLibrary and GetProcAddress. -PVOID -psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { - HMODULE mod; - FARPROC addr; - - Py_BEGIN_ALLOW_THREADS - mod = LoadLibraryA(libname); - Py_END_ALLOW_THREADS - if (mod == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, libname); - return NULL; - } - if ((addr = GetProcAddress(mod, procname)) == NULL) { - PyErr_SetFromWindowsErrWithFilename(0, procname); - FreeLibrary(mod); - return NULL; - } - // Causes crash. - // FreeLibrary(mod); - return addr; -} - - -/* - * Convert a NTSTATUS value to a Win32 error code and set the proper - * Python exception. - */ -PVOID -psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall) { - ULONG err; - char fullmsg[1024]; - - if (NT_NTWIN32(Status)) - err = WIN32_FROM_NTSTATUS(Status); - else - err = RtlNtStatusToDosErrorNoTeb(Status); - // if (GetLastError() != 0) - // err = GetLastError(); - sprintf(fullmsg, "(originated from %s)", syscall); - return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); -} - - -static int -psutil_loadlibs() { - /* - * Mandatory. - */ - NtQuerySystemInformation = psutil_GetProcAddressFromLib( - "ntdll.dll", "NtQuerySystemInformation"); - if (NtQuerySystemInformation == NULL) - return 1; - - NtQueryInformationProcess = psutil_GetProcAddress( - "ntdll.dll", "NtQueryInformationProcess"); - if (! NtQueryInformationProcess) - return 1; - - NtSetInformationProcess = psutil_GetProcAddress( - "ntdll.dll", "NtSetInformationProcess"); - if (! NtSetInformationProcess) - return 1; - - WinStationQueryInformationW = psutil_GetProcAddressFromLib( - "winsta.dll", "WinStationQueryInformationW"); - if (! WinStationQueryInformationW) - return 1; - - NtQueryObject = psutil_GetProcAddressFromLib( - "ntdll.dll", "NtQueryObject"); - if (! NtQueryObject) - return 1; - - RtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv4AddressToStringA"); - if (! RtlIpv4AddressToStringA) - return 1; - - GetExtendedTcpTable = psutil_GetProcAddressFromLib( - "iphlpapi.dll", "GetExtendedTcpTable"); - if (! GetExtendedTcpTable) - return 1; - - GetExtendedUdpTable = psutil_GetProcAddressFromLib( - "iphlpapi.dll", "GetExtendedUdpTable"); - if (! GetExtendedUdpTable) - return 1; - - RtlGetVersion = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlGetVersion"); - if (! RtlGetVersion) - return 1; - - NtSuspendProcess = psutil_GetProcAddressFromLib( - "ntdll", "NtSuspendProcess"); - if (! NtSuspendProcess) - return 1; - - NtResumeProcess = psutil_GetProcAddressFromLib( - "ntdll", "NtResumeProcess"); - if (! NtResumeProcess) - return 1; - - NtQueryVirtualMemory = psutil_GetProcAddressFromLib( - "ntdll", "NtQueryVirtualMemory"); - if (! NtQueryVirtualMemory) - return 1; - - RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( - "ntdll", "RtlNtStatusToDosErrorNoTeb"); - if (! RtlNtStatusToDosErrorNoTeb) - return 1; - - /* - * Optional. - */ - // not available on Wine - RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv6AddressToStringA"); - - // minimum requirement: Win Vista - GetTickCount64 = psutil_GetProcAddress( - "kernel32", "GetTickCount64"); - - // minimum requirement: Win 7 - GetActiveProcessorCount = psutil_GetProcAddress( - "kernel32", "GetActiveProcessorCount"); - - // minumum requirement: Win 7 - GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( - "kernel32", "GetLogicalProcessorInformationEx"); - - PyErr_Clear(); - return 0; -} - - -static int -psutil_set_winver() { - RTL_OSVERSIONINFOEXW versionInfo; - ULONG maj; - ULONG min; - - versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW); - memset(&versionInfo, 0, sizeof(RTL_OSVERSIONINFOEXW)); - RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); - maj = versionInfo.dwMajorVersion; - min = versionInfo.dwMinorVersion; - if (maj == 6 && min == 0) - PSUTIL_WINVER = PSUTIL_WINDOWS_VISTA; // or Server 2008 - else if (maj == 6 && min == 1) - PSUTIL_WINVER = PSUTIL_WINDOWS_7; - else if (maj == 6 && min == 2) - PSUTIL_WINVER = PSUTIL_WINDOWS_8; - else if (maj == 6 && min == 3) - PSUTIL_WINVER = PSUTIL_WINDOWS_8_1; - else if (maj == 10 && min == 0) - PSUTIL_WINVER = PSUTIL_WINDOWS_10; - else - PSUTIL_WINVER = PSUTIL_WINDOWS_NEW; - return 0; -} - - -static int -psutil_load_sysinfo() { - GetSystemInfo(&PSUTIL_SYSTEM_INFO); - return 0; -} - - -int -psutil_load_globals() { - if (psutil_loadlibs() != 0) - return 1; - if (psutil_set_winver() != 0) - return 1; - if (psutil_load_sysinfo() != 0) - return 1; - return 0; -} diff --git a/psutil/arch/windows/globals.h b/psutil/arch/windows/globals.h deleted file mode 100644 index f87a4f8b9b..0000000000 --- a/psutil/arch/windows/globals.h +++ /dev/null @@ -1,34 +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. - - * List of constants and objects that are globally available. - */ - -#include -#include "ntextapi.h" // make it available to any file including us - -extern int PSUTIL_WINVER; -extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; - -#define PSUTIL_WINDOWS_VISTA 60 -#define PSUTIL_WINDOWS_7 61 -#define PSUTIL_WINDOWS_8 62 -#define PSUTIL_WINDOWS_8_1 63 -#define PSUTIL_WINDOWS_10 100 -#define PSUTIL_WINDOWS_NEW MAXLONG - -#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) -#define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) -#define LO_T 1e-7 -#define HI_T 429.4967296 - -#ifndef AF_INET6 -#define AF_INET6 23 -#endif - -int psutil_load_globals(); -PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); -PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); -PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index fc08354078..f0572d5290 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -12,7 +12,7 @@ #include #include -#include "globals.h" +#include "../../_psutil_common.h" static PIP_ADAPTER_ADDRESSES diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index bb1ab42c8d..71c6bfd903 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -9,7 +9,7 @@ #include // GetMappedFileName() #include -#include "globals.h" +#include "../../_psutil_common.h" #include "process_utils.h" CRITICAL_SECTION g_cs; diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 14e8cecf59..1c5a3c338d 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -10,10 +10,9 @@ #include #include -#include "globals.h" +#include "../../_psutil_common.h" #include "process_info.h" #include "process_utils.h" -#include "../../_psutil_common.h" #ifndef _WIN64 diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index d9997ad07a..fd516beae9 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -10,9 +10,8 @@ #include #include // EnumProcesses -#include "globals.h" -#include "process_utils.h" #include "../../_psutil_common.h" +#include "process_utils.h" DWORD * diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index fa83800449..7e400a2541 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -9,6 +9,7 @@ #include #include + #include "../../_psutil_common.h" diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 839cf79e51..a91cb8f797 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -8,8 +8,9 @@ #include #include -#include "services.h" #include "../../_psutil_common.h" +#include "services.h" + // ================================================================== // utils diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index f46ffaee61..b316ddb85b 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -11,9 +11,8 @@ #include #include -#include "globals.h" -#include "process_utils.h" #include "../../_psutil_common.h" +#include "process_utils.h" #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) diff --git a/setup.py b/setup.py index 8c82ad0cbb..37af746ad4 100755 --- a/setup.py +++ b/setup.py @@ -150,7 +150,6 @@ def get_winver(): 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/security.c', 'psutil/arch/windows/services.c', - 'psutil/arch/windows/globals.c', 'psutil/arch/windows/socks.c', 'psutil/arch/windows/wmi.c', ], From 019f16f347c71bc8b08a93a4bf2d7b500d855045 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 6 Jan 2020 18:52:49 -0800 Subject: [PATCH 0437/1714] #1652 / win / XP support: remove routine to collect files on win < vista --- psutil/arch/windows/process_handles.c | 159 +------------------------- 1 file changed, 3 insertions(+), 156 deletions(-) diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 71c6bfd903..d761a8de18 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -6,12 +6,12 @@ */ #include -#include // GetMappedFileName() #include #include "../../_psutil_common.h" #include "process_utils.h" + CRITICAL_SECTION g_cs; BOOL g_initialized = FALSE; NTSTATUS g_status; @@ -96,8 +96,8 @@ psutil_create_thread() { } -static PyObject * -psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { +PyObject * +psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { NTSTATUS status; PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL; DWORD dwInfoSize = 0x10000; @@ -264,156 +264,3 @@ psutil_get_open_files_ntqueryobject(DWORD dwPid, HANDLE hProcess) { LeaveCriticalSection(&g_cs); return py_retlist; } - - -static PyObject * -psutil_get_open_files_getmappedfilename(DWORD dwPid, HANDLE hProcess) { - NTSTATUS status; - PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL; - DWORD dwInfoSize = 0x10000; - DWORD dwRet = 0; - PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX hHandle = NULL; - HANDLE hFile = NULL; - HANDLE hMap = NULL; - DWORD i = 0; - BOOLEAN error = FALSE; - PyObject* py_retlist = NULL; - PyObject* py_path = NULL; - ULONG dwSize = 0; - LPVOID pMem = NULL; - wchar_t pszFilename[MAX_PATH+1]; - - if (g_initialized == FALSE) - psutil_get_open_files_init(FALSE); - - // Py_BuildValue raises an exception if NULL is returned - py_retlist = PyList_New(0); - if (py_retlist == NULL) { - error = TRUE; - goto cleanup; - } - - do { - if (pHandleInfo != NULL) { - FREE(pHandleInfo); - pHandleInfo = NULL; - } - - // NtQuerySystemInformation won't give us the correct buffer size, - // so we guess by doubling the buffer size. - dwInfoSize *= 2; - pHandleInfo = MALLOC_ZERO(dwInfoSize); - - if (pHandleInfo == NULL) { - PyErr_NoMemory(); - error = TRUE; - goto cleanup; - } - } while ((status = NtQuerySystemInformation( - SystemExtendedHandleInformation, - pHandleInfo, - dwInfoSize, - &dwRet)) == STATUS_INFO_LENGTH_MISMATCH); - - // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH - if (! NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, "NtQuerySystemInformation(SystemExtendedHandleInformation)"); - error = TRUE; - goto cleanup; - } - - for (i = 0; i < pHandleInfo->NumberOfHandles; i++) { - hHandle = &pHandleInfo->Handles[i]; - - // Check if this hHandle belongs to the PID the user specified. - if ((ULONG_PTR)hHandle->UniqueProcessId != dwPid) - goto loop_cleanup; - - if (!DuplicateHandle(hProcess, - (HANDLE)hHandle->HandleValue, - GetCurrentProcess(), - &hFile, - 0, - TRUE, - DUPLICATE_SAME_ACCESS)) - { - goto loop_cleanup; - } - - hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); - if (hMap == NULL) { - goto loop_cleanup; - } - - pMem = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 1); - - if (pMem == NULL) { - goto loop_cleanup; - } - - dwSize = GetMappedFileName( - GetCurrentProcess(), pMem, (LPSTR)pszFilename, MAX_PATH); - if (dwSize == 0) { - goto loop_cleanup; - } - - pszFilename[dwSize] = '\0'; - py_path = PyUnicode_FromWideChar(pszFilename, dwSize); - if (py_path == NULL) { - error = TRUE; - goto loop_cleanup; - } - - if (PyList_Append(py_retlist, py_path)) { - error = TRUE; - goto loop_cleanup; - } - -loop_cleanup: - Py_XDECREF(py_path); - py_path = NULL; - if (pMem != NULL) - UnmapViewOfFile(pMem); - pMem = NULL; - if (hMap != NULL) - CloseHandle(hMap); - hMap = NULL; - if (hFile != NULL) - CloseHandle(hFile); - hFile = NULL; - dwSize = 0; - } - -cleanup: - if (pMem != NULL) - UnmapViewOfFile(pMem); - pMem = NULL; - if (hMap != NULL) - CloseHandle(hMap); - hMap = NULL; - if (hFile != NULL) - CloseHandle(hFile); - hFile = NULL; - if (pHandleInfo != NULL) - FREE(pHandleInfo); - pHandleInfo = NULL; - if (error) { - Py_XDECREF(py_retlist); - py_retlist = NULL; - } - return py_retlist; -} - - -/* - * The public function. - */ -PyObject * -psutil_get_open_files(DWORD pid, HANDLE hProcess) { - // Threaded version only works for Vista+ - if (PSUTIL_WINVER >= PSUTIL_WINDOWS_VISTA) - return psutil_get_open_files_ntqueryobject(pid, hProcess); - else - return psutil_get_open_files_getmappedfilename(pid, hProcess); -} From 57356c43cae11322f9ef7d1e882e3c9954736b7b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 7 Jan 2020 15:26:50 +0100 Subject: [PATCH 0438/1714] refactoring test_contracts.py --- psutil/tests/test_contracts.py | 51 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index cb4a2b963b..4f179c40ca 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -48,19 +48,10 @@ # --- APIs availability # =================================================================== +# Make sure code reflects what doc promises in terms of 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) +class TestAvailConstantsAPIs(unittest.TestCase): def test_PROCFS_PATH(self): self.assertEqual(hasattr(psutil, "PROCFS_PATH"), @@ -106,6 +97,15 @@ def test_linux_rlimit(self): ae(hasattr(psutil, "RLIMIT_RTTIME"), hasit) ae(hasattr(psutil, "RLIMIT_SIGPENDING"), hasit) + +class TestAvailSystemAPIs(unittest.TestCase): + + 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): linux = (LINUX and (os.path.exists("/sys/devices/system/cpu/cpufreq") or @@ -124,44 +124,47 @@ def test_battery(self): self.assertEqual(hasattr(psutil, "sensors_battery"), LINUX or WINDOWS or FREEBSD or MACOS) - def test_proc_environ(self): + +class TestAvailProcessAPIs(unittest.TestCase): + + def test_environ(self): self.assertEqual(hasattr(psutil.Process, "environ"), LINUX or MACOS or WINDOWS or AIX or SUNOS) - def test_proc_uids(self): + def test_uids(self): self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) - def test_proc_gids(self): + def test_gids(self): self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) - def test_proc_terminal(self): + def test_terminal(self): self.assertEqual(hasattr(psutil.Process, "terminal"), POSIX) - def test_proc_ionice(self): + def test_ionice(self): self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) - def test_proc_rlimit(self): + def test_rlimit(self): self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX) - def test_proc_io_counters(self): + def test_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): + def test_num_fds(self): self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX) - def test_proc_num_handles(self): + def test_num_handles(self): self.assertEqual(hasattr(psutil.Process, "num_handles"), WINDOWS) - def test_proc_cpu_affinity(self): + def test_cpu_affinity(self): self.assertEqual(hasattr(psutil.Process, "cpu_affinity"), LINUX or WINDOWS or FREEBSD) - def test_proc_cpu_num(self): + def test_cpu_num(self): self.assertEqual(hasattr(psutil.Process, "cpu_num"), LINUX or FREEBSD or SUNOS) - def test_proc_memory_maps(self): + def test_memory_maps(self): hasit = hasattr(psutil.Process, "memory_maps") self.assertEqual( hasit, False if OPENBSD or NETBSD or AIX or MACOS else True) From 306ca7b6f8387f7c8f0e9dec176e795ab0d3f5fc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 7 Jan 2020 16:16:06 +0100 Subject: [PATCH 0439/1714] #1652: ionice(), remove code checking if we're on Win Vista+ --- psutil/_pswindows.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index f5c81c6d25..3e14bc3cec 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -79,7 +79,6 @@ # ===================================================================== CONN_DELETE_TCB = "DELETE_TCB" -HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") ERROR_PARTIAL_COPY = 299 @@ -1006,23 +1005,21 @@ def nice_get(self): def nice_set(self, value): return cext.proc_priority_set(self.pid, value) - # available on Windows >= Vista - if HAS_PROC_IO_PRIORITY: - @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_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: - raise TypeError("value argument not accepted on Windows") - 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 ionice_set(self, ioclass, value): + if value: + raise TypeError("value argument not accepted on Windows") + 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): From eb5ee07c544cfc90e20b3cbd262a9c9c4e8af363 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 7 Jan 2020 16:45:44 +0100 Subject: [PATCH 0440/1714] add contract tests for IOPRIO_ win constants --- psutil/tests/test_contracts.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 4f179c40ca..6d016779f9 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -66,13 +66,20 @@ def test_win_priority(self): ae(hasattr(psutil, "NORMAL_PRIORITY_CLASS"), WINDOWS) ae(hasattr(psutil, "REALTIME_PRIORITY_CLASS"), WINDOWS) - def test_linux_ioprio(self): + 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) + def test_linux_rlimit(self): ae = self.assertEqual hasit = LINUX and get_kernel_version() >= (2, 6, 36) @@ -453,16 +460,20 @@ def io_counters(self, ret, proc): 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.assertIsInstance(ret.ioclass, int) + self.assertIsInstance(ret.value, int) self.assertGreaterEqual(ret.ioclass, 0) self.assertGreaterEqual(ret.value, 0) - else: + 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, (0, 1, 2)) + self.assertIn(ret, choices) def num_threads(self, ret, proc): self.assertIsInstance(ret, int) From feaa3ad83899142de96af432e15ff4660a258b32 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 7 Jan 2020 17:38:39 +0100 Subject: [PATCH 0441/1714] add more types tests --- psutil/tests/test_contracts.py | 61 ++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 6d016779f9..659f6a6ae7 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -29,7 +29,9 @@ from psutil import WINDOWS from psutil._compat import long from psutil.tests import create_sockets +from psutil.tests import enum from psutil.tests import get_kernel_version +from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import HAS_RLIMIT from psutil.tests import HAS_SENSORS_FANS @@ -198,7 +200,7 @@ def test_memory_info_ex(self): # =================================================================== -class TestSystem(unittest.TestCase): +class TestSystemAPITypes(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 @@ -211,18 +213,38 @@ def setUpClass(cls): def tearDown(self): safe_rmpath(TESTFN) + 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): - # 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) + self.assert_ntuple_of_nums(psutil.cpu_times()) + for nt in psutil.cpu_times(percpu=True): + self.assert_ntuple_of_nums(nt) - def test_io_counters(self): + 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) + + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_cpu_freq(self): + self.assert_ntuple_of_nums(psutil.cpu_freq()) + + def test_disk_io_counters(self): # Duplicate of test_system.py. Keep it anyway. - for k in psutil.disk_io_counters(perdisk=True): + 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. @@ -245,14 +267,25 @@ def test_net_if_addrs(self): for ifname, addrs in psutil.net_if_addrs().items(): self.assertIsInstance(ifname, str) for addr in addrs: + if enum is not None: + assert isinstance(addr.family, enum.IntEnum), addr + else: + assert isinstance(addr.family, int), addr 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(): + 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): @@ -267,6 +300,7 @@ def test_sensors_fans(self): 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): @@ -275,6 +309,13 @@ def test_sensors_temperatures(self): 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. From 90b4cef4b62125c8a42bef4a9db9b9e17459bd6d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 9 Jan 2020 11:01:51 -0800 Subject: [PATCH 0442/1714] #1020: remove doc part mentionin open_files() on win is only able to list files living on the C drive --- docs/index.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index dc025642ae..d69e1fac7b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1807,13 +1807,12 @@ Process class on Windows this method is not reliable due to some limitations of the underlying Windows API which may hang when retrieving certain file handles. - In order to work around that psutil spawns a thread for each handle and - kills it if it's not responding after 100ms. + In order to work around that psutil spawns a thread to determine the file + handle name and kills it if it's not responding after 100ms. That implies that this method on Windows is not guaranteed to enumerate all regular file handles (see `issue 597 `_). - Also, it will only list files living in the C:\\ drive (see - `issue 1020 `_). + Tools like ProcessHacker has the same limitation. .. warning:: on BSD this method can return files with a null path ("") due to a From 3880e3f88d886234b3a4347a7ae3479cbecc6aaa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 9 Jan 2020 16:24:10 -0800 Subject: [PATCH 0443/1714] [Windows] rewrite of open_files() (#1660) --- HISTORY.rst | 1 + psutil/_psutil_common.c | 15 +- psutil/_psutil_common.h | 5 +- psutil/arch/windows/process_handles.c | 399 ++++++++++++-------------- psutil/tests/test_contracts.py | 2 +- 5 files changed, 197 insertions(+), 225 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a64ad57a55..f51553a03d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,7 @@ XXXX-XX-XX due to a backward incompatible change in a C type introduced in 12.0. - 1656_: [Windows] Process.memory_full_info() raises AccessDenied even for the current user and os.getpid(). +- 1660_: [Windows] Process.open_files() complete rewrite + check of errors. 5.6.7 ===== diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index f67088b598..890d515f68 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -153,7 +153,8 @@ psutil_setup(void) { // Needed to make these globally visible. int PSUTIL_WINVER; -SYSTEM_INFO PSUTIL_SYSTEM_INFO; +SYSTEM_INFO PSUTIL_SYSTEM_INFO; +CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define NT_FACILITY_MASK 0xfff #define NT_FACILITY_SHIFT 16 @@ -326,22 +327,14 @@ psutil_set_winver() { return 0; } - -static int -psutil_load_sysinfo() { - GetSystemInfo(&PSUTIL_SYSTEM_INFO); - return 0; -} - - int psutil_load_globals() { if (psutil_loadlibs() != 0) return 1; if (psutil_set_winver() != 0) return 1; - if (psutil_load_sysinfo() != 0) - return 1; + GetSystemInfo(&PSUTIL_SYSTEM_INFO); + InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); return 0; } #endif // PSUTIL_WINDOWS diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index f36e132168..92a98b9cc7 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -50,7 +50,8 @@ int psutil_setup(void); #include "arch/windows/ntextapi.h" extern int PSUTIL_WINVER; - extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; + extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; + extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define PSUTIL_WINDOWS_VISTA 60 #define PSUTIL_WINDOWS_7 61 @@ -60,7 +61,9 @@ int psutil_setup(void); #define PSUTIL_WINDOWS_NEW MAXLONG #define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) + #define MALLOC_ZERO(x) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (x)) #define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) + #define LO_T 1e-7 #define HI_T 429.4967296 diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index d761a8de18..4566baffa7 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -2,7 +2,21 @@ * 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. + */ + +/* + * This module retrieves handles opened by a process. + * We use NtQuerySystemInformation to enumerate them and NtQueryObject + * to obtain the corresponding file name. + * Since NtQueryObject hangs for certain handle types we call it in a + * separate thread which gets killed if it doesn't complete within 100ms. + * This is a limitation of the Windows API and ProcessHacker uses the + * same trick: https://github.com/giampaolo/psutil/pull/597 * + * CREDITS: original implementation was written by Jeff Tang. + * It was then rewritten by Giampaolo Rodola many years later. + * Utility functions for getting the file handles and names were re-adapted + * from the excellent ProcessHacker. */ #include @@ -12,255 +26,216 @@ #include "process_utils.h" -CRITICAL_SECTION g_cs; -BOOL g_initialized = FALSE; -NTSTATUS g_status; -HANDLE g_hFile = NULL; -HANDLE g_hEvtStart = NULL; -HANDLE g_hEvtFinish = NULL; -HANDLE g_hThread = NULL; -PUNICODE_STRING g_pNameBuffer = NULL; -ULONG g_dwSize = 0; -ULONG g_dwLength = 0; - -#define NTQO_TIMEOUT 100 -#define MALLOC_ZERO(x) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (x)) - +#define THREAD_TIMEOUT 100 // ms +// Global object shared between the 2 threads. +PUNICODE_STRING globalFileName = NULL; + + +static int +psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { + static ULONG initialBufferSize = 0x10000; + NTSTATUS status; + PVOID buffer; + ULONG bufferSize; + + bufferSize = initialBufferSize; + buffer = MALLOC_ZERO(bufferSize); + + while ((status = NtQuerySystemInformation( + SystemExtendedHandleInformation, + buffer, + bufferSize, + NULL + )) == STATUS_INFO_LENGTH_MISMATCH) + { + FREE(buffer); + bufferSize *= 2; + + // Fail if we're resizing the buffer to something very large. + if (bufferSize > 256 * 1024 * 1024) { + PyErr_SetString( + PyExc_RuntimeError, + "SystemExtendedHandleInformation buffer too big"); + return 1; + } -static VOID -psutil_get_open_files_init(BOOL threaded) { - if (g_initialized == TRUE) - return; + buffer = MALLOC_ZERO(bufferSize); + } - // Create events for signalling work between threads - if (threaded == TRUE) { - g_hEvtStart = CreateEvent(NULL, FALSE, FALSE, NULL); - g_hEvtFinish = CreateEvent(NULL, FALSE, FALSE, NULL); - InitializeCriticalSection(&g_cs); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); + FREE(buffer); + return 1; } - g_initialized = TRUE; + *handles = (PSYSTEM_HANDLE_INFORMATION_EX)buffer; + return 0; } -static DWORD WINAPI -psutil_wait_thread(LPVOID lpvParam) { - // Loop infinitely waiting for work - while (TRUE) { - WaitForSingleObject(g_hEvtStart, INFINITE); +static int +psutil_get_filename(LPVOID lpvParam) { + HANDLE hFile = *((HANDLE*)lpvParam); + NTSTATUS status; + ULONG bufferSize; + ULONG attempts = 8; + + bufferSize = 0x200; + globalFileName = MALLOC_ZERO(bufferSize); + + // Note: also this is supposed to hang, hence why we do it in here. + if (GetFileType(hFile) != FILE_TYPE_DISK) { + globalFileName->Length = 0; + return 0; + } - // TODO: return code not checked - g_status = NtQueryObject( - g_hFile, + // A loop is needed because the I/O subsystem likes to give us the + // wrong return lengths... + do { + status = NtQueryObject( + hFile, ObjectNameInformation, - g_pNameBuffer, - g_dwSize, - &g_dwLength); - SetEvent(g_hEvtFinish); + globalFileName, + bufferSize, + &bufferSize + ); + if (status == STATUS_BUFFER_OVERFLOW || + status == STATUS_INFO_LENGTH_MISMATCH || + status == STATUS_BUFFER_TOO_SMALL) + { + FREE(globalFileName); + globalFileName = MALLOC_ZERO(bufferSize); + } + else { + break; + } + } while (--attempts); + + if (! NT_SUCCESS(status)) { + PyErr_SetFromOSErrnoWithSyscall("NtQuerySystemInformation"); + FREE(globalFileName); + return 1; } + + return 0; } static DWORD -psutil_create_thread() { - DWORD dwWait = 0; - - if (g_hThread == NULL) - g_hThread = CreateThread( - NULL, - 0, - psutil_wait_thread, - NULL, - 0, - NULL); - if (g_hThread == NULL) - return GetLastError(); - - // Signal the worker thread to start - SetEvent(g_hEvtStart); - - // Wait for the worker thread to finish - dwWait = WaitForSingleObject(g_hEvtFinish, NTQO_TIMEOUT); - - // If the thread hangs, kill it and cleanup - if (dwWait == WAIT_TIMEOUT) { - SuspendThread(g_hThread); - TerminateThread(g_hThread, 1); - WaitForSingleObject(g_hThread, INFINITE); - CloseHandle(g_hThread); - - g_hThread = NULL; +psutil_threaded_get_filename(HANDLE hFile) { + DWORD dwWait; + HANDLE hThread; + DWORD threadRetValue; + + hThread = CreateThread( + NULL, 0, (LPTHREAD_START_ROUTINE)psutil_get_filename, &hFile, 0, NULL); + if (hThread == NULL) { + PyErr_SetFromOSErrnoWithSyscall("CreateThread"); + return 1; } - return dwWait; + // Wait for the worker thread to finish. + dwWait = WaitForSingleObject(hThread, THREAD_TIMEOUT); + + // If the thread hangs, kill it and cleanup. + if (dwWait == WAIT_TIMEOUT) { + psutil_debug( + "get file name thread timed out after %i ms", THREAD_TIMEOUT); + TerminateThread(hThread, 1); + CloseHandle(hThread); + return 0; + } + else { + if (GetExitCodeThread(hThread, &threadRetValue) == 0) { + CloseHandle(hThread); + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeThread"); + return 1; + } + CloseHandle(hThread); + return threadRetValue; + } } PyObject * psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { - NTSTATUS status; - PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL; - DWORD dwInfoSize = 0x10000; - DWORD dwRet = 0; + PSYSTEM_HANDLE_INFORMATION_EX handlesList = NULL; PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX hHandle = NULL; - DWORD i = 0; - BOOLEAN error = FALSE; - DWORD dwWait = 0; - PyObject* py_retlist = NULL; + HANDLE hFile = NULL; + ULONG i = 0; + BOOLEAN errorOccurred = FALSE; PyObject* py_path = NULL; + PyObject* py_retlist = PyList_New(0);; - if (g_initialized == FALSE) - psutil_get_open_files_init(TRUE); + if (!py_retlist) + return NULL; // Due to the use of global variables, ensure only 1 call - // to psutil_get_open_files() is running - EnterCriticalSection(&g_cs); - - if (g_hEvtStart == NULL || g_hEvtFinish == NULL) { - PyErr_SetFromWindowsErr(0); - error = TRUE; - goto cleanup; - } - - // Py_BuildValue raises an exception if NULL is returned - py_retlist = PyList_New(0); - if (py_retlist == NULL) { - error = TRUE; - goto cleanup; - } - - do { - if (pHandleInfo != NULL) { - FREE(pHandleInfo); - pHandleInfo = NULL; - } + // to psutil_get_open_files() is running. + EnterCriticalSection(&PSUTIL_CRITICAL_SECTION); - // NtQuerySystemInformation won't give us the correct buffer size, - // so we guess by doubling the buffer size. - dwInfoSize *= 2; - pHandleInfo = MALLOC_ZERO(dwInfoSize); + if (psutil_enum_handles(&handlesList) != 0) + goto error; - if (pHandleInfo == NULL) { - PyErr_NoMemory(); - error = TRUE; - goto cleanup; - } - } while ((status = NtQuerySystemInformation( - SystemExtendedHandleInformation, - pHandleInfo, - dwInfoSize, - &dwRet)) == STATUS_INFO_LENGTH_MISMATCH); - - // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH - if (! NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, "NtQuerySystemInformation(SystemExtendedHandleInformation)"); - error = TRUE; - goto cleanup; - } - - for (i = 0; i < pHandleInfo->NumberOfHandles; i++) { - hHandle = &pHandleInfo->Handles[i]; - - // Check if this hHandle belongs to the PID the user specified. + for (i = 0; i < handlesList->NumberOfHandles; i++) { + hHandle = &handlesList->Handles[i]; if ((ULONG_PTR)hHandle->UniqueProcessId != dwPid) - goto loop_cleanup; - - if (!DuplicateHandle(hProcess, - (HANDLE)hHandle->HandleValue, - GetCurrentProcess(), - &g_hFile, - 0, - TRUE, - DUPLICATE_SAME_ACCESS)) + continue; + if (! DuplicateHandle( + hProcess, + hHandle->HandleValue, + GetCurrentProcess(), + &hFile, + 0, + TRUE, + DUPLICATE_SAME_ACCESS)) { - goto loop_cleanup; + // Will fail if not a regular file; just skip it. + continue; } - // Guess buffer size is MAX_PATH + 1 - g_dwLength = (MAX_PATH+1) * sizeof(WCHAR); - - do { - // Release any previously allocated buffer - if (g_pNameBuffer != NULL) { - FREE(g_pNameBuffer); - g_pNameBuffer = NULL; - g_dwSize = 0; - } - - // NtQueryObject puts the required buffer size in g_dwLength - // WinXP edge case puts g_dwLength == 0, just skip this handle - if (g_dwLength == 0) - goto loop_cleanup; - - g_dwSize = g_dwLength; - if (g_dwSize > 0) { - g_pNameBuffer = MALLOC_ZERO(g_dwSize); - - if (g_pNameBuffer == NULL) - goto loop_cleanup; - } - - dwWait = psutil_create_thread(); - - // If the call does not return, skip this handle - if (dwWait != WAIT_OBJECT_0) - goto loop_cleanup; - - } while (g_status == STATUS_INFO_LENGTH_MISMATCH); - - // NtQueryObject stopped returning STATUS_INFO_LENGTH_MISMATCH - if (!NT_SUCCESS(g_status)) - goto loop_cleanup; - - // Convert to PyUnicode and append it to the return list - if (g_pNameBuffer->Length > 0) { - py_path = PyUnicode_FromWideChar(g_pNameBuffer->Buffer, - g_pNameBuffer->Length / 2); - if (py_path == NULL) { - error = TRUE; - goto loop_cleanup; - } - - if (PyList_Append(py_retlist, py_path)) { - error = TRUE; - goto loop_cleanup; - } + // This will set *globalFileName* global variable. + if (psutil_threaded_get_filename(hFile) != 0) + goto error; + + if (globalFileName->Length > 0) { + py_path = PyUnicode_FromWideChar(globalFileName->Buffer, + wcslen(globalFileName->Buffer)); + if (! py_path) + goto error; + if (PyList_Append(py_retlist, py_path)) + goto error; + Py_CLEAR(py_path); // also sets to NULL } -loop_cleanup: - Py_XDECREF(py_path); - py_path = NULL; - if (g_pNameBuffer != NULL) - FREE(g_pNameBuffer); - g_pNameBuffer = NULL; - g_dwSize = 0; - g_dwLength = 0; - if (g_hFile != NULL) - CloseHandle(g_hFile); - g_hFile = NULL; -} - -cleanup: - if (g_pNameBuffer != NULL) - FREE(g_pNameBuffer); - g_pNameBuffer = NULL; - g_dwSize = 0; - g_dwLength = 0; + // Loop cleanup section. + FREE(globalFileName); + globalFileName = NULL; + CloseHandle(hFile); + hFile = NULL; + } - if (g_hFile != NULL) - CloseHandle(g_hFile); - g_hFile = NULL; + goto exit; - if (pHandleInfo != NULL) - FREE(pHandleInfo); - pHandleInfo = NULL; +error: + Py_XDECREF(py_retlist); + errorOccurred = TRUE; + goto exit; - if (error) { - Py_XDECREF(py_retlist); - py_retlist = NULL; +exit: + if (hFile != NULL) + CloseHandle(hFile); + if (globalFileName != NULL) { + FREE(globalFileName); + globalFileName = NULL; } - - LeaveCriticalSection(&g_cs); + if (py_path != NULL) + Py_DECREF(py_path); + if (handlesList != NULL) + FREE(handlesList); + + LeaveCriticalSection(&PSUTIL_CRITICAL_SECTION); + if (errorOccurred == TRUE) + return NULL; return py_retlist; } diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 659f6a6ae7..ec1aeb1a91 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -238,7 +238,7 @@ def test_cpu_count(self): @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq(self): - self.assert_ntuple_of_nums(psutil.cpu_freq()) + self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int)) def test_disk_io_counters(self): # Duplicate of test_system.py. Keep it anyway. From 4526fb11f18f361f4afc11484c768875e69de1a4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 11 Jan 2020 10:46:23 -0800 Subject: [PATCH 0444/1714] #1652: remove win XP code path checking avilability of GetTickCount64 --- psutil/_psutil_common.c | 14 ++++++++------ psutil/_psutil_windows.c | 12 +----------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 890d515f68..1b83f75088 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -281,14 +281,16 @@ psutil_loadlibs() { "ntdll", "RtlNtStatusToDosErrorNoTeb"); if (! RtlNtStatusToDosErrorNoTeb) return 1; - - // --- Optional - // not available on Wine - RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv6AddressToStringA"); - // minimum requirement: Win Vista GetTickCount64 = psutil_GetProcAddress( "kernel32", "GetTickCount64"); + if (! GetTickCount64) + return 1; + RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv6AddressToStringA"); + if (! RtlIpv6AddressToStringA) + return 1; + + // --- Optional // minimum requirement: Win 7 GetActiveProcessorCount = psutil_GetProcAddress( "kernel32", "GetActiveProcessorCount"); diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index dd336489ee..d5928d72ce 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -101,17 +101,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { ll = (((ULONGLONG) (fileTime.dwHighDateTime)) << 32) + fileTime.dwLowDateTime; pt = (time_t)((ll - 116444736000000000ull) / 10000000ull); - - if (GetTickCount64 != NULL) { - // Windows >= Vista - uptime = GetTickCount64() / 1000ull; - } - else { - // Windows XP. - // GetTickCount() time will wrap around to zero if the - // system is run continuously for 49.7 days. - uptime = (ULONGLONG)GetTickCount() / 1000ull; - } + uptime = GetTickCount64() / 1000ull; return Py_BuildValue("K", pt - uptime); } From 0b21b19fb2ef008d3cdb9a82e494b1a024a5c14b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 11 Jan 2020 10:49:39 -0800 Subject: [PATCH 0445/1714] #1652 remove Windows Vista references --- psutil/__init__.py | 2 +- psutil/_psutil_windows.c | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index d33d55e8f8..ffeead8743 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -775,7 +775,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): diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index d5928d72ce..d11dbdc76b 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -456,14 +456,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; - // Here we differentiate between XP and Vista+ because - // QueryFullProcessImageNameW is better than GetProcessImageFileNameW - // (avoid using QueryDosDevice on the returned path), see: - // https://github.com/giampaolo/psutil/issues/1394 memset(exe, 0, MAX_PATH); if (QueryFullProcessImageNameW(hProcess, 0, exe, &size) == 0) { PyErr_SetFromOSErrnoWithSyscall("QueryFullProcessImageNameW"); From bab3a310ab561bfdd01d44b1d2121daac7469e8f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 13 Jan 2020 00:30:16 +0100 Subject: [PATCH 0446/1714] update setup.py classifiers --- setup.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 37af746ad4..774d566a9f 100755 --- a/setup.py +++ b/setup.py @@ -331,7 +331,6 @@ def main(): license='BSD', packages=['psutil', 'psutil.tests'], ext_modules=extensions, - # see: python setup.py register --list-classifiers classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', @@ -341,9 +340,16 @@ def main(): 'Intended Audience :: System Administrators', 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows :: Windows NT/2000', + 'Operating System :: Microsoft :: Windows :: Windows 10', + 'Operating System :: Microsoft :: Windows :: Windows 7', + 'Operating System :: Microsoft :: Windows :: Windows 8', + 'Operating System :: Microsoft :: Windows :: Windows 8.1', + 'Operating System :: Microsoft :: Windows :: Windows Server 2003', + 'Operating System :: Microsoft :: Windows :: Windows Server 2008', + 'Operating System :: Microsoft :: Windows :: Windows Vista', 'Operating System :: Microsoft', 'Operating System :: OS Independent', + 'Operating System :: POSIX :: AIX', 'Operating System :: POSIX :: BSD :: FreeBSD', 'Operating System :: POSIX :: BSD :: NetBSD', 'Operating System :: POSIX :: BSD :: OpenBSD', @@ -362,11 +368,14 @@ def main(): 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries', 'Topic :: System :: Benchmark', + 'Topic :: System :: Hardware :: Hardware Drivers', 'Topic :: System :: Hardware', 'Topic :: System :: Monitoring', + 'Topic :: System :: Networking :: Monitoring :: Hardware Watchdog', 'Topic :: System :: Networking :: Monitoring', 'Topic :: System :: Networking', 'Topic :: System :: Operating System', + 'Topic :: System :: Power (UPS)' 'Topic :: System :: Systems Administration', 'Topic :: Utilities', ], From 5140642a7ac95889222cbcc2a8118ca803f0ae3d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Jan 2020 03:09:25 -0800 Subject: [PATCH 0447/1714] print/set syscall origin when raising NSP or AD --- psutil/_psutil_aix.c | 2 +- psutil/_psutil_common.c | 24 ++++++++++++++---------- psutil/_psutil_osx.c | 8 ++++---- psutil/_psutil_posix.c | 2 +- psutil/_psutil_sunos.c | 2 +- psutil/_psutil_windows.c | 28 ++++++++++++++-------------- psutil/arch/freebsd/specific.c | 8 ++++---- psutil/arch/netbsd/specific.c | 10 +++++----- psutil/arch/openbsd/specific.c | 6 +++--- psutil/arch/osx/process_info.c | 6 +++--- psutil/arch/windows/process_info.c | 2 +- psutil/arch/windows/process_utils.c | 4 ++-- psutil/arch/windows/socks.c | 2 +- 13 files changed, 54 insertions(+), 50 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index d7fcaefcbb..b8584f26df 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -461,7 +461,7 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { /* finished iteration without finding requested pid */ free(processes); - return NoSuchProcess(""); + return NoSuchProcess("psutil_read_process_table (no PID found)"); } diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 1b83f75088..9075fda32b 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -69,14 +69,16 @@ PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { // ==================================================================== /* - * Set OSError(errno=ESRCH, strerror="No such process") Python exception. - * If msg != "" the exception message will change in accordance. + * Set OSError(errno=ESRCH, strerror="No such process (originated from") + * Python exception. */ PyObject * -NoSuchProcess(const char *msg) { +NoSuchProcess(const char *syscall) { PyObject *exc; - exc = PyObject_CallFunction( - PyExc_OSError, "(is)", ESRCH, strlen(msg) ? msg : strerror(ESRCH)); + char msg[1024]; + + sprintf(msg, "No such process (originated from %s)", syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", ESRCH, msg); PyErr_SetObject(PyExc_OSError, exc); Py_XDECREF(exc); return NULL; @@ -84,14 +86,16 @@ NoSuchProcess(const char *msg) { /* - * Set OSError(errno=EACCES, strerror="Permission denied") Python exception. - * If msg != "" the exception message will change in accordance. + * Set OSError(errno=EACCES, strerror="Permission denied" (originated from ...) + * Python exception. */ PyObject * -AccessDenied(const char *msg) { +AccessDenied(const char *syscall) { PyObject *exc; - exc = PyObject_CallFunction( - PyExc_OSError, "(is)", EACCES, strlen(msg) ? msg : strerror(EACCES)); + char msg[1024]; + + sprintf(msg, "Access denied (originated from %s)", syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", EACCES, msg); PyErr_SetObject(PyExc_OSError, exc); Py_XDECREF(exc); return NULL; diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 76ec0ee850..8d08612244 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -96,7 +96,7 @@ psutil_task_for_pid(long pid, mach_port_t *task) err = task_for_pid(mach_task_self(), (pid_t)pid, task); if (err != KERN_SUCCESS) { if (psutil_pid_exists(pid) == 0) - NoSuchProcess("task_for_pid() failed"); + NoSuchProcess("task_for_pid"); else if (psutil_is_zombie(pid) == 1) PyErr_SetString(ZombieProcessError, "task_for_pid() failed"); else { @@ -104,7 +104,7 @@ psutil_task_for_pid(long pid, mach_port_t *task) "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " "setting AccessDenied()", pid, err, errno, mach_error_string(err)); - AccessDenied("task_for_pid() failed"); + AccessDenied("task_for_pid"); } return 1; } @@ -298,7 +298,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { ret = proc_pidpath((pid_t)pid, &buf, sizeof(buf)); if (ret == 0) { if (pid == 0) - AccessDenied(""); + AccessDenied("automatically set for PID 0"); else psutil_raise_for_pid(pid, "proc_pidpath()"); return NULL; @@ -894,7 +894,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (err != KERN_SUCCESS) { // errcode 4 is "invalid argument" (access denied) if (err == 4) { - AccessDenied(""); + AccessDenied("task_info"); } else { // otherwise throw a runtime error with appropriate error code diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index aa60084917..7fbdcc4831 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -122,7 +122,7 @@ psutil_raise_for_pid(long pid, char *syscall_name) { else if (psutil_pid_exists(pid) == 0) { psutil_debug("%s syscall failed and PID %i no longer exists; " "assume NoSuchProcess", syscall_name, pid); - NoSuchProcess(""); + NoSuchProcess("psutil_pid_exists"); } else { PyErr_Format(PyExc_RuntimeError, "%s syscall failed", syscall_name); diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index bcfb448f5e..8aa7eadd39 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -275,7 +275,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { goto error; if (! info.pr_envp) { - AccessDenied(""); + AccessDenied("/proc/pid/psinfo struct not set"); goto error; } diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index d11dbdc76b..d19e32e982 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -174,7 +174,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; if (pid == 0) - return AccessDenied(""); + return AccessDenied("automatically set for PID 0"); hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); if (hProcess == NULL) { @@ -182,7 +182,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { // see https://github.com/giampaolo/psutil/issues/24 psutil_debug("OpenProcess -> ERROR_INVALID_PARAMETER turned " "into NoSuchProcess"); - NoSuchProcess(""); + NoSuchProcess("OpenProcess"); } else { PyErr_SetFromWindowsErr(0); @@ -218,7 +218,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "ll", &pid, &timeout)) return NULL; if (pid == 0) - return AccessDenied(""); + return AccessDenied("automatically set for PID 0"); hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, pid); @@ -296,7 +296,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { if (GetLastError() == ERROR_ACCESS_DENIED) { // usually means the process has died so we throw a NoSuchProcess // here - NoSuchProcess(""); + NoSuchProcess("GetProcessTimes"); } else { PyErr_SetFromWindowsErr(0); @@ -351,7 +351,7 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { if (GetLastError() == ERROR_ACCESS_DENIED) { // usually means the process has died so we throw a // NoSuchProcess here - NoSuchProcess(""); + NoSuchProcess("GetProcessTimes"); } else { PyErr_SetFromWindowsErr(0); @@ -372,7 +372,7 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { CloseHandle(hProcess); if (ret != 0) { if (exitCode != STILL_ACTIVE) - return NoSuchProcess(""); + return NoSuchProcess("GetExitCodeProcess"); } else { // Ignore access denied as it means the process is still alive. @@ -412,7 +412,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess(""); + return NoSuchProcess("psutil_pid_is_running"); if (pid_return == -1) return NULL; @@ -436,7 +436,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess(""); + return NoSuchProcess("psutil_pid_is_running"); if (pid_return == -1) return NULL; @@ -507,7 +507,7 @@ psutil_proc_name(PyObject *self, PyObject *args) { } CloseHandle(hSnapShot); - NoSuchProcess(""); + NoSuchProcess("CreateToolhelp32Snapshot loop (no PID found)"); return NULL; } @@ -605,10 +605,10 @@ psutil_GetProcWsetInformation( if (!NT_SUCCESS(status)) { if (status == STATUS_ACCESS_DENIED) { - AccessDenied("originated from NtQueryVirtualMemory"); + AccessDenied("NtQueryVirtualMemory"); } else if (psutil_pid_is_running(pid) == 0) { - NoSuchProcess(""); + NoSuchProcess("psutil_pid_is_running"); } else { PyErr_Clear(); @@ -711,7 +711,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess(""); + return NoSuchProcess("psutil_pid_is_running"); if (pid_return == -1) return NULL; @@ -770,13 +770,13 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (pid == 0) { // raise AD instead of returning 0 as procexp is able to // retrieve useful information somehow - AccessDenied(""); + AccessDenied("automatically set for PID 0"); goto error; } pid_return = psutil_pid_is_running(pid); if (pid_return == 0) { - NoSuchProcess(""); + NoSuchProcess("psutil_pid_is_running"); goto error; } if (pid_return == -1) diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index bc267acd97..90ea81e84f 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -61,7 +61,7 @@ psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc) { // sysctl stores 0 in the size if we can't find the process information. if (size == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (size = 0)"); return -1; } return 0; @@ -301,7 +301,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (ret == -1) return NULL; else if (ret == 0) - return NoSuchProcess(""); + return NoSuchProcess("psutil_pid_exists"); else strcpy(pathname, ""); } @@ -358,7 +358,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } if (size == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (size = 0)"); goto error; } @@ -374,7 +374,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } if (size == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (size = 0)"); goto error; } diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 681b237751..62e09325ab 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -72,7 +72,7 @@ psutil_kinfo_proc(pid_t pid, kinfo_proc *proc) { } // sysctl stores 0 in the size if we can't find the process information. if (size == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (size = 0)"); return -1; } return 0; @@ -139,7 +139,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { free(buf); if (len == -1) { if (errno == ENOENT) - NoSuchProcess(""); + NoSuchProcess("readlink (ENOENT)"); else PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -195,7 +195,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (ret == -1) return NULL; else if (ret == 0) - return NoSuchProcess(""); + return NoSuchProcess("psutil_pid_exists"); else strcpy(pathname, ""); } @@ -247,7 +247,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } if (size == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (size = 0)"); goto error; } @@ -264,7 +264,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } if (size == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (size = 0)"); goto error; } diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/specific.c index 33ebdeecba..f8d3cf9677 100644 --- a/psutil/arch/openbsd/specific.c +++ b/psutil/arch/openbsd/specific.c @@ -67,7 +67,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { } // sysctl stores 0 in the size if we can't find the process information. if (size == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (size = 0)"); return -1; } return 0; @@ -242,7 +242,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { kd = kvm_openfiles(0, 0, 0, O_RDONLY, errbuf); if (! kd) { if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(""); + AccessDenied("kvm_openfiles"); else PyErr_Format(PyExc_RuntimeError, "kvm_openfiles() syscall failed"); goto error; @@ -253,7 +253,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { sizeof(*kp), &nentries); if (! kp) { if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(""); + AccessDenied("kvm_getprocs"); else PyErr_Format(PyExc_RuntimeError, "kvm_getprocs() syscall failed"); goto error; diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 9877fd6b13..47cf864f2c 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -167,7 +167,7 @@ psutil_get_cmdline(long pid) { // In case of zombie process we'll get EINVAL. We translate it // to NSP and _psosx.py will translate it to ZP. if ((errno == EINVAL) && (psutil_pid_exists(pid))) - NoSuchProcess(""); + NoSuchProcess("sysctl"); else PyErr_SetFromErrno(PyExc_OSError); goto error; @@ -259,7 +259,7 @@ psutil_get_environ(long pid) { // In case of zombie process we'll get EINVAL. We translate it // to NSP and _psosx.py will translate it to ZP. if ((errno == EINVAL) && (psutil_pid_exists(pid))) - NoSuchProcess(""); + NoSuchProcess("sysctl"); else PyErr_SetFromErrno(PyExc_OSError); goto error; @@ -359,7 +359,7 @@ psutil_get_kinfo_proc(long pid, struct kinfo_proc *kp) { // sysctl succeeds but len is zero, happens when process has gone away if (len == 0) { - NoSuchProcess(""); + NoSuchProcess("sysctl (len == 0)"); return -1; } return 0; diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 1c5a3c338d..64ff0f0da4 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -636,7 +636,7 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, } } while ((process = PSUTIL_NEXT_PROCESS(process))); - NoSuchProcess(""); + NoSuchProcess("NtQuerySystemInformation (no PID found)"); goto error; error: diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index fd516beae9..f6867ca177 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -142,7 +142,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid) { return hProcess; } else if (ret == 0) { - return NoSuchProcess(""); + return NoSuchProcess("psutil_is_phandle_running"); } else if (ret == -1) { if (GetLastError() == ERROR_ACCESS_DENIED) @@ -169,7 +169,7 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { if (pid == 0) { // otherwise we'd get NoSuchProcess - return AccessDenied(""); + return AccessDenied("automatically set for PID 0"); } // needed for GetExitCodeProcess access |= PROCESS_QUERY_LIMITED_INFORMATION; diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index b316ddb85b..5272e127b7 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -151,7 +151,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) { psutil_conn_decref_objs(); - return NoSuchProcess(""); + return NoSuchProcess("psutil_pid_is_running"); } else if (pid_return == -1) { psutil_conn_decref_objs(); From d9497930fc7a77e17a5aace07592c0b1d1701ba9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Jan 2020 03:21:57 -0800 Subject: [PATCH 0448/1714] exec make install before 2 targets --- Makefile | 2 ++ scripts/internal/winmake.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 71f9300107..c86736f996 100644 --- a/Makefile +++ b/Makefile @@ -255,9 +255,11 @@ print-timeline: ## Print releases' timeline. @$(PYTHON) scripts/internal/print_timeline.py print-access-denied: ## Print AD exceptions + ${MAKE} install @$(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls + ${MAKE} install @$(PYTHON) scripts/internal/print_api_speed.py $(ARGS) # =================================================================== diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 69a1bfbe27..6349215a8e 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -504,11 +504,13 @@ def bench_oneshot_2(): def print_access_denied(): """Print AD exceptions raised by all Process methods.""" + install() sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) def print_api_speed(): """Benchmark all API calls.""" + install() sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) From 346999344bf94084e9bed09457abe18260d3bfd1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Jan 2020 04:06:52 -0800 Subject: [PATCH 0449/1714] check MALLOC_ZERO ret code --- psutil/_psutil_windows.c | 6 ----- psutil/arch/windows/process_handles.c | 32 +++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index d19e32e982..3bd07f686f 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -871,12 +871,6 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { return NULL; py_retlist = psutil_get_open_files(pid, processHandle); - if (py_retlist == NULL) { - PyErr_SetFromWindowsErr(0); - CloseHandle(processHandle); - return NULL; - } - CloseHandle(processHandle); return py_retlist; } diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 4566baffa7..4b230595e4 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -40,6 +40,10 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { bufferSize = initialBufferSize; buffer = MALLOC_ZERO(bufferSize); + if (buffer == NULL) { + PyErr_NoMemory(); + return 1; + } while ((status = NtQuerySystemInformation( SystemExtendedHandleInformation, @@ -60,6 +64,10 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { } buffer = MALLOC_ZERO(bufferSize); + if (buffer == NULL) { + PyErr_NoMemory(); + return 1; + } } if (! NT_SUCCESS(status)) { @@ -82,9 +90,15 @@ psutil_get_filename(LPVOID lpvParam) { bufferSize = 0x200; globalFileName = MALLOC_ZERO(bufferSize); + if (globalFileName == NULL) { + PyErr_NoMemory(); + goto error; + } + // Note: also this is supposed to hang, hence why we do it in here. if (GetFileType(hFile) != FILE_TYPE_DISK) { + SetLastError(0); globalFileName->Length = 0; return 0; } @@ -105,6 +119,10 @@ psutil_get_filename(LPVOID lpvParam) { { FREE(globalFileName); globalFileName = MALLOC_ZERO(bufferSize); + if (globalFileName == NULL) { + PyErr_NoMemory(); + goto error; + } } else { break; @@ -114,10 +132,18 @@ psutil_get_filename(LPVOID lpvParam) { if (! NT_SUCCESS(status)) { PyErr_SetFromOSErrnoWithSyscall("NtQuerySystemInformation"); FREE(globalFileName); + globalFileName = NULL; return 1; } return 0; + +error: + if (globalFileName != NULL) { + FREE(globalFileName); + globalFileName = NULL; + } + return 1; } @@ -209,8 +235,10 @@ psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { } // Loop cleanup section. - FREE(globalFileName); - globalFileName = NULL; + if (globalFileName != NULL) { + FREE(globalFileName); + globalFileName = NULL; + } CloseHandle(hFile); hFile = NULL; } From bc0168ad4a5467ab369f120e646fda811175b210 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Jan 2020 12:49:26 -0800 Subject: [PATCH 0450/1714] properly cleanup C thread --- Makefile | 4 +-- psutil/_psutil_windows.c | 7 +++--- psutil/arch/windows/process_handles.c | 35 ++++++++++++++++++++++----- psutil/tests/__init__.py | 2 +- scripts/internal/winmake.py | 2 ++ 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index c86736f996..94fa3b24d4 100644 --- a/Makefile +++ b/Makefile @@ -256,11 +256,11 @@ print-timeline: ## Print releases' timeline. print-access-denied: ## Print AD exceptions ${MAKE} install - @$(PYTHON) scripts/internal/print_access_denied.py + @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls ${MAKE} install - @$(PYTHON) scripts/internal/print_api_speed.py $(ARGS) + @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) # =================================================================== # Misc diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 3bd07f686f..9985e1d3c3 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -581,7 +581,7 @@ psutil_GetProcWsetInformation( SIZE_T bufferSize; bufferSize = 0x8000; - buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize); + buffer = MALLOC_ZERO(bufferSize); while ((status = NtQueryVirtualMemory( hProcess, @@ -591,16 +591,15 @@ psutil_GetProcWsetInformation( bufferSize, NULL)) == STATUS_INFO_LENGTH_MISMATCH) { - HeapFree(GetProcessHeap(), 0, buffer); + FREE(buffer); bufferSize *= 2; - psutil_debug("NtQueryVirtualMemory increase bufsize %i", bufferSize); // Fail if we're resizing the buffer to something very large. if (bufferSize > 256 * 1024 * 1024) { PyErr_SetString(PyExc_RuntimeError, "NtQueryVirtualMemory bufsize is too large"); return 1; } - buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize); + buffer = MALLOC_ZERO(bufferSize); } if (!NT_SUCCESS(status)) { diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 4b230595e4..e5dd74a856 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -166,20 +166,43 @@ psutil_threaded_get_filename(HANDLE hFile) { // If the thread hangs, kill it and cleanup. if (dwWait == WAIT_TIMEOUT) { psutil_debug( - "get file name thread timed out after %i ms", THREAD_TIMEOUT); - TerminateThread(hThread, 1); + "get handle name thread timed out after %i ms", THREAD_TIMEOUT); + if (TerminateThread(hThread, 0) == 0) { + PyErr_SetFromOSErrnoWithSyscall("TerminateThread"); + CloseHandle(hThread); + return 1; + } CloseHandle(hThread); return 0; } - else { - if (GetExitCodeThread(hThread, &threadRetValue) == 0) { + + if (dwWait == WAIT_FAILED) { + psutil_debug("WaitForSingleObject -> WAIT_FAILED"); + if (TerminateThread(hThread, 0) == 0) { + PyErr_SetFromOSErrnoWithSyscall( + "WaitForSingleObject -> WAIT_FAILED -> TerminateThread"); CloseHandle(hThread); - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeThread"); return 1; } + PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); CloseHandle(hThread); - return threadRetValue; + return 1; + } + + if (GetExitCodeThread(hThread, &threadRetValue) == 0) { + if (TerminateThread(hThread, 0) == 0) { + PyErr_SetFromOSErrnoWithSyscall( + "GetExitCodeThread (failed) -> TerminateThread"); + CloseHandle(hThread); + return 1; + } + + CloseHandle(hThread); + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeThread"); + return 1; } + CloseHandle(hThread); + return threadRetValue; } diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 3931f1c680..2844360d7c 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -127,7 +127,7 @@ # 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 +GLOBAL_TIMEOUT = 5 # be more tolerant if we're on travis / appveyor in order to avoid # false positives if TRAVIS or APPVEYOR: diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 6349215a8e..ac08c03f56 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -505,12 +505,14 @@ def bench_oneshot_2(): def print_access_denied(): """Print AD exceptions raised by all Process methods.""" install() + test_setup() sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) def print_api_speed(): """Benchmark all API calls.""" install() + test_setup() sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) From efaa9e0169e790310e19cea1c4d8389395387c0d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Jan 2020 13:02:55 -0800 Subject: [PATCH 0451/1714] fix #1662: QueryFullProcessImageNameW may fail with error code = 0 (Win API bug?) --- HISTORY.rst | 1 + psutil/_psutil_windows.c | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index f51553a03d..4ca36b8186 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,7 @@ XXXX-XX-XX - 1656_: [Windows] Process.memory_full_info() raises AccessDenied even for the current user and os.getpid(). - 1660_: [Windows] Process.open_files() complete rewrite + check of errors. +- 1662_: [Windows] process exe() may raise WinError 0. 5.6.7 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 9985e1d3c3..510bde8a9a 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -463,7 +463,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { memset(exe, 0, MAX_PATH); if (QueryFullProcessImageNameW(hProcess, 0, exe, &size) == 0) { - PyErr_SetFromOSErrnoWithSyscall("QueryFullProcessImageNameW"); + // https://github.com/giampaolo/psutil/issues/1662 + if (GetLastError() == 0) + AccessDenied("QueryFullProcessImageNameW (forced EPERM)"); + else + PyErr_SetFromOSErrnoWithSyscall("QueryFullProcessImageNameW"); CloseHandle(hProcess); return NULL; } From 678292f90c6961a0d0fe9f5bd80f7fbde8f79e85 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 16 Jan 2020 11:12:31 +0100 Subject: [PATCH 0452/1714] AD script: print AD percentage + elapsed time --- HISTORY.rst | 2 +- psutil/tests/test_contracts.py | 2 +- scripts/internal/print_access_denied.py | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4ca36b8186..4841d26e3c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ XXXX-XX-XX **Enhancements** +- 1637_: [SunOS] Add partial support for old SunOS 5.10 Update 0 to 3. - 1648_: [Linux] sensors_temperatures() looks into an additional /sys/device/ directory for additional data. (patch by Javad Karabi) - 1652_: [Windows] dropped support for Windows XP and Windows Server 2003. @@ -14,7 +15,6 @@ XXXX-XX-XX **Bug fixes** -- 1637_: [SunOS] Add partial support for old SunOS 5.10 Update 0 to 3. - 1642_: [SunOS] querying basic info for PID 0 results in FileNotFoundError. - 1646_: [FreeBSD] many Process methods may cause a segfault on FreeBSD 12.0 due to a backward incompatible change in a C type introduced in 12.0. diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index ec1aeb1a91..509ffc0cb9 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -238,7 +238,7 @@ def test_cpu_count(self): @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq(self): - self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int)) + 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. diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 1519c94b17..b94e6e00c4 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -47,6 +47,7 @@ from __future__ import print_function, division from collections import defaultdict +import time import psutil from scriptutils import hilite @@ -59,6 +60,7 @@ def main(): tot_calls = 0 signaler = object() d = defaultdict(int) + start = time.time() for p in psutil.process_iter(attrs=[], ad_value=signaler): tot_procs += 1 for methname, value in p.info.items(): @@ -68,6 +70,7 @@ def main(): d[methname] += 1 else: d[methname] += 0 + elapsed = time.time() - start # print templ = "%-20s %-5s %-9s %s" @@ -79,9 +82,11 @@ def main(): s = templ % (methname, ads, "%6.1f%%" % perc, outcome) s = hilite(s, ok=not ads) print(s) + tot_perc = round((tot_ads / tot_calls) * 100, 1) print("-" * 50) - print("Totals: access-denied=%s, calls=%s, processes=%s" % ( - tot_ads, tot_calls, tot_procs)) + print("Totals: access-denied=%s (%s%%), calls=%s, processes=%s, " + "elapsed=%ss" % (tot_ads, tot_perc, tot_calls, tot_procs, + round(elapsed, 2))) if __name__ == '__main__': From 2e0952e939d6ab517449314876d8d3488ba5b98b Mon Sep 17 00:00:00 2001 From: Mike Hommey Date: Fri, 17 Jan 2020 20:42:04 +0900 Subject: [PATCH 0453/1714] Future-proof disk_io_counters on Linux. (#1665) Kernel 5.5 added 2 more fields to /proc/diskstats, requiring another change after the one for 4.18, which recently added 4 fields. At this point in time, the meaning of the existing fields is unlikely to change, and psutil is not using any of the newer ones. By considering 18 fields and more to have the current layout, psutil will continue to work as newer kernels add more fields. --- psutil/_pslinux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index c681439d4b..d8f8ed5c82 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1066,6 +1066,7 @@ def read_procfs(): # "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 @@ -1080,7 +1081,7 @@ def read_procfs(): reads = int(fields[2]) (reads_merged, rbytes, rtime, writes, writes_merged, wbytes, wtime, _, busy_time, _) = map(int, fields[4:14]) - elif flen == 14 or flen == 18: + 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, From 5f16b3a3c498c89dd11d03b3db1592b2332b5cbf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 18 Jan 2020 11:33:21 +0100 Subject: [PATCH 0454/1714] update HISTORY --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index 988af1fe25..b2234ce311 100644 --- a/CREDITS +++ b/CREDITS @@ -656,3 +656,7 @@ I: 1646 N: Javad Karabi W: https://github.com/karabijavad I: 1648 + +N: Mike Hommey +W: https://github.com/glandium +I: 1665 diff --git a/HISTORY.rst b/HISTORY.rst index 4841d26e3c..96268d98b8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,8 @@ XXXX-XX-XX current user and os.getpid(). - 1660_: [Windows] Process.open_files() complete rewrite + check of errors. - 1662_: [Windows] process exe() may raise WinError 0. +- 1665_: [Linux] disk_io_counters() does not take into account extra fields + added to recent kernels. (patch by Mike Hommey) 5.6.7 ===== From ef28caec6c276c76c771c0bad03aea80cd765866 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 18 Jan 2020 04:04:07 -0800 Subject: [PATCH 0455/1714] Add *new_only* parameter for process_iter() (#1667) --- CREDITS | 4 ++++ HISTORY.rst | 5 ++++- docs/index.rst | 35 +++++++++++++++-------------------- psutil/__init__.py | 12 ++++++++---- psutil/tests/test_system.py | 18 +++++++++++++++++- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/CREDITS b/CREDITS index 988af1fe25..b2234ce311 100644 --- a/CREDITS +++ b/CREDITS @@ -656,3 +656,7 @@ I: 1646 N: Javad Karabi W: https://github.com/karabijavad I: 1648 + +N: Mike Hommey +W: https://github.com/glandium +I: 1665 diff --git a/HISTORY.rst b/HISTORY.rst index 4841d26e3c..28a5ce134f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.6.8 (unreleased) +5.7.0 (unreleased) ================== XXXX-XX-XX @@ -12,6 +12,7 @@ XXXX-XX-XX directory for additional data. (patch by Javad Karabi) - 1652_: [Windows] dropped support for Windows XP and Windows Server 2003. Minimum supported Windows version now is Windows Vista. +- 1667_: added process_iter(new_only=True) parameter. **Bug fixes** @@ -22,6 +23,8 @@ XXXX-XX-XX current user and os.getpid(). - 1660_: [Windows] Process.open_files() complete rewrite + check of errors. - 1662_: [Windows] process exe() may raise WinError 0. +- 1665_: [Linux] disk_io_counters() does not take into account extra fields + added to recent kernels. (patch by Mike Hommey) 5.6.7 ===== diff --git a/docs/index.rst b/docs/index.rst index d69e1fac7b..b1c4ab7411 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -855,7 +855,7 @@ Functions .. versionchanged:: 5.6.0 PIDs are returned in sorted order -.. function:: process_iter(attrs=None, ad_value=None) +.. function:: process_iter(attrs=None, ad_value=None, new_only=False) Return an iterator yielding a :class:`Process` class instance for all running processes on the local machine. @@ -871,24 +871,10 @@ Functions the resulting dict is stored as a ``info`` attribute which is attached to the returned :class:`Process` instances. If *attrs* is an empty list it will retrieve all process info (slow). + If *new_only* is true this function will yield only new processes which + appeared since the last time it was called. Example usage:: - >>> import psutil - >>> for proc in psutil.process_iter(): - ... try: - ... pinfo = proc.as_dict(attrs=['pid', 'name', 'username']) - ... except psutil.NoSuchProcess: - ... pass - ... else: - ... print(pinfo) - ... - {'name': 'systemd', 'pid': 1, 'username': 'root'} - {'name': 'kthreadd', 'pid': 2, 'username': 'root'} - {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} - ... - - More compact version using *attrs* parameter:: - >>> import psutil >>> for proc in psutil.process_iter(attrs=['pid', 'name', 'username']): ... print(proc.info) @@ -909,19 +895,28 @@ Functions 3: {'name': 'ksoftirqd/0', 'username': 'root'}, ...} - Example showing how to filter processes by name:: + Example showing how to filter processes by name (see also + `process filtering <#filtering-and-sorting-processes>`__ section for more + examples):: >>> import psutil >>> [p.info for p in psutil.process_iter(attrs=['pid', 'name']) if 'python' in p.info['name']] [{'name': 'python3', 'pid': 21947}, {'name': 'python', 'pid': 23835}] - See also `process filtering <#filtering-and-sorting-processes>`__ section for - more examples. + Get new processes only (since last call):: + + >>> import psutil + >>> for proc in psutil.process_iter(attrs=['pid', 'name'], new_only=True): + ... print(proc.info) + ... .. versionchanged:: 5.3.0 added "attrs" and "ad_value" parameters. + .. versionchanged:: + 5.7.0 added "new_only" parameter. + .. function:: pid_exists(pid) Check whether the given PID exists in the current process list. This is diff --git a/psutil/__init__.py b/psutil/__init__.py index ffeead8743..e74bb109ce 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -227,7 +227,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.6.8" +__version__ = "5.7.0" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -1407,7 +1407,7 @@ def pid_exists(pid): _lock = threading.Lock() -def process_iter(attrs=None, ad_value=None): +def process_iter(attrs=None, ad_value=None, new_only=False): """Return a generator yielding a Process instance for all running processes. @@ -1427,6 +1427,8 @@ def process_iter(attrs=None, ad_value=None): to returned Process instance. If *attrs* is an empty list it will retrieve all process info (slow). + If *new_only* is true this function will yield only new processes + which appeared since the last time it was called. """ def add(pid): proc = Process(pid) @@ -1448,8 +1450,10 @@ def remove(pid): remove(pid) with _lock: - ls = sorted(list(_pmap.items()) + - list(dict.fromkeys(new_pids).items())) + ls = list(dict.fromkeys(new_pids).items()) + if not new_only: + ls += list(_pmap.items()) + ls.sort() for pid, proc in ls: try: diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index e04e120b33..f2d072824e 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -85,7 +85,7 @@ def test_process_iter(self): with self.assertRaises(psutil.AccessDenied): list(psutil.process_iter()) - def test_prcess_iter_w_params(self): + 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): @@ -105,6 +105,22 @@ def test_prcess_iter_w_params(self): self.assertGreaterEqual(p.info['pid'], 0) assert m.called + def test_process_iter_new_only(self): + ls1 = list(psutil.process_iter(attrs=['pid'])) + ls2 = list(psutil.process_iter(attrs=['pid'], new_only=True)) + self.assertGreater(len(ls1), len(ls2)) + # assume no more than 3 new processes were created in the meantime + self.assertIn(len(ls2), [0, 1, 2, 3, 4, 5]) + + sproc = get_test_subprocess() + ls = list(psutil.process_iter(attrs=['pid'], new_only=True)) + self.assertIn(len(ls2), [0, 1, 2, 3, 4, 5]) + for p in ls: + if p.pid == sproc.pid: + break + else: + self.fail("subprocess not found") + def test_wait_procs(self): def callback(p): pids.append(p.pid) From 33fea55c3e89a38e668675ed3971c923e81fafc3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 18 Jan 2020 14:43:23 +0100 Subject: [PATCH 0456/1714] set proper NTSTATUS error code --- psutil/arch/windows/process_handles.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index e5dd74a856..f63d4af36c 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -130,7 +130,7 @@ psutil_get_filename(LPVOID lpvParam) { } while (--attempts); if (! NT_SUCCESS(status)) { - PyErr_SetFromOSErrnoWithSyscall("NtQuerySystemInformation"); + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); FREE(globalFileName); globalFileName = NULL; return 1; From f76582181e8d3b7c36900eededf904fc4b4f5f89 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 18 Jan 2020 11:22:08 -0800 Subject: [PATCH 0457/1714] [Windows] psutil_handle_from_pid() refactoring (#1668) --- psutil/arch/windows/process_utils.c | 212 +++++++--------------------- 1 file changed, 52 insertions(+), 160 deletions(-) diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index f6867ca177..dbdebd484f 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -52,9 +52,8 @@ psutil_get_pids(DWORD *numberOfReturnedPIDs) { return procArray; } -/* - * Return 1 if PID exists, 0 if not, -1 on error. - */ + +// Return 1 if PID exists, 0 if not, -1 on error. int psutil_pid_in_pids(DWORD pid) { DWORD *proclist = NULL; @@ -75,94 +74,53 @@ psutil_pid_in_pids(DWORD pid) { } -/* - * Given a process HANDLE checks whether it's actually running. - * Returns: - * - 1: running - * - 0: not running - * - -1: WindowsError - * - -2: AssertionError - */ -int -psutil_is_phandle_running(HANDLE hProcess, DWORD pid) { - DWORD processExitCode = 0; +// Given a process handle checks whether it's actually running. If it +// does return the handle, else return NULL with Python exception set. +// This is needed because OpenProcess API sucks. +HANDLE +psutil_check_phandle(HANDLE hProcess, DWORD pid) { + DWORD exitCode; if (hProcess == NULL) { if (GetLastError() == ERROR_INVALID_PARAMETER) { // Yeah, this is the actual error code in case of // "no such process". - if (! psutil_assert_pid_not_exists( - pid, "iphr: OpenProcess() -> ERROR_INVALID_PARAMETER")) { - return -2; - } - return 0; + NoSuchProcess("OpenProcess"); + return NULL; } - return -1; + PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + return NULL; } - if (GetExitCodeProcess(hProcess, &processExitCode)) { + if (GetExitCodeProcess(hProcess, &exitCode)) { // XXX - maybe STILL_ACTIVE is not fully reliable as per: // http://stackoverflow.com/questions/1591342/#comment47830782_1591379 - if (processExitCode == STILL_ACTIVE) { - if (! psutil_assert_pid_exists( - pid, "iphr: GetExitCodeProcess() -> STILL_ACTIVE")) { - return -2; - } - return 1; + if (exitCode == STILL_ACTIVE) { + return hProcess; } - else { - // We can't be sure so we look into pids. - if (psutil_pid_in_pids(pid) == 1) { - return 1; - } - else { - CloseHandle(hProcess); - return 0; - } + if (psutil_pid_in_pids(pid) == 1) { + return hProcess; } + CloseHandle(hProcess); + NoSuchProcess("GetExitCodeProcess != STILL_ACTIVE"); + return NULL; } - CloseHandle(hProcess); - if (! psutil_assert_pid_not_exists( pid, "iphr: exit fun")) { - return -2; - } - return -1; -} - - -/* - * Given a process HANDLE checks whether it's actually running and if - * it does return it, else return NULL with the proper Python exception - * set. - */ -HANDLE -psutil_check_phandle(HANDLE hProcess, DWORD pid) { - int ret = psutil_is_phandle_running(hProcess, pid); - if (ret == 1) { + if (GetLastError() == ERROR_ACCESS_DENIED) { + psutil_debug("GetExitCodeProcess -> ERROR_ACCESS_DENIED (ignored)"); + SetLastError(0); return hProcess; } - else if (ret == 0) { - return NoSuchProcess("psutil_is_phandle_running"); - } - else if (ret == -1) { - if (GetLastError() == ERROR_ACCESS_DENIED) - return PyErr_SetFromWindowsErr(0); - else - return PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); - } - else { - return NULL; - } + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + CloseHandle(hProcess); + return NULL; } -/* - * A wrapper around OpenProcess setting NSP exception if process - * no longer exists. - * "pid" is the process pid, "dwDesiredAccess" is the first argument - * exptected by OpenProcess. - * Return a process handle or NULL. - */ +// A wrapper around OpenProcess setting NSP exception if process no +// longer exists. *pid* is the process PID, *access* is the first +// argument to OpenProcess. +// Return a process handle or NULL with exception set. HANDLE psutil_handle_from_pid(DWORD pid, DWORD access) { HANDLE hProcess; @@ -171,113 +129,47 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { // otherwise we'd get NoSuchProcess return AccessDenied("automatically set for PID 0"); } - // needed for GetExitCodeProcess - access |= PROCESS_QUERY_LIMITED_INFORMATION; - hProcess = OpenProcess(access, FALSE, pid); - return psutil_check_phandle(hProcess, pid); -} + hProcess = OpenProcess(access, FALSE, pid); -int -psutil_assert_pid_exists(DWORD pid, char *err) { - if (PSUTIL_TESTING) { - if (psutil_pid_in_pids(pid) == 0) { - PyErr_SetString(PyExc_AssertionError, err); - return 0; - } + if ((hProcess == NULL) && (GetLastError() == ERROR_ACCESS_DENIED)) { + PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + return NULL; } - return 1; -} - -int -psutil_assert_pid_not_exists(DWORD pid, char *err) { - if (PSUTIL_TESTING) { - if (psutil_pid_in_pids(pid) == 1) { - PyErr_SetString(PyExc_AssertionError, err); - return 0; - } - } - return 1; + hProcess = psutil_check_phandle(hProcess, pid); + return hProcess; } -/* -/* Check for PID existance by using OpenProcess() + GetExitCodeProcess. -/* Returns: - * 1: pid exists - * 0: it doesn't - * -1: error - */ +// Check for PID existance. Return 1 if pid exists, 0 if not, -1 on error. int psutil_pid_is_running(DWORD pid) { HANDLE hProcess; - DWORD exitCode; - DWORD err; // Special case for PID 0 System Idle Process if (pid == 0) return 1; if (pid < 0) return 0; + return psutil_pid_in_pids(pid); + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - if (NULL == hProcess) { - err = GetLastError(); - // Yeah, this is the actual error code in case of "no such process". - if (err == ERROR_INVALID_PARAMETER) { - if (! psutil_assert_pid_not_exists( - pid, "pir: OpenProcess() -> INVALID_PARAMETER")) { - return -1; - } - return 0; - } - // Access denied obviously means there's a process to deny access to. - else if (err == ERROR_ACCESS_DENIED) { - if (! psutil_assert_pid_exists( - pid, "pir: OpenProcess() ACCESS_DENIED")) { - return -1; - } - return 1; - } - // Be strict and raise an exception; the caller is supposed - // to take -1 into account. - else { - PyErr_SetFromOSErrnoWithSyscall("OpenProcess(PROCESS_VM_READ)"); - return -1; - } - } - if (GetExitCodeProcess(hProcess, &exitCode)) { + // Access denied means there's a process to deny access to. + if ((hProcess == NULL) && (GetLastError() == ERROR_ACCESS_DENIED)) + return 1; + + hProcess = psutil_check_phandle(hProcess, pid); + if (hProcess != NULL) { CloseHandle(hProcess); - // XXX - maybe STILL_ACTIVE is not fully reliable as per: - // http://stackoverflow.com/questions/1591342/#comment47830782_1591379 - if (exitCode == STILL_ACTIVE) { - if (! psutil_assert_pid_exists( - pid, "pir: GetExitCodeProcess() -> STILL_ACTIVE")) { - return -1; - } - return 1; - } - // We can't be sure so we look into pids. - else { - return psutil_pid_in_pids(pid); - } + return 1; } - else { - err = GetLastError(); - CloseHandle(hProcess); - // Same as for OpenProcess, assume access denied means there's - // a process to deny access to. - if (err == ERROR_ACCESS_DENIED) { - if (! psutil_assert_pid_exists( - pid, "pir: GetExitCodeProcess() -> ERROR_ACCESS_DENIED")) { - return -1; - } - return 1; - } - else { - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); - return -1; - } + + CloseHandle(hProcess); + if ((PSUTIL_TESTING) && (psutil_pid_in_pids(pid) == 1)) { + PyErr_SetString(PyExc_AssertionError, "NULL handle but PID exists"); + return -1; } + return 0; } From 2093cd01e93f43beb467bf00ce7f9b7d694e25ae Mon Sep 17 00:00:00 2001 From: Caleb Bassi Date: Sun, 19 Jan 2020 12:40:13 -0800 Subject: [PATCH 0458/1714] Remove links to abandoned psutil ports in readme (#1669) --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 639dfb068a..ce859bd978 100644 --- a/README.rst +++ b/README.rst @@ -168,9 +168,7 @@ Portings - Go: https://github.com/shirou/gopsutil - C: https://github.com/hamon-in/cpslib -- Node: https://github.com/christkv/node-psutil - Rust: https://github.com/borntyping/rust-psutil -- Ruby: https://github.com/spacewander/posixpsutil - Nim: https://github.com/johnscillieri/psutil-nim From 634572caf7f65f2e0c749c52f7cdec2e5303a704 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Jan 2020 21:56:15 +0100 Subject: [PATCH 0459/1714] Add CI testing for FreeBSD (#1671) --- .cirrus.yml | 16 ++++++++++++++++ HISTORY.rst | 1 + MANIFEST.in | 2 -- README.rst | 6 +++++- docs/DEVGUIDE.rst | 4 ++++ psutil/_psbsd.py | 2 ++ psutil/tests/__init__.py | 9 +++++---- psutil/tests/test_connections.py | 16 ++++++++-------- psutil/tests/test_contracts.py | 9 ++------- psutil/tests/test_misc.py | 9 +++++---- psutil/tests/test_posix.py | 8 ++++---- psutil/tests/test_process.py | 2 +- psutil/tests/test_system.py | 18 +++++++++++------- psutil/tests/test_unicode.py | 11 ++++------- 14 files changed, 68 insertions(+), 45 deletions(-) create mode 100644 .cirrus.yml diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 0000000000..d86f7e4c3d --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,16 @@ +freebsd_12_1_task: + freebsd_instance: + image: freebsd-12-1-release-amd64 + env: + CIRRUS: 1 + install_script: + - pkg install -y python3 gcc py37-pip + script: + - python3 -m pip install --user setuptools flake8 ipaddress mock + - make clean + - make build + - make install + - make test + - make test-memleaks + - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_access_denied.py + - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_api_speed.py diff --git a/HISTORY.rst b/HISTORY.rst index 28a5ce134f..754c15a0b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,7 @@ XXXX-XX-XX - 1652_: [Windows] dropped support for Windows XP and Windows Server 2003. Minimum supported Windows version now is Windows Vista. - 1667_: added process_iter(new_only=True) parameter. +- 1671_: [FreeBSD] add CI testing/service for FreeBSD (Cirrus CI). **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index 75ad5f15d9..038d4baab1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -68,8 +68,6 @@ include psutil/arch/windows/cpu.c include psutil/arch/windows/cpu.h include psutil/arch/windows/disk.c include psutil/arch/windows/disk.h -include psutil/arch/windows/globals.c -include psutil/arch/windows/globals.h include psutil/arch/windows/net.c include psutil/arch/windows/net.h include psutil/arch/windows/ntextapi.h diff --git a/README.rst b/README.rst index ce859bd978..b38e736aca 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ | |downloads| |stars| |forks| |contributors| |coverage| |quality| | |version| |py-versions| |packages| |license| -| |travis| |appveyor| |doc| |twitter| |tidelift| +| |travis| |appveyor| |cirrus| |doc| |twitter| |tidelift| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil @@ -30,6 +30,10 @@ :target: https://ci.appveyor.com/project/giampaolo/psutil :alt: Windows tests (Appveyor) +.. |cirrus| image:: https://img.shields.io/cirrus/github/giampaolo/psutil?label=FreeBSD + :target: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci + :alt: FreeBSD tests (Cirrus-Ci) + .. |coverage| image:: https://img.shields.io/coveralls/github/giampaolo/psutil.svg?label=test%20coverage :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index d9eb6d7ab0..5ef1c4e42f 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -149,6 +149,10 @@ Two icons in the home page (README) always show the build status: :target: https://ci.appveyor.com/project/giampaolo/psutil :alt: Windows tests (Appveyor) +.. image:: https://img.shields.io/cirrus/github/giampaolo/psutil?label=FreeBSD + :target: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci + :alt: FreeBSD tests (Cirrus-CI) + BSD, AIX and Solaris are currently tested manually. Test coverage diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 78e436f7e0..11a2e59c1e 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -622,6 +622,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: diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 2844360d7c..0c297b9b02 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -71,7 +71,8 @@ # 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', + 'TESTFN', 'TESTFN_UNICODE', 'TOX', 'TRAVIS', 'CIRRUS', 'CI_TESTING', + '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", @@ -113,11 +114,11 @@ 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/) +# whether we're running this test suite on a Continuous Integration service 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')) +CIRRUS = bool(os.environ.get('CIRRUS')) +CI_TESTING = TRAVIS or APPVEYOR or CIRRUS PYPY = '__pypy__' in sys.builtin_module_names # --- configurable defaults diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 5fda78cbf1..b70e6d14d8 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -58,15 +58,15 @@ class Base(object): def setUp(self): safe_rmpath(TESTFN) - if not NETBSD: - # NetBSD opens a UNIX socket to /var/log/run. + if not (NETBSD or FREEBSD): + # process 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: + if not (FREEBSD or NETBSD): # Make sure we closed all resources. # NetBSD opens a UNIX socket to /var/log/run. cons = thisproc.connections(kind='all') @@ -318,11 +318,11 @@ def test_unix(self): 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: + 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) + self.assertEqual(len(cons), 2, msg=cons) if LINUX or FREEBSD or SUNOS: # remote path is never set self.assertEqual(cons[0].raddr, "") @@ -533,9 +533,9 @@ def test_count(self): for conn in cons: self.assertEqual(conn.family, AF_INET6) self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - # unix (skipped on NetBSD because by default the Python process - # creates a connection to '/var/run/log' UNIX socket) - if HAS_CONNECTIONS_UNIX and not NETBSD: + # 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 = thisproc.connections(kind='unix') self.assertEqual(len(cons), 3) for conn in cons: diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 509ffc0cb9..13a5bb4925 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -238,6 +238,7 @@ def test_cpu_count(self): @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq(self): + print(repr(psutil.cpu_freq())) self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int, long)) def test_disk_io_counters(self): @@ -555,13 +556,7 @@ def memory_info(self, ret, proc): 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: + 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) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 4e476871da..616a365454 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -19,6 +19,7 @@ import socket import stat +from psutil import FREEBSD from psutil import LINUX from psutil import NETBSD from psutil import POSIX @@ -35,6 +36,7 @@ from psutil.tests import bind_unix_socket from psutil.tests import call_until from psutil.tests import chdir +from psutil.tests import CI_TESTING from psutil.tests import create_proc_children_pair from psutil.tests import create_sockets from psutil.tests import create_zombie_proc @@ -712,9 +714,7 @@ def test_meminfo(self): 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") + @unittest.skipIf(CI_TESTING and not psutil.users(), "no users") def test_who(self): self.assert_stdout('who.py') @@ -1017,7 +1017,8 @@ def tcp_tcp_socketpair(self): self.assertNotEqual(client.getsockname(), addr) @unittest.skipIf(not POSIX, "POSIX only") - @unittest.skipIf(NETBSD, "/var/run/log UNIX socket opened by default") + @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() diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 348366ad7b..e405393b7c 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -22,7 +22,7 @@ from psutil import OPENBSD from psutil import POSIX from psutil import SUNOS -from psutil.tests import APPVEYOR +from psutil.tests import CI_TESTING from psutil.tests import get_kernel_version from psutil.tests import get_test_subprocess from psutil.tests import HAS_NET_IO_COUNTERS @@ -365,13 +365,13 @@ def test_nic_names(self): "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") + @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") @retry_on_failure() def test_users(self): out = sh("who") lines = out.split('\n') + if not lines: + raise self.skipTest("no users on this system") users = [x.split()[0] for x in lines] terminals = [x.split()[1] for x in lines] self.assertEqual(len(users), len(psutil.users())) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 37eaecf28d..2035305fc4 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -301,7 +301,7 @@ def test_create_time(self): @unittest.skipIf(TRAVIS, 'not reliable on TRAVIS') def test_terminal(self): terminal = psutil.Process().terminal() - if sys.stdin.isatty() or sys.stdout.isatty(): + if sys.stdin.isatty() or sys.stdout.isatty() or sys.stderr.isatty(): tty = os.path.realpath(sh('tty')) self.assertEqual(terminal, tty) else: diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index f2d072824e..c6f8a17a37 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -31,7 +31,7 @@ from psutil import WINDOWS from psutil._compat import FileNotFoundError from psutil._compat import long -from psutil.tests import APPVEYOR +from psutil.tests import CI_TESTING from psutil.tests import ASCII_FS from psutil.tests import check_net_address from psutil.tests import DEVNULL @@ -275,8 +275,9 @@ def test_test(self): finally: sys.stdout = stdout - def test_cpu_count(self): + 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) # @@ -285,7 +286,12 @@ def test_cpu_count(self): cpuinfo_data = fd.read() if "physical id" not in cpuinfo_data: raise unittest.SkipTest("cpuinfo doesn't include physical id") + + def test_cpu_count_physical(self): + logical = psutil.cpu_count() physical = psutil.cpu_count(logical=False) + if physical is None: + raise self.skipTest("physical cpu_count() is None") if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista self.assertIsNone(physical) else: @@ -695,8 +701,8 @@ def test_net_if_stats_enodev(self): @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 + @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) @@ -734,9 +740,7 @@ def test_disk_io_counters_no_disks(self): 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") + @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") def test_users(self): users = psutil.users() self.assertNotEqual(users, []) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 05c94613ad..6308de097b 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -300,13 +300,10 @@ class TestFSAPIs(_BaseFSAPIsTests, unittest.TestCase): 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) + 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") From 356d83ce0706b3a84964931d9cb72449e50eed7e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Jan 2020 22:07:04 +0100 Subject: [PATCH 0460/1714] fix Cirrus failure --- README.rst | 4 ++-- psutil/tests/test_connections.py | 3 ++- psutil/tests/test_process.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b38e736aca..57c30d6c96 100644 --- a/README.rst +++ b/README.rst @@ -22,11 +22,11 @@ :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade :alt: Code quality -.. |travis| image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=linux%20/%20osx +.. |travis| image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20OSX :target: https://travis-ci.org/giampaolo/psutil :alt: Linux tests (Travis) -.. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=windows +.. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil :alt: Windows tests (Appveyor) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index b70e6d14d8..aede66409f 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -321,7 +321,8 @@ def test_unix(self): 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'] + cons = [c for c in cons if c.raddr != '/var/run/log' and + c.laddr] self.assertEqual(len(cons), 2, msg=cons) if LINUX or FREEBSD or SUNOS: # remote path is never set diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 2035305fc4..b1b3075017 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -38,6 +38,7 @@ from psutil._compat import PY3 from psutil.tests import APPVEYOR from psutil.tests import call_until +from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe from psutil.tests import create_proc_children_pair @@ -298,10 +299,10 @@ def test_create_time(self): 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') + @unittest.skipIf(TRAVIS or CIRRUS, 'not reliable on TRAVIS/CIRRUS') def test_terminal(self): terminal = psutil.Process().terminal() - if sys.stdin.isatty() or sys.stdout.isatty() or sys.stderr.isatty(): + if sys.stdin.isatty() or sys.stdout.isatty(): tty = os.path.realpath(sh('tty')) self.assertEqual(terminal, tty) else: From 9dc9dab30f80cc734c170c06f59474cb7102c616 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Jan 2020 22:09:03 +0100 Subject: [PATCH 0461/1714] Cirrus: enable python 2 --- .cirrus.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index d86f7e4c3d..6245a0ca69 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,4 +1,4 @@ -freebsd_12_1_task: +freebsd_12_1_py3_task: freebsd_instance: image: freebsd-12-1-release-amd64 env: @@ -8,7 +8,22 @@ freebsd_12_1_task: script: - python3 -m pip install --user setuptools flake8 ipaddress mock - make clean - - make build + - make install + - make test + - make test-memleaks + - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_access_denied.py + - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_api_speed.py + +freebsd_12_1_py2_task: + freebsd_instance: + image: freebsd-12-1-release-amd64 + env: + CIRRUS: 1 + install_script: + - pkg install -y python gcc py27-pip + script: + - python3 -m pip install --user setuptools flake8 ipaddress mock + - make clean - make install - make test - make test-memleaks From b178279e908476fa8501008d9cef20c8f362dad1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Jan 2020 22:20:09 +0100 Subject: [PATCH 0462/1714] run py2 tests on cirrus --- .cirrus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 6245a0ca69..41c58a93d3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -6,7 +6,7 @@ freebsd_12_1_py3_task: install_script: - pkg install -y python3 gcc py37-pip script: - - python3 -m pip install --user setuptools flake8 ipaddress mock + - python3 -m pip install --user setuptools - make clean - make install - make test @@ -22,7 +22,7 @@ freebsd_12_1_py2_task: install_script: - pkg install -y python gcc py27-pip script: - - python3 -m pip install --user setuptools flake8 ipaddress mock + - python2.7 -m pip install --user setuptools ipaddress mock - make clean - make install - make test From 0e8e5a983ac7e346224cfaa45856738628c1bdc5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Jan 2020 01:20:51 +0100 Subject: [PATCH 0463/1714] OpenBSD fixes (#1673) --- HISTORY.rst | 2 + psutil/_psbsd.py | 2 +- psutil/_psutil_bsd.c | 14 ++---- psutil/_psutil_posix.c | 25 +++------- psutil/arch/freebsd/specific.c | 89 ++++++++++------------------------ psutil/arch/netbsd/specific.c | 6 +-- psutil/arch/openbsd/specific.c | 46 ++++++++++-------- 7 files changed, 69 insertions(+), 115 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 754c15a0b9..3f2a81b085 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -26,6 +26,8 @@ XXXX-XX-XX - 1662_: [Windows] process exe() may raise WinError 0. - 1665_: [Linux] disk_io_counters() does not take into account extra fields added to recent kernels. (patch by Mike Hommey) +- 1673_: [OpenBSD] Process connections(), num_fds() and threads() returned + improper exception if process is gone. 5.6.7 ===== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 11a2e59c1e..49ad1e995c 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -639,7 +639,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 "" diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index f58cc72f84..30d9f9dec3 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -125,18 +125,8 @@ psutil_pids(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - // TODO: RuntimeError is inappropriate here; we could return the - // original error instead. - if (psutil_get_proc_list(&proclist, &num_processes) != 0) { - if (errno != 0) { - PyErr_SetFromErrno(PyExc_OSError); - } - else { - PyErr_SetString(PyExc_RuntimeError, - "failed to retrieve process list"); - } + if (psutil_get_proc_list(&proclist, &num_processes) != 0) goto error; - } if (num_processes > 0) { orig_address = proclist; // save so we can free it after we're done @@ -475,7 +465,9 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { errno = 0; freep = kinfo_getfile(pid, &cnt); if (freep == NULL) { +#if !defined(PSUTIL_OPENBSD) psutil_raise_for_pid(pid, "kinfo_getfile()"); +#endif goto error; } diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 7fbdcc4831..88bea5ceac 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -111,23 +111,14 @@ psutil_pid_exists(long pid) { * If none of this is true we giveup and raise RuntimeError(msg). * This will always set a Python exception and return NULL. */ -int -psutil_raise_for_pid(long pid, char *syscall_name) { - // Set exception to AccessDenied if pid exists else NoSuchProcess. - if (errno != 0) { - // Unlikely we get here. - PyErr_SetFromErrno(PyExc_OSError); - return 0; - } - else if (psutil_pid_exists(pid) == 0) { - psutil_debug("%s syscall failed and PID %i no longer exists; " - "assume NoSuchProcess", syscall_name, pid); - NoSuchProcess("psutil_pid_exists"); - } - else { - PyErr_Format(PyExc_RuntimeError, "%s syscall failed", syscall_name); - } - return 0; +void +psutil_raise_for_pid(long pid, char *syscall) { + if (errno != 0) // unlikely + PyErr_SetFromOSErrnoWithSyscall(syscall); + else if (psutil_pid_exists(pid) == 0) + NoSuchProcess(syscall); + else + PyErr_Format(PyExc_RuntimeError, "%s syscall failed", syscall); } diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 90ea81e84f..18fa51fa9f 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -88,81 +88,42 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { // Returns a list of all BSD processes on the system. This routine // allocates the list and puts it in *procList and a count of the // number of entries in *procCount. You are responsible for freeing - // this list (use "free" from System framework). - // On success, the function returns 0. - // On error, the function returns a BSD errno value. + // this list. On success returns 0, else 1 with exception set. int err; - struct kinfo_proc *result; - int done; + struct kinfo_proc *buf = NULL; int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; - size_t length; + size_t length = 0; - assert( procList != NULL); + assert(procList != NULL); assert(*procList == NULL); assert(procCount != NULL); - *procCount = 0; - - /* - * We start by calling sysctl with result == NULL and length == 0. - * That will succeed, and set length to the appropriate length. - * We then allocate a buffer of that size and call sysctl again - * with that buffer. If that succeeds, we're done. If that fails - * with ENOMEM, we have to throw away our buffer and loop. Note - * that the loop causes use to call sysctl with NULL again; this - * is necessary because the ENOMEM failure case sets length to - * the amount of data returned, not the amount of data that - * could have been returned. - */ - result = NULL; - done = 0; - do { - assert(result == NULL); - // Call sysctl with a NULL buffer. - length = 0; - err = sysctl((int *)name, (sizeof(name) / sizeof(*name)) - 1, - NULL, &length, NULL, 0); - if (err == -1) - err = errno; - - // Allocate an appropriately sized buffer based on the results - // from the previous call. - if (err == 0) { - result = malloc(length); - if (result == NULL) - err = ENOMEM; - } + // Call sysctl with a NULL buffer in order to get buffer length. + err = sysctl(name, 3, NULL, &length, NULL, 0); + if (err == -1) { + PyErr_SetFromOSErrnoWithSyscall("sysctl (null buffer)"); + return 1; + } - // Call sysctl again with the new buffer. If we get an ENOMEM - // error, toss away our buffer and start again. - if (err == 0) { - err = sysctl((int *) name, (sizeof(name) / sizeof(*name)) - 1, - result, &length, NULL, 0); - if (err == -1) - err = errno; - if (err == 0) { - done = 1; - } - else if (err == ENOMEM) { - assert(result != NULL); - free(result); - result = NULL; - err = 0; - } - } - } while (err == 0 && ! done); + // Allocate an appropriately sized buffer based on the results + // from the previous call. + buf = malloc(length); + if (buf == NULL) { + PyErr_NoMemory(); + return 1; + } - // Clean up and establish post conditions. - if (err != 0 && result != NULL) { - free(result); - result = NULL; + // Call sysctl again with the new buffer. + err = sysctl(name, 3, buf, &length, NULL, 0); + if (err == -1) { + PyErr_SetFromOSErrnoWithSyscall("sysctl"); + free(buf); + return 1; } - *procList = result; + *procList = buf; *procCount = length / sizeof(struct kinfo_proc); - - assert((err == 0) == (*procList != NULL)); - return err; + return 0; } diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 62e09325ab..04eab439a9 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -320,14 +320,14 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { if (kd == NULL) { PyErr_Format( PyExc_RuntimeError, "kvm_openfiles() syscall failed: %s", errbuf); - return errno; + return 1; } result = kvm_getproc2(kd, KERN_PROC_ALL, 0, sizeof(kinfo_proc), &cnt); if (result == NULL) { PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() syscall failed"); kvm_close(kd); - return errno; + return 1; } *procCount = (size_t)cnt; @@ -337,7 +337,7 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { if ((*procList = malloc(mlen)) == NULL) { PyErr_NoMemory(); kvm_close(kd); - return errno; + return 1; } memcpy(*procList, result, mlen); diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/specific.c index f8d3cf9677..18079b89ef 100644 --- a/psutil/arch/openbsd/specific.c +++ b/psutil/arch/openbsd/specific.c @@ -46,6 +46,21 @@ // Utility functions // ============================================================================ + +static void +convert_kvm_err(const char *syscall, char *errbuf) { + char fullmsg[8192]; + + sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); + if (strstr(errbuf, "Permission denied") != NULL) + AccessDenied(fullmsg); + else if (strstr(errbuf, "Operation not permitted") != NULL) + AccessDenied(fullmsg); + else + PyErr_Format(PyExc_RuntimeError, fullmsg); +} + + int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // Fills a kinfo_proc struct based on process pid. @@ -133,16 +148,16 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { assert(procCount != NULL); kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); - - if (kd == NULL) { - return errno; + if (! kd) { + convert_kvm_err("kvm_openfiles", errbuf); + return 1; } result = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt); if (result == NULL) { + PyErr_Format(PyExc_RuntimeError, "kvm_getprocs syscall failed"); kvm_close(kd); - err(1, NULL); - return errno; + return 1; } *procCount = (size_t)cnt; @@ -150,9 +165,9 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { size_t mlen = cnt * sizeof(struct kinfo_proc); if ((*procList = malloc(mlen)) == NULL) { + PyErr_NoMemory(); kvm_close(kd); - err(1, NULL); - return errno; + return 1; } memcpy(*procList, result, mlen); @@ -163,7 +178,7 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { } -char ** +static char ** _psutil_get_argv(long pid) { static char **argv; int argv_mib[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV}; @@ -241,10 +256,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { kd = kvm_openfiles(0, 0, 0, O_RDONLY, errbuf); if (! kd) { - if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied("kvm_openfiles"); - else - PyErr_Format(PyExc_RuntimeError, "kvm_openfiles() syscall failed"); + convert_kvm_err("kvm_openfiles()", errbuf); goto error; } @@ -398,17 +410,15 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; + if (psutil_kinfo_proc(pid, &kipp) == -1) return NULL; - errno = 0; freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) { - psutil_raise_for_pid(pid, "kinfo_getfile()"); + if (freep == NULL) return NULL; - } - free(freep); + free(freep); return Py_BuildValue("i", cnt); } @@ -506,10 +516,8 @@ psutil_proc_connections(PyObject *self, PyObject *args) { goto error; } - errno = 0; freep = kinfo_getfile(pid, &cnt); if (freep == NULL) { - psutil_raise_for_pid(pid, "kinfo_getfile()"); goto error; } From 994c429c010049a6a0556ce3b0d1af1f86f27867 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Jan 2020 00:08:14 +0100 Subject: [PATCH 0464/1714] Properly handle PID type in C (#1672) --- HISTORY.rst | 1 + INSTALL.rst | 2 +- Makefile | 1 - appveyor.yml | 8 +-- psutil/_pslinux.py | 4 +- psutil/_psutil_bsd.c | 63 ++++++++++------- psutil/_psutil_common.c | 2 +- psutil/_psutil_common.h | 39 ++++++++++- psutil/_psutil_linux.c | 27 +++++--- psutil/_psutil_osx.c | 69 +++++++++---------- psutil/_psutil_posix.c | 20 +++--- psutil/_psutil_posix.h | 4 +- psutil/_psutil_windows.c | 101 ++++++++++++++-------------- psutil/arch/freebsd/proc_socks.c | 7 +- psutil/arch/freebsd/specific.c | 38 +++++------ psutil/arch/freebsd/sys_socks.c | 11 +-- psutil/arch/openbsd/specific.c | 27 ++++---- psutil/arch/openbsd/specific.h | 6 +- psutil/arch/osx/process_info.c | 18 ++--- psutil/arch/osx/process_info.h | 10 +-- psutil/arch/windows/process_info.c | 12 ++-- psutil/arch/windows/process_info.h | 6 +- psutil/arch/windows/process_utils.c | 8 +-- psutil/arch/windows/socks.c | 10 +-- psutil/tests/test_bsd.py | 3 +- psutil/tests/test_connections.py | 9 ++- psutil/tests/test_contracts.py | 3 +- psutil/tests/test_linux.py | 1 + psutil/tests/test_memory_leaks.py | 24 +++---- psutil/tests/test_process.py | 7 +- psutil/tests/test_unicode.py | 3 +- setup.py | 2 +- 32 files changed, 301 insertions(+), 245 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3f2a81b085..9b6dd98439 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -26,6 +26,7 @@ XXXX-XX-XX - 1662_: [Windows] process exe() may raise WinError 0. - 1665_: [Linux] disk_io_counters() does not take into account extra fields added to recent kernels. (patch by Mike Hommey) +- 1672_: properly handle PID C type. - 1673_: [OpenBSD] Process connections(), num_fds() and threads() returned improper exception if process is gone. diff --git a/INSTALL.rst b/INSTALL.rst index 2b1ea3292a..c3b9e91c0c 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -93,7 +93,7 @@ OpenBSD :: export PKG_PATH=http://ftp.eu.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ - pkg_add -v python3 gcc + pkg_add -v python gcc python3 -m pip install psutil NetBSD diff --git a/Makefile b/Makefile index 94fa3b24d4..890c6e413f 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,6 @@ DEPS = \ ipaddress \ mock==1.0.1 \ pyperf \ - readline \ requests \ setuptools \ sphinx \ diff --git a/appveyor.yml b/appveyor.yml index a99670e30d..b38cbf1ba2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,12 +99,6 @@ only_commits: - psutil/_psutil_windows.* - psutil/_pswindows.py - psutil/arch/windows/* - - psutil/tests/__init__.py - - psutil/tests/__main__.py - - psutil/tests/test_memory_leaks.py - - psutil/tests/test_misc.py - - psutil/tests/test_process.py - - psutil/tests/test_system.py - - psutil/tests/test_windows.py + - psutil/tests/* - scripts/* - setup.py diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index d8f8ed5c82..0693511110 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1213,7 +1213,7 @@ def sensors_temperatures(): current = float(cat(path)) / 1000.0 path = os.path.join(os.path.dirname(base), 'name') unit_name = cat(path, binary=False) - except (IOError, OSError, ValueError) as err: + 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. @@ -1222,8 +1222,6 @@ 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) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 30d9f9dec3..953fcd083c 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -132,9 +132,9 @@ psutil_pids(PyObject *self, PyObject *args) { orig_address = proclist; // save so we can free it after we're done for (idx = 0; idx < num_processes; idx++) { #ifdef PSUTIL_FREEBSD - py_pid = Py_BuildValue("i", proclist->ki_pid); + py_pid = PyLong_FromPid(proclist->ki_pid); #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_pid = Py_BuildValue("i", proclist->p_pid); + py_pid = PyLong_FromPid(proclist->p_pid); #endif if (!py_pid) goto error; @@ -180,7 +180,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_oneshot_info(PyObject *self, PyObject *args) { - long pid; + pid_t pid; long rss; long vms; long memtext; @@ -191,9 +191,10 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { long pagesize = sysconf(_SC_PAGESIZE); char str[1000]; PyObject *py_name; + PyObject *py_ppid; PyObject *py_retlist; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -255,16 +256,25 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { oncpu = -1; #endif +#ifdef PSUTIL_FREEBSD + py_ppid = PyLong_FromPid(kp.ki_ppid); +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + py_ppid = PyLong_FromPid(kp.p_ppid); +#else + py_ppid = Py_BuildfValue(-1); +#endif + if (! py_ppid) + return NULL; + // Return a single big tuple with all process info. py_retlist = Py_BuildValue( #if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031 - "(lillllllLdllllddddlllllbO)", + "(OillllllLdllllddddlllllbO)", #else - "(lillllllidllllddddlllllbO)", + "(OillllllidllllddddlllllbO)", #endif #ifdef PSUTIL_FREEBSD - // - (long)kp.ki_ppid, // (long) ppid + py_ppid, // (pid_t) ppid (int)kp.ki_stat, // (int) status // UIDs (long)kp.ki_ruid, // (long) real uid @@ -297,8 +307,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { // others oncpu, // (int) the CPU we are on #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - // - (long)kp.p_ppid, // (long) ppid + py_ppid, // (pid_t) ppid (int)kp.p_stat, // (int) status // UIDs (long)kp.p_ruid, // (long) real uid @@ -337,6 +346,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { ); Py_DECREF(py_name); + Py_DECREF(py_ppid); return py_retlist; } @@ -346,11 +356,11 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_name(PyObject *self, PyObject *args) { - long pid; + pid_t pid; kinfo_proc kp; char str[1000]; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -369,10 +379,10 @@ psutil_proc_name(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { - long pid; + pid_t pid; PyObject *py_retlist = NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; py_retlist = psutil_get_cmdline(pid); if (py_retlist == NULL) @@ -442,7 +452,7 @@ psutil_cpu_times(PyObject *self, PyObject *args) { #if (defined(__FreeBSD_version) && __FreeBSD_version >= 800000) || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) static PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int i; int cnt; int regular; @@ -457,7 +467,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_kinfo_proc(pid, &kipp) == -1) goto error; @@ -786,6 +796,7 @@ psutil_users(PyObject *self, PyObject *args) { PyObject *py_tty = NULL; PyObject *py_hostname = NULL; PyObject *py_tuple = NULL; + PyObject *py_pid = NULL; if (py_retlist == NULL) return NULL; @@ -823,7 +834,7 @@ psutil_users(PyObject *self, PyObject *args) { #ifdef PSUTIL_OPENBSD -1 // process id (set to None later) #else - ut.ut_pid // process id + ut.ut_pid // TODO: use PyLong_FromPid #endif ); if (!py_tuple) { @@ -856,17 +867,21 @@ psutil_users(PyObject *self, PyObject *args) { py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); if (! py_hostname) goto error; +#ifdef PSUTIL_OPENBSD + py_pid = Py_BuildValue("i", -1); // set to None later +#else + py_pid = PyLong_FromPid(utx->ut_pid); +#endif + if (! py_pid) + goto error; + py_tuple = Py_BuildValue( - "(OOOfi)", + "(OOOfO)", py_username, // username py_tty, // tty py_hostname, // hostname (float)utx->ut_tv.tv_sec, // start time -#ifdef PSUTIL_OPENBSD - -1 // process id (set to None later) -#else - utx->ut_pid // process id -#endif + py_pid // process id ); if (!py_tuple) { @@ -881,6 +896,7 @@ psutil_users(PyObject *self, PyObject *args) { Py_CLEAR(py_tty); Py_CLEAR(py_hostname); Py_CLEAR(py_tuple); + Py_CLEAR(py_pid); } endutxent(); @@ -892,6 +908,7 @@ psutil_users(PyObject *self, PyObject *args) { Py_XDECREF(py_tty); Py_XDECREF(py_hostname); Py_XDECREF(py_tuple); + Py_XDECREF(py_pid); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 9075fda32b..0c86a4e6b0 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -235,7 +235,7 @@ psutil_loadlibs() { // --- Mandatory NtQuerySystemInformation = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQuerySystemInformation"); - if (NtQuerySystemInformation == NULL) + if (! NtQuerySystemInformation) return 1; NtQueryInformationProcess = psutil_GetProcAddress( "ntdll.dll", "NtQueryInformationProcess"); diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 92a98b9cc7..d45cf56be4 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -23,7 +23,43 @@ static const int PSUTIL_CONN_NONE = 128; PyObject* PyUnicode_DecodeFSDefault(char *s); PyObject* PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size); #endif -PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); + +// Python 2: SIZEOF_PID_T not defined but _getpid() returns an int. +#if defined(PSUTIL_WINDOWS) && !defined(SIZEOF_PID_T) + #define SIZEOF_PID_T SIZEOF_INT +#endif + +#if !defined(_Py_PARSE_PID) || PY_MAJOR_VERSION < 3 + #if !defined(SIZEOF_PID_T) || !defined(SIZEOF_INT) || !defined(SIZEOF_LONG) + #error "missing SIZEOF* definition" + #endif +#endif + +// _Py_PARSE_PID is Python 3 only, but since it's private make sure it's +// always present. +#ifndef _Py_PARSE_PID + #if SIZEOF_PID_T == SIZEOF_INT + #define _Py_PARSE_PID "i" + #elif SIZEOF_PID_T == SIZEOF_LONG + #define _Py_PARSE_PID "l" + #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG + #define _Py_PARSE_PID "L" + #else + #error "_Py_PARSE_PID: sizeof(pid_t) is neither sizeof(int), " + "sizeof(long) or sizeof(long long)" + #endif +#endif + +#if PY_MAJOR_VERSION < 3 + #if ((SIZEOF_PID_T == SIZEOF_INT) || (SIZEOF_PID_T == SIZEOF_LONG)) + #define PyLong_FromPid PyInt_FromLong + #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG + #define PyLong_FromPid PyLong_FromLongLong + #else + #error "PyLong_FromPid: sizeof(pid_t) is neither sizeof(int), " + "sizeof(long) or sizeof(long long)" + #endif +#endif // ==================================================================== // --- Custom exceptions @@ -31,6 +67,7 @@ PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); PyObject* AccessDenied(const char *msg); PyObject* NoSuchProcess(const char *msg); +PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); // ==================================================================== // --- Global utils diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 0d16eb4276..93cc071b5a 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -97,9 +97,9 @@ ioprio_set(int which, int who, int ioprio) { */ static PyObject * psutil_proc_ioprio_get(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int ioprio, ioclass, iodata; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; ioprio = ioprio_get(IOPRIO_WHO_PROCESS, pid); if (ioprio == -1) @@ -117,12 +117,14 @@ psutil_proc_ioprio_get(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_ioprio_set(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int ioprio, ioclass, iodata; int retval; - if (! PyArg_ParseTuple(args, "lii", &pid, &ioclass, &iodata)) + if (! PyArg_ParseTuple( + args, _Py_PARSE_PID "ii", &pid, &ioclass, &iodata)) { return NULL; + } ioprio = IOPRIO_PRIO_VALUE(ioclass, iodata); retval = ioprio_set(IOPRIO_WHO_PROCESS, pid, ioprio); if (retval == -1) @@ -140,15 +142,17 @@ psutil_proc_ioprio_set(PyObject *self, PyObject *args) { */ static PyObject * psutil_linux_prlimit(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int ret, resource; struct rlimit old, new; struct rlimit *newp = NULL; PyObject *py_soft = NULL; PyObject *py_hard = NULL; - if (! PyArg_ParseTuple(args, "li|OO", &pid, &resource, &py_soft, &py_hard)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i|OO", &pid, &resource, + &py_soft, &py_hard)) { return NULL; + } // get if (py_soft == NULL && py_hard == NULL) { @@ -290,12 +294,12 @@ psutil_linux_sysinfo(PyObject *self, PyObject *args) { static PyObject * psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { int cpu, ncpus, count, cpucount_s; - long pid; + pid_t pid; size_t setsize; cpu_set_t *mask = NULL; PyObject *py_list = NULL; - if (!PyArg_ParseTuple(args, "l", &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; ncpus = NCPUS_START; while (1) { @@ -358,12 +362,12 @@ static PyObject * psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { cpu_set_t cpu_set; size_t len; - long pid; + pid_t pid; int i, seq_len; PyObject *py_cpu_set; PyObject *py_cpu_seq = NULL; - if (!PyArg_ParseTuple(args, "lO", &pid, &py_cpu_set)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &py_cpu_set)) return NULL; if (!PySequence_Check(py_cpu_set)) { @@ -441,8 +445,9 @@ psutil_users(PyObject *self, PyObject *args) { py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); if (! py_hostname) goto error; + py_tuple = Py_BuildValue( - "(OOOfOi)", + "OOOfO" _Py_PARSE_PID, py_username, // username py_tty, // tty py_hostname, // hostname diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 8d08612244..c51c1c788b 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -88,12 +88,12 @@ psutil_sys_vminfo(vm_statistics_data_t *vmstat) { * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 */ int -psutil_task_for_pid(long pid, mach_port_t *task) +psutil_task_for_pid(pid_t pid, mach_port_t *task) { // See: https://github.com/giampaolo/psutil/issues/1181 kern_return_t err = KERN_SUCCESS; - err = task_for_pid(mach_task_self(), (pid_t)pid, task); + err = task_for_pid(mach_task_self(), pid, task); if (err != KERN_SUCCESS) { if (psutil_pid_exists(pid) == 0) NoSuchProcess("task_for_pid"); @@ -133,7 +133,7 @@ psutil_pids(PyObject *self, PyObject *args) { // save the address of proclist so we can free it later orig_address = proclist; for (idx = 0; idx < num_processes; idx++) { - py_pid = Py_BuildValue("i", proclist->kp_proc.p_pid); + py_pid = PyLong_FromPid(proclist->kp_proc.p_pid); if (! py_pid) goto error; if (PyList_Append(py_retlist, py_pid)) @@ -164,12 +164,12 @@ psutil_pids(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { - long pid; + pid_t pid; struct kinfo_proc kp; PyObject *py_name; PyObject *py_retlist; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_get_kinfo_proc(pid, &kp) == -1) return NULL; @@ -183,8 +183,8 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { } py_retlist = Py_BuildValue( - "lllllllidiO", - (long)kp.kp_eproc.e_ppid, // (long) ppid + _Py_PARSE_PID "llllllidiO", + kp.kp_eproc.e_ppid, // (pid_t) ppid (long)kp.kp_eproc.e_pcred.p_ruid, // (long) real uid (long)kp.kp_eproc.e_ucred.cr_uid, // (long) effective uid (long)kp.kp_eproc.e_pcred.p_svuid, // (long) saved uid @@ -215,10 +215,10 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { - long pid; + pid_t pid; struct proc_taskinfo pti; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) <= 0) return NULL; @@ -250,10 +250,10 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_name(PyObject *self, PyObject *args) { - long pid; + pid_t pid; struct kinfo_proc kp; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_get_kinfo_proc(pid, &kp) == -1) return NULL; @@ -267,10 +267,10 @@ psutil_proc_name(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { - long pid; + pid_t pid; struct proc_vnodepathinfo pathinfo; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_proc_pidinfo( @@ -288,14 +288,14 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { - long pid; + pid_t pid; char buf[PATH_MAX]; int ret; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; errno = 0; - ret = proc_pidpath((pid_t)pid, &buf, sizeof(buf)); + ret = proc_pidpath(pid, &buf, sizeof(buf)); if (ret == 0) { if (pid == 0) AccessDenied("automatically set for PID 0"); @@ -312,10 +312,10 @@ psutil_proc_exe(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { - long pid; + pid_t pid; PyObject *py_retlist = NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; // get the commandline, defined in arch/osx/process_info.c @@ -329,10 +329,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_environ(PyObject *self, PyObject *args) { - long pid; + pid_t pid; PyObject *py_retdict = NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; // get the environment block, defined in arch/osx/process_info.c @@ -422,7 +422,7 @@ psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { */ static PyObject * psutil_proc_memory_uss(PyObject *self, PyObject *args) { - long pid; + pid_t pid; size_t len; cpu_type_t cpu_type; size_t private_pages = 0; @@ -435,7 +435,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { vm_region_top_info_data_t info; mach_port_t object_name; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_task_for_pid(pid, &task) != 0) @@ -865,7 +865,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int err, ret; kern_return_t kr; unsigned int info_count = TASK_BASIC_INFO_COUNT; @@ -882,7 +882,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_task_for_pid(pid, &task) != 0) @@ -968,7 +968,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int pidinfo_result; int iterations; int i; @@ -985,7 +985,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; pidinfo_result = psutil_proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); @@ -1070,7 +1070,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_connections(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int pidinfo_result; int iterations; int i; @@ -1090,8 +1090,10 @@ psutil_proc_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, + &py_type_filter)) { goto error; + } if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); @@ -1124,7 +1126,7 @@ psutil_proc_connections(PyObject *self, PyObject *args) { if (fdp_pointer->proc_fdtype == PROX_FDTYPE_SOCKET) { errno = 0; - nb = proc_pidfdinfo((pid_t)pid, fdp_pointer->proc_fd, + nb = proc_pidfdinfo(pid, fdp_pointer->proc_fd, PROC_PIDFDSOCKETINFO, &si, sizeof(si)); // --- errors checking @@ -1272,22 +1274,22 @@ psutil_proc_connections(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int pidinfo_result; int num; struct proc_fdinfo *fds_pointer; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - pidinfo_result = proc_pidinfo((pid_t)pid, PROC_PIDLISTFDS, 0, NULL, 0); + pidinfo_result = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); if (pidinfo_result <= 0) return PyErr_SetFromErrno(PyExc_OSError); fds_pointer = malloc(pidinfo_result); if (fds_pointer == NULL) return PyErr_NoMemory(); - pidinfo_result = proc_pidinfo((pid_t)pid, PROC_PIDLISTFDS, 0, fds_pointer, + pidinfo_result = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds_pointer, pidinfo_result); if (pidinfo_result <= 0) { free(fds_pointer); @@ -1834,7 +1836,6 @@ static PyMethodDef mod_methods[] = { void init_psutil_osx(void) #endif /* PY_MAJOR_VERSION */ { - PyObject *v; #if PY_MAJOR_VERSION >= 3 PyObject *mod = PyModule_Create(&moduledef); #else diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 88bea5ceac..fa554be9f1 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -51,7 +51,7 @@ * -1: error (Python exception is set) */ int -psutil_pid_exists(long pid) { +psutil_pid_exists(pid_t pid) { int ret; // No negative PID exists, plus -1 is an alias for sending signal @@ -71,12 +71,7 @@ psutil_pid_exists(long pid) { #endif } -#if defined(PSUTIL_OSX) - ret = kill((pid_t)pid , 0); -#else ret = kill(pid , 0); -#endif - if (ret == 0) return 1; else { @@ -127,11 +122,11 @@ psutil_raise_for_pid(long pid, char *syscall) { */ static PyObject * psutil_posix_getpriority(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int priority; errno = 0; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; #ifdef PSUTIL_OSX @@ -150,11 +145,11 @@ psutil_posix_getpriority(PyObject *self, PyObject *args) { */ static PyObject * psutil_posix_setpriority(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int priority; int retval; - if (! PyArg_ParseTuple(args, "li", &pid, &priority)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) return NULL; #ifdef PSUTIL_OSX @@ -666,7 +661,10 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) || defined(PSUTIL_SUNOS) || defined(PSUTIL_AIX) +#if defined(PSUTIL_BSD) || \ + defined(PSUTIL_OSX) || \ + defined(PSUTIL_SUNOS) || \ + defined(PSUTIL_AIX) if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) INITERR; #endif diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h index fe25b36695..59b9e53238 100644 --- a/psutil/_psutil_posix.h +++ b/psutil/_psutil_posix.h @@ -4,5 +4,5 @@ * found in the LICENSE file. */ -int psutil_pid_exists(long pid); -void psutil_raise_for_pid(long pid, char *msg); +int psutil_pid_exists(pid_t pid); +void psutil_raise_for_pid(pid_t pid, char *msg); diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 510bde8a9a..cabfe9348f 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -111,10 +111,10 @@ psutil_boot_time(PyObject *self, PyObject *args) { */ static PyObject * psutil_pid_exists(PyObject *self, PyObject *args) { - long pid; + DWORD pid; int status; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; status = psutil_pid_is_running(pid); @@ -142,7 +142,7 @@ psutil_pids(PyObject *self, PyObject *args) { goto error; for (i = 0; i < numberOfReturnedPIDs; i++) { - py_pid = Py_BuildValue("I", proclist[i]); + py_pid = PyLong_FromPid(proclist[i]); if (!py_pid) goto error; if (PyList_Append(py_retlist, py_pid)) @@ -169,9 +169,9 @@ psutil_pids(PyObject *self, PyObject *args) { static PyObject * psutil_proc_kill(PyObject *self, PyObject *args) { HANDLE hProcess; - long pid; + DWORD pid; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (pid == 0) return AccessDenied("automatically set for PID 0"); @@ -212,10 +212,10 @@ psutil_proc_wait(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD ExitCode; DWORD retVal; - long pid; + DWORD pid; long timeout; - if (! PyArg_ParseTuple(args, "ll", &pid, &timeout)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "l", &pid, &timeout)) return NULL; if (pid == 0) return AccessDenied("automatically set for PID 0"); @@ -281,11 +281,11 @@ psutil_proc_wait(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cpu_times(PyObject *self, PyObject *args) { - long pid; + pid_t pid; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -332,12 +332,12 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_create_time(PyObject *self, PyObject *args) { - long pid; + pid_t pid; long long unix_time; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; // special case for PIDs 0 and 4, return system boot time @@ -397,14 +397,15 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { - long pid; + DWORD pid; int pid_return; int use_peb; PyObject *py_usepeb = Py_True; static char *keywords[] = {"pid", "use_peb", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwdict, "i|O", - keywords, &pid, &py_usepeb)) { + if (!PyArg_ParseTupleAndKeywords(args, kwdict, _Py_PARSE_PID "|O", + keywords, &pid, &py_usepeb)) + { return NULL; } if ((pid == 0) || (pid == 4)) @@ -426,10 +427,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { */ static PyObject * psutil_proc_environ(PyObject *self, PyObject *args) { - long pid; + DWORD pid; int pid_return; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if ((pid == 0) || (pid == 4)) return Py_BuildValue("s", ""); @@ -449,12 +450,12 @@ psutil_proc_environ(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { - long pid; + DWORD pid; HANDLE hProcess; wchar_t exe[MAX_PATH]; unsigned int size = sizeof(exe); - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -484,12 +485,12 @@ psutil_proc_exe(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_name(PyObject *self, PyObject *args) { - long pid; + DWORD pid; int ok; PROCESSENTRY32W pentry; HANDLE hSnapShot; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, pid); if (hSnapShot == INVALID_HANDLE_VALUE) @@ -525,7 +526,7 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { DWORD pid; PROCESS_MEMORY_COUNTERS_EX cnt; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -641,7 +642,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { PMEMORY_WORKING_SET_INFORMATION wsInfo; ULONG_PTR i; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION); if (hProcess == NULL) @@ -706,10 +707,10 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { - long pid; + DWORD pid; int pid_return; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; pid_return = psutil_pid_is_running(pid); @@ -727,13 +728,13 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { - long pid; + DWORD pid; NTSTATUS status; HANDLE hProcess; PyObject* suspend; - if (! PyArg_ParseTuple(args, "lO", &pid, &suspend)) - return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) + return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_SUSPEND_RESUME); if (hProcess == NULL) @@ -756,9 +757,9 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { static PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { - HANDLE hThread; + HANDLE hThread = NULL; THREADENTRY32 te32 = {0}; - long pid; + DWORD pid; int pid_return; int rc; FILETIME ftDummy, ftKernel, ftUser; @@ -768,7 +769,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (pid == 0) { // raise AD instead of returning 0 as procexp is able to @@ -866,7 +867,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { DWORD access = PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION; PyObject *py_retlist; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; processHandle = psutil_handle_from_pid(pid, access); @@ -884,7 +885,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_username(PyObject *self, PyObject *args) { - long pid; + DWORD pid; HANDLE processHandle = NULL; HANDLE tokenHandle = NULL; PTOKEN_USER user = NULL; @@ -898,7 +899,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { PyObject *py_domain = NULL; PyObject *py_tuple = NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; processHandle = psutil_handle_from_pid( @@ -1011,11 +1012,11 @@ psutil_proc_username(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_priority_get(PyObject *self, PyObject *args) { - long pid; + DWORD pid; DWORD priority; HANDLE hProcess; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -1038,13 +1039,13 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_priority_set(PyObject *self, PyObject *args) { - long pid; + DWORD pid; int priority; int retval; HANDLE hProcess; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - if (! PyArg_ParseTuple(args, "li", &pid, &priority)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) return NULL; hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) @@ -1067,12 +1068,12 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_io_priority_get(PyObject *self, PyObject *args) { - long pid; + DWORD pid; HANDLE hProcess; DWORD IoPriority; NTSTATUS status; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -1099,13 +1100,13 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_io_priority_set(PyObject *self, PyObject *args) { - long pid; + DWORD pid; DWORD prio; HANDLE hProcess; NTSTATUS status; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - if (! PyArg_ParseTuple(args, "li", &pid, &prio)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &prio)) return NULL; hProcess = psutil_handle_from_pid(pid, access); @@ -1135,7 +1136,7 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { HANDLE hProcess; IO_COUNTERS IoCounters; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) @@ -1168,7 +1169,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { DWORD_PTR proc_mask; DWORD_PTR system_mask; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) { @@ -1200,9 +1201,9 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { DWORD_PTR mask; #ifdef _WIN64 - if (! PyArg_ParseTuple(args, "lK", &pid, &mask)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "K", &pid, &mask)) #else - if (! PyArg_ParseTuple(args, "lk", &pid, &mask)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "k", &pid, &mask)) #endif { return NULL; @@ -1232,7 +1233,7 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { PSYSTEM_PROCESS_INFORMATION process; PVOID buffer; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (! psutil_get_proc_info(pid, &process, &buffer)) return NULL; @@ -1391,7 +1392,7 @@ psutil_proc_num_handles(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD handleCount; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) @@ -1449,7 +1450,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; hProcess = psutil_handle_from_pid(pid, access); if (NULL == hProcess) @@ -1530,10 +1531,10 @@ psutil_ppid_map(PyObject *self, PyObject *args) { if (Process32First(handle, &pe)) { do { - py_pid = Py_BuildValue("I", pe.th32ProcessID); + py_pid = PyLong_FromPid(pe.th32ProcessID); if (py_pid == NULL) goto error; - py_ppid = Py_BuildValue("I", pe.th32ParentProcessID); + py_ppid = PyLong_FromPid(pe.th32ParentProcessID); if (py_ppid == NULL) goto error; if (PyDict_SetItem(py_retdict, py_pid, py_ppid)) diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index a458a01e53..cdf5770b6f 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -179,7 +179,7 @@ psutil_search_tcplist(char *buf, struct kinfo_file *kif) { PyObject * psutil_proc_connections(PyObject *self, PyObject *args) { // Return connections opened by process. - long pid; + pid_t pid; int i; int cnt; struct kinfo_file *freep = NULL; @@ -202,8 +202,11 @@ psutil_proc_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, + &py_af_filter, &py_type_filter)) + { goto error; + } if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); goto error; diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 18fa51fa9f..3d54b47e5d 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -44,7 +44,7 @@ int -psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc) { +psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // Fills a kinfo_proc struct based on process pid. int mib[4]; size_t size; @@ -141,7 +141,7 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { * 1 for insufficient privileges. */ static char -*psutil_get_cmd_args(long pid, size_t *argsize) { +*psutil_get_cmd_args(pid_t pid, size_t *argsize) { int mib[4]; int argmax; size_t size = sizeof(argmax); @@ -183,7 +183,7 @@ static char // returns the command line as a python list object PyObject * -psutil_get_cmdline(long pid) { +psutil_get_cmdline(pid_t pid) { char *argstr = NULL; size_t pos = 0; size_t argsize = 0; @@ -230,14 +230,14 @@ psutil_get_cmdline(long pid) { */ PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { - long pid; + pid_t pid; char pathname[PATH_MAX]; int error; int mib[4]; int ret; size_t size; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; mib[0] = CTL_KERN; @@ -274,9 +274,9 @@ psutil_proc_exe(PyObject *self, PyObject *args) { PyObject * psutil_proc_num_threads(PyObject *self, PyObject *args) { // Return number of threads used by process as a Python integer. - long pid; + pid_t pid; struct kinfo_proc kp; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -291,7 +291,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // Thanks to Robert N. M. Watson: // http://code.metager.de/source/xref/freebsd/usr.bin/procstat/ // procstat_threads.c - long pid; + pid_t pid; int mib[4]; struct kinfo_proc *kip = NULL; struct kinfo_proc *kipp = NULL; @@ -303,7 +303,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; // we need to re-query for thread information, so don't use *kipp @@ -520,7 +520,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { #if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { - long pid; + pid_t pid; struct kinfo_file *freep = NULL; struct kinfo_file *kif; struct kinfo_proc kipp; @@ -528,7 +528,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { int i, cnt; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_kinfo_proc(pid, &kipp) == -1) goto error; @@ -571,13 +571,13 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { #if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int cnt; struct kinfo_file *freep; struct kinfo_proc kipp; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kipp) == -1) return NULL; @@ -730,7 +730,7 @@ PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { // Return a list of tuples for every process memory maps. //'procstat' cmdline utility has been used as an example. - long pid; + pid_t pid; int ptrwidth; int i, cnt; char addr[1000]; @@ -746,7 +746,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_kinfo_proc(pid, &kp) == -1) goto error; @@ -846,14 +846,14 @@ psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args) { // Get process CPU affinity. // Reference: // http://sources.freebsd.org/RELENG_9/src/usr.bin/cpuset/cpuset.c - long pid; + pid_t pid; int ret; int i; cpuset_t mask; PyObject* py_retlist; PyObject* py_cpu_num; - if (!PyArg_ParseTuple(args, "i", &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; ret = cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, sizeof(mask), &mask); @@ -888,7 +888,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { // Set process CPU affinity. // Reference: // http://sources.freebsd.org/RELENG_9/src/usr.bin/cpuset/cpuset.c - long pid; + pid_t pid; int i; int seq_len; int ret; @@ -896,7 +896,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { PyObject *py_cpu_set; PyObject *py_cpu_seq = NULL; - if (!PyArg_ParseTuple(args, "lO", &pid, &py_cpu_set)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &py_cpu_set)) return NULL; py_cpu_seq = PySequence_Fast(py_cpu_set, "expected a sequence or integer"); diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index e0e2046be6..ab61f393b9 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -129,7 +129,7 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { } while (xig->xig_gen != exig->xig_gen && retry--); for (;;) { - struct xfile *xf; + struct xfile *xf; int lport, rport, status, family; xig = (struct xinpgen *)(void *)((char *)xig + xig->xig_len); @@ -203,19 +203,20 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { if (!py_raddr) goto error; py_tuple = Py_BuildValue( - "(iiiNNii)", + "iiiNNi" _Py_PARSE_PID, xf->xf_fd, // fd family, // family type, // type py_laddr, // laddr py_raddr, // raddr status, // status - xf->xf_pid); // pid + xf->xf_pid // pid + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; - Py_DECREF(py_tuple); + Py_CLEAR(py_tuple); } free(buf); @@ -286,7 +287,7 @@ int psutil_gather_unix(int proto, PyObject *py_retlist) { } while (xug->xug_gen != exug->xug_gen && retry--); for (;;) { - struct xfile *xf; + struct xfile *xf; xug = (struct xunpgen *)(void *)((char *)xug + xug->xug_len); if (xug >= exug) diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/specific.c index 18079b89ef..d97a8f9b93 100644 --- a/psutil/arch/openbsd/specific.c +++ b/psutil/arch/openbsd/specific.c @@ -90,7 +90,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { struct kinfo_file * -kinfo_getfile(long pid, int* cnt) { +kinfo_getfile(pid_t pid, int* cnt) { // Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an // int as arg and returns an array with cnt struct kinfo_file. int mib[6]; @@ -99,7 +99,7 @@ kinfo_getfile(long pid, int* cnt) { mib[0] = CTL_KERN; mib[1] = KERN_FILE; mib[2] = KERN_FILE_BYPID; - mib[3] = (int) pid; + mib[3] = pid; mib[4] = sizeof(struct kinfo_file); mib[5] = 0; @@ -179,7 +179,7 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { static char ** -_psutil_get_argv(long pid) { +_psutil_get_argv(pid_t pid) { static char **argv; int argv_mib[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV}; size_t argv_size = 128; @@ -204,7 +204,7 @@ _psutil_get_argv(long pid) { // returns the command line as a python list object PyObject * -psutil_get_cmdline(long pid) { +psutil_get_cmdline(pid_t pid) { static char **argv; char **p; PyObject *py_arg = NULL; @@ -241,7 +241,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // https://github.com/janmojzis/pstree/blob/master/proc_kvm.c // Note: this requires root access, else it will fail trying // to access /dev/kmem. - long pid; + pid_t pid; kvm_t *kd = NULL; int nentries, i; char errbuf[4096]; @@ -251,7 +251,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; kd = kvm_openfiles(0, 0, 0, O_RDONLY, errbuf); @@ -276,7 +276,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { continue; if (kp[i].p_pid == pid) { py_tuple = Py_BuildValue( - "Idd", + _Py_PARSE_PID "dd", kp[i].p_tid, PSUTIL_KPT2DOUBLE(kp[i].p_uutime), PSUTIL_KPT2DOUBLE(kp[i].p_ustime)); @@ -402,13 +402,13 @@ psutil_swap_mem(PyObject *self, PyObject *args) { PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int cnt; struct kinfo_file *freep; struct kinfo_proc kipp; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kipp) == -1) @@ -428,12 +428,12 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { // Reference: // https://github.com/openbsd/src/blob/ // 588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L191 - long pid; + pid_t pid; struct kinfo_proc kp; char path[MAXPATHLEN]; size_t pathlen = sizeof path; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -492,7 +492,7 @@ psutil_inet6_addrstr(struct in6_addr *p) */ PyObject * psutil_proc_connections(PyObject *self, PyObject *args) { - long pid; + pid_t pid; int i; int cnt; struct kinfo_file *freep = NULL; @@ -509,7 +509,8 @@ psutil_proc_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, + &py_type_filter)) goto error; if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); diff --git a/psutil/arch/openbsd/specific.h b/psutil/arch/openbsd/specific.h index 4f870268d6..b8170a0223 100644 --- a/psutil/arch/openbsd/specific.h +++ b/psutil/arch/openbsd/specific.h @@ -10,10 +10,10 @@ typedef struct kinfo_proc kinfo_proc; int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); -struct kinfo_file * kinfo_getfile(long pid, int* cnt); +struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); -char **_psutil_get_argv(long pid); -PyObject * psutil_get_cmdline(long pid); +char **_psutil_get_argv(pid_t pid); +PyObject * psutil_get_cmdline(pid_t pid); // PyObject *psutil_proc_threads(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 47cf864f2c..4b84a723a0 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -119,7 +119,7 @@ psutil_get_argmax() { // Return 1 if pid refers to a zombie process else 0. int -psutil_is_zombie(long pid) { +psutil_is_zombie(pid_t pid) { struct kinfo_proc kp; if (psutil_get_kinfo_proc(pid, &kp) == -1) @@ -131,7 +131,7 @@ psutil_is_zombie(long pid) { // return process args as a python list PyObject * -psutil_get_cmdline(long pid) { +psutil_get_cmdline(pid_t pid) { int mib[3]; int nargs; size_t len; @@ -162,7 +162,7 @@ psutil_get_cmdline(long pid) { // read argument space mib[0] = CTL_KERN; mib[1] = KERN_PROCARGS2; - mib[2] = (pid_t)pid; + mib[2] = pid; if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { // In case of zombie process we'll get EINVAL. We translate it // to NSP and _psosx.py will translate it to ZP. @@ -225,7 +225,7 @@ psutil_get_cmdline(long pid) { // return process environment as a python string PyObject * -psutil_get_environ(long pid) { +psutil_get_environ(pid_t pid) { int mib[3]; int nargs; char *procargs = NULL; @@ -254,7 +254,7 @@ psutil_get_environ(long pid) { // read argument space mib[0] = CTL_KERN; mib[1] = KERN_PROCARGS2; - mib[2] = (pid_t)pid; + mib[2] = pid; if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { // In case of zombie process we'll get EINVAL. We translate it // to NSP and _psosx.py will translate it to ZP. @@ -339,13 +339,13 @@ psutil_get_environ(long pid) { int -psutil_get_kinfo_proc(long pid, struct kinfo_proc *kp) { +psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { int mib[4]; size_t len; mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; - mib[3] = (pid_t)pid; + mib[3] = pid; // fetch the info with sysctl() len = sizeof(struct kinfo_proc); @@ -371,9 +371,9 @@ psutil_get_kinfo_proc(long pid, struct kinfo_proc *kp) { * Returns 0 on failure (and Python exception gets already set). */ int -psutil_proc_pidinfo(long pid, int flavor, uint64_t arg, void *pti, int size) { +psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { errno = 0; - int ret = proc_pidinfo((int)pid, flavor, arg, pti, size); + int ret = proc_pidinfo(pid, flavor, arg, pti, size); if ((ret <= 0) || ((unsigned long)ret < sizeof(pti))) { psutil_raise_for_pid(pid, "proc_pidinfo()"); return 0; diff --git a/psutil/arch/osx/process_info.h b/psutil/arch/osx/process_info.h index bd7ffa89ca..35755247aa 100644 --- a/psutil/arch/osx/process_info.h +++ b/psutil/arch/osx/process_info.h @@ -9,10 +9,10 @@ typedef struct kinfo_proc kinfo_proc; int psutil_get_argmax(void); -int psutil_is_zombie(long pid); -int psutil_get_kinfo_proc(long pid, struct kinfo_proc *kp); +int psutil_is_zombie(pid_t pid); +int psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp); int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); int psutil_proc_pidinfo( - long pid, int flavor, uint64_t arg, void *pti, int size); -PyObject* psutil_get_cmdline(long pid); -PyObject* psutil_get_environ(long pid); + pid_t pid, int flavor, uint64_t arg, void *pti, int size); +PyObject* psutil_get_cmdline(pid_t pid); +PyObject* psutil_get_environ(pid_t pid); diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 64ff0f0da4..d8104c818b 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -58,7 +58,7 @@ enum psutil_process_data_kind { * -1 is returned, and an appropriate Python exception is set. */ static int -psutil_get_process_data(long pid, +psutil_get_process_data(DWORD pid, enum psutil_process_data_kind kind, WCHAR **pdata, SIZE_T *psize) { @@ -377,7 +377,7 @@ psutil_get_process_data(long pid, * AccessDenied. Requires Windows 8.1+. */ static int -psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { +psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { HANDLE hProcess = NULL; ULONG bufLen = 0; NTSTATUS status; @@ -470,7 +470,7 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { * with given pid or NULL on error. */ PyObject * -psutil_get_cmdline(long pid, int use_peb) { +psutil_get_cmdline(DWORD pid, int use_peb) { PyObject *ret = NULL; WCHAR *data = NULL; SIZE_T size; @@ -531,7 +531,7 @@ psutil_get_cmdline(long pid, int use_peb) { PyObject * -psutil_get_cwd(long pid) { +psutil_get_cwd(DWORD pid) { PyObject *ret = NULL; WCHAR *data = NULL; SIZE_T size; @@ -555,7 +555,7 @@ psutil_get_cwd(long pid) { * process with given pid or NULL on error. */ PyObject * -psutil_get_environ(long pid) { +psutil_get_environ(DWORD pid) { PyObject *ret = NULL; WCHAR *data = NULL; SIZE_T size; @@ -672,7 +672,7 @@ psutil_proc_info(PyObject *self, PyObject *args) { long long create_time; PyObject *py_retlist; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (! psutil_get_proc_info(pid, &process, &buffer)) return NULL; diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index 5fe342b3d5..5e89ddebdf 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -15,7 +15,7 @@ int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); -PyObject* psutil_get_cmdline(long pid, int use_peb); -PyObject* psutil_get_cwd(long pid); -PyObject* psutil_get_environ(long pid); +PyObject* psutil_get_cmdline(DWORD pid, int use_peb); +PyObject* psutil_get_cwd(DWORD pid); +PyObject* psutil_get_environ(DWORD pid); PyObject* psutil_proc_info(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index dbdebd484f..f9d2f2f93d 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -152,7 +152,6 @@ psutil_pid_is_running(DWORD pid) { return 1; if (pid < 0) return 0; - return psutil_pid_in_pids(pid); hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); @@ -167,9 +166,6 @@ psutil_pid_is_running(DWORD pid) { } CloseHandle(hProcess); - if ((PSUTIL_TESTING) && (psutil_pid_in_pids(pid) == 1)) { - PyErr_SetString(PyExc_AssertionError, "NULL handle but PID exists"); - return -1; - } - return 0; + PyErr_Clear(); + return psutil_pid_in_pids(pid); } diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 5272e127b7..cb19d5d23c 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -113,7 +113,7 @@ static DWORD __GetExtendedUdpTable(TYPE_GetExtendedUdpTable call, PyObject * psutil_net_connections(PyObject *self, PyObject *args) { static long null_address[4] = { 0, 0, 0, 0 }; - unsigned long pid; + DWORD pid; int pid_return; PVOID table = NULL; DWORD tableSize; @@ -126,7 +126,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { CHAR addressBufferLocal[65]; CHAR addressBufferRemote[65]; - PyObject *py_retlist; + PyObject *py_retlist = NULL; PyObject *py_conn_tuple = NULL; PyObject *py_af_filter = NULL; PyObject *py_type_filter = NULL; @@ -137,9 +137,11 @@ psutil_net_connections(PyObject *self, PyObject *args) { PyObject *_SOCK_STREAM = PyLong_FromLong((long)SOCK_STREAM); PyObject *_SOCK_DGRAM = PyLong_FromLong((long)SOCK_DGRAM); - // Import some functions. - if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, + &py_type_filter)) + { goto error; + } if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { psutil_conn_decref_objs(); diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 7cba4b78a4..e525e66724 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -372,10 +372,11 @@ 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=100000) + sysctl('vm.stats.sys.v_syscall'), delta=200000) # def test_cpu_stats_traps(self): # self.assertAlmostEqual(psutil.cpu_stats().traps, diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index aede66409f..c7fe199269 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -32,6 +32,7 @@ from psutil.tests import bind_socket from psutil.tests import bind_unix_socket from psutil.tests import check_net_address +from psutil.tests import CIRRUS from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import get_free_port @@ -191,7 +192,7 @@ class TestUnconnectedSockets(Base, unittest.TestCase): def get_conn_from_sock(self, sock): cons = thisproc.connections(kind='all') smap = dict([(c.fd, c) for c in cons]) - if NETBSD: + if NETBSD or FREEBSD: # NetBSD opens a UNIX socket to /var/log/run # so there may be more connections. return smap[sock.fileno()] @@ -321,8 +322,10 @@ def test_unix(self): 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' and - c.laddr] + cons = [c for c in cons if c.raddr != '/var/run/log'] + if CIRRUS: + cons = [c for c in cons if c.fd in + (server.fileno(), client.fileno())] self.assertEqual(len(cons), 2, msg=cons) if LINUX or FREEBSD or SUNOS: # remote path is never set diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 13a5bb4925..bdf055d03a 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -238,7 +238,8 @@ def test_cpu_count(self): @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq(self): - print(repr(psutil.cpu_freq())) + 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): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index f503b384f9..80bbd113d3 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -313,6 +313,7 @@ def test_warnings_on_misses(self): 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 10%. diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index ba75eef00b..132a0b07b4 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -23,6 +23,7 @@ import psutil import psutil._common +from psutil import FREEBSD from psutil import LINUX from psutil import MACOS from psutil import OPENBSD @@ -32,12 +33,13 @@ from psutil._common import bytes2human from psutil._compat import ProcessLookupError from psutil._compat import xrange +from psutil.tests import CIRRUS 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_GETLOADAVG from psutil.tests import HAS_IONICE from psutil.tests import HAS_MEMORY_MAPS from psutil.tests import HAS_NET_IO_COUNTERS @@ -365,14 +367,14 @@ def test_proc_info(self): self.execute(cext.proc_info, os.getpid()) +@unittest.skipIf(not WINDOWS, "WINDOWS only") 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_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) + def test_cmdline_peb_false(self): + self.execute(cext.proc_cmdline, os.getpid(), use_peb=False) class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @@ -485,8 +487,7 @@ 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)") + @unittest.skipIf(SUNOS, "worthless on SUNOS (uses a subprocess)") def test_swap_memory(self): self.execute(psutil.swap_memory) @@ -519,14 +520,14 @@ def test_pids(self): # --- net - @unittest.skipIf(TRAVIS and MACOS, "false positive on travis") + @unittest.skipIf(TRAVIS and MACOS, "false positive on TRAVIS + MACOS") + @unittest.skipIf(CIRRUS and FREEBSD, "false positive on CIRRUS + FREEBSD") @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)") + @skip_if_linux() @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") def test_net_connections(self): with create_sockets(): @@ -564,7 +565,6 @@ def test_sensors_fans(self): 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) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b1b3075017..0b54f5b186 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -24,7 +24,6 @@ from psutil import AIX from psutil import BSD -from psutil import FREEBSD from psutil import LINUX from psutil import MACOS from psutil import NETBSD @@ -1085,17 +1084,13 @@ def test_parent_disappeared(self): side_effect=psutil.NoSuchProcess(0, 'foo')): self.assertIsNone(p.parent()) + @retry_on_failure() 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 and not FREEBSD: - # On FreeBSD PID 1 has an older/smaller time than PID 0 (?) - 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) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 6308de097b..6b1d720e7f 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -65,6 +65,7 @@ from psutil._compat import PY3 from psutil._compat import u from psutil.tests import APPVEYOR +from psutil.tests import CIRRUS from psutil.tests import ASCII_FS from psutil.tests import bind_unix_socket from psutil.tests import chdir @@ -233,7 +234,7 @@ def test_proc_connections(self): conn = psutil.Process().connections('unix')[0] self.assertIsInstance(conn.laddr, str) # AF_UNIX addr not set on OpenBSD - if not OPENBSD: + if not OPENBSD and not CIRRUS: # XXX self.assertEqual(conn.laddr, name) @unittest.skipIf(not POSIX, "POSIX only") diff --git a/setup.py b/setup.py index 774d566a9f..3a3889cfcb 100755 --- a/setup.py +++ b/setup.py @@ -158,7 +158,7 @@ def get_winver(): "psapi", "kernel32", "advapi32", "shell32", "netapi32", "wtsapi32", "ws2_32", "PowrProf", "pdh", ], - # extra_compile_args=["/Z7"], + # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"] ) From f4e337aa04dac93347118f488491941026ec7e4f Mon Sep 17 00:00:00 2001 From: Kamil Rytarowski Date: Thu, 30 Jan 2020 00:40:27 +0100 Subject: [PATCH 0465/1714] Add special case for psutil_proc_cwd/NetBSD (#1538) --- psutil/arch/netbsd/specific.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 04eab439a9..9dab361839 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -125,7 +125,10 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { #ifdef KERN_PROC_CWD int name[] = { CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (errno == ENOENT) + NoSuchProcess(""); + else + PyErr_SetFromErrno(PyExc_OSError); return NULL; } #else From dfb522f43f07baa67794cb418e7368664c77a4f3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Jan 2020 00:41:16 +0100 Subject: [PATCH 0466/1714] update HISTORY --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 9b6dd98439..848d3559ae 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,7 @@ XXXX-XX-XX **Bug fixes** +- 1538_: [NetBSD] process cwd() may return ENOENT instead of NoSuchProcess. - 1642_: [SunOS] querying basic info for PID 0 results in FileNotFoundError. - 1646_: [FreeBSD] many Process methods may cause a segfault on FreeBSD 12.0 due to a backward incompatible change in a C type introduced in 12.0. From 795c1e1dcc5f6965370070c1dd77642ba80387f8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Feb 2020 03:06:18 +0100 Subject: [PATCH 0467/1714] small C refactoring --- psutil/_psutil_common.c | 19 ------------------- psutil/_psutil_common.h | 7 +++++-- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 0c86a4e6b0..990d59a6a1 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -23,25 +23,6 @@ int PSUTIL_TESTING = 0; // --- Python functions and backward compatibility // ==================================================================== -/* - * Backport of unicode FS APIs from Python 3. - * On Python 2 we just return a plain byte string - * which is never supposed to raise decoding errors. - * See: https://github.com/giampaolo/psutil/issues/1040 - */ -#if PY_MAJOR_VERSION < 3 -PyObject * -PyUnicode_DecodeFSDefault(char *s) { - return PyString_FromString(s); -} - - -PyObject * -PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size) { - return PyString_FromStringAndSize(s, size); -} -#endif - /* * Same as PyErr_SetFromErrno(0) but adds the syscall to the exception * message. diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index d45cf56be4..60ea6298f8 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -20,8 +20,11 @@ static const int PSUTIL_CONN_NONE = 128; // ==================================================================== #if PY_MAJOR_VERSION < 3 - PyObject* PyUnicode_DecodeFSDefault(char *s); - PyObject* PyUnicode_DecodeFSDefaultAndSize(char *s, Py_ssize_t size); + // On Python 2 we just return a plain byte string, which is never + // supposed to raise decoding errors, see: + // https://github.com/giampaolo/psutil/issues/1040 + #define PyUnicode_DecodeFSDefault PyString_FromString + #define PyUnicode_DecodeFSDefaultAndSize PyString_FromStringAndSize #endif // Python 2: SIZEOF_PID_T not defined but _getpid() returns an int. From 96cc7ea40d6f4f08e86677434213cef119cf1748 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Feb 2020 03:44:51 +0100 Subject: [PATCH 0468/1714] [Windows] use NtQuerySystemInformation to determine process exe() (#1677) --- HISTORY.rst | 2 + docs/index.rst | 4 +- psutil/_psutil_windows.c | 116 +++++++++++++++++---------------- psutil/_pswindows.py | 21 +++--- psutil/arch/windows/ntextapi.h | 8 +++ psutil/tests/__init__.py | 5 +- psutil/tests/test_contracts.py | 2 + psutil/tests/test_process.py | 10 +++ psutil/tests/test_unicode.py | 26 +------- psutil/tests/test_windows.py | 31 ++------- 10 files changed, 104 insertions(+), 121 deletions(-) mode change 100755 => 100644 psutil/tests/test_unicode.py diff --git a/HISTORY.rst b/HISTORY.rst index 848d3559ae..7bb4fffa48 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,8 @@ XXXX-XX-XX Minimum supported Windows version now is Windows Vista. - 1667_: added process_iter(new_only=True) parameter. - 1671_: [FreeBSD] add CI testing/service for FreeBSD (Cirrus CI). +- 1677_: [Windows] process exe() will succeed for all process PIDs (instead of + raising AccessDenied). **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index b1c4ab7411..74dddd2332 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1098,9 +1098,9 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`gids` | | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_ctx_switches` | | :meth:`ppid` | :meth:`ppid` | | | + | :meth:`num_ctx_switches` | :meth:`exe` | :meth:`ppid` | :meth:`ppid` | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_threads` | | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | + | :meth:`num_threads` | :meth:`name` | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`uids` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index cabfe9348f..e63cf97f34 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -446,74 +446,74 @@ psutil_proc_environ(PyObject *self, PyObject *args) { /* - * Return process executable path. + * Return process executable path. Works for all processes regardless of + * privilege. NtQuerySystemInformation has some sort of internal cache, + * since it succeeds even when a process is gone (but not if a PID never + * existed). */ static PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { DWORD pid; - HANDLE hProcess; - wchar_t exe[MAX_PATH]; - unsigned int size = sizeof(exe); + NTSTATUS status; + PVOID buffer; + ULONG bufferSize = 0x100; + SYSTEM_PROCESS_ID_INFORMATION processIdInfo; + PyObject *py_exe; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (NULL == hProcess) - return NULL; + if (pid == 0) + return AccessDenied("forced for PID 0"); - memset(exe, 0, MAX_PATH); - if (QueryFullProcessImageNameW(hProcess, 0, exe, &size) == 0) { - // https://github.com/giampaolo/psutil/issues/1662 - if (GetLastError() == 0) - AccessDenied("QueryFullProcessImageNameW (forced EPERM)"); + buffer = MALLOC_ZERO(bufferSize); + if (! buffer) + return PyErr_NoMemory(); + processIdInfo.ProcessId = (HANDLE)(ULONG_PTR)pid; + processIdInfo.ImageName.Length = 0; + processIdInfo.ImageName.MaximumLength = (USHORT)bufferSize; + processIdInfo.ImageName.Buffer = buffer; + + status = NtQuerySystemInformation( + SystemProcessIdInformation, + &processIdInfo, + sizeof(SYSTEM_PROCESS_ID_INFORMATION), + NULL); + + if (status == STATUS_INFO_LENGTH_MISMATCH) { + // Required length is stored in MaximumLength. + FREE(buffer); + buffer = MALLOC_ZERO(processIdInfo.ImageName.MaximumLength); + if (! buffer) + return PyErr_NoMemory(); + processIdInfo.ImageName.Buffer = buffer; + + status = NtQuerySystemInformation( + SystemProcessIdInformation, + &processIdInfo, + sizeof(SYSTEM_PROCESS_ID_INFORMATION), + NULL); + } + + if (! NT_SUCCESS(status)) { + FREE(buffer); + if (psutil_pid_is_running(pid) == 0) + NoSuchProcess("NtQuerySystemInformation"); else - PyErr_SetFromOSErrnoWithSyscall("QueryFullProcessImageNameW"); - CloseHandle(hProcess); + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); return NULL; } - CloseHandle(hProcess); - return PyUnicode_FromWideChar(exe, wcslen(exe)); -} - -/* - * Return process base name. - * Note: psutil_proc_exe() is attempted first because it's faster - * but it raise AccessDenied for processes owned by other users - * in which case we fall back on using this. - */ -static PyObject * -psutil_proc_name(PyObject *self, PyObject *args) { - DWORD pid; - int ok; - PROCESSENTRY32W pentry; - HANDLE hSnapShot; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, pid); - if (hSnapShot == INVALID_HANDLE_VALUE) - return PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); - pentry.dwSize = sizeof(PROCESSENTRY32W); - ok = Process32FirstW(hSnapShot, &pentry); - if (! ok) { - PyErr_SetFromOSErrnoWithSyscall("Process32FirstW"); - CloseHandle(hSnapShot); - return NULL; + if (processIdInfo.ImageName.Buffer == NULL) { + // Happens for PID 4. + py_exe = Py_BuildValue("s", ""); } - while (ok) { - if (pentry.th32ProcessID == pid) { - CloseHandle(hSnapShot); - return PyUnicode_FromWideChar( - pentry.szExeFile, wcslen(pentry.szExeFile)); - } - ok = Process32NextW(hSnapShot, &pentry); + else { + py_exe = PyUnicode_FromWideChar(processIdInfo.ImageName.Buffer, + processIdInfo.ImageName.Length / 2); } - - CloseHandle(hSnapShot); - NoSuchProcess("CreateToolhelp32Snapshot loop (no PID found)"); - return NULL; + FREE(buffer); + return py_exe; } @@ -587,6 +587,10 @@ psutil_GetProcWsetInformation( bufferSize = 0x8000; buffer = MALLOC_ZERO(bufferSize); + if (! buffer) { + PyErr_NoMemory(); + return 1; + } while ((status = NtQueryVirtualMemory( hProcess, @@ -605,6 +609,10 @@ psutil_GetProcWsetInformation( return 1; } buffer = MALLOC_ZERO(bufferSize); + if (! buffer) { + PyErr_NoMemory(); + return 1; + } } if (!NT_SUCCESS(status)) { @@ -1602,8 +1610,6 @@ PsutilMethods[] = { "Return process environment data"}, {"proc_exe", psutil_proc_exe, METH_VARARGS, "Return path of the process executable"}, - {"proc_name", psutil_proc_name, METH_VARARGS, - "Return process name"}, {"proc_kill", psutil_proc_kill, METH_VARARGS, "Kill the process identified by the given PID"}, {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 3e14bc3cec..83793c5ae6 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -721,9 +721,11 @@ def __init__(self, pid): def oneshot_enter(self): self.oneshot_info.cache_activate(self) + self.exe.cache_activate(self) def oneshot_exit(self): self.oneshot_info.cache_deactivate(self) + self.exe.cache_deactivate(self) @wrap_exceptions @memoize_when_activated @@ -735,7 +737,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. @@ -744,20 +745,19 @@ 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): exe = cext.proc_exe(self.pid) - return py2_strencode(exe) + 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 @@ -843,7 +843,6 @@ def memory_maps(self): 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) diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index b7b1c9765d..8cb00430e2 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -33,6 +33,8 @@ typedef LONG NTSTATUS; #define ProcessIoPriority 33 #undef ProcessWow64Information #define ProcessWow64Information 26 +#undef SystemProcessIdInformation +#define SystemProcessIdInformation 88 // process suspend() / resume() typedef enum _KTHREAD_STATE { @@ -362,6 +364,12 @@ typedef struct _PSUTIL_PROCESS_WS_COUNTERS { SIZE_T NumberOfShareablePages; } PSUTIL_PROCESS_WS_COUNTERS, *PPSUTIL_PROCESS_WS_COUNTERS; +// exe() +typedef struct _SYSTEM_PROCESS_ID_INFORMATION { + HANDLE ProcessId; + UNICODE_STRING ImageName; +} SYSTEM_PROCESS_ID_INFORMATION, *PSYSTEM_PROCESS_ID_INFORMATION; + // ==================================================================== // PEB structs for cmdline(), cwd(), environ() // ==================================================================== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 0c297b9b02..5251983f64 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -206,9 +206,6 @@ def attempt(exe): 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 @@ -366,7 +363,7 @@ def create_proc_children_pair(): s += "f.write(str(os.getpid()));" s += "f.close();" s += "time.sleep(60);" - p = subprocess.Popen(['%s', '-c', s]) + p = subprocess.Popen([r'%s', '-c', s]) p.wait() """ % (_TESTFN2, PYTHON_EXE)) # On Windows if we create a subprocess with CREATE_NO_WINDOW flag diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index bdf055d03a..5e258b3015 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -431,6 +431,8 @@ def exe(self, ret, proc): if not ret: self.assertEqual(ret, '') else: + 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: diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0b54f5b186..5728f183de 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1325,6 +1325,16 @@ def test_halfway_terminated_process(self): except NotImplementedError: pass else: + # NtQuerySystemInformation succeeds if process is gone. + if WINDOWS and name in ('exe', 'name'): + normcase = os.path.normcase + if name == 'exe': + self.assertEqual(normcase(ret), normcase(PYTHON_EXE)) + else: + self.assertEqual( + normcase(ret), + normcase(os.path.basename(PYTHON_EXE))) + continue self.fail( "NoSuchProcess exception not raised for %r, retval=%s" % ( name, ret)) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py old mode 100755 new mode 100644 index 6b1d720e7f..91395ebfc8 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -61,7 +61,6 @@ 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 @@ -75,7 +74,6 @@ 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 @@ -171,16 +169,7 @@ def test_proc_exe(self): 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() + 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)) @@ -321,19 +310,6 @@ def expect_exact_path_match(cls): 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 # =================================================================== diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index de41aed8dd..6d4e8599e0 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -326,20 +326,6 @@ 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() @@ -524,6 +510,13 @@ def test_error_partial_copy(self): 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) + @unittest.skipIf(not WINDOWS, "WINDOWS only") class TestProcessWMI(unittest.TestCase): @@ -610,16 +603,6 @@ def setUpClass(cls): @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() From 2a253a1cdba398c6db8a521c4eb8899bf1fa113f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Feb 2020 11:28:03 -0800 Subject: [PATCH 0469/1714] print-ad.py: double sort --- scripts/internal/print_access_denied.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index b94e6e00c4..2c757fd782 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -76,7 +76,7 @@ def main(): templ = "%-20s %-5s %-9s %s" s = templ % ("API", "AD", "Percent", "Outcome") print(hilite(s, ok=None, bold=True)) - for methname, ads in sorted(d.items(), key=lambda x: x[1]): + for methname, ads in sorted(d.items(), key=lambda x: (x[1], x[0])): perc = (ads / tot_procs) * 100 outcome = "SUCCESS" if not ads else "ACCESS DENIED" s = templ % (methname, ads, "%6.1f%%" % perc, outcome) From adb558c1d03ef4be87192eb5db57f816b9590ec2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Feb 2020 22:59:49 +0100 Subject: [PATCH 0470/1714] [Windows] connections() refactoring (#1678) --- psutil/arch/windows/socks.c | 164 +++++++++++------------- scripts/internal/print_access_denied.py | 2 +- 2 files changed, 73 insertions(+), 93 deletions(-) diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index cb19d5d23c..61f18c07c6 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -16,87 +16,81 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) -typedef DWORD (WINAPI * TYPE_GetExtendedTcpTable)(); -typedef DWORD (WINAPI * TYPE_GetExtendedUdpTable)(); - - -static DWORD __GetExtendedTcpTable(TYPE_GetExtendedTcpTable call, - ULONG family, - PVOID *data, - DWORD *size) -{ - // Due to other processes being active on the machine, it's possible - // that the size of the table increases between the moment where we - // query the size and the moment where we query the data. Therefore, it's - // important to call this in a loop to retry if that happens. - // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error - // and https://github.com/giampaolo/psutil/issues/1294 - DWORD error = ERROR_INSUFFICIENT_BUFFER; - *size = 0; - *data = NULL; - error = call(NULL, size, FALSE, family, TCP_TABLE_OWNER_PID_ALL, 0); - while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) - { - *data = malloc(*size); - if (*data == NULL) { - error = ERROR_NOT_ENOUGH_MEMORY; - continue; +#define STATUS_UNSUCCESSFUL 0xC0000001 + + +// Note about GetExtended[Tcp|Udp]Table syscalls: due to other processes +// being active on the machine, it's possible that the size of the table +// increases between the moment where we query the size and the moment +// where we query the data. Therefore we call them in a loop to retry if +// that happens. See: +// https://github.com/giampaolo/psutil/pull/1335 +// https://github.com/giampaolo/psutil/issues/1294 + + +static PVOID __GetExtendedTcpTable(ULONG family) { + DWORD err; + PVOID table; + ULONG size = 0; + + while (1) { + // get table size + GetExtendedTcpTable(NULL, &size, FALSE, family, + TCP_TABLE_OWNER_PID_ALL, 0); + + table = malloc(size); + if (table == NULL) { + PyErr_NoMemory(); + return NULL; } - error = call(*data, size, FALSE, family, TCP_TABLE_OWNER_PID_ALL, 0); - if (error != NO_ERROR) { - free(*data); - *data = NULL; + + // get connections + err = GetExtendedTcpTable(table, &size, FALSE, family, + TCP_TABLE_OWNER_PID_ALL, 0); + if (err == NO_ERROR) + return table; + + free(table); + if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { + psutil_debug("GetExtendedTcpTable: retry with different bufsize"); + continue; } - } - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - return 1; - } - if (error != NO_ERROR) { PyErr_SetString(PyExc_RuntimeError, "GetExtendedTcpTable failed"); - return 1; + return NULL; } - return error; } -static DWORD __GetExtendedUdpTable(TYPE_GetExtendedUdpTable call, - ULONG family, - PVOID * data, - DWORD * size) -{ - // Due to other processes being active on the machine, it's possible - // that the size of the table increases between the moment where we - // query the size and the moment where we query the data. Therefore, it's - // important to call this in a loop to retry if that happens. - // See https://github.com/giampaolo/psutil/pull/1335 concerning 0xC0000001 error - // and https://github.com/giampaolo/psutil/issues/1294 - DWORD error = ERROR_INSUFFICIENT_BUFFER; - *size = 0; - *data = NULL; - error = call(NULL, size, FALSE, family, UDP_TABLE_OWNER_PID, 0); - while (error == ERROR_INSUFFICIENT_BUFFER || error == 0xC0000001) - { - *data = malloc(*size); - if (*data == NULL) { - error = ERROR_NOT_ENOUGH_MEMORY; - continue; +static PVOID __GetExtendedUdpTable(ULONG family) { + DWORD err; + PVOID table; + ULONG size = 0; + + while (1) { + // get table size + GetExtendedUdpTable(NULL, &size, FALSE, family, + UDP_TABLE_OWNER_PID, 0); + + table = malloc(size); + if (table == NULL) { + PyErr_NoMemory(); + return NULL; } - error = call(*data, size, FALSE, family, UDP_TABLE_OWNER_PID, 0); - if (error != NO_ERROR) { - free(*data); - *data = NULL; + + // get connections + err = GetExtendedUdpTable(table, &size, FALSE, family, + UDP_TABLE_OWNER_PID, 0); + if (err == NO_ERROR) + return table; + + free(table); + if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { + psutil_debug("GetExtendedUdpTable: retry with different bufsize"); + continue; } - } - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - return 1; - } - if (error != NO_ERROR) { PyErr_SetString(PyExc_RuntimeError, "GetExtendedUdpTable failed"); - return 1; + return NULL; } - return 0; } @@ -116,8 +110,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { DWORD pid; int pid_return; PVOID table = NULL; - DWORD tableSize; - DWORD error; PMIB_TCPTABLE_OWNER_PID tcp4Table; PMIB_UDPTABLE_OWNER_PID udp4Table; PMIB_TCP6TABLE_OWNER_PID tcp6Table; @@ -176,11 +168,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_conn_tuple = NULL; py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; - tableSize = 0; - error = __GetExtendedTcpTable(GetExtendedTcpTable, - AF_INET, &table, &tableSize); - if (error != 0) + table = __GetExtendedTcpTable(AF_INET); + if (table == NULL) goto error; tcp4Table = table; for (i = 0; i < tcp4Table->dwNumEntries; i++) { @@ -250,7 +240,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { free(table); table = NULL; - tableSize = 0; } // TCP IPv6 @@ -262,11 +251,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_conn_tuple = NULL; py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; - tableSize = 0; - error = __GetExtendedTcpTable(GetExtendedTcpTable, - AF_INET6, &table, &tableSize); - if (error != 0) + table = __GetExtendedTcpTable(AF_INET6); + if (table == NULL) goto error; tcp6Table = table; for (i = 0; i < tcp6Table->dwNumEntries; i++) @@ -337,7 +324,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { free(table); table = NULL; - tableSize = 0; } // UDP IPv4 @@ -349,10 +335,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_conn_tuple = NULL; py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; - tableSize = 0; - error = __GetExtendedUdpTable(GetExtendedUdpTable, - AF_INET, &table, &tableSize); - if (error != 0) + table = __GetExtendedUdpTable(AF_INET); + if (table == NULL) goto error; udp4Table = table; for (i = 0; i < udp4Table->dwNumEntries; i++) @@ -400,7 +384,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { free(table); table = NULL; - tableSize = 0; } // UDP IPv6 @@ -413,10 +396,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_conn_tuple = NULL; py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; - tableSize = 0; - error = __GetExtendedUdpTable(GetExtendedUdpTable, - AF_INET6, &table, &tableSize); - if (error != 0) + table = __GetExtendedUdpTable(AF_INET6); + if (table == NULL) goto error; udp6Table = table; for (i = 0; i < udp6Table->dwNumEntries; i++) { @@ -463,7 +444,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { free(table); table = NULL; - tableSize = 0; } psutil_conn_decref_objs(); diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index b94e6e00c4..2c757fd782 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -76,7 +76,7 @@ def main(): templ = "%-20s %-5s %-9s %s" s = templ % ("API", "AD", "Percent", "Outcome") print(hilite(s, ok=None, bold=True)) - for methname, ads in sorted(d.items(), key=lambda x: x[1]): + for methname, ads in sorted(d.items(), key=lambda x: (x[1], x[0])): perc = (ads / tot_procs) * 100 outcome = "SUCCESS" if not ads else "ACCESS DENIED" s = templ % (methname, ads, "%6.1f%%" % perc, outcome) From fe988700b383246cbfc8540920039816cab0812e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 4 Feb 2020 14:56:07 +0100 Subject: [PATCH 0471/1714] #1610: clarify in the doc what cpu_count means --- docs/index.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 74dddd2332..f552bbfa3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -186,8 +186,10 @@ CPU Return the number of logical CPUs in the system (same as `os.cpu_count`_ in Python 3.4) or ``None`` if undetermined. - If *logical* is ``False`` return the number of physical cores only (hyper - thread CPUs are excluded) or ``None`` if undetermined. + *logical* cores means the number of physical cores multiplied by the number + of threads that can run on each core (this is known as Hyper Threading). + If *logical* is ``False`` return the number of physical cores only (Hyper + Thread CPUs are excluded) or ``None`` if undetermined. On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return ``None``. Example on a system having 2 physical hyper-thread CPU cores: From 3d29963a558953d5d64f70f06a4d1568b91b0a18 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Feb 2020 17:23:56 +0100 Subject: [PATCH 0472/1714] [Windows] speedup connections (#1679) --- HISTORY.rst | 1 + psutil/arch/windows/socks.c | 108 ++++++++++++++++++++---------------- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7bb4fffa48..379965cf78 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,7 @@ XXXX-XX-XX - 1671_: [FreeBSD] add CI testing/service for FreeBSD (Cirrus CI). - 1677_: [Windows] process exe() will succeed for all process PIDs (instead of raising AccessDenied). +- 1679_: [Windows] net_connections() and Process.connections() are 10% faster. **Bug fixes** diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 61f18c07c6..5e4c2df802 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -18,79 +18,89 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) #define STATUS_UNSUCCESSFUL 0xC0000001 +ULONG g_TcpTableSize = 0; +ULONG g_UdpTableSize = 0; + // Note about GetExtended[Tcp|Udp]Table syscalls: due to other processes // being active on the machine, it's possible that the size of the table -// increases between the moment where we query the size and the moment -// where we query the data. Therefore we call them in a loop to retry if -// that happens. See: +// increases between the moment we query the size and the moment we query +// the data. Therefore we retry if that happens. See: // https://github.com/giampaolo/psutil/pull/1335 // https://github.com/giampaolo/psutil/issues/1294 +// A global and ever increasing size is used in order to avoid calling +// GetExtended[Tcp|Udp]Table twice per call (faster). static PVOID __GetExtendedTcpTable(ULONG family) { DWORD err; PVOID table; - ULONG size = 0; - - while (1) { - // get table size - GetExtendedTcpTable(NULL, &size, FALSE, family, - TCP_TABLE_OWNER_PID_ALL, 0); + ULONG size; + TCP_TABLE_CLASS class = TCP_TABLE_OWNER_PID_ALL; + + size = g_TcpTableSize; + if (size == 0) { + GetExtendedTcpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space + size = size + (size / 2 / 2); + g_TcpTableSize = size; + } - table = malloc(size); - if (table == NULL) { - PyErr_NoMemory(); - return NULL; - } + table = malloc(size); + if (table == NULL) { + PyErr_NoMemory(); + return NULL; + } - // get connections - err = GetExtendedTcpTable(table, &size, FALSE, family, - TCP_TABLE_OWNER_PID_ALL, 0); - if (err == NO_ERROR) - return table; + err = GetExtendedTcpTable(table, &size, FALSE, family, class, 0); + if (err == NO_ERROR) + return table; - free(table); - if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { - psutil_debug("GetExtendedTcpTable: retry with different bufsize"); - continue; - } - PyErr_SetString(PyExc_RuntimeError, "GetExtendedTcpTable failed"); - return NULL; + free(table); + if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { + psutil_debug("GetExtendedTcpTable: retry with different bufsize"); + g_TcpTableSize = 0; + return __GetExtendedTcpTable(family); } + + PyErr_SetString(PyExc_RuntimeError, "GetExtendedTcpTable failed"); + return NULL; } static PVOID __GetExtendedUdpTable(ULONG family) { DWORD err; PVOID table; - ULONG size = 0; - - while (1) { - // get table size - GetExtendedUdpTable(NULL, &size, FALSE, family, - UDP_TABLE_OWNER_PID, 0); + ULONG size; + UDP_TABLE_CLASS class = UDP_TABLE_OWNER_PID; + + size = g_UdpTableSize; + if (size == 0) { + GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space + size = size + (size / 2 / 2); + g_UdpTableSize = size; + } - table = malloc(size); - if (table == NULL) { - PyErr_NoMemory(); - return NULL; - } + table = malloc(size); + if (table == NULL) { + PyErr_NoMemory(); + return NULL; + } - // get connections - err = GetExtendedUdpTable(table, &size, FALSE, family, - UDP_TABLE_OWNER_PID, 0); - if (err == NO_ERROR) - return table; + err = GetExtendedUdpTable(table, &size, FALSE, family, class, 0); + if (err == NO_ERROR) + return table; - free(table); - if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { - psutil_debug("GetExtendedUdpTable: retry with different bufsize"); - continue; - } - PyErr_SetString(PyExc_RuntimeError, "GetExtendedUdpTable failed"); - return NULL; + free(table); + if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { + psutil_debug("GetExtendedUdpTable: retry with different bufsize"); + g_UdpTableSize = 0; + return __GetExtendedUdpTable(family); } + + PyErr_SetString(PyExc_RuntimeError, "GetExtendedUdpTable failed"); + return NULL; } From 6a4fbe1bd9da21684eecd77a92f168af8d12d950 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 5 Feb 2020 16:29:11 +0000 Subject: [PATCH 0473/1714] Reference exact CVE in HISTORY for 5.6.6 (#1653) --- HISTORY.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 379965cf78..ca651f0405 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -53,7 +53,9 @@ XXXX-XX-XX - 1179_: [Linux] Process cmdline() now takes into account misbehaving processes renaming the command line and using inappropriate chars to separate args. - 1616_: use of Py_DECREF instead of Py_CLEAR will result in double free and - segfault (CVE). (patch by Riccardo Schirone) + segfault + (`CVE-2019-18874 `__). + (patch by Riccardo Schirone) - 1619_: [OpenBSD] compilation fails due to C syntax error. (patch by Nathan Houghton) From fe481ed53fae8b9ce814edf224ffbf14aa2cbbcc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Feb 2020 18:49:45 +0100 Subject: [PATCH 0474/1714] fix #1674, SunOS: disk_partitions() may raise OSError. --- HISTORY.rst | 3 ++- psutil/_pssunos.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ca651f0405..0277db5893 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -33,6 +33,7 @@ XXXX-XX-XX - 1672_: properly handle PID C type. - 1673_: [OpenBSD] Process connections(), num_fds() and threads() returned improper exception if process is gone. +- 1674_: [SunOS] disk_partitions() may raise OSError. 5.6.7 ===== @@ -54,7 +55,7 @@ XXXX-XX-XX renaming the command line and using inappropriate chars to separate args. - 1616_: use of Py_DECREF instead of Py_CLEAR will result in double free and segfault - (`CVE-2019-18874 `__). + (`CVE-2019-18874 `__). (patch by Riccardo Schirone) - 1619_: [OpenBSD] compilation fails due to C syntax error. (patch by Nathan Houghton) diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index b1e2d1c94d..b82771ee4f 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -225,7 +225,11 @@ 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: + # https://github.com/giampaolo/psutil/issues/1674 continue ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) From 72274ac02bd9b3b0c08acf861a2df223538b1c9e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Feb 2020 19:07:48 +0100 Subject: [PATCH 0475/1714] update doc --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index f552bbfa3b..256a658038 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -388,7 +388,7 @@ Disks mount point and filesystem type, similarly to "df" command on UNIX. If *all* parameter is ``False`` it tries to distinguish and return physical devices only (e.g. hard disks, cd-rom drives, USB keys) and ignore all others - (e.g. memory partitions such as /dev/shm). + (e.g. pseudo, memory, duplicate, inaccessible filesystems). Note that this may not be fully reliable on all systems (e.g. on BSD this parameter is ignored). Named tuple's **fstype** field is a string which varies depending on the From 13cf7d7ab356233c3bb8dc34f1143e7bb0c2c088 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 9 Feb 2020 21:07:45 +0100 Subject: [PATCH 0476/1714] fix #1650 [Linux] sensors_temperatures() no longer emit warnings on file not found (print debug msg instead) --- psutil/_common.py | 12 ++++++++++-- psutil/_pslinux.py | 4 ++-- psutil/_pssunos.py | 4 +++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 729b198350..453c771d5e 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -7,7 +7,7 @@ # Note: this module is imported by setup.py so it should not import # psutil or third-party modules. -from __future__ import division +from __future__ import division, print_function import contextlib import errno @@ -66,7 +66,7 @@ '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", - 'bytes2human', 'conn_to_ntuple', 'hilite', + 'bytes2human', 'conn_to_ntuple', 'hilite', 'debug', ] @@ -787,3 +787,11 @@ def hilite(s, ok=True, bold=False): if bold: attr.append('1') return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) + + +if bool(os.getenv('PSUTIL_DEBUG', 0)): + def debug(msg): + print("psutil-debug> " + msg, file=sys.stderr) +else: + def debug(msg): + pass diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 0693511110..7348b3dcba 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -26,6 +26,7 @@ from . import _psutil_linux as cext from . import _psutil_posix as cext_posix from ._common import AccessDenied +from ._common import debug from ._common import decode from ._common import get_procfs_path from ._common import isfile_strict @@ -1253,8 +1254,7 @@ def sensors_temperatures(): path = os.path.join(base, 'type') unit_name = cat(path, binary=False) except (IOError, OSError, ValueError) as err: - warnings.warn("ignoring %r for file %r" % (err, path), - RuntimeWarning) + debug("ignoring %r for file %r" % (err, path)) continue trip_paths = glob.glob(base + '/trip_point*') diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index b82771ee4f..62362b89c6 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -19,6 +19,7 @@ from . import _psutil_sunos as cext from ._common import AccessDenied from ._common import AF_INET6 +from ._common import debug from ._common import get_procfs_path from ._common import isfile_strict from ._common import memoize_when_activated @@ -228,8 +229,9 @@ def disk_partitions(all=False): try: if not disk_usage(mountpoint).total: continue - except OSError: + except OSError as err: # https://github.com/giampaolo/psutil/issues/1674 + debug("skipping %r: %r" % (mountpoint, err)) continue ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) From 796b2dda2e0d8751eee0a4d16ab8c027839f8908 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 9 Feb 2020 21:29:35 +0100 Subject: [PATCH 0477/1714] [Linux] disk_io_counters() ValueError when parsing /sys/block (#1684) Fixes: ``` ====================================================================== ERROR: psutil.tests.test_linux.TestSystemDiskIoCounters.test_emulate_use_sysfs ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/tests/test_linux.py", line 1195, in test_emulate_use_sysfs wsysfs = psutil.disk_io_counters(perdisk=True) File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 2065, in disk_io_counters rawdict = _psplatform.disk_io_counters(**kwargs) File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1124, in disk_io_counters for entry in gen: File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1110, in read_sysfs wbytes, wtime, _, busy_time, _) = map(int, fields) ValueError: too many values to unpack (expected 11) ``` --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 2 +- psutil/tests/test_linux.py | 23 ++--------------------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0277db5893..2a9c046bfd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -34,6 +34,8 @@ XXXX-XX-XX - 1673_: [OpenBSD] Process connections(), num_fds() and threads() returned improper exception if process is gone. - 1674_: [SunOS] disk_partitions() may raise OSError. +- 1684_: [Linux] disk_io_counters() may raise ValueError on systems not + having /proc/diskstats. 5.6.7 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7348b3dcba..ba65aea93b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1106,7 +1106,7 @@ def read_sysfs(): fields = f.read().strip().split() name = os.path.basename(root) (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) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 80bbd113d3..97946a0bbd 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -316,7 +316,7 @@ def test_warnings_on_misses(self): @retry_on_failure() def test_avail_old_percent(self): # Make sure that our calculation of avail mem for old kernels - # is off by max 10%. + # is off by max 15%. from psutil._pslinux import calculate_avail_vmem from psutil._pslinux import open_binary @@ -330,7 +330,7 @@ def test_avail_old_percent(self): if b'MemAvailable:' in mems: b = mems[b'MemAvailable:'] diff_percent = abs(a - b) / a * 100 - self.assertLess(diff_percent, 10) + self.assertLess(diff_percent, 15) def test_avail_old_comes_from_kernel(self): # Make sure "MemAvailable:" coluimn is used instead of relying @@ -1548,25 +1548,6 @@ def test_emulate_no_power(self): @unittest.skipIf(not LINUX, "LINUX only") class TestSensorsTemperatures(unittest.TestCase): - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @unittest.skipIf(LINUX and EMPTY_TEMPERATURES, "no temperatures") - 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'): From 05758f6978aefea709150898ae285b3ff3688c7c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 9 Feb 2020 21:55:23 +0100 Subject: [PATCH 0478/1714] #1672, #1682: SIZEOF_INT is not available on pypy3; assume that on systems where pid_t size can't be determined at runtime pid_t is an int --- psutil/_psutil_common.h | 23 +++++++++++++++-------- psutil/_psutil_linux.c | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 60ea6298f8..e91bf2dd9f 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -27,15 +27,22 @@ static const int PSUTIL_CONN_NONE = 128; #define PyUnicode_DecodeFSDefaultAndSize PyString_FromStringAndSize #endif -// Python 2: SIZEOF_PID_T not defined but _getpid() returns an int. -#if defined(PSUTIL_WINDOWS) && !defined(SIZEOF_PID_T) - #define SIZEOF_PID_T SIZEOF_INT +// --- _Py_PARSE_PID + +// SIZEOF_INT|LONG is missing on Linux + PyPy (only?) +// SIZEOF_PID_T is missing on Windows + Python2 +// In we can't determine we assume PID is an (int). +// On major UNIX platforms I've seen pid_t is treated as int. +// On Windows _getpid() returns an int. We can't be 100% certain though, +// (in that case we'd probably get compiler warnings). +#if !defined(SIZEOF_INT) + #define SIZEOF_INT 4 #endif - -#if !defined(_Py_PARSE_PID) || PY_MAJOR_VERSION < 3 - #if !defined(SIZEOF_PID_T) || !defined(SIZEOF_INT) || !defined(SIZEOF_LONG) - #error "missing SIZEOF* definition" - #endif +#if !defined(SIZEOF_LONG) + #define SIZEOF_LONG 8 +#endif +#if !defined(SIZEOF_PID_T) + #define SIZEOF_PID_T 4 // assume int #endif // _Py_PARSE_PID is Python 3 only, but since it's private make sure it's diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 93cc071b5a..ec8b433aa7 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -204,7 +204,7 @@ static PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { FILE *file = NULL; struct mntent *entry; - const char *mtab_path; + char *mtab_path; PyObject *py_dev = NULL; PyObject *py_mountp = NULL; PyObject *py_tuple = NULL; From 17b702945351f91bb3dc61010eddf26b68fd596b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 9 Feb 2020 22:11:51 +0100 Subject: [PATCH 0479/1714] Fix test errors for PYPY. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It looks like PYPY implementation has some issues with strings (or maybe it's a subprocess bug?). Anyway, this is the full output: PYTHONWARNINGS=all PSUTIL_TESTING=1 PSUTIL_DEBUG=1 pypy3 -c "import psutil.tests.runner as r; r.run(last_failed=True)" psutil.tests.test_unicode.TestFSAPIsWithInvalidPath.test_proc_cmdline ... /home/giampaolo/svn/psutil/@psutil-test-30050f��: error while loading shared libraries: libpypy3-c.so: cannot open shared object file: No such file or directory FAIL psutil.tests.test_unicode.TestFSAPIsWithInvalidPath.test_proc_exe ... /home/giampaolo/svn/psutil/@psutil-test-30050f��: error while loading shared libraries: libpypy3-c.so: cannot open shared object file: No such file or directory FAIL psutil.tests.test_unicode.TestFSAPIsWithInvalidPath.test_proc_name ... /home/giampaolo/svn/psutil/@psutil-test-30050f��: error while loading shared libraries: libpypy3-c.so: cannot open shared object file: No such file or directory FAIL psutil.tests.test_process.TestProcess.test_long_cmdline ... /home/giampaolo/svn/psutil/@psutil-test-30050: error while loading shared libraries: libpypy3-c.so: cannot open shared object file: No such file or directory FAIL psutil.tests.test_process.TestProcess.test_long_name ... /home/giampaolo/svn/psutil/@psutil-test-3005001234567890123456789: error while loading shared libraries: libpypy3-c.so: cannot open shared object file: No such file or directory FAIL psutil.tests.test_process.TestProcess.test_prog_w_funky_name ... /home/giampaolo/svn/psutil/@psutil-test-30050foo bar ): error while loading shared libraries: libpypy3-c.so: cannot open shared object file: No such file or directory FAIL ====================================================================== FAIL: psutil.tests.test_unicode.TestFSAPIsWithInvalidPath.test_proc_cmdline ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/tests/test_unicode.py", line 184, in test_proc_cmdline self.assertEqual(cmdline, [self.funky_name]) AssertionError: Lists differ: [] != ['/home/giampaolo/svn/psutil/@psutil-test-30050f\udcc0\udc80'] Second list contains 1 additional elements. First extra element 0: '/home/giampaolo/svn/psutil/@psutil-test-30050f\udcc0\udc80' - [] + ['/home/giampaolo/svn/psutil/@psutil-test-30050f\udcc0\udc80'] ====================================================================== FAIL: psutil.tests.test_unicode.TestFSAPIsWithInvalidPath.test_proc_exe ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/tests/test_unicode.py", line 168, in test_proc_exe os.path.normcase(self.funky_name)) AssertionError: '' != '/home/giampaolo/svn/psutil/@psutil-test-30050f\udcc0\udc80' + /home/giampaolo/svn/psutil/@psutil-test-30050f\udcc0\udc80 ====================================================================== FAIL: psutil.tests.test_unicode.TestFSAPIsWithInvalidPath.test_proc_name ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/tests/test_unicode.py", line 175, in test_proc_name self.assertEqual(name, os.path.basename(self.funky_name)) AssertionError: '@psutil-test-30' != '@psutil-test-30050f\udcc0\udc80' - @psutil-test-30 + @psutil-test-30050f\udcc0\udc80 ? ++++++ ====================================================================== FAIL: psutil.tests.test_process.TestProcess.test_long_cmdline ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/tests/test_process.py", line 736, in test_long_cmdline self.assertEqual(p.cmdline(), cmdline) AssertionError: Lists differ: [] != ['/home/giampaolo/svn/psutil/@psutil-test-[282 chars]789'] Second list contains 21 additional elements. First extra element 0: '/home/giampaolo/svn/psutil/@psutil-test-30050' - [] + ['/home/giampaolo/svn/psutil/@psutil-test-30050', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789', + '0123456789'] ====================================================================== FAIL: psutil.tests.test_process.TestProcess.test_long_name ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/tests/test_process.py", line 750, in test_long_name self.assertEqual(p.name(), os.path.basename(long_name)) AssertionError: '@psutil-test-30' != '@psutil-test-3005001234567890123456789' - @psutil-test-30 + @psutil-test-3005001234567890123456789 ====================================================================== FAIL: psutil.tests.test_process.TestProcess.test_prog_w_funky_name ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/tests/test_process.py", line 780, in test_prog_w_funky_name self.assertEqual(p.cmdline(), cmdline) AssertionError: Lists differ: [] != ['/home/giampaolo/svn/psutil/@psutil-test-[102 chars], ''] Second list contains 7 additional elements. First extra element 0: '/home/giampaolo/svn/psutil/@psutil-test-30050foo bar )' - [] + ['/home/giampaolo/svn/psutil/@psutil-test-30050foo bar )', + '-c', + 'import time; [time.sleep(0.01) for x in range(3000)];arg1', + 'arg2', + '', + 'arg3', + ''] ---------------------------------------------------------------------- --- psutil/_psutil_common.h | 8 ++++---- psutil/tests/test_process.py | 3 +++ psutil/tests/test_unicode.py | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index e91bf2dd9f..2fccab8160 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -29,11 +29,11 @@ static const int PSUTIL_CONN_NONE = 128; // --- _Py_PARSE_PID -// SIZEOF_INT|LONG is missing on Linux + PyPy (only?) -// SIZEOF_PID_T is missing on Windows + Python2 -// In we can't determine we assume PID is an (int). +// SIZEOF_INT|LONG is missing on Linux + PyPy (only?). +// SIZEOF_PID_T is missing on Windows + Python2. +// In we can't determine pid_t size we assume it's an (int). // On major UNIX platforms I've seen pid_t is treated as int. -// On Windows _getpid() returns an int. We can't be 100% certain though, +// _getpid() on Windows returns an int. We can't be 100% sure though, // (in that case we'd probably get compiler warnings). #if !defined(SIZEOF_INT) #define SIZEOF_INT 4 diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 5728f183de..0277a56a2b 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -727,6 +727,7 @@ def test_cmdline(self): else: raise + @unittest.skipIf(PYPY, "broken on PYPY") def test_long_cmdline(self): create_exe(TESTFN) self.addCleanup(safe_rmpath, TESTFN) @@ -741,6 +742,7 @@ def test_name(self): 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): long_name = TESTFN + ("0123456789" * 2) create_exe(long_name) @@ -752,6 +754,7 @@ def test_long_name(self): # 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: diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 91395ebfc8..eecd7dc4a3 100644 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -298,6 +298,7 @@ def expect_exact_path_match(cls): @unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS") @unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO +@unittest.skipIf(PYPY, "unreliable on PYPY") @unittest.skipIf(not subprocess_supports_unicode(INVALID_NAME), "subprocess can't deal with invalid unicode") class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, unittest.TestCase): From b6237f8190b2344d8381b86d43882fba45585876 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Sun, 9 Feb 2020 16:18:03 -0500 Subject: [PATCH 0480/1714] Enable PyPy3 on CI (#1682) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ced00a0918..fad35121b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ matrix: - python: 3.8 # pypy # - python: pypy - # - python: pypy3 + - python: pypy3 install: - ./.ci/travis/install.sh script: From 76104dbc77623cca46c18c5ef1534399d10f563d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 10 Feb 2020 01:07:51 +0100 Subject: [PATCH 0481/1714] skip memleak tests on PyPy: they are unreliable probably because of the JIT --- .github/ISSUE_TEMPLATE/bug.md | 21 ++++++--------------- README.rst | 2 +- docs/DEVGUIDE.rst | 4 ++-- psutil/tests/__init__.py | 1 - psutil/tests/test_memory_leaks.py | 11 +++++++---- 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index ba4a026c78..67a9601b2b 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -6,23 +6,14 @@ labels: 'bug' --- **Platform** -* { OS version } (also add appropriate OS issue label (linux, windows, ...)) -* { psutil version } (use "pip show psutil") +* { OS version } +* { psutil version: python3 -c "import psutil; print(psutil.__version__)" } +* { python version } -**Bug description** -{ a clear and concise description of what the bug is } -``` -traceback message (if any) -``` +**Bug description** +... -```python -code to reproduce the problem (if any) -``` **Test results** -``` -output of `python -c psutil.tests` (failures only, not full result) -``` -{ you may want to do this in order to discover other issues affecting your platform } -{ if failures look unrelated with the issue at hand open another ticket } +{ output of `python -c psutil.tests` (failures only, not full result) } diff --git a/README.rst b/README.rst index 57c30d6c96..8fadf4a3bd 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade :alt: Code quality -.. |travis| image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20OSX +.. |travis| image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux,%20OSX,%20PyPy :target: https://travis-ci.org/giampaolo/psutil :alt: Linux tests (Travis) diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 5ef1c4e42f..598c8b6118 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -141,9 +141,9 @@ Both services run psutil test suite against all supported python version (2.6 - 3.6). Two icons in the home page (README) always show the build status: -.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux%20/%20macOS +.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux,%20OSX,%20PyPy :target: https://travis-ci.org/giampaolo/psutil - :alt: Linux and macOS tests (Travis) + :alt: Linux, macOS and PyPy3 tests (Travis) .. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5251983f64..8a373386d2 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -113,7 +113,6 @@ 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 a Continuous Integration service TRAVIS = bool(os.environ.get('TRAVIS')) APPVEYOR = bool(os.environ.get('APPVEYOR')) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 132a0b07b4..31a632a478 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -11,6 +11,8 @@ 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 @@ -39,7 +41,6 @@ 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_GETLOADAVG from psutil.tests import HAS_IONICE from psutil.tests import HAS_MEMORY_MAPS from psutil.tests import HAS_NET_IO_COUNTERS @@ -49,6 +50,7 @@ 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 PYPY from psutil.tests import reap_children from psutil.tests import safe_rmpath from psutil.tests import skip_on_access_denied @@ -57,14 +59,14 @@ from psutil.tests import unittest +# configurable opts LOOPS = 1000 MEMORY_TOLERANCE = 4096 RETRY_FOR = 3 +SKIP_PYTHON_IMPL = True -SKIP_PYTHON_IMPL = True if TRAVIS else False cext = psutil._psplatform.cext thisproc = psutil.Process() -SKIP_PYTHON_IMPL = True if TRAVIS else False # =================================================================== @@ -77,6 +79,7 @@ def skip_if_linux(): "worthless on LINUX (pure python)") +@unittest.skipIf(PYPY, "unreliable on PYPY") class TestMemLeak(unittest.TestCase): """Base framework class which calls a function many times and produces a failure if process memory usage keeps increasing @@ -477,7 +480,7 @@ def test_cpu_stats(self): def test_cpu_freq(self): self.execute(psutil.cpu_freq) - @unittest.skipIf(not HAS_GETLOADAVG, "not supported") + @unittest.skipIf(not WINDOWS, "WINDOWS only") def test_getloadavg(self): self.execute(psutil.getloadavg) From 00a339886a63887e1abb225d62827ff2d961a75f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 18:07:55 +0100 Subject: [PATCH 0482/1714] fix #1681 / linux / disk_partitions: show swap --- HISTORY.rst | 2 ++ docs/index.rst | 2 ++ psutil/_pslinux.py | 27 +++++++++++++++++++++++++++ psutil/tests/test_linux.py | 16 ++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 2a9c046bfd..e5d42afa1d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX - 1677_: [Windows] process exe() will succeed for all process PIDs (instead of raising AccessDenied). - 1679_: [Windows] net_connections() and Process.connections() are 10% faster. +- 1681_: [Linux] disk_partitions() now also shows swap partitions. + **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 256a658038..27560da634 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -405,6 +405,8 @@ Disks [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] + .. versionchanged:: 5.7.0 swap partitions are returned on Linux. + .. function:: disk_usage(path) Return disk usage statistics about the partition which contains the given diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ba65aea93b..6094158979 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1177,6 +1177,33 @@ def disk_partitions(all=False): continue ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) + + # swap + if all: + try: + f = open_text("%s/swaps" % procfs_path) + except FileNotFoundError: + pass + else: + with f: + lines = f.readlines() + lines.pop(0) # header + for line in lines: + fields = line.split('\t') + device = fields[0].split()[0] + mountp = None + fstype = 'swap' + # The priority column is useful when multiple swap + # files are in use. The lower the priority, the + # more likely the swap file is to be used. + prio = fields[-1].strip() + if re.match(r'(-)?\d+', prio): + opts = "priority=" + prio + else: + opts = '' + ntuple = _common.sdiskpart(device, mountp, fstype, opts) + retlist.append(ntuple) + return retlist diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 97946a0bbd..4736bd6b47 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1067,6 +1067,22 @@ def test_emulate_realpath_fail(self): finally: psutil.PROCFS_PATH = "/proc" + @unittest.skipIf(not os.path.exists('/proc/swaps'), + "/proc/swaps not available") + def test_swap(self): + types = [x.fstype for x in psutil.disk_partitions(all=False)] + self.assertNotIn('swap', types) + types = [x.fstype for x in psutil.disk_partitions(all=True)] + self.assertIn('swap', types) + for part in psutil.disk_partitions(all=True): + if part.fstype == 'swap': + assert os.path.exists(part.device), part + self.assertIsNone(part.mountpoint) + if part.opts: + assert part.opts.startswith('priority=') + prio = part.opts.split('=')[1] + int(prio) + @unittest.skipIf(not LINUX, "LINUX only") class TestSystemDiskIoCounters(unittest.TestCase): From 1ec2f2ec0c7e751f3c68eca477c197690b4c2667 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 18:10:20 +0100 Subject: [PATCH 0483/1714] small refact --- docs/index.rst | 2 +- psutil/_pslinux.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 27560da634..97dce4440e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -405,7 +405,7 @@ Disks [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] - .. versionchanged:: 5.7.0 swap partitions are returned on Linux. + .. versionchanged:: 5.7.0 swap partitions are shown on Linux. .. function:: disk_usage(path) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 6094158979..ea1a8a08fd 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1186,9 +1186,8 @@ def disk_partitions(all=False): pass else: with f: - lines = f.readlines() - lines.pop(0) # header - for line in lines: + f.readline() # header + for line in f.readlines(): fields = line.split('\t') device = fields[0].split()[0] mountp = None From d8cb832f8cc7ef2695472ec0f752c59c72916274 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 18:16:02 +0100 Subject: [PATCH 0484/1714] fix #1627: [Linux] Process.memory_maps() can raise KeyError --- HISTORY.rst | 1 + psutil/_pslinux.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index e5d42afa1d..68309241a5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,6 +23,7 @@ XXXX-XX-XX **Bug fixes** - 1538_: [NetBSD] process cwd() may return ENOENT instead of NoSuchProcess. +- 1627_: [Linux] Process.memory_maps() can raise KeyError. - 1642_: [SunOS] querying basic info for PID 0 results in FileNotFoundError. - 1646_: [FreeBSD] many Process methods may cause a segfault on FreeBSD 12.0 due to a backward incompatible change in a C type introduced in 12.0. diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ea1a8a08fd..eb03e8a3cb 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1868,7 +1868,7 @@ def get_blocks(lines, current_block): path = path[:-10] ls.append(( decode(addr), decode(perms), path, - data[b'Rss:'], + data.get(b'Rss:', 0), data.get(b'Size:', 0), data.get(b'Pss:', 0), data.get(b'Shared_Clean:', 0), From f32512ab3abe40619f891dc96dd407a2bc59fd3c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 18:20:11 +0100 Subject: [PATCH 0485/1714] fix #1576: remove code duplicate for create_time / boot_time --- psutil/_pswindows.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 83793c5ae6..c9517b91aa 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -916,9 +916,6 @@ 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() try: return cext.proc_create_time(self.pid) except OSError as err: From acfacb47e57df79f0177e5ec3ae79c37d7dea8e3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 18:29:13 +0100 Subject: [PATCH 0486/1714] try to fix travis test --- psutil/tests/test_linux.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4736bd6b47..5a48a4451c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1070,6 +1070,9 @@ def test_emulate_realpath_fail(self): @unittest.skipIf(not os.path.exists('/proc/swaps'), "/proc/swaps not available") def test_swap(self): + with open('/proc/swaps') as f: + if not f.readline() or not f.readlines(): + raise self.skipTest("/proc/swaps is empty") types = [x.fstype for x in psutil.disk_partitions(all=False)] self.assertNotIn('swap', types) types = [x.fstype for x in psutil.disk_partitions(all=True)] From c1da77d8a668beddd72bf5851f941841d57181d2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 09:32:04 -0800 Subject: [PATCH 0487/1714] merge from master --- psutil/tests/runner.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 1a28aa4352..a10c64133e 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -177,6 +177,11 @@ def save_failed_tests(result): with open(FAILED_TESTS_FNAME, 'wt') as f: for t in result.errors + result.failures: tname = str(t[0]) + print(tname) + try: + unittest.defaultTestLoader.loadTestsFromName(tname) + except Exception: + import pdb; pdb.set_trace() f.write(tname + '\n') From 135cce8dfdd678e06aec3d0c06c8a25a1b8cd2d4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 10:19:56 -0800 Subject: [PATCH 0488/1714] windows: convert pid_t to DWORD --- psutil/_psutil_windows.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index e63cf97f34..102b9e3d83 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -281,7 +281,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cpu_times(PyObject *self, PyObject *args) { - pid_t pid; + DWORD pid; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; @@ -332,7 +332,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_create_time(PyObject *self, PyObject *args) { - pid_t pid; + DWORD pid; long long unix_time; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; From 9e2ca978b211993066b0dc41da9aa63429655406 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Feb 2020 23:04:38 +0100 Subject: [PATCH 0489/1714] Add support for PyPy on Windows (#1686) --- HISTORY.rst | 2 +- psutil/_common.py | 8 +++++- psutil/_psutil_common.c | 48 +++++++++++++++++++++++++++++++----- psutil/_psutil_common.h | 16 +++++++++--- psutil/_psutil_windows.c | 22 +++++++++++------ psutil/_pswindows.py | 19 ++++++++++---- psutil/arch/windows/cpu.c | 12 ++++++--- psutil/arch/windows/wmi.c | 2 +- psutil/tests/__init__.py | 6 +++-- psutil/tests/runner.py | 1 + psutil/tests/test_linux.py | 3 +++ psutil/tests/test_system.py | 7 +++++- psutil/tests/test_unicode.py | 5 ++++ psutil/tests/test_windows.py | 25 ++++++++++++------- scripts/internal/winmake.py | 12 ++++++--- 15 files changed, 144 insertions(+), 44 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 68309241a5..233236cc42 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,7 +18,7 @@ XXXX-XX-XX raising AccessDenied). - 1679_: [Windows] net_connections() and Process.connections() are 10% faster. - 1681_: [Linux] disk_partitions() now also shows swap partitions. - +- 1686_: [Windows] added support for PyPy on Windows. **Bug fixes** diff --git a/psutil/_common.py b/psutil/_common.py index 453c771d5e..9306cd1556 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -790,8 +790,14 @@ def hilite(s, ok=True, bold=False): if bool(os.getenv('PSUTIL_DEBUG', 0)): + import inspect + def debug(msg): - print("psutil-debug> " + msg, file=sys.stderr) + """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" + fname, lineno, func_name, lines, index = inspect.getframeinfo( + inspect.currentframe().f_back) + print("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), + file=sys.stderr) else: def debug(msg): pass diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 990d59a6a1..9bfaf92cdd 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -20,7 +20,47 @@ int PSUTIL_TESTING = 0; // ==================================================================== -// --- Python functions and backward compatibility +// --- Backward compatibility with missing Python.h APIs +// ==================================================================== + +// PyPy on Windows +#if defined(PYPY_VERSION) && !defined(PyErr_SetFromWindowsErrWithFilename) +PyObject * +PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { + PyObject *py_exc = NULL; + PyObject *py_winerr = NULL; + + if (winerr == 0) + winerr = GetLastError(); + if (filename == NULL) { + py_exc = PyObject_CallFunction(PyExc_OSError, "(is)", winerr, + strerror(winerr)); + } + else { + py_exc = PyObject_CallFunction(PyExc_OSError, "(iss)", winerr, + strerror(winerr), filename); + } + if (py_exc == NULL) + return NULL; + + py_winerr = Py_BuildValue("i", winerr); + if (py_winerr == NULL) + goto error; + if (PyObject_SetAttrString(py_exc, "winerror", py_winerr) != 0) + goto error; + PyErr_SetObject(PyExc_OSError, py_exc); + Py_XDECREF(py_exc); + return NULL; + +error: + Py_XDECREF(py_exc); + Py_XDECREF(py_winerr); + return NULL; +} +#endif + +// ==================================================================== +// --- Custom exceptions // ==================================================================== /* @@ -44,11 +84,6 @@ PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { return NULL; } - -// ==================================================================== -// --- Custom exceptions -// ==================================================================== - /* * Set OSError(errno=ESRCH, strerror="No such process (originated from") * Python exception. @@ -133,6 +168,7 @@ psutil_setup(void) { // --- Windows // ==================================================================== + #ifdef PSUTIL_WINDOWS #include diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 2fccab8160..2886d61191 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -16,7 +16,7 @@ extern int PSUTIL_DEBUG; static const int PSUTIL_CONN_NONE = 128; // ==================================================================== -// --- Python functions and backward compatibility +// --- Backward compatibility with missing Python.h APIs // ==================================================================== #if PY_MAJOR_VERSION < 3 @@ -60,9 +60,14 @@ static const int PSUTIL_CONN_NONE = 128; #endif #endif -#if PY_MAJOR_VERSION < 3 +// Python 2 or PyPy on Windows +#ifndef PyLong_FromPid #if ((SIZEOF_PID_T == SIZEOF_INT) || (SIZEOF_PID_T == SIZEOF_LONG)) - #define PyLong_FromPid PyInt_FromLong + #if PY_MAJOR_VERSION >= 3 + #define PyLong_FromPid PyLong_FromLong + #else + #define PyLong_FromPid PyInt_FromLong + #endif #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG #define PyLong_FromPid PyLong_FromLongLong #else @@ -122,4 +127,9 @@ int psutil_setup(void); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); + + #if defined(PYPY_VERSION) && !defined(PyErr_SetFromWindowsErrWithFilename) + PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, + const char *filename); + #endif #endif diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index e63cf97f34..f940570707 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -228,8 +228,10 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // return None instead. Py_RETURN_NONE; } - else - return PyErr_SetFromWindowsErr(0); + else { + PyErr_SetFromWindowsErr(0); + return NULL; + } } // wait until the process has terminated @@ -281,7 +283,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cpu_times(PyObject *self, PyObject *args) { - pid_t pid; + DWORD pid; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; @@ -332,7 +334,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_create_time(PyObject *self, PyObject *args) { - pid_t pid; + DWORD pid; long long unix_time; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; @@ -698,8 +700,10 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { MEMORYSTATUSEX memInfo; memInfo.dwLength = sizeof(MEMORYSTATUSEX); - if (! GlobalMemoryStatusEx(&memInfo)) - return PyErr_SetFromWindowsErr(0); + if (! GlobalMemoryStatusEx(&memInfo)) { + PyErr_SetFromWindowsErr(0); + return NULL; + } return Py_BuildValue("(LLLLLL)", memInfo.ullTotalPhys, // total memInfo.ullAvailPhys, // avail @@ -1571,8 +1575,10 @@ static PyObject * psutil_sensors_battery(PyObject *self, PyObject *args) { SYSTEM_POWER_STATUS sps; - if (GetSystemPowerStatus(&sps) == 0) - return PyErr_SetFromWindowsErr(0); + if (GetSystemPowerStatus(&sps) == 0) { + PyErr_SetFromWindowsErr(0); + return NULL; + } return Py_BuildValue( "iiiI", sps.ACLineStatus, // whether AC is connected: 0=no, 1=yes, 255=unknown diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 83793c5ae6..d8abf2e67d 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -17,6 +17,7 @@ from ._common import AccessDenied from ._common import conn_tmap from ._common import conn_to_ntuple +from ._common import debug from ._common import ENCODING from ._common import ENCODING_ERRS from ._common import isfile_strict @@ -80,7 +81,7 @@ CONN_DELETE_TCB = "DELETE_TCB" ERROR_PARTIAL_COPY = 299 - +PYPY = '__pypy__' in sys.builtin_module_names if enum is None: AF_LINK = -1 @@ -752,7 +753,18 @@ def name(self): @wrap_exceptions @memoize_when_activated def exe(self): - exe = 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 forced 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('\\'): @@ -916,9 +928,6 @@ 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() try: return cext.proc_create_time(self.pid) except OSError as err: diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 9a22e1499c..18f32e5983 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -50,8 +50,10 @@ psutil_cpu_times(PyObject *self, PyObject *args) { double idle, kernel, user, system; FILETIME idle_time, kernel_time, user_time; - if (!GetSystemTimes(&idle_time, &kernel_time, &user_time)) - return PyErr_SetFromWindowsErr(0); + if (!GetSystemTimes(&idle_time, &kernel_time, &user_time)) { + PyErr_SetFromWindowsErr(0); + return NULL; + } idle = (double)((HI_T * idle_time.dwHighDateTime) + \ (LO_T * idle_time.dwLowDateTime)); @@ -384,8 +386,10 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { // Allocate size. size = ncpus * sizeof(PROCESSOR_POWER_INFORMATION); pBuffer = (BYTE*)LocalAlloc(LPTR, size); - if (! pBuffer) - return PyErr_SetFromWindowsErr(0); + if (! pBuffer) { + PyErr_SetFromWindowsErr(0); + return NULL; + } // Syscall. ret = CallNtPowerInformation( diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index b790c08e98..42a70df766 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -96,7 +96,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { Py_RETURN_NONE; error: - PyErr_SetExcFromWindowsErr(PyExc_OSError, 0); + PyErr_SetFromWindowsErr(0); return NULL; } diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 8a373386d2..3e4dc88066 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -118,7 +118,6 @@ APPVEYOR = bool(os.environ.get('APPVEYOR')) CIRRUS = bool(os.environ.get('CIRRUS')) CI_TESTING = TRAVIS or APPVEYOR or CIRRUS -PYPY = '__pypy__' in sys.builtin_module_names # --- configurable defaults @@ -1111,9 +1110,12 @@ def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): 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 + 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 diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 1a28aa4352..f8601badb9 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -177,6 +177,7 @@ def save_failed_tests(result): with open(FAILED_TESTS_FNAME, 'wt') as f: for t in result.errors + result.failures: tname = str(t[0]) + unittest.defaultTestLoader.loadTestsFromName(tname) f.write(tname + '\n') diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4736bd6b47..5a48a4451c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1070,6 +1070,9 @@ def test_emulate_realpath_fail(self): @unittest.skipIf(not os.path.exists('/proc/swaps'), "/proc/swaps not available") def test_swap(self): + with open('/proc/swaps') as f: + if not f.readline() or not f.readlines(): + raise self.skipTest("/proc/swaps is empty") types = [x.fstype for x in psutil.disk_partitions(all=False)] self.assertNotIn('swap', types) types = [x.fstype for x in psutil.disk_partitions(all=True)] diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index c6f8a17a37..223f91a265 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -31,9 +31,9 @@ from psutil import WINDOWS from psutil._compat import FileNotFoundError from psutil._compat import long -from psutil.tests import CI_TESTING from psutil.tests import ASCII_FS from psutil.tests import check_net_address +from psutil.tests import CI_TESTING from psutil.tests import DEVNULL from psutil.tests import enum from psutil.tests import get_test_subprocess @@ -45,6 +45,7 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import mock +from psutil.tests import PYPY from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import safe_rmpath @@ -121,6 +122,8 @@ def test_process_iter_new_only(self): else: self.fail("subprocess not found") + @unittest.skipIf(PYPY and WINDOWS, + "get_test_subprocess() unreliable on PYPY + WINDOWS") def test_wait_procs(self): def callback(p): pids.append(p.pid) @@ -176,6 +179,8 @@ def test(procs, callback): for p in gone: self.assertTrue(hasattr(p, 'returncode')) + @unittest.skipIf(PYPY and WINDOWS, + "get_test_subprocess() unreliable on PYPY + WINDOWS") def test_wait_procs_no_timeout(self): sproc1 = get_test_subprocess() sproc2 = get_test_subprocess() diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index eecd7dc4a3..9e459fbebe 100644 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -61,6 +61,7 @@ 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 @@ -194,6 +195,7 @@ def test_proc_cwd(self): 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()) @@ -261,6 +263,8 @@ def test_disk_usage(self): @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") @unittest.skipIf(not PY3, "ctypes does not support unicode on PY2") + @unittest.skipIf(PYPY and WINDOWS, + "copyload_shared_lib() unsupported on PYPY + WINDOWS") 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. @@ -323,6 +327,7 @@ def tearDown(self): reap_children() @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 diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 6d4e8599e0..8a93743f00 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -27,12 +27,14 @@ from psutil.tests import HAS_BATTERY from psutil.tests import mock from psutil.tests import PY3 +from psutil.tests import PYPY from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import unittest -if WINDOWS: + +if WINDOWS and not PYPY: with warnings.catch_warnings(): warnings.simplefilter("ignore") import win32api # requires "pip install pypiwin32" @@ -61,13 +63,18 @@ def wrapper(self, *args, **kwargs): return wrapper +@unittest.skipIf(PYPY, "pywin32 not available on PYPY") # skip whole module +class TestCase(unittest.TestCase): + pass + + # =================================================================== # System APIs # =================================================================== @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestCpuAPIs(unittest.TestCase): +class TestCpuAPIs(TestCase): @unittest.skipIf('NUMBER_OF_PROCESSORS' not in os.environ, 'NUMBER_OF_PROCESSORS env var is not available') @@ -106,7 +113,7 @@ def test_cpu_freq(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestSystemAPIs(unittest.TestCase): +class TestSystemAPIs(TestCase): def test_nic_names(self): out = sh('ipconfig /all') @@ -230,7 +237,7 @@ def test_boot_time_fluctuation(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestSensorsBattery(unittest.TestCase): +class TestSensorsBattery(TestCase): def test_has_battery(self): if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: @@ -291,7 +298,7 @@ def test_emulate_secs_left_unknown(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcess(unittest.TestCase): +class TestProcess(TestCase): @classmethod def setUpClass(cls): @@ -519,7 +526,7 @@ def test_exe(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcessWMI(unittest.TestCase): +class TestProcessWMI(TestCase): """Compare Process API results with WMI.""" @classmethod @@ -585,7 +592,7 @@ def test_create_time(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestDualProcessImplementation(unittest.TestCase): +class TestDualProcessImplementation(TestCase): """ Certain APIs on Windows have 2 internal implementations, one based on documented Windows APIs, another one based @@ -668,7 +675,7 @@ def test_cmdline(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class RemoteProcessTestCase(unittest.TestCase): +class RemoteProcessTestCase(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 @@ -764,7 +771,7 @@ def test_environ_64(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestServices(unittest.TestCase): +class TestServices(TestCase): def test_win_service_iter(self): valid_statuses = set([ diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index ac08c03f56..fe0a73dceb 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -36,6 +36,7 @@ PY3 = sys.version_info[0] == 3 HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) +PYPY = '__pypy__' in sys.builtin_module_names DEPS = [ "coverage", "flake8", @@ -43,7 +44,6 @@ "pdbpp", "pip", "pyperf", - "pypiwin32==219" if sys.version_info[:2] <= (3, 4) else "pypiwin32", "pyreadline", "setuptools", "wheel", @@ -56,6 +56,12 @@ DEPS.append('mock') if sys.version_info[:2] <= (3, 2): DEPS.append('ipaddress') +if PYPY: + pass +elif sys.version_info[:2] <= (3, 4): + DEPS.append("pypiwin32==219") +else: + DEPS.append("pypiwin32") _cmds = {} if PY3: @@ -268,8 +274,8 @@ def upload_wheels(): def install_pip(): """Install pip""" try: - import pip # NOQA - except ImportError: + sh('%s -c "import pip"' % PYTHON) + except SystemExit: if PY3: from urllib.request import urlopen else: From 3e2ee4f26f171e05b9b38a270c6baab63edb3f7e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Feb 2020 02:44:33 +0100 Subject: [PATCH 0490/1714] refactor doc --- Makefile | 2 +- README.rst | 2 +- docs/DEVGUIDE.rst | 29 ++----- docs/Makefile | 2 +- docs/_static/css/custom.css | 22 ++++- docs/index.rst | 160 +++++------------------------------ psutil/tests/test_unicode.py | 31 +++++-- 7 files changed, 75 insertions(+), 173 deletions(-) diff --git a/Makefile b/Makefile index 890c6e413f..fd50aeca2c 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ DEPS = \ pyperf \ requests \ setuptools \ - sphinx \ + sphinx==2.2.2 \ twine \ unittest2 \ virtualenv \ diff --git a/README.rst b/README.rst index 8fadf4a3bd..c28a55f9cf 100644 --- a/README.rst +++ b/README.rst @@ -100,7 +100,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy `__ is also known to work. +...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy3 `__ is also known to work. psutil for enterprise ===================== diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 598c8b6118..e07d977e8c 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -1,24 +1,13 @@ -Setup and running tests -======================= - -If you plan on hacking on psutil this is what you're supposed to do first: +Build, setup and running tests +=============================== -- clone the GIT repository: - -.. code-block:: bash - - $ git clone git@github.com:giampaolo/psutil.git - -- install test deps and GIT hooks: +Make sure to `install `__ +a C compiler first, then: .. code-block:: bash + git clone git@github.com:giampaolo/psutil.git make setup-dev-env - -- run tests: - -.. code-block:: bash - make test - bear in mind that ``make``(see `Makefile`_) is the designated tool to run @@ -60,13 +49,7 @@ On Windows: .. code-block:: bat - set PYTHON=C:\python35\python.exe && make test - -...or: - -.. code-block:: bat - - make -p 35 test + make -p C:\python35\python.exe test If you want to modify psutil and run a script on the fly which uses it do (on UNIX): diff --git a/docs/Makefile b/docs/Makefile index 0c4bdf48a2..c7f4723a71 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -PYTHON = python +PYTHON = python3 SPHINXOPTS = SPHINXBUILD = $(PYTHON) -m sphinx PAPER = diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index b76f442af8..c5c201e4f1 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -15,10 +15,30 @@ border-right:10px !important; } -.local-toc li ul li{ +.local-toc li ul li { padding-left: 20px !important; } +.rst-content ul p { + margin-bottom: 0px !important; +} + +.document td { + padding-bottom: 0px !important; +} + +.document th { + padding-top: 0px !important; + padding-bottom: 0px !important; +} + +.document th p { + margin-bottom: 0px !important; +} + +.document th p { +} + .function .descclassname { font-weight: normal !important; } diff --git a/docs/index.rst b/docs/index.rst index 97dce4440e..e60058cd56 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2345,46 +2345,6 @@ Hardware constants >>> if psutil.version_info >= (4, 5): ... pass ----- - -Unicode -======= - -Starting from version 5.3.0 psutil adds unicode support, see `issue #1040`_. -The notes below apply to *any* API returning a string such as -:meth:`Process.exe` or :meth:`Process.cwd`, including non-filesystem related -methods such as :meth:`Process.username` or :meth:`WindowsService.description`: - -* 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: - -.. code-block:: python - - >>> unicode(p.exe(), sys.getdefaultencoding(), errors="replace") - -Example which filters processes with a funky name working with both Python 2 -and 3:: - - # -*- coding: utf-8 -*- - import psutil, sys - - PY3 = sys.version_info[0] == 2 - LOOKFOR = u"ƒőő" - for proc in psutil.process_iter(attrs=['name']): - name = proc.info['name'] - if not PY3: - name = unicode(name, sys.getdefaultencoding(), errors="replace") - if LOOKFOR == name: - print("process %s found" % p) - Recipes ======= @@ -2450,61 +2410,14 @@ Kill process tree callback=on_terminate) return (gone, alive) -Terminate my children ---------------------- - -This may be useful in unit tests whenever sub-processes are started. -This will help ensure that no extra children (zombies) stick around to hog -resources. - -:: - - import psutil - - def reap_children(timeout=3): - "Tries hard to terminate and ultimately kill all the children of this process." - def on_terminate(proc): - print("process {} terminated with exit code {}".format(proc, proc.returncode)) - - procs = psutil.Process().children() - # send SIGTERM - for p in procs: - try: - p.terminate() - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(procs, timeout=timeout, callback=on_terminate) - if alive: - # send SIGKILL - for p in alive: - print("process {} survived SIGTERM; trying SIGKILL".format(p)) - try: - p.kill() - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(alive, timeout=timeout, callback=on_terminate) - if alive: - # give up - for p in alive: - print("process {} survived SIGKILL; giving up".format(p)) - Filtering and sorting processes ------------------------------- -This is a collection of one-liners showing how to use :func:`process_iter()` in -order to filter for processes and sort them. - -Setup:: +A collection of code samples showing how to use :func:`process_iter()` to filter processes and sort them. Setup:: >>> import psutil >>> from pprint import pprint as pp -Processes having "python" in their name:: - - >>> pp([p.info for p in psutil.process_iter(attrs=['pid', 'name']) if 'python' in p.info['name']]) - [{'name': 'python3', 'pid': 21947}, - {'name': 'python', 'pid': 23835}] - Processes owned by user:: >>> import getpass @@ -2522,11 +2435,9 @@ Processes actively running:: Processes using log files:: - >>> import os - >>> import psutil >>> for p in psutil.process_iter(attrs=['name', 'open_files']): ... for file in p.info['open_files'] or []: - ... if os.path.splitext(file.path)[1] == '.log': + ... if file.path.endswith('.log'): ... print("%-5s %-10s %s" % (p.pid, p.info['name'][:10], file.path)) ... 1510 upstart /home/giampaolo/.cache/upstart/unity-settings-daemon.log @@ -2540,13 +2451,6 @@ Processes consuming more than 500M of memory:: (3038, 'chrome', 1120088064), (21915, 'sublime_text', 615407616)] -Top 3 most memory consuming processes:: - - >>> pp([(p.pid, p.info) for p in sorted(psutil.process_iter(attrs=['name', 'memory_percent']), key=lambda p: p.info['memory_percent'])][-3:]) - [(21915, {'memory_percent': 3.6815453247662737, 'name': 'sublime_text'}), - (3038, {'memory_percent': 6.732935429979187, 'name': 'chrome'}), - (3249, {'memory_percent': 8.994554843376399, 'name': 'chrome'})] - Top 3 processes which consumed the most CPU time:: >>> pp([(p.pid, p.info['name'], sum(p.info['cpu_times'])) for p in sorted(psutil.process_iter(attrs=['name', 'cpu_times']), key=lambda p: sum(p.info['cpu_times'][:2]))][-3:]) @@ -2554,20 +2458,6 @@ Top 3 processes which consumed the most CPU time:: (1150, 'Xorg', 11116.989999999998), (2650, 'chrome', 18451.97)] -Top 3 processes which caused the most I/O:: - - >>> pp([(p.pid, p.info['name']) for p in sorted(psutil.process_iter(attrs=['name', 'io_counters']), key=lambda p: p.info['io_counters'] and p.info['io_counters'][:2])][-3:]) - [(21915, 'sublime_text'), - (1871, 'pulseaudio'), - (1510, 'upstart')] - -Top 3 processes opening more file descriptors:: - - >>> pp([(p.pid, p.info) for p in sorted(psutil.process_iter(attrs=['name', 'num_fds']), key=lambda p: p.info['num_fds'])][-3:]) - [(21915, {'name': 'sublime_text', 'num_fds': 105}), - (2721, {'name': 'chrome', 'num_fds': 185}), - (2650, {'name': 'chrome', 'num_fds': 354})] - Bytes conversion ---------------- @@ -2600,27 +2490,6 @@ Bytes conversion 100399730688 93.5G -Supported platforms -=================== - -These are the platforms I develop and test on: - -* Linux Ubuntu 18.04 -* Windows 10 (support back to Windows Vista) -* MacOS 10.11 El Captain -* Solaris 10 -* FreeBSD 11 -* OpenBSD 6.4 -* NetBSD 8.0 -* AIX 6.1 TL8 (maintainer `Arnon Yaari `__) - -Earlier versions are supposed to work but are not tested. -For Linux, Windows and MacOS we have continuos integration. Other platforms -are tested manually from time to time. -Minimum supported Windows version is Windows Vista (Windows XP and Windows -Server 2003 are not supported). -Supported Python versions are 3.4+, 2.7 and 2.6. - FAQs ==== @@ -2638,21 +2507,30 @@ FAQs Running tests ============= -There are two ways of running tests. If psutil is already installed use:: - - $ python -m psutil.tests - -You can use this method as a quick way to make sure psutil fully works on your -platform. If you have a copy of the source code you can also use:: +:: - $ make test + $ python3 -m psutil.tests Development guide ================= -If you plan on hacking on psutil (e.g. want to add a new feature or fix a bug) +If you want to hacking on psutil (e.g. want to add a new feature or fix a bug) take a look at the `development guide`_. +Platforms support history +========================= + +* psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support +* psutil 5.7.0 (2020-02): **PyPy** on Windows +* psutil 5.4.0 (2017-11): **AIX** +* psutil 3.4.1 (2016-01): **NetBSD** +* psutil 3.3.0 (2015-11): **OpenBSD** +* psutil 1.0.0 (2013-07): **Solaris** +* psutil 0.1.1 (2009-03): **FreeBSD** +* psutil 0.1.0 (2009-01): **Linux, Windows, macOS** + +Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. + Timeline ======== diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 9e459fbebe..81a28807c3 100644 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -9,7 +9,32 @@ Notes about unicode handling in psutil ====================================== -In psutil these are the APIs returning or dealing with a string +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() @@ -46,10 +71,6 @@ * 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 From 205f213dd5f548d19873a55ee1a7dc28a77d46e1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Feb 2020 13:16:20 +0100 Subject: [PATCH 0491/1714] fix pypy on Linux --- psutil/_psutil_common.c | 82 ++++++++++++++++++------------------- psutil/_psutil_common.h | 2 +- psutil/tests/test_system.py | 4 +- 3 files changed, 42 insertions(+), 46 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 9bfaf92cdd..b8c6b5e555 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -10,55 +10,13 @@ #include "_psutil_common.h" // ==================================================================== -// --- Global vars / constants +// --- Global vars // ==================================================================== - int PSUTIL_DEBUG = 0; int PSUTIL_TESTING = 0; // PSUTIL_CONN_NONE - -// ==================================================================== -// --- Backward compatibility with missing Python.h APIs -// ==================================================================== - -// PyPy on Windows -#if defined(PYPY_VERSION) && !defined(PyErr_SetFromWindowsErrWithFilename) -PyObject * -PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { - PyObject *py_exc = NULL; - PyObject *py_winerr = NULL; - - if (winerr == 0) - winerr = GetLastError(); - if (filename == NULL) { - py_exc = PyObject_CallFunction(PyExc_OSError, "(is)", winerr, - strerror(winerr)); - } - else { - py_exc = PyObject_CallFunction(PyExc_OSError, "(iss)", winerr, - strerror(winerr), filename); - } - if (py_exc == NULL) - return NULL; - - py_winerr = Py_BuildValue("i", winerr); - if (py_winerr == NULL) - goto error; - if (PyObject_SetAttrString(py_exc, "winerror", py_winerr) != 0) - goto error; - PyErr_SetObject(PyExc_OSError, py_exc); - Py_XDECREF(py_exc); - return NULL; - -error: - Py_XDECREF(py_exc); - Py_XDECREF(py_winerr); - return NULL; -} -#endif - // ==================================================================== // --- Custom exceptions // ==================================================================== @@ -84,6 +42,7 @@ PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { return NULL; } + /* * Set OSError(errno=ESRCH, strerror="No such process (originated from") * Python exception. @@ -185,6 +144,43 @@ CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define WIN32_FROM_NTSTATUS(Status) (((ULONG)(Status)) & 0xffff) +// PyPy on Windows +#if defined(PYPY_VERSION) && !defined(PyErr_SetFromWindowsErrWithFilename) +PyObject * +PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { + PyObject *py_exc = NULL; + PyObject *py_winerr = NULL; + + if (winerr == 0) + winerr = GetLastError(); + if (filename == NULL) { + py_exc = PyObject_CallFunction(PyExc_OSError, "(is)", winerr, + strerror(winerr)); + } + else { + py_exc = PyObject_CallFunction(PyExc_OSError, "(iss)", winerr, + strerror(winerr), filename); + } + if (py_exc == NULL) + return NULL; + + py_winerr = Py_BuildValue("i", winerr); + if (py_winerr == NULL) + goto error; + if (PyObject_SetAttrString(py_exc, "winerror", py_winerr) != 0) + goto error; + PyErr_SetObject(PyExc_OSError, py_exc); + Py_XDECREF(py_exc); + return NULL; + +error: + Py_XDECREF(py_exc); + Py_XDECREF(py_winerr); + return NULL; +} +#endif + + // A wrapper around GetModuleHandle and GetProcAddress. PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 2886d61191..b072e357a2 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -32,7 +32,7 @@ static const int PSUTIL_CONN_NONE = 128; // SIZEOF_INT|LONG is missing on Linux + PyPy (only?). // SIZEOF_PID_T is missing on Windows + Python2. // In we can't determine pid_t size we assume it's an (int). -// On major UNIX platforms I've seen pid_t is treated as int. +// On all UNIX platforms I've seen pid_t is defined as an int. // _getpid() on Windows returns an int. We can't be 100% sure though, // (in that case we'd probably get compiler warnings). #if !defined(SIZEOF_INT) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 223f91a265..2d606be4d9 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -535,7 +535,7 @@ def test_disk_partitions(self): ls = psutil.disk_partitions(all=True) self.assertTrue(ls, msg=ls) for disk in psutil.disk_partitions(all=True): - if not WINDOWS: + if not WINDOWS and disk.mountpoint: try: os.stat(disk.mountpoint) except OSError as err: @@ -558,7 +558,7 @@ def find_mount_point(path): mount = find_mount_point(__file__) mounts = [x.mountpoint.lower() for x in - psutil.disk_partitions(all=True)] + psutil.disk_partitions(all=True) if x.mountpoint] self.assertIn(mount, mounts) psutil.disk_usage(mount) From b5cf1f92f01b644067b7daf6011df50fda3be3f6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Feb 2020 09:28:21 -0800 Subject: [PATCH 0492/1714] refactoring --- psutil/_psutil_common.c | 84 ++++++++++++++++++++++------------------- psutil/_psutil_common.h | 12 +++--- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index b8c6b5e555..028e48e00c 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -17,6 +17,50 @@ int PSUTIL_DEBUG = 0; int PSUTIL_TESTING = 0; // PSUTIL_CONN_NONE + +// ==================================================================== +// --- Backward compatibility with missing Python.h APIs +// ==================================================================== + +// PyPy on Windows +#if defined(PSUTIL_WINDOWS) && \ + defined(PYPY_VERSION) && \ + !defined(PyErr_SetFromWindowsErrWithFilename) +PyObject * +PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { + PyObject *py_exc = NULL; + PyObject *py_winerr = NULL; + + if (winerr == 0) + winerr = GetLastError(); + if (filename == NULL) { + py_exc = PyObject_CallFunction(PyExc_OSError, "(is)", winerr, + strerror(winerr)); + } + else { + py_exc = PyObject_CallFunction(PyExc_OSError, "(iss)", winerr, + strerror(winerr), filename); + } + if (py_exc == NULL) + return NULL; + + py_winerr = Py_BuildValue("i", winerr); + if (py_winerr == NULL) + goto error; + if (PyObject_SetAttrString(py_exc, "winerror", py_winerr) != 0) + goto error; + PyErr_SetObject(PyExc_OSError, py_exc); + Py_XDECREF(py_exc); + return NULL; + +error: + Py_XDECREF(py_exc); + Py_XDECREF(py_winerr); + return NULL; +} +#endif // PYPY on Windows + + // ==================================================================== // --- Custom exceptions // ==================================================================== @@ -29,7 +73,7 @@ PyObject * PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { char fullmsg[1024]; -#ifdef _WIN32 +#ifdef PSUTIL_WINDOWS sprintf(fullmsg, "(originated from %s)", syscall); PyErr_SetFromWindowsErrWithFilename(GetLastError(), fullmsg); #else @@ -127,7 +171,6 @@ psutil_setup(void) { // --- Windows // ==================================================================== - #ifdef PSUTIL_WINDOWS #include @@ -144,43 +187,6 @@ CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define WIN32_FROM_NTSTATUS(Status) (((ULONG)(Status)) & 0xffff) -// PyPy on Windows -#if defined(PYPY_VERSION) && !defined(PyErr_SetFromWindowsErrWithFilename) -PyObject * -PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { - PyObject *py_exc = NULL; - PyObject *py_winerr = NULL; - - if (winerr == 0) - winerr = GetLastError(); - if (filename == NULL) { - py_exc = PyObject_CallFunction(PyExc_OSError, "(is)", winerr, - strerror(winerr)); - } - else { - py_exc = PyObject_CallFunction(PyExc_OSError, "(iss)", winerr, - strerror(winerr), filename); - } - if (py_exc == NULL) - return NULL; - - py_winerr = Py_BuildValue("i", winerr); - if (py_winerr == NULL) - goto error; - if (PyObject_SetAttrString(py_exc, "winerror", py_winerr) != 0) - goto error; - PyErr_SetObject(PyExc_OSError, py_exc); - Py_XDECREF(py_exc); - return NULL; - -error: - Py_XDECREF(py_exc); - Py_XDECREF(py_winerr); - return NULL; -} -#endif - - // A wrapper around GetModuleHandle and GetProcAddress. PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index b072e357a2..bb26c922f4 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -27,6 +27,13 @@ static const int PSUTIL_CONN_NONE = 128; #define PyUnicode_DecodeFSDefaultAndSize PyString_FromStringAndSize #endif +#if defined(PSUTIL_WINDOWS) && \ + defined(PYPY_VERSION) && \ + !defined(PyErr_SetFromWindowsErrWithFilename) + PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, + const char *filename); +#endif + // --- _Py_PARSE_PID // SIZEOF_INT|LONG is missing on Linux + PyPy (only?). @@ -127,9 +134,4 @@ int psutil_setup(void); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); - - #if defined(PYPY_VERSION) && !defined(PyErr_SetFromWindowsErrWithFilename) - PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, - const char *filename); - #endif #endif From cdb20187518e73ce8de96f57e9c5aca1357d6430 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Feb 2020 20:33:42 +0100 Subject: [PATCH 0493/1714] #1672: determine pid_t size at runtime in setup.py --- HISTORY.rst | 3 ++- psutil/_psutil_common.h | 5 ++++- setup.py | 12 +++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 233236cc42..44d887e381 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -33,7 +33,8 @@ XXXX-XX-XX - 1662_: [Windows] process exe() may raise WinError 0. - 1665_: [Linux] disk_io_counters() does not take into account extra fields added to recent kernels. (patch by Mike Hommey) -- 1672_: properly handle PID C type. +- 1672_: use the right C type when dealing with PIDs (int or long). Thus far + (long) was almost always assumed, which is wrong on most platforms. - 1673_: [OpenBSD] Process connections(), num_fds() and threads() returned improper exception if process is gone. - 1674_: [SunOS] disk_partitions() may raise OSError. diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index bb26c922f4..9e1ef88795 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -49,7 +49,10 @@ static const int PSUTIL_CONN_NONE = 128; #define SIZEOF_LONG 8 #endif #if !defined(SIZEOF_PID_T) - #define SIZEOF_PID_T 4 // assume int + #if PSUTIL_SIZEOF_PID_T != 4 + #warning "SIZEOF_PID_T was guessed" + #endif + #define SIZEOF_PID_T PSUTIL_SIZEOF_PID_T // set as a macro in setup.py #endif // _Py_PARSE_PID is Python 3 only, but since it's private make sure it's diff --git a/setup.py b/setup.py index 3a3889cfcb..e7c6931619 100755 --- a/setup.py +++ b/setup.py @@ -11,11 +11,12 @@ import io import os import platform +import re import shutil +import struct import sys import tempfile import warnings -import re with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -52,6 +53,15 @@ if BSD: macros.append(("PSUTIL_BSD", 1)) +# Needed to determine _Py_PARSE_PID in case it's missing (Python 2, PyPy). +# Taken from Lib/test/test_fcntl.py. +# XXX: not bullet proof as the (long long) case is missing. +if struct.calcsize('l') == 8: + macros.append(('PSUTIL_SIZEOF_PID_T', '4')) # int +else: + macros.append(('PSUTIL_SIZEOF_PID_T', '8')) # long + + sources = ['psutil/_psutil_common.c'] if POSIX: sources.append('psutil/_psutil_posix.c') From 7e7d211de0db4b6c6ad7c721e2be3374d9c1987d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 01:53:04 +0100 Subject: [PATCH 0494/1714] fix #1688: use python3 in GIT commit hook hashbang --- scripts/internal/.git-pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/internal/.git-pre-commit b/scripts/internal/.git-pre-commit index c7d7d1ff59..621879dfbe 100755 --- a/scripts/internal/.git-pre-commit +++ b/scripts/internal/.git-pre-commit @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 From f0b87e1f7dc6f666bd48e26b7a1f84975d7b09b8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 16:10:10 +0100 Subject: [PATCH 0495/1714] small refactoring to accomodate #1691 --- psutil/_pslinux.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index eb03e8a3cb..1bd8e987ae 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -752,6 +752,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) @@ -956,15 +958,14 @@ 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) + 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, From 573886fa81db2c4b0ae9d296fa0fe1ac9055f3f8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:21:54 -0800 Subject: [PATCH 0496/1714] #1672: warning pre-processor directive don't work on win + py2; also if struct.calcsize('l') < 8 assume int --- psutil/_psutil_common.h | 11 ++++------- setup.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 9e1ef88795..9754320693 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -38,10 +38,10 @@ static const int PSUTIL_CONN_NONE = 128; // SIZEOF_INT|LONG is missing on Linux + PyPy (only?). // SIZEOF_PID_T is missing on Windows + Python2. -// In we can't determine pid_t size we assume it's an (int). -// On all UNIX platforms I've seen pid_t is defined as an int. -// _getpid() on Windows returns an int. We can't be 100% sure though, -// (in that case we'd probably get compiler warnings). +// In this case we guess it from setup.py. It's not 100% bullet proof, +// If wrong we'll probably get compiler warnings. +// FWIW on all UNIX platforms I've seen pid_t is defined as an int. +// _getpid() on Windows also returns an int. #if !defined(SIZEOF_INT) #define SIZEOF_INT 4 #endif @@ -49,9 +49,6 @@ static const int PSUTIL_CONN_NONE = 128; #define SIZEOF_LONG 8 #endif #if !defined(SIZEOF_PID_T) - #if PSUTIL_SIZEOF_PID_T != 4 - #warning "SIZEOF_PID_T was guessed" - #endif #define SIZEOF_PID_T PSUTIL_SIZEOF_PID_T // set as a macro in setup.py #endif diff --git a/setup.py b/setup.py index e7c6931619..76c4ddbaaa 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ # Needed to determine _Py_PARSE_PID in case it's missing (Python 2, PyPy). # Taken from Lib/test/test_fcntl.py. # XXX: not bullet proof as the (long long) case is missing. -if struct.calcsize('l') == 8: +if struct.calcsize('l') <= 8: macros.append(('PSUTIL_SIZEOF_PID_T', '4')) # int else: macros.append(('PSUTIL_SIZEOF_PID_T', '8')) # long From b0cff82c6b91f99d2962f10d8d61d64853296e36 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 22:29:14 +0100 Subject: [PATCH 0497/1714] [Windows] increase precision of boot_time() and proc create_time() (#1693) --- HISTORY.rst | 2 + psutil/_psutil_common.c | 38 ++++++++++++++++ psutil/_psutil_common.h | 2 + psutil/_psutil_windows.c | 71 ++++++------------------------ psutil/arch/windows/process_info.c | 11 +++-- 5 files changed, 60 insertions(+), 64 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 44d887e381..539c6fd4bc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,8 @@ XXXX-XX-XX - 1679_: [Windows] net_connections() and Process.connections() are 10% faster. - 1681_: [Linux] disk_partitions() now also shows swap partitions. - 1686_: [Windows] added support for PyPy on Windows. +- 1693_: [Windows] boot_time() and Process.create_time() now have the precision + of a micro second (before the precision was of a second). **Bug fixes** diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 028e48e00c..07578edaa2 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -352,6 +352,7 @@ psutil_set_winver() { return 0; } + int psutil_load_globals() { if (psutil_loadlibs() != 0) @@ -362,4 +363,41 @@ psutil_load_globals() { InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); return 0; } + + +/* + * Convert the hi and lo parts of a FILETIME structure or a LARGE_INTEGER + * to a UNIX time. + * A FILETIME contains a 64-bit value representing the number of + * 100-nanosecond intervals since January 1, 1601 (UTC). + * A UNIX time is the number of seconds that have elapsed since the + * UNIX epoch, that is the time 00:00:00 UTC on 1 January 1970. + */ +static double +_to_unix_time(ULONGLONG hiPart, ULONGLONG loPart) { + ULONGLONG ret; + + // 100 nanosecond intervals since January 1, 1601. + ret = hiPart << 32; + ret += loPart; + // Change starting time to the Epoch (00:00:00 UTC, January 1, 1970). + ret -= 116444736000000000ull; + // Convert nano secs to secs. + return (double) ret / 10000000ull; +} + + +double +psutil_FiletimeToUnixTime(FILETIME ft) { + return _to_unix_time((ULONGLONG)ft.dwHighDateTime, + (ULONGLONG)ft.dwLowDateTime); + +} + + +double +psutil_LargeIntegerToUnixTime(LARGE_INTEGER li) { + return _to_unix_time((ULONGLONG)li.HighPart, + (ULONGLONG)li.LowPart); +} #endif // PSUTIL_WINDOWS diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 9754320693..34c428c058 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -134,4 +134,6 @@ int psutil_setup(void); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); + double psutil_FiletimeToUnixTime(FILETIME ft); + double psutil_LargeIntegerToUnixTime(LARGE_INTEGER li); #endif diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index f940570707..64592103d2 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -79,30 +79,13 @@ psutil_get_num_cpus(int fail_on_err) { */ static PyObject * psutil_boot_time(PyObject *self, PyObject *args) { - ULONGLONG uptime; - time_t pt; + ULONGLONG upTime; FILETIME fileTime; - ULONGLONG ll; GetSystemTimeAsFileTime(&fileTime); - /* - HUGE thanks to: - http://johnstewien.spaces.live.com/blog/cns!E6885DB5CEBABBC8!831.entry - - This function converts the FILETIME structure to the 32 bit - Unix time structure. - The time_t is a 32-bit value for the number of seconds since - January 1, 1970. A FILETIME is a 64-bit for the number of - 100-nanosecond periods since January 1, 1601. Convert by - subtracting the number of 100-nanosecond period between 01-01-1970 - and 01-01-1601, from time_t the divide by 1e+7 to get to the same - base granularity. - */ - ll = (((ULONGLONG) - (fileTime.dwHighDateTime)) << 32) + fileTime.dwLowDateTime; - pt = (time_t)((ll - 116444736000000000ull) / 10000000ull); - uptime = GetTickCount64() / 1000ull; - return Py_BuildValue("K", pt - uptime); + // Number of milliseconds that have elapsed since the system was started. + upTime = GetTickCount64() / 1000ull; + return Py_BuildValue("d", psutil_FiletimeToUnixTime(fileTime) - upTime); } @@ -320,10 +303,10 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { */ return Py_BuildValue( "(dd)", - (double)(ftUser.dwHighDateTime * 429.4967296 + \ - ftUser.dwLowDateTime * 1e-7), - (double)(ftKernel.dwHighDateTime * 429.4967296 + \ - ftKernel.dwLowDateTime * 1e-7) + (double)(ftUser.dwHighDateTime * HI_T + \ + ftUser.dwLowDateTime * LO_T), + (double)(ftKernel.dwHighDateTime * HI_T + \ + ftKernel.dwLowDateTime * LO_T) ); } @@ -335,7 +318,6 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { static PyObject * psutil_proc_create_time(PyObject *self, PyObject *args) { DWORD pid; - long long unix_time; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; @@ -363,34 +345,7 @@ psutil_proc_create_time(PyObject *self, PyObject *args) { } CloseHandle(hProcess); - - /* - // Make sure the process is not gone as OpenProcess alone seems to be - // unreliable in doing so (it seems a previous call to p.wait() makes - // it unreliable). - // This check is important as creation time is used to make sure the - // process is still running. - ret = GetExitCodeProcess(hProcess, &exitCode); - CloseHandle(hProcess); - if (ret != 0) { - if (exitCode != STILL_ACTIVE) - return NoSuchProcess("GetExitCodeProcess"); - } - else { - // Ignore access denied as it means the process is still alive. - // For all other errors, we want an exception. - if (GetLastError() != ERROR_ACCESS_DENIED) - return PyErr_SetFromWindowsErr(0); - } - */ - - // Convert the FILETIME structure to a Unix time. - // It's the best I could find by googling and borrowing code here - // and there. The time returned has a precision of 1 second. - unix_time = ((LONGLONG)ftCreate.dwHighDateTime) << 32; - unix_time += ftCreate.dwLowDateTime - 116444736000000000LL; - unix_time /= 10000000; - return Py_BuildValue("d", (double)unix_time); + return Py_BuildValue("d", psutil_FiletimeToUnixTime(ftCreate)); } @@ -844,10 +799,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { py_tuple = Py_BuildValue( "kdd", te32.th32ThreadID, - (double)(ftUser.dwHighDateTime * 429.4967296 + \ - ftUser.dwLowDateTime * 1e-7), - (double)(ftKernel.dwHighDateTime * 429.4967296 + \ - ftKernel.dwLowDateTime * 1e-7)); + (double)(ftUser.dwHighDateTime * HI_T + \ + ftUser.dwLowDateTime * LO_T), + (double)(ftKernel.dwHighDateTime * HI_T + \ + ftKernel.dwLowDateTime * LO_T)); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index d8104c818b..73a6991279 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -578,7 +578,8 @@ psutil_get_environ(DWORD pid) { * fills the structure with various process information in one shot * by using NtQuerySystemInformation. * We use this as a fallback when faster functions fail with access - * denied. This is slower because it iterates over all processes. + * denied. This is slower because it iterates over all processes + * but it doesn't require any privilege (also work for PID 0). * On success return 1, else 0 with Python exception already set. */ int @@ -669,7 +670,7 @@ psutil_proc_info(PyObject *self, PyObject *args) { ULONG ctx_switches = 0; double user_time; double kernel_time; - long long create_time; + double create_time; PyObject *py_retlist; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) @@ -692,9 +693,7 @@ psutil_proc_info(PyObject *self, PyObject *args) { create_time = 0; } else { - create_time = ((LONGLONG)process->CreateTime.HighPart) << 32; - create_time += process->CreateTime.LowPart - 116444736000000000LL; - create_time /= 10000000; + create_time = psutil_LargeIntegerToUnixTime(process->CreateTime); } py_retlist = Py_BuildValue( @@ -707,7 +706,7 @@ psutil_proc_info(PyObject *self, PyObject *args) { ctx_switches, // num ctx switches user_time, // cpu user time kernel_time, // cpu kernel time - (double)create_time, // create time + create_time, // create time (int)process->NumberOfThreads, // num threads // IO counters process->ReadOperationCount.QuadPart, // io rcount From d6633684407c33673dcdb90067010c8e36361747 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:35:39 -0800 Subject: [PATCH 0498/1714] refactoring: get rid of duplicated code; use one function to return (user, sys, create time) --- psutil/_psutil_windows.c | 50 ++++------------------------------------ psutil/_pswindows.py | 5 ++-- 2 files changed, 8 insertions(+), 47 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 64592103d2..109ed6c589 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -265,7 +265,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { * Return a Python tuple (user_time, kernel_time) */ static PyObject * -psutil_proc_cpu_times(PyObject *self, PyObject *args) { +psutil_proc_times(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; @@ -302,53 +302,16 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { * below from Python's Modules/posixmodule.c */ return Py_BuildValue( - "(dd)", + "(ddd)", (double)(ftUser.dwHighDateTime * HI_T + \ ftUser.dwLowDateTime * LO_T), (double)(ftKernel.dwHighDateTime * HI_T + \ - ftKernel.dwLowDateTime * LO_T) + ftKernel.dwLowDateTime * LO_T), + psutil_FiletimeToUnixTime(ftCreate) ); } -/* - * Return a Python float indicating the process create time expressed in - * seconds since the epoch. - */ -static PyObject * -psutil_proc_create_time(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - FILETIME ftCreate, ftExit, ftKernel, ftUser; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - // special case for PIDs 0 and 4, return system boot time - if (0 == pid || 4 == pid) - return psutil_boot_time(NULL, NULL); - - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (hProcess == NULL) - return NULL; - if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { - if (GetLastError() == ERROR_ACCESS_DENIED) { - // usually means the process has died so we throw a - // NoSuchProcess here - NoSuchProcess("GetProcessTimes"); - } - else { - PyErr_SetFromWindowsErr(0); - } - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); - return Py_BuildValue("d", psutil_FiletimeToUnixTime(ftCreate)); -} - - /* * Return process cmdline as a Python list of cmdline arguments. */ @@ -1573,11 +1536,8 @@ PsutilMethods[] = { "Return path of the process executable"}, {"proc_kill", psutil_proc_kill, METH_VARARGS, "Kill the process identified by the given PID"}, - {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, + {"proc_times", psutil_proc_times, METH_VARARGS, "Return tuple of user/kern time for the given PID"}, - {"proc_create_time", psutil_proc_create_time, METH_VARARGS, - "Return a float indicating the process create time expressed in " - "seconds since the epoch"}, {"proc_memory_info", psutil_proc_memory_info, METH_VARARGS, "Return a tuple of process memory information"}, {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index d8abf2e67d..0cdea9d16d 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -929,7 +929,8 @@ def username(self): @wrap_exceptions def create_time(self): try: - return cext.proc_create_time(self.pid) + user, kernel, created = cext.proc_times(self.pid) + return created except OSError as err: if is_permission_err(err): return self.oneshot_info()[pinfo_map['create_time']] @@ -951,7 +952,7 @@ 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 not is_permission_err(err): raise From 8148cae186d114bceff4e1d98b83689acfb0348a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:38:07 -0800 Subject: [PATCH 0499/1714] rename method --- psutil/_pswindows.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 0cdea9d16d..c6d2d04b9f 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -721,16 +721,16 @@ def __init__(self, pid): # --- oneshot() stuff def oneshot_enter(self): - self.oneshot_info.cache_activate(self) + self._proc_info.cache_activate(self) self.exe.cache_activate(self) def oneshot_exit(self): - self.oneshot_info.cache_deactivate(self) + self._proc_info.cache_deactivate(self) self.exe.cache_deactivate(self) @wrap_exceptions @memoize_when_activated - def oneshot_info(self): + def _proc_info(self): """Return multiple information about this process as a raw tuple. """ @@ -812,7 +812,7 @@ def _get_raw_meminfo(self): 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']], @@ -933,12 +933,12 @@ def create_time(self): return created except OSError as err: if is_permission_err(err): - return self.oneshot_info()[pinfo_map['create_time']] + 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): @@ -956,7 +956,7 @@ def cpu_times(self): except OSError as err: if not is_permission_err(err): raise - info = self.oneshot_info() + 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). @@ -1037,7 +1037,7 @@ def io_counters(self): except OSError as err: if not is_permission_err(err): raise - info = self.oneshot_info() + info = self._proc_info() ret = ( info[pinfo_map['io_rcount']], info[pinfo_map['io_wcount']], @@ -1094,11 +1094,11 @@ def num_handles(self): return cext.proc_num_handles(self.pid) except OSError as err: if is_permission_err(err): - return self.oneshot_info()[pinfo_map['num_handles']] + 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) From 2b28c332f3bfbb4744d6f7de3d10e2bdf27a4570 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:42:44 -0800 Subject: [PATCH 0500/1714] remove unnecessary wrap_exceptions deco --- psutil/_pswindows.py | 1 - 1 file changed, 1 deletion(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index c6d2d04b9f..e9aaeed33f 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -728,7 +728,6 @@ def oneshot_exit(self): self._proc_info.cache_deactivate(self) self.exe.cache_deactivate(self) - @wrap_exceptions @memoize_when_activated def _proc_info(self): """Return multiple information about this process as a From 57367dca114e8224134830ca9b702aa666229b6e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:45:42 -0800 Subject: [PATCH 0501/1714] bind cpu_times() and create_time() with oneshot() --- psutil/_pswindows.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e9aaeed33f..1917a86235 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -722,10 +722,12 @@ def __init__(self, pid): def oneshot_enter(self): self._proc_info.cache_activate(self) + self._proc_times.cache_activate(self) self.exe.cache_activate(self) def oneshot_exit(self): self._proc_info.cache_deactivate(self) + self._proc_times.cache_deactivate(self) self.exe.cache_deactivate(self) @memoize_when_activated @@ -737,6 +739,11 @@ def _proc_info(self): assert len(ret) == len(pinfo_map) return ret + @memoize_when_activated + def _proc_times(self): + user, system, created = cext.proc_times(self.pid) + return (user, system, created) + def name(self): """Return process name, which on Windows is always the final part of the executable. @@ -928,7 +935,7 @@ def username(self): @wrap_exceptions def create_time(self): try: - user, kernel, created = cext.proc_times(self.pid) + user, kernel, created = self._proc_times() return created except OSError as err: if is_permission_err(err): @@ -951,7 +958,7 @@ def threads(self): @wrap_exceptions def cpu_times(self): try: - user, system, created = cext.proc_times(self.pid) + user, system, created = self._proc_times() except OSError as err: if not is_permission_err(err): raise From e92f011300a0dcc66559255f57cda4a0bfbdc2be Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:47:12 -0800 Subject: [PATCH 0502/1714] rename var --- psutil/_pswindows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 1917a86235..3b51dabfbb 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -935,7 +935,7 @@ def username(self): @wrap_exceptions def create_time(self): try: - user, kernel, created = self._proc_times() + user, system, created = self._proc_times() return created except OSError as err: if is_permission_err(err): From 649ca88ea06f587d51117daa6295ae027d836d05 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:55:12 -0800 Subject: [PATCH 0503/1714] remove cache for proc_times(): unnecessary because create_time() is already cached --- psutil/_pswindows.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 3b51dabfbb..99d5d71499 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -722,12 +722,10 @@ def __init__(self, pid): def oneshot_enter(self): self._proc_info.cache_activate(self) - self._proc_times.cache_activate(self) self.exe.cache_activate(self) def oneshot_exit(self): self._proc_info.cache_deactivate(self) - self._proc_times.cache_deactivate(self) self.exe.cache_deactivate(self) @memoize_when_activated @@ -739,11 +737,6 @@ def _proc_info(self): assert len(ret) == len(pinfo_map) return ret - @memoize_when_activated - def _proc_times(self): - user, system, created = cext.proc_times(self.pid) - return (user, system, created) - def name(self): """Return process name, which on Windows is always the final part of the executable. @@ -934,8 +927,10 @@ def username(self): @wrap_exceptions def create_time(self): + # Note: proc_times() not put under oneshot() 'cause create_time() + # is already cached by the main Process class. try: - user, system, created = self._proc_times() + user, system, created = cext.proc_times(self.pid) return created except OSError as err: if is_permission_err(err): @@ -958,7 +953,7 @@ def threads(self): @wrap_exceptions def cpu_times(self): try: - user, system, created = self._proc_times() + user, system, created = cext.proc_times(self.pid) except OSError as err: if not is_permission_err(err): raise From 7ce6c3c1626bd7101f69031e36b980639ca591d6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 13:35:39 -0800 Subject: [PATCH 0504/1714] refactoring: get rid of duplicated code; use one function to return (user, sys, create time) rename method remove unnecessary wrap_exceptions deco bind cpu_times() and create_time() with oneshot() rename var remove cache for proc_times(): unnecessary because create_time() is already cached --- psutil/_psutil_windows.c | 50 ++++------------------------------------ psutil/_pswindows.py | 28 +++++++++++----------- 2 files changed, 20 insertions(+), 58 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 64592103d2..109ed6c589 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -265,7 +265,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { * Return a Python tuple (user_time, kernel_time) */ static PyObject * -psutil_proc_cpu_times(PyObject *self, PyObject *args) { +psutil_proc_times(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; FILETIME ftCreate, ftExit, ftKernel, ftUser; @@ -302,53 +302,16 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { * below from Python's Modules/posixmodule.c */ return Py_BuildValue( - "(dd)", + "(ddd)", (double)(ftUser.dwHighDateTime * HI_T + \ ftUser.dwLowDateTime * LO_T), (double)(ftKernel.dwHighDateTime * HI_T + \ - ftKernel.dwLowDateTime * LO_T) + ftKernel.dwLowDateTime * LO_T), + psutil_FiletimeToUnixTime(ftCreate) ); } -/* - * Return a Python float indicating the process create time expressed in - * seconds since the epoch. - */ -static PyObject * -psutil_proc_create_time(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - FILETIME ftCreate, ftExit, ftKernel, ftUser; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - // special case for PIDs 0 and 4, return system boot time - if (0 == pid || 4 == pid) - return psutil_boot_time(NULL, NULL); - - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (hProcess == NULL) - return NULL; - if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { - if (GetLastError() == ERROR_ACCESS_DENIED) { - // usually means the process has died so we throw a - // NoSuchProcess here - NoSuchProcess("GetProcessTimes"); - } - else { - PyErr_SetFromWindowsErr(0); - } - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); - return Py_BuildValue("d", psutil_FiletimeToUnixTime(ftCreate)); -} - - /* * Return process cmdline as a Python list of cmdline arguments. */ @@ -1573,11 +1536,8 @@ PsutilMethods[] = { "Return path of the process executable"}, {"proc_kill", psutil_proc_kill, METH_VARARGS, "Kill the process identified by the given PID"}, - {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, + {"proc_times", psutil_proc_times, METH_VARARGS, "Return tuple of user/kern time for the given PID"}, - {"proc_create_time", psutil_proc_create_time, METH_VARARGS, - "Return a float indicating the process create time expressed in " - "seconds since the epoch"}, {"proc_memory_info", psutil_proc_memory_info, METH_VARARGS, "Return a tuple of process memory information"}, {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index d8abf2e67d..99d5d71499 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -721,16 +721,15 @@ def __init__(self, pid): # --- oneshot() stuff def oneshot_enter(self): - self.oneshot_info.cache_activate(self) + self._proc_info.cache_activate(self) self.exe.cache_activate(self) def oneshot_exit(self): - self.oneshot_info.cache_deactivate(self) + self._proc_info.cache_deactivate(self) self.exe.cache_deactivate(self) - @wrap_exceptions @memoize_when_activated - def oneshot_info(self): + def _proc_info(self): """Return multiple information about this process as a raw tuple. """ @@ -812,7 +811,7 @@ def _get_raw_meminfo(self): 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']], @@ -928,16 +927,19 @@ def username(self): @wrap_exceptions def create_time(self): + # 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 is_permission_err(err): - return self.oneshot_info()[pinfo_map['create_time']] + 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): @@ -951,11 +953,11 @@ 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 not is_permission_err(err): raise - info = self.oneshot_info() + 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). @@ -1036,7 +1038,7 @@ def io_counters(self): except OSError as err: if not is_permission_err(err): raise - info = self.oneshot_info() + info = self._proc_info() ret = ( info[pinfo_map['io_rcount']], info[pinfo_map['io_wcount']], @@ -1093,11 +1095,11 @@ def num_handles(self): return cext.proc_num_handles(self.pid) except OSError as err: if is_permission_err(err): - return self.oneshot_info()[pinfo_map['num_handles']] + 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) From 29846dab6fdb1d7a9e46fb644417217cedffd115 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 23:28:57 +0100 Subject: [PATCH 0505/1714] #1681, revert 00a3398 --- HISTORY.rst | 1 - docs/index.rst | 2 -- psutil/_pslinux.py | 25 ------------------------- psutil/tests/test_linux.py | 19 ------------------- 4 files changed, 47 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 539c6fd4bc..7341b6d7ae 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,7 +17,6 @@ XXXX-XX-XX - 1677_: [Windows] process exe() will succeed for all process PIDs (instead of raising AccessDenied). - 1679_: [Windows] net_connections() and Process.connections() are 10% faster. -- 1681_: [Linux] disk_partitions() now also shows swap partitions. - 1686_: [Windows] added support for PyPy on Windows. - 1693_: [Windows] boot_time() and Process.create_time() now have the precision of a micro second (before the precision was of a second). diff --git a/docs/index.rst b/docs/index.rst index e60058cd56..ea5f15bd12 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -405,8 +405,6 @@ Disks [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] - .. versionchanged:: 5.7.0 swap partitions are shown on Linux. - .. function:: disk_usage(path) Return disk usage statistics about the partition which contains the given diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1bd8e987ae..9e32f25e7b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1179,31 +1179,6 @@ def disk_partitions(all=False): ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) - # swap - if all: - try: - f = open_text("%s/swaps" % procfs_path) - except FileNotFoundError: - pass - else: - with f: - f.readline() # header - for line in f.readlines(): - fields = line.split('\t') - device = fields[0].split()[0] - mountp = None - fstype = 'swap' - # The priority column is useful when multiple swap - # files are in use. The lower the priority, the - # more likely the swap file is to be used. - prio = fields[-1].strip() - if re.match(r'(-)?\d+', prio): - opts = "priority=" + prio - else: - opts = '' - ntuple = _common.sdiskpart(device, mountp, fstype, opts) - retlist.append(ntuple) - return retlist diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 5a48a4451c..97946a0bbd 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1067,25 +1067,6 @@ def test_emulate_realpath_fail(self): finally: psutil.PROCFS_PATH = "/proc" - @unittest.skipIf(not os.path.exists('/proc/swaps'), - "/proc/swaps not available") - def test_swap(self): - with open('/proc/swaps') as f: - if not f.readline() or not f.readlines(): - raise self.skipTest("/proc/swaps is empty") - types = [x.fstype for x in psutil.disk_partitions(all=False)] - self.assertNotIn('swap', types) - types = [x.fstype for x in psutil.disk_partitions(all=True)] - self.assertIn('swap', types) - for part in psutil.disk_partitions(all=True): - if part.fstype == 'swap': - assert os.path.exists(part.device), part - self.assertIsNone(part.mountpoint) - if part.opts: - assert part.opts.startswith('priority=') - prio = part.opts.split('=')[1] - int(prio) - @unittest.skipIf(not LINUX, "LINUX only") class TestSystemDiskIoCounters(unittest.TestCase): From 29b02fc1bc4f9018f06a876ca4aa03355dd271a7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Feb 2020 00:39:32 +0100 Subject: [PATCH 0506/1714] small refactoring --- psutil/_compat.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/psutil/_compat.py b/psutil/_compat.py index 07ab909a84..bd8e99f904 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -54,6 +54,7 @@ def b(s): else: # https://github.com/PythonCharmers/python-future/blob/exceptions/ # src/future/types/exceptions/pep3151.py + _singleton = object() def instance_checking_exception(base_exception=Exception): def wrapped(instance_checker): @@ -84,28 +85,28 @@ def __subclasscheck__(cls, classinfo): @instance_checking_exception(EnvironmentError) def FileNotFoundError(inst): - return getattr(inst, 'errno', object()) == errno.ENOENT + return getattr(inst, 'errno', _singleton) == errno.ENOENT @instance_checking_exception(EnvironmentError) def ProcessLookupError(inst): - return getattr(inst, 'errno', object()) == errno.ESRCH + return getattr(inst, 'errno', _singleton) == errno.ESRCH @instance_checking_exception(EnvironmentError) def PermissionError(inst): - return getattr(inst, 'errno', object()) in ( + return getattr(inst, 'errno', _singleton) in ( errno.EACCES, errno.EPERM) @instance_checking_exception(EnvironmentError) def InterruptedError(inst): - return getattr(inst, 'errno', object()) == errno.EINTR + return getattr(inst, 'errno', _singleton) == errno.EINTR @instance_checking_exception(EnvironmentError) def ChildProcessError(inst): - return getattr(inst, 'errno', object()) == errno.ECHILD + return getattr(inst, 'errno', _singleton) == errno.ECHILD @instance_checking_exception(EnvironmentError) def FileExistsError(inst): - return getattr(inst, 'errno', object()) == errno.EEXIST + return getattr(inst, 'errno', _singleton) == errno.EEXIST # --- stdlib additions From be6c868f12a7b7c3f0fe0ed5a01d3870567d45eb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Feb 2020 01:13:43 +0100 Subject: [PATCH 0507/1714] #1659: provide error message in case of bugged PYPY2 version --- Makefile | 2 +- psutil/_compat.py | 12 ++++++++++++ psutil/tests/test_unicode.py | 4 ---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index fd50aeca2c..890c6e413f 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ DEPS = \ pyperf \ requests \ setuptools \ - sphinx==2.2.2 \ + sphinx \ twine \ unittest2 \ virtualenv \ diff --git a/psutil/_compat.py b/psutil/_compat.py index bd8e99f904..a9371382bd 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -54,6 +54,8 @@ def b(s): else: # https://github.com/PythonCharmers/python-future/blob/exceptions/ # src/future/types/exceptions/pep3151.py + import platform + _singleton = object() def instance_checking_exception(base_exception=Exception): @@ -108,6 +110,16 @@ def ChildProcessError(inst): def FileExistsError(inst): return getattr(inst, 'errno', _singleton) == errno.EEXIST + if platform.python_implementation() != "CPython": + try: + raise OSError(errno.EEXIST, "perm") + except FileExistsError: + pass + except OSError: + raise RuntimeError( + "broken / incompatible Python implementation, see: " + "https://github.com/giampaolo/psutil/issues/1659") + # --- stdlib additions diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 81a28807c3..3c1c4a396c 100644 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -305,8 +305,6 @@ def normpath(p): @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 @@ -324,8 +322,6 @@ def expect_exact_path_match(cls): @unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS") @unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO @unittest.skipIf(PYPY, "unreliable on PYPY") -@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 From 17843980f8edf665915f09cc9abfa3a32c322fbf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 16:23:20 -0800 Subject: [PATCH 0508/1714] fix tests --- psutil/tests/test_unicode.py | 4 ++++ psutil/tests/test_windows.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 3c1c4a396c..81a28807c3 100644 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -305,6 +305,8 @@ def normpath(p): @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 @@ -322,6 +324,8 @@ def expect_exact_path_match(cls): @unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS") @unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO @unittest.skipIf(PYPY, "unreliable on PYPY") +@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 diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 8a93743f00..934ea8476e 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -625,14 +625,14 @@ def test_memory_info(self): def test_create_time(self): ctime = psutil.Process(self.pid).create_time() - with mock.patch("psutil._psplatform.cext.proc_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_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 From 8bc5a180e96445fcd9e96fa6aebf8bc698910d96 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Feb 2020 03:24:03 +0100 Subject: [PATCH 0509/1714] divide test_system.py unit tests in multiple classes --- psutil/tests/test_system.py | 383 +++++++++++++++++++----------------- 1 file changed, 200 insertions(+), 183 deletions(-) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 2d606be4d9..0d3f43753e 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -60,8 +60,7 @@ # =================================================================== -class TestSystemAPIs(unittest.TestCase): - """Tests for system-related APIs.""" +class TestProcessAPIs(unittest.TestCase): def setUp(self): safe_rmpath(TESTFN) @@ -190,12 +189,67 @@ def test_wait_procs_no_timeout(self): p.terminate() gone, alive = psutil.wait_procs(procs) + 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) + + +class TestMiscAPIs(unittest.TestCase): + 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: + 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) + @unittest.skipIf(not POSIX, 'POSIX only') def test_PAGESIZE(self): # pagesize is used internally to perform different calculations @@ -204,6 +258,55 @@ def test_PAGESIZE(self): import resource self.assertEqual(os.sysconf("SC_PAGE_SIZE"), resource.getpagesize()) + 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.assertIs(getattr(psutil, name), False, msg=name) + + +class TestMemoryAPIs(unittest.TestCase): + def test_virtual_memory(self): mem = psutil.virtual_memory() assert mem.total > 0, mem @@ -238,47 +341,8 @@ def test_swap_memory(self): 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 +class TestCpuAPIs(unittest.TestCase): def test_cpu_count_logical(self): logical = psutil.cpu_count() @@ -472,6 +536,55 @@ def test_per_cpu_times_percent_negative(self): 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) + + @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')) + 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 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) + + +class TestDiskAPIs(unittest.TestCase): + def test_disk_usage(self): usage = psutil.disk_usage(os.getcwd()) self.assertEqual(usage._fields, ('total', 'used', 'free', 'percent')) @@ -562,6 +675,50 @@ def find_mount_point(path): self.assertIn(mount, mounts) psutil.disk_usage(mount) + @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(unittest.TestCase): + @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') def test_net_io_counters(self): def check_ntuple(nt): @@ -704,148 +861,8 @@ def test_net_if_stats_enodev(self): 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(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 - - @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: - 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')) - 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 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) +class TestSensorsAPIs(unittest.TestCase): @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") def test_sensors_temperatures(self): From c67ab01eddd0cce391aeeb315d1fac8984cb2fba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2020 20:32:42 -0800 Subject: [PATCH 0510/1714] #1693: also increase precision of users()'s login time --- HISTORY.rst | 4 ++-- psutil/_psutil_windows.c | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7341b6d7ae..52b46f7fad 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,8 +18,8 @@ XXXX-XX-XX raising AccessDenied). - 1679_: [Windows] net_connections() and Process.connections() are 10% faster. - 1686_: [Windows] added support for PyPy on Windows. -- 1693_: [Windows] boot_time() and Process.create_time() now have the precision - of a micro second (before the precision was of a second). +- 1693_: [Windows] boot_time(), Process.create_time() and users()'s login time + now have 1 micro second precision (before the precision was of 1 second). **Bug fixes** diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 109ed6c589..82fa518eeb 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1195,7 +1195,6 @@ psutil_users(PyObject *self, PyObject *args) { DWORD bytes; PWTS_CLIENT_ADDRESS address; char address_str[50]; - long long unix_time; WINSTATION_INFO station_info; ULONG returnLen; PyObject *py_tuple = NULL; @@ -1271,18 +1270,15 @@ psutil_users(PyObject *self, PyObject *args) { goto error; } - unix_time = ((LONGLONG)station_info.ConnectTime.dwHighDateTime) << 32; - unix_time += \ - station_info.ConnectTime.dwLowDateTime - 116444736000000000LL; - unix_time /= 10000000; - py_username = PyUnicode_FromWideChar(buffer_user, wcslen(buffer_user)); if (py_username == NULL) goto error; - py_tuple = Py_BuildValue("OOd", - py_username, - py_address, - (double)unix_time); + py_tuple = Py_BuildValue( + "OOd", + py_username, + py_address, + psutil_FiletimeToUnixTime(station_info.ConnectTime) + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) From d03c176c0478ed96a529be103318c42fa2f0af9d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 01:53:16 +0100 Subject: [PATCH 0511/1714] update doc --- docs/index.rst | 45 ++++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ea5f15bd12..1a5c2434f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -861,24 +861,26 @@ Functions Return an iterator yielding a :class:`Process` class instance for all running processes on the local machine. - Every instance is only created once and then cached into an internal table - which is updated every time an element is yielded. - Cached :class:`Process` instances are checked for identity so that you're - safe in case a PID has been reused by another process, in which case the - cached instance is updated. - This is preferred over :func:`psutil.pids()` for iterating over processes. - Sorting order in which processes are returned is based on their PID. + This should be preferred over :func:`psutil.pids()` to iterate over processes + as it's safe from race condition. + + Every :class:`Process` instance is only created once, and then cached for the + next time :func:`psutil.process_iter()` is called (if PID is still alive). + Also it makes sure process PIDs are not reused. + *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. - If *attrs* is specified :meth:`Process.as_dict()` is called internally and - the resulting dict is stored as a ``info`` attribute which is attached to the - returned :class:`Process` instances. + If *attrs* is specified :meth:`Process.as_dict()` result will be stored as a + ``info`` attribute attached to the returned :class:`Process` instances. If *attrs* is an empty list it will retrieve all process info (slow). + If *new_only* is true this function will yield only new processes which appeared since the last time it was called. - Example usage:: + Sorting order in which processes are returned is based on their PID. + + Example:: >>> import psutil - >>> for proc in psutil.process_iter(attrs=['pid', 'name', 'username']): + >>> for proc in psutil.process_iter(('pid', 'name', 'username')): ... print(proc.info) ... {'name': 'systemd', 'pid': 1, 'username': 'root'} @@ -886,30 +888,19 @@ Functions {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} ... - Example of a dict comprehensions to create a ``{pid: info, ...}`` data - structure:: + A dict comprehensions to create a ``{pid: info, ...}`` data structure:: >>> import psutil - >>> procs = {p.pid: p.info for p in psutil.process_iter(attrs=['name', 'username'])} + >>> procs = {p.pid: p.info for p in psutil.process_iter(('name', 'username'))} >>> procs {1: {'name': 'systemd', 'username': 'root'}, 2: {'name': 'kthreadd', 'username': 'root'}, 3: {'name': 'ksoftirqd/0', 'username': 'root'}, ...} - Example showing how to filter processes by name (see also - `process filtering <#filtering-and-sorting-processes>`__ section for more - examples):: - - >>> import psutil - >>> [p.info for p in psutil.process_iter(attrs=['pid', 'name']) if 'python' in p.info['name']] - [{'name': 'python3', 'pid': 21947}, - {'name': 'python', 'pid': 23835}] + Get new processes since last call:: - Get new processes only (since last call):: - - >>> import psutil - >>> for proc in psutil.process_iter(attrs=['pid', 'name'], new_only=True): + >>> for proc in psutil.process_iter(('pid', 'name'), new_only=True): ... print(proc.info) ... From cec43a2015da2b7cf02d7f01b719731348881ce2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 13:05:44 +0100 Subject: [PATCH 0512/1714] update doc --- README.rst | 2 +- docs/index.rst | 20 ++++++++++---------- scripts/cpu_distribution.py | 2 +- scripts/netstat.py | 2 +- scripts/pidof.py | 2 +- scripts/procsmem.py | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index c28a55f9cf..56b4017e7d 100644 --- a/README.rst +++ b/README.rst @@ -456,7 +456,7 @@ Further process APIs .. code-block:: python >>> import psutil - >>> for proc in psutil.process_iter(attrs=['pid', 'name']): + >>> for proc in psutil.process_iter(['pid', 'name']): ... print(proc.info) ... {'pid': 1, 'name': 'systemd'} diff --git a/docs/index.rst b/docs/index.rst index 1a5c2434f8..499c7996b6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -880,7 +880,7 @@ Functions Example:: >>> import psutil - >>> for proc in psutil.process_iter(('pid', 'name', 'username')): + >>> for proc in psutil.process_iter(['pid', 'name', 'username']): ... print(proc.info) ... {'name': 'systemd', 'pid': 1, 'username': 'root'} @@ -891,7 +891,7 @@ Functions A dict comprehensions to create a ``{pid: info, ...}`` data structure:: >>> import psutil - >>> procs = {p.pid: p.info for p in psutil.process_iter(('name', 'username'))} + >>> procs = {p.pid: p.info for p in psutil.process_iter(['name', 'username'])} >>> procs {1: {'name': 'systemd', 'username': 'root'}, 2: {'name': 'kthreadd', 'username': 'root'}, @@ -900,7 +900,7 @@ Functions Get new processes since last call:: - >>> for proc in psutil.process_iter(('pid', 'name'), new_only=True): + >>> for proc in psutil.process_iter(['pid', 'name'], new_only=True): ... print(proc.info) ... @@ -2349,7 +2349,7 @@ Check string against :meth:`Process.name()`: def find_procs_by_name(name): "Return a list of processes matching 'name'." ls = [] - for p in psutil.process_iter(attrs=['name']): + for p in psutil.process_iter(['name']): if p.info['name'] == name: ls.append(p) return ls @@ -2365,7 +2365,7 @@ A bit more advanced, check string against :meth:`Process.name()`, def find_procs_by_name(name): "Return a list of processes matching 'name'." ls = [] - for p in psutil.process_iter(attrs=["name", "exe", "cmdline"]): + for p in psutil.process_iter(["name", "exe", "cmdline"]): if name == p.info['name'] or \ p.info['exe'] and os.path.basename(p.info['exe']) == name or \ p.info['cmdline'] and p.info['cmdline'][0] == name: @@ -2410,21 +2410,21 @@ A collection of code samples showing how to use :func:`process_iter()` to filter Processes owned by user:: >>> import getpass - >>> pp([(p.pid, p.info['name']) for p in psutil.process_iter(attrs=['name', 'username']) if p.info['username'] == getpass.getuser()]) + >>> pp([(p.pid, p.info['name']) for p in psutil.process_iter(['name', 'username']) if p.info['username'] == getpass.getuser()]) (16832, 'bash'), (19772, 'ssh'), (20492, 'python')] Processes actively running:: - >>> pp([(p.pid, p.info) for p in psutil.process_iter(attrs=['name', 'status']) if p.info['status'] == psutil.STATUS_RUNNING]) + >>> pp([(p.pid, p.info) for p in psutil.process_iter(['name', 'status']) if p.info['status'] == psutil.STATUS_RUNNING]) [(1150, {'name': 'Xorg', 'status': 'running'}), (1776, {'name': 'unity-panel-service', 'status': 'running'}), (20492, {'name': 'python', 'status': 'running'})] Processes using log files:: - >>> for p in psutil.process_iter(attrs=['name', 'open_files']): + >>> for p in psutil.process_iter(['name', 'open_files']): ... for file in p.info['open_files'] or []: ... if file.path.endswith('.log'): ... print("%-5s %-10s %s" % (p.pid, p.info['name'][:10], file.path)) @@ -2435,14 +2435,14 @@ Processes using log files:: Processes consuming more than 500M of memory:: - >>> pp([(p.pid, p.info['name'], p.info['memory_info'].rss) for p in psutil.process_iter(attrs=['name', 'memory_info']) if p.info['memory_info'].rss > 500 * 1024 * 1024]) + >>> pp([(p.pid, p.info['name'], p.info['memory_info'].rss) for p in psutil.process_iter(['name', 'memory_info']) if p.info['memory_info'].rss > 500 * 1024 * 1024]) [(2650, 'chrome', 532324352), (3038, 'chrome', 1120088064), (21915, 'sublime_text', 615407616)] Top 3 processes which consumed the most CPU time:: - >>> pp([(p.pid, p.info['name'], sum(p.info['cpu_times'])) for p in sorted(psutil.process_iter(attrs=['name', 'cpu_times']), key=lambda p: sum(p.info['cpu_times'][:2]))][-3:]) + >>> pp([(p.pid, p.info['name'], sum(p.info['cpu_times'])) for p in sorted(psutil.process_iter(['name', 'cpu_times']), key=lambda p: sum(p.info['cpu_times'][:2]))][-3:]) [(2721, 'chrome', 10219.73), (1150, 'Xorg', 11116.989999999998), (2650, 'chrome', 18451.97)] diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index c509c73288..839d31dff8 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -84,7 +84,7 @@ def main(): # processes procs = collections.defaultdict(list) - for p in psutil.process_iter(attrs=['name', 'cpu_num']): + for p in psutil.process_iter(['name', 'cpu_num']): procs[p.info['cpu_num']].append(p.info['name'][:5]) curr_line = 3 diff --git a/scripts/netstat.py b/scripts/netstat.py index 490b429f23..af56d03d37 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -41,7 +41,7 @@ def main(): "Proto", "Local address", "Remote address", "Status", "PID", "Program name")) proc_names = {} - for p in psutil.process_iter(attrs=['pid', 'name']): + for p in psutil.process_iter(['pid', 'name']): proc_names[p.info['pid']] = p.info['name'] for c in psutil.net_connections(kind='inet'): laddr = "%s:%s" % (c.laddr) diff --git a/scripts/pidof.py b/scripts/pidof.py index bcb8a2e6da..c1042f4f8e 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -18,7 +18,7 @@ def pidof(pgname): pids = [] - for proc in psutil.process_iter(attrs=['name', 'cmdline']): + for proc in psutil.process_iter(['name', 'cmdline']): # search for matches in the process name and cmdline if proc.info['name'] == pgname or \ proc.info['cmdline'] and proc.info['cmdline'][0] == pgname: diff --git a/scripts/procsmem.py b/scripts/procsmem.py index f660c085fc..4f8b4c84b6 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -64,7 +64,7 @@ def main(): with p.oneshot(): try: mem = p.memory_full_info() - info = p.as_dict(attrs=["cmdline", "username"]) + info = p.as_dict(["cmdline", "username"]) except psutil.AccessDenied: ad_pids.append(p.pid) except psutil.NoSuchProcess: From a1517dd205b46841d7317123dc1c0495ad48599b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 13:29:21 +0100 Subject: [PATCH 0513/1714] update doc --- docs/index.rst | 6 +++--- psutil/__init__.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 499c7996b6..6771999226 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -873,8 +873,8 @@ Functions ``info`` attribute attached to the returned :class:`Process` instances. If *attrs* is an empty list it will retrieve all process info (slow). - If *new_only* is true this function will yield only new processes which - appeared since the last time it was called. + If *new_only* is true this function will take into consideration only + new PIDs which appeared since the last time it was was called. Sorting order in which processes are returned is based on their PID. Example:: @@ -898,7 +898,7 @@ Functions 3: {'name': 'ksoftirqd/0', 'username': 'root'}, ...} - Get new processes since last call:: + Get only new processes since last call:: >>> for proc in psutil.process_iter(['pid', 'name'], new_only=True): ... print(proc.info) diff --git a/psutil/__init__.py b/psutil/__init__.py index e74bb109ce..a58b452cb3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1427,8 +1427,9 @@ def process_iter(attrs=None, ad_value=None, new_only=False): to returned Process instance. If *attrs* is an empty list it will retrieve all process info (slow). - If *new_only* is true this function will yield only new processes - which appeared since the last time it was called. + + If *new_only* is true this function will take into consideration + only new PIDs which appeared since the last time it was called. """ def add(pid): proc = Process(pid) From dc7278afe5a882bef1800e9aecf7acacb6257e94 Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Sat, 15 Feb 2020 13:39:52 +0100 Subject: [PATCH 0514/1714] Fix a compile error in _psutil_linux.c if PSUTIL_HAVE_IOPRIO is false (#1695) The macro PSUTIL_HAVE_IOPRIO is always defined. --- psutil/_psutil_linux.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index ec8b433aa7..915ab9b486 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -548,7 +548,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { static PyMethodDef mod_methods[] = { // --- per-process functions -#ifdef PSUTIL_HAVE_IOPRIO +#if PSUTIL_HAVE_IOPRIO {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS, "Get process I/O priority"}, {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS, From 3e3104abe4ec048b6968e6f8a6eea73cc59d418a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 13:42:34 +0100 Subject: [PATCH 0515/1714] update history for #1695 --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index b2234ce311..716de944c2 100644 --- a/CREDITS +++ b/CREDITS @@ -660,3 +660,7 @@ I: 1648 N: Mike Hommey W: https://github.com/glandium I: 1665 + +N: Anselm Kruis +W: https://github.com/akruis +I: 1695 diff --git a/HISTORY.rst b/HISTORY.rst index 52b46f7fad..605a564811 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -41,6 +41,8 @@ XXXX-XX-XX - 1674_: [SunOS] disk_partitions() may raise OSError. - 1684_: [Linux] disk_io_counters() may raise ValueError on systems not having /proc/diskstats. +- 1695_: [Linux] could not compile on kernels <= 2.6.13 due to + PSUTIL_HAVE_IOPRIO not being defined. (patch by Anselm Kruis) 5.6.7 ===== From a826d41cd880d4aea907f68351c4bc1414d2575c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 14:41:19 +0100 Subject: [PATCH 0516/1714] update doc --- docs/DEVGUIDE.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index e07d977e8c..0ec64c3ce1 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -104,6 +104,7 @@ Make a pull request - commit your changes: ``git commit -am 'add some feature'`` - push to the branch: ``git push origin new-feature`` - create a new pull request by via github web interface +- remember to update `HISTORY.rst`_ and `CREDITS`_ files. Continuous integration ====================== @@ -187,3 +188,5 @@ These are notes for myself (Giampaolo): .. _`RsT syntax`: http://docutils.sourceforge.net/docs/user/rst/quickref.htm .. _`sphinx`: http://sphinx-doc.org .. _`Travis`: https://travis-ci.org/giampaolo/psuti +.. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst +.. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS From a8c9e878d5b43cbb729187ae6e1304eebd09b8dc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 17:46:21 +0100 Subject: [PATCH 0517/1714] refactor print colors utils --- MANIFEST.in | 4 +- Makefile | 18 ++-- docs/Makefile | 7 ++ psutil/_common.py | 77 +++++++++++--- psutil/tests/runner.py | 100 +++++------------- psutil/tests/test_process.py | 1 - scripts/internal/print_access_denied.py | 7 +- scripts/internal/print_api_speed.py | 65 ++++-------- scripts/internal/scriptutils.py | 54 ---------- ...ownload_exes.py => win_download_wheels.py} | 8 +- 10 files changed, 133 insertions(+), 208 deletions(-) delete mode 100644 scripts/internal/scriptutils.py rename scripts/internal/{download_exes.py => win_download_wheels.py} (95%) diff --git a/MANIFEST.in b/MANIFEST.in index 038d4baab1..3b4232e9f8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +include .cirrus.yml include .coveragerc include .gitignore include CREDITS @@ -114,7 +115,6 @@ include scripts/internal/README include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py -include scripts/internal/download_exes.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/print_access_denied.py @@ -122,7 +122,7 @@ include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py include scripts/internal/print_timeline.py include scripts/internal/purge_installation.py -include scripts/internal/scriptutils.py +include scripts/internal/win_download_wheels.py include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py diff --git a/Makefile b/Makefile index 890c6e413f..812ba9c2c9 100644 --- a/Makefile +++ b/Makefile @@ -11,20 +11,24 @@ DEPS = \ check-manifest \ coverage \ flake8 \ - futures \ - ipaddress \ - mock==1.0.1 \ pyperf \ requests \ setuptools \ - sphinx \ twine \ - unittest2 \ virtualenv \ wheel +ifeq ($(PYTHON), $(filter $(PYTHON), python python2 python2.7)) + DEPS += \ + futures \ + ipaddress \ + mock==1.0.1 \ + unittest2 +endif + # In not in a virtualenv, add --user options for install commands. -INSTALL_OPTS = `$(PYTHON) -c "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` +INSTALL_OPTS = `$(PYTHON) -c \ + "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` TEST_PREFIX = PYTHONWARNINGS=all PSUTIL_TESTING=1 PSUTIL_DEBUG=1 all: test @@ -197,7 +201,7 @@ wheel: ## Generate wheel. $(PYTHON) setup.py bdist_wheel win-download-wheels: ## Download wheels hosted on appveyor. - $(TEST_PREFIX) $(PYTHON) scripts/internal/download_exes.py --user giampaolo --project psutil + $(TEST_PREFIX) $(PYTHON) scripts/internal/win_download_wheels.py --user giampaolo --project psutil upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist diff --git a/docs/Makefile b/docs/Makefile index c7f4723a71..cca5435fa2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,6 +15,9 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +DEPS = sphinx + + .PHONY: help help: @echo "Please use \`make ' where is one of" @@ -224,3 +227,7 @@ dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." + +.PHONY: setup-dev-env +setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). + $(PYTHON) -m pip install --user --upgrade --trusted-host files.pythonhosted.org $(DEPS) diff --git a/psutil/_common.py b/psutil/_common.py index 9306cd1556..728d9c6258 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -43,10 +43,9 @@ PY3 = sys.version_info[0] == 3 __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', @@ -58,6 +57,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', @@ -66,7 +67,9 @@ '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", - 'bytes2human', 'conn_to_ntuple', 'hilite', 'debug', + 'bytes2human', 'conn_to_ntuple', 'debug', + # shell utils + 'hilite', 'term_supports_colors', 'print_color', ] @@ -757,38 +760,78 @@ def decode(s): return s -def _term_supports_colors(file=sys.stdout): - if hasattr(_term_supports_colors, "ret"): - return _term_supports_colors.ret +# ===================================================================== +# --- shell utils +# ===================================================================== + + +@memoize +def term_supports_colors(file=sys.stdout): + if os.name == 'nt': + return True try: import curses assert file.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 except Exception: - _term_supports_colors.ret = False return False else: - _term_supports_colors.ret = True - return _term_supports_colors.ret + return True -def hilite(s, ok=True, bold=False): +def hilite(s, color="green", bold=False): """Return an highlighted version of 'string'.""" - if not _term_supports_colors(): + if not term_supports_colors(): return s attr = [] - if ok is None: # no color - pass - elif ok: # green - attr.append('32') - else: # red - attr.append('31') + colors = dict(green='32', red='91', brown='33') + colors[None] = '29' + try: + color = colors[color] + except KeyError: + raise ValueError("invalid color %r; choose between %r" % ( + 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="green", bold=False, file=sys.stdout): + """Print a colorized version of string.""" + if not term_supports_colors(): + print(s, file=file) + elif POSIX: + print(hilite(s, color, bold), file=file) + else: + import ctypes + + DEFAULT_COLOR = 7 + GetStdHandle = ctypes.windll.Kernel32.GetStdHandle + SetConsoleTextAttribute = \ + ctypes.windll.Kernel32.SetConsoleTextAttribute + + colors = dict(green=2, red=4, brown=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) + finally: + SetConsoleTextAttribute(handle, DEFAULT_COLOR) + + if bool(os.getenv('PSUTIL_DEBUG', 0)): import inspect diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index f8601badb9..4c3359ddba 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -5,12 +5,13 @@ # found in the LICENSE file. """ -Unit test runner, providing colourized output and printing failures -on KeyboardInterrupt. +Unit test runner, providing new features on top of unittest module: +- colourized output (error, skip) +- print failures/tracebacks on CTRL+C +- re-run failed tests only (make test-failed) """ from __future__ import print_function -import atexit import os import sys import unittest @@ -23,7 +24,9 @@ ctypes = None import psutil -from psutil._common import memoize +from psutil._common import hilite +from psutil._common import print_color +from psutil._common import term_supports_colors from psutil.tests import safe_rmpath from psutil.tests import TOX @@ -31,95 +34,37 @@ 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) + + +# ===================================================================== +# --- unittest subclasses +# ===================================================================== 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 _print_color(self, s, color, bold=False): + file = sys.stderr if color == "red" else sys.stdout + print_color(s, color, bold=bold, file=file) def addSuccess(self, test): TestResult.addSuccess(self, test) - self._color_print("OK", GREEN) + self._print_color("OK", "green") def addError(self, test, err): TestResult.addError(self, test, err) - self._color_print("ERROR", RED, bold=True) + self._print_color("ERROR", "red", bold=True) def addFailure(self, test, err): TestResult.addFailure(self, test, err) - self._color_print("FAIL", RED) + self._print_color("FAIL", "red") def addSkip(self, test, reason): TestResult.addSkip(self, test, reason) - self._color_print("skipped: %s" % reason, BROWN) + self._print_color("skipped: %s" % reason, "brown") def printErrorList(self, flavour, errors): - flavour = hilite(flavour, RED, bold=flavour == 'ERROR') + flavour = hilite(flavour, "red", bold=flavour == 'ERROR') TextTestResult.printErrorList(self, flavour, errors) @@ -133,6 +78,11 @@ def _makeResult(self): return self.result +# ===================================================================== +# --- public API +# ===================================================================== + + def setup_tests(): if 'PSUTIL_TESTING' not in os.environ: # This won't work on Windows but set_testing() below will do it. diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0277a56a2b..e5ff6e45cd 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -311,7 +311,6 @@ def test_terminal(self): @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: diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 2c757fd782..9123ba6d05 100644 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -50,7 +50,7 @@ import time import psutil -from scriptutils import hilite +from psutil._common import print_color def main(): @@ -75,13 +75,12 @@ def main(): # print templ = "%-20s %-5s %-9s %s" s = templ % ("API", "AD", "Percent", "Outcome") - print(hilite(s, ok=None, bold=True)) + print_color(s, color=None, bold=True) for methname, ads in sorted(d.items(), key=lambda x: (x[1], x[0])): perc = (ads / tot_procs) * 100 outcome = "SUCCESS" if not ads else "ACCESS DENIED" s = templ % (methname, ads, "%6.1f%%" % perc, outcome) - s = hilite(s, ok=not ads) - print(s) + print_color(s, "red" if ads else None) tot_perc = round((tot_ads / tot_calls) * 100, 1) print("-" * 50) print("Totals: access-denied=%s (%s%%), calls=%s, processes=%s, " diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index a99293c458..85d1cfc5bd 100644 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -9,53 +9,45 @@ $ make print_api_speed SYSTEM APIS SECONDS ---------------------------------- -boot_time 0.000140 -cpu_count 0.000016 -cpu_count (cores) 0.000312 -cpu_freq 0.000811 -cpu_percent 0.000138 -cpu_stats 0.000165 -cpu_times 0.000140 +cpu_count 0.000014 +disk_usage 0.000027 +cpu_times 0.000037 +cpu_percent 0.000045 ... PROCESS APIS SECONDS ---------------------------------- -children 0.007246 -cmdline 0.000069 -connections 0.000072 -cpu_affinity 0.000012 -cpu_num 0.000035 -cpu_percent 0.000042 -cpu_times 0.000031 +create_time 0.000001 +nice 0.000005 +cwd 0.000011 +cpu_affinity 0.000011 +ionice 0.000013 +... """ from __future__ import print_function, division from timeit import default_timer as timer -import argparse import inspect import os import psutil -from scriptutils import hilite +from psutil._common import print_color -SORT_BY_TIME = False if psutil.POSIX else True -TOP_SLOWEST = 7 timings = [] templ = "%-25s %s" def print_timings(): - slower = [] - timings.sort(key=lambda x: x[1 if SORT_BY_TIME else 0]) - for x in sorted(timings, key=lambda x: x[1], reverse=1)[:TOP_SLOWEST]: - slower.append(x[0]) + timings.sort(key=lambda x: x[1]) + i = 0 while timings[:]: title, elapsed = timings.pop(0) s = templ % (title, "%f" % elapsed) - if title in slower: - s = hilite(s, ok=False) - print(s) + if i > len(timings) - 5: + print_color(s, color="red") + else: + print(s) def timecall(title, fun, *args, **kw): @@ -65,11 +57,7 @@ def timecall(title, fun, *args, **kw): timings.append((title, elapsed)) -def titlestr(s): - return hilite(s, ok=None, bold=True) - - -def run(): +def main(): # --- system public_apis = [] @@ -83,7 +71,7 @@ def run(): if name not in ignore: public_apis.append(name) - print(titlestr(templ % ("SYSTEM APIS", "SECONDS"))) + print_color(templ % ("SYSTEM APIS", "SECONDS"), color=None, bold=True) print("-" * 34) for name in public_apis: fun = getattr(psutil, name) @@ -99,7 +87,7 @@ def run(): # --- process print("") - print(titlestr(templ % ("PROCESS APIS", "SECONDS"))) + print_color(templ % ("PROCESS APIS", "SECONDS"), color=None, bold=True) print("-" * 34) ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', @@ -114,18 +102,5 @@ def run(): print_timings() -def main(): - global SORT_BY_TIME, TOP_SLOWEST - parser = argparse.ArgumentParser(description='Benchmark all API calls') - parser.add_argument('-t', '--time', required=False, default=SORT_BY_TIME, - action='store_true', help="sort by timings") - parser.add_argument('-s', '--slowest', required=False, default=TOP_SLOWEST, - help="highlight the top N slowest APIs") - args = parser.parse_args() - SORT_BY_TIME = bool(args.time) - TOP_SLOWEST = int(args.slowest) - run() - - if __name__ == '__main__': main() diff --git a/scripts/internal/scriptutils.py b/scripts/internal/scriptutils.py deleted file mode 100644 index 3ee416f825..0000000000 --- a/scripts/internal/scriptutils.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/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. - -"""Utils shared by all files in scripts/internal.""" - -from __future__ import print_function -import sys - -from psutil._compat import lru_cache - - -__all__ = ['hilite', 'printerr', 'exit'] - - -@lru_cache() -def _term_supports_colors(file=sys.stdout): - try: - import curses - assert file.isatty() - curses.setupterm() - assert curses.tigetnum("colors") > 0 - except Exception: - return False - else: - return True - - -def hilite(s, ok=True, bold=False): - """Return an highlighted version of 'string'.""" - if not _term_supports_colors(): - return s - attr = [] - if ok is None: # no color - pass - elif ok: # green - attr.append('32') - else: # red - attr.append('31') - if bold: - attr.append('1') - return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) - - -def printerr(s): - print(hilite(s, ok=False), file=sys.stderr) - - -def exit(msg=""): - if msg: - printerr(msg) - sys.exit(1) diff --git a/scripts/internal/download_exes.py b/scripts/internal/win_download_wheels.py similarity index 95% rename from scripts/internal/download_exes.py rename to scripts/internal/win_download_wheels.py index 4a559bb0be..0cb37afe45 100755 --- a/scripts/internal/download_exes.py +++ b/scripts/internal/win_download_wheels.py @@ -19,10 +19,11 @@ import os import requests import shutil +import sys from psutil import __version__ as PSUTIL_VERSION from psutil._common import bytes2human -from scriptutils import printerr, exit +from psutil._common import print_color BASE_URL = 'https://ci.appveyor.com/api' @@ -81,7 +82,8 @@ def get_file_urls(options): file_url = job_url + '/' + item['fileName'] urls.append(file_url) if not urls: - exit("no artifacts found") + print_color("no artifacts found", 'ret') + sys.exit(1) else: for url in sorted(urls, key=lambda x: os.path.basename(x)): yield url @@ -111,7 +113,7 @@ def run(options): try: local_fname = fut.result() except Exception: - printerr("error while downloading %s" % (url)) + print_color("error while downloading %s" % (url), 'red') raise else: completed += 1 From 3424a1a6f8f91292eca6373ba0cd3fb5170c1648 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 18:17:37 +0100 Subject: [PATCH 0518/1714] point all shebangs to python 3 --- psutil/tests/__main__.py | 2 +- psutil/tests/runner.py | 2 +- psutil/tests/test_aix.py | 2 +- psutil/tests/test_bsd.py | 2 +- psutil/tests/test_connections.py | 2 +- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 2 +- psutil/tests/test_memory_leaks.py | 2 +- psutil/tests/test_misc.py | 2 +- psutil/tests/test_osx.py | 2 +- psutil/tests/test_posix.py | 2 +- psutil/tests/test_process.py | 2 +- psutil/tests/test_sunos.py | 2 +- psutil/tests/test_system.py | 2 +- psutil/tests/test_unicode.py | 2 +- psutil/tests/test_windows.py | 2 +- scripts/battery.py | 2 +- scripts/cpu_distribution.py | 2 +- scripts/disk_usage.py | 2 +- scripts/fans.py | 2 +- scripts/free.py | 2 +- scripts/ifconfig.py | 2 +- scripts/internal/bench_oneshot.py | 2 +- scripts/internal/bench_oneshot_2.py | 2 +- scripts/internal/check_broken_links.py | 4 ++-- scripts/internal/fix_flake8.py | 2 +- scripts/internal/generate_manifest.py | 2 +- scripts/internal/print_access_denied.py | 2 +- scripts/internal/print_announce.py | 2 +- scripts/internal/print_api_speed.py | 2 +- scripts/internal/print_timeline.py | 2 +- scripts/internal/purge_installation.py | 2 +- scripts/internal/win_download_wheels.py | 2 +- scripts/internal/winmake.py | 2 +- scripts/iotop.py | 2 +- scripts/killall.py | 2 +- scripts/meminfo.py | 2 +- scripts/netstat.py | 2 +- scripts/nettop.py | 2 +- scripts/pidof.py | 2 +- scripts/pmap.py | 2 +- scripts/procinfo.py | 2 +- scripts/procsmem.py | 2 +- scripts/ps.py | 2 +- scripts/pstree.py | 2 +- scripts/sensors.py | 2 +- scripts/temperatures.py | 2 +- scripts/top.py | 2 +- scripts/who.py | 2 +- scripts/winservices.py | 2 +- setup.py | 2 +- 51 files changed, 52 insertions(+), 52 deletions(-) mode change 100644 => 100755 psutil/tests/test_unicode.py mode change 100644 => 100755 scripts/internal/bench_oneshot_2.py mode change 100644 => 100755 scripts/internal/fix_flake8.py mode change 100644 => 100755 scripts/internal/print_access_denied.py mode change 100644 => 100755 scripts/internal/print_api_speed.py mode change 100644 => 100755 scripts/internal/print_timeline.py diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 68710d44f5..9dd0804cc6 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 4c3359ddba..589117b80e 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 1757e3e5d3..7171232e08 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Copyright (c) 2009, Giampaolo Rodola' # Copyright (c) 2017, Arnon Yaari diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index e525e66724..899875d076 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index c7fe199269..972ac9d58c 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 5e258b3015..01ebe14167 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 97946a0bbd..e51f8bd573 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 31a632a478..f9cad70fd3 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 616a365454..c20cd9413b 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 723b255eea..e4e77f9353 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index e405393b7c..83c1b22b1b 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index e5ff6e45cd..987bdf38bb 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index 94405d41be..e3beb625bf 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 0d3f43753e..c32e8a737a 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py old mode 100644 new mode 100755 index 81a28807c3..ac2d4f69e0 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 934ea8476e..f68885d0ca 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: UTF-8 -* # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. diff --git a/scripts/battery.py b/scripts/battery.py index abbad8785a..0da2b95889 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index 839d31dff8..08997797c3 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 1860401fda..901dbf8c22 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/fans.py b/scripts/fans.py index 7a0ccf91d5..179af6312c 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/free.py b/scripts/free.py index 82e962ffc9..000323c5dd 100755 --- a/scripts/free.py +++ b/scripts/free.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index ad62a44f79..cfd02f0daf 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 28ad4bac82..436bdd6b01 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py old mode 100644 new mode 100755 index becf930c8b..3867391b40 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 7313481864..1a07611613 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Copyright (c) 2009, Giampaolo Rodola', Himanshu Shekhar. # All rights reserved. Use of this source code is governed by a @@ -175,7 +175,7 @@ def get_urls(fname): return parse_c(fname) else: with open(fname, 'rt', errors='ignore') as f: - if f.readline().strip().startswith('#!/usr/bin/env python'): + if f.readline().strip().startswith('#!/usr/bin/env python3'): return parse_py(fname) return parse_generic(fname) diff --git a/scripts/internal/fix_flake8.py b/scripts/internal/fix_flake8.py old mode 100644 new mode 100755 index 5aa84db5c2..7cde608bba --- a/scripts/internal/fix_flake8.py +++ b/scripts/internal/fix_flake8.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index f6cf0eaa05..c0be6d99d9 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py old mode 100644 new mode 100755 index 9123ba6d05..81d192f0c7 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 7fbe74f3d6..9569c3674d 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py old mode 100644 new mode 100755 index 85d1cfc5bd..e39a1baa7b --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py old mode 100644 new mode 100755 index 9bf6e9d132..64608b26ba --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/purge_installation.py b/scripts/internal/purge_installation.py index d930171955..50c00463cf 100755 --- a/scripts/internal/purge_installation.py +++ b/scripts/internal/purge_installation.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/win_download_wheels.py b/scripts/internal/win_download_wheels.py index 0cb37afe45..3720dd96c0 100755 --- a/scripts/internal/win_download_wheels.py +++ b/scripts/internal/win_download_wheels.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index fe0a73dceb..aba3595cfb 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/iotop.py b/scripts/iotop.py index 6a5d3fa96e..c3afd07151 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/killall.py b/scripts/killall.py index f9cc920185..7bbcd75a88 100755 --- a/scripts/killall.py +++ b/scripts/killall.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/meminfo.py b/scripts/meminfo.py index 0b15ff2b75..550fcd0128 100755 --- a/scripts/meminfo.py +++ b/scripts/meminfo.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/netstat.py b/scripts/netstat.py index af56d03d37..fe3bfe402c 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/nettop.py b/scripts/nettop.py index 45b8879fac..ce647c9dd3 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # $Id: iotop.py 1160 2011-10-14 18:50:36Z g.rodola@gmail.com $ # diff --git a/scripts/pidof.py b/scripts/pidof.py index c1042f4f8e..ee18aae4c5 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Copyright (c) 2009, Giampaolo Rodola', karthikrev. All rights reserved. # Use of this source code is governed by a BSD-style license that can be diff --git a/scripts/pmap.py b/scripts/pmap.py index 300f23e9d2..5f7246a159 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 161b50575f..01974513f0 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 4f8b4c84b6..259d79d42f 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/ps.py b/scripts/ps.py index 8467cca6f3..540c032a7f 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/pstree.py b/scripts/pstree.py index 8e4c9f9572..0005e4a181 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/sensors.py b/scripts/sensors.py index bbf3ac9088..726af3d2c5 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. diff --git a/scripts/temperatures.py b/scripts/temperatures.py index 15b9156b88..e83df44036 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. diff --git a/scripts/top.py b/scripts/top.py index 69890e27ce..0b17471d55 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/who.py b/scripts/who.py index 748d936c94..c2299eb09e 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/scripts/winservices.py b/scripts/winservices.py index 677248359a..8792f752e4 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 diff --git a/setup.py b/setup.py index 76c4ddbaaa..6504d248f8 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/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 From 766541fe9501373eea1ea2e77b865be9ed572cc7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Feb 2020 18:35:56 +0100 Subject: [PATCH 0519/1714] get rid of pip_install() code for py2; move everything in runner.py --- .ci/travis/run.sh | 4 +- Makefile | 4 +- make.bat | 2 +- psutil/tests/README.rst | 3 +- psutil/tests/__main__.py | 83 +------------------------------------ psutil/tests/runner.py | 15 +++++++ scripts/internal/winmake.py | 2 +- tox.ini | 2 +- 8 files changed, 24 insertions(+), 91 deletions(-) diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 8183863366..9a4afa62f5 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -20,9 +20,9 @@ python setup.py develop # run tests (with coverage) if [[ $PYVER == '2.7' ]] && [[ "$(uname -s)" != 'Darwin' ]]; then - PSUTIL_TESTING=1 python -Wa -m coverage run psutil/tests/__main__.py + PSUTIL_TESTING=1 python -Wa -m coverage run psutil/tests/runner.py else - PSUTIL_TESTING=1 python -Wa psutil/tests/__main__.py + PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py fi if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then diff --git a/Makefile b/Makefile index 812ba9c2c9..0dadfca3f2 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # You can set the variables below from the command line. PYTHON = python3 -TSCRIPT = psutil/tests/__main__.py +TSCRIPT = psutil/tests/runner.py ARGS = # List of nice-to-have dev libs. DEPS = \ @@ -155,7 +155,7 @@ test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSyste test-failed: ## Re-run tests which failed on last run ${MAKE} install - $(TEST_PREFIX) $(PYTHON) -c "import psutil.tests.runner as r; r.run(last_failed=True)" + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) --last-failed test-coverage: ## Run test coverage. ${MAKE} install diff --git a/make.bat b/make.bat index ae897aa5f6..8e60811ccf 100644 --- a/make.bat +++ b/make.bat @@ -28,7 +28,7 @@ if "%PYTHON%" == "" ( ) if "%TSCRIPT%" == "" ( - set TSCRIPT=psutil\tests\__main__.py + set TSCRIPT=psutil\tests\runner.py ) rem Needed to locate the .pypirc file and upload exes on PyPI. diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index b647d5138f..9b870d5b1b 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -4,13 +4,12 @@ Instructions for running tests * There are two ways of running tests. As a "user", if psutil is already installed and you just want to test it works:: - python -m psutil.tests --install-deps # install test deps python -m psutil.tests As a "developer", if you have a copy of the source code and you wish to hack on psutil:: - make setup-dev-env # install test deps (+ other things) + make setup-dev-env # install missing third-party deps make test * To run tests on all supported Python versions install tox diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 9dd0804cc6..d5cd02eb1c 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -6,89 +6,8 @@ """ 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() - - +from .runner import main main() diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 589117b80e..2e9264bd42 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -12,6 +12,7 @@ """ from __future__ import print_function +import optparse import os import sys import unittest @@ -145,3 +146,17 @@ def run(name=None, last_failed=False): save_failed_tests(result) success = result.wasSuccessful() sys.exit(0 if success else 1) + + +def main(): + usage = "python3 -m psutil.tests [opts]" + 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") + opts, args = parser.parse_args() + run(last_failed=opts.last_failed) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index aba3595cfb..f54211d93b 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -31,7 +31,7 @@ PYTHON = sys.executable else: PYTHON = os.getenv('PYTHON', sys.executable) -TEST_SCRIPT = 'psutil\\tests\\__main__.py' +TEST_SCRIPT = 'psutil\\tests\\runner.py' GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" PY3 = sys.version_info[0] == 3 HERE = os.path.abspath(os.path.dirname(__file__)) diff --git a/tox.ini b/tox.ini index 2698eb6a87..b148642ba8 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ deps = setenv = TOX = 1 -commands = python psutil/tests/__main__.py +commands = python psutil/tests/runner.py usedevelop = True From 776016fc8aaeb749762122a0866be3517a6e7a7f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Feb 2020 15:34:18 +0100 Subject: [PATCH 0520/1714] remove deprecation test: it fails intermittently because warnings uses a global state --- psutil/tests/test_contracts.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 01ebe14167..312f17d9a3 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -14,7 +14,6 @@ import stat import time import traceback -import warnings from psutil import AIX from psutil import BSD @@ -179,22 +178,6 @@ def test_memory_maps(self): 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 # =================================================================== From 3ed40970e4799448535cf436d3f51d6ed9ac550e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Feb 2020 22:58:21 +0100 Subject: [PATCH 0521/1714] #1053 fix syntax incompatible with py2.6 --- psutil/tests/test_posix.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 83c1b22b1b..a96b310ffe 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -58,8 +58,7 @@ def ps(fmt, pid=None): cmd.append('ax') if SUNOS: - fmt_map = {'command', 'comm', - 'start', 'stime'} + fmt_map = set(('command', 'comm', 'start', 'stime')) fmt = fmt_map.get(fmt, fmt) cmd.extend(['-o', fmt]) From 6c0762212309cba92715a23fd80652c6330fd60e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 16 Feb 2020 16:12:54 -0800 Subject: [PATCH 0522/1714] Remove use of deprecated setuptools test_suite & tests_require (#1696) Since setuptools v41.5.0 (27 Oct 2019), the 'test' command is formally deprecated and should not be used. Test are still easy to run through tox -- as before. --- setup.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/setup.py b/setup.py index 6504d248f8..7ead39f4f4 100755 --- a/setup.py +++ b/setup.py @@ -66,13 +66,6 @@ if POSIX: sources.append('psutil/_psutil_posix.c') -tests_require = [] -if sys.version_info[:2] <= (2, 6): - tests_require.append('unittest2') -if sys.version_info[:2] <= (2, 7): - tests_require.append('mock') -if sys.version_info[:2] <= (3, 2): - tests_require.append('ipaddress') extras_require = {} if sys.version_info[:2] <= (3, 3): @@ -393,8 +386,6 @@ def main(): if setuptools is not None: kwargs.update( python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", - test_suite="psutil.tests.get_suite", - tests_require=tests_require, extras_require=extras_require, zip_safe=False, ) From 793148fee9c46c3df8b7ca941d6c73d5c61bc3a8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2020 00:29:03 +0100 Subject: [PATCH 0523/1714] fix Makefile for freebsd --- Makefile | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 0dadfca3f2..4f4d1238c1 100644 --- a/Makefile +++ b/Makefile @@ -17,15 +17,13 @@ DEPS = \ twine \ virtualenv \ wheel - -ifeq ($(PYTHON), $(filter $(PYTHON), python python2 python2.7)) - DEPS += \ - futures \ - ipaddress \ - mock==1.0.1 \ - unittest2 -endif - +PY2_DEPS = \ + futures \ + ipaddress \ + mock==1.0.1 \ + unittest2 +DEPS += `$(PYTHON) -c \ + "import sys; print('$(PY2_DEPS)' if sys.version_info[0] == 2 else '')"` # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` From c9fc4fdadc5a19d97916cbf13b1b479834b94bd8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2020 01:43:16 +0100 Subject: [PATCH 0524/1714] revert #1667 process_iter() new_only param On a second thought I realized that process_iter() uses a global variable, so it's not thread safe. That means that if the are 2 threads using it, the first thread one calling the function (+ consume the iterator), will "steal" the processes of the second thread. psutil.cpu_percent() has the same problem. That means we have a problem can't solve with the current API and requires a lot of thinking on how to solve it as it's not obvious. --- HISTORY.rst | 1 - docs/index.rst | 13 +------------ psutil/__init__.py | 6 +----- psutil/tests/test_system.py | 16 ---------------- 4 files changed, 2 insertions(+), 34 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 605a564811..1634e4339d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,7 +12,6 @@ XXXX-XX-XX directory for additional data. (patch by Javad Karabi) - 1652_: [Windows] dropped support for Windows XP and Windows Server 2003. Minimum supported Windows version now is Windows Vista. -- 1667_: added process_iter(new_only=True) parameter. - 1671_: [FreeBSD] add CI testing/service for FreeBSD (Cirrus CI). - 1677_: [Windows] process exe() will succeed for all process PIDs (instead of raising AccessDenied). diff --git a/docs/index.rst b/docs/index.rst index 6771999226..c5de1802cb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -857,7 +857,7 @@ Functions .. versionchanged:: 5.6.0 PIDs are returned in sorted order -.. function:: process_iter(attrs=None, ad_value=None, new_only=False) +.. function:: process_iter(attrs=None, ad_value=None) Return an iterator yielding a :class:`Process` class instance for all running processes on the local machine. @@ -873,8 +873,6 @@ Functions ``info`` attribute attached to the returned :class:`Process` instances. If *attrs* is an empty list it will retrieve all process info (slow). - If *new_only* is true this function will take into consideration only - new PIDs which appeared since the last time it was was called. Sorting order in which processes are returned is based on their PID. Example:: @@ -898,18 +896,9 @@ Functions 3: {'name': 'ksoftirqd/0', 'username': 'root'}, ...} - Get only new processes since last call:: - - >>> for proc in psutil.process_iter(['pid', 'name'], new_only=True): - ... print(proc.info) - ... - .. versionchanged:: 5.3.0 added "attrs" and "ad_value" parameters. - .. versionchanged:: - 5.7.0 added "new_only" parameter. - .. function:: pid_exists(pid) Check whether the given PID exists in the current process list. This is diff --git a/psutil/__init__.py b/psutil/__init__.py index a58b452cb3..2f4e147fbf 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1407,7 +1407,7 @@ def pid_exists(pid): _lock = threading.Lock() -def process_iter(attrs=None, ad_value=None, new_only=False): +def process_iter(attrs=None, ad_value=None): """Return a generator yielding a Process instance for all running processes. @@ -1428,8 +1428,6 @@ def process_iter(attrs=None, ad_value=None, new_only=False): If *attrs* is an empty list it will retrieve all process info (slow). - If *new_only* is true this function will take into consideration - only new PIDs which appeared since the last time it was called. """ def add(pid): proc = Process(pid) @@ -1452,8 +1450,6 @@ def remove(pid): with _lock: ls = list(dict.fromkeys(new_pids).items()) - if not new_only: - ls += list(_pmap.items()) ls.sort() for pid, proc in ls: diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index c32e8a737a..3834209fcb 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -105,22 +105,6 @@ def test_prcess_iter_w_attrs(self): self.assertGreaterEqual(p.info['pid'], 0) assert m.called - def test_process_iter_new_only(self): - ls1 = list(psutil.process_iter(attrs=['pid'])) - ls2 = list(psutil.process_iter(attrs=['pid'], new_only=True)) - self.assertGreater(len(ls1), len(ls2)) - # assume no more than 3 new processes were created in the meantime - self.assertIn(len(ls2), [0, 1, 2, 3, 4, 5]) - - sproc = get_test_subprocess() - ls = list(psutil.process_iter(attrs=['pid'], new_only=True)) - self.assertIn(len(ls2), [0, 1, 2, 3, 4, 5]) - for p in ls: - if p.pid == sproc.pid: - break - else: - self.fail("subprocess not found") - @unittest.skipIf(PYPY and WINDOWS, "get_test_subprocess() unreliable on PYPY + WINDOWS") def test_wait_procs(self): From 41fcba5dd7ecbbc788814c722f31e630422ba28e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2020 02:39:13 +0100 Subject: [PATCH 0525/1714] revert process_iter() exactly how it was pre #1667 --- docs/DEVNOTES | 5 +++-- psutil/__init__.py | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/DEVNOTES b/docs/DEVNOTES index abd2e3688d..7fe14f7d0b 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -158,10 +158,11 @@ BUGFIXES - #600: windows / open_files(): support network file handles. -REJECTED -======== +REJECTED IDEAS +============== - #550: threads per core +- #1667: process_iter(new_only=True) INCONSISTENCIES =============== diff --git a/psutil/__init__.py b/psutil/__init__.py index 2f4e147fbf..22bb46f3f9 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1427,7 +1427,6 @@ def process_iter(attrs=None, ad_value=None): to returned Process instance. If *attrs* is an empty list it will retrieve all process info (slow). - """ def add(pid): proc = Process(pid) @@ -1449,8 +1448,8 @@ def remove(pid): remove(pid) with _lock: - ls = list(dict.fromkeys(new_pids).items()) - ls.sort() + ls = sorted(list(_pmap.items()) + + list(dict.fromkeys(new_pids).items())) for pid, proc in ls: try: From 5e47e0bcba7585909a11e9c7424e864fc26a7fd2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2020 18:44:43 +0100 Subject: [PATCH 0526/1714] Add C linter script (#1698) --- .ci/travis/run.sh | 2 +- HISTORY.rst | 9 ++-- MANIFEST.in | 1 + Makefile | 13 +++-- appveyor.yml | 2 +- docs/DEVGUIDE.rst | 2 +- docs/index.rst | 4 ++ psutil/_psutil_aix.c | 2 +- psutil/_psutil_posix.c | 4 +- psutil/_psutil_sunos.c | 2 +- psutil/arch/aix/common.c | 2 +- psutil/arch/aix/ifaddrs.c | 2 +- psutil/arch/aix/ifaddrs.h | 3 +- psutil/arch/aix/net_connections.h | 2 +- psutil/arch/aix/net_kernel_structs.h | 2 +- psutil/arch/freebsd/specific.c | 2 +- psutil/arch/solaris/environ.c | 4 +- psutil/arch/windows/net.c | 2 +- scripts/internal/.git-pre-commit | 16 +++++- scripts/internal/clinter.py | 76 ++++++++++++++++++++++++++++ scripts/internal/winmake.py | 4 +- setup.py | 1 - 22 files changed, 128 insertions(+), 29 deletions(-) create mode 100755 scripts/internal/clinter.py diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 9a4afa62f5..879e78a60c 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -30,7 +30,7 @@ if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then PSUTIL_TESTING=1 python -Wa psutil/tests/test_memory_leaks.py # run linter (on Linux only) if [[ "$(uname -s)" != 'Darwin' ]]; then - python -m flake8 + make lint PYTHON=python fi fi diff --git a/HISTORY.rst b/HISTORY.rst index 1634e4339d..9b39fa0517 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,13 +1,13 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.7.0 (unreleased) -================== +5.7.0 +===== -XXXX-XX-XX +2020-12-18 **Enhancements** -- 1637_: [SunOS] Add partial support for old SunOS 5.10 Update 0 to 3. +- 1637_: [SunOS] add partial support for old SunOS 5.10 Update 0 to 3. - 1648_: [Linux] sensors_temperatures() looks into an additional /sys/device/ directory for additional data. (patch by Javad Karabi) - 1652_: [Windows] dropped support for Windows XP and Windows Server 2003. @@ -16,6 +16,7 @@ XXXX-XX-XX - 1677_: [Windows] process exe() will succeed for all process PIDs (instead of raising AccessDenied). - 1679_: [Windows] net_connections() and Process.connections() are 10% faster. +- 1682_: [PyPy] added CI / test integration for PyPy via Travis. - 1686_: [Windows] added support for PyPy on Windows. - 1693_: [Windows] boot_time(), Process.create_time() and users()'s login time now have 1 micro second precision (before the precision was of 1 second). diff --git a/MANIFEST.in b/MANIFEST.in index 3b4232e9f8..801139b2c1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -115,6 +115,7 @@ include scripts/internal/README include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py +include scripts/internal/clinter.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/print_access_denied.py diff --git a/Makefile b/Makefile index 4f4d1238c1..b15aa5d957 100644 --- a/Makefile +++ b/Makefile @@ -169,10 +169,17 @@ test-coverage: ## Run test coverage. # Linters # =================================================================== -flake8: ## flake8 linter. - @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 +lint-py: ## Run Python (flake8) linter. + @git ls-files '*.py' | xargs $(PYTHON) -m flake8 -fix-flake8: ## Attempt to automaticall fix some flake8 issues. +lint-c: ## Run C linter. + @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py + +lint: ## Run Python (flake8) and C linters. + ${MAKE} lint-py + ${MAKE} lint-c + +fix-lint: ## Attempt to automatically fix some Python lint issues. @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 --exit-zero | $(PYTHON) scripts/internal/fix_flake8.py # =================================================================== diff --git a/appveyor.yml b/appveyor.yml index b38cbf1ba2..d22c1cb4f0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# Build: 2 (bump this up by 1 to force an appveyor run) +# Build: 3 (bump this up by 1 to force an appveyor run) os: Visual Studio 2015 diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 0ec64c3ce1..170a40a9d5 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -36,7 +36,7 @@ Some useful make commands: make test # run unit tests make test-memleaks # run memory leak tests make test-coverage # run test coverage - make flake8 # run PEP8 linter + make lint # run Python (PEP8) and C linters There are some differences between ``make`` on UNIX and Windows. For instance, to run a specific Python version. On UNIX: diff --git a/docs/index.rst b/docs/index.rst index c5de1802cb..c6b9a8cca1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2512,6 +2512,10 @@ Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. Timeline ======== +- 2020-02-18: + `5.7.0 `__ - + `what's new `__ - + `diff `__ - 2019-11-26: `5.6.7 `__ - `what's new `__ - diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index b8584f26df..cf79d307d9 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -15,7 +15,7 @@ * - psutil.Process.io_counters read count is always 0 * - psutil.Process.io_counters may not be available on older AIX versions * - psutil.Process.threads may not be available on older AIX versions - # - psutil.net_io_counters may not be available on older AIX versions + * - psutil.net_io_counters may not be available on older AIX versions * - reading basic process info may fail or return incorrect values when * process is starting (see IBM APAR IV58499 - fixed in newer AIX versions) * - sockets and pipes may not be counted in num_fds (fixed in newer AIX diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index fa554be9f1..38483b3b02 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -192,8 +192,8 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { // XXX we get here on FreeBSD when processing 'lo' / AF_INET6 // broadcast. Not sure what to do other than returning None. // ifconfig does not show anything BTW. - //PyErr_Format(PyExc_RuntimeError, gai_strerror(err)); - //return NULL; + // PyErr_Format(PyExc_RuntimeError, gai_strerror(err)); + // return NULL; Py_INCREF(Py_None); return Py_None; } diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 8aa7eadd39..6548640b70 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -496,7 +496,7 @@ proc_io_counters(PyObject* self, PyObject* args) { info.pr_inblk, info.pr_oublk); } - */ +*/ /* diff --git a/psutil/arch/aix/common.c b/psutil/arch/aix/common.c index 6115a15db5..945cbd978b 100644 --- a/psutil/arch/aix/common.c +++ b/psutil/arch/aix/common.c @@ -76,4 +76,4 @@ psutil_read_process_table(int * num) { *num = np; return processes; -} \ No newline at end of file +} diff --git a/psutil/arch/aix/ifaddrs.c b/psutil/arch/aix/ifaddrs.c index 1a819365ab..1480b60fab 100644 --- a/psutil/arch/aix/ifaddrs.c +++ b/psutil/arch/aix/ifaddrs.c @@ -146,4 +146,4 @@ int getifaddrs(struct ifaddrs **ifap) close(sd); freeifaddrs(*ifap); return (-1); -} \ No newline at end of file +} diff --git a/psutil/arch/aix/ifaddrs.h b/psutil/arch/aix/ifaddrs.h index 3920c1ccca..e15802bf7b 100644 --- a/psutil/arch/aix/ifaddrs.h +++ b/psutil/arch/aix/ifaddrs.h @@ -31,5 +31,4 @@ struct ifaddrs { extern int getifaddrs(struct ifaddrs **); extern void freeifaddrs(struct ifaddrs *); - -#endif \ No newline at end of file +#endif diff --git a/psutil/arch/aix/net_connections.h b/psutil/arch/aix/net_connections.h index 222bcaf354..d57ee42847 100644 --- a/psutil/arch/aix/net_connections.h +++ b/psutil/arch/aix/net_connections.h @@ -12,4 +12,4 @@ PyObject* psutil_net_connections(PyObject *self, PyObject *args); -#endif /* __NET_CONNECTIONS_H__ */ \ No newline at end of file +#endif /* __NET_CONNECTIONS_H__ */ diff --git a/psutil/arch/aix/net_kernel_structs.h b/psutil/arch/aix/net_kernel_structs.h index 4e7a088c14..7e22a1639a 100644 --- a/psutil/arch/aix/net_kernel_structs.h +++ b/psutil/arch/aix/net_kernel_structs.h @@ -108,4 +108,4 @@ struct mbuf64 #define m_len m_hdr.mh_len -#endif /* __64BIT__ */ \ No newline at end of file +#endif /* __64BIT__ */ diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 3d54b47e5d..3f37a08e2d 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -729,7 +729,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { // Return a list of tuples for every process memory maps. - //'procstat' cmdline utility has been used as an example. + // 'procstat' cmdline utility has been used as an example. pid_t pid; int ptrwidth; int i, cnt; diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/solaris/environ.c index eb0fa2abd3..482fe1fc16 100644 --- a/psutil/arch/solaris/environ.c +++ b/psutil/arch/solaris/environ.c @@ -11,8 +11,8 @@ #include #if !defined(_LP64) && _FILE_OFFSET_BITS == 64 -# undef _FILE_OFFSET_BITS -# undef _LARGEFILE64_SOURCE + #undef _FILE_OFFSET_BITS + #undef _LARGEFILE64_SOURCE #endif #include diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index f0572d5290..56c6b6f1f7 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -401,7 +401,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { } // is up? - if((pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_CONNECTED || + if ((pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_CONNECTED || pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_OPERATIONAL) && pIfRow->dwAdminStatus == 1 ) { py_is_up = Py_True; diff --git a/scripts/internal/.git-pre-commit b/scripts/internal/.git-pre-commit index 621879dfbe..e60092538d 100755 --- a/scripts/internal/.git-pre-commit +++ b/scripts/internal/.git-pre-commit @@ -77,6 +77,8 @@ def main(): out = sh("git diff --cached --name-only") py_files = [x for x in out.split('\n') if x.endswith('.py') and os.path.exists(x)] + c_files = [x for x in out.split('\n') if x.endswith(('.c', '.h')) and + os.path.exists(x)] lineno = 0 kw = {'encoding': 'utf8'} if sys.version_info[0] == 3 else {} @@ -100,7 +102,7 @@ def main(): print("%s:%s %s" % (path, lineno, line)) return exit("commit aborted: bare except clause") - # flake8 + # Python linter if py_files: try: import flake8 # NOQA @@ -108,12 +110,22 @@ def main(): return exit("commit aborted: flake8 is not installed; " "run 'make setup-dev-env'") - # XXX: we should scape spaces and possibly other amenities here + # XXX: we should escape spaces and possibly other amenities here ret = subprocess.call( "%s -m flake8 %s" % (sys.executable, " ".join(py_files)), shell=True) if ret != 0: return exit("commit aborted: python code is not flake8 compliant") + # C linter + if c_files: + # XXX: we should escape spaces and possibly other amenities here + cmd = "%s scripts/internal/clinter.py %s" % ( + sys.executable, " ".join(c_files)) + print(cmd) + ret = subprocess.call(cmd, shell=True) + if ret != 0: + return exit("commit aborted: C code didn't pass style check") + main() diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py new file mode 100755 index 0000000000..1d4ba9b14b --- /dev/null +++ b/scripts/internal/clinter.py @@ -0,0 +1,76 @@ +#!/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. + +"""A super simple linter to check C syntax.""" + +from __future__ import print_function +import argparse +import sys + + +warned = False + + +def warn(path, line, lineno, msg): + global warned + warned = True + print("%s:%s: %s" % (path, lineno, msg), file=sys.stderr) + + +def check_line(path, line, idx, lines): + s = line + lineno = idx + 1 + eof = lineno == len(lines) + if s.endswith(' \n'): + warn(path, line, lineno, "extra space at EOL") + elif '\t' in line: + warn(path, line, lineno, "line has a tab") + elif s.endswith('\r\n'): + warn(path, line, lineno, "Windows line ending") + # end of global block, e.g. "}newfunction...": + elif s == "}\n": + if not eof: + nextline = lines[idx + 1] + # "#" is a pre-processor line + if nextline != '\n' and \ + nextline.strip()[0] != '#' and \ + nextline.strip()[:2] != '*/': + warn(path, line, lineno, "expected 1 blank line") + + sls = s.lstrip() + if sls.startswith('//') and sls[2] != ' ' and line.strip() != '//': + warn(path, line, lineno, "no space after // comment") + + # e.g. "if(..." after keywords + keywords = ("if", "else", "while", "do", "enum", "for") + for kw in keywords: + if sls.startswith(kw + '('): + warn(path, line, lineno, "missing space between %r and '('" % kw) + # eof + if eof: + if not line.endswith('\n'): + warn(path, line, lineno, "no blank line at EOF") + + +def process(path): + with open(path, 'rt') as f: + lines = f.readlines() + for idx, line in enumerate(lines): + check_line(path, line, idx, lines) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('paths', nargs='+', help='path(s) to a file(s)') + args = parser.parse_args() + for path in args.paths: + process(path) + if warned: + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index f54211d93b..4d3fa3184b 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -386,7 +386,7 @@ def setup_dev_env(): sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEPS))) -def flake8(): +def lint(): """Run flake8 against all py files""" py_files = subprocess.check_output("git ls-files") if PY3: @@ -551,11 +551,11 @@ def main(): sp.add_parser('build', help="build") sp.add_parser('clean', help="deletes dev files") sp.add_parser('coverage', help="run coverage tests.") - sp.add_parser('flake8', help="run flake8 against all py files") sp.add_parser('help', help="print this help") sp.add_parser('install', help="build + install in develop/edit mode") sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") sp.add_parser('install-pip', help="install pip") + sp.add_parser('lint', help="run flake8 against all py files") sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") sp.add_parser('setup-dev-env', help="install deps") diff --git a/setup.py b/setup.py index 7ead39f4f4..2402a143c4 100755 --- a/setup.py +++ b/setup.py @@ -378,7 +378,6 @@ def main(): 'Topic :: System :: Networking :: Monitoring', 'Topic :: System :: Networking', 'Topic :: System :: Operating System', - 'Topic :: System :: Power (UPS)' 'Topic :: System :: Systems Administration', 'Topic :: Utilities', ], From f2e0c98ec0348810afe0e12347991438123c86f1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2020 19:01:02 +0100 Subject: [PATCH 0527/1714] fix KeyError --- psutil/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index 728d9c6258..17b6eeb3bf 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -790,7 +790,7 @@ def hilite(s, color="green", bold=False): try: color = colors[color] except KeyError: - raise ValueError("invalid color %r; choose between %r" % ( + raise ValueError("invalid color %r; choose between %s" % ( list(colors.keys()))) attr.append(color) if bold: From 567547fa3ba3f11ee4f2dc9e73d37a146fe49e1b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 21 Feb 2020 12:40:20 +0100 Subject: [PATCH 0528/1714] Git hook for renamed/added/deleted files + flake8 print() + tidelift (#1704) --- .cirrus.yml | 12 +-- .flake8 | 10 ++ MANIFEST.in | 4 +- Makefile | 17 ++-- docs/index.rst | 3 +- psutil/__init__.py | 5 +- psutil/_common.py | 8 +- psutil/_compat.py | 2 +- psutil/_psutil_common.c | 1 - psutil/arch/netbsd/socks.c | 13 +-- psutil/tests/__init__.py | 2 +- scripts/internal/.git-pre-commit | 131 --------------------------- scripts/internal/clinter.py | 11 ++- scripts/internal/git_pre_commit.py | 141 +++++++++++++++++++++++++++++ scripts/internal/tidelift.py | 40 ++++++++ scripts/internal/winmake.py | 3 +- 16 files changed, 230 insertions(+), 173 deletions(-) create mode 100644 .flake8 delete mode 100755 scripts/internal/.git-pre-commit create mode 100755 scripts/internal/git_pre_commit.py create mode 100644 scripts/internal/tidelift.py diff --git a/.cirrus.yml b/.cirrus.yml index 41c58a93d3..a0b8f1f0b3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,4 +1,4 @@ -freebsd_12_1_py3_task: +freebsd_13_py3_task: freebsd_instance: image: freebsd-12-1-release-amd64 env: @@ -11,10 +11,10 @@ freebsd_12_1_py3_task: - make install - make test - make test-memleaks - - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_access_denied.py - - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_api_speed.py + - make print-access-denied + - make print-api-speed -freebsd_12_1_py2_task: +freebsd_11_py2_task: freebsd_instance: image: freebsd-12-1-release-amd64 env: @@ -27,5 +27,5 @@ freebsd_12_1_py2_task: - make install - make test - make test-memleaks - - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_access_denied.py - - PSUTIL_TESTING=1 python3 -Wa scripts/internal/print_api_speed.py + - make print-access-denied + - make print-api-speed diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..6581552aac --- /dev/null +++ b/.flake8 @@ -0,0 +1,10 @@ +# Configuration file for flake 8. This is used by "make lint" and by the +# GIT commit hook script. +# T001 = print() statement + +[flake8] +per-file-ignores = + setup.py:T001 + scripts/*:T001 + psutil/tests/runner.py:T001 + psutil/tests/test_memory_leaks.py:T001 diff --git a/MANIFEST.in b/MANIFEST.in index 801139b2c1..380a4fa24d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include .cirrus.yml include .coveragerc +include .flake8 include .gitignore include CREDITS include HISTORY.rst @@ -110,7 +111,6 @@ include scripts/disk_usage.py include scripts/fans.py include scripts/free.py include scripts/ifconfig.py -include scripts/internal/.git-pre-commit include scripts/internal/README include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py @@ -118,11 +118,13 @@ include scripts/internal/check_broken_links.py include scripts/internal/clinter.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py +include scripts/internal/git_pre_commit.py include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py include scripts/internal/print_timeline.py include scripts/internal/purge_installation.py +include scripts/internal/tidelift.py include scripts/internal/win_download_wheels.py include scripts/internal/winmake.py include scripts/iotop.py diff --git a/Makefile b/Makefile index b15aa5d957..5c4d5b70da 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ DEPS = \ check-manifest \ coverage \ flake8 \ + flake8-print \ pyperf \ requests \ setuptools \ @@ -170,7 +171,7 @@ test-coverage: ## Run test coverage. # =================================================================== lint-py: ## Run Python (flake8) linter. - @git ls-files '*.py' | xargs $(PYTHON) -m flake8 + @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py @@ -186,18 +187,18 @@ fix-lint: ## Attempt to automatically fix some Python lint issues. # GIT # =================================================================== -git-tag-release: ## Git-tag a new release. - git tag -a release-`python -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` - git push --follow-tags - install-git-hooks: ## Install GIT pre-commit hook. - ln -sf ../../scripts/internal/.git-pre-commit .git/hooks/pre-commit + ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit chmod +x .git/hooks/pre-commit # =================================================================== # Distribution # =================================================================== +git-tag-release: ## Git-tag a new release. + git tag -a release-`python -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` + git push --follow-tags + sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest $(PYTHON) setup.py sdist @@ -225,6 +226,9 @@ check-sdist: ## Create source distribution and checks its sanity (MANIFEST) build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" +tidelift-relnotes: ## upload release notes from HISTORY + $(PYTHON) scripts/internal/tidelift.py + pre-release: ## Check if we're ready to produce a new release. ${MAKE} check-sdist ${MAKE} install @@ -245,6 +249,7 @@ release: ## Create a release (down/uploads tar.gz, wheels, git tag release). ${MAKE} pre-release $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release + ${MAKE} tidelift-relnotes check-manifest: ## Inspect MANIFEST.in file. $(PYTHON) -m check_manifest -v $(ARGS) diff --git a/docs/index.rst b/docs/index.rst index c6b9a8cca1..7233793f53 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2492,8 +2492,7 @@ Running tests Development guide ================= -If you want to hacking on psutil (e.g. want to add a new feature or fix a bug) -take a look at the `development guide`_. +If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= diff --git a/psutil/__init__.py b/psutil/__init__.py index 22bb46f3f9..18fb1f1342 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -224,7 +224,6 @@ "users", "boot_time", # others ] - __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" __version__ = "5.7.0" @@ -2348,7 +2347,7 @@ def test(): # pragma: no cover 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", + print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "NICE", # NOQA "STATUS", "START", "TIME", "CMDLINE")) for p in process_iter(attrs, ad_value=None): if p.info['create_time']: @@ -2398,7 +2397,7 @@ def test(): # pragma: no cover ctime, cputime, cmdline) - print(line[:get_terminal_size()[0]]) + print(line[:get_terminal_size()[0]]) # NOQA del memoize, memoize_when_activated, division, deprecated_method diff --git a/psutil/_common.py b/psutil/_common.py index 17b6eeb3bf..8a39de7f84 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -801,9 +801,9 @@ def hilite(s, color="green", bold=False): def print_color(s, color="green", bold=False, file=sys.stdout): """Print a colorized version of string.""" if not term_supports_colors(): - print(s, file=file) + print(s, file=file) # NOQA elif POSIX: - print(hilite(s, color, bold), file=file) + print(hilite(s, color, bold), file=file) # NOQA else: import ctypes @@ -827,7 +827,7 @@ def print_color(s, color="green", bold=False, file=sys.stdout): handle = GetStdHandle(handle_id) SetConsoleTextAttribute(handle, color) try: - print(s, file=file) + print(s, file=file) # NOQA finally: SetConsoleTextAttribute(handle, DEFAULT_COLOR) @@ -839,7 +839,7 @@ def debug(msg): """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" fname, lineno, func_name, lines, index = inspect.getframeinfo( inspect.currentframe().f_back) - print("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), + print("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), # NOQA file=sys.stderr) else: def debug(msg): diff --git a/psutil/_compat.py b/psutil/_compat.py index a9371382bd..2965fd1b5d 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -117,7 +117,7 @@ def FileExistsError(inst): pass except OSError: raise RuntimeError( - "broken / incompatible Python implementation, see: " + "broken or incompatible Python implementation, see: " "https://github.com/giampaolo/psutil/issues/1659") diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 07578edaa2..d63b4d9c51 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -391,7 +391,6 @@ double psutil_FiletimeToUnixTime(FILETIME ft) { return _to_unix_time((ULONGLONG)ft.dwHighDateTime, (ULONGLONG)ft.dwLowDateTime); - } diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index f370f09466..08b0b7e672 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -154,7 +154,7 @@ psutil_get_files(void) { // debug struct kif *k; SLIST_FOREACH(k, &kihead, kifs) { - printf("%d\n", k->kif->ki_pid); + printf("%d\n", k->kif->ki_pid); // NOQA } */ @@ -206,17 +206,6 @@ psutil_get_sockets(const char *name) { kpcb->kpcb = &kp[j]; SLIST_INSERT_HEAD(&kpcbhead, kpcb, kpcbs); } - - /* - // debug - struct kif *k; - struct kpcb *k; - SLIST_FOREACH(k, &kpcbhead, kpcbs) { - printf("ki_type: %d\n", k->kpcb->ki_type); - printf("ki_family: %d\n", k->kpcb->ki_family); - } - */ - return 0; } diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 3e4dc88066..cd78fae670 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -819,7 +819,7 @@ def retry_on_failure(retries=NO_RETRIES): actually failing. """ def logfun(exc): - print("%r, retrying" % exc, file=sys.stderr) + print("%r, retrying" % exc, file=sys.stderr) # NOQA return retry(exception=AssertionError, timeout=None, retries=retries, logfun=logfun) diff --git a/scripts/internal/.git-pre-commit b/scripts/internal/.git-pre-commit deleted file mode 100755 index e60092538d..0000000000 --- a/scripts/internal/.git-pre-commit +++ /dev/null @@ -1,131 +0,0 @@ -#!/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. - -""" -This gets executed on 'git commit' and rejects the commit in case the -submitted code does not pass validation. Validation is run only against -the *.py files which were modified in the commit. Checks: - -- assert no space at EOLs -- assert not pdb.set_trace in code -- assert no bare except clause ("except:") in code -- assert "flake8" returns no warnings - -Install this with "make install-git-hooks". -""" - -from __future__ import print_function -import os -import subprocess -import sys - - -def term_supports_colors(): - try: - import curses - assert sys.stderr.isatty() - curses.setupterm() - assert curses.tigetnum("colors") > 0 - except Exception: - return False - else: - return True - - -def hilite(s, ok=True, bold=False): - """Return an highlighted version of 'string'.""" - if not term_supports_colors(): - return s - attr = [] - if ok is None: # no color - pass - elif ok: # green - attr.append('32') - else: # red - attr.append('31') - if bold: - attr.append('1') - return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) - - -def exit(msg): - msg = hilite(msg, ok=False) - print(msg, file=sys.stderr) - sys.exit(1) - - -def sh(cmd): - """run cmd in a subprocess and return its output. - raises RuntimeError on error. - """ - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) - stdout, stderr = p.communicate() - if p.returncode != 0: - raise RuntimeError(stderr) - if stderr: - print(stderr, file=sys.stderr) - if stdout.endswith('\n'): - stdout = stdout[:-1] - return stdout - - -def main(): - out = sh("git diff --cached --name-only") - py_files = [x for x in out.split('\n') if x.endswith('.py') and - os.path.exists(x)] - c_files = [x for x in out.split('\n') if x.endswith(('.c', '.h')) and - os.path.exists(x)] - - lineno = 0 - kw = {'encoding': 'utf8'} if sys.version_info[0] == 3 else {} - for path in py_files: - with open(path, 'rt', **kw) as f: - for line in f: - lineno += 1 - # space at end of line - if line.endswith(' '): - print("%s:%s %r" % (path, lineno, line)) - return exit( - "commit aborted: space at end of line") - line = line.rstrip() - # pdb - if "pdb.set_trace" in line: - print("%s:%s %s" % (path, lineno, line)) - return exit( - "commit aborted: you forgot a pdb in your python code") - # bare except clause - if "except:" in line and not line.endswith("# NOQA"): - print("%s:%s %s" % (path, lineno, line)) - return exit("commit aborted: bare except clause") - - # Python linter - if py_files: - try: - import flake8 # NOQA - except ImportError: - return exit("commit aborted: flake8 is not installed; " - "run 'make setup-dev-env'") - - # XXX: we should escape spaces and possibly other amenities here - ret = subprocess.call( - "%s -m flake8 %s" % (sys.executable, " ".join(py_files)), - shell=True) - if ret != 0: - return exit("commit aborted: python code is not flake8 compliant") - - # C linter - if c_files: - # XXX: we should escape spaces and possibly other amenities here - cmd = "%s scripts/internal/clinter.py %s" % ( - sys.executable, " ".join(c_files)) - print(cmd) - ret = subprocess.call(cmd, shell=True) - if ret != 0: - return exit("commit aborted: C code didn't pass style check") - - -main() diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py index 1d4ba9b14b..fde1a3f2f5 100755 --- a/scripts/internal/clinter.py +++ b/scripts/internal/clinter.py @@ -43,16 +43,19 @@ def check_line(path, line, idx, lines): sls = s.lstrip() if sls.startswith('//') and sls[2] != ' ' and line.strip() != '//': warn(path, line, lineno, "no space after // comment") - # e.g. "if(..." after keywords keywords = ("if", "else", "while", "do", "enum", "for") for kw in keywords: if sls.startswith(kw + '('): warn(path, line, lineno, "missing space between %r and '('" % kw) # eof - if eof: - if not line.endswith('\n'): - warn(path, line, lineno, "no blank line at EOF") + if eof and not line.endswith('\n'): + warn(path, line, lineno, "no blank line at EOF") + + ss = s.strip() + if ss.startswith(("printf(", "printf (", )): + if not ss.endswith(("// NOQA", "// NOQA")): + warn(path, line, lineno, "printf() statement") def process(path): diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py new file mode 100755 index 0000000000..2ec4303d63 --- /dev/null +++ b/scripts/internal/git_pre_commit.py @@ -0,0 +1,141 @@ +#!/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. + +""" +This gets executed on 'git commit' and rejects the commit in case the +submitted code does not pass validation. Validation is run only against +the files which were modified in the commit. Checks: + +- assert no space at EOLs +- assert not pdb.set_trace in code +- assert no bare except clause ("except:") in code +- assert "flake8" checks pass +- assert C linter checks pass +- abort if files were added/renamed/removed and MANIFEST.in was not updated + +Install this with "make install-git-hooks". +""" + +from __future__ import print_function +import os +import subprocess +import sys + + +PYTHON = sys.executable +PY3 = sys.version_info[0] == 3 +THIS_SCRIPT = os.path.realpath(__file__) + + +def term_supports_colors(): + try: + import curses + assert sys.stderr.isatty() + curses.setupterm() + assert curses.tigetnum("colors") > 0 + except Exception: + return False + return True + + +def hilite(s, ok=True, bold=False): + """Return an highlighted version of 'string'.""" + if not term_supports_colors(): + return s + attr = [] + if ok is None: # no color + pass + elif ok: # green + attr.append('32') + else: # red + attr.append('31') + if bold: + attr.append('1') + return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) + + +def exit(msg): + print(hilite("commit aborted: " + msg, ok=False), file=sys.stderr) + sys.exit(1) + + +def sh(cmd): + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True) + stdout, stderr = p.communicate() + if p.returncode != 0: + raise RuntimeError(stderr) + if stderr: + print(stderr, file=sys.stderr) + if stdout.endswith('\n'): + stdout = stdout[:-1] + return stdout + + +def open_text(path): + kw = {'encoding': 'utf8'} if PY3 else {} + return open(path, 'rt', **kw) + + +def git_commit_files(): + out = sh("git diff --cached --name-only") + py_files = [x for x in out.split('\n') if x.endswith('.py') and + os.path.exists(x)] + c_files = [x for x in out.split('\n') if x.endswith(('.c', '.h')) and + os.path.exists(x)] + new_rm_mv = sh("git diff --name-only --diff-filter=ADR --cached") + # XXX: we should escape spaces and possibly other amenities here + new_rm_mv = new_rm_mv.split() + return (py_files, c_files, new_rm_mv) + + +def main(): + py_files, c_files, new_rm_mv = git_commit_files() + # Check file content. + for path in py_files: + if os.path.realpath(path) == THIS_SCRIPT: + continue + with open_text(path) as f: + lines = f.readlines() + for lineno, line in enumerate(lines, 1): + # space at end of line + if line.endswith(' '): + print("%s:%s %r" % (path, lineno, line)) + return exit("space at end of line") + line = line.rstrip() + # pdb + if "pdb.set_trace" in line: + print("%s:%s %s" % (path, lineno, line)) + return exit("you forgot a pdb in your python code") + # bare except clause + if "except:" in line and not line.endswith("# NOQA"): + print("%s:%s %s" % (path, lineno, line)) + return exit("bare except clause") + + # Python linter + if py_files: + assert os.path.exists('.flake8') + # XXX: we should escape spaces and possibly other amenities here + cmd = "%s -m flake8 --config=.flake8 %s" % (PYTHON, " ".join(py_files)) + ret = subprocess.call(cmd, shell=True) + if ret != 0: + return exit("python code is not flake8 compliant") + # C linter + if c_files: + # XXX: we should escape spaces and possibly other amenities here + cmd = "%s scripts/internal/clinter.py %s" % (PYTHON, " ".join(c_files)) + ret = subprocess.call(cmd, shell=True) + if ret != 0: + return exit("C code didn't pass style check") + if new_rm_mv: + out = sh("%s scripts/internal/generate_manifest.py" % PYTHON) + with open_text('MANIFEST.in') as f: + if out.strip() != f.read().strip(): + exit("some files were added, deleted or renamed; " + "run 'make generate-manifest' and commit again") + + +main() diff --git a/scripts/internal/tidelift.py b/scripts/internal/tidelift.py new file mode 100644 index 0000000000..fcba3e610f --- /dev/null +++ b/scripts/internal/tidelift.py @@ -0,0 +1,40 @@ +#!/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. + +""" +Update news entry of Tidelift with latest HISTORY.rst section. +Put your Tidelift API token in a file first: +~/.tidelift.token +""" + +from __future__ import print_function +import os +import requests +import psutil +from psutil.tests import import_module_by_path + + +def upload_relnotes(package, version, text, token): + url = "https://api.tidelift.com/external-api/" + \ + "lifting/pypi/%s/release-notes/%s" % (package, version) + res = requests.put( + url=url, + data=text.encode('utf8'), + headers={"Authorization": "Bearer: %s" % token}) + print(version, res.status_code, res.text) + res.raise_for_status() + + +def main(): + here = os.path.abspath(os.path.dirname(__file__)) + path = os.path.join(here, "print_announce.py") + get_changes = import_module_by_path(path).get_changes + with open(os.path.expanduser("~/.tidelift.token")) as f: + token = f.read().strip() + upload_relnotes('psutil', psutil.__version__, get_changes(), token) + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 4d3fa3184b..c9aa2952b9 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -490,7 +490,8 @@ def test_memleaks(): def install_git_hooks(): """Install GIT pre-commit hook.""" if os.path.isdir('.git'): - src = os.path.join(ROOT_DIR, "scripts", "internal", ".git-pre-commit") + src = os.path.join( + ROOT_DIR, "scripts", "internal", "git_pre_commit.py") dst = os.path.realpath( os.path.join(ROOT_DIR, ".git", "hooks", "pre-commit")) with open(src, "rt") as s: From 544e9daa4f66a9f80d7bf6c7886d693ee42f0a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 21 Feb 2020 12:45:52 +0100 Subject: [PATCH 0529/1714] Fix detecting empty result in TestSystemAPIs.test_users() (#1699) --- psutil/tests/test_posix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index a96b310ffe..d2c4bfb665 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -368,9 +368,9 @@ def test_nic_names(self): @retry_on_failure() def test_users(self): out = sh("who") - lines = out.split('\n') - if not lines: + 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())) From ea4887e758ee2845f8e238655aaadb56f504e128 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Wed, 4 Mar 2020 16:25:18 +0000 Subject: [PATCH 0530/1714] sensors_fans is not available on MacOS (#1710) --- docs/index.rst | 2 +- psutil/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 7233793f53..55e1586dd1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -751,7 +751,7 @@ Sensors See also `fans.py`_ and `sensors.py`_ for an example application. - Availability: Linux, macOS + Availability: Linux .. versionadded:: 5.2.0 diff --git a/psutil/__init__.py b/psutil/__init__.py index 18fb1f1342..46c4a20bd9 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2260,7 +2260,7 @@ def convert(n): __all__.append("sensors_temperatures") -# Linux, macOS +# Linux if hasattr(_psplatform, "sensors_fans"): def sensors_fans(): From 4d6a086411c77b7909cce8f4f141bbdecfc0d354 Mon Sep 17 00:00:00 2001 From: Andrey Babak Date: Sat, 14 Mar 2020 18:27:47 +0800 Subject: [PATCH 0531/1714] Fixup release date (#1716) --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9b39fa0517..0c99965bb5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.7.0 ===== -2020-12-18 +2020-02-18 **Enhancements** From f884ef43fd4547080735565b5cd7f46faa5e2191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 14 Apr 2020 14:14:55 +0200 Subject: [PATCH 0532/1714] Fix handling /proc/cpuinfo without tabs (#1726) /proc/cpuinfo uses spaces rather than tabs on ia64. Since there seems not to be any reason to require specific kind of whitespace before ':' on 'cpu mhz' line, just split on ':'. See: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/ia64/kernel/setup.c#n700 --- psutil/_pslinux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 9e32f25e7b..4fb783d14a 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -716,7 +716,7 @@ def cpu_freq(): with open_binary('%s/cpuinfo' % get_procfs_path()) as f: for line in f: if line.lower().startswith(b'cpu mhz'): - key, value = line.split(b'\t:', 1) + key, value = line.split(b':', 1) ret.append(_common.scpufreq(float(value), 0., 0.)) return ret From 6019ecb9d65cfb3bd0b6da6d8acc2340ff453655 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Apr 2020 14:17:00 +0200 Subject: [PATCH 0533/1714] update HISTORY/CREDITS --- CREDITS | 4 ++++ HISTORY.rst | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/CREDITS b/CREDITS index 716de944c2..690f5717fd 100644 --- a/CREDITS +++ b/CREDITS @@ -664,3 +664,7 @@ I: 1665 N: Anselm Kruis W: https://github.com/akruis I: 1695 + +N: Michał Górny +W: https://github.com/mgorny +I: 1726 diff --git a/HISTORY.rst b/HISTORY.rst index 9b39fa0517..4eed6b8e54 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.7.1 (unreleased) +================== + +XXXX-XX-XX + +**Bug fixes** + +- 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. + (patch by Michał Górny) + 5.7.0 ===== From 9e50354fa37da96040b9a23431c7cec9f6a5326d Mon Sep 17 00:00:00 2001 From: kaiix Date: Thu, 16 Apr 2020 22:59:43 +0800 Subject: [PATCH 0534/1714] Remove useless assignment (#1728) --- psutil/_pslinux.py | 1 - 1 file changed, 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4fb783d14a..aca5fd7d9e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -361,7 +361,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 From 97796454d5a14b38b1a036958ad3dfe35faa3b4a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Apr 2020 05:16:41 +0200 Subject: [PATCH 0535/1714] MemoryLeakTest class enhancements (#1731) --- CREDITS | 4 + HISTORY.rst | 10 + MANIFEST.in | 1 + Makefile | 4 + psutil/__init__.py | 2 +- psutil/_common.py | 4 +- psutil/_compat.py | 17 +- psutil/tests/__init__.py | 121 +++++++- psutil/tests/test_memory_leaks.py | 164 ++-------- psutil/tests/test_misc.py | 299 ------------------ psutil/tests/test_testutils.py | 400 +++++++++++++++++++++++++ scripts/internal/check_broken_links.py | 1 - scripts/sensors.py | 1 - 13 files changed, 586 insertions(+), 442 deletions(-) create mode 100755 psutil/tests/test_testutils.py diff --git a/CREDITS b/CREDITS index 716de944c2..690f5717fd 100644 --- a/CREDITS +++ b/CREDITS @@ -664,3 +664,7 @@ I: 1665 N: Anselm Kruis W: https://github.com/akruis I: 1695 + +N: Michał Górny +W: https://github.com/mgorny +I: 1726 diff --git a/HISTORY.rst b/HISTORY.rst index 0c99965bb5..b906066a31 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.7.1 (unreleased) +================== + +XXXX-XX-XX + +**Bug fixes** + +- 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. + (patch by Michał Górny) + 5.7.0 ===== diff --git a/MANIFEST.in b/MANIFEST.in index 380a4fa24d..d0d7524019 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -103,6 +103,7 @@ include psutil/tests/test_posix.py include psutil/tests/test_process.py include psutil/tests/test_sunos.py include psutil/tests/test_system.py +include psutil/tests/test_testutils.py include psutil/tests/test_unicode.py include psutil/tests/test_windows.py include scripts/battery.py diff --git a/Makefile b/Makefile index 5c4d5b70da..fbd7ffcc88 100644 --- a/Makefile +++ b/Makefile @@ -124,6 +124,10 @@ test-misc: ## Run miscellaneous tests. ${MAKE} install $(TEST_PREFIX) $(PYTHON) psutil/tests/test_misc.py +test-testutils: ## Run test utils tests. + ${MAKE} install + $(TEST_PREFIX) $(PYTHON) psutil/tests/test_testutils.py + test-unicode: ## Test APIs dealing with strings. ${MAKE} install $(TEST_PREFIX) $(PYTHON) psutil/tests/test_unicode.py diff --git a/psutil/__init__.py b/psutil/__init__.py index 46c4a20bd9..e6a2da8d6b 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -226,7 +226,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.7.0" +__version__ = "5.7.1" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_common.py b/psutil/_common.py index 8a39de7f84..b97bb01dcc 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -785,7 +785,7 @@ def hilite(s, color="green", bold=False): if not term_supports_colors(): return s attr = [] - colors = dict(green='32', red='91', brown='33') + colors = dict(green='32', red='91', brown='33', yellow='93') colors[None] = '29' try: color = colors[color] @@ -812,7 +812,7 @@ def print_color(s, color="green", bold=False, file=sys.stdout): SetConsoleTextAttribute = \ ctypes.windll.Kernel32.SetConsoleTextAttribute - colors = dict(green=2, red=4, brown=6) + colors = dict(green=2, red=4, brown=6, yellow=6) colors[None] = DEFAULT_COLOR try: color = colors[color] diff --git a/psutil/_compat.py b/psutil/_compat.py index 2965fd1b5d..64a5761eea 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -5,13 +5,14 @@ """Module which provides compatibility with older Python versions.""" import collections +import contextlib import errno import functools import os import sys __all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", - "lru_cache", "which", "get_terminal_size", + "lru_cache", "which", "get_terminal_size", "redirect_stderr", "FileNotFoundError", "PermissionError", "ProcessLookupError", "InterruptedError", "ChildProcessError", "FileExistsError"] @@ -343,3 +344,17 @@ def get_terminal_size(fallback=(80, 24)): return (res[1], res[0]) except Exception: return fallback + + +# python 3.4 +try: + from contextlib import redirect_stderr +except ImportError: + @contextlib.contextmanager + def redirect_stderr(target): + original = sys.stderr + try: + sys.stderr = target + yield + finally: + sys.stderr = original diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index cd78fae670..f335880670 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -9,12 +9,12 @@ """ from __future__ import print_function - import atexit import contextlib import ctypes import errno import functools +import gc import os import random import re @@ -40,7 +40,9 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil._common import bytes2human from psutil._common import supports_ipv6 +from psutil._common import print_color from psutil._compat import ChildProcessError from psutil._compat import FileExistsError from psutil._compat import FileNotFoundError @@ -48,6 +50,7 @@ from psutil._compat import u from psutil._compat import unicode from psutil._compat import which +from psutil._compat import xrange if sys.version_info < (2, 7): import unittest2 as unittest # requires "pip install unittest2" @@ -84,7 +87,7 @@ 'ThreadTask' # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_on_failure', + 'retry_on_failure', 'TestMemoryLeak', # install utils 'install_pip', 'install_test_deps', # fs utils @@ -814,6 +817,120 @@ def __str__(self): unittest.TestCase = TestCase +@unittest.skipIf(PYPY, "unreliable on PYPY") +class TestMemoryLeak(unittest.TestCase): + """Test framework class for detecting function memory leaks (typically + functions implemented in C). + It does so by calling a function many times, and checks whether the + process memory usage increased before and after having called the + function repeadetly. + Note that sometimes this may produce false positives. + PyPy appears to be completely unstable for this framework, probably + because of how its JIT handles memory, so tests on PYPY are + automatically skipped. + """ + # Configurable class attrs. + times = 1200 + warmup_times = 10 + tolerance = 4096 # memory + retry_for = 3.0 # seconds + verbose = True + + def setUp(self): + self._thisproc = psutil.Process() + gc.collect() + + 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 _call(self, fun): + return fun() + + def _itercall(self, fun, iterator): + """Get 2 distinct memory samples, before and after having + called fun repeadetly, and return the memory difference. + """ + ncalls = 0 + gc.collect() + mem1 = self._get_mem() + for x in iterator: + ret = self._call(fun) + ncalls += 1 + del x, ret + gc.collect() + mem2 = self._get_mem() + self.assertEqual(gc.garbage, []) + diff = mem2 - mem1 + if diff < 0: + self._log("negative memory diff -%s" % (bytes2human(abs(diff)))) + return (diff, ncalls) + + def _call_ntimes(self, fun, times): + return self._itercall(fun, xrange(times))[0] + + def _call_for(self, fun, secs): + def iterator(secs): + stop_at = time.time() + secs + while time.time() < stop_at: + yield + return self._itercall(fun, iterator(secs)) + + def _log(self, msg): + if self.verbose: + print_color(msg, color="yellow", file=sys.stderr) + + def execute(self, fun, times=times, warmup_times=warmup_times, + tolerance=tolerance, retry_for=retry_for): + """Test a callable.""" + if times <= 0: + raise ValueError("times must be > 0") + if warmup_times < 0: + raise ValueError("warmup_times must be >= 0") + if tolerance is not None and tolerance < 0: + raise ValueError("tolerance must be >= 0") + if retry_for is not None and retry_for < 0: + raise ValueError("retry_for must be >= 0") + + # warm up + self._call_ntimes(fun, warmup_times) + mem1 = self._call_ntimes(fun, times) + + if mem1 > 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 N more seconds and + # fail if we notice any difference (ignore tolerance). + msg = "+%s after %s calls; try calling fun for another %s secs" % ( + bytes2human(mem1), times, retry_for) + if not retry_for: + raise self.fail(msg) + else: + self._log(msg) + + mem2, ncalls = self._call_for(fun, retry_for) + if mem2 > mem1: + # failure + msg = "+%s memory increase after %s calls; " % ( + bytes2human(mem1), times) + msg += "+%s after another %s calls over %s secs" % ( + bytes2human(mem2), ncalls, retry_for) + raise self.fail(msg) + + 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 retry_on_failure(retries=NO_RETRIES): """Decorator which runs a test function and retries N times before actually failing. diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index f9cad70fd3..b0b4af1b5e 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -17,11 +17,7 @@ from __future__ import print_function import functools -import gc import os -import sys -import threading -import time import psutil import psutil._common @@ -32,9 +28,7 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS -from psutil._common import bytes2human from psutil._compat import ProcessLookupError -from psutil._compat import xrange from psutil.tests import CIRRUS from psutil.tests import create_sockets from psutil.tests import get_test_subprocess @@ -50,21 +44,15 @@ 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 PYPY 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 TestMemoryLeak from psutil.tests import TRAVIS from psutil.tests import unittest - -# configurable opts -LOOPS = 1000 -MEMORY_TOLERANCE = 4096 -RETRY_FOR = 3 SKIP_PYTHON_IMPL = True - cext = psutil._psplatform.cext thisproc = psutil.Process() @@ -79,107 +67,12 @@ def skip_if_linux(): "worthless on LINUX (pure python)") -@unittest.skipIf(PYPY, "unreliable on PYPY") -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): +class TestProcessObjectLeaks(TestMemoryLeak): """Test leaks of Process class methods.""" proc = thisproc @@ -232,7 +125,7 @@ def test_nice_get(self): def test_nice_set(self): niceness = thisproc.nice() - self.execute(self.proc.nice, niceness) + self.execute(lambda: self.proc.nice(niceness)) @unittest.skipIf(not HAS_IONICE, "not supported") def test_ionice_get(self): @@ -242,9 +135,9 @@ def test_ionice_get(self): def test_ionice_set(self): if WINDOWS: value = thisproc.ionice() - self.execute(self.proc.ionice, value) + self.execute(lambda: self.proc.ionice(value)) else: - self.execute(self.proc.ionice, psutil.IOPRIO_CLASS_NONE) + 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) @@ -322,9 +215,10 @@ def test_cpu_affinity_get(self): @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) + self.execute(lambda: self.proc.cpu_affinity(affinity)) if not TRAVIS: - self.execute_w_exc(ValueError, self.proc.cpu_affinity, [-1]) + self.execute_w_exc( + ValueError, lambda: self.proc.cpu_affinity([-1])) @skip_if_linux() def test_open_files(self): @@ -340,14 +234,14 @@ def test_memory_maps(self): @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) + 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(self.proc.rlimit, psutil.RLIMIT_NOFILE, limit) - self.execute_w_exc(OSError, self.proc.rlimit, -1) + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) + self.execute_w_exc(OSError, lambda: self.proc.rlimit(-1)) @skip_if_linux() # Windows implementation is based on a single system-wide @@ -359,7 +253,7 @@ def test_connections(self): # be executed. with create_sockets(): kind = 'inet' if SUNOS else 'all' - self.execute(self.proc.connections, kind) + self.execute(lambda: self.proc.connections(kind)) @unittest.skipIf(not HAS_ENVIRON, "not supported") def test_environ(self): @@ -371,13 +265,13 @@ def test_proc_info(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcessDualImplementation(TestMemLeak): +class TestProcessDualImplementation(TestMemoryLeak): def test_cmdline_peb_true(self): - self.execute(cext.proc_cmdline, os.getpid(), use_peb=True) + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True)) def test_cmdline_peb_false(self): - self.execute(cext.proc_cmdline, os.getpid(), use_peb=False) + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=False)) class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @@ -400,9 +294,9 @@ def tearDownClass(cls): super(TestTerminatedProcessLeaks, cls).tearDownClass() reap_children() - def _call(self, fun, *args, **kwargs): + def _call(self, fun): try: - fun(*args, **kwargs) + fun() except psutil.NoSuchProcess: pass @@ -439,7 +333,7 @@ def call(): # =================================================================== -class TestModuleFunctionsLeaks(TestMemLeak): +class TestModuleFunctionsLeaks(TestMemoryLeak): """Test leaks of psutil module functions.""" def test_coverage(self): @@ -457,11 +351,11 @@ def test_coverage(self): @skip_if_linux() def test_cpu_count_logical(self): - self.execute(psutil.cpu_count, logical=True) + self.execute(lambda: psutil.cpu_count(logical=True)) @skip_if_linux() def test_cpu_count_physical(self): - self.execute(psutil.cpu_count, logical=False) + self.execute(lambda: psutil.cpu_count(logical=False)) @skip_if_linux() def test_cpu_times(self): @@ -469,7 +363,7 @@ def test_cpu_times(self): @skip_if_linux() def test_per_cpu_times(self): - self.execute(psutil.cpu_times, percpu=True) + self.execute(lambda: psutil.cpu_times(percpu=True)) @skip_if_linux() def test_cpu_stats(self): @@ -497,14 +391,14 @@ def test_swap_memory(self): @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, "worthless on POSIX (pure python)") def test_pid_exists(self): - self.execute(psutil.pid_exists, os.getpid()) + self.execute(lambda: 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, '.') + self.execute(lambda: psutil.disk_usage('.')) def test_disk_partitions(self): self.execute(psutil.disk_partitions) @@ -513,7 +407,7 @@ def test_disk_partitions(self): '/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) + self.execute(lambda: psutil.disk_io_counters(nowrap=False)) # --- proc @@ -528,7 +422,7 @@ def test_pids(self): @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) + self.execute(lambda: psutil.net_io_counters(nowrap=False)) @skip_if_linux() @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") @@ -539,7 +433,7 @@ def test_net_connections(self): 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) + tolerance=80 * 1024 if WINDOWS else 4096) @unittest.skipIf(TRAVIS, "EPERM on travis") def test_net_if_stats(self): @@ -584,15 +478,15 @@ def test_win_service_get(self): def test_win_service_get_config(self): name = next(psutil.win_service_iter()).name() - self.execute(cext.winservice_query_config, 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(cext.winservice_query_status, 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(cext.winservice_query_descr, name) + self.execute(lambda: cext.winservice_query_descr(name)) if __name__ == '__main__': diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index c20cd9413b..ca0a043363 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -11,7 +11,6 @@ import ast import collections -import contextlib import errno import json import os @@ -19,58 +18,33 @@ import socket import stat -from psutil import FREEBSD from psutil import LINUX -from psutil import NETBSD 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 CI_TESTING -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 @@ -787,279 +761,6 @@ 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") - @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() - 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__) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py new file mode 100755 index 0000000000..7d3d6675c7 --- /dev/null +++ b/psutil/tests/test_testutils.py @@ -0,0 +1,400 @@ +#!/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 io +import os +import socket +import stat + +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._compat import PY3 +from psutil._compat import redirect_stderr +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 get_free_port +from psutil.tests import get_test_subprocess +from psutil.tests import HAS_CONNECTIONS_UNIX +from psutil.tests import is_namedtuple +from psutil.tests import mock +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 tcp_socketpair +from psutil.tests import TESTFN +from psutil.tests import TestMemoryLeak +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 + +# =================================================================== +# --- 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") + @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() + 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 TestMemLeakClass(TestMemoryLeak): + + def test_times(self): + def fun(): + cnt['cnt'] += 1 + cnt = {'cnt': 0} + self.execute(fun, times=1, warmup_times=10) + self.assertEqual(cnt['cnt'], 11) + self.execute(fun, times=10, warmup_times=10) + self.assertEqual(cnt['cnt'], 31) + + def test_warmup_times(self): + def fun(): + cnt['cnt'] += 1 + cnt = {'cnt': 0} + self.execute(fun, times=1, warmup_times=10) + self.assertEqual(cnt['cnt'], 11) + + 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, retry_for=-1) + + def test_leak(self): + def fun(): + ls.append("x" * 24 * 1024) + ls = [] + times = 100 + self.assertRaises(AssertionError, self.execute, fun, times=times, + warmup_times=10, retry_for=None) + self.assertEqual(len(ls), times + 10) + + @retry_on_failure(retries=20) # 2 secs + def test_leak_with_retry(self, ls=[]): + def fun(): + ls.append("x" * 24 * 1024) + times = 100 + f = io.StringIO() if PY3 else io.BytesIO() + with redirect_stderr(f): + self.assertRaises(AssertionError, self.execute, fun, times=times, + retry_for=0.1) + self.assertIn("try calling fun for another", f.getvalue()) + self.assertGreater(len(ls), times) + + 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) + + def test_execute_w_exc(self): + def fun(): + 1 / 0 + self.execute_w_exc(ZeroDivisionError, fun, times=2000, warmup_times=20, + tolerance=4096, retry_for=3) + + with self.assertRaises(ZeroDivisionError): + self.execute_w_exc(OSError, fun) + + def fun(): + pass + with self.assertRaises(AssertionError): + self.execute_w_exc(ZeroDivisionError, fun) + + +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__) diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 1a07611613..e66448fd1a 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -40,7 +40,6 @@ """ from __future__ import print_function - import concurrent.futures import functools import os diff --git a/scripts/sensors.py b/scripts/sensors.py index 726af3d2c5..e532bebad1 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -30,7 +30,6 @@ """ from __future__ import print_function - import psutil From 10d6950d9cd4c1fcc4b913c768a51f3a53bbac21 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Apr 2020 10:35:13 +0200 Subject: [PATCH 0536/1714] skip scripts test if can't locate directory May happen on CI and when running tests via "python -m psutil.tests" since scripts dir is not installed. --- psutil/tests/test_misc.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index ca0a043363..74a0c4273e 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -42,7 +42,6 @@ from psutil.tests import ROOT_DIR from psutil.tests import SCRIPTS_DIR from psutil.tests import sh -from psutil.tests import TOX from psutil.tests import TRAVIS from psutil.tests import unittest import psutil @@ -624,9 +623,7 @@ def test_cache_clear_public_apis(self): # =================================================================== -@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), +@unittest.skipIf(not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory") class TestScripts(unittest.TestCase): """Tests for scripts in the "scripts" directory.""" From 6242f7411b882d525e5d267de4bcda1079934ea2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Apr 2020 03:57:09 -0700 Subject: [PATCH 0537/1714] Backport python 3 super() (#1733) --- psutil/_compat.py | 121 ++++++++++++++++++++++++------ psutil/_pslinux.py | 2 +- psutil/_pswindows.py | 4 +- psutil/tests/__init__.py | 6 +- psutil/tests/test_contracts.py | 3 +- psutil/tests/test_memory_leaks.py | 5 +- psutil/tests/test_misc.py | 5 +- 7 files changed, 112 insertions(+), 34 deletions(-) diff --git a/psutil/_compat.py b/psutil/_compat.py index 64a5761eea..145fb71d44 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -2,7 +2,10 @@ # 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 @@ -10,19 +13,33 @@ import functools import os import sys +import types + +__all__ = [ + # constants + "PY3", + # builtins + "long", "range", "super", "unicode", "basestring", + # literals + "u", "b", + # collections module + "lru_cache", "redirect_stderr", + # shutil module + "which", "get_terminal_size", + # python 3 exceptions + "FileNotFoundError", "PermissionError", "ProcessLookupError", + "InterruptedError", "ChildProcessError", "FileExistsError"] -__all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", - "lru_cache", "which", "get_terminal_size", "redirect_stderr", - "FileNotFoundError", "PermissionError", "ProcessLookupError", - "InterruptedError", "ChildProcessError", "FileExistsError"] 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 @@ -31,7 +48,7 @@ def b(s): return s.encode("latin-1") else: long = long - xrange = xrange + range = xrange unicode = unicode basestring = basestring @@ -42,6 +59,70 @@ 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): + raise RuntimeError('super() used in a function with no args') + 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: + raise RuntimeError('super() used in a non-newstyle class') + 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: + raise RuntimeError('super() called outside a method') + + # Dispatch to builtin super(). + if type_or_obj is not _SENTINEL: + return _builtin_super(type_, type_or_obj) + return _builtin_super(type_) + + # --- exceptions @@ -57,9 +138,7 @@ def b(s): # src/future/types/exceptions/pep3151.py import platform - _singleton = object() - - def instance_checking_exception(base_exception=Exception): + def _instance_checking_exception(base_exception=Exception): def wrapped(instance_checker): class TemporaryClass(base_exception): @@ -86,30 +165,30 @@ def __subclasscheck__(cls, classinfo): return wrapped - @instance_checking_exception(EnvironmentError) + @_instance_checking_exception(EnvironmentError) def FileNotFoundError(inst): - return getattr(inst, 'errno', _singleton) == errno.ENOENT + return getattr(inst, 'errno', _SENTINEL) == errno.ENOENT - @instance_checking_exception(EnvironmentError) + @_instance_checking_exception(EnvironmentError) def ProcessLookupError(inst): - return getattr(inst, 'errno', _singleton) == errno.ESRCH + return getattr(inst, 'errno', _SENTINEL) == errno.ESRCH - @instance_checking_exception(EnvironmentError) + @_instance_checking_exception(EnvironmentError) def PermissionError(inst): - return getattr(inst, 'errno', _singleton) in ( + return getattr(inst, 'errno', _SENTINEL) in ( errno.EACCES, errno.EPERM) - @instance_checking_exception(EnvironmentError) + @_instance_checking_exception(EnvironmentError) def InterruptedError(inst): - return getattr(inst, 'errno', _singleton) == errno.EINTR + return getattr(inst, 'errno', _SENTINEL) == errno.EINTR - @instance_checking_exception(EnvironmentError) + @_instance_checking_exception(EnvironmentError) def ChildProcessError(inst): - return getattr(inst, 'errno', _singleton) == errno.ECHILD + return getattr(inst, 'errno', _SENTINEL) == errno.ECHILD - @instance_checking_exception(EnvironmentError) + @_instance_checking_exception(EnvironmentError) def FileExistsError(inst): - return getattr(inst, 'errno', _singleton) == errno.EEXIST + return getattr(inst, 'errno', _SENTINEL) == errno.EEXIST if platform.python_implementation() != "CPython": try: diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index aca5fd7d9e..3e3caace78 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -846,7 +846,7 @@ def decode_address(addr, family): # 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))) + # ''.join(ip[i:i+4][::-1] for i in range(0, 16, 4))) ip = base64.b16decode(ip) try: # see: https://github.com/giampaolo/psutil/issues/201 diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 99d5d71499..c1707db762 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -30,8 +30,8 @@ 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 ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS from ._psutil_windows import HIGH_PRIORITY_CLASS @@ -1060,7 +1060,7 @@ 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) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f335880670..d7dd42b268 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -41,16 +41,16 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import bytes2human -from psutil._common import supports_ipv6 from psutil._common import print_color +from psutil._common import supports_ipv6 from psutil._compat import ChildProcessError from psutil._compat import FileExistsError from psutil._compat import FileNotFoundError from psutil._compat import PY3 +from psutil._compat import range from psutil._compat import u from psutil._compat import unicode from psutil._compat import which -from psutil._compat import xrange if sys.version_info < (2, 7): import unittest2 as unittest # requires "pip install unittest2" @@ -869,7 +869,7 @@ def _itercall(self, fun, iterator): return (diff, ncalls) def _call_ntimes(self, fun, times): - return self._itercall(fun, xrange(times))[0] + return self._itercall(fun, range(times))[0] def _call_for(self, fun, secs): def iterator(secs): diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 312f17d9a3..51ac3609ef 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -27,6 +27,7 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._compat import long +from psutil._compat import range from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import get_kernel_version @@ -620,7 +621,7 @@ def is_running(self, ret, proc): def cpu_affinity(self, ret, proc): self.assertIsInstance(ret, list) assert ret != [], ret - cpus = range(psutil.cpu_count()) + cpus = list(range(psutil.cpu_count())) for n in ret: self.assertIsInstance(n, int) self.assertIn(n, cpus) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index b0b4af1b5e..3d37f35ee4 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -29,6 +29,7 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._compat import ProcessLookupError +from psutil._compat import super from psutil.tests import CIRRUS from psutil.tests import create_sockets from psutil.tests import get_test_subprocess @@ -283,7 +284,7 @@ class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @classmethod def setUpClass(cls): - super(TestTerminatedProcessLeaks, cls).setUpClass() + super().setUpClass() p = get_test_subprocess() cls.proc = psutil.Process(p.pid) cls.proc.kill() @@ -291,7 +292,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - super(TestTerminatedProcessLeaks, cls).tearDownClass() + super().tearDownClass() reap_children() def _call(self, fun): diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index ca0a043363..74a0c4273e 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -42,7 +42,6 @@ from psutil.tests import ROOT_DIR from psutil.tests import SCRIPTS_DIR from psutil.tests import sh -from psutil.tests import TOX from psutil.tests import TRAVIS from psutil.tests import unittest import psutil @@ -624,9 +623,7 @@ def test_cache_clear_public_apis(self): # =================================================================== -@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), +@unittest.skipIf(not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory") class TestScripts(unittest.TestCase): """Tests for scripts in the "scripts" directory.""" From bc899c3f24581aa122e1b5b18ea6e694c2faddce Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Apr 2020 14:56:57 -0700 Subject: [PATCH 0538/1714] Get rid of TESTFN global variable (#1734) --- psutil/tests/__init__.py | 130 +++++++++++++----------------- psutil/tests/runner.py | 6 +- psutil/tests/test_connections.py | 117 +++++++++++++-------------- psutil/tests/test_contracts.py | 5 -- psutil/tests/test_linux.py | 75 +++++++++-------- psutil/tests/test_memory_leaks.py | 6 +- psutil/tests/test_process.py | 71 +++++++--------- psutil/tests/test_system.py | 17 ++-- psutil/tests/test_testutils.py | 116 +++++++++++++------------- psutil/tests/test_unicode.py | 112 ++++++++++++------------- 10 files changed, 300 insertions(+), 355 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index d7dd42b268..187552d5f3 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -73,9 +73,9 @@ __all__ = [ # constants 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'MEMORY_TOLERANCE', 'NO_RETRIES', - 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFILE_PREFIX', - 'TESTFN', 'TESTFN_UNICODE', 'TOX', 'TRAVIS', 'CIRRUS', 'CI_TESTING', - 'VALID_PROC_STATUSES', + 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', + 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TOX', 'TRAVIS', 'CIRRUS', + 'CI_TESTING', '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", @@ -92,15 +92,15 @@ 'install_pip', 'install_test_deps', # fs utils 'chdir', 'safe_rmpath', 'create_exe', 'decode_path', 'encode_path', - 'unique_filename', + 'get_testfn', # os 'get_winver', 'get_kernel_version', # sync primitives 'call_until', 'wait_for_pid', 'wait_for_file', # network 'check_net_address', - 'get_free_port', 'unix_socket_path', 'bind_socket', 'bind_unix_socket', - 'tcp_socketpair', 'unix_socketpair', 'create_sockets', + 'get_free_port', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', + 'unix_socketpair', 'create_sockets', # compat 'reload_module', 'import_module_by_path', # others @@ -136,21 +136,21 @@ NO_RETRIES *= 3 GLOBAL_TIMEOUT *= 3 -# --- files +# --- file names -TESTFILE_PREFIX = '$testfn' +# Disambiguate TESTFN for parallel testing. if os.name == 'java': # Jython disallows @ in module names - TESTFILE_PREFIX = '$psutil-test-' + TESTFN_PREFIX = '$psutil-%s-' % os.getpid() 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("-ƒőő") + 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 @@ -222,20 +222,15 @@ def attempt(exe): _testfiles_created = set() +# --- exit funs (first is executed last) + +atexit.register(DEVNULL.close) + + @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: + while _testfiles_created: + path = _testfiles_created.pop() try: safe_rmpath(path) except Exception: @@ -333,14 +328,15 @@ def get_test_subprocess(cmd=None, **kwds): CREATE_NO_WINDOW = 0x8000000 kwds.setdefault("creationflags", CREATE_NO_WINDOW) if cmd is None: - safe_rmpath(_TESTFN) + testfn = get_testfn() + safe_rmpath(testfn) pyline = "from time import sleep;" \ "open(r'%s', 'w').close();" \ - "sleep(60);" % _TESTFN + "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) + wait_for_file(testfn, delete=True, empty=True) else: sproc = subprocess.Popen(cmd, **kwds) _subprocesses_started.add(sproc) @@ -356,7 +352,8 @@ def create_proc_children_pair(): 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 + # must be relative on Windows + testfn = os.path.basename(get_testfn(dir=os.getcwd())) s = textwrap.dedent("""\ import subprocess, os, sys, time s = "import os, time;" @@ -366,7 +363,7 @@ def create_proc_children_pair(): s += "time.sleep(60);" p = subprocess.Popen([r'%s', '-c', s]) p.wait() - """ % (_TESTFN2, PYTHON_EXE)) + """ % (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. @@ -375,8 +372,8 @@ def create_proc_children_pair(): else: subp = pyrun(s) child1 = psutil.Process(subp.pid) - data = wait_for_file(_TESTFN2, delete=False, empty=False) - safe_rmpath(_TESTFN2) + data = wait_for_file(testfn, delete=False, empty=False) + safe_rmpath(testfn) child2_pid = int(data) _pids_started.add(child2_pid) child2 = psutil.Process(child2_pid) @@ -386,7 +383,7 @@ def create_proc_children_pair(): 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 + unix_file = get_testfn() src = textwrap.dedent("""\ import os, sys, time, socket, contextlib child_pid = os.fork() @@ -427,9 +424,7 @@ def pyrun(src, **kwds): """ 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) + with open(get_testfn(), 'wt') as f: f.write(src) f.flush() subp = get_test_subprocess([PYTHON_EXE, f.name], **kwds) @@ -772,8 +767,7 @@ def create_exe(outpath, c_code=None): } """) assert isinstance(c_code, str), c_code - with tempfile.NamedTemporaryFile( - suffix='.c', delete=False, mode='wt') as f: + with open(get_testfn(suffix='.c'), 'wt') as f: f.write(c_code) try: subprocess.check_call(["gcc", f.name, "-o", outpath]) @@ -787,8 +781,19 @@ def create_exe(outpath, c_code=None): os.chmod(outpath, st.st_mode | stat.S_IEXEC) -def unique_filename(prefix=TESTFILE_PREFIX, suffix=""): - return tempfile.mktemp(prefix=prefix, suffix=suffix) +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. + """ + timer = getattr(time, 'perf_counter', time.time) + while True: + prefix = "%s%.9f-" % (TESTFN_PREFIX, timer()) + name = tempfile.mktemp(prefix=prefix, suffix=suffix, dir=dir) + if not os.path.exists(name): # also include dirs + _testfiles_created.add(name) + return os.path.realpath(name) # needed for OSX # =================================================================== @@ -813,8 +818,9 @@ def __str__(self): assertRaisesRegex = unittest.TestCase.assertRaisesRegexp -# override default unittest.TestCase -unittest.TestCase = TestCase +# monkey patch default unittest.TestCase +if 'PSUTIL_TESTING' in os.environ: + unittest.TestCase = TestCase @unittest.skipIf(PYPY, "unreliable on PYPY") @@ -988,22 +994,6 @@ def get_free_port(host='127.0.0.1'): 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): @@ -1094,8 +1084,8 @@ def create_sockets(): 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__() + fname1 = get_testfn() + fname2 = get_testfn() s1, s2 = unix_socketpair(fname1) s3 = bind_unix_socket(fname2, type=socket.SOCK_DGRAM) # self.addCleanup(safe_rmpath, fname1) @@ -1106,10 +1096,6 @@ def create_sockets(): 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): @@ -1196,14 +1182,14 @@ def is_namedtuple(x): if POSIX: @contextlib.contextmanager - def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): + 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 = tempfile.mktemp(prefix=dst_prefix, suffix=ext) + 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()] @@ -1216,7 +1202,7 @@ def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): safe_rmpath(dst) else: @contextlib.contextmanager - def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): + 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. @@ -1225,7 +1211,7 @@ def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): from ctypes import wintypes from ctypes import WinError ext = ".dll" - dst = tempfile.mktemp(prefix=dst_prefix, suffix=ext) + 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 diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 2e9264bd42..7ffbc1cfaf 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -28,6 +28,7 @@ from psutil._common import hilite from psutil._common import print_color from psutil._common import term_supports_colors +from psutil.tests import APPVEYOR from psutil.tests import safe_rmpath from psutil.tests import TOX @@ -134,7 +135,10 @@ def save_failed_tests(result): def run(name=None, last_failed=False): setup_tests() - runner = ColouredRunner(verbosity=VERBOSITY) + if APPVEYOR: + runner = TextTestRunner(verbosity=VERBOSITY) + else: + runner = ColouredRunner(verbosity=VERBOSITY) suite = get_suite_from_failed() if last_failed else get_suite(name) try: result = runner.run(suite) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 972ac9d58c..5bd71dc8d4 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -10,6 +10,7 @@ import errno import os import socket +import string import textwrap from contextlib import closing from socket import AF_INET @@ -36,17 +37,15 @@ from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import get_free_port +from psutil.tests import get_testfn 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 SKIP_SYSCONS 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 @@ -58,14 +57,12 @@ class Base(object): def setUp(self): - safe_rmpath(TESTFN) if not (NETBSD or FREEBSD): # process 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 (FREEBSD or NETBSD): # Make sure we closed all resources. @@ -269,19 +266,17 @@ def test_udp_v6(self): @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) + with closing(bind_unix_socket(get_testfn(), 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) + with closing(bind_unix_socket(get_testfn(), type=SOCK_STREAM)) as sock: + conn = self.check_socket(sock) + assert not conn.raddr + self.assertEqual(conn.status, psutil.CONN_NONE) class TestConnectedSocket(Base, unittest.TestCase): @@ -313,39 +308,39 @@ def test_tcp(self): @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 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'] - if CIRRUS: - cons = [c for c in cons if c.fd in - (server.fileno(), client.fileno())] - self.assertEqual(len(cons), 2, msg=cons) - 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() + testfn = get_testfn() + server, client = unix_socketpair(testfn) + 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 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'] + if CIRRUS: + cons = [c for c in cons if c.fd in + (server.fileno(), client.fileno())] + self.assertEqual(len(cons), 2, msg=cons) + 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(testfn, 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, testfn) + self.assertEqual(cons[0].raddr or cons[1].raddr, testfn) + finally: + server.close() + client.close() class TestFilters(Base, unittest.TestCase): @@ -435,15 +430,15 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): time.sleep(60) """) - from string import Template - testfile = os.path.basename(TESTFN) - tcp4_template = Template(tcp_template).substitute( + # must be relative on Windows + testfile = os.path.basename(get_testfn(dir=os.getcwd())) + tcp4_template = string.Template(tcp_template).substitute( family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - udp4_template = Template(udp_template).substitute( + udp4_template = string.Template(udp_template).substitute( family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - tcp6_template = Template(tcp_template).substitute( + tcp6_template = string.Template(tcp_template).substitute( family=int(AF_INET6), addr="::1", testfn=testfile) - udp6_template = Template(udp_template).substitute( + udp6_template = string.Template(udp_template).substitute( family=int(AF_INET6), addr="::1", testfn=testfile) # launch various subprocess instantiating a socket of various @@ -583,23 +578,25 @@ def test_multi_sockets_procs(self): expected = len(socks) pids = [] times = 10 + fnames = [] for i in range(times): - fname = os.path.realpath(TESTFN) + str(i) + fname = get_testfn() + fnames.append(fname) src = textwrap.dedent("""\ import time, os - from psutil.tests import create_sockets + from psutil.tests import create_sockets, cleanup_test_files with create_sockets(): with open(r'%s', 'w') as f: - f.write(str(os.getpid())) + f.write("hello") + # 2 UNIX test socket files are created + cleanup_test_files() 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) + for fname in fnames: wait_for_file(fname) syscons = [x for x in psutil.net_connections(kind='all') if x.pid diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 51ac3609ef..74c429be52 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -37,9 +37,7 @@ 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_SYSCONS -from psutil.tests import TESTFN from psutil.tests import unittest from psutil.tests import VALID_PROC_STATUSES from psutil.tests import warn @@ -194,9 +192,6 @@ class TestSystemAPITypes(unittest.TestCase): def setUpClass(cls): cls.proc = psutil.Process() - def tearDown(self): - safe_rmpath(TESTFN) - def assert_ntuple_of_nums(self, nt, type_=float, gezero=True): assert is_namedtuple(nt) for n in nt: diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index e51f8bd573..d529ae7c03 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -17,7 +17,6 @@ import shutil import socket import struct -import tempfile import textwrap import time import warnings @@ -29,6 +28,7 @@ from psutil._compat import PY3 from psutil._compat import u from psutil.tests import call_until +from psutil.tests import get_testfn from psutil.tests import HAS_BATTERY from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_GETLOADAVG @@ -43,7 +43,6 @@ 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 @@ -1218,7 +1217,8 @@ def test_boot_time(self): self.assertEqual(int(vmstat_value), int(psutil_value)) def test_no_procfs_on_import(self): - my_procfs = tempfile.mkdtemp() + my_procfs = 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') @@ -1346,7 +1346,8 @@ def test_users_mocked(self): assert m.called def test_procfs_path(self): - tdir = tempfile.mkdtemp() + tdir = get_testfn() + os.mkdir(tdir) try: psutil.PROCFS_PATH = tdir self.assertRaises(IOError, psutil.virtual_memory) @@ -1362,7 +1363,6 @@ def test_procfs_path(self): self.assertRaises(psutil.NoSuchProcess, psutil.Process) finally: psutil.PROCFS_PATH = "/proc" - os.rmdir(tdir) def test_issue_687(self): # In case of thread ID: @@ -1643,20 +1643,16 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") class TestProcess(unittest.TestCase): - def setUp(self): - safe_rmpath(TESTFN) - - tearDown = setUp - def test_memory_full_info(self): + testfn = get_testfn() src = textwrap.dedent(""" import time with open("%s", "w") as f: time.sleep(10) - """ % TESTFN) + """ % testfn) sproc = pyrun(src) self.addCleanup(reap_children) - call_until(lambda: os.listdir('.'), "'%s' not in ret" % TESTFN) + call_until(lambda: os.listdir('.'), "'%s' not in ret" % testfn) p = psutil.Process(sproc.pid) time.sleep(.1) mem = p.memory_full_info() @@ -1706,46 +1702,47 @@ def test_memory_full_info_mocked(self): # 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(): + def get_test_file(fname): p = psutil.Process() giveup_at = time.time() + 2 while True: for file in p.open_files(): - if file.path == os.path.abspath(TESTFN): + if file.path == os.path.abspath(fname): 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") + testfn = get_testfn() + with open(testfn, "w"): + self.assertEqual(get_test_file(testfn).mode, "w") + with open(testfn, "r"): + 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().mode, "r+") - with open(TESTFN, "w+"): - 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(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().mode, "w") - safe_rmpath(TESTFN) - with open(TESTFN, "x+"): - self.assertEqual(get_test_file().mode, "r+") + 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 tempfile.NamedTemporaryFile(): + with open(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', @@ -1766,7 +1763,7 @@ def test_open_files_fd_gone(self): # https://travis-ci.org/giampaolo/psutil/jobs/225694530 p = psutil.Process() files = p.open_files() - with tempfile.NamedTemporaryFile(): + with open(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' @@ -2104,13 +2101,13 @@ def test_readlink(self): assert m.called def test_cat(self): - fname = os.path.abspath(TESTFN) - with open(fname, "wt") as f: + testfn = get_testfn() + with open(testfn, "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, binary=False), "foo") + self.assertEqual(psutil._psplatform.cat(testfn, binary=True), b"foo") self.assertEqual( - psutil._psplatform.cat(TESTFN + '??', fallback="bar"), "bar") + psutil._psplatform.cat(testfn + '??', fallback="bar"), "bar") if __name__ == '__main__': diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 3d37f35ee4..5cfec577eb 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -33,6 +33,7 @@ from psutil.tests import CIRRUS from psutil.tests import create_sockets from psutil.tests import get_test_subprocess +from psutil.tests import get_testfn from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_ENVIRON @@ -46,9 +47,7 @@ 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 TestMemoryLeak from psutil.tests import TRAVIS from psutil.tests import unittest @@ -223,8 +222,7 @@ def test_cpu_affinity_set(self): @skip_if_linux() def test_open_files(self): - safe_rmpath(TESTFN) # needed after UNIX socket test has run - with open(TESTFN, 'w'): + with open(get_testfn(), 'w'): self.execute(self.proc.open_files) @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 987bdf38bb..ddb1bbbae0 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -15,7 +15,6 @@ import socket import subprocess import sys -import tempfile import textwrap import time import types @@ -44,6 +43,7 @@ from psutil.tests import create_zombie_proc from psutil.tests import enum from psutil.tests import get_test_subprocess +from psutil.tests import get_testfn from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_ENVIRON from psutil.tests import HAS_IONICE @@ -57,12 +57,9 @@ 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 @@ -76,9 +73,6 @@ class TestProcess(unittest.TestCase): """Tests for psutil.Process class.""" - def setUp(self): - safe_rmpath(TESTFN) - def tearDown(self): reap_children() @@ -328,7 +322,7 @@ def test_io_counters(self): # test writes io1 = p.io_counters() - with tempfile.TemporaryFile(prefix=TESTFILE_PREFIX) as f: + with open(get_testfn(), 'wb') as f: if PY3: f.write(bytes("x" * 1000000, 'ascii')) else: @@ -461,16 +455,17 @@ def test_rlimit_set(self): @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit(self): + testfn = get_testfn() p = psutil.Process() soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) try: p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) - with open(TESTFN, "wb") as f: + 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: + with open(testfn, "wb") as f: f.write(b"X" * 1025) self.assertEqual(exc.exception.errno if PY3 else exc.exception[0], errno.EFBIG) @@ -482,12 +477,13 @@ def test_rlimit(self): def test_rlimit_infinity(self): # First set a limit, then re-set it by specifying INFINITY # and assume we overridden the previous limit. + testfn = get_testfn() 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: + with open(testfn, "wb") as f: f.write(b"X" * 2048) finally: p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) @@ -728,9 +724,9 @@ def test_cmdline(self): @unittest.skipIf(PYPY, "broken on PYPY") def test_long_cmdline(self): - create_exe(TESTFN) - self.addCleanup(safe_rmpath, TESTFN) - cmdline = [TESTFN] + (["0123456789"] * 20) + testfn = get_testfn() + create_exe(testfn) + cmdline = [testfn] + (["0123456789"] * 20) sproc = get_test_subprocess(cmdline) p = psutil.Process(sproc.pid) self.assertEqual(p.cmdline(), cmdline) @@ -743,12 +739,11 @@ def test_name(self): @unittest.skipIf(PYPY, "unreliable on PYPY") def test_long_name(self): - long_name = TESTFN + ("0123456789" * 2) - create_exe(long_name) - self.addCleanup(safe_rmpath, long_name) - sproc = get_test_subprocess(long_name) + testfn = get_testfn(suffix="0123456789" * 2) + create_exe(testfn) + sproc = get_test_subprocess(testfn) p = psutil.Process(sproc.pid) - self.assertEqual(p.name(), os.path.basename(long_name)) + self.assertEqual(p.name(), os.path.basename(testfn)) # XXX @unittest.skipIf(SUNOS, "broken on SUNOS") @@ -758,19 +753,8 @@ 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 )' + funky_path = get_testfn(suffix='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", ""] @@ -960,35 +944,36 @@ def test_cpu_affinity_all_combinations(self): @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") def test_open_files(self): # current process + testfn = get_testfn() p = psutil.Process() files = p.open_files() - self.assertFalse(TESTFN in files) - with open(TESTFN, 'wb') as f: + 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)) filenames = [os.path.normcase(x.path) for x in files] - self.assertIn(os.path.normcase(TESTFN), filenames) + self.assertIn(os.path.normcase(testfn), filenames) if LINUX: for file in files: - if file.path == TESTFN: + 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 + 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 = [os.path.normcase(x.path) for x in p.open_files()] - if TESTFN in filenames: + if testfn in filenames: break time.sleep(.01) else: - self.assertIn(os.path.normcase(TESTFN), filenames) + self.assertIn(os.path.normcase(testfn), filenames) for file in filenames: assert os.path.isfile(file), file @@ -999,7 +984,8 @@ def test_open_files(self): def test_open_files_2(self): # test fd and path fields normcase = os.path.normcase - with open(TESTFN, 'w') as fileobj: + testfn = get_testfn() + with open(testfn, 'w') as fileobj: p = psutil.Process() for file in p.open_files(): if normcase(file.path) == normcase(fileobj.name) or \ @@ -1021,9 +1007,10 @@ def test_open_files_2(self): @unittest.skipIf(not POSIX, 'POSIX only') def test_num_fds(self): + testfn = get_testfn() p = psutil.Process() start = p.num_fds() - file = open(TESTFN, 'w') + file = open(testfn, 'w') self.addCleanup(file.close) self.assertEqual(p.num_fds(), start + 1) sock = socket.socket() @@ -1512,9 +1499,8 @@ def test_weird_environ(self): return execve("/bin/cat", argv, envp); } """) - path = TESTFN + path = get_testfn() create_exe(path, c_code=code) - self.addCleanup(safe_rmpath, path) sproc = get_test_subprocess([path], stdin=subprocess.PIPE, stderr=subprocess.PIPE) @@ -1559,7 +1545,6 @@ def test_(self): setattr(self, attr, types.MethodType(test_, self)) def setUp(self): - safe_rmpath(TESTFN) TestProcess.setUp(self) os.setegid(1000) os.seteuid(1000) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 3834209fcb..73401b3ca4 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -15,7 +15,6 @@ import signal import socket import sys -import tempfile import time import psutil @@ -37,6 +36,7 @@ from psutil.tests import DEVNULL from psutil.tests import enum from psutil.tests import get_test_subprocess +from psutil.tests import get_testfn from psutil.tests import HAS_BATTERY from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_GETLOADAVG @@ -48,10 +48,8 @@ from psutil.tests import PYPY 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 UNICODE_SUFFIX from psutil.tests import unittest @@ -62,9 +60,6 @@ class TestProcessAPIs(unittest.TestCase): - def setUp(self): - safe_rmpath(TESTFN) - def tearDown(self): reap_children() @@ -591,15 +586,15 @@ def test_disk_usage(self): # if path does not exist OSError ENOENT is expected across # all platforms - fname = tempfile.mktemp() + fname = 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 - if ASCII_FS: - with self.assertRaises(UnicodeEncodeError): - psutil.disk_usage(TESTFN_UNICODE) + with self.assertRaises(UnicodeEncodeError): + psutil.disk_usage(UNICODE_SUFFIX) def test_disk_usage_bytes(self): psutil.disk_usage(b'.') diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 7d3d6675c7..55c5d3df0c 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -34,6 +34,7 @@ from psutil.tests import create_zombie_proc from psutil.tests import get_free_port from psutil.tests import get_test_subprocess +from psutil.tests import get_testfn from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import is_namedtuple from psutil.tests import mock @@ -43,10 +44,8 @@ from psutil.tests import safe_mkdir from psutil.tests import safe_rmpath from psutil.tests import tcp_socketpair -from psutil.tests import TESTFN from psutil.tests import TestMemoryLeak 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 @@ -126,9 +125,6 @@ def test_retries_and_timeout_args(self, sleep): 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 @@ -136,26 +132,30 @@ def test_wait_for_pid(self): self.assertRaises(psutil.NoSuchProcess, wait_for_pid, nopid) def test_wait_for_file(self): - with open(TESTFN, 'w') as f: + testfn = get_testfn() + with open(testfn, 'w') as f: f.write('foo') - wait_for_file(TESTFN) - assert not os.path.exists(TESTFN) + wait_for_file(testfn) + assert not os.path.exists(testfn) def test_wait_for_file_empty(self): - with open(TESTFN, 'w'): + testfn = get_testfn() + with open(testfn, 'w'): pass - wait_for_file(TESTFN, empty=True) - assert not os.path.exists(TESTFN) + wait_for_file(testfn, empty=True) + assert not os.path.exists(testfn) def test_wait_for_file_no_file(self): + testfn = get_testfn() with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): - self.assertRaises(IOError, wait_for_file, TESTFN) + self.assertRaises(IOError, wait_for_file, testfn) def test_wait_for_file_no_delete(self): - with open(TESTFN, 'w') as f: + testfn = get_testfn() + with open(testfn, 'w') as f: f.write('foo') - wait_for_file(TESTFN, delete=False) - assert os.path.exists(TESTFN) + wait_for_file(testfn, delete=False) + assert os.path.exists(testfn) def test_call_until(self): ret = call_until(lambda: 1, "ret == 1") @@ -164,11 +164,6 @@ def test_call_until(self): 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') @@ -178,34 +173,37 @@ def test_open_binary(self): 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) + testfn = 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 - open(TESTFN, 'w').close() - safe_rmpath(TESTFN) - assert not os.path.exists(TESTFN) + testfn = 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) + safe_rmpath(testfn) # test dir is removed - os.mkdir(TESTFN) - safe_rmpath(TESTFN) - assert not os.path.exists(TESTFN) + 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) + safe_rmpath(testfn) assert m.called def test_chdir(self): + testfn = get_testfn() base = os.getcwd() - os.mkdir(TESTFN) - with chdir(TESTFN): - self.assertEqual(os.getcwd(), os.path.join(base, TESTFN)) + os.mkdir(testfn) + with chdir(testfn): + self.assertEqual(os.getcwd(), os.path.join(base, testfn)) self.assertEqual(os.getcwd(), base) @@ -256,19 +254,19 @@ def bind_socket(self): @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) + name = 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 - 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) + name = 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()) @@ -288,18 +286,18 @@ 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() + name = 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(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: diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index ac2d4f69e0..6fbaa43f87 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -86,27 +86,27 @@ from psutil._compat import PY3 from psutil._compat import u from psutil.tests import APPVEYOR -from psutil.tests import CIRRUS from psutil.tests import ASCII_FS from psutil.tests import bind_unix_socket from psutil.tests import chdir +from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe from psutil.tests import get_test_subprocess +from psutil.tests import get_testfn 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 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 TESTFN_PREFIX from psutil.tests import TRAVIS +from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest -from psutil.tests import unix_socket_path import psutil @@ -130,12 +130,13 @@ def safe_rmpath(path): return _safe_rmpath(path) -def subprocess_supports_unicode(name): +def subprocess_supports_unicode(suffix): """Return True if both the fs and the subprocess module can deal with a unicode file name. """ if PY3: return True + name = get_testfn(suffix=suffix) try: safe_rmpath(name) create_exe(name) @@ -148,38 +149,28 @@ def subprocess_supports_unicode(name): 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 + funky_suffix = None @classmethod def setUpClass(cls): - safe_rmpath(cls.funky_name) + cls.funky_name = get_testfn(suffix=cls.funky_suffix) 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) @@ -234,20 +225,20 @@ def test_proc_open_files(self): @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 and not CIRRUS: # XXX - self.assertEqual(conn.laddr, name) + name = get_testfn(suffix=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) + # AF_UNIX addr not set on OpenBSD + if not OPENBSD and not CIRRUS: # XXX + self.assertEqual(conn.laddr, name) @unittest.skipIf(not POSIX, "POSIX only") @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") @@ -255,26 +246,26 @@ def test_proc_connections(self): def test_net_connections(self): def find_sock(cons): for conn in cons: - if os.path.basename(conn.laddr).startswith(TESTFILE_PREFIX): + if os.path.basename(conn.laddr).startswith(TESTFN_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) + name = get_testfn(suffix=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') + # 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" @@ -289,13 +280,13 @@ def test_disk_usage(self): 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: + 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 TESTFILE_PREFIX in x] + 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) @@ -305,30 +296,29 @@ def normpath(p): @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), +@unittest.skipIf(not subprocess_supports_unicode(UNICODE_SUFFIX), "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 + funky_suffix = UNICODE_SUFFIX - @classmethod - def expect_exact_path_match(cls): + 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(cls.funky_name, str) else u('.') + here = '.' if isinstance(self.funky_name, str) else u('.') with warnings.catch_warnings(): warnings.simplefilter("ignore") - return cls.funky_name in os.listdir(here) + return self.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(PYPY, "unreliable on PYPY") -@unittest.skipIf(not subprocess_supports_unicode(INVALID_NAME), +@unittest.skipIf(not subprocess_supports_unicode(INVALID_UNICODE_SUFFIX), "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 + funky_suffix = INVALID_UNICODE_SUFFIX @classmethod def expect_exact_path_match(cls): @@ -356,7 +346,7 @@ def test_proc_environ(self): # we use "è", which is part of the extended ASCII table # (unicode point <= 255). env = os.environ.copy() - funky_str = TESTFN_UNICODE if PY3 else 'è' + funky_str = UNICODE_SUFFIX if PY3 else 'è' env['FUNNY_ARG'] = funky_str sproc = get_test_subprocess(env=env) p = psutil.Process(sproc.pid) From f4cce6862758cc4502553c05cc04bbc5055b2982 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Apr 2020 00:20:43 +0200 Subject: [PATCH 0539/1714] trick to execute atexit functions in case of SIGTERM --- psutil/tests/__init__.py | 55 +++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 187552d5f3..ec406a27f7 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -20,6 +20,7 @@ import re import select import shutil +import signal import socket import stat import subprocess @@ -222,27 +223,6 @@ def attempt(exe): _testfiles_created = set() -# --- exit funs (first is executed last) - -atexit.register(DEVNULL.close) - - -@atexit.register -def cleanup_test_files(): - while _testfiles_created: - path = _testfiles_created.pop() - try: - safe_rmpath(path) - except Exception: - traceback.print_exc() - - -# this is executed first -@atexit.register -def cleanup_test_procs(): - reap_children(recursive=True) - - # =================================================================== # --- threads # =================================================================== @@ -1238,3 +1218,36 @@ def copyload_shared_lib(suffix=""): if ret == 0: WinError() safe_rmpath(dst) + + +# =================================================================== +# --- Exit funs (first is executed last) +# =================================================================== + + +atexit.register(DEVNULL.close) + + +@atexit.register +def cleanup_test_files(): + while _testfiles_created: + path = _testfiles_created.pop() + try: + safe_rmpath(path) + except Exception: + traceback.print_exc() + + +# 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 +# modul. With this it will. See: +# http://grodola.blogspot.com/ +# 2016/02/how-to-always-execute-exit-functions-in-py.html +if POSIX and 'PSUTIL_TESTING' in os.environ: + signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit(sig)) From 420c9e82fd3321e3062424846870f3104c1b0eb8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Apr 2020 17:02:43 -0700 Subject: [PATCH 0540/1714] Parallel tests (UNIX) (#1709) --- .ci/travis/install.sh | 10 +- .ci/travis/run.sh | 2 +- .cirrus.yml | 8 +- .travis.yml | 1 - HISTORY.rst | 4 + Makefile | 7 +- docs/DEVGUIDE.rst | 13 +- psutil/tests/README.rst | 3 +- psutil/tests/__init__.py | 16 +- psutil/tests/runner.py | 298 +++++++++++++++++++++++------- psutil/tests/test_aix.py | 4 +- psutil/tests/test_bsd.py | 4 +- psutil/tests/test_connections.py | 20 +- psutil/tests/test_contracts.py | 4 +- psutil/tests/test_linux.py | 5 +- psutil/tests/test_memory_leaks.py | 4 +- psutil/tests/test_misc.py | 4 +- psutil/tests/test_osx.py | 4 +- psutil/tests/test_posix.py | 4 +- psutil/tests/test_process.py | 7 +- psutil/tests/test_sunos.py | 4 +- psutil/tests/test_system.py | 11 +- psutil/tests/test_testutils.py | 2 + psutil/tests/test_unicode.py | 6 +- psutil/tests/test_windows.py | 4 +- 25 files changed, 315 insertions(+), 134 deletions(-) diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index 1e37c39b5c..f06e43d575 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -16,10 +16,6 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then fi case "${PYVER}" in - # py26) - # pyenv install 2.6.9 - # pyenv virtualenv 2.6.9 psutil - # ;; py27) pyenv install 2.7.16 pyenv virtualenv 2.7.16 psutil @@ -33,10 +29,8 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then pyenv activate psutil fi -if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]] || [[ $PYVER == 'py26' ]]; then - pip install -U ipaddress unittest2 argparse mock==1.0.1 -elif [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] || [[ $PYVER == 'py27' ]]; then +if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] || [[ $PYVER == 'py27' ]]; then pip install -U ipaddress mock fi -pip install -U coverage coveralls flake8 setuptools +pip install -U coverage coveralls flake8 setuptools concurrencytest diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 879e78a60c..562564b04f 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -22,7 +22,7 @@ python setup.py develop if [[ $PYVER == '2.7' ]] && [[ "$(uname -s)" != 'Darwin' ]]; then PSUTIL_TESTING=1 python -Wa -m coverage run psutil/tests/runner.py else - PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py + PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py --parallel fi if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then diff --git a/.cirrus.yml b/.cirrus.yml index a0b8f1f0b3..129644c517 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -6,10 +6,10 @@ freebsd_13_py3_task: install_script: - pkg install -y python3 gcc py37-pip script: - - python3 -m pip install --user setuptools + - python3 -m pip install --user setuptools concurrencytest - make clean - make install - - make test + - make test-parallel - make test-memleaks - make print-access-denied - make print-api-speed @@ -22,10 +22,10 @@ freebsd_11_py2_task: install_script: - pkg install -y python gcc py27-pip script: - - python2.7 -m pip install --user setuptools ipaddress mock + - python2.7 -m pip install --user setuptools ipaddress mock concurrencytest - make clean - make install - - make test + - make test-parallel - make test-memleaks - make print-access-denied - make print-api-speed diff --git a/.travis.yml b/.travis.yml index fad35121b7..d49f2cf1f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ matrix: env: PYVER=py36 # Linux - python: 2.7 - - python: 3.4 - python: 3.5 - python: 3.6 - python: 3.7 diff --git a/HISTORY.rst b/HISTORY.rst index b906066a31..d68e68fcfe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,10 @@ XXXX-XX-XX +**Enhancements** + +- 1729_: parallel tests on UNIX (make test-parallel). + **Bug fixes** - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. diff --git a/Makefile b/Makefile index fbd7ffcc88..76420bc72a 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ ARGS = DEPS = \ argparse \ check-manifest \ + concurrencytest \ coverage \ flake8 \ flake8-print \ @@ -112,6 +113,10 @@ test: ## Run all tests. ${MAKE} install $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) +test-parallel: ## Run all tests in parallel. + ${MAKE} install + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) --parallel + test-process: ## Run process-related API tests. ${MAKE} install $(TEST_PREFIX) $(PYTHON) psutil/tests/test_process.py @@ -154,7 +159,7 @@ test-memleaks: ## Memory leak tests. test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSystemAPIs ${MAKE} install - @$(TEST_PREFIX) $(PYTHON) -m unittest -v $(ARGS) + @$(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) test-failed: ## Re-run tests which failed on last run ${MAKE} install diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 170a40a9d5..2e8272f067 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -31,12 +31,13 @@ Some useful make commands: .. code-block:: bash - make install # install - make setup-dev-env # install useful dev libs (flake8, unittest2, etc.) - make test # run unit tests - make test-memleaks # run memory leak tests - make test-coverage # run test coverage - make lint # run Python (PEP8) and C linters + make install + make setup-dev-env # install useful dev libs (flake8, unittest2, etc.) + make test # run unit tests + make test-parallel # faster + make test-memleaks + make test-coverage + make lint # Python (PEP8) and C linters There are some differences between ``make`` on UNIX and Windows. For instance, to run a specific Python version. On UNIX: diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index 9b870d5b1b..61e066b7a0 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -10,7 +10,8 @@ Instructions for running tests on psutil:: make setup-dev-env # install missing third-party deps - make test + make test # serial run + make test-parallel # parallel run * To run tests on all supported Python versions install tox (``pip install tox``) then run ``tox`` from within psutil root directory. diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ec406a27f7..afd0b98bf1 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -15,6 +15,7 @@ import errno import functools import gc +import inspect import os import random import re @@ -799,8 +800,17 @@ def __str__(self): # monkey patch default unittest.TestCase -if 'PSUTIL_TESTING' in os.environ: - unittest.TestCase = TestCase +unittest.TestCase = TestCase + + +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 @unittest.skipIf(PYPY, "unreliable on PYPY") @@ -1249,5 +1259,5 @@ def cleanup_test_procs(): # modul. With this it will. See: # http://grodola.blogspot.com/ # 2016/02/how-to-always-execute-exit-functions-in-py.html -if POSIX and 'PSUTIL_TESTING' in os.environ: +if POSIX: signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit(sig)) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 7ffbc1cfaf..bfadc06985 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -6,15 +6,19 @@ """ Unit test runner, providing new features on top of unittest module: -- colourized output (error, skip) +- colourized output +- parallel run (UNIX only) - print failures/tracebacks on CTRL+C - re-run failed tests only (make test-failed) """ from __future__ import print_function +import atexit import optparse import os import sys +import textwrap +import time import unittest from unittest import TestResult from unittest import TextTestResult @@ -24,11 +28,18 @@ 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.tests import APPVEYOR +from psutil.tests import import_module_by_path +from psutil.tests import reap_children from psutil.tests import safe_rmpath from psutil.tests import TOX @@ -36,6 +47,9 @@ HERE = os.path.abspath(os.path.dirname(__file__)) VERBOSITY = 1 if TOX else 2 FAILED_TESTS_FNAME = '.failed-tests.txt' +NWORKERS = psutil.cpu_count() or 1 + +loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase # ===================================================================== @@ -46,8 +60,7 @@ class ColouredResult(TextTestResult): def _print_color(self, s, color, bold=False): - file = sys.stderr if color == "red" else sys.stdout - print_color(s, color, bold=bold, file=file) + print_color(s, color, bold=bold, file=self.stream) def addSuccess(self, test): TestResult.addSuccess(self, test) @@ -63,15 +76,15 @@ def addFailure(self, test, err): def addSkip(self, test, reason): TestResult.addSkip(self, test, reason) - self._print_color("skipped: %s" % reason, "brown") + self._print_color("skipped: %s" % reason.strip(), "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 +class ColouredTextRunner(TextTestRunner): + resultclass = ColouredResult def _makeResult(self): # Store result instance so that it can be accessed on @@ -85,81 +98,232 @@ def _makeResult(self): # ===================================================================== -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() +class SuiteLoader: + + testdir = HERE + skip_files = ['test_memory_leaks.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 parallel(self): + serial = unittest.TestSuite() + parallel = unittest.TestSuite() + for obj in self._iter_testmod_classes(): + test = loadTestsFromTestCase(obj) + if getattr(obj, '_serialrun', False): + serial.addTest(test) + else: + parallel.addTest(test) + return (serial, parallel) + + 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, 'rt') as f: + names = f.read().split() + for n in names: + test = unittest.defaultTestLoader.loadTestsFromName(n) + suite.addTest(test) + return suite -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: + def from_name(self, name): + suite = unittest.TestSuite() name = os.path.splitext(os.path.basename(name))[0] suite.addTest(unittest.defaultTestLoader.loadTestsFromName(name)) - return suite + 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]) - unittest.defaultTestLoader.loadTestsFromName(tname) - f.write(tname + '\n') - - -def run(name=None, last_failed=False): - setup_tests() - if APPVEYOR: - runner = TextTestRunner(verbosity=VERBOSITY) - else: - 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) +class Runner: + + def __init__(self): + self.loader = SuiteLoader() + self.failed_tnames = set() + if term_supports_colors() and not APPVEYOR: + self.runner = ColouredTextRunner(verbosity=VERBOSITY) + else: + self.runner = TextTestRunner(verbosity=VERBOSITY) + + def _write_last_failed(self): + if self.failed_tnames: + with open(FAILED_TESTS_FNAME, 'wt') 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 = self.runner.run(suite) + except (KeyboardInterrupt, SystemExit): + result = self.runner.result + result.printErrors() + raise sys.exit(1) + else: + self._save_result(result) + return result + + def _finalize(self, success): + if success: + safe_rmpath(FAILED_TESTS_FNAME) + else: + self._write_last_failed() + print_color("FAILED", "red") + sys.exit(1) + + def run(self, suite=None): + """Run tests serially (1 process).""" + if suite is None: + suite = self.loader.all() + result = self._run(suite) + self._finalize(result.wasSuccessful()) + + def run_last_failed(self): + """Run tests which failed in the last run.""" + self.run(self.loader.last_failed()) + + def run_from_name(self, name): + """Run test by name, e.g.: + "test_linux.TestSystemCPUStats.test_ctx_switches" + """ + self.run(self.loader.from_name(name)) + + def _parallelize_suite(self, suite): + def fdopen(*args, **kwds): + stream = orig_fdopen(*args, **kwds) + 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) + + def run_parallel(self): + """Run tests in parallel.""" + ser_suite, par_suite = self.loader.parallel() + par_suite = self._parallelize_suite(par_suite) + + # run parallel + print("starting parallel tests using %s workers" % NWORKERS) + t = time.time() + par = self._run(par_suite) + par_elapsed = time.time() - t + + # cleanup workers and test subprocesses + orphans = psutil.Process().children() + gone, alive = psutil.wait_procs(orphans, timeout=1) + if alive: + print_color("alive processes %s" % alive, "red") + reap_children() + + # run serial + t = time.time() + ser = self._run(ser_suite) + ser_elapsed = time.time() - t + + # print + 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("-" * 70) + 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._finalize(ok) + + +runner = Runner() +run_from_name = runner.run_from_name + + +def _setup(): + 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 main(): - usage = "python3 -m psutil.tests [opts]" + _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() - run(last_failed=opts.last_failed) + + if not opts.last_failed: + safe_rmpath(FAILED_TESTS_FNAME) + + # test-by-name + if args: + if len(args) > 1: + parser.print_usage() + return sys.exit(1) + return runner.run_from_name(args[0]) + elif opts.last_failed: + runner.run_last_failed() + elif not opts.parallel: + runner.run() + # parallel + elif concurrencytest is None: + print_color("concurrencytest module is not installed; " + "running serial tests instead", "red") + runner.run() + elif NWORKERS == 1: + print_color("only 1 CPU; running serial tests instead", "red") + runner.run() + else: + runner.run_parallel() if __name__ == '__main__': diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 7171232e08..1a7694246b 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -117,5 +117,5 @@ def test_net_if_addrs_names(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 899875d076..3a948c894a 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -561,5 +561,5 @@ def test_cpu_stats_ctx_switches(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 5bd71dc8d4..f4ddc14e78 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -41,6 +41,7 @@ from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import pyrun from psutil.tests import reap_children +from psutil.tests import serialrun from psutil.tests import skip_on_access_denied from psutil.tests import SKIP_SYSCONS from psutil.tests import tcp_socketpair @@ -54,7 +55,8 @@ SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) -class Base(object): +@serialrun +class _ConnTestCase(unittest.TestCase): def setUp(self): if not (NETBSD or FREEBSD): @@ -165,7 +167,7 @@ def check_status(conn): check_status(conn) -class TestBase(Base, unittest.TestCase): +class TestBasicOperations(_ConnTestCase): @unittest.skipIf(SKIP_SYSCONS, "requires root") def test_system(self): @@ -183,7 +185,8 @@ def test_invalid_kind(self): self.assertRaises(ValueError, psutil.net_connections, kind='???') -class TestUnconnectedSockets(Base, unittest.TestCase): +@serialrun +class TestUnconnectedSockets(_ConnTestCase): """Tests sockets which are open but not connected to anything.""" def get_conn_from_sock(self, sock): @@ -279,7 +282,8 @@ def test_unix_udp(self): self.assertEqual(conn.status, psutil.CONN_NONE) -class TestConnectedSocket(Base, unittest.TestCase): +@serialrun +class TestConnectedSocket(_ConnTestCase): """Test socket pairs which are are actually connected to each other. """ @@ -343,7 +347,7 @@ def test_unix(self): client.close() -class TestFilters(Base, unittest.TestCase): +class TestFilters(_ConnTestCase): def test_filters(self): def check(kind, families, types): @@ -543,7 +547,7 @@ def test_count(self): @unittest.skipIf(SKIP_SYSCONS, "requires root") -class TestSystemWideConnections(Base, unittest.TestCase): +class TestSystemWideConnections(_ConnTestCase): """Tests for net_connections().""" def test_it(self): @@ -630,5 +634,5 @@ def test_connection_constants(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 74c429be52..69bb0b2f91 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -682,5 +682,5 @@ def environ(self, ret, proc): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d529ae7c03..4cb3d3af0f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1643,6 +1643,7 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") class TestProcess(unittest.TestCase): + @retry_on_failure() def test_memory_full_info(self): testfn = get_testfn() src = textwrap.dedent(""" @@ -2111,5 +2112,5 @@ def test_cat(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 5cfec577eb..9069b1a3cb 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -489,5 +489,5 @@ def test_win_service_get_description(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 74a0c4273e..8ae7ea4d5c 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -759,5 +759,5 @@ def test_sensors(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index e4e77f9353..4df05fa82f 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -290,5 +290,5 @@ def test_sensors_battery(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index d2c4bfb665..14ec880b36 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -449,5 +449,5 @@ def df(device): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ddb1bbbae0..8095fc04e2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -70,6 +70,7 @@ # --- psutil.Process class tests # =================================================================== + class TestProcess(unittest.TestCase): """Tests for psutil.Process class.""" @@ -295,7 +296,7 @@ def test_create_time(self): @unittest.skipIf(TRAVIS or CIRRUS, 'not reliable on TRAVIS/CIRRUS') def test_terminal(self): terminal = psutil.Process().terminal() - if sys.stdin.isatty() or sys.stdout.isatty(): + if sys.stdout.isatty(): tty = os.path.realpath(sh('tty')) self.assertEqual(terminal, tty) else: @@ -1624,5 +1625,5 @@ def test_kill_terminate(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index e3beb625bf..bac1a2122a 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -41,5 +41,5 @@ def test_cpu_count(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 73401b3ca4..f5d9f49e55 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -194,13 +194,6 @@ def test_pid_exists_2(self): 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) - class TestMiscAPIs(unittest.TestCase): @@ -895,5 +888,5 @@ def test_sensors_fans(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 55c5d3df0c..45b2557f2f 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -43,6 +43,7 @@ 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 tcp_socketpair from psutil.tests import TestMemoryLeak from psutil.tests import unittest @@ -316,6 +317,7 @@ def test_create_sockets(self): self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2) +@serialrun class TestMemLeakClass(TestMemoryLeak): def test_times(self): diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 6fbaa43f87..da6ec96ecb 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -102,6 +102,7 @@ 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 serialrun from psutil.tests import skip_on_access_denied from psutil.tests import TESTFN_PREFIX from psutil.tests import TRAVIS @@ -154,6 +155,7 @@ def subprocess_supports_unicode(suffix): # =================================================================== +@serialrun class _BaseFSAPIsTests(object): funky_suffix = None @@ -358,5 +360,5 @@ def test_proc_environ(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index f68885d0ca..27343ca2aa 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -866,5 +866,5 @@ def test_win_service_get(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) From 6e2aa0d80f5c8301c27551f65181ffc7e2cb5d8c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 26 Apr 2020 02:05:14 +0200 Subject: [PATCH 0541/1714] rename var --- psutil/tests/__init__.py | 4 ++-- psutil/tests/test_aix.py | 10 ++++----- psutil/tests/test_bsd.py | 42 +++++++++++++++++++------------------- psutil/tests/test_linux.py | 24 +++++++++++----------- psutil/tests/test_osx.py | 10 ++++----- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ec406a27f7..cc40916130 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -73,7 +73,7 @@ __all__ = [ # constants - 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'MEMORY_TOLERANCE', 'NO_RETRIES', + 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'SYSMEM_TOLERANCE', 'NO_RETRIES', 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TOX', 'TRAVIS', 'CIRRUS', 'CI_TESTING', 'VALID_PROC_STATUSES', @@ -128,7 +128,7 @@ # 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 +SYSMEM_TOLERANCE = 500 * 1024 # 500KB # the timeout used in functions which have to wait GLOBAL_TIMEOUT = 5 # be more tolerant if we're on travis / appveyor in order to avoid diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 7171232e08..9a80106c78 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -37,17 +37,17 @@ def test_virtual_memory(self): psutil_result = psutil.virtual_memory() - # MEMORY_TOLERANCE from psutil.tests is not enough. For some reason + # SYSMEM_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 + SYSMEM_TOLERANCE = 2 * KB * KB # 2 MB self.assertEqual(psutil_result.total, total) self.assertAlmostEqual( - psutil_result.used, used, delta=MEMORY_TOLERANCE) + psutil_result.used, used, delta=SYSMEM_TOLERANCE) self.assertAlmostEqual( - psutil_result.available, available, delta=MEMORY_TOLERANCE) + psutil_result.available, available, delta=SYSMEM_TOLERANCE) self.assertAlmostEqual( - psutil_result.free, free, delta=MEMORY_TOLERANCE) + psutil_result.free, free, delta=SYSMEM_TOLERANCE) def test_swap_memory(self): out = sh('/usr/sbin/lsps -a') diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 899875d076..4b348775ce 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -22,7 +22,7 @@ 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 SYSMEM_TOLERANCE from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh @@ -279,37 +279,37 @@ def test_cpu_frequency_against_sysctl(self): def test_vmem_active(self): syst = sysctl("vm.stats.vm.v_active_count") * PAGESIZE self.assertAlmostEqual(psutil.virtual_memory().active, syst, - delta=MEMORY_TOLERANCE) + delta=SYSMEM_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) + delta=SYSMEM_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) + delta=SYSMEM_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) + delta=SYSMEM_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) + delta=SYSMEM_TOLERANCE) @retry_on_failure() def test_vmem_buffers(self): syst = sysctl("vfs.bufspace") self.assertAlmostEqual(psutil.virtual_memory().buffers, syst, - delta=MEMORY_TOLERANCE) + delta=SYSMEM_TOLERANCE) # --- virtual_memory(); tests against muse @@ -323,42 +323,42 @@ def test_muse_vmem_total(self): def test_muse_vmem_active(self): num = muse('Active') self.assertAlmostEqual(psutil.virtual_memory().active, num, - delta=MEMORY_TOLERANCE) + delta=SYSMEM_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) + delta=SYSMEM_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) + delta=SYSMEM_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) + delta=SYSMEM_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) + delta=SYSMEM_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) + delta=SYSMEM_TOLERANCE) def test_cpu_stats_ctx_switches(self): self.assertAlmostEqual(psutil.cpu_stats().ctx_switches, @@ -387,17 +387,17 @@ def test_cpu_stats_syscalls(self): def test_swapmem_free(self): total, used, free = self.parse_swapinfo() self.assertAlmostEqual( - psutil.swap_memory().free, free, delta=MEMORY_TOLERANCE) + psutil.swap_memory().free, free, delta=SYSMEM_TOLERANCE) def test_swapmem_used(self): total, used, free = self.parse_swapinfo() self.assertAlmostEqual( - psutil.swap_memory().used, used, delta=MEMORY_TOLERANCE) + psutil.swap_memory().used, used, delta=SYSMEM_TOLERANCE) def test_swapmem_total(self): total, used, free = self.parse_swapinfo() self.assertAlmostEqual( - psutil.swap_memory().total, total, delta=MEMORY_TOLERANCE) + psutil.swap_memory().total, total, delta=SYSMEM_TOLERANCE) # --- others @@ -511,27 +511,27 @@ def test_vmem_total(self): def test_vmem_free(self): self.assertAlmostEqual( psutil.virtual_memory().free, self.parse_meminfo("MemFree:"), - delta=MEMORY_TOLERANCE) + delta=SYSMEM_TOLERANCE) def test_vmem_buffers(self): self.assertAlmostEqual( psutil.virtual_memory().buffers, self.parse_meminfo("Buffers:"), - delta=MEMORY_TOLERANCE) + delta=SYSMEM_TOLERANCE) def test_vmem_shared(self): self.assertAlmostEqual( psutil.virtual_memory().shared, self.parse_meminfo("MemShared:"), - delta=MEMORY_TOLERANCE) + delta=SYSMEM_TOLERANCE) def test_swapmem_total(self): self.assertAlmostEqual( psutil.swap_memory().total, self.parse_meminfo("SwapTotal:"), - delta=MEMORY_TOLERANCE) + delta=SYSMEM_TOLERANCE) def test_swapmem_free(self): self.assertAlmostEqual( psutil.swap_memory().free, self.parse_meminfo("SwapFree:"), - delta=MEMORY_TOLERANCE) + delta=SYSMEM_TOLERANCE) def test_swapmem_used(self): smem = psutil.swap_memory() diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d529ae7c03..30fadd2b1c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -33,7 +33,7 @@ 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 SYSMEM_TOLERANCE from psutil.tests import mock from psutil.tests import PYPY from psutil.tests import pyrun @@ -211,7 +211,7 @@ def test_used(self): free_value = free.used psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, + free_value, psutil_value, delta=SYSMEM_TOLERANCE, msg='%s %s \n%s' % (free_value, psutil_value, free.output)) @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") @@ -220,14 +220,14 @@ 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) + vmstat_value, psutil_value, delta=SYSMEM_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) + vmstat_value, psutil_value, delta=SYSMEM_TOLERANCE) # https://travis-ci.org/giampaolo/psutil/jobs/226719664 @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") @@ -236,7 +236,7 @@ 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) + vmstat_value, psutil_value, delta=SYSMEM_TOLERANCE) # https://travis-ci.org/giampaolo/psutil/jobs/227242952 @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") @@ -245,7 +245,7 @@ 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) + vmstat_value, psutil_value, delta=SYSMEM_TOLERANCE) @retry_on_failure() def test_shared(self): @@ -255,7 +255,7 @@ def test_shared(self): raise unittest.SkipTest("free does not support 'shared' column") psutil_value = psutil.virtual_memory().shared self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, + free_value, psutil_value, delta=SYSMEM_TOLERANCE, msg='%s %s \n%s' % (free_value, psutil_value, free.output)) @retry_on_failure() @@ -270,7 +270,7 @@ def test_available(self): free_value = int(lines[1].split()[-1]) psutil_value = psutil.virtual_memory().available self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, + free_value, psutil_value, delta=SYSMEM_TOLERANCE, msg='%s %s \n%s' % (free_value, psutil_value, out)) def test_warnings_on_misses(self): @@ -508,21 +508,21 @@ 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) + free_value, psutil_value, delta=SYSMEM_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) + free_value, psutil_value, delta=SYSMEM_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) + free_value, psutil_value, delta=SYSMEM_TOLERANCE) def test_missing_sin_sout(self): with mock.patch('psutil._common.open', create=True) as m: @@ -572,7 +572,7 @@ def test_meminfo_against_sysinfo(self): total *= unit_multiplier free *= unit_multiplier self.assertEqual(swap.total, total) - self.assertAlmostEqual(swap.free, free, delta=MEMORY_TOLERANCE) + self.assertAlmostEqual(swap.free, free, delta=SYSMEM_TOLERANCE) def test_emulate_meminfo_has_no_metrics(self): # Emulate a case where /proc/meminfo provides no swap metrics diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index e4e77f9353..c4008b9dcc 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -15,7 +15,7 @@ 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 SYSMEM_TOLERANCE from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh @@ -219,25 +219,25 @@ def test_vmem_total(self): 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) + self.assertAlmostEqual(psutil_val, vmstat_val, delta=SYSMEM_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) + self.assertAlmostEqual(psutil_val, vmstat_val, delta=SYSMEM_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) + self.assertAlmostEqual(psutil_val, vmstat_val, delta=SYSMEM_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) + self.assertAlmostEqual(psutil_val, vmstat_val, delta=SYSMEM_TOLERANCE) # --- swap mem From b20e8c05c749d1e2a5a2a1fb6b892318191d8575 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Apr 2020 12:38:43 +0200 Subject: [PATCH 0542/1714] add new termina() test util --- psutil/tests/__init__.py | 129 ++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 56 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5d64cce9ae..bf3d973f7a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -83,8 +83,8 @@ "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', + 'pyrun', 'terminate', 'reap_children', 'get_test_subprocess', + 'create_zombie_proc', 'create_proc_children_pair', # threads 'ThreadTask' # test utils @@ -381,10 +381,9 @@ def create_zombie_proc(): pid = bytes(str(os.getpid()), 'ascii') s.sendall(pid) """ % unix_file) - with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock: + sock = bind_unix_socket(unix_file) + with contextlib.closing(sock): sock.settimeout(GLOBAL_TIMEOUT) - sock.bind(unix_file) - sock.listen(5) pyrun(src) conn, _ = sock.accept() try: @@ -441,95 +440,113 @@ def sh(cmd, **kwds): 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. - """ +def _assert_no_pid(pid): # 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 + assert not psutil.pid_exists(pid), pid + assert pid not in psutil.pids(), pid + try: + p = psutil.Process(pid) + except psutil.NoSuchProcess: + pass + else: + assert 0, "%s is still alive" % p + + +def terminate(proc_or_pid, sig=signal.SIGTERM, wait_w_timeout=GLOBAL_TIMEOUT): + """Terminate and flush a psutil.Process, psutil.Popen or + subprocess.Popen instance. + """ + if isinstance(proc_or_pid, int): try: - p = psutil.Process(pid) - assert not p.is_running(), pid + proc = psutil.Process(proc_or_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)) + return else: - children = set() + proc = proc_or_pid - # 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) + if isinstance(proc, subprocess.Popen): try: - subp.terminate() + proc.send_signal(sig) 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() + if proc.stdout: + proc.stdout.close() + if proc.stderr: + proc.stderr.close() try: # Flushing a BufferedWriter may raise an error. - if subp.stdin: - subp.stdin.close() + if proc.stdin: + proc.stdin.close() finally: - # Wait for the process to terminate, to avoid zombies. - try: - subp.wait() - except ChildProcessError: - pass + if wait_w_timeout: + try: + proc.wait(wait_w_timeout) + except ChildProcessError: + pass + else: + try: + proc.send_signal(sig) + except psutil.NoSuchProcess: + _assert_no_pid(proc.pid) + else: + if wait_w_timeout: + proc.wait(wait_w_timeout) + _assert_no_pid(proc.pid) + + +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. + """ + # If recursive, get the children here before terminating them, as + # we don't want to lose the intermediate reference pointing to the + # grandchildren. + if recursive: + children = set(psutil.Process().children(recursive=True)) + else: + children = set() - # Terminate started pids. + # Terminate subprocess.Popen. + while _subprocesses_started: + subp = _subprocesses_started.pop() + _pids_started.add(subp.pid) + terminate(subp) + + # Collect started pids. while _pids_started: pid = _pids_started.pop() try: p = psutil.Process(pid) except psutil.NoSuchProcess: - assert_gone(pid) + _assert_no_pid(pid) else: children.add(p) # Terminate children. if children: for p in children: - try: - p.terminate() - except psutil.NoSuchProcess: - pass + terminate(p, wait_w_timeout=None) 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 + terminate(p, wait_w_timeout=None, sig=signal.SIGKILL) 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) + _assert_no_pid(p.pid) # =================================================================== From 92e150ef5e309ff93378ae4538065f1ca5c00a17 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Apr 2020 18:32:46 -0700 Subject: [PATCH 0543/1714] psutil.Popen: inherit from subprocess + support wait(timeout=...) parameter (#1736) --- HISTORY.rst | 2 + Makefile | 2 +- docs/index.rst | 27 +++++++------ psutil/__init__.py | 75 +++++++++++++++++++++--------------- psutil/tests/__init__.py | 39 ++++++++++++++----- psutil/tests/runner.py | 3 +- psutil/tests/test_linux.py | 6 +-- psutil/tests/test_process.py | 5 ++- 8 files changed, 99 insertions(+), 60 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d68e68fcfe..7b992a8c9c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,6 +8,8 @@ XXXX-XX-XX **Enhancements** - 1729_: parallel tests on UNIX (make test-parallel). +- 1736_: psutil.Popen now inherits from subprocess.Popen instead of + psutil.Process. Also, wait(timeout=...) parameter is backported to Python 2.7. **Bug fixes** diff --git a/Makefile b/Makefile index 76420bc72a..1fad22f516 100644 --- a/Makefile +++ b/Makefile @@ -159,7 +159,7 @@ test-memleaks: ## Memory leak tests. test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSystemAPIs ${MAKE} install - @$(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) test-failed: ## Re-run tests which failed on last run ${MAKE} install diff --git a/docs/index.rst b/docs/index.rst index 55e1586dd1..08a69555a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1966,15 +1966,8 @@ Popen class A more convenient interface to stdlib `subprocess.Popen`_. It starts a sub process and you deal with it exactly as when using - `subprocess.Popen`_. - but in addition it also provides all the methods of :class:`psutil.Process` - class. - For method names common to both classes such as - :meth:`send_signal() `, - :meth:`terminate() ` and - :meth:`kill() ` - :class:`psutil.Process` implementation takes precedence. - For a complete documentation refer to subprocess module documentation. + `subprocess.Popen`_, but in addition it also provides all the methods of + :class:`psutil.Process` class as a unified interface. .. note:: @@ -1999,16 +1992,25 @@ Popen class 0 >>> + *timeout* parameter of `subprocess.Popen.wait`_ is backported for Python < 3.3. :class:`psutil.Popen` objects are supported as context managers via the with - statement: on exit, standard file descriptors are closed, and the process - is waited for. This is supported on all Python versions. + statement (added to Python 3.2). On exit, standard file descriptors are + closed, and the process is waited for. This is supported on all Python + versions. >>> import psutil, subprocess >>> with psutil.Popen(["ifconfig"], stdout=subprocess.PIPE) as proc: >>> log.write(proc.stdout.read()) - .. versionchanged:: 4.4.0 added context manager support + .. versionchanged:: 4.4.0 added context manager support. + + .. versionchanged:: 5.7.1 inherit from `subprocess.Popen`_ instead of + :class:`psutil.Process`. + + .. versionchanged:: 5.7.1 backporint `subprocess.Popen.wait`_ **timeout** + parameter on old Python versions. + Windows services ================ @@ -2882,6 +2884,7 @@ Timeline .. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM .. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd .. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen +.. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait .. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py .. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess .. _Tidelift security contact: https://tidelift.com/security diff --git a/psutil/__init__.py b/psutil/__init__.py index e6a2da8d6b..650fcf0f02 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1274,17 +1274,24 @@ def wait(self, timeout=None): return self._proc.wait(timeout) +# 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', 'parents', 'children', 'rlimit', + 'memory_info_ex', 'oneshot']]) + + # ===================================================================== # --- Popen class # ===================================================================== -class Popen(Process): +class Popen(subprocess.Popen): """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: + subprocess.Popen class, but in addition it also provides all the + methods of psutil.Process class as a unified interface: >>> import psutil >>> from subprocess import PIPE @@ -1302,11 +1309,12 @@ class Popen(Process): 0 >>> - For method names common to both classes such as kill(), terminate() - and wait(), psutil.Process implementation takes precedence. + In addition, it backports the following functionality: + * "with" statement (Python 3.2) + * wait(timeout=...) parameter (Python 3.3) Unlike subprocess.Popen this class pre-emptively checks whether PID - has been reused on send_signal(), terminate() and kill() so that + has been reused on send_signal(), terminate() and kill(), so that you don't accidentally terminate another process, fixing http://bugs.python.org/issue6973. @@ -1318,21 +1326,21 @@ def __init__(self, *args, **kwargs): # Explicitly avoid to raise NoSuchProcess in case the process # spawned by subprocess.Popen terminates too quickly, see: # https://github.com/giampaolo/psutil/issues/193 - self.__subproc = subprocess.Popen(*args, **kwargs) - self._init(self.__subproc.pid, _ignore_nsp=True) + self.__psproc = None + subprocess.Popen.__init__(self, *args, **kwargs) + self.__psproc = Process(self.pid) + self.__psproc._init(self.pid, _ignore_nsp=True) def __dir__(self): - return sorted(set(dir(Popen) + dir(subprocess.Popen))) + return sorted(set(dir(subprocess.Popen) + dir(Process))) - def __enter__(self): - if hasattr(self.__subproc, '__enter__'): - self.__subproc.__enter__() - return self + # Introduced in Python 3.2. + if not hasattr(subprocess.Popen, '__enter__'): - def __exit__(self, *args, **kwargs): - if hasattr(self.__subproc, '__exit__'): - return self.__subproc.__exit__(*args, **kwargs) - else: + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): if self.stdout: self.stdout.close() if self.stderr: @@ -1350,25 +1358,30 @@ def __getattribute__(self, name): return object.__getattribute__(self, name) except AttributeError: try: - return object.__getattribute__(self.__subproc, name) + return object.__getattribute__(self.__psproc, name) except AttributeError: raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name)) - def wait(self, timeout=None): - if self.__subproc.returncode is not None: - return self.__subproc.returncode - ret = super(Popen, self).wait(timeout) - self.__subproc.returncode = ret - return ret + def send_signal(self, sig): + return self.__psproc.send_signal(sig) + def terminate(self): + return self.__psproc.terminate() -# 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', 'parents', 'children', 'rlimit', - 'memory_info_ex', 'oneshot']]) + def kill(self): + return self.__psproc.kill() + + def wait(self, timeout=None): + if sys.version_info < (3, 3): + # backport of timeout parameter + if self.returncode is not None: + return self.returncode + ret = self.__psproc.wait(timeout) + self.returncode = ret + return ret + else: + return super(Popen, self).wait(timeout) # ===================================================================== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index bf3d973f7a..b31b845d02 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -459,6 +459,20 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_w_timeout=GLOBAL_TIMEOUT): """Terminate and flush a psutil.Process, psutil.Popen or subprocess.Popen instance. """ + def wait(proc, timeout=None): + if sys.version_info < (3, 3) and not \ + isinstance(proc, (psutil.Process, psutil.Popen)): + # subprocess.Popen instance + no timeout arg. + try: + ret = psutil.Process(proc.pid).wait(timeout) + except psutil.NoSuchProcess: + pass + else: + proc.returncode = ret + return ret + else: + return proc.wait(timeout) + if isinstance(proc_or_pid, int): try: proc = psutil.Process(proc_or_pid) @@ -467,7 +481,18 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_w_timeout=GLOBAL_TIMEOUT): else: proc = proc_or_pid - if isinstance(proc, subprocess.Popen): + if isinstance(proc, (psutil.Process, psutil.Popen)): + try: + proc.send_signal(sig) + except psutil.NoSuchProcess: + _assert_no_pid(proc.pid) + else: + if wait_w_timeout: + ret = wait(proc, wait_w_timeout) + _assert_no_pid(proc.pid) + return ret + else: + # subprocess.Popen instance try: proc.send_signal(sig) except OSError as err: @@ -475,6 +500,8 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_w_timeout=GLOBAL_TIMEOUT): pass elif err.errno != errno.ESRCH: raise + except psutil.NoSuchProcess: # psutil.Popen + pass if proc.stdout: proc.stdout.close() if proc.stderr: @@ -486,17 +513,9 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_w_timeout=GLOBAL_TIMEOUT): finally: if wait_w_timeout: try: - proc.wait(wait_w_timeout) + return wait(proc, wait_w_timeout) except ChildProcessError: pass - else: - try: - proc.send_signal(sig) - except psutil.NoSuchProcess: - _assert_no_pid(proc.pid) - else: - if wait_w_timeout: - proc.wait(wait_w_timeout) _assert_no_pid(proc.pid) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index bfadc06985..d90feabd7e 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -155,7 +155,8 @@ def last_failed(self): def from_name(self, name): suite = unittest.TestSuite() - name = os.path.splitext(os.path.basename(name))[0] + if name.endswith('.py'): + name = os.path.splitext(os.path.basename(name))[0] suite.addTest(unittest.defaultTestLoader.loadTestsFromName(name)) return suite diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index ea957c17e8..6d4a934ae8 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -33,16 +33,16 @@ from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_RLIMIT -from psutil.tests import SYSMEM_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 SYSMEM_TOLERANCE +from psutil.tests import terminate from psutil.tests import ThreadTask from psutil.tests import TRAVIS from psutil.tests import unittest @@ -1652,7 +1652,7 @@ def test_memory_full_info(self): time.sleep(10) """ % testfn) sproc = pyrun(src) - self.addCleanup(reap_children) + self.addCleanup(terminate, sproc) call_until(lambda: os.listdir('.'), "'%s' not in ret" % testfn) p = psutil.Process(sproc.pid) time.sleep(.1) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 8095fc04e2..ac841eee99 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1338,7 +1338,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): pass zpid = create_zombie_proc() - self.addCleanup(reap_children, recursive=True) + self.addCleanup(reap_children) # A zombie process should always be instantiable zproc = psutil.Process(zpid) # ...and at least its status always be querable @@ -1348,7 +1348,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): # ...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) + ret = succeed_or_zombie_p_exc(zproc.cmdline) if ret is not None: self.assertEqual(ret, []) @@ -1592,6 +1592,7 @@ def test_misc(self): self.assertTrue(dir(proc)) self.assertRaises(AttributeError, getattr, proc, 'foo') proc.terminate() + proc.wait(timeout=3) def test_ctx_manager(self): with psutil.Popen([PYTHON_EXE, "-V"], From efe1cdb3b7177a0e65531cf9b099d28df9bc8271 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Apr 2020 04:15:25 +0200 Subject: [PATCH 0544/1714] create_zombie_proc() make it return parent so that we can kill zombie --- psutil/tests/__init__.py | 13 ++++++++----- psutil/tests/test_osx.py | 8 +++++--- psutil/tests/test_process.py | 16 +++++++++------- psutil/tests/test_testutils.py | 9 +++++---- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b31b845d02..fea5a5d0a3 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -362,7 +362,10 @@ def create_proc_children_pair(): def create_zombie_proc(): - """Create a zombie process and return its PID.""" + """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("""\ @@ -384,15 +387,15 @@ def create_zombie_proc(): sock = bind_unix_socket(unix_file) with contextlib.closing(sock): sock.settimeout(GLOBAL_TIMEOUT) - pyrun(src) + parent = 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 + zombie = psutil.Process(zpid) + call_until(lambda: zombie.status(), "ret == psutil.STATUS_ZOMBIE") + return (parent, zombie) finally: conn.close() diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index f786284375..bcff0ba79c 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -19,6 +19,7 @@ from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh +from psutil.tests import terminate from psutil.tests import unittest @@ -105,12 +106,13 @@ class TestZombieProcessAPIs(unittest.TestCase): @classmethod def setUpClass(cls): - zpid = create_zombie_proc() - cls.p = psutil.Process(zpid) + cls.parent, cls.zombie = create_zombie_proc() + cls.p = psutil.Process(cls.zombie.pid) @classmethod def tearDownClass(cls): - reap_children(recursive=True) + terminate(cls.parent) + terminate(cls.zombie) def test_pidtask_info(self): self.assertEqual(self.p.status(), psutil.STATUS_ZOMBIE) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ac841eee99..ef8d245f74 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -60,6 +60,7 @@ 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 terminate from psutil.tests import ThreadTask from psutil.tests import TRAVIS from psutil.tests import unittest @@ -1337,10 +1338,11 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): except (psutil.ZombieProcess, psutil.AccessDenied): pass - zpid = create_zombie_proc() - self.addCleanup(reap_children) + parent, zombie = create_zombie_proc() + self.addCleanup(terminate, zombie) + self.addCleanup(terminate, parent) # executed first # A zombie process should always be instantiable - zproc = psutil.Process(zpid) + zproc = psutil.Process(zombie.pid) # ...and at least its status always be querable self.assertEqual(zproc.status(), psutil.STATUS_ZOMBIE) # ...and it should be considered 'running' @@ -1392,15 +1394,15 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): # 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)) + self.assertTrue(psutil.pid_exists(zproc.pid)) 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()]) + self.assertIn(zproc.pid, psutil.pids()) + self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) psutil._pmap = {} - self.assertIn(zpid, [x.pid for x in psutil.process_iter()]) + self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process_is_running_w_exc(self): diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 45b2557f2f..fd32e0b7b9 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -45,6 +45,7 @@ from psutil.tests import safe_rmpath from psutil.tests import serialrun from psutil.tests import tcp_socketpair +from psutil.tests import terminate from psutil.tests import TestMemoryLeak from psutil.tests import unittest from psutil.tests import unix_socketpair @@ -240,10 +241,10 @@ def test_create_proc_children_pair(self): @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) + parent, zombie = create_zombie_proc() + self.addCleanup(terminate, zombie) + self.addCleanup(terminate, parent) # executed first + self.assertEqual(zombie.status(), psutil.STATUS_ZOMBIE) class TestNetUtils(unittest.TestCase): From 56db14e14797ac790094a7bd8865d63383fd93a7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Apr 2020 04:27:56 +0200 Subject: [PATCH 0545/1714] show status() in __repr__ --- README.rst | 12 ++++++------ psutil/__init__.py | 30 ++++++++++++++++-------------- psutil/tests/test_misc.py | 2 ++ 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 56b4017e7d..62ab3f2348 100644 --- a/README.rst +++ b/README.rst @@ -327,7 +327,7 @@ Process management >>> >>> p = psutil.Process(7055) >>> p - psutil.Process(pid=7055, name='python', started='09:04:44') + psutil.Process(pid=7055, name='python3', status='running', started='09:04:44') >>> p.name() 'python' >>> p.exe() @@ -342,15 +342,15 @@ Process management >>> p.ppid() 7054 >>> p.children(recursive=True) - [psutil.Process(pid=29835, name='python2.7', started='11:45:38'), - psutil.Process(pid=29836, name='python2.7', started='11:43:39')] + [psutil.Process(pid=29835, name='python3', status='sleeping', started='11:45:38'), + psutil.Process(pid=29836, name='python3', status='waking', started='11:43:39')] >>> >>> p.parent() - psutil.Process(pid=4699, name='bash', started='09:06:44') + psutil.Process(pid=4699, name='bash', status='sleeping', started='09:06:44') >>> p.parents() [psutil.Process(pid=4699, name='bash', started='09:06:44'), - psutil.Process(pid=4689, name='gnome-terminal-server', started='0:06:44'), - psutil.Process(pid=1, name='systemd', started='05:56:55')] + psutil.Process(pid=4689, name='gnome-terminal-server', status='sleeping', started='0:06:44'), + psutil.Process(pid=1, name='systemd', status='sleeping', started='05:56:55')] >>> >>> p.status() 'running' diff --git a/psutil/__init__.py b/psutil/__init__.py index 650fcf0f02..028ab04949 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -397,20 +397,22 @@ def __str__(self): except AttributeError: info = {} # Python 2.6 info["pid"] = self.pid - try: - info["name"] = self.name() - if self._create_time: - 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()])) + with self.oneshot(): + try: + info["name"] = self.name() + info["status"] = self.status() + if self._create_time: + 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()])) __repr__ = __str__ diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 8ae7ea4d5c..18781e7524 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -61,7 +61,9 @@ def test_process__repr__(self, func=repr): self.assertIn("psutil.Process", r) self.assertIn("pid=%s" % p.pid, r) self.assertIn("name=", r) + self.assertIn("status=", r) self.assertIn(p.name(), r) + self.assertIn("status='running'", r) with mock.patch.object(psutil.Process, "name", side_effect=psutil.ZombieProcess(os.getpid())): p = psutil.Process() From 0065a3921b1cf31ad32cca5bd3ca209fe1fbceed Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Apr 2020 12:06:38 -0700 Subject: [PATCH 0546/1714] Test sub-processes cleanup and ProcessTestCase class (#1739) --- psutil/tests/__init__.py | 196 ++++++++++++++++++------------- psutil/tests/runner.py | 2 + psutil/tests/test_bsd.py | 8 +- psutil/tests/test_connections.py | 16 ++- psutil/tests/test_linux.py | 8 +- psutil/tests/test_osx.py | 5 +- psutil/tests/test_posix.py | 7 +- psutil/tests/test_process.py | 104 ++++++++-------- psutil/tests/test_system.py | 25 ++-- psutil/tests/test_testutils.py | 55 ++++++--- psutil/tests/test_unicode.py | 22 ++-- psutil/tests/test_windows.py | 27 +++-- 12 files changed, 260 insertions(+), 215 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index fea5a5d0a3..fc4bff01c9 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -45,7 +45,6 @@ from psutil._common import bytes2human from psutil._common import print_color from psutil._common import supports_ipv6 -from psutil._compat import ChildProcessError from psutil._compat import FileExistsError from psutil._compat import FileNotFoundError from psutil._compat import PY3 @@ -89,7 +88,7 @@ 'ThreadTask' # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_on_failure', 'TestMemoryLeak', + 'retry_on_failure', 'TestMemoryLeak', 'ProcessTestCase', # install utils 'install_pip', 'install_test_deps', # fs utils @@ -291,7 +290,7 @@ def wrapper(*args, **kwargs): @_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. + 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 attemps to make sure the process is in a reasonably @@ -353,9 +352,7 @@ def create_proc_children_pair(): else: subp = pyrun(s) child1 = psutil.Process(subp.pid) - data = wait_for_file(testfn, delete=False, empty=False) - safe_rmpath(testfn) - child2_pid = int(data) + child2_pid = int(wait_for_file(testfn, delete=True, empty=False)) _pids_started.add(child2_pid) child2 = psutil.Process(child2_pid) return (child1, child2) @@ -458,44 +455,37 @@ def _assert_no_pid(pid): assert 0, "%s is still alive" % p -def terminate(proc_or_pid, sig=signal.SIGTERM, wait_w_timeout=GLOBAL_TIMEOUT): - """Terminate and flush a psutil.Process, psutil.Popen or - subprocess.Popen instance. +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=None): - if sys.version_info < (3, 3) and not \ - isinstance(proc, (psutil.Process, psutil.Popen)): - # subprocess.Popen instance + no timeout arg. + if POSIX: + from psutil._psposix import wait_pid + + def wait(proc, timeout): + if sys.version_info < (3, 3) and \ + isinstance(proc, subprocess.Popen) and \ + not isinstance(proc, psutil.Popen): + # subprocess.Popen instance: emulate missing timeout arg. + ret = None try: ret = psutil.Process(proc.pid).wait(timeout) except psutil.NoSuchProcess: - pass - else: - proc.returncode = ret - return ret + # Needed to kill zombies. + if POSIX: + ret = wait_pid(proc.pid, timeout) + proc.returncode = ret + return ret else: return proc.wait(timeout) - if isinstance(proc_or_pid, int): - try: - proc = psutil.Process(proc_or_pid) - except psutil.NoSuchProcess: - return - else: - proc = proc_or_pid - - if isinstance(proc, (psutil.Process, psutil.Popen)): - try: - proc.send_signal(sig) - except psutil.NoSuchProcess: - _assert_no_pid(proc.pid) - else: - if wait_w_timeout: - ret = wait(proc, wait_w_timeout) - _assert_no_pid(proc.pid) - return ret - else: - # subprocess.Popen instance + def term_subproc(proc, timeout): try: proc.send_signal(sig) except OSError as err: @@ -503,72 +493,80 @@ def wait(proc, timeout=None): pass elif err.errno != errno.ESRCH: raise - except psutil.NoSuchProcess: # psutil.Popen + return wait(proc, timeout) + + def term_psproc(proc, timeout): + try: + proc.send_signal(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_psproc(proc, timeout) + + def flush_popen(proc): if proc.stdout: proc.stdout.close() if proc.stderr: proc.stderr.close() - try: - # Flushing a BufferedWriter may raise an error. - if proc.stdin: - proc.stdin.close() - finally: - if wait_w_timeout: - try: - return wait(proc, wait_w_timeout) - except ChildProcessError: - pass - _assert_no_pid(proc.pid) + # 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_psproc(p, wait_timeout) + elif isinstance(p, subprocess.Popen): + return term_subproc(p, wait_timeout) + else: + raise TypeError("wrong type %r" % p) + finally: + if isinstance(p, (subprocess.Popen, psutil.Popen)): + flush_popen(p) + _assert_no_pid(p if isinstance(p, int) else p.pid) 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. - + and any children currently running, ensuring that no processes stick + around to hog resources. If resursive is True it also tries to terminate and wait() all grandchildren started by this process. """ - # If recursive, get the children here before terminating them, as - # we don't want to lose the intermediate reference pointing to the - # grandchildren. - if recursive: - children = set(psutil.Process().children(recursive=True)) - else: - children = set() + # 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() - _pids_started.add(subp.pid) terminate(subp) # Collect started pids. while _pids_started: pid = _pids_started.pop() - try: - p = psutil.Process(pid) - except psutil.NoSuchProcess: - _assert_no_pid(pid) - else: - children.add(p) + terminate(pid) # Terminate children. if children: for p in children: - terminate(p, wait_w_timeout=None) + terminate(p, wait_timeout=None) gone, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) for p in alive: warn("couldn't terminate process %r; attempting kill()" % p) - terminate(p, wait_w_timeout=None, sig=signal.SIGKILL) - 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_no_pid(p.pid) + terminate(p, sig=signal.SIGKILL) # =================================================================== @@ -774,6 +772,7 @@ def chdir(dirname): def create_exe(outpath, c_code=None): """Creates an executable file in the given location.""" assert not os.path.exists(outpath), outpath + _testfiles_created.add(outpath) if c_code: if not which("gcc"): raise ValueError("gcc is not installed") @@ -842,14 +841,37 @@ def __str__(self): unittest.TestCase = TestCase -def serialrun(klass): - """A decorator to mark a TestCase class. When running parallel tests, - class' unit tests will be run serially (1 process). +class ProcessTestCase(TestCase): + """Test class providing auto-cleanup wrappers on top of process + test utilities. """ - # assert issubclass(klass, unittest.TestCase), klass - assert inspect.isclass(klass), klass - klass._serialrun = True - return klass + + def get_test_subprocess(self, *args, **kwds): + sproc = get_test_subprocess(*args, **kwds) + self.addCleanup(terminate, sproc) + return sproc + + def create_proc_children_pair(self): + child1, child2 = create_proc_children_pair() + self.addCleanup(terminate, child1) + self.addCleanup(terminate, child2) + return (child1, child2) + + def create_zombie_proc(self): + parent, zombie = create_zombie_proc() + self.addCleanup(terminate, zombie) + self.addCleanup(terminate, parent) # executed first + return (parent, zombie) + + def pyrun(self, *args, **kwds): + sproc = pyrun(*args, **kwds) + self.addCleanup(terminate, sproc) + return sproc + + def get_testfn(self, suffix="", dir=None): + fname = get_testfn(suffix=suffix, dir=suffix) + self.addCleanup(safe_rmpath(fname)) + return fname @unittest.skipIf(PYPY, "unreliable on PYPY") @@ -966,6 +988,16 @@ def call(): self.execute(call, **kwargs) +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. diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index d90feabd7e..8e4c872a22 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -256,6 +256,8 @@ def run_parallel(self): ser_elapsed = time.time() - t # print + if not par.wasSuccessful(): + par.printErrors() # print them again at the bottom par_fails, par_errs, par_skips = map(len, (par.failures, par.errors, par.skipped)) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index d25eb877a4..427b9219c9 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -22,10 +22,10 @@ from psutil import OPENBSD from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY -from psutil.tests import SYSMEM_TOLERANCE -from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh +from psutil.tests import SYSMEM_TOLERANCE +from psutil.tests import terminate from psutil.tests import unittest from psutil.tests import which @@ -81,7 +81,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reap_children() + terminate(cls.pid) @unittest.skipIf(NETBSD, "-o lstart doesn't work on NETBSD") def test_process_create_time(self): @@ -156,7 +156,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reap_children() + terminate(cls.pid) @retry_on_failure() def test_memory_maps(self): diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index f4ddc14e78..9c1bbe8e5d 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -39,8 +39,7 @@ from psutil.tests import get_free_port from psutil.tests import get_testfn from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import pyrun -from psutil.tests import reap_children +from psutil.tests import ProcessTestCase from psutil.tests import serialrun from psutil.tests import skip_on_access_denied from psutil.tests import SKIP_SYSCONS @@ -56,7 +55,7 @@ @serialrun -class _ConnTestCase(unittest.TestCase): +class _ConnTestCase(ProcessTestCase): def setUp(self): if not (NETBSD or FREEBSD): @@ -65,7 +64,6 @@ def setUp(self): assert not cons, cons def tearDown(self): - reap_children() if not (FREEBSD or NETBSD): # Make sure we closed all resources. # NetBSD opens a UNIX socket to /var/log/run. @@ -447,14 +445,14 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): # launch various subprocess instantiating a socket of various # families and types to enrich psutil results - tcp4_proc = pyrun(tcp4_template) + tcp4_proc = self.pyrun(tcp4_template) tcp4_addr = eval(wait_for_file(testfile)) - udp4_proc = pyrun(udp4_template) + udp4_proc = self.pyrun(udp4_template) udp4_addr = eval(wait_for_file(testfile)) if supports_ipv6(): - tcp6_proc = pyrun(tcp6_template) + tcp6_proc = self.pyrun(tcp6_template) tcp6_addr = eval(wait_for_file(testfile)) - udp6_proc = pyrun(udp6_template) + udp6_proc = self.pyrun(udp6_template) udp6_addr = eval(wait_for_file(testfile)) else: tcp6_proc = None @@ -596,7 +594,7 @@ def test_multi_sockets_procs(self): cleanup_test_files() time.sleep(60) """ % fname) - sproc = pyrun(src) + sproc = self.pyrun(src) pids.append(sproc.pid) # sync diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 6d4a934ae8..bac20b0585 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -34,15 +34,14 @@ from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_RLIMIT from psutil.tests import mock +from psutil.tests import ProcessTestCase from psutil.tests import PYPY -from psutil.tests import pyrun 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 SYSMEM_TOLERANCE -from psutil.tests import terminate from psutil.tests import ThreadTask from psutil.tests import TRAVIS from psutil.tests import unittest @@ -1641,7 +1640,7 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") -class TestProcess(unittest.TestCase): +class TestProcess(ProcessTestCase): @retry_on_failure() def test_memory_full_info(self): @@ -1651,8 +1650,7 @@ def test_memory_full_info(self): with open("%s", "w") as f: time.sleep(10) """ % testfn) - sproc = pyrun(src) - self.addCleanup(terminate, sproc) + sproc = self.pyrun(src) call_until(lambda: os.listdir('.'), "'%s' not in ret" % testfn) p = psutil.Process(sproc.pid) time.sleep(.1) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index bcff0ba79c..4fa8d0af31 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -16,7 +16,6 @@ from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY from psutil.tests import SYSMEM_TOLERANCE -from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import terminate @@ -85,7 +84,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reap_children() + terminate(cls.pid) def test_process_create_time(self): output = sh("ps -o lstart -p %s" % self.pid) @@ -111,8 +110,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - terminate(cls.parent) terminate(cls.zombie) + terminate(cls.parent) # executed first def test_pidtask_info(self): self.assertEqual(self.p.status(), psutil.STATUS_ZOMBIE) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 14ec880b36..1b37fa2fd7 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -28,13 +28,12 @@ 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 terminate from psutil.tests import TRAVIS from psutil.tests import unittest -from psutil.tests import wait_for_pid from psutil.tests import which @@ -134,11 +133,11 @@ class TestProcess(unittest.TestCase): def setUpClass(cls): cls.pid = get_test_subprocess([PYTHON_EXE, "-E", "-O"], stdin=subprocess.PIPE).pid - wait_for_pid(cls.pid) + # wait_for_pid(cls.pid) @classmethod def tearDownClass(cls): - reap_children() + terminate(cls.pid) def test_ppid(self): ppid_ps = ps('ppid', self.pid) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ef8d245f74..178b991b8a 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -39,10 +39,7 @@ from psutil.tests import CIRRUS 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 get_testfn from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_ENVIRON @@ -53,6 +50,7 @@ from psutil.tests import HAS_RLIMIT from psutil.tests import HAS_THREADS from psutil.tests import mock +from psutil.tests import ProcessTestCase from psutil.tests import PYPY from psutil.tests import PYTHON_EXE from psutil.tests import reap_children @@ -60,7 +58,6 @@ 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 terminate from psutil.tests import ThreadTask from psutil.tests import TRAVIS from psutil.tests import unittest @@ -72,22 +69,19 @@ # =================================================================== -class TestProcess(unittest.TestCase): +class TestProcess(ProcessTestCase): """Tests for psutil.Process class.""" - def tearDown(self): - reap_children() - def test_pid(self): p = psutil.Process() self.assertEqual(p.pid, os.getpid()) - sproc = get_test_subprocess() + sproc = self.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() + sproc = self.get_test_subprocess() test_pid = sproc.pid p = psutil.Process(test_pid) p.kill() @@ -97,7 +91,7 @@ def test_kill(self): self.assertEqual(sig, -signal.SIGKILL) def test_terminate(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() test_pid = sproc.pid p = psutil.Process(test_pid) p.terminate() @@ -108,7 +102,7 @@ def test_terminate(self): def test_send_signal(self): sig = signal.SIGKILL if POSIX else signal.SIGTERM - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.send_signal(sig) exit_sig = p.wait() @@ -116,7 +110,7 @@ def test_send_signal(self): if POSIX: self.assertEqual(exit_sig, -sig) # - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.send_signal(sig) with mock.patch('psutil.os.kill', @@ -124,7 +118,7 @@ def test_send_signal(self): with self.assertRaises(psutil.NoSuchProcess): p.send_signal(sig) # - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.send_signal(sig) with mock.patch('psutil.os.kill', @@ -140,7 +134,7 @@ def test_send_signal(self): def test_wait(self): # check exit code signal - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.kill() code = p.wait() @@ -150,7 +144,7 @@ def test_wait(self): self.assertEqual(code, signal.SIGTERM) self.assertFalse(p.is_running()) - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.terminate() code = p.wait() @@ -162,7 +156,7 @@ def test_wait(self): # check sys.exit() code code = "import time, sys; time.sleep(0.01); sys.exit(5);" - sproc = get_test_subprocess([PYTHON_EXE, "-c", code]) + sproc = self.get_test_subprocess([PYTHON_EXE, "-c", code]) p = psutil.Process(sproc.pid) self.assertEqual(p.wait(), 5) self.assertFalse(p.is_running()) @@ -171,13 +165,13 @@ def test_wait(self): # 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]) + sproc = self.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() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.name() self.assertRaises(psutil.TimeoutExpired, p.wait, 0.01) @@ -188,7 +182,7 @@ def test_wait(self): def test_wait_non_children(self): # Test wait() against a process which is not our direct # child. - p1, p2 = create_proc_children_pair() + p1, p2 = self.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 @@ -207,7 +201,7 @@ def test_wait_non_children(self): self.assertEqual(ret1, signal.SIGTERM) def test_wait_timeout_0(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) self.assertRaises(psutil.TimeoutExpired, p.wait, 0) p.kill() @@ -277,7 +271,7 @@ def test_cpu_num(self): self.assertIn(p.cpu_num(), range(psutil.cpu_count())) def test_create_time(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() now = time.time() p = psutil.Process(sproc.pid) create_time = p.create_time() @@ -444,7 +438,7 @@ def test_rlimit_get(self): @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit_set(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) self.assertEqual(p.rlimit(psutil.RLIMIT_NOFILE), (5, 5)) @@ -550,7 +544,7 @@ def test_threads(self): @skip_on_access_denied(only_if=MACOS) @unittest.skipIf(not HAS_THREADS, 'not supported') def test_threads_2(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) if OPENBSD: try: @@ -669,7 +663,7 @@ def test_memory_percent(self): p.memory_percent(memtype='uss') def test_is_running(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) assert p.is_running() assert p.is_running() @@ -679,7 +673,7 @@ def test_is_running(self): assert not p.is_running() def test_exe(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() exe = psutil.Process(sproc.pid).exe() try: self.assertEqual(exe, PYTHON_EXE) @@ -708,7 +702,7 @@ def test_exe(self): def test_cmdline(self): cmdline = [PYTHON_EXE, "-c", "import time; time.sleep(60)"] - sproc = get_test_subprocess(cmdline) + sproc = self.get_test_subprocess(cmdline) try: self.assertEqual(' '.join(psutil.Process(sproc.pid).cmdline()), ' '.join(cmdline)) @@ -729,12 +723,12 @@ def test_long_cmdline(self): testfn = get_testfn() create_exe(testfn) cmdline = [testfn] + (["0123456789"] * 20) - sproc = get_test_subprocess(cmdline) + sproc = self.get_test_subprocess(cmdline) p = psutil.Process(sproc.pid) self.assertEqual(p.cmdline(), cmdline) def test_name(self): - sproc = get_test_subprocess(PYTHON_EXE) + sproc = self.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) @@ -743,7 +737,7 @@ def test_name(self): def test_long_name(self): testfn = get_testfn(suffix="0123456789" * 2) create_exe(testfn) - sproc = get_test_subprocess(testfn) + sproc = self.get_test_subprocess(testfn) p = psutil.Process(sproc.pid) self.assertEqual(p.name(), os.path.basename(testfn)) @@ -760,7 +754,7 @@ def test_prog_w_funky_name(self): cmdline = [funky_path, "-c", "import time; [time.sleep(0.01) for x in range(3000)];" "arg1", "arg2", "", "arg3", ""] - sproc = get_test_subprocess(cmdline) + sproc = self.get_test_subprocess(cmdline) p = psutil.Process(sproc.pid) # ...in order to try to prevent occasional failures on travis if TRAVIS: @@ -844,7 +838,7 @@ def test_status(self): self.assertEqual(p.status(), psutil.STATUS_RUNNING) def test_username(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) username = p.username() if WINDOWS: @@ -856,14 +850,14 @@ def test_username(self): self.assertEqual(username, getpass.getuser()) def test_cwd(self): - sproc = get_test_subprocess() + sproc = self.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) + sproc = self.get_test_subprocess(cmd) p = psutil.Process(sproc.pid) call_until(p.cwd, "ret == os.path.dirname(os.getcwd())") @@ -912,7 +906,7 @@ def test_cpu_affinity(self): @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') def test_cpu_affinity_errs(self): - sproc = get_test_subprocess() + sproc = self.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) @@ -966,7 +960,7 @@ def test_open_files(self): # another process cmdline = "import time; f = open(r'%s', 'r'); time.sleep(60);" % testfn - sproc = get_test_subprocess([PYTHON_EXE, "-c", cmdline]) + sproc = self.get_test_subprocess([PYTHON_EXE, "-c", cmdline]) p = psutil.Process(sproc.pid) for x in range(100): @@ -1037,7 +1031,7 @@ def test_ppid(self): if hasattr(os, 'getppid'): self.assertEqual(psutil.Process().ppid(), os.getppid()) this_parent = os.getpid() - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) self.assertEqual(p.ppid(), this_parent) # no other process is supposed to have us as parent @@ -1055,7 +1049,7 @@ def test_ppid(self): def test_parent(self): this_parent = os.getpid() - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) self.assertEqual(p.parent().pid, this_parent) @@ -1063,13 +1057,13 @@ def test_parent(self): self.assertIsNone(psutil.Process(lowest_pid).parent()) def test_parent_multi(self): - p1, p2 = create_proc_children_pair() + p1, p2 = self.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() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) with mock.patch("psutil.Process", side_effect=psutil.NoSuchProcess(0, 'foo')): @@ -1078,7 +1072,7 @@ def test_parent_disappeared(self): @retry_on_failure() def test_parents(self): assert psutil.Process().parents() - p1, p2 = create_proc_children_pair() + p1, p2 = self.create_proc_children_pair() self.assertEqual(p1.parents()[0], psutil.Process()) self.assertEqual(p2.parents()[0], p1) self.assertEqual(p2.parents()[1], psutil.Process()) @@ -1091,7 +1085,7 @@ def test_children(self): # 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) + sproc = self.get_test_subprocess(creationflags=0) children1 = p.children() children2 = p.children(recursive=True) for children in (children1, children2): @@ -1102,7 +1096,7 @@ def test_children(self): 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() + p1, p2 = self.create_proc_children_pair() p = psutil.Process() self.assertEqual(p.children(), [p1]) self.assertEqual(p.children(recursive=True), [p1, p2]) @@ -1131,7 +1125,7 @@ def test_children_duplicates(self): self.assertEqual(len(c), len(set(c))) def test_parents_and_children(self): - p1, p2 = create_proc_children_pair() + p1, p2 = self.create_proc_children_pair() me = psutil.Process() # forward children = me.children(recursive=True) @@ -1144,7 +1138,7 @@ def test_parents_and_children(self): self.assertEqual(parents[1], me) def test_suspend_resume(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.suspend() for x in range(100): @@ -1241,7 +1235,7 @@ 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, p2 = self.create_proc_children_pair() p1_ppid = p1.ppid() p2_ppid = p2.ppid() self.assertNotEqual(p1_ppid, p2_ppid) @@ -1260,7 +1254,7 @@ def test_halfway_terminated_process(self): # >>> time.sleep(2) # time-consuming task, process dies in meantime # >>> proc.name() # Refers to Issue #15 - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.terminate() p.wait() @@ -1338,9 +1332,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): except (psutil.ZombieProcess, psutil.AccessDenied): pass - parent, zombie = create_zombie_proc() - self.addCleanup(terminate, zombie) - self.addCleanup(terminate, parent) # executed first + parent, zombie = self.create_zombie_proc() # A zombie process should always be instantiable zproc = psutil.Process(zombie.pid) # ...and at least its status always be querable @@ -1349,10 +1341,6 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): 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.cmdline) - if ret is not None: - self.assertEqual(ret, []) if hasattr(zproc, "rlimit"): succeed_or_zombie_p_exc(zproc.rlimit, psutil.RLIMIT_NOFILE) @@ -1504,9 +1492,8 @@ def test_weird_environ(self): """) path = get_testfn() create_exe(path, c_code=code) - sproc = get_test_subprocess([path], - stdin=subprocess.PIPE, - stderr=subprocess.PIPE) + sproc = self.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()) @@ -1578,7 +1565,8 @@ def test_zombie_process(self): class TestPopen(unittest.TestCase): """Tests for psutil.Popen class.""" - def tearDown(self): + @classmethod + def tearDownClass(cls): reap_children() def test_misc(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index f5d9f49e55..d0817459cf 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -35,7 +35,6 @@ from psutil.tests import CI_TESTING from psutil.tests import DEVNULL from psutil.tests import enum -from psutil.tests import get_test_subprocess from psutil.tests import get_testfn from psutil.tests import HAS_BATTERY from psutil.tests import HAS_CPU_FREQ @@ -45,8 +44,8 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import mock +from psutil.tests import ProcessTestCase from psutil.tests import PYPY -from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import TRAVIS from psutil.tests import UNICODE_SUFFIX @@ -58,14 +57,11 @@ # =================================================================== -class TestProcessAPIs(unittest.TestCase): - - def tearDown(self): - reap_children() +class TestProcessAPIs(ProcessTestCase): def test_process_iter(self): self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() self.assertIn(sproc.pid, [x.pid for x in psutil.process_iter()]) p = psutil.Process(sproc.pid) p.kill() @@ -107,9 +103,9 @@ def callback(p): pids.append(p.pid) pids = [] - sproc1 = get_test_subprocess() - sproc2 = get_test_subprocess() - sproc3 = get_test_subprocess() + sproc1 = self.get_test_subprocess() + sproc2 = self.get_test_subprocess() + sproc3 = self.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) @@ -160,16 +156,16 @@ def test(procs, callback): @unittest.skipIf(PYPY and WINDOWS, "get_test_subprocess() unreliable on PYPY + WINDOWS") def test_wait_procs_no_timeout(self): - sproc1 = get_test_subprocess() - sproc2 = get_test_subprocess() - sproc3 = get_test_subprocess() + sproc1 = self.get_test_subprocess() + sproc2 = self.get_test_subprocess() + sproc3 = self.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_pid_exists(self): - sproc = get_test_subprocess() + sproc = self.get_test_subprocess() self.assertTrue(psutil.pid_exists(sproc.pid)) p = psutil.Process(sproc.pid) p.kill() @@ -179,7 +175,6 @@ def test_pid_exists(self): 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: diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index fd32e0b7b9..b2be93ff4f 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -16,6 +16,7 @@ import os import socket import stat +import subprocess from psutil import FREEBSD from psutil import NETBSD @@ -29,15 +30,14 @@ 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 get_free_port -from psutil.tests import get_test_subprocess from psutil.tests import get_testfn from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import is_namedtuple from psutil.tests import mock +from psutil.tests import ProcessTestCase +from psutil.tests import PYTHON_EXE from psutil.tests import reap_children from psutil.tests import retry from psutil.tests import retry_on_failure @@ -209,10 +209,10 @@ def test_chdir(self): self.assertEqual(os.getcwd(), base) -class TestProcessUtils(unittest.TestCase): +class TestProcessUtils(ProcessTestCase): def test_reap_children(self): - subp = get_test_subprocess() + subp = self.get_test_subprocess() p = psutil.Process(subp.pid) assert p.is_running() reap_children() @@ -221,7 +221,7 @@ def test_reap_children(self): assert not psutil.tests._subprocesses_started def test_create_proc_children_pair(self): - p1, p2 = create_proc_children_pair() + p1, p2 = self.create_proc_children_pair() self.assertNotEqual(p1.pid, p2.pid) assert p1.is_running() assert p2.is_running() @@ -241,11 +241,38 @@ def test_create_proc_children_pair(self): @unittest.skipIf(not POSIX, "POSIX only") def test_create_zombie_proc(self): - parent, zombie = create_zombie_proc() - self.addCleanup(terminate, zombie) - self.addCleanup(terminate, parent) # executed first + parent, zombie = self.create_zombie_proc() self.assertEqual(zombie.status(), psutil.STATUS_ZOMBIE) + def test_terminate(self): + # by subprocess.Popen + p = self.get_test_subprocess() + terminate(p) + assert not psutil.pid_exists(p.pid) + terminate(p) + # by psutil.Process + p = psutil.Process(self.get_test_subprocess().pid) + terminate(p) + assert not psutil.pid_exists(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) + terminate(p) + assert not psutil.pid_exists(p.pid) + terminate(p) + # by PID + pid = self.get_test_subprocess().pid + terminate(pid) + assert not psutil.pid_exists(pid) + terminate(pid) + # zombie + parent, zombie = self.create_zombie_proc() + terminate(parent) + terminate(zombie) + assert not psutil.pid_exists(parent.pid) + assert not psutil.pid_exists(zombie.pid) + class TestNetUtils(unittest.TestCase): @@ -377,9 +404,9 @@ def fun(): def test_execute_w_exc(self): def fun(): 1 / 0 - self.execute_w_exc(ZeroDivisionError, fun, times=2000, warmup_times=20, - tolerance=4096, retry_for=3) - + # XXX: use high tolerance, occasional false positive + self.execute_w_exc(ZeroDivisionError, fun, times=2000, + warmup_times=20, tolerance=200 * 1024, retry_for=3) with self.assertRaises(ZeroDivisionError): self.execute_w_exc(OSError, fun) @@ -397,5 +424,5 @@ def test_is_namedtuple(self): if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) + from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index da6ec96ecb..ae9f7f51ed 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -98,12 +98,14 @@ from psutil.tests import HAS_ENVIRON from psutil.tests import HAS_MEMORY_MAPS from psutil.tests import INVALID_UNICODE_SUFFIX +from psutil.tests import ProcessTestCase 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 serialrun from psutil.tests import skip_on_access_denied +from psutil.tests import terminate from psutil.tests import TESTFN_PREFIX from psutil.tests import TRAVIS from psutil.tests import UNICODE_SUFFIX @@ -138,16 +140,18 @@ def subprocess_supports_unicode(suffix): if PY3: return True name = get_testfn(suffix=suffix) + sproc = None try: safe_rmpath(name) create_exe(name) - get_test_subprocess(cmd=[name]) + sproc = get_test_subprocess(cmd=[name]) except UnicodeEncodeError: return False else: return True finally: - reap_children() + if sproc is not None: + terminate(sproc) # =================================================================== @@ -174,7 +178,7 @@ def expect_exact_path_match(self): # --- def test_proc_exe(self): - subp = get_test_subprocess(cmd=[self.funky_name]) + subp = self.get_test_subprocess(cmd=[self.funky_name]) p = psutil.Process(subp.pid) exe = p.exe() self.assertIsInstance(exe, str) @@ -183,14 +187,14 @@ def test_proc_exe(self): os.path.normcase(self.funky_name)) def test_proc_name(self): - subp = get_test_subprocess(cmd=[self.funky_name]) + subp = self.get_test_subprocess(cmd=[self.funky_name]) 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]) + subp = self.get_test_subprocess(cmd=[self.funky_name]) p = psutil.Process(subp.pid) cmdline = p.cmdline() for part in cmdline: @@ -300,7 +304,7 @@ def normpath(p): @unittest.skipIf(ASCII_FS, "ASCII fs") @unittest.skipIf(not subprocess_supports_unicode(UNICODE_SUFFIX), "subprocess can't deal with unicode") -class TestFSAPIs(_BaseFSAPIsTests, unittest.TestCase): +class TestFSAPIs(_BaseFSAPIsTests, ProcessTestCase): """Test FS APIs with a funky, valid, UTF8 path name.""" funky_suffix = UNICODE_SUFFIX @@ -318,7 +322,7 @@ def expect_exact_path_match(self): @unittest.skipIf(PYPY, "unreliable on PYPY") @unittest.skipIf(not subprocess_supports_unicode(INVALID_UNICODE_SUFFIX), "subprocess can't deal with invalid unicode") -class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, unittest.TestCase): +class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, ProcessTestCase): """Test FS APIs with a funky, invalid path name.""" funky_suffix = INVALID_UNICODE_SUFFIX @@ -333,7 +337,7 @@ def expect_exact_path_match(cls): # =================================================================== -class TestNonFSAPIS(unittest.TestCase): +class TestNonFSAPIS(ProcessTestCase): """Unicode tests for non fs-related APIs.""" def tearDown(self): @@ -350,7 +354,7 @@ def test_proc_environ(self): env = os.environ.copy() funky_str = UNICODE_SUFFIX if PY3 else 'è' env['FUNNY_ARG'] = funky_str - sproc = get_test_subprocess(env=env) + sproc = self.get_test_subprocess(env=env) p = psutil.Process(sproc.pid) env = p.environ() for k, v in env.items(): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 27343ca2aa..a51c9c1522 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -26,11 +26,13 @@ from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY from psutil.tests import mock +from psutil.tests import ProcessTestCase from psutil.tests import PY3 from psutil.tests import PYPY from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh +from psutil.tests import terminate from psutil.tests import unittest @@ -298,7 +300,7 @@ def test_emulate_secs_left_unknown(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcess(TestCase): +class TestProcess(ProcessTestCase): @classmethod def setUpClass(cls): @@ -306,7 +308,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reap_children() + terminate(cls.pid) def test_issue_24(self): p = psutil.Process(0) @@ -382,7 +384,7 @@ def call(p, attr): @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 = psutil.Process(self.get_test_subprocess().pid) p.send_signal(signal.CTRL_C_EVENT) p.send_signal(signal.CTRL_BREAK_EVENT) p.kill() @@ -609,7 +611,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reap_children() + terminate(cls.pid) def test_memory_info(self): mem_1 = psutil.Process(self.pid).memory_info() @@ -675,7 +677,7 @@ def test_cmdline(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class RemoteProcessTestCase(TestCase): +class RemoteProcessTestCase(ProcessTestCase): """Certain functions require calling ReadProcessMemory. This trivially works when called on the current process. Check that this works on other processes, especially when they @@ -717,17 +719,18 @@ def setUpClass(cls): 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) + self.proc32 = self.get_test_subprocess( + [self.python32] + self.test_args, + env=env, + stdin=subprocess.PIPE) + self.proc64 = self.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): From 93c1138ca81b8bf5edd250fc470e1a5770dbdcdd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Apr 2020 00:12:40 +0200 Subject: [PATCH 0547/1714] refactoring / rename vars --- psutil/tests/test_process.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 178b991b8a..c87df1b180 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -82,33 +82,31 @@ def test_pid(self): def test_kill(self): sproc = self.get_test_subprocess() - test_pid = sproc.pid - p = psutil.Process(test_pid) + p = psutil.Process(sproc.pid) p.kill() - sig = p.wait() - self.assertFalse(psutil.pid_exists(test_pid)) + code = p.wait() + self.assertFalse(psutil.pid_exists(sproc.pid)) if POSIX: - self.assertEqual(sig, -signal.SIGKILL) + self.assertEqual(code, -signal.SIGKILL) def test_terminate(self): sproc = self.get_test_subprocess() - test_pid = sproc.pid - p = psutil.Process(test_pid) + p = psutil.Process(sproc.pid) p.terminate() - sig = p.wait() - self.assertFalse(psutil.pid_exists(test_pid)) + code = p.wait() + self.assertFalse(psutil.pid_exists(sproc.pid)) if POSIX: - self.assertEqual(sig, -signal.SIGTERM) + self.assertEqual(code, -signal.SIGTERM) def test_send_signal(self): sig = signal.SIGKILL if POSIX else signal.SIGTERM sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) p.send_signal(sig) - exit_sig = p.wait() + code = p.wait() self.assertFalse(psutil.pid_exists(p.pid)) if POSIX: - self.assertEqual(exit_sig, -sig) + self.assertEqual(code, -sig) # sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) From 997bc0d92935b8033e28b79d8dd0595ba4462960 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Apr 2020 19:23:08 -0700 Subject: [PATCH 0548/1714] Parallel build (#1741) --- .ci/travis/run.sh | 2 +- .cirrus.yml | 4 ++-- HISTORY.rst | 2 ++ Makefile | 17 +++++++++++------ psutil/tests/test_testutils.py | 11 ++++++----- scripts/internal/winmake.py | 11 ++++++----- 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 562564b04f..879e78a60c 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -22,7 +22,7 @@ python setup.py develop if [[ $PYVER == '2.7' ]] && [[ "$(uname -s)" != 'Darwin' ]]; then PSUTIL_TESTING=1 python -Wa -m coverage run psutil/tests/runner.py else - PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py --parallel + PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py fi if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then diff --git a/.cirrus.yml b/.cirrus.yml index 129644c517..4b8676bc03 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -9,7 +9,7 @@ freebsd_13_py3_task: - python3 -m pip install --user setuptools concurrencytest - make clean - make install - - make test-parallel + - make test - make test-memleaks - make print-access-denied - make print-api-speed @@ -25,7 +25,7 @@ freebsd_11_py2_task: - python2.7 -m pip install --user setuptools ipaddress mock concurrencytest - make clean - make install - - make test-parallel + - make test - make test-memleaks - make print-access-denied - make print-api-speed diff --git a/HISTORY.rst b/HISTORY.rst index 7b992a8c9c..c375934c85 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,8 @@ XXXX-XX-XX - 1729_: parallel tests on UNIX (make test-parallel). - 1736_: psutil.Popen now inherits from subprocess.Popen instead of psutil.Process. Also, wait(timeout=...) parameter is backported to Python 2.7. +- 1741_: "make build/install" is now run in parallel and it's about 15% faster + on UNIX. **Bug fixes** diff --git a/Makefile b/Makefile index 1fad22f516..89433e15b2 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,12 @@ PY2_DEPS = \ unittest2 DEPS += `$(PYTHON) -c \ "import sys; print('$(PY2_DEPS)' if sys.version_info[0] == 2 else '')"` +# "python3 setup.py build" can be parallelized on Python >= 3.6. +BUILD_OPTS = `$(PYTHON) -c \ + "import sys, os; \ + py36 = sys.version_info[:2] >= (3, 6); \ + cpus = os.cpu_count() or 1 if py36 else 1; \ + print('--parallel %s' % cpus if cpus > 1 else '')"` # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` @@ -62,14 +68,13 @@ clean: ## Remove all build files. _: -build: _ ## Compile without installing. +build: _ ## Compile (in parallel) without installing. # make sure setuptools is installed (needed for 'develop' / edit mode) $(PYTHON) -c "import setuptools" - PYTHONWARNINGS=all $(PYTHON) setup.py build - @# copies compiled *.so files in ./psutil directory in order to allow - @# "import psutil" when using the interactive interpreter from within - @# this directory. - PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i + @# "build_ext -i" copies compiled *.so files in ./psutil directory in order + @# to allow "import psutil" when using the interactive interpreter from + @# within this directory. + PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index b2be93ff4f..85b61aea35 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -267,11 +267,12 @@ def test_terminate(self): assert not psutil.pid_exists(pid) terminate(pid) # zombie - parent, zombie = self.create_zombie_proc() - terminate(parent) - terminate(zombie) - assert not psutil.pid_exists(parent.pid) - assert not psutil.pid_exists(zombie.pid) + if POSIX: + parent, zombie = self.create_zombie_proc() + terminate(parent) + terminate(zombie) + assert not psutil.pid_exists(parent.pid) + assert not psutil.pid_exists(zombie.pid) class TestNetUtils(unittest.TestCase): diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c9aa2952b9..f39d45acfc 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -227,8 +227,13 @@ def build(): # edit mode). sh('%s -c "import setuptools"' % PYTHON) + # "build_ext -i" copies compiled *.pyd files in ./psutil directory in + # order to allow "import psutil" when using the interactive interpreter + # from within psutil root directory. + cmd = [PYTHON, "setup.py", "build_ext", "-i"] + if sys.version_info[:2] >= (3, 6) and os.cpu_count() or 1 > 1: + cmd += ['--parallel', str(os.cpu_count())] # Print coloured warnings in real time. - cmd = [PYTHON, "setup.py", "build"] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) try: for line in iter(p.stdout.readline, b''): @@ -250,10 +255,6 @@ def build(): p.terminate() p.wait() - # Copies compiled *.pyd files in ./psutil directory in order to - # allow "import psutil" when using the interactive interpreter - # from within this directory. - sh("%s setup.py build_ext -i" % PYTHON) # Make sure it actually worked. sh('%s -c "import psutil"' % PYTHON) win_colorprint("build + import successful", GREEN) From c5095d7366d806583df565b364b71cf2aba59fed Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Apr 2020 12:20:05 +0200 Subject: [PATCH 0549/1714] See: #1709: allow per-test parallelization Refactor test runner.py with a saner unittest-based class hierarchy so that --parallel args affects all test suites (all, by-name, failed). Also change Makefile which now can be used like this: make test-process ARGS=--parallel --- .ci/travis/run.sh | 2 +- .cirrus.yml | 4 +- HISTORY.rst | 2 + Makefile | 49 ++++--- psutil/tests/runner.py | 238 +++++++++++++++++---------------- psutil/tests/test_osx.py | 40 +++--- psutil/tests/test_testutils.py | 11 +- scripts/internal/winmake.py | 11 +- 8 files changed, 187 insertions(+), 170 deletions(-) diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 562564b04f..879e78a60c 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -22,7 +22,7 @@ python setup.py develop if [[ $PYVER == '2.7' ]] && [[ "$(uname -s)" != 'Darwin' ]]; then PSUTIL_TESTING=1 python -Wa -m coverage run psutil/tests/runner.py else - PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py --parallel + PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py fi if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then diff --git a/.cirrus.yml b/.cirrus.yml index 129644c517..4b8676bc03 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -9,7 +9,7 @@ freebsd_13_py3_task: - python3 -m pip install --user setuptools concurrencytest - make clean - make install - - make test-parallel + - make test - make test-memleaks - make print-access-denied - make print-api-speed @@ -25,7 +25,7 @@ freebsd_11_py2_task: - python2.7 -m pip install --user setuptools ipaddress mock concurrencytest - make clean - make install - - make test-parallel + - make test - make test-memleaks - make print-access-denied - make print-api-speed diff --git a/HISTORY.rst b/HISTORY.rst index 7b992a8c9c..c375934c85 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,8 @@ XXXX-XX-XX - 1729_: parallel tests on UNIX (make test-parallel). - 1736_: psutil.Popen now inherits from subprocess.Popen instead of psutil.Process. Also, wait(timeout=...) parameter is backported to Python 2.7. +- 1741_: "make build/install" is now run in parallel and it's about 15% faster + on UNIX. **Bug fixes** diff --git a/Makefile b/Makefile index 1fad22f516..f6e8ba7098 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,12 @@ # To use a specific Python version run: "make install PYTHON=python3.3" # You can set the variables below from the command line. +# Configurable. PYTHON = python3 -TSCRIPT = psutil/tests/runner.py ARGS = -# List of nice-to-have dev libs. +TSCRIPT = psutil/tests/runner.py + +# Internal. DEPS = \ argparse \ check-manifest \ @@ -26,6 +28,12 @@ PY2_DEPS = \ unittest2 DEPS += `$(PYTHON) -c \ "import sys; print('$(PY2_DEPS)' if sys.version_info[0] == 2 else '')"` +# "python3 setup.py build" can be parallelized on Python >= 3.6. +BUILD_OPTS = `$(PYTHON) -c \ + "import sys, os; \ + py36 = sys.version_info[:2] >= (3, 6); \ + cpus = os.cpu_count() or 1 if py36 else 1; \ + print('--parallel %s' % cpus if cpus > 1 else '')"` # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` @@ -62,14 +70,13 @@ clean: ## Remove all build files. _: -build: _ ## Compile without installing. +build: _ ## Compile (in parallel) without installing. # make sure setuptools is installed (needed for 'develop' / edit mode) $(PYTHON) -c "import setuptools" - PYTHONWARNINGS=all $(PYTHON) setup.py build - @# copies compiled *.so files in ./psutil directory in order to allow - @# "import psutil" when using the interactive interpreter from within - @# this directory. - PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i + @# "build_ext -i" copies compiled *.so files in ./psutil directory in order + @# to allow "import psutil" when using the interactive interpreter from + @# within this directory. + PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. @@ -111,51 +118,51 @@ setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). test: ## Run all tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) test-parallel: ## Run all tests in parallel. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) --parallel + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --parallel test-process: ## Run process-related API tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_process.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process.py test-system: ## Run system-related API tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_system.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_system.py test-misc: ## Run miscellaneous tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_misc.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_misc.py test-testutils: ## Run test utils tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_testutils.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_testutils.py test-unicode: ## Test APIs dealing with strings. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_unicode.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_unicode.py test-contracts: ## APIs sanity tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_contracts.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_contracts.py test-connections: ## Test net_connections() and Process.connections(). ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_connections.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_connections.py test-posix: ## POSIX specific tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_posix.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_posix.py test-platform: ## Run specific platform tests only. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_memory_leaks.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memory_leaks.py test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSystemAPIs ${MAKE} install @@ -163,7 +170,7 @@ test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSyste test-failed: ## Re-run tests which failed on last run ${MAKE} install - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) --last-failed + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --last-failed test-coverage: ## Run test coverage. ${MAKE} install diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 8e4c872a22..ef135c8d12 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -10,6 +10,14 @@ - 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 @@ -20,9 +28,6 @@ import textwrap import time import unittest -from unittest import TestResult -from unittest import TextTestResult -from unittest import TextTestRunner try: import ctypes except ImportError: @@ -37,6 +42,7 @@ 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 APPVEYOR from psutil.tests import import_module_by_path from psutil.tests import reap_children @@ -44,61 +50,15 @@ 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' NWORKERS = psutil.cpu_count() or 1 +HERE = os.path.abspath(os.path.dirname(__file__)) loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase -# ===================================================================== -# --- unittest subclasses -# ===================================================================== - - -class ColouredResult(TextTestResult): - - def _print_color(self, s, color, bold=False): - print_color(s, color, bold=bold, file=self.stream) - - def addSuccess(self, test): - TestResult.addSuccess(self, test) - self._print_color("OK", "green") - - def addError(self, test, err): - TestResult.addError(self, test, err) - self._print_color("ERROR", "red", bold=True) - - def addFailure(self, test, err): - TestResult.addFailure(self, test, err) - self._print_color("FAIL", "red") - - def addSkip(self, test, reason): - TestResult.addSkip(self, test, reason) - self._print_color("skipped: %s" % reason.strip(), "brown") - - def printErrorList(self, flavour, errors): - flavour = hilite(flavour, "red", bold=flavour == 'ERROR') - TextTestResult.printErrorList(self, flavour, errors) - - -class ColouredTextRunner(TextTestRunner): - resultclass = ColouredResult - - def _makeResult(self): - # Store result instance so that it can be accessed on - # KeyboardInterrupt. - self.result = TextTestRunner._makeResult(self) - return self.result - - -# ===================================================================== -# --- public API -# ===================================================================== - - -class SuiteLoader: +class TestLoader: testdir = HERE skip_files = ['test_memory_leaks.py'] @@ -130,17 +90,6 @@ def all(self): suite.addTest(test) return suite - def parallel(self): - serial = unittest.TestSuite() - parallel = unittest.TestSuite() - for obj in self._iter_testmod_classes(): - test = loadTestsFromTestCase(obj) - if getattr(obj, '_serialrun', False): - serial.addTest(test) - else: - parallel.addTest(test) - return (serial, parallel) - def last_failed(self): # ...from previously failed test run suite = unittest.TestSuite() @@ -154,22 +103,57 @@ def last_failed(self): return suite def from_name(self, name): - suite = unittest.TestSuite() if name.endswith('.py'): name = os.path.splitext(os.path.basename(name))[0] - suite.addTest(unittest.defaultTestLoader.loadTestsFromName(name)) - return suite + return unittest.defaultTestLoader.loadTestsFromName(name) + + +class ColouredResult(unittest.TextTestResult): + + def _print_color(self, s, color, bold=False): + print_color(s, color, bold=bold, file=self.stream) + + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + self._print_color("OK", "green") + + def addError(self, test, err): + unittest.TestResult.addError(self, test, err) + self._print_color("ERROR", "red", bold=True) + + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + self._print_color("FAIL", "red") + + def addSkip(self, test, reason): + unittest.TestResult.addSkip(self, test, reason) + self._print_color("skipped: %s" % reason.strip(), "brown") + + def printErrorList(self, flavour, errors): + flavour = hilite(flavour, "red", bold=flavour == 'ERROR') + super().printErrorList(flavour, errors) -class Runner: +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. + """ + + if term_supports_colors() and not APPVEYOR: + resultclass = ColouredResult + else: + resultclass = unittest.TextTestResult - def __init__(self): - self.loader = SuiteLoader() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.failed_tnames = set() - if term_supports_colors() and not APPVEYOR: - self.runner = ColouredTextRunner(verbosity=VERBOSITY) - else: - self.runner = TextTestRunner(verbosity=VERBOSITY) + + 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: @@ -185,7 +169,7 @@ def _save_result(self, result): def _run(self, suite): try: - result = self.runner.run(suite) + result = super().run(suite) except (KeyboardInterrupt, SystemExit): result = self.runner.result result.printErrors() @@ -194,32 +178,25 @@ def _run(self, suite): self._save_result(result) return result - def _finalize(self, success): + def _exit(self, success): if success: + print_color("SUCCESS", "green", bold=True) safe_rmpath(FAILED_TESTS_FNAME) + sys.exit(0) else: + print_color("FAILED", "red", bold=True) self._write_last_failed() - print_color("FAILED", "red") sys.exit(1) - def run(self, suite=None): - """Run tests serially (1 process).""" - if suite is None: - suite = self.loader.all() + def run(self, suite): result = self._run(suite) - self._finalize(result.wasSuccessful()) + self._exit(result.wasSuccessful()) - def run_last_failed(self): - """Run tests which failed in the last run.""" - self.run(self.loader.last_failed()) - def run_from_name(self, name): - """Run test by name, e.g.: - "test_linux.TestSystemCPUStats.test_ctx_switches" - """ - self.run(self.loader.from_name(name)) +class ParallelRunner(ColouredTextRunner): - def _parallelize_suite(self, suite): + @staticmethod + def _parallelize(suite): def fdopen(*args, **kwds): stream = orig_fdopen(*args, **kwds) atexit.register(stream.close) @@ -232,18 +209,33 @@ def fdopen(*args, **kwds): forker = concurrencytest.fork_for_tests(NWORKERS) return concurrencytest.ConcurrentTestSuite(suite, forker) - def run_parallel(self): - """Run tests in parallel.""" - ser_suite, par_suite = self.loader.parallel() - par_suite = self._parallelize_suite(par_suite) + @staticmethod + def _split_suite(suite): + serial = unittest.TestSuite() + parallel = unittest.TestSuite() + for test in suite._tests: + if test.countTestCases() == 0: + continue + test_class = test._tests[0].__class__ + if getattr(test_class, '_serialrun', False): + serial.addTest(loadTestsFromTestCase(test_class)) + else: + parallel.addTest(loadTestsFromTestCase(test_class)) + return (serial, parallel) + + def run(self, suite): + ser_suite, par_suite = self._split_suite(suite) + par_suite = self._parallelize(par_suite) # run parallel - print("starting parallel tests using %s workers" % NWORKERS) + print_color("starting parallel tests using %s workers" % NWORKERS, + "green", bold=True) t = time.time() par = self._run(par_suite) par_elapsed = time.time() - t - # cleanup workers and test subprocesses + # 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: @@ -256,7 +248,7 @@ def run_parallel(self): ser_elapsed = time.time() - t # print - if not par.wasSuccessful(): + 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, @@ -264,7 +256,6 @@ def run_parallel(self): ser_fails, ser_errs, ser_skips = map(len, (ser.failures, ser.errors, ser.skipped)) - print("-" * 70) print(textwrap.dedent(""" +----------+----------+----------+----------+----------+----------+ | | total | failures | errors | skipped | time | @@ -278,14 +269,33 @@ def run_parallel(self): 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._finalize(ok) + self._exit(ok) + + +def get_runner(parallel=False): + def warn(msg): + print_color(msg + " Running serial tests instead.", + "red", file=sys.stderr) + 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) -runner = Runner() -run_from_name = runner.run_from_name +# Used by test_*,py modules. +def run_from_name(name): + suite = TestLoader().from_name(name) + runner = get_runner() + runner.run(suite) -def _setup(): +def setup(): 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' @@ -293,7 +303,7 @@ def _setup(): def main(): - _setup() + setup() usage = "python3 -m psutil.tests [opts] [test-name]" parser = optparse.OptionParser(usage=usage, description="run unit tests") parser.add_option("--last-failed", @@ -307,26 +317,22 @@ def main(): if not opts.last_failed: safe_rmpath(FAILED_TESTS_FNAME) - # test-by-name + # loader + loader = TestLoader() if args: if len(args) > 1: parser.print_usage() return sys.exit(1) - return runner.run_from_name(args[0]) + else: + suite = loader.from_name(args[0]) elif opts.last_failed: - runner.run_last_failed() - elif not opts.parallel: - runner.run() - # parallel - elif concurrencytest is None: - print_color("concurrencytest module is not installed; " - "running serial tests instead", "red") - runner.run() - elif NWORKERS == 1: - print_color("only 1 CPU; running serial tests instead", "red") - runner.run() + suite = loader.last_failed() else: - runner.run_parallel() + suite = loader.all() + + # runner + runner = get_runner(opts.parallel) + runner.run(suite) if __name__ == '__main__': diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 4fa8d0af31..4df6a884be 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -100,63 +100,63 @@ def test_process_create_time(self): time.strftime("%Y", time.localtime(start_psutil))) +# TODO: probably needs removal (duplicate) @unittest.skipIf(not MACOS, "MACOS only") class TestZombieProcessAPIs(unittest.TestCase): @classmethod def setUpClass(cls): cls.parent, cls.zombie = create_zombie_proc() - cls.p = psutil.Process(cls.zombie.pid) @classmethod def tearDownClass(cls): + terminate(cls.parent) terminate(cls.zombie) - terminate(cls.parent) # executed first 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() + self.assertEqual(self.zombie.status(), psutil.STATUS_ZOMBIE) + self.zombie.ppid() + self.zombie.uids() + self.zombie.gids() + self.zombie.terminal() + self.zombie.create_time() def test_exe(self): - self.assertRaises(psutil.ZombieProcess, self.p.exe) + self.assertRaises(psutil.ZombieProcess, self.zombie.exe) def test_cmdline(self): - self.assertRaises(psutil.ZombieProcess, self.p.cmdline) + self.assertRaises(psutil.ZombieProcess, self.zombie.cmdline) def test_environ(self): - self.assertRaises(psutil.ZombieProcess, self.p.environ) + self.assertRaises(psutil.ZombieProcess, self.zombie.environ) def test_cwd(self): - self.assertRaises(psutil.ZombieProcess, self.p.cwd) + self.assertRaises(psutil.ZombieProcess, self.zombie.cwd) def test_memory_full_info(self): - self.assertRaises(psutil.ZombieProcess, self.p.memory_full_info) + self.assertRaises(psutil.ZombieProcess, self.zombie.memory_full_info) def test_cpu_times(self): - self.assertRaises(psutil.ZombieProcess, self.p.cpu_times) + self.assertRaises(psutil.ZombieProcess, self.zombie.cpu_times) def test_num_ctx_switches(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_ctx_switches) + self.assertRaises(psutil.ZombieProcess, self.zombie.num_ctx_switches) def test_num_threads(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_threads) + self.assertRaises(psutil.ZombieProcess, self.zombie.num_threads) def test_open_files(self): - self.assertRaises(psutil.ZombieProcess, self.p.open_files) + self.assertRaises(psutil.ZombieProcess, self.zombie.open_files) def test_connections(self): - self.assertRaises(psutil.ZombieProcess, self.p.connections) + self.assertRaises(psutil.ZombieProcess, self.zombie.connections) def test_num_fds(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_fds) + self.assertRaises(psutil.ZombieProcess, self.zombie.num_fds) def test_threads(self): self.assertRaises((psutil.ZombieProcess, psutil.AccessDenied), - self.p.threads) + self.zombie.threads) @unittest.skipIf(not MACOS, "MACOS only") diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index b2be93ff4f..85b61aea35 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -267,11 +267,12 @@ def test_terminate(self): assert not psutil.pid_exists(pid) terminate(pid) # zombie - parent, zombie = self.create_zombie_proc() - terminate(parent) - terminate(zombie) - assert not psutil.pid_exists(parent.pid) - assert not psutil.pid_exists(zombie.pid) + if POSIX: + parent, zombie = self.create_zombie_proc() + terminate(parent) + terminate(zombie) + assert not psutil.pid_exists(parent.pid) + assert not psutil.pid_exists(zombie.pid) class TestNetUtils(unittest.TestCase): diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c9aa2952b9..f39d45acfc 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -227,8 +227,13 @@ def build(): # edit mode). sh('%s -c "import setuptools"' % PYTHON) + # "build_ext -i" copies compiled *.pyd files in ./psutil directory in + # order to allow "import psutil" when using the interactive interpreter + # from within psutil root directory. + cmd = [PYTHON, "setup.py", "build_ext", "-i"] + if sys.version_info[:2] >= (3, 6) and os.cpu_count() or 1 > 1: + cmd += ['--parallel', str(os.cpu_count())] # Print coloured warnings in real time. - cmd = [PYTHON, "setup.py", "build"] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) try: for line in iter(p.stdout.readline, b''): @@ -250,10 +255,6 @@ def build(): p.terminate() p.wait() - # Copies compiled *.pyd files in ./psutil directory in order to - # allow "import psutil" when using the interactive interpreter - # from within this directory. - sh("%s setup.py build_ext -i" % PYTHON) # Make sure it actually worked. sh('%s -c "import psutil"' % PYTHON) win_colorprint("build + import successful", GREEN) From 3480e1b05f3e98744a1b6aa6fe286caac86e6bbd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Apr 2020 14:45:08 -0700 Subject: [PATCH 0550/1714] Per-test file cleanup and new PsutilTestCase (#1743) Test files/dirs are now removed after each test. when invoked via self.get_testfn(). Until now test files were stored in a global variable and were removed at process exit, via atexit.register(), but this didn't work with parallel tests because the fork()ed workers use os._exit(0), preventing cleanup functions to run. All test classes now inherit from PsutilTestCase class, which provides the most important methods requiring an automatic cleanup (get_test_subprocess() and others). --- Makefile | 2 +- psutil/tests/__init__.py | 139 ++++++++++++++++-------------- psutil/tests/test_aix.py | 3 +- psutil/tests/test_bsd.py | 11 +-- psutil/tests/test_connections.py | 23 +++-- psutil/tests/test_contracts.py | 11 +-- psutil/tests/test_linux.py | 59 +++++++------ psutil/tests/test_memory_leaks.py | 6 -- psutil/tests/test_misc.py | 7 +- psutil/tests/test_osx.py | 9 +- psutil/tests/test_posix.py | 5 +- psutil/tests/test_process.py | 99 ++++++++++----------- psutil/tests/test_sunos.py | 3 +- psutil/tests/test_system.py | 19 ++-- psutil/tests/test_testutils.py | 65 +++++++------- psutil/tests/test_unicode.py | 38 ++++---- psutil/tests/test_windows.py | 19 ++-- scripts/internal/winmake.py | 13 ++- 18 files changed, 262 insertions(+), 269 deletions(-) mode change 100755 => 100644 psutil/tests/test_system.py mode change 100755 => 100644 psutil/tests/test_testutils.py mode change 100755 => 100644 psutil/tests/test_unicode.py diff --git a/Makefile b/Makefile index f6e8ba7098..86727ca7c5 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ clean: ## Remove all build files. rm -rf \ *.core \ *.egg-info \ - *\$testfn* \ + *\@psutil-* \ .coverage \ .failed-tests.txt \ .tox \ diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index fc4bff01c9..8ce76304c5 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -30,7 +30,6 @@ import textwrap import threading import time -import traceback import warnings from socket import AF_INET from socket import AF_INET6 @@ -88,7 +87,7 @@ 'ThreadTask' # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_on_failure', 'TestMemoryLeak', 'ProcessTestCase', + 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', # install utils 'install_pip', 'install_test_deps', # fs utils @@ -220,7 +219,6 @@ def attempt(exe): _subprocesses_started = set() _pids_started = set() -_testfiles_created = set() # =================================================================== @@ -309,14 +307,17 @@ def get_test_subprocess(cmd=None, **kwds): kwds.setdefault("creationflags", CREATE_NO_WINDOW) if cmd is None: testfn = get_testfn() - 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) + try: + 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) + finally: + safe_rmpath(testfn) else: sproc = subprocess.Popen(cmd, **kwds) _subprocesses_started.add(sproc) @@ -332,30 +333,35 @@ def create_proc_children_pair(): The 2 processes are fully initialized and will live for 60 secs and are registered for cleanup on reap_children(). """ - # must be relative on Windows - testfn = os.path.basename(get_testfn(dir=os.getcwd())) - 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() - """ % (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 = pyrun(s, creationflags=0) - else: - subp = pyrun(s) - child1 = psutil.Process(subp.pid) - child2_pid = int(wait_for_file(testfn, delete=True, empty=False)) - _pids_started.add(child2_pid) - child2 = psutil.Process(child2_pid) - return (child1, child2) + 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 create_zombie_proc(): @@ -381,10 +387,11 @@ def create_zombie_proc(): pid = bytes(str(os.getpid()), 'ascii') s.sendall(pid) """ % unix_file) + tfile = None sock = bind_unix_socket(unix_file) - with contextlib.closing(sock): + try: sock.settimeout(GLOBAL_TIMEOUT) - parent = pyrun(src) + parent, tfile = pyrun(src) conn, _ = sock.accept() try: select.select([conn.fileno()], [], [], GLOBAL_TIMEOUT) @@ -395,21 +402,31 @@ def create_zombie_proc(): 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. + Returns a subprocess.Popen instance and the test file where the source + code was written. """ kwds.setdefault("stdout", None) kwds.setdefault("stderr", None) - with open(get_testfn(), 'wt') as f: - f.write(src) - f.flush() + srcfile = get_testfn() + try: + with open(srcfile, 'wt') as f: + f.write(src) subp = get_test_subprocess([PYTHON_EXE, f.name], **kwds) wait_for_pid(subp.pid) - return subp + return (subp, srcfile) + except Exception: + safe_rmpath(srcfile) + raise @_reap_children_on_err @@ -772,7 +789,6 @@ def chdir(dirname): def create_exe(outpath, c_code=None): """Creates an executable file in the given location.""" assert not os.path.exists(outpath), outpath - _testfiles_created.add(outpath) if c_code: if not which("gcc"): raise ValueError("gcc is not installed") @@ -811,7 +827,6 @@ def get_testfn(suffix="", dir=None): prefix = "%s%.9f-" % (TESTFN_PREFIX, timer()) name = tempfile.mktemp(prefix=prefix, suffix=suffix, dir=dir) if not os.path.exists(name): # also include dirs - _testfiles_created.add(name) return os.path.realpath(name) # needed for OSX @@ -841,11 +856,16 @@ def __str__(self): unittest.TestCase = TestCase -class ProcessTestCase(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 get_test_subprocess(self, *args, **kwds): sproc = get_test_subprocess(*args, **kwds) self.addCleanup(terminate, sproc) @@ -853,8 +873,8 @@ def get_test_subprocess(self, *args, **kwds): def create_proc_children_pair(self): child1, child2 = create_proc_children_pair() - self.addCleanup(terminate, child1) self.addCleanup(terminate, child2) + self.addCleanup(terminate, child1) # executed first return (child1, child2) def create_zombie_proc(self): @@ -864,18 +884,14 @@ def create_zombie_proc(self): return (parent, zombie) def pyrun(self, *args, **kwds): - sproc = pyrun(*args, **kwds) - self.addCleanup(terminate, sproc) + sproc, srcfile = pyrun(*args, **kwds) + self.addCleanup(safe_rmpath, srcfile) + self.addCleanup(terminate, sproc) # executed first return sproc - def get_testfn(self, suffix="", dir=None): - fname = get_testfn(suffix=suffix, dir=suffix) - self.addCleanup(safe_rmpath(fname)) - return fname - @unittest.skipIf(PYPY, "unreliable on PYPY") -class TestMemoryLeak(unittest.TestCase): +class TestMemoryLeak(PsutilTestCase): """Test framework class for detecting function memory leaks (typically functions implemented in C). It does so by calling a function many times, and checks whether the @@ -1149,14 +1165,15 @@ def create_sockets(): fname2 = get_testfn() 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() + for fname in (fname1, fname2): + if fname is not None: + safe_rmpath(fname) def check_net_address(addr, family): @@ -1309,16 +1326,6 @@ def copyload_shared_lib(suffix=""): atexit.register(DEVNULL.close) -@atexit.register -def cleanup_test_files(): - while _testfiles_created: - path = _testfiles_created.pop() - try: - safe_rmpath(path) - except Exception: - traceback.print_exc() - - # this is executed first @atexit.register def cleanup_test_procs(): diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 889526adff..caf20357fe 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -11,13 +11,14 @@ import re from psutil import AIX +from psutil.tests import PsutilTestCase from psutil.tests import sh from psutil.tests import unittest import psutil @unittest.skipIf(not AIX, "AIX only") -class AIXSpecificTestCase(unittest.TestCase): +class AIXSpecificTestCase(PsutilTestCase): def test_virtual_memory(self): out = sh('/usr/bin/svmon -O unit=KB') diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 427b9219c9..598ec0bf9d 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -22,6 +22,7 @@ from psutil import OPENBSD from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY +from psutil.tests import PsutilTestCase from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import SYSMEM_TOLERANCE @@ -72,7 +73,7 @@ def muse(field): @unittest.skipIf(not BSD, "BSD only") -class BSDTestCase(unittest.TestCase): +class BSDTestCase(PsutilTestCase): """Generic tests common to all BSD variants.""" @classmethod @@ -148,7 +149,7 @@ def test_net_if_stats(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") -class FreeBSDProcessTestCase(unittest.TestCase): +class FreeBSDPsutilTestCase(PsutilTestCase): @classmethod def setUpClass(cls): @@ -238,7 +239,7 @@ def test_cpu_times(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") -class FreeBSDSystemTestCase(unittest.TestCase): +class FreeBSDSystemTestCase(PsutilTestCase): @staticmethod def parse_swapinfo(): @@ -479,7 +480,7 @@ def test_sensors_temperatures_against_sysctl(self): @unittest.skipIf(not OPENBSD, "OPENBSD only") -class OpenBSDTestCase(unittest.TestCase): +class OpenBSDTestCase(PsutilTestCase): def test_boot_time(self): s = sysctl('kern.boottime') @@ -494,7 +495,7 @@ def test_boot_time(self): @unittest.skipIf(not NETBSD, "NETBSD only") -class NetBSDTestCase(unittest.TestCase): +class NetBSDTestCase(PsutilTestCase): @staticmethod def parse_meminfo(look_for): diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 9c1bbe8e5d..7fcc2f9826 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -37,9 +37,8 @@ from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import get_free_port -from psutil.tests import get_testfn from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import ProcessTestCase +from psutil.tests import PsutilTestCase from psutil.tests import serialrun from psutil.tests import skip_on_access_denied from psutil.tests import SKIP_SYSCONS @@ -55,7 +54,7 @@ @serialrun -class _ConnTestCase(ProcessTestCase): +class _ConnTestCase(PsutilTestCase): def setUp(self): if not (NETBSD or FREEBSD): @@ -267,14 +266,16 @@ def test_udp_v6(self): @unittest.skipIf(not POSIX, 'POSIX only') def test_unix_tcp(self): - with closing(bind_unix_socket(get_testfn(), type=SOCK_STREAM)) as sock: + testfn = self.get_testfn() + with closing(bind_unix_socket(testfn, 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 closing(bind_unix_socket(get_testfn(), type=SOCK_STREAM)) as sock: + testfn = self.get_testfn() + with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock: conn = self.check_socket(sock) assert not conn.raddr self.assertEqual(conn.status, psutil.CONN_NONE) @@ -310,7 +311,7 @@ def test_tcp(self): @unittest.skipIf(not POSIX, 'POSIX only') def test_unix(self): - testfn = get_testfn() + testfn = self.get_testfn() server, client = unix_socketpair(testfn) try: cons = thisproc.connections(kind='unix') @@ -433,7 +434,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): """) # must be relative on Windows - testfile = os.path.basename(get_testfn(dir=os.getcwd())) + testfile = os.path.basename(self.get_testfn(dir=os.getcwd())) tcp4_template = string.Template(tcp_template).substitute( family=int(AF_INET), addr="127.0.0.1", testfn=testfile) udp4_template = string.Template(udp_template).substitute( @@ -582,16 +583,14 @@ def test_multi_sockets_procs(self): times = 10 fnames = [] for i in range(times): - fname = get_testfn() + fname = self.get_testfn() fnames.append(fname) src = textwrap.dedent("""\ import time, os - from psutil.tests import create_sockets, cleanup_test_files + from psutil.tests import create_sockets with create_sockets(): with open(r'%s', 'w') as f: f.write("hello") - # 2 UNIX test socket files are created - cleanup_test_files() time.sleep(60) """ % fname) sproc = self.pyrun(src) @@ -610,7 +609,7 @@ def test_multi_sockets_procs(self): self.assertEqual(len(p.connections('all')), expected) -class TestMisc(unittest.TestCase): +class TestMisc(PsutilTestCase): def test_connection_constants(self): ints = [] diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 69bb0b2f91..a80a6b78e0 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -37,6 +37,7 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import is_namedtuple +from psutil.tests import PsutilTestCase from psutil.tests import SKIP_SYSCONS from psutil.tests import unittest from psutil.tests import VALID_PROC_STATUSES @@ -51,7 +52,7 @@ # Make sure code reflects what doc promises in terms of APIs # availability. -class TestAvailConstantsAPIs(unittest.TestCase): +class TestAvailConstantsAPIs(PsutilTestCase): def test_PROCFS_PATH(self): self.assertEqual(hasattr(psutil, "PROCFS_PATH"), @@ -105,7 +106,7 @@ def test_linux_rlimit(self): ae(hasattr(psutil, "RLIMIT_SIGPENDING"), hasit) -class TestAvailSystemAPIs(unittest.TestCase): +class TestAvailSystemAPIs(PsutilTestCase): def test_win_service_iter(self): self.assertEqual(hasattr(psutil, "win_service_iter"), WINDOWS) @@ -132,7 +133,7 @@ def test_battery(self): LINUX or WINDOWS or FREEBSD or MACOS) -class TestAvailProcessAPIs(unittest.TestCase): +class TestAvailProcessAPIs(PsutilTestCase): def test_environ(self): self.assertEqual(hasattr(psutil.Process, "environ"), @@ -182,7 +183,7 @@ def test_memory_maps(self): # =================================================================== -class TestSystemAPITypes(unittest.TestCase): +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 @@ -312,7 +313,7 @@ def test_users(self): # =================================================================== -class TestFetchAllProcesses(unittest.TestCase): +class TestFetchAllProcesses(PsutilTestCase): """Test which iterates over all running processes and performs some sanity checks against Process API's returned values. """ diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index bac20b0585..fcc9d5db6c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -28,13 +28,12 @@ from psutil._compat import PY3 from psutil._compat import u from psutil.tests import call_until -from psutil.tests import get_testfn 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 mock -from psutil.tests import ProcessTestCase +from psutil.tests import PsutilTestCase from psutil.tests import PYPY from psutil.tests import reload_module from psutil.tests import retry_on_failure @@ -188,7 +187,7 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemVirtualMemory(unittest.TestCase): +class TestSystemVirtualMemory(PsutilTestCase): def test_total(self): # free_value = free_physmem().total @@ -494,7 +493,7 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemSwapMemory(unittest.TestCase): +class TestSystemSwapMemory(PsutilTestCase): @staticmethod def meminfo_has_swap_info(): @@ -588,7 +587,7 @@ def test_emulate_meminfo_has_no_metrics(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUTimes(unittest.TestCase): +class TestSystemCPUTimes(PsutilTestCase): @unittest.skipIf(TRAVIS, "unknown failure on travis") def test_fields(self): @@ -610,7 +609,7 @@ def test_fields(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUCountLogical(unittest.TestCase): +class TestSystemCPUCountLogical(PsutilTestCase): @unittest.skipIf(not os.path.exists("/sys/devices/system/cpu/online"), "/sys/devices/system/cpu/online does not exist") @@ -674,7 +673,7 @@ def test_emulate_fallbacks(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUCountPhysical(unittest.TestCase): +class TestSystemCPUCountPhysical(PsutilTestCase): @unittest.skipIf(not which("lscpu"), "lscpu utility not available") def test_against_lscpu(self): @@ -695,7 +694,7 @@ def test_emulate_none(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUFrequency(unittest.TestCase): +class TestSystemCPUFrequency(PsutilTestCase): @unittest.skipIf(TRAVIS, "fails on Travis") @unittest.skipIf(not HAS_CPU_FREQ, "not supported") @@ -843,7 +842,7 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUStats(unittest.TestCase): +class TestSystemCPUStats(PsutilTestCase): @unittest.skipIf(TRAVIS, "fails on Travis") def test_ctx_switches(self): @@ -859,7 +858,7 @@ def test_interrupts(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestLoadAvg(unittest.TestCase): +class TestLoadAvg(PsutilTestCase): @unittest.skipIf(not HAS_GETLOADAVG, "not supported") def test_getloadavg(self): @@ -878,7 +877,7 @@ def test_getloadavg(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetIfAddrs(unittest.TestCase): +class TestSystemNetIfAddrs(PsutilTestCase): def test_ips(self): for name, addrs in psutil.net_if_addrs().items(): @@ -907,7 +906,7 @@ def test_ips(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetIfStats(unittest.TestCase): +class TestSystemNetIfStats(PsutilTestCase): def test_against_ifconfig(self): for name, stats in psutil.net_if_stats().items(): @@ -923,7 +922,7 @@ def test_against_ifconfig(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetIOCounters(unittest.TestCase): +class TestSystemNetIOCounters(PsutilTestCase): @retry_on_failure() def test_against_ifconfig(self): @@ -969,7 +968,7 @@ def ifconfig(nic): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetConnections(unittest.TestCase): +class TestSystemNetConnections(PsutilTestCase): @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) @@ -1002,7 +1001,7 @@ def test_emulate_unix(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemDiskPartitions(unittest.TestCase): +class TestSystemDiskPartitions(PsutilTestCase): @unittest.skipIf(not hasattr(os, 'statvfs'), "os.statvfs() not available") @skip_on_not_implemented() @@ -1067,7 +1066,7 @@ def test_emulate_realpath_fail(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemDiskIoCounters(unittest.TestCase): +class TestSystemDiskIoCounters(PsutilTestCase): def test_emulate_kernel_2_4(self): # Tests /proc/diskstats parsing format for 2.4 kernels, see: @@ -1208,7 +1207,7 @@ def exists(path): @unittest.skipIf(not LINUX, "LINUX only") -class TestMisc(unittest.TestCase): +class TestMisc(PsutilTestCase): def test_boot_time(self): vmstat_value = vmstat('boot time') @@ -1216,7 +1215,7 @@ def test_boot_time(self): self.assertEqual(int(vmstat_value), int(psutil_value)) def test_no_procfs_on_import(self): - my_procfs = get_testfn() + my_procfs = self.get_testfn() os.mkdir(my_procfs) with open(os.path.join(my_procfs, 'stat'), 'w') as f: @@ -1345,7 +1344,7 @@ def test_users_mocked(self): assert m.called def test_procfs_path(self): - tdir = get_testfn() + tdir = self.get_testfn() os.mkdir(tdir) try: psutil.PROCFS_PATH = tdir @@ -1397,7 +1396,7 @@ def test_pid_exists_no_proc_status(self): @unittest.skipIf(not LINUX, "LINUX only") @unittest.skipIf(not HAS_BATTERY, "no battery") -class TestSensorsBattery(unittest.TestCase): +class TestSensorsBattery(PsutilTestCase): @unittest.skipIf(not which("acpi"), "acpi utility not available") def test_percent(self): @@ -1545,7 +1544,7 @@ def test_emulate_no_power(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSensorsTemperatures(unittest.TestCase): +class TestSensorsTemperatures(PsutilTestCase): def test_emulate_class_hwmon(self): def open_mock(name, *args, **kwargs): @@ -1611,7 +1610,7 @@ def glob_mock(path): @unittest.skipIf(not LINUX, "LINUX only") -class TestSensorsFans(unittest.TestCase): +class TestSensorsFans(PsutilTestCase): def test_emulate_data(self): def open_mock(name, *args, **kwargs): @@ -1640,11 +1639,11 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") -class TestProcess(ProcessTestCase): +class TestProcess(PsutilTestCase): @retry_on_failure() def test_memory_full_info(self): - testfn = get_testfn() + testfn = self.get_testfn() src = textwrap.dedent(""" import time with open("%s", "w") as f: @@ -1713,7 +1712,7 @@ def get_test_file(fname): raise RuntimeError("timeout looking for test file") # - testfn = get_testfn() + testfn = self.get_testfn() with open(testfn, "w"): self.assertEqual(get_test_file(testfn).mode, "w") with open(testfn, "r"): @@ -1741,7 +1740,7 @@ def test_open_files_file_gone(self): # execution p = psutil.Process() files = p.open_files() - with open(get_testfn(), 'w'): + 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', @@ -1762,7 +1761,7 @@ def test_open_files_fd_gone(self): # https://travis-ci.org/giampaolo/psutil/jobs/225694530 p = psutil.Process() files = p.open_files() - with open(get_testfn(), 'w'): + 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' @@ -2009,7 +2008,7 @@ def test_status_file_parsing(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestProcessAgainstStatus(unittest.TestCase): +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 @@ -2092,7 +2091,7 @@ def test_cpu_affinity_eligible_cpus(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestUtils(unittest.TestCase): +class TestUtils(PsutilTestCase): def test_readlink(self): with mock.patch("os.readlink", return_value="foo (deleted)") as m: @@ -2100,7 +2099,7 @@ def test_readlink(self): assert m.called def test_cat(self): - testfn = get_testfn() + testfn = self.get_testfn() with open(testfn, "wt") as f: f.write("foo ") self.assertEqual(psutil._psplatform.cat(testfn, binary=False), "foo") diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 9069b1a3cb..3d004f0a2b 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -46,7 +46,6 @@ 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 skip_on_access_denied from psutil.tests import TestMemoryLeak from psutil.tests import TRAVIS @@ -288,11 +287,6 @@ def setUpClass(cls): cls.proc.kill() cls.proc.wait() - @classmethod - def tearDownClass(cls): - super().tearDownClass() - reap_children() - def _call(self, fun): try: fun() diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 18781e7524..4fb8ba5a92 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -37,6 +37,7 @@ from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import import_module_by_path from psutil.tests import mock +from psutil.tests import PsutilTestCase from psutil.tests import PYTHON_EXE from psutil.tests import reload_module from psutil.tests import ROOT_DIR @@ -53,7 +54,7 @@ # =================================================================== -class TestMisc(unittest.TestCase): +class TestMisc(PsutilTestCase): def test_process__repr__(self, func=repr): p = psutil.Process() @@ -386,7 +387,7 @@ def test_sanity_version_check(self): nt = collections.namedtuple('foo', 'a b c') -class TestWrapNumbers(unittest.TestCase): +class TestWrapNumbers(PsutilTestCase): def setUp(self): wrap_numbers.cache_clear() @@ -627,7 +628,7 @@ def test_cache_clear_public_apis(self): @unittest.skipIf(not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory") -class TestScripts(unittest.TestCase): +class TestScripts(PsutilTestCase): """Tests for scripts in the "scripts" directory.""" @staticmethod diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 4df6a884be..c2e6ad7226 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -15,9 +15,10 @@ from psutil.tests import create_zombie_proc from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY -from psutil.tests import SYSMEM_TOLERANCE +from psutil.tests import PsutilTestCase from psutil.tests import retry_on_failure from psutil.tests import sh +from psutil.tests import SYSMEM_TOLERANCE from psutil.tests import terminate from psutil.tests import unittest @@ -76,7 +77,7 @@ def human2bytes(s): @unittest.skipIf(not MACOS, "MACOS only") -class TestProcess(unittest.TestCase): +class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): @@ -102,7 +103,7 @@ def test_process_create_time(self): # TODO: probably needs removal (duplicate) @unittest.skipIf(not MACOS, "MACOS only") -class TestZombieProcessAPIs(unittest.TestCase): +class TestZombieProcessAPIs(PsutilTestCase): @classmethod def setUpClass(cls): @@ -160,7 +161,7 @@ def test_threads(self): @unittest.skipIf(not MACOS, "MACOS only") -class TestSystemAPIs(unittest.TestCase): +class TestSystemAPIs(PsutilTestCase): # --- disk diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 1b37fa2fd7..9eeb5c2b90 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -27,6 +27,7 @@ from psutil.tests import get_test_subprocess from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import mock +from psutil.tests import PsutilTestCase from psutil.tests import PYTHON_EXE from psutil.tests import retry_on_failure from psutil.tests import sh @@ -126,7 +127,7 @@ def ps_vsz(pid): @unittest.skipIf(not POSIX, "POSIX only") -class TestProcess(unittest.TestCase): +class TestProcess(PsutilTestCase): """Compare psutil results against 'ps' command line utility (mainly).""" @classmethod @@ -326,7 +327,7 @@ def call(p, attr): @unittest.skipIf(not POSIX, "POSIX only") -class TestSystemAPIs(unittest.TestCase): +class TestSystemAPIs(PsutilTestCase): """Test some system APIs.""" @retry_on_failure() diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index c87df1b180..11ab725be9 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -34,13 +34,13 @@ from psutil._common import open_text from psutil._compat import long from psutil._compat import PY3 +from psutil._compat import super from psutil.tests import APPVEYOR from psutil.tests import call_until from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe from psutil.tests import enum -from psutil.tests import get_testfn from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_ENVIRON from psutil.tests import HAS_IONICE @@ -50,7 +50,7 @@ from psutil.tests import HAS_RLIMIT from psutil.tests import HAS_THREADS from psutil.tests import mock -from psutil.tests import ProcessTestCase +from psutil.tests import PsutilTestCase from psutil.tests import PYPY from psutil.tests import PYTHON_EXE from psutil.tests import reap_children @@ -69,7 +69,7 @@ # =================================================================== -class TestProcess(ProcessTestCase): +class TestProcess(PsutilTestCase): """Tests for psutil.Process class.""" def test_pid(self): @@ -180,23 +180,23 @@ def test_wait(self): def test_wait_non_children(self): # Test wait() against a process which is not our direct # child. - p1, p2 = self.create_proc_children_pair() - self.assertRaises(psutil.TimeoutExpired, p1.wait, 0.01) - self.assertRaises(psutil.TimeoutExpired, p2.wait, 0.01) + child, grandchild = self.create_proc_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. - p1.terminate() - p2.terminate() - ret1 = p1.wait() - ret2 = p2.wait() + child.terminate() + grandchild.terminate() + child_ret = child.wait() + grandchild_ret = grandchild.wait() if POSIX: - self.assertEqual(ret1, -signal.SIGTERM) + self.assertEqual(child_ret, -signal.SIGTERM) # For processes which are not our children we're supposed # to get None. - self.assertEqual(ret2, None) + self.assertEqual(grandchild_ret, None) else: - self.assertEqual(ret1, signal.SIGTERM) - self.assertEqual(ret1, signal.SIGTERM) + self.assertEqual(child_ret, signal.SIGTERM) + self.assertEqual(child_ret, signal.SIGTERM) def test_wait_timeout_0(self): sproc = self.get_test_subprocess() @@ -316,7 +316,7 @@ def test_io_counters(self): # test writes io1 = p.io_counters() - with open(get_testfn(), 'wb') as f: + with open(self.get_testfn(), 'wb') as f: if PY3: f.write(bytes("x" * 1000000, 'ascii')) else: @@ -449,7 +449,7 @@ def test_rlimit_set(self): @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit(self): - testfn = get_testfn() + testfn = self.get_testfn() p = psutil.Process() soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) try: @@ -471,7 +471,7 @@ def test_rlimit(self): def test_rlimit_infinity(self): # First set a limit, then re-set it by specifying INFINITY # and assume we overridden the previous limit. - testfn = get_testfn() + testfn = self.get_testfn() p = psutil.Process() soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) try: @@ -718,7 +718,7 @@ def test_cmdline(self): @unittest.skipIf(PYPY, "broken on PYPY") def test_long_cmdline(self): - testfn = get_testfn() + testfn = self.get_testfn() create_exe(testfn) cmdline = [testfn] + (["0123456789"] * 20) sproc = self.get_test_subprocess(cmdline) @@ -733,7 +733,7 @@ def test_name(self): @unittest.skipIf(PYPY, "unreliable on PYPY") def test_long_name(self): - testfn = get_testfn(suffix="0123456789" * 2) + testfn = self.get_testfn(suffix="0123456789" * 2) create_exe(testfn) sproc = self.get_test_subprocess(testfn) p = psutil.Process(sproc.pid) @@ -747,7 +747,7 @@ 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 - funky_path = get_testfn(suffix='foo bar )') + funky_path = self.get_testfn(suffix='foo bar )') create_exe(funky_path) cmdline = [funky_path, "-c", "import time; [time.sleep(0.01) for x in range(3000)];" @@ -938,7 +938,7 @@ def test_cpu_affinity_all_combinations(self): @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") def test_open_files(self): # current process - testfn = get_testfn() + testfn = self.get_testfn() p = psutil.Process() files = p.open_files() self.assertFalse(testfn in files) @@ -978,7 +978,7 @@ def test_open_files(self): def test_open_files_2(self): # test fd and path fields normcase = os.path.normcase - testfn = get_testfn() + testfn = self.get_testfn() with open(testfn, 'w') as fileobj: p = psutil.Process() for file in p.open_files(): @@ -1001,7 +1001,7 @@ def test_open_files_2(self): @unittest.skipIf(not POSIX, 'POSIX only') def test_num_fds(self): - testfn = get_testfn() + testfn = self.get_testfn() p = psutil.Process() start = p.num_fds() file = open(testfn, 'w') @@ -1028,12 +1028,9 @@ def test_num_ctx_switches(self): def test_ppid(self): if hasattr(os, 'getppid'): self.assertEqual(psutil.Process().ppid(), os.getppid()) - this_parent = os.getpid() sproc = self.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) + self.assertEqual(p.ppid(), os.getpid()) if APPVEYOR: # Occasional failures, see: # https://ci.appveyor.com/project/giampaolo/psutil/build/ @@ -1043,21 +1040,20 @@ def test_ppid(self): if p.pid == sproc.pid: continue # XXX: sometimes this fails on Windows; not sure why. - self.assertNotEqual(p.ppid(), this_parent, msg=p) + self.assertNotEqual(p.ppid(), os.getpid(), msg=p) def test_parent(self): - this_parent = os.getpid() sproc = self.get_test_subprocess() p = psutil.Process(sproc.pid) - self.assertEqual(p.parent().pid, this_parent) + self.assertEqual(p.parent().pid, os.getpid()) lowest_pid = psutil.pids()[0] self.assertIsNone(psutil.Process(lowest_pid).parent()) def test_parent_multi(self): - p1, p2 = self.create_proc_children_pair() - self.assertEqual(p2.parent(), p1) - self.assertEqual(p1.parent(), psutil.Process()) + child, grandchild = self.create_proc_children_pair() + self.assertEqual(grandchild.parent(), child) + self.assertEqual(child.parent(), psutil.Process()) def test_parent_disappeared(self): # Emulate a case where the parent process disappeared. @@ -1070,13 +1066,12 @@ def test_parent_disappeared(self): @retry_on_failure() def test_parents(self): assert psutil.Process().parents() - p1, p2 = self.create_proc_children_pair() - self.assertEqual(p1.parents()[0], psutil.Process()) - self.assertEqual(p2.parents()[0], p1) - self.assertEqual(p2.parents()[1], psutil.Process()) + child, grandchild = self.create_proc_children_pair() + self.assertEqual(child.parents()[0], psutil.Process()) + self.assertEqual(grandchild.parents()[0], child) + self.assertEqual(grandchild.parents()[1], psutil.Process()) def test_children(self): - reap_children(recursive=True) p = psutil.Process() self.assertEqual(p.children(), []) self.assertEqual(p.children(recursive=True), []) @@ -1094,14 +1089,14 @@ def test_children(self): def test_children_recursive(self): # Test children() against two sub processes, p1 and p2, where # p1 (our child) spawned p2 (our grandchild). - p1, p2 = self.create_proc_children_pair() + child, grandchild = self.create_proc_children_pair() p = psutil.Process() - self.assertEqual(p.children(), [p1]) - self.assertEqual(p.children(recursive=True), [p1, p2]) + self.assertEqual(p.children(), [child]) + self.assertEqual(p.children(recursive=True), [child, grandchild]) # If the intermediate process is gone there's no way for # children() to recursively find it. - p1.terminate() - p1.wait() + child.terminate() + child.wait() self.assertEqual(p.children(recursive=True), []) def test_children_duplicates(self): @@ -1123,16 +1118,16 @@ def test_children_duplicates(self): self.assertEqual(len(c), len(set(c))) def test_parents_and_children(self): - p1, p2 = self.create_proc_children_pair() + child, grandchild = self.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) + self.assertEqual(children[0], child) + self.assertEqual(children[1], grandchild) # backward - parents = p2.parents() - self.assertEqual(parents[0], p1) + parents = grandchild.parents() + self.assertEqual(parents[0], child) self.assertEqual(parents[1], me) def test_suspend_resume(self): @@ -1488,7 +1483,7 @@ def test_weird_environ(self): return execve("/bin/cat", argv, envp); } """) - path = get_testfn() + path = self.get_testfn() create_exe(path, c_code=code) sproc = self.get_test_subprocess( [path], stdin=subprocess.PIPE, stderr=subprocess.PIPE) @@ -1533,14 +1528,14 @@ def test_(self): setattr(self, attr, types.MethodType(test_, self)) def setUp(self): - TestProcess.setUp(self) + super().setUp() os.setegid(1000) os.seteuid(1000) def tearDown(self): os.setegid(self.PROCESS_UID) os.seteuid(self.PROCESS_GID) - TestProcess.tearDown(self) + super().tearDown() def test_nice(self): try: @@ -1560,7 +1555,7 @@ def test_zombie_process(self): # =================================================================== -class TestPopen(unittest.TestCase): +class TestPopen(PsutilTestCase): """Tests for psutil.Popen class.""" @classmethod diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index bac1a2122a..ad94f774d6 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -10,12 +10,13 @@ import psutil from psutil import SUNOS +from psutil.tests import PsutilTestCase from psutil.tests import sh from psutil.tests import unittest @unittest.skipIf(not SUNOS, "SUNOS only") -class SunOSSpecificTestCase(unittest.TestCase): +class SunOSSpecificTestCase(PsutilTestCase): def test_swap_memory(self): out = sh('env PATH=/usr/sbin:/sbin:%s swap -l' % os.environ['PATH']) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py old mode 100755 new mode 100644 index d0817459cf..eda6db013a --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -35,7 +35,6 @@ from psutil.tests import CI_TESTING from psutil.tests import DEVNULL from psutil.tests import enum -from psutil.tests import get_testfn from psutil.tests import HAS_BATTERY from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_GETLOADAVG @@ -44,7 +43,7 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import mock -from psutil.tests import ProcessTestCase +from psutil.tests import PsutilTestCase from psutil.tests import PYPY from psutil.tests import retry_on_failure from psutil.tests import TRAVIS @@ -57,7 +56,7 @@ # =================================================================== -class TestProcessAPIs(ProcessTestCase): +class TestProcessAPIs(PsutilTestCase): def test_process_iter(self): self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) @@ -190,7 +189,7 @@ def test_pid_exists_2(self): self.assertFalse(psutil.pid_exists(pid), msg=pid) -class TestMiscAPIs(unittest.TestCase): +class TestMiscAPIs(PsutilTestCase): def test_boot_time(self): bt = psutil.boot_time() @@ -272,7 +271,7 @@ def test_os_constants(self): self.assertIs(getattr(psutil, name), False, msg=name) -class TestMemoryAPIs(unittest.TestCase): +class TestMemoryAPIs(PsutilTestCase): def test_virtual_memory(self): mem = psutil.virtual_memory() @@ -309,7 +308,7 @@ def test_swap_memory(self): assert mem.sout >= 0, mem -class TestCpuAPIs(unittest.TestCase): +class TestCpuAPIs(PsutilTestCase): def test_cpu_count_logical(self): logical = psutil.cpu_count() @@ -550,7 +549,7 @@ def test_getloadavg(self): self.assertGreaterEqual(load, 0.0) -class TestDiskAPIs(unittest.TestCase): +class TestDiskAPIs(PsutilTestCase): def test_disk_usage(self): usage = psutil.disk_usage(os.getcwd()) @@ -574,7 +573,7 @@ def test_disk_usage(self): # if path does not exist OSError ENOENT is expected across # all platforms - fname = get_testfn() + fname = self.get_testfn() with self.assertRaises(FileNotFoundError): psutil.disk_usage(fname) @@ -684,7 +683,7 @@ def test_disk_io_counters_no_disks(self): assert m.called -class TestNetAPIs(unittest.TestCase): +class TestNetAPIs(PsutilTestCase): @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') def test_net_io_counters(self): @@ -829,7 +828,7 @@ def test_net_if_stats_enodev(self): assert m.called -class TestSensorsAPIs(unittest.TestCase): +class TestSensorsAPIs(PsutilTestCase): @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") def test_sensors_temperatures(self): diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py old mode 100755 new mode 100644 index 85b61aea35..e470c1b8d1 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -32,11 +32,10 @@ from psutil.tests import chdir from psutil.tests import create_sockets from psutil.tests import get_free_port -from psutil.tests import get_testfn from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import is_namedtuple from psutil.tests import mock -from psutil.tests import ProcessTestCase +from psutil.tests import PsutilTestCase from psutil.tests import PYTHON_EXE from psutil.tests import reap_children from psutil.tests import retry @@ -59,7 +58,7 @@ # =================================================================== -class TestRetryDecorator(unittest.TestCase): +class TestRetryDecorator(PsutilTestCase): @mock.patch('time.sleep') def test_retry_success(self, sleep): @@ -125,7 +124,7 @@ def test_retries_and_timeout_args(self, sleep): self.assertRaises(ValueError, retry, retries=5, timeout=1) -class TestSyncTestUtils(unittest.TestCase): +class TestSyncTestUtils(PsutilTestCase): def test_wait_for_pid(self): wait_for_pid(os.getpid()) @@ -134,26 +133,26 @@ def test_wait_for_pid(self): self.assertRaises(psutil.NoSuchProcess, wait_for_pid, nopid) def test_wait_for_file(self): - testfn = get_testfn() + 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 = get_testfn() + 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 = get_testfn() + 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 = get_testfn() + testfn = self.get_testfn() with open(testfn, 'w') as f: f.write('foo') wait_for_file(testfn, delete=False) @@ -164,7 +163,7 @@ def test_call_until(self): self.assertEqual(ret, 1) -class TestFSTestUtils(unittest.TestCase): +class TestFSTestUtils(PsutilTestCase): def test_open_text(self): with open_text(__file__) as f: @@ -175,7 +174,7 @@ def test_open_binary(self): self.assertEqual(f.mode, 'rb') def test_safe_mkdir(self): - testfn = get_testfn() + testfn = self.get_testfn() safe_mkdir(testfn) assert os.path.isdir(testfn) safe_mkdir(testfn) @@ -183,7 +182,7 @@ def test_safe_mkdir(self): def test_safe_rmpath(self): # test file is removed - testfn = get_testfn() + testfn = self.get_testfn() open(testfn, 'w').close() safe_rmpath(testfn) assert not os.path.exists(testfn) @@ -201,7 +200,7 @@ def test_safe_rmpath(self): assert m.called def test_chdir(self): - testfn = get_testfn() + testfn = self.get_testfn() base = os.getcwd() os.mkdir(testfn) with chdir(testfn): @@ -209,7 +208,7 @@ def test_chdir(self): self.assertEqual(os.getcwd(), base) -class TestProcessUtils(ProcessTestCase): +class TestProcessUtils(PsutilTestCase): def test_reap_children(self): subp = self.get_test_subprocess() @@ -221,23 +220,25 @@ def test_reap_children(self): assert not psutil.tests._subprocesses_started def test_create_proc_children_pair(self): - p1, p2 = self.create_proc_children_pair() - self.assertNotEqual(p1.pid, p2.pid) - assert p1.is_running() - assert p2.is_running() + child, grandchild = self.create_proc_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(p1, children) - self.assertIn(p2, children) - self.assertEqual(p1.ppid(), os.getpid()) - self.assertEqual(p2.ppid(), p1.pid) + self.assertIn(child, children) + self.assertIn(grandchild, children) + self.assertEqual(child.ppid(), os.getpid()) + self.assertEqual(grandchild.ppid(), child.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 + 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_create_zombie_proc(self): @@ -275,7 +276,7 @@ def test_terminate(self): assert not psutil.pid_exists(zombie.pid) -class TestNetUtils(unittest.TestCase): +class TestNetUtils(PsutilTestCase): def bind_socket(self): port = get_free_port() @@ -284,7 +285,7 @@ def bind_socket(self): @unittest.skipIf(not POSIX, "POSIX only") def test_bind_unix_socket(self): - name = get_testfn() + name = self.get_testfn() sock = bind_unix_socket(name) with contextlib.closing(sock): self.assertEqual(sock.family, socket.AF_UNIX) @@ -293,7 +294,7 @@ def test_bind_unix_socket(self): assert os.path.exists(name) assert stat.S_ISSOCK(os.stat(name).st_mode) # UDP - name = get_testfn() + name = self.get_testfn() sock = bind_unix_socket(name, type=socket.SOCK_DGRAM) with contextlib.closing(sock): self.assertEqual(sock.type, socket.SOCK_DGRAM) @@ -316,7 +317,7 @@ def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() assert not p.connections(kind='unix') - name = get_testfn() + name = self.get_testfn() server, client = unix_socketpair(name) try: assert os.path.exists(name) @@ -417,7 +418,7 @@ def fun(): self.execute_w_exc(ZeroDivisionError, fun) -class TestOtherUtils(unittest.TestCase): +class TestOtherUtils(PsutilTestCase): def test_is_namedtuple(self): assert is_namedtuple(collections.namedtuple('foo', 'a b c')(1, 2, 3)) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py old mode 100755 new mode 100644 index ae9f7f51ed..3f55e79760 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -98,11 +98,10 @@ from psutil.tests import HAS_ENVIRON from psutil.tests import HAS_MEMORY_MAPS from psutil.tests import INVALID_UNICODE_SUFFIX -from psutil.tests import ProcessTestCase +from psutil.tests import PsutilTestCase 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 safe_rmpath from psutil.tests import serialrun from psutil.tests import skip_on_access_denied from psutil.tests import terminate @@ -113,8 +112,8 @@ import psutil -def safe_rmpath(path): - if APPVEYOR: +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/ @@ -125,12 +124,11 @@ def safe_rmpath(path): # https://github.com/giampaolo/psutil/blob/ # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/ # windows/process_info.c#L146 + from psutil.tests import safe_rmpath as _rm try: - return _safe_rmpath(path) + return _rm(path) except WindowsError: traceback.print_exc() - else: - return _safe_rmpath(path) def subprocess_supports_unicode(suffix): @@ -139,12 +137,12 @@ def subprocess_supports_unicode(suffix): """ if PY3: return True - name = get_testfn(suffix=suffix) sproc = None + testfn = get_testfn(suffix=suffix) try: - safe_rmpath(name) - create_exe(name) - sproc = get_test_subprocess(cmd=[name]) + safe_rmpath(testfn) + create_exe(testfn) + sproc = get_test_subprocess(cmd=[testfn]) except UnicodeEncodeError: return False else: @@ -152,6 +150,7 @@ def subprocess_supports_unicode(suffix): finally: if sproc is not None: terminate(sproc) + safe_rmpath(testfn) # =================================================================== @@ -170,7 +169,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reap_children() + safe_rmpath(cls.funky_name) def expect_exact_path_match(self): raise NotImplementedError("must be implemented in subclass") @@ -231,7 +230,7 @@ def test_proc_open_files(self): @unittest.skipIf(not POSIX, "POSIX only") def test_proc_connections(self): suffix = os.path.basename(self.funky_name) - name = get_testfn(suffix=suffix) + name = self.get_testfn(suffix=suffix) try: sock = bind_unix_socket(name) except UnicodeEncodeError: @@ -257,7 +256,7 @@ def find_sock(cons): raise ValueError("connection not found") suffix = os.path.basename(self.funky_name) - name = get_testfn(suffix=suffix) + name = self.get_testfn(suffix=suffix) try: sock = bind_unix_socket(name) except UnicodeEncodeError: @@ -304,7 +303,7 @@ def normpath(p): @unittest.skipIf(ASCII_FS, "ASCII fs") @unittest.skipIf(not subprocess_supports_unicode(UNICODE_SUFFIX), "subprocess can't deal with unicode") -class TestFSAPIs(_BaseFSAPIsTests, ProcessTestCase): +class TestFSAPIs(_BaseFSAPIsTests, PsutilTestCase): """Test FS APIs with a funky, valid, UTF8 path name.""" funky_suffix = UNICODE_SUFFIX @@ -322,7 +321,7 @@ def expect_exact_path_match(self): @unittest.skipIf(PYPY, "unreliable on PYPY") @unittest.skipIf(not subprocess_supports_unicode(INVALID_UNICODE_SUFFIX), "subprocess can't deal with invalid unicode") -class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, ProcessTestCase): +class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, PsutilTestCase): """Test FS APIs with a funky, invalid path name.""" funky_suffix = INVALID_UNICODE_SUFFIX @@ -337,12 +336,9 @@ def expect_exact_path_match(cls): # =================================================================== -class TestNonFSAPIS(ProcessTestCase): +class TestNonFSAPIS(PsutilTestCase): """Unicode tests for non fs-related APIs.""" - def tearDown(self): - reap_children() - @unittest.skipIf(not HAS_ENVIRON, "not supported") @unittest.skipIf(PYPY and WINDOWS, "segfaults on PYPY + WINDOWS") def test_proc_environ(self): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index a51c9c1522..057c2982ec 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -22,14 +22,14 @@ import psutil from psutil import WINDOWS from psutil._compat import FileNotFoundError +from psutil._compat import super 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 ProcessTestCase +from psutil.tests import PsutilTestCase from psutil.tests import PY3 from psutil.tests import PYPY -from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import terminate @@ -66,7 +66,7 @@ def wrapper(self, *args, **kwargs): @unittest.skipIf(PYPY, "pywin32 not available on PYPY") # skip whole module -class TestCase(unittest.TestCase): +class TestCase(PsutilTestCase): pass @@ -300,7 +300,7 @@ def test_emulate_secs_left_unknown(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcess(ProcessTestCase): +class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): @@ -537,7 +537,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reap_children() + terminate(cls.pid) def test_name(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] @@ -677,7 +677,7 @@ def test_cmdline(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class RemoteProcessTestCase(ProcessTestCase): +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 @@ -696,6 +696,7 @@ def find_other_interpreter(): stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output, _ = proc.communicate() + proc.wait() if output == str(not IS_64_BIT): return filename @@ -717,6 +718,7 @@ def setUpClass(cls): test_args = ["-c", "import sys; sys.stdin.read()"] def setUp(self): + super().setUp() env = os.environ.copy() env["THINK_OF_A_NUMBER"] = str(os.getpid()) self.proc32 = self.get_test_subprocess( @@ -729,13 +731,10 @@ def setUp(self): stdin=subprocess.PIPE) def tearDown(self): + super().tearDown() self.proc32.communicate() self.proc64.communicate() - @classmethod - def tearDownClass(cls): - reap_children() - def test_cmdline_32(self): p = psutil.Process(self.proc32.pid) self.assertEqual(len(p.cmdline()), 3) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index f39d45acfc..eebd16923f 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -31,7 +31,7 @@ PYTHON = sys.executable else: PYTHON = os.getenv('PYTHON', sys.executable) -TEST_SCRIPT = 'psutil\\tests\\runner.py' +RUNNER_PY = 'psutil\\tests\\runner.py' GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" PY3 = sys.version_info[0] == 3 HERE = os.path.abspath(os.path.dirname(__file__)) @@ -397,13 +397,11 @@ def lint(): sh("%s -m flake8 %s" % (PYTHON, py_files), nolog=True) -def test(script=TEST_SCRIPT): +def test(name=""): """Run tests""" install() test_setup() - cmdline = "%s %s" % (PYTHON, script) - safe_print(cmdline) - sh(cmdline) + sh("%s %s %s" % (PYTHON, RUNNER_PY, name)) def coverage(): @@ -411,7 +409,7 @@ def coverage(): # Note: coverage options are controlled by .coveragerc file install() test_setup() - sh("%s -m coverage run %s" % (PYTHON, TEST_SCRIPT)) + sh("%s -m coverage run %s" % (PYTHON, RUNNER_PY)) sh("%s -m coverage report" % PYTHON) sh("%s -m coverage html" % PYTHON) sh("%s -m webbrowser -t htmlcov/index.html" % PYTHON) @@ -477,8 +475,7 @@ def test_failed(): """Re-run tests which failed on last run.""" install() test_setup() - sh('%s -c "import psutil.tests.runner as r; r.run(last_failed=True)"' % ( - PYTHON)) + sh("%s %s --last-failed" % (PYTHON, RUNNER_PY)) def test_memleaks(): From 8d8a7804d159e5b80378000b57bbfbaf63ce6e8f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 May 2020 02:20:52 -0700 Subject: [PATCH 0551/1714] Revert #1736: Popen inheriting from subprocess (#1744) --- HISTORY.rst | 4 +-- docs/index.rst | 47 ++++++++------------------- psutil/__init__.py | 69 ++++++++++++++++------------------------ psutil/tests/__init__.py | 21 ++++-------- psutil/tests/runner.py | 40 ++++++++++++----------- 5 files changed, 69 insertions(+), 112 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c375934c85..6f8087db33 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,9 +7,7 @@ XXXX-XX-XX **Enhancements** -- 1729_: parallel tests on UNIX (make test-parallel). -- 1736_: psutil.Popen now inherits from subprocess.Popen instead of - psutil.Process. Also, wait(timeout=...) parameter is backported to Python 2.7. +- 1729_: parallel tests on UNIX (make test-parallel). They're twice as fast! - 1741_: "make build/install" is now run in parallel and it's about 15% faster on UNIX. diff --git a/docs/index.rst b/docs/index.rst index 08a69555a0..133e69fe77 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1959,24 +1959,18 @@ Process class >>> p.terminate() >>> p.wait() -Popen class ------------ - .. class:: Popen(*args, **kwargs) - A more convenient interface to stdlib `subprocess.Popen`_. - It starts a sub process and you deal with it exactly as when using - `subprocess.Popen`_, but in addition it also provides all the methods of - :class:`psutil.Process` class as a unified interface. - - .. note:: - - Unlike `subprocess.Popen`_ this class preemptively checks whether PID has - been reused on - :meth:`send_signal() `, - :meth:`terminate() ` and - :meth:`kill() ` - so that you can't accidentally terminate another process, fixing `BPO-6973`_. + Starts a sub-process via `subprocess.Popen`_, and in addition it provides + all the methods of :class:`psutil.Process` in a single class. + For method names common to both classes such as + :meth:`send_signal() `, + :meth:`terminate() `, + :meth:`kill() ` and + :meth:`wait() ` + :class:`psutil.Process` implementation takes precedence. + This may have some advantages, like making sure PID has not been reused, + fixing `BPO-6973`_. >>> import psutil >>> from subprocess import PIPE @@ -1992,25 +1986,10 @@ Popen class 0 >>> - *timeout* parameter of `subprocess.Popen.wait`_ is backported for Python < 3.3. - :class:`psutil.Popen` objects are supported as context managers via the with - statement (added to Python 3.2). On exit, standard file descriptors are - closed, and the process is waited for. This is supported on all Python - versions. - - >>> import psutil, subprocess - >>> with psutil.Popen(["ifconfig"], stdout=subprocess.PIPE) as proc: - >>> log.write(proc.stdout.read()) - - - .. versionchanged:: 4.4.0 added context manager support. - - .. versionchanged:: 5.7.1 inherit from `subprocess.Popen`_ instead of - :class:`psutil.Process`. - - .. versionchanged:: 5.7.1 backporint `subprocess.Popen.wait`_ **timeout** - parameter on old Python versions. + .. versionchanged:: 4.4.0 added context manager support + .. versionchanged:: 5.7.1 wait() invokes :meth:`wait() ` + instead of `subprocess.Popen.wait`_. Windows services ================ diff --git a/psutil/__init__.py b/psutil/__init__.py index 028ab04949..acf5ee79f2 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1289,12 +1289,12 @@ def wait(self, timeout=None): # ===================================================================== -class Popen(subprocess.Popen): +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 it also provides all the - methods of psutil.Process class as a unified interface: - + subprocess.Popen class but in addition also provides all the + properties and methods of psutil.Process class as a unified + interface: >>> import psutil >>> from subprocess import PIPE >>> p = psutil.Popen(["python", "-c", "print 'hi'"], stdout=PIPE) @@ -1310,16 +1310,12 @@ class Popen(subprocess.Popen): >>> p.wait(timeout=2) 0 >>> - - In addition, it backports the following functionality: - * "with" statement (Python 3.2) - * wait(timeout=...) parameter (Python 3.3) - + 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 + 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/3/library/subprocess.html """ @@ -1328,21 +1324,21 @@ def __init__(self, *args, **kwargs): # Explicitly avoid to raise NoSuchProcess in case the process # spawned by subprocess.Popen terminates too quickly, see: # https://github.com/giampaolo/psutil/issues/193 - self.__psproc = None - subprocess.Popen.__init__(self, *args, **kwargs) - self.__psproc = Process(self.pid) - self.__psproc._init(self.pid, _ignore_nsp=True) + self.__subproc = subprocess.Popen(*args, **kwargs) + self._init(self.__subproc.pid, _ignore_nsp=True) def __dir__(self): - return sorted(set(dir(subprocess.Popen) + dir(Process))) + return sorted(set(dir(Popen) + dir(subprocess.Popen))) - # Introduced in Python 3.2. - if not hasattr(subprocess.Popen, '__enter__'): + def __enter__(self): + if hasattr(self.__subproc, '__enter__'): + self.__subproc.__enter__() + return self - def __enter__(self): - return self - - def __exit__(self, *args, **kwargs): + def __exit__(self, *args, **kwargs): + if hasattr(self.__subproc, '__exit__'): + return self.__subproc.__exit__(*args, **kwargs) + else: if self.stdout: self.stdout.close() if self.stderr: @@ -1360,30 +1356,21 @@ def __getattribute__(self, name): return object.__getattribute__(self, name) except AttributeError: try: - return object.__getattribute__(self.__psproc, name) + return object.__getattribute__(self.__subproc, name) except AttributeError: raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name)) - def send_signal(self, sig): - return self.__psproc.send_signal(sig) - - def terminate(self): - return self.__psproc.terminate() - - def kill(self): - return self.__psproc.kill() - def wait(self, timeout=None): - if sys.version_info < (3, 3): - # backport of timeout parameter - if self.returncode is not None: - return self.returncode - ret = self.__psproc.wait(timeout) - self.returncode = ret - return ret - else: - return super(Popen, self).wait(timeout) + if self.__subproc.returncode is not None: + return self.__subproc.returncode + # Note: using psutil's wait() on UNIX should make no difference. + # On Windows it does, because PID can still be alive (see + # _pswindows.py counterpart addressing this). Python 2.7 doesn't + # have timeout arg, so this acts as a backport. + ret = Process.wait(self, timeout) + self.__subproc.returncode = ret + return ret # ===================================================================== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 8ce76304c5..6cdf3bc8c6 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -486,21 +486,12 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): from psutil._psposix import wait_pid def wait(proc, timeout): - if sys.version_info < (3, 3) and \ - isinstance(proc, subprocess.Popen) and \ - not isinstance(proc, psutil.Popen): - # subprocess.Popen instance: emulate missing timeout arg. - ret = None - try: - ret = psutil.Process(proc.pid).wait(timeout) - except psutil.NoSuchProcess: - # Needed to kill zombies. - if POSIX: - ret = wait_pid(proc.pid, timeout) - proc.returncode = ret - return ret - else: - return proc.wait(timeout) + try: + return psutil.Process(proc.pid).wait(timeout) + except psutil.NoSuchProcess: + # Needed to kill zombies. + if POSIX: + return wait_pid(proc.pid, timeout) def term_subproc(proc, timeout): try: diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index ef135c8d12..a7f74964f9 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -53,11 +53,21 @@ VERBOSITY = 1 if TOX else 2 FAILED_TESTS_FNAME = '.failed-tests.txt' NWORKERS = psutil.cpu_count() or 1 +USE_COLORS = term_supports_colors() and not APPVEYOR HERE = os.path.abspath(os.path.dirname(__file__)) loadTestsFromTestCase = 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 @@ -110,24 +120,21 @@ def from_name(self, name): class ColouredResult(unittest.TextTestResult): - def _print_color(self, s, color, bold=False): - print_color(s, color, bold=bold, file=self.stream) - def addSuccess(self, test): unittest.TestResult.addSuccess(self, test) - self._print_color("OK", "green") + cprint("OK", "green") def addError(self, test, err): unittest.TestResult.addError(self, test, err) - self._print_color("ERROR", "red", bold=True) + cprint("ERROR", "red", bold=True) def addFailure(self, test, err): unittest.TestResult.addFailure(self, test, err) - self._print_color("FAIL", "red") + cprint("FAIL", "red") def addSkip(self, test, reason): unittest.TestResult.addSkip(self, test, reason) - self._print_color("skipped: %s" % reason.strip(), "brown") + cprint("skipped: %s" % reason.strip(), "brown") def printErrorList(self, flavour, errors): flavour = hilite(flavour, "red", bold=flavour == 'ERROR') @@ -139,11 +146,7 @@ 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. """ - - if term_supports_colors() and not APPVEYOR: - resultclass = ColouredResult - else: - resultclass = unittest.TextTestResult + resultclass = ColouredResult if USE_COLORS else unittest.TextTestResult def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -180,11 +183,11 @@ def _run(self, suite): def _exit(self, success): if success: - print_color("SUCCESS", "green", bold=True) + cprint("SUCCESS", "green", bold=True) safe_rmpath(FAILED_TESTS_FNAME) sys.exit(0) else: - print_color("FAILED", "red", bold=True) + cprint("FAILED", "red", bold=True) self._write_last_failed() sys.exit(1) @@ -228,8 +231,8 @@ def run(self, suite): par_suite = self._parallelize(par_suite) # run parallel - print_color("starting parallel tests using %s workers" % NWORKERS, - "green", bold=True) + cprint("starting parallel tests using %s workers" % NWORKERS, + "green", bold=True) t = time.time() par = self._run(par_suite) par_elapsed = time.time() - t @@ -239,7 +242,7 @@ def run(self, suite): orphans = psutil.Process().children() gone, alive = psutil.wait_procs(orphans, timeout=1) if alive: - print_color("alive processes %s" % alive, "red") + cprint("alive processes %s" % alive, "red") reap_children() # run serial @@ -274,8 +277,7 @@ def run(self, suite): def get_runner(parallel=False): def warn(msg): - print_color(msg + " Running serial tests instead.", - "red", file=sys.stderr) + cprint(msg + " Running serial tests instead.", "red") if parallel: if psutil.WINDOWS: warn("Can't run parallel tests on Windows.") From 6f4e38220d9da33931ff9a307d20025a6916c258 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 May 2020 13:37:37 +0200 Subject: [PATCH 0552/1714] Drastically improve "make test/build" speed. Doing "make install" before any test is slow and not really necessary. Instead do "make build", and remove the part import setuptools and test psutil can be imported (do that in make install instead). This way I went down from 0.8 secs (install phase before starting the test) to 0.3 secs! --- .cirrus.yml | 4 +-- Makefile | 44 +++++++++++++------------- scripts/internal/purge_installation.py | 2 +- scripts/internal/winmake.py | 28 ++++++++-------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 4b8676bc03..a0b8f1f0b3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -6,7 +6,7 @@ freebsd_13_py3_task: install_script: - pkg install -y python3 gcc py37-pip script: - - python3 -m pip install --user setuptools concurrencytest + - python3 -m pip install --user setuptools - make clean - make install - make test @@ -22,7 +22,7 @@ freebsd_11_py2_task: install_script: - pkg install -y python gcc py27-pip script: - - python2.7 -m pip install --user setuptools ipaddress mock concurrencytest + - python2.7 -m pip install --user setuptools ipaddress mock - make clean - make install - make test diff --git a/Makefile b/Makefile index 86727ca7c5..2b2a98efd5 100644 --- a/Makefile +++ b/Makefile @@ -71,17 +71,17 @@ clean: ## Remove all build files. _: build: _ ## Compile (in parallel) without installing. - # make sure setuptools is installed (needed for 'develop' / edit mode) - $(PYTHON) -c "import setuptools" @# "build_ext -i" copies compiled *.so files in ./psutil directory in order @# to allow "import psutil" when using the interactive interpreter from @# within this directory. PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) - $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. + # make sure setuptools is installed (needed for 'develop' / edit mode) + $(PYTHON) -c "import setuptools" ${MAKE} build PYTHONWARNINGS=all $(PYTHON) setup.py develop $(INSTALL_OPTS) + $(PYTHON) -c "import psutil" # make sure it actually worked uninstall: ## Uninstall this package via pip. cd ..; $(PYTHON) -m pip uninstall -y -v psutil || true @@ -117,63 +117,63 @@ setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). # =================================================================== test: ## Run all tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) test-parallel: ## Run all tests in parallel. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --parallel test-process: ## Run process-related API tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process.py test-system: ## Run system-related API tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_system.py test-misc: ## Run miscellaneous tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_misc.py test-testutils: ## Run test utils tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_testutils.py test-unicode: ## Test APIs dealing with strings. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_unicode.py test-contracts: ## APIs sanity tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_contracts.py test-connections: ## Test net_connections() and Process.connections(). - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_connections.py test-posix: ## POSIX specific tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_posix.py test-platform: ## Run specific platform tests only. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memory_leaks.py test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSystemAPIs - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) test-failed: ## Re-run tests which failed on last run - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --last-failed test-coverage: ## Run test coverage. - ${MAKE} install + ${MAKE} build # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov $(TEST_PREFIX) $(PYTHON) -m coverage run $(TSCRIPT) @@ -284,11 +284,11 @@ print-timeline: ## Print releases' timeline. @$(PYTHON) scripts/internal/print_timeline.py print-access-denied: ## Print AD exceptions - ${MAKE} install + ${MAKE} build @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls - ${MAKE} install + ${MAKE} build @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) # =================================================================== @@ -299,11 +299,11 @@ grep-todos: ## Look for TODOs in the source files. git grep -EIn "TODO|FIXME|XXX" bench-oneshot: ## Benchmarks for oneshot() ctx manager (see #799). - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) scripts/internal/bench_oneshot.py bench-oneshot-2: ## Same as above but using perf module (supposed to be more precise) - ${MAKE} install + ${MAKE} build $(TEST_PREFIX) $(PYTHON) scripts/internal/bench_oneshot_2.py check-broken-links: ## Look for broken links in source files. diff --git a/scripts/internal/purge_installation.py b/scripts/internal/purge_installation.py index 50c00463cf..8a9597f0b4 100755 --- a/scripts/internal/purge_installation.py +++ b/scripts/internal/purge_installation.py @@ -30,7 +30,7 @@ def rmpath(path): def main(): locations = [site.getusersitepackages()] - locations.extend(site.getsitepackages()) + locations += site.getsitepackages() for root in locations: if os.path.isdir(root): for name in os.listdir(root): diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index eebd16923f..c7091ac461 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -399,7 +399,7 @@ def lint(): def test(name=""): """Run tests""" - install() + build() test_setup() sh("%s %s %s" % (PYTHON, RUNNER_PY, name)) @@ -407,7 +407,7 @@ def test(name=""): def coverage(): """Run coverage tests.""" # Note: coverage options are controlled by .coveragerc file - install() + build() test_setup() sh("%s -m coverage run %s" % (PYTHON, RUNNER_PY)) sh("%s -m coverage report" % PYTHON) @@ -417,70 +417,70 @@ def coverage(): def test_process(): """Run process tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_process.py" % PYTHON) def test_system(): """Run system tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_system.py" % PYTHON) def test_platform(): """Run windows only tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_windows.py" % PYTHON) def test_misc(): """Run misc tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_misc.py" % PYTHON) def test_unicode(): """Run unicode tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_unicode.py" % PYTHON) def test_connections(): """Run connections tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_connections.py" % PYTHON) def test_contracts(): """Run contracts tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_contracts.py" % PYTHON) def test_by_name(name): """Run test by name""" - install() + build() test_setup() sh("%s -m unittest -v %s" % (PYTHON, name)) def test_failed(): """Re-run tests which failed on last run.""" - install() + build() test_setup() sh("%s %s --last-failed" % (PYTHON, RUNNER_PY)) def test_memleaks(): """Run memory leaks tests""" - install() + build() test_setup() sh("%s psutil\\tests\\test_memory_leaks.py" % PYTHON) @@ -509,14 +509,14 @@ def bench_oneshot_2(): def print_access_denied(): """Print AD exceptions raised by all Process methods.""" - install() + build() test_setup() sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) def print_api_speed(): """Benchmark all API calls.""" - install() + build() test_setup() sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) From 13ba2222971781dc8150099d1fddefb538417ba1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 May 2020 08:40:18 -0700 Subject: [PATCH 0553/1714] Refactor process test utils methods (#1745) ...in order to accomodate Cygwin implementation. --- psutil/tests/__init__.py | 27 ++-- psutil/tests/runner.py | 8 +- psutil/tests/test_bsd.py | 6 +- psutil/tests/test_memory_leaks.py | 4 +- psutil/tests/test_osx.py | 8 +- psutil/tests/test_posix.py | 7 +- psutil/tests/test_process.py | 205 +++++++++++++----------------- psutil/tests/test_system.py | 20 +-- psutil/tests/test_testutils.py | 18 +-- psutil/tests/test_unicode.py | 12 +- psutil/tests/test_windows.py | 14 +- 11 files changed, 151 insertions(+), 178 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 6cdf3bc8c6..c2766e23c5 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -48,6 +48,7 @@ from psutil._compat import FileNotFoundError from psutil._compat import PY3 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 @@ -81,8 +82,8 @@ "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", "HAS_SENSORS_TEMPERATURES", "HAS_MEMORY_FULL_INFO", # subprocesses - 'pyrun', 'terminate', 'reap_children', 'get_test_subprocess', - 'create_zombie_proc', 'create_proc_children_pair', + 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', + 'spawn_children_pair', # threads 'ThreadTask' # test utils @@ -230,7 +231,7 @@ class ThreadTask(threading.Thread): """A thread task which does nothing expect staying alive.""" def __init__(self): - threading.Thread.__init__(self) + super().__init__() self._running = False self._interval = 0.001 self._flag = threading.Event() @@ -286,7 +287,7 @@ def wrapper(*args, **kwargs): @_reap_children_on_err -def get_test_subprocess(cmd=None, **kwds): +def spawn_testproc(cmd=None, **kwds): """Creates a python subprocess which does nothing for 60 secs and return it as a subprocess.Popen instance. If "cmd" is specified that is used instead of python. @@ -326,7 +327,7 @@ def get_test_subprocess(cmd=None, **kwds): @_reap_children_on_err -def create_proc_children_pair(): +def spawn_children_pair(): """Create a subprocess which creates another one as in: A (us) -> B (child) -> C (grandchild). Return a (child, grandchild) tuple. @@ -364,7 +365,7 @@ def create_proc_children_pair(): safe_rmpath(tfile) -def create_zombie_proc(): +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. @@ -421,7 +422,7 @@ def pyrun(src, **kwds): try: with open(srcfile, 'wt') as f: f.write(src) - subp = get_test_subprocess([PYTHON_EXE, f.name], **kwds) + subp = spawn_testproc([PYTHON_EXE, f.name], **kwds) wait_for_pid(subp.pid) return (subp, srcfile) except Exception: @@ -857,19 +858,19 @@ def get_testfn(self, suffix="", dir=None): self.addCleanup(safe_rmpath, fname) return fname - def get_test_subprocess(self, *args, **kwds): - sproc = get_test_subprocess(*args, **kwds) + def spawn_testproc(self, *args, **kwds): + sproc = spawn_testproc(*args, **kwds) self.addCleanup(terminate, sproc) return sproc - def create_proc_children_pair(self): - child1, child2 = create_proc_children_pair() + def spawn_children_pair(self): + child1, child2 = spawn_children_pair() self.addCleanup(terminate, child2) self.addCleanup(terminate, child1) # executed first return (child1, child2) - def create_zombie_proc(self): - parent, zombie = create_zombie_proc() + def spawn_zombie(self): + parent, zombie = spawn_zombie() self.addCleanup(terminate, zombie) self.addCleanup(terminate, parent) # executed first return (parent, zombie) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index a7f74964f9..14e33fbae2 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -200,8 +200,8 @@ class ParallelRunner(ColouredTextRunner): @staticmethod def _parallelize(suite): - def fdopen(*args, **kwds): - stream = orig_fdopen(*args, **kwds) + def fdopen(fd, mode, *kwds): + stream = orig_fdopen(fd, mode) atexit.register(stream.close) return stream @@ -221,9 +221,9 @@ def _split_suite(suite): continue test_class = test._tests[0].__class__ if getattr(test_class, '_serialrun', False): - serial.addTest(loadTestsFromTestCase(test_class)) + serial.addTest(test) else: - parallel.addTest(loadTestsFromTestCase(test_class)) + parallel.addTest(test) return (serial, parallel) def run(self, suite): diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 598ec0bf9d..cfbec71d80 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -20,7 +20,7 @@ from psutil import FREEBSD from psutil import NETBSD from psutil import OPENBSD -from psutil.tests import get_test_subprocess +from psutil.tests import spawn_testproc from psutil.tests import HAS_BATTERY from psutil.tests import PsutilTestCase from psutil.tests import retry_on_failure @@ -78,7 +78,7 @@ class BSDTestCase(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = get_test_subprocess().pid + cls.pid = spawn_testproc().pid @classmethod def tearDownClass(cls): @@ -153,7 +153,7 @@ class FreeBSDPsutilTestCase(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = get_test_subprocess().pid + cls.pid = spawn_testproc().pid @classmethod def tearDownClass(cls): diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 3d004f0a2b..d8f3903591 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -32,7 +32,7 @@ from psutil._compat import super from psutil.tests import CIRRUS from psutil.tests import create_sockets -from psutil.tests import get_test_subprocess +from psutil.tests import spawn_testproc from psutil.tests import get_testfn from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_CPU_FREQ @@ -282,7 +282,7 @@ class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @classmethod def setUpClass(cls): super().setUpClass() - p = get_test_subprocess() + p = spawn_testproc() cls.proc = psutil.Process(p.pid) cls.proc.kill() cls.proc.wait() diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index c2e6ad7226..1d6e1dc91e 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -12,8 +12,8 @@ import psutil from psutil import MACOS -from psutil.tests import create_zombie_proc -from psutil.tests import get_test_subprocess +from psutil.tests import spawn_zombie +from psutil.tests import spawn_testproc from psutil.tests import HAS_BATTERY from psutil.tests import PsutilTestCase from psutil.tests import retry_on_failure @@ -81,7 +81,7 @@ class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = get_test_subprocess().pid + cls.pid = spawn_testproc().pid @classmethod def tearDownClass(cls): @@ -107,7 +107,7 @@ class TestZombieProcessAPIs(PsutilTestCase): @classmethod def setUpClass(cls): - cls.parent, cls.zombie = create_zombie_proc() + cls.parent, cls.zombie = spawn_zombie() @classmethod def tearDownClass(cls): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 9eeb5c2b90..12ea668212 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -24,7 +24,7 @@ from psutil import SUNOS from psutil.tests import CI_TESTING from psutil.tests import get_kernel_version -from psutil.tests import get_test_subprocess +from psutil.tests import spawn_testproc from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import mock from psutil.tests import PsutilTestCase @@ -132,9 +132,8 @@ class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = get_test_subprocess([PYTHON_EXE, "-E", "-O"], - stdin=subprocess.PIPE).pid - # wait_for_pid(cls.pid) + cls.pid = spawn_testproc([PYTHON_EXE, "-E", "-O"], + stdin=subprocess.PIPE).pid @classmethod def tearDownClass(cls): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 11ab725be9..45bac0c19e 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -72,57 +72,56 @@ class TestProcess(PsutilTestCase): """Tests for psutil.Process class.""" + def spawn_psproc(self, *args, **kwargs): + sproc = self.spawn_testproc(*args, **kwargs) + return psutil.Process(sproc.pid) + + # --- + def test_pid(self): p = psutil.Process() self.assertEqual(p.pid, os.getpid()) - sproc = self.get_test_subprocess() - self.assertEqual(psutil.Process(sproc.pid).pid, sproc.pid) with self.assertRaises(AttributeError): p.pid = 33 def test_kill(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.kill() code = p.wait() - self.assertFalse(psutil.pid_exists(sproc.pid)) + self.assertFalse(psutil.pid_exists(p.pid)) if POSIX: self.assertEqual(code, -signal.SIGKILL) def test_terminate(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.terminate() code = p.wait() - self.assertFalse(psutil.pid_exists(sproc.pid)) + self.assertFalse(psutil.pid_exists(p.pid)) if POSIX: self.assertEqual(code, -signal.SIGTERM) def test_send_signal(self): sig = signal.SIGKILL if POSIX else signal.SIGTERM - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.send_signal(sig) code = p.wait() self.assertFalse(psutil.pid_exists(p.pid)) if POSIX: self.assertEqual(code, -sig) # - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() 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 = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() 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) + p.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"). @@ -132,8 +131,7 @@ def test_send_signal(self): def test_wait(self): # check exit code signal - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.kill() code = p.wait() if POSIX: @@ -142,8 +140,7 @@ def test_wait(self): self.assertEqual(code, signal.SIGTERM) self.assertFalse(p.is_running()) - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.terminate() code = p.wait() if POSIX: @@ -154,8 +151,7 @@ def test_wait(self): # check sys.exit() code code = "import time, sys; time.sleep(0.01); sys.exit(5);" - sproc = self.get_test_subprocess([PYTHON_EXE, "-c", code]) - p = psutil.Process(sproc.pid) + p = self.spawn_psproc([PYTHON_EXE, "-c", code]) self.assertEqual(p.wait(), 5) self.assertFalse(p.is_running()) @@ -163,14 +159,12 @@ def test_wait(self): # 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 = self.get_test_subprocess([PYTHON_EXE, "-c", code]) - p = psutil.Process(sproc.pid) + p = self.spawn_psproc([PYTHON_EXE, "-c", code]) self.assertEqual(p.wait(), 5) self.assertIn(p.wait(), (5, None)) # test timeout - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.name() self.assertRaises(psutil.TimeoutExpired, p.wait, 0.01) @@ -180,7 +174,7 @@ def test_wait(self): def test_wait_non_children(self): # Test wait() against a process which is not our direct # child. - child, grandchild = self.create_proc_children_pair() + 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 @@ -199,8 +193,7 @@ def test_wait_non_children(self): self.assertEqual(child_ret, signal.SIGTERM) def test_wait_timeout_0(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() self.assertRaises(psutil.TimeoutExpired, p.wait, 0) p.kill() stop_at = time.time() + 2 @@ -269,9 +262,8 @@ def test_cpu_num(self): self.assertIn(p.cpu_num(), range(psutil.cpu_count())) def test_create_time(self): - sproc = self.get_test_subprocess() + p = self.spawn_psproc() 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 @@ -436,8 +428,7 @@ def test_rlimit_get(self): @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit_set(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + 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 @@ -449,8 +440,8 @@ def test_rlimit_set(self): @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit(self): - testfn = self.get_testfn() p = psutil.Process() + testfn = self.get_testfn() soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) try: p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) @@ -471,13 +462,12 @@ def test_rlimit(self): def test_rlimit_infinity(self): # First set a limit, then re-set it by specifying INFINITY # and assume we overridden the previous limit. - testfn = self.get_testfn() 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: + with open(self.get_testfn(), "wb") as f: f.write(b"X" * 2048) finally: p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) @@ -542,8 +532,7 @@ def test_threads(self): @skip_on_access_denied(only_if=MACOS) @unittest.skipIf(not HAS_THREADS, 'not supported') def test_threads_2(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() if OPENBSD: try: p.threads() @@ -588,8 +577,9 @@ def test_memory_info(self): self.assertGreaterEqual(getattr(mem, name), 0) def test_memory_full_info(self): + p = psutil.Process() total = psutil.virtual_memory().total - mem = psutil.Process().memory_full_info() + mem = p.memory_full_info() for name in mem._fields: value = getattr(mem, name) self.assertGreaterEqual(value, 0, msg=(name, value)) @@ -646,11 +636,12 @@ def test_memory_maps(self): @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 psutil.Process().memory_maps()] + for x in p.memory_maps()] self.assertIn(normpath(path), libpaths) def test_memory_percent(self): @@ -661,8 +652,7 @@ def test_memory_percent(self): p.memory_percent(memtype='uss') def test_is_running(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() assert p.is_running() assert p.is_running() p.kill() @@ -671,8 +661,8 @@ def test_is_running(self): assert not p.is_running() def test_exe(self): - sproc = self.get_test_subprocess() - exe = psutil.Process(sproc.pid).exe() + p = self.spawn_psproc() + exe = p.exe() try: self.assertEqual(exe, PYTHON_EXE) except AssertionError: @@ -700,10 +690,9 @@ def test_exe(self): def test_cmdline(self): cmdline = [PYTHON_EXE, "-c", "import time; time.sleep(60)"] - sproc = self.get_test_subprocess(cmdline) + p = self.spawn_psproc(cmdline) try: - self.assertEqual(' '.join(psutil.Process(sproc.pid).cmdline()), - ' '.join(cmdline)) + self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) except AssertionError: # XXX - most of the times the underlying sysctl() call on Net # and Open BSD returns a truncated string. @@ -711,8 +700,7 @@ def test_cmdline(self): # 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) + self.assertEqual(p.cmdline()[0], PYTHON_EXE) else: raise @@ -721,13 +709,12 @@ def test_long_cmdline(self): testfn = self.get_testfn() create_exe(testfn) cmdline = [testfn] + (["0123456789"] * 20) - sproc = self.get_test_subprocess(cmdline) - p = psutil.Process(sproc.pid) + p = self.spawn_psproc(cmdline) self.assertEqual(p.cmdline(), cmdline) def test_name(self): - sproc = self.get_test_subprocess(PYTHON_EXE) - name = psutil.Process(sproc.pid).name().lower() + p = self.spawn_psproc(PYTHON_EXE) + name = p.name().lower() pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) @@ -735,8 +722,7 @@ def test_name(self): def test_long_name(self): testfn = self.get_testfn(suffix="0123456789" * 2) create_exe(testfn) - sproc = self.get_test_subprocess(testfn) - p = psutil.Process(sproc.pid) + p = self.spawn_psproc(testfn) self.assertEqual(p.name(), os.path.basename(testfn)) # XXX @@ -752,8 +738,7 @@ def test_prog_w_funky_name(self): cmdline = [funky_path, "-c", "import time; [time.sleep(0.01) for x in range(3000)];" "arg1", "arg2", "", "arg3", ""] - sproc = self.get_test_subprocess(cmdline) - p = psutil.Process(sproc.pid) + p = self.spawn_psproc(cmdline) # ...in order to try to prevent occasional failures on travis if TRAVIS: wait_for_pid(p.pid) @@ -836,8 +821,7 @@ def test_status(self): self.assertEqual(p.status(), psutil.STATUS_RUNNING) def test_username(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() username = p.username() if WINDOWS: domain, username = username.split('\\') @@ -848,15 +832,13 @@ def test_username(self): self.assertEqual(username, getpass.getuser()) def test_cwd(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + 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)"] - sproc = self.get_test_subprocess(cmd) - p = psutil.Process(sproc.pid) + p = self.spawn_psproc(cmd) call_until(p.cwd, "ret == os.path.dirname(os.getcwd())") @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') @@ -904,8 +886,7 @@ def test_cpu_affinity(self): @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') def test_cpu_affinity_errs(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + 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)) @@ -937,9 +918,8 @@ def test_cpu_affinity_all_combinations(self): # can't find any process file on Appveyor @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") def test_open_files(self): - # current process - testfn = self.get_testfn() p = psutil.Process() + testfn = self.get_testfn() files = p.open_files() self.assertFalse(testfn in files) with open(testfn, 'wb') as f: @@ -958,8 +938,7 @@ def test_open_files(self): # another process cmdline = "import time; f = open(r'%s', 'r'); time.sleep(60);" % testfn - sproc = self.get_test_subprocess([PYTHON_EXE, "-c", cmdline]) - p = psutil.Process(sproc.pid) + 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()] @@ -977,10 +956,10 @@ def test_open_files(self): @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: - p = psutil.Process() for file in p.open_files(): if normcase(file.path) == normcase(fileobj.name) or \ file.fd == fileobj.fileno(): @@ -1001,8 +980,8 @@ def test_open_files_2(self): @unittest.skipIf(not POSIX, 'POSIX only') def test_num_fds(self): - testfn = self.get_testfn() p = psutil.Process() + testfn = self.get_testfn() start = p.num_fds() file = open(testfn, 'w') self.addCleanup(file.close) @@ -1026,78 +1005,73 @@ def test_num_ctx_switches(self): self.fail("num ctx switches still the same after 50.000 iterations") def test_ppid(self): + p = psutil.Process() if hasattr(os, 'getppid'): - self.assertEqual(psutil.Process().ppid(), os.getppid()) - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + self.assertEqual(p.ppid(), os.getppid()) + p = self.spawn_psproc() self.assertEqual(p.ppid(), os.getpid()) 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(), os.getpid(), msg=p) def test_parent(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + 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): - child, grandchild = self.create_proc_children_pair() + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() self.assertEqual(grandchild.parent(), child) - self.assertEqual(child.parent(), psutil.Process()) + self.assertEqual(child.parent(), parent) def test_parent_disappeared(self): # Emulate a case where the parent process disappeared. - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() with mock.patch("psutil.Process", side_effect=psutil.NoSuchProcess(0, 'foo')): self.assertIsNone(p.parent()) @retry_on_failure() def test_parents(self): - assert psutil.Process().parents() - child, grandchild = self.create_proc_children_pair() - self.assertEqual(child.parents()[0], psutil.Process()) + 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], psutil.Process()) + self.assertEqual(grandchild.parents()[1], parent) def test_children(self): - p = psutil.Process() - self.assertEqual(p.children(), []) - self.assertEqual(p.children(recursive=True), []) + 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. - sproc = self.get_test_subprocess(creationflags=0) - children1 = p.children() - children2 = p.children(recursive=True) + 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, sproc.pid) - self.assertEqual(children[0].ppid(), os.getpid()) + 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). - child, grandchild = self.create_proc_children_pair() - p = psutil.Process() - self.assertEqual(p.children(), [child]) - self.assertEqual(p.children(recursive=True), [child, 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(p.children(recursive=True), []) + self.assertEqual(parent.children(recursive=True), []) def test_children_duplicates(self): # find the process which has the highest number of children @@ -1118,21 +1092,20 @@ def test_children_duplicates(self): self.assertEqual(len(c), len(set(c))) def test_parents_and_children(self): - child, grandchild = self.create_proc_children_pair() - me = psutil.Process() + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() # forward - children = me.children(recursive=True) + 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], me) + self.assertEqual(parents[1], parent) def test_suspend_resume(self): - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.suspend() for x in range(100): if p.status() == psutil.STATUS_STOPPED: @@ -1192,8 +1165,8 @@ def test_as_dict(self): p.as_dict(['foo', 'bar']) def test_oneshot(self): + p = psutil.Process() with mock.patch("psutil._psplatform.Process.cpu_times") as m: - p = psutil.Process() with p.oneshot(): p.cpu_times() p.cpu_times() @@ -1207,9 +1180,9 @@ def test_oneshot(self): 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: - p = psutil.Process() with p.oneshot(): p.cpu_times() p.cpu_times() @@ -1228,7 +1201,7 @@ 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.create_proc_children_pair() + p1, p2 = self.spawn_children_pair() p1_ppid = p1.ppid() p2_ppid = p2.ppid() self.assertNotEqual(p1_ppid, p2_ppid) @@ -1247,8 +1220,7 @@ def test_halfway_terminated_process(self): # >>> time.sleep(2) # time-consuming task, process dies in meantime # >>> proc.name() # Refers to Issue #15 - sproc = self.get_test_subprocess() - p = psutil.Process(sproc.pid) + p = self.spawn_psproc() p.terminate() p.wait() if WINDOWS: @@ -1325,7 +1297,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): except (psutil.ZombieProcess, psutil.AccessDenied): pass - parent, zombie = self.create_zombie_proc() + parent, zombie = self.spawn_zombie() # A zombie process should always be instantiable zproc = psutil.Process(zombie.pid) # ...and at least its status always be querable @@ -1485,7 +1457,7 @@ def test_weird_environ(self): """) path = self.get_testfn() create_exe(path, c_code=code) - sproc = self.get_test_subprocess( + sproc = self.spawn_testproc( [path], stdin=subprocess.PIPE, stderr=subprocess.PIPE) p = psutil.Process(sproc.pid) wait_for_pid(p.pid) @@ -1503,6 +1475,7 @@ def test_weird_environ(self): 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 @@ -1514,7 +1487,7 @@ class LimitedUserTestCase(TestProcess): PROCESS_GID = os.getgid() def __init__(self, *args, **kwargs): - TestProcess.__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')]: @@ -1545,8 +1518,8 @@ def test_nice(self): else: self.fail("exception not raised") + @unittest.skipIf(1, "causes problem as root") def test_zombie_process(self): - # causes problems if test test suite is run as root pass diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index eda6db013a..09f5324feb 100644 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -60,7 +60,7 @@ class TestProcessAPIs(PsutilTestCase): def test_process_iter(self): self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) - sproc = self.get_test_subprocess() + sproc = self.spawn_testproc() self.assertIn(sproc.pid, [x.pid for x in psutil.process_iter()]) p = psutil.Process(sproc.pid) p.kill() @@ -96,15 +96,15 @@ def test_prcess_iter_w_attrs(self): assert m.called @unittest.skipIf(PYPY and WINDOWS, - "get_test_subprocess() unreliable on PYPY + WINDOWS") + "spawn_testproc() unreliable on PYPY + WINDOWS") def test_wait_procs(self): def callback(p): pids.append(p.pid) pids = [] - sproc1 = self.get_test_subprocess() - sproc2 = self.get_test_subprocess() - sproc3 = self.get_test_subprocess() + 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) @@ -153,18 +153,18 @@ def test(procs, callback): self.assertTrue(hasattr(p, 'returncode')) @unittest.skipIf(PYPY and WINDOWS, - "get_test_subprocess() unreliable on PYPY + WINDOWS") + "spawn_testproc() unreliable on PYPY + WINDOWS") def test_wait_procs_no_timeout(self): - sproc1 = self.get_test_subprocess() - sproc2 = self.get_test_subprocess() - sproc3 = self.get_test_subprocess() + 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() gone, alive = psutil.wait_procs(procs) def test_pid_exists(self): - sproc = self.get_test_subprocess() + sproc = self.spawn_testproc() self.assertTrue(psutil.pid_exists(sproc.pid)) p = psutil.Process(sproc.pid) p.kill() diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index e470c1b8d1..01176b7d09 100644 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -211,7 +211,7 @@ def test_chdir(self): class TestProcessUtils(PsutilTestCase): def test_reap_children(self): - subp = self.get_test_subprocess() + subp = self.spawn_testproc() p = psutil.Process(subp.pid) assert p.is_running() reap_children() @@ -219,8 +219,8 @@ def test_reap_children(self): assert not psutil.tests._pids_started assert not psutil.tests._subprocesses_started - def test_create_proc_children_pair(self): - child, grandchild = self.create_proc_children_pair() + 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() @@ -241,18 +241,18 @@ def test_create_proc_children_pair(self): assert not grandchild.is_running() @unittest.skipIf(not POSIX, "POSIX only") - def test_create_zombie_proc(self): - parent, zombie = self.create_zombie_proc() + 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.get_test_subprocess() + p = self.spawn_testproc() terminate(p) assert not psutil.pid_exists(p.pid) terminate(p) # by psutil.Process - p = psutil.Process(self.get_test_subprocess().pid) + p = psutil.Process(self.spawn_testproc().pid) terminate(p) assert not psutil.pid_exists(p.pid) terminate(p) @@ -263,13 +263,13 @@ def test_terminate(self): assert not psutil.pid_exists(p.pid) terminate(p) # by PID - pid = self.get_test_subprocess().pid + pid = self.spawn_testproc().pid terminate(pid) assert not psutil.pid_exists(pid) terminate(pid) # zombie if POSIX: - parent, zombie = self.create_zombie_proc() + parent, zombie = self.spawn_zombie() terminate(parent) terminate(zombie) assert not psutil.pid_exists(parent.pid) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 3f55e79760..e43d5326a6 100644 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -92,7 +92,7 @@ from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe -from psutil.tests import get_test_subprocess +from psutil.tests import spawn_testproc from psutil.tests import get_testfn from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import HAS_ENVIRON @@ -142,7 +142,7 @@ def subprocess_supports_unicode(suffix): try: safe_rmpath(testfn) create_exe(testfn) - sproc = get_test_subprocess(cmd=[testfn]) + sproc = spawn_testproc(cmd=[testfn]) except UnicodeEncodeError: return False else: @@ -177,7 +177,7 @@ def expect_exact_path_match(self): # --- def test_proc_exe(self): - subp = self.get_test_subprocess(cmd=[self.funky_name]) + subp = self.spawn_testproc(cmd=[self.funky_name]) p = psutil.Process(subp.pid) exe = p.exe() self.assertIsInstance(exe, str) @@ -186,14 +186,14 @@ def test_proc_exe(self): os.path.normcase(self.funky_name)) def test_proc_name(self): - subp = self.get_test_subprocess(cmd=[self.funky_name]) + subp = self.spawn_testproc(cmd=[self.funky_name]) 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 = self.get_test_subprocess(cmd=[self.funky_name]) + subp = self.spawn_testproc(cmd=[self.funky_name]) p = psutil.Process(subp.pid) cmdline = p.cmdline() for part in cmdline: @@ -350,7 +350,7 @@ def test_proc_environ(self): env = os.environ.copy() funky_str = UNICODE_SUFFIX if PY3 else 'è' env['FUNNY_ARG'] = funky_str - sproc = self.get_test_subprocess(env=env) + sproc = self.spawn_testproc(env=env) p = psutil.Process(sproc.pid) env = p.environ() for k, v in env.items(): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 057c2982ec..7387dfb71c 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -24,7 +24,7 @@ from psutil._compat import FileNotFoundError from psutil._compat import super from psutil.tests import APPVEYOR -from psutil.tests import get_test_subprocess +from psutil.tests import spawn_testproc from psutil.tests import HAS_BATTERY from psutil.tests import mock from psutil.tests import PsutilTestCase @@ -304,7 +304,7 @@ class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = get_test_subprocess().pid + cls.pid = spawn_testproc().pid @classmethod def tearDownClass(cls): @@ -384,7 +384,7 @@ def call(p, attr): @unittest.skipIf(not sys.version_info >= (2, 7), "CTRL_* signals not supported") def test_ctrl_signals(self): - p = psutil.Process(self.get_test_subprocess().pid) + p = psutil.Process(self.spawn_testproc().pid) p.send_signal(signal.CTRL_C_EVENT) p.send_signal(signal.CTRL_BREAK_EVENT) p.kill() @@ -533,7 +533,7 @@ class TestProcessWMI(TestCase): @classmethod def setUpClass(cls): - cls.pid = get_test_subprocess().pid + cls.pid = spawn_testproc().pid @classmethod def tearDownClass(cls): @@ -607,7 +607,7 @@ class TestDualProcessImplementation(TestCase): @classmethod def setUpClass(cls): - cls.pid = get_test_subprocess().pid + cls.pid = spawn_testproc().pid @classmethod def tearDownClass(cls): @@ -721,11 +721,11 @@ def setUp(self): super().setUp() env = os.environ.copy() env["THINK_OF_A_NUMBER"] = str(os.getpid()) - self.proc32 = self.get_test_subprocess( + self.proc32 = self.spawn_testproc( [self.python32] + self.test_args, env=env, stdin=subprocess.PIPE) - self.proc64 = self.get_test_subprocess( + self.proc64 = self.spawn_testproc( [self.python64] + self.test_args, env=env, stdin=subprocess.PIPE) From 26aebb6647b2c0f122f10bfad80bb6ede2a24985 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 May 2020 18:43:16 +0200 Subject: [PATCH 0554/1714] fix AttributeError --- psutil/tests/runner.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 14e33fbae2..97139fbc1c 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -216,10 +216,16 @@ def fdopen(fd, mode, *kwds): def _split_suite(suite): serial = unittest.TestSuite() parallel = unittest.TestSuite() - for test in suite._tests: + for test in suite: if test.countTestCases() == 0: continue - test_class = test._tests[0].__class__ + elif 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: From 5eec333ce3b76ec0d4d1b34ae5782bc9d6850e12 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 May 2020 19:37:19 +0200 Subject: [PATCH 0555/1714] refactor signal tests --- psutil/tests/test_process.py | 88 ++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 45bac0c19e..a9080b2664 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -88,24 +88,31 @@ def test_kill(self): p = self.spawn_psproc() p.kill() code = p.wait() - self.assertFalse(psutil.pid_exists(p.pid)) - if POSIX: + if WINDOWS: + self.assertEqual(code, signal.SIGTERM) + else: self.assertEqual(code, -signal.SIGKILL) + assert not p.is_running() + assert not psutil.pid_exists(p.pid) def test_terminate(self): p = self.spawn_psproc() p.terminate() code = p.wait() - self.assertFalse(psutil.pid_exists(p.pid)) - if POSIX: + if WINDOWS: + self.assertEqual(code, signal.SIGTERM) + else: self.assertEqual(code, -signal.SIGTERM) + assert not p.is_running() + assert not psutil.pid_exists(p.pid) def test_send_signal(self): sig = signal.SIGKILL if POSIX else signal.SIGTERM p = self.spawn_psproc() p.send_signal(sig) code = p.wait() - self.assertFalse(psutil.pid_exists(p.pid)) + assert not p.is_running() + assert not psutil.pid_exists(p.pid) if POSIX: self.assertEqual(code, -sig) # @@ -129,48 +136,22 @@ def test_send_signal(self): p = psutil.Process(0) self.assertRaises(ValueError, p.send_signal, signal.SIGTERM) - def test_wait(self): - # check exit code signal - p = self.spawn_psproc() - p.kill() - code = p.wait() - if POSIX: - self.assertEqual(code, -signal.SIGKILL) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - - p = self.spawn_psproc() - p.terminate() - code = p.wait() - if POSIX: - self.assertEqual(code, -signal.SIGTERM) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - + def test_wait_sysexit(self): # check sys.exit() code - code = "import time, sys; time.sleep(0.01); sys.exit(5);" - p = self.spawn_psproc([PYTHON_EXE, "-c", code]) + pycode = "import time, sys; time.sleep(0.01); sys.exit(5);" + p = self.spawn_psproc([PYTHON_EXE, "-c", pycode]) self.assertEqual(p.wait(), 5) - self.assertFalse(p.is_running()) + assert not p.is_running() - # Test wait() issued twice. + def test_wait_issued_twice(self): # 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. - p = self.spawn_psproc([PYTHON_EXE, "-c", code]) + pycode = "import time, sys; time.sleep(0.01); sys.exit(5);" + p = self.spawn_psproc([PYTHON_EXE, "-c", pycode]) self.assertEqual(p.wait(), 5) self.assertIn(p.wait(), (5, None)) - # test timeout - p = self.spawn_psproc() - 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. @@ -192,24 +173,31 @@ def test_wait_non_children(self): self.assertEqual(child_ret, signal.SIGTERM) self.assertEqual(child_ret, signal.SIGTERM) - def test_wait_timeout_0(self): + 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() + 2 - while True: + while time.time() < stop_at: try: code = p.wait(0) - except psutil.TimeoutExpired: - if time.time() >= stop_at: - raise - else: break + except psutil.TimeoutExpired: + pass + else: + raise self.fail('timeout') if POSIX: self.assertEqual(code, -signal.SIGKILL) else: self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) + assert not p.is_running() def test_cpu_percent(self): p = psutil.Process() @@ -921,7 +909,7 @@ def test_open_files(self): p = psutil.Process() testfn = self.get_testfn() files = p.open_files() - self.assertFalse(testfn in files) + self.assertNotIn(testfn, files) with open(testfn, 'wb') as f: f.write(b'x' * 1024) f.flush() @@ -1303,7 +1291,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): # ...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()) + assert zproc.is_running() # ...and as_dict() shouldn't crash zproc.as_dict() @@ -1347,7 +1335,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): # 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(zproc.pid)) + assert psutil.pid_exists(zproc.pid) if not TRAVIS and MACOS: # For some reason this started failing all of the sudden. # Maybe they upgraded MACOS version? @@ -1414,7 +1402,7 @@ def test_pid_0(self): if not OPENBSD: self.assertIn(0, psutil.pids()) - self.assertTrue(psutil.pid_exists(0)) + assert psutil.pid_exists(0) @unittest.skipIf(not HAS_ENVIRON, "not supported") def test_environ(self): @@ -1461,7 +1449,7 @@ def test_weird_environ(self): [path], stdin=subprocess.PIPE, stderr=subprocess.PIPE) p = psutil.Process(sproc.pid) wait_for_pid(p.pid) - self.assertTrue(p.is_running()) + assert p.is_running() # Wait for process to exec or exit. self.assertEqual(sproc.stderr.read(), b"") self.assertEqual(p.environ(), {"A": "1", "C": "3"}) From a6c0efbdbed7671b8512404090a15fa723805f05 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 1 May 2020 22:50:22 +0200 Subject: [PATCH 0556/1714] cleanup psutil mod namespace a bit --- psutil/__init__.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index acf5ee79f2..cabf0e0e35 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -21,7 +21,6 @@ """ from __future__ import division - import collections import contextlib import datetime @@ -39,9 +38,7 @@ from . import _common from ._common import AccessDenied -from ._common import deprecated_method from ._common import Error -from ._common import memoize from ._common import memoize_when_activated from ._common import NoSuchProcess from ._common import TimeoutExpired @@ -52,19 +49,6 @@ from ._compat import ProcessLookupError 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 CONN_CLOSE from ._common import CONN_CLOSE_WAIT from ._common import CONN_CLOSING @@ -80,6 +64,20 @@ from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN +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 AIX from ._common import BSD @@ -224,15 +222,14 @@ "users", "boot_time", # others ] +AF_LINK = _psplatform.AF_LINK + __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" __version__ = "5.7.1" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) -AF_LINK = _psplatform.AF_LINK -POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED -POWER_TIME_UNKNOWN = _common.POWER_TIME_UNKNOWN _TOTAL_PHYMEM = None _LOWEST_PID = None @@ -1070,7 +1067,7 @@ def memory_info(self): """ 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() @@ -2402,7 +2399,7 @@ def test(): # pragma: no cover 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 From 6f2bdc4223789af13156915e930f2fff7d68d46a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 2 May 2020 10:42:10 -0700 Subject: [PATCH 0557/1714] Parallelize fetch all processes tests with proc pool (#1746) Despite I recently implemented parallel tests on UNIX (#1709), TestFetchAllProcesses class is the slowest one to run because it gets all possible info for all processes in one go. In fact it's a singe unit-test, so it's not parallelized by the test runner. In here I used multiprocessing.Pool to do the trick. On my main linux box (8 cores): Before: ---------------------------------------------------------------------- Ran 1 test in 2.511s After: ---------------------------------------------------------------------- Ran 1 test in 0.931s On Windows (virtualized, 4 cores): Before: ---------------------------------------------------------------------- Ran 1 test in 13.752s After: ---------------------------------------------------------------------- Ran 1 test in 3.951s --- psutil/tests/test_contracts.py | 217 ++++++++++++++++----------------- 1 file changed, 103 insertions(+), 114 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index a80a6b78e0..be5ee78982 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -10,6 +10,7 @@ """ import errno +import multiprocessing import os import stat import time @@ -26,22 +27,25 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil._common import isfile_strict +from psutil._compat import FileNotFoundError from psutil._compat import long from psutil._compat import range from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import get_kernel_version from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_MEMORY_MAPS 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 PsutilTestCase +from psutil.tests import serialrun from psutil.tests import SKIP_SYSCONS from psutil.tests import unittest from psutil.tests import VALID_PROC_STATUSES -from psutil.tests import warn import psutil @@ -313,100 +317,82 @@ def test_users(self): # =================================================================== +def proc_info(pid): + # This function runs in a subprocess. + AD_SENTINEL = object() + names = psutil._as_dict_attrnames.copy() + if HAS_MEMORY_MAPS: + names.remove('memory_maps') + try: + p = psutil.Process(pid) + with p.oneshot(): + info = p.as_dict(names, ad_value=AD_SENTINEL) + if HAS_MEMORY_MAPS: + try: + info['memory_maps'] = p.memory_maps(grouped=False) + except psutil.AccessDenied: + pass + if HAS_RLIMIT: + try: + info['rlimit'] = p.rlimit(psutil.RLIMIT_NOFILE) + except psutil.AccessDenied: + pass + except psutil.NoSuchProcess as err: + assert err.pid == pid + return {} + else: + for k, v in info.copy().items(): + if v is AD_SENTINEL: + del info[k] + 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 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 setUp(self): + self.pool = multiprocessing.Pool() + + def tearDown(self): + self.pool.terminate() + self.pool.join() + + def test_all(self): + # Fixes "can't pickle : it's not the + # same object as test_contracts.proc_info". + from psutil.tests.test_contracts import proc_info - 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 + for info in self.pool.imap_unordered(proc_info, psutil.pids()): + for name, value in info.items(): meth = getattr(self, name) - meth(ret, p) - + try: + meth(value, info) + except AssertionError: + s = '\n' + '=' * 70 + '\n' + s += "FAIL: test_%s pid=%s, ret=%s\n" % ( + name, info['pid'], repr(value)) + s += '-' * 70 + s += "\n%s" % traceback.format_exc() + s = "\n".join((" " * 4) + i for i in s.splitlines()) + s += '\n' + failures.append(s) + else: + if value not in (0, 0.0, [], None, '', {}): + assert value, value if failures: - self.fail(''.join(failures)) - - # we should always have a non-empty list, not including PID 0 etc. - # special cases. - assert valid_procs + raise self.fail(''.join(failures)) - def cmdline(self, ret, proc): + def cmdline(self, ret, info): self.assertIsInstance(ret, list) for part in ret: self.assertIsInstance(part, str) - def exe(self, ret, proc): + def exe(self, ret, info): self.assertIsInstance(ret, (str, type(None))) if not ret: self.assertEqual(ret, '') @@ -422,27 +408,27 @@ def exe(self, ret, proc): # XXX may fail on MACOS assert os.access(ret, os.X_OK) - def pid(self, ret, proc): + def pid(self, ret, info): self.assertIsInstance(ret, int) self.assertGreaterEqual(ret, 0) - def ppid(self, ret, proc): + def ppid(self, ret, info): self.assertIsInstance(ret, (int, long)) self.assertGreaterEqual(ret, 0) - def name(self, ret, proc): + def name(self, ret, info): self.assertIsInstance(ret, str) # on AIX, "" processes don't have names if not AIX: assert ret - def create_time(self, ret, proc): + def create_time(self, ret, info): self.assertIsInstance(ret, float) try: self.assertGreaterEqual(ret, 0) except AssertionError: # XXX - if OPENBSD and proc.status() == psutil.STATUS_ZOMBIE: + if OPENBSD and info['status'] == psutil.STATUS_ZOMBIE: pass else: raise @@ -452,13 +438,13 @@ def create_time(self, ret, proc): # with strftime time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) - def uids(self, ret, proc): + 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, proc): + 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. @@ -467,24 +453,24 @@ def gids(self, ret, proc): if not MACOS and not NETBSD: self.assertGreaterEqual(gid, 0) - def username(self, ret, proc): + def username(self, ret, info): self.assertIsInstance(ret, str) assert ret - def status(self, ret, proc): + def status(self, ret, info): self.assertIsInstance(ret, str) assert ret self.assertNotEqual(ret, '?') # XXX self.assertIn(ret, VALID_PROC_STATUSES) - def io_counters(self, ret, proc): + 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, proc): + def ionice(self, ret, info): if LINUX: self.assertIsInstance(ret.ioclass, int) self.assertIsInstance(ret.value, int) @@ -500,11 +486,11 @@ def ionice(self, ret, proc): self.assertGreaterEqual(ret, 0) self.assertIn(ret, choices) - def num_threads(self, ret, proc): + def num_threads(self, ret, info): self.assertIsInstance(ret, int) self.assertGreaterEqual(ret, 1) - def threads(self, ret, proc): + def threads(self, ret, info): self.assertIsInstance(ret, list) for t in ret: assert is_namedtuple(t) @@ -514,18 +500,18 @@ def threads(self, ret, proc): for field in t: self.assertIsInstance(field, (int, float)) - def cpu_times(self, ret, proc): + 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, proc): + def cpu_percent(self, ret, info): self.assertIsInstance(ret, float) assert 0.0 <= ret <= 100.0, ret - def cpu_num(self, ret, proc): + def cpu_num(self, ret, info): self.assertIsInstance(ret, int) if FREEBSD and ret == -1: return @@ -534,7 +520,7 @@ def cpu_num(self, ret, proc): self.assertEqual(ret, 0) self.assertIn(ret, list(range(psutil.cpu_count()))) - def memory_info(self, ret, proc): + def memory_info(self, ret, info): assert is_namedtuple(ret) for value in ret: self.assertIsInstance(value, (int, long)) @@ -545,7 +531,7 @@ def memory_info(self, ret, proc): self.assertGreaterEqual(ret.peak_nonpaged_pool, ret.nonpaged_pool) self.assertGreaterEqual(ret.peak_pagefile, ret.pagefile) - def memory_full_info(self, ret, proc): + def memory_full_info(self, ret, info): assert is_namedtuple(ret) total = psutil.virtual_memory().total for name in ret._fields: @@ -561,7 +547,7 @@ def memory_full_info(self, ret, proc): if LINUX: self.assertGreaterEqual(ret.pss, ret.uss) - def open_files(self, ret, proc): + def open_files(self, ret, info): self.assertIsInstance(ret, list) for f in ret: self.assertIsInstance(f.fd, int) @@ -579,19 +565,22 @@ def open_files(self, ret, proc): # XXX see: https://github.com/giampaolo/psutil/issues/595 continue assert os.path.isabs(f.path), f - assert os.path.isfile(f.path), f + try: + assert isfile_strict(f.path), f + except FileNotFoundError: + pass - def num_fds(self, ret, proc): + def num_fds(self, ret, info): self.assertIsInstance(ret, int) self.assertGreaterEqual(ret, 0) - def connections(self, ret, proc): + def connections(self, ret, info): with create_sockets(): self.assertEqual(len(ret), len(set(ret))) for conn in ret: assert is_namedtuple(conn) - def cwd(self, ret, proc): + def cwd(self, ret, info): if ret: # 'ret' can be None or empty self.assertIsInstance(ret, str) assert os.path.isabs(ret), ret @@ -607,14 +596,14 @@ def cwd(self, ret, proc): else: assert stat.S_ISDIR(st.st_mode) - def memory_percent(self, ret, proc): + def memory_percent(self, ret, info): self.assertIsInstance(ret, float) assert 0 <= ret <= 100, ret - def is_running(self, ret, proc): + def is_running(self, ret, info): self.assertIsInstance(ret, bool) - def cpu_affinity(self, ret, proc): + def cpu_affinity(self, ret, info): self.assertIsInstance(ret, list) assert ret != [], ret cpus = list(range(psutil.cpu_count())) @@ -622,13 +611,13 @@ def cpu_affinity(self, ret, proc): self.assertIsInstance(n, int) self.assertIn(n, cpus) - def terminal(self, ret, proc): + 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, proc): + def memory_maps(self, ret, info): for nt in ret: self.assertIsInstance(nt.addr, str) self.assertIsInstance(nt.perms, str) @@ -650,11 +639,11 @@ def memory_maps(self, ret, proc): self.assertIsInstance(value, (int, long)) self.assertGreaterEqual(value, 0) - def num_handles(self, ret, proc): + def num_handles(self, ret, info): self.assertIsInstance(ret, int) self.assertGreaterEqual(ret, 0) - def nice(self, ret, proc): + def nice(self, ret, info): self.assertIsInstance(ret, int) if POSIX: assert -20 <= ret <= 20, ret @@ -663,19 +652,19 @@ def nice(self, ret, proc): if x.endswith('_PRIORITY_CLASS')] self.assertIn(ret, priorities) - def num_ctx_switches(self, ret, proc): + 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, proc): + 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, proc): + def environ(self, ret, info): self.assertIsInstance(ret, dict) for k, v in ret.items(): self.assertIsInstance(k, str) From 42368e6a786415d932cb48d0fd006a0c302ab31b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 2 May 2020 15:19:53 -0700 Subject: [PATCH 0558/1714] Process wait() improvements (#1747) * `Process.wait()` on POSIX now returns an `enum` showing the negative which was used to terminate the process: ```python >>> import psutil >>> p = psutil.Process(9891) >>> p.terminate() >>> p.wait() ``` * the return value is cached so that the exit code can be retrieved on then next call, mimicking `subprocess.Popen.wait()` * `Process` object provides more `status` and `exitcode` additional info on `str()` and `repr()`: ``` >>> proc psutil.Process(pid=12739, name='python3', status='terminated', exitcode=, started='15:08:20') ``` Extra: * improved `wait()` doc * reverted #1736: `psutil.Popen` uses original `subprocess.Popen.wait` method (safer) --- HISTORY.rst | 19 ++++- README.rst | 2 +- docs/Makefile | 5 +- docs/index.rst | 49 ++++++++----- psutil/__init__.py | 46 ++++++------ psutil/_psposix.py | 126 ++++++++++++++++++++++---------- psutil/tests/__init__.py | 38 +++++----- psutil/tests/test_contracts.py | 19 ++++- psutil/tests/test_linux.py | 1 + psutil/tests/test_misc.py | 20 ++++-- psutil/tests/test_process.py | 127 +++++++++++++++++++++------------ psutil/tests/test_testutils.py | 13 ++-- 12 files changed, 306 insertions(+), 159 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6f8087db33..0962be3e36 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,7 +10,24 @@ XXXX-XX-XX - 1729_: parallel tests on UNIX (make test-parallel). They're twice as fast! - 1741_: "make build/install" is now run in parallel and it's about 15% faster on UNIX. - +- 1747_: `Process.wait()` on POSIX returns an enum, showing the negative signal + which was used to terminate the process. + ``` + >>> import psutil + >>> p = psutil.Process(9891) + >>> p.terminate() + >>> p.wait() + + ``` +- 1747_: `Process.wait()` return value is cached so that the exit code can be + retrieved on then next call. +- 1747_: Process provides more info about the process on str() and repr() + (status and exit code). + ``` + >>> proc + psutil.Process(pid=12739, name='python3', status='terminated', + exitcode=, started='15:08:20') + ``` **Bug fixes** - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. diff --git a/README.rst b/README.rst index 62ab3f2348..2137a1a281 100644 --- a/README.rst +++ b/README.rst @@ -438,7 +438,7 @@ Process management >>> p.terminate() >>> p.kill() >>> p.wait(timeout=3) - 0 + >>> >>> psutil.test() USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND diff --git a/docs/Makefile b/docs/Makefile index cca5435fa2..860a2b0e2f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,8 +15,9 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -DEPS = sphinx - +DEPS = \ + sphinx \ + sphinx_rtd_theme .PHONY: help help: diff --git a/docs/index.rst b/docs/index.rst index 133e69fe77..699ea1f161 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1943,34 +1943,51 @@ Process class .. method:: wait(timeout=None) - Wait for process termination and if the process is a child of the current - one also return the 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 - :class:`NoSuchProcess`. + Wait for a process PID to terminate. The details about the return value + differ on UNIX and Windows. + + *On UNIX*: if the process terminated normally, the return value is a + positive integer >= 0 indicating the exit code. + If the process was terminated by a signal return the negated value of the + signal which caused the termination (e.g. ``-SIGTERM``). + If PID is not a children of `os.getpid`_ (current process) just wait until + the process disappears and return ``None``. + If PID does not exist return ``None`` immediately. + + *On Windows*: always return the exit code, which is a positive integer as + returned by `GetExitCodeProcess`_. + *timeout* is expressed in seconds. If specified and the process is still alive raise :class:`TimeoutExpired` exception. ``timeout=0`` can be used in non-blocking apps: it will either return immediately or raise :class:`TimeoutExpired`. + + The return value is cached. To wait for multiple processes use :func:`psutil.wait_procs()`. >>> import psutil >>> p = psutil.Process(9891) >>> p.terminate() >>> p.wait() + + + .. versionchanged:: 5.7.1 return value is cached (instead of returning + ``None``). + + .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it + as a human readable `enum`_. .. class:: Popen(*args, **kwargs) - Starts a sub-process via `subprocess.Popen`_, and in addition it provides - all the methods of :class:`psutil.Process` in a single class. - For method names common to both classes such as + Same as `subprocess.Popen`_ but in addition it provides all + :class:`psutil.Process` methods in a single class. + For the following methods which are common to both classes, psutil + implementation takes precedence: :meth:`send_signal() `, :meth:`terminate() `, - :meth:`kill() ` and - :meth:`wait() ` - :class:`psutil.Process` implementation takes precedence. - This may have some advantages, like making sure PID has not been reused, - fixing `BPO-6973`_. + :meth:`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 @@ -1988,9 +2005,6 @@ Process class .. versionchanged:: 4.4.0 added context manager support - .. versionchanged:: 5.7.1 wait() invokes :meth:`wait() ` - instead of `subprocess.Popen.wait`_. - Windows services ================ @@ -2818,11 +2832,12 @@ Timeline .. _`development guide`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py .. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 -.. _`enums`: https://docs.python.org/3/library/enum.html#module-enum +.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py .. _`GetDriveType`: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypea .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ .. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +.. _`GetExitCodeProcess`: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess .. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html .. _`hash`: https://docs.python.org/3/library/functions.html#hash .. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py diff --git a/psutil/__init__.py b/psutil/__init__.py index cabf0e0e35..7fdbc0fc26 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -232,6 +232,7 @@ _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 @@ -364,6 +365,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() @@ -394,18 +396,22 @@ def __str__(self): except AttributeError: info = {} # Python 2.6 info["pid"] = self.pid + if self._name: + info['name'] = self._name with self.oneshot(): try: info["name"] = self.name() info["status"] = self.status() - if self._create_time: - info['started'] = _pprint_secs(self._create_time) 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: + info['started'] = _pprint_secs(self._create_time) return "%s.%s(%s)" % ( self.__class__.__module__, self.__class__.__name__, @@ -1270,7 +1276,10 @@ def wait(self, timeout=None): """ if timeout is not None and not timeout >= 0: raise ValueError("timeout must be a positive integer") - return self._proc.wait(timeout) + 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(). @@ -1287,11 +1296,18 @@ 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 >>> p = psutil.Popen(["python", "-c", "print 'hi'"], stdout=PIPE) @@ -1307,14 +1323,6 @@ class Popen(Process): >>> 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/3/library/subprocess.html """ def __init__(self, *args, **kwargs): @@ -1361,11 +1369,7 @@ def __getattribute__(self, name): def wait(self, timeout=None): if self.__subproc.returncode is not None: return self.__subproc.returncode - # Note: using psutil's wait() on UNIX should make no difference. - # On Windows it does, because PID can still be alive (see - # _pswindows.py counterpart addressing this). Python 2.7 doesn't - # have timeout arg, so this acts as a backport. - ret = Process.wait(self, timeout) + ret = super(Popen, self).wait(timeout) self.__subproc.returncode = ret return ret diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 88213ef8b6..2e6711a3bc 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -6,6 +6,7 @@ import glob import os +import signal import sys import time @@ -21,6 +22,11 @@ from ._compat import PY3 from ._compat import unicode +if sys.version_info >= (3, 4): + import enum +else: + enum = None + __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] @@ -47,66 +53,108 @@ def pid_exists(pid): 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: + 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), + _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: + raise ValueError("can't wait for PID 0") # see "man waitpid" + 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() + retpid, status = os.waitpid(pid, flags) except InterruptedError: - delay = check_timeout(delay) + interval = sleep(interval) except ChildProcessError: # This has two meanings: - # - pid is not a child of os.getpid() in which case + # - 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 + # - 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 + 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): + # 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) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index c2766e23c5..ffb73f5b57 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -458,21 +458,6 @@ def sh(cmd, **kwds): return stdout -def _assert_no_pid(pid): - # This is here to make sure wait_procs() behaves properly and - # investigate: - # https://ci.appveyor.com/project/giampaolo/psutil/build/job/ - # jiq2cgd6stsbtn60 - assert not psutil.pid_exists(pid), pid - assert pid not in psutil.pids(), pid - try: - p = psutil.Process(pid) - except psutil.NoSuchProcess: - pass - else: - assert 0, "%s is still alive" % p - - 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(), @@ -494,9 +479,16 @@ def wait(proc, timeout): if POSIX: return wait_pid(proc.pid, timeout) + def sendsig(proc, sig): + # 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_subproc(proc, timeout): try: - proc.send_signal(sig) + sendsig(proc, sig) except OSError as err: if WINDOWS and err.winerror == 6: # "invalid handle" pass @@ -506,7 +498,7 @@ def term_subproc(proc, timeout): def term_psproc(proc, timeout): try: - proc.send_signal(sig) + sendsig(proc, sig) except psutil.NoSuchProcess: pass return wait(proc, timeout) @@ -543,7 +535,8 @@ def flush_popen(proc): finally: if isinstance(p, (subprocess.Popen, psutil.Popen)): flush_popen(p) - _assert_no_pid(p if isinstance(p, int) else p.pid) + pid = p if isinstance(p, int) else p.pid + assert not psutil.pid_exists(pid), pid def reap_children(recursive=False): @@ -881,6 +874,15 @@ def pyrun(self, *args, **kwds): self.addCleanup(terminate, sproc) # executed first return sproc + def assertProcessGone(self, proc): + self.assertRaises(psutil.NoSuchProcess, psutil.Process, proc.pid) + if isinstance(proc, (psutil.Process, psutil.Popen)): + assert not proc.is_running() + self.assertRaises(psutil.NoSuchProcess, proc.status) + proc.wait(timeout=0) # assert not raise TimeoutExpired + assert not psutil.pid_exists(proc.pid), proc.pid + self.assertNotIn(proc.pid, psutil.pids()) + @unittest.skipIf(PYPY, "unreliable on PYPY") class TestMemoryLeak(PsutilTestCase): diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index be5ee78982..70203c8e91 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -12,6 +12,7 @@ import errno import multiprocessing import os +import signal import stat import time import traceback @@ -254,9 +255,9 @@ def test_net_if_addrs(self): self.assertIsInstance(ifname, str) for addr in addrs: if enum is not None: - assert isinstance(addr.family, enum.IntEnum), addr + self.assertIsInstance(addr.family, enum.IntEnum) else: - assert isinstance(addr.family, int), addr + self.assertIsInstance(addr.family, int) self.assertIsInstance(addr.address, str) self.assertIsInstance(addr.netmask, (str, type(None))) self.assertIsInstance(addr.broadcast, (str, type(None))) @@ -671,6 +672,20 @@ def environ(self, ret, info): self.assertIsInstance(v, str) +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/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index fcc9d5db6c..709c77bab9 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1362,6 +1362,7 @@ def test_procfs_path(self): 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 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 4fb8ba5a92..15b589ad65 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -57,20 +57,25 @@ class TestMisc(PsutilTestCase): def test_process__repr__(self, func=repr): - p = psutil.Process() + 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=", r) + self.assertIn("name='%s'" % p.name(), r) self.assertIn("status=", r) - self.assertIn(p.name(), r) - self.assertIn("status='running'", 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("zombie", r) + self.assertIn("status='zombie'", r) self.assertNotIn("name=", r) with mock.patch.object(psutil.Process, "name", side_effect=psutil.NoSuchProcess(os.getpid())): @@ -303,7 +308,10 @@ def test_supports_ipv6(self): else: with self.assertRaises(Exception): sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - sock.bind(("::1", 0)) + try: + sock.bind(("::1", 0)) + finally: + sock.close() def test_isfile_strict(self): from psutil._common import isfile_strict diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a9080b2664..dbf15f1cf6 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -92,8 +92,7 @@ def test_kill(self): self.assertEqual(code, signal.SIGTERM) else: self.assertEqual(code, -signal.SIGKILL) - assert not p.is_running() - assert not psutil.pid_exists(p.pid) + self.assertProcessGone(p) def test_terminate(self): p = self.spawn_psproc() @@ -103,54 +102,79 @@ def test_terminate(self): self.assertEqual(code, signal.SIGTERM) else: self.assertEqual(code, -signal.SIGTERM) - assert not p.is_running() - assert not psutil.pid_exists(p.pid) + 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() - assert not p.is_running() - assert not psutil.pid_exists(p.pid) - if POSIX: + if WINDOWS: + self.assertEqual(code, sig) + else: self.assertEqual(code, -sig) - # - p = self.spawn_psproc() - p.send_signal(sig) - with mock.patch('psutil.os.kill', - side_effect=OSError(errno.ESRCH, "")): - with self.assertRaises(psutil.NoSuchProcess): - p.send_signal(sig) - # - p = self.spawn_psproc() - p.send_signal(sig) - with mock.patch('psutil.os.kill', - side_effect=OSError(errno.EPERM, "")): - with self.assertRaises(psutil.AccessDenied): - p.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_sysexit(self): - # check sys.exit() code - pycode = "import time, sys; time.sleep(0.01); sys.exit(5);" - p = self.spawn_psproc([PYTHON_EXE, "-c", pycode]) - self.assertEqual(p.wait(), 5) - assert not p.is_running() + 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) - def test_wait_issued_twice(self): - # 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. - pycode = "import time, sys; time.sleep(0.01); sys.exit(5);" - p = self.spawn_psproc([PYTHON_EXE, "-c", pycode]) - self.assertEqual(p.wait(), 5) - self.assertIn(p.wait(), (5, None)) + 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 @@ -197,7 +221,7 @@ def test_wait_timeout_nonblocking(self): self.assertEqual(code, -signal.SIGKILL) else: self.assertEqual(code, signal.SIGTERM) - assert not p.is_running() + self.assertProcessGone(p) def test_cpu_percent(self): p = psutil.Process() @@ -1213,7 +1237,7 @@ def test_halfway_terminated_process(self): p.wait() if WINDOWS: call_until(psutil.pids, "%s not in ret" % p.pid) - assert not p.is_running() + self.assertProcessGone(p) if WINDOWS: with self.assertRaises(psutil.NoSuchProcess): @@ -1371,8 +1395,16 @@ def test_pid_0(self): self.assertRaises(psutil.NoSuchProcess, psutil.Process, 0) return - # test all methods 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 for name in psutil._as_dict_attrnames: if name == 'pid': continue @@ -1536,7 +1568,10 @@ def test_misc(self): self.assertTrue(dir(proc)) self.assertRaises(AttributeError, getattr, proc, 'foo') proc.terminate() - proc.wait(timeout=3) + if POSIX: + self.assertEqual(proc.wait(), -signal.SIGTERM) + else: + self.assertEqual(proc.wait(), signal.SIGTERM) def test_ctx_manager(self): with psutil.Popen([PYTHON_EXE, "-V"], diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 01176b7d09..4fc6b33f50 100644 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -249,31 +249,31 @@ def test_terminate(self): # by subprocess.Popen p = self.spawn_testproc() terminate(p) - assert not psutil.pid_exists(p.pid) + self.assertProcessGone(p) terminate(p) # by psutil.Process p = psutil.Process(self.spawn_testproc().pid) terminate(p) - assert not psutil.pid_exists(p.pid) + self.assertProcessGone(p) terminate(p) # by psutil.Popen cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] p = psutil.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) terminate(p) - assert not psutil.pid_exists(p.pid) + self.assertProcessGone(p) terminate(p) # by PID pid = self.spawn_testproc().pid terminate(pid) - assert not psutil.pid_exists(pid) + self.assertProcessGone(p) terminate(pid) # zombie if POSIX: parent, zombie = self.spawn_zombie() terminate(parent) terminate(zombie) - assert not psutil.pid_exists(parent.pid) - assert not psutil.pid_exists(zombie.pid) + self.assertProcessGone(parent) + self.assertProcessGone(zombie) class TestNetUtils(PsutilTestCase): @@ -373,6 +373,7 @@ def test_param_err(self): self.assertRaises(ValueError, self.execute, lambda: 0, tolerance=-1) self.assertRaises(ValueError, self.execute, lambda: 0, retry_for=-1) + @retry_on_failure() def test_leak(self): def fun(): ls.append("x" * 24 * 1024) From 601b2ba899120e39b280c348a11769dc168c8be3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 May 2020 00:22:46 +0200 Subject: [PATCH 0559/1714] update README --- HISTORY.rst | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0962be3e36..24186c3d29 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,23 +11,20 @@ XXXX-XX-XX - 1741_: "make build/install" is now run in parallel and it's about 15% faster on UNIX. - 1747_: `Process.wait()` on POSIX returns an enum, showing the negative signal - which was used to terminate the process. - ``` - >>> import psutil - >>> p = psutil.Process(9891) - >>> p.terminate() - >>> p.wait() - - ``` + which was used to terminate the process:: + >>> import psutil + >>> p = psutil.Process(9891) + >>> p.terminate() + >>> p.wait() + - 1747_: `Process.wait()` return value is cached so that the exit code can be retrieved on then next call. - 1747_: Process provides more info about the process on str() and repr() - (status and exit code). - ``` - >>> proc - psutil.Process(pid=12739, name='python3', status='terminated', - exitcode=, started='15:08:20') - ``` + (status and exit code):: + >>> proc + psutil.Process(pid=12739, name='python3', status='terminated', + exitcode=, started='15:08:20') + **Bug fixes** - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. From 42b2cd718a3caf813b4eccce13ac3629599cb017 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 4 May 2020 10:22:33 -0700 Subject: [PATCH 0560/1714] Refactor tests calling all process methods (process_namespace class) (#1749) Over the years I have accumulated different unit-tests which use dir() to get all process methods and test them in different circumstances. This produced a lot of code duplication. With this PR I introduce 2 new test classes (process_namespace and system_namespace) which declare all the method names and arguments in a single place, removing a lot cruft and code duplication. --- HISTORY.rst | 25 ++-- psutil/tests/__init__.py | 229 +++++++++++++++++++++++++++++- psutil/tests/test_contracts.py | 99 +++++++------ psutil/tests/test_memory_leaks.py | 121 ++++++++++------ psutil/tests/test_misc.py | 4 +- psutil/tests/test_posix.py | 42 ------ psutil/tests/test_process.py | 157 ++++++-------------- psutil/tests/test_testutils.py | 16 +++ psutil/tests/test_windows.py | 36 ----- 9 files changed, 430 insertions(+), 299 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0962be3e36..24186c3d29 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,23 +11,20 @@ XXXX-XX-XX - 1741_: "make build/install" is now run in parallel and it's about 15% faster on UNIX. - 1747_: `Process.wait()` on POSIX returns an enum, showing the negative signal - which was used to terminate the process. - ``` - >>> import psutil - >>> p = psutil.Process(9891) - >>> p.terminate() - >>> p.wait() - - ``` + which was used to terminate the process:: + >>> import psutil + >>> p = psutil.Process(9891) + >>> p.terminate() + >>> p.wait() + - 1747_: `Process.wait()` return value is cached so that the exit code can be retrieved on then next call. - 1747_: Process provides more info about the process on str() and repr() - (status and exit code). - ``` - >>> proc - psutil.Process(pid=12739, name='python3', status='terminated', - exitcode=, started='15:08:20') - ``` + (status and exit code):: + >>> proc + psutil.Process(pid=12739, name='python3', status='terminated', + exitcode=, started='15:08:20') + **Bug fixes** - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ffb73f5b57..ea283f6e71 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -37,6 +37,7 @@ import psutil from psutil import AIX +from psutil import LINUX from psutil import MACOS from psutil import POSIX from psutil import SUNOS @@ -89,6 +90,7 @@ # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', + 'process_namespace', 'system_namespace', # install utils 'install_pip', 'install_test_deps', # fs utils @@ -214,6 +216,8 @@ def attempt(exe): PYTHON_EXE = _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()) @@ -836,6 +840,11 @@ def __str__(self): if not hasattr(unittest.TestCase, 'assertRaisesRegex'): assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + # ...otherwise multiprocessing.Pool complains + if not PY3: + def runTest(self): + pass + # monkey patch default unittest.TestCase unittest.TestCase = TestCase @@ -998,6 +1007,221 @@ def call(): self.execute(call, **kwargs) +def _get_eligible_cpu(): + p = psutil.Process() + if hasattr(p, "cpu_num"): + return p.cpu_num() + elif hasattr(p, "cpu_affinity"): + return p.cpu_affinity()[0] + 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(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) + + +process_namespace._test() + + +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.append(('cpu_freq', (), {'percpu': True})) + if HAS_GETLOADAVG: + getters.append(('getloadavg', (), {})) + if HAS_SENSORS_TEMPERATURES: + getters.append(('sensors_temperatures', (), {})) + if HAS_SENSORS_FANS: + getters.append(('sensors_fans', (), {})) + if HAS_SENSORS_BATTERY: + getters.append(('sensors_battery', (), {})) + if WINDOWS: + getters.append(('win_service_iter', (), {})) + getters.append(('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) + + @classmethod + def _test(cls): + this = set([x[0] for x in cls.all]) + ignored = set([x[0] for x in cls.ignored]) + # there's a separate test for __all__ + mod = set([x for x in dir(psutil) if x.islower() and x[0] != '_' and + x in psutil.__all__ and callable(getattr(psutil, x))]) + leftout = (this | ignored) ^ mod + if leftout: + raise ValueError("uncovered psutil mod name(s): %r" % leftout) + + +system_namespace._test() + + def serialrun(klass): """A decorator to mark a TestCase class. When running parallel tests, class' unit tests will be run serially (1 process). @@ -1317,9 +1541,6 @@ def copyload_shared_lib(suffix=""): # =================================================================== -atexit.register(DEVNULL.close) - - # this is executed first @atexit.register def cleanup_test_procs(): @@ -1328,7 +1549,7 @@ def cleanup_test_procs(): # 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 -# modul. With this it will. See: +# module. With this it will. See: # http://grodola.blogspot.com/ # 2016/02/how-to-always-execute-exit-functions-in-py.html if POSIX: diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 70203c8e91..edeb1d9a57 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -36,12 +36,11 @@ from psutil.tests import enum from psutil.tests import get_kernel_version from psutil.tests import HAS_CPU_FREQ -from psutil.tests import HAS_MEMORY_MAPS 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 process_namespace from psutil.tests import PsutilTestCase from psutil.tests import serialrun from psutil.tests import SKIP_SYSCONS @@ -184,7 +183,7 @@ def test_memory_maps(self): # =================================================================== -# --- System API types +# --- API types # =================================================================== @@ -313,39 +312,69 @@ def test_users(self): 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) + + # =================================================================== # --- Featch all processes test # =================================================================== def proc_info(pid): - # This function runs in a subprocess. - AD_SENTINEL = object() - names = psutil._as_dict_attrnames.copy() - if HAS_MEMORY_MAPS: - names.remove('memory_maps') + tcase = PsutilTestCase() + + def check_exception(exc, proc, name, ppid): + tcase.assertEqual(exc.pid, pid) + tcase.assertEqual(exc.name, name) + if isinstance(exc, psutil.ZombieProcess): + # XXX investigate zombie/ppid relation on POSIX + # tcase.assertEqual(exc.ppid, ppid) + pass + elif isinstance(exc, psutil.NoSuchProcess): + tcase.assertProcessGone(proc) + str(exc) + assert exc.msg + + def do_wait(): + if pid != 0: + try: + proc.wait(0) + except psutil.Error as exc: + check_exception(exc, proc, name, ppid) + try: - p = psutil.Process(pid) - with p.oneshot(): - info = p.as_dict(names, ad_value=AD_SENTINEL) - if HAS_MEMORY_MAPS: - try: - info['memory_maps'] = p.memory_maps(grouped=False) - except psutil.AccessDenied: - pass - if HAS_RLIMIT: - try: - info['rlimit'] = p.rlimit(psutil.RLIMIT_NOFILE) - except psutil.AccessDenied: - pass - except psutil.NoSuchProcess as err: - assert err.pid == pid + proc = psutil.Process(pid) + d = proc.as_dict(['ppid', 'name']) + except psutil.NoSuchProcess: return {} - else: - for k, v in info.copy().items(): - if v is AD_SENTINEL: - del info[k] - return info + + name, ppid = d['name'], d['ppid'] + info = {'pid': proc.pid} + ns = process_namespace(proc) + with proc.oneshot(): + for fun, fun_name in ns.iter(ns.getters, clear_cache=False): + try: + info[fun_name] = fun() + except psutil.NoSuchProcess as exc: + check_exception(exc, proc, name, ppid) + do_wait() + return info + except psutil.AccessDenied as exc: + check_exception(exc, proc, name, ppid) + continue + do_wait() + return info @serialrun @@ -672,20 +701,6 @@ def environ(self, ret, info): self.assertIsInstance(v, str) -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/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index d8f3903591..2eb2bc65cd 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -32,7 +32,6 @@ from psutil._compat import super from psutil.tests import CIRRUS from psutil.tests import create_sockets -from psutil.tests import spawn_testproc from psutil.tests import get_testfn from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_CPU_FREQ @@ -46,7 +45,11 @@ 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 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 from psutil.tests import TestMemoryLeak from psutil.tests import TRAVIS from psutil.tests import unittest @@ -56,11 +59,6 @@ thisproc = psutil.Process() -# =================================================================== -# utils -# =================================================================== - - def skip_if_linux(): return unittest.skipIf(LINUX and SKIP_PYTHON_IMPL, "worthless on LINUX (pure python)") @@ -77,17 +75,10 @@ class TestProcessObjectLeaks(TestMemoryLeak): 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) + p = psutil.Process() + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters + ns.setters): + assert hasattr(self, "test_" + name), name @skip_if_linux() def test_name(self): @@ -119,7 +110,7 @@ def test_gids(self): def test_status(self): self.execute(self.proc.status) - def test_nice_get(self): + def test_nice(self): self.execute(self.proc.nice) def test_nice_set(self): @@ -127,7 +118,7 @@ def test_nice_set(self): self.execute(lambda: self.proc.nice(niceness)) @unittest.skipIf(not HAS_IONICE, "not supported") - def test_ionice_get(self): + def test_ionice(self): self.execute(self.proc.ionice) @unittest.skipIf(not HAS_IONICE, "not supported") @@ -208,7 +199,7 @@ def test_cwd(self): self.execute(self.proc.cwd) @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") - def test_cpu_affinity_get(self): + def test_cpu_affinity(self): self.execute(self.proc.cpu_affinity) @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") @@ -231,7 +222,7 @@ def test_memory_maps(self): @unittest.skipIf(not LINUX, "LINUX only") @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_get(self): + def test_rlimit(self): self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE)) @unittest.skipIf(not LINUX, "LINUX only") @@ -251,7 +242,7 @@ def test_connections(self): # be executed. with create_sockets(): kind = 'inet' if SUNOS else 'all' - self.execute(lambda: self.proc.connections(kind)) + self.execute(lambda: self.proc.connections(kind), times=100) @unittest.skipIf(not HAS_ENVIRON, "not supported") def test_environ(self): @@ -262,16 +253,6 @@ def test_proc_info(self): self.execute(cext.proc_info, os.getpid()) -@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)) - - class TestTerminatedProcessLeaks(TestProcessObjectLeaks): """Repeat the tests above looking for leaks occurring when dealing with terminated processes raising NoSuchProcess exception. @@ -282,11 +263,16 @@ class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @classmethod def setUpClass(cls): super().setUpClass() - p = spawn_testproc() - cls.proc = psutil.Process(p.pid) + 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() @@ -321,6 +307,16 @@ def call(): 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 # =================================================================== @@ -330,20 +326,14 @@ class TestModuleFunctionsLeaks(TestMemoryLeak): """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) + ns = system_namespace + for fun, name in ns.iter(ns.all): + assert hasattr(self, "test_" + name), name # --- cpu @skip_if_linux() - def test_cpu_count_logical(self): + def test_cpu_count(self): # logical self.execute(lambda: psutil.cpu_count(logical=True)) @skip_if_linux() @@ -421,7 +411,7 @@ def test_net_io_counters(self): @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") def test_net_connections(self): with create_sockets(): - self.execute(psutil.net_connections) + self.execute(psutil.net_connections, times=100) def test_net_if_addrs(self): # Note: verified that on Windows this was a false positive. @@ -482,6 +472,47 @@ def test_win_service_get_description(self): self.execute(lambda: cext.winservice_query_descr(name)) +# ===================================================================== +# --- File descriptors and handlers +# ===================================================================== + + +class TestUnclosedFdsOrHandles(unittest.TestCase): + """Call a function N times (twice) and make sure the number of file + descriptors (POSIX) or handles (Windows) does not increase. Done in + order to discover forgotten close(2) and CloseHandle syscalls. + """ + times = 2 + + def execute(self, iterator): + p = psutil.Process() + failures = [] + for fun, fun_name in iterator: + before = p.num_fds() if POSIX else p.num_handles() + try: + for x in range(self.times): + fun() + except psutil.Error: + continue + else: + after = p.num_fds() if POSIX else p.num_handles() + if abs(after - before) > 0: + fail = "failure while calling %s function " \ + "(before=%s, after=%s)" % (fun, before, after) + failures.append(fail) + if failures: + self.fail('\n' + '\n'.join(failures)) + + def test_process_apis(self): + p = psutil.Process() + ns = process_namespace(p) + self.execute(ns.iter(ns.getters + ns.setters)) + + def test_system_apis(self): + ns = system_namespace + self.execute(ns.iter(ns.all)) + + if __name__ == '__main__': from psutil.tests.runner import run_from_name run_from_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 15b589ad65..300360cd21 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -161,9 +161,7 @@ def test_process__hash__(self): 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', 'PermissionError', + if name in ('long', 'tests', 'test', 'PermissionError', 'ProcessLookupError'): continue if not name.startswith('_'): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 12ea668212..e2d18ccb9f 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -23,7 +23,6 @@ from psutil import POSIX from psutil import SUNOS from psutil.tests import CI_TESTING -from psutil.tests import get_kernel_version from psutil.tests import spawn_testproc from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import mock @@ -283,47 +282,6 @@ def test_nice(self): 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(PsutilTestCase): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index dbf15f1cf6..07a00e81cb 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -50,6 +50,7 @@ from psutil.tests import HAS_RLIMIT from psutil.tests import HAS_THREADS from psutil.tests import mock +from psutil.tests import process_namespace from psutil.tests import PsutilTestCase from psutil.tests import PYPY from psutil.tests import PYTHON_EXE @@ -1232,80 +1233,45 @@ def test_halfway_terminated_process(self): # >>> 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: + 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) + + # NtQuerySystemInformation succeeds even if process is gone. 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: - # NtQuerySystemInformation succeeds if process is gone. - if WINDOWS and name in ('exe', 'name'): - normcase = os.path.normcase - if name == 'exe': - self.assertEqual(normcase(ret), normcase(PYTHON_EXE)) - else: - self.assertEqual( - normcase(ret), - normcase(os.path.basename(PYTHON_EXE))) - continue - self.fail( - "NoSuchProcess exception not raised for %r, retval=%s" % ( - name, ret)) + normcase = os.path.normcase + self.assertEqual(normcase(p.exe()), normcase(PYTHON_EXE)) @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process(self): - def succeed_or_zombie_p_exc(fun, *args, **kwargs): + def succeed_or_zombie_p_exc(fun): try: - return fun(*args, **kwargs) + return fun() except (psutil.ZombieProcess, psutil.AccessDenied): pass @@ -1318,39 +1284,7 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): assert zproc.is_running() # ...and as_dict() shouldn't crash zproc.as_dict() - - 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 + # ...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) @@ -1359,6 +1293,11 @@ def succeed_or_zombie_p_exc(fun, *args, **kwargs): # 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 + + ns = process_namespace(zproc) + for fun, name in ns.iter(ns.all): + succeed_or_zombie_p_exc(fun) + assert psutil.pid_exists(zproc.pid) if not TRAVIS and MACOS: # For some reason this started failing all of the sudden. @@ -1393,6 +1332,10 @@ 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) @@ -1405,33 +1348,21 @@ def test_pid_0(self): self.assertRaises(exc, p.send_signal, signal.SIGTERM) # test all methods - for name in psutil._as_dict_attrnames: - if name == 'pid': - continue - meth = getattr(p, name) + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters + ns.setters): try: - ret = meth() + ret = fun() 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') + user = 'NT AUTHORITY\\SYSTEM' if WINDOWS else 'root' + self.assertEqual(p.username(), user) 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()) assert psutil.pid_exists(0) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 4fc6b33f50..56a465d7b5 100644 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -35,6 +35,7 @@ from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import is_namedtuple from psutil.tests import mock +from psutil.tests import process_namespace from psutil.tests import PsutilTestCase from psutil.tests import PYTHON_EXE from psutil.tests import reap_children @@ -43,6 +44,7 @@ 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 TestMemoryLeak @@ -419,6 +421,20 @@ def fun(): self.execute_w_exc(ZeroDivisionError, fun) +class TestTestingUtils(PsutilTestCase): + + def test_process_namespace(self): + p = psutil.Process() + ns = process_namespace(p) + 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): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 7387dfb71c..23ad05843e 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -345,42 +345,6 @@ def test_num_handles_increment(self): 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): From 15b35646d95b617b04a2e556f1b0902d3f64c3cb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 4 May 2020 20:01:36 +0200 Subject: [PATCH 0561/1714] have mem leak test class check num of fds/handles --- psutil/tests/__init__.py | 24 +++++++++++++++++- psutil/tests/test_memory_leaks.py | 41 ------------------------------- psutil/tests/test_testutils.py | 25 +++++++++++++++---- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ea283f6e71..91a519c18c 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -900,7 +900,13 @@ class TestMemoryLeak(PsutilTestCase): It does so by calling a function many times, and checks whether the process memory usage increased before and after having called the function repeadetly. + + In addition also call the function onces and make sure num_fds() + (POSIX) or num_handles() (Windows) does not increase. This is done + in order to discover forgotten close(2) and CloseHandle syscalls. + Note that sometimes this may produce false positives. + PyPy appears to be completely unstable for this framework, probably because of how its JIT handles memory, so tests on PYPY are automatically skipped. @@ -910,6 +916,7 @@ class TestMemoryLeak(PsutilTestCase): warmup_times = 10 tolerance = 4096 # memory retry_for = 3.0 # seconds + check_fds = True # whether to check if num_fds() increased verbose = True def setUp(self): @@ -922,6 +929,12 @@ def _get_mem(self): mem = self._thisproc.memory_full_info() return getattr(mem, "uss", mem.rss) + def _get_fds_or_handles(self): + if POSIX: + return self._thisproc.num_fds() + else: + return self._thisproc.num_handles() + def _call(self, fun): return fun() @@ -959,7 +972,7 @@ def _log(self, msg): print_color(msg, color="yellow", file=sys.stderr) def execute(self, fun, times=times, warmup_times=warmup_times, - tolerance=tolerance, retry_for=retry_for): + tolerance=tolerance, retry_for=retry_for, check_fds=check_fds): """Test a callable.""" if times <= 0: raise ValueError("times must be > 0") @@ -970,6 +983,15 @@ def execute(self, fun, times=times, warmup_times=warmup_times, if retry_for is not None and retry_for < 0: raise ValueError("retry_for must be >= 0") + if check_fds: + before = self._get_fds_or_handles() + self._call(fun) + after = self._get_fds_or_handles() + diff = abs(after - before) + if diff > 0: + msg = "%s unclosed fd(s) or handle(s)" % (diff) + raise self.fail(msg) + # warm up self._call_ntimes(fun, warmup_times) mem1 = self._call_ntimes(fun, times) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 2eb2bc65cd..61049a910f 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -472,47 +472,6 @@ def test_win_service_get_description(self): self.execute(lambda: cext.winservice_query_descr(name)) -# ===================================================================== -# --- File descriptors and handlers -# ===================================================================== - - -class TestUnclosedFdsOrHandles(unittest.TestCase): - """Call a function N times (twice) and make sure the number of file - descriptors (POSIX) or handles (Windows) does not increase. Done in - order to discover forgotten close(2) and CloseHandle syscalls. - """ - times = 2 - - def execute(self, iterator): - p = psutil.Process() - failures = [] - for fun, fun_name in iterator: - before = p.num_fds() if POSIX else p.num_handles() - try: - for x in range(self.times): - fun() - except psutil.Error: - continue - else: - after = p.num_fds() if POSIX else p.num_handles() - if abs(after - before) > 0: - fail = "failure while calling %s function " \ - "(before=%s, after=%s)" % (fun, before, after) - failures.append(fail) - if failures: - self.fail('\n' + '\n'.join(failures)) - - def test_process_apis(self): - p = psutil.Process() - ns = process_namespace(p) - self.execute(ns.iter(ns.getters + ns.setters)) - - def test_system_apis(self): - ns = system_namespace - self.execute(ns.iter(ns.all)) - - if __name__ == '__main__': from psutil.tests.runner import run_from_name run_from_name(__file__) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 56a465d7b5..383b14709d 100644 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -352,21 +352,23 @@ def test_create_sockets(self): @serialrun class TestMemLeakClass(TestMemoryLeak): + @retry_on_failure() def test_times(self): def fun(): cnt['cnt'] += 1 cnt = {'cnt': 0} self.execute(fun, times=1, warmup_times=10) - self.assertEqual(cnt['cnt'], 11) + self.assertEqual(cnt['cnt'], 12) self.execute(fun, times=10, warmup_times=10) - self.assertEqual(cnt['cnt'], 31) + self.assertEqual(cnt['cnt'], 33) + @retry_on_failure() def test_warmup_times(self): def fun(): cnt['cnt'] += 1 cnt = {'cnt': 0} self.execute(fun, times=1, warmup_times=10) - self.assertEqual(cnt['cnt'], 11) + self.assertEqual(cnt['cnt'], 12) def test_param_err(self): self.assertRaises(ValueError, self.execute, lambda: 0, times=0) @@ -383,7 +385,7 @@ def fun(): times = 100 self.assertRaises(AssertionError, self.execute, fun, times=times, warmup_times=10, retry_for=None) - self.assertEqual(len(ls), times + 10) + self.assertEqual(len(ls), times + 11) @retry_on_failure(retries=20) # 2 secs def test_leak_with_retry(self, ls=[]): @@ -397,15 +399,17 @@ def fun(): self.assertIn("try calling fun for another", f.getvalue()) self.assertGreater(len(ls), times) + @retry_on_failure() 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) + tolerance=200 * 1024 * 1024, check_fds=False) self.assertEqual(len(ls), times) + @retry_on_failure() def test_execute_w_exc(self): def fun(): 1 / 0 @@ -420,6 +424,17 @@ def fun(): with self.assertRaises(AssertionError): self.execute_w_exc(ZeroDivisionError, fun) + def test_unclosed_fds(self): + def fun(): + f = open(__file__) + self.addCleanup(f.close) + box.append(f) + + box = [] + self.assertRaisesRegex( + AssertionError, r"1 unclosed fd\(s\) or handle\(s\)", + self.execute, fun, times=5, warmup_times=5) + class TestTestingUtils(PsutilTestCase): From 55cd834056e3a806f7cffead62e8cc4c23938ea3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 May 2020 02:42:16 +0200 Subject: [PATCH 0562/1714] don't run namespaces test at runtime + refactor coverage test --- psutil/tests/__init__.py | 36 +++++++++++++++++++------------ psutil/tests/test_memory_leaks.py | 11 ++++------ psutil/tests/test_testutils.py | 6 ++++-- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 91a519c18c..28586fd3de 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1154,7 +1154,19 @@ def clear_cache(self): self._proc._init(self._proc.pid, _ignore_nsp=True) @classmethod - def _test(cls): + 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] != '_']) @@ -1163,9 +1175,6 @@ def _test(cls): raise ValueError("uncovered Process class names: %r" % leftout) -process_namespace._test() - - class system_namespace: """A container that lists all the module-level, system-related APIs. Utilities such as cpu_percent() are excluded. Usage: @@ -1195,18 +1204,18 @@ class system_namespace: ('virtual_memory', (), {}), ] if HAS_CPU_FREQ: - getters.append(('cpu_freq', (), {'percpu': True})) + getters += [('cpu_freq', (), {'percpu': True})] if HAS_GETLOADAVG: - getters.append(('getloadavg', (), {})) + getters += [('getloadavg', (), {})] if HAS_SENSORS_TEMPERATURES: - getters.append(('sensors_temperatures', (), {})) + getters += [('sensors_temperatures', (), {})] if HAS_SENSORS_FANS: - getters.append(('sensors_fans', (), {})) + getters += [('sensors_fans', (), {})] if HAS_SENSORS_BATTERY: - getters.append(('sensors_battery', (), {})) + getters += [('sensors_battery', (), {})] if WINDOWS: - getters.append(('win_service_iter', (), {})) - getters.append(('win_service_get', ('alg', ), {})) + getters += [('win_service_iter', (), {})] + getters += [('win_service_get', ('alg', ), {})] ignored = [ ('process_iter', (), {}), @@ -1230,7 +1239,7 @@ def iter(ls): yield (fun, fun_name) @classmethod - def _test(cls): + def test(cls): this = set([x[0] for x in cls.all]) ignored = set([x[0] for x in cls.ignored]) # there's a separate test for __all__ @@ -1240,8 +1249,7 @@ def _test(cls): if leftout: raise ValueError("uncovered psutil mod name(s): %r" % leftout) - -system_namespace._test() + test_class_coverage = process_namespace.test_class_coverage def serialrun(klass): diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 61049a910f..b6845ac811 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -75,10 +75,8 @@ class TestProcessObjectLeaks(TestMemoryLeak): proc = thisproc def test_coverage(self): - p = psutil.Process() - ns = process_namespace(p) - for fun, name in ns.iter(ns.getters + ns.setters): - assert hasattr(self, "test_" + name), name + ns = process_namespace(None) + ns.test_class_coverage(self, ns.getters + ns.setters) @skip_if_linux() def test_name(self): @@ -326,9 +324,8 @@ class TestModuleFunctionsLeaks(TestMemoryLeak): """Test leaks of psutil module functions.""" def test_coverage(self): - ns = system_namespace - for fun, name in ns.iter(ns.all): - assert hasattr(self, "test_" + name), name + ns = system_namespace() + ns.test_class_coverage(self, ns.all) # --- cpu diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 383b14709d..737820798a 100644 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -432,7 +432,7 @@ def fun(): box = [] self.assertRaisesRegex( - AssertionError, r"1 unclosed fd\(s\) or handle\(s\)", + AssertionError, r"unclosed fd\(s\) or handle\(s\)", self.execute, fun, times=5, warmup_times=5) @@ -441,11 +441,13 @@ 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 + ns = system_namespace() + ns.test() fun = [x for x in ns.iter(ns.getters) if x[1] == 'net_if_addrs'][0][0] self.assertEqual(fun(), psutil.net_if_addrs()) From 4221a6c4be0d8edcf271a348c151962b733dfba4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 May 2020 04:13:45 +0200 Subject: [PATCH 0563/1714] (minor) fix arg --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 2402a143c4..fdaf470296 100755 --- a/setup.py +++ b/setup.py @@ -113,9 +113,9 @@ def write(self, s): def missdeps(msg): - s = hilite("C compiler or Python headers are not installed ", ok=False) - s += hilite("on this system. Try to run:\n", ok=False) - s += hilite(msg, ok=False, bold=True) + s = hilite("C compiler or Python headers are not installed ", color="red") + s += hilite("on this system. Try to run:\n", color="red") + s += hilite(msg, color="red", bold=True) print(s, file=sys.stderr) From dbd8dd6b902641a3a6ee69ccc72fa556eee16273 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 May 2020 04:18:56 +0200 Subject: [PATCH 0564/1714] fix winmake test target --- scripts/internal/winmake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c7091ac461..4f1e65ed0b 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -397,11 +397,11 @@ def lint(): sh("%s -m flake8 %s" % (PYTHON, py_files), nolog=True) -def test(name=""): +def test(name=RUNNER_PY): """Run tests""" build() test_setup() - sh("%s %s %s" % (PYTHON, RUNNER_PY, name)) + sh("%s %s" % (PYTHON, name)) def coverage(): From 8ad353bc515d7765e7dc3dfaaa8f9f6db4f85aba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 May 2020 11:16:25 -0700 Subject: [PATCH 0565/1714] fix some memleak tests on win --- psutil/arch/windows/net.c | 2 ++ psutil/tests/test_memory_leaks.py | 22 ++++++++++++++++++++-- scripts/internal/winmake.py | 4 ++-- setup.py | 10 +++++----- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 56c6b6f1f7..0891bdba40 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -17,6 +17,8 @@ static PIP_ADAPTER_ADDRESSES psutil_get_nic_addresses() { + // Note: GetAdaptersAddresses() increase the handle count on first + // call (and not anymore later on). // allocate a 15 KB buffer to start with int outBufLen = 15000; DWORD dwRetVal = 0; diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 61049a910f..5fb1cba4b6 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -138,6 +138,8 @@ def test_io_counters(self): @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) @skip_if_linux() @@ -250,7 +252,7 @@ def test_environ(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") def test_proc_info(self): - self.execute(cext.proc_info, os.getpid()) + self.execute(lambda: cext.proc_info(os.getpid())) class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @@ -359,6 +361,7 @@ def test_cpu_freq(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") def test_getloadavg(self): + psutil.getloadavg() self.execute(psutil.getloadavg) # --- mem @@ -400,26 +403,41 @@ def test_pids(self): # --- net + # XXX @unittest.skipIf(TRAVIS and MACOS, "false positive on TRAVIS + MACOS") @unittest.skipIf(CIRRUS and FREEBSD, "false positive on CIRRUS + FREEBSD") @skip_if_linux() @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') def test_net_io_counters(self): + if WINDOWS: + # GetAdaptersAddresses() increases the handle count on first + # call (only). + psutil.net_io_counters() self.execute(lambda: psutil.net_io_counters(nowrap=False)) @skip_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(psutil.net_connections, times=100) + self.execute(lambda: psutil.net_connections(kind='all'), times=100) def test_net_if_addrs(self): + if WINDOWS: + # GetAdaptersAddresses() increases the handle count on first + # call (only). + psutil.net_if_addrs() # Note: verified that on Windows this was a false positive. self.execute(psutil.net_if_addrs, tolerance=80 * 1024 if WINDOWS else 4096) @unittest.skipIf(TRAVIS, "EPERM on travis") def test_net_if_stats(self): + if WINDOWS: + # GetAdaptersAddresses() increases the handle count on first + # call (only). + psutil.net_if_stats() self.execute(psutil.net_if_stats) # --- sensors diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c7091ac461..4f1e65ed0b 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -397,11 +397,11 @@ def lint(): sh("%s -m flake8 %s" % (PYTHON, py_files), nolog=True) -def test(name=""): +def test(name=RUNNER_PY): """Run tests""" build() test_setup() - sh("%s %s %s" % (PYTHON, RUNNER_PY, name)) + sh("%s %s" % (PYTHON, name)) def coverage(): diff --git a/setup.py b/setup.py index 2402a143c4..19153bda33 100755 --- a/setup.py +++ b/setup.py @@ -113,9 +113,9 @@ def write(self, s): def missdeps(msg): - s = hilite("C compiler or Python headers are not installed ", ok=False) - s += hilite("on this system. Try to run:\n", ok=False) - s += hilite(msg, ok=False, bold=True) + s = hilite("C compiler or Python headers are not installed ", color="red") + s += hilite("on this system. Try to run:\n", color="red") + s += hilite(msg, color="red", bold=True) print(s, file=sys.stderr) @@ -402,7 +402,7 @@ def main(): missdeps("sudo yum install gcc python%s-devel" % py3) elif MACOS: print(hilite("XCode (https://developer.apple.com/xcode/) " - "is not installed"), ok=False, file=sys.stderr) + "is not installed"), color="red", file=sys.stderr) elif FREEBSD: missdeps("pkg install gcc python%s" % py3) elif OPENBSD: @@ -419,7 +419,7 @@ def main(): ur = "http://www.microsoft.com/en-us/download/" ur += "details.aspx?id=44266" s = "VisualStudio is not installed; get it from %s" % ur - print(hilite(s, ok=False), file=sys.stderr) + print(hilite(s, color="red"), file=sys.stderr) if __name__ == '__main__': From 91cd7fb53fe4cc93f837c7f543043c67b7b66674 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 May 2020 12:55:32 -0700 Subject: [PATCH 0566/1714] Windows: refactor proc username(), split it in 2 functions --- psutil/_psutil_windows.c | 111 +++++++++++++++++---------------- psutil/tests/test_testutils.py | 2 +- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 82fa518eeb..be4759ac1c 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -810,54 +810,34 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { } -/* - * Return process username as a "DOMAIN//USERNAME" string. - */ -static PyObject * -psutil_proc_username(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE processHandle = NULL; - HANDLE tokenHandle = NULL; - PTOKEN_USER user = NULL; - ULONG bufferSize; - WCHAR *name = NULL; - WCHAR *domainName = NULL; - ULONG nameSize; - ULONG domainNameSize; - SID_NAME_USE nameUse; - PyObject *py_username = NULL; - PyObject *py_domain = NULL; - PyObject *py_tuple = NULL; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; +static PTOKEN_USER +_psutil_user_token_from_pid(DWORD pid) { + HANDLE hProcess = NULL; + HANDLE hToken = NULL; + PTOKEN_USER userToken = NULL; + ULONG bufferSize = 0x100; - processHandle = psutil_handle_from_pid( - pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (processHandle == NULL) + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) return NULL; - if (!OpenProcessToken(processHandle, TOKEN_QUERY, &tokenHandle)) { + if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); goto error; } - CloseHandle(processHandle); - processHandle = NULL; - // Get the user SID. - bufferSize = 0x100; while (1) { - user = malloc(bufferSize); - if (user == NULL) { + userToken = malloc(bufferSize); + if (userToken == NULL) { PyErr_NoMemory(); goto error; } - if (!GetTokenInformation(tokenHandle, TokenUser, user, bufferSize, + if (!GetTokenInformation(hToken, TokenUser, userToken, bufferSize, &bufferSize)) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - free(user); + free(userToken); continue; } else { @@ -868,15 +848,45 @@ psutil_proc_username(PyObject *self, PyObject *args) { break; } - CloseHandle(tokenHandle); - tokenHandle = NULL; + CloseHandle(hProcess); + CloseHandle(hToken); + return userToken; + +error: + if (hProcess != NULL) + CloseHandle(hProcess); + if (hToken != NULL) + CloseHandle(hToken); + return NULL; +} + + +/* + * Return process username as a "DOMAIN//USERNAME" string. + */ +static PyObject * +psutil_proc_username(PyObject *self, PyObject *args) { + DWORD pid; + PTOKEN_USER userToken = NULL; + WCHAR *userName = NULL; + WCHAR *domainName = NULL; + ULONG nameSize = 0x100; + ULONG domainNameSize = 0x100; + SID_NAME_USE nameUse; + PyObject *py_username = NULL; + PyObject *py_domain = NULL; + PyObject *py_tuple = NULL; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + userToken = _psutil_user_token_from_pid(pid); + if (userToken == NULL) + return NULL; // resolve the SID to a name - nameSize = 0x100; - domainNameSize = 0x100; while (1) { - name = malloc(nameSize * sizeof(WCHAR)); - if (name == NULL) { + userName = malloc(nameSize * sizeof(WCHAR)); + if (userName == NULL) { PyErr_NoMemory(); goto error; } @@ -885,11 +895,11 @@ psutil_proc_username(PyObject *self, PyObject *args) { PyErr_NoMemory(); goto error; } - if (!LookupAccountSidW(NULL, user->User.Sid, name, &nameSize, + if (!LookupAccountSidW(NULL, userToken->User.Sid, userName, &nameSize, domainName, &domainNameSize, &nameUse)) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - free(name); + free(userName); free(domainName); continue; } @@ -904,7 +914,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { py_domain = PyUnicode_FromWideChar(domainName, wcslen(domainName)); if (! py_domain) goto error; - py_username = PyUnicode_FromWideChar(name, wcslen(name)); + py_username = PyUnicode_FromWideChar(userName, wcslen(userName)); if (! py_username) goto error; py_tuple = Py_BuildValue("OO", py_domain, py_username); @@ -913,23 +923,18 @@ psutil_proc_username(PyObject *self, PyObject *args) { Py_DECREF(py_domain); Py_DECREF(py_username); - free(name); + free(userName); free(domainName); - free(user); - + free(userToken); return py_tuple; error: - if (processHandle != NULL) - CloseHandle(processHandle); - if (tokenHandle != NULL) - CloseHandle(tokenHandle); - if (name != NULL) - free(name); + if (userName != NULL) + free(userName); if (domainName != NULL) free(domainName); - if (user != NULL) - free(user); + if (userToken != NULL) + free(userToken); Py_XDECREF(py_domain); Py_XDECREF(py_username); Py_XDECREF(py_tuple); diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 383b14709d..6395b7c4bb 100644 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -432,7 +432,7 @@ def fun(): box = [] self.assertRaisesRegex( - AssertionError, r"1 unclosed fd\(s\) or handle\(s\)", + AssertionError, r"unclosed fd\(s\) or handle\(s\)", self.execute, fun, times=5, warmup_times=5) From 6c9bda56e6b86e538816a24801520d6791e17d30 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 May 2020 22:21:53 +0200 Subject: [PATCH 0567/1714] restore file permissions --- psutil/tests/test_system.py | 0 psutil/tests/test_testutils.py | 0 psutil/tests/test_unicode.py | 0 scripts/internal/tidelift.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 psutil/tests/test_system.py mode change 100644 => 100755 psutil/tests/test_testutils.py mode change 100644 => 100755 psutil/tests/test_unicode.py mode change 100644 => 100755 scripts/internal/tidelift.py diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py old mode 100644 new mode 100755 diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py old mode 100644 new mode 100755 diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py old mode 100644 new mode 100755 diff --git a/scripts/internal/tidelift.py b/scripts/internal/tidelift.py old mode 100644 new mode 100755 From e1ea2bccf8aea404dca0f79398f36f37217c45f6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 May 2020 12:58:49 -0700 Subject: [PATCH 0568/1714] Add new TestFdsLeak test class (#1752) --- psutil/tests/__init__.py | 142 +++++++++++++++++++----------- psutil/tests/test_memory_leaks.py | 54 ++++++++++-- psutil/tests/test_system.py | 0 psutil/tests/test_testutils.py | 24 ++--- psutil/tests/test_unicode.py | 0 scripts/internal/tidelift.py | 0 scripts/internal/winmake.py | 8 ++ 7 files changed, 162 insertions(+), 66 deletions(-) mode change 100644 => 100755 psutil/tests/test_system.py mode change 100644 => 100755 psutil/tests/test_testutils.py mode change 100644 => 100755 psutil/tests/test_unicode.py mode change 100644 => 100755 scripts/internal/tidelift.py diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 91a519c18c..41a791c1bf 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -89,7 +89,7 @@ 'ThreadTask' # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', + 'retry_on_failure', 'TestMemoryLeak', 'TestFdsLeak', 'PsutilTestCase', 'process_namespace', 'system_namespace', # install utils 'install_pip', 'install_test_deps', @@ -895,28 +895,47 @@ def assertProcessGone(self, proc): @unittest.skipIf(PYPY, "unreliable on PYPY") class TestMemoryLeak(PsutilTestCase): - """Test framework class for detecting function memory leaks (typically - functions implemented in C). - It does so by calling a function many times, and checks whether the - process memory usage increased before and after having called the - function repeadetly. + """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. + The logic: + + call_fun_n_times() + if mem_diff > tolerance: + call_fun_for_3_secs() + if mem_diff > 0: + return 1 # failure + return 0 # success + + 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 you should adjust the + tolerance of each individual test case, but most of the times you + won't have to. + + If available (Linux, OSX, Windows) USS memory is used for comparison, + since it's supposed to be more precise, see: + http://grodola.blogspot.com/2016/02/psutil-4-real-process-memory-and-environ.html + 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. - In addition also call the function onces and make sure num_fds() - (POSIX) or num_handles() (Windows) does not increase. This is done - in order to discover forgotten close(2) and CloseHandle syscalls. + PyPy appears to be completely unstable for this framework, probably + because of its JIT, so tests on PYPY are skipped. - Note that sometimes this may produce false positives. + Usage: - PyPy appears to be completely unstable for this framework, probably - because of how its JIT handles memory, so tests on PYPY are - automatically skipped. + class TestLeaks(psutil.tests.TestMemoryLeak): + + def test_fun(self): + self.execute(some_function) """ # Configurable class attrs. - times = 1200 + times = 1000 warmup_times = 10 tolerance = 4096 # memory retry_for = 3.0 # seconds - check_fds = True # whether to check if num_fds() increased verbose = True def setUp(self): @@ -972,7 +991,7 @@ def _log(self, msg): print_color(msg, color="yellow", file=sys.stderr) def execute(self, fun, times=times, warmup_times=warmup_times, - tolerance=tolerance, retry_for=retry_for, check_fds=check_fds): + tolerance=tolerance, retry_for=retry_for): """Test a callable.""" if times <= 0: raise ValueError("times must be > 0") @@ -983,15 +1002,6 @@ def execute(self, fun, times=times, warmup_times=warmup_times, if retry_for is not None and retry_for < 0: raise ValueError("retry_for must be >= 0") - if check_fds: - before = self._get_fds_or_handles() - self._call(fun) - after = self._get_fds_or_handles() - diff = abs(after - before) - if diff > 0: - msg = "%s unclosed fd(s) or handle(s)" % (diff) - raise self.fail(msg) - # warm up self._call_ntimes(fun, warmup_times) mem1 = self._call_ntimes(fun, times) @@ -1029,6 +1039,43 @@ def call(): self.execute(call, **kwargs) +class TestFdsLeak(PsutilTestCase): + """Test framework class which makes sure num_fds() (POSIX) or + num_handles() (Windows) does not increase after calling a function. + This can be used to discover forgotten close(2) and CloseHandle + syscalls. + """ + + tolerance = 0 + _thisproc = psutil.Process() + + def _get_fds_or_handles(self): + if POSIX: + return self._thisproc.num_fds() + else: + return self._thisproc.num_handles() + + def _call(self, fun): + return fun() + + def execute(self, fun, tolerance=tolerance): + # This is supposed to close() any unclosed file object. + gc.collect() + before = self._get_fds_or_handles() + self._call(fun) + after = self._get_fds_or_handles() + 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 _get_eligible_cpu(): p = psutil.Process() if hasattr(p, "cpu_num"): @@ -1154,7 +1201,19 @@ def clear_cache(self): self._proc._init(self._proc.pid, _ignore_nsp=True) @classmethod - def _test(cls): + 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] != '_']) @@ -1163,9 +1222,6 @@ def _test(cls): raise ValueError("uncovered Process class names: %r" % leftout) -process_namespace._test() - - class system_namespace: """A container that lists all the module-level, system-related APIs. Utilities such as cpu_percent() are excluded. Usage: @@ -1195,18 +1251,18 @@ class system_namespace: ('virtual_memory', (), {}), ] if HAS_CPU_FREQ: - getters.append(('cpu_freq', (), {'percpu': True})) + getters += [('cpu_freq', (), {'percpu': True})] if HAS_GETLOADAVG: - getters.append(('getloadavg', (), {})) + getters += [('getloadavg', (), {})] if HAS_SENSORS_TEMPERATURES: - getters.append(('sensors_temperatures', (), {})) + getters += [('sensors_temperatures', (), {})] if HAS_SENSORS_FANS: - getters.append(('sensors_fans', (), {})) + getters += [('sensors_fans', (), {})] if HAS_SENSORS_BATTERY: - getters.append(('sensors_battery', (), {})) + getters += [('sensors_battery', (), {})] if WINDOWS: - getters.append(('win_service_iter', (), {})) - getters.append(('win_service_get', ('alg', ), {})) + getters += [('win_service_iter', (), {})] + getters += [('win_service_get', ('alg', ), {})] ignored = [ ('process_iter', (), {}), @@ -1229,19 +1285,7 @@ def iter(ls): fun = functools.partial(fun, *args, **kwds) yield (fun, fun_name) - @classmethod - def _test(cls): - this = set([x[0] for x in cls.all]) - ignored = set([x[0] for x in cls.ignored]) - # there's a separate test for __all__ - mod = set([x for x in dir(psutil) if x.islower() and x[0] != '_' and - x in psutil.__all__ and callable(getattr(psutil, x))]) - leftout = (this | ignored) ^ mod - if leftout: - raise ValueError("uncovered psutil mod name(s): %r" % leftout) - - -system_namespace._test() + test_class_coverage = process_namespace.test_class_coverage def serialrun(klass): diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 5fb1cba4b6..02e61aa00c 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -50,10 +50,12 @@ from psutil.tests import spawn_testproc from psutil.tests import system_namespace from psutil.tests import terminate +from psutil.tests import TestFdsLeak from psutil.tests import TestMemoryLeak from psutil.tests import TRAVIS from psutil.tests import unittest + SKIP_PYTHON_IMPL = True cext = psutil._psplatform.cext thisproc = psutil.Process() @@ -75,10 +77,8 @@ class TestProcessObjectLeaks(TestMemoryLeak): proc = thisproc def test_coverage(self): - p = psutil.Process() - ns = process_namespace(p) - for fun, name in ns.iter(ns.getters + ns.setters): - assert hasattr(self, "test_" + name), name + ns = process_namespace(None) + ns.test_class_coverage(self, ns.getters + ns.setters) @skip_if_linux() def test_name(self): @@ -328,9 +328,8 @@ class TestModuleFunctionsLeaks(TestMemoryLeak): """Test leaks of psutil module functions.""" def test_coverage(self): - ns = system_namespace - for fun, name in ns.iter(ns.all): - assert hasattr(self, "test_" + name), name + ns = system_namespace() + ns.test_class_coverage(self, ns.all) # --- cpu @@ -490,6 +489,47 @@ def test_win_service_get_description(self): self.execute(lambda: cext.winservice_query_descr(name)) +# ===================================================================== +# --- File descriptors and handlers +# ===================================================================== + + +class TestUnclosedFdsOrHandles(TestFdsLeak): + + def test_process_apis(self): + p = psutil.Process() + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters + ns.setters): + if WINDOWS: + fun() + self.execute(fun) + + def test_process_apis_nsp(self): + def wrapper(fun): + try: + fun() + except psutil.NoSuchProcess: + pass + + p = psutil.Process(self.spawn_testproc().pid) + p.terminate() + p.wait() + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters + ns.setters + ns.killers): + if WINDOWS: + wrapper(fun) + self.execute(lambda: wrapper(fun)) + + def test_system_apis(self): + ns = system_namespace + for fun, name in ns.iter(ns.all): + if WINDOWS: + fun() + if MACOS and name == 'connections': + continue # raise AD + self.execute(fun) + + if __name__ == '__main__': from psutil.tests.runner import run_from_name run_from_name(__file__) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py old mode 100644 new mode 100755 diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py old mode 100644 new mode 100755 index 6395b7c4bb..64bd8d3b25 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -47,6 +47,7 @@ from psutil.tests import system_namespace from psutil.tests import tcp_socketpair from psutil.tests import terminate +from psutil.tests import TestFdsLeak from psutil.tests import TestMemoryLeak from psutil.tests import unittest from psutil.tests import unix_socketpair @@ -358,9 +359,9 @@ def fun(): cnt['cnt'] += 1 cnt = {'cnt': 0} self.execute(fun, times=1, warmup_times=10) - self.assertEqual(cnt['cnt'], 12) + self.assertEqual(cnt['cnt'], 11) self.execute(fun, times=10, warmup_times=10) - self.assertEqual(cnt['cnt'], 33) + self.assertEqual(cnt['cnt'], 31) @retry_on_failure() def test_warmup_times(self): @@ -368,7 +369,7 @@ def fun(): cnt['cnt'] += 1 cnt = {'cnt': 0} self.execute(fun, times=1, warmup_times=10) - self.assertEqual(cnt['cnt'], 12) + self.assertEqual(cnt['cnt'], 11) def test_param_err(self): self.assertRaises(ValueError, self.execute, lambda: 0, times=0) @@ -385,7 +386,7 @@ def fun(): times = 100 self.assertRaises(AssertionError, self.execute, fun, times=times, warmup_times=10, retry_for=None) - self.assertEqual(len(ls), times + 11) + self.assertEqual(len(ls), times + 10) @retry_on_failure(retries=20) # 2 secs def test_leak_with_retry(self, ls=[]): @@ -406,7 +407,7 @@ def fun(): ls = [] times = 100 self.execute(fun, times=times, warmup_times=0, - tolerance=200 * 1024 * 1024, check_fds=False) + tolerance=200 * 1024 * 1024) self.assertEqual(len(ls), times) @retry_on_failure() @@ -424,16 +425,18 @@ def fun(): with self.assertRaises(AssertionError): self.execute_w_exc(ZeroDivisionError, fun) - def test_unclosed_fds(self): + +class TestFdsLeakClass(TestFdsLeak): + + def test_unclosed_files(self): def fun(): f = open(__file__) self.addCleanup(f.close) box.append(f) box = [] - self.assertRaisesRegex( - AssertionError, r"unclosed fd\(s\) or handle\(s\)", - self.execute, fun, times=5, warmup_times=5) + self.assertRaisesRegex(AssertionError, "unclosed", self.execute, fun) + self.assertEqual(len(box), 1) class TestTestingUtils(PsutilTestCase): @@ -441,11 +444,12 @@ 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 + 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()) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py old mode 100644 new mode 100755 diff --git a/scripts/internal/tidelift.py b/scripts/internal/tidelift.py old mode 100644 new mode 100755 diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 4f1e65ed0b..de99c3276e 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -464,6 +464,13 @@ def test_contracts(): sh("%s psutil\\tests\\test_contracts.py" % PYTHON) +def test_testutils(): + """Run test utilities tests""" + build() + test_setup() + sh("%s psutil\\tests\\test_testutils.py" % PYTHON) + + def test_by_name(name): """Run test by name""" build() @@ -569,6 +576,7 @@ def main(): sp.add_parser('test-process', help="run process tests") sp.add_parser('test-system', help="run system tests") sp.add_parser('test-unicode', help="run unicode tests") + sp.add_parser('test-testutils', help="run test utils tests") sp.add_parser('uninstall', help="uninstall psutil") sp.add_parser('upload-wheels', help="upload wheel files on PyPI") sp.add_parser('wheel', help="create wheel file") From 6adcca6c9fda5a36e923bfca3591b972e98a0ce5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 May 2020 15:40:04 -0700 Subject: [PATCH 0569/1714] Memory leak test: take fluctuations into account (#1757) Preamble ======= We have a [memory leak test suite](https://github.com/giampaolo/psutil/blob/e1ea2bccf8aea404dca0f79398f36f37217c45f6/psutil/tests/__init__.py#L897), which calls a function many times and fails if the process memory increased. We do this in order to detect missing `free()` or `Py_DECREF` calls in the C modules. When we do, then we have a memory leak. The problem ========== A problem we've been having for probably over 10 years, is the false positives. That's because the memory fluctuates. Sometimes it may increase (or even decrease!) due to how the OS handles memory, the Python's garbage collector, the fact that RSS is an approximation and who knows what else. So thus far we tried to compensate that by using the following logic: - warmup (call fun 10 times) - call the function many times (1000) - if memory increased before/after calling function 1000 times, then keep calling it for another 3 secs - if it still increased at all (> 0) then fail This logic didn't really solve the problem, as we still had occasional false positives, especially lately on FreeBSD. The solution ========= This PR changes the internal algorithm so that in case of failure (mem > 0 after calling fun() N times) we retry the test for up to 5 times, increasing N (repetitions) each time, so we consider it a failure only if the memory **keeps increasing** between runs. So for instance, here's a legitimate failure: ``` psutil.tests.test_memory_leaks.TestModuleFunctionsLeaks.test_disk_partitions ... Run #1: extra-mem=696.0K, per-call=3.5K, calls=200 Run #2: extra-mem=1.4M, per-call=3.5K, calls=400 Run #3: extra-mem=2.1M, per-call=3.5K, calls=600 Run #4: extra-mem=2.7M, per-call=3.5K, calls=800 Run #5: extra-mem=3.4M, per-call=3.5K, calls=1000 FAIL ``` If, on the other hand, the memory increased on one run (say 200 calls) but decreased on the next run (say 400 calls), then it clearly means it's a false positive, because memory consumption may be > 0 on second run, but if it's lower than the previous run with less repetitions, then it cannot possibly represent a leak (just a fluctuation): ``` psutil.tests.test_memory_leaks.TestModuleFunctionsLeaks.test_net_connections ... Run #1: extra-mem=568.0K, per-call=2.8K, calls=200 Run #2: extra-mem=24.0K, per-call=61.4B, calls=400 OK ``` Note about mallinfo() ================ Aka #1275. `mallinfo()` on Linux is supposed to provide memory metrics about how many bytes gets allocated on the heap by `malloc()`, so it's supposed to be way more precise than RSS and also [USS](http://grodola.blogspot.com/2016/02/psutil-4-real-process-memory-and-environ.html). In another branch were I exposed it, I verified that fluctuations still occur even when using `mallinfo()` though, despite less often. So that means even `mallinfo()` would not grant 100% stability. --- .flake8 | 5 ++ HISTORY.rst | 1 + appveyor.yml | 1 + docs/conf.py | 24 +++--- psutil/_compat.py | 17 +--- psutil/arch/windows/net.c | 64 +++++++------- psutil/tests/__init__.py | 135 +++++++++++++----------------- psutil/tests/test_memory_leaks.py | 32 ++----- psutil/tests/test_testutils.py | 53 +++--------- scripts/internal/winmake.py | 1 - 10 files changed, 127 insertions(+), 206 deletions(-) diff --git a/.flake8 b/.flake8 index 6581552aac..f06f8344ad 100644 --- a/.flake8 +++ b/.flake8 @@ -3,6 +3,11 @@ # T001 = print() statement [flake8] +ignore = + # ambiguous variable name 'l' + E741, + # line break after binary operator + W504 per-file-ignores = setup.py:T001 scripts/*:T001 diff --git a/HISTORY.rst b/HISTORY.rst index 24186c3d29..c99b9c4e53 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,6 +24,7 @@ XXXX-XX-XX >>> proc psutil.Process(pid=12739, name='python3', status='terminated', exitcode=, started='15:08:20') +- 1757_: memory leak tests are now stable. **Bug fixes** diff --git a/appveyor.yml b/appveyor.yml index d22c1cb4f0..e0912a1ed8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -69,6 +69,7 @@ build: off test_script: - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test-memleaks" after_test: - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py wheel" diff --git a/docs/conf.py b/docs/conf.py index b056d20fee..f4a32b987d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -267,21 +267,21 @@ def get_version(): # -- Options for LaTeX output --------------------------------------------- latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples diff --git a/psutil/_compat.py b/psutil/_compat.py index 145fb71d44..17f38485e9 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -8,7 +8,6 @@ """ import collections -import contextlib import errno import functools import os @@ -23,7 +22,7 @@ # literals "u", "b", # collections module - "lru_cache", "redirect_stderr", + "lru_cache", # shutil module "which", "get_terminal_size", # python 3 exceptions @@ -423,17 +422,3 @@ def get_terminal_size(fallback=(80, 24)): return (res[1], res[0]) except Exception: return fallback - - -# python 3.4 -try: - from contextlib import redirect_stderr -except ImportError: - @contextlib.contextmanager - def redirect_stderr(target): - original = sys.stderr - try: - sys.stderr = target - yield - finally: - sys.stderr = original diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 0891bdba40..8d8f7d1c0a 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -16,42 +16,35 @@ static PIP_ADAPTER_ADDRESSES -psutil_get_nic_addresses() { - // Note: GetAdaptersAddresses() increase the handle count on first - // call (and not anymore later on). - // allocate a 15 KB buffer to start with - int outBufLen = 15000; - DWORD dwRetVal = 0; - ULONG attempts = 0; - PIP_ADAPTER_ADDRESSES pAddresses = NULL; - - do { - pAddresses = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen); - if (pAddresses == NULL) { - PyErr_NoMemory(); - return NULL; - } - - dwRetVal = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, pAddresses, - &outBufLen); - if (dwRetVal == ERROR_BUFFER_OVERFLOW) { - free(pAddresses); - pAddresses = NULL; - } - else { - break; - } - - attempts++; - } while ((dwRetVal == ERROR_BUFFER_OVERFLOW) && (attempts < 3)); +psutil_get_nic_addresses(void) { + ULONG bufferLength = 0; + PIP_ADAPTER_ADDRESSES buffer; + + if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, NULL, &bufferLength) + != ERROR_BUFFER_OVERFLOW) + { + PyErr_SetString(PyExc_RuntimeError, + "GetAdaptersAddresses() syscall failed."); + return NULL; + } - if (dwRetVal != NO_ERROR) { - PyErr_SetString( - PyExc_RuntimeError, "GetAdaptersAddresses() syscall failed."); + buffer = malloc(bufferLength); + if (buffer == NULL) { + PyErr_NoMemory(); + return NULL; + } + memset(buffer, 0, bufferLength); + + if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, buffer, &bufferLength) + != ERROR_SUCCESS) + { + free(buffer); + PyErr_SetString(PyExc_RuntimeError, + "GetAdaptersAddresses() syscall failed."); return NULL; } - return pAddresses; + return buffer; } @@ -191,11 +184,11 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { for (i = 0; i < (int) pCurrAddresses->PhysicalAddressLength; i++) { if (i == (pCurrAddresses->PhysicalAddressLength - 1)) { sprintf_s(ptr, _countof(buff_macaddr), "%.2X\n", - (int)pCurrAddresses->PhysicalAddress[i]); + (int)pCurrAddresses->PhysicalAddress[i]); } else { sprintf_s(ptr, _countof(buff_macaddr), "%.2X-", - (int)pCurrAddresses->PhysicalAddress[i]); + (int)pCurrAddresses->PhysicalAddress[i]); } ptr += 3; } @@ -325,8 +318,7 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { /* * Provides stats about NIC interfaces installed on the system. - * TODO: get 'duplex' (currently it's hard coded to '2', aka - 'full duplex') + * TODO: get 'duplex' (currently it's hard coded to '2', aka 'full duplex') */ PyObject * psutil_net_if_stats(PyObject *self, PyObject *args) { diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 41a791c1bf..b452775f74 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -476,12 +476,16 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): from psutil._psposix import wait_pid def wait(proc, timeout): - try: - return psutil.Process(proc.pid).wait(timeout) - except psutil.NoSuchProcess: - # Needed to kill zombies. - if POSIX: - return wait_pid(proc.pid, 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): # If the process received SIGSTOP, SIGCONT is necessary first, @@ -899,22 +903,15 @@ class TestMemoryLeak(PsutilTestCase): 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. - The logic: - - call_fun_n_times() - if mem_diff > tolerance: - call_fun_for_3_secs() - if mem_diff > 0: - return 1 # failure - return 0 # success 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 you should adjust the - tolerance of each individual test case, but most of the times you - won't have to. + 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, + If available (Linux, OSX, Windows), USS memory is used for comparison, since it's supposed to be more precise, see: http://grodola.blogspot.com/2016/02/psutil-4-real-process-memory-and-environ.html If not, RSS memory is used. mallinfo() on Linux and _heapwalk() on @@ -932,15 +929,15 @@ def test_fun(self): self.execute(some_function) """ # Configurable class attrs. - times = 1000 + times = 200 warmup_times = 10 - tolerance = 4096 # memory - retry_for = 3.0 # seconds + retries = 5 + tolerance = 0 # memory verbose = True - def setUp(self): - self._thisproc = psutil.Process() - gc.collect() + @classmethod + def setUpClass(cls): + cls._thisproc = psutil.Process() def _get_mem(self): # USS is the closest thing we have to "real" memory usage and it @@ -957,77 +954,63 @@ def _get_fds_or_handles(self): def _call(self, fun): return fun() - def _itercall(self, fun, iterator): + def _call_ntimes(self, fun, times): """Get 2 distinct memory samples, before and after having called fun repeadetly, and return the memory difference. """ - ncalls = 0 - gc.collect() + gc.collect(generation=1) mem1 = self._get_mem() - for x in iterator: + for x in range(times): ret = self._call(fun) - ncalls += 1 del x, ret - gc.collect() + gc.collect(generation=1) mem2 = self._get_mem() self.assertEqual(gc.garbage, []) - diff = mem2 - mem1 - if diff < 0: - self._log("negative memory diff -%s" % (bytes2human(abs(diff)))) - return (diff, ncalls) - - def _call_ntimes(self, fun, times): - return self._itercall(fun, range(times))[0] - - def _call_for(self, fun, secs): - def iterator(secs): - stop_at = time.time() + secs - while time.time() < stop_at: - yield - return self._itercall(fun, iterator(secs)) + diff = mem2 - mem1 # can also be negative + return diff def _log(self, msg): if self.verbose: print_color(msg, color="yellow", file=sys.stderr) - def execute(self, fun, times=times, warmup_times=warmup_times, - tolerance=tolerance, retry_for=retry_for): + def execute(self, fun, times=None, warmup_times=None, retries=None, + tolerance=None): """Test a callable.""" - if times <= 0: - raise ValueError("times must be > 0") - if warmup_times < 0: - raise ValueError("warmup_times must be >= 0") - if tolerance is not None and tolerance < 0: - raise ValueError("tolerance must be >= 0") - if retry_for is not None and retry_for < 0: - raise ValueError("retry_for must be >= 0") + 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)) # warm up self._call_ntimes(fun, warmup_times) - mem1 = self._call_ntimes(fun, times) - - if mem1 > 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 N more seconds and - # fail if we notice any difference (ignore tolerance). - msg = "+%s after %s calls; try calling fun for another %s secs" % ( - bytes2human(mem1), times, retry_for) - if not retry_for: - raise self.fail(msg) + 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) - - mem2, ncalls = self._call_for(fun, retry_for) - if mem2 > mem1: - # failure - msg = "+%s memory increase after %s calls; " % ( - bytes2human(mem1), times) - msg += "+%s after another %s calls over %s secs" % ( - bytes2human(mem2), ncalls, retry_for) - raise self.fail(msg) + times += increase + prev_mem = mem + raise self.fail(". ".join(messages)) def execute_w_exc(self, exc, fun, **kwargs): """Convenience method to test a callable while making sure it diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 02e61aa00c..0bf2ad8d53 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -21,7 +21,6 @@ import psutil import psutil._common -from psutil import FREEBSD from psutil import LINUX from psutil import MACOS from psutil import OPENBSD @@ -30,7 +29,6 @@ from psutil import WINDOWS from psutil._compat import ProcessLookupError from psutil._compat import super -from psutil.tests import CIRRUS from psutil.tests import create_sockets from psutil.tests import get_testfn from psutil.tests import HAS_CPU_AFFINITY @@ -244,7 +242,7 @@ def test_connections(self): # be executed. with create_sockets(): kind = 'inet' if SUNOS else 'all' - self.execute(lambda: self.proc.connections(kind), times=100) + self.execute(lambda: self.proc.connections(kind)) @unittest.skipIf(not HAS_ENVIRON, "not supported") def test_environ(self): @@ -402,16 +400,9 @@ def test_pids(self): # --- net - # XXX - @unittest.skipIf(TRAVIS and MACOS, "false positive on TRAVIS + MACOS") - @unittest.skipIf(CIRRUS and FREEBSD, "false positive on CIRRUS + FREEBSD") @skip_if_linux() @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') def test_net_io_counters(self): - if WINDOWS: - # GetAdaptersAddresses() increases the handle count on first - # call (only). - psutil.net_io_counters() self.execute(lambda: psutil.net_io_counters(nowrap=False)) @skip_if_linux() @@ -420,23 +411,15 @@ 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'), times=100) + self.execute(lambda: psutil.net_connections(kind='all')) def test_net_if_addrs(self): - if WINDOWS: - # GetAdaptersAddresses() increases the handle count on first - # call (only). - psutil.net_if_addrs() # Note: verified that on Windows this was a false positive. - self.execute(psutil.net_if_addrs, - tolerance=80 * 1024 if WINDOWS else 4096) + tolerance = 80 * 1024 if WINDOWS else self.tolerance + self.execute(psutil.net_if_addrs, tolerance=tolerance) - @unittest.skipIf(TRAVIS, "EPERM on travis") + # @unittest.skipIf(TRAVIS, "EPERM on travis") def test_net_if_stats(self): - if WINDOWS: - # GetAdaptersAddresses() increases the handle count on first - # call (only). - psutil.net_if_stats() self.execute(psutil.net_if_stats) # --- sensors @@ -462,7 +445,6 @@ def test_sensors_fans(self): def test_boot_time(self): self.execute(psutil.boot_time) - @unittest.skipIf(WINDOWS, "XXX produces a false positive on Windows") def test_users(self): self.execute(psutil.users) @@ -495,6 +477,8 @@ def test_win_service_get_description(self): class TestUnclosedFdsOrHandles(TestFdsLeak): + # Note: on Windows certain C functions increase the handles count + # on first call (and never again). def test_process_apis(self): p = psutil.Process() @@ -525,7 +509,7 @@ def test_system_apis(self): for fun, name in ns.iter(ns.all): if WINDOWS: fun() - if MACOS and name == 'connections': + if MACOS and name == 'net_connections': continue # raise AD self.execute(fun) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 64bd8d3b25..6c5100945d 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -12,7 +12,6 @@ import collections import contextlib import errno -import io import os import socket import stat @@ -24,8 +23,6 @@ from psutil._common import open_binary from psutil._common import open_text from psutil._common import supports_ipv6 -from psutil._compat import PY3 -from psutil._compat import redirect_stderr from psutil.tests import bind_socket from psutil.tests import bind_unix_socket from psutil.tests import call_until @@ -40,7 +37,6 @@ from psutil.tests import PYTHON_EXE 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 @@ -353,54 +349,31 @@ def test_create_sockets(self): @serialrun class TestMemLeakClass(TestMemoryLeak): - @retry_on_failure() def test_times(self): def fun(): cnt['cnt'] += 1 cnt = {'cnt': 0} - self.execute(fun, times=1, warmup_times=10) - self.assertEqual(cnt['cnt'], 11) - self.execute(fun, times=10, warmup_times=10) - self.assertEqual(cnt['cnt'], 31) - - @retry_on_failure() - def test_warmup_times(self): - def fun(): - cnt['cnt'] += 1 - cnt = {'cnt': 0} - self.execute(fun, times=1, warmup_times=10) - self.assertEqual(cnt['cnt'], 11) + self.execute(fun, times=10, warmup_times=15) + self.assertEqual(cnt['cnt'], 25) 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, retry_for=-1) + self.assertRaises(ValueError, self.execute, lambda: 0, retries=-1) - @retry_on_failure() def test_leak(self): - def fun(): - ls.append("x" * 24 * 1024) ls = [] - times = 100 - self.assertRaises(AssertionError, self.execute, fun, times=times, - warmup_times=10, retry_for=None) - self.assertEqual(len(ls), times + 10) - @retry_on_failure(retries=20) # 2 secs - def test_leak_with_retry(self, ls=[]): - def fun(): + def fun(ls=ls): ls.append("x" * 24 * 1024) - times = 100 - f = io.StringIO() if PY3 else io.BytesIO() - with redirect_stderr(f): - self.assertRaises(AssertionError, self.execute, fun, times=times, - retry_for=0.1) - self.assertIn("try calling fun for another", f.getvalue()) - self.assertGreater(len(ls), times) - - @retry_on_failure() + + try: + self.assertRaises(AssertionError, self.execute, fun) + finally: + del ls + def test_tolerance(self): def fun(): ls.append("x" * 24 * 1024) @@ -410,13 +383,10 @@ def fun(): tolerance=200 * 1024 * 1024) self.assertEqual(len(ls), times) - @retry_on_failure() def test_execute_w_exc(self): def fun(): 1 / 0 - # XXX: use high tolerance, occasional false positive - self.execute_w_exc(ZeroDivisionError, fun, times=2000, - warmup_times=20, tolerance=200 * 1024, retry_for=3) + self.execute_w_exc(ZeroDivisionError, fun) with self.assertRaises(ZeroDivisionError): self.execute_w_exc(OSError, fun) @@ -426,6 +396,7 @@ def fun(): self.execute_w_exc(ZeroDivisionError, fun) +@serialrun class TestFdsLeakClass(TestFdsLeak): def test_unclosed_files(self): diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index de99c3276e..a83aed3e03 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -488,7 +488,6 @@ def test_failed(): def test_memleaks(): """Run memory leaks tests""" build() - test_setup() sh("%s psutil\\tests\\test_memory_leaks.py" % PYTHON) From 913d4b1d6dcce88dea6ef9382b93883a04a66cd7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 May 2020 01:36:41 +0200 Subject: [PATCH 0570/1714] remove dead code --- psutil/tests/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b452775f74..cda88b3837 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -945,12 +945,6 @@ def _get_mem(self): mem = self._thisproc.memory_full_info() return getattr(mem, "uss", mem.rss) - def _get_fds_or_handles(self): - if POSIX: - return self._thisproc.num_fds() - else: - return self._thisproc.num_handles() - def _call(self, fun): return fun() From f6383a4f81e36a5da3f4046e9cdeb8c5cc68aac2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 May 2020 14:17:36 +0000 Subject: [PATCH 0571/1714] put fds test in mem leak class --- psutil/tests/__init__.py | 119 ++++++++++++++---------------- psutil/tests/test_memory_leaks.py | 44 ----------- psutil/tests/test_testutils.py | 35 ++++----- 3 files changed, 70 insertions(+), 128 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index cda88b3837..ccfcb60c2b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -89,7 +89,7 @@ 'ThreadTask' # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_on_failure', 'TestMemoryLeak', 'TestFdsLeak', 'PsutilTestCase', + 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', 'process_namespace', 'system_namespace', # install utils 'install_pip', 'install_test_deps', @@ -931,13 +931,10 @@ def test_fun(self): # Configurable class attrs. times = 200 warmup_times = 10 - retries = 5 tolerance = 0 # memory + retries = 5 verbose = True - - @classmethod - def setUpClass(cls): - cls._thisproc = psutil.Process() + _thisproc = psutil.Process() def _get_mem(self): # USS is the closest thing we have to "real" memory usage and it @@ -945,9 +942,38 @@ def _get_mem(self): 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 _call(self, fun): return fun() + 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 repeadetly, and return the memory difference. @@ -963,28 +989,7 @@ def _call_ntimes(self, fun, times): diff = mem2 - mem1 # can also be negative return diff - def _log(self, msg): - if self.verbose: - print_color(msg, color="yellow", file=sys.stderr) - - 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)) - - # warm up - self._call_ntimes(fun, warmup_times) + def _check_mem(self, fun, times, warmup_times, retries, tolerance): messages = [] prev_mem = 0 increase = times @@ -1006,6 +1011,27 @@ def execute(self, fun, times=None, warmup_times=None, retries=None, prev_mem = mem raise self.fail(". ".join(messages)) + 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, warmup_times=warmup_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. @@ -1016,43 +1042,6 @@ def call(): self.execute(call, **kwargs) -class TestFdsLeak(PsutilTestCase): - """Test framework class which makes sure num_fds() (POSIX) or - num_handles() (Windows) does not increase after calling a function. - This can be used to discover forgotten close(2) and CloseHandle - syscalls. - """ - - tolerance = 0 - _thisproc = psutil.Process() - - def _get_fds_or_handles(self): - if POSIX: - return self._thisproc.num_fds() - else: - return self._thisproc.num_handles() - - def _call(self, fun): - return fun() - - def execute(self, fun, tolerance=tolerance): - # This is supposed to close() any unclosed file object. - gc.collect() - before = self._get_fds_or_handles() - self._call(fun) - after = self._get_fds_or_handles() - 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 _get_eligible_cpu(): p = psutil.Process() if hasattr(p, "cpu_num"): diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 0bf2ad8d53..b722f4fa7e 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -48,7 +48,6 @@ from psutil.tests import spawn_testproc from psutil.tests import system_namespace from psutil.tests import terminate -from psutil.tests import TestFdsLeak from psutil.tests import TestMemoryLeak from psutil.tests import TRAVIS from psutil.tests import unittest @@ -471,49 +470,6 @@ def test_win_service_get_description(self): self.execute(lambda: cext.winservice_query_descr(name)) -# ===================================================================== -# --- File descriptors and handlers -# ===================================================================== - - -class TestUnclosedFdsOrHandles(TestFdsLeak): - # Note: on Windows certain C functions increase the handles count - # on first call (and never again). - - def test_process_apis(self): - p = psutil.Process() - ns = process_namespace(p) - for fun, name in ns.iter(ns.getters + ns.setters): - if WINDOWS: - fun() - self.execute(fun) - - def test_process_apis_nsp(self): - def wrapper(fun): - try: - fun() - except psutil.NoSuchProcess: - pass - - p = psutil.Process(self.spawn_testproc().pid) - p.terminate() - p.wait() - ns = process_namespace(p) - for fun, name in ns.iter(ns.getters + ns.setters + ns.killers): - if WINDOWS: - wrapper(fun) - self.execute(lambda: wrapper(fun)) - - def test_system_apis(self): - ns = system_namespace - for fun, name in ns.iter(ns.all): - if WINDOWS: - fun() - if MACOS and name == 'net_connections': - continue # raise AD - self.execute(fun) - - if __name__ == '__main__': from psutil.tests.runner import run_from_name run_from_name(__file__) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 6c5100945d..1ceccbe2ad 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -43,7 +43,6 @@ from psutil.tests import system_namespace from psutil.tests import tcp_socketpair from psutil.tests import terminate -from psutil.tests import TestFdsLeak from psutil.tests import TestMemoryLeak from psutil.tests import unittest from psutil.tests import unix_socketpair @@ -354,7 +353,7 @@ def fun(): cnt['cnt'] += 1 cnt = {'cnt': 0} self.execute(fun, times=10, warmup_times=15) - self.assertEqual(cnt['cnt'], 25) + self.assertEqual(cnt['cnt'], 26) def test_param_err(self): self.assertRaises(ValueError, self.execute, lambda: 0, times=0) @@ -363,17 +362,29 @@ def test_param_err(self): self.assertRaises(ValueError, self.execute, lambda: 0, tolerance=-1) self.assertRaises(ValueError, self.execute, lambda: 0, retries=-1) - def test_leak(self): + def test_leak_mem(self): ls = [] def fun(ls=ls): ls.append("x" * 24 * 1024) try: - self.assertRaises(AssertionError, self.execute, fun) + self.assertRaisesRegex(AssertionError, "extra-mem", + self.execute, fun) 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) @@ -381,7 +392,7 @@ def fun(): times = 100 self.execute(fun, times=times, warmup_times=0, tolerance=200 * 1024 * 1024) - self.assertEqual(len(ls), times) + self.assertEqual(len(ls), times + 1) def test_execute_w_exc(self): def fun(): @@ -396,20 +407,6 @@ def fun(): self.execute_w_exc(ZeroDivisionError, fun) -@serialrun -class TestFdsLeakClass(TestFdsLeak): - - def test_unclosed_files(self): - def fun(): - f = open(__file__) - self.addCleanup(f.close) - box.append(f) - - box = [] - self.assertRaisesRegex(AssertionError, "unclosed", self.execute, fun) - self.assertEqual(len(box), 1) - - class TestTestingUtils(PsutilTestCase): def test_process_namespace(self): From 9c0ba43950861afd9fc9ff184bbb1f4fe32079d8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 May 2020 16:43:20 +0200 Subject: [PATCH 0572/1714] always execute python memleak tests on linux (just fewer times) --- psutil/tests/__init__.py | 12 ++-- psutil/tests/test_memory_leaks.py | 112 +++++++++++++++++------------- 2 files changed, 69 insertions(+), 55 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ccfcb60c2b..350f9f38d2 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -948,9 +948,6 @@ def _get_num_fds(self): else: return self._thisproc.num_handles() - def _call(self, fun): - return fun() - def _log(self, msg): if self.verbose: print_color(msg, color="yellow", file=sys.stderr) @@ -961,7 +958,7 @@ def _check_fds(self, fun): close(2) and CloseHandle syscalls. """ before = self._get_num_fds() - self._call(fun) + self.call(fun) after = self._get_num_fds() diff = after - before if diff < 0: @@ -981,7 +978,7 @@ def _call_ntimes(self, fun, times): gc.collect(generation=1) mem1 = self._get_mem() for x in range(times): - ret = self._call(fun) + ret = self.call(fun) del x, ret gc.collect(generation=1) mem2 = self._get_mem() @@ -1011,6 +1008,11 @@ def _check_mem(self, fun, times, warmup_times, retries, tolerance): 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.""" diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index b722f4fa7e..cab91428d2 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -53,14 +53,29 @@ from psutil.tests import unittest -SKIP_PYTHON_IMPL = True cext = psutil._psplatform.cext thisproc = psutil.Process() +FEW_TIMES = 5 -def skip_if_linux(): - return unittest.skipIf(LINUX and SKIP_PYTHON_IMPL, - "worthless on LINUX (pure python)") +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 # =================================================================== @@ -77,33 +92,33 @@ def test_coverage(self): ns = process_namespace(None) ns.test_class_coverage(self, ns.getters + ns.setters) - @skip_if_linux() + @fewtimes_if_linux() def test_name(self): self.execute(self.proc.name) - @skip_if_linux() + @fewtimes_if_linux() def test_cmdline(self): self.execute(self.proc.cmdline) - @skip_if_linux() + @fewtimes_if_linux() def test_exe(self): self.execute(self.proc.exe) - @skip_if_linux() + @fewtimes_if_linux() def test_ppid(self): self.execute(self.proc.ppid) @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() + @fewtimes_if_linux() def test_uids(self): self.execute(self.proc.uids) @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() + @fewtimes_if_linux() def test_gids(self): self.execute(self.proc.gids) - @skip_if_linux() + @fewtimes_if_linux() def test_status(self): self.execute(self.proc.status) @@ -129,7 +144,7 @@ def test_ionice_set(self): self.execute_w_exc(OSError, fun) @unittest.skipIf(not HAS_PROC_IO_COUNTERS, "not supported") - @skip_if_linux() + @fewtimes_if_linux() def test_io_counters(self): self.execute(self.proc.io_counters) @@ -139,11 +154,11 @@ def test_username(self): psutil.Process().username() self.execute(self.proc.username) - @skip_if_linux() + @fewtimes_if_linux() def test_create_time(self): self.execute(self.proc.create_time) - @skip_if_linux() + @fewtimes_if_linux() @skip_on_access_denied(only_if=OPENBSD) def test_num_threads(self): self.execute(self.proc.num_threads) @@ -153,47 +168,46 @@ def test_num_handles(self): self.execute(self.proc.num_handles) @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() + @fewtimes_if_linux() def test_num_fds(self): self.execute(self.proc.num_fds) - @skip_if_linux() + @fewtimes_if_linux() def test_num_ctx_switches(self): self.execute(self.proc.num_ctx_switches) - @skip_if_linux() + @fewtimes_if_linux() @skip_on_access_denied(only_if=OPENBSD) def test_threads(self): self.execute(self.proc.threads) - @skip_if_linux() + @fewtimes_if_linux() def test_cpu_times(self): self.execute(self.proc.cpu_times) - @skip_if_linux() + @fewtimes_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() + @fewtimes_if_linux() def test_memory_info(self): self.execute(self.proc.memory_info) - @skip_if_linux() + @fewtimes_if_linux() def test_memory_full_info(self): self.execute(self.proc.memory_full_info) @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() + @fewtimes_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) + times = FEW_TIMES if POSIX else self.times + self.execute(self.proc.resume, times=times) - @skip_if_linux() + @fewtimes_if_linux() def test_cwd(self): self.execute(self.proc.cwd) @@ -209,13 +223,13 @@ def test_cpu_affinity_set(self): self.execute_w_exc( ValueError, lambda: self.proc.cpu_affinity([-1])) - @skip_if_linux() + @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") - @skip_if_linux() + @fewtimes_if_linux() def test_memory_maps(self): self.execute(self.proc.memory_maps) @@ -231,7 +245,7 @@ def test_rlimit_set(self): self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) self.execute_w_exc(OSError, lambda: self.proc.rlimit(-1)) - @skip_if_linux() + @fewtimes_if_linux() # Windows implementation is based on a single system-wide # function (tested later). @unittest.skipIf(WINDOWS, "worthless on WINDOWS") @@ -272,7 +286,7 @@ def tearDownClass(cls): super().tearDownClass() terminate(cls.subp) - def _call(self, fun): + def call(self, fun): try: fun() except psutil.NoSuchProcess: @@ -330,27 +344,27 @@ def test_coverage(self): # --- cpu - @skip_if_linux() + @fewtimes_if_linux() def test_cpu_count(self): # logical self.execute(lambda: psutil.cpu_count(logical=True)) - @skip_if_linux() + @fewtimes_if_linux() def test_cpu_count_physical(self): self.execute(lambda: psutil.cpu_count(logical=False)) - @skip_if_linux() + @fewtimes_if_linux() def test_cpu_times(self): self.execute(psutil.cpu_times) - @skip_if_linux() + @fewtimes_if_linux() def test_per_cpu_times(self): self.execute(lambda: psutil.cpu_times(percpu=True)) - @skip_if_linux() + @fewtimes_if_linux() def test_cpu_stats(self): self.execute(psutil.cpu_stats) - @skip_if_linux() + @fewtimes_if_linux() @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq(self): self.execute(psutil.cpu_freq) @@ -370,41 +384,39 @@ def test_virtual_memory(self): 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(lambda: psutil.pid_exists(os.getpid())) + times = FEW_TIMES if POSIX else self.times + self.execute(lambda: psutil.pid_exists(os.getpid()), times=times) # --- disk - @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, - "worthless on POSIX (pure python)") def test_disk_usage(self): - self.execute(lambda: psutil.disk_usage('.')) + 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') - @skip_if_linux() + @fewtimes_if_linux() def test_disk_io_counters(self): self.execute(lambda: psutil.disk_io_counters(nowrap=False)) # --- proc - @skip_if_linux() + @fewtimes_if_linux() def test_pids(self): self.execute(psutil.pids) # --- net - @skip_if_linux() + @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)) - @skip_if_linux() + @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) @@ -423,24 +435,24 @@ def test_net_if_stats(self): # --- sensors - @skip_if_linux() + @fewtimes_if_linux() @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") def test_sensors_battery(self): self.execute(psutil.sensors_battery) - @skip_if_linux() + @fewtimes_if_linux() @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") def test_sensors_temperatures(self): self.execute(psutil.sensors_temperatures) - @skip_if_linux() + @fewtimes_if_linux() @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") def test_sensors_fans(self): self.execute(psutil.sensors_fans) # --- others - @skip_if_linux() + @fewtimes_if_linux() def test_boot_time(self): self.execute(psutil.boot_time) From 18e88998a5f25ab9335afbaacfa3db46287bfa1f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 May 2020 16:45:11 +0200 Subject: [PATCH 0573/1714] rename memleaks script --- .ci/travis/run.sh | 2 +- .flake8 | 2 +- MANIFEST.in | 2 +- Makefile | 2 +- psutil/tests/runner.py | 2 +- psutil/tests/{test_memory_leaks.py => test_memleaks.py} | 0 scripts/internal/winmake.py | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename psutil/tests/{test_memory_leaks.py => test_memleaks.py} (100%) diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh index 879e78a60c..ae593df5dd 100755 --- a/.ci/travis/run.sh +++ b/.ci/travis/run.sh @@ -27,7 +27,7 @@ fi if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then # run mem leaks test - PSUTIL_TESTING=1 python -Wa psutil/tests/test_memory_leaks.py + PSUTIL_TESTING=1 python -Wa psutil/tests/test_memleaks.py # run linter (on Linux only) if [[ "$(uname -s)" != 'Darwin' ]]; then make lint PYTHON=python diff --git a/.flake8 b/.flake8 index f06f8344ad..e125df09e4 100644 --- a/.flake8 +++ b/.flake8 @@ -12,4 +12,4 @@ per-file-ignores = setup.py:T001 scripts/*:T001 psutil/tests/runner.py:T001 - psutil/tests/test_memory_leaks.py:T001 + psutil/tests/test_memleaks.py:T001 diff --git a/MANIFEST.in b/MANIFEST.in index d0d7524019..2b51a54552 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -96,7 +96,7 @@ include psutil/tests/test_bsd.py include psutil/tests/test_connections.py include psutil/tests/test_contracts.py include psutil/tests/test_linux.py -include psutil/tests/test_memory_leaks.py +include psutil/tests/test_memleaks.py include psutil/tests/test_misc.py include psutil/tests/test_osx.py include psutil/tests/test_posix.py diff --git a/Makefile b/Makefile index 2b2a98efd5..29afe2de5f 100644 --- a/Makefile +++ b/Makefile @@ -162,7 +162,7 @@ test-platform: ## Run specific platform tests only. test-memleaks: ## Memory leak tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memory_leaks.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memleaks.py test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSystemAPIs ${MAKE} build diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 97139fbc1c..536bb9e44c 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -71,7 +71,7 @@ def cprint(msg, color, bold=False, file=None): class TestLoader: testdir = HERE - skip_files = ['test_memory_leaks.py'] + skip_files = ['test_memleaks.py'] if "WHEELHOUSE_UPLOADER_USERNAME" in os.environ: skip_files.extend(['test_osx.py', 'test_linux.py', 'test_posix.py']) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memleaks.py similarity index 100% rename from psutil/tests/test_memory_leaks.py rename to psutil/tests/test_memleaks.py diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index a83aed3e03..8fc3a98464 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -488,7 +488,7 @@ def test_failed(): def test_memleaks(): """Run memory leaks tests""" build() - sh("%s psutil\\tests\\test_memory_leaks.py" % PYTHON) + sh("%s psutil\\tests\\test_memleaks.py" % PYTHON) def install_git_hooks(): From d8a1f34108e96a023446a2c9b609b22e3692916a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 May 2020 16:52:12 +0200 Subject: [PATCH 0574/1714] have leak test use 3M instead of 70M to avoid swapping out on CI --- psutil/tests/test_testutils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 1ceccbe2ad..b676b76b95 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -369,8 +369,9 @@ def fun(ls=ls): ls.append("x" * 24 * 1024) try: + # will consume around 3M in total self.assertRaisesRegex(AssertionError, "extra-mem", - self.execute, fun) + self.execute, fun, times=50, retries=2) finally: del ls From 34d1006eb52b134e2f135430c1f25533ce62a00e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 May 2020 13:37:54 +0200 Subject: [PATCH 0575/1714] fix some tests --- psutil/tests/test_linux.py | 12 ++++++------ psutil/tests/test_misc.py | 2 +- psutil/tests/test_testutils.py | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 709c77bab9..eb694c3355 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -197,14 +197,14 @@ def test_total(self): 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): + # 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") free = free_physmem() free_value = free.used psutil_value = psutil.virtual_memory().used diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 300360cd21..becd930a1d 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -353,7 +353,7 @@ def check(ret): def test_setup_script(self): setup_py = os.path.join(ROOT_DIR, 'setup.py') - if TRAVIS and not os.path.exists(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) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index b676b76b95..901715a1a0 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -37,6 +37,7 @@ from psutil.tests import PYTHON_EXE 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 @@ -362,6 +363,7 @@ def test_param_err(self): self.assertRaises(ValueError, self.execute, lambda: 0, tolerance=-1) self.assertRaises(ValueError, self.execute, lambda: 0, retries=-1) + @retry_on_failure() def test_leak_mem(self): ls = [] From b543b3fddbcd9a1c931783e3f2230871a3e7ac9e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 May 2020 16:40:32 +0200 Subject: [PATCH 0576/1714] shorter testfn --- psutil/tests/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 350f9f38d2..61115d40ba 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -815,10 +815,8 @@ def get_testfn(suffix="", dir=None): deletion at interpreter exit. It's technically racy but probably not really due to the time variant. """ - timer = getattr(time, 'perf_counter', time.time) while True: - prefix = "%s%.9f-" % (TESTFN_PREFIX, timer()) - name = tempfile.mktemp(prefix=prefix, suffix=suffix, dir=dir) + name = tempfile.mktemp(prefix=TESTFN_PREFIX, suffix=suffix, dir=dir) if not os.path.exists(name): # also include dirs return os.path.realpath(name) # needed for OSX From 2c113c6746d3558e47e575b4169ac15b636ddf57 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 May 2020 17:34:14 +0200 Subject: [PATCH 0577/1714] better #ifdef detection for prlimit() availbility Rely on "__NR_prlimit64" availability and check GLIBC version only. Take kernel version out of the picture. This way it works on both CentOS 6 and 7. Also, have test_contracts.py tests assume prlimit() is always available, so that we will be notified (by failure). Ref: #1758 --- psutil/_psutil_linux.c | 7 +++--- psutil/tests/test_contracts.py | 41 ++++++++++++++++------------------ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 915ab9b486..5f00454ba4 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -42,11 +42,10 @@ static const int NCPUS_START = sizeof(unsigned long) * CHAR_BIT; // Linux >= 2.6.13 #define PSUTIL_HAVE_IOPRIO defined(__NR_ioprio_get) && defined(__NR_ioprio_set) -// Linux >= 2.6.36 (supposedly) and glibc >= 13 +// Linux >= 2.6.36 (supposedly) and glibc >= 2.13 #define PSUTIL_HAVE_PRLIMIT \ - (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 36)) && \ - (__GLIBC__ >= 2 && __GLIBC_MINOR__ >= 13) && \ - defined(__NR_prlimit64) + defined(__NR_prlimit64) && \ + (__GLIBC__ >= 2 && __GLIBC_MINOR__ >= 13) #if PSUTIL_HAVE_PRLIMIT #define _FILE_OFFSET_BITS 64 diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index edeb1d9a57..e9c68a914b 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -34,7 +34,6 @@ from psutil._compat import range from psutil.tests import create_sockets from psutil.tests import enum -from psutil.tests import get_kernel_version from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import HAS_SENSORS_FANS @@ -87,27 +86,25 @@ def test_linux_ioprio_windows(self): 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) + ae(hasattr(psutil.Process, "rlimit"), LINUX) # requires Linux 2.6.36 + ae(hasattr(psutil, "RLIM_INFINITY"), LINUX) + ae(hasattr(psutil, "RLIMIT_AS"), LINUX) + ae(hasattr(psutil, "RLIMIT_CORE"), LINUX) + ae(hasattr(psutil, "RLIMIT_CPU"), LINUX) + ae(hasattr(psutil, "RLIMIT_DATA"), LINUX) + ae(hasattr(psutil, "RLIMIT_FSIZE"), LINUX) + ae(hasattr(psutil, "RLIMIT_LOCKS"), LINUX) + ae(hasattr(psutil, "RLIMIT_MEMLOCK"), LINUX) + ae(hasattr(psutil, "RLIMIT_NOFILE"), LINUX) + ae(hasattr(psutil, "RLIMIT_NPROC"), LINUX) + ae(hasattr(psutil, "RLIMIT_RSS"), LINUX) + ae(hasattr(psutil, "RLIMIT_STACK"), LINUX) + + ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX) # requires Linux 2.6.8 + ae(hasattr(psutil, "RLIMIT_NICE"), LINUX) # requires Linux 2.6.12 + ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX) # requires Linux 2.6.12 + ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX) # requires Linux 2.6.25 + ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX) # requires Linux 2.6.8 class TestAvailSystemAPIs(PsutilTestCase): From c1f70d0a5039d340ccb4d5a66a75dbfcd63bd516 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 May 2020 20:08:17 +0200 Subject: [PATCH 0578/1714] #1758: fix github failures --- psutil/tests/__init__.py | 2 +- psutil/tests/runner.py | 4 ++-- psutil/tests/test_contracts.py | 7 ++----- psutil/tests/test_linux.py | 2 ++ psutil/tests/test_process.py | 12 ++---------- psutil/tests/test_unicode.py | 6 +++--- 6 files changed, 12 insertions(+), 21 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 61115d40ba..9667b5d510 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -698,7 +698,7 @@ def wait_for_pid(pid): time.sleep(0.01) -@retry(exception=(EnvironmentError, AssertionError), logfun=None, +@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.""" diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 536bb9e44c..8b18620345 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -43,7 +43,7 @@ from psutil._common import print_color from psutil._common import term_supports_colors from psutil._compat import super -from psutil.tests import APPVEYOR +from psutil.tests import CI_TESTING from psutil.tests import import_module_by_path from psutil.tests import reap_children from psutil.tests import safe_rmpath @@ -53,7 +53,7 @@ VERBOSITY = 1 if TOX else 2 FAILED_TESTS_FNAME = '.failed-tests.txt' NWORKERS = psutil.cpu_count() or 1 -USE_COLORS = term_supports_colors() and not APPVEYOR +USE_COLORS = not CI_TESTING and term_supports_colors() HERE = os.path.abspath(os.path.dirname(__file__)) loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index e9c68a914b..4cf417ba87 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -86,7 +86,6 @@ def test_linux_ioprio_windows(self): def test_linux_rlimit(self): ae = self.assertEqual - ae(hasattr(psutil.Process, "rlimit"), LINUX) # requires Linux 2.6.36 ae(hasattr(psutil, "RLIM_INFINITY"), LINUX) ae(hasattr(psutil, "RLIMIT_AS"), LINUX) ae(hasattr(psutil, "RLIMIT_CORE"), LINUX) @@ -116,11 +115,8 @@ def test_win_service_get(self): self.assertEqual(hasattr(psutil, "win_service_get"), WINDOWS) 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) + LINUX or MACOS or WINDOWS or FREEBSD) def test_sensors_temperatures(self): self.assertEqual( @@ -153,6 +149,7 @@ def test_ionice(self): self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) def test_rlimit(self): + # requires Linux 2.6.36 self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX) def test_io_counters(self): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index eb694c3355..9cc518bb4b 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -138,6 +138,8 @@ def vmstat(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, out.split()[-1].split('.'))) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 07a00e81cb..4448ec6d9b 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -365,16 +365,6 @@ def test_ionice_linux(self): 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", @@ -1096,6 +1086,8 @@ def test_children_duplicates(self): 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) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index e43d5326a6..181df0967e 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -89,10 +89,10 @@ from psutil.tests import ASCII_FS from psutil.tests import bind_unix_socket from psutil.tests import chdir +from psutil.tests import CI_TESTING from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe -from psutil.tests import spawn_testproc from psutil.tests import get_testfn from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import HAS_ENVIRON @@ -104,6 +104,7 @@ 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 from psutil.tests import TESTFN_PREFIX from psutil.tests import TRAVIS @@ -316,8 +317,7 @@ def expect_exact_path_match(self): return self.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(CI_TESTING, "unreliable on CI") @unittest.skipIf(PYPY, "unreliable on PYPY") @unittest.skipIf(not subprocess_supports_unicode(INVALID_UNICODE_SUFFIX), "subprocess can't deal with invalid unicode") From 9efb453e7163690c82226be3440cd8cb6bdffb5b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 16 May 2020 00:30:23 +0200 Subject: [PATCH 0579/1714] add strncpy / PSUTIL_STRNCPY variant which adds null terminator (fix gcc-9 warning) --- psutil/_psutil_aix.c | 2 +- psutil/_psutil_common.h | 5 +++++ psutil/_psutil_linux.c | 2 +- psutil/_psutil_posix.c | 8 ++++---- psutil/_psutil_sunos.c | 4 ++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index cf79d307d9..791e831e54 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -679,7 +679,7 @@ psutil_net_if_stats(PyObject* self, PyObject* args) { if (sock == -1) goto error; - strncpy(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // is up? ret = ioctl(sock, SIOCGIFFLAGS, &ifr); diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 34c428c058..2408c9f63e 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -15,6 +15,11 @@ extern int PSUTIL_DEBUG; // a signaler for connections without an actual status static const int PSUTIL_CONN_NONE = 128; +// strncpy() variant which appends a null terminator. +#define PSUTIL_STRNCPY(dst, src, n) \ + strncpy(dst, src, n - 1); \ + dst[n - 1] = '\0' + // ==================================================================== // --- Backward compatibility with missing Python.h APIs // ==================================================================== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 5f00454ba4..49e869974a 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -500,7 +500,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) return PyErr_SetFromOSErrnoWithSyscall("socket()"); - strncpy(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // duplex and speed memset(ðcmd, 0, sizeof ethcmd); diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 38483b3b02..f7f8b92dd8 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -356,10 +356,10 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { goto error; #ifdef PSUTIL_SUNOS10 - strncpy(lifr.lifr_name, nic_name, sizeof(lifr.lifr_name)); + PSUTIL_STRNCPY(lifr.lifr_name, nic_name, sizeof(lifr.lifr_name)); ret = ioctl(sock, SIOCGIFMTU, &lifr); #else - strncpy(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); ret = ioctl(sock, SIOCGIFMTU, &ifr); #endif if (ret == -1) @@ -398,7 +398,7 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { if (sock == -1) goto error; - strncpy(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); ret = ioctl(sock, SIOCGIFFLAGS, &ifr); if (ret == -1) goto error; @@ -578,7 +578,7 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) return PyErr_SetFromErrno(PyExc_OSError); - strncpy(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // speed / duplex memset(&ifmed, 0, sizeof(struct ifmediareq)); diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 6548640b70..82114c8c89 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1024,7 +1024,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { goto next; // check if this is a network interface by sending a ioctl - strncpy(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); + PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); if (ret == -1) goto next; @@ -1515,7 +1515,7 @@ psutil_net_if_stats(PyObject* self, PyObject* args) { if (strcmp(ksp->ks_class, "net") != 0) continue; - strncpy(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); + PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); if (ret == -1) continue; // not a network interface From 84a4ac3eb4465d296d00e7549242d2dc8c47b721 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 May 2020 16:28:10 -0700 Subject: [PATCH 0580/1714] [Linux] Process.rlimit() does not handle LONG LONG properly (#1760) --- HISTORY.rst | 1 + psutil/_psutil_linux.c | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c99b9c4e53..3bae76b4ab 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -30,6 +30,7 @@ XXXX-XX-XX - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. (patch by Michał Górny) +- 1760_: [Linux] Process.rlimit() does not handle long long type properly. 5.7.0 ===== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 49e869974a..c94ec03502 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -158,19 +158,17 @@ psutil_linux_prlimit(PyObject *self, PyObject *args) { ret = prlimit(pid, resource, NULL, &old); if (ret == -1) return PyErr_SetFromErrno(PyExc_OSError); -#if defined(PSUTIL_HAVE_LONG_LONG) if (sizeof(old.rlim_cur) > sizeof(long)) { return Py_BuildValue("LL", (PY_LONG_LONG)old.rlim_cur, (PY_LONG_LONG)old.rlim_max); } -#endif return Py_BuildValue("ll", (long)old.rlim_cur, (long)old.rlim_max); } // set else { -#if defined(PSUTIL_HAVE_LARGEFILE_SUPPORT) +#if defined(HAVE_LONG_LONG) new.rlim_cur = PyLong_AsLongLong(py_soft); if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) return NULL; From 135628639bd6d73b5e88aefe300acd13a04a858d Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Sat, 16 May 2020 14:01:07 +0200 Subject: [PATCH 0581/1714] Have GitHub build wheels on commit (#1758) --- .github/workflows/build_wheel.yml | 77 +++++++++++++++++++++++++++++++ psutil/tests/__init__.py | 7 ++- psutil/tests/test_system.py | 3 +- psutil/tests/test_unicode.py | 1 + setup.py | 9 ++-- 5 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/build_wheel.yml diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml new file mode 100644 index 0000000000..917a843641 --- /dev/null +++ b/.github/workflows/build_wheel.yml @@ -0,0 +1,77 @@ +name: Build wheel + +on: [push, pull_request] + +jobs: + wheel_without_test: + name: build wheel for ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + env: + CIBW_SKIP: "pp27-*win* cp27-*manylinux* pp-*manylinux*" + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_MANYLINUX_I686_IMAGE: manylinux2014 + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + name: Install Python 3.7 + with: + python-version: '3.7' + + - name: Install Visual C++ for Python 2.7 + if: startsWith(matrix.os, 'windows') + run: | + choco install vcpython27 -f -y + + - name: "install cibuildwheel" + run: pip install cibuildwheel==1.4.1 + + - name: build wheel + run: cibuildwheel . + + - name: Upload wheels + uses: actions/upload-artifact@v1 + with: + name: wheels2 + path: wheelhouse + + wheel: + name: build wheel for ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + env: + CIBW_SKIP: "pp27-*win* *27* cp27-*manylinux* pp-*manylinux*" + CIBW_TEST_COMMAND: python -Wa {project}/psutil/tests/runner.py + CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py + CIBW_TEST_EXTRAS: test + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_MANYLINUX_I686_IMAGE: manylinux2014 + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + name: Install Python 3.7 + with: + python-version: '3.7' + + - name: Install Visual C++ for Python 2.7 + if: startsWith(matrix.os, 'windows') + run: | + choco install vcpython27 -f -y + + - name: "install cibuildwheel" + run: pip install cibuildwheel==1.4.1 + + - name: build wheel + run: cibuildwheel . + + - name: Upload wheels + uses: actions/upload-artifact@v1 + with: + name: wheels + path: wheelhouse diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 9667b5d510..8a086b6b1e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -123,7 +123,8 @@ TRAVIS = bool(os.environ.get('TRAVIS')) APPVEYOR = bool(os.environ.get('APPVEYOR')) CIRRUS = bool(os.environ.get('CIRRUS')) -CI_TESTING = TRAVIS or APPVEYOR or CIRRUS +GITHUB_WHEELS = bool(os.environ.get('CIBUILDWHEEL', False)) +CI_TESTING = TRAVIS or APPVEYOR or CIRRUS or GITHUB_WHEELS # --- configurable defaults @@ -199,7 +200,9 @@ def attempt(exe): else: return exe - if MACOS: + if GITHUB_WHEELS: + return which('python') + elif MACOS: exe = \ attempt(sys.executable) or \ attempt(os.path.realpath(sys.executable)) or \ diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 09f5324feb..49eb2d0bdf 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -47,6 +47,7 @@ from psutil.tests import PYPY from psutil.tests import retry_on_failure from psutil.tests import TRAVIS +from psutil.tests import GITHUB_WHEELS from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest @@ -618,7 +619,7 @@ def test_disk_partitions(self): try: os.stat(disk.mountpoint) except OSError as err: - if TRAVIS and MACOS and err.errno == errno.EIO: + if (GITHUB_WHEELS or TRAVIS) and MACOS and err.errno == errno.EIO: continue # http://mail.python.org/pipermail/python-dev/ # 2012-June/120787.html diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 181df0967e..ec5ef4e30a 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -108,6 +108,7 @@ from psutil.tests import terminate from psutil.tests import TESTFN_PREFIX from psutil.tests import TRAVIS +from psutil.tests import GITHUB_WHEELS from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest import psutil diff --git a/setup.py b/setup.py index 19153bda33..260df14707 100755 --- a/setup.py +++ b/setup.py @@ -67,9 +67,12 @@ sources.append('psutil/_psutil_posix.c') -extras_require = {} -if sys.version_info[:2] <= (3, 3): - extras_require.update(dict(enum='enum34')) +extras_require = {"test": [ + "ipaddress; python_version < '3.0'", + "mock; python_version < '3.0'", + "pypiwin32; sys.platform == 'win32'", + "wmi; sys.platform == 'win32'", + "enum34; python_version <= '3.4'",]} def get_version(): From 42bc319fd087ede6ab91b149608bbfc030b64890 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 May 2020 00:25:08 +0200 Subject: [PATCH 0582/1714] Wheels2 (#1761) --- .github/workflows/build_wheel.yml | 16 +-- MANIFEST.in | 1 + Makefile | 7 +- psutil/_common.py | 7 +- psutil/arch/osx/process_info.c | 5 +- psutil/tests/__init__.py | 65 ++++++++-- psutil/tests/runner.py | 8 +- psutil/tests/test_aix.py | 10 +- psutil/tests/test_bsd.py | 42 +++---- psutil/tests/test_linux.py | 43 ++++--- psutil/tests/test_osx.py | 25 ++-- psutil/tests/test_process.py | 7 +- psutil/tests/test_system.py | 8 +- psutil/tests/test_unicode.py | 9 +- scripts/internal/download_wheels.py | 154 ++++++++++++++++++++++++ scripts/internal/win_download_wheels.py | 2 +- setup.py | 8 +- 17 files changed, 314 insertions(+), 103 deletions(-) create mode 100644 scripts/internal/download_wheels.py diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 917a843641..7d230b900e 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -7,10 +7,10 @@ jobs: name: build wheel for ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] - env: + env: CIBW_SKIP: "pp27-*win* cp27-*manylinux* pp-*manylinux*" CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 CIBW_MANYLINUX_I686_IMAGE: manylinux2014 @@ -28,7 +28,7 @@ jobs: - name: "install cibuildwheel" run: pip install cibuildwheel==1.4.1 - + - name: build wheel run: cibuildwheel . @@ -36,16 +36,16 @@ jobs: uses: actions/upload-artifact@v1 with: name: wheels2 - path: wheelhouse + path: wheelhouse wheel: name: build wheel for ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] - env: + env: CIBW_SKIP: "pp27-*win* *27* cp27-*manylinux* pp-*manylinux*" CIBW_TEST_COMMAND: python -Wa {project}/psutil/tests/runner.py CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py @@ -66,7 +66,7 @@ jobs: - name: "install cibuildwheel" run: pip install cibuildwheel==1.4.1 - + - name: build wheel run: cibuildwheel . @@ -74,4 +74,4 @@ jobs: uses: actions/upload-artifact@v1 with: name: wheels - path: wheelhouse + path: wheelhouse diff --git a/MANIFEST.in b/MANIFEST.in index 2b51a54552..93c4918011 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -117,6 +117,7 @@ include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py include scripts/internal/clinter.py +include scripts/internal/download_wheels.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py diff --git a/Makefile b/Makefile index 29afe2de5f..e91dfd818a 100644 --- a/Makefile +++ b/Makefile @@ -222,8 +222,11 @@ sdist: ## Create tar.gz source distribution. wheel: ## Generate wheel. $(PYTHON) setup.py bdist_wheel -win-download-wheels: ## Download wheels hosted on appveyor. - $(TEST_PREFIX) $(PYTHON) scripts/internal/win_download_wheels.py --user giampaolo --project psutil +win-download-wheels: ## Download latest wheels hosted on appveyor. + $(PYTHON) scripts/internal/win_download_wheels.py --user giampaolo --project psutil + +download-wheels: ## Download latest wheels hosted on github. + $(PYTHON) scripts/internal/download_wheels.py --user=giampaolo --project=psutil --tokenfile=~/.github.token upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist diff --git a/psutil/_common.py b/psutil/_common.py index b97bb01dcc..743664567e 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -780,12 +780,13 @@ def term_supports_colors(file=sys.stdout): return True -def hilite(s, color="green", bold=False): +def hilite(s, color=None, bold=False): """Return an highlighted version of 'string'.""" if not term_supports_colors(): return s attr = [] - colors = dict(green='32', red='91', brown='33', yellow='93') + colors = dict(green='32', red='91', brown='33', yellow='93', blue='34', + violet='35', lightblue='36', grey='37', darkgrey='30') colors[None] = '29' try: color = colors[color] @@ -798,7 +799,7 @@ def hilite(s, color="green", bold=False): return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) -def print_color(s, color="green", bold=False, file=sys.stdout): +def print_color(s, color=None, bold=False, file=sys.stdout): """Print a colorized version of string.""" if not term_supports_colors(): print(s, file=file) # NOQA diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 4b84a723a0..f83cfe47df 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -261,7 +261,7 @@ psutil_get_environ(pid_t pid) { if ((errno == EINVAL) && (psutil_pid_exists(pid))) NoSuchProcess("sysctl"); else - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); goto error; } @@ -320,7 +320,6 @@ psutil_get_environ(pid_t pid) { free(procargs); free(procenv); - return py_ret; empty: @@ -353,7 +352,7 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { // now read the data from sysctl if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { // raise an exception and throw errno as the error - PyErr_SetFromErrno(PyExc_OSError); + PyErr_SetFromOSErrnoWithSyscall("sysctl"); return -1; } diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 8a086b6b1e..df94d73733 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -74,10 +74,10 @@ __all__ = [ # constants - 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'SYSMEM_TOLERANCE', 'NO_RETRIES', + 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TOX', 'TRAVIS', 'CIRRUS', - 'CI_TESTING', 'VALID_PROC_STATUSES', + 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', "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", @@ -90,7 +90,7 @@ # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', - 'process_namespace', 'system_namespace', + 'process_namespace', 'system_namespace', 'print_sysinfo', # install utils 'install_pip', 'install_test_deps', # fs utils @@ -120,25 +120,28 @@ TOX = os.getenv('TOX') or '' in ('1', 'true') PYPY = '__pypy__' in sys.builtin_module_names # whether we're running this test suite on a Continuous Integration service -TRAVIS = bool(os.environ.get('TRAVIS')) -APPVEYOR = bool(os.environ.get('APPVEYOR')) -CIRRUS = bool(os.environ.get('CIRRUS')) -GITHUB_WHEELS = bool(os.environ.get('CIBUILDWHEEL', False)) +TRAVIS = 'TRAVIS' in os.environ +APPVEYOR = 'APPVEYOR' in os.environ +CIRRUS = 'CIRRUS' in os.environ +GITHUB_WHEELS = 'CIBUILDWHEEL' in os.environ CI_TESTING = TRAVIS or APPVEYOR or CIRRUS or GITHUB_WHEELS # --- configurable defaults # how many times retry_on_failure() decorator will retry NO_RETRIES = 10 -# bytes tolerance for system-wide memory related tests -SYSMEM_TOLERANCE = 500 * 1024 # 500KB +# bytes tolerance for system-wide related tests +TOLERANCE_SYS_MEM = 500 * 1024 # 500KB +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 travis / appveyor in order to avoid # false positives -if TRAVIS or APPVEYOR: +if CI_TESTING: NO_RETRIES *= 3 GLOBAL_TIMEOUT *= 3 + TOLERANCE_SYS_MEM *= 3 + TOLERANCE_DISK_USAGE *= 3 # --- file names @@ -154,7 +157,6 @@ 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 @@ -737,7 +739,7 @@ def retry_fun(fun): # 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 + stop_at = time.time() + GLOBAL_TIMEOUT while time.time() < stop_at: try: return fun() @@ -1045,6 +1047,45 @@ def call(): self.execute(call, **kwargs) +def print_sysinfo(): + import collections + import datetime + import getpass + import platform + + info = collections.OrderedDict() + info['OS'] = platform.system() + if psutil.OSX: + info['version'] = str(platform.mac_ver()) + elif psutil.WINDOWS: + info['version'] = ' '.join(map(str, platform.win32_ver())) + if hasattr(platform, 'win32_edition'): + info['edition'] = platform.win32_edition() + else: + info['version'] = platform.version() + if psutil.POSIX: + info['kernel'] = '.'.join(map(str, get_kernel_version())) + info['arch'] = ', '.join( + list(platform.architecture()) + [platform.machine()]) + info['hostname'] = platform.node() + info['python'] = ', '.join([ + platform.python_implementation(), + platform.python_version(), + platform.python_compiler()]) + if psutil.POSIX: + s = platform.libc_ver()[1] + if s: + info['glibc'] = s + info['fs-encoding'] = sys.getfilesystemencoding() + info['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + info['user'] = getpass.getuser() + info['pid'] = os.getpid() + print("=" * 70) # NOQA + for k, v in info.items(): + print("%-14s %s" % (k + ':', v)) # NOQA + print("=" * 70) # NOQA + + def _get_eligible_cpu(): p = psutil.Process() if hasattr(p, "cpu_num"): diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 8b18620345..35fea42a6f 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -45,6 +45,7 @@ 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 from psutil.tests import TOX @@ -193,6 +194,8 @@ def _exit(self, success): def run(self, suite): result = self._run(suite) + if CI_TESTING: + print_sysinfo() self._exit(result.wasSuccessful()) @@ -277,6 +280,8 @@ def run(self, suite): 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)) + if CI_TESTING: + print_sysinfo() ok = par.wasSuccessful() and ser.wasSuccessful() self._exit(ok) @@ -338,7 +343,8 @@ def main(): else: suite = loader.all() - # runner + if CI_TESTING: + print_sysinfo() runner = get_runner(opts.parallel) runner.run(suite) diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index caf20357fe..a32c3f6a17 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -38,17 +38,17 @@ def test_virtual_memory(self): psutil_result = psutil.virtual_memory() - # SYSMEM_TOLERANCE from psutil.tests is not enough. For some reason + # 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. - SYSMEM_TOLERANCE = 2 * KB * KB # 2 MB + TOLERANCE_SYS_MEM = 2 * KB * KB # 2 MB self.assertEqual(psutil_result.total, total) self.assertAlmostEqual( - psutil_result.used, used, delta=SYSMEM_TOLERANCE) + psutil_result.used, used, delta=TOLERANCE_SYS_MEM) self.assertAlmostEqual( - psutil_result.available, available, delta=SYSMEM_TOLERANCE) + psutil_result.available, available, delta=TOLERANCE_SYS_MEM) self.assertAlmostEqual( - psutil_result.free, free, delta=SYSMEM_TOLERANCE) + psutil_result.free, free, delta=TOLERANCE_SYS_MEM) def test_swap_memory(self): out = sh('/usr/sbin/lsps -a') diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index cfbec71d80..c91ee3a54c 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -25,7 +25,7 @@ from psutil.tests import PsutilTestCase from psutil.tests import retry_on_failure from psutil.tests import sh -from psutil.tests import SYSMEM_TOLERANCE +from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import terminate from psutil.tests import unittest from psutil.tests import which @@ -280,37 +280,37 @@ def test_cpu_frequency_against_sysctl(self): def test_vmem_active(self): syst = sysctl("vm.stats.vm.v_active_count") * PAGESIZE self.assertAlmostEqual(psutil.virtual_memory().active, syst, - delta=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) @retry_on_failure() def test_vmem_buffers(self): syst = sysctl("vfs.bufspace") self.assertAlmostEqual(psutil.virtual_memory().buffers, syst, - delta=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) # --- virtual_memory(); tests against muse @@ -324,42 +324,42 @@ def test_muse_vmem_total(self): def test_muse_vmem_active(self): num = muse('Active') self.assertAlmostEqual(psutil.virtual_memory().active, num, - delta=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) def test_cpu_stats_ctx_switches(self): self.assertAlmostEqual(psutil.cpu_stats().ctx_switches, @@ -388,17 +388,17 @@ def test_cpu_stats_syscalls(self): def test_swapmem_free(self): total, used, free = self.parse_swapinfo() self.assertAlmostEqual( - psutil.swap_memory().free, free, delta=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + psutil.swap_memory().total, total, delta=TOLERANCE_SYS_MEM) # --- others @@ -512,27 +512,27 @@ def test_vmem_total(self): def test_vmem_free(self): self.assertAlmostEqual( psutil.virtual_memory().free, self.parse_meminfo("MemFree:"), - delta=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) def test_vmem_buffers(self): self.assertAlmostEqual( psutil.virtual_memory().buffers, self.parse_meminfo("Buffers:"), - delta=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) def test_vmem_shared(self): self.assertAlmostEqual( psutil.virtual_memory().shared, self.parse_meminfo("MemShared:"), - delta=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) def test_swapmem_total(self): self.assertAlmostEqual( psutil.swap_memory().total, self.parse_meminfo("SwapTotal:"), - delta=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) def test_swapmem_free(self): self.assertAlmostEqual( psutil.swap_memory().free, self.parse_meminfo("SwapFree:"), - delta=SYSMEM_TOLERANCE) + delta=TOLERANCE_SYS_MEM) def test_swapmem_used(self): smem = psutil.swap_memory() diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 9cc518bb4b..9dd12890c6 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -28,6 +28,7 @@ from psutil._compat import PY3 from psutil._compat import u from psutil.tests import call_until +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 @@ -40,8 +41,9 @@ from psutil.tests import safe_rmpath from psutil.tests import sh from psutil.tests import skip_on_not_implemented -from psutil.tests import SYSMEM_TOLERANCE from psutil.tests import ThreadTask +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import TRAVIS from psutil.tests import unittest from psutil.tests import which @@ -211,7 +213,7 @@ def test_used(self): free_value = free.used psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( - free_value, psutil_value, delta=SYSMEM_TOLERANCE, + free_value, psutil_value, delta=TOLERANCE_SYS_MEM, msg='%s %s \n%s' % (free_value, psutil_value, free.output)) @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") @@ -220,14 +222,14 @@ def test_free(self): vmstat_value = vmstat('free memory') * 1024 psutil_value = psutil.virtual_memory().free self.assertAlmostEqual( - vmstat_value, psutil_value, delta=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) # https://travis-ci.org/giampaolo/psutil/jobs/226719664 @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") @@ -236,7 +238,7 @@ def test_active(self): vmstat_value = vmstat('active memory') * 1024 psutil_value = psutil.virtual_memory().active self.assertAlmostEqual( - vmstat_value, psutil_value, delta=SYSMEM_TOLERANCE) + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) # https://travis-ci.org/giampaolo/psutil/jobs/227242952 @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") @@ -245,7 +247,7 @@ def test_inactive(self): vmstat_value = vmstat('inactive memory') * 1024 psutil_value = psutil.virtual_memory().inactive self.assertAlmostEqual( - vmstat_value, psutil_value, delta=SYSMEM_TOLERANCE) + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) @retry_on_failure() def test_shared(self): @@ -255,7 +257,7 @@ def test_shared(self): raise unittest.SkipTest("free does not support 'shared' column") psutil_value = psutil.virtual_memory().shared self.assertAlmostEqual( - free_value, psutil_value, delta=SYSMEM_TOLERANCE, + free_value, psutil_value, delta=TOLERANCE_SYS_MEM, msg='%s %s \n%s' % (free_value, psutil_value, free.output)) @retry_on_failure() @@ -270,7 +272,7 @@ def test_available(self): free_value = int(lines[1].split()[-1]) psutil_value = psutil.virtual_memory().available self.assertAlmostEqual( - free_value, psutil_value, delta=SYSMEM_TOLERANCE, + free_value, psutil_value, delta=TOLERANCE_SYS_MEM, msg='%s %s \n%s' % (free_value, psutil_value, out)) def test_warnings_on_misses(self): @@ -508,21 +510,21 @@ def test_total(self): free_value = free_swap().total psutil_value = psutil.swap_memory().total return self.assertAlmostEqual( - free_value, psutil_value, delta=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + free_value, psutil_value, delta=TOLERANCE_SYS_MEM) def test_missing_sin_sout(self): with mock.patch('psutil._common.open', create=True) as m: @@ -572,7 +574,7 @@ def test_meminfo_against_sysinfo(self): total *= unit_multiplier free *= unit_multiplier self.assertEqual(swap.total, total) - self.assertAlmostEqual(swap.free, free, delta=SYSMEM_TOLERANCE) + 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 @@ -1025,11 +1027,10 @@ def df(path): 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)) + 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. @@ -1375,8 +1376,10 @@ def test_issue_687(self): t.start() try: p = psutil.Process() - tid = p.threads()[1].id - assert not psutil.pid_exists(tid), tid + 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()) @@ -1705,7 +1708,7 @@ def test_memory_full_info_mocked(self): def test_open_files_mode(self): def get_test_file(fname): p = psutil.Process() - giveup_at = time.time() + 2 + giveup_at = time.time() + GLOBAL_TIMEOUT while True: for file in p.open_files(): if file.path == os.path.abspath(fname): diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 1d6e1dc91e..14f6d1496a 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -12,14 +12,15 @@ import psutil from psutil import MACOS -from psutil.tests import spawn_zombie -from psutil.tests import spawn_testproc from psutil.tests import HAS_BATTERY from psutil.tests import PsutilTestCase from psutil.tests import retry_on_failure from psutil.tests import sh -from psutil.tests import SYSMEM_TOLERANCE +from psutil.tests import spawn_testproc +from psutil.tests import spawn_zombie from psutil.tests import terminate +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import unittest @@ -165,6 +166,7 @@ class TestSystemAPIs(PsutilTestCase): # --- disk + @retry_on_failure() def test_disks(self): # test psutil.disk_usage() and psutil.disk_partitions() # against "df -a" @@ -186,11 +188,10 @@ def df(path): 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) + self.assertAlmostEqual(usage.free, free, + delta=TOLERANCE_DISK_USAGE) + self.assertAlmostEqual(usage.used, used, + delta=TOLERANCE_DISK_USAGE) # --- cpu @@ -221,25 +222,25 @@ def test_vmem_total(self): def test_vmem_free(self): vmstat_val = vm_stat("free") psutil_val = psutil.virtual_memory().free - self.assertAlmostEqual(psutil_val, vmstat_val, delta=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + 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=SYSMEM_TOLERANCE) + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) # --- swap mem diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 4448ec6d9b..61890b8e6f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -41,6 +41,8 @@ from psutil.tests import copyload_shared_lib from psutil.tests import create_exe from psutil.tests import enum +from psutil.tests import GITHUB_WHEELS +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 @@ -209,7 +211,7 @@ def test_wait_timeout_nonblocking(self): p = self.spawn_psproc() self.assertRaises(psutil.TimeoutExpired, p.wait, 0) p.kill() - stop_at = time.time() + 2 + stop_at = time.time() + GLOBAL_TIMEOUT while time.time() < stop_at: try: code = p.wait(0) @@ -1379,7 +1381,8 @@ def clean_dict(d): p = psutil.Process() d1 = clean_dict(p.environ()) d2 = clean_dict(os.environ.copy()) - self.assertEqual(d1, d2) + if not OSX and GITHUB_WHEELS: + self.assertEqual(d1, d2) @unittest.skipIf(not HAS_ENVIRON, "not supported") @unittest.skipIf(not POSIX, "POSIX only") diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 49eb2d0bdf..004f4882c7 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -35,6 +35,7 @@ from psutil.tests import CI_TESTING from psutil.tests import DEVNULL from psutil.tests import enum +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 @@ -378,7 +379,7 @@ def test_cpu_times(self): def test_cpu_times_time_increases(self): # Make sure time increases between calls. t1 = sum(psutil.cpu_times()) - stop_at = time.time() + 1 + stop_at = time.time() + GLOBAL_TIMEOUT while time.time() < stop_at: t2 = sum(psutil.cpu_times()) if t2 > t1: @@ -422,7 +423,7 @@ 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() + 1 + giveup_at = time.time() + GLOBAL_TIMEOUT while True: if time.time() >= giveup_at: return self.fail("timeout") @@ -619,7 +620,8 @@ def test_disk_partitions(self): try: os.stat(disk.mountpoint) except OSError as err: - if (GITHUB_WHEELS or TRAVIS) and MACOS and err.errno == errno.EIO: + if (GITHUB_WHEELS or TRAVIS) and \ + MACOS and err.errno == errno.EIO: continue # http://mail.python.org/pipermail/python-dev/ # 2012-June/120787.html diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index ec5ef4e30a..af421d4ef7 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -108,7 +108,6 @@ from psutil.tests import terminate from psutil.tests import TESTFN_PREFIX from psutil.tests import TRAVIS -from psutil.tests import GITHUB_WHEELS from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest import psutil @@ -145,7 +144,7 @@ def subprocess_supports_unicode(suffix): safe_rmpath(testfn) create_exe(testfn) sproc = spawn_testproc(cmd=[testfn]) - except UnicodeEncodeError: + except (UnicodeEncodeError, IOError): return False else: return True @@ -231,8 +230,7 @@ def test_proc_open_files(self): @unittest.skipIf(not POSIX, "POSIX only") def test_proc_connections(self): - suffix = os.path.basename(self.funky_name) - name = self.get_testfn(suffix=suffix) + name = self.get_testfn(suffix=self.funky_suffix) try: sock = bind_unix_socket(name) except UnicodeEncodeError: @@ -257,8 +255,7 @@ def find_sock(cons): return conn raise ValueError("connection not found") - suffix = os.path.basename(self.funky_name) - name = self.get_testfn(suffix=suffix) + name = self.get_testfn(suffix=self.funky_suffix) try: sock = bind_unix_socket(name) except UnicodeEncodeError: diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py new file mode 100644 index 0000000000..1834f1b3af --- /dev/null +++ b/scripts/internal/download_wheels.py @@ -0,0 +1,154 @@ +#!/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. + +""" +Script which downloads wheel files hosted on GitHub: +https://github.com/giampaolo/psutil/actions +It needs an access token string generated from personal GitHub profile: +https://github.com/settings/tokens +The token must be created with at least "public_repo" scope/rights. +If you lose it, just generate a new token. +REST API doc: +https://developer.github.com/v3/actions/artifacts/ +""" + +import argparse +import collections +import json +import os +import requests +import shutil +import zipfile + +from psutil import __version__ as PSUTIL_VERSION +from psutil._common import bytes2human +from psutil._common import print_color + + +USER = "" +PROJECT = "" +TOKEN = "" +OUTFILE = "wheels.zip" + + +# --- GitHub API + + +def get_artifacts(): + base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) + url = base_url + "/actions/artifacts" + res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res.raise_for_status() + data = json.loads(res.content) + return data + + +def download_zip(url): + print("downloading: " + url) + res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res.raise_for_status() + totbytes = 0 + with open(OUTFILE, 'wb') as f: + for chunk in res.iter_content(chunk_size=16384): + f.write(chunk) + totbytes += len(chunk) + print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) + + +# --- extract + + +def rename_27_wheels(): + # See: https://github.com/giampaolo/psutil/issues/810 + src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION + dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION + print("rename: %s\n %s" % (src, dst)) + os.rename(src, dst) + src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PSUTIL_VERSION + dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PSUTIL_VERSION + print("rename: %s\n %s" % (src, dst)) + os.rename(src, dst) + + +def extract(): + with zipfile.ZipFile(OUTFILE, 'r') as zf: + zf.extractall('dist') + + +def print_wheels(): + def is64bit(name): + return name.endswith(('x86_64.whl', 'amd64.whl')) + + groups = collections.defaultdict(list) + for name in os.listdir('dist'): + plat = name.split('-')[-1] + pyimpl = name.split('-')[3] + ispypy = 'pypy' in pyimpl + if 'linux' in plat: + if ispypy: + groups['pypy_on_linux'].append(name) + else: + groups['linux'].append(name) + elif 'win' in plat: + if ispypy: + groups['pypy_on_windows'].append(name) + else: + groups['windows'].append(name) + elif 'macosx' in plat: + if ispypy: + groups['pypy_on_macos'].append(name) + else: + groups['macos'].append(name) + else: + assert 0, name + + totsize = 0 + templ = "%-54s %7s %7s %7s" + for platf, names in groups.items(): + ppn = "%s (total = %s)" % (platf.replace('_', ' '), len(names)) + s = templ % (ppn, "size", "arch", "pyver") + print_color('\n' + s, color=None, bold=True) + for name in sorted(names): + path = os.path.join('dist', name) + size = os.path.getsize(path) + totsize += size + arch = '64' if is64bit(name) else '32' + pyver = 'pypy' if name.split('-')[3].startswith('pypy') else 'py' + pyver += name.split('-')[2][2:] + s = templ % (name, bytes2human(size), arch, pyver) + if 'pypy' in pyver: + print_color(s, color='violet') + else: + print_color(s, color='brown') + + +def run(): + if os.path.isdir('dist'): + shutil.rmtree('dist') + data = get_artifacts() + download_zip(data['artifacts'][0]['archive_download_url']) + os.mkdir('dist') + extract() + # rename_27_wheels() + print_wheels() + + +def main(): + global USER, PROJECT, TOKEN + parser = argparse.ArgumentParser(description='GitHub wheels downloader') + parser.add_argument('--user', required=True) + parser.add_argument('--project', required=True) + parser.add_argument('--tokenfile', required=True) + args = parser.parse_args() + USER = args.user + PROJECT = args.project + with open(os.path.expanduser(args.tokenfile)) as f: + TOKEN = f.read().strip() + run() + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/win_download_wheels.py b/scripts/internal/win_download_wheels.py index 3720dd96c0..8dae05730b 100755 --- a/scripts/internal/win_download_wheels.py +++ b/scripts/internal/win_download_wheels.py @@ -5,7 +5,7 @@ # found in the LICENSE file. """ -Script which downloads exe and wheel files hosted on AppVeyor: +Script which downloads wheel files hosted on AppVeyor: https://ci.appveyor.com/project/giampaolo/psutil Readapted from the original recipe of Ibarra Corretge' : diff --git a/setup.py b/setup.py index 260df14707..ac7b8b53f7 100755 --- a/setup.py +++ b/setup.py @@ -69,10 +69,10 @@ extras_require = {"test": [ "ipaddress; python_version < '3.0'", - "mock; python_version < '3.0'", - "pypiwin32; sys.platform == 'win32'", - "wmi; sys.platform == 'win32'", - "enum34; python_version <= '3.4'",]} + "mock; python_version < '3.0'", + "pypiwin32; sys.platform == 'win32'", + "wmi; sys.platform == 'win32'", + "enum34; python_version <= '3.4'"]} def get_version(): From 407a3e821e620231874d9187eee0d12b015fb006 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 May 2020 13:50:02 +0200 Subject: [PATCH 0583/1714] enable github sponsors --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c39b2b6145..dd130351d7 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,7 @@ # These are supported funding model platforms tidelift: "pypi/psutil" -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: giampaolo patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username From 51c97dc5d98ebfebcc2fde4060f182ae19b76be1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 May 2020 14:48:36 +0200 Subject: [PATCH 0584/1714] fix occasional false positives --- psutil/tests/test_contracts.py | 5 +++-- psutil/tests/test_process.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 4cf417ba87..29e1b7195e 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -28,7 +28,6 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS -from psutil._common import isfile_strict from psutil._compat import FileNotFoundError from psutil._compat import long from psutil._compat import range @@ -590,9 +589,11 @@ def open_files(self, ret, info): continue assert os.path.isabs(f.path), f try: - assert isfile_strict(f.path), f + 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) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 61890b8e6f..59942f596c 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -551,6 +551,7 @@ def test_threads_2(self): 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() From 291d9c9c766eae248e4394b6e03666dbafe45677 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 May 2020 17:12:14 +0200 Subject: [PATCH 0585/1714] refactor FetchAll test --- psutil/tests/test_contracts.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 29e1b7195e..a57e19e915 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -331,9 +331,9 @@ def check_exception(exc, proc, name, ppid): tcase.assertEqual(exc.pid, pid) tcase.assertEqual(exc.name, name) if isinstance(exc, psutil.ZombieProcess): - # XXX investigate zombie/ppid relation on POSIX - # tcase.assertEqual(exc.ppid, ppid) - pass + 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) @@ -359,11 +359,7 @@ def do_wait(): for fun, fun_name in ns.iter(ns.getters, clear_cache=False): try: info[fun_name] = fun() - except psutil.NoSuchProcess as exc: - check_exception(exc, proc, name, ppid) - do_wait() - return info - except psutil.AccessDenied as exc: + except psutil.Error as exc: check_exception(exc, proc, name, ppid) continue do_wait() From 7130b3043800077b48c617325fb36358ee1d33ee Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 May 2020 18:23:21 +0200 Subject: [PATCH 0586/1714] memleak test: retries 10 times on CI instead of 5 --- psutil/tests/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index df94d73733..d3d02d11aa 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -935,7 +935,7 @@ def test_fun(self): times = 200 warmup_times = 10 tolerance = 0 # memory - retries = 5 + retries = 10 if CI_TESTING else 5 verbose = True _thisproc = psutil.Process() @@ -998,7 +998,7 @@ def _check_mem(self, fun, times, warmup_times, retries, tolerance): 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 + success = mem <= tolerance or mem <= prev_mem if success: if idx > 1: self._log(msg) From 978296429c3eac20f25e6dff7c2e2ab59327221e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 May 2020 21:41:23 +0200 Subject: [PATCH 0587/1714] some fixes to help #1762 --- psutil/tests/runner.py | 4 -- psutil/tests/test_contracts.py | 5 ++ psutil/tests/test_process.py | 93 +++++++++++++++++++--------------- psutil/tests/test_testutils.py | 4 +- psutil/tests/test_windows.py | 5 +- 5 files changed, 65 insertions(+), 46 deletions(-) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 35fea42a6f..8b86a3e99f 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -194,8 +194,6 @@ def _exit(self, success): def run(self, suite): result = self._run(suite) - if CI_TESTING: - print_sysinfo() self._exit(result.wasSuccessful()) @@ -280,8 +278,6 @@ def run(self, suite): 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)) - if CI_TESTING: - print_sysinfo() ok = par.wasSuccessful() and ser.wasSuccessful() self._exit(ok) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index a57e19e915..57d11b4368 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -14,6 +14,7 @@ import os import signal import stat +import sys import time import traceback @@ -672,6 +673,10 @@ def nice(self, ret, info): priorities = [getattr(psutil, x) for x in dir(psutil) if x.endswith('_PRIORITY_CLASS')] self.assertIn(ret, priorities) + if sys.version_info > (3, 4): + self.assertIsInstance(ret, enum.IntEnum) + else: + self.assertIsInstance(ret, int) def num_ctx_switches(self, ret, info): assert is_namedtuple(ret) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 59942f596c..4a21cae50d 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -37,10 +37,10 @@ from psutil._compat import super from psutil.tests import APPVEYOR from psutil.tests import call_until +from psutil.tests import CI_TESTING from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe -from psutil.tests import enum from psutil.tests import GITHUB_WHEELS from psutil.tests import GLOBAL_TIMEOUT from psutil.tests import HAS_CPU_AFFINITY @@ -349,11 +349,13 @@ def test_io_counters(self): @unittest.skipIf(not LINUX, "linux only") def test_ionice_linux(self): p = psutil.Process() - self.assertEqual(p.ionice()[0], psutil.IOPRIO_CLASS_NONE) + 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() try: # low p.ionice(psutil.IOPRIO_CLASS_IDLE) @@ -367,6 +369,10 @@ def test_ionice_linux(self): 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 self.assertRaisesRegex( ValueError, "ioclass accepts no value", @@ -378,13 +384,18 @@ def test_ionice_linux(self): ValueError, "'ioclass' argument must be specified", p.ionice, value=1) finally: - p.ionice(psutil.IOPRIO_CLASS_BE) + ioclass, value = init + if ioclass == psutil.IOPRIO_CLASS_NONE: + value = 0 + p.ionice(ioclass, value) @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) + if not CI_TESTING: + self.assertEqual(p.ionice(), psutil.IOPRIO_NORMAL) + init = p.ionice() try: # base p.ionice(psutil.IOPRIO_VERYLOW) @@ -405,8 +416,7 @@ def test_ionice_win(self): 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) + p.ionice(init) @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit_get(self): @@ -784,43 +794,46 @@ def test_gids(self): 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: + init = p.nice() + try: + if WINDOWS: + for prio in [psutil.NORMAL_PRIORITY_CLASS, + psutil.IDLE_PRIORITY_CLASS, + psutil.BELOW_NORMAL_PRIORITY_CLASS, + psutil.REALTIME_PRIORITY_CLASS, + psutil.HIGH_PRIORITY_CLASS, + psutil.ABOVE_NORMAL_PRIORITY_CLASS]: + with self.subTest(prio=prio): + try: + p.nice(prio) + except psutil.AccessDenied: + pass + else: + self.assertEqual(p.nice(), prio) + else: try: - p.nice(first_nice) + 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(init) + except psutil.AccessDenied: + pass def test_status(self): p = psutil.Process() diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 901715a1a0..31ac4dee4c 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -27,6 +27,7 @@ from psutil.tests import bind_unix_socket from psutil.tests import call_until from psutil.tests import chdir +from psutil.tests import CI_TESTING from psutil.tests import create_sockets from psutil.tests import get_free_port from psutil.tests import HAS_CONNECTIONS_UNIX @@ -364,6 +365,7 @@ def test_param_err(self): self.assertRaises(ValueError, self.execute, lambda: 0, retries=-1) @retry_on_failure() + @unittest.skipIf(CI_TESTING, "skipped on CI") def test_leak_mem(self): ls = [] @@ -373,7 +375,7 @@ def fun(ls=ls): try: # will consume around 3M in total self.assertRaisesRegex(AssertionError, "extra-mem", - self.execute, fun, times=50, retries=2) + self.execute, fun, times=50) finally: del ls diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 23ad05843e..7647395ac8 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -166,6 +166,8 @@ def test_disks(self): 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: @@ -199,7 +201,8 @@ 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)] + psutil_value = [x.mountpoint for x in psutil.disk_partitions(all=True) + if not x.startswith('A:')] self.assertEqual(sys_value, psutil_value) def test_net_if_stats(self): From bb2c6cfb6aa83a8557051f3fd1d451bd419dfc04 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 23 May 2020 17:34:17 +0200 Subject: [PATCH 0588/1714] Wheels 3 (#1764) --- .flake8 | 2 - .travis.yml | 2 +- psutil/_pswindows.py | 8 ++-- psutil/tests/__init__.py | 20 +++++---- psutil/tests/test_contracts.py | 9 ++-- psutil/tests/test_misc.py | 2 +- psutil/tests/test_osx.py | 60 -------------------------- psutil/tests/test_process.py | 19 ++++++--- psutil/tests/test_system.py | 7 ++-- psutil/tests/test_unicode.py | 23 +++++----- psutil/tests/test_windows.py | 77 +++++++++++++++++----------------- scripts/internal/winmake.py | 14 +++---- setup.py | 11 +++-- 13 files changed, 107 insertions(+), 147 deletions(-) diff --git a/.flake8 b/.flake8 index e125df09e4..15efab525b 100644 --- a/.flake8 +++ b/.flake8 @@ -4,8 +4,6 @@ [flake8] ignore = - # ambiguous variable name 'l' - E741, # line break after binary operator W504 per-file-ignores = diff --git a/.travis.yml b/.travis.yml index d49f2cf1f7..e51ecf0309 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ matrix: env: PYVER=py27 - language: generic os: osx - env: PYVER=py36 + env: PYVER=py38 # Linux - python: 2.7 - python: 3.5 diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index c1707db762..98baef5955 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -1066,11 +1066,11 @@ def from_bitmask(x): @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: + for b in ls: out |= 2 ** b return out diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index d3d02d11aa..6a119bf56b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -54,10 +54,10 @@ 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: +if PY3: import unittest +else: + import unittest2 as unittest # requires "pip install unittest2" try: from unittest import mock # py3 @@ -77,7 +77,7 @@ 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TOX', 'TRAVIS', 'CIRRUS', - 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', + '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", @@ -125,13 +125,16 @@ CIRRUS = 'CIRRUS' in os.environ GITHUB_WHEELS = 'CIBUILDWHEEL' in os.environ CI_TESTING = TRAVIS or APPVEYOR or CIRRUS or GITHUB_WHEELS +# are we a 64 bit process? +IS_64BIT = sys.maxsize > 2 ** 32 + # --- configurable defaults # how many times retry_on_failure() decorator will retry NO_RETRIES = 10 # bytes tolerance for system-wide related tests -TOLERANCE_SYS_MEM = 500 * 1024 # 500KB +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 @@ -203,7 +206,10 @@ def attempt(exe): return exe if GITHUB_WHEELS: - return which('python') + if PYPY: + return which("pypy3") if PY3 else which("pypy") + else: + return which('python') elif MACOS: exe = \ attempt(sys.executable) or \ @@ -1475,7 +1481,7 @@ def check_net_address(addr, family): IPv6 and MAC addresses. """ import ipaddress # python >= 3.3 / requires "pip install ipaddress" - if enum and PY3: + 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('.')] diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 57d11b4368..35ab61e01c 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -41,6 +41,7 @@ from psutil.tests import is_namedtuple from psutil.tests import process_namespace from psutil.tests import PsutilTestCase +from psutil.tests import PYPY from psutil.tests import serialrun from psutil.tests import SKIP_SYSCONS from psutil.tests import unittest @@ -247,7 +248,7 @@ def test_net_if_addrs(self): for ifname, addrs in psutil.net_if_addrs().items(): self.assertIsInstance(ifname, str) for addr in addrs: - if enum is not None: + if enum is not None and not PYPY: self.assertIsInstance(addr.family, enum.IntEnum) else: self.assertIsInstance(addr.family, int) @@ -381,13 +382,15 @@ def tearDown(self): self.pool.terminate() self.pool.join() - def test_all(self): + def iter_proc_info(self): # Fixes "can't pickle : it's not the # same object as test_contracts.proc_info". from psutil.tests.test_contracts import proc_info + return self.pool.imap_unordered(proc_info, psutil.pids()) + def test_all(self): failures = [] - for info in self.pool.imap_unordered(proc_info, psutil.pids()): + for info in self.iter_proc_info(): for name, value in info.items(): meth = getattr(self, name) try: diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index becd930a1d..10e45d23f5 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -323,7 +323,7 @@ def test_isfile_strict(self): 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")): + 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) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 14f6d1496a..097bff1084 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -17,7 +17,6 @@ from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import spawn_testproc -from psutil.tests import spawn_zombie from psutil.tests import terminate from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM @@ -102,65 +101,6 @@ def test_process_create_time(self): time.strftime("%Y", time.localtime(start_psutil))) -# TODO: probably needs removal (duplicate) -@unittest.skipIf(not MACOS, "MACOS only") -class TestZombieProcessAPIs(PsutilTestCase): - - @classmethod - def setUpClass(cls): - cls.parent, cls.zombie = spawn_zombie() - - @classmethod - def tearDownClass(cls): - terminate(cls.parent) - terminate(cls.zombie) - - def test_pidtask_info(self): - self.assertEqual(self.zombie.status(), psutil.STATUS_ZOMBIE) - self.zombie.ppid() - self.zombie.uids() - self.zombie.gids() - self.zombie.terminal() - self.zombie.create_time() - - def test_exe(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.exe) - - def test_cmdline(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.cmdline) - - def test_environ(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.environ) - - def test_cwd(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.cwd) - - def test_memory_full_info(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.memory_full_info) - - def test_cpu_times(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.cpu_times) - - def test_num_ctx_switches(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.num_ctx_switches) - - def test_num_threads(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.num_threads) - - def test_open_files(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.open_files) - - def test_connections(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.connections) - - def test_num_fds(self): - self.assertRaises(psutil.ZombieProcess, self.zombie.num_fds) - - def test_threads(self): - self.assertRaises((psutil.ZombieProcess, psutil.AccessDenied), - self.zombie.threads) - - @unittest.skipIf(not MACOS, "MACOS only") class TestSystemAPIs(PsutilTestCase): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 4a21cae50d..a0b21c6e55 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -13,6 +13,7 @@ import os import signal import socket +import stat import subprocess import sys import textwrap @@ -32,6 +33,7 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import open_text +from psutil._compat import FileNotFoundError from psutil._compat import long from psutil._compat import PY3 from psutil._compat import super @@ -637,7 +639,12 @@ def test_memory_maps(self): # 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 + 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) @@ -923,8 +930,8 @@ def test_cpu_affinity_all_combinations(self): if len(initial) > 12: initial = initial[:12] # ...otherwise it will take forever combos = [] - for l in range(0, len(initial) + 1): - for subset in itertools.combinations(initial, l): + for i in range(0, len(initial) + 1): + for subset in itertools.combinations(initial, i): if subset: combos.append(list(subset)) @@ -1271,7 +1278,7 @@ def assert_raises_nsp(fun, fun_name): assert_raises_nsp(fun, name) # NtQuerySystemInformation succeeds even if process is gone. - if WINDOWS: + if WINDOWS and not GITHUB_WHEELS: normcase = os.path.normcase self.assertEqual(normcase(p.exe()), normcase(PYTHON_EXE)) @@ -1509,9 +1516,9 @@ def test_misc(self): self.assertRaises(AttributeError, getattr, proc, 'foo') proc.terminate() if POSIX: - self.assertEqual(proc.wait(), -signal.SIGTERM) + self.assertEqual(proc.wait(5), -signal.SIGTERM) else: - self.assertEqual(proc.wait(), signal.SIGTERM) + self.assertEqual(proc.wait(5), signal.SIGTERM) def test_ctx_manager(self): with psutil.Popen([PYTHON_EXE, "-V"], diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 004f4882c7..e368ea7659 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -43,6 +43,7 @@ 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 mock from psutil.tests import PsutilTestCase from psutil.tests import PYPY @@ -544,8 +545,7 @@ def check_ls(ls): @unittest.skipIf(not HAS_GETLOADAVG, "not supported") def test_getloadavg(self): loadavg = psutil.getloadavg() - assert len(loadavg) == 3 - + self.assertEqual(len(loadavg), 3) for load in loadavg: self.assertIsInstance(load, float) self.assertGreaterEqual(load, 0.0) @@ -553,6 +553,7 @@ def test_getloadavg(self): 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')) @@ -748,7 +749,7 @@ def test_net_if_addrs(self): 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): + if sys.version_info >= (3, 4) and not PYPY: self.assertIsInstance(addr.family, enum.IntEnum) if nic_stats[nic].isup: # Do not test binding to addresses of interfaces diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index af421d4ef7..fba5623925 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -74,12 +74,12 @@ """ import os +import shutil 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 @@ -107,7 +107,6 @@ from psutil.tests import spawn_testproc from psutil.tests import terminate from psutil.tests import TESTFN_PREFIX -from psutil.tests import TRAVIS from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest import psutil @@ -132,7 +131,7 @@ def safe_rmpath(path): # NOQA traceback.print_exc() -def subprocess_supports_unicode(suffix): +def try_unicode(suffix): """Return True if both the fs and the subprocess module can deal with a unicode file name. """ @@ -144,6 +143,8 @@ def subprocess_supports_unicode(suffix): safe_rmpath(testfn) create_exe(testfn) sproc = spawn_testproc(cmd=[testfn]) + shutil.copyfile(testfn, testfn + '-2') + safe_rmpath(testfn + '-2') except (UnicodeEncodeError, IOError): return False else: @@ -160,6 +161,8 @@ def subprocess_supports_unicode(suffix): @serialrun +@unittest.skipIf(ASCII_FS, "ASCII fs") +@unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2") class _BaseFSAPIsTests(object): funky_suffix = None @@ -297,11 +300,10 @@ def normpath(p): # 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(UNICODE_SUFFIX), - "subprocess can't deal with unicode") +# @unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS") +# @unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO +@unittest.skipIf(not try_unicode(UNICODE_SUFFIX), + "can't deal with unicode str") class TestFSAPIs(_BaseFSAPIsTests, PsutilTestCase): """Test FS APIs with a funky, valid, UTF8 path name.""" funky_suffix = UNICODE_SUFFIX @@ -316,9 +318,8 @@ def expect_exact_path_match(self): @unittest.skipIf(CI_TESTING, "unreliable on CI") -@unittest.skipIf(PYPY, "unreliable on PYPY") -@unittest.skipIf(not subprocess_supports_unicode(INVALID_UNICODE_SUFFIX), - "subprocess can't deal with invalid unicode") +@unittest.skipIf(not try_unicode(INVALID_UNICODE_SUFFIX), + "can't deal with invalid unicode str") class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, PsutilTestCase): """Test FS APIs with a funky, invalid path name.""" funky_suffix = INVALID_UNICODE_SUFFIX diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 7647395ac8..945bb2eda1 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -24,22 +24,25 @@ from psutil._compat import FileNotFoundError from psutil._compat import super from psutil.tests import APPVEYOR -from psutil.tests import spawn_testproc +from psutil.tests import GITHUB_WHEELS from psutil.tests import HAS_BATTERY +from psutil.tests import IS_64BIT from psutil.tests import mock from psutil.tests import PsutilTestCase from psutil.tests import PY3 from psutil.tests import PYPY 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 TOLERANCE_DISK_USAGE from psutil.tests import unittest if WINDOWS and not PYPY: with warnings.catch_warnings(): warnings.simplefilter("ignore") - import win32api # requires "pip install pypiwin32" + import win32api # requires "pip install pywin32" import win32con import win32process import wmi # requires "pip install wmi" / "make setup-dev-env" @@ -47,9 +50,6 @@ 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): @@ -65,8 +65,12 @@ def wrapper(self, *args, **kwargs): return wrapper -@unittest.skipIf(PYPY, "pywin32 not available on PYPY") # skip whole module -class TestCase(PsutilTestCase): +@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_WHEELS and (not PY3 or not IS_64BIT), + "pywin32 broken on GITHUB + PY2") +class WindowsTestCase(PsutilTestCase): pass @@ -75,8 +79,7 @@ class TestCase(PsutilTestCase): # =================================================================== -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestCpuAPIs(TestCase): +class TestCpuAPIs(WindowsTestCase): @unittest.skipIf('NUMBER_OF_PROCESSORS' not in os.environ, 'NUMBER_OF_PROCESSORS env var is not available') @@ -114,8 +117,7 @@ def test_cpu_freq(self): self.assertEqual(proc.MaxClockSpeed, psutil.cpu_freq().max) -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestSystemAPIs(TestCase): +class TestSystemAPIs(WindowsTestCase): def test_nic_names(self): out = sh('ipconfig /all') @@ -184,6 +186,7 @@ def test_disks(self): else: 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: @@ -191,9 +194,9 @@ def test_disk_usage(self): sys_value = win32api.GetDiskFreeSpaceEx(disk.mountpoint) psutil_value = psutil.disk_usage(disk.mountpoint) self.assertAlmostEqual(sys_value[0], psutil_value.free, - delta=1024 * 1024) + delta=TOLERANCE_DISK_USAGE) self.assertAlmostEqual(sys_value[1], psutil_value.total, - delta=1024 * 1024) + delta=TOLERANCE_DISK_USAGE) self.assertEqual(psutil_value.used, psutil_value.total - psutil_value.free) @@ -202,7 +205,7 @@ def test_disk_partitions(self): 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.startswith('A:')] + if not x.mountpoint.startswith('A:')] self.assertEqual(sys_value, psutil_value) def test_net_if_stats(self): @@ -241,8 +244,7 @@ def test_boot_time_fluctuation(self): # =================================================================== -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestSensorsBattery(TestCase): +class TestSensorsBattery(WindowsTestCase): def test_has_battery(self): if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: @@ -302,8 +304,7 @@ def test_emulate_secs_left_unknown(self): # =================================================================== -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcess(PsutilTestCase): +class TestProcess(WindowsTestCase): @classmethod def setUpClass(cls): @@ -494,8 +495,7 @@ def test_exe(self): self.assertRaises(psutil.NoSuchProcess, proc.exe) -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcessWMI(TestCase): +class TestProcessWMI(WindowsTestCase): """Compare Process API results with WMI.""" @classmethod @@ -511,6 +511,7 @@ def test_name(self): p = psutil.Process(self.pid) self.assertEqual(p.name(), w.Caption) + @unittest.skipIf(GITHUB_WHEELS, "unreliable path on GITHUB_WHEELS") def test_exe(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] p = psutil.Process(self.pid) @@ -531,15 +532,15 @@ def test_username(self): username = "%s\\%s" % (domain, username) self.assertEqual(p.username(), username) + @retry_on_failure() 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)) + @retry_on_failure() 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 @@ -560,8 +561,11 @@ def test_create_time(self): self.assertEqual(wmic_create, psutil_create) +# --- + + @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestDualProcessImplementation(TestCase): +class TestDualProcessImplementation(PsutilTestCase): """ Certain APIs on Windows have 2 internal implementations, one based on documented Windows APIs, another one based @@ -664,28 +668,25 @@ def find_other_interpreter(): stderr=subprocess.STDOUT) output, _ = proc.communicate() proc.wait() - if output == str(not IS_64_BIT): + if output == str(not IS_64BIT): return filename - @classmethod - def setUpClass(cls): - other_python = cls.find_other_interpreter() + 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_64_BIT: - cls.python64 = sys.executable - cls.python32 = other_python + if IS_64BIT: + self.python64 = sys.executable + self.python32 = other_python else: - cls.python64 = other_python - cls.python32 = sys.executable - - test_args = ["-c", "import sys; sys.stdin.read()"] + self.python64 = other_python + self.python32 = sys.executable - def setUp(self): - super().setUp() env = os.environ.copy() env["THINK_OF_A_NUMBER"] = str(os.getpid()) self.proc32 = self.spawn_testproc( @@ -740,7 +741,7 @@ def test_environ_64(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestServices(TestCase): +class TestServices(PsutilTestCase): def test_win_service_iter(self): valid_statuses = set([ diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 8fc3a98464..3d9d0a8d55 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -47,21 +47,19 @@ "pyreadline", "setuptools", "wheel", - "wmi", "requests" ] -if sys.version_info[:2] <= (2, 6): +if sys.version_info[:2] <= (2, 7): DEPS.append('unittest2') if sys.version_info[:2] <= (2, 7): DEPS.append('mock') if sys.version_info[:2] <= (3, 2): DEPS.append('ipaddress') -if PYPY: - pass -elif sys.version_info[:2] <= (3, 4): - DEPS.append("pypiwin32==219") -else: - DEPS.append("pypiwin32") +if sys.version_info[:2] <= (3, 4): + DEPS.append('enum34') +if not PYPY: + DEPS.append("pywin32") + DEPS.append("wmi") _cmds = {} if PY3: diff --git a/setup.py b/setup.py index ac7b8b53f7..3c44230293 100755 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ from _compat import which # NOQA +PYPY = '__pypy__' in sys.builtin_module_names macros = [] if POSIX: macros.append(("PSUTIL_POSIX", 1)) @@ -68,11 +69,15 @@ extras_require = {"test": [ + "enum34; python_version <= '3.4'", "ipaddress; python_version < '3.0'", "mock; python_version < '3.0'", - "pypiwin32; sys.platform == 'win32'", - "wmi; sys.platform == 'win32'", - "enum34; python_version <= '3.4'"]} + "unittest2; python_version < '3.0'", +]} +if not PYPY: + extras_require['test'].extend([ + "pywin32; sys.platform == 'win32'", + "wmi; sys.platform == 'win32'"]) def get_version(): From 1995bd8a0a15c6bbfccbfcf8c16df89411b1ff49 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 23 May 2020 19:39:07 +0200 Subject: [PATCH 0589/1714] go back to py36 for osx --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e51ecf0309..d49f2cf1f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ matrix: env: PYVER=py27 - language: generic os: osx - env: PYVER=py38 + env: PYVER=py36 # Linux - python: 2.7 - python: 3.5 From bd4d2bf420e1dfa3298143daebd485b97335b256 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 23 May 2020 22:02:10 +0200 Subject: [PATCH 0590/1714] fix conn combos failure of #1762 --- psutil/tests/test_connections.py | 33 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 7fcc2f9826..8a9a6eb407 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -10,7 +10,6 @@ import errno import os import socket -import string import textwrap from contextlib import closing from socket import AF_INET @@ -416,45 +415,45 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): tcp_template = textwrap.dedent(""" import socket, time - s = socket.socket($family, socket.SOCK_STREAM) - s.bind(('$addr', 0)) + s = socket.socket({family}, socket.SOCK_STREAM) + s.bind(('{addr}', 0)) s.listen(5) - with open('$testfn', 'w') as f: + 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: + 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 = string.Template(tcp_template).substitute( + tcp4_template = tcp_template.format( family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - udp4_template = string.Template(udp_template).substitute( + udp4_template = udp_template.format( family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - tcp6_template = string.Template(tcp_template).substitute( + tcp6_template = tcp_template.format( family=int(AF_INET6), addr="::1", testfn=testfile) - udp6_template = string.Template(udp_template).substitute( + 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)) + tcp4_addr = eval(wait_for_file(testfile, delete=True)) udp4_proc = self.pyrun(udp4_template) - udp4_addr = eval(wait_for_file(testfile)) + udp4_addr = eval(wait_for_file(testfile, delete=True)) if supports_ipv6(): tcp6_proc = self.pyrun(tcp6_template) - tcp6_addr = eval(wait_for_file(testfile)) + tcp6_addr = eval(wait_for_file(testfile, delete=True)) udp6_proc = self.pyrun(udp6_template) - udp6_addr = eval(wait_for_file(testfile)) + udp6_addr = eval(wait_for_file(testfile, delete=True)) else: tcp6_proc = None udp6_proc = None From c74ece234cf810796aa096e0880646879c758ae9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 May 2020 00:15:41 +0200 Subject: [PATCH 0591/1714] [macOS] Fix zombie leak detection on (#1766) --- .ci/travis/install.sh | 12 ++++++++++-- HISTORY.rst | 1 + psutil/_psbsd.py | 12 ++++++------ psutil/_psosx.py | 13 ++++++++++++- psutil/tests/__init__.py | 3 +++ psutil/tests/test_connections.py | 3 +++ psutil/tests/test_contracts.py | 3 +++ 7 files changed, 38 insertions(+), 9 deletions(-) diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index f06e43d575..16bd5c0c70 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -24,13 +24,21 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then pyenv install 3.6.6 pyenv virtualenv 3.6.6 psutil ;; + py37) + pyenv install 3.7.6 + pyenv virtualenv 3.7.6 psutil + ;; + py38) + pyenv install 3.8.2 + pyenv virtualenv 3.8.2 psutil + ;; esac pyenv rehash pyenv activate psutil fi if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] || [[ $PYVER == 'py27' ]]; then - pip install -U ipaddress mock + pip install -U ipaddress mock unittest2 fi -pip install -U coverage coveralls flake8 setuptools concurrencytest +pip install -U coverage coveralls flake8 setuptools diff --git a/HISTORY.rst b/HISTORY.rst index 3bae76b4ab..3cb021a188 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -31,6 +31,7 @@ XXXX-XX-XX - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. (patch by Michał Górny) - 1760_: [Linux] Process.rlimit() does not handle long long type properly. +- 1766_: [macOS] NoSuchProcess may be raised instead of ZombieProcess. 5.7.0 ===== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 49ad1e995c..d53eb0427a 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -551,10 +551,10 @@ def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except ProcessLookupError: - 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) + else: + raise NoSuchProcess(self.pid, self._name) except PermissionError: raise AccessDenied(self.pid, self._name) except OSError: @@ -576,10 +576,10 @@ def wrap_exceptions_procfs(inst): # 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(inst.pid): - raise NoSuchProcess(inst.pid, inst._name) - else: + 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) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index e4296495c4..2feff93233 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -324,6 +324,14 @@ 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 Exception: + return False + + def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. @@ -333,7 +341,10 @@ def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except ProcessLookupError: - raise NoSuchProcess(self.pid, self._name) + 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 cext.ZombieProcessError: diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 6a119bf56b..aac7614f96 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -499,6 +499,9 @@ def wait(proc, timeout): pass def sendsig(proc, sig): + # XXX: otherwise the build hangs for some reason. + if MACOS and GITHUB_WHEELS: + sig = signal.SIGKILL # If the process received SIGSTOP, SIGCONT is necessary first, # otherwise SIGTERM won't work. if POSIX and sig != signal.SIGKILL: diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 8a9a6eb407..1a9b32f70a 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -38,6 +38,7 @@ from psutil.tests import get_free_port from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import PsutilTestCase +from psutil.tests import reap_children from psutil.tests import serialrun from psutil.tests import skip_on_access_denied from psutil.tests import SKIP_SYSCONS @@ -392,6 +393,8 @@ def check(kind, families, types): @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") diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 35ab61e01c..51bbb9f097 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -34,6 +34,7 @@ from psutil._compat import range from psutil.tests import create_sockets from psutil.tests import enum +from psutil.tests import GITHUB_WHEELS from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import HAS_SENSORS_FANS @@ -85,6 +86,7 @@ def test_linux_ioprio_windows(self): ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) + @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") def test_linux_rlimit(self): ae = self.assertEqual ae(hasattr(psutil, "RLIM_INFINITY"), LINUX) @@ -149,6 +151,7 @@ def test_terminal(self): def test_ionice(self): self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) + @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") def test_rlimit(self): # requires Linux 2.6.36 self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX) From 3140299f045d64c71af2aa9de439078071ef6043 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 25 May 2020 01:56:06 +0200 Subject: [PATCH 0592/1714] Build wheel2 (#1762) --- .github/workflows/build_wheel.yml | 43 +++---------------------------- psutil/tests/test_connections.py | 2 ++ psutil/tests/test_windows.py | 1 + 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 7d230b900e..30e0b89489 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -3,55 +3,18 @@ name: Build wheel on: [push, pull_request] jobs: - wheel_without_test: - name: build wheel for ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-latest, macos-latest, ubuntu-latest] - env: - CIBW_SKIP: "pp27-*win* cp27-*manylinux* pp-*manylinux*" - CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 - CIBW_MANYLINUX_I686_IMAGE: manylinux2014 - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 - name: Install Python 3.7 - with: - python-version: '3.7' - - - name: Install Visual C++ for Python 2.7 - if: startsWith(matrix.os, 'windows') - run: | - choco install vcpython27 -f -y - - - name: "install cibuildwheel" - run: pip install cibuildwheel==1.4.1 - - - name: build wheel - run: cibuildwheel . - - - name: Upload wheels - uses: actions/upload-artifact@v1 - with: - name: wheels2 - path: wheelhouse - wheel: - name: build wheel for ${{ matrix.os }} + name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] env: - CIBW_SKIP: "pp27-*win* *27* cp27-*manylinux* pp-*manylinux*" - CIBW_TEST_COMMAND: python -Wa {project}/psutil/tests/runner.py + CIBW_SKIP: "pp*win32" + CIBW_TEST_COMMAND: python -u -Wa {project}/psutil/tests/runner.py CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py CIBW_TEST_EXTRAS: test - CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 - CIBW_MANYLINUX_I686_IMAGE: manylinux2014 steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 1a9b32f70a..d1425bcb1f 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -39,6 +39,7 @@ from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import PsutilTestCase 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 SKIP_SYSCONS @@ -572,6 +573,7 @@ def check(cons, families, types_): # See: https://travis-ci.org/giampaolo/psutil/jobs/237566297 @unittest.skipIf(MACOS and TRAVIS, "unreliable on MACOS + TRAVIS") + @retry_on_failure() def test_multi_sockets_procs(self): # Creates multiple sub processes, each creating different # sockets. For each process check that proc.connections() diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 945bb2eda1..580e1e5e09 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -511,6 +511,7 @@ def test_name(self): p = psutil.Process(self.pid) self.assertEqual(p.name(), w.Caption) + # This fail on github because using virtualenv for test environment @unittest.skipIf(GITHUB_WHEELS, "unreliable path on GITHUB_WHEELS") def test_exe(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] From 23662e6e5b9476fa33285729494657fb3ed18a80 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 25 May 2020 17:07:58 +0200 Subject: [PATCH 0593/1714] Wheels6 (#1767) --- .github/workflows/build_wheel.yml | 9 +- MANIFEST.in | 5 +- Makefile | 34 ++-- scripts/internal/download_wheels.py | 154 ------------------ ..._wheels.py => download_wheels_appveyor.py} | 26 +-- scripts/internal/download_wheels_github.py | 81 +++++++++ scripts/internal/print_wheels.py | 66 ++++++++ 7 files changed, 177 insertions(+), 198 deletions(-) delete mode 100644 scripts/internal/download_wheels.py rename scripts/internal/{win_download_wheels.py => download_wheels_appveyor.py} (88%) create mode 100644 scripts/internal/download_wheels_github.py create mode 100644 scripts/internal/print_wheels.py diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 30e0b89489..ec784ed51e 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -1,4 +1,4 @@ -name: Build wheel +name: Build wheels on: [push, pull_request] @@ -9,9 +9,8 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, macos-latest, ubuntu-latest] + os: [macos-latest, ubuntu-latest] env: - CIBW_SKIP: "pp*win32" CIBW_TEST_COMMAND: python -u -Wa {project}/psutil/tests/runner.py CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py CIBW_TEST_EXTRAS: test @@ -27,10 +26,10 @@ jobs: run: | choco install vcpython27 -f -y - - name: "install cibuildwheel" + - name: Install cibuildwheel run: pip install cibuildwheel==1.4.1 - - name: build wheel + - name: Build wheels run: cibuildwheel . - name: Upload wheels diff --git a/MANIFEST.in b/MANIFEST.in index 93c4918011..d52a691d5e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -117,7 +117,8 @@ include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py include scripts/internal/clinter.py -include scripts/internal/download_wheels.py +include scripts/internal/download_wheels_appveyor.py +include scripts/internal/download_wheels_github.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py @@ -125,9 +126,9 @@ include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py include scripts/internal/print_timeline.py +include scripts/internal/print_wheels.py include scripts/internal/purge_installation.py include scripts/internal/tidelift.py -include scripts/internal/win_download_wheels.py include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py diff --git a/Makefile b/Makefile index e91dfd818a..25f1dc1a2a 100644 --- a/Makefile +++ b/Makefile @@ -207,6 +207,25 @@ install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit chmod +x .git/hooks/pre-commit +# =================================================================== +# Wheels +# =================================================================== + +download-wheels-appveyor: ## Download latest wheels hosted on appveyor. + $(PYTHON) scripts/internal/download_wheels_appveyor.py --user giampaolo --project psutil + +download-wheels-github: ## Download latest wheels hosted on github. + $(PYTHON) scripts/internal/download_wheels_github.py --user=giampaolo --project=psutil --tokenfile=~/.github.token + +download-wheels: ## Download wheels from github and appveyor + rm -rf dist + ${MAKE} download-wheels-appveyor + ${MAKE} download-wheels-github + ${MAKE} print-wheels + +print-wheels: ## Print downloaded wheels + $(PYTHON) scripts/internal/print_wheels.py + # =================================================================== # Distribution # =================================================================== @@ -219,20 +238,11 @@ sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest $(PYTHON) setup.py sdist -wheel: ## Generate wheel. - $(PYTHON) setup.py bdist_wheel - -win-download-wheels: ## Download latest wheels hosted on appveyor. - $(PYTHON) scripts/internal/win_download_wheels.py --user giampaolo --project psutil - -download-wheels: ## Download latest wheels hosted on github. - $(PYTHON) scripts/internal/download_wheels.py --user=giampaolo --project=psutil --tokenfile=~/.github.token - upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist - $(PYTHON) setup.py sdist upload + $(PYTHON) -m twine upload dist/*.tar.gz -upload-win-wheels: ## Upload wheels in dist/* directory on PyPI. +upload-wheels: ## Upload wheels in dist/* directory on PyPI. $(PYTHON) -m twine upload dist/*.whl # --- others @@ -253,7 +263,7 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} install ${MAKE} generate-manifest git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain - ${MAKE} win-download-wheels + ${MAKE} download-wheels ${MAKE} sdist $(PYTHON) -c \ "from psutil import __version__ as ver; \ diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py deleted file mode 100644 index 1834f1b3af..0000000000 --- a/scripts/internal/download_wheels.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/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. - -""" -Script which downloads wheel files hosted on GitHub: -https://github.com/giampaolo/psutil/actions -It needs an access token string generated from personal GitHub profile: -https://github.com/settings/tokens -The token must be created with at least "public_repo" scope/rights. -If you lose it, just generate a new token. -REST API doc: -https://developer.github.com/v3/actions/artifacts/ -""" - -import argparse -import collections -import json -import os -import requests -import shutil -import zipfile - -from psutil import __version__ as PSUTIL_VERSION -from psutil._common import bytes2human -from psutil._common import print_color - - -USER = "" -PROJECT = "" -TOKEN = "" -OUTFILE = "wheels.zip" - - -# --- GitHub API - - -def get_artifacts(): - base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) - url = base_url + "/actions/artifacts" - res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) - res.raise_for_status() - data = json.loads(res.content) - return data - - -def download_zip(url): - print("downloading: " + url) - res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) - res.raise_for_status() - totbytes = 0 - with open(OUTFILE, 'wb') as f: - for chunk in res.iter_content(chunk_size=16384): - f.write(chunk) - totbytes += len(chunk) - print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) - - -# --- extract - - -def rename_27_wheels(): - # See: https://github.com/giampaolo/psutil/issues/810 - src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PSUTIL_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - - -def extract(): - with zipfile.ZipFile(OUTFILE, 'r') as zf: - zf.extractall('dist') - - -def print_wheels(): - def is64bit(name): - return name.endswith(('x86_64.whl', 'amd64.whl')) - - groups = collections.defaultdict(list) - for name in os.listdir('dist'): - plat = name.split('-')[-1] - pyimpl = name.split('-')[3] - ispypy = 'pypy' in pyimpl - if 'linux' in plat: - if ispypy: - groups['pypy_on_linux'].append(name) - else: - groups['linux'].append(name) - elif 'win' in plat: - if ispypy: - groups['pypy_on_windows'].append(name) - else: - groups['windows'].append(name) - elif 'macosx' in plat: - if ispypy: - groups['pypy_on_macos'].append(name) - else: - groups['macos'].append(name) - else: - assert 0, name - - totsize = 0 - templ = "%-54s %7s %7s %7s" - for platf, names in groups.items(): - ppn = "%s (total = %s)" % (platf.replace('_', ' '), len(names)) - s = templ % (ppn, "size", "arch", "pyver") - print_color('\n' + s, color=None, bold=True) - for name in sorted(names): - path = os.path.join('dist', name) - size = os.path.getsize(path) - totsize += size - arch = '64' if is64bit(name) else '32' - pyver = 'pypy' if name.split('-')[3].startswith('pypy') else 'py' - pyver += name.split('-')[2][2:] - s = templ % (name, bytes2human(size), arch, pyver) - if 'pypy' in pyver: - print_color(s, color='violet') - else: - print_color(s, color='brown') - - -def run(): - if os.path.isdir('dist'): - shutil.rmtree('dist') - data = get_artifacts() - download_zip(data['artifacts'][0]['archive_download_url']) - os.mkdir('dist') - extract() - # rename_27_wheels() - print_wheels() - - -def main(): - global USER, PROJECT, TOKEN - parser = argparse.ArgumentParser(description='GitHub wheels downloader') - parser.add_argument('--user', required=True) - parser.add_argument('--project', required=True) - parser.add_argument('--tokenfile', required=True) - args = parser.parse_args() - USER = args.user - PROJECT = args.project - with open(os.path.expanduser(args.tokenfile)) as f: - TOKEN = f.read().strip() - run() - - -if __name__ == '__main__': - main() diff --git a/scripts/internal/win_download_wheels.py b/scripts/internal/download_wheels_appveyor.py similarity index 88% rename from scripts/internal/win_download_wheels.py rename to scripts/internal/download_wheels_appveyor.py index 8dae05730b..b7c0aeae87 100755 --- a/scripts/internal/win_download_wheels.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -15,10 +15,8 @@ from __future__ import print_function import argparse import concurrent.futures -import errno import os import requests -import shutil import sys from psutil import __version__ as PSUTIL_VERSION @@ -29,33 +27,12 @@ BASE_URL = 'https://ci.appveyor.com/api' PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7', '3.8'] TIMEOUT = 30 -COLORS = True - - -def safe_makedirs(path): - try: - os.makedirs(path) - except OSError as err: - if err.errno == errno.EEXIST: - if not os.path.isdir(path): - raise - else: - raise - - -def safe_rmtree(path): - def onerror(fun, path, excinfo): - exc = excinfo[1] - if exc.errno != errno.ENOENT: - raise - - shutil.rmtree(path, onerror=onerror) def download_file(url): local_fname = url.split('/')[-1] local_fname = os.path.join('dist', local_fname) - safe_makedirs('dist') + os.makedirs('dist', exist_ok=True) r = requests.get(url, stream=True, timeout=TIMEOUT) tot_bytes = 0 with open(local_fname, 'wb') as f: @@ -102,7 +79,6 @@ def rename_27_wheels(): def run(options): - safe_rmtree('dist') urls = get_file_urls(options) completed = 0 exc = None diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py new file mode 100644 index 0000000000..4aa50d5df1 --- /dev/null +++ b/scripts/internal/download_wheels_github.py @@ -0,0 +1,81 @@ +#!/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. + +""" +Script which downloads wheel files hosted on GitHub: +https://github.com/giampaolo/psutil/actions +It needs an access token string generated from personal GitHub profile: +https://github.com/settings/tokens +The token must be created with at least "public_repo" scope/rights. +If you lose it, just generate a new token. +REST API doc: +https://developer.github.com/v3/actions/artifacts/ +""" + +import argparse +import json +import os +import requests +import zipfile + +from psutil._common import bytes2human +from psutil.tests import safe_rmpath + + +USER = "" +PROJECT = "" +TOKEN = "" +OUTFILE = "wheels-github.zip" + + +def get_artifacts(): + base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) + url = base_url + "/actions/artifacts" + res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res.raise_for_status() + data = json.loads(res.content) + return data + + +def download_zip(url): + print("downloading: " + url) + res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res.raise_for_status() + totbytes = 0 + with open(OUTFILE, 'wb') as f: + for chunk in res.iter_content(chunk_size=16384): + f.write(chunk) + totbytes += len(chunk) + print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) + + +def run(): + data = get_artifacts() + download_zip(data['artifacts'][0]['archive_download_url']) + os.makedirs('dist', exist_ok=True) + with zipfile.ZipFile(OUTFILE, 'r') as zf: + zf.extractall('dist') + + +def main(): + global USER, PROJECT, TOKEN + parser = argparse.ArgumentParser(description='GitHub wheels downloader') + parser.add_argument('--user', required=True) + parser.add_argument('--project', required=True) + parser.add_argument('--tokenfile', required=True) + args = parser.parse_args() + USER = args.user + PROJECT = args.project + with open(os.path.expanduser(args.tokenfile)) as f: + TOKEN = f.read().strip() + try: + run() + finally: + safe_rmpath(OUTFILE) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_wheels.py new file mode 100644 index 0000000000..be8290e063 --- /dev/null +++ b/scripts/internal/print_wheels.py @@ -0,0 +1,66 @@ +#!/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. + +"""Nicely print wheels print in dist/ directory.""" + +import collections +import glob +import os + +from psutil._common import print_color +from psutil._common import bytes2human + + +def main(): + def is64bit(name): + return name.endswith(('x86_64.whl', 'amd64.whl')) + + groups = collections.defaultdict(list) + for path in glob.glob('dist/*.whl'): + name = os.path.basename(path) + plat = name.split('-')[-1] + pyimpl = name.split('-')[3] + ispypy = 'pypy' in pyimpl + if 'linux' in plat: + if ispypy: + groups['pypy_on_linux'].append(name) + else: + groups['linux'].append(name) + elif 'win' in plat: + if ispypy: + groups['pypy_on_windows'].append(name) + else: + groups['windows'].append(name) + elif 'macosx' in plat: + if ispypy: + groups['pypy_on_macos'].append(name) + else: + groups['macos'].append(name) + else: + assert 0, name + + totsize = 0 + templ = "%-54s %7s %7s %7s" + for platf, names in groups.items(): + ppn = "%s (total = %s)" % (platf.replace('_', ' '), len(names)) + s = templ % (ppn, "size", "arch", "pyver") + print_color('\n' + s, color=None, bold=True) + for name in sorted(names): + path = os.path.join('dist', name) + size = os.path.getsize(path) + totsize += size + arch = '64' if is64bit(name) else '32' + pyver = 'pypy' if name.split('-')[3].startswith('pypy') else 'py' + pyver += name.split('-')[2][2:] + s = templ % (name, bytes2human(size), arch, pyver) + if 'pypy' in pyver: + print_color(s, color='violet') + else: + print_color(s, color='brown') + + +if __name__ == '__main__': + main() From 3c209c19acbfe91f237fae18a57156fafef3d2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 26 May 2020 13:21:31 +0200 Subject: [PATCH 0594/1714] Sort results in test_cpu_affinity_all_combinations (#1769) Fix test_cpu_affinity_all_combinations to permit any CPU order in results. This fixes test failure due to affinity being reported out of order: ====================================================================== FAIL: psutil.tests.test_process.TestProcess.test_cpu_affinity_all_combinations ---------------------------------------------------------------------- Traceback (most recent call last): File "/tmp/psutil/psutil/tests/test_process.py", line 940, in test_cpu_affinity_all_combinations self.assertEqual(p.cpu_affinity(), combo) AssertionError: Lists differ: [8, 1] != [1, 8] First differing element 0: 8 1 - [8, 1] + [1, 8] ---------------------------------------------------------------------- --- psutil/tests/test_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a0b21c6e55..e3394799c6 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -937,7 +937,7 @@ def test_cpu_affinity_all_combinations(self): for combo in combos: p.cpu_affinity(combo) - self.assertEqual(p.cpu_affinity(), combo) + self.assertEqual(sorted(p.cpu_affinity()), sorted(combo)) # TODO: #595 @unittest.skipIf(BSD, "broken on BSD") From 47ad3c71b9148dea61a189bbc913c50ee04b8793 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 10 Jun 2020 13:48:17 +0200 Subject: [PATCH 0595/1714] update doc --- docs/index.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 699ea1f161..1283ba88f2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -268,16 +268,17 @@ CPU .. function:: getloadavg() Return the average system load over the last 1, 5 and 15 minutes as a tuple. - The load represents the processes which are in a runnable state, either + The "load" represents the processes which are in a runnable state, either using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). On UNIX systems this relies on `os.getloadavg`_. On Windows this is emulated by using a Windows API that spawns a thread which keeps running in - background and updates the load average every 5 seconds, mimicking the UNIX - behavior. Thus, the first time this is called and for the next 5 seconds + background and updates results every 5 seconds, mimicking the UNIX behavior. + Thus, on Windows, the first time this is called and for the next 5 seconds it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. The numbers returned only make sense if related to the number of CPU cores - installed on the system. So, for instance, `3.14` on a system with 10 CPU - cores means that the system load was 31.4% percent over the last N minutes. + installed on the system. So, for instance, a value of `3.14` on a system + with 10 logical CPUs means that the system load was 31.4% percent over the + last N minutes. .. code-block:: python From db3fe97c13aa7308a9cab26fdcbdbf1993cc547a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 13 Jun 2020 19:36:36 +0200 Subject: [PATCH 0596/1714] add paypal donation link, remove images from README --- .github/FUNDING.yml | 2 +- README.rst | 24 ++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index dd130351d7..f1898d98c6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -6,4 +6,4 @@ patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -custom: # Replace with a single custom sponsorship URL +custom: https://www.paypal.me/gmpydev diff --git a/README.rst b/README.rst index 2137a1a281..bb2c927b46 100644 --- a/README.rst +++ b/README.rst @@ -100,7 +100,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy3 `__ is also known to work. +...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**, `PyPy `__ 2.7 and 3.X. psutil for enterprise ===================== @@ -131,19 +131,11 @@ Security To report a security vulnerability, please use the `Tidelift security contact`_. Tidelift will coordinate the fix and disclosure. -Example applications -==================== +Donate +====== -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ -| .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/procinfo-small.png | .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/top-small.png | -| :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/procinfo.png | :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/top.png | -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ -| .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/procsmem-small.png | .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/pmap-small.png | -| :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/procsmem.png | :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/pmap.png | -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ - -Also see `scripts directory `__ -and `doc recipes `__. +A lot of time and effort went into making psutil as it is today. If you whish +to help its future development consider making a `donation `__. Projects using psutil ===================== @@ -154,9 +146,7 @@ psutil has roughly the following monthly downloads: :target: https://pepy.tech/project/psutil :alt: Downloads -There are over -`10.000 open source projects `__ -on github which depend from psutil. +...and has over 45,000 projects on GitHub depending from it. Here's some I find particularly interesting: - https://github.com/google/grr @@ -166,7 +156,6 @@ Here's some I find particularly interesting: - https://github.com/ajenti/ajenti - https://github.com/home-assistant/home-assistant/ - Portings ======== @@ -175,7 +164,6 @@ Portings - Rust: https://github.com/borntyping/rust-psutil - Nim: https://github.com/johnscillieri/psutil-nim - Example usages ============== From ea8e08a6693a6fa0794b5fa33fc7cc007605d0f5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 14 Jun 2020 20:20:23 +0200 Subject: [PATCH 0597/1714] add SUPPORTERS --- MANIFEST.in | 1 + SUPPORTERS.rst | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 SUPPORTERS.rst diff --git a/MANIFEST.in b/MANIFEST.in index d52a691d5e..32ea9d3eac 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,6 +9,7 @@ include LICENSE include MANIFEST.in include Makefile include README.rst +include SUPPORTERS.rst include docs/DEVGUIDE.rst include docs/DEVNOTES include docs/Makefile diff --git a/SUPPORTERS.rst b/SUPPORTERS.rst new file mode 100644 index 0000000000..e2401c7673 --- /dev/null +++ b/SUPPORTERS.rst @@ -0,0 +1,72 @@ +Supporters +========== + +Recurring donations +------------------- + +None. + +One-time donations +------------------ + ++------------------------+-------------------------+--------+ +| Name | Country | Amount | ++========================+=========================+========+ +| Kim Gräsman | Malmo, Sweden | 100€ | ++------------------------+-------------------------+--------+ +| Rodion Stratov | Canada | 77€ | ++------------------------+-------------------------+--------+ +| Remi Chateauneu | London, UK | 45€ | ++------------------------+-------------------------+--------+ +| Olivier Grisel | Paris, France | 40€ | ++------------------------+-------------------------+--------+ +| Praveen Bhamidipati | Bellevue, USA | 20€ | ++------------------------+-------------------------+--------+ +| Willem de Groot | Netherlands | 20€ | ++------------------------+-------------------------+--------+ +| Sigmund Vik | | 20€ | ++------------------------+-------------------------+--------+ +| Kahntent | NYC, USA | 20€ | ++------------------------+-------------------------+--------+ +| Gyula Áfra | Budapest, Hungary | 20€ | ++------------------------+-------------------------+--------+ +| Mahmut Dumlupinar | | 20€ | ++------------------------+-------------------------+--------+ +| Thomas Guettler | Germany | 12€ | ++------------------------+-------------------------+--------+ +| Karthik Kumar | India | 10€ | ++------------------------+-------------------------+--------+ +| Oche Ejembi | UK | 10€ | ++------------------------+-------------------------+--------+ +| Russell Robinson | New Zealand | 10€ | ++------------------------+-------------------------+--------+ +| Wompasoft | Texas, USA | 10€ | ++------------------------+-------------------------+--------+ +| Amit Kulkarni | Santa Clara, USA | 10€ | ++------------------------+-------------------------+--------+ +| Alexander Kaftan | Augsburg Germany | 10€ | ++------------------------+-------------------------+--------+ +| Andrew Bays | Maynard, USA | 10€ | ++------------------------+-------------------------+--------+ +| Carver Koella | Pittsburgh, USA | 10€ | ++------------------------+-------------------------+--------+ +| Brett Harris | Melbourne, Australia | 5€ | ++------------------------+-------------------------+--------+ +| Peter Friedland | CT, USA | 5€ | ++------------------------+-------------------------+--------+ +| Matthew Callow | Australia | 5€ | ++------------------------+-------------------------+--------+ +| Marco Schrank | Germany | 5€ | ++------------------------+-------------------------+--------+ +| Mindview LLC | USA | 5€ | ++------------------------+-------------------------+--------+ +| Григорьев Андрей | Russia | 5€ | ++------------------------+-------------------------+--------+ +| Heijdemann Morgan | Singapore | 5€ | ++------------------------+-------------------------+--------+ +| Florian Bruhin | Winterthur, Switzerland | 5€ | ++------------------------+-------------------------+--------+ +| Heijdemann Morgan | Singapore | 5€ | ++------------------------+-------------------------+--------+ +| Morgan Heijdemann | Singapore | 5€ | ++------------------------+-------------------------+--------+ From 2a44280b2e4a6bcb3ccc0b622acea3305807e175 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 16 Jun 2020 18:29:16 +0200 Subject: [PATCH 0598/1714] update personal site URL --- .github/FUNDING.yml | 4 ++-- CREDITS | 2 +- README.rst | 6 +++--- docs/index.rst | 6 +++--- psutil/tests/__init__.py | 5 ++--- scripts/internal/print_announce.py | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f1898d98c6..03c7c77c17 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,7 +3,7 @@ tidelift: "pypi/psutil" github: giampaolo patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username +open_collective: psutil ko_fi: # Replace with a single Ko-fi username community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -custom: https://www.paypal.me/gmpydev +custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 diff --git a/CREDITS b/CREDITS index 690f5717fd..672f5bbbbe 100644 --- a/CREDITS +++ b/CREDITS @@ -16,7 +16,7 @@ Author N: Giampaolo Rodola C: Italy E: g.rodola@gmail.com -W: http://grodola.blogspot.com/ +W: https://gmpy.dev/about Experts ======= diff --git a/README.rst b/README.rst index bb2c927b46..39822e2961 100644 --- a/README.rst +++ b/README.rst @@ -77,7 +77,7 @@ Quick links - `Download `_ - `Forum `_ - `StackOverflow `_ -- `Blog `_ +- `Blog `_ - `Development guide `_ - `What's new `_ @@ -135,7 +135,7 @@ Donate ====== A lot of time and effort went into making psutil as it is today. If you whish -to help its future development consider making a `donation `__. +to help its future development consider making me a `donation `__. Projects using psutil ===================== @@ -502,7 +502,7 @@ Windows services 'username': 'NT AUTHORITY\\LocalService'} -.. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html +.. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 .. _Tidelift security contact: https://tidelift.com/security .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme diff --git a/docs/index.rst b/docs/index.rst index 1283ba88f2..47d88da971 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ Quick links - `Home page `__ - `Install `_ -- `Blog `__ +- `Blog `__ - `Forum `__ - `Download `__ - `Development guide `_ @@ -1627,7 +1627,7 @@ Process class (USS, PSS and swap). The additional metrics provide a better representation of "effective" process memory consumption (in case of USS) as explained in detail in this - `blog post `__. + `blog post `__. It does so by passing through the whole process address. As such it usually requires higher user privileges than :meth:`memory_info` and is considerably slower. @@ -2839,7 +2839,7 @@ Timeline .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ .. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass .. _`GetExitCodeProcess`: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess -.. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html +.. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`hash`: https://docs.python.org/3/library/functions.html#hash .. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index aac7614f96..5bad53cf3e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -925,7 +925,7 @@ class TestMemoryLeak(PsutilTestCase): If available (Linux, OSX, Windows), USS memory is used for comparison, since it's supposed to be more precise, see: - http://grodola.blogspot.com/2016/02/psutil-4-real-process-memory-and-environ.html + 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. @@ -1635,7 +1635,6 @@ def cleanup_test_procs(): # 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: -# http://grodola.blogspot.com/ -# 2016/02/how-to-always-execute-exit-functions-in-py.html +# https://gmpy.dev/blog/2016/how-to-always-execute-exit-functions-in-python if POSIX: signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit(sig)) diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 9569c3674d..180bf37760 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -58,7 +58,7 @@ -- -Giampaolo - http://grodola.blogspot.com +Giampaolo - https://gmpy.dev/about """ From cbd61676b3818aeeebfb5b71309e73a8717ede6f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 20 Jun 2020 17:53:14 +0200 Subject: [PATCH 0599/1714] fix PYTHONWARNINGS --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 25f1dc1a2a..be92b74429 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ BUILD_OPTS = `$(PYTHON) -c \ # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` -TEST_PREFIX = PYTHONWARNINGS=all PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_TESTING=1 PSUTIL_DEBUG=1 all: test From 4c8874719d912cd93b124bd2c5c939537df78103 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 20 Jun 2020 22:17:13 +0200 Subject: [PATCH 0600/1714] move SUPPORTERS into CREDITS; also remove people's emails --- CREDITS | 166 +++++++++++++++++++++++++++++-------------------- MANIFEST.in | 1 - SUPPORTERS.rst | 72 --------------------- 3 files changed, 99 insertions(+), 140 deletions(-) delete mode 100644 SUPPORTERS.rst diff --git a/CREDITS b/CREDITS index 672f5bbbbe..1c944d9567 100644 --- a/CREDITS +++ b/CREDITS @@ -1,25 +1,26 @@ Intro -===== +------------------------------------------------------------------------------- I would like to recognize some of the people who have been instrumental in the development of psutil. I'm sure I'm forgetting somebody (feel free to email me) but here is a short list. It's modeled after the Linux CREDITS file where the fields are: name (N), e-mail (E), website (W), country (C), description (D), -(I) issues. Issue tracker is at https://github.com/giampaolo/psutil/issues). +(I) issues. Issue tracker is at: +https://github.com/giampaolo/psutil/issues. A big thanks to all of you. - Giampaolo Author -====== +------------------------------------------------------------------------------- N: Giampaolo Rodola C: Italy E: g.rodola@gmail.com -W: https://gmpy.dev/about +W: https://gmpy.dev Experts -======= +------------------------------------------------------------------------------- Github usernames of people to CC on github when in need of help. @@ -45,7 +46,7 @@ Github usernames of people to CC on github when in need of help. - wiggin15, Arnon Yaari (maintainer) Top contributors -================ +------------------------------------------------------------------------------- N: Jay Loden C: NJ, USA @@ -69,7 +70,6 @@ W: https://github.com/mrjefftang I: 340, 529, 616, 653, 654, 648, 641 N: Jeremy Whitlock -E: jcscoobyrs@gmail.com D: great help with macOS C development. I: 125, 150, 174, 206 @@ -79,7 +79,6 @@ D: OpenBSD implementation. I: 615 N: Justin Venus -E: justin.venus@gmail.com D: Solaris support I: 18 @@ -93,28 +92,115 @@ W: https://github.com/ryoon D: NetBSD implementation (co-author). I: 557 +Donations +------------------------------------------------------------------------------- + +N: Rodion Stratov +C: Canada + +N: Remi Chateauneu +C: London, UK + +N: Olivier Grisel +C: Paris, France + +N: Praveen Bhamidipati +C: Bellevue, USA + +N: Willem de Groot +C: Netherlands + +N: Sigmund Vik + +N: Kahntent +C: NYC, USA + +N: Gyula Áfra +C: Budapest, Hungary + +N: Mahmut Dumlupinar + +N: Thomas Guettler +C: Germany + +N: Karthik Kumar +C: India + +N: Oche Ejembi +C: UK + +N: Russell Robinson +C: New Zealand + +N: Wompasoft +C: Texas, USA + +N: Amit Kulkarni +C: Santa Clara, USA + +N: Alexander Kaftan +C: Augsburg Germany + +N: Andrew Bays +C: Maynard, USA + +N: Carver Koella +C: Pittsburgh, USA + +N: Kristjan Võrk +C: Tallin, Estonia + +N: HTB Industries +C: Willow Springs, USA + +N: Brett Harris +C: Melbourne, Australia + +N: Peter Friedland +C: CT, USA + +N: Matthew Callow +C: Australia + +N: Marco Schrank +C: Germany + +N: Mindview LLC +C: USA + +N: Григорьев Андрей +C: Russia + +N: Heijdemann Morgan +C: Singapore + +N: Florian Bruhin +C: Winterthur, Switzerland + +N: Heijdemann Morgan +C: Singapore + +N: Morgan Heijdemann +C: Singapore + Contributors -============ +------------------------------------------------------------------------------- N: wj32 -E: wj32.64@gmail.com D: process username() and get_connections() on Windows I: 114, 115 N: Yan Raber C: Bologna, Italy -E: yanraber@gmail.com D: help on Windows development (initial version of Process.username()) N: Dave Daeschler C: USA -E: david.daeschler@gmail.com W: http://daviddaeschler.com D: some contributions to initial design/bootstrap plus occasional bug fixing I: 522, 536 N: cjgohlke -E: cjgohlke@gmail.com D: Windows 64 bit support I: 107 @@ -133,64 +219,49 @@ I: 1368, 1348 ---- N: Jeffery Kline -E: jeffery.kline@gmail.com I: 130 N: Grabriel Monnerat -E: gabrielmonnerat@gmail.com I: 146 N: Philip Roberts -E: philip.roberts@gmail.com I: 168 N: jcscoobyrs -E: jcscoobyrs@gmail.com I: 125 N: Sandro Tosi -E: sandro.tosi@gmail.com I: 200, 201 N: Andrew Colin -E: andrew.colin@gmail.com I: 248 N: Amoser -E: amoser@google.com I: 266, 267, 340 N: Matthew Grant -E: matthewgrant5@gmail.com I: 271 N: oweidner -E: oweidner@cct.lsu.edu I: 275 N: Tarek Ziade -E: ziade.tarek I: 281 N: Luca Cipriani C: Turin, Italy -E: luca.opensource@gmail.com I: 278 N: Maciej Lach, -E: maciej.lach@gmail.com I: 294 N: James Pye -E: james.pye@gmail.com I: 305, 306 N: Stanchev Emil -E: stanchev.emil I: 314 N: Kim Gräsman -E: kim.grasman@gmail.com D: ...also kindly donated some money. I: 316 @@ -199,15 +270,12 @@ C: Italy I: 318 N: Florent Xicluna -E: florent.xicluna@gmail.com I: 319 N: Michal Spondr -E: michal.spondr I: 313 N: Jean Sebastien -E: dumbboules@gmail.com I: 344 N: Rob Smith @@ -223,50 +291,39 @@ W: https://plus.google.com/116873264322260110710/posts I: 323 N: André Oriani -E: aoriani@gmail.com I: 361 N: clackwell -E: clackwell@gmail.com I: 356 N: m.malycha -E: m.malycha@gmail.com I: 351 N: John Baldwin -E: jhb@FreeBSD.org I: 370 N: Jan Beich -E: jbeich@tormail.org I: 325 N: floppymaster -E: floppymaster@gmail.com I: 380 N: Arfrever.FTA -E: Arfrever.FTA@gmail.com I: 369, 404 N: danudey -E: danudey@gmail.com I: 386 N: Adrien Fallou I: 224 N: Gisle Vanem -E: gisle.vanem@gmail.com I: 411 N: thepyr0 -E: thepyr0@gmail.com I: 414 N: John Pankov -E: john.pankov@gmail.com I: 435 N: Matt Good @@ -274,11 +331,9 @@ W: http://matt-good.net/ I: 438 N: Ulrich Klank -E: ulrich.klank@scitics.de I: 448 N: Josiah Carlson -E: josiah.carlson@gmail.com I: 451, 452 N: Raymond Hettinger @@ -291,41 +346,31 @@ M: Ken Seeho D: @cached_property decorator N: crusaderky -E: crusaderky@gmail.com I: 470, 477 -E: alex@mroja.net I: 471 N: Gautam Singh -E: gautam.singh@gmail.com I: 466 -E: lhn@hupfeldtit.dk I: 476, 479 N: Francois Charron -E: francois.charron.1@gmail.com I: 474 N: Naveed Roudsari -E: naveed.roudsari@gmail.com I: 421 N: Alexander Grothe -E: Alexander.Grothe@gmail.com I: 497 N: Szigeti Gabor Niif -E: szigeti.gabor.niif@gmail.com I: 446 N: msabramo -E: msabramo@gmail.com I: 492 N: Yaolong Huang -E: airekans@gmail.com W: http://airekans.github.io/ I: 530 @@ -335,18 +380,15 @@ I: 496 N: spacewander W: https://github.com/spacewander -E: spacewanderlzx@gmail.com I: 561, 603 N: Sylvain Mouquet -E: sylvain.mouquet@gmail.com I: 565 N: karthikrev I: 568 N: Bruno Binet -E: bruno.binet@gmail.com I: 572 N: Gabi Davar @@ -356,7 +398,6 @@ I: 578, 581, 587 N: spacewanderlzx C: Guangzhou,China -E: spacewanderlzx@gmail.com I: 555 N: Fabian Groffen @@ -373,8 +414,6 @@ C: Irvine, CA, US I: 614 N: Árni Már Jónsson -E: Reykjavik, Iceland -E: https://github.com/arnimarj I: 634 N: Bart van Kleef @@ -406,12 +445,10 @@ W: https://github.com/syohex I: 730 N: Visa Hankala -E: visa@openbsd.org I: 741 N: Sebastian-Gabriel Brestin C: Romania -E: sebastianbrestin@gmail.com I: 704 N: Timmy Konick @@ -427,11 +464,9 @@ W: https://github.com/wxwright I: 776 N: Farhan Khan -E: khanzf@gmail.com I: 823 N: Jake Omann -E: https://github.com/jomann09 I: 816, 775 N: Jeremy Humble @@ -448,7 +483,6 @@ I: 798 N: Andre Caron C: Montreal, QC, Canada -E: andre.l.caron@gmail.com W: https://github.com/AndreLouisCaron I: 880 @@ -466,7 +500,6 @@ I: 936, 1133 N: Pierre Fersing C: France -E: pierre.fersing@bleemeo.com I: 950 N: Thiago Borges Abdnur @@ -609,7 +642,6 @@ W: https://github.com/samertm I: 1480 N: Ammar Askar -E: ammar@ammaraskar.com W: http://ammaraskar.com/ I: 604, 1484 diff --git a/MANIFEST.in b/MANIFEST.in index 32ea9d3eac..d52a691d5e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,7 +9,6 @@ include LICENSE include MANIFEST.in include Makefile include README.rst -include SUPPORTERS.rst include docs/DEVGUIDE.rst include docs/DEVNOTES include docs/Makefile diff --git a/SUPPORTERS.rst b/SUPPORTERS.rst deleted file mode 100644 index e2401c7673..0000000000 --- a/SUPPORTERS.rst +++ /dev/null @@ -1,72 +0,0 @@ -Supporters -========== - -Recurring donations -------------------- - -None. - -One-time donations ------------------- - -+------------------------+-------------------------+--------+ -| Name | Country | Amount | -+========================+=========================+========+ -| Kim Gräsman | Malmo, Sweden | 100€ | -+------------------------+-------------------------+--------+ -| Rodion Stratov | Canada | 77€ | -+------------------------+-------------------------+--------+ -| Remi Chateauneu | London, UK | 45€ | -+------------------------+-------------------------+--------+ -| Olivier Grisel | Paris, France | 40€ | -+------------------------+-------------------------+--------+ -| Praveen Bhamidipati | Bellevue, USA | 20€ | -+------------------------+-------------------------+--------+ -| Willem de Groot | Netherlands | 20€ | -+------------------------+-------------------------+--------+ -| Sigmund Vik | | 20€ | -+------------------------+-------------------------+--------+ -| Kahntent | NYC, USA | 20€ | -+------------------------+-------------------------+--------+ -| Gyula Áfra | Budapest, Hungary | 20€ | -+------------------------+-------------------------+--------+ -| Mahmut Dumlupinar | | 20€ | -+------------------------+-------------------------+--------+ -| Thomas Guettler | Germany | 12€ | -+------------------------+-------------------------+--------+ -| Karthik Kumar | India | 10€ | -+------------------------+-------------------------+--------+ -| Oche Ejembi | UK | 10€ | -+------------------------+-------------------------+--------+ -| Russell Robinson | New Zealand | 10€ | -+------------------------+-------------------------+--------+ -| Wompasoft | Texas, USA | 10€ | -+------------------------+-------------------------+--------+ -| Amit Kulkarni | Santa Clara, USA | 10€ | -+------------------------+-------------------------+--------+ -| Alexander Kaftan | Augsburg Germany | 10€ | -+------------------------+-------------------------+--------+ -| Andrew Bays | Maynard, USA | 10€ | -+------------------------+-------------------------+--------+ -| Carver Koella | Pittsburgh, USA | 10€ | -+------------------------+-------------------------+--------+ -| Brett Harris | Melbourne, Australia | 5€ | -+------------------------+-------------------------+--------+ -| Peter Friedland | CT, USA | 5€ | -+------------------------+-------------------------+--------+ -| Matthew Callow | Australia | 5€ | -+------------------------+-------------------------+--------+ -| Marco Schrank | Germany | 5€ | -+------------------------+-------------------------+--------+ -| Mindview LLC | USA | 5€ | -+------------------------+-------------------------+--------+ -| Григорьев Андрей | Russia | 5€ | -+------------------------+-------------------------+--------+ -| Heijdemann Morgan | Singapore | 5€ | -+------------------------+-------------------------+--------+ -| Florian Bruhin | Winterthur, Switzerland | 5€ | -+------------------------+-------------------------+--------+ -| Heijdemann Morgan | Singapore | 5€ | -+------------------------+-------------------------+--------+ -| Morgan Heijdemann | Singapore | 5€ | -+------------------------+-------------------------+--------+ From 04b40609276c15b59df065659a0a8ce41b316cef Mon Sep 17 00:00:00 2001 From: Ammar Askar Date: Sat, 4 Jul 2020 18:53:44 -0700 Subject: [PATCH 0601/1714] Fix signature of callback function for load avg (#1781) It looks like `WAITORTIMERCALLBACK` should have two arguments, the void* as well as a boolean representing whether the wait time expired: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms687066(v=vs.85) We were previously missing the boolean which might lead to a crash on 32-bit systems, but even if not this should be corrected. --- psutil/arch/windows/wmi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 42a70df766..0a1fb8913d 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -30,7 +30,7 @@ double load_avg_5m = 0; double load_avg_15m = 0; -VOID CALLBACK LoadAvgCallback(PVOID hCounter) { +VOID CALLBACK LoadAvgCallback(PVOID hCounter, BOOLEAN timedOut) { PDH_FMT_COUNTERVALUE displayValue; double currentLoad; PDH_STATUS err; From 54de8823918fc0484223a077eafc917755334b5b Mon Sep 17 00:00:00 2001 From: Julien Lebot Date: Sun, 5 Jul 2020 04:02:03 +0200 Subject: [PATCH 0602/1714] Add support for Windows Nano Server (#1768) --- psutil/_psutil_common.c | 11 +-- psutil/_psutil_windows.c | 66 ++++++++++------- psutil/arch/windows/ntextapi.h | 126 ++++++++++++++++++++++++++++++--- setup.py | 2 +- 4 files changed, 164 insertions(+), 41 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index d63b4d9c51..7a87c3c198 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -264,10 +264,6 @@ psutil_loadlibs() { "ntdll.dll", "NtSetInformationProcess"); if (! NtSetInformationProcess) return 1; - WinStationQueryInformationW = psutil_GetProcAddressFromLib( - "winsta.dll", "WinStationQueryInformationW"); - if (! WinStationQueryInformationW) - return 1; NtQueryObject = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQueryObject"); if (! NtQueryObject) @@ -320,6 +316,13 @@ psutil_loadlibs() { // minumum requirement: Win 7 GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( "kernel32", "GetLogicalProcessorInformationEx"); + // minimum requirements: Windows Server Core + WTSEnumerateSessionsW = psutil_GetProcAddressFromLib( + "wtsapi32.dll", "WTSEnumerateSessionsW"); + WTSQuerySessionInformationW = psutil_GetProcAddressFromLib( + "wtsapi32.dll", "WTSQuerySessionInformationW"); + WTSFreeMemory = psutil_GetProcAddressFromLib( + "wtsapi32.dll", "WTSFreeMemory"); PyErr_Clear(); return 0; diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index be4759ac1c..c8b2f38355 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -22,7 +22,6 @@ #include // memory_info(), memory_maps() #include #include // threads(), PROCESSENTRY32 -#include // users() // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") @@ -1191,17 +1190,17 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { static PyObject * psutil_users(PyObject *self, PyObject *args) { HANDLE hServer = WTS_CURRENT_SERVER_HANDLE; - WCHAR *buffer_user = NULL; - LPTSTR buffer_addr = NULL; - PWTS_SESSION_INFO sessions = NULL; + LPWSTR buffer_user = NULL; + LPWSTR buffer_addr = NULL; + LPWSTR buffer_info = NULL; + PWTS_SESSION_INFOW sessions = NULL; DWORD count; DWORD i; DWORD sessionId; DWORD bytes; PWTS_CLIENT_ADDRESS address; char address_str[50]; - WINSTATION_INFO station_info; - ULONG returnLen; + PWTSINFOW wts_info; PyObject *py_tuple = NULL; PyObject *py_address = NULL; PyObject *py_username = NULL; @@ -1210,8 +1209,21 @@ psutil_users(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (WTSEnumerateSessions(hServer, 0, 1, &sessions, &count) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessions"); + if (WTSEnumerateSessionsW == NULL || + WTSQuerySessionInformationW == NULL || + WTSFreeMemory == NULL) { + // If we don't run in an environment that is a Remote Desktop Services environment + // the Wtsapi32 proc might not be present. + // https://docs.microsoft.com/en-us/windows/win32/termserv/run-time-linking-to-wtsapi32-dll + return py_retlist; + } + + if (WTSEnumerateSessionsW(hServer, 0, 1, &sessions, &count) == 0) { + if (ERROR_CALL_NOT_IMPLEMENTED == GetLastError()) { + // On Windows Nano server, the Wtsapi32 API can be present, but return WinError 120. + return py_retlist; + } + PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); goto error; } @@ -1223,9 +1235,12 @@ psutil_users(PyObject *self, PyObject *args) { WTSFreeMemory(buffer_user); if (buffer_addr != NULL) WTSFreeMemory(buffer_addr); + if (buffer_info != NULL) + WTSFreeMemory(buffer_info); buffer_user = NULL; buffer_addr = NULL; + buffer_info = NULL; // username bytes = 0; @@ -1239,21 +1254,22 @@ psutil_users(PyObject *self, PyObject *args) { // address bytes = 0; - if (WTSQuerySessionInformation(hServer, sessionId, WTSClientAddress, - &buffer_addr, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformation"); + if (WTSQuerySessionInformationW(hServer, sessionId, WTSClientAddress, + &buffer_addr, &bytes) == 0) { + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); goto error; } address = (PWTS_CLIENT_ADDRESS)buffer_addr; - if (address->AddressFamily == 0) { // AF_INET + if (address->AddressFamily == 2) { // AF_INET == 2 sprintf_s(address_str, _countof(address_str), "%u.%u.%u.%u", - address->Address[0], - address->Address[1], + // The IP address is offset by two bytes from the start of the Address member of the WTS_CLIENT_ADDRESS structure. address->Address[2], - address->Address[3]); + address->Address[3], + address->Address[4], + address->Address[5]); py_address = Py_BuildValue("s", address_str); if (!py_address) goto error; @@ -1263,26 +1279,23 @@ psutil_users(PyObject *self, PyObject *args) { } // login time - if (! WinStationQueryInformationW( - hServer, - sessionId, - WinStationInformation, - &station_info, - sizeof(station_info), - &returnLen)) - { - PyErr_SetFromOSErrnoWithSyscall("WinStationQueryInformationW"); + bytes = 0; + if (WTSQuerySessionInformationW(hServer, sessionId, WTSSessionInfo, + &buffer_info, &bytes) == 0) { + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); goto error; } + wts_info = (PWTSINFOW)buffer_info; py_username = PyUnicode_FromWideChar(buffer_user, wcslen(buffer_user)); if (py_username == NULL) goto error; + py_tuple = Py_BuildValue( "OOd", py_username, py_address, - psutil_FiletimeToUnixTime(station_info.ConnectTime) + psutil_LargeIntegerToUnixTime(wts_info->ConnectTime) ); if (!py_tuple) goto error; @@ -1296,6 +1309,7 @@ psutil_users(PyObject *self, PyObject *args) { WTSFreeMemory(sessions); WTSFreeMemory(buffer_user); WTSFreeMemory(buffer_addr); + WTSFreeMemory(buffer_info); return py_retlist; error: @@ -1310,6 +1324,8 @@ psutil_users(PyObject *self, PyObject *args) { WTSFreeMemory(buffer_user); if (buffer_addr != NULL) WTSFreeMemory(buffer_addr); + if (buffer_info != NULL) + WTSFreeMemory(buffer_info); return NULL; } diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 8cb00430e2..ea1f428167 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -19,6 +19,12 @@ typedef LONG NTSTATUS; #define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225L) #define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) +// WtsApi32.h +#define WTS_CURRENT_SERVER_HANDLE ((HANDLE)NULL) +#define WINSTATIONNAME_LENGTH 32 +#define DOMAIN_LENGTH 17 +#define USERNAME_LENGTH 20 + // ================================================================ // Enums // ================================================================ @@ -93,6 +99,53 @@ typedef enum _KWAIT_REASON { MaximumWaitReason } KWAIT_REASON, *PKWAIT_REASON; +// users() +typedef enum _WTS_INFO_CLASS { + WTSInitialProgram, + WTSApplicationName, + WTSWorkingDirectory, + WTSOEMId, + WTSSessionId, + WTSUserName, + WTSWinStationName, + WTSDomainName, + WTSConnectState, + WTSClientBuildNumber, + WTSClientName, + WTSClientDirectory, + WTSClientProductId, + WTSClientHardwareId, + WTSClientAddress, + WTSClientDisplay, + WTSClientProtocolType, + WTSIdleTime, + WTSLogonTime, + WTSIncomingBytes, + WTSOutgoingBytes, + WTSIncomingFrames, + WTSOutgoingFrames, + WTSClientInfo, + WTSSessionInfo, + WTSSessionInfoEx, + WTSConfigInfo, + WTSValidationInfo, // Info Class value used to fetch Validation Information through the WTSQuerySessionInformation + WTSSessionAddressV4, + WTSIsRemoteSession +} WTS_INFO_CLASS; + +typedef enum _WTS_CONNECTSTATE_CLASS { + WTSActive, // User logged on to WinStation + WTSConnected, // WinStation connected to client + WTSConnectQuery, // In the process of connecting to client + WTSShadow, // Shadowing another WinStation + WTSDisconnected, // WinStation logged on without client + WTSIdle, // Waiting for client to connect + WTSListen, // WinStation is listening for connection + WTSReset, // WinStation is being reset + WTSDown, // WinStation is down due to error + WTSInit, // WinStation in initialization +} WTS_CONNECTSTATE_CLASS; + // ================================================================ // Structs. // ================================================================ @@ -309,17 +362,42 @@ typedef struct { } RTL_USER_PROCESS_PARAMETERS_, *PRTL_USER_PROCESS_PARAMETERS_; // users() -typedef struct _WINSTATION_INFO { - BYTE Reserved1[72]; - ULONG SessionId; - BYTE Reserved2[4]; - FILETIME ConnectTime; - FILETIME DisconnectTime; - FILETIME LastInputTime; - FILETIME LoginTime; - BYTE Reserved3[1096]; - FILETIME CurrentTime; -} WINSTATION_INFO, *PWINSTATION_INFO; +typedef struct _WTS_SESSION_INFOW { + DWORD SessionId; // session id + LPWSTR pWinStationName; // name of WinStation this session is + // connected to + WTS_CONNECTSTATE_CLASS State; // connection state (see enum) +} WTS_SESSION_INFOW, * PWTS_SESSION_INFOW; + +#define PWTS_SESSION_INFO PWTS_SESSION_INFOW + +typedef struct _WTS_CLIENT_ADDRESS { + DWORD AddressFamily; // AF_INET, AF_INET6, AF_IPX, AF_NETBIOS, AF_UNSPEC + BYTE Address[20]; // client network address +} WTS_CLIENT_ADDRESS, * PWTS_CLIENT_ADDRESS; + +typedef struct _WTSINFOW { + WTS_CONNECTSTATE_CLASS State; // connection state (see enum) + DWORD SessionId; // session id + DWORD IncomingBytes; + DWORD OutgoingBytes; + DWORD IncomingFrames; + DWORD OutgoingFrames; + DWORD IncomingCompressedBytes; + DWORD OutgoingCompressedBytes; + WCHAR WinStationName[WINSTATIONNAME_LENGTH]; + WCHAR Domain[DOMAIN_LENGTH]; + WCHAR UserName[USERNAME_LENGTH + 1];// name of WinStation this session is + // connected to + LARGE_INTEGER ConnectTime; + LARGE_INTEGER DisconnectTime; + LARGE_INTEGER LastInputTime; + LARGE_INTEGER LogonTime; + LARGE_INTEGER CurrentTime; + +} WTSINFOW, * PWTSINFOW; + +#define PWTSINFO PWTSINFOW // cpu_count_phys() #if (_WIN32_WINNT < 0x0601) // Windows < 7 (Vista and XP) @@ -551,6 +629,32 @@ DWORD (CALLBACK *_GetActiveProcessorCount) ( #define GetActiveProcessorCount _GetActiveProcessorCount +BOOL(CALLBACK *_WTSQuerySessionInformationW) ( + HANDLE hServer, + DWORD SessionId, + WTS_INFO_CLASS WTSInfoClass, + LPWSTR* ppBuffer, + DWORD* pBytesReturned + ); + +#define WTSQuerySessionInformationW _WTSQuerySessionInformationW + +BOOL(CALLBACK *_WTSEnumerateSessionsW)( + HANDLE hServer, + DWORD Reserved, + DWORD Version, + PWTS_SESSION_INFO* ppSessionInfo, + DWORD* pCount + ); + +#define WTSEnumerateSessionsW _WTSEnumerateSessionsW + +VOID(CALLBACK *_WTSFreeMemory)( + IN PVOID pMemory + ); + +#define WTSFreeMemory _WTSFreeMemory + ULONGLONG (CALLBACK *_GetTickCount64) ( void); diff --git a/setup.py b/setup.py index 3c44230293..893eb46ba0 100755 --- a/setup.py +++ b/setup.py @@ -167,7 +167,7 @@ def get_winver(): define_macros=macros, libraries=[ "psapi", "kernel32", "advapi32", "shell32", "netapi32", - "wtsapi32", "ws2_32", "PowrProf", "pdh", + "ws2_32", "PowrProf", "pdh", ], # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"] From 57775dd969ab3064d1c846075460f5e62fa4ad2a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 5 Jul 2020 12:13:51 +0200 Subject: [PATCH 0603/1714] give CREDITS for #1768 and #1781 --- CREDITS | 6 +++++- HISTORY.rst | 4 ++++ psutil/tests/__init__.py | 3 +-- psutil/tests/runner.py | 3 +-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CREDITS b/CREDITS index 1c944d9567..2fe9480fcf 100644 --- a/CREDITS +++ b/CREDITS @@ -643,7 +643,7 @@ I: 1480 N: Ammar Askar W: http://ammaraskar.com/ -I: 604, 1484 +I: 604, 1484, 1781 N: agnewee W: https://github.com/Agnewee @@ -700,3 +700,7 @@ I: 1695 N: Michał Górny W: https://github.com/mgorny I: 1726 + +N: Julien Lebot +W: https://github.com/julien-lebot +I: 1768 diff --git a/HISTORY.rst b/HISTORY.rst index 3cb021a188..0963dd0494 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,6 +25,8 @@ XXXX-XX-XX psutil.Process(pid=12739, name='python3', status='terminated', exitcode=, started='15:08:20') - 1757_: memory leak tests are now stable. +- 1768_: [Windows] added support for Windows Nano Server. (contributed by + Julien Lebot) **Bug fixes** @@ -32,6 +34,8 @@ XXXX-XX-XX (patch by Michał Górny) - 1760_: [Linux] Process.rlimit() does not handle long long type properly. - 1766_: [macOS] NoSuchProcess may be raised instead of ZombieProcess. +- 1781_: fix signature of callback function for getloadavg(). (patch by + Ammar Askar) 5.7.0 ===== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5bad53cf3e..a21f1fd894 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -76,7 +76,7 @@ # constants 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', - 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TOX', 'TRAVIS', 'CIRRUS', + 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TRAVIS', 'CIRRUS', '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", @@ -117,7 +117,6 @@ # --- platforms -TOX = os.getenv('TOX') or '' in ('1', 'true') PYPY = '__pypy__' in sys.builtin_module_names # whether we're running this test suite on a Continuous Integration service TRAVIS = 'TRAVIS' in os.environ diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 8b86a3e99f..0777f5e74d 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -48,10 +48,9 @@ from psutil.tests import print_sysinfo from psutil.tests import reap_children from psutil.tests import safe_rmpath -from psutil.tests import TOX -VERBOSITY = 1 if TOX else 2 +VERBOSITY = 2 FAILED_TESTS_FNAME = '.failed-tests.txt' NWORKERS = psutil.cpu_count() or 1 USE_COLORS = not CI_TESTING and term_supports_colors() From 9267775415c6464478457e9ac332f83787a7acc2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Jul 2020 13:13:43 +0200 Subject: [PATCH 0604/1714] pre-release --- HISTORY.rst | 6 +++--- docs/index.rst | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0963dd0494..e9814fe729 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.7.1 (unreleased) -================== +5.7.1 +===== -XXXX-XX-XX +2020-07-15 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 47d88da971..d9ea8f99ff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2507,6 +2507,10 @@ Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. Timeline ======== +- 2020-07-15: + `5.7.1 `__ - + `what's new `__ - + `diff `__ - 2020-02-18: `5.7.0 `__ - `what's new `__ - From 6e55b5eb48d21548048634ea2304a6c903408435 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Jul 2020 14:54:31 +0200 Subject: [PATCH 0605/1714] pre-release --- HISTORY.rst | 9 +++++++++ Makefile | 2 +- docs/index.rst | 4 ++++ psutil/__init__.py | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e9814fe729..a7d73f6b9a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,14 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.7.2 +===== + +2020-07-15 + +**Bug fixes** + +- wheels for 2.7 were inadvertently deleted. + 5.7.1 ===== diff --git a/Makefile b/Makefile index be92b74429..80901538cf 100644 --- a/Makefile +++ b/Makefile @@ -220,7 +220,7 @@ download-wheels-github: ## Download latest wheels hosted on github. download-wheels: ## Download wheels from github and appveyor rm -rf dist ${MAKE} download-wheels-appveyor - ${MAKE} download-wheels-github + # ${MAKE} download-wheels-github ${MAKE} print-wheels print-wheels: ## Print downloaded wheels diff --git a/docs/index.rst b/docs/index.rst index d9ea8f99ff..374aa79d61 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2507,6 +2507,10 @@ Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. Timeline ======== +- 2020-07-15: + `5.7.2 `__ - + `what's new `__ - + `diff `__ - 2020-07-15: `5.7.1 `__ - `what's new `__ - diff --git a/psutil/__init__.py b/psutil/__init__.py index 7fdbc0fc26..d9855dc046 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -226,7 +226,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.7.1" +__version__ = "5.7.2" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From a0967043b5819b2edc61d9a12306289d5e7f98c2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Jul 2020 15:17:09 +0200 Subject: [PATCH 0606/1714] pre-release --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index 374aa79d61..cb3cf08dfd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2493,6 +2493,7 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= +* psutil 5.7.1 (2020-07): **Windows Nano** * psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support * psutil 5.7.0 (2020-02): **PyPy** on Windows * psutil 5.4.0 (2017-11): **AIX** From b09f5a28c98f1d4ee160e133ee5fc5446efba68f Mon Sep 17 00:00:00 2001 From: Armin Gruner Date: Thu, 13 Aug 2020 15:39:29 +0200 Subject: [PATCH 0607/1714] Implement Process.environ() on *BSD family (#1800) (patch by Armin Gruner) --- psutil/_psbsd.py | 13 +-- psutil/_psutil_bsd.c | 150 +++++++++++++++++++++++++++++++- psutil/_psutil_common.c | 20 +++++ psutil/_psutil_common.h | 6 ++ psutil/arch/freebsd/specific.c | 4 +- psutil/arch/freebsd/sys_socks.c | 5 +- psutil/arch/openbsd/specific.c | 14 --- psutil/tests/test_contracts.py | 3 +- psutil/tests/test_process.py | 2 +- 9 files changed, 193 insertions(+), 24 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index d53eb0427a..0568e3a853 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -51,7 +51,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, @@ -76,12 +76,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 = { @@ -669,6 +668,10 @@ def cmdline(self): 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']] diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 953fcd083c..c4450d7dfc 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -57,6 +57,7 @@ #include #include // process open files/connections #include +#include #include "_psutil_common.h" #include "_psutil_posix.h" @@ -391,6 +392,145 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { } +/* + * Return process environment as a Python dictionary + */ +PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + int i, cnt = -1; + long pid; + char *s, **envs, errbuf[_POSIX2_LINE_MAX]; + PyObject *py_value=NULL, *py_retdict=NULL; + kvm_t *kd; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 *p; +#else + struct kinfo_proc *p; +#endif + + if (!PyArg_ParseTuple(args, "l", &pid)) + return NULL; + +#if defined(PSUTIL_FREEBSD) + kd = kvm_openfiles(NULL, "/dev/null", NULL, 0, errbuf); +#else + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); +#endif + if (!kd) { + convert_kvm_err("kvm_openfiles", errbuf); + return NULL; + } + + py_retdict = PyDict_New(); + if (!py_retdict) + goto error; + +#if defined(PSUTIL_FREEBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, &cnt); +#elif defined(PSUTIL_OPENBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#elif defined(PSUTIL_NETBSD) + p = kvm_getproc2(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#endif + if (!p) { + NoSuchProcess("kvm_getprocs"); + goto error; + } + if (cnt <= 0) { + NoSuchProcess(cnt < 0 ? kvm_geterr(kd) : "kvm_getprocs: cnt==0"); + goto error; + } + + // On *BSD kernels there are a few kernel-only system processes without an + // environment (See e.g. "procstat -e 0 | 1 | 2 ..." on FreeBSD.) + // + // Some system process have no stats attached at all + // (they are marked with P_SYSTEM.) + // + // On FreeBSD, it's possible that the process is swapped or paged out, + // then there no access to the environ stored in the process' user area. + // + // On NetBSD, we cannot call kvm_getenvv2() for a zombie process. + // + // To make unittest suite happy, return an empty environment. + // +#if defined(PSUTIL_FREEBSD) +#if (defined(__FreeBSD_version) && __FreeBSD_version >= 700000) + if (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) { +#else + if ((p)->ki_flag & P_SYSTEM) { +#endif +#elif defined(PSUTIL_NETBSD) + if ((p)->p_stat == SZOMB) { +#elif defined(PSUTIL_OPENBSD) + if ((p)->p_flag & P_SYSTEM) { +#endif + kvm_close(kd); + return py_retdict; + } + +#if defined(PSUTIL_NETBSD) + envs = kvm_getenvv2(kd, p, 0); +#else + envs = kvm_getenvv(kd, p, 0); +#endif + if (!envs) { + // Map to "psutil" general high-level exceptions + switch (errno) { + case 0: + // Process has cleared it's environment, return empty one + kvm_close(kd); + return py_retdict; + case EPERM: + AccessDenied("kvm_getenvv"); + break; + case ESRCH: + NoSuchProcess("kvm_getenvv"); + break; +#if defined(PSUTIL_FREEBSD) + case ENOMEM: + // Unfortunately, under FreeBSD kvm_getenvv() returns + // failure for certain processes ( e.g. try + // "sudo procstat -e ".) + // Map the error condition to 'AccessDenied'. + sprintf(errbuf, + "kvm_getenvv(pid=%ld, ki_uid=%d): errno=ENOMEM", + pid, p->ki_uid); + AccessDenied(errbuf); + break; +#endif + default: + sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); + PyErr_SetFromOSErrnoWithSyscall(errbuf); + break; + } + goto error; + } + + for (i = 0; envs[i] != NULL; i++) { + s = strchr(envs[i], '='); + if (!s) + continue; + *s++ = 0; + py_value = PyUnicode_DecodeFSDefault(s); + if (!py_value) + goto error; + if (PyDict_SetItemString(py_retdict, envs[i], py_value)) { + goto error; + } + Py_DECREF(py_value); + } + + kvm_close(kd); + return py_retdict; + +error: + Py_XDECREF(py_value); + Py_XDECREF(py_retdict); + kvm_close(kd); + return NULL; +} + /* * Return the number of logical CPUs in the system. * XXX this could be shared with macOS @@ -617,8 +757,10 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",softdep", sizeof(opts)); if (flags & MNT_NOSYMFOLLOW) strlcat(opts, ",nosymfollow", sizeof(opts)); +#ifdef MNT_GJOURNAL if (flags & MNT_GJOURNAL) strlcat(opts, ",gjournal", sizeof(opts)); +#endif if (flags & MNT_MULTILABEL) strlcat(opts, ",multilabel", sizeof(opts)); if (flags & MNT_ACLS) @@ -627,8 +769,10 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",noclusterr", sizeof(opts)); if (flags & MNT_NOCLUSTERW) strlcat(opts, ",noclusterw", sizeof(opts)); +#ifdef MNT_NFS4ACLS if (flags & MNT_NFS4ACLS) strlcat(opts, ",nfs4acls", sizeof(opts)); +#endif #elif PSUTIL_NETBSD if (flags & MNT_NODEV) strlcat(opts, ",nodev", sizeof(opts)); @@ -831,7 +975,7 @@ psutil_users(PyObject *self, PyObject *args) { py_tty, // tty py_hostname, // hostname (float)ut.ut_time, // start time -#ifdef PSUTIL_OPENBSD +#if defined(PSUTIL_OPENBSD) || (defined(__FreeBSD_version) && __FreeBSD_version < 900000) -1 // process id (set to None later) #else ut.ut_pid // TODO: use PyLong_FromPid @@ -956,6 +1100,8 @@ static PyMethodDef mod_methods[] = { {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, "Return an XML string to determine the number physical CPUs."}, #endif + {"proc_environ", psutil_proc_environ, METH_VARARGS, + "Return process environment"}, // --- system-related functions @@ -1060,7 +1206,9 @@ static PyMethodDef mod_methods[] = { if (PyModule_AddIntConstant(mod, "SSLEEP", LSSLEEP)) INITERR; if (PyModule_AddIntConstant(mod, "SSTOP", LSSTOP)) INITERR; if (PyModule_AddIntConstant(mod, "SZOMB", LSZOMB)) INITERR; +#if __NetBSD_Version__ < 500000000 if (PyModule_AddIntConstant(mod, "SDEAD", LSDEAD)) INITERR; +#endif if (PyModule_AddIntConstant(mod, "SONPROC", LSONPROC)) INITERR; // unique to NetBSD if (PyModule_AddIntConstant(mod, "SSUSPENDED", LSSUSPENDED)) INITERR; diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 7a87c3c198..f821aba380 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -167,6 +167,26 @@ psutil_setup(void) { } +// ============================================================================ +// Utility functions (BSD) +// ============================================================================ + +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +void +convert_kvm_err(const char *syscall, char *errbuf) { + char fullmsg[8192]; + + sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); + if (strstr(errbuf, "Permission denied") != NULL) + AccessDenied(fullmsg); + else if (strstr(errbuf, "Operation not permitted") != NULL) + AccessDenied(fullmsg); + else + PyErr_Format(PyExc_RuntimeError, fullmsg); +} +#endif + + // ==================================================================== // --- Windows // ==================================================================== diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 2408c9f63e..3162772eb2 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -104,6 +104,12 @@ PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); int psutil_setup(void); +// ==================================================================== +// --- BSD +// ==================================================================== + +void convert_kvm_err(const char *syscall, char *errbuf); + // ==================================================================== // --- Windows // ==================================================================== diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 3f37a08e2d..c783264792 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -517,7 +517,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } -#if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 +#if defined(__FreeBSD_version) && __FreeBSD_version >= 701000 PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { pid_t pid; @@ -795,9 +795,11 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { case KVME_TYPE_DEAD: path = "[dead]"; break; +#ifdef KVME_TYPE_SG case KVME_TYPE_SG: path = "[sg]"; break; +#endif case KVME_TYPE_UNKNOWN: path = "[unknown]"; break; diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index ab61f393b9..9f7cf8d555 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -16,6 +16,9 @@ #include #include #include +#if defined(__FreeBSD_version) && __FreeBSD_version < 800000 +#include +#endif #include // for xinpcb struct #include #include @@ -30,7 +33,7 @@ static int psutil_nxfiles; int -psutil_populate_xfiles() { +psutil_populate_xfiles(void) { size_t len; if ((psutil_xfiles = malloc(len = sizeof *psutil_xfiles)) == NULL) { diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/specific.c index d97a8f9b93..aa4568f592 100644 --- a/psutil/arch/openbsd/specific.c +++ b/psutil/arch/openbsd/specific.c @@ -47,20 +47,6 @@ // ============================================================================ -static void -convert_kvm_err(const char *syscall, char *errbuf) { - char fullmsg[8192]; - - sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); - if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(fullmsg); - else if (strstr(errbuf, "Operation not permitted") != NULL) - AccessDenied(fullmsg); - else - PyErr_Format(PyExc_RuntimeError, fullmsg); -} - - int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // Fills a kinfo_proc struct based on process pid. diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 51bbb9f097..2d9e5917e6 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -137,7 +137,8 @@ class TestAvailProcessAPIs(PsutilTestCase): def test_environ(self): self.assertEqual(hasattr(psutil.Process, "environ"), - LINUX or MACOS or WINDOWS or AIX or SUNOS) + 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) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index e3394799c6..8b5e5c56d2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -298,7 +298,7 @@ def test_create_time(self): @unittest.skipIf(TRAVIS or CIRRUS, 'not reliable on TRAVIS/CIRRUS') def test_terminal(self): terminal = psutil.Process().terminal() - if sys.stdout.isatty(): + if sys.stdin.isatty(): tty = os.path.realpath(sh('tty')) self.assertEqual(terminal, tty) else: From 6b6f98b3d0926901c0929a377dfd2680b93661c9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Aug 2020 15:45:30 +0200 Subject: [PATCH 0608/1714] give CREDITS for #1800 / @ArminGrunner --- CREDITS | 4 ++++ HISTORY.rst | 9 +++++++++ docs/index.rst | 3 +-- psutil/__init__.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CREDITS b/CREDITS index 2fe9480fcf..36633bd8dc 100644 --- a/CREDITS +++ b/CREDITS @@ -704,3 +704,7 @@ I: 1726 N: Julien Lebot W: https://github.com/julien-lebot I: 1768 + +N: Armin Gruner +W: https://github.com/ArminGruner +I: 1800 diff --git a/HISTORY.rst b/HISTORY.rst index a7d73f6b9a..36c38c9511 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,14 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.7.3 +===== + +XXXX-XX-XX + +**Enhancements** + +- 893_: implement Process.environ() on BSD family. (patch by Armin Gruner) + 5.7.2 ===== diff --git a/docs/index.rst b/docs/index.rst index cb3cf08dfd..c6bbe59242 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1144,11 +1144,10 @@ Process class >>> psutil.Process().environ() {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} - Availability: Linux, macOS, Windows, SunOS - .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support .. versionchanged:: 5.6.3 added AIX suport + .. versionchanged:: 5.7.3 added BSD suport .. method:: create_time() diff --git a/psutil/__init__.py b/psutil/__init__.py index d9855dc046..65d0297fac 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -853,7 +853,7 @@ def cpu_num(self): """ return self._proc.cpu_num() - # Linux, macOS, Windows, Solaris, AIX + # All platforms has it, but maybe not in the future. if hasattr(_psplatform.Process, "environ"): def environ(self): From ff088dc310f0cd24509ab4260888ef75da412ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Chv=C3=A1tal?= Date: Thu, 13 Aug 2020 16:00:19 +0200 Subject: [PATCH 0609/1714] If the batery is not available set it to false (#1717) --- psutil/tests/__init__.py | 2 +- psutil/tests/test_misc.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index a21f1fd894..b508a629f6 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -185,7 +185,7 @@ try: HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) except Exception: - HAS_BATTERY = True + HAS_BATTERY = False HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") HAS_THREADS = hasattr(psutil.Process, "threads") diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 10e45d23f5..8fcee12a76 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -763,6 +763,8 @@ def test_fans(self): 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') From c7ded0124e7b51da4f135ded3d105b22bf0d98f2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Aug 2020 10:46:51 +0200 Subject: [PATCH 0610/1714] add lock issues bot --- .github/lock.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/lock.yml diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 0000000000..7099c810c4 --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,35 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 3 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: false + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: false + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo From 5be673aa2a0fccf079803bc2e3720fb46463793b Mon Sep 17 00:00:00 2001 From: Caleb Bassi Date: Sun, 13 Sep 2020 09:17:19 -0700 Subject: [PATCH 0611/1714] readme: update rust-psutil repo url (#1824) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 39822e2961..5c1ef28883 100644 --- a/README.rst +++ b/README.rst @@ -161,7 +161,7 @@ Portings - Go: https://github.com/shirou/gopsutil - C: https://github.com/hamon-in/cpslib -- Rust: https://github.com/borntyping/rust-psutil +- Rust: https://github.com/rust-psutil/rust-psutil - Nim: https://github.com/johnscillieri/psutil-nim Example usages From e38a80cec89f53bbcc5013b76b16aa712326c31f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Sep 2020 16:48:32 +0200 Subject: [PATCH 0612/1714] add Linux specific test for NIC MTU --- psutil/tests/test_linux.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 9dd12890c6..044d440d24 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -924,6 +924,11 @@ def test_against_ifconfig(self): 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, "rt") as f: + self.assertEqual(stats.mtu, int(f.read().strip())) + @unittest.skipIf(not LINUX, "LINUX only") class TestSystemNetIOCounters(PsutilTestCase): From 1d5073aac29a3f50c19b864d22688cf20447f1e9 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 21 Sep 2020 13:01:43 -0500 Subject: [PATCH 0613/1714] Use IFF_RUNNING instead of IFF_UP for Linux (#1831) Co-authored-by: Chris Burger --- psutil/_psutil_posix.c | 2 +- psutil/tests/test_linux.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index f7f8b92dd8..8fe7f6b7d9 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -404,7 +404,7 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { goto error; close(sock); - if ((ifr.ifr_flags & IFF_UP) != 0) + if ((ifr.ifr_flags & IFF_RUNNING) != 0) return Py_BuildValue("O", Py_True); else return Py_BuildValue("O", Py_False); diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 044d440d24..09461ec57c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -919,8 +919,7 @@ def test_against_ifconfig(self): except RuntimeError: pass else: - # Not always reliable. - # self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) + self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) self.assertEqual(stats.mtu, int(re.findall(r'(?i)MTU[: ](\d+)', out)[0])) From ba0c0ab967b0f524807cedf06554b94b7aba3d70 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Sep 2020 21:00:10 +0200 Subject: [PATCH 0614/1714] update doc for #1830 (net_if_stats() isup check if NIC is running) --- HISTORY.rst | 4 +++- docs/index.rst | 4 +++- psutil/__init__.py | 2 +- psutil/_psbsd.py | 2 +- psutil/_pslinux.py | 2 +- psutil/_psosx.py | 2 +- psutil/_psutil_posix.c | 6 +++--- 7 files changed, 13 insertions(+), 9 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 36c38c9511..3f4951743f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,7 +7,9 @@ XXXX-XX-XX **Enhancements** -- 893_: implement Process.environ() on BSD family. (patch by Armin Gruner) +- 893_: implement `Process.environ()` on BSD family. (patch by Armin Gruner) +- 1830_: [UNIX] `net_if_stats()`'s `isup` also checks whether the NIC is + running (meaning Wi-Fi or ethernet cable is connected). 5.7.2 ===== diff --git a/docs/index.rst b/docs/index.rst index c6bbe59242..fa55b02559 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -687,7 +687,8 @@ Network system as a dictionary whose keys are the NIC names and value is a named tuple with the following fields: - - **isup**: a bool indicating whether the NIC is up and running. + - **isup**: a bool indicating whether the NIC is up and running (meaning + ethernet cable or Wi-Fi is connected). - **duplex**: the duplex communication type; it can be either :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or :const:`NIC_DUPLEX_UNKNOWN`. @@ -706,6 +707,7 @@ Network .. versionadded:: 3.0.0 + .. versionchanged:: 5.7.3 `isup` on UNIX also checks whether the NIC is running. Sensors ------- diff --git a/psutil/__init__.py b/psutil/__init__.py index 65d0297fac..9d43f991d0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -226,7 +226,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.7.2" +__version__ = "5.7.3" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 0568e3a853..9565406b10 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -353,7 +353,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + isup = cext_posix.net_if_is_running(name) duplex, speed = cext_posix.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 3e3caace78..52f7dd97dd 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1031,7 +1031,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + isup = cext_posix.net_if_is_running(name) duplex, speed = cext.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 2feff93233..6a1899316f 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -262,7 +262,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + isup = cext_posix.net_if_is_running(name) duplex, speed = cext_posix.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 8fe7f6b7d9..1182765d7e 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -385,7 +385,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { * http://www.i-scream.org/libstatgrab/ */ static PyObject * -psutil_net_if_flags(PyObject *self, PyObject *args) { +psutil_net_if_is_running(PyObject *self, PyObject *args) { char *nic_name; int sock = -1; int ret; @@ -621,8 +621,8 @@ static PyMethodDef mod_methods[] = { "Retrieve NICs information"}, {"net_if_mtu", psutil_net_if_mtu, METH_VARARGS, "Retrieve NIC MTU"}, - {"net_if_flags", psutil_net_if_flags, METH_VARARGS, - "Retrieve NIC flags"}, + {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS, + "Return True if the NIC is running."}, #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, "Return NIC stats."}, From f9b1b5e431ecbecf76b255737d5738ba2c69c1ba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Sep 2020 21:01:58 +0200 Subject: [PATCH 0615/1714] give credits to Chris Burger for #1830 --- CREDITS | 4 ++++ HISTORY.rst | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 36633bd8dc..debb6f733a 100644 --- a/CREDITS +++ b/CREDITS @@ -708,3 +708,7 @@ I: 1768 N: Armin Gruner W: https://github.com/ArminGruner I: 1800 + +N: Chris Burger +W: https://github.com/phobozad +I: 1830 diff --git a/HISTORY.rst b/HISTORY.rst index 3f4951743f..d2c922c89f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,7 +9,7 @@ XXXX-XX-XX - 893_: implement `Process.environ()` on BSD family. (patch by Armin Gruner) - 1830_: [UNIX] `net_if_stats()`'s `isup` also checks whether the NIC is - running (meaning Wi-Fi or ethernet cable is connected). + running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) 5.7.2 ===== From fbcbd8edd030f83767a9b83ee25cd7a142d6171e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 24 Sep 2020 18:21:11 +0200 Subject: [PATCH 0616/1714] add IPv6 address test --- README.rst | 9 +++++---- psutil/tests/test_linux.py | 27 ++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 5c1ef28883..31119c3141 100644 --- a/README.rst +++ b/README.rst @@ -131,11 +131,12 @@ Security To report a security vulnerability, please use the `Tidelift security contact`_. Tidelift will coordinate the fix and disclosure. -Donate -====== +Sponsorship +=========== A lot of time and effort went into making psutil as it is today. If you whish -to help its future development consider making me a `donation `__. +to help its future development consider +`sponsoring `__ it. Projects using psutil ===================== @@ -146,7 +147,7 @@ psutil has roughly the following monthly downloads: :target: https://pepy.tech/project/psutil :alt: Downloads -...and has over 45,000 projects on GitHub depending from it. +...and has over 55,000 projects on GitHub depending from it. Here's some I find particularly interesting: - https://github.com/google/grr diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 09461ec57c..65e554492a 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -75,6 +75,23 @@ def get_ipv4_address(ifname): struct.pack('256s', ifname))[20:24]) +def get_ipv6_address(ifname): + with open("/proc/net/if_inet6", 'rt') as f: + for line in f.readlines(): + fields = line.split() + if fields[-1] == ifname: + break + else: + raise ValueError("could not find interface %r" % ifname) + unformatted = fields[0] + groups = [] + for i in range(0, len(unformatted), 4): + groups.append(unformatted[i:i + 4]) + formatted = ":".join(groups) + packed = socket.inet_pton(socket.AF_INET6, formatted) + return socket.inet_ntop(socket.AF_INET6, packed) + + def get_mac_address(ifname): import fcntl ifname = ifname[:15] @@ -890,7 +907,15 @@ def test_ips(self): 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 + 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.assertEqual(address, get_ipv6_address(name)) # XXX - not reliable when having virtual NICs installed by Docker. # @unittest.skipIf(not which('ip'), "'ip' utility not available") From e540554baba8bf9e2c8499cfb9dad4f0d4e0fffb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 27 Sep 2020 00:40:11 +0200 Subject: [PATCH 0617/1714] remove faulty test --- psutil/tests/test_linux.py | 11 ----------- psutil/tests/test_process.py | 4 +--- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 65e554492a..75b7639f0e 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1440,17 +1440,6 @@ def test_percent(self): 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): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 8b5e5c56d2..b2328ba2f1 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -298,11 +298,9 @@ def test_create_time(self): @unittest.skipIf(TRAVIS or CIRRUS, 'not reliable on TRAVIS/CIRRUS') def test_terminal(self): terminal = psutil.Process().terminal() - if sys.stdin.isatty(): + if terminal is not None: 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) From 549200d214a00f972361c01ea2c8f0b7561b927b Mon Sep 17 00:00:00 2001 From: aristocratos Date: Sun, 27 Sep 2020 00:42:25 +0200 Subject: [PATCH 0618/1714] Expanded battery sensor detection (#1837) --- psutil/_pslinux.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 52f7dd97dd..4a96c906b5 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1340,7 +1340,8 @@ def multi_cat(*paths): return int(ret) if ret.isdigit() else ret 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 @@ -1358,12 +1359,14 @@ def multi_cat(*paths): energy_full = multi_cat( root + "/energy_full", root + "/charge_full") - if energy_now is None or power_now is None: + time_to_empty = multi_cat(root + "/time_to_empty_now") + + if (energy_now is None or power_now is None) and time_to_empty is None: return None # 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: @@ -1395,11 +1398,15 @@ 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 return _common.sbattery(percent, secsleft, power_plugged) From ac551d1fd9e1837a5ab5c34b0425e1f17d8c1009 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 27 Sep 2020 00:44:59 +0200 Subject: [PATCH 0619/1714] give CREDITS for #1837 --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index debb6f733a..c5182cadd6 100644 --- a/CREDITS +++ b/CREDITS @@ -712,3 +712,7 @@ I: 1800 N: Chris Burger W: https://github.com/phobozad I: 1830 + +N: aristocratos +W: https://github.com/aristocratos +I: 1837 diff --git a/HISTORY.rst b/HISTORY.rst index d2c922c89f..e22b3a589f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,8 @@ XXXX-XX-XX - 893_: implement `Process.environ()` on BSD family. (patch by Armin Gruner) - 1830_: [UNIX] `net_if_stats()`'s `isup` also checks whether the NIC is running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) +- 1837_: [Linux] improved battery detection and charge "secsleft" calculation + (patch by aristocratos) 5.7.2 ===== From 7935f91173c813defe45666480e59c6a141cab43 Mon Sep 17 00:00:00 2001 From: aristocratos Date: Wed, 30 Sep 2020 23:41:01 +0200 Subject: [PATCH 0620/1714] sensors_battery() only return None if percentage is undetermined (#1838) --- psutil/_pslinux.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4a96c906b5..4dc3a0aba6 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1361,9 +1361,6 @@ def multi_cat(*paths): root + "/charge_full") time_to_empty = multi_cat(root + "/time_to_empty_now") - if (energy_now is None or power_now is None) and time_to_empty is None: - return None - # 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 and energy_now is not None: @@ -1407,6 +1404,8 @@ def multi_cat(*paths): 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) From 1626bae30349627e4d0a25ac9fb4cf3a743b94e7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 30 Sep 2020 23:44:13 +0200 Subject: [PATCH 0621/1714] give CREDTIS for #1838, #1828 --- CREDITS | 2 +- HISTORY.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index c5182cadd6..3c54d33aff 100644 --- a/CREDITS +++ b/CREDITS @@ -715,4 +715,4 @@ I: 1830 N: aristocratos W: https://github.com/aristocratos -I: 1837 +I: 1837, 1838 diff --git a/HISTORY.rst b/HISTORY.rst index e22b3a589f..1d760dbcd4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,12 @@ XXXX-XX-XX - 1837_: [Linux] improved battery detection and charge "secsleft" calculation (patch by aristocratos) +**Bug fixes** + +- 1838_: [Linux] sensors_battery(): if `percent` can be determined but not + the remaining values, still return a result instead of None. + (patch by aristocratos) + 5.7.2 ===== From 5859f7f14fba32ae5d882157c617cdb1f28cd437 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 1 Oct 2020 02:18:32 +0200 Subject: [PATCH 0622/1714] invoke psutil_setup() from linux C module --- psutil/_psutil_linux.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index c94ec03502..415474388d 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -662,6 +662,8 @@ static PyMethodDef mod_methods[] = { if (PyModule_AddIntConstant(mod, "DUPLEX_FULL", DUPLEX_FULL)) INITERR; if (PyModule_AddIntConstant(mod, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN)) INITERR; + psutil_setup(); + if (mod == NULL) INITERR; #if PY_MAJOR_VERSION >= 3 From 5b519a2469ad21a5913035031c99029018dda4c1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 5 Oct 2020 15:36:10 +0200 Subject: [PATCH 0623/1714] add linux tests for broadcast and netmask addresses --- psutil/tests/test_linux.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 75b7639f0e..163be0f978 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -53,6 +53,8 @@ SIOCGIFADDR = 0x8915 SIOCGIFCONF = 0x8912 SIOCGIFHWADDR = 0x8927 +SIOCGIFNETMASK = 0x891b +SIOCGIFBRDADDR = 0x8919 if LINUX: SECTOR_SIZE = 512 EMPTY_TEMPERATURES = not glob.glob('/sys/class/hwmon/hwmon*') @@ -75,6 +77,32 @@ def get_ipv4_address(ifname): 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_address(ifname): with open("/proc/net/if_inet6", 'rt') as f: for line in f.readlines(): @@ -907,6 +935,12 @@ def test_ips(self): 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: From d3bb5d4d36ae752beecf9cb1d559c81c9a53a9c0 Mon Sep 17 00:00:00 2001 From: Arnon Yaari Date: Tue, 6 Oct 2020 00:54:19 +0300 Subject: [PATCH 0624/1714] remove old unused code (#1843) Co-authored-by: Arnon Yaari --- psutil/_pslinux.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4dc3a0aba6..3c9ff28110 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -843,10 +843,6 @@ 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 range(0, 16, 4))) ip = base64.b16decode(ip) try: # see: https://github.com/giampaolo/psutil/issues/201 From 91c9b3e94d8dab7a94e9d57050b9570b8a3ba4c7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 Oct 2020 22:34:49 +0200 Subject: [PATCH 0625/1714] refactor scripts using curses --- scripts/iotop.py | 46 +++++++++++++++++---------- scripts/nettop.py | 57 ++++++++++++++++++++-------------- scripts/top.py | 79 +++++++++++++++++++++++++++++++---------------- 3 files changed, 117 insertions(+), 65 deletions(-) diff --git a/scripts/iotop.py b/scripts/iotop.py index c3afd07151..0468367347 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -14,7 +14,7 @@ Example output: -$ python scripts/iotop.py +$ python3 scripts/iotop.py Total DISK READ: 0.00 B/s | Total DISK WRITE: 472.00 K/s PID USER DISK READ DISK WRITE COMMAND 13155 giampao 0.00 B/s 428.00 K/s /usr/bin/google-chrome-beta @@ -30,7 +30,6 @@ Author: Giampaolo Rodola' """ -import atexit import time import sys try: @@ -42,20 +41,11 @@ from psutil._common import bytes2human -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - - win = curses.initscr() -atexit.register(tear_down) -curses.endwin() lineno = 0 -def print_line(line, highlight=False): +def printl(line, highlight=False): """A thin wrapper around curses's addstr().""" global lineno try: @@ -129,10 +119,10 @@ def refresh_window(procs, disks_read, disks_write): disks_tot = "Total DISK READ: %s | Total DISK WRITE: %s" \ % (bytes2human(disks_read), bytes2human(disks_write)) - print_line(disks_tot) + printl(disks_tot) header = templ % ("PID", "USER", "DISK READ", "DISK WRITE", "COMMAND") - print_line(header, highlight=True) + printl(header, highlight=True) for p in procs: line = templ % ( @@ -142,21 +132,45 @@ def refresh_window(procs, disks_read, disks_write): bytes2human(p._write_per_sec), p._cmdline) try: - print_line(line) + printl(line) except curses.error: break win.refresh() +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(0, curses.COLORS): + curses.init_pair(i + 1, i, -1) + curses.endwin() + win.nodelay(1) + + +def tear_down(): + win.keypad(0) + curses.nocbreak() + curses.echo() + curses.endwin() + + def main(): + global lineno + setup() try: interval = 0 while True: + if win.getch() == ord('q'): + break args = poll(interval) refresh_window(*args) - interval = 1 + lineno = 0 + interval = 0.5 + time.sleep(interval) except (KeyboardInterrupt, SystemExit): pass + finally: + tear_down() if __name__ == '__main__': diff --git a/scripts/nettop.py b/scripts/nettop.py index ce647c9dd3..8cc19fda4c 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -11,7 +11,7 @@ Author: Giampaolo Rodola' -$ python scripts/nettop.py +$ python3 scripts/nettop.py ----------------------------------------------------------- total bytes: sent: 1.49 G received: 4.82 G total packets: sent: 7338724 received: 8082712 @@ -31,7 +31,6 @@ pkts-recv 1214470 0 """ -import atexit import time import sys try: @@ -43,20 +42,11 @@ from psutil._common import bytes2human -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - - -win = curses.initscr() -atexit.register(tear_down) -curses.endwin() lineno = 0 +win = curses.initscr() -def print_line(line, highlight=False): +def printl(line, highlight=False): """A thin wrapper around curses's addstr().""" global lineno try: @@ -89,59 +79,80 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): global lineno # totals - print_line("total bytes: sent: %-10s received: %s" % ( + printl("total bytes: sent: %-10s received: %s" % ( bytes2human(tot_after.bytes_sent), bytes2human(tot_after.bytes_recv)) ) - print_line("total packets: sent: %-10s received: %s" % ( + printl("total packets: sent: %-10s received: %s" % ( tot_after.packets_sent, tot_after.packets_recv)) # per-network interface details: let's sort network interfaces so # that the ones which generated more traffic are shown first - print_line("") + printl("") nic_names = list(pnic_after.keys()) nic_names.sort(key=lambda x: sum(pnic_after[x]), reverse=True) for name in nic_names: stats_before = pnic_before[name] stats_after = pnic_after[name] templ = "%-15s %15s %15s" - print_line(templ % (name, "TOTAL", "PER-SEC"), highlight=True) - print_line(templ % ( + printl(templ % (name, "TOTAL", "PER-SEC"), highlight=True) + printl(templ % ( "bytes-sent", bytes2human(stats_after.bytes_sent), bytes2human( stats_after.bytes_sent - stats_before.bytes_sent) + '/s', )) - print_line(templ % ( + printl(templ % ( "bytes-recv", bytes2human(stats_after.bytes_recv), bytes2human( stats_after.bytes_recv - stats_before.bytes_recv) + '/s', )) - print_line(templ % ( + printl(templ % ( "pkts-sent", stats_after.packets_sent, stats_after.packets_sent - stats_before.packets_sent, )) - print_line(templ % ( + printl(templ % ( "pkts-recv", stats_after.packets_recv, stats_after.packets_recv - stats_before.packets_recv, )) - print_line("") + printl("") win.refresh() lineno = 0 +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(0, curses.COLORS): + curses.init_pair(i + 1, i, -1) + curses.endwin() + win.nodelay(1) + + +def tear_down(): + win.keypad(0) + curses.nocbreak() + curses.echo() + curses.endwin() + + def main(): + setup() try: interval = 0 while True: + if win.getch() == ord('q'): + break args = poll(interval) refresh_window(*args) - interval = 1 + interval = 0.5 except (KeyboardInterrupt, SystemExit): pass + finally: + tear_down() if __name__ == '__main__': diff --git a/scripts/top.py b/scripts/top.py index 0b17471d55..3c297d9af1 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -9,7 +9,7 @@ Author: Giampaolo Rodola' -$ python scripts/top.py +$ python3 scripts/top.py CPU0 [|||| ] 10.9% CPU1 [||||| ] 13.1% CPU2 [||||| ] 12.8% @@ -33,7 +33,6 @@ ... """ -import atexit import datetime import sys import time @@ -46,30 +45,28 @@ from psutil._common import bytes2human -# --- curses stuff - -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - - win = curses.initscr() -atexit.register(tear_down) -curses.endwin() lineno = 0 +colors_map = dict( + green=3, + red=10, + yellow=4, +) -def print_line(line, highlight=False): +def printl(line, color=None, bold=False, highlight=False): """A thin wrapper around curses's addstr().""" global lineno try: + flags = 0 + if color: + flags |= curses.color_pair(colors_map[color]) + if bold: + flags |= curses.A_BOLD if highlight: line += " " * (win.getmaxyx()[1] - len(line)) - win.addstr(lineno, 0, line, curses.A_REVERSE) - else: - win.addstr(lineno, 0, line, 0) + flags |= curses.A_STANDOUT + win.addstr(lineno, 0, line, flags) except curses.error: lineno = 0 win.refresh() @@ -105,6 +102,15 @@ def poll(interval): return (processes, procs_status) +def get_color(perc): + if perc <= 30: + return "green" + elif perc <= 80: + return "yellow" + else: + return "red" + + def print_header(procs_status, num_procs): """Print system-related info, above the process list.""" @@ -117,8 +123,8 @@ def get_dashes(perc): percs = psutil.cpu_percent(interval=0, percpu=True) for cpu_num, perc in enumerate(percs): dashes, empty_dashes = get_dashes(perc) - print_line(" CPU%-2s [%s%s] %5s%%" % (cpu_num, dashes, empty_dashes, - perc)) + line = " CPU%-2s [%s%s] %5s%%" % (cpu_num, dashes, empty_dashes, perc) + printl(line, color=get_color(perc)) mem = psutil.virtual_memory() dashes, empty_dashes = get_dashes(mem.percent) line = " Mem [%s%s] %5s%% %6s / %s" % ( @@ -127,7 +133,7 @@ def get_dashes(perc): str(int(mem.used / 1024 / 1024)) + "M", str(int(mem.total / 1024 / 1024)) + "M" ) - print_line(line) + printl(line, color=get_color(mem.percent)) # swap usage swap = psutil.swap_memory() @@ -138,7 +144,7 @@ def get_dashes(perc): str(int(swap.used / 1024 / 1024)) + "M", str(int(swap.total / 1024 / 1024)) + "M" ) - print_line(line) + printl(line, color=get_color(swap.percent)) # processes number and status st = [] @@ -146,14 +152,14 @@ def get_dashes(perc): if y: st.append("%s=%s" % (x, y)) st.sort(key=lambda x: x[:3] in ('run', 'sle'), reverse=1) - print_line(" Processes: %s (%s)" % (num_procs, ', '.join(st))) + printl(" Processes: %s (%s)" % (num_procs, ', '.join(st))) # load average, uptime uptime = datetime.datetime.now() - \ datetime.datetime.fromtimestamp(psutil.boot_time()) av1, av2, av3 = psutil.getloadavg() line = " Load average: %.2f %.2f %.2f Uptime: %s" \ % (av1, av2, av3, str(uptime).split('.')[0]) - print_line(line) + printl(line) def refresh_window(procs, procs_status): @@ -164,8 +170,8 @@ def refresh_window(procs, procs_status): header = templ % ("PID", "USER", "NI", "VIRT", "RES", "CPU%", "MEM%", "TIME+", "NAME") print_header(procs_status, len(procs)) - print_line("") - print_line(header, highlight=True) + printl("") + printl(header, bold=True, highlight=True) for p in procs: # TIME+ column shows process CPU cumulative time and it # is expressed as: "mm:ss.ms" @@ -197,21 +203,42 @@ def refresh_window(procs, procs_status): p.dict['name'] or '', ) try: - print_line(line) + printl(line) except curses.error: break win.refresh() +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(0, curses.COLORS): + curses.init_pair(i + 1, i, -1) + curses.endwin() + win.nodelay(1) + + +def tear_down(): + win.keypad(0) + curses.nocbreak() + curses.echo() + curses.endwin() + + def main(): + setup() try: interval = 0 while True: + if win.getch() == ord('q'): + break args = poll(interval) refresh_window(*args) interval = 1 except (KeyboardInterrupt, SystemExit): pass + finally: + tear_down() if __name__ == '__main__': From ecdaac33f5945be19090531be892791ac99fc597 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 Oct 2020 22:35:03 +0200 Subject: [PATCH 0626/1714] use python 3 as example in docstrings --- scripts/battery.py | 2 +- scripts/cpu_distribution.py | 2 +- scripts/disk_usage.py | 2 +- scripts/free.py | 2 +- scripts/ifconfig.py | 2 +- scripts/meminfo.py | 2 +- scripts/netstat.py | 2 +- scripts/pmap.py | 2 +- scripts/procinfo.py | 2 +- scripts/ps.py | 2 +- scripts/pstree.py | 2 +- scripts/sensors.py | 2 +- scripts/temperatures.py | 2 +- scripts/who.py | 2 +- scripts/winservices.py | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/scripts/battery.py b/scripts/battery.py index 0da2b95889..edf4ce8c97 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -7,7 +7,7 @@ """ Show battery information. -$ python scripts/battery.py +$ python3 scripts/battery.py charge: 74% left: 2:11:31 status: discharging diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index 08997797c3..fb39d888ad 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -7,7 +7,7 @@ """ Shows CPU workload split across different CPUs. -$ python scripts/cpu_workload.py +$ python3 scripts/cpu_workload.py CPU 0 CPU 1 CPU 2 CPU 3 CPU 4 CPU 5 CPU 6 CPU 7 19.8 20.6 18.2 15.8 6.9 17.3 5.0 20.4 gvfsd pytho kwork chrom unity kwork kwork kwork diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 901dbf8c22..851ae9b166 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -7,7 +7,7 @@ """ List all mounted disk partitions a-la "df -h" command. -$ python scripts/disk_usage.py +$ python3 scripts/disk_usage.py Device Total Used Free Use % Type Mount /dev/sdb3 18.9G 14.7G 3.3G 77% ext4 / /dev/sda6 345.9G 83.8G 244.5G 24% ext4 /home diff --git a/scripts/free.py b/scripts/free.py index 000323c5dd..8c3359d869 100755 --- a/scripts/free.py +++ b/scripts/free.py @@ -7,7 +7,7 @@ """ A clone of 'free' cmdline utility. -$ python scripts/free.py +$ python3 scripts/free.py total used free shared buffers cache Mem: 10125520 8625996 1499524 0 349500 3307836 Swap: 0 0 0 diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index cfd02f0daf..ae137fb482 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -7,7 +7,7 @@ """ A clone of 'ifconfig' on UNIX. -$ python scripts/ifconfig.py +$ python3 scripts/ifconfig.py lo: stats : speed=0MB, duplex=?, mtu=65536, up=yes incoming : bytes=1.95M, pkts=22158, errs=0, drops=0 diff --git a/scripts/meminfo.py b/scripts/meminfo.py index 550fcd0128..b98aa60be2 100755 --- a/scripts/meminfo.py +++ b/scripts/meminfo.py @@ -7,7 +7,7 @@ """ Print system memory information. -$ python scripts/meminfo.py +$ python3 scripts/meminfo.py MEMORY ------ Total : 9.7G diff --git a/scripts/netstat.py b/scripts/netstat.py index fe3bfe402c..5a21358e79 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -7,7 +7,7 @@ """ A clone of 'netstat -antp' on Linux. -$ python scripts/netstat.py +$ python3 scripts/netstat.py Proto Local address Remote address Status PID Program name tcp 127.0.0.1:48256 127.0.0.1:45884 ESTABLISHED 13646 chrome tcp 127.0.0.1:47073 127.0.0.1:45884 ESTABLISHED 13646 chrome diff --git a/scripts/pmap.py b/scripts/pmap.py index 5f7246a159..459927bfd2 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -8,7 +8,7 @@ A clone of 'pmap' utility on Linux, 'vmmap' on macOS and 'procstat -v' on BSD. Report memory map of a process. -$ python scripts/pmap.py 32402 +$ python3 scripts/pmap.py 32402 Address RSS Mode Mapping 0000000000400000 1200K r-xp /usr/bin/python2.7 0000000000838000 4K r--p /usr/bin/python2.7 diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 01974513f0..f060538686 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -8,7 +8,7 @@ Print detailed information about a process. Author: Giampaolo Rodola' -$ python scripts/procinfo.py +$ python3 scripts/procinfo.py pid 4600 name chrome parent 4554 (bash) diff --git a/scripts/ps.py b/scripts/ps.py index 540c032a7f..a234209fb0 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -7,7 +7,7 @@ """ A clone of 'ps aux'. -$ python scripts/ps.py +$ python3 scripts/ps.py USER PID %MEM VSZ RSS NICE STATUS START TIME CMDLINE root 1 0.0 220.9M 6.5M sleep Mar27 09:10 /lib/systemd root 2 0.0 0.0B 0.0B sleep Mar27 00:00 kthreadd diff --git a/scripts/pstree.py b/scripts/pstree.py index 0005e4a181..dba9f1bdd4 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -8,7 +8,7 @@ Similar to 'ps aux --forest' on Linux, prints the process list as a tree structure. -$ python scripts/pstree.py +$ python3 scripts/pstree.py 0 ? |- 1 init | |- 289 cgmanager diff --git a/scripts/sensors.py b/scripts/sensors.py index e532bebad1..911d7c9b43 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -9,7 +9,7 @@ A clone of 'sensors' utility on Linux printing hardware temperatures, fans speed and battery info. -$ python scripts/sensors.py +$ python3 scripts/sensors.py asus Temperatures: asus 57.0°C (high=None°C, critical=None°C) diff --git a/scripts/temperatures.py b/scripts/temperatures.py index e83df44036..f2dd51a73b 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -8,7 +8,7 @@ """ A clone of 'sensors' utility on Linux printing hardware temperatures. -$ python scripts/sensors.py +$ python3 scripts/sensors.py asus asus 47.0 °C (high = None °C, critical = None °C) diff --git a/scripts/who.py b/scripts/who.py index c2299eb09e..c1e407299e 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -8,7 +8,7 @@ A clone of 'who' command; print information about users who are currently logged in. -$ python scripts/who.py +$ python3 scripts/who.py giampaolo console 2017-03-25 22:24 loginwindow giampaolo ttys000 2017-03-25 23:28 (10.0.2.2) sshd """ diff --git a/scripts/winservices.py b/scripts/winservices.py index 8792f752e4..5c710159be 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -7,7 +7,7 @@ r""" List all Windows services installed. -$ python scripts/winservices.py +$ python3 scripts/winservices.py AeLookupSvc (Application Experience) status: stopped, start: manual, username: localSystem, pid: None binpath: C:\Windows\system32\svchost.exe -k netsvcs From d08f44b5448ce7c86bd044af9be289e0208d5e3e Mon Sep 17 00:00:00 2001 From: Piotr Dworzynski Date: Fri, 9 Oct 2020 17:06:01 +0200 Subject: [PATCH 0627/1714] Update index.rst (#1835) Fixed typo in documentation of user.name attribute. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index fa55b02559..97d6b4203f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -822,7 +822,7 @@ Other system info Return users currently connected on the system as a list of named tuples including the following fields: - - **user**: the name of the user. + - **name**: the name of the user. - **terminal**: the tty or pseudo-tty associated with the user, if any, else ``None``. - **host**: the host name associated with the entry, if any. From 21339fd40cf4fad71c39552e3813286f3068ae27 Mon Sep 17 00:00:00 2001 From: kellurs Date: Thu, 15 Oct 2020 10:40:59 -0600 Subject: [PATCH 0628/1714] Fix labeling in Process cpu_times() documentation (#1853) Changed system_user to children_system to match the named tupple and expected behaviour. Change description to match. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 97d6b4203f..3c9a4b6596 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1426,7 +1426,7 @@ Process class - **system**: time spent in kernel mode. - **children_user**: user time of all child processes (always ``0`` on Windows and macOS). - - **system_user**: user time of all child processes (always ``0`` on + - **children_system**: system time of all child processes (always ``0`` on Windows and macOS). - **iowait**: (Linux) time spent waiting for blocking I/O to complete. This value is excluded from `user` and `system` times count (because the From a8cd5893a920adcd5b8922b80748461fbea8dc1c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 00:53:59 +0200 Subject: [PATCH 0629/1714] pypi download stats script --- .ci/travis/install.sh | 12 +- .github/FUNDING.yml | 4 +- .github/lock.yml | 35 ++++ .github/workflows/build_wheel.yml | 50 +---- CREDITS | 184 +++++++++++------- HISTORY.rst | 37 +++- MANIFEST.in | 6 +- Makefile | 40 ++-- README.rst | 31 +-- docs/index.rst | 35 ++-- psutil/__init__.py | 4 +- psutil/_psbsd.py | 27 +-- psutil/_pslinux.py | 22 ++- psutil/_psosx.py | 15 +- psutil/_psutil_bsd.c | 150 +++++++++++++- psutil/_psutil_common.c | 31 ++- psutil/_psutil_common.h | 6 + psutil/_psutil_linux.c | 2 + psutil/_psutil_posix.c | 8 +- psutil/_psutil_windows.c | 66 ++++--- psutil/arch/freebsd/specific.c | 4 +- psutil/arch/freebsd/sys_socks.c | 5 +- psutil/arch/openbsd/specific.c | 14 -- psutil/arch/windows/ntextapi.h | 126 ++++++++++-- psutil/arch/windows/wmi.c | 2 +- psutil/tests/__init__.py | 13 +- psutil/tests/runner.py | 3 +- psutil/tests/test_connections.py | 5 + psutil/tests/test_contracts.py | 6 +- psutil/tests/test_linux.py | 80 ++++++-- psutil/tests/test_misc.py | 2 + psutil/tests/test_process.py | 6 +- psutil/tests/test_windows.py | 1 + scripts/battery.py | 2 +- scripts/cpu_distribution.py | 2 +- scripts/disk_usage.py | 2 +- scripts/free.py | 2 +- scripts/ifconfig.py | 2 +- scripts/internal/download_wheels.py | 154 --------------- ..._wheels.py => download_wheels_appveyor.py} | 26 +-- scripts/internal/download_wheels_github.py | 81 ++++++++ scripts/internal/print_announce.py | 2 +- scripts/internal/print_downloads.py | 154 +++++++++++++++ scripts/internal/print_wheels.py | 66 +++++++ scripts/iotop.py | 46 +++-- scripts/meminfo.py | 2 +- scripts/netstat.py | 2 +- scripts/nettop.py | 57 +++--- scripts/pmap.py | 2 +- scripts/procinfo.py | 2 +- scripts/ps.py | 2 +- scripts/pstree.py | 2 +- scripts/sensors.py | 2 +- scripts/temperatures.py | 2 +- scripts/top.py | 79 +++++--- scripts/who.py | 2 +- scripts/winservices.py | 2 +- setup.py | 2 +- 58 files changed, 1187 insertions(+), 542 deletions(-) create mode 100644 .github/lock.yml delete mode 100644 scripts/internal/download_wheels.py rename scripts/internal/{win_download_wheels.py => download_wheels_appveyor.py} (88%) create mode 100755 scripts/internal/download_wheels_github.py create mode 100755 scripts/internal/print_downloads.py create mode 100755 scripts/internal/print_wheels.py diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index f06e43d575..16bd5c0c70 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -24,13 +24,21 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then pyenv install 3.6.6 pyenv virtualenv 3.6.6 psutil ;; + py37) + pyenv install 3.7.6 + pyenv virtualenv 3.7.6 psutil + ;; + py38) + pyenv install 3.8.2 + pyenv virtualenv 3.8.2 psutil + ;; esac pyenv rehash pyenv activate psutil fi if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] || [[ $PYVER == 'py27' ]]; then - pip install -U ipaddress mock + pip install -U ipaddress mock unittest2 fi -pip install -U coverage coveralls flake8 setuptools concurrencytest +pip install -U coverage coveralls flake8 setuptools diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index dd130351d7..03c7c77c17 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,7 +3,7 @@ tidelift: "pypi/psutil" github: giampaolo patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username +open_collective: psutil ko_fi: # Replace with a single Ko-fi username community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -custom: # Replace with a single custom sponsorship URL +custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 0000000000..7099c810c4 --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,35 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 3 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: false + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: false + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 7d230b900e..ec784ed51e 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -1,57 +1,19 @@ -name: Build wheel +name: Build wheels on: [push, pull_request] jobs: - wheel_without_test: - name: build wheel for ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-latest, macos-latest, ubuntu-latest] - env: - CIBW_SKIP: "pp27-*win* cp27-*manylinux* pp-*manylinux*" - CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 - CIBW_MANYLINUX_I686_IMAGE: manylinux2014 - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 - name: Install Python 3.7 - with: - python-version: '3.7' - - - name: Install Visual C++ for Python 2.7 - if: startsWith(matrix.os, 'windows') - run: | - choco install vcpython27 -f -y - - - name: "install cibuildwheel" - run: pip install cibuildwheel==1.4.1 - - - name: build wheel - run: cibuildwheel . - - - name: Upload wheels - uses: actions/upload-artifact@v1 - with: - name: wheels2 - path: wheelhouse - wheel: - name: build wheel for ${{ matrix.os }} + name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [windows-latest, macos-latest, ubuntu-latest] + os: [macos-latest, ubuntu-latest] env: - CIBW_SKIP: "pp27-*win* *27* cp27-*manylinux* pp-*manylinux*" - CIBW_TEST_COMMAND: python -Wa {project}/psutil/tests/runner.py + CIBW_TEST_COMMAND: python -u -Wa {project}/psutil/tests/runner.py CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py CIBW_TEST_EXTRAS: test - CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 - CIBW_MANYLINUX_I686_IMAGE: manylinux2014 steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 @@ -64,10 +26,10 @@ jobs: run: | choco install vcpython27 -f -y - - name: "install cibuildwheel" + - name: Install cibuildwheel run: pip install cibuildwheel==1.4.1 - - name: build wheel + - name: Build wheels run: cibuildwheel . - name: Upload wheels diff --git a/CREDITS b/CREDITS index 690f5717fd..3c54d33aff 100644 --- a/CREDITS +++ b/CREDITS @@ -1,25 +1,26 @@ Intro -===== +------------------------------------------------------------------------------- I would like to recognize some of the people who have been instrumental in the development of psutil. I'm sure I'm forgetting somebody (feel free to email me) but here is a short list. It's modeled after the Linux CREDITS file where the fields are: name (N), e-mail (E), website (W), country (C), description (D), -(I) issues. Issue tracker is at https://github.com/giampaolo/psutil/issues). +(I) issues. Issue tracker is at: +https://github.com/giampaolo/psutil/issues. A big thanks to all of you. - Giampaolo Author -====== +------------------------------------------------------------------------------- N: Giampaolo Rodola C: Italy E: g.rodola@gmail.com -W: http://grodola.blogspot.com/ +W: https://gmpy.dev Experts -======= +------------------------------------------------------------------------------- Github usernames of people to CC on github when in need of help. @@ -45,7 +46,7 @@ Github usernames of people to CC on github when in need of help. - wiggin15, Arnon Yaari (maintainer) Top contributors -================ +------------------------------------------------------------------------------- N: Jay Loden C: NJ, USA @@ -69,7 +70,6 @@ W: https://github.com/mrjefftang I: 340, 529, 616, 653, 654, 648, 641 N: Jeremy Whitlock -E: jcscoobyrs@gmail.com D: great help with macOS C development. I: 125, 150, 174, 206 @@ -79,7 +79,6 @@ D: OpenBSD implementation. I: 615 N: Justin Venus -E: justin.venus@gmail.com D: Solaris support I: 18 @@ -93,28 +92,115 @@ W: https://github.com/ryoon D: NetBSD implementation (co-author). I: 557 +Donations +------------------------------------------------------------------------------- + +N: Rodion Stratov +C: Canada + +N: Remi Chateauneu +C: London, UK + +N: Olivier Grisel +C: Paris, France + +N: Praveen Bhamidipati +C: Bellevue, USA + +N: Willem de Groot +C: Netherlands + +N: Sigmund Vik + +N: Kahntent +C: NYC, USA + +N: Gyula Áfra +C: Budapest, Hungary + +N: Mahmut Dumlupinar + +N: Thomas Guettler +C: Germany + +N: Karthik Kumar +C: India + +N: Oche Ejembi +C: UK + +N: Russell Robinson +C: New Zealand + +N: Wompasoft +C: Texas, USA + +N: Amit Kulkarni +C: Santa Clara, USA + +N: Alexander Kaftan +C: Augsburg Germany + +N: Andrew Bays +C: Maynard, USA + +N: Carver Koella +C: Pittsburgh, USA + +N: Kristjan Võrk +C: Tallin, Estonia + +N: HTB Industries +C: Willow Springs, USA + +N: Brett Harris +C: Melbourne, Australia + +N: Peter Friedland +C: CT, USA + +N: Matthew Callow +C: Australia + +N: Marco Schrank +C: Germany + +N: Mindview LLC +C: USA + +N: Григорьев Андрей +C: Russia + +N: Heijdemann Morgan +C: Singapore + +N: Florian Bruhin +C: Winterthur, Switzerland + +N: Heijdemann Morgan +C: Singapore + +N: Morgan Heijdemann +C: Singapore + Contributors -============ +------------------------------------------------------------------------------- N: wj32 -E: wj32.64@gmail.com D: process username() and get_connections() on Windows I: 114, 115 N: Yan Raber C: Bologna, Italy -E: yanraber@gmail.com D: help on Windows development (initial version of Process.username()) N: Dave Daeschler C: USA -E: david.daeschler@gmail.com W: http://daviddaeschler.com D: some contributions to initial design/bootstrap plus occasional bug fixing I: 522, 536 N: cjgohlke -E: cjgohlke@gmail.com D: Windows 64 bit support I: 107 @@ -133,64 +219,49 @@ I: 1368, 1348 ---- N: Jeffery Kline -E: jeffery.kline@gmail.com I: 130 N: Grabriel Monnerat -E: gabrielmonnerat@gmail.com I: 146 N: Philip Roberts -E: philip.roberts@gmail.com I: 168 N: jcscoobyrs -E: jcscoobyrs@gmail.com I: 125 N: Sandro Tosi -E: sandro.tosi@gmail.com I: 200, 201 N: Andrew Colin -E: andrew.colin@gmail.com I: 248 N: Amoser -E: amoser@google.com I: 266, 267, 340 N: Matthew Grant -E: matthewgrant5@gmail.com I: 271 N: oweidner -E: oweidner@cct.lsu.edu I: 275 N: Tarek Ziade -E: ziade.tarek I: 281 N: Luca Cipriani C: Turin, Italy -E: luca.opensource@gmail.com I: 278 N: Maciej Lach, -E: maciej.lach@gmail.com I: 294 N: James Pye -E: james.pye@gmail.com I: 305, 306 N: Stanchev Emil -E: stanchev.emil I: 314 N: Kim Gräsman -E: kim.grasman@gmail.com D: ...also kindly donated some money. I: 316 @@ -199,15 +270,12 @@ C: Italy I: 318 N: Florent Xicluna -E: florent.xicluna@gmail.com I: 319 N: Michal Spondr -E: michal.spondr I: 313 N: Jean Sebastien -E: dumbboules@gmail.com I: 344 N: Rob Smith @@ -223,50 +291,39 @@ W: https://plus.google.com/116873264322260110710/posts I: 323 N: André Oriani -E: aoriani@gmail.com I: 361 N: clackwell -E: clackwell@gmail.com I: 356 N: m.malycha -E: m.malycha@gmail.com I: 351 N: John Baldwin -E: jhb@FreeBSD.org I: 370 N: Jan Beich -E: jbeich@tormail.org I: 325 N: floppymaster -E: floppymaster@gmail.com I: 380 N: Arfrever.FTA -E: Arfrever.FTA@gmail.com I: 369, 404 N: danudey -E: danudey@gmail.com I: 386 N: Adrien Fallou I: 224 N: Gisle Vanem -E: gisle.vanem@gmail.com I: 411 N: thepyr0 -E: thepyr0@gmail.com I: 414 N: John Pankov -E: john.pankov@gmail.com I: 435 N: Matt Good @@ -274,11 +331,9 @@ W: http://matt-good.net/ I: 438 N: Ulrich Klank -E: ulrich.klank@scitics.de I: 448 N: Josiah Carlson -E: josiah.carlson@gmail.com I: 451, 452 N: Raymond Hettinger @@ -291,41 +346,31 @@ M: Ken Seeho D: @cached_property decorator N: crusaderky -E: crusaderky@gmail.com I: 470, 477 -E: alex@mroja.net I: 471 N: Gautam Singh -E: gautam.singh@gmail.com I: 466 -E: lhn@hupfeldtit.dk I: 476, 479 N: Francois Charron -E: francois.charron.1@gmail.com I: 474 N: Naveed Roudsari -E: naveed.roudsari@gmail.com I: 421 N: Alexander Grothe -E: Alexander.Grothe@gmail.com I: 497 N: Szigeti Gabor Niif -E: szigeti.gabor.niif@gmail.com I: 446 N: msabramo -E: msabramo@gmail.com I: 492 N: Yaolong Huang -E: airekans@gmail.com W: http://airekans.github.io/ I: 530 @@ -335,18 +380,15 @@ I: 496 N: spacewander W: https://github.com/spacewander -E: spacewanderlzx@gmail.com I: 561, 603 N: Sylvain Mouquet -E: sylvain.mouquet@gmail.com I: 565 N: karthikrev I: 568 N: Bruno Binet -E: bruno.binet@gmail.com I: 572 N: Gabi Davar @@ -356,7 +398,6 @@ I: 578, 581, 587 N: spacewanderlzx C: Guangzhou,China -E: spacewanderlzx@gmail.com I: 555 N: Fabian Groffen @@ -373,8 +414,6 @@ C: Irvine, CA, US I: 614 N: Árni Már Jónsson -E: Reykjavik, Iceland -E: https://github.com/arnimarj I: 634 N: Bart van Kleef @@ -406,12 +445,10 @@ W: https://github.com/syohex I: 730 N: Visa Hankala -E: visa@openbsd.org I: 741 N: Sebastian-Gabriel Brestin C: Romania -E: sebastianbrestin@gmail.com I: 704 N: Timmy Konick @@ -427,11 +464,9 @@ W: https://github.com/wxwright I: 776 N: Farhan Khan -E: khanzf@gmail.com I: 823 N: Jake Omann -E: https://github.com/jomann09 I: 816, 775 N: Jeremy Humble @@ -448,7 +483,6 @@ I: 798 N: Andre Caron C: Montreal, QC, Canada -E: andre.l.caron@gmail.com W: https://github.com/AndreLouisCaron I: 880 @@ -466,7 +500,6 @@ I: 936, 1133 N: Pierre Fersing C: France -E: pierre.fersing@bleemeo.com I: 950 N: Thiago Borges Abdnur @@ -609,9 +642,8 @@ W: https://github.com/samertm I: 1480 N: Ammar Askar -E: ammar@ammaraskar.com W: http://ammaraskar.com/ -I: 604, 1484 +I: 604, 1484, 1781 N: agnewee W: https://github.com/Agnewee @@ -668,3 +700,19 @@ I: 1695 N: Michał Górny W: https://github.com/mgorny I: 1726 + +N: Julien Lebot +W: https://github.com/julien-lebot +I: 1768 + +N: Armin Gruner +W: https://github.com/ArminGruner +I: 1800 + +N: Chris Burger +W: https://github.com/phobozad +I: 1830 + +N: aristocratos +W: https://github.com/aristocratos +I: 1837, 1838 diff --git a/HISTORY.rst b/HISTORY.rst index 3bae76b4ab..1d760dbcd4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,12 +1,40 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.7.1 (unreleased) -================== +5.7.3 +===== XXXX-XX-XX **Enhancements** +- 893_: implement `Process.environ()` on BSD family. (patch by Armin Gruner) +- 1830_: [UNIX] `net_if_stats()`'s `isup` also checks whether the NIC is + running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) +- 1837_: [Linux] improved battery detection and charge "secsleft" calculation + (patch by aristocratos) + +**Bug fixes** + +- 1838_: [Linux] sensors_battery(): if `percent` can be determined but not + the remaining values, still return a result instead of None. + (patch by aristocratos) + +5.7.2 +===== + +2020-07-15 + +**Bug fixes** + +- wheels for 2.7 were inadvertently deleted. + +5.7.1 +===== + +2020-07-15 + +**Enhancements** + - 1729_: parallel tests on UNIX (make test-parallel). They're twice as fast! - 1741_: "make build/install" is now run in parallel and it's about 15% faster on UNIX. @@ -25,12 +53,17 @@ XXXX-XX-XX psutil.Process(pid=12739, name='python3', status='terminated', exitcode=, started='15:08:20') - 1757_: memory leak tests are now stable. +- 1768_: [Windows] added support for Windows Nano Server. (contributed by + Julien Lebot) **Bug fixes** - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. (patch by Michał Górny) - 1760_: [Linux] Process.rlimit() does not handle long long type properly. +- 1766_: [macOS] NoSuchProcess may be raised instead of ZombieProcess. +- 1781_: fix signature of callback function for getloadavg(). (patch by + Ammar Askar) 5.7.0 ===== diff --git a/MANIFEST.in b/MANIFEST.in index 93c4918011..b8c2064ea1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -117,17 +117,19 @@ include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py include scripts/internal/clinter.py -include scripts/internal/download_wheels.py +include scripts/internal/download_wheels_appveyor.py +include scripts/internal/download_wheels_github.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py +include scripts/internal/print_downloads.py include scripts/internal/print_timeline.py +include scripts/internal/print_wheels.py include scripts/internal/purge_installation.py include scripts/internal/tidelift.py -include scripts/internal/win_download_wheels.py include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py diff --git a/Makefile b/Makefile index e91dfd818a..c603da8a4f 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ DEPS = \ flake8 \ flake8-print \ pyperf \ + pypinfo \ requests \ setuptools \ twine \ @@ -37,7 +38,7 @@ BUILD_OPTS = `$(PYTHON) -c \ # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` -TEST_PREFIX = PYTHONWARNINGS=all PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_TESTING=1 PSUTIL_DEBUG=1 all: test @@ -207,6 +208,25 @@ install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit chmod +x .git/hooks/pre-commit +# =================================================================== +# Wheels +# =================================================================== + +download-wheels-appveyor: ## Download latest wheels hosted on appveyor. + $(PYTHON) scripts/internal/download_wheels_appveyor.py --user giampaolo --project psutil + +download-wheels-github: ## Download latest wheels hosted on github. + $(PYTHON) scripts/internal/download_wheels_github.py --user=giampaolo --project=psutil --tokenfile=~/.github.token + +download-wheels: ## Download wheels from github and appveyor + rm -rf dist + ${MAKE} download-wheels-appveyor + # ${MAKE} download-wheels-github + ${MAKE} print-wheels + +print-wheels: ## Print downloaded wheels + $(PYTHON) scripts/internal/print_wheels.py + # =================================================================== # Distribution # =================================================================== @@ -219,20 +239,11 @@ sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest $(PYTHON) setup.py sdist -wheel: ## Generate wheel. - $(PYTHON) setup.py bdist_wheel - -win-download-wheels: ## Download latest wheels hosted on appveyor. - $(PYTHON) scripts/internal/win_download_wheels.py --user giampaolo --project psutil - -download-wheels: ## Download latest wheels hosted on github. - $(PYTHON) scripts/internal/download_wheels.py --user=giampaolo --project=psutil --tokenfile=~/.github.token - upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist - $(PYTHON) setup.py sdist upload + $(PYTHON) -m twine upload dist/*.tar.gz -upload-win-wheels: ## Upload wheels in dist/* directory on PyPI. +upload-wheels: ## Upload wheels in dist/* directory on PyPI. $(PYTHON) -m twine upload dist/*.whl # --- others @@ -253,7 +264,7 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} install ${MAKE} generate-manifest git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain - ${MAKE} win-download-wheels + ${MAKE} download-wheels ${MAKE} sdist $(PYTHON) -c \ "from psutil import __version__ as ver; \ @@ -294,6 +305,9 @@ print-api-speed: ## Benchmark all API calls ${MAKE} build @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) +print-downloads: ## Print PYPI download statistics + $(PYTHON) scripts/internal/print_downloads.py + # =================================================================== # Misc # =================================================================== diff --git a/README.rst b/README.rst index 2137a1a281..31119c3141 100644 --- a/README.rst +++ b/README.rst @@ -77,7 +77,7 @@ Quick links - `Download `_ - `Forum `_ - `StackOverflow `_ -- `Blog `_ +- `Blog `_ - `Development guide `_ - `What's new `_ @@ -100,7 +100,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**. `PyPy3 `__ is also known to work. +...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**, `PyPy `__ 2.7 and 3.X. psutil for enterprise ===================== @@ -131,19 +131,12 @@ Security To report a security vulnerability, please use the `Tidelift security contact`_. Tidelift will coordinate the fix and disclosure. -Example applications -==================== - -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ -| .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/procinfo-small.png | .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/top-small.png | -| :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/procinfo.png | :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/top.png | -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ -| .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/procsmem-small.png | .. image:: https://github.com/giampaolo/psutil/blob/master/docs/_static/pmap-small.png | -| :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/procsmem.png | :target: https://github.com/giampaolo/psutil/blob/master/docs/_static/pmap.png | -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ +Sponsorship +=========== -Also see `scripts directory `__ -and `doc recipes `__. +A lot of time and effort went into making psutil as it is today. If you whish +to help its future development consider +`sponsoring `__ it. Projects using psutil ===================== @@ -154,9 +147,7 @@ psutil has roughly the following monthly downloads: :target: https://pepy.tech/project/psutil :alt: Downloads -There are over -`10.000 open source projects `__ -on github which depend from psutil. +...and has over 55,000 projects on GitHub depending from it. Here's some I find particularly interesting: - https://github.com/google/grr @@ -166,16 +157,14 @@ Here's some I find particularly interesting: - https://github.com/ajenti/ajenti - https://github.com/home-assistant/home-assistant/ - Portings ======== - Go: https://github.com/shirou/gopsutil - C: https://github.com/hamon-in/cpslib -- Rust: https://github.com/borntyping/rust-psutil +- Rust: https://github.com/rust-psutil/rust-psutil - Nim: https://github.com/johnscillieri/psutil-nim - Example usages ============== @@ -514,7 +503,7 @@ Windows services 'username': 'NT AUTHORITY\\LocalService'} -.. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html +.. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 .. _Tidelift security contact: https://tidelift.com/security .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme diff --git a/docs/index.rst b/docs/index.rst index 699ea1f161..97d6b4203f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ Quick links - `Home page `__ - `Install `_ -- `Blog `__ +- `Blog `__ - `Forum `__ - `Download `__ - `Development guide `_ @@ -268,16 +268,17 @@ CPU .. function:: getloadavg() Return the average system load over the last 1, 5 and 15 minutes as a tuple. - The load represents the processes which are in a runnable state, either + The "load" represents the processes which are in a runnable state, either using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). On UNIX systems this relies on `os.getloadavg`_. On Windows this is emulated by using a Windows API that spawns a thread which keeps running in - background and updates the load average every 5 seconds, mimicking the UNIX - behavior. Thus, the first time this is called and for the next 5 seconds + background and updates results every 5 seconds, mimicking the UNIX behavior. + Thus, on Windows, the first time this is called and for the next 5 seconds it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. The numbers returned only make sense if related to the number of CPU cores - installed on the system. So, for instance, `3.14` on a system with 10 CPU - cores means that the system load was 31.4% percent over the last N minutes. + installed on the system. So, for instance, a value of `3.14` on a system + with 10 logical CPUs means that the system load was 31.4% percent over the + last N minutes. .. code-block:: python @@ -686,7 +687,8 @@ Network system as a dictionary whose keys are the NIC names and value is a named tuple with the following fields: - - **isup**: a bool indicating whether the NIC is up and running. + - **isup**: a bool indicating whether the NIC is up and running (meaning + ethernet cable or Wi-Fi is connected). - **duplex**: the duplex communication type; it can be either :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or :const:`NIC_DUPLEX_UNKNOWN`. @@ -705,6 +707,7 @@ Network .. versionadded:: 3.0.0 + .. versionchanged:: 5.7.3 `isup` on UNIX also checks whether the NIC is running. Sensors ------- @@ -819,7 +822,7 @@ Other system info Return users currently connected on the system as a list of named tuples including the following fields: - - **user**: the name of the user. + - **name**: the name of the user. - **terminal**: the tty or pseudo-tty associated with the user, if any, else ``None``. - **host**: the host name associated with the entry, if any. @@ -1143,11 +1146,10 @@ Process class >>> psutil.Process().environ() {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} - Availability: Linux, macOS, Windows, SunOS - .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support .. versionchanged:: 5.6.3 added AIX suport + .. versionchanged:: 5.7.3 added BSD suport .. method:: create_time() @@ -1626,7 +1628,7 @@ Process class (USS, PSS and swap). The additional metrics provide a better representation of "effective" process memory consumption (in case of USS) as explained in detail in this - `blog post `__. + `blog post `__. It does so by passing through the whole process address. As such it usually requires higher user privileges than :meth:`memory_info` and is considerably slower. @@ -2492,6 +2494,7 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= +* psutil 5.7.1 (2020-07): **Windows Nano** * psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support * psutil 5.7.0 (2020-02): **PyPy** on Windows * psutil 5.4.0 (2017-11): **AIX** @@ -2506,6 +2509,14 @@ Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. Timeline ======== +- 2020-07-15: + `5.7.2 `__ - + `what's new `__ - + `diff `__ +- 2020-07-15: + `5.7.1 `__ - + `what's new `__ - + `diff `__ - 2020-02-18: `5.7.0 `__ - `what's new `__ - @@ -2838,7 +2849,7 @@ Timeline .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ .. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass .. _`GetExitCodeProcess`: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess -.. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html +.. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`hash`: https://docs.python.org/3/library/functions.html#hash .. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get diff --git a/psutil/__init__.py b/psutil/__init__.py index 7fdbc0fc26..9d43f991d0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -226,7 +226,7 @@ __all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" -__version__ = "5.7.1" +__version__ = "5.7.3" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -853,7 +853,7 @@ def cpu_num(self): """ return self._proc.cpu_num() - # Linux, macOS, Windows, Solaris, AIX + # All platforms has it, but maybe not in the future. if hasattr(_psplatform.Process, "environ"): def environ(self): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 49ad1e995c..9565406b10 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -51,7 +51,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, @@ -76,12 +76,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 = { @@ -354,7 +353,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + isup = cext_posix.net_if_is_running(name) duplex, speed = cext_posix.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 @@ -551,10 +550,10 @@ def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except ProcessLookupError: - 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) + else: + raise NoSuchProcess(self.pid, self._name) except PermissionError: raise AccessDenied(self.pid, self._name) except OSError: @@ -576,10 +575,10 @@ def wrap_exceptions_procfs(inst): # 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(inst.pid): - raise NoSuchProcess(inst.pid, inst._name) - else: + 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) @@ -669,6 +668,10 @@ def cmdline(self): 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']] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 3e3caace78..3c9ff28110 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -843,10 +843,6 @@ 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 range(0, 16, 4))) ip = base64.b16decode(ip) try: # see: https://github.com/giampaolo/psutil/issues/201 @@ -1031,7 +1027,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + isup = cext_posix.net_if_is_running(name) duplex, speed = cext.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 @@ -1340,7 +1336,8 @@ def multi_cat(*paths): return int(ret) if ret.isdigit() else ret 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 @@ -1358,12 +1355,11 @@ def multi_cat(*paths): energy_full = multi_cat( root + "/energy_full", root + "/charge_full") - if energy_now is None or power_now is None: - return None + time_to_empty = multi_cat(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: @@ -1395,11 +1391,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) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index e4296495c4..6a1899316f 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -262,7 +262,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + isup = cext_posix.net_if_is_running(name) duplex, speed = cext_posix.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 @@ -324,6 +324,14 @@ 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 Exception: + return False + + def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. @@ -333,7 +341,10 @@ def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except ProcessLookupError: - raise NoSuchProcess(self.pid, self._name) + 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 cext.ZombieProcessError: diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 953fcd083c..c4450d7dfc 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -57,6 +57,7 @@ #include #include // process open files/connections #include +#include #include "_psutil_common.h" #include "_psutil_posix.h" @@ -391,6 +392,145 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { } +/* + * Return process environment as a Python dictionary + */ +PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + int i, cnt = -1; + long pid; + char *s, **envs, errbuf[_POSIX2_LINE_MAX]; + PyObject *py_value=NULL, *py_retdict=NULL; + kvm_t *kd; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 *p; +#else + struct kinfo_proc *p; +#endif + + if (!PyArg_ParseTuple(args, "l", &pid)) + return NULL; + +#if defined(PSUTIL_FREEBSD) + kd = kvm_openfiles(NULL, "/dev/null", NULL, 0, errbuf); +#else + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); +#endif + if (!kd) { + convert_kvm_err("kvm_openfiles", errbuf); + return NULL; + } + + py_retdict = PyDict_New(); + if (!py_retdict) + goto error; + +#if defined(PSUTIL_FREEBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, &cnt); +#elif defined(PSUTIL_OPENBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#elif defined(PSUTIL_NETBSD) + p = kvm_getproc2(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#endif + if (!p) { + NoSuchProcess("kvm_getprocs"); + goto error; + } + if (cnt <= 0) { + NoSuchProcess(cnt < 0 ? kvm_geterr(kd) : "kvm_getprocs: cnt==0"); + goto error; + } + + // On *BSD kernels there are a few kernel-only system processes without an + // environment (See e.g. "procstat -e 0 | 1 | 2 ..." on FreeBSD.) + // + // Some system process have no stats attached at all + // (they are marked with P_SYSTEM.) + // + // On FreeBSD, it's possible that the process is swapped or paged out, + // then there no access to the environ stored in the process' user area. + // + // On NetBSD, we cannot call kvm_getenvv2() for a zombie process. + // + // To make unittest suite happy, return an empty environment. + // +#if defined(PSUTIL_FREEBSD) +#if (defined(__FreeBSD_version) && __FreeBSD_version >= 700000) + if (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) { +#else + if ((p)->ki_flag & P_SYSTEM) { +#endif +#elif defined(PSUTIL_NETBSD) + if ((p)->p_stat == SZOMB) { +#elif defined(PSUTIL_OPENBSD) + if ((p)->p_flag & P_SYSTEM) { +#endif + kvm_close(kd); + return py_retdict; + } + +#if defined(PSUTIL_NETBSD) + envs = kvm_getenvv2(kd, p, 0); +#else + envs = kvm_getenvv(kd, p, 0); +#endif + if (!envs) { + // Map to "psutil" general high-level exceptions + switch (errno) { + case 0: + // Process has cleared it's environment, return empty one + kvm_close(kd); + return py_retdict; + case EPERM: + AccessDenied("kvm_getenvv"); + break; + case ESRCH: + NoSuchProcess("kvm_getenvv"); + break; +#if defined(PSUTIL_FREEBSD) + case ENOMEM: + // Unfortunately, under FreeBSD kvm_getenvv() returns + // failure for certain processes ( e.g. try + // "sudo procstat -e ".) + // Map the error condition to 'AccessDenied'. + sprintf(errbuf, + "kvm_getenvv(pid=%ld, ki_uid=%d): errno=ENOMEM", + pid, p->ki_uid); + AccessDenied(errbuf); + break; +#endif + default: + sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); + PyErr_SetFromOSErrnoWithSyscall(errbuf); + break; + } + goto error; + } + + for (i = 0; envs[i] != NULL; i++) { + s = strchr(envs[i], '='); + if (!s) + continue; + *s++ = 0; + py_value = PyUnicode_DecodeFSDefault(s); + if (!py_value) + goto error; + if (PyDict_SetItemString(py_retdict, envs[i], py_value)) { + goto error; + } + Py_DECREF(py_value); + } + + kvm_close(kd); + return py_retdict; + +error: + Py_XDECREF(py_value); + Py_XDECREF(py_retdict); + kvm_close(kd); + return NULL; +} + /* * Return the number of logical CPUs in the system. * XXX this could be shared with macOS @@ -617,8 +757,10 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",softdep", sizeof(opts)); if (flags & MNT_NOSYMFOLLOW) strlcat(opts, ",nosymfollow", sizeof(opts)); +#ifdef MNT_GJOURNAL if (flags & MNT_GJOURNAL) strlcat(opts, ",gjournal", sizeof(opts)); +#endif if (flags & MNT_MULTILABEL) strlcat(opts, ",multilabel", sizeof(opts)); if (flags & MNT_ACLS) @@ -627,8 +769,10 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",noclusterr", sizeof(opts)); if (flags & MNT_NOCLUSTERW) strlcat(opts, ",noclusterw", sizeof(opts)); +#ifdef MNT_NFS4ACLS if (flags & MNT_NFS4ACLS) strlcat(opts, ",nfs4acls", sizeof(opts)); +#endif #elif PSUTIL_NETBSD if (flags & MNT_NODEV) strlcat(opts, ",nodev", sizeof(opts)); @@ -831,7 +975,7 @@ psutil_users(PyObject *self, PyObject *args) { py_tty, // tty py_hostname, // hostname (float)ut.ut_time, // start time -#ifdef PSUTIL_OPENBSD +#if defined(PSUTIL_OPENBSD) || (defined(__FreeBSD_version) && __FreeBSD_version < 900000) -1 // process id (set to None later) #else ut.ut_pid // TODO: use PyLong_FromPid @@ -956,6 +1100,8 @@ static PyMethodDef mod_methods[] = { {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, "Return an XML string to determine the number physical CPUs."}, #endif + {"proc_environ", psutil_proc_environ, METH_VARARGS, + "Return process environment"}, // --- system-related functions @@ -1060,7 +1206,9 @@ static PyMethodDef mod_methods[] = { if (PyModule_AddIntConstant(mod, "SSLEEP", LSSLEEP)) INITERR; if (PyModule_AddIntConstant(mod, "SSTOP", LSSTOP)) INITERR; if (PyModule_AddIntConstant(mod, "SZOMB", LSZOMB)) INITERR; +#if __NetBSD_Version__ < 500000000 if (PyModule_AddIntConstant(mod, "SDEAD", LSDEAD)) INITERR; +#endif if (PyModule_AddIntConstant(mod, "SONPROC", LSONPROC)) INITERR; // unique to NetBSD if (PyModule_AddIntConstant(mod, "SSUSPENDED", LSSUSPENDED)) INITERR; diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index d63b4d9c51..f821aba380 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -167,6 +167,26 @@ psutil_setup(void) { } +// ============================================================================ +// Utility functions (BSD) +// ============================================================================ + +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +void +convert_kvm_err(const char *syscall, char *errbuf) { + char fullmsg[8192]; + + sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); + if (strstr(errbuf, "Permission denied") != NULL) + AccessDenied(fullmsg); + else if (strstr(errbuf, "Operation not permitted") != NULL) + AccessDenied(fullmsg); + else + PyErr_Format(PyExc_RuntimeError, fullmsg); +} +#endif + + // ==================================================================== // --- Windows // ==================================================================== @@ -264,10 +284,6 @@ psutil_loadlibs() { "ntdll.dll", "NtSetInformationProcess"); if (! NtSetInformationProcess) return 1; - WinStationQueryInformationW = psutil_GetProcAddressFromLib( - "winsta.dll", "WinStationQueryInformationW"); - if (! WinStationQueryInformationW) - return 1; NtQueryObject = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQueryObject"); if (! NtQueryObject) @@ -320,6 +336,13 @@ psutil_loadlibs() { // minumum requirement: Win 7 GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( "kernel32", "GetLogicalProcessorInformationEx"); + // minimum requirements: Windows Server Core + WTSEnumerateSessionsW = psutil_GetProcAddressFromLib( + "wtsapi32.dll", "WTSEnumerateSessionsW"); + WTSQuerySessionInformationW = psutil_GetProcAddressFromLib( + "wtsapi32.dll", "WTSQuerySessionInformationW"); + WTSFreeMemory = psutil_GetProcAddressFromLib( + "wtsapi32.dll", "WTSFreeMemory"); PyErr_Clear(); return 0; diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 2408c9f63e..3162772eb2 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -104,6 +104,12 @@ PyObject* psutil_set_testing(PyObject *self, PyObject *args); void psutil_debug(const char* format, ...); int psutil_setup(void); +// ==================================================================== +// --- BSD +// ==================================================================== + +void convert_kvm_err(const char *syscall, char *errbuf); + // ==================================================================== // --- Windows // ==================================================================== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index c94ec03502..415474388d 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -662,6 +662,8 @@ static PyMethodDef mod_methods[] = { if (PyModule_AddIntConstant(mod, "DUPLEX_FULL", DUPLEX_FULL)) INITERR; if (PyModule_AddIntConstant(mod, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN)) INITERR; + psutil_setup(); + if (mod == NULL) INITERR; #if PY_MAJOR_VERSION >= 3 diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index f7f8b92dd8..1182765d7e 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -385,7 +385,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { * http://www.i-scream.org/libstatgrab/ */ static PyObject * -psutil_net_if_flags(PyObject *self, PyObject *args) { +psutil_net_if_is_running(PyObject *self, PyObject *args) { char *nic_name; int sock = -1; int ret; @@ -404,7 +404,7 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { goto error; close(sock); - if ((ifr.ifr_flags & IFF_UP) != 0) + if ((ifr.ifr_flags & IFF_RUNNING) != 0) return Py_BuildValue("O", Py_True); else return Py_BuildValue("O", Py_False); @@ -621,8 +621,8 @@ static PyMethodDef mod_methods[] = { "Retrieve NICs information"}, {"net_if_mtu", psutil_net_if_mtu, METH_VARARGS, "Retrieve NIC MTU"}, - {"net_if_flags", psutil_net_if_flags, METH_VARARGS, - "Retrieve NIC flags"}, + {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS, + "Return True if the NIC is running."}, #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, "Return NIC stats."}, diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index be4759ac1c..c8b2f38355 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -22,7 +22,6 @@ #include // memory_info(), memory_maps() #include #include // threads(), PROCESSENTRY32 -#include // users() // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") @@ -1191,17 +1190,17 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { static PyObject * psutil_users(PyObject *self, PyObject *args) { HANDLE hServer = WTS_CURRENT_SERVER_HANDLE; - WCHAR *buffer_user = NULL; - LPTSTR buffer_addr = NULL; - PWTS_SESSION_INFO sessions = NULL; + LPWSTR buffer_user = NULL; + LPWSTR buffer_addr = NULL; + LPWSTR buffer_info = NULL; + PWTS_SESSION_INFOW sessions = NULL; DWORD count; DWORD i; DWORD sessionId; DWORD bytes; PWTS_CLIENT_ADDRESS address; char address_str[50]; - WINSTATION_INFO station_info; - ULONG returnLen; + PWTSINFOW wts_info; PyObject *py_tuple = NULL; PyObject *py_address = NULL; PyObject *py_username = NULL; @@ -1210,8 +1209,21 @@ psutil_users(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (WTSEnumerateSessions(hServer, 0, 1, &sessions, &count) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessions"); + if (WTSEnumerateSessionsW == NULL || + WTSQuerySessionInformationW == NULL || + WTSFreeMemory == NULL) { + // If we don't run in an environment that is a Remote Desktop Services environment + // the Wtsapi32 proc might not be present. + // https://docs.microsoft.com/en-us/windows/win32/termserv/run-time-linking-to-wtsapi32-dll + return py_retlist; + } + + if (WTSEnumerateSessionsW(hServer, 0, 1, &sessions, &count) == 0) { + if (ERROR_CALL_NOT_IMPLEMENTED == GetLastError()) { + // On Windows Nano server, the Wtsapi32 API can be present, but return WinError 120. + return py_retlist; + } + PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); goto error; } @@ -1223,9 +1235,12 @@ psutil_users(PyObject *self, PyObject *args) { WTSFreeMemory(buffer_user); if (buffer_addr != NULL) WTSFreeMemory(buffer_addr); + if (buffer_info != NULL) + WTSFreeMemory(buffer_info); buffer_user = NULL; buffer_addr = NULL; + buffer_info = NULL; // username bytes = 0; @@ -1239,21 +1254,22 @@ psutil_users(PyObject *self, PyObject *args) { // address bytes = 0; - if (WTSQuerySessionInformation(hServer, sessionId, WTSClientAddress, - &buffer_addr, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformation"); + if (WTSQuerySessionInformationW(hServer, sessionId, WTSClientAddress, + &buffer_addr, &bytes) == 0) { + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); goto error; } address = (PWTS_CLIENT_ADDRESS)buffer_addr; - if (address->AddressFamily == 0) { // AF_INET + if (address->AddressFamily == 2) { // AF_INET == 2 sprintf_s(address_str, _countof(address_str), "%u.%u.%u.%u", - address->Address[0], - address->Address[1], + // The IP address is offset by two bytes from the start of the Address member of the WTS_CLIENT_ADDRESS structure. address->Address[2], - address->Address[3]); + address->Address[3], + address->Address[4], + address->Address[5]); py_address = Py_BuildValue("s", address_str); if (!py_address) goto error; @@ -1263,26 +1279,23 @@ psutil_users(PyObject *self, PyObject *args) { } // login time - if (! WinStationQueryInformationW( - hServer, - sessionId, - WinStationInformation, - &station_info, - sizeof(station_info), - &returnLen)) - { - PyErr_SetFromOSErrnoWithSyscall("WinStationQueryInformationW"); + bytes = 0; + if (WTSQuerySessionInformationW(hServer, sessionId, WTSSessionInfo, + &buffer_info, &bytes) == 0) { + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); goto error; } + wts_info = (PWTSINFOW)buffer_info; py_username = PyUnicode_FromWideChar(buffer_user, wcslen(buffer_user)); if (py_username == NULL) goto error; + py_tuple = Py_BuildValue( "OOd", py_username, py_address, - psutil_FiletimeToUnixTime(station_info.ConnectTime) + psutil_LargeIntegerToUnixTime(wts_info->ConnectTime) ); if (!py_tuple) goto error; @@ -1296,6 +1309,7 @@ psutil_users(PyObject *self, PyObject *args) { WTSFreeMemory(sessions); WTSFreeMemory(buffer_user); WTSFreeMemory(buffer_addr); + WTSFreeMemory(buffer_info); return py_retlist; error: @@ -1310,6 +1324,8 @@ psutil_users(PyObject *self, PyObject *args) { WTSFreeMemory(buffer_user); if (buffer_addr != NULL) WTSFreeMemory(buffer_addr); + if (buffer_info != NULL) + WTSFreeMemory(buffer_info); return NULL; } diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 3f37a08e2d..c783264792 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -517,7 +517,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } -#if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 +#if defined(__FreeBSD_version) && __FreeBSD_version >= 701000 PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { pid_t pid; @@ -795,9 +795,11 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { case KVME_TYPE_DEAD: path = "[dead]"; break; +#ifdef KVME_TYPE_SG case KVME_TYPE_SG: path = "[sg]"; break; +#endif case KVME_TYPE_UNKNOWN: path = "[unknown]"; break; diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index ab61f393b9..9f7cf8d555 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -16,6 +16,9 @@ #include #include #include +#if defined(__FreeBSD_version) && __FreeBSD_version < 800000 +#include +#endif #include // for xinpcb struct #include #include @@ -30,7 +33,7 @@ static int psutil_nxfiles; int -psutil_populate_xfiles() { +psutil_populate_xfiles(void) { size_t len; if ((psutil_xfiles = malloc(len = sizeof *psutil_xfiles)) == NULL) { diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/specific.c index d97a8f9b93..aa4568f592 100644 --- a/psutil/arch/openbsd/specific.c +++ b/psutil/arch/openbsd/specific.c @@ -47,20 +47,6 @@ // ============================================================================ -static void -convert_kvm_err(const char *syscall, char *errbuf) { - char fullmsg[8192]; - - sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); - if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(fullmsg); - else if (strstr(errbuf, "Operation not permitted") != NULL) - AccessDenied(fullmsg); - else - PyErr_Format(PyExc_RuntimeError, fullmsg); -} - - int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // Fills a kinfo_proc struct based on process pid. diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 8cb00430e2..ea1f428167 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -19,6 +19,12 @@ typedef LONG NTSTATUS; #define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225L) #define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) +// WtsApi32.h +#define WTS_CURRENT_SERVER_HANDLE ((HANDLE)NULL) +#define WINSTATIONNAME_LENGTH 32 +#define DOMAIN_LENGTH 17 +#define USERNAME_LENGTH 20 + // ================================================================ // Enums // ================================================================ @@ -93,6 +99,53 @@ typedef enum _KWAIT_REASON { MaximumWaitReason } KWAIT_REASON, *PKWAIT_REASON; +// users() +typedef enum _WTS_INFO_CLASS { + WTSInitialProgram, + WTSApplicationName, + WTSWorkingDirectory, + WTSOEMId, + WTSSessionId, + WTSUserName, + WTSWinStationName, + WTSDomainName, + WTSConnectState, + WTSClientBuildNumber, + WTSClientName, + WTSClientDirectory, + WTSClientProductId, + WTSClientHardwareId, + WTSClientAddress, + WTSClientDisplay, + WTSClientProtocolType, + WTSIdleTime, + WTSLogonTime, + WTSIncomingBytes, + WTSOutgoingBytes, + WTSIncomingFrames, + WTSOutgoingFrames, + WTSClientInfo, + WTSSessionInfo, + WTSSessionInfoEx, + WTSConfigInfo, + WTSValidationInfo, // Info Class value used to fetch Validation Information through the WTSQuerySessionInformation + WTSSessionAddressV4, + WTSIsRemoteSession +} WTS_INFO_CLASS; + +typedef enum _WTS_CONNECTSTATE_CLASS { + WTSActive, // User logged on to WinStation + WTSConnected, // WinStation connected to client + WTSConnectQuery, // In the process of connecting to client + WTSShadow, // Shadowing another WinStation + WTSDisconnected, // WinStation logged on without client + WTSIdle, // Waiting for client to connect + WTSListen, // WinStation is listening for connection + WTSReset, // WinStation is being reset + WTSDown, // WinStation is down due to error + WTSInit, // WinStation in initialization +} WTS_CONNECTSTATE_CLASS; + // ================================================================ // Structs. // ================================================================ @@ -309,17 +362,42 @@ typedef struct { } RTL_USER_PROCESS_PARAMETERS_, *PRTL_USER_PROCESS_PARAMETERS_; // users() -typedef struct _WINSTATION_INFO { - BYTE Reserved1[72]; - ULONG SessionId; - BYTE Reserved2[4]; - FILETIME ConnectTime; - FILETIME DisconnectTime; - FILETIME LastInputTime; - FILETIME LoginTime; - BYTE Reserved3[1096]; - FILETIME CurrentTime; -} WINSTATION_INFO, *PWINSTATION_INFO; +typedef struct _WTS_SESSION_INFOW { + DWORD SessionId; // session id + LPWSTR pWinStationName; // name of WinStation this session is + // connected to + WTS_CONNECTSTATE_CLASS State; // connection state (see enum) +} WTS_SESSION_INFOW, * PWTS_SESSION_INFOW; + +#define PWTS_SESSION_INFO PWTS_SESSION_INFOW + +typedef struct _WTS_CLIENT_ADDRESS { + DWORD AddressFamily; // AF_INET, AF_INET6, AF_IPX, AF_NETBIOS, AF_UNSPEC + BYTE Address[20]; // client network address +} WTS_CLIENT_ADDRESS, * PWTS_CLIENT_ADDRESS; + +typedef struct _WTSINFOW { + WTS_CONNECTSTATE_CLASS State; // connection state (see enum) + DWORD SessionId; // session id + DWORD IncomingBytes; + DWORD OutgoingBytes; + DWORD IncomingFrames; + DWORD OutgoingFrames; + DWORD IncomingCompressedBytes; + DWORD OutgoingCompressedBytes; + WCHAR WinStationName[WINSTATIONNAME_LENGTH]; + WCHAR Domain[DOMAIN_LENGTH]; + WCHAR UserName[USERNAME_LENGTH + 1];// name of WinStation this session is + // connected to + LARGE_INTEGER ConnectTime; + LARGE_INTEGER DisconnectTime; + LARGE_INTEGER LastInputTime; + LARGE_INTEGER LogonTime; + LARGE_INTEGER CurrentTime; + +} WTSINFOW, * PWTSINFOW; + +#define PWTSINFO PWTSINFOW // cpu_count_phys() #if (_WIN32_WINNT < 0x0601) // Windows < 7 (Vista and XP) @@ -551,6 +629,32 @@ DWORD (CALLBACK *_GetActiveProcessorCount) ( #define GetActiveProcessorCount _GetActiveProcessorCount +BOOL(CALLBACK *_WTSQuerySessionInformationW) ( + HANDLE hServer, + DWORD SessionId, + WTS_INFO_CLASS WTSInfoClass, + LPWSTR* ppBuffer, + DWORD* pBytesReturned + ); + +#define WTSQuerySessionInformationW _WTSQuerySessionInformationW + +BOOL(CALLBACK *_WTSEnumerateSessionsW)( + HANDLE hServer, + DWORD Reserved, + DWORD Version, + PWTS_SESSION_INFO* ppSessionInfo, + DWORD* pCount + ); + +#define WTSEnumerateSessionsW _WTSEnumerateSessionsW + +VOID(CALLBACK *_WTSFreeMemory)( + IN PVOID pMemory + ); + +#define WTSFreeMemory _WTSFreeMemory + ULONGLONG (CALLBACK *_GetTickCount64) ( void); diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 42a70df766..0a1fb8913d 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -30,7 +30,7 @@ double load_avg_5m = 0; double load_avg_15m = 0; -VOID CALLBACK LoadAvgCallback(PVOID hCounter) { +VOID CALLBACK LoadAvgCallback(PVOID hCounter, BOOLEAN timedOut) { PDH_FMT_COUNTERVALUE displayValue; double currentLoad; PDH_STATUS err; diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 6a119bf56b..b508a629f6 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -76,7 +76,7 @@ # constants 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', - 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TOX', 'TRAVIS', 'CIRRUS', + 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TRAVIS', 'CIRRUS', '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", @@ -117,7 +117,6 @@ # --- platforms -TOX = os.getenv('TOX') or '' in ('1', 'true') PYPY = '__pypy__' in sys.builtin_module_names # whether we're running this test suite on a Continuous Integration service TRAVIS = 'TRAVIS' in os.environ @@ -186,7 +185,7 @@ try: HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) except Exception: - HAS_BATTERY = True + HAS_BATTERY = False HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") HAS_THREADS = hasattr(psutil.Process, "threads") @@ -499,6 +498,9 @@ def wait(proc, timeout): pass def sendsig(proc, sig): + # XXX: otherwise the build hangs for some reason. + if MACOS and GITHUB_WHEELS: + sig = signal.SIGKILL # If the process received SIGSTOP, SIGCONT is necessary first, # otherwise SIGTERM won't work. if POSIX and sig != signal.SIGKILL: @@ -922,7 +924,7 @@ class TestMemoryLeak(PsutilTestCase): If available (Linux, OSX, Windows), USS memory is used for comparison, since it's supposed to be more precise, see: - http://grodola.blogspot.com/2016/02/psutil-4-real-process-memory-and-environ.html + 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. @@ -1632,7 +1634,6 @@ def cleanup_test_procs(): # 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: -# http://grodola.blogspot.com/ -# 2016/02/how-to-always-execute-exit-functions-in-py.html +# https://gmpy.dev/blog/2016/how-to-always-execute-exit-functions-in-python if POSIX: signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit(sig)) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 8b86a3e99f..0777f5e74d 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -48,10 +48,9 @@ from psutil.tests import print_sysinfo from psutil.tests import reap_children from psutil.tests import safe_rmpath -from psutil.tests import TOX -VERBOSITY = 1 if TOX else 2 +VERBOSITY = 2 FAILED_TESTS_FNAME = '.failed-tests.txt' NWORKERS = psutil.cpu_count() or 1 USE_COLORS = not CI_TESTING and term_supports_colors() diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 8a9a6eb407..d1425bcb1f 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -38,6 +38,8 @@ from psutil.tests import get_free_port from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import PsutilTestCase +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 SKIP_SYSCONS @@ -392,6 +394,8 @@ def check(kind, families, types): @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") @@ -569,6 +573,7 @@ def check(cons, families, types_): # See: https://travis-ci.org/giampaolo/psutil/jobs/237566297 @unittest.skipIf(MACOS and TRAVIS, "unreliable on MACOS + TRAVIS") + @retry_on_failure() def test_multi_sockets_procs(self): # Creates multiple sub processes, each creating different # sockets. For each process check that proc.connections() diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 35ab61e01c..2d9e5917e6 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -34,6 +34,7 @@ from psutil._compat import range from psutil.tests import create_sockets from psutil.tests import enum +from psutil.tests import GITHUB_WHEELS from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import HAS_SENSORS_FANS @@ -85,6 +86,7 @@ def test_linux_ioprio_windows(self): ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) + @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") def test_linux_rlimit(self): ae = self.assertEqual ae(hasattr(psutil, "RLIM_INFINITY"), LINUX) @@ -135,7 +137,8 @@ class TestAvailProcessAPIs(PsutilTestCase): def test_environ(self): self.assertEqual(hasattr(psutil.Process, "environ"), - LINUX or MACOS or WINDOWS or AIX or SUNOS) + 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) @@ -149,6 +152,7 @@ def test_terminal(self): def test_ionice(self): self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) + @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") def test_rlimit(self): # requires Linux 2.6.36 self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 9dd12890c6..163be0f978 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -53,6 +53,8 @@ SIOCGIFADDR = 0x8915 SIOCGIFCONF = 0x8912 SIOCGIFHWADDR = 0x8927 +SIOCGIFNETMASK = 0x891b +SIOCGIFBRDADDR = 0x8919 if LINUX: SECTOR_SIZE = 512 EMPTY_TEMPERATURES = not glob.glob('/sys/class/hwmon/hwmon*') @@ -75,6 +77,49 @@ def get_ipv4_address(ifname): 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_address(ifname): + with open("/proc/net/if_inet6", 'rt') as f: + for line in f.readlines(): + fields = line.split() + if fields[-1] == ifname: + break + else: + raise ValueError("could not find interface %r" % ifname) + unformatted = fields[0] + groups = [] + for i in range(0, len(unformatted), 4): + groups.append(unformatted[i:i + 4]) + formatted = ":".join(groups) + packed = socket.inet_pton(socket.AF_INET6, formatted) + return socket.inet_ntop(socket.AF_INET6, packed) + + def get_mac_address(ifname): import fcntl ifname = ifname[:15] @@ -890,7 +935,21 @@ def test_ips(self): 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 + 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.assertEqual(address, get_ipv6_address(name)) # XXX - not reliable when having virtual NICs installed by Docker. # @unittest.skipIf(not which('ip'), "'ip' utility not available") @@ -919,11 +978,15 @@ def test_against_ifconfig(self): except RuntimeError: pass else: - # Not always reliable. - # self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) + 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, "rt") as f: + self.assertEqual(stats.mtu, int(f.read().strip())) + @unittest.skipIf(not LINUX, "LINUX only") class TestSystemNetIOCounters(PsutilTestCase): @@ -1411,17 +1474,6 @@ def test_percent(self): 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): diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 10e45d23f5..8fcee12a76 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -763,6 +763,8 @@ def test_fans(self): 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') diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a0b21c6e55..b2328ba2f1 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -298,11 +298,9 @@ def test_create_time(self): @unittest.skipIf(TRAVIS or CIRRUS, 'not reliable on TRAVIS/CIRRUS') def test_terminal(self): terminal = psutil.Process().terminal() - if sys.stdout.isatty(): + if terminal is not None: 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) @@ -937,7 +935,7 @@ def test_cpu_affinity_all_combinations(self): for combo in combos: p.cpu_affinity(combo) - self.assertEqual(p.cpu_affinity(), combo) + self.assertEqual(sorted(p.cpu_affinity()), sorted(combo)) # TODO: #595 @unittest.skipIf(BSD, "broken on BSD") diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 945bb2eda1..580e1e5e09 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -511,6 +511,7 @@ def test_name(self): p = psutil.Process(self.pid) self.assertEqual(p.name(), w.Caption) + # This fail on github because using virtualenv for test environment @unittest.skipIf(GITHUB_WHEELS, "unreliable path on GITHUB_WHEELS") def test_exe(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] diff --git a/scripts/battery.py b/scripts/battery.py index 0da2b95889..edf4ce8c97 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -7,7 +7,7 @@ """ Show battery information. -$ python scripts/battery.py +$ python3 scripts/battery.py charge: 74% left: 2:11:31 status: discharging diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index 08997797c3..fb39d888ad 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -7,7 +7,7 @@ """ Shows CPU workload split across different CPUs. -$ python scripts/cpu_workload.py +$ python3 scripts/cpu_workload.py CPU 0 CPU 1 CPU 2 CPU 3 CPU 4 CPU 5 CPU 6 CPU 7 19.8 20.6 18.2 15.8 6.9 17.3 5.0 20.4 gvfsd pytho kwork chrom unity kwork kwork kwork diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 901dbf8c22..851ae9b166 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -7,7 +7,7 @@ """ List all mounted disk partitions a-la "df -h" command. -$ python scripts/disk_usage.py +$ python3 scripts/disk_usage.py Device Total Used Free Use % Type Mount /dev/sdb3 18.9G 14.7G 3.3G 77% ext4 / /dev/sda6 345.9G 83.8G 244.5G 24% ext4 /home diff --git a/scripts/free.py b/scripts/free.py index 000323c5dd..8c3359d869 100755 --- a/scripts/free.py +++ b/scripts/free.py @@ -7,7 +7,7 @@ """ A clone of 'free' cmdline utility. -$ python scripts/free.py +$ python3 scripts/free.py total used free shared buffers cache Mem: 10125520 8625996 1499524 0 349500 3307836 Swap: 0 0 0 diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index cfd02f0daf..ae137fb482 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -7,7 +7,7 @@ """ A clone of 'ifconfig' on UNIX. -$ python scripts/ifconfig.py +$ python3 scripts/ifconfig.py lo: stats : speed=0MB, duplex=?, mtu=65536, up=yes incoming : bytes=1.95M, pkts=22158, errs=0, drops=0 diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py deleted file mode 100644 index 1834f1b3af..0000000000 --- a/scripts/internal/download_wheels.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/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. - -""" -Script which downloads wheel files hosted on GitHub: -https://github.com/giampaolo/psutil/actions -It needs an access token string generated from personal GitHub profile: -https://github.com/settings/tokens -The token must be created with at least "public_repo" scope/rights. -If you lose it, just generate a new token. -REST API doc: -https://developer.github.com/v3/actions/artifacts/ -""" - -import argparse -import collections -import json -import os -import requests -import shutil -import zipfile - -from psutil import __version__ as PSUTIL_VERSION -from psutil._common import bytes2human -from psutil._common import print_color - - -USER = "" -PROJECT = "" -TOKEN = "" -OUTFILE = "wheels.zip" - - -# --- GitHub API - - -def get_artifacts(): - base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) - url = base_url + "/actions/artifacts" - res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) - res.raise_for_status() - data = json.loads(res.content) - return data - - -def download_zip(url): - print("downloading: " + url) - res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) - res.raise_for_status() - totbytes = 0 - with open(OUTFILE, 'wb') as f: - for chunk in res.iter_content(chunk_size=16384): - f.write(chunk) - totbytes += len(chunk) - print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) - - -# --- extract - - -def rename_27_wheels(): - # See: https://github.com/giampaolo/psutil/issues/810 - src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PSUTIL_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - - -def extract(): - with zipfile.ZipFile(OUTFILE, 'r') as zf: - zf.extractall('dist') - - -def print_wheels(): - def is64bit(name): - return name.endswith(('x86_64.whl', 'amd64.whl')) - - groups = collections.defaultdict(list) - for name in os.listdir('dist'): - plat = name.split('-')[-1] - pyimpl = name.split('-')[3] - ispypy = 'pypy' in pyimpl - if 'linux' in plat: - if ispypy: - groups['pypy_on_linux'].append(name) - else: - groups['linux'].append(name) - elif 'win' in plat: - if ispypy: - groups['pypy_on_windows'].append(name) - else: - groups['windows'].append(name) - elif 'macosx' in plat: - if ispypy: - groups['pypy_on_macos'].append(name) - else: - groups['macos'].append(name) - else: - assert 0, name - - totsize = 0 - templ = "%-54s %7s %7s %7s" - for platf, names in groups.items(): - ppn = "%s (total = %s)" % (platf.replace('_', ' '), len(names)) - s = templ % (ppn, "size", "arch", "pyver") - print_color('\n' + s, color=None, bold=True) - for name in sorted(names): - path = os.path.join('dist', name) - size = os.path.getsize(path) - totsize += size - arch = '64' if is64bit(name) else '32' - pyver = 'pypy' if name.split('-')[3].startswith('pypy') else 'py' - pyver += name.split('-')[2][2:] - s = templ % (name, bytes2human(size), arch, pyver) - if 'pypy' in pyver: - print_color(s, color='violet') - else: - print_color(s, color='brown') - - -def run(): - if os.path.isdir('dist'): - shutil.rmtree('dist') - data = get_artifacts() - download_zip(data['artifacts'][0]['archive_download_url']) - os.mkdir('dist') - extract() - # rename_27_wheels() - print_wheels() - - -def main(): - global USER, PROJECT, TOKEN - parser = argparse.ArgumentParser(description='GitHub wheels downloader') - parser.add_argument('--user', required=True) - parser.add_argument('--project', required=True) - parser.add_argument('--tokenfile', required=True) - args = parser.parse_args() - USER = args.user - PROJECT = args.project - with open(os.path.expanduser(args.tokenfile)) as f: - TOKEN = f.read().strip() - run() - - -if __name__ == '__main__': - main() diff --git a/scripts/internal/win_download_wheels.py b/scripts/internal/download_wheels_appveyor.py similarity index 88% rename from scripts/internal/win_download_wheels.py rename to scripts/internal/download_wheels_appveyor.py index 8dae05730b..b7c0aeae87 100755 --- a/scripts/internal/win_download_wheels.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -15,10 +15,8 @@ from __future__ import print_function import argparse import concurrent.futures -import errno import os import requests -import shutil import sys from psutil import __version__ as PSUTIL_VERSION @@ -29,33 +27,12 @@ BASE_URL = 'https://ci.appveyor.com/api' PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7', '3.8'] TIMEOUT = 30 -COLORS = True - - -def safe_makedirs(path): - try: - os.makedirs(path) - except OSError as err: - if err.errno == errno.EEXIST: - if not os.path.isdir(path): - raise - else: - raise - - -def safe_rmtree(path): - def onerror(fun, path, excinfo): - exc = excinfo[1] - if exc.errno != errno.ENOENT: - raise - - shutil.rmtree(path, onerror=onerror) def download_file(url): local_fname = url.split('/')[-1] local_fname = os.path.join('dist', local_fname) - safe_makedirs('dist') + os.makedirs('dist', exist_ok=True) r = requests.get(url, stream=True, timeout=TIMEOUT) tot_bytes = 0 with open(local_fname, 'wb') as f: @@ -102,7 +79,6 @@ def rename_27_wheels(): def run(options): - safe_rmtree('dist') urls = get_file_urls(options) completed = 0 exc = None diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py new file mode 100755 index 0000000000..4aa50d5df1 --- /dev/null +++ b/scripts/internal/download_wheels_github.py @@ -0,0 +1,81 @@ +#!/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. + +""" +Script which downloads wheel files hosted on GitHub: +https://github.com/giampaolo/psutil/actions +It needs an access token string generated from personal GitHub profile: +https://github.com/settings/tokens +The token must be created with at least "public_repo" scope/rights. +If you lose it, just generate a new token. +REST API doc: +https://developer.github.com/v3/actions/artifacts/ +""" + +import argparse +import json +import os +import requests +import zipfile + +from psutil._common import bytes2human +from psutil.tests import safe_rmpath + + +USER = "" +PROJECT = "" +TOKEN = "" +OUTFILE = "wheels-github.zip" + + +def get_artifacts(): + base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) + url = base_url + "/actions/artifacts" + res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res.raise_for_status() + data = json.loads(res.content) + return data + + +def download_zip(url): + print("downloading: " + url) + res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res.raise_for_status() + totbytes = 0 + with open(OUTFILE, 'wb') as f: + for chunk in res.iter_content(chunk_size=16384): + f.write(chunk) + totbytes += len(chunk) + print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) + + +def run(): + data = get_artifacts() + download_zip(data['artifacts'][0]['archive_download_url']) + os.makedirs('dist', exist_ok=True) + with zipfile.ZipFile(OUTFILE, 'r') as zf: + zf.extractall('dist') + + +def main(): + global USER, PROJECT, TOKEN + parser = argparse.ArgumentParser(description='GitHub wheels downloader') + parser.add_argument('--user', required=True) + parser.add_argument('--project', required=True) + parser.add_argument('--tokenfile', required=True) + args = parser.parse_args() + USER = args.user + PROJECT = args.project + with open(os.path.expanduser(args.tokenfile)) as f: + TOKEN = f.read().strip() + try: + run() + finally: + safe_rmpath(OUTFILE) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 9569c3674d..180bf37760 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -58,7 +58,7 @@ -- -Giampaolo - http://grodola.blogspot.com +Giampaolo - https://gmpy.dev/about """ diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py new file mode 100755 index 0000000000..81132db9bc --- /dev/null +++ b/scripts/internal/print_downloads.py @@ -0,0 +1,154 @@ +#!/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. + +""" +Print PYPI statistics in MarkDown format. +Useful sites: +* https://pepy.tech/project/psutil +* https://pypistats.org/packages/psutil +* https://hugovk.github.io/top-pypi-packages/ +""" + +from __future__ import print_function +import json +import os +import subprocess +import sys + +import pypinfo # NOQA + +from psutil._common import memoize + + +AUTH_FILE = os.path.expanduser("~/.pypinfo.json") +PKGNAME = 'psutil' +DAYS = 30 +LIMIT = 100 +GITHUB_SCRIPT_URL = "https://github.com/giampaolo/psutil/blob/master/" \ + "scripts/internal/pypistats.py" +bytes_billed = 0 + + +# --- get + +@memoize +def sh(cmd): + assert os.path.exists(AUTH_FILE) + env = os.environ.copy() + env['GOOGLE_APPLICATION_CREDENTIALS'] = AUTH_FILE + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True) + stdout, stderr = p.communicate() + if p.returncode != 0: + raise RuntimeError(stderr) + assert not stderr, stderr + return stdout.strip() + + +@memoize +def query(cmd): + global bytes_billed + ret = json.loads(sh(cmd)) + bytes_billed += ret['query']['bytes_billed'] + return ret + + +def top_packages(): + return query( + f"pypinfo --all --json --days {DAYS} --limit {LIMIT} '' project") + + +def ranking(): + data = top_packages() + for i, line in enumerate(data['rows'], 1): + if line['project'] == PKGNAME: + return i + raise ValueError(f"can't find {PKGNAME}") + + +def downloads(): + data = top_packages() + for line in data['rows']: + if line['project'] == PKGNAME: + return line['download_count'] + raise ValueError(f"can't find {PKGNAME}") + + +def downloads_pyver(): + return query(f"pypinfo --json --days {DAYS} {PKGNAME} pyversion") + + +def downloads_by_country(): + return query(f"pypinfo --json --days {DAYS} {PKGNAME} country") + + +def downloads_by_system(): + return query(f"pypinfo --json --days {DAYS} {PKGNAME} system") + + +def downloads_by_distro(): + return query(f"pypinfo --json --days {DAYS} {PKGNAME} distro") + + +# --- print + + +templ = "| %-30s | %15s |" + + +def print_row(left, right): + if isinstance(right, int): + right = '{0:,}'.format(right) + print(templ % (left, right)) + + +def print_header(left, right="Downloads"): + print_row(left, right) + s = templ % ("-" * 30, "-" * 15) + print("|:" + s[2:-2] + ":|") + + +def print_markdown_table(title, left, rows): + pleft = left.replace('_', ' ').capitalize() + print("### " + title) + print() + print_header(pleft) + for row in rows: + lval = row[left] + print_row(lval, row['download_count']) + print() + + +def main(): + last_update = top_packages()['last_update'] + print("# Download stats") + print("") + s = f"psutil download statistics of the last {DAYS} days (last update " + s += f"*{last_update}*).\n" + s += f"Generated via [pypistats.py]({GITHUB_SCRIPT_URL}) script.\n" + print(s) + + data = [ + {'what': 'Per month', 'download_count': downloads()}, + {'what': 'Per day', 'download_count': int(downloads() / 30)}, + {'what': 'PYPI ranking', 'download_count': ranking()} + ] + print_markdown_table('Overview', 'what', data) + print_markdown_table('Operating systems', 'system_name', + downloads_by_system()['rows']) + print_markdown_table('Distros', 'distro_name', + downloads_by_distro()['rows']) + print_markdown_table('Python versions', 'python_version', + downloads_pyver()['rows']) + print_markdown_table('Countries', 'country', + downloads_by_country()['rows']) + + +if __name__ == '__main__': + try: + main() + finally: + print("bytes billed: %s" % bytes_billed, file=sys.stderr) diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_wheels.py new file mode 100755 index 0000000000..be8290e063 --- /dev/null +++ b/scripts/internal/print_wheels.py @@ -0,0 +1,66 @@ +#!/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. + +"""Nicely print wheels print in dist/ directory.""" + +import collections +import glob +import os + +from psutil._common import print_color +from psutil._common import bytes2human + + +def main(): + def is64bit(name): + return name.endswith(('x86_64.whl', 'amd64.whl')) + + groups = collections.defaultdict(list) + for path in glob.glob('dist/*.whl'): + name = os.path.basename(path) + plat = name.split('-')[-1] + pyimpl = name.split('-')[3] + ispypy = 'pypy' in pyimpl + if 'linux' in plat: + if ispypy: + groups['pypy_on_linux'].append(name) + else: + groups['linux'].append(name) + elif 'win' in plat: + if ispypy: + groups['pypy_on_windows'].append(name) + else: + groups['windows'].append(name) + elif 'macosx' in plat: + if ispypy: + groups['pypy_on_macos'].append(name) + else: + groups['macos'].append(name) + else: + assert 0, name + + totsize = 0 + templ = "%-54s %7s %7s %7s" + for platf, names in groups.items(): + ppn = "%s (total = %s)" % (platf.replace('_', ' '), len(names)) + s = templ % (ppn, "size", "arch", "pyver") + print_color('\n' + s, color=None, bold=True) + for name in sorted(names): + path = os.path.join('dist', name) + size = os.path.getsize(path) + totsize += size + arch = '64' if is64bit(name) else '32' + pyver = 'pypy' if name.split('-')[3].startswith('pypy') else 'py' + pyver += name.split('-')[2][2:] + s = templ % (name, bytes2human(size), arch, pyver) + if 'pypy' in pyver: + print_color(s, color='violet') + else: + print_color(s, color='brown') + + +if __name__ == '__main__': + main() diff --git a/scripts/iotop.py b/scripts/iotop.py index c3afd07151..0468367347 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -14,7 +14,7 @@ Example output: -$ python scripts/iotop.py +$ python3 scripts/iotop.py Total DISK READ: 0.00 B/s | Total DISK WRITE: 472.00 K/s PID USER DISK READ DISK WRITE COMMAND 13155 giampao 0.00 B/s 428.00 K/s /usr/bin/google-chrome-beta @@ -30,7 +30,6 @@ Author: Giampaolo Rodola' """ -import atexit import time import sys try: @@ -42,20 +41,11 @@ from psutil._common import bytes2human -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - - win = curses.initscr() -atexit.register(tear_down) -curses.endwin() lineno = 0 -def print_line(line, highlight=False): +def printl(line, highlight=False): """A thin wrapper around curses's addstr().""" global lineno try: @@ -129,10 +119,10 @@ def refresh_window(procs, disks_read, disks_write): disks_tot = "Total DISK READ: %s | Total DISK WRITE: %s" \ % (bytes2human(disks_read), bytes2human(disks_write)) - print_line(disks_tot) + printl(disks_tot) header = templ % ("PID", "USER", "DISK READ", "DISK WRITE", "COMMAND") - print_line(header, highlight=True) + printl(header, highlight=True) for p in procs: line = templ % ( @@ -142,21 +132,45 @@ def refresh_window(procs, disks_read, disks_write): bytes2human(p._write_per_sec), p._cmdline) try: - print_line(line) + printl(line) except curses.error: break win.refresh() +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(0, curses.COLORS): + curses.init_pair(i + 1, i, -1) + curses.endwin() + win.nodelay(1) + + +def tear_down(): + win.keypad(0) + curses.nocbreak() + curses.echo() + curses.endwin() + + def main(): + global lineno + setup() try: interval = 0 while True: + if win.getch() == ord('q'): + break args = poll(interval) refresh_window(*args) - interval = 1 + lineno = 0 + interval = 0.5 + time.sleep(interval) except (KeyboardInterrupt, SystemExit): pass + finally: + tear_down() if __name__ == '__main__': diff --git a/scripts/meminfo.py b/scripts/meminfo.py index 550fcd0128..b98aa60be2 100755 --- a/scripts/meminfo.py +++ b/scripts/meminfo.py @@ -7,7 +7,7 @@ """ Print system memory information. -$ python scripts/meminfo.py +$ python3 scripts/meminfo.py MEMORY ------ Total : 9.7G diff --git a/scripts/netstat.py b/scripts/netstat.py index fe3bfe402c..5a21358e79 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -7,7 +7,7 @@ """ A clone of 'netstat -antp' on Linux. -$ python scripts/netstat.py +$ python3 scripts/netstat.py Proto Local address Remote address Status PID Program name tcp 127.0.0.1:48256 127.0.0.1:45884 ESTABLISHED 13646 chrome tcp 127.0.0.1:47073 127.0.0.1:45884 ESTABLISHED 13646 chrome diff --git a/scripts/nettop.py b/scripts/nettop.py index ce647c9dd3..8cc19fda4c 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -11,7 +11,7 @@ Author: Giampaolo Rodola' -$ python scripts/nettop.py +$ python3 scripts/nettop.py ----------------------------------------------------------- total bytes: sent: 1.49 G received: 4.82 G total packets: sent: 7338724 received: 8082712 @@ -31,7 +31,6 @@ pkts-recv 1214470 0 """ -import atexit import time import sys try: @@ -43,20 +42,11 @@ from psutil._common import bytes2human -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - - -win = curses.initscr() -atexit.register(tear_down) -curses.endwin() lineno = 0 +win = curses.initscr() -def print_line(line, highlight=False): +def printl(line, highlight=False): """A thin wrapper around curses's addstr().""" global lineno try: @@ -89,59 +79,80 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): global lineno # totals - print_line("total bytes: sent: %-10s received: %s" % ( + printl("total bytes: sent: %-10s received: %s" % ( bytes2human(tot_after.bytes_sent), bytes2human(tot_after.bytes_recv)) ) - print_line("total packets: sent: %-10s received: %s" % ( + printl("total packets: sent: %-10s received: %s" % ( tot_after.packets_sent, tot_after.packets_recv)) # per-network interface details: let's sort network interfaces so # that the ones which generated more traffic are shown first - print_line("") + printl("") nic_names = list(pnic_after.keys()) nic_names.sort(key=lambda x: sum(pnic_after[x]), reverse=True) for name in nic_names: stats_before = pnic_before[name] stats_after = pnic_after[name] templ = "%-15s %15s %15s" - print_line(templ % (name, "TOTAL", "PER-SEC"), highlight=True) - print_line(templ % ( + printl(templ % (name, "TOTAL", "PER-SEC"), highlight=True) + printl(templ % ( "bytes-sent", bytes2human(stats_after.bytes_sent), bytes2human( stats_after.bytes_sent - stats_before.bytes_sent) + '/s', )) - print_line(templ % ( + printl(templ % ( "bytes-recv", bytes2human(stats_after.bytes_recv), bytes2human( stats_after.bytes_recv - stats_before.bytes_recv) + '/s', )) - print_line(templ % ( + printl(templ % ( "pkts-sent", stats_after.packets_sent, stats_after.packets_sent - stats_before.packets_sent, )) - print_line(templ % ( + printl(templ % ( "pkts-recv", stats_after.packets_recv, stats_after.packets_recv - stats_before.packets_recv, )) - print_line("") + printl("") win.refresh() lineno = 0 +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(0, curses.COLORS): + curses.init_pair(i + 1, i, -1) + curses.endwin() + win.nodelay(1) + + +def tear_down(): + win.keypad(0) + curses.nocbreak() + curses.echo() + curses.endwin() + + def main(): + setup() try: interval = 0 while True: + if win.getch() == ord('q'): + break args = poll(interval) refresh_window(*args) - interval = 1 + interval = 0.5 except (KeyboardInterrupt, SystemExit): pass + finally: + tear_down() if __name__ == '__main__': diff --git a/scripts/pmap.py b/scripts/pmap.py index 5f7246a159..459927bfd2 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -8,7 +8,7 @@ A clone of 'pmap' utility on Linux, 'vmmap' on macOS and 'procstat -v' on BSD. Report memory map of a process. -$ python scripts/pmap.py 32402 +$ python3 scripts/pmap.py 32402 Address RSS Mode Mapping 0000000000400000 1200K r-xp /usr/bin/python2.7 0000000000838000 4K r--p /usr/bin/python2.7 diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 01974513f0..f060538686 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -8,7 +8,7 @@ Print detailed information about a process. Author: Giampaolo Rodola' -$ python scripts/procinfo.py +$ python3 scripts/procinfo.py pid 4600 name chrome parent 4554 (bash) diff --git a/scripts/ps.py b/scripts/ps.py index 540c032a7f..a234209fb0 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -7,7 +7,7 @@ """ A clone of 'ps aux'. -$ python scripts/ps.py +$ python3 scripts/ps.py USER PID %MEM VSZ RSS NICE STATUS START TIME CMDLINE root 1 0.0 220.9M 6.5M sleep Mar27 09:10 /lib/systemd root 2 0.0 0.0B 0.0B sleep Mar27 00:00 kthreadd diff --git a/scripts/pstree.py b/scripts/pstree.py index 0005e4a181..dba9f1bdd4 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -8,7 +8,7 @@ Similar to 'ps aux --forest' on Linux, prints the process list as a tree structure. -$ python scripts/pstree.py +$ python3 scripts/pstree.py 0 ? |- 1 init | |- 289 cgmanager diff --git a/scripts/sensors.py b/scripts/sensors.py index e532bebad1..911d7c9b43 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -9,7 +9,7 @@ A clone of 'sensors' utility on Linux printing hardware temperatures, fans speed and battery info. -$ python scripts/sensors.py +$ python3 scripts/sensors.py asus Temperatures: asus 57.0°C (high=None°C, critical=None°C) diff --git a/scripts/temperatures.py b/scripts/temperatures.py index e83df44036..f2dd51a73b 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -8,7 +8,7 @@ """ A clone of 'sensors' utility on Linux printing hardware temperatures. -$ python scripts/sensors.py +$ python3 scripts/sensors.py asus asus 47.0 °C (high = None °C, critical = None °C) diff --git a/scripts/top.py b/scripts/top.py index 0b17471d55..3c297d9af1 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -9,7 +9,7 @@ Author: Giampaolo Rodola' -$ python scripts/top.py +$ python3 scripts/top.py CPU0 [|||| ] 10.9% CPU1 [||||| ] 13.1% CPU2 [||||| ] 12.8% @@ -33,7 +33,6 @@ ... """ -import atexit import datetime import sys import time @@ -46,30 +45,28 @@ from psutil._common import bytes2human -# --- curses stuff - -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - - win = curses.initscr() -atexit.register(tear_down) -curses.endwin() lineno = 0 +colors_map = dict( + green=3, + red=10, + yellow=4, +) -def print_line(line, highlight=False): +def printl(line, color=None, bold=False, highlight=False): """A thin wrapper around curses's addstr().""" global lineno try: + flags = 0 + if color: + flags |= curses.color_pair(colors_map[color]) + if bold: + flags |= curses.A_BOLD if highlight: line += " " * (win.getmaxyx()[1] - len(line)) - win.addstr(lineno, 0, line, curses.A_REVERSE) - else: - win.addstr(lineno, 0, line, 0) + flags |= curses.A_STANDOUT + win.addstr(lineno, 0, line, flags) except curses.error: lineno = 0 win.refresh() @@ -105,6 +102,15 @@ def poll(interval): return (processes, procs_status) +def get_color(perc): + if perc <= 30: + return "green" + elif perc <= 80: + return "yellow" + else: + return "red" + + def print_header(procs_status, num_procs): """Print system-related info, above the process list.""" @@ -117,8 +123,8 @@ def get_dashes(perc): percs = psutil.cpu_percent(interval=0, percpu=True) for cpu_num, perc in enumerate(percs): dashes, empty_dashes = get_dashes(perc) - print_line(" CPU%-2s [%s%s] %5s%%" % (cpu_num, dashes, empty_dashes, - perc)) + line = " CPU%-2s [%s%s] %5s%%" % (cpu_num, dashes, empty_dashes, perc) + printl(line, color=get_color(perc)) mem = psutil.virtual_memory() dashes, empty_dashes = get_dashes(mem.percent) line = " Mem [%s%s] %5s%% %6s / %s" % ( @@ -127,7 +133,7 @@ def get_dashes(perc): str(int(mem.used / 1024 / 1024)) + "M", str(int(mem.total / 1024 / 1024)) + "M" ) - print_line(line) + printl(line, color=get_color(mem.percent)) # swap usage swap = psutil.swap_memory() @@ -138,7 +144,7 @@ def get_dashes(perc): str(int(swap.used / 1024 / 1024)) + "M", str(int(swap.total / 1024 / 1024)) + "M" ) - print_line(line) + printl(line, color=get_color(swap.percent)) # processes number and status st = [] @@ -146,14 +152,14 @@ def get_dashes(perc): if y: st.append("%s=%s" % (x, y)) st.sort(key=lambda x: x[:3] in ('run', 'sle'), reverse=1) - print_line(" Processes: %s (%s)" % (num_procs, ', '.join(st))) + printl(" Processes: %s (%s)" % (num_procs, ', '.join(st))) # load average, uptime uptime = datetime.datetime.now() - \ datetime.datetime.fromtimestamp(psutil.boot_time()) av1, av2, av3 = psutil.getloadavg() line = " Load average: %.2f %.2f %.2f Uptime: %s" \ % (av1, av2, av3, str(uptime).split('.')[0]) - print_line(line) + printl(line) def refresh_window(procs, procs_status): @@ -164,8 +170,8 @@ def refresh_window(procs, procs_status): header = templ % ("PID", "USER", "NI", "VIRT", "RES", "CPU%", "MEM%", "TIME+", "NAME") print_header(procs_status, len(procs)) - print_line("") - print_line(header, highlight=True) + printl("") + printl(header, bold=True, highlight=True) for p in procs: # TIME+ column shows process CPU cumulative time and it # is expressed as: "mm:ss.ms" @@ -197,21 +203,42 @@ def refresh_window(procs, procs_status): p.dict['name'] or '', ) try: - print_line(line) + printl(line) except curses.error: break win.refresh() +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(0, curses.COLORS): + curses.init_pair(i + 1, i, -1) + curses.endwin() + win.nodelay(1) + + +def tear_down(): + win.keypad(0) + curses.nocbreak() + curses.echo() + curses.endwin() + + def main(): + setup() try: interval = 0 while True: + if win.getch() == ord('q'): + break args = poll(interval) refresh_window(*args) interval = 1 except (KeyboardInterrupt, SystemExit): pass + finally: + tear_down() if __name__ == '__main__': diff --git a/scripts/who.py b/scripts/who.py index c2299eb09e..c1e407299e 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -8,7 +8,7 @@ A clone of 'who' command; print information about users who are currently logged in. -$ python scripts/who.py +$ python3 scripts/who.py giampaolo console 2017-03-25 22:24 loginwindow giampaolo ttys000 2017-03-25 23:28 (10.0.2.2) sshd """ diff --git a/scripts/winservices.py b/scripts/winservices.py index 8792f752e4..5c710159be 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -7,7 +7,7 @@ r""" List all Windows services installed. -$ python scripts/winservices.py +$ python3 scripts/winservices.py AeLookupSvc (Application Experience) status: stopped, start: manual, username: localSystem, pid: None binpath: C:\Windows\system32\svchost.exe -k netsvcs diff --git a/setup.py b/setup.py index 3c44230293..893eb46ba0 100755 --- a/setup.py +++ b/setup.py @@ -167,7 +167,7 @@ def get_winver(): define_macros=macros, libraries=[ "psapi", "kernel32", "advapi32", "shell32", "netapi32", - "wtsapi32", "ws2_32", "PowrProf", "pdh", + "ws2_32", "PowrProf", "pdh", ], # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"] From fa517a1094a17aff024e9d6c4d2391e7421b27ba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 01:34:24 +0200 Subject: [PATCH 0630/1714] don't use fstrings --- scripts/internal/print_downloads.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 81132db9bc..48828c7482 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -57,8 +57,8 @@ def query(cmd): def top_packages(): - return query( - f"pypinfo --all --json --days {DAYS} --limit {LIMIT} '' project") + return query("pypinfo --all --json --days %s --limit %s '' project" % ( + DAYS, LIMIT)) def ranking(): @@ -66,7 +66,7 @@ def ranking(): for i, line in enumerate(data['rows'], 1): if line['project'] == PKGNAME: return i - raise ValueError(f"can't find {PKGNAME}") + raise ValueError("can't find %s" % PKGNAME) def downloads(): @@ -74,23 +74,23 @@ def downloads(): for line in data['rows']: if line['project'] == PKGNAME: return line['download_count'] - raise ValueError(f"can't find {PKGNAME}") + raise ValueError("can't find %s" % PKGNAME) def downloads_pyver(): - return query(f"pypinfo --json --days {DAYS} {PKGNAME} pyversion") + return query("pypinfo --json --days %s %s pyversion" % (DAYS, PKGNAME)) def downloads_by_country(): - return query(f"pypinfo --json --days {DAYS} {PKGNAME} country") + return query("pypinfo --json --days %s %s country" % (DAYS, PKGNAME)) def downloads_by_system(): - return query(f"pypinfo --json --days {DAYS} {PKGNAME} system") + return query("pypinfo --json --days %s %s system" % (DAYS, PKGNAME)) def downloads_by_distro(): - return query(f"pypinfo --json --days {DAYS} {PKGNAME} distro") + return query("pypinfo --json --days %s %s distro" % (DAYS, PKGNAME)) # --- print @@ -123,12 +123,11 @@ def print_markdown_table(title, left, rows): def main(): - last_update = top_packages()['last_update'] print("# Download stats") print("") - s = f"psutil download statistics of the last {DAYS} days (last update " - s += f"*{last_update}*).\n" - s += f"Generated via [pypistats.py]({GITHUB_SCRIPT_URL}) script.\n" + s = "psutil download statistics of the last %s days (last update " % DAYS + s += "*%s*).\n" % top_packages()['last_update'] + s += "Generated via [pypistats.py](%s) script.\n" % GITHUB_SCRIPT_URL print(s) data = [ From 019546e395e0c15ff5c41f09ce2f53b99d8de8b3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 02:14:27 +0200 Subject: [PATCH 0631/1714] skip GH action tests on osx + pypy --- .github/workflows/build_wheel.yml | 1 + psutil/tests/test_linux.py | 11 ----------- psutil/tests/test_unicode.py | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index ec784ed51e..4f9eeef978 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -14,6 +14,7 @@ jobs: CIBW_TEST_COMMAND: python -u -Wa {project}/psutil/tests/runner.py CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py CIBW_TEST_EXTRAS: test + CIBW_SKIP: pp*-macosx_x86_64 steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 163be0f978..0d247aa57f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1556,17 +1556,6 @@ def open_mock(name, *args, **kwargs): 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( diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index fba5623925..bae81380b6 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -282,8 +282,7 @@ def test_disk_usage(self): @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") @unittest.skipIf(not PY3, "ctypes does not support unicode on PY2") - @unittest.skipIf(PYPY and WINDOWS, - "copyload_shared_lib() unsupported on PYPY + WINDOWS") + @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. From b4c2124ff1f8f4f1f7f50ad0af6945b0d7e945df Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 02:17:01 +0200 Subject: [PATCH 0632/1714] windows / open_files; check globalFileName is not NULL before accessing it; fix #1823 and #1763 --- HISTORY.rst | 2 ++ psutil/arch/windows/process_handles.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1d760dbcd4..8c1c8a49a4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,8 @@ XXXX-XX-XX **Bug fixes** +- 1823_: [Windows] Process.open_files() may cause a segfault due to a NULL + pointer. - 1838_: [Linux] sensors_battery(): if `percent` can be determined but not the remaining values, still return a result instead of None. (patch by aristocratos) diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index f63d4af36c..72e3f4d41e 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -247,7 +247,7 @@ psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { if (psutil_threaded_get_filename(hFile) != 0) goto error; - if (globalFileName->Length > 0) { + if ((globalFileName != NULL) && (globalFileName->Length > 0)) { py_path = PyUnicode_FromWideChar(globalFileName->Buffer, wcslen(globalFileName->Buffer)); if (! py_path) From 0398213c561faf641f60a625a02d7df17f0d1e89 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 02:46:03 +0200 Subject: [PATCH 0633/1714] macOS: include unistd.h needed by getpagesize(); fixes #1791 --- HISTORY.rst | 1 + psutil/_psutil_osx.c | 1 + 2 files changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 8c1c8a49a4..af5dab6830 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,7 @@ XXXX-XX-XX **Bug fixes** +- 1791_: [macOS] fix missing include for getpagesize(). - 1823_: [Windows] Process.open_files() may cause a segfault due to a NULL pointer. - 1838_: [Linux] sensors_battery(): if `percent` can be determined but not diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index c51c1c788b..fdf34d9759 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include From 239ebc19c8477893870e497fd344744cb5d964a7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 03:19:21 +0200 Subject: [PATCH 0634/1714] fix #1738 / osx / exe(): catch ENOENT which may occur if process is still alive but the executable file which launched it got deleted --- HISTORY.rst | 2 ++ psutil/_psutil_osx.c | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index af5dab6830..3be3915fba 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,8 @@ XXXX-XX-XX **Bug fixes** +- 1738_: [macOS] Process.exe() may raise FileNotFoundError if process is still + alive but the exe file which launched it got deleted. - 1791_: [macOS] fix missing include for getpagesize(). - 1823_: [Windows] Process.open_files() may cause a segfault due to a NULL pointer. diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index fdf34d9759..ee8d1c8393 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -298,11 +298,21 @@ psutil_proc_exe(PyObject *self, PyObject *args) { errno = 0; ret = proc_pidpath(pid, &buf, sizeof(buf)); if (ret == 0) { - if (pid == 0) + if (pid == 0) { AccessDenied("automatically set for PID 0"); - else + return NULL; + } + else if (errno == ENOENT) { + // It may happen (file not found error) if the process is + // still alive but the executable which launched it got + // deleted, see: + // https://github.com/giampaolo/psutil/issues/1738 + return Py_BuildValue("s", ""); + } + else { psutil_raise_for_pid(pid, "proc_pidpath()"); - return NULL; + return NULL; + } } return PyUnicode_DecodeFSDefault(buf); } From 59ba7a57144e22cd0b728b6aad564afaa50440f6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 03:45:30 +0200 Subject: [PATCH 0635/1714] increase test coverage --- psutil/__init__.py | 4 ++-- psutil/_common.py | 3 ++- psutil/_pslinux.py | 2 +- psutil/_psposix.py | 4 ++-- psutil/tests/test_system.py | 2 ++ 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 9d43f991d0..2108a40e03 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -264,7 +264,7 @@ 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(). @@ -393,7 +393,7 @@ def _init(self, pid, _ignore_nsp=False): def __str__(self): try: info = collections.OrderedDict() - except AttributeError: + except AttributeError: # pragma: no cover info = {} # Python 2.6 info["pid"] = self.pid if self._name: diff --git a/psutil/_common.py b/psutil/_common.py index 743664567e..0ddb5f9c63 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -799,7 +799,8 @@ def hilite(s, color=None, bold=False): return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) -def print_color(s, color=None, bold=False, file=sys.stdout): +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 diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 3c9ff28110..382a17fab2 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -307,7 +307,7 @@ def cat(fname, fallback=_DEFAULT, binary=True): try: set_scputimes_ntuple("/proc") -except Exception: +except Exception: # pragma: no cover # Don't want to crash at import time. traceback.print_exc() scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 2e6711a3bc..706dab9ae8 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -65,7 +65,7 @@ def negsig_to_enum(num): return Negsignal(num) except ValueError: return num -else: +else: # pragma: no cover def negsig_to_enum(num): return num @@ -167,7 +167,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 diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index e368ea7659..787397ad35 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -483,6 +483,8 @@ def test_cpu_times_percent(self): 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) From c18ba60fe479c8f5dfab71d5a6f1033712ee5832 Mon Sep 17 00:00:00 2001 From: "Vincent A. Arcila" Date: Fri, 16 Oct 2020 21:23:57 -0500 Subject: [PATCH 0636/1714] Fix cpu_count_physical() returning wrong values on systems with SMT or more than one processor (#1727) --- psutil/_pslinux.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 3c9ff28110..4475195b49 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -618,12 +618,12 @@ def cpu_count_logical(): def cpu_count_physical(): """Return the number of physical cores in the system.""" # Method #1 - core_ids = set() + thread_siblings_lists = set() for path in glob.glob( - "/sys/devices/system/cpu/cpu[0-9]*/topology/core_id"): + "/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list"): with open_binary(path) as f: - core_ids.add(int(f.read())) - result = len(core_ids) + thread_siblings_lists.add(f.read()) + result = len(thread_siblings_lists) if result != 0: return result From bfae1fc4a371c9e08f2c3f5053a80542e43d18f7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 04:57:36 +0200 Subject: [PATCH 0637/1714] linux / cpu_count phys: take depreated */thread_siblings_list into account /sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list is deprecated /sys/devices/system/cpu/cpu[0-9]*/topology/core_cpus_list is the new name also add test which makes sure method 1 and 2 return the same result --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ psutil/_pslinux.py | 24 +++++++++++++++--------- psutil/tests/test_linux.py | 8 ++++++++ 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/CREDITS b/CREDITS index 3c54d33aff..ea13b16c53 100644 --- a/CREDITS +++ b/CREDITS @@ -716,3 +716,7 @@ I: 1830 N: aristocratos W: https://github.com/aristocratos I: 1837, 1838 + +N: Vincent A. Arcila +W: https://github.com/jandrovins +I: 1620, 1727 diff --git a/HISTORY.rst b/HISTORY.rst index 3be3915fba..1ef5573ab0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,8 @@ XXXX-XX-XX **Bug fixes** +- 1620_: [Linux] physical cpu_count() result is incorrect on systems with more + than one CPU socket. (patch by Vincent A. Arcila) - 1738_: [macOS] Process.exe() may raise FileNotFoundError if process is still alive but the exe file which launched it got deleted. - 1791_: [macOS] fix missing include for getpagesize(). diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 153ddc29e8..d5ce358c9a 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -618,12 +618,18 @@ def cpu_count_logical(): def cpu_count_physical(): """Return the number of physical cores in the system.""" # Method #1 - thread_siblings_lists = set() - for path in glob.glob( - "/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list"): + 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: - thread_siblings_lists.add(f.read()) - result = len(thread_siblings_lists) + ls.add(f.read().strip()) + result = len(ls) if result != 0: return result @@ -635,15 +641,15 @@ 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): + 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) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0d247aa57f..b303ed6de4 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -734,6 +734,14 @@ def test_against_lscpu(self): 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_physical() + with mock.patch('glob.glob', return_value=[]) as m: + meth_2 = psutil._pslinux.cpu_count_physical() + 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: From 958f18638cfee77e441f29fd54ab0f8c3af084ed Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Oct 2020 05:41:39 +0200 Subject: [PATCH 0638/1714] add battery test; increase coverage --- README.rst | 2 +- psutil/_common.py | 2 +- psutil/tests/test_linux.py | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 31119c3141..f000eefbbf 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ :target: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci :alt: FreeBSD tests (Cirrus-Ci) -.. |coverage| image:: https://img.shields.io/coveralls/github/giampaolo/psutil.svg?label=test%20coverage +.. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) diff --git a/psutil/_common.py b/psutil/_common.py index 0ddb5f9c63..39d03af4bc 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -780,7 +780,7 @@ def term_supports_colors(file=sys.stdout): return True -def hilite(s, color=None, bold=False): +def hilite(s, color=None, bold=False): # pragma: no cover """Return an highlighted version of 'string'.""" if not term_supports_colors(): return s diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index b303ed6de4..519d4b2e2f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1598,6 +1598,29 @@ def test_emulate_no_power(self): 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): From e417170c997732847e65ca8dc76a51859197a005 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 18 Oct 2020 01:37:15 +0200 Subject: [PATCH 0639/1714] pragma no cover --- psutil/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index 39d03af4bc..5ead5d9056 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -766,7 +766,7 @@ def decode(s): @memoize -def term_supports_colors(file=sys.stdout): +def term_supports_colors(file=sys.stdout): # pragma: no cover if os.name == 'nt': return True try: From e2c2ba79f3b23725c4af5e1406b0cd210c3af7a9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 18 Oct 2020 17:04:09 +0200 Subject: [PATCH 0640/1714] Update README (#1854) --- README.rst | 132 ++++++++++++++++----------------- docs/_static/psutil-logo.png | Bin 0 -> 7228 bytes docs/_static/tidelift-logo.png | Bin 0 -> 2271 bytes psutil/_common.py | 2 +- 4 files changed, 66 insertions(+), 68 deletions(-) create mode 100644 docs/_static/psutil-logo.png create mode 100644 docs/_static/tidelift-logo.png diff --git a/README.rst b/README.rst index f000eefbbf..70fe83230e 100644 --- a/README.rst +++ b/README.rst @@ -68,18 +68,21 @@ ----- -Quick links -=========== - -- `Home page `_ -- `Install `_ -- `Documentation `_ -- `Download `_ -- `Forum `_ -- `StackOverflow `_ -- `Blog `_ -- `Development guide `_ -- `What's new `_ +.. raw:: html + +
+ +
+
+ Home    + Install    + Documentation    + Download    + Forum    + Blog    + Funding    + What's new    +
Summary ======= @@ -102,68 +105,39 @@ psutil currently supports the following platforms: ...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**, `PyPy `__ 2.7 and 3.X. -psutil for enterprise -===================== - -.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png - :width: 150 - :alt: Tidelift - :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme - -.. list-table:: - :widths: 10 150 - - * - |tideliftlogo| - - The maintainer of psutil and thousands of other packages are working - with Tidelift to deliver commercial support and maintenance for the open - source dependencies you use to build your applications. Save time, - reduce risk, and improve code health, while paying the maintainers of - the exact dependencies you use. - `Learn more `__. - - By subscribing to Tidelift you will help me (`Giampaolo Rodola`_) support - psutil future development. Alternatively consider making a small - `donation`_. +Funding +======= -Security +While psutil is free software and will always be, the project would benefit +immensely from some funding. +Keeping up with bug reports and maintenance has become hardly sustainable for +me alone in terms of time. +If you're a company that's making significant use of psutil you can consider +becoming a sponsor via `GitHub `__, +`Open Collective `__ or +`PayPal `__ +and have your logo displayed in here and psutil `doc `__. + +Sponsors ======== -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -Sponsorship -=========== - -A lot of time and effort went into making psutil as it is today. If you whish -to help its future development consider -`sponsoring `__ it. - -Projects using psutil -===================== - -psutil has roughly the following monthly downloads: +.. raw:: html -.. image:: https://img.shields.io/pypi/dm/psutil.svg - :target: https://pepy.tech/project/psutil - :alt: Downloads +
+ + + +
+ add your logo -...and has over 55,000 projects on GitHub depending from it. -Here's some I find particularly interesting: +Supporters +========== -- https://github.com/google/grr -- https://github.com/facebook/osquery/ -- https://github.com/nicolargo/glances -- https://github.com/Jahaja/psdash -- https://github.com/ajenti/ajenti -- https://github.com/home-assistant/home-assistant/ +None yet. -Portings -======== +.. raw:: html -- Go: https://github.com/shirou/gopsutil -- C: https://github.com/hamon-in/cpslib -- Rust: https://github.com/rust-psutil/rust-psutil -- Nim: https://github.com/johnscillieri/psutil-nim + add your avatar Example usages ============== @@ -502,9 +476,33 @@ Windows services 'status': 'stopped', 'username': 'NT AUTHORITY\\LocalService'} +Projects using psutil +===================== + +Here's some I find particularly interesting: + +- https://github.com/google/grr +- https://github.com/facebook/osquery/ +- https://github.com/nicolargo/glances +- https://github.com/Jahaja/psdash +- https://github.com/ajenti/ajenti +- https://github.com/home-assistant/home-assistant/ + +Portings +======== + +- Go: https://github.com/shirou/gopsutil +- C: https://github.com/hamon-in/cpslib +- Rust: https://github.com/rust-psutil/rust-psutil +- Nim: https://github.com/johnscillieri/psutil-nim + +Security +======== + +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. .. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 .. _Tidelift security contact: https://tidelift.com/security .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme - diff --git a/docs/_static/psutil-logo.png b/docs/_static/psutil-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e1c67233f5dcbed72cf8e421058824f238106d5c GIT binary patch literal 7228 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 literal 0 HcmV?d00001 diff --git a/docs/_static/tidelift-logo.png b/docs/_static/tidelift-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9e33757fb78463e7b379be88ae0ff8b5a9421795 GIT binary patch literal 2271 zcmV<52q5=~P)WFU8GbZ8()Nlj2>E@cM*00=`#L_t(|+U=chOdMq% z$G;0p>F%ub4|WL^XyFRRZfP5@!tT|WF0Andq0;$c&BRFTue6q^{D(p+*@>%Fif zHMun9TCG)24)$7i>6f85_E`{-hw$L&=JIl_@&hGv` zFJ~WSemwJgzVkf4=XrjQq2bXs0R%x136euqKoA6p2SE^;VHCSqzP<$Znk^|aisN{= zJzj8pByQPtWoyyg)QExt%dKnCXcQmzd2sQI5n0bGUta>3(~gSr(uC}s=6HzVW^d*x9^K*0YYh*s)|s zanx>I16C@+;I9aDcUG4j2VvFt>}2}3A}seRs3OB0~oU@>0XB= zg*NV#D{!@i8GY*(ElEd*qm zJKCWwBTPn>)oj@qrTCJ_55vKGR+W_8R;n&u+1?=|Wl$JujkT~t_* z4ae?I+A5bMAZ%y?@4VNijL43Bt*CPa*|2IeM(K{*MCrfqrL1yi&$+=cri6x6 z7RH3~^(9O4>qX_+u&Aw~;^7?_lobVDNOMyonwuIRE=IkwqW5~^CmuGF(D%2`$H=HnBybpIn|q3oWxdzBQJI&<<}V>^Uwnv zAHmRw4?U-P(em^`9De0^+@YUV(}4t`Y0QI24)4Gq+B*(Q_ADyOOH;ZKk03}qble;1 z>^dsh1G#g789|VE5Vr)R``GE2zt=nMR7nJhhZNa7^*1!)lk9y|+p5S5iL`8Us)`54 zM{wX!ch*n+NlnRVxg46v9{ghg95~c1YjdgY#KS)pKuhbvw49<};P?pIpMOc#4)sJaDs5=@vC|1J zFe8>uz0Bp#lT(OiJ ziVa)DG7S;)V^4xu$mSGnv-gh$6dM_+K2=-BL{VanX0@YO*HBQDxWgw@4wRReqbOt4 zZFj1Q$BrT%FW>(IR^(;(=?+CSqe5uRiaY@?-~R)46sfn~LzQef%W?!fSvd)3G3A9w zCn%4HcCf7a0n_|k0ngrd1shjh$3MPaiRhfN8Icqhk7Cmd_S+_~&T@l1L<1*sd%XDg ze7_>XK`|@skekax37o||D(;Hn)sfZsedAN!>?6+MJ6%Ip|=Z}<%d1PX`MI!Zx zwKnIK3!bVyfco8*B_{TFBt5RtwZsS4ETaqMf~BRR|B z_IPQ?LXdb6^VHUCMr+G1$r%=okKn|~_o=c75|3>Gyl6Yhv z@4WX>#vKcpqv6uwy9-y5c#wEx7aSkK;UmX0a{tNf?3`xL%Fka$&XlR*AxNx1zagmj zg_3yg%uMQb_OhV~y!~#UBJmBqZk4|}&B0x&e}D5(rg$gY%N2~FVIRI4_9+?^^^XM#>Xl`mmL4jpnS))<({reMqesNgVJs7hIt~v)8 zCh@N*$MG2T2M}b#sxJCU#>PYlvf+e;uVz~{B9ZH`ZQOve(qgPywL(#QzWMe$T)O0k z=f4B8%0Ke(c0Bap1IWplUp##|7{VK8KERZWm`SX9PPAV1K)GcG+X^GV0{!@&H{w&) tgxfBrUOWhb$R=oS3qcSh9t1&X$4_yk2>8V&WlR77002ovPDHLkV1ngaS5E){ literal 0 HcmV?d00001 diff --git a/psutil/_common.py b/psutil/_common.py index 39d03af4bc..5ead5d9056 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -766,7 +766,7 @@ def decode(s): @memoize -def term_supports_colors(file=sys.stdout): +def term_supports_colors(file=sys.stdout): # pragma: no cover if os.name == 'nt': return True try: From b6815b66df9594a40d35d8de68448513b8017aae Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 18 Oct 2020 17:14:05 +0200 Subject: [PATCH 0641/1714] add sponsors in doc --- docs/index.rst | 61 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 3c9a4b6596..b3b21ea7cc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,26 +42,44 @@ Supported Python versions are **2.6**, **2.7** and **3.4+**. The psutil documentation you're reading is distributed as a single HTML page. +Funding +======= -Professional support --------------------- +While psutil is free software and will always be, the project would benefit +immensely from some funding. +Keeping up with bug reports and maintenance has become hardly sustainable for +me alone in terms of time. +If you're a company that's making significant use of psutil you can consider +becoming a sponsor via `GitHub `__, +`Open Collective `__ or +`PayPal `__ +and have your logo displayed in here and psutil `doc `__. -.. image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png - :width: 80px - :align: left - -Professional support for psutil is available as part of the `Tidelift Subscription`_. -Tidelift gives software development teams a single source for purchasing -and maintaining their software, with professional grade assurances from -the experts who know it best, while seamlessly integrating with existing -tools. -By subscribing you will help me (`Giampaolo Rodola`_) support psutil -future development. Alternatively consider making a small `donation`_. -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. +Sponsors +-------- + +.. raw:: html + +
+ + + +
+
+ + add your logo + +Supporters +---------- + +None yet. + +.. raw:: html + + add your avatar Install -------- +======= Linux Ubuntu / Debian:: @@ -2486,6 +2504,17 @@ Running tests $ python3 -m psutil.tests +Security +======== + +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. + +.. _`Giampaolo Rodola`: https://gmpy.dev/about +.. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 +.. _Tidelift security contact: https://tidelift.com/security +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme + Development guide ================= From ba083a0eea87331b67b57c3c83e8cc53faa12b10 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 19 Oct 2020 15:43:53 +0200 Subject: [PATCH 0642/1714] fix #1855 doc --- docs/index.rst | 5 ++++- scripts/internal/print_downloads.py | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index b3b21ea7cc..3898f9da49 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2398,7 +2398,10 @@ Kill process tree if include_parent: children.append(parent) for p in children: - p.send_signal(sig) + try: + p.send_signal(sig) + except psutil.NoSuchProcess: + pass gone, alive = psutil.wait_procs(children, timeout=timeout, callback=on_terminate) return (gone, alive) diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 48828c7482..7e5c46315c 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -29,6 +29,7 @@ LIMIT = 100 GITHUB_SCRIPT_URL = "https://github.com/giampaolo/psutil/blob/master/" \ "scripts/internal/pypistats.py" +LAST_UPDATE = None bytes_billed = 0 @@ -57,23 +58,28 @@ def query(cmd): def top_packages(): - return query("pypinfo --all --json --days %s --limit %s '' project" % ( + global LAST_UPDATE + ret = query("pypinfo --all --json --days %s --limit %s '' project" % ( DAYS, LIMIT)) + LAST_UPDATE = ret['last_update'] + return [(x['project'], x['download_count']) for x in ret['rows']] def ranking(): data = top_packages() - for i, line in enumerate(data['rows'], 1): - if line['project'] == PKGNAME: + i = 1 + for name, downloads in data: + if name == PKGNAME: return i + i += 1 raise ValueError("can't find %s" % PKGNAME) def downloads(): data = top_packages() - for line in data['rows']: - if line['project'] == PKGNAME: - return line['download_count'] + for name, downloads in data: + if name == PKGNAME: + return downloads raise ValueError("can't find %s" % PKGNAME) @@ -123,16 +129,18 @@ def print_markdown_table(title, left, rows): def main(): + downs = downloads() + print("# Download stats") print("") s = "psutil download statistics of the last %s days (last update " % DAYS - s += "*%s*).\n" % top_packages()['last_update'] + s += "*%s*).\n" % LAST_UPDATE s += "Generated via [pypistats.py](%s) script.\n" % GITHUB_SCRIPT_URL print(s) data = [ - {'what': 'Per month', 'download_count': downloads()}, - {'what': 'Per day', 'download_count': int(downloads() / 30)}, + {'what': 'Per month', 'download_count': downs}, + {'what': 'Per day', 'download_count': int(downs / 30)}, {'what': 'PYPI ranking', 'download_count': ranking()} ] print_markdown_table('Overview', 'what', data) From 02cd86a57443be9d78d98461730aee4ead564df4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 23 Oct 2020 16:41:01 +0300 Subject: [PATCH 0643/1714] remove weird 'return xxx' left in code --- psutil/_common.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 5ead5d9056..b7a5478658 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -303,9 +303,6 @@ def __init__(self, pid, name=None, msg=None): details = "(pid=%s)" % self.pid self.msg = "process no longer exists " + details - def __path__(self): - return 'xxx' - class ZombieProcess(NoSuchProcess): """Exception raised when querying a zombie process. This is From 89ae354ab7704db69a3f6c880234d21719558511 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 23 Oct 2020 19:57:17 +0200 Subject: [PATCH 0644/1714] [FreeBSD] process resource limits (#1859) (#809) --- HISTORY.rst | 3 +- docs/_static/css/custom.css | 2 +- docs/index.rst | 85 ++++++++++++++++------------- psutil/__init__.py | 72 +++++++++---------------- psutil/_psbsd.py | 12 +++++ psutil/_pslinux.py | 6 --- psutil/_psutil_bsd.c | 4 ++ psutil/_psutil_linux.c | 43 --------------- psutil/_psutil_posix.c | 99 ++++++++++++++++++++++++++++++++++ psutil/arch/freebsd/specific.c | 85 +++++++++++++++++++++++++++++ psutil/arch/freebsd/specific.h | 2 + psutil/tests/test_bsd.py | 1 + psutil/tests/test_contracts.py | 30 ++++++----- psutil/tests/test_process.py | 5 +- scripts/procinfo.py | 5 +- 15 files changed, 303 insertions(+), 151 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1ef5573ab0..d92dce7346 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,7 +7,8 @@ XXXX-XX-XX **Enhancements** -- 893_: implement `Process.environ()` on BSD family. (patch by Armin Gruner) +- 809_: [FreeBSD] add support for `Process.rlimit()`. +- 893_: [BSD] add support for `Process.environ()` (patch by Armin Gruner) - 1830_: [UNIX] `net_if_stats()`'s `isup` also checks whether the NIC is running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) - 1837_: [Linux] improved battery detection and charge "secsleft" calculation diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index c5c201e4f1..e88f530774 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -4,7 +4,7 @@ } .rst-content dl:not(.docutils) { - margin: 0px 0px 0px 0px; + margin: 0px 0px 0px 0px !important; } .data dd { diff --git a/docs/index.rst b/docs/index.rst index 3898f9da49..c4ff6a259e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1348,16 +1348,17 @@ Process class >>> import psutil >>> p = psutil.Process() - >>> # process may open no more than 128 file descriptors - >>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128)) - >>> # process may create files no bigger than 1024 bytes - >>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024)) - >>> # get - >>> p.rlimit(psutil.RLIMIT_FSIZE) + >>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128)) # process can open max 128 file descriptors + >>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024)) # can create files no bigger than 1024 bytes + >>> p.rlimit(psutil.RLIMIT_FSIZE) # get (1024, 1024) >>> - Availability: Linux + Also see `procinfo.py`_ script. + + Availability: Linux, FreeBSD + + .. versionchanged:: 5.7.3 added FreeBSD support .. method:: io_counters() @@ -2247,29 +2248,43 @@ Process priority constants Process resources constants --------------------------- -.. data:: RLIM_INFINITY -.. data:: RLIMIT_AS -.. data:: RLIMIT_CORE -.. data:: RLIMIT_CPU -.. data:: RLIMIT_DATA -.. data:: RLIMIT_FSIZE -.. data:: RLIMIT_LOCKS -.. data:: RLIMIT_MEMLOCK -.. data:: RLIMIT_MSGQUEUE -.. data:: RLIMIT_NICE -.. data:: RLIMIT_NOFILE -.. data:: RLIMIT_NPROC -.. data:: RLIMIT_RSS -.. data:: RLIMIT_RTPRIO -.. data:: RLIMIT_RTTIME -.. data:: RLIMIT_SIGPENDING -.. data:: RLIMIT_STACK - - Constants used for getting and setting process resource limits to be used in - conjunction with :meth:`psutil.Process.rlimit()`. See `man prlimit`_ for - further information. +Linux / FreeBSD: - Availability: Linux + .. data:: RLIM_INFINITY + .. data:: RLIMIT_AS + .. data:: RLIMIT_CORE + .. data:: RLIMIT_CPU + .. data:: RLIMIT_DATA + .. data:: RLIMIT_FSIZE + .. data:: RLIMIT_MEMLOCK + .. data:: RLIMIT_NOFILE + .. data:: RLIMIT_NPROC + .. data:: RLIMIT_RSS + .. data:: RLIMIT_STACK + +Linux specific: + + .. data:: RLIMIT_LOCKS + .. data:: RLIMIT_MSGQUEUE + .. data:: RLIMIT_NICE + .. data:: RLIMIT_RTPRIO + .. data:: RLIMIT_RTTIME + .. data:: RLIMIT_SIGPENDING + +FreeBSD specific: + + .. data:: RLIMIT_SWAP + .. data:: RLIMIT_SBSIZE + .. data:: RLIMIT_NPTS + +Constants used for getting and setting process resource limits to be used in +conjunction with :meth:`psutil.Process.rlimit()`. See `resource.getrlimit`_ +for further information. + +Availability: Linux, FreeBSD + +.. versionchanged:: 5.7.3 added FreeBSD support, added ``RLIMIT_SWAP``, + ``RLIMIT_SBSIZE``, ``RLIMIT_NPTS``. Connections constants --------------------- @@ -2513,11 +2528,6 @@ Security To report a security vulnerability, please use the `Tidelift security contact`_. Tidelift will coordinate the fix and disclosure. -.. _`Giampaolo Rodola`: https://gmpy.dev/about -.. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 -.. _Tidelift security contact: https://tidelift.com/security -.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme - Development guide ================= @@ -2874,13 +2884,12 @@ Timeline .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py .. _`development guide`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 .. _`enum`: https://docs.python.org/3/library/enum.html#module-enum .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py .. _`GetDriveType`: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypea +.. _`GetExitCodeProcess`: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ .. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass -.. _`GetExitCodeProcess`: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess .. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`hash`: https://docs.python.org/3/library/functions.html#hash .. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py @@ -2908,6 +2917,7 @@ Timeline .. _`os.times`: https://docs.python.org//library/os.html#os.times .. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py .. _`PROCESS_MEMORY_COUNTERS_EX`: https://docs.microsoft.com/en-us/windows/desktop/api/psapi/ns-psapi-_process_memory_counters_ex +.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py .. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py .. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit .. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit @@ -2920,9 +2930,10 @@ Timeline .. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET .. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM .. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd -.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen .. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait +.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen .. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py .. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess .. _Tidelift security contact: https://tidelift.com/security .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme diff --git a/psutil/__init__.py b/psutil/__init__.py index 2108a40e03..cc4e79cc7a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -102,44 +102,6 @@ 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 @@ -197,6 +159,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", @@ -207,6 +170,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", @@ -222,9 +190,22 @@ "users", "boot_time", # others ] +__all__.extend(_psplatform.__extra__all__) + +if LINUX or FREEBSD: + # 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 -__all__.extend(_psplatform.__extra__all__) __author__ = "Giampaolo Rodola'" __version__ = "5.7.3" version_info = tuple([int(num) for num in __version__.split('.')]) @@ -801,7 +782,7 @@ def ionice(self, ioclass=None, value=None): else: return self._proc.ionice_set(ioclass, value) - # Linux only + # Linux / FreeBSD only if hasattr(_psplatform.Process, "rlimit"): def rlimit(self, resource, limits=None): @@ -809,15 +790,12 @@ 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) + return self._proc.rlimit(resource, limits) # Windows, Linux and FreeBSD only if hasattr(_psplatform.Process, "cpu_affinity_get"): @@ -831,7 +809,7 @@ def cpu_affinity(self, cpus=None): (Windows, Linux and BSD only). """ if cpus is None: - return list(set(self._proc.cpu_affinity_get())) + return sorted(set(self._proc.cpu_affinity_get())) else: if not cpus: if hasattr(self._proc, "_get_eligible_cpus"): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 9565406b10..428c8bde39 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -904,3 +904,15 @@ def cpu_affinity_set(self, cpus): @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/psutil/_pslinux.py b/psutil/_pslinux.py index d5ce358c9a..683cef5d88 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -80,12 +80,6 @@ HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get") _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) - # Number of clock ticks per second CLOCK_TICKS = os.sysconf("SC_CLK_TCK") PAGESIZE = os.sysconf("SC_PAGE_SIZE") diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index c4450d7dfc..6933260a38 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1097,6 +1097,10 @@ static PyMethodDef mod_methods[] = { "Return process CPU affinity."}, {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS, "Set process CPU affinity."}, + {"proc_getrlimit", psutil_proc_getrlimit, METH_VARARGS, + "Get process resource limits."}, + {"proc_setrlimit", psutil_proc_setrlimit, METH_VARARGS, + "Set process resource limits."}, {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, "Return an XML string to determine the number physical CPUs."}, #endif diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 415474388d..4def9692d4 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -606,7 +606,6 @@ static PyMethodDef mod_methods[] = { void init_psutil_linux(void) #endif /* PY_MAJOR_VERSION */ { - PyObject *v; #if PY_MAJOR_VERSION >= 3 PyObject *mod = PyModule_Create(&moduledef); #else @@ -616,48 +615,6 @@ static PyMethodDef mod_methods[] = { INITERR; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) INITERR; -#if PSUTIL_HAVE_PRLIMIT - if (PyModule_AddIntConstant(mod, "RLIMIT_AS", RLIMIT_AS)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_CORE", RLIMIT_CORE)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_CPU", RLIMIT_CPU)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_DATA", RLIMIT_DATA)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_FSIZE", RLIMIT_FSIZE)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_NOFILE", RLIMIT_NOFILE)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_NPROC", RLIMIT_NPROC)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_RSS", RLIMIT_RSS)) INITERR; - if (PyModule_AddIntConstant(mod, "RLIMIT_STACK", RLIMIT_STACK)) INITERR; - -#if defined(HAVE_LONG_LONG) - if (sizeof(RLIM_INFINITY) > sizeof(long)) { - v = PyLong_FromLongLong((PY_LONG_LONG) RLIM_INFINITY); - } else -#endif - { - v = PyLong_FromLong((long) RLIM_INFINITY); - } - if (v) { - PyModule_AddObject(mod, "RLIM_INFINITY", v); - } - -#ifdef RLIMIT_MSGQUEUE - if (PyModule_AddIntConstant(mod, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE)) INITERR; -#endif -#ifdef RLIMIT_NICE - if (PyModule_AddIntConstant(mod, "RLIMIT_NICE", RLIMIT_NICE)) INITERR; -#endif -#ifdef RLIMIT_RTPRIO - if (PyModule_AddIntConstant(mod, "RLIMIT_RTPRIO", RLIMIT_RTPRIO)) INITERR; -#endif -#ifdef RLIMIT_RTTIME - if (PyModule_AddIntConstant(mod, "RLIMIT_RTTIME", RLIMIT_RTTIME)) INITERR; -#endif -#ifdef RLIMIT_SIGPENDING - if (PyModule_AddIntConstant(mod, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING)) - INITERR; -#endif -#endif if (PyModule_AddIntConstant(mod, "DUPLEX_HALF", DUPLEX_HALF)) INITERR; if (PyModule_AddIntConstant(mod, "DUPLEX_FULL", DUPLEX_FULL)) INITERR; if (PyModule_AddIntConstant(mod, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN)) INITERR; diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 1182765d7e..876b412923 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -41,6 +41,9 @@ #elif defined(PSUTIL_AIX) #include #endif +#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + #include +#endif #include "_psutil_common.h" @@ -668,6 +671,102 @@ static PyMethodDef mod_methods[] = { if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) INITERR; #endif +#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + PyObject *v; + +#ifdef RLIMIT_AS + if (PyModule_AddIntConstant(mod, "RLIMIT_AS", RLIMIT_AS)) INITERR; +#endif + +#ifdef RLIMIT_CORE + if (PyModule_AddIntConstant(mod, "RLIMIT_CORE", RLIMIT_CORE)) INITERR; +#endif + +#ifdef RLIMIT_CPU + if (PyModule_AddIntConstant(mod, "RLIMIT_CPU", RLIMIT_CPU)) INITERR; +#endif + +#ifdef RLIMIT_DATA + if (PyModule_AddIntConstant(mod, "RLIMIT_DATA", RLIMIT_DATA)) INITERR; +#endif + +#ifdef RLIMIT_FSIZE + if (PyModule_AddIntConstant(mod, "RLIMIT_FSIZE", RLIMIT_FSIZE)) INITERR; +#endif + +#ifdef RLIMIT_MEMLOCK + if (PyModule_AddIntConstant(mod, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK)) INITERR; +#endif + +#ifdef RLIMIT_NOFILE + if (PyModule_AddIntConstant(mod, "RLIMIT_NOFILE", RLIMIT_NOFILE)) INITERR; +#endif + +#ifdef RLIMIT_NPROC + if (PyModule_AddIntConstant(mod, "RLIMIT_NPROC", RLIMIT_NPROC)) INITERR; +#endif + +#ifdef RLIMIT_RSS + if (PyModule_AddIntConstant(mod, "RLIMIT_RSS", RLIMIT_RSS)) INITERR; +#endif + +#ifdef RLIMIT_STACK + if (PyModule_AddIntConstant(mod, "RLIMIT_STACK", RLIMIT_STACK)) INITERR; +#endif + +// Linux specific + +#ifdef RLIMIT_LOCKS + if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) INITERR; +#endif + +#ifdef RLIMIT_MSGQUEUE + if (PyModule_AddIntConstant(mod, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE)) INITERR; +#endif + +#ifdef RLIMIT_NICE + if (PyModule_AddIntConstant(mod, "RLIMIT_NICE", RLIMIT_NICE)) INITERR; +#endif + +#ifdef RLIMIT_RTPRIO + if (PyModule_AddIntConstant(mod, "RLIMIT_RTPRIO", RLIMIT_RTPRIO)) INITERR; +#endif + +#ifdef RLIMIT_RTTIME + if (PyModule_AddIntConstant(mod, "RLIMIT_RTTIME", RLIMIT_RTTIME)) INITERR; +#endif + +#ifdef RLIMIT_SIGPENDING + if (PyModule_AddIntConstant(mod, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING)) INITERR; +#endif + +// Free specific + +#ifdef RLIMIT_SWAP + if (PyModule_AddIntConstant(mod, "RLIMIT_SWAP", RLIMIT_SWAP)) INITERR; +#endif + +#ifdef RLIMIT_SBSIZE + if (PyModule_AddIntConstant(mod, "RLIMIT_SBSIZE", RLIMIT_SBSIZE)) INITERR; +#endif + +#ifdef RLIMIT_NPTS + if (PyModule_AddIntConstant(mod, "RLIMIT_NPTS", RLIMIT_NPTS)) INITERR; +#endif + +#if defined(HAVE_LONG_LONG) + if (sizeof(RLIM_INFINITY) > sizeof(long)) { + v = PyLong_FromLongLong((PY_LONG_LONG) RLIM_INFINITY); + } else +#endif + { + v = PyLong_FromLong((long) RLIM_INFINITY); + } + if (v) { + PyModule_AddObject(mod, "RLIM_INFINITY", v); + } +#endif // defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + if (mod == NULL) INITERR; #if PY_MAJOR_VERSION >= 3 diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index c783264792..fcfce131ae 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -1077,3 +1077,88 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } + + +/* + * An emulation of Linux prlimit(). Returns a (soft, hard) tuple. + */ +PyObject * +psutil_proc_getrlimit(PyObject *self, PyObject *args) { + pid_t pid; + int ret; + int resource; + size_t len; + int name[5]; + struct rlimit rlp; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &resource)) + return NULL; + + name[0] = CTL_KERN; + name[1] = KERN_PROC; + name[2] = KERN_PROC_RLIMIT; + name[3] = pid; + name[4] = resource; + len = sizeof(rlp); + + ret = sysctl(name, 5, &rlp, &len, NULL, 0); + if (ret == -1) + return PyErr_SetFromErrno(PyExc_OSError); + +#if defined(HAVE_LONG_LONG) + return Py_BuildValue("LL", + (PY_LONG_LONG) rlp.rlim_cur, + (PY_LONG_LONG) rlp.rlim_max); +#else + return Py_BuildValue("ll", + (long) rlp.rlim_cur, + (long) rlp.rlim_max); +#endif +} + + +/* + * An emulation of Linux prlimit() (set). + */ +PyObject * +psutil_proc_setrlimit(PyObject *self, PyObject *args) { + pid_t pid; + int ret; + int resource; + int name[5]; + struct rlimit new; + struct rlimit *newp = NULL; + PyObject *py_soft = NULL; + PyObject *py_hard = NULL; + + if (! PyArg_ParseTuple( + args, _Py_PARSE_PID "iOO", &pid, &resource, &py_soft, &py_hard)) + return NULL; + + name[0] = CTL_KERN; + name[1] = KERN_PROC; + name[2] = KERN_PROC_RLIMIT; + name[3] = pid; + name[4] = resource; + +#if defined(HAVE_LONG_LONG) + new.rlim_cur = PyLong_AsLongLong(py_soft); + if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; + new.rlim_max = PyLong_AsLongLong(py_hard); + if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; +#else + new.rlim_cur = PyLong_AsLong(py_soft); + if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; + new.rlim_max = PyLong_AsLong(py_hard); + if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; +#endif + newp = &new; + ret = sysctl(name, 5, NULL, 0, newp, sizeof(*newp)); + if (ret == -1) + return PyErr_SetFromErrno(PyExc_OSError); + Py_RETURN_NONE; +} diff --git a/psutil/arch/freebsd/specific.h b/psutil/arch/freebsd/specific.h index 875c816646..61c3f07b70 100644 --- a/psutil/arch/freebsd/specific.h +++ b/psutil/arch/freebsd/specific.h @@ -24,6 +24,8 @@ PyObject* psutil_proc_memory_maps(PyObject* self, PyObject* args); PyObject* psutil_proc_num_fds(PyObject* self, PyObject* args); PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); PyObject* psutil_proc_threads(PyObject* self, PyObject* args); +PyObject* psutil_proc_getrlimit(PyObject* self, PyObject* args); +PyObject* psutil_proc_setrlimit(PyObject* self, PyObject* args); PyObject* psutil_swap_mem(PyObject* self, PyObject* args); PyObject* psutil_virtual_mem(PyObject* self, PyObject* args); PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index c91ee3a54c..06a65087a4 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -474,6 +474,7 @@ def test_sensors_temperatures_against_sysctl(self): psutil.sensors_temperatures()["coretemp"][cpu].high, sysctl_result) + # ===================================================================== # --- OpenBSD # ===================================================================== diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 2d9e5917e6..39a5256960 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -89,25 +89,29 @@ def test_linux_ioprio_windows(self): @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") def test_linux_rlimit(self): ae = self.assertEqual - ae(hasattr(psutil, "RLIM_INFINITY"), LINUX) - ae(hasattr(psutil, "RLIMIT_AS"), LINUX) - ae(hasattr(psutil, "RLIMIT_CORE"), LINUX) - ae(hasattr(psutil, "RLIMIT_CPU"), LINUX) - ae(hasattr(psutil, "RLIMIT_DATA"), LINUX) - ae(hasattr(psutil, "RLIMIT_FSIZE"), LINUX) - ae(hasattr(psutil, "RLIMIT_LOCKS"), LINUX) - ae(hasattr(psutil, "RLIMIT_MEMLOCK"), LINUX) - ae(hasattr(psutil, "RLIMIT_NOFILE"), LINUX) - ae(hasattr(psutil, "RLIMIT_NPROC"), LINUX) - ae(hasattr(psutil, "RLIMIT_RSS"), LINUX) - ae(hasattr(psutil, "RLIMIT_STACK"), LINUX) + 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) ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX) # requires Linux 2.6.8 ae(hasattr(psutil, "RLIMIT_NICE"), LINUX) # requires Linux 2.6.12 ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX) # requires Linux 2.6.12 ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX) # requires Linux 2.6.25 ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX) # requires Linux 2.6.8 + ae(hasattr(psutil, "RLIMIT_SWAP"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_SBSIZE"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_NPTS"), FREEBSD) + class TestAvailSystemAPIs(PsutilTestCase): @@ -155,7 +159,7 @@ def test_ionice(self): @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") def test_rlimit(self): # requires Linux 2.6.36 - self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX) + self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX or FREEBSD) def test_io_counters(self): hasit = hasattr(psutil.Process, "io_counters") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b2328ba2f1..0d63979bfa 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -448,8 +448,9 @@ def test_rlimit_set(self): 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) + 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)) diff --git a/scripts/procinfo.py b/scripts/procinfo.py index f060538686..8eea3377a3 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -108,11 +108,14 @@ "RLIMIT_NICE": "nice", "RLIMIT_NOFILE": "openfiles", "RLIMIT_NPROC": "maxprocesses", + "RLIMIT_NPTS": "pseudoterms", "RLIMIT_RSS": "rss", "RLIMIT_RTPRIO": "realtimeprio", "RLIMIT_RTTIME": "rtimesched", + "RLIMIT_SBSIZE": "sockbufsize", "RLIMIT_SIGPENDING": "sigspending", "RLIMIT_STACK": "stack", + "RLIMIT_SWAP": "swapuse", } @@ -317,7 +320,7 @@ def run(pid, verbose=False): def main(argv=None): parser = argparse.ArgumentParser( description="print information about a process") - parser.add_argument("pid", type=int, help="process pid") + parser.add_argument("pid", type=int, help="process pid", nargs='?') parser.add_argument('--verbose', '-v', action='store_true', help="print more info") args = parser.parse_args() From 204d55d7e327795418bdf62db4c03eb3fb192109 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 23 Oct 2020 21:04:41 +0300 Subject: [PATCH 0645/1714] pre release --- HISTORY.rst | 2 +- docs/index.rst | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d92dce7346..355471873c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.7.3 ===== -XXXX-XX-XX +2020-10-23 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index c4ff6a259e..bcf8d9b8ad 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2551,9 +2551,13 @@ Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. Timeline ======== +- 2020-10-23: + `5.7.3 `__ - + `what's new `__ - + `diff `__ - 2020-07-15: `5.7.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2020-07-15: `5.7.1 `__ - From 6d42d4949faa2acd0a8e4f55f6472f641ee90199 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 24 Oct 2020 11:08:21 +0300 Subject: [PATCH 0646/1714] setup.py: add long_description_content_type --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 893eb46ba0..615f6d7b4f 100755 --- a/setup.py +++ b/setup.py @@ -328,6 +328,7 @@ def main(): version=VERSION, description=__doc__ .replace('\n', ' ').strip() if __doc__ else '', long_description=get_description(), + long_description_content_type='text/x-rst', keywords=[ 'ps', 'top', 'kill', 'free', 'lsof', 'netstat', 'nice', 'tty', 'ionice', 'uptime', 'taskmgr', 'process', 'df', 'iotop', 'iostat', From b5867a85362f9cb8c02ecf1668aa2dd2ffee6d0e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 24 Oct 2020 16:31:31 +0300 Subject: [PATCH 0647/1714] script to convert README for PYPI --- .flake8 | 1 + MANIFEST.in | 1 + Makefile | 2 ++ scripts/internal/convert_readme.py | 50 ++++++++++++++++++++++++++++++ setup.py | 13 ++++++-- 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100755 scripts/internal/convert_readme.py diff --git a/.flake8 b/.flake8 index 15efab525b..89225a6abd 100644 --- a/.flake8 +++ b/.flake8 @@ -9,5 +9,6 @@ ignore = per-file-ignores = setup.py:T001 scripts/*:T001 + scripts/internal/convert_readme.py:E501,T001 psutil/tests/runner.py:T001 psutil/tests/test_memleaks.py:T001 diff --git a/MANIFEST.in b/MANIFEST.in index b8c2064ea1..f5ceeb7cf6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -117,6 +117,7 @@ include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py include scripts/internal/clinter.py +include scripts/internal/convert_readme.py include scripts/internal/download_wheels_appveyor.py include scripts/internal/download_wheels_github.py include scripts/internal/fix_flake8.py diff --git a/Makefile b/Makefile index c603da8a4f..d9d344f7f2 100644 --- a/Makefile +++ b/Makefile @@ -238,6 +238,7 @@ git-tag-release: ## Git-tag a new release. sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest $(PYTHON) setup.py sdist + $(PYTHON) -m twine check dist/*.tar.gz upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ ${MAKE} sdist @@ -266,6 +267,7 @@ pre-release: ## Check if we're ready to produce a new release. git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain ${MAKE} download-wheels ${MAKE} sdist + $(PYTHON) -m twine check dist/* $(PYTHON) -c \ "from psutil import __version__ as ver; \ doc = open('docs/index.rst').read(); \ diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py new file mode 100755 index 0000000000..d6cae9181a --- /dev/null +++ b/scripts/internal/convert_readme.py @@ -0,0 +1,50 @@ +#!/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. + +""" +Convert README.rst format to make it compatible with PyPI (no raw html). +""" + +import re +import sys + +summary = """\ +Quick links +=========== + +- `Home page `_ +- `Install `_ +- `Documentation `_ +- `Download `_ +- `Forum `_ +- `StackOverflow `_ +- `Blog `_ +- `What's new `_ +""" + +funding = """\ +Sponsors +======== + +.. image:: https://github.com/giampaolo/psutil/raw/master/docs/_static/tidelift-logo.png + :width: 200 + :alt: Alternative text + +`Add your logo `__. + +Example usages""" + + +def main(): + with open(sys.argv[1]) as f: + data = f.read() + data = re.sub(r".. raw:: html\n+\s+
", summary, data) + data = re.sub(r"Sponsors\n========[\s\S]*?Example usages", funding, data) + print(data) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 615f6d7b4f..3a8dab59f1 100755 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ import re import shutil import struct +import subprocess import sys import tempfile import warnings @@ -98,9 +99,15 @@ def get_version(): def get_description(): - README = os.path.join(HERE, 'README.rst') - with open(README, 'r') as f: - return f.read() + script = os.path.join(HERE, "scripts", "internal", "convert_readme.py") + readme = os.path.join(HERE, 'README.rst') + p = subprocess.Popen([sys.executable, script, readme], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if p.returncode != 0: + raise RuntimeError(stderr) + assert not stderr, stderr + return stdout.decode('utf8') @contextlib.contextmanager From 4650d44a9ba6ac06a8161e7778b0556cd5573b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Sat, 24 Oct 2020 09:37:48 -0400 Subject: [PATCH 0648/1714] Use Python 3.8.5 in Travis (#1860) --- .ci/travis/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index 16bd5c0c70..164d1663b2 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -29,8 +29,8 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then pyenv virtualenv 3.7.6 psutil ;; py38) - pyenv install 3.8.2 - pyenv virtualenv 3.8.2 psutil + pyenv install 3.8.5 + pyenv virtualenv 3.8.5 psutil ;; esac pyenv rehash From 241f8ed0cd5c0c792a28e4e6479586a70587177c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 24 Oct 2020 07:40:39 -0700 Subject: [PATCH 0649/1714] replace win line endings --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3a8dab59f1..80c3bc3c8a 100755 --- a/setup.py +++ b/setup.py @@ -106,8 +106,10 @@ def get_description(): stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) - assert not stderr, stderr - return stdout.decode('utf8') + data = stdout.decode('utf8') + if WINDOWS: + data = data.replace('\r\n', '\n') + return data @contextlib.contextmanager From 06c265bafc204be4b8cbc8bbf10213397750e7a0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 24 Oct 2020 18:05:30 +0300 Subject: [PATCH 0650/1714] bump up version --- HISTORY.rst | 5 +++++ psutil/__init__.py | 2 +- scripts/internal/download_wheels_appveyor.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 355471873c..0c24144a4a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,10 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.7.4 (development version) +=========================== + +XXXX-XX-XX + 5.7.3 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index cc4e79cc7a..b2a6efd5ec 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -207,7 +207,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.7.3" +__version__ = "5.7.4" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index b7c0aeae87..bbae2e942b 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -59,7 +59,7 @@ def get_file_urls(options): file_url = job_url + '/' + item['fileName'] urls.append(file_url) if not urls: - print_color("no artifacts found", 'ret') + print_color("no artifacts found", 'red') sys.exit(1) else: for url in sorted(urls, key=lambda x: os.path.basename(x)): From 40abe5c0aa3e258161284b58cedcee1e11a66e75 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 24 Oct 2020 22:00:09 +0200 Subject: [PATCH 0651/1714] disk_partitions() maxfile and maxpath (#1863) --- HISTORY.rst | 10 +++++++ README.rst | 4 +-- docs/index.rst | 30 +++++++++++++------- psutil/__init__.py | 20 +++++++++++-- psutil/_common.py | 3 +- psutil/_psaix.py | 4 ++- psutil/_psbsd.py | 4 ++- psutil/_pslinux.py | 4 ++- psutil/_psosx.py | 4 ++- psutil/_pssunos.py | 4 ++- psutil/_psutil_bsd.c | 5 ---- psutil/arch/freebsd/specific.c | 24 ++++++++-------- psutil/arch/windows/disk.c | 30 ++++++++++++++------ psutil/tests/test_contracts.py | 2 ++ psutil/tests/test_system.py | 20 +++++++++---- scripts/internal/download_wheels_appveyor.py | 2 +- 16 files changed, 117 insertions(+), 53 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 355471873c..54f2d79f8a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.7.4 (development version) +=========================== + +XXXX-XX-XX + +**Enhancements** + +- 1875_: `disk_partitions()` exposes 2 extra fields: `maxfile` and `maxpath`, + which are the maximum file name and path name length. + 5.7.3 ===== diff --git a/README.rst b/README.rst index 70fe83230e..3b28ee5c98 100644 --- a/README.rst +++ b/README.rst @@ -206,8 +206,8 @@ Disks .. code-block:: python >>> psutil.disk_partitions() - [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), - sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext, opts='rw')] + [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid', maxfile=255, maxpath=4096), + sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext, opts='rw', maxfile=255, maxpath=4096)] >>> >>> psutil.disk_usage('/') sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) diff --git a/docs/index.rst b/docs/index.rst index bcf8d9b8ad..86b7ad1dbf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -410,19 +410,27 @@ Disks (e.g. pseudo, memory, duplicate, inaccessible filesystems). Note that this may not be fully reliable on all systems (e.g. on BSD this parameter is ignored). - Named tuple's **fstype** field is a string which varies depending on the - platform. - On Linux it can be one of the values found in /proc/filesystems (e.g. - ``'ext3'`` for an ext3 hard drive o ``'iso9660'`` for the CD-ROM drive). - On Windows it is determined via `GetDriveType`_ and can be either - ``"removable"``, ``"fixed"``, ``"remote"``, ``"cdrom"``, ``"unmounted"`` or - ``"ramdisk"``. On macOS and BSD it is retrieved via `getfsstat`_ syscall. See `disk_usage.py`_ script providing an example usage. + Returns a list of namedtuples with the following fields: + + * **device**: the device path (e.g. ``"/dev/hda1"``). On Windows this is the + drive letter (e.g. ``"C:\\"``). + * **mountpoint**: the mount point path (e.g. ``"/"``). On Windows this is the + drive letter (e.g. ``"C:\\"``). + * **fstype**: the partition filesystem (e.g. ``"ext3"`` on UNIX or ``"NTFS"`` + on Windows). + * **opts**: a comma-separated string indicating different mount options for + the drive/partition. Platform-dependent. + * **maxfile**: the maximum length a file name can have. + * **maxpath**: the maximum length a path name (directory name + base file + name) can have. - >>> import psutil - >>> psutil.disk_partitions() - [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), - sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] + >>> import psutil + >>> psutil.disk_partitions() + [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro', maxfile=255, maxpath=4096), + sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw', maxfile=255, maxpath=4096)] + + .. versionchanged:: 5.7.4 added *maxfile* and *maxpath* fields .. function:: disk_usage(path) diff --git a/psutil/__init__.py b/psutil/__init__.py index cc4e79cc7a..829bb33ea7 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -207,7 +207,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.7.3" +__version__ = "5.7.4" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -2002,7 +2002,23 @@ 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): diff --git a/psutil/_common.py b/psutil/_common.py index b7a5478658..771461d692 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -178,7 +178,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', diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 994366aaa1..57d5378ab2 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -189,7 +189,9 @@ 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 diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 428c8bde39..f0a0c14449 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -328,7 +328,9 @@ def disk_partitions(all=False): 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 diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 683cef5d88..b17b96a25b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1171,7 +1171,9 @@ def disk_partitions(all=False): 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 diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 6a1899316f..be22b48d52 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -201,7 +201,9 @@ 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 diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 62362b89c6..c64ea9ca8a 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -233,7 +233,9 @@ def disk_partitions(all=False): # https://github.com/giampaolo/psutil/issues/1674 debug("skipping %r: %r" % (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 diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 6933260a38..c1b811c6a7 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -443,17 +443,12 @@ psutil_proc_environ(PyObject *self, PyObject *args) { // On *BSD kernels there are a few kernel-only system processes without an // environment (See e.g. "procstat -e 0 | 1 | 2 ..." on FreeBSD.) - // // Some system process have no stats attached at all // (they are marked with P_SYSTEM.) - // // On FreeBSD, it's possible that the process is swapped or paged out, // then there no access to the environ stored in the process' user area. - // // On NetBSD, we cannot call kvm_getenvv2() for a zombie process. - // // To make unittest suite happy, return an empty environment. - // #if defined(PSUTIL_FREEBSD) #if (defined(__FreeBSD_version) && __FreeBSD_version >= 700000) if (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) { diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index fcfce131ae..e7517a4068 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -1142,19 +1142,19 @@ psutil_proc_setrlimit(PyObject *self, PyObject *args) { name[4] = resource; #if defined(HAVE_LONG_LONG) - new.rlim_cur = PyLong_AsLongLong(py_soft); - if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; - new.rlim_max = PyLong_AsLongLong(py_hard); - if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; + new.rlim_cur = PyLong_AsLongLong(py_soft); + if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; + new.rlim_max = PyLong_AsLongLong(py_hard); + if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; #else - new.rlim_cur = PyLong_AsLong(py_soft); - if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; - new.rlim_max = PyLong_AsLong(py_hard); - if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; + new.rlim_cur = PyLong_AsLong(py_soft); + if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; + new.rlim_max = PyLong_AsLong(py_hard); + if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) + return NULL; #endif newp = &new; ret = sysctl(name, 5, NULL, 0, newp, sizeof(*newp)); diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 45e0ee1e62..92171fe59b 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -206,6 +206,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { BOOL mp_flag= TRUE; LPTSTR fs_type[MAX_PATH + 1] = { 0 }; DWORD pflags = 0; + DWORD lpMaximumComponentLength = 0; // max file name PyObject *py_all; PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; @@ -257,8 +258,14 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } ret = GetVolumeInformation( - (LPCTSTR)drive_letter, NULL, _ARRAYSIZE(drive_letter), - NULL, NULL, &pflags, (LPTSTR)fs_type, _ARRAYSIZE(fs_type)); + (LPCTSTR)drive_letter, + NULL, + _ARRAYSIZE(drive_letter), + NULL, + &lpMaximumComponentLength, + &pflags, + (LPTSTR)fs_type, + _ARRAYSIZE(fs_type)); if (ret == 0) { // We might get here in case of a floppy hard drive, in // which case the error is (21, "device not ready"). @@ -274,6 +281,8 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strcat_s(opts, _countof(opts), "rw"); if (pflags & FILE_VOLUME_IS_COMPRESSED) strcat_s(opts, _countof(opts), ",compressed"); + if (pflags & FILE_READ_ONLY_VOLUME) + strcat_s(opts, _countof(opts), ",readonly"); // Check for mount points on this volume and add/get info // (checks first to know if we can even have mount points) @@ -282,17 +291,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { drive_letter, mp_buf, MAX_PATH); if (mp_h != INVALID_HANDLE_VALUE) { while (mp_flag) { - // Append full mount path with drive letter strcpy_s(mp_path, _countof(mp_path), drive_letter); strcat_s(mp_path, _countof(mp_path), mp_buf); py_tuple = Py_BuildValue( - "(ssss)", + "(ssssIi)", drive_letter, mp_path, - fs_type, // Typically NTFS - opts); + fs_type, // typically "NTFS" + opts, + lpMaximumComponentLength, // max file length + MAX_PATH // max path length + ); if (!py_tuple || PyList_Append(py_retlist, py_tuple) == -1) { @@ -317,11 +328,14 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strcat_s(opts, _countof(opts), psutil_get_drive_type(type)); py_tuple = Py_BuildValue( - "(ssss)", + "(ssssIi)", drive_letter, drive_letter, fs_type, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS - opts); + opts, + lpMaximumComponentLength, // max file length + MAX_PATH // max path length + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 39a5256960..72fe8c0c80 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -242,6 +242,8 @@ def test_disk_partitions(self): self.assertIsInstance(disk.mountpoint, str) self.assertIsInstance(disk.fstype, str) self.assertIsInstance(disk.opts, str) + self.assertIsInstance(disk.maxfile, int) + self.assertIsInstance(disk.maxpath, int) @unittest.skipIf(SKIP_SYSCONS, "requires root") def test_net_connections(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 787397ad35..bb6d2b89bd 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -592,6 +592,16 @@ 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) + self.assertIsInstance(nt.maxpath, int) + self.assertGreater(nt.maxfile, 0) + self.assertGreater(nt.maxpath, 0) + # all = False ls = psutil.disk_partitions(all=False) # on travis we get: @@ -599,10 +609,7 @@ def test_disk_partitions(self): # 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) + check_ntuple(disk) if WINDOWS and 'cdrom' in disk.opts: continue if not POSIX: @@ -619,6 +626,7 @@ def test_disk_partitions(self): 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) @@ -632,8 +640,8 @@ def test_disk_partitions(self): 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) diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index b7c0aeae87..bbae2e942b 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -59,7 +59,7 @@ def get_file_urls(options): file_url = job_url + '/' + item['fileName'] urls.append(file_url) if not urls: - print_color("no artifacts found", 'ret') + print_color("no artifacts found", 'red') sys.exit(1) else: for url in sorted(urls, key=lambda x: os.path.basename(x)): From 5fbfcdac559a0c29aefd50acdf92433c1216dd68 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 Oct 2020 16:06:10 +0100 Subject: [PATCH 0652/1714] update README --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3b28ee5c98..4095a3c5d6 100644 --- a/README.rst +++ b/README.rst @@ -225,7 +225,7 @@ Network {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0), 'lo': netio(bytes_sent=2838627, bytes_recv=2838627, packets_sent=30567, packets_recv=30567, errin=0, errout=0, dropin=0, dropout=0)} >>> - >>> psutil.net_connections() + >>> psutil.net_connections(kind='tcp') [sconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254), sconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING', pid=2987), ...] @@ -359,7 +359,7 @@ Process management [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768), popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] >>> - >>> p.connections() + >>> p.connections(kind='tcp') [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')] >>> From 9aba2efe6cee0a343002d0453674aca68a7b7aec Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 31 Oct 2020 15:37:31 +0100 Subject: [PATCH 0653/1714] Fix py 3.9 [WinError 998] Invalid access to memory location (#1866) --- README.rst | 4 +- appveyor.yml | 19 +++--- psutil/_psutil_common.c | 7 -- psutil/_psutil_common.h | 8 +++ psutil/arch/windows/process_info.c | 72 +++++++++++++------- psutil/tests/test_contracts.py | 5 ++ psutil/tests/test_misc.py | 3 +- scripts/internal/download_wheels_appveyor.py | 2 +- scripts/internal/winmake.py | 29 +++++++- scripts/netstat.py | 3 +- 10 files changed, 105 insertions(+), 47 deletions(-) diff --git a/README.rst b/README.rst index 3b28ee5c98..4095a3c5d6 100644 --- a/README.rst +++ b/README.rst @@ -225,7 +225,7 @@ Network {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0), 'lo': netio(bytes_sent=2838627, bytes_recv=2838627, packets_sent=30567, packets_recv=30567, errin=0, errout=0, dropin=0, dropout=0)} >>> - >>> psutil.net_connections() + >>> psutil.net_connections(kind='tcp') [sconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254), sconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING', pid=2987), ...] @@ -359,7 +359,7 @@ Process management [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768), popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] >>> - >>> p.connections() + >>> p.connections(kind='tcp') [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')] >>> diff --git a/appveyor.yml b/appveyor.yml index e0912a1ed8..ff9d35b91f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,10 +15,6 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" @@ -31,16 +27,17 @@ environment: PYTHON_VERSION: "3.8.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python39" + PYTHON_VERSION: "3.9.x" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + PYTHON_ARCH: "32" + # 64 bits - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" @@ -53,6 +50,11 @@ environment: PYTHON_VERSION: "3.8.x" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python39-x64" + PYTHON_VERSION: "3.9.x" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + PYTHON_ARCH: "64" + init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" @@ -103,3 +105,4 @@ only_commits: - psutil/tests/* - scripts/* - setup.py + diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index f821aba380..4178e0c0b7 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -199,13 +199,6 @@ int PSUTIL_WINVER; SYSTEM_INFO PSUTIL_SYSTEM_INFO; CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; -#define NT_FACILITY_MASK 0xfff -#define NT_FACILITY_SHIFT 16 -#define NT_FACILITY(Status) \ - ((((ULONG)(Status)) >> NT_FACILITY_SHIFT) & NT_FACILITY_MASK) -#define NT_NTWIN32(status) (NT_FACILITY(Status) == FACILITY_WIN32) -#define WIN32_FROM_NTSTATUS(Status) (((ULONG)(Status)) & 0xffff) - // A wrapper around GetModuleHandle and GetProcAddress. PVOID diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 3162772eb2..cb0b399d8e 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -134,6 +134,14 @@ void convert_kvm_err(const char *syscall, char *errbuf); #define MALLOC_ZERO(x) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (x)) #define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) + #define _NT_FACILITY_MASK 0xfff + #define _NT_FACILITY_SHIFT 16 + #define _NT_FACILITY(status) \ + ((((ULONG)(status)) >> _NT_FACILITY_SHIFT) & _NT_FACILITY_MASK) + + #define NT_NTWIN32(status) (_NT_FACILITY(status) == FACILITY_WIN32) + #define WIN32_FROM_NTSTATUS(status) (((ULONG)(status)) & 0xffff) + #define LO_T 1e-7 #define HI_T 429.4967296 diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 73a6991279..36283addc4 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -50,6 +50,36 @@ enum psutil_process_data_kind { }; +static void +psutil_convert_winerr(ULONG err, char* syscall) { + char fullmsg[8192]; + + if (err == ERROR_NOACCESS) { + sprintf( + fullmsg, + "(originated from %s -> ERROR_NOACCESS; converted to AccessDenied)", + syscall); + psutil_debug(fullmsg); + AccessDenied(fullmsg); + } + else { + PyErr_SetFromOSErrnoWithSyscall(syscall); + } +} + + +static void +psutil_convert_ntstatus_err(NTSTATUS status, char* syscall) { + ULONG err; + + if (NT_NTWIN32(status)) + err = WIN32_FROM_NTSTATUS(status); + else + err = RtlNtStatusToDosErrorNoTeb(status); + psutil_convert_winerr(err, syscall); +} + + /* * Get data from the process with the given pid. The data is returned * in the pdata output member as a nul terminated string which must be @@ -87,16 +117,14 @@ psutil_get_process_data(DWORD pid, http://www.drdobbs.com/embracing-64-bit-windows/184401966 */ SIZE_T size = 0; -#ifndef _WIN64 - static __NtQueryInformationProcess NtWow64QueryInformationProcess64 = NULL; - static _NtWow64ReadVirtualMemory64 NtWow64ReadVirtualMemory64 = NULL; -#endif HANDLE hProcess = NULL; LPCVOID src; WCHAR *buffer = NULL; #ifdef _WIN64 LPVOID ppeb32 = NULL; #else + static __NtQueryInformationProcess NtWow64QueryInformationProcess64 = NULL; + static _NtWow64ReadVirtualMemory64 NtWow64ReadVirtualMemory64 = NULL; PVOID64 src64; BOOL weAreWow64; BOOL theyAreWow64; @@ -133,7 +161,7 @@ psutil_get_process_data(DWORD pid, if (!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 - PyErr_SetFromWindowsErr(0); + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); goto error; } @@ -146,7 +174,7 @@ psutil_get_process_data(DWORD pid, { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 - PyErr_SetFromWindowsErr(0); + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); goto error; } @@ -164,7 +192,7 @@ psutil_get_process_data(DWORD pid, break; } } else -#else +#else // #ifdef _WIN64 /* 32 bit case. Check if the target is also 32 bit. */ if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || !IsWow64Process(hProcess, &theyAreWow64)) { @@ -206,10 +234,9 @@ psutil_get_process_data(DWORD pid, sizeof(pbi64), NULL); if (!NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, - "NtWow64QueryInformationProcess64(ProcessBasicInformation)" - ); + psutil_convert_ntstatus_err( + status, + "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); goto error; } @@ -221,7 +248,8 @@ psutil_get_process_data(DWORD pid, sizeof(peb64), NULL); if (!NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr(status, "NtWow64ReadVirtualMemory64"); + psutil_convert_ntstatus_err( + status, "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)"); goto error; } @@ -233,10 +261,8 @@ psutil_get_process_data(DWORD pid, sizeof(procParameters64), NULL); if (!NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr( - status, - "NtWow64ReadVirtualMemory64(ProcessParameters)" - ); + psutil_convert_ntstatus_err( + status, "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)"); goto error; } @@ -284,7 +310,7 @@ psutil_get_process_data(DWORD pid, { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 - PyErr_SetFromWindowsErr(0); + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); goto error; } @@ -297,7 +323,7 @@ psutil_get_process_data(DWORD pid, { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 - PyErr_SetFromWindowsErr(0); + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); goto error; } @@ -343,7 +369,7 @@ psutil_get_process_data(DWORD pid, size, NULL); if (!NT_SUCCESS(status)) { - psutil_SetFromNTStatusErr(status, "NtWow64ReadVirtualMemory64"); + psutil_convert_ntstatus_err(status, "NtWow64ReadVirtualMemory64"); goto error; } } else @@ -351,7 +377,7 @@ psutil_get_process_data(DWORD pid, if (!ReadProcessMemory(hProcess, src, buffer, size, NULL)) { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 - PyErr_SetFromWindowsErr(0); + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); goto error; } @@ -698,16 +724,16 @@ psutil_proc_info(PyObject *self, PyObject *args) { py_retlist = Py_BuildValue( #if defined(_WIN64) - "kkdddiKKKKKK" "kKKKKKKKKK", + "kkdddkKKKKKK" "kKKKKKKKKK", #else - "kkdddiKKKKKK" "kIIIIIIIII", + "kkdddkKKKKKK" "kIIIIIIIII", #endif process->HandleCount, // num handles ctx_switches, // num ctx switches user_time, // cpu user time kernel_time, // cpu kernel time create_time, // create time - (int)process->NumberOfThreads, // num threads + process->NumberOfThreads, // num threads // IO counters process->ReadOperationCount.QuadPart, // io rcount process->WriteOperationCount.QuadPart, // io wcount diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 72fe8c0c80..659dc01806 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -32,6 +32,7 @@ from psutil._compat import FileNotFoundError from psutil._compat import long from psutil._compat import range +from psutil.tests import APPVEYOR from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import GITHUB_WHEELS @@ -451,6 +452,8 @@ def ppid(self, ret, info): def name(self, ret, info): self.assertIsInstance(ret, str) + if APPVEYOR and not ret and info['status'] == 'stopped': + return # on AIX, "" processes don't have names if not AIX: assert ret @@ -521,6 +524,8 @@ def ionice(self, ret, info): def num_threads(self, ret, info): self.assertIsInstance(ret, int) + if APPVEYOR and not ret and info['status'] == 'stopped': + return self.assertGreaterEqual(ret, 1) def threads(self, ret, info): diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 8fcee12a76..f4e1a5df12 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -28,7 +28,6 @@ from psutil._compat import PY3 from psutil.tests import APPVEYOR from psutil.tests import CI_TESTING -from psutil.tests import DEVNULL from psutil.tests import HAS_BATTERY from psutil.tests import HAS_MEMORY_MAPS from psutil.tests import HAS_NET_IO_COUNTERS @@ -719,7 +718,7 @@ def test_pmap(self): 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) + self.assert_stdout('procsmem.py') def test_killall(self): self.assert_syntax('killall.py') diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index bbae2e942b..a6773f34c2 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -25,7 +25,7 @@ BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.5', '3.6', '3.7', '3.8'] +PY_VERSIONS = ['2.7', '3.6', '3.7', '3.8', '3.9'] TIMEOUT = 30 diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 3d9d0a8d55..faa2aea8a6 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -525,6 +525,12 @@ def print_api_speed(): sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) +def download_appveyor_wheels(): + """Download appveyor wheels.""" + sh("%s -Wa scripts\\internal\\download_wheels_appveyor.py " + "--user giampaolo --project psutil" % PYTHON) + + def get_python(path): if not path: return sys.executable @@ -532,9 +538,25 @@ def get_python(path): return path # try to look for a python installation given a shortcut name path = path.replace('.', '') - vers = ('26', '27', '36', '37', '38', - '26-64', '27-64', '36-64', '37-64', '38-64' - '26-32', '27-32', '36-32', '37-32', '38-32') + vers = ( + '26', + '26-32', + '26-64', + '27', + '27-32', + '27-64', + '36', + '36-32', + '36-64', + '37', + '37-32', + '37-64', + '38', + '38-32', + '38-64', + '39-32', + '39-64', + ) for v in vers: pypath = r'C:\\python%s\python.exe' % v if path in pypath and os.path.isfile(pypath): @@ -554,6 +576,7 @@ def main(): sp.add_parser('build', help="build") sp.add_parser('clean', help="deletes dev files") sp.add_parser('coverage', help="run coverage tests.") + sp.add_parser('download-appveyor-wheels', help="download wheels.") sp.add_parser('help', help="print this help") sp.add_parser('install', help="build + install in develop/edit mode") sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") diff --git a/scripts/netstat.py b/scripts/netstat.py index 5a21358e79..1832a0966f 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -48,13 +48,14 @@ def main(): raddr = "" if c.raddr: raddr = "%s:%s" % (c.raddr) + name = proc_names.get(c.pid, '?') or '' print(templ % ( proto_map[(c.family, c.type)], laddr, raddr or AD, c.status, c.pid or AD, - proc_names.get(c.pid, '?')[:15], + name[:15], )) From 9b0efdd06aaac77e4579cab13d894ebed209749f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Nov 2020 14:50:48 +0100 Subject: [PATCH 0654/1714] fix #1868: missing fields from /proc/pid/stat on Alpine Linux --- HISTORY.rst | 5 +++++ psutil/_pslinux.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 54f2d79f8a..fb8ec8f158 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,11 @@ XXXX-XX-XX - 1875_: `disk_partitions()` exposes 2 extra fields: `maxfile` and `maxpath`, which are the maximum file name and path name length. +**Bug fixes** + +- 1868_: [Linux] /proc/pid/stat is missing 2 fields on Alpine Linux (cpu_num + and delayacct_blkio_ticks) and are now being set to None. + 5.7.3 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b17b96a25b..63b76697f0 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1577,8 +1577,15 @@ def _parse_stat_file(self): 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' + # https://github.com/giampaolo/psutil/issues/1868 + try: + ret['cpu_num'] = fields[36] + except IndexError: + ret['cpu_num'] = None + try: + ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks' + except IndexError: + ret['blkio_ticks'] = None return ret From db80c3e2ff263b17332e9c7cf35fdc4fd2af2266 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Nov 2020 21:15:30 +0100 Subject: [PATCH 0655/1714] revert last commit --- HISTORY.rst | 5 ----- psutil/_pslinux.py | 11 ++--------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index fb8ec8f158..54f2d79f8a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,11 +10,6 @@ XXXX-XX-XX - 1875_: `disk_partitions()` exposes 2 extra fields: `maxfile` and `maxpath`, which are the maximum file name and path name length. -**Bug fixes** - -- 1868_: [Linux] /proc/pid/stat is missing 2 fields on Alpine Linux (cpu_num - and delayacct_blkio_ticks) and are now being set to None. - 5.7.3 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 63b76697f0..b17b96a25b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1577,15 +1577,8 @@ def _parse_stat_file(self): ret['children_utime'] = fields[13] ret['children_stime'] = fields[14] ret['create_time'] = fields[19] - # https://github.com/giampaolo/psutil/issues/1868 - try: - ret['cpu_num'] = fields[36] - except IndexError: - ret['cpu_num'] = None - try: - ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks' - except IndexError: - ret['blkio_ticks'] = None + ret['cpu_num'] = fields[36] + ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks' return ret From 8415355c8badc9c94418b19bdf26e622f06f0cce Mon Sep 17 00:00:00 2001 From: Christopher Hewett Date: Wed, 4 Nov 2020 18:03:04 +0000 Subject: [PATCH 0656/1714] Fix definition of RPM (#1870) RPM is actually revolutions per minute, current docs say its rounds per minute --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 86b7ad1dbf..1c71f823ab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -770,7 +770,7 @@ Sensors Return hardware fans speed. Each entry is a named tuple representing a certain hardware sensor fan. - Fan speed is expressed in RPM (rounds per minute). + Fan speed is expressed in RPM (revolutions per minute). If sensors are not supported by the OS an empty dict is returned. Example:: From 1e8fdf27eff0a19a9d68e24973bc76d8a953654a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 10 Nov 2020 00:39:46 +0100 Subject: [PATCH 0657/1714] [Windows] add support for pypy2 on windows (#1872) --- HISTORY.rst | 8 +++++++- psutil/_psutil_common.c | 17 +++++++++++++---- psutil/arch/windows/wmi.c | 2 ++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 54f2d79f8a..8512793ba6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,9 +7,15 @@ XXXX-XX-XX **Enhancements** -- 1875_: `disk_partitions()` exposes 2 extra fields: `maxfile` and `maxpath`, +- 1872_: [Windows] added support for PyPy 2.7. +- 1863_: `disk_partitions()` exposes 2 extra fields: `maxfile` and `maxpath`, which are the maximum file name and path name length. +**Bug fixes** + +- 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid + access to memory location" on Python 3.9. + 5.7.3 ===== diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 4178e0c0b7..a01919d57c 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -23,9 +23,8 @@ int PSUTIL_TESTING = 0; // ==================================================================== // PyPy on Windows -#if defined(PSUTIL_WINDOWS) && \ - defined(PYPY_VERSION) && \ - !defined(PyErr_SetFromWindowsErrWithFilename) +#if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) +#if !defined(PyErr_SetFromWindowsErrWithFilename) PyObject * PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { PyObject *py_exc = NULL; @@ -58,7 +57,17 @@ PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { Py_XDECREF(py_winerr); return NULL; } -#endif // PYPY on Windows +#endif // !defined(PyErr_SetFromWindowsErrWithFilename) + + +// PyPy 2.7 +#if !defined(PyErr_SetFromWindowsErr) +PyObject * +PyErr_SetFromWindowsErr(int winerr) { + return PyErr_SetFromWindowsErrWithFilename(winerr, ""); +} +#endif // !defined(PyErr_SetFromWindowsErr) +#endif // defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) // ==================================================================== diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 0a1fb8913d..ec5cdeb514 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -10,6 +10,8 @@ #include #include +#include "../../_psutil_common.h" + // We use an exponentially weighted moving average, just like Unix systems do // https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation From 7050e73c48b38caf2f19409167ba8dfaa9ab9fd4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 11 Nov 2020 01:08:21 +0100 Subject: [PATCH 0658/1714] OSX: put sysctl() + KERN_PROCARGS2 in its own function --- psutil/arch/osx/process_info.c | 88 +++++++++++++++++----------------- psutil/arch/osx/process_info.h | 1 - psutil/tests/test_process.py | 19 ++++---- psutil/tests/test_system.py | 10 ++-- psutil/tests/test_unicode.py | 2 - 5 files changed, 57 insertions(+), 63 deletions(-) diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index f83cfe47df..51df3483af 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -9,13 +9,7 @@ #include -#include #include -#include // for INT_MAX -#include -#include -#include -#include #include #include @@ -23,6 +17,7 @@ #include "../../_psutil_posix.h" #include "process_info.h" + /* * Returns a list of all BSD processes on the system. This routine * allocates the list and puts it in *procList and a count of the @@ -33,16 +28,15 @@ */ int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { - int mib3[3] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; + int mib[3]; size_t size, size2; void *ptr; int err; int lim = 8; // some limit - assert( procList != NULL); - assert(*procList == NULL); - assert(procCount != NULL); - + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_ALL; *procCount = 0; /* @@ -59,7 +53,7 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { */ while (lim-- > 0) { size = 0; - if (sysctl((int *)mib3, 3, NULL, &size, NULL, 0) == -1) { + if (sysctl((int *)mib, 3, NULL, &size, NULL, 0) == -1) { PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); return 1; } @@ -79,7 +73,7 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { return 1; } - if (sysctl((int *)mib3, 3, ptr, &size, NULL, 0) == -1) { + if (sysctl((int *)mib, 3, ptr, &size, NULL, 0) == -1) { err = errno; free(ptr); if (err != ENOMEM) { @@ -104,12 +98,15 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { // Read the maximum argument size for processes -int -psutil_get_argmax() { +static int +psutil_sysctl_argmax() { int argmax; - int mib[] = { CTL_KERN, KERN_ARGMAX }; + int mib[2]; size_t size = sizeof(argmax); + mib[0] = CTL_KERN; + mib[1] = KERN_ARGMAX; + if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) return argmax; PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); @@ -117,6 +114,32 @@ psutil_get_argmax() { } +// Read process argument space. +static int +psutil_sysctl_procargs(pid_t pid, char *procargs, size_t argmax) { + int mib[3]; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROCARGS2; + mib[2] = pid; + + if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { + // In case of zombie process we'll get EINVAL. We translate it + // to NSP and _psosx.py will translate it to ZP. + if ((errno == EINVAL) && (psutil_pid_exists(pid))) + NoSuchProcess("sysctl"); + else if (errno == EIO) { + psutil_debug("EIO, pid_exists() = %i\n", psutil_pid_exists(pid)); + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + } + else + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + return 1; + } + return 0; +} + + // Return 1 if pid refers to a zombie process else 0. int psutil_is_zombie(pid_t pid) { @@ -128,11 +151,9 @@ psutil_is_zombie(pid_t pid) { } - // return process args as a python list PyObject * psutil_get_cmdline(pid_t pid) { - int mib[3]; int nargs; size_t len; char *procargs = NULL; @@ -149,7 +170,7 @@ psutil_get_cmdline(pid_t pid) { return Py_BuildValue("[]"); // read argmax and allocate memory for argument space. - argmax = psutil_get_argmax(); + argmax = psutil_sysctl_argmax(); if (! argmax) goto error; @@ -159,19 +180,8 @@ psutil_get_cmdline(pid_t pid) { goto error; } - // read argument space - mib[0] = CTL_KERN; - mib[1] = KERN_PROCARGS2; - mib[2] = pid; - if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { - // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psosx.py will translate it to ZP. - if ((errno == EINVAL) && (psutil_pid_exists(pid))) - NoSuchProcess("sysctl"); - else - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_procargs(pid, procargs, argmax) != 0) goto error; - } arg_end = &procargs[argmax]; // copy the number of arguments to nargs @@ -226,7 +236,6 @@ psutil_get_cmdline(pid_t pid) { // return process environment as a python string PyObject * psutil_get_environ(pid_t pid) { - int mib[3]; int nargs; char *procargs = NULL; char *procenv = NULL; @@ -241,7 +250,7 @@ psutil_get_environ(pid_t pid) { goto empty; // read argmax and allocate memory for argument space. - argmax = psutil_get_argmax(); + argmax = psutil_sysctl_argmax(); if (! argmax) goto error; @@ -251,19 +260,8 @@ psutil_get_environ(pid_t pid) { goto error; } - // read argument space - mib[0] = CTL_KERN; - mib[1] = KERN_PROCARGS2; - mib[2] = pid; - if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { - // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psosx.py will translate it to ZP. - if ((errno == EINVAL) && (psutil_pid_exists(pid))) - NoSuchProcess("sysctl"); - else - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + if (psutil_sysctl_procargs(pid, procargs, argmax) != 0) goto error; - } arg_end = &procargs[argmax]; // copy the number of arguments to nargs diff --git a/psutil/arch/osx/process_info.h b/psutil/arch/osx/process_info.h index 35755247aa..ffa6230f7c 100644 --- a/psutil/arch/osx/process_info.h +++ b/psutil/arch/osx/process_info.h @@ -8,7 +8,6 @@ typedef struct kinfo_proc kinfo_proc; -int psutil_get_argmax(void); int psutil_is_zombie(pid_t pid); int psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp); int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0d63979bfa..33fcbf848c 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -713,18 +713,15 @@ def test_exe(self): def test_cmdline(self): cmdline = [PYTHON_EXE, "-c", "import time; time.sleep(60)"] p = self.spawn_psproc(cmdline) - try: + # 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: self.assertEqual(' '.join(p.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(p.cmdline()[0], PYTHON_EXE) - else: - raise @unittest.skipIf(PYPY, "broken on PYPY") def test_long_cmdline(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index bb6d2b89bd..872b8988e4 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -597,10 +597,12 @@ def check_ntuple(nt): self.assertIsInstance(nt.mountpoint, str) self.assertIsInstance(nt.fstype, str) self.assertIsInstance(nt.opts, str) - self.assertIsInstance(nt.maxfile, int) - self.assertIsInstance(nt.maxpath, int) - self.assertGreater(nt.maxfile, 0) - self.assertGreater(nt.maxpath, 0) + self.assertIsInstance(nt.maxfile, (int, type(None))) + self.assertIsInstance(nt.maxpath, (int, type(None))) + if nt.maxfile is not None: + self.assertGreater(nt.maxfile, 0) + if nt.maxpath is not None: + self.assertGreater(nt.maxpath, 0) # all = False ls = psutil.disk_partitions(all=False) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index bae81380b6..7ec6c497f0 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -135,8 +135,6 @@ def try_unicode(suffix): """Return True if both the fs and the subprocess module can deal with a unicode file name. """ - if PY3: - return True sproc = None testfn = get_testfn(suffix=suffix) try: From fd69f22ee5ca38ceff32ae3fac15420c5d8fce29 Mon Sep 17 00:00:00 2001 From: Jake Omann Date: Fri, 13 Nov 2020 14:52:30 -0600 Subject: [PATCH 0659/1714] Fix solaris swap output when 'encrypted' column exists (#1876) --- CREDITS | 3 ++- HISTORY.rst | 1 + psutil/_pssunos.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index ea13b16c53..168f05d467 100644 --- a/CREDITS +++ b/CREDITS @@ -467,7 +467,8 @@ N: Farhan Khan I: 823 N: Jake Omann -I: 816, 775 +W: https://github.com/jomann09 +I: 816, 775, 1874 N: Jeremy Humble W: https://github.com/jhumble diff --git a/HISTORY.rst b/HISTORY.rst index 8512793ba6..bcbffa69d6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,7 @@ XXXX-XX-XX - 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid access to memory location" on Python 3.9. +- 1874_: [Solaris] wrong swap output given when encrypted column is present 5.7.3 ===== diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index c64ea9ca8a..2af15dfa40 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -155,7 +155,7 @@ def swap_memory(): total = free = 0 for line in lines: line = line.split() - t, f = line[-2:] + t, f = line[3:4] total += int(int(t) * 512) free += int(int(f) * 512) used = total - free From 8fc5ed1b20c9c9fab75164aae1984698a46974dc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Nov 2020 00:27:05 +0100 Subject: [PATCH 0660/1714] Rewrite Linux prlimit() with ctypes (Linux wheels) (#1879) --- .ci/travis/install.sh | 6 +- .github/workflows/build_wheel.yml | 45 +++-- .travis.yml | 15 +- INSTALL.rst | 125 ++++++------- MANIFEST.in | 21 --- Makefile | 19 +- docs/DEVGUIDE.rst | 153 +++++++--------- docs/index.rst | 8 +- psutil/__init__.py | 4 +- psutil/_pslinux.py | 58 +++++- psutil/_psutil_linux.c | 76 +------- psutil/_psutil_posix.c | 9 +- psutil/_psutil_windows.c | 2 +- psutil/arch/osx/process_info.c | 3 - psutil/arch/windows/wmi.c | 4 +- psutil/tests/__init__.py | 179 ++++++++++++++++--- psutil/tests/test_connections.py | 116 +++--------- psutil/tests/test_contracts.py | 47 +++-- psutil/tests/test_linux.py | 2 +- psutil/tests/test_memleaks.py | 2 +- psutil/tests/test_misc.py | 9 +- psutil/tests/test_posix.py | 1 + psutil/tests/test_process.py | 6 +- psutil/tests/test_system.py | 6 +- psutil/tests/test_windows.py | 7 +- scripts/internal/download_wheels_appveyor.py | 4 +- scripts/internal/download_wheels_github.py | 14 ++ scripts/internal/generate_manifest.py | 4 +- scripts/internal/print_announce.py | 16 +- scripts/internal/print_hashes.py | 35 ++++ scripts/internal/print_wheels.py | 9 +- scripts/procinfo.py | 1 - setup.py | 8 - 33 files changed, 534 insertions(+), 480 deletions(-) mode change 100644 => 100755 scripts/internal/download_wheels_github.py create mode 100755 scripts/internal/print_hashes.py mode change 100644 => 100755 scripts/internal/print_wheels.py diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh index 164d1663b2..ff6b93780d 100755 --- a/.ci/travis/install.sh +++ b/.ci/travis/install.sh @@ -32,6 +32,10 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then pyenv install 3.8.5 pyenv virtualenv 3.8.5 psutil ;; + py39) + pyenv install 3.9.0 + pyenv virtualenv 3.9.0 psutil + ;; esac pyenv rehash pyenv activate psutil @@ -41,4 +45,4 @@ if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] || [[ $PYVER == 'py27' ]]; then pip install -U ipaddress mock unittest2 fi -pip install -U coverage coveralls flake8 setuptools +pip install -U coverage coveralls flake8 setuptools twine diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 4f9eeef978..1a8b486843 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -1,4 +1,4 @@ -name: Build wheels +name: Linux, macOS, Windows on: [push, pull_request] @@ -6,35 +6,46 @@ jobs: wheel: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: - fail-fast: false + fail-fast: false # whether to exit the whole run on first failure matrix: - os: [macos-latest, ubuntu-latest] + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: python -u -Wa {project}/psutil/tests/runner.py CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py CIBW_TEST_EXTRAS: test - CIBW_SKIP: pp*-macosx_x86_64 + # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip + CIBW_SKIP: cp35-* pp* steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 - name: Install Python 3.7 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: 3.9 - - name: Install Visual C++ for Python 2.7 - if: startsWith(matrix.os, 'windows') + - name: (Windows) install Visual C++ for Python 2.7 + if: matrix.os == 'windows-latest' run: | choco install vcpython27 -f -y - - name: Install cibuildwheel - run: pip install cibuildwheel==1.4.1 - - - name: Build wheels - run: cibuildwheel . + - name: Run tests + run: | + pip install cibuildwheel + cibuildwheel . - - name: Upload wheels - uses: actions/upload-artifact@v1 + - name: Create wheels + uses: actions/upload-artifact@v2 with: name: wheels path: wheelhouse + + - name: Print hashes + if: matrix.os == 'ubuntu-latest' + run: | + make generate-manifest + python setup.py sdist + mv dist/psutil*.tar.gz wheelhouse/ + python scripts/internal/print_hashes.py wheelhouse/ + diff --git a/.travis.yml b/.travis.yml index d49f2cf1f7..d9420682c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,21 +2,18 @@ language: python cache: pip matrix: include: + # Linux + - python: 3.8 + - python: 3.9-dev # macOS - language: generic os: osx - env: PYVER=py27 + env: PYVER=py38 - language: generic os: osx - env: PYVER=py36 - # Linux - - python: 2.7 - - python: 3.5 - - python: 3.6 - - python: 3.7 - - python: 3.8 + env: PYVER=py27 # pypy - # - python: pypy + - python: pypy - python: pypy3 install: - ./.ci/travis/install.sh diff --git a/INSTALL.rst b/INSTALL.rst index c3b9e91c0c..a4f2bf11a2 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,83 +1,38 @@ -Install pip -=========== - -pip is the easiest way to install psutil. It is shipped by default with Python -2.7.9+ and 3.4+. For other Python versions you can install it manually. -On Linux or via wget:: - - wget https://bootstrap.pypa.io/get-pip.py -O - | python +Linux, Windows, macOS (wheels) +============================== -On macOS or via curl:: - - python < <(curl -s https://bootstrap.pypa.io/get-pip.py) - -On Windows, `download pip `__, open -cmd.exe and install it:: +psutil makes extensive use of C extension modules, meaning a C compiler is +required. +For these 3 platforms though, pre-compiled cPython wheels are provided on each +psutil release, so all you have to do is this:: - C:\Python27\python.exe get-pip.py - -Permission issues (UNIX) -======================== - -The commands below assume you're running as root. -If you aren't or you bump into permission errors you can either install psutil -for your user only:: - - pip3 install --user psutil - -...or prepend ``sudo`` and install it globally, e.g.:: + pip3 install psutil - sudo pip3 install psutil +If wheels are not available and you whish to install from sources, keep reading. -Linux -===== +Linux (install from sources) +============================ Ubuntu / Debian:: sudo apt-get install gcc python3-dev - pip3 install psutil + pip3 install --user psutil --no-binary :all: RedHat / CentOS:: sudo yum install gcc python3-devel - pip3 install psutil - -If you're on Python 2 use ``python-dev`` instead. - -macOS -===== + pip3 install --user psutil --no-binary :all: -Install `Xcode `__ then run:: - - pip3 install psutil - -Windows -======= - -Open a cmd.exe shell and run:: - - python3 -m pip install psutil +Windows (install from sources) +============================== -This assumes "python" is in your PATH. If not, specify the full python.exe -path. +In order to compile psutil on Windows you'll need **Visual Studio**. +Here's a couple of guides describing how to do it: `1 `__ +and `2 `__. And then:: -In order to compile psutil from sources you'll need **Visual Studio** (Mingw32 -is not supported). -This `blog post `__ -provides numerous info on how to properly set up VS. The needed VS versions are: + pip3 install --user psutil --no-binary :all: -* Python 2.6, 2.7: `VS-2008 `__ -* Python 3.4: `VS-2010 `__ -* Python 3.5+: `VS-2015 `__ - -Compiling 64 bit versions of Python 2.6 and 2.7 with VS 2008 requires -`Windows SDK and .NET Framework 3.5 SP1 `__. -Once installed run `vcvars64.bat` -(see `here `__). -Once VS is setup open a cmd.exe shell, cd into psutil directory and run:: - - python3 setup.py build - python3 setup.py install +Note that MinGW compiler is not supported. FreeBSD ======= @@ -85,7 +40,7 @@ FreeBSD :: pkg install python3 gcc - python -m pip3 install psutil + python3 -m pip3 install psutil OpenBSD ======= @@ -118,15 +73,6 @@ Install:: pkg install gcc python3 -m pip install psutil -Install from sources -==================== - -:: - - git clone https://github.com/giampaolo/psutil.git - cd psutil - python3 setup.py install - Testing installation ==================== @@ -137,4 +83,33 @@ Testing installation Dev Guide ========= -See: `dev guide `__. +`Link `__. + +Install pip +=========== + +Pip is shipped by default with Python 2.7.9+ and 3.4+. +If you don't have it you can install with wget:: + + wget https://bootstrap.pypa.io/get-pip.py -O - | python3 + +...ow with curl:: + + python3 < <(curl -s https://bootstrap.pypa.io/get-pip.py) + +On Windows, `download pip `__, open +cmd.exe and install it:: + + C:\Python27\python.exe get-pip.py + +Permission issues (UNIX) +======================== + +If you bump into permission errors you have two options. +Install psutil for your user only:: + + pip3 install --user psutil + +...or prepend ``sudo`` and install it at system level:: + + sudo pip3 install psutil diff --git a/MANIFEST.in b/MANIFEST.in index f5ceeb7cf6..780bbdc735 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -include .cirrus.yml include .coveragerc include .flake8 include .gitignore @@ -112,26 +111,6 @@ include scripts/disk_usage.py include scripts/fans.py include scripts/free.py include scripts/ifconfig.py -include scripts/internal/README -include scripts/internal/bench_oneshot.py -include scripts/internal/bench_oneshot_2.py -include scripts/internal/check_broken_links.py -include scripts/internal/clinter.py -include scripts/internal/convert_readme.py -include scripts/internal/download_wheels_appveyor.py -include scripts/internal/download_wheels_github.py -include scripts/internal/fix_flake8.py -include scripts/internal/generate_manifest.py -include scripts/internal/git_pre_commit.py -include scripts/internal/print_access_denied.py -include scripts/internal/print_announce.py -include scripts/internal/print_api_speed.py -include scripts/internal/print_downloads.py -include scripts/internal/print_timeline.py -include scripts/internal/print_wheels.py -include scripts/internal/purge_installation.py -include scripts/internal/tidelift.py -include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py diff --git a/Makefile b/Makefile index d9d344f7f2..4718976d68 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ uninstall: ## Uninstall this package via pip. $(PYTHON) scripts/internal/purge_installation.py install-pip: ## Install pip (no-op if already installed). - $(PYTHON) -c \ + @$(PYTHON) -c \ "import sys, ssl, os, pkgutil, tempfile, atexit; \ sys.exit(0) if pkgutil.find_loader('pip') else None; \ pyexc = 'from urllib.request import urlopen' if sys.version_info[0] == 3 else 'from urllib2 import urlopen'; \ @@ -103,7 +103,7 @@ install-pip: ## Install pip (no-op if already installed). f.write(data); \ f.flush(); \ print('downloaded %s' % f.name); \ - code = os.system('%s %s --user' % (sys.executable, f.name)); \ + code = os.system('%s %s --user --upgrade' % (sys.executable, f.name)); \ f.close(); \ sys.exit(code);" @@ -214,14 +214,10 @@ install-git-hooks: ## Install GIT pre-commit hook. download-wheels-appveyor: ## Download latest wheels hosted on appveyor. $(PYTHON) scripts/internal/download_wheels_appveyor.py --user giampaolo --project psutil + ${MAKE} print-wheels download-wheels-github: ## Download latest wheels hosted on github. $(PYTHON) scripts/internal/download_wheels_github.py --user=giampaolo --project=psutil --tokenfile=~/.github.token - -download-wheels: ## Download wheels from github and appveyor - rm -rf dist - ${MAKE} download-wheels-appveyor - # ${MAKE} download-wheels-github ${MAKE} print-wheels print-wheels: ## Print downloaded wheels @@ -265,9 +261,10 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} install ${MAKE} generate-manifest git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain - ${MAKE} download-wheels + ${MAKE} download-wheels-github ${MAKE} sdist $(PYTHON) -m twine check dist/* + ${MAKE} md5-dist $(PYTHON) -c \ "from psutil import __version__ as ver; \ doc = open('docs/index.rst').read(); \ @@ -275,10 +272,9 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in doc, '%r not in docs/index.rst' % ver; \ assert ver in history, '%r not in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX in HISTORY.rst';" - $(PYTHON) -c "import subprocess, sys; out = subprocess.check_output('git diff --quiet && git diff --cached --quiet', shell=True).strip(); sys.exit('there are uncommitted changes:\n%s' % out) if out else 0 ;" release: ## Create a release (down/uploads tar.gz, wheels, git tag release). - ${MAKE} pre-release + $(PYTHON) -c "import subprocess, sys; out = subprocess.check_output('git diff --quiet && git diff --cached --quiet', shell=True).strip(); sys.exit('there are uncommitted changes:\n%s' % out) if out else 0 ;" $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release ${MAKE} tidelift-relnotes @@ -310,6 +306,9 @@ print-api-speed: ## Benchmark all API calls print-downloads: ## Print PYPI download statistics $(PYTHON) scripts/internal/print_downloads.py +print-hashes: ## Prints hashes of files in dist/ directory + $(PYTHON) scripts/internal/print_hashes.py dist/ + # =================================================================== # Misc # =================================================================== diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 2e8272f067..573afdd5d5 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -1,98 +1,91 @@ +Setup +===== + +psutil makes extensive use of C extension modules, meaning a C compiler is +required, see +`install instructions `__. + Build, setup and running tests =============================== -Make sure to `install `__ -a C compiler first, then: +Once you have a compiler installed: .. code-block:: bash - git clone git@github.com:giampaolo/psutil.git - make setup-dev-env - make test + git clone git@github.com:giampaolo/psutil.git + make setup-dev-env # install useful dev libs (flake8, coverage, ...) + make build + make install + make test -- bear in mind that ``make``(see `Makefile`_) is the designated tool to run - tests, build, install etc. and that it is also available on Windows (see +- ``make`` (see `Makefile`_) is the designated tool to run tests, build, install + try new features you're developing, etc. This also includes Windows (see `make.bat`_ ). -- do not use ``sudo``; ``make install`` installs psutil as a limited user in - "edit" mode; also ``make setup-dev-env`` installs deps as a limited user. -- use `make help` to see the list of available commands. - -Coding style -============ - -- python code strictly follows `PEP-8`_ styling guides and this is enforced by - a commit GIT hook installed via ``make install-git-hooks``. -- C code follows `PEP-7`_ styling guides. - -Makefile -======== - -Some useful make commands: + Some useful commands are: .. code-block:: bash - make install - make setup-dev-env # install useful dev libs (flake8, unittest2, etc.) - make test # run unit tests make test-parallel # faster make test-memleaks make test-coverage make lint # Python (PEP8) and C linters + make uninstall + make help -There are some differences between ``make`` on UNIX and Windows. -For instance, to run a specific Python version. On UNIX: +- if you're working on a new feature and you whish to compile & test it "on the + fly" from a test script, this is a quick & dirty way to do it: .. code-block:: bash - make test PYTHON=python3.5 - -On Windows: - -.. code-block:: bat + make test TSCRIPT=test_script.py # on UNIX + make test test_script.py # on Windows - make -p C:\python35\python.exe test +- do not use ``sudo``. ``make install`` installs psutil as a limited user in + "edit" mode, meaning you can edit psutil code on the fly while you develop. -If you want to modify psutil and run a script on the fly which uses it do -(on UNIX): +- if you want to target a specific Python version: .. code-block:: bash - make test TSCRIPT=foo.py + make test PYTHON=python3.5 # UNIX + make test -p C:\python35\python.exe # Windows -On Windows: - -.. code-block:: bat - - make test foo.py +Coding style +============ -Adding a new feature -==================== +- python code strictly follows `PEP-8`_ styling guides and this is enforced by + a commit GIT hook installed via ``make install-git-hooks`` which will reject + commits if code is not PEP-8 complieant. +- C code should follow `PEP-7`_ styling guides. -Usually the files involved when adding a new functionality are: +Code organization +================= .. code-block:: bash - psutil/__init__.py # main psutil namespace - psutil/_ps{platform}.py # python platform wrapper - psutil/_psutil_{platform}.c # C platform extension - psutil/_psutil_{platform}.h # C header file + psutil/__init__.py # main psutil namespace ("import psutil") + psutil/_ps{platform}.py # platform-specific python wrapper + psutil/_psutil_{platform}.c # platform-specific C extension psutil/tests/test_process|system.py # main test suite - psutil/tests/test_{platform}.py # platform specific test suite + psutil/tests/test_{platform}.py # platform-specific test suite + +Adding a new API +================ -Typical process occurring when adding a new functionality (API): +Typically, this is what you do: -- define the new function in `psutil/__init__.py`_. +- define the new API in `psutil/__init__.py`_. - write the platform specific implementation in ``psutil/_ps{platform}.py`` (e.g. `psutil/_pslinux.py`_). -- if the change requires C, write the C implementation in +- if the change requires C code, write the C implementation in ``psutil/_psutil_{platform}.c`` (e.g. `psutil/_psutil_linux.c`_). - write a generic test in `psutil/tests/test_system.py`_ or `psutil/tests/test_process.py`_. -- if possible, write a platform specific test in +- if possible, write a platform-specific test in ``psutil/tests/test_{platform}.py`` (e.g. `psutil/tests/test_linux.py`_). - This usually means testing the return value of the new feature against + This usually means testing the return value of the new API against a system CLI tool. -- update doc in ``doc/index.py``. +- update the doc in ``doc/index.py``. - update ``HISTORY.rst``. - make a pull request. @@ -100,52 +93,44 @@ Make a pull request =================== - fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") -- git clone your fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git``) -- create your feature branch:``git checkout -b new-feature`` +- git clone the fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git``) +- create a branch:``git checkout -b new-feature`` - commit your changes: ``git commit -am 'add some feature'`` -- push to the branch: ``git push origin new-feature`` -- create a new pull request by via github web interface -- remember to update `HISTORY.rst`_ and `CREDITS`_ files. +- push the branch: ``git push origin new-feature`` +- create a new PR by via GitHub web interface Continuous integration ====================== -All of the services listed below are automatically run on ``git push``. +All of the services listed below are automatically run on each ``git push``. Unit tests ---------- -Tests are automatically run for every GIT push on **Linux**, **macOS** and -**Windows** by using: +Tests are automatically run on every GIT push on **Linux**, **macOS**, +**Windows** and **FreeBSD** by using: - `Travis`_ (Linux, macOS) +- `Github Actions`_ (Linux, macOS, Windows) - `Appveyor`_ (Windows) - -Test files controlling these are `.travis.yml`_ and `appveyor.yml`_. -Both services run psutil test suite against all supported python version -(2.6 - 3.6). -Two icons in the home page (README) always show the build status: +- `Cirrus CI`_ (FreeBSD) .. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux,%20OSX,%20PyPy :target: https://travis-ci.org/giampaolo/psutil - :alt: Linux, macOS and PyPy3 tests (Travis) .. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil - :alt: Windows tests (Appveyor) .. image:: https://img.shields.io/cirrus/github/giampaolo/psutil?label=FreeBSD :target: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci - :alt: FreeBSD tests (Cirrus-CI) -BSD, AIX and Solaris are currently tested manually. +OpenBSD, NetBSD, AIX and Solaris does not have continuos test integration. Test coverage ------------- Test coverage is provided by `coveralls.io`_ and it is controlled via `.travis.yml`_. -An icon in the home page (README) always shows the last coverage percentage: .. image:: https://coveralls.io/repos/giampaolo/psutil/badge.svg?branch=master&service=github :target: https://coveralls.io/github/giampaolo/psutil?branch=master @@ -155,26 +140,18 @@ Documentation ============= - doc source code is written in a single file: `/docs/index.rst`_. -- it uses `RsT syntax`_ - and it's built with `sphinx`_. - doc can be built with ``make setup-dev-env; cd docs; make html``. -- public doc is hosted on http://psutil.readthedocs.io/ +- public doc is hosted at https://psutil.readthedocs.io -Releasing a new version -======================= -These are notes for myself (Giampaolo): - -- ``make release`` -- post announce (``make print-announce``) on psutil and python-announce mailing - lists, twitter, g+, blog. - - -.. _`.travis.yml`: https://github.com/giampaolo/psutil/blob/master/.travis.ym -.. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.ym +.. _`.travis.yml`: https://github.com/giampaolo/psutil/blob/master/.travis.yml +.. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.yml .. _`Appveyor`: https://ci.appveyor.com/project/giampaolo/psuti +.. _`Cirrus CI`: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci .. _`coveralls.io`: https://coveralls.io/github/giampaolo/psuti +.. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS .. _`doc/index.rst`: https://github.com/giampaolo/psutil/blob/master/doc/index.rst +.. _`Github Actions`: https://github.com/giampaolo/psutil/actions .. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst .. _`make.bat`: https://github.com/giampaolo/psutil/blob/master/make.bat .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile @@ -189,5 +166,3 @@ These are notes for myself (Giampaolo): .. _`RsT syntax`: http://docutils.sourceforge.net/docs/user/rst/quickref.htm .. _`sphinx`: http://sphinx-doc.org .. _`Travis`: https://travis-ci.org/giampaolo/psuti -.. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst -.. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS diff --git a/docs/index.rst b/docs/index.rst index 1c71f823ab..467951506e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2518,10 +2518,12 @@ FAQs Unfortunately there's not much you can do about this except running the Python process with higher privileges. On Unix you may run the Python process as root or use the SUID bit - (this is the trick used by tools such as ``ps`` and ``netstat``). + (``ps`` and ``netstat`` does this). On Windows you may run the Python process as NT AUTHORITY\\SYSTEM or install - the Python script as a Windows service (this is the trick used by tools - such as ProcessHacker). + the Python script as a Windows service (ProcessHacker does this). + +* Q: is MinGW supported on Windows? +* A: no, you should Visual Studio (see `development guide`_). Running tests ============= diff --git a/psutil/__init__.py b/psutil/__init__.py index 829bb33ea7..fc7f225a3f 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -190,9 +190,11 @@ "users", "boot_time", # others ] + __all__.extend(_psplatform.__extra__all__) -if LINUX or FREEBSD: +# Linux, FreeBSD +if hasattr(_psplatform.Process, "rlimit"): # Populate global namespace with RLIM* constants. from . import _psutil_posix diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b17b96a25b..18439c82d3 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -75,7 +75,6 @@ POWER_SUPPLY_PATH = "/sys/class/power_supply" HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) -HAS_PRLIMIT = hasattr(cext, "linux_prlimit") HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get") HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get") _DEFAULT = object() @@ -307,6 +306,54 @@ def cat(fname, fallback=_DEFAULT, binary=True): 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 # ===================================================================== @@ -1990,10 +2037,10 @@ def ionice_set(self, ioclass, value): raise ValueError("value not in 0-7 range") 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. @@ -2002,15 +2049,14 @@ def rlimit(self, resource, limits=None): 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) + prlimit(self.pid, resource_, limits) except OSError as err: if err.errno == errno.ENOSYS and pid_exists(self.pid): # I saw this happening on Travis: diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 4def9692d4..5836cd6b78 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -23,6 +23,7 @@ #include #include #include +#include // see: https://github.com/giampaolo/psutil/issues/659 #ifdef PSUTIL_ETHTOOL_MISSING_TYPES @@ -42,17 +43,6 @@ static const int NCPUS_START = sizeof(unsigned long) * CHAR_BIT; // Linux >= 2.6.13 #define PSUTIL_HAVE_IOPRIO defined(__NR_ioprio_get) && defined(__NR_ioprio_set) -// Linux >= 2.6.36 (supposedly) and glibc >= 2.13 -#define PSUTIL_HAVE_PRLIMIT \ - defined(__NR_prlimit64) && \ - (__GLIBC__ >= 2 && __GLIBC_MINOR__ >= 13) - -#if PSUTIL_HAVE_PRLIMIT - #define _FILE_OFFSET_BITS 64 - #include - #include -#endif - // Should exist starting from CentOS 6 (year 2011). #ifdef CPU_ALLOC #define PSUTIL_HAVE_CPU_AFFINITY @@ -133,66 +123,6 @@ psutil_proc_ioprio_set(PyObject *self, PyObject *args) { #endif -#if PSUTIL_HAVE_PRLIMIT -/* - * A wrapper around prlimit(2); sets process resource limits. - * This can be used for both get and set, in which case extra - * 'soft' and 'hard' args must be provided. - */ -static PyObject * -psutil_linux_prlimit(PyObject *self, PyObject *args) { - pid_t pid; - int ret, resource; - struct rlimit old, new; - struct rlimit *newp = NULL; - PyObject *py_soft = NULL; - PyObject *py_hard = NULL; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i|OO", &pid, &resource, - &py_soft, &py_hard)) { - return NULL; - } - - // get - if (py_soft == NULL && py_hard == NULL) { - ret = prlimit(pid, resource, NULL, &old); - if (ret == -1) - return PyErr_SetFromErrno(PyExc_OSError); - if (sizeof(old.rlim_cur) > sizeof(long)) { - return Py_BuildValue("LL", - (PY_LONG_LONG)old.rlim_cur, - (PY_LONG_LONG)old.rlim_max); - } - return Py_BuildValue("ll", (long)old.rlim_cur, (long)old.rlim_max); - } - - // set - else { -#if defined(HAVE_LONG_LONG) - new.rlim_cur = PyLong_AsLongLong(py_soft); - if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; - new.rlim_max = PyLong_AsLongLong(py_hard); - if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; -#else - new.rlim_cur = PyLong_AsLong(py_soft); - if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; - new.rlim_max = PyLong_AsLong(py_hard); - if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) - return NULL; -#endif - newp = &new; - ret = prlimit(pid, resource, newp, &old); - if (ret == -1) - return PyErr_SetFromErrno(PyExc_OSError); - Py_RETURN_NONE; - } -} -#endif - - /* * Return disk mounted partitions as a list of tuples including device, * mount point and filesystem type @@ -572,10 +502,6 @@ static PyMethodDef mod_methods[] = { {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS, "A wrapper around sysinfo(), return system memory usage statistics"}, -#if PSUTIL_HAVE_PRLIMIT - {"linux_prlimit", psutil_linux_prlimit, METH_VARARGS, - "Get or set process resource limits."}, -#endif // --- others {"set_testing", psutil_set_testing, METH_NOARGS, "Set psutil in testing mode"}, diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 876b412923..b41c6dec95 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -28,17 +28,20 @@ #include #include #include -#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#endif +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) #include #include #include #include #include #include -#elif defined(PSUTIL_SUNOS) +#endif +#if defined(PSUTIL_SUNOS) #include #include -#elif defined(PSUTIL_AIX) +#endif +#if defined(PSUTIL_AIX) #include #endif #if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index c8b2f38355..b836d70885 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -211,7 +211,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { Py_RETURN_NONE; } else { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); return NULL; } } diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 51df3483af..1dca7364b0 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -297,12 +297,9 @@ psutil_get_environ(pid_t pid) { while (*arg_ptr != '\0' && arg_ptr < arg_end) { char *s = memchr(arg_ptr + 1, '\0', arg_end - arg_ptr); - if (s == NULL) break; - memcpy(procenv + (arg_ptr - env_start), arg_ptr, s - arg_ptr); - arg_ptr = s + 1; } diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index ec5cdeb514..f9a847d3bf 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -73,7 +73,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { event = CreateEventW(NULL, FALSE, FALSE, L"LoadUpdateEvent"); if (event == NULL) { - PyErr_SetFromWindowsErr(GetLastError()); + PyErr_SetFromWindowsErr(0); return NULL; } @@ -91,7 +91,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { WT_EXECUTEDEFAULT); if (ret == 0) { - PyErr_SetFromWindowsErr(GetLastError()); + PyErr_SetFromWindowsErr(0); return NULL; } diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b508a629f6..36f83435e6 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -97,7 +97,7 @@ 'chdir', 'safe_rmpath', 'create_exe', 'decode_path', 'encode_path', 'get_testfn', # os - 'get_winver', 'get_kernel_version', + 'get_winver', 'kernel_version', # sync primitives 'call_until', 'wait_for_pid', 'wait_for_file', # network @@ -122,8 +122,8 @@ TRAVIS = 'TRAVIS' in os.environ APPVEYOR = 'APPVEYOR' in os.environ CIRRUS = 'CIRRUS' in os.environ -GITHUB_WHEELS = 'CIBUILDWHEEL' in os.environ -CI_TESTING = TRAVIS or APPVEYOR or CIRRUS or GITHUB_WHEELS +GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ +CI_TESTING = TRAVIS or APPVEYOR or CIRRUS or GITHUB_ACTIONS # are we a 64 bit process? IS_64BIT = sys.maxsize > 2 ** 32 @@ -204,7 +204,7 @@ def attempt(exe): else: return exe - if GITHUB_WHEELS: + if GITHUB_ACTIONS: if PYPY: return which("pypy3") if PY3 else which("pypy") else: @@ -499,7 +499,7 @@ def wait(proc, timeout): def sendsig(proc, sig): # XXX: otherwise the build hangs for some reason. - if MACOS and GITHUB_WHEELS: + if MACOS and GITHUB_ACTIONS: sig = signal.SIGKILL # If the process received SIGSTOP, SIGCONT is necessary first, # otherwise SIGTERM won't work. @@ -597,7 +597,7 @@ def reap_children(recursive=False): # =================================================================== -def get_kernel_version(): +def kernel_version(): """Return a tuple such as (2, 6, 36).""" if not POSIX: raise NotImplementedError("not POSIX") @@ -902,7 +902,13 @@ def assertProcessGone(self, proc): self.assertRaises(psutil.NoSuchProcess, psutil.Process, proc.pid) if isinstance(proc, (psutil.Process, psutil.Popen)): assert not proc.is_running() - self.assertRaises(psutil.NoSuchProcess, proc.status) + try: + status = proc.status() + except psutil.NoSuchProcess: + pass + else: + raise AssertionError("Process.status() didn't raise exception " + "(status=%s)" % status) proc.wait(timeout=0) # assert not raise TimeoutExpired assert not psutil.pid_exists(proc.pid), proc.pid self.assertNotIn(proc.pid, psutil.pids()) @@ -1059,39 +1065,90 @@ def print_sysinfo(): import collections import datetime import getpass + import locale import platform + import pprint + try: + import pip + except ImportError: + pip = None + try: + import wheel + except ImportError: + wheel = None info = collections.OrderedDict() - info['OS'] = platform.system() - if psutil.OSX: - info['version'] = str(platform.mac_ver()) + + # 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['version'] = ' '.join(map(str, platform.win32_ver())) + info['OS'] = "Windows " + ' '.join( + map(str, platform.win32_ver())) if hasattr(platform, 'win32_edition'): - info['edition'] = platform.win32_edition() + info['OS'] += ", " + platform.win32_edition() else: - info['version'] = platform.version() - if psutil.POSIX: - info['kernel'] = '.'.join(map(str, get_kernel_version())) + info['OS'] = "%s %s" % (platform.system(), platform.version()) info['arch'] = ', '.join( list(platform.architecture()) + [platform.machine()]) - info['hostname'] = platform.node() + 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['pid'] = os.getpid() - print("=" * 70) # NOQA + info['home'] = os.path.expanduser("~") + info['cwd'] = os.getcwd() + info['hostname'] = platform.node() + info['PID'] = os.getpid() + + # metrics + pinfo = psutil.Process().as_dict() + pinfo.pop('memory_maps', None) + 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%%, %s/%s" % ( + int(mem.percent), bytes2human(mem.used), bytes2human(mem.total)) + swap = psutil.swap_memory() + info['swap'] = "%s%%, %s/%s" % ( + int(swap.percent), bytes2human(swap.used), bytes2human(swap.total)) + info['pids'] = len(psutil.pids()) + info['proc'] = pprint.pformat(pinfo) + + print("=" * 70, file=sys.stderr) # NOQA for k, v in info.items(): - print("%-14s %s" % (k + ':', v)) # NOQA - print("=" * 70) # NOQA + print("%-17s %s" % (k + ':', v), file=sys.stderr) # NOQA + print("=" * 70, file=sys.stderr) # NOQA + sys.stdout.flush() def _get_eligible_cpu(): @@ -1099,7 +1156,7 @@ def _get_eligible_cpu(): if hasattr(p, "cpu_num"): return p.cpu_num() elif hasattr(p, "cpu_affinity"): - return p.cpu_affinity()[0] + return random.choice(p.cpu_affinity()) return 0 @@ -1366,8 +1423,9 @@ def wrapper(*args, **kwargs): # =================================================================== +# XXX: no longer used def get_free_port(host='127.0.0.1'): - """Return an unused TCP port.""" + """Return an unused TCP port. Subject to race conditions.""" with contextlib.closing(socket.socket()) as sock: sock.bind((host, 0)) return sock.getsockname()[1] @@ -1504,6 +1562,83 @@ def check_net_address(addr, family): 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) + + # =================================================================== # --- compatibility # =================================================================== diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index d1425bcb1f..ba019ec0c8 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -6,10 +6,9 @@ """Tests for net_connections() and Process.connections() APIs.""" -import contextlib -import errno import os import socket +import sys import textwrap from contextlib import closing from socket import AF_INET @@ -31,11 +30,9 @@ from psutil.tests import AF_UNIX from psutil.tests import bind_socket from psutil.tests import bind_unix_socket -from psutil.tests import check_net_address +from psutil.tests import check_connection_ntuple from psutil.tests import CIRRUS from psutil.tests import create_sockets -from psutil.tests import enum -from psutil.tests import get_free_port from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import PsutilTestCase from psutil.tests import reap_children @@ -52,10 +49,11 @@ thisproc = psutil.Process() SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) +PYTHON_39 = sys.version_info[:2] == (3, 9) @serialrun -class _ConnTestCase(PsutilTestCase): +class ConnectionTestCase(PsutilTestCase): def setUp(self): if not (NETBSD or FREEBSD): @@ -90,93 +88,19 @@ def compare_procsys_connections(self, pid, proc_cons, kind='all'): proc_cons.sort() self.assertEqual(proc_cons, sys_cons) - def check_connection_ntuple(self, conn): - """Check validity of a connection namedtuple.""" - def check_ntuple(conn): - has_pid = len(conn) == 7 - self.assertIn(len(conn), (6, 7)) - self.assertEqual(conn[0], conn.fd) - self.assertEqual(conn[1], conn.family) - self.assertEqual(conn[2], conn.type) - self.assertEqual(conn[3], conn.laddr) - self.assertEqual(conn[4], conn.raddr) - self.assertEqual(conn[5], conn.status) - if has_pid: - self.assertEqual(conn[6], conn.pid) - - def check_family(conn): - self.assertIn(conn.family, (AF_INET, AF_INET6, AF_UNIX)) - 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: - self.assertEqual(conn.status, psutil.CONN_NONE) - - def check_type(conn): - # SOCK_SEQPACKET may happen in case of AF_UNIX socks - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET)) - if enum is not None: - assert isinstance(conn.type, enum.IntEnum), conn - else: - assert isinstance(conn.type, int), conn - if conn.type == SOCK_DGRAM: - self.assertEqual(conn.status, psutil.CONN_NONE) - - 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): - self.assertIsInstance(addr, tuple) - if not addr: - continue - self.assertIsInstance(addr.port, int) - assert 0 <= addr.port <= 65535, addr.port - check_net_address(addr.ip, conn.family) - elif conn.family == AF_UNIX: - self.assertIsInstance(addr, str) - - def check_status(conn): - self.assertIsInstance(conn.status, str) - valids = [getattr(psutil, x) for x in dir(psutil) - if x.startswith('CONN_')] - self.assertIn(conn.status, valids) - if conn.family in (AF_INET, AF_INET6) and conn.type == SOCK_STREAM: - self.assertNotEqual(conn.status, psutil.CONN_NONE) - else: - self.assertEqual(conn.status, psutil.CONN_NONE) - - check_ntuple(conn) - check_family(conn) - check_type(conn) - check_addrs(conn) - check_status(conn) - -class TestBasicOperations(_ConnTestCase): +class TestBasicOperations(ConnectionTestCase): @unittest.skipIf(SKIP_SYSCONS, "requires root") def test_system(self): with create_sockets(): for conn in psutil.net_connections(kind='all'): - self.check_connection_ntuple(conn) + check_connection_ntuple(conn) def test_process(self): with create_sockets(): for conn in psutil.Process().connections(kind='all'): - self.check_connection_ntuple(conn) + check_connection_ntuple(conn) def test_invalid_kind(self): self.assertRaises(ValueError, thisproc.connections, kind='???') @@ -184,7 +108,7 @@ def test_invalid_kind(self): @serialrun -class TestUnconnectedSockets(_ConnTestCase): +class TestUnconnectedSockets(ConnectionTestCase): """Tests sockets which are open but not connected to anything.""" def get_conn_from_sock(self, sock): @@ -206,7 +130,7 @@ def check_socket(self, sock): only (the one supposed to be checked). """ conn = self.get_conn_from_sock(sock) - self.check_connection_ntuple(conn) + check_connection_ntuple(conn) # fd, family, type if conn.fd != -1: @@ -236,7 +160,7 @@ def check_socket(self, sock): return conn def test_tcp_v4(self): - addr = ("127.0.0.1", get_free_port()) + addr = ("127.0.0.1", 0) with closing(bind_socket(AF_INET, SOCK_STREAM, addr=addr)) as sock: conn = self.check_socket(sock) assert not conn.raddr @@ -244,14 +168,14 @@ def test_tcp_v4(self): @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") def test_tcp_v6(self): - addr = ("::1", get_free_port()) + addr = ("::1", 0) 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()) + addr = ("127.0.0.1", 0) with closing(bind_socket(AF_INET, SOCK_DGRAM, addr=addr)) as sock: conn = self.check_socket(sock) assert not conn.raddr @@ -259,7 +183,7 @@ def test_udp_v4(self): @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") def test_udp_v6(self): - addr = ("::1", get_free_port()) + addr = ("::1", 0) with closing(bind_socket(AF_INET6, SOCK_DGRAM, addr=addr)) as sock: conn = self.check_socket(sock) assert not conn.raddr @@ -283,7 +207,7 @@ def test_unix_udp(self): @serialrun -class TestConnectedSocket(_ConnTestCase): +class TestConnectedSocket(ConnectionTestCase): """Test socket pairs which are are actually connected to each other. """ @@ -292,7 +216,7 @@ class TestConnectedSocket(_ConnTestCase): # in TIME_WAIT state. @unittest.skipIf(SUNOS, "unreliable on SUONS") def test_tcp(self): - addr = ("127.0.0.1", get_free_port()) + addr = ("127.0.0.1", 0) assert not thisproc.connections(kind='tcp4') server, client = tcp_socketpair(AF_INET, addr=addr) try: @@ -347,7 +271,7 @@ def test_unix(self): client.close() -class TestFilters(_ConnTestCase): +class TestFilters(ConnectionTestCase): def test_filters(self): def check(kind, families, types): @@ -399,7 +323,7 @@ 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") - self.check_connection_ntuple(conn) + check_connection_ntuple(conn) self.assertEqual(conn.family, family) self.assertEqual(conn.type, type) self.assertEqual(conn.laddr, laddr) @@ -549,7 +473,7 @@ def test_count(self): @unittest.skipIf(SKIP_SYSCONS, "requires root") -class TestSystemWideConnections(_ConnTestCase): +class TestSystemWideConnections(ConnectionTestCase): """Tests for net_connections().""" def test_it(self): @@ -558,7 +482,7 @@ def check(cons, families, types_): self.assertIn(conn.family, families, msg=conn) if conn.family != AF_UNIX: self.assertIn(conn.type, types_, msg=conn) - self.check_connection_ntuple(conn) + check_connection_ntuple(conn) with create_sockets(): from psutil._common import conn_tmap @@ -573,6 +497,8 @@ def check(cons, families, types_): # See: https://travis-ci.org/giampaolo/psutil/jobs/237566297 @unittest.skipIf(MACOS and TRAVIS, "unreliable on MACOS + TRAVIS") + # XXX + @unittest.skipIf(TRAVIS and PYTHON_39, "unreliable on TRAVIS + PYTHON_39") @retry_on_failure() def test_multi_sockets_procs(self): # Creates multiple sub processes, each creating different diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 659dc01806..43eb9cbc19 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -33,14 +33,16 @@ from psutil._compat import long from psutil._compat import range from psutil.tests import APPVEYOR +from psutil.tests import check_connection_ntuple from psutil.tests import create_sockets from psutil.tests import enum -from psutil.tests import GITHUB_WHEELS +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 is_namedtuple +from psutil.tests import kernel_version from psutil.tests import process_namespace from psutil.tests import PsutilTestCase from psutil.tests import PYPY @@ -87,8 +89,9 @@ def test_linux_ioprio_windows(self): ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) - @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") - def test_linux_rlimit(self): + @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) @@ -103,11 +106,17 @@ def test_linux_rlimit(self): ae(hasattr(psutil, "RLIMIT_STACK"), LINUX or FREEBSD) ae(hasattr(psutil, "RLIMIT_LOCKS"), LINUX) - ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX) # requires Linux 2.6.8 - ae(hasattr(psutil, "RLIMIT_NICE"), LINUX) # requires Linux 2.6.12 - ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX) # requires Linux 2.6.12 - ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX) # requires Linux 2.6.25 - ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX) # requires Linux 2.6.8 + 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) @@ -157,9 +166,9 @@ def test_terminal(self): def test_ionice(self): self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) - @unittest.skipIf(GITHUB_WHEELS, "not exposed via GITHUB_WHEELS") + @unittest.skipIf(GITHUB_ACTIONS and LINUX, + "unsupported on GITHUB_ACTIONS + LINUX") def test_rlimit(self): - # requires Linux 2.6.36 self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX or FREEBSD) def test_io_counters(self): @@ -368,14 +377,15 @@ def do_wait(): name, ppid = d['name'], d['ppid'] info = {'pid': proc.pid} ns = process_namespace(proc) - with proc.oneshot(): - 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() + # 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 @@ -619,6 +629,7 @@ def connections(self, ret, info): self.assertEqual(len(ret), len(set(ret))) for conn in ret: assert is_namedtuple(conn) + check_connection_ntuple(conn) def cwd(self, ret, info): if ret: # 'ret' can be None or empty diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 519d4b2e2f..fbfa05a96f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1975,7 +1975,7 @@ 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", + with mock.patch("psutil._pslinux.prlimit", side_effect=OSError(errno.ENOSYS, "")) as m: p = psutil.Process() p.name() diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index cab91428d2..48a65dd535 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -243,7 +243,7 @@ def test_rlimit(self): 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, lambda: self.proc.rlimit(-1)) + self.execute_w_exc((OSError, ValueError), lambda: self.proc.rlimit(-1)) @fewtimes_if_linux() # Windows implementation is based on a single system-wide diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index f4e1a5df12..dac818598a 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -17,6 +17,7 @@ import pickle import socket import stat +import sys from psutil import LINUX from psutil import POSIX @@ -48,6 +49,9 @@ import psutil.tests +PYTHON_39 = sys.version_info[:2] == (3, 9) + + # =================================================================== # --- Misc / generic tests. # =================================================================== @@ -60,7 +64,8 @@ def test_process__repr__(self, func=repr): r = func(p) self.assertIn("psutil.Process", r) self.assertIn("pid=%s" % p.pid, r) - self.assertIn("name='%s'" % p.name(), r) + self.assertIn("name='%s'" % str(p.name()), + r.replace("name=u'", "name='")) self.assertIn("status=", r) self.assertNotIn("exitcode=", r) p.terminate() @@ -633,6 +638,8 @@ def test_cache_clear_public_apis(self): @unittest.skipIf(not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory") +# XXX +@unittest.skipIf(TRAVIS and PYTHON_39, "unreliable on TRAVIS + PYTHON_39") class TestScripts(PsutilTestCase): """Tests for scripts in the "scripts" directory.""" diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index e2d18ccb9f..54cf5ceb54 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -371,6 +371,7 @@ def test_os_waitpid_bad_ret_status(self): # 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): out = sh("df -k %s" % device).strip() diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 33fcbf848c..bf301ed0aa 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -43,7 +43,7 @@ from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe -from psutil.tests import GITHUB_WHEELS +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 @@ -1274,7 +1274,7 @@ def assert_raises_nsp(fun, fun_name): assert_raises_nsp(fun, name) # NtQuerySystemInformation succeeds even if process is gone. - if WINDOWS and not GITHUB_WHEELS: + if WINDOWS and not GITHUB_ACTIONS: normcase = os.path.normcase self.assertEqual(normcase(p.exe()), normcase(PYTHON_EXE)) @@ -1398,7 +1398,7 @@ def clean_dict(d): p = psutil.Process() d1 = clean_dict(p.environ()) d2 = clean_dict(os.environ.copy()) - if not OSX and GITHUB_WHEELS: + if not OSX and GITHUB_ACTIONS: self.assertEqual(d1, d2) @unittest.skipIf(not HAS_ENVIRON, "not supported") diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 872b8988e4..69f318b492 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -49,7 +49,7 @@ from psutil.tests import PYPY from psutil.tests import retry_on_failure from psutil.tests import TRAVIS -from psutil.tests import GITHUB_WHEELS +from psutil.tests import GITHUB_ACTIONS from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest @@ -599,7 +599,7 @@ def check_ntuple(nt): 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: + 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) @@ -633,7 +633,7 @@ def check_ntuple(nt): try: os.stat(disk.mountpoint) except OSError as err: - if (GITHUB_WHEELS or TRAVIS) and \ + if (GITHUB_ACTIONS or TRAVIS) and \ MACOS and err.errno == errno.EIO: continue # http://mail.python.org/pipermail/python-dev/ diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 580e1e5e09..a9254e2c76 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -24,7 +24,7 @@ from psutil._compat import FileNotFoundError from psutil._compat import super from psutil.tests import APPVEYOR -from psutil.tests import GITHUB_WHEELS +from psutil.tests import GITHUB_ACTIONS from psutil.tests import HAS_BATTERY from psutil.tests import IS_64BIT from psutil.tests import mock @@ -68,8 +68,7 @@ def wrapper(self, *args, **kwargs): @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_WHEELS and (not PY3 or not IS_64BIT), - "pywin32 broken on GITHUB + PY2") +@unittest.skipIf(GITHUB_ACTIONS and not PY3, "pywin32 broken on GITHUB + PY2") class WindowsTestCase(PsutilTestCase): pass @@ -512,7 +511,7 @@ def test_name(self): self.assertEqual(p.name(), w.Caption) # This fail on github because using virtualenv for test environment - @unittest.skipIf(GITHUB_WHEELS, "unreliable path on GITHUB_WHEELS") + @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) diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index a6773f34c2..83ea55a1b9 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -66,7 +66,7 @@ def get_file_urls(options): yield url -def rename_27_wheels(): +def rename_win27_wheels(): # See: https://github.com/giampaolo/psutil/issues/810 src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION @@ -101,7 +101,7 @@ def run(options): return exit("expected %s files, got %s" % (expected, completed)) if exc: return exit() - rename_27_wheels() + rename_win27_wheels() def main(): diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py old mode 100644 new mode 100755 index 4aa50d5df1..5623bb59b3 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels_github.py @@ -21,6 +21,7 @@ import requests import zipfile +from psutil import __version__ as PSUTIL_VERSION from psutil._common import bytes2human from psutil.tests import safe_rmpath @@ -52,12 +53,25 @@ def download_zip(url): print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) +def rename_win27_wheels(): + # See: https://github.com/giampaolo/psutil/issues/810 + src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION + dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION + print("rename: %s\n %s" % (src, dst)) + os.rename(src, dst) + src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PSUTIL_VERSION + dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PSUTIL_VERSION + print("rename: %s\n %s" % (src, dst)) + os.rename(src, dst) + + def run(): data = get_artifacts() download_zip(data['artifacts'][0]['archive_download_url']) os.makedirs('dist', exist_ok=True) with zipfile.ZipFile(OUTFILE, 'r') as zf: zf.extractall('dist') + rename_win27_wheels() def main(): diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index c0be6d99d9..f760dd65c1 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -13,8 +13,8 @@ SKIP_EXTS = ('.png', '.jpg', '.jpeg') -SKIP_FILES = ('.travis.yml', 'appveyor.yml') -SKIP_PREFIXES = ('.ci/', '.github/') +SKIP_FILES = ('.cirrus.yml', '.travis.yml', 'appveyor.yml') +SKIP_PREFIXES = ('.ci/', '.github/', 'scripts/internal/') def sh(cmd): diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 180bf37760..c9948c1d9f 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -6,16 +6,22 @@ """ Prints release announce based on HISTORY.rst file content. +See: https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode """ import os import re +import subprocess +import sys from psutil import __version__ as PRJ_VERSION HERE = os.path.abspath(os.path.dirname(__file__)) -HISTORY = os.path.abspath(os.path.join(HERE, '../../HISTORY.rst')) +ROOT = os.path.realpath(os.path.join(HERE, '..', '..')) +HISTORY = os.path.join(ROOT, 'HISTORY.rst') +PRINT_HASHES_SCRIPT = os.path.join( + ROOT, 'scripts', 'internal', 'print_hashes.py') PRJ_NAME = 'psutil' PRJ_URL_HOME = 'https://github.com/giampaolo/psutil' @@ -56,6 +62,11 @@ - Documentation: {prj_urldoc} - What's new: {prj_urlwhatsnew} +Hashes +====== + +{hashes} + -- Giampaolo - https://gmpy.dev/about @@ -100,6 +111,8 @@ def get_changes(): def main(): changes = get_changes() + hashes = subprocess.check_output( + [sys.executable, PRINT_HASHES_SCRIPT, 'dist/']).strip().decode() print(template.format( prj_name=PRJ_NAME, prj_version=PRJ_VERSION, @@ -108,6 +121,7 @@ def main(): prj_urldoc=PRJ_URL_DOC, prj_urlwhatsnew=PRJ_URL_WHATSNEW, changes=changes, + hashes=hashes, )) diff --git a/scripts/internal/print_hashes.py b/scripts/internal/print_hashes.py new file mode 100755 index 0000000000..434c43d59e --- /dev/null +++ b/scripts/internal/print_hashes.py @@ -0,0 +1,35 @@ +#!/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. + +""" +Prints files hashes. +See: https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode +""" + +import hashlib +import os +import sys + + +def csum(file, kind): + h = hashlib.new(kind) + with open(file, "rb") as f: + h.update(f.read()) + return h.hexdigest() + + +def main(): + dir = sys.argv[1] + for name in sorted(os.listdir(dir)): + file = os.path.join(dir, name) + md5 = csum(file, "md5") + sha256 = csum(file, "sha256") + print("%s\nmd5: %s\nsha256: %s\n" % ( + os.path.basename(file), md5, sha256)) + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_wheels.py old mode 100644 new mode 100755 index be8290e063..3c966173cb --- a/scripts/internal/print_wheels.py +++ b/scripts/internal/print_wheels.py @@ -42,16 +42,18 @@ def is64bit(name): else: assert 0, name - totsize = 0 + tot_files = 0 + tot_size = 0 templ = "%-54s %7s %7s %7s" for platf, names in groups.items(): ppn = "%s (total = %s)" % (platf.replace('_', ' '), len(names)) s = templ % (ppn, "size", "arch", "pyver") print_color('\n' + s, color=None, bold=True) for name in sorted(names): + tot_files += 1 path = os.path.join('dist', name) size = os.path.getsize(path) - totsize += size + tot_size += size arch = '64' if is64bit(name) else '32' pyver = 'pypy' if name.split('-')[3].startswith('pypy') else 'py' pyver += name.split('-')[2][2:] @@ -61,6 +63,9 @@ def is64bit(name): else: print_color(s, color='brown') + print_color("\ntotals: files=%s, size=%s" % ( + tot_files, bytes2human(tot_size)), bold=1) + if __name__ == '__main__': main() diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 8eea3377a3..743a177761 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -102,7 +102,6 @@ "RLIMIT_CPU": "cputime", "RLIMIT_DATA": "datasize", "RLIMIT_FSIZE": "filesize", - "RLIMIT_LOCKS": "locks", "RLIMIT_MEMLOCK": "memlock", "RLIMIT_MSGQUEUE": "msgqueue", "RLIMIT_NICE": "nice", diff --git a/setup.py b/setup.py index 80c3bc3c8a..90c79c7c76 100755 --- a/setup.py +++ b/setup.py @@ -430,14 +430,6 @@ def main(): elif SUNOS: missdeps("sudo ln -s /usr/bin/gcc /usr/local/bin/cc && " "pkg install gcc") - elif not success and WINDOWS: - if PY3: - ur = "http://www.visualstudio.com/en-au/news/vs2015-preview-vs" - else: - ur = "http://www.microsoft.com/en-us/download/" - ur += "details.aspx?id=44266" - s = "VisualStudio is not installed; get it from %s" % ur - print(hilite(s, color="red"), file=sys.stderr) if __name__ == '__main__': From 21bb0822c7d30adc1e144e87d730cd67eb4fa828 Mon Sep 17 00:00:00 2001 From: zed Date: Sun, 15 Nov 2020 17:09:31 +0300 Subject: [PATCH 0661/1714] remove misleading "in UTC" (#1882) "seconds since the epoch" (value returned by `time.time()`) doesn't depend on the local time zone. It is the same time instance around the world. It is not in any particular time zone: ```python import datetime as DT import zoneinfo local_time = DT.datetime.fromtimestamp(seconds_since_epoch) # naive datetime object representing local time utc_time = DT.datetime.utcfromtimestamp(seconds_since_epoch) # naive datetime object representing utc time la_time = DT.datetime.fromtimestamp(seconds_since_epoch, zoneinfo.ZoneInfo("America/Los_Angeles")) # timezone aware dt ``` Here `local_time`, `utc_time`, `la_time` may correspond to different clock times but it is exactly the same time instance. --- docs/index.rst | 2 +- psutil/__init__.py | 2 +- psutil/_pslinux.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 467951506e..a99ab20622 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1180,7 +1180,7 @@ Process class .. method:: create_time() The process creation time as a floating point number expressed in seconds - since the epoch, in UTC. The return value is cached after first call. + since the epoch. The return value is cached after first call. >>> import psutil, datetime >>> p = psutil.Process() diff --git a/psutil/__init__.py b/psutil/__init__.py index fc7f225a3f..9b4eb8946c 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -703,7 +703,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: diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 18439c82d3..ff13b5eb29 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1779,7 +1779,7 @@ def create_time(self): # 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 (ctime / CLOCK_TICKS) + bt From e58b0fdaeaedd73a0ca19ad23a63874708d86b91 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Nov 2020 21:54:42 +0100 Subject: [PATCH 0662/1714] Remove Travis and Cirrus, use GH also for FreeBSD (#1880) --- .ci/appveyor/run_with_compiler.cmd | 1 + .ci/travis/README | 2 - .ci/travis/install.sh | 48 ----------- .ci/travis/run.sh | 38 --------- .cirrus.yml | 31 ------- .github/ISSUE_TEMPLATE/bug.md | 6 +- .github/ISSUE_TEMPLATE/enhancement.md | 1 - .github/lock.yml | 35 -------- .github/workflows/build_wheel.yml | 51 ------------ .github/workflows/ci.yml | 95 ++++++++++++++++++++++ .travis.yml | 27 ------ HISTORY.rst | 5 +- MANIFEST.in | 22 ++++- Makefile | 14 ++-- README.rst | 22 +++-- appveyor.yml | 14 +++- docs/DEVGUIDE.rst | 19 ++--- psutil/__init__.py | 2 +- psutil/_psutil_common.c | 1 + psutil/tests/README.rst | 9 -- psutil/tests/__init__.py | 21 ++--- psutil/tests/runner.py | 6 +- psutil/tests/test_connections.py | 9 -- psutil/tests/test_linux.py | 12 --- psutil/tests/test_memleaks.py | 7 +- psutil/tests/test_misc.py | 7 -- psutil/tests/test_posix.py | 2 - psutil/tests/test_process.py | 27 ++---- psutil/tests/test_system.py | 11 +-- psutil/tests/test_unicode.py | 59 ++++++-------- scripts/internal/download_wheels_github.py | 10 ++- scripts/internal/generate_manifest.py | 4 +- scripts/internal/print_wheels.py | 80 ++++++++++++------ scripts/internal/winmake.py | 21 ----- tox.ini | 28 ------- 35 files changed, 269 insertions(+), 478 deletions(-) delete mode 100644 .ci/travis/README delete mode 100755 .ci/travis/install.sh delete mode 100755 .ci/travis/run.sh delete mode 100644 .cirrus.yml delete mode 100644 .github/lock.yml delete mode 100644 .github/workflows/build_wheel.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 tox.ini diff --git a/.ci/appveyor/run_with_compiler.cmd b/.ci/appveyor/run_with_compiler.cmd index 5da547c499..7965f865fe 100644 --- a/.ci/appveyor/run_with_compiler.cmd +++ b/.ci/appveyor/run_with_compiler.cmd @@ -29,6 +29,7 @@ :: The CALL lines at the end of this file look redundant, but if you move them :: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y :: case, I don't know why. + @ECHO OFF SET COMMAND_TO_RUN=%* diff --git a/.ci/travis/README b/.ci/travis/README deleted file mode 100644 index d9d5f65adf..0000000000 --- a/.ci/travis/README +++ /dev/null @@ -1,2 +0,0 @@ -This directory contains support files for Travis, a continuous integration -service which runs tests on Linux and Windows on every push. diff --git a/.ci/travis/install.sh b/.ci/travis/install.sh deleted file mode 100755 index ff6b93780d..0000000000 --- a/.ci/travis/install.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -set -e -set -x - -uname -a -python -c "import sys; print(sys.version)" - -if [[ "$(uname -s)" == 'Darwin' ]]; then - brew update || brew update - brew outdated pyenv || brew upgrade pyenv - brew install pyenv-virtualenv - - if which pyenv > /dev/null; then - eval "$(pyenv init -)" - fi - - case "${PYVER}" in - py27) - pyenv install 2.7.16 - pyenv virtualenv 2.7.16 psutil - ;; - py36) - pyenv install 3.6.6 - pyenv virtualenv 3.6.6 psutil - ;; - py37) - pyenv install 3.7.6 - pyenv virtualenv 3.7.6 psutil - ;; - py38) - pyenv install 3.8.5 - pyenv virtualenv 3.8.5 psutil - ;; - py39) - pyenv install 3.9.0 - pyenv virtualenv 3.9.0 psutil - ;; - esac - pyenv rehash - pyenv activate psutil -fi - -if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] || [[ $PYVER == 'py27' ]]; then - pip install -U ipaddress mock unittest2 -fi - -pip install -U coverage coveralls flake8 setuptools twine diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh deleted file mode 100755 index ae593df5dd..0000000000 --- a/.ci/travis/run.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -set -e -set -x - -PYVER=`python -c 'import sys; print(".".join(map(str, sys.version_info[:2])))'` - -# setup macOS -if [[ "$(uname -s)" == 'Darwin' ]]; then - if which pyenv > /dev/null; then - eval "$(pyenv init -)" - fi - pyenv activate psutil -fi - -# install psutil -make clean -python setup.py build -python setup.py develop - -# run tests (with coverage) -if [[ $PYVER == '2.7' ]] && [[ "$(uname -s)" != 'Darwin' ]]; then - PSUTIL_TESTING=1 python -Wa -m coverage run psutil/tests/runner.py -else - PSUTIL_TESTING=1 python -Wa psutil/tests/runner.py -fi - -if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then - # run mem leaks test - PSUTIL_TESTING=1 python -Wa psutil/tests/test_memleaks.py - # run linter (on Linux only) - if [[ "$(uname -s)" != 'Darwin' ]]; then - make lint PYTHON=python - fi -fi - -PSUTIL_TESTING=1 python -Wa scripts/internal/print_access_denied.py -PSUTIL_TESTING=1 python -Wa scripts/internal/print_api_speed.py diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index a0b8f1f0b3..0000000000 --- a/.cirrus.yml +++ /dev/null @@ -1,31 +0,0 @@ -freebsd_13_py3_task: - freebsd_instance: - image: freebsd-12-1-release-amd64 - env: - CIRRUS: 1 - install_script: - - pkg install -y python3 gcc py37-pip - script: - - python3 -m pip install --user setuptools - - make clean - - make install - - make test - - make test-memleaks - - make print-access-denied - - make print-api-speed - -freebsd_11_py2_task: - freebsd_instance: - image: freebsd-12-1-release-amd64 - env: - CIRRUS: 1 - install_script: - - pkg install -y python gcc py27-pip - script: - - python2.7 -m pip install --user setuptools ipaddress mock - - make clean - - make install - - make test - - make test-memleaks - - make print-access-denied - - make print-api-speed diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 67a9601b2b..1cdba8105a 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -7,13 +7,9 @@ labels: 'bug' --- **Platform** * { OS version } -* { psutil version: python3 -c "import psutil; print(psutil.__version__)" } +* { psutil version (print psutil.__version__) } * { python version } **Bug description** ... - - -**Test results** -{ output of `python -c psutil.tests` (failures only, not full result) } diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 7e7159f272..2111c572f3 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -6,4 +6,3 @@ title: "[OS] title" --- -{ a clear and concise description of what the enhancment is about } diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 7099c810c4..0000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 3 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: false - -# Issues and pull requests with these labels will be ignored. Set to `[]` to disable -exemptLabels: [] - -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: false - -# Comment to post before locking. Set to `false` to disable -lockComment: false - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: false - -# Limit to only `issues` or `pulls` -# only: issues - -# Optionally, specify configuration settings just for `issues` or `pulls` -# issues: -# exemptLabels: -# - help-wanted -# lockLabel: outdated - -# pulls: -# daysUntilLock: 30 - -# Repository to extend settings from -# _extends: repo diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml deleted file mode 100644 index 1a8b486843..0000000000 --- a/.github/workflows/build_wheel.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Linux, macOS, Windows - -on: [push, pull_request] - -jobs: - wheel: - name: ${{ matrix.os }} - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - strategy: - fail-fast: false # whether to exit the whole run on first failure - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - include: - - {name: Linux, python: '3.9', os: ubuntu-latest} - env: - CIBW_TEST_COMMAND: python -u -Wa {project}/psutil/tests/runner.py - CIBW_TEST_COMMAND_MACOS: LC_ALL='en_US.utf8' python -Wa {project}/psutil/tests/runner.py - CIBW_TEST_EXTRAS: test - # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip - CIBW_SKIP: cp35-* pp* - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - - name: (Windows) install Visual C++ for Python 2.7 - if: matrix.os == 'windows-latest' - run: | - choco install vcpython27 -f -y - - - name: Run tests - run: | - pip install cibuildwheel - cibuildwheel . - - - name: Create wheels - uses: actions/upload-artifact@v2 - with: - name: wheels - path: wheelhouse - - - name: Print hashes - if: matrix.os == 'ubuntu-latest' - run: | - make generate-manifest - python setup.py sdist - mv dist/psutil*.tar.gz wheelhouse/ - python scripts/internal/print_hashes.py wheelhouse/ - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..c93ad4808c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +# Executed on every push by GitHub Actions. This runs CI tests and +# generates wheels on the following platforms: +# +# * Linux +# * macOS +# * FreeBSD +# +# Windows works as well but it's disabled (we do it via AppVeyor). +# To skip certain builds see: +# https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip + +name: CI +on: [push] +jobs: + linux-macos-win: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + # os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] + include: + - {name: Linux, python: '3.9', os: ubuntu-latest} + env: + CIBW_TEST_COMMAND: + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 + python {project}/psutil/tests/runner.py && + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 + python {project}/psutil/tests/test_memleaks.py + CIBW_TEST_EXTRAS: test + CIBW_SKIP: cp35-* pp* + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.9 + + # - name: (Windows) install Visual C++ for Python 2.7 + # if: matrix.os == 'windows-latest' + # run: | + # choco install vcpython27 -f -y + + - name: Run tests + run: | + pip install cibuildwheel + cibuildwheel . + + - name: Create wheels + uses: actions/upload-artifact@v2 + with: + name: wheels + path: wheelhouse + + - name: Print hashes + if: matrix.os == 'ubuntu-latest' + run: | + make generate-manifest + python setup.py sdist + mv dist/psutil*.tar.gz wheelhouse/ + python scripts/internal/print_hashes.py wheelhouse/ + + freebsd: + runs-on: macos-latest + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v2 + + - name: Run tests + id: test + uses: vmactions/freebsd-vm@v0.0.8 + with: + usesh: true + prepare: pkg install -y gcc python3 + run: | + set +e + export \ + PYTHONWARNINGS=always \ + PYTHONUNBUFFERED=1 \ + PSUTIL_TESTING=1 \ + PSUTIL_DEBUG=1 + python3 -m pip install --user setuptools + python3 setup.py install + python3 psutil/tests/runner.py + python3 psutil/tests/test_memleaks.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d9420682c2..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: python -cache: pip -matrix: - include: - # Linux - - python: 3.8 - - python: 3.9-dev - # macOS - - language: generic - os: osx - env: PYVER=py38 - - language: generic - os: osx - env: PYVER=py27 - # pypy - - python: pypy - - python: pypy3 -install: - - ./.ci/travis/install.sh -script: - - ./.ci/travis/run.sh -after_success: - - | - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]] && [[ "$(uname -s)" != 'Darwin' ]]; then - echo "sending test coverage results to coveralls.io" - coveralls - fi diff --git a/HISTORY.rst b/HISTORY.rst index bcbffa69d6..58626e10ac 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,9 +7,12 @@ XXXX-XX-XX **Enhancements** -- 1872_: [Windows] added support for PyPy 2.7. - 1863_: `disk_partitions()` exposes 2 extra fields: `maxfile` and `maxpath`, which are the maximum file name and path name length. +- 1872_: [Windows] added support for PyPy 2.7. +- 1879_: provide pre-compiled wheels for Linux and macOS (yey!). +- 1880_: get rid of Travis and Cirrus CI services (they are no longer free). + CI testing is now done by GitHub Actions on Linux, macOS and FreeBSD (yes). AppVeyor is still being used for Windows CI. **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index 780bbdc735..2bed87e78b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -111,6 +111,27 @@ include scripts/disk_usage.py include scripts/fans.py include scripts/free.py include scripts/ifconfig.py +include scripts/internal/README +include scripts/internal/bench_oneshot.py +include scripts/internal/bench_oneshot_2.py +include scripts/internal/check_broken_links.py +include scripts/internal/clinter.py +include scripts/internal/convert_readme.py +include scripts/internal/download_wheels_appveyor.py +include scripts/internal/download_wheels_github.py +include scripts/internal/fix_flake8.py +include scripts/internal/generate_manifest.py +include scripts/internal/git_pre_commit.py +include scripts/internal/print_access_denied.py +include scripts/internal/print_announce.py +include scripts/internal/print_api_speed.py +include scripts/internal/print_downloads.py +include scripts/internal/print_hashes.py +include scripts/internal/print_timeline.py +include scripts/internal/print_wheels.py +include scripts/internal/purge_installation.py +include scripts/internal/tidelift.py +include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py @@ -128,4 +149,3 @@ include scripts/top.py include scripts/who.py include scripts/winservices.py include setup.py -include tox.ini diff --git a/Makefile b/Makefile index 4718976d68..8b4c992812 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,6 @@ clean: ## Remove all build files. *\@psutil-* \ .coverage \ .failed-tests.txt \ - .tox \ build/ \ dist/ \ docs/_build/ \ @@ -212,13 +211,11 @@ install-git-hooks: ## Install GIT pre-commit hook. # Wheels # =================================================================== -download-wheels-appveyor: ## Download latest wheels hosted on appveyor. - $(PYTHON) scripts/internal/download_wheels_appveyor.py --user giampaolo --project psutil - ${MAKE} print-wheels - download-wheels-github: ## Download latest wheels hosted on github. $(PYTHON) scripts/internal/download_wheels_github.py --user=giampaolo --project=psutil --tokenfile=~/.github.token - ${MAKE} print-wheels + +download-wheels-appveyor: ## Download latest wheels hosted on appveyor. + $(PYTHON) scripts/internal/download_wheels_appveyor.py --user giampaolo --project psutil print-wheels: ## Print downloaded wheels $(PYTHON) scripts/internal/print_wheels.py @@ -261,10 +258,11 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} install ${MAKE} generate-manifest git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain - ${MAKE} download-wheels-github ${MAKE} sdist + ${MAKE} download-wheels-github + ${MAKE} download-wheels-appveyor + ${MAKE} print-wheels $(PYTHON) -m twine check dist/* - ${MAKE} md5-dist $(PYTHON) -c \ "from psutil import __version__ as ver; \ doc = open('docs/index.rst').read(); \ diff --git a/README.rst b/README.rst index 4095a3c5d6..ec323253eb 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ | |downloads| |stars| |forks| |contributors| |coverage| |quality| | |version| |py-versions| |packages| |license| -| |travis| |appveyor| |cirrus| |doc| |twitter| |tidelift| +| |github-actions| |appveyor| |doc| |twitter| |tidelift| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil @@ -22,18 +22,14 @@ :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade :alt: Code quality -.. |travis| image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux,%20OSX,%20PyPy - :target: https://travis-ci.org/giampaolo/psutil - :alt: Linux tests (Travis) +.. |github-actions| image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD + :target: https://github.com/giampaolo/psutil/actions?query=workflow%3ACI + :alt: Linux, macOS, Windows tests .. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil :alt: Windows tests (Appveyor) -.. |cirrus| image:: https://img.shields.io/cirrus/github/giampaolo/psutil?label=FreeBSD - :target: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci - :alt: FreeBSD tests (Cirrus-Ci) - .. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) @@ -283,10 +279,12 @@ Process management >>> import psutil >>> psutil.pids() - [1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, 222, 223, 224, 268, 1215, 1216, 1220, 1221, 1243, 1244, - 1301, 1601, 2237, 2355, 2637, 2774, 3932, 4176, 4177, 4185, 4187, 4189, 4225, 4243, 4245, 4263, 4282, - 4306, 4311, 4312, 4313, 4314, 4337, 4339, 4357, 4358, 4363, 4383, 4395, 4408, 4433, 4443, 4445, 4446, - 5167, 5234, 5235, 5252, 5318, 5424, 5644, 6987, 7054, 7055, 7071] + [1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, 222, 223, 224, 268, 1215, + 1216, 1220, 1221, 1243, 1244, 1301, 1601, 2237, 2355, 2637, 2774, 3932, + 4176, 4177, 4185, 4187, 4189, 4225, 4243, 4245, 4263, 4282, 4306, 4311, + 4312, 4313, 4314, 4337, 4339, 4357, 4358, 4363, 4383, 4395, 4408, 4433, + 4443, 4445, 4446, 5167, 5234, 5235, 5252, 5318, 5424, 5644, 6987, 7054, + 7055, 7071] >>> >>> p = psutil.Process(7055) >>> p diff --git a/appveyor.yml b/appveyor.yml index ff9d35b91f..a05322060c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,23 @@ # Build: 3 (bump this up by 1 to force an appveyor run) os: Visual Studio 2015 - +# avoid 2 builds when pushing on PRs +skip_branch_with_pr: true +# avoid build on new GIT tag +skip_tags: true +matrix: + # stop build on first failure + fast_finish: true environment: global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script intepreter # See: http://stackoverflow.com/a/13751649/163740 WITH_COMPILER: "cmd /E:ON /V:ON /C .\\.ci\\appveyor\\run_with_compiler.cmd" + PYTHONWARNINGS: always + PYTHONUNBUFFERED: 1 + PSUTIL_TESTING: 1 + PSUTIL_DEBUG: 1 matrix: # 32 bits @@ -75,6 +85,7 @@ test_script: after_test: - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py wheel" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_hashes.py dist" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_access_denied.py" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_api_speed.py" @@ -105,4 +116,3 @@ only_commits: - psutil/tests/* - scripts/* - setup.py - diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 573afdd5d5..2ed8c42a12 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -107,30 +107,24 @@ All of the services listed below are automatically run on each ``git push``. Unit tests ---------- -Tests are automatically run on every GIT push on **Linux**, **macOS**, +Tests are automatically run on every GIT push and PR on **Linux**, **macOS**, **Windows** and **FreeBSD** by using: -- `Travis`_ (Linux, macOS) - `Github Actions`_ (Linux, macOS, Windows) - `Appveyor`_ (Windows) -- `Cirrus CI`_ (FreeBSD) -.. image:: https://img.shields.io/travis/giampaolo/psutil/master.svg?maxAge=3600&label=Linux,%20OSX,%20PyPy - :target: https://travis-ci.org/giampaolo/psutil +.. image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=linux%2C%20macos%2C%20freebsd + :target: https://github.com/giampaolo/psutil/actions?query=workflow%3ACI -.. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows +.. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=windows :target: https://ci.appveyor.com/project/giampaolo/psutil -.. image:: https://img.shields.io/cirrus/github/giampaolo/psutil?label=FreeBSD - :target: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci - OpenBSD, NetBSD, AIX and Solaris does not have continuos test integration. Test coverage ------------- -Test coverage is provided by `coveralls.io`_ and it is controlled via -`.travis.yml`_. +Test coverage is provided by `coveralls.io`_. .. image:: https://coveralls.io/repos/giampaolo/psutil/badge.svg?branch=master&service=github :target: https://coveralls.io/github/giampaolo/psutil?branch=master @@ -144,10 +138,8 @@ Documentation - public doc is hosted at https://psutil.readthedocs.io -.. _`.travis.yml`: https://github.com/giampaolo/psutil/blob/master/.travis.yml .. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.yml .. _`Appveyor`: https://ci.appveyor.com/project/giampaolo/psuti -.. _`Cirrus CI`: https://cirrus-ci.com/github/giampaolo/psutil-cirrus-ci .. _`coveralls.io`: https://coveralls.io/github/giampaolo/psuti .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS .. _`doc/index.rst`: https://github.com/giampaolo/psutil/blob/master/doc/index.rst @@ -165,4 +157,3 @@ Documentation .. _`psutil/tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_system.py .. _`RsT syntax`: http://docutils.sourceforge.net/docs/user/rst/quickref.htm .. _`sphinx`: http://sphinx-doc.org -.. _`Travis`: https://travis-ci.org/giampaolo/psuti diff --git a/psutil/__init__.py b/psutil/__init__.py index 9b4eb8946c..acd42ac264 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -209,7 +209,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.7.4" +__version__ = "5.8.0" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index a01919d57c..e72904deff 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -142,6 +142,7 @@ AccessDenied(const char *syscall) { PyObject * psutil_set_testing(PyObject *self, PyObject *args) { PSUTIL_TESTING = 1; + PSUTIL_DEBUG = 1; Py_INCREF(Py_None); return Py_None; } diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index 61e066b7a0..9dca118674 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -12,12 +12,3 @@ Instructions for running tests make setup-dev-env # install missing third-party deps make test # serial run make test-parallel # parallel run - -* To run tests on all supported Python versions install tox - (``pip install tox``) then run ``tox`` from within psutil root directory. - -* Every time a commit is pushed tests are automatically run on Travis - (Linux, MACOS) and appveyor (Windows): - - * Travis builds: https://travis-ci.org/giampaolo/psutil - * AppVeyor builds: https://ci.appveyor.com/project/giampaolo/psutil diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 36f83435e6..e0df67fa30 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -37,6 +37,7 @@ import psutil from psutil import AIX +from psutil import FREEBSD from psutil import LINUX from psutil import MACOS from psutil import POSIX @@ -76,7 +77,7 @@ # constants 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', - 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TRAVIS', 'CIRRUS', + '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", @@ -119,11 +120,9 @@ PYPY = '__pypy__' in sys.builtin_module_names # whether we're running this test suite on a Continuous Integration service -TRAVIS = 'TRAVIS' in os.environ APPVEYOR = 'APPVEYOR' in os.environ -CIRRUS = 'CIRRUS' in os.environ GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ -CI_TESTING = TRAVIS or APPVEYOR or CIRRUS or GITHUB_ACTIONS +CI_TESTING = APPVEYOR or GITHUB_ACTIONS # are we a 64 bit process? IS_64BIT = sys.maxsize > 2 ** 32 @@ -137,8 +136,7 @@ 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 travis / appveyor in order to avoid -# false positives +# be more tolerant if we're on CI in order to avoid false positives if CI_TESTING: NO_RETRIES *= 3 GLOBAL_TIMEOUT *= 3 @@ -207,6 +205,8 @@ def attempt(exe): if GITHUB_ACTIONS: if PYPY: return which("pypy3") if PY3 else which("pypy") + elif FREEBSD: + return os.path.realpath(sys.executable) else: return which('python') elif MACOS: @@ -1126,22 +1126,23 @@ def print_sysinfo(): 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 - pinfo = psutil.Process().as_dict() - pinfo.pop('memory_maps', None) 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%%, %s/%s" % ( + info['memory'] = "%s%%, used=%s, total=%s" % ( int(mem.percent), bytes2human(mem.used), bytes2human(mem.total)) swap = psutil.swap_memory() - info['swap'] = "%s%%, %s/%s" % ( + 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 diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 0777f5e74d..d761cd21dc 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -304,9 +304,9 @@ def run_from_name(name): def setup(): - 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' + # Note: doc states that altering os.environment may cause memory + # leaks on some platforms. + # Sets PSUTIL_TESTING and PSUTIL_DEBUG in the C module. psutil._psplatform.cext.set_testing() diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index ba019ec0c8..6bbf2194d3 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -31,7 +31,6 @@ from psutil.tests import bind_socket from psutil.tests import bind_unix_socket from psutil.tests import check_connection_ntuple -from psutil.tests import CIRRUS from psutil.tests import create_sockets from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import PsutilTestCase @@ -41,7 +40,6 @@ from psutil.tests import skip_on_access_denied from psutil.tests import SKIP_SYSCONS from psutil.tests import tcp_socketpair -from psutil.tests import TRAVIS from psutil.tests import unittest from psutil.tests import unix_socketpair from psutil.tests import wait_for_file @@ -246,9 +244,6 @@ def test_unix(self): # 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'] - if CIRRUS: - cons = [c for c in cons if c.fd in - (server.fileno(), client.fileno())] self.assertEqual(len(cons), 2, msg=cons) if LINUX or FREEBSD or SUNOS: # remote path is never set @@ -495,10 +490,6 @@ def check(cons, families, types_): self.assertEqual(len(cons), len(set(cons))) check(cons, families, types_) - # See: https://travis-ci.org/giampaolo/psutil/jobs/237566297 - @unittest.skipIf(MACOS and TRAVIS, "unreliable on MACOS + TRAVIS") - # XXX - @unittest.skipIf(TRAVIS and PYTHON_39, "unreliable on TRAVIS + PYTHON_39") @retry_on_failure() def test_multi_sockets_procs(self): # Creates multiple sub processes, each creating different diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index fbfa05a96f..f8a9e00899 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -44,7 +44,6 @@ from psutil.tests import ThreadTask from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM -from psutil.tests import TRAVIS from psutil.tests import unittest from psutil.tests import which @@ -261,7 +260,6 @@ def test_used(self): free_value, psutil_value, delta=TOLERANCE_SYS_MEM, 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 @@ -276,8 +274,6 @@ def test_buffers(self): self.assertAlmostEqual( vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) - # 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 @@ -285,8 +281,6 @@ def test_active(self): self.assertAlmostEqual( vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) - # 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 @@ -638,7 +632,6 @@ def test_emulate_meminfo_has_no_metrics(self): @unittest.skipIf(not LINUX, "LINUX only") class TestSystemCPUTimes(PsutilTestCase): - @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] @@ -753,7 +746,6 @@ def test_emulate_none(self): @unittest.skipIf(not LINUX, "LINUX only") class TestSystemCPUFrequency(PsutilTestCase): - @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 @@ -874,7 +866,6 @@ def open_mock(name, *args, **kwargs): if freq[1].max != 0.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 @@ -901,13 +892,11 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") class TestSystemCPUStats(PsutilTestCase): - @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 @@ -961,7 +950,6 @@ def test_ips(self): # 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] diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 48a65dd535..ab6aac6914 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -49,7 +49,6 @@ from psutil.tests import system_namespace from psutil.tests import terminate from psutil.tests import TestMemoryLeak -from psutil.tests import TRAVIS from psutil.tests import unittest @@ -219,9 +218,8 @@ def test_cpu_affinity(self): def test_cpu_affinity_set(self): affinity = thisproc.cpu_affinity() self.execute(lambda: self.proc.cpu_affinity(affinity)) - if not TRAVIS: - self.execute_w_exc( - ValueError, lambda: self.proc.cpu_affinity([-1])) + self.execute_w_exc( + ValueError, lambda: self.proc.cpu_affinity([-1])) @fewtimes_if_linux() def test_open_files(self): @@ -429,7 +427,6 @@ def test_net_if_addrs(self): tolerance = 80 * 1024 if WINDOWS else self.tolerance self.execute(psutil.net_if_addrs, tolerance=tolerance) - # @unittest.skipIf(TRAVIS, "EPERM on travis") def test_net_if_stats(self): self.execute(psutil.net_if_stats) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index dac818598a..81fa8f390d 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -43,7 +43,6 @@ from psutil.tests import ROOT_DIR from psutil.tests import SCRIPTS_DIR from psutil.tests import sh -from psutil.tests import TRAVIS from psutil.tests import unittest import psutil import psutil.tests @@ -638,8 +637,6 @@ def test_cache_clear_public_apis(self): @unittest.skipIf(not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory") -# XXX -@unittest.skipIf(TRAVIS and PYTHON_39, "unreliable on TRAVIS + PYTHON_39") class TestScripts(PsutilTestCase): """Tests for scripts in the "scripts" directory.""" @@ -713,8 +710,6 @@ def test_pstree(self): 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') @@ -751,14 +746,12 @@ 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): if not psutil.sensors_temperatures(): self.skipTest("no temperatures") self.assert_stdout('temperatures.py') @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") def test_fans(self): if not psutil.sensors_fans(): self.skipTest("no fans") diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 54cf5ceb54..d9513eedb6 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -32,7 +32,6 @@ from psutil.tests import sh from psutil.tests import skip_on_access_denied from psutil.tests import terminate -from psutil.tests import TRAVIS from psutil.tests import unittest from psutil.tests import which @@ -307,7 +306,6 @@ def test_pids(self): # 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): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index bf301ed0aa..af35a58c7d 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -40,7 +40,6 @@ from psutil.tests import APPVEYOR from psutil.tests import call_until from psutil.tests import CI_TESTING -from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe from psutil.tests import GITHUB_ACTIONS @@ -64,7 +63,6 @@ from psutil.tests import skip_on_access_denied from psutil.tests import skip_on_not_implemented from psutil.tests import ThreadTask -from psutil.tests import TRAVIS from psutil.tests import unittest from psutil.tests import wait_for_pid @@ -295,7 +293,6 @@ def test_create_time(self): time.strftime("%Y %m %d %H:%M:%S", time.localtime(p.create_time())) @unittest.skipIf(not POSIX, 'POSIX only') - @unittest.skipIf(TRAVIS or CIRRUS, 'not reliable on TRAVIS/CIRRUS') def test_terminal(self): terminal = psutil.Process().terminal() if terminal is not None: @@ -758,9 +755,6 @@ def test_prog_w_funky_name(self): "import time; [time.sleep(0.01) for x in range(3000)];" "arg1", "arg2", "", "arg3", ""] p = self.spawn_psproc(cmdline) - # ...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()), @@ -875,9 +869,7 @@ def test_cpu_affinity(self): 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: + for n in all_cpus: p.cpu_affinity([n]) self.assertEqual(p.cpu_affinity(), [n]) if hasattr(os, "sched_getaffinity"): @@ -902,9 +894,8 @@ def test_cpu_affinity(self): 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)) + 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): @@ -1310,14 +1301,10 @@ def succeed_or_zombie_p_exc(fun): succeed_or_zombie_p_exc(fun) assert psutil.pid_exists(zproc.pid) - 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(zproc.pid, psutil.pids()) - self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) - psutil._pmap = {} - self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) + self.assertIn(zproc.pid, psutil.pids()) + self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) + psutil._pmap = {} + self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process_is_running_w_exc(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 69f318b492..4da4e784d5 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -48,7 +48,6 @@ from psutil.tests import PsutilTestCase from psutil.tests import PYPY from psutil.tests import retry_on_failure -from psutil.tests import TRAVIS from psutil.tests import GITHUB_ACTIONS from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest @@ -533,8 +532,6 @@ def check_ls(ls): 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") @@ -606,9 +603,6 @@ def check_ntuple(nt): # 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: check_ntuple(disk) @@ -633,8 +627,7 @@ def check_ntuple(nt): try: os.stat(disk.mountpoint) except OSError as err: - if (GITHUB_ACTIONS or TRAVIS) and \ - MACOS and err.errno == errno.EIO: + if GITHUB_ACTIONS and MACOS and err.errno == errno.EIO: continue # http://mail.python.org/pipermail/python-dev/ # 2012-June/120787.html @@ -655,7 +648,6 @@ def find_mount_point(path): mounts = [x.mountpoint.lower() for x in psutil.disk_partitions(all=True) if x.mountpoint] self.assertIn(mount, mounts) - psutil.disk_usage(mount) @unittest.skipIf(LINUX and not os.path.exists('/proc/diskstats'), '/proc/diskstats not available on this linux version') @@ -817,7 +809,6 @@ def test_net_if_addrs_mac_null_bytes(self): 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 diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 7ec6c497f0..9edb8c89c0 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -90,7 +90,6 @@ from psutil.tests import bind_unix_socket from psutil.tests import chdir from psutil.tests import CI_TESTING -from psutil.tests import CIRRUS from psutil.tests import copyload_shared_lib from psutil.tests import create_exe from psutil.tests import get_testfn @@ -124,9 +123,9 @@ def safe_rmpath(path): # NOQA # https://github.com/giampaolo/psutil/blob/ # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/ # windows/process_info.c#L146 - from psutil.tests import safe_rmpath as _rm + from psutil.tests import safe_rmpath as rm try: - return _rm(path) + return rm(path) except WindowsError: traceback.print_exc() @@ -158,11 +157,22 @@ def try_unicode(suffix): # =================================================================== +class BaseUnicodeTest(PsutilTestCase): + funky_suffix = None + + def setUp(self): + if self.funky_suffix is not None: + if not try_unicode(self.funky_suffix): + 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 _BaseFSAPIsTests(object): - funky_suffix = None +class TestFSAPIs(BaseUnicodeTest): + """Test FS APIs with a funky, valid, UTF8 path name.""" + + funky_suffix = UNICODE_SUFFIX @classmethod def setUpClass(cls): @@ -174,7 +184,12 @@ def tearDownClass(cls): safe_rmpath(cls.funky_name) def expect_exact_path_match(self): - raise NotImplementedError("must be implemented in subclass") + # 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) # --- @@ -243,7 +258,7 @@ def test_proc_connections(self): conn = psutil.Process().connections('unix')[0] self.assertIsInstance(conn.laddr, str) # AF_UNIX addr not set on OpenBSD - if not OPENBSD and not CIRRUS: # XXX + if not OPENBSD: # XXX self.assertEqual(conn.laddr, name) @unittest.skipIf(not POSIX, "POSIX only") @@ -296,28 +311,8 @@ def normpath(p): 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(not try_unicode(UNICODE_SUFFIX), - "can't deal with unicode str") -class TestFSAPIs(_BaseFSAPIsTests, PsutilTestCase): - """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) - - @unittest.skipIf(CI_TESTING, "unreliable on CI") -@unittest.skipIf(not try_unicode(INVALID_UNICODE_SUFFIX), - "can't deal with invalid unicode str") -class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, PsutilTestCase): +class TestFSAPIsWithInvalidPath(TestFSAPIs): """Test FS APIs with a funky, invalid path name.""" funky_suffix = INVALID_UNICODE_SUFFIX @@ -332,8 +327,9 @@ def expect_exact_path_match(cls): # =================================================================== -class TestNonFSAPIS(PsutilTestCase): +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") @@ -344,15 +340,14 @@ def test_proc_environ(self): # we use "è", which is part of the extended ASCII table # (unicode point <= 255). env = os.environ.copy() - funky_str = UNICODE_SUFFIX if PY3 else 'è' - env['FUNNY_ARG'] = funky_str + 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'], funky_str) + self.assertEqual(env['FUNNY_ARG'], self.funky_suffix) if __name__ == '__main__': diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py index 5623bb59b3..9de66fc251 100755 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels_github.py @@ -57,12 +57,14 @@ def rename_win27_wheels(): # See: https://github.com/giampaolo/psutil/issues/810 src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) + if os.path.exists(src): + print("rename: %s\n %s" % (src, dst)) + os.rename(src, dst) src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PSUTIL_VERSION dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PSUTIL_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) + if os.path.exists(src): + print("rename: %s\n %s" % (src, dst)) + os.rename(src, dst) def run(): diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index f760dd65c1..384fb329bf 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -13,8 +13,8 @@ SKIP_EXTS = ('.png', '.jpg', '.jpeg') -SKIP_FILES = ('.cirrus.yml', '.travis.yml', 'appveyor.yml') -SKIP_PREFIXES = ('.ci/', '.github/', 'scripts/internal/') +SKIP_FILES = ('appveyor.yml') +SKIP_PREFIXES = ('.ci/', '.github/') def sh(cmd): diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_wheels.py index 3c966173cb..c2b8d36b97 100755 --- a/scripts/internal/print_wheels.py +++ b/scripts/internal/print_wheels.py @@ -14,57 +14,83 @@ from psutil._common import bytes2human -def main(): - def is64bit(name): - return name.endswith(('x86_64.whl', 'amd64.whl')) +class Wheel: - groups = collections.defaultdict(list) - for path in glob.glob('dist/*.whl'): - name = os.path.basename(path) - plat = name.split('-')[-1] - pyimpl = name.split('-')[3] + def __init__(self, path): + self._path = path + self._name = os.path.basename(path) + + def __repr__(self): + return "" % ( + self.name, self.platform(), self.arch(), self.pyver()) + + __str__ = __repr__ + + @property + def name(self): + return self._name + + def platform(self): + plat = self.name.split('-')[-1] + pyimpl = self.name.split('-')[3] ispypy = 'pypy' in pyimpl if 'linux' in plat: if ispypy: - groups['pypy_on_linux'].append(name) + return 'pypy_on_linux' else: - groups['linux'].append(name) + return 'linux' elif 'win' in plat: if ispypy: - groups['pypy_on_windows'].append(name) + return 'pypy_on_windows' else: - groups['windows'].append(name) + return 'windows' elif 'macosx' in plat: if ispypy: - groups['pypy_on_macos'].append(name) + return 'pypy_on_macos' else: - groups['macos'].append(name) + return 'macos' else: - assert 0, name + raise ValueError("unknown platform %r" % self.name) + + def arch(self): + if self.name.endswith(('x86_64.whl', 'amd64.whl')): + return '64' + return '32' + + def pyver(self): + pyver = 'pypy' if self.name.split('-')[3].startswith('pypy') else 'py' + pyver += self.name.split('-')[2][2:] + return pyver + + def size(self): + return os.path.getsize(self._path) + + +def main(): + groups = collections.defaultdict(list) + for path in glob.glob('dist/*.whl'): + wheel = Wheel(path) + groups[wheel.platform()].append(wheel) tot_files = 0 tot_size = 0 templ = "%-54s %7s %7s %7s" - for platf, names in groups.items(): - ppn = "%s (total = %s)" % (platf.replace('_', ' '), len(names)) + for platf, wheels in groups.items(): + ppn = "%s (total = %s)" % (platf, len(wheels)) s = templ % (ppn, "size", "arch", "pyver") print_color('\n' + s, color=None, bold=True) - for name in sorted(names): + for wheel in sorted(wheels, key=lambda x: x.name): tot_files += 1 - path = os.path.join('dist', name) - size = os.path.getsize(path) - tot_size += size - arch = '64' if is64bit(name) else '32' - pyver = 'pypy' if name.split('-')[3].startswith('pypy') else 'py' - pyver += name.split('-')[2][2:] - s = templ % (name, bytes2human(size), arch, pyver) - if 'pypy' in pyver: + tot_size += wheel.size() + s = templ % (wheel.name, bytes2human(wheel.size()), wheel.arch(), + wheel.pyver()) + if 'pypy' in wheel.pyver(): print_color(s, color='violet') else: print_color(s, color='brown') print_color("\ntotals: files=%s, size=%s" % ( - tot_files, bytes2human(tot_size)), bold=1) + tot_files, bytes2human(tot_size)), bold=True) if __name__ == '__main__': diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index faa2aea8a6..933951a233 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -208,12 +208,6 @@ def recursive_rm(*patterns): safe_rmtree(os.path.join(root, dir)) -def test_setup(): - os.environ['PYTHONWARNINGS'] = 'all' - os.environ['PSUTIL_TESTING'] = '1' - os.environ['PSUTIL_DEBUG'] = '1' - - # =================================================================== # commands # =================================================================== @@ -368,7 +362,6 @@ def clean(): "*__pycache__", ".coverage", ".failed-tests.txt", - ".tox", ) safe_rmtree("build") safe_rmtree(".coverage") @@ -398,7 +391,6 @@ def lint(): def test(name=RUNNER_PY): """Run tests""" build() - test_setup() sh("%s %s" % (PYTHON, name)) @@ -406,7 +398,6 @@ def coverage(): """Run coverage tests.""" # Note: coverage options are controlled by .coveragerc file build() - test_setup() sh("%s -m coverage run %s" % (PYTHON, RUNNER_PY)) sh("%s -m coverage report" % PYTHON) sh("%s -m coverage html" % PYTHON) @@ -416,70 +407,60 @@ def coverage(): def test_process(): """Run process tests""" build() - test_setup() sh("%s psutil\\tests\\test_process.py" % PYTHON) def test_system(): """Run system tests""" build() - test_setup() sh("%s psutil\\tests\\test_system.py" % PYTHON) def test_platform(): """Run windows only tests""" build() - test_setup() sh("%s psutil\\tests\\test_windows.py" % PYTHON) def test_misc(): """Run misc tests""" build() - test_setup() sh("%s psutil\\tests\\test_misc.py" % PYTHON) def test_unicode(): """Run unicode tests""" build() - test_setup() sh("%s psutil\\tests\\test_unicode.py" % PYTHON) def test_connections(): """Run connections tests""" build() - test_setup() sh("%s psutil\\tests\\test_connections.py" % PYTHON) def test_contracts(): """Run contracts tests""" build() - test_setup() sh("%s psutil\\tests\\test_contracts.py" % PYTHON) def test_testutils(): """Run test utilities tests""" build() - test_setup() sh("%s psutil\\tests\\test_testutils.py" % PYTHON) def test_by_name(name): """Run test by name""" build() - test_setup() sh("%s -m unittest -v %s" % (PYTHON, name)) def test_failed(): """Re-run tests which failed on last run.""" build() - test_setup() sh("%s %s --last-failed" % (PYTHON, RUNNER_PY)) @@ -514,14 +495,12 @@ def bench_oneshot_2(): def print_access_denied(): """Print AD exceptions raised by all Process methods.""" build() - test_setup() sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) def print_api_speed(): """Benchmark all API calls.""" build() - test_setup() sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index b148642ba8..0000000000 --- a/tox.ini +++ /dev/null @@ -1,28 +0,0 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. -# To use it run "pip install tox" and then run "tox" from this -# directory. - -[tox] -envlist = py26, py27, py34, py35, py36, py37, py38, lint - -[testenv] -deps = - py26: ipaddress - py26: mock==1.0.1 - py26: unittest2 - py27: ipaddress - py27: mock - -setenv = - TOX = 1 - -commands = python psutil/tests/runner.py - -usedevelop = True - -[testenv:lint] -deps = flake8 -commands = flake8 -skip_install = True From a5eeaadb7fd07b855d7e9a98659ce102ab904ee4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Nov 2020 22:12:41 +0100 Subject: [PATCH 0663/1714] Add github action to automatically set new issue labels --- .github/workflows/issue_labels.yml | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/issue_labels.yml diff --git a/.github/workflows/issue_labels.yml b/.github/workflows/issue_labels.yml new file mode 100644 index 0000000000..8b94e49891 --- /dev/null +++ b/.github/workflows/issue_labels.yml @@ -0,0 +1,38 @@ +# Executed every time an issue or PR is opened. Inspects the title string +# and sets the appropriate labels. +# See: https://github.com/Naturalclar/issue-action + +name: "Set issue labels" +on: + issues: + types: [opened] + pull_request: + typed: [opened] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: Naturalclar/issue-action@v2.0.2 + with: + title-or-body: "title" + github-token: "${{ secrets.GITHUB_TOKEN }}" + parameters: '[ + {"keywords": ["aix"], "labels": ["aix"], "assignees": ["wiggin15"]}, + {"keywords": ["cygwin"], "labels": ["cygwin"], "assignees": [""]}, + {"keywords": ["freebsd"], "labels": ["freebsd"], "assignees": [""]}, + {"keywords": ["linux", "ubuntu", "redhat", "mint"], "labels": ["linux"], "assignees": [""]}, + {"keywords": ["macos", "osx"], "labels": ["osx"], "assignees": [""]}, + {"keywords": ["netbsd"], "labels": ["netbsd"], "assignees": [""]}, + {"keywords": ["openbsd"], "labels": ["openbsd"], "assignees": [""]}, + {"keywords": ["sunos", "solaris"], "labels": ["sunos"], "assignees": ["wiggin15"]}, + {"keywords": ["unix", "posix"], "labels": ["unix"], "assignees": [""]}, + {"keywords": ["windows"], "labels": ["windows"], "assignees": [""]}, + {"keywords": ["wsl"], "labels": ["wsl"], "assignees": [""]}, + {"keywords": ["bug", "bug"], "labels": ["bug"], "assignees": [""]}, + {"keywords": ["doc", "documentation"], "labels": ["doc"], "assignees": [""]}, + {"keywords": ["idea", "proposal", "api", "request", "feature"], "labels": ["api", "enhancement"], "assignees": [""]}, + {"keywords": ["performance", "speed"], "labels": ["performance"], "assignees": [""]}, + {"keywords": ["pypy", "pypy2", "pypy3"], "labels": ["pypy"], "assignees": [""]}, + {"keywords": ["test", "tests"], "labels": ["tests"], "assignees": [""]}, + {"keywords": ["wheel", "wheels"], "labels": ["wheels"], "assignees": [""]} +]' From e12d8853cd5d6f44f75ab1bab23c54f7f91b32af Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Nov 2020 22:53:59 +0100 Subject: [PATCH 0664/1714] refactor wheels download scripts --- .github/workflows/issue_labels.yml | 6 +++--- Makefile | 4 ++-- scripts/internal/download_wheels_appveyor.py | 18 +++++++----------- scripts/internal/download_wheels_github.py | 12 ++++-------- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/.github/workflows/issue_labels.yml b/.github/workflows/issue_labels.yml index 8b94e49891..a5a7cf0c99 100644 --- a/.github/workflows/issue_labels.yml +++ b/.github/workflows/issue_labels.yml @@ -20,13 +20,13 @@ jobs: {"keywords": ["aix"], "labels": ["aix"], "assignees": ["wiggin15"]}, {"keywords": ["cygwin"], "labels": ["cygwin"], "assignees": [""]}, {"keywords": ["freebsd"], "labels": ["freebsd"], "assignees": [""]}, - {"keywords": ["linux", "ubuntu", "redhat", "mint"], "labels": ["linux"], "assignees": [""]}, - {"keywords": ["macos", "osx"], "labels": ["osx"], "assignees": [""]}, + {"keywords": ["linux", "ubuntu", "redhat", "mint", "centos", "archlinux", "debian", "alpine", "gentoo", "fedora", "slackware", "suse", "opensuse", "manylinux"], "labels": ["linux"], "assignees": [""]}, + {"keywords": ["macos", "osx", "mojave", "sierra", "capitan", "yosemite"], "labels": ["osx"], "assignees": [""]}, {"keywords": ["netbsd"], "labels": ["netbsd"], "assignees": [""]}, {"keywords": ["openbsd"], "labels": ["openbsd"], "assignees": [""]}, {"keywords": ["sunos", "solaris"], "labels": ["sunos"], "assignees": ["wiggin15"]}, {"keywords": ["unix", "posix"], "labels": ["unix"], "assignees": [""]}, - {"keywords": ["windows"], "labels": ["windows"], "assignees": [""]}, + {"keywords": ["windows", "WinError", "win10", "win7", "win"], "labels": ["windows"], "assignees": [""]}, {"keywords": ["wsl"], "labels": ["wsl"], "assignees": [""]}, {"keywords": ["bug", "bug"], "labels": ["bug"], "assignees": [""]}, {"keywords": ["doc", "documentation"], "labels": ["doc"], "assignees": [""]}, diff --git a/Makefile b/Makefile index 8b4c992812..f03930dd16 100644 --- a/Makefile +++ b/Makefile @@ -212,10 +212,10 @@ install-git-hooks: ## Install GIT pre-commit hook. # =================================================================== download-wheels-github: ## Download latest wheels hosted on github. - $(PYTHON) scripts/internal/download_wheels_github.py --user=giampaolo --project=psutil --tokenfile=~/.github.token + $(PYTHON) scripts/internal/download_wheels_github.py --tokenfile=~/.github.token download-wheels-appveyor: ## Download latest wheels hosted on appveyor. - $(PYTHON) scripts/internal/download_wheels_appveyor.py --user giampaolo --project psutil + $(PYTHON) scripts/internal/download_wheels_appveyor.py print-wheels: ## Print downloaded wheels $(PYTHON) scripts/internal/print_wheels.py diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index 83ea55a1b9..bc6c9717e4 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -13,7 +13,6 @@ """ from __future__ import print_function -import argparse import concurrent.futures import os import requests @@ -24,6 +23,8 @@ from psutil._common import print_color +USER = "giampaolo" +PROJECT = "psutil" BASE_URL = 'https://ci.appveyor.com/api' PY_VERSIONS = ['2.7', '3.6', '3.7', '3.8', '3.9'] TIMEOUT = 30 @@ -43,10 +44,10 @@ def download_file(url): return local_fname -def get_file_urls(options): +def get_file_urls(): with requests.Session() as session: data = session.get( - BASE_URL + '/projects/' + options.user + '/' + options.project, + BASE_URL + '/projects/' + USER + '/' + PROJECT, timeout=TIMEOUT) data = data.json() @@ -78,8 +79,8 @@ def rename_win27_wheels(): os.rename(src, dst) -def run(options): - urls = get_file_urls(options) +def run(): + urls = get_file_urls() completed = 0 exc = None with concurrent.futures.ThreadPoolExecutor() as e: @@ -105,12 +106,7 @@ def run(options): def main(): - parser = argparse.ArgumentParser( - description='AppVeyor artifact downloader') - parser.add_argument('--user', required=True) - parser.add_argument('--project', required=True) - args = parser.parse_args() - run(args) + run() if __name__ == '__main__': diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py index 9de66fc251..5ce98b84e0 100755 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels_github.py @@ -26,10 +26,10 @@ from psutil.tests import safe_rmpath -USER = "" -PROJECT = "" -TOKEN = "" +USER = "giampaolo" +PROJECT = "psutil" OUTFILE = "wheels-github.zip" +TOKEN = "" def get_artifacts(): @@ -77,14 +77,10 @@ def run(): def main(): - global USER, PROJECT, TOKEN + global TOKEN parser = argparse.ArgumentParser(description='GitHub wheels downloader') - parser.add_argument('--user', required=True) - parser.add_argument('--project', required=True) parser.add_argument('--tokenfile', required=True) args = parser.parse_args() - USER = args.user - PROJECT = args.project with open(os.path.expanduser(args.tokenfile)) as f: TOKEN = f.read().strip() try: From b777a5177d0b8a492fc76320491b0791f25e17a1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 13 Dec 2020 23:33:58 +0100 Subject: [PATCH 0665/1714] add CONTRIBUTING.md --- .github/ISSUE_TEMPLATE/bug.md | 7 +++-- .github/ISSUE_TEMPLATE/config.yml | 5 +++ .github/PULL_REQUEST_TEMPLATE.md | 9 ++++++ CONTRIBUTING.md | 33 ++++++++++++++++++++ HISTORY.rst | 3 +- MANIFEST.in | 1 + README.rst | 7 ++++- docs/DEVGUIDE.rst | 51 ++++++++++++------------------- docs/index.rst | 3 +- 9 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 1cdba8105a..f90e13e158 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -5,11 +5,14 @@ title: "[OS] title" labels: 'bug' --- -**Platform** +## Platform + * { OS version } * { psutil version (print psutil.__version__) } * { python version } -**Bug description** +## Bug description + ... + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..39dc113f1a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://groups.google.com/g/psutil + about: Use this to ask for support diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..88f416f0fa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## Resume + +* OS: { OS name } +* Bug fix: { yes/no } +* Fixes: { comma-separated list of tickets fixed by the PR, if any } + +## Description + +... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..69c9dd9478 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +Contributing to psutil project +============================== + +Issues +------ + +* The issue tracker is for reporting problems or proposing enhancements related + to the **program code**. +* Please do not open issues **asking for support**. Instead, use the forum at: + https://groups.google.com/g/psutil. +* Before submitting a new issue, **search** if there are existing issues for + the same topic. +* **Be clear** in describing what the problem is and try to be accurate in + editing the default issue **template**. There is a bot which automatically + assigns **labels** based on issue's title and body format. Labels help + keeping the issues properly organized and searchable (by OS, issue type, etc.). +* To report a **security vulnerability**, use the + [Tidelift security contact](https://tidelift.com/security). + Tidelift will coordinate the fix and the disclosure of the reported problem. + +Pull Requests +------------- + +* The PR system is for fixing bugs or make enhancements related to the + **program code**. +* If you whish to implement a new feature or add support for a new platform it's + better to **discuss it first**, either on the issue tracker, the forum or via + private email. +* In order to get acquainted with the code base and tooling, take a look at the + **[Development Guide](https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst)**. +* If you can, remember to update + [HISTORY.rst](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) + and [CREDITS](https://github.com/giampaolo/psutil/blob/master/CREDITS) file. diff --git a/HISTORY.rst b/HISTORY.rst index 58626e10ac..99e85a5be8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,7 +12,8 @@ XXXX-XX-XX - 1872_: [Windows] added support for PyPy 2.7. - 1879_: provide pre-compiled wheels for Linux and macOS (yey!). - 1880_: get rid of Travis and Cirrus CI services (they are no longer free). - CI testing is now done by GitHub Actions on Linux, macOS and FreeBSD (yes). AppVeyor is still being used for Windows CI. + CI testing is now done by GitHub Actions on Linux, macOS and FreeBSD (yes). + AppVeyor is still being used for Windows CI. **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index 2bed87e78b..d651cd4e2d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include .coveragerc include .flake8 include .gitignore +include CONTRIBUTING.md include CREDITS include HISTORY.rst include INSTALL.rst diff --git a/README.rst b/README.rst index ec323253eb..837446a9e9 100644 --- a/README.rst +++ b/README.rst @@ -74,7 +74,7 @@ Install    Documentation    Download    - Forum    + Forum    Blog    Funding    What's new    @@ -135,6 +135,11 @@ None yet. add your avatar +Contributing +============ + +See `CONTRIBUTING.md`__ guidelines. + Example usages ============== diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 2ed8c42a12..cb9545bcfe 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -1,13 +1,13 @@ -Setup -===== +psutil development guide +======================== + +Build, setup and running tests +.............................. psutil makes extensive use of C extension modules, meaning a C compiler is required, see `install instructions `__. -Build, setup and running tests -=============================== - Once you have a compiler installed: .. code-block:: bash @@ -51,7 +51,7 @@ Once you have a compiler installed: make test -p C:\python35\python.exe # Windows Coding style -============ +------------ - python code strictly follows `PEP-8`_ styling guides and this is enforced by a commit GIT hook installed via ``make install-git-hooks`` which will reject @@ -59,7 +59,7 @@ Coding style - C code should follow `PEP-7`_ styling guides. Code organization -================= +----------------- .. code-block:: bash @@ -70,7 +70,7 @@ Code organization psutil/tests/test_{platform}.py # platform-specific test suite Adding a new API -================ +---------------- Typically, this is what you do: @@ -86,62 +86,49 @@ Typically, this is what you do: This usually means testing the return value of the new API against a system CLI tool. - update the doc in ``doc/index.py``. -- update ``HISTORY.rst``. +- update `HISTORY.rst`_ and `CREDITS`_ files. - make a pull request. Make a pull request -=================== +------------------- - fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") -- git clone the fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git``) +- git clone the fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git`` - create a branch:``git checkout -b new-feature`` - commit your changes: ``git commit -am 'add some feature'`` - push the branch: ``git push origin new-feature`` -- create a new PR by via GitHub web interface +- create a new PR via the GitHub web interface and sign-off your work (see + `CONTRIBUTING.md`_ guidelines) Continuous integration -====================== - -All of the services listed below are automatically run on each ``git push``. - -Unit tests ----------- +---------------------- -Tests are automatically run on every GIT push and PR on **Linux**, **macOS**, +Unit tests are automatically run on every ``git push`` on **Linux**, **macOS**, **Windows** and **FreeBSD** by using: - `Github Actions`_ (Linux, macOS, Windows) - `Appveyor`_ (Windows) -.. image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=linux%2C%20macos%2C%20freebsd +.. image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD :target: https://github.com/giampaolo/psutil/actions?query=workflow%3ACI -.. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=windows +.. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil OpenBSD, NetBSD, AIX and Solaris does not have continuos test integration. -Test coverage -------------- - -Test coverage is provided by `coveralls.io`_. - -.. image:: https://coveralls.io/repos/giampaolo/psutil/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/giampaolo/psutil?branch=master - :alt: Test coverage (coverall.io) - Documentation -============= +------------- - doc source code is written in a single file: `/docs/index.rst`_. - doc can be built with ``make setup-dev-env; cd docs; make html``. - public doc is hosted at https://psutil.readthedocs.io - .. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.yml .. _`Appveyor`: https://ci.appveyor.com/project/giampaolo/psuti .. _`coveralls.io`: https://coveralls.io/github/giampaolo/psuti .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS +.. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`doc/index.rst`: https://github.com/giampaolo/psutil/blob/master/doc/index.rst .. _`Github Actions`: https://github.com/giampaolo/psutil/actions .. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst diff --git a/docs/index.rst b/docs/index.rst index a99ab20622..ee5863c320 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,9 +10,10 @@ Quick links - `Home page `__ - `Install `_ -- `Blog `__ - `Forum `__ - `Download `__ +- `Blog `__ +- `Contributing `__ - `Development guide `_ - `What's new `__ From 1cbfd322895e345becee23c3c7f5c2f1f42f4c50 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 13 Dec 2020 23:34:41 +0100 Subject: [PATCH 0666/1714] fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 837446a9e9..8836d7b422 100644 --- a/README.rst +++ b/README.rst @@ -138,7 +138,7 @@ None yet. Contributing ============ -See `CONTRIBUTING.md`__ guidelines. +See `CONTRIBUTING.md `__ guidelines. Example usages ============== From 311a5bd98d42e569ff8bcc6da83ffbbc01054511 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 13 Dec 2020 23:35:33 +0100 Subject: [PATCH 0667/1714] fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8836d7b422..46cfc970b9 100644 --- a/README.rst +++ b/README.rst @@ -138,7 +138,7 @@ None yet. Contributing ============ -See `CONTRIBUTING.md `__ guidelines. +See `CONTRIBUTING.md `__ guidelines. Example usages ============== From 70b273265d0329868eac37f199261d7ef8fc1ad3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Dec 2020 00:33:42 +0100 Subject: [PATCH 0668/1714] update issue labeler Signed-off-by: Giampaolo Rodola --- .github/workflows/issue_labels.yml | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/issue_labels.yml b/.github/workflows/issue_labels.yml index a5a7cf0c99..9f4adb114f 100644 --- a/.github/workflows/issue_labels.yml +++ b/.github/workflows/issue_labels.yml @@ -20,19 +20,24 @@ jobs: {"keywords": ["aix"], "labels": ["aix"], "assignees": ["wiggin15"]}, {"keywords": ["cygwin"], "labels": ["cygwin"], "assignees": [""]}, {"keywords": ["freebsd"], "labels": ["freebsd"], "assignees": [""]}, - {"keywords": ["linux", "ubuntu", "redhat", "mint", "centos", "archlinux", "debian", "alpine", "gentoo", "fedora", "slackware", "suse", "opensuse", "manylinux"], "labels": ["linux"], "assignees": [""]}, - {"keywords": ["macos", "osx", "mojave", "sierra", "capitan", "yosemite"], "labels": ["osx"], "assignees": [""]}, + {"keywords": ["linux", "ubuntu", "redhat", "red hat", "mint", "centos", "archlinux", "debian", "alpine", "gentoo", "fedora", "slackware", "suse", "opensuse", "manylinux", "apt", "rpm", "yum", "RHEL", "kali", "/sys/class", "/sys/block", "/proc/net", "/proc/disk", "/proc/smaps", "/proc/vmstat"], "labels": ["linux"], "assignees": [""]}, + {"keywords": ["macos", "mac ", "osx", "os x", "mojave", "sierra", "capitan", "darwin", "yosemite", "catalina", "xcode", "dylib"], "labels": ["macos"], "assignees": [""]}, {"keywords": ["netbsd"], "labels": ["netbsd"], "assignees": [""]}, {"keywords": ["openbsd"], "labels": ["openbsd"], "assignees": [""]}, {"keywords": ["sunos", "solaris"], "labels": ["sunos"], "assignees": ["wiggin15"]}, - {"keywords": ["unix", "posix"], "labels": ["unix"], "assignees": [""]}, - {"keywords": ["windows", "WinError", "win10", "win7", "win"], "labels": ["windows"], "assignees": [""]}, + {"keywords": ["windows", "win32", "WinError", "WindowsError", "win10", "win7", "win", "mingw", "msys", "studio", "microsoft", "MSVC", "TCHAR", "WCHAR", "make.bat", ".bat", "appveyor", "handles", "CloseHandle", "GetLastError", "NtQuery", "OpenProcess", "TerminateProcess", "DLL", "System Idle Process"], "labels": ["windows"], "assignees": [""]}, {"keywords": ["wsl"], "labels": ["wsl"], "assignees": [""]}, - {"keywords": ["bug", "bug"], "labels": ["bug"], "assignees": [""]}, - {"keywords": ["doc", "documentation"], "labels": ["doc"], "assignees": [""]}, - {"keywords": ["idea", "proposal", "api", "request", "feature"], "labels": ["api", "enhancement"], "assignees": [""]}, - {"keywords": ["performance", "speed"], "labels": ["performance"], "assignees": [""]}, - {"keywords": ["pypy", "pypy2", "pypy3"], "labels": ["pypy"], "assignees": [""]}, - {"keywords": ["test", "tests"], "labels": ["tests"], "assignees": [""]}, - {"keywords": ["wheel", "wheels"], "labels": ["wheels"], "assignees": [""]} + {"keywords": ["enhancement"], "labels": ["enhancement"], "assignees": [""]}, + {"keywords": ["doc ", "document ", "documentation", "docfix", "readthedocs", "pythonhosted", "HISTORY", "README", "index.rst"], "labels": ["doc"], "assignees": [""]}, + {"keywords": ["idea", "proposal", "api", "feature"], "labels": ["api", "enhancement"], "assignees": [""]}, + {"keywords": ["performance", "speedup", "slow", "fast"], "labels": ["performance"], "assignees": [""]}, + {"keywords": ["pypy"], "labels": ["pypy"], "assignees": [""]}, + {"keywords": ["psposix", "_psutil_posix", "waitpid", "statvfs", "/dev/tty", "/dev/pts"], "labels": ["unix"], "assignees": [""]}, + {"keywords": ["memory leak", "leaks memory", "memleak", "mem leak"], "labels": ["memleak"], "assignees": [""]}, + {"keywords": ["example script", "examples script", "example dir", "scripts/"], "labels": ["scripts"], "assignees": [""]}, + {"keywords": ["test", "tests", "travis", "coverage", "travis", "cirrus", "appveyor", "continuous integration", "dev guide", "devguide", "sphinx", "unittest", "pytest"], "labels": ["tests"], "assignees": [""]}, + {"keywords": ["Makefile"], "labels": ["unix"], "assignees": [""]}, + {"keywords": ["segfault", "segmentation fault", "core dumped", "RuntimeError", "WinError", "WindowsError", "MemoryError", "OverflowError", "ZeroDivisionError", "SystemError", "MemoryError"], "labels": ["priority-high"], "assignees": [""]}, + {"keywords": ["fail", "can''t execute", "can''t install", "install error", "installation error", "crash", "critical"], "labels": ["bug"], "assignees": [""]}, + {"keywords": ["wheel"], "labels": ["wheels"], "assignees": [""]} ]' From 6ea12c3c0a93b1370a1db2670cda60eadbf618dd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Dec 2020 00:46:18 +0100 Subject: [PATCH 0669/1714] [macOS] EIO error occurring on cmdline() and environ (#1886) See: https://github.com/nicolargo/glances/issues/1769 Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/arch/osx/process_info.c | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 99e85a5be8..ea283782cb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,8 @@ XXXX-XX-XX - 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid access to memory location" on Python 3.9. - 1874_: [Solaris] wrong swap output given when encrypted column is present +- 1886_: [macOS] EIO error may be raised on cmdline() and environment(). Now + it gets translated into AccessDenied. 5.7.3 ===== diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 1dca7364b0..3732f91d4b 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -124,16 +124,24 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t argmax) { mib[2] = pid; if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { + if (psutil_pid_exists(pid) == 0) { + NoSuchProcess(""); + return 1; + } // In case of zombie process we'll get EINVAL. We translate it // to NSP and _psosx.py will translate it to ZP. - if ((errno == EINVAL) && (psutil_pid_exists(pid))) - NoSuchProcess("sysctl"); - else if (errno == EIO) { - psutil_debug("EIO, pid_exists() = %i\n", psutil_pid_exists(pid)); - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + if (errno == EINVAL) { + psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to NSP"); + NoSuchProcess("sysctl(KERN_PROCARGS2) -> EINVAL"); + return 1; + } + // There's nothing we can do other than raising AD. + if (errno == EIO) { + psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); + AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); + return 1; } - else - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); return 1; } return 0; From b6699b41e2c4873deac532e925561686bdb827d5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Dec 2020 20:27:47 +0100 Subject: [PATCH 0670/1714] [Windows] #1877: turn OpenProcess -> ERROR_SUCCESS into AD or NSP (#1887) Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/_psutil_bsd.c | 6 +++--- psutil/_psutil_common.c | 4 ++-- psutil/_psutil_osx.c | 3 ++- psutil/_psutil_windows.c | 31 +++++++++++------------------ psutil/arch/freebsd/specific.c | 2 +- psutil/arch/netbsd/specific.c | 6 +++--- psutil/arch/osx/process_info.c | 2 +- psutil/arch/windows/process_info.c | 7 ++----- psutil/arch/windows/process_utils.c | 28 +++++++++++++++++++++----- psutil/arch/windows/process_utils.h | 1 + psutil/tests/test_contracts.py | 8 ++++++-- scripts/procsmem.py | 7 ++++--- 13 files changed, 62 insertions(+), 45 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ea283782cb..d55a4ee3a8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,8 @@ XXXX-XX-XX - 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid access to memory location" on Python 3.9. - 1874_: [Solaris] wrong swap output given when encrypted column is present +- 1877_: [Windows] OpenProcess may fail with ERROR_SUCCESS. Turn it into + AccessDenied or NoSuchProcess depending on whether the PID is alive. - 1886_: [macOS] EIO error may be raised on cmdline() and environment(). Now it gets translated into AccessDenied. diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index c1b811c6a7..1956ff27fd 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -477,10 +477,10 @@ psutil_proc_environ(PyObject *self, PyObject *args) { kvm_close(kd); return py_retdict; case EPERM: - AccessDenied("kvm_getenvv"); + AccessDenied("kvm_getenvv -> EPERM"); break; case ESRCH: - NoSuchProcess("kvm_getenvv"); + NoSuchProcess("kvm_getenvv -> ESRCH"); break; #if defined(PSUTIL_FREEBSD) case ENOMEM: @@ -489,7 +489,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { // "sudo procstat -e ".) // Map the error condition to 'AccessDenied'. sprintf(errbuf, - "kvm_getenvv(pid=%ld, ki_uid=%d): errno=ENOMEM", + "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM", pid, p->ki_uid); AccessDenied(errbuf); break; diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index e72904deff..fae8d97014 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -105,7 +105,7 @@ NoSuchProcess(const char *syscall) { PyObject *exc; char msg[1024]; - sprintf(msg, "No such process (originated from %s)", syscall); + sprintf(msg, "assume no such process (originated from %s)", syscall); exc = PyObject_CallFunction(PyExc_OSError, "(is)", ESRCH, msg); PyErr_SetObject(PyExc_OSError, exc); Py_XDECREF(exc); @@ -122,7 +122,7 @@ AccessDenied(const char *syscall) { PyObject *exc; char msg[1024]; - sprintf(msg, "Access denied (originated from %s)", syscall); + sprintf(msg, "assume access denied (originated from %s)", syscall); exc = PyObject_CallFunction(PyExc_OSError, "(is)", EACCES, msg); PyErr_SetObject(PyExc_OSError, exc); Py_XDECREF(exc); diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index ee8d1c8393..33d466357d 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -99,7 +99,8 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) if (psutil_pid_exists(pid) == 0) NoSuchProcess("task_for_pid"); else if (psutil_is_zombie(pid) == 1) - PyErr_SetString(ZombieProcessError, "task_for_pid() failed"); + PyErr_SetString(ZombieProcessError, + "task_for_pid -> psutil_is_zombie -> 1"); else { psutil_debug( "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index b836d70885..711c1a793f 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -159,22 +159,15 @@ psutil_proc_kill(PyObject *self, PyObject *args) { return AccessDenied("automatically set for PID 0"); hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + hProcess = psutil_check_phandle(hProcess, pid, 0); if (hProcess == NULL) { - if (GetLastError() == ERROR_INVALID_PARAMETER) { - // see https://github.com/giampaolo/psutil/issues/24 - psutil_debug("OpenProcess -> ERROR_INVALID_PARAMETER turned " - "into NoSuchProcess"); - NoSuchProcess("OpenProcess"); - } - else { - PyErr_SetFromWindowsErr(0); - } return NULL; } if (! TerminateProcess(hProcess, SIGTERM)) { // ERROR_ACCESS_DENIED may happen if the process already died. See: // https://github.com/giampaolo/psutil/issues/1099 + // http://bugs.python.org/issue14252 if (GetLastError() != ERROR_ACCESS_DENIED) { PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); return NULL; @@ -280,7 +273,7 @@ psutil_proc_times(PyObject *self, PyObject *args) { if (GetLastError() == ERROR_ACCESS_DENIED) { // usually means the process has died so we throw a NoSuchProcess // here - NoSuchProcess("GetProcessTimes"); + NoSuchProcess("GetProcessTimes -> ERROR_ACCESS_DENIED"); } else { PyErr_SetFromWindowsErr(0); @@ -332,7 +325,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running"); + return NoSuchProcess("psutil_pid_is_running -> 0"); if (pid_return == -1) return NULL; @@ -356,7 +349,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running"); + return NoSuchProcess("psutil_pid_is_running -> 0"); if (pid_return == -1) return NULL; @@ -383,7 +376,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { return NULL; if (pid == 0) - return AccessDenied("forced for PID 0"); + return AccessDenied("automatically set for PID 0"); buffer = MALLOC_ZERO(bufferSize); if (! buffer) @@ -417,7 +410,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (! NT_SUCCESS(status)) { FREE(buffer); if (psutil_pid_is_running(pid) == 0) - NoSuchProcess("NtQuerySystemInformation"); + NoSuchProcess("psutil_pid_is_running -> 0"); else psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); return NULL; @@ -536,10 +529,10 @@ psutil_GetProcWsetInformation( if (!NT_SUCCESS(status)) { if (status == STATUS_ACCESS_DENIED) { - AccessDenied("NtQueryVirtualMemory"); + AccessDenied("NtQueryVirtualMemory -> STATUS_ACCESS_DENIED"); } else if (psutil_pid_is_running(pid) == 0) { - NoSuchProcess("psutil_pid_is_running"); + NoSuchProcess("psutil_pid_is_running -> 0"); } else { PyErr_Clear(); @@ -644,7 +637,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running"); + return NoSuchProcess("psutil_pid_is_running -> 0"); if (pid_return == -1) return NULL; @@ -703,13 +696,13 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (pid == 0) { // raise AD instead of returning 0 as procexp is able to // retrieve useful information somehow - AccessDenied("automatically set for PID 0"); + AccessDenied("forced for PID 0"); goto error; } pid_return = psutil_pid_is_running(pid); if (pid_return == 0) { - NoSuchProcess("psutil_pid_is_running"); + NoSuchProcess("psutil_pid_is_running -> 0"); goto error; } if (pid_return == -1) diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index e7517a4068..e498a7b330 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -262,7 +262,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (ret == -1) return NULL; else if (ret == 0) - return NoSuchProcess("psutil_pid_exists"); + return NoSuchProcess("psutil_pid_exists -> 0"); else strcpy(pathname, ""); } diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 9dab361839..2fbc241876 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -126,7 +126,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { int name[] = { CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) - NoSuchProcess(""); + NoSuchProcess("sysctl -> ENOENT"); else PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -142,7 +142,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { free(buf); if (len == -1) { if (errno == ENOENT) - NoSuchProcess("readlink (ENOENT)"); + NoSuchProcess("readlink -> ENOENT"); else PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -198,7 +198,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (ret == -1) return NULL; else if (ret == 0) - return NoSuchProcess("psutil_pid_exists"); + return NoSuchProcess("psutil_pid_exists -> 0"); else strcpy(pathname, ""); } diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 3732f91d4b..fb9f24ffac 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -125,7 +125,7 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t argmax) { if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { if (psutil_pid_exists(pid) == 0) { - NoSuchProcess(""); + NoSuchProcess("psutil_pid_exists -> 0"); return 1; } // In case of zombie process we'll get EINVAL. We translate it diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 36283addc4..0ce6297ac1 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -55,10 +55,7 @@ psutil_convert_winerr(ULONG err, char* syscall) { char fullmsg[8192]; if (err == ERROR_NOACCESS) { - sprintf( - fullmsg, - "(originated from %s -> ERROR_NOACCESS; converted to AccessDenied)", - syscall); + sprintf(fullmsg, "%s -> ERROR_NOACCESS", syscall); psutil_debug(fullmsg); AccessDenied(fullmsg); } @@ -434,7 +431,7 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { // https://github.com/giampaolo/psutil/issues/1501 if (status == STATUS_NOT_FOUND) { AccessDenied("NtQueryInformationProcess(ProcessBasicInformation) -> " - "STATUS_NOT_FOUND translated into PermissionError"); + "STATUS_NOT_FOUND"); goto error; } diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index f9d2f2f93d..acbda301a4 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -61,8 +61,10 @@ psutil_pid_in_pids(DWORD pid) { DWORD i; proclist = psutil_get_pids(&numberOfReturnedPIDs); - if (proclist == NULL) + if (proclist == NULL) { + psutil_debug("psutil_get_pids() failed"); return -1; + } for (i = 0; i < numberOfReturnedPIDs; i++) { if (proclist[i] == pid) { free(proclist); @@ -78,20 +80,36 @@ psutil_pid_in_pids(DWORD pid) { // does return the handle, else return NULL with Python exception set. // This is needed because OpenProcess API sucks. HANDLE -psutil_check_phandle(HANDLE hProcess, DWORD pid) { +psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { DWORD exitCode; if (hProcess == NULL) { if (GetLastError() == ERROR_INVALID_PARAMETER) { // Yeah, this is the actual error code in case of // "no such process". - NoSuchProcess("OpenProcess"); + NoSuchProcess("OpenProcess -> ERROR_INVALID_PARAMETER"); + return NULL; + } + if (GetLastError() == ERROR_SUCCESS) { + // Yeah, it's this bad. + // https://github.com/giampaolo/psutil/issues/1877 + if (psutil_pid_in_pids(pid) == 1) { + psutil_debug("OpenProcess -> ERROR_SUCCESS turned into AD"); + AccessDenied("OpenProcess -> ERROR_SUCCESS"); + } + else { + psutil_debug("OpenProcess -> ERROR_SUCCESS turned into NSP"); + NoSuchProcess("OpenProcess -> ERROR_SUCCESS"); + } return NULL; } PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); return NULL; } + if (check_exit_code == 0) + return hProcess; + if (GetExitCodeProcess(hProcess, &exitCode)) { // XXX - maybe STILL_ACTIVE is not fully reliable as per: // http://stackoverflow.com/questions/1591342/#comment47830782_1591379 @@ -137,7 +155,7 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { return NULL; } - hProcess = psutil_check_phandle(hProcess, pid); + hProcess = psutil_check_phandle(hProcess, pid, 1); return hProcess; } @@ -159,7 +177,7 @@ psutil_pid_is_running(DWORD pid) { if ((hProcess == NULL) && (GetLastError() == ERROR_ACCESS_DENIED)) return 1; - hProcess = psutil_check_phandle(hProcess, pid); + hProcess = psutil_check_phandle(hProcess, pid, 1); if (hProcess != NULL) { CloseHandle(hProcess); return 1; diff --git a/psutil/arch/windows/process_utils.h b/psutil/arch/windows/process_utils.h index a7171c5ca4..dca7c991a5 100644 --- a/psutil/arch/windows/process_utils.h +++ b/psutil/arch/windows/process_utils.h @@ -6,6 +6,7 @@ DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); +HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code); int psutil_pid_is_running(DWORD pid); int psutil_assert_pid_exists(DWORD pid, char *err); int psutil_assert_pid_not_exists(DWORD pid, char *err); diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 43eb9cbc19..dd6002e606 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -449,8 +449,12 @@ def exe(self, ret, info): # 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) + # XXX: may fail on MACOS + try: + assert os.access(ret, os.X_OK) + except AssertionError: + if os.path.exists(ret): + raise def pid(self, ret, info): self.assertIsInstance(ret, int) diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 259d79d42f..1074c4c20f 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -80,18 +80,19 @@ def main(): procs.append(p) procs.sort(key=lambda p: p._uss) - templ = "%-7s %-7s %-30s %7s %7s %7s %7s" - print(templ % ("PID", "User", "Cmdline", "USS", "PSS", "Swap", "RSS")) + templ = "%-7s %-7s %7s %7s %7s %7s %7s" + print(templ % ("PID", "User", "USS", "PSS", "Swap", "RSS", "Cmdline")) print("=" * 78) for p in procs[:86]: + cmd = " ".join(p._info["cmdline"])[:50] if p._info["cmdline"] else "" line = templ % ( p.pid, p._info["username"][:7] if p._info["username"] else "", - " ".join(p._info["cmdline"])[:30], convert_bytes(p._uss), convert_bytes(p._pss) if p._pss != "" else "", convert_bytes(p._swap) if p._swap != "" else "", convert_bytes(p._rss), + cmd, ) print(line) if ad_pids: From 6e288e0105b001ad37e67279d73578b3e1790877 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Dec 2020 11:54:18 -0800 Subject: [PATCH 0671/1714] fix #1875: username return ERROR_NONE_MAPPED --- HISTORY.rst | 4 +++- psutil/_psutil_windows.c | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index d55a4ee3a8..109547eba5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,7 +19,9 @@ XXXX-XX-XX - 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid access to memory location" on Python 3.9. -- 1874_: [Solaris] wrong swap output given when encrypted column is present +- 1874_: [Solaris] wrong swap output given when encrypted column is present. +- 1875_: [Windows] process username() may raise ERROR_NONE_MAPPED if the SID + has no corresponding account name. In this case AccessDenied is now raised. - 1877_: [Windows] OpenProcess may fail with ERROR_SUCCESS. Turn it into AccessDenied or NoSuchProcess depending on whether the PID is alive. - 1886_: [macOS] EIO error may be raised on cmdline() and environment(). Now diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 711c1a793f..a2154e923e 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -895,6 +895,19 @@ psutil_proc_username(PyObject *self, PyObject *args) { free(domainName); continue; } + else if (GetLastError() == ERROR_NONE_MAPPED) { + // From MS doc: + // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ + // nf-winbase-lookupaccountsida + // If the function cannot find an account name for the SID, + // GetLastError returns ERROR_NONE_MAPPED. This can occur if + // a network time-out prevents the function from finding the + // name. It also occurs for SIDs that have no corresponding + // account name, such as a logon SID that identifies a logon + // session. + AccessDenied("LookupAccountSidW -> ERROR_NONE_MAPPED"); + goto error; + } else { PyErr_SetFromOSErrnoWithSyscall("LookupAccountSidW"); goto error; From a6e5e3cf959d803e0dbabd9eade8959bab086ee3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Dec 2020 22:03:32 +0100 Subject: [PATCH 0672/1714] [Windows] giveup with AD for all NtWow64 API calls (query 64-bit process from 32-bit) (#1888) On Windows, cmdline(), cwd() and environ() use some complex logic to query a 64 bit process from a 32 bit one by using NtWow64* APIs which may randomly file with: [Error 0] The operation completed successfully [Error 998] Invalid access to memory location possibly others Since this happens randomly and it's unclear how to do this properly, this PR turns any error from NtWow64* APIs into AccessDenied. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/arch/windows/process_info.c | 46 ++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 109547eba5..09fd9a4b46 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX **Bug fixes** +- 1839_: [Windows] always raise AccessDenied when failing to query 64 processes + from 32 bit ones (NtWoW64 APIs). - 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid access to memory location" on Python 3.9. - 1874_: [Solaris] wrong swap output given when encrypted column is present. diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 0ce6297ac1..d44c4eb75e 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -77,6 +77,21 @@ psutil_convert_ntstatus_err(NTSTATUS status, char* syscall) { } +static void +psutil_giveup_with_ad(NTSTATUS status, char* syscall) { + ULONG err; + char fullmsg[8192]; + + if (NT_NTWIN32(status)) + err = WIN32_FROM_NTSTATUS(status); + else + err = RtlNtStatusToDosErrorNoTeb(status); + sprintf(fullmsg, "%s -> %lu (%s)", syscall, err, strerror(err)); + psutil_debug(fullmsg); + AccessDenied(fullmsg); +} + + /* * Get data from the process with the given pid. The data is returned * in the pdata output member as a nul terminated string which must be @@ -190,7 +205,18 @@ psutil_get_process_data(DWORD pid, } } else #else // #ifdef _WIN64 - /* 32 bit case. Check if the target is also 32 bit. */ + // 32 bit process. In here we may run into a lot of errors, e.g.: + // * [Error 0] The operation completed successfully + // (originated from NtWow64ReadVirtualMemory64) + // * [Error 998] Invalid access to memory location: + // (originated from NtWow64ReadVirtualMemory64) + // Refs: + // * https://github.com/giampaolo/psutil/issues/1839 + // * https://github.com/giampaolo/psutil/pull/1866 + // Since the following code is quite hackish and fails unpredictably, + // in case of any error from NtWow64* APIs we raise AccessDenied. + + // 32 bit case. Check if the target is also 32 bit. if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || !IsWow64Process(hProcess, &theyAreWow64)) { PyErr_SetFromOSErrnoWithSyscall("IsWow64Process"); @@ -231,9 +257,14 @@ psutil_get_process_data(DWORD pid, sizeof(pbi64), NULL); if (!NT_SUCCESS(status)) { + /* psutil_convert_ntstatus_err( status, "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); + */ + psutil_giveup_with_ad( + status, + "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); goto error; } @@ -245,8 +276,13 @@ psutil_get_process_data(DWORD pid, sizeof(peb64), NULL); if (!NT_SUCCESS(status)) { + /* psutil_convert_ntstatus_err( status, "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)"); + */ + psutil_giveup_with_ad( + status, + "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)"); goto error; } @@ -258,8 +294,13 @@ psutil_get_process_data(DWORD pid, sizeof(procParameters64), NULL); if (!NT_SUCCESS(status)) { + /* psutil_convert_ntstatus_err( status, "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)"); + */ + psutil_giveup_with_ad( + status, + "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)"); goto error; } @@ -366,7 +407,8 @@ psutil_get_process_data(DWORD pid, size, NULL); if (!NT_SUCCESS(status)) { - psutil_convert_ntstatus_err(status, "NtWow64ReadVirtualMemory64"); + // psutil_convert_ntstatus_err(status, "NtWow64ReadVirtualMemory64"); + psutil_giveup_with_ad(status, "NtWow64ReadVirtualMemory64"); goto error; } } else From cc624f17789a1df4d03f632a943b8442081781d7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Dec 2020 22:10:03 +0100 Subject: [PATCH 0673/1714] workaround for environ() on CI + MACOS Signed-off-by: Giampaolo Rodola --- psutil/tests/test_process.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index af35a58c7d..0ec2ead8e2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1414,7 +1414,16 @@ def test_weird_environ(self): assert p.is_running() # Wait for process to exec or exit. self.assertEqual(sproc.stderr.read(), b"") - self.assertEqual(p.environ(), {"A": "1", "C": "3"}) + 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) From 9820f2e365c4930c88ddb39d9d24e02d1b1461fc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 16 Dec 2020 22:48:50 +0100 Subject: [PATCH 0674/1714] update issue/pr templates Signed-off-by: Giampaolo Rodola --- .github/ISSUE_TEMPLATE/bug.md | 19 +++++++++++-------- .github/ISSUE_TEMPLATE/enhancement.md | 11 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 12 ++++++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index f90e13e158..edff6d4375 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -5,14 +5,17 @@ title: "[OS] title" labels: 'bug' --- -## Platform +## Summary -* { OS version } -* { psutil version (print psutil.__version__) } -* { python version } +* OS: { type-or-version } +* Architecture: { 64bit, 32bit, ARM, PowerPC, s390 } +* Psutil version: { pip3 show psutil } +* Python version: { python3 -V } +* Type: { core, doc, performance, scripts, tests, wheels, newapi } +## Description -## Bug description - -... - +{{{ + A clear explanation of the bug, including traceback nessage (if any). Please read the contributing guidelines before submit: + https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md +}}} diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 2111c572f3..0a1cbb77ef 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -6,3 +6,14 @@ title: "[OS] title" --- +## Summary + +* OS: { type-or-version } +* Type: { core, doc, performance, scripts, tests, wheels, newapi } + +## Description + +{{{ + A clear explanation of your proposal. Please read the contributing guidelines before submit: + https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md +}}} diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 88f416f0fa..6b7fdee79d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,13 @@ -## Resume +## Summary -* OS: { OS name } +* OS: { type-or-version } * Bug fix: { yes/no } -* Fixes: { comma-separated list of tickets fixed by the PR, if any } +* Type: { core, doc, performance, scripts, tests, wheels, newapi } +* Fixes: { comma-separated list of issues fixed by this PR, if any } ## Description -... +{{{ + A clear explanation of your bugfix or enhancement. Please read the contributing guidelines before submit: + https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md +}}} From 31304fecf3b95bd4f27380c3066a8f95b515a00a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 16 Dec 2020 23:18:03 +0100 Subject: [PATCH 0675/1714] update INSTALL instructions Signed-off-by: Giampaolo Rodola --- .github/ISSUE_TEMPLATE/bug.md | 2 +- INSTALL.rst | 71 ++++++++++++++++++----------------- README.rst | 3 +- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index edff6d4375..096033db8d 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -16,6 +16,6 @@ labels: 'bug' ## Description {{{ - A clear explanation of the bug, including traceback nessage (if any). Please read the contributing guidelines before submit: + A clear explanation of the bug, including traceback message (if any). Please read the contributing guidelines before submit: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md }}} diff --git a/INSTALL.rst b/INSTALL.rst index a4f2bf11a2..9516a18334 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,17 +1,21 @@ +Install psutil +============== + Linux, Windows, macOS (wheels) -============================== +------------------------------ psutil makes extensive use of C extension modules, meaning a C compiler is -required. -For these 3 platforms though, pre-compiled cPython wheels are provided on each -psutil release, so all you have to do is this:: +required to build the sources. +Pre-compiled cPython wheels are provided on each release though, so on +**Linux**, **Windows** and **macOS** all you have to do is:: pip3 install psutil -If wheels are not available and you whish to install from sources, keep reading. +If wheels are not available for your platform or architecture, or you whish to +install psutil from sources, keep reading. Linux (install from sources) -============================ +---------------------------- Ubuntu / Debian:: @@ -24,18 +28,18 @@ RedHat / CentOS:: pip3 install --user psutil --no-binary :all: Windows (install from sources) -============================== +------------------------------ -In order to compile psutil on Windows you'll need **Visual Studio**. -Here's a couple of guides describing how to do it: `1 `__ -and `2 `__. And then:: +In order to compile psutil on Windows you'll need **Visual Studio** compiler +(**MinGW** is not supported). +Here's a couple of guides describing how to do it: `link `__ +and `link `__. +Once VS is installed do:: pip3 install --user psutil --no-binary :all: -Note that MinGW compiler is not supported. - FreeBSD -======= +------- :: @@ -43,7 +47,7 @@ FreeBSD python3 -m pip3 install psutil OpenBSD -======= +------- :: @@ -52,7 +56,7 @@ OpenBSD python3 -m pip install psutil NetBSD -====== +------ :: @@ -61,10 +65,10 @@ NetBSD pkgin install python3 gcc python3 -m pip install psutil -Solaris -======= +Sun Solaris +----------- -If ``cc`` compiler is not installed create a symlink to ``gcc``:: +If ``cc`` compiler is not installed create a symbolic link to ``gcc``:: sudo ln -s /usr/bin/gcc /usr/local/bin/cc @@ -73,22 +77,14 @@ Install:: pkg install gcc python3 -m pip install psutil -Testing installation -==================== - -:: - - python3 -m psutil.tests -Dev Guide -========= - -`Link `__. +Troubleshooting +=============== Install pip -=========== +----------- -Pip is shipped by default with Python 2.7.9+ and 3.4+. +**Pip** is shipped by default with Python 2.7.9+ and 3.4+. If you don't have it you can install with wget:: wget https://bootstrap.pypa.io/get-pip.py -O - | python3 @@ -98,18 +94,23 @@ If you don't have it you can install with wget:: python3 < <(curl -s https://bootstrap.pypa.io/get-pip.py) On Windows, `download pip `__, open -cmd.exe and install it:: +cmd.exe and install it with:: C:\Python27\python.exe get-pip.py -Permission issues (UNIX) -======================== +Permission errors (UNIX) +------------------------ If you bump into permission errors you have two options. -Install psutil for your user only:: + +* Install psutil for your user only (non system-wide):: + + pip3 install --user psutil + +* Install it as root user (system-wide): pip3 install --user psutil -...or prepend ``sudo`` and install it at system level:: +* Prepend ``sudo`` (system-wide):: sudo pip3 install psutil diff --git a/README.rst b/README.rst index 46cfc970b9..a9334da283 100644 --- a/README.rst +++ b/README.rst @@ -99,7 +99,8 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -...both **32-bit** and **64-bit** architectures. Supported Python versions are **2.6**, **2.7** and **3.4+**, `PyPy `__ 2.7 and 3.X. +Supported Python versions are **2.6**, **2.7**, **3.4+** and +`PyPy `__. Funding ======= From ef3946da95beee5bcbb298dd103341c90e63892f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 10:10:35 +0100 Subject: [PATCH 0676/1714] update doc Signed-off-by: Giampaolo Rodola --- INSTALL.rst | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 9516a18334..88cef33f36 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -9,8 +9,9 @@ required to build the sources. Pre-compiled cPython wheels are provided on each release though, so on **Linux**, **Windows** and **macOS** all you have to do is:: - pip3 install psutil + pip3 install --user psutil +This (``--user``) will install psutil as a limited user (not system-wide). If wheels are not available for your platform or architecture, or you whish to install psutil from sources, keep reading. @@ -20,12 +21,12 @@ Linux (install from sources) Ubuntu / Debian:: sudo apt-get install gcc python3-dev - pip3 install --user psutil --no-binary :all: + pip3 install --user --no-binary :all: psutil RedHat / CentOS:: sudo yum install gcc python3-devel - pip3 install --user psutil --no-binary :all: + pip3 install --user --no-binary :all: psutil Windows (install from sources) ------------------------------ @@ -36,7 +37,7 @@ Here's a couple of guides describing how to do it: `link `__. Once VS is installed do:: - pip3 install --user psutil --no-binary :all: + pip3 install --user --no-binary :all: psutil FreeBSD ------- @@ -77,15 +78,14 @@ Install:: pkg install gcc python3 -m pip install psutil - Troubleshooting =============== Install pip ----------- -**Pip** is shipped by default with Python 2.7.9+ and 3.4+. -If you don't have it you can install with wget:: +Pip is shipped by default with Python 2.7.9+ and 3.4+. +If you don't have pip you can install with wget:: wget https://bootstrap.pypa.io/get-pip.py -O - | python3 @@ -98,19 +98,18 @@ cmd.exe and install it with:: C:\Python27\python.exe get-pip.py -Permission errors (UNIX) ------------------------- - -If you bump into permission errors you have two options. +"pip not found" +--------------- -* Install psutil for your user only (non system-wide):: +Sometimes pip is installed but it's not available in your ``PATH`` +(``pip command not found`` or similar). Try this:: - pip3 install --user psutil - -* Install it as root user (system-wide): + python3 -m pip install psutil - pip3 install --user psutil +Permission errors (UNIX) +------------------------ -* Prepend ``sudo`` (system-wide):: +If you want to install psutil system-wide and you bump into permission errors +either run as root or prepend ``sudo``:: sudo pip3 install psutil From c39fef497f0c2c963a9db03ef80cdb156f64a425 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 18:44:31 +0100 Subject: [PATCH 0677/1714] add issue/PR bot Signed-off-by: Giampaolo Rodola --- .github/workflows/issue_bot.yml | 22 +++ .github/workflows/issue_labels.yml | 43 ---- MANIFEST.in | 1 + scripts/internal/github_issue_bot.py | 286 +++++++++++++++++++++++++++ 4 files changed, 309 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/issue_bot.yml delete mode 100644 .github/workflows/issue_labels.yml create mode 100644 scripts/internal/github_issue_bot.py diff --git a/.github/workflows/issue_bot.yml b/.github/workflows/issue_bot.yml new file mode 100644 index 0000000000..ee0c12cc02 --- /dev/null +++ b/.github/workflows/issue_bot.yml @@ -0,0 +1,22 @@ +name: Issue/PR bot +on: + issues: + types: [opened] + pull_request: + typed: [opened] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Run + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ISSUE_URL: ${{ github.event.issue.url }} + run: | + PYTHONUNBUFFERED=1 python -m pip install --upgrade pip PyGithub + PYTHONUNBUFFERED=1 python scripts/internal/issue_bot.py diff --git a/.github/workflows/issue_labels.yml b/.github/workflows/issue_labels.yml deleted file mode 100644 index 9f4adb114f..0000000000 --- a/.github/workflows/issue_labels.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Executed every time an issue or PR is opened. Inspects the title string -# and sets the appropriate labels. -# See: https://github.com/Naturalclar/issue-action - -name: "Set issue labels" -on: - issues: - types: [opened] - pull_request: - typed: [opened] -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: Naturalclar/issue-action@v2.0.2 - with: - title-or-body: "title" - github-token: "${{ secrets.GITHUB_TOKEN }}" - parameters: '[ - {"keywords": ["aix"], "labels": ["aix"], "assignees": ["wiggin15"]}, - {"keywords": ["cygwin"], "labels": ["cygwin"], "assignees": [""]}, - {"keywords": ["freebsd"], "labels": ["freebsd"], "assignees": [""]}, - {"keywords": ["linux", "ubuntu", "redhat", "red hat", "mint", "centos", "archlinux", "debian", "alpine", "gentoo", "fedora", "slackware", "suse", "opensuse", "manylinux", "apt", "rpm", "yum", "RHEL", "kali", "/sys/class", "/sys/block", "/proc/net", "/proc/disk", "/proc/smaps", "/proc/vmstat"], "labels": ["linux"], "assignees": [""]}, - {"keywords": ["macos", "mac ", "osx", "os x", "mojave", "sierra", "capitan", "darwin", "yosemite", "catalina", "xcode", "dylib"], "labels": ["macos"], "assignees": [""]}, - {"keywords": ["netbsd"], "labels": ["netbsd"], "assignees": [""]}, - {"keywords": ["openbsd"], "labels": ["openbsd"], "assignees": [""]}, - {"keywords": ["sunos", "solaris"], "labels": ["sunos"], "assignees": ["wiggin15"]}, - {"keywords": ["windows", "win32", "WinError", "WindowsError", "win10", "win7", "win", "mingw", "msys", "studio", "microsoft", "MSVC", "TCHAR", "WCHAR", "make.bat", ".bat", "appveyor", "handles", "CloseHandle", "GetLastError", "NtQuery", "OpenProcess", "TerminateProcess", "DLL", "System Idle Process"], "labels": ["windows"], "assignees": [""]}, - {"keywords": ["wsl"], "labels": ["wsl"], "assignees": [""]}, - {"keywords": ["enhancement"], "labels": ["enhancement"], "assignees": [""]}, - {"keywords": ["doc ", "document ", "documentation", "docfix", "readthedocs", "pythonhosted", "HISTORY", "README", "index.rst"], "labels": ["doc"], "assignees": [""]}, - {"keywords": ["idea", "proposal", "api", "feature"], "labels": ["api", "enhancement"], "assignees": [""]}, - {"keywords": ["performance", "speedup", "slow", "fast"], "labels": ["performance"], "assignees": [""]}, - {"keywords": ["pypy"], "labels": ["pypy"], "assignees": [""]}, - {"keywords": ["psposix", "_psutil_posix", "waitpid", "statvfs", "/dev/tty", "/dev/pts"], "labels": ["unix"], "assignees": [""]}, - {"keywords": ["memory leak", "leaks memory", "memleak", "mem leak"], "labels": ["memleak"], "assignees": [""]}, - {"keywords": ["example script", "examples script", "example dir", "scripts/"], "labels": ["scripts"], "assignees": [""]}, - {"keywords": ["test", "tests", "travis", "coverage", "travis", "cirrus", "appveyor", "continuous integration", "dev guide", "devguide", "sphinx", "unittest", "pytest"], "labels": ["tests"], "assignees": [""]}, - {"keywords": ["Makefile"], "labels": ["unix"], "assignees": [""]}, - {"keywords": ["segfault", "segmentation fault", "core dumped", "RuntimeError", "WinError", "WindowsError", "MemoryError", "OverflowError", "ZeroDivisionError", "SystemError", "MemoryError"], "labels": ["priority-high"], "assignees": [""]}, - {"keywords": ["fail", "can''t execute", "can''t install", "install error", "installation error", "crash", "critical"], "labels": ["bug"], "assignees": [""]}, - {"keywords": ["wheel"], "labels": ["wheels"], "assignees": [""]} -]' diff --git a/MANIFEST.in b/MANIFEST.in index d651cd4e2d..df1128335b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -123,6 +123,7 @@ include scripts/internal/download_wheels_github.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py +include scripts/internal/github_issue_bot.py include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py diff --git a/scripts/internal/github_issue_bot.py b/scripts/internal/github_issue_bot.py new file mode 100644 index 0000000000..ce66a2937e --- /dev/null +++ b/scripts/internal/github_issue_bot.py @@ -0,0 +1,286 @@ +#!/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. + +import os +import re +import textwrap + +from github import Github + + +ROOT_DIR = os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..')) +SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') + + +# --- constants + + +LABELS_MAP = { + # platforms + "linux": [ + "linux", "ubuntu", "redhat", "mint", "centos", "red hat", "archlinux", + "debian", "alpine", "gentoo", "fedora", "slackware", "suse", "RHEL", + "opensuse", "manylinux", "apt ", "apt-", "rpm", "yum", "kali", + "/sys/class", "/proc/net", "/proc/disk", "/proc/smaps", + "/proc/vmstat", + ], + "windows": [ + "windows", "win32", "WinError", "WindowsError", "win10", "win7", + "win ", "mingw", "msys", "studio", "microsoft", "make.bat", + "CloseHandle", "GetLastError", "NtQuery", "DLL", "MSVC", "TCHAR", + "WCHAR", ".bat", "OpenProcess", "TerminateProcess", "appveyor", + "windows error", "NtWow64", "NTSTATUS", "Visual Studio", + ], + "macos": [ + "macos", "mac ", "osx", "os x", "mojave", "sierra", "capitan", + "yosemite", "catalina", "mojave", "big sur", "xcode", "darwin", + "dylib", + ], + "aix": ["aix"], + "cygwin": ["cygwin"], + "freebsd": ["freebsd"], + "netbsd": ["netbsd"], + "openbsd": ["openbsd"], + "sunos": ["sunos", "solaris"], + "wsl": ["wsl"], + "unix": [ + "psposix", "_psutil_posix", "waitpid", "statvfs", "/dev/tty", + "/dev/pts", + ], + "pypy": ["pypy"], + # types + "enhancement": ["enhancement"], + "memleak": ["memory leak", "leaks memory", "memleak", "mem leak"], + "api": ["idea", "proposal", "api", "feature"], + "performance": ["performance", "speedup", "speed up", "slow", "fast"], + "wheels": ["wheel", "wheels"], + "scripts": [ + "example script", "examples script", "example dir", "scripts/", + ], + # bug + "bug": [ + "fail", "can't execute", "can't install", "cannot execute", + "cannot install", "install error", "crash", "critical", + ], + # doc + "doc": [ + "doc ", "document ", "documentation", "readthedocs", "pythonhosted", + "HISTORY", "README", "dev guide", "devguide", "sphinx", "docfix", + "index.rst", + ], + # tests + "tests": [ + " test ", "tests", "travis", "coverage", "cirrus", "appveyor", + "continuous integration", "unittest", "pytest", "unit test", + ], + # critical errors + "priority-high": [ + "WinError", "WindowsError", "RuntimeError", "ZeroDivisionError", + "SystemError", "MemoryError", "core dumped", + "segfault", "segmentation fault", + ], +} + +LABELS_MAP['scripts'].extend( + [x for x in os.listdir(SCRIPTS_DIR) if x.endswith('.py')]) + +OS_LABELS = [ + "linux", "windows", "macos", "freebsd", "openbsd", "netbsd", "openbsd", + "bsd", "sunos", "unix", "wsl", "aix", "cygwin", +] + +ILLOGICAL_PAIRS = [ + ('bug', 'enhancement'), + ('doc', 'tests'), + ('scripts', 'doc'), + ('scripts', 'tests'), + ('bsd', 'freebsd'), + ('bsd', 'openbsd'), + ('bsd', 'netbsd'), +] + +# --- replies + +REPLY_MISSING_PYTHON_HEADERS = """\ +It looks like you're missing `Python.h` headers. This usually means you have \ +to install them first, then retry psutil installation. +Please read \ +[INSTALL](https://github.com/giampaolo/psutil/blob/master/INSTALL.rst) \ +instructions for your platform. \ +This is an auto-generated response based on the text you submitted. \ +If this was a mistake or you think there's a bug with psutil installation \ +process, please add a comment to reopen this issue. +""" + + +# --- utils + + +def is_pr(issue): + return 'PullRequest' in issue.__module__ + + +def is_issue(issue): + return not is_pr(issue) + + +def is_new(issue): + return issue.comments == 0 + + +def has_label(issue, label): + assigned = [x.name for x in issue.labels] + return label in assigned + + +def has_os_label(issue): + labels = set([x.name for x in issue.labels]) + for label in OS_LABELS: + if label in labels: + return True + return False + + +def get_repo(): + repo = os.environ['GITHUB_REPOSITORY'] + token = os.environ['GITHUB_TOKEN'] + return Github(token).get_repo(repo) + + +def get_issue_event(): + url = os.environ.get('GITHUB_ISSUE_URL') + if url: # issue + num = int(url.split('/')[-1]) + else: # PR + url = os.environ['GITHUB_REF'] + num = int(url.split('/')[-2]) + repo = get_repo() + return repo.get_issue(number=num) + + +# --- actions + + +def log(msg): + print(">>> %s <<<" % msg) + + +def add_label(issue, label): + def should_add(issue, label): + if has_label(issue, label): + log("issue %r already has label %r" % (issue, label)) + return False + + for left, right in ILLOGICAL_PAIRS: + if label == left and has_label(issue, right): + log("issue %r already has label %r" % (issue, label)) + return False + + return not has_label(issue, label) + + if not should_add(issue, label): + return + + type_ = "PR:" if is_pr(issue) else "issue:" + assigned = ', '.join([x.name for x in issue.labels]) + log(textwrap.dedent("""\ + %-10s %s: %s + assigned: %s + new: %s""" % ( + type_, + issue.number, + issue.title, + assigned, + label, + ))) + issue.add_to_labels(label) + + +def _guess_labels_from_text(issue, text): + for label, keywords in LABELS_MAP.items(): + for keyword in keywords: + if keyword.lower() in text.lower(): + yield (label, keyword) + + +def add_labels_from_text(issue, text): + for label, keyword in _guess_labels_from_text(issue, text): + add_label(issue, label) + + +def add_labels_from_body(issue, text): + # add os label + print(repr(text)) + r = re.search(r"\* OS:.*?\n", text) + if r: + add_labels_from_text(issue, r.group(0)) + # add bug/enhancement label + r = re.search(r"\* Bug fix:.*?\n", text) + if is_pr(issue) and \ + r is not None and \ + not has_label(issue, "bug") and \ + not has_label(issue, "enhancement"): + s = r.group(0).lower() + if 'yes' in s: + add_label(issue, 'bug') + else: + add_label(issue, 'enhancement') + # add type labels + r = re.search(r"\* Type:.*?\n", text) + if r: + s = r.group(0).lower() + if 'doc' in s: + add_label(issue, 'doc') + if 'performance' in s: + add_label(issue, 'performance') + if 'scripts' in s: + add_label(issue, 'scripts') + if 'tests' in s: + add_label(issue, 'tests') + if 'wheels' in s: + add_label(issue, 'wheels') + if 'newapi' in s: + add_label(issue, 'api') + + +# --- run + + +def process_both(issue): + add_labels_from_text(issue, issue.title) + add_labels_from_body(issue, issue.body) + + +def process_issue(issue): + def has_text(text): + return text in issue.title.lower() or text in issue.body.lower() + + if has_text("missing python.h") or \ + has_text("python.h: no such file or directory") or \ + "#include\n^~~~" in issue.body.replace(' ', '') or \ + "#include\r\n^~~~" in issue.body.replace(' ', ''): + issue.create_comment(REPLY_MISSING_PYTHON_HEADERS) + issue.edit(state='closed') + return + + +def process_pr(pr): + pass + + +def main(): + log("Running issue/PR bot.") + issue = get_issue_event() + process_both(issue) + if is_issue(issue): + process_issue(issue) + else: + process_pr(issue) + + +if __name__ == '__main__': + main() From a89215e1853223983194cc03830604688a479022 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 18:57:58 +0100 Subject: [PATCH 0678/1714] fix wrong path Signed-off-by: Giampaolo Rodola --- .github/workflows/issue_bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue_bot.yml b/.github/workflows/issue_bot.yml index ee0c12cc02..223c0b6e52 100644 --- a/.github/workflows/issue_bot.yml +++ b/.github/workflows/issue_bot.yml @@ -19,4 +19,4 @@ jobs: GITHUB_ISSUE_URL: ${{ github.event.issue.url }} run: | PYTHONUNBUFFERED=1 python -m pip install --upgrade pip PyGithub - PYTHONUNBUFFERED=1 python scripts/internal/issue_bot.py + PYTHONUNBUFFERED=1 python scripts/internal/github_issue_bot.py From 6bdde37049ba9fae31ccf83620468f77a6b9f396 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 18:59:13 +0100 Subject: [PATCH 0679/1714] [macOS, UNIX] prefer _SC_PAGESIZE over (partially) deprecated getpagesize() (#1891) Add a reusable `psutil_getpagesize()` utility common to all UNIXes. Related to #1885 (`getpagesize()` is deprecated on recent macOS, POSIX.1-2001 and possibly other UNIXes). The problem emerged on macOS but `getpagesize()` is also used in FreeBSD, NetBSD, OpenBSD and AIX, so it makes sense to do this in one place only, similarly to Windows which also provide a `psutil_getpagesize()` utility. Follow cPython's `mmapmodule.c` and `resourcemodule.c` lead and rely on `sysconf(_SC_PAGESIZE)` instead, but leave `getpagesize()` in place as last resort/attempt for systems where it's not deprecated and/or they still legitimately rely on it. Also provide a python wrapper so we can test the return value of this C function against Python's stdlib modules. Signed-off-by: Giampaolo Rodola --- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 5 +--- psutil/_pslinux.py | 2 +- psutil/_psosx.py | 2 +- psutil/_pssunos.py | 2 +- psutil/_psutil_aix.c | 4 +-- psutil/_psutil_bsd.c | 2 +- psutil/_psutil_osx.c | 12 +++------ psutil/_psutil_posix.c | 47 ++++++++++++++++++++++++++++++++++ psutil/_psutil_posix.h | 1 + psutil/arch/freebsd/specific.c | 10 ++------ psutil/arch/netbsd/specific.c | 4 +-- psutil/arch/openbsd/specific.c | 2 +- psutil/tests/test_bsd.py | 11 ++++---- psutil/tests/test_osx.py | 10 +++----- psutil/tests/test_posix.py | 16 ++++++++++++ psutil/tests/test_system.py | 8 ------ 17 files changed, 91 insertions(+), 49 deletions(-) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 57d5378ab2..7160ecd63a 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -46,7 +46,7 @@ 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 = { diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index f0a0c14449..764463e980 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -98,10 +98,7 @@ 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") diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ff13b5eb29..068c3f03bf 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -81,7 +81,7 @@ # 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 diff --git a/psutil/_psosx.py b/psutil/_psosx.py index be22b48d52..c7770d6591 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -35,7 +35,7 @@ # ===================================================================== -PAGESIZE = os.sysconf("SC_PAGE_SIZE") +PAGESIZE = cext_posix.getpagesize() AF_LINK = cext_posix.AF_LINK TCP_STATUSES = { diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 2af15dfa40..5618bd4460 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -43,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 diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 791e831e54..a80bed70d7 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -876,7 +876,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { static PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { int rc; - int pagesize = getpagesize(); + long pagesize = psutil_getpagesize(); perfstat_memory_total_t memory; rc = perfstat_memory_total( @@ -902,7 +902,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { static PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { int rc; - int pagesize = getpagesize(); + long pagesize = psutil_getpagesize(); perfstat_memory_total_t memory; rc = perfstat_memory_total( diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 1956ff27fd..15b646e33c 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -189,7 +189,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { long memstack; int oncpu; kinfo_proc kp; - long pagesize = sysconf(_SC_PAGESIZE); + long pagesize = psutil_getpagesize(); char str[1000]; PyObject *py_name; PyObject *py_ppid; diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 33d466357d..13d0bb6856 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -441,7 +441,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { mach_vm_size_t size = 0; mach_msg_type_number_t info_count = VM_REGION_TOP_INFO_COUNT; kern_return_t kr; - vm_size_t page_size; + long pagesize = psutil_getpagesize(); mach_vm_address_t addr = MACH_VM_MIN_ADDRESS; mach_port_t task = MACH_PORT_NULL; vm_region_top_info_data_t info; @@ -505,11 +505,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { } mach_port_deallocate(mach_task_self(), task); - - if (host_page_size(mach_host_self(), &page_size) != KERN_SUCCESS) - page_size = PAGE_SIZE; - - return Py_BuildValue("K", private_pages * page_size); + return Py_BuildValue("K", private_pages * pagesize); } @@ -525,7 +521,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { uint64_t total; size_t len = sizeof(total); vm_statistics_data_t vm; - int pagesize = getpagesize(); + long pagesize = psutil_getpagesize(); // physical mem mib[0] = CTL_HW; mib[1] = HW_MEMSIZE; @@ -565,7 +561,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { size_t size; struct xsw_usage totals; vm_statistics_data_t vmstat; - int pagesize = getpagesize(); + long pagesize = psutil_getpagesize(); mib[0] = CTL_VM; mib[1] = VM_SWAPUSAGE; diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index b41c6dec95..305cec76d1 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -15,6 +15,7 @@ #include #include #include +#include #ifdef PSUTIL_SUNOS10 #include "arch/solaris/v10/ifaddrs.h" @@ -50,6 +51,38 @@ #include "_psutil_common.h" + +// ==================================================================== +// --- Utils +// ==================================================================== + + +/* + * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: + * + * > In SUSv2 the getpagesize() call is labeled LEGACY, and in POSIX.1-2001 + * > it has been dropped. + * > Portable applications should employ sysconf(_SC_PAGESIZE) instead + * > of getpagesize(). + * > Most systems allow the synonym _SC_PAGE_SIZE for _SC_PAGESIZE. + * > Whether getpagesize() is present as a Linux system call depends on the + * > architecture. + */ +long +psutil_getpagesize(void) { +#ifdef _SC_PAGESIZE + // recommended POSIX + return sysconf(_SC_PAGESIZE); +#elif _SC_PAGE_SIZE + // alias + return sysconf(_SC_PAGE_SIZE); +#else + // legacy + return (long) getpagesize(); +#endif +} + + /* * Check if PID exists. Return values: * 1: exists @@ -123,6 +156,18 @@ psutil_raise_for_pid(long pid, char *syscall) { } +// ==================================================================== +// --- Python wrappers +// ==================================================================== + + +// Exposed so we can test it against Python's stdlib. +static PyObject * +psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { + return Py_BuildValue("l", psutil_getpagesize()); +} + + /* * Given a PID return process priority as a Python integer. */ @@ -629,6 +674,8 @@ static PyMethodDef mod_methods[] = { "Retrieve NIC MTU"}, {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS, "Return True if the NIC is running."}, + {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS, + "Return memory page size."}, #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, "Return NIC stats."}, diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h index 59b9e53238..5a37e48b15 100644 --- a/psutil/_psutil_posix.h +++ b/psutil/_psutil_posix.h @@ -4,5 +4,6 @@ * found in the LICENSE file. */ +long psutil_getpagesize(void); int psutil_pid_exists(pid_t pid); void psutil_raise_for_pid(pid_t pid, char *msg); diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index e498a7b330..2776de8ce2 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -404,7 +404,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { size_t size = sizeof(total); struct vmtotal vm; int mib[] = {CTL_VM, VM_METER}; - long pagesize = getpagesize(); + long pagesize = psutil_getpagesize(); #if __FreeBSD_version > 702101 long buffers; #else @@ -465,7 +465,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { struct kvm_swap kvmsw[1]; unsigned int swapin, swapout, nodein, nodeout; size_t size = sizeof(unsigned int); - int pagesize; + long pagesize = psutil_getpagesize(); kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open failed"); if (kd == NULL) { @@ -499,12 +499,6 @@ psutil_swap_mem(PyObject *self, PyObject *args) { "sysctlbyname('vm.stats.vm.v_vnodeout)'"); } - pagesize = getpagesize(); - if (pagesize <= 0) { - PyErr_SetString(PyExc_ValueError, "invalid getpagesize()"); - return NULL; - } - return Py_BuildValue( "(KKKII)", (unsigned long long)kvmsw[0].ksw_total * pagesize, // total diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 2fbc241876..4e286e5ee9 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -445,7 +445,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { size_t size; struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; - long pagesize = getpagesize(); + long pagesize = psutil_getpagesize(); size = sizeof(uv); if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { @@ -472,6 +472,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { uint64_t swap_total, swap_free; struct swapent *swdev; int nswap, i; + long pagesize = psutil_getpagesize(); nswap = swapctl(SWAP_NSWAP, 0, 0); if (nswap == 0) { @@ -505,7 +506,6 @@ psutil_swap_mem(PyObject *self, PyObject *args) { size_t size = sizeof(total); struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; - long pagesize = getpagesize(); size = sizeof(uv); if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { PyErr_SetFromErrno(PyExc_OSError); diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/specific.c index aa4568f592..cdd4f62314 100644 --- a/psutil/arch/openbsd/specific.c +++ b/psutil/arch/openbsd/specific.c @@ -297,7 +297,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { struct uvmexp uvmexp; struct bcachestats bcstats; struct vmtotal vmdata; - long pagesize = getpagesize(); + long pagesize = psutil_getpagesize(); size = sizeof(total_physmem); if (sysctl(physmem_mib, 2, &total_physmem, &size, NULL, 0) < 0) { diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 06a65087a4..b0bff87f65 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -32,12 +32,13 @@ if BSD: - PAGESIZE = os.sysconf("SC_PAGE_SIZE") - if os.getuid() == 0: # muse requires root privileges - MUSE_AVAILABLE = which('muse') - else: - MUSE_AVAILABLE = False + from psutil._psutil_posix import getpagesize + + PAGESIZE = getpagesize() + # muse requires root privileges + MUSE_AVAILABLE = True if os.getuid() == 0 and which('muse') else False else: + PAGESIZE = None MUSE_AVAILABLE = False diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 097bff1084..348976f88d 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -4,9 +4,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""MACOS specific tests.""" +"""macOS specific tests.""" -import os import re import time @@ -23,9 +22,6 @@ 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. @@ -40,13 +36,15 @@ def sysctl(cmdline): def vm_stat(field): """Wrapper around 'vm_stat' cmdline utility.""" + from psutil._psutil_posix import getpagesize + 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 + return int(re.search(r'\d+', line).group(0)) * getpagesize() # http://code.activestate.com/recipes/578019/ diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index d9513eedb6..acb6aa200a 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -35,6 +35,12 @@ from psutil.tests import unittest from psutil.tests import which +if POSIX: + import mmap + import resource + + from psutil._psutil_posix import getpagesize + def ps(fmt, pid=None): """ @@ -404,6 +410,16 @@ def df(device): 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/psutil/tests/test_system.py b/psutil/tests/test_system.py index 4da4e784d5..90ecff945a 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -218,14 +218,6 @@ def test_users(self): else: psutil.Process(user.pid) - @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_test(self): # test for psutil.test() function stdout = sys.stdout From 1618797864a88ce514e91954dbe402a3ce746298 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 19:22:02 +0100 Subject: [PATCH 0680/1714] update HISTORY Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 09fd9a4b46..f49a0662a2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -28,6 +28,7 @@ XXXX-XX-XX AccessDenied or NoSuchProcess depending on whether the PID is alive. - 1886_: [macOS] EIO error may be raised on cmdline() and environment(). Now it gets translated into AccessDenied. +- 1891_: [macOS] get rid of deprecated getpagesize(). 5.7.3 ===== From 9e7fd6a6110f07d5afd17f284bdc3f81d25daec1 Mon Sep 17 00:00:00 2001 From: Tim Schlueter <2689079+modelrockettier@users.noreply.github.com> Date: Thu, 17 Dec 2020 10:31:35 -0800 Subject: [PATCH 0681/1714] Don't duplicate coretemp sensor readings (#1822) Fixes giampaolo/psutil#1708 --- psutil/_pslinux.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 068c3f03bf..be43f08b1c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1250,10 +1250,20 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/971 # https://github.com/nicolargo/glances/issues/1060 basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*')) - basenames.extend(glob.glob( - '/sys/devices/platform/coretemp.*/hwmon/hwmon*/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' From ddd201268c8d07c7f27c9b87485eba376828c337 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 19:34:20 +0100 Subject: [PATCH 0682/1714] give CREDITS to @modelrockettier for #1822 Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index 168f05d467..9ab16c6e13 100644 --- a/CREDITS +++ b/CREDITS @@ -721,3 +721,7 @@ I: 1837, 1838 N: Vincent A. Arcila W: https://github.com/jandrovins I: 1620, 1727 + +N: Tim Schlueter +W: https://github.com/modelrockettier +I: 1822 diff --git a/HISTORY.rst b/HISTORY.rst index f49a0662a2..38a2353636 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX **Bug fixes** +- 1708_: [Linux] get rid of sensors_temperatures() duplicates. (patch by Tim + Schlueter). - 1839_: [Windows] always raise AccessDenied when failing to query 64 processes from 32 bit ones (NtWoW64 APIs). - 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid From 57d1f5a905329b8b5f502f09dbd1434adf3efe46 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:20:06 +0100 Subject: [PATCH 0683/1714] add py/c linter to CI run Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c93ad4808c..6a4a019f9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,9 @@ jobs: PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py + python {project}/psutil/tests/test_memleaks.py && + find . -type f -iname "*.py" | xargs python -m flake8 --config=.flake8 && + find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: @@ -49,7 +51,7 @@ jobs: - name: Run tests run: | - pip install cibuildwheel + pip install cibuildwheel flake8 cibuildwheel . - name: Create wheels From f3ff964df2950d27db30af9b3c3dbce4278f6e99 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:25:08 +0100 Subject: [PATCH 0684/1714] try to fix yml cmdline Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a4a019f9a..79d99261d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,11 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: + python -m pip install flake8 && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/runner.py && + python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py && + python {project}/psutil/tests/test_memleaks.py && find . -type f -iname "*.py" | xargs python -m flake8 --config=.flake8 && find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py CIBW_TEST_EXTRAS: test @@ -51,7 +52,6 @@ jobs: - name: Run tests run: | - pip install cibuildwheel flake8 cibuildwheel . - name: Create wheels From 29d98151c6ab28a2a4ed5ee9deb801bbc5b28287 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:27:01 +0100 Subject: [PATCH 0685/1714] revert previous change Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79d99261d6..bc9976f465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,13 +25,10 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - python -m pip install flake8 && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py && - find . -type f -iname "*.py" | xargs python -m flake8 --config=.flake8 && - find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py + python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: From a9462074f2fcc547f3c0c88af394a9258ad254ab Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:27:28 +0100 Subject: [PATCH 0686/1714] revert previous change Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79d99261d6..bc9976f465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,13 +25,10 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - python -m pip install flake8 && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py && - find . -type f -iname "*.py" | xargs python -m flake8 --config=.flake8 && - find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py + python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: From 60475128881f3c89608dfc45e62bf3872c32a427 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:30:31 +0100 Subject: [PATCH 0687/1714] revert previous change Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc9976f465..c93ad4808c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,9 @@ jobs: env: CIBW_TEST_COMMAND: PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/runner.py && + python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py + python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: @@ -49,6 +49,7 @@ jobs: - name: Run tests run: | + pip install cibuildwheel cibuildwheel . - name: Create wheels From e53ebd3b0a2f192151d71d4264ae1a53e5abc491 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:40:49 +0100 Subject: [PATCH 0688/1714] progress Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 157 +++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc9976f465..a2466bd2a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,86 +9,99 @@ # To skip certain builds see: # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip -name: CI +name: Build on: [push] jobs: - linux-macos-win: - name: ${{ matrix.os }} - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - # os: [ubuntu-latest, macos-latest, windows-latest] - os: [ubuntu-latest, macos-latest] - include: - - {name: Linux, python: '3.9', os: ubuntu-latest} - env: - CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py - CIBW_TEST_EXTRAS: test - CIBW_SKIP: cp35-* pp* - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ github.token }} +# linux-macos-win: +# name: ${{ matrix.os }} +# runs-on: ${{ matrix.os }} +# timeout-minutes: 30 +# strategy: +# fail-fast: false +# matrix: +# # os: [ubuntu-latest, macos-latest, windows-latest] +# os: [ubuntu-latest, macos-latest] +# include: +# - {name: Linux, python: '3.9', os: ubuntu-latest} +# env: +# CIBW_TEST_COMMAND: +# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +# python {project}/psutil/tests/runner.py && +# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +# python {project}/psutil/tests/test_memleaks.py +# CIBW_TEST_EXTRAS: test +# CIBW_SKIP: cp35-* pp* +# steps: +# - name: Cancel previous runs +# uses: styfle/cancel-workflow-action@0.6.0 +# with: +# access_token: ${{ github.token }} - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.9 +# - uses: actions/checkout@v2 +# - uses: actions/setup-python@v2 +# with: +# python-version: 3.9 - # - name: (Windows) install Visual C++ for Python 2.7 - # if: matrix.os == 'windows-latest' - # run: | - # choco install vcpython27 -f -y +# # - name: (Windows) install Visual C++ for Python 2.7 +# # if: matrix.os == 'windows-latest' +# # run: | +# # choco install vcpython27 -f -y - - name: Run tests - run: | - cibuildwheel . +# - name: Run tests +# run: | +# cibuildwheel . - - name: Create wheels - uses: actions/upload-artifact@v2 - with: - name: wheels - path: wheelhouse +# - name: Create wheels +# uses: actions/upload-artifact@v2 +# with: +# name: wheels +# path: wheelhouse - - name: Print hashes - if: matrix.os == 'ubuntu-latest' - run: | - make generate-manifest - python setup.py sdist - mv dist/psutil*.tar.gz wheelhouse/ - python scripts/internal/print_hashes.py wheelhouse/ +# - name: Print hashes +# if: matrix.os == 'ubuntu-latest' +# run: | +# make generate-manifest +# python setup.py sdist +# mv dist/psutil*.tar.gz wheelhouse/ +# python scripts/internal/print_hashes.py wheelhouse/ - freebsd: - runs-on: macos-latest - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ github.token }} +# freebsd: +# runs-on: macos-latest +# steps: +# - name: Cancel previous runs +# uses: styfle/cancel-workflow-action@0.6.0 +# with: +# access_token: ${{ github.token }} - - uses: actions/checkout@v2 +# - uses: actions/checkout@v2 - - name: Run tests - id: test - uses: vmactions/freebsd-vm@v0.0.8 +# - name: Run tests +# id: test +# uses: vmactions/freebsd-vm@v0.0.8 +# with: +# usesh: true +# prepare: pkg install -y gcc python3 +# run: | +# set +e +# export \ +# PYTHONWARNINGS=always \ +# PYTHONUNBUFFERED=1 \ +# PSUTIL_TESTING=1 \ +# PSUTIL_DEBUG=1 +# python3 -m pip install --user setuptools +# python3 setup.py install +# python3 psutil/tests/runner.py +# python3 psutil/tests/test_memleaks.py + + linters: + name: "Linters" + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.8] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: - usesh: true - prepare: pkg install -y gcc python3 - run: | - set +e - export \ - PYTHONWARNINGS=always \ - PYTHONUNBUFFERED=1 \ - PSUTIL_TESTING=1 \ - PSUTIL_DEBUG=1 - python3 -m pip install --user setuptools - python3 setup.py install - python3 psutil/tests/runner.py - python3 psutil/tests/test_memleaks.py + python-version: ${{ matrix.python-version }} + - run: make lint From ff0ed127ca75d40ec3057b202b4ce49c00f78267 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:40:49 +0100 Subject: [PATCH 0689/1714] progress Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 157 +++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc9976f465..a2466bd2a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,86 +9,99 @@ # To skip certain builds see: # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip -name: CI +name: Build on: [push] jobs: - linux-macos-win: - name: ${{ matrix.os }} - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - # os: [ubuntu-latest, macos-latest, windows-latest] - os: [ubuntu-latest, macos-latest] - include: - - {name: Linux, python: '3.9', os: ubuntu-latest} - env: - CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py - CIBW_TEST_EXTRAS: test - CIBW_SKIP: cp35-* pp* - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ github.token }} +# linux-macos-win: +# name: ${{ matrix.os }} +# runs-on: ${{ matrix.os }} +# timeout-minutes: 30 +# strategy: +# fail-fast: false +# matrix: +# # os: [ubuntu-latest, macos-latest, windows-latest] +# os: [ubuntu-latest, macos-latest] +# include: +# - {name: Linux, python: '3.9', os: ubuntu-latest} +# env: +# CIBW_TEST_COMMAND: +# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +# python {project}/psutil/tests/runner.py && +# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +# python {project}/psutil/tests/test_memleaks.py +# CIBW_TEST_EXTRAS: test +# CIBW_SKIP: cp35-* pp* +# steps: +# - name: Cancel previous runs +# uses: styfle/cancel-workflow-action@0.6.0 +# with: +# access_token: ${{ github.token }} - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.9 +# - uses: actions/checkout@v2 +# - uses: actions/setup-python@v2 +# with: +# python-version: 3.9 - # - name: (Windows) install Visual C++ for Python 2.7 - # if: matrix.os == 'windows-latest' - # run: | - # choco install vcpython27 -f -y +# # - name: (Windows) install Visual C++ for Python 2.7 +# # if: matrix.os == 'windows-latest' +# # run: | +# # choco install vcpython27 -f -y - - name: Run tests - run: | - cibuildwheel . +# - name: Run tests +# run: | +# cibuildwheel . - - name: Create wheels - uses: actions/upload-artifact@v2 - with: - name: wheels - path: wheelhouse +# - name: Create wheels +# uses: actions/upload-artifact@v2 +# with: +# name: wheels +# path: wheelhouse - - name: Print hashes - if: matrix.os == 'ubuntu-latest' - run: | - make generate-manifest - python setup.py sdist - mv dist/psutil*.tar.gz wheelhouse/ - python scripts/internal/print_hashes.py wheelhouse/ +# - name: Print hashes +# if: matrix.os == 'ubuntu-latest' +# run: | +# make generate-manifest +# python setup.py sdist +# mv dist/psutil*.tar.gz wheelhouse/ +# python scripts/internal/print_hashes.py wheelhouse/ - freebsd: - runs-on: macos-latest - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ github.token }} +# freebsd: +# runs-on: macos-latest +# steps: +# - name: Cancel previous runs +# uses: styfle/cancel-workflow-action@0.6.0 +# with: +# access_token: ${{ github.token }} - - uses: actions/checkout@v2 +# - uses: actions/checkout@v2 - - name: Run tests - id: test - uses: vmactions/freebsd-vm@v0.0.8 +# - name: Run tests +# id: test +# uses: vmactions/freebsd-vm@v0.0.8 +# with: +# usesh: true +# prepare: pkg install -y gcc python3 +# run: | +# set +e +# export \ +# PYTHONWARNINGS=always \ +# PYTHONUNBUFFERED=1 \ +# PSUTIL_TESTING=1 \ +# PSUTIL_DEBUG=1 +# python3 -m pip install --user setuptools +# python3 setup.py install +# python3 psutil/tests/runner.py +# python3 psutil/tests/test_memleaks.py + + linters: + name: "Linters" + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.8] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: - usesh: true - prepare: pkg install -y gcc python3 - run: | - set +e - export \ - PYTHONWARNINGS=always \ - PYTHONUNBUFFERED=1 \ - PSUTIL_TESTING=1 \ - PSUTIL_DEBUG=1 - python3 -m pip install --user setuptools - python3 setup.py install - python3 psutil/tests/runner.py - python3 psutil/tests/test_memleaks.py + python-version: ${{ matrix.python-version }} + - run: make lint From a5c6c531e34d833808b34f115d881aff9d7197e7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:42:11 +0100 Subject: [PATCH 0690/1714] progress Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2466bd2a3..5559f23e99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,4 +104,6 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - run: make lint + - run: | + python -m pip install flake8 + make lint From d279eb314486d6d3e20ce55810eb8c31ddc36306 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:44:55 +0100 Subject: [PATCH 0691/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a05322060c..aaf7de3006 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,7 +99,7 @@ cache: # - might want to upload the content of dist/*.whl to a public wheelhouse skip_commits: - message: skip-ci + message: skip-appveyor # run build only if one of the following files is modified on commit only_commits: From c72db4c92efbe94579f6133d79fd17fba1855394 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:46:22 +0100 Subject: [PATCH 0692/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5559f23e99..06e55495d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,5 +105,4 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: | - python -m pip install flake8 make lint From 59e53045ea7f1d6d5477185e741e35fa21be1530 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:46:54 +0100 Subject: [PATCH 0693/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06e55495d7..a2466bd2a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,5 +104,4 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - run: | - make lint + - run: make lint From c46216e9195ca07885eade682ee4112bc4395a2b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:47:28 +0100 Subject: [PATCH 0694/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 94 ++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2466bd2a3..917333c9f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,58 +12,58 @@ name: Build on: [push] jobs: -# linux-macos-win: -# name: ${{ matrix.os }} -# runs-on: ${{ matrix.os }} -# timeout-minutes: 30 -# strategy: -# fail-fast: false -# matrix: -# # os: [ubuntu-latest, macos-latest, windows-latest] -# os: [ubuntu-latest, macos-latest] -# include: -# - {name: Linux, python: '3.9', os: ubuntu-latest} -# env: -# CIBW_TEST_COMMAND: -# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 -# python {project}/psutil/tests/runner.py && -# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 -# python {project}/psutil/tests/test_memleaks.py -# CIBW_TEST_EXTRAS: test -# CIBW_SKIP: cp35-* pp* -# steps: -# - name: Cancel previous runs -# uses: styfle/cancel-workflow-action@0.6.0 -# with: -# access_token: ${{ github.token }} + linux-macos-win: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + # os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] + include: + - {name: Linux, python: '3.9', os: ubuntu-latest} + env: + CIBW_TEST_COMMAND: + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 + python {project}/psutil/tests/runner.py && + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 + python {project}/psutil/tests/test_memleaks.py + CIBW_TEST_EXTRAS: test + CIBW_SKIP: cp35-* pp* + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ github.token }} -# - uses: actions/checkout@v2 -# - uses: actions/setup-python@v2 -# with: -# python-version: 3.9 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.9 -# # - name: (Windows) install Visual C++ for Python 2.7 -# # if: matrix.os == 'windows-latest' -# # run: | -# # choco install vcpython27 -f -y + # - name: (Windows) install Visual C++ for Python 2.7 + # if: matrix.os == 'windows-latest' + # run: | + # choco install vcpython27 -f -y -# - name: Run tests -# run: | -# cibuildwheel . + - name: Run tests + run: | + cibuildwheel . -# - name: Create wheels -# uses: actions/upload-artifact@v2 -# with: -# name: wheels -# path: wheelhouse + - name: Create wheels + uses: actions/upload-artifact@v2 + with: + name: wheels + path: wheelhouse -# - name: Print hashes -# if: matrix.os == 'ubuntu-latest' -# run: | -# make generate-manifest -# python setup.py sdist -# mv dist/psutil*.tar.gz wheelhouse/ -# python scripts/internal/print_hashes.py wheelhouse/ + - name: Print hashes + if: matrix.os == 'ubuntu-latest' + run: | + make generate-manifest + python setup.py sdist + mv dist/psutil*.tar.gz wheelhouse/ + python scripts/internal/print_hashes.py wheelhouse/ # freebsd: # runs-on: macos-latest From 957f6cfd95598e5ae87bd73f4ab0ac900ec49081 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:48:40 +0100 Subject: [PATCH 0695/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 98 ++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 917333c9f7..5559f23e99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,58 +12,58 @@ name: Build on: [push] jobs: - linux-macos-win: - name: ${{ matrix.os }} - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - # os: [ubuntu-latest, macos-latest, windows-latest] - os: [ubuntu-latest, macos-latest] - include: - - {name: Linux, python: '3.9', os: ubuntu-latest} - env: - CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py - CIBW_TEST_EXTRAS: test - CIBW_SKIP: cp35-* pp* - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ github.token }} +# linux-macos-win: +# name: ${{ matrix.os }} +# runs-on: ${{ matrix.os }} +# timeout-minutes: 30 +# strategy: +# fail-fast: false +# matrix: +# # os: [ubuntu-latest, macos-latest, windows-latest] +# os: [ubuntu-latest, macos-latest] +# include: +# - {name: Linux, python: '3.9', os: ubuntu-latest} +# env: +# CIBW_TEST_COMMAND: +# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +# python {project}/psutil/tests/runner.py && +# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +# python {project}/psutil/tests/test_memleaks.py +# CIBW_TEST_EXTRAS: test +# CIBW_SKIP: cp35-* pp* +# steps: +# - name: Cancel previous runs +# uses: styfle/cancel-workflow-action@0.6.0 +# with: +# access_token: ${{ github.token }} - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.9 +# - uses: actions/checkout@v2 +# - uses: actions/setup-python@v2 +# with: +# python-version: 3.9 - # - name: (Windows) install Visual C++ for Python 2.7 - # if: matrix.os == 'windows-latest' - # run: | - # choco install vcpython27 -f -y +# # - name: (Windows) install Visual C++ for Python 2.7 +# # if: matrix.os == 'windows-latest' +# # run: | +# # choco install vcpython27 -f -y - - name: Run tests - run: | - cibuildwheel . +# - name: Run tests +# run: | +# cibuildwheel . - - name: Create wheels - uses: actions/upload-artifact@v2 - with: - name: wheels - path: wheelhouse +# - name: Create wheels +# uses: actions/upload-artifact@v2 +# with: +# name: wheels +# path: wheelhouse - - name: Print hashes - if: matrix.os == 'ubuntu-latest' - run: | - make generate-manifest - python setup.py sdist - mv dist/psutil*.tar.gz wheelhouse/ - python scripts/internal/print_hashes.py wheelhouse/ +# - name: Print hashes +# if: matrix.os == 'ubuntu-latest' +# run: | +# make generate-manifest +# python setup.py sdist +# mv dist/psutil*.tar.gz wheelhouse/ +# python scripts/internal/print_hashes.py wheelhouse/ # freebsd: # runs-on: macos-latest @@ -104,4 +104,6 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - run: make lint + - run: | + python -m pip install flake8 + make lint From 0defe1b905273e16548563098d5c81b328d65117 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:52:00 +0100 Subject: [PATCH 0696/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5559f23e99..fb42147545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,21 @@ # To skip certain builds see: # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip -name: Build on: [push] jobs: + linters: + runs-on: ubuntu-latest + strategy: + matrix: + # in this example, there is a newer version already installed, 3.7.7, so the older version will be downloaded + python-version: [2.7, 3.8] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install flake8 + # linux-macos-win: # name: ${{ matrix.os }} # runs-on: ${{ matrix.os }} @@ -93,17 +105,3 @@ jobs: # python3 psutil/tests/runner.py # python3 psutil/tests/test_memleaks.py - linters: - name: "Linters" - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [2.7, 3.8] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - run: | - python -m pip install flake8 - make lint From 28be3a1bb0448ce34b5daaf98df8189dc3174713 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:52:51 +0100 Subject: [PATCH 0697/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb42147545..8c3c56706e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ on: [push] jobs: linters: + name: "Linters" runs-on: ubuntu-latest strategy: matrix: @@ -22,7 +23,9 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - run: python -m pip install flake8 + - run: | + python -m pip install flake8 + make lint # linux-macos-win: # name: ${{ matrix.os }} From 7633a1b58598e7a64ca44bb6395a80f645643512 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:53:46 +0100 Subject: [PATCH 0698/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c3c56706e..3242a3677d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,9 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - run: | - python -m pip install flake8 - make lint + run: | + python -m pip install flake8 + make lint # linux-macos-win: # name: ${{ matrix.os }} From 9b23cf58a16fd4d7dad0cf94ff89f2e8a21540d8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:54:36 +0100 Subject: [PATCH 0699/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3242a3677d..b302dd8894 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,10 @@ # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip on: [push] +name: Build jobs: linters: - name: "Linters" + name: Linters runs-on: ubuntu-latest strategy: matrix: From 935b32e5f4d15046036f9366a17f21929dc5bc82 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 20:57:29 +0100 Subject: [PATCH 0700/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b302dd8894..6cfb1058ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,21 +12,41 @@ on: [push] name: Build jobs: - linters: - name: Linters + linux: + name: Linux runs-on: ubuntu-latest strategy: matrix: - # in this example, there is a newer version already installed, 3.7.7, so the older version will be downloaded - python-version: [2.7, 3.8] + python-version: [2.7, 3.9] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} run: | - python -m pip install flake8 - make lint + make install + make test + + # linters: + # name: Linters + # runs-on: ubuntu-latest + # strategy: + # matrix: + # python-version: [2.7, 3.8] + # steps: + # - uses: actions/checkout@v2 + # - uses: actions/setup-python@v2 + # with: + # python-version: ${{ matrix.python-version }} + # run: | + # python -m pip install flake8 + # make lint + + + + + + # linux-macos-win: # name: ${{ matrix.os }} From a58803585d723ba00d3b13954256c10115e0ed52 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:02:12 +0100 Subject: [PATCH 0701/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 50 +++++++++++----------------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cfb1058ef..1ea383a20e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,42 +12,6 @@ on: [push] name: Build jobs: - linux: - name: Linux - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [2.7, 3.9] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - run: | - make install - make test - - # linters: - # name: Linters - # runs-on: ubuntu-latest - # strategy: - # matrix: - # python-version: [2.7, 3.8] - # steps: - # - uses: actions/checkout@v2 - # - uses: actions/setup-python@v2 - # with: - # python-version: ${{ matrix.python-version }} - # run: | - # python -m pip install flake8 - # make lint - - - - - - - # linux-macos-win: # name: ${{ matrix.os }} # runs-on: ${{ matrix.os }} @@ -129,3 +93,17 @@ jobs: # python3 psutil/tests/runner.py # python3 psutil/tests/test_memleaks.py +linters: + name: Linters + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.8] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + run: | + python -m pip install flake8 + make lint From 8b8ee2ed154bac7c577c9534cc0e363d5ed6f91a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:03:01 +0100 Subject: [PATCH 0702/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ea383a20e..4f8b6d2097 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,17 +93,17 @@ jobs: # python3 psutil/tests/runner.py # python3 psutil/tests/test_memleaks.py -linters: - name: Linters - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [2.7, 3.8] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - run: | - python -m pip install flake8 - make lint + linters: + name: Linters + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.8] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + run: | + python -m pip install flake8 + make lint From 3b13bb6b11080c2714ba059d9cb759061ea0f33d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:04:47 +0100 Subject: [PATCH 0703/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 95 ++++++++++++++++++++-------------------- setup.py | 3 ++ 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f8b6d2097..32172e7776 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,58 +12,57 @@ on: [push] name: Build jobs: -# linux-macos-win: -# name: ${{ matrix.os }} -# runs-on: ${{ matrix.os }} -# timeout-minutes: 30 -# strategy: -# fail-fast: false -# matrix: -# # os: [ubuntu-latest, macos-latest, windows-latest] -# os: [ubuntu-latest, macos-latest] -# include: -# - {name: Linux, python: '3.9', os: ubuntu-latest} -# env: -# CIBW_TEST_COMMAND: -# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 -# python {project}/psutil/tests/runner.py && -# PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 -# python {project}/psutil/tests/test_memleaks.py -# CIBW_TEST_EXTRAS: test -# CIBW_SKIP: cp35-* pp* -# steps: -# - name: Cancel previous runs -# uses: styfle/cancel-workflow-action@0.6.0 -# with: -# access_token: ${{ github.token }} + # linux-macos-win: + # name: ${{ matrix.os }} + # runs-on: ${{ matrix.os }} + # timeout-minutes: 30 + # strategy: + # fail-fast: false + # matrix: + # # os: [ubuntu-latest, macos-latest, windows-latest] + # os: [ubuntu-latest, macos-latest] + # include: + # - {name: Linux, python: '3.9', os: ubuntu-latest} + # env: + # CIBW_TEST_COMMAND: + # PYTHONUNBUFFERED=1 make install && + # PYTHONUNBUFFERED=1 make test && + # PYTHONUNBUFFERED=1 make test-memleaks + # CIBW_TEST_EXTRAS: test + # CIBW_SKIP: cp35-* pp* + # steps: + # - name: Cancel previous runs + # uses: styfle/cancel-workflow-action@0.6.0 + # with: + # access_token: ${{ github.token }} -# - uses: actions/checkout@v2 -# - uses: actions/setup-python@v2 -# with: -# python-version: 3.9 + # - uses: actions/checkout@v2 + # - uses: actions/setup-python@v2 + # with: + # python-version: 3.9 -# # - name: (Windows) install Visual C++ for Python 2.7 -# # if: matrix.os == 'windows-latest' -# # run: | -# # choco install vcpython27 -f -y + # # - name: (Windows) install Visual C++ for Python 2.7 + # # if: matrix.os == 'windows-latest' + # # run: | + # # choco install vcpython27 -f -y -# - name: Run tests -# run: | -# cibuildwheel . + # - name: Run tests + # run: | + # cibuildwheel . -# - name: Create wheels -# uses: actions/upload-artifact@v2 -# with: -# name: wheels -# path: wheelhouse + # - name: Create wheels + # uses: actions/upload-artifact@v2 + # with: + # name: wheels + # path: wheelhouse -# - name: Print hashes -# if: matrix.os == 'ubuntu-latest' -# run: | -# make generate-manifest -# python setup.py sdist -# mv dist/psutil*.tar.gz wheelhouse/ -# python scripts/internal/print_hashes.py wheelhouse/ + # - name: Print hashes + # if: matrix.os == 'ubuntu-latest' + # run: | + # make generate-manifest + # python setup.py sdist + # mv dist/psutil*.tar.gz wheelhouse/ + # python scripts/internal/print_hashes.py wheelhouse/ # freebsd: # runs-on: macos-latest @@ -98,7 +97,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.8] + python-version: [2.7, 3.9] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 diff --git a/setup.py b/setup.py index 90c79c7c76..7c9f7e5d78 100755 --- a/setup.py +++ b/setup.py @@ -6,6 +6,9 @@ """Cross-platform lib for process and system monitoring in Python.""" + + + from __future__ import print_function import contextlib import io From 55e1f56cb499f7afa69174aed1d5cec14760705b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:06:44 +0100 Subject: [PATCH 0704/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- Makefile | 2 +- setup.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f03930dd16..fe3670319e 100644 --- a/Makefile +++ b/Makefile @@ -187,7 +187,7 @@ test-coverage: ## Run test coverage. # =================================================================== lint-py: ## Run Python (flake8) linter. - @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 + @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 && echo "flake8 OK" lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py diff --git a/setup.py b/setup.py index 7c9f7e5d78..90c79c7c76 100755 --- a/setup.py +++ b/setup.py @@ -6,9 +6,6 @@ """Cross-platform lib for process and system monitoring in Python.""" - - - from __future__ import print_function import contextlib import io From c37d21323ef8ef5334562fb60cd1e258915b8cb9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:08:20 +0100 Subject: [PATCH 0705/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32172e7776..655f668552 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,3 +106,4 @@ jobs: run: | python -m pip install flake8 make lint + echo "xxx" From 4beeecb73e8a5d238d11961032c5ad56f8e6914f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:09:27 +0100 Subject: [PATCH 0706/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 655f668552..007d7e3edc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,4 +106,5 @@ jobs: run: | python -m pip install flake8 make lint + make print-announce echo "xxx" From 1f859ff47211ad349ac28af235e81fae268651bc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:13:40 +0100 Subject: [PATCH 0707/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 007d7e3edc..0eda8f1ebd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,19 +92,30 @@ jobs: # python3 psutil/tests/runner.py # python3 psutil/tests/test_memleaks.py + # linters: + # name: Linters + # runs-on: ubuntu-latest + # strategy: + # matrix: + # python-version: [2.7, 3.9] + # steps: + # - uses: actions/checkout@v2 + # - uses: actions/setup-python@v2 + # with: + # python-version: ${{ matrix.python-version }} + # run: | + # python -m pip install flake8 + # make lint + # make print-announce + linters: - name: Linters + name: 'Linters' runs-on: ubuntu-latest - strategy: - matrix: - python-version: [2.7, 3.9] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Install Dependencies run: | python -m pip install flake8 make lint make print-announce - echo "xxx" From c0e45dca5c9b2aeedea5c67ed7fa50e6e29e8d49 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:15:48 +0100 Subject: [PATCH 0708/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0eda8f1ebd..75ad71207f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,5 +117,4 @@ jobs: - name: Install Dependencies run: | python -m pip install flake8 - make lint - make print-announce + python3 -m flake8 . From 8d2efad358583b094194315cdb0843ab8a1b458b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:17:06 +0100 Subject: [PATCH 0709/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75ad71207f..b694526229 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,12 +109,12 @@ jobs: # make print-announce linters: - name: 'Linters' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - name: Install Dependencies + - name: 'Linters' run: | python -m pip install flake8 python3 -m flake8 . + echo "OK" From 033ac0b842a983e1100ae8bb129bbb0272924392 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:18:53 +0100 Subject: [PATCH 0710/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 6 ++++-- setup.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b694526229..fccb14e844 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,5 +116,7 @@ jobs: - name: 'Linters' run: | python -m pip install flake8 - python3 -m flake8 . - echo "OK" + python -m flake8 . + echo "flake8 linting OK" + find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py + echo "C linting OK" diff --git a/setup.py b/setup.py index 90c79c7c76..1f6f448be1 100755 --- a/setup.py +++ b/setup.py @@ -6,6 +6,8 @@ """Cross-platform lib for process and system monitoring in Python.""" + + from __future__ import print_function import contextlib import io From 36d083c1d77ae9adeeaac4839e2f819e64d9b602 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:20:02 +0100 Subject: [PATCH 0711/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 2 +- setup.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fccb14e844..afeee84f1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,10 +110,10 @@ jobs: linters: runs-on: ubuntu-latest + name: 'Linters' steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - name: 'Linters' run: | python -m pip install flake8 python -m flake8 . diff --git a/setup.py b/setup.py index 1f6f448be1..90c79c7c76 100755 --- a/setup.py +++ b/setup.py @@ -6,8 +6,6 @@ """Cross-platform lib for process and system monitoring in Python.""" - - from __future__ import print_function import contextlib import io From efbefc8f0f2d73f3c07c7af829f920fe58a15dd9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:20:23 +0100 Subject: [PATCH 0712/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afeee84f1e..b6203cb190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 run: | + find . python -m pip install flake8 python -m flake8 . echo "flake8 linting OK" From d52279815bf3aa8c132bd8b97fa8b3a710b6a3f5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:21:03 +0100 Subject: [PATCH 0713/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6203cb190..26d384bfd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,12 +110,11 @@ jobs: linters: runs-on: ubuntu-latest - name: 'Linters' - steps: + steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 + name: Linters run: | - find . python -m pip install flake8 python -m flake8 . echo "flake8 linting OK" From 691c883a666e8b2a136ab9a5c8595af2efd39ece Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:22:13 +0100 Subject: [PATCH 0714/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26d384bfd8..04aa96270e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,13 +110,13 @@ jobs: linters: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - name: Linters - run: | - python -m pip install flake8 - python -m flake8 . - echo "flake8 linting OK" - find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py - echo "C linting OK" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: 'Linters' + run: | + python -m pip install flake8 + python -m flake8 . + echo "flake8 linting OK" + find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py + echo "C linting OK" From 8734ad9828ad4c520563b39544cd3b310c2f4644 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:24:06 +0100 Subject: [PATCH 0715/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04aa96270e..eb801b4488 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,8 @@ jobs: - uses: actions/setup-python@v2 - name: 'Linters' run: | + ls + find . python -m pip install flake8 python -m flake8 . echo "flake8 linting OK" From 29225d30cb8f51cf102064c36a1755cf96ca8db4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:25:16 +0100 Subject: [PATCH 0716/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 94 ++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb801b4488..7ebcf08c47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,57 +12,57 @@ on: [push] name: Build jobs: - # linux-macos-win: - # name: ${{ matrix.os }} - # runs-on: ${{ matrix.os }} - # timeout-minutes: 30 - # strategy: - # fail-fast: false - # matrix: - # # os: [ubuntu-latest, macos-latest, windows-latest] - # os: [ubuntu-latest, macos-latest] - # include: - # - {name: Linux, python: '3.9', os: ubuntu-latest} - # env: - # CIBW_TEST_COMMAND: - # PYTHONUNBUFFERED=1 make install && - # PYTHONUNBUFFERED=1 make test && - # PYTHONUNBUFFERED=1 make test-memleaks - # CIBW_TEST_EXTRAS: test - # CIBW_SKIP: cp35-* pp* - # steps: - # - name: Cancel previous runs - # uses: styfle/cancel-workflow-action@0.6.0 - # with: - # access_token: ${{ github.token }} + linux-macos-win: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + # os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] + include: + - {name: Linux, python: '3.9', os: ubuntu-latest} + env: + CIBW_TEST_COMMAND: + PYTHONUNBUFFERED=1 make install && + PYTHONUNBUFFERED=1 make test && + PYTHONUNBUFFERED=1 make test-memleaks + CIBW_TEST_EXTRAS: test + CIBW_SKIP: cp35-* pp* + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ github.token }} - # - uses: actions/checkout@v2 - # - uses: actions/setup-python@v2 - # with: - # python-version: 3.9 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.9 - # # - name: (Windows) install Visual C++ for Python 2.7 - # # if: matrix.os == 'windows-latest' - # # run: | - # # choco install vcpython27 -f -y + # - name: (Windows) install Visual C++ for Python 2.7 + # if: matrix.os == 'windows-latest' + # run: | + # choco install vcpython27 -f -y - # - name: Run tests - # run: | - # cibuildwheel . + - name: Run tests + run: | + cibuildwheel . - # - name: Create wheels - # uses: actions/upload-artifact@v2 - # with: - # name: wheels - # path: wheelhouse + - name: Create wheels + uses: actions/upload-artifact@v2 + with: + name: wheels + path: wheelhouse - # - name: Print hashes - # if: matrix.os == 'ubuntu-latest' - # run: | - # make generate-manifest - # python setup.py sdist - # mv dist/psutil*.tar.gz wheelhouse/ - # python scripts/internal/print_hashes.py wheelhouse/ + - name: Print hashes + if: matrix.os == 'ubuntu-latest' + run: | + make generate-manifest + python setup.py sdist + mv dist/psutil*.tar.gz wheelhouse/ + python scripts/internal/print_hashes.py wheelhouse/ # freebsd: # runs-on: macos-latest @@ -115,8 +115,6 @@ jobs: - uses: actions/setup-python@v2 - name: 'Linters' run: | - ls - find . python -m pip install flake8 python -m flake8 . echo "flake8 linting OK" From 44a4dbea594664bf7bf69177a62d1191d31cbd99 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:27:35 +0100 Subject: [PATCH 0717/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ebcf08c47..7247e68e85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,8 @@ jobs: - name: Run tests run: | + pip install cibuildwheel cibuildwheel . - - name: Create wheels uses: actions/upload-artifact@v2 with: From 0a9711fff7a1a65b929aa2d8645cf498f862a39a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:31:00 +0100 Subject: [PATCH 0718/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7247e68e85..1f49ef27f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,9 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - PYTHONUNBUFFERED=1 make install && - PYTHONUNBUFFERED=1 make test && - PYTHONUNBUFFERED=1 make test-memleaks + PYTHON=python PYTHONUNBUFFERED=1 make install && + PYTHON=python PYTHONUNBUFFERED=1 make test && + PYTHON=python PYTHONUNBUFFERED=1 make test-memleaks CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: From 19d9e334670650199cac84b3c8a587473b177b1f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:34:39 +0100 Subject: [PATCH 0719/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f49ef27f0..1c27734524 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,9 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - PYTHON=python PYTHONUNBUFFERED=1 make install && - PYTHON=python PYTHONUNBUFFERED=1 make test && - PYTHON=python PYTHONUNBUFFERED=1 make test-memleaks + make install PYTHON=python && + make test PYTHON=python && + make test-memleaks PYTHON=python CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: @@ -84,7 +84,6 @@ jobs: # set +e # export \ # PYTHONWARNINGS=always \ -# PYTHONUNBUFFERED=1 \ # PSUTIL_TESTING=1 \ # PSUTIL_DEBUG=1 # python3 -m pip install --user setuptools From 0d273f9dad3f3307f7821640b3bfdaa9ea939622 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:36:10 +0100 Subject: [PATCH 0720/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c27734524..6381cbef0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: make install PYTHON=python && make test PYTHON=python && make test-memleaks PYTHON=python + CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: From 92f3f2457af21ba3dce01c124ff3fafc6afb005f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:38:59 +0100 Subject: [PATCH 0721/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 8 ++++---- Makefile | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6381cbef0e..b47b96cf03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,10 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - make install PYTHON=python && - make test PYTHON=python && - make test-memleaks PYTHON=python - + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 + python {project}/psutil/tests/runner.py && + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 + python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: diff --git a/Makefile b/Makefile index fe3670319e..f03930dd16 100644 --- a/Makefile +++ b/Makefile @@ -187,7 +187,7 @@ test-coverage: ## Run test coverage. # =================================================================== lint-py: ## Run Python (flake8) linter. - @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 && echo "flake8 OK" + @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py From 207a0018f1413157c0791e88fe3cc0971ff21a06 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:43:15 +0100 Subject: [PATCH 0722/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 64 +++++++++++++++------------------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b47b96cf03..534aa998cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,48 +65,32 @@ jobs: mv dist/psutil*.tar.gz wheelhouse/ python scripts/internal/print_hashes.py wheelhouse/ -# freebsd: -# runs-on: macos-latest -# steps: -# - name: Cancel previous runs -# uses: styfle/cancel-workflow-action@0.6.0 -# with: -# access_token: ${{ github.token }} - -# - uses: actions/checkout@v2 + freebsd: + runs-on: macos-latest + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ github.token }} -# - name: Run tests -# id: test -# uses: vmactions/freebsd-vm@v0.0.8 -# with: -# usesh: true -# prepare: pkg install -y gcc python3 -# run: | -# set +e -# export \ -# PYTHONWARNINGS=always \ -# PSUTIL_TESTING=1 \ -# PSUTIL_DEBUG=1 -# python3 -m pip install --user setuptools -# python3 setup.py install -# python3 psutil/tests/runner.py -# python3 psutil/tests/test_memleaks.py + - uses: actions/checkout@v2 - # linters: - # name: Linters - # runs-on: ubuntu-latest - # strategy: - # matrix: - # python-version: [2.7, 3.9] - # steps: - # - uses: actions/checkout@v2 - # - uses: actions/setup-python@v2 - # with: - # python-version: ${{ matrix.python-version }} - # run: | - # python -m pip install flake8 - # make lint - # make print-announce + - name: Run tests + id: test + uses: vmactions/freebsd-vm@v0.0.8 + with: + usesh: true + prepare: pkg install -y gcc python3 + run: | + set +e + export \ + PYTHONWARNINGS=always \ + PSUTIL_TESTING=1 \ + PSUTIL_DEBUG=1 + python3 -m pip install --user setuptools + python3 setup.py install + python3 psutil/tests/runner.py + python3 psutil/tests/test_memleaks.py linters: runs-on: ubuntu-latest From 76b471a618b9968f145a90fc1e6c23ee1419e593 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:45:07 +0100 Subject: [PATCH 0723/1714] skip-appveyor Signed-off-by: Giampaolo Rodola --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 534aa998cb..c8a98f7914 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,8 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: From 8ddbc612971adeca7f9c829138a3eab8f6ed5a23 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Dec 2020 21:51:01 +0100 Subject: [PATCH 0724/1714] CI: run linters Signed-off-by: Giampaolo Rodola --- .github/workflows/{ci.yml => build.yml} | 27 ++++++++++++++++--------- appveyor.yml | 2 +- 2 files changed, 18 insertions(+), 11 deletions(-) rename .github/workflows/{ci.yml => build.yml} (84%) diff --git a/.github/workflows/ci.yml b/.github/workflows/build.yml similarity index 84% rename from .github/workflows/ci.yml rename to .github/workflows/build.yml index 6a4a019f9a..c8a98f7914 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/build.yml @@ -9,8 +9,8 @@ # To skip certain builds see: # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip -name: CI on: [push] +name: Build jobs: linux-macos-win: name: ${{ matrix.os }} @@ -25,12 +25,8 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 - python {project}/psutil/tests/test_memleaks.py && - find . -type f -iname "*.py" | xargs python -m flake8 --config=.flake8 && - find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* steps: @@ -51,9 +47,8 @@ jobs: - name: Run tests run: | - pip install cibuildwheel flake8 + pip install cibuildwheel cibuildwheel . - - name: Create wheels uses: actions/upload-artifact@v2 with: @@ -88,10 +83,22 @@ jobs: set +e export \ PYTHONWARNINGS=always \ - PYTHONUNBUFFERED=1 \ PSUTIL_TESTING=1 \ PSUTIL_DEBUG=1 python3 -m pip install --user setuptools python3 setup.py install python3 psutil/tests/runner.py python3 psutil/tests/test_memleaks.py + + linters: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: 'Linters' + run: | + python -m pip install flake8 + python -m flake8 . + echo "flake8 linting OK" + find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py + echo "C linting OK" diff --git a/appveyor.yml b/appveyor.yml index a05322060c..aaf7de3006 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,7 +99,7 @@ cache: # - might want to upload the content of dist/*.whl to a public wheelhouse skip_commits: - message: skip-ci + message: skip-appveyor # run build only if one of the following files is modified on commit only_commits: From f6ec148529004013f6411d938461dee19d498255 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 01:13:10 +0100 Subject: [PATCH 0725/1714] pre-release Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 6 +++--- docs/index.rst | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 38a2353636..5aee3a9563 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.7.4 (development version) -=========================== +5.8.0 +===== -XXXX-XX-XX +2020-12-19 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index ee5863c320..adbec06fbe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2562,6 +2562,10 @@ Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. Timeline ======== +- 2020-12-19: + `5.8.0 `__ - + `what's new `__ - + `diff `__ - 2020-10-23: `5.7.3 `__ - `what's new `__ - From db8cd0ccdd9a9dc9fb3abaaaba77690e205a6a50 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 01:36:55 +0100 Subject: [PATCH 0726/1714] update doc Signed-off-by: Giampaolo Rodola --- Makefile | 1 + docs/index.rst | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f03930dd16..d8ee5edc6c 100644 --- a/Makefile +++ b/Makefile @@ -261,6 +261,7 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} sdist ${MAKE} download-wheels-github ${MAKE} download-wheels-appveyor + ${MAKE} print-hashes ${MAKE} print-wheels $(PYTHON) -m twine check dist/* $(PYTHON) -c \ diff --git a/docs/index.rst b/docs/index.rst index adbec06fbe..8c3b432b1e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2547,9 +2547,10 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= +* psutil 5.8.0 (2020-12): **PyPy 2** on Windows * psutil 5.7.1 (2020-07): **Windows Nano** * psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support -* psutil 5.7.0 (2020-02): **PyPy** on Windows +* psutil 5.7.0 (2020-02): **PyPy 3** on Windows * psutil 5.4.0 (2017-11): **AIX** * psutil 3.4.1 (2016-01): **NetBSD** * psutil 3.3.0 (2015-11): **OpenBSD** From b760f4848db4db49de57918660f6a5059666b720 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 01:45:00 +0100 Subject: [PATCH 0727/1714] fix around macos failure on CI Signed-off-by: Giampaolo Rodola --- psutil/tests/test_contracts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index dd6002e606..32c75fd7f3 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -34,6 +34,7 @@ from psutil._compat import range from psutil.tests import APPVEYOR from psutil.tests import check_connection_ntuple +from psutil.tests import CI_TESTING from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import GITHUB_ACTIONS @@ -453,7 +454,7 @@ def exe(self, ret, info): try: assert os.access(ret, os.X_OK) except AssertionError: - if os.path.exists(ret): + if os.path.exists(ret) and not CI_TESTING: raise def pid(self, ret, info): From a12dda352d8309048400b337521a780b8884e8cd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 12:58:27 +0100 Subject: [PATCH 0728/1714] update INSTALL.rst + fix CI failures Signed-off-by: Giampaolo Rodola --- INSTALL.rst | 27 ++++++++++++++++----------- README.rst | 13 +------------ psutil/tests/test_process.py | 6 ++++++ psutil/tests/test_testutils.py | 1 + 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 88cef33f36..53969d9a7d 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -4,10 +4,8 @@ Install psutil Linux, Windows, macOS (wheels) ------------------------------ -psutil makes extensive use of C extension modules, meaning a C compiler is -required to build the sources. -Pre-compiled cPython wheels are provided on each release though, so on -**Linux**, **Windows** and **macOS** all you have to do is:: +Pre-compiled wheels are distributed on each release for ***Linux**, **Windows** +and **macOS**, so you don't need a C compiler. All you have to do is:: pip3 install --user psutil @@ -15,8 +13,8 @@ This (``--user``) will install psutil as a limited user (not system-wide). If wheels are not available for your platform or architecture, or you whish to install psutil from sources, keep reading. -Linux (install from sources) ----------------------------- +Linux (build) +------------- Ubuntu / Debian:: @@ -28,10 +26,10 @@ RedHat / CentOS:: sudo yum install gcc python3-devel pip3 install --user --no-binary :all: psutil -Windows (install from sources) ------------------------------- +Windows (build) +--------------- -In order to compile psutil on Windows you'll need **Visual Studio** compiler +In order to install psutil from sources on Windows you need **Visual Studio** (**MinGW** is not supported). Here's a couple of guides describing how to do it: `link `__ and `link `__. @@ -39,6 +37,13 @@ Once VS is installed do:: pip3 install --user --no-binary :all: psutil +macOS (build) +------------- + +Install `Xcode `__ then run:: + + pip3 install psutil + FreeBSD ------- @@ -96,7 +101,7 @@ If you don't have pip you can install with wget:: On Windows, `download pip `__, open cmd.exe and install it with:: - C:\Python27\python.exe get-pip.py + py get-pip.py "pip not found" --------------- @@ -110,6 +115,6 @@ Permission errors (UNIX) ------------------------ If you want to install psutil system-wide and you bump into permission errors -either run as root or prepend ``sudo``:: +either run as root user or prepend ``sudo``:: sudo pip3 install psutil diff --git a/README.rst b/README.rst index a9334da283..8486cda761 100644 --- a/README.rst +++ b/README.rst @@ -110,7 +110,7 @@ immensely from some funding. Keeping up with bug reports and maintenance has become hardly sustainable for me alone in terms of time. If you're a company that's making significant use of psutil you can consider -becoming a sponsor via `GitHub `__, +becoming a sponsor via `GitHub Sponsors `__, `Open Collective `__ or `PayPal `__ and have your logo displayed in here and psutil `doc `__. @@ -499,14 +499,3 @@ Portings - C: https://github.com/hamon-in/cpslib - Rust: https://github.com/rust-psutil/rust-psutil - Nim: https://github.com/johnscillieri/psutil-nim - -Security -======== - -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -.. _`Giampaolo Rodola`: https://gmpy.dev/about -.. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 -.. _Tidelift security contact: https://tidelift.com/security -.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0ec2ead8e2..dfe8854724 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -718,6 +718,12 @@ def test_cmdline(self): 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") diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 31ac4dee4c..09adbdb180 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -350,6 +350,7 @@ def test_create_sockets(self): @serialrun class TestMemLeakClass(TestMemoryLeak): + @retry_on_failure() def test_times(self): def fun(): cnt['cnt'] += 1 From 2c1189553d2fae85262ac93f9ba498f445536394 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 13:53:58 +0100 Subject: [PATCH 0729/1714] update INSTALL Signed-off-by: Giampaolo Rodola --- INSTALL.rst | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 53969d9a7d..5d4a87ae8c 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -4,14 +4,14 @@ Install psutil Linux, Windows, macOS (wheels) ------------------------------ -Pre-compiled wheels are distributed on each release for ***Linux**, **Windows** -and **macOS**, so you don't need a C compiler. All you have to do is:: +Pre-compiled wheels are distributed for these platforms, so you won't have to +install a C compiler. All you have to do is:: - pip3 install --user psutil + pip install --user psutil This (``--user``) will install psutil as a limited user (not system-wide). If wheels are not available for your platform or architecture, or you whish to -install psutil from sources, keep reading. +build & install psutil from sources, keep reading. Linux (build) ------------- @@ -19,30 +19,30 @@ Linux (build) Ubuntu / Debian:: sudo apt-get install gcc python3-dev - pip3 install --user --no-binary :all: psutil + pip install --user --no-binary :all: psutil RedHat / CentOS:: sudo yum install gcc python3-devel - pip3 install --user --no-binary :all: psutil + pip install --user --no-binary :all: psutil Windows (build) --------------- -In order to install psutil from sources on Windows you need **Visual Studio** -(**MinGW** is not supported). +In order to install psutil from sources on Windows you need Visual Studio +(MinGW is not supported). Here's a couple of guides describing how to do it: `link `__ and `link `__. Once VS is installed do:: - pip3 install --user --no-binary :all: psutil + pip install --user --no-binary :all: psutil macOS (build) ------------- Install `Xcode `__ then run:: - pip3 install psutil + pip install --user --no-binary :all: psutil FreeBSD ------- @@ -50,7 +50,7 @@ FreeBSD :: pkg install python3 gcc - python3 -m pip3 install psutil + python3 -m pip install psutil OpenBSD ------- @@ -59,7 +59,7 @@ OpenBSD export PKG_PATH=http://ftp.eu.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ pkg_add -v python gcc - python3 -m pip install psutil + pip install psutil NetBSD ------ @@ -69,7 +69,7 @@ NetBSD export PKG_PATH="ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" pkg_add -v pkgin pkgin install python3 gcc - python3 -m pip install psutil + pip install psutil Sun Solaris ----------- @@ -81,7 +81,7 @@ If ``cc`` compiler is not installed create a symbolic link to ``gcc``:: Install:: pkg install gcc - python3 -m pip install psutil + pip install psutil Troubleshooting =============== @@ -107,7 +107,7 @@ cmd.exe and install it with:: --------------- Sometimes pip is installed but it's not available in your ``PATH`` -(``pip command not found`` or similar). Try this:: +("pip command not found" or similar). Try this:: python3 -m pip install psutil @@ -117,4 +117,4 @@ Permission errors (UNIX) If you want to install psutil system-wide and you bump into permission errors either run as root user or prepend ``sudo``:: - sudo pip3 install psutil + sudo pip install psutil From adf5aa0ab0a6e91a8e3cd659d5b91a5dcdee1b4a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 14:42:02 +0100 Subject: [PATCH 0730/1714] add awaiting-response bot Signed-off-by: Giampaolo Rodola --- .github/no-response.yml | 10 ++++++++++ INSTALL.rst | 11 +++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 .github/no-response.yml diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000000..e5afde07c1 --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,10 @@ +# Configuration for probot-no-response: https://github.com/probot/no-response + +# Number of days of inactivity before an issue is closed for lack of response +daysUntilClose: 14 +# Label requiring a response +responseRequiredLabel: awaiting-response +# Comment to post when closing an Issue for lack of response. +# Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response for more information from the original author. Please reach out if you have or find the answers requested so that this can be investigated further. diff --git a/INSTALL.rst b/INSTALL.rst index 5d4a87ae8c..22693b5695 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -7,9 +7,8 @@ Linux, Windows, macOS (wheels) Pre-compiled wheels are distributed for these platforms, so you won't have to install a C compiler. All you have to do is:: - pip install --user psutil + pip install psutil -This (``--user``) will install psutil as a limited user (not system-wide). If wheels are not available for your platform or architecture, or you whish to build & install psutil from sources, keep reading. @@ -19,12 +18,12 @@ Linux (build) Ubuntu / Debian:: sudo apt-get install gcc python3-dev - pip install --user --no-binary :all: psutil + pip install --no-binary :all: psutil RedHat / CentOS:: sudo yum install gcc python3-devel - pip install --user --no-binary :all: psutil + pip install --no-binary :all: psutil Windows (build) --------------- @@ -35,14 +34,14 @@ Here's a couple of guides describing how to do it: `link `__. Once VS is installed do:: - pip install --user --no-binary :all: psutil + pip install --no-binary :all: psutil macOS (build) ------------- Install `Xcode `__ then run:: - pip install --user --no-binary :all: psutil + pip install --no-binary :all: psutil FreeBSD ------- From bd28500252d015f715e2b7a51d365a3625cc54c5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 20:11:26 +0100 Subject: [PATCH 0731/1714] improve github actions Signed-off-by: Giampaolo Rodola --- .flake8 | 1 + .github/ISSUE_TEMPLATE/bug.md | 2 +- .github/ISSUE_TEMPLATE/enhancement.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/no-response.yml | 2 +- .github/workflows/build.yml | 11 +- .../workflows/issues.py | 107 ++++++++++-------- .../workflows/{issue_bot.yml => issues.yml} | 15 ++- MANIFEST.in | 1 - scripts/internal/download_wheels_github.py | 15 ++- 10 files changed, 96 insertions(+), 62 deletions(-) rename scripts/internal/github_issue_bot.py => .github/workflows/issues.py (78%) rename .github/workflows/{issue_bot.yml => issues.yml} (52%) diff --git a/.flake8 b/.flake8 index 89225a6abd..1244cd48bf 100644 --- a/.flake8 +++ b/.flake8 @@ -12,3 +12,4 @@ per-file-ignores = scripts/internal/convert_readme.py:E501,T001 psutil/tests/runner.py:T001 psutil/tests/test_memleaks.py:T001 + .github/workflows/*:T001 diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 096033db8d..24d01efa7a 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -11,7 +11,7 @@ labels: 'bug' * Architecture: { 64bit, 32bit, ARM, PowerPC, s390 } * Psutil version: { pip3 show psutil } * Python version: { python3 -V } -* Type: { core, doc, performance, scripts, tests, wheels, newapi } +* Type: { core, doc, performance, scripts, tests, wheels, new-api, installation } ## Description diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 0a1cbb77ef..2f7d75a565 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -9,7 +9,7 @@ title: "[OS] title" ## Summary * OS: { type-or-version } -* Type: { core, doc, performance, scripts, tests, wheels, newapi } +* Type: { core, doc, performance, scripts, tests, wheels, new-api } ## Description diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6b7fdee79d..e8bbb2a4c5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ * OS: { type-or-version } * Bug fix: { yes/no } -* Type: { core, doc, performance, scripts, tests, wheels, newapi } +* Type: { core, doc, performance, scripts, tests, wheels, new-api } * Fixes: { comma-separated list of issues fixed by this PR, if any } ## Description diff --git a/.github/no-response.yml b/.github/no-response.yml index e5afde07c1..56457a28d0 100644 --- a/.github/no-response.yml +++ b/.github/no-response.yml @@ -3,7 +3,7 @@ # Number of days of inactivity before an issue is closed for lack of response daysUntilClose: 14 # Label requiring a response -responseRequiredLabel: awaiting-response +responseRequiredLabel: need-more-info # Comment to post when closing an Issue for lack of response. # Set to `false` to disable closeComment: > diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8a98f7914..25efdbc189 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip on: [push] -name: Build +name: build jobs: linux-macos-win: name: ${{ matrix.os }} @@ -29,6 +29,7 @@ jobs: PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* + steps: - name: Cancel previous runs uses: styfle/cancel-workflow-action@0.6.0 @@ -40,15 +41,17 @@ jobs: with: python-version: 3.9 + - name: Install cibuildwheel + run: pip install cibuildwheel + # - name: (Windows) install Visual C++ for Python 2.7 # if: matrix.os == 'windows-latest' # run: | # choco install vcpython27 -f -y - name: Run tests - run: | - pip install cibuildwheel - cibuildwheel . + run: cibuildwheel . + - name: Create wheels uses: actions/upload-artifact@v2 with: diff --git a/scripts/internal/github_issue_bot.py b/.github/workflows/issues.py similarity index 78% rename from scripts/internal/github_issue_bot.py rename to .github/workflows/issues.py index ce66a2937e..a2beecc785 100644 --- a/scripts/internal/github_issue_bot.py +++ b/.github/workflows/issues.py @@ -4,9 +4,15 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +""" +Bot triggered by Github Actions every time a new issue, PR or comment +is created. Assign labels, provide replies, closes issues, etc. depending +on the situation. +""" + import os import re -import textwrap +import sys from github import Github @@ -132,6 +138,10 @@ def is_new(issue): return issue.comments == 0 +def is_comment(issue): + return not is_new(issue) + + def has_label(issue, label): assigned = [x.name for x in issue.labels] return label in assigned @@ -151,52 +161,34 @@ def get_repo(): return Github(token).get_repo(repo) -def get_issue_event(): - url = os.environ.get('GITHUB_ISSUE_URL') - if url: # issue - num = int(url.split('/')[-1]) - else: # PR - url = os.environ['GITHUB_REF'] - num = int(url.split('/')[-2]) - repo = get_repo() - return repo.get_issue(number=num) - - # --- actions def log(msg): - print(">>> %s <<<" % msg) + if '\n' in msg or "\r\n" in msg: + print(">>>\n%s\n<<<" % msg) + else: + print(">>> %s <<<" % msg) def add_label(issue, label): def should_add(issue, label): if has_label(issue, label): - log("issue %r already has label %r" % (issue, label)) + log("already has label %r" % (label)) return False for left, right in ILLOGICAL_PAIRS: if label == left and has_label(issue, right): - log("issue %r already has label %r" % (issue, label)) + log("already has label" % (label)) return False return not has_label(issue, label) if not should_add(issue, label): + log("should not add label %r" % label) return - type_ = "PR:" if is_pr(issue) else "issue:" - assigned = ', '.join([x.name for x in issue.labels]) - log(textwrap.dedent("""\ - %-10s %s: %s - assigned: %s - new: %s""" % ( - type_, - issue.number, - issue.title, - assigned, - label, - ))) + log("add label %r" % label) issue.add_to_labels(label) @@ -212,26 +204,38 @@ def add_labels_from_text(issue, text): add_label(issue, label) -def add_labels_from_body(issue, text): +def add_labels_from_new_body(issue, text): + log("start searching for template lines in new issue/PR body") # add os label - print(repr(text)) r = re.search(r"\* OS:.*?\n", text) + log("search for 'OS: ...' line") if r: + log("found") add_labels_from_text(issue, r.group(0)) + else: + log("not found") + # add bug/enhancement label + log("search for 'Bug fix: y/n' line") r = re.search(r"\* Bug fix:.*?\n", text) if is_pr(issue) and \ r is not None and \ not has_label(issue, "bug") and \ not has_label(issue, "enhancement"): + log("found") s = r.group(0).lower() if 'yes' in s: add_label(issue, 'bug') else: add_label(issue, 'enhancement') + else: + log("not found") + # add type labels + log("search for 'Type: ...' line") r = re.search(r"\* Type:.*?\n", text) if r: + log("found") s = r.group(0).lower() if 'doc' in s: add_label(issue, 'doc') @@ -243,43 +247,56 @@ def add_labels_from_body(issue, text): add_label(issue, 'tests') if 'wheels' in s: add_label(issue, 'wheels') - if 'newapi' in s: - add_label(issue, 'api') - - -# --- run + if 'new-api' in s: + add_label(issue, 'new-api') + if 'new-platform' in s: + add_label(issue, 'new-platform') + else: + log("not found") -def process_both(issue): - add_labels_from_text(issue, issue.title) - add_labels_from_body(issue, issue.body) +# --- events -def process_issue(issue): +def on_new_issue(issue): def has_text(text): return text in issue.title.lower() or text in issue.body.lower() + log("searching for missing Python.h") if has_text("missing python.h") or \ has_text("python.h: no such file or directory") or \ "#include\n^~~~" in issue.body.replace(' ', '') or \ "#include\r\n^~~~" in issue.body.replace(' ', ''): + log("found") issue.create_comment(REPLY_MISSING_PYTHON_HEADERS) issue.edit(state='closed') return -def process_pr(pr): +def on_new_pr(issue): + pass + + +def on_new_comment(issue): pass def main(): - log("Running issue/PR bot.") - issue = get_issue_event() - process_both(issue) - if is_issue(issue): - process_issue(issue) + issue = get_repo().get_issue(number=int(sys.argv[1])) + stype = "issue" if is_issue(issue) else "PR" + log("running issue bot for %s %r" % (stype, issue)) + + if is_new(issue): + log("new %s\n%s" % (stype, issue.body)) + add_labels_from_text(issue, issue.title) + add_labels_from_new_body(issue, issue.body) + if is_issue(issue): + on_new_issue(issue) + if is_pr(issue): + on_new_pr(issue) else: - process_pr(issue) + log("new comment: \n", issue.body) + on_new_comment(issue) if __name__ == '__main__': diff --git a/.github/workflows/issue_bot.yml b/.github/workflows/issues.yml similarity index 52% rename from .github/workflows/issue_bot.yml rename to .github/workflows/issues.yml index 223c0b6e52..17355dee5a 100644 --- a/.github/workflows/issue_bot.yml +++ b/.github/workflows/issues.yml @@ -1,22 +1,27 @@ -name: Issue/PR bot +name: issue-bot on: issues: types: [opened] pull_request: typed: [opened] + issue_comment: + types: [created] jobs: build: runs-on: ubuntu-latest steps: + # install python - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Install Python uses: actions/setup-python@v2 with: python-version: 3.8 + # install deps + - name: Install deps + run: python -m pip install --upgrade pip PyGithub + # run - name: Run env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_ISSUE_URL: ${{ github.event.issue.url }} run: | - PYTHONUNBUFFERED=1 python -m pip install --upgrade pip PyGithub - PYTHONUNBUFFERED=1 python scripts/internal/github_issue_bot.py + python .github/workflows/issues.py ${{ github.event.issue.number }} diff --git a/MANIFEST.in b/MANIFEST.in index df1128335b..d651cd4e2d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -123,7 +123,6 @@ include scripts/internal/download_wheels_github.py include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py -include scripts/internal/github_issue_bot.py include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py index 5ce98b84e0..a344ec494e 100755 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels_github.py @@ -19,6 +19,7 @@ import json import os import requests +import sys import zipfile from psutil import __version__ as PSUTIL_VERSION @@ -79,10 +80,18 @@ def run(): def main(): global TOKEN parser = argparse.ArgumentParser(description='GitHub wheels downloader') - parser.add_argument('--tokenfile', required=True) + parser.add_argument('--token') + parser.add_argument('--tokenfile') args = parser.parse_args() - with open(os.path.expanduser(args.tokenfile)) as f: - TOKEN = f.read().strip() + + if args.tokenfile: + with open(os.path.expanduser(args.tokenfile)) as f: + TOKEN = f.read().strip() + elif args.token: + TOKEN = args.token + else: + return sys.exit('specify --token or --tokenfile args') + try: run() finally: From c02648acc81fdfeca640d11d400fcbcd094b0fe4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 20:29:54 +0100 Subject: [PATCH 0732/1714] small change Signed-off-by: Giampaolo Rodola --- .github/workflows/issues.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 17355dee5a..2c2e38e329 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -1,4 +1,5 @@ -name: issue-bot +# Fired by Github Actions every time an issue, PR or comment is created. +name: issues on: issues: types: [opened] From c0e9e1b4ff694c4dc88540f9868a5a518cbc1604 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 19 Dec 2020 21:18:54 +0100 Subject: [PATCH 0733/1714] fix gh action Signed-off-by: Giampaolo Rodola --- .github/workflows/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index a2beecc785..c9f92d3142 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -295,7 +295,7 @@ def main(): if is_pr(issue): on_new_pr(issue) else: - log("new comment: \n", issue.body) + log("new comment: \n" % issue.body) on_new_comment(issue) From c70ceea3757a0664131605a012b2d41d81edb935 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 20 Dec 2020 01:48:53 +0100 Subject: [PATCH 0734/1714] #1892 fix cpu_freq() broken on Apple M1 (#1895) fix #1456: [macOS] psutil.cpu_freq()'s min and max are set to 0 if can't be determined (instead of crashing). fix #1892: [macOS] psutil.cpu_freq() broken on Apple M1. --- HISTORY.rst | 12 ++++++++++++ psutil/_psutil_osx.c | 39 +++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5aee3a9563..66efe586f5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,17 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.8.0 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +**Bug fixes** + +- 1456_: [macOS] psutil.cpu_freq()'s min and max are set to 0 if can't be + determined (instead of crashing). +- 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. + + 5.8.0 ===== diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 13d0bb6856..5720de9f7f 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -684,30 +684,33 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { /* - * Retrieve CPU frequency. + * Retrieve CPU frequency. Note: all of these are static vendor values + * that never change. */ static PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { - int64_t curr; - int64_t min; - int64_t max; - size_t size = sizeof(int64_t); + unsigned int curr; + int64_t min = 0; + int64_t max = 0; + int mib[2]; + size_t len = sizeof(curr); + size_t size = sizeof(min); - if (sysctlbyname("hw.cpufrequency", &curr, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('hw.cpufrequency')"); - } - if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('hw.cpufrequency_min')"); - } - if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('hw.cpufrequency_max')"); - } + // also availble as "hw.cpufrequency" but it's deprecated + mib[0] = CTL_HW; + mib[1] = HW_CPU_FREQ; + + if (sysctl(mib, 2, &curr, &len, NULL, 0) < 0) + return PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); + + if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) + psutil_debug("sysct('hw.cpufrequency_min') failed (set to 0)"); + + if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) + psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); return Py_BuildValue( - "KKK", + "IKK", curr / 1000 / 1000, min / 1000 / 1000, max / 1000 / 1000); From 58c4b1f83c531c0c61d153eb85eb8c7cac2e3449 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 20 Dec 2020 14:51:41 +0100 Subject: [PATCH 0735/1714] adjust CI bots Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 22 ++++++--- .github/workflows/issues.py | 94 ++++++++++++++++++++++++++---------- .github/workflows/issues.yml | 2 +- HISTORY.rst | 3 +- psutil/__init__.py | 2 +- 5 files changed, 88 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25efdbc189..8427a929f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,20 @@ # Executed on every push by GitHub Actions. This runs CI tests and -# generates wheels on the following platforms: +# generates wheels (not all) on the following platforms: # # * Linux # * macOS +# * Windows (commented) # * FreeBSD # -# Windows works as well but it's disabled (we do it via AppVeyor). # To skip certain builds see: # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip +# +# External GH actions: +# * https://github.com/actions/checkout +# * https://github.com/actions/setup-python +# * https://github.com/actions/upload-artifact +# * https://github.com/marketplace/actions/cancel-workflow-action +# * https://github.com/vmactions/freebsd-vm on: [push] name: build @@ -85,6 +92,7 @@ jobs: run: | set +e export \ + PYTHONUNBUFFERED=1 \ PYTHONWARNINGS=always \ PSUTIL_TESTING=1 \ PSUTIL_DEBUG=1 @@ -98,10 +106,12 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - name: 'Linters' + - name: 'Run linters' run: | - python -m pip install flake8 - python -m flake8 . + python2 -m pip install flake8 + python3 -m pip install flake8 + python2 -m flake8 . + python3 -m flake8 . echo "flake8 linting OK" - find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python scripts/internal/clinter.py + find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python3 scripts/internal/clinter.py echo "C linting OK" diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index c9f92d3142..91e122025d 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -10,9 +10,11 @@ on the situation. """ +import functools +import json import os import re -import sys +from pprint import pprint as pp from github import Github @@ -122,24 +124,15 @@ process, please add a comment to reopen this issue. """ +# REPLY_UPDATE_CHANGELOG = """\ +# """ -# --- utils - -def is_pr(issue): - return 'PullRequest' in issue.__module__ - - -def is_issue(issue): - return not is_pr(issue) - - -def is_new(issue): - return issue.comments == 0 +# --- github API utils -def is_comment(issue): - return not is_new(issue) +def is_pr(issue): + return issue.pull_request is not None def has_label(issue, label): @@ -161,6 +154,49 @@ def get_repo(): return Github(token).get_repo(repo) +# --- event utils + + +@functools.lru_cache() +def _get_event_data(): + ret = json.load(open(os.environ["GITHUB_EVENT_PATH"])) + pp(ret) + return ret + + +def is_event_new_issue(): + data = _get_event_data() + try: + return data['action'] == 'opened' and 'issue' in data + except KeyError: + return False + + +def is_event_new_pr(): + data = _get_event_data() + try: + return data['action'] == 'opened' and 'pull_request' in data + except KeyError: + return False + + +def is_event_new_comment(): + data = _get_event_data() + try: + return data['action'] == 'created' and 'comment' in data + except KeyError: + return False + + +def get_issue(): + data = _get_event_data() + try: + num = data['issue']['number'] + except KeyError: + num = data['pull_request']['number'] + return get_repo().get_issue(number=num) + + # --- actions @@ -275,6 +311,10 @@ def has_text(text): def on_new_pr(issue): pass + # pr = get_repo().get_pull(issue.number) + # files = [x.filename for x in list(pr.get_files())] + # if "HISTORY.rst" not in files: + # issue.create_comment(REPLY_UPDATE_CHANGELOG) def on_new_comment(issue): @@ -282,21 +322,25 @@ def on_new_comment(issue): def main(): - issue = get_repo().get_issue(number=int(sys.argv[1])) - stype = "issue" if is_issue(issue) else "PR" + issue = get_issue() + stype = "PR" if is_pr(issue) else "issue" log("running issue bot for %s %r" % (stype, issue)) - if is_new(issue): - log("new %s\n%s" % (stype, issue.body)) + if is_event_new_issue(): + log("created new issue %s" % issue) add_labels_from_text(issue, issue.title) add_labels_from_new_body(issue, issue.body) - if is_issue(issue): - on_new_issue(issue) - if is_pr(issue): - on_new_pr(issue) - else: - log("new comment: \n" % issue.body) + on_new_issue(issue) + elif is_event_new_pr(): + log("created new PR %s" % issue) + add_labels_from_text(issue, issue.title) + add_labels_from_new_body(issue, issue.body) + on_new_pr(issue) + elif is_event_new_comment(): + log("created new comment for %s" % issue) on_new_comment(issue) + else: + raise ValueError("unhandled event") if __name__ == '__main__': diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 2c2e38e329..fa739eab17 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -25,4 +25,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - python .github/workflows/issues.py ${{ github.event.issue.number }} + PYTHONUNBUFFERED=1 python .github/workflows/issues.py diff --git a/HISTORY.rst b/HISTORY.rst index 66efe586f5..cf81ddc7fa 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.8.0 (IN DEVELOPMENT) +5.8.1 (IN DEVELOPMENT) ====================== XXXX-XX-XX @@ -11,7 +11,6 @@ XXXX-XX-XX determined (instead of crashing). - 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. - 5.8.0 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index acd42ac264..488eb69469 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -209,7 +209,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.8.0" +__version__ = "5.8.1" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From a4c0a0eb0d2a872ab7a45e47fcf37ef1fde5b012 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Dec 2020 01:20:18 +0100 Subject: [PATCH 0736/1714] Rename cpu_count_physical() to cpu_count_cores() This has always been cause of confusion, e.g. see: https://github.com/giampaolo/psutil/pull/1727#issuecomment-698934643 Removed the reference to "physical" from dostrings, functions and test. I still left it in the doc though, as it's more explanatory. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 14 +++++++------- docs/index.rst | 16 ++++++++-------- psutil/__init__.py | 2 +- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 11 +++++------ psutil/_pslinux.py | 4 ++-- psutil/_psosx.py | 6 +++--- psutil/_pssunos.py | 6 +++--- psutil/_psutil_bsd.c | 4 ++-- psutil/_psutil_osx.c | 8 ++++---- psutil/_psutil_sunos.c | 8 ++++---- psutil/_psutil_windows.c | 4 ++-- psutil/_pswindows.py | 6 +++--- psutil/arch/freebsd/specific.c | 4 ++-- psutil/arch/freebsd/specific.h | 2 +- psutil/arch/windows/cpu.c | 7 +++---- psutil/arch/windows/cpu.h | 2 +- psutil/arch/windows/ntextapi.h | 2 +- psutil/tests/test_linux.py | 8 ++++---- psutil/tests/test_memleaks.py | 2 +- psutil/tests/test_osx.py | 2 +- psutil/tests/test_system.py | 16 ++++++++-------- psutil/tests/test_windows.py | 2 +- 23 files changed, 68 insertions(+), 70 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index cf81ddc7fa..9f5281f973 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -59,8 +59,8 @@ XXXX-XX-XX **Bug fixes** -- 1620_: [Linux] physical cpu_count() result is incorrect on systems with more - than one CPU socket. (patch by Vincent A. Arcila) +- 1620_: [Linux] cpu_count(logical=False) result is incorrect on systems with + more than one CPU socket. (patch by Vincent A. Arcila) - 1738_: [macOS] Process.exe() may raise FileNotFoundError if process is still alive but the exe file which launched it got deleted. - 1791_: [macOS] fix missing include for getpagesize(). @@ -256,7 +256,7 @@ XXXX-XX-XX average calculation, including on Windows (emulated). (patch by Ammar Askar) - 1404_: [Linux] cpu_count(logical=False) uses a second method (read from `/sys/devices/system/cpu/cpu[0-9]/topology/core_id`) in order to determine - the number of physical CPUs in case /proc/cpuinfo does not provide this info. + the number of CPU cores in case /proc/cpuinfo does not provide this info. - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. - 1464_: various docfixes (always point to python3 doc, fix links, etc.). - 1476_: [Windows] it is now possible to set process high I/O priority @@ -502,7 +502,7 @@ XXXX-XX-XX - 694_: [SunOS] cmdline() could be truncated at the 15th character when reading it from /proc. An extra effort is made by reading it from process address space first. (patch by Georg Sauthoff) -- 771_: [Windows] cpu_count() (both logical and physical) return a wrong +- 771_: [Windows] cpu_count() (both logical and cores) return a wrong (smaller) number on systems using process groups (> 64 cores). - 771_: [Windows] cpu_times(percpu=True) return fewer CPUs on systems using process groups (> 64 cores). @@ -1306,8 +1306,8 @@ XXXX-XX-XX - 593_: [FreeBSD] Process().memory_maps() segfaults. - 606_: Process.parent() may swallow NoSuchProcess exceptions. - 611_: [SunOS] net_io_counters has send and received swapped -- 614_: [Linux]: cpu_count(logical=False) return the number of physical CPUs - instead of physical cores. +- 614_: [Linux]: cpu_count(logical=False) return the number of sockets instead + of cores. - 618_: [SunOS] swap tests fail on Solaris when run as normal user - 628_: [Linux] Process.name() truncates process name in case it contains spaces or parentheses. @@ -1424,7 +1424,7 @@ XXXX-XX-XX **Enhancements** - 424_: [Windows] installer for Python 3.X 64 bit. -- 427_: number of logical and physical CPUs (psutil.cpu_count()). +- 427_: number of logical CPUs and physical cores (psutil.cpu_count()). - 447_: psutil.wait_procs() timeout parameter is now optional. - 452_: make Process instances hashable and usable with set()s. - 453_: tests on Python < 2.7 require unittest2 module. diff --git a/docs/index.rst b/docs/index.rst index 8c3b432b1e..d452c51ce4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -205,13 +205,13 @@ CPU Return the number of logical CPUs in the system (same as `os.cpu_count`_ in Python 3.4) or ``None`` if undetermined. - *logical* cores means the number of physical cores multiplied by the number + "logical CPUs" means the number of physical cores multiplied by the number of threads that can run on each core (this is known as Hyper Threading). - If *logical* is ``False`` return the number of physical cores only (Hyper - Thread CPUs are excluded) or ``None`` if undetermined. + If *logical* is ``False`` return the number of physical cores only, or + ``None`` if undetermined. On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return ``None``. - Example on a system having 2 physical hyper-thread CPU cores: + Example on a system having 2 cores + Hyper Threading: >>> import psutil >>> psutil.cpu_count() @@ -219,11 +219,11 @@ CPU >>> psutil.cpu_count(logical=False) 2 - Note that this number is not equivalent to the number of CPUs the current - process can actually use. + Note that ``psutil.cpu_count()`` may not necessarily be equivalent to the + actual number of CPUs the current process can use. That can vary in case process CPU affinity has been changed, Linux cgroups - are being used or on Windows systems using processor groups or having more - than 64 CPUs. + are being used or (in case of Windows) on systems using processor groups or + having more than 64 CPUs. The number of usable CPUs can be obtained with: >>> len(psutil.Process().cpu_affinity()) diff --git a/psutil/__init__.py b/psutil/__init__.py index 488eb69469..44efb7ffe3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1575,7 +1575,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 diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 7160ecd63a..3e3a3d145a 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -143,7 +143,7 @@ def cpu_count_logical(): return None -def cpu_count_physical(): +def cpu_count_cores(): cmd = "lsdev -Cc processor" p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 764463e980..1e47696a1d 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -249,19 +249,19 @@ 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_count_cores() if s is not None: # get rid of padding chars appended at the end of the string index = s.rfind("") @@ -274,8 +274,7 @@ def cpu_count_physical(): # 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 diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index be43f08b1c..3915574551 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -656,8 +656,8 @@ 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 diff --git a/psutil/_psosx.py b/psutil/_psosx.py index c7770d6591..d948cc15a8 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -159,9 +159,9 @@ 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(): diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 5618bd4460..816ebf07f3 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -190,9 +190,9 @@ 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(): diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 15b646e33c..dc157e68e1 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1096,8 +1096,8 @@ static PyMethodDef mod_methods[] = { "Get process resource limits."}, {"proc_setrlimit", psutil_proc_setrlimit, METH_VARARGS, "Set process resource limits."}, - {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, - "Return an XML string to determine the number physical CPUs."}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, + "Return an XML string to determine the number CPU cores."}, #endif {"proc_environ", psutil_proc_environ, METH_VARARGS, "Return process environment"}, diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 5720de9f7f..b5aa7653ee 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -383,10 +383,10 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { /* - * Return the number of physical CPUs in the system. + * Return the number of CPU cores in the system. */ static PyObject * -psutil_cpu_count_phys(PyObject *self, PyObject *args) { +psutil_cpu_count_cores(PyObject *self, PyObject *args) { int num; size_t size = sizeof(int); @@ -1789,8 +1789,8 @@ static PyMethodDef mod_methods[] = { "Returns a list of PIDs currently running on the system"}, {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS, "Return number of logical CPUs on the system"}, - {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, - "Return number of physical CPUs on the system"}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, + "Return number of CPU cores on the system"}, {"virtual_mem", psutil_virtual_mem, METH_VARARGS, "Return system virtual memory stats"}, {"swap_mem", psutil_swap_mem, METH_VARARGS, diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 82114c8c89..342798a882 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1437,10 +1437,10 @@ psutil_boot_time(PyObject *self, PyObject *args) { /* - * Return the number of physical CPU cores on the system. + * Return the number of CPU cores on the system. */ static PyObject * -psutil_cpu_count_phys(PyObject *self, PyObject *args) { +psutil_cpu_count_cores(PyObject *self, PyObject *args) { kstat_ctl_t *kc; kstat_t *ksp; int ncpus = 0; @@ -1669,8 +1669,8 @@ PsutilMethods[] = { "Return a Python dict of tuples for network I/O statistics."}, {"boot_time", psutil_boot_time, METH_VARARGS, "Return system boot time in seconds since the EPOCH."}, - {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, - "Return the number of physical CPUs on the system."}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, + "Return the number of CPU cores on the system."}, {"net_connections", psutil_net_connections, METH_VARARGS, "Return TCP and UDP syste-wide open connections."}, {"net_if_stats", psutil_net_if_stats, METH_VARARGS, diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index a2154e923e..93a659bf8e 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1611,8 +1611,8 @@ PsutilMethods[] = { "Determine if the process exists in the current process list."}, {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS, "Returns the number of logical CPUs on the system"}, - {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, - "Returns the number of physical CPUs on the system"}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, + "Returns the number of CPU cores on the system"}, {"boot_time", psutil_boot_time, METH_VARARGS, "Return the system boot time expressed in seconds since the epoch."}, {"virtual_mem", psutil_virtual_mem, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 98baef5955..138bc83dea 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -304,9 +304,9 @@ 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(): diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 2776de8ce2..748089cf60 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -364,9 +364,9 @@ psutil_proc_threads(PyObject *self, PyObject *args) { PyObject * -psutil_cpu_count_phys(PyObject *self, PyObject *args) { +psutil_cpu_count_cores(PyObject *self, PyObject *args) { // Return an XML string from which we'll determine the number of - // physical CPU cores in the system. + // CPU cores in the system. void *topology = NULL; size_t size = 0; PyObject *py_str; diff --git a/psutil/arch/freebsd/specific.h b/psutil/arch/freebsd/specific.h index 61c3f07b70..c579cff7aa 100644 --- a/psutil/arch/freebsd/specific.h +++ b/psutil/arch/freebsd/specific.h @@ -12,7 +12,7 @@ int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); // -PyObject* psutil_cpu_count_phys(PyObject* self, PyObject* args); +PyObject* psutil_cpu_count_cores(PyObject* self, PyObject* args); PyObject* psutil_disk_io_counters(PyObject* self, PyObject* args); PyObject* psutil_get_cmdline(long pid); PyObject* psutil_per_cpu_times(PyObject* self, PyObject* args); diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 18f32e5983..355de6df72 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -177,11 +177,10 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { /* - * Return the number of physical CPU cores (hyper-thread CPUs count - * is excluded). + * Return the number of CPU cores (non hyper-threading). */ PyObject * -psutil_cpu_count_phys(PyObject *self, PyObject *args) { +psutil_cpu_count_cores(PyObject *self, PyObject *args) { DWORD rc; PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX buffer = NULL; PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX ptr = NULL; @@ -196,7 +195,7 @@ psutil_cpu_count_phys(PyObject *self, PyObject *args) { // than 64 CPUs. See: // https://bugs.python.org/issue33166 if (GetLogicalProcessorInformationEx == NULL) { - psutil_debug("Win < 7; cpu_count_phys() forced to None"); + psutil_debug("Win < 7; cpu_count_cores() forced to None"); Py_RETURN_NONE; } diff --git a/psutil/arch/windows/cpu.h b/psutil/arch/windows/cpu.h index d88c221210..1ef3ff1f04 100644 --- a/psutil/arch/windows/cpu.h +++ b/psutil/arch/windows/cpu.h @@ -7,7 +7,7 @@ #include PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); -PyObject *psutil_cpu_count_phys(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); PyObject *psutil_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index ea1f428167..e0662fa044 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -399,7 +399,7 @@ typedef struct _WTSINFOW { #define PWTSINFO PWTSINFOW -// cpu_count_phys() +// cpu_count_cores() #if (_WIN32_WINNT < 0x0601) // Windows < 7 (Vista and XP) typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX { LOGICAL_PROCESSOR_RELATIONSHIP Relationship; diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index f8a9e00899..2f6065f596 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -715,7 +715,7 @@ def test_emulate_fallbacks(self): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUCountPhysical(PsutilTestCase): +class TestSystemCPUCountCores(PsutilTestCase): @unittest.skipIf(not which("lscpu"), "lscpu utility not available") def test_against_lscpu(self): @@ -728,9 +728,9 @@ def test_against_lscpu(self): self.assertEqual(psutil.cpu_count(logical=False), len(core_ids)) def test_method_2(self): - meth_1 = psutil._pslinux.cpu_count_physical() + meth_1 = psutil._pslinux.cpu_count_cores() with mock.patch('glob.glob', return_value=[]) as m: - meth_2 = psutil._pslinux.cpu_count_physical() + meth_2 = psutil._pslinux.cpu_count_cores() assert m.called if meth_1 is not None: self.assertEqual(meth_1, meth_2) @@ -738,7 +738,7 @@ def test_method_2(self): 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()) + self.assertIsNone(psutil._pslinux.cpu_count_cores()) assert m1.called assert m2.called diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index ab6aac6914..e6940a3070 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -347,7 +347,7 @@ def test_cpu_count(self): # logical self.execute(lambda: psutil.cpu_count(logical=True)) @fewtimes_if_linux() - def test_cpu_count_physical(self): + def test_cpu_count_cores(self): self.execute(lambda: psutil.cpu_count(logical=False)) @fewtimes_if_linux() diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 348976f88d..b7a0b088e2 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -137,7 +137,7 @@ 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): + def test_cpu_count_cores(self): num = sysctl("sysctl hw.physicalcpu") self.assertEqual(num, psutil.cpu_count(logical=False)) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 90ecff945a..4e3ac3e4ae 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -316,16 +316,16 @@ def test_cpu_count_logical(self): if "physical id" not in cpuinfo_data: raise unittest.SkipTest("cpuinfo doesn't include physical id") - def test_cpu_count_physical(self): + def test_cpu_count_cores(self): logical = psutil.cpu_count() - physical = psutil.cpu_count(logical=False) - if physical is None: - raise self.skipTest("physical cpu_count() is None") + 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(physical) + self.assertIsNone(cores) else: - self.assertGreaterEqual(physical, 1) - self.assertGreaterEqual(logical, physical) + self.assertGreaterEqual(cores, 1) + self.assertGreaterEqual(logical, cores) def test_cpu_count_none(self): # https://github.com/giampaolo/psutil/issues/1085 @@ -334,7 +334,7 @@ def test_cpu_count_none(self): return_value=val) as m: self.assertIsNone(psutil.cpu_count()) assert m.called - with mock.patch('psutil._psplatform.cpu_count_physical', + with mock.patch('psutil._psplatform.cpu_count_cores', return_value=val) as m: self.assertIsNone(psutil.cpu_count(logical=False)) assert m.called diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index a9254e2c76..aeb282c878 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -100,7 +100,7 @@ def test_cpu_count_logical_vs_wmi(self): proc = w.Win32_Processor()[0] self.assertEqual(psutil.cpu_count(), proc.NumberOfLogicalProcessors) - def test_cpu_count_phys_vs_wmi(self): + def test_cpu_count_cores_vs_wmi(self): w = wmi.WMI() proc = w.Win32_Processor()[0] self.assertEqual(psutil.cpu_count(logical=False), proc.NumberOfCores) From 03211cf6b1ec529c8e6cb4b528658547a01d645b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Dec 2020 22:53:56 +0100 Subject: [PATCH 0737/1714] Refactor macOS CPU code (#1896) --- psutil/_psutil_osx.c | 149 +----------------------------------------- psutil/arch/osx/cpu.c | 140 +++++++++++++++++++++++++++++++++++++++ psutil/arch/osx/cpu.h | 13 ++++ setup.py | 1 + 4 files changed, 156 insertions(+), 147 deletions(-) create mode 100644 psutil/arch/osx/cpu.c create mode 100644 psutil/arch/osx/cpu.h diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index b5aa7653ee..62a95774c6 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. + * Copyright (c) 2009, Jay Loden, 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. * @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -22,13 +21,7 @@ #include #include #include - #include -#include -#include -#include -#include -#include #include #include @@ -45,6 +38,7 @@ #include "_psutil_common.h" #include "_psutil_posix.h" #include "arch/osx/process_info.h" +#include "arch/osx/cpu.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -353,50 +347,6 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } -/* - * Return the number of logical CPUs in the system. - * XXX this could be shared with BSD. - */ -static PyObject * -psutil_cpu_count_logical(PyObject *self, PyObject *args) { - /* - int mib[2]; - int ncpu; - size_t len; - mib[0] = CTL_HW; - mib[1] = HW_NCPU; - len = sizeof(ncpu); - - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) - Py_RETURN_NONE; // mimic os.cpu_count() - else - return Py_BuildValue("i", ncpu); - */ - int num; - size_t size = sizeof(int); - - if (sysctlbyname("hw.logicalcpu", &num, &size, NULL, 2)) - Py_RETURN_NONE; // mimic os.cpu_count() - else - return Py_BuildValue("i", num); -} - - -/* - * Return the number of CPU cores in the system. - */ -static PyObject * -psutil_cpu_count_cores(PyObject *self, PyObject *args) { - int num; - size_t size = sizeof(int); - - if (sysctlbyname("hw.physicalcpu", &num, &size, NULL, 0)) - Py_RETURN_NONE; // mimic os.cpu_count() - else - return Py_BuildValue("i", num); -} - - /* * Indicates if the given virtual address on the given architecture is in the * shared VM region. @@ -587,36 +537,6 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } -/* - * Return a Python tuple representing user, kernel and idle CPU times - */ -static PyObject * -psutil_cpu_times(PyObject *self, PyObject *args) { - mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT; - kern_return_t error; - host_cpu_load_info_data_t r_load; - - mach_port_t host_port = mach_host_self(); - error = host_statistics(host_port, HOST_CPU_LOAD_INFO, - (host_info_t)&r_load, &count); - if (error != KERN_SUCCESS) { - return PyErr_Format( - PyExc_RuntimeError, - "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", - mach_error_string(error)); - } - mach_port_deallocate(mach_task_self(), host_port); - - return Py_BuildValue( - "(dddd)", - (double)r_load.cpu_ticks[CPU_STATE_USER] / CLK_TCK, - (double)r_load.cpu_ticks[CPU_STATE_NICE] / CLK_TCK, - (double)r_load.cpu_ticks[CPU_STATE_SYSTEM] / CLK_TCK, - (double)r_load.cpu_ticks[CPU_STATE_IDLE] / CLK_TCK - ); -} - - /* * Return a Python list of tuple representing per-cpu times */ @@ -683,40 +603,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } -/* - * Retrieve CPU frequency. Note: all of these are static vendor values - * that never change. - */ -static PyObject * -psutil_cpu_freq(PyObject *self, PyObject *args) { - unsigned int curr; - int64_t min = 0; - int64_t max = 0; - int mib[2]; - size_t len = sizeof(curr); - size_t size = sizeof(min); - - // also availble as "hw.cpufrequency" but it's deprecated - mib[0] = CTL_HW; - mib[1] = HW_CPU_FREQ; - - if (sysctl(mib, 2, &curr, &len, NULL, 0) < 0) - return PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); - - if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) - psutil_debug("sysct('hw.cpufrequency_min') failed (set to 0)"); - - if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) - psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); - - return Py_BuildValue( - "IKK", - curr / 1000 / 1000, - min / 1000 / 1000, - max / 1000 / 1000); -} - - /* * Return a Python float indicating the system boot time expressed in * seconds since the epoch. @@ -1634,37 +1520,6 @@ psutil_users(PyObject *self, PyObject *args) { } -/* - * Return CPU statistics. - */ -static PyObject * -psutil_cpu_stats(PyObject *self, PyObject *args) { - struct vmmeter vmstat; - kern_return_t ret; - mach_msg_type_number_t count = sizeof(vmstat) / sizeof(integer_t); - mach_port_t mport = mach_host_self(); - - ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); - if (ret != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, - "host_statistics(HOST_VM_INFO) failed: %s", - mach_error_string(ret)); - return NULL; - } - mach_port_deallocate(mach_task_self(), mport); - - return Py_BuildValue( - "IIIII", - vmstat.v_swtch, // ctx switches - vmstat.v_intr, // interrupts - vmstat.v_soft, // software interrupts - vmstat.v_syscall, // syscalls - vmstat.v_trap // traps - ); -} - - /* * Return battery information. */ diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c new file mode 100644 index 0000000000..37141a2d31 --- /dev/null +++ b/psutil/arch/osx/cpu.c @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +/* +System-wide CPU related functions. + +Original code was refactored and moved from psutil/_psutil_osx.c in 2020 +right before a4c0a0eb0d2a872ab7a45e47fcf37ef1fde5b012. +For reference, here's the git history with original implementations: + +- CPU count logical: 3d291d425b856077e65163e43244050fb188def1 +- CPU count physical: 4263e354bb4984334bc44adf5dd2f32013d69fba +- CPU times: 32488bdf54aed0f8cef90d639c1667ffaa3c31c7 +- CPU stat: fa00dfb961ef63426c7818899340866ced8d2418 +- CPU frequency: 6ba1ac4ebfcd8c95fca324b15606ab0ec1412d39 +*/ + +#include +#include +#include + +#include +#include +#include + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" + + + +PyObject * +psutil_cpu_count_logical(PyObject *self, PyObject *args) { + int num; + size_t size = sizeof(int); + + if (sysctlbyname("hw.logicalcpu", &num, &size, NULL, 2)) + Py_RETURN_NONE; // mimic os.cpu_count() + else + return Py_BuildValue("i", num); +} + + +PyObject * +psutil_cpu_count_cores(PyObject *self, PyObject *args) { + int num; + size_t size = sizeof(int); + + if (sysctlbyname("hw.physicalcpu", &num, &size, NULL, 0)) + Py_RETURN_NONE; // mimic os.cpu_count() + else + return Py_BuildValue("i", num); +} + + +PyObject * +psutil_cpu_times(PyObject *self, PyObject *args) { + mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT; + kern_return_t error; + host_cpu_load_info_data_t r_load; + + mach_port_t host_port = mach_host_self(); + error = host_statistics(host_port, HOST_CPU_LOAD_INFO, + (host_info_t)&r_load, &count); + if (error != KERN_SUCCESS) { + return PyErr_Format( + PyExc_RuntimeError, + "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", + mach_error_string(error)); + } + mach_port_deallocate(mach_task_self(), host_port); + + return Py_BuildValue( + "(dddd)", + (double)r_load.cpu_ticks[CPU_STATE_USER] / CLK_TCK, + (double)r_load.cpu_ticks[CPU_STATE_NICE] / CLK_TCK, + (double)r_load.cpu_ticks[CPU_STATE_SYSTEM] / CLK_TCK, + (double)r_load.cpu_ticks[CPU_STATE_IDLE] / CLK_TCK + ); +} + + +PyObject * +psutil_cpu_stats(PyObject *self, PyObject *args) { + struct vmmeter vmstat; + kern_return_t ret; + mach_msg_type_number_t count = sizeof(vmstat) / sizeof(integer_t); + mach_port_t mport = mach_host_self(); + + ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); + if (ret != KERN_SUCCESS) { + PyErr_Format( + PyExc_RuntimeError, + "host_statistics(HOST_VM_INFO) failed: %s", + mach_error_string(ret)); + return NULL; + } + mach_port_deallocate(mach_task_self(), mport); + + return Py_BuildValue( + "IIIII", + vmstat.v_swtch, // ctx switches + vmstat.v_intr, // interrupts + vmstat.v_soft, // software interrupts + vmstat.v_syscall, // syscalls + vmstat.v_trap // traps + ); +} + + +PyObject * +psutil_cpu_freq(PyObject *self, PyObject *args) { + unsigned int curr; + int64_t min = 0; + int64_t max = 0; + int mib[2]; + size_t len = sizeof(curr); + size_t size = sizeof(min); + + // also availble as "hw.cpufrequency" but it's deprecated + mib[0] = CTL_HW; + mib[1] = HW_CPU_FREQ; + + if (sysctl(mib, 2, &curr, &len, NULL, 0) < 0) + return PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); + + if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) + psutil_debug("sysct('hw.cpufrequency_min') failed (set to 0)"); + + if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) + psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); + + return Py_BuildValue( + "IKK", + curr / 1000 / 1000, + min / 1000 / 1000, + max / 1000 / 1000); +} diff --git a/psutil/arch/osx/cpu.h b/psutil/arch/osx/cpu.h new file mode 100644 index 0000000000..aac0f809b1 --- /dev/null +++ b/psutil/arch/osx/cpu.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index 90c79c7c76..fcf89ae62a 100755 --- a/setup.py +++ b/setup.py @@ -189,6 +189,7 @@ def get_winver(): sources=sources + [ 'psutil/_psutil_osx.c', 'psutil/arch/osx/process_info.c', + 'psutil/arch/osx/cpu.c', ], define_macros=macros, extra_link_args=[ From 88a017345616cacf718c76980821d26b448f730a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Dec 2020 14:33:33 -0800 Subject: [PATCH 0738/1714] rename C fun --- psutil/_psutil_windows.c | 2 +- psutil/_pswindows.py | 2 +- psutil/arch/windows/disk.c | 2 +- psutil/arch/windows/disk.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 93a659bf8e..7a3ab329b1 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1666,7 +1666,7 @@ PsutilMethods[] = { "Stop a service"}, // --- windows API bindings - {"win32_QueryDosDevice", psutil_win32_QueryDosDevice, METH_VARARGS, + {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS, "QueryDosDevice binding"}, // --- others diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 138bc83dea..0ad60c4aea 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -197,7 +197,7 @@ def convert_dos_path(s): "C:\Windows\systemew\file.txt" """ rawdrive = '\\'.join(s.split('\\')[:3]) - driveletter = cext.win32_QueryDosDevice(rawdrive) + driveletter = cext.QueryDosDevice(rawdrive) remainder = s[len(rawdrive):] return os.path.join(driveletter, remainder) diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 92171fe59b..29bd0a20c5 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -364,7 +364,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { If no match is found return an empty string. */ PyObject * -psutil_win32_QueryDosDevice(PyObject *self, PyObject *args) { +psutil_QueryDosDevice(PyObject *self, PyObject *args) { LPCTSTR lpDevicePath; TCHAR d = TEXT('A'); TCHAR szBuff[5]; diff --git a/psutil/arch/windows/disk.h b/psutil/arch/windows/disk.h index 298fb6ba0e..28bed22b54 100644 --- a/psutil/arch/windows/disk.h +++ b/psutil/arch/windows/disk.h @@ -9,4 +9,4 @@ PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); PyObject *psutil_disk_usage(PyObject *self, PyObject *args); -PyObject *psutil_win32_QueryDosDevice(PyObject *self, PyObject *args); +PyObject *psutil_QueryDosDevice(PyObject *self, PyObject *args); From d4e56d9ea6a411b7cadd89e45ce2d7f9286a10b3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 22 Dec 2020 18:18:31 +0100 Subject: [PATCH 0739/1714] make clean: add -v opt Signed-off-by: Giampaolo Rodola --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d8ee5edc6c..087ddd7853 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ all: test # =================================================================== clean: ## Remove all build files. - rm -rf `find . -type d -name __pycache__ \ + @rm -rfv `find . -type d -name __pycache__ \ -o -type f -name \*.bak \ -o -type f -name \*.orig \ -o -type f -name \*.pyc \ @@ -57,7 +57,7 @@ clean: ## Remove all build files. -o -type f -name \*.so \ -o -type f -name \*.~ \ -o -type f -name \*\$testfn` - rm -rf \ + @rm -rfv \ *.core \ *.egg-info \ *\@psutil-* \ From 8f7826cb7d6ce35ecb2a681a4fca09fb5a71668f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 24 Dec 2020 00:15:24 +0000 Subject: [PATCH 0740/1714] FreeBSD: move CPU C functions in their own cpu.c module --- MANIFEST.in | 4 + psutil/_psbsd.py | 2 +- psutil/_psutil_bsd.c | 5 +- psutil/arch/freebsd/cpu.c | 130 +++++++++++++++++++++++++++++++++ psutil/arch/freebsd/cpu.h | 11 +++ psutil/arch/freebsd/specific.c | 109 --------------------------- psutil/arch/freebsd/specific.h | 14 +--- setup.py | 1 + 8 files changed, 154 insertions(+), 122 deletions(-) create mode 100644 psutil/arch/freebsd/cpu.c create mode 100644 psutil/arch/freebsd/cpu.h diff --git a/MANIFEST.in b/MANIFEST.in index d651cd4e2d..e9c20d8139 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -48,6 +48,8 @@ include psutil/arch/aix/ifaddrs.h include psutil/arch/aix/net_connections.c include psutil/arch/aix/net_connections.h include psutil/arch/aix/net_kernel_structs.h +include psutil/arch/freebsd/cpu.c +include psutil/arch/freebsd/cpu.h include psutil/arch/freebsd/proc_socks.c include psutil/arch/freebsd/proc_socks.h include psutil/arch/freebsd/specific.c @@ -60,6 +62,8 @@ include psutil/arch/netbsd/specific.c include psutil/arch/netbsd/specific.h include psutil/arch/openbsd/specific.c include psutil/arch/openbsd/specific.h +include psutil/arch/osx/cpu.c +include psutil/arch/osx/cpu.h include psutil/arch/osx/process_info.c include psutil/arch/osx/process_info.h include psutil/arch/solaris/environ.c diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 1e47696a1d..bdcfc1e6e6 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -261,7 +261,7 @@ def cpu_count_cores(): # 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_cores() + s = cext.cpu_topology() if s is not None: # get rid of padding chars appended at the end of the string index = s.rfind("") diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index dc157e68e1..69ce6e8e45 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -63,6 +63,7 @@ #include "_psutil_posix.h" #ifdef PSUTIL_FREEBSD + #include "arch/freebsd/cpu.h" #include "arch/freebsd/specific.h" #include "arch/freebsd/sys_socks.h" #include "arch/freebsd/proc_socks.h" @@ -1096,8 +1097,8 @@ static PyMethodDef mod_methods[] = { "Get process resource limits."}, {"proc_setrlimit", psutil_proc_setrlimit, METH_VARARGS, "Set process resource limits."}, - {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, - "Return an XML string to determine the number CPU cores."}, + {"cpu_topology", psutil_cpu_topology, METH_VARARGS, + "Return CPU topology as an XML string."}, #endif {"proc_environ", psutil_proc_environ, METH_VARARGS, "Return process environment"}, diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c new file mode 100644 index 0000000000..f31e9bb0e1 --- /dev/null +++ b/psutil/arch/freebsd/cpu.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +/* +System-wide CPU related functions. +Original code was refactored and moved from psutil/arch/freebsd/specific.c +in 2020 (and was moved in there previously already) from cset. +a4c0a0eb0d2a872ab7a45e47fcf37ef1fde5b012 +For reference, here's the git history with original(ish) implementations: +- CPU stats: fb0154ef164d0e5942ac85102ab660b8d2938fbb +- CPU freq: 459556dd1e2979cdee22177339ced0761caf4c83 +- CPU cores: e0d6d7865df84dc9a1d123ae452fd311f79b1dde +*/ + + +#include +#include + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" + + +PyObject * +psutil_cpu_topology(PyObject *self, PyObject *args) { + void *topology = NULL; + size_t size = 0; + PyObject *py_str; + + if (sysctlbyname("kern.sched.topology_spec", NULL, &size, NULL, 0)) + goto error; + + topology = malloc(size); + if (!topology) { + PyErr_NoMemory(); + return NULL; + } + + if (sysctlbyname("kern.sched.topology_spec", topology, &size, NULL, 0)) + goto error; + + py_str = Py_BuildValue("s", topology); + free(topology); + return py_str; + +error: + if (topology != NULL) + free(topology); + Py_RETURN_NONE; +} + + +PyObject * +psutil_cpu_stats(PyObject *self, PyObject *args) { + unsigned int v_soft; + unsigned int v_intr; + unsigned int v_syscall; + unsigned int v_trap; + unsigned int v_swtch; + size_t size = sizeof(v_soft); + + if (sysctlbyname("vm.stats.sys.v_soft", &v_soft, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_soft')"); + } + if (sysctlbyname("vm.stats.sys.v_intr", &v_intr, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_intr')"); + } + if (sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_syscall')"); + } + if (sysctlbyname("vm.stats.sys.v_trap", &v_trap, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_trap')"); + } + if (sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.sys.v_swtch')"); + } + + return Py_BuildValue( + "IIIII", + v_swtch, // ctx switches + v_intr, // interrupts + v_soft, // software interrupts + v_syscall, // syscalls + v_trap // traps + ); +} + + +/* + * Return frequency information of a given CPU. + * As of Dec 2018 only CPU 0 appears to be supported and all other + * cores match the frequency of CPU 0. + */ +PyObject * +psutil_cpu_freq(PyObject *self, PyObject *args) { + int current; + int core; + char sensor[26]; + char available_freq_levels[1000]; + size_t size = sizeof(current); + + if (! PyArg_ParseTuple(args, "i", &core)) + return NULL; + // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ + sprintf(sensor, "dev.cpu.%d.freq", core); + if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) + goto error; + + size = sizeof(available_freq_levels); + // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ + // In case of failure, an empty string is returned. + sprintf(sensor, "dev.cpu.%d.freq_levels", core); + sysctlbyname(sensor, &available_freq_levels, &size, NULL, 0); + + return Py_BuildValue("is", current, available_freq_levels); + +error: + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "unable to read frequency"); + else + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} diff --git a/psutil/arch/freebsd/cpu.h b/psutil/arch/freebsd/cpu.h new file mode 100644 index 0000000000..8decd77323 --- /dev/null +++ b/psutil/arch/freebsd/cpu.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject* psutil_cpu_freq(PyObject* self, PyObject* args); +PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); +PyObject* psutil_cpu_topology(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c index 748089cf60..423f0c7b1e 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/specific.c @@ -363,37 +363,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -PyObject * -psutil_cpu_count_cores(PyObject *self, PyObject *args) { - // Return an XML string from which we'll determine the number of - // CPU cores in the system. - void *topology = NULL; - size_t size = 0; - PyObject *py_str; - - if (sysctlbyname("kern.sched.topology_spec", NULL, &size, NULL, 0)) - goto error; - - topology = malloc(size); - if (!topology) { - PyErr_NoMemory(); - return NULL; - } - - if (sysctlbyname("kern.sched.topology_spec", topology, &size, NULL, 0)) - goto error; - - py_str = Py_BuildValue("s", topology); - free(topology); - return py_str; - -error: - if (topology != NULL) - free(topology); - Py_RETURN_NONE; -} - - /* * Return virtual memory usage statistics. */ @@ -932,47 +901,6 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { } -PyObject * -psutil_cpu_stats(PyObject *self, PyObject *args) { - unsigned int v_soft; - unsigned int v_intr; - unsigned int v_syscall; - unsigned int v_trap; - unsigned int v_swtch; - size_t size = sizeof(v_soft); - - if (sysctlbyname("vm.stats.sys.v_soft", &v_soft, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_soft')"); - } - if (sysctlbyname("vm.stats.sys.v_intr", &v_intr, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_intr')"); - } - if (sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_syscall')"); - } - if (sysctlbyname("vm.stats.sys.v_trap", &v_trap, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_trap')"); - } - if (sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_swtch')"); - } - - return Py_BuildValue( - "IIIII", - v_swtch, // ctx switches - v_intr, // interrupts - v_soft, // software interrupts - v_syscall, // syscalls - v_trap // traps - ); -} - - /* * Return battery information. */ @@ -1036,43 +964,6 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { } -/* - * Return frequency information of a given CPU. - * As of Dec 2018 only CPU 0 appears to be supported and all other - * cores match the frequency of CPU 0. - */ -PyObject * -psutil_cpu_freq(PyObject *self, PyObject *args) { - int current; - int core; - char sensor[26]; - char available_freq_levels[1000]; - size_t size = sizeof(current); - - if (! PyArg_ParseTuple(args, "i", &core)) - return NULL; - // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ - sprintf(sensor, "dev.cpu.%d.freq", core); - if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) - goto error; - - size = sizeof(available_freq_levels); - // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ - // In case of failure, an empty string is returned. - sprintf(sensor, "dev.cpu.%d.freq_levels", core); - sysctlbyname(sensor, &available_freq_levels, &size, NULL, 0); - - return Py_BuildValue("is", current, available_freq_levels); - -error: - if (errno == ENOENT) - PyErr_SetString(PyExc_NotImplementedError, "unable to read frequency"); - else - PyErr_SetFromErrno(PyExc_OSError); - return NULL; -} - - /* * An emulation of Linux prlimit(). Returns a (soft, hard) tuple. */ diff --git a/psutil/arch/freebsd/specific.h b/psutil/arch/freebsd/specific.h index c579cff7aa..57f0a2a4c2 100644 --- a/psutil/arch/freebsd/specific.h +++ b/psutil/arch/freebsd/specific.h @@ -11,8 +11,6 @@ typedef struct kinfo_proc kinfo_proc; int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); -// -PyObject* psutil_cpu_count_cores(PyObject* self, PyObject* args); PyObject* psutil_disk_io_counters(PyObject* self, PyObject* args); PyObject* psutil_get_cmdline(long pid); PyObject* psutil_per_cpu_times(PyObject* self, PyObject* args); @@ -20,17 +18,13 @@ PyObject* psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args); PyObject* psutil_proc_cpu_affinity_set(PyObject* self, PyObject* args); PyObject* psutil_proc_cwd(PyObject* self, PyObject* args); PyObject* psutil_proc_exe(PyObject* self, PyObject* args); +PyObject* psutil_proc_getrlimit(PyObject* self, PyObject* args); PyObject* psutil_proc_memory_maps(PyObject* self, PyObject* args); PyObject* psutil_proc_num_fds(PyObject* self, PyObject* args); PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); -PyObject* psutil_proc_threads(PyObject* self, PyObject* args); -PyObject* psutil_proc_getrlimit(PyObject* self, PyObject* args); PyObject* psutil_proc_setrlimit(PyObject* self, PyObject* args); -PyObject* psutil_swap_mem(PyObject* self, PyObject* args); -PyObject* psutil_virtual_mem(PyObject* self, PyObject* args); -PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); -#if defined(PSUTIL_FREEBSD) +PyObject* psutil_proc_threads(PyObject* self, PyObject* args); PyObject* psutil_sensors_battery(PyObject* self, PyObject* args); PyObject* psutil_sensors_cpu_temperature(PyObject* self, PyObject* args); -PyObject* psutil_cpu_freq(PyObject* self, PyObject* args); -#endif +PyObject* psutil_swap_mem(PyObject* self, PyObject* args); +PyObject* psutil_virtual_mem(PyObject* self, PyObject* args); diff --git a/setup.py b/setup.py index fcf89ae62a..16b476f26d 100755 --- a/setup.py +++ b/setup.py @@ -202,6 +202,7 @@ def get_winver(): 'psutil._psutil_bsd', sources=sources + [ 'psutil/_psutil_bsd.c', + 'psutil/arch/freebsd/cpu.c', 'psutil/arch/freebsd/specific.c', 'psutil/arch/freebsd/sys_socks.c', 'psutil/arch/freebsd/proc_socks.c', From a7520d5e27c99dc9f70e74e2f34cb90f0e5c2337 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 24 Dec 2020 23:31:57 +0100 Subject: [PATCH 0741/1714] give CREDITS to @dbwiddis for becoming a GH sponsor (thanks ;)) Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ README.rst | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CREDITS b/CREDITS index 9ab16c6e13..7e7ce95447 100644 --- a/CREDITS +++ b/CREDITS @@ -95,6 +95,10 @@ I: 557 Donations ------------------------------------------------------------------------------- +N: Daniel Widdis +C: Washington, USA +W: https://github.com/dbwiddis + N: Rodion Stratov C: Canada diff --git a/README.rst b/README.rst index 8486cda761..83e79aabc9 100644 --- a/README.rst +++ b/README.rst @@ -130,16 +130,17 @@ Sponsors Supporters ========== -None yet. - .. raw:: html +
+ +
add your avatar Contributing ============ -See `CONTRIBUTING.md `__ guidelines. +See `contributing guidelines `__. Example usages ============== From f5621b6eb353d675e25b3aeb6194a6ff18a4268a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 27 Dec 2020 12:04:28 +0100 Subject: [PATCH 0742/1714] update doc Signed-off-by: Giampaolo Rodola --- README.rst | 5 ++--- docs/DEVNOTES | 6 ------ docs/index.rst | 6 ++++-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 83e79aabc9..62b2a7d164 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ :alt: Code quality .. |github-actions| image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD - :target: https://github.com/giampaolo/psutil/actions?query=workflow%3ACI + :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild :alt: Linux, macOS, Windows tests .. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows @@ -35,7 +35,7 @@ :alt: Test coverage (coverall.io) .. |doc| image:: https://readthedocs.org/projects/psutil/badge/?version=latest - :target: http://psutil.readthedocs.io/en/latest/?badge=latest + :target: https://psutil.readthedocs.io/en/latest/ :alt: Documentation Status .. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi @@ -43,7 +43,6 @@ :alt: Latest version .. |py-versions| image:: https://img.shields.io/pypi/pyversions/psutil.svg - :target: https://pypi.org/project/psutil :alt: Supported Python versions .. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 7fe14f7d0b..915754d67a 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -34,18 +34,12 @@ FEATURES - #772: extended net_io_counters() metrics. -- #900: wheels for macOS and Linux. - - #922: extended net_io_stats() info. - #914: extended platform specific process info. - #898: wifi stats -- #893: (BSD) process environ - -- #809: (BSD) per-process resource limits (rlimit()). - - (UNIX) process root (different from cwd) - #782: (UNIX) process num of signals received. diff --git a/docs/index.rst b/docs/index.rst index d452c51ce4..5aae7201ba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,10 +73,12 @@ Sponsors Supporters ---------- -None yet. - .. raw:: html +
+ +
+
add your avatar Install From cb666693389ca50e86406c03f36c736702ee1dcf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 27 Dec 2020 13:40:23 +0100 Subject: [PATCH 0743/1714] remove win dead code Signed-off-by: Giampaolo Rodola --- psutil/_psutil_windows.c | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 7a3ab329b1..13cf58c482 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -43,35 +43,6 @@ static PyObject *TimeoutExpired; static PyObject *TimeoutAbandoned; -/* - * Return the number of logical, active CPUs. Return 0 if undetermined. - * See discussion at: https://bugs.python.org/issue33166#msg314631 - */ -unsigned int -psutil_get_num_cpus(int fail_on_err) { - unsigned int ncpus = 0; - - // Minimum requirement: Windows 7 - if (GetActiveProcessorCount != NULL) { - ncpus = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); - if ((ncpus == 0) && (fail_on_err == 1)) { - PyErr_SetFromWindowsErr(0); - } - } - else { - psutil_debug("GetActiveProcessorCount() not available; " - "using GetSystemInfo()"); - ncpus = (unsigned int)PSUTIL_SYSTEM_INFO.dwNumberOfProcessors; - if ((ncpus <= 0) && (fail_on_err == 1)) { - PyErr_SetString( - PyExc_RuntimeError, - "GetSystemInfo() failed to retrieve CPU count"); - } - } - return ncpus; -} - - /* * Return a Python float representing the system uptime expressed in seconds * since the epoch. From 8687a11697865febb033edcac7d130c87425464e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 28 Dec 2020 14:44:55 +0100 Subject: [PATCH 0744/1714] give CREDITS to @aristocratos for new sponsorship (thank you ;)) Signed-off-by: Giampaolo Rodola --- CREDITS | 3 +++ README.rst | 4 +++- docs/index.rst | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index 7e7ce95447..e1d5d782d1 100644 --- a/CREDITS +++ b/CREDITS @@ -95,6 +95,9 @@ I: 557 Donations ------------------------------------------------------------------------------- +N: aristocratos +W: https://github.com/aristocratos + N: Daniel Widdis C: Washington, USA W: https://github.com/dbwiddis diff --git a/README.rst b/README.rst index 62b2a7d164..2631e0590e 100644 --- a/README.rst +++ b/README.rst @@ -132,10 +132,12 @@ Supporters .. raw:: html
- + +
add your avatar + Contributing ============ diff --git a/docs/index.rst b/docs/index.rst index 5aae7201ba..9a516e0daf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -76,7 +76,8 @@ Supporters .. raw:: html
- + +

add your avatar From d8a8a85997bd59b720dd7456ebc1835d0e9dcd9b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 29 Dec 2020 02:08:48 +0100 Subject: [PATCH 0745/1714] Fix #1512 proc connections() fails with EOPNOTSUPP ...on macOS. Just occurs sometimes for 1 socket only. Ignore the error and continue. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/_psutil_osx.c | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9f5281f973..a33f5eb30f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 1456_: [macOS] psutil.cpu_freq()'s min and max are set to 0 if can't be determined (instead of crashing). +- 1512_: [macOS] sometimes Process.connections() will crash with EOPNOTSUPP + for one connection; this is now ignored. - 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. 5.8.0 diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 62a95774c6..5a77de1438 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1027,9 +1027,18 @@ psutil_proc_connections(PyObject *self, PyObject *args) { PROC_PIDFDSOCKETINFO, &si, sizeof(si)); // --- errors checking - if ((nb <= 0) || (nb < sizeof(si))) { + if ((nb <= 0) || (nb < sizeof(si)) || (errno != 0)) { if (errno == EBADF) { // let's assume socket has been closed + psutil_debug("proc_pidfdinfo(PROC_PIDFDSOCKETINFO) -> " + "EBADF (ignored)"); + continue; + } + else if (errno == EOPNOTSUPP) { + // may happen sometimes, see: + // https://github.com/giampaolo/psutil/issues/1512 + psutil_debug("proc_pidfdinfo(PROC_PIDFDSOCKETINFO) -> " + "EOPNOTSUPP (ignored)"); continue; } else { @@ -1063,11 +1072,6 @@ psutil_proc_connections(PyObject *self, PyObject *args) { if (inseq == 0) continue; - if (errno != 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - if ((family == AF_INET) || (family == AF_INET6)) { if (family == AF_INET) { inet_ntop(AF_INET, From e9e419727aa7470d5f97d146a50702fb211d6467 Mon Sep 17 00:00:00 2001 From: Andre F de Miranda Date: Tue, 29 Dec 2020 13:22:28 +1100 Subject: [PATCH 0746/1714] Fix typo in index.rst (#1899) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 9a516e0daf..45ca33a0ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2416,7 +2416,7 @@ Kill process tree timeout=None, on_terminate=None): """Kill a process tree (including grandchildren) with signal "sig" and return a (gone, still_alive) tuple. - "on_terminate", if specified, is a callabck function which is + "on_terminate", if specified, is a callback function which is called as soon as a child terminates. """ assert pid != os.getpid(), "won't kill myself" From b875a45e5caa147604fb499a52f9c09551f17729 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 29 Dec 2020 13:53:01 +0100 Subject: [PATCH 0747/1714] update doc Signed-off-by: Giampaolo Rodola --- docs/index.rst | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 9a516e0daf..6d38c3b02f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -85,19 +85,9 @@ Supporters Install ======= -Linux Ubuntu / Debian:: +On Linux, Windows, macOS:: - sudo apt-get install gcc python3-dev - sudo pip3 install psutil - -Linux Redhat:: - - sudo yum install gcc python3-devel - sudo pip3 install psutil - -Windows:: - - pip3 install psutil + pip install psutil For other platforms see more detailed `install `_ From f18438d135c12f7eb186f49622e0f6683c37f7f5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 30 Dec 2020 00:37:39 +0100 Subject: [PATCH 0748/1714] Linux, cpu_freq(): diminish os.stat() calls a bit Micro optimization in reference to #1852 and #1851. Use glob.glob(), which internally relies on os.scandir() in order to list /sys/devices/system/cpu/cpufreq files. In doing so, we avoid os.path.exists() for each CPU, which internally uses os.stat(). Signed-off-by: Giampaolo Rodola --- psutil/_pslinux.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 3915574551..ff8d924767 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -726,19 +726,12 @@ def cpu_freq(): Contrarily to other OSes, Linux updates these values in real-time. """ - def get_path(num): - for p in ("/sys/devices/system/cpu/cpufreq/policy%s" % num, - "/sys/devices/system/cpu/cpu%s/cpufreq" % num): - if os.path.exists(p): - return p - + paths = sorted( + glob.glob("/sys/devices/system/cpu/cpufreq/policy[0-9]*") or + glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq")) ret = [] - for n in range(cpu_count_logical()): - path = get_path(n) - if not path: - continue - - pjoin = os.path.join + pjoin = os.path.join + for path in paths: curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) if curr is None: # Likely an old RedHat, see: From 51eb1dae7bf96dcc7dae51641d5770fd0d99d0ac Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 30 Dec 2020 10:57:35 +0100 Subject: [PATCH 0749/1714] provide debug info in case of error for getloadavg() on Windows. See: https://github.com/nicolargo/glances/issues/1780 Signed-off-by: Giampaolo Rodola --- psutil/arch/windows/wmi.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index f9a847d3bf..5fad4053ea 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -64,22 +64,28 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { HANDLE event; HANDLE waitHandle; - if ((PdhOpenQueryW(NULL, 0, &hQuery)) != ERROR_SUCCESS) - goto error; + if ((PdhOpenQueryW(NULL, 0, &hQuery)) != ERROR_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "PdhOpenQueryW failed"); + return NULL; + } s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); - if (s != ERROR_SUCCESS) - goto error; + if (s != ERROR_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "PdhAddEnglishCounterW failed"); + return NULL; + } event = CreateEventW(NULL, FALSE, FALSE, L"LoadUpdateEvent"); if (event == NULL) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("CreateEventW"); return NULL; } s = PdhCollectQueryDataEx(hQuery, SAMPLING_INTERVAL, event); - if (s != ERROR_SUCCESS) - goto error; + if (s != ERROR_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "PdhCollectQueryDataEx failed"); + return NULL; + } ret = RegisterWaitForSingleObject( &waitHandle, @@ -91,15 +97,11 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { WT_EXECUTEDEFAULT); if (ret == 0) { - PyErr_SetFromWindowsErr(0); + PyErr_SetFromOSErrnoWithSyscall("RegisterWaitForSingleObject"); return NULL; } Py_RETURN_NONE; - -error: - PyErr_SetFromWindowsErr(0); - return NULL; } From fd77b0cd0e79c8f0f61969a4c2eb46d6d5c18f4c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Jan 2021 15:10:55 +0100 Subject: [PATCH 0750/1714] fix issue labeler issue Signed-off-by: Giampaolo Rodola --- .github/workflows/issues.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 91e122025d..964e519285 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -180,14 +180,6 @@ def is_event_new_pr(): return False -def is_event_new_comment(): - data = _get_event_data() - try: - return data['action'] == 'created' and 'comment' in data - except KeyError: - return False - - def get_issue(): data = _get_event_data() try: @@ -317,10 +309,6 @@ def on_new_pr(issue): # issue.create_comment(REPLY_UPDATE_CHANGELOG) -def on_new_comment(issue): - pass - - def main(): issue = get_issue() stype = "PR" if is_pr(issue) else "issue" @@ -336,11 +324,8 @@ def main(): add_labels_from_text(issue, issue.title) add_labels_from_new_body(issue, issue.body) on_new_pr(issue) - elif is_event_new_comment(): - log("created new comment for %s" % issue) - on_new_comment(issue) else: - raise ValueError("unhandled event") + log("unhandled event") if __name__ == '__main__': From f95276d744dee12a237d1a3f2836f36340c6abc0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Jan 2021 16:01:05 +0100 Subject: [PATCH 0751/1714] #1901 / macOS: better err msg on proc_pidinfo() Signed-off-by: Giampaolo Rodola --- psutil/arch/osx/process_info.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index fb9f24ffac..15ad3b89c4 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -370,14 +370,21 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { /* * A wrapper around proc_pidinfo(). + * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c. * Returns 0 on failure (and Python exception gets already set). */ int psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { errno = 0; - int ret = proc_pidinfo(pid, flavor, arg, pti, size); - if ((ret <= 0) || ((unsigned long)ret < sizeof(pti))) { - psutil_raise_for_pid(pid, "proc_pidinfo()"); + int ret; + + ret = proc_pidinfo(pid, flavor, arg, pti, size); + if (ret <= 0) { + psutil_raise_for_pid(pid, "proc_pidinfo() failed"); + return 0; + } + else if ((unsigned long )ret < sizeof(pti)) { + psutil_raise_for_pid(pid, "proc_pidinfo() len mismatch"); return 0; } return ret; From 6e494bd4024c3d25769688b53e95942e529392d6 Mon Sep 17 00:00:00 2001 From: marxin Date: Thu, 7 Jan 2021 12:04:51 +0100 Subject: [PATCH 0752/1714] Speed up cpu_frequncy() on Linux systems (#1851) (#1852) The change is about using /proc/cpuinfo when available. It provides cached values for frequencies and one can fill up minimum and maximum frequency from /sys/devices/system/cpu/cpufreq/policy/* sub-system (which is fast). Fixes #1851. --- psutil/_pslinux.py | 37 +++++++++++++++++++++---------------- psutil/tests/test_linux.py | 4 ---- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ff8d924767..640a0f3de3 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -719,6 +719,17 @@ def cpu_stats(): 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/policy0") or \ os.path.exists("/sys/devices/system/cpu/cpu0/cpufreq"): def cpu_freq(): @@ -726,13 +737,19 @@ def cpu_freq(): Contrarily to other OSes, Linux updates these values in real-time. """ + cpuinfo_freqs = _cpu_get_cpuinfo_freq() paths = sorted( glob.glob("/sys/devices/system/cpu/cpufreq/policy[0-9]*") or glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq")) ret = [] pjoin = os.path.join - for path in paths: - 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] + else: + curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) if curr is None: # Likely an old RedHat, see: # https://github.com/giampaolo/psutil/issues/1071 @@ -746,24 +763,12 @@ def cpu_freq(): ret.append(_common.scpufreq(curr, min_, max_)) return ret -elif os.path.exists("/proc/cpuinfo"): +else: def cpu_freq(): """Alternate implementation using /proc/cpuinfo. min and max frequencies are not available and are set to None. """ - ret = [] - with open_binary('%s/cpuinfo' % get_procfs_path()) as f: - for line in f: - if line.lower().startswith(b'cpu mhz'): - key, value = line.split(b':', 1) - ret.append(_common.scpufreq(float(value), 0., 0.)) - return ret - -else: - def cpu_freq(): - """Dummy implementation when none of the above files are present. - """ - return [] + return [_common.scpufreq(x, 0., 0.) for x in _cpu_get_cpuinfo_freq()] # ===================================================================== diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 2f6065f596..0c6d498c81 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -768,18 +768,14 @@ 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): From ede5e46dbee0c0f6c33f80629f75aecc1ad4143c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Jan 2021 12:18:59 +0100 Subject: [PATCH 0753/1714] give CREDITS to @marxin for #1851 and #1852 Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ HISTORY.rst | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/CREDITS b/CREDITS index e1d5d782d1..451b7a4d61 100644 --- a/CREDITS +++ b/CREDITS @@ -732,3 +732,7 @@ I: 1620, 1727 N: Tim Schlueter W: https://github.com/modelrockettier I: 1822 + +N: marxin +W: https://github.com/marxin +I: 1851 diff --git a/HISTORY.rst b/HISTORY.rst index a33f5eb30f..cb9ba8b689 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,12 @@ XXXX-XX-XX +**Enhancements** + +- 1851_: [Linux] cpu_freq() is slow on systems with many CPUs. Read current + frequency values for all CPUs from /proc/cpuinfo instead of opening many + files in /sys fs. (patch by marxin) + **Bug fixes** - 1456_: [macOS] psutil.cpu_freq()'s min and max are set to 0 if can't be From e96307312c4858f9178786a2c7c4cf542de6bf4f Mon Sep 17 00:00:00 2001 From: alxchk Date: Fri, 8 Jan 2021 11:29:54 +0200 Subject: [PATCH 0754/1714] windows: Preserve GetLastError() value before calling sprintf (#1904) https://github.com/giampaolo/psutil/issues/1877#issuecomment-756342272 --- psutil/_psutil_common.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index fae8d97014..ff060a51e2 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -83,8 +83,9 @@ PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { char fullmsg[1024]; #ifdef PSUTIL_WINDOWS + DWORD dwLastError = GetLastError(); sprintf(fullmsg, "(originated from %s)", syscall); - PyErr_SetFromWindowsErrWithFilename(GetLastError(), fullmsg); + PyErr_SetFromWindowsErrWithFilename(dwLastError, fullmsg); #else PyObject *exc; sprintf(fullmsg, "%s (originated from %s)", strerror(errno), syscall); From e80cabe5206fd7ef14fd6a47e2571f660f95babf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Jan 2021 19:10:08 +0100 Subject: [PATCH 0755/1714] give CREDITS to @alxchk for #1904 Signed-off-by: Giampaolo Rodola --- CREDITS | 2 +- HISTORY.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 451b7a4d61..fa8cbf1c67 100644 --- a/CREDITS +++ b/CREDITS @@ -545,7 +545,7 @@ I: 1042, 1079, 1070 N: Oleksii Shevchuk W: https://github.com/alxchk -I: 1077, 1093, 1091, 1220, 1346 +I: 1077, 1093, 1091, 1220, 1346, 1904 N: Prodesire W: https://github.com/Prodesire diff --git a/HISTORY.rst b/HISTORY.rst index cb9ba8b689..dc4afbff96 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,8 @@ XXXX-XX-XX - 1512_: [macOS] sometimes Process.connections() will crash with EOPNOTSUPP for one connection; this is now ignored. - 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. +- 1904_: [Windows] OpenProcess fails with ERROR_SUCCESS due to GetLastError() + called after sprintf(). (patch by alxchk) 5.8.0 ===== From 48b71387901859566ba197a65fb2cff01d8b12b1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Jan 2021 18:25:23 +0100 Subject: [PATCH 0756/1714] update DEVNOTES Signed-off-by: Giampaolo Rodola --- docs/DEVNOTES | 56 ++++----------------------------------------------- 1 file changed, 4 insertions(+), 52 deletions(-) diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 915754d67a..1748bfdab3 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -5,45 +5,11 @@ A collection of ideas and notes about stuff to implement in future versions. "#NNN" occurrences refer to bug tracker issues at: https://github.com/giampaolo/psutil/issues -PLATFORMS -========= - -- #355: Android (with patch) -- #82: Cygwin (PR at #998) -- #276: GNU/Hurd -- #693: Windows Nano -- #1251: Windows bash -- DragonFlyBSD -- HP-UX - FEATURES ======== -- set process name/title - -- #1115: users() idle time. - -- #1102: Process.is64bit(). - -- #371: sensors_temperatures() at least for macOS. - -- #669: Windows / net_if_addrs(): return broadcast addr. - -- #550: CPU info (frequency, architecture, threads per core, cores per socket, - sockets, ...) - -- #772: extended net_io_counters() metrics. - -- #922: extended net_io_stats() info. - -- #914: extended platform specific process info. - -- #898: wifi stats - - (UNIX) process root (different from cwd) -- #782: (UNIX) process num of signals received. - - (Linux) locked files via /proc/locks: https://www.centos.org/docs/5/html/5.2/Deployment_Guide/s2-proc-locks.html @@ -52,16 +18,14 @@ FEATURES Linux: yes Others: ? -- Process.threads(): thread names; patch for macOS available at: - https://code.google.com/p/plcrashreporter/issues/detail?id=65 - Sample code: - https://github.com/janmojzis/pstree/blob/master/proc_kvm.c - - Asynchronous psutil.Popen (see http://bugs.python.org/issue1191964) - (Windows) fall back on using WMIC for Process methods returning AccessDenied -- #613: thread names. +- #613: thread names; patch for macOS available at: + https://code.google.com/p/plcrashreporter/issues/detail?id=65 + Sample code: + https://github.com/janmojzis/pstree/blob/master/proc_kvm.c - scripts/taskmgr-gui.py (using tk). @@ -133,20 +97,8 @@ FEATURES NoSuchProcess and AccessDenied? Not that we need it, but currently we cannot raise a TimeoutExpired exception with a specific error string. -- process_iter() might grow an "attrs" parameter similar to Process.as_dict() - invoke the necessary methods and include the results into a "cache" - attribute attached to the returned Process instances so that one can avoid - catching NSP and AccessDenied: - for p in process_iter(attrs=['cpu_percent']): - print(p.cache['cpu_percent']) - This also leads questions as whether we should introduce a sorting order. - - round Process.memory_percent() result? -- #550: number of threads per core. - -- cpu_percent() and cpu_times_percent() use global vars so are not thread safe. - BUGFIXES ======== From dd237b19a4ca286ed7ce9f89b213b2848c9078cf Mon Sep 17 00:00:00 2001 From: Guillermo Date: Thu, 18 Feb 2021 13:18:22 +0100 Subject: [PATCH 0757/1714] Linux: wait_procs ignoring timeout (#1913) (#1917) The function was exiting after one second due to a subprocess.TimeoutException Fixes #1913 Signed-off-by: guille --- CREDITS | 4 ++++ HISTORY.rst | 1 + psutil/__init__.py | 3 +++ psutil/_compat.py | 8 ++++++++ 4 files changed, 16 insertions(+) diff --git a/CREDITS b/CREDITS index fa8cbf1c67..360688bd82 100644 --- a/CREDITS +++ b/CREDITS @@ -736,3 +736,7 @@ I: 1822 N: marxin W: https://github.com/marxin I: 1851 + +N: guille +W: https://github.com/guille +I: 1913 diff --git a/HISTORY.rst b/HISTORY.rst index dc4afbff96..0998908ae0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,7 @@ XXXX-XX-XX - 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. - 1904_: [Windows] OpenProcess fails with ERROR_SUCCESS due to GetLastError() called after sprintf(). (patch by alxchk) +- 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown 5.8.0 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 44efb7ffe3..7dac47d589 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -47,6 +47,7 @@ from ._compat import long from ._compat import PermissionError from ._compat import ProcessLookupError +from ._compat import SubprocessTimeoutExpired as _SubprocessTimeoutExpired from ._compat import PY3 as _PY3 from ._common import CONN_CLOSE @@ -1505,6 +1506,8 @@ def check_gone(proc, timeout): 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. diff --git a/psutil/_compat.py b/psutil/_compat.py index 17f38485e9..909386870f 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -422,3 +422,11 @@ def get_terminal_size(fallback=(80, 24)): return (res[1], res[0]) except Exception: return fallback + + +# python 3.3 +try: + from subprocess import TimeoutExpired as SubprocessTimeoutExpired +except ImportError: + class SubprocessTimeoutExpired: + pass From c3e63b4dd59f1724a7fa29371e53dfa7f46cbcd3 Mon Sep 17 00:00:00 2001 From: Jake Omann Date: Sun, 21 Feb 2021 04:25:07 -0600 Subject: [PATCH 0758/1714] Fix incorrect range for Solaris swap output (#1874) (#1914) Signed-off-by: Jake Omann --- HISTORY.rst | 1 + psutil/_pssunos.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0998908ae0..b42261abd9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,7 @@ XXXX-XX-XX - 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. - 1904_: [Windows] OpenProcess fails with ERROR_SUCCESS due to GetLastError() called after sprintf(). (patch by alxchk) +- 1874_: [Solaris] swap output error due to incorrect range. - 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown 5.8.0 diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 816ebf07f3..84d78814f9 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -155,7 +155,7 @@ def swap_memory(): total = free = 0 for line in lines: line = line.split() - t, f = line[3:4] + t, f = line[3:5] total += int(int(t) * 512) free += int(int(f) * 512) used = total - free From 35a965912f055804ceb2939e7960e175a5da7db1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 6 Apr 2021 12:47:57 +0200 Subject: [PATCH 0759/1714] add @cybersecgeek to sponsors Signed-off-by: Giampaolo Rodola --- README.rst | 1 + docs/index.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 2631e0590e..a789d182c1 100644 --- a/README.rst +++ b/README.rst @@ -134,6 +134,7 @@ Supporters
+
add your avatar diff --git a/docs/index.rst b/docs/index.rst index d7d326321b..079b313cc5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -78,6 +78,7 @@ Supporters
+

add your avatar From 93e1c468ab4ac7922472114b3aeeacda2c8676a1 Mon Sep 17 00:00:00 2001 From: David Knaack Date: Thu, 8 Apr 2021 11:30:33 +0200 Subject: [PATCH 0760/1714] [Windows] psutil.swap_memory() show swap instead of committed memory (#1927) Signed-off-by: David Knaack --- CREDITS | 4 ++++ HISTORY.rst | 1 + psutil/_pswindows.py | 12 ++++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index 360688bd82..3f3888541d 100644 --- a/CREDITS +++ b/CREDITS @@ -740,3 +740,7 @@ I: 1851 N: guille W: https://github.com/guille I: 1913 + +N: David Knaack +W: https://github.com/davidkna +I: 1921 diff --git a/HISTORY.rst b/HISTORY.rst index b42261abd9..c826f47b50 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,7 @@ XXXX-XX-XX called after sprintf(). (patch by alxchk) - 1874_: [Solaris] swap output error due to incorrect range. - 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown +- 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap 5.8.0 ===== diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 0ad60c4aea..6b1a34de23 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -241,8 +241,16 @@ 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] + + total_phys = mem[0] + free_phys = mem[1] + total_system = mem[2] + free_system = mem[3] + + # Despite the name PageFile refers to total system memory here + # thus physical memory values need to be substracted to get swap values + total = total_system - total_phys + free = min(total, free_system - free_phys) used = total - free percent = usage_percent(used, total, round_=1) return _common.sswap(total, used, free, percent, 0, 0) From ea5b2df02e39816c1f6626dd9d93ae7ede422687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Thu, 15 Apr 2021 17:16:26 +0200 Subject: [PATCH 0761/1714] Add tolerance to test_linux.TestSystemVirtualMemory.test_total (#1935) We see this test as very flaky without tolerance in Fedora and CentOS --- psutil/tests/test_linux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0c6d498c81..f5243c2cde 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -243,7 +243,8 @@ def test_total(self): # self.assertEqual(free_value, psutil_value) vmstat_value = vmstat('total memory') * 1024 psutil_value = psutil.virtual_memory().total - self.assertAlmostEqual(vmstat_value, psutil_value) + self.assertAlmostEqual( + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) @retry_on_failure() def test_used(self): From 4d30272e3de81a0c4244143595284e040cfb070f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 3 May 2021 13:33:16 +0200 Subject: [PATCH 0762/1714] Re. #1210: add doc warning explaining that cpu_times() values can sometimes go backwards Signed-off-by: Giampaolo Rodola --- README.rst | 21 ++------------------- docs/index.rst | 6 ++++++ psutil/__init__.py | 1 - 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index a789d182c1..3e41d4a9dd 100644 --- a/README.rst +++ b/README.rst @@ -299,9 +299,9 @@ Process management >>> p psutil.Process(pid=7055, name='python3', status='running', started='09:04:44') >>> p.name() - 'python' + 'python3' >>> p.exe() - '/usr/bin/python' + '/usr/bin/python3' >>> p.cwd() '/home/giampaolo' >>> p.cmdline() @@ -444,23 +444,6 @@ Further process APIs >>> gone, alive = psutil.wait_procs(procs_list, timeout=3, callback=on_terminate) >>> -Popen wrapper: - -.. code-block:: python - - >>> import psutil - >>> from subprocess import PIPE - >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) - >>> p.name() - 'python' - >>> p.username() - 'giampaolo' - >>> p.communicate() - ('hello\n', None) - >>> p.wait(timeout=2) - 0 - >>> - Windows services ---------------- diff --git a/docs/index.rst b/docs/index.rst index 079b313cc5..a884ec807b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -144,6 +144,12 @@ CPU .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. + .. warning:: + 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 `#1210 `__. + .. function:: cpu_percent(interval=None, percpu=False) Return a float representing the current system-wide CPU utilization as a diff --git a/psutil/__init__.py b/psutil/__init__.py index 7dac47d589..0b5dff552a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1724,7 +1724,6 @@ def cpu_percent(interval=None, percpu=False): 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) From 95db8bb96caf5540c45b9eff2229c0401b578c31 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 May 2021 17:02:54 +0200 Subject: [PATCH 0763/1714] give CREDITS to @sansecio for the new sponsorship Signed-off-by: Giampaolo Rodola --- README.rst | 4 ++++ docs/index.rst | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3e41d4a9dd..22f0f8c0cd 100644 --- a/README.rst +++ b/README.rst @@ -123,6 +123,10 @@ Sponsors +      + + +
add your logo diff --git a/docs/index.rst b/docs/index.rst index a884ec807b..cc1c8dd164 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -65,8 +65,11 @@ Sponsors +      + + + -
add your logo From a405736c26f77bb1befea0b4e65cde5c7c891e92 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Jun 2021 12:28:28 +0200 Subject: [PATCH 0764/1714] try to fix py2 linting and freebsd installation Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8427a929f3..dddd2fa923 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,7 +85,7 @@ jobs: - name: Run tests id: test - uses: vmactions/freebsd-vm@v0.0.8 + uses: vmactions/freebsd-vm@v0.1.4 with: usesh: true prepare: pkg install -y gcc python3 @@ -108,6 +108,8 @@ jobs: - uses: actions/setup-python@v2 - name: 'Run linters' run: | + curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py + python2 get-pip.py python2 -m pip install flake8 python3 -m pip install flake8 python2 -m flake8 . From a3d68de308df49459ee1f5bc08b400cf106b59e5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Jun 2021 14:17:32 +0200 Subject: [PATCH 0765/1714] add Chenyoo Hao to list of supporters Signed-off-by: Giampaolo Rodola --- .github/workflows/issues.py | 2 +- Makefile | 1 + README.rst | 4 ++-- docs/index.rst | 4 +++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 964e519285..ef68649582 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -46,7 +46,7 @@ "macos": [ "macos", "mac ", "osx", "os x", "mojave", "sierra", "capitan", "yosemite", "catalina", "mojave", "big sur", "xcode", "darwin", - "dylib", + "dylib", "m1", ], "aix": ["aix"], "cygwin": ["cygwin"], diff --git a/Makefile b/Makefile index 087ddd7853..1f4f27d460 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ DEPS = \ pypinfo \ requests \ setuptools \ + sphinx_rtd_theme \ twine \ virtualenv \ wheel diff --git a/README.rst b/README.rst index 22f0f8c0cd..61a9604f5a 100644 --- a/README.rst +++ b/README.rst @@ -121,7 +121,7 @@ Sponsors add your avatar - Contributing ============ diff --git a/docs/index.rst b/docs/index.rst index cc1c8dd164..e58bacb349 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,7 +63,7 @@ Sponsors +
add your logo Supporters @@ -82,6 +83,7 @@ Supporters +
add your avatar From a5ed04cf6e6b3debcdb7ad9f1b63c1d4584f2d13 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Jun 2021 14:41:42 +0200 Subject: [PATCH 0766/1714] change tidelift logo Signed-off-by: Giampaolo Rodola --- README.rst | 5 ++-- docs/_static/tidelift-logo.png | Bin 2271 -> 0 bytes docs/_static/tidelift-logo.svg | 33 ++++++++++++++++++++++++++ docs/index.rst | 4 ++-- scripts/internal/generate_manifest.py | 2 +- 5 files changed, 39 insertions(+), 5 deletions(-) delete mode 100644 docs/_static/tidelift-logo.png create mode 100644 docs/_static/tidelift-logo.svg diff --git a/README.rst b/README.rst index 61a9604f5a..42f71b097d 100644 --- a/README.rst +++ b/README.rst @@ -121,9 +121,9 @@ Sponsors
- + -      +    @@ -143,6 +143,7 @@ Supporters
add your avatar + Contributing ============ diff --git a/docs/_static/tidelift-logo.png b/docs/_static/tidelift-logo.png deleted file mode 100644 index 9e33757fb78463e7b379be88ae0ff8b5a9421795..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2271 zcmV<52q5=~P)WFU8GbZ8()Nlj2>E@cM*00=`#L_t(|+U=chOdMq% z$G;0p>F%ub4|WL^XyFRRZfP5@!tT|WF0Andq0;$c&BRFTue6q^{D(p+*@>%Fif zHMun9TCG)24)$7i>6f85_E`{-hw$L&=JIl_@&hGv` zFJ~WSemwJgzVkf4=XrjQq2bXs0R%x136euqKoA6p2SE^;VHCSqzP<$Znk^|aisN{= zJzj8pByQPtWoyyg)QExt%dKnCXcQmzd2sQI5n0bGUta>3(~gSr(uC}s=6HzVW^d*x9^K*0YYh*s)|s zanx>I16C@+;I9aDcUG4j2VvFt>}2}3A}seRs3OB0~oU@>0XB= zg*NV#D{!@i8GY*(ElEd*qm zJKCWwBTPn>)oj@qrTCJ_55vKGR+W_8R;n&u+1?=|Wl$JujkT~t_* z4ae?I+A5bMAZ%y?@4VNijL43Bt*CPa*|2IeM(K{*MCrfqrL1yi&$+=cri6x6 z7RH3~^(9O4>qX_+u&Aw~;^7?_lobVDNOMyonwuIRE=IkwqW5~^CmuGF(D%2`$H=HnBybpIn|q3oWxdzBQJI&<<}V>^Uwnv zAHmRw4?U-P(em^`9De0^+@YUV(}4t`Y0QI24)4Gq+B*(Q_ADyOOH;ZKk03}qble;1 z>^dsh1G#g789|VE5Vr)R``GE2zt=nMR7nJhhZNa7^*1!)lk9y|+p5S5iL`8Us)`54 zM{wX!ch*n+NlnRVxg46v9{ghg95~c1YjdgY#KS)pKuhbvw49<};P?pIpMOc#4)sJaDs5=@vC|1J zFe8>uz0Bp#lT(OiJ ziVa)DG7S;)V^4xu$mSGnv-gh$6dM_+K2=-BL{VanX0@YO*HBQDxWgw@4wRReqbOt4 zZFj1Q$BrT%FW>(IR^(;(=?+CSqe5uRiaY@?-~R)46sfn~LzQef%W?!fSvd)3G3A9w zCn%4HcCf7a0n_|k0ngrd1shjh$3MPaiRhfN8Icqhk7Cmd_S+_~&T@l1L<1*sd%XDg ze7_>XK`|@skekax37o||D(;Hn)sfZsedAN!>?6+MJ6%Ip|=Z}<%d1PX`MI!Zx zwKnIK3!bVyfco8*B_{TFBt5RtwZsS4ETaqMf~BRR|B z_IPQ?LXdb6^VHUCMr+G1$r%=okKn|~_o=c75|3>Gyl6Yhv z@4WX>#vKcpqv6uwy9-y5c#wEx7aSkK;UmX0a{tNf?3`xL%Fka$&XlR*AxNx1zagmj zg_3yg%uMQb_OhV~y!~#UBJmBqZk4|}&B0x&e}D5(rg$gY%N2~FVIRI4_9+?^^^XM#>Xl`mmL4jpnS))<({reMqesNgVJs7hIt~v)8 zCh@N*$MG2T2M}b#sxJCU#>PYlvf+e;uVz~{B9ZH`ZQOve(qgPywL(#QzWMe$T)O0k z=f4B8%0Ke(c0Bap1IWplUp##|7{VK8KERZWm`SX9PPAV1K)GcG+X^GV0{!@&H{w&) tgxfBrUOWhb$R=oS3qcSh9t1&X$4_yk2>8V&WlR77002ovPDHLkV1ngaS5E){ diff --git a/docs/_static/tidelift-logo.svg b/docs/_static/tidelift-logo.svg new file mode 100644 index 0000000000..af12d68417 --- /dev/null +++ b/docs/_static/tidelift-logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst index e58bacb349..f01cceed82 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,9 +63,9 @@ Sponsors
- + -      +    diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 384fb329bf..a1ed6b38f8 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -12,7 +12,7 @@ import subprocess -SKIP_EXTS = ('.png', '.jpg', '.jpeg') +SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') SKIP_FILES = ('appveyor.yml') SKIP_PREFIXES = ('.ci/', '.github/') From cf7439fddcb1158ba00672c64bdd32246ed9e818 Mon Sep 17 00:00:00 2001 From: MaWe2019 <47883145+MaWe2019@users.noreply.github.com> Date: Tue, 29 Jun 2021 19:22:08 +0200 Subject: [PATCH 0767/1714] Changed size of opts array in psutil/arch/windows/disk.c to 50 (#1962) --- psutil/arch/windows/disk.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 29bd0a20c5..0d8be2faa7 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -201,7 +201,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { int type; int ret; unsigned int old_mode = 0; - char opts[20]; + char opts[50]; HANDLE mp_h; BOOL mp_flag= TRUE; LPTSTR fs_type[MAX_PATH + 1] = { 0 }; From ac898c7c8cd4d232c602b70d58fc53474e8b9154 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 29 Jun 2021 19:23:56 +0200 Subject: [PATCH 0768/1714] ggive CREDITS to @MaWe2019 Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CREDITS b/CREDITS index 3f3888541d..bd3750186e 100644 --- a/CREDITS +++ b/CREDITS @@ -744,3 +744,7 @@ I: 1913 N: David Knaack W: https://github.com/davidkna I: 1921 + +N: MaWe2019 +W: https://github.com/MaWe2019 +I: 1953 diff --git a/HISTORY.rst b/HISTORY.rst index c826f47b50..82e5e782bc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,6 +23,8 @@ XXXX-XX-XX - 1874_: [Solaris] swap output error due to incorrect range. - 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown - 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap +- 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. + (patch by MaWe2019) 5.8.0 ===== From 12994a739dbeae7a4e7273cb742ca9270e168763 Mon Sep 17 00:00:00 2001 From: Dmitry Gorbunov Date: Sun, 25 Jul 2021 03:28:51 +1000 Subject: [PATCH 0769/1714] Fix typos in documentation (#1970) --- CREDITS | 6 ++++++ docs/index.rst | 4 ++-- psutil/__init__.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CREDITS b/CREDITS index bd3750186e..a46ea44ab1 100644 --- a/CREDITS +++ b/CREDITS @@ -748,3 +748,9 @@ I: 1921 N: MaWe2019 W: https://github.com/MaWe2019 I: 1953 + +N: Dmitry Gorbunov +C: Russia +E: gorbunov.dmitry.1999@gmail.com +W: https://gorbunov-dmitry.github.io +D: fix typos in documentation diff --git a/docs/index.rst b/docs/index.rst index f01cceed82..34e136f6e8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -261,7 +261,7 @@ CPU .. function:: cpu_freq(percpu=False) - Return CPU frequency as a nameduple including *current*, *min* and *max* + Return CPU frequency as a named tuple including *current*, *min* and *max* frequencies expressed in Mhz. On Linux *current* frequency reports the real-time value, on all other platforms it represents the nominal "fixed" value. @@ -417,7 +417,7 @@ Disks Note that this may not be fully reliable on all systems (e.g. on BSD this parameter is ignored). See `disk_usage.py`_ script providing an example usage. - Returns a list of namedtuples with the following fields: + Returns a list of named tuples with the following fields: * **device**: the device path (e.g. ``"/dev/hda1"``). On Windows this is the drive letter (e.g. ``"C:\\"``). diff --git a/psutil/__init__.py b/psutil/__init__.py index 0b5dff552a..92b6398ac8 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1851,7 +1851,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 From 5a76cfadcabf37f65f0d70e4c5b7493478b53c2c Mon Sep 17 00:00:00 2001 From: Pablo Baeyens Date: Thu, 5 Aug 2021 15:54:39 +0200 Subject: [PATCH 0770/1714] [Windows] Reset `mp_flag` after each drive on `psutil.disk_partitions()` (#1961) --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ psutil/arch/windows/disk.c | 1 + 3 files changed, 7 insertions(+) diff --git a/CREDITS b/CREDITS index a46ea44ab1..c614c42229 100644 --- a/CREDITS +++ b/CREDITS @@ -754,3 +754,7 @@ C: Russia E: gorbunov.dmitry.1999@gmail.com W: https://gorbunov-dmitry.github.io D: fix typos in documentation + +N: Pablo Baeyens +W: https://github.com/mx-psi +I: 1598 diff --git a/HISTORY.rst b/HISTORY.rst index 82e5e782bc..4058d66011 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,6 +25,8 @@ XXXX-XX-XX - 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap - 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. (patch by MaWe2019) +- 1598_: [Windows] psutil.disk_partitions() only returns mountpoints on drives + where it first finds one 5.8.0 ===== diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 0d8be2faa7..7c35e81243 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -290,6 +290,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { mp_h = FindFirstVolumeMountPoint( drive_letter, mp_buf, MAX_PATH); if (mp_h != INVALID_HANDLE_VALUE) { + mp_flag = TRUE; while (mp_flag) { // Append full mount path with drive letter strcpy_s(mp_path, _countof(mp_path), drive_letter); From dccf8bedf34fced63f1962dd77b58f0da339759c Mon Sep 17 00:00:00 2001 From: Michael Dudyak Date: Fri, 20 Aug 2021 09:55:56 +0200 Subject: [PATCH 0771/1714] Minor typo fix (#1982) --- INSTALL.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.rst b/INSTALL.rst index 22693b5695..c7efea5ee0 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -93,7 +93,7 @@ If you don't have pip you can install with wget:: wget https://bootstrap.pypa.io/get-pip.py -O - | python3 -...ow with curl:: +...or with curl:: python3 < <(curl -s https://bootstrap.pypa.io/get-pip.py) From 778f5e61babfda3998f6ea0e051e1e15791f6466 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 10 Sep 2021 13:43:33 +0200 Subject: [PATCH 0772/1714] top.py: report mem usage in GB Signed-off-by: Giampaolo Rodola --- scripts/top.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/top.py b/scripts/top.py index 3c297d9af1..989f830682 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -125,13 +125,15 @@ def get_dashes(perc): dashes, empty_dashes = get_dashes(perc) line = " CPU%-2s [%s%s] %5s%%" % (cpu_num, dashes, empty_dashes, perc) printl(line, color=get_color(perc)) + + # memory usage mem = psutil.virtual_memory() dashes, empty_dashes = get_dashes(mem.percent) line = " Mem [%s%s] %5s%% %6s / %s" % ( dashes, empty_dashes, mem.percent, - str(int(mem.used / 1024 / 1024)) + "M", - str(int(mem.total / 1024 / 1024)) + "M" + bytes2human(mem.used), + bytes2human(mem.total), ) printl(line, color=get_color(mem.percent)) @@ -141,8 +143,8 @@ def get_dashes(perc): line = " Swap [%s%s] %5s%% %6s / %s" % ( dashes, empty_dashes, swap.percent, - str(int(swap.used / 1024 / 1024)) + "M", - str(int(swap.total / 1024 / 1024)) + "M" + bytes2human(swap.used), + bytes2human(swap.total), ) printl(line, color=get_color(swap.percent)) From 11429f87725ab8e301ab5f4e3e43d45590c0de03 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 26 Sep 2021 22:04:00 +0200 Subject: [PATCH 0773/1714] give CREDITS to @scoutapm-sponsorships and Alexey Vazhnov for sponsorship Signed-off-by: Giampaolo Rodola --- README.rst | 2 ++ docs/index.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 42f71b097d..1f1f42c808 100644 --- a/README.rst +++ b/README.rst @@ -139,7 +139,9 @@ Supporters + +
add your avatar diff --git a/docs/index.rst b/docs/index.rst index f01cceed82..3473ebfe35 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -83,7 +83,9 @@ Supporters + +
add your avatar From 2fabe4714ef77c6b6152293b861fd9e55be06406 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 2 Oct 2021 11:23:20 +0200 Subject: [PATCH 0774/1714] try to fix CI not running for PRs Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dddd2fa923..84f3e74902 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ # * https://github.com/marketplace/actions/cancel-workflow-action # * https://github.com/vmactions/freebsd-vm -on: [push] +on: [push, pull_request] name: build jobs: linux-macos-win: From 53a6c03e36db43d70b3b4e18abe7b81eaab235ce Mon Sep 17 00:00:00 2001 From: Wilfried Goesgens Date: Sat, 2 Oct 2021 11:31:09 +0200 Subject: [PATCH 0775/1714] [Windows] fix service handle leak in start service (#1990) Signed-off-by: Wilfried Goesgens --- psutil/arch/windows/services.c | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index a91cb8f797..fa3e646e51 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -433,6 +433,7 @@ psutil_winservice_start(PyObject *self, PyObject *args) { goto error; } + CloseServiceHandle(hService); Py_RETURN_NONE; error: From 29214fc820cb2c8ac062da8e9f99f3459f10f93c Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 3 Oct 2021 00:15:48 +0800 Subject: [PATCH 0776/1714] Fix thread safety of cached functions lock-freely (#1949) Signed-off-by: XuehaiPan --- psutil/_common.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index 771461d692..d5548d1cf3 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -440,6 +440,7 @@ def memoize_when_activated(fun): >>> foo() >>> """ + @functools.wraps(fun) def wrapper(self): try: @@ -451,7 +452,11 @@ def wrapper(self): except KeyError: # case 3: we entered oneshot() ctx but there's no cache # for this entry yet - ret = self._cache[fun] = fun(self) + ret = fun(self) + try: + self._cache[fun] = ret + except AttributeError: + pass # inconsistency caused by multi-threading, just ignore it return ret def cache_activate(proc): From 102a3dd115ef763920b7db0741d585155e8282ed Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 2 Oct 2021 18:22:49 +0200 Subject: [PATCH 0777/1714] give credits to @XuehaiPan for #1948, #1949 Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ HISTORY.rst | 8 +++++--- psutil/_common.py | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CREDITS b/CREDITS index c614c42229..280c3b1cd5 100644 --- a/CREDITS +++ b/CREDITS @@ -758,3 +758,7 @@ D: fix typos in documentation N: Pablo Baeyens W: https://github.com/mx-psi I: 1598 + +N: Xuehai Pan +W: https://github.com/XuehaiPan +I: 1948 diff --git a/HISTORY.rst b/HISTORY.rst index 4058d66011..660467cdbe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,16 +17,18 @@ XXXX-XX-XX determined (instead of crashing). - 1512_: [macOS] sometimes Process.connections() will crash with EOPNOTSUPP for one connection; this is now ignored. +- 1598_: [Windows] psutil.disk_partitions() only returns mountpoints on drives + where it first finds one +- 1874_: [Solaris] swap output error due to incorrect range. - 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. - 1904_: [Windows] OpenProcess fails with ERROR_SUCCESS due to GetLastError() called after sprintf(). (patch by alxchk) -- 1874_: [Solaris] swap output error due to incorrect range. - 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown - 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap +- 1948_: Process' memoize_when_activated decorator was not thread-safe. (patch + by Xuehai Pan) - 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. (patch by MaWe2019) -- 1598_: [Windows] psutil.disk_partitions() only returns mountpoints on drives - where it first finds one 5.8.0 ===== diff --git a/psutil/_common.py b/psutil/_common.py index d5548d1cf3..2acab626cd 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -456,7 +456,9 @@ def wrapper(self): try: self._cache[fun] = ret except AttributeError: - pass # inconsistency caused by multi-threading, just ignore it + # multi-threading race condition, see: + # https://github.com/giampaolo/psutil/issues/1948 + pass return ret def cache_activate(proc): From d01233263f046f07d5139a8611671525f74e3dd0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 Oct 2021 09:48:35 +0200 Subject: [PATCH 0778/1714] Fix #1991: process_iter() raise TypeError with multi threads. When entering the function, use a copy() of the global dict, and do operations on that instead of the global object, then update the global object on function exit. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/__init__.py | 78 +++++++++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 660467cdbe..ee7803d123 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,8 @@ XXXX-XX-XX by Xuehai Pan) - 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. (patch by MaWe2019) +- 1991_: process_iter() can raise TypeError if invoked from multiple threads + (not thread-safe). 5.8.0 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 92b6398ac8..41f9bf5a1d 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1387,7 +1387,6 @@ def pid_exists(pid): _pmap = {} -_lock = threading.Lock() def process_iter(attrs=None, ad_value=None): @@ -1411,58 +1410,59 @@ 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) - with _lock: - _pmap[proc.pid] = proc + pmap[proc.pid] = proc return proc def remove(pid): - with _lock: - _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) - - with _lock: - 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) - 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): From 5d39cc9c8d8dc04862786abba0307ed9350144ce Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 Oct 2021 15:55:16 +0200 Subject: [PATCH 0779/1714] Improve custom error tracebacks and messages (#1992) Removal of duplicated `psutil.NoSuchProcess` text. Before: ``` psutil.NoSuchProcess: psutil.NoSuchProcess process no longer exists (pid=4651, name="python") psutil.ZombieProcess: psutil.ZombieProcess process no longer exists and it's a zombie (pid=4651, name="python") psutil.AccessDenied: psutil.AccessDenied (pid=4651, name="python") psutil.TimeoutExpired: psutil.TimeoutExpired timeout after 5 seconds (pid=4651, name="python") ``` Now: ``` psutil.NoSuchProcess: process no longer exists (pid=4651, name="python") psutil.ZombieProcess: process no longer exists and it's a zombie (pid=4651, name="python") psutil.AccessDenied: (pid=4651, name="python") psutil.TimeoutExpired: timeout after 5 seconds (pid=4651, name="python") ``` --- More info if process PID has been reused: Before: ``` psutil.NoSuchProcess: psutil.NoSuchProcess process no longer exists (pid=465148) ``` Now: ``` psutil.NoSuchProcess: process no longer exists and its PID has been reused (pid=465148) ``` --- Before: ``` psutil.NoSuchProcess: psutil.NoSuchProcess no process found with pid 666 ``` Now: ``` psutil.NoSuchProcess: process PID not found (pid=666) ``` --- Before: ``` >>> psutil.NoSuchProcess(212, name="python") psutil.NoSuchProcess process no longer exists (pid=212, name='python') ``` Now: ``` >>> psutil.NoSuchProcess(212, name="python") psutil.NoSuchProcess(pid=212, name='python', msg='process no longer exists') ``` --- HISTORY.rst | 3 ++ psutil/__init__.py | 15 ++++--- psutil/_common.py | 79 +++++++++++++++------------------- psutil/tests/test_contracts.py | 1 - psutil/tests/test_misc.py | 78 +++++++++++++++++++-------------- psutil/tests/test_process.py | 14 ++++++ 6 files changed, 108 insertions(+), 82 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ee7803d123..db0e4261a8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,9 @@ XXXX-XX-XX - 1851_: [Linux] cpu_freq() is slow on systems with many CPUs. Read current frequency values for all CPUs from /proc/cpuinfo instead of opening many files in /sys fs. (patch by marxin) +- 1992_: NoSuchProcess message now specifies if the PID has been reused. +- 1992_: error classes (NoSuchProcess, AccessDenied, etc.) now have a better + formatted and separated `__repr__` and `__str__` implementations. **Bug fixes** diff --git a/psutil/__init__.py b/psutil/__init__.py index 41f9bf5a1d..e52fdcda62 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -269,7 +269,11 @@ def _assert_pid_not_reused(fun): @functools.wraps(fun) def wrapper(self, *args, **kwargs): if not self.is_running(): - raise NoSuchProcess(self.pid, self._name) + if self._pid_reused: + msg = "process no longer exists and its PID has been reused" + else: + msg = None + raise NoSuchProcess(self.pid, self._name, msg=msg) return fun(self, *args, **kwargs) return wrapper @@ -340,6 +344,7 @@ def _init(self, pid, _ignore_nsp=False): self._exe = None self._create_time = None self._gone = False + self._pid_reused = False self._hash = None self._lock = threading.RLock() # used for caching on Windows only (on POSIX ppid may change) @@ -364,8 +369,7 @@ 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) + raise NoSuchProcess(pid, msg='process PID not found') else: self._gone = True # This pair is supposed to indentify a Process instance @@ -571,7 +575,7 @@ def is_running(self): 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 @@ -579,7 +583,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. diff --git a/psutil/_common.py b/psutil/_common.py index 2acab626cd..306301ede8 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -9,6 +9,7 @@ from __future__ import division, print_function +import collections import contextlib import errno import functools @@ -18,7 +19,6 @@ import sys import threading import warnings -from collections import defaultdict from collections import namedtuple from socket import AF_INET from socket import SOCK_DGRAM @@ -275,15 +275,32 @@ class Error(Exception): """ __module__ = 'psutil' - def __init__(self, msg=""): - Exception.__init__(self, msg) - self.msg = msg + def _infodict(self, attrs): + try: + info = collections.OrderedDict() + except AttributeError: # pragma: no cover + info = {} # Python 2.6 + for name in attrs: + value = getattr(self, name, None) + if value: + 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 (self.msg, details) if x]) def __repr__(self): - ret = "psutil.%s %s" % (self.__class__.__name__, self.msg) - return ret.strip() - - __str__ = __repr__ + # 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): @@ -293,16 +310,10 @@ class NoSuchProcess(Error): __module__ = 'psutil' def __init__(self, pid, name=None, msg=None): - Error.__init__(self, msg) + Error.__init__(self) 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 + self.msg = msg or "process no longer exists" class ZombieProcess(NoSuchProcess): @@ -315,19 +326,9 @@ class ZombieProcess(NoSuchProcess): __module__ = 'psutil' def __init__(self, pid, name=None, ppid=None, msg=None): - NoSuchProcess.__init__(self, msg) - self.pid = pid + NoSuchProcess.__init__(self, pid, name, msg) 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 + self.msg = msg or "PID still exists but it's a zombie" class AccessDenied(Error): @@ -335,17 +336,10 @@ class AccessDenied(Error): __module__ = 'psutil' def __init__(self, pid=None, name=None, msg=None): - Error.__init__(self, msg) + Error.__init__(self) 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 = "" + self.msg = msg or "" class TimeoutExpired(Error): @@ -355,14 +349,11 @@ class TimeoutExpired(Error): __module__ = 'psutil' def __init__(self, seconds, pid=None, name=None): - Error.__init__(self, "timeout after %s seconds" % seconds) + Error.__init__(self) 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 + self.msg = "timeout after %s seconds" % seconds # =================================================================== @@ -629,8 +620,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 diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 32c75fd7f3..b03477d9ab 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -360,7 +360,6 @@ def check_exception(exc, proc, name, ppid): elif isinstance(exc, psutil.NoSuchProcess): tcase.assertProcessGone(proc) str(exc) - assert exc.msg def do_wait(): if pid != 0: diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 81fa8f390d..fc9f5b13f8 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -97,57 +97,71 @@ def test_process__repr__(self, func=repr): def test_process__str__(self): self.test_process__repr__(func=str) - def test_no_such_process__repr__(self, func=repr): + def test_no_such_process__repr__(self): self.assertEqual( repr(psutil.NoSuchProcess(321)), - "psutil.NoSuchProcess process no longer exists (pid=321)") + "psutil.NoSuchProcess(pid=321, msg='process no longer exists')") self.assertEqual( - repr(psutil.NoSuchProcess(321, name='foo')), - "psutil.NoSuchProcess process no longer exists (pid=321, " - "name='foo')") + 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( - repr(psutil.NoSuchProcess(321, msg='foo')), - "psutil.NoSuchProcess foo") + str(psutil.NoSuchProcess(321, name="name", msg="msg")), + "msg (pid=321, name='name')") - def test_zombie_process__repr__(self, func=repr): + def test_zombie_process__repr__(self): self.assertEqual( repr(psutil.ZombieProcess(321)), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321)") + 'psutil.ZombieProcess(pid=321, msg="PID still ' + 'exists but it\'s a zombie")') self.assertEqual( - repr(psutil.ZombieProcess(321, name='foo')), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321, name='foo')") + 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( - repr(psutil.ZombieProcess(321, name='foo', ppid=1)), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321, name='foo', ppid=1)") + str(psutil.ZombieProcess(321)), + "PID still exists but it's a zombie (pid=321)") self.assertEqual( - repr(psutil.ZombieProcess(321, msg='foo')), - "psutil.ZombieProcess foo") + str(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")), + "foo (pid=321, ppid=320, name='name')") - def test_access_denied__repr__(self, func=repr): + def test_access_denied__repr__(self): self.assertEqual( repr(psutil.AccessDenied(321)), - "psutil.AccessDenied (pid=321)") + "psutil.AccessDenied(pid=321)") self.assertEqual( - repr(psutil.AccessDenied(321, name='foo')), - "psutil.AccessDenied (pid=321, name='foo')") + repr(psutil.AccessDenied(321, name="name", msg="msg")), + "psutil.AccessDenied(pid=321, name='name', msg='msg')") + + def test_access_denied__str__(self): self.assertEqual( - repr(psutil.AccessDenied(321, msg='foo')), - "psutil.AccessDenied foo") + 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, func=repr): + def test_timeout_expired__repr__(self): self.assertEqual( - repr(psutil.TimeoutExpired(321)), - "psutil.TimeoutExpired timeout after 321 seconds") + 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( - repr(psutil.TimeoutExpired(321, pid=111)), - "psutil.TimeoutExpired timeout after 321 seconds (pid=111)") + str(psutil.TimeoutExpired(5)), + "timeout after 5 seconds") self.assertEqual( - repr(psutil.TimeoutExpired(321, pid=111, name='foo')), - "psutil.TimeoutExpired timeout after 321 seconds " - "(pid=111, name='foo')") + str(psutil.TimeoutExpired(5, pid=321, name="name")), + "timeout after 5 seconds (pid=321, name='name')") def test_process__eq__(self): p1 = psutil.Process() diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index dfe8854724..002c58de24 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1332,6 +1332,20 @@ def test_zombie_process_status_w_exc(self): 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" + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.suspend) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.resume) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.terminate) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.kill) + 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(): From 98915102cc97869b8418d38088a1dcb59b931ffe Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 Oct 2021 20:55:31 +0200 Subject: [PATCH 0780/1714] update doc re. to exception classes Signed-off-by: Giampaolo Rodola --- docs/index.rst | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 11ea8780b6..51ec3cc773 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -985,31 +985,37 @@ Exceptions .. class:: NoSuchProcess(pid, name=None, msg=None) Raised by :class:`Process` class methods when no process with the given - *pid* is found in the current process list or when a process no longer + *pid* is found in the current process list, or when a process no longer exists. *name* is the name the process had before disappearing and gets set only if :meth:`Process.name()` was previously called. .. class:: ZombieProcess(pid, name=None, ppid=None, msg=None) This may be raised by :class:`Process` class methods when querying a zombie - process on UNIX (Windows doesn't have zombie processes). Depending on the - method called the OS may be able to succeed in retrieving the process - information or not. - Note: this is a subclass of :class:`NoSuchProcess` so if you're not - interested in retrieving zombies (e.g. when using :func:`process_iter()`) - you can ignore this exception and just catch :class:`NoSuchProcess`. + process on UNIX (Windows doesn't have zombie processes). + *name* and *ppid* attributes are available if :meth:`Process.name()` or + :meth:`Process.ppid()` methods were called before the process turned into a + zombie. + + .. note:: + + this is a subclass of :class:`NoSuchProcess` so if you're not interested + in retrieving zombies (e.g. when using :func:`process_iter()`) you can + ignore this exception and just catch :class:`NoSuchProcess`. .. versionadded:: 3.0.0 .. class:: AccessDenied(pid=None, name=None, msg=None) Raised by :class:`Process` class methods when permission to perform an - action is denied. "name" is the name of the process (may be ``None``). + action is denied due to insufficient privileges. + *name* attribute is available if :meth:`Process.name()` was previously called. .. class:: TimeoutExpired(seconds, pid=None, name=None, msg=None) - Raised by :meth:`Process.wait` if timeout expires and process is still - alive. + Raised by :meth:`Process.wait` method if timeout expires and the process is + still alive. + *name* attribute is available if :meth:`Process.name()` was previously called. Process class ------------- From 0a51174fd4ec1e7da6ccb6174f24d9ddf093d479 Mon Sep 17 00:00:00 2001 From: Daniel Asztalos Date: Sun, 3 Oct 2021 21:10:19 +0200 Subject: [PATCH 0781/1714] Typo fixed (#1930) Signed-off-by: asztalosdani --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 11ea8780b6..52e6a995c2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -556,7 +556,7 @@ Network numbers will always be increasing or remain the same, but never decrease. ``net_io_counters.cache_clear()`` can be used to invalidate the *nowrap* cache. - On machines with no network iterfaces this function will return ``None`` or + On machines with no network interfaces this function will return ``None`` or ``{}`` if *pernic* is ``True``. >>> import psutil From 88055533025aa92f8c607e05263fc565f8b929ab Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 3 Oct 2021 21:21:43 +0200 Subject: [PATCH 0782/1714] fix #1919 / Linux: sensors_battery() may raise TypeError on PureOS Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 1 + psutil/_common.py | 1 - psutil/_pslinux.py | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index db0e4261a8..adcaaa459a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -27,6 +27,7 @@ XXXX-XX-XX - 1904_: [Windows] OpenProcess fails with ERROR_SUCCESS due to GetLastError() called after sprintf(). (patch by alxchk) - 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown +- 1919_: [Linux] sensors_battery() can raise TypeError on PureOS. - 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap - 1948_: Process' memoize_when_activated decorator was not thread-safe. (patch by Xuehai Pan) diff --git a/psutil/_common.py b/psutil/_common.py index 306301ede8..e83568b50d 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -431,7 +431,6 @@ def memoize_when_activated(fun): >>> foo() >>> """ - @functools.wraps(fun) def wrapper(self): try: diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 640a0f3de3..3afe6c653c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1390,7 +1390,10 @@ def multi_cat(*paths): for path in paths: ret = cat(path, fallback=null) if ret != null: - return int(ret) if ret.isdigit() else ret + try: + return int(ret) + except ValueError: + return ret return None bats = [x for x in os.listdir(POWER_SUPPLY_PATH) if x.startswith('BAT') or From 46042187f58635598c8d7c7a771d131fc4e84ace Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 4 Oct 2021 23:01:57 +0200 Subject: [PATCH 0783/1714] expand Process.threads() doc (take inspiration from #1989) Signed-off-by: Giampaolo Rodola --- docs/index.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f0bf2ba7ea..498b30943e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1453,9 +1453,16 @@ Process class .. method:: threads() - Return threads opened by process as a list of named tuples including thread - id and thread CPU times (user/system). On OpenBSD this method requires - root privileges. + Return threads opened by process as a list of named tuples. On OpenBSD this + method requires root privileges. + + - **id**: the native thread ID assigned by the kernel. If :attr:`pid` refers + to the current process, this matches the + `native_id `__ + attribute of the `threading.Thread`_ class, and can be used to reference + individual Python threads running within your own Python app. + - **user_time**: time spent in user mode. + - **system_time**: time spent in kernel mode. .. method:: cpu_times() @@ -2967,6 +2974,7 @@ Timeline .. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen .. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py .. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess +.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread .. _Tidelift security contact: https://tidelift.com/security .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme From 71e56f7141caa8cc451be23998d250c62b58f5a1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Oct 2021 00:05:57 +0200 Subject: [PATCH 0784/1714] use ThreadTask as a ctx manager Signed-off-by: Giampaolo Rodola --- psutil/tests/test_linux.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index f5243c2cde..23f6b22124 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1428,9 +1428,7 @@ def test_issue_687(self): # - 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: + with ThreadTask(): p = psutil.Process() threads = p.threads() self.assertEqual(len(threads), 2) @@ -1439,8 +1437,6 @@ def test_issue_687(self): 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. From a02ebdedc945fedb7173edfed5ad4d6f7a04a760 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Oct 2021 18:53:42 +0200 Subject: [PATCH 0785/1714] fix #1965 / win / users() / critical: Py_INCREF(Py_None) before setting None object in C Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/_psutil_windows.c | 1 + 2 files changed, 3 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index adcaaa459a..55c6b2b955 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -33,6 +33,8 @@ XXXX-XX-XX by Xuehai Pan) - 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. (patch by MaWe2019) +- 1965_: [Windows] fix "Fatal Python error: deallocating None" when calling + psutil.users() multiple times. - 1991_: process_iter() can raise TypeError if invoked from multiple threads (not thread-safe). diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 13cf58c482..8abd8a8d76 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1252,6 +1252,7 @@ psutil_users(PyObject *self, PyObject *args) { goto error; } else { + Py_INCREF(Py_None); py_address = Py_None; } From e15922b9f7f389679c73ab15ab5ac237b7c0d37f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Oct 2021 19:20:51 +0200 Subject: [PATCH 0786/1714] [macOS] dynamic set buffer size for process connections/fds (fixes #1901) (#1903) --- HISTORY.rst | 5 ++ psutil/_psosx.py | 57 +++----------- psutil/_psutil_osx.c | 136 +++++++++++++++++++-------------- psutil/_psutil_posix.c | 2 +- psutil/arch/osx/process_info.c | 13 ++-- 5 files changed, 102 insertions(+), 111 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 55c6b2b955..a8284ad290 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,6 +24,11 @@ XXXX-XX-XX where it first finds one - 1874_: [Solaris] swap output error due to incorrect range. - 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. +- 1901_: [macOS] different functions, especially process' open_files() and + connections() methods, could randomly raise AccessDenied because the internal + buffer of `proc_pidinfo(PROC_PIDLISTFDS)` syscall was not big enough. We now + dynamically increase the buffer size until it's big enough instead of giving + up and raising AccessDenied, which was a fallback to avoid crashing. - 1904_: [Windows] OpenProcess fails with ERROR_SUCCESS due to GetLastError() called after sprintf(). (patch by alxchk) - 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown diff --git a/psutil/_psosx.py b/psutil/_psosx.py index d948cc15a8..eda70d213d 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -4,7 +4,6 @@ """macOS platform implementation.""" -import contextlib import errno import functools import os @@ -354,32 +353,6 @@ def wrapper(self, *args, **kwargs): return wrapper -@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 - - class Process(object): """Wrapper class around underlying C implementation.""" @@ -402,8 +375,7 @@ def _get_kinfo_proc(self): @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 @@ -422,18 +394,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): @@ -442,8 +411,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): @@ -516,8 +484,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) @@ -530,8 +497,7 @@ def connections(self, kind='inet'): 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 @@ -544,8 +510,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): @@ -553,13 +518,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): diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 5a77de1438..6f824eb21f 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -108,6 +108,72 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) } +/* + * A wrapper around proc_pidinfo(PROC_PIDLISTFDS), which dynamically sets + * the buffer size. + */ +static struct proc_fdinfo* +psutil_proc_list_fds(pid_t pid, int *num_fds) { + int ret; + int fds_size = 0; + int max_size = 24 * 1024 * 1024; // 24M + struct proc_fdinfo *fds_pointer = NULL; + + errno = 0; + ret = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); + if (ret <= 0) { + psutil_raise_for_pid(pid, "proc_pidinfo(PROC_PIDLISTFDS) 1/2"); + goto error; + } + + while (1) { + if (ret > fds_size) { + while (ret > fds_size) { + fds_size += PROC_PIDLISTFD_SIZE * 32; + if (fds_size > max_size) { + PyErr_Format(PyExc_RuntimeError, + "prevent malloc() to allocate > 24M"); + goto error; + } + } + + if (fds_pointer != NULL) { + free(fds_pointer); + } + fds_pointer = malloc(fds_size); + + if (fds_pointer == NULL) { + PyErr_NoMemory(); + goto error; + } + } + + errno = 0; + ret = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds_pointer, fds_size); + if (ret <= 0) { + psutil_raise_for_pid(pid, "proc_pidinfo(PROC_PIDLISTFDS) 2/2"); + goto error; + } + + if (ret + (int)PROC_PIDLISTFD_SIZE >= fds_size) { + psutil_debug("PROC_PIDLISTFDS: make room for 1 extra fd"); + ret = fds_size + (int)PROC_PIDLISTFD_SIZE; + continue; + } + + break; + } + + *num_fds = (ret / (int)PROC_PIDLISTFD_SIZE); + return fds_pointer; + +error: + if (fds_pointer != NULL) + free(fds_pointer); + return NULL; +} + + /* * Return a Python list of all the PIDs running on the system. */ @@ -791,7 +857,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (err != KERN_SUCCESS) { // errcode 4 is "invalid argument" (access denied) if (err == 4) { - AccessDenied("task_info"); + AccessDenied("task_info(TASK_BASIC_INFO)"); } else { // otherwise throw a runtime error with appropriate error code @@ -866,15 +932,12 @@ psutil_proc_threads(PyObject *self, PyObject *args) { static PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { pid_t pid; - int pidinfo_result; - int iterations; + int num_fds; int i; unsigned long nb; - struct proc_fdinfo *fds_pointer = NULL; struct proc_fdinfo *fdp_pointer; struct vnode_fdinfowithpath vi; - PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; PyObject *py_path = NULL; @@ -885,23 +948,11 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; - pidinfo_result = psutil_proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); - if (pidinfo_result <= 0) - goto error; - - fds_pointer = malloc(pidinfo_result); - if (fds_pointer == NULL) { - PyErr_NoMemory(); - goto error; - } - pidinfo_result = psutil_proc_pidinfo( - pid, PROC_PIDLISTFDS, 0, fds_pointer, pidinfo_result); - if (pidinfo_result <= 0) + fds_pointer = psutil_proc_list_fds(pid, &num_fds); + if (fds_pointer == NULL) goto error; - iterations = (pidinfo_result / PROC_PIDLISTFD_SIZE); - - for (i = 0; i < iterations; i++) { + for (i = 0; i < num_fds; i++) { fdp_pointer = &fds_pointer[i]; if (fdp_pointer->proc_fdtype == PROX_FDTYPE_VNODE) { @@ -968,15 +1019,12 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { static PyObject * psutil_proc_connections(PyObject *self, PyObject *args) { pid_t pid; - int pidinfo_result; - int iterations; + int num_fds; int i; unsigned long nb; - struct proc_fdinfo *fds_pointer = NULL; struct proc_fdinfo *fdp_pointer; struct socket_fdinfo si; - PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; @@ -997,25 +1045,11 @@ psutil_proc_connections(PyObject *self, PyObject *args) { goto error; } - if (pid == 0) - return py_retlist; - pidinfo_result = psutil_proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); - if (pidinfo_result <= 0) - goto error; - - fds_pointer = malloc(pidinfo_result); - if (fds_pointer == NULL) { - PyErr_NoMemory(); - goto error; - } - - pidinfo_result = psutil_proc_pidinfo( - pid, PROC_PIDLISTFDS, 0, fds_pointer, pidinfo_result); - if (pidinfo_result <= 0) + fds_pointer = psutil_proc_list_fds(pid, &num_fds); + if (fds_pointer == NULL) goto error; - iterations = (pidinfo_result / PROC_PIDLISTFD_SIZE); - for (i = 0; i < iterations; i++) { + for (i = 0; i < num_fds; i++) { py_tuple = NULL; py_laddr = NULL; py_raddr = NULL; @@ -1176,30 +1210,18 @@ psutil_proc_connections(PyObject *self, PyObject *args) { static PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { pid_t pid; - int pidinfo_result; - int num; + int num_fds; struct proc_fdinfo *fds_pointer; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - pidinfo_result = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); - if (pidinfo_result <= 0) - return PyErr_SetFromErrno(PyExc_OSError); - - fds_pointer = malloc(pidinfo_result); + fds_pointer = psutil_proc_list_fds(pid, &num_fds); if (fds_pointer == NULL) - return PyErr_NoMemory(); - pidinfo_result = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds_pointer, - pidinfo_result); - if (pidinfo_result <= 0) { - free(fds_pointer); - return PyErr_SetFromErrno(PyExc_OSError); - } + return NULL; - num = (pidinfo_result / PROC_PIDLISTFD_SIZE); free(fds_pointer); - return Py_BuildValue("i", num); + return Py_BuildValue("i", num_fds); } diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 305cec76d1..7c0d548c4c 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -147,7 +147,7 @@ psutil_pid_exists(pid_t pid) { */ void psutil_raise_for_pid(long pid, char *syscall) { - if (errno != 0) // unlikely + if (errno != 0) PyErr_SetFromOSErrnoWithSyscall(syscall); else if (psutil_pid_exists(pid) == 0) NoSuchProcess(syscall); diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 15ad3b89c4..6ab4275089 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -361,7 +361,7 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { // sysctl succeeds but len is zero, happens when process has gone away if (len == 0) { - NoSuchProcess("sysctl (len == 0)"); + NoSuchProcess("sysctl(kinfo_proc), len == 0"); return -1; } return 0; @@ -370,8 +370,8 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { /* * A wrapper around proc_pidinfo(). - * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c. - * Returns 0 on failure (and Python exception gets already set). + * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c + * Returns 0 on failure. */ int psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { @@ -380,11 +380,12 @@ psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { ret = proc_pidinfo(pid, flavor, arg, pti, size); if (ret <= 0) { - psutil_raise_for_pid(pid, "proc_pidinfo() failed"); + psutil_raise_for_pid(pid, "proc_pidinfo()"); return 0; } - else if ((unsigned long )ret < sizeof(pti)) { - psutil_raise_for_pid(pid, "proc_pidinfo() len mismatch"); + if ((unsigned long)ret < sizeof(pti)) { + psutil_raise_for_pid( + pid, "proc_pidinfo() return size < sizeof(struct_pointer)"); return 0; } return ret; From 741c143ea371ef3ecf83254341f443182a9547f1 Mon Sep 17 00:00:00 2001 From: Nikita Radchenko Date: Tue, 5 Oct 2021 21:13:38 +0300 Subject: [PATCH 0787/1714] Handle ENAMETOOLONG on Linux (#1940) (#1955) When resolving process file descriptors symlinks in procfs (/proc/PID/fd/FD), the kernel can only deal with file paths no longer than PAGE_SIZE (which usually equals to PATH_MAX). https://elixir.bootlin.com/linux/v5.12/source/fs/proc/base.c#L1759 Resolving fd symlink that corresponds to a file with a path longer than PATH_MAX with readlink(2) would result in ENAMETOOLONG error (see details in #1940). We can do nothing to fix this in userspace; therefore these errors should be ignored. --- CREDITS | 6 +++++- HISTORY.rst | 2 ++ psutil/_pslinux.py | 6 ++++++ psutil/tests/test_linux.py | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 280c3b1cd5..cc7e7a596d 100644 --- a/CREDITS +++ b/CREDITS @@ -745,6 +745,10 @@ N: David Knaack W: https://github.com/davidkna I: 1921 +N: Nikita Radchenko +W: https://github.com/nradchenko +I: 1940 + N: MaWe2019 W: https://github.com/MaWe2019 I: 1953 @@ -761,4 +765,4 @@ I: 1598 N: Xuehai Pan W: https://github.com/XuehaiPan -I: 1948 +I: 1948 \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst index a8284ad290..ccc5c82945 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -34,6 +34,8 @@ XXXX-XX-XX - 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown - 1919_: [Linux] sensors_battery() can raise TypeError on PureOS. - 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap +- 1940_: [Linux] psutil does not handle ENAMETOOLONG when accessing process + file descriptors in procfs - 1948_: Process' memoize_when_activated decorator was not thread-safe. (patch by Xuehai Pan) - 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 3afe6c653c..1917e20d08 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -832,6 +832,9 @@ def get_proc_inodes(self, pid): if err.errno == errno.EINVAL: # not a link continue + if err.errno == errno.ENAMETOOLONG: + # file name too long + continue raise else: if inode.startswith('socket:['): @@ -2101,6 +2104,9 @@ def open_files(self): if err.errno == errno.EINVAL: # not a link continue + if err.errno == errno.ENAMETOOLONG: + # file name too long + continue raise else: # If path is not an absolute there's no way to tell diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 23f6b22124..3fda7874b4 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1446,6 +1446,25 @@ def test_pid_exists_no_proc_status(self): assert psutil.pid_exists(os.getpid()) assert m.called + def test_get_proc_inodes_fd_broken(self): + # Simulate a case where /proc/{pid}/fd/{fd} symlink + # points to a file with full path longer than PATH_MAX + + def get_inodes(): + connections = psutil._pslinux.Connections() + connections._procfs_path = psutil._common.get_procfs_path() + return connections.get_proc_inodes(os.getpid()) + + 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: + assert not get_inodes() + assert m.called # ===================================================================== # --- sensors @@ -1830,6 +1849,22 @@ def test_open_files_fd_gone(self): assert not files assert m.called + def test_open_files_fd_broken(self): + # Simulate a case where /proc/{pid}/fd/{fd} symlink + # points to a file with full path longer than PATH_MAX + + 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: + files = p.open_files() + assert not files + assert m.called + # --- mocked tests def test_terminal_mocked(self): From 5b4ee076dd570454eee99fb41cb09989501220ed Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Oct 2021 20:24:20 +0200 Subject: [PATCH 0788/1714] move/rename tests re. to #1955 Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 +- psutil/tests/test_linux.py | 35 +++++++++++++---------------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ccc5c82945..cd27633faf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -35,7 +35,7 @@ XXXX-XX-XX - 1919_: [Linux] sensors_battery() can raise TypeError on PureOS. - 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap - 1940_: [Linux] psutil does not handle ENAMETOOLONG when accessing process - file descriptors in procfs + file descriptors in procfs. (patch by Nikita Radchenko) - 1948_: Process' memoize_when_activated decorator was not thread-safe. (patch by Xuehai Pan) - 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 3fda7874b4..6c01edbda3 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1446,25 +1446,6 @@ def test_pid_exists_no_proc_status(self): assert psutil.pid_exists(os.getpid()) assert m.called - def test_get_proc_inodes_fd_broken(self): - # Simulate a case where /proc/{pid}/fd/{fd} symlink - # points to a file with full path longer than PATH_MAX - - def get_inodes(): - connections = psutil._pslinux.Connections() - connections._procfs_path = psutil._common.get_procfs_path() - return connections.get_proc_inodes(os.getpid()) - - 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: - assert not get_inodes() - assert m.called # ===================================================================== # --- sensors @@ -1849,10 +1830,10 @@ def test_open_files_fd_gone(self): assert not files assert m.called - def test_open_files_fd_broken(self): + 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 - + # 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'): @@ -2100,6 +2081,16 @@ def test_status_file_parsing(self): self.assertEqual(gids.saved, 1006) self.assertEqual(p._proc._get_eligible_cpus(), list(range(0, 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() + assert not p.connections() + assert m.called + @unittest.skipIf(not LINUX, "LINUX only") class TestProcessAgainstStatus(PsutilTestCase): From abbf5e42b396e2d6e132364a15b66d94f28dc62f Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Fri, 8 Oct 2021 11:53:01 +0530 Subject: [PATCH 0789/1714] Use assertEqual instead of assertEquals for Python 3.11 compatibility. (#1995) --- psutil/tests/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index aeb282c878..01b710c209 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -725,7 +725,7 @@ 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())) + self.assertEqual(e["THINK_OF_A_NUMBER"], str(os.getpid())) def test_environ_64(self): p = psutil.Process(self.proc64.pid) From 9825ca2fd123a051cb612f8501d702fb51568411 Mon Sep 17 00:00:00 2001 From: Saeed Rasooli Date: Tue, 12 Oct 2021 18:34:11 +0330 Subject: [PATCH 0790/1714] add support for MidnightBSD (#1996) Signed-off-by: Saeed Rasooli --- psutil/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index e83568b50d..879e8477d5 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -83,7 +83,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 From aa0412e4ead1f2ab53cb95d6121aea8ac2fd20eb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Oct 2021 17:08:27 +0200 Subject: [PATCH 0791/1714] give CREDITS to @ilius for #1996 + update doc Signed-off-by: Giampaolo Rodola --- CREDITS | 6 +++++- HISTORY.rst | 1 + docs/index.rst | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index cc7e7a596d..794bb53ad4 100644 --- a/CREDITS +++ b/CREDITS @@ -765,4 +765,8 @@ I: 1598 N: Xuehai Pan W: https://github.com/XuehaiPan -I: 1948 \ No newline at end of file +I: 1948 + +N: Saeed Rasooli +W: https://github.com/ilius +I: 1996 diff --git a/HISTORY.rst b/HISTORY.rst index cd27633faf..3399e186fe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,7 @@ XXXX-XX-XX - 1992_: NoSuchProcess message now specifies if the PID has been reused. - 1992_: error classes (NoSuchProcess, AccessDenied, etc.) now have a better formatted and separated `__repr__` and `__str__` implementations. +- 1996_: add support for MidnightBSD. (patch by Saeed Rasooli) **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 498b30943e..4ddddba509 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2567,6 +2567,7 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= +* psutil 5.8.1 (2021-10): **MidnightBSD** * psutil 5.8.0 (2020-12): **PyPy 2** on Windows * psutil 5.7.1 (2020-07): **Windows Nano** * psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support From 4e5626d05e876ef0630e93976449de309e18b499 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Oct 2021 17:16:23 +0200 Subject: [PATCH 0792/1714] tests: move per-module imports at the top of the file(s) Signed-off-by: Giampaolo Rodola --- psutil/tests/__init__.py | 6 +++--- psutil/tests/test_linux.py | 11 ++++++----- psutil/tests/test_osx.py | 6 ++++-- psutil/tests/test_windows.py | 6 ++++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index e0df67fa30..245ff2f3dc 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -72,6 +72,9 @@ else: enum = None +if POSIX: + from psutil._psposix import wait_pid + __all__ = [ # constants @@ -482,9 +485,6 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): Does nothing if the process does not exist. Return process exit status. """ - if POSIX: - from psutil._psposix import wait_pid - def wait(proc, timeout): if isinstance(proc, subprocess.Popen) and not PY3: proc.wait() diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 6c01edbda3..d2d8e7d4be 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -47,6 +47,11 @@ from psutil.tests import unittest from psutil.tests import which +if LINUX: + from psutil._pslinux import calculate_avail_vmem + from psutil._pslinux import CLOCK_TICKS + from psutil._pslinux import open_binary + HERE = os.path.abspath(os.path.dirname(__file__)) SIOCGIFADDR = 0x8915 @@ -58,6 +63,7 @@ SECTOR_SIZE = 512 EMPTY_TEMPERATURES = not glob.glob('/sys/class/hwmon/hwmon*') + # ===================================================================== # --- utils # ===================================================================== @@ -358,9 +364,6 @@ def test_warnings_on_misses(self): def test_avail_old_percent(self): # Make sure that our calculation of avail mem for old kernels # is off by max 15%. - 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: @@ -1994,8 +1997,6 @@ def test_cwd_zombie(self): self.assertEqual(exc.exception.name, p.name()) def test_stat_file_parsing(self): - from psutil._pslinux import CLOCK_TICKS - args = [ "0", # pid "(cat)", # name diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index b7a0b088e2..f797de7146 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -11,6 +11,7 @@ import psutil from psutil import MACOS +from psutil import POSIX from psutil.tests import HAS_BATTERY from psutil.tests import PsutilTestCase from psutil.tests import retry_on_failure @@ -21,6 +22,9 @@ from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import unittest +if POSIX: + from psutil._psutil_posix import getpagesize + def sysctl(cmdline): """Expects a sysctl command with an argument and parse the result @@ -36,8 +40,6 @@ def sysctl(cmdline): def vm_stat(field): """Wrapper around 'vm_stat' cmdline utility.""" - from psutil._psutil_posix import getpagesize - out = sh('vm_stat') for line in out.split('\n'): if field in line: diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 01b710c209..9d5b0113b8 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -47,6 +47,10 @@ import win32process import wmi # requires "pip install wmi" / "make setup-dev-env" +if WINDOWS: + from psutil._pswindows import ACCESS_DENIED_SET + from psutil._pswindows import convert_oserror + cext = psutil._psplatform.cext @@ -56,7 +60,6 @@ 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: @@ -633,7 +636,6 @@ def test_num_handles(self): 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) From 74c4d65957379b17fb82aa59a3ecdbba209a7cd4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Oct 2021 19:02:05 +0200 Subject: [PATCH 0793/1714] remove dead windows code Signed-off-by: Giampaolo Rodola --- psutil/tests/test_windows.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 9d5b0113b8..d9b65adbeb 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -48,26 +48,12 @@ import wmi # requires "pip install wmi" / "make setup-dev-env" if WINDOWS: - from psutil._pswindows import ACCESS_DENIED_SET from psutil._pswindows import convert_oserror cext = psutil._psplatform.cext -def wrap_exceptions(fun): - def wrapper(self, *args, **kwargs): - try: - return fun(self, *args, **kwargs) - except OSError as err: - 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 - - @unittest.skipIf(not WINDOWS, "WINDOWS only") @unittest.skipIf(PYPY, "pywin32 not available on PYPY") # https://github.com/giampaolo/psutil/pull/1762#issuecomment-632892692 From 4d03c917561297de1e6017059c60eb879cf8353c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Oct 2021 21:34:57 +0200 Subject: [PATCH 0794/1714] [Linux] convert /dev/root device to real path (fixes #1999) (#2000) Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 + psutil/_pslinux.py | 76 ++++++++++++++++++++++++++++++++++++++ psutil/tests/test_linux.py | 64 ++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 3399e186fe..fb43a19658 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,8 @@ XXXX-XX-XX - 1992_: error classes (NoSuchProcess, AccessDenied, etc.) now have a better formatted and separated `__repr__` and `__str__` implementations. - 1996_: add support for MidnightBSD. (patch by Saeed Rasooli) +- 1999_: [Linux] disk_partitions(): convert "/dev/root" device (an alias used + on some Linux distros) to real root device path. **Bug fixes** diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1917e20d08..f6a833017a 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1189,6 +1189,80 @@ def read_sysfs(): 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() @@ -1216,6 +1290,8 @@ def disk_partitions(all=False): 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 diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d2d8e7d4be..38322db637 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -28,6 +28,7 @@ from psutil._compat import PY3 from psutil._compat import u from psutil.tests import call_until +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 @@ -51,6 +52,7 @@ from psutil._pslinux import calculate_avail_vmem from psutil._pslinux import CLOCK_TICKS from psutil._pslinux import open_binary + from psutil._pslinux import RootFsDeviceFinder HERE = os.path.abspath(os.path.dirname(__file__)) @@ -1263,6 +1265,68 @@ def exists(path): 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 # ===================================================================== From e91bae62e6e6ee9c3ad27c1fe3013f929309e1b0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Oct 2021 22:14:53 +0200 Subject: [PATCH 0795/1714] add debug() prints in a couple of places Signed-off-by: Giampaolo Rodola --- psutil/_common.py | 2 ++ psutil/_pslinux.py | 8 ++++++-- psutil/_pssunos.py | 2 +- psutil/_pswindows.py | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 879e8477d5..a2b8bab6ca 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -836,6 +836,8 @@ def debug(msg): """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" fname, lineno, func_name, lines, index = inspect.getframeinfo( inspect.currentframe().f_back) + if isinstance(msg, Exception): + msg = "ignoring %s" % msg print("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), # NOQA file=sys.stderr) else: diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index f6a833017a..57925b87c7 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -834,6 +834,7 @@ def get_proc_inodes(self, pid): continue if err.errno == errno.ENAMETOOLONG: # file name too long + debug(err) continue raise else: @@ -1081,6 +1082,8 @@ def net_if_stats(): # 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) return ret @@ -1387,7 +1390,7 @@ def sensors_temperatures(): path = os.path.join(base, 'type') unit_name = cat(path, binary=False) except (IOError, OSError, ValueError) as err: - debug("ignoring %r for file %r" % (err, path)) + debug(err) continue trip_paths = glob.glob(base + '/trip_point*') @@ -1443,7 +1446,7 @@ def sensors_fans(): try: current = int(cat(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) @@ -2182,6 +2185,7 @@ def open_files(self): continue if err.errno == errno.ENAMETOOLONG: # file name too long + debug(err) continue raise else: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 84d78814f9..355a762399 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -231,7 +231,7 @@ def disk_partitions(all=False): continue except OSError as err: # https://github.com/giampaolo/psutil/issues/1674 - debug("skipping %r: %r" % (mountpoint, err)) + debug("skipping %r: %s" % (mountpoint, err)) continue maxfile = maxpath = None # set later ntuple = _common.sdiskpart(device, mountpoint, fstype, opts, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 6b1a34de23..348b72e132 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -767,7 +767,7 @@ def exe(self): # 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 forced into AccessDenied" % err) + debug("%r translated into AccessDenied" % err) raise AccessDenied(self.pid, self._name) raise else: From e72438f08ac7aa93b33fddcb11298ece28d0150d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Oct 2021 22:52:32 +0200 Subject: [PATCH 0796/1714] Changes to debug() function: * use str() if exception derives from OSError / EnvironmentError. This way we will print the file name (if it exists). * use repr() for any other exception * add tests for debug() function * backport contextlib.redirect_stderr Signed-off-by: Giampaolo Rodola --- psutil/_common.py | 6 +++++- psutil/_compat.py | 17 +++++++++++++++++ psutil/tests/test_misc.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index a2b8bab6ca..6a2f38e160 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -837,7 +837,11 @@ def debug(msg): fname, lineno, func_name, lines, index = inspect.getframeinfo( inspect.currentframe().f_back) if isinstance(msg, Exception): - msg = "ignoring %s" % msg + 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("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), # NOQA file=sys.stderr) else: diff --git a/psutil/_compat.py b/psutil/_compat.py index 909386870f..e5275f5fc6 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -8,6 +8,7 @@ """ import collections +import contextlib import errno import functools import os @@ -25,6 +26,8 @@ "lru_cache", # shutil module "which", "get_terminal_size", + # contextlib module + "redirect_stderr", # python 3 exceptions "FileNotFoundError", "PermissionError", "ProcessLookupError", "InterruptedError", "ChildProcessError", "FileExistsError"] @@ -430,3 +433,17 @@ def get_terminal_size(fallback=(80, 24)): except ImportError: class SubprocessTimeoutExpired: pass + + +# python 3.5 +try: + from contextlib import redirect_stderr +except ImportError: + @contextlib.contextmanager + def redirect_stderr(new_target): + original = getattr(sys, "stderr") + try: + setattr(sys, "stderr", new_target) + yield new_target + finally: + setattr(sys, "stderr", original) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index fc9f5b13f8..70f6a37eb5 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -22,11 +22,13 @@ from psutil import LINUX from psutil import POSIX from psutil import WINDOWS +from psutil._common import debug 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._compat import PY3 +from psutil._compat import redirect_stderr from psutil.tests import APPVEYOR from psutil.tests import CI_TESTING from psutil.tests import HAS_BATTERY @@ -401,6 +403,35 @@ def test_sanity_version_check(self): reload_module(psutil) self.assertIn("version conflict", str(cm.exception).lower()) + 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__, 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) + # =================================================================== # --- Tests for wrap_numbers() function. From adbfeae3525769c7c001000ab8dd816e88b18736 Mon Sep 17 00:00:00 2001 From: PetrPospisil Date: Mon, 18 Oct 2021 20:47:18 +0200 Subject: [PATCH 0797/1714] Fix WOW64 issue: NtQuerySystemInformation does not set ImageName.MaximumLength (#1981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix WOW64 issue when the NtQuerySystemInformation does not set ImageName.MaximumLength when STATUS_INFO_LENGTH_MISMATCH is returned Signed-off-by: Petr Pospíšil --- psutil/_psutil_windows.c | 46 ++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 8abd8a8d76..0a6ac72669 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -338,8 +338,8 @@ static PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { DWORD pid; NTSTATUS status; - PVOID buffer; - ULONG bufferSize = 0x100; + PVOID buffer = NULL; + ULONG bufferSize = 0x104 * 2; // WIN_MAX_PATH * sizeof(wchar_t) SYSTEM_PROCESS_ID_INFORMATION processIdInfo; PyObject *py_exe; @@ -350,8 +350,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { return AccessDenied("automatically set for PID 0"); buffer = MALLOC_ZERO(bufferSize); - if (! buffer) - return PyErr_NoMemory(); + if (! buffer) { + PyErr_NoMemory(); + return NULL; + } + processIdInfo.ProcessId = (HANDLE)(ULONG_PTR)pid; processIdInfo.ImageName.Length = 0; processIdInfo.ImageName.MaximumLength = (USHORT)bufferSize; @@ -363,12 +366,41 @@ psutil_proc_exe(PyObject *self, PyObject *args) { sizeof(SYSTEM_PROCESS_ID_INFORMATION), NULL); - if (status == STATUS_INFO_LENGTH_MISMATCH) { + if (status == STATUS_INFO_LENGTH_MISMATCH && processIdInfo.ImageName.MaximumLength <= bufferSize) { + // Required length was NOT stored in MaximumLength (WOW64 issue). + + ULONG maxBufferSize = 0x7FFF * 2; // NTFS_MAX_PATH * sizeof(wchar_t) + + do { + // Iteratively double the size of the buffer up to maxBufferSize + bufferSize *= 2; + + FREE(buffer); + buffer = MALLOC_ZERO(bufferSize); + if (! buffer) { + PyErr_NoMemory(); + return NULL; + } + + processIdInfo.ImageName.MaximumLength = (USHORT)bufferSize; + processIdInfo.ImageName.Buffer = buffer; + + status = NtQuerySystemInformation( + SystemProcessIdInformation, + &processIdInfo, + sizeof(SYSTEM_PROCESS_ID_INFORMATION), + NULL); + } while (status == STATUS_INFO_LENGTH_MISMATCH && bufferSize <= maxBufferSize); + } + else if (status == STATUS_INFO_LENGTH_MISMATCH) { // Required length is stored in MaximumLength. FREE(buffer); buffer = MALLOC_ZERO(processIdInfo.ImageName.MaximumLength); - if (! buffer) - return PyErr_NoMemory(); + if (! buffer) { + PyErr_NoMemory(); + return NULL; + } + processIdInfo.ImageName.Buffer = buffer; status = NtQuerySystemInformation( From d1cce5caed1b9c3809ed5e2f80c9c413afd0c09a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Oct 2021 20:53:32 +0200 Subject: [PATCH 0798/1714] update HISTORY to include #1981, CREDIT @PetrPospisil, fix C linter warnings Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ HISTORY.rst | 3 +++ psutil/_psutil_windows.c | 12 ++++++------ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CREDITS b/CREDITS index 794bb53ad4..e7d9fa7821 100644 --- a/CREDITS +++ b/CREDITS @@ -770,3 +770,7 @@ I: 1948 N: Saeed Rasooli W: https://github.com/ilius I: 1996 + +N: PetrPospisil +W: https://github.com/PetrPospisil +I: 1980 diff --git a/HISTORY.rst b/HISTORY.rst index fb43a19658..9319ba1c5b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -45,6 +45,9 @@ XXXX-XX-XX (patch by MaWe2019) - 1965_: [Windows] fix "Fatal Python error: deallocating None" when calling psutil.users() multiple times. +- 1980_: [Windows] 32bit / WOW64 processes fails to read process name longer + than 128 characters resulting in AccessDenied. This is now fixed. (patch + by PetrPospisil) - 1991_: process_iter() can raise TypeError if invoked from multiple threads (not thread-safe). diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 0a6ac72669..6a128ae2c1 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -366,15 +366,14 @@ psutil_proc_exe(PyObject *self, PyObject *args) { sizeof(SYSTEM_PROCESS_ID_INFORMATION), NULL); - if (status == STATUS_INFO_LENGTH_MISMATCH && processIdInfo.ImageName.MaximumLength <= bufferSize) { + if ((status == STATUS_INFO_LENGTH_MISMATCH) && + (processIdInfo.ImageName.MaximumLength <= bufferSize)) + { // Required length was NOT stored in MaximumLength (WOW64 issue). - - ULONG maxBufferSize = 0x7FFF * 2; // NTFS_MAX_PATH * sizeof(wchar_t) - + ULONG maxBufferSize = 0x7FFF * 2; // NTFS_MAX_PATH * sizeof(wchar_t) do { // Iteratively double the size of the buffer up to maxBufferSize bufferSize *= 2; - FREE(buffer); buffer = MALLOC_ZERO(bufferSize); if (! buffer) { @@ -390,7 +389,8 @@ psutil_proc_exe(PyObject *self, PyObject *args) { &processIdInfo, sizeof(SYSTEM_PROCESS_ID_INFORMATION), NULL); - } while (status == STATUS_INFO_LENGTH_MISMATCH && bufferSize <= maxBufferSize); + } while ((status == STATUS_INFO_LENGTH_MISMATCH) && + (bufferSize <= maxBufferSize)); } else if (status == STATUS_INFO_LENGTH_MISMATCH) { // Required length is stored in MaximumLength. From 0e15b4890a1d84f2aa800b745fdb0642d2ee966d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Oct 2021 01:04:48 +0200 Subject: [PATCH 0799/1714] PSUTIL_DEBUG: print file + line number for C ext modules (#2005) --- .github/workflows/build.yml | 5 ++-- CONTRIBUTING.md | 2 ++ HISTORY.rst | 2 ++ Makefile | 2 +- appveyor.yml | 1 - docs/index.rst | 24 +++++++++++++++++++ psutil/__init__.py | 9 +++++++ psutil/_common.py | 13 ++++------ psutil/_psutil_aix.c | 4 ++-- psutil/_psutil_bsd.c | 4 ++-- psutil/_psutil_common.c | 45 +++++++++++++---------------------- psutil/_psutil_common.h | 14 ++++++++--- psutil/_psutil_linux.c | 4 ++-- psutil/_psutil_osx.c | 4 ++-- psutil/_psutil_sunos.c | 4 ++-- psutil/_psutil_windows.c | 4 ++-- psutil/tests/__init__.py | 9 +++++++ psutil/tests/runner.py | 5 +--- psutil/tests/test_memleaks.py | 3 +++ psutil/tests/test_process.py | 1 - 20 files changed, 98 insertions(+), 61 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84f3e74902..0559acd25c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,8 +32,8 @@ jobs: - {name: Linux, python: '3.9', os: ubuntu-latest} env: CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_TESTING=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_SKIP: cp35-* pp* @@ -94,7 +94,6 @@ jobs: export \ PYTHONUNBUFFERED=1 \ PYTHONWARNINGS=always \ - PSUTIL_TESTING=1 \ PSUTIL_DEBUG=1 python3 -m pip install --user setuptools python3 setup.py install diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69c9dd9478..481793fc61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,8 @@ Issues editing the default issue **template**. There is a bot which automatically assigns **labels** based on issue's title and body format. Labels help keeping the issues properly organized and searchable (by OS, issue type, etc.). +* When reporting a malfunction, consider enabling + [debug mode](https://psutil.readthedocs.io/en/latest/#debug-mode) first. * To report a **security vulnerability**, use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and the disclosure of the reported problem. diff --git a/HISTORY.rst b/HISTORY.rst index 9319ba1c5b..5531f9d156 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,8 @@ XXXX-XX-XX - 1996_: add support for MidnightBSD. (patch by Saeed Rasooli) - 1999_: [Linux] disk_partitions(): convert "/dev/root" device (an alias used on some Linux distros) to real root device path. +- 2005_: PSUTIL_DEBUG mode now prints file name and line number of the debug + messages coming from C extension modules. **Bug fixes** diff --git a/Makefile b/Makefile index 1f4f27d460..f255b6511b 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ BUILD_OPTS = `$(PYTHON) -c \ # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` -TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_TESTING=1 PSUTIL_DEBUG=1 +TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_DEBUG=1 all: test diff --git a/appveyor.yml b/appveyor.yml index aaf7de3006..59c47f0ad3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,7 +16,6 @@ environment: WITH_COMPILER: "cmd /E:ON /V:ON /C .\\.ci\\appveyor\\run_with_compiler.cmd" PYTHONWARNINGS: always PYTHONUNBUFFERED: 1 - PSUTIL_TESTING: 1 PSUTIL_DEBUG: 1 matrix: # 32 bits diff --git a/docs/index.rst b/docs/index.rst index 4ddddba509..6738f5a28d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2553,6 +2553,30 @@ Running tests $ python3 -m psutil.tests +Debug mode +========== + +If you want to debug unusual situations or want to report a bug, it may be +useful to enable debug mode via ``PSUTIL_DEBUG`` environment variable. +In this mode, psutil may (or may not) print additional information to stderr. +Usually these are error conditions which are not severe, and hence are ignored +(instead of crashing). +Unit tests automatically run with debug mode enabled. +On UNIX: + +:: + + $ PSUTIL_DEBUG=1 python3 script.py + psutil-debug [psutil/_psutil_linux.c:150]> setmntent() failed (ignored) + +On Windows: + +:: + + set PSUTIL_DEBUG=1 python.exe script.py + psutil-debug [psutil/arch/windows/process_info.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) + + Security ======== diff --git a/psutil/__init__.py b/psutil/__init__.py index e52fdcda62..2760c76b75 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2345,6 +2345,15 @@ def win_service_get(name): # ===================================================================== +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 diff --git a/psutil/_common.py b/psutil/_common.py index 6a2f38e160..5e6dbb6d6b 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -41,6 +41,7 @@ # 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', 0)) __all__ = [ # OS constants @@ -829,11 +830,10 @@ def print_color( SetConsoleTextAttribute(handle, DEFAULT_COLOR) -if bool(os.getenv('PSUTIL_DEBUG', 0)): - import inspect - - def debug(msg): - """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" +def debug(msg): + """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" + if PSUTIL_DEBUG: + import inspect fname, lineno, func_name, lines, index = inspect.getframeinfo( inspect.currentframe().f_back) if isinstance(msg, Exception): @@ -844,6 +844,3 @@ def debug(msg): msg = "ignoring %r" % msg print("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), # NOQA file=sys.stderr) -else: - def debug(msg): - pass diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index a80bed70d7..ba638641e9 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -1039,8 +1039,8 @@ PsutilMethods[] = "Return CPU statistics"}, // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"set_debug", psutil_set_debug, METH_VARARGS, + "Enable or disable PSUTIL_DEBUG messages"}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 69ce6e8e45..873ebbc927 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1144,8 +1144,8 @@ static PyMethodDef mod_methods[] = { #endif // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"set_debug", psutil_set_debug, METH_VARARGS, + "Enable or disable PSUTIL_DEBUG messages"}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index ff060a51e2..6ed14658b9 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -14,8 +14,6 @@ // ==================================================================== int PSUTIL_DEBUG = 0; -int PSUTIL_TESTING = 0; -// PSUTIL_CONN_NONE // ==================================================================== @@ -135,33 +133,26 @@ AccessDenied(const char *syscall) { // --- Global utils // ==================================================================== -/* - * Enable testing mode. This has the same effect as setting PSUTIL_TESTING - * env var. This dual method exists because updating os.environ on - * Windows has no effect. Called on unit tests setup. - */ -PyObject * -psutil_set_testing(PyObject *self, PyObject *args) { - PSUTIL_TESTING = 1; - PSUTIL_DEBUG = 1; - Py_INCREF(Py_None); - return Py_None; -} +// Enable or disable PSUTIL_DEBUG messages. +PyObject * +psutil_set_debug(PyObject *self, PyObject *args) { + PyObject *value; + int x; -/* - * Print a debug message on stderr. No-op if PSUTIL_DEBUG env var is not set. - */ -void -psutil_debug(const char* format, ...) { - va_list argptr; - if (PSUTIL_DEBUG) { - va_start(argptr, format); - fprintf(stderr, "psutil-debug> "); - vfprintf(stderr, format, argptr); - fprintf(stderr, "\n"); - va_end(argptr); + if (!PyArg_ParseTuple(args, "O", &value)) + return NULL; + x = PyObject_IsTrue(value); + if (x < 0) { + return NULL; + } + else if (x == 0) { + PSUTIL_DEBUG = 0; + } + else { + PSUTIL_DEBUG = 1; } + Py_RETURN_NONE; } @@ -172,8 +163,6 @@ int psutil_setup(void) { if (getenv("PSUTIL_DEBUG") != NULL) PSUTIL_DEBUG = 1; - if (getenv("PSUTIL_TESTING") != NULL) - PSUTIL_TESTING = 1; return 0; } diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index cb0b399d8e..6cf19d6575 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -10,7 +10,6 @@ // --- Global vars / constants // ==================================================================== -extern int PSUTIL_TESTING; extern int PSUTIL_DEBUG; // a signaler for connections without an actual status static const int PSUTIL_CONN_NONE = 128; @@ -100,10 +99,19 @@ PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); // --- Global utils // ==================================================================== -PyObject* psutil_set_testing(PyObject *self, PyObject *args); -void psutil_debug(const char* format, ...); +PyObject* psutil_set_debug(PyObject *self, PyObject *args); int psutil_setup(void); + +// Print a debug message on stderr. +#define psutil_debug(...) do { \ + if (! PSUTIL_DEBUG) \ + break; \ + fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n");} while(0) + + // ==================================================================== // --- BSD // ==================================================================== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 5836cd6b78..bb21361085 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -503,8 +503,8 @@ static PyMethodDef mod_methods[] = { {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS, "A wrapper around sysinfo(), return system memory usage statistics"}, // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"set_debug", psutil_set_debug, METH_VARARGS, + "Enable or disable PSUTIL_DEBUG messages"}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 6f824eb21f..8c5990aee9 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1699,8 +1699,8 @@ static PyMethodDef mod_methods[] = { "Return battery information."}, // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"set_debug", psutil_set_debug, METH_VARARGS, + "Enable or disable PSUTIL_DEBUG messages"}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 342798a882..2e0bd94310 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1679,8 +1679,8 @@ PsutilMethods[] = { "Return CPU statistics"}, // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"set_debug", psutil_set_debug, METH_VARARGS, + "Enable or disable PSUTIL_DEBUG messages"}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 6a128ae2c1..596aa5c045 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1674,8 +1674,8 @@ PsutilMethods[] = { "QueryDosDevice binding"}, // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"set_debug", psutil_set_debug, METH_VARARGS, + "Enable or disable PSUTIL_DEBUG messages"}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 245ff2f3dc..d871abe495 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -952,6 +952,15 @@ def test_fun(self): retries = 10 if CI_TESTING else 5 verbose = True _thisproc = psutil.Process() + _psutil_debug_orig = bool(os.getenv('PSUTIL_DEBUG', 0)) + + @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 diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index d761cd21dc..65f222c440 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -304,10 +304,7 @@ def run_from_name(name): def setup(): - # Note: doc states that altering os.environment may cause memory - # leaks on some platforms. - # Sets PSUTIL_TESTING and PSUTIL_DEBUG in the C module. - psutil._psplatform.cext.set_testing() + psutil._set_debug(True) def main(): diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index e6940a3070..f0bd59eebc 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -456,6 +456,9 @@ def test_boot_time(self): def test_users(self): self.execute(psutil.users) + def test_set_debug(self): + self.execute(lambda: psutil._set_debug(False)) + if WINDOWS: # --- win services diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 002c58de24..fe74e6013c 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1389,7 +1389,6 @@ def test_pid_0(self): 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: From 8fa1746685a86b8ddda454e2e2746d535e2b6d89 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Nov 2021 10:17:23 +0100 Subject: [PATCH 0800/1714] c / win: get rid of psutil_load_globals() and move it into psutil_setup() fun Signed-off-by: Giampaolo Rodola --- psutil/_psutil_common.c | 47 ++++++++++++++++------------------------ psutil/_psutil_common.h | 1 - psutil/_psutil_windows.c | 2 -- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 6ed14658b9..94aa997b34 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -129,11 +129,6 @@ AccessDenied(const char *syscall) { } -// ==================================================================== -// --- Global utils -// ==================================================================== - - // Enable or disable PSUTIL_DEBUG messages. PyObject * psutil_set_debug(PyObject *self, PyObject *args) { @@ -156,17 +151,6 @@ psutil_set_debug(PyObject *self, PyObject *args) { } -/* - * Called on module import on all platforms. - */ -int -psutil_setup(void) { - if (getenv("PSUTIL_DEBUG") != NULL) - PSUTIL_DEBUG = 1; - return 0; -} - - // ============================================================================ // Utility functions (BSD) // ============================================================================ @@ -369,18 +353,6 @@ psutil_set_winver() { } -int -psutil_load_globals() { - if (psutil_loadlibs() != 0) - return 1; - if (psutil_set_winver() != 0) - return 1; - GetSystemInfo(&PSUTIL_SYSTEM_INFO); - InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); - return 0; -} - - /* * Convert the hi and lo parts of a FILETIME structure or a LARGE_INTEGER * to a UNIX time. @@ -416,3 +388,22 @@ psutil_LargeIntegerToUnixTime(LARGE_INTEGER li) { (ULONGLONG)li.LowPart); } #endif // PSUTIL_WINDOWS + + +// Called on module import on all platforms. +int +psutil_setup(void) { + if (getenv("PSUTIL_DEBUG") != NULL) + PSUTIL_DEBUG = 1; + +#ifdef PSUTIL_WINDOWS + if (psutil_loadlibs() != 0) + return 1; + if (psutil_set_winver() != 0) + return 1; + GetSystemInfo(&PSUTIL_SYSTEM_INFO); + InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); +#endif + + return 0; +} diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 6cf19d6575..6aa7da6d49 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -157,7 +157,6 @@ void convert_kvm_err(const char *syscall, char *errbuf); #define AF_INET6 23 #endif - int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 596aa5c045..f9ec237670 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1736,8 +1736,6 @@ void init_psutil_windows(void) if (psutil_setup() != 0) INITERROR; - if (psutil_load_globals() != 0) - INITERROR; if (psutil_set_se_debug() != 0) INITERROR; From 324b297de09feb1d5982db1145db2b6a6a4609b8 Mon Sep 17 00:00:00 2001 From: Olivier Dormond Date: Wed, 10 Nov 2021 23:28:27 +0100 Subject: [PATCH 0801/1714] [macOS / M1] cpu_times(): convert mach tick units to nsecs (fixes #1956) (PR #2011) Signed-off-by: Olivier Dormond --- psutil/_psutil_common.c | 13 +++++++++++++ psutil/_psutil_common.h | 10 ++++++++++ psutil/_psutil_osx.c | 11 +++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 94aa997b34..683db9bd41 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -170,6 +170,15 @@ convert_kvm_err(const char *syscall, char *errbuf) { } #endif +// ==================================================================== +// --- macOS +// ==================================================================== + +#ifdef PSUTIL_OSX +#include + +struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; +#endif // ==================================================================== // --- Windows @@ -405,5 +414,9 @@ psutil_setup(void) { InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); #endif +#ifdef PSUTIL_OSX + mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); +#endif + return 0; } diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 6aa7da6d49..591f5521f0 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -118,6 +118,16 @@ int psutil_setup(void); void convert_kvm_err(const char *syscall, char *errbuf); +// ==================================================================== +// --- macOS +// ==================================================================== + +#ifdef PSUTIL_OSX + #include + + extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; +#endif + // ==================================================================== // --- Windows // ==================================================================== diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 8c5990aee9..87ec495b29 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -279,16 +279,23 @@ static PyObject * psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { pid_t pid; struct proc_taskinfo pti; + uint64_t total_user; + uint64_t total_system; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) <= 0) return NULL; + total_user = pti.pti_total_user * PSUTIL_MACH_TIMEBASE_INFO.numer; + total_user /= PSUTIL_MACH_TIMEBASE_INFO.denom; + total_system = pti.pti_total_system * PSUTIL_MACH_TIMEBASE_INFO.numer; + total_system /= PSUTIL_MACH_TIMEBASE_INFO.denom; + return Py_BuildValue( "(ddKKkkkk)", - (float)pti.pti_total_user / 1000000000.0, // (float) cpu user time - (float)pti.pti_total_system / 1000000000.0, // (float) cpu sys time + (float)total_user / 1000000000.0, // (float) cpu user time + (float)total_system / 1000000000.0, // (float) cpu sys time // Note about memory: determining other mem stats on macOS is a mess: // http://www.opensource.apple.com/source/top/top-67/libtop.c?txt // I just give up. From 2c43bb7b5b4fb45d667d16eb3239fd933b777322 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 10 Nov 2021 23:34:05 +0100 Subject: [PATCH 0802/1714] give credits to @odormond for #1956 and #2011 Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ psutil/_psutil_common.c | 2 -- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index e7d9fa7821..131bbfe9f5 100644 --- a/CREDITS +++ b/CREDITS @@ -774,3 +774,7 @@ I: 1996 N: PetrPospisil W: https://github.com/PetrPospisil I: 1980 + +N: Olivier Dormond +W: https://github.com/odormond +I: 1956 diff --git a/HISTORY.rst b/HISTORY.rst index 5531f9d156..139560f724 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -52,6 +52,8 @@ XXXX-XX-XX by PetrPospisil) - 1991_: process_iter() can raise TypeError if invoked from multiple threads (not thread-safe). +- 1956_: [macOS] Process.cpu_times() reports incorrect timings on M1 machines. + (patch by Olivier Dormond) 5.8.0 ===== diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 683db9bd41..9679da671a 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -413,10 +413,8 @@ psutil_setup(void) { GetSystemInfo(&PSUTIL_SYSTEM_INFO); InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); #endif - #ifdef PSUTIL_OSX mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); #endif - return 0; } From 7fa22e8b8510e2bcf03694204ada1ee7be82e477 Mon Sep 17 00:00:00 2001 From: Shannon Pamperl Date: Sun, 14 Nov 2021 12:03:48 -0600 Subject: [PATCH 0803/1714] appveyor: add support for Python 3.10 (#2012) (#2015) --- appveyor.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 59c47f0ad3..83e570ae12 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -41,6 +41,11 @@ environment: APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 PYTHON_ARCH: "32" + - PYTHON: "C:\\Python310" + PYTHON_VERSION: "3.10.x" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + PYTHON_ARCH: "32" + # 64 bits - PYTHON: "C:\\Python27-x64" @@ -64,6 +69,11 @@ environment: APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 PYTHON_ARCH: "64" + - PYTHON: "C:\\Python310-x64" + PYTHON_VERSION: "3.10.x" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + PYTHON_ARCH: "64" + init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" From 875d2195fc8efa642c7bca714d468551d1805c6c Mon Sep 17 00:00:00 2001 From: Lucas Holt Date: Fri, 19 Nov 2021 16:04:29 -0500 Subject: [PATCH 0804/1714] Handle missing dependencies on MidnightBSD (#2019) --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 16b476f26d..90ae51700b 100755 --- a/setup.py +++ b/setup.py @@ -424,7 +424,10 @@ def main(): print(hilite("XCode (https://developer.apple.com/xcode/) " "is not installed"), color="red", file=sys.stderr) elif FREEBSD: - missdeps("pkg install gcc python%s" % py3) + if which('pkg'): + missdeps("pkg install gcc python%s" % py3) + elif which('mport'): # MidnightBSD + missdeps("mport install gcc python%s" % py3) elif OPENBSD: missdeps("pkg_add -v gcc python%s" % py3) elif NETBSD: From a1ae994cabff37eb86c6ca4564b4f193a73a7b0d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 25 Nov 2021 11:33:35 +0100 Subject: [PATCH 0805/1714] fix #2023 [Linux] cpu_freq() return order is wrong on systems with > 9 CPUs. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 1 + psutil/_pslinux.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 139560f724..4b6ece583f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -54,6 +54,7 @@ XXXX-XX-XX (not thread-safe). - 1956_: [macOS] Process.cpu_times() reports incorrect timings on M1 machines. (patch by Olivier Dormond) +- 2023_: [Linux] cpu_freq() return order is wrong on systems with > 9 CPUs. 5.8.0 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 57925b87c7..1cbbec42d0 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -738,9 +738,10 @@ def cpu_freq(): real-time. """ cpuinfo_freqs = _cpu_get_cpuinfo_freq() - paths = sorted( - glob.glob("/sys/devices/system/cpu/cpufreq/policy[0-9]*") or - glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq")) + 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 = [] pjoin = os.path.join for i, path in enumerate(paths): From eb2f74c153987b4e0d03aa16931d97e8137d9257 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 29 Nov 2021 10:31:58 +0100 Subject: [PATCH 0806/1714] Fix CI tests / wheels / workflow (#2024) Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 66 ++++++++++++++++++-- HISTORY.rst | 2 +- Makefile | 4 +- appveyor.yml | 8 --- psutil/__init__.py | 2 +- psutil/tests/runner.py | 2 + psutil/tests/test_connections.py | 2 - psutil/tests/test_linux.py | 10 +-- psutil/tests/test_misc.py | 6 +- scripts/internal/download_wheels_appveyor.py | 2 +- scripts/internal/print_wheels.py | 2 +- 11 files changed, 75 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0559acd25c..ca026299ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,13 +3,14 @@ # # * Linux # * macOS -# * Windows (commented) +# * Windows (disabled) # * FreeBSD # # To skip certain builds see: # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip # # External GH actions: +# * https://github.com/pypa/cibuildwheel # * https://github.com/actions/checkout # * https://github.com/actions/setup-python # * https://github.com/actions/upload-artifact @@ -19,10 +20,11 @@ on: [push, pull_request] name: build jobs: - linux-macos-win: - name: ${{ matrix.os }} + # Linux + macOS + Python 3 + linux-macos-py3: + name: ${{ matrix.os }}-py3 runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -35,11 +37,12 @@ jobs: PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test - CIBW_SKIP: cp35-* pp* + CIBW_BUILD: 'cp37-* cp38-* cp39-* cp310-*' + CIBW_SKIP: '*-musllinux_*' steps: - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.6.0 + uses: styfle/cancel-workflow-action@0.9.1 with: access_token: ${{ github.token }} @@ -73,6 +76,57 @@ jobs: mv dist/psutil*.tar.gz wheelhouse/ python scripts/internal/print_hashes.py wheelhouse/ + # Linux + macOS + Python 2 + linux-macos-py2: + name: ${{ matrix.os }}-py2 + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + include: + - {name: Linux, python: '3.9', os: ubuntu-latest} + env: + CIBW_ARCHS: 'x86_64 i686' + CIBW_TEST_COMMAND: + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && + PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py + CIBW_TEST_EXTRAS: test + CIBW_BUILD: 'cp27-*' + CIBW_SKIP: '*-musllinux_*' + + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install cibuildwheel + run: pip install cibuildwheel==1.12.0 + + - name: Run tests + run: cibuildwheel . + + - name: Create wheels + uses: actions/upload-artifact@v2 + with: + name: wheels + path: wheelhouse + + - name: Print hashes + if: matrix.os == 'ubuntu-latest' + run: | + make generate-manifest + python setup.py sdist + mv dist/psutil*.tar.gz wheelhouse/ + python scripts/internal/print_hashes.py wheelhouse/ + freebsd: runs-on: macos-latest steps: diff --git a/HISTORY.rst b/HISTORY.rst index 4b6ece583f..37ee57f118 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.8.1 (IN DEVELOPMENT) +5.9.0 (IN DEVELOPMENT) ====================== XXXX-XX-XX diff --git a/Makefile b/Makefile index f255b6511b..01512b7c3e 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_DEBUG=1 -all: test +all: help # =================================================================== # Install @@ -214,9 +214,11 @@ install-git-hooks: ## Install GIT pre-commit hook. download-wheels-github: ## Download latest wheels hosted on github. $(PYTHON) scripts/internal/download_wheels_github.py --tokenfile=~/.github.token + ${MAKE} print-wheels download-wheels-appveyor: ## Download latest wheels hosted on appveyor. $(PYTHON) scripts/internal/download_wheels_appveyor.py + ${MAKE} print-wheels print-wheels: ## Print downloaded wheels $(PYTHON) scripts/internal/print_wheels.py diff --git a/appveyor.yml b/appveyor.yml index 83e570ae12..485182d319 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,10 +24,6 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.x" - PYTHON_ARCH: "32" - - PYTHON: "C:\\Python37" PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "32" @@ -52,10 +48,6 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.x" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "64" diff --git a/psutil/__init__.py b/psutil/__init__.py index 2760c76b75..4632aa85a2 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -210,7 +210,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.8.1" +__version__ = "5.9.0" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 65f222c440..cd7e20b2de 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -298,6 +298,8 @@ def warn(msg): # 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) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 6bbf2194d3..a7a97544b3 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -8,7 +8,6 @@ import os import socket -import sys import textwrap from contextlib import closing from socket import AF_INET @@ -47,7 +46,6 @@ thisproc = psutil.Process() SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) -PYTHON_39 = sys.version_info[:2] == (3, 9) @serialrun diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 38322db637..7451e2bc35 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -149,7 +149,7 @@ 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"}) + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) lines = out.split('\n') for line in lines: if line.startswith('Swap'): @@ -168,7 +168,7 @@ def free_physmem(): # 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"}) + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) lines = out.split('\n') for line in lines: if line.startswith('Mem'): @@ -182,7 +182,7 @@ def free_physmem(): def vmstat(stat): - out = sh("vmstat -s", env={"LANG": "C.UTF-8"}) + out = sh(["vmstat", "-s"], env={"LANG": "C.UTF-8"}) for line in out.split("\n"): line = line.strip() if stat in line: @@ -191,7 +191,7 @@ def vmstat(stat): def get_free_version_info(): - out = sh("free -V").strip() + out = sh(["free", "-V"]).strip() if 'UNKNOWN' in out: raise unittest.SkipTest("can't determine free version") return tuple(map(int, out.split()[-1].split('.'))) @@ -312,7 +312,7 @@ def test_shared(self): 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") + out = sh(["free", "-b"]) lines = out.split('\n') if 'available' not in lines[0]: raise unittest.SkipTest("free does not support 'available' column") diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 70f6a37eb5..323c5406cb 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -17,7 +17,6 @@ import pickle import socket import stat -import sys from psutil import LINUX from psutil import POSIX @@ -50,9 +49,6 @@ import psutil.tests -PYTHON_39 = sys.version_info[:2] == (3, 9) - - # =================================================================== # --- Misc / generic tests. # =================================================================== @@ -414,7 +410,7 @@ def test_debug(self): msg = f.getvalue() assert msg.startswith("psutil-debug"), msg self.assertIn("hello", msg) - self.assertIn(__file__, msg) + self.assertIn(__file__.replace('.pyc', '.py'), msg) # supposed to use repr(exc) with redirect_stderr(StringIO()) as f: diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index bc6c9717e4..5e633d52c0 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -26,7 +26,7 @@ USER = "giampaolo" PROJECT = "psutil" BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.6', '3.7', '3.8', '3.9'] +PY_VERSIONS = ['2.7', '3.7', '3.8', '3.9', '3.10'] TIMEOUT = 30 diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_wheels.py index c2b8d36b97..d13a6aa7ca 100755 --- a/scripts/internal/print_wheels.py +++ b/scripts/internal/print_wheels.py @@ -74,7 +74,7 @@ def main(): tot_files = 0 tot_size = 0 - templ = "%-54s %7s %7s %7s" + templ = "%-100s %7s %7s %7s" for platf, wheels in groups.items(): ppn = "%s (total = %s)" % (platf, len(wheels)) s = templ % (ppn, "size", "arch", "pyver") From b490b5d51af6ed29709c357a00fcdb6bda26df78 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 30 Nov 2021 09:48:40 +0100 Subject: [PATCH 0807/1714] fix missing arg passed to C psutil_debug() Signed-off-by: Giampaolo Rodola --- psutil/arch/windows/cpu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 355de6df72..20c01a0dac 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -215,7 +215,7 @@ psutil_cpu_count_cores(PyObject *self, PyObject *args) { } } else { - psutil_debug("GetLogicalProcessorInformationEx() returned ", + psutil_debug("GetLogicalProcessorInformationEx() returned %u", GetLastError()); goto return_none; } From 39dc44bfa5fbb9500166b3480295379602e5bbc5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 14 Dec 2021 23:54:58 +0100 Subject: [PATCH 0808/1714] Automatically sort imports (isort CLI tool) (#2033) --- .github/workflows/build.yml | 8 +-- .isort.cfg | 7 +++ MANIFEST.in | 1 + Makefile | 24 ++++++--- psutil/__init__.py | 53 ++++++++++---------- psutil/_common.py | 4 +- psutil/_compat.py | 3 +- psutil/_psaix.py | 12 ++--- psutil/_psbsd.py | 12 ++--- psutil/_pslinux.py | 17 ++++--- psutil/_psosx.py | 4 +- psutil/_psposix.py | 5 +- psutil/_pssunos.py | 10 ++-- psutil/_pswindows.py | 11 ++-- psutil/tests/__init__.py | 6 ++- psutil/tests/__main__.py | 2 + psutil/tests/runner.py | 3 ++ psutil/tests/test_aix.py | 2 +- psutil/tests/test_bsd.py | 4 +- psutil/tests/test_connections.py | 6 +-- psutil/tests/test_contracts.py | 16 +++--- psutil/tests/test_linux.py | 22 ++++---- psutil/tests/test_memleaks.py | 7 +-- psutil/tests/test_misc.py | 12 ++--- psutil/tests/test_osx.py | 5 +- psutil/tests/test_posix.py | 7 +-- psutil/tests/test_process.py | 17 +++---- psutil/tests/test_system.py | 12 ++--- psutil/tests/test_testutils.py | 15 +++--- psutil/tests/test_unicode.py | 18 +++---- psutil/tests/test_windows.py | 6 +-- scripts/battery.py | 1 + scripts/cpu_distribution.py | 1 + scripts/disk_usage.py | 3 +- scripts/fans.py | 1 + scripts/ifconfig.py | 1 + scripts/internal/bench_oneshot.py | 6 ++- scripts/internal/bench_oneshot_2.py | 2 +- scripts/internal/check_broken_links.py | 1 + scripts/internal/clinter.py | 1 + scripts/internal/convert_readme.py | 1 + scripts/internal/download_wheels_appveyor.py | 4 +- scripts/internal/download_wheels_github.py | 3 +- scripts/internal/fix_flake8.py | 2 +- scripts/internal/git_pre_commit.py | 17 ++++++- scripts/internal/print_access_denied.py | 6 ++- scripts/internal/print_api_speed.py | 6 ++- scripts/internal/print_downloads.py | 1 + scripts/internal/print_wheels.py | 2 +- scripts/internal/tidelift.py | 3 ++ scripts/internal/winmake.py | 5 +- scripts/iotop.py | 4 +- scripts/killall.py | 1 + scripts/netstat.py | 4 +- scripts/nettop.py | 4 +- scripts/pidof.py | 4 +- scripts/procsmem.py | 1 + scripts/pstree.py | 1 + scripts/sensors.py | 1 + scripts/temperatures.py | 1 + scripts/top.py | 2 + setup.py | 12 +++-- 62 files changed, 264 insertions(+), 169 deletions(-) create mode 100644 .isort.cfg diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca026299ea..5fd6e73532 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,12 +161,14 @@ jobs: - uses: actions/setup-python@v2 - name: 'Run linters' run: | + # py2 curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py python2 get-pip.py python2 -m pip install flake8 - python3 -m pip install flake8 python2 -m flake8 . + # py3 + python3 -m pip install flake8 isort python3 -m flake8 . - echo "flake8 linting OK" + python3 -m isort --settings=.isort.cfg . + # clinter find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python3 scripts/internal/clinter.py - echo "C linting OK" diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000000..58f66946ce --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,7 @@ +# See: https://pycqa.github.io/isort/docs/configuration/options + +[settings] +# one import per line +force_single_line = true +# blank spaces after import section +lines_after_imports = 2 diff --git a/MANIFEST.in b/MANIFEST.in index e9c20d8139..4a10365d61 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include .coveragerc include .flake8 include .gitignore +include .isort.cfg include CONTRIBUTING.md include CREDITS include HISTORY.rst diff --git a/Makefile b/Makefile index 01512b7c3e..0ab1ea06ea 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ DEPS = \ coverage \ flake8 \ flake8-print \ + isort \ pyperf \ pypinfo \ requests \ @@ -187,19 +188,30 @@ test-coverage: ## Run test coverage. # Linters # =================================================================== -lint-py: ## Run Python (flake8) linter. +check-flake8: ## Run flake8 linter. @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 -lint-c: ## Run C linter. +check-imports: ## Run isort linter. + @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg --check-only + +check-c-code: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py -lint: ## Run Python (flake8) and C linters. - ${MAKE} lint-py - ${MAKE} lint-c +lint: ## Run all linters + ${MAKE} check-flake8 + ${MAKE} check-imports + ${MAKE} check-c-code -fix-lint: ## Attempt to automatically fix some Python lint issues. +# =================================================================== +# Fixers +# =================================================================== + +fix-flake8: ## Attempt to automatically fix some Python flake8 issues. @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 --exit-zero | $(PYTHON) scripts/internal/fix_flake8.py +fix-imports: ## Fix imports with isort. + @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg + # =================================================================== # GIT # =================================================================== diff --git a/psutil/__init__.py b/psutil/__init__.py index 4632aa85a2..1a113bc3a8 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -21,6 +21,7 @@ """ from __future__ import division + import collections import contextlib import datetime @@ -31,25 +32,16 @@ import sys import threading import time + + try: import pwd except ImportError: pwd = None from . import _common -from ._common import AccessDenied -from ._common import Error -from ._common import memoize_when_activated -from ._common import NoSuchProcess -from ._common import TimeoutExpired -from ._common import wrap_numbers as _wrap_numbers -from ._common import ZombieProcess -from ._compat import long -from ._compat import PermissionError -from ._compat import ProcessLookupError -from ._compat import SubprocessTimeoutExpired as _SubprocessTimeoutExpired -from ._compat import PY3 as _PY3 - +from ._common import AIX +from ._common import BSD from ._common import CONN_CLOSE from ._common import CONN_CLOSE_WAIT from ._common import CONN_CLOSING @@ -62,9 +54,16 @@ from ._common import CONN_SYN_RECV from ._common import CONN_SYN_SENT from ._common import CONN_TIME_WAIT +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 @@ -79,18 +78,21 @@ 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 FREEBSD # NOQA -from ._common import LINUX -from ._common import MACOS -from ._common import NETBSD # NOQA -from ._common import OPENBSD # NOQA -from ._common import OSX # deprecated alias -from ._common import POSIX # NOQA 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 + if LINUX: # This is public API and it will be retrieved from _pslinux.py @@ -98,7 +100,6 @@ 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 @@ -113,10 +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_VERYLOW # NOQA + from ._pswindows import IOPRIO_HIGH # NOQA from ._pswindows import IOPRIO_LOW # NOQA from ._pswindows import IOPRIO_NORMAL # NOQA - from ._pswindows import IOPRIO_HIGH # NOQA + from ._pswindows import IOPRIO_VERYLOW # NOQA elif MACOS: from . import _psosx as _psplatform diff --git a/psutil/_common.py b/psutil/_common.py index 5e6dbb6d6b..16d3b3b56e 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -7,7 +7,8 @@ # Note: this module is imported by setup.py so it should not import # psutil or third-party modules. -from __future__ import division, print_function +from __future__ import division +from __future__ import print_function import collections import contextlib @@ -24,6 +25,7 @@ from socket import SOCK_DGRAM from socket import SOCK_STREAM + try: from socket import AF_INET6 except ImportError: diff --git a/psutil/_compat.py b/psutil/_compat.py index e5275f5fc6..251e595f7c 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -15,6 +15,7 @@ import sys import types + __all__ = [ # constants "PY3", @@ -413,8 +414,8 @@ def _access_check(fn, mode): def get_terminal_size(fallback=(80, 24)): try: import fcntl - import termios import struct + import termios except ImportError: return fallback else: diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 3e3a3d145a..9cc7d56e3e 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -18,20 +18,20 @@ from . import _psposix from . import _psutil_aix as cext from . import _psutil_posix as cext_posix -from ._common import AccessDenied -from ._common import conn_to_ntuple -from ._common import get_procfs_path -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 usage_percent 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 ._compat import FileNotFoundError from ._compat import PermissionError from ._compat import ProcessLookupError -from ._compat import PY3 __extra__all__ = ["PROCFS_PATH"] diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index bdcfc1e6e6..528850657f 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -9,24 +9,24 @@ import functools import os import xml.etree.ElementTree as ET -from collections import namedtuple from collections import defaultdict +from collections import namedtuple from . import _common from . import _psposix from . import _psutil_bsd as cext from . import _psutil_posix as cext_posix +from ._common import FREEBSD +from ._common import NETBSD +from ._common import OPENBSD 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 FREEBSD from ._common import memoize from ._common import memoize_when_activated -from ._common import NETBSD -from ._common import NoSuchProcess -from ._common import OPENBSD from ._common import usage_percent -from ._common import ZombieProcess from ._compat import FileNotFoundError from ._compat import PermissionError from ._compat import ProcessLookupError diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1cbbec42d0..5a35691207 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -25,30 +25,31 @@ from . import _psposix from . import _psutil_linux as cext from . import _psutil_posix as cext_posix +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 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 NIC_DUPLEX_FULL -from ._common import NIC_DUPLEX_HALF -from ._common import NIC_DUPLEX_UNKNOWN -from ._common import NoSuchProcess 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 ._common import ZombieProcess -from ._compat import b -from ._compat import basestring +from ._compat import PY3 from ._compat import FileNotFoundError from ._compat import PermissionError from ._compat import ProcessLookupError -from ._compat import PY3 +from ._compat import b +from ._compat import basestring + if sys.version_info >= (3, 4): import enum diff --git a/psutil/_psosx.py b/psutil/_psosx.py index eda70d213d..ac8ecc5308 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -14,14 +14,14 @@ from . import _psutil_osx as cext from . import _psutil_posix as cext_posix 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 NoSuchProcess from ._common import parse_environ_block from ._common import usage_percent -from ._common import ZombieProcess from ._compat import PermissionError from ._compat import ProcessLookupError diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 706dab9ae8..8e6629d726 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -10,18 +10,19 @@ import sys import time +from ._common import TimeoutExpired from ._common import memoize from ._common import sdiskusage -from ._common import TimeoutExpired 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 PY3 from ._compat import unicode + if sys.version_info >= (3, 4): import enum else: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 355a762399..69b579c5ff 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -17,22 +17,22 @@ from . import _psposix from . import _psutil_posix as cext_posix from . import _psutil_sunos as cext -from ._common import AccessDenied 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 NoSuchProcess from ._common import sockfam_to_enum from ._common import socktype_to_enum from ._common import usage_percent -from ._common import ZombieProcess -from ._compat import b +from ._compat import PY3 from ._compat import FileNotFoundError from ._compat import PermissionError from ._compat import ProcessLookupError -from ._compat import PY3 +from ._compat import b __extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 348b72e132..9966b1b4e2 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -14,22 +14,22 @@ from collections import namedtuple from . import _common +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 ENCODING -from ._common import ENCODING_ERRS from ._common import isfile_strict from ._common import memoize from ._common import memoize_when_activated -from ._common import NoSuchProcess from ._common import parse_environ_block -from ._common import TimeoutExpired 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 ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS @@ -39,6 +39,7 @@ from ._psutil_windows import NORMAL_PRIORITY_CLASS from ._psutil_windows import REALTIME_PRIORITY_CLASS + try: from . import _psutil_windows as cext except ImportError as err: diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index d871abe495..21bb3e611d 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -9,6 +9,7 @@ """ from __future__ import print_function + import atexit import contextlib import ctypes @@ -46,15 +47,16 @@ from psutil._common import bytes2human 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 PY3 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 + if PY3: import unittest else: @@ -1733,8 +1735,8 @@ def copyload_shared_lib(suffix=""): in memory via ctypes. Return the new absolutized, normcased path. """ - from ctypes import wintypes 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 diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index d5cd02eb1c..e67735275c 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -10,4 +10,6 @@ """ from .runner import main + + main() diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index cd7e20b2de..2e6f83e264 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -21,6 +21,7 @@ """ from __future__ import print_function + import atexit import optparse import os @@ -28,6 +29,8 @@ import textwrap import time import unittest + + try: import ctypes except ImportError: diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index a32c3f6a17..6a5debfa2d 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -10,11 +10,11 @@ import re +import psutil from psutil import AIX from psutil.tests import PsutilTestCase from psutil.tests import sh from psutil.tests import unittest -import psutil @unittest.skipIf(not AIX, "AIX only") diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index b0bff87f65..54b488bc75 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -20,12 +20,12 @@ from psutil import FREEBSD from psutil import NETBSD from psutil import OPENBSD -from psutil.tests import spawn_testproc 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 TOLERANCE_SYS_MEM +from psutil.tests import spawn_testproc from psutil.tests import terminate from psutil.tests import unittest from psutil.tests import which diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index a7a97544b3..5381608a9b 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -27,17 +27,17 @@ 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 HAS_CONNECTIONS_UNIX -from psutil.tests import PsutilTestCase 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 SKIP_SYSCONS from psutil.tests import tcp_socketpair from psutil.tests import unittest from psutil.tests import unix_socketpair diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index b03477d9ab..7401cc15ec 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -18,6 +18,7 @@ import time import traceback +import psutil from psutil import AIX from psutil import BSD from psutil import FREEBSD @@ -33,25 +34,24 @@ from psutil._compat import long from psutil._compat import range from psutil.tests import APPVEYOR -from psutil.tests import check_connection_ntuple from psutil.tests import CI_TESTING -from psutil.tests import create_sockets -from psutil.tests import enum 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 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 enum from psutil.tests import is_namedtuple from psutil.tests import kernel_version from psutil.tests import process_namespace -from psutil.tests import PsutilTestCase -from psutil.tests import PYPY from psutil.tests import serialrun -from psutil.tests import SKIP_SYSCONS from psutil.tests import unittest -from psutil.tests import VALID_PROC_STATUSES -import psutil # =================================================================== diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 7451e2bc35..20e28d2991 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -7,6 +7,7 @@ """Linux specific tests.""" from __future__ import division + import collections import contextlib import errno @@ -23,36 +24,37 @@ import psutil from psutil import LINUX -from psutil._compat import basestring -from psutil._compat import FileNotFoundError from psutil._compat import PY3 +from psutil._compat import FileNotFoundError +from psutil._compat import basestring from psutil._compat import u -from psutil.tests import call_until 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 mock -from psutil.tests import PsutilTestCase 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 ThreadTask -from psutil.tests import TOLERANCE_DISK_USAGE -from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import unittest from psutil.tests import which + if LINUX: - from psutil._pslinux import calculate_avail_vmem from psutil._pslinux import CLOCK_TICKS - from psutil._pslinux import open_binary 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__)) diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index f0bd59eebc..d5baffa55f 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -16,6 +16,7 @@ """ from __future__ import print_function + import functools import os @@ -29,8 +30,6 @@ from psutil import WINDOWS from psutil._compat import ProcessLookupError from psutil._compat import super -from psutil.tests import create_sockets -from psutil.tests import get_testfn from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_ENVIRON @@ -43,12 +42,14 @@ 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 -from psutil.tests import TestMemoryLeak from psutil.tests import unittest diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 323c5406cb..fb4d0c0228 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -18,6 +18,8 @@ import socket import stat +import psutil +import psutil.tests from psutil import LINUX from psutil import POSIX from psutil import WINDOWS @@ -36,17 +38,15 @@ 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 mock -from psutil.tests import PsutilTestCase from psutil.tests import PYTHON_EXE -from psutil.tests import reload_module from psutil.tests import ROOT_DIR from psutil.tests import SCRIPTS_DIR +from psutil.tests import PsutilTestCase +from psutil.tests import import_module_by_path +from psutil.tests import mock +from psutil.tests import reload_module from psutil.tests import sh from psutil.tests import unittest -import psutil -import psutil.tests # =================================================================== diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index f797de7146..4f4b1c2906 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -13,15 +13,16 @@ 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 -from psutil.tests import TOLERANCE_DISK_USAGE -from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import unittest + if POSIX: from psutil._psutil_posix import getpagesize diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index acb6aa200a..31b819267b 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -23,18 +23,19 @@ from psutil import POSIX from psutil import SUNOS from psutil.tests import CI_TESTING -from psutil.tests import spawn_testproc from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import mock -from psutil.tests import PsutilTestCase 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 unittest from psutil.tests import which + if POSIX: import mmap import resource diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index fe74e6013c..08f1664703 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -21,7 +21,6 @@ import types import psutil - from psutil import AIX from psutil import BSD from psutil import LINUX @@ -33,15 +32,12 @@ 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 PY3 from psutil._compat import super from psutil.tests import APPVEYOR -from psutil.tests import call_until from psutil.tests import CI_TESTING -from psutil.tests import copyload_shared_lib -from psutil.tests import create_exe from psutil.tests import GITHUB_ACTIONS from psutil.tests import GLOBAL_TIMEOUT from psutil.tests import HAS_CPU_AFFINITY @@ -52,17 +48,20 @@ 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 process_namespace -from psutil.tests import PsutilTestCase from psutil.tests import PYPY from psutil.tests import PYTHON_EXE +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_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 ThreadTask from psutil.tests import unittest from psutil.tests import wait_for_pid diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 4e3ac3e4ae..db2cb3488f 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -31,10 +31,9 @@ from psutil._compat import FileNotFoundError from psutil._compat import long from psutil.tests import ASCII_FS -from psutil.tests import check_net_address from psutil.tests import CI_TESTING from psutil.tests import DEVNULL -from psutil.tests import enum +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 @@ -44,12 +43,13 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import IS_64BIT -from psutil.tests import mock -from psutil.tests import PsutilTestCase from psutil.tests import PYPY -from psutil.tests import retry_on_failure -from psutil.tests import GITHUB_ACTIONS 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 from psutil.tests import unittest diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 09adbdb180..89888dfe05 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -17,25 +17,28 @@ import stat import subprocess +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 HAS_CONNECTIONS_UNIX +from psutil.tests import PYTHON_EXE +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 CI_TESTING from psutil.tests import create_sockets from psutil.tests import get_free_port -from psutil.tests import HAS_CONNECTIONS_UNIX from psutil.tests import is_namedtuple from psutil.tests import mock from psutil.tests import process_namespace -from psutil.tests import PsutilTestCase -from psutil.tests import PYTHON_EXE from psutil.tests import reap_children from psutil.tests import retry from psutil.tests import retry_on_failure @@ -45,13 +48,11 @@ from psutil.tests import system_namespace from psutil.tests import tcp_socketpair from psutil.tests import terminate -from psutil.tests import TestMemoryLeak from psutil.tests import unittest 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 + # =================================================================== # --- Unit tests for test utilities. diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 9edb8c89c0..e635726943 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -79,6 +79,7 @@ import warnings from contextlib import closing +import psutil from psutil import BSD from psutil import OPENBSD from psutil import POSIX @@ -87,28 +88,27 @@ 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 CI_TESTING -from psutil.tests import copyload_shared_lib -from psutil.tests import create_exe -from psutil.tests import get_testfn 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 PsutilTestCase 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_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 -from psutil.tests import TESTFN_PREFIX -from psutil.tests import UNICODE_SUFFIX from psutil.tests import unittest -import psutil if APPVEYOR: diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index d9b65adbeb..ea694be4a4 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -27,15 +27,15 @@ from psutil.tests import GITHUB_ACTIONS from psutil.tests import HAS_BATTERY from psutil.tests import IS_64BIT -from psutil.tests import mock -from psutil.tests import PsutilTestCase from psutil.tests import PY3 from psutil.tests import PYPY +from psutil.tests import TOLERANCE_DISK_USAGE +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 -from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import unittest diff --git a/scripts/battery.py b/scripts/battery.py index edf4ce8c97..bf0503e0e8 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -15,6 +15,7 @@ """ from __future__ import print_function + import sys import psutil diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index fb39d888ad..ba71ca9cc8 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -40,6 +40,7 @@ """ from __future__ import print_function + import collections import os import sys diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 851ae9b166..65ae313821 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -15,8 +15,9 @@ /dev/sda2 600.0M 312.4M 287.6M 52% fuseblk /media/Recovery """ -import sys import os +import sys + import psutil from psutil._common import bytes2human diff --git a/scripts/fans.py b/scripts/fans.py index 179af6312c..304277157c 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -13,6 +13,7 @@ """ from __future__ import print_function + import sys import psutil diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index ae137fb482..23fd26b477 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -44,6 +44,7 @@ """ from __future__ import print_function + import socket import psutil diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 436bdd6b01..6059587605 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -10,10 +10,12 @@ See: https://github.com/giampaolo/psutil/issues/799 """ -from __future__ import print_function, division +from __future__ import division +from __future__ import print_function + import sys -import timeit import textwrap +import timeit import psutil diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index 3867391b40..051d00360b 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -12,9 +12,9 @@ import sys import pyperf # requires "pip install pyperf" +from bench_oneshot import names import psutil -from bench_oneshot import names p = psutil.Process() diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index e66448fd1a..1a07611613 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -40,6 +40,7 @@ """ from __future__ import print_function + import concurrent.futures import functools import os diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py index fde1a3f2f5..384951da89 100755 --- a/scripts/internal/clinter.py +++ b/scripts/internal/clinter.py @@ -7,6 +7,7 @@ """A super simple linter to check C syntax.""" from __future__ import print_function + import argparse import sys diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index d6cae9181a..cca7dcb001 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -11,6 +11,7 @@ import re import sys + summary = """\ Quick links =========== diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index 5e633d52c0..e4d6ffc0f7 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -13,11 +13,13 @@ """ from __future__ import print_function + import concurrent.futures import os -import requests import sys +import requests + from psutil import __version__ as PSUTIL_VERSION from psutil._common import bytes2human from psutil._common import print_color diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py index a344ec494e..00f57116f6 100755 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels_github.py @@ -18,10 +18,11 @@ import argparse import json import os -import requests import sys import zipfile +import requests + from psutil import __version__ as PSUTIL_VERSION from psutil._common import bytes2human from psutil.tests import safe_rmpath diff --git a/scripts/internal/fix_flake8.py b/scripts/internal/fix_flake8.py index 7cde608bba..14fbb4d22d 100755 --- a/scripts/internal/fix_flake8.py +++ b/scripts/internal/fix_flake8.py @@ -11,9 +11,9 @@ $ python3 -m flake8 --exit-zero | python3 scripts/fix_flake8.py """ +import shutil import sys import tempfile -import shutil from collections import defaultdict from collections import namedtuple from pprint import pprint as pp # NOQA diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 2ec4303d63..92bc0f0a46 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -20,6 +20,7 @@ """ from __future__ import print_function + import os import subprocess import sys @@ -115,14 +116,26 @@ def main(): print("%s:%s %s" % (path, lineno, line)) return exit("bare except clause") - # Python linter + # Python linters if py_files: + # Flake8 assert os.path.exists('.flake8') # XXX: we should escape spaces and possibly other amenities here cmd = "%s -m flake8 --config=.flake8 %s" % (PYTHON, " ".join(py_files)) ret = subprocess.call(cmd, shell=True) if ret != 0: - return exit("python code is not flake8 compliant") + return exit("python code is not flake8 compliant; " + "try running 'make fix-flake8'") + + # isort + assert os.path.exists('.isort.cfg') + cmd = "%s -m isort --settings=.isort.cfg --check-only %s" % ( + PYTHON, " ".join(py_files)) + ret = subprocess.call(cmd, shell=True) + if ret != 0: + return exit("python code is not flake8 compliant; " + "try running 'make fix-imports'") + # C linter if c_files: # XXX: we should escape spaces and possibly other amenities here diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 81d192f0c7..f3d0166e50 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -45,9 +45,11 @@ Totals: access-denied=1744, calls=10020, processes=334 """ -from __future__ import print_function, division -from collections import defaultdict +from __future__ import division +from __future__ import print_function + import time +from collections import defaultdict import psutil from psutil._common import print_color diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index e39a1baa7b..ee2e3254ee 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -25,10 +25,12 @@ ... """ -from __future__ import print_function, division -from timeit import default_timer as timer +from __future__ import division +from __future__ import print_function + import inspect import os +from timeit import default_timer as timer import psutil from psutil._common import print_color diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 7e5c46315c..b6df3b38c6 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -13,6 +13,7 @@ """ from __future__ import print_function + import json import os import subprocess diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_wheels.py index d13a6aa7ca..5e5faccdd8 100755 --- a/scripts/internal/print_wheels.py +++ b/scripts/internal/print_wheels.py @@ -10,8 +10,8 @@ import glob import os -from psutil._common import print_color from psutil._common import bytes2human +from psutil._common import print_color class Wheel: diff --git a/scripts/internal/tidelift.py b/scripts/internal/tidelift.py index fcba3e610f..9470fc8521 100755 --- a/scripts/internal/tidelift.py +++ b/scripts/internal/tidelift.py @@ -10,8 +10,11 @@ """ from __future__ import print_function + import os + import requests + import psutil from psutil.tests import import_module_by_path diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 933951a233..4452ef0993 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -12,6 +12,7 @@ """ from __future__ import print_function + import argparse import atexit import ctypes @@ -378,7 +379,7 @@ def setup_dev_env(): sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEPS))) -def lint(): +def check_flake8(): """Run flake8 against all py files""" py_files = subprocess.check_output("git ls-files") if PY3: @@ -560,7 +561,7 @@ def main(): sp.add_parser('install', help="build + install in develop/edit mode") sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") sp.add_parser('install-pip', help="install pip") - sp.add_parser('lint', help="run flake8 against all py files") + sp.add_parser('check_flake8', help="run flake8 against all py files") sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") sp.add_parser('setup-dev-env', help="install deps") diff --git a/scripts/iotop.py b/scripts/iotop.py index 0468367347..91bc3b18fb 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -30,8 +30,10 @@ Author: Giampaolo Rodola' """ -import time import sys +import time + + try: import curses except ImportError: diff --git a/scripts/killall.py b/scripts/killall.py index 7bbcd75a88..d985185f85 100755 --- a/scripts/killall.py +++ b/scripts/killall.py @@ -10,6 +10,7 @@ import os import sys + import psutil diff --git a/scripts/netstat.py b/scripts/netstat.py index 1832a0966f..476b082e5e 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -20,7 +20,9 @@ """ import socket -from socket import AF_INET, SOCK_STREAM, SOCK_DGRAM +from socket import AF_INET +from socket import SOCK_DGRAM +from socket import SOCK_STREAM import psutil diff --git a/scripts/nettop.py b/scripts/nettop.py index 8cc19fda4c..9e1abe7641 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -31,8 +31,10 @@ pkts-recv 1214470 0 """ -import time import sys +import time + + try: import curses except ImportError: diff --git a/scripts/pidof.py b/scripts/pidof.py index ee18aae4c5..da93710723 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -12,9 +12,11 @@ """ from __future__ import print_function -import psutil + import sys +import psutil + def pidof(pgname): pids = [] diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 1074c4c20f..ca03729ecf 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -36,6 +36,7 @@ """ from __future__ import print_function + import sys import psutil diff --git a/scripts/pstree.py b/scripts/pstree.py index dba9f1bdd4..18732b8cbf 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -29,6 +29,7 @@ """ from __future__ import print_function + import collections import sys diff --git a/scripts/sensors.py b/scripts/sensors.py index 911d7c9b43..3dc823803d 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -30,6 +30,7 @@ """ from __future__ import print_function + import psutil diff --git a/scripts/temperatures.py b/scripts/temperatures.py index f2dd51a73b..90097e5148 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -24,6 +24,7 @@ """ from __future__ import print_function + import sys import psutil diff --git a/scripts/top.py b/scripts/top.py index 989f830682..e07a58f1af 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -36,6 +36,8 @@ import datetime import sys import time + + try: import curses except ImportError: diff --git a/setup.py b/setup.py index 90ae51700b..7521d08e1d 100755 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ """Cross-platform lib for process and system monitoring in Python.""" from __future__ import print_function + import contextlib import io import os @@ -19,14 +20,17 @@ import tempfile import warnings + with warnings.catch_warnings(): warnings.simplefilter("ignore") try: import setuptools - from setuptools import setup, Extension + from setuptools import Extension + from setuptools import setup except ImportError: setuptools = None - from distutils.core import setup, Extension + from distutils.core import Extension + from distutils.core import setup HERE = os.path.abspath(os.path.dirname(__file__)) @@ -36,7 +40,6 @@ from _common import AIX # NOQA from _common import BSD # NOQA from _common import FREEBSD # NOQA -from _common import hilite # NOQA from _common import LINUX # NOQA from _common import MACOS # NOQA from _common import NETBSD # NOQA @@ -44,6 +47,7 @@ from _common import POSIX # NOQA from _common import SUNOS # NOQA from _common import WINDOWS # NOQA +from _common import hilite # NOQA from _compat import PY3 # NOQA from _compat import which # NOQA @@ -236,8 +240,8 @@ def get_winver(): elif LINUX: def get_ethtool_macro(): # see: https://github.com/giampaolo/psutil/issues/659 - from distutils.unixccompiler import UnixCCompiler from distutils.errors import CompileError + from distutils.unixccompiler import UnixCCompiler with tempfile.NamedTemporaryFile( suffix='.c', delete=False, mode="wt") as f: From ebbaae8d1f42f051282af79d60f19cb1161088a5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Dec 2021 11:07:37 +0100 Subject: [PATCH 0809/1714] git pre commit hook: use shlex.split() Signed-off-by: Giampaolo Rodola --- scripts/internal/git_pre_commit.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 92bc0f0a46..dca17ffb0e 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -22,6 +22,7 @@ from __future__ import print_function import os +import shlex import subprocess import sys @@ -118,24 +119,21 @@ def main(): # Python linters if py_files: - # Flake8 + # flake8 assert os.path.exists('.flake8') - # XXX: we should escape spaces and possibly other amenities here cmd = "%s -m flake8 --config=.flake8 %s" % (PYTHON, " ".join(py_files)) - ret = subprocess.call(cmd, shell=True) + ret = subprocess.call(shlex.split(cmd)) if ret != 0: return exit("python code is not flake8 compliant; " "try running 'make fix-flake8'") - # isort assert os.path.exists('.isort.cfg') cmd = "%s -m isort --settings=.isort.cfg --check-only %s" % ( PYTHON, " ".join(py_files)) - ret = subprocess.call(cmd, shell=True) + ret = subprocess.call(shlex.split(cmd)) if ret != 0: return exit("python code is not flake8 compliant; " "try running 'make fix-imports'") - # C linter if c_files: # XXX: we should escape spaces and possibly other amenities here From ff2f4d4c4986bffbc5348a197396e11bef057346 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Dec 2021 11:46:07 +0100 Subject: [PATCH 0810/1714] assert scripts have +x bit Signed-off-by: Giampaolo Rodola --- psutil/tests/test_misc.py | 11 ++++++----- scripts/internal/git_pre_commit.py | 5 +++-- scripts/internal/winmake.py | 10 +++++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index fb4d0c0228..0362432c55 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -720,11 +720,12 @@ def test_coverage(self): @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) + 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') diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index dca17ffb0e..c6f223bba9 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -13,6 +13,7 @@ - assert not pdb.set_trace in code - assert no bare except clause ("except:") in code - assert "flake8" checks pass +- assert "isort" checks pass - assert C linter checks pass - abort if files were added/renamed/removed and MANIFEST.in was not updated @@ -124,7 +125,7 @@ def main(): cmd = "%s -m flake8 --config=.flake8 %s" % (PYTHON, " ".join(py_files)) ret = subprocess.call(shlex.split(cmd)) if ret != 0: - return exit("python code is not flake8 compliant; " + return exit("python code didn't pass 'flake8' style check; " "try running 'make fix-flake8'") # isort assert os.path.exists('.isort.cfg') @@ -132,7 +133,7 @@ def main(): PYTHON, " ".join(py_files)) ret = subprocess.call(shlex.split(cmd)) if ret != 0: - return exit("python code is not flake8 compliant; " + return exit("python code didn't pass 'isort' style check; " "try running 'make fix-imports'") # C linter if c_files: diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 4452ef0993..6c48297517 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -156,11 +156,11 @@ def onerror(fun, path, excinfo): safe_remove(pattern) return - for root, subdirs, subfiles in os.walk('.'): + for root, dirs, files in os.walk('.'): root = os.path.normpath(root) if root.startswith('.git/'): continue - found = fnmatch.filter(subdirs if directory else subfiles, pattern) + found = fnmatch.filter(dirs if directory else files, pattern) for name in found: path = os.path.join(root, name) if directory: @@ -195,15 +195,15 @@ def onerror(fun, path, excinfo): def recursive_rm(*patterns): """Recursively remove a file or matching a list of patterns.""" - for root, subdirs, subfiles in os.walk(u'.'): + for root, dirs, files in os.walk(u'.'): root = os.path.normpath(root) if root.startswith('.git/'): continue - for file in subfiles: + for file in files: for pattern in patterns: if fnmatch.fnmatch(file, pattern): safe_remove(os.path.join(root, file)) - for dir in subdirs: + for dir in dirs: for pattern in patterns: if fnmatch.fnmatch(dir, pattern): safe_rmtree(os.path.join(root, dir)) From a7165b5c02670cb7c4886425dd3911dd1b1116a6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Dec 2021 12:26:48 +0100 Subject: [PATCH 0811/1714] add sponsorhips / supporters @indeedeng and @PySimpleGUI; thanks a lot! ;) Signed-off-by: Giampaolo Rodola --- README.rst | 3 +++ docs/index.rst | 2 ++ 2 files changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 1f1f42c808..6ad07db238 100644 --- a/README.rst +++ b/README.rst @@ -142,6 +142,9 @@ Supporters + + + add your avatar diff --git a/docs/index.rst b/docs/index.rst index 6738f5a28d..47b53b5146 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -86,6 +86,8 @@ Supporters + +
add your avatar From d81e75e94a1dd2b8d64caa0e72c771a7196a5d15 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Dec 2021 21:05:44 +0100 Subject: [PATCH 0812/1714] HISTORY.rst add hyperlinks pointing to psutil API doc (#2042) --- .github/workflows/build.yml | 6 +- HISTORY.rst | 3417 +++++++++++++++++++++----------- psutil/arch/osx/process_info.c | 5 +- psutil/tests/test_process.py | 6 +- 4 files changed, 2278 insertions(+), 1156 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fd6e73532..d8940cd059 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: fail-fast: false matrix: # os: [ubuntu-latest, macos-latest, windows-latest] - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-10.15] include: - {name: Linux, python: '3.9', os: ubuntu-latest} env: @@ -128,7 +128,7 @@ jobs: python scripts/internal/print_hashes.py wheelhouse/ freebsd: - runs-on: macos-latest + runs-on: macos-10.15 steps: - name: Cancel previous runs uses: styfle/cancel-workflow-action@0.6.0 @@ -139,7 +139,7 @@ jobs: - name: Run tests id: test - uses: vmactions/freebsd-vm@v0.1.4 + uses: vmactions/freebsd-vm@v0.1.5 with: usesh: true prepare: pkg install -y gcc python3 diff --git a/HISTORY.rst b/HISTORY.rst index 37ee57f118..ae2a584ee6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,54 +7,57 @@ XXXX-XX-XX **Enhancements** -- 1851_: [Linux] cpu_freq() is slow on systems with many CPUs. Read current - frequency values for all CPUs from /proc/cpuinfo instead of opening many - files in /sys fs. (patch by marxin) -- 1992_: NoSuchProcess message now specifies if the PID has been reused. -- 1992_: error classes (NoSuchProcess, AccessDenied, etc.) now have a better - formatted and separated `__repr__` and `__str__` implementations. -- 1996_: add support for MidnightBSD. (patch by Saeed Rasooli) -- 1999_: [Linux] disk_partitions(): convert "/dev/root" device (an alias used - on some Linux distros) to real root device path. -- 2005_: PSUTIL_DEBUG mode now prints file name and line number of the debug +- 1851_, [Linux]: `cpu_freq()`_ is slow on systems with many CPUs. Read current + frequency values for all CPUs from ``/proc/cpuinfo`` instead of opening many + files in ``/sys`` fs. (patch by marxin) +- 1992_: `NoSuchProcess`_ message now specifies if the PID has been reused. +- 1992_: error classes (`NoSuchProcess`_, `AccessDenied`_, etc.) now have a better + formatted and separated ``__repr__`` and ``__str__`` implementations. +- 1996_, [BSD]: add support for MidnightBSD. (patch by Saeed Rasooli) +- 1999_, [Linux]: `disk_partitions()`_: convert ``/dev/root`` device (an alias + used on some Linux distros) to real root device path. +- 2005_: ``PSUTIL_DEBUG`` mode now prints file name and line number of the debug messages coming from C extension modules. +- 2042_: rewrite HISTORY.rst to use hyperlinks pointing to psutil API doc. **Bug fixes** -- 1456_: [macOS] psutil.cpu_freq()'s min and max are set to 0 if can't be - determined (instead of crashing). -- 1512_: [macOS] sometimes Process.connections() will crash with EOPNOTSUPP - for one connection; this is now ignored. -- 1598_: [Windows] psutil.disk_partitions() only returns mountpoints on drives - where it first finds one -- 1874_: [Solaris] swap output error due to incorrect range. -- 1892_: [macOS] psutil.cpu_freq() broken on Apple M1. -- 1901_: [macOS] different functions, especially process' open_files() and - connections() methods, could randomly raise AccessDenied because the internal - buffer of `proc_pidinfo(PROC_PIDLISTFDS)` syscall was not big enough. We now - dynamically increase the buffer size until it's big enough instead of giving - up and raising AccessDenied, which was a fallback to avoid crashing. -- 1904_: [Windows] OpenProcess fails with ERROR_SUCCESS due to GetLastError() - called after sprintf(). (patch by alxchk) -- 1913_: [Linux] wait_procs seemingly ignoring timeout, TimeoutExpired thrown -- 1919_: [Linux] sensors_battery() can raise TypeError on PureOS. -- 1921_: [Windows] psutil.swap_memory() shows committed memory instead of swap -- 1940_: [Linux] psutil does not handle ENAMETOOLONG when accessing process +- 1456_, [macOS], **[critical]**: `cpu_freq()`_ ``min`` and ``max`` are set to + 0 if can't be determined (instead of crashing). +- 1512_, [macOS]: sometimes `Process.connections()`_ will crash with + ``EOPNOTSUPP`` for one connection; this is now ignored. +- 1598_, [Windows]: `disk_partitions()`_ only returns mountpoints on drives + where it first finds one. +- 1874_, [SunOS]: swap output error due to incorrect range. +- 1892_, [macOS]: `cpu_freq()`_ broken on Apple M1. +- 1901_, [macOS]: different functions, especially `Process.open_files()`_ and + `Process.connections()`_, could randomly raise `AccessDenied`_ because the + internal buffer of ``proc_pidinfo(PROC_PIDLISTFDS)`` syscall was not big enough. + We now dynamically increase the buffer size until it's big enough instead of + giving up and raising `AccessDenied`_, which was a fallback to avoid crashing. +- 1904_, [Windows]: ``OpenProcess`` fails with ``ERROR_SUCCESS`` due to + ``GetLastError()`` called after ``sprintf()``. (patch by alxchk) +- 1913_, [Linux]: `wait_procs()`_ should catch ``subprocess.TimeoutExpired`` + exception. +- 1919_, [Linux]: `sensors_battery()`_ can raise ``TypeError`` on PureOS. +- 1921_, [Windows]: `swap_memory()`_ shows committed memory instead of swap. +- 1940_, [Linux]: psutil does not handle ``ENAMETOOLONG`` when accessing process file descriptors in procfs. (patch by Nikita Radchenko) -- 1948_: Process' memoize_when_activated decorator was not thread-safe. (patch - by Xuehai Pan) -- 1953_: [Windows] disk_partitions() crashes due to insufficient buffer len. - (patch by MaWe2019) -- 1965_: [Windows] fix "Fatal Python error: deallocating None" when calling - psutil.users() multiple times. -- 1980_: [Windows] 32bit / WOW64 processes fails to read process name longer - than 128 characters resulting in AccessDenied. This is now fixed. (patch +- 1948_, **[critical]**: ``memoize_when_activated`` decorator is not thread-safe. + (patch by Xuehai Pan) +- 1953_, [Windows], **[critical]**: `disk_partitions()`_ crashes due to + insufficient buffer len. (patch by MaWe2019) +- 1965_, [Windows], **[critical]**: fix "Fatal Python error: deallocating None" + when calling `users()`_ multiple times. +- 1980_, [Windows]: 32bit / WoW64 processes fails to read `Process.name()`_ longer + than 128 characters resulting in `AccessDenied`_. This is now fixed. (patch by PetrPospisil) -- 1991_: process_iter() can raise TypeError if invoked from multiple threads - (not thread-safe). -- 1956_: [macOS] Process.cpu_times() reports incorrect timings on M1 machines. +- 1991_, **[critical]**: `process_iter()`_ is not thread safe and can raise + ``TypeError`` if invoked from multiple threads. +- 1956_, [macOS]: `Process.cpu_times()`_ reports incorrect timings on M1 machines. (patch by Olivier Dormond) -- 2023_: [Linux] cpu_freq() return order is wrong on systems with > 9 CPUs. +- 2023_, [Linux]: `cpu_freq()`_ return order is wrong on systems with more than + 9 CPUs. 5.8.0 ===== @@ -63,9 +66,9 @@ XXXX-XX-XX **Enhancements** -- 1863_: `disk_partitions()` exposes 2 extra fields: `maxfile` and `maxpath`, +- 1863_: `disk_partitions()`_ exposes 2 extra fields: ``maxfile`` and ``maxpath``, which are the maximum file name and path name length. -- 1872_: [Windows] added support for PyPy 2.7. +- 1872_, [Windows]: added support for PyPy 2.7. - 1879_: provide pre-compiled wheels for Linux and macOS (yey!). - 1880_: get rid of Travis and Cirrus CI services (they are no longer free). CI testing is now done by GitHub Actions on Linux, macOS and FreeBSD (yes). @@ -73,20 +76,25 @@ XXXX-XX-XX **Bug fixes** -- 1708_: [Linux] get rid of sensors_temperatures() duplicates. (patch by Tim +- 1708_, [Linux]: get rid of `sensors_temperatures()`_ duplicates. (patch by Tim Schlueter). -- 1839_: [Windows] always raise AccessDenied when failing to query 64 processes - from 32 bit ones (NtWoW64 APIs). -- 1866_: [Windows] process exe(), cmdline(), environ() may raise "invalid - access to memory location" on Python 3.9. -- 1874_: [Solaris] wrong swap output given when encrypted column is present. -- 1875_: [Windows] process username() may raise ERROR_NONE_MAPPED if the SID - has no corresponding account name. In this case AccessDenied is now raised. -- 1877_: [Windows] OpenProcess may fail with ERROR_SUCCESS. Turn it into - AccessDenied or NoSuchProcess depending on whether the PID is alive. -- 1886_: [macOS] EIO error may be raised on cmdline() and environment(). Now - it gets translated into AccessDenied. -- 1891_: [macOS] get rid of deprecated getpagesize(). +- 1839_, [Windows], **[critical]**: always raise `AccessDenied`_ instead of + ``WindowsError`` when failing to query 64 processes from 32 bit ones by using + ``NtWoW64`` APIs. +- 1866_, [Windows], **[critical]**: `Process.exe()`_, `Process.cmdline()`_, + `Process.environ()`_ may raise "[WinError 998] Invalid access to memory + location" on Python 3.9 / VS 2019. +- 1874_, [SunOS]: wrong swap output given when encrypted column is present. +- 1875_, [Windows], **[critical]**: `Process.username()`_ may raise + ``ERROR_NONE_MAPPED`` if the SID has no corresponding account name. In this + case `AccessDenied`_ is now raised. +- 1886_, [macOS]: ``EIO`` error may be raised on `Process.cmdline()`_ and + `Process.environ()`_. Now it gets translated into `AccessDenied`_. +- 1887_, [Windows], **[critical]**: ``OpenProcess`` may fail with + "[WinError 0] The operation completed successfully"." + Turn it into `AccessDenied`_ or `NoSuchProcess`_ depending on whether the + PID is alive. +- 1891_, [macOS]: get rid of deprecated ``getpagesize()``. 5.7.3 ===== @@ -95,24 +103,24 @@ XXXX-XX-XX **Enhancements** -- 809_: [FreeBSD] add support for `Process.rlimit()`. -- 893_: [BSD] add support for `Process.environ()` (patch by Armin Gruner) -- 1830_: [UNIX] `net_if_stats()`'s `isup` also checks whether the NIC is +- 809_, [FreeBSD]: add support for `Process.rlimit()`_. +- 893_, [BSD]: add support for `Process.environ()`_ (patch by Armin Gruner) +- 1830_, [POSIX]: `net_if_stats()`_ ``isup`` also checks whether the NIC is running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) -- 1837_: [Linux] improved battery detection and charge "secsleft" calculation +- 1837_, [Linux]: improved battery detection and charge ``secsleft`` calculation (patch by aristocratos) **Bug fixes** -- 1620_: [Linux] cpu_count(logical=False) result is incorrect on systems with - more than one CPU socket. (patch by Vincent A. Arcila) -- 1738_: [macOS] Process.exe() may raise FileNotFoundError if process is still +- 1620_, [Linux]: `cpu_count()`_ with ``logical=False`` result is incorrect on + systems with more than one CPU socket. (patch by Vincent A. Arcila) +- 1738_, [macOS]: `Process.exe()`_ may raise ``FileNotFoundError`` if process is still alive but the exe file which launched it got deleted. -- 1791_: [macOS] fix missing include for getpagesize(). -- 1823_: [Windows] Process.open_files() may cause a segfault due to a NULL - pointer. -- 1838_: [Linux] sensors_battery(): if `percent` can be determined but not - the remaining values, still return a result instead of None. +- 1791_, [macOS]: fix missing include for ``getpagesize()``. +- 1823_, [Windows], **[critical]**: `Process.open_files()`_ may cause a segfault + due to a NULL pointer. +- 1838_, [Linux]: `sensors_battery()`_: if `percent` can be determined but not + the remaining values, still return a result instead of ``None``. (patch by aristocratos) 5.7.2 @@ -131,35 +139,28 @@ XXXX-XX-XX **Enhancements** -- 1729_: parallel tests on UNIX (make test-parallel). They're twice as fast! -- 1741_: "make build/install" is now run in parallel and it's about 15% faster - on UNIX. -- 1747_: `Process.wait()` on POSIX returns an enum, showing the negative signal - which was used to terminate the process:: - >>> import psutil - >>> p = psutil.Process(9891) - >>> p.terminate() - >>> p.wait() - -- 1747_: `Process.wait()` return value is cached so that the exit code can be +- 1729_: parallel tests on POSIX (``make test-parallel``). They're twice as fast! +- 1741_, [POSIX]: ``make build`` now runs in parallel on Python >= 3.6 and + it's about 15% faster. +- 1747_: `Process.wait()`_ return value is cached so that the exit code can be retrieved on then next call. -- 1747_: Process provides more info about the process on str() and repr() - (status and exit code):: - >>> proc - psutil.Process(pid=12739, name='python3', status='terminated', - exitcode=, started='15:08:20') +- 1747_, [POSIX]: `Process.wait()`_ on POSIX now returns an enum, showing the + negative signal which was used to terminate the process. It returns something + like ````. +- 1747_: `Process`_ class provides more info about the process on ``str()`` + and ``repr()`` (status and exit code). - 1757_: memory leak tests are now stable. -- 1768_: [Windows] added support for Windows Nano Server. (contributed by +- 1768_, [Windows]: added support for Windows Nano Server. (contributed by Julien Lebot) **Bug fixes** -- 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64. +- 1726_, [Linux]: `cpu_freq()`_ parsing should use spaces instead of tabs on ia64. (patch by Michał Górny) -- 1760_: [Linux] Process.rlimit() does not handle long long type properly. -- 1766_: [macOS] NoSuchProcess may be raised instead of ZombieProcess. -- 1781_: fix signature of callback function for getloadavg(). (patch by - Ammar Askar) +- 1760_, [Linux]: `Process.rlimit()`_ does not handle long long type properly. +- 1766_, [macOS]: `NoSuchProcess`_ may be raised instead of `ZombieProcess`_. +- 1781_, **[critical]**: `getloadavg()`_ can crash the Python interpreter. + (patch by Ammar Askar) 5.7.0 ===== @@ -168,42 +169,44 @@ XXXX-XX-XX **Enhancements** -- 1637_: [SunOS] add partial support for old SunOS 5.10 Update 0 to 3. -- 1648_: [Linux] sensors_temperatures() looks into an additional /sys/device/ - directory for additional data. (patch by Javad Karabi) -- 1652_: [Windows] dropped support for Windows XP and Windows Server 2003. +- 1637_, [SunOS]: add partial support for old SunOS 5.10 Update 0 to 3. +- 1648_, [Linux]: `sensors_temperatures()`_ looks into an additional + ``/sys/device/`` directory for additional data. (patch by Javad Karabi) +- 1652_, [Windows]: dropped support for Windows XP and Windows Server 2003. Minimum supported Windows version now is Windows Vista. -- 1671_: [FreeBSD] add CI testing/service for FreeBSD (Cirrus CI). -- 1677_: [Windows] process exe() will succeed for all process PIDs (instead of - raising AccessDenied). -- 1679_: [Windows] net_connections() and Process.connections() are 10% faster. -- 1682_: [PyPy] added CI / test integration for PyPy via Travis. -- 1686_: [Windows] added support for PyPy on Windows. -- 1693_: [Windows] boot_time(), Process.create_time() and users()'s login time - now have 1 micro second precision (before the precision was of 1 second). +- 1671_, [FreeBSD]: add CI testing/service for FreeBSD (Cirrus CI). +- 1677_, [Windows]: `Process.exe()`_ will succeed for all process PIDs (instead of + raising `AccessDenied`_). +- 1679_, [Windows]: `net_connections()`_ and `Process.connections()`_ are 10% faster. +- 1682_, [PyPy]: added CI / test integration for PyPy via Travis. +- 1686_, [Windows]: added support for PyPy on Windows. +- 1693_, [Windows]: `boot_time()`_, `Process.create_time()`_ and `users()`_'s + login time now have 1 micro second precision (before the precision was of 1 + second). **Bug fixes** -- 1538_: [NetBSD] process cwd() may return ENOENT instead of NoSuchProcess. -- 1627_: [Linux] Process.memory_maps() can raise KeyError. -- 1642_: [SunOS] querying basic info for PID 0 results in FileNotFoundError. -- 1646_: [FreeBSD] many Process methods may cause a segfault on FreeBSD 12.0 - due to a backward incompatible change in a C type introduced in 12.0. -- 1656_: [Windows] Process.memory_full_info() raises AccessDenied even for the +- 1538_, [NetBSD]: `Process.cwd()`_ may return ``ENOENT`` instead of `NoSuchProcess`_. +- 1627_, [Linux]: `Process.memory_maps()`_ can raise ``KeyError``. +- 1642_, [SunOS]: querying basic info for PID 0 results in ``FileNotFoundError``. +- 1646_, [FreeBSD], **[critical]**: many `Process`_ methods may cause a segfault + due to a backward incompatible change in a C type on FreeBSD 12.0. +- 1656_, [Windows]: `Process.memory_full_info()`_ raises `AccessDenied`_ even for the current user and os.getpid(). -- 1660_: [Windows] Process.open_files() complete rewrite + check of errors. -- 1662_: [Windows] process exe() may raise WinError 0. -- 1665_: [Linux] disk_io_counters() does not take into account extra fields +- 1660_, [Windows]: `Process.open_files()`_ complete rewrite + check of errors. +- 1662_, [Windows], **[critical]**: `Process.exe()`_ may raise "[WinError 0] + The operation completed successfully". +- 1665_, [Linux]: `disk_io_counters()`_ does not take into account extra fields added to recent kernels. (patch by Mike Hommey) - 1672_: use the right C type when dealing with PIDs (int or long). Thus far (long) was almost always assumed, which is wrong on most platforms. -- 1673_: [OpenBSD] Process connections(), num_fds() and threads() returned - improper exception if process is gone. -- 1674_: [SunOS] disk_partitions() may raise OSError. -- 1684_: [Linux] disk_io_counters() may raise ValueError on systems not - having /proc/diskstats. -- 1695_: [Linux] could not compile on kernels <= 2.6.13 due to - PSUTIL_HAVE_IOPRIO not being defined. (patch by Anselm Kruis) +- 1673_, [OpenBSD]: `Process.connections()`_, `Process.num_fds()`_ and + `Process.threads()`_ returned improper exception if process is gone. +- 1674_, [SunOS]: `disk_partitions()`_ may raise ``OSError``. +- 1684_, [Linux]: `disk_io_counters()`_ may raise ``ValueError`` on systems not + having ``/proc/diskstats``. +- 1695_, [Linux]: could not compile on kernels <= 2.6.13 due to + ``PSUTIL_HAVE_IOPRIO`` not being defined. (patch by Anselm Kruis) 5.6.7 ===== @@ -212,7 +215,8 @@ XXXX-XX-XX **Bug fixes** -- 1630_: [Windows] can't compile source distribution due to C syntax error. +- 1630_, [Windows], **[critical]**: can't compile source distribution due to C + syntax error. 5.6.6 ===== @@ -221,14 +225,14 @@ XXXX-XX-XX **Bug fixes** -- 1179_: [Linux] Process cmdline() now takes into account misbehaving processes +- 1179_, [Linux]: `Process.cmdline()`_ now takes into account misbehaving processes renaming the command line and using inappropriate chars to separate args. -- 1616_: use of Py_DECREF instead of Py_CLEAR will result in double free and - segfault +- 1616_, **[critical]**: use of ``Py_DECREF`` instead of ``Py_CLEAR`` will + result in double ``free()`` and segfault (`CVE-2019-18874 `__). (patch by Riccardo Schirone) -- 1619_: [OpenBSD] compilation fails due to C syntax error. (patch by Nathan - Houghton) +- 1619_, [OpenBSD], **[critical]**: compilation fails due to C syntax error. + (patch by Nathan Houghton) 5.6.5 ===== @@ -237,7 +241,7 @@ XXXX-XX-XX **Bug fixes** -- 1615_: remove pyproject.toml as it was causing installation issues. +- 1615_: remove ``pyproject.toml`` as it was causing installation issues. 5.6.4 ===== @@ -246,31 +250,33 @@ XXXX-XX-XX **Enhancements** -- 1527_: [Linux] added Process.cpu_times().iowait counter, which is the time - spent waiting for blocking I/O to complete. +- 1527_, [Linux]: added `Process.cpu_times()`_ ``iowait`` counter, which is the + time spent waiting for blocking I/O to complete. - 1565_: add PEP 517/8 build backend and requirements specification for better pip integration. (patch by Bernát Gábor) **Bug fixes** -- 875_: [Windows] Process' cmdline(), environ() or cwd() may occasionally fail - with ERROR_PARTIAL_COPY which now gets translated to AccessDenied. -- 1126_: [Linux] cpu_affinity() segfaults on CentOS 5 / manylinux. - cpu_affinity() support for CentOS 5 was removed. -- 1528_: [AIX] compilation error on AIX 7.2 due to 32 vs 64 bit differences. - (patch by Arnon Yaari) -- 1535_: 'type' and 'family' fields returned by net_connections() are not +- 875_, [Windows], **[critical]**: `Process.cmdline()`_, `Process.environ()`_ or + `Process.cwd()`_ may occasionally fail with ``ERROR_PARTIAL_COPY`` which now + gets translated to `AccessDenied`_. +- 1126_, [Linux], **[critical]**: `Process.cpu_affinity()`_ segfaults on CentOS + 5 / manylinux. `Process.cpu_affinity()`_ support for CentOS 5 was removed. +- 1528_, [AIX], **[critical]**: compilation error on AIX 7.2 due to 32 vs 64 + bit differences. (patch by Arnon Yaari) +- 1535_: ``type`` and ``family`` fields returned by `net_connections()`_ are not always turned into enums. -- 1536_: [NetBSD] process cmdline() erroneously raise ZombieProcess error if +- 1536_, [NetBSD]: `Process.cmdline()`_ erroneously raise `ZombieProcess`_ error if cmdline has non encodable chars. - 1546_: usage percent may be rounded to 0 on Python 2. -- 1552_: [Windows] getloadavg() math for calculating 5 and 15 mins values is +- 1552_, [Windows]: `getloadavg()`_ math for calculating 5 and 15 mins values is incorrect. -- 1568_: [Linux] use CC compiler env var if defined. -- 1570_: [Windows] `NtWow64*` syscalls fail to raise the proper error code -- 1585_: [OSX] calling close() (in C) on possible negative integers. (patch - by Athos Ribeiro) -- 1606_: [SunOS] compilation fails on SunOS 5.10. (patch by vser1) +- 1568_, [Linux]: use CC compiler env var if defined. +- 1570_, [Windows]: ``NtWow64*`` syscalls fail to raise the proper error code +- 1585_, [OSX]: avoid calling ``close()`` (in C) on possible negative integers. + (patch by Athos Ribeiro) +- 1606_, [SunOS], **[critical]**: compilation fails on SunOS 5.10. + (patch by vser1) 5.6.3 ===== @@ -279,16 +285,16 @@ XXXX-XX-XX **Enhancements** -- 1494_: [AIX] added support for Process.environ(). (patch by Arnon Yaari) +- 1494_, [AIX]: added support for `Process.environ()`_. (patch by Arnon Yaari) **Bug fixes** -- 1276_: [AIX] can't get whole cmdline(). (patch by Arnon Yaari) -- 1501_: [Windows] Process cmdline() and exe() raise unhandled "WinError 1168 - element not found" exceptions for "Registry" and "Memory Compression" psuedo - processes on Windows 10. -- 1526_: [NetBSD] process cmdline() could raise MemoryError. (patch by - Kamil Rytarowski) +- 1276_, [AIX]: can't get whole `Process.cmdline()`_. (patch by Arnon Yaari) +- 1501_, [Windows]: `Process.cmdline()`_ and `Process.exe()`_ raise unhandled + "WinError 1168 element not found" exceptions for "Registry" and + "Memory Compression" psuedo processes on Windows 10. +- 1526_, [NetBSD], **[critical]**: `Process.cmdline()`_ could raise + ``MemoryError``. (patch by Kamil Rytarowski) 5.6.2 ===== @@ -297,44 +303,48 @@ XXXX-XX-XX **Enhancements** -- 604_: [Windows, Windows] add new psutil.getloadavg(), returning system load - average calculation, including on Windows (emulated). (patch by Ammar Askar) -- 1404_: [Linux] cpu_count(logical=False) uses a second method (read from - `/sys/devices/system/cpu/cpu[0-9]/topology/core_id`) in order to determine - the number of CPU cores in case /proc/cpuinfo does not provide this info. -- 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. -- 1464_: various docfixes (always point to python3 doc, fix links, etc.). -- 1476_: [Windows] it is now possible to set process high I/O priority - (ionice()).Also, I/O priority values are now exposed as 4 new constants: - IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH. +- 604_, [Windows]: add new `getloadavg()`_, returning system load average + calculation, including on Windows (emulated). (patch by Ammar Askar) +- 1404_, [Linux]: `cpu_count()`_ with ``logical=False`` uses a second method + (read from ``/sys/devices/system/cpu/cpu[0-9]/topology/core_id``) in order to + determine the number of CPU cores in case ``/proc/cpuinfo`` does not provide this + info. +- 1458_: provide coloured test output. Also show failures on + ``KeyboardInterrupt``. +- 1464_: various docfixes (always point to Python 3 doc, fix links, etc.). +- 1476_, [Windows]: it is now possible to set process high I/O priority + (`Process.ionice()`_). Also, I/O priority values are now exposed as 4 new + constants: ``IOPRIO_VERYLOW``, ``IOPRIO_LOW``, ``IOPRIO_NORMAL``, + ``IOPRIO_HIGH``. - 1478_: add make command to re-run tests failed on last run. **Bug fixes** -- 1223_: [Windows] boot_time() may return value on Windows XP. -- 1456_: [Linux] cpu_freq() returns None instead of 0.0 when min/max not - available (patch by Alex Manuskin) -- 1462_: [Linux] (tests) make tests invariant to LANG setting (patch by +- 1223_, [Windows]: `boot_time()`_ may return incorrect value on Windows XP. +- 1456_, [Linux]: `cpu_freq()`_ returns ``None`` instead of 0.0 when ``min`` + and ``max`` fields can't be determined. (patch by Alex Manuskin) +- 1462_, [Linux]: (tests) make tests invariant to ``LANG`` setting (patch by Benjamin Drung) -- 1463_: cpu_distribution.py script was broken. -- 1470_: [Linux] disk_partitions(): fix corner case when /etc/mtab doesn't - exist. (patch by Cedric Lamoriniere) -- 1471_: [SunOS] Process name() and cmdline() can return SystemError. (patch - by Daniel Beer) -- 1472_: [Linux] cpu_freq() does not return all CPUs on Rasbperry-pi 3. -- 1474_: fix formatting of psutil.tests() which mimicks 'ps aux' output. -- 1475_: [Windows] OSError.winerror attribute wasn't properly checked resuling - in WindowsError being raised instead of AccessDenied. -- 1477_: [Windows] wrong or absent error handling for private NTSTATUS Windows - APIs. Different process methods were affected by this. -- 1480_: [Windows] psutil.cpu_count(logical=False) could cause a crash due to - fixed read violation. (patch by Samer Masterson) -- 1486_: [AIX, SunOS] AttributeError when interacting with Process methods - involved into oneshot() context. -- 1491_: [SunOS] net_if_addrs(): free() ifap struct on error. (patch by - Agnewee) -- 1493_: [Linux] cpu_freq(): handle the case where - /sys/devices/system/cpu/cpufreq/ exists but is empty. +- 1463_: `cpu_distribution.py`_ script was broken. +- 1470_, [Linux]: `disk_partitions()`_: fix corner case when ``/etc/mtab`` + doesn't exist. (patch by Cedric Lamoriniere) +- 1471_, [SunOS]: `Process.name()`_ and `Process.cmdline()`_ can return + ``SystemError``. (patch by Daniel Beer) +- 1472_, [Linux]: `cpu_freq()`_ does not return all CPUs on Rasbperry-pi 3. +- 1474_: fix formatting of ``psutil.tests()`` which mimicks ``ps aux`` output. +- 1475_, [Windows], **[critical]**: ``OSError.winerror`` attribute wasn't + properly checked resuling in ``WindowsError(ERROR_ACCESS_DENIED)`` being + raised instead of `AccessDenied`_. +- 1477_, [Windows]: wrong or absent error handling for private ``NTSTATUS`` + Windows APIs. Different process methods were affected by this. +- 1480_, [Windows], **[critical]**: `cpu_count()`_ with ``logical=False`` could + cause a crash due to fixed read violation. (patch by Samer Masterson) +- 1486_, [AIX], [SunOS]: ``AttributeError`` when interacting with `Process`_ + methods involved into `Process.oneshot()`_ context. +- 1491_, [SunOS]: `net_if_addrs()`_: use ``free()`` against ``ifap`` struct + on error. (patch by Agnewee) +- 1493_, [Linux]: `cpu_freq()`_: handle the case where + ``/sys/devices/system/cpu/cpufreq/`` exists but it's empty. 5.6.1 ===== @@ -343,11 +353,12 @@ XXXX-XX-XX **Bug fixes** -- 1329_: [AIX] psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) -- 1448_: [Windows] crash on import due to rtlIpv6AddressToStringA not available - on Wine. -- 1451_: [Windows] Process.memory_full_info() segfaults. NtQueryVirtualMemory - is now used instead of QueryWorkingSet to calculate USS memory. +- 1329_, [AIX]: psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) +- 1448_, [Windows], **[critical]**: crash on import due to ``rtlIpv6AddressToStringA`` + not available on Wine. +- 1451_, [Windows], **[critical]**: `Process.memory_full_info()`_ segfaults. + ``NtQueryVirtualMemory`` is now used instead of ``QueryWorkingSet`` to + calculate USS memory. 5.6.0 ===== @@ -356,48 +367,51 @@ XXXX-XX-XX **Enhancements** -- 1379_: [Windows] Process suspend() and resume() now use NtSuspendProcess - and NtResumeProcess instead of stopping/resuming all threads of a process. - This is faster and more reliable (aka this is what ProcessHacker does). -- 1420_: [Windows] in case of exception disk_usage() now also shows the path +- 1379_, [Windows]: `Process.suspend()`_ and `Process.resume()`_ now use + ``NtSuspendProcess`` and ``NtResumeProcess`` instead of stopping/resuming all + threads of a process. This is faster and more reliable (aka this is what + ProcessHacker does). +- 1420_, [Windows]: in case of exception `disk_usage()`_ now also shows the path name. -- 1422_: [Windows] Windows APIs requiring to be dynamically loaded from DLL +- 1422_, [Windows]: Windows APIs requiring to be dynamically loaded from DLL libraries are now loaded only once on startup (instead of on per function call) significantly speeding up different functions and methods. -- 1426_: [Windows] PAGESIZE and number of processors is now calculated on +- 1426_, [Windows]: ``PAGESIZE`` and number of processors is now calculated on startup. - 1428_: in case of error, the traceback message now shows the underlying C function called which failed. -- 1433_: new Process.parents() method. (idea by Ghislain Le Meur) -- 1437_: pids() are returned in sorted order. -- 1442_: python3 is now the default interpreter used by Makefile. +- 1433_: new `Process.parents()`_ method. (idea by Ghislain Le Meur) +- 1437_: `pids()`_ are returned in sorted order. +- 1442_: Python 3 is now the default interpreter used by Makefile. **Bug fixes** -- 1353_: process_iter() is now thread safe (it rarely raised TypeError). -- 1394_: [Windows] Process name() and exe() may erroneously return "Registry". - QueryFullProcessImageNameW is now used instead of GetProcessImageFileNameW - in order to prevent that. -- 1411_: [BSD] lack of Py_DECREF could cause segmentation fault on process +- 1353_: `process_iter()`_ is now thread safe (it rarely raised ``TypeError``). +- 1394_, [Windows], **[critical]**: `Process.name()`_ and `Process.exe()`_ may + erroneously return "Registry" or fail with "[Error 0] The operation completed + successfully". + ``QueryFullProcessImageNameW`` is now used instead of + ``GetProcessImageFileNameW`` in order to prevent that. +- 1411_, [BSD]: lack of ``Py_DECREF`` could cause segmentation fault on process instantiation. -- 1419_: [Windows] Process.environ() raises NotImplementedError when querying - a 64-bit process in 32-bit-WoW mode. Now it raises AccessDenied. -- 1427_: [OSX] Process cmdline() and environ() may erroneously raise OSError - on failed malloc(). -- 1429_: [Windows] SE DEBUG was not properly set for current process. It is - now, and it should result in less AccessDenied exceptions for low-pid +- 1419_, [Windows]: `Process.environ()`_ raises ``NotImplementedError`` when + querying a 64-bit process in 32-bit-WoW mode. Now it raises `AccessDenied`_. +- 1427_, [OSX]: `Process.cmdline()`_ and `Process.environ()`_ may erroneously + raise ``OSError`` on failed ``malloc()``. +- 1429_, [Windows]: ``SE DEBUG`` was not properly set for current process. It is + now, and it should result in less `AccessDenied`_ exceptions for low PID processes. -- 1432_: [Windows] Process.memory_info_ex()'s USS memory is miscalculated - because we're not using the actual system PAGESIZE. -- 1439_: [NetBSD] Process.connections() may return incomplete results if using - oneshot(). -- 1447_: original exception wasn't turned into NSP/AD exceptions when using - Process.oneshot() ctx manager. +- 1432_, [Windows]: `Process.memory_info_ex()`_'s USS memory is miscalculated + because we're not using the actual system ``PAGESIZE``. +- 1439_, [NetBSD]: `Process.connections()`_ may return incomplete results if using + `Process.oneshot()`_. +- 1447_: original exception wasn't turned into `NoSuchProcess`_ / `AccessDenied`_ + exceptions when using `Process.oneshot()`_ context manager. **Incompatible API changes** -- 1291_: [OSX] Process.memory_maps() was removed because inherently broken - (segfault) for years. +- 1291_, [OSX], **[critical]**: `Process.memory_maps()`_ was removed because + inherently broken (segfault) for years. 5.5.1 ===== @@ -406,17 +420,17 @@ XXXX-XX-XX **Enhancements** -- 1348_: [Windows] on Windows >= 8.1 if Process.cmdline() fails due to - ERROR_ACCESS_DENIED attempt using NtQueryInformationProcess + - ProcessCommandLineInformation. (patch by EccoTheFlintstone) +- 1348_, [Windows]: on Windows >= 8.1 if `Process.cmdline()`_ fails due to + ``ERROR_ACCESS_DENIED`` attempt using ``NtQueryInformationProcess`` + + ``ProcessCommandLineInformation``. (patch by EccoTheFlintstone) **Bug fixes** -- 1394_: [Windows] Process.exe() returns "[Error 0] The operation completed +- 1394_, [Windows]: `Process.exe()`_ returns "[Error 0] The operation completed successfully" when Python process runs in "Virtual Secure Mode". -- 1402_: psutil exceptions' repr() show the internal private module path. -- 1408_: [AIX] psutil won't compile on AIX 7.1 due to missing header. (patch - by Arnon Yaari) +- 1402_: psutil exceptions' ``repr()`` show the internal private module path. +- 1408_, [AIX], **[critical]**: psutil won't compile on AIX 7.1 due to missing + header. (patch by Arnon Yaari) 5.5.0 ===== @@ -425,27 +439,27 @@ XXXX-XX-XX **Enhancements** -- 1350_: [FreeBSD] added support for sensors_temperatures(). (patch by Alex +- 1350_, [FreeBSD]: added support for `sensors_temperatures()`_. (patch by Alex Manuskin) -- 1352_: [FreeBSD] added support for CPU frequency. (patch by Alex Manuskin) +- 1352_, [FreeBSD]: added support for `cpu_freq()`_. (patch by Alex Manuskin) **Bug fixes** -- 1111_: Process.oneshot() is now thread safe. -- 1354_: [Linux] disk_io_counters() fails on Linux kernel 4.18+. -- 1357_: [Linux] Process' memory_maps() and io_counters() method are no longer - exposed if not supported by the kernel. -- 1368_: [Windows] fix psutil.Process().ionice(...) mismatch. (patch by +- 1111_: `Process.oneshot()`_ is now thread safe. +- 1354_, [Linux]: `disk_io_counters()`_ fails on Linux kernel 4.18+. +- 1357_, [Linux]: `Process.memory_maps()`_ and `Process.io_counters()`_ methods + are no longer exposed if not supported by the kernel. +- 1368_, [Windows]: fix `Process.ionice()`_ mismatch. (patch by EccoTheFlintstone) -- 1370_: [Windows] improper usage of CloseHandle() may lead to override the +- 1370_, [Windows]: improper usage of ``CloseHandle()`` may lead to override the original error code when raising an exception. -- 1373_: incorrect handling of cache in Process.oneshot() context causes - Process instances to return incorrect results. -- 1376_: [Windows] OpenProcess() now uses PROCESS_QUERY_LIMITED_INFORMATION - access rights wherever possible, resulting in less AccessDenied exceptions +- 1373_, **[critical]**: incorrect handling of cache in `Process.oneshot()`_ + context causes `Process`_ instances to return incorrect results. +- 1376_, [Windows]: ``OpenProcess`` now uses ``PROCESS_QUERY_LIMITED_INFORMATION`` + access rights wherever possible, resulting in less `AccessDenied`_ exceptions being thrown for system processes. -- 1376_: [Windows] check if variable is NULL before free()ing it. (patch by - EccoTheFlintstone) +- 1376_, [Windows]: check if variable is ``NULL`` before ``free()`` ing it. + (patch by EccoTheFlintstone) 5.4.8 ===== @@ -454,28 +468,28 @@ XXXX-XX-XX **Enhancements** -- 1197_: [Linux] cpu_freq() is now implemented by parsing /proc/cpuinfo in case - /sys/devices/system/cpu/* filesystem is not available. -- 1310_: [Linux] psutil.sensors_temperatures() now parses /sys/class/thermal - in case /sys/class/hwmon fs is not available (e.g. Raspberry Pi). (patch +- 1197_, [Linux]: `cpu_freq()`_ is now implemented by parsing ``/proc/cpuinfo`` + in case ``/sys/devices/system/cpu/*`` filesystem is not available. +- 1310_, [Linux]: `sensors_temperatures()`_ now parses ``/sys/class/thermal`` + in case ``/sys/class/hwmon`` fs is not available (e.g. Raspberry Pi). (patch by Alex Manuskin) -- 1320_: [Posix] better compilation support when using g++ instead of gcc. +- 1320_, [POSIX]: better compilation support when using g++ instead of GCC. (patch by Jaime Fullaondo) **Bug fixes** -- 715_: do not print exception on import time in case cpu_times() fails. -- 1004_: [Linux] Process.io_counters() may raise ValueError. -- 1277_: [OSX] available and used memory (psutil.virtual_memory()) metrics are +- 715_: do not print exception on import time in case `cpu_times()`_ fails. +- 1004_, [Linux]: `Process.io_counters()`_ may raise ``ValueError``. +- 1277_, [OSX]: available and used memory (`virtual_memory()`_) metrics are not accurate. -- 1294_: [Windows] psutil.Process().connections() may sometimes fail with - intermittent 0xC0000001. (patch by Sylvain Duchesne) -- 1307_: [Linux] disk_partitions() does not honour PROCFS_PATH. -- 1320_: [AIX] system CPU times (psutil.cpu_times()) were being reported with +- 1294_, [Windows]: `Process.connections()`_ may sometimes fail with + intermittent ``0xC0000001``. (patch by Sylvain Duchesne) +- 1307_, [Linux]: `disk_partitions()`_ does not honour `PROCFS_PATH`_. +- 1320_, [AIX]: system CPU times (`cpu_times()`_) were being reported with ticks unit as opposed to seconds. (patch by Jaime Fullaondo) -- 1332_: [OSX] psutil debug messages are erroneously printed all the time. +- 1332_, [OSX]: psutil debug messages are erroneously printed all the time. (patch by Ilya Yanok) -- 1346_: [SunOS] net_connections() returns an empty list. (patch by Oleksii +- 1346_, [SunOS]: `net_connections()`_ returns an empty list. (patch by Oleksii Shevchuk) 5.4.7 @@ -485,28 +499,28 @@ XXXX-XX-XX **Enhancements** -- 1286_: [macOS] psutil.OSX constant is now deprecated in favor of new - psutil.MACOS. -- 1309_: [Linux] added psutil.STATUS_PARKED constant for Process.status(). -- 1321_: [Linux] add disk_io_counters() dual implementation relying on - /sys/block filesystem in case /proc/diskstats is not available. (patch by - Lawrence Ye) +- 1286_, [macOS]: ``psutil.OSX`` constant is now deprecated in favor of new + ``psutil.MACOS``. +- 1309_, [Linux]: added ``psutil.STATUS_PARKED`` constant for `Process.status()`_. +- 1321_, [Linux]: add `disk_io_counters()`_ dual implementation relying on + ``/sys/block`` filesystem in case ``/proc/diskstats`` is not available. + (patch by Lawrence Ye) **Bug fixes** -- 1209_: [macOS] Process.memory_maps() may fail with EINVAL due to poor - task_for_pid() syscall. AccessDenied is now raised instead. -- 1278_: [macOS] Process.threads() incorrectly return microseconds instead of +- 1209_, [macOS]: `Process.memory_maps()`_ may fail with ``EINVAL`` due to poor + ``task_for_pid()`` syscall. `AccessDenied`_ is now raised instead. +- 1278_, [macOS]: `Process.threads()`_ incorrectly return microseconds instead of seconds. (patch by Nikhil Marathe) -- 1279_: [Linux, macOS, BSD] net_if_stats() may return ENODEV. -- 1294_: [Windows] psutil.Process().connections() may sometime fail with - MemoryError. (patch by sylvainduchesne) -- 1305_: [Linux] disk_io_stats() may report inflated r/w bytes values. -- 1309_: [Linux] Process.status() is unable to recognize "idle" and "parked" - statuses (returns '?'). -- 1313_: [Linux] disk_io_counters() can report inflated IO counters due to - erroneously counting base disk device and its partition(s) twice. -- 1323_: [Linux] sensors_temperatures() may fail with ValueError. +- 1279_, [Linux], [macOS], [BSD]: `net_if_stats()`_ may return ``ENODEV``. +- 1294_, [Windows]: `Process.connections()`_ may sometime fail with + ``MemoryError``. (patch by sylvainduchesne) +- 1305_, [Linux]: `disk_io_counters()`_ may report inflated r/w bytes values. +- 1309_, [Linux]: `Process.status()`_ is unable to recognize ``"idle"`` and + ``"parked"`` statuses (returns ``"?"``). +- 1313_, [Linux]: `disk_io_counters()`_ can report inflated values due to + counting base disk device and its partition(s) twice. +- 1323_, [Linux]: `sensors_temperatures()`_ may fail with ``ValueError``. 5.4.6 ===== @@ -515,12 +529,12 @@ XXXX-XX-XX **Bug fixes** -- 1258_: [Windows] Process.username() may cause a segfault (Python interpreter - crash). (patch by Jean-Luc Migot) -- 1273_: net_if_addr() namedtuple's name has been renamed from "snic" to - "snicaddr". -- 1274_: [Linux] there was a small chance Process.children() may swallow - AccessDenied exceptions. +- 1258_, [Windows], **[critical]**: `Process.username()`_ may cause a segfault + (Python interpreter crash). (patch by Jean-Luc Migot) +- 1273_: `net_if_addrs()`_ namedtuple's name has been renamed from ``snic`` to + ``snicaddr``. +- 1274_, [Linux]: there was a small chance `Process.children()`_ may swallow + `AccessDenied`_ exceptions. 5.4.5 ===== @@ -529,7 +543,7 @@ XXXX-XX-XX **Bug fixes** -- 1268_: setup.py's extra_require parameter requires latest setuptools version, +- 1268_: setup.py's ``extra_require`` parameter requires latest setuptools version, breaking quite a lot of installations. 5.4.4 @@ -539,47 +553,47 @@ XXXX-XX-XX **Enhancements** -- 1239_: [Linux] expose kernel "slab" memory for psutil.virtual_memory(). +- 1239_, [Linux]: expose kernel ``slab`` memory field for `virtual_memory()`_. (patch by Maxime Mouial) **Bug fixes** -- 694_: [SunOS] cmdline() could be truncated at the 15th character when - reading it from /proc. An extra effort is made by reading it from process +- 694_, [SunOS]: `Process.cmdline()`_ could be truncated at the 15th character when + reading it from ``/proc``. An extra effort is made by reading it from process address space first. (patch by Georg Sauthoff) -- 771_: [Windows] cpu_count() (both logical and cores) return a wrong +- 771_, [Windows]: `cpu_count()`_ (both logical and cores) return a wrong (smaller) number on systems using process groups (> 64 cores). -- 771_: [Windows] cpu_times(percpu=True) return fewer CPUs on systems using - process groups (> 64 cores). -- 771_: [Windows] cpu_stats() and cpu_freq() may return incorrect results on +- 771_, [Windows]: `cpu_times()`_ with ``percpu=True`` return fewer CPUs on + systems using process groups (> 64 cores). +- 771_, [Windows]: `cpu_stats()`_ and `cpu_freq()`_ may return incorrect results on systems using process groups (> 64 cores). -- 1193_: [SunOS] Return uid/gid from /proc/pid/psinfo if there aren't - enough permissions for /proc/pid/cred. (patch by Georg Sauthoff) -- 1194_: [SunOS] Return nice value from psinfo as getpriority() doesn't +- 1193_, [SunOS]: return uid/gid from ``/proc/pid/psinfo`` if there aren't + enough permissions for ``/proc/pid/cred``. (patch by Georg Sauthoff) +- 1194_, [SunOS]: return nice value from ``psinfo`` as ``getpriority()`` doesn't support real-time processes. (patch by Georg Sauthoff) -- 1194_: [SunOS] Fix double free in psutil_proc_cpu_num(). (patch by Georg +- 1194_, [SunOS]: fix double ``free()`` in `Process.cpu_num()`_. (patch by Georg Sauthoff) -- 1194_: [SunOS] Fix undefined behavior related to strict-aliasing rules +- 1194_, [SunOS]: fix undefined behavior related to strict-aliasing rules and warnings. (patch by Georg Sauthoff) -- 1210_: [Linux] cpu_percent() steal time may remain stuck at 100% due to Linux +- 1210_, [Linux]: `cpu_percent()`_ steal time may remain stuck at 100% due to Linux erroneously reporting a decreased steal time between calls. (patch by Arnon Yaari) -- 1216_: fix compatibility with python 2.6 on Windows (patch by Dan Vinakovsky) -- 1222_: [Linux] Process.memory_full_info() was erroneously summing "Swap:" and +- 1216_: fix compatibility with Python 2.6 on Windows (patch by Dan Vinakovsky) +- 1222_, [Linux]: `Process.memory_full_info()`_ was erroneously summing "Swap:" and "SwapPss:". Same for "Pss:" and "SwapPss". Not anymore. -- 1224_: [Windows] Process.wait() may erroneously raise TimeoutExpired. -- 1238_: [Linux] sensors_battery() may return None in case battery is not - listed as "BAT0" under /sys/class/power_supply. -- 1240_: [Windows] cpu_times() float loses accuracy in a long running system. +- 1224_, [Windows]: `Process.wait()`_ may erroneously raise `TimeoutExpired`_. +- 1238_, [Linux]: `sensors_battery()`_ may return ``None`` in case battery is not + listed as "BAT0" under ``/sys/class/power_supply``. +- 1240_, [Windows]: `cpu_times()`_ float loses accuracy in a long running system. (patch by stswandering) -- 1245_: [Linux] sensors_temperatures() may fail with IOError "no such file". -- 1255_: [FreeBSD] swap_memory() stats were erroneously represented in KB. +- 1245_, [Linux]: `sensors_temperatures()`_ may fail with ``IOError`` "no such file". +- 1255_, [FreeBSD]: `swap_memory()`_ stats were erroneously represented in KB. (patch by Denis Krienbühl) **Backward compatibility** -- 771_: [Windows] cpu_count(logical=False) on Windows XP and Vista is no - longer supported and returns None. +- 771_, [Windows]: `cpu_count()`_ with ``logical=False`` on Windows XP and Vista + is no longer supported and returns ``None``. 5.4.3 ===== @@ -588,11 +602,11 @@ XXXX-XX-XX **Enhancements** -- 775_: disk_partitions() on Windows return mount points. +- 775_: `disk_partitions()`_ on Windows return mount points. **Bug fixes** -- 1193_: pids() may return False on macOS. +- 1193_: `pids()`_ may return ``False`` on macOS. 5.4.2 ===== @@ -601,24 +615,24 @@ XXXX-XX-XX **Enhancements** -- 1173_: introduced PSUTIL_DEBUG environment variable which can be set in order +- 1173_: introduced ``PSUTIL_DEBUG`` environment variable which can be set in order to print useful debug messages on stderr (useful in case of nasty errors). -- 1177_: added support for sensors_battery() on macOS. (patch by Arnon Yaari) -- 1183_: Process.children() is 2x faster on UNIX and 2.4x faster on Linux. -- 1188_: deprecated method Process.memory_info_ex() now warns by using - FutureWarning instead of DeprecationWarning. +- 1177_, [macOS]: added support for `sensors_battery()`_. (patch by Arnon Yaari) +- 1183_: `Process.children()`_ is 2x faster on POSIX and 2.4x faster on Linux. +- 1188_: deprecated method `Process.memory_info_ex()`_ now warns by using + ``FutureWarning`` instead of ``DeprecationWarning``. **Bug fixes** -- 1152_: [Windows] disk_io_counters() may return an empty dict. -- 1169_: [Linux] users() "hostname" returns username instead. (patch by +- 1152_, [Windows]: `disk_io_counters()`_ may return an empty dict. +- 1169_, [Linux]: `users()`_ ``hostname`` returns username instead. (patch by janderbrain) -- 1172_: [Windows] `make test` does not work. -- 1179_: [Linux] Process.cmdline() is now able to splits cmdline args for - misbehaving processes which overwrite /proc/pid/cmdline and use spaces +- 1172_, [Windows]: ``make test`` does not work. +- 1179_, [Linux]: `Process.cmdline()`_ is now able to split cmdline args for + misbehaving processes which overwrite ``/proc/pid/cmdline`` and use spaces instead of null bytes as args separator. -- 1181_: [macOS] Process.memory_maps() may raise ENOENT. -- 1187_: [macOS] pids() does not return PID 0 on recent macOS versions. +- 1181_, [macOS]: `Process.memory_maps()`_ may raise ``ENOENT``. +- 1187_, [macOS]: `pids()`_ does not return PID 0 on recent macOS versions. 5.4.1 ===== @@ -627,18 +641,19 @@ XXXX-XX-XX **Enhancements** -- 1164_: [AIX] add support for Process.num_ctx_switches(). (patch by Arnon +- 1164_, [AIX]: add support for `Process.num_ctx_switches()`_. (patch by Arnon Yaari) -- 1053_: abandon Python 3.3 support (psutil still works but it's no longer +- 1053_: drop Python 3.3 support (psutil still works but it's no longer tested). **Bug fixes** -- 1150_: [Windows] when a process is terminate()d now the exit code is set to - SIGTERM instead of 0. (patch by Akos Kiss) -- 1151_: python -m psutil.tests fail -- 1154_: [AIX] psutil won't compile on AIX 6.1.0. (patch by Arnon Yaari) -- 1167_: [Windows] net_io_counter() packets count now include also non-unicast +- 1150_, [Windows]: when a process is terminated now the exit code is set to + ``SIGTERM`` instead of ``0``. (patch by Akos Kiss) +- 1151_: ``python -m psutil.tests`` fail. +- 1154_, [AIX], **[critical]**: psutil won't compile on AIX 6.1.0. + (patch by Arnon Yaari) +- 1167_, [Windows]: `net_io_counters()`_ packets count now include also non-unicast packets. (patch by Matthew Long) 5.4.0 @@ -648,22 +663,21 @@ XXXX-XX-XX **Enhancements** -- 1123_: [AIX] added support for AIX platform. (patch by Arnon Yaari) +- 1123_, [AIX]: added support for AIX platform. (patch by Arnon Yaari) **Bug fixes** -- 1009_: [Linux] sensors_temperatures() may crash with IOError. -- 1012_: [Windows] disk_io_counters()'s read_time and write_time were expressed - in tens of micro seconds instead of milliseconds. -- 1127_: [macOS] invalid reference counting in Process.open_files() may lead to - segfault. (patch by Jakub Bacic) -- 1129_: [Linux] sensors_fans() may crash with IOError. (patch by Sebastian - Saip) -- 1131_: [SunOS] fix compilation warnings. (patch by Arnon Yaari) -- 1133_: [Windows] can't compile on newer versions of Visual Studio 2017 15.4. +- 1009_, [Linux]: `sensors_temperatures()`_ may crash with ``IOError``. +- 1012_, [Windows]: `disk_io_counters()`_ ``read_time`` and ``write_time`` + were expressed in tens of micro seconds instead of milliseconds. +- 1127_, [macOS], **[critical]**: invalid reference counting in + `Process.open_files()`_ may lead to segfault. (patch by Jakub Bacic) +- 1129_, [Linux]: `sensors_fans()`_ may crash with ``IOError``. (patch by + Sebastian Saip) +- 1131_, [SunOS]: fix compilation warnings. (patch by Arnon Yaari) +- 1133_, [Windows]: can't compile on newer versions of Visual Studio 2017 15.4. (patch by Max Bélanger) -- 1138_: [Linux] can't compile on CentOS 5.0 and RedHat 5.0. - (patch by Prodesire) +- 1138_, [Linux]: can't compile on CentOS 5.0 and RedHat 5.0. (patch by Prodesire) 5.3.1 ===== @@ -676,13 +690,13 @@ XXXX-XX-XX **Bug fixes** -- 1105_: [FreeBSD] psutil does not compile on FreeBSD 12. -- 1125_: [BSD] net_connections() raises TypeError. +- 1105_, [FreeBSD]: psutil does not compile on FreeBSD 12. +- 1125_, [BSD]: `net_connections()`_ raises ``TypeError``. **Compatibility notes** -- 1120_: .exe files for Windows are no longer uploaded on PyPI as per PEP-527; - only wheels are provided. +- 1120_: ``.exe`` files for Windows are no longer uploaded on PyPI as per + PEP-527. Only wheels are provided. 5.3.0 ===== @@ -691,104 +705,99 @@ XXXX-XX-XX **Enhancements** -- 802_: disk_io_counters() and net_io_counters() numbers no longer wrap - (restart from 0). Introduced a new "nowrap" argument. -- 928_: psutil.net_connections() and psutil.Process.connections() "laddr" and - "raddr" are now named tuples. -- 1015_: swap_memory() now relies on /proc/meminfo instead of sysinfo() syscall - so that it can be used in conjunction with PROCFS_PATH in order to retrieve - memory info about Linux containers such as Docker and Heroku. -- 1022_: psutil.users() provides a new "pid" field. -- 1025_: process_iter() accepts two new parameters in order to invoke - Process.as_dict(): "attrs" and "ad_value". With this you can iterate over all - processes in one shot without needing to catch NoSuchProcess and do list/dict - comprehensions. +- 802_: `disk_io_counters()`_ and `net_io_counters()`_ numbers no longer wrap + (restart from 0). Introduced a new ``nowrap`` argument. +- 928_: `net_connections()`_ and `Process.connections()`_ ``laddr`` and + ``raddr`` are now named tuples. +- 1015_: `swap_memory()`_ now relies on ``/proc/meminfo`` instead of ``sysinfo()`` + syscall so that it can be used in conjunction with `PROCFS_PATH`_ in order to + retrieve memory info about Linux containers such as Docker and Heroku. +- 1022_: `users()`_ provides a new ``pid`` field. +- 1025_: `process_iter()`_ accepts two new parameters in order to invoke + `Process.as_dict()`_: ``attrs`` and ``ad_value``. With these you can iterate + over all processes in one shot without needing to catch `NoSuchProcess`_ and + do list/dict comprehensions. - 1040_: implemented full unicode support. -- 1051_: disk_usage() on Python 3 is now able to accept bytes. +- 1051_: `disk_usage()`_ on Python 3 is now able to accept bytes. - 1058_: test suite now enables all warnings by default. - 1060_: source distribution is dynamically generated so that it only includes relevant files. -- 1079_: [FreeBSD] net_connections()'s fd number is now being set for real - (instead of -1). (patch by Gleb Smirnoff) -- 1091_: [SunOS] implemented Process.environ(). (patch by Oleksii Shevchuk) +- 1079_, [FreeBSD]: `net_connections()`_ ``fd`` number is now being set for real + (instead of ``-1``). (patch by Gleb Smirnoff) +- 1091_, [SunOS]: implemented `Process.environ()`_. (patch by Oleksii Shevchuk) **Bug fixes** -- 989_: [Windows] boot_time() may return a negative value. -- 1007_: [Windows] boot_time() can have a 1 sec fluctuation between calls; the - value of the first call is now cached so that boot_time() always returns the - same value if fluctuation is <= 1 second. -- 1013_: [FreeBSD] psutil.net_connections() may return incorrect PID. (patch +- 989_, [Windows]: `boot_time()`_ may return a negative value. +- 1007_, [Windows]: `boot_time()`_ can have a 1 sec fluctuation between calls. + The value of the first call is now cached so that `boot_time()`_ always + returns the same value if fluctuation is <= 1 second. +- 1013_, [FreeBSD]: `net_connections()`_ may return incorrect PID. (patch by Gleb Smirnoff) -- 1014_: [Linux] Process class can mask legitimate ENOENT exceptions as - NoSuchProcess. -- 1016_: disk_io_counters() raises RuntimeError on a system with no disks. -- 1017_: net_io_counters() raises RuntimeError on a system with no network +- 1014_, [Linux]: `Process`_ class can mask legitimate ``ENOENT`` exceptions as + `NoSuchProcess`_. +- 1016_: `disk_io_counters()`_ raises ``RuntimeError`` on a system with no disks. +- 1017_: `net_io_counters()`_ raises ``RuntimeError`` on a system with no network cards installed. -- 1021_: [Linux] open_files() may erroneously raise NoSuchProcess instead of - skipping a file which gets deleted while open files are retrieved. -- 1029_: [macOS, FreeBSD] Process.connections('unix') on Python 3 doesn't - properly handle unicode paths and may raise UnicodeDecodeError. -- 1033_: [macOS, FreeBSD] memory leak for net_connections() and - Process.connections() when retrieving UNIX sockets (kind='unix'). -- 1040_: fixed many unicode related issues such as UnicodeDecodeError on - Python 3 + UNIX and invalid encoded data on Windows. -- 1042_: [FreeBSD] psutil won't compile on FreeBSD 12. -- 1044_: [macOS] different Process methods incorrectly raise AccessDenied for - zombie processes. -- 1046_: [Windows] disk_partitions() on Windows overrides user's SetErrorMode. -- 1047_: [Windows] Process username(): memory leak in case exception is thrown. -- 1048_: [Windows] users()'s host field report an invalid IP address. -- 1050_: [Windows] Process.memory_maps memory() leaks memory. -- 1055_: cpu_count() is no longer cached; this is useful on systems such as +- 1021_, [Linux]: `Process.open_files()`_ may erroneously raise `NoSuchProcess`_ + instead of skipping a file which gets deleted while open files are retrieved. +- 1029_, [macOS], [FreeBSD]: `Process.connections()`_ with ``family=unix`` on Python + 3 doesn't properly handle unicode paths and may raise ``UnicodeDecodeError``. +- 1033_, [macOS], [FreeBSD]: memory leak for `net_connections()`_ and + `Process.connections()`_ when retrieving UNIX sockets (``kind='unix'``). +- 1040_: fixed many unicode related issues such as ``UnicodeDecodeError`` on + Python 3 + POSIX and invalid encoded data on Windows. +- 1042_, [FreeBSD], **[critical]**: psutil won't compile on FreeBSD 12. +- 1044_, [macOS]: different `Process`_ methods incorrectly raise `AccessDenied`_ + for zombie processes. +- 1046_, [Windows]: `disk_partitions()`_ on Windows overrides user's ``SetErrorMode``. +- 1047_, [Windows]: `Process.username()`_: memory leak in case exception is thrown. +- 1048_, [Windows]: `users()`_ ``host`` field report an invalid IP address. +- 1050_, [Windows]: `Process.memory_maps()`_ leaks memory. +- 1055_: `cpu_count()`_ is no longer cached. This is useful on systems such as Linux where CPUs can be disabled at runtime. This also reflects on - Process.cpu_percent() which no longer uses the cache. + `Process.cpu_percent()`_ which no longer uses the cache. - 1058_: fixed Python warnings. -- 1062_: disk_io_counters() and net_io_counters() raise TypeError if no disks - or NICs are installed on the system. -- 1063_: [NetBSD] net_connections() may list incorrect sockets. -- 1064_: [NetBSD] swap_memory() may segfault in case of error. -- 1065_: [OpenBSD] Process.cmdline() may raise SystemError. -- 1067_: [NetBSD] Process.cmdline() leaks memory if process has terminated. -- 1069_: [FreeBSD] Process.cpu_num() may return 255 for certain kernel +- 1062_: `disk_io_counters()`_ and `net_io_counters()`_ raise ``TypeError`` if + no disks or NICs are installed on the system. +- 1063_, [NetBSD]: `net_connections()`_ may list incorrect sockets. +- 1064_, [NetBSD], **[critical]**: `swap_memory()`_ may segfault in case of error. +- 1065_, [OpenBSD], **[critical]**: `Process.cmdline()`_ may raise ``SystemError``. +- 1067_, [NetBSD]: `Process.cmdline()`_ leaks memory if process has terminated. +- 1069_, [FreeBSD]: `Process.cpu_num()`_ may return 255 for certain kernel processes. -- 1071_: [Linux] cpu_freq() may raise IOError on old RedHat distros. -- 1074_: [FreeBSD] sensors_battery() raises OSError in case of no battery. -- 1075_: [Windows] net_if_addrs(): inet_ntop() return value is not checked. -- 1077_: [SunOS] net_if_addrs() shows garbage addresses on SunOS 5.10. +- 1071_, [Linux]: `cpu_freq()`_ may raise ``IOError`` on old RedHat distros. +- 1074_, [FreeBSD]: `sensors_battery()`_ raises ``OSError`` in case of no battery. +- 1075_, [Windows]: `net_if_addrs()`_: ``inet_ntop()`` return value is not checked. +- 1077_, [SunOS]: `net_if_addrs()`_ shows garbage addresses on SunOS 5.10. (patch by Oleksii Shevchuk) -- 1077_: [SunOS] net_connections() does not work on SunOS 5.10. (patch by +- 1077_, [SunOS]: `net_connections()`_ does not work on SunOS 5.10. (patch by Oleksii Shevchuk) -- 1079_: [FreeBSD] net_connections() didn't list locally connected sockets. +- 1079_, [FreeBSD]: `net_connections()`_ didn't list locally connected sockets. (patch by Gleb Smirnoff) -- 1085_: cpu_count() return value is now checked and forced to None if <= 1. -- 1087_: Process.cpu_percent() guard against cpu_count() returning None and - assumes 1 instead. -- 1093_: [SunOS] memory_maps() shows wrong 64 bit addresses. -- 1094_: [Windows] psutil.pid_exists() may lie. Also, all process APIs relying - on OpenProcess Windows API now check whether the PID is actually running. -- 1098_: [Windows] Process.wait() may erroneously return sooner, when the PID +- 1085_: `cpu_count()`_ return value is now checked and forced to ``None`` if <= 1. +- 1087_: `Process.cpu_percent()`_ guard against `cpu_count()`_ returning ``None`` + and assumes 1 instead. +- 1093_, [SunOS]: `Process.memory_maps()`_ shows wrong 64 bit addresses. +- 1094_, [Windows]: `pid_exists()`_ may lie. Also, all process APIs relying + on ``OpenProcess`` Windows API now check whether the PID is actually running. +- 1098_, [Windows]: `Process.wait()`_ may erroneously return sooner, when the PID is still alive. -- 1099_: [Windows] Process.terminate() may raise AccessDenied even if the +- 1099_, [Windows]: `Process.terminate()`_ may raise `AccessDenied`_ even if the process already died. -- 1101_: [Linux] sensors_temperatures() may raise ENODEV. +- 1101_, [Linux]: `sensors_temperatures()`_ may raise ``ENODEV``. **Porting notes** -- 1039_: returned types consolidation: - - Windows / Process.cpu_times(): fields #3 and #4 were int instead of float - - Linux / FreeBSD: connections('unix'): raddr is now set to "" instead of - None - - OpenBSD: connections('unix'): laddr and raddr are now set to "" instead of - None +- 1039_: returned types consolidation. 1) Windows / `Process.cpu_times()`_: + fields #3 and #4 were int instead of float. 2) Linux / FreeBSD / OpenBSD: + `Process.connections()`_ ``raddr`` is now set to ``""`` instead of ``None`` + when retrieving UNIX sockets. - 1040_: all strings are encoded by using OS fs encoding. - 1040_: the following Windows APIs on Python 2 now return a string instead of - unicode: - - Process.memory_maps().path - - WindowsService.bin_path() - - WindowsService.description() - - WindowsService.display_name() - - WindowsService.username() + unicode: ``Process.memory_maps().path``, ``WindowsService.bin_path()``, + ``WindowsService.description()``, ``WindowsService.display_name()``, + ``WindowsService.username()``. 5.2.2 ===== @@ -798,13 +807,13 @@ XXXX-XX-XX **Bug fixes** - 1000_: fixed some setup.py warnings. -- 1002_: [SunOS] remove C macro which will not be available on new Solaris +- 1002_, [SunOS]: remove C macro which will not be available on new Solaris versions. (patch by Danek Duvall) -- 1004_: [Linux] Process.io_counters() may raise ValueError. -- 1006_: [Linux] cpu_freq() may return None on some Linux versions does not - support the function; now the function is not declared instead. -- 1009_: [Linux] sensors_temperatures() may raise OSError. -- 1010_: [Linux] virtual_memory() may raise ValueError on Ubuntu 14.04. +- 1004_, [Linux]: `Process.io_counters()`_ may raise ``ValueError``. +- 1006_, [Linux]: `cpu_freq()`_ may return ``None`` on some Linux versions does not + support the function. Let's not make the function available instead. +- 1009_, [Linux]: `sensors_temperatures()`_ may raise ``OSError``. +- 1010_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` on Ubuntu 14.04. 5.2.1 ===== @@ -813,12 +822,12 @@ XXXX-XX-XX **Bug fixes** -- 981_: [Linux] cpu_freq() may return an empty list. -- 993_: [Windows] Process.memory_maps() on Python 3 may raise - UnicodeDecodeError. -- 996_: [Linux] sensors_temperatures() may not show all temperatures. -- 997_: [FreeBSD] virtual_memory() may fail due to missing sysctl parameter on - FreeBSD 12. +- 981_, [Linux]: `cpu_freq()`_ may return an empty list. +- 993_, [Windows]: `Process.memory_maps()`_ on Python 3 may raise + ``UnicodeDecodeError``. +- 996_, [Linux]: `sensors_temperatures()`_ may not show all temperatures. +- 997_, [FreeBSD]: `virtual_memory()`_ may fail due to missing ``sysctl`` + parameter on FreeBSD 12. 5.2.0 ===== @@ -827,26 +836,26 @@ XXXX-XX-XX **Enhancements** -- 971_: [Linux] Add psutil.sensors_fans() function. (patch by Nicolas Hennion) -- 976_: [Windows] Process.io_counters() has 2 new fields: *other_count* and - *other_bytes*. -- 976_: [Linux] Process.io_counters() has 2 new fields: *read_chars* and - *write_chars*. +- 971_, [Linux]: Add `sensors_fans()`_ function. (patch by Nicolas Hennion) +- 976_, [Windows]: `Process.io_counters()`_ has 2 new fields: ``other_count`` and + ``other_bytes``. +- 976_, [Linux]: `Process.io_counters()`_ has 2 new fields: ``read_chars`` and + ``write_chars``. **Bug fixes** -- 872_: [Linux] can now compile on Linux by using MUSL C library. -- 985_: [Windows] Fix a crash in `Process.open_files` when the worker thread - for `NtQueryObject` times out. -- 986_: [Linux] Process.cwd() may raise NoSuchProcess instead of ZombieProcess. +- 872_, [Linux]: can now compile on Linux by using MUSL C library. +- 985_, [Windows]: Fix a crash in `Process.open_files()`_ when the worker thread + for ``NtQueryObject`` times out. +- 986_, [Linux]: `Process.cwd()`_ may raise `NoSuchProcess`_ instead of `ZombieProcess`_. 5.1.3 ===== **Bug fixes** -- 971_: [Linux] sensors_temperatures() didn't work on CentOS 7. -- 973_: cpu_percent() may raise ZeroDivisionError. +- 971_, [Linux]: `sensors_temperatures()`_ didn't work on CentOS 7. +- 973_, **[critical]**: `cpu_percent()`_ may raise ``ZeroDivisionError``. 5.1.2 ===== @@ -855,11 +864,11 @@ XXXX-XX-XX **Bug fixes** -- 966_: [Linux] sensors_battery().power_plugged may erroneously return None on - Python 3. -- 968_: [Linux] disk_io_counters() raises TypeError on python 3. -- 970_: [Linux] sensors_battery()'s name and label fields on Python 3 are bytes - instead of str. +- 966_, [Linux]: `sensors_battery()`_ ``power_plugged`` may erroneously return + ``None`` on Python 3. +- 968_, [Linux]: `disk_io_counters()`_ raises ``TypeError`` on Python 3. +- 970_, [Linux]: `sensors_battery()`_ ``name`` and ``label`` fields on Python 3 + are bytes instead of str. 5.1.1 ===== @@ -868,16 +877,16 @@ XXXX-XX-XX **Enhancements** -- 966_: [Linux] sensors_battery().percent is a float and is more precise. +- 966_, [Linux]: `sensors_battery()`_ ``percent`` is a float and is more precise. **Bug fixes** -- 964_: [Windows] Process.username() and psutil.users() may return badly - decoding character on Python 3. -- 965_: [Linux] disk_io_counters() may miscalculate sector size and report the - wrong number of bytes read and written. -- 966_: [Linux] sensors_battery() may fail with "no such file error". -- 966_: [Linux] sensors_battery().power_plugged may lie. +- 964_, [Windows]: `Process.username()`_ and `users()`_ may return badly + decoded character on Python 3. +- 965_, [Linux]: `disk_io_counters()`_ may miscalculate sector size and report + the wrong number of bytes read and written. +- 966_, [Linux]: `sensors_battery()`_ may fail with ``FileNotFoundError``. +- 966_, [Linux]: `sensors_battery()`_ ``power_plugged`` may lie. 5.1.0 ===== @@ -886,26 +895,26 @@ XXXX-XX-XX **Enhancements** -- 357_: added psutil.Process.cpu_num() (what CPU a process is on). -- 371_: added psutil.sensors_temperatures() (Linux only). -- 941_: added psutil.cpu_freq() (CPU frequency). -- 955_: added psutil.sensors_battery() (Linux, Windows, only). -- 956_: cpu_affinity([]) can now be used as an alias to set affinity against - all eligible CPUs. +- 357_: added `Process.cpu_num()`_ (what CPU a process is on). +- 371_: added `sensors_temperatures()`_ (Linux only). +- 941_: added `cpu_freq()`_ (CPU frequency). +- 955_: added `sensors_battery()`_ (Linux, Windows, only). +- 956_: `Process.cpu_affinity()`_ can now be passed ``[]`` argument as an + alias to set affinity against all eligible CPUs. **Bug fixes** -- 687_: [Linux] pid_exists() no longer returns True if passed a process thread - ID. -- 948_: cannot install psutil with PYTHONOPTIMIZE=2. -- 950_: [Windows] Process.cpu_percent() was calculated incorrectly and showed +- 687_, [Linux]: `pid_exists()`_ no longer returns ``True`` if passed a process + thread ID. +- 948_: cannot install psutil with ``PYTHONOPTIMIZE=2``. +- 950_, [Windows]: `Process.cpu_percent()`_ was calculated incorrectly and showed higher number than real usage. -- 951_: [Windows] the uploaded wheels for Python 3.6 64 bit didn't work. +- 951_, [Windows]: the uploaded wheels for Python 3.6 64 bit didn't work. - 959_: psutil exception objects could not be pickled. -- 960_: Popen.wait() did not return the correct negative exit status if process - is ``kill()``ed by a signal. -- 961_: [Windows] WindowsService.description() may fail with - ERROR_MUI_FILE_NOT_FOUND. +- 960_: `psutil.Popen`_ ``wait()`` did not return the correct negative exit + status if process is killed by a signal. +- 961_, [Windows]: ``WindowsService.description()`` method may fail with + ``ERROR_MUI_FILE_NOT_FOUND``. 5.0.1 ===== @@ -915,17 +924,17 @@ XXXX-XX-XX **Enhancements** - 939_: tar.gz distribution went from 1.8M to 258K. -- 811_: [Windows] provide a more meaningful error message if trying to use +- 811_, [Windows]: provide a more meaningful error message if trying to use psutil on unsupported Windows XP. **Bug fixes** -- 609_: [SunOS] psutil does not compile on Solaris 10. -- 936_: [Windows] fix compilation error on VS 2013 (patch by Max Bélanger). -- 940_: [Linux] cpu_percent() and cpu_times_percent() was calculated - incorrectly as "iowait", "guest" and "guest_nice" times were not properly - taken into account. -- 944_: [OpenBSD] psutil.pids() was omitting PID 0. +- 609_, [SunOS], **[critical]**: psutil does not compile on Solaris 10. +- 936_, [Windows]: fix compilation error on VS 2013 (patch by Max Bélanger). +- 940_, [Linux]: `cpu_percent()`_ and `cpu_times_percent()`_ was calculated + incorrectly as ``iowait``, ``guest`` and ``guest_nice`` times were not + properly taken into account. +- 944_, [OpenBSD]: `pids()`_ was omitting PID 0. 5.0.0 ===== @@ -934,15 +943,16 @@ XXXX-XX-XX **Enhncements** -- 799_: new Process.oneshot() context manager making Process methods around +- 799_: new `Process.oneshot()`_ context manager making `Process`_ methods around +2x faster in general and from +2x to +6x faster on Windows. - 943_: better error message in case of version conflict on import. **Bug fixes** -- 932_: [NetBSD] net_connections() and Process.connections() may fail without - raising an exception. -- 933_: [Windows] memory leak in cpu_stats() and WindowsService.description(). +- 932_, [NetBSD]: `net_connections()`_ and `Process.connections()`_ may fail + without raising an exception. +- 933_, [Windows]: memory leak in `cpu_stats()`_ and + ``WindowsService.description()`` method. 4.4.2 ===== @@ -951,7 +961,7 @@ XXXX-XX-XX **Bug fixes** -- 931_: psutil no longer compiles on Solaris. +- 931_, **[critical]**: psutil no longer compiles on Solaris. 4.4.1 ===== @@ -960,7 +970,8 @@ XXXX-XX-XX **Bug fixes** -- 927_: ``Popen.__del__`` may cause maximum recursion depth error. +- 927_, **[critical]**: `psutil.Popen`_ ``__del__`` may cause maximum recursion + depth error. 4.4.0 ===== @@ -969,34 +980,34 @@ XXXX-XX-XX **Enhancements** -- 874_: [Windows] net_if_addrs() returns also the netmask. -- 887_: [Linux] virtual_memory()'s 'available' and 'used' values are more - precise and match "free" cmdline utility. "available" also takes into - account LCX containers preventing "available" to overflow "total". -- 891_: procinfo.py script has been updated and provides a lot more info. +- 874_, [Windows]: make `net_if_addrs()`_ also return the ``netmask``. +- 887_, [Linux]: `virtual_memory()`_ ``available`` and ``used`` values are more + precise and match ``free`` cmdline utility. ``available`` also takes into + account LCX containers preventing ``available`` to overflow ``total``. +- 891_: `procinfo.py`_ script has been updated and provides a lot more info. **Bug fixes** -- 514_: [macOS] possibly fix Process.memory_maps() segfault (critical!). -- 783_: [macOS] Process.status() may erroneously return "running" for zombie - processes. -- 798_: [Windows] Process.open_files() returns and empty list on Windows 10. -- 825_: [Linux] cpu_affinity; fix possible double close and use of unopened - socket. -- 880_: [Windows] Handle race condition inside psutil_net_connections. -- 885_: ValueError is raised if a negative integer is passed to cpu_percent() +- 514_, [macOS], **[critical]**: `Process.memory_maps()`_ can segfault. +- 783_, [macOS]: `Process.status()`_ may erroneously return ``"running"`` for + zombie processes. +- 798_, [Windows]: `Process.open_files()`_ returns and empty list on Windows 10. +- 825_, [Linux]: `Process.cpu_affinity()`_: fix possible double close and use of + unopened socket. +- 880_, [Windows]: fix race condition inside `net_connections()`_. +- 885_: ``ValueError`` is raised if a negative integer is passed to `cpu_percent()`_ functions. -- 892_: [Linux] Process.cpu_affinity([-1]) raise SystemError with no error - set; now ValueError is raised. -- 906_: [BSD] disk_partitions(all=False) returned an empty list. Now the - argument is ignored and all partitions are always returned. -- 907_: [FreeBSD] Process.exe() may fail with OSError(ENOENT). -- 908_: [macOS, BSD] different process methods could errounesuly mask the real - error for high-privileged PIDs and raise NoSuchProcess and AccessDenied - instead of OSError and RuntimeError. -- 909_: [macOS] Process open_files() and connections() methods may raise - OSError with no exception set if process is gone. -- 916_: [macOS] fix many compilation warnings. +- 892_, [Linux], **[critical]**: `Process.cpu_affinity()`_ with ``[-1]`` as arg + raises ``SystemError`` with no error set; now ``ValueError`` is raised. +- 906_, [BSD]: `disk_partitions()`_ with ``all=False`` returned an empty list. + Now the argument is ignored and all partitions are always returned. +- 907_, [FreeBSD]: `Process.exe()`_ may fail with ``OSError(ENOENT)``. +- 908_, [macOS], [BSD]: different process methods could errounesuly mask the real + error for high-privileged PIDs and raise `NoSuchProcess`_ and `AccessDenied`_ + instead of ``OSError`` and ``RuntimeError``. +- 909_, [macOS]: `Process.open_files()`_ and `Process.connections()`_ methods + may raise ``OSError`` with no exception set if process is gone. +- 916_, [macOS]: fix many compilation warnings. 4.3.1 ===== @@ -1005,22 +1016,23 @@ XXXX-XX-XX **Enhancements** -- 881_: "make install" now works also when using a virtual env. +- 881_: ``make install`` now works also when using a virtual env. **Bug fixes** -- 854_: Process.as_dict() raises ValueError if passed an erroneous attrs name. -- 857_: [SunOS] Process cpu_times(), cpu_percent(), threads() amd memory_maps() - may raise RuntimeError if attempting to query a 64bit process with a 32bit - python. "Null" values are returned as a fallback. -- 858_: Process.as_dict() should not return memory_info_ex() because it's - deprecated. -- 863_: [Windows] memory_map truncates addresses above 32 bits -- 866_: [Windows] win_service_iter() and services in general are not able to +- 854_: `Process.as_dict()`_ raises ``ValueError`` if passed an erroneous attrs name. +- 857_, [SunOS]: `Process.cpu_times()`_, `Process.cpu_percent()`_, + `Process.threads()`_ and `Process.memory_maps()`_ may raise ``RuntimeError`` if + attempting to query a 64bit process with a 32bit Python. "Null" values are + returned as a fallback. +- 858_: `Process.as_dict()`_ should not call `Process.memory_info_ex()`_ + because it's deprecated. +- 863_, [Windows]: `Process.memory_maps()`_ truncates addresses above 32 bits. +- 866_, [Windows]: `win_service_iter()`_ and services in general are not able to handle unicode service names / descriptions. -- 869_: [Windows] Process.wait() may raise TimeoutExpired with wrong timeout +- 869_, [Windows]: `Process.wait()`_ may raise `TimeoutExpired`_ with wrong timeout unit (ms instead of sec). -- 870_: [Windows] Handle leak inside psutil_get_process_data. +- 870_, [Windows]: handle leak inside ``psutil_get_process_data``. 4.3.0 ===== @@ -1029,20 +1041,20 @@ XXXX-XX-XX **Enhancements** -- 819_: [Linux] different speedup improvements: - Process.ppid() is 20% faster - Process.status() is 28% faster - Process.name() is 25% faster - Process.num_threads is 20% faster on Python 3 +- 819_, [Linux]: different speedup improvements: + `Process.ppid()`_ +20% faster. + `Process.status()`_ +28% faster. + `Process.name()`_ +25% faster. + `Process.num_threads()`_ +20% faster on Python 3. **Bug fixes** -- 810_: [Windows] Windows wheels are incompatible with pip 7.1.2. -- 812_: [NetBSD] fix compilation on NetBSD-5.x. -- 823_: [NetBSD] virtual_memory() raises TypeError on Python 3. -- 829_: [UNIX] psutil.disk_usage() percent field takes root reserved space +- 810_, [Windows]: Windows wheels are incompatible with pip 7.1.2. +- 812_, [NetBSD], **[critical]**: fix compilation on NetBSD-5.x. +- 823_, [NetBSD]: `virtual_memory()`_ raises ``TypeError`` on Python 3. +- 829_, [POSIX]: `disk_usage()`_ ``percent`` field takes root reserved space into account. -- 816_: [Windows] fixed net_io_counter() values wrapping after 4.3GB in +- 816_, [Windows]: fixed `net_io_counters()`_ values wrapping after 4.3GB in Windows Vista (NT 6.0) and above using 64bit values from newer win APIs. 4.2.0 @@ -1052,20 +1064,20 @@ XXXX-XX-XX **Enhancements** -- 795_: [Windows] new APIs to deal with Windows services: win_service_iter() - and win_service_get(). -- 800_: [Linux] psutil.virtual_memory() returns a new "shared" memory field. -- 819_: [Linux] speedup /proc parsing: - - Process.ppid() is 20% faster - - Process.status() is 28% faster - - Process.name() is 25% faster - - Process.num_threads is 20% faster on Python 3 +- 795_, [Windows]: new APIs to deal with Windows services: `win_service_iter()`_ + and `win_service_get()`_. +- 800_, [Linux]: `virtual_memory()`_ returns a new ``shared`` memory field. +- 819_, [Linux]: speedup ``/proc`` parsing: + `Process.ppid()`_ +20% faster. + `Process.status()`_ +28% faster. + `Process.name()`_ +25% faster. + `Process.num_threads()`_ +20% faster on Python 3. **Bug fixes** -- 797_: [Linux] net_if_stats() may raise OSError for certain NIC cards. -- 813_: Process.as_dict() should ignore extraneous attribute names which gets - attached to the Process instance. +- 797_, [Linux]: `net_if_stats()`_ may raise ``OSError`` for certain NIC cards. +- 813_: `Process.as_dict()`_ should ignore extraneous attribute names which gets + attached to the `Process`_ instance. 4.1.0 ===== @@ -1074,25 +1086,26 @@ XXXX-XX-XX **Enhancements** -- 777_: [Linux] Process.open_files() on Linux return 3 new fields: position, - mode and flags. -- 779_: Process.cpu_times() returns two new fields, 'children_user' and - 'children_system' (always set to 0 on macOS and Windows). -- 789_: [Windows] psutil.cpu_times() return two new fields: "interrupt" and - "dpc". Same for psutil.cpu_times_percent(). -- 792_: new psutil.cpu_stats() function returning number of CPU ctx switches - interrupts, soft interrupts and syscalls. +- 777_, [Linux]: `Process.open_files()`_ on Linux return 3 new fields: + ``position``, ``mode`` and ``flags``. +- 779_: `Process.cpu_times()`_ returns two new fields, ``children_user`` and + ``children_system`` (always set to 0 on macOS and Windows). +- 789_, [Windows]: `cpu_times()`_ return two new fields: ``interrupt`` and + ``dpc``. Same for `cpu_times_percent()`_. +- 792_: new `cpu_stats()`_ function returning number of CPU ``ctx_switches``, + ``interrupts``, ``soft_interrupts`` and ``syscalls``. **Bug fixes** -- 774_: [FreeBSD] net_io_counters() dropout is no longer set to 0 if the kernel +- 774_, [FreeBSD]: `net_io_counters()`_ dropout is no longer set to 0 if the kernel provides it. -- 776_: [Linux] Process.cpu_affinity() may erroneously raise NoSuchProcess. +- 776_, [Linux]: `Process.cpu_affinity()`_ may erroneously raise `NoSuchProcess`_. (patch by wxwright) -- 780_: [macOS] psutil does not compile with some gcc versions. -- 786_: net_if_addrs() may report incomplete MAC addresses. -- 788_: [NetBSD] virtual_memory()'s buffers and shared values were set to 0. -- 790_: [macOS] psutil won't compile on macOS 10.4. +- 780_, [macOS]: psutil does not compile with some GCC versions. +- 786_: `net_if_addrs()`_ may report incomplete MAC addresses. +- 788_, [NetBSD]: `virtual_memory()`_ ``buffers`` and ``shared`` values were + set to 0. +- 790_, [macOS], **[critical]**: psutil won't compile on macOS 10.4. 4.0.0 ===== @@ -1101,41 +1114,43 @@ XXXX-XX-XX **Enhancements** -- 523_: [Linux, FreeBSD] disk_io_counters() return a new "busy_time" field. -- 660_: [Windows] make.bat is smarter in finding alternative VS install +- 523_, [Linux], [FreeBSD]: `disk_io_counters()`_ return a new ``busy_time`` field. +- 660_, [Windows]: make.bat is smarter in finding alternative VS install locations. (patch by mpderbec) -- 732_: Process.environ(). (patch by Frank Benkstein) -- 753_: [Linux, macOS, Windows] Process USS and PSS (Linux) "real" memory stats. - (patch by Eric Rahm) -- 755_: Process.memory_percent() "memtype" parameter. +- 732_: `Process.environ()`_. (patch by Frank Benkstein) +- 753_, [Linux], [macOS], [Windows]: process USS and PSS (Linux) "real" memory + stats. (patch by Eric Rahm) +- 755_: `Process.memory_percent()`_ ``memtype`` parameter. - 758_: tests now live in psutil namespace. -- 760_: expose OS constants (psutil.LINUX, psutil.macOS, etc.) -- 756_: [Linux] disk_io_counters() return 2 new fields: read_merged_count and - write_merged_count. -- 762_: new scripts/procsmem.py script. +- 760_: expose OS constants (``psutil.LINUX``, ``psutil.OSX``, etc.) +- 756_, [Linux]: `disk_io_counters()`_ return 2 new fields: ``read_merged_count`` + and ``write_merged_count``. +- 762_: new `procsmem.py`_ script. **Bug fixes** -- 685_: [Linux] virtual_memory() provides wrong results on systems with a lot +- 685_, [Linux]: `virtual_memory()`_ provides wrong results on systems with a lot of physical memory. -- 704_: [Solaris] psutil does not compile on Solaris sparc. -- 734_: on Python 3 invalid UTF-8 data is not correctly handled for process - name(), cwd(), exe(), cmdline() and open_files() methods resulting in - UnicodeDecodeError exceptions. 'surrogateescape' error handler is now - used as a workaround for replacing the corrupted data. -- 737_: [Windows] when the bitness of psutil and the target process was - different cmdline() and cwd() could return a wrong result or incorrectly - report an AccessDenied error. -- 741_: [OpenBSD] psutil does not compile on mips64. -- 751_: [Linux] fixed call to Py_DECREF on possible Null object. -- 754_: [Linux] cmdline() can be wrong in case of zombie process. -- 759_: [Linux] Process.memory_maps() may return paths ending with " (deleted)" -- 761_: [Windows] psutil.boot_time() wraps to 0 after 49 days. -- 764_: [NetBSD] fix compilation on NetBSD-6.x. -- 766_: [Linux] net_connections() can't handle malformed /proc/net/unix file. -- 767_: [Linux] disk_io_counters() may raise ValueError on 2.6 kernels and it's +- 704_, [SunOS]: psutil does not compile on Solaris sparc. +- 734_: on Python 3 invalid UTF-8 data is not correctly handled for + `Process.name()`_, `Process.cwd()`_, `Process.exe()`_, `Process.cmdline()`_ + and `Process.open_files()`_ methods resulting in ``UnicodeDecodeError`` + exceptions. ``'surrogateescape'`` error handler is now used as a workaround for + replacing the corrupted data. +- 737_, [Windows]: when the bitness of psutil and the target process was + different, `Process.cmdline()`_ and `Process.cwd()`_ could return a wrong + result or incorrectly report an `AccessDenied`_ error. +- 741_, [OpenBSD]: psutil does not compile on mips64. +- 751_, [Linux]: fixed call to ``Py_DECREF`` on possible ``NULL`` object. +- 754_, [Linux]: `Process.cmdline()`_ can be wrong in case of zombie process. +- 759_, [Linux]: `Process.memory_maps()`_ may return paths ending with ``" (deleted)"``. +- 761_, [Windows]: `boot_time()`_ wraps to 0 after 49 days. +- 764_, [NetBSD]: fix compilation on NetBSD-6.x. +- 766_, [Linux]: `net_connections()`_ can't handle malformed ``/proc/net/unix`` + file. +- 767_, [Linux]: `disk_io_counters()`_ may raise ``ValueError`` on 2.6 kernels and it's broken on 2.4 kernels. -- 770_: [NetBSD] disk_io_counters() metrics didn't update. +- 770_, [NetBSD]: `disk_io_counters()`_ metrics didn't update. 3.4.2 ===== @@ -1144,13 +1159,14 @@ XXXX-XX-XX **Enhancements** -- 728_: [Solaris] exposed psutil.PROCFS_PATH constant to change the default - location of /proc filesystem. +- 728_, [SunOS]: exposed `PROCFS_PATH`_ constant to change the default + location of ``/proc`` filesystem. **Bug fixes** -- 724_: [FreeBSD] psutil.virtual_memory().total is incorrect. -- 730_: [FreeBSD] psutil.virtual_memory() crashes. +- 724_, [FreeBSD]: `virtual_memory()`_ ``total`` is incorrect. +- 730_, [FreeBSD], **[critical]**: `virtual_memory()`_ crashes with + "OSError: [Errno 12] Cannot allocate memory". 3.4.1 ===== @@ -1159,21 +1175,22 @@ XXXX-XX-XX **Enhancements** -- 557_: [NetBSD] added NetBSD support. (contributed by Ryo Onodera and +- 557_, [NetBSD]: added NetBSD support. (contributed by Ryo Onodera and Thomas Klausner) -- 708_: [Linux] psutil.net_connections() and Process.connections() on Python 2 +- 708_, [Linux]: `net_connections()`_ and `Process.connections()`_ on Python 2 can be up to 3x faster in case of many connections. - Also psutil.Process.memory_maps() is slightly faster. -- 718_: process_iter() is now thread safe. + Also `Process.memory_maps()`_ is slightly faster. +- 718_: `process_iter()`_ is now thread safe. **Bug fixes** -- 714_: [OpenBSD] virtual_memory().cached value was always set to 0. -- 715_: don't crash at import time if cpu_times() fail for some reason. -- 717_: [Linux] Process.open_files fails if deleted files still visible. -- 722_: [Linux] swap_memory() no longer crashes if sin/sout can't be determined - due to missing /proc/vmstat. -- 724_: [FreeBSD] virtual_memory().total is slightly incorrect. +- 714_, [OpenBSD]: `virtual_memory()`_ ``cached`` value was always set to 0. +- 715_, **[critical]**: don't crash at import time if `cpu_times()`_ fail for + some reason. +- 717_, [Linux]: `Process.open_files()`_ fails if deleted files still visible. +- 722_, [Linux]: `swap_memory()`_ no longer crashes if ``sin`` / ``sout`` can't + be determined due to missing ``/proc/vmstat``. +- 724_, [FreeBSD]: `virtual_memory()`_ ``total`` is slightly incorrect. 3.3.0 ===== @@ -1182,13 +1199,13 @@ XXXX-XX-XX **Enhancements** -- 558_: [Linux] exposed psutil.PROCFS_PATH constant to change the default - location of /proc filesystem. -- 615_: [OpenBSD] added OpenBSD support. (contributed by Landry Breuil) +- 558_, [Linux]: exposed `PROCFS_PATH`_ constant to change the default + location of ``/proc`` filesystem. +- 615_, [OpenBSD]: added OpenBSD support. (contributed by Landry Breuil) **Bug fixes** -- 692_: [UNIX] Process.name() is no longer cached as it may change. +- 692_, [POSIX]: `Process.name()`_ is no longer cached as it may change. 3.2.2 ===== @@ -1197,15 +1214,15 @@ XXXX-XX-XX **Bug fixes** -- 517_: [SunOS] net_io_counters failed to detect network interfaces +- 517_, [SunOS]: `net_io_counters()`_ failed to detect network interfaces correctly on Solaris 10 -- 541_: [FreeBSD] disk_io_counters r/w times were expressed in seconds instead +- 541_, [FreeBSD]: `disk_io_counters()`_ r/w times were expressed in seconds instead of milliseconds. (patch by dasumin) -- 610_: [SunOS] fix build and tests on Solaris 10 -- 623_: [Linux] process or system connections raises ValueError if IPv6 is not +- 610_, [SunOS]: fix build and tests on Solaris 10 +- 623_, [Linux]: process or system connections raises ``ValueError`` if IPv6 is not supported by the system. -- 678_: [Linux] can't install psutil due to bug in setup.py. -- 688_: [Windows] compilation fails with MSVC 2015, Python 3.5. (patch by +- 678_, [Linux], **[critical]**: can't install psutil due to bug in setup.py. +- 688_, [Windows]: compilation fails with MSVC 2015, Python 3.5. (patch by Mike Sarahan) 3.2.1 @@ -1215,7 +1232,7 @@ XXXX-XX-XX **Bug fixes** -- 677_: [Linux] can't install psutil due to bug in setup.py. +- 677_, [Linux], **[critical]**: can't install psutil due to bug in setup.py. 3.2.0 ===== @@ -1224,37 +1241,32 @@ XXXX-XX-XX **Enhancements** -- 644_: [Windows] added support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals - to use with Process.send_signal(). +- 644_, [Windows]: added support for ``CTRL_C_EVENT`` and ``CTRL_BREAK_EVENT`` + signals to use with `Process.send_signal()`_. - 648_: CI test integration for macOS. (patch by Jeff Tang) -- 663_: [UNIX] net_if_addrs() now returns point-to-point (VPNs) addresses. -- 655_: [Windows] different issues regarding unicode handling were fixed. On +- 663_, [POSIX]: `net_if_addrs()`_ now returns point-to-point (VPNs) addresses. +- 655_, [Windows]: different issues regarding unicode handling were fixed. On Python 2 all APIs returning a string will now return an encoded version of it by using sys.getfilesystemencoding() codec. The APIs involved are: - - psutil.net_if_addrs() - - psutil.net_if_stats() - - psutil.net_io_counters() - - psutil.Process.cmdline() - - psutil.Process.name() - - psutil.Process.username() - - psutil.users() + `net_if_addrs()`_, `net_if_stats()`_, `net_io_counters()`_, + `Process.cmdline()`_, `Process.name()`_, `Process.username()`_, `users()`_. **Bug fixes** -- 513_: [Linux] fixed integer overflow for RLIM_INFINITY. -- 641_: [Windows] fixed many compilation warnings. (patch by Jeff Tang) -- 652_: [Windows] net_if_addrs() UnicodeDecodeError in case of non-ASCII NIC +- 513_, [Linux]: fixed integer overflow for ``RLIM_INFINITY``. +- 641_, [Windows]: fixed many compilation warnings. (patch by Jeff Tang) +- 652_, [Windows]: `net_if_addrs()`_ ``UnicodeDecodeError`` in case of non-ASCII NIC names. -- 655_: [Windows] net_if_stats() UnicodeDecodeError in case of non-ASCII NIC +- 655_, [Windows]: `net_if_stats()`_ ``UnicodeDecodeError`` in case of non-ASCII NIC names. -- 659_: [Linux] compilation error on Suse 10. (patch by maozguttman) -- 664_: [Linux] compilation error on Alpine Linux. (patch by Bart van Kleef) -- 670_: [Windows] segfgault of net_if_addrs() in case of non-ASCII NIC names. +- 659_, [Linux]: compilation error on Suse 10. (patch by maozguttman) +- 664_, [Linux]: compilation error on Alpine Linux. (patch by Bart van Kleef) +- 670_, [Windows]: segfgault of `net_if_addrs()`_ in case of non-ASCII NIC names. (patch by sk6249) -- 672_: [Windows] compilation fails if using Windows SDK v8.0. (patch by +- 672_, [Windows]: compilation fails if using Windows SDK v8.0. (patch by Steven Winfield) -- 675_: [Linux] net_connections(); UnicodeDecodeError may occur when listing - UNIX sockets. +- 675_, [Linux]: `net_connections()`_: ``UnicodeDecodeError`` may occur when + listing UNIX sockets. 3.1.1 ===== @@ -1263,9 +1275,10 @@ XXXX-XX-XX **Bug fixes** -- 603_: [Linux] ionice_set value range is incorrect. (patch by spacewander) -- 645_: [Linux] psutil.cpu_times_percent() may produce negative results. -- 656_: 'from psutil import *' does not work. +- 603_, [Linux]: `Process.ionice()`_ set value range is incorrect. + (patch by spacewander) +- 645_, [Linux]: `cpu_times_percent()`_ may produce negative results. +- 656_: ``from psutil import *`` does not work. 3.1.0 ===== @@ -1274,8 +1287,8 @@ XXXX-XX-XX **Enhancements** -- 534_: [Linux] disk_partitions() added support for ZFS filesystems. -- 646_: continuous tests integration for Windows with +- 534_, [Linux]: `disk_partitions()`_ added support for ZFS filesystems. +- 646_, [Windows]: continuous tests integration for Windows with https://ci.appveyor.com/project/giampaolo/psutil. - 647_: new dev guide: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst @@ -1283,20 +1296,20 @@ XXXX-XX-XX **Bug fixes** -- 340_: [Windows] Process.open_files() no longer hangs. Instead it uses a - thred which times out and skips the file handle in case it's taking too long - to be retrieved. (patch by Jeff Tang, PR #597) -- 627_: [Windows] Process.name() no longer raises AccessDenied for pids owned - by another user. -- 636_: [Windows] Process.memory_info() raise AccessDenied. -- 637_: [UNIX] raise exception if trying to send signal to Process PID 0 as it - will affect os.getpid()'s process group instead of PID 0. -- 639_: [Linux] Process.cmdline() can be truncated. -- 640_: [Linux] *connections functions may swallow errors and return an +- 340_, [Windows], **[critical]**: `Process.open_files()`_ no longer hangs. + Instead it uses a thred which times out and skips the file handle in case it's + taking too long to be retrieved. (patch by Jeff Tang) +- 627_, [Windows]: `Process.name()`_ no longer raises `AccessDenied`_ for pids + owned by another user. +- 636_, [Windows]: `Process.memory_info()`_ raise `AccessDenied`_. +- 637_, [POSIX]: raise exception if trying to send signal to PID 0 as it will + affect ``os.getpid()`` 's process group and not PID 0. +- 639_, [Linux]: `Process.cmdline()`_ can be truncated. +- 640_, [Linux]: ``*connections`` functions may swallow errors and return an incomplete list of connnections. -- 642_: repr() of exceptions is incorrect. -- 653_: [Windows] Add inet_ntop function for Windows XP to support IPv6. -- 641_: [Windows] Replace deprecated string functions with safe equivalents. +- 642_: ``repr()`` of exceptions is incorrect. +- 653_, [Windows]: add ``inet_ntop()`` function for Windows XP to support IPv6. +- 641_, [Windows]: replace deprecated string functions with safe equivalents. 3.0.1 ===== @@ -1305,10 +1318,10 @@ XXXX-XX-XX **Bug fixes** -- 632_: [Linux] better error message if cannot parse process UNIX connections. -- 634_: [Linux] Proces.cmdline() does not include empty string arguments. -- 635_: [UNIX] crash on module import if 'enum' package is installed on python - < 3.4. +- 632_, [Linux]: better error message if cannot parse process UNIX connections. +- 634_, [Linux]: `Process.cmdline()`_ does not include empty string arguments. +- 635_, [POSIX], **[critical]**: crash on module import if ``enum`` package is + installed on Python < 3.4. 3.0.0 ===== @@ -1317,45 +1330,45 @@ XXXX-XX-XX **Enhancements** -- 250_: new psutil.net_if_stats() returning NIC statistics (isup, duplex, - speed, MTU). -- 376_: new psutil.net_if_addrs() returning all NIC addresses a-la ifconfig. +- 250_: new `net_if_stats()`_ returning NIC statistics (``isup``, ``duplex``, + ``speed``, ``mtu``). +- 376_: new `net_if_addrs()`_ returning all NIC addresses a-la ``ifconfig``. - 469_: on Python >= 3.4 ``IOPRIO_CLASS_*`` and ``*_PRIORITY_CLASS`` constants - returned by psutil.Process' ionice() and nice() methods are enums instead of + returned by `Process.ionice()`_ and `Process.nice()`_ are enums instead of plain integers. -- 581_: add .gitignore. (patch by Gabi Davar) -- 582_: connection constants returned by psutil.net_connections() and - psutil.Process.connections() were turned from int to enums on Python > 3.4. -- 587_: Move native extension into the package. -- 589_: Process.cpu_affinity() accepts any kind of iterable (set, tuple, ...), +- 581_: add ``.gitignore``. (patch by Gabi Davar) +- 582_: connection constants returned by `net_connections()`_ and + `Process.connections()`_ were turned from int to enums on Python > 3.4. +- 587_: move native extension into the package. +- 589_: `Process.cpu_affinity()`_ accepts any kind of iterable (set, tuple, ...), not only lists. - 594_: all deprecated APIs were removed. -- 599_: [Windows] process name() can now be determined for all processes even +- 599_, [Windows]: `Process.name()`_ can now be determined for all processes even when running as a limited user. - 602_: pre-commit GIT hook. -- 629_: enhanced support for py.test and nose test discovery and tests run. -- 616_: [Windows] Add inet_ntop function for Windows XP. +- 629_: enhanced support for ``pytest`` and ``nose`` test runners. +- 616_, [Windows]: add ``inet_ntop()`` function for Windows XP. **Bug fixes** -- 428_: [all UNIXes except Linux] correct handling of zombie processes; - introduced new ZombieProcess exception class. -- 512_: [BSD] fix segfault in net_connections(). -- 555_: [Linux] psutil.users() correctly handles ":0" as an alias for - "localhost" -- 579_: [Windows] Fixed open_files() for PID>64K. -- 579_: [Windows] fixed many compiler warnings. -- 585_: [FreeBSD] net_connections() may raise KeyError. -- 586_: [FreeBSD] cpu_affinity() segfaults on set in case an invalid CPU - number is provided. -- 593_: [FreeBSD] Process().memory_maps() segfaults. -- 606_: Process.parent() may swallow NoSuchProcess exceptions. -- 611_: [SunOS] net_io_counters has send and received swapped -- 614_: [Linux]: cpu_count(logical=False) return the number of sockets instead - of cores. -- 618_: [SunOS] swap tests fail on Solaris when run as normal user -- 628_: [Linux] Process.name() truncates process name in case it contains - spaces or parentheses. +- 428_, [POSIX], **[critical]**: correct handling of zombie processes on POSIX. + Introduced new `ZombieProcess`_ exception class. +- 512_, [BSD], **[critical]**: fix segfault in `net_connections()`_. +- 555_, [Linux]: `users()`_ correctly handles ``":0"`` as an alias for + ``"localhost"``. +- 579_, [Windows]: fixed `Process.open_files()`_ for PID > 64K. +- 579_, [Windows]: fixed many compiler warnings. +- 585_, [FreeBSD]: `net_connections()`_ may raise ``KeyError``. +- 586_, [FreeBSD], **[critical]**: `Process.cpu_affinity()`_ segfaults on set + in case an invalid CPU number is provided. +- 593_, [FreeBSD], **[critical]**: `Process.memory_maps()`_ segfaults. +- 606_: `Process.parent()`_ may swallow `NoSuchProcess`_ exceptions. +- 611_, [SunOS]: `net_io_counters()`_ has send and received swapped +- 614_, [Linux]:: `cpu_count()`_ with ``logical=False`` return the number of + sockets instead of cores. +- 618_, [SunOS]: swap tests fail on Solaris when run as normal user. +- 628_, [Linux]: `Process.name()`_ truncates string in case it contains spaces + or parentheses. 2.2.1 ===== @@ -1364,8 +1377,8 @@ XXXX-XX-XX **Bug fixes** -- 496_: [Linux] fix "ValueError: ambiguos inode with multiple PIDs references" - (patch by Bruno Binet) +- 572_, [Linux]: fix "ValueError: ambiguos inode with multiple PIDs references" + for `Process.connections()`_. (patch by Bruno Binet) 2.2.0 ===== @@ -1375,34 +1388,35 @@ XXXX-XX-XX **Enhancements** - 521_: drop support for Python 2.4 and 2.5. -- 553_: new examples/pstree.py script. +- 553_: new `pstree.py`_ script. - 564_: C extension version mismatch in case the user messed up with psutil installation or with sys.path is now detected at import time. -- 568_: New examples/pidof.py script. -- 569_: [FreeBSD] add support for process CPU affinity. +- 568_: new `pidof.py`_ script. +- 569_, [FreeBSD]: add support for `Process.cpu_affinity`_ on FreeBSD. **Bug fixes** -- 496_: [Solaris] can't import psutil. -- 547_: [UNIX] Process.username() may raise KeyError if UID can't be resolved. -- 551_: [Windows] get rid of the unicode hack for net_io_counters() NIC names. -- 556_: [Linux] lots of file handles were left open. -- 561_: [Linux] net_connections() might skip some legitimate UNIX sockets. +- 496_, [SunOS], **[critical]**: can't import psutil. +- 547_, [POSIX]: `Process.username()`_ may raise ``KeyError`` if UID can't be resolved. +- 551_, [Windows]: get rid of the unicode hack for `net_io_counters()`_ NIC names. +- 556_, [Linux]: lots of file handles were left open. +- 561_, [Linux]: `net_connections()`_ might skip some legitimate UNIX sockets. (patch by spacewander) -- 565_: [Windows] use proper encoding for psutil.Process.username() and - psutil.users(). (patch by Sylvain Mouquet) -- 567_: [Linux] in the alternative implementation of CPU affinity PyList_Append - and Py_BuildValue return values are not checked. -- 569_: [FreeBSD] fix memory leak in psutil.cpu_count(logical=False). -- 571_: [Linux] Process.open_files() might swallow AccessDenied exceptions and - return an incomplete list of open files. +- 565_, [Windows]: use proper encoding for `Process.username()`_ and `users()`_. + (patch by Sylvain Mouquet) +- 567_, [Linux]: in the alternative implementation of `Process.cpu_affinity`_ + ``PyList_Append`` and ``Py_BuildValue`` return values are not checked. +- 569_, [FreeBSD]: fix memory leak in `cpu_count()`_ with ``logical=False``. +- 571_, [Linux]: `Process.open_files()`_ might swallow `AccessDenied`_ + exceptions and return an incomplete list of open files. 2.1.3 ===== *2014-09-26* -- 536_: [Linux]: fix "undefined symbol: CPU_ALLOC" compilation error. +- 536_, [Linux], **[critical]**: fix "undefined symbol: CPU_ALLOC" compilation + error. 2.1.2 ===== @@ -1413,26 +1427,27 @@ XXXX-XX-XX - 407_: project moved from Google Code to Github; code moved from Mercurial to Git. -- 492_: use tox to run tests on multiple python versions. (patch by msabramo) -- 505_: [Windows] distribution as wheel packages. -- 511_: new examples/ps.py sample code. +- 492_: use ``tox`` to run tests on multiple Python versions. (patch by msabramo) +- 505_, [Windows]: distribution as wheel packages. +- 511_: add `ps.py`_ script. **Bug fixes** -- 340_: [Windows] Process.get_open_files() no longer hangs. (patch by +- 340_, [Windows]: `Process.open_files()`_ no longer hangs. (patch by Jeff Tang) -- 501_: [Windows] disk_io_counters() may return negative values. -- 503_: [Linux] in rare conditions Process exe(), open_files() and - connections() methods can raise OSError(ESRCH) instead of NoSuchProcess. -- 504_: [Linux] can't build RPM packages via setup.py -- 506_: [Linux] python 2.4 support was broken. -- 522_: [Linux] Process.cpu_affinity() might return EINVAL. (patch by David +- 501_, [Windows]: `disk_io_counters()`_ may return negative values. +- 503_, [Linux]: in rare conditions `Process.exe()`_, `Process.open_files()`_ and + `Process.connections()`_ can raise ``OSError(ESRCH)`` instead of `NoSuchProcess`_. +- 504_, [Linux]: can't build RPM packages via setup.py +- 506_, [Linux], **[critical]**: Python 2.4 support was broken. +- 522_, [Linux]: `Process.cpu_affinity()`_ might return ``EINVAL``. (patch by David Daeschler) -- 529_: [Windows] Process.exe() may raise unhandled WindowsError exception +- 529_, [Windows]: `Process.exe()`_ may raise unhandled ``WindowsError`` exception for PIDs 0 and 4. (patch by Jeff Tang) -- 530_: [Linux] psutil.disk_io_counters() may crash on old Linux distros +- 530_, [Linux]: `disk_io_counters()`_ may crash on old Linux distros (< 2.6.5) (patch by Yaolong Huang) -- 533_: [Linux] Process.memory_maps() may raise TypeError on old Linux distros. +- 533_, [Linux]: `Process.memory_maps()`_ may raise ``TypeError`` on old Linux + distros. 2.1.1 ===== @@ -1441,10 +1456,10 @@ XXXX-XX-XX **Bug fixes** -- 446_: [Windows] fix encoding error when using net_io_counters() on Python 3. +- 446_, [Windows]: fix encoding error when using `net_io_counters()`_ on Python 3. (patch by Szigeti Gabor Niif) -- 460_: [Windows] net_io_counters() wraps after 4G. -- 491_: [Linux] psutil.net_connections() exceptions. (patch by Alexander Grothe) +- 460_, [Windows]: `net_io_counters()`_ wraps after 4G. +- 491_, [Linux]: `net_connections()`_ exceptions. (patch by Alexander Grothe) 2.1.0 ===== @@ -1453,13 +1468,13 @@ XXXX-XX-XX **Enhancements** -- 387_: system-wide open connections a-la netstat. +- 387_: system-wide open connections a-la ``netstat`` (add `net_connections()`_). **Bug fixes** -- 421_: [Solaris] psutil does not compile on SunOS 5.10 (patch by Naveed - Roudsari) -- 489_: [Linux] psutil.disk_partitions() return an empty list. +- 421_, [SunOS], **[critical]**: psutil does not compile on SunOS 5.10. + (patch by Naveed Roudsari) +- 489_, [Linux]: `disk_partitions()`_ return an empty list. 2.0.0 ===== @@ -1468,75 +1483,75 @@ XXXX-XX-XX **Enhancements** -- 424_: [Windows] installer for Python 3.X 64 bit. -- 427_: number of logical CPUs and physical cores (psutil.cpu_count()). -- 447_: psutil.wait_procs() timeout parameter is now optional. -- 452_: make Process instances hashable and usable with set()s. -- 453_: tests on Python < 2.7 require unittest2 module. -- 459_: add a make file for running tests and other repetitive tasks (also +- 424_, [Windows]: installer for Python 3.X 64 bit. +- 427_: number of logical CPUs and physical cores (`cpu_count()`_). +- 447_: `wait_procs()`_ ``timeout`` parameter is now optional. +- 452_: make `Process`_ instances hashable and usable with ``set()`` s. +- 453_: tests on Python < 2.7 require ``unittest2`` module. +- 459_: add a Makefile for running tests and other repetitive tasks (also on Windows). -- 463_: make timeout parameter of cpu_percent* functions default to 0.0 'cause - it's a common trap to introduce slowdowns. +- 463_: make timeout parameter of ``cpu_percent*`` functions default to ``0.0`` + 'cause it's a common trap to introduce slowdowns. - 468_: move documentation to readthedocs.com. -- 477_: process cpu_percent() is about 30% faster. (suggested by crusaderky) -- 478_: [Linux] almost all APIs are about 30% faster on Python 3.X. -- 479_: long deprecated psutil.error module is gone; exception classes now - live in "psutil" namespace only. +- 477_: `Process.cpu_percent()`_ is about 30% faster. (suggested by crusaderky) +- 478_, [Linux]: almost all APIs are about 30% faster on Python 3.X. +- 479_: long deprecated ``psutil.error`` module is gone; exception classes now + live in psutil namespace only. **Bug fixes** -- 193_: psutil.Popen constructor can throw an exception if the spawned process +- 193_: `psutil.Popen`_ constructor can throw an exception if the spawned process terminates quickly. -- 340_: [Windows] process get_open_files() no longer hangs. (patch by +- 340_, [Windows]: `Process.open_files()`_ no longer hangs. (patch by jtang@vahna.net) -- 443_: [Linux] fix a potential overflow issue for Process.set_cpu_affinity() - on systems with more than 64 CPUs. -- 448_: [Windows] get_children() and ppid() memory leak (patch by Ulrich - Klank). -- 457_: [POSIX] pid_exists() always returns True for PID 0. +- 443_, [Linux]: fix a potential overflow issue for `Process.cpu_affinity()`_ + (set) on systems with more than 64 CPUs. +- 448_, [Windows]: `Process.children()`_ and `Process.ppid()`_ memory leak (patch + by Ulrich Klank). +- 457_, [POSIX]: `pid_exists()`_ always returns ``True`` for PID 0. - 461_: namedtuples are not pickle-able. -- 466_: [Linux] process exe improper null bytes handling. (patch by +- 466_, [Linux]: `Process.exe()`_ improper null bytes handling. (patch by Gautam Singh) -- 470_: wait_procs() might not wait. (patch by crusaderky) -- 471_: [Windows] process exe improper unicode handling. (patch by +- 470_: `wait_procs()`_ might not wait. (patch by crusaderky) +- 471_, [Windows]: `Process.exe()`_ improper unicode handling. (patch by alex@mroja.net) -- 473_: psutil.Popen.wait() does not set returncode attribute. -- 474_: [Windows] Process.cpu_percent() is no longer capped at 100%. -- 476_: [Linux] encoding error for process name and cmdline. +- 473_: `psutil.Popen`_ ``wait()`` method does not set returncode attribute. +- 474_, [Windows]: `Process.cpu_percent()`_ is no longer capped at 100%. +- 476_, [Linux]: encoding error for `Process.name()`_ and `Process.cmdline()`_. **API changes** For the sake of consistency a lot of psutil APIs have been renamed. In most cases accessing the old names will work but it will cause a -DeprecationWarning. - -- psutil.* module level constants have being replaced by functions: - - +-----------------------+-------------------------------+ - | Old name | Replacement | - +=======================+===============================+ - | psutil.NUM_CPUS | psutil.cpu_cpunt() | - +-----------------------+-------------------------------+ - | psutil.BOOT_TIME | psutil.boot_time() | - +-----------------------+-------------------------------+ - | psutil.TOTAL_PHYMEM | psutil.virtual_memory().total | - +-----------------------+-------------------------------+ - -- Renamed psutil.* functions: - - +--------------------------+-------------------------------+ - | Old name | Replacement | - +==========================+===============================+ - | - psutil.get_pid_list() | psutil.pids() | - +--------------------------+-------------------------------+ - | - psutil.get_users() | psutil.users() | - +--------------------------+-------------------------------+ - | - psutil.get_boot_time() | psutil.boot_time() | - +--------------------------+-------------------------------+ - -- All psutil.Process ``get_*`` methods lost the ``get_`` prefix. - get_ext_memory_info() renamed to memory_info_ex(). - Assuming "p = psutil.Process()": +``DeprecationWarning``. + +- ``psutil.*`` module level constants have being replaced by functions: + + +-----------------------+----------------------------------+ + | Old name | Replacement | + +=======================+==================================+ + | psutil.NUM_CPUS | psutil.cpu_count() | + +-----------------------+----------------------------------+ + | psutil.BOOT_TIME | psutil.boot_time() | + +-----------------------+----------------------------------+ + | psutil.TOTAL_PHYMEM | virtual_memory.total | + +-----------------------+----------------------------------+ + +- Renamed ``psutil.*`` functions: + + +------------------------+-------------------------------+ + | Old name | Replacement | + +========================+===============================+ + | psutil.get_pid_list() | psutil.pids() | + +------------------------+-------------------------------+ + | psutil.get_users() | psutil.users() | + +------------------------+-------------------------------+ + | psutil.get_boot_time() | psutil.boot_time() | + +------------------------+-------------------------------+ + +- All `Process`_ ``get_*`` methods lost the ``get_`` prefix. + E.g. ``get_ext_memory_info()`` was renamed to ``memory_info_ex()``. + Assuming ``p = psutil.Process()``: +--------------------------+----------------------+ | Old name | Replacement | @@ -1580,8 +1595,8 @@ DeprecationWarning. | p.getcwd() | p.cwd() | +--------------------------+----------------------+ -- All psutil.Process ``set_*`` methods lost the ``set_`` prefix. - Assuming "p = psutil.Process()": +- All `Process`_ ``set_*`` methods lost the ``set_`` prefix. + Assuming ``p = psutil.Process()``: +----------------------+---------------------------------+ | Old name | Replacement | @@ -1595,9 +1610,9 @@ DeprecationWarning. | p.set_rlimit() | p.rlimit(resource, limits=None) | +----------------------+---------------------------------+ -- Except for 'pid' all psutil.Process class properties have been turned into +- Except for ``pid``, all `Process`_ class properties have been turned into methods. This is the only case which there are no aliases. - Assuming "p = psutil.Process()": + Assuming ``p = psutil.Process()``: +---------------+-----------------+ | Old name | Replacement | @@ -1623,11 +1638,11 @@ DeprecationWarning. | p.create_time | p.create_time() | +---------------+-----------------+ -- timeout parameter of cpu_percent* functions defaults to 0.0 instead of 0.1. -- long deprecated psutil.error module is gone; exception classes now live in +- timeout parameter of ``cpu_percent*`` functions defaults to 0.0 instead of 0.1. +- long deprecated ``psutil.error`` module is gone; exception classes now live in "psutil" namespace only. -- Process instances' "retcode" attribute returned by psutil.wait_procs() has - been renamed to "returncode" for consistency with subprocess.Popen. +- `Process`_ instances' ``retcode`` attribute returned by `wait_procs()`_ has + been renamed to ``returncode`` for consistency with ``subprocess.Popen``. 1.2.1 ===== @@ -1636,10 +1651,12 @@ DeprecationWarning. **Bug fixes** -- 348_: [Windows XP] fixed "ImportError: DLL load failed" occurring on module - import. -- 425_: [Solaris] crash on import due to failure at determining BOOT_TIME. -- 443_: [Linux] can't set CPU affinity on systems with more than 64 cores. +- 348_, [Windows], **[critical]**: fixed "ImportError: DLL load failed" occurring + on module import on Windows XP. +- 425_, [SunOS], **[critical]**: crash on import due to failure at determining + ``BOOT_TIME``. +- 443_, [Linux]: `Process.cpu_affinity()`_ can't set affinity on systems with + more than 64 cores. 1.2.0 ===== @@ -1648,15 +1665,15 @@ DeprecationWarning. **Enhancements** -- 439_: assume os.getpid() if no argument is passed to psutil.Process +- 439_: assume ``os.getpid()`` if no argument is passed to `Process`_ class constructor. -- 440_: new psutil.wait_procs() utility function which waits for multiple +- 440_: new `wait_procs()`_ utility function which waits for multiple processes to terminate. **Bug fixes** -- 348_: [Windows XP/Vista] fix "ImportError: DLL load failed" occurring on - module import. +- 348_, [Windows]: fix "ImportError: DLL load failed" occurring on module + import on Windows XP / Vista. 1.1.3 ===== @@ -1665,8 +1682,8 @@ DeprecationWarning. **Bug fixes** -- 442_: [Linux] psutil won't compile on certain version of Linux because of - missing prlimit(2) syscall. +- 442_, [Linux], **[critical]**: psutil won't compile on certain version of + Linux because of missing ``prlimit(2)`` syscall. 1.1.2 ===== @@ -1675,8 +1692,8 @@ DeprecationWarning. **Bug fixes** -- 442_: [Linux] psutil won't compile on Debian 6.0 because of missing - prlimit(2) syscall. +- 442_, [Linux], **[critical]**: psutil won't compile on Debian 6.0 because of + missing ``prlimit(2)`` syscall. 1.1.1 ===== @@ -1685,8 +1702,8 @@ DeprecationWarning. **Bug fixes** -- 442_: [Linux] psutil won't compile on kernels < 2.6.36 due to missing - prlimit(2) syscall. +- 442_, [Linux], **[critical]**: psutil won't compile on kernels < 2.6.36 due + to missing ``prlimit(2)`` syscall. 1.1.0 ===== @@ -1695,28 +1712,28 @@ DeprecationWarning. **Enhancements** -- 410_: host tar.gz and windows binary files are on PyPI. -- 412_: [Linux] get/set process resource limits. -- 415_: [Windows] Process.get_children() is an order of magnitude faster. -- 426_: [Windows] Process.name is an order of magnitude faster. -- 431_: [UNIX] Process.name is slightly faster because it unnecessarily - retrieved also process cmdline. +- 410_: host tar.gz and Windows binary files are on PyPI. +- 412_, [Linux]: get/set process resource limits (`Process.rlimit()`_). +- 415_, [Windows]: `Process.children()`_ is an order of magnitude faster. +- 426_, [Windows]: `Process.name()`_ is an order of magnitude faster. +- 431_, [POSIX]: `Process.name()`_ is slightly faster because it unnecessarily + retrieved also `Process.cmdline()`_. **Bug fixes** -- 391_: [Windows] psutil.cpu_times_percent() returns negative percentages. -- 408_: STATUS_* and CONN_* constants don't properly serialize on JSON. -- 411_: [Windows] examples/disk_usage.py may pop-up a GUI error. -- 413_: [Windows] Process.get_memory_info() leaks memory. -- 414_: [Windows] Process.exe on Windows XP may raise ERROR_INVALID_PARAMETER. -- 416_: psutil.disk_usage() doesn't work well with unicode path names. -- 430_: [Linux] process IO counters report wrong number of r/w syscalls. -- 435_: [Linux] psutil.net_io_counters() might report erreneous NIC names. -- 436_: [Linux] psutil.net_io_counters() reports a wrong 'dropin' value. +- 391_, [Windows]: `cpu_times_percent()`_ returns negative percentages. +- 408_: ``STATUS_*`` and ``CONN_*`` constants don't properly serialize on JSON. +- 411_, [Windows]: `disk_usage.py`_ may pop-up a GUI error. +- 413_, [Windows]: `Process.memory_info()`_ leaks memory. +- 414_, [Windows]: `Process.exe()`_ on Windows XP may raise ``ERROR_INVALID_PARAMETER``. +- 416_: `disk_usage()`_ doesn't work well with unicode path names. +- 430_, [Linux]: `Process.io_counters()`_ report wrong number of r/w syscalls. +- 435_, [Linux]: `net_io_counters()`_ might report erreneous NIC names. +- 436_, [Linux]: `net_io_counters()`_ reports a wrong ``dropin`` value. **API changes** -- 408_: turn STATUS_* and CONN_* constants into plain Python strings. +- 408_: turn ``STATUS_*`` and ``CONN_*`` constants into plain Python strings. 1.0.1 ===== @@ -1725,7 +1742,7 @@ DeprecationWarning. **Bug fixes** -- 405_: network_io_counters(pernic=True) no longer works as intended in 1.0.0. +- 405_: `net_io_counters()`_ ``pernic=True`` no longer works as intended in 1.0.0. 1.0.0 ===== @@ -1734,27 +1751,28 @@ DeprecationWarning. **Enhancements** -- 18_: Solaris support (yay!) (thanks Justin Venus) -- 367_: Process.get_connections() 'status' strings are now constants. +- 18_, [SunOS]: add Solaris support (yay!) (thanks Justin Venus) +- 367_: `Process.connections()`_ ``status`` strings are now constants. - 380_: test suite exits with non-zero on failure. (patch by floppymaster) - 391_: introduce unittest2 facilities and provide workarounds if unittest2 - is not installed (python < 2.7). + is not installed (Python < 2.7). **Bug fixes** -- 374_: [Windows] negative memory usage reported if process uses a lot of +- 374_, [Windows]: negative memory usage reported if process uses a lot of memory. -- 379_: [Linux] Process.get_memory_maps() may raise ValueError. -- 394_: [macOS] Mapped memory regions report incorrect file name. -- 404_: [Linux] sched_*affinity() are implicitly declared. (patch by Arfrever) +- 379_, [Linux]: `Process.memory_maps()`_ may raise ``ValueError``. +- 394_, [macOS]: mapped memory regions of `Process.memory_maps()`_ report + incorrect file name. +- 404_, [Linux]: ``sched_*affinity()`` are implicitly declared. (patch by Arfrever) **API changes** -- Process.get_connections() 'status' field is no longer a string but a - constant object (psutil.CONN_*). -- Process.get_connections() 'local_address' and 'remote_address' fields - renamed to 'laddr' and 'raddr'. -- psutil.network_io_counters() renamed to psutil.net_io_counters(). +- `Process.connections()`_ ``status`` field is no longer a string but a + constant object (``psutil.CONN_*``). +- `Process.connections()`_ ``local_address`` and ``remote_address`` fields + renamed to ``laddr`` and ``raddr``. +- psutil.network_io_counters() renamed to `net_io_counters()`_. 0.7.1 ===== @@ -1763,11 +1781,11 @@ DeprecationWarning. **Bug fixes** -- 325_: [BSD] psutil.virtual_memory() can raise SystemError. +- 325_, [BSD], **[critical]**: `virtual_memory()`_ can raise ``SystemError``. (patch by Jan Beich) -- 370_: [BSD] Process.get_connections() requires root. (patch by John Baldwin) -- 372_: [BSD] different process methods raise NoSuchProcess instead of - AccessDenied. +- 370_, [BSD]: `Process.connections()`_ requires root. (patch by John Baldwin) +- 372_, [BSD]: different process methods raise `NoSuchProcess`_ instead of + `AccessDenied`_. 0.7.0 ===== @@ -1778,59 +1796,63 @@ DeprecationWarning. - 233_: code migrated to Mercurial (yay!) - 246_: psutil.error module is deprecated and scheduled for removal. -- 328_: [Windows] process IO nice/priority support. -- 359_: psutil.get_boot_time() -- 361_: [Linux] psutil.cpu_times() now includes new 'steal', 'guest' and - 'guest_nice' fields available on recent Linux kernels. - Also, psutil.cpu_percent() is more accurate. -- 362_: cpu_times_percent() (per-CPU-time utilization as a percentage) +- 328_, [Windows]: `Process.ionice()`_ support. +- 359_: add `boot_time()`_ as a substitute of ``psutil.BOOT_TIME`` since the + latter cannot reflect system clock updates. +- 361_, [Linux]: `cpu_times()`_ now includes new ``steal``, ``guest`` and + ``guest_nice`` fields available on recent Linux kernels. Also, `cpu_percent()`_ + is more accurate. +- 362_: add `cpu_times_percent()`_ (per-CPU-time utilization as a percentage). **Bug fixes** -- 234_: [Windows] disk_io_counters() fails to list certain disks. -- 264_: [Windows] use of psutil.disk_partitions() may cause a message box to +- 234_, [Windows]: `disk_io_counters()`_ fails to list certain disks. +- 264_, [Windows]: use of `disk_partitions()`_ may cause a message box to appear. -- 313_: [Linux] psutil.virtual_memory() and psutil.swap_memory() can crash on - certain exotic Linux flavors having an incomplete /proc interface. - If that's the case we now set the unretrievable stats to 0 and raise a - RuntimeWarning. -- 315_: [macOS] fix some compilation warnings. -- 317_: [Windows] cannot set process CPU affinity above 31 cores. -- 319_: [Linux] process get_memory_maps() raises KeyError 'Anonymous' on Debian +- 313_, [Linux], **[critical]**: `virtual_memory()`_ and `swap_memory()`_ can + crash on certain exotic Linux flavors having an incomplete ``/proc`` interface. + If that's the case we now set the unretrievable stats to ``0`` and raise + ``RuntimeWarning`` instead. +- 315_, [macOS]: fix some compilation warnings. +- 317_, [Windows]: cannot set process CPU affinity above 31 cores. +- 319_, [Linux]: `Process.memory_maps()`_ raises ``KeyError`` 'Anonymous' on Debian squeeze. -- 321_: [UNIX] Process.ppid property is no longer cached as the kernel may set - the ppid to 1 in case of a zombie process. -- 323_: [macOS] disk_io_counters()'s read_time and write_time parameters were - reporting microseconds not milliseconds. (patch by Gregory Szorc) -- 331_: Process cmdline is no longer cached after first acces as it may change. -- 333_: [macOS] Leak of Mach ports on macOS (patch by rsesek@google.com) -- 337_: [Linux] process methods not working because of a poor /proc - implementation will raise NotImplementedError rather than RuntimeError - and Process.as_dict() will not blow up. (patch by Curtin1060) -- 338_: [Linux] disk_io_counters() fails to find some disks. -- 339_: [FreeBSD] get_pid_list() can allocate all the memory on system. -- 341_: [Linux] psutil might crash on import due to error in retrieving system - terminals map. -- 344_: [FreeBSD] swap_memory() might return incorrect results due to - kvm_open(3) not being called. (patch by Jean Sebastien) -- 338_: [Linux] disk_io_counters() fails to find some disks. -- 351_: [Windows] if psutil is compiled with mingw32 (provided installers for - py2.4 and py2.5 are) disk_io_counters() will fail. (Patch by m.malycha) -- 353_: [macOS] get_users() returns an empty list on macOS 10.8. -- 356_: Process.parent now checks whether parent PID has been reused in which - case returns None. -- 365_: Process.set_nice() should check PID has not been reused by another +- 321_, [POSIX]: `Process.ppid()`_ property is no longer cached as the kernel may set + the PPID to 1 in case of a zombie process. +- 323_, [macOS]: `disk_io_counters()`_ ``read_time`` and ``write_time`` + parameters were reporting microseconds not milliseconds. (patch by Gregory Szorc) +- 331_: `Process.cmdline()`_ is no longer cached after first acces as it may + change. +- 333_, [macOS]: leak of Mach ports (patch by rsesek@google.com) +- 337_, [Linux], **[critical]**: `Process`_ methods not working because of a + poor ``/proc`` implementation will raise ``NotImplementedError`` rather than + ``RuntimeError`` and `Process.as_dict()`_ will not blow up. + (patch by Curtin1060) +- 338_, [Linux]: `disk_io_counters()`_ fails to find some disks. +- 339_, [FreeBSD]: ``get_pid_list()`` can allocate all the memory on system. +- 341_, [Linux], **[critical]**: psutil might crash on import due to error in + retrieving system terminals map. +- 344_, [FreeBSD]: `swap_memory()`_ might return incorrect results due to + ``kvm_open(3)`` not being called. (patch by Jean Sebastien) +- 338_, [Linux]: `disk_io_counters()`_ fails to find some disks. +- 351_, [Windows]: if psutil is compiled with MinGW32 (provided installers for + py2.4 and py2.5 are) `disk_io_counters()`_ will fail. (Patch by m.malycha) +- 353_, [macOS]: `users()`_ returns an empty list on macOS 10.8. +- 356_: `Process.parent()`_ now checks whether parent PID has been reused in which + case returns ``None``. +- 365_: `Process.nice()`_ (set) should check PID has not been reused by another process. -- 366_: [FreeBSD] get_memory_maps(), get_num_fds(), get_open_files() and - getcwd() Process methods raise RuntimeError instead of AccessDenied. +- 366_, [FreeBSD], **[critical]**: `Process.memory_maps()`_, `Process.num_fds()`_, + `Process.open_files()`_ and `Process.cwd()`_ methods raise ``RuntimeError`` + instead of `AccessDenied`_. **API changes** -- Process.cmdline property is no longer cached after first access. -- Process.ppid property is no longer cached after first access. -- [Linux] Process methods not working because of a poor /proc implementation - will raise NotImplementedError instead of RuntimeError. -- psutil.error module is deprecated and scheduled for removal. +- `Process.cmdline()`_ property is no longer cached after first access. +- `Process.ppid()`_ property is no longer cached after first access. +- [Linux] `Process`_ methods not working because of a poor ``/proc`` + implementation will raise ``NotImplementedError`` instead of ``RuntimeError``. +- ``psutil.error`` module is deprecated and scheduled for removal. 0.6.1 ===== @@ -1839,18 +1861,18 @@ DeprecationWarning. **Enhancements** -- 316_: process cmdline property now makes a better job at guessing the process - executable from the cmdline. +- 316_: `Process.cmdline()`_ property now makes a better job at guessing the + process executable from the cmdline. **Bug fixes** -- 316_: process exe was resolved in case it was a symlink. -- 318_: python 2.4 compatibility was broken. +- 316_: `Process.exe()`_ was resolved in case it was a symlink. +- 318_, **[critical]**: Python 2.4 compatibility was broken. **API changes** -- process exe can now return an empty string instead of raising AccessDenied. -- process exe is no longer resolved in case it's a symlink. +- `Process.exe()`_ can now return an empty string instead of raising `AccessDenied`_. +- `Process.exe()`_ is no longer resolved in case it's a symlink. 0.6.0 ===== @@ -1859,87 +1881,62 @@ DeprecationWarning. **Enhancements** -- 216_: [POSIX] get_connections() UNIX sockets support. -- 220_: [FreeBSD] get_connections() has been rewritten in C and no longer - requires lsof. -- 222_: [macOS] add support for process cwd. -- 261_: process extended memory info. -- 295_: [macOS] process executable path is now determined by asking the OS - instead of being guessed from process cmdline. -- 297_: [macOS] the Process methods below were always raising AccessDenied for - any process except the current one. Now this is no longer true. Also - they are 2.5x faster. - - name - - get_memory_info() - - get_memory_percent() - - get_cpu_times() - - get_cpu_percent() - - get_num_threads() -- 300_: examples/pmap.py script. -- 301_: process_iter() now yields processes sorted by their PIDs. -- 302_: process number of voluntary and involuntary context switches. -- 303_: [Windows] the Process methods below were always raising AccessDenied +- 216_, [POSIX]: `Process.connections()`_ UNIX sockets support. +- 220_, [FreeBSD]: ``get_connections()`` has been rewritten in C and no longer + requires ``lsof``. +- 222_, [macOS]: add support for `Process.cwd()`_. +- 261_: per-process extended memory info (`Process.memory_info_ex()`_). +- 295_, [macOS]: `Process.exe()`_ path is now determined by asking the OS + instead of being guessed from `Process.cmdline()`_. +- 297_, [macOS]: the `Process`_ methods below were always raising `AccessDenied`_ + for any process except the current one. Now this is no longer true. Also + they are 2.5x faster. `Process.name()`_, `Process.memory_info()`_, + `Process.memory_percent()`_, `Process.cpu_times()`_, `Process.cpu_percent()`_, + `Process.num_threads()`_. +- 300_: add `pmap.py`_ script. +- 301_: `process_iter()`_ now yields processes sorted by their PIDs. +- 302_: per-process number of voluntary and involuntary context switches + (`Process.num_ctx_switches()`_). +- 303_, [Windows]: the `Process`_ methods below were always raising `AccessDenied`_ for any process not owned by current user. Now this is no longer true: - - create_time - - get_cpu_times() - - get_cpu_percent() - - get_memory_info() - - get_memory_percent() - - get_num_handles() - - get_io_counters() -- 305_: add examples/netstat.py script. + `Process.create_time()`_, `Process.cpu_times()`_, `Process.cpu_percent()`_, + `Process.memory_info()`_, `Process.memory_percent()`_, `Process.num_handles()`_, + `Process.io_counters()`_. +- 305_: add `netstat.py`_ script. - 311_: system memory functions has been refactorized and rewritten and now provide a more detailed and consistent representation of the system - memory. New psutil.virtual_memory() function provides the following - memory amounts: - - total - - available - - percent - - used - - active [POSIX] - - inactive [POSIX] - - buffers (BSD, Linux) - - cached (BSD, macOS) - - wired (macOS, BSD) - - shared [FreeBSD] - New psutil.swap_memory() provides: - - total - - used - - free - - percent - - sin (no. of bytes the system has swapped in from disk (cumulative)) - - sout (no. of bytes the system has swapped out from disk (cumulative)) - All old memory-related functions are deprecated. - Also two new example scripts were added: free.py and meminfo.py. -- 312_: psutil.network_io_counters() namedtuple includes 4 new fields: - errin, errout dropin and dropout, reflecting the number of packets - dropped and with errors. + memory. Added new `virtual_memory()`_ and `swap_memory()`_ functions. + All old memory-related functions are deprecated. Also two new example scripts + were added: `free.py`_ and `meminfo.py`_. +- 312_: ``net_io_counters()`` namedtuple includes 4 new fields: + ``errin``, ``errout``, ``dropin`` and ``dropout``, reflecting the number of + packets dropped and with errors. **Bug fixes** -- 298_: [macOS and BSD] memory leak in get_num_fds(). -- 299_: potential memory leak every time PyList_New(0) is used. -- 303_: [Windows] potential heap corruption in get_num_threads() and - get_status() Process methods. -- 305_: [FreeBSD] psutil can't compile on FreeBSD 9 due to removal of utmp.h. -- 306_: at C level, errors are not checked when invoking Py* functions which - create or manipulate Python objects leading to potential memory related - errors and/or segmentation faults. -- 307_: [FreeBSD] values returned by psutil.network_io_counters() are wrong. -- 308_: [BSD / Windows] psutil.virtmem_usage() wasn't actually returning - information about swap memory usage as it was supposed to do. It does - now. -- 309_: get_open_files() might not return files which can not be accessed - due to limited permissions. AccessDenied is now raised instead. +- 298_, [macOS], [BSD]: memory leak in `Process.num_fds()`_. +- 299_: potential memory leak every time ``PyList_New(0)`` is used. +- 303_, [Windows], **[critical]**: potential heap corruption in + `Process.num_threads()`_ and `Process.status()`_ methods. +- 305_, [FreeBSD], **[critical]**: can't compile on FreeBSD 9 due to removal of + ``utmp.h``. +- 306_, **[critical]**: at C level, errors are not checked when invoking ``Py*`` + functions which create or manipulate Python objects leading to potential + memory related errors and/or segmentation faults. +- 307_, [FreeBSD]: values returned by `net_io_counters()`_ are wrong. +- 308_, [BSD], [Windows]: ``psutil.virtmem_usage()`` wasn't actually returning + information about swap memory usage as it was supposed to do. It does now. +- 309_: `Process.open_files()`_ might not return files which can not be accessed + due to limited permissions. `AccessDenied`_ is now raised instead. **API changes** -- psutil.phymem_usage() is deprecated (use psutil.virtual_memory()) -- psutil.virtmem_usage() is deprecated (use psutil.swap_memory()) -- psutil.phymem_buffers() on Linux is deprecated (use psutil.virtual_memory()) -- psutil.cached_phymem() on Linux is deprecated (use psutil.virtual_memory()) -- [Windows and BSD] psutil.virtmem_usage() now returns information about swap - memory instead of virtual memory. +- ``psutil.phymem_usage()`` is deprecated (use `virtual_memory()`_) +- ``psutil.virtmem_usage()`` is deprecated (use `swap_memory()`_) +- [Linux]: ``psutil.phymem_buffers()`` is deprecated (use `virtual_memory()`_) +- [Linux]: ``psutil.cached_phymem()`` is deprecated (use `virtual_memory()`_) +- [Windows], [BSD]: ``psutil.virtmem_usage()`` now returns information about + swap memory instead of virtual memory. 0.5.1 ===== @@ -1948,13 +1945,14 @@ DeprecationWarning. **Enhancements** -- 293_: [Windows] process executable path is now determined by asking the OS - instead of being guessed from process cmdline. +- 293_, [Windows]: `Process.exe()`_ path is now determined by asking the OS + instead of being guessed from `Process.cmdline()`_. **Bug fixes** -- 292_: [Linux] race condition in process files/threads/connections. -- 294_: [Windows] Process CPU affinity is only able to set CPU #0. +- 292_, [Linux]: race condition in process `Process.open_files()`_, + `Process.connections()`_, `Process.threads()`_. +- 294_, [Windows]: `Process.cpu_affinity()`_ is only able to set CPU #0. 0.5.0 ===== @@ -1963,67 +1961,69 @@ DeprecationWarning. **Enhancements** -- 195_: [Windows] number of handles opened by process. -- 209_: psutil.disk_partitions() now provides also mount options. -- 229_: list users currently connected on the system (psutil.get_users()). -- 238_: [Linux, Windows] process CPU affinity (get and set). -- 242_: Process.get_children(recursive=True): return all process +- 195_, [Windows]: number of handles opened by process (`Process.num_handles()`_). +- 209_: `disk_partitions()`_ now provides also mount options. +- 229_: list users currently connected on the system (`users()`_). +- 238_, [Linux], [Windows]: process CPU affinity (get and set, + `Process.cpu_affinity()`_). +- 242_: add ``recursive=True`` to `Process.children()`_: return all process descendants. -- 245_: [POSIX] Process.wait() incrementally consumes less CPU cycles. -- 257_: [Windows] removed Windows 2000 support. -- 258_: [Linux] Process.get_memory_info() is now 0.5x faster. +- 245_, [POSIX]: `Process.wait()`_ incrementally consumes less CPU cycles. +- 257_, [Windows]: removed Windows 2000 support. +- 258_, [Linux]: `Process.memory_info()`_ is now 0.5x faster. - 260_: process's mapped memory regions. (Windows patch by wj32.64, macOS patch by Jeremy Whitlock) -- 262_: [Windows] psutil.disk_partitions() was slow due to inspecting the - floppy disk drive also when "all" argument was False. -- 273_: psutil.get_process_list() is deprecated. -- 274_: psutil no longer requires 2to3 at installation time in order to work +- 262_, [Windows]: `disk_partitions()`_ was slow due to inspecting the + floppy disk drive also when parameter is ``all=False``. +- 273_: ``psutil.get_process_list()`` is deprecated. +- 274_: psutil no longer requires ``2to3`` at installation time in order to work with Python 3. -- 278_: new Process.as_dict() method. -- 281_: ppid, name, exe, cmdline and create_time properties of Process class +- 278_: new `Process.as_dict()`_ method. +- 281_: `Process.ppid()`_, `Process.name()`_, `Process.exe()`_, + `Process.cmdline()`_ and `Process.create_time()`_ properties of `Process`_ class are now cached after being accessed. -- 282_: psutil.STATUS_* constants can now be compared by using their string +- 282_: ``psutil.STATUS_*`` constants can now be compared by using their string representation. -- 283_: speedup Process.is_running() by caching its return value in case the +- 283_: speedup `Process.is_running()`_ by caching its return value in case the process is terminated. -- 284_: [POSIX] per-process number of opened file descriptors. -- 287_: psutil.process_iter() now caches Process instances between calls. -- 290_: Process.nice property is deprecated in favor of new get_nice() and - set_nice() methods. +- 284_, [POSIX]: per-process number of opened file descriptors (`Process.num_fds`_). +- 287_: `process_iter()`_ now caches `Process`_ instances between calls. +- 290_: `Process.nice()`_ property is deprecated in favor of new ``get_nice()`` + and ``set_nice()`` methods. **Bug fixes** -- 193_: psutil.Popen constructor can throw an exception if the spawned process +- 193_: `psutil.Popen`_ constructor can throw an exception if the spawned process terminates quickly. -- 240_: [macOS] incorrect use of free() for Process.get_connections(). -- 244_: [POSIX] Process.wait() can hog CPU resources if called against a +- 240_, [macOS]: incorrect use of ``free()`` for `Process.connections()`_. +- 244_, [POSIX]: `Process.wait()`_ can hog CPU resources if called against a process which is not our children. -- 248_: [Linux] psutil.network_io_counters() might return erroneous NIC names. -- 252_: [Windows] process getcwd() erroneously raise NoSuchProcess for - processes owned by another user. It now raises AccessDenied instead. -- 266_: [Windows] psutil.get_pid_list() only shows 1024 processes. +- 248_, [Linux]: `net_io_counters()`_ might return erroneous NIC names. +- 252_, [Windows]: `Process.cwd()`_ erroneously raise `NoSuchProcess`_ for + processes owned by another user. It now raises `AccessDenied`_ instead. +- 266_, [Windows]: ``psutil.get_pid_list()`` only shows 1024 processes. (patch by Amoser) -- 267_: [macOS] Process.get_connections() - an erroneous remote address was - returned. (Patch by Amoser) -- 272_: [Linux] Porcess.get_open_files() - potential race condition can lead to - unexpected NoSuchProcess exception. Also, we can get incorrect reports +- 267_, [macOS]: `Process.connections()`_ returns wrong remote address. + (Patch by Amoser) +- 272_, [Linux]: `Process.open_files()`_ potential race condition can lead to + unexpected `NoSuchProcess`_ exception. Also, we can get incorrect reports of not absolutized path names. -- 275_: [Linux] Process.get_io_counters() erroneously raise NoSuchProcess on - old Linux versions. Where not available it now raises - NotImplementedError. -- 286_: Process.is_running() doesn't actually check whether PID has been +- 275_, [Linux]: ``Process.io_counters()`` erroneously raise `NoSuchProcess`_ on + old Linux versions. Where not available it now raises ``NotImplementedError``. +- 286_: `Process.is_running()`_ doesn't actually check whether PID has been reused. -- 314_: Process.get_children() can sometimes return non-children. +- 314_: `Process.children()`_ can sometimes return non-children. **API changes** -- Process.nice property is deprecated in favor of new get_nice() and set_nice() - methods. -- psutil.get_process_list() is deprecated. -- ppid, name, exe, cmdline and create_time properties of Process class are now - cached after being accessed, meaning NoSuchProcess will no longer be raised - in case the process is gone in the meantime. -- psutil.STATUS_* constants can now be compared by using their string +- ``Process.nice`` property is deprecated in favor of new ``get_nice()`` and + ``set_nice()`` methods. +- ``psutil.get_process_list()`` is deprecated. +- `Process.ppid()`_, `Process.name()`_, `Process.exe()`_, `Process.cmdline()`_ + and `Process.create_time()`_ properties of `Process`_ class are now cached after + being accessed, meaning `NoSuchProcess`_ will no longer be raised in case the + process is gone in the meantime. +- ``psutil.STATUS_*`` constants can now be compared by using their string representation. 0.4.1 @@ -2033,12 +2033,12 @@ DeprecationWarning. **Bug fixes** -- 228_: some example scripts were not working with python 3. -- 230_: [Windows / macOS] memory leak in Process.get_connections(). -- 232_: [Linux] psutil.phymem_usage() can report erroneous values which are - different than "free" command. -- 236_: [Windows] memory/handle leak in Process's get_memory_info(), - suspend() and resume() methods. +- 228_: some example scripts were not working with Python 3. +- 230_, [Windows], [macOS]: fix memory leak in `Process.connections()`_. +- 232_, [Linux]: ``psutil.phymem_usage()`` can report erroneous values which are + different than ``free`` command. +- 236_, [Windows]: fix memory/handle leak in `Process.memory_info()`_, + `Process.suspend()`_ and `Process.resume()`_ methods. 0.4.0 ===== @@ -2047,38 +2047,40 @@ DeprecationWarning. **Enhancements** -- 150_: network I/O counters. (macOS and Windows patch by Jeremy Whitlock) -- 154_: [FreeBSD] add support for process getcwd() -- 157_: [Windows] provide installer for Python 3.2 64-bit. -- 198_: Process.wait(timeout=0) can now be used to make wait() return - immediately. -- 206_: disk I/O counters. (macOS and Windows patch by Jeremy Whitlock) -- 213_: examples/iotop.py script. -- 217_: Process.get_connections() now has a "kind" argument to filter +- 150_: network I/O counters (`net_io_counters()`_). (macOS and Windows patch + by Jeremy Whitlock) +- 154_, [FreeBSD]: add support for `Process.cwd()`_. +- 157_, [Windows]: provide installer for Python 3.2 64-bit. +- 198_: `Process.wait()`_ with ``timeout=0`` can now be used to make the + function return immediately. +- 206_: disk I/O counters (`disk_io_counters()`_). (macOS and Windows patch by + Jeremy Whitlock) +- 213_: add `iotop.py`_ script. +- 217_: `Process.connections()`_ now has a ``kind`` argument to filter for connections with different criteria. -- 221_: [FreeBSD] Process.get_open_files has been rewritten in C and no longer - relies on lsof. -- 223_: examples/top.py script. -- 227_: examples/nettop.py script. +- 221_, [FreeBSD]: `Process.open_files()`_ has been rewritten in C and no longer + relies on ``lsof``. +- 223_: add `top.py`_ script. +- 227_: add `nettop.py`_ script. **Bug fixes** -- 135_: [macOS] psutil cannot create Process object. -- 144_: [Linux] no longer support 0 special PID. -- 188_: [Linux] psutil import error on Linux ARM architectures. -- 194_: [POSIX] psutil.Process.get_cpu_percent() now reports a percentage over +- 135_, [macOS]: psutil cannot create `Process`_ object. +- 144_, [Linux]: no longer support 0 special PID. +- 188_, [Linux]: psutil import error on Linux ARM architectures. +- 194_, [POSIX]: `Process.cpu_percent()`_ now reports a percentage over 100 on multicore processors. -- 197_: [Linux] Process.get_connections() is broken on platforms not +- 197_, [Linux]: `Process.connections()`_ is broken on platforms not supporting IPv6. -- 200_: [Linux] psutil.NUM_CPUS not working on armel and sparc architectures - and causing crash on module import. -- 201_: [Linux] Process.get_connections() is broken on big-endian +- 200_, [Linux], **[critical]**: ``psutil.NUM_CPUS`` not working on armel and + sparc architectures and causing crash on module import. +- 201_, [Linux]: `Process.connections()`_ is broken on big-endian architectures. -- 211_: Process instance can unexpectedly raise NoSuchProcess if tested for - equality with another object. -- 218_: [Linux] crash at import time on Debian 64-bit because of a missing - line in /proc/meminfo. -- 226_: [FreeBSD] crash at import time on FreeBSD 7 and minor. +- 211_: `Process`_ instance can unexpectedly raise `NoSuchProcess`_ if tested + for equality with another object. +- 218_, [Linux], **[critical]**: crash at import time on Debian 64-bit because + of a missing line in ``/proc/meminfo``. +- 226_, [FreeBSD], **[critical]**: crash at import time on FreeBSD 7 and minor. 0.3.0 ===== @@ -2087,27 +2089,29 @@ DeprecationWarning. **Enhancements** -- 125_: system per-cpu percentage utilization and times. -- 163_: per-process associated terminal (TTY). -- 171_: added get_phymem() and get_virtmem() functions returning system - memory information (total, used, free) and memory percent usage. - total_* avail_* and used_* memory functions are deprecated. -- 172_: disk usage statistics. -- 174_: mounted disk partitions. +- 125_: system per-cpu percentage utilization and times (`Process.cpu_times()`_, + `Process.cpu_percent()`_). +- 163_: per-process associated terminal / TTY (`Process.terminal()`_). +- 171_: added ``get_phymem()`` and ``get_virtmem()`` functions returning system + memory information (``total``, ``used``, ``free``) and memory percent usage. + ``total_*``, ``avail_*`` and ``used_*`` memory functions are deprecated. +- 172_: disk usage statistics (`disk_usage()`_). +- 174_: mounted disk partitions (`disk_partitions()`_). - 179_: setuptools is now used in setup.py **Bug fixes** -- 159_: SetSeDebug() does not close handles or unset impersonation on return. -- 164_: [Windows] wait function raises a TimeoutException when a process - returns -1 . -- 165_: process.status raises an unhandled exception. -- 166_: get_memory_info() leaks handles hogging system resources. -- 168_: psutil.cpu_percent() returns erroneous results when used in +- 159_, [Windows]: ``SetSeDebug()`` does not close handles or unset + impersonation on return. +- 164_, [Windows]: wait function raises a ``TimeoutException`` when a process + returns ``-1``. +- 165_: `Process.status()`_ raises an unhandled exception. +- 166_: `Process.memory_info()`_ leaks handles hogging system resources. +- 168_: `cpu_percent()`_ returns erroneous results when used in non-blocking mode. (patch by Philip Roberts) -- 178_: macOS - Process.get_threads() leaks memory -- 180_: [Windows] Process's get_num_threads() and get_threads() methods can - raise NoSuchProcess exception while process still exists. +- 178_, [macOS]: `Process.threads()`_ leaks memory. +- 180_, [Windows]: `Process.num_threads()`_ and `Process.threads()`_ methods + can raise `NoSuchProcess`_ exception while process still exists. 0.2.1 ===== @@ -2116,40 +2120,41 @@ DeprecationWarning. **Enhancements** -- 64_: per-process I/O counters. -- 116_: per-process wait() (wait for process to terminate and return its exit - code). -- 134_: per-process get_threads() returning information (id, user and kernel - times) about threads opened by process. -- 136_: process executable path on FreeBSD is now determined by asking the +- 64_: per-process I/O counters (`Process.io_counters()`_). +- 116_: per-process `Process.wait()`_ (wait for process to terminate and return + its exit code). +- 134_: per-process threads (`Process.threads()`_). +- 136_: `Process.exe()`_ path on FreeBSD is now determined by asking the kernel instead of guessing it from cmdline[0]. -- 137_: per-process real, effective and saved user and group ids. -- 140_: system boot time. -- 142_: per-process get and set niceness (priority). -- 143_: per-process status. -- 147_: per-process I/O nice (priority) - Linux only. -- 148_: psutil.Popen class which tidies up subprocess.Popen and psutil.Process - in a unique interface. -- 152_: [macOS] get_process_open_files() implementation has been rewritten - in C and no longer relies on lsof resulting in a 3x speedup. -- 153_: [macOS] get_process_connection() implementation has been rewritten - in C and no longer relies on lsof resulting in a 3x speedup. +- 137_: per-process real, effective and saved user and group ids + (`Process.gids()`_). +- 140_: system boot time (`boot_time()`_). +- 142_: per-process get and set niceness (priority) (`Process.nice()`_). +- 143_: per-process status (`Process.status()`_). +- 147_ [Linux]: per-process I/O niceness / priority (`Process.ionice()`_). +- 148_: `psutil.Popen`_ class which tidies up ``subprocess.Popen`` and `Process`_ + class in a single interface. +- 152_, [macOS]: `Process.open_files()`_ implementation has been rewritten + in C and no longer relies on ``lsof`` resulting in a 3x speedup. +- 153_, [macOS]: `Process.connections()`_ implementation has been rewritten + in C and no longer relies on ``lsof`` resulting in a 3x speedup. **Bug fixes** -- 83_: process cmdline is empty on macOS 64-bit. -- 130_: a race condition can cause IOError exception be raised on - Linux if process disappears between open() and subsequent read() calls. -- 145_: WindowsError was raised instead of psutil.AccessDenied when using - process resume() or suspend() on Windows. -- 146_: 'exe' property on Linux can raise TypeError if path contains NULL - bytes. -- 151_: exe and getcwd() for PID 0 on Linux return inconsistent data. +- 83_, [macOS]: `Process.cmdline()`_ is empty on macOS 64-bit. +- 130_, [Linux]: a race condition can cause ``IOError`` exception be raised on + if process disappears between ``open()`` and the subsequent ``read()`` call. +- 145_, [Windows], **[critical]**: ``WindowsError`` was raised instead of + `AccessDenied`_ when using `Process.resume()`_ or `Process.suspend()`_. +- 146_, [Linux]: `Process.exe()`_ property can raise ``TypeError`` if path + contains NULL bytes. +- 151_, [Linux]: `Process.exe()`_ and `Process.cwd()`_ for PID 0 return + inconsistent data. **API changes** -- Process "uid" and "gid" properties are deprecated in favor of "uids" and - "gids" properties. +- `Process`_ ``uid`` and ``gid`` properties are deprecated in favor of ``uids`` + and ``gids`` properties. 0.2.0 ===== @@ -2158,59 +2163,62 @@ DeprecationWarning. **Enhancements** -- 79_: per-process open files. +- 79_: per-process open files (`Process.open_files()`_). - 88_: total system physical cached memory. - 88_: total system physical memory buffers used by the kernel. -- 91_: per-process send_signal() and terminate() methods. -- 95_: NoSuchProcess and AccessDenied exception classes now provide "pid", - "name" and "msg" attributes. -- 97_: per-process children. -- 98_: Process.get_cpu_times() and Process.get_memory_info now return +- 91_: add `Process.send_signal()`_ and `Process.terminate()`_ methods. +- 95_: `NoSuchProcess`_ and `AccessDenied`_ exception classes now provide + ``pid``, ``name`` and ``msg`` attributes. +- 97_: per-process children (`Process.children()`_). +- 98_: `Process.cpu_times()`_ and `Process.memory_info()`_ now return a namedtuple instead of a tuple. -- 103_: per-process opened TCP and UDP connections. -- 107_: add support for Windows 64 bit. (patch by cjgohlke) -- 111_: per-process executable name. -- 113_: exception messages now include process name and pid. -- 114_: process username Windows implementation has been rewritten in pure - C and no longer uses WMI resulting in a big speedup. Also, pywin32 is no - longer required as a third-party dependancy. (patch by wj32) -- 117_: added support for Windows 2000. -- 123_: psutil.cpu_percent() and psutil.Process.cpu_percent() accept a - new 'interval' parameter. -- 129_: per-process number of threads. +- 103_: per-process opened TCP and UDP connections (`Process.connections()`_). +- 107_, [Windows]: add support for Windows 64 bit. (patch by cjgohlke) +- 111_: per-process executable name (`Process.exe()`_). +- 113_: exception messages now include `Process.name()`_ and `Process.pid`_. +- 114_, [Windows]: `Process.username()`_ has been rewritten in pure C and no + longer uses WMI resulting in a big speedup. Also, pywin32 is no longer + required as a third-party dependancy. (patch by wj32) +- 117_, [Windows]: added support for Windows 2000. +- 123_: `cpu_percent()`_ and `Process.cpu_percent()`_ accept a + new ``interval`` parameter. +- 129_: per-process threads (`Process.threads()`_). **Bug fixes** - 80_: fixed warnings when installing psutil with easy_install. -- 81_: psutil fails to compile with Visual Studio. -- 94_: suspend() raises OSError instead of AccessDenied. -- 86_: psutil didn't compile against FreeBSD 6.x. -- 102_: orphaned process handles obtained by using OpenProcess in C were - left behind every time Process class was instantiated. -- 111_: path and name Process properties report truncated or erroneous - values on UNIX. -- 120_: cpu_percent() always returning 100% on macOS. -- 112_: uid and gid properties don't change if process changes effective +- 81_, [Windows]: psutil fails to compile with Visual Studio. +- 94_: `Process.suspend()`_ raises ``OSError`` instead of `AccessDenied`_. +- 86_, [FreeBSD]: psutil didn't compile against FreeBSD 6.x. +- 102_, [Windows]: orphaned process handles obtained by using ``OpenProcess`` + in C were left behind every time `Process`_ class was instantiated. +- 111_, [POSIX]: ``path`` and ``name`` `Process`_ properties report truncated + or erroneous values on POSIX. +- 120_, [macOS]: `cpu_percent()`_ always returning 100%. +- 112_: ``uid`` and ``gid`` properties don't change if process changes effective user/group id at some point. -- 126_: ppid, uid, gid, name, exe, cmdline and create_time properties are - no longer cached and correctly raise NoSuchProcess exception if the process - disappears. +- 126_: `Process.ppid()`_, `Process.uids()`_, `Process.gids()`_, `Process.name()`_, + `Process.exe()`_, `Process.cmdline()`_ and `Process.create_time()`_ + properties are no longer cached and correctly raise `NoSuchProcess`_ exception + if the process disappears. **API changes** -- psutil.Process.path property is deprecated and works as an alias for "exe" - property. -- psutil.Process.kill(): signal argument was removed - to send a signal to the - process use send_signal(signal) method instead. -- psutil.Process.get_memory_info() returns a nametuple instead of a tuple. -- psutil.cpu_times() returns a nametuple instead of a tuple. -- New psutil.Process methods: get_open_files(), get_connections(), - send_signal() and terminate(). -- ppid, uid, gid, name, exe, cmdline and create_time properties are no longer - cached and raise NoSuchProcess exception if process disappears. -- psutil.cpu_percent() no longer returns immediately (see issue 123). -- psutil.Process.get_cpu_percent() and psutil.cpu_percent() no longer returns - immediately by default (see issue 123). +- ``psutil.Process.path`` property is deprecated and works as an alias for + ``psutil.Process.exe`` property. +- `Process.kill()`_: signal argument was removed - to send a signal to the + process use `Process.send_signal()`_ method instead. +- `Process.memory_info()`_ returns a nametuple instead of a tuple. +- `cpu_times()`_ returns a nametuple instead of a tuple. +- New `Process`_ methods: `Process.open_files()`_, `Process.connections()`_, + `Process.send_signal()`_ and `Process.terminate()`_. +- `Process.ppid()`_, `Process.uids()`_, `Process.gids()`_, `Process.name()`_, + `Process.exe()`_, `Process.cmdline()`_ and `Process.create_time()`_ + properties are no longer cached and raise `NoSuchProcess`_ exception if process + disappears. +- `cpu_percent()`_ no longer returns immediately (see issue 123). +- `Process.cpu_percent()`_ and `cpu_percent()`_ no longer returns immediately + by default (see issue 123_). 0.1.3 ===== @@ -2219,28 +2227,29 @@ DeprecationWarning. **Enhancements** -- 14_: per-process username -- 51_: per-process current working directory (Windows and Linux only) -- 59_: Process.is_running() is now 10 times faster -- 61_: added supoprt for FreeBSD 64 bit -- 71_: implemented suspend/resume process -- 75_: python 3 support +- 14_: `Process.username()`_. +- 51_, [Linux], [Windows]: per-process current working directory (`Process.cwd()`_). +- 59_: `Process.is_running()`_ is now 10 times faster. +- 61_, [FreeBSD]: added supoprt for FreeBSD 64 bit. +- 71_: per-process suspend and resume (`Process.suspend()`_ and `Process.resume()`_). +- 75_: Python 3 support. **Bug fixes** -- 36_: process cpu_times() and memory_info() functions succeeded also for dead - processes while a NoSuchProcess exception is supposed to be raised. -- 48_: incorrect size for mib array defined in getcmdargs for BSD -- 49_: possible memory leak due to missing free() on error condition on -- 50_: fixed getcmdargs() memory fragmentation on BSD -- 55_: test_pid_4 was failing on Windows Vista +- 36_: `Process.cpu_times()`_ and `Process.memory_info()`_ functions succeeded. + also for dead processes while a `NoSuchProcess`_ exception is supposed to be raised. +- 48_, [FreeBSD]: incorrect size for MIB array defined in ``getcmdargs``. +- 49_, [FreeBSD]: possible memory leak due to missing ``free()`` on error + condition in ``getcmdpath()``. +- 50_, [BSD]: fixed ``getcmdargs()`` memory fragmentation. +- 55_, [Windows]: ``test_pid_4`` was failing on Windows Vista. - 57_: some unit tests were failing on systems where no swap memory is - available -- 58_: is_running() is now called before kill() to make sure we are going - to kill the correct process. -- 73_: virtual memory size reported on macOS includes shared library size -- 77_: NoSuchProcess wasn't raised on Process.create_time if kill() was - used first. + available. +- 58_: `Process.is_running()`_ is now called before `Process.kill()`_ to make + sure we are going to kill the correct process. +- 73_, [macOS]: virtual memory size reported on includes shared library size. +- 77_: `NoSuchProcess`_ wasn't raised on `Process.create_time()`_ if `Process.kill()`_ + was used first. 0.1.2 ===== @@ -2249,21 +2258,21 @@ DeprecationWarning. **Enhancements** -- 32_: Per-process CPU user/kernel times -- 33_: Process create time -- 34_: Per-process CPU utilization percentage -- 38_: Per-process memory usage (bytes) -- 41_: Per-process memory utilization (percent) -- 39_: System uptime -- 43_: Total system virtual memory -- 46_: Total system physical memory -- 44_: Total system used/free virtual and physical memory +- 32_: Per-process CPU user/kernel times (`Process.cpu_times()`_). +- 33_: Per-process create time (`Process.create_time()`_). +- 34_: Per-process CPU utilization percentage (`Process.cpu_percent()`_). +- 38_: Per-process memory usage (bytes) (`Process.memory_info()`_). +- 41_: Per-process memory percent (`Process.memory_percent()`_). +- 39_: System uptime (`boot_time()`_). +- 43_: Total system virtual memory. +- 46_: Total system physical memory. +- 44_: Total system used/free virtual and physical memory. **Bug fixes** -- 36_: [Windows] NoSuchProcess not raised when accessing timing methods. -- 40_: test_get_cpu_times() failing on FreeBSD and macOS. -- 42_: [Windows] get_memory_percent() raises AccessDenied. +- 36_, [Windows]: `NoSuchProcess`_ not raised when accessing timing methods. +- 40_, [FreeBSD], [macOS]: fix ``test_get_cpu_times`` failures. +- 42_, [Windows]: `Process.memory_percent()`_ raises `AccessDenied`_. 0.1.1 ===== @@ -2272,33 +2281,141 @@ DeprecationWarning. **Enhancements** -- 4_: FreeBSD support for all functions of psutil -- 9_: Process.uid and Process.gid now retrieve process UID and GID. -- 11_: Support for parent/ppid - Process.parent property returns a - Process object representing the parent process, and Process.ppid returns - the parent PID. -- 12_ & 15: - NoSuchProcess exception now raised when creating an object +- 4_, [FreeBSD]: support for all functions of psutil. +- 9_, [macOS], [Windows]: add ``Process.uid`` and ``Process.gid``, returning + process UID and GID. +- 11_: per-process parent object: `Process.parent()`_ property returns a + `Process`_ object representing the parent process, and `Process.ppid()`_ + returns the parent PID. +- 12_, 15_: + `NoSuchProcess`_ exception now raised when creating an object for a nonexistent process, or when retrieving information about a process that has gone away. -- 21_: AccessDenied exception created for raising access denied errors - from OSError or WindowsError on individual platforms. -- 26_: psutil.process_iter() function to iterate over processes as - Process objects with a generator. -- Process objects can now also be compared with == operator for equality +- 21_, [Windows]: `AccessDenied`_ exception created for raising access denied + errors from ``OSError`` or ``WindowsError`` on individual platforms. +- 26_: `process_iter()`_ function to iterate over processes as + `Process`_ objects with a generator. +- `Process`_ objects can now also be compared with == operator for equality (PID, name, command line are compared). **Bug fixes** -- 16_: [Windows] Special case for "System Idle Process" (PID 0) which +- 16_, [Windows]: Special case for "System Idle Process" (PID 0) which otherwise would return an "invalid parameter" exception. -- 17_: get_process_list() ignores NoSuchProcess and AccessDenied +- 17_: get_process_list() ignores `NoSuchProcess`_ and `AccessDenied`_ exceptions during building of the list. -- 22_: [Windows] Process(0).kill() was failing with an unset exception. -- 23_: Special case for pid_exists(0) -- 24_: [Windows] Process(0).kill() now raises AccessDenied exception instead - of WindowsError. -- 30_: psutil.get_pid_list() was returning two ins +- 22_, [Windows]: `Process.kill()`_ for PID 0 was failing with an unset exception. +- 23_, [Linux], [macOS]: create special case for `pid_exists()`_ with PID 0. +- 24_, [Windows], **[critical]**: `Process.kill()`_ for PID 0 now raises + `AccessDenied`_ exception instead of ``WindowsError``. +- 30_: psutil.get_pid_list() was returning two 0 PIDs. + + +.. _`PROCFS_PATH`: https://psutil.readthedocs.io/en/latest/#psutil.PROCFS_PATH + +.. _`boot_time()`: https://psutil.readthedocs.io/en/latest/#psutil.boot_time +.. _`cpu_count()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_count +.. _`cpu_freq()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_freq +.. _`cpu_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent +.. _`cpu_stats()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_stats +.. _`cpu_times()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_times +.. _`cpu_times_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_times_percent +.. _`disk_io_counters()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_io_counters +.. _`disk_partitions()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_partitions +.. _`disk_usage()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_usage +.. _`getloadavg()`: https://psutil.readthedocs.io/en/latest/#psutil.getloadavg +.. _`net_connections()`: https://psutil.readthedocs.io/en/latest/#psutil.net_connections +.. _`net_if_addrs()`: https://psutil.readthedocs.io/en/latest/#psutil.net_if_addrs +.. _`net_if_stats()`: https://psutil.readthedocs.io/en/latest/#psutil.net_if_stats +.. _`net_io_counters()`: https://psutil.readthedocs.io/en/latest/#psutil.net_io_counters +.. _`pid_exists()`: https://psutil.readthedocs.io/en/latest/#psutil.pid_exists +.. _`pids()`: https://psutil.readthedocs.io/en/latest/#psutil.pids +.. _`process_iter()`: https://psutil.readthedocs.io/en/latest/#psutil.process_iter +.. _`sensors_battery()`: https://psutil.readthedocs.io/en/latest/#psutil.sensors_battery +.. _`sensors_fans()`: https://psutil.readthedocs.io/en/latest/#psutil.sensors_fans +.. _`sensors_temperatures()`: https://psutil.readthedocs.io/en/latest/#psutil.sensors_temperatures +.. _`swap_memory()`: https://psutil.readthedocs.io/en/latest/#psutil.swap_memory +.. _`users()`: https://psutil.readthedocs.io/en/latest/#psutil.users +.. _`virtual_memory()`: https://psutil.readthedocs.io/en/latest/#psutil.virtual_memory +.. _`wait_procs()`: https://psutil.readthedocs.io/en/latest/#psutil.wait_procs +.. _`win_service_get()`: https://psutil.readthedocs.io/en/latest/#psutil.win_service_get +.. _`win_service_iter()`: https://psutil.readthedocs.io/en/latest/#psutil.win_service_iter + + +.. _`Process`: https://psutil.readthedocs.io/en/latest/#psutil.Process +.. _`psutil.Popen`: https://psutil.readthedocs.io/en/latest/#psutil.Popen +.. _`psutil.Process`: https://psutil.readthedocs.io/en/latest/#psutil.Process + + +.. _`AccessDenied`: https://psutil.readthedocs.io/en/latest/#psutil.AccessDenied +.. _`NoSuchProcess`: https://psutil.readthedocs.io/en/latest/#psutil.NoSuchProcess +.. _`TimeoutExpired`: https://psutil.readthedocs.io/en/latest/#psutil.TimeoutExpired +.. _`ZombieProcess`: https://psutil.readthedocs.io/en/latest/#psutil.ZombieProcess + + +.. _`Process.as_dict()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.as_dict +.. _`Process.children()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.children +.. _`Process.cmdline()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.connections +.. _`Process.connections()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.connections +.. _`Process.cpu_affinity()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_affinity +.. _`Process.cpu_num()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_num +.. _`Process.cpu_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_percent +.. _`Process.cpu_times()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_times +.. _`Process.create_time()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.create_time +.. _`Process.cwd()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cwd +.. _`Process.environ()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.environ +.. _`Process.exe()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.exe +.. _`Process.gids()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.gids +.. _`Process.io_counters()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.io_counters +.. _`Process.ionice()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.ionice +.. _`Process.is_running()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.is_running +.. _`Process.kill()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.kill +.. _`Process.memory_full_info()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_full_info +.. _`Process.memory_info()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info +.. _`Process.memory_info_ex()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info_ex +.. _`Process.memory_maps()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_maps +.. _`Process.memory_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_percent +.. _`Process.name()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.name +.. _`Process.nice()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.nice +.. _`Process.num_ctx_switches()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_ctx_switches +.. _`Process.num_fds()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_fds +.. _`Process.num_handles()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_handles +.. _`Process.num_threads()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_threads +.. _`Process.oneshot()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.oneshot +.. _`Process.open_files()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.open_files +.. _`Process.parent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.parent +.. _`Process.parents()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.parents +.. _`Process.pid`: https://psutil.readthedocs.io/en/latest/#psutil.Process.pid +.. _`Process.ppid()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.ppid +.. _`Process.resume()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.resume +.. _`Process.rlimit()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.rlimit +.. _`Process.send_signal()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.send_signal +.. _`Process.status()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.status +.. _`Process.suspend()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.suspend +.. _`Process.terminal()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.terminal +.. _`Process.terminate()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.terminate +.. _`Process.threads()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.threads +.. _`Process.uids()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.uids +.. _`Process.username()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.username +.. _`Process.wait()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.wait + + +.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py +.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py +.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py +.. _`pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py +.. _`top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py + .. _1: https://github.com/giampaolo/psutil/issues/1 .. _2: https://github.com/giampaolo/psutil/issues/2 @@ -4300,3 +4417,1003 @@ DeprecationWarning. .. _1998: https://github.com/giampaolo/psutil/issues/1998 .. _1999: https://github.com/giampaolo/psutil/issues/1999 .. _2000: https://github.com/giampaolo/psutil/issues/2000 +.. _2001: https://github.com/giampaolo/psutil/issues/2001 +.. _2002: https://github.com/giampaolo/psutil/issues/2002 +.. _2003: https://github.com/giampaolo/psutil/issues/2003 +.. _2004: https://github.com/giampaolo/psutil/issues/2004 +.. _2005: https://github.com/giampaolo/psutil/issues/2005 +.. _2006: https://github.com/giampaolo/psutil/issues/2006 +.. _2007: https://github.com/giampaolo/psutil/issues/2007 +.. _2008: https://github.com/giampaolo/psutil/issues/2008 +.. _2009: https://github.com/giampaolo/psutil/issues/2009 +.. _2010: https://github.com/giampaolo/psutil/issues/2010 +.. _2011: https://github.com/giampaolo/psutil/issues/2011 +.. _2012: https://github.com/giampaolo/psutil/issues/2012 +.. _2013: https://github.com/giampaolo/psutil/issues/2013 +.. _2014: https://github.com/giampaolo/psutil/issues/2014 +.. _2015: https://github.com/giampaolo/psutil/issues/2015 +.. _2016: https://github.com/giampaolo/psutil/issues/2016 +.. _2017: https://github.com/giampaolo/psutil/issues/2017 +.. _2018: https://github.com/giampaolo/psutil/issues/2018 +.. _2019: https://github.com/giampaolo/psutil/issues/2019 +.. _2020: https://github.com/giampaolo/psutil/issues/2020 +.. _2021: https://github.com/giampaolo/psutil/issues/2021 +.. _2022: https://github.com/giampaolo/psutil/issues/2022 +.. _2023: https://github.com/giampaolo/psutil/issues/2023 +.. _2024: https://github.com/giampaolo/psutil/issues/2024 +.. _2025: https://github.com/giampaolo/psutil/issues/2025 +.. _2026: https://github.com/giampaolo/psutil/issues/2026 +.. _2027: https://github.com/giampaolo/psutil/issues/2027 +.. _2028: https://github.com/giampaolo/psutil/issues/2028 +.. _2029: https://github.com/giampaolo/psutil/issues/2029 +.. _2030: https://github.com/giampaolo/psutil/issues/2030 +.. _2031: https://github.com/giampaolo/psutil/issues/2031 +.. _2032: https://github.com/giampaolo/psutil/issues/2032 +.. _2033: https://github.com/giampaolo/psutil/issues/2033 +.. _2034: https://github.com/giampaolo/psutil/issues/2034 +.. _2035: https://github.com/giampaolo/psutil/issues/2035 +.. _2036: https://github.com/giampaolo/psutil/issues/2036 +.. _2037: https://github.com/giampaolo/psutil/issues/2037 +.. _2038: https://github.com/giampaolo/psutil/issues/2038 +.. _2039: https://github.com/giampaolo/psutil/issues/2039 +.. _2040: https://github.com/giampaolo/psutil/issues/2040 +.. _2041: https://github.com/giampaolo/psutil/issues/2041 +.. _2042: https://github.com/giampaolo/psutil/issues/2042 +.. _2043: https://github.com/giampaolo/psutil/issues/2043 +.. _2044: https://github.com/giampaolo/psutil/issues/2044 +.. _2045: https://github.com/giampaolo/psutil/issues/2045 +.. _2046: https://github.com/giampaolo/psutil/issues/2046 +.. _2047: https://github.com/giampaolo/psutil/issues/2047 +.. _2048: https://github.com/giampaolo/psutil/issues/2048 +.. _2049: https://github.com/giampaolo/psutil/issues/2049 +.. _2050: https://github.com/giampaolo/psutil/issues/2050 +.. _2051: https://github.com/giampaolo/psutil/issues/2051 +.. _2052: https://github.com/giampaolo/psutil/issues/2052 +.. _2053: https://github.com/giampaolo/psutil/issues/2053 +.. _2054: https://github.com/giampaolo/psutil/issues/2054 +.. _2055: https://github.com/giampaolo/psutil/issues/2055 +.. _2056: https://github.com/giampaolo/psutil/issues/2056 +.. _2057: https://github.com/giampaolo/psutil/issues/2057 +.. _2058: https://github.com/giampaolo/psutil/issues/2058 +.. _2059: https://github.com/giampaolo/psutil/issues/2059 +.. _2060: https://github.com/giampaolo/psutil/issues/2060 +.. _2061: https://github.com/giampaolo/psutil/issues/2061 +.. _2062: https://github.com/giampaolo/psutil/issues/2062 +.. _2063: https://github.com/giampaolo/psutil/issues/2063 +.. _2064: https://github.com/giampaolo/psutil/issues/2064 +.. _2065: https://github.com/giampaolo/psutil/issues/2065 +.. _2066: https://github.com/giampaolo/psutil/issues/2066 +.. _2067: https://github.com/giampaolo/psutil/issues/2067 +.. _2068: https://github.com/giampaolo/psutil/issues/2068 +.. _2069: https://github.com/giampaolo/psutil/issues/2069 +.. _2070: https://github.com/giampaolo/psutil/issues/2070 +.. _2071: https://github.com/giampaolo/psutil/issues/2071 +.. _2072: https://github.com/giampaolo/psutil/issues/2072 +.. _2073: https://github.com/giampaolo/psutil/issues/2073 +.. _2074: https://github.com/giampaolo/psutil/issues/2074 +.. _2075: https://github.com/giampaolo/psutil/issues/2075 +.. _2076: https://github.com/giampaolo/psutil/issues/2076 +.. _2077: https://github.com/giampaolo/psutil/issues/2077 +.. _2078: https://github.com/giampaolo/psutil/issues/2078 +.. _2079: https://github.com/giampaolo/psutil/issues/2079 +.. _2080: https://github.com/giampaolo/psutil/issues/2080 +.. _2081: https://github.com/giampaolo/psutil/issues/2081 +.. _2082: https://github.com/giampaolo/psutil/issues/2082 +.. _2083: https://github.com/giampaolo/psutil/issues/2083 +.. _2084: https://github.com/giampaolo/psutil/issues/2084 +.. _2085: https://github.com/giampaolo/psutil/issues/2085 +.. _2086: https://github.com/giampaolo/psutil/issues/2086 +.. _2087: https://github.com/giampaolo/psutil/issues/2087 +.. _2088: https://github.com/giampaolo/psutil/issues/2088 +.. _2089: https://github.com/giampaolo/psutil/issues/2089 +.. _2090: https://github.com/giampaolo/psutil/issues/2090 +.. _2091: https://github.com/giampaolo/psutil/issues/2091 +.. _2092: https://github.com/giampaolo/psutil/issues/2092 +.. _2093: https://github.com/giampaolo/psutil/issues/2093 +.. _2094: https://github.com/giampaolo/psutil/issues/2094 +.. _2095: https://github.com/giampaolo/psutil/issues/2095 +.. _2096: https://github.com/giampaolo/psutil/issues/2096 +.. _2097: https://github.com/giampaolo/psutil/issues/2097 +.. _2098: https://github.com/giampaolo/psutil/issues/2098 +.. _2099: https://github.com/giampaolo/psutil/issues/2099 +.. _2100: https://github.com/giampaolo/psutil/issues/2100 +.. _2101: https://github.com/giampaolo/psutil/issues/2101 +.. _2102: https://github.com/giampaolo/psutil/issues/2102 +.. _2103: https://github.com/giampaolo/psutil/issues/2103 +.. _2104: https://github.com/giampaolo/psutil/issues/2104 +.. _2105: https://github.com/giampaolo/psutil/issues/2105 +.. _2106: https://github.com/giampaolo/psutil/issues/2106 +.. _2107: https://github.com/giampaolo/psutil/issues/2107 +.. _2108: https://github.com/giampaolo/psutil/issues/2108 +.. _2109: https://github.com/giampaolo/psutil/issues/2109 +.. _2110: https://github.com/giampaolo/psutil/issues/2110 +.. _2111: https://github.com/giampaolo/psutil/issues/2111 +.. _2112: https://github.com/giampaolo/psutil/issues/2112 +.. _2113: https://github.com/giampaolo/psutil/issues/2113 +.. _2114: https://github.com/giampaolo/psutil/issues/2114 +.. _2115: https://github.com/giampaolo/psutil/issues/2115 +.. _2116: https://github.com/giampaolo/psutil/issues/2116 +.. _2117: https://github.com/giampaolo/psutil/issues/2117 +.. _2118: https://github.com/giampaolo/psutil/issues/2118 +.. _2119: https://github.com/giampaolo/psutil/issues/2119 +.. _2120: https://github.com/giampaolo/psutil/issues/2120 +.. _2121: https://github.com/giampaolo/psutil/issues/2121 +.. _2122: https://github.com/giampaolo/psutil/issues/2122 +.. _2123: https://github.com/giampaolo/psutil/issues/2123 +.. _2124: https://github.com/giampaolo/psutil/issues/2124 +.. _2125: https://github.com/giampaolo/psutil/issues/2125 +.. _2126: https://github.com/giampaolo/psutil/issues/2126 +.. _2127: https://github.com/giampaolo/psutil/issues/2127 +.. _2128: https://github.com/giampaolo/psutil/issues/2128 +.. _2129: https://github.com/giampaolo/psutil/issues/2129 +.. _2130: https://github.com/giampaolo/psutil/issues/2130 +.. _2131: https://github.com/giampaolo/psutil/issues/2131 +.. _2132: https://github.com/giampaolo/psutil/issues/2132 +.. _2133: https://github.com/giampaolo/psutil/issues/2133 +.. _2134: https://github.com/giampaolo/psutil/issues/2134 +.. _2135: https://github.com/giampaolo/psutil/issues/2135 +.. _2136: https://github.com/giampaolo/psutil/issues/2136 +.. _2137: https://github.com/giampaolo/psutil/issues/2137 +.. _2138: https://github.com/giampaolo/psutil/issues/2138 +.. _2139: https://github.com/giampaolo/psutil/issues/2139 +.. _2140: https://github.com/giampaolo/psutil/issues/2140 +.. _2141: https://github.com/giampaolo/psutil/issues/2141 +.. _2142: https://github.com/giampaolo/psutil/issues/2142 +.. _2143: https://github.com/giampaolo/psutil/issues/2143 +.. _2144: https://github.com/giampaolo/psutil/issues/2144 +.. _2145: https://github.com/giampaolo/psutil/issues/2145 +.. _2146: https://github.com/giampaolo/psutil/issues/2146 +.. _2147: https://github.com/giampaolo/psutil/issues/2147 +.. _2148: https://github.com/giampaolo/psutil/issues/2148 +.. _2149: https://github.com/giampaolo/psutil/issues/2149 +.. _2150: https://github.com/giampaolo/psutil/issues/2150 +.. _2151: https://github.com/giampaolo/psutil/issues/2151 +.. _2152: https://github.com/giampaolo/psutil/issues/2152 +.. _2153: https://github.com/giampaolo/psutil/issues/2153 +.. _2154: https://github.com/giampaolo/psutil/issues/2154 +.. _2155: https://github.com/giampaolo/psutil/issues/2155 +.. _2156: https://github.com/giampaolo/psutil/issues/2156 +.. _2157: https://github.com/giampaolo/psutil/issues/2157 +.. _2158: https://github.com/giampaolo/psutil/issues/2158 +.. _2159: https://github.com/giampaolo/psutil/issues/2159 +.. _2160: https://github.com/giampaolo/psutil/issues/2160 +.. _2161: https://github.com/giampaolo/psutil/issues/2161 +.. _2162: https://github.com/giampaolo/psutil/issues/2162 +.. _2163: https://github.com/giampaolo/psutil/issues/2163 +.. _2164: https://github.com/giampaolo/psutil/issues/2164 +.. _2165: https://github.com/giampaolo/psutil/issues/2165 +.. _2166: https://github.com/giampaolo/psutil/issues/2166 +.. _2167: https://github.com/giampaolo/psutil/issues/2167 +.. _2168: https://github.com/giampaolo/psutil/issues/2168 +.. _2169: https://github.com/giampaolo/psutil/issues/2169 +.. _2170: https://github.com/giampaolo/psutil/issues/2170 +.. _2171: https://github.com/giampaolo/psutil/issues/2171 +.. _2172: https://github.com/giampaolo/psutil/issues/2172 +.. _2173: https://github.com/giampaolo/psutil/issues/2173 +.. _2174: https://github.com/giampaolo/psutil/issues/2174 +.. _2175: https://github.com/giampaolo/psutil/issues/2175 +.. _2176: https://github.com/giampaolo/psutil/issues/2176 +.. _2177: https://github.com/giampaolo/psutil/issues/2177 +.. _2178: https://github.com/giampaolo/psutil/issues/2178 +.. _2179: https://github.com/giampaolo/psutil/issues/2179 +.. _2180: https://github.com/giampaolo/psutil/issues/2180 +.. _2181: https://github.com/giampaolo/psutil/issues/2181 +.. _2182: https://github.com/giampaolo/psutil/issues/2182 +.. _2183: https://github.com/giampaolo/psutil/issues/2183 +.. _2184: https://github.com/giampaolo/psutil/issues/2184 +.. _2185: https://github.com/giampaolo/psutil/issues/2185 +.. _2186: https://github.com/giampaolo/psutil/issues/2186 +.. _2187: https://github.com/giampaolo/psutil/issues/2187 +.. _2188: https://github.com/giampaolo/psutil/issues/2188 +.. _2189: https://github.com/giampaolo/psutil/issues/2189 +.. _2190: https://github.com/giampaolo/psutil/issues/2190 +.. _2191: https://github.com/giampaolo/psutil/issues/2191 +.. _2192: https://github.com/giampaolo/psutil/issues/2192 +.. _2193: https://github.com/giampaolo/psutil/issues/2193 +.. _2194: https://github.com/giampaolo/psutil/issues/2194 +.. _2195: https://github.com/giampaolo/psutil/issues/2195 +.. _2196: https://github.com/giampaolo/psutil/issues/2196 +.. _2197: https://github.com/giampaolo/psutil/issues/2197 +.. _2198: https://github.com/giampaolo/psutil/issues/2198 +.. _2199: https://github.com/giampaolo/psutil/issues/2199 +.. _2200: https://github.com/giampaolo/psutil/issues/2200 +.. _2201: https://github.com/giampaolo/psutil/issues/2201 +.. _2202: https://github.com/giampaolo/psutil/issues/2202 +.. _2203: https://github.com/giampaolo/psutil/issues/2203 +.. _2204: https://github.com/giampaolo/psutil/issues/2204 +.. _2205: https://github.com/giampaolo/psutil/issues/2205 +.. _2206: https://github.com/giampaolo/psutil/issues/2206 +.. _2207: https://github.com/giampaolo/psutil/issues/2207 +.. _2208: https://github.com/giampaolo/psutil/issues/2208 +.. _2209: https://github.com/giampaolo/psutil/issues/2209 +.. _2210: https://github.com/giampaolo/psutil/issues/2210 +.. _2211: https://github.com/giampaolo/psutil/issues/2211 +.. _2212: https://github.com/giampaolo/psutil/issues/2212 +.. _2213: https://github.com/giampaolo/psutil/issues/2213 +.. _2214: https://github.com/giampaolo/psutil/issues/2214 +.. _2215: https://github.com/giampaolo/psutil/issues/2215 +.. _2216: https://github.com/giampaolo/psutil/issues/2216 +.. _2217: https://github.com/giampaolo/psutil/issues/2217 +.. _2218: https://github.com/giampaolo/psutil/issues/2218 +.. _2219: https://github.com/giampaolo/psutil/issues/2219 +.. _2220: https://github.com/giampaolo/psutil/issues/2220 +.. _2221: https://github.com/giampaolo/psutil/issues/2221 +.. _2222: https://github.com/giampaolo/psutil/issues/2222 +.. _2223: https://github.com/giampaolo/psutil/issues/2223 +.. _2224: https://github.com/giampaolo/psutil/issues/2224 +.. _2225: https://github.com/giampaolo/psutil/issues/2225 +.. _2226: https://github.com/giampaolo/psutil/issues/2226 +.. _2227: https://github.com/giampaolo/psutil/issues/2227 +.. _2228: https://github.com/giampaolo/psutil/issues/2228 +.. _2229: https://github.com/giampaolo/psutil/issues/2229 +.. _2230: https://github.com/giampaolo/psutil/issues/2230 +.. _2231: https://github.com/giampaolo/psutil/issues/2231 +.. _2232: https://github.com/giampaolo/psutil/issues/2232 +.. _2233: https://github.com/giampaolo/psutil/issues/2233 +.. _2234: https://github.com/giampaolo/psutil/issues/2234 +.. _2235: https://github.com/giampaolo/psutil/issues/2235 +.. _2236: https://github.com/giampaolo/psutil/issues/2236 +.. _2237: https://github.com/giampaolo/psutil/issues/2237 +.. _2238: https://github.com/giampaolo/psutil/issues/2238 +.. _2239: https://github.com/giampaolo/psutil/issues/2239 +.. _2240: https://github.com/giampaolo/psutil/issues/2240 +.. _2241: https://github.com/giampaolo/psutil/issues/2241 +.. _2242: https://github.com/giampaolo/psutil/issues/2242 +.. _2243: https://github.com/giampaolo/psutil/issues/2243 +.. _2244: https://github.com/giampaolo/psutil/issues/2244 +.. _2245: https://github.com/giampaolo/psutil/issues/2245 +.. _2246: https://github.com/giampaolo/psutil/issues/2246 +.. _2247: https://github.com/giampaolo/psutil/issues/2247 +.. _2248: https://github.com/giampaolo/psutil/issues/2248 +.. _2249: https://github.com/giampaolo/psutil/issues/2249 +.. _2250: https://github.com/giampaolo/psutil/issues/2250 +.. _2251: https://github.com/giampaolo/psutil/issues/2251 +.. _2252: https://github.com/giampaolo/psutil/issues/2252 +.. _2253: https://github.com/giampaolo/psutil/issues/2253 +.. _2254: https://github.com/giampaolo/psutil/issues/2254 +.. _2255: https://github.com/giampaolo/psutil/issues/2255 +.. _2256: https://github.com/giampaolo/psutil/issues/2256 +.. _2257: https://github.com/giampaolo/psutil/issues/2257 +.. _2258: https://github.com/giampaolo/psutil/issues/2258 +.. _2259: https://github.com/giampaolo/psutil/issues/2259 +.. _2260: https://github.com/giampaolo/psutil/issues/2260 +.. _2261: https://github.com/giampaolo/psutil/issues/2261 +.. _2262: https://github.com/giampaolo/psutil/issues/2262 +.. _2263: https://github.com/giampaolo/psutil/issues/2263 +.. _2264: https://github.com/giampaolo/psutil/issues/2264 +.. _2265: https://github.com/giampaolo/psutil/issues/2265 +.. _2266: https://github.com/giampaolo/psutil/issues/2266 +.. _2267: https://github.com/giampaolo/psutil/issues/2267 +.. _2268: https://github.com/giampaolo/psutil/issues/2268 +.. _2269: https://github.com/giampaolo/psutil/issues/2269 +.. _2270: https://github.com/giampaolo/psutil/issues/2270 +.. _2271: https://github.com/giampaolo/psutil/issues/2271 +.. _2272: https://github.com/giampaolo/psutil/issues/2272 +.. _2273: https://github.com/giampaolo/psutil/issues/2273 +.. _2274: https://github.com/giampaolo/psutil/issues/2274 +.. _2275: https://github.com/giampaolo/psutil/issues/2275 +.. _2276: https://github.com/giampaolo/psutil/issues/2276 +.. _2277: https://github.com/giampaolo/psutil/issues/2277 +.. _2278: https://github.com/giampaolo/psutil/issues/2278 +.. _2279: https://github.com/giampaolo/psutil/issues/2279 +.. _2280: https://github.com/giampaolo/psutil/issues/2280 +.. _2281: https://github.com/giampaolo/psutil/issues/2281 +.. _2282: https://github.com/giampaolo/psutil/issues/2282 +.. _2283: https://github.com/giampaolo/psutil/issues/2283 +.. _2284: https://github.com/giampaolo/psutil/issues/2284 +.. _2285: https://github.com/giampaolo/psutil/issues/2285 +.. _2286: https://github.com/giampaolo/psutil/issues/2286 +.. _2287: https://github.com/giampaolo/psutil/issues/2287 +.. _2288: https://github.com/giampaolo/psutil/issues/2288 +.. _2289: https://github.com/giampaolo/psutil/issues/2289 +.. _2290: https://github.com/giampaolo/psutil/issues/2290 +.. _2291: https://github.com/giampaolo/psutil/issues/2291 +.. _2292: https://github.com/giampaolo/psutil/issues/2292 +.. _2293: https://github.com/giampaolo/psutil/issues/2293 +.. _2294: https://github.com/giampaolo/psutil/issues/2294 +.. _2295: https://github.com/giampaolo/psutil/issues/2295 +.. _2296: https://github.com/giampaolo/psutil/issues/2296 +.. _2297: https://github.com/giampaolo/psutil/issues/2297 +.. _2298: https://github.com/giampaolo/psutil/issues/2298 +.. _2299: https://github.com/giampaolo/psutil/issues/2299 +.. _2300: https://github.com/giampaolo/psutil/issues/2300 +.. _2301: https://github.com/giampaolo/psutil/issues/2301 +.. _2302: https://github.com/giampaolo/psutil/issues/2302 +.. _2303: https://github.com/giampaolo/psutil/issues/2303 +.. _2304: https://github.com/giampaolo/psutil/issues/2304 +.. _2305: https://github.com/giampaolo/psutil/issues/2305 +.. _2306: https://github.com/giampaolo/psutil/issues/2306 +.. _2307: https://github.com/giampaolo/psutil/issues/2307 +.. _2308: https://github.com/giampaolo/psutil/issues/2308 +.. _2309: https://github.com/giampaolo/psutil/issues/2309 +.. _2310: https://github.com/giampaolo/psutil/issues/2310 +.. _2311: https://github.com/giampaolo/psutil/issues/2311 +.. _2312: https://github.com/giampaolo/psutil/issues/2312 +.. _2313: https://github.com/giampaolo/psutil/issues/2313 +.. _2314: https://github.com/giampaolo/psutil/issues/2314 +.. _2315: https://github.com/giampaolo/psutil/issues/2315 +.. _2316: https://github.com/giampaolo/psutil/issues/2316 +.. _2317: https://github.com/giampaolo/psutil/issues/2317 +.. _2318: https://github.com/giampaolo/psutil/issues/2318 +.. _2319: https://github.com/giampaolo/psutil/issues/2319 +.. _2320: https://github.com/giampaolo/psutil/issues/2320 +.. _2321: https://github.com/giampaolo/psutil/issues/2321 +.. _2322: https://github.com/giampaolo/psutil/issues/2322 +.. _2323: https://github.com/giampaolo/psutil/issues/2323 +.. _2324: https://github.com/giampaolo/psutil/issues/2324 +.. _2325: https://github.com/giampaolo/psutil/issues/2325 +.. _2326: https://github.com/giampaolo/psutil/issues/2326 +.. _2327: https://github.com/giampaolo/psutil/issues/2327 +.. _2328: https://github.com/giampaolo/psutil/issues/2328 +.. _2329: https://github.com/giampaolo/psutil/issues/2329 +.. _2330: https://github.com/giampaolo/psutil/issues/2330 +.. _2331: https://github.com/giampaolo/psutil/issues/2331 +.. _2332: https://github.com/giampaolo/psutil/issues/2332 +.. _2333: https://github.com/giampaolo/psutil/issues/2333 +.. _2334: https://github.com/giampaolo/psutil/issues/2334 +.. _2335: https://github.com/giampaolo/psutil/issues/2335 +.. _2336: https://github.com/giampaolo/psutil/issues/2336 +.. _2337: https://github.com/giampaolo/psutil/issues/2337 +.. _2338: https://github.com/giampaolo/psutil/issues/2338 +.. _2339: https://github.com/giampaolo/psutil/issues/2339 +.. _2340: https://github.com/giampaolo/psutil/issues/2340 +.. _2341: https://github.com/giampaolo/psutil/issues/2341 +.. _2342: https://github.com/giampaolo/psutil/issues/2342 +.. _2343: https://github.com/giampaolo/psutil/issues/2343 +.. _2344: https://github.com/giampaolo/psutil/issues/2344 +.. _2345: https://github.com/giampaolo/psutil/issues/2345 +.. _2346: https://github.com/giampaolo/psutil/issues/2346 +.. _2347: https://github.com/giampaolo/psutil/issues/2347 +.. _2348: https://github.com/giampaolo/psutil/issues/2348 +.. _2349: https://github.com/giampaolo/psutil/issues/2349 +.. _2350: https://github.com/giampaolo/psutil/issues/2350 +.. _2351: https://github.com/giampaolo/psutil/issues/2351 +.. _2352: https://github.com/giampaolo/psutil/issues/2352 +.. _2353: https://github.com/giampaolo/psutil/issues/2353 +.. _2354: https://github.com/giampaolo/psutil/issues/2354 +.. _2355: https://github.com/giampaolo/psutil/issues/2355 +.. _2356: https://github.com/giampaolo/psutil/issues/2356 +.. _2357: https://github.com/giampaolo/psutil/issues/2357 +.. _2358: https://github.com/giampaolo/psutil/issues/2358 +.. _2359: https://github.com/giampaolo/psutil/issues/2359 +.. _2360: https://github.com/giampaolo/psutil/issues/2360 +.. _2361: https://github.com/giampaolo/psutil/issues/2361 +.. _2362: https://github.com/giampaolo/psutil/issues/2362 +.. _2363: https://github.com/giampaolo/psutil/issues/2363 +.. _2364: https://github.com/giampaolo/psutil/issues/2364 +.. _2365: https://github.com/giampaolo/psutil/issues/2365 +.. _2366: https://github.com/giampaolo/psutil/issues/2366 +.. _2367: https://github.com/giampaolo/psutil/issues/2367 +.. _2368: https://github.com/giampaolo/psutil/issues/2368 +.. _2369: https://github.com/giampaolo/psutil/issues/2369 +.. _2370: https://github.com/giampaolo/psutil/issues/2370 +.. _2371: https://github.com/giampaolo/psutil/issues/2371 +.. _2372: https://github.com/giampaolo/psutil/issues/2372 +.. _2373: https://github.com/giampaolo/psutil/issues/2373 +.. _2374: https://github.com/giampaolo/psutil/issues/2374 +.. _2375: https://github.com/giampaolo/psutil/issues/2375 +.. _2376: https://github.com/giampaolo/psutil/issues/2376 +.. _2377: https://github.com/giampaolo/psutil/issues/2377 +.. _2378: https://github.com/giampaolo/psutil/issues/2378 +.. _2379: https://github.com/giampaolo/psutil/issues/2379 +.. _2380: https://github.com/giampaolo/psutil/issues/2380 +.. _2381: https://github.com/giampaolo/psutil/issues/2381 +.. _2382: https://github.com/giampaolo/psutil/issues/2382 +.. _2383: https://github.com/giampaolo/psutil/issues/2383 +.. _2384: https://github.com/giampaolo/psutil/issues/2384 +.. _2385: https://github.com/giampaolo/psutil/issues/2385 +.. _2386: https://github.com/giampaolo/psutil/issues/2386 +.. _2387: https://github.com/giampaolo/psutil/issues/2387 +.. _2388: https://github.com/giampaolo/psutil/issues/2388 +.. _2389: https://github.com/giampaolo/psutil/issues/2389 +.. _2390: https://github.com/giampaolo/psutil/issues/2390 +.. _2391: https://github.com/giampaolo/psutil/issues/2391 +.. _2392: https://github.com/giampaolo/psutil/issues/2392 +.. _2393: https://github.com/giampaolo/psutil/issues/2393 +.. _2394: https://github.com/giampaolo/psutil/issues/2394 +.. _2395: https://github.com/giampaolo/psutil/issues/2395 +.. _2396: https://github.com/giampaolo/psutil/issues/2396 +.. _2397: https://github.com/giampaolo/psutil/issues/2397 +.. _2398: https://github.com/giampaolo/psutil/issues/2398 +.. _2399: https://github.com/giampaolo/psutil/issues/2399 +.. _2400: https://github.com/giampaolo/psutil/issues/2400 +.. _2401: https://github.com/giampaolo/psutil/issues/2401 +.. _2402: https://github.com/giampaolo/psutil/issues/2402 +.. _2403: https://github.com/giampaolo/psutil/issues/2403 +.. _2404: https://github.com/giampaolo/psutil/issues/2404 +.. _2405: https://github.com/giampaolo/psutil/issues/2405 +.. _2406: https://github.com/giampaolo/psutil/issues/2406 +.. _2407: https://github.com/giampaolo/psutil/issues/2407 +.. _2408: https://github.com/giampaolo/psutil/issues/2408 +.. _2409: https://github.com/giampaolo/psutil/issues/2409 +.. _2410: https://github.com/giampaolo/psutil/issues/2410 +.. _2411: https://github.com/giampaolo/psutil/issues/2411 +.. _2412: https://github.com/giampaolo/psutil/issues/2412 +.. _2413: https://github.com/giampaolo/psutil/issues/2413 +.. _2414: https://github.com/giampaolo/psutil/issues/2414 +.. _2415: https://github.com/giampaolo/psutil/issues/2415 +.. _2416: https://github.com/giampaolo/psutil/issues/2416 +.. _2417: https://github.com/giampaolo/psutil/issues/2417 +.. _2418: https://github.com/giampaolo/psutil/issues/2418 +.. _2419: https://github.com/giampaolo/psutil/issues/2419 +.. _2420: https://github.com/giampaolo/psutil/issues/2420 +.. _2421: https://github.com/giampaolo/psutil/issues/2421 +.. _2422: https://github.com/giampaolo/psutil/issues/2422 +.. _2423: https://github.com/giampaolo/psutil/issues/2423 +.. _2424: https://github.com/giampaolo/psutil/issues/2424 +.. _2425: https://github.com/giampaolo/psutil/issues/2425 +.. _2426: https://github.com/giampaolo/psutil/issues/2426 +.. _2427: https://github.com/giampaolo/psutil/issues/2427 +.. _2428: https://github.com/giampaolo/psutil/issues/2428 +.. _2429: https://github.com/giampaolo/psutil/issues/2429 +.. _2430: https://github.com/giampaolo/psutil/issues/2430 +.. _2431: https://github.com/giampaolo/psutil/issues/2431 +.. _2432: https://github.com/giampaolo/psutil/issues/2432 +.. _2433: https://github.com/giampaolo/psutil/issues/2433 +.. _2434: https://github.com/giampaolo/psutil/issues/2434 +.. _2435: https://github.com/giampaolo/psutil/issues/2435 +.. _2436: https://github.com/giampaolo/psutil/issues/2436 +.. _2437: https://github.com/giampaolo/psutil/issues/2437 +.. _2438: https://github.com/giampaolo/psutil/issues/2438 +.. _2439: https://github.com/giampaolo/psutil/issues/2439 +.. _2440: https://github.com/giampaolo/psutil/issues/2440 +.. _2441: https://github.com/giampaolo/psutil/issues/2441 +.. _2442: https://github.com/giampaolo/psutil/issues/2442 +.. _2443: https://github.com/giampaolo/psutil/issues/2443 +.. _2444: https://github.com/giampaolo/psutil/issues/2444 +.. _2445: https://github.com/giampaolo/psutil/issues/2445 +.. _2446: https://github.com/giampaolo/psutil/issues/2446 +.. _2447: https://github.com/giampaolo/psutil/issues/2447 +.. _2448: https://github.com/giampaolo/psutil/issues/2448 +.. _2449: https://github.com/giampaolo/psutil/issues/2449 +.. _2450: https://github.com/giampaolo/psutil/issues/2450 +.. _2451: https://github.com/giampaolo/psutil/issues/2451 +.. _2452: https://github.com/giampaolo/psutil/issues/2452 +.. _2453: https://github.com/giampaolo/psutil/issues/2453 +.. _2454: https://github.com/giampaolo/psutil/issues/2454 +.. _2455: https://github.com/giampaolo/psutil/issues/2455 +.. _2456: https://github.com/giampaolo/psutil/issues/2456 +.. _2457: https://github.com/giampaolo/psutil/issues/2457 +.. _2458: https://github.com/giampaolo/psutil/issues/2458 +.. _2459: https://github.com/giampaolo/psutil/issues/2459 +.. _2460: https://github.com/giampaolo/psutil/issues/2460 +.. _2461: https://github.com/giampaolo/psutil/issues/2461 +.. _2462: https://github.com/giampaolo/psutil/issues/2462 +.. _2463: https://github.com/giampaolo/psutil/issues/2463 +.. _2464: https://github.com/giampaolo/psutil/issues/2464 +.. _2465: https://github.com/giampaolo/psutil/issues/2465 +.. _2466: https://github.com/giampaolo/psutil/issues/2466 +.. _2467: https://github.com/giampaolo/psutil/issues/2467 +.. _2468: https://github.com/giampaolo/psutil/issues/2468 +.. _2469: https://github.com/giampaolo/psutil/issues/2469 +.. _2470: https://github.com/giampaolo/psutil/issues/2470 +.. _2471: https://github.com/giampaolo/psutil/issues/2471 +.. _2472: https://github.com/giampaolo/psutil/issues/2472 +.. _2473: https://github.com/giampaolo/psutil/issues/2473 +.. _2474: https://github.com/giampaolo/psutil/issues/2474 +.. _2475: https://github.com/giampaolo/psutil/issues/2475 +.. _2476: https://github.com/giampaolo/psutil/issues/2476 +.. _2477: https://github.com/giampaolo/psutil/issues/2477 +.. _2478: https://github.com/giampaolo/psutil/issues/2478 +.. _2479: https://github.com/giampaolo/psutil/issues/2479 +.. _2480: https://github.com/giampaolo/psutil/issues/2480 +.. _2481: https://github.com/giampaolo/psutil/issues/2481 +.. _2482: https://github.com/giampaolo/psutil/issues/2482 +.. _2483: https://github.com/giampaolo/psutil/issues/2483 +.. _2484: https://github.com/giampaolo/psutil/issues/2484 +.. _2485: https://github.com/giampaolo/psutil/issues/2485 +.. _2486: https://github.com/giampaolo/psutil/issues/2486 +.. _2487: https://github.com/giampaolo/psutil/issues/2487 +.. _2488: https://github.com/giampaolo/psutil/issues/2488 +.. _2489: https://github.com/giampaolo/psutil/issues/2489 +.. _2490: https://github.com/giampaolo/psutil/issues/2490 +.. _2491: https://github.com/giampaolo/psutil/issues/2491 +.. _2492: https://github.com/giampaolo/psutil/issues/2492 +.. _2493: https://github.com/giampaolo/psutil/issues/2493 +.. _2494: https://github.com/giampaolo/psutil/issues/2494 +.. _2495: https://github.com/giampaolo/psutil/issues/2495 +.. _2496: https://github.com/giampaolo/psutil/issues/2496 +.. _2497: https://github.com/giampaolo/psutil/issues/2497 +.. _2498: https://github.com/giampaolo/psutil/issues/2498 +.. _2499: https://github.com/giampaolo/psutil/issues/2499 +.. _2500: https://github.com/giampaolo/psutil/issues/2500 +.. _2501: https://github.com/giampaolo/psutil/issues/2501 +.. _2502: https://github.com/giampaolo/psutil/issues/2502 +.. _2503: https://github.com/giampaolo/psutil/issues/2503 +.. _2504: https://github.com/giampaolo/psutil/issues/2504 +.. _2505: https://github.com/giampaolo/psutil/issues/2505 +.. _2506: https://github.com/giampaolo/psutil/issues/2506 +.. _2507: https://github.com/giampaolo/psutil/issues/2507 +.. _2508: https://github.com/giampaolo/psutil/issues/2508 +.. _2509: https://github.com/giampaolo/psutil/issues/2509 +.. _2510: https://github.com/giampaolo/psutil/issues/2510 +.. _2511: https://github.com/giampaolo/psutil/issues/2511 +.. _2512: https://github.com/giampaolo/psutil/issues/2512 +.. _2513: https://github.com/giampaolo/psutil/issues/2513 +.. _2514: https://github.com/giampaolo/psutil/issues/2514 +.. _2515: https://github.com/giampaolo/psutil/issues/2515 +.. _2516: https://github.com/giampaolo/psutil/issues/2516 +.. _2517: https://github.com/giampaolo/psutil/issues/2517 +.. _2518: https://github.com/giampaolo/psutil/issues/2518 +.. _2519: https://github.com/giampaolo/psutil/issues/2519 +.. _2520: https://github.com/giampaolo/psutil/issues/2520 +.. _2521: https://github.com/giampaolo/psutil/issues/2521 +.. _2522: https://github.com/giampaolo/psutil/issues/2522 +.. _2523: https://github.com/giampaolo/psutil/issues/2523 +.. _2524: https://github.com/giampaolo/psutil/issues/2524 +.. _2525: https://github.com/giampaolo/psutil/issues/2525 +.. _2526: https://github.com/giampaolo/psutil/issues/2526 +.. _2527: https://github.com/giampaolo/psutil/issues/2527 +.. _2528: https://github.com/giampaolo/psutil/issues/2528 +.. _2529: https://github.com/giampaolo/psutil/issues/2529 +.. _2530: https://github.com/giampaolo/psutil/issues/2530 +.. _2531: https://github.com/giampaolo/psutil/issues/2531 +.. _2532: https://github.com/giampaolo/psutil/issues/2532 +.. _2533: https://github.com/giampaolo/psutil/issues/2533 +.. _2534: https://github.com/giampaolo/psutil/issues/2534 +.. _2535: https://github.com/giampaolo/psutil/issues/2535 +.. _2536: https://github.com/giampaolo/psutil/issues/2536 +.. _2537: https://github.com/giampaolo/psutil/issues/2537 +.. _2538: https://github.com/giampaolo/psutil/issues/2538 +.. _2539: https://github.com/giampaolo/psutil/issues/2539 +.. _2540: https://github.com/giampaolo/psutil/issues/2540 +.. _2541: https://github.com/giampaolo/psutil/issues/2541 +.. _2542: https://github.com/giampaolo/psutil/issues/2542 +.. _2543: https://github.com/giampaolo/psutil/issues/2543 +.. _2544: https://github.com/giampaolo/psutil/issues/2544 +.. _2545: https://github.com/giampaolo/psutil/issues/2545 +.. _2546: https://github.com/giampaolo/psutil/issues/2546 +.. _2547: https://github.com/giampaolo/psutil/issues/2547 +.. _2548: https://github.com/giampaolo/psutil/issues/2548 +.. _2549: https://github.com/giampaolo/psutil/issues/2549 +.. _2550: https://github.com/giampaolo/psutil/issues/2550 +.. _2551: https://github.com/giampaolo/psutil/issues/2551 +.. _2552: https://github.com/giampaolo/psutil/issues/2552 +.. _2553: https://github.com/giampaolo/psutil/issues/2553 +.. _2554: https://github.com/giampaolo/psutil/issues/2554 +.. _2555: https://github.com/giampaolo/psutil/issues/2555 +.. _2556: https://github.com/giampaolo/psutil/issues/2556 +.. _2557: https://github.com/giampaolo/psutil/issues/2557 +.. _2558: https://github.com/giampaolo/psutil/issues/2558 +.. _2559: https://github.com/giampaolo/psutil/issues/2559 +.. _2560: https://github.com/giampaolo/psutil/issues/2560 +.. _2561: https://github.com/giampaolo/psutil/issues/2561 +.. _2562: https://github.com/giampaolo/psutil/issues/2562 +.. _2563: https://github.com/giampaolo/psutil/issues/2563 +.. _2564: https://github.com/giampaolo/psutil/issues/2564 +.. _2565: https://github.com/giampaolo/psutil/issues/2565 +.. _2566: https://github.com/giampaolo/psutil/issues/2566 +.. _2567: https://github.com/giampaolo/psutil/issues/2567 +.. _2568: https://github.com/giampaolo/psutil/issues/2568 +.. _2569: https://github.com/giampaolo/psutil/issues/2569 +.. _2570: https://github.com/giampaolo/psutil/issues/2570 +.. _2571: https://github.com/giampaolo/psutil/issues/2571 +.. _2572: https://github.com/giampaolo/psutil/issues/2572 +.. _2573: https://github.com/giampaolo/psutil/issues/2573 +.. _2574: https://github.com/giampaolo/psutil/issues/2574 +.. _2575: https://github.com/giampaolo/psutil/issues/2575 +.. _2576: https://github.com/giampaolo/psutil/issues/2576 +.. _2577: https://github.com/giampaolo/psutil/issues/2577 +.. _2578: https://github.com/giampaolo/psutil/issues/2578 +.. _2579: https://github.com/giampaolo/psutil/issues/2579 +.. _2580: https://github.com/giampaolo/psutil/issues/2580 +.. _2581: https://github.com/giampaolo/psutil/issues/2581 +.. _2582: https://github.com/giampaolo/psutil/issues/2582 +.. _2583: https://github.com/giampaolo/psutil/issues/2583 +.. _2584: https://github.com/giampaolo/psutil/issues/2584 +.. _2585: https://github.com/giampaolo/psutil/issues/2585 +.. _2586: https://github.com/giampaolo/psutil/issues/2586 +.. _2587: https://github.com/giampaolo/psutil/issues/2587 +.. _2588: https://github.com/giampaolo/psutil/issues/2588 +.. _2589: https://github.com/giampaolo/psutil/issues/2589 +.. _2590: https://github.com/giampaolo/psutil/issues/2590 +.. _2591: https://github.com/giampaolo/psutil/issues/2591 +.. _2592: https://github.com/giampaolo/psutil/issues/2592 +.. _2593: https://github.com/giampaolo/psutil/issues/2593 +.. _2594: https://github.com/giampaolo/psutil/issues/2594 +.. _2595: https://github.com/giampaolo/psutil/issues/2595 +.. _2596: https://github.com/giampaolo/psutil/issues/2596 +.. _2597: https://github.com/giampaolo/psutil/issues/2597 +.. _2598: https://github.com/giampaolo/psutil/issues/2598 +.. _2599: https://github.com/giampaolo/psutil/issues/2599 +.. _2600: https://github.com/giampaolo/psutil/issues/2600 +.. _2601: https://github.com/giampaolo/psutil/issues/2601 +.. _2602: https://github.com/giampaolo/psutil/issues/2602 +.. _2603: https://github.com/giampaolo/psutil/issues/2603 +.. _2604: https://github.com/giampaolo/psutil/issues/2604 +.. _2605: https://github.com/giampaolo/psutil/issues/2605 +.. _2606: https://github.com/giampaolo/psutil/issues/2606 +.. _2607: https://github.com/giampaolo/psutil/issues/2607 +.. _2608: https://github.com/giampaolo/psutil/issues/2608 +.. _2609: https://github.com/giampaolo/psutil/issues/2609 +.. _2610: https://github.com/giampaolo/psutil/issues/2610 +.. _2611: https://github.com/giampaolo/psutil/issues/2611 +.. _2612: https://github.com/giampaolo/psutil/issues/2612 +.. _2613: https://github.com/giampaolo/psutil/issues/2613 +.. _2614: https://github.com/giampaolo/psutil/issues/2614 +.. _2615: https://github.com/giampaolo/psutil/issues/2615 +.. _2616: https://github.com/giampaolo/psutil/issues/2616 +.. _2617: https://github.com/giampaolo/psutil/issues/2617 +.. _2618: https://github.com/giampaolo/psutil/issues/2618 +.. _2619: https://github.com/giampaolo/psutil/issues/2619 +.. _2620: https://github.com/giampaolo/psutil/issues/2620 +.. _2621: https://github.com/giampaolo/psutil/issues/2621 +.. _2622: https://github.com/giampaolo/psutil/issues/2622 +.. _2623: https://github.com/giampaolo/psutil/issues/2623 +.. _2624: https://github.com/giampaolo/psutil/issues/2624 +.. _2625: https://github.com/giampaolo/psutil/issues/2625 +.. _2626: https://github.com/giampaolo/psutil/issues/2626 +.. _2627: https://github.com/giampaolo/psutil/issues/2627 +.. _2628: https://github.com/giampaolo/psutil/issues/2628 +.. _2629: https://github.com/giampaolo/psutil/issues/2629 +.. _2630: https://github.com/giampaolo/psutil/issues/2630 +.. _2631: https://github.com/giampaolo/psutil/issues/2631 +.. _2632: https://github.com/giampaolo/psutil/issues/2632 +.. _2633: https://github.com/giampaolo/psutil/issues/2633 +.. _2634: https://github.com/giampaolo/psutil/issues/2634 +.. _2635: https://github.com/giampaolo/psutil/issues/2635 +.. _2636: https://github.com/giampaolo/psutil/issues/2636 +.. _2637: https://github.com/giampaolo/psutil/issues/2637 +.. _2638: https://github.com/giampaolo/psutil/issues/2638 +.. _2639: https://github.com/giampaolo/psutil/issues/2639 +.. _2640: https://github.com/giampaolo/psutil/issues/2640 +.. _2641: https://github.com/giampaolo/psutil/issues/2641 +.. _2642: https://github.com/giampaolo/psutil/issues/2642 +.. _2643: https://github.com/giampaolo/psutil/issues/2643 +.. _2644: https://github.com/giampaolo/psutil/issues/2644 +.. _2645: https://github.com/giampaolo/psutil/issues/2645 +.. _2646: https://github.com/giampaolo/psutil/issues/2646 +.. _2647: https://github.com/giampaolo/psutil/issues/2647 +.. _2648: https://github.com/giampaolo/psutil/issues/2648 +.. _2649: https://github.com/giampaolo/psutil/issues/2649 +.. _2650: https://github.com/giampaolo/psutil/issues/2650 +.. _2651: https://github.com/giampaolo/psutil/issues/2651 +.. _2652: https://github.com/giampaolo/psutil/issues/2652 +.. _2653: https://github.com/giampaolo/psutil/issues/2653 +.. _2654: https://github.com/giampaolo/psutil/issues/2654 +.. _2655: https://github.com/giampaolo/psutil/issues/2655 +.. _2656: https://github.com/giampaolo/psutil/issues/2656 +.. _2657: https://github.com/giampaolo/psutil/issues/2657 +.. _2658: https://github.com/giampaolo/psutil/issues/2658 +.. _2659: https://github.com/giampaolo/psutil/issues/2659 +.. _2660: https://github.com/giampaolo/psutil/issues/2660 +.. _2661: https://github.com/giampaolo/psutil/issues/2661 +.. _2662: https://github.com/giampaolo/psutil/issues/2662 +.. _2663: https://github.com/giampaolo/psutil/issues/2663 +.. _2664: https://github.com/giampaolo/psutil/issues/2664 +.. _2665: https://github.com/giampaolo/psutil/issues/2665 +.. _2666: https://github.com/giampaolo/psutil/issues/2666 +.. _2667: https://github.com/giampaolo/psutil/issues/2667 +.. _2668: https://github.com/giampaolo/psutil/issues/2668 +.. _2669: https://github.com/giampaolo/psutil/issues/2669 +.. _2670: https://github.com/giampaolo/psutil/issues/2670 +.. _2671: https://github.com/giampaolo/psutil/issues/2671 +.. _2672: https://github.com/giampaolo/psutil/issues/2672 +.. _2673: https://github.com/giampaolo/psutil/issues/2673 +.. _2674: https://github.com/giampaolo/psutil/issues/2674 +.. _2675: https://github.com/giampaolo/psutil/issues/2675 +.. _2676: https://github.com/giampaolo/psutil/issues/2676 +.. _2677: https://github.com/giampaolo/psutil/issues/2677 +.. _2678: https://github.com/giampaolo/psutil/issues/2678 +.. _2679: https://github.com/giampaolo/psutil/issues/2679 +.. _2680: https://github.com/giampaolo/psutil/issues/2680 +.. _2681: https://github.com/giampaolo/psutil/issues/2681 +.. _2682: https://github.com/giampaolo/psutil/issues/2682 +.. _2683: https://github.com/giampaolo/psutil/issues/2683 +.. _2684: https://github.com/giampaolo/psutil/issues/2684 +.. _2685: https://github.com/giampaolo/psutil/issues/2685 +.. _2686: https://github.com/giampaolo/psutil/issues/2686 +.. _2687: https://github.com/giampaolo/psutil/issues/2687 +.. _2688: https://github.com/giampaolo/psutil/issues/2688 +.. _2689: https://github.com/giampaolo/psutil/issues/2689 +.. _2690: https://github.com/giampaolo/psutil/issues/2690 +.. _2691: https://github.com/giampaolo/psutil/issues/2691 +.. _2692: https://github.com/giampaolo/psutil/issues/2692 +.. _2693: https://github.com/giampaolo/psutil/issues/2693 +.. _2694: https://github.com/giampaolo/psutil/issues/2694 +.. _2695: https://github.com/giampaolo/psutil/issues/2695 +.. _2696: https://github.com/giampaolo/psutil/issues/2696 +.. _2697: https://github.com/giampaolo/psutil/issues/2697 +.. _2698: https://github.com/giampaolo/psutil/issues/2698 +.. _2699: https://github.com/giampaolo/psutil/issues/2699 +.. _2700: https://github.com/giampaolo/psutil/issues/2700 +.. _2701: https://github.com/giampaolo/psutil/issues/2701 +.. _2702: https://github.com/giampaolo/psutil/issues/2702 +.. _2703: https://github.com/giampaolo/psutil/issues/2703 +.. _2704: https://github.com/giampaolo/psutil/issues/2704 +.. _2705: https://github.com/giampaolo/psutil/issues/2705 +.. _2706: https://github.com/giampaolo/psutil/issues/2706 +.. _2707: https://github.com/giampaolo/psutil/issues/2707 +.. _2708: https://github.com/giampaolo/psutil/issues/2708 +.. _2709: https://github.com/giampaolo/psutil/issues/2709 +.. _2710: https://github.com/giampaolo/psutil/issues/2710 +.. _2711: https://github.com/giampaolo/psutil/issues/2711 +.. _2712: https://github.com/giampaolo/psutil/issues/2712 +.. _2713: https://github.com/giampaolo/psutil/issues/2713 +.. _2714: https://github.com/giampaolo/psutil/issues/2714 +.. _2715: https://github.com/giampaolo/psutil/issues/2715 +.. _2716: https://github.com/giampaolo/psutil/issues/2716 +.. _2717: https://github.com/giampaolo/psutil/issues/2717 +.. _2718: https://github.com/giampaolo/psutil/issues/2718 +.. _2719: https://github.com/giampaolo/psutil/issues/2719 +.. _2720: https://github.com/giampaolo/psutil/issues/2720 +.. _2721: https://github.com/giampaolo/psutil/issues/2721 +.. _2722: https://github.com/giampaolo/psutil/issues/2722 +.. _2723: https://github.com/giampaolo/psutil/issues/2723 +.. _2724: https://github.com/giampaolo/psutil/issues/2724 +.. _2725: https://github.com/giampaolo/psutil/issues/2725 +.. _2726: https://github.com/giampaolo/psutil/issues/2726 +.. _2727: https://github.com/giampaolo/psutil/issues/2727 +.. _2728: https://github.com/giampaolo/psutil/issues/2728 +.. _2729: https://github.com/giampaolo/psutil/issues/2729 +.. _2730: https://github.com/giampaolo/psutil/issues/2730 +.. _2731: https://github.com/giampaolo/psutil/issues/2731 +.. _2732: https://github.com/giampaolo/psutil/issues/2732 +.. _2733: https://github.com/giampaolo/psutil/issues/2733 +.. _2734: https://github.com/giampaolo/psutil/issues/2734 +.. _2735: https://github.com/giampaolo/psutil/issues/2735 +.. _2736: https://github.com/giampaolo/psutil/issues/2736 +.. _2737: https://github.com/giampaolo/psutil/issues/2737 +.. _2738: https://github.com/giampaolo/psutil/issues/2738 +.. _2739: https://github.com/giampaolo/psutil/issues/2739 +.. _2740: https://github.com/giampaolo/psutil/issues/2740 +.. _2741: https://github.com/giampaolo/psutil/issues/2741 +.. _2742: https://github.com/giampaolo/psutil/issues/2742 +.. _2743: https://github.com/giampaolo/psutil/issues/2743 +.. _2744: https://github.com/giampaolo/psutil/issues/2744 +.. _2745: https://github.com/giampaolo/psutil/issues/2745 +.. _2746: https://github.com/giampaolo/psutil/issues/2746 +.. _2747: https://github.com/giampaolo/psutil/issues/2747 +.. _2748: https://github.com/giampaolo/psutil/issues/2748 +.. _2749: https://github.com/giampaolo/psutil/issues/2749 +.. _2750: https://github.com/giampaolo/psutil/issues/2750 +.. _2751: https://github.com/giampaolo/psutil/issues/2751 +.. _2752: https://github.com/giampaolo/psutil/issues/2752 +.. _2753: https://github.com/giampaolo/psutil/issues/2753 +.. _2754: https://github.com/giampaolo/psutil/issues/2754 +.. _2755: https://github.com/giampaolo/psutil/issues/2755 +.. _2756: https://github.com/giampaolo/psutil/issues/2756 +.. _2757: https://github.com/giampaolo/psutil/issues/2757 +.. _2758: https://github.com/giampaolo/psutil/issues/2758 +.. _2759: https://github.com/giampaolo/psutil/issues/2759 +.. _2760: https://github.com/giampaolo/psutil/issues/2760 +.. _2761: https://github.com/giampaolo/psutil/issues/2761 +.. _2762: https://github.com/giampaolo/psutil/issues/2762 +.. _2763: https://github.com/giampaolo/psutil/issues/2763 +.. _2764: https://github.com/giampaolo/psutil/issues/2764 +.. _2765: https://github.com/giampaolo/psutil/issues/2765 +.. _2766: https://github.com/giampaolo/psutil/issues/2766 +.. _2767: https://github.com/giampaolo/psutil/issues/2767 +.. _2768: https://github.com/giampaolo/psutil/issues/2768 +.. _2769: https://github.com/giampaolo/psutil/issues/2769 +.. _2770: https://github.com/giampaolo/psutil/issues/2770 +.. _2771: https://github.com/giampaolo/psutil/issues/2771 +.. _2772: https://github.com/giampaolo/psutil/issues/2772 +.. _2773: https://github.com/giampaolo/psutil/issues/2773 +.. _2774: https://github.com/giampaolo/psutil/issues/2774 +.. _2775: https://github.com/giampaolo/psutil/issues/2775 +.. _2776: https://github.com/giampaolo/psutil/issues/2776 +.. _2777: https://github.com/giampaolo/psutil/issues/2777 +.. _2778: https://github.com/giampaolo/psutil/issues/2778 +.. _2779: https://github.com/giampaolo/psutil/issues/2779 +.. _2780: https://github.com/giampaolo/psutil/issues/2780 +.. _2781: https://github.com/giampaolo/psutil/issues/2781 +.. _2782: https://github.com/giampaolo/psutil/issues/2782 +.. _2783: https://github.com/giampaolo/psutil/issues/2783 +.. _2784: https://github.com/giampaolo/psutil/issues/2784 +.. _2785: https://github.com/giampaolo/psutil/issues/2785 +.. _2786: https://github.com/giampaolo/psutil/issues/2786 +.. _2787: https://github.com/giampaolo/psutil/issues/2787 +.. _2788: https://github.com/giampaolo/psutil/issues/2788 +.. _2789: https://github.com/giampaolo/psutil/issues/2789 +.. _2790: https://github.com/giampaolo/psutil/issues/2790 +.. _2791: https://github.com/giampaolo/psutil/issues/2791 +.. _2792: https://github.com/giampaolo/psutil/issues/2792 +.. _2793: https://github.com/giampaolo/psutil/issues/2793 +.. _2794: https://github.com/giampaolo/psutil/issues/2794 +.. _2795: https://github.com/giampaolo/psutil/issues/2795 +.. _2796: https://github.com/giampaolo/psutil/issues/2796 +.. _2797: https://github.com/giampaolo/psutil/issues/2797 +.. _2798: https://github.com/giampaolo/psutil/issues/2798 +.. _2799: https://github.com/giampaolo/psutil/issues/2799 +.. _2800: https://github.com/giampaolo/psutil/issues/2800 +.. _2801: https://github.com/giampaolo/psutil/issues/2801 +.. _2802: https://github.com/giampaolo/psutil/issues/2802 +.. _2803: https://github.com/giampaolo/psutil/issues/2803 +.. _2804: https://github.com/giampaolo/psutil/issues/2804 +.. _2805: https://github.com/giampaolo/psutil/issues/2805 +.. _2806: https://github.com/giampaolo/psutil/issues/2806 +.. _2807: https://github.com/giampaolo/psutil/issues/2807 +.. _2808: https://github.com/giampaolo/psutil/issues/2808 +.. _2809: https://github.com/giampaolo/psutil/issues/2809 +.. _2810: https://github.com/giampaolo/psutil/issues/2810 +.. _2811: https://github.com/giampaolo/psutil/issues/2811 +.. _2812: https://github.com/giampaolo/psutil/issues/2812 +.. _2813: https://github.com/giampaolo/psutil/issues/2813 +.. _2814: https://github.com/giampaolo/psutil/issues/2814 +.. _2815: https://github.com/giampaolo/psutil/issues/2815 +.. _2816: https://github.com/giampaolo/psutil/issues/2816 +.. _2817: https://github.com/giampaolo/psutil/issues/2817 +.. _2818: https://github.com/giampaolo/psutil/issues/2818 +.. _2819: https://github.com/giampaolo/psutil/issues/2819 +.. _2820: https://github.com/giampaolo/psutil/issues/2820 +.. _2821: https://github.com/giampaolo/psutil/issues/2821 +.. _2822: https://github.com/giampaolo/psutil/issues/2822 +.. _2823: https://github.com/giampaolo/psutil/issues/2823 +.. _2824: https://github.com/giampaolo/psutil/issues/2824 +.. _2825: https://github.com/giampaolo/psutil/issues/2825 +.. _2826: https://github.com/giampaolo/psutil/issues/2826 +.. _2827: https://github.com/giampaolo/psutil/issues/2827 +.. _2828: https://github.com/giampaolo/psutil/issues/2828 +.. _2829: https://github.com/giampaolo/psutil/issues/2829 +.. _2830: https://github.com/giampaolo/psutil/issues/2830 +.. _2831: https://github.com/giampaolo/psutil/issues/2831 +.. _2832: https://github.com/giampaolo/psutil/issues/2832 +.. _2833: https://github.com/giampaolo/psutil/issues/2833 +.. _2834: https://github.com/giampaolo/psutil/issues/2834 +.. _2835: https://github.com/giampaolo/psutil/issues/2835 +.. _2836: https://github.com/giampaolo/psutil/issues/2836 +.. _2837: https://github.com/giampaolo/psutil/issues/2837 +.. _2838: https://github.com/giampaolo/psutil/issues/2838 +.. _2839: https://github.com/giampaolo/psutil/issues/2839 +.. _2840: https://github.com/giampaolo/psutil/issues/2840 +.. _2841: https://github.com/giampaolo/psutil/issues/2841 +.. _2842: https://github.com/giampaolo/psutil/issues/2842 +.. _2843: https://github.com/giampaolo/psutil/issues/2843 +.. _2844: https://github.com/giampaolo/psutil/issues/2844 +.. _2845: https://github.com/giampaolo/psutil/issues/2845 +.. _2846: https://github.com/giampaolo/psutil/issues/2846 +.. _2847: https://github.com/giampaolo/psutil/issues/2847 +.. _2848: https://github.com/giampaolo/psutil/issues/2848 +.. _2849: https://github.com/giampaolo/psutil/issues/2849 +.. _2850: https://github.com/giampaolo/psutil/issues/2850 +.. _2851: https://github.com/giampaolo/psutil/issues/2851 +.. _2852: https://github.com/giampaolo/psutil/issues/2852 +.. _2853: https://github.com/giampaolo/psutil/issues/2853 +.. _2854: https://github.com/giampaolo/psutil/issues/2854 +.. _2855: https://github.com/giampaolo/psutil/issues/2855 +.. _2856: https://github.com/giampaolo/psutil/issues/2856 +.. _2857: https://github.com/giampaolo/psutil/issues/2857 +.. _2858: https://github.com/giampaolo/psutil/issues/2858 +.. _2859: https://github.com/giampaolo/psutil/issues/2859 +.. _2860: https://github.com/giampaolo/psutil/issues/2860 +.. _2861: https://github.com/giampaolo/psutil/issues/2861 +.. _2862: https://github.com/giampaolo/psutil/issues/2862 +.. _2863: https://github.com/giampaolo/psutil/issues/2863 +.. _2864: https://github.com/giampaolo/psutil/issues/2864 +.. _2865: https://github.com/giampaolo/psutil/issues/2865 +.. _2866: https://github.com/giampaolo/psutil/issues/2866 +.. _2867: https://github.com/giampaolo/psutil/issues/2867 +.. _2868: https://github.com/giampaolo/psutil/issues/2868 +.. _2869: https://github.com/giampaolo/psutil/issues/2869 +.. _2870: https://github.com/giampaolo/psutil/issues/2870 +.. _2871: https://github.com/giampaolo/psutil/issues/2871 +.. _2872: https://github.com/giampaolo/psutil/issues/2872 +.. _2873: https://github.com/giampaolo/psutil/issues/2873 +.. _2874: https://github.com/giampaolo/psutil/issues/2874 +.. _2875: https://github.com/giampaolo/psutil/issues/2875 +.. _2876: https://github.com/giampaolo/psutil/issues/2876 +.. _2877: https://github.com/giampaolo/psutil/issues/2877 +.. _2878: https://github.com/giampaolo/psutil/issues/2878 +.. _2879: https://github.com/giampaolo/psutil/issues/2879 +.. _2880: https://github.com/giampaolo/psutil/issues/2880 +.. _2881: https://github.com/giampaolo/psutil/issues/2881 +.. _2882: https://github.com/giampaolo/psutil/issues/2882 +.. _2883: https://github.com/giampaolo/psutil/issues/2883 +.. _2884: https://github.com/giampaolo/psutil/issues/2884 +.. _2885: https://github.com/giampaolo/psutil/issues/2885 +.. _2886: https://github.com/giampaolo/psutil/issues/2886 +.. _2887: https://github.com/giampaolo/psutil/issues/2887 +.. _2888: https://github.com/giampaolo/psutil/issues/2888 +.. _2889: https://github.com/giampaolo/psutil/issues/2889 +.. _2890: https://github.com/giampaolo/psutil/issues/2890 +.. _2891: https://github.com/giampaolo/psutil/issues/2891 +.. _2892: https://github.com/giampaolo/psutil/issues/2892 +.. _2893: https://github.com/giampaolo/psutil/issues/2893 +.. _2894: https://github.com/giampaolo/psutil/issues/2894 +.. _2895: https://github.com/giampaolo/psutil/issues/2895 +.. _2896: https://github.com/giampaolo/psutil/issues/2896 +.. _2897: https://github.com/giampaolo/psutil/issues/2897 +.. _2898: https://github.com/giampaolo/psutil/issues/2898 +.. _2899: https://github.com/giampaolo/psutil/issues/2899 +.. _2900: https://github.com/giampaolo/psutil/issues/2900 +.. _2901: https://github.com/giampaolo/psutil/issues/2901 +.. _2902: https://github.com/giampaolo/psutil/issues/2902 +.. _2903: https://github.com/giampaolo/psutil/issues/2903 +.. _2904: https://github.com/giampaolo/psutil/issues/2904 +.. _2905: https://github.com/giampaolo/psutil/issues/2905 +.. _2906: https://github.com/giampaolo/psutil/issues/2906 +.. _2907: https://github.com/giampaolo/psutil/issues/2907 +.. _2908: https://github.com/giampaolo/psutil/issues/2908 +.. _2909: https://github.com/giampaolo/psutil/issues/2909 +.. _2910: https://github.com/giampaolo/psutil/issues/2910 +.. _2911: https://github.com/giampaolo/psutil/issues/2911 +.. _2912: https://github.com/giampaolo/psutil/issues/2912 +.. _2913: https://github.com/giampaolo/psutil/issues/2913 +.. _2914: https://github.com/giampaolo/psutil/issues/2914 +.. _2915: https://github.com/giampaolo/psutil/issues/2915 +.. _2916: https://github.com/giampaolo/psutil/issues/2916 +.. _2917: https://github.com/giampaolo/psutil/issues/2917 +.. _2918: https://github.com/giampaolo/psutil/issues/2918 +.. _2919: https://github.com/giampaolo/psutil/issues/2919 +.. _2920: https://github.com/giampaolo/psutil/issues/2920 +.. _2921: https://github.com/giampaolo/psutil/issues/2921 +.. _2922: https://github.com/giampaolo/psutil/issues/2922 +.. _2923: https://github.com/giampaolo/psutil/issues/2923 +.. _2924: https://github.com/giampaolo/psutil/issues/2924 +.. _2925: https://github.com/giampaolo/psutil/issues/2925 +.. _2926: https://github.com/giampaolo/psutil/issues/2926 +.. _2927: https://github.com/giampaolo/psutil/issues/2927 +.. _2928: https://github.com/giampaolo/psutil/issues/2928 +.. _2929: https://github.com/giampaolo/psutil/issues/2929 +.. _2930: https://github.com/giampaolo/psutil/issues/2930 +.. _2931: https://github.com/giampaolo/psutil/issues/2931 +.. _2932: https://github.com/giampaolo/psutil/issues/2932 +.. _2933: https://github.com/giampaolo/psutil/issues/2933 +.. _2934: https://github.com/giampaolo/psutil/issues/2934 +.. _2935: https://github.com/giampaolo/psutil/issues/2935 +.. _2936: https://github.com/giampaolo/psutil/issues/2936 +.. _2937: https://github.com/giampaolo/psutil/issues/2937 +.. _2938: https://github.com/giampaolo/psutil/issues/2938 +.. _2939: https://github.com/giampaolo/psutil/issues/2939 +.. _2940: https://github.com/giampaolo/psutil/issues/2940 +.. _2941: https://github.com/giampaolo/psutil/issues/2941 +.. _2942: https://github.com/giampaolo/psutil/issues/2942 +.. _2943: https://github.com/giampaolo/psutil/issues/2943 +.. _2944: https://github.com/giampaolo/psutil/issues/2944 +.. _2945: https://github.com/giampaolo/psutil/issues/2945 +.. _2946: https://github.com/giampaolo/psutil/issues/2946 +.. _2947: https://github.com/giampaolo/psutil/issues/2947 +.. _2948: https://github.com/giampaolo/psutil/issues/2948 +.. _2949: https://github.com/giampaolo/psutil/issues/2949 +.. _2950: https://github.com/giampaolo/psutil/issues/2950 +.. _2951: https://github.com/giampaolo/psutil/issues/2951 +.. _2952: https://github.com/giampaolo/psutil/issues/2952 +.. _2953: https://github.com/giampaolo/psutil/issues/2953 +.. _2954: https://github.com/giampaolo/psutil/issues/2954 +.. _2955: https://github.com/giampaolo/psutil/issues/2955 +.. _2956: https://github.com/giampaolo/psutil/issues/2956 +.. _2957: https://github.com/giampaolo/psutil/issues/2957 +.. _2958: https://github.com/giampaolo/psutil/issues/2958 +.. _2959: https://github.com/giampaolo/psutil/issues/2959 +.. _2960: https://github.com/giampaolo/psutil/issues/2960 +.. _2961: https://github.com/giampaolo/psutil/issues/2961 +.. _2962: https://github.com/giampaolo/psutil/issues/2962 +.. _2963: https://github.com/giampaolo/psutil/issues/2963 +.. _2964: https://github.com/giampaolo/psutil/issues/2964 +.. _2965: https://github.com/giampaolo/psutil/issues/2965 +.. _2966: https://github.com/giampaolo/psutil/issues/2966 +.. _2967: https://github.com/giampaolo/psutil/issues/2967 +.. _2968: https://github.com/giampaolo/psutil/issues/2968 +.. _2969: https://github.com/giampaolo/psutil/issues/2969 +.. _2970: https://github.com/giampaolo/psutil/issues/2970 +.. _2971: https://github.com/giampaolo/psutil/issues/2971 +.. _2972: https://github.com/giampaolo/psutil/issues/2972 +.. _2973: https://github.com/giampaolo/psutil/issues/2973 +.. _2974: https://github.com/giampaolo/psutil/issues/2974 +.. _2975: https://github.com/giampaolo/psutil/issues/2975 +.. _2976: https://github.com/giampaolo/psutil/issues/2976 +.. _2977: https://github.com/giampaolo/psutil/issues/2977 +.. _2978: https://github.com/giampaolo/psutil/issues/2978 +.. _2979: https://github.com/giampaolo/psutil/issues/2979 +.. _2980: https://github.com/giampaolo/psutil/issues/2980 +.. _2981: https://github.com/giampaolo/psutil/issues/2981 +.. _2982: https://github.com/giampaolo/psutil/issues/2982 +.. _2983: https://github.com/giampaolo/psutil/issues/2983 +.. _2984: https://github.com/giampaolo/psutil/issues/2984 +.. _2985: https://github.com/giampaolo/psutil/issues/2985 +.. _2986: https://github.com/giampaolo/psutil/issues/2986 +.. _2987: https://github.com/giampaolo/psutil/issues/2987 +.. _2988: https://github.com/giampaolo/psutil/issues/2988 +.. _2989: https://github.com/giampaolo/psutil/issues/2989 +.. _2990: https://github.com/giampaolo/psutil/issues/2990 +.. _2991: https://github.com/giampaolo/psutil/issues/2991 +.. _2992: https://github.com/giampaolo/psutil/issues/2992 +.. _2993: https://github.com/giampaolo/psutil/issues/2993 +.. _2994: https://github.com/giampaolo/psutil/issues/2994 +.. _2995: https://github.com/giampaolo/psutil/issues/2995 +.. _2996: https://github.com/giampaolo/psutil/issues/2996 +.. _2997: https://github.com/giampaolo/psutil/issues/2997 +.. _2998: https://github.com/giampaolo/psutil/issues/2998 +.. _2999: https://github.com/giampaolo/psutil/issues/2999 +.. _3000: https://github.com/giampaolo/psutil/issues/3000 diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 6ab4275089..3a446a62eb 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -279,8 +279,11 @@ psutil_get_environ(pid_t pid) { arg_ptr = procargs + sizeof(nargs); arg_ptr = memchr(arg_ptr, '\0', arg_end - arg_ptr); - if (arg_ptr == NULL || arg_ptr == arg_end) + if (arg_ptr == NULL || arg_ptr == arg_end) { + psutil_debug( + "(arg_ptr == NULL || arg_ptr == arg_end); set environ to empty"); goto empty; + } // skip ahead to the first argument for (; arg_ptr < arg_end; arg_ptr++) { diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 08f1664703..c9059e3363 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1413,11 +1413,13 @@ def test_weird_environ(self): 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. */ + // 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); From f1a54ad88527e0706fb8a88ad7daae80686acc62 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Dec 2021 22:26:13 +0100 Subject: [PATCH 0813/1714] pre-release Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 +- docs/index.rst | 4 ++++ psutil/tests/test_misc.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index ae2a584ee6..7ee1f9308a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 5.9.0 (IN DEVELOPMENT) ====================== -XXXX-XX-XX +2021-12-29 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 47b53b5146..2ac9c2ce3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2610,6 +2610,10 @@ Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. Timeline ======== +- 2021-12-29: + `5.9.0 `__ - + `what's new `__ - + `diff `__ - 2020-12-19: `5.8.0 `__ - `what's new `__ - diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 0362432c55..d946eb6288 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -366,6 +366,8 @@ def check(ret): 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): From c687f99789d8bdd7efa0b9b4787ae67cf184884b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Dec 2021 22:32:54 +0100 Subject: [PATCH 0814/1714] update HISTORY Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7ee1f9308a..6b16affa82 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,7 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.0 (IN DEVELOPMENT) -====================== +5.9.0 +===== 2021-12-29 From 36f45ee2e6ef751b52bcb868fdf6f09617eafdb5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 31 Dec 2021 11:04:16 +0100 Subject: [PATCH 0815/1714] move import on top of the file Signed-off-by: Giampaolo Rodola --- docs/index.rst | 2 +- psutil/_common.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 2ac9c2ce3b..ef9d3c26b0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2593,7 +2593,7 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= -* psutil 5.8.1 (2021-10): **MidnightBSD** +* psutil 5.9.0 (2021-12): **MidnightBSD** * psutil 5.8.0 (2020-12): **PyPy 2** on Windows * psutil 5.7.1 (2020-07): **Windows Nano** * psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support diff --git a/psutil/_common.py b/psutil/_common.py index 16d3b3b56e..bec8333f05 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -44,6 +44,8 @@ # 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', 0)) +if PSUTIL_DEBUG: + import inspect __all__ = [ # OS constants @@ -835,7 +837,6 @@ def print_color( def debug(msg): """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" if PSUTIL_DEBUG: - import inspect fname, lineno, func_name, lines, index = inspect.getframeinfo( inspect.currentframe().f_back) if isinstance(msg, Exception): From 10fbb4656a3ba010d8712e4fe0300cbdf4f29471 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 12:21:16 +0100 Subject: [PATCH 0816/1714] add @u93 to list of sponsors Signed-off-by: Giampaolo Rodola --- README.rst | 2 ++ docs/index.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 6ad07db238..36f707d9cc 100644 --- a/README.rst +++ b/README.rst @@ -144,6 +144,8 @@ Supporters + add your avatar diff --git a/docs/index.rst b/docs/index.rst index ef9d3c26b0..94f44307c2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -88,6 +88,8 @@ Supporters +
add your avatar From d0024aadb2fd9187ae0d3c8456fa705d1fb1814e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 12:22:37 +0100 Subject: [PATCH 0817/1714] add @u93 to list of sponsors Signed-off-by: Giampaolo Rodola --- docs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 94f44307c2..7c9e8ae3a9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -88,8 +88,7 @@ Supporters - +
add your avatar From 16e65265b6850e58d3363ddde2d5ea145e3c488a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 12:23:12 +0100 Subject: [PATCH 0818/1714] add @u93 to list of sponsors Signed-off-by: Giampaolo Rodola --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 36f707d9cc..e6f5cb2d77 100644 --- a/README.rst +++ b/README.rst @@ -144,8 +144,7 @@ Supporters - + add your avatar From 83d7067d7568f09ad95f094d17731e643a4a7ce6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 12:37:35 +0100 Subject: [PATCH 0819/1714] fix #2049: [Linux] cpu_freq returns current value in GHz but min/max in MHz Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 8 ++++++++ psutil/_pslinux.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6b16affa82..036563142f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,13 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.1 +===== + +**Bug fixes** + +- 2049_, [Linux]: `cpu_freq`_ erroneously returns ``curr`` value in GHz while + ``min`` and ``max`` are in MHz. + 5.9.0 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 5a35691207..ab1b6f1b3f 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -749,7 +749,7 @@ def cpu_freq(): 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] + curr = cpuinfo_freqs[i] * 1000 else: curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) if curr is None: From 24b560186482f02b830fbbbd09f9f09426d84114 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 12:55:07 +0100 Subject: [PATCH 0820/1714] fix #2048: str(psutil.Error) raise AttributeError Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/_common.py | 2 +- psutil/tests/test_misc.py | 11 ++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 036563142f..cef152367b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,8 @@ **Bug fixes** +- 2048_: ``AttributeError`` is raised if ``psutil.Error`` class is raised + manually and passed through ``str``. - 2049_, [Linux]: `cpu_freq`_ erroneously returns ``curr`` value in GHz while ``min`` and ``max`` are in MHz. diff --git a/psutil/_common.py b/psutil/_common.py index bec8333f05..540d2f2d82 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -299,7 +299,7 @@ def __str__(self): ["%s=%r" % (k, v) for k, v in info.items()]) else: details = None - return " ".join([x for x in (self.msg, details) if x]) + return " ".join([x for x in (getattr(self, "msg", ""), details) if x]) def __repr__(self): # invoked on `repr(Error)` diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index d946eb6288..00bac13bae 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -54,7 +54,7 @@ # =================================================================== -class TestMisc(PsutilTestCase): +class TestExceptionClasses(PsutilTestCase): def test_process__repr__(self, func=repr): p = psutil.Process(self.spawn_testproc().pid) @@ -95,6 +95,12 @@ def test_process__repr__(self, func=repr): 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)), @@ -161,6 +167,9 @@ def test_timeout_expired__str__(self): str(psutil.TimeoutExpired(5, pid=321, name="name")), "timeout after 5 seconds (pid=321, name='name')") + +class TestMisc(PsutilTestCase): + def test_process__eq__(self): p1 = psutil.Process() p2 = psutil.Process() From 9883d336a86a06b8eb445c8a333f507c330d9a56 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 13:23:25 +0100 Subject: [PATCH 0821/1714] move stuff around in test_misc.py Signed-off-by: Giampaolo Rodola --- psutil/tests/test_misc.py | 144 +++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 66 deletions(-) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 00bac13bae..3e10fee828 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -24,8 +24,10 @@ from psutil import POSIX from psutil import WINDOWS 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 @@ -50,11 +52,11 @@ # =================================================================== -# --- Misc / generic tests. +# --- Test classes' repr(), str(), ... # =================================================================== -class TestExceptionClasses(PsutilTestCase): +class TestSpecialMethods(PsutilTestCase): def test_process__repr__(self, func=repr): p = psutil.Process(self.spawn_testproc().pid) @@ -167,9 +169,6 @@ def test_timeout_expired__str__(self): str(psutil.TimeoutExpired(5, pid=321, name="name")), "timeout after 5 seconds (pid=321, name='name')") - -class TestMisc(PsutilTestCase): - def test_process__eq__(self): p1 = psutil.Process() p2 = psutil.Process() @@ -182,6 +181,14 @@ 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: @@ -217,6 +224,72 @@ def test_process_as_dict_no_new_names(self): 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 TestCommonModule(PsutilTestCase): + def test_memoize(self): @memoize def foo(*args, **kwargs): @@ -280,8 +353,6 @@ def foo(self): 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 @@ -336,7 +407,6 @@ def test_supports_ipv6(self): sock.close() 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)) @@ -352,64 +422,6 @@ def test_isfile_strict(self): 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()) - - # 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()) - def test_debug(self): if PY3: from io import StringIO From 93c670e0927d18b8fcd191d278fc0a885a3d8c6c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 13:29:16 +0100 Subject: [PATCH 0822/1714] use explicit 'raise self.fail(...)' in unit tests Signed-off-by: Giampaolo Rodola --- psutil/tests/test_bsd.py | 4 ++-- psutil/tests/test_linux.py | 2 +- psutil/tests/test_misc.py | 6 +++--- psutil/tests/test_posix.py | 4 ++-- psutil/tests/test_process.py | 16 +++++++++------- psutil/tests/test_system.py | 11 +++++------ psutil/tests/test_windows.py | 8 ++++---- 7 files changed, 26 insertions(+), 25 deletions(-) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 54b488bc75..1ae810f179 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -117,9 +117,9 @@ def df(path): 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)) + raise 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)) + 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): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 20e28d2991..f9d092d963 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1104,7 +1104,7 @@ def test_zfs_fs(self): if part.fstype == 'zfs': break else: - self.fail("couldn't find any ZFS partition") + 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")) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 3e10fee828..a13c295d5c 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -205,7 +205,7 @@ def test__all__(self): continue if (fun.__doc__ is not None and 'deprecated' not in fun.__doc__.lower()): - self.fail('%r not in psutil.__all__' % name) + 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 @@ -738,8 +738,8 @@ def test_coverage(self): 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)) + 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): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 31b819267b..edef7e7d44 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -308,7 +308,7 @@ def test_pids(self): 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)) + raise self.fail("difference: " + str(difference)) # for some reason ifconfig -a does not report all interfaces # returned by psutil @@ -322,7 +322,7 @@ def test_nic_names(self): if line.startswith(nic): break else: - self.fail( + raise self.fail( "couldn't find %s nic in 'ifconfig -a' output\n%s" % ( nic, output)) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index c9059e3363..2a2af93c69 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -261,10 +261,10 @@ def test_cpu_times_2(self): # 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)) + raise 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)) + raise self.fail("expected: %s, found: %s" % (ktime, kernel_time)) @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") def test_cpu_num(self): @@ -285,8 +285,8 @@ def test_create_time(self): # 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)) + 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())) @@ -983,7 +983,8 @@ def test_open_files_2(self): file.fd == fileobj.fileno(): break else: - self.fail("no file found; files=%s" % repr(p.open_files())) + 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) @@ -1020,7 +1021,8 @@ def test_num_ctx_switches(self): after = sum(p.num_ctx_switches()) if after > before: return - self.fail("num ctx switches still the same after 50.000 iterations") + raise self.fail( + "num ctx switches still the same after 50.000 iterations") def test_ppid(self): p = psutil.Process() @@ -1495,7 +1497,7 @@ def test_nice(self): except psutil.AccessDenied: pass else: - self.fail("exception not raised") + raise self.fail("exception not raised") @unittest.skipIf(1, "causes problem as root") def test_zombie_process(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index db2cb3488f..d98ec5c3d4 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -184,8 +184,7 @@ def test_pid_exists_2(self): # 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) + self.assertIn(pid, psutil.pids()) pids = range(max(pids) + 5000, max(pids) + 6000) for pid in pids: self.assertFalse(psutil.pid_exists(pid), msg=pid) @@ -280,10 +279,10 @@ def test_virtual_memory(self): self.assertIsInstance(value, (int, long)) if name != 'total': if not value >= 0: - self.fail("%r < 0 (%s)" % (name, value)) + raise self.fail("%r < 0 (%s)" % (name, value)) if value > mem.total: - self.fail("%r > total (total=%s, %s=%s)" - % (name, mem.total, name, value)) + raise self.fail("%r > total (total=%s, %s=%s)" + % (name, mem.total, name, value)) def test_swap_memory(self): mem = psutil.swap_memory() @@ -376,7 +375,7 @@ def test_cpu_times_time_increases(self): t2 = sum(psutil.cpu_times()) if t2 > t1: return - self.fail("time remained the same") + raise self.fail("time remained the same") def test_per_cpu_times(self): # Check type, value >= 0, str(). diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index ea694be4a4..55b6bc7b13 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -114,7 +114,7 @@ def test_nic_names(self): if "pseudo-interface" in nic.replace(' ', '-').lower(): continue if nic not in out: - self.fail( + raise self.fail( "%r nic wasn't found in 'ipconfig /all' output" % nic) def test_total_phymem(self): @@ -168,11 +168,11 @@ def test_disks(self): self.assertEqual(usage.free, wmi_free) # 10 MB tollerance if abs(usage.free - wmi_free) > 10 * 1024 * 1024: - self.fail("psutil=%s, wmi=%s" % ( + raise self.fail("psutil=%s, wmi=%s" % ( usage.free, wmi_free)) break else: - self.fail("can't find partition %s" % repr(ps_part)) + raise self.fail("can't find partition %s" % repr(ps_part)) @retry_on_failure() def test_disk_usage(self): @@ -539,7 +539,7 @@ def test_memory_vms(self): # 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)) + raise self.fail("wmi=%s, psutil=%s" % (wmi_usage, vms)) def test_create_time(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] From 860e157a9439c4ef354fb2e85fd8dd310fa754da Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Jan 2022 14:48:47 +0100 Subject: [PATCH 0823/1714] 3.6 wheels Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 2 +- appveyor.yml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8940cd059..f9c0a435d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test - CIBW_BUILD: 'cp37-* cp38-* cp39-* cp310-*' + CIBW_BUILD: 'cp36-* cp37-* cp38-* cp39-* cp310-*' CIBW_SKIP: '*-musllinux_*' steps: diff --git a/appveyor.yml b/appveyor.yml index 485182d319..83e570ae12 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,6 +24,10 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.x" + PYTHON_ARCH: "32" + - PYTHON: "C:\\Python37" PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "32" @@ -48,6 +52,10 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + PYTHON_ARCH: "64" + - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "64" From 46cb6c212a870b36bd0af17c48dd29f53468734b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Jan 2022 11:49:44 +0100 Subject: [PATCH 0824/1714] [Linux] cat/bcat utils refactoring (#2053) --- psutil/_common.py | 24 ++++++++++++ psutil/_pslinux.py | 77 +++++++++++++++---------------------- psutil/tests/test_linux.py | 9 ----- psutil/tests/test_misc.py | 14 +++++++ psutil/tests/test_system.py | 2 +- 5 files changed, 69 insertions(+), 57 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 540d2f2d82..8030e2e01b 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -46,6 +46,7 @@ PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG', 0)) if PSUTIL_DEBUG: import inspect +_DEFAULT = object() __all__ = [ # OS constants @@ -72,6 +73,7 @@ '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', @@ -727,6 +729,28 @@ def open_text(fname, **kwargs): return open(fname, "rt", **kwargs) +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 diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ab1b6f1b3f..9291d8c238 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -31,6 +31,8 @@ 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 @@ -78,7 +80,6 @@ HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get") HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get") -_DEFAULT = object() # Number of clock ticks per second CLOCK_TICKS = os.sysconf("SC_CLK_TCK") @@ -283,22 +284,6 @@ 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: # pragma: no cover @@ -751,17 +736,17 @@ def cpu_freq(): # https://github.com/giampaolo/psutil/issues/1851 curr = cpuinfo_freqs[i] * 1000 else: - curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) + 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") 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 @@ -1349,9 +1334,9 @@ def sensors_temperatures(): 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) + 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 @@ -1363,9 +1348,9 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/1323 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: @@ -1388,9 +1373,9 @@ def sensors_temperatures(): for base in basenames: try: path = os.path.join(base, 'temp') - current = float(cat(path)) / 1000.0 + current = float(bcat(path)) / 1000.0 path = os.path.join(base, 'type') - unit_name = cat(path, binary=False) + unit_name = cat(path).strip() except (IOError, OSError, ValueError) as err: debug(err) continue @@ -1402,13 +1387,13 @@ def sensors_temperatures(): high = None for trip_point in trip_points: path = os.path.join(base, trip_point + "_type") - trip_type = cat(path, fallback='', binary=False) + trip_type = cat(path, fallback='').strip() if trip_type == 'critical': - critical = cat(os.path.join(base, trip_point + "_temp"), - fallback=None) + critical = bcat(os.path.join(base, trip_point + "_temp"), + fallback=None) elif trip_type == 'high': - high = cat(os.path.join(base, trip_point + "_temp"), - fallback=None) + high = bcat(os.path.join(base, trip_point + "_temp"), + fallback=None) if high is not None: try: @@ -1446,13 +1431,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: 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')) + label = cat(base + '_label', fallback='') ret[unit_name].append(_common.sfan(label, current)) return dict(ret) @@ -1467,12 +1451,12 @@ def sensors_battery(): """ 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: try: return int(ret) @@ -1490,16 +1474,16 @@ def multi_cat(*paths): root = os.path.join(POWER_SUPPLY_PATH, sorted(bats)[0]) # Base metrics. - energy_now = multi_cat( + energy_now = multi_bcat( root + "/energy_now", root + "/charge_now") - power_now = multi_cat( + power_now = multi_bcat( root + "/power_now", root + "/current_now") - energy_full = multi_cat( + energy_full = multi_bcat( root + "/energy_full", root + "/charge_full") - time_to_empty = multi_cat(root + "/time_to_empty_now") + 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). @@ -1517,13 +1501,13 @@ 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")) 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"): @@ -1700,8 +1684,7 @@ def _parse_stat_file(self): 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 ")". diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index f9d092d963..b69840be19 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -2250,15 +2250,6 @@ def test_readlink(self): self.assertEqual(psutil._psplatform.readlink("bar"), "foo") assert m.called - def test_cat(self): - testfn = self.get_testfn() - with open(testfn, "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_from_name diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index a13c295d5c..aa30cbd6c0 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -23,6 +23,8 @@ 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 @@ -31,6 +33,7 @@ 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 @@ -451,6 +454,17 @@ def test_debug(self): self.assertIn("no such file", msg) self.assertIn("/foo", msg) + def test_cat_bcat(self): + testfn = self.get_testfn() + with open(testfn, "wt") 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. diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index d98ec5c3d4..ed328d017f 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -184,7 +184,7 @@ def test_pid_exists_2(self): # in case the process disappeared in meantime fail only # if it is no longer in psutil.pids() time.sleep(.1) - self.assertIn(pid, psutil.pids()) + self.assertNotIn(pid, psutil.pids()) pids = range(max(pids) + 5000, max(pids) + 6000) for pid in pids: self.assertFalse(psutil.pid_exists(pid), msg=pid) From 18348c9b6b66ff12abbbfeaebbe2218cdc6b5556 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Jan 2022 11:57:09 +0100 Subject: [PATCH 0825/1714] bump up ver + strip() some cat() results Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 6 ++++-- psutil/__init__.py | 2 +- psutil/_pslinux.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index cef152367b..b1af019982 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.1 -===== +5.9.1 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX **Bug fixes** diff --git a/psutil/__init__.py b/psutil/__init__.py index 1a113bc3a8..6deebb2633 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.0" +__version__ = "5.9.1" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 9291d8c238..447d67326f 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1435,8 +1435,8 @@ def sensors_fans(): except (IOError, OSError) as err: debug(err) continue - unit_name = cat(os.path.join(os.path.dirname(base), 'name')) - label = cat(base + '_label', fallback='') + 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) @@ -1461,7 +1461,7 @@ def multi_bcat(*paths): try: return int(ret) except ValueError: - return ret + return ret.strip() return None bats = [x for x in os.listdir(POWER_SUPPLY_PATH) if x.startswith('BAT') or From 83b1a846f4bf0b6c95be9950d01456d13401c92f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Jan 2022 13:30:44 +0100 Subject: [PATCH 0826/1714] fix failing test on py 2.7 Signed-off-by: Giampaolo Rodola --- psutil/tests/test_linux.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index b69840be19..2f8c40b0d7 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1911,9 +1911,10 @@ def test_open_files_enametoolong(self): patch_point = 'psutil._pslinux.os.readlink' with mock.patch(patch_point, side_effect=OSError(errno.ENAMETOOLONG, "")) as m: - files = p.open_files() - assert not files - assert m.called + with mock.patch("psutil._common.debug"): + files = p.open_files() + assert not files + assert m.called # --- mocked tests @@ -2155,8 +2156,9 @@ def test_connections_enametoolong(self): with mock.patch('psutil._pslinux.os.readlink', side_effect=OSError(errno.ENAMETOOLONG, "")) as m: p = psutil.Process() - assert not p.connections() - assert m.called + with mock.patch("psutil._common.debug"): + assert not p.connections() + assert m.called @unittest.skipIf(not LINUX, "LINUX only") From 55161bd4850986359a029f1c9a81bcf66f37afa8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Jan 2022 15:02:21 +0100 Subject: [PATCH 0827/1714] [Linux]: increase `read(2)` buffer size when reading /proc files lines (#2054) This should help having more consistent results. --- HISTORY.rst | 8 ++++++++ psutil/_common.py | 51 +++++++++++++++++++++++++++++++++++----------- psutil/_pslinux.py | 11 +++------- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b1af019982..b19c714265 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,12 +5,20 @@ XXXX-XX-XX +**Enhancements** + +- 2050_, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading + ``/proc`` pseudo files line by line. This should help having more consistent + results. + **Bug fixes** - 2048_: ``AttributeError`` is raised if ``psutil.Error`` class is raised manually and passed through ``str``. - 2049_, [Linux]: `cpu_freq`_ erroneously returns ``curr`` value in GHz while ``min`` and ``max`` are in MHz. +- 2050_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` if running in a + LCX container. 5.9.0 ===== diff --git a/psutil/_common.py b/psutil/_common.py index 8030e2e01b..1e33699c59 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -711,22 +711,49 @@ def wrap_numbers(input_dict, name): wrap_numbers.cache_info = _wn.cache_info -def open_binary(fname, **kwargs): - return open(fname, "rb", **kwargs) - - -def open_text(fname, **kwargs): +# 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 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 not PY3: + return open(fname, "rt", buffering=FILE_READ_BUFFER_SIZE) + + # See: + # https://github.com/giampaolo/psutil/issues/675 + # https://github.com/giampaolo/psutil/pull/733 + fobj = open(fname, "rt", 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): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 447d67326f..5f149a035d 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -85,10 +85,6 @@ CLOCK_TICKS = os.sysconf("SC_CLK_TCK") 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 @@ -904,7 +900,7 @@ 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: @@ -941,7 +937,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() @@ -1720,8 +1716,7 @@ def _read_status_file(self): @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): From d135e2c3a99bfd1bd01d1158ec31dbc8dc1c910f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Jan 2022 22:21:39 +0100 Subject: [PATCH 0828/1714] fix repr(Error): PID was not shown if PID == 0 Signed-off-by: Giampaolo Rodola --- psutil/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index 1e33699c59..c94a8378aa 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -289,7 +289,7 @@ def _infodict(self, attrs): info = {} # Python 2.6 for name in attrs: value = getattr(self, name, None) - if value: + if value is not None: info[name] = value return info From 7803582ae7a331e3a0be367cae377c92f294148d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Jan 2022 12:41:25 +0100 Subject: [PATCH 0829/1714] fix py 2.7 failures Signed-off-by: Giampaolo Rodola --- README.rst | 6 +----- psutil/_common.py | 4 +++- psutil/tests/test_linux.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index e6f5cb2d77..29bbc3f127 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -| |downloads| |stars| |forks| |contributors| |coverage| |quality| +| |downloads| |stars| |forks| |contributors| |coverage| | |version| |py-versions| |packages| |license| | |github-actions| |appveyor| |doc| |twitter| |tidelift| @@ -18,10 +18,6 @@ :target: https://github.com/giampaolo/psutil/graphs/contributors :alt: Contributors -.. |quality| image:: https://img.shields.io/codacy/grade/ce63e7f7f69d44b5b59682196e6fbfca.svg - :target: https://www.codacy.com/app/g-rodola/psutil?utm_source=github.com&utm_medium=referral&utm_content=giampaolo/psutil&utm_campaign=Badge_Grade - :alt: Code quality - .. |github-actions| image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild :alt: Linux, macOS, Windows tests diff --git a/psutil/_common.py b/psutil/_common.py index c94a8378aa..00a9c6fdf1 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -289,7 +289,9 @@ def _infodict(self, attrs): info = {} # Python 2.6 for name in attrs: value = getattr(self, name, None) - if value is not None: + if value: + info[name] = value + elif name == "pid" and value == 0: info[name] = value return info diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 2f8c40b0d7..e36fb8745d 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1911,7 +1911,7 @@ def test_open_files_enametoolong(self): patch_point = 'psutil._pslinux.os.readlink' with mock.patch(patch_point, side_effect=OSError(errno.ENAMETOOLONG, "")) as m: - with mock.patch("psutil._common.debug"): + with mock.patch("psutil._pslinux.debug"): files = p.open_files() assert not files assert m.called @@ -2156,7 +2156,7 @@ def test_connections_enametoolong(self): with mock.patch('psutil._pslinux.os.readlink', side_effect=OSError(errno.ENAMETOOLONG, "")) as m: p = psutil.Process() - with mock.patch("psutil._common.debug"): + with mock.patch("psutil._pslinux.debug"): assert not p.connections() assert m.called From 01f5f1ecb1b3470c1df2a120f0f6dca7f58088fb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Jan 2022 19:06:05 +0100 Subject: [PATCH 0830/1714] OpenBSD files refactoring (#2056) --- MANIFEST.in | 10 +- psutil/_psutil_bsd.c | 5 +- psutil/arch/openbsd/cpu.c | 91 ++++++++ psutil/arch/openbsd/cpu.h | 11 + psutil/arch/openbsd/disk.c | 67 ++++++ psutil/arch/openbsd/disk.h | 10 + psutil/arch/openbsd/mem.c | 118 ++++++++++ psutil/arch/openbsd/mem.h | 11 + psutil/arch/openbsd/{specific.c => proc.c} | 253 --------------------- psutil/arch/openbsd/{specific.h => proc.h} | 9 +- setup.py | 5 +- 11 files changed, 326 insertions(+), 264 deletions(-) create mode 100644 psutil/arch/openbsd/cpu.c create mode 100644 psutil/arch/openbsd/cpu.h create mode 100644 psutil/arch/openbsd/disk.c create mode 100644 psutil/arch/openbsd/disk.h create mode 100644 psutil/arch/openbsd/mem.c create mode 100644 psutil/arch/openbsd/mem.h rename psutil/arch/openbsd/{specific.c => proc.c} (68%) rename psutil/arch/openbsd/{specific.h => proc.h} (67%) diff --git a/MANIFEST.in b/MANIFEST.in index 4a10365d61..5b4eb69ad8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -61,8 +61,14 @@ include psutil/arch/netbsd/socks.c include psutil/arch/netbsd/socks.h include psutil/arch/netbsd/specific.c include psutil/arch/netbsd/specific.h -include psutil/arch/openbsd/specific.c -include psutil/arch/openbsd/specific.h +include psutil/arch/openbsd/cpu.c +include psutil/arch/openbsd/cpu.h +include psutil/arch/openbsd/disk.c +include psutil/arch/openbsd/disk.h +include psutil/arch/openbsd/mem.c +include psutil/arch/openbsd/mem.h +include psutil/arch/openbsd/proc.c +include psutil/arch/openbsd/proc.h include psutil/arch/osx/cpu.c include psutil/arch/osx/cpu.h include psutil/arch/osx/process_info.c diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 873ebbc927..80efe6a614 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -77,7 +77,10 @@ #include #endif #elif PSUTIL_OPENBSD - #include "arch/openbsd/specific.h" + #include "arch/openbsd/cpu.h" + #include "arch/openbsd/disk.h" + #include "arch/openbsd/mem.h" + #include "arch/openbsd/proc.h" #include #include // for VREG diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c new file mode 100644 index 0000000000..2a366fe01a --- /dev/null +++ b/psutil/arch/openbsd/cpu.c @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include +#include +#include // for CPUSTATES & CP_* + + +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + int mib[3]; + int ncpu; + size_t len; + size_t size; + int i; + PyObject *py_retlist = PyList_New(0); + PyObject *py_cputime = NULL; + + if (py_retlist == NULL) + return NULL; + + // retrieve the number of cpus + mib[0] = CTL_HW; + mib[1] = HW_NCPU; + len = sizeof(ncpu); + if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + uint64_t cpu_time[CPUSTATES]; + + for (i = 0; i < ncpu; i++) { + mib[0] = CTL_KERN; + mib[1] = KERN_CPTIME2; + mib[2] = i; + size = sizeof(cpu_time); + if (sysctl(mib, 3, &cpu_time, &size, NULL, 0) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + py_cputime = Py_BuildValue( + "(ddddd)", + (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC); + if (!py_cputime) + goto error; + if (PyList_Append(py_retlist, py_cputime)) + goto error; + Py_DECREF(py_cputime); + } + + return py_retlist; + +error: + Py_XDECREF(py_cputime); + Py_DECREF(py_retlist); + return NULL; +} + + +PyObject * +psutil_cpu_stats(PyObject *self, PyObject *args) { + size_t size; + struct uvmexp uv; + int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; + + size = sizeof(uv); + if (sysctl(uvmexp_mib, 2, &uv, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return Py_BuildValue( + "IIIIIII", + uv.swtch, // ctx switches + uv.intrs, // interrupts - XXX always 0, will be determined via /proc + uv.softs, // soft interrupts + uv.syscalls, // syscalls - XXX always 0 + uv.traps, // traps + uv.faults, // faults + uv.forks // forks + ); +} diff --git a/psutil/arch/openbsd/cpu.h b/psutil/arch/openbsd/cpu.h new file mode 100644 index 0000000000..6bace0b18a --- /dev/null +++ b/psutil/arch/openbsd/cpu.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +PyObject *psutil_cpu_stats(PyObject* self, PyObject* args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/disk.c b/psutil/arch/openbsd/disk.c new file mode 100644 index 0000000000..e533a082c0 --- /dev/null +++ b/psutil/arch/openbsd/disk.c @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include +#include +#include + + +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + int i, dk_ndrive, mib[3]; + size_t len; + struct diskstats *stats = NULL; + + PyObject *py_retdict = PyDict_New(); + PyObject *py_disk_info = NULL; + if (py_retdict == NULL) + return NULL; + + mib[0] = CTL_HW; + mib[1] = HW_DISKSTATS; + len = 0; + if (sysctl(mib, 2, NULL, &len, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + dk_ndrive = (int)(len / sizeof(struct diskstats)); + + stats = malloc(len); + if (stats == NULL) { + PyErr_NoMemory(); + goto error; + } + if (sysctl(mib, 2, stats, &len, NULL, 0) < 0 ) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (i = 0; i < dk_ndrive; i++) { + py_disk_info = Py_BuildValue( + "(KKKK)", + stats[i].ds_rxfer, // num reads + stats[i].ds_wxfer, // num writes + stats[i].ds_rbytes, // read bytes + stats[i].ds_wbytes // write bytes + ); + if (!py_disk_info) + goto error; + if (PyDict_SetItemString(py_retdict, stats[i].ds_name, py_disk_info)) + goto error; + Py_DECREF(py_disk_info); + } + + free(stats); + return py_retdict; + +error: + Py_XDECREF(py_disk_info); + Py_DECREF(py_retdict); + if (stats != NULL) + free(stats); + return NULL; +} diff --git a/psutil/arch/openbsd/disk.h b/psutil/arch/openbsd/disk.h new file mode 100644 index 0000000000..b6348dd316 --- /dev/null +++ b/psutil/arch/openbsd/disk.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +PyObject *psutil_disk_io_counters(PyObject* self, PyObject* args); diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c new file mode 100644 index 0000000000..c6cab6189c --- /dev/null +++ b/psutil/arch/openbsd/mem.c @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include +#include +#include +#include +#include +#include + +#include "../../_psutil_posix.h" + + +PyObject * +psutil_virtual_mem(PyObject *self, PyObject *args) { + int64_t total_physmem; + int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; + int bcstats_mib[] = {CTL_VFS, VFS_GENERIC, VFS_BCACHESTAT}; + int physmem_mib[] = {CTL_HW, HW_PHYSMEM64}; + int vmmeter_mib[] = {CTL_VM, VM_METER}; + size_t size; + struct uvmexp uvmexp; + struct bcachestats bcstats; + struct vmtotal vmdata; + long pagesize = psutil_getpagesize(); + + size = sizeof(total_physmem); + if (sysctl(physmem_mib, 2, &total_physmem, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + size = sizeof(uvmexp); + if (sysctl(uvmexp_mib, 2, &uvmexp, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + size = sizeof(bcstats); + if (sysctl(bcstats_mib, 3, &bcstats, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + size = sizeof(vmdata); + if (sysctl(vmmeter_mib, 2, &vmdata, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return Py_BuildValue("KKKKKKKK", + // Note: many programs calculate total memory as + // "uvmexp.npages * pagesize" but this is incorrect and does not + // match "sysctl | grep hw.physmem". + (unsigned long long) total_physmem, + (unsigned long long) uvmexp.free * pagesize, + (unsigned long long) uvmexp.active * pagesize, + (unsigned long long) uvmexp.inactive * pagesize, + (unsigned long long) uvmexp.wired * pagesize, + // this is how "top" determines it + (unsigned long long) bcstats.numbufpages * pagesize, // cached + (unsigned long long) 0, // buffers + (unsigned long long) vmdata.t_vmshr + vmdata.t_rmshr // shared + ); +} + + +PyObject * +psutil_swap_mem(PyObject *self, PyObject *args) { + uint64_t swap_total, swap_free; + struct swapent *swdev; + int nswap, i; + + if ((nswap = swapctl(SWAP_NSWAP, 0, 0)) == 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + if ((swdev = calloc(nswap, sizeof(*swdev))) == NULL) { + PyErr_NoMemory(); + return NULL; + } + + if (swapctl(SWAP_STATS, swdev, nswap) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + // Total things up. + swap_total = swap_free = 0; + for (i = 0; i < nswap; i++) { + if (swdev[i].se_flags & SWF_ENABLE) { + swap_free += (swdev[i].se_nblks - swdev[i].se_inuse); + swap_total += swdev[i].se_nblks; + } + } + + free(swdev); + return Py_BuildValue( + "(LLLII)", + swap_total * DEV_BSIZE, + (swap_total - swap_free) * DEV_BSIZE, + swap_free * DEV_BSIZE, + // swap in / swap out is not supported as the + // swapent struct does not provide any info + // about it. + 0, + 0 + ); + +error: + free(swdev); + return NULL; +} diff --git a/psutil/arch/openbsd/mem.h b/psutil/arch/openbsd/mem.h new file mode 100644 index 0000000000..1de3f3c434 --- /dev/null +++ b/psutil/arch/openbsd/mem.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/proc.c similarity index 68% rename from psutil/arch/openbsd/specific.c rename to psutil/arch/openbsd/proc.c index cdd4f62314..35acab252b 100644 --- a/psutil/arch/openbsd/specific.c +++ b/psutil/arch/openbsd/proc.c @@ -3,8 +3,6 @@ * All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. - * - * Platform-specific module methods for OpenBSD. */ #include @@ -19,21 +17,14 @@ #include #include #include -#include // for VFS_* -#include // for swap_mem -#include // for vmtotal struct #include #include -// connection stuff #include // for NI_MAXHOST #include -#include // for CPUSTATES & CP_* #define _KERNEL // for DTYPE_* #include #undef _KERNEL -#include // struct diskstats #include // for inet_ntoa() -#include // for warn() & err() #include "../../_psutil_common.h" #include "../../_psutil_posix.h" @@ -286,106 +277,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -PyObject * -psutil_virtual_mem(PyObject *self, PyObject *args) { - int64_t total_physmem; - int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; - int bcstats_mib[] = {CTL_VFS, VFS_GENERIC, VFS_BCACHESTAT}; - int physmem_mib[] = {CTL_HW, HW_PHYSMEM64}; - int vmmeter_mib[] = {CTL_VM, VM_METER}; - size_t size; - struct uvmexp uvmexp; - struct bcachestats bcstats; - struct vmtotal vmdata; - long pagesize = psutil_getpagesize(); - - size = sizeof(total_physmem); - if (sysctl(physmem_mib, 2, &total_physmem, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - size = sizeof(uvmexp); - if (sysctl(uvmexp_mib, 2, &uvmexp, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - size = sizeof(bcstats); - if (sysctl(bcstats_mib, 3, &bcstats, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - size = sizeof(vmdata); - if (sysctl(vmmeter_mib, 2, &vmdata, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - return Py_BuildValue("KKKKKKKK", - // Note: many programs calculate total memory as - // "uvmexp.npages * pagesize" but this is incorrect and does not - // match "sysctl | grep hw.physmem". - (unsigned long long) total_physmem, - (unsigned long long) uvmexp.free * pagesize, - (unsigned long long) uvmexp.active * pagesize, - (unsigned long long) uvmexp.inactive * pagesize, - (unsigned long long) uvmexp.wired * pagesize, - // this is how "top" determines it - (unsigned long long) bcstats.numbufpages * pagesize, // cached - (unsigned long long) 0, // buffers - (unsigned long long) vmdata.t_vmshr + vmdata.t_rmshr // shared - ); -} - - -PyObject * -psutil_swap_mem(PyObject *self, PyObject *args) { - uint64_t swap_total, swap_free; - struct swapent *swdev; - int nswap, i; - - if ((nswap = swapctl(SWAP_NSWAP, 0, 0)) == 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - if ((swdev = calloc(nswap, sizeof(*swdev))) == NULL) { - PyErr_NoMemory(); - return NULL; - } - - if (swapctl(SWAP_STATS, swdev, nswap) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - // Total things up. - swap_total = swap_free = 0; - for (i = 0; i < nswap; i++) { - if (swdev[i].se_flags & SWF_ENABLE) { - swap_free += (swdev[i].se_nblks - swdev[i].se_inuse); - swap_total += swdev[i].se_nblks; - } - } - - free(swdev); - return Py_BuildValue("(LLLII)", - swap_total * DEV_BSIZE, - (swap_total - swap_free) * DEV_BSIZE, - swap_free * DEV_BSIZE, - // swap in / swap out is not supported as the - // swapent struct does not provide any info - // about it. - 0, 0); - -error: - free(swdev); - return NULL; -} - - PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { pid_t pid; @@ -640,147 +531,3 @@ psutil_proc_connections(PyObject *self, PyObject *args) { free(tcplist); return NULL; } - - -PyObject * -psutil_per_cpu_times(PyObject *self, PyObject *args) { - int mib[3]; - int ncpu; - size_t len; - size_t size; - int i; - PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; - - if (py_retlist == NULL) - return NULL; - - - // retrieve the number of cpus - mib[0] = CTL_HW; - mib[1] = HW_NCPU; - len = sizeof(ncpu); - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - uint64_t cpu_time[CPUSTATES]; - - for (i = 0; i < ncpu; i++) { - // per-cpu info - mib[0] = CTL_KERN; - mib[1] = KERN_CPTIME2; - mib[2] = i; - size = sizeof(cpu_time); - if (sysctl(mib, 3, &cpu_time, &size, NULL, 0) == -1) { - warn("failed to get kern.cptime2"); - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - py_cputime = Py_BuildValue( - "(ddddd)", - (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC); - if (!py_cputime) - goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_DECREF(py_cputime); - } - - return py_retlist; - -error: - Py_XDECREF(py_cputime); - Py_DECREF(py_retlist); - return NULL; -} - - -PyObject * -psutil_disk_io_counters(PyObject *self, PyObject *args) { - int i, dk_ndrive, mib[3]; - size_t len; - struct diskstats *stats = NULL; - - PyObject *py_retdict = PyDict_New(); - PyObject *py_disk_info = NULL; - if (py_retdict == NULL) - return NULL; - - mib[0] = CTL_HW; - mib[1] = HW_DISKSTATS; - len = 0; - if (sysctl(mib, 2, NULL, &len, NULL, 0) < 0) { - warn("can't get hw.diskstats size"); - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - dk_ndrive = (int)(len / sizeof(struct diskstats)); - - stats = malloc(len); - if (stats == NULL) { - warn("can't malloc"); - PyErr_NoMemory(); - goto error; - } - if (sysctl(mib, 2, stats, &len, NULL, 0) < 0 ) { - warn("could not read hw.diskstats"); - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (i = 0; i < dk_ndrive; i++) { - py_disk_info = Py_BuildValue( - "(KKKK)", - stats[i].ds_rxfer, // num reads - stats[i].ds_wxfer, // num writes - stats[i].ds_rbytes, // read bytes - stats[i].ds_wbytes // write bytes - ); - if (!py_disk_info) - goto error; - if (PyDict_SetItemString(py_retdict, stats[i].ds_name, py_disk_info)) - goto error; - Py_DECREF(py_disk_info); - } - - free(stats); - return py_retdict; - -error: - Py_XDECREF(py_disk_info); - Py_DECREF(py_retdict); - if (stats != NULL) - free(stats); - return NULL; -} - - -PyObject * -psutil_cpu_stats(PyObject *self, PyObject *args) { - size_t size; - struct uvmexp uv; - int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; - - size = sizeof(uv); - if (sysctl(uvmexp_mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - return Py_BuildValue( - "IIIIIII", - uv.swtch, // ctx switches - uv.intrs, // interrupts - XXX always 0, will be determined via /proc - uv.softs, // soft interrupts - uv.syscalls, // syscalls - XXX always 0 - uv.traps, // traps - uv.faults, // faults - uv.forks // forks - ); -} diff --git a/psutil/arch/openbsd/specific.h b/psutil/arch/openbsd/proc.h similarity index 67% rename from psutil/arch/openbsd/specific.h rename to psutil/arch/openbsd/proc.h index b8170a0223..5ca5890d2a 100644 --- a/psutil/arch/openbsd/specific.h +++ b/psutil/arch/openbsd/proc.h @@ -13,15 +13,10 @@ int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); char **_psutil_get_argv(pid_t pid); -PyObject * psutil_get_cmdline(pid_t pid); -// +PyObject *psutil_get_cmdline(pid_t pid); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_connections(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); -PyObject* psutil_disk_io_counters(PyObject* self, PyObject* args); -PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); + diff --git a/setup.py b/setup.py index 7521d08e1d..e0bb5d095d 100755 --- a/setup.py +++ b/setup.py @@ -220,7 +220,10 @@ def get_winver(): 'psutil._psutil_bsd', sources=sources + [ 'psutil/_psutil_bsd.c', - 'psutil/arch/openbsd/specific.c', + 'psutil/arch/openbsd/cpu.c', + 'psutil/arch/openbsd/disk.c', + 'psutil/arch/openbsd/mem.c', + 'psutil/arch/openbsd/proc.c', ], define_macros=macros, libraries=["kvm"]) From 98b49486f81f085bceda60b01b0a8f69a7008290 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Jan 2022 20:06:14 +0100 Subject: [PATCH 0831/1714] OpenBSD: add support for CPU frequency (#2057) --- HISTORY.rst | 1 + docs/index.rst | 8 +++-- psutil/_psbsd.py | 54 +++++++++++++++++++--------------- psutil/_psutil_bsd.c | 7 +++-- psutil/arch/openbsd/cpu.c | 18 ++++++++++++ psutil/arch/openbsd/cpu.h | 1 + psutil/tests/test_contracts.py | 2 +- 7 files changed, 60 insertions(+), 31 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b19c714265..595de97548 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,7 @@ XXXX-XX-XX - 2050_, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading ``/proc`` pseudo files line by line. This should help having more consistent results. +- 2057_, [OpenBSD]: add support for `cpu_freq()`_. **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 7c9e8ae3a9..6df7ccefb1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -269,11 +269,11 @@ CPU Return CPU frequency as a named tuple including *current*, *min* and *max* frequencies expressed in Mhz. On Linux *current* frequency reports the real-time value, on all other - platforms it represents the nominal "fixed" value. + platforms this usually represents the nominal "fixed" value (never changing). If *percpu* is ``True`` and the system supports per-cpu frequency retrieval (Linux only) a list of frequencies is returned for each CPU, if not, a list with a single element is returned. - If *min* and *max* cannot be determined they are set to ``0``. + If *min* and *max* cannot be determined they are set to ``0.0``. Example (Linux): @@ -288,12 +288,14 @@ CPU scpufreq(current=1703.609, min=800.0, max=3500.0), scpufreq(current=1754.289, min=800.0, max=3500.0)] - Availability: Linux, macOS, Windows, FreeBSD + Availability: Linux, macOS, Windows, FreeBSD, OpenBSD .. versionadded:: 5.1.0 .. versionchanged:: 5.5.1 added FreeBSD support. + .. versionchanged:: 5.9.1 added OpenBSD support. + .. function:: getloadavg() Return the average system load over the last 1, 5 and 15 minutes as a tuple. diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 528850657f..c4200cce15 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -310,6 +310,36 @@ def 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 # ===================================================================== @@ -439,30 +469,6 @@ def sensors_temperatures(): return ret - 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_frequency(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 - # ===================================================================== # --- other system functions diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 80efe6a614..732a560f23 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1133,6 +1133,10 @@ static PyMethodDef mod_methods[] = { "Return currently connected users as a list of tuples"}, {"cpu_stats", psutil_cpu_stats, METH_VARARGS, "Return CPU statistics"}, +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) + {"cpu_freq", psutil_cpu_freq, METH_VARARGS, + "Return CPU frequency"}, +#endif #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) {"net_connections", psutil_net_connections, METH_VARARGS, "Return system-wide open connections."}, @@ -1142,10 +1146,7 @@ static PyMethodDef mod_methods[] = { "Return battery information."}, {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS, "Return temperature information for a given CPU core number."}, - {"cpu_frequency", psutil_cpu_freq, METH_VARARGS, - "Return frequency of a given CPU"}, #endif - // --- others {"set_debug", psutil_set_debug, METH_VARARGS, "Enable or disable PSUTIL_DEBUG messages"}, diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c index 2a366fe01a..0691fd1ff0 100644 --- a/psutil/arch/openbsd/cpu.c +++ b/psutil/arch/openbsd/cpu.c @@ -89,3 +89,21 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { uv.forks // forks ); } + + +PyObject * +psutil_cpu_freq(PyObject *self, PyObject *args) { + int freq; + size_t size; + int mib[2] = {CTL_HW, HW_CPUSPEED}; + + // On VirtualBox I get "sysctl hw.cpuspeed=2593" (never changing), + // which appears to be expressed in Mhz. + size = sizeof(freq); + if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return Py_BuildValue("i", freq); +} diff --git a/psutil/arch/openbsd/cpu.h b/psutil/arch/openbsd/cpu.h index 6bace0b18a..07bf95fd82 100644 --- a/psutil/arch/openbsd/cpu.h +++ b/psutil/arch/openbsd/cpu.h @@ -7,5 +7,6 @@ #include +PyObject *psutil_cpu_freq(PyObject* self, PyObject* args); PyObject *psutil_cpu_stats(PyObject* self, PyObject* args); PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 7401cc15ec..9def408a90 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -134,7 +134,7 @@ def test_win_service_get(self): def test_cpu_freq(self): self.assertEqual(hasattr(psutil, "cpu_freq"), - LINUX or MACOS or WINDOWS or FREEBSD) + LINUX or MACOS or WINDOWS or FREEBSD or OPENBSD) def test_sensors_temperatures(self): self.assertEqual( From 6173da20bc7e697167e25d672b2d20f5e4b9bc4d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Jan 2022 21:35:04 +0100 Subject: [PATCH 0832/1714] Remove docstrings from C function definitions (#2058) --- psutil/_psutil_aix.c | 63 +++++---------- psutil/_psutil_bsd.c | 104 ++++++++---------------- psutil/_psutil_linux.c | 31 +++----- psutil/_psutil_osx.c | 87 +++++++------------- psutil/_psutil_posix.c | 21 ++--- psutil/_psutil_sunos.c | 63 +++++---------- psutil/_psutil_windows.c | 166 +++++++++++++-------------------------- 7 files changed, 175 insertions(+), 360 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index ba638641e9..f5c5cc66bb 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -989,58 +989,37 @@ static PyMethodDef PsutilMethods[] = { // --- process-related functions - {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS, - "Return process ppid, rss, vms, ctime, nice, nthreads, status and tty"}, - {"proc_name", psutil_proc_name, METH_VARARGS, - "Return process name."}, - {"proc_args", psutil_proc_args, METH_VARARGS, - "Return process command line arguments."}, - {"proc_environ", psutil_proc_environ, METH_VARARGS, - "Return process environment variables."}, - {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, - "Return process user and system CPU times."}, - {"proc_cred", psutil_proc_cred, METH_VARARGS, - "Return process uids/gids."}, + {"proc_args", psutil_proc_args, METH_VARARGS}, + {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS}, + {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS}, + {"proc_cred", psutil_proc_cred, METH_VARARGS}, + {"proc_environ", psutil_proc_environ, METH_VARARGS}, + {"proc_name", psutil_proc_name, METH_VARARGS}, #ifdef CURR_VERSION_THREAD - {"proc_threads", psutil_proc_threads, METH_VARARGS, - "Return process threads"}, + {"proc_threads", psutil_proc_threads, METH_VARARGS}, #endif #ifdef CURR_VERSION_PROCESS - {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS, - "Get process I/O counters."}, + {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS}, #endif - {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS, - "Get process I/O counters."}, + {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS}, // --- system-related functions - {"users", psutil_users, METH_VARARGS, - "Return currently connected users."}, - {"disk_partitions", psutil_disk_partitions, METH_VARARGS, - "Return disk partitions."}, - {"boot_time", psutil_boot_time, METH_VARARGS, - "Return system boot time in seconds since the EPOCH."}, - {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS, - "Return system per-cpu times as a list of tuples"}, - {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS, - "Return a Python dict of tuples for disk I/O statistics."}, - {"virtual_mem", psutil_virtual_mem, METH_VARARGS, - "Return system virtual memory usage statistics"}, - {"swap_mem", psutil_swap_mem, METH_VARARGS, - "Return stats about swap memory, in bytes"}, + {"boot_time", psutil_boot_time, METH_VARARGS}, + {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, + {"swap_mem", psutil_swap_mem, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, #if defined(CURR_VERSION_NETINTERFACE) && CURR_VERSION_NETINTERFACE >= 3 - {"net_io_counters", psutil_net_io_counters, METH_VARARGS, - "Return a Python dict of tuples for network I/O statistics."}, + {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, #endif - {"net_connections", psutil_net_connections, METH_VARARGS, - "Return system-wide connections"}, - {"net_if_stats", psutil_net_if_stats, METH_VARARGS, - "Return NIC stats (isup, mtu)"}, - {"cpu_stats", psutil_cpu_stats, METH_VARARGS, - "Return CPU statistics"}, + {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, + {"net_connections", psutil_net_connections, METH_VARARGS}, + {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, // --- others - {"set_debug", psutil_set_debug, METH_VARARGS, - "Enable or disable PSUTIL_DEBUG messages"}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 732a560f23..131708380e 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1063,93 +1063,57 @@ psutil_users(PyObject *self, PyObject *args) { static PyMethodDef mod_methods[] = { // --- per-process functions - {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS, - "Return multiple info about a process"}, - {"proc_name", psutil_proc_name, METH_VARARGS, - "Return process name"}, - {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS, - "Return process cmdline as a list of cmdline arguments"}, - {"proc_threads", psutil_proc_threads, METH_VARARGS, - "Return process threads"}, + {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, + {"proc_name", psutil_proc_name, METH_VARARGS}, + {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS}, + {"proc_threads", psutil_proc_threads, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) - {"proc_connections", psutil_proc_connections, METH_VARARGS, - "Return connections opened by process"}, + {"proc_connections", psutil_proc_connections, METH_VARARGS}, #endif - {"proc_cwd", psutil_proc_cwd, METH_VARARGS, - "Return process current working directory."}, + {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, #if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) - {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS, - "Return the number of file descriptors opened by this process"}, - {"proc_open_files", psutil_proc_open_files, METH_VARARGS, - "Return files opened by process as a list of (path, fd) tuples"}, + {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, + {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) - {"proc_num_threads", psutil_proc_num_threads, METH_VARARGS, - "Return number of threads used by process"}, + {"proc_num_threads", psutil_proc_num_threads, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) - {"proc_exe", psutil_proc_exe, METH_VARARGS, - "Return process pathname executable"}, - {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS, - "Return a list of tuples for every process's memory map"}, - {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS, - "Return process CPU affinity."}, - {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS, - "Set process CPU affinity."}, - {"proc_getrlimit", psutil_proc_getrlimit, METH_VARARGS, - "Get process resource limits."}, - {"proc_setrlimit", psutil_proc_setrlimit, METH_VARARGS, - "Set process resource limits."}, - {"cpu_topology", psutil_cpu_topology, METH_VARARGS, - "Return CPU topology as an XML string."}, + {"cpu_topology", psutil_cpu_topology, METH_VARARGS}, + {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, + {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, + {"proc_exe", psutil_proc_exe, METH_VARARGS}, + {"proc_getrlimit", psutil_proc_getrlimit, METH_VARARGS}, + {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, + {"proc_setrlimit", psutil_proc_setrlimit, METH_VARARGS}, #endif - {"proc_environ", psutil_proc_environ, METH_VARARGS, - "Return process environment"}, + {"proc_environ", psutil_proc_environ, METH_VARARGS}, // --- system-related functions - - {"pids", psutil_pids, METH_VARARGS, - "Returns a list of PIDs currently running on the system"}, - {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS, - "Return number of logical CPUs on the system"}, - {"virtual_mem", psutil_virtual_mem, METH_VARARGS, - "Return system virtual memory usage statistics"}, - {"swap_mem", psutil_swap_mem, METH_VARARGS, - "Return swap mem stats"}, - {"cpu_times", psutil_cpu_times, METH_VARARGS, - "Return system cpu times as a tuple (user, system, nice, idle, irc)"}, - {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS, - "Return system per-cpu times as a list of tuples"}, - {"boot_time", psutil_boot_time, METH_VARARGS, - "Return the system boot time expressed in seconds since the epoch."}, - {"disk_partitions", psutil_disk_partitions, METH_VARARGS, - "Return a list of tuples including device, mount point and " - "fs type for all partitions mounted on the system."}, - {"net_io_counters", psutil_net_io_counters, METH_VARARGS, - "Return dict of tuples of networks I/O information."}, - {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS, - "Return a Python dict of tuples for disk I/O information"}, - {"users", psutil_users, METH_VARARGS, - "Return currently connected users as a list of tuples"}, - {"cpu_stats", psutil_cpu_stats, METH_VARARGS, - "Return CPU statistics"}, + {"boot_time", psutil_boot_time, METH_VARARGS}, + {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, + {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, + {"cpu_times", psutil_cpu_times, METH_VARARGS}, + {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, + {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, + {"pids", psutil_pids, METH_VARARGS}, + {"swap_mem", psutil_swap_mem, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) - {"cpu_freq", psutil_cpu_freq, METH_VARARGS, - "Return CPU frequency"}, + {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) - {"net_connections", psutil_net_connections, METH_VARARGS, - "Return system-wide open connections."}, + {"net_connections", psutil_net_connections, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) - {"sensors_battery", psutil_sensors_battery, METH_VARARGS, - "Return battery information."}, - {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS, - "Return temperature information for a given CPU core number."}, + {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, + {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS}, #endif // --- others - {"set_debug", psutil_set_debug, METH_VARARGS, - "Enable or disable PSUTIL_DEBUG messages"}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index bb21361085..70cf5d1bb2 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -476,35 +476,22 @@ static PyMethodDef mod_methods[] = { // --- per-process functions #if PSUTIL_HAVE_IOPRIO - {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS, - "Get process I/O priority"}, - {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS, - "Set process I/O priority"}, + {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS}, + {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS}, #endif #ifdef PSUTIL_HAVE_CPU_AFFINITY - {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS, - "Return process CPU affinity as a Python long (the bitmask)."}, - {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS, - "Set process CPU affinity; expects a bitmask."}, + {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, + {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, #endif - // --- system related functions - - {"disk_partitions", psutil_disk_partitions, METH_VARARGS, - "Return disk mounted partitions as a list of tuples including " - "device, mount point and filesystem type"}, - {"users", psutil_users, METH_VARARGS, - "Return currently connected users as a list of tuples"}, - {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, - "Return duplex and speed info about a NIC"}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, + {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, // --- linux specific - - {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS, - "A wrapper around sysinfo(), return system memory usage statistics"}, + {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS}, // --- others - {"set_debug", psutil_set_debug, METH_VARARGS, - "Enable or disable PSUTIL_DEBUG messages"}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 87ec495b29..2470c3ebd3 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1645,69 +1645,38 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { */ static PyMethodDef mod_methods[] = { // --- per-process functions - - {"proc_kinfo_oneshot", psutil_proc_kinfo_oneshot, METH_VARARGS, - "Return multiple process info."}, - {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS, - "Return multiple process info."}, - {"proc_name", psutil_proc_name, METH_VARARGS, - "Return process name"}, - {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS, - "Return process cmdline as a list of cmdline arguments"}, - {"proc_environ", psutil_proc_environ, METH_VARARGS, - "Return process environment data"}, - {"proc_exe", psutil_proc_exe, METH_VARARGS, - "Return path of the process executable"}, - {"proc_cwd", psutil_proc_cwd, METH_VARARGS, - "Return process current working directory."}, - {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS, - "Return process USS memory"}, - {"proc_threads", psutil_proc_threads, METH_VARARGS, - "Return process threads as a list of tuples"}, - {"proc_open_files", psutil_proc_open_files, METH_VARARGS, - "Return files opened by process as a list of tuples"}, - {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS, - "Return the number of fds opened by process."}, - {"proc_connections", psutil_proc_connections, METH_VARARGS, - "Get process TCP and UDP connections as a list of tuples"}, + {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, + {"proc_connections", psutil_proc_connections, METH_VARARGS}, + {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, + {"proc_environ", psutil_proc_environ, METH_VARARGS}, + {"proc_exe", psutil_proc_exe, METH_VARARGS}, + {"proc_kinfo_oneshot", psutil_proc_kinfo_oneshot, METH_VARARGS}, + {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, + {"proc_name", psutil_proc_name, METH_VARARGS}, + {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, + {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, + {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS}, + {"proc_threads", psutil_proc_threads, METH_VARARGS}, // --- system-related functions - - {"pids", psutil_pids, METH_VARARGS, - "Returns a list of PIDs currently running on the system"}, - {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS, - "Return number of logical CPUs on the system"}, - {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, - "Return number of CPU cores on the system"}, - {"virtual_mem", psutil_virtual_mem, METH_VARARGS, - "Return system virtual memory stats"}, - {"swap_mem", psutil_swap_mem, METH_VARARGS, - "Return stats about swap memory, in bytes"}, - {"cpu_times", psutil_cpu_times, METH_VARARGS, - "Return system cpu times as a tuple (user, system, nice, idle, irc)"}, - {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS, - "Return system per-cpu times as a list of tuples"}, - {"cpu_freq", psutil_cpu_freq, METH_VARARGS, - "Return cpu current frequency"}, - {"boot_time", psutil_boot_time, METH_VARARGS, - "Return the system boot time expressed in seconds since the epoch."}, - {"disk_partitions", psutil_disk_partitions, METH_VARARGS, - "Return a list of tuples including device, mount point and " - "fs type for all partitions mounted on the system."}, - {"net_io_counters", psutil_net_io_counters, METH_VARARGS, - "Return dict of tuples of networks I/O information."}, - {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS, - "Return dict of tuples of disks I/O information."}, - {"users", psutil_users, METH_VARARGS, - "Return currently connected users as a list of tuples"}, - {"cpu_stats", psutil_cpu_stats, METH_VARARGS, - "Return CPU statistics"}, - {"sensors_battery", psutil_sensors_battery, METH_VARARGS, - "Return battery information."}, + {"boot_time", psutil_boot_time, METH_VARARGS}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, + {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, + {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, + {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, + {"cpu_times", psutil_cpu_times, METH_VARARGS}, + {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, + {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, + {"pids", psutil_pids, METH_VARARGS}, + {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, + {"swap_mem", psutil_swap_mem, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, // --- others - {"set_debug", psutil_set_debug, METH_VARARGS, - "Enable or disable PSUTIL_DEBUG messages"}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 7c0d548c4c..045366145a 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -664,21 +664,14 @@ extern "C" { * define the psutil C module methods and initialize the module. */ static PyMethodDef mod_methods[] = { - {"getpriority", psutil_posix_getpriority, METH_VARARGS, - "Return process priority"}, - {"setpriority", psutil_posix_setpriority, METH_VARARGS, - "Set process priority"}, - {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS, - "Retrieve NICs information"}, - {"net_if_mtu", psutil_net_if_mtu, METH_VARARGS, - "Retrieve NIC MTU"}, - {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS, - "Return True if the NIC is running."}, - {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS, - "Return memory page size."}, + {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS}, + {"getpriority", psutil_posix_getpriority, METH_VARARGS}, + {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, + {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS}, + {"net_if_mtu", psutil_net_if_mtu, METH_VARARGS}, + {"setpriority", psutil_posix_setpriority, METH_VARARGS}, #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, - "Return NIC stats."}, + {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, #endif {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 2e0bd94310..42a1ffe821 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1635,52 +1635,31 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { static PyMethodDef PsutilMethods[] = { // --- process-related functions - {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS, - "Return process ppid, rss, vms, ctime, nice, nthreads, status and tty"}, - {"proc_name_and_args", psutil_proc_name_and_args, METH_VARARGS, - "Return process name and args."}, - {"proc_environ", psutil_proc_environ, METH_VARARGS, - "Return process environment."}, - {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, - "Return process user and system CPU times."}, - {"proc_cred", psutil_proc_cred, METH_VARARGS, - "Return process uids/gids."}, - {"query_process_thread", psutil_proc_query_thread, METH_VARARGS, - "Return info about a process thread"}, - {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS, - "Return process memory mappings"}, - {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS, - "Return the number of context switches performed by process"}, - {"proc_cpu_num", psutil_proc_cpu_num, METH_VARARGS, - "Return what CPU the process is on"}, + {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS}, + {"proc_cpu_num", psutil_proc_cpu_num, METH_VARARGS}, + {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS}, + {"proc_cred", psutil_proc_cred, METH_VARARGS}, + {"proc_environ", psutil_proc_environ, METH_VARARGS}, + {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, + {"proc_name_and_args", psutil_proc_name_and_args, METH_VARARGS}, + {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS}, + {"query_process_thread", psutil_proc_query_thread, METH_VARARGS}, // --- system-related functions - {"swap_mem", psutil_swap_mem, METH_VARARGS, - "Return information about system swap memory."}, - {"users", psutil_users, METH_VARARGS, - "Return currently connected users."}, - {"disk_partitions", psutil_disk_partitions, METH_VARARGS, - "Return disk partitions."}, - {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS, - "Return system per-CPU times."}, - {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS, - "Return a Python dict of tuples for disk I/O statistics."}, - {"net_io_counters", psutil_net_io_counters, METH_VARARGS, - "Return a Python dict of tuples for network I/O statistics."}, - {"boot_time", psutil_boot_time, METH_VARARGS, - "Return system boot time in seconds since the EPOCH."}, - {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, - "Return the number of CPU cores on the system."}, - {"net_connections", psutil_net_connections, METH_VARARGS, - "Return TCP and UDP syste-wide open connections."}, - {"net_if_stats", psutil_net_if_stats, METH_VARARGS, - "Return NIC stats (isup, duplex, speed, mtu)"}, - {"cpu_stats", psutil_cpu_stats, METH_VARARGS, - "Return CPU statistics"}, + {"boot_time", psutil_boot_time, METH_VARARGS}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, + {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, + {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"net_connections", psutil_net_connections, METH_VARARGS}, + {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, + {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, + {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, + {"swap_mem", psutil_swap_mem, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, // --- others - {"set_debug", psutil_set_debug, METH_VARARGS, - "Enable or disable PSUTIL_DEBUG messages"}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index f9ec237670..83da3a26c1 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1555,127 +1555,71 @@ static PyMethodDef PsutilMethods[] = { // --- per-process functions {"proc_cmdline", (PyCFunction)(void(*)(void))psutil_proc_cmdline, - METH_VARARGS | METH_KEYWORDS, - "Return process cmdline as a list of cmdline arguments"}, - {"proc_environ", psutil_proc_environ, METH_VARARGS, - "Return process environment data"}, - {"proc_exe", psutil_proc_exe, METH_VARARGS, - "Return path of the process executable"}, - {"proc_kill", psutil_proc_kill, METH_VARARGS, - "Kill the process identified by the given PID"}, - {"proc_times", psutil_proc_times, METH_VARARGS, - "Return tuple of user/kern time for the given PID"}, - {"proc_memory_info", psutil_proc_memory_info, METH_VARARGS, - "Return a tuple of process memory information"}, - {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS, - "Return the USS of the process"}, - {"proc_cwd", psutil_proc_cwd, METH_VARARGS, - "Return process current working directory"}, - {"proc_suspend_or_resume", psutil_proc_suspend_or_resume, METH_VARARGS, - "Suspend or resume a process"}, - {"proc_open_files", psutil_proc_open_files, METH_VARARGS, - "Return files opened by process"}, - {"proc_username", psutil_proc_username, METH_VARARGS, - "Return the username of a process"}, - {"proc_threads", psutil_proc_threads, METH_VARARGS, - "Return process threads information as a list of tuple"}, - {"proc_wait", psutil_proc_wait, METH_VARARGS, - "Wait for process to terminate and return its exit code."}, - {"proc_priority_get", psutil_proc_priority_get, METH_VARARGS, - "Return process priority."}, - {"proc_priority_set", psutil_proc_priority_set, METH_VARARGS, - "Set process priority."}, - {"proc_io_priority_get", psutil_proc_io_priority_get, METH_VARARGS, - "Return process IO priority."}, - {"proc_io_priority_set", psutil_proc_io_priority_set, METH_VARARGS, - "Set process IO priority."}, - {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS, - "Return process CPU affinity as a bitmask."}, - {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS, - "Set process CPU affinity."}, - {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS, - "Get process I/O counters."}, - {"proc_is_suspended", psutil_proc_is_suspended, METH_VARARGS, - "Return True if one of the process threads is in a suspended state"}, - {"proc_num_handles", psutil_proc_num_handles, METH_VARARGS, - "Return the number of handles opened by process."}, - {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS, - "Return a list of process's memory mappings"}, + METH_VARARGS | METH_KEYWORDS}, + {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, + {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, + {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, + {"proc_environ", psutil_proc_environ, METH_VARARGS}, + {"proc_exe", psutil_proc_exe, METH_VARARGS}, + {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS}, + {"proc_io_priority_get", psutil_proc_io_priority_get, METH_VARARGS}, + {"proc_io_priority_set", psutil_proc_io_priority_set, METH_VARARGS}, + {"proc_is_suspended", psutil_proc_is_suspended, METH_VARARGS}, + {"proc_kill", psutil_proc_kill, METH_VARARGS}, + {"proc_memory_info", psutil_proc_memory_info, METH_VARARGS}, + {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, + {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, + {"proc_num_handles", psutil_proc_num_handles, METH_VARARGS}, + {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, + {"proc_priority_get", psutil_proc_priority_get, METH_VARARGS}, + {"proc_priority_set", psutil_proc_priority_set, METH_VARARGS}, + {"proc_suspend_or_resume", psutil_proc_suspend_or_resume, METH_VARARGS}, + {"proc_threads", psutil_proc_threads, METH_VARARGS}, + {"proc_times", psutil_proc_times, METH_VARARGS}, + {"proc_username", psutil_proc_username, METH_VARARGS}, + {"proc_wait", psutil_proc_wait, METH_VARARGS}, // --- alternative pinfo interface - {"proc_info", psutil_proc_info, METH_VARARGS, - "Various process information"}, + {"proc_info", psutil_proc_info, METH_VARARGS}, // --- system-related functions - {"pids", psutil_pids, METH_VARARGS, - "Returns a list of PIDs currently running on the system"}, - {"ppid_map", psutil_ppid_map, METH_VARARGS, - "Return a {pid:ppid, ...} dict for all running processes"}, - {"pid_exists", psutil_pid_exists, METH_VARARGS, - "Determine if the process exists in the current process list."}, - {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS, - "Returns the number of logical CPUs on the system"}, - {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS, - "Returns the number of CPU cores on the system"}, - {"boot_time", psutil_boot_time, METH_VARARGS, - "Return the system boot time expressed in seconds since the epoch."}, - {"virtual_mem", psutil_virtual_mem, METH_VARARGS, - "Return the total amount of physical memory, in bytes"}, - {"cpu_times", psutil_cpu_times, METH_VARARGS, - "Return system cpu times as a list"}, - {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS, - "Return system per-cpu times as a list of tuples"}, - {"disk_usage", psutil_disk_usage, METH_VARARGS, - "Return path's disk total and free as a Python tuple."}, - {"net_io_counters", psutil_net_io_counters, METH_VARARGS, - "Return dict of tuples of networks I/O information."}, - {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS, - "Return dict of tuples of disks I/O information."}, - {"users", psutil_users, METH_VARARGS, - "Return a list of currently connected users."}, - {"disk_partitions", psutil_disk_partitions, METH_VARARGS, - "Return disk partitions."}, - {"net_connections", psutil_net_connections, METH_VARARGS, - "Return system-wide connections"}, - {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS, - "Return NICs addresses."}, - {"net_if_stats", psutil_net_if_stats, METH_VARARGS, - "Return NICs stats."}, - {"cpu_stats", psutil_cpu_stats, METH_VARARGS, - "Return NICs stats."}, - {"cpu_freq", psutil_cpu_freq, METH_VARARGS, - "Return CPU frequency."}, - {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, - METH_VARARGS, - "Initializes the emulated load average calculator."}, - {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS, - "Returns the emulated POSIX-like load average."}, - {"sensors_battery", psutil_sensors_battery, METH_VARARGS, - "Return battery metrics usage."}, - {"getpagesize", psutil_getpagesize, METH_VARARGS, - "Return system memory page size."}, + {"boot_time", psutil_boot_time, METH_VARARGS}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, + {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, + {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, + {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, + {"cpu_times", psutil_cpu_times, METH_VARARGS}, + {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"disk_usage", psutil_disk_usage, METH_VARARGS}, + {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS}, + {"getpagesize", psutil_getpagesize, METH_VARARGS}, + {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, METH_VARARGS}, + {"net_connections", psutil_net_connections, METH_VARARGS}, + {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, + {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, + {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, + {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, + {"pid_exists", psutil_pid_exists, METH_VARARGS}, + {"pids", psutil_pids, METH_VARARGS}, + {"ppid_map", psutil_ppid_map, METH_VARARGS}, + {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, // --- windows services - {"winservice_enumerate", psutil_winservice_enumerate, METH_VARARGS, - "List all services"}, - {"winservice_query_config", psutil_winservice_query_config, METH_VARARGS, - "Return service config"}, - {"winservice_query_status", psutil_winservice_query_status, METH_VARARGS, - "Return service config"}, - {"winservice_query_descr", psutil_winservice_query_descr, METH_VARARGS, - "Return the description of a service"}, - {"winservice_start", psutil_winservice_start, METH_VARARGS, - "Start a service"}, - {"winservice_stop", psutil_winservice_stop, METH_VARARGS, - "Stop a service"}, + {"winservice_enumerate", psutil_winservice_enumerate, METH_VARARGS}, + {"winservice_query_config", psutil_winservice_query_config, METH_VARARGS}, + {"winservice_query_descr", psutil_winservice_query_descr, METH_VARARGS}, + {"winservice_query_status", psutil_winservice_query_status, METH_VARARGS}, + {"winservice_start", psutil_winservice_start, METH_VARARGS}, + {"winservice_stop", psutil_winservice_stop, METH_VARARGS}, // --- windows API bindings - {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS, - "QueryDosDevice binding"}, + {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS}, // --- others - {"set_debug", psutil_set_debug, METH_VARARGS, - "Enable or disable PSUTIL_DEBUG messages"}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; From f716afc81d4c9ded08b23d376612e1491d3ea5da Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Jan 2022 23:52:11 +0100 Subject: [PATCH 0833/1714] FreeBSD files refactoring (#2059) --- MANIFEST.in | 10 +- docs/DEVNOTES | 1 + psutil/_psutil_bsd.c | 11 +- psutil/arch/freebsd/cpu.c | 64 ++++ psutil/arch/freebsd/cpu.h | 7 +- psutil/arch/freebsd/disk.c | 86 ++++++ psutil/arch/freebsd/disk.h | 9 + psutil/arch/freebsd/mem.c | 137 +++++++++ psutil/arch/freebsd/mem.h | 10 + psutil/arch/freebsd/{specific.c => proc.c} | 323 +-------------------- psutil/arch/freebsd/{specific.h => proc.h} | 6 - psutil/arch/freebsd/sensors.c | 82 ++++++ psutil/arch/freebsd/sensors.h | 10 + setup.py | 5 +- 14 files changed, 420 insertions(+), 341 deletions(-) create mode 100644 psutil/arch/freebsd/disk.c create mode 100644 psutil/arch/freebsd/disk.h create mode 100644 psutil/arch/freebsd/mem.c create mode 100644 psutil/arch/freebsd/mem.h rename psutil/arch/freebsd/{specific.c => proc.c} (65%) rename psutil/arch/freebsd/{specific.h => proc.h} (73%) create mode 100644 psutil/arch/freebsd/sensors.c create mode 100644 psutil/arch/freebsd/sensors.h diff --git a/MANIFEST.in b/MANIFEST.in index 5b4eb69ad8..13acb0fd68 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -51,10 +51,16 @@ include psutil/arch/aix/net_connections.h include psutil/arch/aix/net_kernel_structs.h include psutil/arch/freebsd/cpu.c include psutil/arch/freebsd/cpu.h +include psutil/arch/freebsd/disk.c +include psutil/arch/freebsd/disk.h +include psutil/arch/freebsd/mem.c +include psutil/arch/freebsd/mem.h +include psutil/arch/freebsd/proc.c +include psutil/arch/freebsd/proc.h include psutil/arch/freebsd/proc_socks.c include psutil/arch/freebsd/proc_socks.h -include psutil/arch/freebsd/specific.c -include psutil/arch/freebsd/specific.h +include psutil/arch/freebsd/sensors.c +include psutil/arch/freebsd/sensors.h include psutil/arch/freebsd/sys_socks.c include psutil/arch/freebsd/sys_socks.h include psutil/arch/netbsd/socks.c diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 1748bfdab3..fdd2bad180 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -124,6 +124,7 @@ INCONSISTENCIES RESOURCES ========= +- conky: https://github.com/brndnmtthws/conky/ - sigar: https://github.com/hyperic/sigar (Java) - zabbix: https://zabbix.org/wiki/Get_Zabbix - libstatgrab: http://www.i-scream.org/libstatgrab/ diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 131708380e..1ffa7b00de 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -64,7 +64,10 @@ #ifdef PSUTIL_FREEBSD #include "arch/freebsd/cpu.h" - #include "arch/freebsd/specific.h" + #include "arch/freebsd/mem.h" + #include "arch/freebsd/disk.h" + #include "arch/freebsd/sensors.h" + #include "arch/freebsd/proc.h" #include "arch/freebsd/sys_socks.h" #include "arch/freebsd/proc_socks.h" @@ -104,12 +107,6 @@ // convert a timeval struct to a double #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) -#ifdef PSUTIL_FREEBSD - // convert a bintime struct to milliseconds - #define PSUTIL_BT2MSEC(bt) (bt.sec * 1000 + (((uint64_t) 1000000000 * \ - (uint32_t) (bt.frac >> 32) ) >> 32 ) / 1000000) -#endif - #if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) #endif diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index f31e9bb0e1..a15d96efc1 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -18,11 +18,75 @@ For reference, here's the git history with original(ish) implementations: #include #include +#include #include "../../_psutil_common.h" #include "../../_psutil_posix.h" +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + static int maxcpus; + int mib[2]; + int ncpu; + size_t len; + size_t size; + int i; + PyObject *py_retlist = PyList_New(0); + PyObject *py_cputime = NULL; + + if (py_retlist == NULL) + return NULL; + + // retrieve maxcpus value + size = sizeof(maxcpus); + if (sysctlbyname("kern.smp.maxcpus", &maxcpus, &size, NULL, 0) < 0) { + Py_DECREF(py_retlist); + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('kern.smp.maxcpus')"); + } + long cpu_time[maxcpus][CPUSTATES]; + + // retrieve the number of cpus + mib[0] = CTL_HW; + mib[1] = HW_NCPU; + len = sizeof(ncpu); + if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { + PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_NCPU)"); + goto error; + } + + // per-cpu info + size = sizeof(cpu_time); + if (sysctlbyname("kern.cp_times", &cpu_time, &size, NULL, 0) == -1) { + PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('kern.smp.maxcpus')"); + goto error; + } + + for (i = 0; i < ncpu; i++) { + py_cputime = Py_BuildValue( + "(ddddd)", + (double)cpu_time[i][CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_INTR] / CLOCKS_PER_SEC); + if (!py_cputime) + goto error; + if (PyList_Append(py_retlist, py_cputime)) + goto error; + Py_DECREF(py_cputime); + } + + return py_retlist; + +error: + Py_XDECREF(py_cputime); + Py_DECREF(py_retlist); + return NULL; +} + + PyObject * psutil_cpu_topology(PyObject *self, PyObject *args) { void *topology = NULL; diff --git a/psutil/arch/freebsd/cpu.h b/psutil/arch/freebsd/cpu.h index 8decd77323..b78c439197 100644 --- a/psutil/arch/freebsd/cpu.h +++ b/psutil/arch/freebsd/cpu.h @@ -6,6 +6,7 @@ #include -PyObject* psutil_cpu_freq(PyObject* self, PyObject* args); -PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); -PyObject* psutil_cpu_topology(PyObject* self, PyObject* args); +PyObject *psutil_cpu_freq(PyObject* self, PyObject* args); +PyObject *psutil_cpu_stats(PyObject* self, PyObject* args); +PyObject *psutil_cpu_topology(PyObject* self, PyObject* args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c new file mode 100644 index 0000000000..320b2bc819 --- /dev/null +++ b/psutil/arch/freebsd/disk.c @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" + + +// convert a bintime struct to milliseconds +#define PSUTIL_BT2MSEC(bt) (bt.sec * 1000 + (((uint64_t) 1000000000 * (uint32_t) \ + (bt.frac >> 32) ) >> 32 ) / 1000000) + + +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + int i; + struct statinfo stats; + + PyObject *py_retdict = PyDict_New(); + PyObject *py_disk_info = NULL; + + if (py_retdict == NULL) + return NULL; + if (devstat_checkversion(NULL) < 0) { + PyErr_Format(PyExc_RuntimeError, + "devstat_checkversion() syscall failed"); + goto error; + } + + stats.dinfo = (struct devinfo *)malloc(sizeof(struct devinfo)); + if (stats.dinfo == NULL) { + PyErr_NoMemory(); + goto error; + } + bzero(stats.dinfo, sizeof(struct devinfo)); + + if (devstat_getdevs(NULL, &stats) == -1) { + PyErr_Format(PyExc_RuntimeError, "devstat_getdevs() syscall failed"); + goto error; + } + + for (i = 0; i < stats.dinfo->numdevs; i++) { + py_disk_info = NULL; + struct devstat current; + char disk_name[128]; + current = stats.dinfo->devices[i]; + snprintf(disk_name, sizeof(disk_name), "%s%d", + current.device_name, + current.unit_number); + + py_disk_info = Py_BuildValue( + "(KKKKLLL)", + current.operations[DEVSTAT_READ], // no reads + current.operations[DEVSTAT_WRITE], // no writes + current.bytes[DEVSTAT_READ], // bytes read + current.bytes[DEVSTAT_WRITE], // bytes written + (long long) PSUTIL_BT2MSEC(current.duration[DEVSTAT_READ]), // r time + (long long) PSUTIL_BT2MSEC(current.duration[DEVSTAT_WRITE]), // w time + (long long) PSUTIL_BT2MSEC(current.busy_time) // busy time + ); // finished transactions + if (!py_disk_info) + goto error; + if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) + goto error; + Py_DECREF(py_disk_info); + } + + if (stats.dinfo->mem_ptr) + free(stats.dinfo->mem_ptr); + free(stats.dinfo); + return py_retdict; + +error: + Py_XDECREF(py_disk_info); + Py_DECREF(py_retdict); + if (stats.dinfo != NULL) + free(stats.dinfo); + return NULL; +} + diff --git a/psutil/arch/freebsd/disk.h b/psutil/arch/freebsd/disk.h new file mode 100644 index 0000000000..9e29f664b0 --- /dev/null +++ b/psutil/arch/freebsd/disk.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_disk_io_counters(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c new file mode 100644 index 0000000000..f5374ef4e2 --- /dev/null +++ b/psutil/arch/freebsd/mem.c @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + + +#include +#include +#include +#include +#include +#include +#include + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" + + +#ifndef _PATH_DEVNULL + #define _PATH_DEVNULL "/dev/null" +#endif + + +PyObject * +psutil_virtual_mem(PyObject *self, PyObject *args) { + unsigned long total; + unsigned int active, inactive, wired, cached, free; + size_t size = sizeof(total); + struct vmtotal vm; + int mib[] = {CTL_VM, VM_METER}; + long pagesize = psutil_getpagesize(); +#if __FreeBSD_version > 702101 + long buffers; +#else + int buffers; +#endif + size_t buffers_size = sizeof(buffers); + + if (sysctlbyname("hw.physmem", &total, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('hw.physmem')"); + } + if (sysctlbyname("vm.stats.vm.v_active_count", &active, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_active_count')"); + } + if (sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, &size, NULL, 0)) + { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_inactive_count')"); + } + if (sysctlbyname("vm.stats.vm.v_wire_count", &wired, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_wire_count')"); + } + // https://github.com/giampaolo/psutil/issues/997 + if (sysctlbyname("vm.stats.vm.v_cache_count", &cached, &size, NULL, 0)) { + cached = 0; + } + if (sysctlbyname("vm.stats.vm.v_free_count", &free, &size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_free_count')"); + } + if (sysctlbyname("vfs.bufspace", &buffers, &buffers_size, NULL, 0)) { + return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('vfs.bufspace')"); + } + + size = sizeof(vm); + if (sysctl(mib, 2, &vm, &size, NULL, 0) != 0) { + return PyErr_SetFromOSErrnoWithSyscall("sysctl(CTL_VM | VM_METER)"); + } + + return Py_BuildValue("KKKKKKKK", + (unsigned long long) total, + (unsigned long long) free * pagesize, + (unsigned long long) active * pagesize, + (unsigned long long) inactive * pagesize, + (unsigned long long) wired * pagesize, + (unsigned long long) cached * pagesize, + (unsigned long long) buffers, + (unsigned long long) (vm.t_vmshr + vm.t_rmshr) * pagesize // shared + ); +} + + +PyObject * +psutil_swap_mem(PyObject *self, PyObject *args) { + // Return swap memory stats (see 'swapinfo' cmdline tool) + kvm_t *kd; + struct kvm_swap kvmsw[1]; + unsigned int swapin, swapout, nodein, nodeout; + size_t size = sizeof(unsigned int); + long pagesize = psutil_getpagesize(); + + kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open failed"); + if (kd == NULL) { + PyErr_SetString(PyExc_RuntimeError, "kvm_open() syscall failed"); + return NULL; + } + + if (kvm_getswapinfo(kd, kvmsw, 1, 0) < 0) { + kvm_close(kd); + PyErr_SetString(PyExc_RuntimeError, + "kvm_getswapinfo() syscall failed"); + return NULL; + } + + kvm_close(kd); + + if (sysctlbyname("vm.stats.vm.v_swapin", &swapin, &size, NULL, 0) == -1) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_swapin)'"); + } + if (sysctlbyname("vm.stats.vm.v_swapout", &swapout, &size, NULL, 0) == -1){ + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_swapout)'"); + } + if (sysctlbyname("vm.stats.vm.v_vnodein", &nodein, &size, NULL, 0) == -1) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_vnodein)'"); + } + if (sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, &size, NULL, 0) == -1) { + return PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_vnodeout)'"); + } + + return Py_BuildValue( + "(KKKII)", + (unsigned long long)kvmsw[0].ksw_total * pagesize, // total + (unsigned long long)kvmsw[0].ksw_used * pagesize, // used + (unsigned long long)kvmsw[0].ksw_total * pagesize - // free + kvmsw[0].ksw_used * pagesize, + swapin + swapout, // swap in + nodein + nodeout // swap out + ); +} + diff --git a/psutil/arch/freebsd/mem.h b/psutil/arch/freebsd/mem.h new file mode 100644 index 0000000000..e7dcfc570a --- /dev/null +++ b/psutil/arch/freebsd/mem.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_swap_mem(PyObject* self, PyObject* args); +PyObject *psutil_virtual_mem(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/proc.c similarity index 65% rename from psutil/arch/freebsd/specific.c rename to psutil/arch/freebsd/proc.c index 423f0c7b1e..a2e130b558 100644 --- a/psutil/arch/freebsd/specific.c +++ b/psutil/arch/freebsd/proc.c @@ -2,9 +2,6 @@ * Copyright (c) 2009, Jay Loden, 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. - * - * Helper functions specific to FreeBSD. - * Used by _psutil_bsd module methods. */ #include @@ -20,8 +17,7 @@ #include #include #include -#include // needed for vmtotal struct -#include // for swap mem +#include #include // process open files, shared libs (kinfo_getvmmap), cwd #include @@ -30,12 +26,6 @@ #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) -#define PSUTIL_BT2MSEC(bt) (bt.sec * 1000 + (((uint64_t) 1000000000 * (uint32_t) \ - (bt.frac >> 32) ) >> 32 ) / 1000000) -#define DECIKELVIN_2_CELCIUS(t) (t - 2731) / 10 -#ifndef _PATH_DEVNULL -#define _PATH_DEVNULL "/dev/null" -#endif // ============================================================================ @@ -363,123 +353,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -/* - * Return virtual memory usage statistics. - */ -PyObject * -psutil_virtual_mem(PyObject *self, PyObject *args) { - unsigned long total; - unsigned int active, inactive, wired, cached, free; - size_t size = sizeof(total); - struct vmtotal vm; - int mib[] = {CTL_VM, VM_METER}; - long pagesize = psutil_getpagesize(); -#if __FreeBSD_version > 702101 - long buffers; -#else - int buffers; -#endif - size_t buffers_size = sizeof(buffers); - - if (sysctlbyname("hw.physmem", &total, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('hw.physmem')"); - } - if (sysctlbyname("vm.stats.vm.v_active_count", &active, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_active_count')"); - } - if (sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, &size, NULL, 0)) - { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_inactive_count')"); - } - if (sysctlbyname("vm.stats.vm.v_wire_count", &wired, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_wire_count')"); - } - // https://github.com/giampaolo/psutil/issues/997 - if (sysctlbyname("vm.stats.vm.v_cache_count", &cached, &size, NULL, 0)) { - cached = 0; - } - if (sysctlbyname("vm.stats.vm.v_free_count", &free, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_free_count')"); - } - if (sysctlbyname("vfs.bufspace", &buffers, &buffers_size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('vfs.bufspace')"); - } - - size = sizeof(vm); - if (sysctl(mib, 2, &vm, &size, NULL, 0) != 0) { - return PyErr_SetFromOSErrnoWithSyscall("sysctl(CTL_VM | VM_METER)"); - } - - return Py_BuildValue("KKKKKKKK", - (unsigned long long) total, - (unsigned long long) free * pagesize, - (unsigned long long) active * pagesize, - (unsigned long long) inactive * pagesize, - (unsigned long long) wired * pagesize, - (unsigned long long) cached * pagesize, - (unsigned long long) buffers, - (unsigned long long) (vm.t_vmshr + vm.t_rmshr) * pagesize // shared - ); -} - - -PyObject * -psutil_swap_mem(PyObject *self, PyObject *args) { - // Return swap memory stats (see 'swapinfo' cmdline tool) - kvm_t *kd; - struct kvm_swap kvmsw[1]; - unsigned int swapin, swapout, nodein, nodeout; - size_t size = sizeof(unsigned int); - long pagesize = psutil_getpagesize(); - - kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open failed"); - if (kd == NULL) { - PyErr_SetString(PyExc_RuntimeError, "kvm_open() syscall failed"); - return NULL; - } - - if (kvm_getswapinfo(kd, kvmsw, 1, 0) < 0) { - kvm_close(kd); - PyErr_SetString(PyExc_RuntimeError, - "kvm_getswapinfo() syscall failed"); - return NULL; - } - - kvm_close(kd); - - if (sysctlbyname("vm.stats.vm.v_swapin", &swapin, &size, NULL, 0) == -1) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_swapin)'"); - } - if (sysctlbyname("vm.stats.vm.v_swapout", &swapout, &size, NULL, 0) == -1){ - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_swapout)'"); - } - if (sysctlbyname("vm.stats.vm.v_vnodein", &nodein, &size, NULL, 0) == -1) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_vnodein)'"); - } - if (sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, &size, NULL, 0) == -1) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_vnodeout)'"); - } - - return Py_BuildValue( - "(KKKII)", - (unsigned long long)kvmsw[0].ksw_total * pagesize, // total - (unsigned long long)kvmsw[0].ksw_used * pagesize, // used - (unsigned long long)kvmsw[0].ksw_total * pagesize - // free - kvmsw[0].ksw_used * pagesize, - swapin + swapout, // swap in - nodein + nodeout // swap out - ); -} - - #if defined(__FreeBSD_version) && __FreeBSD_version >= 701000 PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { @@ -558,137 +431,6 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { #endif -PyObject * -psutil_per_cpu_times(PyObject *self, PyObject *args) { - static int maxcpus; - int mib[2]; - int ncpu; - size_t len; - size_t size; - int i; - PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; - - if (py_retlist == NULL) - return NULL; - - // retrieve maxcpus value - size = sizeof(maxcpus); - if (sysctlbyname("kern.smp.maxcpus", &maxcpus, &size, NULL, 0) < 0) { - Py_DECREF(py_retlist); - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('kern.smp.maxcpus')"); - } - long cpu_time[maxcpus][CPUSTATES]; - - // retrieve the number of cpus - mib[0] = CTL_HW; - mib[1] = HW_NCPU; - len = sizeof(ncpu); - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_NCPU)"); - goto error; - } - - // per-cpu info - size = sizeof(cpu_time); - if (sysctlbyname("kern.cp_times", &cpu_time, &size, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('kern.smp.maxcpus')"); - goto error; - } - - for (i = 0; i < ncpu; i++) { - py_cputime = Py_BuildValue( - "(ddddd)", - (double)cpu_time[i][CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_INTR] / CLOCKS_PER_SEC); - if (!py_cputime) - goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_DECREF(py_cputime); - } - - return py_retlist; - -error: - Py_XDECREF(py_cputime); - Py_DECREF(py_retlist); - return NULL; -} - - -PyObject * -psutil_disk_io_counters(PyObject *self, PyObject *args) { - int i; - struct statinfo stats; - - PyObject *py_retdict = PyDict_New(); - PyObject *py_disk_info = NULL; - - if (py_retdict == NULL) - return NULL; - if (devstat_checkversion(NULL) < 0) { - PyErr_Format(PyExc_RuntimeError, - "devstat_checkversion() syscall failed"); - goto error; - } - - stats.dinfo = (struct devinfo *)malloc(sizeof(struct devinfo)); - if (stats.dinfo == NULL) { - PyErr_NoMemory(); - goto error; - } - bzero(stats.dinfo, sizeof(struct devinfo)); - - if (devstat_getdevs(NULL, &stats) == -1) { - PyErr_Format(PyExc_RuntimeError, "devstat_getdevs() syscall failed"); - goto error; - } - - for (i = 0; i < stats.dinfo->numdevs; i++) { - py_disk_info = NULL; - struct devstat current; - char disk_name[128]; - current = stats.dinfo->devices[i]; - snprintf(disk_name, sizeof(disk_name), "%s%d", - current.device_name, - current.unit_number); - - py_disk_info = Py_BuildValue( - "(KKKKLLL)", - current.operations[DEVSTAT_READ], // no reads - current.operations[DEVSTAT_WRITE], // no writes - current.bytes[DEVSTAT_READ], // bytes read - current.bytes[DEVSTAT_WRITE], // bytes written - (long long) PSUTIL_BT2MSEC(current.duration[DEVSTAT_READ]), // r time - (long long) PSUTIL_BT2MSEC(current.duration[DEVSTAT_WRITE]), // w time - (long long) PSUTIL_BT2MSEC(current.busy_time) // busy time - ); // finished transactions - if (!py_disk_info) - goto error; - if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) - goto error; - Py_DECREF(py_disk_info); - } - - if (stats.dinfo->mem_ptr) - free(stats.dinfo->mem_ptr); - free(stats.dinfo); - return py_retdict; - -error: - Py_XDECREF(py_disk_info); - Py_DECREF(py_retdict); - if (stats.dinfo != NULL) - free(stats.dinfo); - return NULL; -} - - PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { // Return a list of tuples for every process memory maps. @@ -901,69 +643,6 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { } -/* - * Return battery information. - */ -PyObject * -psutil_sensors_battery(PyObject *self, PyObject *args) { - int percent; - int minsleft; - int power_plugged; - size_t size = sizeof(percent); - - if (sysctlbyname("hw.acpi.battery.life", &percent, &size, NULL, 0)) - goto error; - if (sysctlbyname("hw.acpi.battery.time", &minsleft, &size, NULL, 0)) - goto error; - if (sysctlbyname("hw.acpi.acline", &power_plugged, &size, NULL, 0)) - goto error; - return Py_BuildValue("iii", percent, minsleft, power_plugged); - -error: - // see: https://github.com/giampaolo/psutil/issues/1074 - if (errno == ENOENT) - PyErr_SetString(PyExc_NotImplementedError, "no battery"); - else - PyErr_SetFromErrno(PyExc_OSError); - return NULL; -} - - -/* - * Return temperature information for a given CPU core number. - */ -PyObject * -psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { - int current; - int tjmax; - int core; - char sensor[26]; - size_t size = sizeof(current); - - if (! PyArg_ParseTuple(args, "i", &core)) - return NULL; - sprintf(sensor, "dev.cpu.%d.temperature", core); - if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) - goto error; - current = DECIKELVIN_2_CELCIUS(current); - - // Return -273 in case of faliure. - sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); - if (sysctlbyname(sensor, &tjmax, &size, NULL, 0)) - tjmax = 0; - tjmax = DECIKELVIN_2_CELCIUS(tjmax); - - return Py_BuildValue("ii", current, tjmax); - -error: - if (errno == ENOENT) - PyErr_SetString(PyExc_NotImplementedError, "no temperature sensors"); - else - PyErr_SetFromErrno(PyExc_OSError); - return NULL; -} - - /* * An emulation of Linux prlimit(). Returns a (soft, hard) tuple. */ diff --git a/psutil/arch/freebsd/specific.h b/psutil/arch/freebsd/proc.h similarity index 73% rename from psutil/arch/freebsd/specific.h rename to psutil/arch/freebsd/proc.h index 57f0a2a4c2..9c16f3cb9d 100644 --- a/psutil/arch/freebsd/specific.h +++ b/psutil/arch/freebsd/proc.h @@ -11,9 +11,7 @@ typedef struct kinfo_proc kinfo_proc; int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); -PyObject* psutil_disk_io_counters(PyObject* self, PyObject* args); PyObject* psutil_get_cmdline(long pid); -PyObject* psutil_per_cpu_times(PyObject* self, PyObject* args); PyObject* psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args); PyObject* psutil_proc_cpu_affinity_set(PyObject* self, PyObject* args); PyObject* psutil_proc_cwd(PyObject* self, PyObject* args); @@ -24,7 +22,3 @@ PyObject* psutil_proc_num_fds(PyObject* self, PyObject* args); PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); PyObject* psutil_proc_setrlimit(PyObject* self, PyObject* args); PyObject* psutil_proc_threads(PyObject* self, PyObject* args); -PyObject* psutil_sensors_battery(PyObject* self, PyObject* args); -PyObject* psutil_sensors_cpu_temperature(PyObject* self, PyObject* args); -PyObject* psutil_swap_mem(PyObject* self, PyObject* args); -PyObject* psutil_virtual_mem(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c new file mode 100644 index 0000000000..ce7b2d9289 --- /dev/null +++ b/psutil/arch/freebsd/sensors.c @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +/* +Original code was refactored and moved from psutil/arch/freebsd/specific.c +For reference, here's the git history with original(ish) implementations: +- sensors_battery(): 022cf0a05d34f4274269d4f8002ee95b9f3e32d2 +- sensors_cpu_temperature(): bb5d032be76980a9e110f03f1203bd35fa85a793 + (patch by Alex Manuskin) +*/ + + +#include +#include + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" + + +#define DECIKELVIN_2_CELCIUS(t) (t - 2731) / 10 + + +PyObject * +psutil_sensors_battery(PyObject *self, PyObject *args) { + int percent; + int minsleft; + int power_plugged; + size_t size = sizeof(percent); + + if (sysctlbyname("hw.acpi.battery.life", &percent, &size, NULL, 0)) + goto error; + if (sysctlbyname("hw.acpi.battery.time", &minsleft, &size, NULL, 0)) + goto error; + if (sysctlbyname("hw.acpi.acline", &power_plugged, &size, NULL, 0)) + goto error; + return Py_BuildValue("iii", percent, minsleft, power_plugged); + +error: + // see: https://github.com/giampaolo/psutil/issues/1074 + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "no battery"); + else + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + + +// Return temperature information for a given CPU core number. +PyObject * +psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { + int current; + int tjmax; + int core; + char sensor[26]; + size_t size = sizeof(current); + + if (! PyArg_ParseTuple(args, "i", &core)) + return NULL; + sprintf(sensor, "dev.cpu.%d.temperature", core); + if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) + goto error; + current = DECIKELVIN_2_CELCIUS(current); + + // Return -273 in case of faliure. + sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); + if (sysctlbyname(sensor, &tjmax, &size, NULL, 0)) + tjmax = 0; + tjmax = DECIKELVIN_2_CELCIUS(tjmax); + + return Py_BuildValue("ii", current, tjmax); + +error: + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "no temperature sensors"); + else + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + diff --git a/psutil/arch/freebsd/sensors.h b/psutil/arch/freebsd/sensors.h new file mode 100644 index 0000000000..e5c4107bf4 --- /dev/null +++ b/psutil/arch/freebsd/sensors.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject* psutil_sensors_battery(PyObject* self, PyObject* args); +PyObject* psutil_sensors_cpu_temperature(PyObject* self, PyObject* args); diff --git a/setup.py b/setup.py index e0bb5d095d..3bad6d5d5d 100755 --- a/setup.py +++ b/setup.py @@ -207,7 +207,10 @@ def get_winver(): sources=sources + [ 'psutil/_psutil_bsd.c', 'psutil/arch/freebsd/cpu.c', - 'psutil/arch/freebsd/specific.c', + 'psutil/arch/freebsd/mem.c', + 'psutil/arch/freebsd/disk.c', + 'psutil/arch/freebsd/sensors.c', + 'psutil/arch/freebsd/proc.c', 'psutil/arch/freebsd/sys_socks.c', 'psutil/arch/freebsd/proc_socks.c', ], From 471b19d2aa799cd73bded23379e864dd35bec2b6 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Thu, 27 Jan 2022 08:26:28 +0800 Subject: [PATCH 0834/1714] Fix typos --- CONTRIBUTING.md | 2 +- HISTORY.rst | 16 ++++++++-------- INSTALL.rst | 2 +- appveyor.yml | 2 +- docs/DEVGUIDE.rst | 4 ++-- docs/index.rst | 10 +++++----- make.bat | 2 +- psutil/__init__.py | 6 +++--- psutil/_pslinux.py | 8 ++++---- psutil/_psutil_common.c | 2 +- psutil/_psutil_sunos.c | 6 +++--- psutil/_pswindows.py | 2 +- psutil/arch/freebsd/sensors.c | 2 +- psutil/arch/osx/cpu.c | 2 +- psutil/arch/solaris/environ.c | 8 ++++---- psutil/arch/solaris/v10/ifaddrs.c | 2 +- psutil/arch/windows/cpu.c | 6 +++--- psutil/arch/windows/process_utils.c | 2 +- psutil/tests/__init__.py | 4 ++-- psutil/tests/test_bsd.py | 2 +- psutil/tests/test_linux.py | 4 ++-- psutil/tests/test_process.py | 4 ++-- psutil/tests/test_system.py | 2 +- psutil/tests/test_windows.py | 2 +- scripts/internal/download_wheels_appveyor.py | 2 +- scripts/internal/fix_flake8.py | 4 ++-- scripts/iotop.py | 2 +- setup.py | 2 +- 28 files changed, 56 insertions(+), 56 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 481793fc61..250398f8a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Pull Requests * The PR system is for fixing bugs or make enhancements related to the **program code**. -* If you whish to implement a new feature or add support for a new platform it's +* If you wish to implement a new feature or add support for a new platform it's better to **discuss it first**, either on the issue tracker, the forum or via private email. * In order to get acquainted with the code base and tooling, take a look at the diff --git a/HISTORY.rst b/HISTORY.rst index 595de97548..32f311680c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -313,7 +313,7 @@ XXXX-XX-XX - 1276_, [AIX]: can't get whole `Process.cmdline()`_. (patch by Arnon Yaari) - 1501_, [Windows]: `Process.cmdline()`_ and `Process.exe()`_ raise unhandled "WinError 1168 element not found" exceptions for "Registry" and - "Memory Compression" psuedo processes on Windows 10. + "Memory Compression" pseudo processes on Windows 10. - 1526_, [NetBSD], **[critical]**: `Process.cmdline()`_ could raise ``MemoryError``. (patch by Kamil Rytarowski) @@ -352,9 +352,9 @@ XXXX-XX-XX - 1471_, [SunOS]: `Process.name()`_ and `Process.cmdline()`_ can return ``SystemError``. (patch by Daniel Beer) - 1472_, [Linux]: `cpu_freq()`_ does not return all CPUs on Rasbperry-pi 3. -- 1474_: fix formatting of ``psutil.tests()`` which mimicks ``ps aux`` output. +- 1474_: fix formatting of ``psutil.tests()`` which mimics ``ps aux`` output. - 1475_, [Windows], **[critical]**: ``OSError.winerror`` attribute wasn't - properly checked resuling in ``WindowsError(ERROR_ACCESS_DENIED)`` being + properly checked resulting in ``WindowsError(ERROR_ACCESS_DENIED)`` being raised instead of `AccessDenied`_. - 1477_, [Windows]: wrong or absent error handling for private ``NTSTATUS`` Windows APIs. Different process methods were affected by this. @@ -1318,7 +1318,7 @@ XXXX-XX-XX **Bug fixes** - 340_, [Windows], **[critical]**: `Process.open_files()`_ no longer hangs. - Instead it uses a thred which times out and skips the file handle in case it's + Instead it uses a thread which times out and skips the file handle in case it's taking too long to be retrieved. (patch by Jeff Tang) - 627_, [Windows]: `Process.name()`_ no longer raises `AccessDenied`_ for pids owned by another user. @@ -1327,7 +1327,7 @@ XXXX-XX-XX affect ``os.getpid()`` 's process group and not PID 0. - 639_, [Linux]: `Process.cmdline()`_ can be truncated. - 640_, [Linux]: ``*connections`` functions may swallow errors and return an - incomplete list of connnections. + incomplete list of connections. - 642_: ``repr()`` of exceptions is incorrect. - 653_, [Windows]: add ``inet_ntop()`` function for Windows XP to support IPv6. - 641_, [Windows]: replace deprecated string functions with safe equivalents. @@ -1398,7 +1398,7 @@ XXXX-XX-XX **Bug fixes** -- 572_, [Linux]: fix "ValueError: ambiguos inode with multiple PIDs references" +- 572_, [Linux]: fix "ValueError: ambiguous inode with multiple PIDs references" for `Process.connections()`_. (patch by Bruno Binet) 2.2.0 @@ -1842,7 +1842,7 @@ In most cases accessing the old names will work but it will cause a the PPID to 1 in case of a zombie process. - 323_, [macOS]: `disk_io_counters()`_ ``read_time`` and ``write_time`` parameters were reporting microseconds not milliseconds. (patch by Gregory Szorc) -- 331_: `Process.cmdline()`_ is no longer cached after first acces as it may +- 331_: `Process.cmdline()`_ is no longer cached after first access as it may change. - 333_, [macOS]: leak of Mach ports (patch by rsesek@google.com) - 337_, [Linux], **[critical]**: `Process`_ methods not working because of a @@ -2199,7 +2199,7 @@ In most cases accessing the old names will work but it will cause a - 113_: exception messages now include `Process.name()`_ and `Process.pid`_. - 114_, [Windows]: `Process.username()`_ has been rewritten in pure C and no longer uses WMI resulting in a big speedup. Also, pywin32 is no longer - required as a third-party dependancy. (patch by wj32) + required as a third-party dependency. (patch by wj32) - 117_, [Windows]: added support for Windows 2000. - 123_: `cpu_percent()`_ and `Process.cpu_percent()`_ accept a new ``interval`` parameter. diff --git a/INSTALL.rst b/INSTALL.rst index c7efea5ee0..46aee6cd0c 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -9,7 +9,7 @@ install a C compiler. All you have to do is:: pip install psutil -If wheels are not available for your platform or architecture, or you whish to +If wheels are not available for your platform or architecture, or you wish to build & install psutil from sources, keep reading. Linux (build) diff --git a/appveyor.yml b/appveyor.yml index 83e570ae12..c487da065b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,7 +11,7 @@ matrix: environment: global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script intepreter + # /E:ON and /V:ON options are not enabled in the batch script interpreter # See: http://stackoverflow.com/a/13751649/163740 WITH_COMPILER: "cmd /E:ON /V:ON /C .\\.ci\\appveyor\\run_with_compiler.cmd" PYTHONWARNINGS: always diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index cb9545bcfe..384f8b25ea 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -32,7 +32,7 @@ Once you have a compiler installed: make uninstall make help -- if you're working on a new feature and you whish to compile & test it "on the +- if you're working on a new feature and you wish to compile & test it "on the fly" from a test script, this is a quick & dirty way to do it: .. code-block:: bash @@ -115,7 +115,7 @@ Unit tests are automatically run on every ``git push`` on **Linux**, **macOS**, .. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil -OpenBSD, NetBSD, AIX and Solaris does not have continuos test integration. +OpenBSD, NetBSD, AIX and Solaris does not have continuous test integration. Documentation ------------- diff --git a/docs/index.rst b/docs/index.rst index 6df7ccefb1..38fb7e985c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1100,7 +1100,7 @@ Process class Here's a list of methods which can take advantage of the speedup depending on what platform you're on. - In the table below horizontal emtpy rows indicate what process methods can + In the table below horizontal empty rows indicate what process methods can be efficiently grouped together internally. The last column (speedup) shows an approximation of the speedup you can get if you call all the methods together (best case scenario). @@ -1193,8 +1193,8 @@ Process class .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support - .. versionchanged:: 5.6.3 added AIX suport - .. versionchanged:: 5.7.3 added BSD suport + .. versionchanged:: 5.6.3 added AIX support + .. versionchanged:: 5.7.3 added BSD support .. method:: create_time() @@ -1593,7 +1593,7 @@ Process class Return a named tuple 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. +---------+---------+-------+---------+-----+------------------------------+ @@ -2539,7 +2539,7 @@ FAQs ==== * Q: Why do I get :class:`AccessDenied` for certain processes? -* A: This may happen when you query processess owned by another user, +* A: This may happen when you query processes owned by another user, especially on macOS (see `issue #883`_) and Windows. Unfortunately there's not much you can do about this except running the Python process with higher privileges. diff --git a/make.bat b/make.bat index 8e60811ccf..e292bb16b6 100644 --- a/make.bat +++ b/make.bat @@ -2,7 +2,7 @@ rem ========================================================================== rem Shortcuts for various tasks, emulating UNIX "make" on Windows. -rem It is primarly intended as a shortcut for compiling / installing +rem It is primarily intended as a shortcut for compiling / installing rem psutil ("make.bat build", "make.bat install") and running tests rem ("make.bat test"). rem diff --git a/psutil/__init__.py b/psutil/__init__.py index 6deebb2633..db63fdbb8f 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -373,7 +373,7 @@ def _init(self, pid, _ignore_nsp=False): raise NoSuchProcess(pid, msg='process PID not found') 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(). @@ -1054,7 +1054,7 @@ 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. """ @@ -1208,7 +1208,7 @@ def send_signal(self, sig): 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) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 5f149a035d..8549b5255d 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -242,7 +242,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...). @@ -915,7 +915,7 @@ def process_inet(file, family, type_, inodes, filter_pid=None): # # 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: @@ -1675,7 +1675,7 @@ def _parse_stat_file(self): """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 3 (e.g ppid position 4 in + 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. @@ -1683,7 +1683,7 @@ def _parse_stat_file(self): 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] fields = data[rpar + 2:].split() diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 9679da671a..096e2f373f 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -319,7 +319,7 @@ psutil_loadlibs() { // minimum requirement: Win 7 GetActiveProcessorCount = psutil_GetProcAddress( "kernel32", "GetActiveProcessorCount"); - // minumum requirement: Win 7 + // minimum requirement: Win 7 GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( "kernel32", "GetLogicalProcessorInformationEx"); // minimum requirements: Windows Server Core diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 42a1ffe821..84bf1c40ea 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -124,7 +124,7 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { } /* - * Join array of C strings to C string with delemiter dm. + * Join array of C strings to C string with delimiter dm. * Omit empty records. */ static int @@ -1255,7 +1255,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { lport = tp.tcpConnLocalPort; rport = tp.tcpConnRemPort; - // contruct python tuple/list + // construct python tuple/list py_laddr = Py_BuildValue("(si)", lip, lport); if (!py_laddr) goto error; @@ -1300,7 +1300,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { lport = tp6.tcp6ConnLocalPort; rport = tp6.tcp6ConnRemPort; - // contruct python tuple/list + // construct python tuple/list py_laddr = Py_BuildValue("(si)", lip, lport); if (!py_laddr) goto error; diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 9966b1b4e2..31edffc1c7 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -249,7 +249,7 @@ def swap_memory(): free_system = mem[3] # Despite the name PageFile refers to total system memory here - # thus physical memory values need to be substracted to get swap values + # thus physical memory values need to be subtracted to get swap values total = total_system - total_phys free = min(total, free_system - free_phys) used = total - free diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index ce7b2d9289..0042adbeb8 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -64,7 +64,7 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { goto error; current = DECIKELVIN_2_CELCIUS(current); - // Return -273 in case of faliure. + // Return -273 in case of failure. sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); if (sysctlbyname(sensor, &tjmax, &size, NULL, 0)) tjmax = 0; diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 37141a2d31..2d94f31dd2 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -119,7 +119,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { size_t len = sizeof(curr); size_t size = sizeof(min); - // also availble as "hw.cpufrequency" but it's deprecated + // also available as "hw.cpufrequency" but it's deprecated mib[0] = CTL_HW; mib[1] = HW_CPU_FREQ; diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/solaris/environ.c index 482fe1fc16..58afd63acc 100644 --- a/psutil/arch/solaris/environ.c +++ b/psutil/arch/solaris/environ.c @@ -227,7 +227,7 @@ is_ptr_dereference_possible(psinfo_t info) { /* * Return pointer size according to psinfo_t structure - * @param info a ponter to process info (psinfo_t) structure of the + * @param info a pointer to process info (psinfo_t) structure of the * interesting process. * @return pointer size (4 or 8). */ @@ -284,9 +284,9 @@ search_pointers_vector_size_offt(int fd, off_t offt, size_t ptr_size) { /* - * Derefence and read array of strings by psinfo_t.pr_argv pointer from + * Dereference and read array of strings by psinfo_t.pr_argv pointer from * remote process. - * @param info a ponter to process info (psinfo_t) structure of the + * @param info a pointer to process info (psinfo_t) structure of the * interesting process * @param procfs_path a cstring with path to mounted procfs filesystem. * @param count a pointer to variable where to store amount of elements in @@ -333,7 +333,7 @@ psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count) { /* * Dereference and read array of strings by psinfo_t.pr_envp pointer * from remote process. - * @param info a ponter to process info (psinfo_t) structure of the + * @param info a pointer to process info (psinfo_t) structure of the * interesting process. * @param procfs_path a cstring with path to mounted procfs filesystem. * @param count a pointer to variable where to store amount of elements in diff --git a/psutil/arch/solaris/v10/ifaddrs.c b/psutil/arch/solaris/v10/ifaddrs.c index b741a6b92b..3719c8c18a 100644 --- a/psutil/arch/solaris/v10/ifaddrs.c +++ b/psutil/arch/solaris/v10/ifaddrs.c @@ -1,4 +1,4 @@ -/* Refrences: +/* References: * https://lists.samba.org/archive/samba-technical/2009-February/063079.html * http://stackoverflow.com/questions/4139405/#4139811 * https://github.com/steve-o/openpgm/blob/master/openpgm/pgm/getifaddrs.c diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 20c01a0dac..9d89e5bb6c 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -100,7 +100,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { goto error; } - // gets cpu time informations + // gets cpu time information status = NtQuerySystemInformation( SystemProcessorPerformanceInformation, sppi, @@ -172,7 +172,7 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { if (ncpus != 0) return Py_BuildValue("I", ncpus); else - Py_RETURN_NONE; // mimick os.cpu_count() + Py_RETURN_NONE; // mimic os.cpu_count() } @@ -248,7 +248,7 @@ psutil_cpu_count_cores(PyObject *self, PyObject *args) { } else { psutil_debug("GetLogicalProcessorInformationEx() count was 0"); - Py_RETURN_NONE; // mimick os.cpu_count() + Py_RETURN_NONE; // mimic os.cpu_count() } return_none: diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index acbda301a4..f9d4bbc8a5 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -160,7 +160,7 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { } -// Check for PID existance. Return 1 if pid exists, 0 if not, -1 on error. +// Check for PID existence. Return 1 if pid exists, 0 if not, -1 on error. int psutil_pid_is_running(DWORD pid) { HANDLE hProcess; diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 21bb3e611d..b4ab6531fe 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -311,7 +311,7 @@ def spawn_testproc(cmd=None, **kwds): 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 attemps to make sure the process is in a reasonably + It also attempts to make sure the process is in a reasonably initialized state. The process is registered for cleanup on reap_children(). """ @@ -566,7 +566,7 @@ 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 resursive is True it also tries to terminate and wait() + 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 diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 1ae810f179..8d8923100b 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -115,7 +115,7 @@ def df(path): dev, total, used, free = df(part.mountpoint) self.assertEqual(part.device, dev) self.assertEqual(usage.total, total) - # 10 MB tollerance + # 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: diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index e36fb8745d..14553cd6d7 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -698,8 +698,8 @@ def test_emulate_fallbacks(self): 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()). + # 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) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 2a2af93c69..1be27ef20c 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -700,7 +700,7 @@ def test_exe(self): self.assertEqual(exe.replace(ver, ''), PYTHON_EXE.replace(ver, '')) except AssertionError: - # Tipically MACOS. Really not sure what to do here. + # Typically MACOS. Really not sure what to do here. pass out = sh([exe, "-c", "import os; print('hey')"]) @@ -1518,7 +1518,7 @@ def tearDownClass(cls): def test_misc(self): # XXX this test causes a ResourceWarning on Python 3 because - # psutil.__subproc instance doesn't get propertly freed. + # 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, diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index ed328d017f..bb5edd5c3c 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -510,7 +510,7 @@ def test_cpu_stats(self): if not AIX and name in ('ctx_switches', 'interrupts'): self.assertGreater(value, 0) - @unittest.skipIf(not HAS_CPU_FREQ, "not suported") + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq(self): def check_ls(ls): for nt in ls: diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 55b6bc7b13..4aa2518482 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -166,7 +166,7 @@ def test_disks(self): self.assertEqual(usage.total, int(wmi_part.Size)) wmi_free = int(wmi_part.FreeSpace) self.assertEqual(usage.free, wmi_free) - # 10 MB tollerance + # 10 MB tolerance if abs(usage.free - wmi_free) > 10 * 1024 * 1024: raise self.fail("psutil=%s, wmi=%s" % ( usage.free, wmi_free)) diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index e4d6ffc0f7..786e23d812 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -7,7 +7,7 @@ """ Script which downloads wheel files hosted on AppVeyor: https://ci.appveyor.com/project/giampaolo/psutil -Readapted from the original recipe of Ibarra Corretge' +Re-adapted from the original recipe of Ibarra Corretge' : http://code.saghul.net/index.php/2015/09/09/ """ diff --git a/scripts/internal/fix_flake8.py b/scripts/internal/fix_flake8.py index 14fbb4d22d..44da4a3274 100755 --- a/scripts/internal/fix_flake8.py +++ b/scripts/internal/fix_flake8.py @@ -81,7 +81,7 @@ def handle_f401(fname, lineno): def remove_lines(fname, entries): """Check if we should remove lines, then do it. - Return the numner of lines removed. + Return the number of lines removed. """ to_remove = [] for entry in entries: @@ -116,7 +116,7 @@ def remove_lines(fname, entries): def add_lines(fname, entries): """Check if we should remove lines, then do it. - Return the numner of lines removed. + Return the number of lines removed. """ EXPECTED_BLANK_LINES = ( 'E302', # 'expected 2 blank limes, found 1' diff --git a/scripts/iotop.py b/scripts/iotop.py index 91bc3b18fb..f4d62d4671 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -65,7 +65,7 @@ def printl(line, highlight=False): def poll(interval): - """Calculate IO usage by comparing IO statics before and + """Calculate IO usage by comparing IO statistics before and after the interval. Return a tuple including all currently running processes sorted by IO activity and total disks I/O activity. diff --git a/setup.py b/setup.py index 3bad6d5d5d..8284f959bb 100755 --- a/setup.py +++ b/setup.py @@ -328,7 +328,7 @@ def get_sunos_update(): # Detect Solaris 5.10, update >= 4, see: # https://github.com/giampaolo/psutil/pull/1638 if get_sunos_update() >= 4: - # MIB compliancy starts with SunOS 5.10 Update 4: + # MIB compliance starts with SunOS 5.10 Update 4: posix_extension.define_macros.append(('NEW_MIB_COMPLIANT', 1)) posix_extension.sources.append('psutil/arch/solaris/v10/ifaddrs.c') posix_extension.define_macros.append(('PSUTIL_SUNOS10', 1)) From dd57b9a109d6db9a87247d5f47303b87672dfa78 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 1 Feb 2022 22:25:52 +0100 Subject: [PATCH 0835/1714] add @guilt to list of sponsors (thanks a lot! ;-)) Signed-off-by: Giampaolo Rodola --- README.rst | 1 + docs/index.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 29bbc3f127..a1d737a7a2 100644 --- a/README.rst +++ b/README.rst @@ -141,6 +141,7 @@ Supporters + add your avatar diff --git a/docs/index.rst b/docs/index.rst index 6df7ccefb1..595aafb2d3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -89,6 +89,7 @@ Supporters +
add your avatar From 4225d799439525bea5d77935e3a73af1caf27738 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 23 Feb 2022 22:57:44 +0000 Subject: [PATCH 0836/1714] Fixes #2078: Import the inspect module on use (#2079) --- psutil/_common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 00a9c6fdf1..ef3e8664b5 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -44,8 +44,6 @@ # 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', 0)) -if PSUTIL_DEBUG: - import inspect _DEFAULT = object() __all__ = [ @@ -890,6 +888,7 @@ def print_color( def debug(msg): """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" if PSUTIL_DEBUG: + import inspect fname, lineno, func_name, lines, index = inspect.getframeinfo( inspect.currentframe().f_back) if isinstance(msg, Exception): From 8091fa5999d2ac5c83e4aaf99a5560dc63dbee4b Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 25 Feb 2022 07:41:36 +0000 Subject: [PATCH 0837/1714] Improves robustness of some tests that were too environment-specific (#2080) Signed-off-by: Steve Dower --- psutil/tests/test_process.py | 28 ++++++++++++++++++++++------ psutil/tests/test_windows.py | 28 ++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 2a2af93c69..181097e3b7 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -799,19 +799,29 @@ def test_nice(self): init = p.nice() try: if WINDOWS: - for prio in [psutil.NORMAL_PRIORITY_CLASS, - psutil.IDLE_PRIORITY_CLASS, + # A CI runner may limit our maximum priority, which will break + # this test. Instead, we test in order of increasing priority, + # and match either the expected value or the highest so far. + highest_prio = None + for prio in [psutil.IDLE_PRIORITY_CLASS, psutil.BELOW_NORMAL_PRIORITY_CLASS, - psutil.REALTIME_PRIORITY_CLASS, + psutil.NORMAL_PRIORITY_CLASS, + psutil.ABOVE_NORMAL_PRIORITY_CLASS, psutil.HIGH_PRIORITY_CLASS, - psutil.ABOVE_NORMAL_PRIORITY_CLASS]: + psutil.REALTIME_PRIORITY_CLASS]: with self.subTest(prio=prio): try: p.nice(prio) except psutil.AccessDenied: pass else: - self.assertEqual(p.nice(), prio) + new_prio = p.nice() + if CI_TESTING: + 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"): @@ -846,7 +856,13 @@ def test_username(self): username = p.username() if WINDOWS: domain, username = username.split('\\') - self.assertEqual(username, getpass.getuser()) + 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: diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 55b6bc7b13..7a843bc385 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -86,13 +86,14 @@ def test_cpu_count_vs_GetSystemInfo(self): def test_cpu_count_logical_vs_wmi(self): w = wmi.WMI() - proc = w.Win32_Processor()[0] - self.assertEqual(psutil.cpu_count(), proc.NumberOfLogicalProcessors) + 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() - proc = w.Win32_Processor()[0] - self.assertEqual(psutil.cpu_count(logical=False), proc.NumberOfCores) + 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(), @@ -213,7 +214,7 @@ def test_boot_time(self): 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, 3) + self.assertLessEqual(diff, 5) def test_boot_time_fluctuation(self): # https://github.com/giampaolo/psutil/issues/1007 @@ -351,12 +352,23 @@ def test_ctrl_signals(self): p.send_signal, signal.CTRL_BREAK_EVENT) def test_username(self): - self.assertEqual(psutil.Process().username(), - win32api.GetUserNameEx(win32con.NameSamCompatible)) + 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() + 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 From 446f4bfed1463f9016cf6bdd36f6b30700bec0ad Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 15 Mar 2022 17:17:00 +0800 Subject: [PATCH 0838/1714] fix some typos (#2085) Signed-off-by: cuishuang --- docs/DEVGUIDE.rst | 2 +- docs/index.rst | 6 +++--- make.bat | 2 +- psutil/_pslinux.py | 2 +- psutil/_pswindows.py | 2 +- psutil/arch/osx/cpu.c | 2 +- psutil/arch/solaris/v10/ifaddrs.c | 2 +- psutil/arch/windows/process_utils.c | 2 +- psutil/tests/test_linux.py | 2 +- psutil/tests/test_system.py | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index cb9545bcfe..d4e6778f28 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -115,7 +115,7 @@ Unit tests are automatically run on every ``git push`` on **Linux**, **macOS**, .. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/psutil -OpenBSD, NetBSD, AIX and Solaris does not have continuos test integration. +OpenBSD, NetBSD, AIX and Solaris does not have continuous test integration. Documentation ------------- diff --git a/docs/index.rst b/docs/index.rst index 595aafb2d3..82872a2b54 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1101,7 +1101,7 @@ Process class Here's a list of methods which can take advantage of the speedup depending on what platform you're on. - In the table below horizontal emtpy rows indicate what process methods can + In the table below horizontal empty rows indicate what process methods can be efficiently grouped together internally. The last column (speedup) shows an approximation of the speedup you can get if you call all the methods together (best case scenario). @@ -1194,8 +1194,8 @@ Process class .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support - .. versionchanged:: 5.6.3 added AIX suport - .. versionchanged:: 5.7.3 added BSD suport + .. versionchanged:: 5.6.3 added AIX support + .. versionchanged:: 5.7.3 added BSD support .. method:: create_time() diff --git a/make.bat b/make.bat index 8e60811ccf..e292bb16b6 100644 --- a/make.bat +++ b/make.bat @@ -2,7 +2,7 @@ rem ========================================================================== rem Shortcuts for various tasks, emulating UNIX "make" on Windows. -rem It is primarly intended as a shortcut for compiling / installing +rem It is primarily intended as a shortcut for compiling / installing rem psutil ("make.bat build", "make.bat install") and running tests rem ("make.bat test"). rem diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 5f149a035d..afcb78f555 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1675,7 +1675,7 @@ def _parse_stat_file(self): """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 3 (e.g ppid position 4 in + 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. diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 9966b1b4e2..31edffc1c7 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -249,7 +249,7 @@ def swap_memory(): free_system = mem[3] # Despite the name PageFile refers to total system memory here - # thus physical memory values need to be substracted to get swap values + # thus physical memory values need to be subtracted to get swap values total = total_system - total_phys free = min(total, free_system - free_phys) used = total - free diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 37141a2d31..2d94f31dd2 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -119,7 +119,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { size_t len = sizeof(curr); size_t size = sizeof(min); - // also availble as "hw.cpufrequency" but it's deprecated + // also available as "hw.cpufrequency" but it's deprecated mib[0] = CTL_HW; mib[1] = HW_CPU_FREQ; diff --git a/psutil/arch/solaris/v10/ifaddrs.c b/psutil/arch/solaris/v10/ifaddrs.c index b741a6b92b..3719c8c18a 100644 --- a/psutil/arch/solaris/v10/ifaddrs.c +++ b/psutil/arch/solaris/v10/ifaddrs.c @@ -1,4 +1,4 @@ -/* Refrences: +/* References: * https://lists.samba.org/archive/samba-technical/2009-February/063079.html * http://stackoverflow.com/questions/4139405/#4139811 * https://github.com/steve-o/openpgm/blob/master/openpgm/pgm/getifaddrs.c diff --git a/psutil/arch/windows/process_utils.c b/psutil/arch/windows/process_utils.c index acbda301a4..f9d4bbc8a5 100644 --- a/psutil/arch/windows/process_utils.c +++ b/psutil/arch/windows/process_utils.c @@ -160,7 +160,7 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { } -// Check for PID existance. Return 1 if pid exists, 0 if not, -1 on error. +// Check for PID existence. Return 1 if pid exists, 0 if not, -1 on error. int psutil_pid_is_running(DWORD pid) { HANDLE hProcess; diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index e36fb8745d..0e6c1fd70f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -698,7 +698,7 @@ def test_emulate_fallbacks(self): self.assertEqual(psutil._pslinux.cpu_count_logical(), original) assert m.called - # Let's have open() return emtpy data and make sure None is + # Let's have open() return empty 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()) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index ed328d017f..bb5edd5c3c 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -510,7 +510,7 @@ def test_cpu_stats(self): if not AIX and name in ('ctx_switches', 'interrupts'): self.assertGreater(value, 0) - @unittest.skipIf(not HAS_CPU_FREQ, "not suported") + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_cpu_freq(self): def check_ls(ls): for nt in ls: From 3b52ce5ff9be8dd37fb59109016b655f078fe499 Mon Sep 17 00:00:00 2001 From: myheroyuki <101724658+myheroyuki@users.noreply.github.com> Date: Sat, 19 Mar 2022 00:02:56 +0900 Subject: [PATCH 0839/1714] Fixed an issue that caused linux tests to fail when running on a system with multiple ipv6 addresses per interface (#2086) --- psutil/tests/test_linux.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0e6c1fd70f..a0c69c7866 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -112,21 +112,26 @@ def get_ipv4_broadcast(ifname): struct.pack('256s', ifname))[20:24]) -def get_ipv6_address(ifname): +def get_ipv6_addresses(ifname): with open("/proc/net/if_inet6", 'rt') as f: + all_fields = [] for line in f.readlines(): fields = line.split() if fields[-1] == ifname: - break - else: + all_fields.append(fields) + + if len(all_fields) ==0: raise ValueError("could not find interface %r" % ifname) - unformatted = fields[0] - groups = [] - for i in range(0, len(unformatted), 4): - groups.append(unformatted[i:i + 4]) - formatted = ":".join(groups) - packed = socket.inet_pton(socket.AF_INET6, formatted) - return socket.inet_ntop(socket.AF_INET6, packed) + + for i in range(0, 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): @@ -950,8 +955,8 @@ def test_ips(self): # That is the "zone id" portion, which usually is the name # of the network interface. address = addr.address.split('%')[0] - self.assertEqual(address, get_ipv6_address(name)) - + 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): From 48a036cb829294ee508bed6428ab927042e5d5fb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Apr 2022 17:27:22 +0200 Subject: [PATCH 0840/1714] use autopep8 to automatically fix (some) flake8 failures; get rid of old handmade script Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 1 - Makefile | 5 +- psutil/tests/test_linux.py | 6 +- scripts/internal/fix_flake8.py | 185 --------------------------------- 4 files changed, 6 insertions(+), 191 deletions(-) delete mode 100755 scripts/internal/fix_flake8.py diff --git a/MANIFEST.in b/MANIFEST.in index 13acb0fd68..1816eb5f86 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -137,7 +137,6 @@ include scripts/internal/clinter.py include scripts/internal/convert_readme.py include scripts/internal/download_wheels_appveyor.py include scripts/internal/download_wheels_github.py -include scripts/internal/fix_flake8.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py include scripts/internal/print_access_denied.py diff --git a/Makefile b/Makefile index 0ab1ea06ea..16c7bb26e3 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ TSCRIPT = psutil/tests/runner.py # Internal. DEPS = \ argparse \ + autopep8 \ check-manifest \ concurrencytest \ coverage \ @@ -206,8 +207,8 @@ lint: ## Run all linters # Fixers # =================================================================== -fix-flake8: ## Attempt to automatically fix some Python flake8 issues. - @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 --exit-zero | $(PYTHON) scripts/internal/fix_flake8.py +fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. + @git ls-files | grep \\.py$ | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 fix-imports: ## Fix imports with isort. @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index a0c69c7866..0d42d0515c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -119,8 +119,8 @@ def get_ipv6_addresses(ifname): fields = line.split() if fields[-1] == ifname: all_fields.append(fields) - - if len(all_fields) ==0: + + if len(all_fields) == 0: raise ValueError("could not find interface %r" % ifname) for i in range(0, len(all_fields)): @@ -956,7 +956,7 @@ def test_ips(self): # 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): diff --git a/scripts/internal/fix_flake8.py b/scripts/internal/fix_flake8.py deleted file mode 100755 index 14fbb4d22d..0000000000 --- a/scripts/internal/fix_flake8.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/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. - -""" -Fix some flake8 errors by rewriting files for which flake8 emitted -an error/warning. Usage (from the root dir): - - $ python3 -m flake8 --exit-zero | python3 scripts/fix_flake8.py -""" - -import shutil -import sys -import tempfile -from collections import defaultdict -from collections import namedtuple -from pprint import pprint as pp # NOQA - - -ntentry = namedtuple('ntentry', 'msg, issue, lineno, pos, descr') - - -# ===================================================================== -# utils -# ===================================================================== - - -def enter_pdb(): - from pdb import set_trace as st # trick GIT commit hook - sys.stdin = open('/dev/tty') - st() - - -def read_lines(fname): - with open(fname, 'rt') as f: - return f.readlines() - - -def read_line(fname, lineno): - with open(fname, 'rt') as f: - for i, line in enumerate(f, 1): - if i == lineno: - return line - raise ValueError("lineno too big") - - -def write_file(fname, newlines): - with tempfile.NamedTemporaryFile('wt', delete=False) as f: - for line in newlines: - f.write(line) - shutil.move(f.name, fname) - - -# ===================================================================== -# handlers -# ===================================================================== - - -def handle_f401(fname, lineno): - """ This is 'module imported but not used' - Able to handle this case: - import os - - ...but not this: - from os import listdir - """ - line = read_line(fname, lineno).strip() - if line.startswith('import '): - return True - else: - mods = line.partition(' import ')[2] # everything after import - return ',' not in mods - - -# ===================================================================== -# converters -# ===================================================================== - - -def remove_lines(fname, entries): - """Check if we should remove lines, then do it. - Return the numner of lines removed. - """ - to_remove = [] - for entry in entries: - msg, issue, lineno, pos, descr = entry - # 'module imported but not used' - if issue == 'F401' and handle_f401(fname, lineno): - to_remove.append(lineno) - # 'blank line(s) at end of file' - elif issue == 'W391': - lines = read_lines(fname) - i = len(lines) - 1 - while lines[i] == '\n': - to_remove.append(i + 1) - i -= 1 - # 'too many blank lines' - elif issue == 'E303': - howmany = descr.replace('(', '').replace(')', '') - howmany = int(howmany[-1]) - for x in range(lineno - howmany, lineno): - to_remove.append(x) - - if to_remove: - newlines = [] - for i, line in enumerate(read_lines(fname), 1): - if i not in to_remove: - newlines.append(line) - print("removing line(s) from %s" % fname) - write_file(fname, newlines) - - return len(to_remove) - - -def add_lines(fname, entries): - """Check if we should remove lines, then do it. - Return the numner of lines removed. - """ - EXPECTED_BLANK_LINES = ( - 'E302', # 'expected 2 blank limes, found 1' - 'E305') # ìexpected 2 blank lines after class or fun definition' - to_add = {} - for entry in entries: - msg, issue, lineno, pos, descr = entry - if issue in EXPECTED_BLANK_LINES: - howmany = 2 if descr.endswith('0') else 1 - to_add[lineno] = howmany - - if to_add: - newlines = [] - for i, line in enumerate(read_lines(fname), 1): - if i in to_add: - newlines.append('\n' * to_add[i]) - newlines.append(line) - print("adding line(s) to %s" % fname) - write_file(fname, newlines) - - return len(to_add) - - -# ===================================================================== -# main -# ===================================================================== - - -def build_table(): - table = defaultdict(list) - for line in sys.stdin: - line = line.strip() - if not line: - break - fields = line.split(':') - fname, lineno, pos = fields[:3] - issue = fields[3].split()[0] - descr = fields[3].strip().partition(' ')[2] - lineno, pos = int(lineno), int(pos) - table[fname].append(ntentry(line, issue, lineno, pos, descr)) - return table - - -def main(): - table = build_table() - - # remove lines (unused imports) - removed = 0 - for fname, entries in table.items(): - removed += remove_lines(fname, entries) - if removed: - print("%s lines were removed from some file(s); please re-run" % - removed) - return - - # add lines (missing \n between functions/classes) - added = 0 - for fname, entries in table.items(): - added += add_lines(fname, entries) - if added: - print("%s lines were added from some file(s); please re-run" % - added) - return - - -main() From b22babd410bd623a7812301ba733a94dcaf90c40 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Apr 2022 12:48:08 +0200 Subject: [PATCH 0841/1714] add autoflake automation tool to remove unused modules & variable Signed-off-by: Giampaolo Rodola --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 16c7bb26e3..a0a70739bf 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ TSCRIPT = psutil/tests/runner.py # Internal. DEPS = \ argparse \ + autoflake \ autopep8 \ check-manifest \ concurrencytest \ @@ -208,7 +209,8 @@ lint: ## Run all linters # =================================================================== fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. - @git ls-files | grep \\.py$ | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 + git ls-files | grep \\.py$ | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 + git ls-files | grep \\.py$ | xargs $(PYTHON) -m autoflake --in-place --remove-all-unused-imports --remove-unused-variables fix-imports: ## Fix imports with isort. @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg From c45af430d3bfb6cbeed9b72c4865bc497fd006f1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Apr 2022 14:04:42 +0200 Subject: [PATCH 0842/1714] rename test functions Signed-off-by: Giampaolo Rodola --- psutil/tests/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 21bb3e611d..0d0e00df6f 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -509,7 +509,7 @@ def sendsig(proc, sig): proc.send_signal(signal.SIGCONT) proc.send_signal(sig) - def term_subproc(proc, timeout): + def term_subprocess_proc(proc, timeout): try: sendsig(proc, sig) except OSError as err: @@ -519,7 +519,7 @@ def term_subproc(proc, timeout): raise return wait(proc, timeout) - def term_psproc(proc, timeout): + def term_psutil_proc(proc, timeout): try: sendsig(proc, sig) except psutil.NoSuchProcess: @@ -534,7 +534,7 @@ def term_pid(pid, timeout): if POSIX: return wait_pid(pid, timeout) else: - return term_psproc(proc, timeout) + return term_psutil_proc(proc, timeout) def flush_popen(proc): if proc.stdout: @@ -550,9 +550,9 @@ def flush_popen(proc): if isinstance(p, int): return term_pid(p, wait_timeout) elif isinstance(p, (psutil.Process, psutil.Popen)): - return term_psproc(p, wait_timeout) + return term_psutil_proc(p, wait_timeout) elif isinstance(p, subprocess.Popen): - return term_subproc(p, wait_timeout) + return term_subprocess_proc(p, wait_timeout) else: raise TypeError("wrong type %r" % p) finally: From 0f996734bdbd413f7dd8ce88d0c12227045609d1 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Fri, 15 Apr 2022 14:08:58 +0200 Subject: [PATCH 0843/1714] Drop Python 2.6 support (#2039) Signed-off-by: mayeut --- Makefile | 2 +- README.rst | 2 +- docs/index.rst | 4 ++-- make.bat | 2 +- psutil/_common.py | 5 +---- psutil/tests/test_process.py | 2 +- psutil/tests/test_windows.py | 2 -- scripts/internal/print_announce.py | 2 +- setup.py | 3 +-- 9 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index a0a70739bf..7e998042ae 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ DEPS = \ PY2_DEPS = \ futures \ ipaddress \ - mock==1.0.1 \ + mock \ unittest2 DEPS += `$(PYTHON) -c \ "import sys; print('$(PY2_DEPS)' if sys.version_info[0] == 2 else '')"` diff --git a/README.rst b/README.rst index a1d737a7a2..0e0a29afde 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are **2.6**, **2.7**, **3.4+** and +Supported Python versions are **2.7**, **3.4+** and `PyPy `__. Funding diff --git a/docs/index.rst b/docs/index.rst index 82872a2b54..7cf673ef48 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are **2.6**, **2.7** and **3.4+**. +Supported Python versions are **2.7** and **3.4+**. `PyPy `__ is also known to work. The psutil documentation you're reading is distributed as a single HTML page. @@ -2609,7 +2609,7 @@ Platforms support history * psutil 0.1.1 (2009-03): **FreeBSD** * psutil 0.1.0 (2009-01): **Linux, Windows, macOS** -Supported Python versions are 2.6, 2.7, 3.4+ and PyPy3. +Supported Python versions are 2.7, 3.4+ and PyPy3. Timeline ======== diff --git a/make.bat b/make.bat index e292bb16b6..b424ed2039 100644 --- a/make.bat +++ b/make.bat @@ -7,7 +7,7 @@ rem psutil ("make.bat build", "make.bat install") and running tests rem ("make.bat test"). rem rem This script is modeled after my Windows installation which uses: -rem - Visual studio 2008 for Python 2.6, 2.7 +rem - Visual studio 2008 for Python 2.7 rem - Visual studio 2010 for Python 3.4+ rem ...therefore it might not work on your Windows installation. rem diff --git a/psutil/_common.py b/psutil/_common.py index ef3e8664b5..9937eb8329 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -281,10 +281,7 @@ class Error(Exception): __module__ = 'psutil' def _infodict(self, attrs): - try: - info = collections.OrderedDict() - except AttributeError: # pragma: no cover - info = {} # Python 2.6 + info = collections.OrderedDict() for name in attrs: value = getattr(self, name, None) if value: diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 181097e3b7..ef96e7971f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1574,7 +1574,7 @@ def test_kill_terminate(self): self.assertRaises(psutil.NoSuchProcess, proc.kill) self.assertRaises(psutil.NoSuchProcess, proc.send_signal, signal.SIGTERM) - if WINDOWS and sys.version_info >= (2, 7): + if WINDOWS: self.assertRaises(psutil.NoSuchProcess, proc.send_signal, signal.CTRL_C_EVENT) self.assertRaises(psutil.NoSuchProcess, proc.send_signal, diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 7a843bc385..0333dbe554 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -338,8 +338,6 @@ def test_num_handles_increment(self): win32api.CloseHandle(handle) self.assertEqual(p.num_handles(), before) - @unittest.skipIf(not sys.version_info >= (2, 7), - "CTRL_* signals not supported") def test_ctrl_signals(self): p = psutil.Process(self.spawn_testproc().pid) p.send_signal(signal.CTRL_C_EVENT) diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index c9948c1d9f..1c22b1c1d2 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -47,7 +47,7 @@ nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap. It \ currently supports Linux, Windows, macOS, Sun Solaris, FreeBSD, OpenBSD, \ NetBSD and AIX, both 32-bit and 64-bit architectures. Supported Python \ -versions are 2.6, 2.7 and 3.4+. PyPy is also known to work. +versions are 2.7 and 3.4+. PyPy is also known to work. What's new ========== diff --git a/setup.py b/setup.py index 3bad6d5d5d..250191c9db 100755 --- a/setup.py +++ b/setup.py @@ -392,7 +392,6 @@ def main(): 'Operating System :: POSIX', 'Programming Language :: C', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: Implementation :: CPython', @@ -414,7 +413,7 @@ def main(): ) if setuptools is not None: kwargs.update( - python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", extras_require=extras_require, zip_safe=False, ) From 83bb5f8cdcbe10fadf64e62e61d011afe73b6a27 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Fri, 15 Apr 2022 08:16:01 -0400 Subject: [PATCH 0844/1714] Use a generator to avoid a needless list allocation (#1567) --- psutil/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 6deebb2633..d2f35b6855 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2075,7 +2075,7 @@ 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( From e8ee8704e83bf5220a567d73ef86daaae920e64d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Apr 2022 14:18:56 +0200 Subject: [PATCH 0845/1714] give CREDITS for #1053 Signed-off-by: Giampaolo Rodola --- CREDITS | 4 ++++ HISTORY.rst | 1 + 2 files changed, 5 insertions(+) diff --git a/CREDITS b/CREDITS index 131bbfe9f5..1e220aba2e 100644 --- a/CREDITS +++ b/CREDITS @@ -778,3 +778,7 @@ I: 1980 N: Olivier Dormond W: https://github.com/odormond I: 1956 + +N: Matthieu Darbois +W: https://github.com/mayeut +I: 2039 diff --git a/HISTORY.rst b/HISTORY.rst index 595de97548..590aaed285 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ XXXX-XX-XX **Enhancements** +- 1053_: dropped support for Python 2.6. (patch by Matthieu Darbois) - 2050_, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading ``/proc`` pseudo files line by line. This should help having more consistent results. From 6ab8a54615ddb9c2f6287047686e3f5b063440ba Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 16 Apr 2022 02:35:41 +0300 Subject: [PATCH 0846/1714] Drop Python 2.6 support (#2099) Signed-off-by: Hugo van Kemenade --- .github/workflows/build.yml | 25 ++++++++++++++++--------- .github/workflows/issues.yml | 6 ++++-- CREDITS | 4 ++++ HISTORY.rst | 4 ++-- Makefile | 4 +--- psutil/__init__.py | 7 ++----- psutil/tests/__init__.py | 11 ++++++----- psutil/tests/test_aix.py | 2 +- psutil/tests/test_bsd.py | 2 +- psutil/tests/test_connections.py | 2 +- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 2 +- psutil/tests/test_memleaks.py | 2 +- psutil/tests/test_misc.py | 2 +- psutil/tests/test_osx.py | 2 +- psutil/tests/test_posix.py | 2 +- psutil/tests/test_process.py | 2 +- psutil/tests/test_sunos.py | 2 +- psutil/tests/test_system.py | 2 +- psutil/tests/test_testutils.py | 2 +- psutil/tests/test_unicode.py | 2 +- psutil/tests/test_windows.py | 2 +- scripts/internal/winmake.py | 2 -- setup.py | 1 - 24 files changed, 50 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9c0a435d3..7a592a32c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,10 +46,12 @@ jobs: with: access_token: ${{ github.token }} - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: python-version: 3.9 + cache: pip + cache-dependency-path: .github/workflows/build.yml - name: Install cibuildwheel run: pip install cibuildwheel @@ -63,7 +65,7 @@ jobs: run: cibuildwheel . - name: Create wheels - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: wheels path: wheelhouse @@ -102,10 +104,12 @@ jobs: with: access_token: ${{ github.token }} - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: python-version: 3.9 + cache: pip + cache-dependency-path: .github/workflows/build.yml - name: Install cibuildwheel run: pip install cibuildwheel==1.12.0 @@ -114,7 +118,7 @@ jobs: run: cibuildwheel . - name: Create wheels - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: wheels path: wheelhouse @@ -135,7 +139,7 @@ jobs: with: access_token: ${{ github.token }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Run tests id: test @@ -157,8 +161,11 @@ jobs: linters: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + with: + cache: pip + cache-dependency-path: .github/workflows/build.yml - name: 'Run linters' run: | # py2 diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index fa739eab17..d5118157f1 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -12,11 +12,13 @@ jobs: runs-on: ubuntu-latest steps: # install python - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: 3.8 + cache: pip + cache-dependency-path: .github/workflows/issues.yml # install deps - name: Install deps run: python -m pip install --upgrade pip PyGithub diff --git a/CREDITS b/CREDITS index 1e220aba2e..36b04a1437 100644 --- a/CREDITS +++ b/CREDITS @@ -782,3 +782,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut I: 2039 + +N: Hugo van Kemenade +W: https://github.com/hugovk +I: 2099 diff --git a/HISTORY.rst b/HISTORY.rst index 590aaed285..8f2aaa32f7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,7 +7,7 @@ XXXX-XX-XX **Enhancements** -- 1053_: dropped support for Python 2.6. (patch by Matthieu Darbois) +- 1053_: dropped support for Python 2.6. (patches by Matthieu Darbois and Hugo van Kemenade) - 2050_, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading ``/proc`` pseudo files line by line. This should help having more consistent results. @@ -17,7 +17,7 @@ XXXX-XX-XX - 2048_: ``AttributeError`` is raised if ``psutil.Error`` class is raised manually and passed through ``str``. -- 2049_, [Linux]: `cpu_freq`_ erroneously returns ``curr`` value in GHz while +- 2049_, [Linux]: `cpu_freq()`_ erroneously returns ``curr`` value in GHz while ``min`` and ``max`` are in MHz. - 2050_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` if running in a LCX container. diff --git a/Makefile b/Makefile index 7e998042ae..a307501280 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,6 @@ TSCRIPT = psutil/tests/runner.py # Internal. DEPS = \ - argparse \ autoflake \ autopep8 \ check-manifest \ @@ -29,8 +28,7 @@ DEPS = \ PY2_DEPS = \ futures \ ipaddress \ - mock \ - unittest2 + mock DEPS += `$(PYTHON) -c \ "import sys; print('$(PY2_DEPS)' if sys.version_info[0] == 2 else '')"` # "python3 setup.py build" can be parallelized on Python >= 3.6. diff --git a/psutil/__init__.py b/psutil/__init__.py index d2f35b6855..2f2a593dbd 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -17,7 +17,7 @@ - Sun Solaris - AIX -Works with Python versions from 2.6 to 3.4+. +Works with Python versions 2.7 and 3.4+. """ from __future__ import division @@ -380,10 +380,7 @@ def _init(self, pid, _ignore_nsp=False): self._ident = (self.pid, self._create_time) def __str__(self): - try: - info = collections.OrderedDict() - except AttributeError: # pragma: no cover - info = {} # Python 2.6 + info = collections.OrderedDict() info["pid"] = self.pid if self._name: info['name'] = self._name diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 0d0e00df6f..82e0896339 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -31,6 +31,7 @@ import textwrap import threading import time +import unittest import warnings from socket import AF_INET from socket import AF_INET6 @@ -57,11 +58,6 @@ from psutil._compat import which -if PY3: - import unittest -else: - import unittest2 as unittest # requires "pip install unittest2" - try: from unittest import mock # py3 except ImportError: @@ -862,6 +858,11 @@ def __str__(self): 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 diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 6a5debfa2d..4a23b774ac 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -9,12 +9,12 @@ """AIX specific tests.""" import re +import unittest import psutil from psutil import AIX from psutil.tests import PsutilTestCase from psutil.tests import sh -from psutil.tests import unittest @unittest.skipIf(not AIX, "AIX only") diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 1ae810f179..a236810dfc 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -14,6 +14,7 @@ import os import re import time +import unittest import psutil from psutil import BSD @@ -27,7 +28,6 @@ from psutil.tests import sh from psutil.tests import spawn_testproc from psutil.tests import terminate -from psutil.tests import unittest from psutil.tests import which diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 5381608a9b..f3b1f837ce 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -9,6 +9,7 @@ import os import socket import textwrap +import unittest from contextlib import closing from socket import AF_INET from socket import AF_INET6 @@ -39,7 +40,6 @@ from psutil.tests import serialrun from psutil.tests import skip_on_access_denied from psutil.tests import tcp_socketpair -from psutil.tests import unittest from psutil.tests import unix_socketpair from psutil.tests import wait_for_file diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 9def408a90..5c927d68da 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -17,6 +17,7 @@ import sys import time import traceback +import unittest import psutil from psutil import AIX @@ -51,7 +52,6 @@ from psutil.tests import kernel_version from psutil.tests import process_namespace from psutil.tests import serialrun -from psutil.tests import unittest # =================================================================== diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0d42d0515c..8dd4caaeaf 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -20,6 +20,7 @@ import struct import textwrap import time +import unittest import warnings import psutil @@ -46,7 +47,6 @@ from psutil.tests import safe_rmpath from psutil.tests import sh from psutil.tests import skip_on_not_implemented -from psutil.tests import unittest from psutil.tests import which diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index d5baffa55f..e507e837c2 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -19,6 +19,7 @@ import functools import os +import unittest import psutil import psutil._common @@ -50,7 +51,6 @@ from psutil.tests import spawn_testproc from psutil.tests import system_namespace from psutil.tests import terminate -from psutil.tests import unittest cext = psutil._psplatform.cext diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index aa30cbd6c0..add9646eb6 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -17,6 +17,7 @@ import pickle import socket import stat +import unittest import psutil import psutil.tests @@ -51,7 +52,6 @@ from psutil.tests import mock from psutil.tests import reload_module from psutil.tests import sh -from psutil.tests import unittest # =================================================================== diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 4f4b1c2906..d0f588ad6f 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -8,6 +8,7 @@ import re import time +import unittest import psutil from psutil import MACOS @@ -20,7 +21,6 @@ from psutil.tests import sh from psutil.tests import spawn_testproc from psutil.tests import terminate -from psutil.tests import unittest if POSIX: diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index edef7e7d44..ebbf7a6e88 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -13,6 +13,7 @@ import re import subprocess import time +import unittest import psutil from psutil import AIX @@ -32,7 +33,6 @@ from psutil.tests import skip_on_access_denied from psutil.tests import spawn_testproc from psutil.tests import terminate -from psutil.tests import unittest from psutil.tests import which diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ef96e7971f..eb17e23939 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -19,6 +19,7 @@ import textwrap import time import types +import unittest import psutil from psutil import AIX @@ -62,7 +63,6 @@ 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 unittest from psutil.tests import wait_for_pid diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index ad94f774d6..dd74a49b0a 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -7,12 +7,12 @@ """Sun OS specific tests.""" import os +import unittest import psutil from psutil import SUNOS from psutil.tests import PsutilTestCase from psutil.tests import sh -from psutil.tests import unittest @unittest.skipIf(not SUNOS, "SUNOS only") diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index bb5edd5c3c..e130c935e1 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -16,6 +16,7 @@ import socket import sys import time +import unittest import psutil from psutil import AIX @@ -50,7 +51,6 @@ from psutil.tests import enum from psutil.tests import mock from psutil.tests import retry_on_failure -from psutil.tests import unittest # =================================================================== diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 89888dfe05..dd98538c0a 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -16,6 +16,7 @@ import socket import stat import subprocess +import unittest import psutil import psutil.tests @@ -48,7 +49,6 @@ from psutil.tests import system_namespace from psutil.tests import tcp_socketpair from psutil.tests import terminate -from psutil.tests import unittest from psutil.tests import unix_socketpair from psutil.tests import wait_for_file from psutil.tests import wait_for_pid diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index e635726943..3fa3f0172a 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -76,6 +76,7 @@ import os import shutil import traceback +import unittest import warnings from contextlib import closing @@ -108,7 +109,6 @@ from psutil.tests import skip_on_access_denied from psutil.tests import spawn_testproc from psutil.tests import terminate -from psutil.tests import unittest if APPVEYOR: diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 0333dbe554..b89c67a961 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -17,6 +17,7 @@ import subprocess import sys import time +import unittest import warnings import psutil @@ -36,7 +37,6 @@ from psutil.tests import sh from psutil.tests import spawn_testproc from psutil.tests import terminate -from psutil.tests import unittest if WINDOWS and not PYPY: diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 6c48297517..101a64ef71 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -50,8 +50,6 @@ "wheel", "requests" ] -if sys.version_info[:2] <= (2, 7): - DEPS.append('unittest2') if sys.version_info[:2] <= (2, 7): DEPS.append('mock') if sys.version_info[:2] <= (3, 2): diff --git a/setup.py b/setup.py index 250191c9db..a6f6eb16e9 100755 --- a/setup.py +++ b/setup.py @@ -77,7 +77,6 @@ "enum34; python_version <= '3.4'", "ipaddress; python_version < '3.0'", "mock; python_version < '3.0'", - "unittest2; python_version < '3.0'", ]} if not PYPY: extras_require['test'].extend([ From 51cd8470eee87d8f69226a58f73e29e2ddad794a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 16 Apr 2022 14:55:15 +0200 Subject: [PATCH 0847/1714] update doc Signed-off-by: Giampaolo Rodola --- Makefile | 6 +++++- docs/index.rst | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a307501280..dc18e62e3c 100644 --- a/Makefile +++ b/Makefile @@ -197,7 +197,7 @@ check-imports: ## Run isort linter. check-c-code: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py -lint: ## Run all linters +check-all: ## Run all linters ${MAKE} check-flake8 ${MAKE} check-imports ${MAKE} check-c-code @@ -213,6 +213,10 @@ fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. fix-imports: ## Fix imports with isort. @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg +fix-all: + ${MAKE} fix-flake8 + ${MAKE} fix-imports + # =================================================================== # GIT # =================================================================== diff --git a/docs/index.rst b/docs/index.rst index 7cf673ef48..045ba83b12 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2597,6 +2597,7 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= +* psutil 5.9.1 (2022-XX): drop Python 2.6 support * psutil 5.9.0 (2021-12): **MidnightBSD** * psutil 5.8.0 (2020-12): **PyPy 2** on Windows * psutil 5.7.1 (2020-07): **Windows Nano** From 33981759a67601b91d925a14403077e8d218c015 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 17 May 2022 15:44:23 +0200 Subject: [PATCH 0848/1714] #2105: give hint on how to solve the problem if PdhAddEnglishCounter fails Signed-off-by: Giampaolo Rodola --- psutil/arch/windows/wmi.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 5fad4053ea..fc7a66529e 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -71,7 +71,10 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); if (s != ERROR_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, "PdhAddEnglishCounterW failed"); + PyErr_Format( + PyExc_RuntimeError, + "PdhAddEnglishCounterW failed. Performance counters may be disabled." + ); return NULL; } From 1d41e8f42e41e6d4f2facfe42134316121e632a5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 May 2022 14:32:31 +0200 Subject: [PATCH 0849/1714] shorten test by using spawn_testproc() --- psutil/tests/test_linux.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 8dd4caaeaf..8cee2d8a04 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1776,16 +1776,8 @@ class TestProcess(PsutilTestCase): @retry_on_failure() def test_memory_full_info(self): - testfn = self.get_testfn() - src = textwrap.dedent(""" - import time - with open("%s", "w") as f: - time.sleep(10) - """ % testfn) - sproc = self.pyrun(src) - call_until(lambda: os.listdir('.'), "'%s' not in ret" % testfn) + sproc = self.spawn_testproc() p = psutil.Process(sproc.pid) - time.sleep(.1) mem = p.memory_full_info() maps = p.memory_maps(grouped=False) self.assertAlmostEqual( From f1f299527634a425cb34b621d6201fa9172d3529 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 May 2022 21:20:37 +0200 Subject: [PATCH 0850/1714] [Linux] Speedup `Process.full_memory_info()` (#2108) `Process.memory_full_info()` (reporting proecss USS/PSS/Swap memory) now reads ``/proc/pid/smaps_rollup`` instead of ``/proc/pids/smaps`` which makes it 5 times faster. Without patch: ``` ~/svn/psutil {linux-smaps-rollup}$ python3 -m timeit -s "import psutil; p = psutil.Process()" "p.memory_full_info()" 500 loops, best of 5: 518 usec per loop ``` With patch (5 times faster): ``` ~/svn/psutil {linux-smaps-rollup}$ python3 -m timeit -s "import psutil; p = psutil.Process()" "p.memory_full_info()" 2000 loops, best of 5: 111 usec per loop ``` ---- `make test-memleaks` suite, who heavily rely on `Process.memory_full_info()`, also received a nice speedup: Before patch: ``` $ make test-memleaks ---------------------------------------------------------------------- Ran 99 tests in 1.646s OK (skipped=9) SUCCESS ``` After patch: ``` $ make test-memleaks ---------------------------------------------------------------------- Ran 99 tests in 1.195s OK (skipped=9) SUCCESS ``` --- HISTORY.rst | 3 +++ psutil/_pslinux.py | 47 ++++++++++++++++++++++++++++++++------ psutil/tests/test_linux.py | 35 +++++++++++----------------- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8f2aaa32f7..49242a6ead 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,9 @@ XXXX-XX-XX ``/proc`` pseudo files line by line. This should help having more consistent results. - 2057_, [OpenBSD]: add support for `cpu_freq()`_. +- 2107_ [Linux]: `Process.memory_full_info()`_ (reporting process USS/PSS/Swap + memory) now reads ``/proc/pid/smaps_rollup`` instead of ``/proc/pids/smaps``, + which makes it 5 times faster. **Bug fixes** diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index afcb78f555..e7cef439d9 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -77,7 +77,8 @@ POWER_SUPPLY_PATH = "/sys/class/power_supply" -HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) +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") @@ -1875,18 +1876,42 @@ def memory_info(self): [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: @wrap_exceptions - def memory_full_info( + 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). + uss = pss = swap = 0 + try: + 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 + except ProcessLookupError: # happens on readline() + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + return (uss, pss, swap) + + @wrap_exceptions + 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+)")): - basic_mem = self.memory_info() + # /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 @@ -1905,12 +1930,20 @@ 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) + + def memory_full_info(self): + if HAS_PROC_SMAPS_ROLLUP: # faster + uss, pss, swap = self._parse_smaps_rollup() + 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): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 8dd4caaeaf..51e8be51be 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1775,28 +1775,19 @@ def open_mock(name, *args, **kwargs): class TestProcess(PsutilTestCase): @retry_on_failure() - def test_memory_full_info(self): - testfn = self.get_testfn() - src = textwrap.dedent(""" - import time - with open("%s", "w") as f: - time.sleep(10) - """ % testfn) - sproc = self.pyrun(src) - 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) + 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( - mem.uss, sum([x.private_dirty + x.private_clean for x in maps]), + 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) + pss, sum([x.pss for x in maps]), delta=4096) self.assertAlmostEqual( - mem.swap, sum([x.swap for x in maps]), delta=4096) + swap, sum([x.swap for x in maps]), delta=4096) - def test_memory_full_info_mocked(self): + def test_parse_smaps_mocked(self): # See: https://github.com/giampaolo/psutil/issues/1222 with mock_open_content( "/proc/%s/smaps" % os.getpid(), @@ -1823,12 +1814,12 @@ def test_memory_full_info_mocked(self): Locked: 19 kB VmFlags: rd ex """).encode()) as m: - p = psutil.Process() - mem = p.memory_full_info() + p = psutil._pslinux.Process(os.getpid()) + uss, pss, swap = p._parse_smaps() assert m.called - self.assertEqual(mem.uss, (6 + 7 + 14) * 1024) - self.assertEqual(mem.pss, 3 * 1024) - self.assertEqual(mem.swap, 15 * 1024) + 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") From 992dfe4620bbda3316b54c53b63236f3159aa5f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 May 2022 22:09:40 +0200 Subject: [PATCH 0851/1714] pre release --- HISTORY.rst | 11 ++++++----- docs/index.rst | 6 +++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 49242a6ead..d91cf461cf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,18 +1,19 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.1 (IN DEVELOPMENT) -====================== +5.9.1 +===== -XXXX-XX-XX +2022-05-20 **Enhancements** -- 1053_: dropped support for Python 2.6. (patches by Matthieu Darbois and Hugo van Kemenade) +- 1053_: drop Python 2.6 support. (patches by Matthieu Darbois and Hugo van + Kemenade) - 2050_, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading ``/proc`` pseudo files line by line. This should help having more consistent results. - 2057_, [OpenBSD]: add support for `cpu_freq()`_. -- 2107_ [Linux]: `Process.memory_full_info()`_ (reporting process USS/PSS/Swap +- 2107_, [Linux]: `Process.memory_full_info()`_ (reporting process USS/PSS/Swap memory) now reads ``/proc/pid/smaps_rollup`` instead of ``/proc/pids/smaps``, which makes it 5 times faster. diff --git a/docs/index.rst b/docs/index.rst index 045ba83b12..8d663c7ece 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2597,7 +2597,7 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= -* psutil 5.9.1 (2022-XX): drop Python 2.6 support +* psutil 5.9.1 (2022-05): drop Python 2.6 support * psutil 5.9.0 (2021-12): **MidnightBSD** * psutil 5.8.0 (2020-12): **PyPy 2** on Windows * psutil 5.7.1 (2020-07): **Windows Nano** @@ -2615,6 +2615,10 @@ Supported Python versions are 2.7, 3.4+ and PyPy3. Timeline ======== +- 2022-05-20: + `5.9.1 `__ - + `what's new `__ - + `diff `__ - 2021-12-29: `5.9.0 `__ - `what's new `__ - From 1fa6e3c1f81a312c8f652e8ef446dbdd3f72c36c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 May 2022 22:13:49 +0200 Subject: [PATCH 0852/1714] remove tidelift.py script --- MANIFEST.in | 1 - Makefile | 4 ---- scripts/internal/tidelift.py | 43 ------------------------------------ 3 files changed, 48 deletions(-) delete mode 100755 scripts/internal/tidelift.py diff --git a/MANIFEST.in b/MANIFEST.in index 1816eb5f86..c9cd85e991 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -147,7 +147,6 @@ include scripts/internal/print_hashes.py include scripts/internal/print_timeline.py include scripts/internal/print_wheels.py include scripts/internal/purge_installation.py -include scripts/internal/tidelift.py include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py diff --git a/Makefile b/Makefile index dc18e62e3c..d544dd1c24 100644 --- a/Makefile +++ b/Makefile @@ -270,9 +270,6 @@ check-sdist: ## Create source distribution and checks its sanity (MANIFEST) build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" -tidelift-relnotes: ## upload release notes from HISTORY - $(PYTHON) scripts/internal/tidelift.py - pre-release: ## Check if we're ready to produce a new release. ${MAKE} check-sdist ${MAKE} install @@ -296,7 +293,6 @@ release: ## Create a release (down/uploads tar.gz, wheels, git tag release). $(PYTHON) -c "import subprocess, sys; out = subprocess.check_output('git diff --quiet && git diff --cached --quiet', shell=True).strip(); sys.exit('there are uncommitted changes:\n%s' % out) if out else 0 ;" $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release - ${MAKE} tidelift-relnotes check-manifest: ## Inspect MANIFEST.in file. $(PYTHON) -m check_manifest -v $(ARGS) diff --git a/scripts/internal/tidelift.py b/scripts/internal/tidelift.py deleted file mode 100755 index 9470fc8521..0000000000 --- a/scripts/internal/tidelift.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/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. - -""" -Update news entry of Tidelift with latest HISTORY.rst section. -Put your Tidelift API token in a file first: -~/.tidelift.token -""" - -from __future__ import print_function - -import os - -import requests - -import psutil -from psutil.tests import import_module_by_path - - -def upload_relnotes(package, version, text, token): - url = "https://api.tidelift.com/external-api/" + \ - "lifting/pypi/%s/release-notes/%s" % (package, version) - res = requests.put( - url=url, - data=text.encode('utf8'), - headers={"Authorization": "Bearer: %s" % token}) - print(version, res.status_code, res.text) - res.raise_for_status() - - -def main(): - here = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(here, "print_announce.py") - get_changes = import_module_by_path(path).get_changes - with open(os.path.expanduser("~/.tidelift.token")) as f: - token = f.read().strip() - upload_relnotes('psutil', psutil.__version__, get_changes(), token) - - -if __name__ == "__main__": - main() From c14744db097b1955f2b668dc753b2d2439db0bdf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 May 2022 22:39:15 +0200 Subject: [PATCH 0853/1714] fix print_announce.py --- scripts/internal/print_announce.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 1c22b1c1d2..fae53ea3e2 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -92,10 +92,8 @@ def get_changes(): for i, line in enumerate(lines): line = lines.pop(0) line = line.rstrip() - if re.match(r"^- \d+_: ", line): - num, _, rest = line.partition(': ') - num = ''.join([x for x in num if x.isdigit()]) - line = "- #%s: %s" % (num, rest) + if re.match(r"^- \d+_", line): + line = re.sub(r"^- (\d+)_", r"- #\1", line) if line.startswith('===='): break From 379598312f60bf414afa8bf549f7f26af9e578ea Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 May 2022 14:20:31 +0200 Subject: [PATCH 0854/1714] introduce flake8-bugbear code checker Signed-off-by: Giampaolo Rodola --- .flake8 | 10 ++++++---- Makefile | 15 ++++++++------- psutil/__init__.py | 2 +- psutil/_compat.py | 10 +++++----- psutil/tests/test_misc.py | 2 +- scripts/internal/convert_readme.py | 2 +- scripts/internal/print_announce.py | 2 +- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.flake8 b/.flake8 index 1244cd48bf..2002578523 100644 --- a/.flake8 +++ b/.flake8 @@ -1,15 +1,17 @@ # Configuration file for flake 8. This is used by "make lint" and by the # GIT commit hook script. -# T001 = print() statement [flake8] ignore = - # line break after binary operator - W504 + W504 # line break after binary operator + # --- flake8-bugbear + B007 # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. + B014 # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. + B008 # Do not perform function calls in argument defaults. per-file-ignores = + # T001 = print() statement setup.py:T001 scripts/*:T001 - scripts/internal/convert_readme.py:E501,T001 psutil/tests/runner.py:T001 psutil/tests/test_memleaks.py:T001 .github/workflows/*:T001 diff --git a/Makefile b/Makefile index d544dd1c24..838d7b473c 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ DEPS = \ coverage \ flake8 \ flake8-print \ + flake8-bugbear \ isort \ pyperf \ pypinfo \ @@ -188,19 +189,19 @@ test-coverage: ## Run test coverage. # Linters # =================================================================== -check-flake8: ## Run flake8 linter. +flake8: ## Run flake8 linter. @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 -check-imports: ## Run isort linter. +isort: ## Run isort linter. @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg --check-only -check-c-code: ## Run C linter. +c-linter: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py -check-all: ## Run all linters - ${MAKE} check-flake8 - ${MAKE} check-imports - ${MAKE} check-c-code +lint-all: ## Run all linters + ${MAKE} flake8 + ${MAKE} isort + ${MAKE} c-linter # =================================================================== # Fixers diff --git a/psutil/__init__.py b/psutil/__init__.py index 2f2a593dbd..1ba0c19f17 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -228,7 +228,7 @@ 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__") + "version of psutil" % _psplatform.cext.__file__ if hasattr(_psplatform.cext, 'version'): msg += " (%s instead of %s)" % ( '.'.join([x for x in str(_psplatform.cext.version)]), __version__) diff --git a/psutil/_compat.py b/psutil/_compat.py index 251e595f7c..52e762b19e 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -232,8 +232,8 @@ def __hash__(self): return self.hashvalue def _make_key(args, kwds, typed, - kwd_mark=(object(), ), - fasttypes=set((int, str, frozenset, type(None))), + kwd_mark=(_SENTINEL, ), + fasttypes=set((int, str, frozenset, type(None))), # noqa sorted=sorted, tuple=tuple, type=type, len=len): key = args if kwds: @@ -442,9 +442,9 @@ class SubprocessTimeoutExpired: except ImportError: @contextlib.contextmanager def redirect_stderr(new_target): - original = getattr(sys, "stderr") + original = sys.stderr try: - setattr(sys, "stderr", new_target) + sys.stderr = new_target yield new_target finally: - setattr(sys, "stderr", original) + sys.stderr = original diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index add9646eb6..90b49c1f9b 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -402,7 +402,7 @@ def test_supports_ipv6(self): supports_ipv6.cache_clear() assert s.called else: - with self.assertRaises(Exception): + with self.assertRaises(socket.error): sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) try: sock.bind(("::1", 0)) diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index cca7dcb001..81f7bdb6dc 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -36,7 +36,7 @@ `Add your logo `__. -Example usages""" +Example usages""" # noqa def main(): diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index fae53ea3e2..c2113f3677 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -83,7 +83,7 @@ def get_changes(): block = [] # eliminate the part preceding the first block - for i, line in enumerate(lines): + for line in lines: line = lines.pop(0) if line.startswith('===='): break From 54c0648bd1d7552d27c577d65f70c25023001071 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 May 2022 15:26:09 +0200 Subject: [PATCH 0855/1714] integrate flake8-blind-except plugin Signed-off-by: Giampaolo Rodola --- .flake8 | 27 ++++++++++++++++----------- Makefile | 3 ++- scripts/internal/git_pre_commit.py | 8 ++++---- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.flake8 b/.flake8 index 2002578523..c9a9cbb638 100644 --- a/.flake8 +++ b/.flake8 @@ -3,15 +3,20 @@ [flake8] ignore = - W504 # line break after binary operator - # --- flake8-bugbear - B007 # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. - B014 # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. - B008 # Do not perform function calls in argument defaults. + W504 # line break after binary operator + + # --- flake8-bugbear plugin + B007 # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. + B014 # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. + B008 # Do not perform function calls in argument defaults. + + # --- flake8-blind-except plugin + B902 # blind except Exception: statement + per-file-ignores = - # T001 = print() statement - setup.py:T001 - scripts/*:T001 - psutil/tests/runner.py:T001 - psutil/tests/test_memleaks.py:T001 - .github/workflows/*:T001 + # T001, T201 = print() statement (flake8-print plugin) + setup.py:T001,T201 + scripts/*:T001,T201 + psutil/tests/runner.py:T001,T201 + psutil/tests/test_memleaks.py:T001,T201 + .github/workflows/*:T001,T201 diff --git a/Makefile b/Makefile index 838d7b473c..c67f7df0b2 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,9 @@ DEPS = \ concurrencytest \ coverage \ flake8 \ - flake8-print \ flake8-bugbear \ + flake8-print \ + flake8-blind-except \ isort \ pyperf \ pypinfo \ diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index c6f223bba9..c86c9f936b 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -113,10 +113,10 @@ def main(): if "pdb.set_trace" in line: print("%s:%s %s" % (path, lineno, line)) return exit("you forgot a pdb in your python code") - # bare except clause - if "except:" in line and not line.endswith("# NOQA"): - print("%s:%s %s" % (path, lineno, line)) - return exit("bare except clause") + # # bare except clause (now provided by flake8-blind-except plugin) + # if "except:" in line and not line.endswith("# NOQA"): + # print("%s:%s %s" % (path, lineno, line)) + # return exit("bare except clause") # Python linters if py_files: From 4f053312934cdca739c3fefe4ada8a4f2d096bdd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 May 2022 15:34:10 +0200 Subject: [PATCH 0856/1714] add flake8-debugger plugin to detect pdb/set_trace() lines --- Makefile | 3 ++- scripts/internal/git_pre_commit.py | 8 ++++---- scripts/internal/winmake.py | 6 +++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index c67f7df0b2..38cf8df739 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,10 @@ DEPS = \ concurrencytest \ coverage \ flake8 \ + flake8-blind-except \ flake8-bugbear \ + flake8-debugger \ flake8-print \ - flake8-blind-except \ isort \ pyperf \ pypinfo \ diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index c86c9f936b..46b3bc5335 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -109,10 +109,10 @@ def main(): print("%s:%s %r" % (path, lineno, line)) return exit("space at end of line") line = line.rstrip() - # pdb - if "pdb.set_trace" in line: - print("%s:%s %s" % (path, lineno, line)) - return exit("you forgot a pdb in your python code") + # # pdb (now provided by flake8-debugger plugin) + # if "pdb.set_trace" in line: + # print("%s:%s %s" % (path, lineno, line)) + # return exit("you forgot a pdb in your python code") # # bare except clause (now provided by flake8-blind-except plugin) # if "except:" in line and not line.endswith("# NOQA"): # print("%s:%s %s" % (path, lineno, line)) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 101a64ef71..6dd1a9b797 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -41,14 +41,18 @@ DEPS = [ "coverage", "flake8", + "flake8-blind-except", + "flake8-bugbear", + "flake8-debugger", + "flake8-print", "nose", "pdbpp", "pip", "pyperf", "pyreadline", + "requests" "setuptools", "wheel", - "requests" ] if sys.version_info[:2] <= (2, 7): DEPS.append('mock') From ddea4072684561fc8fe754a7f2baf0cc6a787c33 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 May 2022 17:52:05 +0200 Subject: [PATCH 0857/1714] add flake8-quotes plugin Signed-off-by: Giampaolo Rodola --- .flake8 | 3 +++ Makefile | 1 + psutil/_psbsd.py | 2 +- psutil/tests/__init__.py | 6 +++--- psutil/tests/test_misc.py | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.flake8 b/.flake8 index c9a9cbb638..29fdec97e4 100644 --- a/.flake8 +++ b/.flake8 @@ -13,6 +13,9 @@ ignore = # --- flake8-blind-except plugin B902 # blind except Exception: statement + # --- flake8-quotes plugin + Q000 # Double quotes found but single quotes preferred + per-file-ignores = # T001, T201 = print() statement (flake8-print plugin) setup.py:T001,T201 diff --git a/Makefile b/Makefile index 38cf8df739..9893b56618 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ DEPS = \ flake8-bugbear \ flake8-debugger \ flake8-print \ + flake8-quotes \ isort \ pyperf \ pypinfo \ diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index c4200cce15..fd518985b3 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -453,7 +453,7 @@ def sensors_battery(): return _common.sbattery(percent, secsleft, power_plugged) def sensors_temperatures(): - "Return CPU cores temperatures if available, else an empty dict." + """Return CPU cores temperatures if available, else an empty dict.""" ret = defaultdict(list) num_cpus = cpu_count_logical() for cpu in range(num_cpus): diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 82e0896339..878af41172 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -739,7 +739,7 @@ def call_until(fun, expr): def safe_rmpath(path): - "Convenience function for removing temporary test files or dirs" + """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 @@ -772,7 +772,7 @@ def retry_fun(fun): def safe_mkdir(dir): - "Convenience function for creating a directory" + """Convenience function for creating a directory.""" try: os.mkdir(dir) except FileExistsError: @@ -781,7 +781,7 @@ def safe_mkdir(dir): @contextlib.contextmanager def chdir(dirname): - "Context manager which temporarily changes the current directory." + """Context manager which temporarily changes the current directory.""" curdir = os.getcwd() try: os.chdir(dirname) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 90b49c1f9b..e22789c149 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -296,7 +296,7 @@ class TestCommonModule(PsutilTestCase): def test_memoize(self): @memoize def foo(*args, **kwargs): - "foo docstring" + """foo docstring""" calls.append(None) return (args, kwargs) From 5ca68709c44885f6902820e8dcb9fcff1cc1e33b Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Sun, 5 Jun 2022 01:11:41 +0200 Subject: [PATCH 0858/1714] Fix AppVeyor CI (#2111) --- appveyor.yml | 1 + scripts/internal/winmake.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 83e570ae12..3c26939eb4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -124,4 +124,5 @@ only_commits: - psutil/arch/windows/* - psutil/tests/* - scripts/* + - scripts/internal/* - setup.py diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 6dd1a9b797..bb88005199 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -42,7 +42,6 @@ "coverage", "flake8", "flake8-blind-except", - "flake8-bugbear", "flake8-debugger", "flake8-print", "nose", @@ -50,10 +49,13 @@ "pip", "pyperf", "pyreadline", - "requests" + "requests", "setuptools", "wheel", ] + +if sys.version_info[:2] >= (3, 5): + DEPS.append('flake8-bugbear') if sys.version_info[:2] <= (2, 7): DEPS.append('mock') if sys.version_info[:2] <= (3, 2): From 8151bbe1ec2057a9c44eaefdd23e4f59788147b0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 8 Jun 2022 00:42:49 +0200 Subject: [PATCH 0859/1714] update flake8-ers Signed-off-by: Giampaolo Rodola --- .flake8 | 33 ++++++++++++++++++--------------- Makefile | 7 ++++--- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.flake8 b/.flake8 index 29fdec97e4..8be0bfcfd5 100644 --- a/.flake8 +++ b/.flake8 @@ -3,23 +3,26 @@ [flake8] ignore = - W504 # line break after binary operator + W504 # line break after binary operator - # --- flake8-bugbear plugin - B007 # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. - B014 # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. - B008 # Do not perform function calls in argument defaults. + # --- flake8-bugbear plugin + B007 # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. + B014 # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. + B008 # Do not perform function calls in argument defaults. - # --- flake8-blind-except plugin - B902 # blind except Exception: statement + # --- flake8-blind-except plugin + B902 # blind except Exception: statement - # --- flake8-quotes plugin - Q000 # Double quotes found but single quotes preferred + # --- flake8-quotes plugin + Q000 # Double quotes found but single quotes preferred + + # --- flake8-quotes naming; disable all except N804 and N805 + N801, N802, N803, N806, N807, N811, N812, N813, N814, N815, N816, N817, N818 per-file-ignores = - # T001, T201 = print() statement (flake8-print plugin) - setup.py:T001,T201 - scripts/*:T001,T201 - psutil/tests/runner.py:T001,T201 - psutil/tests/test_memleaks.py:T001,T201 - .github/workflows/*:T001,T201 + # T001, T201 = print() statement (flake8-print plugin) + setup.py:T001,T201 + scripts/*:T001,T201 + psutil/tests/runner.py:T001,T201 + psutil/tests/test_memleaks.py:T001,T201 + .github/workflows/*:T001,T201 diff --git a/Makefile b/Makefile index 9893b56618..4910c13e5b 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ DEPS = \ flake8-print \ flake8-quotes \ isort \ + pep8-naming \ pyperf \ pypinfo \ requests \ @@ -211,13 +212,13 @@ lint-all: ## Run all linters # =================================================================== fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. - git ls-files | grep \\.py$ | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 - git ls-files | grep \\.py$ | xargs $(PYTHON) -m autoflake --in-place --remove-all-unused-imports --remove-unused-variables + @git ls-files '*.py' | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 + @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --remove-all-unused-imports --remove-unused-variables fix-imports: ## Fix imports with isort. @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg -fix-all: +fix-all: ## Run all code fixers. ${MAKE} fix-flake8 ${MAKE} fix-imports From 375214708b61cf2c7f671c0855484d81e62a2bdd Mon Sep 17 00:00:00 2001 From: Torsten-B <99562466+Torsten-B@users.noreply.github.com> Date: Fri, 24 Jun 2022 13:18:39 +0200 Subject: [PATCH 0860/1714] =?UTF-8?q?Implicitly=20include=20?= =?UTF-8?q?=20so=20that=20=5F=5FFreeBSD=5Fversion=20checks=20wor=E2=80=A6?= =?UTF-8?q?=20(#2114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CREDITS | 3 +++ HISTORY.rst | 5 +++++ psutil/arch/freebsd/mem.c | 1 + psutil/arch/freebsd/proc_socks.c | 1 + psutil/arch/freebsd/sys_socks.c | 1 + 5 files changed, 11 insertions(+) diff --git a/CREDITS b/CREDITS index 36b04a1437..61e7545315 100644 --- a/CREDITS +++ b/CREDITS @@ -786,3 +786,6 @@ I: 2039 N: Hugo van Kemenade W: https://github.com/hugovk I: 2099 + +N: Torsten Blum +I: 2114 diff --git a/HISTORY.rst b/HISTORY.rst index d91cf461cf..c270f7d58d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,10 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +**Bug fixes** + +- 2113, [FreeBSD]: __FreeBSD_version used in mem.c without including + + 5.9.1 ===== diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index f5374ef4e2..3326f63a1d 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -6,6 +6,7 @@ #include +#include #include #include #include diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index cdf5770b6f..737467a8cb 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -8,6 +8,7 @@ */ #include +#include #include #include // for struct xsocket #include diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 9f7cf8d555..56c89724fb 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -10,6 +10,7 @@ */ #include +#include #include #include #include // for struct xsocket From 84e9ffa7b55811dc7c22c04ccb7002bd891486d5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Jun 2022 15:05:14 +0200 Subject: [PATCH 0861/1714] make fix-flake8: parallelize autoflake8 execution ...by pip-installing a PR I provided for autoflake8 packages which adds --jobs option to the tool, see https://github.com/PyCQA/autoflake/pull/107 Signed-off-by: Giampaolo Rodola --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4910c13e5b..8954e72ffd 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ TSCRIPT = psutil/tests/runner.py # Internal. DEPS = \ - autoflake \ + git+https://github.com/PyCQA/autoflake.git@refs/pull/107/head \ autopep8 \ check-manifest \ concurrencytest \ @@ -213,7 +213,7 @@ lint-all: ## Run all linters fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. @git ls-files '*.py' | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 - @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --remove-all-unused-imports --remove-unused-variables + @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --jobs 0 --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys fix-imports: ## Fix imports with isort. @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg From be85beb5227c3288a44711abeda140f10ed6c5f4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Jul 2022 03:50:21 +0200 Subject: [PATCH 0862/1714] refactor Makefile a bit Signed-off-by: Giampaolo Rodola --- Makefile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 8954e72ffd..1234fc6a80 100644 --- a/Makefile +++ b/Makefile @@ -44,10 +44,11 @@ BUILD_OPTS = `$(PYTHON) -c \ print('--parallel %s' % cpus if cpus > 1 else '')"` # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ - "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` + "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_DEBUG=1 -all: help +# if make is invoked with no arg, default to `make help` +.DEFAULT_GOAL := help # =================================================================== # Install @@ -75,9 +76,7 @@ clean: ## Remove all build files. docs/_build/ \ htmlcov/ -_: - -build: _ ## Compile (in parallel) without installing. +build: ## Compile (in parallel) without installing. @# "build_ext -i" copies compiled *.so files in ./psutil directory in order @# to allow "import psutil" when using the interactive interpreter from @# within this directory. From a3e569a7adadba6988d9b26692baef91e9d7e959 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Jul 2022 23:19:53 +0200 Subject: [PATCH 0863/1714] remove 'import setuptools' from 'make install' as it's unnecessary and slow --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 1234fc6a80..0d1ef9583d 100644 --- a/Makefile +++ b/Makefile @@ -83,8 +83,6 @@ build: ## Compile (in parallel) without installing. PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) install: ## Install this package as current user in "edit" mode. - # make sure setuptools is installed (needed for 'develop' / edit mode) - $(PYTHON) -c "import setuptools" ${MAKE} build PYTHONWARNINGS=all $(PYTHON) setup.py develop $(INSTALL_OPTS) $(PYTHON) -c "import psutil" # make sure it actually worked From c13017f04a721188f4da7822b503d9fad002805d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Jul 2022 17:15:37 +0200 Subject: [PATCH 0864/1714] subprocess; avoid using shell=True during tests --- psutil/tests/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 878af41172..1647712999 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -21,6 +21,7 @@ import random import re import select +import shlex import shutil import signal import socket @@ -450,14 +451,14 @@ 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) + 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: From d4eea1f8fc138b39ea78b3bf555c2e0438f2404e Mon Sep 17 00:00:00 2001 From: garrisoncarter <31416563+garrisoncarter@users.noreply.github.com> Date: Mon, 11 Jul 2022 00:57:14 -0700 Subject: [PATCH 0865/1714] [Linux] `net_if_stats()` returns an incorrect speed value for 100GbE network cards (#2096) --- HISTORY.rst | 2 ++ psutil/_psutil_linux.c | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index c270f7d58d..d4c68192c7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -30,6 +30,8 @@ ``min`` and ``max`` are in MHz. - 2050_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` if running in a LCX container. +- 2095_, [Linux]: `net_if_stats()` returns incorrect interface speed for 100GbE + network cards 5.9.0 ===== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 70cf5d1bb2..c59050cb05 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -417,6 +417,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { int sock = 0; int ret; int duplex; + __u32 uint_speed; int speed; struct ifreq ifr; struct ethtool_cmd ethcmd; @@ -438,7 +439,15 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { if (ret != -1) { duplex = ethcmd.duplex; - speed = ethcmd.speed; + // speed is returned from ethtool as a __u32 ranging from 0 to INT_MAX + // or SPEED_UNKNOWN (-1) + uint_speed = ethtool_cmd_speed(ðcmd); + if (uint_speed == (__u32)SPEED_UNKNOWN || uint_speed > INT_MAX) { + speed = 0; + } + else { + speed = (int)uint_speed; + } } else { if ((errno == EOPNOTSUPP) || (errno == EINVAL)) { From 04e7aa604155736cce0abcc15c9b7b63d941b0e9 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Thu, 14 Jul 2022 08:50:58 +1000 Subject: [PATCH 0866/1714] docs: fix simple typo, repeadetly -> repeatedly (#2123) --- psutil/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 878af41172..4d514a33f8 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1002,7 +1002,7 @@ def _check_fds(self, fun): def _call_ntimes(self, fun, times): """Get 2 distinct memory samples, before and after having - called fun repeadetly, and return the memory difference. + called fun repeatedly, and return the memory difference. """ gc.collect(generation=1) mem1 = self._get_mem() From 57ed46de8a988e7ab26279c2a967fb15b05397a3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 26 Jul 2022 22:45:18 +0200 Subject: [PATCH 0867/1714] linux: skip test if ifconfig CLI is not installed --- psutil/tests/test_linux.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 51e8be51be..41645e3bc9 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -976,6 +976,7 @@ def test_ips(self): @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: @@ -996,6 +997,7 @@ def test_mtu(self): @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): From df3fba560125a1694140418458d85076ebc546fd Mon Sep 17 00:00:00 2001 From: Ben Raz Date: Sun, 31 Jul 2022 08:21:07 +0300 Subject: [PATCH 0868/1714] Fix missing whitespace in try-except caluses (#2127) --- psutil/_psbsd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index fd518985b3..56acedb3a0 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -326,11 +326,11 @@ def cpu_freq(): if available_freq: try: min_freq = int(available_freq.split(" ")[-1].split("/")[0]) - except(IndexError, ValueError): + except (IndexError, ValueError): min_freq = None try: max_freq = int(available_freq.split(" ")[0].split("/")[0]) - except(IndexError, ValueError): + except (IndexError, ValueError): max_freq = None ret.append(_common.scpufreq(current, min_freq, max_freq)) return ret From 84219ad536370eda325d27947831be270e7077b0 Mon Sep 17 00:00:00 2001 From: Thomas Klausner Date: Thu, 18 Aug 2022 19:16:05 +0200 Subject: [PATCH 0869/1714] [NetBSD] two fixes for swap code (#2128) --- CREDITS | 2 +- HISTORY.rst | 1 + psutil/arch/netbsd/specific.c | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CREDITS b/CREDITS index 61e7545315..c697e21446 100644 --- a/CREDITS +++ b/CREDITS @@ -85,7 +85,7 @@ I: 18 N: Thomas Klausner W: https://github.com/0-wiz-0 D: NetBSD implementation (co-author). -I: 557 +I: 557, 2128 N: Ryo Onodera W: https://github.com/ryoon diff --git a/HISTORY.rst b/HISTORY.rst index d4c68192c7..7c99a4f62a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,7 @@ - 2113, [FreeBSD]: __FreeBSD_version used in mem.c without including +- 2128, [NetBSD]: fix bugs in swap code 5.9.1 ===== diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c index 4e286e5ee9..11b8c1dd25 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/specific.c @@ -459,7 +459,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { (unsigned long long) uv.active << uv.pageshift, // active (unsigned long long) uv.inactive << uv.pageshift, // inactive (unsigned long long) uv.wired << uv.pageshift, // wired - (unsigned long long) uv.filepages + uv.execpages * pagesize, // cached + (unsigned long long) (uv.filepages + uv.execpages) * pagesize, // cached // These are determined from /proc/meminfo in Python. (unsigned long long) 0, // buffers (unsigned long long) 0 // shared @@ -495,8 +495,8 @@ psutil_swap_mem(PyObject *self, PyObject *args) { swap_total = swap_free = 0; for (i = 0; i < nswap; i++) { if (swdev[i].se_flags & SWF_ENABLE) { - swap_total += swdev[i].se_nblks * DEV_BSIZE; - swap_free += (swdev[i].se_nblks - swdev[i].se_inuse) * DEV_BSIZE; + swap_total += (uint64_t)swdev[i].se_nblks * DEV_BSIZE; + swap_free += (uint64_t)(swdev[i].se_nblks - swdev[i].se_inuse) * DEV_BSIZE; } } free(swdev); From 4446e3bd5fd29e0598f2ca6672d5ffe8ca40d442 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 19 Aug 2022 01:16:47 +0800 Subject: [PATCH 0870/1714] fix sphinx warning: unexpected unindent (#2118) --- psutil/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 1ba0c19f17..614c3fcc0b 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -325,7 +325,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): From d2fab7c8de16b6ffbfb0faa3e832845bc1ef0df0 Mon Sep 17 00:00:00 2001 From: Thomas Klausner Date: Sat, 27 Aug 2022 14:25:06 +0000 Subject: [PATCH 0871/1714] [NetBSD] get data about cached memory usage from /proc/meminfo as well (#2088) --- psutil/_psbsd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 56acedb3a0..8672ca6eaa 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -189,6 +189,8 @@ def virtual_memory(): buffers = int(line.split()[1]) * 1024 elif line.startswith(b'MemShared:'): shared = int(line.split()[1]) * 1024 + elif line.startswith(b'Cached:'): + cached = int(line.split()[1]) * 1024 avail = inactive + cached + free used = active + wired + cached percent = usage_percent((total - avail), total, round_=1) From c7c7bbcfc762d73a50d3cb2ad6f277dad7bb9a6b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 2 Sep 2022 11:38:20 +0000 Subject: [PATCH 0872/1714] FreeBSD / pids(): increase buf size if not enough ...dynamically, in case of ENOMEM, instead of crashing. Fixes #2093. --- HISTORY.rst | 4 +++- Makefile | 1 + psutil/arch/freebsd/proc.c | 43 ++++++++++++++++++++++++++------------ 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7c99a4f62a..9bb7d5726f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -31,7 +31,9 @@ ``min`` and ``max`` are in MHz. - 2050_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` if running in a LCX container. -- 2095_, [Linux]: `net_if_stats()` returns incorrect interface speed for 100GbE +- 2093_, [FreeBSD]: `psutil.pids()` may fail with ENOMEM. Dynamically increase + the ``malloc()`` buffer size until it's big enough. +- 2095_, [Linux]: `net_if_stats()` returns incorrect interface speed for 100GbE network cards 5.9.0 diff --git a/Makefile b/Makefile index 0d1ef9583d..4bcea213fd 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,7 @@ clean: ## Remove all build files. docs/_build/ \ htmlcov/ +.PHONY: build build: ## Compile (in parallel) without installing. @# "build_ext -i" copies compiled *.so files in ./psutil directory in order @# to allow "import psutil" when using the interactive interpreter from diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index a2e130b558..214dbc4822 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -83,6 +83,7 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { struct kinfo_proc *buf = NULL; int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; size_t length = 0; + size_t max_length = 12 * 1024 * 1024; // 12MB assert(procList != NULL); assert(*procList == NULL); @@ -95,20 +96,36 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { return 1; } - // Allocate an appropriately sized buffer based on the results - // from the previous call. - buf = malloc(length); - if (buf == NULL) { - PyErr_NoMemory(); - return 1; - } + while (1) { + // Allocate an appropriately sized buffer based on the results + // from the previous call. + buf = malloc(length); + if (buf == NULL) { + PyErr_NoMemory(); + return 1; + } - // Call sysctl again with the new buffer. - err = sysctl(name, 3, buf, &length, NULL, 0); - if (err == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl"); - free(buf); - return 1; + // Call sysctl again with the new buffer. + err = sysctl(name, 3, buf, &length, NULL, 0); + if (err == -1) { + free(buf); + if (errno == ENOMEM) { + // Sometimes the first sysctl() suggested size is not enough, + // so we dynamically increase it until it's big enough : + // https://github.com/giampaolo/psutil/issues/2093 + psutil_debug("errno=ENOMEM, length=%zu; retrying", length); + length *= 2; + if (length < max_length) { + continue; + } + } + + PyErr_SetFromOSErrnoWithSyscall("sysctl()"); + return 1; + } + else { + break; + } } *procList = buf; From 274dd47958c9d6bf306e3cff1d3a51e11c3970f0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 2 Sep 2022 18:53:54 +0200 Subject: [PATCH 0873/1714] update HISTORY + bump up version --- HISTORY.rst | 19 ++++++++++++------- psutil/__init__.py | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9bb7d5726f..1a7f553df0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,10 +1,19 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.2 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + **Bug fixes** -- 2113, [FreeBSD]: __FreeBSD_version used in mem.c without including - -- 2128, [NetBSD]: fix bugs in swap code +- 2093_, [FreeBSD]: `pids()`_ may fail with ENOMEM. Dynamically increase + the ``malloc()`` buffer size until it's big enough. +- 2095_, [Linux]: `net_if_stats()`_ returns incorrect interface speed for 100GbE + network cards. +- 2113_, [FreeBSD]: `virtual_memory()`_ may raise ENOMEM due to missing + ``#include `` directive. (patch by Peter Jeremy) +- 2128_, [NetBSD]: `swap_memory()`_ was miscalculated. (patch by Thomas Klausner) 5.9.1 ===== @@ -31,10 +40,6 @@ ``min`` and ``max`` are in MHz. - 2050_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` if running in a LCX container. -- 2093_, [FreeBSD]: `psutil.pids()` may fail with ENOMEM. Dynamically increase - the ``malloc()`` buffer size until it's big enough. -- 2095_, [Linux]: `net_if_stats()` returns incorrect interface speed for 100GbE - network cards 5.9.0 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 614c3fcc0b..5c66d2176a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.1" +__version__ = "5.9.2" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From 9f9a82d02c901f62512236b44edb050f84cbe5b6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 4 Sep 2022 22:15:06 +0200 Subject: [PATCH 0874/1714] fix venv --- HISTORY.rst | 18 +++++++++--------- Makefile | 7 +++---- psutil/tests/test_contracts.py | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1a7f553df0..7767c08a7c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,18 +1,18 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.2 (IN DEVELOPMENT) -====================== +5.9.2 +===== -XXXX-XX-XX +2022-09-04 **Bug fixes** -- 2093_, [FreeBSD]: `pids()`_ may fail with ENOMEM. Dynamically increase - the ``malloc()`` buffer size until it's big enough. -- 2095_, [Linux]: `net_if_stats()`_ returns incorrect interface speed for 100GbE - network cards. -- 2113_, [FreeBSD]: `virtual_memory()`_ may raise ENOMEM due to missing - ``#include `` directive. (patch by Peter Jeremy) +- 2093_, [FreeBSD], **[critical]**: `pids()`_ may fail with ENOMEM. Dynamically + increase the ``malloc()`` buffer size until it's big enough. +- 2095_, [Linux]: `net_if_stats()`_ returns incorrect interface speed for + 100GbE network cards. +- 2113_, [FreeBSD], **[critical]**: `virtual_memory()`_ may raise ENOMEM due to + missing ``#include `` directive. (patch by Peter Jeremy) - 2128_, [NetBSD]: `swap_memory()`_ was miscalculated. (patch by Thomas Klausner) 5.9.1 diff --git a/Makefile b/Makefile index 4bcea213fd..05ca565500 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ TSCRIPT = psutil/tests/runner.py # Internal. DEPS = \ - git+https://github.com/PyCQA/autoflake.git@refs/pull/107/head \ + git+https://github.com/PyCQA/autoflake.git \ autopep8 \ check-manifest \ concurrencytest \ @@ -270,8 +270,8 @@ check-sdist: ## Create source distribution and checks its sanity (MANIFEST) ${MAKE} clean $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv PYTHONWARNINGS=all $(PYTHON) setup.py sdist - build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz - build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" + build/venv/local/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz + build/venv/local/bin/python -c "import os; os.chdir('build/venv'); import psutil" pre-release: ## Check if we're ready to produce a new release. ${MAKE} check-sdist @@ -293,7 +293,6 @@ pre-release: ## Check if we're ready to produce a new release. assert 'XXXX' not in history, 'XXXX in HISTORY.rst';" release: ## Create a release (down/uploads tar.gz, wheels, git tag release). - $(PYTHON) -c "import subprocess, sys; out = subprocess.check_output('git diff --quiet && git diff --cached --quiet', shell=True).strip(); sys.exit('there are uncommitted changes:\n%s' % out) if out else 0 ;" $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 5c927d68da..0d900669a4 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -680,7 +680,7 @@ def memory_maps(self, ret, info): for fname in nt._fields: value = getattr(nt, fname) if fname == 'path': - if not value.startswith('['): + if not value.startswith(("[", "anon_inode:")): assert os.path.isabs(nt.path), nt.path # commented as on Linux we might get # '/foo/bar (deleted)' From 70eecaf44d61f2cabcd22ffb407b904242a122c9 Mon Sep 17 00:00:00 2001 From: Chris Lalancette Date: Tue, 6 Sep 2022 12:23:08 -0400 Subject: [PATCH 0875/1714] Add in support for network interface flags. (#2037) Signed-off-by: Chris Lalancette --- CREDITS | 4 + HISTORY.rst | 1 + README.rst | 4 +- docs/index.rst | 14 ++- psutil/_common.py | 3 +- psutil/_psaix.py | 7 +- psutil/_psbsd.py | 7 +- psutil/_pslinux.py | 7 +- psutil/_psosx.py | 7 +- psutil/_pssunos.py | 2 +- psutil/_psutil_posix.c | 201 ++++++++++++++++++++++++++++++++++++ psutil/_pswindows.py | 2 +- psutil/tests/test_system.py | 3 +- 13 files changed, 246 insertions(+), 16 deletions(-) diff --git a/CREDITS b/CREDITS index c697e21446..b212397e09 100644 --- a/CREDITS +++ b/CREDITS @@ -789,3 +789,7 @@ I: 2099 N: Torsten Blum I: 2114 + +N: Chris Lalancette +W: https://github.com/clalancette +I: 2037 diff --git a/HISTORY.rst b/HISTORY.rst index 7767c08a7c..572c7f2616 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,6 +24,7 @@ - 1053_: drop Python 2.6 support. (patches by Matthieu Darbois and Hugo van Kemenade) +- 2037_: Add additional flags to net_if_stats. - 2050_, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading ``/proc`` pseudo files line by line. This should help having more consistent results. diff --git a/README.rst b/README.rst index 0e0a29afde..5f0afcdfa7 100644 --- a/README.rst +++ b/README.rst @@ -252,8 +252,8 @@ Network snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> >>> psutil.net_if_stats() - {'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536), - 'wlan0': snicstats(isup=True, duplex=, speed=100, mtu=1500)} + {'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running'), + 'wlan0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast')} >>> Sensors diff --git a/docs/index.rst b/docs/index.rst index 8d663c7ece..c30f7d6a1e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -735,13 +735,21 @@ Network - **speed**: the NIC speed expressed in mega bits (MB), if it can't be determined (e.g. 'localhost') it will be set to ``0``. - **mtu**: NIC's maximum transmission unit expressed in bytes. + - **flags**: a string of comma-separated flags on the interface (may be an empty string). + Possible flags are: ``up``, ``broadcast``, ``debug``, ``loopback``, + ``pointopoint``, ``notrailers``, ``running``, ``noarp``, ``promisc``, + ``allmulti``, ``master``, ``slave``, ``multicast``, ``portsel``, + ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, + and ``d2`` (some flags are only available on certain platforms). + + Availability: UNIX Example: >>> import psutil >>> psutil.net_if_stats() - {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500), - 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536)} + {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast'), + 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running')} Also see `nettop.py`_ and `ifconfig.py`_ for an example application. @@ -749,6 +757,8 @@ Network .. versionchanged:: 5.7.3 `isup` on UNIX also checks whether the NIC is running. + .. versionchanged:: 5.9.3 *flags* field was added on POSIX. + Sensors ------- diff --git a/psutil/_common.py b/psutil/_common.py index 9937eb8329..3414e8cad7 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -199,7 +199,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']) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 9cc7d56e3e..2391478c6e 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -237,7 +237,8 @@ def net_if_stats(): 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. @@ -257,8 +258,10 @@ def net_if_stats(): 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 diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 8672ca6eaa..a25c96cd45 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -383,7 +383,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_is_running(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 @@ -392,7 +392,10 @@ 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 diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e7cef439d9..573498892e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1060,7 +1060,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_is_running(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 @@ -1069,7 +1069,10 @@ def net_if_stats(): 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 diff --git a/psutil/_psosx.py b/psutil/_psosx.py index ac8ecc5308..58359bc90c 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -263,7 +263,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_is_running(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 +272,10 @@ 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 diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 69b579c5ff..541c1aa4a2 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -293,7 +293,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 diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 045366145a..7742eb4177 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -429,6 +429,206 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { return PyErr_SetFromErrno(PyExc_OSError); } +static int +append_flag(PyObject *py_retlist, const char * flag_name) +{ + PyObject *py_str = NULL; + + py_str = PyUnicode_DecodeFSDefault(flag_name); + if (! py_str) + return 0; + if (PyList_Append(py_retlist, py_str)) { + Py_DECREF(py_str); + return 0; + } + Py_CLEAR(py_str); + + return 1; +} + +/* + * Get all of the NIC flags and return them. + */ +static PyObject * +psutil_net_if_flags(PyObject *self, PyObject *args) { + char *nic_name; + int sock = -1; + int ret; + struct ifreq ifr; + PyObject *py_retlist = PyList_New(0); + short int flags; + + if (py_retlist == NULL) + return NULL; + + if (! PyArg_ParseTuple(args, "s", &nic_name)) + goto error; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) { + PyErr_SetFromOSErrnoWithSyscall("socket(SOCK_DGRAM)"); + goto error; + } + + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + ret = ioctl(sock, SIOCGIFFLAGS, &ifr); + if (ret == -1) { + PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCGIFFLAGS)"); + goto error; + } + + close(sock); + sock = -1; + + flags = ifr.ifr_flags & 0xFFFF; + + // Linux/glibc IFF flags: https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/gnu/net/if.h;h=251418f82331c0426e58707fe4473d454893b132;hb=HEAD + // macOS IFF flags: https://opensource.apple.com/source/xnu/xnu-792/bsd/net/if.h.auto.html + // AIX IFF flags: https://www.ibm.com/support/pages/how-hexadecimal-flags-displayed-ifconfig-are-calculated + // FreeBSD IFF flags: https://www.freebsd.org/cgi/man.cgi?query=if_allmulti&apropos=0&sektion=0&manpath=FreeBSD+10-current&format=html + +#ifdef IFF_UP + // Available in (at least) Linux, macOS, AIX, BSD + if (flags & IFF_UP) + if (!append_flag(py_retlist, "up")) + goto error; +#endif +#ifdef IFF_BROADCAST + // Available in (at least) Linux, macOS, AIX, BSD + if (flags & IFF_BROADCAST) + if (!append_flag(py_retlist, "broadcast")) + goto error; +#endif +#ifdef IFF_DEBUG + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_DEBUG) + if (!append_flag(py_retlist, "debug")) + goto error; +#endif +#ifdef IFF_LOOPBACK + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_LOOPBACK) + if (!append_flag(py_retlist, "loopback")) + goto error; +#endif +#ifdef IFF_POINTOPOINT + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_POINTOPOINT) + if (!append_flag(py_retlist, "pointopoint")) + goto error; +#endif +#ifdef IFF_NOTRAILERS + // Available in (at least) Linux, macOS, AIX + if (flags & IFF_NOTRAILERS) + if (!append_flag(py_retlist, "notrailers")) + goto error; +#endif +#ifdef IFF_RUNNING + // Available in (at least) Linux, macOS, AIX, BSD + if (flags & IFF_RUNNING) + if (!append_flag(py_retlist, "running")) + goto error; +#endif +#ifdef IFF_NOARP + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_NOARP) + if (!append_flag(py_retlist, "noarp")) + goto error; +#endif +#ifdef IFF_PROMISC + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_PROMISC) + if (!append_flag(py_retlist, "promisc")) + goto error; +#endif +#ifdef IFF_ALLMULTI + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_ALLMULTI) + if (!append_flag(py_retlist, "allmulti")) + goto error; +#endif +#ifdef IFF_MASTER + // Available in (at least) Linux + if (flags & IFF_MASTER) + if (!append_flag(py_retlist, "master")) + goto error; +#endif +#ifdef IFF_SLAVE + // Available in (at least) Linux + if (flags & IFF_SLAVE) + if (!append_flag(py_retlist, "slave")) + goto error; +#endif +#ifdef IFF_MULTICAST + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_MULTICAST) + if (!append_flag(py_retlist, "multicast")) + goto error; +#endif +#ifdef IFF_PORTSEL + // Available in (at least) Linux + if (flags & IFF_PORTSEL) + if (!append_flag(py_retlist, "portsel")) + goto error; +#endif +#ifdef IFF_AUTOMEDIA + // Available in (at least) Linux + if (flags & IFF_AUTOMEDIA) + if (!append_flag(py_retlist, "automedia")) + goto error; +#endif +#ifdef IFF_DYNAMIC + // Available in (at least) Linux + if (flags & IFF_DYNAMIC) + if (!append_flag(py_retlist, "dynamic")) + goto error; +#endif +#ifdef IFF_OACTIVE + // Available in (at least) macOS, BSD + if (flags & IFF_OACTIVE) + if (!append_flag(py_retlist, "oactive")) + goto error; +#endif +#ifdef IFF_SIMPLEX + // Available in (at least) macOS, AIX, BSD + if (flags & IFF_SIMPLEX) + if (!append_flag(py_retlist, "simplex")) + goto error; +#endif +#ifdef IFF_LINK0 + // Available in (at least) macOS, BSD + if (flags & IFF_LINK0) + if (!append_flag(py_retlist, "link0")) + goto error; +#endif +#ifdef IFF_LINK1 + // Available in (at least) macOS, BSD + if (flags & IFF_LINK1) + if (!append_flag(py_retlist, "link1")) + goto error; +#endif +#ifdef IFF_LINK2 + // Available in (at least) macOS, BSD + if (flags & IFF_LINK2) + if (!append_flag(py_retlist, "link2")) + goto error; +#endif +#ifdef IFF_D2 + // Available in (at least) AIX + if (flags & IFF_D2) + if (!append_flag(py_retlist, "d2")) + goto error; +#endif + + return py_retlist; + +error: + Py_DECREF(py_retlist); + if (sock != -1) + close(sock); + return NULL; +} + /* * Inspect NIC flags, returns a bool indicating whether the NIC is @@ -667,6 +867,7 @@ static PyMethodDef mod_methods[] = { {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS}, {"getpriority", psutil_posix_getpriority, METH_VARARGS}, {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, + {"net_if_flags", psutil_net_if_flags, METH_VARARGS}, {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS}, {"net_if_mtu", psutil_net_if_mtu, METH_VARARGS}, {"setpriority", psutil_posix_setpriority, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 31edffc1c7..7d882b7747 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -386,7 +386,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 diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index e130c935e1..d6b7a21a4e 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -808,12 +808,13 @@ def test_net_if_stats(self): psutil.NIC_DUPLEX_UNKNOWN) for name, stats in nics.items(): self.assertIsInstance(name, str) - isup, duplex, speed, mtu = stats + 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") From 5cf18078d81741f1783100ec62e85fab6ba55e91 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 6 Sep 2022 19:48:27 +0200 Subject: [PATCH 0876/1714] add ifconfig test case for NIC flags re. to #2037 --- Makefile | 2 +- psutil/tests/test_linux.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 05ca565500..caf4c9e9d9 100644 --- a/Makefile +++ b/Makefile @@ -248,7 +248,7 @@ print-wheels: ## Print downloaded wheels # =================================================================== git-tag-release: ## Git-tag a new release. - git tag -a release-`python -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` + git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` git push --follow-tags sdist: ## Create tar.gz source distribution. diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 41645e3bc9..feec0a59f7 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -993,6 +993,27 @@ def test_mtu(self): with open("/sys/class/net/%s/mtu" % name, "rt") 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) + + if not matches_found: + raise self.fail("no matches were found") + @unittest.skipIf(not LINUX, "LINUX only") class TestSystemNetIOCounters(PsutilTestCase): From 40c9c545191204ff623aa2f95d1e6b1b0dbac5f3 Mon Sep 17 00:00:00 2001 From: Ben Raz Date: Thu, 15 Sep 2022 22:48:57 +0300 Subject: [PATCH 0877/1714] Use recommended (more-updated) freebsd-vm version (#2139) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a592a32c7..9e0caa2a16 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -143,7 +143,7 @@ jobs: - name: Run tests id: test - uses: vmactions/freebsd-vm@v0.1.5 + uses: vmactions/freebsd-vm@v0 with: usesh: true prepare: pkg install -y gcc python3 From f6d01ace5d86435a1b865fd066c68eb2ca433d76 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Mon, 19 Sep 2022 11:51:34 +0200 Subject: [PATCH 0878/1714] net_if_stats() -> flags: do not return unicode on Python 2 (#2142) Do not use unicode string for network interface flags on Python 2 Update network interface flags tests to work on CentOS 6 (CI tests are currently failing) Signed-off-by: mayeut --- psutil/_psutil_posix.c | 6 +++++- psutil/tests/test_linux.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 7742eb4177..07c10398bb 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -434,7 +434,11 @@ append_flag(PyObject *py_retlist, const char * flag_name) { PyObject *py_str = NULL; - py_str = PyUnicode_DecodeFSDefault(flag_name); +#if PY_MAJOR_VERSION >= 3 + py_str = PyUnicode_FromString(flag_name); +#else + py_str = PyString_FromString(flag_name); +#endif if (! py_str) return 0; if (PyList_Append(py_retlist, py_str)) { diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index feec0a59f7..c53dfd8a01 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1010,6 +1010,15 @@ def test_flags(self): 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") From 29e644042eff71c0ca3706a563d46322822ca28b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 19 Sep 2022 11:59:23 +0200 Subject: [PATCH 0879/1714] update version + HISTORY + CREDITS for #2138 --- CREDITS | 2 +- HISTORY.rst | 10 ++++++++++ psutil/__init__.py | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index b212397e09..7d1e7a696d 100644 --- a/CREDITS +++ b/CREDITS @@ -781,7 +781,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039 +I: 2039, 2142 N: Hugo van Kemenade W: https://github.com/hugovk diff --git a/HISTORY.rst b/HISTORY.rst index 572c7f2616..f3c00fced6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.3 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 2142_, [POSIX]: `net_if_stats()`_ 's ``flags`` on Python 2 returned unicode + instead of str. (patch by Matthieu Darbois) + 5.9.2 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 5c66d2176a..475059f2df 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.2" +__version__ = "5.9.3" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From 9c38dd74e8a393371e6547b9404c303fa85a1459 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 19 Sep 2022 12:33:08 +0200 Subject: [PATCH 0880/1714] Set parsable BSD-3-Clause license in setup.py (#2141) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a6f6eb16e9..2fa216c7b7 100755 --- a/setup.py +++ b/setup.py @@ -360,7 +360,7 @@ def main(): author_email='g.rodola@gmail.com', url='https://github.com/giampaolo/psutil', platforms='Platform Independent', - license='BSD', + license='BSD-3-Clause', packages=['psutil', 'psutil.tests'], ext_modules=extensions, classifiers=[ From 5d46b358d31ff7f0a67b104ad4cffffa032b3b35 Mon Sep 17 00:00:00 2001 From: Bernhard Urban-Forster Date: Mon, 19 Sep 2022 12:55:00 +0200 Subject: [PATCH 0881/1714] [macOS] Fix out-of-bounds read around sysctl_procargs (#2135) The buffer size is initially determined by querying `argmax`: https://github.com/giampaolo/psutil/blob/9f9a82d02c901f62512236b44edb050f84cbe5b6/psutil/arch/osx/process_info.c#L260-L265 This length is passed to the `sysctl()` call for querying the procargs. `sysctl()` is allowed to set it to a smaller value, to indicate the buffer wasn't fully used. However this is not passed back to the caller "owning" that memory, and also parsing the result. Depending on the allocator, it's possible that we can observe "garbage" values, consider this example: ```python import psutil import subprocess import os import sys environment_cookie = "__MAGIC_ENV_COOKIE__" for i in range(0,200): os.environ["PATH"] = os.environ.get("PATH", "") + os.pathsep + environment_cookie + str(i) print("sys.executable: " + str(sys.executable)) exe = os.path.split(sys.executable)[1] exe = exe.split(".")[0] print("exe: " + str(exe)) polluted_process = subprocess.Popen([exe, "-c", "import time; time.sleep(3)"], env=os.environ.copy()) clean_process = subprocess.Popen([exe, "-c", "import time; time.sleep(3)"], env={}) print("polluted pid=" + str(polluted_process.pid)) print("clean pid=" + str(clean_process.pid)) pp = psutil.Process(polluted_process.pid) pc = psutil.Process(clean_process.pid) for i in range(0,8): pp.environ() for i in range(0,8): e = pc.environ() if len(e) > 0: print("clean subprocess (should be empty) env=" + str(e)) exit(2) print("no repro, try again :-/") ``` In order to better illustrate the problem, I added this debug output to `psutils`: ```diff +++ psutil/arch/osx/process_info.c @@ -263,6 +263,7 @@ psutil_get_environ(pid_t pid) { goto error; procargs = (char *)malloc(argmax); + printf("[psutil_get_environ] pid=%d, procargs=%p\n", pid, procargs); if (NULL == procargs) { PyErr_NoMemory(); goto error; ``` Example output: ``` $ python3 .py sys.executable: /opt/homebrew/opt/python@3.10/bin/python3.10 exe: python3 polluted pid=63479 clean pid=63480 [psutil_get_environ] pid=63479, procargs=0x140088000 [psutil_get_environ] pid=63479, procargs=0x140188000 [psutil_get_environ] pid=63479, procargs=0x140088000 [psutil_get_environ] pid=63479, procargs=0x140188000 [psutil_get_environ] pid=63479, procargs=0x140088000 [psutil_get_environ] pid=63479, procargs=0x140188000 [psutil_get_environ] pid=63479, procargs=0x140088000 [psutil_get_environ] pid=63479, procargs=0x140188000 [psutil_get_environ] pid=63480, procargs=0x140088000 clean subprocess (should be empty) env={'EDITOR': 'vim', [[removed]], 'PATH': '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:__MAGIC_ENV_COOKIE__0:__MAGIC_ENV_COOKIE__1:__MAGIC_ENV_COOKIE__2:__MAGIC_ENV_COOKIE__3:__MAGIC_ENV_COOKIE__4:__MAGIC_ENV_COOKIE__5:__MAGIC_ENV_COOKIE__6:__MAGIC_ENV_COOKIE__7:__MAGIC_ENV_COOKIE__8:__MAGIC_ENV_COOKIE__9:__MAGIC_ENV_COOKIE__10:__MAGIC_ENV_COOKIE__11:__MAGIC_ENV_COOKIE__12:__MAGIC_ENV_COOKIE__13:__MAGIC_ENV_COOKIE__14:__MAGIC_ENV_COOKIE__15:__MAGIC_ENV_COOKIE__16:__MAGIC_ENV_COOKIE__17:__MAGIC_ENV_COOKIE__18:__MAGIC_ENV_COOKIE__19:__MAGIC_ENV_COOKIE__20:__MAGIC_ENV_COOKIE__21:__MAGIC_ENV_COOKIE__22:__MAGIC_ENV_COOKIE__23:__MAGIC_ENV_COOKIE__24:__MAGIC_ENV_COOKIE__25:__MAGIC_ENV_COOKIE__26:__MAGIC_ENV_COOKIE__27:__MAGIC_ENV_COOKIE__28:__MAGIC_ENV_COOKIE__29:__MAGIC_ENV_COOKIE__30:__MAGIC_ENV_COOKIE__31:__MAGIC_ENV_COOKIE__32:__MAGIC_ENV_COOKIE__33:__MAGIC_ENV_COOKIE__34:__MAGIC_ENV_COOKIE__35:__MAGIC_ENV_COOKIE__36:__MAGIC_ENV_COOKIE__37:__MAGIC_ENV_COOKIE__38:__MAGIC_ENV_COOKIE__39:__MAGIC_ENV_COOKIE__40:__MAGIC_ENV_COOKIE__41:__MAGIC_ENV_COOKIE__42:__MAGIC_ENV_COOKIE__43:__MAGIC_ENV_COOKIE__44:__MAGIC_ENV_COOKIE__45:__MAGIC_ENV_COOKIE__46:__MAGIC_ENV_COOKIE__47:__MAGIC_ENV_COOKIE__48:__MAGIC_ENV_COOKIE__49:__MAGIC_ENV_COOKIE__50:__MAGIC_ENV_COOKIE__51:__MAGIC_ENV_COOKIE__52:__MAGIC_ENV_COOKIE__53:__MAGIC_ENV_COOKIE__54:__MAGIC_ENV_COOKIE__55:__MAGIC_ENV_COOKIE__56:__MAGIC_ENV_COOKIE__57:__MAGIC_ENV_COOKIE__58:__MAGIC_ENV_COOKIE__59:__MAGIC_ENV_COOKIE__60:__MAGIC_ENV_COOKIE__61:__MAGIC_ENV_COOKIE__62:__MAGIC_ENV_COOKIE__63:__MAGIC_ENV_COOKIE__64:__MAGIC_ENV_COOKIE__65:__MAGIC_ENV_COOKIE__66:__MAGIC_ENV_COOKIE__67:__MAGIC_ENV_COOKIE__68:__MAGIC_ENV_COOKIE__69:__MAGIC_ENV_COOKIE__70:__MAGIC_ENV_COOKIE__71:__MAGIC_ENV_COOKIE__72:__MAGIC_ENV_COOKIE__73:__MAGIC_ENV_COOKIE__74:__MAGIC_ENV_COOKIE__75:__MAGIC_ENV_COOKIE__76:__MAGIC_ENV_COOKIE__77:__MAGIC_ENV_COOKIE__78:__MAGIC_ENV_COOKIE__79:__MAGIC_ENV_COOKIE__80:__MAGIC_ENV_COOKIE__81:__MAGIC_ENV_COOKIE__82:__MAGIC_ENV_COOKIE__83:__MAGIC_ENV_COOKIE__84:__MAGIC_ENV_COOKIE__85:__MAGIC_ENV_COOKIE__86:__MAGIC_ENV_COOKIE__87:__MAGIC_ENV_COOKIE__88:__MAGIC_ENV_COOKIE__89:__MAGIC_ENV_COOKIE__90:__MAGIC_ENV_COOKIE__91:__MAGIC_ENV_COOKIE__92:__MAGIC_ENV_COOKIE__93:__MAGIC_ENV_COOKIE__94:__MAGIC_ENV_COOKIE__95:__MAGIC_ENV_COOKIE__96:__MAGIC_ENV_COOKIE__97:__MAGIC_ENV_COOKIE__98:__MAGIC_ENV_COOKIE__99:__MAGIC_ENV_COOKIE__100:__MAGIC_ENV_COOKIE__101:__MAGIC_ENV_COOKIE__102:__MAGIC_ENV_COOKIE__103:__MAGIC_ENV_COOKIE__104:__MAGIC_ENV_COOKIE__105:__MAGIC_ENV_COOKIE__106:__MAGIC_ENV_COOKIE__107:__MAGIC_ENV_COOKIE__108:__MAGIC_ENV_COOKIE__109:__MAGIC_ENV_COOKIE__110:__MAGIC_ENV_COOKIE__111:__MAGIC_ENV_COOKIE__112:__MAGIC_ENV_COOKIE__113:__MAGIC_ENV_COOKIE__114:__MAGIC_ENV_COOKIE__115:__MAGIC_ENV_COOKIE__116:__MAGIC_ENV_COOKIE__117:__MAGIC_ENV_COOKIE__118:__MAGIC_ENV_COOKIE__119:__MAGIC_ENV_COOKIE__120:__MAGIC_ENV_COOKIE__121:__MAGIC_ENV_COOKIE__122:__MAGIC_ENV_COOKIE__123:__MAGIC_ENV_COOKIE__124:__MAGIC_ENV_COOKIE__125:__MAGIC_ENV_COOKIE__126:__MAGIC_ENV_COOKIE__127:__MAGIC_ENV_COOKIE__128:__MAGIC_ENV_COOKIE__129:__MAGIC_ENV_COOKIE__130:__MAGIC_ENV_COOKIE__131:__MAGIC_ENV_COOKIE__132:__MAGIC_ENV_COOKIE__133:__MAGIC_ENV_COOKIE__134:__MAGIC_ENV_COOKIE__135:__MAGIC_ENV_COOKIE__136:__MAGIC_ENV_COOKIE__137:__MAGIC_ENV_COOKIE__138:__MAGIC_ENV_COOKIE__139:__MAGIC_ENV_COOKIE__140:__MAGIC_ENV_COOKIE__141:__MAGIC_ENV_COOKIE__142:__MAGIC_ENV_COOKIE__143:__MAGIC_ENV_COOKIE__144:__MAGIC_ENV_COOKIE__145:__MAGIC_ENV_COOKIE__146:__MAGIC_ENV_COOKIE__147:__MAGIC_ENV_COOKIE__148:__MAGIC_ENV_COOKIE__149:__MAGIC_ENV_COOKIE__150:__MAGIC_ENV_COOKIE__151:__MAGIC_ENV_COOKIE__152:__MAGIC_ENV_COOKIE__153:__MAGIC_ENV_COOKIE__154:__MAGIC_ENV_COOKIE__155:__MAGIC_ENV_COOKIE__156:__MAGIC_ENV_COOKIE__157:__MAGIC_ENV_COOKIE__158:__MAGIC_ENV_COOKIE__159:__MAGIC_ENV_COOKIE__160:__MAGIC_ENV_COOKIE__161:__MAGIC_ENV_COOKIE__162:__MAGIC_ENV_COOKIE__163:__MAGIC_ENV_COOKIE__164:__MAGIC_ENV_COOKIE__165:__MAGIC_ENV_COOKIE__166:__MAGIC_ENV_COOKIE__167:__MAGIC_ENV_COOKIE__168:__MAGIC_ENV_COOKIE__169:__MAGIC_ENV_COOKIE__170:__MAGIC_ENV_COOKIE__171:__MAGIC_ENV_COOKIE__172:__MAGIC_ENV_COOKIE__173:__MAGIC_ENV_COOKIE__174:__MAGIC_ENV_COOKIE__175:__MAGIC_ENV_COOKIE__176:__MAGIC_ENV_COOKIE__177:__MAGIC_ENV_COOKIE__178:__MAGIC_ENV_COOKIE__179:__MAGIC_ENV_COOKIE__180:__MAGIC_ENV_COOKIE__181:__MAGIC_ENV_COOKIE__182:__MAGIC_ENV_COOKIE__183:__MAGIC_ENV_COOKIE__184:__MAGIC_ENV_COOKIE__185:__MAGIC_ENV_COOKIE__186:__MAGIC_ENV_COOKIE__187:__MAGIC_ENV_COOKIE__188:__MAGIC_ENV_COOKIE__189:__MAGIC_ENV_COOKIE__190:__MAGIC_ENV_COOKIE__191:__MAGIC_ENV_COOKIE__192:__MAGIC_ENV_COOKIE__193:__MAGIC_ENV_COOKIE__194:__MAGIC_ENV_COOKIE__195:__MAGIC_ENV_COOKIE__196:__MAGIC_ENV_COOKIE__197:__MAGIC_ENV_COOKIE__198:__MAGIC_ENV_COOKIE__199'} ``` As you can see the allocated buffer for `pid=63480` matches with some instances of `pid=63479`. Kudos to @naoufal450 and @gilles-duboscq for debugging this issue! Signed-off-by: Bernhard Urban-Forster Signed-off-by: Bernhard Urban-Forster Co-authored-by: Giampaolo Rodola --- CREDITS | 5 +++++ HISTORY.rst | 6 ++++-- psutil/arch/osx/process_info.c | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CREDITS b/CREDITS index 7d1e7a696d..93866be5ff 100644 --- a/CREDITS +++ b/CREDITS @@ -793,3 +793,8 @@ I: 2114 N: Chris Lalancette W: https://github.com/clalancette I: 2037 + +N: Bernhard Urban-Forster +C: Austria +W: https://github.com/lewurm +I: 2135 diff --git a/HISTORY.rst b/HISTORY.rst index f3c00fced6..a0a3bb25a8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,12 +1,14 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.3 -===== +5.9.3 (IN DEVELOPMENT) +====================== XXXX-XX-XX **Bug fixes** +- 2135_, [macOS]: `Process.environ()`_ may contain garbage data. Fix + out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) - 2142_, [POSIX]: `net_if_stats()`_ 's ``flags`` on Python 2 returned unicode instead of str. (patch by Matthieu Darbois) diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 3a446a62eb..4af510e8bc 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -116,14 +116,14 @@ psutil_sysctl_argmax() { // Read process argument space. static int -psutil_sysctl_procargs(pid_t pid, char *procargs, size_t argmax) { +psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { int mib[3]; mib[0] = CTL_KERN; mib[1] = KERN_PROCARGS2; mib[2] = pid; - if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { + if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { if (psutil_pid_exists(pid) == 0) { NoSuchProcess("psutil_pid_exists -> 0"); return 1; @@ -188,7 +188,7 @@ psutil_get_cmdline(pid_t pid) { goto error; } - if (psutil_sysctl_procargs(pid, procargs, argmax) != 0) + if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) goto error; arg_end = &procargs[argmax]; @@ -268,7 +268,7 @@ psutil_get_environ(pid_t pid) { goto error; } - if (psutil_sysctl_procargs(pid, procargs, argmax) != 0) + if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) goto error; arg_end = &procargs[argmax]; From cafd10b174e9edec1b8aae5337caac4461c15b1e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 19 Sep 2022 13:06:38 +0200 Subject: [PATCH 0882/1714] remove fix_flake8.py re-introduced by accident Signed-off-by: Giampaolo Rodola --- psutil/tests/test_process.py | 2 +- scripts/internal/fix_flake8.py | 185 --------------------------------- 2 files changed, 1 insertion(+), 186 deletions(-) delete mode 100755 scripts/internal/fix_flake8.py diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a79daabd5a..b2c3b2c768 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1492,7 +1492,7 @@ def __init__(self, *args, **kwargs): def test_(self): try: - meth() + meth() # noqa except psutil.AccessDenied: pass setattr(self, attr, types.MethodType(test_, self)) diff --git a/scripts/internal/fix_flake8.py b/scripts/internal/fix_flake8.py deleted file mode 100755 index 44da4a3274..0000000000 --- a/scripts/internal/fix_flake8.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/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. - -""" -Fix some flake8 errors by rewriting files for which flake8 emitted -an error/warning. Usage (from the root dir): - - $ python3 -m flake8 --exit-zero | python3 scripts/fix_flake8.py -""" - -import shutil -import sys -import tempfile -from collections import defaultdict -from collections import namedtuple -from pprint import pprint as pp # NOQA - - -ntentry = namedtuple('ntentry', 'msg, issue, lineno, pos, descr') - - -# ===================================================================== -# utils -# ===================================================================== - - -def enter_pdb(): - from pdb import set_trace as st # trick GIT commit hook - sys.stdin = open('/dev/tty') - st() - - -def read_lines(fname): - with open(fname, 'rt') as f: - return f.readlines() - - -def read_line(fname, lineno): - with open(fname, 'rt') as f: - for i, line in enumerate(f, 1): - if i == lineno: - return line - raise ValueError("lineno too big") - - -def write_file(fname, newlines): - with tempfile.NamedTemporaryFile('wt', delete=False) as f: - for line in newlines: - f.write(line) - shutil.move(f.name, fname) - - -# ===================================================================== -# handlers -# ===================================================================== - - -def handle_f401(fname, lineno): - """ This is 'module imported but not used' - Able to handle this case: - import os - - ...but not this: - from os import listdir - """ - line = read_line(fname, lineno).strip() - if line.startswith('import '): - return True - else: - mods = line.partition(' import ')[2] # everything after import - return ',' not in mods - - -# ===================================================================== -# converters -# ===================================================================== - - -def remove_lines(fname, entries): - """Check if we should remove lines, then do it. - Return the number of lines removed. - """ - to_remove = [] - for entry in entries: - msg, issue, lineno, pos, descr = entry - # 'module imported but not used' - if issue == 'F401' and handle_f401(fname, lineno): - to_remove.append(lineno) - # 'blank line(s) at end of file' - elif issue == 'W391': - lines = read_lines(fname) - i = len(lines) - 1 - while lines[i] == '\n': - to_remove.append(i + 1) - i -= 1 - # 'too many blank lines' - elif issue == 'E303': - howmany = descr.replace('(', '').replace(')', '') - howmany = int(howmany[-1]) - for x in range(lineno - howmany, lineno): - to_remove.append(x) - - if to_remove: - newlines = [] - for i, line in enumerate(read_lines(fname), 1): - if i not in to_remove: - newlines.append(line) - print("removing line(s) from %s" % fname) - write_file(fname, newlines) - - return len(to_remove) - - -def add_lines(fname, entries): - """Check if we should remove lines, then do it. - Return the number of lines removed. - """ - EXPECTED_BLANK_LINES = ( - 'E302', # 'expected 2 blank limes, found 1' - 'E305') # ìexpected 2 blank lines after class or fun definition' - to_add = {} - for entry in entries: - msg, issue, lineno, pos, descr = entry - if issue in EXPECTED_BLANK_LINES: - howmany = 2 if descr.endswith('0') else 1 - to_add[lineno] = howmany - - if to_add: - newlines = [] - for i, line in enumerate(read_lines(fname), 1): - if i in to_add: - newlines.append('\n' * to_add[i]) - newlines.append(line) - print("adding line(s) to %s" % fname) - write_file(fname, newlines) - - return len(to_add) - - -# ===================================================================== -# main -# ===================================================================== - - -def build_table(): - table = defaultdict(list) - for line in sys.stdin: - line = line.strip() - if not line: - break - fields = line.split(':') - fname, lineno, pos = fields[:3] - issue = fields[3].split()[0] - descr = fields[3].strip().partition(' ')[2] - lineno, pos = int(lineno), int(pos) - table[fname].append(ntentry(line, issue, lineno, pos, descr)) - return table - - -def main(): - table = build_table() - - # remove lines (unused imports) - removed = 0 - for fname, entries in table.items(): - removed += remove_lines(fname, entries) - if removed: - print("%s lines were removed from some file(s); please re-run" % - removed) - return - - # add lines (missing \n between functions/classes) - added = 0 - for fname, entries in table.items(): - added += add_lines(fname, entries) - if added: - print("%s lines were added from some file(s); please re-run" % - added) - return - - -main() From a3eca35f084b44185a9950b5725fe8491c91510e Mon Sep 17 00:00:00 2001 From: Ben Raz Date: Mon, 19 Sep 2022 15:07:50 +0300 Subject: [PATCH 0883/1714] Fix macOS Python 2.7 build (#2140) Currently Python 2.7 tests & builds were run twice for Linux instead. Swap configuration to macOS 10.15, in par with Python 3 workflow, and to make Process.environ tests pass (broken in macOS 11+). Signed-off-by: Ben Raz --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e0caa2a16..6b600bf03a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,16 +81,16 @@ jobs: # Linux + macOS + Python 2 linux-macos-py2: name: ${{ matrix.os }}-py2 - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-10.15] include: - {name: Linux, python: '3.9', os: ubuntu-latest} env: - CIBW_ARCHS: 'x86_64 i686' + CIBW_ARCHS_LINUX: 'x86_64 i686' CIBW_TEST_COMMAND: PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py From b88f4d839dc4d665526e91b098d018a3d796a0ac Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 19 Sep 2022 17:40:19 +0200 Subject: [PATCH 0884/1714] fix #2138 (critical): re-define ethtool_cmd_speed --- HISTORY.rst | 2 ++ psutil/_psutil_linux.c | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index da1acd546d..00bd5043cc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 2135_, [macOS]: `Process.environ()`_ may contain garbage data. Fix out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) +- 2138_, [Linux], **[critical]**: can't compile psutil on Android due to + undefined ``ethtool_cmd_speed`` symbol. - 2142_, [POSIX]: `net_if_stats()`_ 's ``flags`` on Python 2 returned unicode instead of str. (patch by Matthieu Darbois) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index c59050cb05..64cdf0b550 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -73,6 +73,12 @@ ioprio_set(int which, int who, int ioprio) { return syscall(__NR_ioprio_set, which, who, ioprio); } +// defined in linux/ethtool.h but not always available (e.g. Android) +static inline uint32_t +psutil_ethtool_cmd_speed(const struct ethtool_cmd *ecmd) { + return (ecmd->speed_hi << 16) | ecmd->speed; +} + #define IOPRIO_CLASS_SHIFT 13 #define IOPRIO_PRIO_MASK ((1UL << IOPRIO_CLASS_SHIFT) - 1) @@ -441,7 +447,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { duplex = ethcmd.duplex; // speed is returned from ethtool as a __u32 ranging from 0 to INT_MAX // or SPEED_UNKNOWN (-1) - uint_speed = ethtool_cmd_speed(ðcmd); + uint_speed = psutil_ethtool_cmd_speed(ðcmd); if (uint_speed == (__u32)SPEED_UNKNOWN || uint_speed > INT_MAX) { speed = 0; } From f4784d0789cbb911836f5cfcfa0e8a513af6b381 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 19 Sep 2022 14:59:58 -0700 Subject: [PATCH 0885/1714] fix #2116, macOS, net_connections() crashing It turns out that proc_pidinfo() crashes for PID 0 Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 1 + psutil/_psutil_osx.c | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 00bd5043cc..e088de08a1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ XXXX-XX-XX **Bug fixes** +- 2116_, [macOS]: `psutil.net_connections`_ fails with RuntimeError. - 2135_, [macOS]: `Process.environ()`_ may contain garbage data. Fix out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) - 2138_, [Linux], **[critical]**: can't compile psutil on Android due to diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 2470c3ebd3..cd375babac 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -955,6 +955,10 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; + // see: https://github.com/giampaolo/psutil/issues/2116 + if (pid == 0) + return py_retlist; + fds_pointer = psutil_proc_list_fds(pid, &num_fds); if (fds_pointer == NULL) goto error; @@ -1047,6 +1051,10 @@ psutil_proc_connections(PyObject *self, PyObject *args) { goto error; } + // see: https://github.com/giampaolo/psutil/issues/2116 + if (pid == 0) + return py_retlist; + if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); goto error; From 735de787b84a1c8410d97ce402847bfc0c535487 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 20 Sep 2022 00:23:09 +0200 Subject: [PATCH 0886/1714] #2104 / TestFetchAllProcesses: don't use process pool on CI (fix deadlock) --- HISTORY.rst | 2 +- psutil/tests/test_contracts.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e088de08a1..263bad0661 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,7 +7,7 @@ XXXX-XX-XX **Bug fixes** -- 2116_, [macOS]: `psutil.net_connections`_ fails with RuntimeError. +- 2116_, [macOS], [critical]: `psutil.net_connections`_ fails with RuntimeError. - 2135_, [macOS]: `Process.environ()`_ may contain garbage data. Fix out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) - 2138_, [Linux], **[critical]**: can't compile psutil on Android due to diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 0d900669a4..f4ace838b0 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -397,17 +397,28 @@ class TestFetchAllProcesses(PsutilTestCase): """ def setUp(self): - self.pool = multiprocessing.Pool() + # Using a pool in a CI env may result in deadlock, see: + # https://github.com/giampaolo/psutil/issues/2104 + if not CI_TESTING: + self.pool = multiprocessing.Pool() def tearDown(self): - self.pool.terminate() - self.pool.join() + if not CI_TESTING: + self.pool.terminate() + self.pool.join() def iter_proc_info(self): # Fixes "can't pickle : it's not the # same object as test_contracts.proc_info". from psutil.tests.test_contracts import proc_info - return self.pool.imap_unordered(proc_info, psutil.pids()) + + if not CI_TESTING: + 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 = [] From 2b57f247128dc7f243c98c323a301942264d9a5d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 20 Sep 2022 01:00:54 +0200 Subject: [PATCH 0887/1714] #2084: document limitations of environ() on macOS Big Sur --- docs/conf.py | 2 +- docs/index.rst | 9 +++++++++ psutil/_psutil_osx.c | 12 +++++++++--- psutil/arch/osx/process_info.c | 9 ++++++++- psutil/tests/test_contracts.py | 5 +++-- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f4a32b987d..f0de77723b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -96,7 +96,7 @@ def get_version(): # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "eng" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/index.rst b/docs/index.rst index aa0fd932a9..1996110377 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1202,6 +1202,11 @@ Process class >>> psutil.Process().environ() {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} + .. note:: + on macOS Big Sur this function returns something meaningful only for the + current process or in + `other specific circumstances `__). + .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support .. versionchanged:: 5.6.3 added AIX support @@ -2625,6 +2630,10 @@ Supported Python versions are 2.7, 3.4+ and PyPy3. Timeline ======== +- 2022-09-04: + `5.9.2 `__ - + `what's new `__ - + `diff `__ - 2022-05-20: `5.9.1 `__ - `what's new `__ - diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index cd375babac..f634be362f 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -405,18 +405,24 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { /* * Return process environment as a Python string. + * On Big Sur this function returns an empty string unless: + * * kernel is DEVELOPMENT || DEBUG + * * target process is same as current_proc() + * * target process is not cs_restricted + * * SIP is off + * * caller has an entitlement */ static PyObject * psutil_proc_environ(PyObject *self, PyObject *args) { pid_t pid; - PyObject *py_retdict = NULL; + PyObject *py_str = NULL; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; // get the environment block, defined in arch/osx/process_info.c - py_retdict = psutil_get_environ(pid); - return py_retdict; + py_str = psutil_get_environ(pid); + return py_str; } diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 4af510e8bc..47330ea67d 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -241,7 +241,14 @@ psutil_get_cmdline(pid_t pid) { } -// return process environment as a python string +// Return process environment as a python string. +// On Big Sur this function returns an empty string unless: +// * kernel is DEVELOPMENT || DEBUG +// * target process is same as current_proc() +// * target process is not cs_restricted +// * SIP is off +// * caller has an entitlement +// See: https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/kern/kern_sysctl.c#L1315-L1321 PyObject * psutil_get_environ(pid_t pid) { int nargs; diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index f4ace838b0..d376a33853 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -34,6 +34,7 @@ from psutil._compat import FileNotFoundError from psutil._compat import long from psutil._compat import range +from psutil._compat import unicode from psutil.tests import APPVEYOR from psutil.tests import CI_TESTING from psutil.tests import GITHUB_ACTIONS @@ -448,7 +449,7 @@ def cmdline(self, ret, info): self.assertIsInstance(part, str) def exe(self, ret, info): - self.assertIsInstance(ret, (str, type(None))) + self.assertIsInstance(ret, (str, unicode, type(None))) if not ret: self.assertEqual(ret, '') else: @@ -476,7 +477,7 @@ def ppid(self, ret, info): self.assertGreaterEqual(ret, 0) def name(self, ret, info): - self.assertIsInstance(ret, str) + self.assertIsInstance(ret, (str, unicode)) if APPVEYOR and not ret and info['status'] == 'stopped': return # on AIX, "" processes don't have names From 69b572ef62ff349495d7884e231ed9faec7775c6 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Thu, 22 Sep 2022 13:33:08 +0200 Subject: [PATCH 0888/1714] chore: use macos-12 runner for FreeBSD tests (#2145) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b600bf03a..967b67622a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -132,7 +132,7 @@ jobs: python scripts/internal/print_hashes.py wheelhouse/ freebsd: - runs-on: macos-10.15 + runs-on: macos-12 steps: - name: Cancel previous runs uses: styfle/cancel-workflow-action@0.6.0 From d1c7df8231df76d0b4c2c8b3033bc767f486865f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 23 Sep 2022 10:20:33 +0200 Subject: [PATCH 0889/1714] refactor git_log.py --- docs/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 1996110377..aa6b3f4bc2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3036,4 +3036,3 @@ Timeline .. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread .. _Tidelift security contact: https://tidelift.com/security .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme -.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme From 052c1e2ddbd712c201786b7cc9983a4284d3a6c8 Mon Sep 17 00:00:00 2001 From: Daniel Li Date: Thu, 29 Sep 2022 11:44:52 -0400 Subject: [PATCH 0890/1714] Resolve race condition in Process.threads() (#2151) * Resolve race condition in Process.threads() Process.threads() has a race condition triggered when a thread exits after the open_binary() call and before the read() call. When this happens, the read() call raises ProcessLookupError. Handle the race condition by catching ProcessLookupError from read() and treating the same as a FileNotFoundError from open_binary(). This is the same approach used in ppid_map(). Signed-off-by: Daniel Li * Also catch ProcessLookupError in open_files() Signed-off-by: Daniel Li --- CREDITS | 3 +++ HISTORY.rst | 2 ++ psutil/_pslinux.py | 8 ++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CREDITS b/CREDITS index 93866be5ff..65b9f4a284 100644 --- a/CREDITS +++ b/CREDITS @@ -798,3 +798,6 @@ N: Bernhard Urban-Forster C: Austria W: https://github.com/lewurm I: 2135 + +N: Daniel Li +I: 2150 diff --git a/HISTORY.rst b/HISTORY.rst index 263bad0661..c9ea732006 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,8 @@ XXXX-XX-XX undefined ``ethtool_cmd_speed`` symbol. - 2142_, [POSIX]: `net_if_stats()`_ 's ``flags`` on Python 2 returned unicode instead of str. (patch by Matthieu Darbois) +- 2150_, [Linux] `Process.threads()`_ may raise ``NoSuchProcess``. Fix race + condition. (patch by Daniel Li) 5.9.2 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 206241f6b9..9dc9643ab3 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -2061,9 +2061,9 @@ def threads(self): try: with open_binary(fname) as f: st = f.read().strip() - except FileNotFoundError: - # no such file or directory; it means thread - # disappeared on us + 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)") @@ -2217,7 +2217,7 @@ def open_files(self): with open_binary(file) as f: pos = int(f.readline().split()[1]) flags = int(f.readline().split()[1], 8) - except FileNotFoundError: + except (FileNotFoundError, ProcessLookupError): # fd gone in the meantime; process may # still be alive hit_enoent = True From 7271ec7e0fc4e346a339cdc2e1640a52e92845f4 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Wed, 5 Oct 2022 23:15:37 +0200 Subject: [PATCH 0891/1714] fix: disk usage report on macOS 12+ (#2152) --- .github/workflows/build.yml | 4 +-- CREDITS | 2 +- HISTORY.rst | 1 + psutil/_psposix.py | 7 ++++++ psutil/_psutil_osx.c | 47 ++++++++++++++++++++++++++++++++++++ psutil/tests/__init__.py | 34 +++++++++++++++++++++++++- psutil/tests/test_process.py | 5 ++++ psutil/tests/test_system.py | 7 ++++-- 8 files changed, 101 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 967b67622a..e7d3c42240 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: fail-fast: false matrix: # os: [ubuntu-latest, macos-latest, windows-latest] - os: [ubuntu-latest, macos-10.15] + os: [ubuntu-latest, macos-12] include: - {name: Linux, python: '3.9', os: ubuntu-latest} env: @@ -86,7 +86,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-10.15] + os: [ubuntu-latest, macos-12] include: - {name: Linux, python: '3.9', os: ubuntu-latest} env: diff --git a/CREDITS b/CREDITS index 65b9f4a284..a44a16bdf5 100644 --- a/CREDITS +++ b/CREDITS @@ -781,7 +781,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039, 2142 +I: 2039, 2142, 2147 N: Hugo van Kemenade W: https://github.com/hugovk diff --git a/HISTORY.rst b/HISTORY.rst index c9ea732006..fa18f51672 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,7 @@ XXXX-XX-XX undefined ``ethtool_cmd_speed`` symbol. - 2142_, [POSIX]: `net_if_stats()`_ 's ``flags`` on Python 2 returned unicode instead of str. (patch by Matthieu Darbois) +- 2147_, [macOS] Fix disk usage report on macOS 12+. (patch by Matthieu Darbois) - 2150_, [Linux] `Process.threads()`_ may raise ``NoSuchProcess``. Fix race condition. (patch by Daniel Li) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 8e6629d726..39912d97e4 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -14,6 +14,7 @@ from ._common import memoize from ._common import sdiskusage from ._common import usage_percent +from ._common import MACOS from ._compat import PY3 from ._compat import ChildProcessError from ._compat import FileNotFoundError @@ -22,6 +23,9 @@ from ._compat import ProcessLookupError from ._compat import unicode +if MACOS: + from . import _psutil_osx + if sys.version_info >= (3, 4): import enum @@ -193,6 +197,9 @@ def disk_usage(path): avail_to_user = (st.f_bavail * st.f_frsize) # Total space being used in general. 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 diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index f634be362f..ab43871f8f 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -836,6 +836,52 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } +static PyObject * +psutil_disk_usage_used(PyObject *self, PyObject *args) { + PyObject *py_default_value; + PyObject *py_mount_point_bytes = NULL; + char* mount_point; + +#if PY_MAJOR_VERSION >= 3 + if (!PyArg_ParseTuple(args, "O&O", PyUnicode_FSConverter, &py_mount_point_bytes, &py_default_value)) { + return NULL; + } + mount_point = PyBytes_AsString(py_mount_point_bytes); + if (NULL == mount_point) { + Py_XDECREF(py_mount_point_bytes); + return NULL; + } +#else + if (!PyArg_ParseTuple(args, "sO", &mount_point, &py_default_value)) { + return NULL; + } +#endif + +#ifdef ATTR_VOL_SPACEUSED + /* Call getattrlist(ATTR_VOL_SPACEUSED) to get used space info. */ + int ret; + struct { + uint32_t size; + uint64_t spaceused; + } __attribute__((aligned(4), packed)) attrbuf = {0}; + struct attrlist attrs = {0}; + + attrs.bitmapcount = ATTR_BIT_MAP_COUNT; + attrs.volattr = ATTR_VOL_INFO | ATTR_VOL_SPACEUSED; + Py_BEGIN_ALLOW_THREADS + ret = getattrlist(mount_point, &attrs, &attrbuf, sizeof(attrbuf), 0); + Py_END_ALLOW_THREADS + if (ret == 0) { + Py_XDECREF(py_mount_point_bytes); + return PyLong_FromUnsignedLongLong(attrbuf.spaceused); + } + psutil_debug("getattrlist(ATTR_VOL_SPACEUSED) failed, fall-back to default value"); +#endif + Py_XDECREF(py_mount_point_bytes); + Py_INCREF(py_default_value); + return py_default_value; +} + /* * Return process threads */ @@ -1681,6 +1727,7 @@ static PyMethodDef mod_methods[] = { {"cpu_times", psutil_cpu_times, METH_VARARGS}, {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"disk_usage_used", psutil_disk_usage_used, METH_VARARGS}, {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"pids", psutil_pids, METH_VARARGS}, diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 6ddafc9724..a7da8d237a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -18,6 +18,7 @@ import gc import inspect import os +import platform import random import re import select @@ -47,6 +48,7 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import bytes2human +from psutil._common import memoize from psutil._common import print_color from psutil._common import supports_ipv6 from psutil._compat import PY3 @@ -84,7 +86,8 @@ "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", + "HAS_SENSORS_TEMPERATURES", "HAS_MEMORY_FULL_INFO", "MACOS_11PLUS", + "MACOS_12PLUS", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', 'spawn_children_pair', @@ -129,6 +132,35 @@ 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 diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b2c3b2c768..26869e9838 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -49,6 +49,7 @@ 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 PsutilTestCase @@ -1426,6 +1427,10 @@ def clean_dict(d): @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(""" diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index d6b7a21a4e..753249bc33 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -44,6 +44,7 @@ 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 @@ -561,8 +562,10 @@ def test_disk_usage(self): 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 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 From 0707c16339c3918f77eeb83cb1d6047cdb2f0e10 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Sat, 8 Oct 2022 11:14:46 +0200 Subject: [PATCH 0892/1714] fix: race condition in test_posix.TestProcess.test_cmdline (#2153) --- CREDITS | 2 +- HISTORY.rst | 2 ++ psutil/tests/test_posix.py | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index a44a16bdf5..e53c1577d1 100644 --- a/CREDITS +++ b/CREDITS @@ -781,7 +781,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039, 2142, 2147 +I: 2039, 2142, 2147, 2153 N: Hugo van Kemenade W: https://github.com/hugovk diff --git a/HISTORY.rst b/HISTORY.rst index fa18f51672..f77b206072 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX - 2147_, [macOS] Fix disk usage report on macOS 12+. (patch by Matthieu Darbois) - 2150_, [Linux] `Process.threads()`_ may raise ``NoSuchProcess``. Fix race condition. (patch by Daniel Li) +- 2153_, [macOS] Fix race condition in test_posix.TestProcess.test_cmdline. + (patch by Matthieu Darbois) 5.9.2 ===== diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index ebbf7a6e88..d87322300f 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -271,6 +271,12 @@ def test_exe(self): 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()) From 3f81c62ecb37098318fd1f80eebb0bdffc8f097c Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Tue, 18 Oct 2022 20:05:17 +0200 Subject: [PATCH 0893/1714] fix #2021, fix #1954, provide OSX arm64 bins + add pyproject.toml (#2040) This commit updates the build workflow to use the latest cibuildwheel as a GitHub Action. cibuildwheel configuration is now in its own file (as there's no `pyproject.toml` yet) Signed-off-by: mayeut --- .github/workflows/build.yml | 29 ++--------------------------- .gitignore | 1 + pyproject.toml | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 27 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7d3c42240..4c0ef79bd2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,17 +28,7 @@ jobs: strategy: fail-fast: false matrix: - # os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-12] - include: - - {name: Linux, python: '3.9', os: ubuntu-latest} - env: - CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py - CIBW_TEST_EXTRAS: test - CIBW_BUILD: 'cp36-* cp37-* cp38-* cp39-* cp310-*' - CIBW_SKIP: '*-musllinux_*' steps: - name: Cancel previous runs @@ -53,16 +43,8 @@ jobs: cache: pip cache-dependency-path: .github/workflows/build.yml - - name: Install cibuildwheel - run: pip install cibuildwheel - - # - name: (Windows) install Visual C++ for Python 2.7 - # if: matrix.os == 'windows-latest' - # run: | - # choco install vcpython27 -f -y - - name: Run tests - run: cibuildwheel . + uses: pypa/cibuildwheel@v2.11.1 - name: Create wheels uses: actions/upload-artifact@v3 @@ -87,16 +69,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-12] - include: - - {name: Linux, python: '3.9', os: ubuntu-latest} env: - CIBW_ARCHS_LINUX: 'x86_64 i686' CIBW_TEST_COMMAND: PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py CIBW_TEST_EXTRAS: test CIBW_BUILD: 'cp27-*' - CIBW_SKIP: '*-musllinux_*' steps: - name: Cancel previous runs @@ -111,11 +89,8 @@ jobs: cache: pip cache-dependency-path: .github/workflows/build.yml - - name: Install cibuildwheel - run: pip install cibuildwheel==1.12.0 - - name: Run tests - run: cibuildwheel . + uses: pypa/cibuildwheel@v1.12.0 - name: Create wheels uses: actions/upload-artifact@v3 diff --git a/.gitignore b/.gitignore index 3d22b0b355..ddafc64c6a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ syntax: glob .tox/ build/ dist/ +wheelhouse/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..47031d2689 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["setuptools>=43"] +build-backend = "setuptools.build_meta" + +[tool.cibuildwheel] +build = ["cp36-*", "cp37-*", "cp38-*", "cp39-*", "cp310-*"] +skip = ["*-musllinux*"] +test-extras = "test" +test-command = [ + "PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py", + "PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py" +] + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] From 8ad2d5ba0cd2665e28e6ab84af11f74a83163471 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Oct 2022 21:12:05 +0200 Subject: [PATCH 0894/1714] move isort and coverage config into pyproject.toml ...since pyproject.toml was introduced in #2040. CC @mayeut Signed-off-by: Giampaolo Rodola --- .coveragerc | 32 ------------------------ .github/workflows/build.yml | 2 +- .isort.cfg | 7 ------ MANIFEST.in | 3 +-- Makefile | 6 ++--- psutil/_psposix.py | 3 ++- pyproject.toml | 39 ++++++++++++++++++++++++++++++ scripts/internal/git_pre_commit.py | 3 +-- 8 files changed, 47 insertions(+), 48 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .isort.cfg diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index c6772e9539..0000000000 --- a/.coveragerc +++ /dev/null @@ -1,32 +0,0 @@ -[report] - -include = - *psutil* -omit = - psutil/_compat.py - psutil/tests/* - setup.py -exclude_lines = - enum.IntEnum - except ImportError: - globals().update - if __name__ == .__main__.: - if _WINDOWS: - if BSD - if enum is None: - if enum is not None: - if FREEBSD - if has_enums: - if LINUX - if LITTLE_ENDIAN: - if NETBSD - if OPENBSD - if MACOS - if ppid_map is None: - if PY3: - if SUNOS - if sys.platform.startswith - if WINDOWS - import enum - pragma: no cover - raise NotImplementedError diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c0ef79bd2..e572b0297f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -151,6 +151,6 @@ jobs: # py3 python3 -m pip install flake8 isort python3 -m flake8 . - python3 -m isort --settings=.isort.cfg . + python3 -m isort . # clinter find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python3 scripts/internal/clinter.py diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 58f66946ce..0000000000 --- a/.isort.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# See: https://pycqa.github.io/isort/docs/configuration/options - -[settings] -# one import per line -force_single_line = true -# blank spaces after import section -lines_after_imports = 2 diff --git a/MANIFEST.in b/MANIFEST.in index c9cd85e991..e19c7e2de2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,5 @@ -include .coveragerc include .flake8 include .gitignore -include .isort.cfg include CONTRIBUTING.md include CREDITS include HISTORY.rst @@ -123,6 +121,7 @@ include psutil/tests/test_system.py include psutil/tests/test_testutils.py include psutil/tests/test_unicode.py include psutil/tests/test_windows.py +include pyproject.toml include scripts/battery.py include scripts/cpu_distribution.py include scripts/disk_usage.py diff --git a/Makefile b/Makefile index caf4c9e9d9..fb7f89ae0a 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ TSCRIPT = psutil/tests/runner.py # Internal. DEPS = \ - git+https://github.com/PyCQA/autoflake.git \ + autoflake \ autopep8 \ check-manifest \ concurrencytest \ @@ -195,7 +195,7 @@ flake8: ## Run flake8 linter. @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 isort: ## Run isort linter. - @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg --check-only + @git ls-files '*.py' | xargs $(PYTHON) -m isort --check-only c-linter: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py @@ -214,7 +214,7 @@ fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --jobs 0 --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys fix-imports: ## Fix imports with isort. - @git ls-files '*.py' | xargs $(PYTHON) -m isort --settings=.isort.cfg + @git ls-files '*.py' | xargs $(PYTHON) -m isort fix-all: ## Run all code fixers. ${MAKE} fix-flake8 diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 39912d97e4..1d250bf75c 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -10,11 +10,11 @@ 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 ._common import MACOS from ._compat import PY3 from ._compat import ChildProcessError from ._compat import FileNotFoundError @@ -23,6 +23,7 @@ from ._compat import ProcessLookupError from ._compat import unicode + if MACOS: from . import _psutil_osx diff --git a/pyproject.toml b/pyproject.toml index 47031d2689..52d4c72865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,42 @@ +[tool.isort] +force_single_line = true # one import per line +lines_after_imports = 2 # blank spaces after import section + +[tool.coverage.report] +include = [ + "*psutil*" +] +omit = [ + "psutil/_compat.py", + "psutil/tests/*", + "setup.py", +] +exclude_lines = [ + "enum.IntEnum", + "except ImportError:", + "globals().update", + "if __name__ == .__main__.:", + "if _WINDOWS:", + "if BSD", + "if enum is None:", + "if enum is not None:", + "if FREEBSD", + "if has_enums:", + "if LINUX", + "if LITTLE_ENDIAN:", + "if MACOS", + "if NETBSD", + "if OPENBSD", + "if ppid_map is None:", + "if PY3:", + "if SUNOS", + "if sys.platform.startswith", + "if WINDOWS", + "import enum", + "pragma: no cover", + "raise NotImplementedError", +] + [build-system] requires = ["setuptools>=43"] build-backend = "setuptools.build_meta" diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 46b3bc5335..8778362727 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -128,8 +128,7 @@ def main(): return exit("python code didn't pass 'flake8' style check; " "try running 'make fix-flake8'") # isort - assert os.path.exists('.isort.cfg') - cmd = "%s -m isort --settings=.isort.cfg --check-only %s" % ( + cmd = "%s -m isort --check-only %s" % ( PYTHON, " ".join(py_files)) ret = subprocess.call(shlex.split(cmd)) if ret != 0: From e1e48a8822f734a9d5ce1729c08984e975fabb1f Mon Sep 17 00:00:00 2001 From: iam-py-test <84232764+iam-py-test@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:27:09 -0400 Subject: [PATCH 0895/1714] Fix a typo (#2047) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5f0afcdfa7..4961c695f9 100644 --- a/README.rst +++ b/README.rst @@ -220,7 +220,7 @@ Disks >>> psutil.disk_partitions() [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid', maxfile=255, maxpath=4096), - sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext, opts='rw', maxfile=255, maxpath=4096)] + sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext', opts='rw', maxfile=255, maxpath=4096)] >>> >>> psutil.disk_usage('/') sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) From aae4346a560601e0417188f55cdd2d2a0e606fa7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Oct 2022 22:11:08 +0200 Subject: [PATCH 0896/1714] pre-release + give CREDITS to @mayeut (PR #2040) and @eallrich (new supporter) Signed-off-by: Giampaolo Rodola --- CREDITS | 2 +- HISTORY.rst | 11 ++++++++--- README.rst | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CREDITS b/CREDITS index e53c1577d1..6f1b6e5b86 100644 --- a/CREDITS +++ b/CREDITS @@ -781,7 +781,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039, 2142, 2147, 2153 +I: 2039, 2142, 2147, 2153, 2040 N: Hugo van Kemenade W: https://github.com/hugovk diff --git a/HISTORY.rst b/HISTORY.rst index f77b206072..e983399019 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,14 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.3 (IN DEVELOPMENT) -====================== +5.9.3 +===== + +2022-10-18 + +**Enhancements** -XXXX-XX-XX +- 2040_, [macOS]: provide wheels for arm64 architecture. (patch by Matthieu + Darbois) **Bug fixes** diff --git a/README.rst b/README.rst index 5f0afcdfa7..6eb3cfe964 100644 --- a/README.rst +++ b/README.rst @@ -142,6 +142,7 @@ Supporters + add your avatar From 1da9f7928e79ecc127768e7a5bc4eb7f785f4513 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Tue, 18 Oct 2022 22:18:05 +0200 Subject: [PATCH 0897/1714] chore: skip test_cpu_freq on macOS arm64 (#2146) --- psutil/tests/test_contracts.py | 3 +++ psutil/tests/test_memleaks.py | 3 +++ psutil/tests/test_osx.py | 3 +++ psutil/tests/test_system.py | 3 +++ 4 files changed, 12 insertions(+) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index d376a33853..fde857b670 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -12,6 +12,7 @@ import errno import multiprocessing import os +import platform import signal import stat import sys @@ -235,6 +236,8 @@ def test_cpu_times_percent(self): 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: diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index e507e837c2..8d31193b06 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -19,6 +19,7 @@ import functools import os +import platform import unittest import psutil @@ -364,6 +365,8 @@ 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) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index d0f588ad6f..8abddb528a 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -6,6 +6,7 @@ """macOS specific tests.""" +import platform import re import time import unittest @@ -144,6 +145,8 @@ 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( diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 753249bc33..42b29e8fab 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -10,6 +10,7 @@ import datetime import errno import os +import platform import pprint import shutil import signal @@ -511,6 +512,8 @@ def test_cpu_stats(self): 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): From 669b672dc7acc696376e5ca8104cf01086fe6f8c Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Tue, 18 Oct 2022 22:30:29 +0200 Subject: [PATCH 0898/1714] fix: linter issues from #2146 (#2155) Signed-off-by: mayeut --- psutil/tests/test_contracts.py | 3 ++- psutil/tests/test_memleaks.py | 3 ++- psutil/tests/test_system.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index fde857b670..3b806ee376 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -237,7 +237,8 @@ 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(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: diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 8d31193b06..dbd1588dfe 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -366,7 +366,8 @@ def test_cpu_stats(self): @fewtimes_if_linux() # TODO: remove this once 1892 is fixed - @unittest.skipIf(MACOS and platform.machine() == 'arm64', "skipped due to #1892") + @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) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 42b29e8fab..1722b515de 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -513,7 +513,8 @@ def test_cpu_stats(self): self.assertGreater(value, 0) # TODO: remove this once 1892 is fixed - @unittest.skipIf(MACOS and platform.machine() == 'arm64', "skipped due to #1892") + @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): From 085691ac46b060aeb6d17a202fcc33062f61d9b5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 20 Oct 2022 22:38:18 +0200 Subject: [PATCH 0899/1714] improve API speed benchmark script #2102 --- scripts/internal/print_api_speed.py | 147 ++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index ee2e3254ee..f41d0466ff 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -4,30 +4,75 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Benchmark all API calls. +""" +Benchmark all API calls and print them from fastest to slowest. $ make print_api_speed -SYSTEM APIS SECONDS ----------------------------------- -cpu_count 0.000014 -disk_usage 0.000027 -cpu_times 0.000037 -cpu_percent 0.000045 -... - -PROCESS APIS SECONDS ----------------------------------- -create_time 0.000001 -nice 0.000005 -cwd 0.000011 -cpu_affinity 0.000011 -ionice 0.000013 -... +SYSTEM APIS NUM CALLS SECONDS +------------------------------------------------- +disk_usage 300 0.00157 +cpu_count 300 0.00255 +pid_exists 300 0.00792 +cpu_times 300 0.01044 +boot_time 300 0.01136 +cpu_percent 300 0.01290 +cpu_times_percent 300 0.01515 +virtual_memory 300 0.01594 +users 300 0.01964 +net_io_counters 300 0.02027 +cpu_stats 300 0.02034 +net_if_addrs 300 0.02962 +swap_memory 300 0.03209 +sensors_battery 300 0.05186 +pids 300 0.07954 +net_if_stats 300 0.09321 +disk_io_counters 300 0.09406 +cpu_count (cores) 300 0.10293 +disk_partitions 300 0.10345 +cpu_freq 300 0.20817 +sensors_fans 300 0.63476 +sensors_temperatures 231 2.00039 +process_iter (all) 171 2.01300 +net_connections 97 2.00206 + +PROCESS APIS NUM CALLS SECONDS +------------------------------------------------- +create_time 300 0.00009 +exe 300 0.00015 +nice 300 0.00057 +ionice 300 0.00091 +cpu_affinity 300 0.00091 +cwd 300 0.00151 +num_fds 300 0.00391 +memory_info 300 0.00597 +memory_percent 300 0.00648 +io_counters 300 0.00707 +name 300 0.00894 +status 300 0.00900 +ppid 300 0.00906 +num_threads 300 0.00932 +cpu_num 300 0.00933 +num_ctx_switches 300 0.00943 +uids 300 0.00979 +gids 300 0.01002 +cpu_times 300 0.01008 +cmdline 300 0.01009 +terminal 300 0.01059 +is_running 300 0.01063 +threads 300 0.01209 +connections 300 0.01276 +cpu_percent 300 0.01463 +open_files 300 0.01630 +username 300 0.01655 +environ 300 0.02250 +memory_full_info 300 0.07066 +memory_maps 300 0.74281 """ from __future__ import division from __future__ import print_function +import argparse import inspect import os from timeit import default_timer as timer @@ -36,16 +81,23 @@ from psutil._common import print_color +TIMES = 300 timings = [] -templ = "%-25s %s" +templ = "%-25s %10s %10s" + + +def print_header(what): + s = templ % (what, "NUM CALLS", "SECONDS") + print_color(s, color=None, bold=True) + print("-" * len(s)) def print_timings(): - timings.sort(key=lambda x: x[1]) + timings.sort(key=lambda x: (x[1], -x[2]), reverse=True) i = 0 while timings[:]: - title, elapsed = timings.pop(0) - s = templ % (title, "%f" % elapsed) + title, times, elapsed = timings.pop(0) + s = templ % (title, str(times), "%.5f" % elapsed) if i > len(timings) - 5: print_color(s, color="red") else: @@ -53,13 +105,48 @@ def print_timings(): def timecall(title, fun, *args, **kw): + print("%-50s" % title, end="", flush=True) t = timer() - fun(*args, **kw) - elapsed = timer() - t - timings.append((title, elapsed)) + for n in range(TIMES): + fun(*args, **kw) + elapsed = timer() - t + if elapsed > 2: + break + print("\r", end="", flush=True) + timings.append((title, n + 1, elapsed)) + + +def set_highest_priority(): + """Set highest CPU and I/O priority (requires root).""" + p = psutil.Process() + if psutil.WINDOWS: + p.nice(psutil.HIGH_PRIORITY_CLASS) + else: + p.nice(-20) + + if psutil.LINUX: + p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + elif psutil.WINDOWS: + p.ionice(psutil.IOPRIO_HIGH) def main(): + global TIMES + + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('-t', '--times', type=int, default=TIMES) + args = parser.parse_args() + TIMES = args.times + assert TIMES > 1, TIMES + + try: + set_highest_priority() + except psutil.AccessDenied: + prio_set = False + else: + prio_set = True + # --- system public_apis = [] @@ -73,8 +160,7 @@ def main(): if name not in ignore: public_apis.append(name) - print_color(templ % ("SYSTEM APIS", "SECONDS"), color=None, bold=True) - print("-" * 34) + print_header("SYSTEM APIS") for name in public_apis: fun = getattr(psutil, name) args = () @@ -89,11 +175,10 @@ def main(): # --- process print("") - print_color(templ % ("PROCESS APIS", "SECONDS"), color=None, bold=True) - print("-" * 34) + print_header("PROCESS APIS") ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', - 'pid', 'rlimit'] + 'pid', 'rlimit', 'children'] if psutil.MACOS: ignore.append('memory_maps') # XXX p = psutil.Process() @@ -103,6 +188,10 @@ def main(): timecall(name, fun) print_timings() + if not prio_set: + print_color("\nWARN: couldn't set highest process priority " + + "(requires root)", "red") + if __name__ == '__main__': main() From 1b09c1b54cb2ae46145de7f710e9b990cd264016 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 20 Oct 2022 23:56:43 +0200 Subject: [PATCH 0900/1714] fix py2 compatibility Signed-off-by: Giampaolo Rodola --- scripts/internal/print_api_speed.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index f41d0466ff..e85b703820 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -75,6 +75,7 @@ import argparse import inspect import os +import sys from timeit import default_timer as timer import psutil @@ -105,14 +106,16 @@ def print_timings(): def timecall(title, fun, *args, **kw): - print("%-50s" % title, end="", flush=True) + print("%-50s" % title, end="") + sys.stdout.flush() t = timer() for n in range(TIMES): fun(*args, **kw) elapsed = timer() - t if elapsed > 2: break - print("\r", end="", flush=True) + print("\r", end="") + sys.stdout.flush() timings.append((title, n + 1, elapsed)) From 614e91158ced7a65aabbe7244d52de1cebfc494b Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Fri, 21 Oct 2022 01:02:23 +0200 Subject: [PATCH 0901/1714] feature: use ABI3 for cp36+ (#2102) --- .ci/appveyor/install.ps1 | 85 ------------------------------ .github/workflows/build.yml | 49 +++++++++++++++-- appveyor.yml | 45 ---------------- psutil/_psutil_linux.c | 32 +++++------ psutil/arch/windows/process_info.c | 2 +- pyproject.toml | 20 +++++-- setup.py | 52 ++++++++++++++---- 7 files changed, 120 insertions(+), 165 deletions(-) delete mode 100644 .ci/appveyor/install.ps1 diff --git a/.ci/appveyor/install.ps1 b/.ci/appveyor/install.ps1 deleted file mode 100644 index 3f05628255..0000000000 --- a/.ci/appveyor/install.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -# Sample script to install Python and pip under Windows -# Authors: Olivier Grisel and Kyle Kastner -# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ - -$BASE_URL = "https://www.python.org/ftp/python/" -$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -$GET_PIP_PATH = "C:\get-pip.py" - - -function DownloadPython ($python_version, $platform_suffix) { - $webclient = New-Object System.Net.WebClient - $filename = "python-" + $python_version + $platform_suffix + ".msi" - $url = $BASE_URL + $python_version + "/" + $filename - - $basedir = $pwd.Path + "\" - $filepath = $basedir + $filename - if (Test-Path $filename) { - Write-Host "Reusing" $filepath - return $filepath - } - - # Download and retry up to 5 times in case of network transient errors. - Write-Host "Downloading" $filename "from" $url - $retry_attempts = 3 - for($i=0; $i -lt $retry_attempts; $i++){ - try { - $webclient.DownloadFile($url, $filepath) - break - } - Catch [Exception]{ - Start-Sleep 1 - } - } - Write-Host "File saved at" $filepath - return $filepath -} - - -function InstallPython ($python_version, $architecture, $python_home) { - Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home - if (Test-Path $python_home) { - Write-Host $python_home "already exists, skipping." - return $false - } - if ($architecture -eq "32") { - $platform_suffix = "" - } else { - $platform_suffix = ".amd64" - } - $filepath = DownloadPython $python_version $platform_suffix - Write-Host "Installing" $filepath "to" $python_home - $args = "/qn /i $filepath TARGETDIR=$python_home" - Write-Host "msiexec.exe" $args - Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru - Write-Host "Python $python_version ($architecture) installation complete" - return $true -} - - -function InstallPip ($python_home) { - $pip_path = $python_home + "/Scripts/pip.exe" - $python_path = $python_home + "/python.exe" - if (-not(Test-Path $pip_path)) { - Write-Host "Installing pip..." - $webclient = New-Object System.Net.WebClient - $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) - Write-Host "Executing:" $python_path $GET_PIP_PATH - Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru - } else { - Write-Host "pip already installed." - } -} - -function InstallPackage ($python_home, $pkg) { - $pip_path = $python_home + "/Scripts/pip.exe" - & $pip_path install $pkg -} - -function main () { - InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON - InstallPip $env:PYTHON - InstallPackage $env:PYTHON wheel -} - -main diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e572b0297f..3a52c8fe7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,15 +20,15 @@ on: [push, pull_request] name: build jobs: - # Linux + macOS + Python 3 - linux-macos-py3: + # Linux + macOS + Windows Python 3 + py3: name: ${{ matrix.os }}-py3 runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-12] + os: [ubuntu-latest, macos-12, windows-2019] steps: - name: Cancel previous runs @@ -60,6 +60,49 @@ jobs: mv dist/psutil*.tar.gz wheelhouse/ python scripts/internal/print_hashes.py wheelhouse/ + # Windows cp37+ tests + # psutil tests do not like running from a virtualenv with python>=3.7 so + # not using cibuildwheel for those. run them "manually" with this job. + windows-py3-test: + name: windows-py3-test-${{ matrix.python }}-${{ matrix.architecture }} + needs: py3 + runs-on: windows-2019 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + python: ["3.7", "3.8", "3.9", "3.10", "~3.11.0-0"] + architecture: ["x86", "x64"] + + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "${{ matrix.python }}" + architecture: "${{ matrix.architecture }}" + cache: pip + cache-dependency-path: .github/workflows/build.yml + - name: Download wheels + uses: actions/download-artifact@v3 + with: + name: wheels + path: wheelhouse + - name: Run tests + run: | + mkdir .tests + cd .tests + pip install $(find ../wheelhouse -name '*-cp36-abi3-${{ matrix.architecture == 'x86' && 'win32' || 'win_amd64'}}.whl')[test] + export PYTHONWARNINGS=always + export PYTHONUNBUFFERED=1 + export PSUTIL_DEBUG=1 + python ../psutil/tests/runner.py + python ../psutil/tests/test_memleaks.py + shell: bash + # Linux + macOS + Python 2 linux-macos-py2: name: ${{ matrix.os }}-py2 diff --git a/appveyor.yml b/appveyor.yml index 4bbd51aeb5..0752e610c9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,62 +24,17 @@ environment: PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python37" - PYTHON_VERSION: "3.7.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python38" - PYTHON_VERSION: "3.8.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python39" - PYTHON_VERSION: "3.9.x" - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python310" - PYTHON_VERSION: "3.10.x" - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - PYTHON_ARCH: "32" - # 64 bits - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.x" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python37-x64" - PYTHON_VERSION: "3.7.x" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python38-x64" - PYTHON_VERSION: "3.8.x" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python39-x64" - PYTHON_VERSION: "3.9.x" - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python310-x64" - PYTHON_VERSION: "3.10.x" - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - PYTHON_ARCH: "64" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" install: - - "powershell .ci\\appveyor\\install.ps1" - # - ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:/get-pip.py') - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user setuptools pip" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py setup-dev-env" diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 64cdf0b550..1ed1f91a0f 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -296,52 +296,46 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { cpu_set_t cpu_set; size_t len; pid_t pid; - int i, seq_len; + Py_ssize_t i, seq_len; PyObject *py_cpu_set; - PyObject *py_cpu_seq = NULL; if (!PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &py_cpu_set)) return NULL; if (!PySequence_Check(py_cpu_set)) { - PyErr_Format(PyExc_TypeError, "sequence argument expected, got %s", - Py_TYPE(py_cpu_set)->tp_name); - goto error; + return PyErr_Format(PyExc_TypeError, "sequence argument expected, got %R", Py_TYPE(py_cpu_set)); } - py_cpu_seq = PySequence_Fast(py_cpu_set, "expected a sequence or integer"); - if (!py_cpu_seq) - goto error; - seq_len = PySequence_Fast_GET_SIZE(py_cpu_seq); + seq_len = PySequence_Size(py_cpu_set); + if (seq_len < 0) { + return NULL; + } CPU_ZERO(&cpu_set); for (i = 0; i < seq_len; i++) { - PyObject *item = PySequence_Fast_GET_ITEM(py_cpu_seq, i); + PyObject *item = PySequence_GetItem(py_cpu_set, i); + if (!item) { + return NULL; + } #if PY_MAJOR_VERSION >= 3 long value = PyLong_AsLong(item); #else long value = PyInt_AsLong(item); #endif + Py_XDECREF(item); if ((value == -1) || PyErr_Occurred()) { if (!PyErr_Occurred()) PyErr_SetString(PyExc_ValueError, "invalid CPU value"); - goto error; + return NULL; } CPU_SET(value, &cpu_set); } len = sizeof(cpu_set); if (sched_setaffinity(pid, len, &cpu_set)) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; + return PyErr_SetFromErrno(PyExc_OSError); } - Py_DECREF(py_cpu_seq); Py_RETURN_NONE; - -error: - if (py_cpu_seq != NULL) - Py_DECREF(py_cpu_seq); - return NULL; } #endif /* PSUTIL_HAVE_CPU_AFFINITY */ diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index d44c4eb75e..1981d306a6 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -578,7 +578,7 @@ psutil_get_cmdline(DWORD pid, int use_peb) { wcslen(szArglist[i])); if (py_unicode == NULL) goto out; - PyList_SET_ITEM(py_retlist, i, py_unicode); + PyList_SetItem(py_retlist, i, py_unicode); py_unicode = NULL; } ret = py_retlist; diff --git a/pyproject.toml b/pyproject.toml index 52d4c72865..87b84fc677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,12 +38,11 @@ exclude_lines = [ ] [build-system] -requires = ["setuptools>=43"] +requires = ["setuptools>=43", "wheel"] build-backend = "setuptools.build_meta" [tool.cibuildwheel] -build = ["cp36-*", "cp37-*", "cp38-*", "cp39-*", "cp310-*"] -skip = ["*-musllinux*"] +skip = ["pp*", "*-musllinux*"] test-extras = "test" test-command = [ "PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py", @@ -52,3 +51,18 @@ test-command = [ [tool.cibuildwheel.macos] archs = ["x86_64", "arm64"] + +[tool.cibuildwheel.windows] +# psutil tests do not like running from a virtualenv with python>=3.7 +# restrict build & tests to cp36 +# cp36-abi3 wheels will need to be tested outside cibuildwheel for python>=3.7 +build = "cp36-*" +test-command = [ + "python {project}/psutil/tests/runner.py", + "python {project}/psutil/tests/test_memleaks.py" +] + +[tool.cibuildwheel.windows.environment] +PYTHONWARNINGS = "always" +PYTHONUNBUFFERED = "1" +PSUTIL_DEBUG = "1" diff --git a/setup.py b/setup.py index 0f6716b3b5..b3f20666df 100755 --- a/setup.py +++ b/setup.py @@ -31,6 +31,12 @@ setuptools = None from distutils.core import Extension from distutils.core import setup + try: + from wheel.bdist_wheel import bdist_wheel + except ImportError: + if "CIBUILDWHEEL" in os.environ: + raise + bdist_wheel = None HERE = os.path.abspath(os.path.dirname(__file__)) @@ -53,6 +59,9 @@ PYPY = '__pypy__' in sys.builtin_module_names +PY36_PLUS = sys.version_info[:2] >= (3, 6) +CP36_PLUS = PY36_PLUS and sys.implementation.name == "cpython" + macros = [] if POSIX: macros.append(("PSUTIL_POSIX", 1)) @@ -100,6 +109,12 @@ def get_version(): VERSION = get_version() macros.append(('PSUTIL_VERSION', int(VERSION.replace('.', '')))) +if bdist_wheel and CP36_PLUS and (MACOS or LINUX or WINDOWS): + py_limited_api = {"py_limited_api": True} + macros.append(('Py_LIMITED_API', '0x03060000')) +else: + py_limited_api = {} + def get_description(): script = os.path.join(HERE, "scripts", "internal", "convert_readme.py") @@ -182,7 +197,8 @@ def get_winver(): "ws2_32", "PowrProf", "pdh", ], # extra_compile_args=["/W 4"], - # extra_link_args=["/DEBUG"] + # extra_link_args=["/DEBUG"], + **py_limited_api ) elif MACOS: @@ -197,7 +213,8 @@ def get_winver(): define_macros=macros, extra_link_args=[ '-framework', 'CoreFoundation', '-framework', 'IOKit' - ]) + ], + **py_limited_api) elif FREEBSD: macros.append(("PSUTIL_FREEBSD", 1)) @@ -214,7 +231,8 @@ def get_winver(): 'psutil/arch/freebsd/proc_socks.c', ], define_macros=macros, - libraries=["devstat"]) + libraries=["devstat"], + **py_limited_api) elif OPENBSD: macros.append(("PSUTIL_OPENBSD", 1)) @@ -228,7 +246,8 @@ def get_winver(): 'psutil/arch/openbsd/proc.c', ], define_macros=macros, - libraries=["kvm"]) + libraries=["kvm"], + **py_limited_api) elif NETBSD: macros.append(("PSUTIL_NETBSD", 1)) @@ -240,7 +259,8 @@ def get_winver(): 'psutil/arch/netbsd/socks.c', ], define_macros=macros, - libraries=["kvm"]) + libraries=["kvm"], + **py_limited_api) elif LINUX: def get_ethtool_macro(): @@ -276,7 +296,8 @@ def get_ethtool_macro(): ext = Extension( 'psutil._psutil_linux', sources=sources + ['psutil/_psutil_linux.c'], - define_macros=macros) + define_macros=macros, + **py_limited_api) elif SUNOS: macros.append(("PSUTIL_SUNOS", 1)) @@ -288,7 +309,8 @@ def get_ethtool_macro(): 'psutil/arch/solaris/environ.c' ], define_macros=macros, - libraries=['kstat', 'nsl', 'socket']) + libraries=['kstat', 'nsl', 'socket'], + **py_limited_api) elif AIX: macros.append(("PSUTIL_AIX", 1)) @@ -300,7 +322,8 @@ def get_ethtool_macro(): 'psutil/arch/aix/common.c', 'psutil/arch/aix/ifaddrs.c'], libraries=['perfstat'], - define_macros=macros) + define_macros=macros, + **py_limited_api) else: sys.exit('platform %s is not supported' % sys.platform) @@ -310,7 +333,8 @@ def get_ethtool_macro(): posix_extension = Extension( 'psutil._psutil_posix', define_macros=macros, - sources=sources) + sources=sources, + **py_limited_api) if SUNOS: def get_sunos_update(): # See https://serverfault.com/q/524883 @@ -341,11 +365,21 @@ def get_sunos_update(): else: extensions = [ext] +cmdclass = {} +if py_limited_api: + class bdist_wheel_abi3(bdist_wheel): + def get_tag(self): + python, abi, plat = bdist_wheel.get_tag(self) + return python, "abi3", plat + + cmdclass["bdist_wheel"] = bdist_wheel_abi3 + def main(): kwargs = dict( name='psutil', version=VERSION, + cmdclass=cmdclass, description=__doc__ .replace('\n', ' ').strip() if __doc__ else '', long_description=get_description(), long_description_content_type='text/x-rst', From cd8827d065d63114060aae69f36b447006c5a70f Mon Sep 17 00:00:00 2001 From: Daniel Widdis Date: Thu, 20 Oct 2022 16:11:21 -0700 Subject: [PATCH 0902/1714] Use system-level values for Windows virtual memory (#2077) * Use system-level values for Windows virtual memory Submitting this as a draft PR as I believe it will address #2074. I do not have either a C or Python development environment to test these changes, so they are provided in the hopes someone else can test and confirm they fix the issue. There's probably some room to simplify the code with temporary variables to reduce redundancy. For background, I have implemented precisely this logic in my own (Java-based) project [here](https://github.com/oshi/oshi/blob/3bb9eafbe2062edad4108b7b12501b1f9be27361/oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsVirtualMemory.java#L110-L118) and [here](https://github.com/oshi/oshi/blob/3bb9eafbe2062edad4108b7b12501b1f9be27361/oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsGlobalMemory.java#L253-L263) following a detailed investigation in https://github.com/oshi/oshi/issues/1182 Signed-off-by: Daniel Widdis * Formatting, credits, change log Signed-off-by: Daniel Widdis Signed-off-by: Daniel Widdis --- CREDITS | 4 ++++ HISTORY.rst | 10 ++++++++++ psutil/_psutil_windows.c | 25 +++++++++++++++---------- psutil/_pswindows.py | 6 +++--- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/CREDITS b/CREDITS index 6f1b6e5b86..29298ca3e0 100644 --- a/CREDITS +++ b/CREDITS @@ -801,3 +801,7 @@ I: 2135 N: Daniel Li I: 2150 + +N: Daniel Widdis +W: https://github.com/dbwiddis +I: 2077 diff --git a/HISTORY.rst b/HISTORY.rst index e983399019..f0d0905d4a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.4 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +**Bug fixes** + +- 2077_, [Windows]: Use system-level values for `virtual_memory()`. (patch by + Daniel Widdis) + 5.9.3 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 83da3a26c1..6e51c449dd 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -610,20 +610,25 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { */ static PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { - MEMORYSTATUSEX memInfo; - memInfo.dwLength = sizeof(MEMORYSTATUSEX); + unsigned long long totalPhys, availPhys, totalSys, availSys, pageSize; + PERFORMANCE_INFORMATION perfInfo; - if (! GlobalMemoryStatusEx(&memInfo)) { + if (! GetPerformanceInfo(&perfInfo, sizeof(PERFORMANCE_INFORMATION))) { PyErr_SetFromWindowsErr(0); return NULL; } - return Py_BuildValue("(LLLLLL)", - memInfo.ullTotalPhys, // total - memInfo.ullAvailPhys, // avail - memInfo.ullTotalPageFile, // total page file - memInfo.ullAvailPageFile, // avail page file - memInfo.ullTotalVirtual, // total virtual - memInfo.ullAvailVirtual); // avail virtual + // values are size_t, widen (if needed) to long long + pageSize = perfInfo.PageSize; + totalPhys = perfInfo.PhysicalTotal * pageSize; + availPhys = perfInfo.PhysicalAvailable * pageSize; + totalSys = perfInfo.CommitLimit * pageSize; + availSys = totalSys - perfInfo.CommitTotal * pageSize; + return Py_BuildValue( + "(LLLL)", + totalPhys, + availPhys, + totalSys, + availSys); } diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 7d882b7747..b546f15d83 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -229,7 +229,7 @@ def getpagesize(): 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 @@ -248,8 +248,8 @@ def swap_memory(): total_system = mem[2] free_system = mem[3] - # Despite the name PageFile refers to total system memory here - # thus physical memory values need to be subtracted to get swap values + # system memory (commit total/limit) is the sum of physical and swap + # thus physical memory values need to be substracted to get swap values total = total_system - total_phys free = min(total, free_system - free_phys) used = total - free From 8e58bce0409db7415b963fc394e93a2a4c68d61c Mon Sep 17 00:00:00 2001 From: Amir Rossert Date: Thu, 20 Oct 2022 19:13:28 -0400 Subject: [PATCH 0903/1714] Linux: fix missing SPEED_UNKNOWN definition (#2156) Signed-off-by: Amir Rossert --- psutil/_psutil_linux.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 1ed1f91a0f..0bacb2048a 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -58,6 +58,11 @@ static const int NCPUS_START = sizeof(unsigned long) * CHAR_BIT; #endif +#ifndef SPEED_UNKNOWN + #define SPEED_UNKNOWN -1 +#endif + + #if PSUTIL_HAVE_IOPRIO enum { IOPRIO_WHO_PROCESS = 1, From b0f780a1f46775e7d18a5176c839dac1a0977191 Mon Sep 17 00:00:00 2001 From: Lawrence D'Anna Date: Thu, 20 Oct 2022 19:29:01 -0400 Subject: [PATCH 0904/1714] build fix for Mac OS, Apple Silicon (#2010) On MacOS, arm64 IFM_1000_TX and IFM_1000_T are the same value, causing a build failure. Signed-off-by: Lawrence D'Anna --- psutil/_psutil_posix.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 07c10398bb..24628afc78 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -708,13 +708,14 @@ int psutil_get_nic_speed(int ifm_active) { case(IFM_1000_LX): // 1000baseLX - single-mode fiber case(IFM_1000_CX): // 1000baseCX - 150ohm STP #if defined(IFM_1000_TX) && !defined(PSUTIL_OPENBSD) + #define HAS_CASE_IFM_1000_TX 1 // FreeBSD 4 and others (but NOT OpenBSD) -> #define IFM_1000_T in net/if_media.h case(IFM_1000_TX): #endif #ifdef IFM_1000_FX case(IFM_1000_FX): #endif -#ifdef IFM_1000_T +#if defined(IFM_1000_T) && (!HAS_CASE_IFM_1000_TX || IFM_1000_T != IFM_1000_TX) case(IFM_1000_T): #endif return 1000; From 57a7a7047e22f79cabb984e883a70888eb370558 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 21 Oct 2022 01:40:23 +0200 Subject: [PATCH 0905/1714] update HISTORY + give CREDITS for @arossert, @smoofra, @mayeut for #2102, #2156, #2010 --- CREDITS | 10 +++++++++- HISTORY.rst | 12 +++++++++++- psutil/__init__.py | 2 +- psutil/tests/__init__.py | 2 +- psutil/tests/test_osx.py | 4 ++-- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CREDITS b/CREDITS index 29298ca3e0..1990231593 100644 --- a/CREDITS +++ b/CREDITS @@ -781,7 +781,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039, 2142, 2147, 2153, 2040 +I: 2039, 2142, 2147, 2153, 2040, 2102 N: Hugo van Kemenade W: https://github.com/hugovk @@ -805,3 +805,11 @@ I: 2150 N: Daniel Widdis W: https://github.com/dbwiddis I: 2077 + +N: Amir Rossert +W: https://github.com/arossert +I: 2156 + +N: Lawrence D'Anna +W: https://github.com/smoofra +I: 2010 diff --git a/HISTORY.rst b/HISTORY.rst index f0d0905d4a..2b1257bd4b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,10 +5,20 @@ XXXX-XX-XX +**Enhancements** + +- 2102_: use Limited API when building wheels with CPython 3.6+ on Linux, + macOS and Windows. This allows to use pre-built wheels in all future versions + of cPython 3. (patch by Matthieu Darbois) + **Bug fixes** -- 2077_, [Windows]: Use system-level values for `virtual_memory()`. (patch by +- 2077_, [Windows]: Use system-level values for `virtual_memory()`_. (patch by Daniel Widdis) +- 2156_, [Linux]: compilation may fail on very old gcc compilers due to missing + ``SPEED_UNKNOWN`` definition. (patch by Amir Rossert) +- 2010_, [macOS]: on MacOS, arm64 ``IFM_1000_TX`` and ``IFM_1000_T`` are the + same value, causing a build failure. (patch by Lawrence D'Anna) 5.9.3 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index ca2d9273cf..5674279188 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.3" +__version__ = "5.9.4" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index a7da8d237a..ec9c7480a9 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -174,7 +174,7 @@ def macos_version(): if CI_TESTING: NO_RETRIES *= 3 GLOBAL_TIMEOUT *= 3 - TOLERANCE_SYS_MEM *= 3 + TOLERANCE_SYS_MEM *= 4 TOLERANCE_DISK_USAGE *= 3 # --- file names diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 8abddb528a..3141cef902 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -192,13 +192,13 @@ def test_vmem_wired(self): def test_swapmem_sin(self): vmstat_val = vm_stat("Pageins") psutil_val = psutil.swap_memory().sin - self.assertEqual(psutil_val, vmstat_val) + self.assertEqual(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.assertEqual(psutil_val, vmstat_val) + self.assertEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) # Not very reliable. # def test_swapmem_total(self): From 55b588ac48d9b8e37f01c8428090066c01771627 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 21 Oct 2022 01:43:31 +0200 Subject: [PATCH 0906/1714] fix OSX tests broken by accident --- psutil/tests/test_osx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 3141cef902..af12648414 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -192,13 +192,13 @@ def test_vmem_wired(self): def test_swapmem_sin(self): vmstat_val = vm_stat("Pageins") psutil_val = psutil.swap_memory().sin - self.assertEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + 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.assertEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) # Not very reliable. # def test_swapmem_total(self): From 8e4099d9f063ceb4ee3da5845562c5b934f83544 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 21 Oct 2022 11:34:55 -0700 Subject: [PATCH 0907/1714] add windows test for free physical mem #2074 --- psutil/tests/test_windows.py | 39 ++++++++++++++++++++++++++++++++++++ scripts/internal/winmake.py | 4 ++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 3bf458079f..cb2642d192 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -23,6 +23,7 @@ import psutil from psutil import WINDOWS from psutil._compat import FileNotFoundError +from psutil._compat import which from psutil._compat import super from psutil.tests import APPVEYOR from psutil.tests import GITHUB_ACTIONS @@ -31,6 +32,7 @@ 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 @@ -62,6 +64,37 @@ class WindowsTestCase(PsutilTestCase): pass +def powershell(cmd): + """Currently not used, but avalable 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 avalable 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 # =================================================================== @@ -123,6 +156,12 @@ def test_total_phymem(self): 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) + # @unittest.skipIf(wmi is None, "wmi module is not installed") # def test__UPTIME(self): # # _UPTIME constant is not public but it is used internally diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index bb88005199..466887c2c0 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -383,7 +383,7 @@ def setup_dev_env(): sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEPS))) -def check_flake8(): +def flake8(): """Run flake8 against all py files""" py_files = subprocess.check_output("git ls-files") if PY3: @@ -565,7 +565,7 @@ def main(): sp.add_parser('install', help="build + install in develop/edit mode") sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") sp.add_parser('install-pip', help="install pip") - sp.add_parser('check_flake8', help="run flake8 against all py files") + sp.add_parser('flake8', help="run flake8 against all py files") sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") sp.add_parser('setup-dev-env', help="install deps") From a307e8f09143c9ba11b19517d0175c050fd3b11d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Nov 2022 19:44:18 +0100 Subject: [PATCH 0908/1714] pre release Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 6 +++--- Makefile | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2b1257bd4b..32ed0b38c3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.4 (IN DEVELOPMENT) -====================== +5.9.4 +===== -XXXX-XX-XX +2022-11-07 **Enhancements** diff --git a/Makefile b/Makefile index fb7f89ae0a..bdafabaf1f 100644 --- a/Makefile +++ b/Makefile @@ -270,8 +270,8 @@ check-sdist: ## Create source distribution and checks its sanity (MANIFEST) ${MAKE} clean $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv PYTHONWARNINGS=all $(PYTHON) setup.py sdist - build/venv/local/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz - build/venv/local/bin/python -c "import os; os.chdir('build/venv'); import psutil" + build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz + build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" pre-release: ## Check if we're ready to produce a new release. ${MAKE} check-sdist From aa1253c92984e5be6335c024d84e7ec1072baddf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Nov 2022 20:55:58 +0100 Subject: [PATCH 0909/1714] minor fixes to pypi upload machinery Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 4 ++++ Makefile | 8 +------- scripts/internal/download_wheels_appveyor.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a52c8fe7a..8a651a0796 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,18 +79,22 @@ jobs: uses: styfle/cancel-workflow-action@0.9.1 with: access_token: ${{ github.token }} + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: "${{ matrix.python }}" architecture: "${{ matrix.architecture }}" cache: pip cache-dependency-path: .github/workflows/build.yml + - name: Download wheels uses: actions/download-artifact@v3 with: name: wheels path: wheelhouse + - name: Run tests run: | mkdir .tests diff --git a/Makefile b/Makefile index bdafabaf1f..a5ee28e29b 100644 --- a/Makefile +++ b/Makefile @@ -256,13 +256,6 @@ sdist: ## Create tar.gz source distribution. $(PYTHON) setup.py sdist $(PYTHON) -m twine check dist/*.tar.gz -upload-src: ## Upload source tarball on https://pypi.org/project/psutil/ - ${MAKE} sdist - $(PYTHON) -m twine upload dist/*.tar.gz - -upload-wheels: ## Upload wheels in dist/* directory on PyPI. - $(PYTHON) -m twine upload dist/*.whl - # --- others check-sdist: ## Create source distribution and checks its sanity (MANIFEST) @@ -293,6 +286,7 @@ pre-release: ## Check if we're ready to produce a new release. assert 'XXXX' not in history, 'XXXX in HISTORY.rst';" release: ## Create a release (down/uploads tar.gz, wheels, git tag release). + $(PYTHON) -m twine check dist/* $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index 786e23d812..d69ace62f4 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -28,7 +28,7 @@ USER = "giampaolo" PROJECT = "psutil" BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.7', '3.8', '3.9', '3.10'] +PY_VERSIONS = ['2.7'] TIMEOUT = 30 From 04eaa1584d62aa7f4dd99771a0363198706c2a51 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Nov 2022 20:57:43 +0100 Subject: [PATCH 0910/1714] remove HTML from README so that I can build wheels on win Signed-off-by: Giampaolo Rodola --- README.rst | 71 +++++++++++++----------------------------------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/README.rst b/README.rst index d1598ca3ef..d995cd84fc 100644 --- a/README.rst +++ b/README.rst @@ -59,21 +59,18 @@ ----- -.. raw:: html - -
- -
-
- Home    - Install    - Documentation    - Download    - Forum    - Blog    - Funding    - What's new    -
+Quick links +=========== + +- `Home page `_ +- `Install `_ +- `Documentation `_ +- `Download `_ +- `Forum `_ +- `StackOverflow `_ +- `Blog `_ +- `What's new `_ + Summary ======= @@ -113,45 +110,11 @@ and have your logo displayed in here and psutil `doc - - - -    - - - - - add your logo - -Supporters -========== - -.. raw:: html - -
- - - - - - - - - - - - -
- add your avatar - - -Contributing -============ - -See `contributing guidelines `__. +.. image:: https://github.com/giampaolo/psutil/raw/master/docs/_static/tidelift-logo.png + :width: 200 + :alt: Alternative text + +`Add your logo `__. Example usages ============== From bcf0f31dcebba770f7e5b5047b47867c6924e2bc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Nov 2022 21:24:24 +0100 Subject: [PATCH 0911/1714] remove HTML from README so that I can build wheels on win Signed-off-by: Giampaolo Rodola --- README.rst | 61 ------------------------------------------------------ 1 file changed, 61 deletions(-) diff --git a/README.rst b/README.rst index d995cd84fc..87b5765796 100644 --- a/README.rst +++ b/README.rst @@ -1,64 +1,3 @@ -| |downloads| |stars| |forks| |contributors| |coverage| -| |version| |py-versions| |packages| |license| -| |github-actions| |appveyor| |doc| |twitter| |tidelift| - -.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg - :target: https://pepy.tech/project/psutil - :alt: Downloads - -.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/stargazers - :alt: Github stars - -.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/network/members - :alt: Github forks - -.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/graphs/contributors - :alt: Contributors - -.. |github-actions| image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD - :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild - :alt: Linux, macOS, Windows tests - -.. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows - :target: https://ci.appveyor.com/project/giampaolo/psutil - :alt: Windows tests (Appveyor) - -.. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master - :target: https://coveralls.io/github/giampaolo/psutil?branch=master - :alt: Test coverage (coverall.io) - -.. |doc| image:: https://readthedocs.org/projects/psutil/badge/?version=latest - :target: https://psutil.readthedocs.io/en/latest/ - :alt: Documentation Status - -.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi - :target: https://pypi.org/project/psutil - :alt: Latest version - -.. |py-versions| image:: https://img.shields.io/pypi/pyversions/psutil.svg - :alt: Supported Python versions - -.. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg - :target: https://repology.org/metapackage/python:psutil/versions - :alt: Binary packages - -.. |license| image:: https://img.shields.io/pypi/l/psutil.svg - :target: https://github.com/giampaolo/psutil/blob/master/LICENSE - :alt: License - -.. |twitter| image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=flat&logo=twitter&logoColor=4FADFF - :target: https://twitter.com/grodola - :alt: Twitter Follow - -.. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat - :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme - :alt: Tidelift - ------ - Quick links =========== From 7da5a631234af1425027b9190dd6c1029327ae6f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Nov 2022 21:54:38 +0100 Subject: [PATCH 0912/1714] revert previous commit Signed-off-by: Giampaolo Rodola --- README.rst | 128 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 87b5765796..d1598ca3ef 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,79 @@ -Quick links -=========== +| |downloads| |stars| |forks| |contributors| |coverage| +| |version| |py-versions| |packages| |license| +| |github-actions| |appveyor| |doc| |twitter| |tidelift| -- `Home page `_ -- `Install `_ -- `Documentation `_ -- `Download `_ -- `Forum `_ -- `StackOverflow `_ -- `Blog `_ -- `What's new `_ +.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg + :target: https://pepy.tech/project/psutil + :alt: Downloads +.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/stargazers + :alt: Github stars + +.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/network/members + :alt: Github forks + +.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/graphs/contributors + :alt: Contributors + +.. |github-actions| image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD + :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild + :alt: Linux, macOS, Windows tests + +.. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows + :target: https://ci.appveyor.com/project/giampaolo/psutil + :alt: Windows tests (Appveyor) + +.. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master + :target: https://coveralls.io/github/giampaolo/psutil?branch=master + :alt: Test coverage (coverall.io) + +.. |doc| image:: https://readthedocs.org/projects/psutil/badge/?version=latest + :target: https://psutil.readthedocs.io/en/latest/ + :alt: Documentation Status + +.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi + :target: https://pypi.org/project/psutil + :alt: Latest version + +.. |py-versions| image:: https://img.shields.io/pypi/pyversions/psutil.svg + :alt: Supported Python versions + +.. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg + :target: https://repology.org/metapackage/python:psutil/versions + :alt: Binary packages + +.. |license| image:: https://img.shields.io/pypi/l/psutil.svg + :target: https://github.com/giampaolo/psutil/blob/master/LICENSE + :alt: License + +.. |twitter| image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=flat&logo=twitter&logoColor=4FADFF + :target: https://twitter.com/grodola + :alt: Twitter Follow + +.. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme + :alt: Tidelift + +----- + +.. raw:: html + +
+ +
+
+ Home    + Install    + Documentation    + Download    + Forum    + Blog    + Funding    + What's new    +
Summary ======= @@ -49,11 +113,45 @@ and have your logo displayed in here and psutil `doc `__. +.. raw:: html + +
+ + + +    + + + +
+ add your logo + +Supporters +========== + +.. raw:: html + +
+ + + + + + + + + + + + +
+ add your avatar + + +Contributing +============ + +See `contributing guidelines `__. Example usages ============== From 6941261ad010d4d1e936af22fec224e9900509c0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 8 Nov 2022 22:51:09 +0100 Subject: [PATCH 0913/1714] update and fix issue/PR labeler script (CI) Signed-off-by: Giampaolo Rodola --- .github/workflows/issues.py | 28 ++++++++++++++++++++-------- .github/workflows/issues.yml | 13 +++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index ef68649582..fce52ce757 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -194,9 +194,9 @@ def get_issue(): def log(msg): if '\n' in msg or "\r\n" in msg: - print(">>>\n%s\n<<<" % msg) + print(">>>\n%s\n<<<" % msg, flush=True) else: - print(">>> %s <<<" % msg) + print(">>> %s <<<" % msg, flush=True) def add_label(issue, label): @@ -221,6 +221,7 @@ def should_add(issue, label): def _guess_labels_from_text(issue, text): + assert isinstance(text, str), text for label, keywords in LABELS_MAP.items(): for keyword in keywords: if keyword.lower() in text.lower(): @@ -228,11 +229,13 @@ def _guess_labels_from_text(issue, text): def add_labels_from_text(issue, text): + assert isinstance(text, str), text for label, keyword in _guess_labels_from_text(issue, text): add_label(issue, label) def add_labels_from_new_body(issue, text): + assert isinstance(text, str), text log("start searching for template lines in new issue/PR body") # add os label r = re.search(r"\* OS:.*?\n", text) @@ -288,14 +291,21 @@ def add_labels_from_new_body(issue, text): def on_new_issue(issue): def has_text(text): - return text in issue.title.lower() or text in issue.body.lower() + return text in issue.title.lower() or \ + (issue.body and text in issue.body.lower()) + + def body_mentions_python_h(): + if not issue.body: + return False + body = issue.body.replace(' ', '') + return "#include\n^~~~" in body or \ + "#include\r\n^~~~" in body log("searching for missing Python.h") if has_text("missing python.h") or \ has_text("python.h: no such file or directory") or \ - "#include\n^~~~" in issue.body.replace(' ', '') or \ - "#include\r\n^~~~" in issue.body.replace(' ', ''): - log("found") + body_mentions_python_h(): + log("found mention of Python.h") issue.create_comment(REPLY_MISSING_PYTHON_HEADERS) issue.edit(state='closed') return @@ -317,12 +327,14 @@ def main(): if is_event_new_issue(): log("created new issue %s" % issue) add_labels_from_text(issue, issue.title) - add_labels_from_new_body(issue, issue.body) + if issue.body: + add_labels_from_new_body(issue, issue.body) on_new_issue(issue) elif is_event_new_pr(): log("created new PR %s" % issue) add_labels_from_text(issue, issue.title) - add_labels_from_new_body(issue, issue.body) + if issue.body: + add_labels_from_new_body(issue, issue.body) on_new_pr(issue) else: log("unhandled event") diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index d5118157f1..245d00cb4e 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -1,4 +1,5 @@ # Fired by Github Actions every time an issue, PR or comment is created. + name: issues on: issues: @@ -14,17 +15,17 @@ jobs: # install python - uses: actions/checkout@v3 - name: Install Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: - python-version: 3.8 - cache: pip - cache-dependency-path: .github/workflows/issues.yml + python-version: '3.x' + # install deps - name: Install deps - run: python -m pip install --upgrade pip PyGithub + run: python3 -m pip install PyGithub + # run - name: Run env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - PYTHONUNBUFFERED=1 python .github/workflows/issues.py + PYTHONUNBUFFERED=1 PYTHONWARNINGS=always python3 .github/workflows/issues.py From 1f1074f8fbed46077ddc05dae92af95fecc78a33 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Nov 2022 10:35:58 +0100 Subject: [PATCH 0914/1714] #2164: fix compilation failures on linux < 2.6.27 / CentOS 5 (#2171) --- .github/workflows/issues.py | 2 ++ HISTORY.rst | 5 +++++ psutil/__init__.py | 2 +- psutil/_psutil_linux.c | 8 +++++++- psutil/tests/test_windows.py | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index fce52ce757..2aea40bed9 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -10,6 +10,8 @@ on the situation. """ +from __future__ import print_function + import functools import json import os diff --git a/HISTORY.rst b/HISTORY.rst index 32ed0b38c3..e0b7083714 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,10 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.5 (IN DEVELOPMENT) +====================== + +- 2164_, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). + 5.9.4 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 5674279188..deaf02eaf2 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.4" +__version__ = "5.9.5" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 0bacb2048a..f04fe776ee 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -78,10 +78,16 @@ ioprio_set(int which, int who, int ioprio) { return syscall(__NR_ioprio_set, which, who, ioprio); } -// defined in linux/ethtool.h but not always available (e.g. Android) +// * defined in linux/ethtool.h but not always available (e.g. Android) +// * #ifdef check needed for old kernels, see: +// https://github.com/giampaolo/psutil/issues/2164 static inline uint32_t psutil_ethtool_cmd_speed(const struct ethtool_cmd *ecmd) { +#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 27) + return ecmd->speed; +#else return (ecmd->speed_hi << 16) | ecmd->speed; +#endif } #define IOPRIO_CLASS_SHIFT 13 diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index cb2642d192..9b163a185f 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -23,8 +23,8 @@ import psutil from psutil import WINDOWS from psutil._compat import FileNotFoundError -from psutil._compat import which 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 e390ffea34f9b9d2243e80ed47e7954dd4c4d930 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Nov 2022 12:42:04 +0100 Subject: [PATCH 0915/1714] setup.py: provide better err msg if can't compile ...also refactor it a bit and add install instructions for Alpine Linux Signed-off-by: Giampaolo Rodola --- INSTALL.rst | 5 ++++ setup.py | 80 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 46aee6cd0c..4f5831e67b 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -25,6 +25,11 @@ RedHat / CentOS:: sudo yum install gcc python3-devel pip install --no-binary :all: psutil +Alpine:: + + sudo apk add gcc python3-dev + pip install --no-binary :all: psutil + Windows (build) --------------- diff --git a/setup.py b/setup.py index b3f20666df..31777b9d2d 100755 --- a/setup.py +++ b/setup.py @@ -109,6 +109,8 @@ def get_version(): VERSION = get_version() macros.append(('PSUTIL_VERSION', int(VERSION.replace('.', '')))) +# Py_LIMITED_API lets us create a single wheel which works with multiple +# python versions, including unreleased ones. if bdist_wheel and CP36_PLUS and (MACOS or LINUX or WINDOWS): py_limited_api = {"py_limited_api": True} macros.append(('Py_LIMITED_API', '0x03060000')) @@ -147,11 +149,41 @@ def write(self, s): setattr(sys, stream_name, orig) -def missdeps(msg): - s = hilite("C compiler or Python headers are not installed ", color="red") - s += hilite("on this system. Try to run:\n", color="red") - s += hilite(msg, color="red", bold=True) - print(s, file=sys.stderr) +def missdeps(cmdline): + s = "psutil could not be installed from sources" + if not SUNOS and not which("gcc"): + s += " because gcc is not installed. " + else: + s += ". Perhaps Python header files are not installed. " + s += "Try running:\n" + s += " %s" % cmdline + print(hilite(s, color="red", bold=True), file=sys.stderr) + + +def unix_can_compile(c_code): + from distutils.errors import CompileError + from distutils.unixccompiler import UnixCCompiler + + with tempfile.NamedTemporaryFile( + suffix='.c', delete=False, mode="wt") as f: + f.write(c_code) + + tempdir = tempfile.mkdtemp() + try: + compiler = UnixCCompiler() + # https://github.com/giampaolo/psutil/pull/1568 + if os.getenv('CC'): + compiler.set_executable('compiler_so', os.getenv('CC')) + with silenced_output('stderr'): + with silenced_output('stdout'): + compiler.compile([f.name], output_dir=tempdir) + except CompileError: + return False + else: + return True + finally: + os.remove(f.name) + shutil.rmtree(tempdir) if WINDOWS: @@ -263,36 +295,11 @@ def get_winver(): **py_limited_api) elif LINUX: - def get_ethtool_macro(): - # see: https://github.com/giampaolo/psutil/issues/659 - from distutils.errors import CompileError - from distutils.unixccompiler import UnixCCompiler - - with tempfile.NamedTemporaryFile( - suffix='.c', delete=False, mode="wt") as f: - f.write("#include ") - - output_dir = tempfile.mkdtemp() - try: - compiler = UnixCCompiler() - # https://github.com/giampaolo/psutil/pull/1568 - if os.getenv('CC'): - compiler.set_executable('compiler_so', os.getenv('CC')) - with silenced_output('stderr'): - with silenced_output('stdout'): - compiler.compile([f.name], output_dir=output_dir) - except CompileError: - return ("PSUTIL_ETHTOOL_MISSING_TYPES", 1) - else: - return None - finally: - os.remove(f.name) - shutil.rmtree(output_dir) + # see: https://github.com/giampaolo/psutil/issues/659 + if not unix_can_compile("#include "): + macros.append(("PSUTIL_ETHTOOL_MISSING_TYPES", 1)) macros.append(("PSUTIL_LINUX", 1)) - ETHTOOL_MACRO = get_ethtool_macro() - if ETHTOOL_MACRO is not None: - macros.append(ETHTOOL_MACRO) ext = Extension( 'psutil._psutil_linux', sources=sources + ['psutil/_psutil_linux.c'], @@ -455,13 +462,18 @@ def main(): setup(**kwargs) success = True finally: - if not success and POSIX and not which('gcc'): + cmd = sys.argv[1] if len(sys.argv) >= 2 else '' + if not success and POSIX and \ + cmd.startswith(("build", "install", "sdist", "bdist", + "develop")): py3 = "3" if PY3 else "" if LINUX: if which('dpkg'): missdeps("sudo apt-get install gcc python%s-dev" % py3) elif which('rpm'): missdeps("sudo yum install gcc python%s-devel" % py3) + elif which('apk'): + missdeps("sudo apk add gcc python%s-dev" % py3) elif MACOS: print(hilite("XCode (https://developer.apple.com/xcode/) " "is not installed"), color="red", file=sys.stderr) From ad8426c463b24b4b39b722b4d28949f2080182fc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Nov 2022 16:19:19 +0100 Subject: [PATCH 0916/1714] use argparse module in 3 scripts/internal scripts --- scripts/internal/check_broken_links.py | 12 +++++++----- scripts/internal/convert_readme.py | 10 +++++++--- scripts/internal/print_hashes.py | 26 ++++++++++++++++---------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 1a07611613..8ceab1c75f 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -41,6 +41,7 @@ from __future__ import print_function +import argparse import concurrent.futures import functools import os @@ -227,13 +228,14 @@ def parallel_validator(urls): def main(): - files = sys.argv[1:] - if not files: - print("usage: %s " % sys.argv[0], file=sys.stderr) - return sys.exit(1) + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('files', nargs="+") + parser.parse_args() + args = parser.parse_args() all_urls = [] - for fname in files: + for fname in args.files: urls = get_urls(fname) if urls: print("%4s %s" % (len(urls), fname)) diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index 81f7bdb6dc..d96c6c5d36 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -5,11 +5,12 @@ # found in the LICENSE file. """ -Convert README.rst format to make it compatible with PyPI (no raw html). +Remove raw HTML from README.rst to make it compatible with PyPI on +dist upload. """ +import argparse import re -import sys summary = """\ @@ -40,7 +41,10 @@ def main(): - with open(sys.argv[1]) as f: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('file', type=str) + args = parser.parse_args() + with open(args.file) as f: data = f.read() data = re.sub(r".. raw:: html\n+\s+
", summary, data) data = re.sub(r"Sponsors\n========[\s\S]*?Example usages", funding, data) diff --git a/scripts/internal/print_hashes.py b/scripts/internal/print_hashes.py index 434c43d59e..69bc9edbe0 100755 --- a/scripts/internal/print_hashes.py +++ b/scripts/internal/print_hashes.py @@ -5,13 +5,13 @@ # found in the LICENSE file. """ -Prints files hashes. -See: https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode +Prints files hashes, see: +https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode """ +import argparse import hashlib import os -import sys def csum(file, kind): @@ -22,13 +22,19 @@ def csum(file, kind): def main(): - dir = sys.argv[1] - for name in sorted(os.listdir(dir)): - file = os.path.join(dir, name) - md5 = csum(file, "md5") - sha256 = csum(file, "sha256") - print("%s\nmd5: %s\nsha256: %s\n" % ( - os.path.basename(file), md5, sha256)) + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('dir', type=str, + help='directory containing tar.gz or wheel files') + args = parser.parse_args() + for name in sorted(os.listdir(args.dir)): + file = os.path.join(args.dir, name) + if os.path.isfile(file): + md5 = csum(file, "md5") + sha256 = csum(file, "sha256") + print("%s\nmd5: %s\nsha256: %s\n" % ( + os.path.basename(file), md5, sha256)) + else: + print("skipping %r (not a file)" % file) if __name__ == "__main__": From 167833fe27daac88a73407c99987980fd878e0e0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Nov 2022 16:53:33 +0100 Subject: [PATCH 0917/1714] GH actions: update actions' deps to latest version Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a651a0796..eecd6eabe5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,14 +37,12 @@ jobs: access_token: ${{ github.token }} - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: - python-version: 3.9 - cache: pip - cache-dependency-path: .github/workflows/build.yml + python-version: 3.11 - name: Run tests - uses: pypa/cibuildwheel@v2.11.1 + uses: pypa/cibuildwheel@v2.11.2 - name: Create wheels uses: actions/upload-artifact@v3 @@ -86,8 +84,6 @@ jobs: with: python-version: "${{ matrix.python }}" architecture: "${{ matrix.architecture }}" - cache: pip - cache-dependency-path: .github/workflows/build.yml - name: Download wheels uses: actions/download-artifact@v3 @@ -130,11 +126,9 @@ jobs: access_token: ${{ github.token }} - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: python-version: 3.9 - cache: pip - cache-dependency-path: .github/workflows/build.yml - name: Run tests uses: pypa/cibuildwheel@v1.12.0 @@ -157,7 +151,7 @@ jobs: runs-on: macos-12 steps: - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.6.0 + uses: styfle/cancel-workflow-action@0.9.1 with: access_token: ${{ github.token }} @@ -184,10 +178,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 - with: - cache: pip - cache-dependency-path: .github/workflows/build.yml + - uses: actions/setup-python@v4 - name: 'Run linters' run: | # py2 From 90c2eec6daa69977b75ebfccc6dc977404faab45 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Thu, 10 Nov 2022 22:10:20 +0100 Subject: [PATCH 0918/1714] fix: long-description on Windows (#2168) --- .github/workflows/build.yml | 11 +++++++++++ scripts/internal/convert_readme.py | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eecd6eabe5..502ecb5a22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,3 +192,14 @@ jobs: python3 -m isort . # clinter find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python3 scripts/internal/clinter.py + + check_dist: + name: Check dist + needs: [linux-macos-py2, py3] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + path: wheelhouse + - run: pipx run twine check --strict wheelhouse/* diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index d96c6c5d36..bd00cf2340 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -48,7 +48,11 @@ def main(): data = f.read() data = re.sub(r".. raw:: html\n+\s+
", summary, data) data = re.sub(r"Sponsors\n========[\s\S]*?Example usages", funding, data) - print(data) + if len(sys.argv) > 2: + with open(sys.argv[2], "wb") as f: + f.write(data.encode("utf8")) + else: + print(data) if __name__ == '__main__': From 8543b11cc8f9575c28dc748931e590219accf6f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 11 Nov 2022 03:25:31 -0800 Subject: [PATCH 0919/1714] make.bat: use default 'python' exe; get rid of hard-coded python path --- make.bat | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/make.bat b/make.bat index b424ed2039..2c79d42911 100644 --- a/make.bat +++ b/make.bat @@ -3,28 +3,26 @@ rem ========================================================================== rem Shortcuts for various tasks, emulating UNIX "make" on Windows. rem It is primarily intended as a shortcut for compiling / installing -rem psutil ("make.bat build", "make.bat install") and running tests -rem ("make.bat test"). +rem psutil and running tests. E.g.: +rem +rem make build +rem make install +rem make test rem rem This script is modeled after my Windows installation which uses: rem - Visual studio 2008 for Python 2.7 rem - Visual studio 2010 for Python 3.4+ rem ...therefore it might not work on your Windows installation. rem -rem By default C:\Python27\python.exe is used. rem To compile for a specific Python version run: rem set PYTHON=C:\Python34\python.exe & make.bat build rem rem To use a different test script: -rem set PYTHON=C:\Python34\python.exe & set TSCRIPT=foo.py & make.bat test +rem set TSCRIPT=foo.py & make.bat test rem ========================================================================== if "%PYTHON%" == "" ( - if exist "C:\Python38-64\python.exe" ( - set PYTHON=C:\Python38-64\python.exe - ) else ( - set PYTHON=C:\Python27\python.exe - ) + set PYTHON=python ) if "%TSCRIPT%" == "" ( From c0aadb18038f2ce29dbdd9cc11be072b15fd34eb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 11 Nov 2022 07:29:43 -0800 Subject: [PATCH 0920/1714] fix long_description on Windows (see: #2168) --- .github/workflows/build.yml | 6 +++--- Makefile | 6 +++--- scripts/internal/convert_readme.py | 6 +----- setup.py | 12 +++++------- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 502ecb5a22..1d9999f8b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ name: build jobs: # Linux + macOS + Windows Python 3 py3: - name: ${{ matrix.os }}-py3 + name: py3-${{ matrix.os }} runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: @@ -62,7 +62,7 @@ jobs: # psutil tests do not like running from a virtualenv with python>=3.7 so # not using cibuildwheel for those. run them "manually" with this job. windows-py3-test: - name: windows-py3-test-${{ matrix.python }}-${{ matrix.architecture }} + name: py3-windows-test-${{ matrix.python }}-${{ matrix.architecture }} needs: py3 runs-on: windows-2019 timeout-minutes: 20 @@ -105,7 +105,7 @@ jobs: # Linux + macOS + Python 2 linux-macos-py2: - name: ${{ matrix.os }}-py2 + name: py2-${{ matrix.os }} runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: diff --git a/Makefile b/Makefile index a5ee28e29b..930325dd14 100644 --- a/Makefile +++ b/Makefile @@ -254,7 +254,7 @@ git-tag-release: ## Git-tag a new release. sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest $(PYTHON) setup.py sdist - $(PYTHON) -m twine check dist/*.tar.gz + $(PYTHON) -m twine check --strict dist/*.tar.gz # --- others @@ -276,7 +276,7 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} download-wheels-appveyor ${MAKE} print-hashes ${MAKE} print-wheels - $(PYTHON) -m twine check dist/* + $(PYTHON) -m twine check --strict dist/* $(PYTHON) -c \ "from psutil import __version__ as ver; \ doc = open('docs/index.rst').read(); \ @@ -286,7 +286,7 @@ pre-release: ## Check if we're ready to produce a new release. assert 'XXXX' not in history, 'XXXX in HISTORY.rst';" release: ## Create a release (down/uploads tar.gz, wheels, git tag release). - $(PYTHON) -m twine check dist/* + $(PYTHON) -m twine check --strict dist/* $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI ${MAKE} git-tag-release diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index bd00cf2340..d96c6c5d36 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -48,11 +48,7 @@ def main(): data = f.read() data = re.sub(r".. raw:: html\n+\s+
", summary, data) data = re.sub(r"Sponsors\n========[\s\S]*?Example usages", funding, data) - if len(sys.argv) > 2: - with open(sys.argv[2], "wb") as f: - f.write(data.encode("utf8")) - else: - print(data) + print(data) if __name__ == '__main__': diff --git a/setup.py b/setup.py index 31777b9d2d..29039aad7b 100755 --- a/setup.py +++ b/setup.py @@ -118,18 +118,16 @@ def get_version(): py_limited_api = {} -def get_description(): +def get_long_description(): script = os.path.join(HERE, "scripts", "internal", "convert_readme.py") readme = os.path.join(HERE, 'README.rst') p = subprocess.Popen([sys.executable, script, readme], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) - data = stdout.decode('utf8') - if WINDOWS: - data = data.replace('\r\n', '\n') - return data + return stdout @contextlib.contextmanager @@ -388,7 +386,7 @@ def main(): version=VERSION, cmdclass=cmdclass, description=__doc__ .replace('\n', ' ').strip() if __doc__ else '', - long_description=get_description(), + long_description=get_long_description(), long_description_content_type='text/x-rst', keywords=[ 'ps', 'top', 'kill', 'free', 'lsof', 'netstat', 'nice', 'tty', From f65346fd57df6a23b5ab3ed661d425efe37cbd2a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 11 Nov 2022 17:52:35 +0100 Subject: [PATCH 0921/1714] use argparse module in 3 scripts/internal scripts --- scripts/internal/print_wheels.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_wheels.py index 5e5faccdd8..6b19909bc1 100755 --- a/scripts/internal/print_wheels.py +++ b/scripts/internal/print_wheels.py @@ -6,6 +6,7 @@ """Nicely print wheels print in dist/ directory.""" +import argparse import collections import glob import os @@ -67,8 +68,15 @@ def size(self): def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('dir', nargs="?", default="dist", + help='directory containing tar.gz or wheel files') + args = parser.parse_args() + + if not os.path.isdir(args.dir): + raise NotADirectoryError(args.dir) groups = collections.defaultdict(list) - for path in glob.glob('dist/*.whl'): + for path in glob.glob('%s/*.whl' % args.dir): wheel = Wheel(path) groups[wheel.platform()].append(wheel) From 3290ead1b20633b5f77f114bd9b33ad85e28f7fc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 11 Nov 2022 18:13:58 +0100 Subject: [PATCH 0922/1714] improve and rename print_wheels.py script Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 2 +- Makefile | 10 ++-- .../{print_wheels.py => print_dist.py} | 57 +++++++++++++------ 3 files changed, 47 insertions(+), 22 deletions(-) rename scripts/internal/{print_wheels.py => print_dist.py} (62%) diff --git a/MANIFEST.in b/MANIFEST.in index e19c7e2de2..13ed1c17fe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -141,10 +141,10 @@ include scripts/internal/git_pre_commit.py include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py +include scripts/internal/print_dist.py include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_timeline.py -include scripts/internal/print_wheels.py include scripts/internal/purge_installation.py include scripts/internal/winmake.py include scripts/iotop.py diff --git a/Makefile b/Makefile index 930325dd14..5088cb72a6 100644 --- a/Makefile +++ b/Makefile @@ -234,14 +234,14 @@ install-git-hooks: ## Install GIT pre-commit hook. download-wheels-github: ## Download latest wheels hosted on github. $(PYTHON) scripts/internal/download_wheels_github.py --tokenfile=~/.github.token - ${MAKE} print-wheels + ${MAKE} print-dist download-wheels-appveyor: ## Download latest wheels hosted on appveyor. $(PYTHON) scripts/internal/download_wheels_appveyor.py - ${MAKE} print-wheels + ${MAKE} print-dist -print-wheels: ## Print downloaded wheels - $(PYTHON) scripts/internal/print_wheels.py +print-dist: ## Print downloaded wheels / tar.gs + $(PYTHON) scripts/internal/print_dist.py # =================================================================== # Distribution @@ -275,7 +275,7 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} download-wheels-github ${MAKE} download-wheels-appveyor ${MAKE} print-hashes - ${MAKE} print-wheels + ${MAKE} print-dist $(PYTHON) -m twine check --strict dist/* $(PYTHON) -c \ "from psutil import __version__ as ver; \ diff --git a/scripts/internal/print_wheels.py b/scripts/internal/print_dist.py similarity index 62% rename from scripts/internal/print_wheels.py rename to scripts/internal/print_dist.py index 6b19909bc1..1e8e556266 100755 --- a/scripts/internal/print_wheels.py +++ b/scripts/internal/print_dist.py @@ -4,11 +4,10 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Nicely print wheels print in dist/ directory.""" +"""List and pretty print tarball & wheel files in the dist/ directory.""" import argparse import collections -import glob import os from psutil._common import bytes2human @@ -22,8 +21,9 @@ def __init__(self, path): self._name = os.path.basename(path) def __repr__(self): - return "" % ( - self.name, self.platform(), self.arch(), self.pyver()) + return "<%s(name=%s, plat=%s, arch=%s, pyver=%s)>" % ( + self.__class__.__name__, self.name, self.platform(), self.arch(), + self.pyver()) __str__ = __repr__ @@ -56,7 +56,11 @@ def platform(self): def arch(self): if self.name.endswith(('x86_64.whl', 'amd64.whl')): return '64' - return '32' + if self.name.endswith(("i686.whl", "win32.whl")): + return '32' + if self.name.endswith("arm64.whl"): + return 'arm64' + return '?' def pyver(self): pyver = 'pypy' if self.name.split('-')[3].startswith('pypy') else 'py' @@ -67,6 +71,18 @@ def size(self): return os.path.getsize(self._path) +class Tarball(Wheel): + + def platform(self): + return "source" + + def arch(self): + return "-" + + def pyver(self): + return "-" + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('dir', nargs="?", default="dist", @@ -76,28 +92,37 @@ def main(): if not os.path.isdir(args.dir): raise NotADirectoryError(args.dir) groups = collections.defaultdict(list) - for path in glob.glob('%s/*.whl' % args.dir): - wheel = Wheel(path) - groups[wheel.platform()].append(wheel) + + ls = sorted(os.listdir(args.dir), + key=lambda x: x.endswith("tar.gz")) + for name in ls: + path = os.path.join(args.dir, name) + if path.endswith(".whl"): + pkg = Wheel(path) + elif path.endswith(".tar.gz"): + pkg = Tarball(path) + else: + raise ValueError("invalid package %r", path) + groups[pkg.platform()].append(pkg) tot_files = 0 tot_size = 0 templ = "%-100s %7s %7s %7s" - for platf, wheels in groups.items(): - ppn = "%s (total = %s)" % (platf, len(wheels)) + for platf, pkgs in groups.items(): + ppn = "%s (%s)" % (platf, len(pkgs)) s = templ % (ppn, "size", "arch", "pyver") print_color('\n' + s, color=None, bold=True) - for wheel in sorted(wheels, key=lambda x: x.name): + for pkg in sorted(pkgs, key=lambda x: x.name): tot_files += 1 - tot_size += wheel.size() - s = templ % (wheel.name, bytes2human(wheel.size()), wheel.arch(), - wheel.pyver()) - if 'pypy' in wheel.pyver(): + tot_size += pkg.size() + s = templ % (" " + pkg.name, bytes2human(pkg.size()), pkg.arch(), + pkg.pyver()) + if 'pypy' in pkg.pyver(): print_color(s, color='violet') else: print_color(s, color='brown') - print_color("\ntotals: files=%s, size=%s" % ( + print_color("\n\ntotals: files=%s, size=%s" % ( tot_files, bytes2human(tot_size)), bold=True) From 5b3016ba3d7429c827c992699f50c2b7ff29b023 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Nov 2022 12:18:35 +0100 Subject: [PATCH 0923/1714] update build.yml Signed-off-by: Giampaolo Rodola --- .github/workflows/build.yml | 39 ++++++++++++++++++++-------------- scripts/internal/print_dist.py | 6 +----- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d9999f8b8..b99a1d6d07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-12, windows-2019] + # python: ["3.6", "3.11"] steps: - name: Cancel previous runs @@ -41,27 +42,26 @@ jobs: with: python-version: 3.11 - - name: Run tests + - name: Create wheels + run tests uses: pypa/cibuildwheel@v2.11.2 - - name: Create wheels + - name: Upload wheels uses: actions/upload-artifact@v3 with: name: wheels path: wheelhouse - - name: Print hashes + - name: Generate .tar.gz if: matrix.os == 'ubuntu-latest' run: | make generate-manifest python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - python scripts/internal/print_hashes.py wheelhouse/ # Windows cp37+ tests # psutil tests do not like running from a virtualenv with python>=3.7 so # not using cibuildwheel for those. run them "manually" with this job. - windows-py3-test: + py3-windows-tests: name: py3-windows-test-${{ matrix.python }}-${{ matrix.architecture }} needs: py3 runs-on: windows-2019 @@ -69,7 +69,7 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.7", "3.8", "3.9", "3.10", "~3.11.0-0"] + python: ["3.7", "3.8", "3.9", "3.10", "3.11"] architecture: ["x86", "x64"] steps: @@ -104,7 +104,7 @@ jobs: shell: bash # Linux + macOS + Python 2 - linux-macos-py2: + py2: name: py2-${{ matrix.os }} runs-on: ${{ matrix.os }} timeout-minutes: 20 @@ -130,24 +130,24 @@ jobs: with: python-version: 3.9 - - name: Run tests + - name: Create wheels + run tests uses: pypa/cibuildwheel@v1.12.0 - - name: Create wheels + - name: Upload wheels uses: actions/upload-artifact@v3 with: name: wheels path: wheelhouse - - name: Print hashes + - name: Generate .tar.gz if: matrix.os == 'ubuntu-latest' run: | make generate-manifest python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - python scripts/internal/print_hashes.py wheelhouse/ - freebsd: + # FreeBSD (tests only) + py3-freebsd: runs-on: macos-12 steps: - name: Cancel previous runs @@ -174,6 +174,7 @@ jobs: python3 psutil/tests/runner.py python3 psutil/tests/test_memleaks.py + # Run linters linters: runs-on: ubuntu-latest steps: @@ -193,13 +194,19 @@ jobs: # clinter find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python3 scripts/internal/clinter.py - check_dist: - name: Check dist - needs: [linux-macos-py2, py3] + # Check sanity of .tar.gz + wheel files + check-dist: + needs: [py2, py3] runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x - uses: actions/download-artifact@v3 with: name: wheels path: wheelhouse - - run: pipx run twine check --strict wheelhouse/* + - run: | + python scripts/internal/print_hashes.py wheelhouse/ + pipx run twine check --strict wheelhouse/* diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 1e8e556266..492bed1cac 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -89,12 +89,8 @@ def main(): help='directory containing tar.gz or wheel files') args = parser.parse_args() - if not os.path.isdir(args.dir): - raise NotADirectoryError(args.dir) groups = collections.defaultdict(list) - - ls = sorted(os.listdir(args.dir), - key=lambda x: x.endswith("tar.gz")) + ls = sorted(os.listdir(args.dir), key=lambda x: x.endswith("tar.gz")) for name in ls: path = os.path.join(args.dir, name) if path.endswith(".whl"): From f0a0d6d0771f5bd8b1df8e5a8d2b8991870533a5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 15 Dec 2022 21:42:36 +0100 Subject: [PATCH 0924/1714] write extensive test suite for @memoize decorator --- Makefile | 6 +-- psutil/_common.py | 9 ++++ psutil/tests/test_misc.py | 106 +++++++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 5088cb72a6..c27db46d68 100644 --- a/Makefile +++ b/Makefile @@ -121,7 +121,7 @@ setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). # Tests # =================================================================== -test: ## Run all tests. +test: ## Run all tests. To run a specific test do "make test ARGS=psutil.tests.test_system.TestDiskAPIs" ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) @@ -169,10 +169,6 @@ test-memleaks: ## Memory leak tests. ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memleaks.py -test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSystemAPIs - ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) - test-failed: ## Re-run tests which failed on last run ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --last-failed diff --git a/psutil/_common.py b/psutil/_common.py index 3414e8cad7..387d95945c 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -391,6 +391,15 @@ 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): diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index e22789c149..1ff0f52f2d 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -291,9 +291,108 @@ def test_sanity_version_check(self): # =================================================================== -class TestCommonModule(PsutilTestCase): +class TestMemoizeDecorator(PsutilTestCase): + + def setUp(self): + self.calls = [] + + tearDown = setUp + + def run_against(self, obj, expected_retval=None): + # no args + for x in range(2): + ret = obj() + self.assertEqual(self.calls, [((), {})]) + if expected_retval is not None: + self.assertEqual(ret, expected_retval) + # with args + for x 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 x 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_memoize(self): + 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""" @@ -328,6 +427,9 @@ def foo(*args, **kwargs): # docstring self.assertEqual(foo.__doc__, "foo docstring") + +class TestCommonModule(PsutilTestCase): + def test_memoize_when_activated(self): class Foo: From d86ac5536d147f4fdf60ae3e232b3c15387b0d88 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 27 Dec 2022 12:27:47 +0100 Subject: [PATCH 0925/1714] Fixed typo in debug message (#2187) - this should be referring to `sysctl`, like the statement below --- psutil/arch/osx/cpu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 2d94f31dd2..6e56471812 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -127,7 +127,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { return PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) - psutil_debug("sysct('hw.cpufrequency_min') failed (set to 0)"); + psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); From 54fb40c09bf84b0f3a26b59fbbedcd5811bfedc8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 Dec 2022 12:34:43 +0100 Subject: [PATCH 0926/1714] fix CI linters run + add @robusta-dev to list of supporters --- .github/workflows/build.yml | 5 ----- README.rst | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b99a1d6d07..e2d015c1de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -182,11 +182,6 @@ jobs: - uses: actions/setup-python@v4 - name: 'Run linters' run: | - # py2 - curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py - python2 get-pip.py - python2 -m pip install flake8 - python2 -m flake8 . # py3 python3 -m pip install flake8 isort python3 -m flake8 . diff --git a/README.rst b/README.rst index d1598ca3ef..35d252b8e5 100644 --- a/README.rst +++ b/README.rst @@ -143,6 +143,7 @@ Supporters +
add your avatar From 8a9b61bcd5ae307e9c402deeecb424b750fdcfd8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 Dec 2022 12:41:12 +0100 Subject: [PATCH 0927/1714] fix flake8 config --- .flake8 | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.flake8 b/.flake8 index 8be0bfcfd5..8347d048a6 100644 --- a/.flake8 +++ b/.flake8 @@ -3,18 +3,24 @@ [flake8] ignore = - W504 # line break after binary operator + # line break after binary operator + W504, # --- flake8-bugbear plugin - B007 # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. - B014 # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. - B008 # Do not perform function calls in argument defaults. + # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. + B007, + # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. + B014, + # Do not perform function calls in argument defaults. + B008, # --- flake8-blind-except plugin - B902 # blind except Exception: statement + # blind except Exception: statement + B902, # --- flake8-quotes plugin - Q000 # Double quotes found but single quotes preferred + # Double quotes found but single quotes preferred + Q000, # --- flake8-quotes naming; disable all except N804 and N805 N801, N802, N803, N806, N807, N811, N812, N813, N814, N815, N816, N817, N818 From e93a0f11eacebf58070705552c0e8c48fa5f1822 Mon Sep 17 00:00:00 2001 From: Po-Chuan Hsieh Date: Wed, 28 Dec 2022 04:05:49 +0800 Subject: [PATCH 0928/1714] Fix build with Clang 15 (#2186) Reference: https://github.com/freebsd/freebsd-ports/commit/4131f9bfd8d3adb98d096856ee08d2ac7756d7cd Co-authored-by: Dimitry Andric --- psutil/arch/freebsd/sys_socks.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 56c89724fb..48d3fb4f55 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -62,7 +62,7 @@ psutil_populate_xfiles(void) { struct xfile * -psutil_get_file_from_sock(void *sock) { +psutil_get_file_from_sock(kvaddr_t sock) { struct xfile *xf; int n; From 43a42d23e28275349b534f36cc830fd0837c69de Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 Dec 2022 21:07:36 +0100 Subject: [PATCH 0929/1714] give CREDITS to @sunpoet for #2186 Signed-off-by: Giampaolo Rodola --- CREDITS | 2 +- HISTORY.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 1990231593..0a9e09ae8e 100644 --- a/CREDITS +++ b/CREDITS @@ -691,7 +691,7 @@ I: 1616 N: Po-Chuan Hsieh W: https://github.com/sunpoet C: Taiwan -I: 1646 +I: 1646, 2186 N: Javad Karabi W: https://github.com/karabijavad diff --git a/HISTORY.rst b/HISTORY.rst index e0b7083714..f439e48ee8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,7 @@ ====================== - 2164_, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). +- 2186_, [FreeBSD]: compilation fails with Clang 15. (patch by Po-Chuan Hsieh) 5.9.4 ===== From b73d7ee952db293c6bee4397e13b2039a3d78d9d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 Dec 2022 21:17:33 +0100 Subject: [PATCH 0930/1714] update shields/badges Signed-off-by: Giampaolo Rodola --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 35d252b8e5..361b0f0182 100644 --- a/README.rst +++ b/README.rst @@ -18,11 +18,11 @@ :target: https://github.com/giampaolo/psutil/graphs/contributors :alt: Contributors -.. |github-actions| image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD +.. |github-actions| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml?label=Linux%2C%20macOS%2C%20FreeBSD%20tests :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild :alt: Linux, macOS, Windows tests -.. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows +.. |appveyor| image:: https://img.shields.io/appveyor/build/giampaolo/psutil/master.svg?maxAge=3600&label=Windows%20tests :target: https://ci.appveyor.com/project/giampaolo/psutil :alt: Windows tests (Appveyor) From c6b71453653609827f74aa074989ae4b9da69ece Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 30 Dec 2022 22:21:09 +0100 Subject: [PATCH 0931/1714] fix make test-coverage target --- Makefile | 2 +- psutil/tests/__init__.py | 3 ++- psutil/tests/test_testutils.py | 2 ++ pyproject.toml | 3 --- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index c27db46d68..4850292e4b 100644 --- a/Makefile +++ b/Makefile @@ -177,7 +177,7 @@ test-coverage: ## Run test coverage. ${MAKE} build # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov - $(TEST_PREFIX) $(PYTHON) -m coverage run $(TSCRIPT) + $(TEST_PREFIX) $(PYTHON) -m coverage run -m unittest -v $(PYTHON) -m coverage report @echo "writing results to htmlcov/index.html" $(PYTHON) -m coverage html diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ec9c7480a9..f66ff81356 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -87,7 +87,7 @@ "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", "MACOS_11PLUS", - "MACOS_12PLUS", + "MACOS_12PLUS", "COVERAGE", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', 'spawn_children_pair', @@ -128,6 +128,7 @@ 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 diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index dd98538c0a..0298ea4e30 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -27,6 +27,7 @@ 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 PsutilTestCase @@ -368,6 +369,7 @@ def test_param_err(self): @retry_on_failure() @unittest.skipIf(CI_TESTING, "skipped on CI") + @unittest.skipIf(COVERAGE, "skipped during test coverage") def test_leak_mem(self): ls = [] diff --git a/pyproject.toml b/pyproject.toml index 87b84fc677..7f3b43d573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,6 @@ force_single_line = true # one import per line lines_after_imports = 2 # blank spaces after import section [tool.coverage.report] -include = [ - "*psutil*" -] omit = [ "psutil/_compat.py", "psutil/tests/*", From 2da99502238852f18f74db05304e67a01ee77005 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 9 Jan 2023 15:30:38 +0100 Subject: [PATCH 0932/1714] #2191 / disk_partitions(): if all=True, do not unnecessarily list disk partitions Signed-off-by: Giampaolo Rodola --- psutil/_pslinux.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 9dc9643ab3..33ee1b146e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1257,16 +1257,17 @@ def disk_partitions(all=False): """Return mounted disk partitions as a list of namedtuples.""" fstypes = set() procfs_path = get_procfs_path() - 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") + 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'): From 2312057d267c85185e5f49eb86bb6f90bd9ab206 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 24 Jan 2023 14:45:21 +0100 Subject: [PATCH 0933/1714] disable flaky test + small Makefile refact --- .ci/README | 3 --- .ci/appveyor/README | 2 -- MANIFEST.in | 1 + Makefile | 15 ++++++++------ appveyor.yml | 2 +- psutil/tests/test_misc.py | 20 +++++++++---------- .../internal/appveyor_run_with_compiler.cmd | 0 7 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 .ci/README delete mode 100644 .ci/appveyor/README rename .ci/appveyor/run_with_compiler.cmd => scripts/internal/appveyor_run_with_compiler.cmd (100%) diff --git a/.ci/README b/.ci/README deleted file mode 100644 index 810fe63800..0000000000 --- a/.ci/README +++ /dev/null @@ -1,3 +0,0 @@ -This directory contains support scripts for Travis and Appveyor continuous -integration services. -Travis is used to run tests on Linux and macOS, Appveyor runs tests on Windows. diff --git a/.ci/appveyor/README b/.ci/appveyor/README deleted file mode 100644 index 2e092a07c8..0000000000 --- a/.ci/appveyor/README +++ /dev/null @@ -1,2 +0,0 @@ -This directory contains support files for appveyor, a continuous integration -service which runs tests on Windows on every push. diff --git a/MANIFEST.in b/MANIFEST.in index 13ed1c17fe..94b2d1202e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -129,6 +129,7 @@ include scripts/fans.py include scripts/free.py include scripts/ifconfig.py include scripts/internal/README +include scripts/internal/appveyor_run_with_compiler.cmd include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py diff --git a/Makefile b/Makefile index 4850292e4b..1816bd967a 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ARGS = TSCRIPT = psutil/tests/runner.py # Internal. -DEPS = \ +PY3_DEPS = \ autoflake \ autopep8 \ check-manifest \ @@ -34,8 +34,8 @@ PY2_DEPS = \ futures \ ipaddress \ mock -DEPS += `$(PYTHON) -c \ - "import sys; print('$(PY2_DEPS)' if sys.version_info[0] == 2 else '')"` +PY_DEPS = `$(PYTHON) -c \ + "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')"` # "python3 setup.py build" can be parallelized on Python >= 3.6. BUILD_OPTS = `$(PYTHON) -c \ "import sys, os; \ @@ -55,7 +55,8 @@ TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_DEBUG=1 # =================================================================== clean: ## Remove all build files. - @rm -rfv `find . -type d -name __pycache__ \ + @rm -rfv `find . \ + -type d -name __pycache__ \ -o -type f -name \*.bak \ -o -type f -name \*.orig \ -o -type f -name \*.pyc \ @@ -71,10 +72,12 @@ clean: ## Remove all build files. *\@psutil-* \ .coverage \ .failed-tests.txt \ + .pytest_cache \ build/ \ dist/ \ docs/_build/ \ - htmlcov/ + htmlcov/ \ + wheelhouse .PHONY: build build: ## Compile (in parallel) without installing. @@ -115,7 +118,7 @@ setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). ${MAKE} install-git-hooks ${MAKE} install-pip $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade --trusted-host files.pythonhosted.org pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade --trusted-host files.pythonhosted.org $(DEPS) + $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade --trusted-host files.pythonhosted.org $(PY_DEPS) # =================================================================== # Tests diff --git a/appveyor.yml b/appveyor.yml index 0752e610c9..70a4daec25 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,7 +13,7 @@ environment: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script interpreter # See: http://stackoverflow.com/a/13751649/163740 - WITH_COMPILER: "cmd /E:ON /V:ON /C .\\.ci\\appveyor\\run_with_compiler.cmd" + WITH_COMPILER: "cmd /E:ON /V:ON /C .\\scripts\\internal\\appveyor_run_with_compiler.cmd" PYTHONWARNINGS: always PYTHONUNBUFFERED: 1 PSUTIL_DEBUG: 1 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 1ff0f52f2d..44922216a2 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -45,10 +45,8 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import PYTHON_EXE -from psutil.tests import ROOT_DIR from psutil.tests import SCRIPTS_DIR from psutil.tests import PsutilTestCase -from psutil.tests import import_module_by_path from psutil.tests import mock from psutil.tests import reload_module from psutil.tests import sh @@ -250,15 +248,15 @@ def check(ret): 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__) + # # 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 diff --git a/.ci/appveyor/run_with_compiler.cmd b/scripts/internal/appveyor_run_with_compiler.cmd similarity index 100% rename from .ci/appveyor/run_with_compiler.cmd rename to scripts/internal/appveyor_run_with_compiler.cmd From 633d801919400b5c7524a21d19ae03ebed0f1929 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 24 Jan 2023 14:55:11 +0100 Subject: [PATCH 0934/1714] Clearer tracebacks in case of chained exceptions (#2196) In case of exception, display a cleaner error traceback by hiding the `KeyError` bit deriving from a missed cache hit. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 10 ++++++++++ psutil/_common.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f439e48ee8..b200cb3dd8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,8 +3,18 @@ 5.9.5 (IN DEVELOPMENT) ====================== +**Enhancements** + +- 2196_: in case of exception, display a cleaner error traceback by hiding the + `KeyError` bit deriving from a missed cache hit. + +**Bug fixes** + - 2164_, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). - 2186_, [FreeBSD]: compilation fails with Clang 15. (patch by Po-Chuan Hsieh) +- 2191_, [Linux]: `disk_partitions()`_: do not unnecessarily read + /proc/filesystems and raise `AccessDenied`_ unless user specified `all=False` + argument. 5.9.4 ===== diff --git a/psutil/_common.py b/psutil/_common.py index 387d95945c..da8a96e599 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -366,6 +366,20 @@ def __init__(self, seconds, pid=None, name=None): # =================================================================== +# 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: + __builtins__["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: @@ -407,7 +421,10 @@ def wrapper(*args, **kwargs): try: return cache[key] except KeyError: - ret = cache[key] = fun(*args, **kwargs) + try: + ret = cache[key] = fun(*args, **kwargs) + except Exception as err: + raise raise_from(err, None) return ret def cache_clear(): @@ -452,11 +469,17 @@ def wrapper(self): ret = self._cache[fun] except AttributeError: # case 2: we never entered oneshot() ctx - return fun(self) + try: + return fun(self) + except Exception as err: + raise raise_from(err, None) except KeyError: # case 3: we entered oneshot() ctx but there's no cache # for this entry yet - ret = fun(self) + try: + ret = fun(self) + except Exception as err: + raise raise_from(err, None) try: self._cache[fun] = ret except AttributeError: From bea3cf2d16899251b4b5f6b2609db9881645ea2d Mon Sep 17 00:00:00 2001 From: SnoopProject Date: Thu, 26 Jan 2023 18:50:30 +0300 Subject: [PATCH 0935/1714] Fix license: extra open quote. (#2197) The problem is that the open quote messes up the formatting of the entire document for other developers, which includes mentioning the author and his permission to freely use psutil. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 0bf4a7fc04..cff5eb74e1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola' +Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola All rights reserved. Redistribution and use in source and binary forms, with or without modification, From 56586c925d30dbac177fe756687f619c454c702c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 7 Mar 2023 18:30:55 +0100 Subject: [PATCH 0936/1714] fix exec() compatibility with PYPY (closes #2198) --- psutil/_common.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/psutil/_common.py b/psutil/_common.py index da8a96e599..90e76c6cc8 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -369,7 +369,12 @@ def __init__(self, seconds, pid=None, name=None): # 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: - __builtins__["exec"]("""def raise_from(value, from_value): + if isinstance(__builtins__, dict): # cpython + exec_ = __builtins__["exec"] + else: # pypy + exec_ = getattr(__builtins__, "exec") + + exec_("""def raise_from(value, from_value): try: raise value from from_value finally: From 1aa9bc838a7dbc96c40d9ffef712308205caa7e8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 7 Mar 2023 18:39:21 +0100 Subject: [PATCH 0937/1714] setup.py: on failure, give suggestion about PYPY installation --- setup.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 29039aad7b..65dad252ca 100755 --- a/setup.py +++ b/setup.py @@ -466,12 +466,14 @@ def main(): "develop")): py3 = "3" if PY3 else "" if LINUX: + pyimpl = "pypy" if PYPY else "python" if which('dpkg'): - missdeps("sudo apt-get install gcc python%s-dev" % py3) + missdeps("sudo apt-get install gcc %s%s-dev" % + (pyimpl, py3)) elif which('rpm'): - missdeps("sudo yum install gcc python%s-devel" % py3) + missdeps("sudo yum install gcc %s%s-devel" % (pyimpl, py3)) elif which('apk'): - missdeps("sudo apk add gcc python%s-dev" % py3) + missdeps("sudo apk add gcc %s%s-dev" % (pyimpl, py3)) elif MACOS: print(hilite("XCode (https://developer.apple.com/xcode/) " "is not installed"), color="red", file=sys.stderr) From dc4f7f50e859144c1092a6c225949c536072ea64 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Sat, 18 Mar 2023 13:29:15 +0100 Subject: [PATCH 0938/1714] CI: fix TestScripts tests failing due to ambiguous SCRIPTS_DIR (#2211) Tests are being run with psutil installed in a virtual environment within cibuildwheel builds. The scripts folder is not being found in this installation & thus, those tests are skipped. This commit allows to pass the path of the scripts folder through an environment variable & re-enables those tests. Signed-off-by: mayeut --- psutil/tests/__init__.py | 5 ++++- pyproject.toml | 13 ++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f66ff81356..61a06e2163 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -198,7 +198,10 @@ def macos_version(): ROOT_DIR = os.path.realpath( os.path.join(os.path.dirname(__file__), '..', '..')) -SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') +SCRIPTS_DIR = os.environ.get( + "PSUTIL_SCRIPTS_DIR", + os.path.join(ROOT_DIR, 'scripts') +) HERE = os.path.realpath(os.path.dirname(__file__)) # --- support diff --git a/pyproject.toml b/pyproject.toml index 7f3b43d573..435cc989cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,8 @@ build-backend = "setuptools.build_meta" skip = ["pp*", "*-musllinux*"] test-extras = "test" test-command = [ - "PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py", - "PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py" + "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/runner.py", + "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/test_memleaks.py" ] [tool.cibuildwheel.macos] @@ -54,12 +54,3 @@ archs = ["x86_64", "arm64"] # restrict build & tests to cp36 # cp36-abi3 wheels will need to be tested outside cibuildwheel for python>=3.7 build = "cp36-*" -test-command = [ - "python {project}/psutil/tests/runner.py", - "python {project}/psutil/tests/test_memleaks.py" -] - -[tool.cibuildwheel.windows.environment] -PYTHONWARNINGS = "always" -PYTHONUNBUFFERED = "1" -PSUTIL_DEBUG = "1" From fb0d5d3325efeb56dd0cf48c0f0d2379baca20d5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 18 Mar 2023 13:44:21 +0100 Subject: [PATCH 0939/1714] Makefile: set PSUTIL_SCRIPTS_DIR for 'test' target (re. to #2211) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1816bd967a..f5472bf466 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ BUILD_OPTS = `$(PYTHON) -c \ # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` -TEST_PREFIX = PYTHONWARNINGS=always PSUTIL_DEBUG=1 +TEST_PREFIX = PSUTIL_SCRIPTS_DIR=`pwd`/scripts PYTHONWARNINGS=always PSUTIL_DEBUG=1 # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help From d07e7f84579b05934bc525614177ec0e1a3b4dd4 Mon Sep 17 00:00:00 2001 From: Gerardwx Date: Sat, 18 Mar 2023 14:10:02 -0400 Subject: [PATCH 0940/1714] typo (#2055) Signed-off-by: Gerard Weatherby Co-authored-by: Gerard Weatherby --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index aa6b3f4bc2..dcc25ab04a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -505,7 +505,7 @@ Disks numbers will always be increasing or remain the same, but never decrease. ``disk_io_counters.cache_clear()`` can be used to invalidate the *nowrap* cache. - On Windows it may be ncessary to issue ``diskperf -y`` command from cmd.exe + On Windows it may be necessary to issue ``diskperf -y`` command from cmd.exe first in order to enable IO counters. On diskless machines this function will return ``None`` or ``{}`` if *perdisk* is ``True``. From 64b4318bce1a2ba8dcc0b8f7cbaefeb029c12419 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Mon, 27 Mar 2023 23:37:16 +0200 Subject: [PATCH 0941/1714] Win: fix running tests in a virtual environment (#2216) On windows, starting with python 3.7, virtual environments use a venvlauncher 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. connection check per pid This commit detects this situation and uses the base python executable to spawn processes when required. Signed-off-by: mayeut --- .github/workflows/build.yml | 60 +++++++--------------------------- psutil/tests/__init__.py | 35 ++++++++++++-------- psutil/tests/test_misc.py | 2 ++ psutil/tests/test_process.py | 7 ++-- psutil/tests/test_testutils.py | 4 ++- pyproject.toml | 6 ---- 6 files changed, 42 insertions(+), 72 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2d015c1de..eb6996daa0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,14 +22,21 @@ name: build jobs: # Linux + macOS + Windows Python 3 py3: - name: py3-${{ matrix.os }} + name: py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'windows') && matrix.archs || 'all' }} runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-12, windows-2019] - # python: ["3.6", "3.11"] + include: + - os: ubuntu-latest + archs: "x86_64 i686" + - os: macos-12 + archs: "x86_64 arm64" + - os: windows-2019 + archs: "AMD64" + - os: windows-2019 + archs: "x86" steps: - name: Cancel previous runs @@ -44,6 +51,8 @@ jobs: - name: Create wheels + run tests uses: pypa/cibuildwheel@v2.11.2 + env: + CIBW_ARCHS: "${{ matrix.archs }}" - name: Upload wheels uses: actions/upload-artifact@v3 @@ -58,51 +67,6 @@ jobs: python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - # Windows cp37+ tests - # psutil tests do not like running from a virtualenv with python>=3.7 so - # not using cibuildwheel for those. run them "manually" with this job. - py3-windows-tests: - name: py3-windows-test-${{ matrix.python }}-${{ matrix.architecture }} - needs: py3 - runs-on: windows-2019 - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - python: ["3.7", "3.8", "3.9", "3.10", "3.11"] - architecture: ["x86", "x64"] - - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v3 - - - uses: actions/setup-python@v4 - with: - python-version: "${{ matrix.python }}" - architecture: "${{ matrix.architecture }}" - - - name: Download wheels - uses: actions/download-artifact@v3 - with: - name: wheels - path: wheelhouse - - - name: Run tests - run: | - mkdir .tests - cd .tests - pip install $(find ../wheelhouse -name '*-cp36-abi3-${{ matrix.architecture == 'x86' && 'win32' || 'win_amd64'}}.whl')[test] - export PYTHONWARNINGS=always - export PYTHONUNBUFFERED=1 - export PSUTIL_DEBUG=1 - python ../psutil/tests/runner.py - python ../psutil/tests/test_memleaks.py - shell: bash - # Linux + macOS + Python 2 py2: name: py2-${{ matrix.os }} diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 61a06e2163..e3766eea1c 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -41,7 +41,6 @@ import psutil from psutil import AIX -from psutil import FREEBSD from psutil import LINUX from psutil import MACOS from psutil import POSIX @@ -80,8 +79,8 @@ __all__ = [ # constants 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', - 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', - 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', + '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", @@ -240,13 +239,21 @@ def attempt(exe): else: return exe - if GITHUB_ACTIONS: - if PYPY: - return which("pypy3") if PY3 else which("pypy") - elif FREEBSD: - return os.path.realpath(sys.executable) - else: - return which('python') + 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 \ @@ -255,14 +262,14 @@ def attempt(exe): attempt(psutil.Process().exe()) if not exe: raise ValueError("can't find python exe real abspath") - return exe + return exe, env else: exe = os.path.realpath(sys.executable) assert os.path.exists(exe), exe - return exe + return exe, env -PYTHON_EXE = _get_py_exe() +PYTHON_EXE, PYTHON_EXE_ENV = _get_py_exe() DEVNULL = open(os.devnull, 'r+') atexit.register(DEVNULL.close) @@ -351,7 +358,7 @@ def spawn_testproc(cmd=None, **kwds): kwds.setdefault("stdin", DEVNULL) kwds.setdefault("stdout", DEVNULL) kwds.setdefault("cwd", os.getcwd()) - kwds.setdefault("env", os.environ) + 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 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 44922216a2..afa60b1c31 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -45,6 +45,7 @@ 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 @@ -820,6 +821,7 @@ class TestScripts(PsutilTestCase): @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: diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 26869e9838..df6a84c39f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -52,6 +52,7 @@ 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 @@ -1543,7 +1544,7 @@ def test_misc(self): # 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: + stderr=subprocess.PIPE, env=PYTHON_EXE_ENV) as proc: proc.name() proc.cpu_times() proc.stdin @@ -1559,7 +1560,7 @@ def test_ctx_manager(self): with psutil.Popen([PYTHON_EXE, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - stdin=subprocess.PIPE) as proc: + stdin=subprocess.PIPE, env=PYTHON_EXE_ENV) as proc: proc.communicate() assert proc.stdout.closed assert proc.stderr.closed @@ -1572,7 +1573,7 @@ def test_kill_terminate(self): # diverges from that. cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] with psutil.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: + stderr=subprocess.PIPE, env=PYTHON_EXE_ENV) as proc: proc.terminate() proc.wait() self.assertRaises(psutil.NoSuchProcess, proc.terminate) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 0298ea4e30..e757e017aa 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -30,6 +30,7 @@ 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 @@ -260,7 +261,8 @@ def test_terminate(self): terminate(p) # by psutil.Popen cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] - p = psutil.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = psutil.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV) terminate(p) self.assertProcessGone(p) terminate(p) diff --git a/pyproject.toml b/pyproject.toml index 435cc989cd..c8cb62af01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,3 @@ test-command = [ [tool.cibuildwheel.macos] archs = ["x86_64", "arm64"] - -[tool.cibuildwheel.windows] -# psutil tests do not like running from a virtualenv with python>=3.7 -# restrict build & tests to cp36 -# cp36-abi3 wheels will need to be tested outside cibuildwheel for python>=3.7 -build = "cp36-*" From f2216559ffa3e48dfccda331f8951a906a32c771 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Mar 2023 23:44:46 +0200 Subject: [PATCH 0942/1714] When raising warning, always use stacklevel=2 B028: No explicit stacklevel argument found. The warn method from the warnings module uses a stacklevel of 1 by default. This will only show a stack trace for the line on which the warn method is called. It is therefore recommended to use a stacklevel of 2 or greater to provide more information to the user. Signed-off-by: Giampaolo Rodola --- psutil/_common.py | 2 +- psutil/_pslinux.py | 6 +++--- psutil/tests/__init__.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 90e76c6cc8..74ae2557c3 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -372,7 +372,7 @@ def __init__(self, seconds, pid=None, name=None): if isinstance(__builtins__, dict): # cpython exec_ = __builtins__["exec"] else: # pypy - exec_ = getattr(__builtins__, "exec") + exec_ = getattr(__builtins__, "exec") # noqa exec_("""def raise_from(value, from_value): try: diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 33ee1b146e..11e62944a1 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -510,7 +510,7 @@ def virtual_memory(): 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) + warnings.warn(msg, RuntimeWarning, stacklevel=2) return svmem(total, avail, percent, used, free, active, inactive, buffers, cached, shared, slab) @@ -544,7 +544,7 @@ def swap_memory(): # 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) + warnings.warn(msg, RuntimeWarning, stacklevel=2) sin = sout = 0 else: with f: @@ -564,7 +564,7 @@ def swap_memory(): # 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) + warnings.warn(msg, RuntimeWarning, stacklevel=2) sin = sout = 0 return _common.sswap(total, used, free, percent, sin, sout) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index e3766eea1c..cdc8b671d4 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -248,7 +248,7 @@ def attempt(exe): # 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 + # 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 @@ -1737,7 +1737,7 @@ def import_module_by_path(path): def warn(msg): """Raise a warning msg.""" - warnings.warn(msg, UserWarning) + warnings.warn(msg, UserWarning, stacklevel=2) def is_namedtuple(x): From d57aeaab1743be8ce0485e620b27282fd172ccd4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Mar 2023 23:49:33 +0200 Subject: [PATCH 0943/1714] update HISTORY/CREDITS for #2216 / @mayeut --- CREDITS | 2 +- HISTORY.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 0a9e09ae8e..f414636087 100644 --- a/CREDITS +++ b/CREDITS @@ -781,7 +781,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039, 2142, 2147, 2153, 2040, 2102 +I: 2039, 2142, 2147, 2153, 2040, 2102, 2216 N: Hugo van Kemenade W: https://github.com/hugovk diff --git a/HISTORY.rst b/HISTORY.rst index b200cb3dd8..962604cf44 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,8 @@ - 2191_, [Linux]: `disk_partitions()`_: do not unnecessarily read /proc/filesystems and raise `AccessDenied`_ unless user specified `all=False` argument. +- 2216_, [Windows]: fix tests when running in a virtual environment (patch by + Matthieu Darbois) 5.9.4 ===== From 32e8190365667855a86c7f2d980eed9a28fbea24 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Mar 2023 11:07:46 +0200 Subject: [PATCH 0944/1714] update HISTORY Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 962604cf44..1e259cbb7f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ - 2196_: in case of exception, display a cleaner error traceback by hiding the `KeyError` bit deriving from a missed cache hit. +- 2217_: print the full traceback when a `DeprecationWarning` or `UserWarning` + is raised. **Bug fixes** From cbb5ca34d719044f7ffb35ee181dbdfdbc78cb2b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Mar 2023 19:29:24 +0200 Subject: [PATCH 0945/1714] fix linux tests --- psutil/tests/test_linux.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 3e1afc4fb6..b8be6ca3b0 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -352,7 +352,6 @@ def test_warnings_on_misses(self): 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)) @@ -586,7 +585,6 @@ def test_missing_sin_sout(self): 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)) @@ -604,7 +602,6 @@ def test_no_vmstat_mocked(self): 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", From c81248b02928f12e84e2bd3cdf2da29d15b5f5b8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Mar 2023 00:02:18 +0200 Subject: [PATCH 0946/1714] fix C compilation warning on Linux + Python 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit psutil/_psutil_linux.c: In function ‘psutil_proc_cpu_affinity_set’: psutil/_psutil_linux.c:317:80: warning: unknown conversion type character ‘R’ in format [-Wformat=] 317 | return PyErr_Format(PyExc_TypeError, "sequence argument expected, got %R", Py_TYPE(py_cpu_set)); | ^ psutil/_psutil_linux.c:317:46: warning: too many arguments for format [-Wformat-extra-args] 317 | return PyErr_Format(PyExc_TypeError, "sequence argument expected, got %R", Py_TYPE(py_cpu_set)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ creating build/lib.linux-x86_64-2.7 --- psutil/_psutil_linux.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index f04fe776ee..924fe7027e 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -314,7 +314,14 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { return NULL; if (!PySequence_Check(py_cpu_set)) { - return PyErr_Format(PyExc_TypeError, "sequence argument expected, got %R", Py_TYPE(py_cpu_set)); + return PyErr_Format( + PyExc_TypeError, +#if PY_MAJOR_VERSION >= 3 + "sequence argument expected, got %R", Py_TYPE(py_cpu_set) +#else + "sequence argument expected, got %s", Py_TYPE(py_cpu_set)->tp_name +#endif + ); } seq_len = PySequence_Size(py_cpu_set); From 534dace8278f983a1704bb98b75d85cbbf73e5a5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Mar 2023 09:25:07 +0200 Subject: [PATCH 0947/1714] [Linux] guess `available` virtual_memory() if kernel says it's 0 (#2052) Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 4 ++ psutil/_pslinux.py | 71 ++++++++++++++------------ psutil/tests/test_linux.py | 100 ++++++++++++++++++++++++------------- 3 files changed, 107 insertions(+), 68 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1e259cbb7f..a86f56c429 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,10 @@ **Bug fixes** +- 1915_, [Linux]: on certain kernels, ``"MemAvailable"`` field from + ``/proc/meminfo`` returns ``0`` (possibly a kernel bug), in which case we + calculate an approximation for ``available`` memory which matches "free" + CLI utility. - 2164_, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). - 2186_, [FreeBSD]: compilation fails with Clang 15. (patch by Po-Chuan Hsieh) - 2191_, [Linux]: `disk_partitions()`_: do not unnecessarily read diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 11e62944a1..1bdeabfebd 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -344,34 +344,43 @@ class StructRlimit(ctypes.Structure): def calculate_avail_vmem(mems): """Fallback for kernels < 3.14 where /proc/meminfo does not provide - "MemAvailable:" column, see: + "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("%r 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()) @@ -396,19 +405,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 = {} @@ -490,17 +491,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) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index b8be6ca3b0..964416b350 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -250,12 +250,64 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") -class TestSystemVirtualMemory(PsutilTestCase): +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): - # 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( @@ -269,12 +321,10 @@ def test_used(self): # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e if get_free_version_info() < (3, 3, 12): raise self.skipTest("old free version") - free = free_physmem() - free_value = free.used + vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( - free_value, psutil_value, delta=TOLERANCE_SYS_MEM, - msg='%s %s \n%s' % (free_value, psutil_value, free.output)) + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) @retry_on_failure() def test_free(self): @@ -304,31 +354,9 @@ def test_inactive(self): self.assertAlmostEqual( vmstat_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 TestSystemVirtualMemoryMocks(PsutilTestCase): def test_warnings_on_misses(self): # Emulate a case where /proc/meminfo provides few info. @@ -808,7 +836,7 @@ def open_mock(name, *args, **kwargs): name.startswith("/sys/devices/system/cpu/cpufreq/policy")): return io.BytesIO(b"700000") elif name == '/proc/cpuinfo': - return io.BytesIO(b"cpu MHz : 500") + return io.BytesIO(b"cpu MHz : 500") else: return orig_open(name, *args, **kwargs) @@ -849,8 +877,8 @@ def open_mock(name, *args, **kwargs): n.startswith("/sys/devices/system/cpu/cpufreq/policy1")): return io.BytesIO(b"600000") elif name == '/proc/cpuinfo': - return io.BytesIO(b"cpu MHz : 100\n" - b"cpu MHz : 400") + return io.BytesIO(b"cpu MHz : 100\n" + b"cpu MHz : 400") else: return orig_open(name, *args, **kwargs) @@ -881,7 +909,7 @@ def open_mock(name, *args, **kwargs): elif name.endswith('/cpuinfo_cur_freq'): return io.BytesIO(b"200000") elif name == '/proc/cpuinfo': - return io.BytesIO(b"cpu MHz : 200") + return io.BytesIO(b"cpu MHz : 200") else: return orig_open(name, *args, **kwargs) From 6b8700270e2c32e876dfca081dbbec832aa4e7a0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 31 Mar 2023 18:47:59 +0200 Subject: [PATCH 0948/1714] Makefile: define num CPUs for parallel workers Signed-off-by: Giampaolo Rodola --- Makefile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f5472bf466..7bbb06930a 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,7 @@ PY2_DEPS = \ mock PY_DEPS = `$(PYTHON) -c \ "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')"` +NUM_WORKERS = `$(PYTHON) -c "import os; print(os.cpu_count() or 1)"` # "python3 setup.py build" can be parallelized on Python >= 3.6. BUILD_OPTS = `$(PYTHON) -c \ "import sys, os; \ @@ -191,10 +192,10 @@ test-coverage: ## Run test coverage. # =================================================================== flake8: ## Run flake8 linter. - @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 + @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 --jobs=${NUM_WORKERS} isort: ## Run isort linter. - @git ls-files '*.py' | xargs $(PYTHON) -m isort --check-only + @git ls-files '*.py' | xargs $(PYTHON) -m isort --check-only --jobs=${NUM_WORKERS} c-linter: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py @@ -209,11 +210,11 @@ lint-all: ## Run all linters # =================================================================== fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. - @git ls-files '*.py' | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 - @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --jobs 0 --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys + @git ls-files '*.py' | xargs $(PYTHON) -m autopep8 --in-place --jobs=${NUM_WORKERS} --global-config=.flake8 + @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --jobs=${NUM_WORKERS} --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys fix-imports: ## Fix imports with isort. - @git ls-files '*.py' | xargs $(PYTHON) -m isort + @git ls-files '*.py' | xargs $(PYTHON) -m isort --jobs=${NUM_WORKERS} fix-all: ## Run all code fixers. ${MAKE} fix-flake8 From 7eadee31db2f038763a3a6f978db1ea76bbc4674 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Apr 2023 12:24:59 +0200 Subject: [PATCH 0949/1714] Fix pylint warnings / cleanup (#2218) --- .github/workflows/issues.py | 4 +-- Makefile | 15 +++++--- psutil/__init__.py | 2 +- psutil/_common.py | 4 +-- psutil/_compat.py | 4 +-- psutil/_psaix.py | 4 +-- psutil/_pslinux.py | 6 ++-- psutil/_pssunos.py | 2 +- psutil/_pswindows.py | 2 +- psutil/tests/__init__.py | 23 +++++------- psutil/tests/test_bsd.py | 2 +- psutil/tests/test_connections.py | 2 +- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 18 +++++----- psutil/tests/test_misc.py | 20 +++++------ psutil/tests/test_osx.py | 37 ------------------- psutil/tests/test_posix.py | 5 +-- psutil/tests/test_process.py | 9 +++-- psutil/tests/test_system.py | 18 +++++----- psutil/tests/test_testutils.py | 10 +++--- psutil/tests/test_unicode.py | 3 +- pyproject.toml | 38 ++++++++++++++++++++ scripts/internal/download_wheels_appveyor.py | 4 +-- scripts/internal/git_pre_commit.py | 20 +++++------ scripts/internal/print_announce.py | 2 +- scripts/internal/print_dist.py | 2 +- scripts/internal/winmake.py | 22 +++++++----- scripts/iotop.py | 2 +- scripts/procinfo.py | 2 +- setup.py | 5 +-- 30 files changed, 146 insertions(+), 143 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 2aea40bed9..06438b97ff 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -222,7 +222,7 @@ def should_add(issue, label): issue.add_to_labels(label) -def _guess_labels_from_text(issue, text): +def _guess_labels_from_text(text): assert isinstance(text, str), text for label, keywords in LABELS_MAP.items(): for keyword in keywords: @@ -232,7 +232,7 @@ def _guess_labels_from_text(issue, text): def add_labels_from_text(issue, text): assert isinstance(text, str), text - for label, keyword in _guess_labels_from_text(issue, text): + for label, keyword in _guess_labels_from_text(text): add_label(issue, label) diff --git a/Makefile b/Makefile index f5472bf466..27bba26618 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ PY3_DEPS = \ flake8-quotes \ isort \ pep8-naming \ + pylint \ pyperf \ pypinfo \ requests \ @@ -36,6 +37,7 @@ PY2_DEPS = \ mock PY_DEPS = `$(PYTHON) -c \ "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')"` +NUM_WORKERS = `$(PYTHON) -c "import os; print(os.cpu_count() or 1)"` # "python3 setup.py build" can be parallelized on Python >= 3.6. BUILD_OPTS = `$(PYTHON) -c \ "import sys, os; \ @@ -191,10 +193,13 @@ test-coverage: ## Run test coverage. # =================================================================== flake8: ## Run flake8 linter. - @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 + @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 --jobs=${NUM_WORKERS} isort: ## Run isort linter. - @git ls-files '*.py' | xargs $(PYTHON) -m isort --check-only + @git ls-files '*.py' | xargs $(PYTHON) -m isort --check-only --jobs=${NUM_WORKERS} + +pylint: ## Python pylint (not mandatory, just run it from time to time) + @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} c-linter: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py @@ -209,11 +214,11 @@ lint-all: ## Run all linters # =================================================================== fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. - @git ls-files '*.py' | xargs $(PYTHON) -m autopep8 --in-place --jobs 0 --global-config=.flake8 - @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --jobs 0 --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys + @git ls-files '*.py' | xargs $(PYTHON) -m autopep8 --in-place --jobs=${NUM_WORKERS} --global-config=.flake8 + @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --jobs=${NUM_WORKERS} --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys fix-imports: ## Fix imports with isort. - @git ls-files '*.py' | xargs $(PYTHON) -m isort + @git ls-files '*.py' | xargs $(PYTHON) -m isort --jobs=${NUM_WORKERS} fix-all: ## Run all code fixers. ${MAKE} fix-flake8 diff --git a/psutil/__init__.py b/psutil/__init__.py index deaf02eaf2..6036cbe945 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -516,7 +516,7 @@ def as_dict(self, attrs=None, ad_value=None): "s" if len(invalid_names) > 1 else "", ", ".join(map(repr, invalid_names)))) - retdict = dict() + retdict = {} ls = attrs or valid_names with self.oneshot(): for name in ls: diff --git a/psutil/_common.py b/psutil/_common.py index 74ae2557c3..9228674648 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -43,7 +43,7 @@ # 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', 0)) +PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG')) _DEFAULT = object() __all__ = [ @@ -924,7 +924,7 @@ def debug(msg): """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" if PSUTIL_DEBUG: import inspect - fname, lineno, func_name, lines, index = inspect.getframeinfo( + fname, lineno, _, lines, index = inspect.getframeinfo( inspect.currentframe().f_back) if isinstance(msg, Exception): if isinstance(msg, (OSError, IOError, EnvironmentError)): diff --git a/psutil/_compat.py b/psutil/_compat.py index 52e762b19e..2531cf4b67 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -254,7 +254,7 @@ def lru_cache(maxsize=100, typed=False): 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 @@ -432,7 +432,7 @@ def get_terminal_size(fallback=(80, 24)): try: from subprocess import TimeoutExpired as SubprocessTimeoutExpired except ImportError: - class SubprocessTimeoutExpired: + class SubprocessTimeoutExpired(Exception): pass diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 2391478c6e..5c41069cff 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -465,8 +465,8 @@ 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): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1bdeabfebd..d178fe1638 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -327,8 +327,8 @@ class StructRlimit(ctypes.Structure): pid, resource_, ctypes.byref(new), ctypes.byref(current)) if ret != 0: - errno = ctypes.get_errno() - raise OSError(errno, os.strerror(errno)) + errno_ = ctypes.get_errno() + raise OSError(errno_, os.strerror(errno_)) return (current.rlim_cur, current.rlim_max) @@ -1837,7 +1837,7 @@ def io_counters(self): ) except KeyError as err: raise ValueError("%r field was not found in %s; found fields " - "are %r" % (err[0], fname, fields)) + "are %r" % (err.args[0], fname, fields)) @wrap_exceptions def cpu_times(self): diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 541c1aa4a2..8663de3cd3 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -143,7 +143,7 @@ def swap_memory(): p = subprocess.Popen(['/usr/bin/env', 'PATH=/usr/sbin:/sbin:%s' % os.environ['PATH'], 'swap', '-l'], stdout=subprocess.PIPE) - stdout, stderr = p.communicate() + stdout, _ = p.communicate() if PY3: stdout = stdout.decode(sys.stdout.encoding) if p.returncode != 0: diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index b546f15d83..3802f3edb0 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -699,7 +699,7 @@ def retry_error_partial_copy(fun): def wrapper(self, *args, **kwargs): delay = 0.0001 times = 33 - for x in range(times): # retries for roughly 1 second + for _ in range(times): # retries for roughly 1 second try: return fun(self, *args, **kwargs) except WindowsError as _: diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index cdc8b671d4..e82bb38d8c 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -85,22 +85,19 @@ "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", "MACOS_11PLUS", + "HAS_SENSORS_TEMPERATURES", "MACOS_11PLUS", "MACOS_12PLUS", "COVERAGE", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', 'spawn_children_pair', # threads - 'ThreadTask' + 'ThreadTask', # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', 'process_namespace', 'system_namespace', 'print_sysinfo', - # install utils - 'install_pip', 'install_test_deps', # fs utils - 'chdir', 'safe_rmpath', 'create_exe', 'decode_path', 'encode_path', - 'get_testfn', + 'chdir', 'safe_rmpath', 'create_exe', 'get_testfn', # os 'get_winver', 'kernel_version', # sync primitives @@ -458,7 +455,7 @@ def spawn_zombie(): zpid = int(conn.recv(1024)) _pids_started.add(zpid) zombie = psutil.Process(zpid) - call_until(lambda: zombie.status(), "ret == psutil.STATUS_ZOMBIE") + call_until(zombie.status, "ret == psutil.STATUS_ZOMBIE") return (parent, zombie) finally: conn.close() @@ -628,7 +625,7 @@ def reap_children(recursive=False): if children: for p in children: terminate(p, wait_timeout=None) - gone, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) + _, 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) @@ -999,7 +996,7 @@ def test_fun(self): retries = 10 if CI_TESTING else 5 verbose = True _thisproc = psutil.Process() - _psutil_debug_orig = bool(os.getenv('PSUTIL_DEBUG', 0)) + _psutil_debug_orig = bool(os.getenv('PSUTIL_DEBUG')) @classmethod def setUpClass(cls): @@ -1059,7 +1056,7 @@ def _call_ntimes(self, fun, times): diff = mem2 - mem1 # can also be negative return diff - def _check_mem(self, fun, times, warmup_times, retries, tolerance): + def _check_mem(self, fun, times, retries, tolerance): messages = [] prev_mem = 0 increase = times @@ -1104,8 +1101,7 @@ def execute(self, fun, times=None, warmup_times=None, retries=None, self._call_ntimes(fun, warmup_times) # warm up self._check_fds(fun) - self._check_mem(fun, times=times, warmup_times=warmup_times, - retries=retries, tolerance=tolerance) + 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 @@ -1122,7 +1118,6 @@ def print_sysinfo(): import datetime import getpass import locale - import platform import pprint try: import pip @@ -1616,7 +1611,7 @@ def check_net_address(addr, family): 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) + raise ValueError("unknown family %r" % family) def check_connection_ntuple(conn): diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index e541547d97..29ea384d51 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -36,7 +36,7 @@ PAGESIZE = getpagesize() # muse requires root privileges - MUSE_AVAILABLE = True if os.getuid() == 0 and which('muse') else False + MUSE_AVAILABLE = os.getuid() == 0 and which('muse') else: PAGESIZE = None MUSE_AVAILABLE = False diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index f3b1f837ce..d47233bc80 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -501,7 +501,7 @@ def test_multi_sockets_procs(self): pids = [] times = 10 fnames = [] - for i in range(times): + for _ in range(times): fname = self.get_testfn() fnames.append(fname) src = textwrap.dedent("""\ diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 3b806ee376..b767e3ebf1 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -176,7 +176,7 @@ def test_rlimit(self): def test_io_counters(self): hasit = hasattr(psutil.Process, "io_counters") - self.assertEqual(hasit, False if MACOS or SUNOS else True) + self.assertEqual(hasit, not (MACOS or SUNOS)) def test_num_fds(self): self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 964416b350..e1de6706aa 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -749,7 +749,7 @@ def test_emulate_fallbacks(self): # 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 + assert m.called @unittest.skipIf(not LINUX, "LINUX only") @@ -817,8 +817,8 @@ def path_exists_mock(path): 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) + self.assertEqual(freq.max, 0.0) + self.assertEqual(freq.min, 0.0) finally: reload_module(psutil._pslinux) reload_module(psutil) @@ -1057,7 +1057,7 @@ class TestSystemNetIOCounters(PsutilTestCase): def test_against_ifconfig(self): def ifconfig(nic): ret = {} - out = sh("ifconfig %s" % name) + out = sh("ifconfig %s" % nic) ret['packets_recv'] = int( re.findall(r'RX packets[: ](\d+)', out)[0]) ret['packets_sent'] = int( @@ -1150,7 +1150,7 @@ def df(path): for part in psutil.disk_partitions(all=False): usage = psutil.disk_usage(part.mountpoint) - dev, total, used, free = df(part.mountpoint) + _, total, used, free = df(part.mountpoint) self.assertEqual(usage.total, total) self.assertAlmostEqual(usage.free, free, delta=TOLERANCE_DISK_USAGE) @@ -2034,7 +2034,7 @@ def test_threads_mocked(self): # 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): + def open_mock_1(name, *args, **kwargs): if name.startswith('/proc/%s/task' % os.getpid()): raise IOError(errno.ENOENT, "") else: @@ -2042,20 +2042,20 @@ def open_mock(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 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(name, *args, **kwargs): + 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): + with mock.patch(patch_point, side_effect=open_mock_2): self.assertRaises(psutil.AccessDenied, psutil.Process().threads) def test_exe_mocked(self): diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index afa60b1c31..a7b5d02d5f 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -299,19 +299,19 @@ def setUp(self): def run_against(self, obj, expected_retval=None): # no args - for x in range(2): + for _ in range(2): ret = obj() self.assertEqual(self.calls, [((), {})]) if expected_retval is not None: self.assertEqual(ret, expected_retval) # with args - for x in range(2): + 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 x in range(2): + for _ in range(2): ret = obj(1, bar=2) self.assertEqual( self.calls, [((), {}), ((1, ), {}), ((1, ), {'bar': 2})]) @@ -400,19 +400,19 @@ def foo(*args, **kwargs): calls = [] # no args - for x in range(2): + for _ in range(2): ret = foo() expected = ((), {}) self.assertEqual(ret, expected) self.assertEqual(len(calls), 1) # with args - for x in range(2): + for _ 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): + for _ in range(2): ret = foo(1, bar=2) expected = ((1, ), {'bar': 2}) self.assertEqual(ret, expected) @@ -732,7 +732,7 @@ def test_cache_wrap(self): {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100}}) self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}}) - def assert_(): + def check_cache_info(): cache = wrap_numbers.cache_info() self.assertEqual( cache[1], @@ -746,14 +746,14 @@ def assert_(): wrap_numbers(input, 'disk_io') cache = wrap_numbers.cache_info() self.assertEqual(cache[0], {'disk_io': input}) - assert_() + 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}) - assert_() + check_cache_info() # then it wraps again input = {'disk1': nt(100, 100, 20)} @@ -837,7 +837,7 @@ def assert_stdout(exe, *args, **kwargs): return out @staticmethod - def assert_syntax(exe, args=None): + def assert_syntax(exe): exe = os.path.join(SCRIPTS_DIR, exe) if PY3: f = open(exe, 'rt', encoding='utf8') diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index af12648414..dc0dd6baaa 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -51,33 +51,6 @@ def vm_stat(field): return int(re.search(r'\d+', line).group(0)) * getpagesize() -# 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(PsutilTestCase): @@ -200,16 +173,6 @@ def test_swapmem_sout(self): psutil_val = psutil.swap_memory().sout self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) - # 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): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index d87322300f..0dc1dfda29 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -63,6 +63,8 @@ def ps(fmt, pid=None): cmd.append('ax') if SUNOS: + # XXX: set() has not get() method so this cannot work; not sure + # what I meant in here. fmt_map = set(('command', 'comm', 'start', 'stime')) fmt = fmt_map.get(fmt, fmt) @@ -408,8 +410,7 @@ def df(device): "raw devices not supported" in err or \ "permission denied" in err: continue - else: - raise + raise else: self.assertAlmostEqual(usage.total, total, delta=tolerance) self.assertAlmostEqual(usage.used, used, delta=tolerance) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index df6a84c39f..ec15ffda53 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -231,7 +231,7 @@ 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): + for _ in range(100): percent = p.cpu_percent(interval=None) self.assertIsInstance(percent, float) self.assertGreaterEqual(percent, 0.0) @@ -611,8 +611,7 @@ def test_memory_full_info(self): 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))) + self.assertEqual(len(maps), len(set(maps))) ext_maps = p.memory_maps(grouped=False) for nt in maps: @@ -1035,7 +1034,7 @@ def test_num_fds(self): def test_num_ctx_switches(self): p = psutil.Process() before = sum(p.num_ctx_switches()) - for x in range(500000): + for _ in range(500000): after = sum(p.num_ctx_switches()) if after > before: return @@ -1147,7 +1146,7 @@ def test_parents_and_children(self): def test_suspend_resume(self): p = self.spawn_psproc() p.suspend() - for x in range(100): + for _ in range(100): if p.status() == psutil.STATUS_STOPPED: break time.sleep(0.01) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 1722b515de..c2b1df8473 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -123,7 +123,7 @@ def callback(p): self.assertFalse(hasattr(p, 'returncode')) @retry_on_failure(30) - def test(procs, callback): + def test_1(procs, callback): gone, alive = psutil.wait_procs(procs, timeout=0.03, callback=callback) self.assertEqual(len(gone), 1) @@ -131,7 +131,7 @@ def test(procs, callback): return gone, alive sproc3.terminate() - gone, alive = test(procs, callback) + 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) @@ -142,7 +142,7 @@ def test(procs, callback): self.assertFalse(hasattr(p, 'returncode')) @retry_on_failure(30) - def test(procs, callback): + def test_2(procs, callback): gone, alive = psutil.wait_procs(procs, timeout=0.03, callback=callback) self.assertEqual(len(gone), 3) @@ -151,7 +151,7 @@ def test(procs, callback): sproc1.terminate() sproc2.terminate() - gone, alive = test(procs, callback) + 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')) @@ -165,7 +165,7 @@ def test_wait_procs_no_timeout(self): procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] for p in procs: p.terminate() - gone, alive = psutil.wait_procs(procs) + psutil.wait_procs(procs) def test_pid_exists(self): sproc = self.spawn_testproc() @@ -449,7 +449,7 @@ def _test_cpu_percent(self, percent, last_ret, new_ret): def test_cpu_percent(self): last = psutil.cpu_percent(interval=0.001) - for x in range(100): + for _ in range(100): new = psutil.cpu_percent(interval=None) self._test_cpu_percent(new, last, new) last = new @@ -459,7 +459,7 @@ def test_cpu_percent(self): 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): + for _ in range(100): new = psutil.cpu_percent(interval=None, percpu=True) for percent in new: self._test_cpu_percent(percent, last, new) @@ -469,7 +469,7 @@ def test_per_cpu_percent(self): def test_cpu_times_percent(self): last = psutil.cpu_times_percent(interval=0.001) - for x in range(100): + for _ in range(100): new = psutil.cpu_times_percent(interval=None) for percent in new: self._test_cpu_percent(percent, last, new) @@ -481,7 +481,7 @@ def test_cpu_times_percent(self): 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): + for _ in range(100): new = psutil.cpu_times_percent(interval=None, percpu=True) for cpu in new: for percent in cpu: diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index e757e017aa..77e52b6969 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -406,16 +406,16 @@ def fun(): self.assertEqual(len(ls), times + 1) def test_execute_w_exc(self): - def fun(): + def fun_1(): 1 / 0 - self.execute_w_exc(ZeroDivisionError, fun) + self.execute_w_exc(ZeroDivisionError, fun_1) with self.assertRaises(ZeroDivisionError): - self.execute_w_exc(OSError, fun) + self.execute_w_exc(OSError, fun_1) - def fun(): + def fun_2(): pass with self.assertRaises(AssertionError): - self.execute_w_exc(ZeroDivisionError, fun) + self.execute_w_exc(ZeroDivisionError, fun_2) class TestTestingUtils(PsutilTestCase): diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 3fa3f0172a..43cf2b49b5 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -316,8 +316,7 @@ class TestFSAPIsWithInvalidPath(TestFSAPIs): """Test FS APIs with a funky, invalid path name.""" funky_suffix = INVALID_UNICODE_SUFFIX - @classmethod - def expect_exact_path_match(cls): + def expect_exact_path_match(self): # Invalid unicode names are supposed to work on Python 2. return True diff --git a/pyproject.toml b/pyproject.toml index c8cb62af01..8d68131c95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,44 @@ exclude_lines = [ "raise NotImplementedError", ] +[tool.pylint.messages_control] +# Important ones: +# undefined-all-variable, invalid-envvar-default, reimported, raising-format-tuple, simplifiable-if-expression, useless-object-inheritance +disable = [ + "broad-except", # except Exception: + "consider-using-dict-comprehension", + "consider-using-f-string", + "consider-using-set-comprehension", + "consider-using-with", + "disallowed-name", + "fixme", + "global-statement", + "import-error", + "import-outside-toplevel", + "inconsistent-return-statements", + "invalid-name", + "missing-class-docstring", + "missing-function-docstring", + "no-else-raise", + "no-else-return", + "protected-access", + "raise-missing-from", + "redefined-builtin", + "super-with-arguments", + "too-few-public-methods", + "too-many-arguments", + "too-many-branches", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-return-statements", + "too-many-statements", + "ungrouped-imports", + "unspecified-encoding", + "wrong-import-position", +] + [build-system] requires = ["setuptools>=43", "wheel"] build-backend = "setuptools.build_meta" diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index d69ace62f4..dcded559b3 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -101,9 +101,9 @@ def run(): # 2 wheels (32 and 64 bit) per supported python version expected = len(PY_VERSIONS) * 2 if expected != completed: - return exit("expected %s files, got %s" % (expected, completed)) + return sys.exit("expected %s files, got %s" % (expected, completed)) if exc: - return exit() + return sys.exit() rename_win27_wheels() diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 8778362727..dad0066b2a 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -107,16 +107,16 @@ def main(): # space at end of line if line.endswith(' '): print("%s:%s %r" % (path, lineno, line)) - return exit("space at end of line") + return sys.exit("space at end of line") line = line.rstrip() # # pdb (now provided by flake8-debugger plugin) # if "pdb.set_trace" in line: # print("%s:%s %s" % (path, lineno, line)) - # return exit("you forgot a pdb in your python code") + # return sys.exit("you forgot a pdb in your python code") # # bare except clause (now provided by flake8-blind-except plugin) # if "except:" in line and not line.endswith("# NOQA"): # print("%s:%s %s" % (path, lineno, line)) - # return exit("bare except clause") + # return sys.exit("bare except clause") # Python linters if py_files: @@ -125,28 +125,28 @@ def main(): cmd = "%s -m flake8 --config=.flake8 %s" % (PYTHON, " ".join(py_files)) ret = subprocess.call(shlex.split(cmd)) if ret != 0: - return exit("python code didn't pass 'flake8' style check; " - "try running 'make fix-flake8'") + return sys.exit("python code didn't pass 'flake8' style check; " + "try running 'make fix-flake8'") # isort cmd = "%s -m isort --check-only %s" % ( PYTHON, " ".join(py_files)) ret = subprocess.call(shlex.split(cmd)) if ret != 0: - return exit("python code didn't pass 'isort' style check; " - "try running 'make fix-imports'") + return sys.exit("python code didn't pass 'isort' style check; " + "try running 'make fix-imports'") # C linter if c_files: # XXX: we should escape spaces and possibly other amenities here cmd = "%s scripts/internal/clinter.py %s" % (PYTHON, " ".join(c_files)) ret = subprocess.call(cmd, shell=True) if ret != 0: - return exit("C code didn't pass style check") + return sys.exit("C code didn't pass style check") if new_rm_mv: out = sh("%s scripts/internal/generate_manifest.py" % PYTHON) with open_text('MANIFEST.in') as f: if out.strip() != f.read().strip(): - exit("some files were added, deleted or renamed; " - "run 'make generate-manifest' and commit again") + sys.exit("some files were added, deleted or renamed; " + "run 'make generate-manifest' and commit again") main() diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index c2113f3677..510dc15f64 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -89,7 +89,7 @@ def get_changes(): break lines.pop(0) - for i, line in enumerate(lines): + for line in lines: line = lines.pop(0) line = line.rstrip() if re.match(r"^- \d+_", line): diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 492bed1cac..978e50e2da 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -98,7 +98,7 @@ def main(): elif path.endswith(".tar.gz"): pkg = Tarball(path) else: - raise ValueError("invalid package %r", path) + raise ValueError("invalid package %r" % path) groups[pkg.platform()].append(pkg) tot_files = 0 diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 466887c2c0..16de39084d 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -82,7 +82,7 @@ # =================================================================== -def safe_print(text, file=sys.stdout, flush=False): +def safe_print(text, file=sys.stdout): """Prints a (unicode) string to the console, encoded depending on the stdout/file encoding (eg. cp437 on Windows). This is to avoid encoding errors in case of funky path names. @@ -228,7 +228,7 @@ def build(): # order to allow "import psutil" when using the interactive interpreter # from within psutil root directory. cmd = [PYTHON, "setup.py", "build_ext", "-i"] - if sys.version_info[:2] >= (3, 6) and os.cpu_count() or 1 > 1: + if sys.version_info[:2] >= (3, 6) and (os.cpu_count() or 1) > 1: cmd += ['--parallel', str(os.cpu_count())] # Print coloured warnings in real time. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -547,8 +547,7 @@ def get_python(path): return pypath -def main(): - global PYTHON +def parse_args(): parser = argparse.ArgumentParser() # option shared by all commands parser.add_argument( @@ -587,8 +586,19 @@ def main(): for p in (test, test_by_name): p.add_argument('arg', type=str, nargs='?', default="", help="arg") + args = parser.parse_args() + if not args.command or args.command == 'help': + parser.print_help(sys.stderr) + sys.exit(1) + + return args + + +def main(): + global PYTHON + args = parse_args() # set python exe PYTHON = get_python(args.python) if not PYTHON: @@ -597,10 +607,6 @@ def main(): os.putenv('PYTHON', PYTHON) win_colorprint("using " + PYTHON) - if not args.command or args.command == 'help': - parser.print_help(sys.stderr) - sys.exit(1) - fname = args.command.replace('-', '_') fun = getattr(sys.modules[__name__], fname) # err if fun not defined funargs = [] diff --git a/scripts/iotop.py b/scripts/iotop.py index f4d62d4671..07ae2fa3d4 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -71,7 +71,7 @@ def poll(interval): sorted by IO activity and total disks I/O activity. """ # first get a list of all processes and disk io counters - procs = [p for p in psutil.process_iter()] + procs = list(psutil.process_iter()) for p in procs[:]: try: p._before = p.io_counters() diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 743a177761..fd44c83e7f 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -316,7 +316,7 @@ def run(pid, verbose=False): print_("", template % (bytes2human(region.rss), region.path)) -def main(argv=None): +def main(): parser = argparse.ArgumentParser( description="print information about a process") parser.add_argument("pid", type=int, help="process pid", nargs='?') diff --git a/setup.py b/setup.py index 65dad252ca..4e6f6f1701 100755 --- a/setup.py +++ b/setup.py @@ -346,10 +346,7 @@ def get_sunos_update(): # for an explanation of Solaris /etc/release with open('/etc/release') as f: update = re.search(r'(?<=s10s_u)[0-9]{1,2}', f.readline()) - if update is None: - return 0 - else: - return int(update.group(0)) + return int(update.group(0)) if update else 0 posix_extension.libraries.append('socket') if platform.release() == '5.10': From 55b7ca0163ac19178d615a067bc35fef177f3e84 Mon Sep 17 00:00:00 2001 From: Mark Asselstine Date: Wed, 5 Apr 2023 13:57:32 -0400 Subject: [PATCH 0950/1714] Fix Linux test: allow '-dirty' or other version postfixes (#2221) It is possible that 'free -V' will return a version that includes a postfix such as '-dirty'. For example procps-ng will explicitly add this when building from git: https://gitlab.com/procps-ng/procps/-/blob/master/local/git-version-gen#L154 Process the version string to drop these string postfixes from the version tuple, failing to do so will result in the test failing File ".../psutil/tests/test_linux.py", line 204, in get_free_version_info return tuple(map(int, out.split()[-1].split('.'))) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ValueError: invalid literal for int() with base 10: '3-dirty' Signed-off-by: Mark Asselstine --- psutil/tests/test_linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index e1de6706aa..ac11017a3a 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -201,7 +201,7 @@ 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, out.split()[-1].split('.'))) + return tuple(map(int, re.findall(r'\d+', out.split()[-1]))) @contextlib.contextmanager From 52fe5517f716dedf9c9918e56325e49a49146130 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 9 Apr 2023 11:32:21 +0200 Subject: [PATCH 0951/1714] update cpu_freq() doc --- docs/index.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index dcc25ab04a..119102a8d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -268,13 +268,13 @@ CPU .. function:: cpu_freq(percpu=False) Return CPU frequency as a named tuple including *current*, *min* and *max* - frequencies expressed in Mhz. - On Linux *current* frequency reports the real-time value, on all other - platforms this usually represents the nominal "fixed" value (never changing). - If *percpu* is ``True`` and the system supports per-cpu frequency - retrieval (Linux only) a list of frequencies is returned for each CPU, - if not, a list with a single element is returned. - If *min* and *max* cannot be determined they are set to ``0.0``. + frequencies expressed in Mhz. On Linux *current* frequency reports the + real-time value, on all other platforms this usually represents the + nominal "fixed" value (never changing). If *percpu* is ``True`` and the + system supports per-cpu frequency retrieval (Linux and FreeBSD), a list of + frequencies is returned for each CPU, if not, a list with a single element + is returned. If *min* and *max* cannot be determined they are set to + ``0.0``. Example (Linux): @@ -289,7 +289,8 @@ CPU scpufreq(current=1703.609, min=800.0, max=3500.0), scpufreq(current=1754.289, min=800.0, max=3500.0)] - Availability: Linux, macOS, Windows, FreeBSD, OpenBSD + Availability: Linux, macOS, Windows, FreeBSD, OpenBSD. *percpu* only + supported on Linux and FreeBSD. .. versionadded:: 5.1.0 From e6bf2bcafc41319a22945e98f50906149527ff51 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 10 Apr 2023 13:17:26 -0700 Subject: [PATCH 0952/1714] move psutil_virtual_mem() in a new mem.c file --- psutil/_psutil_windows.c | 30 +----------------------------- psutil/arch/windows/mem.c | 35 +++++++++++++++++++++++++++++++++++ psutil/arch/windows/mem.h | 9 +++++++++ setup.py | 1 + 4 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 psutil/arch/windows/mem.c create mode 100644 psutil/arch/windows/mem.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 6e51c449dd..969c571389 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -33,6 +33,7 @@ #include "arch/windows/process_handles.h" #include "arch/windows/disk.h" #include "arch/windows/cpu.h" +#include "arch/windows/mem.h" #include "arch/windows/net.h" #include "arch/windows/services.h" #include "arch/windows/socks.h" @@ -603,35 +604,6 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { return Py_BuildValue("I", wsCounters.NumberOfPrivatePages); } - -/* - * Return a Python integer indicating the total amount of physical memory - * in bytes. - */ -static PyObject * -psutil_virtual_mem(PyObject *self, PyObject *args) { - unsigned long long totalPhys, availPhys, totalSys, availSys, pageSize; - PERFORMANCE_INFORMATION perfInfo; - - if (! GetPerformanceInfo(&perfInfo, sizeof(PERFORMANCE_INFORMATION))) { - PyErr_SetFromWindowsErr(0); - return NULL; - } - // values are size_t, widen (if needed) to long long - pageSize = perfInfo.PageSize; - totalPhys = perfInfo.PhysicalTotal * pageSize; - availPhys = perfInfo.PhysicalAvailable * pageSize; - totalSys = perfInfo.CommitLimit * pageSize; - availSys = totalSys - perfInfo.CommitTotal * pageSize; - return Py_BuildValue( - "(LLLL)", - totalPhys, - availPhys, - totalSys, - availSys); -} - - /* * Return process current working directory as a Python string. */ diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c new file mode 100644 index 0000000000..8b678254bd --- /dev/null +++ b/psutil/arch/windows/mem.c @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include + +#include "../../_psutil_common.h" + + +PyObject * +psutil_virtual_mem(PyObject *self, PyObject *args) { + unsigned long long totalPhys, availPhys, totalSys, availSys, pageSize; + PERFORMANCE_INFORMATION perfInfo; + + if (! GetPerformanceInfo(&perfInfo, sizeof(PERFORMANCE_INFORMATION))) { + PyErr_SetFromWindowsErr(0); + return NULL; + } + // values are size_t, widen (if needed) to long long + pageSize = perfInfo.PageSize; + totalPhys = perfInfo.PhysicalTotal * pageSize; + availPhys = perfInfo.PhysicalAvailable * pageSize; + totalSys = perfInfo.CommitLimit * pageSize; + availSys = totalSys - perfInfo.CommitTotal * pageSize; + return Py_BuildValue( + "(LLLL)", + totalPhys, + availPhys, + totalSys, + availSys); +} diff --git a/psutil/arch/windows/mem.h b/psutil/arch/windows/mem.h new file mode 100644 index 0000000000..871cd64ea2 --- /dev/null +++ b/psutil/arch/windows/mem.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_virtual_mem(PyObject* self, PyObject* args); diff --git a/setup.py b/setup.py index 4e6f6f1701..a4f719b2b1 100755 --- a/setup.py +++ b/setup.py @@ -214,6 +214,7 @@ def get_winver(): 'psutil/arch/windows/process_info.c', 'psutil/arch/windows/process_handles.c', 'psutil/arch/windows/disk.c', + 'psutil/arch/windows/mem.c', 'psutil/arch/windows/net.c', 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/security.c', From 4dd1e215f3d49fb3a5074b82430bf6199c14898e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 10 Apr 2023 13:20:39 -0700 Subject: [PATCH 0953/1714] move psutil_getpagesize() in a new mem.c file --- psutil/_psutil_windows.c | 11 ----------- psutil/arch/windows/mem.c | 8 ++++++++ psutil/arch/windows/mem.h | 3 ++- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 969c571389..2ed937ee95 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1515,17 +1515,6 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { } -/* - * System memory page size as an int. - */ -static PyObject * -psutil_getpagesize(PyObject *self, PyObject *args) { - // XXX: we may want to use GetNativeSystemInfo to differentiate - // page size for WoW64 processes (but am not sure). - return Py_BuildValue("I", PSUTIL_SYSTEM_INFO.dwPageSize); -} - - // ------------------------ Python init --------------------------- static PyMethodDef diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c index 8b678254bd..18b535e6af 100644 --- a/psutil/arch/windows/mem.c +++ b/psutil/arch/windows/mem.c @@ -11,6 +11,14 @@ #include "../../_psutil_common.h" +PyObject * +psutil_getpagesize(PyObject *self, PyObject *args) { + // XXX: we may want to use GetNativeSystemInfo to differentiate + // page size for WoW64 processes (but am not sure). + return Py_BuildValue("I", PSUTIL_SYSTEM_INFO.dwPageSize); +} + + PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { unsigned long long totalPhys, availPhys, totalSys, availSys, pageSize; diff --git a/psutil/arch/windows/mem.h b/psutil/arch/windows/mem.h index 871cd64ea2..a10781dfce 100644 --- a/psutil/arch/windows/mem.h +++ b/psutil/arch/windows/mem.h @@ -6,4 +6,5 @@ #include -PyObject *psutil_virtual_mem(PyObject* self, PyObject* args); +PyObject *psutil_getpagesize(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); From 90b35e3ae85651d67a095205adc94aaa0a4ec98f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Apr 2023 12:04:03 +0200 Subject: [PATCH 0954/1714] [POSIX] psutil.users() loses precision for "started" attribute #2225 (#2226) --- .github/workflows/issues.py | 2 +- HISTORY.rst | 2 ++ psutil/_psutil_aix.c | 4 ++-- psutil/_psutil_bsd.c | 4 ++-- psutil/_psutil_linux.c | 4 ++-- psutil/_psutil_osx.c | 4 ++-- psutil/_psutil_sunos.c | 4 ++-- psutil/tests/test_posix.py | 40 +++++++++++++++++++++++++++++++------ 8 files changed, 47 insertions(+), 17 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 06438b97ff..77a9bc96b4 100644 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -59,7 +59,7 @@ "wsl": ["wsl"], "unix": [ "psposix", "_psutil_posix", "waitpid", "statvfs", "/dev/tty", - "/dev/pts", + "/dev/pts", "posix", ], "pypy": ["pypy"], # types diff --git a/HISTORY.rst b/HISTORY.rst index a86f56c429..ac50e89792 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,6 +23,8 @@ argument. - 2216_, [Windows]: fix tests when running in a virtual environment (patch by Matthieu Darbois) +- 2225_, [POSIX]: `users()`_ loses precision for ``started`` attribute (off by + 1 minute). 5.9.4 ===== diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index f5c5cc66bb..8a1b8de94b 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -497,11 +497,11 @@ psutil_users(PyObject *self, PyObject *args) { if (! py_hostname) goto error; py_tuple = Py_BuildValue( - "(OOOfOi)", + "(OOOdOi)", py_username, // username py_tty, // tty py_hostname, // hostname - (float)ut->ut_tv.tv_sec, // tstamp + (double)ut->ut_tv.tv_sec, // tstamp py_user_proc, // (bool) user process ut->ut_pid // process id ); diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 1ffa7b00de..c2de7c911b 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -966,11 +966,11 @@ psutil_users(PyObject *self, PyObject *args) { if (! py_hostname) goto error; py_tuple = Py_BuildValue( - "(OOOfi)", + "(OOOdi)", py_username, // username py_tty, // tty py_hostname, // hostname - (float)ut.ut_time, // start time + (double)ut.ut_time, // start time #if defined(PSUTIL_OPENBSD) || (defined(__FreeBSD_version) && __FreeBSD_version < 900000) -1 // process id (set to None later) #else diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 924fe7027e..a6ee60254b 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -392,11 +392,11 @@ psutil_users(PyObject *self, PyObject *args) { goto error; py_tuple = Py_BuildValue( - "OOOfO" _Py_PARSE_PID, + "OOOdO" _Py_PARSE_PID, py_username, // username py_tty, // tty py_hostname, // hostname - (float)ut->ut_tv.tv_sec, // tstamp + (double)ut->ut_tv.tv_sec, // tstamp py_user_proc, // (bool) user process ut->ut_pid // process id ); diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index ab43871f8f..ed29b33b12 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1579,11 +1579,11 @@ psutil_users(PyObject *self, PyObject *args) { if (! py_hostname) goto error; py_tuple = Py_BuildValue( - "(OOOfi)", + "(OOOdi)", py_username, // username py_tty, // tty py_hostname, // hostname - (float)utx->ut_tv.tv_sec, // start time + (double)utx->ut_tv.tv_sec, // start time utx->ut_pid // process id ); if (!py_tuple) { diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 84bf1c40ea..e2e7c018ed 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -647,11 +647,11 @@ psutil_users(PyObject *self, PyObject *args) { if (! py_hostname) goto error; py_tuple = Py_BuildValue( - "(OOOfOi)", + "(OOOdOi)", py_username, // username py_tty, // tty py_hostname, // hostname - (float)ut->ut_tv.tv_sec, // tstamp + (double)ut->ut_tv.tv_sec, // tstamp py_user_proc, // (bool) user process ut->ut_pid // process id ); diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 0dc1dfda29..a37899fded 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -23,7 +23,6 @@ from psutil import OPENBSD from psutil import POSIX from psutil import SUNOS -from psutil.tests import CI_TESTING from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import PYTHON_EXE from psutil.tests import PsutilTestCase @@ -334,19 +333,48 @@ def test_nic_names(self): "couldn't find %s nic in 'ifconfig -a' output\n%s" % ( nic, output)) - @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") + # @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") @retry_on_failure() def test_users(self): - out = sh("who") + 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())) - for u in psutil.users(): - self.assertIn(u.name, users) - self.assertIn(u.terminal, terminals) + 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]) + p = psutil.Process(u.pid) + # on macOS time is off by ~47 secs for some reason, but + # the next test against 'who' CLI succeeds + delta = 60 if MACOS else 1 + self.assertAlmostEqual(u.started, p.create_time(), delta=delta) + + @retry_on_failure() + def test_users_started(self): + out = sh("who -u") + if not out.strip(): + raise self.skipTest("no users on this system") + # '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: + raise ValueError( + "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 From 81e3a613df6af5d6ffefae8b617da81aa4007868 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Apr 2023 12:37:54 +0200 Subject: [PATCH 0955/1714] fix some tests on BSD platforms --- psutil/tests/__init__.py | 2 +- psutil/tests/test_bsd.py | 1 + psutil/tests/test_contracts.py | 4 ++-- psutil/tests/test_posix.py | 12 ++++++++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index e82bb38d8c..e6f0abc356 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -836,7 +836,7 @@ def create_exe(outpath, c_code=None): assert not os.path.exists(outpath), outpath if c_code: if not which("gcc"): - raise ValueError("gcc is not installed") + raise unittest.SkipTest("gcc is not installed") if isinstance(c_code, bool): # c_code is True c_code = textwrap.dedent( """ diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 29ea384d51..1ce54b199b 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -131,6 +131,7 @@ 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: diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index b767e3ebf1..392eb69b2f 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -258,8 +258,8 @@ def test_disk_partitions(self): self.assertIsInstance(disk.mountpoint, str) self.assertIsInstance(disk.fstype, str) self.assertIsInstance(disk.opts, str) - self.assertIsInstance(disk.maxfile, int) - self.assertIsInstance(disk.maxpath, int) + 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): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index a37899fded..774c7f3c04 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -115,7 +115,10 @@ def ps_args(pid): field = "command" if AIX or SUNOS: field = "args" - return ps(field, pid) + 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): @@ -415,7 +418,12 @@ def test_os_waitpid_bad_ret_status(self): @retry_on_failure() def test_disk_usage(self): def df(device): - out = sh("df -k %s" % device).strip() + 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 From 501e636bdb946a18f94a72941212417b53b5dbbe Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Apr 2023 16:14:14 +0200 Subject: [PATCH 0956/1714] [POSIX] psutil.users() loses precision for "started" attribute #2225 (#2226) --- psutil/_psutil_bsd.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index c2de7c911b..3c3f50e32b 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1016,11 +1016,11 @@ psutil_users(PyObject *self, PyObject *args) { goto error; py_tuple = Py_BuildValue( - "(OOOfO)", + "(OOOdO)", py_username, // username py_tty, // tty py_hostname, // hostname - (float)utx->ut_tv.tv_sec, // start time + (double)utx->ut_tv.tv_sec, // start time py_pid // process id ); From e6b1659df484bd2709d09f62b52851424630c200 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Apr 2023 16:37:41 +0200 Subject: [PATCH 0957/1714] OpenBSD is unable to recognize zombie process. (#2229) OpenBSD is unable to recognize zombie process. It will raise NoSuchProcess instead of ZombieProcess. Test failure: ====================================================================== ERROR: psutil.tests.test_process.TestProcess.test_zombie_process ---------------------------------------------------------------------- Traceback (most recent call last): File "/vagrant/psutil/psutil/_psbsd.py", line 560, in wrapper return fun(self, *args, **kwargs) File "/vagrant/psutil/psutil/_psbsd.py", line 862, in open_files rawlist = cext.proc_open_files(self.pid) ProcessLookupError: [Errno 3] No such process During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/vagrant/psutil/psutil/tests/test_process.py", line 1312, in test_zombie_process zproc.as_dict() File "/vagrant/psutil/psutil/__init__.py", line 528, in as_dict ret = meth() File "/vagrant/psutil/psutil/__init__.py", line 1142, in open_files return self._proc.open_files() File "/vagrant/psutil/psutil/_psbsd.py", line 565, in wrapper raise NoSuchProcess(self.pid, self._name) psutil.NoSuchProcess: process no longer exists (pid=67013) This happens because OpenBSD is the only BSD defining 2 codes for zombie processes: # According to /usr/include/sys/proc.h SZOMB is unused. # test_zombie_process() shows that SDEAD is the right # equivalent. Also it appears there's no equivalent of # psutil.STATUS_DEAD. SDEAD really means STATUS_ZOMBIE. # cext.SZOMB: _common.STATUS_ZOMBIE, cext.SDEAD: _common.STATUS_ZOMBIE, cext.SZOMB: _common.STATUS_ZOMBIE, --- HISTORY.rst | 2 ++ psutil/_psbsd.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index ac50e89792..e82605d8ac 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,6 +25,8 @@ Matthieu Darbois) - 2225_, [POSIX]: `users()`_ loses precision for ``started`` attribute (off by 1 minute). +- 2229_, [OpenBSD]: unable to properly recognize zombie processes. + `NoSuchProcess`_ may be raised instead of `ZombieProcess`_. 5.9.4 ===== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index a25c96cd45..a0f07ee234 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -545,7 +545,7 @@ def pid_exists(pid): def is_zombie(pid): try: st = cext.proc_oneshot_info(pid)[kinfo_proc_map['status']] - return st == cext.SZOMB + return PROC_STATUSES.get(st) == _common.STATUS_ZOMBIE except Exception: return False From 67d988aeb5701bca3cc84795ae3f249b5c843451 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Apr 2023 16:55:13 +0200 Subject: [PATCH 0958/1714] fix users() test on OpenBSD Signed-off-by: Giampaolo Rodola --- psutil/tests/test_posix.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 774c7f3c04..d26312151b 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -350,11 +350,13 @@ def test_users(self): for idx, u in enumerate(psutil.users()): self.assertEqual(u.name, users[idx]) self.assertEqual(u.terminal, terminals[idx]) - p = psutil.Process(u.pid) - # on macOS time is off by ~47 secs for some reason, but - # the next test against 'who' CLI succeeds - delta = 60 if MACOS else 1 - self.assertAlmostEqual(u.started, p.create_time(), delta=delta) + if u.pid is not None: # None on OpenBSD + p = psutil.Process(u.pid) + # on macOS time is off by ~47 secs for some reason, but + # the next test against 'who' CLI succeeds + delta = 60 if MACOS else 1 + self.assertAlmostEqual( + u.started, p.create_time(), delta=delta) @retry_on_failure() def test_users_started(self): From 0dde184075f81a9b6e22caa7255396d21ae63769 Mon Sep 17 00:00:00 2001 From: Daniel Widdis Date: Thu, 13 Apr 2023 01:11:27 -0700 Subject: [PATCH 0959/1714] Get Windows percent swap usage from performance counters (#2160) Signed-off-by: Daniel Widdis --- CREDITS | 2 +- HISTORY.rst | 2 ++ psutil/_psutil_windows.c | 1 + psutil/_pswindows.py | 16 +++++++---- psutil/arch/windows/mem.c | 51 ++++++++++++++++++++++++++++++++++++ psutil/arch/windows/mem.h | 1 + psutil/tests/test_windows.py | 21 +++++++++++++++ 7 files changed, 88 insertions(+), 6 deletions(-) diff --git a/CREDITS b/CREDITS index f414636087..c845a8c661 100644 --- a/CREDITS +++ b/CREDITS @@ -804,7 +804,7 @@ I: 2150 N: Daniel Widdis W: https://github.com/dbwiddis -I: 2077 +I: 2077, 2160 N: Amir Rossert W: https://github.com/arossert diff --git a/HISTORY.rst b/HISTORY.rst index e82605d8ac..8d22111a73 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -47,6 +47,8 @@ ``SPEED_UNKNOWN`` definition. (patch by Amir Rossert) - 2010_, [macOS]: on MacOS, arm64 ``IFM_1000_TX`` and ``IFM_1000_T`` are the same value, causing a build failure. (patch by Lawrence D'Anna) +- 2160_, [Windows]: Get Windows percent swap usage from performance counters. + (patch by Daniel Widdis) 5.9.3 ===== diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 2ed937ee95..11176de738 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1560,6 +1560,7 @@ PsutilMethods[] = { {"disk_usage", psutil_disk_usage, METH_VARARGS}, {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS}, {"getpagesize", psutil_getpagesize, METH_VARARGS}, + {"swap_percent", psutil_swap_percent, METH_VARARGS}, {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, METH_VARARGS}, {"net_connections", psutil_net_connections, METH_VARARGS}, {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 3802f3edb0..4081aa17fb 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -244,16 +244,22 @@ def swap_memory(): mem = cext.virtual_mem() total_phys = mem[0] - free_phys = mem[1] total_system = mem[2] - free_system = mem[3] # system memory (commit total/limit) is the sum of physical and swap # thus physical memory values need to be substracted to get swap values total = total_system - total_phys - free = min(total, free_system - free_phys) - used = total - free - percent = usage_percent(used, total, round_=1) + # 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: + used = 0 + free = total - used + percent = round(percentswap, 1) return _common.sswap(total, used, free, percent, 0, 0) diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c index 18b535e6af..24dc15ad0e 100644 --- a/psutil/arch/windows/mem.c +++ b/psutil/arch/windows/mem.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "../../_psutil_common.h" @@ -41,3 +42,53 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { totalSys, availSys); } + + +// Return a float representing the percent usage of all paging files on +// the system. +PyObject * +psutil_swap_percent(PyObject *self, PyObject *args) { + WCHAR *szCounterPath = L"\\Paging File(_Total)\\% Usage"; + PDH_STATUS s; + HQUERY hQuery; + HCOUNTER hCounter; + PDH_FMT_COUNTERVALUE counterValue; + double percentUsage; + + if ((PdhOpenQueryW(NULL, 0, &hQuery)) != ERROR_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "PdhOpenQueryW failed"); + return NULL; + } + + s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); + if (s != ERROR_SUCCESS) { + PdhCloseQuery(hQuery); + PyErr_Format( + PyExc_RuntimeError, + "PdhAddEnglishCounterW failed. Performance counters may be disabled." + ); + return NULL; + } + + s = PdhCollectQueryData(hQuery); + if (s != ERROR_SUCCESS) { + // If swap disabled this will fail. + psutil_debug("PdhCollectQueryData failed; assume swap percent is 0"); + percentUsage = 0; + } + else { + s = PdhGetFormattedCounterValue( + (PDH_HCOUNTER)hCounter, PDH_FMT_DOUBLE, 0, &counterValue); + if (s != ERROR_SUCCESS) { + PdhCloseQuery(hQuery); + PyErr_Format( + PyExc_RuntimeError, "PdhGetFormattedCounterValue failed"); + return NULL; + } + percentUsage = counterValue.doubleValue; + } + + PdhRemoveCounter(hCounter); + PdhCloseQuery(hQuery); + return Py_BuildValue("d", percentUsage); +} diff --git a/psutil/arch/windows/mem.h b/psutil/arch/windows/mem.h index a10781dfce..48d3dadee6 100644 --- a/psutil/arch/windows/mem.h +++ b/psutil/arch/windows/mem.h @@ -8,3 +8,4 @@ PyObject *psutil_getpagesize(PyObject *self, PyObject *args); PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); +PyObject *psutil_swap_percent(PyObject *self, PyObject *args); diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 9b163a185f..a9f6893364 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -162,6 +162,27 @@ def test_free_phymem(self): 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 From aa2946513b85161f732963d1b7e30d0fe7d00d79 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Apr 2023 15:14:30 +0200 Subject: [PATCH 0960/1714] OpenBSD: rewrite net_connections() from scratch (#2230) --- HISTORY.rst | 7 + MANIFEST.in | 2 + docs/index.rst | 19 ++- psutil/_psbsd.py | 46 ++++--- psutil/_psutil_bsd.c | 7 +- psutil/_psutil_osx.c | 1 + psutil/arch/openbsd/proc.c | 223 +------------------------------ psutil/arch/openbsd/proc.h | 2 - psutil/arch/openbsd/socks.c | 180 +++++++++++++++++++++++++ psutil/arch/openbsd/socks.h | 8 ++ psutil/tests/test_connections.py | 14 +- psutil/tests/test_unicode.py | 13 +- setup.py | 1 + 13 files changed, 246 insertions(+), 277 deletions(-) create mode 100644 psutil/arch/openbsd/socks.c create mode 100644 psutil/arch/openbsd/socks.h diff --git a/HISTORY.rst b/HISTORY.rst index 8d22111a73..dcb6a0d2b7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,9 +9,16 @@ `KeyError` bit deriving from a missed cache hit. - 2217_: print the full traceback when a `DeprecationWarning` or `UserWarning` is raised. +- 2230_, [OpenBSD]: `psutil.net_connections`_ implementation was rewritten from + scratch: + - We're now able to retrieve the path of AF_UNIX sockets (before it was an + empty string) + - The function is faster since it no longer iterates over all processes. + - No longer produces duplicate connection entries. **Bug fixes** +- 1043_, [OpenBSD] `psutil.net_connections`_ returns duplicate entries. - 1915_, [Linux]: on certain kernels, ``"MemAvailable"`` field from ``/proc/meminfo`` returns ``0`` (possibly a kernel bug), in which case we calculate an approximation for ``available`` memory which matches "free" diff --git a/MANIFEST.in b/MANIFEST.in index 0ed5c320ec..2653b691c3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -73,6 +73,8 @@ include psutil/arch/openbsd/mem.c include psutil/arch/openbsd/mem.h include psutil/arch/openbsd/proc.c include psutil/arch/openbsd/proc.h +include psutil/arch/openbsd/socks.c +include psutil/arch/openbsd/socks.h include psutil/arch/osx/cpu.c include psutil/arch/osx/cpu.h include psutil/arch/osx/process_info.c diff --git a/docs/index.rst b/docs/index.rst index 119102a8d9..c722ee2111 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -659,19 +659,18 @@ Network (Solaris) UNIX sockets are not supported. .. note:: - (Linux, FreeBSD) "raddr" field for UNIX sockets is always set to "". - This is a limitation of the OS. - - .. note:: - (OpenBSD) "laddr" and "raddr" fields for UNIX sockets are always set to - "". This is a limitation of the OS. + (Linux, FreeBSD, OpenBSD) *raddr* field for UNIX sockets is always set to + ``""`` (empty string). This is a limitation of the OS. .. versionadded:: 2.1.0 .. versionchanged:: 5.3.0 : socket "fd" is now set for real instead of being ``-1``. - .. versionchanged:: 5.3.0 : "laddr" and "raddr" are named tuples. + .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. + + .. versionchanged:: 5.9.5 : OpenBSD: retrieve *laddr* path for AF_UNIX + sockets (before it was an empty string). .. function:: net_if_addrs() @@ -1943,18 +1942,18 @@ Process class (Solaris) UNIX sockets are not supported. .. note:: - (Linux, FreeBSD) "raddr" field for UNIX sockets is always set to "". + (Linux, FreeBSD) *raddr* field for UNIX sockets is always set to "". This is a limitation of the OS. .. note:: - (OpenBSD) "laddr" and "raddr" fields for UNIX sockets are always set to + (OpenBSD) *laddr* and *raddr* fields for UNIX sockets are always set to "". This is a limitation of the OS. .. note:: (AIX) :class:`psutil.AccessDenied` is always raised unless running as root (lsof does the same). - .. versionchanged:: 5.3.0 : "laddr" and "raddr" are named tuples. + .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. .. method:: is_running() diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index a0f07ee234..1180a94e2d 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -401,26 +401,23 @@ def net_if_stats(): 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]))) families, types = conn_tmap[kind] ret = set() - if NETBSD: + + if OPENBSD: + rawlist = cext.net_connections(-1, families, types) + ret = set() + for item in rawlist: + fd, fam, type, laddr, raddr, status, pid = item + if fam in families and type in types: + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES, pid) + ret.add(nt) + return list(ret) + elif NETBSD: rawlist = cext.net_connections(-1) else: rawlist = cext.net_connections() @@ -773,9 +770,9 @@ 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]))) + families, types = conn_tmap[kind] if NETBSD: - families, types = conn_tmap[kind] ret = [] rawlist = cext.net_connections(self.pid) for item in rawlist: @@ -786,9 +783,22 @@ def connections(self, kind='inet'): TCP_STATUSES) ret.append(nt) self._assert_alive() - return list(ret) + return ret - families, types = conn_tmap[kind] + elif OPENBSD: + ret = [] + rawlist = cext.net_connections(self.pid, families, types) + for item in rawlist: + fd, fam, type, laddr, raddr, status, pid = item + assert pid == self.pid + if fam in families and type in types: + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES) + ret.append(nt) + self._assert_alive() + return ret + + # FreeBSD rawlist = cext.proc_connections(self.pid, families, types) ret = [] for item in rawlist: diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 3c3f50e32b..b2afbb2885 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -84,6 +84,7 @@ #include "arch/openbsd/disk.h" #include "arch/openbsd/mem.h" #include "arch/openbsd/proc.h" + #include "arch/openbsd/socks.h" #include #include // for VREG @@ -1064,7 +1065,7 @@ static PyMethodDef mod_methods[] = { {"proc_name", psutil_proc_name, METH_VARARGS}, {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS}, {"proc_threads", psutil_proc_threads, METH_VARARGS}, -#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) +#if defined(PSUTIL_FREEBSD) {"proc_connections", psutil_proc_connections, METH_VARARGS}, #endif {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, @@ -1093,6 +1094,7 @@ static PyMethodDef mod_methods[] = { {"cpu_times", psutil_cpu_times, METH_VARARGS}, {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"net_connections", psutil_net_connections, METH_VARARGS}, {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"pids", psutil_pids, METH_VARARGS}, @@ -1102,9 +1104,6 @@ static PyMethodDef mod_methods[] = { #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, #endif -#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) - {"net_connections", psutil_net_connections, METH_VARARGS}, -#endif #if defined(PSUTIL_FREEBSD) {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS}, diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index ed29b33b12..9ba0fd2b7e 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1152,6 +1152,7 @@ psutil_proc_connections(PyObject *self, PyObject *args) { // int fd, family, type, lport, rport, state; + // TODO: use INET6_ADDRSTRLEN instead of 200 char lip[200], rip[200]; int inseq; PyObject *py_family; diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 35acab252b..344d6010ec 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -6,25 +6,12 @@ */ #include -#include -#include -#include -#include -#include #include #include #include #include -#include #include -#include #include -#include // for NI_MAXHOST -#include -#define _KERNEL // for DTYPE_* -#include -#undef _KERNEL -#include // for inet_ntoa() #include "../../_psutil_common.h" #include "../../_psutil_posix.h" @@ -155,6 +142,7 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { } +// TODO: refactor this (it's clunky) static char ** _psutil_get_argv(pid_t pid) { static char **argv; @@ -322,212 +310,3 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { } return PyUnicode_DecodeFSDefault(path); } - - -// see sys/kern/kern_sysctl.c lines 1100 and -// usr.bin/fstat/fstat.c print_inet_details() -static char * -psutil_convert_ipv4(int family, uint32_t addr[4]) { - struct in_addr a; - memcpy(&a, addr, sizeof(a)); - return inet_ntoa(a); -} - - -static char * -psutil_inet6_addrstr(struct in6_addr *p) -{ - struct sockaddr_in6 sin6; - static char hbuf[NI_MAXHOST]; - const int niflags = NI_NUMERICHOST; - - memset(&sin6, 0, sizeof(sin6)); - sin6.sin6_family = AF_INET6; - sin6.sin6_len = sizeof(struct sockaddr_in6); - sin6.sin6_addr = *p; - if (IN6_IS_ADDR_LINKLOCAL(p) && - *(u_int16_t *)&sin6.sin6_addr.s6_addr[2] != 0) { - sin6.sin6_scope_id = - ntohs(*(u_int16_t *)&sin6.sin6_addr.s6_addr[2]); - sin6.sin6_addr.s6_addr[2] = sin6.sin6_addr.s6_addr[3] = 0; - } - - if (getnameinfo((struct sockaddr *)&sin6, sin6.sin6_len, - hbuf, sizeof(hbuf), NULL, 0, niflags)) - return "invalid"; - - return hbuf; -} - - -/* - * List process connections. - * Note: there is no net_connections() on OpenBSD. The Python - * implementation will iterate over all processes and use this - * function. - * Note: local and remote paths cannot be determined for UNIX sockets. - */ -PyObject * -psutil_proc_connections(PyObject *self, PyObject *args) { - pid_t pid; - int i; - int cnt; - struct kinfo_file *freep = NULL; - struct kinfo_file *kif; - char *tcplist = NULL; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_laddr = NULL; - PyObject *py_raddr = NULL; - PyObject *py_af_filter = NULL; - PyObject *py_type_filter = NULL; - PyObject *py_family = NULL; - PyObject *_type = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, - &py_type_filter)) - goto error; - if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { - PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); - goto error; - } - - freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) { - goto error; - } - - for (i = 0; i < cnt; i++) { - int state; - int lport; - int rport; - char addrbuf[NI_MAXHOST + 2]; - int inseq; - struct in6_addr laddr6; - py_tuple = NULL; - py_laddr = NULL; - py_raddr = NULL; - - kif = &freep[i]; - if (kif->f_type == DTYPE_SOCKET) { - // apply filters - py_family = PyLong_FromLong((long)kif->so_family); - inseq = PySequence_Contains(py_af_filter, py_family); - Py_DECREF(py_family); - if (inseq == 0) - continue; - _type = PyLong_FromLong((long)kif->so_type); - inseq = PySequence_Contains(py_type_filter, _type); - Py_DECREF(_type); - if (inseq == 0) - continue; - - // IPv4 / IPv6 socket - if ((kif->so_family == AF_INET) || (kif->so_family == AF_INET6)) { - // fill status - if (kif->so_type == SOCK_STREAM) - state = kif->t_state; - else - state = PSUTIL_CONN_NONE; - - // ports - lport = ntohs(kif->inp_lport); - rport = ntohs(kif->inp_fport); - - // local address, IPv4 - if (kif->so_family == AF_INET) { - py_laddr = Py_BuildValue( - "(si)", - psutil_convert_ipv4(kif->so_family, kif->inp_laddru), - lport); - if (!py_laddr) - goto error; - } - else { - // local address, IPv6 - memcpy(&laddr6, kif->inp_laddru, sizeof(laddr6)); - snprintf(addrbuf, sizeof(addrbuf), "%s", - psutil_inet6_addrstr(&laddr6)); - py_laddr = Py_BuildValue("(si)", addrbuf, lport); - if (!py_laddr) - goto error; - } - - if (rport != 0) { - // remote address, IPv4 - if (kif->so_family == AF_INET) { - py_raddr = Py_BuildValue( - "(si)", - psutil_convert_ipv4( - kif->so_family, kif->inp_faddru), - rport); - } - else { - // remote address, IPv6 - memcpy(&laddr6, kif->inp_faddru, sizeof(laddr6)); - snprintf(addrbuf, sizeof(addrbuf), "%s", - psutil_inet6_addrstr(&laddr6)); - py_raddr = Py_BuildValue("(si)", addrbuf, rport); - if (!py_raddr) - goto error; - } - } - else { - py_raddr = Py_BuildValue("()"); - } - - if (!py_raddr) - goto error; - py_tuple = Py_BuildValue( - "(iiiNNi)", - kif->fd_fd, - kif->so_family, - kif->so_type, - py_laddr, - py_raddr, - state); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); - } - // UNIX socket. - // XXX: local addr is supposed to be in "unp_path" but it - // always empty; also "fstat" command is not able to show - // UNIX socket paths. - else if (kif->so_family == AF_UNIX) { - py_tuple = Py_BuildValue( - "(iiissi)", - kif->fd_fd, - kif->so_family, - kif->so_type, - "", // laddr (kif->unp_path is empty) - "", // raddr - PSUTIL_CONN_NONE); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); - Py_INCREF(Py_None); - } - } - } - free(freep); - free(tcplist); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_XDECREF(py_laddr); - Py_XDECREF(py_raddr); - Py_DECREF(py_retlist); - if (freep != NULL) - free(freep); - if (tcplist != NULL) - free(tcplist); - return NULL; -} diff --git a/psutil/arch/openbsd/proc.h b/psutil/arch/openbsd/proc.h index 5ca5890d2a..747507dd45 100644 --- a/psutil/arch/openbsd/proc.h +++ b/psutil/arch/openbsd/proc.h @@ -18,5 +18,3 @@ PyObject *psutil_get_cmdline(pid_t pid); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); -PyObject *psutil_proc_connections(PyObject *self, PyObject *args); - diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c new file mode 100644 index 0000000000..69daa447b0 --- /dev/null +++ b/psutil/arch/openbsd/socks.c @@ -0,0 +1,180 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#define _KERNEL // silence compiler warning +#include // DTYPE_SOCKET +#include // INET6_ADDRSTRLEN, in6_addr +#undef _KERNEL + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" + + +PyObject * +psutil_net_connections(PyObject *self, PyObject *args) { + pid_t pid; + int i; + int cnt; + int state; + int lport; + int rport; + char lip[INET6_ADDRSTRLEN]; + char rip[INET6_ADDRSTRLEN]; + int inseq; + + char errbuf[_POSIX2_LINE_MAX]; + kvm_t *kd = NULL; + + struct kinfo_file *kif; + struct kinfo_file *ikf; + struct in6_addr laddr6; + + PyObject *py_retlist = PyList_New(0); + PyObject *py_tuple = NULL; + PyObject *py_laddr = NULL; + PyObject *py_raddr = NULL; + PyObject *py_lpath = NULL; + PyObject *py_af_filter = NULL; + PyObject *py_type_filter = NULL; + PyObject *py_family = NULL; + PyObject *_type = NULL; + + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, + &py_type_filter)) { + goto error; + } + if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { + PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); + goto error; + } + + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); + if (! kd) { + convert_kvm_err("kvm_openfiles", errbuf); + goto error; + } + + ikf = kvm_getfiles(kd, KERN_FILE_BYPID, -1, sizeof(*ikf), &cnt); + if (! ikf) { + PyErr_SetFromOSErrnoWithSyscall("kvm_getfiles"); + goto error; + } + + for (int i = 0; i < cnt; i++) { + const struct kinfo_file *kif = ikf + i; + py_tuple = NULL; + py_laddr = NULL; + py_raddr = NULL; + py_lpath = NULL; + + // apply filters + if (kif->f_type != DTYPE_SOCKET) + continue; + if (pid != -1 && kif->p_pid != (uint32_t)pid) + continue; + py_family = PyLong_FromLong((long)kif->so_family); + inseq = PySequence_Contains(py_af_filter, py_family); + Py_DECREF(py_family); + if (inseq == 0) + continue; + _type = PyLong_FromLong((long)kif->so_type); + inseq = PySequence_Contains(py_type_filter, _type); + Py_DECREF(_type); + if (inseq == 0) + continue; + + // IPv4 / IPv6 socket + if ((kif->so_family == AF_INET) || (kif->so_family == AF_INET6)) { + // status + if (kif->so_type == SOCK_STREAM) + state = kif->t_state; + else + state = PSUTIL_CONN_NONE; + + // local & remote port + lport = ntohs(kif->inp_lport); + rport = ntohs(kif->inp_fport); + + // local addr + inet_ntop(kif->so_family, &kif->inp_laddru, lip, sizeof(lip)); + py_laddr = Py_BuildValue("(si)", lip, lport); + if (! py_laddr) + goto error; + + // remote addr + if (rport != 0) { + inet_ntop(kif->so_family, &kif->inp_faddru, rip, sizeof(rip)); + py_raddr = Py_BuildValue("(si)", rip, rport); + } + else { + py_raddr = Py_BuildValue("()"); + } + if (! py_raddr) + goto error; + + // populate tuple and list + py_tuple = Py_BuildValue( + "(iiiNNil)", + kif->fd_fd, + kif->so_family, + kif->so_type, + py_laddr, + py_raddr, + state, + kif->p_pid + ); + if (! py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_DECREF(py_tuple); + } + // UNIX socket + else if (kif->so_family == AF_UNIX) { + py_lpath = PyUnicode_DecodeFSDefault(kif->unp_path); + if (! py_lpath) + goto error; + + py_tuple = Py_BuildValue( + "(iiiOsil)", + kif->fd_fd, + kif->so_family, + kif->so_type, + py_lpath, + "", // raddr + PSUTIL_CONN_NONE, + kif->p_pid + ); + if (! py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_DECREF(py_lpath); + Py_DECREF(py_tuple); + Py_INCREF(Py_None); + } + } + + kvm_close(kd); + return py_retlist; + +error: + Py_XDECREF(py_tuple); + Py_XDECREF(py_laddr); + Py_XDECREF(py_raddr); + Py_DECREF(py_retlist); + if (kd != NULL) + kvm_close(kd); + return NULL; +} diff --git a/psutil/arch/openbsd/socks.h b/psutil/arch/openbsd/socks.h new file mode 100644 index 0000000000..90b678bbb8 --- /dev/null +++ b/psutil/arch/openbsd/socks.h @@ -0,0 +1,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. + */ + +PyObject *psutil_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index d47233bc80..ad615ed07e 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -143,11 +143,7 @@ def check_socket(self, sock): 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) + self.assertEqual(conn.laddr, laddr) # XXX Solaris can't retrieve system-wide UNIX sockets if sock.family == AF_UNIX and HAS_CONNECTIONS_UNIX: @@ -243,22 +239,16 @@ def test_unix(self): # 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: + 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) - 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, testfn) - self.assertEqual(cons[0].raddr or cons[1].raddr, testfn) finally: server.close() client.close() diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 43cf2b49b5..c7d8dfbc0b 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -82,7 +82,6 @@ import psutil from psutil import BSD -from psutil import OPENBSD from psutil import POSIX from psutil import WINDOWS from psutil._compat import PY3 @@ -257,9 +256,7 @@ def test_proc_connections(self): with closing(sock): conn = psutil.Process().connections('unix')[0] self.assertIsInstance(conn.laddr, str) - # AF_UNIX addr not set on OpenBSD - if not OPENBSD: # XXX - self.assertEqual(conn.laddr, name) + self.assertEqual(conn.laddr, name) @unittest.skipIf(not POSIX, "POSIX only") @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") @@ -281,11 +278,9 @@ def find_sock(cons): 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) + conn = find_sock(cons) + self.assertIsInstance(conn.laddr, str) + self.assertEqual(conn.laddr, name) def test_disk_usage(self): dname = self.funky_name + "2" diff --git a/setup.py b/setup.py index a4f719b2b1..c69f6e3d12 100755 --- a/setup.py +++ b/setup.py @@ -275,6 +275,7 @@ def get_winver(): 'psutil/arch/openbsd/disk.c', 'psutil/arch/openbsd/mem.c', 'psutil/arch/openbsd/proc.c', + 'psutil/arch/openbsd/socks.c', ], define_macros=macros, libraries=["kvm"], From 59d8550c147795c6ab89535f719fbf6c8c40d2d5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Apr 2023 20:52:09 +0000 Subject: [PATCH 0961/1714] fix NetBSD test failure + add test for cached mem Signed-off-by: Giampaolo Rodola --- psutil/tests/test_bsd.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 1ce54b199b..69f50732b1 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -127,6 +127,7 @@ def test_cpu_count_logical(self): 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) @@ -508,6 +509,8 @@ def parse_meminfo(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:")) @@ -527,6 +530,13 @@ def test_vmem_shared(self): 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:"), @@ -541,6 +551,8 @@ 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: From eb8e318628eeddc3b237449c08226785e9e2d589 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Apr 2023 00:19:55 +0200 Subject: [PATCH 0962/1714] [NetBSD] move files / refactoring of C files (#2232) --- MANIFEST.in | 10 +- psutil/_psutil_bsd.c | 5 +- psutil/arch/netbsd/cpu.c | 102 +++++++++ psutil/arch/netbsd/cpu.h | 11 + psutil/arch/netbsd/disk.c | 75 ++++++ psutil/arch/netbsd/disk.h | 10 + psutil/arch/netbsd/mem.c | 111 +++++++++ psutil/arch/netbsd/mem.h | 11 + psutil/arch/netbsd/{specific.c => proc.c} | 263 +--------------------- psutil/arch/netbsd/{specific.h => proc.h} | 5 - setup.py | 5 +- 11 files changed, 337 insertions(+), 271 deletions(-) create mode 100644 psutil/arch/netbsd/cpu.c create mode 100644 psutil/arch/netbsd/cpu.h create mode 100644 psutil/arch/netbsd/disk.c create mode 100644 psutil/arch/netbsd/disk.h create mode 100644 psutil/arch/netbsd/mem.c create mode 100644 psutil/arch/netbsd/mem.h rename psutil/arch/netbsd/{specific.c => proc.c} (59%) rename psutil/arch/netbsd/{specific.h => proc.h} (74%) diff --git a/MANIFEST.in b/MANIFEST.in index 2653b691c3..a594328da7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -61,10 +61,16 @@ include psutil/arch/freebsd/sensors.c include psutil/arch/freebsd/sensors.h include psutil/arch/freebsd/sys_socks.c include psutil/arch/freebsd/sys_socks.h +include psutil/arch/netbsd/cpu.c +include psutil/arch/netbsd/cpu.h +include psutil/arch/netbsd/disk.c +include psutil/arch/netbsd/disk.h +include psutil/arch/netbsd/mem.c +include psutil/arch/netbsd/mem.h +include psutil/arch/netbsd/proc.c +include psutil/arch/netbsd/proc.h include psutil/arch/netbsd/socks.c include psutil/arch/netbsd/socks.h -include psutil/arch/netbsd/specific.c -include psutil/arch/netbsd/specific.h include psutil/arch/openbsd/cpu.c include psutil/arch/openbsd/cpu.h include psutil/arch/openbsd/disk.c diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index b2afbb2885..ff5fd72da9 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -93,7 +93,10 @@ #undef _KERNEL #include // for CPUSTATES & CP_* #elif PSUTIL_NETBSD - #include "arch/netbsd/specific.h" + #include "arch/netbsd/cpu.h" + #include "arch/netbsd/disk.h" + #include "arch/netbsd/mem.h" + #include "arch/netbsd/proc.h" #include "arch/netbsd/socks.h" #include diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c new file mode 100644 index 0000000000..33eb906f1f --- /dev/null +++ b/psutil/arch/netbsd/cpu.c @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include +#include +#include + + +/* +CPU related functions. Original code was refactored and moved from +psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously +already) from cset 84219ad. For reference, here's the git history with +original(ish) implementations: +- per CPU times: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) +- CPU stats: a991494e4502e1235ebc62b5ba450287d0dedec0 (Jan 2016) +*/ + + +PyObject * +psutil_cpu_stats(PyObject *self, PyObject *args) { + size_t size; + struct uvmexp_sysctl uv; + int uvmexp_mib[] = {CTL_VM, VM_UVMEXP2}; + + size = sizeof(uv); + if (sysctl(uvmexp_mib, 2, &uv, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return Py_BuildValue( + "IIIIIII", + uv.swtch, // ctx switches + uv.intrs, // interrupts - XXX always 0, will be determined via /proc + uv.softs, // soft interrupts + uv.syscalls, // syscalls - XXX always 0 + uv.traps, // traps + uv.faults, // faults + uv.forks // forks + ); +} + + +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + int mib[3]; + int ncpu; + size_t len; + size_t size; + int i; + PyObject *py_cputime = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + // retrieve the number of cpus + mib[0] = CTL_HW; + mib[1] = HW_NCPU; + len = sizeof(ncpu); + if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + uint64_t cpu_time[CPUSTATES]; + + for (i = 0; i < ncpu; i++) { + // per-cpu info + mib[0] = CTL_KERN; + mib[1] = KERN_CP_TIME; + mib[2] = i; + size = sizeof(cpu_time); + if (sysctl(mib, 3, &cpu_time, &size, NULL, 0) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + py_cputime = Py_BuildValue( + "(ddddd)", + (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC + ); + if (!py_cputime) + goto error; + if (PyList_Append(py_retlist, py_cputime)) + goto error; + Py_DECREF(py_cputime); + } + + return py_retlist; + +error: + Py_XDECREF(py_cputime); + Py_DECREF(py_retlist); + return NULL; +} diff --git a/psutil/arch/netbsd/cpu.h b/psutil/arch/netbsd/cpu.h new file mode 100644 index 0000000000..6c86103250 --- /dev/null +++ b/psutil/arch/netbsd/cpu.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/disk.c b/psutil/arch/netbsd/disk.c new file mode 100644 index 0000000000..5481f65df6 --- /dev/null +++ b/psutil/arch/netbsd/disk.c @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +/* +Disk related functions. Original code was refactored and moved from +psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously +already) from cset 84219ad. For reference, here's the git history with +original(ish) implementations: +- disk IO counters: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) +*/ + +#include +#include +#include + + +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + int i, dk_ndrive, mib[3]; + size_t len; + struct io_sysctl *stats = NULL; + PyObject *py_disk_info = NULL; + PyObject *py_retdict = PyDict_New(); + + if (py_retdict == NULL) + return NULL; + mib[0] = CTL_HW; + mib[1] = HW_IOSTATS; + mib[2] = sizeof(struct io_sysctl); + len = 0; + if (sysctl(mib, 3, NULL, &len, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + dk_ndrive = (int)(len / sizeof(struct io_sysctl)); + + stats = malloc(len); + if (stats == NULL) { + PyErr_NoMemory(); + goto error; + } + if (sysctl(mib, 3, stats, &len, NULL, 0) < 0 ) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (i = 0; i < dk_ndrive; i++) { + py_disk_info = Py_BuildValue( + "(KKKK)", + stats[i].rxfer, + stats[i].wxfer, + stats[i].rbytes, + stats[i].wbytes + ); + if (!py_disk_info) + goto error; + if (PyDict_SetItemString(py_retdict, stats[i].name, py_disk_info)) + goto error; + Py_DECREF(py_disk_info); + } + + free(stats); + return py_retdict; + +error: + Py_XDECREF(py_disk_info); + Py_DECREF(py_retdict); + if (stats != NULL) + free(stats); + return NULL; +} diff --git a/psutil/arch/netbsd/disk.h b/psutil/arch/netbsd/disk.h new file mode 100644 index 0000000000..77691c0dfc --- /dev/null +++ b/psutil/arch/netbsd/disk.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c new file mode 100644 index 0000000000..b7f5ba23a6 --- /dev/null +++ b/psutil/arch/netbsd/mem.c @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +/* +Memory related functions. Original code was refactored and moved from +psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously +already) from cset 84219ad. For reference, here's the git history with +original(ish) implementations: +- virtual memory: 0749a69c01b374ca3e2180aaafc3c95e3b2d91b9 (Oct 2016) +- swap memory: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) +*/ + +#include +#include +#include +#include + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" + + +// Virtual memory stats, taken from: +// https://github.com/satterly/zabbix-stats/blob/master/src/libs/zbxsysinfo/ +// netbsd/memory.c +PyObject * +psutil_virtual_mem(PyObject *self, PyObject *args) { + size_t size; + struct uvmexp_sysctl uv; + int mib[] = {CTL_VM, VM_UVMEXP2}; + long pagesize = psutil_getpagesize(); + + size = sizeof(uv); + if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return Py_BuildValue("KKKKKKKK", + (unsigned long long) uv.npages << uv.pageshift, // total + (unsigned long long) uv.free << uv.pageshift, // free + (unsigned long long) uv.active << uv.pageshift, // active + (unsigned long long) uv.inactive << uv.pageshift, // inactive + (unsigned long long) uv.wired << uv.pageshift, // wired + (unsigned long long) (uv.filepages + uv.execpages) * pagesize, // cached + // These are determined from /proc/meminfo in Python. + (unsigned long long) 0, // buffers + (unsigned long long) 0 // shared + ); +} + + +PyObject * +psutil_swap_mem(PyObject *self, PyObject *args) { + uint64_t swap_total, swap_free; + struct swapent *swdev; + int nswap, i; + long pagesize = psutil_getpagesize(); + + nswap = swapctl(SWAP_NSWAP, 0, 0); + if (nswap == 0) { + // This means there's no swap partition. + return Py_BuildValue("(iiiii)", 0, 0, 0, 0, 0); + } + + swdev = calloc(nswap, sizeof(*swdev)); + if (swdev == NULL) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + if (swapctl(SWAP_STATS, swdev, nswap) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + // Total things up. + swap_total = swap_free = 0; + for (i = 0; i < nswap; i++) { + if (swdev[i].se_flags & SWF_ENABLE) { + swap_total += (uint64_t)swdev[i].se_nblks * DEV_BSIZE; + swap_free += (uint64_t)(swdev[i].se_nblks - swdev[i].se_inuse) * DEV_BSIZE; + } + } + free(swdev); + + // Get swap in/out + unsigned int total; + size_t size = sizeof(total); + struct uvmexp_sysctl uv; + int mib[] = {CTL_VM, VM_UVMEXP2}; + size = sizeof(uv); + if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + return Py_BuildValue("(LLLll)", + swap_total, + (swap_total - swap_free), + swap_free, + (long) uv.pgswapin * pagesize, // swap in + (long) uv.pgswapout * pagesize); // swap out + +error: + free(swdev); + return NULL; +} diff --git a/psutil/arch/netbsd/mem.h b/psutil/arch/netbsd/mem.h new file mode 100644 index 0000000000..1de3f3c434 --- /dev/null +++ b/psutil/arch/netbsd/mem.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/proc.c similarity index 59% rename from psutil/arch/netbsd/specific.c rename to psutil/arch/netbsd/proc.c index 11b8c1dd25..b87473a6a6 100644 --- a/psutil/arch/netbsd/specific.c +++ b/psutil/arch/netbsd/proc.c @@ -7,39 +7,13 @@ * Platform-specific module methods for NetBSD. */ -#if defined(PSUTIL_NETBSD) - #define _KMEMUSER -#endif - #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include // for swap_mem -#include #include -// connection stuff -#include // for NI_MAXHOST -#include -#include // for CPUSTATES & CP_* -#define _KERNEL // for DTYPE_* - #include -#undef _KERNEL -#include // struct diskstats -#include -#include #include "../../_psutil_common.h" #include "../../_psutil_posix.h" -#include "specific.h" +#include "proc.h" #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) @@ -295,10 +269,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -// ============================================================================ -// APIS -// ============================================================================ - int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { // Returns a list of all BSD processes on the system. This routine @@ -435,96 +405,6 @@ psutil_get_cmdline(pid_t pid) { } -/* - * Virtual memory stats, taken from: - * https://github.com/satterly/zabbix-stats/blob/master/src/libs/zbxsysinfo/ - * netbsd/memory.c - */ -PyObject * -psutil_virtual_mem(PyObject *self, PyObject *args) { - size_t size; - struct uvmexp_sysctl uv; - int mib[] = {CTL_VM, VM_UVMEXP2}; - long pagesize = psutil_getpagesize(); - - size = sizeof(uv); - if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - return Py_BuildValue("KKKKKKKK", - (unsigned long long) uv.npages << uv.pageshift, // total - (unsigned long long) uv.free << uv.pageshift, // free - (unsigned long long) uv.active << uv.pageshift, // active - (unsigned long long) uv.inactive << uv.pageshift, // inactive - (unsigned long long) uv.wired << uv.pageshift, // wired - (unsigned long long) (uv.filepages + uv.execpages) * pagesize, // cached - // These are determined from /proc/meminfo in Python. - (unsigned long long) 0, // buffers - (unsigned long long) 0 // shared - ); -} - - -PyObject * -psutil_swap_mem(PyObject *self, PyObject *args) { - uint64_t swap_total, swap_free; - struct swapent *swdev; - int nswap, i; - long pagesize = psutil_getpagesize(); - - nswap = swapctl(SWAP_NSWAP, 0, 0); - if (nswap == 0) { - // This means there's no swap partition. - return Py_BuildValue("(iiiii)", 0, 0, 0, 0, 0); - } - - swdev = calloc(nswap, sizeof(*swdev)); - if (swdev == NULL) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - if (swapctl(SWAP_STATS, swdev, nswap) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - // Total things up. - swap_total = swap_free = 0; - for (i = 0; i < nswap; i++) { - if (swdev[i].se_flags & SWF_ENABLE) { - swap_total += (uint64_t)swdev[i].se_nblks * DEV_BSIZE; - swap_free += (uint64_t)(swdev[i].se_nblks - swdev[i].se_inuse) * DEV_BSIZE; - } - } - free(swdev); - - // Get swap in/out - unsigned int total; - size_t size = sizeof(total); - struct uvmexp_sysctl uv; - int mib[] = {CTL_VM, VM_UVMEXP2}; - size = sizeof(uv); - if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - return Py_BuildValue("(LLLll)", - swap_total, - (swap_total - swap_free), - swap_free, - (long) uv.pgswapin * pagesize, // swap in - (long) uv.pgswapout * pagesize); // swap out - -error: - free(swdev); - return NULL; -} - - PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { long pid; @@ -545,144 +425,3 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { return Py_BuildValue("i", cnt); } - - -PyObject * -psutil_per_cpu_times(PyObject *self, PyObject *args) { - // XXX: why static? - int mib[3]; - int ncpu; - size_t len; - size_t size; - int i; - PyObject *py_cputime = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - // retrieve the number of cpus - mib[0] = CTL_HW; - mib[1] = HW_NCPU; - len = sizeof(ncpu); - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - uint64_t cpu_time[CPUSTATES]; - - for (i = 0; i < ncpu; i++) { - // per-cpu info - mib[0] = CTL_KERN; - mib[1] = KERN_CP_TIME; - mib[2] = i; - size = sizeof(cpu_time); - if (sysctl(mib, 3, &cpu_time, &size, NULL, 0) == -1) { - warn("failed to get kern.cptime2"); - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - py_cputime = Py_BuildValue( - "(ddddd)", - (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC); - if (!py_cputime) - goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_DECREF(py_cputime); - } - - return py_retlist; - -error: - Py_XDECREF(py_cputime); - Py_DECREF(py_retlist); - return NULL; -} - - -PyObject * -psutil_disk_io_counters(PyObject *self, PyObject *args) { - int i, dk_ndrive, mib[3]; - size_t len; - struct io_sysctl *stats = NULL; - PyObject *py_disk_info = NULL; - PyObject *py_retdict = PyDict_New(); - - if (py_retdict == NULL) - return NULL; - mib[0] = CTL_HW; - mib[1] = HW_IOSTATS; - mib[2] = sizeof(struct io_sysctl); - len = 0; - if (sysctl(mib, 3, NULL, &len, NULL, 0) < 0) { - warn("can't get HW_IOSTATS"); - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - dk_ndrive = (int)(len / sizeof(struct io_sysctl)); - - stats = malloc(len); - if (stats == NULL) { - PyErr_NoMemory(); - goto error; - } - if (sysctl(mib, 3, stats, &len, NULL, 0) < 0 ) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (i = 0; i < dk_ndrive; i++) { - py_disk_info = Py_BuildValue( - "(KKKK)", - stats[i].rxfer, - stats[i].wxfer, - stats[i].rbytes, - stats[i].wbytes - ); - if (!py_disk_info) - goto error; - if (PyDict_SetItemString(py_retdict, stats[i].name, py_disk_info)) - goto error; - Py_DECREF(py_disk_info); - } - - free(stats); - return py_retdict; - -error: - Py_XDECREF(py_disk_info); - Py_DECREF(py_retdict); - if (stats != NULL) - free(stats); - return NULL; -} - - -PyObject * -psutil_cpu_stats(PyObject *self, PyObject *args) { - size_t size; - struct uvmexp_sysctl uv; - int uvmexp_mib[] = {CTL_VM, VM_UVMEXP2}; - - size = sizeof(uv); - if (sysctl(uvmexp_mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - return Py_BuildValue( - "IIIIIII", - uv.swtch, // ctx switches - uv.intrs, // interrupts - XXX always 0, will be determined via /proc - uv.softs, // soft interrupts - uv.syscalls, // syscalls - XXX always 0 - uv.traps, // traps - uv.faults, // faults - uv.forks // forks - ); -} diff --git a/psutil/arch/netbsd/specific.h b/psutil/arch/netbsd/proc.h similarity index 74% rename from psutil/arch/netbsd/specific.h rename to psutil/arch/netbsd/proc.h index 391ed164a4..b4c88851fc 100644 --- a/psutil/arch/netbsd/specific.h +++ b/psutil/arch/netbsd/proc.h @@ -17,13 +17,8 @@ char *psutil_get_cmd_args(pid_t pid, size_t *argsize); // PyObject *psutil_get_cmdline(pid_t pid); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_connections(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); -PyObject* psutil_disk_io_counters(PyObject* self, PyObject* args); PyObject* psutil_proc_exe(PyObject* self, PyObject* args); PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); -PyObject* psutil_cpu_stats(PyObject* self, PyObject* args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index c69f6e3d12..9fda1b303d 100755 --- a/setup.py +++ b/setup.py @@ -287,7 +287,10 @@ def get_winver(): 'psutil._psutil_bsd', sources=sources + [ 'psutil/_psutil_bsd.c', - 'psutil/arch/netbsd/specific.c', + 'psutil/arch/netbsd/cpu.c', + 'psutil/arch/netbsd/disk.c', + 'psutil/arch/netbsd/mem.c', + 'psutil/arch/netbsd/proc.c', 'psutil/arch/netbsd/socks.c', ], define_macros=macros, From 6e2dc7728f612b91d692fac5ab92cdd4521811c9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Apr 2023 01:07:42 +0200 Subject: [PATCH 0963/1714] [NetBSD] available mem can be higher than total (#2233) On NetBSD "available" memory can be higher than "total". From now now calculate it exactly the same as Zabbix: https://github.com/zabbix/zabbix/blob/af5e0f80253e585ca7082ca6bc9cc07400afe2a7/src/libs/zbxsysinfo/netbsd/memory.c Fixes https://github.com/giampaolo/psutil/issues/2231 which produced this failure: ``` ====================================================================== FAIL: psutil.tests.test_system.TestMemoryAPIs.test_virtual_memory ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/vagrant/psutil/psutil/tests/test_system.py", line 275, in test_virtual_memory assert 0 <= mem.percent <= 100, mem ^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: svmem(total=1019899904, available=1046573056, percent=-2.6, used=603414528, free=545050624, active=234807296, inactive=133210112, buffers=260288512, cached=368312320, shared=0, wired=294912) ``` --- HISTORY.rst | 1 + psutil/_psbsd.py | 29 +++++++++++++++++++---------- psutil/arch/netbsd/mem.c | 10 ++++++---- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index dcb6a0d2b7..63b43c694d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -34,6 +34,7 @@ 1 minute). - 2229_, [OpenBSD]: unable to properly recognize zombie processes. `NoSuchProcess`_ may be raised instead of `ZombieProcess`_. +- 2231_, [NetBSD]: *available* `virtual_memory()`_ is higher than *total*. 5.9.4 ===== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 1180a94e2d..c1cc1c035e 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -176,11 +176,11 @@ # ===================================================================== -def virtual_memory(): - """System virtual memory as a namedtuple.""" - mem = cext.virtual_mem() - total, free, active, inactive, wired, cached, buffers, shared = mem - if NETBSD: +if NETBSD: + def virtual_memory(): + """System virtual memory as a namedtuple.""" + mem = cext.virtual_mem() + total, free, active, inactive, wired, cached, avail = 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: @@ -191,11 +191,20 @@ def virtual_memory(): shared = int(line.split()[1]) * 1024 elif line.startswith(b'Cached:'): cached = int(line.split()[1]) * 1024 - 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) + 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) +else: + def virtual_memory(): + """System virtual memory as a namedtuple.""" + mem = cext.virtual_mem() + total, free, active, inactive, wired, cached, buffers, shared = mem + 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) def swap_memory(): diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index b7f5ba23a6..26ab8e690a 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -32,6 +32,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; long pagesize = psutil_getpagesize(); + unsigned long long available; size = sizeof(uv); if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { @@ -39,16 +40,17 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return NULL; } - return Py_BuildValue("KKKKKKKK", + // follow zabbix + available = uv.inactive + uv.execpages + uv.filepages + uv.free; + return Py_BuildValue( + "KKKKKKK", (unsigned long long) uv.npages << uv.pageshift, // total (unsigned long long) uv.free << uv.pageshift, // free (unsigned long long) uv.active << uv.pageshift, // active (unsigned long long) uv.inactive << uv.pageshift, // inactive (unsigned long long) uv.wired << uv.pageshift, // wired (unsigned long long) (uv.filepages + uv.execpages) * pagesize, // cached - // These are determined from /proc/meminfo in Python. - (unsigned long long) 0, // buffers - (unsigned long long) 0 // shared + available << uv.pageshift // available ); } From 9322179812d3177719c7bab23301646bbf862974 Mon Sep 17 00:00:00 2001 From: Aaron Shaw Date: Fri, 14 Apr 2023 15:31:02 +0100 Subject: [PATCH 0964/1714] docs: add percentage usage of virtual memory (#2223) docs was missing a note about percentage memory usage Signed-off-by: Aaron Shaw --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index c722ee2111..dfca444d04 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -345,6 +345,7 @@ Memory Other metrics: + - **percent**: the percentage usage calculated as ``(total - available) / total * 100`` - **used**: memory used, calculated differently depending on the platform and designed for informational purposes only. **total - free** does not necessarily match **used**. From a0b096c88421548593ecebe93bbe369385087f3b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 15 Apr 2023 13:39:02 +0200 Subject: [PATCH 0965/1714] [NetBSD] virtual_memory() metrics are completely wrong (#2235), fixes #2234 Match values shown by **htop**. This is before. In here: * available mem is almost the same as total (unrealistic) * used is higher than total; there's also a failing test: ``` MEMORY ------ Total : 972.7M Available : 959.1M Percent : 1.4 Used : 1.1G Free : 173.6M Active : 434.3M Inactive : 258.4M Buffers : 509.4M Cached : 692.9M Shared : 0.0B Wired : 280.0K ``` Now: ``` MEMORY ------ Total : 972.7M Available : 538.1M Percent : 44.7 Used : 434.5M Free : 173.6M Active : 434.2M Inactive : 258.4M Buffers : 509.4M Cached : 692.9M Shared : 0.0B Wired : 280.0K ``` --- HISTORY.rst | 2 ++ docs/index.rst | 15 +++++++++------ psutil/_psbsd.py | 41 ++++++++++++++++++++++------------------ psutil/arch/netbsd/mem.c | 24 +++++++++++------------ 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 63b43c694d..d214e61ff4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -35,6 +35,8 @@ - 2229_, [OpenBSD]: unable to properly recognize zombie processes. `NoSuchProcess`_ may be raised instead of `ZombieProcess`_. - 2231_, [NetBSD]: *available* `virtual_memory()`_ is higher than *total*. +- 2234_, [NetBSD]: `virtual_memory()`_ metrics are wrong: *available* and + *used* are too high. We now match values shown by *htop* CLI utility. 5.9.4 ===== diff --git a/docs/index.rst b/docs/index.rst index dfca444d04..2a1c5b6abc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -334,18 +334,20 @@ Memory .. function:: virtual_memory() Return statistics about system memory usage as a named tuple including the - following fields, expressed in bytes. Main metrics: + following fields, expressed in bytes. + + Main metrics: - **total**: total physical memory (exclusive swap). - **available**: the memory that can be given instantly to processes without the system going into swap. - This is calculated by summing different memory values depending on the - platform and it is supposed to be used to monitor actual memory usage in a - cross platform fashion. + This is calculated by summing different memory metrics that vary depending + on the platform. It is supposed to be used to monitor actual memory usage + in a cross platform fashion. + - **percent**: the percentage usage calculated as ``(total - available) / total * 100``. Other metrics: - - **percent**: the percentage usage calculated as ``(total - available) / total * 100`` - **used**: memory used, calculated differently depending on the platform and designed for informational purposes only. **total - free** does not necessarily match **used**. @@ -370,7 +372,8 @@ Memory human readable form. .. note:: if you just want to know how much physical memory is left in a - cross platform fashion simply rely on the **available** field. + cross platform fashion simply rely on **available** and **percent** + fields. >>> import psutil >>> mem = psutil.virtual_memory() diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index c1cc1c035e..9dd8420d71 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -176,11 +176,10 @@ # ===================================================================== -if NETBSD: - def virtual_memory(): - """System virtual memory as a namedtuple.""" - mem = cext.virtual_mem() - total, free, active, inactive, wired, cached, avail = mem +def virtual_memory(): + mem = cext.virtual_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: @@ -189,22 +188,28 @@ def virtual_memory(): buffers = int(line.split()[1]) * 1024 elif line.startswith(b'MemShared:'): shared = int(line.split()[1]) * 1024 - elif line.startswith(b'Cached:'): - cached = int(line.split()[1]) * 1024 - 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) -else: - def virtual_memory(): - """System virtual memory as a namedtuple.""" - mem = cext.virtual_mem() + # 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) + + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, used, free, + active, inactive, buffers, cached, shared, wired) def swap_memory(): diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 26ab8e690a..456479ba1b 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -31,8 +31,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { size_t size; struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; - long pagesize = psutil_getpagesize(); - unsigned long long available; + long long cached; size = sizeof(uv); if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { @@ -40,17 +39,18 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return NULL; } - // follow zabbix - available = uv.inactive + uv.execpages + uv.filepages + uv.free; + // Note: zabbix does not include anonpages, but that doesn't match the + // "Cached" value in /proc/meminfo. + // https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L182 + cached = (uv.filepages + uv.execpages + uv.anonpages) << uv.pageshift; return Py_BuildValue( - "KKKKKKK", - (unsigned long long) uv.npages << uv.pageshift, // total - (unsigned long long) uv.free << uv.pageshift, // free - (unsigned long long) uv.active << uv.pageshift, // active - (unsigned long long) uv.inactive << uv.pageshift, // inactive - (unsigned long long) uv.wired << uv.pageshift, // wired - (unsigned long long) (uv.filepages + uv.execpages) * pagesize, // cached - available << uv.pageshift // available + "LLLLLL", + (long long) uv.npages << uv.pageshift, // total + (long long) uv.free << uv.pageshift, // free + (long long) uv.active << uv.pageshift, // active + (long long) uv.inactive << uv.pageshift, // inactive + (long long) uv.wired << uv.pageshift, // wired + cached // cached ); } From 19461905377ce3060b60df1949c082086492dab9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 14 Apr 2023 21:38:07 +0000 Subject: [PATCH 0966/1714] Fix #2236 / NetNBSD: skip terminated process threads Process threads() and num_threads() methods now skip threads which are in ZOMBIE or IDLE state. It turns out that after a thread is terminated / join()ed, instead of disappearing it can stick around in a ZOMBIE or IDLE state, presumably for a while before being garbage collected. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 2 ++ psutil/arch/netbsd/proc.c | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index d214e61ff4..324961bc86 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -37,6 +37,8 @@ - 2231_, [NetBSD]: *available* `virtual_memory()`_ is higher than *total*. - 2234_, [NetBSD]: `virtual_memory()`_ metrics are wrong: *available* and *used* are too high. We now match values shown by *htop* CLI utility. +- 2236_, [NetBSD]: `Process.num_threads()`_ and `Process.threads()`_ return + threads that are already terminated. 5.9.4 ===== diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index b87473a6a6..e71afb388f 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -247,6 +247,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { nlwps = (int)(size / sizeof(struct kinfo_lwp)); for (i = 0; i < nlwps; i++) { + if ((&kl[i])->l_stat == LSIDL || (&kl[i])->l_stat == LSZOMB) + continue; + // XXX: we return 2 "user" times because the struct does not provide + // any "system" time. py_tuple = Py_BuildValue("idd", (&kl[i])->l_lid, PSUTIL_KPT2DOUBLE((&kl[i])->l_rtime), From b1c1a005e5919291abc2edb5e6611806bce3346b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Apr 2023 01:48:10 +0200 Subject: [PATCH 0967/1714] BSD: refactor py code related to filtering connections --- psutil/_psbsd.py | 60 +++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 9dd8420d71..8fc256b215 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -423,25 +423,20 @@ def net_connections(kind): if OPENBSD: rawlist = cext.net_connections(-1, families, types) - ret = set() - for item in rawlist: - fd, fam, type, laddr, raddr, status, pid = item - if fam in families and type in types: - nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, - TCP_STATUSES, pid) - ret.add(nt) - return list(ret) elif NETBSD: rawlist = cext.net_connections(-1) - else: + else: # FreeBSD rawlist = cext.net_connections() + 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: - nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, - TCP_STATUSES, pid) - ret.add(nt) + if NETBSD or FREEBSD: + # OpenBSD implements filtering in C + if (fam not in families) or (type not in types): + continue + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, + status, TCP_STATUSES, pid) + ret.add(nt) return list(ret) @@ -785,45 +780,26 @@ def connections(self, kind='inet'): 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: - ret = [] 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: - nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, - TCP_STATUSES) - ret.append(nt) - self._assert_alive() - return ret - elif OPENBSD: - ret = [] rawlist = cext.net_connections(self.pid, families, types) - for item in rawlist: - fd, fam, type, laddr, raddr, status, pid = item - assert pid == self.pid - if fam in families and type in types: - nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, - TCP_STATUSES) - ret.append(nt) - self._assert_alive() - return ret + else: # FreeBSD + rawlist = cext.proc_connections(self.pid, families, types) - # FreeBSD - rawlist = cext.proc_connections(self.pid, families, types) - ret = [] for item in rawlist: - fd, fam, type, laddr, raddr, status = item + fd, fam, type, laddr, raddr, status = item[:6] + if NETBSD: + # FreeBSD and OpenBSD implement filtering in C + 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: - self._assert_alive() - + self._assert_alive() return ret @wrap_exceptions From b070015104ea01689fee9f7c91709c0e2d35a9a8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 16 Apr 2023 02:31:37 +0200 Subject: [PATCH 0968/1714] Fix #2237, OpenBSD, cwd(): return None instead of FileNotFoundError --- HISTORY.rst | 2 ++ psutil/arch/openbsd/proc.c | 10 ++++++++-- psutil/tests/test_contracts.py | 3 +-- psutil/tests/test_system.py | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 324961bc86..d946381798 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -39,6 +39,8 @@ *used* are too high. We now match values shown by *htop* CLI utility. - 2236_, [NetBSD]: `Process.num_threads()`_ and `Process.threads()`_ return threads that are already terminated. +- 2237_, [OpenBSD]: `Process.cwd()`_ may raise ``FileNotFoundError`` if cwd no + longer exists. Return ``None`` instead. 5.9.4 ===== diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 344d6010ec..e66cac667b 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -305,8 +305,14 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { int name[] = { CTL_KERN, KERN_PROC_CWD, pid }; if (sysctl(name, 3, path, &pathlen, NULL, 0) != 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; + if (errno == ENOENT) { + psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to None"); + Py_RETURN_NONE; // mimic os.cpu_count() + } + else { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } } return PyUnicode_DecodeFSDefault(path); } diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 392eb69b2f..6859f69fd8 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -438,8 +438,7 @@ def test_all(self): name, info['pid'], repr(value)) s += '-' * 70 s += "\n%s" % traceback.format_exc() - s = "\n".join((" " * 4) + i for i in s.splitlines()) - s += '\n' + s = "\n".join((" " * 4) + i for i in s.splitlines()) + "\n" failures.append(s) else: if value not in (0, 0.0, [], None, '', {}): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index c2b1df8473..414c86e99b 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -187,7 +187,7 @@ def test_pid_exists_2(self): # if it is no longer in psutil.pids() time.sleep(.1) self.assertNotIn(pid, psutil.pids()) - pids = range(max(pids) + 5000, max(pids) + 6000) + pids = range(max(pids) + 15000, max(pids) + 16000) for pid in pids: self.assertFalse(psutil.pid_exists(pid), msg=pid) From 0e3f6c465181734181f2428b4a99429a82f3fc45 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 17 Apr 2023 01:01:44 +0200 Subject: [PATCH 0969/1714] fix #2238 if cwd() cannot be determined always return "" instead of None --- HISTORY.rst | 8 ++++++-- docs/index.rst | 11 +++++++---- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 2 +- psutil/_pssunos.py | 2 +- psutil/arch/netbsd/proc.c | 9 ++++++--- psutil/arch/openbsd/proc.c | 4 ++-- psutil/tests/test_contracts.py | 16 +++++++++------- psutil/tests/test_posix.py | 7 +------ 9 files changed, 34 insertions(+), 27 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d946381798..97c2c20e22 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,10 @@ empty string) - The function is faster since it no longer iterates over all processes. - No longer produces duplicate connection entries. +- 2238_: there are cases where `Process.cwd()`_ cannot be determined + (e.g. directory no longer exists), in which case we returned either ``None`` + or an empty string. This was consolidated and we now return ``""`` on all + platforms. **Bug fixes** @@ -39,8 +43,8 @@ *used* are too high. We now match values shown by *htop* CLI utility. - 2236_, [NetBSD]: `Process.num_threads()`_ and `Process.threads()`_ return threads that are already terminated. -- 2237_, [OpenBSD]: `Process.cwd()`_ may raise ``FileNotFoundError`` if cwd no - longer exists. Return ``None`` instead. +- 2237_, [OpenBSD], [NetBSD]: `Process.cwd()`_ may raise ``FileNotFoundError`` + if cwd no longer exists. Return an empty string instead. 5.9.4 ===== diff --git a/docs/index.rst b/docs/index.rst index 2a1c5b6abc..6f0132d2c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1180,9 +1180,10 @@ Process class .. method:: exe() - The process executable as an absolute path. - On some systems this may also be an empty string. - The return value is cached after first call. + The process executable as an absolute path. On some systems, if exe cannot + be determined for some internal reason (e.g. system process or path no + longer exists), this may be an empty string. The return value is cached + after first call. >>> import psutil >>> psutil.Process().exe() @@ -1281,7 +1282,9 @@ Process class .. method:: cwd() - The process current working directory as an absolute path. + The process current working directory as an absolute path. If cwd cannot be + determined for some internal reason (e.g. system process or directiory no + longer exists) it may return an empty string. .. versionchanged:: 5.6.4 added support for NetBSD diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 5c41069cff..67f0314f74 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -487,7 +487,7 @@ def cwd(self): return result.rstrip('/') except FileNotFoundError: os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD - return None + return "" @wrap_exceptions def memory_info(self): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 8fc256b215..b77a9d6888 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -839,7 +839,7 @@ def cwd(self): 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) or "" else: raise NotImplementedError( "supported only starting from FreeBSD 8" if diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 8663de3cd3..d44bf2d788 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -542,7 +542,7 @@ def cwd(self): return os.readlink("%s/%s/path/cwd" % (procfs_path, self.pid)) except FileNotFoundError: os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD - return None + return "" @wrap_exceptions def memory_info(self): diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index e71afb388f..2688dceda1 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -115,10 +115,13 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { ssize_t len = readlink(buf, path, sizeof(path) - 1); free(buf); if (len == -1) { - if (errno == ENOENT) - NoSuchProcess("readlink -> ENOENT"); - else + if (errno == ENOENT) { + psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); + return Py_BuildValue("", ""); + } + else { PyErr_SetFromErrno(PyExc_OSError); + } return NULL; } path[len] = '\0'; diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index e66cac667b..38538a4a9e 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -306,8 +306,8 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { int name[] = { CTL_KERN, KERN_PROC_CWD, pid }; if (sysctl(name, 3, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) { - psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to None"); - Py_RETURN_NONE; // mimic os.cpu_count() + psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); + return Py_BuildValue("", ""); } else { PyErr_SetFromErrno(PyExc_OSError); diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 6859f69fd8..7f299a6243 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -452,10 +452,9 @@ def cmdline(self, ret, info): self.assertIsInstance(part, str) def exe(self, ret, info): - self.assertIsInstance(ret, (str, unicode, type(None))) - if not ret: - self.assertEqual(ret, '') - else: + 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 @@ -520,7 +519,8 @@ def gids(self, ret, info): def username(self, ret, info): self.assertIsInstance(ret, str) - assert ret + self.assertEqual(ret.strip(), ret) + assert ret.strip() def status(self, ret, info): self.assertIsInstance(ret, str) @@ -619,6 +619,7 @@ def open_files(self, ret, info): 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: @@ -651,8 +652,9 @@ def connections(self, ret, info): check_connection_ntuple(conn) def cwd(self, ret, info): - if ret: # 'ret' can be None or empty - self.assertIsInstance(ret, str) + self.assertIsInstance(ret, (str, unicode)) + self.assertEqual(ret.strip(), ret) + if ret: assert os.path.isabs(ret), ret try: st = os.stat(ret) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index d26312151b..8ea6cf7c23 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -351,12 +351,7 @@ def test_users(self): self.assertEqual(u.name, users[idx]) self.assertEqual(u.terminal, terminals[idx]) if u.pid is not None: # None on OpenBSD - p = psutil.Process(u.pid) - # on macOS time is off by ~47 secs for some reason, but - # the next test against 'who' CLI succeeds - delta = 60 if MACOS else 1 - self.assertAlmostEqual( - u.started, p.create_time(), delta=delta) + psutil.Process(u.pid) @retry_on_failure() def test_users_started(self): From 0a81fa089fd4b25b4b7ee71ed39213b83f73c052 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 17 Apr 2023 01:04:20 +0200 Subject: [PATCH 0970/1714] #2238: passed wrong value to Py_BuildValue Signed-off-by: Giampaolo Rodola --- psutil/arch/netbsd/proc.c | 2 +- psutil/arch/openbsd/proc.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 2688dceda1..d4fa212668 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -117,7 +117,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { if (len == -1) { if (errno == ENOENT) { psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); - return Py_BuildValue("", ""); + return Py_BuildValue("s", ""); } else { PyErr_SetFromErrno(PyExc_OSError); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 38538a4a9e..285467bf65 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -307,7 +307,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { if (sysctl(name, 3, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) { psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); - return Py_BuildValue("", ""); + return Py_BuildValue("s", ""); } else { PyErr_SetFromErrno(PyExc_OSError); From aa42066eacc5b1a2135d1737d16138de3511868a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 17 Apr 2023 02:51:06 +0200 Subject: [PATCH 0971/1714] Fix #2239 / proc name(): don't fail with ZombieProcess on cmdline() A recent failure observed on OpenBSD led me to an interesting consideration. ``` ====================================================================== ERROR: psutil.tests.test_process.TestProcess.test_long_name ---------------------------------------------------------------------- Traceback (most recent call last): File "/vagrant/psutil/psutil/_psbsd.py", line 566, in wrapper return fun(self, *args, **kwargs) File "/vagrant/psutil/psutil/_psbsd.py", line 684, in cmdline return cext.proc_cmdline(self.pid) ProcessLookupError: [Errno 3] No such process During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/vagrant/psutil/psutil/tests/test_process.py", line 751, in test_long_name self.assertEqual(p.name(), os.path.basename(testfn)) File "/vagrant/psutil/psutil/__init__.py", line 628, in name cmdline = self.cmdline() File "/vagrant/psutil/psutil/__init__.py", line 681, in cmdline return self._proc.cmdline() File "/vagrant/psutil/psutil/_psbsd.py", line 569, in wrapper raise ZombieProcess(self.pid, self._name, self._ppid) psutil.ZombieProcess: PID still exists but it's a zombie (pid=48379) ---------------------------------------------------------------------- ``` The exception above occurs sporadically. It originates from `sysctl (KERN_PROC_ARGV)`: https://github.com/giampaolo/psutil/blob/0a81fa089fd4b25b4b7ee71ed39213b83f73c052/psutil/arch/openbsd/proc.c#L149 The error per se does not represent a bug in the OpenBSD `cmdline ()` implemention because the process **really** is a zombie at that point (I'm not sure why it's a zombie - this seems only to occur only on OpenBSD for this specific test case - but that's not the point). The interesting thing is that the test calls process `name()` (which succeeds, despite it's a zombie process), but since the process name is too long it gets truncated to 15 chars (this is a UNIX thing) so psutil tries to guess the remaining characters from the process `cmdline()`, which fails: https://github.com/giampaolo/psutil/blob/0a81fa089fd4b25b4b7ee71ed39213b83f73c052/psutil/__init__.py#L623-L630 The problem to fix here is that, if `name()` succeeds but `cmdline()` fails, we should not raise `ZombieProcess`: we should simply return the (truncated) process `name()` instead, because that is better than nothing. Not on OpenBSD but on all platforms. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 4 ++++ psutil/__init__.py | 7 ++++++- psutil/tests/test_process.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 97c2c20e22..003dad2399 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,10 @@ (e.g. directory no longer exists), in which case we returned either ``None`` or an empty string. This was consolidated and we now return ``""`` on all platforms. +- 2239_, [UNIX]: if process is a zombie, and we can only determine part of the + its truncated `Process.name()`_ (15 chars), don't fail with `ZombieProcess`_ + when we try to guess the full name from the `Process.cmdline()`_. Just + return the truncated name. **Bug fixes** diff --git a/psutil/__init__.py b/psutil/__init__.py index 6036cbe945..2b0b3c6b00 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -626,7 +626,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: diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ec15ffda53..a67baa72e5 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -732,7 +732,15 @@ def test_long_cmdline(self): create_exe(testfn) cmdline = [testfn] + (["0123456789"] * 20) p = self.spawn_psproc(cmdline) - self.assertEqual(p.cmdline(), 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(PYTHON_EXE) @@ -745,7 +753,23 @@ def test_long_name(self): testfn = self.get_testfn(suffix="0123456789" * 2) create_exe(testfn) p = self.spawn_psproc(testfn) - self.assertEqual(p.name(), os.path.basename(testfn)) + 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(testfn)) + except AssertionError: + if p.status() == psutil.STATUS_ZOMBIE: + assert os.path.basename(testfn).startswith(p.name()) + else: + raise + else: + self.assertEqual(p.name(), os.path.basename(testfn)) # XXX @unittest.skipIf(SUNOS, "broken on SUNOS") From 190059c69d0aa01b3f17ba5191d0a6e6d9669841 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 17 Apr 2023 19:58:47 +0200 Subject: [PATCH 0972/1714] Add CI testing for OpenBSD and NetBSD (#2240) --- .github/workflows/bsd.yml | 67 ++++++++++++++++++++++++++++++++++++ .github/workflows/build.yml | 44 +++-------------------- HISTORY.rst | 2 ++ INSTALL.rst | 10 +++--- README.rst | 14 +++++--- psutil/_psbsd.py | 4 +-- psutil/tests/test_process.py | 1 + 7 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/bsd.yml diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml new file mode 100644 index 0000000000..9c811d183f --- /dev/null +++ b/.github/workflows/bsd.yml @@ -0,0 +1,67 @@ +# Execute tests on *BSD platforms. Does not produce wheels. +# Useful URLs: +# https://github.com/vmactions/freebsd-vm +# https://github.com/vmactions/openbsd-vm +# https://github.com/vmactions/netbsd-vm + +on: [push, pull_request] +name: bsd-tests +concurrency: + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} + cancel-in-progress: true +jobs: + freebsd: + runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + - name: Run tests + uses: vmactions/freebsd-vm@v0 + with: + usesh: true + prepare: | + pkg install -y gcc python3 + run: | + set -e -x + make install-pip + python3 -m pip install --user setuptools + make install + make test + make test-memleaks + openbsd: + runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + - name: Run tests + uses: vmactions/openbsd-vm@v0 + with: + usesh: true + prepare: | + set -e + pkg_add gcc python3 + run: | + set -e + make install-pip + python3 -m pip install --user setuptools + make install + make test + make test-memleaks + netbsd: + runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + - name: Run tests + uses: vmactions/netbsd-vm@v0 + with: + usesh: true + prepare: | + set -e + pkg_add -v pkgin + pkgin update + pkgin -y install python311-* py311-setuptools-* gcc12-* + run: | + set -e + make install-pip PYTHON=python3.11 + python3.11 -m pip install --user setuptools + make install PYTHON=python3.11 + make test PYTHON=python3.11 + make test-memleaks PYTHON=python3.11 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb6996daa0..d735a0ceef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,21 +1,15 @@ -# Executed on every push by GitHub Actions. This runs CI tests and -# generates wheels (not all) on the following platforms: +# Runs CI tests and generates wheels on the following platforms: # -# * Linux -# * macOS -# * Windows (disabled) -# * FreeBSD +# * Linux (py2 and py3) +# * macOS (py2 and py3) +# * Windows (py3, py2 is done by appveyor) # -# To skip certain builds see: -# https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip -# -# External GH actions: +# Useful URLs: # * https://github.com/pypa/cibuildwheel # * https://github.com/actions/checkout # * https://github.com/actions/setup-python # * https://github.com/actions/upload-artifact # * https://github.com/marketplace/actions/cancel-workflow-action -# * https://github.com/vmactions/freebsd-vm on: [push, pull_request] name: build @@ -110,34 +104,6 @@ jobs: python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - # FreeBSD (tests only) - py3-freebsd: - runs-on: macos-12 - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v3 - - - name: Run tests - id: test - uses: vmactions/freebsd-vm@v0 - with: - usesh: true - prepare: pkg install -y gcc python3 - run: | - set +e - export \ - PYTHONUNBUFFERED=1 \ - PYTHONWARNINGS=always \ - PSUTIL_DEBUG=1 - python3 -m pip install --user setuptools - python3 setup.py install - python3 psutil/tests/runner.py - python3 psutil/tests/test_memleaks.py - # Run linters linters: runs-on: ubuntu-latest diff --git a/HISTORY.rst b/HISTORY.rst index 003dad2399..fec64c0b2e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,6 +23,8 @@ its truncated `Process.name()`_ (15 chars), don't fail with `ZombieProcess`_ when we try to guess the full name from the `Process.cmdline()`_. Just return the truncated name. +- 2240_, [NetBSD], [OpenBSD]: add CI testing on every commit for NetBSD and + OpenBSD platforms (python 3 only). **Bug fixes** diff --git a/INSTALL.rst b/INSTALL.rst index 4f5831e67b..2e8a1cd512 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -62,18 +62,20 @@ OpenBSD :: export PKG_PATH=http://ftp.eu.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ - pkg_add -v python gcc + pkg_add -v python3 gcc pip install psutil NetBSD ------ +Assuming Python 3.11 (the most recent at the time of writing): + :: - export PKG_PATH="ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" + export PKG_PATH="http://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" pkg_add -v pkgin - pkgin install python3 gcc - pip install psutil + pkgin install python311-* gcc12-* py311-setuptools-* py311-pip-* + python3.11 -m pip install psutil Sun Solaris ----------- diff --git a/README.rst b/README.rst index 361b0f0182..2e991cb354 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ | |downloads| |stars| |forks| |contributors| |coverage| | |version| |py-versions| |packages| |license| -| |github-actions| |appveyor| |doc| |twitter| |tidelift| +| |github-actions-wheels| |github-actions-bsd| |appveyor| |doc| |twitter| |tidelift| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil @@ -18,13 +18,17 @@ :target: https://github.com/giampaolo/psutil/graphs/contributors :alt: Contributors -.. |github-actions| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml?label=Linux%2C%20macOS%2C%20FreeBSD%20tests +.. |github-actions-wheels| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml?label=Linux%2C%20macOS%2C%20Windows :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild - :alt: Linux, macOS, Windows tests + :alt: Linux, macOS, Windows -.. |appveyor| image:: https://img.shields.io/appveyor/build/giampaolo/psutil/master.svg?maxAge=3600&label=Windows%20tests +.. |github-actions-bsd| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/bsd.yml?label=FreeBSD,%20NetBSD,%20OpenBSD + :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests + :alt: FreeBSD, NetBSD, OpenBSD + +.. |appveyor| image:: https://img.shields.io/appveyor/build/giampaolo/psutil/master.svg?maxAge=3600&label=Windows%20(py2) :target: https://ci.appveyor.com/project/giampaolo/psutil - :alt: Windows tests (Appveyor) + :alt: Windows (Appveyor) .. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master :target: https://coveralls.io/github/giampaolo/psutil?branch=master diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index b77a9d6888..99808bd289 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -835,11 +835,11 @@ 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 + 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 "" + return cext.proc_cwd(self.pid) else: raise NotImplementedError( "supported only starting from FreeBSD 8" if diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a67baa72e5..b3cf9ff54f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -159,6 +159,7 @@ def test_wait_exited(self): self.assertEqual(code, 5) self.assertProcessGone(p) + @unittest.skipIf(NETBSD, "fails on NETBSD") def test_wait_stopped(self): p = self.spawn_psproc() if POSIX: From 0d4900b073f8697ab21c47d823621bea61f39a3b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 17 Apr 2023 20:24:02 +0200 Subject: [PATCH 0973/1714] pre release Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 6 ++++-- docs/index.rst | 18 +++++++++++++++--- scripts/internal/print_timeline.py | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index fec64c0b2e..fb74f36745 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.5 (IN DEVELOPMENT) -====================== +5.9.5 +===== + +2023-04-17 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 6f0132d2c8..344aecdf02 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2637,6 +2637,18 @@ Supported Python versions are 2.7, 3.4+ and PyPy3. Timeline ======== +- 2023-04-17: + `5.9.5 `__ - + `what's new `__ - + `diff `__ +- 2022-11-07: + `5.9.4 `__ - + `what's new `__ - + `diff `__ +- 2022-10-18: + `5.9.3 `__ - + `what's new `__ - + `diff `__ - 2022-09-04: `5.9.2 `__ - `what's new `__ - @@ -2653,7 +2665,7 @@ Timeline `5.8.0 `__ - `what's new `__ - `diff `__ -- 2020-10-23: +- 2020-10-24: `5.7.3 `__ - `what's new `__ - `diff `__ @@ -2721,7 +2733,7 @@ Timeline `5.4.6 `__ - `what's new `__ - `diff `__ -- 2018-04-14: +- 2018-04-13: `5.4.5 `__ - `what's new `__ - `diff `__ @@ -2747,7 +2759,7 @@ Timeline `diff `__ - 2017-09-10: `5.3.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-09-01: `5.3.0 `__ - diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index 64608b26ba..0ea7355eb3 100755 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -13,7 +13,7 @@ entry = """\ - {date}: - `{ver} `__ - + `{ver} `__ - `what's new `__ - `diff `__""" # NOQA From efd7ed3d2c4aca57226572b2a81e5d7ebb9f3b8b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Apr 2023 13:05:04 +0200 Subject: [PATCH 0974/1714] C refact: remove useless cmdline / cwd / environ layers. Call direct functions --- psutil/_psutil_bsd.c | 16 ------ psutil/_psutil_osx.c | 40 -------------- psutil/_psutil_windows.c | 73 ------------------------- psutil/arch/freebsd/proc.c | 64 +++++++--------------- psutil/arch/freebsd/proc.h | 2 +- psutil/arch/netbsd/proc.c | 74 +++++++++---------------- psutil/arch/netbsd/proc.h | 9 ++- psutil/arch/openbsd/proc.c | 48 ++++++++-------- psutil/arch/openbsd/proc.h | 2 +- psutil/arch/osx/process_info.c | 24 +++++--- psutil/arch/osx/process_info.h | 5 +- psutil/arch/windows/process_info.c | 88 ++++++++++++++++++++++++------ psutil/arch/windows/process_info.h | 6 +- psutil/tests/test_memleaks.py | 0 14 files changed, 170 insertions(+), 281 deletions(-) mode change 100755 => 100644 psutil/tests/test_memleaks.py diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index ff5fd72da9..dfebf9ff94 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -380,22 +380,6 @@ psutil_proc_name(PyObject *self, PyObject *args) { } -/* - * Return process cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args) { - pid_t pid; - PyObject *py_retlist = NULL; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - py_retlist = psutil_get_cmdline(pid); - if (py_retlist == NULL) - return NULL; - return Py_BuildValue("N", py_retlist); -} - /* * Return process environment as a Python dictionary diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 9ba0fd2b7e..713f3d6c8e 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -386,46 +386,6 @@ psutil_proc_exe(PyObject *self, PyObject *args) { } -/* - * Return process cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args) { - pid_t pid; - PyObject *py_retlist = NULL; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - // get the commandline, defined in arch/osx/process_info.c - py_retlist = psutil_get_cmdline(pid); - return py_retlist; -} - - -/* - * Return process environment as a Python string. - * On Big Sur this function returns an empty string unless: - * * kernel is DEVELOPMENT || DEBUG - * * target process is same as current_proc() - * * target process is not cs_restricted - * * SIP is off - * * caller has an entitlement - */ -static PyObject * -psutil_proc_environ(PyObject *self, PyObject *args) { - pid_t pid; - PyObject *py_str = NULL; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - // get the environment block, defined in arch/osx/process_info.c - py_str = psutil_get_environ(pid); - return py_str; -} - - /* * Indicates if the given virtual address on the given architecture is in the * shared VM region. diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 11176de738..8e51c0bdcd 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -276,59 +276,6 @@ psutil_proc_times(PyObject *self, PyObject *args) { } -/* - * Return process cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { - DWORD pid; - int pid_return; - int use_peb; - PyObject *py_usepeb = Py_True; - static char *keywords[] = {"pid", "use_peb", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwdict, _Py_PARSE_PID "|O", - keywords, &pid, &py_usepeb)) - { - return NULL; - } - if ((pid == 0) || (pid == 4)) - return Py_BuildValue("[]"); - - pid_return = psutil_pid_is_running(pid); - if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running -> 0"); - if (pid_return == -1) - return NULL; - - use_peb = (py_usepeb == Py_True) ? 1 : 0; - return psutil_get_cmdline(pid, use_peb); -} - - -/* - * Return process cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_environ(PyObject *self, PyObject *args) { - DWORD pid; - int pid_return; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if ((pid == 0) || (pid == 4)) - return Py_BuildValue("s", ""); - - pid_return = psutil_pid_is_running(pid); - if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running -> 0"); - if (pid_return == -1) - return NULL; - - return psutil_get_environ(pid); -} - - /* * Return process executable path. Works for all processes regardless of * privilege. NtQuerySystemInformation has some sort of internal cache, @@ -604,26 +551,6 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { return Py_BuildValue("I", wsCounters.NumberOfPrivatePages); } -/* - * Return process current working directory as a Python string. - */ -static PyObject * -psutil_proc_cwd(PyObject *self, PyObject *args) { - DWORD pid; - int pid_return; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - pid_return = psutil_pid_is_running(pid); - if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running -> 0"); - if (pid_return == -1) - return NULL; - - return psutil_get_cwd(pid); -} - /* * Resume or suspends a process diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 214dbc4822..6528ece452 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -135,24 +135,24 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { /* - * XXX no longer used; it probably makese sense to remove it. * Borrowed from psi Python System Information project - * - * Get command arguments and environment variables. - * * Based on code from ps. - * - * Returns: - * 0 for success; - * -1 for failure (Exception raised); - * 1 for insufficient privileges. */ -static char -*psutil_get_cmd_args(pid_t pid, size_t *argsize) { +PyObject * +psutil_proc_cmdline(PyObject *self, PyObject *args) { + pid_t pid; int mib[4]; int argmax; size_t size = sizeof(argmax); char *procargs = NULL; + size_t pos = 0; + PyObject *py_retlist = PyList_New(0); + PyObject *py_arg = NULL; + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; // Get the maximum process arguments size. mib[0] = CTL_KERN; @@ -160,13 +160,13 @@ static char size = sizeof(argmax); if (sysctl(mib, 2, &argmax, &size, NULL, 0) == -1) - return NULL; + goto error; // Allocate space for the arguments. procargs = (char *)malloc(argmax); if (procargs == NULL) { PyErr_NoMemory(); - return NULL; + goto error; } // Make a sysctl() call to get the raw argument space of the process. @@ -177,55 +177,33 @@ static char size = argmax; if (sysctl(mib, 4, procargs, &size, NULL, 0) == -1) { - free(procargs); PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGS)"); - return NULL; - } - - // return string and set the length of arguments - *argsize = size; - return procargs; -} - - -// returns the command line as a python list object -PyObject * -psutil_get_cmdline(pid_t pid) { - char *argstr = NULL; - size_t pos = 0; - size_t argsize = 0; - PyObject *py_retlist = Py_BuildValue("[]"); - PyObject *py_arg = NULL; - - if (pid < 0) - return py_retlist; - argstr = psutil_get_cmd_args(pid, &argsize); - if (argstr == NULL) goto error; + } // args are returned as a flattened string with \0 separators between // arguments add each string to the list then step forward to the next // separator - if (argsize > 0) { - while (pos < argsize) { - py_arg = PyUnicode_DecodeFSDefault(&argstr[pos]); + if (size > 0) { + while (pos < size) { + py_arg = PyUnicode_DecodeFSDefault(&procargs[pos]); if (!py_arg) goto error; if (PyList_Append(py_retlist, py_arg)) goto error; Py_DECREF(py_arg); - pos = pos + strlen(&argstr[pos]) + 1; + pos = pos + strlen(&procargs[pos]) + 1; } } - free(argstr); + free(procargs); return py_retlist; error: Py_XDECREF(py_arg); Py_DECREF(py_retlist); - if (argstr != NULL) - free(argstr); + if (procargs != NULL) + free(procargs); return NULL; } diff --git a/psutil/arch/freebsd/proc.h b/psutil/arch/freebsd/proc.h index 9c16f3cb9d..f24535eb7d 100644 --- a/psutil/arch/freebsd/proc.h +++ b/psutil/arch/freebsd/proc.h @@ -11,7 +11,7 @@ typedef struct kinfo_proc kinfo_proc; int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); -PyObject* psutil_get_cmdline(long pid); +PyObject* psutil_proc_cmdline(PyObject* self, PyObject* args); PyObject* psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args); PyObject* psutil_proc_cpu_affinity_set(PyObject* self, PyObject* args); PyObject* psutil_proc_cwd(PyObject* self, PyObject* args); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index d4fa212668..1b05d61adb 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -187,6 +187,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { } */ + PyObject * psutil_proc_num_threads(PyObject *self, PyObject *args) { // Return number of threads used by process as a Python integer. @@ -199,6 +200,7 @@ psutil_proc_num_threads(PyObject *self, PyObject *args) { return Py_BuildValue("l", (long)kp.p_nlwps); } + PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { pid_t pid; @@ -328,86 +330,64 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { } -char * -psutil_get_cmd_args(pid_t pid, size_t *argsize) { +PyObject * +psutil_proc_cmdline(PyObject *self, PyObject *args) { + pid_t pid; int mib[4]; int st; - size_t len; - char *procargs; + size_t len = 0; + size_t pos = 0; + char *procargs = NULL; + PyObject *py_retlist = PyList_New(0); + PyObject *py_arg = NULL; + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; mib[0] = CTL_KERN; mib[1] = KERN_PROC_ARGS; mib[2] = pid; mib[3] = KERN_PROC_ARGV; - len = 0; st = sysctl(mib, __arraycount(mib), NULL, &len, NULL, 0); if (st == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV) get size"); + goto error; } procargs = (char *)malloc(len); if (procargs == NULL) { PyErr_NoMemory(); - return NULL; + goto error; } st = sysctl(mib, __arraycount(mib), procargs, &len, NULL, 0); if (st == -1) { - free(procargs); - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - *argsize = len; - return procargs; -} - - -// Return the command line as a python list object. -// XXX - most of the times sysctl() returns a truncated string. -// Also /proc/pid/cmdline behaves the same so it looks like this -// is a kernel bug. -PyObject * -psutil_get_cmdline(pid_t pid) { - char *argstr = NULL; - size_t pos = 0; - size_t argsize = 0; - PyObject *py_arg = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - if (pid == 0) - return py_retlist; - - argstr = psutil_get_cmd_args(pid, &argsize); - if (argstr == NULL) + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV)"); goto error; + } - // args are returned as a flattened string with \0 separators between - // arguments add each string to the list then step forward to the next - // separator - if (argsize > 0) { - while (pos < argsize) { - py_arg = PyUnicode_DecodeFSDefault(&argstr[pos]); + if (len > 0) { + while (pos < len) { + py_arg = PyUnicode_DecodeFSDefault(&procargs[pos]); if (!py_arg) goto error; if (PyList_Append(py_retlist, py_arg)) goto error; Py_DECREF(py_arg); - pos = pos + strlen(&argstr[pos]) + 1; + pos = pos + strlen(&procargs[pos]) + 1; } } - free(argstr); + free(procargs); return py_retlist; error: Py_XDECREF(py_arg); Py_DECREF(py_retlist); - if (argstr != NULL) - free(argstr); + if (procargs != NULL) + free(procargs); return NULL; } diff --git a/psutil/arch/netbsd/proc.h b/psutil/arch/netbsd/proc.h index b4c88851fc..8c51914d7e 100644 --- a/psutil/arch/netbsd/proc.h +++ b/psutil/arch/netbsd/proc.h @@ -14,11 +14,10 @@ struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); char *psutil_get_cmd_args(pid_t pid, size_t *argsize); -// -PyObject *psutil_get_cmdline(pid_t pid); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); PyObject* psutil_proc_exe(PyObject* self, PyObject* args); PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); -PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 285467bf65..5c984fc536 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -143,45 +143,42 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { // TODO: refactor this (it's clunky) -static char ** -_psutil_get_argv(pid_t pid) { +PyObject * +psutil_proc_cmdline(PyObject *self, PyObject *args) { + pid_t pid; + int mib[4]; static char **argv; - int argv_mib[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV}; + char **p; size_t argv_size = 128; + PyObject *py_retlist = PyList_New(0); + PyObject *py_arg = NULL; + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC_ARGS; + mib[2] = pid; + mib[3] = KERN_PROC_ARGV; + // Loop and reallocate until we have enough space to fit argv. for (;; argv_size *= 2) { if (argv_size >= 8192) { PyErr_SetString(PyExc_RuntimeError, "can't allocate enough space for KERN_PROC_ARGV"); - return NULL; + goto error; } if ((argv = realloc(argv, argv_size)) == NULL) continue; - if (sysctl(argv_mib, 4, argv, &argv_size, NULL, 0) == 0) - return argv; + if (sysctl(mib, 4, argv, &argv_size, NULL, 0) == 0) + break; if (errno == ENOMEM) continue; PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } -} - - -// returns the command line as a python list object -PyObject * -psutil_get_cmdline(pid_t pid) { - static char **argv; - char **p; - PyObject *py_arg = NULL; - PyObject *py_retlist = Py_BuildValue("[]"); - - if (!py_retlist) - return NULL; - if (pid < 0) - return py_retlist; - - if ((argv = _psutil_get_argv(pid)) == NULL) goto error; + } for (p = argv; *p != NULL; p++) { py_arg = PyUnicode_DecodeFSDefault(*p); @@ -191,6 +188,7 @@ psutil_get_cmdline(pid_t pid) { goto error; Py_DECREF(py_arg); } + return py_retlist; error: diff --git a/psutil/arch/openbsd/proc.h b/psutil/arch/openbsd/proc.h index 747507dd45..a577e5f1c2 100644 --- a/psutil/arch/openbsd/proc.h +++ b/psutil/arch/openbsd/proc.h @@ -14,7 +14,7 @@ struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); char **_psutil_get_argv(pid_t pid); -PyObject *psutil_get_cmdline(pid_t pid); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c index 47330ea67d..4b98d92a60 100644 --- a/psutil/arch/osx/process_info.c +++ b/psutil/arch/osx/process_info.c @@ -161,7 +161,8 @@ psutil_is_zombie(pid_t pid) { // return process args as a python list PyObject * -psutil_get_cmdline(pid_t pid) { +psutil_proc_cmdline(PyObject *self, PyObject *args) { + pid_t pid; int nargs; size_t len; char *procargs = NULL; @@ -169,13 +170,17 @@ psutil_get_cmdline(pid_t pid) { char *arg_end; char *curr_arg; size_t argmax; - + PyObject *py_retlist = PyList_New(0); PyObject *py_arg = NULL; - PyObject *py_retlist = NULL; + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; // special case for PID 0 (kernel_task) where cmdline cannot be fetched if (pid == 0) - return Py_BuildValue("[]"); + return py_retlist; // read argmax and allocate memory for argument space. argmax = psutil_sysctl_argmax(); @@ -201,7 +206,7 @@ psutil_get_cmdline(pid_t pid) { if (arg_ptr == arg_end) { free(procargs); - return Py_BuildValue("[]"); + return py_retlist; } // skip ahead to the first argument @@ -212,9 +217,6 @@ psutil_get_cmdline(pid_t pid) { // iterate through arguments curr_arg = arg_ptr; - py_retlist = Py_BuildValue("[]"); - if (!py_retlist) - goto error; while (arg_ptr < arg_end && nargs > 0) { if (*arg_ptr++ == '\0') { py_arg = PyUnicode_DecodeFSDefault(curr_arg); @@ -250,7 +252,8 @@ psutil_get_cmdline(pid_t pid) { // * caller has an entitlement // See: https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/kern/kern_sysctl.c#L1315-L1321 PyObject * -psutil_get_environ(pid_t pid) { +psutil_proc_environ(PyObject *self, PyObject *args) { + pid_t pid; int nargs; char *procargs = NULL; char *procenv = NULL; @@ -260,6 +263,9 @@ psutil_get_environ(pid_t pid) { size_t argmax; PyObject *py_ret = NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + // special case for PID 0 (kernel_task) where cmdline cannot be fetched if (pid == 0) goto empty; diff --git a/psutil/arch/osx/process_info.h b/psutil/arch/osx/process_info.h index ffa6230f7c..08046bcb6f 100644 --- a/psutil/arch/osx/process_info.h +++ b/psutil/arch/osx/process_info.h @@ -13,5 +13,6 @@ int psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp); int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); int psutil_proc_pidinfo( pid_t pid, int flavor, uint64_t arg, void *pti, int size); -PyObject* psutil_get_cmdline(pid_t pid); -PyObject* psutil_get_environ(pid_t pid); + +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 1981d306a6..7a227d4d61 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -24,6 +24,13 @@ typedef NTSTATUS (NTAPI *__NtQueryInformationProcess)( PDWORD ReturnLength); #endif +#define PSUTIL_FIRST_PROCESS(Processes) ( \ + (PSYSTEM_PROCESS_INFORMATION)(Processes)) +#define PSUTIL_NEXT_PROCESS(Process) ( \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ + (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) + /* * Given a pointer into a process's memory, figure out how much @@ -535,15 +542,38 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { * with given pid or NULL on error. */ PyObject * -psutil_get_cmdline(DWORD pid, int use_peb) { - PyObject *ret = NULL; +psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { WCHAR *data = NULL; + LPWSTR *szArglist = NULL; SIZE_T size; + int nArgs; + int i; + int func_ret; + DWORD pid; + int pid_return; + int use_peb; + // TODO: shouldn't this be decref-ed in case of error on + // PyArg_ParseTuple? + PyObject *py_usepeb = Py_True; PyObject *py_retlist = NULL; PyObject *py_unicode = NULL; - LPWSTR *szArglist = NULL; - int nArgs, i; - int func_ret; + static char *keywords[] = {"pid", "use_peb", NULL}; + + if (! PyArg_ParseTupleAndKeywords(args, kwdict, _Py_PARSE_PID "|O", + keywords, &pid, &py_usepeb)) + { + return NULL; + } + if ((pid == 0) || (pid == 4)) + return Py_BuildValue("[]"); + + pid_return = psutil_pid_is_running(pid); + if (pid_return == 0) + return NoSuchProcess("psutil_pid_is_running -> 0"); + if (pid_return == -1) + return NULL; + + use_peb = (py_usepeb == Py_True) ? 1 : 0; /* Reading the PEB to get the cmdline seem to be the best method if @@ -559,47 +589,60 @@ psutil_get_cmdline(DWORD pid, int use_peb) { else func_ret = psutil_cmdline_query_proc(pid, &data, &size); if (func_ret != 0) - goto out; + goto error; // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(data, &nArgs); if (szArglist == NULL) { PyErr_SetFromOSErrnoWithSyscall("CommandLineToArgvW"); - goto out; + goto error; } // arglist parsed as array of UNICODE_STRING, so convert each to // Python string object and add to arg list py_retlist = PyList_New(nArgs); if (py_retlist == NULL) - goto out; + goto error; for (i = 0; i < nArgs; i++) { py_unicode = PyUnicode_FromWideChar(szArglist[i], wcslen(szArglist[i])); if (py_unicode == NULL) - goto out; + goto error; PyList_SetItem(py_retlist, i, py_unicode); py_unicode = NULL; } - ret = py_retlist; - py_retlist = NULL; -out: + LocalFree(szArglist); + free(data); + return py_retlist; + +error: if (szArglist != NULL) LocalFree(szArglist); if (data != NULL) free(data); Py_XDECREF(py_unicode); Py_XDECREF(py_retlist); - return ret; + return NULL; } PyObject * -psutil_get_cwd(DWORD pid) { +psutil_proc_cwd(PyObject *self, PyObject *args) { + DWORD pid; PyObject *ret = NULL; WCHAR *data = NULL; SIZE_T size; + int pid_return; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + pid_return = psutil_pid_is_running(pid); + if (pid_return == 0) + return NoSuchProcess("psutil_pid_is_running -> 0"); + if (pid_return == -1) + return NULL; if (psutil_get_process_data(pid, KIND_CWD, &data, &size) != 0) goto out; @@ -620,10 +663,23 @@ psutil_get_cwd(DWORD pid) { * process with given pid or NULL on error. */ PyObject * -psutil_get_environ(DWORD pid) { - PyObject *ret = NULL; +psutil_proc_environ(PyObject *self, PyObject *args) { + DWORD pid; WCHAR *data = NULL; SIZE_T size; + int pid_return; + PyObject *ret = NULL; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if ((pid == 0) || (pid == 4)) + return Py_BuildValue("s", ""); + + pid_return = psutil_pid_is_running(pid); + if (pid_return == 0) + return NoSuchProcess("psutil_pid_is_running -> 0"); + if (pid_return == -1) + return NULL; if (psutil_get_process_data(pid, KIND_ENVIRON, &data, &size) != 0) goto out; diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index 5e89ddebdf..26190427f3 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -15,7 +15,7 @@ int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); -PyObject* psutil_get_cmdline(DWORD pid, int use_peb); -PyObject* psutil_get_cwd(DWORD pid); -PyObject* psutil_get_environ(DWORD pid); +PyObject* psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict); +PyObject* psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject* psutil_proc_environ(PyObject *self, PyObject *args); PyObject* psutil_proc_info(PyObject *self, PyObject *args); diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py old mode 100755 new mode 100644 From 4a327a6183ac2d5d8a812357f239a244f16f8211 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Apr 2023 10:20:59 -0700 Subject: [PATCH 0975/1714] Windows / refact: new sensors.c file --- psutil/_psutil_windows.c | 26 +------------------------- psutil/arch/windows/sensors.c | 32 ++++++++++++++++++++++++++++++++ psutil/arch/windows/sensors.h | 9 +++++++++ setup.py | 3 ++- 4 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 psutil/arch/windows/sensors.c create mode 100644 psutil/arch/windows/sensors.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 8e51c0bdcd..d7ffbfe38c 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -35,6 +35,7 @@ #include "arch/windows/cpu.h" #include "arch/windows/mem.h" #include "arch/windows/net.h" +#include "arch/windows/sensors.h" #include "arch/windows/services.h" #include "arch/windows/socks.h" #include "arch/windows/wmi.h" @@ -1417,31 +1418,6 @@ psutil_ppid_map(PyObject *self, PyObject *args) { } -/* - * Return battery usage stats. - */ -static PyObject * -psutil_sensors_battery(PyObject *self, PyObject *args) { - SYSTEM_POWER_STATUS sps; - - if (GetSystemPowerStatus(&sps) == 0) { - PyErr_SetFromWindowsErr(0); - return NULL; - } - return Py_BuildValue( - "iiiI", - sps.ACLineStatus, // whether AC is connected: 0=no, 1=yes, 255=unknown - // status flag: - // 1, 2, 4 = high, low, critical - // 8 = charging - // 128 = no battery - sps.BatteryFlag, - sps.BatteryLifePercent, // percent - sps.BatteryLifeTime // remaining secs - ); -} - - // ------------------------ Python init --------------------------- static PyMethodDef diff --git a/psutil/arch/windows/sensors.c b/psutil/arch/windows/sensors.c new file mode 100644 index 0000000000..fbe2c2fe9f --- /dev/null +++ b/psutil/arch/windows/sensors.c @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include + + +// Added in https://github.com/giampaolo/psutil/commit/109f873 in 2017. +// Moved in here in 2023. +PyObject * +psutil_sensors_battery(PyObject *self, PyObject *args) { + SYSTEM_POWER_STATUS sps; + + if (GetSystemPowerStatus(&sps) == 0) { + PyErr_SetFromWindowsErr(0); + return NULL; + } + return Py_BuildValue( + "iiiI", + sps.ACLineStatus, // whether AC is connected: 0=no, 1=yes, 255=unknown + // status flag: + // 1, 2, 4 = high, low, critical + // 8 = charging + // 128 = no battery + sps.BatteryFlag, + sps.BatteryLifePercent, // percent + sps.BatteryLifeTime // remaining secs + ); +} diff --git a/psutil/arch/windows/sensors.h b/psutil/arch/windows/sensors.h new file mode 100644 index 0000000000..edace25d3d --- /dev/null +++ b/psutil/arch/windows/sensors.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index 9fda1b303d..434522d2af 100755 --- a/setup.py +++ b/setup.py @@ -213,11 +213,12 @@ def get_winver(): 'psutil/arch/windows/process_utils.c', 'psutil/arch/windows/process_info.c', 'psutil/arch/windows/process_handles.c', + 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/disk.c', 'psutil/arch/windows/mem.c', 'psutil/arch/windows/net.c', - 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/security.c', + 'psutil/arch/windows/sensors.c', 'psutil/arch/windows/services.c', 'psutil/arch/windows/socks.c', 'psutil/arch/windows/wmi.c', From 59504a56500a8332f0eb8563178e20f843ff8820 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Apr 2023 11:16:53 -0700 Subject: [PATCH 0976/1714] Win, C, refact: move boot_time() and users() in new sys.c --- psutil/_psutil_windows.c | 173 ++---------------------------------- psutil/arch/windows/sys.c | 178 ++++++++++++++++++++++++++++++++++++++ psutil/arch/windows/sys.h | 10 +++ setup.py | 7 +- 4 files changed, 198 insertions(+), 170 deletions(-) create mode 100644 psutil/arch/windows/sys.c create mode 100644 psutil/arch/windows/sys.h diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index d7ffbfe38c..ef85433272 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -27,17 +27,18 @@ #pragma comment(lib, "IPHLPAPI.lib") #include "_psutil_common.h" -#include "arch/windows/security.h" -#include "arch/windows/process_utils.h" -#include "arch/windows/process_info.h" -#include "arch/windows/process_handles.h" -#include "arch/windows/disk.h" #include "arch/windows/cpu.h" +#include "arch/windows/disk.h" #include "arch/windows/mem.h" #include "arch/windows/net.h" +#include "arch/windows/process_handles.h" +#include "arch/windows/process_info.h" +#include "arch/windows/process_utils.h" +#include "arch/windows/security.h" #include "arch/windows/sensors.h" #include "arch/windows/services.h" #include "arch/windows/socks.h" +#include "arch/windows/sys.h" #include "arch/windows/wmi.h" // Raised by Process.wait(). @@ -45,21 +46,6 @@ static PyObject *TimeoutExpired; static PyObject *TimeoutAbandoned; -/* - * Return a Python float representing the system uptime expressed in seconds - * since the epoch. - */ -static PyObject * -psutil_boot_time(PyObject *self, PyObject *args) { - ULONGLONG upTime; - FILETIME fileTime; - - GetSystemTimeAsFileTime(&fileTime); - // Number of milliseconds that have elapsed since the system was started. - upTime = GetTickCount64() / 1000ull; - return Py_BuildValue("d", psutil_FiletimeToUnixTime(fileTime) - upTime); -} - /* * Return 1 if PID exists in the current process list, else 0. @@ -1098,153 +1084,6 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { } -/* - * Return a Python dict of tuples for disk I/O information - */ -static PyObject * -psutil_users(PyObject *self, PyObject *args) { - HANDLE hServer = WTS_CURRENT_SERVER_HANDLE; - LPWSTR buffer_user = NULL; - LPWSTR buffer_addr = NULL; - LPWSTR buffer_info = NULL; - PWTS_SESSION_INFOW sessions = NULL; - DWORD count; - DWORD i; - DWORD sessionId; - DWORD bytes; - PWTS_CLIENT_ADDRESS address; - char address_str[50]; - PWTSINFOW wts_info; - PyObject *py_tuple = NULL; - PyObject *py_address = NULL; - PyObject *py_username = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - if (WTSEnumerateSessionsW == NULL || - WTSQuerySessionInformationW == NULL || - WTSFreeMemory == NULL) { - // If we don't run in an environment that is a Remote Desktop Services environment - // the Wtsapi32 proc might not be present. - // https://docs.microsoft.com/en-us/windows/win32/termserv/run-time-linking-to-wtsapi32-dll - return py_retlist; - } - - if (WTSEnumerateSessionsW(hServer, 0, 1, &sessions, &count) == 0) { - if (ERROR_CALL_NOT_IMPLEMENTED == GetLastError()) { - // On Windows Nano server, the Wtsapi32 API can be present, but return WinError 120. - return py_retlist; - } - PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); - goto error; - } - - for (i = 0; i < count; i++) { - py_address = NULL; - py_tuple = NULL; - sessionId = sessions[i].SessionId; - if (buffer_user != NULL) - WTSFreeMemory(buffer_user); - if (buffer_addr != NULL) - WTSFreeMemory(buffer_addr); - if (buffer_info != NULL) - WTSFreeMemory(buffer_info); - - buffer_user = NULL; - buffer_addr = NULL; - buffer_info = NULL; - - // username - bytes = 0; - if (WTSQuerySessionInformationW(hServer, sessionId, WTSUserName, - &buffer_user, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); - goto error; - } - if (bytes <= 2) - continue; - - // address - bytes = 0; - if (WTSQuerySessionInformationW(hServer, sessionId, WTSClientAddress, - &buffer_addr, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); - goto error; - } - - address = (PWTS_CLIENT_ADDRESS)buffer_addr; - if (address->AddressFamily == 2) { // AF_INET == 2 - sprintf_s(address_str, - _countof(address_str), - "%u.%u.%u.%u", - // The IP address is offset by two bytes from the start of the Address member of the WTS_CLIENT_ADDRESS structure. - address->Address[2], - address->Address[3], - address->Address[4], - address->Address[5]); - py_address = Py_BuildValue("s", address_str); - if (!py_address) - goto error; - } - else { - Py_INCREF(Py_None); - py_address = Py_None; - } - - // login time - bytes = 0; - if (WTSQuerySessionInformationW(hServer, sessionId, WTSSessionInfo, - &buffer_info, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); - goto error; - } - wts_info = (PWTSINFOW)buffer_info; - - py_username = PyUnicode_FromWideChar(buffer_user, wcslen(buffer_user)); - if (py_username == NULL) - goto error; - - py_tuple = Py_BuildValue( - "OOd", - py_username, - py_address, - psutil_LargeIntegerToUnixTime(wts_info->ConnectTime) - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_username); - Py_CLEAR(py_address); - Py_CLEAR(py_tuple); - } - - WTSFreeMemory(sessions); - WTSFreeMemory(buffer_user); - WTSFreeMemory(buffer_addr); - WTSFreeMemory(buffer_info); - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tuple); - Py_XDECREF(py_address); - Py_DECREF(py_retlist); - - if (sessions != NULL) - WTSFreeMemory(sessions); - if (buffer_user != NULL) - WTSFreeMemory(buffer_user); - if (buffer_addr != NULL) - WTSFreeMemory(buffer_addr); - if (buffer_info != NULL) - WTSFreeMemory(buffer_info); - return NULL; -} - - /* * Return the number of handles opened by process. */ diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c new file mode 100644 index 0000000000..3e12e71b72 --- /dev/null +++ b/psutil/arch/windows/sys.c @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +/* +System related functions. Original code moved in here from +psutil/_psutil_windows.c in 2023. For reference, here's the GIT blame +history before the move: + +* boot_time(): https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L51-L60 +* users(): https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L1103-L1244 +*/ + +#include +#include + +#include "ntextapi.h" +#include "../../_psutil_common.h" + + +// Return a Python float representing the system uptime expressed in +// seconds since the epoch. +PyObject * +psutil_boot_time(PyObject *self, PyObject *args) { + ULONGLONG upTime; + FILETIME fileTime; + + GetSystemTimeAsFileTime(&fileTime); + // Number of milliseconds that have elapsed since the system was started. + upTime = GetTickCount64() / 1000ull; + return Py_BuildValue("d", psutil_FiletimeToUnixTime(fileTime) - upTime); +} + + +PyObject * +psutil_users(PyObject *self, PyObject *args) { + HANDLE hServer = WTS_CURRENT_SERVER_HANDLE; + LPWSTR buffer_user = NULL; + LPWSTR buffer_addr = NULL; + LPWSTR buffer_info = NULL; + PWTS_SESSION_INFOW sessions = NULL; + DWORD count; + DWORD i; + DWORD sessionId; + DWORD bytes; + PWTS_CLIENT_ADDRESS address; + char address_str[50]; + PWTSINFOW wts_info; + PyObject *py_tuple = NULL; + PyObject *py_address = NULL; + PyObject *py_username = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + if (WTSEnumerateSessionsW == NULL || + WTSQuerySessionInformationW == NULL || + WTSFreeMemory == NULL) { + // If we don't run in an environment that is a Remote Desktop Services environment + // the Wtsapi32 proc might not be present. + // https://docs.microsoft.com/en-us/windows/win32/termserv/run-time-linking-to-wtsapi32-dll + return py_retlist; + } + + if (WTSEnumerateSessionsW(hServer, 0, 1, &sessions, &count) == 0) { + if (ERROR_CALL_NOT_IMPLEMENTED == GetLastError()) { + // On Windows Nano server, the Wtsapi32 API can be present, but return WinError 120. + return py_retlist; + } + PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); + goto error; + } + + for (i = 0; i < count; i++) { + py_address = NULL; + py_tuple = NULL; + sessionId = sessions[i].SessionId; + if (buffer_user != NULL) + WTSFreeMemory(buffer_user); + if (buffer_addr != NULL) + WTSFreeMemory(buffer_addr); + if (buffer_info != NULL) + WTSFreeMemory(buffer_info); + + buffer_user = NULL; + buffer_addr = NULL; + buffer_info = NULL; + + // username + bytes = 0; + if (WTSQuerySessionInformationW(hServer, sessionId, WTSUserName, + &buffer_user, &bytes) == 0) { + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); + goto error; + } + if (bytes <= 2) + continue; + + // address + bytes = 0; + if (WTSQuerySessionInformationW(hServer, sessionId, WTSClientAddress, + &buffer_addr, &bytes) == 0) { + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); + goto error; + } + + address = (PWTS_CLIENT_ADDRESS)buffer_addr; + if (address->AddressFamily == 2) { // AF_INET == 2 + sprintf_s(address_str, + _countof(address_str), + "%u.%u.%u.%u", + // The IP address is offset by two bytes from the start of the Address member of the WTS_CLIENT_ADDRESS structure. + address->Address[2], + address->Address[3], + address->Address[4], + address->Address[5]); + py_address = Py_BuildValue("s", address_str); + if (!py_address) + goto error; + } + else { + Py_INCREF(Py_None); + py_address = Py_None; + } + + // login time + bytes = 0; + if (WTSQuerySessionInformationW(hServer, sessionId, WTSSessionInfo, + &buffer_info, &bytes) == 0) { + PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); + goto error; + } + wts_info = (PWTSINFOW)buffer_info; + + py_username = PyUnicode_FromWideChar(buffer_user, wcslen(buffer_user)); + if (py_username == NULL) + goto error; + + py_tuple = Py_BuildValue( + "OOd", + py_username, + py_address, + psutil_LargeIntegerToUnixTime(wts_info->ConnectTime) + ); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_username); + Py_CLEAR(py_address); + Py_CLEAR(py_tuple); + } + + WTSFreeMemory(sessions); + WTSFreeMemory(buffer_user); + WTSFreeMemory(buffer_addr); + WTSFreeMemory(buffer_info); + return py_retlist; + +error: + Py_XDECREF(py_username); + Py_XDECREF(py_tuple); + Py_XDECREF(py_address); + Py_DECREF(py_retlist); + + if (sessions != NULL) + WTSFreeMemory(sessions); + if (buffer_user != NULL) + WTSFreeMemory(buffer_user); + if (buffer_addr != NULL) + WTSFreeMemory(buffer_addr); + if (buffer_info != NULL) + WTSFreeMemory(buffer_info); + return NULL; +} diff --git a/psutil/arch/windows/sys.h b/psutil/arch/windows/sys.h new file mode 100644 index 0000000000..344ca21d42 --- /dev/null +++ b/psutil/arch/windows/sys.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index 434522d2af..4923600adb 100755 --- a/setup.py +++ b/setup.py @@ -210,17 +210,18 @@ def get_winver(): 'psutil._psutil_windows', sources=sources + [ 'psutil/_psutil_windows.c', - 'psutil/arch/windows/process_utils.c', - 'psutil/arch/windows/process_info.c', - 'psutil/arch/windows/process_handles.c', 'psutil/arch/windows/cpu.c', 'psutil/arch/windows/disk.c', 'psutil/arch/windows/mem.c', 'psutil/arch/windows/net.c', + 'psutil/arch/windows/process_handles.c', + 'psutil/arch/windows/process_info.c', + 'psutil/arch/windows/process_utils.c', 'psutil/arch/windows/security.c', 'psutil/arch/windows/sensors.c', 'psutil/arch/windows/services.c', 'psutil/arch/windows/socks.c', + 'psutil/arch/windows/sys.c', 'psutil/arch/windows/wmi.c', ], define_macros=macros, From 968e4bba7dd015d5a469c77ec65956f211d3a5bd Mon Sep 17 00:00:00 2001 From: Thomas Klausner Date: Wed, 19 Apr 2023 09:25:08 +0200 Subject: [PATCH 0977/1714] Fix build on NetBSD due to missing .h include. (#2241) --- psutil/arch/netbsd/cpu.c | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c index 33eb906f1f..3601e2e4a0 100644 --- a/psutil/arch/netbsd/cpu.c +++ b/psutil/arch/netbsd/cpu.c @@ -6,6 +6,7 @@ */ #include +#include #include #include From caa184349c934958b3673c8d3532c607caa2052a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Apr 2023 18:26:01 +0200 Subject: [PATCH 0978/1714] BSD big refact: move proc funcs in new proc.c file --- MANIFEST.in | 14 + Makefile | 1 + psutil/_psutil_bsd.c | 1012 +--------------------------------------- psutil/arch/bsd/cpu.c | 55 +++ psutil/arch/bsd/cpu.h | 10 + psutil/arch/bsd/disk.c | 183 ++++++++ psutil/arch/bsd/disk.h | 9 + psutil/arch/bsd/net.c | 105 +++++ psutil/arch/bsd/net.h | 9 + psutil/arch/bsd/proc.c | 494 ++++++++++++++++++++ psutil/arch/bsd/proc.h | 13 + psutil/arch/bsd/sys.c | 161 +++++++ psutil/arch/bsd/sys.h | 10 + setup.py | 21 +- 14 files changed, 1093 insertions(+), 1004 deletions(-) create mode 100644 psutil/arch/bsd/cpu.c create mode 100644 psutil/arch/bsd/cpu.h create mode 100644 psutil/arch/bsd/disk.c create mode 100644 psutil/arch/bsd/disk.h create mode 100644 psutil/arch/bsd/net.c create mode 100644 psutil/arch/bsd/net.h create mode 100644 psutil/arch/bsd/proc.c create mode 100644 psutil/arch/bsd/proc.h create mode 100644 psutil/arch/bsd/sys.c create mode 100644 psutil/arch/bsd/sys.h diff --git a/MANIFEST.in b/MANIFEST.in index a594328da7..db7079354b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -47,6 +47,16 @@ include psutil/arch/aix/ifaddrs.h include psutil/arch/aix/net_connections.c include psutil/arch/aix/net_connections.h include psutil/arch/aix/net_kernel_structs.h +include psutil/arch/bsd/cpu.c +include psutil/arch/bsd/cpu.h +include psutil/arch/bsd/disk.c +include psutil/arch/bsd/disk.h +include psutil/arch/bsd/net.c +include psutil/arch/bsd/net.h +include psutil/arch/bsd/proc.c +include psutil/arch/bsd/proc.h +include psutil/arch/bsd/sys.c +include psutil/arch/bsd/sys.h include psutil/arch/freebsd/cpu.c include psutil/arch/freebsd/cpu.h include psutil/arch/freebsd/disk.c @@ -106,10 +116,14 @@ include psutil/arch/windows/process_utils.c include psutil/arch/windows/process_utils.h include psutil/arch/windows/security.c include psutil/arch/windows/security.h +include psutil/arch/windows/sensors.c +include psutil/arch/windows/sensors.h include psutil/arch/windows/services.c include psutil/arch/windows/services.h include psutil/arch/windows/socks.c include psutil/arch/windows/socks.h +include psutil/arch/windows/sys.c +include psutil/arch/windows/sys.h include psutil/arch/windows/wmi.c include psutil/arch/windows/wmi.h include psutil/tests/README.rst diff --git a/Makefile b/Makefile index 27bba26618..ba62aa983d 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,7 @@ build: ## Compile (in parallel) without installing. @# to allow "import psutil" when using the interactive interpreter from @# within this directory. PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) + $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. ${MAKE} build diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index dfebf9ff94..a1b4a3e5be 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1,5 +1,6 @@ /* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil (OpenBSD). + * Copyright (c) 2009, Jay Loden, Giampaolo Rodola', Landry Breuil + * (OpenBSD implementation), Ryo Onodera (NetBSD implementation). * All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. @@ -15,1033 +16,42 @@ * - psutil.Process.memory_maps() */ -#if defined(PSUTIL_NETBSD) - #define _KMEMUSER -#endif - #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#if !defined(__NetBSD__) - #include -#endif #include -#include -#include -#include -#include // for struct xsocket -#include -#include -// for xinpcb struct -#include -#include -#include -#include -#include -#include -#include -#include // for struct xtcpcb +#include // BSD version #include // for TCP connection states -#include // for inet_ntop() -#include -#include // net io counters -#include -#include -#include // process open files/connections -#include -#include #include "_psutil_common.h" #include "_psutil_posix.h" +#include "arch/bsd/cpu.h" +#include "arch/bsd/disk.h" +#include "arch/bsd/net.h" +#include "arch/bsd/proc.h" +#include "arch/bsd/sys.h" #ifdef PSUTIL_FREEBSD #include "arch/freebsd/cpu.h" - #include "arch/freebsd/mem.h" #include "arch/freebsd/disk.h" - #include "arch/freebsd/sensors.h" + #include "arch/freebsd/mem.h" #include "arch/freebsd/proc.h" - #include "arch/freebsd/sys_socks.h" #include "arch/freebsd/proc_socks.h" - - #include - #include // get io counters - #include // process open files, shared libs (kinfo_getvmmap) - #if __FreeBSD_version < 900000 - #include // system users - #else - #include - #endif + #include "arch/freebsd/sensors.h" + #include "arch/freebsd/sys_socks.h" #elif PSUTIL_OPENBSD #include "arch/openbsd/cpu.h" #include "arch/openbsd/disk.h" #include "arch/openbsd/mem.h" #include "arch/openbsd/proc.h" #include "arch/openbsd/socks.h" - - #include - #include // for VREG - #define _KERNEL // for DTYPE_VNODE - #include - #undef _KERNEL - #include // for CPUSTATES & CP_* #elif PSUTIL_NETBSD #include "arch/netbsd/cpu.h" #include "arch/netbsd/disk.h" #include "arch/netbsd/mem.h" #include "arch/netbsd/proc.h" #include "arch/netbsd/socks.h" - - #include - #include // for VREG - #include // for CPUSTATES & CP_* - #ifndef DTYPE_VNODE - #define DTYPE_VNODE 1 - #endif -#endif - - -// convert a timeval struct to a double -#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) - -#if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) - #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) #endif -/* - * Return a Python list of all the PIDs running on the system. - */ -static PyObject * -psutil_pids(PyObject *self, PyObject *args) { - kinfo_proc *proclist = NULL; - kinfo_proc *orig_address = NULL; - size_t num_processes; - size_t idx; - PyObject *py_retlist = PyList_New(0); - PyObject *py_pid = NULL; - - if (py_retlist == NULL) - return NULL; - - if (psutil_get_proc_list(&proclist, &num_processes) != 0) - goto error; - - if (num_processes > 0) { - orig_address = proclist; // save so we can free it after we're done - for (idx = 0; idx < num_processes; idx++) { -#ifdef PSUTIL_FREEBSD - py_pid = PyLong_FromPid(proclist->ki_pid); -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_pid = PyLong_FromPid(proclist->p_pid); -#endif - if (!py_pid) - goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_CLEAR(py_pid); - proclist++; - } - free(orig_address); - } - - return py_retlist; - -error: - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - if (orig_address != NULL) - free(orig_address); - return NULL; -} - - -/* - * Return a Python float indicating the system boot time expressed in - * seconds since the epoch. - */ -static PyObject * -psutil_boot_time(PyObject *self, PyObject *args) { - // fetch sysctl "kern.boottime" - static int request[2] = { CTL_KERN, KERN_BOOTTIME }; - struct timeval boottime; - size_t len = sizeof(boottime); - - if (sysctl(request, 2, &boottime, &len, NULL, 0) == -1) - return PyErr_SetFromErrno(PyExc_OSError); - return Py_BuildValue("d", (double)boottime.tv_sec); -} - - -/* - * Collect different info about a process in one shot and return - * them as a big Python tuple. - */ -static PyObject * -psutil_proc_oneshot_info(PyObject *self, PyObject *args) { - pid_t pid; - long rss; - long vms; - long memtext; - long memdata; - long memstack; - int oncpu; - kinfo_proc kp; - long pagesize = psutil_getpagesize(); - char str[1000]; - PyObject *py_name; - PyObject *py_ppid; - PyObject *py_retlist; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (psutil_kinfo_proc(pid, &kp) == -1) - return NULL; - - // Process -#ifdef PSUTIL_FREEBSD - sprintf(str, "%s", kp.ki_comm); -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - sprintf(str, "%s", kp.p_comm); -#endif - py_name = PyUnicode_DecodeFSDefault(str); - if (! py_name) { - // Likely a decoding error. We don't want to fail the whole - // operation. The python module may retry with proc_name(). - PyErr_Clear(); - py_name = Py_None; - } - // Py_INCREF(py_name); - - // Calculate memory. -#ifdef PSUTIL_FREEBSD - rss = (long)kp.ki_rssize * pagesize; - vms = (long)kp.ki_size; - memtext = (long)kp.ki_tsize * pagesize; - memdata = (long)kp.ki_dsize * pagesize; - memstack = (long)kp.ki_ssize * pagesize; -#else - rss = (long)kp.p_vm_rssize * pagesize; - #ifdef PSUTIL_OPENBSD - // VMS, this is how ps determines it on OpenBSD: - // https://github.com/openbsd/src/blob/ - // 588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L505 - vms = (long)(kp.p_vm_dsize + kp.p_vm_ssize + kp.p_vm_tsize) * pagesize; - #elif PSUTIL_NETBSD - // VMS, this is how top determines it on NetBSD: - // https://github.com/IIJ-NetBSD/netbsd-src/blob/master/external/ - // bsd/top/dist/machine/m_netbsd.c - vms = (long)kp.p_vm_msize * pagesize; - #endif - memtext = (long)kp.p_vm_tsize * pagesize; - memdata = (long)kp.p_vm_dsize * pagesize; - memstack = (long)kp.p_vm_ssize * pagesize; -#endif - -#ifdef PSUTIL_FREEBSD - // what CPU we're on; top was used as an example: - // https://svnweb.freebsd.org/base/head/usr.bin/top/machine.c? - // view=markup&pathrev=273835 - // XXX - note: for "intr" PID this is -1. - if (kp.ki_stat == SRUN && kp.ki_oncpu != NOCPU) - oncpu = kp.ki_oncpu; - else - oncpu = kp.ki_lastcpu; -#else - // On Net/OpenBSD we have kp.p_cpuid but it appears it's always - // set to KI_NOCPU. Even if it's not, ki_lastcpu does not exist - // so there's no way to determine where "sleeping" processes - // were. Not supported. - oncpu = -1; -#endif - -#ifdef PSUTIL_FREEBSD - py_ppid = PyLong_FromPid(kp.ki_ppid); -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_ppid = PyLong_FromPid(kp.p_ppid); -#else - py_ppid = Py_BuildfValue(-1); -#endif - if (! py_ppid) - return NULL; - - // Return a single big tuple with all process info. - py_retlist = Py_BuildValue( -#if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031 - "(OillllllLdllllddddlllllbO)", -#else - "(OillllllidllllddddlllllbO)", -#endif -#ifdef PSUTIL_FREEBSD - py_ppid, // (pid_t) ppid - (int)kp.ki_stat, // (int) status - // UIDs - (long)kp.ki_ruid, // (long) real uid - (long)kp.ki_uid, // (long) effective uid - (long)kp.ki_svuid, // (long) saved uid - // GIDs - (long)kp.ki_rgid, // (long) real gid - (long)kp.ki_groups[0], // (long) effective gid - (long)kp.ki_svuid, // (long) saved gid - // - kp.ki_tdev, // (int or long long) tty nr - PSUTIL_TV2DOUBLE(kp.ki_start), // (double) create time - // ctx switches - kp.ki_rusage.ru_nvcsw, // (long) ctx switches (voluntary) - kp.ki_rusage.ru_nivcsw, // (long) ctx switches (unvoluntary) - // IO count - kp.ki_rusage.ru_inblock, // (long) read io count - kp.ki_rusage.ru_oublock, // (long) write io count - // CPU times: convert from micro seconds to seconds. - PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_utime), // (double) user time - PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_stime), // (double) sys time - PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_utime), // (double) children utime - PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_stime), // (double) children stime - // memory - rss, // (long) rss - vms, // (long) vms - memtext, // (long) mem text - memdata, // (long) mem data - memstack, // (long) mem stack - // others - oncpu, // (int) the CPU we are on -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_ppid, // (pid_t) ppid - (int)kp.p_stat, // (int) status - // UIDs - (long)kp.p_ruid, // (long) real uid - (long)kp.p_uid, // (long) effective uid - (long)kp.p_svuid, // (long) saved uid - // GIDs - (long)kp.p_rgid, // (long) real gid - (long)kp.p_groups[0], // (long) effective gid - (long)kp.p_svuid, // (long) saved gid - // - kp.p_tdev, // (int) tty nr - PSUTIL_KPT2DOUBLE(kp.p_ustart), // (double) create time - // ctx switches - kp.p_uru_nvcsw, // (long) ctx switches (voluntary) - kp.p_uru_nivcsw, // (long) ctx switches (unvoluntary) - // IO count - kp.p_uru_inblock, // (long) read io count - kp.p_uru_oublock, // (long) write io count - // CPU times: convert from micro seconds to seconds. - PSUTIL_KPT2DOUBLE(kp.p_uutime), // (double) user time - PSUTIL_KPT2DOUBLE(kp.p_ustime), // (double) sys time - // OpenBSD and NetBSD provide children user + system times summed - // together (no distinction). - kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch utime - kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch stime - // memory - rss, // (long) rss - vms, // (long) vms - memtext, // (long) mem text - memdata, // (long) mem data - memstack, // (long) mem stack - // others - oncpu, // (int) the CPU we are on -#endif - py_name // (pystr) name - ); - - Py_DECREF(py_name); - Py_DECREF(py_ppid); - return py_retlist; -} - - -/* - * Return process name from kinfo_proc as a Python string. - */ -static PyObject * -psutil_proc_name(PyObject *self, PyObject *args) { - pid_t pid; - kinfo_proc kp; - char str[1000]; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (psutil_kinfo_proc(pid, &kp) == -1) - return NULL; - -#ifdef PSUTIL_FREEBSD - sprintf(str, "%s", kp.ki_comm); -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - sprintf(str, "%s", kp.p_comm); -#endif - return PyUnicode_DecodeFSDefault(str); -} - - - -/* - * Return process environment as a Python dictionary - */ -PyObject * -psutil_proc_environ(PyObject *self, PyObject *args) { - int i, cnt = -1; - long pid; - char *s, **envs, errbuf[_POSIX2_LINE_MAX]; - PyObject *py_value=NULL, *py_retdict=NULL; - kvm_t *kd; -#ifdef PSUTIL_NETBSD - struct kinfo_proc2 *p; -#else - struct kinfo_proc *p; -#endif - - if (!PyArg_ParseTuple(args, "l", &pid)) - return NULL; - -#if defined(PSUTIL_FREEBSD) - kd = kvm_openfiles(NULL, "/dev/null", NULL, 0, errbuf); -#else - kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); -#endif - if (!kd) { - convert_kvm_err("kvm_openfiles", errbuf); - return NULL; - } - - py_retdict = PyDict_New(); - if (!py_retdict) - goto error; - -#if defined(PSUTIL_FREEBSD) - p = kvm_getprocs(kd, KERN_PROC_PID, pid, &cnt); -#elif defined(PSUTIL_OPENBSD) - p = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); -#elif defined(PSUTIL_NETBSD) - p = kvm_getproc2(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); -#endif - if (!p) { - NoSuchProcess("kvm_getprocs"); - goto error; - } - if (cnt <= 0) { - NoSuchProcess(cnt < 0 ? kvm_geterr(kd) : "kvm_getprocs: cnt==0"); - goto error; - } - - // On *BSD kernels there are a few kernel-only system processes without an - // environment (See e.g. "procstat -e 0 | 1 | 2 ..." on FreeBSD.) - // Some system process have no stats attached at all - // (they are marked with P_SYSTEM.) - // On FreeBSD, it's possible that the process is swapped or paged out, - // then there no access to the environ stored in the process' user area. - // On NetBSD, we cannot call kvm_getenvv2() for a zombie process. - // To make unittest suite happy, return an empty environment. -#if defined(PSUTIL_FREEBSD) -#if (defined(__FreeBSD_version) && __FreeBSD_version >= 700000) - if (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) { -#else - if ((p)->ki_flag & P_SYSTEM) { -#endif -#elif defined(PSUTIL_NETBSD) - if ((p)->p_stat == SZOMB) { -#elif defined(PSUTIL_OPENBSD) - if ((p)->p_flag & P_SYSTEM) { -#endif - kvm_close(kd); - return py_retdict; - } - -#if defined(PSUTIL_NETBSD) - envs = kvm_getenvv2(kd, p, 0); -#else - envs = kvm_getenvv(kd, p, 0); -#endif - if (!envs) { - // Map to "psutil" general high-level exceptions - switch (errno) { - case 0: - // Process has cleared it's environment, return empty one - kvm_close(kd); - return py_retdict; - case EPERM: - AccessDenied("kvm_getenvv -> EPERM"); - break; - case ESRCH: - NoSuchProcess("kvm_getenvv -> ESRCH"); - break; -#if defined(PSUTIL_FREEBSD) - case ENOMEM: - // Unfortunately, under FreeBSD kvm_getenvv() returns - // failure for certain processes ( e.g. try - // "sudo procstat -e ".) - // Map the error condition to 'AccessDenied'. - sprintf(errbuf, - "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM", - pid, p->ki_uid); - AccessDenied(errbuf); - break; -#endif - default: - sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); - PyErr_SetFromOSErrnoWithSyscall(errbuf); - break; - } - goto error; - } - - for (i = 0; envs[i] != NULL; i++) { - s = strchr(envs[i], '='); - if (!s) - continue; - *s++ = 0; - py_value = PyUnicode_DecodeFSDefault(s); - if (!py_value) - goto error; - if (PyDict_SetItemString(py_retdict, envs[i], py_value)) { - goto error; - } - Py_DECREF(py_value); - } - - kvm_close(kd); - return py_retdict; - -error: - Py_XDECREF(py_value); - Py_XDECREF(py_retdict); - kvm_close(kd); - return NULL; -} - -/* - * Return the number of logical CPUs in the system. - * XXX this could be shared with macOS - */ -static PyObject * -psutil_cpu_count_logical(PyObject *self, PyObject *args) { - int mib[2]; - int ncpu; - size_t len; - - mib[0] = CTL_HW; - mib[1] = HW_NCPU; - len = sizeof(ncpu); - - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) - Py_RETURN_NONE; // mimic os.cpu_count() - else - return Py_BuildValue("i", ncpu); -} - - -/* - * Return a Python tuple representing user, kernel and idle CPU times - */ -static PyObject * -psutil_cpu_times(PyObject *self, PyObject *args) { -#ifdef PSUTIL_NETBSD - u_int64_t cpu_time[CPUSTATES]; -#else - long cpu_time[CPUSTATES]; -#endif - size_t size = sizeof(cpu_time); - int ret; - -#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) - ret = sysctlbyname("kern.cp_time", &cpu_time, &size, NULL, 0); -#elif PSUTIL_OPENBSD - int mib[] = {CTL_KERN, KERN_CPTIME}; - ret = sysctl(mib, 2, &cpu_time, &size, NULL, 0); -#endif - if (ret == -1) - return PyErr_SetFromErrno(PyExc_OSError); - return Py_BuildValue("(ddddd)", - (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC - ); -} - - - /* - * Return files opened by process as a list of (path, fd) tuples. - * TODO: this is broken as it may report empty paths. 'procstat' - * utility has the same problem see: - * https://github.com/giampaolo/psutil/issues/595 - */ -#if (defined(__FreeBSD_version) && __FreeBSD_version >= 800000) || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) -static PyObject * -psutil_proc_open_files(PyObject *self, PyObject *args) { - pid_t pid; - int i; - int cnt; - int regular; - int fd; - char *path; - struct kinfo_file *freep = NULL; - struct kinfo_file *kif; - kinfo_proc kipp; - PyObject *py_tuple = NULL; - PyObject *py_path = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - goto error; - if (psutil_kinfo_proc(pid, &kipp) == -1) - goto error; - - errno = 0; - freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) { -#if !defined(PSUTIL_OPENBSD) - psutil_raise_for_pid(pid, "kinfo_getfile()"); -#endif - goto error; - } - - for (i = 0; i < cnt; i++) { - kif = &freep[i]; - -#ifdef PSUTIL_FREEBSD - regular = (kif->kf_type == KF_TYPE_VNODE) && \ - (kif->kf_vnode_type == KF_VTYPE_VREG); - fd = kif->kf_fd; - path = kif->kf_path; -#elif PSUTIL_OPENBSD - regular = (kif->f_type == DTYPE_VNODE) && (kif->v_type == VREG); - fd = kif->fd_fd; - // XXX - it appears path is not exposed in the kinfo_file struct. - path = ""; -#elif PSUTIL_NETBSD - regular = (kif->ki_ftype == DTYPE_VNODE) && (kif->ki_vtype == VREG); - fd = kif->ki_fd; - // XXX - it appears path is not exposed in the kinfo_file struct. - path = ""; -#endif - if (regular == 1) { - py_path = PyUnicode_DecodeFSDefault(path); - if (! py_path) - goto error; - py_tuple = Py_BuildValue("(Oi)", py_path, fd); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_path); - Py_CLEAR(py_tuple); - } - } - free(freep); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (freep != NULL) - free(freep); - return NULL; -} -#endif - - -/* - * Return a list of tuples including device, mount point and fs type - * for all partitions mounted on the system. - */ -static PyObject * -psutil_disk_partitions(PyObject *self, PyObject *args) { - int num; - int i; - long len; - uint64_t flags; - char opts[200]; -#ifdef PSUTIL_NETBSD - struct statvfs *fs = NULL; -#else - struct statfs *fs = NULL; -#endif - PyObject *py_retlist = PyList_New(0); - PyObject *py_dev = NULL; - PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; - - if (py_retlist == NULL) - return NULL; - - // get the number of mount points - Py_BEGIN_ALLOW_THREADS -#ifdef PSUTIL_NETBSD - num = getvfsstat(NULL, 0, MNT_NOWAIT); -#else - num = getfsstat(NULL, 0, MNT_NOWAIT); -#endif - Py_END_ALLOW_THREADS - if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - len = sizeof(*fs) * num; - fs = malloc(len); - if (fs == NULL) { - PyErr_NoMemory(); - goto error; - } - - Py_BEGIN_ALLOW_THREADS -#ifdef PSUTIL_NETBSD - num = getvfsstat(fs, len, MNT_NOWAIT); -#else - num = getfsstat(fs, len, MNT_NOWAIT); -#endif - Py_END_ALLOW_THREADS - if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (i = 0; i < num; i++) { - py_tuple = NULL; - opts[0] = 0; -#ifdef PSUTIL_NETBSD - flags = fs[i].f_flag; -#else - flags = fs[i].f_flags; -#endif - - // see sys/mount.h - if (flags & MNT_RDONLY) - strlcat(opts, "ro", sizeof(opts)); - else - strlcat(opts, "rw", sizeof(opts)); - if (flags & MNT_SYNCHRONOUS) - strlcat(opts, ",sync", sizeof(opts)); - if (flags & MNT_NOEXEC) - strlcat(opts, ",noexec", sizeof(opts)); - if (flags & MNT_NOSUID) - strlcat(opts, ",nosuid", sizeof(opts)); - if (flags & MNT_ASYNC) - strlcat(opts, ",async", sizeof(opts)); - if (flags & MNT_NOATIME) - strlcat(opts, ",noatime", sizeof(opts)); - if (flags & MNT_SOFTDEP) - strlcat(opts, ",softdep", sizeof(opts)); -#ifdef PSUTIL_FREEBSD - if (flags & MNT_UNION) - strlcat(opts, ",union", sizeof(opts)); - if (flags & MNT_SUIDDIR) - strlcat(opts, ",suiddir", sizeof(opts)); - if (flags & MNT_SOFTDEP) - strlcat(opts, ",softdep", sizeof(opts)); - if (flags & MNT_NOSYMFOLLOW) - strlcat(opts, ",nosymfollow", sizeof(opts)); -#ifdef MNT_GJOURNAL - if (flags & MNT_GJOURNAL) - strlcat(opts, ",gjournal", sizeof(opts)); -#endif - if (flags & MNT_MULTILABEL) - strlcat(opts, ",multilabel", sizeof(opts)); - if (flags & MNT_ACLS) - strlcat(opts, ",acls", sizeof(opts)); - if (flags & MNT_NOCLUSTERR) - strlcat(opts, ",noclusterr", sizeof(opts)); - if (flags & MNT_NOCLUSTERW) - strlcat(opts, ",noclusterw", sizeof(opts)); -#ifdef MNT_NFS4ACLS - if (flags & MNT_NFS4ACLS) - strlcat(opts, ",nfs4acls", sizeof(opts)); -#endif -#elif PSUTIL_NETBSD - if (flags & MNT_NODEV) - strlcat(opts, ",nodev", sizeof(opts)); - if (flags & MNT_UNION) - strlcat(opts, ",union", sizeof(opts)); - if (flags & MNT_NOCOREDUMP) - strlcat(opts, ",nocoredump", sizeof(opts)); -#ifdef MNT_RELATIME - if (flags & MNT_RELATIME) - strlcat(opts, ",relatime", sizeof(opts)); -#endif - if (flags & MNT_IGNORE) - strlcat(opts, ",ignore", sizeof(opts)); -#ifdef MNT_DISCARD - if (flags & MNT_DISCARD) - strlcat(opts, ",discard", sizeof(opts)); -#endif -#ifdef MNT_EXTATTR - if (flags & MNT_EXTATTR) - strlcat(opts, ",extattr", sizeof(opts)); -#endif - if (flags & MNT_LOG) - strlcat(opts, ",log", sizeof(opts)); - if (flags & MNT_SYMPERM) - strlcat(opts, ",symperm", sizeof(opts)); - if (flags & MNT_NODEVMTIME) - strlcat(opts, ",nodevmtime", sizeof(opts)); -#endif - py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); - if (! py_dev) - goto error; - py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); - if (! py_mountp) - goto error; - py_tuple = Py_BuildValue("(OOss)", - py_dev, // device - py_mountp, // mount point - fs[i].f_fstypename, // fs type - opts); // options - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_dev); - Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); - } - - free(fs); - return py_retlist; - -error: - Py_XDECREF(py_dev); - Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (fs != NULL) - free(fs); - return NULL; -} - - -/* - * Return a Python list of named tuples with overall network I/O information - */ -static PyObject * -psutil_net_io_counters(PyObject *self, PyObject *args) { - char *buf = NULL, *lim, *next; - struct if_msghdr *ifm; - int mib[6]; - size_t len; - PyObject *py_retdict = PyDict_New(); - PyObject *py_ifc_info = NULL; - if (py_retdict == NULL) - return NULL; - - mib[0] = CTL_NET; // networking subsystem - mib[1] = PF_ROUTE; // type of information - mib[2] = 0; // protocol (IPPROTO_xxx) - mib[3] = 0; // address family - mib[4] = NET_RT_IFLIST; // operation - mib[5] = 0; - - if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - buf = malloc(len); - if (buf == NULL) { - PyErr_NoMemory(); - goto error; - } - - if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - lim = buf + len; - - for (next = buf; next < lim; ) { - py_ifc_info = NULL; - ifm = (struct if_msghdr *)next; - next += ifm->ifm_msglen; - - if (ifm->ifm_type == RTM_IFINFO) { - struct if_msghdr *if2m = (struct if_msghdr *)ifm; - struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); - char ifc_name[32]; - - strncpy(ifc_name, sdl->sdl_data, sdl->sdl_nlen); - ifc_name[sdl->sdl_nlen] = 0; - // XXX: ignore usbus interfaces: - // http://lists.freebsd.org/pipermail/freebsd-current/ - // 2011-October/028752.html - // 'ifconfig -a' doesn't show them, nor do we. - if (strncmp(ifc_name, "usbus", 5) == 0) - continue; - - py_ifc_info = Py_BuildValue("(kkkkkkki)", - if2m->ifm_data.ifi_obytes, - if2m->ifm_data.ifi_ibytes, - if2m->ifm_data.ifi_opackets, - if2m->ifm_data.ifi_ipackets, - if2m->ifm_data.ifi_ierrors, - if2m->ifm_data.ifi_oerrors, - if2m->ifm_data.ifi_iqdrops, -#ifdef _IFI_OQDROPS - if2m->ifm_data.ifi_oqdrops -#else - 0 -#endif - ); - if (!py_ifc_info) - goto error; - if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) - goto error; - Py_CLEAR(py_ifc_info); - } - else { - continue; - } - } - - free(buf); - return py_retdict; - -error: - Py_XDECREF(py_ifc_info); - Py_DECREF(py_retdict); - if (buf != NULL) - free(buf); - return NULL; -} - - -/* - * Return currently connected users as a list of tuples. - */ -static PyObject * -psutil_users(PyObject *self, PyObject *args) { - PyObject *py_retlist = PyList_New(0); - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_tuple = NULL; - PyObject *py_pid = NULL; - - if (py_retlist == NULL) - return NULL; - -#if (defined(__FreeBSD_version) && (__FreeBSD_version < 900000)) || PSUTIL_OPENBSD - struct utmp ut; - FILE *fp; - - Py_BEGIN_ALLOW_THREADS - fp = fopen(_PATH_UTMP, "r"); - Py_END_ALLOW_THREADS - if (fp == NULL) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, _PATH_UTMP); - goto error; - } - - while (fread(&ut, sizeof(ut), 1, fp) == 1) { - if (*ut.ut_name == '\0') - continue; - py_username = PyUnicode_DecodeFSDefault(ut.ut_name); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(ut.ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(ut.ut_host); - if (! py_hostname) - goto error; - py_tuple = Py_BuildValue( - "(OOOdi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut.ut_time, // start time -#if defined(PSUTIL_OPENBSD) || (defined(__FreeBSD_version) && __FreeBSD_version < 900000) - -1 // process id (set to None later) -#else - ut.ut_pid // TODO: use PyLong_FromPid -#endif - ); - if (!py_tuple) { - fclose(fp); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - fclose(fp); - goto error; - } - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - - fclose(fp); -#else - struct utmpx *utx; - setutxent(); - while ((utx = getutxent()) != NULL) { - if (utx->ut_type != USER_PROCESS) - continue; - py_username = PyUnicode_DecodeFSDefault(utx->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(utx->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); - if (! py_hostname) - goto error; -#ifdef PSUTIL_OPENBSD - py_pid = Py_BuildValue("i", -1); // set to None later -#else - py_pid = PyLong_FromPid(utx->ut_pid); -#endif - if (! py_pid) - goto error; - - py_tuple = Py_BuildValue( - "(OOOdO)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)utx->ut_tv.tv_sec, // start time - py_pid // process id - ); - - if (!py_tuple) { - endutxent(); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - endutxent(); - goto error; - } - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - Py_CLEAR(py_pid); - } - - endutxent(); -#endif - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - return NULL; -} - - /* * define the psutil C module methods and initialize the module. */ diff --git a/psutil/arch/bsd/cpu.c b/psutil/arch/bsd/cpu.c new file mode 100644 index 0000000000..69325c636f --- /dev/null +++ b/psutil/arch/bsd/cpu.c @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include + + +PyObject * +psutil_cpu_count_logical(PyObject *self, PyObject *args) { + int mib[2]; + int ncpu; + size_t len; + + mib[0] = CTL_HW; + mib[1] = HW_NCPU; + len = sizeof(ncpu); + + if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) + Py_RETURN_NONE; // mimic os.cpu_count() + else + return Py_BuildValue("i", ncpu); +} + + +PyObject * +psutil_cpu_times(PyObject *self, PyObject *args) { +#ifdef PSUTIL_NETBSD + u_int64_t cpu_time[CPUSTATES]; +#else + long cpu_time[CPUSTATES]; +#endif + size_t size = sizeof(cpu_time); + int ret; + +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) + ret = sysctlbyname("kern.cp_time", &cpu_time, &size, NULL, 0); +#elif PSUTIL_OPENBSD + int mib[] = {CTL_KERN, KERN_CPTIME}; + ret = sysctl(mib, 2, &cpu_time, &size, NULL, 0); +#endif + if (ret == -1) + return PyErr_SetFromErrno(PyExc_OSError); + return Py_BuildValue("(ddddd)", + (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC + ); +} diff --git a/psutil/arch/bsd/cpu.h b/psutil/arch/bsd/cpu.h new file mode 100644 index 0000000000..9c5d297fdd --- /dev/null +++ b/psutil/arch/bsd/cpu.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/disk.c b/psutil/arch/bsd/disk.c new file mode 100644 index 0000000000..bc1cf86329 --- /dev/null +++ b/psutil/arch/bsd/disk.c @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#if PSUTIL_NETBSD + // getvfsstat() + #include + #include +#else + // getfsstat() + #include + #include + #include +#endif + + +PyObject * +psutil_disk_partitions(PyObject *self, PyObject *args) { + int num; + int i; + long len; + uint64_t flags; + char opts[200]; +#ifdef PSUTIL_NETBSD + struct statvfs *fs = NULL; +#else + struct statfs *fs = NULL; +#endif + PyObject *py_retlist = PyList_New(0); + PyObject *py_dev = NULL; + PyObject *py_mountp = NULL; + PyObject *py_tuple = NULL; + + if (py_retlist == NULL) + return NULL; + + // get the number of mount points + Py_BEGIN_ALLOW_THREADS +#ifdef PSUTIL_NETBSD + num = getvfsstat(NULL, 0, MNT_NOWAIT); +#else + num = getfsstat(NULL, 0, MNT_NOWAIT); +#endif + Py_END_ALLOW_THREADS + if (num == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + len = sizeof(*fs) * num; + fs = malloc(len); + if (fs == NULL) { + PyErr_NoMemory(); + goto error; + } + + Py_BEGIN_ALLOW_THREADS +#ifdef PSUTIL_NETBSD + num = getvfsstat(fs, len, MNT_NOWAIT); +#else + num = getfsstat(fs, len, MNT_NOWAIT); +#endif + Py_END_ALLOW_THREADS + if (num == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (i = 0; i < num; i++) { + py_tuple = NULL; + opts[0] = 0; +#ifdef PSUTIL_NETBSD + flags = fs[i].f_flag; +#else + flags = fs[i].f_flags; +#endif + + // see sys/mount.h + if (flags & MNT_RDONLY) + strlcat(opts, "ro", sizeof(opts)); + else + strlcat(opts, "rw", sizeof(opts)); + if (flags & MNT_SYNCHRONOUS) + strlcat(opts, ",sync", sizeof(opts)); + if (flags & MNT_NOEXEC) + strlcat(opts, ",noexec", sizeof(opts)); + if (flags & MNT_NOSUID) + strlcat(opts, ",nosuid", sizeof(opts)); + if (flags & MNT_ASYNC) + strlcat(opts, ",async", sizeof(opts)); + if (flags & MNT_NOATIME) + strlcat(opts, ",noatime", sizeof(opts)); + if (flags & MNT_SOFTDEP) + strlcat(opts, ",softdep", sizeof(opts)); +#ifdef PSUTIL_FREEBSD + if (flags & MNT_UNION) + strlcat(opts, ",union", sizeof(opts)); + if (flags & MNT_SUIDDIR) + strlcat(opts, ",suiddir", sizeof(opts)); + if (flags & MNT_SOFTDEP) + strlcat(opts, ",softdep", sizeof(opts)); + if (flags & MNT_NOSYMFOLLOW) + strlcat(opts, ",nosymfollow", sizeof(opts)); +#ifdef MNT_GJOURNAL + if (flags & MNT_GJOURNAL) + strlcat(opts, ",gjournal", sizeof(opts)); +#endif + if (flags & MNT_MULTILABEL) + strlcat(opts, ",multilabel", sizeof(opts)); + if (flags & MNT_ACLS) + strlcat(opts, ",acls", sizeof(opts)); + if (flags & MNT_NOCLUSTERR) + strlcat(opts, ",noclusterr", sizeof(opts)); + if (flags & MNT_NOCLUSTERW) + strlcat(opts, ",noclusterw", sizeof(opts)); +#ifdef MNT_NFS4ACLS + if (flags & MNT_NFS4ACLS) + strlcat(opts, ",nfs4acls", sizeof(opts)); +#endif +#elif PSUTIL_NETBSD + if (flags & MNT_NODEV) + strlcat(opts, ",nodev", sizeof(opts)); + if (flags & MNT_UNION) + strlcat(opts, ",union", sizeof(opts)); + if (flags & MNT_NOCOREDUMP) + strlcat(opts, ",nocoredump", sizeof(opts)); +#ifdef MNT_RELATIME + if (flags & MNT_RELATIME) + strlcat(opts, ",relatime", sizeof(opts)); +#endif + if (flags & MNT_IGNORE) + strlcat(opts, ",ignore", sizeof(opts)); +#ifdef MNT_DISCARD + if (flags & MNT_DISCARD) + strlcat(opts, ",discard", sizeof(opts)); +#endif +#ifdef MNT_EXTATTR + if (flags & MNT_EXTATTR) + strlcat(opts, ",extattr", sizeof(opts)); +#endif + if (flags & MNT_LOG) + strlcat(opts, ",log", sizeof(opts)); + if (flags & MNT_SYMPERM) + strlcat(opts, ",symperm", sizeof(opts)); + if (flags & MNT_NODEVMTIME) + strlcat(opts, ",nodevmtime", sizeof(opts)); +#endif + py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); + if (! py_dev) + goto error; + py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); + if (! py_mountp) + goto error; + py_tuple = Py_BuildValue("(OOss)", + py_dev, // device + py_mountp, // mount point + fs[i].f_fstypename, // fs type + opts); // options + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); + } + + free(fs); + return py_retlist; + +error: + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + if (fs != NULL) + free(fs); + return NULL; +} diff --git a/psutil/arch/bsd/disk.h b/psutil/arch/bsd/disk.h new file mode 100644 index 0000000000..628907a9a7 --- /dev/null +++ b/psutil/arch/bsd/disk.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/net.c b/psutil/arch/bsd/net.c new file mode 100644 index 0000000000..c2fcc06632 --- /dev/null +++ b/psutil/arch/bsd/net.c @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include +#include +#include + + +PyObject * +psutil_net_io_counters(PyObject *self, PyObject *args) { + char *buf = NULL, *lim, *next; + struct if_msghdr *ifm; + int mib[6]; + size_t len; + PyObject *py_retdict = PyDict_New(); + PyObject *py_ifc_info = NULL; + + if (py_retdict == NULL) + return NULL; + + mib[0] = CTL_NET; // networking subsystem + mib[1] = PF_ROUTE; // type of information + mib[2] = 0; // protocol (IPPROTO_xxx) + mib[3] = 0; // address family + mib[4] = NET_RT_IFLIST; // operation + mib[5] = 0; + + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + buf = malloc(len); + if (buf == NULL) { + PyErr_NoMemory(); + goto error; + } + + if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + lim = buf + len; + + for (next = buf; next < lim; ) { + py_ifc_info = NULL; + ifm = (struct if_msghdr *)next; + next += ifm->ifm_msglen; + + if (ifm->ifm_type == RTM_IFINFO) { + struct if_msghdr *if2m = (struct if_msghdr *)ifm; + struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); + char ifc_name[32]; + + strncpy(ifc_name, sdl->sdl_data, sdl->sdl_nlen); + ifc_name[sdl->sdl_nlen] = 0; + // XXX: ignore usbus interfaces: + // http://lists.freebsd.org/pipermail/freebsd-current/ + // 2011-October/028752.html + // 'ifconfig -a' doesn't show them, nor do we. + if (strncmp(ifc_name, "usbus", 5) == 0) + continue; + + py_ifc_info = Py_BuildValue("(kkkkkkki)", + if2m->ifm_data.ifi_obytes, + if2m->ifm_data.ifi_ibytes, + if2m->ifm_data.ifi_opackets, + if2m->ifm_data.ifi_ipackets, + if2m->ifm_data.ifi_ierrors, + if2m->ifm_data.ifi_oerrors, + if2m->ifm_data.ifi_iqdrops, +#ifdef _IFI_OQDROPS + if2m->ifm_data.ifi_oqdrops +#else + 0 +#endif + ); + if (!py_ifc_info) + goto error; + if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) + goto error; + Py_CLEAR(py_ifc_info); + } + else { + continue; + } + } + + free(buf); + return py_retdict; + +error: + Py_XDECREF(py_ifc_info); + Py_DECREF(py_retdict); + if (buf != NULL) + free(buf); + return NULL; +} diff --git a/psutil/arch/bsd/net.h b/psutil/arch/bsd/net.h new file mode 100644 index 0000000000..99079523c1 --- /dev/null +++ b/psutil/arch/bsd/net.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c new file mode 100644 index 0000000000..e64cf80dc1 --- /dev/null +++ b/psutil/arch/bsd/proc.c @@ -0,0 +1,494 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include +#include +#include +#include // VREG +#ifdef PSUTIL_FREEBSD + #include // kinfo_proc, kinfo_file, KF_* + #include // kinfo_getfile() +#endif + +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" +#ifdef PSUTIL_FREEBSD + #include "../../arch/freebsd/proc.h" +#elif PSUTIL_OPENBSD + #include "../../arch/openbsd/proc.h" +#elif PSUTIL_NETBSD + #include "../../arch/netbsd/proc.h" +#endif + + +// convert a timeval struct to a double +#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) + +#if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) + #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) +#endif + + +/* + * Return a Python list of all the PIDs running on the system. + */ +PyObject * +psutil_pids(PyObject *self, PyObject *args) { + kinfo_proc *proclist = NULL; + kinfo_proc *orig_address = NULL; + size_t num_processes; + size_t idx; + PyObject *py_retlist = PyList_New(0); + PyObject *py_pid = NULL; + + if (py_retlist == NULL) + return NULL; + + if (psutil_get_proc_list(&proclist, &num_processes) != 0) + goto error; + + if (num_processes > 0) { + orig_address = proclist; // save so we can free it after we're done + for (idx = 0; idx < num_processes; idx++) { +#ifdef PSUTIL_FREEBSD + py_pid = PyLong_FromPid(proclist->ki_pid); +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + py_pid = PyLong_FromPid(proclist->p_pid); +#endif + if (!py_pid) + goto error; + if (PyList_Append(py_retlist, py_pid)) + goto error; + Py_CLEAR(py_pid); + proclist++; + } + free(orig_address); + } + + return py_retlist; + +error: + Py_XDECREF(py_pid); + Py_DECREF(py_retlist); + if (orig_address != NULL) + free(orig_address); + return NULL; +} + + +/* + * Collect different info about a process in one shot and return + * them as a big Python tuple. + */ +PyObject * +psutil_proc_oneshot_info(PyObject *self, PyObject *args) { + pid_t pid; + long rss; + long vms; + long memtext; + long memdata; + long memstack; + int oncpu; + kinfo_proc kp; + long pagesize = psutil_getpagesize(); + char str[1000]; + PyObject *py_name; + PyObject *py_ppid; + PyObject *py_retlist; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (psutil_kinfo_proc(pid, &kp) == -1) + return NULL; + + // Process +#ifdef PSUTIL_FREEBSD + sprintf(str, "%s", kp.ki_comm); +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + sprintf(str, "%s", kp.p_comm); +#endif + py_name = PyUnicode_DecodeFSDefault(str); + if (! py_name) { + // Likely a decoding error. We don't want to fail the whole + // operation. The python module may retry with proc_name(). + PyErr_Clear(); + py_name = Py_None; + } + // Py_INCREF(py_name); + + // Calculate memory. +#ifdef PSUTIL_FREEBSD + rss = (long)kp.ki_rssize * pagesize; + vms = (long)kp.ki_size; + memtext = (long)kp.ki_tsize * pagesize; + memdata = (long)kp.ki_dsize * pagesize; + memstack = (long)kp.ki_ssize * pagesize; +#else + rss = (long)kp.p_vm_rssize * pagesize; + #ifdef PSUTIL_OPENBSD + // VMS, this is how ps determines it on OpenBSD: + // https://github.com/openbsd/src/blob/ + // 588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L505 + vms = (long)(kp.p_vm_dsize + kp.p_vm_ssize + kp.p_vm_tsize) * pagesize; + #elif PSUTIL_NETBSD + // VMS, this is how top determines it on NetBSD: + // https://github.com/IIJ-NetBSD/netbsd-src/blob/master/external/ + // bsd/top/dist/machine/m_netbsd.c + vms = (long)kp.p_vm_msize * pagesize; + #endif + memtext = (long)kp.p_vm_tsize * pagesize; + memdata = (long)kp.p_vm_dsize * pagesize; + memstack = (long)kp.p_vm_ssize * pagesize; +#endif + +#ifdef PSUTIL_FREEBSD + // what CPU we're on; top was used as an example: + // https://svnweb.freebsd.org/base/head/usr.bin/top/machine.c? + // view=markup&pathrev=273835 + // XXX - note: for "intr" PID this is -1. + if (kp.ki_stat == SRUN && kp.ki_oncpu != NOCPU) + oncpu = kp.ki_oncpu; + else + oncpu = kp.ki_lastcpu; +#else + // On Net/OpenBSD we have kp.p_cpuid but it appears it's always + // set to KI_NOCPU. Even if it's not, ki_lastcpu does not exist + // so there's no way to determine where "sleeping" processes + // were. Not supported. + oncpu = -1; +#endif + +#ifdef PSUTIL_FREEBSD + py_ppid = PyLong_FromPid(kp.ki_ppid); +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + py_ppid = PyLong_FromPid(kp.p_ppid); +#else + py_ppid = Py_BuildfValue(-1); +#endif + if (! py_ppid) + return NULL; + + // Return a single big tuple with all process info. + py_retlist = Py_BuildValue( +#if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031 + "(OillllllLdllllddddlllllbO)", +#else + "(OillllllidllllddddlllllbO)", +#endif +#ifdef PSUTIL_FREEBSD + py_ppid, // (pid_t) ppid + (int)kp.ki_stat, // (int) status + // UIDs + (long)kp.ki_ruid, // (long) real uid + (long)kp.ki_uid, // (long) effective uid + (long)kp.ki_svuid, // (long) saved uid + // GIDs + (long)kp.ki_rgid, // (long) real gid + (long)kp.ki_groups[0], // (long) effective gid + (long)kp.ki_svuid, // (long) saved gid + // + kp.ki_tdev, // (int or long long) tty nr + PSUTIL_TV2DOUBLE(kp.ki_start), // (double) create time + // ctx switches + kp.ki_rusage.ru_nvcsw, // (long) ctx switches (voluntary) + kp.ki_rusage.ru_nivcsw, // (long) ctx switches (unvoluntary) + // IO count + kp.ki_rusage.ru_inblock, // (long) read io count + kp.ki_rusage.ru_oublock, // (long) write io count + // CPU times: convert from micro seconds to seconds. + PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_utime), // (double) user time + PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_stime), // (double) sys time + PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_utime), // (double) children utime + PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_stime), // (double) children stime + // memory + rss, // (long) rss + vms, // (long) vms + memtext, // (long) mem text + memdata, // (long) mem data + memstack, // (long) mem stack + // others + oncpu, // (int) the CPU we are on +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + py_ppid, // (pid_t) ppid + (int)kp.p_stat, // (int) status + // UIDs + (long)kp.p_ruid, // (long) real uid + (long)kp.p_uid, // (long) effective uid + (long)kp.p_svuid, // (long) saved uid + // GIDs + (long)kp.p_rgid, // (long) real gid + (long)kp.p_groups[0], // (long) effective gid + (long)kp.p_svuid, // (long) saved gid + // + kp.p_tdev, // (int) tty nr + PSUTIL_KPT2DOUBLE(kp.p_ustart), // (double) create time + // ctx switches + kp.p_uru_nvcsw, // (long) ctx switches (voluntary) + kp.p_uru_nivcsw, // (long) ctx switches (unvoluntary) + // IO count + kp.p_uru_inblock, // (long) read io count + kp.p_uru_oublock, // (long) write io count + // CPU times: convert from micro seconds to seconds. + PSUTIL_KPT2DOUBLE(kp.p_uutime), // (double) user time + PSUTIL_KPT2DOUBLE(kp.p_ustime), // (double) sys time + // OpenBSD and NetBSD provide children user + system times summed + // together (no distinction). + kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch utime + kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch stime + // memory + rss, // (long) rss + vms, // (long) vms + memtext, // (long) mem text + memdata, // (long) mem data + memstack, // (long) mem stack + // others + oncpu, // (int) the CPU we are on +#endif + py_name // (pystr) name + ); + + Py_DECREF(py_name); + Py_DECREF(py_ppid); + return py_retlist; +} + + +PyObject * +psutil_proc_name(PyObject *self, PyObject *args) { + pid_t pid; + kinfo_proc kp; + char str[1000]; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (psutil_kinfo_proc(pid, &kp) == -1) + return NULL; + +#ifdef PSUTIL_FREEBSD + sprintf(str, "%s", kp.ki_comm); +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + sprintf(str, "%s", kp.p_comm); +#endif + return PyUnicode_DecodeFSDefault(str); +} + + +PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + int i, cnt = -1; + long pid; + char *s, **envs, errbuf[_POSIX2_LINE_MAX]; + PyObject *py_value=NULL, *py_retdict=NULL; + kvm_t *kd; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 *p; +#else + struct kinfo_proc *p; +#endif + + if (!PyArg_ParseTuple(args, "l", &pid)) + return NULL; + +#if defined(PSUTIL_FREEBSD) + kd = kvm_openfiles(NULL, "/dev/null", NULL, 0, errbuf); +#else + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); +#endif + if (!kd) { + convert_kvm_err("kvm_openfiles", errbuf); + return NULL; + } + + py_retdict = PyDict_New(); + if (!py_retdict) + goto error; + +#if defined(PSUTIL_FREEBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, &cnt); +#elif defined(PSUTIL_OPENBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#elif defined(PSUTIL_NETBSD) + p = kvm_getproc2(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#endif + if (!p) { + NoSuchProcess("kvm_getprocs"); + goto error; + } + if (cnt <= 0) { + NoSuchProcess(cnt < 0 ? kvm_geterr(kd) : "kvm_getprocs: cnt==0"); + goto error; + } + + // On *BSD kernels there are a few kernel-only system processes without an + // environment (See e.g. "procstat -e 0 | 1 | 2 ..." on FreeBSD.) + // Some system process have no stats attached at all + // (they are marked with P_SYSTEM.) + // On FreeBSD, it's possible that the process is swapped or paged out, + // then there no access to the environ stored in the process' user area. + // On NetBSD, we cannot call kvm_getenvv2() for a zombie process. + // To make unittest suite happy, return an empty environment. +#if defined(PSUTIL_FREEBSD) +#if (defined(__FreeBSD_version) && __FreeBSD_version >= 700000) + if (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) { +#else + if ((p)->ki_flag & P_SYSTEM) { +#endif +#elif defined(PSUTIL_NETBSD) + if ((p)->p_stat == SZOMB) { +#elif defined(PSUTIL_OPENBSD) + if ((p)->p_flag & P_SYSTEM) { +#endif + kvm_close(kd); + return py_retdict; + } + +#if defined(PSUTIL_NETBSD) + envs = kvm_getenvv2(kd, p, 0); +#else + envs = kvm_getenvv(kd, p, 0); +#endif + if (!envs) { + // Map to "psutil" general high-level exceptions + switch (errno) { + case 0: + // Process has cleared it's environment, return empty one + kvm_close(kd); + return py_retdict; + case EPERM: + AccessDenied("kvm_getenvv -> EPERM"); + break; + case ESRCH: + NoSuchProcess("kvm_getenvv -> ESRCH"); + break; +#if defined(PSUTIL_FREEBSD) + case ENOMEM: + // Unfortunately, under FreeBSD kvm_getenvv() returns + // failure for certain processes ( e.g. try + // "sudo procstat -e ".) + // Map the error condition to 'AccessDenied'. + sprintf(errbuf, + "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM", + pid, p->ki_uid); + AccessDenied(errbuf); + break; +#endif + default: + sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); + PyErr_SetFromOSErrnoWithSyscall(errbuf); + break; + } + goto error; + } + + for (i = 0; envs[i] != NULL; i++) { + s = strchr(envs[i], '='); + if (!s) + continue; + *s++ = 0; + py_value = PyUnicode_DecodeFSDefault(s); + if (!py_value) + goto error; + if (PyDict_SetItemString(py_retdict, envs[i], py_value)) { + goto error; + } + Py_DECREF(py_value); + } + + kvm_close(kd); + return py_retdict; + +error: + Py_XDECREF(py_value); + Py_XDECREF(py_retdict); + kvm_close(kd); + return NULL; +} + + + /* + * Return files opened by process as a list of (path, fd) tuples. + * TODO: this is broken as it may report empty paths. 'procstat' + * utility has the same problem see: + * https://github.com/giampaolo/psutil/issues/595 + */ +#if (defined(__FreeBSD_version) && __FreeBSD_version >= 800000) || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) +PyObject * +psutil_proc_open_files(PyObject *self, PyObject *args) { + pid_t pid; + int i; + int cnt; + int regular; + int fd; + char *path; + struct kinfo_file *freep = NULL; + struct kinfo_file *kif; + kinfo_proc kipp; + PyObject *py_tuple = NULL; + PyObject *py_path = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + if (psutil_kinfo_proc(pid, &kipp) == -1) + goto error; + + errno = 0; + freep = kinfo_getfile(pid, &cnt); + if (freep == NULL) { +#if !defined(PSUTIL_OPENBSD) + psutil_raise_for_pid(pid, "kinfo_getfile()"); +#endif + goto error; + } + + for (i = 0; i < cnt; i++) { + kif = &freep[i]; + +#ifdef PSUTIL_FREEBSD + regular = (kif->kf_type == KF_TYPE_VNODE) && \ + (kif->kf_vnode_type == KF_VTYPE_VREG); + fd = kif->kf_fd; + path = kif->kf_path; +#elif PSUTIL_OPENBSD + regular = (kif->f_type == DTYPE_VNODE) && (kif->v_type == VREG); + fd = kif->fd_fd; + // XXX - it appears path is not exposed in the kinfo_file struct. + path = ""; +#elif PSUTIL_NETBSD + regular = (kif->ki_ftype == DTYPE_VNODE) && (kif->ki_vtype == VREG); + fd = kif->ki_fd; + // XXX - it appears path is not exposed in the kinfo_file struct. + path = ""; +#endif + if (regular == 1) { + py_path = PyUnicode_DecodeFSDefault(path); + if (! py_path) + goto error; + py_tuple = Py_BuildValue("(Oi)", py_path, fd); + if (py_tuple == NULL) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_path); + Py_CLEAR(py_tuple); + } + } + free(freep); + return py_retlist; + +error: + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + if (freep != NULL) + free(freep); + return NULL; +} +#endif diff --git a/psutil/arch/bsd/proc.h b/psutil/arch/bsd/proc.h new file mode 100644 index 0000000000..2ed8e42d63 --- /dev/null +++ b/psutil/arch/bsd/proc.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_name(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/sys.c b/psutil/arch/bsd/sys.c new file mode 100644 index 0000000000..5911f7a53d --- /dev/null +++ b/psutil/arch/bsd/sys.c @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include // OS version +#ifdef PSUTIL_FREEBSD + #if __FreeBSD_version < 900000 + #include + #else + #include + #endif +#elif PSUTIL_NETBSD + #include +#elif PSUTIL_OPENBSD + #include +#endif + + +// Return a Python float indicating the system boot time expressed in +// seconds since the epoch. +PyObject * +psutil_boot_time(PyObject *self, PyObject *args) { + // fetch sysctl "kern.boottime" + static int request[2] = { CTL_KERN, KERN_BOOTTIME }; + struct timeval boottime; + size_t len = sizeof(boottime); + + if (sysctl(request, 2, &boottime, &len, NULL, 0) == -1) + return PyErr_SetFromErrno(PyExc_OSError); + return Py_BuildValue("d", (double)boottime.tv_sec); +} + + +PyObject * +psutil_users(PyObject *self, PyObject *args) { + PyObject *py_retlist = PyList_New(0); + PyObject *py_username = NULL; + PyObject *py_tty = NULL; + PyObject *py_hostname = NULL; + PyObject *py_tuple = NULL; + PyObject *py_pid = NULL; + + if (py_retlist == NULL) + return NULL; + +#if (defined(__FreeBSD_version) && (__FreeBSD_version < 900000)) || PSUTIL_OPENBSD + struct utmp ut; + FILE *fp; + + Py_BEGIN_ALLOW_THREADS + fp = fopen(_PATH_UTMP, "r"); + Py_END_ALLOW_THREADS + if (fp == NULL) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, _PATH_UTMP); + goto error; + } + + while (fread(&ut, sizeof(ut), 1, fp) == 1) { + if (*ut.ut_name == '\0') + continue; + py_username = PyUnicode_DecodeFSDefault(ut.ut_name); + if (! py_username) + goto error; + py_tty = PyUnicode_DecodeFSDefault(ut.ut_line); + if (! py_tty) + goto error; + py_hostname = PyUnicode_DecodeFSDefault(ut.ut_host); + if (! py_hostname) + goto error; + py_tuple = Py_BuildValue( + "(OOOdi)", + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)ut.ut_time, // start time +#if defined(PSUTIL_OPENBSD) || (defined(__FreeBSD_version) && __FreeBSD_version < 900000) + -1 // process id (set to None later) +#else + ut.ut_pid // TODO: use PyLong_FromPid +#endif + ); + if (!py_tuple) { + fclose(fp); + goto error; + } + if (PyList_Append(py_retlist, py_tuple)) { + fclose(fp); + goto error; + } + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); + } + + fclose(fp); +#else + struct utmpx *utx; + setutxent(); + while ((utx = getutxent()) != NULL) { + if (utx->ut_type != USER_PROCESS) + continue; + py_username = PyUnicode_DecodeFSDefault(utx->ut_user); + if (! py_username) + goto error; + py_tty = PyUnicode_DecodeFSDefault(utx->ut_line); + if (! py_tty) + goto error; + py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); + if (! py_hostname) + goto error; +#ifdef PSUTIL_OPENBSD + py_pid = Py_BuildValue("i", -1); // set to None later +#else + py_pid = PyLong_FromPid(utx->ut_pid); +#endif + if (! py_pid) + goto error; + + py_tuple = Py_BuildValue( + "(OOOdO)", + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)utx->ut_tv.tv_sec, // start time + py_pid // process id + ); + + if (!py_tuple) { + endutxent(); + goto error; + } + if (PyList_Append(py_retlist, py_tuple)) { + endutxent(); + goto error; + } + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); + Py_CLEAR(py_pid); + } + + endutxent(); +#endif + return py_retlist; + +error: + Py_XDECREF(py_username); + Py_XDECREF(py_tty); + Py_XDECREF(py_hostname); + Py_XDECREF(py_tuple); + Py_XDECREF(py_pid); + Py_DECREF(py_retlist); + return NULL; +} diff --git a/psutil/arch/bsd/sys.h b/psutil/arch/bsd/sys.h new file mode 100644 index 0000000000..344ca21d42 --- /dev/null +++ b/psutil/arch/bsd/sys.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index 4923600adb..4682a4dd54 100755 --- a/setup.py +++ b/setup.py @@ -255,13 +255,18 @@ def get_winver(): 'psutil._psutil_bsd', sources=sources + [ 'psutil/_psutil_bsd.c', + 'psutil/arch/bsd/cpu.c', + 'psutil/arch/bsd/disk.c', + 'psutil/arch/bsd/net.c', + 'psutil/arch/bsd/proc.c', + 'psutil/arch/bsd/sys.c', 'psutil/arch/freebsd/cpu.c', - 'psutil/arch/freebsd/mem.c', 'psutil/arch/freebsd/disk.c', - 'psutil/arch/freebsd/sensors.c', + 'psutil/arch/freebsd/mem.c', 'psutil/arch/freebsd/proc.c', - 'psutil/arch/freebsd/sys_socks.c', 'psutil/arch/freebsd/proc_socks.c', + 'psutil/arch/freebsd/sensors.c', + 'psutil/arch/freebsd/sys_socks.c', ], define_macros=macros, libraries=["devstat"], @@ -273,6 +278,11 @@ def get_winver(): 'psutil._psutil_bsd', sources=sources + [ 'psutil/_psutil_bsd.c', + 'psutil/arch/bsd/cpu.c', + 'psutil/arch/bsd/disk.c', + 'psutil/arch/bsd/net.c', + 'psutil/arch/bsd/proc.c', + 'psutil/arch/bsd/sys.c', 'psutil/arch/openbsd/cpu.c', 'psutil/arch/openbsd/disk.c', 'psutil/arch/openbsd/mem.c', @@ -289,6 +299,11 @@ def get_winver(): 'psutil._psutil_bsd', sources=sources + [ 'psutil/_psutil_bsd.c', + 'psutil/arch/bsd/cpu.c', + 'psutil/arch/bsd/disk.c', + 'psutil/arch/bsd/net.c', + 'psutil/arch/bsd/proc.c', + 'psutil/arch/bsd/sys.c', 'psutil/arch/netbsd/cpu.c', 'psutil/arch/netbsd/disk.c', 'psutil/arch/netbsd/mem.c', From 08c24c0767a83faaa8fd9e1fb54723b6dc78f00c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Apr 2023 22:12:16 +0200 Subject: [PATCH 0979/1714] Win, C, refact: move proc funcs into proc.c file Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 2 + psutil/_psutil_windows.c | 1225 +-------------------------- psutil/arch/windows/proc.c | 1238 ++++++++++++++++++++++++++++ psutil/arch/windows/proc.h | 34 + psutil/arch/windows/process_info.h | 3 + 5 files changed, 1278 insertions(+), 1224 deletions(-) create mode 100644 psutil/arch/windows/proc.c create mode 100644 psutil/arch/windows/proc.h diff --git a/MANIFEST.in b/MANIFEST.in index db7079354b..d6f45fb257 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -108,6 +108,8 @@ include psutil/arch/windows/mem.h include psutil/arch/windows/net.c include psutil/arch/windows/net.h include psutil/arch/windows/ntextapi.h +include psutil/arch/windows/proc.c +include psutil/arch/windows/proc.h include psutil/arch/windows/process_handles.c include psutil/arch/windows/process_handles.h include psutil/arch/windows/process_info.c diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index ef85433272..272e39a8c6 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -14,23 +14,15 @@ * - NtResumeProcess */ -// Fixes clash between winsock2.h and windows.h -#define WIN32_LEAN_AND_MEAN - #include #include -#include // memory_info(), memory_maps() -#include -#include // threads(), PROCESSENTRY32 - -// Link with Iphlpapi.lib -#pragma comment(lib, "IPHLPAPI.lib") #include "_psutil_common.h" #include "arch/windows/cpu.h" #include "arch/windows/disk.h" #include "arch/windows/mem.h" #include "arch/windows/net.h" +#include "arch/windows/proc.h" #include "arch/windows/process_handles.h" #include "arch/windows/process_info.h" #include "arch/windows/process_utils.h" @@ -41,1221 +33,6 @@ #include "arch/windows/sys.h" #include "arch/windows/wmi.h" -// Raised by Process.wait(). -static PyObject *TimeoutExpired; -static PyObject *TimeoutAbandoned; - - - -/* - * Return 1 if PID exists in the current process list, else 0. - */ -static PyObject * -psutil_pid_exists(PyObject *self, PyObject *args) { - DWORD pid; - int status; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - status = psutil_pid_is_running(pid); - if (-1 == status) - return NULL; // exception raised in psutil_pid_is_running() - return PyBool_FromLong(status); -} - - -/* - * Return a Python list of all the PIDs running on the system. - */ -static PyObject * -psutil_pids(PyObject *self, PyObject *args) { - DWORD *proclist = NULL; - DWORD numberOfReturnedPIDs; - DWORD i; - PyObject *py_pid = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - proclist = psutil_get_pids(&numberOfReturnedPIDs); - if (proclist == NULL) - goto error; - - for (i = 0; i < numberOfReturnedPIDs; i++) { - py_pid = PyLong_FromPid(proclist[i]); - if (!py_pid) - goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_CLEAR(py_pid); - } - - // free C array allocated for PIDs - free(proclist); - return py_retlist; - -error: - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - if (proclist != NULL) - free(proclist); - return NULL; -} - - -/* - * Kill a process given its PID. - */ -static PyObject * -psutil_proc_kill(PyObject *self, PyObject *args) { - HANDLE hProcess; - DWORD pid; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (pid == 0) - return AccessDenied("automatically set for PID 0"); - - hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); - hProcess = psutil_check_phandle(hProcess, pid, 0); - if (hProcess == NULL) { - return NULL; - } - - if (! TerminateProcess(hProcess, SIGTERM)) { - // ERROR_ACCESS_DENIED may happen if the process already died. See: - // https://github.com/giampaolo/psutil/issues/1099 - // http://bugs.python.org/issue14252 - if (GetLastError() != ERROR_ACCESS_DENIED) { - PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); - return NULL; - } - } - - CloseHandle(hProcess); - Py_RETURN_NONE; -} - - -/* - * Wait for process to terminate and return its exit code. - */ -static PyObject * -psutil_proc_wait(PyObject *self, PyObject *args) { - HANDLE hProcess; - DWORD ExitCode; - DWORD retVal; - DWORD pid; - long timeout; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "l", &pid, &timeout)) - return NULL; - if (pid == 0) - return AccessDenied("automatically set for PID 0"); - - hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, - FALSE, pid); - if (hProcess == NULL) { - if (GetLastError() == ERROR_INVALID_PARAMETER) { - // no such process; we do not want to raise NSP but - // return None instead. - Py_RETURN_NONE; - } - else { - PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); - return NULL; - } - } - - // wait until the process has terminated - Py_BEGIN_ALLOW_THREADS - retVal = WaitForSingleObject(hProcess, timeout); - Py_END_ALLOW_THREADS - - // handle return code - if (retVal == WAIT_FAILED) { - PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); - CloseHandle(hProcess); - return NULL; - } - if (retVal == WAIT_TIMEOUT) { - PyErr_SetString(TimeoutExpired, - "WaitForSingleObject() returned WAIT_TIMEOUT"); - CloseHandle(hProcess); - return NULL; - } - if (retVal == WAIT_ABANDONED) { - psutil_debug("WaitForSingleObject() -> WAIT_ABANDONED"); - PyErr_SetString(TimeoutAbandoned, - "WaitForSingleObject() returned WAIT_ABANDONED"); - CloseHandle(hProcess); - return NULL; - } - - // WaitForSingleObject() returned WAIT_OBJECT_0. It means the - // process is gone so we can get its process exit code. The PID - // may still stick around though but we'll handle that from Python. - if (GetExitCodeProcess(hProcess, &ExitCode) == 0) { - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); - -#if PY_MAJOR_VERSION >= 3 - return PyLong_FromLong((long) ExitCode); -#else - return PyInt_FromLong((long) ExitCode); -#endif -} - - -/* - * Return a Python tuple (user_time, kernel_time) - */ -static PyObject * -psutil_proc_times(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - FILETIME ftCreate, ftExit, ftKernel, ftUser; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - - if (hProcess == NULL) - return NULL; - if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { - if (GetLastError() == ERROR_ACCESS_DENIED) { - // usually means the process has died so we throw a NoSuchProcess - // here - NoSuchProcess("GetProcessTimes -> ERROR_ACCESS_DENIED"); - } - else { - PyErr_SetFromWindowsErr(0); - } - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); - - /* - * User and kernel times are represented as a FILETIME structure - * which contains a 64-bit value representing the number of - * 100-nanosecond intervals since January 1, 1601 (UTC): - * http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx - * To convert it into a float representing the seconds that the - * process has executed in user/kernel mode I borrowed the code - * below from Python's Modules/posixmodule.c - */ - return Py_BuildValue( - "(ddd)", - (double)(ftUser.dwHighDateTime * HI_T + \ - ftUser.dwLowDateTime * LO_T), - (double)(ftKernel.dwHighDateTime * HI_T + \ - ftKernel.dwLowDateTime * LO_T), - psutil_FiletimeToUnixTime(ftCreate) - ); -} - - -/* - * Return process executable path. Works for all processes regardless of - * privilege. NtQuerySystemInformation has some sort of internal cache, - * since it succeeds even when a process is gone (but not if a PID never - * existed). - */ -static PyObject * -psutil_proc_exe(PyObject *self, PyObject *args) { - DWORD pid; - NTSTATUS status; - PVOID buffer = NULL; - ULONG bufferSize = 0x104 * 2; // WIN_MAX_PATH * sizeof(wchar_t) - SYSTEM_PROCESS_ID_INFORMATION processIdInfo; - PyObject *py_exe; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - if (pid == 0) - return AccessDenied("automatically set for PID 0"); - - buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { - PyErr_NoMemory(); - return NULL; - } - - processIdInfo.ProcessId = (HANDLE)(ULONG_PTR)pid; - processIdInfo.ImageName.Length = 0; - processIdInfo.ImageName.MaximumLength = (USHORT)bufferSize; - processIdInfo.ImageName.Buffer = buffer; - - status = NtQuerySystemInformation( - SystemProcessIdInformation, - &processIdInfo, - sizeof(SYSTEM_PROCESS_ID_INFORMATION), - NULL); - - if ((status == STATUS_INFO_LENGTH_MISMATCH) && - (processIdInfo.ImageName.MaximumLength <= bufferSize)) - { - // Required length was NOT stored in MaximumLength (WOW64 issue). - ULONG maxBufferSize = 0x7FFF * 2; // NTFS_MAX_PATH * sizeof(wchar_t) - do { - // Iteratively double the size of the buffer up to maxBufferSize - bufferSize *= 2; - FREE(buffer); - buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { - PyErr_NoMemory(); - return NULL; - } - - processIdInfo.ImageName.MaximumLength = (USHORT)bufferSize; - processIdInfo.ImageName.Buffer = buffer; - - status = NtQuerySystemInformation( - SystemProcessIdInformation, - &processIdInfo, - sizeof(SYSTEM_PROCESS_ID_INFORMATION), - NULL); - } while ((status == STATUS_INFO_LENGTH_MISMATCH) && - (bufferSize <= maxBufferSize)); - } - else if (status == STATUS_INFO_LENGTH_MISMATCH) { - // Required length is stored in MaximumLength. - FREE(buffer); - buffer = MALLOC_ZERO(processIdInfo.ImageName.MaximumLength); - if (! buffer) { - PyErr_NoMemory(); - return NULL; - } - - processIdInfo.ImageName.Buffer = buffer; - - status = NtQuerySystemInformation( - SystemProcessIdInformation, - &processIdInfo, - sizeof(SYSTEM_PROCESS_ID_INFORMATION), - NULL); - } - - if (! NT_SUCCESS(status)) { - FREE(buffer); - if (psutil_pid_is_running(pid) == 0) - NoSuchProcess("psutil_pid_is_running -> 0"); - else - psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); - return NULL; - } - - if (processIdInfo.ImageName.Buffer == NULL) { - // Happens for PID 4. - py_exe = Py_BuildValue("s", ""); - } - else { - py_exe = PyUnicode_FromWideChar(processIdInfo.ImageName.Buffer, - processIdInfo.ImageName.Length / 2); - } - FREE(buffer); - return py_exe; -} - - -/* - * Return process memory information as a Python tuple. - */ -static PyObject * -psutil_proc_memory_info(PyObject *self, PyObject *args) { - HANDLE hProcess; - DWORD pid; - PROCESS_MEMORY_COUNTERS_EX cnt; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (NULL == hProcess) - return NULL; - - if (! GetProcessMemoryInfo(hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, - sizeof(cnt))) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hProcess); - return NULL; - } - CloseHandle(hProcess); - - // PROCESS_MEMORY_COUNTERS values are defined as SIZE_T which on 64bits - // is an (unsigned long long) and on 32bits is an (unsigned int). - // "_WIN64" is defined if we're running a 64bit Python interpreter not - // exclusively if the *system* is 64bit. -#if defined(_WIN64) - return Py_BuildValue( - "(kKKKKKKKKK)", - cnt.PageFaultCount, // unsigned long - (unsigned long long)cnt.PeakWorkingSetSize, - (unsigned long long)cnt.WorkingSetSize, - (unsigned long long)cnt.QuotaPeakPagedPoolUsage, - (unsigned long long)cnt.QuotaPagedPoolUsage, - (unsigned long long)cnt.QuotaPeakNonPagedPoolUsage, - (unsigned long long)cnt.QuotaNonPagedPoolUsage, - (unsigned long long)cnt.PagefileUsage, - (unsigned long long)cnt.PeakPagefileUsage, - (unsigned long long)cnt.PrivateUsage); -#else - return Py_BuildValue( - "(kIIIIIIIII)", - cnt.PageFaultCount, // unsigned long - (unsigned int)cnt.PeakWorkingSetSize, - (unsigned int)cnt.WorkingSetSize, - (unsigned int)cnt.QuotaPeakPagedPoolUsage, - (unsigned int)cnt.QuotaPagedPoolUsage, - (unsigned int)cnt.QuotaPeakNonPagedPoolUsage, - (unsigned int)cnt.QuotaNonPagedPoolUsage, - (unsigned int)cnt.PagefileUsage, - (unsigned int)cnt.PeakPagefileUsage, - (unsigned int)cnt.PrivateUsage); -#endif -} - - -static int -psutil_GetProcWsetInformation( - DWORD pid, - HANDLE hProcess, - PMEMORY_WORKING_SET_INFORMATION *wSetInfo) -{ - NTSTATUS status; - PVOID buffer; - SIZE_T bufferSize; - - bufferSize = 0x8000; - buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { - PyErr_NoMemory(); - return 1; - } - - while ((status = NtQueryVirtualMemory( - hProcess, - NULL, - MemoryWorkingSetInformation, - buffer, - bufferSize, - NULL)) == STATUS_INFO_LENGTH_MISMATCH) - { - FREE(buffer); - bufferSize *= 2; - // Fail if we're resizing the buffer to something very large. - if (bufferSize > 256 * 1024 * 1024) { - PyErr_SetString(PyExc_RuntimeError, - "NtQueryVirtualMemory bufsize is too large"); - return 1; - } - buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { - PyErr_NoMemory(); - return 1; - } - } - - if (!NT_SUCCESS(status)) { - if (status == STATUS_ACCESS_DENIED) { - AccessDenied("NtQueryVirtualMemory -> STATUS_ACCESS_DENIED"); - } - else if (psutil_pid_is_running(pid) == 0) { - NoSuchProcess("psutil_pid_is_running -> 0"); - } - else { - PyErr_Clear(); - psutil_SetFromNTStatusErr( - status, "NtQueryVirtualMemory(MemoryWorkingSetInformation)"); - } - HeapFree(GetProcessHeap(), 0, buffer); - return 1; - } - - *wSetInfo = (PMEMORY_WORKING_SET_INFORMATION)buffer; - return 0; -} - - -/* - * Returns the USS of the process. - * Reference: - * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ - * nsMemoryReporterManager.cpp - */ -static PyObject * -psutil_proc_memory_uss(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - PSUTIL_PROCESS_WS_COUNTERS wsCounters; - PMEMORY_WORKING_SET_INFORMATION wsInfo; - ULONG_PTR i; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION); - if (hProcess == NULL) - return NULL; - - if (psutil_GetProcWsetInformation(pid, hProcess, &wsInfo) != 0) { - CloseHandle(hProcess); - return NULL; - } - memset(&wsCounters, 0, sizeof(PSUTIL_PROCESS_WS_COUNTERS)); - - for (i = 0; i < wsInfo->NumberOfEntries; i++) { - // This is what ProcessHacker does. - /* - wsCounters.NumberOfPages++; - if (wsInfo->WorkingSetInfo[i].ShareCount > 1) - wsCounters.NumberOfSharedPages++; - if (wsInfo->WorkingSetInfo[i].ShareCount == 0) - wsCounters.NumberOfPrivatePages++; - if (wsInfo->WorkingSetInfo[i].Shared) - wsCounters.NumberOfShareablePages++; - */ - - // This is what we do: count shared pages that only one process - // is using as private (USS). - if (!wsInfo->WorkingSetInfo[i].Shared || - wsInfo->WorkingSetInfo[i].ShareCount <= 1) { - wsCounters.NumberOfPrivatePages++; - } - } - - HeapFree(GetProcessHeap(), 0, wsInfo); - CloseHandle(hProcess); - - return Py_BuildValue("I", wsCounters.NumberOfPrivatePages); -} - - -/* - * Resume or suspends a process - */ -static PyObject * -psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { - DWORD pid; - NTSTATUS status; - HANDLE hProcess; - PyObject* suspend; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) - return NULL; - - hProcess = psutil_handle_from_pid(pid, PROCESS_SUSPEND_RESUME); - if (hProcess == NULL) - return NULL; - - if (PyObject_IsTrue(suspend)) - status = NtSuspendProcess(hProcess); - else - status = NtResumeProcess(hProcess); - - if (! NT_SUCCESS(status)) { - CloseHandle(hProcess); - return psutil_SetFromNTStatusErr(status, "NtSuspend|ResumeProcess"); - } - - CloseHandle(hProcess); - Py_RETURN_NONE; -} - - -static PyObject * -psutil_proc_threads(PyObject *self, PyObject *args) { - HANDLE hThread = NULL; - THREADENTRY32 te32 = {0}; - DWORD pid; - int pid_return; - int rc; - FILETIME ftDummy, ftKernel, ftUser; - HANDLE hThreadSnap = NULL; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - goto error; - if (pid == 0) { - // raise AD instead of returning 0 as procexp is able to - // retrieve useful information somehow - AccessDenied("forced for PID 0"); - goto error; - } - - pid_return = psutil_pid_is_running(pid); - if (pid_return == 0) { - NoSuchProcess("psutil_pid_is_running -> 0"); - goto error; - } - if (pid_return == -1) - goto error; - - hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); - if (hThreadSnap == INVALID_HANDLE_VALUE) { - PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); - goto error; - } - - // Fill in the size of the structure before using it - te32.dwSize = sizeof(THREADENTRY32); - - if (! Thread32First(hThreadSnap, &te32)) { - PyErr_SetFromOSErrnoWithSyscall("Thread32First"); - goto error; - } - - // Walk the thread snapshot to find all threads of the process. - // If the thread belongs to the process, increase the counter. - do { - if (te32.th32OwnerProcessID == pid) { - py_tuple = NULL; - hThread = NULL; - hThread = OpenThread(THREAD_QUERY_INFORMATION, - FALSE, te32.th32ThreadID); - if (hThread == NULL) { - // thread has disappeared on us - continue; - } - - rc = GetThreadTimes(hThread, &ftDummy, &ftDummy, &ftKernel, - &ftUser); - if (rc == 0) { - PyErr_SetFromOSErrnoWithSyscall("GetThreadTimes"); - goto error; - } - - /* - * User and kernel times are represented as a FILETIME structure - * which contains a 64-bit value representing the number of - * 100-nanosecond intervals since January 1, 1601 (UTC): - * http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx - * To convert it into a float representing the seconds that the - * process has executed in user/kernel mode I borrowed the code - * below from Python's Modules/posixmodule.c - */ - py_tuple = Py_BuildValue( - "kdd", - te32.th32ThreadID, - (double)(ftUser.dwHighDateTime * HI_T + \ - ftUser.dwLowDateTime * LO_T), - (double)(ftKernel.dwHighDateTime * HI_T + \ - ftKernel.dwLowDateTime * LO_T)); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - - CloseHandle(hThread); - } - } while (Thread32Next(hThreadSnap, &te32)); - - CloseHandle(hThreadSnap); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (hThread != NULL) - CloseHandle(hThread); - if (hThreadSnap != NULL) - CloseHandle(hThreadSnap); - return NULL; -} - - -static PyObject * -psutil_proc_open_files(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE processHandle; - DWORD access = PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION; - PyObject *py_retlist; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - processHandle = psutil_handle_from_pid(pid, access); - if (processHandle == NULL) - return NULL; - - py_retlist = psutil_get_open_files(pid, processHandle); - CloseHandle(processHandle); - return py_retlist; -} - - -static PTOKEN_USER -_psutil_user_token_from_pid(DWORD pid) { - HANDLE hProcess = NULL; - HANDLE hToken = NULL; - PTOKEN_USER userToken = NULL; - ULONG bufferSize = 0x100; - - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (hProcess == NULL) - return NULL; - - if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { - PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); - goto error; - } - - // Get the user SID. - while (1) { - userToken = malloc(bufferSize); - if (userToken == NULL) { - PyErr_NoMemory(); - goto error; - } - if (!GetTokenInformation(hToken, TokenUser, userToken, bufferSize, - &bufferSize)) - { - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - free(userToken); - continue; - } - else { - PyErr_SetFromOSErrnoWithSyscall("GetTokenInformation"); - goto error; - } - } - break; - } - - CloseHandle(hProcess); - CloseHandle(hToken); - return userToken; - -error: - if (hProcess != NULL) - CloseHandle(hProcess); - if (hToken != NULL) - CloseHandle(hToken); - return NULL; -} - - -/* - * Return process username as a "DOMAIN//USERNAME" string. - */ -static PyObject * -psutil_proc_username(PyObject *self, PyObject *args) { - DWORD pid; - PTOKEN_USER userToken = NULL; - WCHAR *userName = NULL; - WCHAR *domainName = NULL; - ULONG nameSize = 0x100; - ULONG domainNameSize = 0x100; - SID_NAME_USE nameUse; - PyObject *py_username = NULL; - PyObject *py_domain = NULL; - PyObject *py_tuple = NULL; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - userToken = _psutil_user_token_from_pid(pid); - if (userToken == NULL) - return NULL; - - // resolve the SID to a name - while (1) { - userName = malloc(nameSize * sizeof(WCHAR)); - if (userName == NULL) { - PyErr_NoMemory(); - goto error; - } - domainName = malloc(domainNameSize * sizeof(WCHAR)); - if (domainName == NULL) { - PyErr_NoMemory(); - goto error; - } - if (!LookupAccountSidW(NULL, userToken->User.Sid, userName, &nameSize, - domainName, &domainNameSize, &nameUse)) - { - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - free(userName); - free(domainName); - continue; - } - else if (GetLastError() == ERROR_NONE_MAPPED) { - // From MS doc: - // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ - // nf-winbase-lookupaccountsida - // If the function cannot find an account name for the SID, - // GetLastError returns ERROR_NONE_MAPPED. This can occur if - // a network time-out prevents the function from finding the - // name. It also occurs for SIDs that have no corresponding - // account name, such as a logon SID that identifies a logon - // session. - AccessDenied("LookupAccountSidW -> ERROR_NONE_MAPPED"); - goto error; - } - else { - PyErr_SetFromOSErrnoWithSyscall("LookupAccountSidW"); - goto error; - } - } - break; - } - - py_domain = PyUnicode_FromWideChar(domainName, wcslen(domainName)); - if (! py_domain) - goto error; - py_username = PyUnicode_FromWideChar(userName, wcslen(userName)); - if (! py_username) - goto error; - py_tuple = Py_BuildValue("OO", py_domain, py_username); - if (! py_tuple) - goto error; - Py_DECREF(py_domain); - Py_DECREF(py_username); - - free(userName); - free(domainName); - free(userToken); - return py_tuple; - -error: - if (userName != NULL) - free(userName); - if (domainName != NULL) - free(domainName); - if (userToken != NULL) - free(userToken); - Py_XDECREF(py_domain); - Py_XDECREF(py_username); - Py_XDECREF(py_tuple); - return NULL; -} - - -/* - * Get process priority as a Python integer. - */ -static PyObject * -psutil_proc_priority_get(PyObject *self, PyObject *args) { - DWORD pid; - DWORD priority; - HANDLE hProcess; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (hProcess == NULL) - return NULL; - - priority = GetPriorityClass(hProcess); - if (priority == 0) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hProcess); - return NULL; - } - CloseHandle(hProcess); - return Py_BuildValue("i", priority); -} - - -/* - * Set process priority. - */ -static PyObject * -psutil_proc_priority_set(PyObject *self, PyObject *args) { - DWORD pid; - int priority; - int retval; - HANDLE hProcess; - DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) - return NULL; - hProcess = psutil_handle_from_pid(pid, access); - if (hProcess == NULL) - return NULL; - - retval = SetPriorityClass(hProcess, priority); - if (retval == 0) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); - Py_RETURN_NONE; -} - - -/* - * Get process IO priority as a Python integer. - */ -static PyObject * -psutil_proc_io_priority_get(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - DWORD IoPriority; - NTSTATUS status; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (hProcess == NULL) - return NULL; - - status = NtQueryInformationProcess( - hProcess, - ProcessIoPriority, - &IoPriority, - sizeof(DWORD), - NULL - ); - - CloseHandle(hProcess); - if (! NT_SUCCESS(status)) - return psutil_SetFromNTStatusErr(status, "NtQueryInformationProcess"); - return Py_BuildValue("i", IoPriority); -} - - -/* - * Set process IO priority. - */ -static PyObject * -psutil_proc_io_priority_set(PyObject *self, PyObject *args) { - DWORD pid; - DWORD prio; - HANDLE hProcess; - NTSTATUS status; - DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &prio)) - return NULL; - - hProcess = psutil_handle_from_pid(pid, access); - if (hProcess == NULL) - return NULL; - - status = NtSetInformationProcess( - hProcess, - ProcessIoPriority, - (PVOID)&prio, - sizeof(DWORD) - ); - - CloseHandle(hProcess); - if (! NT_SUCCESS(status)) - return psutil_SetFromNTStatusErr(status, "NtSetInformationProcess"); - Py_RETURN_NONE; -} - - -/* - * Return a Python tuple referencing process I/O counters. - */ -static PyObject * -psutil_proc_io_counters(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - IO_COUNTERS IoCounters; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (NULL == hProcess) - return NULL; - - if (! GetProcessIoCounters(hProcess, &IoCounters)) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); - return Py_BuildValue("(KKKKKK)", - IoCounters.ReadOperationCount, - IoCounters.WriteOperationCount, - IoCounters.ReadTransferCount, - IoCounters.WriteTransferCount, - IoCounters.OtherOperationCount, - IoCounters.OtherTransferCount); -} - - -/* - * Return process CPU affinity as a bitmask - */ -static PyObject * -psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - DWORD_PTR proc_mask; - DWORD_PTR system_mask; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (hProcess == NULL) { - return NULL; - } - if (GetProcessAffinityMask(hProcess, &proc_mask, &system_mask) == 0) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); -#ifdef _WIN64 - return Py_BuildValue("K", (unsigned long long)proc_mask); -#else - return Py_BuildValue("k", (unsigned long)proc_mask); -#endif -} - - -/* - * Set process CPU affinity - */ -static PyObject * -psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - DWORD_PTR mask; - -#ifdef _WIN64 - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "K", &pid, &mask)) -#else - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "k", &pid, &mask)) -#endif - { - return NULL; - } - hProcess = psutil_handle_from_pid(pid, access); - if (hProcess == NULL) - return NULL; - - if (SetProcessAffinityMask(hProcess, mask) == 0) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hProcess); - return NULL; - } - - CloseHandle(hProcess); - Py_RETURN_NONE; -} - - -/* - * Return True if all process threads are in waiting/suspended state. - */ -static PyObject * -psutil_proc_is_suspended(PyObject *self, PyObject *args) { - DWORD pid; - ULONG i; - PSYSTEM_PROCESS_INFORMATION process; - PVOID buffer; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (! psutil_get_proc_info(pid, &process, &buffer)) - return NULL; - for (i = 0; i < process->NumberOfThreads; i++) { - if (process->Threads[i].ThreadState != Waiting || - process->Threads[i].WaitReason != Suspended) - { - free(buffer); - Py_RETURN_FALSE; - } - } - free(buffer); - Py_RETURN_TRUE; -} - - -/* - * Return the number of handles opened by process. - */ -static PyObject * -psutil_proc_num_handles(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - DWORD handleCount; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); - if (NULL == hProcess) - return NULL; - if (! GetProcessHandleCount(hProcess, &handleCount)) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hProcess); - return NULL; - } - CloseHandle(hProcess); - return Py_BuildValue("k", handleCount); -} - - -static char *get_region_protection_string(ULONG protection) { - switch (protection & 0xff) { - case PAGE_NOACCESS: - return ""; - case PAGE_READONLY: - return "r"; - case PAGE_READWRITE: - return "rw"; - case PAGE_WRITECOPY: - return "wc"; - case PAGE_EXECUTE: - return "x"; - case PAGE_EXECUTE_READ: - return "xr"; - case PAGE_EXECUTE_READWRITE: - return "xrw"; - case PAGE_EXECUTE_WRITECOPY: - return "xwc"; - default: - return "?"; - } -} - - -/* - * Return a list of process's memory mappings. - */ -static PyObject * -psutil_proc_memory_maps(PyObject *self, PyObject *args) { - MEMORY_BASIC_INFORMATION basicInfo; - DWORD pid; - HANDLE hProcess = NULL; - PVOID baseAddress; - WCHAR mappedFileName[MAX_PATH]; - LPVOID maxAddr; - // required by GetMappedFileNameW - DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_str = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - goto error; - hProcess = psutil_handle_from_pid(pid, access); - if (NULL == hProcess) - goto error; - - maxAddr = PSUTIL_SYSTEM_INFO.lpMaximumApplicationAddress; - baseAddress = NULL; - - while (VirtualQueryEx(hProcess, baseAddress, &basicInfo, - sizeof(MEMORY_BASIC_INFORMATION))) - { - py_tuple = NULL; - if (baseAddress > maxAddr) - break; - if (GetMappedFileNameW(hProcess, baseAddress, mappedFileName, - sizeof(mappedFileName))) - { - py_str = PyUnicode_FromWideChar(mappedFileName, - wcslen(mappedFileName)); - if (py_str == NULL) - goto error; -#ifdef _WIN64 - py_tuple = Py_BuildValue( - "(KsOI)", - (unsigned long long)baseAddress, -#else - py_tuple = Py_BuildValue( - "(ksOI)", - (unsigned long)baseAddress, -#endif - get_region_protection_string(basicInfo.Protect), - py_str, - basicInfo.RegionSize); - - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - Py_CLEAR(py_str); - } - baseAddress = (PCHAR)baseAddress + basicInfo.RegionSize; - } - - CloseHandle(hProcess); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_XDECREF(py_str); - Py_DECREF(py_retlist); - if (hProcess != NULL) - CloseHandle(hProcess); - return NULL; -} - - -/* - * Return a {pid:ppid, ...} dict for all running processes. - */ -static PyObject * -psutil_ppid_map(PyObject *self, PyObject *args) { - PyObject *py_pid = NULL; - PyObject *py_ppid = NULL; - PyObject *py_retdict = PyDict_New(); - HANDLE handle = NULL; - PROCESSENTRY32 pe = {0}; - pe.dwSize = sizeof(PROCESSENTRY32); - - if (py_retdict == NULL) - return NULL; - handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (handle == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); - Py_DECREF(py_retdict); - return NULL; - } - - if (Process32First(handle, &pe)) { - do { - py_pid = PyLong_FromPid(pe.th32ProcessID); - if (py_pid == NULL) - goto error; - py_ppid = PyLong_FromPid(pe.th32ParentProcessID); - if (py_ppid == NULL) - goto error; - if (PyDict_SetItem(py_retdict, py_pid, py_ppid)) - goto error; - Py_CLEAR(py_pid); - Py_CLEAR(py_ppid); - } while (Process32Next(handle, &pe)); - } - - CloseHandle(handle); - return py_retdict; - -error: - Py_XDECREF(py_pid); - Py_XDECREF(py_ppid); - Py_DECREF(py_retdict); - CloseHandle(handle); - return NULL; -} - // ------------------------ Python init --------------------------- diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c new file mode 100644 index 0000000000..fb5f7bb904 --- /dev/null +++ b/psutil/arch/windows/proc.c @@ -0,0 +1,1238 @@ +/* + * 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. + */ + +// Fixes clash between winsock2.h and windows.h +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include // memory_info(), memory_maps() +#include +#include // threads(), PROCESSENTRY32 + +// Link with Iphlpapi.lib +#pragma comment(lib, "IPHLPAPI.lib") + +#include "../../_psutil_common.h" +#include "proc.h" +#include "process_info.h" +#include "process_handles.h" +#include "process_utils.h" + + +// Raised by Process.wait(). +PyObject *TimeoutExpired; +PyObject *TimeoutAbandoned; + + +/* + * Return 1 if PID exists in the current process list, else 0. + */ +PyObject * +psutil_pid_exists(PyObject *self, PyObject *args) { + DWORD pid; + int status; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + status = psutil_pid_is_running(pid); + if (-1 == status) + return NULL; // exception raised in psutil_pid_is_running() + return PyBool_FromLong(status); +} + + +/* + * Return a Python list of all the PIDs running on the system. + */ +PyObject * +psutil_pids(PyObject *self, PyObject *args) { + DWORD *proclist = NULL; + DWORD numberOfReturnedPIDs; + DWORD i; + PyObject *py_pid = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + proclist = psutil_get_pids(&numberOfReturnedPIDs); + if (proclist == NULL) + goto error; + + for (i = 0; i < numberOfReturnedPIDs; i++) { + py_pid = PyLong_FromPid(proclist[i]); + if (!py_pid) + goto error; + if (PyList_Append(py_retlist, py_pid)) + goto error; + Py_CLEAR(py_pid); + } + + // free C array allocated for PIDs + free(proclist); + return py_retlist; + +error: + Py_XDECREF(py_pid); + Py_DECREF(py_retlist); + if (proclist != NULL) + free(proclist); + return NULL; +} + + +/* + * Kill a process given its PID. + */ +PyObject * +psutil_proc_kill(PyObject *self, PyObject *args) { + HANDLE hProcess; + DWORD pid; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (pid == 0) + return AccessDenied("automatically set for PID 0"); + + hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + hProcess = psutil_check_phandle(hProcess, pid, 0); + if (hProcess == NULL) { + return NULL; + } + + if (! TerminateProcess(hProcess, SIGTERM)) { + // ERROR_ACCESS_DENIED may happen if the process already died. See: + // https://github.com/giampaolo/psutil/issues/1099 + // http://bugs.python.org/issue14252 + if (GetLastError() != ERROR_ACCESS_DENIED) { + PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); + return NULL; + } + } + + CloseHandle(hProcess); + Py_RETURN_NONE; +} + + +/* + * Wait for process to terminate and return its exit code. + */ +PyObject * +psutil_proc_wait(PyObject *self, PyObject *args) { + HANDLE hProcess; + DWORD ExitCode; + DWORD retVal; + DWORD pid; + long timeout; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "l", &pid, &timeout)) + return NULL; + if (pid == 0) + return AccessDenied("automatically set for PID 0"); + + hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, + FALSE, pid); + if (hProcess == NULL) { + if (GetLastError() == ERROR_INVALID_PARAMETER) { + // no such process; we do not want to raise NSP but + // return None instead. + Py_RETURN_NONE; + } + else { + PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + return NULL; + } + } + + // wait until the process has terminated + Py_BEGIN_ALLOW_THREADS + retVal = WaitForSingleObject(hProcess, timeout); + Py_END_ALLOW_THREADS + + // handle return code + if (retVal == WAIT_FAILED) { + PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); + CloseHandle(hProcess); + return NULL; + } + if (retVal == WAIT_TIMEOUT) { + PyErr_SetString(TimeoutExpired, + "WaitForSingleObject() returned WAIT_TIMEOUT"); + CloseHandle(hProcess); + return NULL; + } + if (retVal == WAIT_ABANDONED) { + psutil_debug("WaitForSingleObject() -> WAIT_ABANDONED"); + PyErr_SetString(TimeoutAbandoned, + "WaitForSingleObject() returned WAIT_ABANDONED"); + CloseHandle(hProcess); + return NULL; + } + + // WaitForSingleObject() returned WAIT_OBJECT_0. It means the + // process is gone so we can get its process exit code. The PID + // may still stick around though but we'll handle that from Python. + if (GetExitCodeProcess(hProcess, &ExitCode) == 0) { + PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + +#if PY_MAJOR_VERSION >= 3 + return PyLong_FromLong((long) ExitCode); +#else + return PyInt_FromLong((long) ExitCode); +#endif +} + + +/* + * Return a Python tuple (user_time, kernel_time) + */ +PyObject * +psutil_proc_times(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE hProcess; + FILETIME ftCreate, ftExit, ftKernel, ftUser; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + + if (hProcess == NULL) + return NULL; + if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { + if (GetLastError() == ERROR_ACCESS_DENIED) { + // usually means the process has died so we throw a NoSuchProcess + // here + NoSuchProcess("GetProcessTimes -> ERROR_ACCESS_DENIED"); + } + else { + PyErr_SetFromWindowsErr(0); + } + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + + /* + * User and kernel times are represented as a FILETIME structure + * which contains a 64-bit value representing the number of + * 100-nanosecond intervals since January 1, 1601 (UTC): + * http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx + * To convert it into a float representing the seconds that the + * process has executed in user/kernel mode I borrowed the code + * below from Python's Modules/posixmodule.c + */ + return Py_BuildValue( + "(ddd)", + (double)(ftUser.dwHighDateTime * HI_T + \ + ftUser.dwLowDateTime * LO_T), + (double)(ftKernel.dwHighDateTime * HI_T + \ + ftKernel.dwLowDateTime * LO_T), + psutil_FiletimeToUnixTime(ftCreate) + ); +} + + +/* + * Return process executable path. Works for all processes regardless of + * privilege. NtQuerySystemInformation has some sort of internal cache, + * since it succeeds even when a process is gone (but not if a PID never + * existed). + */ +PyObject * +psutil_proc_exe(PyObject *self, PyObject *args) { + DWORD pid; + NTSTATUS status; + PVOID buffer = NULL; + ULONG bufferSize = 0x104 * 2; // WIN_MAX_PATH * sizeof(wchar_t) + SYSTEM_PROCESS_ID_INFORMATION processIdInfo; + PyObject *py_exe; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + if (pid == 0) + return AccessDenied("automatically set for PID 0"); + + buffer = MALLOC_ZERO(bufferSize); + if (! buffer) { + PyErr_NoMemory(); + return NULL; + } + + processIdInfo.ProcessId = (HANDLE)(ULONG_PTR)pid; + processIdInfo.ImageName.Length = 0; + processIdInfo.ImageName.MaximumLength = (USHORT)bufferSize; + processIdInfo.ImageName.Buffer = buffer; + + status = NtQuerySystemInformation( + SystemProcessIdInformation, + &processIdInfo, + sizeof(SYSTEM_PROCESS_ID_INFORMATION), + NULL); + + if ((status == STATUS_INFO_LENGTH_MISMATCH) && + (processIdInfo.ImageName.MaximumLength <= bufferSize)) + { + // Required length was NOT stored in MaximumLength (WOW64 issue). + ULONG maxBufferSize = 0x7FFF * 2; // NTFS_MAX_PATH * sizeof(wchar_t) + do { + // Iteratively double the size of the buffer up to maxBufferSize + bufferSize *= 2; + FREE(buffer); + buffer = MALLOC_ZERO(bufferSize); + if (! buffer) { + PyErr_NoMemory(); + return NULL; + } + + processIdInfo.ImageName.MaximumLength = (USHORT)bufferSize; + processIdInfo.ImageName.Buffer = buffer; + + status = NtQuerySystemInformation( + SystemProcessIdInformation, + &processIdInfo, + sizeof(SYSTEM_PROCESS_ID_INFORMATION), + NULL); + } while ((status == STATUS_INFO_LENGTH_MISMATCH) && + (bufferSize <= maxBufferSize)); + } + else if (status == STATUS_INFO_LENGTH_MISMATCH) { + // Required length is stored in MaximumLength. + FREE(buffer); + buffer = MALLOC_ZERO(processIdInfo.ImageName.MaximumLength); + if (! buffer) { + PyErr_NoMemory(); + return NULL; + } + + processIdInfo.ImageName.Buffer = buffer; + + status = NtQuerySystemInformation( + SystemProcessIdInformation, + &processIdInfo, + sizeof(SYSTEM_PROCESS_ID_INFORMATION), + NULL); + } + + if (! NT_SUCCESS(status)) { + FREE(buffer); + if (psutil_pid_is_running(pid) == 0) + NoSuchProcess("psutil_pid_is_running -> 0"); + else + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); + return NULL; + } + + if (processIdInfo.ImageName.Buffer == NULL) { + // Happens for PID 4. + py_exe = Py_BuildValue("s", ""); + } + else { + py_exe = PyUnicode_FromWideChar(processIdInfo.ImageName.Buffer, + processIdInfo.ImageName.Length / 2); + } + FREE(buffer); + return py_exe; +} + + +/* + * Return process memory information as a Python tuple. + */ +PyObject * +psutil_proc_memory_info(PyObject *self, PyObject *args) { + HANDLE hProcess; + DWORD pid; + PROCESS_MEMORY_COUNTERS_EX cnt; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (NULL == hProcess) + return NULL; + + if (! GetProcessMemoryInfo(hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, + sizeof(cnt))) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + CloseHandle(hProcess); + + // PROCESS_MEMORY_COUNTERS values are defined as SIZE_T which on 64bits + // is an (unsigned long long) and on 32bits is an (unsigned int). + // "_WIN64" is defined if we're running a 64bit Python interpreter not + // exclusively if the *system* is 64bit. +#if defined(_WIN64) + return Py_BuildValue( + "(kKKKKKKKKK)", + cnt.PageFaultCount, // unsigned long + (unsigned long long)cnt.PeakWorkingSetSize, + (unsigned long long)cnt.WorkingSetSize, + (unsigned long long)cnt.QuotaPeakPagedPoolUsage, + (unsigned long long)cnt.QuotaPagedPoolUsage, + (unsigned long long)cnt.QuotaPeakNonPagedPoolUsage, + (unsigned long long)cnt.QuotaNonPagedPoolUsage, + (unsigned long long)cnt.PagefileUsage, + (unsigned long long)cnt.PeakPagefileUsage, + (unsigned long long)cnt.PrivateUsage); +#else + return Py_BuildValue( + "(kIIIIIIIII)", + cnt.PageFaultCount, // unsigned long + (unsigned int)cnt.PeakWorkingSetSize, + (unsigned int)cnt.WorkingSetSize, + (unsigned int)cnt.QuotaPeakPagedPoolUsage, + (unsigned int)cnt.QuotaPagedPoolUsage, + (unsigned int)cnt.QuotaPeakNonPagedPoolUsage, + (unsigned int)cnt.QuotaNonPagedPoolUsage, + (unsigned int)cnt.PagefileUsage, + (unsigned int)cnt.PeakPagefileUsage, + (unsigned int)cnt.PrivateUsage); +#endif +} + + +static int +psutil_GetProcWsetInformation( + DWORD pid, + HANDLE hProcess, + PMEMORY_WORKING_SET_INFORMATION *wSetInfo) +{ + NTSTATUS status; + PVOID buffer; + SIZE_T bufferSize; + + bufferSize = 0x8000; + buffer = MALLOC_ZERO(bufferSize); + if (! buffer) { + PyErr_NoMemory(); + return 1; + } + + while ((status = NtQueryVirtualMemory( + hProcess, + NULL, + MemoryWorkingSetInformation, + buffer, + bufferSize, + NULL)) == STATUS_INFO_LENGTH_MISMATCH) + { + FREE(buffer); + bufferSize *= 2; + // Fail if we're resizing the buffer to something very large. + if (bufferSize > 256 * 1024 * 1024) { + PyErr_SetString(PyExc_RuntimeError, + "NtQueryVirtualMemory bufsize is too large"); + return 1; + } + buffer = MALLOC_ZERO(bufferSize); + if (! buffer) { + PyErr_NoMemory(); + return 1; + } + } + + if (!NT_SUCCESS(status)) { + if (status == STATUS_ACCESS_DENIED) { + AccessDenied("NtQueryVirtualMemory -> STATUS_ACCESS_DENIED"); + } + else if (psutil_pid_is_running(pid) == 0) { + NoSuchProcess("psutil_pid_is_running -> 0"); + } + else { + PyErr_Clear(); + psutil_SetFromNTStatusErr( + status, "NtQueryVirtualMemory(MemoryWorkingSetInformation)"); + } + HeapFree(GetProcessHeap(), 0, buffer); + return 1; + } + + *wSetInfo = (PMEMORY_WORKING_SET_INFORMATION)buffer; + return 0; +} + + +/* + * Returns the USS of the process. + * Reference: + * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ + * nsMemoryReporterManager.cpp + */ +PyObject * +psutil_proc_memory_uss(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE hProcess; + PSUTIL_PROCESS_WS_COUNTERS wsCounters; + PMEMORY_WORKING_SET_INFORMATION wsInfo; + ULONG_PTR i; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION); + if (hProcess == NULL) + return NULL; + + if (psutil_GetProcWsetInformation(pid, hProcess, &wsInfo) != 0) { + CloseHandle(hProcess); + return NULL; + } + memset(&wsCounters, 0, sizeof(PSUTIL_PROCESS_WS_COUNTERS)); + + for (i = 0; i < wsInfo->NumberOfEntries; i++) { + // This is what ProcessHacker does. + /* + wsCounters.NumberOfPages++; + if (wsInfo->WorkingSetInfo[i].ShareCount > 1) + wsCounters.NumberOfSharedPages++; + if (wsInfo->WorkingSetInfo[i].ShareCount == 0) + wsCounters.NumberOfPrivatePages++; + if (wsInfo->WorkingSetInfo[i].Shared) + wsCounters.NumberOfShareablePages++; + */ + + // This is what we do: count shared pages that only one process + // is using as private (USS). + if (!wsInfo->WorkingSetInfo[i].Shared || + wsInfo->WorkingSetInfo[i].ShareCount <= 1) { + wsCounters.NumberOfPrivatePages++; + } + } + + HeapFree(GetProcessHeap(), 0, wsInfo); + CloseHandle(hProcess); + + return Py_BuildValue("I", wsCounters.NumberOfPrivatePages); +} + + +/* + * Resume or suspends a process + */ +PyObject * +psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { + DWORD pid; + NTSTATUS status; + HANDLE hProcess; + PyObject* suspend; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) + return NULL; + + hProcess = psutil_handle_from_pid(pid, PROCESS_SUSPEND_RESUME); + if (hProcess == NULL) + return NULL; + + if (PyObject_IsTrue(suspend)) + status = NtSuspendProcess(hProcess); + else + status = NtResumeProcess(hProcess); + + if (! NT_SUCCESS(status)) { + CloseHandle(hProcess); + return psutil_SetFromNTStatusErr(status, "NtSuspend|ResumeProcess"); + } + + CloseHandle(hProcess); + Py_RETURN_NONE; +} + + +PyObject * +psutil_proc_threads(PyObject *self, PyObject *args) { + HANDLE hThread = NULL; + THREADENTRY32 te32 = {0}; + DWORD pid; + int pid_return; + int rc; + FILETIME ftDummy, ftKernel, ftUser; + HANDLE hThreadSnap = NULL; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + if (pid == 0) { + // raise AD instead of returning 0 as procexp is able to + // retrieve useful information somehow + AccessDenied("forced for PID 0"); + goto error; + } + + pid_return = psutil_pid_is_running(pid); + if (pid_return == 0) { + NoSuchProcess("psutil_pid_is_running -> 0"); + goto error; + } + if (pid_return == -1) + goto error; + + hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (hThreadSnap == INVALID_HANDLE_VALUE) { + PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); + goto error; + } + + // Fill in the size of the structure before using it + te32.dwSize = sizeof(THREADENTRY32); + + if (! Thread32First(hThreadSnap, &te32)) { + PyErr_SetFromOSErrnoWithSyscall("Thread32First"); + goto error; + } + + // Walk the thread snapshot to find all threads of the process. + // If the thread belongs to the process, increase the counter. + do { + if (te32.th32OwnerProcessID == pid) { + py_tuple = NULL; + hThread = NULL; + hThread = OpenThread(THREAD_QUERY_INFORMATION, + FALSE, te32.th32ThreadID); + if (hThread == NULL) { + // thread has disappeared on us + continue; + } + + rc = GetThreadTimes(hThread, &ftDummy, &ftDummy, &ftKernel, + &ftUser); + if (rc == 0) { + PyErr_SetFromOSErrnoWithSyscall("GetThreadTimes"); + goto error; + } + + /* + * User and kernel times are represented as a FILETIME structure + * which contains a 64-bit value representing the number of + * 100-nanosecond intervals since January 1, 1601 (UTC): + * http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx + * To convert it into a float representing the seconds that the + * process has executed in user/kernel mode I borrowed the code + * below from Python's Modules/posixmodule.c + */ + py_tuple = Py_BuildValue( + "kdd", + te32.th32ThreadID, + (double)(ftUser.dwHighDateTime * HI_T + \ + ftUser.dwLowDateTime * LO_T), + (double)(ftKernel.dwHighDateTime * HI_T + \ + ftKernel.dwLowDateTime * LO_T)); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + + CloseHandle(hThread); + } + } while (Thread32Next(hThreadSnap, &te32)); + + CloseHandle(hThreadSnap); + return py_retlist; + +error: + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + if (hThread != NULL) + CloseHandle(hThread); + if (hThreadSnap != NULL) + CloseHandle(hThreadSnap); + return NULL; +} + + +PyObject * +psutil_proc_open_files(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE processHandle; + DWORD access = PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION; + PyObject *py_retlist; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + processHandle = psutil_handle_from_pid(pid, access); + if (processHandle == NULL) + return NULL; + + py_retlist = psutil_get_open_files(pid, processHandle); + CloseHandle(processHandle); + return py_retlist; +} + + +static PTOKEN_USER +_psutil_user_token_from_pid(DWORD pid) { + HANDLE hProcess = NULL; + HANDLE hToken = NULL; + PTOKEN_USER userToken = NULL; + ULONG bufferSize = 0x100; + + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) + return NULL; + + if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { + PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + goto error; + } + + // Get the user SID. + while (1) { + userToken = malloc(bufferSize); + if (userToken == NULL) { + PyErr_NoMemory(); + goto error; + } + if (!GetTokenInformation(hToken, TokenUser, userToken, bufferSize, + &bufferSize)) + { + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + free(userToken); + continue; + } + else { + PyErr_SetFromOSErrnoWithSyscall("GetTokenInformation"); + goto error; + } + } + break; + } + + CloseHandle(hProcess); + CloseHandle(hToken); + return userToken; + +error: + if (hProcess != NULL) + CloseHandle(hProcess); + if (hToken != NULL) + CloseHandle(hToken); + return NULL; +} + + +/* + * Return process username as a "DOMAIN//USERNAME" string. + */ +PyObject * +psutil_proc_username(PyObject *self, PyObject *args) { + DWORD pid; + PTOKEN_USER userToken = NULL; + WCHAR *userName = NULL; + WCHAR *domainName = NULL; + ULONG nameSize = 0x100; + ULONG domainNameSize = 0x100; + SID_NAME_USE nameUse; + PyObject *py_username = NULL; + PyObject *py_domain = NULL; + PyObject *py_tuple = NULL; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + userToken = _psutil_user_token_from_pid(pid); + if (userToken == NULL) + return NULL; + + // resolve the SID to a name + while (1) { + userName = malloc(nameSize * sizeof(WCHAR)); + if (userName == NULL) { + PyErr_NoMemory(); + goto error; + } + domainName = malloc(domainNameSize * sizeof(WCHAR)); + if (domainName == NULL) { + PyErr_NoMemory(); + goto error; + } + if (!LookupAccountSidW(NULL, userToken->User.Sid, userName, &nameSize, + domainName, &domainNameSize, &nameUse)) + { + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + free(userName); + free(domainName); + continue; + } + else if (GetLastError() == ERROR_NONE_MAPPED) { + // From MS doc: + // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ + // nf-winbase-lookupaccountsida + // If the function cannot find an account name for the SID, + // GetLastError returns ERROR_NONE_MAPPED. This can occur if + // a network time-out prevents the function from finding the + // name. It also occurs for SIDs that have no corresponding + // account name, such as a logon SID that identifies a logon + // session. + AccessDenied("LookupAccountSidW -> ERROR_NONE_MAPPED"); + goto error; + } + else { + PyErr_SetFromOSErrnoWithSyscall("LookupAccountSidW"); + goto error; + } + } + break; + } + + py_domain = PyUnicode_FromWideChar(domainName, wcslen(domainName)); + if (! py_domain) + goto error; + py_username = PyUnicode_FromWideChar(userName, wcslen(userName)); + if (! py_username) + goto error; + py_tuple = Py_BuildValue("OO", py_domain, py_username); + if (! py_tuple) + goto error; + Py_DECREF(py_domain); + Py_DECREF(py_username); + + free(userName); + free(domainName); + free(userToken); + return py_tuple; + +error: + if (userName != NULL) + free(userName); + if (domainName != NULL) + free(domainName); + if (userToken != NULL) + free(userToken); + Py_XDECREF(py_domain); + Py_XDECREF(py_username); + Py_XDECREF(py_tuple); + return NULL; +} + + +/* + * Get process priority as a Python integer. + */ +PyObject * +psutil_proc_priority_get(PyObject *self, PyObject *args) { + DWORD pid; + DWORD priority; + HANDLE hProcess; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) + return NULL; + + priority = GetPriorityClass(hProcess); + if (priority == 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + CloseHandle(hProcess); + return Py_BuildValue("i", priority); +} + + +/* + * Set process priority. + */ +PyObject * +psutil_proc_priority_set(PyObject *self, PyObject *args) { + DWORD pid; + int priority; + int retval; + HANDLE hProcess; + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) + return NULL; + hProcess = psutil_handle_from_pid(pid, access); + if (hProcess == NULL) + return NULL; + + retval = SetPriorityClass(hProcess, priority); + if (retval == 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + Py_RETURN_NONE; +} + + +/* + * Get process IO priority as a Python integer. + */ +PyObject * +psutil_proc_io_priority_get(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE hProcess; + DWORD IoPriority; + NTSTATUS status; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) + return NULL; + + status = NtQueryInformationProcess( + hProcess, + ProcessIoPriority, + &IoPriority, + sizeof(DWORD), + NULL + ); + + CloseHandle(hProcess); + if (! NT_SUCCESS(status)) + return psutil_SetFromNTStatusErr(status, "NtQueryInformationProcess"); + return Py_BuildValue("i", IoPriority); +} + + +/* + * Set process IO priority. + */ +PyObject * +psutil_proc_io_priority_set(PyObject *self, PyObject *args) { + DWORD pid; + DWORD prio; + HANDLE hProcess; + NTSTATUS status; + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &prio)) + return NULL; + + hProcess = psutil_handle_from_pid(pid, access); + if (hProcess == NULL) + return NULL; + + status = NtSetInformationProcess( + hProcess, + ProcessIoPriority, + (PVOID)&prio, + sizeof(DWORD) + ); + + CloseHandle(hProcess); + if (! NT_SUCCESS(status)) + return psutil_SetFromNTStatusErr(status, "NtSetInformationProcess"); + Py_RETURN_NONE; +} + + +/* + * Return a Python tuple referencing process I/O counters. + */ +PyObject * +psutil_proc_io_counters(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE hProcess; + IO_COUNTERS IoCounters; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (NULL == hProcess) + return NULL; + + if (! GetProcessIoCounters(hProcess, &IoCounters)) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + return Py_BuildValue("(KKKKKK)", + IoCounters.ReadOperationCount, + IoCounters.WriteOperationCount, + IoCounters.ReadTransferCount, + IoCounters.WriteTransferCount, + IoCounters.OtherOperationCount, + IoCounters.OtherTransferCount); +} + + +/* + * Return process CPU affinity as a bitmask + */ +PyObject * +psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE hProcess; + DWORD_PTR proc_mask; + DWORD_PTR system_mask; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) { + return NULL; + } + if (GetProcessAffinityMask(hProcess, &proc_mask, &system_mask) == 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); +#ifdef _WIN64 + return Py_BuildValue("K", (unsigned long long)proc_mask); +#else + return Py_BuildValue("k", (unsigned long)proc_mask); +#endif +} + + +/* + * Set process CPU affinity + */ +PyObject * +psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE hProcess; + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; + DWORD_PTR mask; + +#ifdef _WIN64 + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "K", &pid, &mask)) +#else + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "k", &pid, &mask)) +#endif + { + return NULL; + } + hProcess = psutil_handle_from_pid(pid, access); + if (hProcess == NULL) + return NULL; + + if (SetProcessAffinityMask(hProcess, mask) == 0) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + Py_RETURN_NONE; +} + + +/* + * Return True if all process threads are in waiting/suspended state. + */ +PyObject * +psutil_proc_is_suspended(PyObject *self, PyObject *args) { + DWORD pid; + ULONG i; + PSYSTEM_PROCESS_INFORMATION process; + PVOID buffer; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (! psutil_get_proc_info(pid, &process, &buffer)) + return NULL; + for (i = 0; i < process->NumberOfThreads; i++) { + if (process->Threads[i].ThreadState != Waiting || + process->Threads[i].WaitReason != Suspended) + { + free(buffer); + Py_RETURN_FALSE; + } + } + free(buffer); + Py_RETURN_TRUE; +} + + +/* + * Return the number of handles opened by process. + */ +PyObject * +psutil_proc_num_handles(PyObject *self, PyObject *args) { + DWORD pid; + HANDLE hProcess; + DWORD handleCount; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (NULL == hProcess) + return NULL; + if (! GetProcessHandleCount(hProcess, &handleCount)) { + PyErr_SetFromWindowsErr(0); + CloseHandle(hProcess); + return NULL; + } + CloseHandle(hProcess); + return Py_BuildValue("k", handleCount); +} + + +static char *get_region_protection_string(ULONG protection) { + switch (protection & 0xff) { + case PAGE_NOACCESS: + return ""; + case PAGE_READONLY: + return "r"; + case PAGE_READWRITE: + return "rw"; + case PAGE_WRITECOPY: + return "wc"; + case PAGE_EXECUTE: + return "x"; + case PAGE_EXECUTE_READ: + return "xr"; + case PAGE_EXECUTE_READWRITE: + return "xrw"; + case PAGE_EXECUTE_WRITECOPY: + return "xwc"; + default: + return "?"; + } +} + + +/* + * Return a list of process's memory mappings. + */ +PyObject * +psutil_proc_memory_maps(PyObject *self, PyObject *args) { + MEMORY_BASIC_INFORMATION basicInfo; + DWORD pid; + HANDLE hProcess = NULL; + PVOID baseAddress; + WCHAR mappedFileName[MAX_PATH]; + LPVOID maxAddr; + // required by GetMappedFileNameW + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; + PyObject *py_retlist = PyList_New(0); + PyObject *py_tuple = NULL; + PyObject *py_str = NULL; + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + hProcess = psutil_handle_from_pid(pid, access); + if (NULL == hProcess) + goto error; + + maxAddr = PSUTIL_SYSTEM_INFO.lpMaximumApplicationAddress; + baseAddress = NULL; + + while (VirtualQueryEx(hProcess, baseAddress, &basicInfo, + sizeof(MEMORY_BASIC_INFORMATION))) + { + py_tuple = NULL; + if (baseAddress > maxAddr) + break; + if (GetMappedFileNameW(hProcess, baseAddress, mappedFileName, + sizeof(mappedFileName))) + { + py_str = PyUnicode_FromWideChar(mappedFileName, + wcslen(mappedFileName)); + if (py_str == NULL) + goto error; +#ifdef _WIN64 + py_tuple = Py_BuildValue( + "(KsOI)", + (unsigned long long)baseAddress, +#else + py_tuple = Py_BuildValue( + "(ksOI)", + (unsigned long)baseAddress, +#endif + get_region_protection_string(basicInfo.Protect), + py_str, + basicInfo.RegionSize); + + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + Py_CLEAR(py_str); + } + baseAddress = (PCHAR)baseAddress + basicInfo.RegionSize; + } + + CloseHandle(hProcess); + return py_retlist; + +error: + Py_XDECREF(py_tuple); + Py_XDECREF(py_str); + Py_DECREF(py_retlist); + if (hProcess != NULL) + CloseHandle(hProcess); + return NULL; +} + + +/* + * Return a {pid:ppid, ...} dict for all running processes. + */ +PyObject * +psutil_ppid_map(PyObject *self, PyObject *args) { + PyObject *py_pid = NULL; + PyObject *py_ppid = NULL; + PyObject *py_retdict = PyDict_New(); + HANDLE handle = NULL; + PROCESSENTRY32 pe = {0}; + pe.dwSize = sizeof(PROCESSENTRY32); + + if (py_retdict == NULL) + return NULL; + handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (handle == INVALID_HANDLE_VALUE) { + PyErr_SetFromWindowsErr(0); + Py_DECREF(py_retdict); + return NULL; + } + + if (Process32First(handle, &pe)) { + do { + py_pid = PyLong_FromPid(pe.th32ProcessID); + if (py_pid == NULL) + goto error; + py_ppid = PyLong_FromPid(pe.th32ParentProcessID); + if (py_ppid == NULL) + goto error; + if (PyDict_SetItem(py_retdict, py_pid, py_ppid)) + goto error; + Py_CLEAR(py_pid); + Py_CLEAR(py_ppid); + } while (Process32Next(handle, &pe)); + } + + CloseHandle(handle); + return py_retdict; + +error: + Py_XDECREF(py_pid); + Py_XDECREF(py_ppid); + Py_DECREF(py_retdict); + CloseHandle(handle); + return NULL; +} diff --git a/psutil/arch/windows/proc.h b/psutil/arch/windows/proc.h new file mode 100644 index 0000000000..ba119f16ba --- /dev/null +++ b/psutil/arch/windows/proc.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *TimeoutExpired; +PyObject *TimeoutAbandoned; + +PyObject *psutil_pid_exists(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_ppid_map(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_is_suspended(PyObject *self, PyObject *args); +PyObject *psutil_proc_kill(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_suspend_or_resume(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_username(PyObject *self, PyObject *args); +PyObject *psutil_proc_wait(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index 26190427f3..b7795451dc 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -5,6 +5,9 @@ */ #include +#include + +#include "ntextapi.h" #define PSUTIL_FIRST_PROCESS(Processes) ( \ (PSYSTEM_PROCESS_INFORMATION)(Processes)) From 063854c162b402cf87ee107717a274be8a3fd786 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Apr 2023 23:04:33 +0200 Subject: [PATCH 0980/1714] rename _psutil_windows.c -> proc.c Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 2 +- psutil/{_psutil_windows.c => arch/windows/proc.c} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename psutil/{_psutil_windows.c => arch/windows/proc.c} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index db7079354b..e55d8027e5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -38,7 +38,6 @@ include psutil/_psutil_osx.c include psutil/_psutil_posix.c include psutil/_psutil_posix.h include psutil/_psutil_sunos.c -include psutil/_psutil_windows.c include psutil/_pswindows.py include psutil/arch/aix/common.c include psutil/arch/aix/common.h @@ -108,6 +107,7 @@ include psutil/arch/windows/mem.h include psutil/arch/windows/net.c include psutil/arch/windows/net.h include psutil/arch/windows/ntextapi.h +include psutil/arch/windows/proc.c include psutil/arch/windows/process_handles.c include psutil/arch/windows/process_handles.h include psutil/arch/windows/process_info.c diff --git a/psutil/_psutil_windows.c b/psutil/arch/windows/proc.c similarity index 100% rename from psutil/_psutil_windows.c rename to psutil/arch/windows/proc.c From aeceeb74b7bdd2d6f5769f9dfcce9256ddd7832c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Apr 2023 23:14:27 +0200 Subject: [PATCH 0981/1714] win C refact: reconstruct _psutil_windows.c trying to preserve history Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 2 + psutil/_psutil_windows.c | 284 ++++++++++++++++++++++++ psutil/arch/windows/proc.c | 340 ++++------------------------- psutil/arch/windows/proc.h | 34 +++ psutil/arch/windows/process_info.h | 3 + setup.py | 1 + 6 files changed, 363 insertions(+), 301 deletions(-) create mode 100644 psutil/_psutil_windows.c create mode 100644 psutil/arch/windows/proc.h diff --git a/MANIFEST.in b/MANIFEST.in index e55d8027e5..d6f45fb257 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -38,6 +38,7 @@ include psutil/_psutil_osx.c include psutil/_psutil_posix.c include psutil/_psutil_posix.h include psutil/_psutil_sunos.c +include psutil/_psutil_windows.c include psutil/_pswindows.py include psutil/arch/aix/common.c include psutil/arch/aix/common.h @@ -108,6 +109,7 @@ include psutil/arch/windows/net.c include psutil/arch/windows/net.h include psutil/arch/windows/ntextapi.h include psutil/arch/windows/proc.c +include psutil/arch/windows/proc.h include psutil/arch/windows/process_handles.c include psutil/arch/windows/process_handles.h include psutil/arch/windows/process_info.c diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c new file mode 100644 index 0000000000..54678f7557 --- /dev/null +++ b/psutil/_psutil_windows.c @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2009, Jay Loden, 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 platform-specific module methods for _psutil_windows. + * + * List of undocumented Windows NT APIs which are used in here and in + * other modules: + * - NtQuerySystemInformation + * - NtQueryInformationProcess + * - NtQueryObject + * - NtSuspendProcess + * - NtResumeProcess + */ + +#include +#include + +#include "_psutil_common.h" +#include "arch/windows/cpu.h" +#include "arch/windows/disk.h" +#include "arch/windows/mem.h" +#include "arch/windows/net.h" +#include "arch/windows/proc.h" +#include "arch/windows/proc_handles.h" +#include "arch/windows/proc_info.h" +#include "arch/windows/proc_utils.h" +#include "arch/windows/security.h" +#include "arch/windows/sensors.h" +#include "arch/windows/services.h" +#include "arch/windows/socks.h" +#include "arch/windows/sys.h" +#include "arch/windows/wmi.h" + + +// ------------------------ Python init --------------------------- + +static PyMethodDef +PsutilMethods[] = { + // --- per-process functions + {"proc_cmdline", (PyCFunction)(void(*)(void))psutil_proc_cmdline, + METH_VARARGS | METH_KEYWORDS}, + {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, + {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, + {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, + {"proc_environ", psutil_proc_environ, METH_VARARGS}, + {"proc_exe", psutil_proc_exe, METH_VARARGS}, + {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS}, + {"proc_io_priority_get", psutil_proc_io_priority_get, METH_VARARGS}, + {"proc_io_priority_set", psutil_proc_io_priority_set, METH_VARARGS}, + {"proc_is_suspended", psutil_proc_is_suspended, METH_VARARGS}, + {"proc_kill", psutil_proc_kill, METH_VARARGS}, + {"proc_memory_info", psutil_proc_memory_info, METH_VARARGS}, + {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, + {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, + {"proc_num_handles", psutil_proc_num_handles, METH_VARARGS}, + {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, + {"proc_priority_get", psutil_proc_priority_get, METH_VARARGS}, + {"proc_priority_set", psutil_proc_priority_set, METH_VARARGS}, + {"proc_suspend_or_resume", psutil_proc_suspend_or_resume, METH_VARARGS}, + {"proc_threads", psutil_proc_threads, METH_VARARGS}, + {"proc_times", psutil_proc_times, METH_VARARGS}, + {"proc_username", psutil_proc_username, METH_VARARGS}, + {"proc_wait", psutil_proc_wait, METH_VARARGS}, + + // --- alternative pinfo interface + {"proc_info", psutil_proc_info, METH_VARARGS}, + + // --- system-related functions + {"boot_time", psutil_boot_time, METH_VARARGS}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, + {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, + {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, + {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, + {"cpu_times", psutil_cpu_times, METH_VARARGS}, + {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"disk_usage", psutil_disk_usage, METH_VARARGS}, + {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS}, + {"getpagesize", psutil_getpagesize, METH_VARARGS}, + {"swap_percent", psutil_swap_percent, METH_VARARGS}, + {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, METH_VARARGS}, + {"net_connections", psutil_net_connections, METH_VARARGS}, + {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, + {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, + {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, + {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, + {"pid_exists", psutil_pid_exists, METH_VARARGS}, + {"pids", psutil_pids, METH_VARARGS}, + {"ppid_map", psutil_ppid_map, METH_VARARGS}, + {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, + + // --- windows services + {"winservice_enumerate", psutil_winservice_enumerate, METH_VARARGS}, + {"winservice_query_config", psutil_winservice_query_config, METH_VARARGS}, + {"winservice_query_descr", psutil_winservice_query_descr, METH_VARARGS}, + {"winservice_query_status", psutil_winservice_query_status, METH_VARARGS}, + {"winservice_start", psutil_winservice_start, METH_VARARGS}, + {"winservice_stop", psutil_winservice_stop, METH_VARARGS}, + + // --- windows API bindings + {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS}, + + // --- others + {"set_debug", psutil_set_debug, METH_VARARGS}, + + {NULL, NULL, 0, NULL} +}; + + +struct module_state { + PyObject *error; +}; + +#if PY_MAJOR_VERSION >= 3 +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) +#else +#define GETSTATE(m) (&_state) +static struct module_state _state; +#endif + +#if PY_MAJOR_VERSION >= 3 + +static int psutil_windows_traverse(PyObject *m, visitproc visit, void *arg) { + Py_VISIT(GETSTATE(m)->error); + return 0; +} + +static int psutil_windows_clear(PyObject *m) { + Py_CLEAR(GETSTATE(m)->error); + return 0; +} + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "psutil_windows", + NULL, + sizeof(struct module_state), + PsutilMethods, + NULL, + psutil_windows_traverse, + psutil_windows_clear, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC PyInit__psutil_windows(void) + +#else +#define INITERROR return +void init_psutil_windows(void) +#endif +{ + struct module_state *st = NULL; +#if PY_MAJOR_VERSION >= 3 + PyObject *module = PyModule_Create(&moduledef); +#else + PyObject *module = Py_InitModule("_psutil_windows", PsutilMethods); +#endif + if (module == NULL) + INITERROR; + + if (psutil_setup() != 0) + INITERROR; + if (psutil_set_se_debug() != 0) + INITERROR; + + st = GETSTATE(module); + st->error = PyErr_NewException("_psutil_windows.Error", NULL, NULL); + if (st->error == NULL) { + Py_DECREF(module); + INITERROR; + } + + // Exceptions. + TimeoutExpired = PyErr_NewException( + "_psutil_windows.TimeoutExpired", NULL, NULL); + Py_INCREF(TimeoutExpired); + PyModule_AddObject(module, "TimeoutExpired", TimeoutExpired); + + TimeoutAbandoned = PyErr_NewException( + "_psutil_windows.TimeoutAbandoned", NULL, NULL); + Py_INCREF(TimeoutAbandoned); + PyModule_AddObject(module, "TimeoutAbandoned", TimeoutAbandoned); + + // version constant + PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); + + // process status constants + // http://msdn.microsoft.com/en-us/library/ms683211(v=vs.85).aspx + PyModule_AddIntConstant( + module, "ABOVE_NORMAL_PRIORITY_CLASS", ABOVE_NORMAL_PRIORITY_CLASS); + PyModule_AddIntConstant( + module, "BELOW_NORMAL_PRIORITY_CLASS", BELOW_NORMAL_PRIORITY_CLASS); + PyModule_AddIntConstant( + module, "HIGH_PRIORITY_CLASS", HIGH_PRIORITY_CLASS); + PyModule_AddIntConstant( + module, "IDLE_PRIORITY_CLASS", IDLE_PRIORITY_CLASS); + PyModule_AddIntConstant( + module, "NORMAL_PRIORITY_CLASS", NORMAL_PRIORITY_CLASS); + PyModule_AddIntConstant( + module, "REALTIME_PRIORITY_CLASS", REALTIME_PRIORITY_CLASS); + + // connection status constants + // http://msdn.microsoft.com/en-us/library/cc669305.aspx + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_CLOSED", MIB_TCP_STATE_CLOSED); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_CLOSING", MIB_TCP_STATE_CLOSING); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_CLOSE_WAIT", MIB_TCP_STATE_CLOSE_WAIT); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_LISTEN", MIB_TCP_STATE_LISTEN); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_ESTAB", MIB_TCP_STATE_ESTAB); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_SYN_SENT", MIB_TCP_STATE_SYN_SENT); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_SYN_RCVD", MIB_TCP_STATE_SYN_RCVD); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_FIN_WAIT1", MIB_TCP_STATE_FIN_WAIT1); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_FIN_WAIT2", MIB_TCP_STATE_FIN_WAIT2); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_LAST_ACK", MIB_TCP_STATE_LAST_ACK); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT); + PyModule_AddIntConstant( + module, "MIB_TCP_STATE_DELETE_TCB", MIB_TCP_STATE_DELETE_TCB); + PyModule_AddIntConstant( + module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); + + // service status constants + /* + PyModule_AddIntConstant( + module, "SERVICE_CONTINUE_PENDING", SERVICE_CONTINUE_PENDING); + PyModule_AddIntConstant( + module, "SERVICE_PAUSE_PENDING", SERVICE_PAUSE_PENDING); + PyModule_AddIntConstant( + module, "SERVICE_PAUSED", SERVICE_PAUSED); + PyModule_AddIntConstant( + module, "SERVICE_RUNNING", SERVICE_RUNNING); + PyModule_AddIntConstant( + module, "SERVICE_START_PENDING", SERVICE_START_PENDING); + PyModule_AddIntConstant( + module, "SERVICE_STOP_PENDING", SERVICE_STOP_PENDING); + PyModule_AddIntConstant( + module, "SERVICE_STOPPED", SERVICE_STOPPED); + */ + + // ...for internal use in _psutil_windows.py + PyModule_AddIntConstant( + module, "INFINITE", INFINITE); + PyModule_AddIntConstant( + module, "ERROR_ACCESS_DENIED", ERROR_ACCESS_DENIED); + PyModule_AddIntConstant( + module, "ERROR_INVALID_NAME", ERROR_INVALID_NAME); + PyModule_AddIntConstant( + module, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST); + PyModule_AddIntConstant( + module, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD); + PyModule_AddIntConstant( + module, "WINVER", PSUTIL_WINVER); + PyModule_AddIntConstant( + module, "WINDOWS_VISTA", PSUTIL_WINDOWS_VISTA); + PyModule_AddIntConstant( + module, "WINDOWS_7", PSUTIL_WINDOWS_7); + PyModule_AddIntConstant( + module, "WINDOWS_8", PSUTIL_WINDOWS_8); + PyModule_AddIntConstant( + module, "WINDOWS_8_1", PSUTIL_WINDOWS_8_1); + PyModule_AddIntConstant( + module, "WINDOWS_10", PSUTIL_WINDOWS_10); + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index ef85433272..d9b69744f1 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -1,19 +1,16 @@ /* - * Copyright (c) 2009, Jay Loden, Giampaolo Rodola'. All rights reserved. + * 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 platform-specific module methods for _psutil_windows. - * - * List of undocumented Windows NT APIs which are used in here and in - * other modules: - * - NtQuerySystemInformation - * - NtQueryInformationProcess - * - NtQueryObject - * - NtSuspendProcess - * - NtResumeProcess */ +/* + * Process related functions. Original code was moved in here from + * psutil/_psutil_windows.c in 2023. For reference, here's the GIT blame + * history before the move: + * https://github.com/giampaolo/psutil/blame/59504a5/psutil/_psutil_windows.c +*/ + // Fixes clash between winsock2.h and windows.h #define WIN32_LEAN_AND_MEAN @@ -26,31 +23,22 @@ // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") -#include "_psutil_common.h" -#include "arch/windows/cpu.h" -#include "arch/windows/disk.h" -#include "arch/windows/mem.h" -#include "arch/windows/net.h" -#include "arch/windows/process_handles.h" -#include "arch/windows/process_info.h" -#include "arch/windows/process_utils.h" -#include "arch/windows/security.h" -#include "arch/windows/sensors.h" -#include "arch/windows/services.h" -#include "arch/windows/socks.h" -#include "arch/windows/sys.h" -#include "arch/windows/wmi.h" +#include "../../_psutil_common.h" +#include "proc.h" +#include "proc_info.h" +#include "proc_handles.h" +#include "proc_utils.h" -// Raised by Process.wait(). -static PyObject *TimeoutExpired; -static PyObject *TimeoutAbandoned; +// Raised by Process.wait(). +PyObject *TimeoutExpired; +PyObject *TimeoutAbandoned; /* * Return 1 if PID exists in the current process list, else 0. */ -static PyObject * +PyObject * psutil_pid_exists(PyObject *self, PyObject *args) { DWORD pid; int status; @@ -68,7 +56,7 @@ psutil_pid_exists(PyObject *self, PyObject *args) { /* * Return a Python list of all the PIDs running on the system. */ -static PyObject * +PyObject * psutil_pids(PyObject *self, PyObject *args) { DWORD *proclist = NULL; DWORD numberOfReturnedPIDs; @@ -107,7 +95,7 @@ psutil_pids(PyObject *self, PyObject *args) { /* * Kill a process given its PID. */ -static PyObject * +PyObject * psutil_proc_kill(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD pid; @@ -141,7 +129,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { /* * Wait for process to terminate and return its exit code. */ -static PyObject * +PyObject * psutil_proc_wait(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD ExitCode; @@ -215,7 +203,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { /* * Return a Python tuple (user_time, kernel_time) */ -static PyObject * +PyObject * psutil_proc_times(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -269,7 +257,7 @@ psutil_proc_times(PyObject *self, PyObject *args) { * since it succeeds even when a process is gone (but not if a PID never * existed). */ -static PyObject * +PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { DWORD pid; NTSTATUS status; @@ -370,7 +358,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { /* * Return process memory information as a Python tuple. */ -static PyObject * +PyObject * psutil_proc_memory_info(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD pid; @@ -492,7 +480,7 @@ psutil_GetProcWsetInformation( * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ * nsMemoryReporterManager.cpp */ -static PyObject * +PyObject * psutil_proc_memory_uss(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -542,7 +530,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { /* * Resume or suspends a process */ -static PyObject * +PyObject * psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { DWORD pid; NTSTATUS status; @@ -571,7 +559,7 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { } -static PyObject * +PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { HANDLE hThread = NULL; THREADENTRY32 te32 = {0}; @@ -676,7 +664,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -static PyObject * +PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { DWORD pid; HANDLE processHandle; @@ -750,7 +738,7 @@ _psutil_user_token_from_pid(DWORD pid) { /* * Return process username as a "DOMAIN//USERNAME" string. */ -static PyObject * +PyObject * psutil_proc_username(PyObject *self, PyObject *args) { DWORD pid; PTOKEN_USER userToken = NULL; @@ -844,7 +832,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { /* * Get process priority as a Python integer. */ -static PyObject * +PyObject * psutil_proc_priority_get(PyObject *self, PyObject *args) { DWORD pid; DWORD priority; @@ -871,7 +859,7 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { /* * Set process priority. */ -static PyObject * +PyObject * psutil_proc_priority_set(PyObject *self, PyObject *args) { DWORD pid; int priority; @@ -900,7 +888,7 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { /* * Get process IO priority as a Python integer. */ -static PyObject * +PyObject * psutil_proc_io_priority_get(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -932,7 +920,7 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { /* * Set process IO priority. */ -static PyObject * +PyObject * psutil_proc_io_priority_set(PyObject *self, PyObject *args) { DWORD pid; DWORD prio; @@ -964,7 +952,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { /* * Return a Python tuple referencing process I/O counters. */ -static PyObject * +PyObject * psutil_proc_io_counters(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -996,7 +984,7 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { /* * Return process CPU affinity as a bitmask */ -static PyObject * +PyObject * psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -1027,7 +1015,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { /* * Set process CPU affinity */ -static PyObject * +PyObject * psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -1060,7 +1048,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { /* * Return True if all process threads are in waiting/suspended state. */ -static PyObject * +PyObject * psutil_proc_is_suspended(PyObject *self, PyObject *args) { DWORD pid; ULONG i; @@ -1087,7 +1075,7 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { /* * Return the number of handles opened by process. */ -static PyObject * +PyObject * psutil_proc_num_handles(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -1135,7 +1123,7 @@ static char *get_region_protection_string(ULONG protection) { /* * Return a list of process's memory mappings. */ -static PyObject * +PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { MEMORY_BASIC_INFORMATION basicInfo; DWORD pid; @@ -1212,7 +1200,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { /* * Return a {pid:ppid, ...} dict for all running processes. */ -static PyObject * +PyObject * psutil_ppid_map(PyObject *self, PyObject *args) { PyObject *py_pid = NULL; PyObject *py_ppid = NULL; @@ -1255,253 +1243,3 @@ psutil_ppid_map(PyObject *self, PyObject *args) { CloseHandle(handle); return NULL; } - - -// ------------------------ Python init --------------------------- - -static PyMethodDef -PsutilMethods[] = { - // --- per-process functions - {"proc_cmdline", (PyCFunction)(void(*)(void))psutil_proc_cmdline, - METH_VARARGS | METH_KEYWORDS}, - {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, - {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, - {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, - {"proc_environ", psutil_proc_environ, METH_VARARGS}, - {"proc_exe", psutil_proc_exe, METH_VARARGS}, - {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS}, - {"proc_io_priority_get", psutil_proc_io_priority_get, METH_VARARGS}, - {"proc_io_priority_set", psutil_proc_io_priority_set, METH_VARARGS}, - {"proc_is_suspended", psutil_proc_is_suspended, METH_VARARGS}, - {"proc_kill", psutil_proc_kill, METH_VARARGS}, - {"proc_memory_info", psutil_proc_memory_info, METH_VARARGS}, - {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, - {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, - {"proc_num_handles", psutil_proc_num_handles, METH_VARARGS}, - {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, - {"proc_priority_get", psutil_proc_priority_get, METH_VARARGS}, - {"proc_priority_set", psutil_proc_priority_set, METH_VARARGS}, - {"proc_suspend_or_resume", psutil_proc_suspend_or_resume, METH_VARARGS}, - {"proc_threads", psutil_proc_threads, METH_VARARGS}, - {"proc_times", psutil_proc_times, METH_VARARGS}, - {"proc_username", psutil_proc_username, METH_VARARGS}, - {"proc_wait", psutil_proc_wait, METH_VARARGS}, - - // --- alternative pinfo interface - {"proc_info", psutil_proc_info, METH_VARARGS}, - - // --- system-related functions - {"boot_time", psutil_boot_time, METH_VARARGS}, - {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, - {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, - {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, - {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, - {"cpu_times", psutil_cpu_times, METH_VARARGS}, - {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, - {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, - {"disk_usage", psutil_disk_usage, METH_VARARGS}, - {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS}, - {"getpagesize", psutil_getpagesize, METH_VARARGS}, - {"swap_percent", psutil_swap_percent, METH_VARARGS}, - {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, METH_VARARGS}, - {"net_connections", psutil_net_connections, METH_VARARGS}, - {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, - {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, - {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, - {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, - {"pid_exists", psutil_pid_exists, METH_VARARGS}, - {"pids", psutil_pids, METH_VARARGS}, - {"ppid_map", psutil_ppid_map, METH_VARARGS}, - {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, - {"users", psutil_users, METH_VARARGS}, - {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, - - // --- windows services - {"winservice_enumerate", psutil_winservice_enumerate, METH_VARARGS}, - {"winservice_query_config", psutil_winservice_query_config, METH_VARARGS}, - {"winservice_query_descr", psutil_winservice_query_descr, METH_VARARGS}, - {"winservice_query_status", psutil_winservice_query_status, METH_VARARGS}, - {"winservice_start", psutil_winservice_start, METH_VARARGS}, - {"winservice_stop", psutil_winservice_stop, METH_VARARGS}, - - // --- windows API bindings - {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS}, - - // --- others - {"set_debug", psutil_set_debug, METH_VARARGS}, - - {NULL, NULL, 0, NULL} -}; - - -struct module_state { - PyObject *error; -}; - -#if PY_MAJOR_VERSION >= 3 -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -static struct module_state _state; -#endif - -#if PY_MAJOR_VERSION >= 3 - -static int psutil_windows_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - -static int psutil_windows_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "psutil_windows", - NULL, - sizeof(struct module_state), - PsutilMethods, - NULL, - psutil_windows_traverse, - psutil_windows_clear, - NULL -}; - -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_windows(void) - -#else -#define INITERROR return -void init_psutil_windows(void) -#endif -{ - struct module_state *st = NULL; -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_windows", PsutilMethods); -#endif - if (module == NULL) - INITERROR; - - if (psutil_setup() != 0) - INITERROR; - if (psutil_set_se_debug() != 0) - INITERROR; - - st = GETSTATE(module); - st->error = PyErr_NewException("_psutil_windows.Error", NULL, NULL); - if (st->error == NULL) { - Py_DECREF(module); - INITERROR; - } - - // Exceptions. - TimeoutExpired = PyErr_NewException( - "_psutil_windows.TimeoutExpired", NULL, NULL); - Py_INCREF(TimeoutExpired); - PyModule_AddObject(module, "TimeoutExpired", TimeoutExpired); - - TimeoutAbandoned = PyErr_NewException( - "_psutil_windows.TimeoutAbandoned", NULL, NULL); - Py_INCREF(TimeoutAbandoned); - PyModule_AddObject(module, "TimeoutAbandoned", TimeoutAbandoned); - - // version constant - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); - - // process status constants - // http://msdn.microsoft.com/en-us/library/ms683211(v=vs.85).aspx - PyModule_AddIntConstant( - module, "ABOVE_NORMAL_PRIORITY_CLASS", ABOVE_NORMAL_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "BELOW_NORMAL_PRIORITY_CLASS", BELOW_NORMAL_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "HIGH_PRIORITY_CLASS", HIGH_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "IDLE_PRIORITY_CLASS", IDLE_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "NORMAL_PRIORITY_CLASS", NORMAL_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "REALTIME_PRIORITY_CLASS", REALTIME_PRIORITY_CLASS); - - // connection status constants - // http://msdn.microsoft.com/en-us/library/cc669305.aspx - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_CLOSED", MIB_TCP_STATE_CLOSED); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_CLOSING", MIB_TCP_STATE_CLOSING); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_CLOSE_WAIT", MIB_TCP_STATE_CLOSE_WAIT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_LISTEN", MIB_TCP_STATE_LISTEN); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_ESTAB", MIB_TCP_STATE_ESTAB); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_SYN_SENT", MIB_TCP_STATE_SYN_SENT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_SYN_RCVD", MIB_TCP_STATE_SYN_RCVD); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_FIN_WAIT1", MIB_TCP_STATE_FIN_WAIT1); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_FIN_WAIT2", MIB_TCP_STATE_FIN_WAIT2); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_LAST_ACK", MIB_TCP_STATE_LAST_ACK); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_DELETE_TCB", MIB_TCP_STATE_DELETE_TCB); - PyModule_AddIntConstant( - module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); - - // service status constants - /* - PyModule_AddIntConstant( - module, "SERVICE_CONTINUE_PENDING", SERVICE_CONTINUE_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_PAUSE_PENDING", SERVICE_PAUSE_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_PAUSED", SERVICE_PAUSED); - PyModule_AddIntConstant( - module, "SERVICE_RUNNING", SERVICE_RUNNING); - PyModule_AddIntConstant( - module, "SERVICE_START_PENDING", SERVICE_START_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_STOP_PENDING", SERVICE_STOP_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_STOPPED", SERVICE_STOPPED); - */ - - // ...for internal use in _psutil_windows.py - PyModule_AddIntConstant( - module, "INFINITE", INFINITE); - PyModule_AddIntConstant( - module, "ERROR_ACCESS_DENIED", ERROR_ACCESS_DENIED); - PyModule_AddIntConstant( - module, "ERROR_INVALID_NAME", ERROR_INVALID_NAME); - PyModule_AddIntConstant( - module, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST); - PyModule_AddIntConstant( - module, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD); - PyModule_AddIntConstant( - module, "WINVER", PSUTIL_WINVER); - PyModule_AddIntConstant( - module, "WINDOWS_VISTA", PSUTIL_WINDOWS_VISTA); - PyModule_AddIntConstant( - module, "WINDOWS_7", PSUTIL_WINDOWS_7); - PyModule_AddIntConstant( - module, "WINDOWS_8", PSUTIL_WINDOWS_8); - PyModule_AddIntConstant( - module, "WINDOWS_8_1", PSUTIL_WINDOWS_8_1); - PyModule_AddIntConstant( - module, "WINDOWS_10", PSUTIL_WINDOWS_10); - -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} diff --git a/psutil/arch/windows/proc.h b/psutil/arch/windows/proc.h new file mode 100644 index 0000000000..ba119f16ba --- /dev/null +++ b/psutil/arch/windows/proc.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *TimeoutExpired; +PyObject *TimeoutAbandoned; + +PyObject *psutil_pid_exists(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_ppid_map(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_is_suspended(PyObject *self, PyObject *args); +PyObject *psutil_proc_kill(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_suspend_or_resume(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_username(PyObject *self, PyObject *args); +PyObject *psutil_proc_wait(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/process_info.h b/psutil/arch/windows/process_info.h index 26190427f3..b7795451dc 100644 --- a/psutil/arch/windows/process_info.h +++ b/psutil/arch/windows/process_info.h @@ -5,6 +5,9 @@ */ #include +#include + +#include "ntextapi.h" #define PSUTIL_FIRST_PROCESS(Processes) ( \ (PSYSTEM_PROCESS_INFORMATION)(Processes)) diff --git a/setup.py b/setup.py index 4682a4dd54..f78eb5f652 100755 --- a/setup.py +++ b/setup.py @@ -214,6 +214,7 @@ def get_winver(): 'psutil/arch/windows/disk.c', 'psutil/arch/windows/mem.c', 'psutil/arch/windows/net.c', + 'psutil/arch/windows/proc.c', 'psutil/arch/windows/process_handles.c', 'psutil/arch/windows/process_info.c', 'psutil/arch/windows/process_utils.c', From 3287ea328f76da7e5ff6cc148fdbd5afe33099dd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 20 Apr 2023 00:22:49 +0200 Subject: [PATCH 0982/1714] fix failing users() test; update HISTORY; give CREDITS to @0-wiz-0 for #2241 Signed-off-by: Giampaolo Rodola --- CREDITS | 2 +- HISTORY.rst | 8 ++++++++ psutil/tests/test_posix.py | 11 +++++++++-- scripts/internal/generate_manifest.py | 7 +++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/CREDITS b/CREDITS index c845a8c661..f5888fb79b 100644 --- a/CREDITS +++ b/CREDITS @@ -85,7 +85,7 @@ I: 18 N: Thomas Klausner W: https://github.com/0-wiz-0 D: NetBSD implementation (co-author). -I: 557, 2128 +I: 557, 2128, 2241 N: Ryo Onodera W: https://github.com/ryoon diff --git a/HISTORY.rst b/HISTORY.rst index fb74f36745..1a79b360c6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,13 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.6 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +- 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas + Klausner) + 5.9.5 ===== diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 8ea6cf7c23..9ce82cae51 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -358,6 +358,7 @@ 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: @@ -368,8 +369,14 @@ def test_users_started(self): if started: tstamp = "%b %d %H:%M" else: - raise ValueError( - "cannot interpret tstamp in who output\n%s" % (out)) + # 'Apr 10' + started = re.findall(r"[A-Z][a-z][a-z] \d\d", out) + if started: + tstamp = "%b %d" + + if not tstamp: + raise ValueError( + "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( diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 3d3e83b0cb..b7ad8c7e1e 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -23,16 +23,15 @@ def sh(cmd): def main(): - files = [] + files = set() for file in sh("git ls-files").split('\n'): if file.startswith(SKIP_PREFIXES) or \ os.path.splitext(file)[1].lower() in SKIP_EXTS or \ file in SKIP_FILES: continue - if file not in files: - files.append(file) + files.add(file) - for file in files: + for file in sorted(files): print("include " + file) From 4982b4a46da0b50951b0978e41419321d1a45ec5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 20 Apr 2023 10:47:22 +0200 Subject: [PATCH 0983/1714] OSX big C refactoring (#2242) --- MANIFEST.in | 10 + psutil/_psutil_osx.c | 792 +------------------------------------- psutil/arch/osx/cpu.c | 71 +++- psutil/arch/osx/cpu.h | 5 +- psutil/arch/osx/disk.c | 377 ++++++++++++++++++ psutil/arch/osx/disk.h | 11 + psutil/arch/osx/mem.c | 113 ++++++ psutil/arch/osx/mem.h | 10 + psutil/arch/osx/net.c | 101 +++++ psutil/arch/osx/net.h | 9 + psutil/arch/osx/sensors.c | 102 +++++ psutil/arch/osx/sensors.h | 9 + psutil/arch/osx/sys.c | 88 +++++ psutil/arch/osx/sys.h | 10 + setup.py | 5 + 15 files changed, 921 insertions(+), 792 deletions(-) create mode 100644 psutil/arch/osx/disk.c create mode 100644 psutil/arch/osx/disk.h create mode 100644 psutil/arch/osx/mem.c create mode 100644 psutil/arch/osx/mem.h create mode 100644 psutil/arch/osx/net.c create mode 100644 psutil/arch/osx/net.h create mode 100644 psutil/arch/osx/sensors.c create mode 100644 psutil/arch/osx/sensors.h create mode 100644 psutil/arch/osx/sys.c create mode 100644 psutil/arch/osx/sys.h diff --git a/MANIFEST.in b/MANIFEST.in index 5d97acacdc..94d7708efd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -93,8 +93,18 @@ include psutil/arch/openbsd/socks.c include psutil/arch/openbsd/socks.h include psutil/arch/osx/cpu.c include psutil/arch/osx/cpu.h +include psutil/arch/osx/disk.c +include psutil/arch/osx/disk.h +include psutil/arch/osx/mem.c +include psutil/arch/osx/mem.h +include psutil/arch/osx/net.c +include psutil/arch/osx/net.h include psutil/arch/osx/process_info.c include psutil/arch/osx/process_info.h +include psutil/arch/osx/sensors.c +include psutil/arch/osx/sensors.h +include psutil/arch/osx/sys.c +include psutil/arch/osx/sys.h include psutil/arch/solaris/environ.c include psutil/arch/solaris/environ.h include psutil/arch/solaris/v10/ifaddrs.c diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 713f3d6c8e..30a5312e35 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -12,33 +12,27 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include #include #include - #include -#include -#include -#include -#include -#include -#include -#include - #include "_psutil_common.h" #include "_psutil_posix.h" -#include "arch/osx/process_info.h" #include "arch/osx/cpu.h" +#include "arch/osx/disk.h" +#include "arch/osx/mem.h" +#include "arch/osx/net.h" +#include "arch/osx/process_info.h" +#include "arch/osx/sensors.h" +#include "arch/osx/sys.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -46,28 +40,6 @@ static PyObject *ZombieProcessError; -/* - * A wrapper around host_statistics() invoked with HOST_VM_INFO. - */ -int -psutil_sys_vminfo(vm_statistics_data_t *vmstat) { - kern_return_t ret; - mach_msg_type_number_t count = sizeof(*vmstat) / sizeof(integer_t); - mach_port_t mport = mach_host_self(); - - ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)vmstat, &count); - if (ret != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, - "host_statistics(HOST_VM_INFO) syscall failed: %s", - mach_error_string(ret)); - return 0; - } - mach_port_deallocate(mach_task_self(), mport); - return 1; -} - - /* * A wrapper around task_for_pid() which sucks big time: * - it's not documented @@ -498,350 +470,6 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { } -/* - * Return system virtual memory stats. - * See: - * https://opensource.apple.com/source/system_cmds/system_cmds-790/ - * vm_stat.tproj/vm_stat.c.auto.html - */ -static PyObject * -psutil_virtual_mem(PyObject *self, PyObject *args) { - int mib[2]; - uint64_t total; - size_t len = sizeof(total); - vm_statistics_data_t vm; - long pagesize = psutil_getpagesize(); - // physical mem - mib[0] = CTL_HW; - mib[1] = HW_MEMSIZE; - - // This is also available as sysctlbyname("hw.memsize"). - if (sysctl(mib, 2, &total, &len, NULL, 0)) { - if (errno != 0) - PyErr_SetFromErrno(PyExc_OSError); - else - PyErr_Format( - PyExc_RuntimeError, "sysctl(HW_MEMSIZE) syscall failed"); - return NULL; - } - - // vm - if (!psutil_sys_vminfo(&vm)) - return NULL; - - return Py_BuildValue( - "KKKKKK", - total, - (unsigned long long) vm.active_count * pagesize, // active - (unsigned long long) vm.inactive_count * pagesize, // inactive - (unsigned long long) vm.wire_count * pagesize, // wired - (unsigned long long) vm.free_count * pagesize, // free - (unsigned long long) vm.speculative_count * pagesize // speculative - ); -} - - -/* - * Return stats about swap memory. - */ -static PyObject * -psutil_swap_mem(PyObject *self, PyObject *args) { - int mib[2]; - size_t size; - struct xsw_usage totals; - vm_statistics_data_t vmstat; - long pagesize = psutil_getpagesize(); - - mib[0] = CTL_VM; - mib[1] = VM_SWAPUSAGE; - size = sizeof(totals); - if (sysctl(mib, 2, &totals, &size, NULL, 0) == -1) { - if (errno != 0) - PyErr_SetFromErrno(PyExc_OSError); - else - PyErr_Format( - PyExc_RuntimeError, "sysctl(VM_SWAPUSAGE) syscall failed"); - return NULL; - } - if (!psutil_sys_vminfo(&vmstat)) - return NULL; - - return Py_BuildValue( - "LLLKK", - totals.xsu_total, - totals.xsu_used, - totals.xsu_avail, - (unsigned long long)vmstat.pageins * pagesize, - (unsigned long long)vmstat.pageouts * pagesize); -} - - -/* - * Return a Python list of tuple representing per-cpu times - */ -static PyObject * -psutil_per_cpu_times(PyObject *self, PyObject *args) { - natural_t cpu_count; - natural_t i; - processor_info_array_t info_array; - mach_msg_type_number_t info_count; - kern_return_t error; - processor_cpu_load_info_data_t *cpu_load_info = NULL; - int ret; - PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; - - if (py_retlist == NULL) - return NULL; - - mach_port_t host_port = mach_host_self(); - error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, - &cpu_count, &info_array, &info_count); - if (error != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, - "host_processor_info(PROCESSOR_CPU_LOAD_INFO) syscall failed: %s", - mach_error_string(error)); - goto error; - } - mach_port_deallocate(mach_task_self(), host_port); - - cpu_load_info = (processor_cpu_load_info_data_t *) info_array; - - for (i = 0; i < cpu_count; i++) { - py_cputime = Py_BuildValue( - "(dddd)", - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_USER] / CLK_TCK, - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_NICE] / CLK_TCK, - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM] / CLK_TCK, - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE] / CLK_TCK - ); - if (!py_cputime) - goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_CLEAR(py_cputime); - } - - ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, - info_count * sizeof(int)); - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); - return py_retlist; - -error: - Py_XDECREF(py_cputime); - Py_DECREF(py_retlist); - if (cpu_load_info != NULL) { - ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, - info_count * sizeof(int)); - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); - } - return NULL; -} - - -/* - * Return a Python float indicating the system boot time expressed in - * seconds since the epoch. - */ -static PyObject * -psutil_boot_time(PyObject *self, PyObject *args) { - // fetch sysctl "kern.boottime" - static int request[2] = { CTL_KERN, KERN_BOOTTIME }; - struct timeval result; - size_t result_len = sizeof result; - time_t boot_time = 0; - - if (sysctl(request, 2, &result, &result_len, NULL, 0) == -1) - return PyErr_SetFromErrno(PyExc_OSError); - boot_time = result.tv_sec; - return Py_BuildValue("f", (float)boot_time); -} - - -/* - * Return a list of tuples including device, mount point and fs type - * for all partitions mounted on the system. - */ -static PyObject * -psutil_disk_partitions(PyObject *self, PyObject *args) { - int num; - int i; - int len; - uint64_t flags; - char opts[400]; - struct statfs *fs = NULL; - PyObject *py_dev = NULL; - PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - // get the number of mount points - Py_BEGIN_ALLOW_THREADS - num = getfsstat(NULL, 0, MNT_NOWAIT); - Py_END_ALLOW_THREADS - if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - len = sizeof(*fs) * num; - fs = malloc(len); - if (fs == NULL) { - PyErr_NoMemory(); - goto error; - } - - Py_BEGIN_ALLOW_THREADS - num = getfsstat(fs, len, MNT_NOWAIT); - Py_END_ALLOW_THREADS - if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (i = 0; i < num; i++) { - opts[0] = 0; - flags = fs[i].f_flags; - - // see sys/mount.h - if (flags & MNT_RDONLY) - strlcat(opts, "ro", sizeof(opts)); - else - strlcat(opts, "rw", sizeof(opts)); - if (flags & MNT_SYNCHRONOUS) - strlcat(opts, ",sync", sizeof(opts)); - if (flags & MNT_NOEXEC) - strlcat(opts, ",noexec", sizeof(opts)); - if (flags & MNT_NOSUID) - strlcat(opts, ",nosuid", sizeof(opts)); - if (flags & MNT_UNION) - strlcat(opts, ",union", sizeof(opts)); - if (flags & MNT_ASYNC) - strlcat(opts, ",async", sizeof(opts)); - if (flags & MNT_EXPORTED) - strlcat(opts, ",exported", sizeof(opts)); - if (flags & MNT_QUARANTINE) - strlcat(opts, ",quarantine", sizeof(opts)); - if (flags & MNT_LOCAL) - strlcat(opts, ",local", sizeof(opts)); - if (flags & MNT_QUOTA) - strlcat(opts, ",quota", sizeof(opts)); - if (flags & MNT_ROOTFS) - strlcat(opts, ",rootfs", sizeof(opts)); - if (flags & MNT_DOVOLFS) - strlcat(opts, ",dovolfs", sizeof(opts)); - if (flags & MNT_DONTBROWSE) - strlcat(opts, ",dontbrowse", sizeof(opts)); - if (flags & MNT_IGNORE_OWNERSHIP) - strlcat(opts, ",ignore-ownership", sizeof(opts)); - if (flags & MNT_AUTOMOUNTED) - strlcat(opts, ",automounted", sizeof(opts)); - if (flags & MNT_JOURNALED) - strlcat(opts, ",journaled", sizeof(opts)); - if (flags & MNT_NOUSERXATTR) - strlcat(opts, ",nouserxattr", sizeof(opts)); - if (flags & MNT_DEFWRITE) - strlcat(opts, ",defwrite", sizeof(opts)); - if (flags & MNT_MULTILABEL) - strlcat(opts, ",multilabel", sizeof(opts)); - if (flags & MNT_NOATIME) - strlcat(opts, ",noatime", sizeof(opts)); - if (flags & MNT_UPDATE) - strlcat(opts, ",update", sizeof(opts)); - if (flags & MNT_RELOAD) - strlcat(opts, ",reload", sizeof(opts)); - if (flags & MNT_FORCE) - strlcat(opts, ",force", sizeof(opts)); - if (flags & MNT_CMDFLAGS) - strlcat(opts, ",cmdflags", sizeof(opts)); - - py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); - if (! py_dev) - goto error; - py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); - if (! py_mountp) - goto error; - py_tuple = Py_BuildValue( - "(OOss)", - py_dev, // device - py_mountp, // mount point - fs[i].f_fstypename, // fs type - opts); // options - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_dev); - Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); - } - - free(fs); - return py_retlist; - -error: - Py_XDECREF(py_dev); - Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (fs != NULL) - free(fs); - return NULL; -} - - -static PyObject * -psutil_disk_usage_used(PyObject *self, PyObject *args) { - PyObject *py_default_value; - PyObject *py_mount_point_bytes = NULL; - char* mount_point; - -#if PY_MAJOR_VERSION >= 3 - if (!PyArg_ParseTuple(args, "O&O", PyUnicode_FSConverter, &py_mount_point_bytes, &py_default_value)) { - return NULL; - } - mount_point = PyBytes_AsString(py_mount_point_bytes); - if (NULL == mount_point) { - Py_XDECREF(py_mount_point_bytes); - return NULL; - } -#else - if (!PyArg_ParseTuple(args, "sO", &mount_point, &py_default_value)) { - return NULL; - } -#endif - -#ifdef ATTR_VOL_SPACEUSED - /* Call getattrlist(ATTR_VOL_SPACEUSED) to get used space info. */ - int ret; - struct { - uint32_t size; - uint64_t spaceused; - } __attribute__((aligned(4), packed)) attrbuf = {0}; - struct attrlist attrs = {0}; - - attrs.bitmapcount = ATTR_BIT_MAP_COUNT; - attrs.volattr = ATTR_VOL_INFO | ATTR_VOL_SPACEUSED; - Py_BEGIN_ALLOW_THREADS - ret = getattrlist(mount_point, &attrs, &attrbuf, sizeof(attrbuf), 0); - Py_END_ALLOW_THREADS - if (ret == 0) { - Py_XDECREF(py_mount_point_bytes); - return PyLong_FromUnsignedLongLong(attrbuf.spaceused); - } - psutil_debug("getattrlist(ATTR_VOL_SPACEUSED) failed, fall-back to default value"); -#endif - Py_XDECREF(py_mount_point_bytes); - Py_INCREF(py_default_value); - return py_default_value; -} - /* * Return process threads */ @@ -1253,414 +881,6 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { } -/* - * Return a Python list of named tuples with overall network I/O information - */ -static PyObject * -psutil_net_io_counters(PyObject *self, PyObject *args) { - char *buf = NULL, *lim, *next; - struct if_msghdr *ifm; - int mib[6]; - mib[0] = CTL_NET; // networking subsystem - mib[1] = PF_ROUTE; // type of information - mib[2] = 0; // protocol (IPPROTO_xxx) - mib[3] = 0; // address family - mib[4] = NET_RT_IFLIST2; // operation - mib[5] = 0; - size_t len; - PyObject *py_ifc_info = NULL; - PyObject *py_retdict = PyDict_New(); - - if (py_retdict == NULL) - return NULL; - - if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - buf = malloc(len); - if (buf == NULL) { - PyErr_NoMemory(); - goto error; - } - - if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - lim = buf + len; - - for (next = buf; next < lim; ) { - ifm = (struct if_msghdr *)next; - next += ifm->ifm_msglen; - - if (ifm->ifm_type == RTM_IFINFO2) { - py_ifc_info = NULL; - struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; - struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); - char ifc_name[32]; - - strncpy(ifc_name, sdl->sdl_data, sdl->sdl_nlen); - ifc_name[sdl->sdl_nlen] = 0; - - py_ifc_info = Py_BuildValue( - "(KKKKKKKi)", - if2m->ifm_data.ifi_obytes, - if2m->ifm_data.ifi_ibytes, - if2m->ifm_data.ifi_opackets, - if2m->ifm_data.ifi_ipackets, - if2m->ifm_data.ifi_ierrors, - if2m->ifm_data.ifi_oerrors, - if2m->ifm_data.ifi_iqdrops, - 0); // dropout not supported - - if (!py_ifc_info) - goto error; - if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) - goto error; - Py_CLEAR(py_ifc_info); - } - else { - continue; - } - } - - free(buf); - return py_retdict; - -error: - Py_XDECREF(py_ifc_info); - Py_DECREF(py_retdict); - if (buf != NULL) - free(buf); - return NULL; -} - - -/* - * Return a Python dict of tuples for disk I/O information - */ -static PyObject * -psutil_disk_io_counters(PyObject *self, PyObject *args) { - CFDictionaryRef parent_dict; - CFDictionaryRef props_dict; - CFDictionaryRef stats_dict; - io_registry_entry_t parent; - io_registry_entry_t disk; - io_iterator_t disk_list; - PyObject *py_disk_info = NULL; - PyObject *py_retdict = PyDict_New(); - - if (py_retdict == NULL) - return NULL; - - // Get list of disks - if (IOServiceGetMatchingServices(kIOMasterPortDefault, - IOServiceMatching(kIOMediaClass), - &disk_list) != kIOReturnSuccess) { - PyErr_SetString( - PyExc_RuntimeError, "unable to get the list of disks."); - goto error; - } - - // Iterate over disks - while ((disk = IOIteratorNext(disk_list)) != 0) { - py_disk_info = NULL; - parent_dict = NULL; - props_dict = NULL; - stats_dict = NULL; - - if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) - != kIOReturnSuccess) { - PyErr_SetString(PyExc_RuntimeError, - "unable to get the disk's parent."); - IOObjectRelease(disk); - goto error; - } - - if (IOObjectConformsTo(parent, "IOBlockStorageDriver")) { - if (IORegistryEntryCreateCFProperties( - disk, - (CFMutableDictionaryRef *) &parent_dict, - kCFAllocatorDefault, - kNilOptions - ) != kIOReturnSuccess) - { - PyErr_SetString(PyExc_RuntimeError, - "unable to get the parent's properties."); - IOObjectRelease(disk); - IOObjectRelease(parent); - goto error; - } - - if (IORegistryEntryCreateCFProperties( - parent, - (CFMutableDictionaryRef *) &props_dict, - kCFAllocatorDefault, - kNilOptions - ) != kIOReturnSuccess) - { - PyErr_SetString(PyExc_RuntimeError, - "unable to get the disk properties."); - CFRelease(props_dict); - IOObjectRelease(disk); - IOObjectRelease(parent); - goto error; - } - - const int kMaxDiskNameSize = 64; - CFStringRef disk_name_ref = (CFStringRef)CFDictionaryGetValue( - parent_dict, CFSTR(kIOBSDNameKey)); - char disk_name[kMaxDiskNameSize]; - - CFStringGetCString(disk_name_ref, - disk_name, - kMaxDiskNameSize, - CFStringGetSystemEncoding()); - - stats_dict = (CFDictionaryRef)CFDictionaryGetValue( - props_dict, CFSTR(kIOBlockStorageDriverStatisticsKey)); - - if (stats_dict == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "Unable to get disk stats."); - goto error; - } - - CFNumberRef number; - int64_t reads = 0; - int64_t writes = 0; - int64_t read_bytes = 0; - int64_t write_bytes = 0; - int64_t read_time = 0; - int64_t write_time = 0; - - // Get disk reads/writes - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsReadsKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &reads); - } - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsWritesKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &writes); - } - - // Get disk bytes read/written - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsBytesReadKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &read_bytes); - } - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsBytesWrittenKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &write_bytes); - } - - // Get disk time spent reading/writing (nanoseconds) - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsTotalReadTimeKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &read_time); - } - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsTotalWriteTimeKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &write_time); - } - - // Read/Write time on macOS comes back in nanoseconds and in psutil - // we've standardized on milliseconds so do the conversion. - py_disk_info = Py_BuildValue( - "(KKKKKK)", - reads, - writes, - read_bytes, - write_bytes, - read_time / 1000 / 1000, - write_time / 1000 / 1000); - if (!py_disk_info) - goto error; - if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) - goto error; - Py_CLEAR(py_disk_info); - - CFRelease(parent_dict); - IOObjectRelease(parent); - CFRelease(props_dict); - IOObjectRelease(disk); - } - } - - IOObjectRelease (disk_list); - - return py_retdict; - -error: - Py_XDECREF(py_disk_info); - Py_DECREF(py_retdict); - return NULL; -} - - -/* - * Return currently connected users as a list of tuples. - */ -static PyObject * -psutil_users(PyObject *self, PyObject *args) { - struct utmpx *utx; - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - while ((utx = getutxent()) != NULL) { - if (utx->ut_type != USER_PROCESS) - continue; - py_username = PyUnicode_DecodeFSDefault(utx->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(utx->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); - if (! py_hostname) - goto error; - py_tuple = Py_BuildValue( - "(OOOdi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)utx->ut_tv.tv_sec, // start time - utx->ut_pid // process id - ); - if (!py_tuple) { - endutxent(); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - endutxent(); - goto error; - } - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - - endutxent(); - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - return NULL; -} - - -/* - * Return battery information. - */ -static PyObject * -psutil_sensors_battery(PyObject *self, PyObject *args) { - PyObject *py_tuple = NULL; - CFTypeRef power_info = NULL; - CFArrayRef power_sources_list = NULL; - CFDictionaryRef power_sources_information = NULL; - CFNumberRef capacity_ref = NULL; - CFNumberRef time_to_empty_ref = NULL; - CFStringRef ps_state_ref = NULL; - uint32_t capacity; /* units are percent */ - int time_to_empty; /* units are minutes */ - int is_power_plugged; - - power_info = IOPSCopyPowerSourcesInfo(); - - if (!power_info) { - PyErr_SetString(PyExc_RuntimeError, - "IOPSCopyPowerSourcesInfo() syscall failed"); - goto error; - } - - power_sources_list = IOPSCopyPowerSourcesList(power_info); - if (!power_sources_list) { - PyErr_SetString(PyExc_RuntimeError, - "IOPSCopyPowerSourcesList() syscall failed"); - goto error; - } - - /* Should only get one source. But in practice, check for > 0 sources */ - if (!CFArrayGetCount(power_sources_list)) { - PyErr_SetString(PyExc_NotImplementedError, "no battery"); - goto error; - } - - power_sources_information = IOPSGetPowerSourceDescription( - power_info, CFArrayGetValueAtIndex(power_sources_list, 0)); - - capacity_ref = (CFNumberRef) CFDictionaryGetValue( - power_sources_information, CFSTR(kIOPSCurrentCapacityKey)); - if (!CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) { - PyErr_SetString(PyExc_RuntimeError, - "No battery capacity infomration in power sources info"); - goto error; - } - - ps_state_ref = (CFStringRef) CFDictionaryGetValue( - power_sources_information, CFSTR(kIOPSPowerSourceStateKey)); - is_power_plugged = CFStringCompare( - ps_state_ref, CFSTR(kIOPSACPowerValue), 0) - == kCFCompareEqualTo; - - time_to_empty_ref = (CFNumberRef) CFDictionaryGetValue( - power_sources_information, CFSTR(kIOPSTimeToEmptyKey)); - if (!CFNumberGetValue(time_to_empty_ref, - kCFNumberIntType, &time_to_empty)) { - /* This value is recommended for non-Apple power sources, so it's not - * an error if it doesn't exist. We'll return -1 for "unknown" */ - /* A value of -1 indicates "Still Calculating the Time" also for - * apple power source */ - time_to_empty = -1; - } - - py_tuple = Py_BuildValue("Iii", - capacity, time_to_empty, is_power_plugged); - if (!py_tuple) { - goto error; - } - - CFRelease(power_info); - CFRelease(power_sources_list); - /* Caller should NOT release power_sources_information */ - - return py_tuple; - -error: - if (power_info) - CFRelease(power_info); - if (power_sources_list) - CFRelease(power_sources_list); - Py_XDECREF(py_tuple); - return NULL; -} - - /* * define the psutil C module methods and initialize the module. */ diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 6e56471812..a1ba114296 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -19,18 +19,18 @@ For reference, here's the git history with original implementations: */ #include -#include -#include - #include #include #include +#include +#include +#include +#include #include "../../_psutil_common.h" #include "../../_psutil_posix.h" - PyObject * psutil_cpu_count_logical(PyObject *self, PyObject *args) { int num; @@ -138,3 +138,66 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { min / 1000 / 1000, max / 1000 / 1000); } + + +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + natural_t cpu_count; + natural_t i; + processor_info_array_t info_array; + mach_msg_type_number_t info_count; + kern_return_t error; + processor_cpu_load_info_data_t *cpu_load_info = NULL; + int ret; + PyObject *py_retlist = PyList_New(0); + PyObject *py_cputime = NULL; + + if (py_retlist == NULL) + return NULL; + + mach_port_t host_port = mach_host_self(); + error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, + &cpu_count, &info_array, &info_count); + if (error != KERN_SUCCESS) { + PyErr_Format( + PyExc_RuntimeError, + "host_processor_info(PROCESSOR_CPU_LOAD_INFO) syscall failed: %s", + mach_error_string(error)); + goto error; + } + mach_port_deallocate(mach_task_self(), host_port); + + cpu_load_info = (processor_cpu_load_info_data_t *) info_array; + + for (i = 0; i < cpu_count; i++) { + py_cputime = Py_BuildValue( + "(dddd)", + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_USER] / CLK_TCK, + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_NICE] / CLK_TCK, + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM] / CLK_TCK, + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE] / CLK_TCK + ); + if (!py_cputime) + goto error; + if (PyList_Append(py_retlist, py_cputime)) + goto error; + Py_CLEAR(py_cputime); + } + + ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, + info_count * sizeof(int)); + if (ret != KERN_SUCCESS) + PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); + return py_retlist; + +error: + Py_XDECREF(py_cputime); + Py_DECREF(py_retlist); + if (cpu_load_info != NULL) { + ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, + info_count * sizeof(int)); + if (ret != KERN_SUCCESS) + PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); + } + return NULL; +} diff --git a/psutil/arch/osx/cpu.h b/psutil/arch/osx/cpu.h index aac0f809b1..6cf92f82b3 100644 --- a/psutil/arch/osx/cpu.h +++ b/psutil/arch/osx/cpu.h @@ -6,8 +6,9 @@ #include -PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); -PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c new file mode 100644 index 0000000000..961fc42a48 --- /dev/null +++ b/psutil/arch/osx/disk.c @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +// Disk related functions. Original code was refactored and moved +// from psutil/_psutil_osx.c in 2023. This is the GIT blame before the move: +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../_psutil_common.h" + + +/* + * Return a list of tuples including device, mount point and fs type + * for all partitions mounted on the system. + */ +PyObject * +psutil_disk_partitions(PyObject *self, PyObject *args) { + int num; + int i; + int len; + uint64_t flags; + char opts[400]; + struct statfs *fs = NULL; + PyObject *py_dev = NULL; + PyObject *py_mountp = NULL; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + // get the number of mount points + Py_BEGIN_ALLOW_THREADS + num = getfsstat(NULL, 0, MNT_NOWAIT); + Py_END_ALLOW_THREADS + if (num == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + len = sizeof(*fs) * num; + fs = malloc(len); + if (fs == NULL) { + PyErr_NoMemory(); + goto error; + } + + Py_BEGIN_ALLOW_THREADS + num = getfsstat(fs, len, MNT_NOWAIT); + Py_END_ALLOW_THREADS + if (num == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (i = 0; i < num; i++) { + opts[0] = 0; + flags = fs[i].f_flags; + + // see sys/mount.h + if (flags & MNT_RDONLY) + strlcat(opts, "ro", sizeof(opts)); + else + strlcat(opts, "rw", sizeof(opts)); + if (flags & MNT_SYNCHRONOUS) + strlcat(opts, ",sync", sizeof(opts)); + if (flags & MNT_NOEXEC) + strlcat(opts, ",noexec", sizeof(opts)); + if (flags & MNT_NOSUID) + strlcat(opts, ",nosuid", sizeof(opts)); + if (flags & MNT_UNION) + strlcat(opts, ",union", sizeof(opts)); + if (flags & MNT_ASYNC) + strlcat(opts, ",async", sizeof(opts)); + if (flags & MNT_EXPORTED) + strlcat(opts, ",exported", sizeof(opts)); + if (flags & MNT_QUARANTINE) + strlcat(opts, ",quarantine", sizeof(opts)); + if (flags & MNT_LOCAL) + strlcat(opts, ",local", sizeof(opts)); + if (flags & MNT_QUOTA) + strlcat(opts, ",quota", sizeof(opts)); + if (flags & MNT_ROOTFS) + strlcat(opts, ",rootfs", sizeof(opts)); + if (flags & MNT_DOVOLFS) + strlcat(opts, ",dovolfs", sizeof(opts)); + if (flags & MNT_DONTBROWSE) + strlcat(opts, ",dontbrowse", sizeof(opts)); + if (flags & MNT_IGNORE_OWNERSHIP) + strlcat(opts, ",ignore-ownership", sizeof(opts)); + if (flags & MNT_AUTOMOUNTED) + strlcat(opts, ",automounted", sizeof(opts)); + if (flags & MNT_JOURNALED) + strlcat(opts, ",journaled", sizeof(opts)); + if (flags & MNT_NOUSERXATTR) + strlcat(opts, ",nouserxattr", sizeof(opts)); + if (flags & MNT_DEFWRITE) + strlcat(opts, ",defwrite", sizeof(opts)); + if (flags & MNT_MULTILABEL) + strlcat(opts, ",multilabel", sizeof(opts)); + if (flags & MNT_NOATIME) + strlcat(opts, ",noatime", sizeof(opts)); + if (flags & MNT_UPDATE) + strlcat(opts, ",update", sizeof(opts)); + if (flags & MNT_RELOAD) + strlcat(opts, ",reload", sizeof(opts)); + if (flags & MNT_FORCE) + strlcat(opts, ",force", sizeof(opts)); + if (flags & MNT_CMDFLAGS) + strlcat(opts, ",cmdflags", sizeof(opts)); + + py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); + if (! py_dev) + goto error; + py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); + if (! py_mountp) + goto error; + py_tuple = Py_BuildValue( + "(OOss)", + py_dev, // device + py_mountp, // mount point + fs[i].f_fstypename, // fs type + opts); // options + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); + } + + free(fs); + return py_retlist; + +error: + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + if (fs != NULL) + free(fs); + return NULL; +} + + +PyObject * +psutil_disk_usage_used(PyObject *self, PyObject *args) { + PyObject *py_default_value; + PyObject *py_mount_point_bytes = NULL; + char* mount_point; + +#if PY_MAJOR_VERSION >= 3 + if (!PyArg_ParseTuple(args, "O&O", PyUnicode_FSConverter, &py_mount_point_bytes, &py_default_value)) { + return NULL; + } + mount_point = PyBytes_AsString(py_mount_point_bytes); + if (NULL == mount_point) { + Py_XDECREF(py_mount_point_bytes); + return NULL; + } +#else + if (!PyArg_ParseTuple(args, "sO", &mount_point, &py_default_value)) { + return NULL; + } +#endif + +#ifdef ATTR_VOL_SPACEUSED + /* Call getattrlist(ATTR_VOL_SPACEUSED) to get used space info. */ + int ret; + struct { + uint32_t size; + uint64_t spaceused; + } __attribute__((aligned(4), packed)) attrbuf = {0}; + struct attrlist attrs = {0}; + + attrs.bitmapcount = ATTR_BIT_MAP_COUNT; + attrs.volattr = ATTR_VOL_INFO | ATTR_VOL_SPACEUSED; + Py_BEGIN_ALLOW_THREADS + ret = getattrlist(mount_point, &attrs, &attrbuf, sizeof(attrbuf), 0); + Py_END_ALLOW_THREADS + if (ret == 0) { + Py_XDECREF(py_mount_point_bytes); + return PyLong_FromUnsignedLongLong(attrbuf.spaceused); + } + psutil_debug("getattrlist(ATTR_VOL_SPACEUSED) failed, fall-back to default value"); +#endif + Py_XDECREF(py_mount_point_bytes); + Py_INCREF(py_default_value); + return py_default_value; +} + + +/* + * Return a Python dict of tuples for disk I/O information + */ +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + CFDictionaryRef parent_dict; + CFDictionaryRef props_dict; + CFDictionaryRef stats_dict; + io_registry_entry_t parent; + io_registry_entry_t disk; + io_iterator_t disk_list; + PyObject *py_disk_info = NULL; + PyObject *py_retdict = PyDict_New(); + + if (py_retdict == NULL) + return NULL; + + // Get list of disks + if (IOServiceGetMatchingServices(kIOMasterPortDefault, + IOServiceMatching(kIOMediaClass), + &disk_list) != kIOReturnSuccess) { + PyErr_SetString( + PyExc_RuntimeError, "unable to get the list of disks."); + goto error; + } + + // Iterate over disks + while ((disk = IOIteratorNext(disk_list)) != 0) { + py_disk_info = NULL; + parent_dict = NULL; + props_dict = NULL; + stats_dict = NULL; + + if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) + != kIOReturnSuccess) { + PyErr_SetString(PyExc_RuntimeError, + "unable to get the disk's parent."); + IOObjectRelease(disk); + goto error; + } + + if (IOObjectConformsTo(parent, "IOBlockStorageDriver")) { + if (IORegistryEntryCreateCFProperties( + disk, + (CFMutableDictionaryRef *) &parent_dict, + kCFAllocatorDefault, + kNilOptions + ) != kIOReturnSuccess) + { + PyErr_SetString(PyExc_RuntimeError, + "unable to get the parent's properties."); + IOObjectRelease(disk); + IOObjectRelease(parent); + goto error; + } + + if (IORegistryEntryCreateCFProperties( + parent, + (CFMutableDictionaryRef *) &props_dict, + kCFAllocatorDefault, + kNilOptions + ) != kIOReturnSuccess) + { + PyErr_SetString(PyExc_RuntimeError, + "unable to get the disk properties."); + CFRelease(props_dict); + IOObjectRelease(disk); + IOObjectRelease(parent); + goto error; + } + + const int kMaxDiskNameSize = 64; + CFStringRef disk_name_ref = (CFStringRef)CFDictionaryGetValue( + parent_dict, CFSTR(kIOBSDNameKey)); + char disk_name[kMaxDiskNameSize]; + + CFStringGetCString(disk_name_ref, + disk_name, + kMaxDiskNameSize, + CFStringGetSystemEncoding()); + + stats_dict = (CFDictionaryRef)CFDictionaryGetValue( + props_dict, CFSTR(kIOBlockStorageDriverStatisticsKey)); + + if (stats_dict == NULL) { + PyErr_SetString(PyExc_RuntimeError, + "Unable to get disk stats."); + goto error; + } + + CFNumberRef number; + int64_t reads = 0; + int64_t writes = 0; + int64_t read_bytes = 0; + int64_t write_bytes = 0; + int64_t read_time = 0; + int64_t write_time = 0; + + // Get disk reads/writes + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsReadsKey)))) + { + CFNumberGetValue(number, kCFNumberSInt64Type, &reads); + } + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsWritesKey)))) + { + CFNumberGetValue(number, kCFNumberSInt64Type, &writes); + } + + // Get disk bytes read/written + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsBytesReadKey)))) + { + CFNumberGetValue(number, kCFNumberSInt64Type, &read_bytes); + } + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsBytesWrittenKey)))) + { + CFNumberGetValue(number, kCFNumberSInt64Type, &write_bytes); + } + + // Get disk time spent reading/writing (nanoseconds) + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsTotalReadTimeKey)))) + { + CFNumberGetValue(number, kCFNumberSInt64Type, &read_time); + } + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsTotalWriteTimeKey)))) + { + CFNumberGetValue(number, kCFNumberSInt64Type, &write_time); + } + + // Read/Write time on macOS comes back in nanoseconds and in psutil + // we've standardized on milliseconds so do the conversion. + py_disk_info = Py_BuildValue( + "(KKKKKK)", + reads, + writes, + read_bytes, + write_bytes, + read_time / 1000 / 1000, + write_time / 1000 / 1000); + if (!py_disk_info) + goto error; + if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) + goto error; + Py_CLEAR(py_disk_info); + + CFRelease(parent_dict); + IOObjectRelease(parent); + CFRelease(props_dict); + IOObjectRelease(disk); + } + } + + IOObjectRelease (disk_list); + + return py_retdict; + +error: + Py_XDECREF(py_disk_info); + Py_DECREF(py_retdict); + return NULL; +} diff --git a/psutil/arch/osx/disk.h b/psutil/arch/osx/disk.h new file mode 100644 index 0000000000..88ca9a28b1 --- /dev/null +++ b/psutil/arch/osx/disk.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c new file mode 100644 index 0000000000..53493065c2 --- /dev/null +++ b/psutil/arch/osx/mem.c @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +// System memory related functions. Original code was refactored and moved +// from psutil/_psutil_osx.c in 2023. This is the GIT blame before the move: +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c + +#include +#include +#include +#include + +#include "../../_psutil_posix.h" + + +static int +psutil_sys_vminfo(vm_statistics_data_t *vmstat) { + kern_return_t ret; + mach_msg_type_number_t count = sizeof(*vmstat) / sizeof(integer_t); + mach_port_t mport = mach_host_self(); + + ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)vmstat, &count); + if (ret != KERN_SUCCESS) { + PyErr_Format( + PyExc_RuntimeError, + "host_statistics(HOST_VM_INFO) syscall failed: %s", + mach_error_string(ret)); + return 0; + } + mach_port_deallocate(mach_task_self(), mport); + return 1; +} + + +/* + * Return system virtual memory stats. + * See: + * https://opensource.apple.com/source/system_cmds/system_cmds-790/ + * vm_stat.tproj/vm_stat.c.auto.html + */ +PyObject * +psutil_virtual_mem(PyObject *self, PyObject *args) { + int mib[2]; + uint64_t total; + size_t len = sizeof(total); + vm_statistics_data_t vm; + long pagesize = psutil_getpagesize(); + // physical mem + mib[0] = CTL_HW; + mib[1] = HW_MEMSIZE; + + // This is also available as sysctlbyname("hw.memsize"). + if (sysctl(mib, 2, &total, &len, NULL, 0)) { + if (errno != 0) + PyErr_SetFromErrno(PyExc_OSError); + else + PyErr_Format( + PyExc_RuntimeError, "sysctl(HW_MEMSIZE) syscall failed"); + return NULL; + } + + // vm + if (!psutil_sys_vminfo(&vm)) + return NULL; + + return Py_BuildValue( + "KKKKKK", + total, + (unsigned long long) vm.active_count * pagesize, // active + (unsigned long long) vm.inactive_count * pagesize, // inactive + (unsigned long long) vm.wire_count * pagesize, // wired + (unsigned long long) vm.free_count * pagesize, // free + (unsigned long long) vm.speculative_count * pagesize // speculative + ); +} + + +/* + * Return stats about swap memory. + */ +PyObject * +psutil_swap_mem(PyObject *self, PyObject *args) { + int mib[2]; + size_t size; + struct xsw_usage totals; + vm_statistics_data_t vmstat; + long pagesize = psutil_getpagesize(); + + mib[0] = CTL_VM; + mib[1] = VM_SWAPUSAGE; + size = sizeof(totals); + if (sysctl(mib, 2, &totals, &size, NULL, 0) == -1) { + if (errno != 0) + PyErr_SetFromErrno(PyExc_OSError); + else + PyErr_Format( + PyExc_RuntimeError, "sysctl(VM_SWAPUSAGE) syscall failed"); + return NULL; + } + if (!psutil_sys_vminfo(&vmstat)) + return NULL; + + return Py_BuildValue( + "LLLKK", + totals.xsu_total, + totals.xsu_used, + totals.xsu_avail, + (unsigned long long)vmstat.pageins * pagesize, + (unsigned long long)vmstat.pageouts * pagesize); +} diff --git a/psutil/arch/osx/mem.h b/psutil/arch/osx/mem.h new file mode 100644 index 0000000000..dc4cd74388 --- /dev/null +++ b/psutil/arch/osx/mem.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c new file mode 100644 index 0000000000..e9cc61e9b1 --- /dev/null +++ b/psutil/arch/osx/net.c @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +// Networks related functions. Original code was refactored and moved +// from psutil/_psutil_osx.c in 2023. This is the GIT blame before the move: +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c + +#include +#include +#include +#include +#include +#include + +#include "../../_psutil_common.h" + + +PyObject * +psutil_net_io_counters(PyObject *self, PyObject *args) { + char *buf = NULL, *lim, *next; + struct if_msghdr *ifm; + int mib[6]; + mib[0] = CTL_NET; // networking subsystem + mib[1] = PF_ROUTE; // type of information + mib[2] = 0; // protocol (IPPROTO_xxx) + mib[3] = 0; // address family + mib[4] = NET_RT_IFLIST2; // operation + mib[5] = 0; + size_t len; + PyObject *py_ifc_info = NULL; + PyObject *py_retdict = PyDict_New(); + + if (py_retdict == NULL) + return NULL; + + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + buf = malloc(len); + if (buf == NULL) { + PyErr_NoMemory(); + goto error; + } + + if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + lim = buf + len; + + for (next = buf; next < lim; ) { + ifm = (struct if_msghdr *)next; + next += ifm->ifm_msglen; + + if (ifm->ifm_type == RTM_IFINFO2) { + py_ifc_info = NULL; + struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; + struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); + char ifc_name[32]; + + strncpy(ifc_name, sdl->sdl_data, sdl->sdl_nlen); + ifc_name[sdl->sdl_nlen] = 0; + + py_ifc_info = Py_BuildValue( + "(KKKKKKKi)", + if2m->ifm_data.ifi_obytes, + if2m->ifm_data.ifi_ibytes, + if2m->ifm_data.ifi_opackets, + if2m->ifm_data.ifi_ipackets, + if2m->ifm_data.ifi_ierrors, + if2m->ifm_data.ifi_oerrors, + if2m->ifm_data.ifi_iqdrops, + 0); // dropout not supported + + if (!py_ifc_info) + goto error; + if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) + goto error; + Py_CLEAR(py_ifc_info); + } + else { + continue; + } + } + + free(buf); + return py_retdict; + +error: + Py_XDECREF(py_ifc_info); + Py_DECREF(py_retdict); + if (buf != NULL) + free(buf); + return NULL; +} diff --git a/psutil/arch/osx/net.h b/psutil/arch/osx/net.h new file mode 100644 index 0000000000..99079523c1 --- /dev/null +++ b/psutil/arch/osx/net.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c new file mode 100644 index 0000000000..a2faa157c4 --- /dev/null +++ b/psutil/arch/osx/sensors.c @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +// Sensors related functions. Original code was refactored and moved +// from psutil/_psutil_osx.c in 2023. This is the GIT blame before the move: +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c +// Original battery code: +// https://github.com/giampaolo/psutil/commit/e0df5da + + +#include +#include +#include + +#include "../../_psutil_common.h" + + +PyObject * +psutil_sensors_battery(PyObject *self, PyObject *args) { + PyObject *py_tuple = NULL; + CFTypeRef power_info = NULL; + CFArrayRef power_sources_list = NULL; + CFDictionaryRef power_sources_information = NULL; + CFNumberRef capacity_ref = NULL; + CFNumberRef time_to_empty_ref = NULL; + CFStringRef ps_state_ref = NULL; + uint32_t capacity; /* units are percent */ + int time_to_empty; /* units are minutes */ + int is_power_plugged; + + power_info = IOPSCopyPowerSourcesInfo(); + + if (!power_info) { + PyErr_SetString(PyExc_RuntimeError, + "IOPSCopyPowerSourcesInfo() syscall failed"); + goto error; + } + + power_sources_list = IOPSCopyPowerSourcesList(power_info); + if (!power_sources_list) { + PyErr_SetString(PyExc_RuntimeError, + "IOPSCopyPowerSourcesList() syscall failed"); + goto error; + } + + /* Should only get one source. But in practice, check for > 0 sources */ + if (!CFArrayGetCount(power_sources_list)) { + PyErr_SetString(PyExc_NotImplementedError, "no battery"); + goto error; + } + + power_sources_information = IOPSGetPowerSourceDescription( + power_info, CFArrayGetValueAtIndex(power_sources_list, 0)); + + capacity_ref = (CFNumberRef) CFDictionaryGetValue( + power_sources_information, CFSTR(kIOPSCurrentCapacityKey)); + if (!CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) { + PyErr_SetString(PyExc_RuntimeError, + "No battery capacity infomration in power sources info"); + goto error; + } + + ps_state_ref = (CFStringRef) CFDictionaryGetValue( + power_sources_information, CFSTR(kIOPSPowerSourceStateKey)); + is_power_plugged = CFStringCompare( + ps_state_ref, CFSTR(kIOPSACPowerValue), 0) + == kCFCompareEqualTo; + + time_to_empty_ref = (CFNumberRef) CFDictionaryGetValue( + power_sources_information, CFSTR(kIOPSTimeToEmptyKey)); + if (!CFNumberGetValue(time_to_empty_ref, + kCFNumberIntType, &time_to_empty)) { + /* This value is recommended for non-Apple power sources, so it's not + * an error if it doesn't exist. We'll return -1 for "unknown" */ + /* A value of -1 indicates "Still Calculating the Time" also for + * apple power source */ + time_to_empty = -1; + } + + py_tuple = Py_BuildValue("Iii", + capacity, time_to_empty, is_power_plugged); + if (!py_tuple) { + goto error; + } + + CFRelease(power_info); + CFRelease(power_sources_list); + /* Caller should NOT release power_sources_information */ + + return py_tuple; + +error: + if (power_info) + CFRelease(power_info); + if (power_sources_list) + CFRelease(power_sources_list); + Py_XDECREF(py_tuple); + return NULL; +} diff --git a/psutil/arch/osx/sensors.h b/psutil/arch/osx/sensors.h new file mode 100644 index 0000000000..edace25d3d --- /dev/null +++ b/psutil/arch/osx/sensors.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/sys.c b/psutil/arch/osx/sys.c new file mode 100644 index 0000000000..4fe6642597 --- /dev/null +++ b/psutil/arch/osx/sys.c @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +// System related functions. Original code was refactored and moved +// from psutil/_psutil_osx.c in 2023. This is the GIT blame before the move: +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c + +#include +#include +#include + +#include "../../_psutil_common.h" + + +PyObject * +psutil_boot_time(PyObject *self, PyObject *args) { + // fetch sysctl "kern.boottime" + static int request[2] = { CTL_KERN, KERN_BOOTTIME }; + struct timeval result; + size_t result_len = sizeof result; + time_t boot_time = 0; + + if (sysctl(request, 2, &result, &result_len, NULL, 0) == -1) + return PyErr_SetFromErrno(PyExc_OSError); + boot_time = result.tv_sec; + return Py_BuildValue("f", (float)boot_time); +} + + +PyObject * +psutil_users(PyObject *self, PyObject *args) { + struct utmpx *utx; + PyObject *py_username = NULL; + PyObject *py_tty = NULL; + PyObject *py_hostname = NULL; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + while ((utx = getutxent()) != NULL) { + if (utx->ut_type != USER_PROCESS) + continue; + py_username = PyUnicode_DecodeFSDefault(utx->ut_user); + if (! py_username) + goto error; + py_tty = PyUnicode_DecodeFSDefault(utx->ut_line); + if (! py_tty) + goto error; + py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); + if (! py_hostname) + goto error; + py_tuple = Py_BuildValue( + "(OOOdi)", + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)utx->ut_tv.tv_sec, // start time + utx->ut_pid // process id + ); + if (!py_tuple) { + endutxent(); + goto error; + } + if (PyList_Append(py_retlist, py_tuple)) { + endutxent(); + goto error; + } + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); + } + + endutxent(); + return py_retlist; + +error: + Py_XDECREF(py_username); + Py_XDECREF(py_tty); + Py_XDECREF(py_hostname); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + return NULL; +} diff --git a/psutil/arch/osx/sys.h b/psutil/arch/osx/sys.h new file mode 100644 index 0000000000..344ca21d42 --- /dev/null +++ b/psutil/arch/osx/sys.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index ed379f7ec6..c3153a1e3e 100755 --- a/setup.py +++ b/setup.py @@ -243,6 +243,11 @@ def get_winver(): 'psutil/_psutil_osx.c', 'psutil/arch/osx/process_info.c', 'psutil/arch/osx/cpu.c', + 'psutil/arch/osx/disk.c', + 'psutil/arch/osx/mem.c', + 'psutil/arch/osx/net.c', + 'psutil/arch/osx/sensors.c', + 'psutil/arch/osx/sys.c', ], define_macros=macros, extra_link_args=[ From d7552bd1438890bfe16dc6fdf8a62750e5072ebf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Apr 2023 19:02:32 -0700 Subject: [PATCH 0984/1714] OSX: rename psutil/_psutil_osx.c to arch/osx/proc.c to preserve GIT history Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 2 +- psutil/{_psutil_osx.c => arch/osx/proc.c} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename psutil/{_psutil_osx.c => arch/osx/proc.c} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 94d7708efd..d7e68f0421 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -34,7 +34,6 @@ include psutil/_psutil_bsd.c include psutil/_psutil_common.c include psutil/_psutil_common.h include psutil/_psutil_linux.c -include psutil/_psutil_osx.c include psutil/_psutil_posix.c include psutil/_psutil_posix.h include psutil/_psutil_sunos.c @@ -99,6 +98,7 @@ include psutil/arch/osx/mem.c include psutil/arch/osx/mem.h include psutil/arch/osx/net.c include psutil/arch/osx/net.h +include psutil/arch/osx/proc.c include psutil/arch/osx/process_info.c include psutil/arch/osx/process_info.h include psutil/arch/osx/sensors.c diff --git a/psutil/_psutil_osx.c b/psutil/arch/osx/proc.c similarity index 100% rename from psutil/_psutil_osx.c rename to psutil/arch/osx/proc.c From 6e23a5129375ae19abadc0de49f920899b6e765c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Apr 2023 20:37:32 -0700 Subject: [PATCH 0985/1714] OSX C refact: reconstruct _psutil_osx.c to preserve history Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 2 + psutil/_psosx.py | 2 - psutil/_psutil_osx.c | 142 +++++++++++++++++++++++++++++++ psutil/arch/osx/proc.c | 188 ++++++----------------------------------- psutil/arch/osx/proc.h | 19 +++++ setup.py | 5 +- 6 files changed, 191 insertions(+), 167 deletions(-) create mode 100644 psutil/_psutil_osx.c create mode 100644 psutil/arch/osx/proc.h diff --git a/MANIFEST.in b/MANIFEST.in index d7e68f0421..82d0e9a74c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -34,6 +34,7 @@ include psutil/_psutil_bsd.c include psutil/_psutil_common.c include psutil/_psutil_common.h include psutil/_psutil_linux.c +include psutil/_psutil_osx.c include psutil/_psutil_posix.c include psutil/_psutil_posix.h include psutil/_psutil_sunos.c @@ -99,6 +100,7 @@ include psutil/arch/osx/mem.h include psutil/arch/osx/net.c include psutil/arch/osx/net.h include psutil/arch/osx/proc.c +include psutil/arch/osx/proc.h include psutil/arch/osx/process_info.c include psutil/arch/osx/process_info.h include psutil/arch/osx/sensors.c diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 58359bc90c..893bdba189 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -351,8 +351,6 @@ def wrapper(self, *args, **kwargs): raise NoSuchProcess(self.pid, self._name) except PermissionError: raise AccessDenied(self.pid, self._name) - except cext.ZombieProcessError: - raise ZombieProcess(self.pid, self._name, self._ppid) return wrapper diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c new file mode 100644 index 0000000000..59fe5b6850 --- /dev/null +++ b/psutil/_psutil_osx.c @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2009, Jay Loden, 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 platform-specific module methods. + */ + +#include +#include +#include + +#include "_psutil_common.h" +#include "arch/osx/cpu.h" +#include "arch/osx/disk.h" +#include "arch/osx/mem.h" +#include "arch/osx/net.h" +#include "arch/osx/proc.h" +#include "arch/osx/process_info.h" +#include "arch/osx/sensors.h" +#include "arch/osx/sys.h" + + +static PyMethodDef mod_methods[] = { + // --- per-process functions + {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, + {"proc_connections", psutil_proc_connections, METH_VARARGS}, + {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, + {"proc_environ", psutil_proc_environ, METH_VARARGS}, + {"proc_exe", psutil_proc_exe, METH_VARARGS}, + {"proc_kinfo_oneshot", psutil_proc_kinfo_oneshot, METH_VARARGS}, + {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, + {"proc_name", psutil_proc_name, METH_VARARGS}, + {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, + {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, + {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS}, + {"proc_threads", psutil_proc_threads, METH_VARARGS}, + + // --- system-related functions + {"boot_time", psutil_boot_time, METH_VARARGS}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, + {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, + {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, + {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, + {"cpu_times", psutil_cpu_times, METH_VARARGS}, + {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, + {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, + {"disk_usage_used", psutil_disk_usage_used, METH_VARARGS}, + {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, + {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, + {"pids", psutil_pids, METH_VARARGS}, + {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, + {"swap_mem", psutil_swap_mem, METH_VARARGS}, + {"users", psutil_users, METH_VARARGS}, + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, + + // --- others + {"set_debug", psutil_set_debug, METH_VARARGS}, + + {NULL, NULL, 0, NULL} +}; + + +#if PY_MAJOR_VERSION >= 3 + #define INITERR return NULL + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_osx", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL + }; + + PyObject *PyInit__psutil_osx(void) +#else /* PY_MAJOR_VERSION */ + #define INITERR return + + void init_psutil_osx(void) +#endif /* PY_MAJOR_VERSION */ +{ +#if PY_MAJOR_VERSION >= 3 + PyObject *mod = PyModule_Create(&moduledef); +#else + PyObject *mod = Py_InitModule("_psutil_osx", mod_methods); +#endif + if (mod == NULL) + INITERR; + + if (psutil_setup() != 0) + INITERR; + + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + INITERR; + // process status constants, defined in: + // http://fxr.watson.org/fxr/source/bsd/sys/proc.h?v=xnu-792.6.70#L149 + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) + INITERR; + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) + INITERR; + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) + INITERR; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) + INITERR; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) + INITERR; + // connection status constants + if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) + INITERR; + if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) + INITERR; + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) + INITERR; + + if (mod == NULL) + INITERR; +#if PY_MAJOR_VERSION >= 3 + return mod; +#endif +} diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 30a5312e35..948d080bbf 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -2,10 +2,13 @@ * Copyright (c) 2009, Jay Loden, 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 platform-specific module methods. */ +// Process related functions. Original code was moved in here from +// psutil/_psutil_osx.c in 2023. For reference, here's the GIT blame +// history before the move: +// https://github.com/giampaolo/psutil/blame/59504a5/psutil/_psutil_osx.c + #include #include #include @@ -15,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -24,21 +28,13 @@ #include #include -#include "_psutil_common.h" -#include "_psutil_posix.h" -#include "arch/osx/cpu.h" -#include "arch/osx/disk.h" -#include "arch/osx/mem.h" -#include "arch/osx/net.h" -#include "arch/osx/process_info.h" -#include "arch/osx/sensors.h" -#include "arch/osx/sys.h" +#include "../../_psutil_common.h" +#include "../../_psutil_posix.h" +#include "process_info.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) -static PyObject *ZombieProcessError; - /* * A wrapper around task_for_pid() which sucks big time: @@ -48,7 +44,7 @@ static PyObject *ZombieProcessError; * - for PIDs != getpid() or PIDs which are not members of the procmod * it requires root * As such we can only guess what the heck went wrong and fail either - * with NoSuchProcess, ZombieProcessError or giveup with AccessDenied. + * with NoSuchProcess or giveup with AccessDenied. * Here's some history: * https://github.com/giampaolo/psutil/issues/1181 * https://github.com/giampaolo/psutil/issues/1209 @@ -64,9 +60,10 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) if (err != KERN_SUCCESS) { if (psutil_pid_exists(pid) == 0) NoSuchProcess("task_for_pid"); - else if (psutil_is_zombie(pid) == 1) - PyErr_SetString(ZombieProcessError, - "task_for_pid -> psutil_is_zombie -> 1"); + // Now done in Python. + // else if (psutil_is_zombie(pid) == 1) + // PyErr_SetString(ZombieProcessError, + // "task_for_pid -> psutil_is_zombie -> 1"); else { psutil_debug( "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " @@ -149,7 +146,7 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { /* * Return a Python list of all the PIDs running on the system. */ -static PyObject * +PyObject * psutil_pids(PyObject *self, PyObject *args) { kinfo_proc *proclist = NULL; kinfo_proc *orig_address = NULL; @@ -196,7 +193,7 @@ psutil_pids(PyObject *self, PyObject *args) { * This will also succeed for zombie processes returning correct * information. */ -static PyObject * +PyObject * psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { pid_t pid; struct kinfo_proc kp; @@ -247,7 +244,7 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { * EACCES for PIDs owned by another user and with ESRCH for zombie * processes. */ -static PyObject * +PyObject * psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { pid_t pid; struct proc_taskinfo pti; @@ -289,7 +286,7 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { /* * Return process name from kinfo_proc as a Python string. */ -static PyObject * +PyObject * psutil_proc_name(PyObject *self, PyObject *args) { pid_t pid; struct kinfo_proc kp; @@ -306,7 +303,7 @@ psutil_proc_name(PyObject *self, PyObject *args) { * Return process current working directory. * Raises NSP in case of zombie process. */ -static PyObject * +PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { pid_t pid; struct proc_vnodepathinfo pathinfo; @@ -327,7 +324,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { /* * Return path of the process executable. */ -static PyObject * +PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { pid_t pid; char buf[PATH_MAX]; @@ -393,7 +390,7 @@ psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ * nsMemoryReporterManager.cpp */ -static PyObject * +PyObject * psutil_proc_memory_uss(PyObject *self, PyObject *args) { pid_t pid; size_t len; @@ -473,7 +470,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { /* * Return process threads */ -static PyObject * +PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { pid_t pid; int err, ret; @@ -576,7 +573,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { * - lsof source code: http://goo.gl/SYW79 and http://goo.gl/m78fd * - /usr/include/sys/proc_info.h */ -static PyObject * +PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { pid_t pid; int num_fds; @@ -667,7 +664,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { * - lsof source code: http://goo.gl/SYW79 and http://goo.gl/wNrC0 * - /usr/include/sys/proc_info.h */ -static PyObject * +PyObject * psutil_proc_connections(PyObject *self, PyObject *args) { pid_t pid; int num_fds; @@ -863,7 +860,7 @@ psutil_proc_connections(PyObject *self, PyObject *args) { * Return number of file descriptors opened by process. * Raises NSP in case of zombie process. */ -static PyObject * +PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { pid_t pid; int num_fds; @@ -879,138 +876,3 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { free(fds_pointer); return Py_BuildValue("i", num_fds); } - - -/* - * define the psutil C module methods and initialize the module. - */ -static PyMethodDef mod_methods[] = { - // --- per-process functions - {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, - {"proc_connections", psutil_proc_connections, METH_VARARGS}, - {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, - {"proc_environ", psutil_proc_environ, METH_VARARGS}, - {"proc_exe", psutil_proc_exe, METH_VARARGS}, - {"proc_kinfo_oneshot", psutil_proc_kinfo_oneshot, METH_VARARGS}, - {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, - {"proc_name", psutil_proc_name, METH_VARARGS}, - {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, - {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, - {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS}, - {"proc_threads", psutil_proc_threads, METH_VARARGS}, - - // --- system-related functions - {"boot_time", psutil_boot_time, METH_VARARGS}, - {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, - {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, - {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, - {"cpu_stats", psutil_cpu_stats, METH_VARARGS}, - {"cpu_times", psutil_cpu_times, METH_VARARGS}, - {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, - {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, - {"disk_usage_used", psutil_disk_usage_used, METH_VARARGS}, - {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, - {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, - {"pids", psutil_pids, METH_VARARGS}, - {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, - {"swap_mem", psutil_swap_mem, METH_VARARGS}, - {"users", psutil_users, METH_VARARGS}, - {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, - - // --- others - {"set_debug", psutil_set_debug, METH_VARARGS}, - - {NULL, NULL, 0, NULL} -}; - - -#if PY_MAJOR_VERSION >= 3 - #define INITERR return NULL - - static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_psutil_osx", - NULL, - -1, - mod_methods, - NULL, - NULL, - NULL, - NULL - }; - - PyObject *PyInit__psutil_osx(void) -#else /* PY_MAJOR_VERSION */ - #define INITERR return - - void init_psutil_osx(void) -#endif /* PY_MAJOR_VERSION */ -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *mod = PyModule_Create(&moduledef); -#else - PyObject *mod = Py_InitModule("_psutil_osx", mod_methods); -#endif - if (mod == NULL) - INITERR; - - if (psutil_setup() != 0) - INITERR; - - if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) - INITERR; - // process status constants, defined in: - // http://fxr.watson.org/fxr/source/bsd/sys/proc.h?v=xnu-792.6.70#L149 - if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) - INITERR; - if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) - INITERR; - if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) - INITERR; - if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) - INITERR; - if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) - INITERR; - // connection status constants - if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) - INITERR; - if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) - INITERR; - if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) - INITERR; - - // Exception. - ZombieProcessError = PyErr_NewException( - "_psutil_osx.ZombieProcessError", NULL, NULL); - if (ZombieProcessError == NULL) - INITERR; - Py_INCREF(ZombieProcessError); - if (PyModule_AddObject(mod, "ZombieProcessError", ZombieProcessError)) { - Py_DECREF(ZombieProcessError); - INITERR; - } - - if (mod == NULL) - INITERR; -#if PY_MAJOR_VERSION >= 3 - return mod; -#endif -} diff --git a/psutil/arch/osx/proc.h b/psutil/arch/osx/proc.h new file mode 100644 index 0000000000..621f0cad5d --- /dev/null +++ b/psutil/arch/osx/proc.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include + +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_proc_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); +PyObject *psutil_proc_name(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index c3153a1e3e..91b1ab1446 100755 --- a/setup.py +++ b/setup.py @@ -241,11 +241,12 @@ def get_winver(): 'psutil._psutil_osx', sources=sources + [ 'psutil/_psutil_osx.c', - 'psutil/arch/osx/process_info.c', 'psutil/arch/osx/cpu.c', 'psutil/arch/osx/disk.c', 'psutil/arch/osx/mem.c', 'psutil/arch/osx/net.c', + 'psutil/arch/osx/proc.c', + 'psutil/arch/osx/process_info.c', 'psutil/arch/osx/sensors.c', 'psutil/arch/osx/sys.c', ], @@ -501,7 +502,7 @@ def main(): missdeps("sudo apk add gcc %s%s-dev" % (pyimpl, py3)) elif MACOS: print(hilite("XCode (https://developer.apple.com/xcode/) " - "is not installed"), color="red", file=sys.stderr) + "is not installed", color="red"), file=sys.stderr) elif FREEBSD: if which('pkg'): missdeps("pkg install gcc python%s" % py3) From 747965f6dbffe0a664a2ae7816f6d9b9a0f83fb5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 20 Apr 2023 20:19:12 +0200 Subject: [PATCH 0986/1714] OSX / refact: get rid of process_info.c (#2243) --- MANIFEST.in | 2 - docs/index.rst | 2 +- psutil/_psutil_osx.c | 1 - psutil/arch/osx/proc.c | 399 +++++++++++++++++++++++++++++++- psutil/arch/osx/proc.h | 2 + psutil/arch/osx/process_info.c | 408 --------------------------------- psutil/arch/osx/process_info.h | 18 -- setup.py | 1 - 8 files changed, 397 insertions(+), 436 deletions(-) delete mode 100644 psutil/arch/osx/process_info.c delete mode 100644 psutil/arch/osx/process_info.h diff --git a/MANIFEST.in b/MANIFEST.in index 82d0e9a74c..8defe71770 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -101,8 +101,6 @@ include psutil/arch/osx/net.c include psutil/arch/osx/net.h include psutil/arch/osx/proc.c include psutil/arch/osx/proc.h -include psutil/arch/osx/process_info.c -include psutil/arch/osx/process_info.h include psutil/arch/osx/sensors.c include psutil/arch/osx/sensors.h include psutil/arch/osx/sys.c diff --git a/docs/index.rst b/docs/index.rst index 344aecdf02..2bf050a11d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2602,7 +2602,7 @@ On Windows: :: set PSUTIL_DEBUG=1 python.exe script.py - psutil-debug [psutil/arch/windows/process_info.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) + psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) Security diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 59fe5b6850..dd7168eb7a 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -16,7 +16,6 @@ #include "arch/osx/mem.h" #include "arch/osx/net.h" #include "arch/osx/proc.h" -#include "arch/osx/process_info.h" #include "arch/osx/sensors.h" #include "arch/osx/sys.h" diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 948d080bbf..6f66c8613f 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -5,9 +5,10 @@ */ // Process related functions. Original code was moved in here from -// psutil/_psutil_osx.c in 2023. For reference, here's the GIT blame -// history before the move: +// psutil/_psutil_osx.c and psutil/arc/osx/process_info.c in 2023. +// For reference, here's the GIT blame history before the move: // https://github.com/giampaolo/psutil/blame/59504a5/psutil/_psutil_osx.c +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/arch/osx/process_info.c #include #include @@ -30,10 +31,196 @@ #include "../../_psutil_common.h" #include "../../_psutil_posix.h" -#include "process_info.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) +typedef struct kinfo_proc kinfo_proc; + + +// ==================================================================== +// --- utils +// ==================================================================== + +/* + * Returns a list of all BSD processes on the system. This routine + * allocates the list and puts it in *procList and a count of the + * number of entries in *procCount. You are responsible for freeing + * this list (use "free" from System framework). + * On success, the function returns 0. + * On error, the function returns a BSD errno value. + */ +static int +psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { + int mib[3]; + size_t size, size2; + void *ptr; + int err; + int lim = 8; // some limit + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_ALL; + *procCount = 0; + + /* + * We start by calling sysctl with ptr == NULL and size == 0. + * That will succeed, and set size to the appropriate length. + * We then allocate a buffer of at least that size and call + * sysctl with that buffer. If that succeeds, we're done. + * If that call fails with ENOMEM, we throw the buffer away + * and try again. + * Note that the loop calls sysctl with NULL again. This is + * is necessary because the ENOMEM failure case sets size to + * the amount of data returned, not the amount of data that + * could have been returned. + */ + while (lim-- > 0) { + size = 0; + if (sysctl((int *)mib, 3, NULL, &size, NULL, 0) == -1) { + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); + return 1; + } + size2 = size + (size >> 3); // add some + if (size2 > size) { + ptr = malloc(size2); + if (ptr == NULL) + ptr = malloc(size); + else + size = size2; + } + else { + ptr = malloc(size); + } + if (ptr == NULL) { + PyErr_NoMemory(); + return 1; + } + + if (sysctl((int *)mib, 3, ptr, &size, NULL, 0) == -1) { + err = errno; + free(ptr); + if (err != ENOMEM) { + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); + return 1; + } + } + else { + *procList = (kinfo_proc *)ptr; + *procCount = size / sizeof(kinfo_proc); + if (procCount <= 0) { + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + return 1; + } + return 0; // success + } + } + + PyErr_Format(PyExc_RuntimeError, "couldn't collect PIDs list"); + return 1; +} + + +// Read the maximum argument size for processes +static int +psutil_sysctl_argmax() { + int argmax; + int mib[2]; + size_t size = sizeof(argmax); + + mib[0] = CTL_KERN; + mib[1] = KERN_ARGMAX; + + if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) + return argmax; + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); + return 0; +} + + +// Read process argument space. +static int +psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { + int mib[3]; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROCARGS2; + mib[2] = pid; + + if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { + if (psutil_pid_exists(pid) == 0) { + NoSuchProcess("psutil_pid_exists -> 0"); + return 1; + } + // In case of zombie process we'll get EINVAL. We translate it + // to NSP and _psosx.py will translate it to ZP. + if (errno == EINVAL) { + psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to NSP"); + NoSuchProcess("sysctl(KERN_PROCARGS2) -> EINVAL"); + return 1; + } + // There's nothing we can do other than raising AD. + if (errno == EIO) { + psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); + AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); + return 1; + } + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + return 1; + } + return 0; +} + + +static int +psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { + int mib[4]; + size_t len; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = pid; + + // fetch the info with sysctl() + len = sizeof(struct kinfo_proc); + + // now read the data from sysctl + if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { + // raise an exception and throw errno as the error + PyErr_SetFromOSErrnoWithSyscall("sysctl"); + return -1; + } + + // sysctl succeeds but len is zero, happens when process has gone away + if (len == 0) { + NoSuchProcess("sysctl(kinfo_proc), len == 0"); + return -1; + } + return 0; +} + + +/* + * A wrapper around proc_pidinfo(). + * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c + * Returns 0 on failure. + */ +static int +psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { + errno = 0; + int ret; + + ret = proc_pidinfo(pid, flavor, arg, pti, size); + if (ret <= 0) { + psutil_raise_for_pid(pid, "proc_pidinfo()"); + return 0; + } + if ((unsigned long)ret < sizeof(pti)) { + psutil_raise_for_pid( + pid, "proc_pidinfo() return size < sizeof(struct_pointer)"); + return 0; + } + return ret; +} /* @@ -50,7 +237,7 @@ * https://github.com/giampaolo/psutil/issues/1209 * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 */ -int +static int psutil_task_for_pid(pid_t pid, mach_port_t *task) { // See: https://github.com/giampaolo/psutil/issues/1181 @@ -68,7 +255,7 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) psutil_debug( "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " "setting AccessDenied()", - pid, err, errno, mach_error_string(err)); + (long)pid, err, errno, mach_error_string(err)); AccessDenied("task_for_pid"); } return 1; @@ -143,6 +330,11 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { } +// ==================================================================== +// --- Python APIs +// ==================================================================== + + /* * Return a Python list of all the PIDs running on the system. */ @@ -876,3 +1068,200 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { free(fds_pointer); return Py_BuildValue("i", num_fds); } + + +// return process args as a python list +PyObject * +psutil_proc_cmdline(PyObject *self, PyObject *args) { + pid_t pid; + int nargs; + size_t len; + char *procargs = NULL; + char *arg_ptr; + char *arg_end; + char *curr_arg; + size_t argmax; + PyObject *py_retlist = PyList_New(0); + PyObject *py_arg = NULL; + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + + // special case for PID 0 (kernel_task) where cmdline cannot be fetched + if (pid == 0) + return py_retlist; + + // read argmax and allocate memory for argument space. + argmax = psutil_sysctl_argmax(); + if (! argmax) + goto error; + + procargs = (char *)malloc(argmax); + if (NULL == procargs) { + PyErr_NoMemory(); + goto error; + } + + if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) + goto error; + + arg_end = &procargs[argmax]; + // copy the number of arguments to nargs + memcpy(&nargs, procargs, sizeof(nargs)); + + arg_ptr = procargs + sizeof(nargs); + len = strlen(arg_ptr); + arg_ptr += len + 1; + + if (arg_ptr == arg_end) { + free(procargs); + return py_retlist; + } + + // skip ahead to the first argument + for (; arg_ptr < arg_end; arg_ptr++) { + if (*arg_ptr != '\0') + break; + } + + // iterate through arguments + curr_arg = arg_ptr; + while (arg_ptr < arg_end && nargs > 0) { + if (*arg_ptr++ == '\0') { + py_arg = PyUnicode_DecodeFSDefault(curr_arg); + if (! py_arg) + goto error; + if (PyList_Append(py_retlist, py_arg)) + goto error; + Py_DECREF(py_arg); + // iterate to next arg and decrement # of args + curr_arg = arg_ptr; + nargs--; + } + } + + free(procargs); + return py_retlist; + +error: + Py_XDECREF(py_arg); + Py_XDECREF(py_retlist); + if (procargs != NULL) + free(procargs); + return NULL; +} + + +// Return process environment as a python string. +// On Big Sur this function returns an empty string unless: +// * kernel is DEVELOPMENT || DEBUG +// * target process is same as current_proc() +// * target process is not cs_restricted +// * SIP is off +// * caller has an entitlement +// See: https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/kern/kern_sysctl.c#L1315-L1321 +PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + pid_t pid; + int nargs; + char *procargs = NULL; + char *procenv = NULL; + char *arg_ptr; + char *arg_end; + char *env_start; + size_t argmax; + PyObject *py_ret = NULL; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + // special case for PID 0 (kernel_task) where cmdline cannot be fetched + if (pid == 0) + goto empty; + + // read argmax and allocate memory for argument space. + argmax = psutil_sysctl_argmax(); + if (! argmax) + goto error; + + procargs = (char *)malloc(argmax); + if (NULL == procargs) { + PyErr_NoMemory(); + goto error; + } + + if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) + goto error; + + arg_end = &procargs[argmax]; + // copy the number of arguments to nargs + memcpy(&nargs, procargs, sizeof(nargs)); + + // skip executable path + arg_ptr = procargs + sizeof(nargs); + arg_ptr = memchr(arg_ptr, '\0', arg_end - arg_ptr); + + if (arg_ptr == NULL || arg_ptr == arg_end) { + psutil_debug( + "(arg_ptr == NULL || arg_ptr == arg_end); set environ to empty"); + goto empty; + } + + // skip ahead to the first argument + for (; arg_ptr < arg_end; arg_ptr++) { + if (*arg_ptr != '\0') + break; + } + + // iterate through arguments + while (arg_ptr < arg_end && nargs > 0) { + if (*arg_ptr++ == '\0') + nargs--; + } + + // build an environment variable block + env_start = arg_ptr; + + procenv = calloc(1, arg_end - arg_ptr); + if (procenv == NULL) { + PyErr_NoMemory(); + goto error; + } + + while (*arg_ptr != '\0' && arg_ptr < arg_end) { + char *s = memchr(arg_ptr + 1, '\0', arg_end - arg_ptr); + if (s == NULL) + break; + memcpy(procenv + (arg_ptr - env_start), arg_ptr, s - arg_ptr); + arg_ptr = s + 1; + } + + py_ret = PyUnicode_DecodeFSDefaultAndSize( + procenv, arg_ptr - env_start + 1); + if (!py_ret) { + // XXX: don't want to free() this as per: + // https://github.com/giampaolo/psutil/issues/926 + // It sucks but not sure what else to do. + procargs = NULL; + goto error; + } + + free(procargs); + free(procenv); + return py_ret; + +empty: + if (procargs != NULL) + free(procargs); + return Py_BuildValue("s", ""); + +error: + Py_XDECREF(py_ret); + if (procargs != NULL) + free(procargs); + if (procenv != NULL) + free(procargs); + return NULL; +} diff --git a/psutil/arch/osx/proc.h b/psutil/arch/osx/proc.h index 621f0cad5d..63f16ccdd2 100644 --- a/psutil/arch/osx/proc.h +++ b/psutil/arch/osx/proc.h @@ -7,8 +7,10 @@ #include PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_connections(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_exe(PyObject *self, PyObject *args); PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c deleted file mode 100644 index 4b98d92a60..0000000000 --- a/psutil/arch/osx/process_info.c +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - * - * Helper functions related to fetching process information. - * Used by _psutil_osx module methods. - */ - - -#include -#include -#include -#include - -#include "../../_psutil_common.h" -#include "../../_psutil_posix.h" -#include "process_info.h" - - -/* - * Returns a list of all BSD processes on the system. This routine - * allocates the list and puts it in *procList and a count of the - * number of entries in *procCount. You are responsible for freeing - * this list (use "free" from System framework). - * On success, the function returns 0. - * On error, the function returns a BSD errno value. - */ -int -psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { - int mib[3]; - size_t size, size2; - void *ptr; - int err; - int lim = 8; // some limit - - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_ALL; - *procCount = 0; - - /* - * We start by calling sysctl with ptr == NULL and size == 0. - * That will succeed, and set size to the appropriate length. - * We then allocate a buffer of at least that size and call - * sysctl with that buffer. If that succeeds, we're done. - * If that call fails with ENOMEM, we throw the buffer away - * and try again. - * Note that the loop calls sysctl with NULL again. This is - * is necessary because the ENOMEM failure case sets size to - * the amount of data returned, not the amount of data that - * could have been returned. - */ - while (lim-- > 0) { - size = 0; - if (sysctl((int *)mib, 3, NULL, &size, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); - return 1; - } - size2 = size + (size >> 3); // add some - if (size2 > size) { - ptr = malloc(size2); - if (ptr == NULL) - ptr = malloc(size); - else - size = size2; - } - else { - ptr = malloc(size); - } - if (ptr == NULL) { - PyErr_NoMemory(); - return 1; - } - - if (sysctl((int *)mib, 3, ptr, &size, NULL, 0) == -1) { - err = errno; - free(ptr); - if (err != ENOMEM) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); - return 1; - } - } - else { - *procList = (kinfo_proc *)ptr; - *procCount = size / sizeof(kinfo_proc); - if (procCount <= 0) { - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); - return 1; - } - return 0; // success - } - } - - PyErr_Format(PyExc_RuntimeError, "couldn't collect PIDs list"); - return 1; -} - - -// Read the maximum argument size for processes -static int -psutil_sysctl_argmax() { - int argmax; - int mib[2]; - size_t size = sizeof(argmax); - - mib[0] = CTL_KERN; - mib[1] = KERN_ARGMAX; - - if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) - return argmax; - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); - return 0; -} - - -// Read process argument space. -static int -psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { - int mib[3]; - - mib[0] = CTL_KERN; - mib[1] = KERN_PROCARGS2; - mib[2] = pid; - - if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { - if (psutil_pid_exists(pid) == 0) { - NoSuchProcess("psutil_pid_exists -> 0"); - return 1; - } - // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psosx.py will translate it to ZP. - if (errno == EINVAL) { - psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to NSP"); - NoSuchProcess("sysctl(KERN_PROCARGS2) -> EINVAL"); - return 1; - } - // There's nothing we can do other than raising AD. - if (errno == EIO) { - psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); - AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); - return 1; - } - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); - return 1; - } - return 0; -} - - -// Return 1 if pid refers to a zombie process else 0. -int -psutil_is_zombie(pid_t pid) { - struct kinfo_proc kp; - - if (psutil_get_kinfo_proc(pid, &kp) == -1) - return 0; - return (kp.kp_proc.p_stat == SZOMB) ? 1 : 0; -} - - -// return process args as a python list -PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args) { - pid_t pid; - int nargs; - size_t len; - char *procargs = NULL; - char *arg_ptr; - char *arg_end; - char *curr_arg; - size_t argmax; - PyObject *py_retlist = PyList_New(0); - PyObject *py_arg = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - goto error; - - // special case for PID 0 (kernel_task) where cmdline cannot be fetched - if (pid == 0) - return py_retlist; - - // read argmax and allocate memory for argument space. - argmax = psutil_sysctl_argmax(); - if (! argmax) - goto error; - - procargs = (char *)malloc(argmax); - if (NULL == procargs) { - PyErr_NoMemory(); - goto error; - } - - if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) - goto error; - - arg_end = &procargs[argmax]; - // copy the number of arguments to nargs - memcpy(&nargs, procargs, sizeof(nargs)); - - arg_ptr = procargs + sizeof(nargs); - len = strlen(arg_ptr); - arg_ptr += len + 1; - - if (arg_ptr == arg_end) { - free(procargs); - return py_retlist; - } - - // skip ahead to the first argument - for (; arg_ptr < arg_end; arg_ptr++) { - if (*arg_ptr != '\0') - break; - } - - // iterate through arguments - curr_arg = arg_ptr; - while (arg_ptr < arg_end && nargs > 0) { - if (*arg_ptr++ == '\0') { - py_arg = PyUnicode_DecodeFSDefault(curr_arg); - if (! py_arg) - goto error; - if (PyList_Append(py_retlist, py_arg)) - goto error; - Py_DECREF(py_arg); - // iterate to next arg and decrement # of args - curr_arg = arg_ptr; - nargs--; - } - } - - free(procargs); - return py_retlist; - -error: - Py_XDECREF(py_arg); - Py_XDECREF(py_retlist); - if (procargs != NULL) - free(procargs); - return NULL; -} - - -// Return process environment as a python string. -// On Big Sur this function returns an empty string unless: -// * kernel is DEVELOPMENT || DEBUG -// * target process is same as current_proc() -// * target process is not cs_restricted -// * SIP is off -// * caller has an entitlement -// See: https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/kern/kern_sysctl.c#L1315-L1321 -PyObject * -psutil_proc_environ(PyObject *self, PyObject *args) { - pid_t pid; - int nargs; - char *procargs = NULL; - char *procenv = NULL; - char *arg_ptr; - char *arg_end; - char *env_start; - size_t argmax; - PyObject *py_ret = NULL; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - - // special case for PID 0 (kernel_task) where cmdline cannot be fetched - if (pid == 0) - goto empty; - - // read argmax and allocate memory for argument space. - argmax = psutil_sysctl_argmax(); - if (! argmax) - goto error; - - procargs = (char *)malloc(argmax); - if (NULL == procargs) { - PyErr_NoMemory(); - goto error; - } - - if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) - goto error; - - arg_end = &procargs[argmax]; - // copy the number of arguments to nargs - memcpy(&nargs, procargs, sizeof(nargs)); - - // skip executable path - arg_ptr = procargs + sizeof(nargs); - arg_ptr = memchr(arg_ptr, '\0', arg_end - arg_ptr); - - if (arg_ptr == NULL || arg_ptr == arg_end) { - psutil_debug( - "(arg_ptr == NULL || arg_ptr == arg_end); set environ to empty"); - goto empty; - } - - // skip ahead to the first argument - for (; arg_ptr < arg_end; arg_ptr++) { - if (*arg_ptr != '\0') - break; - } - - // iterate through arguments - while (arg_ptr < arg_end && nargs > 0) { - if (*arg_ptr++ == '\0') - nargs--; - } - - // build an environment variable block - env_start = arg_ptr; - - procenv = calloc(1, arg_end - arg_ptr); - if (procenv == NULL) { - PyErr_NoMemory(); - goto error; - } - - while (*arg_ptr != '\0' && arg_ptr < arg_end) { - char *s = memchr(arg_ptr + 1, '\0', arg_end - arg_ptr); - if (s == NULL) - break; - memcpy(procenv + (arg_ptr - env_start), arg_ptr, s - arg_ptr); - arg_ptr = s + 1; - } - - py_ret = PyUnicode_DecodeFSDefaultAndSize( - procenv, arg_ptr - env_start + 1); - if (!py_ret) { - // XXX: don't want to free() this as per: - // https://github.com/giampaolo/psutil/issues/926 - // It sucks but not sure what else to do. - procargs = NULL; - goto error; - } - - free(procargs); - free(procenv); - return py_ret; - -empty: - if (procargs != NULL) - free(procargs); - return Py_BuildValue("s", ""); - -error: - Py_XDECREF(py_ret); - if (procargs != NULL) - free(procargs); - if (procenv != NULL) - free(procargs); - return NULL; -} - - -int -psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { - int mib[4]; - size_t len; - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PID; - mib[3] = pid; - - // fetch the info with sysctl() - len = sizeof(struct kinfo_proc); - - // now read the data from sysctl - if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { - // raise an exception and throw errno as the error - PyErr_SetFromOSErrnoWithSyscall("sysctl"); - return -1; - } - - // sysctl succeeds but len is zero, happens when process has gone away - if (len == 0) { - NoSuchProcess("sysctl(kinfo_proc), len == 0"); - return -1; - } - return 0; -} - - -/* - * A wrapper around proc_pidinfo(). - * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c - * Returns 0 on failure. - */ -int -psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { - errno = 0; - int ret; - - ret = proc_pidinfo(pid, flavor, arg, pti, size); - if (ret <= 0) { - psutil_raise_for_pid(pid, "proc_pidinfo()"); - return 0; - } - if ((unsigned long)ret < sizeof(pti)) { - psutil_raise_for_pid( - pid, "proc_pidinfo() return size < sizeof(struct_pointer)"); - return 0; - } - return ret; -} diff --git a/psutil/arch/osx/process_info.h b/psutil/arch/osx/process_info.h deleted file mode 100644 index 08046bcb6f..0000000000 --- a/psutil/arch/osx/process_info.h +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -typedef struct kinfo_proc kinfo_proc; - -int psutil_is_zombie(pid_t pid); -int psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp); -int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); -int psutil_proc_pidinfo( - pid_t pid, int flavor, uint64_t arg, void *pti, int size); - -PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_environ(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index 91b1ab1446..54652aae28 100755 --- a/setup.py +++ b/setup.py @@ -246,7 +246,6 @@ def get_winver(): 'psutil/arch/osx/mem.c', 'psutil/arch/osx/net.c', 'psutil/arch/osx/proc.c', - 'psutil/arch/osx/process_info.c', 'psutil/arch/osx/sensors.c', 'psutil/arch/osx/sys.c', ], From 995fa82a5f0b5013ca5535a9f1669c96d07ef4ca Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 20 Apr 2023 21:00:25 +0200 Subject: [PATCH 0987/1714] use glob.glob() in setup.py Signed-off-by: Giampaolo Rodola --- setup.py | 97 +++++++++++++++++--------------------------------------- 1 file changed, 29 insertions(+), 68 deletions(-) diff --git a/setup.py b/setup.py index 54652aae28..b38c6a6ab7 100755 --- a/setup.py +++ b/setup.py @@ -9,6 +9,7 @@ from __future__ import print_function import contextlib +import glob import io import os import platform @@ -208,23 +209,11 @@ def get_winver(): ext = Extension( 'psutil._psutil_windows', - sources=sources + [ - 'psutil/_psutil_windows.c', - 'psutil/arch/windows/cpu.c', - 'psutil/arch/windows/disk.c', - 'psutil/arch/windows/mem.c', - 'psutil/arch/windows/net.c', - 'psutil/arch/windows/proc.c', - 'psutil/arch/windows/proc_handles.c', - 'psutil/arch/windows/proc_info.c', - 'psutil/arch/windows/proc_utils.c', - 'psutil/arch/windows/security.c', - 'psutil/arch/windows/sensors.c', - 'psutil/arch/windows/services.c', - 'psutil/arch/windows/socks.c', - 'psutil/arch/windows/sys.c', - 'psutil/arch/windows/wmi.c', - ], + sources=( + sources + + ["psutil/_psutil_windows.c"] + + glob.glob("psutil/arch/windows/*.c") + ), define_macros=macros, libraries=[ "psapi", "kernel32", "advapi32", "shell32", "netapi32", @@ -239,16 +228,11 @@ def get_winver(): macros.append(("PSUTIL_OSX", 1)) ext = Extension( 'psutil._psutil_osx', - sources=sources + [ - 'psutil/_psutil_osx.c', - 'psutil/arch/osx/cpu.c', - 'psutil/arch/osx/disk.c', - 'psutil/arch/osx/mem.c', - 'psutil/arch/osx/net.c', - 'psutil/arch/osx/proc.c', - 'psutil/arch/osx/sensors.c', - 'psutil/arch/osx/sys.c', - ], + sources=( + sources + + ["psutil/_psutil_osx.c"] + + glob.glob("psutil/arch/osx/*.c") + ), define_macros=macros, extra_link_args=[ '-framework', 'CoreFoundation', '-framework', 'IOKit' @@ -259,21 +243,12 @@ def get_winver(): macros.append(("PSUTIL_FREEBSD", 1)) ext = Extension( 'psutil._psutil_bsd', - sources=sources + [ - 'psutil/_psutil_bsd.c', - 'psutil/arch/bsd/cpu.c', - 'psutil/arch/bsd/disk.c', - 'psutil/arch/bsd/net.c', - 'psutil/arch/bsd/proc.c', - 'psutil/arch/bsd/sys.c', - 'psutil/arch/freebsd/cpu.c', - 'psutil/arch/freebsd/disk.c', - 'psutil/arch/freebsd/mem.c', - 'psutil/arch/freebsd/proc.c', - 'psutil/arch/freebsd/proc_socks.c', - 'psutil/arch/freebsd/sensors.c', - 'psutil/arch/freebsd/sys_socks.c', - ], + sources=( + sources + + ["psutil/_psutil_bsd.c"] + + glob.glob("psutil/arch/bsd/*.c") + + glob.glob("psutil/arch/freebsd/*.c") + ), define_macros=macros, libraries=["devstat"], **py_limited_api) @@ -282,19 +257,12 @@ def get_winver(): macros.append(("PSUTIL_OPENBSD", 1)) ext = Extension( 'psutil._psutil_bsd', - sources=sources + [ - 'psutil/_psutil_bsd.c', - 'psutil/arch/bsd/cpu.c', - 'psutil/arch/bsd/disk.c', - 'psutil/arch/bsd/net.c', - 'psutil/arch/bsd/proc.c', - 'psutil/arch/bsd/sys.c', - 'psutil/arch/openbsd/cpu.c', - 'psutil/arch/openbsd/disk.c', - 'psutil/arch/openbsd/mem.c', - 'psutil/arch/openbsd/proc.c', - 'psutil/arch/openbsd/socks.c', - ], + sources=( + sources + + ["psutil/_psutil_bsd.c"] + + glob.glob("psutil/arch/bsd/*.c") + + glob.glob("psutil/arch/openbsd/*.c") + ), define_macros=macros, libraries=["kvm"], **py_limited_api) @@ -303,19 +271,12 @@ def get_winver(): macros.append(("PSUTIL_NETBSD", 1)) ext = Extension( 'psutil._psutil_bsd', - sources=sources + [ - 'psutil/_psutil_bsd.c', - 'psutil/arch/bsd/cpu.c', - 'psutil/arch/bsd/disk.c', - 'psutil/arch/bsd/net.c', - 'psutil/arch/bsd/proc.c', - 'psutil/arch/bsd/sys.c', - 'psutil/arch/netbsd/cpu.c', - 'psutil/arch/netbsd/disk.c', - 'psutil/arch/netbsd/mem.c', - 'psutil/arch/netbsd/proc.c', - 'psutil/arch/netbsd/socks.c', - ], + sources=( + sources + + ["psutil/_psutil_bsd.c"] + + glob.glob("psutil/arch/bsd/*.c") + + glob.glob("psutil/arch/netbsd/*.c") + ), define_macros=macros, libraries=["kvm"], **py_limited_api) From 12fe73d2ed9c10ee5fa9b2e9fe00a2a27fe77e22 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 22 Apr 2023 20:00:03 +0000 Subject: [PATCH 0988/1714] SunOS: fix some C compilation warnings Signed-off-by: Giampaolo Rodola --- Makefile | 2 +- psutil/_psutil_sunos.c | 4 ++-- psutil/arch/solaris/environ.c | 8 ++++---- psutil/tests/test_posix.py | 11 +++++++---- psutil/tests/test_sunos.py | 7 +++---- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index ba62aa983d..a82940d9de 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ INSTALL_OPTS = `$(PYTHON) -c \ TEST_PREFIX = PSUTIL_SCRIPTS_DIR=`pwd`/scripts PYTHONWARNINGS=always PSUTIL_DEBUG=1 # if make is invoked with no arg, default to `make help` -.DEFAULT_GOAL := help +# .DEFAULT_GOAL := help # =================================================================== # Install diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index e2e7c018ed..5422005fef 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -129,7 +129,7 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { */ static int cstrings_array_to_string(char **joined, char ** array, size_t count, char dm) { - int i; + size_t i; size_t total_length = 0; size_t item_length = 0; char *result = NULL; @@ -353,7 +353,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { */ static PyObject * psutil_proc_cpu_num(PyObject *self, PyObject *args) { - int fd = NULL; + int fd = -1; int pid; char path[1000]; struct prheader header; diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/solaris/environ.c index 58afd63acc..dd627eb004 100644 --- a/psutil/arch/solaris/environ.c +++ b/psutil/arch/solaris/environ.c @@ -55,7 +55,7 @@ open_address_space(pid_t pid, const char *procfs_path) { * @return amount of bytes stored to the buffer or -1 in case of * error. */ -static int +static size_t read_offt(int fd, off_t offset, char *buf, size_t buf_size) { size_t to_read = buf_size; size_t stored = 0; @@ -161,7 +161,7 @@ read_cstrings_block(int fd, off_t offset, size_t ptr_size, size_t count) { char **result = NULL; char *pblock = NULL; size_t pblock_size; - int i; + size_t i; assert(ptr_size == 4 || ptr_size == 8); @@ -247,7 +247,7 @@ ptr_size_by_psinfo(psinfo_t info) { static int search_pointers_vector_size_offt(int fd, off_t offt, size_t ptr_size) { int count = 0; - int r; + size_t r; char buf[8]; static const char zeros[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; @@ -391,7 +391,7 @@ psutil_read_raw_env(psinfo_t info, const char *procfs_path, ssize_t *count) { */ void psutil_free_cstrings_array(char **array, size_t count) { - int i; + size_t i; if (!array) return; diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 9ce82cae51..c789ee875e 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -62,10 +62,7 @@ def ps(fmt, pid=None): cmd.append('ax') if SUNOS: - # XXX: set() has not get() method so this cannot work; not sure - # what I meant in here. - fmt_map = set(('command', 'comm', 'start', 'stime')) - fmt = fmt_map.get(fmt, fmt) + fmt = fmt.replace("start", "stime") cmd.extend(['-o', fmt]) @@ -373,6 +370,12 @@ def test_users_started(self): 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 ValueError( diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index dd74a49b0a..274584d652 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -25,10 +25,9 @@ def test_swap_memory(self): 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) + fields = line.split() + total = int(fields[3]) * 512 + free = int(fields[4]) * 512 used = total - free psutil_swap = psutil.swap_memory() From 7282a9202bf101b13991e2163c8cabdae3ba62ba Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Mon, 24 Apr 2023 12:22:58 +0200 Subject: [PATCH 0989/1714] Run `abi3audit` on produced abi3 wheels (#2247) Signed-off-by: mayeut --- .github/workflows/build.yml | 1 + setup.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d735a0ceef..6c3ab484d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,3 +135,4 @@ jobs: - run: | python scripts/internal/print_hashes.py wheelhouse/ pipx run twine check --strict wheelhouse/* + pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl diff --git a/setup.py b/setup.py index b38c6a6ab7..2b8b2abb22 100755 --- a/setup.py +++ b/setup.py @@ -61,7 +61,9 @@ PYPY = '__pypy__' in sys.builtin_module_names PY36_PLUS = sys.version_info[:2] >= (3, 6) +PY37_PLUS = sys.version_info[:2] >= (3, 7) CP36_PLUS = PY36_PLUS and sys.implementation.name == "cpython" +CP37_PLUS = PY37_PLUS and sys.implementation.name == "cpython" macros = [] if POSIX: @@ -112,9 +114,14 @@ def get_version(): # Py_LIMITED_API lets us create a single wheel which works with multiple # python versions, including unreleased ones. -if bdist_wheel and CP36_PLUS and (MACOS or LINUX or WINDOWS): +if bdist_wheel and CP36_PLUS and (MACOS or LINUX): py_limited_api = {"py_limited_api": True} macros.append(('Py_LIMITED_API', '0x03060000')) +elif bdist_wheel and CP37_PLUS and WINDOWS: + # PyErr_SetFromWindowsErr / PyErr_SetFromWindowsErrWithFilename are + # part of the stable API/ABI starting with CPython 3.7 + py_limited_api = {"py_limited_api": True} + macros.append(('Py_LIMITED_API', '0x03070000')) else: py_limited_api = {} From 29bd071a8180ad89f44988712d3ee68ce1c96faf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 29 Apr 2023 01:12:33 +0200 Subject: [PATCH 0990/1714] Makefile: expand variables Using $(shell ...) instead of `...` expands all variables. Signed-off-by: Giampaolo Rodola --- Makefile | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index a82940d9de..7749d4a033 100644 --- a/Makefile +++ b/Makefile @@ -35,22 +35,23 @@ PY2_DEPS = \ futures \ ipaddress \ mock -PY_DEPS = `$(PYTHON) -c \ - "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')"` -NUM_WORKERS = `$(PYTHON) -c "import os; print(os.cpu_count() or 1)"` +PY_DEPS = $(shell $(PYTHON) -c \ + "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')") +NUM_WORKERS = $(shell $(PYTHON) -c \ + "import os; print(os.cpu_count() or 1)") # "python3 setup.py build" can be parallelized on Python >= 3.6. -BUILD_OPTS = `$(PYTHON) -c \ +BUILD_OPTS = $(shell $(PYTHON) -c \ "import sys, os; \ py36 = sys.version_info[:2] >= (3, 6); \ cpus = os.cpu_count() or 1 if py36 else 1; \ - print('--parallel %s' % cpus if cpus > 1 else '')"` + print('--parallel %s' % cpus if cpus > 1 else '')") # In not in a virtualenv, add --user options for install commands. -INSTALL_OPTS = `$(PYTHON) -c \ - "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` -TEST_PREFIX = PSUTIL_SCRIPTS_DIR=`pwd`/scripts PYTHONWARNINGS=always PSUTIL_DEBUG=1 +INSTALL_OPTS = $(shell $(PYTHON) -c \ + "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')") +TEST_PREFIX = PSUTIL_SCRIPTS_DIR=$(shell pwd)/scripts PYTHONWARNINGS=always PSUTIL_DEBUG=1 # if make is invoked with no arg, default to `make help` -# .DEFAULT_GOAL := help +.DEFAULT_GOAL := help # =================================================================== # Install @@ -169,7 +170,7 @@ test-posix: ## POSIX specific tests. test-platform: ## Run specific platform tests only. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_$(shell $(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])').py test-memleaks: ## Memory leak tests. ${MAKE} build @@ -253,7 +254,7 @@ print-dist: ## Print downloaded wheels / tar.gs # =================================================================== git-tag-release: ## Git-tag a new release. - git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` + git tag -a release-$(shell python3 -c "import setup; print(setup.get_version())") -m $(shell git rev-list HEAD --count):$(shell git rev-parse --short HEAD) git push --follow-tags sdist: ## Create tar.gz source distribution. From d6d368037d5eb463c47d6c7232d37229975e61b1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 29 Apr 2023 09:52:41 +0200 Subject: [PATCH 0991/1714] revert previous makefile commit as it requires bash --- Makefile | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 7749d4a033..ba62aa983d 100644 --- a/Makefile +++ b/Makefile @@ -35,20 +35,19 @@ PY2_DEPS = \ futures \ ipaddress \ mock -PY_DEPS = $(shell $(PYTHON) -c \ - "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')") -NUM_WORKERS = $(shell $(PYTHON) -c \ - "import os; print(os.cpu_count() or 1)") +PY_DEPS = `$(PYTHON) -c \ + "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')"` +NUM_WORKERS = `$(PYTHON) -c "import os; print(os.cpu_count() or 1)"` # "python3 setup.py build" can be parallelized on Python >= 3.6. -BUILD_OPTS = $(shell $(PYTHON) -c \ +BUILD_OPTS = `$(PYTHON) -c \ "import sys, os; \ py36 = sys.version_info[:2] >= (3, 6); \ cpus = os.cpu_count() or 1 if py36 else 1; \ - print('--parallel %s' % cpus if cpus > 1 else '')") + print('--parallel %s' % cpus if cpus > 1 else '')"` # In not in a virtualenv, add --user options for install commands. -INSTALL_OPTS = $(shell $(PYTHON) -c \ - "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')") -TEST_PREFIX = PSUTIL_SCRIPTS_DIR=$(shell pwd)/scripts PYTHONWARNINGS=always PSUTIL_DEBUG=1 +INSTALL_OPTS = `$(PYTHON) -c \ + "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` +TEST_PREFIX = PSUTIL_SCRIPTS_DIR=`pwd`/scripts PYTHONWARNINGS=always PSUTIL_DEBUG=1 # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help @@ -170,7 +169,7 @@ test-posix: ## POSIX specific tests. test-platform: ## Run specific platform tests only. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_$(shell $(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])').py + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} build @@ -254,7 +253,7 @@ print-dist: ## Print downloaded wheels / tar.gs # =================================================================== git-tag-release: ## Git-tag a new release. - git tag -a release-$(shell python3 -c "import setup; print(setup.get_version())") -m $(shell git rev-list HEAD --count):$(shell git rev-parse --short HEAD) + git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` git push --follow-tags sdist: ## Create tar.gz source distribution. From 4d5b9e89b5f258b71d8caab0d720e8762a31eb92 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 29 Apr 2023 11:49:44 +0200 Subject: [PATCH 0992/1714] makefile: integrate abi3audit CLI tool + refactoring --- Makefile | 68 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index ba62aa983d..376f01895c 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,11 @@ PY2_DEPS = \ ipaddress \ mock PY_DEPS = `$(PYTHON) -c \ - "import sys; print('$(PY3_DEPS)' if sys.version_info[0] == 3 else '$(PY2_DEPS)')"` + "import sys; \ + py3 = sys.version_info[0] == 3; \ + py38 = sys.version_info[:2] >= (3, 8); \ + py3_extra = ' abi3audit' if py38 else ''; \ + print('$(PY3_DEPS)' + py3_extra if py3 else '$(PY2_DEPS)')"` NUM_WORKERS = `$(PYTHON) -c "import os; print(os.cpu_count() or 1)"` # "python3 setup.py build" can be parallelized on Python >= 3.6. BUILD_OPTS = `$(PYTHON) -c \ @@ -234,9 +238,13 @@ install-git-hooks: ## Install GIT pre-commit hook. chmod +x .git/hooks/pre-commit # =================================================================== -# Wheels +# Distribution # =================================================================== +sdist: ## Create tar.gz source distribution. + ${MAKE} generate-manifest + PYTHONWARNINGS=all $(PYTHON) setup.py sdist + download-wheels-github: ## Download latest wheels hosted on github. $(PYTHON) scripts/internal/download_wheels_github.py --tokenfile=~/.github.token ${MAKE} print-dist @@ -245,43 +253,26 @@ download-wheels-appveyor: ## Download latest wheels hosted on appveyor. $(PYTHON) scripts/internal/download_wheels_appveyor.py ${MAKE} print-dist -print-dist: ## Print downloaded wheels / tar.gs - $(PYTHON) scripts/internal/print_dist.py - -# =================================================================== -# Distribution -# =================================================================== - -git-tag-release: ## Git-tag a new release. - git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` - git push --follow-tags - -sdist: ## Create tar.gz source distribution. - ${MAKE} generate-manifest - $(PYTHON) setup.py sdist - $(PYTHON) -m twine check --strict dist/*.tar.gz - -# --- others - -check-sdist: ## Create source distribution and checks its sanity (MANIFEST) - rm -rf dist - ${MAKE} clean +check-sdist: ## Check sanity of source distribution. $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv - PYTHONWARNINGS=all $(PYTHON) setup.py sdist build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" + $(PYTHON) -m twine check --strict dist/*.tar.gz + +check-wheels: ## Check sanity of wheels. + $(PYTHON) -m abi3audit --verbose --strict dist/*-abi3-*.whl + $(PYTHON) -m twine check --strict dist/*.whl pre-release: ## Check if we're ready to produce a new release. + ${MAKE} clean + ${MAKE} sdist ${MAKE} check-sdist ${MAKE} install - ${MAKE} generate-manifest - git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain - ${MAKE} sdist ${MAKE} download-wheels-github ${MAKE} download-wheels-appveyor + ${MAKE} check-wheels ${MAKE} print-hashes ${MAKE} print-dist - $(PYTHON) -m twine check --strict dist/* $(PYTHON) -c \ "from psutil import __version__ as ver; \ doc = open('docs/index.rst').read(); \ @@ -290,17 +281,23 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in history, '%r not in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX in HISTORY.rst';" -release: ## Create a release (down/uploads tar.gz, wheels, git tag release). - $(PYTHON) -m twine check --strict dist/* - $(PYTHON) -m twine upload dist/* # upload tar.gz and Windows wheels on PyPI +release: ## Upload a new release. + ${MAKE} check-sdist + ${MAKE} check-wheels + $(PYTHON) -m twine upload dist/*.tar.gz + $(PYTHON) -m twine upload dist/*.whl ${MAKE} git-tag-release -check-manifest: ## Inspect MANIFEST.in file. - $(PYTHON) -m check_manifest -v $(ARGS) - generate-manifest: ## Generates MANIFEST.in file. $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in +print-dist: ## Print downloaded wheels / tar.gs + $(PYTHON) scripts/internal/print_dist.py + +git-tag-release: ## Git-tag a new release. + git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` + git push --follow-tags + # =================================================================== # Printers # =================================================================== @@ -343,5 +340,8 @@ bench-oneshot-2: ## Same as above but using perf module (supposed to be more pr check-broken-links: ## Look for broken links in source files. git ls-files | xargs $(PYTHON) -Wa scripts/internal/check_broken_links.py +check-manifest: ## Inspect MANIFEST.in file. + $(PYTHON) -m check_manifest -v $(ARGS) + help: ## Display callable targets. @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' From 72906618b6353ef7b47582d74b98aff61b0d577e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 2 May 2023 12:38:32 +0200 Subject: [PATCH 0993/1714] NetBSD, cmdline(): add debug message on EINVAL (re. to #2250) --- psutil/_psbsd.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 99808bd289..37433c2db2 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -24,6 +24,7 @@ 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 @@ -662,10 +663,10 @@ 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: @@ -677,6 +678,7 @@ def cmdline(self): 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 From 5c52826e7eac9648006392f5d491f6fdf6ff50a8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 2 May 2023 13:01:20 +0200 Subject: [PATCH 0994/1714] skip who CLI related test if necessary --- README.rst | 4 ++-- psutil/tests/test_posix.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 2e991cb354..683044f6d3 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ | |downloads| |stars| |forks| |contributors| |coverage| -| |version| |py-versions| |packages| |license| -| |github-actions-wheels| |github-actions-bsd| |appveyor| |doc| |twitter| |tidelift| +| |version| |py-versions| |packages| |license| |twitter| |tidelift| +| |github-actions-wheels| |github-actions-bsd| |appveyor| |doc| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index c789ee875e..fe6936de56 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -378,8 +378,9 @@ def test_users_started(self): started = [x.capitalize() for x in started] if not tstamp: - raise ValueError( + 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( From 49aba759744c06fb3a6fa998155428520f161734 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 9 May 2023 18:03:08 +0800 Subject: [PATCH 0995/1714] fix typos and some other minor bugs (#2253) Signed-off-by: cui fliter --- HISTORY.rst | 2 +- psutil/_pswindows.py | 2 +- psutil/arch/freebsd/sensors.c | 6 +++--- psutil/tests/test_connections.py | 2 +- psutil/tests/test_windows.py | 4 ++-- scripts/internal/check_broken_links.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1a79b360c6..8a6b880d24 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -481,7 +481,7 @@ XXXX-XX-XX doesn't exist. (patch by Cedric Lamoriniere) - 1471_, [SunOS]: `Process.name()`_ and `Process.cmdline()`_ can return ``SystemError``. (patch by Daniel Beer) -- 1472_, [Linux]: `cpu_freq()`_ does not return all CPUs on Rasbperry-pi 3. +- 1472_, [Linux]: `cpu_freq()`_ does not return all CPUs on Raspberry-pi 3. - 1474_: fix formatting of ``psutil.tests()`` which mimics ``ps aux`` output. - 1475_, [Windows], **[critical]**: ``OSError.winerror`` attribute wasn't properly checked resulting in ``WindowsError(ERROR_ACCESS_DENIED)`` being diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 4081aa17fb..38e26ebc56 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -247,7 +247,7 @@ def swap_memory(): total_system = mem[2] # system memory (commit total/limit) is the sum of physical and swap - # thus physical memory values need to be substracted to get swap values + # 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 diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 0042adbeb8..08a882dd93 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -20,7 +20,7 @@ For reference, here's the git history with original(ish) implementations: #include "../../_psutil_posix.h" -#define DECIKELVIN_2_CELCIUS(t) (t - 2731) / 10 +#define DECIKELVIN_2_CELSIUS(t) (t - 2731) / 10 PyObject * @@ -62,13 +62,13 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { sprintf(sensor, "dev.cpu.%d.temperature", core); if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) goto error; - current = DECIKELVIN_2_CELCIUS(current); + current = DECIKELVIN_2_CELSIUS(current); // Return -273 in case of failure. sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); if (sysctlbyname(sensor, &tjmax, &size, NULL, 0)) tjmax = 0; - tjmax = DECIKELVIN_2_CELCIUS(tjmax); + tjmax = DECIKELVIN_2_CELSIUS(tjmax); return Py_BuildValue("ii", current, tjmax); diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index ad615ed07e..aec164e857 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -200,7 +200,7 @@ def test_unix_udp(self): @serialrun class TestConnectedSocket(ConnectionTestCase): - """Test socket pairs which are are actually connected to + """Test socket pairs which are actually connected to each other. """ diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index a9f6893364..dd1dcbb1fb 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -65,7 +65,7 @@ class WindowsTestCase(PsutilTestCase): def powershell(cmd): - """Currently not used, but avalable just in case. Usage: + """Currently not used, but available just in case. Usage: >>> powershell( "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") @@ -79,7 +79,7 @@ def powershell(cmd): def wmic(path, what, converter=int): - """Currently not used, but avalable just in case. Usage: + """Currently not used, but available just in case. Usage: >>> wmic("Win32_OperatingSystem", "FreePhysicalMemory") 2134124534 diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 8ceab1c75f..f5375a860a 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -25,7 +25,7 @@ Handles redirects, http, https, ftp as well. REFERENCES: -Using [1] with some modificatons for including ftp +Using [1] with some modifications for including ftp [1] http://stackoverflow.com/a/6883094/5163807 [2] http://stackoverflow.com/a/31952097/5163807 [3] http://daringfireball.net/2010/07/improved_regex_for_matching_urls From 5c1cd64e2965fd09ae8061d2f935b88fb53d5145 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 9 May 2023 18:58:24 +0200 Subject: [PATCH 0996/1714] small README changes Signed-off-by: Giampaolo Rodola --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 683044f6d3..95a7f359f6 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ | |downloads| |stars| |forks| |contributors| |coverage| -| |version| |py-versions| |packages| |license| |twitter| |tidelift| -| |github-actions-wheels| |github-actions-bsd| |appveyor| |doc| +| |version| |py-versions| |packages| |license| +| |github-actions-wheels| |github-actions-bsd| |appveyor| |doc| |twitter| |tidelift| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil @@ -488,6 +488,7 @@ Here's some I find particularly interesting: - https://github.com/google/grr - https://github.com/facebook/osquery/ - https://github.com/nicolargo/glances +- https://github.com/aristocratos/bpytop - https://github.com/Jahaja/psdash - https://github.com/ajenti/ajenti - https://github.com/home-assistant/home-assistant/ From f24a82439d56396d911e17e734184571ee692cf7 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Thu, 8 Jun 2023 19:05:06 +0800 Subject: [PATCH 0997/1714] Check PID range before any `cext` call (#2266) Signed-off-by: Xuehai Pan Giampaolo: I think it makes sense. The error originates from PyArg_ParseTuple (used pretty much everywhere). The Process class can invoke PyArg_ParseTuple on __init__ (hence only once) and turn OverflowError into NoSuchProcess. The reason why I think this is a good idea is because OverflowError is only raised when invoking C functions. On Linux most of Process APIs are implemented in python instead, by reading /proc, so we do not get OverflowError in those cases (we get NoSuchProcess). As such, making this change would make things more consistent across all platforms, which will always rase NoSuchProcess. --- CREDITS | 2 +- HISTORY.rst | 2 ++ psutil/__init__.py | 8 ++++++++ psutil/_psutil_aix.c | 1 + psutil/_psutil_bsd.c | 1 + psutil/_psutil_common.c | 21 +++++++++++++++++++++ psutil/_psutil_common.h | 1 + psutil/_psutil_linux.c | 1 + psutil/_psutil_osx.c | 1 + psutil/_psutil_sunos.c | 1 + psutil/_psutil_windows.c | 1 + psutil/tests/test_misc.py | 6 ++++++ 12 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index f5888fb79b..0461d2eca5 100644 --- a/CREDITS +++ b/CREDITS @@ -765,7 +765,7 @@ I: 1598 N: Xuehai Pan W: https://github.com/XuehaiPan -I: 1948 +I: 1948, 2264 N: Saeed Rasooli W: https://github.com/ilius diff --git a/HISTORY.rst b/HISTORY.rst index 8a6b880d24..c0cfe9d2d6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ XXXX-XX-XX - 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas Klausner) +- 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ + instead of OverflowError. (patch by Xuehai Pan) 5.9.5 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 2b0b3c6b00..bb38808406 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -340,6 +340,14 @@ def _init(self, pid, _ignore_nsp=False): if pid < 0: raise ValueError('pid must be a positive integer (got %s)' % pid) + try: + _psplatform.cext.check_pid_range(pid) + except OverflowError: + raise NoSuchProcess( + pid, + msg='process PID out of range (got %s)' % pid, + ) + self._pid = pid self._name = None self._exe = None diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 8a1b8de94b..ce89a7bd7c 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -1019,6 +1019,7 @@ PsutilMethods[] = {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, // --- others + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index a1b4a3e5be..fde3916d31 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -106,6 +106,7 @@ static PyMethodDef mod_methods[] = { {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS}, #endif // --- others + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 096e2f373f..d8d78bf640 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -128,6 +128,27 @@ AccessDenied(const char *syscall) { return NULL; } +/* + * Raise OverflowError if Python int value overflowed when converting to pid_t. + * Raise ValueError if Python int value is negative. + * Otherwise, return None. + */ +PyObject * +psutil_check_pid_range(PyObject *self, PyObject *args) { +#ifdef PSUTIL_WINDOWS + DWORD pid; +#else + pid_t pid; +#endif + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (pid < 0) { + PyErr_SetString(PyExc_ValueError, "pid must be a positive integer"); + return NULL; + } + Py_RETURN_NONE; +} // Enable or disable PSUTIL_DEBUG messages. PyObject * diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 591f5521f0..a425f8685e 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -99,6 +99,7 @@ PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); // --- Global utils // ==================================================================== +PyObject* psutil_check_pid_range(PyObject *self, PyObject *args); PyObject* psutil_set_debug(PyObject *self, PyObject *args); int psutil_setup(void); diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index a6ee60254b..3e6b3b9004 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -518,6 +518,7 @@ static PyMethodDef mod_methods[] = { // --- linux specific {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS}, // --- others + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index dd7168eb7a..369fbbfb48 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -54,6 +54,7 @@ static PyMethodDef mod_methods[] = { {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, // --- others + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 5422005fef..11335993f6 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1659,6 +1659,7 @@ PsutilMethods[] = { {"users", psutil_users, METH_VARARGS}, // --- others + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 54678f7557..bb6e12ff80 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -105,6 +105,7 @@ PsutilMethods[] = { {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS}, // --- others + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index a7b5d02d5f..53c6401216 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -60,6 +60,12 @@ 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) From 962cb9ad58ca3bc21b20130f435f8d507d931051 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 9 Jun 2023 15:37:14 +0200 Subject: [PATCH 0998/1714] bytes2human() is unable to print negative values (#2268) Signed-off-by: Giampaolo Rodola The bug: ``` >>> from psutil._common import bytes2human >>> bytes2human(212312454) 202.5M >>> bytes2human(-212312454) -212312454 ``` --- HISTORY.rst | 4 +++- docs/index.rst | 2 +- psutil/_common.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c0cfe9d2d6..6e1f065f4f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,8 +7,10 @@ XXXX-XX-XX - 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas Klausner) -- 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ +- 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ instead of OverflowError. (patch by Xuehai Pan) +- 2268_: ``bytes2human()`` utility function was unable to properly represent + negative values. 5.9.5 ===== diff --git a/docs/index.rst b/docs/index.rst index 2bf050a11d..31d5d7ef50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2544,7 +2544,7 @@ Bytes conversion for i, s in enumerate(symbols): prefix[s] = 1 << (i + 1) * 10 for s in reversed(symbols): - if n >= prefix[s]: + if abs(n) >= prefix[s]: value = float(n) / prefix[s] return '%.1f%s' % (value, s) return "%sB" % n diff --git a/psutil/_common.py b/psutil/_common.py index 9228674648..63792edbd0 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -827,7 +827,7 @@ def bytes2human(n, format="%(value).1f%(symbol)s"): for i, s in enumerate(symbols[1:]): prefix[s] = 1 << (i + 1) * 10 for symbol in reversed(symbols[1:]): - if n >= prefix[symbol]: + if abs(n) >= prefix[symbol]: value = float(n) / prefix[symbol] return format % locals() return format % dict(symbol=symbols[0], value=n) From 1336977f3a544d28f7492859de6be6fbc0234b0e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 30 Jul 2023 16:31:22 +0200 Subject: [PATCH 0999/1714] add a fix-unittests make target to fix unit tests thanks to https://github.com/isidentical/teyit and @isidentical Signed-off-by: Giampaolo Rodola --- Makefile | 5 +++++ psutil/tests/test_system.py | 2 +- psutil/tests/test_windows.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 376f01895c..0143abe080 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ PY3_DEPS = \ requests \ setuptools \ sphinx_rtd_theme \ + teyit \ twine \ virtualenv \ wheel @@ -225,9 +226,13 @@ fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. fix-imports: ## Fix imports with isort. @git ls-files '*.py' | xargs $(PYTHON) -m isort --jobs=${NUM_WORKERS} +fix-unittests: ## Fix unittest idioms. + @git ls-files '*test_*.py' | xargs $(PYTHON) -m teyit --show-stats + fix-all: ## Run all code fixers. ${MAKE} fix-flake8 ${MAKE} fix-imports + ${MAKE} fix-unittests # =================================================================== # GIT diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 414c86e99b..fdca7fac0f 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -263,7 +263,7 @@ def test_os_constants(self): # assert all other constants are set to False for name in names: - self.assertIs(getattr(psutil, name), False, msg=name) + self.assertFalse(getattr(psutil, name), msg=name) class TestMemoryAPIs(PsutilTestCase): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index dd1dcbb1fb..bb6faabe2e 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -374,7 +374,7 @@ def test_special_pid(self): # that nothing strange happens str(p) p.username() - self.assertTrue(p.create_time() >= 0.0) + self.assertGreaterEqual(p.create_time(), 0.0) try: rss, vms = p.memory_info()[:2] except psutil.AccessDenied: @@ -382,7 +382,7 @@ def test_special_pid(self): if not platform.uname()[1] in ('vista', 'win-7', 'win7'): raise else: - self.assertTrue(rss > 0) + self.assertGreater(rss, 0) def test_send_signal(self): p = psutil.Process(self.pid) From ee1007d3fe330bd8ce3de731494a9d48c090a034 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 30 Jul 2023 16:52:12 +0200 Subject: [PATCH 1000/1714] make flake8 happy Signed-off-by: Giampaolo Rodola --- psutil/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index e6f0abc356..521c3a612b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1744,7 +1744,7 @@ def is_namedtuple(x): f = getattr(t, '_fields', None) if not isinstance(f, tuple): return False - return all(type(n) == str for n in f) + return all(isinstance(n, str) for n in f) if POSIX: From 46b100648fc3a4987ae84ecd54fa20ddd1bc1c38 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Mon, 31 Jul 2023 03:06:01 +0200 Subject: [PATCH 1001/1714] chore(ci): fix linters job warning (#2269) --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c3ab484d1..a5a8fab709 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,6 +110,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + with: + python-version: 3.x - name: 'Run linters' run: | # py3 From fc85c02c600fe1bdbe05116e033b6a5eb286ecfe Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Mon, 31 Jul 2023 05:48:32 +0200 Subject: [PATCH 1002/1714] chore: test with Python 3.12 (#2270) --- .github/workflows/build.yml | 3 ++- HISTORY.rst | 1 + psutil/arch/windows/disk.c | 29 +++++++++++++++++++++++++---- psutil/tests/test_process.py | 6 +++--- psutil/tests/test_system.py | 4 ++-- 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5a8fab709..8332730ba6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,9 +44,10 @@ jobs: python-version: 3.11 - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.11.2 + uses: pypa/cibuildwheel@v2.14.1 env: CIBW_ARCHS: "${{ matrix.archs }}" + CIBW_PRERELEASE_PYTHONS: True - name: Upload wheels uses: actions/upload-artifact@v3 diff --git a/HISTORY.rst b/HISTORY.rst index 6e1f065f4f..34ed06515f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,7 @@ XXXX-XX-XX instead of OverflowError. (patch by Xuehai Pan) - 2268_: ``bytes2human()`` utility function was unable to properly represent negative values. +- 2252_: [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by Matthieu Darbois) 5.9.5 ===== diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 7c35e81243..7cf0f20ccf 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -44,6 +44,8 @@ PyObject * psutil_disk_usage(PyObject *self, PyObject *args) { BOOL retval; ULARGE_INTEGER _, total, free; + +#if PY_MAJOR_VERSION <= 2 char *path; if (PyArg_ParseTuple(args, "u", &path)) { @@ -55,7 +57,6 @@ psutil_disk_usage(PyObject *self, PyObject *args) { // on Python 2 we also want to accept plain strings other // than Unicode -#if PY_MAJOR_VERSION <= 2 PyErr_Clear(); // drop the argument parsing error if (PyArg_ParseTuple(args, "s", &path)) { Py_BEGIN_ALLOW_THREADS @@ -63,15 +64,35 @@ psutil_disk_usage(PyObject *self, PyObject *args) { Py_END_ALLOW_THREADS goto return_; } -#endif return NULL; return_: if (retval == 0) return PyErr_SetFromWindowsErrWithFilename(0, path); - else - return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); +#else + PyObject *py_path; + wchar_t *path; + + if (!PyArg_ParseTuple(args, "U", &py_path)) { + return NULL; + } + + path = PyUnicode_AsWideCharString(py_path, NULL); + if (path == NULL) { + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + retval = GetDiskFreeSpaceExW(path, &_, &total, &free); + Py_END_ALLOW_THREADS + + PyMem_Free(path); + + if (retval == 0) + return PyErr_SetExcFromWindowsErrWithFilenameObject(PyExc_OSError, 0, py_path); +#endif + return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); } diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b3cf9ff54f..f08f034b23 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1059,12 +1059,12 @@ def test_num_fds(self): def test_num_ctx_switches(self): p = psutil.Process() before = sum(p.num_ctx_switches()) - for _ in range(500000): + 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 50.000 iterations") + raise self.fail("num ctx switches still the same after 2 iterations") def test_ppid(self): p = psutil.Process() diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index fdca7fac0f..0385a2b925 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -349,7 +349,7 @@ def test_cpu_times(self): self.assertIsInstance(cp_time, float) self.assertGreaterEqual(cp_time, 0.0) total += cp_time - self.assertEqual(total, sum(times)) + 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 @@ -388,7 +388,7 @@ def test_per_cpu_times(self): self.assertIsInstance(cp_time, float) self.assertGreaterEqual(cp_time, 0.0) total += cp_time - self.assertEqual(total, sum(times)) + self.assertAlmostEqual(total, sum(times)) str(times) self.assertEqual(len(psutil.cpu_times(percpu=True)[0]), len(psutil.cpu_times(percpu=False))) From aebc26011c22ca5866d86b151494280e3509c80f Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Sun, 30 Jul 2023 23:49:36 -0400 Subject: [PATCH 1003/1714] Fix a dead link in the documentation (#2280) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 31d5d7ef50..669b8b4620 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1758,7 +1758,7 @@ Process class fields are variable depending on the platform. This method is useful to obtain a detailed representation of process memory usage as explained - `here `__ + `here `__ (the most important value is "private" memory). If *grouped* is ``True`` the mapped regions with the same *path* are grouped together and the different memory fields are summed. If *grouped* From 88d33f7413ecf06224826a97ab138b6d24ce676c Mon Sep 17 00:00:00 2001 From: student_2333 Date: Mon, 31 Jul 2023 13:31:05 +0800 Subject: [PATCH 1004/1714] fix var unbound (#2245) --- psutil/_pswindows.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 38e26ebc56..5a10efc5f7 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -257,7 +257,9 @@ def swap_memory(): 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) From 1fe0497955c41fe18f246fb796d35cf22b922b7f Mon Sep 17 00:00:00 2001 From: Simba Date: Mon, 31 Jul 2023 15:24:28 +0800 Subject: [PATCH 1005/1714] Update __init__.py (#2276) --- psutil/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index bb38808406..35a3c9ad59 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2121,7 +2121,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() From 252b6fda98e9c99282f7883663e54b405aad0434 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Tue, 1 Aug 2023 03:17:10 +0200 Subject: [PATCH 1006/1714] Drop python 3.4 & 3.5 support (#2246) --- README.rst | 2 +- docs/index.rst | 4 ++-- psutil/__init__.py | 4 ++-- psutil/_common.py | 7 ++++--- psutil/_pslinux.py | 2 +- psutil/_psposix.py | 2 +- psutil/_pswindows.py | 2 +- psutil/tests/__init__.py | 5 +---- psutil/tests/test_contracts.py | 4 ++-- psutil/tests/test_system.py | 3 ++- scripts/internal/print_announce.py | 2 +- scripts/internal/winmake.py | 9 ++++----- setup.py | 3 ++- 13 files changed, 24 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index 95a7f359f6..7c7408a591 100644 --- a/README.rst +++ b/README.rst @@ -98,7 +98,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are **2.7**, **3.4+** and +Supported Python versions are **2.7**, **3.6+** and `PyPy `__. Funding diff --git a/docs/index.rst b/docs/index.rst index 669b8b4620..6b0408dea8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are **2.7** and **3.4+**. +Supported Python versions are **2.7** and **3.6+**. `PyPy `__ is also known to work. The psutil documentation you're reading is distributed as a single HTML page. @@ -2632,7 +2632,7 @@ Platforms support history * psutil 0.1.1 (2009-03): **FreeBSD** * psutil 0.1.0 (2009-01): **Linux, Windows, macOS** -Supported Python versions are 2.7, 3.4+ and PyPy3. +Supported Python versions are 2.7, 3.6+ and PyPy3. Timeline ======== diff --git a/psutil/__init__.py b/psutil/__init__.py index 35a3c9ad59..2b15670658 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -17,7 +17,7 @@ - Sun Solaris - AIX -Works with Python versions 2.7 and 3.4+. +Works with Python versions 2.7 and 3.6+. """ from __future__ import division @@ -2189,7 +2189,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() diff --git a/psutil/_common.py b/psutil/_common.py index 63792edbd0..a0d49d6384 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -35,14 +35,15 @@ 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() diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index d178fe1638..e0acb0e297 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -53,7 +53,7 @@ from ._compat import basestring -if sys.version_info >= (3, 4): +if PY3: import enum else: enum = None diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 1d250bf75c..0039daf44a 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -28,7 +28,7 @@ from . import _psutil_osx -if sys.version_info >= (3, 4): +if PY3: import enum else: enum = None diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 5a10efc5f7..eec2db84db 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -56,7 +56,7 @@ else: raise -if sys.version_info >= (3, 4): +if PY3: import enum else: enum = None diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 521c3a612b..0781f06862 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -67,7 +67,7 @@ warnings.simplefilter("ignore") import mock # NOQA - requires "pip install mock" -if sys.version_info >= (3, 4): +if PY3: import enum else: enum = None @@ -1714,9 +1714,6 @@ def import_module_by_path(path): 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) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 7f299a6243..e0bb92ce56 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -15,7 +15,6 @@ import platform import signal import stat -import sys import time import traceback import unittest @@ -36,6 +35,7 @@ from psutil._compat import long from psutil._compat import range from psutil._compat import unicode +from psutil._compat import PY3 from psutil.tests import APPVEYOR from psutil.tests import CI_TESTING from psutil.tests import GITHUB_ACTIONS @@ -723,7 +723,7 @@ def nice(self, ret, info): priorities = [getattr(psutil, x) for x in dir(psutil) if x.endswith('_PRIORITY_CLASS')] self.assertIn(ret, priorities) - if sys.version_info > (3, 4): + if PY3: self.assertIsInstance(ret, enum.IntEnum) else: self.assertIsInstance(ret, int) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 0385a2b925..3f96762947 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -32,6 +32,7 @@ from psutil import WINDOWS from psutil._compat import FileNotFoundError from psutil._compat import long +from psutil._compat import PY3 from psutil.tests import ASCII_FS from psutil.tests import CI_TESTING from psutil.tests import DEVNULL @@ -751,7 +752,7 @@ def test_net_if_addrs(self): 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) and not PYPY: + if PY3 and not PYPY: self.assertIsInstance(addr.family, enum.IntEnum) if nic_stats[nic].isup: # Do not test binding to addresses of interfaces diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 510dc15f64..33fca4220d 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -47,7 +47,7 @@ nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap. It \ currently supports Linux, Windows, macOS, Sun Solaris, FreeBSD, OpenBSD, \ NetBSD and AIX, both 32-bit and 64-bit architectures. Supported Python \ -versions are 2.7 and 3.4+. PyPy is also known to work. +versions are 2.7 and 3.6+. PyPy is also known to work. What's new ========== diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index f92b2a8969..5ec2ecbfd2 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -54,14 +54,13 @@ "wheel", ] -if sys.version_info[:2] >= (3, 5): - DEPS.append('flake8-bugbear') -if sys.version_info[:2] <= (2, 7): +if sys.version_info[0] == 2: DEPS.append('mock') -if sys.version_info[:2] <= (3, 2): DEPS.append('ipaddress') -if sys.version_info[:2] <= (3, 4): DEPS.append('enum34') +else: + DEPS.append('flake8-bugbear') + if not PYPY: DEPS.append("pywin32") DEPS.append("wmi") diff --git a/setup.py b/setup.py index 2b8b2abb22..35467e131a 100755 --- a/setup.py +++ b/setup.py @@ -444,7 +444,8 @@ def main(): ) if setuptools is not None: kwargs.update( - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + python_requires=( + ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"), extras_require=extras_require, zip_safe=False, ) From ef666acbd201ef37cdff5ff3257fe37da860980a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 1 Aug 2023 08:10:04 +0200 Subject: [PATCH 1007/1714] psutil.cpu_percent() is not thread safe (fixes #1703) (#2282) `psutil.cpu_percent(interval=None)` and `psutil.cpu_times_percent(interval=None)` are non-blocking functions returning the CPU percent consumption since the last time they were called. In order to do so they use a global variable, in which the last CPU timings are saved, and that means they are not thread safe. E.g., if 10 threads call `cpu_percent(interval=None)` with a 1 second interval, only 1 thread out of 10 will get the right result, as it will "invalidate" the timings for the other 9. Problem can be reproduced with the following script: ```python import threading, time, psutil NUM_WORKERS = psutil.cpu_count() def measure_cpu(): while 1: print(psutil.cpu_percent()) time.sleep(1) for x in range(NUM_WORKERS): threading.Thread(target=measure_cpu).start() while 1: print() time.sleep(1.1) ``` The output looks like this, and it shows how inconsistent CPU measurements are between different threads (notice 0.0 values): ``` 3.5 3.5 0.0 0.0 2.8 2.8 0.0 0.0 2.5 2.5 0.0 0.0 2.5 2.5 2.5 2.5 3.3 3.3 3.3 50.0 2.8 0.0 0.0 0.0 ``` After patch: ``` 0.0 0.0 0.0 0.0 2.0 2.3 2.3 2.3 5.5 5.3 5.5 5.5 3.3 3.3 3.0 3.0 9.0 8.9 9.0 9.4 30.0 30.0 29.6 30.0 24.7 24.7 24.7 24.7 ``` --- HISTORY.rst | 14 +++++++-- docs/index.rst | 9 ++++++ psutil/__init__.py | 71 ++++++++++++++++------------------------------ 3 files changed, 46 insertions(+), 48 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 34ed06515f..80db358e48 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,10 +5,20 @@ XXXX-XX-XX -- 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas - Klausner) +**Enhancements** + +- 1703_: `cpu_percent()`_ and `cpu_times_percent()`_ are now thread safe, + meaning they can be called from different threads and still return + meaningful and independent results. Before, if (say) 10 threads called + ``cpu_percent(interval=None)`` at the same time, only 1 thread out of 10 + would get the right result. - 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ instead of OverflowError. (patch by Xuehai Pan) + +**Bug fixes** + +- 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas + Klausner) - 2268_: ``bytes2human()`` utility function was unable to properly represent negative values. - 2252_: [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by Matthieu Darbois) diff --git a/docs/index.rst b/docs/index.rst index 6b0408dea8..a43e8f51e6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -176,6 +176,10 @@ CPU utilization as a percentage for each CPU. First element of the list refers to first CPU, second element to second CPU and so on. The order of the list is consistent across calls. + Internally this function maintains a global map (a dict) where each key is + the ID of the calling thread (`threading.get_ident`_). This means it can be + called from different threads, at different intervals, and still return + meaningful and independent results. >>> import psutil >>> # blocking @@ -194,6 +198,8 @@ CPU it will return a meaningless ``0.0`` value which you are supposed to ignore. + .. versionchanged:: 5.9.6 function is now thread safe. + .. function:: cpu_times_percent(interval=None, percpu=False) Same as :func:`cpu_percent()` but provides utilization percentages for each @@ -212,6 +218,8 @@ CPU .. versionchanged:: 4.1.0 two new *interrupt* and *dpc* fields are returned on Windows. + .. versionchanged:: 5.9.6 function is now thread safe. + .. function:: cpu_count(logical=True) Return the number of logical CPUs in the system (same as `os.cpu_count`_ @@ -3052,6 +3060,7 @@ Timeline .. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen .. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py .. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess +.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident .. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread .. _Tidelift security contact: https://tidelift.com/security .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme diff --git a/psutil/__init__.py b/psutil/__init__.py index 2b15670658..68f3c7ca92 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.5" +__version__ = "5.9.6" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -1629,16 +1629,18 @@ def cpu_times(percpu=False): try: - _last_cpu_times = cpu_times() + _last_cpu_times = {threading.current_thread().ident: cpu_times()} except Exception: # Don't want to crash at import time. - _last_cpu_times = None + _last_cpu_times = {} try: - _last_per_cpu_times = cpu_times(percpu=True) + _last_per_cpu_times = { + threading.current_thread().ident: cpu_times(percpu=True) + } except Exception: # Don't want to crash at import time. - _last_per_cpu_times = None + _last_per_cpu_times = {} def _cpu_tot_time(times): @@ -1732,8 +1734,7 @@ 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) @@ -1756,14 +1757,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 = [] @@ -1771,23 +1767,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): @@ -1803,8 +1793,7 @@ 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) @@ -1832,14 +1821,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 = [] @@ -1847,14 +1831,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 From 179efa130817f989ff089de106455626b1e79d85 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 1 Aug 2023 08:18:22 +0200 Subject: [PATCH 1008/1714] update HISTORY/CREDITS for #2246. #2245. #2252, CC @mayeut, @student_2333 --- CREDITS | 2 +- HISTORY.rst | 6 +++++- docs/index.rst | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index 0461d2eca5..74e7617264 100644 --- a/CREDITS +++ b/CREDITS @@ -781,7 +781,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039, 2142, 2147, 2153, 2040, 2102, 2216 +I: 2039, 2142, 2147, 2153, 2040, 2102, 2216, 2246, 2252 N: Hugo van Kemenade W: https://github.com/hugovk diff --git a/HISTORY.rst b/HISTORY.rst index 80db358e48..52e909f008 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,14 +14,18 @@ XXXX-XX-XX would get the right result. - 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ instead of OverflowError. (patch by Xuehai Pan) +- 2246_: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) **Bug fixes** - 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas Klausner) +- 2245_, [Windows]: fix var unbound error on possibly in `swap_memory()`_ + (patch by student_2333) - 2268_: ``bytes2human()`` utility function was unable to properly represent negative values. -- 2252_: [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by Matthieu Darbois) +- 2252_: [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by + Matthieu Darbois) 5.9.5 ===== diff --git a/docs/index.rst b/docs/index.rst index a43e8f51e6..dbd9ab7bbc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2627,6 +2627,7 @@ If you want to develop psutil take a look at the `development guide`_. Platforms support history ========================= +* psutil 5.9.6 (XXXX-XX): drop Python 3.4 and 3.5 support * psutil 5.9.1 (2022-05): drop Python 2.6 support * psutil 5.9.0 (2021-12): **MidnightBSD** * psutil 5.8.0 (2020-12): **PyPy 2** on Windows From 1b7a3e1acaf1478579377d31bf060485cc60ac8f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 1 Aug 2023 19:59:07 +0200 Subject: [PATCH 1009/1714] [Linux] memory_full_info() incorrectly raise ZombieProcess (#2284) ...this happens, for example, with PID 2 (kthreadd): ``` >>> import psutil >>> psutil.Process(2).memory_info() pmem(rss=0, vms=0, shared=0, text=0, lib=0, data=0, dirty=0) >>> psutil.Process(2).memory_full_info() Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1901, in _parse_smaps_rollup for line in f: ProcessLookupError: [Errno 3] No such process During handling of the above exception, another exception occurred: Traceback (most recent call last): File "", line 1, in File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 1091, in memory_full_info return self._proc.memory_full_info() File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1948, in memory_full_info uss, pss, swap = self._parse_smaps_rollup() File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1653, in wrapper return fun(self, *args, **kwargs) File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1913, in _parse_smaps_rollup raise ZombieProcess(self.pid, self._name, self._ppid) psutil.ZombieProcess: PID still exists but it's a zombie (pid=2) ``` The error originates from `/proc/pid/smaps_rollup`, which acts weirdly: it raises ESRCH / ENOENT for many PIDs, even if they're alive (also as root). In that case we'll have to use `/proc/pid/smaps` as fallback, which is slower but has a +50% success rate compared to ``/proc/pid/smaps_rollup``. --- HISTORY.rst | 5 ++++- psutil/_pslinux.py | 40 +++++++++++++++++++--------------- psutil/tests/__init__.py | 33 ++++++++++++++++++++++++++++ psutil/tests/test_contracts.py | 11 ++++++---- psutil/tests/test_process.py | 29 +++++------------------- psutil/tests/test_system.py | 2 +- 6 files changed, 73 insertions(+), 47 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 52e909f008..72ad966c20 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,8 +24,11 @@ XXXX-XX-XX (patch by student_2333) - 2268_: ``bytes2human()`` utility function was unable to properly represent negative values. -- 2252_: [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by +- 2252_, [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by Matthieu Darbois) +- 2283_, [Linux]: `memory_full_info`_ may incorrectly raise `ZombieProcess`_ + if it's determined via ``/proc/pid/smaps_rollup``. Instead we now fallback on + reading ``/proc/pid/smaps``. 5.9.5 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e0acb0e297..7ca6abcfca 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1889,28 +1889,26 @@ def memory_info(self): if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS: - @wrap_exceptions 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 - try: - 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 - except ProcessLookupError: # happens on readline() - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) + 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 @@ -1943,9 +1941,15 @@ def _parse_smaps( 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 - uss, pss, swap = self._parse_smaps_rollup() + try: + uss, pss, swap = self._parse_smaps_rollup() + except (ProcessLookupError, FileNotFoundError) as err: + debug("ignore %r for pid %s and retry using " + "/proc/pid/smaps" % (err, self.pid)) + uss, pss, swap = self._parse_smaps() else: uss, pss, swap = self._parse_smaps() basic_mem = self.memory_info() diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 0781f06862..fe9b2fcc7b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -957,6 +957,39 @@ def assertProcessGone(self, proc): assert not psutil.pid_exists(proc.pid), proc.pid self.assertNotIn(proc.pid, psutil.pids()) + def assertProcessZombie(self, proc): + # A zombie process should always be instantiable. + psutil.Process(proc.pid) + # Its status always be querable. + self.assertEqual(proc.status(), psutil.STATUS_ZOMBIE) + # It should be considered 'running'. + assert proc.is_running() + # 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()]) + # It cannot be signaled or terminated. + proc.suspend() + proc.resume() + proc.terminate() + proc.kill() + assert proc.is_running() + + # 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): diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index e0bb92ce56..9ff6d6ada0 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -31,11 +31,11 @@ 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._compat import range from psutil._compat import unicode -from psutil._compat import PY3 from psutil.tests import APPVEYOR from psutil.tests import CI_TESTING from psutil.tests import GITHUB_ACTIONS @@ -359,6 +359,7 @@ def check_exception(exc, proc, name, ppid): tcase.assertEqual(exc.pid, pid) 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) @@ -401,14 +402,16 @@ class TestFetchAllProcesses(PsutilTestCase): Uses a process pool to get info about all processes. """ + use_proc_pool = not CI_TESTING + def setUp(self): # Using a pool in a CI env may result in deadlock, see: # https://github.com/giampaolo/psutil/issues/2104 - if not CI_TESTING: + if self.use_proc_pool: self.pool = multiprocessing.Pool() def tearDown(self): - if not CI_TESTING: + if self.use_proc_pool: self.pool.terminate() self.pool.join() @@ -417,7 +420,7 @@ def iter_proc_info(self): # same object as test_contracts.proc_info". from psutil.tests.test_contracts import proc_info - if not CI_TESTING: + if self.use_proc_pool: return self.pool.imap_unordered(proc_info, psutil.pids()) else: ls = [] diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index f08f034b23..46daa9a53a 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1327,33 +1327,16 @@ def succeed_or_zombie_p_exc(fun): pass parent, zombie = self.spawn_zombie() - # A zombie process should always be instantiable - zproc = psutil.Process(zombie.pid) - # ...and at least its status always be querable - self.assertEqual(zproc.status(), psutil.STATUS_ZOMBIE) - # ...and it should be considered 'running' - assert zproc.is_running() - # ...and as_dict() shouldn't crash - zproc.as_dict() - # ...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 - - ns = process_namespace(zproc) + self.assertProcessZombie(zombie) + ns = process_namespace(zombie) for fun, name in ns.iter(ns.all): succeed_or_zombie_p_exc(fun) - assert psutil.pid_exists(zproc.pid) - self.assertIn(zproc.pid, psutil.pids()) - self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) + assert psutil.pid_exists(zombie.pid) + self.assertIn(zombie.pid, psutil.pids()) + self.assertIn(zombie.pid, [x.pid for x in psutil.process_iter()]) psutil._pmap = {} - self.assertIn(zproc.pid, [x.pid for x in psutil.process_iter()]) + self.assertIn(zombie.pid, [x.pid for x in psutil.process_iter()]) @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process_is_running_w_exc(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 3f96762947..9e81e2f5d8 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -30,9 +30,9 @@ 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._compat import PY3 from psutil.tests import ASCII_FS from psutil.tests import CI_TESTING from psutil.tests import DEVNULL From 7ee77b08c641506f5402e31600fc5f8a9cb0ccf8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 1 Aug 2023 20:00:54 +0200 Subject: [PATCH 1010/1714] update HISTORY --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 72ad966c20..bb14e6cfdc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -26,7 +26,7 @@ XXXX-XX-XX negative values. - 2252_, [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by Matthieu Darbois) -- 2283_, [Linux]: `memory_full_info`_ may incorrectly raise `ZombieProcess`_ +- 2284_, [Linux]: `memory_full_info`_ may incorrectly raise `ZombieProcess`_ if it's determined via ``/proc/pid/smaps_rollup``. Instead we now fallback on reading ``/proc/pid/smaps``. From 8bd2405f2bcc647dce62b46d61437d0abef01817 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Aug 2023 11:02:12 +0200 Subject: [PATCH 1011/1714] [Linux] correctly raise ZombieProcess on exe(), cmdline() and memory_maps() (#2288) --- HISTORY.rst | 2 ++ Makefile | 2 +- psutil/_psbsd.py | 2 +- psutil/_pslinux.py | 63 +++++++++++++++++++++--------------- psutil/_psosx.py | 2 +- psutil/tests/__init__.py | 23 ++++++++++++- psutil/tests/test_linux.py | 48 ++++++++------------------- psutil/tests/test_process.py | 15 --------- 8 files changed, 77 insertions(+), 80 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index bb14e6cfdc..91c37bcb57 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,8 @@ XXXX-XX-XX - 2284_, [Linux]: `memory_full_info`_ may incorrectly raise `ZombieProcess`_ if it's determined via ``/proc/pid/smaps_rollup``. Instead we now fallback on reading ``/proc/pid/smaps``. +- 2288_, [Linux]: correctly raise `ZombieProcess`_ on `exe`_, `cmdline`_ and + `memory_maps`_ instead of returning a "null" value. 5.9.5 ===== diff --git a/Makefile b/Makefile index 0143abe080..a8274a649e 100644 --- a/Makefile +++ b/Makefile @@ -180,7 +180,7 @@ test-memleaks: ## Memory leak tests. ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memleaks.py -test-failed: ## Re-run tests which failed on last run +test-last-failed: ## Re-run tests which failed on last run ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --last-failed diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 37433c2db2..fb4217efeb 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -553,7 +553,7 @@ def is_zombie(pid): try: st = cext.proc_oneshot_info(pid)[kinfo_proc_map['status']] return PROC_STATUSES.get(st) == _common.STATUS_ZOMBIE - except Exception: + except OSError: return False diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7ca6abcfca..0f102cbfa5 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1654,12 +1654,12 @@ def wrapper(self, *args, **kwargs): 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 @@ -1675,7 +1675,27 @@ def __init__(self, pid): self._ppid = None self._procfs_path = get_procfs_path() - def _assert_alive(self): + 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. @@ -1749,22 +1769,18 @@ def name(self): # 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 (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 "" - else: - 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) + raise @wrap_exceptions def cmdline(self): @@ -1772,6 +1788,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 @@ -1990,8 +2007,10 @@ def get_blocks(lines, current_block): 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 = [] @@ -2030,14 +2049,7 @@ def get_blocks(lines, current_block): @wrap_exceptions def cwd(self): - try: - return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) - except (FileNotFoundError, ProcessLookupError): - # https://github.com/giampaolo/psutil/issues/986 - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) + return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) @wrap_exceptions def num_ctx_switches(self, @@ -2086,7 +2098,7 @@ def threads(self): ntuple = _common.pthread(int(thread_id), utime, stime) retlist.append(ntuple) if hit_enoent: - self._assert_alive() + self._raise_if_not_alive() return retlist @wrap_exceptions @@ -2179,12 +2191,11 @@ def rlimit(self, resource_, limits=None): "got %s" % repr(limits)) 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): @@ -2239,13 +2250,13 @@ def open_files(self): path, int(fd), int(pos), mode, flags) retlist.append(ntuple) if hit_enoent: - self._assert_alive() + self._raise_if_not_alive() return retlist @wrap_exceptions def connections(self, kind='inet'): ret = _connections.retrieve(kind, self.pid) - self._assert_alive() + self._raise_if_not_alive() return ret @wrap_exceptions diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 893bdba189..8da2d9a328 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -332,7 +332,7 @@ def is_zombie(pid): try: st = cext.proc_kinfo_oneshot(pid)[kinfo_proc_map['status']] return st == cext.SZOMB - except Exception: + except OSError: return False diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index fe9b2fcc7b..1cb9fffde8 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -964,17 +964,38 @@ def assertProcessZombie(self, proc): 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()]) - # It cannot be signaled or terminated. + 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): + with self.subTest(name): + try: + fun() + except (psutil.ZombieProcess, psutil.AccessDenied): + pass + if LINUX: + # https://github.com/giampaolo/psutil/pull/2288 + self.assertRaises(psutil.ZombieProcess, proc.cmdline) + self.assertRaises(psutil.ZombieProcess, proc.exe) + self.assertRaises(psutil.ZombieProcess, proc.memory_maps) + # 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( diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index ac11017a3a..adb6ff6064 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -2060,24 +2060,10 @@ def open_mock_2(name, *args, **kwargs): 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) + 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 @@ -2096,23 +2082,15 @@ def test_rlimit_zombie(self): # 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 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 + 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()) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 46daa9a53a..3ab05ba0c4 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1320,23 +1320,8 @@ def assert_raises_nsp(fun, fun_name): @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process(self): - def succeed_or_zombie_p_exc(fun): - try: - return fun() - except (psutil.ZombieProcess, psutil.AccessDenied): - pass - parent, zombie = self.spawn_zombie() self.assertProcessZombie(zombie) - ns = process_namespace(zombie) - for fun, name in ns.iter(ns.all): - succeed_or_zombie_p_exc(fun) - - assert psutil.pid_exists(zombie.pid) - self.assertIn(zombie.pid, psutil.pids()) - self.assertIn(zombie.pid, [x.pid for x in psutil.process_iter()]) - psutil._pmap = {} - self.assertIn(zombie.pid, [x.pid for x in psutil.process_iter()]) @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process_is_running_w_exc(self): From a384858e772bc36c54739999b96562193954047e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Aug 2023 16:22:28 +0200 Subject: [PATCH 1012/1714] [OpenBSD, NetBSD] fix zombie process with no ctime (#2289) Zombie processes on Open/NetBSD have a creation time of 0.0. Modify __eq__ so that it takes this into account, fixing is_running() return False for a process which turned into a zombie. --- Makefile | 4 ++-- psutil/__init__.py | 16 +++++++++++++++- psutil/tests/__init__.py | 9 ++++++++- psutil/tests/test_system.py | 5 +++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a8274a649e..c6b002a603 100644 --- a/Makefile +++ b/Makefile @@ -125,8 +125,8 @@ install-pip: ## Install pip (no-op if already installed). setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade --trusted-host files.pythonhosted.org pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade --trusted-host files.pythonhosted.org $(PY_DEPS) + $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip + $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade $(PY_DEPS) # =================================================================== # Tests diff --git a/psutil/__init__.py b/psutil/__init__.py index 68f3c7ca92..49ab94d507 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -404,7 +404,7 @@ def __str__(self): pass if self._exitcode not in (_SENTINEL, None): info["exitcode"] = self._exitcode - if self._create_time: + if self._create_time is not None: info['started'] = _pprint_secs(self._create_time) return "%s.%s(%s)" % ( self.__class__.__module__, @@ -418,6 +418,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): diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 1cb9fffde8..62cef7633f 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -43,6 +43,8 @@ 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 @@ -959,7 +961,12 @@ def assertProcessGone(self, proc): def assertProcessZombie(self, proc): # A zombie process should always be instantiable. - psutil.Process(proc.pid) + 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'. diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 9e81e2f5d8..80e4988d53 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -72,6 +72,11 @@ def test_process_iter(self): 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()), []) From 11b117f34860d2cf2327b04d0894e33a3e3d1958 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Aug 2023 22:27:38 +0800 Subject: [PATCH 1013/1714] update HISTORY --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 91c37bcb57..f3bcba9d0f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,8 @@ XXXX-XX-XX - 2284_, [Linux]: `memory_full_info`_ may incorrectly raise `ZombieProcess`_ if it's determined via ``/proc/pid/smaps_rollup``. Instead we now fallback on reading ``/proc/pid/smaps``. +- 2287_, [OpenBSD], [NetBSD]: `Process.is_running()`_ erroneously return + ``False`` for zombie processes, because creation time cannot be determined. - 2288_, [Linux]: correctly raise `ZombieProcess`_ on `exe`_, `cmdline`_ and `memory_maps`_ instead of returning a "null" value. From 173eed60b24d3ae057a242418e48e6665b175cea Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 3 Aug 2023 15:49:31 +0800 Subject: [PATCH 1014/1714] PID reusage is not checked for different set methods (#2291) Doc states: > process identity is preemptively checked (via PID + creation time) is for the following methods: nice() (set), ionice() (set), cpu_affinity() (set), rlimit() (set), children(), parent(), parents(), suspend() resume(), send_signal(), terminate() kill(). It turns out we're currently doing this only for "kill" methods (suspend() resume(), send_signal(), terminate() kill()) and children(). We're not doing it for set methods: nice() (set) ionice() (set) cpu_affinity() (set) rlimit() (set) parent() Also, since we do it for parent(), we should also do it for parents() and ppid(). --- HISTORY.rst | 14 ++++++++--- docs/index.rst | 1 + psutil/__init__.py | 48 ++++++++++++++++++------------------ psutil/tests/test_process.py | 18 ++++++-------- 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f3bcba9d0f..12b48f5eb0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,8 @@ XXXX-XX-XX - 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ instead of OverflowError. (patch by Xuehai Pan) - 2246_: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) +- 2290_: PID reuse is now pre-emptively checked for `Process.ppid`_ and + `Process.parents`_. **Bug fixes** @@ -24,15 +26,19 @@ XXXX-XX-XX (patch by student_2333) - 2268_: ``bytes2human()`` utility function was unable to properly represent negative values. -- 2252_, [Windows]: `psutil.disk_usage`_ fails on Python 3.12+. (patch by +- 2252_, [Windows]: `disk_usage`_ fails on Python 3.12+. (patch by Matthieu Darbois) -- 2284_, [Linux]: `memory_full_info`_ may incorrectly raise `ZombieProcess`_ - if it's determined via ``/proc/pid/smaps_rollup``. Instead we now fallback on - reading ``/proc/pid/smaps``. +- 2284_, [Linux]: `Process.memory_full_info`_ may incorrectly raise + `ZombieProcess`_ if it's determined via ``/proc/pid/smaps_rollup``. Instead + we now fallback on reading ``/proc/pid/smaps``. - 2287_, [OpenBSD], [NetBSD]: `Process.is_running()`_ erroneously return ``False`` for zombie processes, because creation time cannot be determined. - 2288_, [Linux]: correctly raise `ZombieProcess`_ on `exe`_, `cmdline`_ and `memory_maps`_ instead of returning a "null" value. +- 2290_: differently from what stated in the doc, PID reuse is not + pre-emptively checked for `Process.nice()`_ (set), `Process.ionice()`_, + (set), `Process.cpu_affinity()`_ (set), `Process.rlimit()`_ + (set), `Process.parent()`_. 5.9.5 ===== diff --git a/docs/index.rst b/docs/index.rst index dbd9ab7bbc..6336ed55a6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1079,6 +1079,7 @@ Process class :meth:`cpu_affinity` (set), :meth:`rlimit` (set), :meth:`children`, + :meth:`ppid`, :meth:`parent`, :meth:`parents`, :meth:`suspend` diff --git a/psutil/__init__.py b/psutil/__init__.py index 49ab94d507..c3c74f1ff8 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -263,22 +263,6 @@ 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(): - if self._pid_reused: - msg = "process no longer exists and its PID has been reused" - else: - msg = None - raise NoSuchProcess(self.pid, self._name, msg=msg) - return fun(self, *args, **kwargs) - return wrapper - - def _pprint_secs(secs): """Format seconds in a human readable form.""" now = time.time() @@ -442,6 +426,17 @@ def __hash__(self): self._hash = hash(self._ident) return self._hash + def _raise_if_pid_reused(self): + """Raises NoSuchProcess in case a process is no longer running + or its PID has been reused. + """ + if not self.is_running(): + if self._pid_reused: + msg = "process no longer exists and its PID has been reused" + else: + msg = None + raise NoSuchProcess(self.pid, self._name, msg=msg) + @property def pid(self): """The process PID.""" @@ -627,6 +622,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 @@ -750,8 +746,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: @@ -813,6 +808,7 @@ def ionice(self, ioclass=None, value=None): raise ValueError("'ioclass' argument must be specified") return self._proc.ionice_get() else: + self._raise_if_pid_reused() return self._proc.ionice_set(ioclass, value) # Linux / FreeBSD only @@ -828,6 +824,8 @@ def rlimit(self, resource, limits=None): See "man prlimit" for further info. Available on Linux and FreeBSD only. """ + if limits is not None: + self._raise_if_pid_reused() return self._proc.rlimit(resource, limits) # Windows, Linux and FreeBSD only @@ -844,6 +842,7 @@ def cpu_affinity(self, cpus=None): if cpus is None: 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() @@ -900,7 +899,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. @@ -927,6 +925,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: @@ -1197,6 +1196,7 @@ def connections(self, kind='inet'): 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( @@ -1216,7 +1216,6 @@ def _send_signal(self, sig): 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) . @@ -1226,9 +1225,9 @@ def send_signal(self, sig): if POSIX: self._send_signal(sig) else: # pragma: no cover + self._raise_if_pid_reused() 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. @@ -1237,9 +1236,9 @@ def suspend(self): 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. @@ -1248,9 +1247,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. @@ -1259,9 +1258,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. @@ -1269,6 +1268,7 @@ 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): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 3ab05ba0c4..90a064137f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1091,13 +1091,6 @@ def test_parent_multi(self): self.assertEqual(grandchild.parent(), child) self.assertEqual(child.parent(), parent) - def test_parent_disappeared(self): - # Emulate a case where the parent process disappeared. - p = self.spawn_psproc() - with mock.patch("psutil.Process", - side_effect=psutil.NoSuchProcess(0, 'foo')): - self.assertIsNone(p.parent()) - @retry_on_failure() def test_parents(self): parent = psutil.Process() @@ -1351,10 +1344,13 @@ def test_reused_pid(self): assert not p.is_running() assert p != psutil.Process(subp.pid) msg = "process no longer exists and its PID has been reused" - self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.suspend) - self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.resume) - self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.terminate) - self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.kill) + 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): From 685aad0bf4d7d3698e59c97864e05e72d6963a68 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 3 Aug 2023 21:50:31 +0800 Subject: [PATCH 1015/1714] fix DEVGUIDE.rst --- README.rst | 25 +++++++++-------- docs/DEVGUIDE.rst | 69 ++++++++++++++++++++++------------------------- 2 files changed, 44 insertions(+), 50 deletions(-) diff --git a/README.rst b/README.rst index 7c7408a591..53b2affa67 100644 --- a/README.rst +++ b/README.rst @@ -312,6 +312,8 @@ Process management >>> p = psutil.Process(7055) >>> p psutil.Process(pid=7055, name='python3', status='running', started='09:04:44') + >>> p.pid + 7055 >>> p.name() 'python3' >>> p.exe() @@ -319,32 +321,29 @@ Process management >>> p.cwd() '/home/giampaolo' >>> p.cmdline() - ['/usr/bin/python', 'main.py'] + ['/usr/bin/python3', 'main.py'] >>> - >>> p.pid - 7055 >>> p.ppid() 7054 - >>> p.children(recursive=True) - [psutil.Process(pid=29835, name='python3', status='sleeping', started='11:45:38'), - psutil.Process(pid=29836, name='python3', status='waking', started='11:43:39')] - >>> >>> p.parent() psutil.Process(pid=4699, name='bash', status='sleeping', started='09:06:44') >>> p.parents() [psutil.Process(pid=4699, name='bash', started='09:06:44'), psutil.Process(pid=4689, name='gnome-terminal-server', status='sleeping', started='0:06:44'), psutil.Process(pid=1, name='systemd', status='sleeping', started='05:56:55')] + >>> p.children(recursive=True) + [psutil.Process(pid=29835, name='python3', status='sleeping', started='11:45:38'), + psutil.Process(pid=29836, name='python3', status='waking', started='11:43:39')] >>> >>> p.status() 'running' - >>> p.username() - 'giampaolo' >>> p.create_time() 1267551141.5019531 >>> p.terminal() '/dev/pts/0' >>> + >>> p.username() + 'giampaolo' >>> p.uids() puids(real=1000, effective=1000, saved=1000) >>> p.gids() @@ -384,14 +383,14 @@ Process management [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')] >>> - >>> p.num_threads() - 4 - >>> p.num_fds() - 8 >>> p.threads() [pthread(id=5234, user_time=22.5, system_time=9.2891), pthread(id=5237, user_time=0.0707, system_time=1.1)] >>> + >>> p.num_threads() + 4 + >>> p.num_fds() + 8 >>> p.num_ctx_switches() pctxsw(voluntary=78, involuntary=19) >>> diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 384f8b25ea..03957a4ac6 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -7,8 +7,7 @@ Build, setup and running tests psutil makes extensive use of C extension modules, meaning a C compiler is required, see `install instructions `__. - -Once you have a compiler installed: +Once you have a compiler installed run: .. code-block:: bash @@ -28,11 +27,19 @@ Once you have a compiler installed: make test-parallel # faster make test-memleaks make test-coverage - make lint # Python (PEP8) and C linters + make lint-all # Run Python and C linter + make fix-all # Fix linting erors make uninstall make help -- if you're working on a new feature and you wish to compile & test it "on the + +- To run a specific unit test: + +.. code-block:: bash + + make test ARGS=psutil.tests.test_system.TestDiskAPIs + +- If you're working on a new feature and you wish to compile & test it "on the fly" from a test script, this is a quick & dirty way to do it: .. code-block:: bash @@ -40,10 +47,10 @@ Once you have a compiler installed: make test TSCRIPT=test_script.py # on UNIX make test test_script.py # on Windows -- do not use ``sudo``. ``make install`` installs psutil as a limited user in +- Do not use ``sudo``. ``make install`` installs psutil as a limited user in "edit" mode, meaning you can edit psutil code on the fly while you develop. -- if you want to target a specific Python version: +- If you want to target a specific Python version: .. code-block:: bash @@ -53,7 +60,7 @@ Once you have a compiler installed: Coding style ------------ -- python code strictly follows `PEP-8`_ styling guides and this is enforced by +- Oython code strictly follows `PEP-8`_ styling guides and this is enforced by a commit GIT hook installed via ``make install-git-hooks`` which will reject commits if code is not PEP-8 complieant. - C code should follow `PEP-7`_ styling guides. @@ -74,63 +81,51 @@ Adding a new API Typically, this is what you do: -- define the new API in `psutil/__init__.py`_. -- write the platform specific implementation in ``psutil/_ps{platform}.py`` +- Define the new API in `psutil/__init__.py`_. +- Write the platform specific implementation in ``psutil/_ps{platform}.py`` (e.g. `psutil/_pslinux.py`_). -- if the change requires C code, write the C implementation in +- If the change requires C code, write the C implementation in ``psutil/_psutil_{platform}.c`` (e.g. `psutil/_psutil_linux.c`_). -- write a generic test in `psutil/tests/test_system.py`_ or +- Write a generic test in `psutil/tests/test_system.py`_ or `psutil/tests/test_process.py`_. -- if possible, write a platform-specific test in +- If possible, write a platform-specific test in ``psutil/tests/test_{platform}.py`` (e.g. `psutil/tests/test_linux.py`_). This usually means testing the return value of the new API against a system CLI tool. -- update the doc in ``doc/index.py``. -- update `HISTORY.rst`_ and `CREDITS`_ files. -- make a pull request. +- Update the doc in ``docs/index.py``. +- Update `HISTORY.rst`_ and `CREDITS`_ files. +- Make a pull request. Make a pull request ------------------- -- fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") -- git clone the fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git`` -- create a branch:``git checkout -b new-feature`` -- commit your changes: ``git commit -am 'add some feature'`` -- push the branch: ``git push origin new-feature`` -- create a new PR via the GitHub web interface and sign-off your work (see +- Fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") +- Git clone the fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git`` +- Create a branch:``git checkout -b new-feature`` +- Commit your changes: ``git commit -am 'add some feature'`` +- Push the branch: ``git push origin new-feature`` +- Create a new PR via the GitHub web interface and sign-off your work (see `CONTRIBUTING.md`_ guidelines) Continuous integration ---------------------- Unit tests are automatically run on every ``git push`` on **Linux**, **macOS**, -**Windows** and **FreeBSD** by using: - -- `Github Actions`_ (Linux, macOS, Windows) -- `Appveyor`_ (Windows) - -.. image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD - :target: https://github.com/giampaolo/psutil/actions?query=workflow%3ACI - -.. image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows - :target: https://ci.appveyor.com/project/giampaolo/psutil - -OpenBSD, NetBSD, AIX and Solaris does not have continuous test integration. +**Windows**, **FreeBSD**, **NetBSD**, **OpenBSD**. +AIX and Solaris does not have continuous test integration. Documentation ------------- -- doc source code is written in a single file: `/docs/index.rst`_. +- doc source code is written in a single file: ``docs/index.rst``. - doc can be built with ``make setup-dev-env; cd docs; make html``. -- public doc is hosted at https://psutil.readthedocs.io +- public doc is hosted at https://psutil.readthedocs.io. .. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.yml .. _`Appveyor`: https://ci.appveyor.com/project/giampaolo/psuti .. _`coveralls.io`: https://coveralls.io/github/giampaolo/psuti .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md -.. _`doc/index.rst`: https://github.com/giampaolo/psutil/blob/master/doc/index.rst -.. _`Github Actions`: https://github.com/giampaolo/psutil/actions .. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst .. _`make.bat`: https://github.com/giampaolo/psutil/blob/master/make.bat .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile From f3bc333f14e0822edaa1f548049afbbf843c15fb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 4 Aug 2023 22:24:24 +0800 Subject: [PATCH 1016/1714] Make _raise_if_pid_reused() raise NSP only if pid has been 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(). --- psutil/__init__.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index c3c74f1ff8..6e7be6391f 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -427,14 +427,15 @@ def __hash__(self): return self._hash def _raise_if_pid_reused(self): - """Raises NoSuchProcess in case a process is no longer running - or its PID has been reused. - """ - if not self.is_running(): - if self._pid_reused: - msg = "process no longer exists and its PID has been reused" - else: - msg = None + """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 From 8bd827ff3b32d3594d282f31108a035384f05903 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Aug 2023 15:34:36 +0800 Subject: [PATCH 1017/1714] Add RsT linter (#2292) --- .github/workflows/build.yml | 8 ++--- HISTORY.rst | 33 ++++++++++---------- Makefile | 9 ++++-- docs/DEVGUIDE.rst | 5 ---- docs/index.rst | 10 +++---- pyproject.toml | 7 +++++ scripts/internal/git_pre_commit.py | 48 +++++++++++++++++++++--------- 7 files changed, 70 insertions(+), 50 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8332730ba6..c35eb77576 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,12 +115,8 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - # py3 - python3 -m pip install flake8 isort - python3 -m flake8 . - python3 -m isort . - # clinter - find . -type f \( -iname "*.c" -o -iname "*.h" \) | xargs python3 scripts/internal/clinter.py + python3 -m pip install isort rstcheck flake8 flake8-blind-except flake8-bugbear flake8-debugger flake8-print flake8-quotes sphinx + make lint-all # Check sanity of .tar.gz + wheel files check-dist: diff --git a/HISTORY.rst b/HISTORY.rst index 12b48f5eb0..67e3a0935c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,8 +15,8 @@ XXXX-XX-XX - 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ instead of OverflowError. (patch by Xuehai Pan) - 2246_: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) -- 2290_: PID reuse is now pre-emptively checked for `Process.ppid`_ and - `Process.parents`_. +- 2290_: PID reuse is now pre-emptively checked for `Process.ppid()`_ and + `Process.parents()`_. **Bug fixes** @@ -26,15 +26,16 @@ XXXX-XX-XX (patch by student_2333) - 2268_: ``bytes2human()`` utility function was unable to properly represent negative values. -- 2252_, [Windows]: `disk_usage`_ fails on Python 3.12+. (patch by +- 2252_, [Windows]: `disk_usage()`_ fails on Python 3.12+. (patch by Matthieu Darbois) -- 2284_, [Linux]: `Process.memory_full_info`_ may incorrectly raise +- 2284_, [Linux]: `Process.memory_full_info()`_ may incorrectly raise `ZombieProcess`_ if it's determined via ``/proc/pid/smaps_rollup``. Instead we now fallback on reading ``/proc/pid/smaps``. - 2287_, [OpenBSD], [NetBSD]: `Process.is_running()`_ erroneously return ``False`` for zombie processes, because creation time cannot be determined. -- 2288_, [Linux]: correctly raise `ZombieProcess`_ on `exe`_, `cmdline`_ and - `memory_maps`_ instead of returning a "null" value. +- 2288_, [Linux]: correctly raise `ZombieProcess`_ on `Process.exe()`_, + `Process.cmdline()`_ and `Process.memory_maps()`_ instead of returning a + "null" value. - 2290_: differently from what stated in the doc, PID reuse is not pre-emptively checked for `Process.nice()`_ (set), `Process.ionice()`_, (set), `Process.cpu_affinity()`_ (set), `Process.rlimit()`_ @@ -51,10 +52,10 @@ XXXX-XX-XX `KeyError` bit deriving from a missed cache hit. - 2217_: print the full traceback when a `DeprecationWarning` or `UserWarning` is raised. -- 2230_, [OpenBSD]: `psutil.net_connections`_ implementation was rewritten from - scratch: +- 2230_, [OpenBSD]: `net_connections()`_ implementation was rewritten + from scratch: - We're now able to retrieve the path of AF_UNIX sockets (before it was an - empty string) + empty string) - The function is faster since it no longer iterates over all processes. - No longer produces duplicate connection entries. - 2238_: there are cases where `Process.cwd()`_ cannot be determined @@ -70,7 +71,7 @@ XXXX-XX-XX **Bug fixes** -- 1043_, [OpenBSD] `psutil.net_connections`_ returns duplicate entries. +- 1043_, [OpenBSD] `net_connections()`_ returns duplicate entries. - 1915_, [Linux]: on certain kernels, ``"MemAvailable"`` field from ``/proc/meminfo`` returns ``0`` (possibly a kernel bug), in which case we calculate an approximation for ``available`` memory which matches "free" @@ -128,7 +129,7 @@ XXXX-XX-XX **Bug fixes** -- 2116_, [macOS], [critical]: `psutil.net_connections`_ fails with RuntimeError. +- 2116_, [macOS], [critical]: `net_connections()`_ fails with RuntimeError. - 2135_, [macOS]: `Process.environ()`_ may contain garbage data. Fix out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) - 2138_, [Linux], **[critical]**: can't compile psutil on Android due to @@ -1575,7 +1576,7 @@ XXXX-XX-XX - 564_: C extension version mismatch in case the user messed up with psutil installation or with sys.path is now detected at import time. - 568_: new `pidof.py`_ script. -- 569_, [FreeBSD]: add support for `Process.cpu_affinity`_ on FreeBSD. +- 569_, [FreeBSD]: add support for `Process.cpu_affinity()`_ on FreeBSD. **Bug fixes** @@ -1587,7 +1588,7 @@ XXXX-XX-XX (patch by spacewander) - 565_, [Windows]: use proper encoding for `Process.username()`_ and `users()`_. (patch by Sylvain Mouquet) -- 567_, [Linux]: in the alternative implementation of `Process.cpu_affinity`_ +- 567_, [Linux]: in the alternative implementation of `Process.cpu_affinity()`_ ``PyList_Append`` and ``Py_BuildValue`` return values are not checked. - 569_, [FreeBSD]: fix memory leak in `cpu_count()`_ with ``logical=False``. - 571_, [Linux]: `Process.open_files()`_ might swallow `AccessDenied`_ @@ -2169,7 +2170,8 @@ In most cases accessing the old names will work but it will cause a representation. - 283_: speedup `Process.is_running()`_ by caching its return value in case the process is terminated. -- 284_, [POSIX]: per-process number of opened file descriptors (`Process.num_fds`_). +- 284_, [POSIX]: per-process number of opened file descriptors + (`Process.num_fds()`_). - 287_: `process_iter()`_ now caches `Process`_ instances between calls. - 290_: `Process.nice()`_ property is deprecated in favor of new ``get_nice()`` and ``set_nice()`` methods. @@ -2527,8 +2529,6 @@ In most cases accessing the old names will work but it will cause a .. _`Process`: https://psutil.readthedocs.io/en/latest/#psutil.Process .. _`psutil.Popen`: https://psutil.readthedocs.io/en/latest/#psutil.Popen -.. _`psutil.Process`: https://psutil.readthedocs.io/en/latest/#psutil.Process - .. _`AccessDenied`: https://psutil.readthedocs.io/en/latest/#psutil.AccessDenied .. _`NoSuchProcess`: https://psutil.readthedocs.io/en/latest/#psutil.NoSuchProcess @@ -2586,7 +2586,6 @@ In most cases accessing the old names will work but it will cause a .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py .. _`free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py -.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py .. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py .. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py .. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py diff --git a/Makefile b/Makefile index c6b002a603..ecd2e17d5b 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ PY3_DEPS = \ pyperf \ pypinfo \ requests \ + rstcheck \ setuptools \ sphinx_rtd_theme \ teyit \ @@ -207,13 +208,17 @@ isort: ## Run isort linter. pylint: ## Python pylint (not mandatory, just run it from time to time) @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} -c-linter: ## Run C linter. +lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py +lint-rst: ## Run C linter. + @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml + lint-all: ## Run all linters ${MAKE} flake8 ${MAKE} isort - ${MAKE} c-linter + ${MAKE} lint-c + ${MAKE} lint-rst # =================================================================== # Fixers diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 03957a4ac6..c59502dfdc 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -121,9 +121,6 @@ Documentation - doc can be built with ``make setup-dev-env; cd docs; make html``. - public doc is hosted at https://psutil.readthedocs.io. -.. _`appveyor.yml`: https://github.com/giampaolo/psutil/blob/master/appveyor.yml -.. _`Appveyor`: https://ci.appveyor.com/project/giampaolo/psuti -.. _`coveralls.io`: https://coveralls.io/github/giampaolo/psuti .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst @@ -137,5 +134,3 @@ Documentation .. _`psutil/tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_linux.py .. _`psutil/tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_process.py .. _`psutil/tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_system.py -.. _`RsT syntax`: http://docutils.sourceforge.net/docs/user/rst/quickref.htm -.. _`sphinx`: http://sphinx-doc.org diff --git a/docs/index.rst b/docs/index.rst index 6336ed55a6..20f18fc891 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2581,7 +2581,7 @@ FAQs the Python script as a Windows service (ProcessHacker does this). * Q: is MinGW supported on Windows? -* A: no, you should Visual Studio (see `development guide`_). +* A: no, you should Visual Studio (see `development guide `_). Running tests ============= @@ -2623,7 +2623,7 @@ contact`_. Tidelift will coordinate the fix and disclosure. Development guide ================= -If you want to develop psutil take a look at the `development guide`_. +If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= @@ -3010,7 +3010,7 @@ Timeline .. _`BPO-6973`: https://bugs.python.org/issue6973 .. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`development guide`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst +.. _`DEVGUIDE.rst`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py .. _`enum`: https://docs.python.org/3/library/enum.html#module-enum .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py @@ -3033,7 +3033,6 @@ Timeline .. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py .. _`open`: https://docs.python.org/3/library/functions.html#open .. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count -.. _`os.getloadavg`: https://docs.python.org/3/library/os.html#os.getloadavg .. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid .. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority .. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid @@ -3064,5 +3063,4 @@ Timeline .. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess .. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident .. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread -.. _Tidelift security contact: https://tidelift.com/security -.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme +.. _`Tidelift security contact`: https://tidelift.com/security diff --git a/pyproject.toml b/pyproject.toml index 8d68131c95..25d436086e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,13 @@ disable = [ "wrong-import-position", ] +[tool.rstcheck] +ignore_messages = [ + "Duplicate explicit target name", + "Duplicate implicit target name", + "Hyperlink target \".*?\" is not referenced", +] + [build-system] requires = ["setuptools>=43", "wheel"] build-backend = "setuptools.build_meta" diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index dad0066b2a..7ace3c08be 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -15,6 +15,7 @@ - assert "flake8" checks pass - assert "isort" checks pass - assert C linter checks pass +- assert RsT checks pass - abort if files were added/renamed/removed and MANIFEST.in was not updated Install this with "make install-git-hooks". @@ -66,8 +67,12 @@ def exit(msg): def sh(cmd): - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) + if isinstance(cmd, str): + cmd = shlex.split(cmd) + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True + ) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) @@ -84,19 +89,23 @@ def open_text(path): def git_commit_files(): - out = sh("git diff --cached --name-only") + out = sh(["git", "diff", "--cached", "--name-only"]) py_files = [x for x in out.split('\n') if x.endswith('.py') and os.path.exists(x)] c_files = [x for x in out.split('\n') if x.endswith(('.c', '.h')) and os.path.exists(x)] - new_rm_mv = sh("git diff --name-only --diff-filter=ADR --cached") + rst_files = [x for x in out.split('\n') if x.endswith('.rst') and + os.path.exists(x)] + new_rm_mv = sh( + ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] + ) # XXX: we should escape spaces and possibly other amenities here new_rm_mv = new_rm_mv.split() - return (py_files, c_files, new_rm_mv) + return (py_files, c_files, rst_files, new_rm_mv) def main(): - py_files, c_files, new_rm_mv = git_commit_files() + py_files, c_files, rst_files, new_rm_mv = git_commit_files() # Check file content. for path in py_files: if os.path.realpath(path) == THIS_SCRIPT: @@ -122,27 +131,38 @@ def main(): if py_files: # flake8 assert os.path.exists('.flake8') - cmd = "%s -m flake8 --config=.flake8 %s" % (PYTHON, " ".join(py_files)) - ret = subprocess.call(shlex.split(cmd)) + print("running flake8 (%s files)" % len(py_files)) + cmd = [PYTHON, "-m", "flake8", "--config=.flake8"] + py_files + ret = subprocess.call(cmd) if ret != 0: return sys.exit("python code didn't pass 'flake8' style check; " "try running 'make fix-flake8'") # isort - cmd = "%s -m isort --check-only %s" % ( - PYTHON, " ".join(py_files)) - ret = subprocess.call(shlex.split(cmd)) + print("running isort (%s files)" % len(py_files)) + cmd = [PYTHON, "-m", "isort", "--check-only"] + py_files + ret = subprocess.call(cmd) if ret != 0: return sys.exit("python code didn't pass 'isort' style check; " "try running 'make fix-imports'") # C linter if c_files: + print("running clinter (%s files)" % len(c_files)) # XXX: we should escape spaces and possibly other amenities here - cmd = "%s scripts/internal/clinter.py %s" % (PYTHON, " ".join(c_files)) - ret = subprocess.call(cmd, shell=True) + cmd = [PYTHON, "scripts/internal/clinter.py"] + c_files + ret = subprocess.call(cmd) if ret != 0: return sys.exit("C code didn't pass style check") + + # RST linter + if rst_files: + print("running rst linter (%s)" % len(rst_files)) + cmd = ["rstcheck", "--config=pyproject.toml"] + rst_files + ret = subprocess.call(cmd) + if ret != 0: + return sys.exit("RST code didn't pass style check") + if new_rm_mv: - out = sh("%s scripts/internal/generate_manifest.py" % PYTHON) + out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) with open_text('MANIFEST.in') as f: if out.strip() != f.read().strip(): sys.exit("some files were added, deleted or renamed; " From 77e5b7445748d30d22c0e3b2e651414da96a88b4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Aug 2023 14:49:18 +0800 Subject: [PATCH 1018/1714] refact assertProcessGone --- psutil/tests/__init__.py | 71 ++++++++++++++++++++++++---------- psutil/tests/test_contracts.py | 42 +++++++++++--------- psutil/tests/test_process.py | 5 --- psutil/tests/test_testutils.py | 12 +++--- 4 files changed, 81 insertions(+), 49 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 62cef7633f..2e07620cf6 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -944,20 +944,45 @@ def pyrun(self, *args, **kwds): self.addCleanup(terminate, sproc) # executed first return sproc - def assertProcessGone(self, proc): - self.assertRaises(psutil.NoSuchProcess, psutil.Process, proc.pid) - if isinstance(proc, (psutil.Process, psutil.Popen)): - assert not proc.is_running() + 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: - status = proc.status() - except psutil.NoSuchProcess: - pass - else: - raise AssertionError("Process.status() didn't raise exception " - "(status=%s)" % status) - proc.wait(timeout=0) # assert not raise TimeoutExpired - assert not psutil.pid_exists(proc.pid), proc.pid - self.assertNotIn(proc.pid, psutil.pids()) + 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): + with self.assertRaises(psutil.NoSuchProcess) as cm: + try: + fun() + except psutil.ZombieProcess: + raise AssertionError( + "wasn't supposed to raise ZombieProcess") + self._check_proc_exc(proc, cm.exception) + proc.wait(timeout=0) # assert not raise TimeoutExpired def assertProcessZombie(self, proc): # A zombie process should always be instantiable. @@ -981,17 +1006,23 @@ def assertProcessZombie(self, proc): 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): - with self.subTest(name): + 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): - pass + except (psutil.ZombieProcess, psutil.AccessDenied) as exc: + self._check_proc_exc(proc, exc) if LINUX: # https://github.com/giampaolo/psutil/pull/2288 - self.assertRaises(psutil.ZombieProcess, proc.cmdline) - self.assertRaises(psutil.ZombieProcess, proc.exe) - self.assertRaises(psutil.ZombieProcess, proc.memory_maps) + 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() diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 9ff6d6ada0..0408078218 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -366,6 +366,7 @@ def check_exception(exc, proc, name, ppid): elif isinstance(exc, psutil.NoSuchProcess): tcase.assertProcessGone(proc) str(exc) + repr(exc) def do_wait(): if pid != 0: @@ -376,23 +377,27 @@ def do_wait(): try: proc = psutil.Process(pid) - d = proc.as_dict(['ppid', 'name']) except psutil.NoSuchProcess: + tcase.assertPidGone(pid) return {} - - 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 + 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 @@ -402,7 +407,7 @@ class TestFetchAllProcesses(PsutilTestCase): Uses a process pool to get info about all processes. """ - use_proc_pool = not CI_TESTING + use_proc_pool = 0 def setUp(self): # Using a pool in a CI env may result in deadlock, see: @@ -435,9 +440,9 @@ def test_all(self): meth = getattr(self, name) try: meth(value, info) - except AssertionError: + except Exception: s = '\n' + '=' * 70 + '\n' - s += "FAIL: test_%s pid=%s, ret=%s\n" % ( + s += "FAIL: name=test_%s, pid=%s, ret=%s\n" % ( name, info['pid'], repr(value)) s += '-' * 70 s += "\n%s" % traceback.format_exc() @@ -480,6 +485,7 @@ def pid(self, ret, info): 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)) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 90a064137f..48d3db9835 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1072,11 +1072,6 @@ def test_ppid(self): self.assertEqual(p.ppid(), os.getppid()) p = self.spawn_psproc() self.assertEqual(p.ppid(), os.getpid()) - if APPVEYOR: - # Occasional failures, see: - # https://ci.appveyor.com/project/giampaolo/psutil/build/ - # job/0hs623nenj7w4m33 - return def test_parent(self): p = self.spawn_psproc() diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 77e52b6969..65ca0c4d40 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -252,32 +252,32 @@ def test_terminate(self): # by subprocess.Popen p = self.spawn_testproc() terminate(p) - self.assertProcessGone(p) + self.assertPidGone(p.pid) terminate(p) # by psutil.Process p = psutil.Process(self.spawn_testproc().pid) terminate(p) - self.assertProcessGone(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.assertProcessGone(p) + self.assertPidGone(p.pid) terminate(p) # by PID pid = self.spawn_testproc().pid terminate(pid) - self.assertProcessGone(p) + self.assertPidGone(p.pid) terminate(pid) # zombie if POSIX: parent, zombie = self.spawn_zombie() terminate(parent) terminate(zombie) - self.assertProcessGone(parent) - self.assertProcessGone(zombie) + self.assertPidGone(parent.pid) + self.assertPidGone(zombie.pid) class TestNetUtils(PsutilTestCase): From a5c8428e4001abdc668a85f0432a8031e01846a6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Oct 2023 07:26:23 +0200 Subject: [PATCH 1019/1714] refact assertProcessGone --- psutil/tests/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 2e07620cf6..1aba0418fa 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -975,13 +975,16 @@ def assertProcessGone(self, proc): ns = process_namespace(proc) for fun, name in ns.iter(ns.all, clear_cache=True): with self.subTest(proc=proc, name=name): - with self.assertRaises(psutil.NoSuchProcess) as cm: - try: - fun() - except psutil.ZombieProcess: - raise AssertionError( - "wasn't supposed to raise ZombieProcess") - self._check_proc_exc(proc, cm.exception) + 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): From 0c0840a465e91ba1ceac699709b8a09cb4960ace Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Oct 2023 22:50:54 +0200 Subject: [PATCH 1020/1714] Win: fix OpenProcess not recognizing when proc is gone. This is due to ExitCodeProcess needing PROCESS_QUERY_INFORMATION access rights. --- make.bat | 1 + psutil/__init__.py | 3 +++ psutil/arch/windows/proc.c | 16 +++++++++++----- psutil/tests/test_process.py | 5 ----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/make.bat b/make.bat index 2c79d42911..2ac53d9ec2 100644 --- a/make.bat +++ b/make.bat @@ -31,5 +31,6 @@ if "%TSCRIPT%" == "" ( rem Needed to locate the .pypirc file and upload exes on PyPI. set HOME=%USERPROFILE% +set PSUTIL_DEBUG=1 %PYTHON% scripts\internal\winmake.py %1 %2 %3 %4 %5 %6 diff --git a/psutil/__init__.py b/psutil/__init__.py index 6e7be6391f..5a5a20606a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1227,6 +1227,9 @@ def send_signal(self, sig): self._send_signal(sig) else: # pragma: no cover 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) def suspend(self): diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index d9b69744f1..af3df267ac 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -99,14 +99,14 @@ PyObject * psutil_proc_kill(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD pid; + DWORD access = PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (pid == 0) return AccessDenied("automatically set for PID 0"); - hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); - hProcess = psutil_check_phandle(hProcess, pid, 0); + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) { return NULL; } @@ -272,6 +272,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (pid == 0) return AccessDenied("automatically set for PID 0"); + // ...because NtQuerySystemInformation can succeed for terminated + // processes. + if (psutil_pid_is_running(pid) == 0) + return NoSuchProcess("psutil_pid_is_running -> 0"); + buffer = MALLOC_ZERO(bufferSize); if (! buffer) { PyErr_NoMemory(); @@ -535,12 +540,13 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { DWORD pid; NTSTATUS status; HANDLE hProcess; + DWORD access = PROCESS_SUSPEND_RESUME | PROCESS_QUERY_LIMITED_INFORMATION; PyObject* suspend; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) - return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) + return NULL; - hProcess = psutil_handle_from_pid(pid, PROCESS_SUSPEND_RESUME); + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 48d3db9835..ae0192708a 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1301,11 +1301,6 @@ def assert_raises_nsp(fun, fun_name): for fun, name in ns.iter(ns.all): assert_raises_nsp(fun, name) - # NtQuerySystemInformation succeeds even if process is gone. - if WINDOWS and not GITHUB_ACTIONS: - normcase = os.path.normcase - self.assertEqual(normcase(p.exe()), normcase(PYTHON_EXE)) - @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process(self): parent, zombie = self.spawn_zombie() From ed396476fe2f503c405400ac8e3a9b65bb0ea751 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Oct 2023 23:40:44 +0200 Subject: [PATCH 1021/1714] Fix #2308, OpenBSD: Process.threads() always fail with AccessDenied. --- HISTORY.rst | 2 ++ psutil/arch/openbsd/proc.c | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 67e3a0935c..3b00b3b9d1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -40,6 +40,8 @@ XXXX-XX-XX pre-emptively checked for `Process.nice()`_ (set), `Process.ionice()`_, (set), `Process.cpu_affinity()`_ (set), `Process.rlimit()`_ (set), `Process.parent()`_. +- 2308_, [OpenBSD]: `Process.threads()`_ always fail with AccessDenied (also as + root). 5.9.5 ===== diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 5c984fc536..96b85bc502 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -219,8 +219,15 @@ psutil_proc_threads(PyObject *self, PyObject *args) { kd = kvm_openfiles(0, 0, 0, O_RDONLY, errbuf); if (! kd) { - convert_kvm_err("kvm_openfiles()", errbuf); - goto error; + // Usually fails due to EPERM against /dev/mem. We retry with + // KVM_NO_FILES which apparently has the same effect. + // https://stackoverflow.com/questions/22369736/ + psutil_debug("kvm_openfiles(O_RDONLY) failed"); + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); + if (! kd) { + convert_kvm_err("kvm_openfiles()", errbuf); + goto error; + } } kp = kvm_getprocs( From ffceba42a3e4584e71aca4bf0b8696f56d6e4ce2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Oct 2023 23:51:53 +0200 Subject: [PATCH 1022/1714] new RTD config --- MANIFEST.in | 2 ++ docs/.readthedocs.yaml | 20 ++++++++++++++++++++ docs/requirements.txt | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 docs/.readthedocs.yaml create mode 100644 docs/requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index 8defe71770..78cb836a6d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,7 @@ include LICENSE include MANIFEST.in include Makefile include README.rst +include docs/.readthedocs.yaml include docs/DEVGUIDE.rst include docs/DEVNOTES include docs/Makefile @@ -19,6 +20,7 @@ include docs/_static/sidebar.js include docs/conf.py include docs/index.rst include docs/make.bat +include docs/requirements.txt include make.bat include psutil/__init__.py include psutil/_common.py diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml new file mode 100644 index 0000000000..24e302ef02 --- /dev/null +++ b/docs/.readthedocs.yaml @@ -0,0 +1,20 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: conf.py + +# RTD recommends specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000000..82133027c9 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx +sphinx_rtd_theme From 2cd8ef767a6d0bfa08ff45572feb55ea33774221 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Oct 2023 23:57:00 +0200 Subject: [PATCH 1023/1714] new RTD config --- docs/.readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index 24e302ef02..cf1ffbaee7 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -17,4 +17,4 @@ sphinx: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: requirements.txt + - requirements: docs/requirements.txt From ddf9531858af0dc34121fd7a7a51f1ac30bd721c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Oct 2023 23:59:13 +0200 Subject: [PATCH 1024/1714] new RTD config --- docs/.readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index cf1ffbaee7..44ffd9687f 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -11,7 +11,7 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: - configuration: conf.py + configuration: docs/conf.py # RTD recommends specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html From e7ba381b77d24cdfbd1a1a8df9acd02db8e81e54 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 3 Oct 2023 23:19:12 +0200 Subject: [PATCH 1025/1714] Add toml-sort linting tool for pyproject.toml (#2311) --- .github/workflows/build.yml | 2 +- Makefile | 11 +++- pyproject.toml | 46 +++++++++------- scripts/internal/git_pre_commit.py | 87 ++++++++++++++++++------------ 4 files changed, 92 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c35eb77576..3f3ad76d90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,7 +115,7 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install isort rstcheck flake8 flake8-blind-except flake8-bugbear flake8-debugger flake8-print flake8-quotes sphinx + python3 -m pip install isort rstcheck toml-sort flake8 flake8-blind-except flake8-bugbear flake8-debugger flake8-print flake8-quotes sphinx make lint-all # Check sanity of .tar.gz + wheel files diff --git a/Makefile b/Makefile index ecd2e17d5b..95276f6648 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ PY3_DEPS = \ setuptools \ sphinx_rtd_theme \ teyit \ + toml-sort \ twine \ virtualenv \ wheel @@ -205,7 +206,7 @@ flake8: ## Run flake8 linter. isort: ## Run isort linter. @git ls-files '*.py' | xargs $(PYTHON) -m isort --check-only --jobs=${NUM_WORKERS} -pylint: ## Python pylint (not mandatory, just run it from time to time) +_pylint: ## Python pylint (not mandatory, just run it from time to time) @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} lint-c: ## Run C linter. @@ -214,11 +215,15 @@ lint-c: ## Run C linter. lint-rst: ## Run C linter. @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml +lint-toml: ## Linter for pyproject.toml + @git ls-files '*.toml' | xargs toml-sort --check + lint-all: ## Run all linters ${MAKE} flake8 ${MAKE} isort ${MAKE} lint-c ${MAKE} lint-rst + ${MAKE} lint-toml # =================================================================== # Fixers @@ -234,10 +239,14 @@ fix-imports: ## Fix imports with isort. fix-unittests: ## Fix unittest idioms. @git ls-files '*test_*.py' | xargs $(PYTHON) -m teyit --show-stats +fix-toml: ## Fix pyproject.toml + @git ls-files '*.toml' | xargs toml-sort + fix-all: ## Run all code fixers. ${MAKE} fix-flake8 ${MAKE} fix-imports ${MAKE} fix-unittests + ${MAKE} fix-toml # =================================================================== # GIT diff --git a/pyproject.toml b/pyproject.toml index 25d436086e..bf94359582 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,36 +3,36 @@ force_single_line = true # one import per line lines_after_imports = 2 # blank spaces after import section [tool.coverage.report] -omit = [ - "psutil/_compat.py", - "psutil/tests/*", - "setup.py", -] exclude_lines = [ "enum.IntEnum", "except ImportError:", "globals().update", - "if __name__ == .__main__.:", - "if _WINDOWS:", "if BSD", - "if enum is None:", - "if enum is not None:", "if FREEBSD", - "if has_enums:", "if LINUX", "if LITTLE_ENDIAN:", "if MACOS", "if NETBSD", "if OPENBSD", - "if ppid_map is None:", "if PY3:", "if SUNOS", - "if sys.platform.startswith", "if WINDOWS", + "if _WINDOWS:", + "if __name__ == .__main__.:", + "if enum is None:", + "if enum is not None:", + "if has_enums:", + "if ppid_map is None:", + "if sys.platform.startswith", "import enum", "pragma: no cover", "raise NotImplementedError", ] +omit = [ + "psutil/_compat.py", + "psutil/tests/*", + "setup.py", +] [tool.pylint.messages_control] # Important ones: @@ -79,17 +79,25 @@ ignore_messages = [ "Hyperlink target \".*?\" is not referenced", ] -[build-system] -requires = ["setuptools>=43", "wheel"] -build-backend = "setuptools.build_meta" +[tool.tomlsort] +in_place = true +no_sort_tables = true +sort_inline_arrays = true +spaces_before_inline_comment = 2 +spaces_indent_inline_array = 4 +trailing_comma_inline_array = true [tool.cibuildwheel] -skip = ["pp*", "*-musllinux*"] -test-extras = "test" +skip = ["*-musllinux*", "pp*"] test-command = [ "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/runner.py", - "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/test_memleaks.py" + "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/test_memleaks.py", ] +test-extras = "test" [tool.cibuildwheel.macos] -archs = ["x86_64", "arm64"] +archs = ["arm64", "x86_64"] + +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=43", "wheel"] diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 7ace3c08be..40eaee980f 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -16,6 +16,7 @@ - assert "isort" checks pass - assert C linter checks pass - assert RsT checks pass +- assert TOML checks pass - abort if files were added/renamed/removed and MANIFEST.in was not updated Install this with "make install-git-hooks". @@ -96,16 +97,61 @@ def git_commit_files(): os.path.exists(x)] rst_files = [x for x in out.split('\n') if x.endswith('.rst') and os.path.exists(x)] + toml_files = [ + x for x in out.split("\n") if x.endswith(".toml") and os.path.exists(x) + ] new_rm_mv = sh( ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] ) # XXX: we should escape spaces and possibly other amenities here new_rm_mv = new_rm_mv.split() - return (py_files, c_files, rst_files, new_rm_mv) + return (py_files, c_files, rst_files, toml_files, new_rm_mv) + + +def flake8(files): + assert os.path.exists('.flake8') + print("running flake8 (%s files)" % len(files)) + cmd = [PYTHON, "-m", "flake8", "--config=.flake8"] + files + if subprocess.call(cmd) != 0: + return sys.exit( + "python code didn't pass 'flake8' style check; " + + "try running 'make fix-flake8'" + ) + + +def isort(files): + print("running isort (%s)" % len(files)) + cmd = [PYTHON, "-m", "isort", "--check-only"] + files + if subprocess.call(cmd) != 0: + return sys.exit( + "python code didn't pass 'isort' style check; " + + "try running 'make fix-imports'") + + +def c_linter(files): + print("running clinter (%s)" % len(files)) + # XXX: we should escape spaces and possibly other amenities here + cmd = [PYTHON, "scripts/internal/clinter.py"] + files + if subprocess.call(cmd) != 0: + return sys.exit("C code didn't pass style check") + + +def toml_sort(files): + print("running toml linter (%s)" % len(files)) + cmd = ["toml-sort", "--check"] + files + if subprocess.call(cmd) != 0: + return sys.exit("%s didn't pass style check" % ' '.join(files)) + + +def rstcheck(files): + print("running rst linter (%s)" % len(files)) + cmd = ["rstcheck", "--config=pyproject.toml"] + files + if subprocess.call(cmd) != 0: + return sys.exit("RST code didn't pass style check") def main(): - py_files, c_files, rst_files, new_rm_mv = git_commit_files() + py_files, c_files, rst_files, toml_files, new_rm_mv = git_commit_files() # Check file content. for path in py_files: if os.path.realpath(path) == THIS_SCRIPT: @@ -127,40 +173,15 @@ def main(): # print("%s:%s %s" % (path, lineno, line)) # return sys.exit("bare except clause") - # Python linters if py_files: - # flake8 - assert os.path.exists('.flake8') - print("running flake8 (%s files)" % len(py_files)) - cmd = [PYTHON, "-m", "flake8", "--config=.flake8"] + py_files - ret = subprocess.call(cmd) - if ret != 0: - return sys.exit("python code didn't pass 'flake8' style check; " - "try running 'make fix-flake8'") - # isort - print("running isort (%s files)" % len(py_files)) - cmd = [PYTHON, "-m", "isort", "--check-only"] + py_files - ret = subprocess.call(cmd) - if ret != 0: - return sys.exit("python code didn't pass 'isort' style check; " - "try running 'make fix-imports'") - # C linter + flake8(py_files) + isort(py_files) if c_files: - print("running clinter (%s files)" % len(c_files)) - # XXX: we should escape spaces and possibly other amenities here - cmd = [PYTHON, "scripts/internal/clinter.py"] + c_files - ret = subprocess.call(cmd) - if ret != 0: - return sys.exit("C code didn't pass style check") - - # RST linter + c_linter(c_files) if rst_files: - print("running rst linter (%s)" % len(rst_files)) - cmd = ["rstcheck", "--config=pyproject.toml"] + rst_files - ret = subprocess.call(cmd) - if ret != 0: - return sys.exit("RST code didn't pass style check") - + rstcheck(rst_files) + if toml_files: + toml_sort(toml_files) if new_rm_mv: out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) with open_text('MANIFEST.in') as f: From 84cdeb426dc8f8083fffbbeff95c6ac7caa82e18 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Oct 2023 14:46:12 +0200 Subject: [PATCH 1026/1714] Add ruff Python linter, remove flake8 (#2312) See: https://blog.jerrycodes.com/ruff-the-python-linter/. Advantages: 1) an order of magnitude faster than flake8 + isort (0.1 secs instead of 2.5 secs to lint all .py files): Before: ``` $ time make flake8 isort real 0m2,554s user 0m15,626s sys 0m0,436s ``` Now: ``` $ time make ruff real 0m0,102s user 0m0,297s sys 0m0,025s ``` 2) A lot more code quality checks than before. Previously done also for pyftpdlib: https://github.com/giampaolo/pyftpdlib/pull/611. --- .flake8 | 34 -------- .github/workflows/build.yml | 2 +- .github/workflows/issues.py | 8 +- HISTORY.rst | 2 + MANIFEST.in | 1 - Makefile | 32 ++------ docs/DEVGUIDE.rst | 2 +- docs/conf.py | 15 ++-- psutil/__init__.py | 22 +++-- psutil/_common.py | 29 ++++--- psutil/_compat.py | 12 +-- psutil/_psaix.py | 23 +++--- psutil/_psbsd.py | 14 ++-- psutil/_pslinux.py | 34 ++++---- psutil/_psosx.py | 6 +- psutil/_psposix.py | 4 +- psutil/_pssunos.py | 14 ++-- psutil/_pswindows.py | 19 +++-- psutil/tests/__init__.py | 34 ++++---- psutil/tests/__main__.py | 5 +- psutil/tests/runner.py | 18 ++--- psutil/tests/test_bsd.py | 2 +- psutil/tests/test_connections.py | 14 ++-- psutil/tests/test_contracts.py | 8 +- psutil/tests/test_linux.py | 32 ++++---- psutil/tests/test_memleaks.py | 3 +- psutil/tests/test_misc.py | 26 +++--- psutil/tests/test_posix.py | 10 +-- psutil/tests/test_process.py | 5 +- psutil/tests/test_system.py | 6 +- psutil/tests/test_testutils.py | 16 ++-- psutil/tests/test_unicode.py | 7 +- psutil/tests/test_windows.py | 7 +- pyproject.toml | 85 +++++++++++++++++++- scripts/battery.py | 3 +- scripts/cpu_distribution.py | 3 +- scripts/disk_usage.py | 3 +- scripts/fans.py | 3 +- scripts/free.py | 3 +- scripts/ifconfig.py | 3 +- scripts/internal/bench_oneshot.py | 5 +- scripts/internal/bench_oneshot_2.py | 3 +- scripts/internal/check_broken_links.py | 13 ++- scripts/internal/clinter.py | 4 +- scripts/internal/convert_readme.py | 3 +- scripts/internal/download_wheels_appveyor.py | 16 ++-- scripts/internal/download_wheels_github.py | 23 +++--- scripts/internal/generate_manifest.py | 7 +- scripts/internal/git_pre_commit.py | 70 +++------------- scripts/internal/print_access_denied.py | 3 +- scripts/internal/print_announce.py | 8 +- scripts/internal/print_api_speed.py | 8 +- scripts/internal/print_downloads.py | 16 ++-- scripts/internal/print_hashes.py | 5 +- scripts/internal/print_timeline.py | 7 +- scripts/internal/purge_installation.py | 3 +- scripts/internal/winmake.py | 71 ++++++---------- scripts/iotop.py | 5 +- scripts/killall.py | 4 +- scripts/meminfo.py | 3 +- scripts/netstat.py | 3 +- scripts/nettop.py | 5 +- scripts/pidof.py | 4 +- scripts/pmap.py | 5 +- scripts/procinfo.py | 11 +-- scripts/procsmem.py | 3 +- scripts/ps.py | 3 +- scripts/pstree.py | 3 +- scripts/sensors.py | 8 +- scripts/temperatures.py | 3 +- scripts/top.py | 14 ++-- scripts/who.py | 3 +- scripts/winservices.py | 3 +- setup.py | 5 +- 74 files changed, 419 insertions(+), 500 deletions(-) delete mode 100644 .flake8 mode change 100644 => 100755 .github/workflows/issues.py mode change 100644 => 100755 psutil/tests/test_memleaks.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 8347d048a6..0000000000 --- a/.flake8 +++ /dev/null @@ -1,34 +0,0 @@ -# Configuration file for flake 8. This is used by "make lint" and by the -# GIT commit hook script. - -[flake8] -ignore = - # line break after binary operator - W504, - - # --- flake8-bugbear plugin - # Loop control variable 'keyword' not used within the loop body. If this is intended, start the name with an underscore. - B007, - # Redundant exception types in `except (IOError, OSError) as err:`. Write `except OSError as err:`, which catches exactly the same exceptions. - B014, - # Do not perform function calls in argument defaults. - B008, - - # --- flake8-blind-except plugin - # blind except Exception: statement - B902, - - # --- flake8-quotes plugin - # Double quotes found but single quotes preferred - Q000, - - # --- flake8-quotes naming; disable all except N804 and N805 - N801, N802, N803, N806, N807, N811, N812, N813, N814, N815, N816, N817, N818 - -per-file-ignores = - # T001, T201 = print() statement (flake8-print plugin) - setup.py:T001,T201 - scripts/*:T001,T201 - psutil/tests/runner.py:T001,T201 - psutil/tests/test_memleaks.py:T001,T201 - .github/workflows/*:T001,T201 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f3ad76d90..4bb8d108be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,7 +115,7 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install isort rstcheck toml-sort flake8 flake8-blind-except flake8-bugbear flake8-debugger flake8-print flake8-quotes sphinx + python3 -m pip install ruff rstcheck toml-sort sphinx make lint-all # Check sanity of .tar.gz + wheel files diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py old mode 100644 new mode 100755 index 77a9bc96b4..b89def6ffb --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Bot triggered by Github Actions every time a new issue, PR or comment +"""Bot triggered by Github Actions every time a new issue, PR or comment is created. Assign labels, provide replies, closes issues, etc. depending on the situation. """ @@ -144,10 +143,7 @@ def has_label(issue, label): def has_os_label(issue): labels = set([x.name for x in issue.labels]) - for label in OS_LABELS: - if label in labels: - return True - return False + return any(x in labels for x in OS_LABELS) def get_repo(): diff --git a/HISTORY.rst b/HISTORY.rst index 3b00b3b9d1..78e5332344 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX - 2246_: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) - 2290_: PID reuse is now pre-emptively checked for `Process.ppid()`_ and `Process.parents()`_. +- 2312_: use ``ruff`` Python linter instead of ``flake8 + isort``. It's an + order of magnitude faster + it adds a ton of new code quality checks. **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index 78cb836a6d..fb25643aff 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -include .flake8 include .gitignore include CONTRIBUTING.md include CREDITS diff --git a/Makefile b/Makefile index 95276f6648..862c8b6c47 100644 --- a/Makefile +++ b/Makefile @@ -9,19 +9,9 @@ TSCRIPT = psutil/tests/runner.py # Internal. PY3_DEPS = \ - autoflake \ - autopep8 \ check-manifest \ concurrencytest \ coverage \ - flake8 \ - flake8-blind-except \ - flake8-bugbear \ - flake8-debugger \ - flake8-print \ - flake8-quotes \ - isort \ - pep8-naming \ pylint \ pyperf \ pypinfo \ @@ -82,6 +72,7 @@ clean: ## Remove all build files. .coverage \ .failed-tests.txt \ .pytest_cache \ + .ruff_cache/ \ build/ \ dist/ \ docs/_build/ \ @@ -200,11 +191,8 @@ test-coverage: ## Run test coverage. # Linters # =================================================================== -flake8: ## Run flake8 linter. - @git ls-files '*.py' | xargs $(PYTHON) -m flake8 --config=.flake8 --jobs=${NUM_WORKERS} - -isort: ## Run isort linter. - @git ls-files '*.py' | xargs $(PYTHON) -m isort --check-only --jobs=${NUM_WORKERS} +ruff: ## Run ruff linter. + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --config=pyproject.toml --no-cache _pylint: ## Python pylint (not mandatory, just run it from time to time) @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} @@ -219,8 +207,7 @@ lint-toml: ## Linter for pyproject.toml @git ls-files '*.toml' | xargs toml-sort --check lint-all: ## Run all linters - ${MAKE} flake8 - ${MAKE} isort + ${MAKE} ruff ${MAKE} lint-c ${MAKE} lint-rst ${MAKE} lint-toml @@ -229,12 +216,8 @@ lint-all: ## Run all linters # Fixers # =================================================================== -fix-flake8: ## Run autopep8, fix some Python flake8 / pep8 issues. - @git ls-files '*.py' | xargs $(PYTHON) -m autopep8 --in-place --jobs=${NUM_WORKERS} --global-config=.flake8 - @git ls-files '*.py' | xargs $(PYTHON) -m autoflake --in-place --jobs=${NUM_WORKERS} --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys - -fix-imports: ## Fix imports with isort. - @git ls-files '*.py' | xargs $(PYTHON) -m isort --jobs=${NUM_WORKERS} +fix-ruff: + @git ls-files '*.py' | xargs $(PYTHON) -m ruff --config=pyproject.toml --no-cache --fix fix-unittests: ## Fix unittest idioms. @git ls-files '*test_*.py' | xargs $(PYTHON) -m teyit --show-stats @@ -243,8 +226,7 @@ fix-toml: ## Fix pyproject.toml @git ls-files '*.toml' | xargs toml-sort fix-all: ## Run all code fixers. - ${MAKE} fix-flake8 - ${MAKE} fix-imports + ${MAKE} fix-ruff ${MAKE} fix-unittests ${MAKE} fix-toml diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index c59502dfdc..a53235dab2 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -12,7 +12,7 @@ Once you have a compiler installed run: .. code-block:: bash git clone git@github.com:giampaolo/psutil.git - make setup-dev-env # install useful dev libs (flake8, coverage, ...) + make setup-dev-env # install useful dev libs (ruff, coverage, ...) make build make install make test diff --git a/docs/conf.py b/docs/conf.py index f0de77723b..9e0434706c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,22 +22,23 @@ # -- General configuration ------------------------------------------------ +import ast import datetime import os PROJECT_NAME = "psutil" -AUTHOR = u"Giampaolo Rodola" +AUTHOR = "Giampaolo Rodola" THIS_YEAR = str(datetime.datetime.now().year) HERE = os.path.abspath(os.path.dirname(__file__)) def get_version(): INIT = os.path.abspath(os.path.join(HERE, '../psutil/__init__.py')) - with open(INIT, 'r') as f: + with open(INIT) as f: for line in f: if line.startswith('__version__'): - ret = eval(line.strip().split(' = ')[1]) + ret = ast.literal_eval(line.strip().split(' = ')[1]) assert ret.count('.') == 2, ret for num in ret.split('.'): assert num.isdigit(), ret @@ -288,7 +289,7 @@ def get_version(): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'psutil.tex', u'psutil Documentation', + (master_doc, 'psutil.tex', 'psutil Documentation', AUTHOR, 'manual'), ] @@ -314,7 +315,7 @@ def get_version(): # # latex_appendices = [] -# It false, will not define \strong, \code, itleref, \crossref ... but only +# It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # @@ -330,7 +331,7 @@ def get_version(): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'psutil', u'psutil Documentation', + (master_doc, 'psutil', 'psutil Documentation', [author], 1) ] @@ -345,7 +346,7 @@ def get_version(): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'psutil', u'psutil Documentation', + (master_doc, 'psutil', 'psutil Documentation', author, 'psutil', 'One line description of project.', 'Miscellaneous'), ] diff --git a/psutil/__init__.py b/psutil/__init__.py index 5a5a20606a..5bf6501abb 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -227,8 +227,8 @@ # 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" % _psplatform.cext.__file__ + 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__) @@ -267,10 +267,7 @@ 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) @@ -279,7 +276,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. @@ -869,7 +866,8 @@ def cpu_num(self): 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: @@ -1333,7 +1331,7 @@ class Popen(Process): >>> p.username() 'giampaolo' >>> p.communicate() - ('hi\n', None) + ('hi', None) >>> p.terminate() >>> p.wait(timeout=2) 0 @@ -1384,7 +1382,7 @@ def __getattribute__(self, name): 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 @@ -2200,7 +2198,7 @@ def net_if_addrs(): if WINDOWS and fam == -1: fam = _psplatform.AF_LINK elif (hasattr(_psplatform, "AF_LINK") and - _psplatform.AF_LINK == fam): + 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 @@ -2425,7 +2423,7 @@ def test(): # pragma: no cover 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/psutil/_common.py b/psutil/_common.py index a0d49d6384..4057f541b6 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -37,7 +37,7 @@ # can't take it from _common.py as this script is imported by setup.py -PY3 = sys.version_info[0] == 3 +PY3 = sys.version_info[0] >= 3 if PY3: import enum else: @@ -280,13 +280,14 @@ 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: + if value: # noqa info[name] = value elif name == "pid" and value == 0: info[name] = value @@ -313,6 +314,7 @@ 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): @@ -329,6 +331,7 @@ class ZombieProcess(NoSuchProcess): 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): @@ -339,6 +342,7 @@ def __init__(self, pid, name=None, ppid=None, msg=None): class AccessDenied(Error): """Exception raised when permission to perform an action is denied.""" + __module__ = 'psutil' def __init__(self, pid=None, name=None, msg=None): @@ -352,6 +356,7 @@ 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): @@ -496,7 +501,8 @@ def wrapper(self): def cache_activate(proc): """Activate cache. Expects a Process instance. Cache will be - stored as a "_cache" instance attribute.""" + stored as a "_cache" instance attribute. + """ proc._cache = {} def cache_deactivate(proc): @@ -514,7 +520,7 @@ def cache_deactivate(proc): 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) @@ -528,8 +534,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) @@ -678,7 +684,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. @@ -689,7 +695,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] @@ -772,12 +778,12 @@ def open_text(fname): On Python 2 this is just an alias for open(name, 'rt'). """ if not PY3: - return open(fname, "rt", buffering=FILE_READ_BUFFER_SIZE) + 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, "rt", buffering=FILE_READ_BUFFER_SIZE, + 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: @@ -815,8 +821,7 @@ def bcat(fname, fallback=_DEFAULT): def bytes2human(n, format="%(value).1f%(symbol)s"): - """Used by various scripts. See: - http://goo.gl/zeJZl + """Used by various scripts. See: http://goo.gl/zeJZl. >>> bytes2human(10000) '9.8K' diff --git a/psutil/_compat.py b/psutil/_compat.py index 2531cf4b67..95754113d0 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -34,7 +34,7 @@ "InterruptedError", "ChildProcessError", "FileExistsError"] -PY3 = sys.version_info[0] == 3 +PY3 = sys.version_info[0] >= 3 _SENTINEL = object() if PY3: @@ -152,7 +152,7 @@ def __init__(self, *args, **kwargs): if not attr.startswith('__'): setattr(self, attr, getattr(unwrap_me, attr)) else: - super(TemporaryClass, self).__init__(*args, **kwargs) + super(TemporaryClass, self).__init__(*args, **kwargs) # noqa class __metaclass__(type): def __instancecheck__(cls, inst): @@ -222,7 +222,7 @@ def FileExistsError(inst): "CacheInfo", ["hits", "misses", "maxsize", "currsize"]) class _HashedSeq(list): - __slots__ = 'hashvalue' + __slots__ = ('hashvalue', ) def __init__(self, tup, hash=hash): self[:] = tup @@ -251,7 +251,7 @@ 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 = {} @@ -328,7 +328,7 @@ 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, @@ -337,7 +337,7 @@ def cache_info(): lock.release() def cache_clear(): - """Clear the cache and cache statistics""" + """Clear the cache and cache statistics.""" lock.acquire() try: cache.clear() diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 67f0314f74..6c2962c5e5 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -123,13 +123,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] @@ -144,13 +144,12 @@ def cpu_count_logical(): def cpu_count_cores(): - cmd = "lsdev -Cc processor" - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + 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() @@ -249,8 +248,8 @@ def net_if_stats(): 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) @@ -330,7 +329,7 @@ def wrapper(self, *args, **kwargs): return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] @@ -511,8 +510,8 @@ def open_files(self): 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) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index fb4217efeb..eeab18a9a7 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -8,9 +8,9 @@ import errno import functools import os -import xml.etree.ElementTree as ET from collections import defaultdict from collections import namedtuple +from xml.etree import ElementTree from . import _common from . import _psposix @@ -226,14 +226,14 @@ 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 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 @@ -249,7 +249,7 @@ 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__: @@ -284,7 +284,7 @@ def cpu_count_cores(): index = s.rfind("") if index != -1: s = s[:index + 9] - root = ET.fromstring(s) + root = ElementTree.fromstring(s) try: ret = len(root.findall('group/children/group/cpu')) or None finally: @@ -365,7 +365,7 @@ def cpu_freq(): 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() @@ -599,7 +599,7 @@ def wrap_exceptions_procfs(inst): raise AccessDenied(inst.pid, inst._name) -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" __slots__ = ["pid", "_name", "_ppid", "_cache"] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 0f102cbfa5..cc37c0615a 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -68,7 +68,8 @@ # 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" +] # ===================================================================== @@ -345,7 +346,7 @@ class StructRlimit(ctypes.Structure): def calculate_avail_vmem(mems): """Fallback for kernels < 3.14 where /proc/meminfo does not provide "MemAvailable", see: - https://blog.famzah.net/2014/09/24/ + 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/ @@ -549,8 +550,8 @@ 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) + 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: @@ -569,8 +570,8 @@ 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" + 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) @@ -710,8 +711,7 @@ def cpu_stats(): def _cpu_get_cpuinfo_freq(): - """Return current CPU frequency from cpuinfo if available. - """ + """Return current CPU frequency from cpuinfo if available.""" ret = [] with open_binary('%s/cpuinfo' % get_procfs_path()) as f: for line in f: @@ -958,7 +958,7 @@ def process_unix(file, family, inodes, filter_pid=None): raise RuntimeError( "error while parsing %s; malformed line %r" % ( file, line)) - if inode in inodes: + if inode in inodes: # noqa # With UNIX sockets we can have a single inode # referencing many file descriptors. pairs = inodes[inode] @@ -968,10 +968,7 @@ 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 = "" + 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: @@ -1191,8 +1188,9 @@ class RootFsDeviceFinder: 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/ + https://www.systutorials.com/how-to-find-the-disk-where-root-is-on-in-bash-on-linux/. """ + __slots__ = ['major', 'minor'] def __init__(self): @@ -1455,7 +1453,7 @@ 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() @@ -1664,7 +1662,7 @@ def wrapper(self, *args, **kwargs): return wrapper -class Process(object): +class Process: """Linux process implementation.""" __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] @@ -1901,7 +1899,7 @@ def memory_info(self): # ============================================================ 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]] + (int(x) * PAGESIZE for x in f.readline().split()[:7]) return pmem(rss, vms, shared, text, lib, data, dirty) if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS: @@ -1981,7 +1979,7 @@ def memory_full_info(self): 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. diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 8da2d9a328..482a9d430b 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -144,7 +144,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 @@ -174,7 +174,7 @@ 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_)] @@ -354,7 +354,7 @@ def wrapper(self, *args, **kwargs): return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" __slots__ = ["pid", "_name", "_ppid", "_cache"] diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 0039daf44a..1b26589dbf 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -78,7 +78,7 @@ def negsig_to_enum(num): def wait_pid(pid, timeout=None, proc_name=None, _waitpid=os.waitpid, - _timer=getattr(time, 'monotonic', time.time), + _timer=getattr(time, 'monotonic', time.time), # noqa: B008 _min=min, _sleep=time.sleep, _pid_exists=pid_exists): @@ -219,7 +219,7 @@ def disk_usage(path): @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/*') diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index d44bf2d788..291dc5a004 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -170,13 +170,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] @@ -369,7 +369,7 @@ def wrapper(self, *args, **kwargs): return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] @@ -617,13 +617,13 @@ 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, + 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) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index eec2db84db..6fd2b54bb1 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -195,7 +195,7 @@ 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.QueryDosDevice(rawdrive) @@ -348,7 +348,8 @@ def cpu_freq(): 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""" + over the last 1, 5, and 15 minutes respectively as a tuple. + """ global _loadavg_inititialized if not _loadavg_inititialized: @@ -493,7 +494,7 @@ def win_service_get(name): return service -class WindowsService(object): +class WindowsService: """Represents an installed Windows service.""" def __init__(self, name, display_name): @@ -701,7 +702,7 @@ def wrapper(self, *args, **kwargs): def retry_error_partial_copy(fun): """Workaround for https://github.com/giampaolo/psutil/issues/875. - See: https://stackoverflow.com/questions/4457745#4457745 + See: https://stackoverflow.com/questions/4457745#4457745. """ @functools.wraps(fun) def wrapper(self, *args, **kwargs): @@ -719,13 +720,15 @@ def wrapper(self, *args, **kwargs): else: raise else: - msg = "%s retried %s times, converted to AccessDenied as it's " \ - "still returning %r" % (fun, times, err) + 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(object): +class Process: """Wrapper class around underlying C implementation.""" __slots__ = ["pid", "_name", "_ppid", "_cache"] @@ -851,7 +854,7 @@ 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): diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 1aba0418fa..d4d8faf6df 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -4,9 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Test utilities. -""" +"""Test utilities.""" from __future__ import print_function @@ -368,9 +366,11 @@ def spawn_testproc(cmd=None, **kwds): testfn = get_testfn() try: safe_rmpath(testfn) - pyline = "from time import sleep;" \ - "open(r'%s', 'w').close();" \ - "sleep(60);" % testfn + pyline = ( + "from time import sleep;" + + "open(r'%s', 'w').close();" % testfn + + "sleep(60);" + ) cmd = [PYTHON_EXE, "-c", pyline] sproc = subprocess.Popen(cmd, **kwds) _subprocesses_started.add(sproc) @@ -478,7 +478,7 @@ def pyrun(src, **kwds): kwds.setdefault("stderr", None) srcfile = get_testfn() try: - with open(srcfile, 'wt') as f: + with open(srcfile, "w") as f: f.write(src) subp = spawn_testproc([PYTHON_EXE, f.name], **kwds) wait_for_pid(subp.pid) @@ -490,7 +490,7 @@ def pyrun(src, **kwds): @_reap_children_on_err def sh(cmd, **kwds): - """run cmd in a subprocess and return its output. + """Run cmd in a subprocess and return its output. raises RuntimeError on error. """ # Prevents subprocess to open error dialogs in case of error. @@ -670,10 +670,7 @@ def get_winver(): 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 + sp = int(r.group(0)) if r else 0 return (wv[0], wv[1], sp) @@ -682,7 +679,7 @@ def get_winver(): # =================================================================== -class retry(object): +class retry: """A retry decorator.""" def __init__(self, @@ -772,7 +769,7 @@ def call_until(fun, expr): expression is True. """ ret = fun() - assert eval(expr) + assert eval(expr) # noqa return ret @@ -849,7 +846,7 @@ def create_exe(outpath, c_code=None): } """) assert isinstance(c_code, str), c_code - with open(get_testfn(suffix='.c'), 'wt') as f: + with open(get_testfn(suffix='.c'), "w") as f: f.write(c_code) try: subprocess.check_call(["gcc", f.name, "-o", outpath]) @@ -894,7 +891,7 @@ def __str__(self): # assertRaisesRegexp renamed to assertRaisesRegex in 3.3; # add support for the new name. if not hasattr(unittest.TestCase, 'assertRaisesRegex'): - assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + assertRaisesRegex = unittest.TestCase.assertRaisesRegexp # noqa # ...otherwise multiprocessing.Pool complains if not PY3: @@ -1084,6 +1081,7 @@ class TestLeaks(psutil.tests.TestMemoryLeak): def test_fun(self): self.execute(some_function) """ + # Configurable class attrs. times = 200 warmup_times = 10 @@ -1316,6 +1314,7 @@ class process_namespace: >>> for fun, name in ns.iter(ns.getters): ... fun() """ + utils = [ ('cpu_percent', (), {}), ('memory_percent', (), {}), @@ -1452,6 +1451,7 @@ class system_namespace: >>> for fun, name in ns.iter(ns.getters): ... fun() """ + getters = [ ('boot_time', (), {}), ('cpu_count', (), {'logical': False}), @@ -1915,4 +1915,4 @@ def cleanup_test_procs(): # 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, frame: sys.exit(sig)) + signal.signal(signal.SIGTERM, lambda sig, _: sys.exit(sig)) diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index e67735275c..434515d211 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -4,9 +4,8 @@ # 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 +"""Run unit tests. This is invoked by: +$ python -m psutil.tests. """ from .runner import main diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 2e6f83e264..d3938b05a0 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -4,12 +4,11 @@ # 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: +"""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) +- re-run failed tests only (make test-failed). Invocation examples: - make test @@ -59,7 +58,7 @@ USE_COLORS = not CI_TESTING and term_supports_colors() HERE = os.path.abspath(os.path.dirname(__file__)) -loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase +loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase # noqa def cprint(msg, color, bold=False, file=None): @@ -108,7 +107,7 @@ def last_failed(self): suite = unittest.TestSuite() if not os.path.isfile(FAILED_TESTS_FNAME): return suite - with open(FAILED_TESTS_FNAME, 'rt') as f: + with open(FAILED_TESTS_FNAME) as f: names = f.read().split() for n in names: test = unittest.defaultTestLoader.loadTestsFromName(n) @@ -145,10 +144,11 @@ def printErrorList(self, 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. """ - 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): @@ -163,7 +163,7 @@ def _makeResult(self): def _write_last_failed(self): if self.failed_tnames: - with open(FAILED_TESTS_FNAME, 'wt') as f: + with open(FAILED_TESTS_FNAME, "w") as f: for tname in self.failed_tnames: f.write(tname + '\n') diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 69f50732b1..c5a5e7abc9 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -503,7 +503,7 @@ class NetBSDTestCase(PsutilTestCase): @staticmethod def parse_meminfo(look_for): - with open('/proc/meminfo', 'rt') as f: + with open('/proc/meminfo') as f: for line in f: if line.startswith(look_for): return int(line.split()[1]) * 1024 diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index aec164e857..13d1aebe9b 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -357,14 +357,14 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): # 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)) + 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)) + 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)) + 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)) + udp6_addr = eval(wait_for_file(testfile, delete=True)) # noqa else: tcp6_proc = None udp6_proc = None @@ -533,10 +533,10 @@ def test_connection_constants(self): ints.append(num) strs.append(str_) if SUNOS: - psutil.CONN_IDLE - psutil.CONN_BOUND + psutil.CONN_IDLE # noqa + psutil.CONN_BOUND # noqa if WINDOWS: - psutil.CONN_DELETE_TCB + psutil.CONN_DELETE_TCB # noqa if __name__ == '__main__': diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 0408078218..152a5b7391 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -6,7 +6,7 @@ """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 +Some of these are duplicates of tests test_system.py and test_process.py. """ import errno @@ -195,7 +195,7 @@ def test_cpu_num(self): def test_memory_maps(self): hasit = hasattr(psutil.Process, "memory_maps") self.assertEqual( - hasit, False if OPENBSD or NETBSD or AIX or MACOS else True) + hasit, not (OPENBSD or NETBSD or AIX or MACOS)) # =================================================================== @@ -206,7 +206,7 @@ def test_memory_maps(self): 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 + https://github.com/giampaolo/psutil/issues/1039. """ @classmethod @@ -297,7 +297,7 @@ def test_net_if_stats(self): @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(): + for ifname in psutil.net_io_counters(pernic=True): self.assertIsInstance(ifname, str) @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index adb6ff6064..55839a1baf 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -113,7 +113,7 @@ def get_ipv4_broadcast(ifname): def get_ipv6_addresses(ifname): - with open("/proc/net/if_inet6", 'rt') as f: + with open("/proc/net/if_inet6") as f: all_fields = [] for line in f.readlines(): fields = line.split() @@ -123,7 +123,7 @@ def get_ipv6_addresses(ifname): if len(all_fields) == 0: raise ValueError("could not find interface %r" % ifname) - for i in range(0, len(all_fields)): + for i in range(len(all_fields)): unformatted = all_fields[i][0] groups = [] for j in range(0, len(unformatted), 4): @@ -180,7 +180,7 @@ def free_physmem(): for line in lines: if line.startswith('Mem'): total, used, free, shared = \ - [int(x) for x in line.split()[1:5]] + (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) @@ -943,7 +943,7 @@ class TestLoadAvg(PsutilTestCase): @unittest.skipIf(not HAS_GETLOADAVG, "not supported") def test_getloadavg(self): psutil_value = psutil.getloadavg() - with open("/proc/loadavg", "r") as f: + with open("/proc/loadavg") as f: proc_value = f.read().split() self.assertAlmostEqual(float(proc_value[0]), psutil_value[0], delta=1) @@ -1015,7 +1015,7 @@ def test_against_ifconfig(self): def test_mtu(self): for name, stats in psutil.net_if_stats().items(): - with open("/sys/class/net/%s/mtu" % name, "rt") as f: + 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") @@ -1159,7 +1159,7 @@ def df(path): def test_zfs_fs(self): # Test that ZFS partitions are returned. - with open("/proc/filesystems", "r") as f: + with open("/proc/filesystems") as f: data = f.read() if 'zfs' in data: for part in psutil.disk_partitions(): @@ -1597,7 +1597,7 @@ def test_percent(self): 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"): + if name.endswith(('AC0/online', 'AC/online')): return io.BytesIO(b"1") else: return orig_open(name, *args, **kwargs) @@ -1614,7 +1614,7 @@ 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"): + if name.endswith(('AC0/online', 'AC/online')): raise IOError(errno.ENOENT, "") elif name.endswith("/status"): return io.StringIO(u("charging")) @@ -1630,7 +1630,7 @@ def open_mock(name, *args, **kwargs): 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"): + if name.endswith(('AC0/online', 'AC/online')): return io.BytesIO(b"0") else: return orig_open(name, *args, **kwargs) @@ -1645,7 +1645,7 @@ 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"): + if name.endswith(('AC0/online', 'AC/online')): raise IOError(errno.ENOENT, "") elif name.endswith("/status"): return io.StringIO(u("discharging")) @@ -1662,8 +1662,10 @@ 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"): + 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"???") @@ -1777,7 +1779,7 @@ def open_mock(name, *args, **kwargs): return orig_open(name, *args, **kwargs) def glob_mock(path): - if path == '/sys/class/hwmon/hwmon*/temp*_*': + if path == '/sys/class/hwmon/hwmon*/temp*_*': # noqa return [] elif path == '/sys/class/hwmon/hwmon*/device/temp*_*': return [] @@ -1896,7 +1898,7 @@ def get_test_file(fname): testfn = self.get_testfn() with open(testfn, "w"): self.assertEqual(get_test_file(testfn).mode, "w") - with open(testfn, "r"): + with open(testfn): self.assertEqual(get_test_file(testfn).mode, "r") with open(testfn, "a"): self.assertEqual(get_test_file(testfn).mode, "a") @@ -2178,7 +2180,7 @@ def test_status_file_parsing(self): 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))) + 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 diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py old mode 100644 new mode 100755 index dbd1588dfe..cd1b3f290e --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -4,8 +4,7 @@ # 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 +"""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. diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 53c6401216..b3515de31c 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -5,9 +5,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Miscellaneous tests. -""" +"""Miscellaneous tests.""" import ast import collections @@ -331,12 +329,12 @@ def run_against(self, obj, expected_retval=None): self.assertEqual(ret, expected_retval) self.assertEqual(len(self.calls), 4) # docstring - self.assertEqual(obj.__doc__, "my docstring") + self.assertEqual(obj.__doc__, "My docstring.") def test_function(self): @memoize def foo(*args, **kwargs): - """my docstring""" + """My docstring.""" baseclass.calls.append((args, kwargs)) return 22 @@ -346,7 +344,7 @@ def foo(*args, **kwargs): def test_class(self): @memoize class Foo: - """my docstring""" + """My docstring.""" def __init__(self, *args, **kwargs): baseclass.calls.append((args, kwargs)) @@ -376,7 +374,7 @@ class Foo: @staticmethod @memoize def bar(*args, **kwargs): - """my docstring""" + """My docstring.""" baseclass.calls.append((args, kwargs)) return 22 @@ -388,7 +386,7 @@ class Foo: @classmethod @memoize def bar(cls, *args, **kwargs): - """my docstring""" + """My docstring.""" baseclass.calls.append((args, kwargs)) return 22 @@ -400,7 +398,7 @@ def test_original(self): # against different types. Keeping it anyway. @memoize def foo(*args, **kwargs): - """foo docstring""" + """Foo docstring.""" calls.append(None) return (args, kwargs) @@ -430,7 +428,7 @@ def foo(*args, **kwargs): self.assertEqual(ret, expected) self.assertEqual(len(calls), 4) # docstring - self.assertEqual(foo.__doc__, "foo docstring") + self.assertEqual(foo.__doc__, "Foo docstring.") class TestCommonModule(PsutilTestCase): @@ -563,7 +561,7 @@ def test_debug(self): def test_cat_bcat(self): testfn = self.get_testfn() - with open(testfn, "wt") as f: + with open(testfn, "w") as f: f.write("foo") self.assertEqual(cat(testfn), "foo") self.assertEqual(bcat(testfn), b"foo") @@ -845,11 +843,7 @@ def assert_stdout(exe, *args, **kwargs): @staticmethod def assert_syntax(exe): exe = os.path.join(SCRIPTS_DIR, exe) - if PY3: - f = open(exe, 'rt', encoding='utf8') - else: - f = open(exe, 'rt') - with f: + with open(exe, encoding="utf8") if PY3 else open(exe) as f: src = f.read() ast.parse(src) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index fe6936de56..eaaf0d1c2a 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -43,8 +43,7 @@ def ps(fmt, pid=None): - """ - Wrapper for calling the ps command with a little bit of cross-platform + """Wrapper for calling the ps command with a little bit of cross-platform support for a narrow range of features. """ @@ -68,10 +67,7 @@ def ps(fmt, pid=None): output = sh(cmd) - if LINUX: - output = output.splitlines() - else: - output = output.splitlines()[1:] + output = output.splitlines() if LINUX else output.splitlines()[1:] all_output = [] for line in output: @@ -324,7 +320,7 @@ def test_pids(self): @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 nic in psutil.net_io_counters(pernic=True): for line in output.split(): if line.startswith(nic): break diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ae0192708a..6a90c5b931 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -964,7 +964,7 @@ def test_cpu_affinity_all_combinations(self): if len(initial) > 12: initial = initial[:12] # ...otherwise it will take forever combos = [] - for i in range(0, len(initial) + 1): + for i in range(len(initial) + 1): for subset in itertools.combinations(initial, i): if subset: combos.append(list(subset)) @@ -1462,6 +1462,7 @@ class LimitedUserTestCase(TestProcess): 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() @@ -1525,7 +1526,7 @@ def test_misc(self): stderr=subprocess.PIPE, env=PYTHON_EXE_ENV) as proc: proc.name() proc.cpu_times() - proc.stdin + proc.stdin # noqa self.assertTrue(dir(proc)) self.assertRaises(AttributeError, getattr, proc, 'foo') proc.terminate() diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 80e4988d53..68bfc24c96 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -216,8 +216,8 @@ def test_users(self): self.assertIsInstance(user.terminal, (str, type(None))) if user.host is not None: self.assertIsInstance(user.host, (str, type(None))) - user.terminal - user.host + user.terminal # noqa + user.host # noqa assert user.started > 0.0, user datetime.datetime.fromtimestamp(user.started) if WINDOWS or OPENBSD: @@ -617,7 +617,7 @@ def check_ntuple(nt): else: # we cannot make any assumption about this, see: # http://goo.gl/p9c43 - disk.device + disk.device # noqa # on modern systems mount points can also be files assert os.path.exists(disk.mountpoint), disk assert disk.fstype, disk diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 65ca0c4d40..bff43b1c9b 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -5,9 +5,7 @@ # 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). -""" +"""Tests for testing utils (psutil.tests namespace).""" import collections import contextlib @@ -71,7 +69,7 @@ def test_retry_success(self, sleep): def foo(): while queue: queue.pop() - 1 / 0 + 1 / 0 # noqa return 1 queue = list(range(3)) @@ -85,7 +83,7 @@ def test_retry_failure(self, sleep): def foo(): while queue: queue.pop() - 1 / 0 + 1 / 0 # noqa return 1 queue = list(range(6)) @@ -107,7 +105,7 @@ def test_no_interval_arg(self, sleep): @retry(retries=5, interval=None, logfun=None) def foo(): - 1 / 0 + 1 / 0 # noqa self.assertRaises(ZeroDivisionError, foo) self.assertEqual(sleep.call_count, 0) @@ -117,7 +115,7 @@ def test_retries_arg(self, sleep): @retry(retries=5, interval=1, logfun=None) def foo(): - 1 / 0 + 1 / 0 # noqa self.assertRaises(ZeroDivisionError, foo) self.assertEqual(sleep.call_count, 5) @@ -170,7 +168,7 @@ class TestFSTestUtils(PsutilTestCase): def test_open_text(self): with open_text(__file__) as f: - self.assertEqual(f.mode, 'rt') + self.assertEqual(f.mode, 'r') def test_open_binary(self): with open_binary(__file__) as f: @@ -407,7 +405,7 @@ def fun(): def test_execute_w_exc(self): def fun_1(): - 1 / 0 + 1 / 0 # noqa self.execute_w_exc(ZeroDivisionError, fun_1) with self.assertRaises(ZeroDivisionError): self.execute_w_exc(OSError, fun_1) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index c7d8dfbc0b..cf9500a3f8 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -5,9 +5,8 @@ # 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 -====================================== +"""Notes about unicode handling in psutil +======================================. Starting from version 5.3.0 psutil adds unicode support, see: https://github.com/giampaolo/psutil/issues/1040 @@ -309,6 +308,7 @@ def normpath(p): @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): @@ -323,6 +323,7 @@ def expect_exact_path_match(self): class TestNonFSAPIS(BaseUnicodeTest): """Unicode tests for non fs-related APIs.""" + funky_suffix = UNICODE_SUFFIX if PY3 else 'è' @unittest.skipIf(not HAS_ENVIRON, "not supported") diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index bb6faabe2e..47d6ad3f13 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -379,7 +379,7 @@ def test_special_pid(self): 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'): + if platform.uname()[1] not in ('vista', 'win-7', 'win7'): raise else: self.assertGreater(rss, 0) @@ -625,14 +625,13 @@ def test_create_time(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") class TestDualProcessImplementation(PsutilTestCase): - """ - Certain APIs on Windows have 2 internal implementations, one + """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 + https://github.com/giampaolo/psutil/issues/304. """ @classmethod diff --git a/pyproject.toml b/pyproject.toml index bf94359582..d99de42717 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,85 @@ -[tool.isort] -force_single_line = true # one import per line -lines_after_imports = 2 # blank spaces after import section +[tool.ruff] +# https://beta.ruff.rs/docs/settings/ +target-version = "py37" +line-length = 79 +select = [ + # To get a list of all values: `python3 -m ruff linter`. + "ALL", + "D200", # [*] One-line docstring should fit on one line + "D204", # [*] 1 blank line required after class docstring + "D209", # [*] Multi-line docstring closing quotes should be on a separate line + "D212", # [*] Multi-line docstring summary should start at the first line + "D301", # Use `r"""` if any backslashes in a docstring + "D403", # [*] First word of the first line should be capitalized + "PERF102", # [*] When using only the keys of a dict use the `keys()` method + "S113", # Probable use of requests call without timeout + "S602", # `subprocess` call with `shell=True` identified, security issue +] +ignore = [ + "A", # flake8-builtins + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "B007", # Loop control variable `x` not used within loop body + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` (PYTHON2.7 COMPAT) + "BLE001", # Do not catch blind exception: `Exception` + "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) + "C408", # Unnecessary `dict` call (rewrite as a literal) + "C90", # mccabe (function `X` is too complex) + "COM812", # Trailing comma missing + "D", # pydocstyle + "DTZ", # flake8-datetimez + "EM", # flake8-errmsg + "ERA001", # Found commented-out code + "FBT", # flake8-boolean-trap (makes zero sense) + "FIX", # Line contains TODO / XXX / ..., consider resolving the issue + "FLY", # flynt (PYTHON2.7 COMPAT) + "INP", # flake8-no-pep420 + "N801", # Class name `async_chat` should use CapWords convention (ASYNCORE COMPAT) + "N802", # Function name X should be lowercase. + "N803", # Argument name X should be lowercase. + "N806", # Variable X in function should be lowercase. + "N818", # Exception name `FooBar` should be named with an Error suffix + "PERF", # Perflint + "PGH004", # Use specific rule codes when using `noqa` + "PLR", # pylint + "PLW", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "Q000", # Single quotes found but double quotes preferred + "RET", # flake8-return + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM102", # Use a single `if` statement instead of nested `if` statements + "SIM105", # Use `contextlib.suppress(OSError)` instead of `try`-`except`-`pass` + "SIM115", # Use context handler for opening files + "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements + "SLF", # flake8-self + "TD", # all TODOs, XXXs, etc. + "TRY003", # Avoid specifying long messages outside the exception class + "TRY200", # Use `raise from` to specify exception cause (PYTHON2.7 COMPAT) + "TRY300", # Consider moving this statement to an `else` block + "TRY301", # Abstract `raise` to an inner function + "UP009", # [*] UTF-8 encoding declaration is unnecessary (PYTHON2.7 COMPAT) + "UP010", # [*] Unnecessary `__future__` import `print_function` for target Python version (PYTHON2.7 COMPAT) + "UP024", # [*] Replace aliased errors with `OSError` (PYTHON2.7 COMPAT) + "UP028", # [*] Replace `yield` over `for` loop with `yield from` (PYTHON2.7 COMPAT) + "UP031", # [*] Use format specifiers instead of percent format + "UP032", # [*] Use f-string instead of `format` call (PYTHON2.7 COMPAT) +] + +[tool.ruff.per-file-ignores] +# T201 == print(), T203 == pprint() +".github/workflows/*" = ["T201", "T203"] +"psutil/tests/runner.py" = ["T201", "T203"] +"scripts/*" = ["T201", "T203"] +"scripts/internal/*" = ["T201", "T203"] +"setup.py" = ["T201", "T203"] + +[tool.ruff.isort] +# https://beta.ruff.rs/docs/settings/#isort +force-single-line = true # one import per line +lines-after-imports = 2 [tool.coverage.report] exclude_lines = [ diff --git a/scripts/battery.py b/scripts/battery.py index bf0503e0e8..040f948195 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Show battery information. +"""Show battery information. $ python3 scripts/battery.py charge: 74% diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index ba71ca9cc8..bfbb14b6ca 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Shows CPU workload split across different CPUs. +"""Shows CPU workload split across different CPUs. $ python3 scripts/cpu_workload.py CPU 0 CPU 1 CPU 2 CPU 3 CPU 4 CPU 5 CPU 6 CPU 7 diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 65ae313821..d801c6dddb 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -List all mounted disk partitions a-la "df -h" command. +"""List all mounted disk partitions a-la "df -h" command. $ python3 scripts/disk_usage.py Device Total Used Free Use % Type Mount diff --git a/scripts/fans.py b/scripts/fans.py index 304277157c..a9a8b8e671 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Show fans information. +"""Show fans information. $ python fans.py asus diff --git a/scripts/free.py b/scripts/free.py index 8c3359d869..f72149ac30 100755 --- a/scripts/free.py +++ b/scripts/free.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'free' cmdline utility. +"""A clone of 'free' cmdline utility. $ python3 scripts/free.py total used free shared buffers cache diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index 23fd26b477..7fdfa1e124 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'ifconfig' on UNIX. +"""A clone of 'ifconfig' on UNIX. $ python3 scripts/ifconfig.py lo: diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 6059587605..74f8150ae2 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -4,10 +4,9 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A simple micro benchmark script which prints the speedup when using +"""A simple micro benchmark script which prints the speedup when using Process.oneshot() ctx manager. -See: https://github.com/giampaolo/psutil/issues/799 +See: https://github.com/giampaolo/psutil/issues/799. """ from __future__ import division diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index 051d00360b..2a63dca254 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Same as bench_oneshot.py but uses perf module instead, which is +"""Same as bench_oneshot.py but uses perf module instead, which is supposed to be more precise. """ diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index f5375a860a..315f06fa1f 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -4,8 +4,7 @@ # All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. -""" -Checks for broken links in file names specified as command line +"""Checks for broken links in file names specified as command line parameters. There are a ton of a solutions available for validating URLs in string @@ -161,7 +160,7 @@ def parse_c(fname): def parse_generic(fname): - with open(fname, 'rt', errors='ignore') as f: + with open(fname, errors='ignore') as f: text = f.read() return find_urls(text) @@ -172,10 +171,10 @@ def get_urls(fname): return parse_rst(fname) elif fname.endswith('.py'): return parse_py(fname) - elif fname.endswith('.c') or fname.endswith('.h'): + elif fname.endswith(('.c', '.h')): return parse_c(fname) else: - with open(fname, 'rt', errors='ignore') as f: + with open(fname, errors='ignore') as f: if f.readline().strip().startswith('#!/usr/bin/env python3'): return parse_py(fname) return parse_generic(fname) @@ -198,8 +197,8 @@ def validate_url(url): def parallel_validator(urls): - """validates all urls in parallel - urls: tuple(filename, url) + """Validates all urls in parallel + urls: tuple(filename, url). """ fails = [] # list of tuples (filename, url) current = 0 diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py index 384951da89..71c77e4027 100755 --- a/scripts/internal/clinter.py +++ b/scripts/internal/clinter.py @@ -54,13 +54,13 @@ def check_line(path, line, idx, lines): warn(path, line, lineno, "no blank line at EOF") ss = s.strip() - if ss.startswith(("printf(", "printf (", )): + if ss.startswith(("printf(", "printf (")): if not ss.endswith(("// NOQA", "// NOQA")): warn(path, line, lineno, "printf() statement") def process(path): - with open(path, 'rt') as f: + with open(path) as f: lines = f.readlines() for idx, line in enumerate(lines): check_line(path, line, idx, lines) diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index d96c6c5d36..0c4fade509 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Remove raw HTML from README.rst to make it compatible with PyPI on +"""Remove raw HTML from README.rst to make it compatible with PyPI on dist upload. """ diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index dcded559b3..47a33d9964 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -4,12 +4,11 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Script which downloads wheel files hosted on AppVeyor: +"""Script which downloads wheel files hosted on AppVeyor: https://ci.appveyor.com/project/giampaolo/psutil Re-adapted from the original recipe of Ibarra Corretge' : -http://code.saghul.net/index.php/2015/09/09/ +http://code.saghul.net/index.php/2015/09/09/. """ from __future__ import print_function @@ -20,13 +19,14 @@ import requests -from psutil import __version__ as PSUTIL_VERSION +from psutil import __version__ from psutil._common import bytes2human from psutil._common import print_color USER = "giampaolo" PROJECT = "psutil" +PROJECT_VERSION = __version__ BASE_URL = 'https://ci.appveyor.com/api' PY_VERSIONS = ['2.7'] TIMEOUT = 30 @@ -71,12 +71,12 @@ def get_file_urls(): def rename_win27_wheels(): # See: https://github.com/giampaolo/psutil/issues/810 - src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION + src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PROJECT_VERSION + dst = 'dist/psutil-%s-cp27-none-win32.whl' % PROJECT_VERSION print("rename: %s\n %s" % (src, dst)) os.rename(src, dst) - src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PSUTIL_VERSION + src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PROJECT_VERSION + dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PROJECT_VERSION print("rename: %s\n %s" % (src, dst)) os.rename(src, dst) diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py index 00f57116f6..de6c34faa2 100755 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels_github.py @@ -4,15 +4,14 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Script which downloads wheel files hosted on GitHub: +"""Script which downloads wheel files hosted on GitHub: https://github.com/giampaolo/psutil/actions It needs an access token string generated from personal GitHub profile: https://github.com/settings/tokens The token must be created with at least "public_repo" scope/rights. If you lose it, just generate a new token. REST API doc: -https://developer.github.com/v3/actions/artifacts/ +https://developer.github.com/v3/actions/artifacts/. """ import argparse @@ -23,21 +22,24 @@ import requests -from psutil import __version__ as PSUTIL_VERSION +from psutil import __version__ from psutil._common import bytes2human from psutil.tests import safe_rmpath USER = "giampaolo" PROJECT = "psutil" +PROJECT_VERSION = __version__ OUTFILE = "wheels-github.zip" TOKEN = "" +TIMEOUT = 30 def get_artifacts(): base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) url = base_url + "/actions/artifacts" - res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res = requests.get(url=url, headers={ + "Authorization": "token %s" % TOKEN}, timeout=TIMEOUT) res.raise_for_status() data = json.loads(res.content) return data @@ -45,7 +47,8 @@ def get_artifacts(): def download_zip(url): print("downloading: " + url) - res = requests.get(url=url, headers={"Authorization": "token %s" % TOKEN}) + res = requests.get(url=url, headers={ + "Authorization": "token %s" % TOKEN}, timeout=TIMEOUT) res.raise_for_status() totbytes = 0 with open(OUTFILE, 'wb') as f: @@ -57,13 +60,13 @@ def download_zip(url): def rename_win27_wheels(): # See: https://github.com/giampaolo/psutil/issues/810 - src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win32.whl' % PSUTIL_VERSION + src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PROJECT_VERSION + dst = 'dist/psutil-%s-cp27-none-win32.whl' % PROJECT_VERSION if os.path.exists(src): print("rename: %s\n %s" % (src, dst)) os.rename(src, dst) - src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PSUTIL_VERSION - dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PSUTIL_VERSION + src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PROJECT_VERSION + dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PROJECT_VERSION if os.path.exists(src): print("rename: %s\n %s" % (src, dst)) os.rename(src, dst) diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index b7ad8c7e1e..290e8b49fd 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -4,11 +4,10 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Generate MANIFEST.in file. -""" +"""Generate MANIFEST.in file.""" import os +import shlex import subprocess @@ -19,7 +18,7 @@ def sh(cmd): return subprocess.check_output( - cmd, shell=True, universal_newlines=True).strip() + shlex.split(cmd), universal_newlines=True).strip() def main(): diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 40eaee980f..92852f8362 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -4,22 +4,10 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -This gets executed on 'git commit' and rejects the commit in case the -submitted code does not pass validation. Validation is run only against -the files which were modified in the commit. Checks: - -- assert no space at EOLs -- assert not pdb.set_trace in code -- assert no bare except clause ("except:") in code -- assert "flake8" checks pass -- assert "isort" checks pass -- assert C linter checks pass -- assert RsT checks pass -- assert TOML checks pass -- abort if files were added/renamed/removed and MANIFEST.in was not updated - -Install this with "make install-git-hooks". +"""This gets executed on 'git commit' and rejects the commit in case +the submitted code does not pass validation. Validation is run only +against the files which were modified in the commit. Install this with +"make install-git-hooks". """ from __future__ import print_function @@ -31,7 +19,7 @@ PYTHON = sys.executable -PY3 = sys.version_info[0] == 3 +PY3 = sys.version_info[0] >= 3 THIS_SCRIPT = os.path.realpath(__file__) @@ -86,7 +74,7 @@ def sh(cmd): def open_text(path): kw = {'encoding': 'utf8'} if PY3 else {} - return open(path, 'rt', **kw) + return open(path, **kw) def git_commit_files(): @@ -108,26 +96,16 @@ def git_commit_files(): return (py_files, c_files, rst_files, toml_files, new_rm_mv) -def flake8(files): - assert os.path.exists('.flake8') - print("running flake8 (%s files)" % len(files)) - cmd = [PYTHON, "-m", "flake8", "--config=.flake8"] + files +def ruff(files): + print("running ruff (%s)" % len(files)) + cmd = [PYTHON, "-m", "ruff", "check", "--no-cache"] + files if subprocess.call(cmd) != 0: - return sys.exit( - "python code didn't pass 'flake8' style check; " + - "try running 'make fix-flake8'" + return exit( + "Python code didn't pass 'ruff' style check." + "Try running 'make fix-ruff'." ) -def isort(files): - print("running isort (%s)" % len(files)) - cmd = [PYTHON, "-m", "isort", "--check-only"] + files - if subprocess.call(cmd) != 0: - return sys.exit( - "python code didn't pass 'isort' style check; " + - "try running 'make fix-imports'") - - def c_linter(files): print("running clinter (%s)" % len(files)) # XXX: we should escape spaces and possibly other amenities here @@ -152,30 +130,8 @@ def rstcheck(files): def main(): py_files, c_files, rst_files, toml_files, new_rm_mv = git_commit_files() - # Check file content. - for path in py_files: - if os.path.realpath(path) == THIS_SCRIPT: - continue - with open_text(path) as f: - lines = f.readlines() - for lineno, line in enumerate(lines, 1): - # space at end of line - if line.endswith(' '): - print("%s:%s %r" % (path, lineno, line)) - return sys.exit("space at end of line") - line = line.rstrip() - # # pdb (now provided by flake8-debugger plugin) - # if "pdb.set_trace" in line: - # print("%s:%s %s" % (path, lineno, line)) - # return sys.exit("you forgot a pdb in your python code") - # # bare except clause (now provided by flake8-blind-except plugin) - # if "except:" in line and not line.endswith("# NOQA"): - # print("%s:%s %s" % (path, lineno, line)) - # return sys.exit("bare except clause") - if py_files: - flake8(py_files) - isort(py_files) + ruff(py_files) if c_files: c_linter(c_files) if rst_files: diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index f3d0166e50..7759ca7b2c 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Helper script iterates over all processes and . +"""Helper script iterates over all processes and . It prints how many AccessDenied exceptions are raised in total and for what Process method. diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 33fca4220d..2297c09506 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -4,9 +4,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Prints release announce based on HISTORY.rst file content. -See: https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode +"""Prints release announce based on HISTORY.rst file content. +See: https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode. """ import os @@ -14,7 +13,7 @@ import subprocess import sys -from psutil import __version__ as PRJ_VERSION +from psutil import __version__ HERE = os.path.abspath(os.path.dirname(__file__)) @@ -24,6 +23,7 @@ ROOT, 'scripts', 'internal', 'print_hashes.py') PRJ_NAME = 'psutil' +PRJ_VERSION = __version__ PRJ_URL_HOME = 'https://github.com/giampaolo/psutil' PRJ_URL_DOC = 'http://psutil.readthedocs.io' PRJ_URL_DOWNLOAD = 'https://pypi.org/project/psutil/#files' diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index e85b703820..8abaed0c48 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Benchmark all API calls and print them from fastest to slowest. +"""Benchmark all API calls and print them from fastest to slowest. $ make print_api_speed SYSTEM APIS NUM CALLS SECONDS @@ -192,8 +191,9 @@ def main(): print_timings() if not prio_set: - print_color("\nWARN: couldn't set highest process priority " + - "(requires root)", "red") + msg = "\nWARN: couldn't set highest process priority " + msg += "(requires root)" + print_color(msg, "red") if __name__ == '__main__': diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index b6df3b38c6..1ee37e534a 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -4,18 +4,18 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Print PYPI statistics in MarkDown format. +"""Print PYPI statistics in MarkDown format. Useful sites: * https://pepy.tech/project/psutil * https://pypistats.org/packages/psutil -* https://hugovk.github.io/top-pypi-packages/ +* https://hugovk.github.io/top-pypi-packages/. """ from __future__ import print_function import json import os +import shlex import subprocess import sys @@ -28,8 +28,10 @@ PKGNAME = 'psutil' DAYS = 30 LIMIT = 100 -GITHUB_SCRIPT_URL = "https://github.com/giampaolo/psutil/blob/master/" \ - "scripts/internal/pypistats.py" +GITHUB_SCRIPT_URL = ( + "https://github.com/giampaolo/psutil/blob/master/" + "scripts/internal/pypistats.py" +) LAST_UPDATE = None bytes_billed = 0 @@ -41,7 +43,7 @@ def sh(cmd): assert os.path.exists(AUTH_FILE) env = os.environ.copy() env['GOOGLE_APPLICATION_CREDENTIALS'] = AUTH_FILE - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, + p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) stdout, stderr = p.communicate() if p.returncode != 0: @@ -108,7 +110,7 @@ def downloads_by_distro(): def print_row(left, right): if isinstance(right, int): - right = '{0:,}'.format(right) + right = '{:,}'.format(right) print(templ % (left, right)) diff --git a/scripts/internal/print_hashes.py b/scripts/internal/print_hashes.py index 69bc9edbe0..68e06201c6 100755 --- a/scripts/internal/print_hashes.py +++ b/scripts/internal/print_hashes.py @@ -4,9 +4,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Prints files hashes, see: -https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode +"""Prints files hashes, see: +https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode. """ import argparse diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index 0ea7355eb3..a046e670a1 100755 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -4,10 +4,9 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Prints releases' timeline in RST format. -""" +"""Prints releases' timeline in RST format.""" +import shlex import subprocess @@ -20,7 +19,7 @@ def sh(cmd): return subprocess.check_output( - cmd, shell=True, universal_newlines=True).strip() + shlex.split(cmd), universal_newlines=True).strip() def get_tag_date(tag): diff --git a/scripts/internal/purge_installation.py b/scripts/internal/purge_installation.py index 8a9597f0b4..55b2f5c504 100755 --- a/scripts/internal/purge_installation.py +++ b/scripts/internal/purge_installation.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Purge psutil installation by removing psutil-related files and +"""Purge psutil installation by removing psutil-related files and directories found in site-packages directories. This is needed mainly because sometimes "import psutil" imports a leftover installation from site-packages directory instead of the main working directory. diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 5ec2ecbfd2..6978a7e617 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -28,23 +28,15 @@ APPVEYOR = bool(os.environ.get('APPVEYOR')) -if APPVEYOR: - PYTHON = sys.executable -else: - PYTHON = os.getenv('PYTHON', sys.executable) +PYTHON = sys.executable if APPVEYOR else os.getenv('PYTHON', sys.executable) RUNNER_PY = 'psutil\\tests\\runner.py' GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -PY3 = sys.version_info[0] == 3 +PY3 = sys.version_info[0] >= 3 HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) PYPY = '__pypy__' in sys.builtin_module_names DEPS = [ "coverage", - "flake8", - "flake8-blind-except", - "flake8-debugger", - "flake8-print", - "nose", "pdbpp", "pip", "pyperf", @@ -58,8 +50,6 @@ DEPS.append('mock') DEPS.append('ipaddress') DEPS.append('enum34') -else: - DEPS.append('flake8-bugbear') if not PYPY: DEPS.append("pywin32") @@ -124,7 +114,7 @@ def win_colorprint(s, color=LIGHTBLUE): def sh(cmd, nolog=False): if not nolog: safe_print("cmd: " + cmd) - p = subprocess.Popen(cmd, shell=True, env=os.environ, cwd=os.getcwd()) + p = subprocess.Popen(cmd, shell=True, env=os.environ, cwd=os.getcwd()) # noqa p.communicate() if p.returncode != 0: sys.exit(p.returncode) @@ -198,7 +188,7 @@ def onerror(fun, path, excinfo): def recursive_rm(*patterns): """Recursively remove a file or matching a list of patterns.""" - for root, dirs, files in os.walk(u'.'): + for root, dirs, files in os.walk('.'): root = os.path.normpath(root) if root.startswith('.git/'): continue @@ -218,7 +208,7 @@ def recursive_rm(*patterns): def build(): - """Build / compile""" + """Build / compile.""" # Make sure setuptools is installed (needed for 'develop' / # edit mode). sh('%s -c "import setuptools"' % PYTHON) @@ -269,7 +259,7 @@ def upload_wheels(): def install_pip(): - """Install pip""" + """Install pip.""" try: sh('%s -c "import pip"' % PYTHON) except SystemExit: @@ -298,13 +288,13 @@ def install_pip(): def install(): - """Install in develop / edit mode""" + """Install in develop / edit mode.""" build() sh("%s setup.py develop" % PYTHON) def uninstall(): - """Uninstall psutil""" + """Uninstall psutil.""" # Uninstalling psutil on Windows seems to be tricky. # On "import psutil" tests may import a psutil version living in # C:\PythonXY\Lib\site-packages which is not what we want, so @@ -333,7 +323,7 @@ def uninstall(): # easy_install can add a line (installation path) into # easy-install.pth; that line alters sys.path. path = os.path.join(dir, name) - with open(path, 'rt') as f: + with open(path) as f: lines = f.readlines() hasit = False for line in lines: @@ -341,7 +331,7 @@ def uninstall(): hasit = True break if hasit: - with open(path, 'wt') as f: + with open(path, "w") as f: for line in lines: if 'psutil' not in line: f.write(line) @@ -350,7 +340,7 @@ def uninstall(): def clean(): - """Deletes dev files""" + """Deletes dev files.""" recursive_rm( "$testfn*", "*.bak", @@ -376,24 +366,14 @@ def clean(): def setup_dev_env(): - """Install useful deps""" + """Install useful deps.""" install_pip() install_git_hooks() sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEPS))) -def flake8(): - """Run flake8 against all py files""" - py_files = subprocess.check_output("git ls-files") - if PY3: - py_files = py_files.decode() - py_files = [x for x in py_files.split() if x.endswith('.py')] - py_files = ' '.join(py_files) - sh("%s -m flake8 %s" % (PYTHON, py_files), nolog=True) - - def test(name=RUNNER_PY): - """Run tests""" + """Run tests.""" build() sh("%s %s" % (PYTHON, name)) @@ -409,55 +389,55 @@ def coverage(): def test_process(): - """Run process tests""" + """Run process tests.""" build() sh("%s psutil\\tests\\test_process.py" % PYTHON) def test_system(): - """Run system tests""" + """Run system tests.""" build() sh("%s psutil\\tests\\test_system.py" % PYTHON) def test_platform(): - """Run windows only tests""" + """Run windows only tests.""" build() sh("%s psutil\\tests\\test_windows.py" % PYTHON) def test_misc(): - """Run misc tests""" + """Run misc tests.""" build() sh("%s psutil\\tests\\test_misc.py" % PYTHON) def test_unicode(): - """Run unicode tests""" + """Run unicode tests.""" build() sh("%s psutil\\tests\\test_unicode.py" % PYTHON) def test_connections(): - """Run connections tests""" + """Run connections tests.""" build() sh("%s psutil\\tests\\test_connections.py" % PYTHON) def test_contracts(): - """Run contracts tests""" + """Run contracts tests.""" build() sh("%s psutil\\tests\\test_contracts.py" % PYTHON) def test_testutils(): - """Run test utilities tests""" + """Run test utilities tests.""" build() sh("%s psutil\\tests\\test_testutils.py" % PYTHON) def test_by_name(name): - """Run test by name""" + """Run test by name.""" build() sh("%s -m unittest -v %s" % (PYTHON, name)) @@ -469,7 +449,7 @@ def test_failed(): def test_memleaks(): - """Run memory leaks tests""" + """Run memory leaks tests.""" build() sh("%s psutil\\tests\\test_memleaks.py" % PYTHON) @@ -481,8 +461,8 @@ def install_git_hooks(): ROOT_DIR, "scripts", "internal", "git_pre_commit.py") dst = os.path.realpath( os.path.join(ROOT_DIR, ".git", "hooks", "pre-commit")) - with open(src, "rt") as s: - with open(dst, "wt") as d: + with open(src) as s: + with open(dst, "w") as d: d.write(s.read()) @@ -572,7 +552,6 @@ def parse_args(): sp.add_parser('install', help="build + install in develop/edit mode") sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") sp.add_parser('install-pip', help="install pip") - sp.add_parser('flake8', help="run flake8 against all py files") sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") sp.add_parser('setup-dev-env', help="install deps") diff --git a/scripts/iotop.py b/scripts/iotop.py index 07ae2fa3d4..a8f04c870c 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of iotop (http://guichaz.free.fr/iotop/) showing real time +"""A clone of iotop (http://guichaz.free.fr/iotop/) showing real time disk I/O statistics. It works on Linux only (FreeBSD and macOS are missing support for IO @@ -143,7 +142,7 @@ def refresh_window(procs, disks_read, disks_write): def setup(): curses.start_color() curses.use_default_colors() - for i in range(0, curses.COLORS): + for i in range(curses.COLORS): curses.init_pair(i + 1, i, -1) curses.endwin() win.nodelay(1) diff --git a/scripts/killall.py b/scripts/killall.py index d985185f85..0308370dec 100755 --- a/scripts/killall.py +++ b/scripts/killall.py @@ -4,9 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Kill a process by name. -""" +"""Kill a process by name.""" import os import sys diff --git a/scripts/meminfo.py b/scripts/meminfo.py index b98aa60be2..a13b7e00ba 100755 --- a/scripts/meminfo.py +++ b/scripts/meminfo.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Print system memory information. +"""Print system memory information. $ python3 scripts/meminfo.py MEMORY diff --git a/scripts/netstat.py b/scripts/netstat.py index 476b082e5e..7a9b2908cd 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'netstat -antp' on Linux. +"""A clone of 'netstat -antp' on Linux. $ python3 scripts/netstat.py Proto Local address Remote address Status PID Program name diff --git a/scripts/nettop.py b/scripts/nettop.py index 9e1abe7641..fedc644d0c 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -6,8 +6,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Shows real-time network statistics. +"""Shows real-time network statistics. Author: Giampaolo Rodola' @@ -128,7 +127,7 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): def setup(): curses.start_color() curses.use_default_colors() - for i in range(0, curses.COLORS): + for i in range(curses.COLORS): curses.init_pair(i + 1, i, -1) curses.endwin() win.nodelay(1) diff --git a/scripts/pidof.py b/scripts/pidof.py index da93710723..b809fafbed 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -5,8 +5,8 @@ # found in the LICENSE file. -""" -A clone of 'pidof' cmdline utility. +"""A clone of 'pidof' cmdline utility. + $ pidof python 1140 1138 1136 1134 1133 1129 1127 1125 1121 1120 1119 """ diff --git a/scripts/pmap.py b/scripts/pmap.py index 459927bfd2..56c1b48829 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -4,9 +4,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'pmap' utility on Linux, 'vmmap' on macOS and 'procstat -v' on BSD. -Report memory map of a process. +"""A clone of 'pmap' utility on Linux, 'vmmap' on macOS and 'procstat +-v' on BSD. Report memory map of a process. $ python3 scripts/pmap.py 32402 Address RSS Mode Mapping diff --git a/scripts/procinfo.py b/scripts/procinfo.py index fd44c83e7f..562a61a464 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -4,8 +4,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Print detailed information about a process. +"""Print detailed information about a process. + Author: Giampaolo Rodola' $ python3 scripts/procinfo.py @@ -147,10 +147,7 @@ def run(pid, verbose=False): with proc.oneshot(): try: parent = proc.parent() - if parent: - parent = '(%s)' % parent.name() - else: - parent = '' + parent = '(%s)' % parent.name() if parent else '' except psutil.Error: parent = '' try: @@ -175,7 +172,7 @@ def run(pid, verbose=False): cpu_tot_time = datetime.timedelta(seconds=sum(pinfo['cpu_times'])) cpu_tot_time = "%s:%s.%s" % ( cpu_tot_time.seconds // 60 % 60, - str((cpu_tot_time.seconds % 60)).zfill(2), + str(cpu_tot_time.seconds % 60).zfill(2), str(cpu_tot_time.microseconds)[:2]) print_('cpu-tspent', cpu_tot_time) print_('cpu-times', str_ntuple(pinfo['cpu_times'])) diff --git a/scripts/procsmem.py b/scripts/procsmem.py index ca03729ecf..574e370412 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Show detailed memory usage about all (querable) processes. +"""Show detailed memory usage about all (querable) processes. Processes are sorted by their "USS" (Unique Set Size) memory, which is probably the most representative metric for determining how much memory diff --git a/scripts/ps.py b/scripts/ps.py index a234209fb0..58a1d8c296 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'ps aux'. +"""A clone of 'ps aux'. $ python3 scripts/ps.py USER PID %MEM VSZ RSS NICE STATUS START TIME CMDLINE diff --git a/scripts/pstree.py b/scripts/pstree.py index 18732b8cbf..e873e467d7 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -Similar to 'ps aux --forest' on Linux, prints the process list +"""Similar to 'ps aux --forest' on Linux, prints the process list as a tree structure. $ python3 scripts/pstree.py diff --git a/scripts/sensors.py b/scripts/sensors.py index 3dc823803d..a5f9729b4e 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -5,8 +5,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'sensors' utility on Linux printing hardware temperatures, +"""A clone of 'sensors' utility on Linux printing hardware temperatures, fans speed and battery info. $ python3 scripts/sensors.py @@ -45,10 +44,7 @@ def main(): temps = psutil.sensors_temperatures() else: temps = {} - if hasattr(psutil, "sensors_fans"): - fans = psutil.sensors_fans() - else: - fans = {} + fans = psutil.sensors_fans() if hasattr(psutil, "sensors_fans") else {} if hasattr(psutil, "sensors_battery"): battery = psutil.sensors_battery() else: diff --git a/scripts/temperatures.py b/scripts/temperatures.py index 90097e5148..a211b88732 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -5,8 +5,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'sensors' utility on Linux printing hardware temperatures. +"""A clone of 'sensors' utility on Linux printing hardware temperatures. $ python3 scripts/sensors.py asus diff --git a/scripts/top.py b/scripts/top.py index e07a58f1af..675f541ef1 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of top / htop. +"""A clone of top / htop. Author: Giampaolo Rodola' @@ -117,7 +116,7 @@ def print_header(procs_status, num_procs): """Print system-related info, above the process list.""" def get_dashes(perc): - dashes = "|" * int((float(perc) / 10 * 4)) + dashes = "|" * int(float(perc) / 10 * 4) empty_dashes = " " * (40 - len(dashes)) return dashes, empty_dashes @@ -182,7 +181,7 @@ def refresh_window(procs, procs_status): if p.dict['cpu_times'] is not None: ctime = datetime.timedelta(seconds=sum(p.dict['cpu_times'])) ctime = "%s:%s.%s" % (ctime.seconds // 60 % 60, - str((ctime.seconds % 60)).zfill(2), + str(ctime.seconds % 60).zfill(2), str(ctime.microseconds)[:2]) else: ctime = '' @@ -192,10 +191,7 @@ def refresh_window(procs, procs_status): p.dict['memory_percent'] = '' if p.dict['cpu_percent'] is None: p.dict['cpu_percent'] = '' - if p.dict['username']: - username = p.dict['username'][:8] - else: - username = "" + username = p.dict['username'][:8] if p.dict['username'] else '' line = templ % (p.pid, username, p.dict['nice'], @@ -216,7 +212,7 @@ def refresh_window(procs, procs_status): def setup(): curses.start_color() curses.use_default_colors() - for i in range(0, curses.COLORS): + for i in range(curses.COLORS): curses.init_pair(i + 1, i, -1) curses.endwin() win.nodelay(1) diff --git a/scripts/who.py b/scripts/who.py index c1e407299e..18db1b17a9 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -""" -A clone of 'who' command; print information about users who are +"""A clone of 'who' command; print information about users who are currently logged in. $ python3 scripts/who.py diff --git a/scripts/winservices.py b/scripts/winservices.py index 5c710159be..d9c6a14a97 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -4,8 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -r""" -List all Windows services installed. +r"""List all Windows services installed. $ python3 scripts/winservices.py AeLookupSvc (Application Experience) diff --git a/setup.py b/setup.py index 35467e131a..eef7bf4558 100755 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ from __future__ import print_function +import ast import contextlib import glob import io @@ -98,10 +99,10 @@ def get_version(): INIT = os.path.join(HERE, 'psutil/__init__.py') - with open(INIT, 'r') as f: + with open(INIT) as f: for line in f: if line.startswith('__version__'): - ret = eval(line.strip().split(' = ')[1]) + ret = ast.literal_eval(line.strip().split(' = ')[1]) assert ret.count('.') == 2, ret for num in ret.split('.'): assert num.isdigit(), ret From becbe86f6e5b686ad3cfaee3ea6afddffae82170 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Oct 2023 20:41:10 +0200 Subject: [PATCH 1027/1714] fix #2195 / linux: no longer print exception at import time --- HISTORY.rst | 3 +++ psutil/_pslinux.py | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 78e5332344..ad07d07c0f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,9 @@ XXXX-XX-XX **Bug fixes** +- 2195_, [Linux]: no longer print exception at import time in case /proc/stat + can't be read due to permission error. Redirect it to ``PSUTIL_DEBUG`` + instead. - 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas Klausner) - 2245_, [Windows]: fix var unbound error on possibly in `swap_memory()`_ diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index cc37c0615a..628cd4b350 100644 --- a/psutil/_pslinux.py +++ b/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 @@ -284,9 +283,9 @@ def set_scputimes_ntuple(procfs_path): try: set_scputimes_ntuple("/proc") -except Exception: # pragma: no cover +except Exception as err: # pragma: no cover # 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) From a7e70bb66d5823f2cdcdf0f950bdbf26875058b4 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Sat, 14 Oct 2023 13:03:13 +0200 Subject: [PATCH 1028/1714] chore: update GHA workflows (#2315) --- .github/workflows/bsd.yml | 6 +++--- .github/workflows/build.yml | 26 +++++++++----------------- .github/workflows/issues.yml | 2 +- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 9c811d183f..efae0fc9bd 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -13,7 +13,7 @@ jobs: freebsd: runs-on: macos-12 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run tests uses: vmactions/freebsd-vm@v0 with: @@ -30,7 +30,7 @@ jobs: openbsd: runs-on: macos-12 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run tests uses: vmactions/openbsd-vm@v0 with: @@ -48,7 +48,7 @@ jobs: netbsd: runs-on: macos-12 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run tests uses: vmactions/netbsd-vm@v0 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bb8d108be..df248a6307 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,16 +9,18 @@ # * https://github.com/actions/checkout # * https://github.com/actions/setup-python # * https://github.com/actions/upload-artifact -# * https://github.com/marketplace/actions/cancel-workflow-action on: [push, pull_request] name: build +concurrency: + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} + cancel-in-progress: true jobs: # Linux + macOS + Windows Python 3 py3: name: py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'windows') && matrix.archs || 'all' }} runs-on: ${{ matrix.os }} - timeout-minutes: 20 + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -33,18 +35,13 @@ jobs: archs: "x86" steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.11 - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.14.1 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_ARCHS: "${{ matrix.archs }}" CIBW_PRERELEASE_PYTHONS: True @@ -79,12 +76,7 @@ jobs: CIBW_BUILD: 'cp27-*' steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.9 @@ -109,7 +101,7 @@ jobs: linters: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.x @@ -123,7 +115,7 @@ jobs: needs: [py2, py3] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.x diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 245d00cb4e..3d3adbf837 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: # install python - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v4 with: From 4a35f513ab5acf8329152f64da1eb22626dc1b52 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 14 Oct 2023 13:04:20 +0200 Subject: [PATCH 1029/1714] fix error in test_contracts.py on win --- psutil/tests/test_contracts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 152a5b7391..2874c0d906 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -668,8 +668,7 @@ def cwd(self, ret, info): try: st = os.stat(ret) except OSError as err: - if WINDOWS and err.errno in \ - psutil._psplatform.ACCESS_DENIED_SET: + if WINDOWS and psutil._psplatform.is_permission_err(err): pass # directory has been removed in mean time elif err.errno != errno.ENOENT: From 8815b9baa219b925b1893ef4b68a7a01106bafc0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 14 Oct 2023 13:11:46 +0200 Subject: [PATCH 1030/1714] give CREDIT to @JeremyGrosser and @getsentry for sponsorship (thanks\!) Signed-off-by: Giampaolo Rodola --- README.rst | 2 ++ docs/index.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 53b2affa67..98a9119182 100644 --- a/README.rst +++ b/README.rst @@ -148,6 +148,8 @@ Supporters + +
add your avatar diff --git a/docs/index.rst b/docs/index.rst index 20f18fc891..8b30ece17f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -90,6 +90,8 @@ Supporters + +

add your avatar From 8eb2930539e8030bae94255040a6254889c0fb05 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Oct 2023 11:08:24 +0200 Subject: [PATCH 1031/1714] pre-release Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 6 +++--- docs/index.rst | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ad07d07c0f..b9891788fd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.6 (IN DEVELOPMENT) -====================== +5.9.6 +===== -XXXX-XX-XX +2023-10-15 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 8b30ece17f..0e782ebc7b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2649,6 +2649,10 @@ Supported Python versions are 2.7, 3.6+ and PyPy3. Timeline ======== +- 2023-10-15: + `5.9.6 `__ - + `what's new `__ - + `diff `__ - 2023-04-17: `5.9.5 `__ - `what's new `__ - From 08cc3b6cd727157cb4f3729ff77812d18247e6d8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Oct 2023 23:30:07 +0200 Subject: [PATCH 1032/1714] doc wording Signed-off-by: Giampaolo Rodola --- docs/index.rst | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 0e782ebc7b..00a6f6006d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2630,21 +2630,22 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= -* psutil 5.9.6 (XXXX-XX): drop Python 3.4 and 3.5 support -* psutil 5.9.1 (2022-05): drop Python 2.6 support -* psutil 5.9.0 (2021-12): **MidnightBSD** -* psutil 5.8.0 (2020-12): **PyPy 2** on Windows -* psutil 5.7.1 (2020-07): **Windows Nano** -* psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support -* psutil 5.7.0 (2020-02): **PyPy 3** on Windows -* psutil 5.4.0 (2017-11): **AIX** -* psutil 3.4.1 (2016-01): **NetBSD** -* psutil 3.3.0 (2015-11): **OpenBSD** -* psutil 1.0.0 (2013-07): **Solaris** -* psutil 0.1.1 (2009-03): **FreeBSD** -* psutil 0.1.0 (2009-01): **Linux, Windows, macOS** - -Supported Python versions are 2.7, 3.6+ and PyPy3. +* psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 +* psutil 5.9.1 (2022-05): drop Python 2.6 +* psutil 5.9.0 (2021-12): add **MidnightBSD** +* psutil 5.8.0 (2020-12): add **PyPy 2** on Windows +* psutil 5.7.1 (2020-07): add **Windows Nano** +* psutil 5.7.0 (2020-02): drop Windows XP & Windows Server 2003 +* psutil 5.7.0 (2020-02): add **PyPy 3** on Windows +* psutil 5.4.0 (2017-11): add **AIX** +* psutil 3.4.1 (2016-01): add **NetBSD** +* psutil 3.3.0 (2015-11): add **OpenBSD** +* psutil 1.0.0 (2013-07): add **Solaris** +* psutil 0.1.1 (2009-03): add **FreeBSD** +* psutil 0.1.0 (2009-01): add **Linux, Windows, macOS** + +Supported Python versions at the time of writing are cPython 2.7, 3.6+ and +PyPy3. Timeline ======== From a7205fc9d77b13a161da1502bd0988db8b9b5971 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Oct 2023 18:05:16 +0200 Subject: [PATCH 1033/1714] Linux C files refactoring (#2320) --- MANIFEST.in | 10 + docs/index.rst | 31 +-- psutil/_psutil_linux.c | 496 +------------------------------------- psutil/arch/linux/disk.c | 75 ++++++ psutil/arch/linux/disk.h | 9 + psutil/arch/linux/mem.c | 30 +++ psutil/arch/linux/mem.h | 9 + psutil/arch/linux/net.c | 119 +++++++++ psutil/arch/linux/net.h | 9 + psutil/arch/linux/proc.c | 200 +++++++++++++++ psutil/arch/linux/proc.h | 25 ++ psutil/arch/linux/users.c | 72 ++++++ psutil/arch/linux/users.h | 9 + setup.py | 6 +- 14 files changed, 595 insertions(+), 505 deletions(-) create mode 100644 psutil/arch/linux/disk.c create mode 100644 psutil/arch/linux/disk.h create mode 100644 psutil/arch/linux/mem.c create mode 100644 psutil/arch/linux/mem.h create mode 100644 psutil/arch/linux/net.c create mode 100644 psutil/arch/linux/net.h create mode 100644 psutil/arch/linux/proc.c create mode 100644 psutil/arch/linux/proc.h create mode 100644 psutil/arch/linux/users.c create mode 100644 psutil/arch/linux/users.h diff --git a/MANIFEST.in b/MANIFEST.in index fb25643aff..ae80563492 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -72,6 +72,16 @@ include psutil/arch/freebsd/sensors.c include psutil/arch/freebsd/sensors.h include psutil/arch/freebsd/sys_socks.c include psutil/arch/freebsd/sys_socks.h +include psutil/arch/linux/disk.c +include psutil/arch/linux/disk.h +include psutil/arch/linux/mem.c +include psutil/arch/linux/mem.h +include psutil/arch/linux/net.c +include psutil/arch/linux/net.h +include psutil/arch/linux/proc.c +include psutil/arch/linux/proc.h +include psutil/arch/linux/users.c +include psutil/arch/linux/users.h include psutil/arch/netbsd/cpu.c include psutil/arch/netbsd/cpu.h include psutil/arch/netbsd/disk.c diff --git a/docs/index.rst b/docs/index.rst index 0e782ebc7b..00a6f6006d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2630,21 +2630,22 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= -* psutil 5.9.6 (XXXX-XX): drop Python 3.4 and 3.5 support -* psutil 5.9.1 (2022-05): drop Python 2.6 support -* psutil 5.9.0 (2021-12): **MidnightBSD** -* psutil 5.8.0 (2020-12): **PyPy 2** on Windows -* psutil 5.7.1 (2020-07): **Windows Nano** -* psutil 5.7.0 (2020-02): drop Windows XP & Server 2003 support -* psutil 5.7.0 (2020-02): **PyPy 3** on Windows -* psutil 5.4.0 (2017-11): **AIX** -* psutil 3.4.1 (2016-01): **NetBSD** -* psutil 3.3.0 (2015-11): **OpenBSD** -* psutil 1.0.0 (2013-07): **Solaris** -* psutil 0.1.1 (2009-03): **FreeBSD** -* psutil 0.1.0 (2009-01): **Linux, Windows, macOS** - -Supported Python versions are 2.7, 3.6+ and PyPy3. +* psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 +* psutil 5.9.1 (2022-05): drop Python 2.6 +* psutil 5.9.0 (2021-12): add **MidnightBSD** +* psutil 5.8.0 (2020-12): add **PyPy 2** on Windows +* psutil 5.7.1 (2020-07): add **Windows Nano** +* psutil 5.7.0 (2020-02): drop Windows XP & Windows Server 2003 +* psutil 5.7.0 (2020-02): add **PyPy 3** on Windows +* psutil 5.4.0 (2017-11): add **AIX** +* psutil 3.4.1 (2016-01): add **NetBSD** +* psutil 3.3.0 (2015-11): add **OpenBSD** +* psutil 1.0.0 (2013-07): add **Solaris** +* psutil 0.1.1 (2009-03): add **FreeBSD** +* psutil 0.1.0 (2009-01): add **Linux, Windows, macOS** + +Supported Python versions at the time of writing are cPython 2.7, 3.6+ and +PyPy3. Timeline ======== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 3e6b3b9004..38fa2cbaf3 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -10,499 +10,19 @@ #define _GNU_SOURCE 1 #endif #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// see: https://github.com/giampaolo/psutil/issues/659 -#ifdef PSUTIL_ETHTOOL_MISSING_TYPES - #include - typedef __u64 u64; - typedef __u32 u32; - typedef __u16 u16; - typedef __u8 u8; -#endif -/* Avoid redefinition of struct sysinfo with musl libc */ -#define _LINUX_SYSINFO_H -#include - -/* The minimum number of CPUs allocated in a cpu_set_t */ -static const int NCPUS_START = sizeof(unsigned long) * CHAR_BIT; - -// Linux >= 2.6.13 -#define PSUTIL_HAVE_IOPRIO defined(__NR_ioprio_get) && defined(__NR_ioprio_set) - -// Should exist starting from CentOS 6 (year 2011). -#ifdef CPU_ALLOC - #define PSUTIL_HAVE_CPU_AFFINITY -#endif +#include // DUPLEX_* #include "_psutil_common.h" -#include "_psutil_posix.h" - -// May happen on old RedHat versions, see: -// https://github.com/giampaolo/psutil/issues/607 -#ifndef DUPLEX_UNKNOWN - #define DUPLEX_UNKNOWN 0xff -#endif - - -#ifndef SPEED_UNKNOWN - #define SPEED_UNKNOWN -1 -#endif - - -#if PSUTIL_HAVE_IOPRIO -enum { - IOPRIO_WHO_PROCESS = 1, -}; - -static inline int -ioprio_get(int which, int who) { - return syscall(__NR_ioprio_get, which, who); -} - -static inline int -ioprio_set(int which, int who, int ioprio) { - return syscall(__NR_ioprio_set, which, who, ioprio); -} - -// * defined in linux/ethtool.h but not always available (e.g. Android) -// * #ifdef check needed for old kernels, see: -// https://github.com/giampaolo/psutil/issues/2164 -static inline uint32_t -psutil_ethtool_cmd_speed(const struct ethtool_cmd *ecmd) { -#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 27) - return ecmd->speed; -#else - return (ecmd->speed_hi << 16) | ecmd->speed; -#endif -} - -#define IOPRIO_CLASS_SHIFT 13 -#define IOPRIO_PRIO_MASK ((1UL << IOPRIO_CLASS_SHIFT) - 1) - -#define IOPRIO_PRIO_CLASS(mask) ((mask) >> IOPRIO_CLASS_SHIFT) -#define IOPRIO_PRIO_DATA(mask) ((mask) & IOPRIO_PRIO_MASK) -#define IOPRIO_PRIO_VALUE(class, data) (((class) << IOPRIO_CLASS_SHIFT) | data) - - -/* - * Return a (ioclass, iodata) Python tuple representing process I/O priority. - */ -static PyObject * -psutil_proc_ioprio_get(PyObject *self, PyObject *args) { - pid_t pid; - int ioprio, ioclass, iodata; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - ioprio = ioprio_get(IOPRIO_WHO_PROCESS, pid); - if (ioprio == -1) - return PyErr_SetFromErrno(PyExc_OSError); - ioclass = IOPRIO_PRIO_CLASS(ioprio); - iodata = IOPRIO_PRIO_DATA(ioprio); - return Py_BuildValue("ii", ioclass, iodata); -} - - -/* - * A wrapper around ioprio_set(); sets process I/O priority. - * ioclass can be either IOPRIO_CLASS_RT, IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE - * or 0. iodata goes from 0 to 7 depending on ioclass specified. - */ -static PyObject * -psutil_proc_ioprio_set(PyObject *self, PyObject *args) { - pid_t pid; - int ioprio, ioclass, iodata; - int retval; - - if (! PyArg_ParseTuple( - args, _Py_PARSE_PID "ii", &pid, &ioclass, &iodata)) { - return NULL; - } - ioprio = IOPRIO_PRIO_VALUE(ioclass, iodata); - retval = ioprio_set(IOPRIO_WHO_PROCESS, pid, ioprio); - if (retval == -1) - return PyErr_SetFromErrno(PyExc_OSError); - Py_RETURN_NONE; -} -#endif - - -/* - * Return disk mounted partitions as a list of tuples including device, - * mount point and filesystem type - */ -static PyObject * -psutil_disk_partitions(PyObject *self, PyObject *args) { - FILE *file = NULL; - struct mntent *entry; - char *mtab_path; - PyObject *py_dev = NULL; - PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; +#include "arch/linux/disk.h" +#include "arch/linux/mem.h" +#include "arch/linux/net.h" +#include "arch/linux/proc.h" +#include "arch/linux/users.h" - if (!PyArg_ParseTuple(args, "s", &mtab_path)) - return NULL; - - Py_BEGIN_ALLOW_THREADS - file = setmntent(mtab_path, "r"); - Py_END_ALLOW_THREADS - if ((file == 0) || (file == NULL)) { - psutil_debug("setmntent() failed"); - PyErr_SetFromErrnoWithFilename(PyExc_OSError, mtab_path); - goto error; - } - - while ((entry = getmntent(file))) { - if (entry == NULL) { - PyErr_Format(PyExc_RuntimeError, "getmntent() syscall failed"); - goto error; - } - py_dev = PyUnicode_DecodeFSDefault(entry->mnt_fsname); - if (! py_dev) - goto error; - py_mountp = PyUnicode_DecodeFSDefault(entry->mnt_dir); - if (! py_mountp) - goto error; - py_tuple = Py_BuildValue("(OOss)", - py_dev, // device - py_mountp, // mount point - entry->mnt_type, // fs type - entry->mnt_opts); // options - if (! py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_dev); - Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); - } - endmntent(file); - return py_retlist; - -error: - if (file != NULL) - endmntent(file); - Py_XDECREF(py_dev); - Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - return NULL; -} - - -/* - * A wrapper around sysinfo(), return system memory usage statistics. - */ -static PyObject * -psutil_linux_sysinfo(PyObject *self, PyObject *args) { - struct sysinfo info; - - if (sysinfo(&info) != 0) - return PyErr_SetFromErrno(PyExc_OSError); - // note: boot time might also be determined from here - return Py_BuildValue( - "(kkkkkkI)", - info.totalram, // total - info.freeram, // free - info.bufferram, // buffer - info.sharedram, // shared - info.totalswap, // swap tot - info.freeswap, // swap free - info.mem_unit // multiplier - ); -} - - -/* - * Return process CPU affinity as a Python list - */ -#ifdef PSUTIL_HAVE_CPU_AFFINITY - -static PyObject * -psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { - int cpu, ncpus, count, cpucount_s; - pid_t pid; - size_t setsize; - cpu_set_t *mask = NULL; - PyObject *py_list = NULL; - - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - ncpus = NCPUS_START; - while (1) { - setsize = CPU_ALLOC_SIZE(ncpus); - mask = CPU_ALLOC(ncpus); - if (mask == NULL) { - psutil_debug("CPU_ALLOC() failed"); - return PyErr_NoMemory(); - } - if (sched_getaffinity(pid, setsize, mask) == 0) - break; - CPU_FREE(mask); - if (errno != EINVAL) - return PyErr_SetFromErrno(PyExc_OSError); - if (ncpus > INT_MAX / 2) { - PyErr_SetString(PyExc_OverflowError, "could not allocate " - "a large enough CPU set"); - return NULL; - } - ncpus = ncpus * 2; - } - - py_list = PyList_New(0); - if (py_list == NULL) - goto error; - - cpucount_s = CPU_COUNT_S(setsize, mask); - for (cpu = 0, count = cpucount_s; count; cpu++) { - if (CPU_ISSET_S(cpu, setsize, mask)) { -#if PY_MAJOR_VERSION >= 3 - PyObject *cpu_num = PyLong_FromLong(cpu); -#else - PyObject *cpu_num = PyInt_FromLong(cpu); -#endif - if (cpu_num == NULL) - goto error; - if (PyList_Append(py_list, cpu_num)) { - Py_DECREF(cpu_num); - goto error; - } - Py_DECREF(cpu_num); - --count; - } - } - CPU_FREE(mask); - return py_list; - -error: - if (mask) - CPU_FREE(mask); - Py_XDECREF(py_list); - return NULL; -} - - -/* - * Set process CPU affinity; expects a bitmask - */ -static PyObject * -psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { - cpu_set_t cpu_set; - size_t len; - pid_t pid; - Py_ssize_t i, seq_len; - PyObject *py_cpu_set; - - if (!PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &py_cpu_set)) - return NULL; - - if (!PySequence_Check(py_cpu_set)) { - return PyErr_Format( - PyExc_TypeError, -#if PY_MAJOR_VERSION >= 3 - "sequence argument expected, got %R", Py_TYPE(py_cpu_set) -#else - "sequence argument expected, got %s", Py_TYPE(py_cpu_set)->tp_name -#endif - ); - } - - seq_len = PySequence_Size(py_cpu_set); - if (seq_len < 0) { - return NULL; - } - CPU_ZERO(&cpu_set); - for (i = 0; i < seq_len; i++) { - PyObject *item = PySequence_GetItem(py_cpu_set, i); - if (!item) { - return NULL; - } -#if PY_MAJOR_VERSION >= 3 - long value = PyLong_AsLong(item); -#else - long value = PyInt_AsLong(item); -#endif - Py_XDECREF(item); - if ((value == -1) || PyErr_Occurred()) { - if (!PyErr_Occurred()) - PyErr_SetString(PyExc_ValueError, "invalid CPU value"); - return NULL; - } - CPU_SET(value, &cpu_set); - } - - len = sizeof(cpu_set); - if (sched_setaffinity(pid, len, &cpu_set)) { - return PyErr_SetFromErrno(PyExc_OSError); - } - - Py_RETURN_NONE; -} -#endif /* PSUTIL_HAVE_CPU_AFFINITY */ - - -/* - * Return currently connected users as a list of tuples. - */ -static PyObject * -psutil_users(PyObject *self, PyObject *args) { - struct utmp *ut; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_user_proc = NULL; - - if (py_retlist == NULL) - return NULL; - setutent(); - while (NULL != (ut = getutent())) { - py_tuple = NULL; - py_user_proc = NULL; - if (ut->ut_type == USER_PROCESS) - py_user_proc = Py_True; - else - py_user_proc = Py_False; - py_username = PyUnicode_DecodeFSDefault(ut->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); - if (! py_hostname) - goto error; - - py_tuple = Py_BuildValue( - "OOOdO" _Py_PARSE_PID, - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut->ut_tv.tv_sec, // tstamp - py_user_proc, // (bool) user process - ut->ut_pid // process id - ); - if (! py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - endutent(); - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - endutent(); - return NULL; -} - - -/* - * Return stats about a particular network - * interface. References: - * https://github.com/dpaleino/wicd/blob/master/wicd/backends/be-ioctl.py - * http://www.i-scream.org/libstatgrab/ - */ -static PyObject* -psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { - char *nic_name; - int sock = 0; - int ret; - int duplex; - __u32 uint_speed; - int speed; - struct ifreq ifr; - struct ethtool_cmd ethcmd; - PyObject *py_retlist = NULL; - - if (! PyArg_ParseTuple(args, "s", &nic_name)) - return NULL; - - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock == -1) - return PyErr_SetFromOSErrnoWithSyscall("socket()"); - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); - - // duplex and speed - memset(ðcmd, 0, sizeof ethcmd); - ethcmd.cmd = ETHTOOL_GSET; - ifr.ifr_data = (void *)ðcmd; - ret = ioctl(sock, SIOCETHTOOL, &ifr); - - if (ret != -1) { - duplex = ethcmd.duplex; - // speed is returned from ethtool as a __u32 ranging from 0 to INT_MAX - // or SPEED_UNKNOWN (-1) - uint_speed = psutil_ethtool_cmd_speed(ðcmd); - if (uint_speed == (__u32)SPEED_UNKNOWN || uint_speed > INT_MAX) { - speed = 0; - } - else { - speed = (int)uint_speed; - } - } - else { - if ((errno == EOPNOTSUPP) || (errno == EINVAL)) { - // EOPNOTSUPP may occur in case of wi-fi cards. - // For EINVAL see: - // https://github.com/giampaolo/psutil/issues/797 - // #issuecomment-202999532 - duplex = DUPLEX_UNKNOWN; - speed = 0; - } - else { - PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCETHTOOL)"); - goto error; - } - } - - py_retlist = Py_BuildValue("[ii]", duplex, speed); - if (!py_retlist) - goto error; - close(sock); - return py_retlist; - -error: - if (sock != -1) - close(sock); - return NULL; -} - - -/* - * Module init. - */ static PyMethodDef mod_methods[] = { // --- per-process functions - -#if PSUTIL_HAVE_IOPRIO +#ifdef PSUTIL_HAVE_IOPRIO {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS}, {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS}, #endif @@ -514,13 +34,11 @@ static PyMethodDef mod_methods[] = { {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, {"users", psutil_users, METH_VARARGS}, {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, - // --- linux specific {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS}, // --- others {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, {"set_debug", psutil_set_debug, METH_VARARGS}, - {NULL, NULL, 0, NULL} }; diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c new file mode 100644 index 0000000000..692a7d5d47 --- /dev/null +++ b/psutil/arch/linux/disk.c @@ -0,0 +1,75 @@ +/* + * 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. + */ + +#include +#include + +#include "../../_psutil_common.h" + + +// Return disk mounted partitions as a list of tuples including device, +// mount point and filesystem type. +PyObject * +psutil_disk_partitions(PyObject *self, PyObject *args) { + FILE *file = NULL; + struct mntent *entry; + char *mtab_path; + PyObject *py_dev = NULL; + PyObject *py_mountp = NULL; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + if (!PyArg_ParseTuple(args, "s", &mtab_path)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + file = setmntent(mtab_path, "r"); + Py_END_ALLOW_THREADS + if ((file == 0) || (file == NULL)) { + psutil_debug("setmntent() failed"); + PyErr_SetFromErrnoWithFilename(PyExc_OSError, mtab_path); + goto error; + } + + while ((entry = getmntent(file))) { + if (entry == NULL) { + PyErr_Format(PyExc_RuntimeError, "getmntent() syscall failed"); + goto error; + } + py_dev = PyUnicode_DecodeFSDefault(entry->mnt_fsname); + if (! py_dev) + goto error; + py_mountp = PyUnicode_DecodeFSDefault(entry->mnt_dir); + if (! py_mountp) + goto error; + py_tuple = Py_BuildValue("(OOss)", + py_dev, // device + py_mountp, // mount point + entry->mnt_type, // fs type + entry->mnt_opts); // options + if (! py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); + } + endmntent(file); + return py_retlist; + +error: + if (file != NULL) + endmntent(file); + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + return NULL; +} diff --git a/psutil/arch/linux/disk.h b/psutil/arch/linux/disk.h new file mode 100644 index 0000000000..90a86d611b --- /dev/null +++ b/psutil/arch/linux/disk.h @@ -0,0 +1,9 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_disk_partitions(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/mem.c b/psutil/arch/linux/mem.c new file mode 100644 index 0000000000..3b9b4fef3f --- /dev/null +++ b/psutil/arch/linux/mem.c @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#include +#include + +#include "../../_psutil_common.h" + + +PyObject * +psutil_linux_sysinfo(PyObject *self, PyObject *args) { + struct sysinfo info; + + if (sysinfo(&info) != 0) + return PyErr_SetFromErrno(PyExc_OSError); + // note: boot time might also be determined from here + return Py_BuildValue( + "(kkkkkkI)", + info.totalram, // total + info.freeram, // free + info.bufferram, // buffer + info.sharedram, // shared + info.totalswap, // swap tot + info.freeswap, // swap free + info.mem_unit // multiplier + ); +} diff --git a/psutil/arch/linux/mem.h b/psutil/arch/linux/mem.h new file mode 100644 index 0000000000..582d3e0314 --- /dev/null +++ b/psutil/arch/linux/mem.h @@ -0,0 +1,9 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_linux_sysinfo(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c new file mode 100644 index 0000000000..522a55dfcd --- /dev/null +++ b/psutil/arch/linux/net.c @@ -0,0 +1,119 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include + +// see: https://github.com/giampaolo/psutil/issues/659 +#ifdef PSUTIL_ETHTOOL_MISSING_TYPES + #include + typedef __u64 u64; + typedef __u32 u32; + typedef __u16 u16; + typedef __u8 u8; +#endif + +// Avoid redefinition of struct sysinfo with musl libc. +#define _LINUX_SYSINFO_H +#include + +#include "../../_psutil_common.h" + + +// * defined in linux/ethtool.h but not always available (e.g. Android) +// * #ifdef check needed for old kernels, see: +// https://github.com/giampaolo/psutil/issues/2164 +static inline uint32_t +psutil_ethtool_cmd_speed(const struct ethtool_cmd *ecmd) { +#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 27) + return ecmd->speed; +#else + return (ecmd->speed_hi << 16) | ecmd->speed; +#endif +} + +// May happen on old RedHat versions, see: +// https://github.com/giampaolo/psutil/issues/607 +#ifndef DUPLEX_UNKNOWN + #define DUPLEX_UNKNOWN 0xff +#endif +// https://github.com/giampaolo/psutil/pull/2156 +#ifndef SPEED_UNKNOWN + #define SPEED_UNKNOWN -1 +#endif + + +// References: +// * https://github.com/dpaleino/wicd/blob/master/wicd/backends/be-ioctl.py +// * http://www.i-scream.org/libstatgrab/ +PyObject* +psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { + char *nic_name; + int sock = 0; + int ret; + int duplex; + __u32 uint_speed; + int speed; + struct ifreq ifr; + struct ethtool_cmd ethcmd; + PyObject *py_retlist = NULL; + + if (! PyArg_ParseTuple(args, "s", &nic_name)) + return NULL; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) + return PyErr_SetFromOSErrnoWithSyscall("socket()"); + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + + // duplex and speed + memset(ðcmd, 0, sizeof ethcmd); + ethcmd.cmd = ETHTOOL_GSET; + ifr.ifr_data = (void *)ðcmd; + ret = ioctl(sock, SIOCETHTOOL, &ifr); + + if (ret != -1) { + duplex = ethcmd.duplex; + // speed is returned from ethtool as a __u32 ranging from 0 to INT_MAX + // or SPEED_UNKNOWN (-1) + uint_speed = psutil_ethtool_cmd_speed(ðcmd); + if (uint_speed == (__u32)SPEED_UNKNOWN || uint_speed > INT_MAX) { + speed = 0; + } + else { + speed = (int)uint_speed; + } + } + else { + if ((errno == EOPNOTSUPP) || (errno == EINVAL)) { + // EOPNOTSUPP may occur in case of wi-fi cards. + // For EINVAL see: + // https://github.com/giampaolo/psutil/issues/797 + // #issuecomment-202999532 + duplex = DUPLEX_UNKNOWN; + speed = 0; + } + else { + PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCETHTOOL)"); + goto error; + } + } + + py_retlist = Py_BuildValue("[ii]", duplex, speed); + if (!py_retlist) + goto error; + close(sock); + return py_retlist; + +error: + if (sock != -1) + close(sock); + return NULL; +} diff --git a/psutil/arch/linux/net.h b/psutil/arch/linux/net.h new file mode 100644 index 0000000000..55095c06c9 --- /dev/null +++ b/psutil/arch/linux/net.h @@ -0,0 +1,9 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_net_if_duplex_speed(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c new file mode 100644 index 0000000000..ac87af917c --- /dev/null +++ b/psutil/arch/linux/proc.c @@ -0,0 +1,200 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "proc.h" +#include "../../_psutil_common.h" + + +#ifdef PSUTIL_HAVE_IOPRIO +enum { + IOPRIO_WHO_PROCESS = 1, +}; + +static inline int +ioprio_get(int which, int who) { + return syscall(__NR_ioprio_get, which, who); +} + +static inline int +ioprio_set(int which, int who, int ioprio) { + return syscall(__NR_ioprio_set, which, who, ioprio); +} + +#define IOPRIO_CLASS_SHIFT 13 +#define IOPRIO_PRIO_MASK ((1UL << IOPRIO_CLASS_SHIFT) - 1) + +#define IOPRIO_PRIO_CLASS(mask) ((mask) >> IOPRIO_CLASS_SHIFT) +#define IOPRIO_PRIO_DATA(mask) ((mask) & IOPRIO_PRIO_MASK) +#define IOPRIO_PRIO_VALUE(class, data) (((class) << IOPRIO_CLASS_SHIFT) | data) + + +// Return a (ioclass, iodata) Python tuple representing process I/O +// priority. +PyObject * +psutil_proc_ioprio_get(PyObject *self, PyObject *args) { + pid_t pid; + int ioprio, ioclass, iodata; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + ioprio = ioprio_get(IOPRIO_WHO_PROCESS, pid); + if (ioprio == -1) + return PyErr_SetFromErrno(PyExc_OSError); + ioclass = IOPRIO_PRIO_CLASS(ioprio); + iodata = IOPRIO_PRIO_DATA(ioprio); + return Py_BuildValue("ii", ioclass, iodata); +} + + +// A wrapper around ioprio_set(); sets process I/O priority. ioclass +// can be either IOPRIO_CLASS_RT, IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE or +// 0. iodata goes from 0 to 7 depending on ioclass specified. +PyObject * +psutil_proc_ioprio_set(PyObject *self, PyObject *args) { + pid_t pid; + int ioprio, ioclass, iodata; + int retval; + + if (! PyArg_ParseTuple( + args, _Py_PARSE_PID "ii", &pid, &ioclass, &iodata)) { + return NULL; + } + ioprio = IOPRIO_PRIO_VALUE(ioclass, iodata); + retval = ioprio_set(IOPRIO_WHO_PROCESS, pid, ioprio); + if (retval == -1) + return PyErr_SetFromErrno(PyExc_OSError); + Py_RETURN_NONE; +} +#endif // PSUTIL_HAVE_IOPRIO + + +#ifdef PSUTIL_HAVE_CPU_AFFINITY + +// Return process CPU affinity as a Python list. +PyObject * +psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { + int cpu, ncpus, count, cpucount_s; + pid_t pid; + size_t setsize; + cpu_set_t *mask = NULL; + PyObject *py_list = NULL; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + // The minimum number of CPUs allocated in a cpu_set_t. + ncpus = sizeof(unsigned long) * CHAR_BIT; + while (1) { + setsize = CPU_ALLOC_SIZE(ncpus); + mask = CPU_ALLOC(ncpus); + if (mask == NULL) { + psutil_debug("CPU_ALLOC() failed"); + return PyErr_NoMemory(); + } + if (sched_getaffinity(pid, setsize, mask) == 0) + break; + CPU_FREE(mask); + if (errno != EINVAL) + return PyErr_SetFromErrno(PyExc_OSError); + if (ncpus > INT_MAX / 2) { + PyErr_SetString(PyExc_OverflowError, "could not allocate " + "a large enough CPU set"); + return NULL; + } + ncpus = ncpus * 2; + } + + py_list = PyList_New(0); + if (py_list == NULL) + goto error; + + cpucount_s = CPU_COUNT_S(setsize, mask); + for (cpu = 0, count = cpucount_s; count; cpu++) { + if (CPU_ISSET_S(cpu, setsize, mask)) { +#if PY_MAJOR_VERSION >= 3 + PyObject *cpu_num = PyLong_FromLong(cpu); +#else + PyObject *cpu_num = PyInt_FromLong(cpu); +#endif + if (cpu_num == NULL) + goto error; + if (PyList_Append(py_list, cpu_num)) { + Py_DECREF(cpu_num); + goto error; + } + Py_DECREF(cpu_num); + --count; + } + } + CPU_FREE(mask); + return py_list; + +error: + if (mask) + CPU_FREE(mask); + Py_XDECREF(py_list); + return NULL; +} + + +// Set process CPU affinity; expects a bitmask. +PyObject * +psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { + cpu_set_t cpu_set; + size_t len; + pid_t pid; + Py_ssize_t i, seq_len; + PyObject *py_cpu_set; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &py_cpu_set)) + return NULL; + + if (!PySequence_Check(py_cpu_set)) { + return PyErr_Format( + PyExc_TypeError, +#if PY_MAJOR_VERSION >= 3 + "sequence argument expected, got %R", Py_TYPE(py_cpu_set) +#else + "sequence argument expected, got %s", Py_TYPE(py_cpu_set)->tp_name +#endif + ); + } + + seq_len = PySequence_Size(py_cpu_set); + if (seq_len < 0) { + return NULL; + } + CPU_ZERO(&cpu_set); + for (i = 0; i < seq_len; i++) { + PyObject *item = PySequence_GetItem(py_cpu_set, i); + if (!item) { + return NULL; + } +#if PY_MAJOR_VERSION >= 3 + long value = PyLong_AsLong(item); +#else + long value = PyInt_AsLong(item); +#endif + Py_XDECREF(item); + if ((value == -1) || PyErr_Occurred()) { + if (!PyErr_Occurred()) + PyErr_SetString(PyExc_ValueError, "invalid CPU value"); + return NULL; + } + CPU_SET(value, &cpu_set); + } + + len = sizeof(cpu_set); + if (sched_setaffinity(pid, len, &cpu_set)) { + return PyErr_SetFromErrno(PyExc_OSError); + } + + Py_RETURN_NONE; +} +#endif // PSUTIL_HAVE_CPU_AFFINITY diff --git a/psutil/arch/linux/proc.h b/psutil/arch/linux/proc.h new file mode 100644 index 0000000000..94a84c62ec --- /dev/null +++ b/psutil/arch/linux/proc.h @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#include +#include // __NR_* +#include // CPU_ALLOC + +// Linux >= 2.6.13 +#if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) + #define PSUTIL_HAVE_IOPRIO + + PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); + PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); +#endif + +// Should exist starting from CentOS 6 (year 2011). +#ifdef CPU_ALLOC + #define PSUTIL_HAVE_CPU_AFFINITY + + PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); + PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +#endif diff --git a/psutil/arch/linux/users.c b/psutil/arch/linux/users.c new file mode 100644 index 0000000000..9ca010a198 --- /dev/null +++ b/psutil/arch/linux/users.c @@ -0,0 +1,72 @@ +/* + * 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. + */ + +#include +#include + +#include "../../_psutil_common.h" + + +PyObject * +psutil_users(PyObject *self, PyObject *args) { + struct utmp *ut; + PyObject *py_retlist = PyList_New(0); + PyObject *py_tuple = NULL; + PyObject *py_username = NULL; + PyObject *py_tty = NULL; + PyObject *py_hostname = NULL; + PyObject *py_user_proc = NULL; + + if (py_retlist == NULL) + return NULL; + setutent(); + while (NULL != (ut = getutent())) { + py_tuple = NULL; + py_user_proc = NULL; + if (ut->ut_type == USER_PROCESS) + py_user_proc = Py_True; + else + py_user_proc = Py_False; + py_username = PyUnicode_DecodeFSDefault(ut->ut_user); + if (! py_username) + goto error; + py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); + if (! py_tty) + goto error; + py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); + if (! py_hostname) + goto error; + + py_tuple = Py_BuildValue( + "OOOdO" _Py_PARSE_PID, + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)ut->ut_tv.tv_sec, // tstamp + py_user_proc, // (bool) user process + ut->ut_pid // process id + ); + if (! py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); + } + endutent(); + return py_retlist; + +error: + Py_XDECREF(py_username); + Py_XDECREF(py_tty); + Py_XDECREF(py_hostname); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + endutent(); + return NULL; +} diff --git a/psutil/arch/linux/users.h b/psutil/arch/linux/users.h new file mode 100644 index 0000000000..ba2735d1d2 --- /dev/null +++ b/psutil/arch/linux/users.h @@ -0,0 +1,9 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_users(PyObject* self, PyObject* args); diff --git a/setup.py b/setup.py index eef7bf4558..6702268f8f 100755 --- a/setup.py +++ b/setup.py @@ -297,7 +297,11 @@ def get_winver(): macros.append(("PSUTIL_LINUX", 1)) ext = Extension( 'psutil._psutil_linux', - sources=sources + ['psutil/_psutil_linux.c'], + sources=( + sources + + ["psutil/_psutil_linux.c"] + + glob.glob("psutil/arch/linux/*.c") + ), define_macros=macros, **py_limited_api) From 0c3a1c5a4e6a56ccf77aa15fc20d31334cd02f5e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 24 Oct 2023 00:22:59 +0200 Subject: [PATCH 1034/1714] users() / Linux: make ":0" -> "localhost" host conversion in C instead of python --- psutil/_pslinux.py | 9 +-------- psutil/arch/linux/users.c | 17 ++++++++--------- psutil/tests/test_linux.py | 24 +++++------------------- 3 files changed, 14 insertions(+), 36 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 628cd4b350..d778075f29 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1549,14 +1549,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 diff --git a/psutil/arch/linux/users.c b/psutil/arch/linux/users.c index 9ca010a198..546e29ee98 100644 --- a/psutil/arch/linux/users.c +++ b/psutil/arch/linux/users.c @@ -6,6 +6,7 @@ #include #include +#include #include "../../_psutil_common.h" @@ -18,35 +19,33 @@ psutil_users(PyObject *self, PyObject *args) { PyObject *py_username = NULL; PyObject *py_tty = NULL; PyObject *py_hostname = NULL; - PyObject *py_user_proc = NULL; if (py_retlist == NULL) return NULL; setutent(); while (NULL != (ut = getutent())) { + if (ut->ut_type != USER_PROCESS) + continue; py_tuple = NULL; - py_user_proc = NULL; - if (ut->ut_type == USER_PROCESS) - py_user_proc = Py_True; - else - py_user_proc = Py_False; py_username = PyUnicode_DecodeFSDefault(ut->ut_user); if (! py_username) goto error; py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); if (! py_tty) goto error; - py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); + if (strcmp(ut->ut_host, ":0") || strcmp(ut->ut_host, ":0.0")) + py_hostname = PyUnicode_DecodeFSDefault("localhost"); + else + py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); if (! py_hostname) goto error; py_tuple = Py_BuildValue( - "OOOdO" _Py_PARSE_PID, + "OOOd" _Py_PARSE_PID, py_username, // username py_tty, // tty py_hostname, // hostname (double)ut->ut_tv.tv_sec, // tstamp - py_user_proc, // (bool) user process ut->ut_pid // process id ); if (! py_tuple) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 55839a1baf..12814ea878 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1513,25 +1513,11 @@ def test_boot_time_mocked(self): 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_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() From e9dabbbcae38a52a725722c6e3df0dc94c6051ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 24 Oct 2023 22:25:00 +0200 Subject: [PATCH 1035/1714] Include to avoid implicit-function-declaration for close and syscall (#2321) See https://docs.python.org/3.13/whatsnew/3.13.html """ Python.h no longer includes the standard header file. If needed, it should now be included explicitly. For example, it provides the functions: read(), write(), close(), isatty(), lseek(), getpid(), getcwd(), sysconf() and getpagesize(). """ --- psutil/_psutil_sunos.c | 1 + psutil/arch/linux/net.c | 1 + psutil/arch/linux/proc.c | 1 + psutil/arch/solaris/environ.c | 1 + 4 files changed, 4 insertions(+) diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 11335993f6..54f353c106 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -51,6 +51,7 @@ #include #include #include // fabs() +#include #include "_psutil_common.h" #include "_psutil_posix.h" diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index 522a55dfcd..d193e94087 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -10,6 +10,7 @@ #include #include #include +#include // see: https://github.com/giampaolo/psutil/issues/659 #ifdef PSUTIL_ETHTOOL_MISSING_TYPES diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index ac87af917c..b58a3ce2a2 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "proc.h" #include "../../_psutil_common.h" diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/solaris/environ.c index dd627eb004..4b4e041a45 100644 --- a/psutil/arch/solaris/environ.c +++ b/psutil/arch/solaris/environ.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "environ.h" From 902fada98ef1899d86717e7b34be46485d55e016 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 27 Oct 2023 00:00:32 +0200 Subject: [PATCH 1036/1714] Enforce ruff rule `raw-string-in-exception rule` (#2324) --- HISTORY.rst | 10 ++++++++++ docs/conf.py | 3 ++- psutil/__init__.py | 16 ++++++++++------ psutil/_compat.py | 15 +++++++++------ psutil/_psbsd.py | 3 ++- psutil/_pslinux.py | 10 ++++++---- psutil/_psposix.py | 4 +++- psutil/_pssunos.py | 3 ++- psutil/_pswindows.py | 9 ++++++--- psutil/tests/__init__.py | 2 +- pyproject.toml | 2 +- scripts/internal/winmake.py | 2 +- setup.py | 3 ++- 13 files changed, 55 insertions(+), 27 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b9891788fd..6044d8c506 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.7 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +**Enhancements** + +- 2324_: enforce Ruff rule `raw-string-in-exception`, which helps providing + clearer tracebacks when exceptions are raised by psutil. + 5.9.6 ===== diff --git a/docs/conf.py b/docs/conf.py index 9e0434706c..1079293de9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,8 @@ def get_version(): assert num.isdigit(), ret return ret else: - raise ValueError("couldn't find version string") + msg = "couldn't find version string" + raise ValueError(msg) VERSION = get_version() diff --git a/psutil/__init__.py b/psutil/__init__.py index 5bf6501abb..6a5a5eedfa 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -715,8 +715,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 @@ -803,7 +803,8 @@ 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() @@ -1198,10 +1199,12 @@ def _send_signal(self, sig): 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 ProcessLookupError: @@ -1288,7 +1291,8 @@ 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") + 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) diff --git a/psutil/_compat.py b/psutil/_compat.py index 95754113d0..613bd8a2b7 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -83,7 +83,8 @@ def super(type_=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1): # Get the function's first positional argument. type_or_obj = f.f_locals[f.f_code.co_varnames[0]] except (IndexError, KeyError): - raise RuntimeError('super() used in a function with no args') + 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__ @@ -91,7 +92,8 @@ def super(type_=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1): try: mro = type_or_obj.__class__.__mro__ except AttributeError: - raise RuntimeError('super() used in a non-newstyle class') + 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(): @@ -118,7 +120,8 @@ def super(type_=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1): continue break # found else: - raise RuntimeError('super() called outside a method') + msg = 'super() called outside a method' + raise RuntimeError(msg) # Dispatch to builtin super(). if type_or_obj is not _SENTINEL: @@ -199,9 +202,9 @@ def FileExistsError(inst): except FileExistsError: pass except OSError: - raise RuntimeError( - "broken or incompatible Python implementation, see: " - "https://github.com/giampaolo/psutil/issues/1659") + msg = ("broken or incompatible Python implementation, see: " + "https://github.com/giampaolo/psutil/issues/1659") + raise RuntimeError(msg) # --- stdlib additions diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index eeab18a9a7..6653dc9e1b 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -253,7 +253,8 @@ def per_cpu_times(): 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()] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index d778075f29..8c253d2f0e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -745,8 +745,8 @@ def cpu_freq(): # https://github.com/giampaolo/psutil/issues/1071 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(bcat(pjoin(path, "scaling_max_freq"))) / 1000 min_ = int(bcat(pjoin(path, "scaling_min_freq"))) / 1000 @@ -2157,7 +2157,8 @@ def ionice_set(self, ioclass, value): 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: - raise ValueError("value not in 0-7 range") + msg = "value not in 0-7 range" + raise ValueError(msg) return cext.proc_ioprio_set(self.pid, ioclass, value) if prlimit is not None: @@ -2168,7 +2169,8 @@ def rlimit(self, resource_, limits=None): # 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 diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 1b26589dbf..408cd56c77 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -100,7 +100,9 @@ def wait_pid(pid, timeout=None, proc_name=None, timeout=0 is also possible (either return immediately or raise). """ if pid <= 0: - raise ValueError("can't wait for PID 0") # see "man waitpid" + # see "man waitpid" + msg = "can't wait for PID 0" + raise ValueError(msg) interval = 0.0001 flags = 0 if timeout is not None: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 291dc5a004..4061af0141 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -151,7 +151,8 @@ 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() diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 6fd2b54bb1..c536b24e3f 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -891,9 +891,11 @@ def send_signal(self, sig): getattr(signal, "CTRL_BREAK_EVENT", object())): os.kill(self.pid, sig) else: - raise ValueError( + msg = ( "only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals " - "are supported on Windows") + "are supported on Windows" + ) + raise ValueError(msg) @wrap_exceptions def wait(self, timeout=None): @@ -1045,7 +1047,8 @@ def ionice_get(self): @wrap_exceptions def ionice_set(self, ioclass, value): if value: - raise TypeError("value argument not accepted on Windows") + 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) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index d4d8faf6df..8e552e9ab5 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1806,7 +1806,7 @@ def reload_module(module): def import_module_by_path(path): name = os.path.splitext(os.path.basename(path))[0] - if sys.version_info[0] == 2: + if sys.version_info[0] < 3: import imp return imp.load_source(name, path) else: diff --git a/pyproject.toml b/pyproject.toml index d99de42717..6f2aeb0323 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ ignore = [ "COM812", # Trailing comma missing "D", # pydocstyle "DTZ", # flake8-datetimez - "EM", # flake8-errmsg "ERA001", # Found commented-out code "FBT", # flake8-boolean-trap (makes zero sense) "FIX", # Line contains TODO / XXX / ..., consider resolving the issue @@ -71,6 +70,7 @@ ignore = [ [tool.ruff.per-file-ignores] # T201 == print(), T203 == pprint() ".github/workflows/*" = ["T201", "T203"] +"psutil/tests/*" = ["EM101"] # raw-string-in-exception "psutil/tests/runner.py" = ["T201", "T203"] "scripts/*" = ["T201", "T203"] "scripts/internal/*" = ["T201", "T203"] diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 6978a7e617..931a2c0014 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -46,7 +46,7 @@ "wheel", ] -if sys.version_info[0] == 2: +if sys.version_info[0] < 3: DEPS.append('mock') DEPS.append('ipaddress') DEPS.append('enum34') diff --git a/setup.py b/setup.py index 6702268f8f..1995fa0552 100755 --- a/setup.py +++ b/setup.py @@ -107,7 +107,8 @@ def get_version(): for num in ret.split('.'): assert num.isdigit(), ret return ret - raise ValueError("couldn't find version string") + msg = "couldn't find version string" + raise ValueError(msg) VERSION = get_version() From ccdb775abe50125a845362fd0b65f1189c02e78b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 9 Nov 2023 14:34:11 -0800 Subject: [PATCH 1037/1714] fix #2325: fix compilation on PyPy --- HISTORY.rst | 5 +++++ psutil/_psutil_common.c | 12 ++++++++++++ psutil/_psutil_common.h | 14 +++++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6044d8c506..0c62d823c6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,11 @@ XXXX-XX-XX - 2324_: enforce Ruff rule `raw-string-in-exception`, which helps providing clearer tracebacks when exceptions are raised by psutil. +**Bug fixes** + +- 2325_, [PyPy]: psutil did not compile on PyPy due to missing + `PyErr_SetExcFromWindowsErrWithFilenameObject` cPython API. + 5.9.6 ===== diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index d8d78bf640..d78c140ec5 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -58,6 +58,18 @@ PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { #endif // !defined(PyErr_SetFromWindowsErrWithFilename) +#if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) +PyObject * +PyErr_SetExcFromWindowsErrWithFilenameObject(PyObject *type, + int ierr, + PyObject *filename) { + // Original function is too complex. Just raise OSError without + // filename. + return PyErr_SetFromWindowsErrWithFilename(ierr, NULL); +} +#endif // !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) + + // PyPy 2.7 #if !defined(PyErr_SetFromWindowsErr) PyObject * diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index a425f8685e..20c03ebd7c 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -31,11 +31,15 @@ static const int PSUTIL_CONN_NONE = 128; #define PyUnicode_DecodeFSDefaultAndSize PyString_FromStringAndSize #endif -#if defined(PSUTIL_WINDOWS) && \ - defined(PYPY_VERSION) && \ - !defined(PyErr_SetFromWindowsErrWithFilename) - PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, - const char *filename); +#if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) + #if !defined(PyErr_SetFromWindowsErrWithFilename) + PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, + const char *filename); + #endif + #if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) + PyObject *PyErr_SetExcFromWindowsErrWithFilenameObject( + PyObject *type, int ierr, PyObject *filename); + #endif #endif // --- _Py_PARSE_PID From 4407540e720c46641622a33bb579430e047cd511 Mon Sep 17 00:00:00 2001 From: shawn Date: Wed, 29 Nov 2023 16:06:41 +0800 Subject: [PATCH 1038/1714] Update README.rst (#2330) fix psutil.cpu_times() output --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 98a9119182..12bf64254e 100644 --- a/README.rst +++ b/README.rst @@ -173,7 +173,7 @@ CPU >>> import psutil >>> >>> psutil.cpu_times() - scputimes(user=3961.46, nice=169.729, system=2150.659, idle=16900.540, iowait=629.59, irq=0.0, softirq=19.42, steal=0.0, guest=0, nice=0.0) + scputimes(user=3961.46, nice=169.729, system=2150.659, idle=16900.540, iowait=629.59, irq=0.0, softirq=19.42, steal=0.0, guest=0, guest_nice=0.0) >>> >>> for x in range(3): ... psutil.cpu_percent(interval=1) From 7c102951c50341cb9b11b1d5932185ab3033a3ca Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Dec 2023 18:24:08 +0100 Subject: [PATCH 1039/1714] micro optimization on python startup ("x in set" instead of "x in list") --- psutil/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 6a5a5eedfa..7ba00b3cb6 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1302,9 +1302,9 @@ def wait(self, timeout=None): # 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', + {'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', 'is_running', 'as_dict', 'parent', 'parents', 'children', 'rlimit', - 'memory_info_ex', 'oneshot']]) + 'memory_info_ex', 'oneshot'}]) # ===================================================================== From 3080df45c13149f939106bf7f2d42169a939e513 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Dec 2023 18:48:09 +0100 Subject: [PATCH 1040/1714] make pre-release: check if psutil version already exists on PYPI --- Makefile | 8 ++++++++ psutil/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 862c8b6c47..4832ced96b 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ PY3_DEPS = \ check-manifest \ concurrencytest \ coverage \ + packaging \ pylint \ pyperf \ pypinfo \ @@ -269,6 +270,13 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} sdist ${MAKE} check-sdist ${MAKE} install + $(PYTHON) -c \ + "import requests, sys; \ + from packaging.version import parse; \ + from psutil import __version__; \ + res = requests.get('https://pypi.org/pypi/psutil/json', timeout=5); \ + versions = sorted(res.json()['releases'], key=parse, reverse=True); \ + sys.exit('version %r already exists on PYPI' % __version__) if __version__ in versions else 0" ${MAKE} download-wheels-github ${MAKE} download-wheels-appveyor ${MAKE} check-wheels diff --git a/psutil/__init__.py b/psutil/__init__.py index 7ba00b3cb6..0c702fc762 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.6" +__version__ = "5.9.7" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From 8e21684cf34fccab5868b13fcedd69b3598be6f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Dec 2023 12:24:35 +0100 Subject: [PATCH 1041/1714] pre-release --- HISTORY.rst | 6 +++--- Makefile | 10 +++++----- docs/index.rst | 4 ++++ psutil/_psutil_common.c | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0c62d823c6..30f4338a42 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.7 (IN DEVELOPMENT) -====================== +5.9.7 +===== -XXXX-XX-XX +2023-12-17 **Enhancements** diff --git a/Makefile b/Makefile index 4832ced96b..4e86550f6b 100644 --- a/Makefile +++ b/Makefile @@ -277,11 +277,6 @@ pre-release: ## Check if we're ready to produce a new release. res = requests.get('https://pypi.org/pypi/psutil/json', timeout=5); \ versions = sorted(res.json()['releases'], key=parse, reverse=True); \ sys.exit('version %r already exists on PYPI' % __version__) if __version__ in versions else 0" - ${MAKE} download-wheels-github - ${MAKE} download-wheels-appveyor - ${MAKE} check-wheels - ${MAKE} print-hashes - ${MAKE} print-dist $(PYTHON) -c \ "from psutil import __version__ as ver; \ doc = open('docs/index.rst').read(); \ @@ -289,6 +284,11 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in doc, '%r not in docs/index.rst' % ver; \ assert ver in history, '%r not in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX in HISTORY.rst';" + ${MAKE} download-wheels-github + ${MAKE} download-wheels-appveyor + ${MAKE} check-wheels + ${MAKE} print-hashes + ${MAKE} print-dist release: ## Upload a new release. ${MAKE} check-sdist diff --git a/docs/index.rst b/docs/index.rst index 00a6f6006d..cc0f7dccab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2650,6 +2650,10 @@ PyPy3. Timeline ======== +- 2023-12-17: + `5.9.7 `__ - + `what's new `__ - + `diff `__ - 2023-10-15: `5.9.6 `__ - `what's new `__ - diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index d78c140ec5..2f2edca260 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -20,7 +20,7 @@ int PSUTIL_DEBUG = 0; // --- Backward compatibility with missing Python.h APIs // ==================================================================== -// PyPy on Windows +// PyPy on Windows. Missing APIs added in PyPy 7.3.14. #if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) #if !defined(PyErr_SetFromWindowsErrWithFilename) PyObject * From 71283a15b83dc3cfb86233a58449ef805a1038f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Dec 2023 12:36:56 +0100 Subject: [PATCH 1042/1714] temporary win skip to produce wheels --- psutil/tests/test_contracts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 2874c0d906..3e79f6fa48 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -433,6 +433,8 @@ def iter_proc_info(self): ls.append(proc_info(pid)) return ls + # XXX + @unittest.skipIf(WINDOWS, "temporary") def test_all(self): failures = [] for info in self.iter_proc_info(): From 389ed8b509cc59de020df0846991b2df8f2c3789 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Dec 2023 12:43:56 +0100 Subject: [PATCH 1043/1714] temporary win skip to produce wheels --- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_unicode.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 3e79f6fa48..6ed7eaae4e 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -357,7 +357,7 @@ def proc_info(pid): def check_exception(exc, proc, name, ppid): tcase.assertEqual(exc.pid, pid) - tcase.assertEqual(exc.name, name) + tcase.assertEqual(exc.name, proc._name) if isinstance(exc, psutil.ZombieProcess): tcase.assertProcessZombie(proc) if exc.ppid is not None: diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index cf9500a3f8..66627702c7 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -167,6 +167,7 @@ def setUp(self): @serialrun @unittest.skipIf(ASCII_FS, "ASCII fs") @unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2") +@unittest.skipIf(WINDOWS, "temporary") class TestFSAPIs(BaseUnicodeTest): """Test FS APIs with a funky, valid, UTF8 path name.""" From dbd59776590c04ff1369d22e64d54b97a6b14309 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Dec 2023 13:13:43 +0100 Subject: [PATCH 1044/1714] temporary win skip to produce wheels --- psutil/tests/test_process.py | 1 + scripts/internal/print_dist.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 6a90c5b931..7153dae858 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -728,6 +728,7 @@ def test_cmdline(self): self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) @unittest.skipIf(PYPY, "broken on PYPY") + @unittest.skipIf(WINDOWS, "temporary") def test_long_cmdline(self): testfn = self.get_testfn() create_exe(testfn) diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 978e50e2da..740951ac05 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -103,7 +103,7 @@ def main(): tot_files = 0 tot_size = 0 - templ = "%-100s %7s %7s %7s" + templ = "%-120s %7s %7s %7s" for platf, pkgs in groups.items(): ppn = "%s (%s)" % (platf, len(pkgs)) s = templ % (ppn, "size", "arch", "pyver") From 7dd31acfee786bfb8887573b9f1f62c17c125e56 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Dec 2023 13:42:08 +0100 Subject: [PATCH 1045/1714] remove temporary @skipIf windows tests --- psutil/tests/test_contracts.py | 4 +--- psutil/tests/test_process.py | 1 - psutil/tests/test_unicode.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 6ed7eaae4e..2874c0d906 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -357,7 +357,7 @@ def proc_info(pid): def check_exception(exc, proc, name, ppid): tcase.assertEqual(exc.pid, pid) - tcase.assertEqual(exc.name, proc._name) + tcase.assertEqual(exc.name, name) if isinstance(exc, psutil.ZombieProcess): tcase.assertProcessZombie(proc) if exc.ppid is not None: @@ -433,8 +433,6 @@ def iter_proc_info(self): ls.append(proc_info(pid)) return ls - # XXX - @unittest.skipIf(WINDOWS, "temporary") def test_all(self): failures = [] for info in self.iter_proc_info(): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 7153dae858..6a90c5b931 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -728,7 +728,6 @@ def test_cmdline(self): self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) @unittest.skipIf(PYPY, "broken on PYPY") - @unittest.skipIf(WINDOWS, "temporary") def test_long_cmdline(self): testfn = self.get_testfn() create_exe(testfn) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 66627702c7..cf9500a3f8 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -167,7 +167,6 @@ def setUp(self): @serialrun @unittest.skipIf(ASCII_FS, "ASCII fs") @unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2") -@unittest.skipIf(WINDOWS, "temporary") class TestFSAPIs(BaseUnicodeTest): """Test FS APIs with a funky, valid, UTF8 path name.""" From 8e574ff2611094a54c6f3bceede1b6c17b8c18d7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Dec 2023 14:28:38 +0100 Subject: [PATCH 1046/1714] fix some win tests + upgrade BSD* vmactions --- .github/workflows/bsd.yml | 14 +++++++------- .github/workflows/build.yml | 1 - psutil/tests/__init__.py | 26 ++++++++++++++++++++++++++ psutil/tests/test_contracts.py | 14 ++++++++------ psutil/tests/test_process.py | 13 ++++++++----- scripts/internal/winmake.py | 5 +++-- 6 files changed, 52 insertions(+), 21 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index efae0fc9bd..3af8d2371c 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -11,11 +11,11 @@ concurrency: cancel-in-progress: true jobs: freebsd: - runs-on: macos-12 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Run tests - uses: vmactions/freebsd-vm@v0 + uses: vmactions/freebsd-vm@v1 with: usesh: true prepare: | @@ -28,11 +28,11 @@ jobs: make test make test-memleaks openbsd: - runs-on: macos-12 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Run tests - uses: vmactions/openbsd-vm@v0 + uses: vmactions/openbsd-vm@v1 with: usesh: true prepare: | @@ -46,16 +46,16 @@ jobs: make test make test-memleaks netbsd: - runs-on: macos-12 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Run tests - uses: vmactions/netbsd-vm@v0 + uses: vmactions/netbsd-vm@v1 with: usesh: true prepare: | set -e - pkg_add -v pkgin + /usr/sbin/pkg_add -v pkgin pkgin update pkgin -y install python311-* py311-setuptools-* gcc12-* run: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df248a6307..f600b9d16f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,6 @@ jobs: archs: "AMD64" - os: windows-2019 archs: "x86" - steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 8e552e9ab5..f28e13bc4d 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -96,6 +96,7 @@ '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_exe', 'get_testfn', # os @@ -1295,6 +1296,31 @@ def print_sysinfo(): 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() diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 2874c0d906..0bf03b53dc 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -36,7 +36,6 @@ from psutil._compat import long from psutil._compat import range from psutil._compat import unicode -from psutil.tests import APPVEYOR from psutil.tests import CI_TESTING from psutil.tests import GITHUB_ACTIONS from psutil.tests import HAS_CPU_FREQ @@ -51,6 +50,7 @@ from psutil.tests import create_sockets from psutil.tests import enum from psutil.tests import is_namedtuple +from psutil.tests import is_win_secure_system_proc from psutil.tests import kernel_version from psutil.tests import process_namespace from psutil.tests import serialrun @@ -442,8 +442,8 @@ def test_all(self): meth(value, info) except Exception: s = '\n' + '=' * 70 + '\n' - s += "FAIL: name=test_%s, pid=%s, ret=%s\n" % ( - name, info['pid'], repr(value)) + 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" @@ -489,11 +489,12 @@ def ppid(self, ret, info): def name(self, ret, info): self.assertIsInstance(ret, (str, unicode)) - if APPVEYOR and not ret and info['status'] == 'stopped': + 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 + assert ret, repr(ret) def create_time(self, ret, info): self.assertIsInstance(ret, float) @@ -562,7 +563,8 @@ def ionice(self, ret, info): def num_threads(self, ret, info): self.assertIsInstance(ret, int) - if APPVEYOR and not ret and info['status'] == 'stopped': + 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) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 6a90c5b931..63705f9aa5 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -825,9 +825,6 @@ def test_nice(self): init = p.nice() try: if WINDOWS: - # A CI runner may limit our maximum priority, which will break - # this test. Instead, we test in order of increasing priority, - # and match either the expected value or the highest so far. highest_prio = None for prio in [psutil.IDLE_PRIORITY_CLASS, psutil.BELOW_NORMAL_PRIORITY_CLASS, @@ -842,10 +839,16 @@ def test_nice(self): pass else: new_prio = p.nice() - if CI_TESTING: + # 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) + self.assertEqual(new_prio, highest_prio) else: self.assertEqual(new_prio, prio) else: diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 931a2c0014..890a3a4154 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -442,7 +442,7 @@ def test_by_name(name): sh("%s -m unittest -v %s" % (PYTHON, name)) -def test_failed(): +def test_last_failed(): """Re-run tests which failed on last run.""" build() sh("%s %s --last-failed" % (PYTHON, RUNNER_PY)) @@ -559,7 +559,8 @@ def parse_args(): test_by_name = sp.add_parser('test-by-name', help=" run test by name") sp.add_parser('test-connections', help="run connections tests") sp.add_parser('test-contracts', help="run contracts tests") - sp.add_parser('test-failed', help="re-run tests which failed on last run") + sp.add_parser('test-last-failed', + help="re-run tests which failed on last run") sp.add_parser('test-memleaks', help="run memory leaks tests") sp.add_parser('test-misc', help="run misc tests") sp.add_parser('test-platform', help="run windows only tests") From 69bca49dfeee36529fc634e7e7846abe42bbab08 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Dec 2023 22:59:54 +0100 Subject: [PATCH 1047/1714] fix windows unicode tests --- psutil/tests/test_unicode.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index cf9500a3f8..cfc451219c 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -84,6 +84,7 @@ 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 @@ -158,10 +159,18 @@ def try_unicode(suffix): class BaseUnicodeTest(PsutilTestCase): funky_suffix = None + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.skip_tests = False + if cls.funky_suffix is not None: + if not try_unicode(cls.funky_suffix): + cls.skip_tests = True + def setUp(self): - if self.funky_suffix is not None: - if not try_unicode(self.funky_suffix): - raise self.skipTest("can't handle unicode str") + super().setUp() + if self.skip_tests: + raise self.skipTest("can't handle unicode str") @serialrun @@ -174,11 +183,13 @@ class TestFSAPIs(BaseUnicodeTest): @classmethod def setUpClass(cls): + super().setUpClass() cls.funky_name = get_testfn(suffix=cls.funky_suffix) create_exe(cls.funky_name) @classmethod def tearDownClass(cls): + super().tearDownClass() safe_rmpath(cls.funky_name) def expect_exact_path_match(self): @@ -192,7 +203,8 @@ def expect_exact_path_match(self): # --- def test_proc_exe(self): - subp = self.spawn_testproc(cmd=[self.funky_name]) + cmd = [self.funky_name, "-c", "time.sleep(10)"] + subp = self.spawn_testproc(cmd) p = psutil.Process(subp.pid) exe = p.exe() self.assertIsInstance(exe, str) @@ -201,20 +213,23 @@ def test_proc_exe(self): os.path.normcase(self.funky_name)) def test_proc_name(self): - subp = self.spawn_testproc(cmd=[self.funky_name]) + cmd = [self.funky_name, "-c", "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): - subp = self.spawn_testproc(cmd=[self.funky_name]) + cmd = [self.funky_name, "-c", "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, [self.funky_name]) + self.assertEqual( + cmdline, [self.funky_name, "-c", "time.sleep(10)"]) def test_proc_cwd(self): dname = self.funky_name + "2" From 6615ea87d7f1cb04f35ed47ab86911c9c68c98c2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Dec 2023 23:19:01 +0100 Subject: [PATCH 1048/1714] fix win test --- psutil/tests/test_process.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 63705f9aa5..f1765d3e99 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -753,7 +753,8 @@ def test_name(self): def test_long_name(self): testfn = self.get_testfn(suffix="0123456789" * 2) create_exe(testfn) - p = self.spawn_psproc(testfn) + cmdline = [testfn] + (["0123456789"] * 20) + 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 From 725e2236547468d9b5232b1be54f792dd614c80d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 19 Dec 2023 23:42:47 +0100 Subject: [PATCH 1049/1714] fix failing tests --- psutil/tests/test_memleaks.py | 3 ++ psutil/tests/test_system.py | 54 +++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index cd1b3f290e..da3c379324 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -25,6 +25,7 @@ import psutil._common from psutil import LINUX from psutil import MACOS +from psutil import NETBSD from psutil import OPENBSD from psutil import POSIX from psutil import SUNOS @@ -248,6 +249,7 @@ def test_rlimit_set(self): # Windows implementation is based on a single system-wide # function (tested later). @unittest.skipIf(WINDOWS, "worthless on WINDOWS") + @unittest.skipIf(NETBSD, "critically broken on NETBSD (#930)") 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 @@ -420,6 +422,7 @@ def test_net_io_counters(self): @fewtimes_if_linux() @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") + @unittest.skipIf(NETBSD, "critically broken on NETBSD (#930)") def test_net_connections(self): # always opens and handle on Windows() (once) psutil.net_connections(kind='all') diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 68bfc24c96..ad8fca3970 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -211,19 +211,20 @@ 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 # noqa - user.host # noqa - assert user.started > 0.0, user - datetime.datetime.fromtimestamp(user.started) - if WINDOWS or OPENBSD: - self.assertIsNone(user.pid) - else: - psutil.Process(user.pid) + 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 @@ -282,15 +283,13 @@ def test_virtual_memory(self): 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)) + with self.subTest(name=name): + value = getattr(mem, name) + if name != 'percent': + self.assertIsInstance(value, (int, long)) + if name != 'total': + self.assertGreaterEqual(value, 0) + self.assertGreater(value, mem.total) def test_swap_memory(self): mem = psutil.swap_memory() @@ -433,15 +432,20 @@ def test_per_cpu_times_2(self): 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. + # 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: - self.assertAlmostEqual( - getattr(base, field), getattr(summed_values, field), delta=1) + 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: From ed75c886d4054ee8e2570fcbf220da6e2fadfb68 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 19 Dec 2023 23:51:11 +0100 Subject: [PATCH 1050/1714] netbsd / cwd: raise NSP on ENOENT (#2340) --- HISTORY.rst | 6 ++++++ psutil/__init__.py | 2 +- psutil/arch/netbsd/proc.c | 9 +++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 30f4338a42..46591705b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,11 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.8 (IN DEVELOPMENT) +====================== + +- 2340_, [NetBSD]: if process is terminated, `Process.cwd()`_ will return an + empty string instead of raising `NoSuchProcess`_. + 5.9.7 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 0c702fc762..c4686ae2a4 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -211,7 +211,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.7" +__version__ = "5.9.8" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 1b05d61adb..300f81753b 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -115,13 +115,10 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { ssize_t len = readlink(buf, path, sizeof(path) - 1); free(buf); if (len == -1) { - if (errno == ENOENT) { - psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); - return Py_BuildValue("s", ""); - } - else { + if (errno == ENOENT) + NoSuchProcess("sysctl -> ENOENT"); + else PyErr_SetFromErrno(PyExc_OSError); - } return NULL; } path[len] = '\0'; From 91b0db19a3ff74e71c1576925c8c9a3fbdb408c2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 19 Dec 2023 23:55:33 +0100 Subject: [PATCH 1051/1714] revert prev commit which broke one test Signed-off-by: Giampaolo Rodola --- psutil/tests/test_system.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index ad8fca3970..6da77d58f1 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -283,13 +283,15 @@ def test_virtual_memory(self): assert mem.used > 0, mem assert mem.free >= 0, mem for name in mem._fields: - with self.subTest(name=name): - value = getattr(mem, name) - if name != 'percent': - self.assertIsInstance(value, (int, long)) - if name != 'total': - self.assertGreaterEqual(value, 0) - self.assertGreater(value, mem.total) + 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() From ad24d6ca66aeb1928ac18bbc787402478db0799f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Dec 2023 12:20:33 +0100 Subject: [PATCH 1052/1714] Fix NetBSD connections memory leak / core dump (#2341) --- HISTORY.rst | 4 ++ psutil/arch/netbsd/socks.c | 131 ++++++++++++++++++++++------------ psutil/tests/test_memleaks.py | 3 - 3 files changed, 88 insertions(+), 50 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 46591705b9..c6ad7e1448 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ 5.9.8 (IN DEVELOPMENT) ====================== +**Bug fixes** + +- 930_, [NetBSD], [critical]: `net_connections()`_ implementation was broken. + It could either leak memory or core dump. - 2340_, [NetBSD]: if process is terminated, `Process.cwd()`_ will return an empty string instead of raising `NoSuchProcess`_. diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index 08b0b7e672..5c8ad3bd6e 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -44,6 +44,8 @@ enum af_filter { struct kif { SLIST_ENTRY(kif) kifs; struct kinfo_file *kif; + char *buf; + int has_buf; }; // kinfo_file results list @@ -54,36 +56,31 @@ SLIST_HEAD(kifhead, kif) kihead = SLIST_HEAD_INITIALIZER(kihead); struct kpcb { SLIST_ENTRY(kpcb) kpcbs; struct kinfo_pcb *kpcb; + struct kinfo_pcb *buf; + int has_buf; }; // kinfo_pcb results list SLIST_HEAD(kpcbhead, kpcb) kpcbhead = SLIST_HEAD_INITIALIZER(kpcbhead); -static void psutil_kiflist_init(void); -static void psutil_kiflist_clear(void); -static void psutil_kpcblist_init(void); -static void psutil_kpcblist_clear(void); -static int psutil_get_files(void); -static int psutil_get_sockets(const char *name); -static int psutil_get_info(int aff); - // Initialize kinfo_file results list. static void psutil_kiflist_init(void) { SLIST_INIT(&kihead); - return; } // Clear kinfo_file results list. static void psutil_kiflist_clear(void) { - while (!SLIST_EMPTY(&kihead)) { - SLIST_REMOVE_HEAD(&kihead, kifs); - } - - return; + while (!SLIST_EMPTY(&kihead)) { + struct kif *kif = SLIST_FIRST(&kihead); + if (kif->has_buf == 1) + free(kif->buf); + free(kif); + SLIST_REMOVE_HEAD(&kihead, kifs); + } } @@ -91,18 +88,19 @@ psutil_kiflist_clear(void) { static void psutil_kpcblist_init(void) { SLIST_INIT(&kpcbhead); - return; } // Clear kinof_pcb result list. static void psutil_kpcblist_clear(void) { - while (!SLIST_EMPTY(&kpcbhead)) { - SLIST_REMOVE_HEAD(&kpcbhead, kpcbs); - } - - return; + while (!SLIST_EMPTY(&kpcbhead)) { + struct kpcb *kpcb = SLIST_FIRST(&kpcbhead); + if (kpcb->has_buf == 1) + free(kpcb->buf); + free(kpcb); + SLIST_REMOVE_HEAD(&kpcbhead, kpcbs); + } } @@ -112,7 +110,7 @@ psutil_get_files(void) { size_t len; size_t j; int mib[6]; - char *buf; + char *buf = NULL; off_t offset; mib[0] = CTL_KERN; @@ -124,7 +122,7 @@ psutil_get_files(void) { if (sysctl(mib, 6, NULL, &len, NULL, 0) == -1) { PyErr_SetFromErrno(PyExc_OSError); - return -1; + goto error; } offset = len % sizeof(off_t); @@ -132,33 +130,46 @@ psutil_get_files(void) { if ((buf = malloc(len + offset)) == NULL) { PyErr_NoMemory(); - return -1; + goto error; } if (sysctl(mib, 6, buf + offset, &len, NULL, 0) == -1) { - free(buf); PyErr_SetFromErrno(PyExc_OSError); - return -1; + goto error; } len /= sizeof(struct kinfo_file); struct kinfo_file *ki = (struct kinfo_file *)(buf + offset); - for (j = 0; j < len; j++) { - struct kif *kif = malloc(sizeof(struct kif)); - kif->kif = &ki[j]; - SLIST_INSERT_HEAD(&kihead, kif, kifs); + if (len > 0) { + for (j = 0; j < len; j++) { + struct kif *kif = malloc(sizeof(struct kif)); + if (kif == NULL) { + PyErr_NoMemory(); + goto error; + } + kif->kif = &ki[j]; + if (j == 0) { + kif->has_buf = 1; + kif->buf = buf; + } + else { + kif->has_buf = 0; + kif->buf = NULL; + } + SLIST_INSERT_HEAD(&kihead, kif, kifs); + } } - - /* - // debug - struct kif *k; - SLIST_FOREACH(k, &kihead, kifs) { - printf("%d\n", k->kif->ki_pid); // NOQA + else { + free(buf); } - */ return 0; + +error: + if (buf != NULL) + free(buf); + return -1; } @@ -167,7 +178,7 @@ static int psutil_get_sockets(const char *name) { size_t namelen; int mib[8]; - struct kinfo_pcb *pcb; + struct kinfo_pcb *pcb = NULL; size_t len; size_t j; @@ -175,17 +186,17 @@ psutil_get_sockets(const char *name) { if (sysctlnametomib(name, mib, &namelen) == -1) { PyErr_SetFromErrno(PyExc_OSError); - return -1; + goto error; } if (sysctl(mib, __arraycount(mib), NULL, &len, NULL, 0) == -1) { PyErr_SetFromErrno(PyExc_OSError); - return -1; + goto error; } if ((pcb = malloc(len)) == NULL) { PyErr_NoMemory(); - return -1; + goto error; } memset(pcb, 0, len); @@ -193,20 +204,42 @@ psutil_get_sockets(const char *name) { mib[7] = len / sizeof(*pcb); if (sysctl(mib, __arraycount(mib), pcb, &len, NULL, 0) == -1) { - free(pcb); PyErr_SetFromErrno(PyExc_OSError); - return -1; + goto error; } len /= sizeof(struct kinfo_pcb); struct kinfo_pcb *kp = (struct kinfo_pcb *)pcb; - for (j = 0; j < len; j++) { - struct kpcb *kpcb = malloc(sizeof(struct kpcb)); - kpcb->kpcb = &kp[j]; - SLIST_INSERT_HEAD(&kpcbhead, kpcb, kpcbs); + if (len > 0) { + for (j = 0; j < len; j++) { + struct kpcb *kpcb = malloc(sizeof(struct kpcb)); + if (kpcb == NULL) { + PyErr_NoMemory(); + goto error; + } + kpcb->kpcb = &kp[j]; + if (j == 0) { + kpcb->has_buf = 1; + kpcb->buf = pcb; + } + else { + kpcb->has_buf = 0; + kpcb->buf = NULL; + } + SLIST_INSERT_HEAD(&kpcbhead, kpcb, kpcbs); + } + } + else { + free(pcb); } + return 0; + +error: + if (pcb != NULL) + free(pcb); + return -1; } @@ -318,6 +351,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { psutil_kiflist_init(); psutil_kpcblist_init(); + if (psutil_get_files() != 0) goto error; if (psutil_get_info(ALL) != 0) @@ -429,8 +463,11 @@ psutil_net_connections(PyObject *self, PyObject *args) { return py_retlist; error: + psutil_kiflist_clear(); + psutil_kpcblist_clear(); + Py_DECREF(py_retlist); Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); - return 0; + return NULL; } diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index da3c379324..cd1b3f290e 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -25,7 +25,6 @@ import psutil._common from psutil import LINUX from psutil import MACOS -from psutil import NETBSD from psutil import OPENBSD from psutil import POSIX from psutil import SUNOS @@ -249,7 +248,6 @@ def test_rlimit_set(self): # Windows implementation is based on a single system-wide # function (tested later). @unittest.skipIf(WINDOWS, "worthless on WINDOWS") - @unittest.skipIf(NETBSD, "critically broken on NETBSD (#930)") 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 @@ -422,7 +420,6 @@ def test_net_io_counters(self): @fewtimes_if_linux() @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") - @unittest.skipIf(NETBSD, "critically broken on NETBSD (#930)") def test_net_connections(self): # always opens and handle on Windows() (once) psutil.net_connections(kind='all') From cb3ab3782faaff9b78bcbdc2f47582c8f4ef1c0a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Dec 2023 16:42:41 +0100 Subject: [PATCH 1053/1714] [NetBSD] speedup net_connections() with proper filtering in C (#2342) Modify psutil.net_connections() to retrieving unnecessary connection types unless explicitly asked. E.g., on an IDLE system with few IPv6 connections this will run around 170% faster: ``` import psutil, time started = time.monotonic() for x in range(1000): psutil.net_connections("tcp6") print(f"completed in {(time.monotonic() - started):.4f} secs") ``` Before all connection types (TCP, UDP, UNIX) were retrived internally, even if they were not returned. --- HISTORY.rst | 14 +++ psutil/_psbsd.py | 12 ++- psutil/arch/netbsd/socks.c | 178 +++++++++++++++++-------------------- 3 files changed, 102 insertions(+), 102 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c6ad7e1448..932ac21b90 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,20 @@ 5.9.8 (IN DEVELOPMENT) ====================== +**Enhancements** + +- 2342_, [NetBSD]: filter `net_connections()`_ returned list in C instead of + Python, and avoid to retrieve unnecessary connection types unless explicitly + asked. E.g., on an IDLE system with few IPv6 connections this will run around + 170% faster. Before all connection types (TCP, UDP, UNIX) were retrived + internally, even if they were not returned.:: + + import psutil, time + started = time.monotonic() + for x in range(1000): + psutil.net_connections("tcp6") + print(f"completed in {(time.monotonic() - started):.4f} secs") + **Bug fixes** - 930_, [NetBSD], [critical]: `net_connections()`_ implementation was broken. diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 6653dc9e1b..04b615ff9c 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -426,14 +426,13 @@ def net_connections(kind): if OPENBSD: rawlist = cext.net_connections(-1, families, types) elif NETBSD: - rawlist = cext.net_connections(-1) + rawlist = cext.net_connections(-1, kind) else: # FreeBSD rawlist = cext.net_connections() for item in rawlist: fd, fam, type, laddr, raddr, status, pid = item - if NETBSD or FREEBSD: - # OpenBSD implements filtering in C + if FREEBSD: if (fam not in families) or (type not in types): continue nt = conn_to_ntuple(fd, fam, type, laddr, raddr, @@ -786,16 +785,15 @@ def connections(self, kind='inet'): ret = [] if NETBSD: - rawlist = cext.net_connections(self.pid) + rawlist = cext.net_connections(self.pid, kind) elif OPENBSD: rawlist = cext.net_connections(self.pid, families, types) - else: # FreeBSD + else: rawlist = cext.proc_connections(self.pid, families, types) for item in rawlist: fd, fam, type, laddr, raddr, status = item[:6] - if NETBSD: - # FreeBSD and OpenBSD implement filtering in C + if FREEBSD: if (fam not in families) or (type not in types): continue nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index 5c8ad3bd6e..b3d6aac510 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -25,21 +25,6 @@ #include "../../_psutil_posix.h" -// address family filter -enum af_filter { - INET, - INET4, - INET6, - TCP, - TCP4, - TCP6, - UDP, - UDP4, - UDP6, - UNIX, - ALL, -}; - // kinfo_file results struct kif { SLIST_ENTRY(kif) kifs; @@ -245,84 +230,85 @@ psutil_get_sockets(const char *name) { // Collect open file and connections. static int -psutil_get_info(int aff) { - switch (aff) { - case INET: - if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet.udp.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) - return -1; - break; - case INET4: - if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet.udp.pcblist") != 0) - return -1; - break; - case INET6: - if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) - return -1; - break; - case TCP: - if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) - return -1; - break; - case TCP4: - if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) - return -1; - break; - case TCP6: - if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) - return -1; - break; - case UDP: - if (psutil_get_sockets("net.inet.udp.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) - return -1; - break; - case UDP4: - if (psutil_get_sockets("net.inet.udp.pcblist") != 0) - return -1; - break; - case UDP6: - if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) - return -1; - break; - case UNIX: - if (psutil_get_sockets("net.local.stream.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.local.seqpacket.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.local.dgram.pcblist") != 0) - return -1; - break; - case ALL: - if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet.udp.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.local.stream.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.local.seqpacket.pcblist") != 0) - return -1; - if (psutil_get_sockets("net.local.dgram.pcblist") != 0) - return -1; - break; +psutil_get_info(char *kind) { + if (strcmp(kind, "inet") == 0) { + if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet.udp.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "inet4") == 0) { + if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet.udp.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "inet6") == 0) { + if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "tcp") == 0) { + if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "tcp4") == 0) { + if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "tcp6") == 0) { + if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "udp") == 0) { + if (psutil_get_sockets("net.inet.udp.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "udp4") == 0) { + if (psutil_get_sockets("net.inet.udp.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "udp6") == 0) { + if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "unix") == 0) { + if (psutil_get_sockets("net.local.stream.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.local.seqpacket.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.local.dgram.pcblist") != 0) + return -1; + } + else if (strcmp(kind, "all") == 0) { + if (psutil_get_sockets("net.inet.tcp.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet.udp.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet6.tcp6.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.inet6.udp6.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.local.stream.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.local.seqpacket.pcblist") != 0) + return -1; + if (psutil_get_sockets("net.local.dgram.pcblist") != 0) + return -1; + } + else { + PyErr_SetString(PyExc_ValueError, "invalid kind value"); + return -1; } - return 0; } @@ -334,6 +320,7 @@ PyObject * psutil_net_connections(PyObject *self, PyObject *args) { char laddr[PATH_MAX]; char raddr[PATH_MAX]; + char *kind; int32_t lport; int32_t rport; int32_t status; @@ -346,15 +333,16 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "s", &pid, &kind)) { + goto error; + } psutil_kiflist_init(); psutil_kpcblist_init(); if (psutil_get_files() != 0) goto error; - if (psutil_get_info(ALL) != 0) + if (psutil_get_info(kind) != 0) goto error; struct kif *k; From 5003480c03a648f2406e7d962c5b165e29fbfb87 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Dec 2023 07:57:30 +0000 Subject: [PATCH 1054/1714] NetBSD: use _Py_PARSE_PID instead of long Signed-off-by: Giampaolo Rodola --- psutil/arch/netbsd/proc.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 300f81753b..c645f301ef 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -93,7 +93,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { char path[MAXPATHLEN]; size_t pathlen = sizeof path; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; #ifdef KERN_PROC_CWD @@ -116,7 +116,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { free(buf); if (len == -1) { if (errno == ENOENT) - NoSuchProcess("sysctl -> ENOENT"); + NoSuchProcess("readlink -> ENOENT"); else PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -143,7 +143,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { int ret; size_t size; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (pid == 0) { // else returns ENOENT @@ -190,7 +190,7 @@ psutil_proc_num_threads(PyObject *self, PyObject *args) { // Return number of threads used by process as a Python integer. long pid; kinfo_proc kp; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -211,7 +211,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; mib[0] = CTL_KERN; @@ -396,7 +396,7 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { struct kinfo_file *freep; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; errno = 0; From 3aa5147ea7529e44825fd796d7fd216f0c4b8c46 Mon Sep 17 00:00:00 2001 From: Hang Date: Thu, 21 Dec 2023 02:38:29 +0800 Subject: [PATCH 1055/1714] Fix Alpine build doc (#2326) Signed-off-by: Hang --- INSTALL.rst | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 2e8a1cd512..e3dab9cb41 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -27,7 +27,7 @@ RedHat / CentOS:: Alpine:: - sudo apk add gcc python3-dev + sudo apk add gcc python3-dev musl-dev linux-headers pip install --no-binary :all: psutil Windows (build) diff --git a/setup.py b/setup.py index 1995fa0552..ae25116ae0 100755 --- a/setup.py +++ b/setup.py @@ -473,7 +473,7 @@ def main(): elif which('rpm'): missdeps("sudo yum install gcc %s%s-devel" % (pyimpl, py3)) elif which('apk'): - missdeps("sudo apk add gcc %s%s-dev" % (pyimpl, py3)) + missdeps("sudo apk add gcc %s%s-dev musl-dev linux-headers" % (pyimpl, py3)) elif MACOS: print(hilite("XCode (https://developer.apple.com/xcode/) " "is not installed", color="red"), file=sys.stderr) From e0f5b0842fb8a1100ad12ca4fb27350c2886260e Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Wed, 20 Dec 2023 19:40:30 +0100 Subject: [PATCH 1056/1714] Remove useless shebang from tests/__main__.py (#2316) --- psutil/tests/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 434515d211..0cf39cc80a 100755 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -1,4 +1,3 @@ -#!/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 From 2786c095c7cd0177940789daac3ba25d66efb84f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Dec 2023 19:41:39 +0100 Subject: [PATCH 1057/1714] remove +x bit from psutil/tests/__main__.py Signed-off-by: Giampaolo Rodola --- psutil/tests/__main__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 psutil/tests/__main__.py diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py old mode 100755 new mode 100644 From 13e1fe7eaef37193f4f2980d3684a06dfcb953a9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Dec 2023 22:43:27 +0100 Subject: [PATCH 1058/1714] [FreeBSD] speed up net_connections() (#2343) --- HISTORY.rst | 13 ++---- psutil/_psbsd.py | 5 +- psutil/arch/freebsd/sys_socks.c | 83 ++++++++++++++++++++++++++++----- 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 932ac21b90..0f84d59f41 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,17 +5,12 @@ **Enhancements** -- 2342_, [NetBSD]: filter `net_connections()`_ returned list in C instead of +- 2343_, [FreeBSD]: filter `net_connections()`_ returned list in C instead of Python, and avoid to retrieve unnecessary connection types unless explicitly asked. E.g., on an IDLE system with few IPv6 connections this will run around - 170% faster. Before all connection types (TCP, UDP, UNIX) were retrived - internally, even if they were not returned.:: - - import psutil, time - started = time.monotonic() - for x in range(1000): - psutil.net_connections("tcp6") - print(f"completed in {(time.monotonic() - started):.4f} secs") + 4 times faster. Before all connection types (TCP, UDP, UNIX) were retrieved + internally, even if only a portion was returned. +- 2342_, [NetBSD]: same as above (#2343) but for NetBSD. **Bug fixes** diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 04b615ff9c..fc03d1114f 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -428,13 +428,10 @@ def net_connections(kind): elif NETBSD: rawlist = cext.net_connections(-1, kind) else: # FreeBSD - rawlist = cext.net_connections() + rawlist = cext.net_connections(families, types) for item in rawlist: fd, fam, type, laddr, raddr, status, pid = item - 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, pid) ret.add(nt) diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 48d3fb4f55..3887bd403a 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -33,7 +33,7 @@ static struct xfile *psutil_xfiles; static int psutil_nxfiles; -int +static int psutil_populate_xfiles(void) { size_t len; @@ -61,7 +61,7 @@ psutil_populate_xfiles(void) { } -struct xfile * +static struct xfile * psutil_get_file_from_sock(kvaddr_t sock) { struct xfile *xf; int n; @@ -76,7 +76,10 @@ psutil_get_file_from_sock(kvaddr_t sock) { // Reference: // https://github.com/freebsd/freebsd/blob/master/usr.bin/sockstat/sockstat.c -int psutil_gather_inet(int proto, PyObject *py_retlist) { +static int +psutil_gather_inet( + int proto, int include_v4, int include_v6, PyObject *py_retlist) +{ struct xinpgen *xig, *exig; struct xinpcb *xip; struct xtcpcb *xtp; @@ -177,6 +180,12 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { goto error; } + // filter + if ((inp->inp_vflag & INP_IPV4) && (include_v4 == 0)) + continue; + if ((inp->inp_vflag & INP_IPV6) && (include_v6 == 0)) + continue; + char lip[200], rip[200]; xf = psutil_get_file_from_sock(so->xso_so); @@ -235,7 +244,8 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { } -int psutil_gather_unix(int proto, PyObject *py_retlist) { +static int +psutil_gather_unix(int proto, PyObject *py_retlist) { struct xunpgen *xug, *exug; struct xunpcb *xup; const char *varname = NULL; @@ -339,23 +349,74 @@ int psutil_gather_unix(int proto, PyObject *py_retlist) { } +static int +psutil_int_in_seq(int value, PyObject *py_seq) { + int inseq; + PyObject *py_value; + + py_value = PyLong_FromLong((long)value); + if (py_value == NULL) + return -1; + inseq = PySequence_Contains(py_seq, py_value); // return -1 on failure + Py_DECREF(py_value); + return inseq; +} + + PyObject* psutil_net_connections(PyObject* self, PyObject* args) { - // Return system-wide open connections. + int include_v4, include_v6, include_unix, include_tcp, include_udp; + PyObject *py_af_filter = NULL; + PyObject *py_type_filter = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) return NULL; - if (psutil_populate_xfiles() != 1) + if (! PyArg_ParseTuple(args, "OO", &py_af_filter, &py_type_filter)) { + goto error; + } + if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { + PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); + goto error; + } + + if ((include_v4 = psutil_int_in_seq(AF_INET, py_af_filter)) == -1) goto error; - if (psutil_gather_inet(IPPROTO_TCP, py_retlist) == 0) + if ((include_v6 = psutil_int_in_seq(AF_INET6, py_af_filter)) == -1) goto error; - if (psutil_gather_inet(IPPROTO_UDP, py_retlist) == 0) + if ((include_unix = psutil_int_in_seq(AF_UNIX, py_af_filter)) == -1) goto error; - if (psutil_gather_unix(SOCK_STREAM, py_retlist) == 0) - goto error; - if (psutil_gather_unix(SOCK_DGRAM, py_retlist) == 0) + if ((include_tcp = psutil_int_in_seq(SOCK_STREAM, py_type_filter)) == -1) goto error; + if ((include_udp = psutil_int_in_seq(SOCK_DGRAM, py_type_filter)) == -1) + goto error; + + if (psutil_populate_xfiles() != 1) + goto error; + + // TCP + if (include_tcp == 1) { + if (psutil_gather_inet( + IPPROTO_TCP, include_v4, include_v6, py_retlist) == 0) + { + goto error; + } + } + // UDP + if (include_udp == 1) { + if (psutil_gather_inet( + IPPROTO_UDP, include_v4, include_v6, py_retlist) == 0) + { + goto error; + } + } + // UNIX + if (include_unix == 1) { + if (psutil_gather_unix(SOCK_STREAM, py_retlist) == 0) + goto error; + if (psutil_gather_unix(SOCK_DGRAM, py_retlist) == 0) + goto error; + } free(psutil_xfiles); return py_retlist; From 52a537c137113c8f9d9aaf54366a16872ed40178 Mon Sep 17 00:00:00 2001 From: Amir Rossert Date: Sat, 23 Dec 2023 11:13:18 +0200 Subject: [PATCH 1059/1714] Fix missing DUPLEX_UNKNOWN on older compilers (#2346) Signed-off-by: Amir Rossert Co-authored-by: Amir Rossert --- CREDITS | 2 +- HISTORY.rst | 1 + psutil/_psutil_linux.c | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 74e7617264..1ade9d3efa 100644 --- a/CREDITS +++ b/CREDITS @@ -808,7 +808,7 @@ I: 2077, 2160 N: Amir Rossert W: https://github.com/arossert -I: 2156 +I: 2156, 2345 N: Lawrence D'Anna W: https://github.com/smoofra diff --git a/HISTORY.rst b/HISTORY.rst index 0f84d59f41..0af24c1c67 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ It could either leak memory or core dump. - 2340_, [NetBSD]: if process is terminated, `Process.cwd()`_ will return an empty string instead of raising `NoSuchProcess`_. +- 2345_, [Linux]: fix compilation on older compiler missing DUPLEX_UNKNOWN 5.9.7 ===== diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 38fa2cbaf3..292e1c5524 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -41,6 +41,11 @@ static PyMethodDef mod_methods[] = { {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; +// May happen on old RedHat versions, see: +// https://github.com/giampaolo/psutil/issues/607 +#ifndef DUPLEX_UNKNOWN + #define DUPLEX_UNKNOWN 0xff +#endif #if PY_MAJOR_VERSION >= 3 From 3a75258bd381f46bce8404b10b355b4a7c955e0b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 31 Dec 2023 16:47:15 +0100 Subject: [PATCH 1060/1714] linux tests: refact mock_open_content() --- psutil/tests/test_linux.py | 398 ++++++++++++++++++------------------- 1 file changed, 188 insertions(+), 210 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 12814ea878..52d9e62f97 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -205,12 +205,13 @@ def get_free_version_info(): @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 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 == for_path: + if name in pairs: + content = pairs[name] if PY3: if isinstance(content, basestring): return io.StringIO(content) @@ -362,18 +363,17 @@ 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: + 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() @@ -415,23 +415,22 @@ def test_avail_old_percent(self): 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: + 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 @@ -444,19 +443,18 @@ 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: + 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 @@ -468,22 +466,21 @@ def test_avail_old_missing_fields(self): 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()): + 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')): @@ -498,64 +495,57 @@ def test_avail_old_missing_zoneinfo(self): 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: + 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) @@ -657,7 +647,7 @@ 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: + with mock_open_content({"/proc/meminfo": b""}) as m: psutil.swap_memory() assert m.called @@ -747,7 +737,7 @@ def test_emulate_fallbacks(self): # 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: + with mock_open_content({"/proc/cpuinfo": b""}) as m: self.assertEqual(psutil._pslinux.cpu_count_logical(), original) assert m.called @@ -1112,14 +1102,13 @@ def test_emulate_ipv6_unsupported(self, supports_ipv6, inet_ntop): 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: + 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 @@ -1199,9 +1188,8 @@ 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 - with mock_open_content( - '/proc/diskstats', - " 3 0 1 hda 2 3 4 5 6 7 8 9 10 11 12"): + 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) @@ -1219,9 +1207,8 @@ 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"): + 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) @@ -1241,9 +1228,7 @@ def test_emulate_kernel_2_6_limited(self): # 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_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) @@ -1262,12 +1247,11 @@ 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 - """)): + 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) @@ -1281,12 +1265,11 @@ 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 - """)): + 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) @@ -1296,12 +1279,11 @@ def test_emulate_exclude_partitions(self): 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 - """)): + 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) @@ -1468,13 +1450,12 @@ def open_mock(name, *args, **kwargs): 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: + 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 @@ -1484,13 +1465,12 @@ def test_cpu_steal_decrease(self): 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: + 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 @@ -1559,7 +1539,7 @@ 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: + with mock_open_content({"/proc/%s/status": ""}) as m: assert psutil.pid_exists(os.getpid()) assert m.called @@ -1667,7 +1647,7 @@ def open_mock(name, *args, **kwargs): 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: + {"/sys/class/power_supply/BAT0/energy_full": b"0"}) as m: self.assertEqual(psutil.sensors_battery().percent, 0) assert m.called @@ -1681,7 +1661,7 @@ def test_emulate_energy_full_not_avail(self): "/sys/class/power_supply/BAT0/charge_full", IOError(errno.ENOENT, "")): with mock_open_content( - "/sys/class/power_supply/BAT0/capacity", b"88"): + {"/sys/class/power_supply/BAT0/capacity": b"88"}): self.assertEqual(psutil.sensors_battery().percent, 88) def test_emulate_no_power(self): @@ -1834,31 +1814,30 @@ def test_parse_smaps_vs_memory_maps(self): def test_parse_smaps_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: + 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 @@ -2128,7 +2107,7 @@ def test_stat_file_parsing(self): "7", # delayacct_blkio_ticks ] content = " ".join(args).encode() - with mock_open_content('/proc/%s/stat' % os.getpid(), content): + 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) @@ -2144,16 +2123,15 @@ def test_stat_file_parsing(self): 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()): + 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) From d9230a94074dd76832c3f9bfac129175a3c4c99f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 3 Jan 2024 18:59:46 +0100 Subject: [PATCH 1061/1714] add SECURITY.md Signed-off-by: Giampaolo Rodola --- MANIFEST.in | 1 + SECURITY.md | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 SECURITY.md diff --git a/MANIFEST.in b/MANIFEST.in index ae80563492..14ede94554 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ include LICENSE include MANIFEST.in include Makefile include README.rst +include SECURITY.md include docs/.readthedocs.yaml include docs/DEVGUIDE.rst include docs/DEVNOTES diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..21dc174c7c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Security Policy + +If you have discovered a security vulnerability in this project, please report +it privately. **Do not disclose it as a public issue**. This gives me time to +fix the issue before public exposure, reducing the chance that an exploit will +be used before a patch is released. + +To report a security vulnerability use the [Tidelift security +contact](https://tidelift.com/security). Tidelift will coordinate the fix and +the disclosure of the reported problem. From 49efffcc52622af6d342bc1b045d8246dca3d7f6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Jan 2024 02:16:12 +0100 Subject: [PATCH 1062/1714] add more ruff rules --- MANIFEST.in | 1 + SECURITY.md | 10 ++++ docs/conf.py | 7 ++- psutil/__init__.py | 64 +++++++++++++++----------- psutil/_common.py | 8 ++-- psutil/_compat.py | 2 +- psutil/_pslinux.py | 8 ++-- psutil/_psposix.py | 3 +- psutil/_pswindows.py | 14 +++--- psutil/tests/__init__.py | 6 +-- psutil/tests/runner.py | 2 +- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 2 +- psutil/tests/test_process.py | 4 +- psutil/tests/test_windows.py | 4 +- pyproject.toml | 28 ++++++----- scripts/internal/check_broken_links.py | 2 +- scripts/internal/git_pre_commit.py | 8 ++-- scripts/internal/print_downloads.py | 2 +- scripts/killall.py | 6 +-- scripts/nettop.py | 2 +- scripts/who.py | 2 +- setup.py | 11 +++-- 23 files changed, 114 insertions(+), 84 deletions(-) create mode 100644 SECURITY.md diff --git a/MANIFEST.in b/MANIFEST.in index ae80563492..14ede94554 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ include LICENSE include MANIFEST.in include Makefile include README.rst +include SECURITY.md include docs/.readthedocs.yaml include docs/DEVGUIDE.rst include docs/DEVNOTES diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..21dc174c7c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Security Policy + +If you have discovered a security vulnerability in this project, please report +it privately. **Do not disclose it as a public issue**. This gives me time to +fix the issue before public exposure, reducing the chance that an exploit will +be used before a patch is released. + +To report a security vulnerability use the [Tidelift security +contact](https://tidelift.com/security). Tidelift will coordinate the fix and +the disclosure of the reported problem. diff --git a/docs/conf.py b/docs/conf.py index 1079293de9..3adde46eb6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,9 +43,8 @@ def get_version(): for num in ret.split('.'): assert num.isdigit(), ret return ret - else: - msg = "couldn't find version string" - raise ValueError(msg) + msg = "couldn't find version string" + raise ValueError(msg) VERSION = get_version() @@ -333,7 +332,7 @@ def get_version(): # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'psutil', 'psutil Documentation', - [author], 1) + [author], 1), ] # If true, show URL addresses after external links. diff --git a/psutil/__init__.py b/psutil/__init__.py index c4686ae2a4..3e12b5de2d 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -317,17 +317,16 @@ 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: - raise NoSuchProcess( - pid, - msg='process PID out of range (got %s)' % pid, - ) + msg = "process PID out of range (got %s)" % pid + raise NoSuchProcess(pid, msg=msg) self._pid = pid self._name = None @@ -359,7 +358,8 @@ def _init(self, pid, _ignore_nsp=False): pass except NoSuchProcess: if not _ignore_nsp: - raise NoSuchProcess(pid, msg='process PID not found') + msg = "process PID not found" + raise NoSuchProcess(pid, msg=msg) else: self._gone = True # This pair is supposed to identify a Process instance @@ -523,13 +523,16 @@ 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 = {} ls = attrs or valid_names @@ -1006,7 +1009,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(): @@ -1115,8 +1119,10 @@ 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))) + 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() @@ -1126,10 +1132,11 @@ 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"): @@ -1380,8 +1387,10 @@ 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: @@ -1558,7 +1567,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 @@ -1650,15 +1660,15 @@ def cpu_times(percpu=False): try: _last_cpu_times = {threading.current_thread().ident: cpu_times()} -except Exception: +except Exception: # noqa: BLE001 # Don't want to crash at import time. _last_cpu_times = {} try: _last_per_cpu_times = { - threading.current_thread().ident: cpu_times(percpu=True) + threading.current_thread().ident: cpu_times(percpu=True), } -except Exception: +except Exception: # noqa: BLE001 # Don't want to crash at import time. _last_per_cpu_times = {} @@ -1757,7 +1767,8 @@ def cpu_percent(interval=None, percpu=False): 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) @@ -1816,7 +1827,8 @@ def cpu_times_percent(interval=None, percpu=False): 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 = [] diff --git a/psutil/_common.py b/psutil/_common.py index 4057f541b6..95323aabc1 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -434,7 +434,7 @@ def wrapper(*args, **kwargs): except KeyError: try: ret = cache[key] = fun(*args, **kwargs) - except Exception as err: + except Exception as err: # noqa: BLE001 raise raise_from(err, None) return ret @@ -482,14 +482,14 @@ def wrapper(self): # case 2: we never entered oneshot() ctx try: return fun(self) - except Exception as err: + 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: + except Exception as err: # noqa: BLE001 raise raise_from(err, None) try: self._cache[fun] = ret @@ -866,7 +866,7 @@ def term_supports_colors(file=sys.stdout): # pragma: no cover assert file.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 - except Exception: + except Exception: # noqa: BLE001 return False else: return True diff --git a/psutil/_compat.py b/psutil/_compat.py index 613bd8a2b7..4957b22fdd 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -427,7 +427,7 @@ def get_terminal_size(fallback=(80, 24)): res = struct.unpack( 'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234')) return (res[1], res[0]) - except Exception: + except Exception: # noqa: BLE001 return fallback diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 8c253d2f0e..a252f3850b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -67,7 +67,7 @@ # 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", ] @@ -154,7 +154,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, } @@ -283,7 +283,7 @@ def set_scputimes_ntuple(procfs_path): try: set_scputimes_ntuple("/proc") -except Exception as err: # pragma: no cover +except Exception as err: # noqa: BLE001 # Don't want to crash at import time. debug("ignoring exception on import: %r" % err) scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) @@ -2033,7 +2033,7 @@ 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 diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 408cd56c77..22d7c4f0d7 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -138,7 +138,8 @@ def sleep(interval): # WNOHANG flag was used and PID is still running. interval = sleep(interval) continue - 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(). diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index c536b24e3f..f44a7010cf 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -717,14 +717,12 @@ def wrapper(self, *args, **kwargs): time.sleep(delay) delay = min(delay * 2, 0.04) continue - else: - raise - else: - 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) + 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 diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f28e13bc4d..0dd92b8b97 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -197,7 +197,7 @@ def macos_version(): os.path.join(os.path.dirname(__file__), '..', '..')) SCRIPTS_DIR = os.environ.get( "PSUTIL_SCRIPTS_DIR", - os.path.join(ROOT_DIR, 'scripts') + os.path.join(ROOT_DIR, 'scripts'), ) HERE = os.path.realpath(os.path.dirname(__file__)) @@ -217,7 +217,7 @@ def macos_version(): HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") try: HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) -except Exception: +except Exception: # noqa: BLE001 HAS_BATTERY = False HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") @@ -232,7 +232,7 @@ def attempt(exe): try: subprocess.check_call( [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except Exception: + except subprocess.CalledProcessError: return None else: return exe diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index d3938b05a0..a678b4c419 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -222,7 +222,7 @@ def _split_suite(suite): for test in suite: if test.countTestCases() == 0: continue - elif isinstance(test, unittest.TestSuite): + if isinstance(test, unittest.TestSuite): test_class = test._tests[0].__class__ elif isinstance(test, unittest.TestCase): test_class = test diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 0bf03b53dc..9c54299a27 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -440,7 +440,7 @@ def test_all(self): meth = getattr(self, name) try: meth(value, info) - except Exception: + 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) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 52d9e62f97..5c62842a4d 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1630,7 +1630,7 @@ def test_emulate_power_undetermined(self): def open_mock(name, *args, **kwargs): if name.startswith( ('/sys/class/power_supply/AC0/online', - '/sys/class/power_supply/AC/online') + '/sys/class/power_supply/AC/online'), ): raise IOError(errno.ENOENT, "") elif name.startswith("/sys/class/power_supply/BAT0/status"): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index f1765d3e99..e9a8f3ae64 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -647,7 +647,7 @@ def test_memory_maps(self): value = getattr(nt, fname) if fname == 'path': continue - elif fname in ('addr', 'perms'): + if fname in ('addr', 'perms'): assert value, value else: self.assertIsInstance(value, (int, long)) @@ -1412,7 +1412,7 @@ def clean_dict(d): @unittest.skipIf(not POSIX, "POSIX only") @unittest.skipIf( MACOS_11PLUS, - "macOS 11+ can't get another process environment, issue #2084" + "macOS 11+ can't get another process environment, issue #2084", ) def test_weird_environ(self): # environment variables can contain values without an equals sign diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 47d6ad3f13..dfc8444bdd 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -608,7 +608,7 @@ def test_memory_vms(self): # 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): + if vms not in (wmi_usage, wmi_usage * 1024): raise self.fail("wmi=%s, psutil=%s" % (wmi_usage, vms)) def test_create_time(self): @@ -822,7 +822,7 @@ def test_win_service_iter(self): "pause_pending", "continue_pending", "stop_pending", - "stopped" + "stopped", ]) for serv in psutil.win_service_iter(): data = serv.as_dict() diff --git a/pyproject.toml b/pyproject.toml index 6f2aeb0323..6e5e9563aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,20 +12,19 @@ select = [ "D301", # Use `r"""` if any backslashes in a docstring "D403", # [*] First word of the first line should be capitalized "PERF102", # [*] When using only the keys of a dict use the `keys()` method + "RET507", # Unnecessary `elif` after `continue` statement "S113", # Probable use of requests call without timeout "S602", # `subprocess` call with `shell=True` identified, security issue ] ignore = [ - "A", # flake8-builtins + "A", # flake8-builtins (shadowing of builtins like all, any, ...) "ANN", # flake8-annotations - "ARG", # flake8-unused-arguments + "ARG001", # unused-function-argument + "ARG002", # unused-method-argument "B007", # Loop control variable `x` not used within loop body "B904", # Within an `except` clause, raise exceptions with `raise ... from err` (PYTHON2.7 COMPAT) - "BLE001", # Do not catch blind exception: `Exception` "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) - "C408", # Unnecessary `dict` call (rewrite as a literal) "C90", # mccabe (function `X` is too complex) - "COM812", # Trailing comma missing "D", # pydocstyle "DTZ", # flake8-datetimez "ERA001", # Found commented-out code @@ -35,16 +34,21 @@ ignore = [ "INP", # flake8-no-pep420 "N801", # Class name `async_chat` should use CapWords convention (ASYNCORE COMPAT) "N802", # Function name X should be lowercase. - "N803", # Argument name X should be lowercase. "N806", # Variable X in function should be lowercase. "N818", # Exception name `FooBar` should be named with an Error suffix "PERF", # Perflint "PGH004", # Use specific rule codes when using `noqa` - "PLR", # pylint - "PLW", # pylint + "PLR0911", # Too many return statements (8 > 6) + "PLR0912", # Too many branches (x > y) + "PLR0913", # Too many arguments in function definition (x > y) + "PLR0915", # Too many statements (92 > 50) + "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable + "PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation + "PLW0603", # Using the global statement to update `lineno` is discouraged + "PLW2901", # `for` loop variable `x` overwritten by assignment target "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib - "PYI", # flake8-pyi + "PYI", # flake8-pyi (python types stuff) "Q000", # Single quotes found but double quotes preferred "RET", # flake8-return "RUF", # Ruff-specific rules @@ -55,7 +59,6 @@ ignore = [ "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements "SLF", # flake8-self "TD", # all TODOs, XXXs, etc. - "TRY003", # Avoid specifying long messages outside the exception class "TRY200", # Use `raise from` to specify exception cause (PYTHON2.7 COMPAT) "TRY300", # Consider moving this statement to an `else` block "TRY301", # Abstract `raise` to an inner function @@ -69,8 +72,11 @@ ignore = [ [tool.ruff.per-file-ignores] # T201 == print(), T203 == pprint() +# EM101 == raw-string-in-exception +# TRY003 == raise-vanilla-args ".github/workflows/*" = ["T201", "T203"] -"psutil/tests/*" = ["EM101"] # raw-string-in-exception +"psutil/_compat.py" = ["PLW0127"] # self-assigning-variable +"psutil/tests/*" = ["EM101", "TRY003"] "psutil/tests/runner.py" = ["T201", "T203"] "scripts/*" = ["T201", "T203"] "scripts/internal/*" = ["T201", "T203"] diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 315f06fa1f..af53b878cb 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -213,7 +213,7 @@ def parallel_validator(urls): fname, url = fut_to_url[fut] try: ok = fut.result() - except Exception: + except Exception: # noqa: BLE001 fails.append((fname, url)) print() print("warn: error while validating %s" % url, file=sys.stderr) diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 92852f8362..79582feeb1 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -29,7 +29,7 @@ def term_supports_colors(): assert sys.stderr.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 - except Exception: + except Exception: # noqa: BLE001 return False return True @@ -60,7 +60,7 @@ def sh(cmd): cmd = shlex.split(cmd) p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True + universal_newlines=True, ) stdout, stderr = p.communicate() if p.returncode != 0: @@ -89,7 +89,7 @@ def git_commit_files(): x for x in out.split("\n") if x.endswith(".toml") and os.path.exists(x) ] new_rm_mv = sh( - ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] + ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"], ) # XXX: we should escape spaces and possibly other amenities here new_rm_mv = new_rm_mv.split() @@ -102,7 +102,7 @@ def ruff(files): if subprocess.call(cmd) != 0: return exit( "Python code didn't pass 'ruff' style check." - "Try running 'make fix-ruff'." + "Try running 'make fix-ruff'.", ) diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 1ee37e534a..8e47a08b49 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -144,7 +144,7 @@ def main(): data = [ {'what': 'Per month', 'download_count': downs}, {'what': 'Per day', 'download_count': int(downs / 30)}, - {'what': 'PYPI ranking', 'download_count': ranking()} + {'what': 'PYPI ranking', 'download_count': ranking()}, ] print_markdown_table('Overview', 'what', data) print_markdown_table('Operating systems', 'system_name', diff --git a/scripts/killall.py b/scripts/killall.py index 0308370dec..592b8d6e3b 100755 --- a/scripts/killall.py +++ b/scripts/killall.py @@ -16,15 +16,15 @@ def main(): if len(sys.argv) != 2: sys.exit('usage: %s name' % __file__) else: - NAME = sys.argv[1] + name = sys.argv[1] killed = [] for proc in psutil.process_iter(): - if proc.name() == NAME and proc.pid != os.getpid(): + if proc.name() == name and proc.pid != os.getpid(): proc.kill() killed.append(proc.pid) if not killed: - sys.exit('%s: no process found' % NAME) + sys.exit('%s: no process found' % name) else: sys.exit(0) diff --git a/scripts/nettop.py b/scripts/nettop.py index fedc644d0c..64d408fc4f 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -82,7 +82,7 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): # totals printl("total bytes: sent: %-10s received: %s" % ( bytes2human(tot_after.bytes_sent), - bytes2human(tot_after.bytes_recv)) + bytes2human(tot_after.bytes_recv)), ) printl("total packets: sent: %-10s received: %s" % ( tot_after.packets_sent, tot_after.packets_recv)) diff --git a/scripts/who.py b/scripts/who.py index 18db1b17a9..77c474ff9b 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -26,7 +26,7 @@ def main(): user.terminal or '-', datetime.fromtimestamp(user.started).strftime("%Y-%m-%d %H:%M"), "(%s)" % user.host if user.host else "", - proc_name + proc_name, )) diff --git a/setup.py b/setup.py index ae25116ae0..1ab704854f 100755 --- a/setup.py +++ b/setup.py @@ -230,7 +230,7 @@ def get_winver(): ], # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"], - **py_limited_api + **py_limited_api # noqa (noqa needed for python 2.7) ) elif MACOS: @@ -244,7 +244,7 @@ def get_winver(): ), define_macros=macros, extra_link_args=[ - '-framework', 'CoreFoundation', '-framework', 'IOKit' + '-framework', 'CoreFoundation', '-framework', 'IOKit', ], **py_limited_api) @@ -313,7 +313,7 @@ def get_winver(): sources=sources + [ 'psutil/_psutil_sunos.c', 'psutil/arch/solaris/v10/ifaddrs.c', - 'psutil/arch/solaris/environ.c' + 'psutil/arch/solaris/environ.c', ], define_macros=macros, libraries=['kstat', 'nsl', 'socket'], @@ -473,7 +473,10 @@ def main(): elif which('rpm'): missdeps("sudo yum install gcc %s%s-devel" % (pyimpl, py3)) elif which('apk'): - missdeps("sudo apk add gcc %s%s-dev musl-dev linux-headers" % (pyimpl, py3)) + missdeps( + "sudo apk add gcc %s%s-dev musl-dev linux-headers" % ( + pyimpl, py3), + ) elif MACOS: print(hilite("XCode (https://developer.apple.com/xcode/) " "is not installed", color="red"), file=sys.stderr) From 3882a56488d411103ba556d0ca9ad66f4aa34d0c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Jan 2024 11:35:57 +0100 Subject: [PATCH 1063/1714] some tests refactoring --- psutil/tests/test_connections.py | 20 +-- psutil/tests/test_process.py | 201 ++++++++++++++++--------------- 2 files changed, 113 insertions(+), 108 deletions(-) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 13d1aebe9b..70929b84c1 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -52,17 +52,19 @@ class ConnectionTestCase(PsutilTestCase): def setUp(self): - if not (NETBSD or FREEBSD): - # process opens a UNIX socket to /var/log/run. - cons = thisproc.connections(kind='all') - assert not cons, cons + if NETBSD or FREEBSD or (MACOS and not PY3): + # Process opens a UNIX socket to /var/log/run. + return + cons = thisproc.connections(kind='all') + assert not cons, cons def tearDown(self): - if not (FREEBSD or 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 + # Make sure we closed all resources. + # Some BSDs open a UNIX socket to /var/log/run. + if NETBSD or FREEBSD or (MACOS and not PY3): + return + cons = thisproc.connections(kind='all') + assert not cons, cons def compare_procsys_connections(self, pid, proc_cons, kind='all'): """Given a process PID and its list of connections compare diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index e9a8f3ae64..d7d06854c5 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -347,6 +347,13 @@ def test_io_counters(self): @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) @@ -355,38 +362,33 @@ def test_ionice_linux(self): 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: - # 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 - 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: - ioclass, value = init - if ioclass == psutil.IOPRIO_CLASS_NONE: - value = 0 - p.ionice(ioclass, value) + 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') @@ -395,27 +397,26 @@ def test_ionice_win(self): 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: - # 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(init) + 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): @@ -821,59 +822,61 @@ def test_gids(self): 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() - try: - 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 + 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: - 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 - finally: + self.assertEqual(new_prio, prio) + else: try: - p.nice(init) + 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 From c458816fc24474a8879918d80d097480b66d6818 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Jan 2024 18:44:28 +0100 Subject: [PATCH 1064/1714] Adopt black formatting style (#2349) --- .github/workflows/build.yml | 2 +- .github/workflows/issues.py | 42 +- HISTORY.rst | 1 + Makefile | 14 +- docs/conf.py | 39 +- psutil/__init__.py | 111 +++- psutil/_common.py | 93 ++- psutil/_compat.py | 70 ++- psutil/_psaix.py | 90 ++- psutil/_psbsd.py | 120 +++- psutil/_pslinux.py | 368 +++++++---- psutil/_psosx.py | 46 +- psutil/_psposix.py | 32 +- psutil/_pssunos.py | 98 +-- psutil/_pswindows.py | 112 +++- psutil/tests/__init__.py | 245 +++++--- psutil/tests/__main__.py | 1 - psutil/tests/runner.py | 91 ++- psutil/tests/test_aix.py | 55 +- psutil/tests/test_bsd.py | 199 +++--- psutil/tests/test_connections.py | 145 +++-- psutil/tests/test_contracts.py | 83 ++- psutil/tests/test_linux.py | 628 +++++++++++-------- psutil/tests/test_memleaks.py | 19 +- psutil/tests/test_misc.py | 318 ++++++---- psutil/tests/test_osx.py | 34 +- psutil/tests/test_posix.py | 96 +-- psutil/tests/test_process.py | 248 +++++--- psutil/tests/test_sunos.py | 2 +- psutil/tests/test_system.py | 229 ++++--- psutil/tests/test_testutils.py | 47 +- psutil/tests/test_unicode.py | 23 +- psutil/tests/test_windows.py | 350 ++++++----- pyproject.toml | 7 + scripts/battery.py | 6 +- scripts/disk_usage.py | 9 +- scripts/free.py | 15 +- scripts/ifconfig.py | 36 +- scripts/internal/bench_oneshot.py | 12 +- scripts/internal/bench_oneshot_2.py | 6 +- scripts/internal/check_broken_links.py | 18 +- scripts/internal/clinter.py | 8 +- scripts/internal/download_wheels_appveyor.py | 12 +- scripts/internal/download_wheels_github.py | 10 +- scripts/internal/generate_manifest.py | 13 +- scripts/internal/git_pre_commit.py | 47 +- scripts/internal/print_access_denied.py | 7 +- scripts/internal/print_announce.py | 18 +- scripts/internal/print_api_speed.py | 34 +- scripts/internal/print_dist.py | 32 +- scripts/internal/print_downloads.py | 34 +- scripts/internal/print_hashes.py | 11 +- scripts/internal/print_timeline.py | 3 +- scripts/internal/winmake.py | 31 +- scripts/iotop.py | 9 +- scripts/netstat.py | 17 +- scripts/nettop.py | 13 +- scripts/pidof.py | 7 +- scripts/pmap.py | 8 +- scripts/procinfo.py | 44 +- scripts/procsmem.py | 6 +- scripts/ps.py | 33 +- scripts/sensors.py | 22 +- scripts/temperatures.py | 10 +- scripts/top.py | 86 ++- scripts/who.py | 5 +- scripts/winservices.py | 9 +- setup.py | 193 ++++-- 68 files changed, 3107 insertions(+), 1775 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f600b9d16f..38c164e7dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,7 +106,7 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install ruff rstcheck toml-sort sphinx + python3 -m pip install ruff black rstcheck toml-sort sphinx make lint-all # Check sanity of .tar.gz + wheel files diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index b89def6ffb..eddaa7f998 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -21,13 +21,14 @@ ROOT_DIR = os.path.realpath( - os.path.join(os.path.dirname(__file__), '..', '..')) + os.path.join(os.path.dirname(__file__), '..', '..') +) SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') # --- constants - +# fmt: off LABELS_MAP = { # platforms "linux": [ @@ -94,13 +95,15 @@ ], } -LABELS_MAP['scripts'].extend( - [x for x in os.listdir(SCRIPTS_DIR) if x.endswith('.py')]) - OS_LABELS = [ "linux", "windows", "macos", "freebsd", "openbsd", "netbsd", "openbsd", "bsd", "sunos", "unix", "wsl", "aix", "cygwin", ] +# fmt: on + +LABELS_MAP['scripts'].extend( + [x for x in os.listdir(SCRIPTS_DIR) if x.endswith('.py')] +) ILLOGICAL_PAIRS = [ ('bug', 'enhancement'), @@ -247,10 +250,12 @@ def add_labels_from_new_body(issue, text): # add bug/enhancement label log("search for 'Bug fix: y/n' line") r = re.search(r"\* Bug fix:.*?\n", text) - if is_pr(issue) and \ - r is not None and \ - not has_label(issue, "bug") and \ - not has_label(issue, "enhancement"): + if ( + is_pr(issue) + and r is not None + and not has_label(issue, "bug") + and not has_label(issue, "enhancement") + ): log("found") s = r.group(0).lower() if 'yes' in s: @@ -289,20 +294,25 @@ def add_labels_from_new_body(issue, text): def on_new_issue(issue): def has_text(text): - return text in issue.title.lower() or \ - (issue.body and text in issue.body.lower()) + return text in issue.title.lower() or ( + issue.body and text in issue.body.lower() + ) def body_mentions_python_h(): if not issue.body: return False body = issue.body.replace(' ', '') - return "#include\n^~~~" in body or \ - "#include\r\n^~~~" in body + return ( + "#include\n^~~~" in body + or "#include\r\n^~~~" in body + ) log("searching for missing Python.h") - if has_text("missing python.h") or \ - has_text("python.h: no such file or directory") or \ - body_mentions_python_h(): + if ( + has_text("missing python.h") + or has_text("python.h: no such file or directory") + or body_mentions_python_h() + ): log("found mention of Python.h") issue.create_comment(REPLY_MISSING_PYTHON_HEADERS) issue.edit(state='closed') diff --git a/HISTORY.rst b/HISTORY.rst index 0af24c1c67..8467b90e5b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,7 @@ 4 times faster. Before all connection types (TCP, UDP, UNIX) were retrieved internally, even if only a portion was returned. - 2342_, [NetBSD]: same as above (#2343) but for NetBSD. +- 2349_: adopted black formatting style. **Bug fixes** diff --git a/Makefile b/Makefile index 4e86550f6b..6b6c3f32e9 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ TSCRIPT = psutil/tests/runner.py # Internal. PY3_DEPS = \ + black \ check-manifest \ concurrencytest \ coverage \ @@ -18,6 +19,7 @@ PY3_DEPS = \ pypinfo \ requests \ rstcheck \ + ruff \ setuptools \ sphinx_rtd_theme \ teyit \ @@ -193,7 +195,10 @@ test-coverage: ## Run test coverage. # =================================================================== ruff: ## Run ruff linter. - @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --config=pyproject.toml --no-cache + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache + +black: ## Python files linting (via black) + @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe _pylint: ## Python pylint (not mandatory, just run it from time to time) @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} @@ -208,6 +213,7 @@ lint-toml: ## Linter for pyproject.toml @git ls-files '*.toml' | xargs toml-sort --check lint-all: ## Run all linters + ${MAKE} black ${MAKE} ruff ${MAKE} lint-c ${MAKE} lint-rst @@ -217,8 +223,11 @@ lint-all: ## Run all linters # Fixers # =================================================================== +fix-black: + git ls-files '*.py' | xargs $(PYTHON) -m black + fix-ruff: - @git ls-files '*.py' | xargs $(PYTHON) -m ruff --config=pyproject.toml --no-cache --fix + @git ls-files '*.py' | xargs $(PYTHON) -m ruff --no-cache --fix fix-unittests: ## Fix unittest idioms. @git ls-files '*test_*.py' | xargs $(PYTHON) -m teyit --show-stats @@ -228,6 +237,7 @@ fix-toml: ## Fix pyproject.toml fix-all: ## Run all code fixers. ${MAKE} fix-ruff + ${MAKE} fix-black ${MAKE} fix-unittests ${MAKE} fix-toml diff --git a/docs/conf.py b/docs/conf.py index 3adde46eb6..f2b05483cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,11 +56,13 @@ def get_version(): # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.imgmath', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.imgmath', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -271,15 +273,12 @@ def get_version(): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -289,8 +288,7 @@ def get_version(): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'psutil.tex', 'psutil Documentation', - AUTHOR, 'manual'), + (master_doc, 'psutil.tex', 'psutil Documentation', AUTHOR, 'manual') ] # The name of an image file (relative to this directory) to place at the top of @@ -330,10 +328,7 @@ def get_version(): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'psutil', 'psutil Documentation', - [author], 1), -] +man_pages = [(master_doc, 'psutil', 'psutil Documentation', [author], 1)] # If true, show URL addresses after external links. # @@ -345,11 +340,15 @@ def get_version(): # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'psutil', 'psutil Documentation', - author, 'psutil', 'One line description of project.', - 'Miscellaneous'), -] +texinfo_documents = [( + master_doc, + 'psutil', + 'psutil Documentation', + author, + 'psutil', + 'One line description of project.', + 'Miscellaneous', +)] # Documents to append as an appendix to all manuals. # @@ -373,5 +372,5 @@ def get_version(): 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', '_static/css/custom.css', - ], + ] } diff --git a/psutil/__init__.py b/psutil/__init__.py index 3e12b5de2d..8138db41e1 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -145,6 +145,7 @@ raise NotImplementedError('platform %s is not supported' % sys.platform) +# fmt: off __all__ = [ # exceptions "Error", "NoSuchProcess", "ZombieProcess", "AccessDenied", @@ -191,6 +192,7 @@ # "sensors_temperatures", "sensors_battery", "sensors_fans" # sensors "users", "boot_time", # others ] +# fmt: on __all__.extend(_psplatform.__extra__all__) @@ -225,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)): +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) @@ -250,6 +259,7 @@ # Faster version (Windows and Linux). _ppid_map = _psplatform.ppid_map 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(). @@ -390,7 +400,8 @@ def __str__(self): return "%s.%s(%s)" % ( self.__class__.__module__, self.__class__.__name__, - ", ".join(["%s=%r" % (k, v) for k, v in info.items()])) + ", ".join(["%s=%r" % (k, v) for k, v in info.items()]), + ) __repr__ = __str__ @@ -666,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 @@ -675,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 @@ -1042,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 @@ -1120,11 +1134,15 @@ def memory_percent(self, memtype="rss"): valid_types = list(_psplatform.pfullmem._fields) if memtype not in valid_types: msg = "invalid memtype %r; valid types are %r" % ( - memtype, tuple(valid_types), + memtype, + tuple(valid_types), ) raise ValueError(msg) - fun = self.memory_info if memtype in _psplatform.pmem._fields else \ - self.memory_full_info + fun = ( + self.memory_info + if memtype in _psplatform.pmem._fields + else self.memory_full_info + ) metrics = fun() value = getattr(metrics, memtype) @@ -1140,6 +1158,7 @@ def memory_percent(self, memtype="rss"): return (value / float(total_phymem)) * 100 if hasattr(_psplatform.Process, "memory_maps"): + def memory_maps(self, grouped=True): """Return process' mapped memory regions as a list of namedtuples whose fields are variable depending on the platform. @@ -1201,6 +1220,7 @@ 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() @@ -1307,11 +1327,13 @@ def wait(self, timeout=None): # 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 # ===================================================================== @@ -1388,7 +1410,8 @@ def __getattribute__(self, name): return object.__getattribute__(self.__subproc, name) except AttributeError: msg = "%s instance has no attribute '%s'" % ( - self.__class__.__name__, name, + self.__class__.__name__, + name, ) raise AttributeError(msg) @@ -1487,7 +1510,8 @@ def remove(pid): if proc.is_running(): if attrs is not None: proc.info = proc.as_dict( - attrs=attrs, ad_value=ad_value) + attrs=attrs, ad_value=ad_value + ) yield proc else: yield add(pid) @@ -1546,6 +1570,7 @@ 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) @@ -1666,7 +1691,7 @@ def cpu_times(percpu=False): try: _last_per_cpu_times = { - threading.current_thread().ident: cpu_times(percpu=True), + threading.current_thread().ident: cpu_times(percpu=True) } except Exception: # noqa: BLE001 # Don't want to crash at import time. @@ -2033,6 +2058,7 @@ def disk_partitions(all=False): If *all* parameter is False return physical devices only and ignore all others. """ + def pathconf(path, name): try: return os.pathconf(path, name) @@ -2045,7 +2071,8 @@ def pathconf(path, name): for item in ret: nt = item._replace( maxfile=pathconf(item.mountpoint, 'PC_NAME_MAX'), - maxpath=pathconf(item.mountpoint, 'PC_PATH_MAX')) + maxpath=pathconf(item.mountpoint, 'PC_PATH_MAX'), + ) new.append(nt) return new else: @@ -2100,7 +2127,8 @@ def disk_io_counters(perdisk=False, nowrap=True): 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" @@ -2149,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" @@ -2213,8 +2242,10 @@ def net_if_addrs(): except ValueError: if WINDOWS and fam == -1: fam = _psplatform.AF_LINK - elif (hasattr(_psplatform, "AF_LINK") and - fam == _psplatform.AF_LINK): + 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 @@ -2261,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 @@ -2281,7 +2313,8 @@ def convert(n): high = critical ret[name].append( - _common.shwtemp(label, current, high, critical)) + _common.shwtemp(label, current, high, critical) + ) return dict(ret) @@ -2372,6 +2405,7 @@ def _set_debug(value): messages to stderr. """ import psutil._common + psutil._common.PSUTIL_DEBUG = bool(value) _psplatform.cext.set_debug(bool(value)) @@ -2381,11 +2415,13 @@ def test(): # pragma: no cover from ._compat import get_terminal_size today_day = datetime.date.today() + # 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']) @@ -2396,8 +2432,9 @@ def test(): # pragma: no cover else: ctime = '' if p.info['cpu_times']: - cputime = time.strftime("%M:%S", - time.localtime(sum(p.info['cpu_times']))) + cputime = time.strftime( + "%M:%S", time.localtime(sum(p.info['cpu_times'])) + ) else: cputime = '' @@ -2410,12 +2447,21 @@ def test(): # pragma: no cover if user and WINDOWS and '\\' in user: user = user.split('\\')[1] 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 '' + 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']) @@ -2433,8 +2479,9 @@ def test(): # pragma: no cover status, ctime, cputime, - cmdline) - print(line[:get_terminal_size()[0]]) # NOQA + cmdline, + ) + print(line[: get_terminal_size()[0]]) # NOQA del memoize_when_activated, division diff --git a/psutil/_common.py b/psutil/_common.py index 95323aabc1..6989feafda 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -47,6 +47,7 @@ PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG')) _DEFAULT = object() +# fmt: off __all__ = [ # OS constants 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX', @@ -77,6 +78,7 @@ # shell utils 'hilite', 'term_supports_colors', 'print_color', ] +# fmt: on # =================================================================== @@ -138,6 +140,7 @@ NIC_DUPLEX_HALF = 1 NIC_DUPLEX_UNKNOWN = 0 else: + class NicDuplex(enum.IntEnum): NIC_DUPLEX_FULL = 2 NIC_DUPLEX_HALF = 1 @@ -150,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 @@ -174,6 +178,7 @@ class BatteryTime(enum.IntEnum): # --- for system functions +# fmt: off # psutil.swap_memory() sswap = namedtuple('sswap', ['total', 'used', 'free', 'percent', 'sin', 'sout']) @@ -214,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() @@ -229,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']) @@ -266,9 +275,7 @@ 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])}) # ===================================================================== @@ -298,7 +305,8 @@ def __str__(self): info = self._infodict(("pid", "ppid", "name")) if info: details = "(%s)" % ", ".join( - ["%s=%r" % (k, v) for k, v in info.items()]) + ["%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]) @@ -387,6 +395,7 @@ def __init__(self, seconds, pid=None, name=None): value = None """) else: + def raise_from(value, from_value): raise value @@ -426,6 +435,7 @@ def memoize(fun): It does NOT support: - methods """ + @functools.wraps(fun) def wrapper(*args, **kwargs): key = (args, frozenset(sorted(kwargs.items()))) @@ -473,6 +483,7 @@ def memoize_when_activated(fun): >>> foo() >>> """ + @functools.wraps(fun) def wrapper(self): try: @@ -579,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() @@ -638,9 +649,12 @@ 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 @@ -648,7 +662,9 @@ def outer(fun): def inner(self, *args, **kwargs): warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return getattr(self, replacement)(*args, **kwargs) + return inner + return outer @@ -783,8 +799,12 @@ def open_text(fname): # 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) + 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 @@ -845,9 +865,12 @@ def get_procfs_path(): if PY3: + def decode(s): return s.decode(encoding=ENCODING, errors=ENCODING_ERRS) + else: + def decode(s): return s @@ -863,6 +886,7 @@ def term_supports_colors(file=sys.stdout): # pragma: no cover return True try: import curses + assert file.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 @@ -877,14 +901,24 @@ def hilite(s, color=None, bold=False): # pragma: no cover if not term_supports_colors(): return s attr = [] - colors = dict(green='32', red='91', brown='33', yellow='93', blue='34', - violet='35', lightblue='36', grey='37', darkgrey='30') + 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()))) + raise ValueError( + "invalid color %r; choose between %s" % (list(colors.keys())) + ) attr.append(color) if bold: attr.append('1') @@ -892,7 +926,8 @@ def hilite(s, color=None, bold=False): # pragma: no cover def print_color( - s, color=None, bold=False, file=sys.stdout): # pragma: no cover + 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 @@ -903,16 +938,19 @@ def print_color( DEFAULT_COLOR = 7 GetStdHandle = ctypes.windll.Kernel32.GetStdHandle - SetConsoleTextAttribute = \ + 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()))) + raise ValueError( + "invalid color %r; choose between %r" + % (color, list(colors.keys())) + ) if bold and color <= 7: color += 8 @@ -921,7 +959,7 @@ def print_color( handle = GetStdHandle(handle_id) SetConsoleTextAttribute(handle, color) try: - print(s, file=file) # NOQA + print(s, file=file) # NOQA finally: SetConsoleTextAttribute(handle, DEFAULT_COLOR) @@ -930,13 +968,16 @@ 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) + 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("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), # NOQA - file=sys.stderr) + print( # noqa + "psutil-debug [%s:%s]> %s" % (fname, lineno, msg), file=sys.stderr + ) diff --git a/psutil/_compat.py b/psutil/_compat.py index 4957b22fdd..3db56c6019 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -16,6 +16,7 @@ import types +# fmt: off __all__ = [ # constants "PY3", @@ -31,7 +32,9 @@ "redirect_stderr", # python 3 exceptions "FileNotFoundError", "PermissionError", "ProcessLookupError", - "InterruptedError", "ChildProcessError", "FileExistsError"] + "InterruptedError", "ChildProcessError", "FileExistsError", +] +# fmt: on PY3 = sys.version_info[0] >= 3 @@ -49,6 +52,7 @@ def u(s): def b(s): return s.encode("latin-1") + else: long = long range = xrange @@ -147,7 +151,6 @@ def super(type_=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1): 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] @@ -155,7 +158,9 @@ def __init__(self, *args, **kwargs): if not attr.startswith('__'): setattr(self, attr, getattr(unwrap_me, attr)) else: - super(TemporaryClass, self).__init__(*args, **kwargs) # noqa + super(TemporaryClass, self).__init__( # noqa + *args, **kwargs + ) class __metaclass__(type): def __instancecheck__(cls, inst): @@ -181,8 +186,7 @@ def ProcessLookupError(inst): @_instance_checking_exception(EnvironmentError) def PermissionError(inst): - return getattr(inst, 'errno', _SENTINEL) in ( - errno.EACCES, errno.EPERM) + return getattr(inst, 'errno', _SENTINEL) in (errno.EACCES, errno.EPERM) @_instance_checking_exception(EnvironmentError) def InterruptedError(inst): @@ -202,8 +206,10 @@ def FileExistsError(inst): except FileExistsError: pass except OSError: - msg = ("broken or incompatible Python implementation, see: " - "https://github.com/giampaolo/psutil/issues/1659") + msg = ( + "broken or incompatible Python implementation, see: " + "https://github.com/giampaolo/psutil/issues/1659" + ) raise RuntimeError(msg) @@ -222,10 +228,11 @@ def FileExistsError(inst): 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 @@ -234,10 +241,17 @@ def __init__(self, tup, hash=hash): def __hash__(self): return self.hashvalue - 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): + 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()) @@ -256,6 +270,7 @@ def lru_cache(maxsize=100, typed=False): """Least-recently-used cache decorator, see: http://docs.python.org/3/library/functools.html#functools.lru_cache. """ + def decorating_function(user_function): cache = {} stats = [0, 0] @@ -269,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) @@ -284,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) @@ -294,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 @@ -309,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: @@ -334,8 +354,9 @@ def cache_info(): """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() @@ -362,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 @@ -371,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): @@ -414,6 +440,7 @@ def _access_check(fn, mode): try: from shutil import get_terminal_size except ImportError: + def get_terminal_size(fallback=(80, 24)): try: import fcntl @@ -425,7 +452,8 @@ def get_terminal_size(fallback=(80, 24)): try: # This should work on Linux. res = struct.unpack( - 'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234')) + 'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234') + ) return (res[1], res[0]) except Exception: # noqa: BLE001 return fallback @@ -435,6 +463,7 @@ def get_terminal_size(fallback=(80, 24)): try: from subprocess import TimeoutExpired as SubprocessTimeoutExpired except ImportError: + class SubprocessTimeoutExpired(Exception): pass @@ -443,6 +472,7 @@ class SubprocessTimeoutExpired(Exception): try: from contextlib import redirect_stderr except ImportError: + @contextlib.contextmanager def redirect_stderr(new_target): original = sys.stderr diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 6c2962c5e5..7310ab6c3d 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -53,7 +53,7 @@ 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, } @@ -80,7 +80,8 @@ nice=4, num_threads=5, status=6, - ttynr=7) + ttynr=7, +) # ===================================================================== @@ -148,8 +149,9 @@ def cpu_count_cores(): 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() @@ -160,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 + ) # ===================================================================== @@ -189,8 +192,9 @@ def disk_partitions(all=False): if not disk_usage(mountpoint).total: continue maxfile = maxpath = None # set later - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts, - maxfile, maxpath) + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -212,8 +216,10 @@ 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 = [] @@ -223,16 +229,23 @@ def net_connections(kind, _pid=-1): continue if type_ not in types: continue - nt = conn_to_ntuple(fd, fam, type_, laddr, raddr, status, - TCP_STATUSES, pid=pid if _pid == -1 else None) + 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: @@ -244,15 +257,20 @@ def net_if_stats(): # 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) @@ -312,6 +330,7 @@ def wrap_exceptions(fun): """Call callable into a try/except clause and translate ENOENT, EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: @@ -326,6 +345,7 @@ def wrapper(self, *args, **kwargs): raise ZombieProcess(self.pid, self._name, self._ppid) except PermissionError: raise AccessDenied(self.pid, self._name) + return wrapper @@ -378,17 +398,20 @@ def exe(self): 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 '' @@ -409,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) @@ -471,7 +495,7 @@ def cpu_times(self): 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: @@ -506,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) @@ -527,20 +555,20 @@ 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) if HAS_PROC_IO_COUNTERS: + @wrap_exceptions def io_counters(self): try: diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index fc03d1114f..da68f5efd5 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -141,6 +141,7 @@ # ===================================================================== +# fmt: off # psutil.virtual_memory() svmem = namedtuple( 'svmem', ['total', 'available', 'percent', 'used', 'free', @@ -170,6 +171,7 @@ else: sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', 'read_bytes', 'write_bytes']) +# fmt: on # ===================================================================== @@ -209,8 +211,19 @@ def virtual_memory(): 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(): @@ -232,6 +245,7 @@ def cpu_times(): if HAS_PER_CPU_TIMES: + def per_cpu_times(): """Return system CPU times as a namedtuple.""" ret = [] @@ -240,6 +254,7 @@ def per_cpu_times(): item = scputimes(user, nice, system, idle, irq) ret.append(item) return ret + else: # XXX # Ok, this is very dirty. @@ -267,10 +282,13 @@ def cpu_count_logical(): if OPENBSD or NETBSD: + def cpu_count_cores(): # OpenBSD and NetBSD do not implement this. return 1 if cpu_count_logical() == 1 else None + else: + 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: @@ -284,7 +302,7 @@ def cpu_count_cores(): # get rid of padding chars appended at the end of the string index = s.rfind("") if index != -1: - s = s[:index + 9] + s = s[: index + 9] root = ElementTree.fromstring(s) try: ret = len(root.findall('group/children/group/cpu')) or None @@ -314,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'): @@ -323,12 +342,14 @@ 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 @@ -352,7 +373,9 @@ def cpu_freq(): 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)] @@ -373,8 +396,9 @@ def disk_partitions(all=False): for partition in partitions: device, mountpoint, fstype, opts = partition maxfile = maxpath = None # set later - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts, - maxfile, maxpath) + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -410,16 +434,19 @@ def net_if_stats(): duplex = _common.NicDuplex(duplex) output_flags = ','.join(flags) isup = 'running' in flags - ret[name] = _common.snicstats(isup, duplex, speed, mtu, - output_flags) + ret[name] = _common.snicstats( + isup, duplex, speed, mtu, output_flags + ) return ret def net_connections(kind): """System-wide network connections.""" 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() @@ -432,8 +459,9 @@ def net_connections(kind): for item in rawlist: fd, fam, type, laddr, raddr, status, pid = item - nt = conn_to_ntuple(fd, fam, type, laddr, raddr, - status, TCP_STATUSES, pid) + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES, pid + ) ret.add(nt) return list(ret) @@ -472,7 +500,8 @@ def sensors_temperatures(): high = None name = "Core %s" % cpu ret["coretemp"].append( - _common.shwtemp(name, current, high, high)) + _common.shwtemp(name, current, high, high) + ) except NotImplementedError: pass @@ -533,6 +562,7 @@ def pids(): if OPENBSD or NETBSD: + def pid_exists(pid): """Return True if pid exists.""" exists = _psposix.pid_exists(pid) @@ -542,6 +572,7 @@ def pid_exists(pid): return pid in pids() else: return True + else: pid_exists = _psposix.pid_exists @@ -558,6 +589,7 @@ def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: @@ -576,6 +608,7 @@ def wrapper(self, *args, **kwargs): else: raise raise + return wrapper @@ -706,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): @@ -714,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): @@ -723,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']] @@ -738,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 @@ -759,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): @@ -776,8 +815,10 @@ def threads(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] ret = [] @@ -793,8 +834,9 @@ def connections(self, kind='inet'): 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) + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES + ) ret.append(nt) self._assert_alive() @@ -825,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): @@ -840,13 +883,15 @@ def cwd(self): 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 @@ -854,17 +899,20 @@ def _not_implemented(self): # FreeBSD < 8 does not support functions based on kinfo_getfile() # and kinfo_getvmmap() 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 HAS_PROC_NUM_FDS: + @wrap_exceptions def num_fds(self): """Return the number of file descriptors opened by this process.""" @@ -872,6 +920,7 @@ def num_fds(self): if NETBSD: self._assert_alive() return ret + else: num_fds = _not_implemented @@ -891,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: @@ -904,8 +954,9 @@ 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 @@ -919,7 +970,8 @@ def rlimit(self, resource, limits=None): else: if len(limits) != 2: raise ValueError( - "second argument must be a (soft, hard) tuple, " - "got %s" % repr(limits)) + "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/psutil/_pslinux.py b/psutil/_pslinux.py index a252f3850b..e19e37a974 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -58,6 +58,7 @@ enum = None +# fmt: off __extra__all__ = [ # 'PROCFS_PATH', @@ -69,6 +70,7 @@ "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", ] +# fmt: on # ===================================================================== @@ -104,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 @@ -115,6 +118,7 @@ IOPRIO_CLASS_BE = 2 IOPRIO_CLASS_IDLE = 3 else: + class IOPriority(enum.IntEnum): IOPRIO_CLASS_NONE = 0 IOPRIO_CLASS_RT = 1 @@ -163,6 +167,7 @@ class IOPriority(enum.IntEnum): # ===================================================================== +# fmt: off # psutil.virtual_memory() svmem = namedtuple( 'svmem', ['total', 'available', 'percent', 'used', 'free', @@ -197,6 +202,7 @@ class IOPriority(enum.IntEnum): pcputimes = namedtuple('pcputimes', ['user', 'system', 'children_user', 'children_system', 'iowait']) +# fmt: on # ===================================================================== @@ -311,8 +317,10 @@ def set_scputimes_ntuple(procfs_path): def prlimit(pid, resource_, limits=None): class StructRlimit(ctypes.Structure): - _fields_ = [('rlim_cur', ctypes.c_longlong), - ('rlim_max', ctypes.c_longlong)] + _fields_ = [ + ('rlim_cur', ctypes.c_longlong), + ('rlim_max', ctypes.c_longlong), + ] current = StructRlimit() if limits is None: @@ -324,7 +332,8 @@ class StructRlimit(ctypes.Structure): new.rlim_cur = limits[0] new.rlim_max = limits[1] ret = libc.prlimit( - pid, resource_, ctypes.byref(new), ctypes.byref(current)) + pid, resource_, ctypes.byref(new), ctypes.byref(current) + ) if ret != 0: errno_ = ctypes.get_errno() @@ -334,7 +343,8 @@ class StructRlimit(ctypes.Structure): if prlimit is not None: __extra__all__.extend( - [x for x in dir(cext) if x.startswith('RLIM') and x.isupper()]) + [x for x in dir(cext) if x.startswith('RLIM') and x.isupper()] + ) # ===================================================================== @@ -379,8 +389,11 @@ def calculate_avail_vmem(mems): lru_inactive_file = mems[b'Inactive(file):'] slab_reclaimable = mems[b'SReclaimable:'] except KeyError as err: - debug("%r is missing from /proc/meminfo; using an approximation " - "for calculating available memory" % err.args[0]) + 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()) @@ -461,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') @@ -516,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") + "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) + return svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + slab, + ) def swap_memory(): @@ -549,8 +575,10 @@ 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) + 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: @@ -592,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) @@ -610,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) @@ -673,8 +701,9 @@ def cpu_count_cores(): if not line: # new section try: - mapping[current_info[b'physical id']] = \ - current_info[b'cpu cores'] + mapping[current_info[b'physical id']] = current_info[ + b'cpu cores' + ] except KeyError: pass current_info = {} @@ -701,12 +730,16 @@ 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(): @@ -719,17 +752,19 @@ def _cpu_get_cpuinfo_freq(): return ret -if os.path.exists("/sys/devices/system/cpu/cpufreq/policy0") 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. """ 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 = 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 = [] pjoin = os.path.join @@ -754,11 +789,12 @@ def cpu_freq(): 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.) for x in _cpu_get_cpuinfo_freq()] + return [_common.scpufreq(x, 0.0, 0.0) for x in _cpu_get_cpuinfo_freq()] # ===================================================================== @@ -888,11 +924,13 @@ def decode_address(addr, family): 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(): @@ -911,12 +949,14 @@ def process_inet(file, family, type_, inodes, filter_pid=None): 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 @@ -955,8 +995,9 @@ 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)) + "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. @@ -978,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) @@ -993,17 +1036,19 @@ def retrieve(self, kind, pid=None): path = "%s/net/%s" % (self._procfs_path, proto_name) if family in (socket.AF_INET, socket.AF_INET6): ls = self.process_inet( - path, family, type_, inodes, filter_pid=pid) + path, family, type_, inodes, filter_pid=pid + ) else: - ls = self.process_unix( - path, 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) @@ -1027,37 +1072,49 @@ 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: @@ -1074,8 +1131,9 @@ def net_if_stats(): else: output_flags = ','.join(flags) isup = 'running' in flags - ret[name] = _common.snicstats(isup, duplex_map[duplex], speed, mtu, - output_flags) + ret[name] = _common.snicstats( + isup, duplex_map[duplex], speed, mtu, output_flags + ) return ret @@ -1091,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. @@ -1113,6 +1172,7 @@ def read_procfs(): for line in lines: fields = line.split() flen = len(fields) + # fmt: off if flen == 15: # Linux 2.4 name = fields[3] @@ -1133,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'): @@ -1142,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[: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() @@ -1154,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): @@ -1178,6 +1244,7 @@ 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 @@ -1291,8 +1358,9 @@ def disk_partitions(all=False): if device == '' or fstype not in fstypes: continue maxfile = maxpath = None # set later - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts, - maxfile, maxpath) + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -1329,7 +1397,8 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/1708 # https://github.com/giampaolo/psutil/pull/1648 basenames2 = glob.glob( - '/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*') + '/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) @@ -1386,19 +1455,23 @@ def sensors_temperatures(): continue trip_paths = glob.glob(base + '/trip_point*') - trip_points = set(['_'.join( - os.path.basename(p).split('_')[0:3]) for p in trip_paths]) + 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) + 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) + high = bcat( + os.path.join(base, trip_point + "_temp"), fallback=None + ) if high is not None: try: @@ -1469,8 +1542,11 @@ def multi_bcat(*paths): return ret.strip() return None - bats = [x for x in os.listdir(POWER_SUPPLY_PATH) if x.startswith('BAT') or - 'battery' in x.lower()] + 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 @@ -1479,15 +1555,9 @@ def multi_bcat(*paths): root = os.path.join(POWER_SUPPLY_PATH, sorted(bats)[0]) # Base metrics. - 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") + 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 @@ -1508,7 +1578,8 @@ def multi_bcat(*paths): power_plugged = None 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: @@ -1565,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) # ===================================================================== @@ -1627,7 +1697,7 @@ def ppid_map(): 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 @@ -1637,6 +1707,7 @@ 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: @@ -1651,6 +1722,7 @@ def wrapper(self, *args, **kwargs): if not os.path.exists("%s/%s" % (self._procfs_path, self.pid)): raise NoSuchProcess(self.pid, self._name) raise + return wrapper @@ -1678,7 +1750,7 @@ def _is_zombie(self): return False else: rpar = data.rfind(b')') - status = data[rpar + 2:rpar + 3] + status = data[rpar + 2 : rpar + 3] return status == b"Z" def _raise_if_zombie(self): @@ -1707,8 +1779,8 @@ def _parse_stat_file(self): # other parentheses. This is taken into account by looking for # the first occurrence of "(" and the last occurrence of ")". rpar = data.rfind(b')') - name = data[data.find(b'(') + 1:rpar] - fields = data[rpar + 2:].split() + name = data[data.find(b'(') + 1 : rpar] + fields = data[rpar + 2 :].split() ret = {} ret['name'] = name @@ -1815,6 +1887,7 @@ def terminal(self): # 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) @@ -1843,8 +1916,10 @@ def io_counters(self): 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)) + raise ValueError( + "%r field was not found in %s; found fields are %r" + % (err.args[0], fname, fields) + ) @wrap_exceptions def cpu_times(self): @@ -1890,8 +1965,9 @@ 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) if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS: @@ -1906,8 +1982,9 @@ def _parse_smaps_rollup(self): # 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: + 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 @@ -1920,11 +1997,12 @@ def _parse_smaps_rollup(self): @wrap_exceptions 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+)")): + 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. @@ -1954,8 +2032,10 @@ def memory_full_info(self): try: uss, pss, swap = self._parse_smaps_rollup() except (ProcessLookupError, FileNotFoundError) as err: - debug("ignore %r for pid %s and retry using " - "/proc/pid/smaps" % (err, self.pid)) + debug( + "ignore %r for pid %s and retry using /proc/pid/smaps" + % (err, self.pid) + ) uss, pss, swap = self._parse_smaps() else: uss, pss, swap = self._parse_smaps() @@ -1976,6 +2056,7 @@ def memory_maps(self): /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: @@ -1992,8 +2073,10 @@ 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() @@ -2011,19 +2094,21 @@ 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, + decode(addr), + decode(perms), + path, data.get(b'Rss:', 0), data.get(b'Size:', 0), data.get(b'Pss:', 0), @@ -2042,16 +2127,17 @@ def cwd(self): 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])) @@ -2071,7 +2157,10 @@ 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() @@ -2081,7 +2170,7 @@ def threads(self): 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 @@ -2112,7 +2201,8 @@ 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+)")): + 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) @@ -2132,12 +2222,14 @@ def cpu_affinity_set(self, cpus): for cpu in cpus: if cpu not in all_cpus: raise ValueError( - "invalid CPU number %r; choose between %s" % ( - cpu, eligible_cpus)) + "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)) + "between %s" % (cpu, eligible_cpus) + ) raise # only starting from kernel 2.6.13 @@ -2178,9 +2270,11 @@ def rlimit(self, resource_, limits=None): else: # set if len(limits) != 2: - raise ValueError( - "second argument must be a (soft, hard) tuple, " - "got %s" % repr(limits)) + 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: @@ -2227,7 +2321,10 @@ 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]) @@ -2239,7 +2336,8 @@ def open_files(self): 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: self._raise_if_not_alive() diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 482a9d430b..673ac0db75 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -91,6 +91,7 @@ # ===================================================================== +# fmt: off # psutil.cpu_times() scputimes = namedtuple('scputimes', ['user', 'nice', 'system', 'idle']) # psutil.virtual_memory() @@ -101,6 +102,7 @@ pmem = namedtuple('pmem', ['rss', 'vms', 'pfaults', 'pageins']) # psutil.Process.memory_full_info() pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) +# fmt: on # ===================================================================== @@ -121,8 +123,7 @@ def virtual_memory(): # 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(): @@ -164,10 +165,12 @@ def 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(): @@ -201,8 +204,9 @@ def disk_partitions(all=False): if not os.path.isabs(device) or not os.path.exists(device): continue maxfile = maxpath = None # set later - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts, - maxfile, maxpath) + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -274,8 +278,9 @@ def net_if_stats(): duplex = _common.NicDuplex(duplex) output_flags = ','.join(flags) isup = 'running' in flags - ret[name] = _common.snicstats(isup, duplex, speed, mtu, - output_flags) + ret[name] = _common.snicstats( + isup, duplex, speed, mtu, output_flags + ) return ret @@ -340,6 +345,7 @@ def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: @@ -351,6 +357,7 @@ def wrapper(self, *args, **kwargs): raise NoSuchProcess(self.pid, self._name) except PermissionError: raise AccessDenied(self.pid, self._name) + return wrapper @@ -420,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): @@ -428,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): @@ -453,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): @@ -462,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): @@ -495,15 +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] rawlist = cext.proc_connections(self.pid, families, types) ret = [] for item in rawlist: fd, fam, type, laddr, raddr, status = item - nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, - TCP_STATUSES) + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES + ) ret.append(nt) return ret diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 22d7c4f0d7..42bdfa7ef6 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -63,7 +63,8 @@ def pid_exists(pid): # 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])) + 'Negsignal', dict([(x.name, -x.value) for x in signal.Signals]) + ) def negsig_to_enum(num): """Convert a negative signal value to an enum.""" @@ -71,17 +72,23 @@ def negsig_to_enum(num): 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): +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), @@ -194,13 +201,13 @@ 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) @@ -216,7 +223,8 @@ 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 diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 4061af0141..dddbece1f3 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -89,7 +89,8 @@ uid=8, euid=9, gid=10, - egid=11) + egid=11, +) # ===================================================================== @@ -100,19 +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)) + 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields) +) # ===================================================================== @@ -140,9 +144,15 @@ 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) + 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) @@ -161,8 +171,9 @@ def swap_memory(): 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 + ) # ===================================================================== @@ -200,8 +211,9 @@ 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 + ) # ===================================================================== @@ -235,8 +247,9 @@ def disk_partitions(all=False): debug("skipping %r: %s" % (mountpoint, err)) continue maxfile = maxpath = None # set later - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts, - maxfile, maxpath) + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -259,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() @@ -346,6 +361,7 @@ def wrap_exceptions(fun): """Call callable into a try/except clause and translate ENOENT, EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: @@ -367,6 +383,7 @@ def wrapper(self, *args, **kwargs): else: raise raise + return wrapper @@ -405,8 +422,9 @@ def _proc_name_and_args(self): @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)): + 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) @@ -426,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. @@ -519,13 +538,13 @@ 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)) + '%s/%d/path/%d' % (procfs_path, self.pid, x) + ) except FileNotFoundError: hit_enoent = True continue @@ -570,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 @@ -619,12 +639,14 @@ def _get_unix_sockets(self, pid): # TODO: rewrite this in C (...but the damn netstat source code # does not include this part! Argh!!) cmd = ["pfiles", str(pid)] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + 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) @@ -660,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') @@ -670,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 = [] @@ -696,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 @@ -721,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/psutil/_pswindows.py b/psutil/_pswindows.py index f44a7010cf..2d3a0c9fdb 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -43,8 +43,10 @@ try: from . import _psutil_windows as cext except ImportError as err: - if str(err).lower().startswith("dll load failed") and \ - sys.getwindowsversion()[0] < 6: + 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 @@ -63,6 +65,7 @@ # 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 @@ -74,6 +77,7 @@ # others "CONN_DELETE_TCB", "AF_LINK", ] +# fmt: on # ===================================================================== @@ -107,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 @@ -123,11 +128,13 @@ class Priority(enum.IntEnum): 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( @@ -161,6 +168,7 @@ class IOPriority(enum.IntEnum): # ===================================================================== +# fmt: off # psutil.cpu_times() scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'interrupt', 'dpc']) @@ -183,6 +191,7 @@ class IOPriority(enum.IntEnum): pio = namedtuple('pio', ['read_count', 'write_count', 'read_bytes', 'write_bytes', 'other_count', 'other_bytes']) +# fmt: on # ===================================================================== @@ -199,7 +208,7 @@ def convert_dos_path(s): """ rawdrive = '\\'.join(s.split('\\')[:3]) driveletter = cext.QueryDosDevice(rawdrive) - remainder = s[len(rawdrive):] + remainder = s[len(rawdrive) :] return os.path.join(driveletter, remainder) @@ -253,7 +262,7 @@ def swap_memory(): # 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): + if total > 0: percentswap = cext.swap_percent() used = int(0.01 * percentswap * total) else: @@ -303,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(): @@ -330,8 +340,9 @@ 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(): @@ -371,15 +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 - nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, TCP_STATUSES, - pid=pid if _pid == -1 else None) + 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) @@ -503,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): @@ -521,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(): @@ -546,15 +571,17 @@ def _wrap_exceptions(self): yield except OSError as err: if is_permission_err(err): - raise AccessDenied( - pid=None, name=self._name, - msg="service %r is not querable (not enough privileges)" % - self._name) - elif err.winerror in (cext.ERROR_INVALID_NAME, - cext.ERROR_SERVICE_DOES_NOT_EXIST): - raise NoSuchProcess( - pid=None, name=self._name, - msg="service %r does not exist)" % self._name) + 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 @@ -671,12 +698,17 @@ def as_dict(self): 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). - return exc.errno in (errno.EPERM, errno.EACCES) or \ - getattr(exc, "winerror", -1) in (cext.ERROR_ACCESS_DENIED, - cext.ERROR_PRIVILEGE_NOT_HELD) + 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): @@ -691,12 +723,14 @@ def convert_oserror(exc, pid=None, name=None): def wrap_exceptions(fun): """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: raise convert_oserror(err, pid=self.pid, name=self._name) + return wrapper @@ -704,6 +738,7 @@ 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 @@ -723,6 +758,7 @@ def wrapper(self, *args, **kwargs): "returning {}".format(fun, times, err) ) raise AccessDenied(pid=self.pid, name=self._name, msg=msg) + return wrapper @@ -859,7 +895,7 @@ def memory_full_info(self): basic_mem = self.memory_info() uss = cext.proc_memory_uss(self.pid) uss *= getpagesize() - return pfullmem(*basic_mem + (uss, )) + return pfullmem(*basic_mem + (uss,)) def memory_maps(self): try: @@ -885,8 +921,10 @@ def send_signal(self, 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())): + elif sig in ( + getattr(signal, "CTRL_C_EVENT", object()), + getattr(signal, "CTRL_BREAK_EVENT", object()), + ): os.kill(self.pid, sig) else: msg = ( @@ -1047,8 +1085,12 @@ 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): + 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) @@ -1082,6 +1124,7 @@ def status(self): def cpu_affinity_get(self): def from_bitmask(x): return [i for i in range(64) if (1 << i) & x] + bitmask = cext.proc_cpu_affinity_get(self.pid) return from_bitmask(bitmask) @@ -1092,7 +1135,7 @@ def to_bitmask(ls): raise ValueError("invalid argument %r" % ls) out = 0 for b in ls: - out |= 2 ** b + out |= 2**b return out # SetProcessAffinityMask() states that ERROR_INVALID_PARAMETER @@ -1103,7 +1146,8 @@ def to_bitmask(ls): 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) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 0dd92b8b97..05f0d63b83 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -76,6 +76,7 @@ from psutil._psposix import wait_pid +# fmt: off __all__ = [ # constants 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', @@ -112,6 +113,7 @@ # others 'warn', 'copyload_shared_lib', 'is_namedtuple', ] +# fmt: on # =================================================================== @@ -127,7 +129,7 @@ CI_TESTING = APPVEYOR or GITHUB_ACTIONS COVERAGE = 'COVERAGE_RUN' in os.environ # are we a 64 bit process? -IS_64BIT = sys.maxsize > 2 ** 32 +IS_64BIT = sys.maxsize > 2**32 @memoize @@ -194,10 +196,10 @@ def macos_version(): # --- paths ROOT_DIR = os.path.realpath( - os.path.join(os.path.dirname(__file__), '..', '..')) + os.path.join(os.path.dirname(__file__), '..', '..') +) SCRIPTS_DIR = os.environ.get( - "PSUTIL_SCRIPTS_DIR", - os.path.join(ROOT_DIR, 'scripts'), + "PSUTIL_SCRIPTS_DIR", os.path.join(ROOT_DIR, 'scripts') ) HERE = os.path.realpath(os.path.dirname(__file__)) @@ -231,7 +233,8 @@ def _get_py_exe(): def attempt(exe): try: subprocess.check_call( - [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) except subprocess.CalledProcessError: return None else: @@ -253,11 +256,12 @@ def attempt(exe): 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()) + 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 @@ -271,8 +275,9 @@ def attempt(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_')] +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() @@ -340,6 +345,7 @@ def wrapper(*args, **kwargs): except Exception: reap_children() raise + return wrapper @@ -368,9 +374,9 @@ def spawn_testproc(cmd=None, **kwds): try: safe_rmpath(testfn) pyline = ( - "from time import sleep;" + - "open(r'%s', 'w').close();" % testfn + - "sleep(60);" + "from time import sleep;" + + "open(r'%s', 'w').close();" % testfn + + "sleep(60);" ) cmd = [PYTHON_EXE, "-c", pyline] sproc = subprocess.Popen(cmd, **kwds) @@ -527,6 +533,7 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): 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() @@ -683,13 +690,14 @@ def get_winver(): class retry: """A retry decorator.""" - def __init__(self, - exception=Exception, - timeout=None, - retries=None, - interval=0.001, - logfun=None, - ): + 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 @@ -738,8 +746,12 @@ def wrapper(*args, **kwargs): return wrapper -@retry(exception=psutil.NoSuchProcess, logfun=None, timeout=GLOBAL_TIMEOUT, - interval=0.001) +@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. @@ -750,8 +762,12 @@ def wait_for_pid(pid): time.sleep(0.01) -@retry(exception=(FileNotFoundError, AssertionError), logfun=None, - timeout=GLOBAL_TIMEOUT, interval=0.001) +@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: @@ -763,8 +779,12 @@ def wait_for_file(fname, delete=True, empty=False): return data -@retry(exception=AssertionError, logfun=None, timeout=GLOBAL_TIMEOUT, - interval=0.001) +@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. @@ -781,6 +801,7 @@ def call_until(fun, expr): 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 @@ -837,9 +858,8 @@ def create_exe(outpath, c_code=None): if c_code: if not which("gcc"): raise unittest.SkipTest("gcc is not installed") - if isinstance(c_code, bool): # c_code is True - c_code = textwrap.dedent( - """ + if isinstance(c_code, bool): # c_code is True + c_code = textwrap.dedent(""" #include int main() { pause(); @@ -887,7 +907,10 @@ def __str__(self): if not fqmod.startswith('psutil.'): fqmod = 'psutil.tests.' + fqmod return "%s.%s.%s" % ( - fqmod, self.__class__.__name__, self._testMethodName) + fqmod, + self.__class__.__name__, + self._testMethodName, + ) # assertRaisesRegexp renamed to assertRaisesRegex in 3.3; # add support for the new name. @@ -896,6 +919,7 @@ def __str__(self): # ...otherwise multiprocessing.Pool complains if not PY3: + def runTest(self): pass @@ -960,8 +984,7 @@ def assertPidGone(self, pid): try: psutil.Process(pid) except psutil.ZombieProcess: - raise AssertionError( - "wasn't supposed to raise 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 @@ -981,7 +1004,9 @@ def assertProcessGone(self, proc): self._check_proc_exc(proc, exc) else: msg = "Process.%s() didn't raise NSP and returned %r" % ( - name, ret) + name, + ret, + ) raise AssertionError(msg) proc.wait(timeout=0) # assert not raise TimeoutExpired @@ -1126,8 +1151,10 @@ def _check_fds(self, 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) + 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: @@ -1157,7 +1184,11 @@ def _check_mem(self, fun, times, retries, tolerance): 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) + idx, + bytes2human(mem), + bytes2human(mem / times), + times, + ) messages.append(msg) success = mem <= tolerance or mem <= prev_mem if success: @@ -1177,12 +1208,14 @@ def _check_mem(self, fun, times, retries, tolerance): def call(self, fun): return fun() - def execute(self, fun, times=None, warmup_times=None, retries=None, - tolerance=None): + 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 + 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: @@ -1201,6 +1234,7 @@ 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) @@ -1213,6 +1247,7 @@ def print_sysinfo(): import getpass import locale import pprint + try: import pip except ImportError: @@ -1230,14 +1265,14 @@ def print_sysinfo(): elif psutil.OSX: info['OS'] = 'Darwin %s' % platform.mac_ver()[0] elif psutil.WINDOWS: - info['OS'] = "Windows " + ' '.join( - map(str, platform.win32_ver())) + 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()]) + list(platform.architecture()) + [platform.machine()] + ) if psutil.POSIX: info['kernel'] = platform.uname()[2] @@ -1245,7 +1280,8 @@ def print_sysinfo(): info['python'] = ', '.join([ platform.python_implementation(), platform.python_version(), - platform.python_compiler()]) + platform.python_compiler(), + ]) info['pip'] = getattr(pip, '__version__', 'not installed') if wheel is not None: info['pip'] += " (wheel=%s)" % wheel.__version__ @@ -1266,7 +1302,8 @@ def print_sysinfo(): 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") + 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("~") @@ -1278,13 +1315,20 @@ def print_sysinfo(): # metrics info['cpus'] = psutil.cpu_count() info['loadavg'] = "%.1f%%, %.1f%%, %.1f%%" % ( - tuple([x / psutil.cpu_count() * 100 for x in psutil.getloadavg()])) + 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)) + 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)) + int(swap.percent), + bytes2human(swap.used), + bytes2human(swap.total), + ) info['pids'] = len(psutil.pids()) pinfo = psutil.Process().as_dict() pinfo.pop('memory_maps', None) @@ -1341,10 +1385,7 @@ class process_namespace: ... fun() """ - utils = [ - ('cpu_percent', (), {}), - ('memory_percent', (), {}), - ] + utils = [('cpu_percent', (), {}), ('memory_percent', (), {})] ignored = [ ('as_dict', (), {}), @@ -1355,7 +1396,7 @@ class process_namespace: ('parent', (), {}), ('parents', (), {}), ('pid', (), {}), - ('wait', (0, ), {}), + ('wait', (0,), {}), ] getters = [ @@ -1387,7 +1428,7 @@ class process_namespace: if HAS_IONICE: getters += [('ionice', (), {})] if HAS_RLIMIT: - getters += [('rlimit', (psutil.RLIMIT_NOFILE, ), {})] + getters += [('rlimit', (psutil.RLIMIT_NOFILE,), {})] if HAS_CPU_AFFINITY: getters += [('cpu_affinity', (), {})] if HAS_PROC_CPU_NUM: @@ -1401,29 +1442,29 @@ class process_namespace: setters = [] if POSIX: - setters += [('nice', (0, ), {})] + setters += [('nice', (0,), {})] else: - setters += [('nice', (psutil.NORMAL_PRIORITY_CLASS, ), {})] + 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, ), {})] + setters += [('ionice', (psutil.IOPRIO_NORMAL,), {})] if HAS_CPU_AFFINITY: - setters += [('cpu_affinity', ([_get_eligible_cpu()], ), {})] + setters += [('cpu_affinity', ([_get_eligible_cpu()],), {})] killers = [ - ('send_signal', (signal.SIGTERM, ), {}), + ('send_signal', (signal.SIGTERM,), {}), ('suspend', (), {}), ('resume', (), {}), ('terminate', (), {}), ('kill', (), {}), ] if WINDOWS: - killers += [('send_signal', (signal.CTRL_C_EVENT, ), {})] - killers += [('send_signal', (signal.CTRL_BREAK_EVENT, ), {})] + killers += [('send_signal', (signal.CTRL_C_EVENT,), {})] + killers += [('send_signal', (signal.CTRL_BREAK_EVENT,), {})] all = utils + getters + setters + killers @@ -1456,7 +1497,9 @@ def test_class_coverage(cls, test_class, 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) + test_class.__class__.__name__, + meth_name, + ) raise AttributeError(msg) @classmethod @@ -1487,12 +1530,12 @@ class system_namespace: ('cpu_times', (), {'percpu': True}), ('disk_io_counters', (), {'perdisk': True}), ('disk_partitions', (), {'all': True}), - ('disk_usage', (os.getcwd(), ), {}), + ('disk_usage', (os.getcwd(),), {}), ('net_connections', (), {'kind': 'all'}), ('net_if_addrs', (), {}), ('net_if_stats', (), {}), ('net_io_counters', (), {'pernic': True}), - ('pid_exists', (os.getpid(), ), {}), + ('pid_exists', (os.getpid(),), {}), ('pids', (), {}), ('swap_memory', (), {}), ('users', (), {}), @@ -1510,11 +1553,11 @@ class system_namespace: getters += [('sensors_battery', (), {})] if WINDOWS: getters += [('win_service_iter', (), {})] - getters += [('win_service_get', ('alg', ), {})] + getters += [('win_service_get', ('alg',), {})] ignored = [ ('process_iter', (), {}), - ('wait_procs', ([psutil.Process()], ), {}), + ('wait_procs', ([psutil.Process()],), {}), ('cpu_percent', (), {}), ('cpu_times_percent', (), {}), ] @@ -1550,15 +1593,18 @@ 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) + 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): @@ -1569,12 +1615,15 @@ def wrapper(*args, **kwargs): 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): @@ -1584,10 +1633,14 @@ def wrapper(*args, **kwargs): if only_if is not None: if not only_if: raise - msg = "%r was skipped because it raised NotImplementedError" \ - % fun.__name__ + msg = ( + "%r was skipped because it raised NotImplementedError" + % fun.__name__ + ) raise unittest.SkipTest(msg) + return wrapper + return decorator @@ -1714,6 +1767,7 @@ def check_net_address(addr, family): 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: @@ -1737,6 +1791,7 @@ def check_net_address(addr, 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) @@ -1773,8 +1828,11 @@ def check_family(conn): 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 + 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: @@ -1797,8 +1855,9 @@ def check_addrs(conn): def check_status(conn): assert isinstance(conn.status, str), conn.status - valids = [getattr(psutil, x) for x in dir(psutil) - if x.startswith('CONN_')] + 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 @@ -1821,10 +1880,12 @@ 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) @@ -1834,9 +1895,11 @@ 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) @@ -1866,6 +1929,7 @@ def is_namedtuple(x): if POSIX: + @contextlib.contextmanager def copyload_shared_lib(suffix=""): """Ctx manager which picks up a random shared CO lib used @@ -1875,9 +1939,11 @@ def copyload_shared_lib(suffix=""): 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()] + 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: @@ -1885,7 +1951,9 @@ def copyload_shared_lib(suffix=""): 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 @@ -1895,15 +1963,22 @@ def copyload_shared_lib(suffix=""): """ 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()] + 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()] + 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 diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 0cf39cc80a..2460abdb36 100644 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -1,4 +1,3 @@ - # 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. diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index a678b4c419..a054e4817e 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -58,7 +58,9 @@ USE_COLORS = not CI_TESTING and term_supports_colors() HERE = os.path.abspath(os.path.dirname(__file__)) -loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase # noqa +loadTestsFromTestCase = ( # noqa: N816 + unittest.defaultTestLoader.loadTestsFromTestCase +) def cprint(msg, color, bold=False, file=None): @@ -78,10 +80,13 @@ class TestLoader: 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] + 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 @@ -91,8 +96,9 @@ def _iter_testmod_classes(self): mod = import_module_by_path(path) for name in dir(mod): obj = getattr(mod, name) - if isinstance(obj, type) and \ - issubclass(obj, unittest.TestCase): + if isinstance(obj, type) and issubclass( + obj, unittest.TestCase + ): yield obj def all(self): @@ -121,7 +127,6 @@ def from_name(self, name): class ColouredResult(unittest.TextTestResult): - def addSuccess(self, test): unittest.TestResult.addSuccess(self, test) cprint("OK", "green") @@ -200,7 +205,6 @@ def run(self, suite): class ParallelRunner(ColouredTextRunner): - @staticmethod def _parallelize(suite): def fdopen(fd, mode, *kwds): @@ -240,8 +244,11 @@ def run(self, suite): par_suite = self._parallelize(par_suite) # run parallel - cprint("starting parallel tests using %s workers" % NWORKERS, - "green", bold=True) + cprint( + "starting parallel tests using %s workers" % NWORKERS, + "green", + bold=True, + ) t = time.time() par = self._run(par_suite) par_elapsed = time.time() - t @@ -262,13 +269,15 @@ def run(self, suite): # 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(""" + 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 | +----------+----------+----------+----------+----------+----------+ @@ -276,10 +285,29 @@ def run(self, suite): +----------+----------+----------+----------+----------+----------+ | 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)) + """ + % ( + 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) @@ -287,6 +315,7 @@ def run(self, suite): 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.") @@ -316,12 +345,18 @@ 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") + 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: diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 4a23b774ac..e7e0c8aa51 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -19,7 +19,6 @@ @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*" @@ -28,7 +27,8 @@ def test_virtual_memory(self): matchobj = re.search(re_pattern, out) self.assertIsNotNone( - matchobj, "svmon command returned unexpected output") + matchobj, "svmon command returned unexpected output" + ) KB = 1024 total = int(matchobj.group("size")) * KB @@ -41,14 +41,17 @@ def test_virtual_memory(self): # 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 + TOLERANCE_SYS_MEM = 2 * KB * KB # 2 MB self.assertEqual(psutil_result.total, total) self.assertAlmostEqual( - psutil_result.used, used, delta=TOLERANCE_SYS_MEM) + psutil_result.used, used, delta=TOLERANCE_SYS_MEM + ) self.assertAlmostEqual( - psutil_result.available, available, delta=TOLERANCE_SYS_MEM) + psutil_result.available, available, delta=TOLERANCE_SYS_MEM + ) self.assertAlmostEqual( - psutil_result.free, free, delta=TOLERANCE_SYS_MEM) + psutil_result.free, free, delta=TOLERANCE_SYS_MEM + ) def test_swap_memory(self): out = sh('/usr/sbin/lsps -a') @@ -56,16 +59,20 @@ def test_swap_memory(self): # 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) + 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") + matchobj, "lsps command returned unexpected output" + ) total_mb = int(matchobj.group("size")) - MB = 1024 ** 2 + 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 @@ -75,14 +82,17 @@ 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(): + 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") + matchobj, "mpstat command returned unexpected output" + ) # numbers are usually in the millions so 1000 is ok for tolerance CPU_STATS_TOLERANCE = 1000 @@ -90,19 +100,23 @@ def test_cpu_stats(self): self.assertAlmostEqual( psutil_result.ctx_switches, int(matchobj.group("cs")), - delta=CPU_STATS_TOLERANCE) + delta=CPU_STATS_TOLERANCE, + ) self.assertAlmostEqual( psutil_result.syscalls, int(matchobj.group("sysc")), - delta=CPU_STATS_TOLERANCE) + delta=CPU_STATS_TOLERANCE, + ) self.assertAlmostEqual( psutil_result.interrupts, int(matchobj.group("dev")), - delta=CPU_STATS_TOLERANCE) + delta=CPU_STATS_TOLERANCE, + ) self.assertAlmostEqual( psutil_result.soft_interrupts, int(matchobj.group("soft")), - delta=CPU_STATS_TOLERANCE) + delta=CPU_STATS_TOLERANCE, + ) def test_cpu_count_logical(self): out = sh('/usr/bin/mpstat -a') @@ -119,4 +133,5 @@ def test_net_if_addrs_names(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index c5a5e7abc9..7b502bcb80 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -48,9 +48,9 @@ def sysctl(cmdline): """ result = sh("sysctl " + cmdline) if FREEBSD: - result = result[result.find(": ") + 2:] + result = result[result.find(": ") + 2 :] elif OPENBSD or NETBSD: - result = result[result.find("=") + 1:] + result = result[result.find("=") + 1 :] try: return int(result) except ValueError: @@ -90,8 +90,9 @@ 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)) + 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): @@ -142,8 +143,9 @@ def test_net_if_stats(self): 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])) + self.assertEqual( + stats.mtu, int(re.findall(r'mtu (\d+)', out)[0]) + ) # ===================================================================== @@ -153,7 +155,6 @@ def test_net_if_stats(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") class FreeBSDPsutilTestCase(PsutilTestCase): - @classmethod def setUpClass(cls): cls.pid = spawn_testproc().pid @@ -179,13 +180,16 @@ def test_memory_maps(self): def test_exe(self): out = sh('procstat -b %s' % self.pid) - self.assertEqual(psutil.Process(self.pid).exe(), - out.split('\n')[1].split()[-1]) + 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:])) + 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) @@ -243,7 +247,6 @@ def test_cpu_times(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") class FreeBSDSystemTestCase(PsutilTestCase): - @staticmethod def parse_swapinfo(): # the last line is always the total @@ -282,38 +285,44 @@ def test_cpu_frequency_against_sysctl(self): @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) + 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) + 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) + 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) + 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) + 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) + self.assertAlmostEqual( + psutil.virtual_memory().buffers, syst, delta=TOLERANCE_SYS_MEM + ) # --- virtual_memory(); tests against muse @@ -326,61 +335,79 @@ def test_muse_vmem_total(self): @retry_on_failure() def test_muse_vmem_active(self): num = muse('Active') - self.assertAlmostEqual(psutil.virtual_memory().active, num, - delta=TOLERANCE_SYS_MEM) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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, @@ -391,24 +418,27 @@ def test_cpu_stats_syscalls(self): def test_swapmem_free(self): total, used, free = self.parse_swapinfo() self.assertAlmostEqual( - psutil.swap_memory().free, free, delta=TOLERANCE_SYS_MEM) + 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) + 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) + 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(',')] + s = s[s.find(" sec = ") + 7 :] + s = s[: s.find(',')] btime = int(s) self.assertEqual(btime, psutil.boot_time()) @@ -422,8 +452,9 @@ def secs2hours(secs): 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")]) + 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:'] @@ -435,10 +466,13 @@ def secs2hours(secs): @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) + 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) @@ -469,13 +503,16 @@ def test_sensors_temperatures_against_sysctl(self): self.skipTest("temperatures not supported by kernel") self.assertAlmostEqual( psutil.sensors_temperatures()["coretemp"][cpu].current, - sysctl_result, delta=10) + 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) + sysctl_result, + ) # ===================================================================== @@ -485,7 +522,6 @@ def test_sensors_temperatures_against_sysctl(self): @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") @@ -500,7 +536,6 @@ def test_boot_time(self): @unittest.skipIf(not NETBSD, "NETBSD only") class NetBSDTestCase(PsutilTestCase): - @staticmethod def parse_meminfo(look_for): with open('/proc/meminfo') as f: @@ -513,39 +548,52 @@ def parse_meminfo(look_for): def test_vmem_total(self): self.assertEqual( - psutil.virtual_memory().total, self.parse_meminfo("MemTotal:")) + 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) + 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) + 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) + 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) + 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) + 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) + psutil.swap_memory().free, + self.parse_meminfo("SwapFree:"), + delta=TOLERANCE_SYS_MEM, + ) def test_swapmem_used(self): smem = psutil.swap_memory() @@ -562,7 +610,8 @@ def test_cpu_stats_interrupts(self): else: raise ValueError("couldn't find line") self.assertAlmostEqual( - psutil.cpu_stats().interrupts, interrupts, delta=1000) + psutil.cpu_stats().interrupts, interrupts, delta=1000 + ) def test_cpu_stats_ctx_switches(self): with open('/proc/stat', 'rb') as f: @@ -573,9 +622,11 @@ def test_cpu_stats_ctx_switches(self): else: raise ValueError("couldn't find line") self.assertAlmostEqual( - psutil.cpu_stats().ctx_switches, ctx_switches, delta=1000) + 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/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 70929b84c1..5924dbb2c0 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -50,7 +50,6 @@ @serialrun class ConnectionTestCase(PsutilTestCase): - def setUp(self): if NETBSD or FREEBSD or (MACOS and not PY3): # Process opens a UNIX socket to /var/log/run. @@ -88,7 +87,6 @@ def compare_procsys_connections(self, pid, proc_cons, kind='all'): class TestBasicOperations(ConnectionTestCase): - @unittest.skipIf(SKIP_SYSCONS, "requires root") def test_system(self): with create_sockets(): @@ -136,7 +134,8 @@ def check_socket(self, sock): self.assertEqual(conn.family, sock.family) # see: http://bugs.python.org/issue30204 self.assertEqual( - conn.type, sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)) + conn.type, sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) + ) # local address laddr = sock.getsockname() @@ -257,7 +256,6 @@ def test_unix(self): class TestFilters(ConnectionTestCase): - def test_filters(self): def check(kind, families, types): for conn in thisproc.connections(kind=kind): @@ -269,45 +267,43 @@ def check(kind, families, types): 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]) + 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]) + 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") + 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) @@ -348,13 +344,17 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): # 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) + 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) + family=int(AF_INET), addr="127.0.0.1", testfn=testfile + ) tcp6_template = tcp_template.format( - family=int(AF_INET6), addr="::1", testfn=testfile) + family=int(AF_INET6), addr="::1", testfn=testfile + ) udp6_template = udp_template.format( - family=int(AF_INET6), addr="::1", testfn=testfile) + family=int(AF_INET6), addr="::1", testfn=testfile + ) # launch various subprocess instantiating a socket of various # families and types to enrich psutil results @@ -379,24 +379,52 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): 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")) + 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")) + 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")) + 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")) + 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(): @@ -471,6 +499,7 @@ def check(cons, families, types_): 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: @@ -511,17 +540,18 @@ def test_multi_sockets_procs(self): for fname in fnames: wait_for_file(fname) - syscons = [x for x in psutil.net_connections(kind='all') if x.pid - in pids] + 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) + 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 = [] @@ -543,4 +573,5 @@ def test_connection_constants(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 9c54299a27..aa5a20abb9 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -63,11 +63,10 @@ # Make sure code reflects what doc promises in terms of APIs # availability. -class TestAvailConstantsAPIs(PsutilTestCase): +class TestAvailConstantsAPIs(PsutilTestCase): def test_PROCFS_PATH(self): - self.assertEqual(hasattr(psutil, "PROCFS_PATH"), - LINUX or SUNOS or AIX) + self.assertEqual(hasattr(psutil, "PROCFS_PATH"), LINUX or SUNOS or AIX) def test_win_priority(self): ae = self.assertEqual @@ -92,8 +91,9 @@ def test_linux_ioprio_windows(self): ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) - @unittest.skipIf(GITHUB_ACTIONS and LINUX, - "unsupported on GITHUB_ACTIONS + LINUX") + @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) @@ -127,7 +127,6 @@ def test_rlimit(self): class TestAvailSystemAPIs(PsutilTestCase): - def test_win_service_iter(self): self.assertEqual(hasattr(psutil, "win_service_iter"), WINDOWS) @@ -135,27 +134,39 @@ 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) + 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) + 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) + 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) + 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) @@ -169,8 +180,9 @@ def test_terminal(self): def test_ionice(self): self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) - @unittest.skipIf(GITHUB_ACTIONS and LINUX, - "unsupported on GITHUB_ACTIONS + LINUX") + @unittest.skipIf( + GITHUB_ACTIONS and LINUX, "unsupported on GITHUB_ACTIONS + LINUX" + ) def test_rlimit(self): self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX or FREEBSD) @@ -185,17 +197,19 @@ 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) + 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) + 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)) + self.assertEqual(hasit, not (OPENBSD or NETBSD or AIX or MACOS)) # =================================================================== @@ -237,8 +251,9 @@ 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( + 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: @@ -334,7 +349,6 @@ def test_users(self): class TestProcessWaitType(PsutilTestCase): - @unittest.skipIf(not POSIX, "not POSIX") def test_negative_signal(self): p = psutil.Process(self.spawn_testproc().pid) @@ -443,7 +457,11 @@ def test_all(self): 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) + 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" @@ -556,7 +574,8 @@ def ionice(self, ret, info): psutil.IOPRIO_VERYLOW, psutil.IOPRIO_LOW, psutil.IOPRIO_NORMAL, - psutil.IOPRIO_HIGH] + psutil.IOPRIO_HIGH, + ] self.assertIsInstance(ret, int) self.assertGreaterEqual(ret, 0) self.assertIn(ret, choices) @@ -730,8 +749,11 @@ def nice(self, ret, info): if POSIX: assert -20 <= ret <= 20, ret else: - priorities = [getattr(psutil, x) for x in dir(psutil) - if x.endswith('_PRIORITY_CLASS')] + 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) @@ -759,4 +781,5 @@ def environ(self, ret, info): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 5c62842a4d..ada196bcda 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -61,7 +61,7 @@ SIOCGIFADDR = 0x8915 SIOCGIFCONF = 0x8912 SIOCGIFHWADDR = 0x8927 -SIOCGIFNETMASK = 0x891b +SIOCGIFNETMASK = 0x891B SIOCGIFBRDADDR = 0x8919 if LINUX: SECTOR_SIZE = 512 @@ -75,41 +75,47 @@ 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]) + 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]) + 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]) + fcntl.ioctl( + s.fileno(), SIOCGIFBRDADDR, struct.pack('256s', ifname) + )[20:24] + ) def get_ipv6_addresses(ifname): @@ -127,7 +133,7 @@ def get_ipv6_addresses(ifname): unformatted = all_fields[i][0] groups = [] for j in range(0, len(unformatted), 4): - groups.append(unformatted[j:j + 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) @@ -136,18 +142,23 @@ def get_ipv6_addresses(ifname): 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)) + 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] @@ -164,7 +175,8 @@ def free_swap(): 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)) + "can't find 'Swap' in 'free' output:\n%s" % '\n'.join(lines) + ) def free_physmem(): @@ -179,13 +191,14 @@ def free_physmem(): 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]) + total, used, free, shared = (int(x) for x in line.split()[1:5]) nt = collections.namedtuple( - 'free', 'total used free shared output') + '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)) + "can't find 'Mem' in 'free' output:\n%s" % '\n'.join(lines) + ) def vmstat(stat): @@ -209,6 +222,7 @@ 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] @@ -233,6 +247,7 @@ 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 @@ -252,7 +267,6 @@ def open_mock(name, *args, **kwargs): @unittest.skipIf(not LINUX, "LINUX only") class TestSystemVirtualMemoryAgainstFree(PsutilTestCase): - def test_total(self): cli_value = free_physmem().total psutil_value = psutil.virtual_memory().total @@ -268,15 +282,17 @@ def test_used(self): 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) + 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) + self.assertAlmostEqual( + cli_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) @retry_on_failure() def test_shared(self): @@ -286,8 +302,11 @@ def test_shared(self): 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)) + 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): @@ -301,18 +320,21 @@ def test_available(self): 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)) + 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) + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) @retry_on_failure() def test_used(self): @@ -325,40 +347,44 @@ def test_used(self): vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( - vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM) + 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) + 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) + 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) + 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) + 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 @@ -381,7 +407,8 @@ def test_warnings_on_misses(self): self.assertEqual(len(ws), 1) w = ws[0] self.assertIn( - "memory stats couldn't be determined", str(w.message)) + "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)) @@ -437,7 +464,8 @@ def test_avail_old_comes_from_kernel(self): self.assertEqual(ret.available, 6574984 * 1024) w = ws[0] self.assertIn( - "inactive memory stats couldn't be determined", str(w.message)) + "inactive memory stats couldn't be determined", str(w.message) + ) def test_avail_old_missing_fields(self): # Remove Active(file), Inactive(file) and SReclaimable @@ -461,7 +489,8 @@ def test_avail_old_missing_fields(self): self.assertEqual(ret.available, 2057400 * 1024 + 4818144 * 1024) w = ws[0] self.assertIn( - "inactive memory stats couldn't be determined", str(w.message)) + "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 @@ -482,16 +511,19 @@ def test_avail_old_missing_zoneinfo(self): """).encode() with mock_open_content({"/proc/meminfo": content}): with mock_open_exception( - "/proc/zoneinfo", - IOError(errno.ENOENT, 'no such file or directory')): + "/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) + ret.available, 2057400 * 1024 + 4818144 * 1024 + ) w = ws[0] self.assertIn( "inactive memory stats couldn't be determined", - str(w.message)) + str(w.message), + ) def test_virtual_memory_mocked(self): # Emulate /proc/meminfo because neither vmstat nor free return slab. @@ -567,7 +599,6 @@ def test_virtual_memory_mocked(self): @unittest.skipIf(not LINUX, "LINUX only") class TestSystemSwapMemory(PsutilTestCase): - @staticmethod def meminfo_has_swap_info(): """Return True if /proc/meminfo provides swap metrics.""" @@ -579,21 +610,24 @@ 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) + 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) + 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) + free_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) def test_missing_sin_sout(self): with mock.patch('psutil._common.open', create=True) as m: @@ -605,15 +639,17 @@ def test_missing_sin_sout(self): w = ws[0] self.assertIn( "'sin' and 'sout' swap memory stats couldn't " - "be determined", str(w.message)) + "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: + "/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() @@ -623,7 +659,8 @@ def test_no_vmstat_mocked(self): self.assertIn( "'sin' and 'sout' swap memory stats couldn't " "be determined and were set to 0", - str(w.message)) + str(w.message), + ) self.assertEqual(ret.sin, 0) self.assertEqual(ret.sout, 0) @@ -637,6 +674,7 @@ def test_meminfo_against_sysinfo(self): 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 @@ -659,7 +697,6 @@ def test_emulate_meminfo_has_no_metrics(self): @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] @@ -680,9 +717,10 @@ def test_fields(self): @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") + @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() @@ -690,8 +728,10 @@ def test_against_sysdev_cpu_online(self): 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") + @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]) @@ -710,11 +750,13 @@ def test_against_lscpu(self): 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: + 'psutil._pslinux.os.sysconf', side_effect=ValueError + ) as m: self.assertEqual(psutil._pslinux.cpu_count_logical(), original) assert m.called @@ -731,8 +773,9 @@ def test_emulate_fallbacks(self): 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: + 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; @@ -744,7 +787,6 @@ def test_emulate_fallbacks(self): @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") @@ -773,7 +815,6 @@ def test_emulate_none(self): @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 @@ -784,8 +825,9 @@ def path_exists_mock(path): return orig_exists(path) orig_exists = os.path.exists - with mock.patch("os.path.exists", side_effect=path_exists_mock, - create=True): + 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") @@ -816,14 +858,17 @@ def path_exists_mock(path): @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")): + 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")): + 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")): + 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") @@ -833,8 +878,7 @@ def open_mock(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('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 @@ -848,27 +892,32 @@ def open_mock(name, *args, **kwargs): 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")): + 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")): + 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")): + 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")): + 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")): + 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")): + 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\n" - b"cpu MHz : 400") + return io.BytesIO(b"cpu MHz : 100\ncpu MHz : 400") else: return orig_open(name, *args, **kwargs) @@ -876,8 +925,9 @@ def open_mock(name, *args, **kwargs): 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): + 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: @@ -907,15 +957,15 @@ def open_mock(name, *args, **kwargs): 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): + 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 @@ -929,7 +979,6 @@ def test_interrupts(self): @unittest.skipIf(not LINUX, "LINUX only") class TestLoadAvg(PsutilTestCase): - @unittest.skipIf(not HAS_GETLOADAVG, "not supported") def test_getloadavg(self): psutil_value = psutil.getloadavg() @@ -948,7 +997,6 @@ def test_getloadavg(self): @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: @@ -958,8 +1006,9 @@ def test_ips(self): 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)) + 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: @@ -990,7 +1039,6 @@ def test_ips(self): @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(): @@ -1000,8 +1048,9 @@ def test_against_ifconfig(self): pass else: self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - self.assertEqual(stats.mtu, - int(re.findall(r'(?i)MTU[: ](\d+)', out)[0])) + 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(): @@ -1041,7 +1090,6 @@ def test_flags(self): @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): @@ -1049,17 +1097,21 @@ def ifconfig(nic): ret = {} out = sh("ifconfig %s" % nic) ret['packets_recv'] = int( - re.findall(r'RX packets[: ](\d+)', out)[0]) + re.findall(r'RX packets[: ](\d+)', out)[0] + ) ret['packets_sent'] = int( - re.findall(r'TX packets[: ](\d+)', out)[0]) + 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]) + re.findall(r'RX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) ret['bytes_sent'] = int( - re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0]) + re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) return ret nio = psutil.net_io_counters(pernic=True, nowrap=False) @@ -1069,26 +1121,33 @@ def ifconfig(nic): except RuntimeError: continue self.assertAlmostEqual( - stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 5) + stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 5 + ) self.assertAlmostEqual( - stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 5) + stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 5 + ) self.assertAlmostEqual( - stats.packets_recv, ifconfig_ret['packets_recv'], delta=1024) + stats.packets_recv, ifconfig_ret['packets_recv'], delta=1024 + ) self.assertAlmostEqual( - stats.packets_sent, ifconfig_ret['packets_sent'], delta=1024) + stats.packets_sent, ifconfig_ret['packets_sent'], delta=1024 + ) self.assertAlmostEqual( - stats.errin, ifconfig_ret['errin'], delta=10) + stats.errin, ifconfig_ret['errin'], delta=10 + ) self.assertAlmostEqual( - stats.errout, ifconfig_ret['errout'], delta=10) + stats.errout, ifconfig_ret['errout'], delta=10 + ) self.assertAlmostEqual( - stats.dropin, ifconfig_ret['dropin'], delta=10) + stats.dropin, ifconfig_ret['dropin'], delta=10 + ) self.assertAlmostEqual( - stats.dropout, ifconfig_ret['dropout'], delta=10) + 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): @@ -1120,7 +1179,6 @@ def test_emulate_unix(self): @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): @@ -1141,10 +1199,12 @@ def df(path): 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) + 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. @@ -1159,11 +1219,13 @@ def test_zfs_fs(self): 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._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: + 'psutil._pslinux.cext.disk_partitions', + return_value=[('/dev/sdb3', '/', 'zfs', 'rw')], + ) as m2: ret = psutil.disk_partitions() assert m1.called assert m2.called @@ -1173,8 +1235,9 @@ def test_zfs_fs(self): 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 mock.patch( + 'os.path.realpath', return_value='/non/existent' + ) as m: with self.assertRaises(FileNotFoundError): psutil.disk_partitions() assert m.called @@ -1184,14 +1247,14 @@ def test_emulate_realpath_fail(self): @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): + 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) @@ -1209,8 +1272,9 @@ def test_emulate_kernel_2_6_full(self): # 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): + 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) @@ -1229,8 +1293,9 @@ def test_emulate_kernel_2_6_limited(self): # (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): + 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) @@ -1252,8 +1317,9 @@ def test_emulate_include_partitions(self): 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): + 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) @@ -1270,8 +1336,9 @@ def test_emulate_exclude_partitions(self): 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): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=False + ): ret = psutil.disk_io_counters(perdisk=False, nowrap=False) self.assertIsNone(ret) @@ -1284,8 +1351,11 @@ def is_storage_device(name): 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): + 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) @@ -1297,8 +1367,9 @@ def exists(path): return True wprocfs = psutil.disk_io_counters(perdisk=True) - with mock.patch('psutil._pslinux.os.path.exists', - create=True, side_effect=exists): + 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)) @@ -1306,14 +1377,14 @@ def test_emulate_not_impl(self): def exists(path): return False - with mock.patch('psutil._pslinux.os.path.exists', - create=True, side_effect=exists): + 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) @@ -1325,8 +1396,9 @@ def test_call_methods(self): 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)): + 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) @@ -1340,8 +1412,9 @@ def test_comparisons(self): 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)): + 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() @@ -1362,8 +1435,9 @@ def test_against_findmnt(self): def test_disk_partitions_mocked(self): with mock.patch( - 'psutil._pslinux.cext.disk_partitions', - return_value=[('/dev/root', '/', 'ext4', 'rw')]) as m: + '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: @@ -1380,7 +1454,6 @@ def test_disk_partitions_mocked(self): @unittest.skipIf(not LINUX, "LINUX only") class TestMisc(PsutilTestCase): - def test_boot_time(self): vmstat_value = vmstat('boot time') psutil_value = psutil.boot_time() @@ -1413,7 +1486,8 @@ def open_mock(name, *args, **kwargs): self.assertRaises(IOError, psutil.cpu_percent, percpu=True) self.assertRaises(IOError, psutil.cpu_times_percent) self.assertRaises( - IOError, psutil.cpu_times_percent, percpu=True) + IOError, psutil.cpu_times_percent, percpu=True + ) psutil.PROCFS_PATH = my_procfs @@ -1436,11 +1510,11 @@ def open_mock(name, *args, **kwargs): 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_percent(percpu=True)), 0) self.assertNotEqual(sum(psutil.cpu_times_percent()), 0) self.assertNotEqual( - sum(map(sum, psutil.cpu_times_percent(percpu=True))), 0) + sum(map(sum, psutil.cpu_times_percent(percpu=True))), 0 + ) finally: shutil.rmtree(my_procfs) reload_module(psutil) @@ -1488,9 +1562,7 @@ def test_cpu_steal_decrease(self): def test_boot_time_mocked(self): with mock.patch('psutil._common.open', create=True) as m: - self.assertRaises( - RuntimeError, - psutil._pslinux.boot_time) + self.assertRaises(RuntimeError, psutil._pslinux.boot_time) assert m.called def test_users(self): @@ -1552,7 +1624,6 @@ def test_pid_exists_no_proc_status(self): @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") @@ -1573,7 +1644,8 @@ def open_mock(name, *args, **kwargs): 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) + psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNLIMITED + ) assert m.called def test_emulate_power_plugged_2(self): @@ -1628,10 +1700,10 @@ 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'), - ): + 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"???") @@ -1647,7 +1719,8 @@ def open_mock(name, *args, **kwargs): 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: + {"/sys/class/power_supply/BAT0/energy_full": b"0"} + ) as m: self.assertEqual(psutil.sensors_battery().percent, 0) assert m.called @@ -1655,32 +1728,35 @@ 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, "")): + "/sys/class/power_supply/BAT0/energy_full", + IOError(errno.ENOENT, ""), + ): with mock_open_exception( - "/sys/class/power_supply/BAT0/charge_full", - IOError(errno.ENOENT, "")): + "/sys/class/power_supply/BAT0/charge_full", + IOError(errno.ENOENT, ""), + ): with mock_open_content( - {"/sys/class/power_supply/BAT0/capacity": b"88"}): + {"/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, "")): + "/sys/class/power_supply/AC/online", IOError(errno.ENOENT, "") + ): with mock_open_exception( - "/sys/class/power_supply/AC0/online", - IOError(errno.ENOENT, "")): + "/sys/class/power_supply/AC0/online", IOError(errno.ENOENT, "") + ): with mock_open_exception( - "/sys/class/power_supply/BAT0/status", - IOError(errno.ENOENT, "")): + "/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"): @@ -1703,7 +1779,6 @@ def open_mock(name, *args, **kwargs): @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'): @@ -1723,8 +1798,9 @@ def open_mock(name, *args, **kwargs): 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']): + 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) @@ -1752,8 +1828,10 @@ def glob_mock(path): 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 [ + '/sys/class/thermal/thermal_zone1/trip_point_0_type', + '/sys/class/thermal/thermal_zone1/trip_point_0_temp', + ] return [] orig_open = open @@ -1769,7 +1847,6 @@ def glob_mock(path): @unittest.skipIf(not LINUX, "LINUX only") class TestSensorsFans(PsutilTestCase): - def test_emulate_data(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): @@ -1784,8 +1861,9 @@ def open_mock(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']): + 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) @@ -1798,19 +1876,18 @@ def open_mock(name, *args, **kwargs): @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) + 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 @@ -1891,15 +1968,19 @@ def test_open_files_file_gone(self): 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: + 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: + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.EINVAL, ""), + ) as m: self.assertEqual(p.open_files(), []) assert m.called @@ -1913,8 +1994,9 @@ def test_open_files_fd_gone(self): # 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: + with mock.patch( + patch_point, side_effect=IOError(errno.ENOENT, "") + ) as m: files = p.open_files() assert not files assert m.called @@ -1929,8 +2011,9 @@ def test_open_files_enametoolong(self): # 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( + patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") + ) as m: with mock.patch("psutil._pslinux.debug"): files = p.open_files() assert not files @@ -1939,8 +2022,9 @@ def test_open_files_enametoolong(self): # --- mocked tests def test_terminal_mocked(self): - with mock.patch('psutil._pslinux._psposix.get_terminal_map', - return_value={}) as m: + with mock.patch( + 'psutil._pslinux._psposix.get_terminal_map', return_value={} + ) as m: self.assertIsNone(psutil._pslinux.Process(os.getpid()).terminal()) assert m.called @@ -1956,13 +2040,15 @@ 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: + 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: + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: self.assertEqual(p.cmdline(), ['foo', 'bar', '']) assert m.called @@ -1970,13 +2056,15 @@ 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: + 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: + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: self.assertEqual(p.cmdline(), ['foo', 'bar', '']) assert m.called @@ -1985,14 +2073,16 @@ def test_cmdline_mixed_separators(self): # 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: + 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)'): + 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") @@ -2026,8 +2116,9 @@ def open_mock_2(name, *args, **kwargs): 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: + with mock.patch( + 'psutil._pslinux.readlink', side_effect=OSError(errno.ENOENT, "") + ) as m: ret = psutil.Process().exe() assert m.called self.assertEqual(ret, "") @@ -2036,8 +2127,8 @@ 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: + '/proc/%s/smaps' % os.getpid(), IOError(errno.ENOENT, "") + ) as m: p = psutil.Process() with self.assertRaises(FileNotFoundError): p.memory_maps() @@ -2048,10 +2139,12 @@ 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: + 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: @@ -2063,48 +2156,48 @@ def test_rlimit_zombie(self): def test_stat_file_parsing(self): args = [ - "0", # pid + "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 + "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}): @@ -2113,7 +2206,8 @@ def test_stat_file_parsing(self): self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) self.assertEqual(p.ppid(), 1) self.assertEqual( - p.create_time(), 6 / CLOCK_TICKS + psutil.boot_time()) + 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) @@ -2150,8 +2244,10 @@ 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: + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.ENAMETOOLONG, ""), + ) as m: p = psutil.Process() with mock.patch("psutil._pslinux.debug"): assert not p.connections() @@ -2173,7 +2269,8 @@ def setUpClass(cls): def read_status_file(self, linestart): with psutil._psplatform.open_text( - '/proc/%s/status' % self.proc.pid) as f: + '/proc/%s/status' % self.proc.pid + ) as f: for line in f: line = line.strip() if line.startswith(linestart): @@ -2190,7 +2287,7 @@ def test_name(self): def test_status(self): value = self.read_status_file("State:") - value = value[value.find('(') + 1:value.rfind(')')] + value = value[value.find('(') + 1 : value.rfind(')')] value = value.replace(' ', '-') self.assertEqual(self.proc.status(), value) @@ -2224,7 +2321,8 @@ def test_cpu_affinity(self): if '-' in str(value): min_, max_ = map(int, value.split('-')) self.assertEqual( - self.proc.cpu_affinity(), list(range(min_, max_ + 1))) + self.proc.cpu_affinity(), list(range(min_, max_ + 1)) + ) def test_cpu_affinity_eligible_cpus(self): value = self.read_status_file("Cpus_allowed_list:") @@ -2243,7 +2341,6 @@ def test_cpu_affinity_eligible_cpus(self): @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") @@ -2252,4 +2349,5 @@ def test_readlink(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index cd1b3f290e..8a5b7f7f93 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -62,6 +62,7 @@ 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): @@ -74,7 +75,9 @@ def wrapper(self, *args, **kwargs): self.__class__.times = before else: return fun(self, *args, **kwargs) + return wrapper + return decorator @@ -219,8 +222,7 @@ def test_cpu_affinity(self): 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])) + self.execute_w_exc(ValueError, lambda: self.proc.cpu_affinity([-1])) @fewtimes_if_linux() def test_open_files(self): @@ -321,7 +323,6 @@ def 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)) @@ -365,8 +366,9 @@ def test_cpu_stats(self): @fewtimes_if_linux() # TODO: remove this once 1892 is fixed - @unittest.skipIf(MACOS and platform.machine() == 'arm64', - "skipped due to #1892") + @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) @@ -399,8 +401,10 @@ def test_disk_usage(self): 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') + @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)) @@ -488,4 +492,5 @@ def test_win_service_get_description(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index b3515de31c..700c054f87 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -57,20 +57,20 @@ class TestSpecialMethods(PsutilTestCase): - def test_check_pid_range(self): with self.assertRaises(OverflowError): - psutil._psplatform.cext.check_pid_range(2 ** 128) + psutil._psplatform.cext.check_pid_range(2**128) with self.assertRaises(psutil.NoSuchProcess): - psutil.Process(2 ** 128) + 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( + "name='%s'" % str(p.name()), r.replace("name=u'", "name='") + ) self.assertIn("status=", r) self.assertNotIn("exitcode=", r) p.terminate() @@ -79,22 +79,31 @@ def test_process__repr__(self, func=repr): self.assertIn("status='terminated'", r) self.assertIn("exitcode=", r) - with mock.patch.object(psutil.Process, "name", - side_effect=psutil.ZombieProcess(os.getpid())): + 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())): + 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())): + 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) @@ -112,68 +121,79 @@ def test_error__str__(self): def test_no_such_process__repr__(self): self.assertEqual( repr(psutil.NoSuchProcess(321)), - "psutil.NoSuchProcess(pid=321, msg='process no longer exists')") + "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')") + "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)") + "process no longer exists (pid=321)", + ) self.assertEqual( str(psutil.NoSuchProcess(321, name="name", msg="msg")), - "msg (pid=321, name='name')") + "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")') + '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')") + "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)") + "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')") + "foo (pid=321, ppid=320, name='name')", + ) def test_access_denied__repr__(self): self.assertEqual( - repr(psutil.AccessDenied(321)), - "psutil.AccessDenied(pid=321)") + 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')") + "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)), "(pid=321)") self.assertEqual( str(psutil.AccessDenied(321, name="name", msg="msg")), - "msg (pid=321, name='name')") + "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')") + "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')") + "msg='timeout after 5 seconds')", + ) def test_timeout_expired__str__(self): self.assertEqual( - str(psutil.TimeoutExpired(5)), - "timeout after 5 seconds") + 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')") + "timeout after 5 seconds (pid=321, name='name')", + ) def test_process__eq__(self): p1 = psutil.Process() @@ -194,12 +214,16 @@ def test_process__hash__(self): class TestMisc(PsutilTestCase): - def test__all__(self): dir_psutil = dir(psutil) for name in dir_psutil: - if name in ('long', 'tests', 'test', 'PermissionError', - 'ProcessLookupError'): + if name in ( + 'long', + 'tests', + 'test', + 'PermissionError', + 'ProcessLookupError', + ): continue if not name.startswith('_'): try: @@ -209,8 +233,10 @@ def test__all__(self): fun = getattr(psutil, name) if fun is None: continue - if (fun.__doc__ is not None and - 'deprecated' not in fun.__doc__.lower()): + 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: @@ -221,8 +247,9 @@ def test__all__(self): self.assertIn(name, dir_psutil) def test_version(self): - self.assertEqual('.'.join([str(x) for x in psutil.version_info]), - psutil.__version__) + 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 @@ -266,16 +293,19 @@ def check(ret): 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: + 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: + 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 mock.patch.object( + psutil.Process, 'create_time', side_effect=ValueError + ) as meth: with self.assertRaises(ValueError): psutil.Process() assert meth.called @@ -283,7 +313,8 @@ def test_ad_on_process_creation(self): 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"): + "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()) @@ -295,7 +326,6 @@ def test_sanity_version_check(self): class TestMemoizeDecorator(PsutilTestCase): - def setUp(self): self.calls = [] @@ -311,14 +341,15 @@ def run_against(self, obj, expected_retval=None): # with args for _ in range(2): ret = obj(1) - self.assertEqual(self.calls, [((), {}), ((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})]) + self.calls, [((), {}), ((1,), {}), ((1,), {'bar': 2})] + ) if expected_retval is not None: self.assertEqual(ret, expected_retval) # clear cache @@ -412,13 +443,13 @@ def foo(*args, **kwargs): # with args for _ in range(2): ret = foo(1) - expected = ((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}) + expected = ((1,), {'bar': 2}) self.assertEqual(ret, expected) self.assertEqual(len(calls), 3) # clear cache @@ -432,10 +463,8 @@ def foo(*args, **kwargs): class TestCommonModule(PsutilTestCase): - def test_memoize_when_activated(self): class Foo: - @memoize_when_activated def foo(self): calls.append(None) @@ -464,15 +493,18 @@ 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"): ""}) + 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"}) + 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"}) @@ -488,21 +520,25 @@ def test_supports_ipv6(self): assert not supports_ipv6() supports_ipv6.cache_clear() - with mock.patch('psutil._common.socket.socket', - side_effect=socket.error) as s: + 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: + 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: + 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 @@ -518,14 +554,17 @@ 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")): + 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")): + 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")): + 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) @@ -580,7 +619,6 @@ def test_cat_bcat(self): class TestWrapNumbers(PsutilTestCase): - def setUp(self): wrap_numbers.cache_clear() @@ -611,36 +649,44 @@ def test_wrap(self): 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)}) + 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)}) + 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)}) + 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)}) + 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)}) + 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)}) + 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)}) + 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)}) + 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() @@ -648,54 +694,54 @@ def test_changing_keys(self): # 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)} + 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)} + 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)}) + 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)} + 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)} + 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)}) + 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)} + 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)} + 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) @@ -718,7 +764,8 @@ def test_cache_call_twice(self): self.assertEqual(cache[0], {'disk_io': input}) self.assertEqual( cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}) + {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}, + ) self.assertEqual(cache[2], {'disk_io': {}}) def test_cache_wrap(self): @@ -733,17 +780,25 @@ def test_cache_wrap(self): self.assertEqual(cache[0], {'disk_io': input}) self.assertEqual( cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100}}) + {'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)])}}) + { + '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)} @@ -766,20 +821,21 @@ def check_cache_info(): self.assertEqual(cache[0], {'disk_io': input}) self.assertEqual( cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190}}) + {'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)} + 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}}) + {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}, + ) self.assertEqual(cache[2], {'disk_io': {}}) def test_cache_clear(self): @@ -818,8 +874,9 @@ def test_cache_clear_public_apis(self): # =================================================================== -@unittest.skipIf(not os.path.exists(SCRIPTS_DIR), - "can't locate scripts directory") +@unittest.skipIf( + not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory" +) class TestScripts(PsutilTestCase): """Tests for scripts in the "scripts" directory.""" @@ -854,8 +911,10 @@ def test_coverage(self): 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)) + 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): @@ -951,4 +1010,5 @@ def test_sensors(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index dc0dd6baaa..8fce8ae08e 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -53,7 +53,6 @@ def vm_stat(field): @unittest.skipIf(not MACOS, "MACOS only") class TestProcess(PsutilTestCase): - @classmethod def setUpClass(cls): cls.pid = spawn_testproc().pid @@ -69,11 +68,11 @@ def test_process_create_time(self): 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))) + hhmmss, time.strftime("%H:%M:%S", time.localtime(start_psutil)) + ) self.assertEqual( - year, - time.strftime("%Y", time.localtime(start_psutil))) + year, time.strftime("%Y", time.localtime(start_psutil)) + ) @unittest.skipIf(not MACOS, "MACOS only") @@ -103,10 +102,12 @@ def df(path): 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) + self.assertAlmostEqual( + usage.free, free, delta=TOLERANCE_DISK_USAGE + ) + self.assertAlmostEqual( + usage.used, used, delta=TOLERANCE_DISK_USAGE + ) # --- cpu @@ -123,11 +124,14 @@ def test_cpu_count_cores(self): def test_cpu_freq(self): freq = psutil.cpu_freq() self.assertEqual( - freq.current * 1000 * 1000, sysctl("sysctl hw.cpufrequency")) + freq.current * 1000 * 1000, sysctl("sysctl hw.cpufrequency") + ) self.assertEqual( - freq.min * 1000 * 1000, sysctl("sysctl hw.cpufrequency_min")) + freq.min * 1000 * 1000, sysctl("sysctl hw.cpufrequency_min") + ) self.assertEqual( - freq.max * 1000 * 1000, sysctl("sysctl hw.cpufrequency_max")) + freq.max * 1000 * 1000, sysctl("sysctl hw.cpufrequency_max") + ) # --- virtual mem @@ -183,8 +187,9 @@ def test_net_if_stats(self): pass else: self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - self.assertEqual(stats.mtu, - int(re.findall(r'mtu (\d+)', out)[0])) + self.assertEqual( + stats.mtu, int(re.findall(r'mtu (\d+)', out)[0]) + ) # --- sensors_battery @@ -201,4 +206,5 @@ def test_sensors_battery(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index eaaf0d1c2a..53852cef8e 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -85,6 +85,7 @@ def ps(fmt, pid=None): 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 @@ -134,8 +135,9 @@ class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc([PYTHON_EXE, "-E", "-O"], - stdin=subprocess.PIPE).pid + cls.pid = spawn_testproc( + [PYTHON_EXE, "-E", "-O"], stdin=subprocess.PIPE + ).pid @classmethod def tearDownClass(cls): @@ -210,10 +212,10 @@ def test_name_long(self): # 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): + 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") @@ -222,10 +224,11 @@ def test_name_long_cmdline_ad_exc(self): # 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, "")): + 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") @@ -233,10 +236,11 @@ 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, "")): + 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) @@ -245,12 +249,14 @@ 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") + 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") + round_time_psutil + ).strftime("%H:%M:%S") self.assertIn(time_ps, [time_psutil_tstamp, round_time_psutil_tstamp]) def test_exe(self): @@ -265,7 +271,7 @@ def test_exe(self): # "/usr/local/bin/python" # We do not want to consider this difference in accuracy # an error. - adjusted_ps_pathname = ps_pathname[:len(ps_pathname)] + 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 @@ -309,8 +315,9 @@ def test_pids(self): # 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] + 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 @@ -326,8 +333,9 @@ def test_nic_names(self): break else: raise self.fail( - "couldn't find %s nic in 'ifconfig -a' output\n%s" % ( - nic, output)) + "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() @@ -375,46 +383,56 @@ def test_users_started(self): if not tstamp: raise unittest.SkipTest( - "cannot interpret tstamp in who output\n%s" % (out)) + "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) + 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: + 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: + 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: + 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) + 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()) + 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 @@ -446,9 +464,11 @@ def df(device): # 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: + if ( + "no such file or directory" in err + or "raw devices not supported" in err + or "permission denied" in err + ): continue raise else: @@ -460,7 +480,6 @@ def df(device): @unittest.skipIf(not POSIX, "POSIX only") class TestMisc(PsutilTestCase): - def test_getpagesize(self): pagesize = getpagesize() self.assertGreater(pagesize, 0) @@ -470,4 +489,5 @@ def test_getpagesize(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index d7d06854c5..5ccd43b3f2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -123,13 +123,15 @@ def test_send_signal(self): def test_send_signal_mocked(self): sig = signal.SIGTERM p = self.spawn_psproc() - with mock.patch('psutil.os.kill', - side_effect=OSError(errno.ESRCH, "")): + 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, "")): + with mock.patch( + 'psutil.os.kill', side_effect=OSError(errno.EPERM, "") + ): self.assertRaises(psutil.AccessDenied, p.send_signal, sig) def test_wait_exited(self): @@ -248,8 +250,8 @@ def test_cpu_percent_numcpus_none(self): 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 + 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 @@ -288,8 +290,10 @@ def test_create_time(self): # 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)) + 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())) @@ -347,7 +351,6 @@ def test_io_counters(self): @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: @@ -386,7 +389,7 @@ def cleanup(init): with self.assertRaisesRegex(ValueError, "ioclass accepts no value"): p.ionice(psutil.IOPRIO_CLASS_IDLE, 1) with self.assertRaisesRegex( - ValueError, "'ioclass' argument must be specified", + ValueError, "'ioclass' argument must be specified" ): p.ionice(value=1) @@ -412,7 +415,7 @@ def test_ionice_win(self): self.assertEqual(p.ionice(), psutil.IOPRIO_HIGH) # errs with self.assertRaisesRegex( - TypeError, "value argument not accepted on Windows", + TypeError, "value argument not accepted on Windows" ): p.ionice(psutil.IOPRIO_NORMAL, value=1) with self.assertRaisesRegex(ValueError, "is not a valid priority"): @@ -421,6 +424,7 @@ def test_ionice_win(self): @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 @@ -468,8 +472,9 @@ def test_rlimit(self): 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) + 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)) @@ -553,14 +558,17 @@ def test_threads_2(self): try: p.threads() except psutil.AccessDenied: - raise unittest.SkipTest( - "on OpenBSD this requires root access") + 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) + 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) + sum([x.system_time for x in p.threads()]), + delta=0.1, + ) @retry_on_failure() def test_memory_info(self): @@ -621,8 +629,9 @@ def test_memory_maps(self): 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 + assert os.path.exists(nt.path) or os.path.islink( + nt.path + ), nt.path except AssertionError: if not LINUX: raise @@ -659,10 +668,11 @@ 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()] + + libpaths = [normpath(x.path) for x in p.memory_maps()] self.assertIn(normpath(path), libpaths) def test_memory_percent(self): @@ -700,8 +710,9 @@ def test_exe(self): # an error. ver = "%s.%s" % (sys.version_info[0], sys.version_info[1]) try: - self.assertEqual(exe.replace(ver, ''), - PYTHON_EXE.replace(ver, '')) + self.assertEqual( + exe.replace(ver, ''), PYTHON_EXE.replace(ver, '') + ) except AssertionError: # Typically MACOS. Really not sure what to do here. pass @@ -723,8 +734,9 @@ def test_cmdline(self): if MACOS and CI_TESTING: pyexe = p.cmdline()[0] if pyexe != PYTHON_EXE: - self.assertEqual(' '.join(p.cmdline()[1:]), - ' '.join(cmdline[1:])) + self.assertEqual( + ' '.join(p.cmdline()[1:]), ' '.join(cmdline[1:]) + ) return self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) @@ -784,14 +796,21 @@ def test_prog_w_funky_name(self): # https://github.com/giampaolo/psutil/issues/628 funky_path = self.get_testfn(suffix='foo bar )') create_exe(funky_path) - cmdline = [funky_path, "-c", - "import time; [time.sleep(0.01) for x in range(3000)];" - "arg1", "arg2", "", "arg3", ""] + cmdline = [ + funky_path, + "-c", + "import time; [time.sleep(0.01) for x in range(3000)];arg1", + "arg2", + "", + "arg3", + "", + ] p = self.spawn_psproc(cmdline) 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)) + self.assertEqual( + os.path.normcase(p.exe()), os.path.normcase(funky_path) + ) @unittest.skipIf(not POSIX, 'POSIX only') def test_uids(self): @@ -835,12 +854,14 @@ def 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]: + 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) @@ -852,9 +873,11 @@ def cleanup(init): # 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 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) @@ -864,14 +887,14 @@ def cleanup(init): try: if hasattr(os, "getpriority"): self.assertEqual( - os.getpriority(os.PRIO_PROCESS, os.getpid()), - p.nice()) + 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()) + os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice() + ) # XXX - going back to previous nice value raises # AccessDenied on MACOS if not MACOS: @@ -906,8 +929,11 @@ def test_cwd(self): self.assertEqual(p.cwd(), os.getcwd()) def test_cwd_2(self): - cmd = [PYTHON_EXE, "-c", - "import os, time; os.chdir('..'); time.sleep(60)"] + 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())") @@ -927,8 +953,9 @@ def test_cpu_affinity(self): 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))) + 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()) @@ -942,8 +969,9 @@ def test_cpu_affinity(self): 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.assertEqual( + p.cpu_affinity(), list(os.sched_getaffinity(p.pid)) + ) # self.assertRaises(TypeError, p.cpu_affinity, 1) p.cpu_affinity(initial) @@ -1011,7 +1039,7 @@ def test_open_files(self): filenames = [os.path.normcase(x.path) for x in p.open_files()] if testfn in filenames: break - time.sleep(.01) + time.sleep(0.01) else: self.assertIn(os.path.normcase(testfn), filenames) for file in filenames: @@ -1028,12 +1056,15 @@ def test_open_files_2(self): 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(): + 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()))) + 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) @@ -1188,27 +1219,36 @@ def test_as_dict(self): self.assertEqual(d['connections'], 'foo') # Test ad_value is set on AccessDenied. - with mock.patch('psutil.Process.nice', create=True, - side_effect=psutil.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}) + 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"]) + 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")): + 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"}) + 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): + 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. @@ -1294,8 +1334,9 @@ def assert_raises_nsp(fun, fun_name): # 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)) + raise self.fail( + "%r didn't raise NSP and returned %r instead" % (fun, ret) + ) p = self.spawn_psproc() p.terminate() @@ -1318,8 +1359,9 @@ 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: + with mock.patch( + "psutil.Process", side_effect=psutil.ZombieProcess(0) + ) as m: assert p.is_running() assert m.called @@ -1328,8 +1370,10 @@ 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: + with mock.patch( + "psutil._psplatform.Process.status", + side_effect=psutil.ZombieProcess(0), + ) as m: self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) assert m.called @@ -1399,10 +1443,13 @@ def clean_dict(d): 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()]) + return dict([ + ( + k.replace("\r", "").replace("\n", ""), + v.replace("\r", "").replace("\n", ""), + ) + for k, v in d.items() + ]) self.maxDiff = None p = psutil.Process() @@ -1437,7 +1484,8 @@ def test_weird_environ(self): path = self.get_testfn() create_exe(path, c_code=code) sproc = self.spawn_testproc( - [path], stdin=subprocess.PIPE, stderr=subprocess.PIPE) + [path], stdin=subprocess.PIPE, stderr=subprocess.PIPE + ) p = psutil.Process(sproc.pid) wait_for_pid(p.pid) assert p.is_running() @@ -1487,6 +1535,7 @@ def test_(self): meth() # noqa except psutil.AccessDenied: pass + setattr(self, attr, types.MethodType(test_, self)) def setUp(self): @@ -1529,8 +1578,12 @@ def test_misc(self): # 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: + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: proc.name() proc.cpu_times() proc.stdin # noqa @@ -1543,10 +1596,13 @@ def test_misc(self): 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: + 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 @@ -1558,21 +1614,31 @@ def test_kill_terminate(self): # 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: + 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) + 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) + 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/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index 274584d652..5483522816 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -17,7 +17,6 @@ @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:] @@ -42,4 +41,5 @@ def test_cpu_count(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 6da77d58f1..152e378fde 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -62,7 +62,6 @@ class TestProcessAPIs(PsutilTestCase): - def test_process_iter(self): self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) sproc = self.spawn_testproc() @@ -74,14 +73,18 @@ def test_process_iter(self): # 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)) + 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())): + 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 mock.patch( + 'psutil.Process', side_effect=psutil.AccessDenied(os.getpid()) + ): with self.assertRaises(psutil.AccessDenied): list(psutil.process_iter()) @@ -90,23 +93,29 @@ def test_prcess_iter_w_attrs(self): 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: + 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: + 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): + 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") + @unittest.skipIf( + PYPY and WINDOWS, "spawn_testproc() unreliable on PYPY + WINDOWS" + ) def test_wait_procs(self): def callback(p): pids.append(p.pid) @@ -130,8 +139,9 @@ def callback(p): @retry_on_failure(30) def test_1(procs, callback): - gone, alive = psutil.wait_procs(procs, timeout=0.03, - callback=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 @@ -149,8 +159,9 @@ def test_1(procs, callback): @retry_on_failure(30) def test_2(procs, callback): - gone, alive = psutil.wait_procs(procs, timeout=0.03, - callback=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 @@ -162,8 +173,9 @@ def test_2(procs, callback): for p in gone: self.assertTrue(hasattr(p, 'returncode')) - @unittest.skipIf(PYPY and WINDOWS, - "spawn_testproc() unreliable on PYPY + WINDOWS") + @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() @@ -191,7 +203,7 @@ def test_pid_exists_2(self): except AssertionError: # in case the process disappeared in meantime fail only # if it is no longer in psutil.pids() - time.sleep(.1) + time.sleep(0.1) self.assertNotIn(pid, psutil.pids()) pids = range(max(pids) + 15000, max(pids) + 16000) for pid in pids: @@ -199,7 +211,6 @@ def test_pid_exists_2(self): class TestMiscAPIs(PsutilTestCase): - def test_boot_time(self): bt = psutil.boot_time() self.assertIsInstance(bt, float) @@ -236,8 +247,17 @@ def test_test(self): sys.stdout = stdout def test_os_constants(self): - names = ["POSIX", "WINDOWS", "LINUX", "MACOS", "FREEBSD", "OPENBSD", - "NETBSD", "BSD", "SUNOS"] + names = [ + "POSIX", + "WINDOWS", + "LINUX", + "MACOS", + "FREEBSD", + "OPENBSD", + "NETBSD", + "BSD", + "SUNOS", + ] for name in names: self.assertIsInstance(getattr(psutil, name), bool, msg=name) @@ -250,14 +270,20 @@ def test_os_constants(self): names.remove("LINUX") elif "bsd" in sys.platform.lower(): assert psutil.BSD - self.assertEqual([psutil.FREEBSD, psutil.OPENBSD, - psutil.NETBSD].count(True), 1) + 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(): + elif ( + "sunos" in sys.platform.lower() + or "solaris" in sys.platform.lower() + ): assert psutil.SUNOS names.remove("SUNOS") elif "darwin" in sys.platform.lower(): @@ -274,7 +300,6 @@ def test_os_constants(self): class TestMemoryAPIs(PsutilTestCase): - def test_virtual_memory(self): mem = psutil.virtual_memory() assert mem.total > 0, mem @@ -290,13 +315,16 @@ def test_virtual_memory(self): 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)) + 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')) + mem._fields, ('total', 'used', 'free', 'percent', 'sin', 'sout') + ) assert mem.total >= 0, mem assert mem.used >= 0, mem @@ -311,7 +339,6 @@ def test_swap_memory(self): class TestCpuAPIs(PsutilTestCase): - def test_cpu_count_logical(self): logical = psutil.cpu_count() self.assertIsNotNone(logical) @@ -338,12 +365,14 @@ def test_cpu_count_cores(self): 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: + 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: + with mock.patch( + 'psutil._psplatform.cpu_count_cores', return_value=val + ) as m: self.assertIsNone(psutil.cpu_count(logical=False)) assert m.called @@ -397,8 +426,10 @@ def test_per_cpu_times(self): 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))) + 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 @@ -447,7 +478,8 @@ def test_cpu_times_comparison(self): self.assertAlmostEqual( getattr(base, field), getattr(summed_values, field), - delta=1) + delta=1, + ) def _test_cpu_percent(self, percent, last_ret, new_ret): try: @@ -456,8 +488,10 @@ def _test_cpu_percent(self, percent, last_ret, new_ret): 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))) + 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) @@ -504,8 +538,10 @@ def test_per_cpu_times_percent(self): 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)] + 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: @@ -516,7 +552,8 @@ def test_cpu_stats(self): infos = psutil.cpu_stats() self.assertEqual( infos._fields, - ('ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls')) + ('ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls'), + ) for name in infos._fields: value = getattr(infos, name) self.assertGreaterEqual(value, 0) @@ -525,8 +562,9 @@ def test_cpu_stats(self): self.assertGreater(value, 0) # TODO: remove this once 1892 is fixed - @unittest.skipIf(MACOS and platform.machine() == 'arm64', - "skipped due to #1892") + @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): @@ -559,7 +597,6 @@ def test_getloadavg(self): 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()) @@ -576,12 +613,14 @@ def test_disk_usage(self): 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.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) + self.assertAlmostEqual( + usage.used, shutil_usage.used, delta=tolerance + ) # if path does not exist OSError ENOENT is expected across # all platforms @@ -655,14 +694,20 @@ def find_mount_point(path): return path.lower() mount = find_mount_point(__file__) - mounts = [x.mountpoint.lower() for x in - psutil.disk_partitions(all=True) if x.mountpoint] + 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 + @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) @@ -694,15 +739,15 @@ def check_ntuple(nt): 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: + 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): @@ -736,8 +781,9 @@ def check_ntuple(nt): 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: + 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 @@ -774,14 +820,23 @@ def test_net_if_addrs(self): 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] + 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): + 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 @@ -810,8 +865,9 @@ def test_net_if_addrs_mac_null_bytes(self): 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: + 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: @@ -822,9 +878,11 @@ def test_net_if_addrs_mac_null_bytes(self): 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) + 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 @@ -835,19 +893,21 @@ def test_net_if_stats(self): self.assertGreaterEqual(mtu, 0) self.assertIsInstance(flags, str) - @unittest.skipIf(not (LINUX or BSD or MACOS), - "LINUX or BSD or MACOS specific") + @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: + 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() @@ -865,10 +925,10 @@ def test_sensors_temperatures(self): @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] + 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) @@ -880,8 +940,10 @@ 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): + 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: @@ -901,4 +963,5 @@ def test_sensors_fans(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index bff43b1c9b..825311064f 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -60,7 +60,6 @@ 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. @@ -112,7 +111,6 @@ def foo(): @mock.patch('time.sleep') def test_retries_arg(self, sleep): - @retry(retries=5, interval=1, logfun=None) def foo(): 1 / 0 # noqa @@ -126,7 +124,6 @@ def test_retries_and_timeout_args(self, sleep): class TestSyncTestUtils(PsutilTestCase): - def test_wait_for_pid(self): wait_for_pid(os.getpid()) nopid = max(psutil.pids()) + 99999 @@ -165,7 +162,6 @@ def test_call_until(self): class TestFSTestUtils(PsutilTestCase): - def test_open_text(self): with open_text(__file__) as f: self.assertEqual(f.mode, 'r') @@ -194,8 +190,9 @@ def test_safe_rmpath(self): 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 mock.patch( + 'psutil.tests.os.stat', side_effect=OSError(errno.EINVAL, "") + ) as m: with self.assertRaises(OSError): safe_rmpath(testfn) assert m.called @@ -210,7 +207,6 @@ def test_chdir(self): class TestProcessUtils(PsutilTestCase): - def test_reap_children(self): subp = self.spawn_testproc() p = psutil.Process(subp.pid) @@ -259,8 +255,12 @@ def test_terminate(self): 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) + p = psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) terminate(p) self.assertPidGone(p.pid) terminate(p) @@ -279,7 +279,6 @@ def test_terminate(self): class TestNetUtils(PsutilTestCase): - def bind_socket(self): port = get_free_port() with contextlib.closing(bind_socket(addr=('', port))) as s: @@ -313,8 +312,9 @@ def tcp_tcp_socketpair(self): self.assertNotEqual(client.getsockname(), addr) @unittest.skipIf(not POSIX, "POSIX only") - @unittest.skipIf(NETBSD or FREEBSD, - "/var/run/log UNIX socket opened by default") + @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() @@ -351,11 +351,11 @@ def test_create_sockets(self): @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) @@ -378,8 +378,9 @@ def fun(ls=ls): try: # will consume around 3M in total - self.assertRaisesRegex(AssertionError, "extra-mem", - self.execute, fun, times=50) + self.assertRaisesRegex( + AssertionError, "extra-mem", self.execute, fun, times=50 + ) finally: del ls @@ -391,33 +392,37 @@ def fun(): box = [] kind = "fd" if POSIX else "handle" - self.assertRaisesRegex(AssertionError, "unclosed " + kind, - self.execute, fun) + 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.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) @@ -432,7 +437,6 @@ def test_system_namespace(self): 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()) @@ -440,4 +444,5 @@ def test_is_namedtuple(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index cfc451219c..3ab71b03a8 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -111,6 +111,7 @@ 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: @@ -123,6 +124,7 @@ def safe_rmpath(path): # NOQA # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/ # windows/process_info.c#L146 from psutil.tests import safe_rmpath as rm + try: return rm(path) except WindowsError: @@ -209,8 +211,9 @@ def test_proc_exe(self): 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)) + self.assertEqual( + os.path.normcase(exe), os.path.normcase(self.funky_name) + ) def test_proc_name(self): cmd = [self.funky_name, "-c", "time.sleep(10)"] @@ -229,7 +232,8 @@ def test_proc_cmdline(self): self.assertIsInstance(part, str) if self.expect_exact_path_match(): self.assertEqual( - cmdline, [self.funky_name, "-c", "time.sleep(10)"]) + cmdline, [self.funky_name, "-c", "time.sleep(10)"] + ) def test_proc_cwd(self): dname = self.funky_name + "2" @@ -254,8 +258,9 @@ def test_proc_open_files(self): # 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)) + self.assertEqual( + os.path.normcase(path), os.path.normcase(self.funky_name) + ) @unittest.skipIf(not POSIX, "POSIX only") def test_proc_connections(self): @@ -309,10 +314,13 @@ 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()] + + 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) @@ -362,4 +370,5 @@ def test_proc_environ(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index dfc8444bdd..5983af70a1 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -72,9 +72,10 @@ def powershell(cmd): """ 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 + cmdline = ( + 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive ' + + '-NoProfile -WindowStyle Hidden -Command "%s"' % cmd + ) return sh(cmdline) @@ -101,9 +102,10 @@ def wmic(path, what, converter=int): class TestCpuAPIs(WindowsTestCase): - - @unittest.skipIf('NUMBER_OF_PROCESSORS' not in os.environ, - 'NUMBER_OF_PROCESSORS env var is not available') + @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 @@ -119,8 +121,9 @@ def test_cpu_count_vs_GetSystemInfo(self): def test_cpu_count_logical_vs_wmi(self): w = wmi.WMI() - procs = sum(proc.NumberOfLogicalProcessors - for proc in w.Win32_Processor()) + procs = sum( + proc.NumberOfLogicalProcessors for proc in w.Win32_Processor() + ) self.assertEqual(psutil.cpu_count(), procs) def test_cpu_count_cores_vs_wmi(self): @@ -129,8 +132,9 @@ def test_cpu_count_cores_vs_wmi(self): 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))) + self.assertEqual( + psutil.cpu_count(), len(psutil.cpu_times(percpu=True)) + ) def test_cpu_freq(self): w = wmi.WMI() @@ -140,7 +144,6 @@ def test_cpu_freq(self): class TestSystemAPIs(WindowsTestCase): - def test_nic_names(self): out = sh('ipconfig /all') nics = psutil.net_io_counters(pernic=True).keys() @@ -149,38 +152,44 @@ def test_nic_names(self): continue if nic not in out: raise self.fail( - "%r nic wasn't found in 'ipconfig /all' output" % nic) + "%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) + 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) + 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( + 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] + 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.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") @@ -229,8 +238,9 @@ def test_disks(self): 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)) + raise self.fail( + "psutil=%s, wmi=%s" % (usage.free, wmi_free) + ) break else: raise self.fail("can't find partition %s" % repr(ps_part)) @@ -242,19 +252,27 @@ def test_disk_usage(self): 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) + 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:')] + 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): @@ -264,14 +282,17 @@ def test_net_if_stats(self): 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)) + 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") + 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) @@ -294,7 +315,6 @@ def test_boot_time_fluctuation(self): class TestSensorsBattery(WindowsTestCase): - def test_has_battery(self): if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: self.assertIsNotNone(psutil.sensors_battery()) @@ -307,8 +327,10 @@ def test_percent(self): battery_wmi = w.query('select * from Win32_Battery')[0] battery_psutil = psutil.sensors_battery() self.assertAlmostEqual( - battery_psutil.percent, battery_wmi.EstimatedChargeRemaining, - delta=1) + battery_psutil.percent, + battery_wmi.EstimatedChargeRemaining, + delta=1, + ) @unittest.skipIf(not HAS_BATTERY, "no battery") def test_power_plugged(self): @@ -317,34 +339,44 @@ def test_power_plugged(self): 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) + 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: + 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) + 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) + 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) + 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 @@ -354,7 +386,6 @@ def test_emulate_secs_left_unknown(self): class TestProcess(WindowsTestCase): - @classmethod def setUpClass(cls): cls.pid = spawn_testproc().pid @@ -391,8 +422,9 @@ def test_send_signal(self): 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()) + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) after = p.num_handles() self.assertEqual(after, before + 1) win32api.CloseHandle(handle) @@ -404,10 +436,12 @@ def test_ctrl_signals(self): 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) + 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) @@ -445,43 +479,50 @@ def test_cmdline(self): # delta=0.2) def test_nice(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, os.getpid()) + 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) + 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) + sys_value['PeakWorkingSetSize'], psutil_value.peak_wset + ) + self.assertEqual(sys_value['WorkingSetSize'], psutil_value.wset) self.assertEqual( - sys_value['WorkingSetSize'], psutil_value.wset) + sys_value['QuotaPeakPagedPoolUsage'], psutil_value.peak_paged_pool + ) self.assertEqual( - sys_value['QuotaPeakPagedPoolUsage'], psutil_value.peak_paged_pool) - self.assertEqual( - sys_value['QuotaPagedPoolUsage'], psutil_value.paged_pool) + 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) + psutil_value.peak_nonpaged_pool, + ) self.assertEqual( - sys_value['PagefileUsage'], psutil_value.pagefile) + sys_value['QuotaNonPagedPoolUsage'], psutil_value.nonpaged_pool + ) + self.assertEqual(sys_value['PagefileUsage'], psutil_value.pagefile) self.assertEqual( - sys_value['PeakPagefileUsage'], psutil_value.peak_pagefile) + 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) + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) self.addCleanup(win32api.CloseHandle, handle) p = psutil.Process(self.pid) p.terminate() @@ -493,44 +534,56 @@ 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) + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) self.addCleanup(win32api.CloseHandle, handle) sys_value = from_bitmask( - win32process.GetProcessAffinityMask(handle)[0]) + 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()) + 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']) + psutil_value.read_count, sys_value['ReadOperationCount'] + ) self.assertEqual( - psutil_value.write_count, sys_value['WriteOperationCount']) + psutil_value.write_count, sys_value['WriteOperationCount'] + ) self.assertEqual( - psutil_value.read_bytes, sys_value['ReadTransferCount']) + psutil_value.read_bytes, sys_value['ReadTransferCount'] + ) self.assertEqual( - psutil_value.write_bytes, sys_value['WriteTransferCount']) + psutil_value.write_bytes, sys_value['WriteTransferCount'] + ) self.assertEqual( - psutil_value.other_count, sys_value['OtherOperationCount']) + psutil_value.other_count, sys_value['OtherOperationCount'] + ) self.assertEqual( - psutil_value.other_bytes, sys_value['OtherTransferCount']) + 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) + 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)) + handle, ctypes.byref(hndcnt) + ) sys_value = hndcnt.value psutil_value = psutil.Process(self.pid).num_handles() self.assertEqual(psutil_value, sys_value) @@ -581,8 +634,7 @@ def test_exe(self): 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('"', '')) + self.assertEqual(' '.join(p.cmdline()), w.CommandLine.replace('"', '')) def test_username(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] @@ -615,8 +667,9 @@ 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())) + psutil_create = time.strftime( + "%Y%m%d%H%M%S", time.localtime(p.create_time()) + ) self.assertEqual(wmic_create, psutil_create) @@ -644,8 +697,10 @@ def tearDownClass(cls): 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: + 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)): @@ -656,38 +711,50 @@ def test_memory_info(self): 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: + 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: + 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) + cpu_times_1.user, cpu_times_2.user, delta=0.01 + ) self.assertAlmostEqual( - cpu_times_1.system, cpu_times_2.system, delta=0.01) + 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: + 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) + 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) + 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): @@ -697,8 +764,9 @@ def test_cmdline(self): b = cext.proc_cmdline(pid, use_peb=False) except OSError as err: err = convert_oserror(err) - if not isinstance(err, (psutil.AccessDenied, - psutil.NoSuchProcess)): + if not isinstance( + err, (psutil.AccessDenied, psutil.NoSuchProcess) + ): raise else: self.assertEqual(a, b) @@ -720,9 +788,11 @@ def find_other_interpreter(): # 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) + proc = subprocess.Popen( + args=[filename, "-c", code], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) output, _ = proc.communicate() proc.wait() if output == str(not IS_64BIT): @@ -736,7 +806,8 @@ def setUp(self): other_python = self.find_other_interpreter() if other_python is None: raise unittest.SkipTest( - "could not find interpreter with opposite bitness") + "could not find interpreter with opposite bitness" + ) if IS_64BIT: self.python64 = sys.executable self.python32 = other_python @@ -747,13 +818,11 @@ def setUp(self): 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.python32] + self.test_args, env=env, stdin=subprocess.PIPE + ) self.proc64 = self.spawn_testproc( - [self.python64] + self.test_args, - env=env, - stdin=subprocess.PIPE) + [self.python64] + self.test_args, env=env, stdin=subprocess.PIPE + ) def tearDown(self): super().tearDown() @@ -799,7 +868,6 @@ def test_environ_64(self): @unittest.skipIf(not WINDOWS, "WINDOWS only") class TestServices(PsutilTestCase): - def test_win_service_iter(self): valid_statuses = set([ "running", @@ -810,11 +878,7 @@ def test_win_service_iter(self): "stop", "stopped", ]) - valid_start_types = set([ - "automatic", - "manual", - "disabled", - ]) + valid_start_types = set(["automatic", "manual", "disabled"]) valid_statuses = set([ "running", "paused", @@ -849,8 +913,9 @@ def test_win_service_iter(self): self.assertEqual(serv, s) def test_win_service_get(self): - ERROR_SERVICE_DOES_NOT_EXIST = \ + 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() @@ -865,11 +930,13 @@ def test_win_service_get(self): else: args = (ERROR_SERVICE_DOES_NOT_EXIST, "msg") exc = WindowsError(*args) - with mock.patch("psutil._psplatform.cext.winservice_query_status", - side_effect=exc): + 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): + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): self.assertRaises(psutil.NoSuchProcess, service.username) # test AccessDenied @@ -878,11 +945,13 @@ def test_win_service_get(self): else: args = (ERROR_ACCESS_DENIED, "msg") exc = WindowsError(*args) - with mock.patch("psutil._psplatform.cext.winservice_query_status", - side_effect=exc): + 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): + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): self.assertRaises(psutil.AccessDenied, service.username) # test __str__ and __repr__ @@ -894,4 +963,5 @@ def test_win_service_get(self): if __name__ == '__main__': from psutil.tests.runner import run_from_name + run_from_name(__file__) diff --git a/pyproject.toml b/pyproject.toml index 6e5e9563aa..677722dbe0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,9 @@ +[tool.black] +line-length = 79 +skip-string-normalization = true +preview = true +target-version = ["py37"] + [tool.ruff] # https://beta.ruff.rs/docs/settings/ target-version = "py37" @@ -25,6 +31,7 @@ ignore = [ "B904", # Within an `except` clause, raise exceptions with `raise ... from err` (PYTHON2.7 COMPAT) "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) "C90", # mccabe (function `X` is too complex) + "COM812", # Trailing comma missing "D", # pydocstyle "DTZ", # flake8-datetimez "ERA001", # Found commented-out code diff --git a/scripts/battery.py b/scripts/battery.py index 040f948195..0595d1ad10 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -35,8 +35,10 @@ def main(): print("charge: %s%%" % round(batt.percent, 2)) if batt.power_plugged: - print("status: %s" % ( - "charging" if batt.percent < 100 else "fully charged")) + print( + "status: %s" + % ("charging" if batt.percent < 100 else "fully charged") + ) print("plugged in: yes") else: print("left: %s" % secs2hours(batt.secsleft)) diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index d801c6dddb..934b24a1c5 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -23,8 +23,7 @@ def main(): templ = "%-17s %8s %8s %8s %5s%% %9s %s" - print(templ % ("Device", "Total", "Used", "Free", "Use ", "Type", - "Mount")) + print(templ % ("Device", "Total", "Used", "Free", "Use ", "Type", "Mount")) for part in psutil.disk_partitions(all=False): if os.name == 'nt': if 'cdrom' in part.opts or part.fstype == '': @@ -33,14 +32,16 @@ def main(): # partition or just hang. continue usage = psutil.disk_usage(part.mountpoint) - print(templ % ( + line = templ % ( part.device, bytes2human(usage.total), bytes2human(usage.used), bytes2human(usage.free), int(usage.percent), part.fstype, - part.mountpoint)) + part.mountpoint, + ) + print(line) if __name__ == '__main__': diff --git a/scripts/free.py b/scripts/free.py index f72149ac30..332507284c 100755 --- a/scripts/free.py +++ b/scripts/free.py @@ -20,21 +20,26 @@ def main(): swap = psutil.swap_memory() templ = "%-7s %10s %10s %10s %10s %10s %10s" print(templ % ('', 'total', 'used', 'free', 'shared', 'buffers', 'cache')) - print(templ % ( + sect = templ % ( 'Mem:', int(virt.total / 1024), int(virt.used / 1024), int(virt.free / 1024), int(getattr(virt, 'shared', 0) / 1024), int(getattr(virt, 'buffers', 0) / 1024), - int(getattr(virt, 'cached', 0) / 1024))) - print(templ % ( - 'Swap:', int(swap.total / 1024), + int(getattr(virt, 'cached', 0) / 1024), + ) + print(sect) + sect = templ % ( + 'Swap:', + int(swap.total / 1024), int(swap.used / 1024), int(swap.free / 1024), '', '', - '')) + '', + ) + print(sect) if __name__ == '__main__': diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index 7fdfa1e124..4b4246aaa5 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -71,19 +71,37 @@ def main(): if nic in stats: st = stats[nic] print(" stats : ", end='') - print("speed=%sMB, duplex=%s, mtu=%s, up=%s" % ( - st.speed, duplex_map[st.duplex], st.mtu, - "yes" if st.isup else "no")) + print( + "speed=%sMB, duplex=%s, mtu=%s, up=%s" + % ( + st.speed, + duplex_map[st.duplex], + st.mtu, + "yes" if st.isup else "no", + ) + ) if nic in io_counters: io = io_counters[nic] print(" incoming : ", end='') - print("bytes=%s, pkts=%s, errs=%s, drops=%s" % ( - bytes2human(io.bytes_recv), io.packets_recv, io.errin, - io.dropin)) + print( + "bytes=%s, pkts=%s, errs=%s, drops=%s" + % ( + bytes2human(io.bytes_recv), + io.packets_recv, + io.errin, + io.dropin, + ) + ) print(" outgoing : ", end='') - print("bytes=%s, pkts=%s, errs=%s, drops=%s" % ( - bytes2human(io.bytes_sent), io.packets_sent, io.errout, - io.dropout)) + print( + "bytes=%s, pkts=%s, errs=%s, drops=%s" + % ( + bytes2human(io.bytes_sent), + io.packets_sent, + io.errout, + io.dropout, + ) + ) for addr in addrs: print(" %-4s" % af_map.get(addr.family, addr.family), end="") print(" address : %s" % addr.address) diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 74f8150ae2..b58224bb6e 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -127,19 +127,23 @@ def call_oneshot(funs): def main(): - print("%s methods involved on platform %r (%s iterations, psutil %s):" % ( - len(names), sys.platform, ITERATIONS, psutil.__version__)) + print( + "%s methods involved on platform %r (%s iterations, psutil %s):" + % (len(names), sys.platform, ITERATIONS, psutil.__version__) + ) for name in sorted(names): print(" " + name) # "normal" run elapsed1 = timeit.timeit( - "call_normal(funs)", setup=setup, number=ITERATIONS) + "call_normal(funs)", setup=setup, number=ITERATIONS + ) print("normal: %.3f secs" % elapsed1) # "one shot" run elapsed2 = timeit.timeit( - "call_oneshot(funs)", setup=setup, number=ITERATIONS) + "call_oneshot(funs)", setup=setup, number=ITERATIONS + ) print("onshot: %.3f secs" % elapsed2) # done diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index 2a63dca254..fe5151d3ed 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -40,8 +40,10 @@ def main(): args = runner.parse_args() if not args.worker: - print("%s methods involved on platform %r (psutil %s):" % ( - len(names), sys.platform, psutil.__version__)) + print( + "%s methods involved on platform %r (psutil %s):" + % (len(names), sys.platform, psutil.__version__) + ) for name in sorted(names): print(" " + name) diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index af53b878cb..90cb258c33 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -54,7 +54,8 @@ HERE = os.path.abspath(os.path.dirname(__file__)) REGEX = re.compile( r'(?:http|ftp|https)?://' - r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+') + r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' +) REQUEST_TIMEOUT = 15 # There are some status codes sent by websites on HEAD request. # Like 503 by Microsoft, and 401 by Apple @@ -64,6 +65,7 @@ def memoize(fun): """A memoize decorator.""" + @functools.wraps(fun) def wrapper(*args, **kwargs): key = (args, frozenset(sorted(kwargs.items()))) @@ -102,8 +104,10 @@ def parse_rst(fname): # HISTORY file has a lot of dead links. if fname == 'HISTORY.rst' and urls: urls = [ - x for x in urls if - not x.startswith('https://github.com/giampaolo/psutil/issues')] + x + for x in urls + if not x.startswith('https://github.com/giampaolo/psutil/issues') + ] return urls @@ -204,8 +208,9 @@ def parallel_validator(urls): current = 0 total = len(urls) with concurrent.futures.ThreadPoolExecutor() as executor: - fut_to_url = {executor.submit(validate_url, url[1]): url - for url in urls} + fut_to_url = { + executor.submit(validate_url, url[1]): url for url in urls + } for fut in concurrent.futures.as_completed(fut_to_url): current += 1 sys.stdout.write("\r%s / %s" % (current, total)) @@ -228,7 +233,8 @@ def parallel_validator(urls): def main(): parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawTextHelpFormatter) + description=__doc__, formatter_class=argparse.RawTextHelpFormatter + ) parser.add_argument('files', nargs="+") parser.parse_args() args = parser.parse_args() diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py index 71c77e4027..516ca894ef 100755 --- a/scripts/internal/clinter.py +++ b/scripts/internal/clinter.py @@ -36,9 +36,11 @@ def check_line(path, line, idx, lines): if not eof: nextline = lines[idx + 1] # "#" is a pre-processor line - if nextline != '\n' and \ - nextline.strip()[0] != '#' and \ - nextline.strip()[:2] != '*/': + if ( + nextline != '\n' + and nextline.strip()[0] != '#' + and nextline.strip()[:2] != '*/' + ): warn(path, line, lineno, "expected 1 blank line") sls = s.lstrip() diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index 47a33d9964..e8b0c54e0c 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -40,7 +40,7 @@ def download_file(url): tot_bytes = 0 with open(local_fname, 'wb') as f: for chunk in r.iter_content(chunk_size=16384): - if chunk: # filter out keep-alive new chunks + if chunk: # filter out keep-alive new chunks f.write(chunk) tot_bytes += len(chunk) return local_fname @@ -49,8 +49,8 @@ def download_file(url): def get_file_urls(): with requests.Session() as session: data = session.get( - BASE_URL + '/projects/' + USER + '/' + PROJECT, - timeout=TIMEOUT) + BASE_URL + '/projects/' + USER + '/' + PROJECT, timeout=TIMEOUT + ) data = data.json() urls = [] @@ -96,8 +96,10 @@ def run(): raise else: completed += 1 - print("downloaded %-45s %s" % ( - local_fname, bytes2human(os.path.getsize(local_fname)))) + print( + "downloaded %-45s %s" + % (local_fname, bytes2human(os.path.getsize(local_fname))) + ) # 2 wheels (32 and 64 bit) per supported python version expected = len(PY_VERSIONS) * 2 if expected != completed: diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels_github.py index de6c34faa2..c6ecb5dda0 100755 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels_github.py @@ -38,8 +38,9 @@ def get_artifacts(): base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) url = base_url + "/actions/artifacts" - res = requests.get(url=url, headers={ - "Authorization": "token %s" % TOKEN}, timeout=TIMEOUT) + res = requests.get( + url=url, headers={"Authorization": "token %s" % TOKEN}, timeout=TIMEOUT + ) res.raise_for_status() data = json.loads(res.content) return data @@ -47,8 +48,9 @@ def get_artifacts(): def download_zip(url): print("downloading: " + url) - res = requests.get(url=url, headers={ - "Authorization": "token %s" % TOKEN}, timeout=TIMEOUT) + res = requests.get( + url=url, headers={"Authorization": "token %s" % TOKEN}, timeout=TIMEOUT + ) res.raise_for_status() totbytes = 0 with open(OUTFILE, 'wb') as f: diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 290e8b49fd..9eeeb1a24b 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -12,21 +12,24 @@ SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') -SKIP_FILES = ('appveyor.yml') +SKIP_FILES = 'appveyor.yml' SKIP_PREFIXES = ('.ci/', '.github/') def sh(cmd): return subprocess.check_output( - shlex.split(cmd), universal_newlines=True).strip() + shlex.split(cmd), universal_newlines=True + ).strip() def main(): files = set() for file in sh("git ls-files").split('\n'): - if file.startswith(SKIP_PREFIXES) or \ - os.path.splitext(file)[1].lower() in SKIP_EXTS or \ - file in SKIP_FILES: + if ( + file.startswith(SKIP_PREFIXES) + or os.path.splitext(file)[1].lower() in SKIP_EXTS + or file in SKIP_FILES + ): continue files.add(file) diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 79582feeb1..84c38cccd0 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -26,6 +26,7 @@ def term_supports_colors(): try: import curses + assert sys.stderr.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 @@ -41,9 +42,9 @@ def hilite(s, ok=True, bold=False): attr = [] if ok is None: # no color pass - elif ok: # green + elif ok: # green attr.append('32') - else: # red + else: # red attr.append('31') if bold: attr.append('1') @@ -59,7 +60,9 @@ def sh(cmd): if isinstance(cmd, str): cmd = shlex.split(cmd) p = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True, ) stdout, stderr = p.communicate() @@ -79,30 +82,45 @@ def open_text(path): def git_commit_files(): out = sh(["git", "diff", "--cached", "--name-only"]) - py_files = [x for x in out.split('\n') if x.endswith('.py') and - os.path.exists(x)] - c_files = [x for x in out.split('\n') if x.endswith(('.c', '.h')) and - os.path.exists(x)] - rst_files = [x for x in out.split('\n') if x.endswith('.rst') and - os.path.exists(x)] + py_files = [ + x for x in out.split('\n') if x.endswith('.py') and os.path.exists(x) + ] + c_files = [ + x + for x in out.split('\n') + if x.endswith(('.c', '.h')) and os.path.exists(x) + ] + rst_files = [ + x for x in out.split('\n') if x.endswith('.rst') and os.path.exists(x) + ] toml_files = [ x for x in out.split("\n") if x.endswith(".toml") and os.path.exists(x) ] new_rm_mv = sh( - ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"], + ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] ) # XXX: we should escape spaces and possibly other amenities here new_rm_mv = new_rm_mv.split() return (py_files, c_files, rst_files, toml_files, new_rm_mv) +def black(files): + print("running black (%s)" % len(files)) + cmd = [PYTHON, "-m", "black", "--check", "--safe"] + files + if subprocess.call(cmd) != 0: + return exit( + "Python code didn't pass 'ruff' style check." + "Try running 'make fix-ruff'." + ) + + def ruff(files): print("running ruff (%s)" % len(files)) cmd = [PYTHON, "-m", "ruff", "check", "--no-cache"] + files if subprocess.call(cmd) != 0: return exit( "Python code didn't pass 'ruff' style check." - "Try running 'make fix-ruff'.", + "Try running 'make fix-ruff'." ) @@ -131,6 +149,7 @@ def rstcheck(files): def main(): py_files, c_files, rst_files, toml_files, new_rm_mv = git_commit_files() if py_files: + black(py_files) ruff(py_files) if c_files: c_linter(c_files) @@ -142,8 +161,10 @@ def main(): out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) with open_text('MANIFEST.in') as f: if out.strip() != f.read().strip(): - sys.exit("some files were added, deleted or renamed; " - "run 'make generate-manifest' and commit again") + sys.exit( + "some files were added, deleted or renamed; " + "run 'make generate-manifest' and commit again" + ) main() diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 7759ca7b2c..f7017b04db 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -84,9 +84,10 @@ def main(): print_color(s, "red" if ads else None) tot_perc = round((tot_ads / tot_calls) * 100, 1) print("-" * 50) - print("Totals: access-denied=%s (%s%%), calls=%s, processes=%s, " - "elapsed=%ss" % (tot_ads, tot_perc, tot_calls, tot_procs, - round(elapsed, 2))) + print( + "Totals: access-denied=%s (%s%%), calls=%s, processes=%s, elapsed=%ss" + % (tot_ads, tot_perc, tot_calls, tot_procs, round(elapsed, 2)) + ) if __name__ == '__main__': diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 2297c09506..9f89f635a9 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -20,15 +20,17 @@ ROOT = os.path.realpath(os.path.join(HERE, '..', '..')) HISTORY = os.path.join(ROOT, 'HISTORY.rst') PRINT_HASHES_SCRIPT = os.path.join( - ROOT, 'scripts', 'internal', 'print_hashes.py') + ROOT, 'scripts', 'internal', 'print_hashes.py' +) PRJ_NAME = 'psutil' PRJ_VERSION = __version__ PRJ_URL_HOME = 'https://github.com/giampaolo/psutil' PRJ_URL_DOC = 'http://psutil.readthedocs.io' PRJ_URL_DOWNLOAD = 'https://pypi.org/project/psutil/#files' -PRJ_URL_WHATSNEW = \ +PRJ_URL_WHATSNEW = ( 'https://github.com/giampaolo/psutil/blob/master/HISTORY.rst' +) template = """\ Hello all, @@ -109,9 +111,12 @@ def get_changes(): def main(): changes = get_changes() - hashes = subprocess.check_output( - [sys.executable, PRINT_HASHES_SCRIPT, 'dist/']).strip().decode() - print(template.format( + hashes = ( + subprocess.check_output([sys.executable, PRINT_HASHES_SCRIPT, 'dist/']) + .strip() + .decode() + ) + text = template.format( prj_name=PRJ_NAME, prj_version=PRJ_VERSION, prj_urlhome=PRJ_URL_HOME, @@ -120,7 +125,8 @@ def main(): prj_urlwhatsnew=PRJ_URL_WHATSNEW, changes=changes, hashes=hashes, - )) + ) + print(text) if __name__ == '__main__': diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 8abaed0c48..adbaa89a0b 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -136,7 +136,8 @@ def main(): global TIMES parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawTextHelpFormatter) + description=__doc__, formatter_class=argparse.RawTextHelpFormatter + ) parser.add_argument('-t', '--times', type=int, default=TIMES) args = parser.parse_args() TIMES = args.times @@ -152,8 +153,12 @@ def main(): # --- system public_apis = [] - ignore = ['wait_procs', 'process_iter', 'win_service_get', - 'win_service_iter'] + ignore = [ + 'wait_procs', + 'process_iter', + 'win_service_get', + 'win_service_iter', + ] if psutil.MACOS: ignore.append('net_connections') # raises AD for name in psutil.__all__: @@ -167,9 +172,9 @@ def main(): fun = getattr(psutil, name) args = () if name == 'pid_exists': - args = (os.getpid(), ) + args = (os.getpid(),) elif name == 'disk_usage': - args = (os.getcwd(), ) + args = (os.getcwd(),) timecall(name, fun, *args) timecall('cpu_count (cores)', psutil.cpu_count, logical=False) timecall('process_iter (all)', lambda: list(psutil.process_iter())) @@ -178,9 +183,22 @@ def main(): # --- process print("") print_header("PROCESS APIS") - ignore = ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', - 'as_dict', 'parent', 'parents', 'memory_info_ex', 'oneshot', - 'pid', 'rlimit', 'children'] + ignore = [ + 'send_signal', + 'suspend', + 'resume', + 'terminate', + 'kill', + 'wait', + 'as_dict', + 'parent', + 'parents', + 'memory_info_ex', + 'oneshot', + 'pid', + 'rlimit', + 'children', + ] if psutil.MACOS: ignore.append('memory_maps') # XXX p = psutil.Process() diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 740951ac05..5b6531608b 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -15,15 +15,18 @@ class Wheel: - def __init__(self, path): self._path = path self._name = os.path.basename(path) def __repr__(self): return "<%s(name=%s, plat=%s, arch=%s, pyver=%s)>" % ( - self.__class__.__name__, self.name, self.platform(), self.arch(), - self.pyver()) + self.__class__.__name__, + self.name, + self.platform(), + self.arch(), + self.pyver(), + ) __str__ = __repr__ @@ -72,7 +75,6 @@ def size(self): class Tarball(Wheel): - def platform(self): return "source" @@ -85,8 +87,12 @@ def pyver(self): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('dir', nargs="?", default="dist", - help='directory containing tar.gz or wheel files') + parser.add_argument( + 'dir', + nargs="?", + default="dist", + help='directory containing tar.gz or wheel files', + ) args = parser.parse_args() groups = collections.defaultdict(list) @@ -111,15 +117,21 @@ def main(): for pkg in sorted(pkgs, key=lambda x: x.name): tot_files += 1 tot_size += pkg.size() - s = templ % (" " + pkg.name, bytes2human(pkg.size()), pkg.arch(), - pkg.pyver()) + s = templ % ( + " " + pkg.name, + bytes2human(pkg.size()), + pkg.arch(), + pkg.pyver(), + ) if 'pypy' in pkg.pyver(): print_color(s, color='violet') else: print_color(s, color='brown') - print_color("\n\ntotals: files=%s, size=%s" % ( - tot_files, bytes2human(tot_size)), bold=True) + print_color( + "\n\ntotals: files=%s, size=%s" % (tot_files, bytes2human(tot_size)), + bold=True, + ) if __name__ == '__main__': diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 8e47a08b49..09169984d8 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -38,13 +38,18 @@ # --- get + @memoize def sh(cmd): assert os.path.exists(AUTH_FILE) env = os.environ.copy() env['GOOGLE_APPLICATION_CREDENTIALS'] = AUTH_FILE - p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) + p = subprocess.Popen( + shlex.split(cmd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) @@ -62,8 +67,9 @@ def query(cmd): def top_packages(): global LAST_UPDATE - ret = query("pypinfo --all --json --days %s --limit %s '' project" % ( - DAYS, LIMIT)) + ret = query( + "pypinfo --all --json --days %s --limit %s '' project" % (DAYS, LIMIT) + ) LAST_UPDATE = ret['last_update'] return [(x['project'], x['download_count']) for x in ret['rows']] @@ -147,14 +153,18 @@ def main(): {'what': 'PYPI ranking', 'download_count': ranking()}, ] print_markdown_table('Overview', 'what', data) - print_markdown_table('Operating systems', 'system_name', - downloads_by_system()['rows']) - print_markdown_table('Distros', 'distro_name', - downloads_by_distro()['rows']) - print_markdown_table('Python versions', 'python_version', - downloads_pyver()['rows']) - print_markdown_table('Countries', 'country', - downloads_by_country()['rows']) + print_markdown_table( + 'Operating systems', 'system_name', downloads_by_system()['rows'] + ) + print_markdown_table( + 'Distros', 'distro_name', downloads_by_distro()['rows'] + ) + print_markdown_table( + 'Python versions', 'python_version', downloads_pyver()['rows'] + ) + print_markdown_table( + 'Countries', 'country', downloads_by_country()['rows'] + ) if __name__ == '__main__': diff --git a/scripts/internal/print_hashes.py b/scripts/internal/print_hashes.py index 68e06201c6..5b9cfb2091 100755 --- a/scripts/internal/print_hashes.py +++ b/scripts/internal/print_hashes.py @@ -22,16 +22,19 @@ def csum(file, kind): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('dir', type=str, - help='directory containing tar.gz or wheel files') + parser.add_argument( + 'dir', type=str, help='directory containing tar.gz or wheel files' + ) args = parser.parse_args() for name in sorted(os.listdir(args.dir)): file = os.path.join(args.dir, name) if os.path.isfile(file): md5 = csum(file, "md5") sha256 = csum(file, "sha256") - print("%s\nmd5: %s\nsha256: %s\n" % ( - os.path.basename(file), md5, sha256)) + print( + "%s\nmd5: %s\nsha256: %s\n" + % (os.path.basename(file), md5, sha256) + ) else: print("skipping %r (not a file)" % file) diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index a046e670a1..706601943b 100755 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -19,7 +19,8 @@ def sh(cmd): return subprocess.check_output( - shlex.split(cmd), universal_newlines=True).strip() + shlex.split(cmd), universal_newlines=True + ).strip() def get_tag_date(tag): diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 890a3a4154..158c46eeca 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -93,7 +93,7 @@ def safe_print(text, file=sys.stdout): def stderr_handle(): GetStdHandle = ctypes.windll.Kernel32.GetStdHandle - STD_ERROR_HANDLE_ID = ctypes.c_ulong(0xfffffff4) + 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) @@ -114,7 +114,9 @@ def win_colorprint(s, color=LIGHTBLUE): def sh(cmd, nolog=False): if not nolog: safe_print("cmd: " + cmd) - p = subprocess.Popen(cmd, shell=True, env=os.environ, cwd=os.getcwd()) # noqa + p = subprocess.Popen( + cmd, shell=True, env=os.environ, cwd=os.getcwd() # noqa + ) p.communicate() if p.returncode != 0: sys.exit(p.returncode) @@ -122,6 +124,7 @@ def sh(cmd, nolog=False): def rm(pattern, directory=False): """Recursively remove a file or dir by pattern.""" + def safe_remove(path): try: os.remove(path) @@ -458,9 +461,11 @@ def install_git_hooks(): """Install GIT pre-commit hook.""" if os.path.isdir('.git'): src = os.path.join( - ROOT_DIR, "scripts", "internal", "git_pre_commit.py") + ROOT_DIR, "scripts", "internal", "git_pre_commit.py" + ) dst = os.path.realpath( - os.path.join(ROOT_DIR, ".git", "hooks", "pre-commit")) + os.path.join(ROOT_DIR, ".git", "hooks", "pre-commit") + ) with open(src) as s: with open(dst, "w") as d: d.write(s.read()) @@ -490,8 +495,10 @@ def print_api_speed(): def download_appveyor_wheels(): """Download appveyor wheels.""" - sh("%s -Wa scripts\\internal\\download_wheels_appveyor.py " - "--user giampaolo --project psutil" % PYTHON) + sh( + "%s -Wa scripts\\internal\\download_wheels_appveyor.py " + "--user giampaolo --project psutil" % PYTHON + ) def generate_manifest(): @@ -537,9 +544,7 @@ def get_python(path): def parse_args(): parser = argparse.ArgumentParser() # option shared by all commands - parser.add_argument( - '-p', '--python', - help="use python executable path") + parser.add_argument('-p', '--python', help="use python executable path") sp = parser.add_subparsers(dest='command', title='targets') sp.add_parser('bench-oneshot', help="benchmarks for oneshot()") sp.add_parser('bench-oneshot_2', help="benchmarks for oneshot() (perf)") @@ -559,8 +564,9 @@ def parse_args(): test_by_name = sp.add_parser('test-by-name', help=" run test by name") sp.add_parser('test-connections', help="run connections tests") sp.add_parser('test-contracts', help="run contracts tests") - sp.add_parser('test-last-failed', - help="re-run tests which failed on last run") + sp.add_parser( + 'test-last-failed', help="re-run tests which failed on last run" + ) sp.add_parser('test-memleaks', help="run memory leaks tests") sp.add_parser('test-misc', help="run misc tests") sp.add_parser('test-platform', help="run windows only tests") @@ -591,7 +597,8 @@ def main(): PYTHON = get_python(args.python) if not PYTHON: return sys.exit( - "can't find any python installation matching %r" % args.python) + "can't find any python installation matching %r" % args.python + ) os.putenv('PYTHON', PYTHON) win_colorprint("using " + PYTHON) diff --git a/scripts/iotop.py b/scripts/iotop.py index a8f04c870c..23c3a376ee 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -118,8 +118,10 @@ def refresh_window(procs, disks_read, disks_write): templ = "%-5s %-7s %11s %11s %s" win.erase() - disks_tot = "Total DISK READ: %s | Total DISK WRITE: %s" \ - % (bytes2human(disks_read), bytes2human(disks_write)) + disks_tot = "Total DISK READ: %s | Total DISK WRITE: %s" % ( + bytes2human(disks_read), + bytes2human(disks_write), + ) printl(disks_tot) header = templ % ("PID", "USER", "DISK READ", "DISK WRITE", "COMMAND") @@ -131,7 +133,8 @@ def refresh_window(procs, disks_read, disks_write): p._username[:7], bytes2human(p._read_per_sec), bytes2human(p._write_per_sec), - p._cmdline) + p._cmdline, + ) try: printl(line) except curses.error: diff --git a/scripts/netstat.py b/scripts/netstat.py index 7a9b2908cd..b589009379 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -38,9 +38,15 @@ def main(): templ = "%-5s %-30s %-30s %-13s %-6s %s" - print(templ % ( - "Proto", "Local address", "Remote address", "Status", "PID", - "Program name")) + header = templ % ( + "Proto", + "Local address", + "Remote address", + "Status", + "PID", + "Program name", + ) + print(header) proc_names = {} for p in psutil.process_iter(['pid', 'name']): proc_names[p.info['pid']] = p.info['name'] @@ -50,14 +56,15 @@ def main(): if c.raddr: raddr = "%s:%s" % (c.raddr) name = proc_names.get(c.pid, '?') or '' - print(templ % ( + line = templ % ( proto_map[(c.family, c.type)], laddr, raddr or AD, c.status, c.pid or AD, name[:15], - )) + ) + print(line) if __name__ == '__main__': diff --git a/scripts/nettop.py b/scripts/nettop.py index 64d408fc4f..eafcd0f5db 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -80,12 +80,13 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): global lineno # totals - printl("total bytes: sent: %-10s received: %s" % ( - bytes2human(tot_after.bytes_sent), - bytes2human(tot_after.bytes_recv)), + printl( + "total bytes: sent: %-10s received: %s" + % ( + bytes2human(tot_after.bytes_sent), + bytes2human(tot_after.bytes_recv), + ) ) - printl("total packets: sent: %-10s received: %s" % ( - tot_after.packets_sent, tot_after.packets_recv)) # per-network interface details: let's sort network interfaces so # that the ones which generated more traffic are shown first @@ -96,6 +97,7 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): stats_before = pnic_before[name] stats_after = pnic_after[name] templ = "%-15s %15s %15s" + # fmt: off printl(templ % (name, "TOTAL", "PER-SEC"), highlight=True) printl(templ % ( "bytes-sent", @@ -120,6 +122,7 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): stats_after.packets_recv - stats_before.packets_recv, )) printl("") + # fmt: on win.refresh() lineno = 0 diff --git a/scripts/pidof.py b/scripts/pidof.py index b809fafbed..662d5d6573 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -22,8 +22,11 @@ def pidof(pgname): pids = [] for proc in psutil.process_iter(['name', 'cmdline']): # search for matches in the process name and cmdline - if proc.info['name'] == pgname or \ - proc.info['cmdline'] and proc.info['cmdline'][0] == pgname: + if ( + proc.info['name'] == pgname + or proc.info['cmdline'] + and proc.info['cmdline'][0] == pgname + ): pids.append(str(proc.pid)) return pids diff --git a/scripts/pmap.py b/scripts/pmap.py index 56c1b48829..577e4b2947 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -36,7 +36,7 @@ def safe_print(s): - s = s[:get_terminal_size()[0]] + s = s[: get_terminal_size()[0]] try: print(s) except UnicodeEncodeError: @@ -52,11 +52,13 @@ def main(): total_rss = 0 for m in p.memory_maps(grouped=False): total_rss += m.rss - safe_print(templ % ( + line = templ % ( m.addr.split('-')[0].zfill(16), bytes2human(m.rss), m.perms, - m.path)) + m.path, + ) + safe_print(line) print("-" * 31) print(templ % ("Total", bytes2human(total_rss), '', '')) safe_print("PID = %s, name = %s" % (p.pid, p.name())) diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 562a61a464..95c99d442a 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -132,8 +132,9 @@ def str_ntuple(nt, convert_bytes=False): if not convert_bytes: return ", ".join(["%s=%s" % (x, getattr(nt, x)) for x in nt._fields]) else: - return ", ".join(["%s=%s" % (x, bytes2human(getattr(nt, x))) - for x in nt._fields]) + return ", ".join( + ["%s=%s" % (x, bytes2human(getattr(nt, x))) for x in nt._fields] + ) def run(pid, verbose=False): @@ -156,7 +157,8 @@ def run(pid, verbose=False): pinfo['children'] = [] if pinfo['create_time']: started = datetime.datetime.fromtimestamp( - pinfo['create_time']).strftime('%Y-%m-%d %H:%M') + pinfo['create_time'] + ).strftime('%Y-%m-%d %H:%M') else: started = ACCESS_DENIED @@ -173,7 +175,8 @@ def run(pid, verbose=False): cpu_tot_time = "%s:%s.%s" % ( cpu_tot_time.seconds // 60 % 60, str(cpu_tot_time.seconds % 60).zfill(2), - str(cpu_tot_time.microseconds)[:2]) + str(cpu_tot_time.microseconds)[:2], + ) print_('cpu-tspent', cpu_tot_time) print_('cpu-times', str_ntuple(pinfo['cpu_times'])) if hasattr(proc, "cpu_affinity"): @@ -202,8 +205,10 @@ def run(pid, verbose=False): if psutil.WINDOWS: print_("ionice", ionice) else: - print_("ionice", "class=%s, value=%s" % ( - str(ionice.ioclass), ionice.value)) + print_( + "ionice", + "class=%s, value=%s" % (str(ionice.ioclass), ionice.value), + ) print_('num-threads', pinfo['num_threads']) if psutil.POSIX: @@ -238,8 +243,10 @@ def run(pid, verbose=False): if pinfo['connections']: template = '%-5s %-25s %-25s %s' - print_('connections', - template % ('PROTO', 'LOCAL ADDR', 'REMOTE ADDR', 'STATUS')) + print_( + 'connections', + template % ('PROTO', 'LOCAL ADDR', 'REMOTE ADDR', 'STATUS'), + ) for conn in pinfo['connections']: if conn.type == socket.SOCK_STREAM: type = 'TCP' @@ -252,11 +259,13 @@ def run(pid, verbose=False): rip, rport = '*', '*' else: rip, rport = conn.raddr - print_('', template % ( + line = template % ( type, "%s:%s" % (lip, lport), "%s:%s" % (rip, rport), - conn.status)) + conn.status, + ) + print_('', line) else: print_('connections', '') @@ -290,8 +299,11 @@ def run(pid, verbose=False): soft = "infinity" if hard == psutil.RLIM_INFINITY: hard = "infinity" - print_('', template % ( - RLIMITS_MAP.get(res_name, res_name), soft, hard)) + print_( + '', + template + % (RLIMITS_MAP.get(res_name, res_name), soft, hard), + ) if hasattr(proc, "environ") and pinfo['environ']: template = "%-25s %s" @@ -315,10 +327,12 @@ def run(pid, verbose=False): def main(): parser = argparse.ArgumentParser( - description="print information about a process") + description="print information about a process" + ) parser.add_argument("pid", type=int, help="process pid", nargs='?') - parser.add_argument('--verbose', '-v', action='store_true', - help="print more info") + parser.add_argument( + '--verbose', '-v', action='store_true', help="print more info" + ) args = parser.parse_args() run(args.pid, args.verbose) diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 574e370412..54e89b05e1 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -96,8 +96,10 @@ def main(): ) print(line) if ad_pids: - print("warning: access denied for %s pids" % (len(ad_pids)), - file=sys.stderr) + print( + "warning: access denied for %s pids" % (len(ad_pids)), + file=sys.stderr, + ) if __name__ == '__main__': diff --git a/scripts/ps.py b/scripts/ps.py index 58a1d8c296..f07b568652 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -41,11 +41,13 @@ def main(): today_day = datetime.date.today() + # 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", "STATUS", "START", "TIME", "CMDLINE")) + # fmt: on for p in psutil.process_iter(attrs, ad_value=None): if p.info['create_time']: ctime = datetime.datetime.fromtimestamp(p.info['create_time']) @@ -56,8 +58,9 @@ def main(): else: ctime = '' if p.info['cpu_times']: - cputime = time.strftime("%M:%S", - time.localtime(sum(p.info['cpu_times']))) + cputime = time.strftime( + "%M:%S", time.localtime(sum(p.info['cpu_times'])) + ) else: cputime = '' @@ -72,12 +75,21 @@ def main(): if not user: user = '' 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 '' + 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']) @@ -95,8 +107,9 @@ def main(): status, ctime, cputime, - cmdline) - print(line[:get_terminal_size()[0]]) + cmdline, + ) + print(line[: get_terminal_size()[0]]) if __name__ == '__main__': diff --git a/scripts/sensors.py b/scripts/sensors.py index a5f9729b4e..668cca0a22 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -61,23 +61,31 @@ def main(): if name in temps: print(" Temperatures:") for entry in temps[name]: - print(" %-20s %s°C (high=%s°C, critical=%s°C)" % ( - entry.label or name, entry.current, entry.high, - entry.critical)) + s = " %-20s %s°C (high=%s°C, critical=%s°C)" % ( + entry.label or name, + entry.current, + entry.high, + entry.critical, + ) + print(s) # Fans. if name in fans: print(" Fans:") for entry in fans[name]: - print(" %-20s %s RPM" % ( - entry.label or name, entry.current)) + print( + " %-20s %s RPM" + % (entry.label or name, entry.current) + ) # Battery. if battery: print("Battery:") print(" charge: %s%%" % round(battery.percent, 2)) if battery.power_plugged: - print(" status: %s" % ( - "charging" if battery.percent < 100 else "fully charged")) + print( + " status: %s" + % ("charging" if battery.percent < 100 else "fully charged") + ) print(" plugged in: yes") else: print(" left: %s" % secs2hours(battery.secsleft)) diff --git a/scripts/temperatures.py b/scripts/temperatures.py index a211b88732..118ebc2652 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -38,9 +38,13 @@ def main(): for name, entries in temps.items(): print(name) for entry in entries: - print(" %-20s %s °C (high = %s °C, critical = %s °C)" % ( - entry.label or name, entry.current, entry.high, - entry.critical)) + line = " %-20s %s °C (high = %s °C, critical = %s °C)" % ( + entry.label or name, + entry.current, + entry.high, + entry.critical, + ) + print(line) print() diff --git a/scripts/top.py b/scripts/top.py index 675f541ef1..c0687ae1fe 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -48,11 +48,7 @@ win = curses.initscr() lineno = 0 -colors_map = dict( - green=3, - red=10, - yellow=4, -) +colors_map = dict(green=3, red=10, yellow=4) def printl(line, color=None, bold=False, highlight=False): @@ -75,6 +71,7 @@ def printl(line, color=None, bold=False, highlight=False): else: lineno += 1 + # --- /curses stuff @@ -85,9 +82,16 @@ def poll(interval): procs_status = {} for p in psutil.process_iter(): try: - p.dict = p.as_dict(['username', 'nice', 'memory_info', - 'memory_percent', 'cpu_percent', - 'cpu_times', 'name', 'status']) + p.dict = p.as_dict([ + 'username', + 'nice', + 'memory_info', + 'memory_percent', + 'cpu_percent', + 'cpu_times', + 'name', + 'status', + ]) try: procs_status[p.dict['status']] += 1 except KeyError: @@ -98,8 +102,9 @@ def poll(interval): procs.append(p) # return processes sorted by CPU percent usage - processes = sorted(procs, key=lambda p: p.dict['cpu_percent'], - reverse=True) + processes = sorted( + procs, key=lambda p: p.dict['cpu_percent'], reverse=True + ) return (processes, procs_status) @@ -131,7 +136,8 @@ def get_dashes(perc): mem = psutil.virtual_memory() dashes, empty_dashes = get_dashes(mem.percent) line = " Mem [%s%s] %5s%% %6s / %s" % ( - dashes, empty_dashes, + dashes, + empty_dashes, mem.percent, bytes2human(mem.used), bytes2human(mem.total), @@ -142,7 +148,8 @@ def get_dashes(perc): swap = psutil.swap_memory() dashes, empty_dashes = get_dashes(swap.percent) line = " Swap [%s%s] %5s%% %6s / %s" % ( - dashes, empty_dashes, + dashes, + empty_dashes, swap.percent, bytes2human(swap.used), bytes2human(swap.total), @@ -157,11 +164,16 @@ def get_dashes(perc): st.sort(key=lambda x: x[:3] in ('run', 'sle'), reverse=1) printl(" Processes: %s (%s)" % (num_procs, ', '.join(st))) # load average, uptime - uptime = datetime.datetime.now() - \ - datetime.datetime.fromtimestamp(psutil.boot_time()) + uptime = datetime.datetime.now() - datetime.datetime.fromtimestamp( + psutil.boot_time() + ) av1, av2, av3 = psutil.getloadavg() - line = " Load average: %.2f %.2f %.2f Uptime: %s" \ - % (av1, av2, av3, str(uptime).split('.')[0]) + line = " Load average: %.2f %.2f %.2f Uptime: %s" % ( + av1, + av2, + av3, + str(uptime).split('.')[0], + ) printl(line) @@ -170,8 +182,17 @@ def refresh_window(procs, procs_status): curses.endwin() templ = "%-6s %-8s %4s %6s %6s %5s %5s %9s %2s" win.erase() - header = templ % ("PID", "USER", "NI", "VIRT", "RES", "CPU%", "MEM%", - "TIME+", "NAME") + header = templ % ( + "PID", + "USER", + "NI", + "VIRT", + "RES", + "CPU%", + "MEM%", + "TIME+", + "NAME", + ) print_header(procs_status, len(procs)) printl("") printl(header, bold=True, highlight=True) @@ -180,9 +201,11 @@ def refresh_window(procs, procs_status): # is expressed as: "mm:ss.ms" if p.dict['cpu_times'] is not None: ctime = datetime.timedelta(seconds=sum(p.dict['cpu_times'])) - ctime = "%s:%s.%s" % (ctime.seconds // 60 % 60, - str(ctime.seconds % 60).zfill(2), - str(ctime.microseconds)[:2]) + ctime = "%s:%s.%s" % ( + ctime.seconds // 60 % 60, + str(ctime.seconds % 60).zfill(2), + str(ctime.microseconds)[:2], + ) else: ctime = '' if p.dict['memory_percent'] is not None: @@ -192,16 +215,17 @@ def refresh_window(procs, procs_status): if p.dict['cpu_percent'] is None: p.dict['cpu_percent'] = '' username = p.dict['username'][:8] if p.dict['username'] else '' - line = templ % (p.pid, - username, - p.dict['nice'], - bytes2human(getattr(p.dict['memory_info'], 'vms', 0)), - bytes2human(getattr(p.dict['memory_info'], 'rss', 0)), - p.dict['cpu_percent'], - p.dict['memory_percent'], - ctime, - p.dict['name'] or '', - ) + line = templ % ( + p.pid, + username, + p.dict['nice'], + bytes2human(getattr(p.dict['memory_info'], 'vms', 0)), + bytes2human(getattr(p.dict['memory_info'], 'rss', 0)), + p.dict['cpu_percent'], + p.dict['memory_percent'], + ctime, + p.dict['name'] or '', + ) try: printl(line) except curses.error: diff --git a/scripts/who.py b/scripts/who.py index 77c474ff9b..64a9481075 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -21,13 +21,14 @@ def main(): users = psutil.users() for user in users: proc_name = psutil.Process(user.pid).name() if user.pid else "" - print("%-12s %-10s %-10s %-14s %s" % ( + line = "%-12s %-10s %-10s %-14s %s" % ( user.name, user.terminal or '-', datetime.fromtimestamp(user.started).strftime("%Y-%m-%d %H:%M"), "(%s)" % user.host if user.host else "", proc_name, - )) + ) + print(line) if __name__ == '__main__': diff --git a/scripts/winservices.py b/scripts/winservices.py index d9c6a14a97..8d941d5324 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -44,8 +44,13 @@ def main(): for service in psutil.win_service_iter(): info = service.as_dict() print("%r (%r)" % (info['name'], info['display_name'])) - print("status: %s, start: %s, username: %s, pid: %s" % ( - info['status'], info['start_type'], info['username'], info['pid'])) + s = "status: %s, start: %s, username: %s, pid: %s" % ( + info['status'], + info['start_type'], + info['username'], + info['pid'], + ) + print(s) print("binpath: %s" % info['binpath']) print("") diff --git a/setup.py b/setup.py index 1ab704854f..7c59f56450 100755 --- a/setup.py +++ b/setup.py @@ -86,15 +86,17 @@ sources.append('psutil/_psutil_posix.c') -extras_require = {"test": [ - "enum34; python_version <= '3.4'", - "ipaddress; python_version < '3.0'", - "mock; python_version < '3.0'", -]} +extras_require = { + "test": [ + "enum34; python_version <= '3.4'", + "ipaddress; python_version < '3.0'", + "mock; python_version < '3.0'", + ] +} if not PYPY: - extras_require['test'].extend([ - "pywin32; sys.platform == 'win32'", - "wmi; sys.platform == 'win32'"]) + extras_require['test'].extend( + ["pywin32; sys.platform == 'win32'", "wmi; sys.platform == 'win32'"] + ) def get_version(): @@ -131,9 +133,12 @@ def get_version(): def get_long_description(): script = os.path.join(HERE, "scripts", "internal", "convert_readme.py") readme = os.path.join(HERE, 'README.rst') - p = subprocess.Popen([sys.executable, script, readme], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True) + p = subprocess.Popen( + [sys.executable, script, readme], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) @@ -173,7 +178,8 @@ def unix_can_compile(c_code): from distutils.unixccompiler import UnixCCompiler with tempfile.NamedTemporaryFile( - suffix='.c', delete=False, mode="wt") as f: + suffix='.c', delete=False, mode="wt" + ) as f: f.write(c_code) tempdir = tempfile.mkdtemp() @@ -195,6 +201,7 @@ def unix_can_compile(c_code): if WINDOWS: + def get_winver(): maj, min = sys.getwindowsversion()[0:2] return '0x0%s' % ((maj * 100) + min) @@ -219,18 +226,27 @@ def get_winver(): ext = Extension( 'psutil._psutil_windows', sources=( - sources + - ["psutil/_psutil_windows.c"] + - glob.glob("psutil/arch/windows/*.c") + sources + + ["psutil/_psutil_windows.c"] + + glob.glob("psutil/arch/windows/*.c") ), define_macros=macros, libraries=[ - "psapi", "kernel32", "advapi32", "shell32", "netapi32", - "ws2_32", "PowrProf", "pdh", + "psapi", + "kernel32", + "advapi32", + "shell32", + "netapi32", + "ws2_32", + "PowrProf", + "pdh", ], # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"], - **py_limited_api # noqa (noqa needed for python 2.7) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on ) elif MACOS: @@ -238,57 +254,76 @@ def get_winver(): ext = Extension( 'psutil._psutil_osx', sources=( - sources + - ["psutil/_psutil_osx.c"] + - glob.glob("psutil/arch/osx/*.c") + sources + + ["psutil/_psutil_osx.c"] + + glob.glob("psutil/arch/osx/*.c") ), define_macros=macros, extra_link_args=[ - '-framework', 'CoreFoundation', '-framework', 'IOKit', + '-framework', + 'CoreFoundation', + '-framework', + 'IOKit', ], - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) elif FREEBSD: macros.append(("PSUTIL_FREEBSD", 1)) ext = Extension( 'psutil._psutil_bsd', sources=( - sources + - ["psutil/_psutil_bsd.c"] + - glob.glob("psutil/arch/bsd/*.c") + - glob.glob("psutil/arch/freebsd/*.c") + sources + + ["psutil/_psutil_bsd.c"] + + glob.glob("psutil/arch/bsd/*.c") + + glob.glob("psutil/arch/freebsd/*.c") ), define_macros=macros, libraries=["devstat"], - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) elif OPENBSD: macros.append(("PSUTIL_OPENBSD", 1)) ext = Extension( 'psutil._psutil_bsd', sources=( - sources + - ["psutil/_psutil_bsd.c"] + - glob.glob("psutil/arch/bsd/*.c") + - glob.glob("psutil/arch/openbsd/*.c") + sources + + ["psutil/_psutil_bsd.c"] + + glob.glob("psutil/arch/bsd/*.c") + + glob.glob("psutil/arch/openbsd/*.c") ), define_macros=macros, libraries=["kvm"], - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) elif NETBSD: macros.append(("PSUTIL_NETBSD", 1)) ext = Extension( 'psutil._psutil_bsd', sources=( - sources + - ["psutil/_psutil_bsd.c"] + - glob.glob("psutil/arch/bsd/*.c") + - glob.glob("psutil/arch/netbsd/*.c") + sources + + ["psutil/_psutil_bsd.c"] + + glob.glob("psutil/arch/bsd/*.c") + + glob.glob("psutil/arch/netbsd/*.c") ), define_macros=macros, libraries=["kvm"], - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) elif LINUX: # see: https://github.com/giampaolo/psutil/issues/659 @@ -299,38 +334,53 @@ def get_winver(): ext = Extension( 'psutil._psutil_linux', sources=( - sources + - ["psutil/_psutil_linux.c"] + - glob.glob("psutil/arch/linux/*.c") + sources + + ["psutil/_psutil_linux.c"] + + glob.glob("psutil/arch/linux/*.c") ), define_macros=macros, - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) elif SUNOS: macros.append(("PSUTIL_SUNOS", 1)) ext = Extension( 'psutil._psutil_sunos', - sources=sources + [ + sources=sources + + [ 'psutil/_psutil_sunos.c', 'psutil/arch/solaris/v10/ifaddrs.c', 'psutil/arch/solaris/environ.c', ], define_macros=macros, libraries=['kstat', 'nsl', 'socket'], - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) elif AIX: macros.append(("PSUTIL_AIX", 1)) ext = Extension( 'psutil._psutil_aix', - sources=sources + [ + sources=sources + + [ 'psutil/_psutil_aix.c', 'psutil/arch/aix/net_connections.c', 'psutil/arch/aix/common.c', - 'psutil/arch/aix/ifaddrs.c'], + 'psutil/arch/aix/ifaddrs.c', + ], libraries=['perfstat'], define_macros=macros, - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) else: sys.exit('platform %s is not supported' % sys.platform) @@ -341,8 +391,13 @@ def get_winver(): 'psutil._psutil_posix', define_macros=macros, sources=sources, - **py_limited_api) + # fmt: off + # python 2.7 compatibility requires no comma + **py_limited_api + # fmt: on + ) if SUNOS: + def get_sunos_update(): # See https://serverfault.com/q/524883 # for an explanation of Solaris /etc/release @@ -371,6 +426,7 @@ def get_sunos_update(): cmdclass = {} if py_limited_api: + class bdist_wheel_abi3(bdist_wheel): def get_tag(self): python, abi, plat = bdist_wheel.get_tag(self) @@ -384,9 +440,10 @@ def main(): name='psutil', version=VERSION, cmdclass=cmdclass, - description=__doc__ .replace('\n', ' ').strip() if __doc__ else '', + description=__doc__.replace('\n', ' ').strip() if __doc__ else '', long_description=get_long_description(), long_description_content_type='text/x-rst', + # fmt: off keywords=[ 'ps', 'top', 'kill', 'free', 'lsof', 'netstat', 'nice', 'tty', 'ionice', 'uptime', 'taskmgr', 'process', 'df', 'iotop', 'iostat', @@ -394,6 +451,7 @@ def main(): 'monitoring', 'ulimit', 'prlimit', 'smem', 'performance', 'metrics', 'agent', 'observability', ], + # fmt: on author='Giampaolo Rodola', author_email='g.rodola@gmail.com', url='https://github.com/giampaolo/psutil', @@ -451,7 +509,8 @@ def main(): if setuptools is not None: kwargs.update( python_requires=( - ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"), + ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + ), extras_require=extras_require, zip_safe=False, ) @@ -461,37 +520,47 @@ def main(): success = True finally: cmd = sys.argv[1] if len(sys.argv) >= 2 else '' - if not success and POSIX and \ - cmd.startswith(("build", "install", "sdist", "bdist", - "develop")): + if ( + not success + and POSIX + and cmd.startswith( + ("build", "install", "sdist", "bdist", "develop") + ) + ): py3 = "3" if PY3 else "" if LINUX: pyimpl = "pypy" if PYPY else "python" if which('dpkg'): - missdeps("sudo apt-get install gcc %s%s-dev" % - (pyimpl, py3)) + missdeps( + "sudo apt-get install gcc %s%s-dev" % (pyimpl, py3) + ) elif which('rpm'): missdeps("sudo yum install gcc %s%s-devel" % (pyimpl, py3)) elif which('apk'): missdeps( - "sudo apk add gcc %s%s-dev musl-dev linux-headers" % ( - pyimpl, py3), + "sudo apk add gcc %s%s-dev musl-dev linux-headers" + % (pyimpl, py3) ) elif MACOS: - print(hilite("XCode (https://developer.apple.com/xcode/) " - "is not installed", color="red"), file=sys.stderr) + msg = ( + "XCode (https://developer.apple.com/xcode/)" + " is not installed" + ) + print(hilite(msg, color="red"), file=sys.stderr) elif FREEBSD: if which('pkg'): missdeps("pkg install gcc python%s" % py3) - elif which('mport'): # MidnightBSD + elif which('mport'): # MidnightBSD missdeps("mport install gcc python%s" % py3) elif OPENBSD: missdeps("pkg_add -v gcc python%s" % py3) elif NETBSD: missdeps("pkgin install gcc python%s" % py3) elif SUNOS: - missdeps("sudo ln -s /usr/bin/gcc /usr/local/bin/cc && " - "pkg install gcc") + missdeps( + "sudo ln -s /usr/bin/gcc /usr/local/bin/cc && " + "pkg install gcc" + ) if __name__ == '__main__': From 89eac06f6dd5e19868c21ff1e881cee919f81adb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 7 Jan 2024 00:22:09 +0100 Subject: [PATCH 1065/1714] refactor tests + make them more robust --- psutil/_pslinux.py | 6 +----- psutil/tests/test_connections.py | 26 +++++++++++++------------- psutil/tests/test_contracts.py | 7 ++++--- psutil/tests/test_linux.py | 13 +++++-------- psutil/tests/test_process.py | 6 +++++- psutil/tests/test_testutils.py | 2 +- 6 files changed, 29 insertions(+), 31 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e19e37a974..798dd3651e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -2031,11 +2031,7 @@ def memory_full_info(self): if HAS_PROC_SMAPS_ROLLUP: # faster try: uss, pss, swap = self._parse_smaps_rollup() - except (ProcessLookupError, FileNotFoundError) as err: - debug( - "ignore %r for pid %s and retry using /proc/pid/smaps" - % (err, self.pid) - ) + except (ProcessLookupError, FileNotFoundError): uss, pss, swap = self._parse_smaps() else: uss, pss, swap = self._parse_smaps() diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 5924dbb2c0..082bf981a9 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -55,7 +55,7 @@ def setUp(self): # Process opens a UNIX socket to /var/log/run. return cons = thisproc.connections(kind='all') - assert not cons, cons + self.assertEqual(cons, []) def tearDown(self): # Make sure we closed all resources. @@ -63,7 +63,7 @@ def tearDown(self): if NETBSD or FREEBSD or (MACOS and not PY3): return cons = thisproc.connections(kind='all') - assert not cons, cons + self.assertEqual(cons, []) def compare_procsys_connections(self, pid, proc_cons, kind='all'): """Given a process PID and its list of connections compare @@ -156,7 +156,7 @@ 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) - assert not conn.raddr + self.assertEqual(conn.raddr, ()) self.assertEqual(conn.status, psutil.CONN_LISTEN) @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") @@ -164,14 +164,14 @@ 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) - assert not conn.raddr + 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) - assert not conn.raddr + self.assertEqual(conn.raddr, ()) self.assertEqual(conn.status, psutil.CONN_NONE) @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") @@ -179,7 +179,7 @@ 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) - assert not conn.raddr + self.assertEqual(conn.raddr, ()) self.assertEqual(conn.status, psutil.CONN_NONE) @unittest.skipIf(not POSIX, 'POSIX only') @@ -187,7 +187,7 @@ 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) - assert not conn.raddr + self.assertEqual(conn.raddr, "") self.assertEqual(conn.status, psutil.CONN_NONE) @unittest.skipIf(not POSIX, 'POSIX only') @@ -195,7 +195,7 @@ 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) - assert not conn.raddr + self.assertEqual(conn.raddr, "") self.assertEqual(conn.status, psutil.CONN_NONE) @@ -210,7 +210,7 @@ class TestConnectedSocket(ConnectionTestCase): @unittest.skipIf(SUNOS, "unreliable on SUONS") def test_tcp(self): addr = ("127.0.0.1", 0) - assert not thisproc.connections(kind='tcp4') + self.assertEqual(thisproc.connections(kind='tcp4'), []) server, client = tcp_socketpair(AF_INET, addr=addr) try: cons = thisproc.connections(kind='tcp4') @@ -233,8 +233,8 @@ def test_unix(self): server, client = unix_socketpair(testfn) try: cons = thisproc.connections(kind='unix') - assert not (cons[0].laddr and cons[0].raddr) - assert not (cons[1].laddr and cons[1].raddr) + 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. @@ -313,9 +313,9 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): for kind in all_kinds: cons = proc.connections(kind=kind) if kind in kinds: - assert cons + self.assertNotEqual(cons, []) else: - assert not cons, cons + self.assertEqual(cons, []) # compare against system-wide connections # XXX Solaris can't retrieve system-wide UNIX # sockets. diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index aa5a20abb9..9dc54f8647 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -371,7 +371,8 @@ def proc_info(pid): def check_exception(exc, proc, name, ppid): tcase.assertEqual(exc.pid, pid) - tcase.assertEqual(exc.name, name) + 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: @@ -552,7 +553,7 @@ def username(self, ret, info): def status(self, ret, info): self.assertIsInstance(ret, str) - assert ret + assert ret, ret self.assertNotEqual(ret, '?') # XXX self.assertIn(ret, VALID_PROC_STATUSES) @@ -706,7 +707,7 @@ def is_running(self, ret, info): def cpu_affinity(self, ret, info): self.assertIsInstance(ret, list) - assert ret != [], ret + self.assertNotEqual(ret, []) cpus = list(range(psutil.cpu_count())) for n in ret: self.assertIsInstance(n, int) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index ada196bcda..0aa04f1214 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -845,7 +845,7 @@ def path_exists_mock(path): with mock.patch("os.path.exists", side_effect=path_exists_mock): reload_module(psutil._pslinux) ret = psutil.cpu_freq() - assert ret + assert ret, ret self.assertEqual(ret.max, 0.0) self.assertEqual(ret.min, 0.0) for freq in psutil.cpu_freq(percpu=True): @@ -1972,8 +1972,7 @@ def test_open_files_file_gone(self): 'psutil._pslinux.os.readlink', side_effect=OSError(errno.ENOENT, ""), ) as m: - files = p.open_files() - assert not files + 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' @@ -1997,8 +1996,7 @@ def test_open_files_fd_gone(self): with mock.patch( patch_point, side_effect=IOError(errno.ENOENT, "") ) as m: - files = p.open_files() - assert not files + self.assertEqual(p.open_files(), []) assert m.called def test_open_files_enametoolong(self): @@ -2015,8 +2013,7 @@ def test_open_files_enametoolong(self): patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") ) as m: with mock.patch("psutil._pslinux.debug"): - files = p.open_files() - assert not files + self.assertEqual(p.open_files(), []) assert m.called # --- mocked tests @@ -2250,7 +2247,7 @@ def test_connections_enametoolong(self): ) as m: p = psutil.Process() with mock.patch("psutil._pslinux.debug"): - assert not p.connections() + self.assertEqual(p.connections(), []) assert m.called diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 5ccd43b3f2..0418fd54ed 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -78,7 +78,11 @@ class TestProcess(PsutilTestCase): def spawn_psproc(self, *args, **kwargs): sproc = self.spawn_testproc(*args, **kwargs) - return psutil.Process(sproc.pid) + try: + return psutil.Process(sproc.pid) + except psutil.NoSuchProcess: + self.assertPidGone(sproc.pid) + raise # --- diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 825311064f..e92c4e11e8 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -318,7 +318,7 @@ def tcp_tcp_socketpair(self): def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() - assert not p.connections(kind='unix') + self.assertEqual(p.connections(kind='unix'), []) name = self.get_testfn() server, client = unix_socketpair(name) try: From 14a33ffe057e02abd1adc44dea20924e9a5e41fd Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 6 Jan 2024 18:24:45 -0500 Subject: [PATCH 1066/1714] Fix cpu_freq for Apple silicon (#2222) Apple SoC returns empty string after querying the cpu frequency using sysctl, this information however, can still be extracted from the IOKit registry. This PR adds a new method that is specific to Apple ARM architecture. Signed-off-by: Oliver T --- CREDITS | 4 ++ HISTORY.rst | 2 + psutil/arch/osx/cpu.c | 121 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 1ade9d3efa..59715ac762 100644 --- a/CREDITS +++ b/CREDITS @@ -813,3 +813,7 @@ I: 2156, 2345 N: Lawrence D'Anna W: https://github.com/smoofra I: 2010 + +N: Oliver Tomé +W: https://github.com/snom3ad +I: 2222 diff --git a/HISTORY.rst b/HISTORY.rst index 8467b90e5b..e0316f66cf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,8 @@ - 2340_, [NetBSD]: if process is terminated, `Process.cwd()`_ will return an empty string instead of raising `NoSuchProcess`_. - 2345_, [Linux]: fix compilation on older compiler missing DUPLEX_UNKNOWN +- 2222_, [macOS]: `cpu_freq()` now returns fixed values for `min` and `max` + frequencies in all Apple Silicon chips. 5.9.7 ===== diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index a1ba114296..4196083ecc 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -26,6 +26,10 @@ For reference, here's the git history with original implementations: #include #include #include +#if defined(__arm64__) || defined(__aarch64__) +#include +#include +#endif #include "../../_psutil_common.h" #include "../../_psutil_posix.h" @@ -109,7 +113,122 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ); } +#if defined(__arm64__) || defined(__aarch64__) +PyObject * +psutil_cpu_freq(PyObject *self, PyObject *args) { + uint32_t min; + uint32_t curr; + uint32_t pMin; + uint32_t eMin; + uint32_t max; + kern_return_t status; + CFDictionaryRef matching = NULL; + CFTypeRef pCoreRef = NULL; + CFTypeRef eCoreRef = NULL; + io_iterator_t iter = 0; + io_registry_entry_t entry = 0; + io_name_t name; + + matching = IOServiceMatching("AppleARMIODevice"); + if (matching == 0) { + return PyErr_Format( + PyExc_RuntimeError, + "IOServiceMatching call failed, 'AppleARMIODevice' not found" + ); + } + + status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); + if (status != KERN_SUCCESS) { + PyErr_Format( + PyExc_RuntimeError, "IOServiceGetMatchingServices call failed" + ); + goto error; + } + + while ((entry = IOIteratorNext(iter)) != 0) { + status = IORegistryEntryGetName(entry, name); + if (status != KERN_SUCCESS) { + IOObjectRelease(entry); + continue; + } + if (strcmp(name, "pmgr") == 0) { + break; + } + IOObjectRelease(entry); + } + if (entry == 0) { + PyErr_Format( + PyExc_RuntimeError, + "'pmgr' entry was not found in AppleARMIODevice service" + ); + goto error; + } + + pCoreRef = IORegistryEntryCreateCFProperty( + entry, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, 0); + if (pCoreRef == NULL) { + PyErr_Format( + PyExc_RuntimeError, "'voltage-states5-sram' property not found"); + goto error; + } + + eCoreRef = IORegistryEntryCreateCFProperty( + entry, CFSTR("voltage-states1-sram"), kCFAllocatorDefault, 0); + if (eCoreRef == NULL) { + PyErr_Format( + PyExc_RuntimeError, "'voltage-states1-sram' property not found"); + goto error; + } + + size_t pCoreLength = CFDataGetLength(pCoreRef); + size_t eCoreLength = CFDataGetLength(eCoreRef); + if (pCoreLength < 8) { + PyErr_Format( + PyExc_RuntimeError, + "expected 'voltage-states5-sram' buffer to have at least size 8" + ); + goto error; + } + if (eCoreLength < 4) { + PyErr_Format( + PyExc_RuntimeError, + "expected 'voltage-states1-sram' buffer to have at least size 4" + ); + goto error; + } + + CFDataGetBytes(pCoreRef, CFRangeMake(0, 4), (UInt8 *) &pMin); + CFDataGetBytes(eCoreRef, CFRangeMake(0, 4), (UInt8 *) &eMin); + CFDataGetBytes(pCoreRef, CFRangeMake(pCoreLength - 8, 4), (UInt8 *) &max); + + min = pMin < eMin ? pMin : eMin; + curr = max; + + CFRelease(pCoreRef); + CFRelease(eCoreRef); + IOObjectRelease(iter); + IOObjectRelease(entry); + + return Py_BuildValue( + "IKK", + curr / 1000 / 1000, + min / 1000 / 1000, + max / 1000 / 1000 + ); + +error: + if (pCoreRef != NULL) + CFRelease(pCoreRef); + if (eCoreRef != NULL) + CFRelease(eCoreRef); + if (iter != 0) + IOObjectRelease(iter); + if (entry != 0) + IOObjectRelease(entry); + return NULL; +} +#else PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { unsigned int curr; @@ -138,7 +257,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { min / 1000 / 1000, max / 1000 / 1000); } - +#endif PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { From 82a43754a7f4898e7a3a2f9721ba32bf7c00a925 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 7 Jan 2024 02:09:37 +0100 Subject: [PATCH 1067/1714] improve tests reliability --- MANIFEST.in | 1 + Makefile | 4 + psutil/tests/__init__.py | 17 +- psutil/tests/test_connections.py | 59 ++-- psutil/tests/test_contracts.py | 437 ----------------------------- psutil/tests/test_process.py | 18 +- psutil/tests/test_process_all.py | 464 +++++++++++++++++++++++++++++++ psutil/tests/test_testutils.py | 13 +- scripts/internal/winmake.py | 27 +- 9 files changed, 541 insertions(+), 499 deletions(-) create mode 100755 psutil/tests/test_process_all.py diff --git a/MANIFEST.in b/MANIFEST.in index 14ede94554..bb60aa849b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -164,6 +164,7 @@ include psutil/tests/test_misc.py include psutil/tests/test_osx.py include psutil/tests/test_posix.py include psutil/tests/test_process.py +include psutil/tests/test_process_all.py include psutil/tests/test_sunos.py include psutil/tests/test_system.py include psutil/tests/test_testutils.py diff --git a/Makefile b/Makefile index 6b6c3f32e9..34279c90aa 100644 --- a/Makefile +++ b/Makefile @@ -140,6 +140,10 @@ test-process: ## Run process-related API tests. ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process.py +test-process-all: ## Run tests which iterate over all process PIDs. + ${MAKE} build + $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process_all.py + test-system: ## Run system-related API tests. ${MAKE} build $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_system.py diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 05f0d63b83..df30e30698 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -47,6 +47,7 @@ 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 @@ -105,7 +106,7 @@ # sync primitives 'call_until', 'wait_for_pid', 'wait_for_file', # network - 'check_net_address', + 'check_net_address', 'filter_proc_connections', 'get_free_port', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', 'unix_socketpair', 'create_sockets', # compat @@ -1871,6 +1872,20 @@ def check_status(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 # =================================================================== diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 082bf981a9..e261fc0f9d 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -35,6 +35,7 @@ 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 @@ -44,26 +45,24 @@ from psutil.tests import wait_for_file -thisproc = psutil.Process() 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): - if NETBSD or FREEBSD or (MACOS and not PY3): - # Process opens a UNIX socket to /var/log/run. - return - cons = thisproc.connections(kind='all') - self.assertEqual(cons, []) + self.assertEqual(this_proc_connections(kind='all'), []) def tearDown(self): # Make sure we closed all resources. - # Some BSDs open a UNIX socket to /var/log/run. - if NETBSD or FREEBSD or (MACOS and not PY3): - return - cons = thisproc.connections(kind='all') - self.assertEqual(cons, []) + 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 @@ -95,11 +94,11 @@ def test_system(self): def test_process(self): with create_sockets(): - for conn in psutil.Process().connections(kind='all'): + for conn in this_proc_connections(kind='all'): check_connection_ntuple(conn) def test_invalid_kind(self): - self.assertRaises(ValueError, thisproc.connections, kind='???') + self.assertRaises(ValueError, this_proc_connections, kind='???') self.assertRaises(ValueError, psutil.net_connections, kind='???') @@ -108,7 +107,7 @@ class TestUnconnectedSockets(ConnectionTestCase): """Tests sockets which are open but not connected to anything.""" def get_conn_from_sock(self, sock): - cons = thisproc.connections(kind='all') + 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 @@ -148,7 +147,7 @@ def check_socket(self, sock): # XXX Solaris can't retrieve system-wide UNIX sockets if sock.family == AF_UNIX and HAS_CONNECTIONS_UNIX: - cons = thisproc.connections(kind='all') + cons = this_proc_connections(kind='all') self.compare_procsys_connections(os.getpid(), cons, kind='all') return conn @@ -210,17 +209,17 @@ class TestConnectedSocket(ConnectionTestCase): @unittest.skipIf(SUNOS, "unreliable on SUONS") def test_tcp(self): addr = ("127.0.0.1", 0) - self.assertEqual(thisproc.connections(kind='tcp4'), []) + self.assertEqual(this_proc_connections(kind='tcp4'), []) server, client = tcp_socketpair(AF_INET, addr=addr) try: - cons = thisproc.connections(kind='tcp4') + 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 = thisproc.connections(kind='all') + # cons = this_proc_connections(kind='all') # self.assertEqual(len(cons), 1) # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) finally: @@ -232,7 +231,7 @@ def test_unix(self): testfn = self.get_testfn() server, client = unix_socketpair(testfn) try: - cons = thisproc.connections(kind='unix') + 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: @@ -258,7 +257,7 @@ def test_unix(self): class TestFilters(ConnectionTestCase): def test_filters(self): def check(kind, families, types): - for conn in thisproc.connections(kind=kind): + for conn in this_proc_connections(kind=kind): self.assertIn(conn.family, families) self.assertIn(conn.type, types) if not SKIP_SYSCONS: @@ -373,7 +372,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): tcp6_addr = None udp6_addr = None - for p in thisproc.children(): + for p in psutil.Process().children(): cons = p.connections() self.assertEqual(len(cons), 1) for conn in cons: @@ -429,48 +428,48 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): def test_count(self): with create_sockets(): # tcp - cons = thisproc.connections(kind='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 = thisproc.connections(kind='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 = thisproc.connections(kind='tcp6') + 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 = thisproc.connections(kind='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 = thisproc.connections(kind='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 = thisproc.connections(kind='udp6') + 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 = thisproc.connections(kind='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 = thisproc.connections(kind='inet6') + cons = this_proc_connections(kind='inet6') self.assertEqual(len(cons), 2) for conn in cons: self.assertEqual(conn.family, AF_INET6) @@ -478,7 +477,7 @@ def test_count(self): # 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 = thisproc.connections(kind='unix') + cons = this_proc_connections(kind='unix') self.assertEqual(len(cons), 3) for conn in cons: self.assertEqual(conn.family, AF_UNIX) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 9dc54f8647..4736f5f1ad 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -9,34 +9,21 @@ Some of these are duplicates of tests test_system.py and test_process.py. """ -import errno -import multiprocessing -import os import platform import signal -import stat -import time -import traceback 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 OSX 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._compat import range -from psutil._compat import unicode -from psutil.tests import CI_TESTING from psutil.tests import GITHUB_ACTIONS from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_NET_IO_COUNTERS @@ -44,16 +31,11 @@ from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import PYPY from psutil.tests import SKIP_SYSCONS -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 enum from psutil.tests import is_namedtuple -from psutil.tests import is_win_secure_system_proc from psutil.tests import kernel_version -from psutil.tests import process_namespace -from psutil.tests import serialrun # =================================================================== @@ -361,425 +343,6 @@ def test_negative_signal(self): self.assertIsInstance(code, int) -# =================================================================== -# --- Featch all processes test -# =================================================================== - - -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. - """ - - use_proc_pool = 0 - - def setUp(self): - # Using a pool in a CI env may result in deadlock, see: - # https://github.com/giampaolo/psutil/issues/2104 - if self.use_proc_pool: - self.pool = multiprocessing.Pool() - - def tearDown(self): - if self.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_contracts.proc_info". - from psutil.tests.test_contracts import proc_info - - if self.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 diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0418fd54ed..f03a22da0b 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -746,9 +746,9 @@ def test_cmdline(self): @unittest.skipIf(PYPY, "broken on PYPY") def test_long_cmdline(self): - testfn = self.get_testfn() - create_exe(testfn) - cmdline = [testfn] + (["0123456789"] * 20) + cmdline = [PYTHON_EXE] + cmdline.extend(["-v"] * 50) + cmdline.extend(["-c", "time.sleep(10)"]) p = self.spawn_psproc(cmdline) if OPENBSD: # XXX: for some reason the test process may turn into a @@ -770,7 +770,7 @@ def test_name(self): def test_long_name(self): testfn = self.get_testfn(suffix="0123456789" * 2) create_exe(testfn) - cmdline = [testfn] + (["0123456789"] * 20) + cmdline = [testfn, "-c", "time.sleep(10)"] p = self.spawn_psproc(cmdline) if OPENBSD: # XXX: for some reason the test process may turn into a @@ -800,15 +800,7 @@ def test_prog_w_funky_name(self): # https://github.com/giampaolo/psutil/issues/628 funky_path = self.get_testfn(suffix='foo bar )') create_exe(funky_path) - cmdline = [ - funky_path, - "-c", - "import time; [time.sleep(0.01) for x in range(3000)];arg1", - "arg2", - "", - "arg3", - "", - ] + cmdline = [funky_path, "-c", "time.sleep(10)"] p = self.spawn_psproc(cmdline) self.assertEqual(p.cmdline(), cmdline) self.assertEqual(p.name(), os.path.basename(funky_path)) diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py new file mode 100755 index 0000000000..ee1a9b58d5 --- /dev/null +++ b/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/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index e92c4e11e8..a93f9f09f6 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -36,6 +36,7 @@ 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 @@ -318,14 +319,18 @@ def tcp_tcp_socketpair(self): def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() - self.assertEqual(p.connections(kind='unix'), []) + 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(p.connections(kind='unix')), 2) + self.assertEqual( + len(filter_proc_connections(p.connections(kind='unix'))), 2 + ) self.assertEqual(server.getsockname(), name) self.assertEqual(client.getpeername(), name) finally: @@ -374,10 +379,10 @@ def test_leak_mem(self): ls = [] def fun(ls=ls): - ls.append("x" * 24 * 1024) + ls.append("x" * 124 * 1024) try: - # will consume around 3M in total + # will consume around 30M in total self.assertRaisesRegex( AssertionError, "extra-mem", self.execute, fun, times=50 ) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 158c46eeca..43db68fd9f 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -397,6 +397,12 @@ def test_process(): sh("%s psutil\\tests\\test_process.py" % PYTHON) +def test_process_all(): + """Run process all tests.""" + build() + sh("%s psutil\\tests\\test_process_all.py" % PYTHON) + + def test_system(): """Run system tests.""" build() @@ -517,23 +523,15 @@ def get_python(path): # try to look for a python installation given a shortcut name path = path.replace('.', '') vers = ( - '26', - '26-32', - '26-64', '27', '27-32', '27-64', - '36', - '36-32', - '36-64', - '37', - '37-32', - '37-64', - '38', - '38-32', - '38-64', - '39-32', - '39-64', + '310-32', + '310-64', + '311-32', + '311-64', + '312-32', + '312-64', ) for v in vers: pypath = r'C:\\python%s\python.exe' % v @@ -571,6 +569,7 @@ def parse_args(): sp.add_parser('test-misc', help="run misc tests") sp.add_parser('test-platform', help="run windows only tests") sp.add_parser('test-process', help="run process tests") + sp.add_parser('test-process-all', help="run process all tests") sp.add_parser('test-system', help="run system tests") sp.add_parser('test-unicode', help="run unicode tests") sp.add_parser('test-testutils', help="run test utils tests") From 20ba2662bbd66e01d2ce0d93b84eb8db9e723542 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 7 Jan 2024 12:19:40 +0100 Subject: [PATCH 1068/1714] more tests refactoring --- psutil/tests/__init__.py | 69 +++++++++++++++++++++--------------- psutil/tests/test_process.py | 30 +++++++--------- psutil/tests/test_unicode.py | 19 ++++------ 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index df30e30698..336e176448 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -100,7 +100,7 @@ 'process_namespace', 'system_namespace', 'print_sysinfo', 'is_win_secure_system_proc', # fs utils - 'chdir', 'safe_rmpath', 'create_exe', 'get_testfn', + 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', # os 'get_winver', 'kernel_version', # sync primitives @@ -377,7 +377,7 @@ def spawn_testproc(cmd=None, **kwds): pyline = ( "from time import sleep;" + "open(r'%s', 'w').close();" % testfn - + "sleep(60);" + + "[sleep(0.1) for x in range(100)];" # 10 secs ) cmd = [PYTHON_EXE, "-c", pyline] sproc = subprocess.Popen(cmd, **kwds) @@ -853,33 +853,41 @@ def chdir(dirname): 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 unittest.SkipTest("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 open(get_testfn(suffix='.c'), "w") as f: - f.write(c_code) - try: - subprocess.check_call(["gcc", f.name, "-o", outpath]) - finally: - safe_rmpath(f.name) +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: - # copy python executable - shutil.copyfile(PYTHON_EXE, outpath) - if POSIX: - st = os.stat(outpath) - os.chmod(outpath, st.st_mode | stat.S_IEXEC) + 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): @@ -891,7 +899,9 @@ def get_testfn(suffix="", dir=None): while True: name = tempfile.mktemp(prefix=TESTFN_PREFIX, suffix=suffix, dir=dir) if not os.path.exists(name): # also include dirs - return os.path.realpath(name) # needed for OSX + path = os.path.realpath(name) # needed for OSX + atexit.register(safe_rmpath, path) + return path # =================================================================== @@ -940,6 +950,7 @@ class PsutilTestCase(TestCase): """ def get_testfn(self, suffix="", dir=None): + suffix += "-" + self.id() # add the test name fname = get_testfn(suffix=suffix, dir=dir) self.addCleanup(safe_rmpath, fname) return fname diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index f03a22da0b..fdd90dcbe2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -57,7 +57,8 @@ from psutil.tests import ThreadTask from psutil.tests import call_until from psutil.tests import copyload_shared_lib -from psutil.tests import create_exe +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 @@ -768,9 +769,8 @@ def test_name(self): @unittest.skipIf(PYPY, "unreliable on PYPY") def test_long_name(self): - testfn = self.get_testfn(suffix="0123456789" * 2) - create_exe(testfn) - cmdline = [testfn, "-c", "time.sleep(10)"] + pyexe = create_py_exe(self.get_testfn(suffix="0123456789" * 2)) + cmdline = [pyexe, "-c", "time.sleep(10)"] p = self.spawn_psproc(cmdline) if OPENBSD: # XXX: for some reason the test process may turn into a @@ -781,14 +781,14 @@ def test_long_name(self): # just compare the first 15 chars. Full explanation: # https://github.com/giampaolo/psutil/issues/2239 try: - self.assertEqual(p.name(), os.path.basename(testfn)) + self.assertEqual(p.name(), os.path.basename(pyexe)) except AssertionError: if p.status() == psutil.STATUS_ZOMBIE: - assert os.path.basename(testfn).startswith(p.name()) + assert os.path.basename(pyexe).startswith(p.name()) else: raise else: - self.assertEqual(p.name(), os.path.basename(testfn)) + self.assertEqual(p.name(), os.path.basename(pyexe)) # XXX @unittest.skipIf(SUNOS, "broken on SUNOS") @@ -798,15 +798,12 @@ 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 - funky_path = self.get_testfn(suffix='foo bar )') - create_exe(funky_path) - cmdline = [funky_path, "-c", "time.sleep(10)"] + pyexe = create_py_exe(self.get_testfn(suffix='foo bar )')) + cmdline = [pyexe, "-c", "time.sleep(10)"] p = self.spawn_psproc(cmdline) 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) - ) + 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): @@ -1477,10 +1474,9 @@ def test_weird_environ(self): return execve("/bin/cat", argv, envp); } """) - path = self.get_testfn() - create_exe(path, c_code=code) + cexe = create_c_exe(self.get_testfn(), c_code=code) sproc = self.spawn_testproc( - [path], stdin=subprocess.PIPE, stderr=subprocess.PIPE + [cexe], stdin=subprocess.PIPE, stderr=subprocess.PIPE ) p = psutil.Process(sproc.pid) wait_for_pid(p.pid) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 3ab71b03a8..aeac62ce5a 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -100,7 +100,7 @@ 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 create_py_exe from psutil.tests import get_testfn from psutil.tests import safe_mkdir from psutil.tests import safe_rmpath @@ -139,7 +139,7 @@ def try_unicode(suffix): testfn = get_testfn(suffix=suffix) try: safe_rmpath(testfn) - create_exe(testfn) + create_py_exe(testfn) sproc = spawn_testproc(cmd=[testfn]) shutil.copyfile(testfn, testfn + '-2') safe_rmpath(testfn + '-2') @@ -165,9 +165,13 @@ class BaseUnicodeTest(PsutilTestCase): 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() @@ -183,17 +187,6 @@ class TestFSAPIs(BaseUnicodeTest): funky_suffix = UNICODE_SUFFIX - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.funky_name = get_testfn(suffix=cls.funky_suffix) - create_exe(cls.funky_name) - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - safe_rmpath(cls.funky_name) - 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. From d4ae6a0b527407bcbea09654528129e58e9787bd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 12 Jan 2024 23:20:45 +0100 Subject: [PATCH 1069/1714] refact some tests --- psutil/tests/__init__.py | 3 ++- psutil/tests/test_process.py | 2 +- psutil/tests/test_unicode.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 336e176448..4d8e355838 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -757,6 +757,8 @@ 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) if WINDOWS: # give it some more time to allow better initialization @@ -950,7 +952,6 @@ class PsutilTestCase(TestCase): """ def get_testfn(self, suffix="", dir=None): - suffix += "-" + self.id() # add the test name fname = get_testfn(suffix=suffix, dir=dir) self.addCleanup(safe_rmpath, fname) return fname diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index fdd90dcbe2..b37944a871 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -762,7 +762,7 @@ def test_long_cmdline(self): self.assertEqual(p.cmdline(), cmdline) def test_name(self): - p = self.spawn_psproc(PYTHON_EXE) + p = self.spawn_psproc() name = p.name().lower() pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index aeac62ce5a..ab06cebed3 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -224,9 +224,7 @@ def test_proc_cmdline(self): for part in cmdline: self.assertIsInstance(part, str) if self.expect_exact_path_match(): - self.assertEqual( - cmdline, [self.funky_name, "-c", "time.sleep(10)"] - ) + self.assertEqual(cmdline, cmd) def test_proc_cwd(self): dname = self.funky_name + "2" From 86f171ac02c9862f9f47157fa9179498c54dca74 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 13 Jan 2024 13:12:35 +0100 Subject: [PATCH 1070/1714] refac t --- psutil/tests/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 4d8e355838..797969bf1a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -352,7 +352,7 @@ def wrapper(*args, **kwargs): @_reap_children_on_err def spawn_testproc(cmd=None, **kwds): - """Creates a python subprocess which does nothing for 60 secs and + """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. @@ -371,13 +371,13 @@ def spawn_testproc(cmd=None, **kwds): CREATE_NO_WINDOW = 0x8000000 kwds.setdefault("creationflags", CREATE_NO_WINDOW) if cmd is None: - testfn = get_testfn() + testfn = get_testfn(dir=os.getcwd()) try: safe_rmpath(testfn) pyline = ( - "from time import sleep;" + "import time;" + "open(r'%s', 'w').close();" % testfn - + "[sleep(0.1) for x in range(100)];" # 10 secs + + "[time.sleep(0.1) for x in range(100)];" # 10 secs ) cmd = [PYTHON_EXE, "-c", pyline] sproc = subprocess.Popen(cmd, **kwds) From 2d880c8a3bf9c239318bb283c52ce177f37244a7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 13 Jan 2024 15:09:26 +0100 Subject: [PATCH 1071/1714] fix failing tests --- psutil/tests/__init__.py | 3 --- psutil/tests/test_process.py | 6 +++--- psutil/tests/test_unicode.py | 6 +++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 797969bf1a..83bcfa5a96 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -760,9 +760,6 @@ def wait_for_pid(pid): if pid not in psutil.pids(): raise psutil.NoSuchProcess(pid) psutil.Process(pid) - if WINDOWS: - # give it some more time to allow better initialization - time.sleep(0.01) @retry( diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b37944a871..451c492005 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -749,7 +749,7 @@ def test_cmdline(self): def test_long_cmdline(self): cmdline = [PYTHON_EXE] cmdline.extend(["-v"] * 50) - cmdline.extend(["-c", "time.sleep(10)"]) + 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 @@ -770,7 +770,7 @@ def test_name(self): @unittest.skipIf(PYPY, "unreliable on PYPY") def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix="0123456789" * 2)) - cmdline = [pyexe, "-c", "time.sleep(10)"] + 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 @@ -799,7 +799,7 @@ def test_prog_w_funky_name(self): # 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", "time.sleep(10)"] + 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)) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index ab06cebed3..f4883bf9b2 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -198,7 +198,7 @@ def expect_exact_path_match(self): # --- def test_proc_exe(self): - cmd = [self.funky_name, "-c", "time.sleep(10)"] + cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] subp = self.spawn_testproc(cmd) p = psutil.Process(subp.pid) exe = p.exe() @@ -209,7 +209,7 @@ def test_proc_exe(self): ) def test_proc_name(self): - cmd = [self.funky_name, "-c", "time.sleep(10)"] + 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) @@ -217,7 +217,7 @@ def test_proc_name(self): self.assertEqual(name, os.path.basename(self.funky_name)) def test_proc_cmdline(self): - cmd = [self.funky_name, "-c", "time.sleep(10)"] + cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] subp = self.spawn_testproc(cmd) p = psutil.Process(subp.pid) cmdline = p.cmdline() From 27a1432daeacd46b287517c11bfea2af4fd95a88 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 19 Jan 2024 21:12:44 +0100 Subject: [PATCH 1072/1714] pre-release --- HISTORY.rst | 8 +++++--- docs/index.rst | 6 +++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e0316f66cf..fae3b43b2c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.8 (IN DEVELOPMENT) -====================== +5.9.8 +===== + +2024-01-19 **Enhancements** @@ -19,7 +21,7 @@ It could either leak memory or core dump. - 2340_, [NetBSD]: if process is terminated, `Process.cwd()`_ will return an empty string instead of raising `NoSuchProcess`_. -- 2345_, [Linux]: fix compilation on older compiler missing DUPLEX_UNKNOWN +- 2345_, [Linux]: fix compilation on older compiler missing DUPLEX_UNKNOWN. - 2222_, [macOS]: `cpu_freq()` now returns fixed values for `min` and `max` frequencies in all Apple Silicon chips. diff --git a/docs/index.rst b/docs/index.rst index cc0f7dccab..fc4cddc245 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2650,9 +2650,13 @@ PyPy3. Timeline ======== +- 2024-01-19: + `5.9.8 `__ - + `what's new `__ - + `diff `__ - 2023-12-17: `5.9.7 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2023-10-15: `5.9.6 `__ - From 494d8b84f601a19f4f298ffbb3b47a647384d521 Mon Sep 17 00:00:00 2001 From: Ryan Carsten Schmidt Date: Fri, 26 Jan 2024 02:42:14 -0600 Subject: [PATCH 1073/1714] Include net/if.h before net/if_dl.h (#2361) In old versions of macOS, net/if_dl.h neglects to include sys/types.h, which results in build failure: error: unknown type name 'u_char'; did you mean 'char'? Including net/if.h first works around the problem because net/if.h includes sys/types.h. Fixes #2360 --- psutil/arch/osx/net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index e9cc61e9b1..24ce1b834e 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -9,11 +9,11 @@ // https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c #include +#include #include #include #include #include -#include #include "../../_psutil_common.h" From 2f1cd0cafa895786183cf43bb5f02fceeb68de55 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 26 Jan 2024 09:46:03 +0100 Subject: [PATCH 1074/1714] update CREDITS + mention @c0m4r for sponsorship (thanks!) --- CREDITS | 4 ++++ HISTORY.rst | 7 +++++++ README.rst | 1 + 3 files changed, 12 insertions(+) diff --git a/CREDITS b/CREDITS index 59715ac762..ca58471317 100644 --- a/CREDITS +++ b/CREDITS @@ -817,3 +817,7 @@ I: 2010 N: Oliver Tomé W: https://github.com/snom3ad I: 2222 + +N: Ryan Carsten Schmidt +W: https://github.com/ryandesign +I: 2361 diff --git a/HISTORY.rst b/HISTORY.rst index fae3b43b2c..3877d437a8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,12 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +5.9.9 (IN DEVELOPMENT) +====================== + +**Bug fixes** + +- 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) + 5.9.8 ===== diff --git a/README.rst b/README.rst index 12bf64254e..0d14b99cd2 100644 --- a/README.rst +++ b/README.rst @@ -150,6 +150,7 @@ Supporters + add your avatar From 5f0a409607d8105e380c05e4d94d52474fbb73d8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 Feb 2024 00:28:18 +0100 Subject: [PATCH 1075/1714] adapt to new ruff config directives --- pyproject.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 677722dbe0..59c2b6a223 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,15 @@ [tool.black] +target-version = ["py37"] line-length = 79 skip-string-normalization = true preview = true -target-version = ["py37"] [tool.ruff] # https://beta.ruff.rs/docs/settings/ target-version = "py37" line-length = 79 + +[tool.ruff.lint] select = [ # To get a list of all values: `python3 -m ruff linter`. "ALL", @@ -29,6 +31,7 @@ ignore = [ "ARG002", # unused-method-argument "B007", # Loop control variable `x` not used within loop body "B904", # Within an `except` clause, raise exceptions with `raise ... from err` (PYTHON2.7 COMPAT) + "B904", # Use `raise from` to specify exception cause (PYTHON2.7 COMPAT) "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) "C90", # mccabe (function `X` is too complex) "COM812", # Trailing comma missing @@ -66,7 +69,6 @@ ignore = [ "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements "SLF", # flake8-self "TD", # all TODOs, XXXs, etc. - "TRY200", # Use `raise from` to specify exception cause (PYTHON2.7 COMPAT) "TRY300", # Consider moving this statement to an `else` block "TRY301", # Abstract `raise` to an inner function "UP009", # [*] UTF-8 encoding declaration is unnecessary (PYTHON2.7 COMPAT) @@ -77,7 +79,7 @@ ignore = [ "UP032", # [*] Use f-string instead of `format` call (PYTHON2.7 COMPAT) ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] # T201 == print(), T203 == pprint() # EM101 == raw-string-in-exception # TRY003 == raise-vanilla-args @@ -89,7 +91,7 @@ ignore = [ "scripts/internal/*" = ["T201", "T203"] "setup.py" = ["T201", "T203"] -[tool.ruff.isort] +[tool.ruff.lint.isort] # https://beta.ruff.rs/docs/settings/#isort force-single-line = true # one import per line lines-after-imports = 2 From 61dfd67808efc8ee99a7d6ea385abfb0bb97558b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 Feb 2024 10:20:40 +0100 Subject: [PATCH 1076/1714] more ruff rules --- docs/conf.py | 6 +++++- psutil/_pslinux.py | 3 +-- psutil/_pswindows.py | 3 +-- psutil/tests/__init__.py | 6 +++--- psutil/tests/test_linux.py | 3 --- psutil/tests/test_process.py | 4 ++-- psutil/tests/test_system.py | 2 +- pyproject.toml | 21 +++++++++++++++++++- scripts/disk_usage.py | 3 ++- scripts/ifconfig.py | 2 +- scripts/internal/download_wheels_appveyor.py | 2 +- scripts/internal/print_announce.py | 1 + scripts/internal/print_api_speed.py | 2 +- scripts/internal/print_downloads.py | 2 +- scripts/internal/winmake.py | 4 ++-- scripts/procsmem.py | 5 +++-- scripts/winservices.py | 2 +- 17 files changed, 46 insertions(+), 25 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f2b05483cf..3ebc64178c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,9 @@ # -*- 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. + # psutil documentation build configuration file, created by # sphinx-quickstart on Wed Oct 19 21:54:30 2016. # diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 798dd3651e..5d0deb4037 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -60,7 +60,6 @@ # fmt: off __extra__all__ = [ - # 'PROCFS_PATH', # io prio constants "IOPRIO_CLASS_NONE", "IOPRIO_CLASS_RT", "IOPRIO_CLASS_BE", @@ -1355,7 +1354,7 @@ def disk_partitions(all=False): if device in ("/dev/root", "rootfs"): device = RootFsDeviceFinder().find() or device if not all: - if device == '' or fstype not in fstypes: + if not device or fstype not in fstypes: continue maxfile = maxpath = None # set later ntuple = _common.sdiskpart( diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 2d3a0c9fdb..6199e5748e 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -239,7 +239,6 @@ def virtual_memory(): """System virtual memory as a namedtuple.""" mem = cext.virtual_mem() totphys, availphys, totsys, availsys = mem - # total = totphys avail = availphys free = availphys @@ -515,7 +514,7 @@ def win_service_get(name): return service -class WindowsService: +class WindowsService: # noqa: PLW1641 """Represents an installed Windows service.""" def __init__(self, name, display_name): diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 83bcfa5a96..5d74c185e8 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -737,9 +737,9 @@ def wrapper(*args, **kwargs): self.sleep() continue if PY3: - raise exc + raise exc # noqa: PLE0704 else: - raise + raise # noqa: PLE0704 # This way the user of the decorated function can change config # parameters. @@ -1944,7 +1944,7 @@ 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: + if len(b) != 1 or b[0] is not tuple: return False f = getattr(t, '_fields', None) if not isinstance(f, tuple): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0aa04f1214..9fe399d52c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1342,7 +1342,6 @@ def test_emulate_exclude_partitions(self): ret = psutil.disk_io_counters(perdisk=False, nowrap=False) self.assertIsNone(ret) - # def is_storage_device(name): return name == 'nvme0n1' @@ -1936,7 +1935,6 @@ def get_test_file(fname): break raise RuntimeError("timeout looking for test file") - # testfn = self.get_testfn() with open(testfn, "w"): self.assertEqual(get_test_file(testfn).mode, "w") @@ -1944,7 +1942,6 @@ def get_test_file(fname): 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+"): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 451c492005..35beb41e06 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -965,7 +965,7 @@ def test_cpu_affinity(self): 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 @@ -1344,7 +1344,7 @@ def assert_raises_nsp(fun, fun_name): @unittest.skipIf(not POSIX, 'POSIX only') def test_zombie_process(self): - parent, zombie = self.spawn_zombie() + _parent, zombie = self.spawn_zombie() self.assertProcessZombie(zombie) @unittest.skipIf(not POSIX, 'POSIX only') diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 152e378fde..6656c19ba0 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -344,7 +344,7 @@ def test_cpu_count_logical(self): 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() diff --git a/pyproject.toml b/pyproject.toml index 59c2b6a223..dd7e22603c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,10 +2,13 @@ target-version = ["py37"] line-length = 79 skip-string-normalization = true +# https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html preview = true +enable-unstable-feature = ["multiline_string_handling", "string_processing", "wrap_long_dict_values_in_parens"] [tool.ruff] # https://beta.ruff.rs/docs/settings/ +preview = true target-version = "py37" line-length = 79 @@ -38,9 +41,15 @@ ignore = [ "D", # pydocstyle "DTZ", # flake8-datetimez "ERA001", # Found commented-out code + "F841", # Local variable `parent` is assigned to but never used "FBT", # flake8-boolean-trap (makes zero sense) "FIX", # Line contains TODO / XXX / ..., consider resolving the issue "FLY", # flynt (PYTHON2.7 COMPAT) + "FURB101", # `open` and `read` should be replaced by `Path(src).read_text()` + "FURB113", # Use `x.extend(('a', 'b', 'c'))` instead of repeatedly calling `x.append()` + "FURB118", # [*] Use `operator.add` instead of defining a lambda + "FURB140", # [*] Use `itertools.starmap` instead of the generator + "FURB145", # [*] Prefer `copy` method over slicing (PYTHON2.7 COMPAT) "INP", # flake8-no-pep420 "N801", # Class name `async_chat` should use CapWords convention (ASYNCORE COMPAT) "N802", # Function name X should be lowercase. @@ -48,13 +57,23 @@ ignore = [ "N818", # Exception name `FooBar` should be named with an Error suffix "PERF", # Perflint "PGH004", # Use specific rule codes when using `noqa` + "PLC0415", # `import` should be at the top-level of a file + "PLC2701", # Private name import `x` from external module `y` + "PLR0904", # Too many public methods (x > y) "PLR0911", # Too many return statements (8 > 6) "PLR0912", # Too many branches (x > y) "PLR0913", # Too many arguments in function definition (x > y) - "PLR0915", # Too many statements (92 > 50) + "PLR0914", # Too many local variables (x/y) + "PLR0915", # Too many statements (x > y) + "PLR0917", # Too many positional arguments (x/y) + "PLR1702", # Too many nested blocks (x > y) + "PLR1704", # Redefining argument with the local name `type_` "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable "PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation + "PLR6201", # Use a `set` literal when testing for membership + "PLR6301", # Method `x` could be a function, class method, or static method "PLW0603", # Using the global statement to update `lineno` is discouraged + "PLW1514", # `open` in text mode without explicit `encoding` argument "PLW2901", # `for` loop variable `x` overwritten by assignment target "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 934b24a1c5..c75c4f61d6 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -12,6 +12,7 @@ /dev/sda6 345.9G 83.8G 244.5G 24% ext4 /home /dev/sda1 296.0M 43.1M 252.9M 14% vfat /boot/efi /dev/sda2 600.0M 312.4M 287.6M 52% fuseblk /media/Recovery + """ import os @@ -26,7 +27,7 @@ def main(): print(templ % ("Device", "Total", "Used", "Free", "Use ", "Type", "Mount")) for part in psutil.disk_partitions(all=False): if os.name == 'nt': - if 'cdrom' in part.opts or part.fstype == '': + if 'cdrom' in part.opts or not part.fstype: # skip cd-rom drives with no disk in it; they may raise # ENOENT, pop-up a Windows GUI error for a non-ready # partition or just hang. diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index 4b4246aaa5..e23472ba9e 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -111,7 +111,7 @@ def main(): print(" netmask : %s" % addr.netmask) if addr.ptp: print(" p2p : %s" % addr.ptp) - print("") + print() if __name__ == '__main__': diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index e8b0c54e0c..0e6490b395 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -65,7 +65,7 @@ def get_file_urls(): print_color("no artifacts found", 'red') sys.exit(1) else: - for url in sorted(urls, key=lambda x: os.path.basename(x)): + for url in sorted(urls, key=os.path.basename): yield url diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 9f89f635a9..68201d7fc2 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -6,6 +6,7 @@ """Prints release announce based on HISTORY.rst file content. See: https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode. + """ import os diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index adbaa89a0b..786644b5a1 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -181,7 +181,7 @@ def main(): print_timings() # --- process - print("") + print() print_header("PROCESS APIS") ignore = [ 'send_signal', diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 09169984d8..b453f15b92 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -141,7 +141,7 @@ def main(): downs = downloads() print("# Download stats") - print("") + print() s = "psutil download statistics of the last %s days (last update " % DAYS s += "*%s*).\n" % LAST_UPDATE s += "Generated via [pypistats.py](%s) script.\n" % GITHUB_SCRIPT_URL diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 43db68fd9f..b789aa80d8 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -138,7 +138,7 @@ def safe_rmtree(path): def onerror(fun, path, excinfo): exc = excinfo[1] if exc.errno != errno.ENOENT: - raise + raise # noqa: PLE0704 existed = os.path.isdir(path) shutil.rmtree(path, onerror=onerror) @@ -181,7 +181,7 @@ def safe_rmtree(path): def onerror(fun, path, excinfo): exc = excinfo[1] if exc.errno != errno.ENOENT: - raise + raise # noqa: PLE0704 existed = os.path.isdir(path) shutil.rmtree(path, onerror=onerror) diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 54e89b05e1..eec5cd51a0 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -32,6 +32,7 @@ 20513 giampao /opt/sublime_text/sublime_text 65.8M 73.0M 0B 87.9M 3976 giampao compiz 115.0M 117.0M 0B 130.9M 32486 giampao skype 145.1M 147.5M 0B 149.6M + """ from __future__ import print_function @@ -89,8 +90,8 @@ def main(): p.pid, p._info["username"][:7] if p._info["username"] else "", convert_bytes(p._uss), - convert_bytes(p._pss) if p._pss != "" else "", - convert_bytes(p._swap) if p._swap != "" else "", + convert_bytes(p._pss) if p._pss else "", + convert_bytes(p._swap) if p._swap else "", convert_bytes(p._rss), cmd, ) diff --git a/scripts/winservices.py b/scripts/winservices.py index 8d941d5324..216d0a6529 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -52,7 +52,7 @@ def main(): ) print(s) print("binpath: %s" % info['binpath']) - print("") + print() if __name__ == '__main__': From 87e08c8ef681c5c21d1cf06fd6cd6d44f4ac58e8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 Feb 2024 12:28:22 +0100 Subject: [PATCH 1077/1714] make install-pip: fix installation on python 2 --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 34279c90aa..1ab6601192 100644 --- a/Makefile +++ b/Makefile @@ -103,11 +103,13 @@ install-pip: ## Install pip (no-op if already installed). @$(PYTHON) -c \ "import sys, ssl, os, pkgutil, tempfile, atexit; \ sys.exit(0) if pkgutil.find_loader('pip') else None; \ - pyexc = 'from urllib.request import urlopen' if sys.version_info[0] == 3 else 'from urllib2 import urlopen'; \ + PY3 = sys.version_info[0] == 3; \ + pyexc = 'from urllib.request import urlopen' if PY3 else 'from urllib2 import urlopen'; \ exec(pyexc); \ ctx = ssl._create_unverified_context() if hasattr(ssl, '_create_unverified_context') else None; \ + url = 'https://bootstrap.pypa.io/pip/2.7/get-pip.py' if not PY3 else 'https://bootstrap.pypa.io/get-pip.py'; \ kw = dict(context=ctx) if ctx else {}; \ - req = urlopen('https://bootstrap.pypa.io/get-pip.py', **kw); \ + req = urlopen(url, **kw); \ data = req.read(); \ f = tempfile.NamedTemporaryFile(suffix='.py'); \ atexit.register(f.close); \ From db60971e76042cc1eb115be9d9cadb58b76a047f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 Feb 2024 12:56:07 +0100 Subject: [PATCH 1078/1714] use unicode literals u"" instead of u("") --- psutil/_compat.py | 8 +------- psutil/tests/__init__.py | 3 +-- psutil/tests/test_linux.py | 37 ++++++++++++++++++------------------ psutil/tests/test_unicode.py | 3 +-- pyproject.toml | 1 + 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/psutil/_compat.py b/psutil/_compat.py index 3db56c6019..6070c2a044 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -23,7 +23,7 @@ # builtins "long", "range", "super", "unicode", "basestring", # literals - "u", "b", + "b", # collections module "lru_cache", # shutil module @@ -47,9 +47,6 @@ basestring = str range = range - def u(s): - return s - def b(s): return s.encode("latin-1") @@ -59,9 +56,6 @@ def b(s): unicode = unicode basestring = basestring - def u(s): - return unicode(s, "unicode_escape") - def b(s): return s diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5d74c185e8..dd5a6c8568 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -56,7 +56,6 @@ 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 @@ -186,7 +185,7 @@ def macos_version(): TESTFN_PREFIX = '$psutil-%s-' % os.getpid() else: TESTFN_PREFIX = '@psutil-%s-' % os.getpid() -UNICODE_SUFFIX = u("-ƒőő") +UNICODE_SUFFIX = u"-ƒőő" # An invalid unicode string. if PY3: INVALID_UNICODE_SUFFIX = b"f\xc0\x80".decode('utf8', 'surrogateescape') diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 9fe399d52c..0583e619de 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -28,7 +28,6 @@ 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 @@ -1218,7 +1217,7 @@ def test_zfs_fs(self): 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")) + fake_file = io.StringIO(u"nodev\tzfs\n") with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m1: @@ -1654,7 +1653,7 @@ 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")) + return io.StringIO(u"charging") else: return orig_open(name, *args, **kwargs) @@ -1685,7 +1684,7 @@ 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")) + return io.StringIO(u"discharging") else: return orig_open(name, *args, **kwargs) @@ -1759,11 +1758,11 @@ class TestSensorsBatteryEmulated(PsutilTestCase): def test_it(self): def open_mock(name, *args, **kwargs): if name.endswith("/energy_now"): - return io.StringIO(u("60000000")) + return io.StringIO(u"60000000") elif name.endswith("/power_now"): - return io.StringIO(u("0")) + return io.StringIO(u"0") elif name.endswith("/energy_full"): - return io.StringIO(u("60000001")) + return io.StringIO(u"60000001") else: return orig_open(name, *args, **kwargs) @@ -1781,9 +1780,9 @@ class TestSensorsTemperatures(PsutilTestCase): def test_emulate_class_hwmon(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): - return io.StringIO(u("name")) + return io.StringIO(u"name") elif name.endswith('/temp1_label'): - return io.StringIO(u("label")) + return io.StringIO(u"label") elif name.endswith('/temp1_input'): return io.BytesIO(b"30000") elif name.endswith('/temp1_max'): @@ -1813,9 +1812,9 @@ def open_mock(name, *args, **kwargs): elif name.endswith('temp'): return io.BytesIO(b"30000") elif name.endswith('0_type'): - return io.StringIO(u("critical")) + return io.StringIO(u"critical") elif name.endswith('type'): - return io.StringIO(u("name")) + return io.StringIO(u"name") else: return orig_open(name, *args, **kwargs) @@ -1849,11 +1848,11 @@ class TestSensorsFans(PsutilTestCase): def test_emulate_data(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): - return io.StringIO(u("name")) + return io.StringIO(u"name") elif name.endswith('/fan1_label'): - return io.StringIO(u("label")) + return io.StringIO(u"label") elif name.endswith('/fan1_input'): - return io.StringIO(u("2000")) + return io.StringIO(u"2000") else: return orig_open(name, *args, **kwargs) @@ -2033,13 +2032,13 @@ def test_terminal_mocked(self): def test_cmdline_mocked(self): # see: https://github.com/giampaolo/psutil/issues/639 p = psutil.Process() - fake_file = io.StringIO(u('foo\x00bar\x00')) + 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')) + fake_file = io.StringIO(u'foo\x00bar\x00\x00') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: @@ -2049,13 +2048,13 @@ def test_cmdline_mocked(self): def test_cmdline_spaces_mocked(self): # see: https://github.com/giampaolo/psutil/issues/1179 p = psutil.Process() - fake_file = io.StringIO(u('foo bar ')) + 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 ')) + fake_file = io.StringIO(u'foo bar ') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: @@ -2066,7 +2065,7 @@ 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')) + fake_file = io.StringIO(u'foo\x20bar\x00') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index f4883bf9b2..d09003d270 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -85,7 +85,6 @@ 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 @@ -190,7 +189,7 @@ class TestFSAPIs(BaseUnicodeTest): 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('.') + here = '.' if isinstance(self.funky_name, str) else u'.' with warnings.catch_warnings(): warnings.simplefilter("ignore") return self.funky_name in os.listdir(here) diff --git a/pyproject.toml b/pyproject.toml index dd7e22603c..5821c1a03c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,7 @@ ignore = [ "UP009", # [*] UTF-8 encoding declaration is unnecessary (PYTHON2.7 COMPAT) "UP010", # [*] Unnecessary `__future__` import `print_function` for target Python version (PYTHON2.7 COMPAT) "UP024", # [*] Replace aliased errors with `OSError` (PYTHON2.7 COMPAT) + "UP025", # [*] Remove unicode literals from strings (PYTHON2.7 COMPAT) "UP028", # [*] Replace `yield` over `for` loop with `yield from` (PYTHON2.7 COMPAT) "UP031", # [*] Use format specifiers instead of percent format "UP032", # [*] Use f-string instead of `format` call (PYTHON2.7 COMPAT) From bb3d59a371b3e142d7bf2e98f20340ef603d70ce Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Fri, 9 Feb 2024 15:23:11 +0100 Subject: [PATCH 1079/1714] chore: update cibuildwheel on windows (#2370) close #2369 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38c164e7dc..8a26449810 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,7 +40,7 @@ jobs: python-version: 3.11 - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.16.5 env: CIBW_ARCHS: "${{ matrix.archs }}" CIBW_PRERELEASE_PYTHONS: True From 45c7fa4d74b624f5ff630684bfc3bab48bfcefe4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 18 Feb 2024 20:45:51 +0100 Subject: [PATCH 1080/1714] update style to latest black ver --- docs/conf.py | 20 ++++++++++-------- psutil/_common.py | 10 +++++---- psutil/_pslinux.py | 15 ++++++++------ psutil/_pssunos.py | 10 +++++---- psutil/tests/__init__.py | 12 ++++++----- psutil/tests/test_linux.py | 12 ++++++----- psutil/tests/test_process.py | 16 ++++++++------- psutil/tests/test_windows.py | 40 ++++++++++++++++++++---------------- scripts/top.py | 22 +++++++++++--------- setup.py | 20 ++++++++++-------- 10 files changed, 100 insertions(+), 77 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3ebc64178c..d94b93c655 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -344,15 +344,17 @@ def get_version(): # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) -texinfo_documents = [( - master_doc, - 'psutil', - 'psutil Documentation', - author, - 'psutil', - 'One line description of project.', - 'Miscellaneous', -)] +texinfo_documents = [ + ( + master_doc, + 'psutil', + 'psutil Documentation', + author, + 'psutil', + 'One line description of project.', + 'Miscellaneous', + ) +] # Documents to append as an appendix to all manuals. # diff --git a/psutil/_common.py b/psutil/_common.py index 6989feafda..7a53cc13b4 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -269,10 +269,12 @@ class BatteryTime(enum.IntEnum): } if AF_INET6 is not None: - conn_tmap.update({ - "tcp6": ([AF_INET6], [SOCK_STREAM]), - "udp6": ([AF_INET6], [SOCK_DGRAM]), - }) + conn_tmap.update( + { + "tcp6": ([AF_INET6], [SOCK_STREAM]), + "udp6": ([AF_INET6], [SOCK_DGRAM]), + } + ) if AF_UNIX is not None: conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])}) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 5d0deb4037..41ecf0c903 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1454,10 +1454,12 @@ def sensors_temperatures(): continue trip_paths = glob.glob(base + '/trip_point*') - trip_points = set([ - '_'.join(os.path.basename(p).split('_')[0:3]) - for p in trip_paths - ]) + 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: @@ -2100,7 +2102,7 @@ def get_blocks(lines, current_block): path ): path = path[:-10] - ls.append(( + item = ( decode(addr), decode(perms), path, @@ -2114,7 +2116,8 @@ def get_blocks(lines, current_block): data.get(b'Referenced:', 0), data.get(b'Anonymous:', 0), data.get(b'Swap:', 0), - )) + ) + ls.append(item) return ls @wrap_exceptions diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index dddbece1f3..3e72967a3b 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -682,10 +682,12 @@ 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') diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index dd5a6c8568..10f71cd393 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1286,11 +1286,13 @@ def print_sysinfo(): info['kernel'] = platform.uname()[2] # python - info['python'] = ', '.join([ - platform.python_implementation(), - platform.python_version(), - platform.python_compiler(), - ]) + 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__ diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0583e619de..d133cd06f1 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -120,7 +120,7 @@ def get_ipv4_broadcast(ifname): def get_ipv6_addresses(ifname): with open("/proc/net/if_inet6") as f: all_fields = [] - for line in f.readlines(): + for line in f: fields = line.split() if fields[-1] == ifname: all_fields.append(fields) @@ -1698,10 +1698,12 @@ 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', - )): + 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"???") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 35beb41e06..4e20f0e6b2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1436,13 +1436,15 @@ def clean_dict(d): 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() - ]) + return dict( + [ + ( + k.replace("\r", "").replace("\n", ""), + v.replace("\r", "").replace("\n", ""), + ) + for k, v in d.items() + ] + ) self.maxDiff = None p = psutil.Process() diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 5983af70a1..502d926381 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -869,25 +869,29 @@ def test_environ_64(self): @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_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", - ]) + 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) diff --git a/scripts/top.py b/scripts/top.py index c0687ae1fe..1caf9804db 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -82,16 +82,18 @@ def poll(interval): procs_status = {} for p in psutil.process_iter(): try: - p.dict = p.as_dict([ - 'username', - 'nice', - 'memory_info', - 'memory_percent', - 'cpu_percent', - 'cpu_times', - 'name', - 'status', - ]) + p.dict = p.as_dict( + [ + 'username', + 'nice', + 'memory_info', + 'memory_percent', + 'cpu_percent', + 'cpu_times', + 'name', + 'status', + ] + ) try: procs_status[p.dict['status']] += 1 except KeyError: diff --git a/setup.py b/setup.py index 7c59f56450..dac0a973a9 100755 --- a/setup.py +++ b/setup.py @@ -213,15 +213,17 @@ def get_winver(): raise RuntimeError(msg) macros.append(("PSUTIL_WINDOWS", 1)) - macros.extend([ - # be nice to mingw, see: - # http://www.mingw.org/wiki/Use_more_recent_defined_functions - ('_WIN32_WINNT', get_winver()), - ('_AVAIL_WINVER_', get_winver()), - ('_CRT_SECURE_NO_WARNINGS', None), - # see: https://github.com/giampaolo/psutil/issues/348 - ('PSAPI_VERSION', 1), - ]) + macros.extend( + [ + # be nice to mingw, see: + # http://www.mingw.org/wiki/Use_more_recent_defined_functions + ('_WIN32_WINNT', get_winver()), + ('_AVAIL_WINVER_', get_winver()), + ('_CRT_SECURE_NO_WARNINGS', None), + # see: https://github.com/giampaolo/psutil/issues/348 + ('PSAPI_VERSION', 1), + ] + ) ext = Extension( 'psutil._psutil_windows', From da0b7c6880165fe45de68ee819c68e304958498a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 20 Feb 2024 18:38:19 +0100 Subject: [PATCH 1081/1714] fix py2 failure --- psutil/tests/test_process_all.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index ee1a9b58d5..e59de3fb2f 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -28,6 +28,7 @@ from psutil import POSIX from psutil import WINDOWS from psutil._compat import PY3 +from psutil._compat import FileNotFoundError from psutil._compat import long from psutil._compat import unicode from psutil.tests import CI_TESTING From 4cf56e08c1bc883ec89758834b50954380759858 Mon Sep 17 00:00:00 2001 From: shadeyg56 <31134255+shadeyg56@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:55:35 -0600 Subject: [PATCH 1082/1714] Linux: skip offline cpu cores in cpu_freq (#2376) Signed-off-by: shadeyg56 --- CREDITS | 4 ++++ HISTORY.rst | 1 + psutil/_pslinux.py | 7 +++++++ 3 files changed, 12 insertions(+) diff --git a/CREDITS b/CREDITS index ca58471317..a3206bb9ab 100644 --- a/CREDITS +++ b/CREDITS @@ -821,3 +821,7 @@ I: 2222 N: Ryan Carsten Schmidt W: https://github.com/ryandesign I: 2361 + +N: Shade Gladden +W: https://github.com/shadeyg56 +I: 2376 \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst index 3877d437a8..5ffa6151b5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,7 @@ **Bug fixes** - 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) +- 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch by Shade Gladden) 5.9.8 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 41ecf0c903..cae515be86 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -779,6 +779,13 @@ def cpu_freq(): # https://github.com/giampaolo/psutil/issues/1071 curr = bcat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) if curr is None: + online_path = ( + "/sys/devices/system/cpu/cpu{}/online".format(i) + ) + # if cpu core is offline, set to all zeroes + if cat(online_path, fallback=None) == "0\n": + ret.append(_common.scpufreq(0.0, 0.0, 0.0)) + continue msg = "can't find current frequency file" raise NotImplementedError(msg) curr = int(curr) / 1000 From 3dc2fbd82d2f4c4b4f33e475007309243e4376c5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 26 Feb 2024 19:00:49 +0100 Subject: [PATCH 1083/1714] #2366 [Windows]: log debug message when using slower process APIs --- HISTORY.rst | 4 ++++ psutil/_pswindows.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 5ffa6151b5..ed42233742 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ 5.9.9 (IN DEVELOPMENT) ====================== +**Enhancements** + +- 2366_, [Windows]: log debug message when using slower process APIs. + **Bug fixes** - 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 6199e5748e..8db66f0a0b 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -864,6 +864,7 @@ def _get_raw_meminfo(self): if is_permission_err(err): # TODO: the C ext can probably be refactored in order # to get this from cext.proc_info() + debug("attempting memory_info() fallback (slower)") info = self._proc_info() return ( info[pinfo_map['num_page_faults']], @@ -991,6 +992,7 @@ def create_time(self): return created except OSError as err: if is_permission_err(err): + debug("attempting create_time() fallback (slower)") return self._proc_info()[pinfo_map['create_time']] raise @@ -1014,6 +1016,7 @@ def cpu_times(self): except OSError as err: if not is_permission_err(err): raise + debug("attempting cpu_times() fallback (slower)") info = self._proc_info() user = info[pinfo_map['user_time']] system = info[pinfo_map['kernel_time']] @@ -1100,6 +1103,7 @@ def io_counters(self): except OSError as err: if not is_permission_err(err): raise + debug("attempting io_counters() fallback (slower)") info = self._proc_info() ret = ( info[pinfo_map['io_rcount']], @@ -1159,6 +1163,7 @@ def num_handles(self): return cext.proc_num_handles(self.pid) except OSError as err: if is_permission_err(err): + debug("attempting num_handles() fallback (slower)") return self._proc_info()[pinfo_map['num_handles']] raise From 004146a3a45f1fb26df73e99207c2fa40b185a46 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 Feb 2024 00:40:40 +0100 Subject: [PATCH 1084/1714] add black opt to make lines more compact Signed-off-by: Giampaolo Rodola --- docs/conf.py | 20 ++++++++---------- psutil/_common.py | 10 ++++----- psutil/_pslinux.py | 10 ++++----- psutil/_pssunos.py | 10 ++++----- psutil/tests/__init__.py | 12 +++++------ psutil/tests/test_linux.py | 10 ++++----- psutil/tests/test_process.py | 16 +++++++-------- psutil/tests/test_windows.py | 40 ++++++++++++++++-------------------- pyproject.toml | 2 +- scripts/top.py | 22 +++++++++----------- setup.py | 20 ++++++++---------- 11 files changed, 75 insertions(+), 97 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d94b93c655..3ebc64178c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -344,17 +344,15 @@ def get_version(): # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - 'psutil', - 'psutil Documentation', - author, - 'psutil', - 'One line description of project.', - 'Miscellaneous', - ) -] +texinfo_documents = [( + master_doc, + 'psutil', + 'psutil Documentation', + author, + 'psutil', + 'One line description of project.', + 'Miscellaneous', +)] # Documents to append as an appendix to all manuals. # diff --git a/psutil/_common.py b/psutil/_common.py index 7a53cc13b4..6989feafda 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -269,12 +269,10 @@ class BatteryTime(enum.IntEnum): } if AF_INET6 is not None: - conn_tmap.update( - { - "tcp6": ([AF_INET6], [SOCK_STREAM]), - "udp6": ([AF_INET6], [SOCK_DGRAM]), - } - ) + conn_tmap.update({ + "tcp6": ([AF_INET6], [SOCK_STREAM]), + "udp6": ([AF_INET6], [SOCK_DGRAM]), + }) if AF_UNIX is not None: conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])}) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index cae515be86..2a59bfe13d 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1461,12 +1461,10 @@ def sensors_temperatures(): continue trip_paths = glob.glob(base + '/trip_point*') - trip_points = set( - [ - '_'.join(os.path.basename(p).split('_')[0:3]) - for p in trip_paths - ] - ) + 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: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 3e72967a3b..dddbece1f3 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -682,12 +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') diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 10f71cd393..dd5a6c8568 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1286,13 +1286,11 @@ def print_sysinfo(): info['kernel'] = platform.uname()[2] # python - info['python'] = ', '.join( - [ - platform.python_implementation(), - platform.python_version(), - platform.python_compiler(), - ] - ) + 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__ diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d133cd06f1..af27095a38 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1698,12 +1698,10 @@ 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', - ) - ): + 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"???") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 4e20f0e6b2..35beb41e06 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1436,15 +1436,13 @@ def clean_dict(d): 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() - ] - ) + return dict([ + ( + k.replace("\r", "").replace("\n", ""), + v.replace("\r", "").replace("\n", ""), + ) + for k, v in d.items() + ]) self.maxDiff = None p = psutil.Process() diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 502d926381..5983af70a1 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -869,29 +869,25 @@ def test_environ_64(self): @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_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", - ] - ) + 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) diff --git a/pyproject.toml b/pyproject.toml index 5821c1a03c..0625af1fd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ line-length = 79 skip-string-normalization = true # https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html preview = true -enable-unstable-feature = ["multiline_string_handling", "string_processing", "wrap_long_dict_values_in_parens"] +enable-unstable-feature = ["hug_parens_with_braces_and_square_brackets", "multiline_string_handling", "string_processing", "wrap_long_dict_values_in_parens"] [tool.ruff] # https://beta.ruff.rs/docs/settings/ diff --git a/scripts/top.py b/scripts/top.py index 1caf9804db..c0687ae1fe 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -82,18 +82,16 @@ def poll(interval): procs_status = {} for p in psutil.process_iter(): try: - p.dict = p.as_dict( - [ - 'username', - 'nice', - 'memory_info', - 'memory_percent', - 'cpu_percent', - 'cpu_times', - 'name', - 'status', - ] - ) + p.dict = p.as_dict([ + 'username', + 'nice', + 'memory_info', + 'memory_percent', + 'cpu_percent', + 'cpu_times', + 'name', + 'status', + ]) try: procs_status[p.dict['status']] += 1 except KeyError: diff --git a/setup.py b/setup.py index dac0a973a9..7c59f56450 100755 --- a/setup.py +++ b/setup.py @@ -213,17 +213,15 @@ def get_winver(): raise RuntimeError(msg) macros.append(("PSUTIL_WINDOWS", 1)) - macros.extend( - [ - # be nice to mingw, see: - # http://www.mingw.org/wiki/Use_more_recent_defined_functions - ('_WIN32_WINNT', get_winver()), - ('_AVAIL_WINVER_', get_winver()), - ('_CRT_SECURE_NO_WARNINGS', None), - # see: https://github.com/giampaolo/psutil/issues/348 - ('PSAPI_VERSION', 1), - ] - ) + macros.extend([ + # be nice to mingw, see: + # http://www.mingw.org/wiki/Use_more_recent_defined_functions + ('_WIN32_WINNT', get_winver()), + ('_AVAIL_WINVER_', get_winver()), + ('_CRT_SECURE_NO_WARNINGS', None), + # see: https://github.com/giampaolo/psutil/issues/348 + ('PSAPI_VERSION', 1), + ]) ext = Extension( 'psutil._psutil_windows', From 79cd04c83bd5094eb9b9912dff5b6c69ebcdd486 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 Feb 2024 21:14:05 +0100 Subject: [PATCH 1085/1714] enable ruff preview mode Signed-off-by: Giampaolo Rodola --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0625af1fd2..94266a7bfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,11 @@ enable-unstable-feature = ["hug_parens_with_braces_and_square_brackets", "multil [tool.ruff] # https://beta.ruff.rs/docs/settings/ -preview = true target-version = "py37" line-length = 79 [tool.ruff.lint] +preview = true select = [ # To get a list of all values: `python3 -m ruff linter`. "ALL", From bd033622d5645233f8623ec7991666f3d9ed8199 Mon Sep 17 00:00:00 2001 From: Anthony Ryan Date: Fri, 15 Mar 2024 13:45:14 -0400 Subject: [PATCH 1086/1714] Add pickle support to psutil Exceptions (#2380) Add __reduce__ method to exceptions commonly thrown, and start testing that exceptions can be pickled in test_serialization. Fixes #2272 Signed-off-by: Anthony Ryan --- CREDITS | 6 +++++- HISTORY.rst | 1 + psutil/_common.py | 12 ++++++++++++ psutil/tests/test_misc.py | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index a3206bb9ab..3520723d4e 100644 --- a/CREDITS +++ b/CREDITS @@ -824,4 +824,8 @@ I: 2361 N: Shade Gladden W: https://github.com/shadeyg56 -I: 2376 \ No newline at end of file +I: 2376 + +N: Anthony Ryan +W: https://github.com/anthonyryan1 +I: 2272 diff --git a/HISTORY.rst b/HISTORY.rst index ed42233742..1d82aefe77 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,7 @@ - 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) - 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch by Shade Gladden) +- 2272_, Add pickle support to psutil Exceptions 5.9.8 ===== diff --git a/psutil/_common.py b/psutil/_common.py index 6989feafda..f33f3e0351 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -331,6 +331,9 @@ def __init__(self, pid, name=None, msg=None): self.name = name self.msg = msg or "process no longer exists" + def __reduce__(self): + return (self.__class__, (self.pid, self.name, self.msg)) + class ZombieProcess(NoSuchProcess): """Exception raised when querying a zombie process. This is @@ -347,6 +350,9 @@ def __init__(self, pid, name=None, ppid=None, msg=None): self.ppid = ppid self.msg = msg or "PID still exists but it's a zombie" + def __reduce__(self): + return (self.__class__, (self.pid, self.name, self.ppid, self.msg)) + class AccessDenied(Error): """Exception raised when permission to perform an action is denied.""" @@ -359,6 +365,9 @@ def __init__(self, pid=None, name=None, msg=None): self.name = name self.msg = msg or "" + def __reduce__(self): + return (self.__class__, (self.pid, self.name, self.msg)) + class TimeoutExpired(Error): """Raised on Process.wait(timeout) if timeout expires and process @@ -374,6 +383,9 @@ def __init__(self, seconds, pid=None, name=None): self.name = name self.msg = "timeout after %s seconds" % seconds + def __reduce__(self): + return (self.__class__, (self.seconds, self.pid, self.name)) + # =================================================================== # --- utils diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 700c054f87..bfd1f35355 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -280,6 +280,45 @@ def check(ret): check(psutil.disk_usage(os.getcwd())) check(psutil.users()) + b = pickle.loads( + pickle.dumps( + psutil.NoSuchProcess( + pid=4567, name='name test', msg='msg test' + ) + ) + ) + self.assertEqual(b.pid, 4567) + self.assertEqual(b.name, 'name test') + self.assertEqual(b.msg, 'msg test') + + b = pickle.loads( + pickle.dumps( + psutil.ZombieProcess( + pid=4567, name='name test', ppid=42, msg='msg test' + ) + ) + ) + self.assertEqual(b.pid, 4567) + self.assertEqual(b.ppid, 42) + self.assertEqual(b.name, 'name test') + self.assertEqual(b.msg, 'msg test') + + b = pickle.loads( + pickle.dumps(psutil.AccessDenied(pid=123, name='name', msg='msg')) + ) + self.assertEqual(b.pid, 123) + self.assertEqual(b.name, 'name') + self.assertEqual(b.msg, 'msg') + + b = pickle.loads( + pickle.dumps( + psutil.TimeoutExpired(seconds=33, pid=4567, name='name') + ) + ) + self.assertEqual(b.seconds, 33) + self.assertEqual(b.pid, 4567) + self.assertEqual(b.name, 'name') + # # XXX: https://github.com/pypa/setuptools/pull/2896 # @unittest.skipIf(APPVEYOR, "temporarily disabled due to setuptools bug") # def test_setup_script(self): From bf9e1c7c76b1601db147a375714980cc02cfe15e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Mar 2024 18:46:12 +0100 Subject: [PATCH 1087/1714] always use unittest.SkipTest where needed --- psutil/tests/test_bsd.py | 4 ++-- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 4 ++-- psutil/tests/test_misc.py | 10 +++++----- psutil/tests/test_posix.py | 6 +++--- psutil/tests/test_process.py | 4 ++-- psutil/tests/test_system.py | 4 ++-- psutil/tests/test_unicode.py | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 7b502bcb80..a714632dc9 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -267,7 +267,7 @@ def test_cpu_frequency_against_sysctl(self): try: sysctl_result = int(sysctl(sensor)) except RuntimeError: - self.skipTest("frequencies not supported by kernel") + raise unittest.SkipTest("frequencies not supported by kernel") self.assertEqual(psutil.cpu_freq().current, sysctl_result) sensor = "dev.cpu.0.freq_levels" @@ -500,7 +500,7 @@ def test_sensors_temperatures_against_sysctl(self): try: sysctl_result = int(float(sysctl(sensor)[:-1])) except RuntimeError: - self.skipTest("temperatures not supported by kernel") + raise unittest.SkipTest("temperatures not supported by kernel") self.assertAlmostEqual( psutil.sensors_temperatures()["coretemp"][cpu].current, sysctl_result, diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 4736f5f1ad..b2bfe29d6f 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -239,7 +239,7 @@ def test_cpu_count(self): @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") + raise unittest.SkipTest("cpu_freq() returns None") self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int, long)) def test_disk_io_counters(self): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index af27095a38..818a6be2fd 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -278,7 +278,7 @@ def test_used(self): # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e if get_free_version_info() < (3, 3, 12): - raise self.skipTest("old free version") + raise unittest.SkipTest("old free version") cli_value = free_physmem().used psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( @@ -342,7 +342,7 @@ def test_used(self): # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e if get_free_version_info() < (3, 3, 12): - raise self.skipTest("old free version") + raise unittest.SkipTest("old free version") vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 700c054f87..69bce8aaf9 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -285,7 +285,7 @@ def check(ret): # 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") + # raise unittest.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__) @@ -850,7 +850,7 @@ def test_cache_clear(self): @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") + raise unittest.SkipTest("no disks or NICs available") psutil.disk_io_counters() psutil.net_io_counters() caches = wrap_numbers.cache_info() @@ -959,7 +959,7 @@ def test_pmap(self): def test_procsmem(self): if 'uss' not in psutil.Process().memory_full_info()._fields: - raise self.skipTest("not supported") + raise unittest.SkipTest("not supported") self.assert_stdout('procsmem.py') def test_killall(self): @@ -988,13 +988,13 @@ def test_cpu_distribution(self): @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") def test_temperatures(self): if not psutil.sensors_temperatures(): - self.skipTest("no temperatures") + raise unittest.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") + raise unittest.SkipTest("no fans") self.assert_stdout('fans.py') @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 53852cef8e..5203c2707a 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -342,7 +342,7 @@ def test_nic_names(self): def test_users(self): out = sh("who -u") if not out.strip(): - raise self.skipTest("no users on this system") + raise unittest.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] @@ -358,7 +358,7 @@ def test_users(self): def test_users_started(self): out = sh("who -u") if not out.strip(): - raise self.skipTest("no users on this system") + raise unittest.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) @@ -444,7 +444,7 @@ def df(device): 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 unittest.SkipTest("df returned EBUSY") raise line = out.split('\n')[1] fields = line.split() diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 35beb41e06..bfba9f2d08 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -757,7 +757,7 @@ def test_long_cmdline(self): try: self.assertEqual(p.cmdline(), cmdline) except psutil.ZombieProcess: - raise self.skipTest("OPENBSD: process turned into zombie") + raise unittest.SkipTest("OPENBSD: process turned into zombie") else: self.assertEqual(p.cmdline(), cmdline) @@ -1165,7 +1165,7 @@ def test_children_duplicates(self): # 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") + raise unittest.SkipTest("PID 0") p = psutil.Process(pid) try: c = p.children(recursive=True) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 6656c19ba0..ce6786a432 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -355,7 +355,7 @@ 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") + raise unittest.SkipTest("cpu_count_cores() is None") if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista self.assertIsNone(cores) else: @@ -579,7 +579,7 @@ def check_ls(ls): ls = psutil.cpu_freq(percpu=True) if FREEBSD and not ls: - raise self.skipTest("returns empty list on FreeBSD") + raise unittest.SkipTest("returns empty list on FreeBSD") assert ls, ls check_ls([psutil.cpu_freq(percpu=False)]) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index d09003d270..cb4bccf7fd 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -175,7 +175,7 @@ def setUpClass(cls): def setUp(self): super().setUp() if self.skip_tests: - raise self.skipTest("can't handle unicode str") + raise unittest.SkipTest("can't handle unicode str") @serialrun @@ -246,7 +246,7 @@ def test_proc_open_files(self): 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") + raise unittest.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) From bc18030af16c3d51b977b803fdd2ae5df31da321 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Mar 2024 19:19:20 +0100 Subject: [PATCH 1088/1714] refact serialization tests --- psutil/tests/test_misc.py | 68 +++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index d7ee038dcd..9835436400 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -19,7 +19,6 @@ import psutil import psutil.tests -from psutil import LINUX from psutil import POSIX from psutil import WINDOWS from psutil._common import bcat @@ -34,7 +33,6 @@ 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 @@ -47,8 +45,10 @@ from psutil.tests import SCRIPTS_DIR from psutil.tests import PsutilTestCase from psutil.tests import mock +from psutil.tests import process_namespace from psutil.tests import reload_module from psutil.tests import sh +from psutil.tests import system_namespace # =================================================================== @@ -259,53 +259,66 @@ def test_process_as_dict_no_new_names(self): def test_serialization(self): def check(ret): - if json is not None: - json.loads(json.dumps(ret)) + json.loads(json.dumps(ret)) + a = pickle.dumps(ret) b = pickle.loads(a) self.assertEqual(ret, b) + # --- process APIs + + proc = psutil.Process() 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()) + + ns = process_namespace(proc) + for fun, name in ns.iter(ns.getters, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + ret = fun() + except psutil.Error: + pass + else: + check(ret) + + # --- system APIs + + ns = system_namespace() + for fun, name in ns.iter(ns.getters): + with self.subTest(name=name): + try: + ret = fun() + except psutil.AccessDenied: + pass + else: + check(ret) + + # --- exception classes b = pickle.loads( pickle.dumps( - psutil.NoSuchProcess( - pid=4567, name='name test', msg='msg test' - ) + psutil.NoSuchProcess(pid=4567, name='name', msg='msg') ) ) + self.assertIsInstance(b, psutil.NoSuchProcess) self.assertEqual(b.pid, 4567) - self.assertEqual(b.name, 'name test') - self.assertEqual(b.msg, 'msg test') + self.assertEqual(b.name, 'name') + self.assertEqual(b.msg, 'msg') b = pickle.loads( pickle.dumps( - psutil.ZombieProcess( - pid=4567, name='name test', ppid=42, msg='msg test' - ) + psutil.ZombieProcess(pid=4567, name='name', ppid=42, msg='msg') ) ) + self.assertIsInstance(b, psutil.ZombieProcess) self.assertEqual(b.pid, 4567) self.assertEqual(b.ppid, 42) - self.assertEqual(b.name, 'name test') - self.assertEqual(b.msg, 'msg test') + self.assertEqual(b.name, 'name') + self.assertEqual(b.msg, 'msg') b = pickle.loads( pickle.dumps(psutil.AccessDenied(pid=123, name='name', msg='msg')) ) + self.assertIsInstance(b, psutil.AccessDenied) self.assertEqual(b.pid, 123) self.assertEqual(b.name, 'name') self.assertEqual(b.msg, 'msg') @@ -315,6 +328,7 @@ def check(ret): psutil.TimeoutExpired(seconds=33, pid=4567, name='name') ) ) + self.assertIsInstance(b, psutil.TimeoutExpired) self.assertEqual(b.seconds, 33) self.assertEqual(b.pid, 4567) self.assertEqual(b.name, 'name') From 058b4ccc82dbcae965fb5f1931051b710e813f96 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Mar 2024 19:32:09 +0100 Subject: [PATCH 1089/1714] fix win tests --- psutil/tests/test_misc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 9835436400..64fce79961 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -284,6 +284,8 @@ def check(ret): ns = system_namespace() for fun, name in ns.iter(ns.getters): + if name in {"win_service_iter", "win_service_get"}: + continue with self.subTest(name=name): try: ret = fun() From aab8d09d029420756f85df839b993e3f139dd2ac Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Mar 2024 19:55:55 +0100 Subject: [PATCH 1090/1714] Makefile: define a PYTHON_ENV_VARS var to use with the $PYTHON var. Also try to address #2374. --- Makefile | 70 +++++++++++++++++++-------------------- psutil/tests/test_misc.py | 2 ++ 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/Makefile b/Makefile index 1ab6601192..a1fdf6fd32 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ # Configurable. PYTHON = python3 +PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 ARGS = TSCRIPT = psutil/tests/runner.py @@ -47,7 +48,6 @@ BUILD_OPTS = `$(PYTHON) -c \ # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` -TEST_PREFIX = PSUTIL_SCRIPTS_DIR=`pwd`/scripts PYTHONWARNINGS=always PSUTIL_DEBUG=1 # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help @@ -87,17 +87,17 @@ build: ## Compile (in parallel) without installing. @# "build_ext -i" copies compiled *.so files in ./psutil directory in order @# to allow "import psutil" when using the interactive interpreter from @# within this directory. - PYTHONWARNINGS=all $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) - $(PYTHON) -c "import psutil" # make sure it actually worked + $(PYTHON_ENV_VARS) $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) + $(PYTHON_ENV_VARS) $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. ${MAKE} build - PYTHONWARNINGS=all $(PYTHON) setup.py develop $(INSTALL_OPTS) - $(PYTHON) -c "import psutil" # make sure it actually worked + $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(INSTALL_OPTS) + $(PYTHON_ENV_VARS) $(PYTHON) -c "import psutil" # make sure it actually worked uninstall: ## Uninstall this package via pip. - cd ..; $(PYTHON) -m pip uninstall -y -v psutil || true - $(PYTHON) scripts/internal/purge_installation.py + cd ..; $(PYTHON_ENV_VARS) $(PYTHON) -m pip uninstall -y -v psutil || true + $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/purge_installation.py install-pip: ## Install pip (no-op if already installed). @$(PYTHON) -c \ @@ -123,8 +123,8 @@ install-pip: ## Install pip (no-op if already installed). setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade $(PY_DEPS) + $(PYTHON_ENV_VARS) $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip + $(PYTHON_ENV_VARS) $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade $(PY_DEPS) # =================================================================== # Tests @@ -132,65 +132,65 @@ setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). test: ## Run all tests. To run a specific test do "make test ARGS=psutil.tests.test_system.TestDiskAPIs" ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) test-parallel: ## Run all tests in parallel. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --parallel + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) --parallel test-process: ## Run process-related API tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process.py test-process-all: ## Run tests which iterate over all process PIDs. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process_all.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process_all.py test-system: ## Run system-related API tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_system.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_system.py test-misc: ## Run miscellaneous tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_misc.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_misc.py test-testutils: ## Run test utils tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_testutils.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_testutils.py test-unicode: ## Test APIs dealing with strings. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_unicode.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_unicode.py test-contracts: ## APIs sanity tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_contracts.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_contracts.py test-connections: ## Test net_connections() and Process.connections(). ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_connections.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_connections.py test-posix: ## POSIX specific tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_posix.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_posix.py test-platform: ## Run specific platform tests only. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memleaks.py + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memleaks.py test-last-failed: ## Re-run tests which failed on last run ${MAKE} build - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) $(ARGS) --last-failed + $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) --last-failed test-coverage: ## Run test coverage. ${MAKE} build # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov - $(TEST_PREFIX) $(PYTHON) -m coverage run -m unittest -v + $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m unittest -v $(PYTHON) -m coverage report @echo "writing results to htmlcov/index.html" $(PYTHON) -m coverage html @@ -210,7 +210,7 @@ _pylint: ## Python pylint (not mandatory, just run it from time to time) @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} lint-c: ## Run C linter. - @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py + @git ls-files '*.c' '*.h' | xargs $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/clinter.py lint-rst: ## Run C linter. @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml @@ -261,20 +261,20 @@ install-git-hooks: ## Install GIT pre-commit hook. sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest - PYTHONWARNINGS=all $(PYTHON) setup.py sdist + $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist download-wheels-github: ## Download latest wheels hosted on github. - $(PYTHON) scripts/internal/download_wheels_github.py --tokenfile=~/.github.token + $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels_github.py --tokenfile=~/.github.token ${MAKE} print-dist download-wheels-appveyor: ## Download latest wheels hosted on appveyor. - $(PYTHON) scripts/internal/download_wheels_appveyor.py + $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels_appveyor.py ${MAKE} print-dist check-sdist: ## Check sanity of source distribution. - $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv - build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz - build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" + $(PYTHON_ENV_VARS) $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv + $(PYTHON_ENV_VARS) build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz + $(PYTHON_ENV_VARS) build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" $(PYTHON) -m twine check --strict dist/*.tar.gz check-wheels: ## Check sanity of wheels. @@ -335,11 +335,11 @@ print-timeline: ## Print releases' timeline. print-access-denied: ## Print AD exceptions ${MAKE} build - @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_access_denied.py + @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls ${MAKE} build - @$(TEST_PREFIX) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) + @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) print-downloads: ## Print PYPI download statistics $(PYTHON) scripts/internal/print_downloads.py @@ -356,11 +356,11 @@ grep-todos: ## Look for TODOs in the source files. bench-oneshot: ## Benchmarks for oneshot() ctx manager (see #799). ${MAKE} build - $(TEST_PREFIX) $(PYTHON) scripts/internal/bench_oneshot.py + $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot.py bench-oneshot-2: ## Same as above but using perf module (supposed to be more precise) ${MAKE} build - $(TEST_PREFIX) $(PYTHON) scripts/internal/bench_oneshot_2.py + $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot_2.py check-broken-links: ## Look for broken links in source files. git ls-files | xargs $(PYTHON) -Wa scripts/internal/check_broken_links.py diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 64fce79961..93204fa062 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -15,6 +15,7 @@ import pickle import socket import stat +import sys import unittest import psutil @@ -632,6 +633,7 @@ def test_debug(self): with redirect_stderr(StringIO()) as f: debug("hello") + sys.stderr.flush() msg = f.getvalue() assert msg.startswith("psutil-debug"), msg self.assertIn("hello", msg) From 03a0ecc04be165745cfd69ad20a9ebb99db261b5 Mon Sep 17 00:00:00 2001 From: Mayank Jha <44309707+maynk27@users.noreply.github.com> Date: Sat, 16 Mar 2024 00:47:30 +0530 Subject: [PATCH 1091/1714] Update to fix OSX older version build failure (#2379) * Update to fix OSX older version build failure Signed-off-by: Mayank Jha --- psutil/arch/osx/net.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index 24ce1b834e..d365676ce1 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -9,11 +9,11 @@ // https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c #include +#include +#include #include #include #include -#include -#include #include "../../_psutil_common.h" From b6d8c20b2db38c63587c17cfbe520061bdb01132 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Fri, 15 Mar 2024 20:21:32 +0100 Subject: [PATCH 1092/1714] chore: build macOS arm64 wheels on macos-14 (#2375) macos-14 runners are running on arm64 processors. This allows to test macOS arm64 wheels. Signed-off-by: mayeut --- .github/workflows/build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a26449810..2c2ac100d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ concurrency: jobs: # Linux + macOS + Windows Python 3 py3: - name: py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'windows') && matrix.archs || 'all' }} + name: py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'ubuntu') && 'all' || matrix.archs }} runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: @@ -28,7 +28,9 @@ jobs: - os: ubuntu-latest archs: "x86_64 i686" - os: macos-12 - archs: "x86_64 arm64" + archs: "x86_64" + - os: macos-14 + archs: "arm64" - os: windows-2019 archs: "AMD64" - os: windows-2019 @@ -40,7 +42,7 @@ jobs: python-version: 3.11 - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_ARCHS: "${{ matrix.archs }}" CIBW_PRERELEASE_PYTHONS: True From 1f14eeda74ebfd2abd267f85c61d7dafaad2ff5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Fri, 15 Mar 2024 20:24:55 +0100 Subject: [PATCH 1093/1714] Tests: Compare floats less strictly (#2372) We see: ====================================================================== FAIL: psutil.tests.test_system.TestCpuAPIs.test_cpu_times ---------------------------------------------------------------------- Traceback (most recent call last): File "/builddir/build/BUILD/psutil-release-5.9.5/psutil/tests/test_system.py", line 351, in test_cpu_times self.assertAlmostEqual(total, sum(times)) AssertionError: 885725913.3 != 885725913.3000001 within 7 places (1.1920928955078125e-07 difference) ---------------------------------------------------------------------- Or: ====================================================================== FAIL: psutil.tests.test_system.TestCpuAPIs.test_cpu_times ---------------------------------------------------------------------- Traceback (most recent call last): File "/builddir/build/BUILD/psutil-release-5.9.5/psutil/tests/test_system.py", line 351, in test_cpu_times self.assertAlmostEqual(total, sum(times)) AssertionError: 324284741.90999997 != 324284741.91 within 7 places (5.960464477539063e-08 difference) ---------------------------------------------------------------------- In CentOS Stream 10 builds on i686 and x86_64. --- psutil/tests/test_system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index ce6786a432..d517b7405c 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -385,7 +385,7 @@ def test_cpu_times(self): self.assertIsInstance(cp_time, float) self.assertGreaterEqual(cp_time, 0.0) total += cp_time - self.assertAlmostEqual(total, sum(times)) + self.assertAlmostEqual(total, sum(times), places=6) str(times) # CPU times are always supposed to increase over time # or at least remain the same and that's because time @@ -424,7 +424,7 @@ def test_per_cpu_times(self): self.assertIsInstance(cp_time, float) self.assertGreaterEqual(cp_time, 0.0) total += cp_time - self.assertAlmostEqual(total, sum(times)) + self.assertAlmostEqual(total, sum(times), places=6) str(times) self.assertEqual( len(psutil.cpu_times(percpu=True)[0]), From 4fda16d63befb7432b56ad096731ed62d3a6c2b4 Mon Sep 17 00:00:00 2001 From: Ryan Carsten Schmidt Date: Fri, 15 Mar 2024 14:29:17 -0500 Subject: [PATCH 1094/1714] Include CoreFoundation/CoreFoundation.h (#2364) * Include CoreFoundation/CoreFoundation.h This is correct, since this file does use CoreFoundation types, and it is necessary because on old macOS versions IOKit/ps/IOPowerSources.h, which is already included and which also uses CoreFoundation types, forgot to include CoreFoundation/CoreFoundation.h. Fixes #2362 --- psutil/arch/osx/sensors.c | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index a2faa157c4..28ee5f0b24 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -12,6 +12,7 @@ #include +#include #include #include From 8e40fe64897f7e10224c6da20ad5b9f8e00a2f08 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Mar 2024 20:35:53 +0100 Subject: [PATCH 1095/1714] update HISTORY / CREDITS --- CREDITS | 4 +++- HISTORY.rst | 7 +++++-- Makefile | 2 +- docs/index.rst | 1 + psutil/__init__.py | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CREDITS b/CREDITS index 3520723d4e..baff0c0899 100644 --- a/CREDITS +++ b/CREDITS @@ -44,6 +44,8 @@ Github usernames of people to CC on github when in need of help. - alxchk, Oleksii Shevchuk - AIX: - wiggin15, Arnon Yaari (maintainer) +- wheels / packaging / CI matrix: + - mayeut, Matthieu Darbois Top contributors ------------------------------------------------------------------------------- @@ -820,7 +822,7 @@ I: 2222 N: Ryan Carsten Schmidt W: https://github.com/ryandesign -I: 2361 +I: 2361, 2365 N: Shade Gladden W: https://github.com/shadeyg56 diff --git a/HISTORY.rst b/HISTORY.rst index 1d82aefe77..af4d56253a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,12 +6,15 @@ **Enhancements** - 2366_, [Windows]: log debug message when using slower process APIs. +- 2375_, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) **Bug fixes** -- 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) - 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch by Shade Gladden) -- 2272_, Add pickle support to psutil Exceptions +- 2272_: Add pickle support to psutil Exceptions. +- 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) +- 2362_, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) +- 2365_, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) 5.9.8 ===== diff --git a/Makefile b/Makefile index a1fdf6fd32..8b81764cf2 100644 --- a/Makefile +++ b/Makefile @@ -210,7 +210,7 @@ _pylint: ## Python pylint (not mandatory, just run it from time to time) @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} lint-c: ## Run C linter. - @git ls-files '*.c' '*.h' | xargs $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/clinter.py + @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py lint-rst: ## Run C linter. @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml diff --git a/docs/index.rst b/docs/index.rst index fc4cddc245..c5ebf59e11 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3048,6 +3048,7 @@ Timeline .. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py .. _`open`: https://docs.python.org/3/library/functions.html#open .. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count +.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg .. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid .. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority .. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid diff --git a/psutil/__init__.py b/psutil/__init__.py index 8138db41e1..91f3dd65c3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -213,7 +213,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.8" +__version__ = "5.9.9" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From f51f62beef5e2ce0d64c9cb76fb7b4e9094b1864 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Mar 2024 21:17:06 +0100 Subject: [PATCH 1096/1714] fix doc style Signed-off-by: Giampaolo Rodola --- docs/_static/css/custom.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index e88f530774..49da69dfb8 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -23,6 +23,11 @@ margin-bottom: 0px !important; } +.rst-content li { + list-style: outside; + margin-left: 15px; +} + .document td { padding-bottom: 0px !important; } @@ -536,3 +541,10 @@ div.body div.admonition, div.body div.impl-detail { .highlight .il { color: #208050 } + +.rst-content pre.literal-block, .rst-content div[class^='highlight'] { + border: 1px solid #e1e4e5; + padding: 0px; + overflow-x: auto; + margin: 1px 0 0px 0; +} From cfe897e2e7f2e15be97c338a9237a300ffc6e88e Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Fri, 29 Mar 2024 19:15:00 +0800 Subject: [PATCH 1097/1714] Fix typos again (#2388) Found via `codespell -L bacic,nd,hda,numer,fo,ser,pytho,syste,ue` and `typos --hidden --format brief` Signed-off-by: Kian-Meng Ang --- docs/DEVGUIDE.rst | 2 +- docs/index.rst | 2 +- psutil/__init__.py | 2 +- psutil/_pslinux.py | 2 +- psutil/_pssunos.py | 2 +- psutil/arch/osx/sensors.c | 2 +- psutil/arch/solaris/environ.c | 2 +- psutil/tests/test_system.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index a53235dab2..744ddadf4b 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -28,7 +28,7 @@ Once you have a compiler installed run: make test-memleaks make test-coverage make lint-all # Run Python and C linter - make fix-all # Fix linting erors + make fix-all # Fix linting errors make uninstall make help diff --git a/docs/index.rst b/docs/index.rst index c5ebf59e11..4959801f68 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1294,7 +1294,7 @@ Process class .. method:: cwd() The process current working directory as an absolute path. If cwd cannot be - determined for some internal reason (e.g. system process or directiory no + determined for some internal reason (e.g. system process or directory no longer exists) it may return an empty string. .. versionchanged:: 5.6.4 added support for NetBSD diff --git a/psutil/__init__.py b/psutil/__init__.py index 91f3dd65c3..71ae4533a9 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2037,7 +2037,7 @@ def swap_memory(): # ===================================================================== -# --- disks/paritions related functions +# --- disks/partitions related functions # ===================================================================== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 2a59bfe13d..7c1a4f9726 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -417,7 +417,7 @@ def calculate_avail_vmem(mems): def virtual_memory(): """Report virtual memory stats. - This implementation mimicks procps-ng-3.3.12, aka "free" CLI tool: + This implementation mimics procps-ng-3.3.12, aka "free" CLI tool: https://gitlab.com/procps-ng/procps/blob/ 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L778-791 The returned values are supposed to match both "free" and "vmstat -s" diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index dddbece1f3..20987ecc81 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -730,7 +730,7 @@ def toaddr(start, end): # readlink() even if it exists (ls shows it). # If that's the case we just return the # unresolved link path. - # This seems an incosistency with /proc similar + # This seems an inconsistency with /proc similar # to: http://goo.gl/55XgO name = '%s/%s/path/%s' % (procfs_path, self.pid, name) hit_enoent = True diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index 28ee5f0b24..53626c2dc4 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -60,7 +60,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { power_sources_information, CFSTR(kIOPSCurrentCapacityKey)); if (!CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) { PyErr_SetString(PyExc_RuntimeError, - "No battery capacity infomration in power sources info"); + "No battery capacity information in power sources info"); goto error; } diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/solaris/environ.c index 4b4e041a45..a2c7938556 100644 --- a/psutil/arch/solaris/environ.c +++ b/psutil/arch/solaris/environ.c @@ -240,7 +240,7 @@ ptr_size_by_psinfo(psinfo_t info) { /* * Count amount of pointers in a block which ends with NULL. - * @param fd a discriptor of /proc/PID/as special file. + * @param fd a descriptor of /proc/PID/as special file. * @param offt an offset of block of pointers at the file. * @param ptr_size a pointer size (allowed values: {4, 8}). * @return amount of non-NULL pointers or -1 in case of error. diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index d517b7405c..c1b88f5951 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -88,7 +88,7 @@ def test_process_iter(self): with self.assertRaises(psutil.AccessDenied): list(psutil.process_iter()) - def test_prcess_iter_w_attrs(self): + def test_process_iter_w_attrs(self): for p in psutil.process_iter(attrs=['pid']): self.assertEqual(list(p.info.keys()), ['pid']) with self.assertRaises(ValueError): From 034a1a6996d4ff5116fc45a9c5ed8477d0d73d37 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 5 Apr 2024 21:37:25 +0200 Subject: [PATCH 1098/1714] fix ruff errors --- Makefile | 4 ++-- psutil/_pswindows.py | 6 ++---- psutil/tests/test_linux.py | 4 +--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 8b81764cf2..17d4c7570e 100644 --- a/Makefile +++ b/Makefile @@ -230,10 +230,10 @@ lint-all: ## Run all linters # =================================================================== fix-black: - git ls-files '*.py' | xargs $(PYTHON) -m black + @git ls-files '*.py' | xargs $(PYTHON) -m black fix-ruff: - @git ls-files '*.py' | xargs $(PYTHON) -m ruff --no-cache --fix + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --fix fix-unittests: ## Fix unittest idioms. @git ls-files '*test_*.py' | xargs $(PYTHON) -m teyit --show-stats diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 8db66f0a0b..3c60a949a8 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -702,12 +702,10 @@ def is_permission_err(exc): # 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 ( + return 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): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 818a6be2fd..203b5d4871 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1360,9 +1360,7 @@ def is_storage_device(name): def test_emulate_use_sysfs(self): def exists(path): - if path == '/proc/diskstats': - return False - return True + return path == '/proc/diskstats' wprocfs = psutil.disk_io_counters(perdisk=True) with mock.patch( From 5bac1427631ee7c3a6ffb3c8071f3c11fef06524 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 6 Apr 2024 17:29:04 +0200 Subject: [PATCH 1099/1714] pid_exists() and Process() disagree on whether a pid exists when ERROR_ACCESS_DENIED (#2394) ## Summary * OS: Windows * Bug fix: yes * Type: core * Fixes: 2359 ## Description On Windows, `pid_exists()` may return True but `psutil.Process()` raises `NoSuchProcess`. Internally, this happens because of: https://github.com/giampaolo/psutil/blob/034a1a6996d4ff5116fc45a9c5ed8477d0d73d37/psutil/arch/windows/proc_utils.c#L176-L178. Differently from UNIX, the assumption in the code that ERROR_ACCESS_DENIED means there's a process to deny access to (hence it exists) is wrong. We therefore remove this assumption and also write a test case which ensures that `pid_exists()`, `Process()` and `pids()` APIs are all consistent with each other. As a bonus, I also discovered there are "hidden" PIDs on Windows (oh well!). --- .github/workflows/build.yml | 2 +- HISTORY.rst | 2 + Makefile | 2 +- psutil/arch/windows/proc_utils.c | 12 +++-- psutil/tests/__init__.py | 5 ++- psutil/tests/test_osx.py | 4 +- psutil/tests/test_process_all.py | 75 ++++++++++++++++++++++++++++++++ 7 files changed, 91 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c2ac100d9..5bd907fc65 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -108,7 +108,7 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install ruff black rstcheck toml-sort sphinx + python3 -m pip install ruff==0.3.4 black rstcheck toml-sort sphinx make lint-all # Check sanity of .tar.gz + wheel files diff --git a/HISTORY.rst b/HISTORY.rst index af4d56253a..0ae9a74e02 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,8 @@ - 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch by Shade Gladden) - 2272_: Add pickle support to psutil Exceptions. +- 2359_, [Windows], [CRITICAL]: `pid_exists()`_ disagrees with `Process`_ on + whether a pid exists when ERROR_ACCESS_DENIED. - 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) - 2362_, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) - 2365_, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) diff --git a/Makefile b/Makefile index 17d4c7570e..f09d83ec5b 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ PY3_DEPS = \ pypinfo \ requests \ rstcheck \ - ruff \ + ruff==0.3.4 \ setuptools \ sphinx_rtd_theme \ teyit \ diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index dac1129c0f..77b6dbf1e8 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -173,17 +173,15 @@ psutil_pid_is_running(DWORD pid) { hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - // Access denied means there's a process to deny access to. - if ((hProcess == NULL) && (GetLastError() == ERROR_ACCESS_DENIED)) - return 1; - - hProcess = psutil_check_phandle(hProcess, pid, 1); if (hProcess != NULL) { + hProcess = psutil_check_phandle(hProcess, pid, 1); + if (hProcess != NULL) { + CloseHandle(hProcess); + return 1; + } CloseHandle(hProcess); - return 1; } - CloseHandle(hProcess); PyErr_Clear(); return psutil_pid_in_pids(pid); } diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index dd5a6c8568..5e50e17870 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1551,7 +1551,10 @@ class system_namespace: ('virtual_memory', (), {}), ] if HAS_CPU_FREQ: - getters += [('cpu_freq', (), {'percpu': True})] + if MACOS and platform.machine() == 'arm64': # skipped due to #1892 + pass + else: + getters += [('cpu_freq', (), {'percpu': True})] if HAS_GETLOADAVG: getters += [('getloadavg', (), {})] if HAS_SENSORS_TEMPERATURES: diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 8fce8ae08e..1fe02d3e41 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -120,7 +120,9 @@ def test_cpu_count_cores(self): self.assertEqual(num, psutil.cpu_count(logical=False)) # TODO: remove this once 1892 is fixed - @unittest.skipIf(platform.machine() == 'arm64', "skipped due to #1892") + @unittest.skipIf( + MACOS and platform.machine() == 'arm64', "skipped due to #1892" + ) def test_cpu_freq(self): freq = psutil.cpu_freq() self.assertEqual( diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index e59de3fb2f..68b720a4b8 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -104,12 +104,14 @@ class TestFetchAllProcesses(PsutilTestCase): """ def setUp(self): + psutil._set_debug(False) # 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): + psutil._set_debug(True) if USE_PROC_POOL: self.pool.terminate() self.pool.join() @@ -459,6 +461,79 @@ def environ(self, ret, info): self.assertIsInstance(v, str) +class TestPidsRange(PsutilTestCase): + """Given pid_exists() return value for a range of PIDs which may or + may not exist, make sure that psutil.Process() and psutil.pids() + agree with pid_exists(). This guarantees that the 3 APIs are all + consistent with each other. See: + https://github.com/giampaolo/psutil/issues/2359 + + XXX - Note about Windows: it turns out there are some "hidden" PIDs + which are not returned by psutil.pids() and are also not revealed + by taskmgr.exe and ProcessHacker, still they can be instantiated by + psutil.Process() and queried. One of such PIDs is "conhost.exe". + Running as_dict() for it reveals that some Process() APIs + erroneously raise NoSuchProcess, so we know we have problem there. + Let's ignore this for now, since it's quite a corner case (who even + imagined hidden PIDs existed on Windows?). + """ + + def setUp(self): + psutil._set_debug(False) + + def tearDown(self): + psutil._set_debug(True) + + def test_it(self): + def is_linux_tid(pid): + try: + f = open("/proc/%s/status" % pid, "rb") + except FileNotFoundError: + return False + else: + with f: + for line in f: + if line.startswith(b"Tgid:"): + tgid = int(line.split()[1]) + # If tgid and pid are different then we're + # dealing with a process TID. + return tgid != pid + raise ValueError("'Tgid' line not found") + + def check(pid): + # In case of failure retry up to 3 times in order to avoid + # race conditions, especially when running in a CI + # environment where PIDs may appear and disappear at any + # time. + x = 3 + while True: + exists = psutil.pid_exists(pid) + try: + if exists: + psutil.Process(pid) + if not WINDOWS: # see docstring + self.assertIn(pid, psutil.pids()) + else: + with self.assertRaises(psutil.NoSuchProcess): + psutil.Process(pid) + if not WINDOWS: # see docstring + self.assertNotIn(pid, psutil.pids()) + except (psutil.Error, AssertionError) as err: + x -= 1 + if x == 0: + raise + else: + return + + for pid in range(1, 3000): + if LINUX and is_linux_tid(pid): + # On Linux a TID (thread ID) can be passed to the + # Process class and is querable like a PID (process + # ID). Skip it. + continue + check(pid) + + if __name__ == '__main__': from psutil.tests.runner import run_from_name From 841902c1c342121ee8d07d4b061c23de43de050a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 6 Apr 2024 21:51:42 +0200 Subject: [PATCH 1100/1714] OpenBSD: pid_exists() returns True for thread IDs (TIDs) (#2395) --- HISTORY.rst | 2 ++ psutil/_psbsd.py | 17 ++++++++++++++--- psutil/tests/test_process_all.py | 11 ++++++++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0ae9a74e02..89207a63a1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,8 @@ **Bug fixes** +- 2395_, [OpenBSD]: `pid_exists()`_ erroneously return True if the argument is + a thread ID (TID) instead of a PID (process ID). - 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch by Shade Gladden) - 2272_: Add pickle support to psutil Exceptions. - 2359_, [Windows], [CRITICAL]: `pid_exists()`_ disagrees with `Process`_ on diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index da68f5efd5..4e67a9d85a 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -561,10 +561,9 @@ def pids(): return ret -if OPENBSD or NETBSD: +if NETBSD: def pid_exists(pid): - """Return True if pid exists.""" exists = _psposix.pid_exists(pid) if not exists: # We do this because _psposix.pid_exists() lies in case of @@ -573,7 +572,19 @@ def pid_exists(pid): else: return True -else: +elif OPENBSD: + + def pid_exists(pid): + exists = _psposix.pid_exists(pid) + if not exists: + return False + else: + # OpenBSD seems to be the only BSD platform where + # _psposix.pid_exists() returns True for thread IDs (tids), + # so we can't use it. + return pid in pids() + +else: # FreeBSD pid_exists = _psposix.pid_exists diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 68b720a4b8..700ffe0782 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -514,8 +514,12 @@ def check(pid): if not WINDOWS: # see docstring self.assertIn(pid, psutil.pids()) else: - with self.assertRaises(psutil.NoSuchProcess): - psutil.Process(pid) + # On OpenBSD thread IDs can be instantiated, + # and oneshot() succeeds, but other APIs fail + # with EINVAL. + if not OPENBSD: + with self.assertRaises(psutil.NoSuchProcess): + psutil.Process(pid) if not WINDOWS: # see docstring self.assertNotIn(pid, psutil.pids()) except (psutil.Error, AssertionError) as err: @@ -531,7 +535,8 @@ def check(pid): # Process class and is querable like a PID (process # ID). Skip it. continue - check(pid) + with self.subTest(pid=pid): + check(pid) if __name__ == '__main__': From 5a3d56be329559e9aa06e44680da312986c1b9fa Mon Sep 17 00:00:00 2001 From: Andrea Blengino Date: Mon, 8 Apr 2024 19:36:03 +0200 Subject: [PATCH 1101/1714] Fix workflow visibility badges in README (#2399) Signed-off-by: Andrea Blengino In README file in .rst format, badges from Shields.io on GitHub workflows require a ".svg" in the path after the ".yml" --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0d14b99cd2..586a2ca4ed 100644 --- a/README.rst +++ b/README.rst @@ -18,11 +18,11 @@ :target: https://github.com/giampaolo/psutil/graphs/contributors :alt: Contributors -.. |github-actions-wheels| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml?label=Linux%2C%20macOS%2C%20Windows +.. |github-actions-wheels| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml.svg?label=Linux%2C%20macOS%2C%20Windows :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild :alt: Linux, macOS, Windows -.. |github-actions-bsd| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/bsd.yml?label=FreeBSD,%20NetBSD,%20OpenBSD +.. |github-actions-bsd| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/bsd.yml.svg?label=FreeBSD,%20NetBSD,%20OpenBSD :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests :alt: FreeBSD, NetBSD, OpenBSD From 7556e5d4bab656d4357204fb8772bdc9fe387e58 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 17 Apr 2024 17:01:29 +0200 Subject: [PATCH 1102/1714] Speedup process iter (don't check for PID reuse) (#2404) No longer make process_iter() check whether PID has been reused. This makes it around 20x times faster on Linux. Also changed Process.is_running() so that it will automatically remove the reused PID from process_iter() internal cache. In addition, also add a new process_iter.cache_clear() API. --- HISTORY.rst | 6 ++- docs/index.rst | 38 ++++++++----- psutil/__init__.py | 102 +++++++++++++++-------------------- psutil/tests/test_process.py | 11 ++++ psutil/tests/test_system.py | 50 ++++++++++++----- 5 files changed, 120 insertions(+), 87 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 89207a63a1..9813551273 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,12 +1,16 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -5.9.9 (IN DEVELOPMENT) +6.0.0 (IN DEVELOPMENT) ====================== **Enhancements** - 2366_, [Windows]: log debug message when using slower process APIs. - 2375_, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) +- 2396_: `process_iter()`_ no longer pre-emptively checks whether PIDs have + been reused. This makes `process_iter()`_ around 20x times faster. +- 2396_: a new ``psutil.process_iter.cache_clear()`` API can be used the clear + `process_iter()`_ internal cache. **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 4959801f68..428e431110 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -928,12 +928,12 @@ Functions Return an iterator yielding a :class:`Process` class instance for all running processes on the local machine. - This should be preferred over :func:`psutil.pids()` to iterate over processes - as it's safe from race condition. + This should be preferred over :func:`psutil.pids()` to iterate over + processes, as retrieving info is safe from race conditions. Every :class:`Process` instance is only created once, and then cached for the next time :func:`psutil.process_iter()` is called (if PID is still alive). - Also it makes sure process PIDs are not reused. + Cache can optionally be cleared via ``process_iter.clear_cache()``. *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. If *attrs* is specified :meth:`Process.as_dict()` result will be stored as a @@ -963,9 +963,19 @@ Functions 3: {'name': 'ksoftirqd/0', 'username': 'root'}, ...} + Clear internal cache:: + + >>> psutil.process_iter.cache_clear() + .. versionchanged:: 5.3.0 added "attrs" and "ad_value" parameters. + .. versionchanged:: + 6.0.0 no longer checks whether each yielded process PID has been reused. + + .. versionchanged:: + 6.0.0 added ``psutil.process_iter.cache_clear()`` API. + .. function:: pid_exists(pid) Check whether the given PID exists in the current process list. This is @@ -1071,11 +1081,12 @@ Process class .. note:: - the way this class is bound to a process is uniquely via its **PID**. + the way this class is bound to a process is via its **PID**. That means that if the process terminates and the OS reuses its PID you may - end up interacting with another process. - The only exceptions for which process identity is preemptively checked - (via PID + creation time) is for the following methods: + inadvertently end up querying another process. To prevent this problem + you can use :meth:`is_running()` first. + The only methods which preemptively check whether PID has been reused + (via PID + creation time) are: :meth:`nice` (set), :meth:`ionice` (set), :meth:`cpu_affinity` (set), @@ -1087,13 +1098,8 @@ Process class :meth:`suspend` :meth:`resume`, :meth:`send_signal`, - :meth:`terminate` + :meth:`terminate` and :meth:`kill`. - To prevent this problem for all other methods you can use - :meth:`is_running()` before querying the process or - :func:`process_iter()` in case you're iterating over all processes. - It must be noted though that unless you deal with very "old" (inactive) - :class:`Process` instances this will hardly represent a problem. .. method:: oneshot() @@ -1979,11 +1985,17 @@ Process class This is reliable also in case the process is gone and its PID reused by another process, therefore it must be preferred over doing ``psutil.pid_exists(p.pid)``. + If PID has been reused this method will also remove the process from + :func:`process_iter()` internal cache. .. note:: this will return ``True`` also if the process is a zombie (``p.status() == psutil.STATUS_ZOMBIE``). + .. versionchanged:: 6.0.0 : automatically remove process from + :func:`process_iter()` internal cache if PID has been reused by another + process. + .. method:: send_signal(signal) Send a signal to process (see `signal module`_ constants) preemptively diff --git a/psutil/__init__.py b/psutil/__init__.py index 71ae4533a9..934013819c 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -213,7 +213,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "5.9.9" +__version__ = "6.0.0" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -291,11 +291,9 @@ class Process(object): # noqa: UP004 If PID is omitted current process PID (os.getpid()) is used. Raise NoSuchProcess if PID does not exist. - Note that most of the methods of this class do not make sure - the PID of the process being queried has been reused over time. - That means you might end up retrieving an information referring - to another process in case the original one this instance - refers to is gone in the meantime. + Note that most of the methods of this class do not make sure that + the PID of the process being queried has been reused. That means + that you may end up retrieving information for another process. The only exceptions for which process identity is pre-emptively checked and guaranteed are: @@ -312,11 +310,8 @@ class Process(object): # noqa: UP004 - terminate() - kill() - To prevent this problem for all other methods you can: - - 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 + To prevent this problem for all other methods you can use + is_running() before querying the process. """ def __init__(self, pid=None): @@ -384,19 +379,24 @@ def __str__(self): 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._pid_reused: + info["status"] = "terminated + PID reused" + else: + 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) + return "%s.%s(%s)" % ( self.__class__.__module__, self.__class__.__name__, @@ -436,7 +436,7 @@ def __hash__(self): def _raise_if_pid_reused(self): """Raises NoSuchProcess in case process PID has been reused.""" - if not self.is_running() and self._pid_reused: + if self._pid_reused or (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 @@ -599,19 +599,24 @@ def parents(self): 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. + + It also checks if PID has been reused by another process, in + which case it will remove the process from `process_iter()` + internal cache and return False. """ if self._gone or self._pid_reused: return False try: # Checking if PID is alive is not enough as the PID might - # have been reused by another process: we also want to - # verify process identity. - # Process identity / uniqueness over time is guaranteed by - # (PID + creation time) and that is verified in __eq__. + # have been reused by another process. Process identity / + # uniqueness over time is guaranteed by (PID + creation + # time) and that is verified in __eq__. self._pid_reused = self != Process(self.pid) - return not self._pid_reused + if self._pid_reused: + # remove this PID from `process_iter()` internal cache + _pmap.pop(self.pid, None) + raise NoSuchProcess(self.pid) + return True except ZombieProcess: # We should never get here as it's already handled in # Process.__init__; here just for extra safety. @@ -1463,10 +1468,7 @@ def process_iter(attrs=None, ad_value=None): Every new Process instance is only created once and then cached into an internal table which is updated every time this is used. - - Cached Process instances are checked for identity so that you're - safe in case a PID has been reused by another process, in which - case the cached instance is updated. + Cache can optionally be cleared via `process_iter.clear_cache()`. The sorting order in which processes are yielded is based on their PIDs. @@ -1482,8 +1484,6 @@ def process_iter(attrs=None, ad_value=None): 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 return proc @@ -1502,38 +1502,20 @@ def remove(pid): for pid, proc in ls: 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: - yield add(pid) + proc = add(pid) + if attrs is not None: + proc.info = proc.as_dict(attrs=attrs, ad_value=ad_value) + yield proc 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 +process_iter.cache_clear = lambda: _pmap.clear() # noqa +process_iter.cache_clear.__doc__ = "Clear process_iter() internal cache." + + def wait_procs(procs, timeout=None, callback=None): """Convenience function which waits for a list of processes to terminate. diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index bfba9f2d08..9613eac181 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1375,13 +1375,24 @@ def test_reused_pid(self): subp = self.spawn_testproc() p = psutil.Process(subp.pid) p._ident = (p.pid, p.create_time() + 100) + + list(psutil.process_iter()) + self.assertIn(p.pid, psutil._pmap) assert not p.is_running() + # make sure is_running() removed PID from process_iter() + # internal cache + self.assertNotIn(p.pid, psutil._pmap) + 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.assertIn("terminated + PID reused", str(p)) + self.assertIn("terminated + PID reused", repr(p)) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.ppid) self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.parent) self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.parents) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index c1b88f5951..0c4affd0f3 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -61,8 +61,8 @@ # =================================================================== -class TestProcessAPIs(PsutilTestCase): - def test_process_iter(self): +class TestProcessIter(PsutilTestCase): + def test_pid_presence(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()]) @@ -71,24 +71,40 @@ def test_process_iter(self): p.wait() self.assertNotIn(sproc.pid, [x.pid for x in psutil.process_iter()]) - # assert there are no duplicates + def test_no_duplicates(self): 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_emulate_nsp(self): + list(psutil.process_iter()) # populate cache + for x in range(2): + with mock.patch( + 'psutil.Process.as_dict', + side_effect=psutil.NoSuchProcess(os.getpid()), + ): + self.assertEqual( + list(psutil.process_iter(attrs=["cpu_times"])), [] + ) + psutil.process_iter.cache_clear() # repeat test without cache - def test_process_iter_w_attrs(self): + def test_emulate_access_denied(self): + list(psutil.process_iter()) # populate cache + for x in range(2): + with mock.patch( + 'psutil.Process.as_dict', + side_effect=psutil.AccessDenied(os.getpid()), + ): + with self.assertRaises(psutil.AccessDenied): + list(psutil.process_iter(attrs=["cpu_times"])) + psutil.process_iter.cache_clear() # repeat test without cache + + def test_attrs(self): + for p in psutil.process_iter(attrs=['pid']): + self.assertEqual(list(p.info.keys()), ['pid']) + # yield again for p in psutil.process_iter(attrs=['pid']): self.assertEqual(list(p.info.keys()), ['pid']) with self.assertRaises(ValueError): @@ -113,6 +129,14 @@ def test_process_iter_w_attrs(self): self.assertGreaterEqual(p.info['pid'], 0) assert m.called + def test_cache_clear(self): + list(psutil.process_iter()) # populate cache + assert psutil._pmap + psutil.process_iter.cache_clear() + assert not psutil._pmap + + +class TestProcessAPIs(PsutilTestCase): @unittest.skipIf( PYPY and WINDOWS, "spawn_testproc() unreliable on PYPY + WINDOWS" ) From b6281c4674682eaa1d2e49517e6090c7f6361206 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 17 Apr 2024 17:52:40 +0200 Subject: [PATCH 1103/1714] Remove disk_partitions() `maxfile` and `maxpath` fields (#2405) --- HISTORY.rst | 14 ++++++++++++++ README.rst | 4 ++-- docs/DEVNOTES | 12 +++++------- docs/index.rst | 9 ++++----- psutil/__init__.py | 20 +------------------- psutil/_common.py | 3 +-- psutil/_psaix.py | 5 +---- psutil/_psbsd.py | 5 +---- psutil/_pslinux.py | 5 +---- psutil/_psosx.py | 5 +---- psutil/_pssunos.py | 5 +---- psutil/arch/windows/disk.c | 12 ++++-------- psutil/tests/test_contracts.py | 2 -- psutil/tests/test_system.py | 6 ------ 14 files changed, 36 insertions(+), 71 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9813551273..ca8e313a59 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,9 @@ **Enhancements** +- 2109_: ``maxfile`` and ``maxpath`` fields were removed from the namedtuple + returned by `disk_partitions()`_. Reason: on network filesystems (NFS) this + can potentially take a very long time to complete. - 2366_, [Windows]: log debug message when using slower process APIs. - 2375_, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) - 2396_: `process_iter()`_ no longer pre-emptively checks whether PIDs have @@ -24,6 +27,17 @@ - 2362_, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) - 2365_, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) +**Porting notes** + +Version 6.0.0 introduces some changes which affect backward compatibility: + +- 2109_: the namedtuple returned by `disk_partitions()`_' no longer has + ``maxfile`` and ``maxpath`` fields. +- 2396_: `process_iter()`_ no longer pre-emptively checks whether PIDs have + been reused. If you want to check for PID reusage you are supposed to use + `Process.is_running()`_ against the yielded `Process`_ instances. That will + also automatically remove reused PIDs from `process_iter()`_ internal cache. + 5.9.8 ===== diff --git a/README.rst b/README.rst index 586a2ca4ed..9df03860bf 100644 --- a/README.rst +++ b/README.rst @@ -228,8 +228,8 @@ Disks .. code-block:: python >>> psutil.disk_partitions() - [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid', maxfile=255, maxpath=4096), - sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext', opts='rw', maxfile=255, maxpath=4096)] + [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), + sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext', opts='rw')] >>> >>> psutil.disk_usage('/') sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) diff --git a/docs/DEVNOTES b/docs/DEVNOTES index fdd2bad180..49d449403a 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -113,13 +113,11 @@ REJECTED IDEAS INCONSISTENCIES =============== -- disk_partitions(all=False) should have been "perdisk=False" instead: - * disk_io_counters(perdisk=False) - * net_io_counters(pernic=False) - * cpu_times_percent(percpu=False) - * cpu_times(percpu=False) - * cpu_freq(percpu=False) -- PROCFS_PATH should have been set_procfs_path() +- `Process.connections()` should have been `Process.net_connections()` for + consistency with `psutil.net_connections()`. +- PROCFS_PATH should have been set_procfs_path(). +- `virtual_memory()` should have been `memory_virtual()`. +- `swap_memory()` should have been `memory_swap()`. RESOURCES ========= diff --git a/docs/index.rst b/docs/index.rst index 428e431110..eb2b113883 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -450,17 +450,16 @@ Disks on Windows). * **opts**: a comma-separated string indicating different mount options for the drive/partition. Platform-dependent. - * **maxfile**: the maximum length a file name can have. - * **maxpath**: the maximum length a path name (directory name + base file - name) can have. >>> import psutil >>> psutil.disk_partitions() - [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro', maxfile=255, maxpath=4096), - sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw', maxfile=255, maxpath=4096)] + [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), + sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] .. versionchanged:: 5.7.4 added *maxfile* and *maxpath* fields + .. versionchanged:: 6.0.0 removed *maxfile* and *maxpath* fields + .. function:: disk_usage(path) Return disk usage statistics about the partition which contains the given diff --git a/psutil/__init__.py b/psutil/__init__.py index 934013819c..f70ad56968 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2040,25 +2040,7 @@ def disk_partitions(all=False): If *all* parameter is False return physical devices only and ignore all others. """ - - 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 + return _psplatform.disk_partitions(all) def disk_io_counters(perdisk=False, nowrap=True): diff --git a/psutil/_common.py b/psutil/_common.py index f33f3e0351..50179fb3ca 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -189,8 +189,7 @@ class BatteryTime(enum.IntEnum): 'read_bytes', 'write_bytes', 'read_time', 'write_time']) # psutil.disk_partitions() -sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts', - 'maxfile', 'maxpath']) +sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts']) # psutil.net_io_counters() snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 7310ab6c3d..204c1bcf24 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -191,10 +191,7 @@ def disk_partitions(all=False): # filter by filesystem having a total size > 0. if not disk_usage(mountpoint).total: continue - maxfile = maxpath = None # set later - ntuple = _common.sdiskpart( - device, mountpoint, fstype, opts, maxfile, maxpath - ) + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 4e67a9d85a..094a59f881 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -395,10 +395,7 @@ def disk_partitions(all=False): partitions = cext.disk_partitions() for partition in partitions: device, mountpoint, fstype, opts = partition - maxfile = maxpath = None # set later - ntuple = _common.sdiskpart( - device, mountpoint, fstype, opts, maxfile, maxpath - ) + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7c1a4f9726..b0ca247771 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1363,10 +1363,7 @@ def disk_partitions(all=False): if not all: if not device or fstype not in fstypes: continue - maxfile = maxpath = None # set later - ntuple = _common.sdiskpart( - device, mountpoint, fstype, opts, maxfile, maxpath - ) + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 673ac0db75..037e1d2b5a 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -203,10 +203,7 @@ def disk_partitions(all=False): if not all: if not os.path.isabs(device) or not os.path.exists(device): continue - maxfile = maxpath = None # set later - ntuple = _common.sdiskpart( - device, mountpoint, fstype, opts, maxfile, maxpath - ) + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 20987ecc81..8bb89961f9 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -246,10 +246,7 @@ def disk_partitions(all=False): # https://github.com/giampaolo/psutil/issues/1674 debug("skipping %r: %s" % (mountpoint, err)) continue - maxfile = maxpath = None # set later - ntuple = _common.sdiskpart( - device, mountpoint, fstype, opts, maxfile, maxpath - ) + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 7cf0f20ccf..0041745617 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -318,13 +318,11 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strcat_s(mp_path, _countof(mp_path), mp_buf); py_tuple = Py_BuildValue( - "(ssssIi)", + "(ssss)", drive_letter, mp_path, fs_type, // typically "NTFS" - opts, - lpMaximumComponentLength, // max file length - MAX_PATH // max path length + opts ); if (!py_tuple || @@ -350,13 +348,11 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strcat_s(opts, _countof(opts), psutil_get_drive_type(type)); py_tuple = Py_BuildValue( - "(ssssIi)", + "(ssss)", drive_letter, drive_letter, fs_type, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS - opts, - lpMaximumComponentLength, // max file length - MAX_PATH // max path length + opts ); if (!py_tuple) goto error; diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index b2bfe29d6f..e768e7d523 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -255,8 +255,6 @@ def test_disk_partitions(self): 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): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 0c4affd0f3..32ac62cbea 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -667,12 +667,6 @@ def check_ntuple(nt): 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) From aefb59cdd14d1c33bf9b0b3d22887a756eb94022 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 18 Apr 2024 09:23:21 +0200 Subject: [PATCH 1104/1714] Rename `Process.connections()` to `Process.net_connections()` (#2408) --- HISTORY.rst | 7 +++ Makefile | 2 +- README.rst | 2 +- docs/DEVNOTES | 4 +- docs/index.rst | 31 ++++++++----- psutil/__init__.py | 8 +++- psutil/_common.py | 8 ++-- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 4 +- psutil/_pslinux.py | 14 +++--- psutil/_psosx.py | 6 +-- psutil/_pssunos.py | 2 +- psutil/_psutil_bsd.c | 2 +- psutil/_psutil_osx.c | 2 +- psutil/_pswindows.py | 2 +- psutil/arch/freebsd/proc_socks.c | 2 +- psutil/arch/freebsd/proc_socks.h | 2 +- psutil/arch/netbsd/proc.h | 2 +- psutil/arch/netbsd/socks.h | 2 +- psutil/arch/osx/proc.c | 2 +- psutil/arch/osx/proc.h | 2 +- psutil/tests/__init__.py | 17 ++++---- psutil/tests/test_connections.py | 74 ++++++++++++++++---------------- psutil/tests/test_linux.py | 4 +- psutil/tests/test_memleaks.py | 4 +- psutil/tests/test_process_all.py | 2 +- psutil/tests/test_testutils.py | 13 +++--- psutil/tests/test_unicode.py | 10 ++--- 28 files changed, 128 insertions(+), 104 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ca8e313a59..15333dbafc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,9 @@ been reused. This makes `process_iter()`_ around 20x times faster. - 2396_: a new ``psutil.process_iter.cache_clear()`` API can be used the clear `process_iter()`_ internal cache. +- 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. + The old name is still available, but it's deprecated (triggers a + ``DeprecationWarning``) and will be removed in the future. **Bug fixes** @@ -37,6 +40,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: been reused. If you want to check for PID reusage you are supposed to use `Process.is_running()`_ against the yielded `Process`_ instances. That will also automatically remove reused PIDs from `process_iter()`_ internal cache. +- 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. + The old name is still available, but it's deprecated (triggers a + ``DeprecationWarning``) and will be removed in the future. 5.9.8 ===== @@ -2644,6 +2650,7 @@ In most cases accessing the old names will work but it will cause a .. _`Process.memory_maps()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_maps .. _`Process.memory_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_percent .. _`Process.name()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.name +.. _`Process.net_connections()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.net_connections .. _`Process.nice()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.nice .. _`Process.num_ctx_switches()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_ctx_switches .. _`Process.num_fds()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_fds diff --git a/Makefile b/Makefile index f09d83ec5b..e123a93c0e 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ test-contracts: ## APIs sanity tests. ${MAKE} build $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_contracts.py -test-connections: ## Test net_connections() and Process.connections(). +test-connections: ## Test psutil.net_connections() and Process.net_connections(). ${MAKE} build $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_connections.py diff --git a/README.rst b/README.rst index 9df03860bf..3fc6e601b1 100644 --- a/README.rst +++ b/README.rst @@ -382,7 +382,7 @@ Process management [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768), popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] >>> - >>> p.connections(kind='tcp') + >>> p.net_connections(kind='tcp') [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')] >>> diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 49d449403a..38ae74f821 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -39,7 +39,7 @@ FEATURES Example: ``` df -a -> psutil.disk_partitions - lsof -> psutil.Process.open_files() and psutil.Process.open_connections() + lsof -> psutil.Process.open_files() and psutil.Process.net_connections() killall-> (actual script) tty -> psutil.Process.terminal() who -> psutil.users() @@ -113,8 +113,6 @@ REJECTED IDEAS INCONSISTENCIES =============== -- `Process.connections()` should have been `Process.net_connections()` for - consistency with `psutil.net_connections()`. - PROCFS_PATH should have been set_procfs_path(). - `virtual_memory()` should have been `memory_virtual()`. - `swap_memory()` should have been `memory_swap()`. diff --git a/docs/index.rst b/docs/index.rst index eb2b113883..201333af4b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -652,7 +652,7 @@ Network +----------------+-----------------------------------------------------+ On macOS and AIX this function requires root privileges. - To get per-process connections use :meth:`Process.connections`. + To get per-process connections use :meth:`Process.net_connections`. Also, see `netstat.py`_ example script. Example: @@ -1221,7 +1221,7 @@ Process class >>> import psutil >>> psutil.Process().environ() - {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} + {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} .. note:: on macOS Big Sur this function returns something meaningful only for the @@ -1251,7 +1251,7 @@ Process class If *attrs* is specified it must be a list of strings reflecting available :class:`Process` class's attribute names. Here's a list of possible string values: - ``'cmdline'``, ``'connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. + ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. If *attrs* argument is not passed all public read only attributes are assumed. *ad_value* is the value which gets assigned to a dict key in case @@ -1267,7 +1267,7 @@ Process class >>> >>> # get a list of valid attrs names >>> list(psutil.Process().as_dict().keys()) - ['status', 'cpu_num', 'num_ctx_switches', 'pid', 'memory_full_info', 'connections', 'cmdline', 'create_time', 'ionice', 'num_fds', 'memory_maps', 'cpu_percent', 'terminal', 'ppid', 'cwd', 'nice', 'username', 'cpu_times', 'io_counters', 'memory_info', 'threads', 'open_files', 'name', 'num_threads', 'exe', 'uids', 'gids', 'cpu_affinity', 'memory_percent', 'environ'] + ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_full_info', 'memory_info', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] .. versionchanged:: 3.0.0 *ad_value* is used also when incurring into @@ -1896,15 +1896,16 @@ Process class .. versionchanged:: 4.1.0 new *position*, *mode* and *flags* fields on Linux. - .. method:: connections(kind="inet") + .. method:: net_connections(kind="inet") Return socket connections opened by process as a list of named tuples. To get system-wide connections use :func:`psutil.net_connections()`. Every named tuple provides 6 attributes: - - **fd**: the socket file descriptor. This can be passed to `socket.fromfd`_ - to obtain a usable socket object. On Windows, FreeBSD and SunOS this is - always set to ``-1``. + - **fd**: the socket file descriptor. If the connection refers to the + current process this may be passed to `socket.fromfd`_ to obtain a usable + socket object. + On Windows, FreeBSD and SunOS this is always set to ``-1``. - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or @@ -1955,7 +1956,7 @@ Process class >>> p = psutil.Process(1694) >>> p.name() 'firefox' - >>> p.connections() + >>> p.net_connections() [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING'), pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status='ESTABLISHED'), @@ -1978,6 +1979,16 @@ Process class .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. + .. versionchanged:: 6.0.0 : method renamed from `connections` to + `net_connections`. + + .. method:: connections() + + Same as :meth:`net_connections` (deprecated). + + .. warning:: + deprecated in version 6.0.0; use :meth:`net_connections` instead. + .. method:: is_running() Return whether the current process is running in the current process list. @@ -2386,7 +2397,7 @@ Connections constants .. data:: CONN_BOUND (Solaris) A set of strings representing the status of a TCP connection. - Returned by :meth:`psutil.Process.connections()` and + Returned by :meth:`psutil.Process.net_connections()` and :func:`psutil.net_connections` (`status` field). Hardware constants diff --git a/psutil/__init__.py b/psutil/__init__.py index f70ad56968..ba90d097a1 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1198,7 +1198,7 @@ def open_files(self): """ return self._proc.open_files() - def connections(self, kind='inet'): + def net_connections(self, kind='inet'): """Return socket connections opened by process as a list of (fd, family, type, laddr, raddr, status) namedtuples. The *kind* parameter filters for connections that match the @@ -1220,7 +1220,11 @@ def connections(self, kind='inet'): | all | the sum of all the possible families and protocols | +------------+----------------------------------------------------+ """ - return self._proc.connections(kind) + return self._proc.net_connections(kind) + + @_common.deprecated_method(replacement="net_connections") + def connections(self, kind="inet"): + return self.net_connections(kind=kind) # --- signals diff --git a/psutil/_common.py b/psutil/_common.py index 50179fb3ca..c1ff18d1f0 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -120,7 +120,7 @@ STATUS_SUSPENDED = "suspended" # NetBSD STATUS_PARKED = "parked" # Linux -# Process.connections() and psutil.net_connections() +# Process.net_connections() and psutil.net_connections() CONN_ESTABLISHED = "ESTABLISHED" CONN_SYN_SENT = "SYN_SENT" CONN_SYN_RECV = "SYN_RECV" @@ -242,17 +242,17 @@ class BatteryTime(enum.IntEnum): pionice = namedtuple('pionice', ['ioclass', 'value']) # psutil.Process.ctx_switches() pctxsw = namedtuple('pctxsw', ['voluntary', 'involuntary']) -# psutil.Process.connections() +# psutil.Process.net_connections() pconn = namedtuple( 'pconn', ['fd', 'family', 'type', 'laddr', 'raddr', 'status'] ) -# psutil.connections() and psutil.Process.connections() +# psutil.net_connections() and psutil.Process.net_connections() addr = namedtuple('addr', ['ip', 'port']) # =================================================================== -# --- Process.connections() 'kind' parameter mapping +# --- Process.net_connections() 'kind' parameter mapping # =================================================================== diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 204c1bcf24..65ce3374f7 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -448,7 +448,7 @@ def threads(self): return retlist @wrap_exceptions - def connections(self, kind='inet'): + def net_connections(self, kind='inet'): ret = net_connections(kind, _pid=self.pid) # The underlying C implementation retrieves all OS connections # and filters them by PID. At this point we can't tell whether diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 094a59f881..b11b81c350 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -821,7 +821,7 @@ def threads(self): return retlist @wrap_exceptions - def connections(self, kind='inet'): + def net_connections(self, kind='inet'): if kind not in conn_tmap: raise ValueError( "invalid %r kind argument; choose between %s" @@ -835,7 +835,7 @@ def connections(self, kind='inet'): elif OPENBSD: rawlist = cext.net_connections(self.pid, families, types) else: - rawlist = cext.proc_connections(self.pid, families, types) + rawlist = cext.proc_net_connections(self.pid, families, types) for item in rawlist: fd, fam, type, laddr, raddr, status = item[:6] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b0ca247771..0ee1347998 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -815,7 +815,7 @@ class _Ipv6UnsupportedError(Exception): pass -class Connections: +class NetConnections: """A wrapper on top of /proc/net/* files, retrieving per-process and system-wide open connections (TCP, UDP, UNIX) similarly to "netstat -an". @@ -981,8 +981,8 @@ def process_inet(file, family, type_, inodes, filter_pid=None): else: status = _common.CONN_NONE try: - laddr = Connections.decode_address(laddr, family) - raddr = Connections.decode_address(raddr, family) + laddr = NetConnections.decode_address(laddr, family) + raddr = NetConnections.decode_address(raddr, family) except _Ipv6UnsupportedError: continue yield (fd, family, type_, laddr, raddr, status, pid) @@ -1059,12 +1059,12 @@ def retrieve(self, kind, pid=None): return list(ret) -_connections = Connections() +_net_connections = NetConnections() def net_connections(kind='inet'): """Return system-wide open connections.""" - return _connections.retrieve(kind) + return _net_connections.retrieve(kind) def net_io_counters(): @@ -2344,8 +2344,8 @@ def open_files(self): return retlist @wrap_exceptions - def connections(self, kind='inet'): - ret = _connections.retrieve(kind, self.pid) + def net_connections(self, kind='inet'): + ret = _net_connections.retrieve(kind, self.pid) self._raise_if_not_alive() return ret diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 037e1d2b5a..1067094679 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -246,7 +246,7 @@ def net_connections(kind='inet'): ret = [] for pid in pids(): try: - cons = Process(pid).connections(kind) + cons = Process(pid).net_connections(kind) except NoSuchProcess: continue else: @@ -501,14 +501,14 @@ def open_files(self): return files @wrap_exceptions - def connections(self, kind='inet'): + def net_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])) ) families, types = conn_tmap[kind] - rawlist = cext.proc_connections(self.pid, families, types) + rawlist = cext.proc_net_connections(self.pid, families, types) ret = [] for item in rawlist: fd, fam, type, laddr, raddr, status = item diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 8bb89961f9..6112728b15 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -666,7 +666,7 @@ def _get_unix_sockets(self, pid): yield (-1, socket.AF_UNIX, type, path, "", _common.CONN_NONE) @wrap_exceptions - def connections(self, kind='inet'): + def net_connections(self, kind='inet'): ret = net_connections(kind, _pid=self.pid) # The underlying C implementation retrieves all OS connections # and filters them by PID. At this point we can't tell whether diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index fde3916d31..6517d5800a 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -63,7 +63,7 @@ static PyMethodDef mod_methods[] = { {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS}, {"proc_threads", psutil_proc_threads, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) - {"proc_connections", psutil_proc_connections, METH_VARARGS}, + {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, #endif {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, #if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 369fbbfb48..4aa11d1700 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -23,7 +23,7 @@ static PyMethodDef mod_methods[] = { // --- per-process functions {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, - {"proc_connections", psutil_proc_connections, METH_VARARGS}, + {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, {"proc_environ", psutil_proc_environ, METH_VARARGS}, {"proc_exe", psutil_proc_exe, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 3c60a949a8..babb8e82e0 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -1059,7 +1059,7 @@ def open_files(self): return list(ret) @wrap_exceptions - def connections(self, kind='inet'): + def net_connections(self, kind='inet'): return net_connections(kind, _pid=self.pid) @wrap_exceptions diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 737467a8cb..c84c3b04a8 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -178,7 +178,7 @@ psutil_search_tcplist(char *buf, struct kinfo_file *kif) { PyObject * -psutil_proc_connections(PyObject *self, PyObject *args) { +psutil_proc_net_connections(PyObject *self, PyObject *args) { // Return connections opened by process. pid_t pid; int i; diff --git a/psutil/arch/freebsd/proc_socks.h b/psutil/arch/freebsd/proc_socks.h index a7996b1074..edc29b7aaf 100644 --- a/psutil/arch/freebsd/proc_socks.h +++ b/psutil/arch/freebsd/proc_socks.h @@ -6,4 +6,4 @@ #include -PyObject* psutil_proc_connections(PyObject* self, PyObject* args); +PyObject* psutil_proc_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/netbsd/proc.h b/psutil/arch/netbsd/proc.h index 8c51914d7e..138ff6ebd2 100644 --- a/psutil/arch/netbsd/proc.h +++ b/psutil/arch/netbsd/proc.h @@ -15,8 +15,8 @@ int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); char *psutil_get_cmd_args(pid_t pid, size_t *argsize); PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_connections(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); PyObject* psutil_proc_exe(PyObject* self, PyObject* args); diff --git a/psutil/arch/netbsd/socks.h b/psutil/arch/netbsd/socks.h index 9e6a97c0a8..9c2a87d4d7 100644 --- a/psutil/arch/netbsd/socks.h +++ b/psutil/arch/netbsd/socks.h @@ -6,5 +6,5 @@ * found in the LICENSE file. */ -PyObject *psutil_proc_connections(PyObject *, PyObject *); PyObject *psutil_net_connections(PyObject *, PyObject *); +PyObject *psutil_proc_net_connections(PyObject *, PyObject *); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 6f66c8613f..2cdb9911c0 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -857,7 +857,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { * - /usr/include/sys/proc_info.h */ PyObject * -psutil_proc_connections(PyObject *self, PyObject *args) { +psutil_proc_net_connections(PyObject *self, PyObject *args) { pid_t pid; int num_fds; int i; diff --git a/psutil/arch/osx/proc.h b/psutil/arch/osx/proc.h index 63f16ccdd2..f18f5f1fd6 100644 --- a/psutil/arch/osx/proc.h +++ b/psutil/arch/osx/proc.h @@ -8,13 +8,13 @@ PyObject *psutil_pids(PyObject *self, PyObject *args); PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_connections(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_exe(PyObject *self, PyObject *args); PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); PyObject *psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args); diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5e50e17870..b18b742396 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -86,7 +86,7 @@ "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", + "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", "MACOS_12PLUS", "COVERAGE", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', @@ -105,7 +105,7 @@ # sync primitives 'call_until', 'wait_for_pid', 'wait_for_file', # network - 'check_net_address', 'filter_proc_connections', + 'check_net_address', 'filter_proc_net_connections', 'get_free_port', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', 'unix_socketpair', 'create_sockets', # compat @@ -205,13 +205,13 @@ def macos_version(): # --- 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_GETLOADAVG = hasattr(psutil, "getloadavg") HAS_IONICE = hasattr(psutil.Process, "ionice") HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") +HAS_NET_CONNECTIONS_UNIX = POSIX and not SUNOS 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") @@ -1399,8 +1399,9 @@ class process_namespace: ignored = [ ('as_dict', (), {}), ('children', (), {'recursive': True}), + ('connections', (), {}), # deprecated ('is_running', (), {}), - ('memory_info_ex', (), {}), + ('memory_info_ex', (), {}), # deprecated ('oneshot', (), {}), ('parent', (), {}), ('parents', (), {}), @@ -1410,7 +1411,6 @@ class process_namespace: getters = [ ('cmdline', (), {}), - ('connections', (), {'kind': 'all'}), ('cpu_times', (), {}), ('create_time', (), {}), ('cwd', (), {}), @@ -1418,6 +1418,7 @@ class process_namespace: ('memory_full_info', (), {}), ('memory_info', (), {}), ('name', (), {}), + ('net_connections', (), {'kind': 'all'}), ('nice', (), {}), ('num_ctx_switches', (), {}), ('num_threads', (), {}), @@ -1758,7 +1759,7 @@ def create_sockets(): 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: + if POSIX and HAS_NET_CONNECTIONS_UNIX: fname1 = get_testfn() fname2 = get_testfn() s1, s2 = unix_socketpair(fname1) @@ -1883,7 +1884,7 @@ def check_status(conn): check_status(conn) -def filter_proc_connections(cons): +def filter_proc_net_connections(cons): """Our process may start with some open UNIX sockets which are not initialized by us, invalidating unit tests. """ diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index e261fc0f9d..de3ae59dfd 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -4,7 +4,7 @@ # 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.""" +"""Tests for psutil.net_connections() and Process.net_connections() APIs.""" import os import socket @@ -28,14 +28,14 @@ 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 HAS_NET_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 filter_proc_net_connections from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import serialrun @@ -48,21 +48,21 @@ SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) -def this_proc_connections(kind): - cons = psutil.Process().connections(kind=kind) +def this_proc_net_connections(kind): + cons = psutil.Process().net_connections(kind=kind) if kind in ("all", "unix"): - return filter_proc_connections(cons) + return filter_proc_net_connections(cons) return cons @serialrun class ConnectionTestCase(PsutilTestCase): def setUp(self): - self.assertEqual(this_proc_connections(kind='all'), []) + self.assertEqual(this_proc_net_connections(kind='all'), []) def tearDown(self): # Make sure we closed all resources. - self.assertEqual(this_proc_connections(kind='all'), []) + self.assertEqual(this_proc_net_connections(kind='all'), []) def compare_procsys_connections(self, pid, proc_cons, kind='all'): """Given a process PID and its list of connections compare @@ -94,11 +94,11 @@ def test_system(self): def test_process(self): with create_sockets(): - for conn in this_proc_connections(kind='all'): + for conn in this_proc_net_connections(kind='all'): check_connection_ntuple(conn) def test_invalid_kind(self): - self.assertRaises(ValueError, this_proc_connections, kind='???') + self.assertRaises(ValueError, this_proc_net_connections, kind='???') self.assertRaises(ValueError, psutil.net_connections, kind='???') @@ -107,7 +107,7 @@ 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') + cons = this_proc_net_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 @@ -146,8 +146,8 @@ def check_socket(self, sock): 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') + if sock.family == AF_UNIX and HAS_NET_CONNECTIONS_UNIX: + cons = this_proc_net_connections(kind='all') self.compare_procsys_connections(os.getpid(), cons, kind='all') return conn @@ -209,17 +209,17 @@ class TestConnectedSocket(ConnectionTestCase): @unittest.skipIf(SUNOS, "unreliable on SUONS") def test_tcp(self): addr = ("127.0.0.1", 0) - self.assertEqual(this_proc_connections(kind='tcp4'), []) + self.assertEqual(this_proc_net_connections(kind='tcp4'), []) server, client = tcp_socketpair(AF_INET, addr=addr) try: - cons = this_proc_connections(kind='tcp4') + cons = this_proc_net_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') + # cons = this_proc_net_connections(kind='all') # self.assertEqual(len(cons), 1) # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) finally: @@ -231,7 +231,7 @@ def test_unix(self): testfn = self.get_testfn() server, client = unix_socketpair(testfn) try: - cons = this_proc_connections(kind='unix') + cons = this_proc_net_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: @@ -257,7 +257,7 @@ def test_unix(self): class TestFilters(ConnectionTestCase): def test_filters(self): def check(kind, families, types): - for conn in this_proc_connections(kind=kind): + for conn in this_proc_net_connections(kind=kind): self.assertIn(conn.family, families) self.assertIn(conn.type, types) if not SKIP_SYSCONS: @@ -279,7 +279,7 @@ def check(kind, families, types): check('udp', [AF_INET, AF_INET6], [SOCK_DGRAM]) check('udp4', [AF_INET], [SOCK_DGRAM]) check('udp6', [AF_INET6], [SOCK_DGRAM]) - if HAS_CONNECTIONS_UNIX: + if HAS_NET_CONNECTIONS_UNIX: check( 'unix', [AF_UNIX], @@ -310,7 +310,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): self.assertEqual(conn.raddr, raddr) self.assertEqual(conn.status, status) for kind in all_kinds: - cons = proc.connections(kind=kind) + cons = proc.net_connections(kind=kind) if kind in kinds: self.assertNotEqual(cons, []) else: @@ -318,7 +318,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): # compare against system-wide connections # XXX Solaris can't retrieve system-wide UNIX # sockets. - if HAS_CONNECTIONS_UNIX: + if HAS_NET_CONNECTIONS_UNIX: self.compare_procsys_connections(proc.pid, [conn]) tcp_template = textwrap.dedent(""" @@ -373,7 +373,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): udp6_addr = None for p in psutil.Process().children(): - cons = p.connections() + cons = p.net_connections() self.assertEqual(len(cons), 1) for conn in cons: # TCP v4 @@ -428,56 +428,56 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): def test_count(self): with create_sockets(): # tcp - cons = this_proc_connections(kind='tcp') + cons = this_proc_net_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') + cons = this_proc_net_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') + cons = this_proc_net_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') + cons = this_proc_net_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') + cons = this_proc_net_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') + cons = this_proc_net_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') + cons = this_proc_net_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') + cons = this_proc_net_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') + if HAS_NET_CONNECTIONS_UNIX and not (FREEBSD or NETBSD): + cons = this_proc_net_connections(kind='unix') self.assertEqual(len(cons), 3) for conn in cons: self.assertEqual(conn.family, AF_UNIX) @@ -501,7 +501,7 @@ def check(cons, families, types_): for kind, groups in conn_tmap.items(): # XXX: SunOS does not retrieve UNIX sockets. - if kind == 'unix' and not HAS_CONNECTIONS_UNIX: + if kind == 'unix' and not HAS_NET_CONNECTIONS_UNIX: continue families, types_ = groups cons = psutil.net_connections(kind) @@ -511,8 +511,8 @@ def 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. + # sockets. For each process check that proc.net_connections() + # and psutil.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 @@ -547,11 +547,11 @@ def test_multi_sockets_procs(self): len([x for x in syscons if x.pid == pid]), expected ) p = psutil.Process(pid) - self.assertEqual(len(p.connections('all')), expected) + self.assertEqual(len(p.net_connections('all')), expected) class TestMisc(PsutilTestCase): - def test_connection_constants(self): + def test_net_connection_constants(self): ints = [] strs = [] for name in dir(psutil): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 203b5d4871..233584c84f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -2231,7 +2231,7 @@ def test_status_file_parsing(self): self.assertEqual(gids.saved, 1006) self.assertEqual(p._proc._get_eligible_cpus(), list(range(8))) - def test_connections_enametoolong(self): + def test_net_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 @@ -2241,7 +2241,7 @@ def test_connections_enametoolong(self): ) as m: p = psutil.Process() with mock.patch("psutil._pslinux.debug"): - self.assertEqual(p.connections(), []) + self.assertEqual(p.net_connections(), []) assert m.called diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 8a5b7f7f93..8232b18ad8 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -250,13 +250,13 @@ def test_rlimit_set(self): # Windows implementation is based on a single system-wide # function (tested later). @unittest.skipIf(WINDOWS, "worthless on WINDOWS") - def test_connections(self): + def test_net_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)) + self.execute(lambda: self.proc.net_connections(kind)) @unittest.skipIf(not HAS_ENVIRON, "not supported") def test_environ(self): diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 700ffe0782..7c6ce78086 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -356,7 +356,7 @@ def num_fds(self, ret, info): self.assertIsInstance(ret, int) self.assertGreaterEqual(ret, 0) - def connections(self, ret, info): + def net_connections(self, ret, info): with create_sockets(): self.assertEqual(len(ret), len(set(ret))) for conn in ret: diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index a93f9f09f6..17cc9eb085 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -26,7 +26,7 @@ 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 HAS_NET_CONNECTIONS_UNIX from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV from psutil.tests import PsutilTestCase @@ -36,7 +36,7 @@ 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 filter_proc_net_connections from psutil.tests import get_free_port from psutil.tests import is_namedtuple from psutil.tests import mock @@ -320,7 +320,7 @@ def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() self.assertEqual( - filter_proc_connections(p.connections(kind='unix')), [] + filter_proc_net_connections(p.net_connections(kind='unix')), [] ) name = self.get_testfn() server, client = unix_socketpair(name) @@ -329,7 +329,10 @@ def test_unix_socketpair(self): 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 + len( + filter_proc_net_connections(p.net_connections(kind='unix')) + ), + 2, ) self.assertEqual(server.getsockname(), name) self.assertEqual(client.getpeername(), name) @@ -348,7 +351,7 @@ def test_create_sockets(self): self.assertGreaterEqual(fams[socket.AF_INET], 2) if supports_ipv6(): self.assertGreaterEqual(fams[socket.AF_INET6], 2) - if POSIX and HAS_CONNECTIONS_UNIX: + if POSIX and HAS_NET_CONNECTIONS_UNIX: self.assertGreaterEqual(fams[socket.AF_UNIX], 2) self.assertGreaterEqual(types[socket.SOCK_STREAM], 2) self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index cb4bccf7fd..7fb2ef1990 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -37,12 +37,12 @@ ('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.net_connections('unix') * Process.open_files() * Process.username() (not tested) @@ -88,9 +88,9 @@ 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 HAS_NET_CONNECTIONS_UNIX from psutil.tests import INVALID_UNICODE_SUFFIX from psutil.tests import PYPY from psutil.tests import TESTFN_PREFIX @@ -253,7 +253,7 @@ def test_proc_open_files(self): ) @unittest.skipIf(not POSIX, "POSIX only") - def test_proc_connections(self): + def test_proc_net_connections(self): name = self.get_testfn(suffix=self.funky_suffix) try: sock = bind_unix_socket(name) @@ -263,12 +263,12 @@ def test_proc_connections(self): else: raise unittest.SkipTest("not supported") with closing(sock): - conn = psutil.Process().connections('unix')[0] + conn = psutil.Process().net_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") + @unittest.skipIf(not HAS_NET_CONNECTIONS_UNIX, "can't list UNIX sockets") @skip_on_access_denied() def test_net_connections(self): def find_sock(cons): From 20ed8f9b3d53847408e08442857704590d4f8b4c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 18 Apr 2024 11:23:09 +0200 Subject: [PATCH 1105/1714] #2408: ignore old "connections" name into as_dict() --- psutil/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index ba90d097a1..e1e2b7d5d0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1341,7 +1341,7 @@ def wait(self, timeout=None): [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'}]) + 'memory_info_ex', 'connections', 'oneshot'}]) # fmt: on From 8bfecf0459216f67b1ec089ced03dd162fca75de Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 18 Apr 2024 14:00:29 +0200 Subject: [PATCH 1106/1714] #2408: fix some tests which were still failing due to the old name Signed-off-by: Giampaolo Rodola --- psutil/tests/test_process.py | 6 +++--- scripts/procinfo.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 9613eac181..44a5e89017 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1207,9 +1207,9 @@ def test_as_dict(self): 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') + d = p.as_dict(attrs=['net_connections'], ad_value='foo') + if not isinstance(d['net_connections'], list): + self.assertEqual(d['net_connections'], 'foo') # Test ad_value is set on AccessDenied. with mock.patch( diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 95c99d442a..24004a9606 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -241,13 +241,13 @@ def run(pid, verbose=False): else: print_('open-files', '') - if pinfo['connections']: + if pinfo['net_connections']: template = '%-5s %-25s %-25s %s' print_( 'connections', template % ('PROTO', 'LOCAL ADDR', 'REMOTE ADDR', 'STATUS'), ) - for conn in pinfo['connections']: + for conn in pinfo['net_connections']: if conn.type == socket.SOCK_STREAM: type = 'TCP' elif conn.type == socket.SOCK_DGRAM: From 0b0ea8e55fb5eeb00074d25f9292a04cc70c82ef Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 18 Apr 2024 18:10:27 +0200 Subject: [PATCH 1107/1714] skip flaky test on netbsd --- psutil/tests/test_process.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 44a5e89017..307d3dfa96 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1468,6 +1468,7 @@ def clean_dict(d): MACOS_11PLUS, "macOS 11+ can't get another process environment, issue #2084", ) + @unittest.skipIf(NETBSD, "sometimes fails on `assert is_running()`") def test_weird_environ(self): # environment variables can contain values without an equals sign code = textwrap.dedent(""" From 3c518a384dda6dd11223499fc26b5bacf08bb8d5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 May 2024 19:35:05 +0200 Subject: [PATCH 1108/1714] fix ruff errs --- psutil/__init__.py | 4 ++-- pyproject.toml | 5 ++++- scripts/internal/print_announce.py | 10 +++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index e1e2b7d5d0..30f45987e1 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1601,14 +1601,14 @@ def check_gone(proc, timeout): check_gone(proc, timeout) else: check_gone(proc, max_timeout) - alive = alive - gone + alive = alive - gone # noqa PLR6104 if alive: # Last attempt over processes survived so far. # timeout == 0 won't make this function wait any further. for proc in alive: check_gone(proc, 0) - alive = alive - gone + alive = alive - gone # noqa: PLR6104 return (list(gone), list(alive)) diff --git a/pyproject.toml b/pyproject.toml index 94266a7bfc..6f5bab2da7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,10 +46,13 @@ ignore = [ "FIX", # Line contains TODO / XXX / ..., consider resolving the issue "FLY", # flynt (PYTHON2.7 COMPAT) "FURB101", # `open` and `read` should be replaced by `Path(src).read_text()` + "FURB103", # `open` and `write` should be replaced by `Path(...).write_text(...)` "FURB113", # Use `x.extend(('a', 'b', 'c'))` instead of repeatedly calling `x.append()` + "FURB116", # [*] Replace `hex` call with `f"{start:x}"` "FURB118", # [*] Use `operator.add` instead of defining a lambda "FURB140", # [*] Use `itertools.starmap` instead of the generator "FURB145", # [*] Prefer `copy` method over slicing (PYTHON2.7 COMPAT) + "FURB192", # [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence "INP", # flake8-no-pep420 "N801", # Class name `async_chat` should use CapWords convention (ASYNCORE COMPAT) "N802", # Function name X should be lowercase. @@ -108,7 +111,7 @@ ignore = [ "psutil/tests/*" = ["EM101", "TRY003"] "psutil/tests/runner.py" = ["T201", "T203"] "scripts/*" = ["T201", "T203"] -"scripts/internal/*" = ["T201", "T203"] +"scripts/internal/*" = ["EM101", "T201", "T203", "TRY003"] "setup.py" = ["T201", "T203"] [tool.ruff.lint.isort] diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 68201d7fc2..d1a7d297f1 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -86,13 +86,15 @@ def get_changes(): block = [] # eliminate the part preceding the first block - for line in lines: + while lines: line = lines.pop(0) if line.startswith('===='): break - lines.pop(0) + else: + raise ValueError("something wrong") - for line in lines: + lines.pop(0) + while lines: line = lines.pop(0) line = line.rstrip() if re.match(r"^- \d+_", line): @@ -101,6 +103,8 @@ def get_changes(): if line.startswith('===='): break block.append(line) + else: + raise ValueError("something wrong") # eliminate bottom empty lines block.pop(-1) From 1c7cb0aaf24f135e4ffded5a39387514f862c92e Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Sat, 18 May 2024 12:43:20 -0400 Subject: [PATCH 1109/1714] Don't build with limited API for 3.13 free-threaded build (#2402) The `--disable-gil` configuration of CPython 3.13 does not currently support the limited API. Signed-off-by: Sam Gross --- HISTORY.rst | 1 + setup.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 15333dbafc..a535905553 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,7 @@ been reused. This makes `process_iter()`_ around 20x times faster. - 2396_: a new ``psutil.process_iter.cache_clear()`` API can be used the clear `process_iter()`_ internal cache. +- 2401_, Support building with free-threaded CPython 3.13. - 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. The old name is still available, but it's deprecated (triggers a ``DeprecationWarning``) and will be removed in the future. diff --git a/setup.py b/setup.py index 7c59f56450..3c7900669d 100755 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ import struct import subprocess import sys +import sysconfig import tempfile import warnings @@ -65,6 +66,7 @@ PY37_PLUS = sys.version_info[:2] >= (3, 7) CP36_PLUS = PY36_PLUS and sys.implementation.name == "cpython" CP37_PLUS = PY37_PLUS and sys.implementation.name == "cpython" +Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED") macros = [] if POSIX: @@ -118,10 +120,10 @@ def get_version(): # Py_LIMITED_API lets us create a single wheel which works with multiple # python versions, including unreleased ones. -if bdist_wheel and CP36_PLUS and (MACOS or LINUX): +if bdist_wheel and CP36_PLUS and (MACOS or LINUX) and not Py_GIL_DISABLED: py_limited_api = {"py_limited_api": True} macros.append(('Py_LIMITED_API', '0x03060000')) -elif bdist_wheel and CP37_PLUS and WINDOWS: +elif bdist_wheel and CP37_PLUS and WINDOWS and not Py_GIL_DISABLED: # PyErr_SetFromWindowsErr / PyErr_SetFromWindowsErrWithFilename are # part of the stable API/ABI starting with CPython 3.7 py_limited_api = {"py_limited_api": True} From 553098524de5367c04a3e057b88208dc4b86aba4 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Sat, 18 May 2024 20:04:11 +0200 Subject: [PATCH 1110/1714] chore(ci): update actions (#2417) --- .github/workflows/build.yml | 46 ++++++++++++++++++++++++++---------- .github/workflows/issues.yml | 2 +- Makefile | 2 +- pyproject.toml | 6 ++++- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bd907fc65..c4e9c745ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,20 +37,30 @@ jobs: archs: "x86" steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + + # see https://cibuildwheel.pypa.io/en/stable/faq/#macos-building-cpython-38-wheels-on-arm64 + - name: "Install python 3.8 universal2 on macOS arm64" + if: runner.os == 'macOS' && runner.arch == 'ARM64' + uses: actions/setup-python@v5 + env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + with: + python-version: 3.8 + + - uses: actions/setup-python@v5 with: python-version: 3.11 - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.18.0 env: CIBW_ARCHS: "${{ matrix.archs }}" CIBW_PRERELEASE_PYTHONS: True - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels + name: wheels-py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'ubuntu') && 'all' || matrix.archs }} path: wheelhouse - name: Generate .tar.gz @@ -78,7 +88,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.9 @@ -86,9 +96,9 @@ jobs: uses: pypa/cibuildwheel@v1.12.0 - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels + name: wheels-py2-${{ matrix.os }} path: wheelhouse - name: Generate .tar.gz @@ -103,24 +113,36 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install ruff==0.3.4 black rstcheck toml-sort sphinx + python3 -m pip install ruff==0.4.4 black rstcheck toml-sort sphinx make lint-all + # upload weels as a single artefact + upload-wheels: + needs: [py2, py3] + runs-on: ubuntu-latest + steps: + - uses: actions/upload-artifact/merge@v4 + with: + name: wheels + pattern: wheels-* + separate-directories: false + delete-merged: true + # Check sanity of .tar.gz + wheel files check-dist: - needs: [py2, py3] + needs: [upload-wheels] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: wheels path: wheelhouse diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 3d3adbf837..a9f665eb6b 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -15,7 +15,7 @@ jobs: # install python - uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' diff --git a/Makefile b/Makefile index e123a93c0e..0406fb6dab 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ PY3_DEPS = \ pypinfo \ requests \ rstcheck \ - ruff==0.3.4 \ + ruff==0.4.4 \ setuptools \ sphinx_rtd_theme \ teyit \ diff --git a/pyproject.toml b/pyproject.toml index 6f5bab2da7..99a9b44d55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -205,7 +205,11 @@ spaces_indent_inline_array = 4 trailing_comma_inline_array = true [tool.cibuildwheel] -skip = ["*-musllinux*", "pp*"] +skip = [ + "*-musllinux*", + "cp313-win*", # pywin32 is not available on cp313 yet + "pp*", +] test-command = [ "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/runner.py", "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/test_memleaks.py", From 20be5ae95a4a01f734ba0692bc38c35df4e434d3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 3 Jun 2024 01:36:06 +0200 Subject: [PATCH 1111/1714] ruff: enable and fix 'unused variable' rule --- Makefile | 2 +- psutil/_common.py | 2 +- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 6 +++--- psutil/_pslinux.py | 22 +++++++++++----------- psutil/_psosx.py | 2 +- psutil/_pssunos.py | 2 +- psutil/_pswindows.py | 8 ++++---- psutil/tests/runner.py | 2 +- psutil/tests/test_bsd.py | 10 +++++----- psutil/tests/test_process.py | 4 ++-- psutil/tests/test_process_all.py | 2 +- psutil/tests/test_system.py | 2 +- psutil/tests/test_testutils.py | 2 +- psutil/tests/test_windows.py | 2 +- pyproject.toml | 1 - setup.py | 2 +- 17 files changed, 36 insertions(+), 37 deletions(-) diff --git a/Makefile b/Makefile index 0406fb6dab..60d82336e8 100644 --- a/Makefile +++ b/Makefile @@ -233,7 +233,7 @@ fix-black: @git ls-files '*.py' | xargs $(PYTHON) -m black fix-ruff: - @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --fix + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --fix $(ARGS) fix-unittests: ## Fix unittest idioms. @git ls-files '*test_*.py' | xargs $(PYTHON) -m teyit --show-stats diff --git a/psutil/_common.py b/psutil/_common.py index c1ff18d1f0..9fd7b0cfb8 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -980,7 +980,7 @@ def debug(msg): if PSUTIL_DEBUG: import inspect - fname, lineno, _, lines, index = inspect.getframeinfo( + fname, lineno, _, _lines, _index = inspect.getframeinfo( inspect.currentframe().f_back ) if isinstance(msg, Exception): diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 65ce3374f7..f48425eb8a 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -105,7 +105,7 @@ def virtual_memory(): - total, avail, free, pinned, inuse = cext.virtual_mem() + total, avail, free, _pinned, inuse = cext.virtual_mem() percent = usage_percent((total - avail), total, round_=1) return svmem(total, avail, percent, inuse, free) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index b11b81c350..4d49cf98bd 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -321,7 +321,7 @@ def cpu_stats(): if FREEBSD: # Note: the C ext is returning some metrics we are not exposing: # traps. - ctxsw, intrs, soft_intrs, syscalls, traps = cext.cpu_stats() + ctxsw, intrs, soft_intrs, syscalls, _traps = cext.cpu_stats() elif NETBSD: # XXX # Note about intrs: the C extension returns 0. intrs @@ -332,7 +332,7 @@ 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: @@ -342,7 +342,7 @@ 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) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 0ee1347998..1671838815 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1080,25 +1080,25 @@ def net_io_counters(): name = line[:colon].strip() fields = line[colon + 1 :].strip().split() - # in ( + # in bytes_recv, packets_recv, errin, dropin, - fifoin, # unused - framein, # unused - compressedin, # unused - multicastin, # unused + _fifoin, # unused + _framein, # unused + _compressedin, # unused + _multicastin, # unused # out bytes_sent, packets_sent, errout, dropout, - fifoout, # unused - collisionsout, # unused - carrierout, # unused - compressedout, + _fifoout, # unused + _collisionsout, # unused + _carrierout, # unused + _compressedout, # unused ) = map(int, fields) retdict[name] = ( @@ -2091,9 +2091,9 @@ def get_blocks(lines, current_block): for header, data in get_blocks(lines, current_block): hfields = header.split(None, 5) try: - addr, perms, offset, dev, inode, path = hfields + 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: diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 1067094679..41263fd735 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -165,7 +165,7 @@ def 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( diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 6112728b15..1c0b96e9e9 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -209,7 +209,7 @@ def cpu_count_cores(): def cpu_stats(): """Return various CPU stats as a named tuple.""" - ctx_switches, interrupts, syscalls, traps = cext.cpu_stats() + ctx_switches, interrupts, syscalls, _traps = cext.cpu_stats() soft_interrupts = 0 return _common.scpustats( ctx_switches, interrupts, soft_interrupts, syscalls diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index babb8e82e0..0ba511b901 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -238,7 +238,7 @@ def getpagesize(): def virtual_memory(): """System virtual memory as a namedtuple.""" mem = cext.virtual_mem() - totphys, availphys, totsys, availsys = mem + totphys, availphys, _totsys, _availsys = mem total = totphys avail = availphys free = availphys @@ -337,7 +337,7 @@ def cpu_count_cores(): def cpu_stats(): """Return CPU statistics.""" - ctx_switches, interrupts, dpcs, syscalls = cext.cpu_stats() + ctx_switches, interrupts, _dpcs, syscalls = cext.cpu_stats() soft_interrupts = 0 return _common.scpustats( ctx_switches, interrupts, soft_interrupts, syscalls @@ -986,7 +986,7 @@ def create_time(self): # Note: proc_times() not put under oneshot() 'cause create_time() # is already cached by the main Process class. try: - user, system, created = cext.proc_times(self.pid) + _user, _system, created = cext.proc_times(self.pid) return created except OSError as err: if is_permission_err(err): @@ -1010,7 +1010,7 @@ def threads(self): @wrap_exceptions def cpu_times(self): try: - user, system, created = cext.proc_times(self.pid) + user, system, _created = cext.proc_times(self.pid) except OSError as err: if not is_permission_err(err): raise diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index a054e4817e..3b28b64f1c 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -256,7 +256,7 @@ def run(self, suite): # 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) + _gone, alive = psutil.wait_procs(orphans, timeout=1) if alive: cprint("alive processes %s" % alive, "red") reap_children() diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index a714632dc9..8512b4f9ee 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -171,7 +171,7 @@ def test_memory_maps(self): while lines: line = lines.pop() fields = line.split() - _, start, stop, perms, res = fields[:5] + _, start, stop, _perms, res = fields[:5] map = maps.pop() self.assertEqual("%s-%s" % (start, stop), map.addr) self.assertEqual(int(res), map.rss) @@ -416,19 +416,19 @@ def test_cpu_stats_syscalls(self): # --- swap memory def test_swapmem_free(self): - total, used, free = self.parse_swapinfo() + _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() + _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() + total, _used, _free = self.parse_swapinfo() self.assertAlmostEqual( psutil.swap_memory().total, total, delta=TOLERANCE_SYS_MEM ) @@ -447,7 +447,7 @@ def test_boot_time(self): @unittest.skipIf(not HAS_BATTERY, "no battery") def test_sensors_battery(self): def secs2hours(secs): - m, s = divmod(secs, 60) + m, _s = divmod(secs, 60) h, m = divmod(m, 60) return "%d:%02d" % (h, m) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 307d3dfa96..0cae26d7e0 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -808,7 +808,7 @@ def test_prog_w_funky_name(self): @unittest.skipIf(not POSIX, 'POSIX only') def test_uids(self): p = psutil.Process() - real, effective, saved = p.uids() + real, effective, _saved = p.uids() # os.getuid() refers to "real" uid self.assertEqual(real, os.getuid()) # os.geteuid() refers to "effective" uid @@ -822,7 +822,7 @@ def test_uids(self): @unittest.skipIf(not POSIX, 'POSIX only') def test_gids(self): p = psutil.Process() - real, effective, saved = p.gids() + real, effective, _saved = p.gids() # os.getuid() refers to "real" uid self.assertEqual(real, os.getgid()) # os.geteuid() refers to "effective" uid diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 7c6ce78086..d1f476bb53 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -522,7 +522,7 @@ def check(pid): psutil.Process(pid) if not WINDOWS: # see docstring self.assertNotIn(pid, psutil.pids()) - except (psutil.Error, AssertionError) as err: + except (psutil.Error, AssertionError): x -= 1 if x == 0: raise diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 32ac62cbea..554fbffb28 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -845,7 +845,7 @@ def test_net_if_addrs(self): 0, socket.AI_PASSIVE, )[0] - af, socktype, proto, canonname, sa = info + af, socktype, proto, _canonname, sa = info s = socket.socket(af, socktype, proto) with contextlib.closing(s): s.bind(sa) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 17cc9eb085..1a18b65a85 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -240,7 +240,7 @@ def test_spawn_children_pair(self): @unittest.skipIf(not POSIX, "POSIX only") def test_spawn_zombie(self): - parent, zombie = self.spawn_zombie() + _parent, zombie = self.spawn_zombie() self.assertEqual(zombie.status(), psutil.STATUS_ZOMBIE) def test_terminate(self): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 5983af70a1..7778cf4a66 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -407,7 +407,7 @@ def test_special_pid(self): p.username() self.assertGreaterEqual(p.create_time(), 0.0) try: - rss, vms = p.memory_info()[:2] + 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'): diff --git a/pyproject.toml b/pyproject.toml index 99a9b44d55..b03392071f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ ignore = [ "D", # pydocstyle "DTZ", # flake8-datetimez "ERA001", # Found commented-out code - "F841", # Local variable `parent` is assigned to but never used "FBT", # flake8-boolean-trap (makes zero sense) "FIX", # Line contains TODO / XXX / ..., consider resolving the issue "FLY", # flynt (PYTHON2.7 COMPAT) diff --git a/setup.py b/setup.py index 3c7900669d..e3375004bb 100755 --- a/setup.py +++ b/setup.py @@ -431,7 +431,7 @@ def get_sunos_update(): class bdist_wheel_abi3(bdist_wheel): def get_tag(self): - python, abi, plat = bdist_wheel.get_tag(self) + python, _abi, plat = bdist_wheel.get_tag(self) return python, "abi3", plat cmdclass["bdist_wheel"] = bdist_wheel_abi3 From 4b1a05419df6101c43ce327ae97ea9154f8aa0f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 6 Jun 2024 21:30:17 +0200 Subject: [PATCH 1112/1714] Fix #2250 / NetBSD / cmdline: retry on EBUSY. (#2421) `Process.cmdline()` sometimes fail with EBUSY. It usually happens for long cmdlines with lots of arguments. In this case retry getting the cmdline for up to 50 times, and return an empty list as last resort. --- HISTORY.rst | 10 +++++++--- psutil/arch/netbsd/proc.c | 32 ++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a535905553..bc183170dd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,15 +21,19 @@ **Bug fixes** -- 2395_, [OpenBSD]: `pid_exists()`_ erroneously return True if the argument is - a thread ID (TID) instead of a PID (process ID). -- 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch by Shade Gladden) +- 2250_, [NetBSD]: `Process.cmdline()`_ sometimes fail with EBUSY. It usually + happens for long cmdlines with lots of arguments. In this case retry getting + the cmdline for up to 50 times, and return an empty list as last resort. +- 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch + by Shade Gladden) - 2272_: Add pickle support to psutil Exceptions. - 2359_, [Windows], [CRITICAL]: `pid_exists()`_ disagrees with `Process`_ on whether a pid exists when ERROR_ACCESS_DENIED. - 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) - 2362_, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) - 2365_, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) +- 2395_, [OpenBSD]: `pid_exists()`_ erroneously return True if the argument is + a thread ID (TID) instead of a PID (process ID). **Porting notes** diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index c645f301ef..4cd43c4c7f 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -332,6 +332,8 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; int mib[4]; int st; + int attempt; + int max_attempts = 50; size_t len = 0; size_t pos = 0; char *procargs = NULL; @@ -359,10 +361,32 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { PyErr_NoMemory(); goto error; } - st = sysctl(mib, __arraycount(mib), procargs, &len, NULL, 0); - if (st == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV)"); - goto error; + + while (1) { + st = sysctl(mib, __arraycount(mib), procargs, &len, NULL, 0); + if (st == -1) { + if (errno == EBUSY) { + // Usually happens with TestProcess.test_long_cmdline. See: + // https://github.com/giampaolo/psutil/issues/2250 + attempt += 1; + if (attempt < max_attempts) { + psutil_debug("proc %zu cmdline(): retry on EBUSY", pid); + continue; + } + else { + psutil_debug( + "proc %zu cmdline(): return [] due to EBUSY", pid + ); + free(procargs); + return py_retlist; + } + } + else { + PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV)"); + goto error; + } + } + break; } if (len > 0) { From 9421bf8e81994b511d39c70f3bf68ccf69cf6567 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 6 Jun 2024 22:03:05 +0200 Subject: [PATCH 1113/1714] openbsd: skip test if cmdline() returns [] due to EBUSY --- psutil/tests/test_process.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0cae26d7e0..9ddaf9cfe2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -758,9 +758,15 @@ def test_long_cmdline(self): self.assertEqual(p.cmdline(), cmdline) except psutil.ZombieProcess: raise unittest.SkipTest("OPENBSD: process turned into zombie") - else: + elif NETBSD: + ret = p.cmdline() + if ret == []: + # https://github.com/giampaolo/psutil/issues/2250 + raise unittest.SkipTest("OPENBSD: returned EBUSY") self.assertEqual(p.cmdline(), cmdline) + self.assertEqual(p.cmdline(), cmdline) + def test_name(self): p = self.spawn_psproc() name = p.name().lower() From 89b6096f2a026ec85f2a188920877371d4515b60 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 9 Jun 2024 23:06:40 +0200 Subject: [PATCH 1114/1714] process_iter(): use another global var to keep track of reused PIDs --- psutil/__init__.py | 9 +++++++-- psutil/tests/test_linux.py | 4 ++-- psutil/tests/test_misc.py | 1 + psutil/tests/test_process.py | 14 +++++++++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 30f45987e1..3a503503cc 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -85,6 +85,7 @@ from ._common import NoSuchProcess from ._common import TimeoutExpired from ._common import ZombieProcess +from ._common import debug from ._common import memoize_when_activated from ._common import wrap_numbers as _wrap_numbers from ._compat import PY3 as _PY3 @@ -613,8 +614,7 @@ def is_running(self): # time) and that is verified in __eq__. self._pid_reused = self != Process(self.pid) if self._pid_reused: - # remove this PID from `process_iter()` internal cache - _pmap.pop(self.pid, None) + _pids_reused.add(self.pid) raise NoSuchProcess(self.pid) return True except ZombieProcess: @@ -1464,6 +1464,7 @@ def pid_exists(pid): _pmap = {} +_pids_reused = set() def process_iter(attrs=None, ad_value=None): @@ -1501,6 +1502,10 @@ def remove(pid): gone_pids = b - a for pid in gone_pids: remove(pid) + while _pids_reused: + pid = _pids_reused.pop() + debug("refreshing Process instance for reused PID %s" % pid) + remove(pid) try: ls = sorted(list(pmap.items()) + list(dict.fromkeys(new_pids).items())) for pid, proc in ls: diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 233584c84f..be264ae1db 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1120,10 +1120,10 @@ def ifconfig(nic): except RuntimeError: continue self.assertAlmostEqual( - stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 5 + stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 10 ) self.assertAlmostEqual( - stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 5 + stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 10 ) self.assertAlmostEqual( stats.packets_recv, ifconfig_ret['packets_recv'], delta=1024 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 93204fa062..59416592e1 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -219,6 +219,7 @@ def test__all__(self): dir_psutil = dir(psutil) for name in dir_psutil: if name in ( + 'debug', 'long', 'tests', 'test', diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 9ddaf9cfe2..4f80277b76 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -36,6 +36,7 @@ from psutil._compat import PY3 from psutil._compat import FileNotFoundError from psutil._compat import long +from psutil._compat import redirect_stderr from psutil._compat import super from psutil.tests import APPVEYOR from psutil.tests import CI_TESTING @@ -763,7 +764,6 @@ def test_long_cmdline(self): if ret == []: # https://github.com/giampaolo/psutil/issues/2250 raise unittest.SkipTest("OPENBSD: returned EBUSY") - self.assertEqual(p.cmdline(), cmdline) self.assertEqual(p.cmdline(), cmdline) @@ -1378,6 +1378,11 @@ def test_zombie_process_status_w_exc(self): def test_reused_pid(self): # Emulate a case where PID has been reused by another process. + if PY3: + from io import StringIO + else: + from StringIO import StringIO + subp = self.spawn_testproc() p = psutil.Process(subp.pid) p._ident = (p.pid, p.create_time() + 100) @@ -1385,8 +1390,15 @@ def test_reused_pid(self): list(psutil.process_iter()) self.assertIn(p.pid, psutil._pmap) assert not p.is_running() + # make sure is_running() removed PID from process_iter() # internal cache + with redirect_stderr(StringIO()) as f: + list(psutil.process_iter()) + self.assertIn( + "refreshing Process instance for reused PID %s" % p.pid, + f.getvalue(), + ) self.assertNotIn(p.pid, psutil._pmap) assert p != psutil.Process(subp.pid) From 5f80c123d2497af404639669049213a48531e2fe Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 11 Jun 2024 23:51:52 +0200 Subject: [PATCH 1115/1714] Fix #2412, [macOS]: can't compile on macOS 10.4 PowerPC due to missing `MNT_` constants. --- HISTORY.rst | 2 ++ psutil/arch/osx/disk.c | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index bc183170dd..ae664900f6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -34,6 +34,8 @@ - 2365_, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) - 2395_, [OpenBSD]: `pid_exists()`_ erroneously return True if the argument is a thread ID (TID) instead of a PID (process ID). +- 2412_, [macOS]: can't compile on macOS 10.4 PowerPC due to missing `MNT_` + constants. **Porting notes** diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index 961fc42a48..d02cf794d5 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -86,8 +86,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",async", sizeof(opts)); if (flags & MNT_EXPORTED) strlcat(opts, ",exported", sizeof(opts)); - if (flags & MNT_QUARANTINE) - strlcat(opts, ",quarantine", sizeof(opts)); if (flags & MNT_LOCAL) strlcat(opts, ",local", sizeof(opts)); if (flags & MNT_QUOTA) @@ -108,10 +106,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",nouserxattr", sizeof(opts)); if (flags & MNT_DEFWRITE) strlcat(opts, ",defwrite", sizeof(opts)); - if (flags & MNT_MULTILABEL) - strlcat(opts, ",multilabel", sizeof(opts)); - if (flags & MNT_NOATIME) - strlcat(opts, ",noatime", sizeof(opts)); if (flags & MNT_UPDATE) strlcat(opts, ",update", sizeof(opts)); if (flags & MNT_RELOAD) @@ -120,7 +114,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",force", sizeof(opts)); if (flags & MNT_CMDFLAGS) strlcat(opts, ",cmdflags", sizeof(opts)); - + // requires macOS >= 10.5 +#ifdef MNT_QUARANTINE + if (flags & MNT_QUARANTINE) + strlcat(opts, ",quarantine", sizeof(opts)); +#endif +#ifdef MNT_MULTILABEL + if (flags & MNT_MULTILABEL) + strlcat(opts, ",multilabel", sizeof(opts)); +#endif +#ifdef MNT_NOATIME + if (flags & MNT_NOATIME) + strlcat(opts, ",noatime", sizeof(opts)); +#endif py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); if (! py_dev) goto error; From 1d092e728abddb628738d2f0a110b74152969771 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Jun 2024 00:15:21 +0200 Subject: [PATCH 1116/1714] test subprocesses: sleep() with an interval of 0.1 to make the test process more 'killable' --- psutil/tests/__init__.py | 2 +- psutil/tests/test_connections.py | 6 ++-- psutil/tests/test_process.py | 52 ++++++++++++++++++++++++++------ psutil/tests/test_testutils.py | 6 +++- psutil/tests/test_unicode.py | 18 +++++++++-- 5 files changed, 67 insertions(+), 17 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b18b742396..684c68ae2a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -408,7 +408,7 @@ def spawn_children_pair(): s += "f = open('%s', 'w');" s += "f.write(str(os.getpid()));" s += "f.close();" - s += "time.sleep(60);" + s += "[time.sleep(0.1) for x in range(100 * 6)];" p = subprocess.Popen([r'%s', '-c', s]) p.wait() """ % (os.path.basename(testfn), PYTHON_EXE)) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index de3ae59dfd..4a0674d620 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -328,7 +328,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): s.listen(5) with open('{testfn}', 'w') as f: f.write(str(s.getsockname()[:2])) - time.sleep(60) + [time.sleep(0.1) for x in range(100)] """) udp_template = textwrap.dedent(""" @@ -337,7 +337,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): s.bind(('{addr}', 0)) with open('{testfn}', 'w') as f: f.write(str(s.getsockname()[:2])) - time.sleep(60) + [time.sleep(0.1) for x in range(100)] """) # must be relative on Windows @@ -530,7 +530,7 @@ def test_multi_sockets_procs(self): with create_sockets(): with open(r'%s', 'w') as f: f.write("hello") - time.sleep(60) + [time.sleep(0.1) for x in range(100)] """ % fname) sproc = self.pyrun(src) pids.append(sproc.pid) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 4f80277b76..1ee5393c21 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -255,7 +255,8 @@ def test_cpu_percent_numcpus_none(self): def test_cpu_times(self): times = psutil.Process().cpu_times() - assert (times.user > 0.0) or (times.system > 0.0), times + assert times.user >= 0.0, times + assert times.system >= 0.0, times assert times.children_user >= 0.0, times assert times.children_system >= 0.0, times if LINUX: @@ -727,8 +728,17 @@ def test_exe(self): self.assertEqual(out, 'hey') def test_cmdline(self): - cmdline = [PYTHON_EXE, "-c", "import time; time.sleep(60)"] + cmdline = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] p = self.spawn_psproc(cmdline) + + if NETBSD and p.cmdline() == []: + # https://github.com/giampaolo/psutil/issues/2250 + raise unittest.SkipTest("OPENBSD: returned EBUSY") + # 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 @@ -750,7 +760,9 @@ def test_cmdline(self): def test_long_cmdline(self): cmdline = [PYTHON_EXE] cmdline.extend(["-v"] * 50) - cmdline.extend(["-c", "import time; time.sleep(10)"]) + cmdline.extend( + ["-c", "import time; [time.sleep(0.1) for x in range(100)]"] + ) p = self.spawn_psproc(cmdline) if OPENBSD: # XXX: for some reason the test process may turn into a @@ -776,7 +788,11 @@ def test_name(self): @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)"] + cmdline = [ + pyexe, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] p = self.spawn_psproc(cmdline) if OPENBSD: # XXX: for some reason the test process may turn into a @@ -805,7 +821,11 @@ def test_prog_w_funky_name(self): # 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)"] + cmdline = [ + pyexe, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] p = self.spawn_psproc(cmdline) self.assertEqual(p.cmdline(), cmdline) self.assertEqual(p.name(), os.path.basename(pyexe)) @@ -931,7 +951,10 @@ def test_cwd_2(self): cmd = [ PYTHON_EXE, "-c", - "import os, time; os.chdir('..'); time.sleep(60)", + ( + "import os, time; os.chdir('..'); [time.sleep(0.1) for x in" + " range(100)]" + ), ] p = self.spawn_psproc(cmd) call_until(p.cwd, "ret == os.path.dirname(os.getcwd())") @@ -1031,7 +1054,10 @@ def test_open_files(self): assert os.path.isfile(file.path), file # another process - cmdline = "import time; f = open(r'%s', 'r'); time.sleep(60);" % testfn + cmdline = ( + "import time; f = open(r'%s', 'r'); [time.sleep(0.1) for x in" + " range(100)];" % testfn + ) p = self.spawn_psproc([PYTHON_EXE, "-c", cmdline]) for x in range(100): @@ -1599,7 +1625,11 @@ 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);"] + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] with psutil.Popen( cmd, stdout=subprocess.PIPE, @@ -1635,7 +1665,11 @@ 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);"] + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] with psutil.Popen( cmd, stdout=subprocess.PIPE, diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 1a18b65a85..ef98a3abe9 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -255,7 +255,11 @@ def test_terminate(self): self.assertPidGone(p.pid) terminate(p) # by psutil.Popen - cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] p = psutil.Popen( cmd, stdout=subprocess.PIPE, diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 7fb2ef1990..45aeb4e926 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -197,7 +197,11 @@ def expect_exact_path_match(self): # --- def test_proc_exe(self): - cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] subp = self.spawn_testproc(cmd) p = psutil.Process(subp.pid) exe = p.exe() @@ -208,7 +212,11 @@ def test_proc_exe(self): ) def test_proc_name(self): - cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] subp = self.spawn_testproc(cmd) name = psutil.Process(subp.pid).name() self.assertIsInstance(name, str) @@ -216,7 +224,11 @@ def test_proc_name(self): self.assertEqual(name, os.path.basename(self.funky_name)) def test_proc_cmdline(self): - cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] subp = self.spawn_testproc(cmd) p = psutil.Process(subp.pid) cmdline = p.cmdline() From 5b30ef4796f7d0a1c4cd31a3b74b7405105331d0 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Tue, 18 Jun 2024 22:32:26 +0200 Subject: [PATCH 1117/1714] Add aarch64 manylinux wheels (#2425) --- .github/workflows/build.yml | 29 +++++++++++++++-------------- HISTORY.rst | 1 + psutil/tests/__init__.py | 8 +++++++- psutil/tests/test_contracts.py | 2 ++ psutil/tests/test_linux.py | 23 ++++++++++++++++++++--- psutil/tests/test_memleaks.py | 9 +++++++++ psutil/tests/test_misc.py | 5 +++++ psutil/tests/test_posix.py | 7 ++++++- psutil/tests/test_process.py | 24 +++++++++++++++++++----- psutil/tests/test_process_all.py | 4 ++++ psutil/tests/test_system.py | 3 +++ pyproject.toml | 4 ++++ 12 files changed, 95 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4e9c745ec..d23bd179c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,23 +18,20 @@ concurrency: jobs: # Linux + macOS + Windows Python 3 py3: - name: py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'ubuntu') && 'all' || matrix.archs }} + name: "py3-${{ matrix.os }}-${{ matrix.arch }}" runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: include: - - os: ubuntu-latest - archs: "x86_64 i686" - - os: macos-12 - archs: "x86_64" - - os: macos-14 - archs: "arm64" - - os: windows-2019 - archs: "AMD64" - - os: windows-2019 - archs: "x86" + - {os: ubuntu-latest, arch: x86_64} + - {os: ubuntu-latest, arch: i686} + - {os: ubuntu-latest, arch: aarch64} + - {os: macos-12, arch: x86_64} + - {os: macos-14, arch: arm64} + - {os: windows-2019, arch: AMD64} + - {os: windows-2019, arch: x86} steps: - uses: actions/checkout@v4 @@ -51,16 +48,20 @@ jobs: with: python-version: 3.11 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + if: matrix.arch == 'aarch64' + - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.18.0 + uses: pypa/cibuildwheel@v2.19.1 env: - CIBW_ARCHS: "${{ matrix.archs }}" + CIBW_ARCHS: "${{ matrix.arch }}" CIBW_PRERELEASE_PYTHONS: True - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'ubuntu') && 'all' || matrix.archs }} + name: wheels-py3-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse - name: Generate .tar.gz diff --git a/HISTORY.rst b/HISTORY.rst index ae664900f6..5107b1345c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ - 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. The old name is still available, but it's deprecated (triggers a ``DeprecationWarning``) and will be removed in the future. +- 2425_: [Linux]: provide aarch64 wheels. (patch by Matthieu Darbois / Ben Raz) **Bug fixes** diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 684c68ae2a..dccb9e4b28 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -87,7 +87,7 @@ "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", - "MACOS_12PLUS", "COVERAGE", + "MACOS_12PLUS", "COVERAGE", 'AARCH64', "QEMU_USER", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', 'spawn_children_pair', @@ -128,8 +128,14 @@ GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ CI_TESTING = APPVEYOR or GITHUB_ACTIONS COVERAGE = 'COVERAGE_RUN' in os.environ +if LINUX and GITHUB_ACTIONS: + with open('/proc/1/cmdline') as f: + QEMU_USER = "/bin/qemu-" in f.read() +else: + QEMU_USER = False # are we a 64 bit process? IS_64BIT = sys.maxsize > 2**32 +AARCH64 = platform.machine() == "aarch64" @memoize diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index e768e7d523..a5469ac8a2 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -30,6 +30,7 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import PYPY +from psutil.tests import QEMU_USER from psutil.tests import SKIP_SYSCONS from psutil.tests import PsutilTestCase from psutil.tests import create_sockets @@ -277,6 +278,7 @@ def test_net_if_addrs(self): self.assertIsInstance(addr.netmask, (str, type(None))) self.assertIsInstance(addr.broadcast, (str, type(None))) + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_net_if_stats(self): # Duplicate of test_system.py. Keep it anyway. for ifname, info in psutil.net_if_stats().items(): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index be264ae1db..d8faac9684 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -28,6 +28,7 @@ from psutil._compat import PY3 from psutil._compat import FileNotFoundError from psutil._compat import basestring +from psutil.tests import AARCH64 from psutil.tests import GITHUB_ACTIONS from psutil.tests import GLOBAL_TIMEOUT from psutil.tests import HAS_BATTERY @@ -35,6 +36,7 @@ from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_RLIMIT from psutil.tests import PYPY +from psutil.tests import QEMU_USER from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase @@ -277,8 +279,14 @@ def test_used(self): # This got changed in: # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d if get_free_version_info() < (3, 3, 12): - raise unittest.SkipTest("old free version") + raise unittest.SkipTest("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise unittest.SkipTest("free version too recent") cli_value = free_physmem().used psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( @@ -341,8 +349,14 @@ def test_used(self): # This got changed in: # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d if get_free_version_info() < (3, 3, 12): - raise unittest.SkipTest("old free version") + raise unittest.SkipTest("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise unittest.SkipTest("free version too recent") vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( @@ -830,6 +844,7 @@ def path_exists_mock(path): assert psutil.cpu_freq() @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @unittest.skipIf(AARCH64, "aarch64 does not report mhz in /proc/cpuinfo") def test_emulate_use_cpuinfo(self): # Emulate a case where /sys/devices/system/cpu/cpufreq* does not # exist and /proc/cpuinfo is used instead. @@ -1037,6 +1052,7 @@ def test_ips(self): @unittest.skipIf(not LINUX, "LINUX only") +@unittest.skipIf(QEMU_USER, "QEMU user not supported") class TestSystemNetIfStats(PsutilTestCase): @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") def test_against_ifconfig(self): @@ -1596,7 +1612,7 @@ def test_issue_687(self): with ThreadTask(): p = psutil.Process() threads = p.threads() - self.assertEqual(len(threads), 2) + self.assertEqual(len(threads), 3 if QEMU_USER else 2) tid = sorted(threads, key=lambda x: x.id)[1].id self.assertNotEqual(p.pid, tid) pt = psutil.Process(tid) @@ -2276,6 +2292,7 @@ def test_name(self): value = self.read_status_file("Name:") self.assertEqual(self.proc.name(), value) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_status(self): value = self.read_status_file("State:") value = value[value.find('(') + 1 : value.rfind(')')] diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 8232b18ad8..6506497c9c 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -19,6 +19,7 @@ import functools import os import platform +import sys import unittest import psutil @@ -43,6 +44,7 @@ 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 QEMU_USER from psutil.tests import TestMemoryLeak from psutil.tests import create_sockets from psutil.tests import get_testfn @@ -398,6 +400,7 @@ def test_disk_usage(self): times = FEW_TIMES if POSIX else self.times self.execute(lambda: psutil.disk_usage('.'), times=times) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_disk_partitions(self): self.execute(psutil.disk_partitions) @@ -435,6 +438,7 @@ def test_net_if_addrs(self): tolerance = 80 * 1024 if WINDOWS else self.tolerance self.execute(psutil.net_if_addrs, tolerance=tolerance) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_net_if_stats(self): self.execute(psutil.net_if_stats) @@ -491,6 +495,11 @@ def test_win_service_get_description(self): if __name__ == '__main__': + from psutil.tests.runner import cprint from psutil.tests.runner import run_from_name + if QEMU_USER: + cprint("skipping %s tests under QEMU_USER" % __file__, "brown") + sys.exit(0) + run_from_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 59416592e1..5c05d17643 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -43,6 +43,7 @@ from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import QEMU_USER from psutil.tests import SCRIPTS_DIR from psutil.tests import PsutilTestCase from psutil.tests import mock @@ -288,6 +289,9 @@ def check(ret): for fun, name in ns.iter(ns.getters): if name in {"win_service_iter", "win_service_get"}: continue + if QEMU_USER and name == "net_if_stats": + # OSError: [Errno 38] ioctl(SIOCETHTOOL) not implemented + continue with self.subTest(name=name): try: ret = fun() @@ -1008,6 +1012,7 @@ def test_pstree(self): def test_netstat(self): self.assert_stdout('netstat.py') + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_ifconfig(self): self.assert_stdout('ifconfig.py') diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 5203c2707a..941f0fac12 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -25,6 +25,7 @@ from psutil import SUNOS from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import PYTHON_EXE +from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import mock from psutil.tests import retry_on_failure @@ -102,7 +103,11 @@ def ps_name(pid): field = "command" if SUNOS: field = "comm" - return ps(field, pid).split()[0] + command = ps(field, pid).split() + if QEMU_USER: + assert "/bin/qemu-" in command[0] + return command[1] + return command[0] def ps_args(pid): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 1ee5393c21..363474c78a 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -54,6 +54,7 @@ from psutil.tests import PYPY from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import ThreadTask from psutil.tests import call_until @@ -253,6 +254,7 @@ def test_cpu_percent_numcpus_none(self): psutil.Process().cpu_percent() assert m.called + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_cpu_times(self): times = psutil.Process().cpu_times() assert times.user >= 0.0, times @@ -265,6 +267,7 @@ def test_cpu_times(self): for name in times._fields: time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_cpu_times_2(self): user_time, kernel_time = psutil.Process().cpu_times()[:2] utime, ktime = os.times()[:2] @@ -633,6 +636,8 @@ def test_memory_maps(self): for nt in maps: if not nt.path.startswith('['): + if QEMU_USER and "/bin/qemu-" in nt.path: + continue assert os.path.isabs(nt.path), nt.path if POSIX: try: @@ -698,6 +703,7 @@ def test_is_running(self): assert not p.is_running() assert not p.is_running() + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_exe(self): p = self.spawn_psproc() exe = p.exe() @@ -754,6 +760,9 @@ def test_cmdline(self): ' '.join(p.cmdline()[1:]), ' '.join(cmdline[1:]) ) return + if QEMU_USER: + self.assertEqual(' '.join(p.cmdline()[2:]), ' '.join(cmdline)) + return self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) @unittest.skipIf(PYPY, "broken on PYPY") @@ -771,13 +780,14 @@ def test_long_cmdline(self): self.assertEqual(p.cmdline(), cmdline) except psutil.ZombieProcess: raise unittest.SkipTest("OPENBSD: process turned into zombie") - elif NETBSD: + elif QEMU_USER: + self.assertEqual(p.cmdline()[2:], cmdline) + else: ret = p.cmdline() - if ret == []: + if NETBSD and ret == []: # https://github.com/giampaolo/psutil/issues/2250 raise unittest.SkipTest("OPENBSD: returned EBUSY") - - self.assertEqual(p.cmdline(), cmdline) + self.assertEqual(ret, cmdline) def test_name(self): p = self.spawn_psproc() @@ -785,7 +795,8 @@ def test_name(self): pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) - @unittest.skipIf(PYPY, "unreliable on PYPY") + @unittest.skipIf(PYPY or QEMU_USER, "unreliable on PYPY") + @unittest.skipIf(QEMU_USER, "unreliable on QEMU user") def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix="0123456789" * 2)) cmdline = [ @@ -816,6 +827,7 @@ def test_long_name(self): @unittest.skipIf(SUNOS, "broken on SUNOS") @unittest.skipIf(AIX, "broken on AIX") @unittest.skipIf(PYPY, "broken on PYPY") + @unittest.skipIf(QEMU_USER, "broken on QEMU user") def test_prog_w_funky_name(self): # Test that name(), exe() and cmdline() correctly handle programs # with funky chars such as spaces and ")", see: @@ -922,6 +934,7 @@ def cleanup(init): except psutil.AccessDenied: pass + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_status(self): p = psutil.Process() self.assertEqual(p.status(), psutil.STATUS_RUNNING) @@ -1149,6 +1162,7 @@ def test_parent_multi(self): self.assertEqual(grandchild.parent(), child) self.assertEqual(child.parent(), parent) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") @retry_on_failure() def test_parents(self): parent = psutil.Process() diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index d1f476bb53..48833a105c 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -32,6 +32,7 @@ from psutil._compat import long from psutil._compat import unicode from psutil.tests import CI_TESTING +from psutil.tests import QEMU_USER from psutil.tests import VALID_PROC_STATUSES from psutil.tests import PsutilTestCase from psutil.tests import check_connection_ntuple @@ -235,6 +236,9 @@ def username(self, ret, info): def status(self, ret, info): self.assertIsInstance(ret, str) assert ret, ret + if QEMU_USER: + # status does not work under qemu user + return self.assertNotEqual(ret, '?') # XXX self.assertIn(ret, VALID_PROC_STATUSES) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 554fbffb28..e228f6d32c 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -48,6 +48,7 @@ from psutil.tests import IS_64BIT from psutil.tests import MACOS_12PLUS from psutil.tests import PYPY +from psutil.tests import QEMU_USER from psutil.tests import UNICODE_SUFFIX from psutil.tests import PsutilTestCase from psutil.tests import check_net_address @@ -806,6 +807,7 @@ def test_net_io_counters_no_nics(self): self.assertEqual(psutil.net_io_counters(pernic=True), {}) assert m.called + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_net_if_addrs(self): nics = psutil.net_if_addrs() assert nics, nics @@ -893,6 +895,7 @@ def test_net_if_addrs_mac_null_bytes(self): else: self.assertEqual(addr.address, '06-3d-29-00-00-00') + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_net_if_stats(self): nics = psutil.net_if_stats() assert nics, nics diff --git a/pyproject.toml b/pyproject.toml index b03392071f..ba13f2829e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -207,6 +207,7 @@ trailing_comma_inline_array = true skip = [ "*-musllinux*", "cp313-win*", # pywin32 is not available on cp313 yet + "cp3{7,8,9,10,11,12}-*linux_{aarch64,ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures "pp*", ] test-command = [ @@ -218,6 +219,9 @@ test-extras = "test" [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] +[tool.cibuildwheel.linux] +before-all = "yum install -y net-tools" + [build-system] build-backend = "setuptools.build_meta" requires = ["setuptools>=43", "wheel"] From 3d5522aafb6c9cbcbcf4edfa11348f755af97d96 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Jun 2024 23:20:42 +0200 Subject: [PATCH 1118/1714] release --- HISTORY.rst | 4 ++-- Makefile | 4 ++-- docs/index.rst | 4 ++++ scripts/internal/print_dist.py | 8 +++++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5107b1345c..d290221f21 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,7 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -6.0.0 (IN DEVELOPMENT) -====================== +6.0.0 2024-06-18 +================ **Enhancements** diff --git a/Makefile b/Makefile index 60d82336e8..f54354d350 100644 --- a/Makefile +++ b/Makefile @@ -309,8 +309,8 @@ pre-release: ## Check if we're ready to produce a new release. release: ## Upload a new release. ${MAKE} check-sdist ${MAKE} check-wheels - $(PYTHON) -m twine upload dist/*.tar.gz - $(PYTHON) -m twine upload dist/*.whl + $(PYTHON) -m twine upload --verbose dist/*.tar.gz + $(PYTHON) -m twine upload --verbose dist/*.whl ${MAKE} git-tag-release generate-manifest: ## Generates MANIFEST.in file. diff --git a/docs/index.rst b/docs/index.rst index 201333af4b..d9fe1c03cd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2672,6 +2672,10 @@ PyPy3. Timeline ======== +- 2024-06-18: + `6.0.0 `__ - + `what's new `__ - + `diff `__ - 2024-01-19: `5.9.8 `__ - `what's new `__ - diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 5b6531608b..1fb4b3f3aa 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -58,11 +58,13 @@ def platform(self): def arch(self): if self.name.endswith(('x86_64.whl', 'amd64.whl')): - return '64' + return '64-bit' if self.name.endswith(("i686.whl", "win32.whl")): - return '32' + return '32-bit' if self.name.endswith("arm64.whl"): return 'arm64' + if self.name.endswith("aarch64.whl"): + return 'aarch64' return '?' def pyver(self): @@ -109,7 +111,7 @@ def main(): tot_files = 0 tot_size = 0 - templ = "%-120s %7s %7s %7s" + templ = "%-120s %7s %8s %7s" for platf, pkgs in groups.items(): ppn = "%s (%s)" % (platf, len(pkgs)) s = templ % (ppn, "size", "arch", "pyver") From 9c84c85c2e0edd95a415283b96450d0dfa8fbcbf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 22 Jun 2024 01:01:46 +0200 Subject: [PATCH 1119/1714] minor changes --- Makefile | 6 +++--- scripts/internal/print_announce.py | 4 ++-- scripts/internal/print_downloads.py | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f54354d350..259ad43890 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ PY3_DEPS = \ pypinfo \ requests \ rstcheck \ - ruff==0.4.4 \ + ruff \ setuptools \ sphinx_rtd_theme \ teyit \ @@ -309,8 +309,8 @@ pre-release: ## Check if we're ready to produce a new release. release: ## Upload a new release. ${MAKE} check-sdist ${MAKE} check-wheels - $(PYTHON) -m twine upload --verbose dist/*.tar.gz - $(PYTHON) -m twine upload --verbose dist/*.whl + $(PYTHON) -m twine upload dist/*.tar.gz + $(PYTHON) -m twine upload dist/*.whl ${MAKE} git-tag-release generate-manifest: ## Generates MANIFEST.in file. diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index d1a7d297f1..65e28e3511 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -49,8 +49,8 @@ line tools such as: ps, top, lsof, netstat, ifconfig, who, df, kill, free, \ nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap. It \ currently supports Linux, Windows, macOS, Sun Solaris, FreeBSD, OpenBSD, \ -NetBSD and AIX, both 32-bit and 64-bit architectures. Supported Python \ -versions are 2.7 and 3.6+. PyPy is also known to work. +NetBSD and AIX. Supported Python versions are 2.7 and 3.6+. PyPy is also \ +known to work. What's new ========== diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index b453f15b92..8afb7c1fde 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -49,6 +49,7 @@ def sh(cmd): stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, + env=env, ) stdout, stderr = p.communicate() if p.returncode != 0: From 73af407310689e73ef89fcbdedae7de839cb0d3a Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Mon, 24 Jun 2024 14:39:22 -0400 Subject: [PATCH 1120/1714] Fix GIL warnings and a few thread-safety issues in free-threaded CPython (#2428) * Fix GIL warnings and a few thread-safety issues in free-threaded CPython * The temporary `argv` C array is no longer global in OpenBSD's proc_cmdline * The `maxcpus` variable is no longer global in FreeBSD's per_cpu_times. Signed-off-by: Sam Gross --- psutil/_psutil_aix.c | 3 +++ psutil/_psutil_bsd.c | 4 ++++ psutil/_psutil_linux.c | 4 ++++ psutil/_psutil_osx.c | 4 ++++ psutil/_psutil_posix.c | 4 ++++ psutil/_psutil_sunos.c | 4 ++++ psutil/_psutil_windows.c | 4 ++++ psutil/arch/freebsd/cpu.c | 2 +- psutil/arch/openbsd/proc.c | 5 ++++- 9 files changed, 32 insertions(+), 2 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index ce89a7bd7c..42f921188e 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -1080,6 +1080,9 @@ void init_psutil_aix(void) PyObject *module = PyModule_Create(&moduledef); #else PyObject *module = Py_InitModule("_psutil_aix", PsutilMethods); +#endif +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); #endif PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 6517d5800a..facaba831f 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -143,6 +143,10 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); +#endif + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) INITERR; // process status constants diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 292e1c5524..46244c5792 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -78,6 +78,10 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); +#endif + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) INITERR; if (PyModule_AddIntConstant(mod, "DUPLEX_HALF", DUPLEX_HALF)) INITERR; if (PyModule_AddIntConstant(mod, "DUPLEX_FULL", DUPLEX_FULL)) INITERR; diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 4aa11d1700..09fa267a98 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -91,6 +91,10 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); +#endif + if (psutil_setup() != 0) INITERR; diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 24628afc78..8ced7beaac 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -913,6 +913,10 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); +#endif + #if defined(PSUTIL_BSD) || \ defined(PSUTIL_OSX) || \ defined(PSUTIL_SUNOS) || \ diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 54f353c106..d21f59c618 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1721,6 +1721,10 @@ void init_psutil_sunos(void) if (module == NULL) INITERROR; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); +#endif + if (psutil_setup() != 0) INITERROR; diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index bb6e12ff80..0c221bdc23 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -165,6 +165,10 @@ void init_psutil_windows(void) if (module == NULL) INITERROR; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); +#endif + if (psutil_setup() != 0) INITERROR; if (psutil_set_se_debug() != 0) diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index a15d96efc1..9fa1a7dbe6 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -26,7 +26,7 @@ For reference, here's the git history with original(ish) implementations: PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { - static int maxcpus; + int maxcpus; int mib[2]; int ncpu; size_t len; diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 96b85bc502..0881ccd555 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -147,7 +147,7 @@ PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; int mib[4]; - static char **argv; + char **argv = NULL; char **p; size_t argv_size = 128; PyObject *py_retlist = PyList_New(0); @@ -189,9 +189,12 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { Py_DECREF(py_arg); } + free(argv); return py_retlist; error: + if (argv != NULL) + free(argv); Py_XDECREF(py_arg); Py_DECREF(py_retlist); return NULL; From c034e6692cf736b5e87d14418a8153bb03f6cf42 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 24 Jun 2024 20:47:56 +0200 Subject: [PATCH 1121/1714] give credit to @colesbury / Sam Gross for #2401 and #2427 --- CREDITS | 4 ++++ HISTORY.rst | 18 +++++++++++++++--- psutil/__init__.py | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CREDITS b/CREDITS index baff0c0899..543536370a 100644 --- a/CREDITS +++ b/CREDITS @@ -831,3 +831,7 @@ I: 2376 N: Anthony Ryan W: https://github.com/anthonyryan1 I: 2272 + +N: Sam Gross +W: https://github.com/colesbury +I: 2401, 2427 diff --git a/HISTORY.rst b/HISTORY.rst index d290221f21..2aa985bfe1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,19 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -6.0.0 2024-06-18 -================ +6.0.1 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +**Bug fixes** + +- 2427_: psutil (segfault) on import in the free-threaded (no GIL) version of + Python 3.13. (patch by Sam Gross) + +6.0.0 +====== + +2024-06-18 **Enhancements** @@ -14,7 +26,7 @@ been reused. This makes `process_iter()`_ around 20x times faster. - 2396_: a new ``psutil.process_iter.cache_clear()`` API can be used the clear `process_iter()`_ internal cache. -- 2401_, Support building with free-threaded CPython 3.13. +- 2401_, Support building with free-threaded CPython 3.13. (patch by Sam Gross) - 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. The old name is still available, but it's deprecated (triggers a ``DeprecationWarning``) and will be removed in the future. diff --git a/psutil/__init__.py b/psutil/__init__.py index 3a503503cc..c1e038f3ed 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -214,7 +214,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "6.0.0" +__version__ = "6.0.1" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) From 8d943015ffce86de31d9494ffa3a1eae7dd91719 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Wed, 21 Aug 2024 14:19:48 +0200 Subject: [PATCH 1122/1714] chore: bump cibuildwheel to 2.19.2 (#2429) Signed-off-by: mayeut --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d23bd179c6..12f8dcdf05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: if: matrix.arch == 'aarch64' - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_ARCHS: "${{ matrix.arch }}" CIBW_PRERELEASE_PYTHONS: True From 2dc2cd753c22fae91300173c99022d4f4690e367 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 Sep 2024 16:23:54 +0200 Subject: [PATCH 1123/1714] update ruff --- Makefile | 2 +- psutil/_psbsd.py | 2 +- pyproject.toml | 6 ++++-- scripts/internal/winmake.py | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 259ad43890..473ba6d3ab 100644 --- a/Makefile +++ b/Makefile @@ -201,7 +201,7 @@ test-coverage: ## Run test coverage. # =================================================================== ruff: ## Run ruff linter. - @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --output-format=concise black: ## Python files linting (via black) @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 4d49cf98bd..cf84207e79 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -10,7 +10,7 @@ import os from collections import defaultdict from collections import namedtuple -from xml.etree import ElementTree +from xml.etree import ElementTree # noqa ICN001 from . import _common from . import _psposix diff --git a/pyproject.toml b/pyproject.toml index ba13f2829e..04ad6414d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,14 +39,15 @@ ignore = [ "C90", # mccabe (function `X` is too complex) "COM812", # Trailing comma missing "D", # pydocstyle + "DOC", # various docstring warnings "DTZ", # flake8-datetimez "ERA001", # Found commented-out code "FBT", # flake8-boolean-trap (makes zero sense) "FIX", # Line contains TODO / XXX / ..., consider resolving the issue "FLY", # flynt (PYTHON2.7 COMPAT) "FURB101", # `open` and `read` should be replaced by `Path(src).read_text()` - "FURB103", # `open` and `write` should be replaced by `Path(...).write_text(...)` - "FURB113", # Use `x.extend(('a', 'b', 'c'))` instead of repeatedly calling `x.append()` + "FURB103", # `open` and `write` should be replaced by `Path(src).write_text()` + "FURB113", # Use `ls.extend(...)` instead of repeatedly calling `ls.append()` "FURB116", # [*] Replace `hex` call with `f"{start:x}"` "FURB118", # [*] Use `operator.add` instead of defining a lambda "FURB140", # [*] Use `itertools.starmap` instead of the generator @@ -99,6 +100,7 @@ ignore = [ "UP028", # [*] Replace `yield` over `for` loop with `yield from` (PYTHON2.7 COMPAT) "UP031", # [*] Use format specifiers instead of percent format "UP032", # [*] Use f-string instead of `format` call (PYTHON2.7 COMPAT) + "UP036", # Version block is outdated for minimum Python version (PYTHON2.7 COMPAT) ] [tool.ruff.lint.per-file-ignores] diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index b789aa80d8..20126ba7cc 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -114,8 +114,8 @@ def win_colorprint(s, color=LIGHTBLUE): def sh(cmd, nolog=False): if not nolog: safe_print("cmd: " + cmd) - p = subprocess.Popen( - cmd, shell=True, env=os.environ, cwd=os.getcwd() # noqa + p = subprocess.Popen( # noqa S602 + cmd, shell=True, env=os.environ, cwd=os.getcwd() ) p.communicate() if p.returncode != 0: From b1e52fcfe4bd0a3d09334fddd6ffd23288f77a79 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Sep 2024 01:15:33 +0200 Subject: [PATCH 1124/1714] comment freebsd --- .github/workflows/bsd.yml | 132 +++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 3af8d2371c..8182177c0f 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -1,67 +1,67 @@ -# Execute tests on *BSD platforms. Does not produce wheels. -# Useful URLs: -# https://github.com/vmactions/freebsd-vm -# https://github.com/vmactions/openbsd-vm -# https://github.com/vmactions/netbsd-vm +# # Execute tests on *BSD platforms. Does not produce wheels. +# # Useful URLs: +# # https://github.com/vmactions/freebsd-vm +# # https://github.com/vmactions/openbsd-vm +# # https://github.com/vmactions/netbsd-vm -on: [push, pull_request] -name: bsd-tests -concurrency: - group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} - cancel-in-progress: true -jobs: - freebsd: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Run tests - uses: vmactions/freebsd-vm@v1 - with: - usesh: true - prepare: | - pkg install -y gcc python3 - run: | - set -e -x - make install-pip - python3 -m pip install --user setuptools - make install - make test - make test-memleaks - openbsd: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Run tests - uses: vmactions/openbsd-vm@v1 - with: - usesh: true - prepare: | - set -e - pkg_add gcc python3 - run: | - set -e - make install-pip - python3 -m pip install --user setuptools - make install - make test - make test-memleaks - netbsd: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Run tests - uses: vmactions/netbsd-vm@v1 - with: - usesh: true - prepare: | - set -e - /usr/sbin/pkg_add -v pkgin - pkgin update - pkgin -y install python311-* py311-setuptools-* gcc12-* - run: | - set -e - make install-pip PYTHON=python3.11 - python3.11 -m pip install --user setuptools - make install PYTHON=python3.11 - make test PYTHON=python3.11 - make test-memleaks PYTHON=python3.11 +# on: [push, pull_request] +# name: bsd-tests +# concurrency: +# group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} +# cancel-in-progress: true +# jobs: +# freebsd: +# runs-on: ubuntu-22.04 +# steps: +# - uses: actions/checkout@v4 +# - name: Run tests +# uses: vmactions/freebsd-vm@v1 +# with: +# usesh: true +# prepare: | +# pkg install -y gcc python3 +# run: | +# set -e -x +# make install-pip +# python3 -m pip install --user setuptools +# make install +# make test +# make test-memleaks +# openbsd: +# runs-on: ubuntu-22.04 +# steps: +# - uses: actions/checkout@v4 +# - name: Run tests +# uses: vmactions/openbsd-vm@v1 +# with: +# usesh: true +# prepare: | +# set -e +# pkg_add gcc python3 +# run: | +# set -e +# make install-pip +# python3 -m pip install --user setuptools +# make install +# make test +# make test-memleaks +# netbsd: +# runs-on: ubuntu-22.04 +# steps: +# - uses: actions/checkout@v4 +# - name: Run tests +# uses: vmactions/netbsd-vm@v1 +# with: +# usesh: true +# prepare: | +# set -e +# /usr/sbin/pkg_add -v pkgin +# pkgin update +# pkgin -y install python311-* py311-setuptools-* gcc12-* +# run: | +# set -e +# make install-pip PYTHON=python3.11 +# python3.11 -m pip install --user setuptools +# make install PYTHON=python3.11 +# make test PYTHON=python3.11 +# make test-memleaks PYTHON=python3.11 From a06e7620b0ea0c4d80fc270e1307e574703629fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20V=C3=AEjdea?= Date: Mon, 30 Sep 2024 22:24:40 +0300 Subject: [PATCH 1125/1714] Define Py_GIL_DISABLED macro on Windows (#2442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/python/cpython/issues/111650 Signed-off-by: Cristi Vîjdea --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index e3375004bb..23e4f17c7a 100755 --- a/setup.py +++ b/setup.py @@ -224,6 +224,9 @@ def get_winver(): # see: https://github.com/giampaolo/psutil/issues/348 ('PSAPI_VERSION', 1), ]) + + if Py_GIL_DISABLED: + macros.append(('Py_GIL_DISABLED', 1)) ext = Extension( 'psutil._psutil_windows', From 622bd442eef704627202282d5805e4b39358b897 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 1 Oct 2024 09:49:53 +0200 Subject: [PATCH 1126/1714] Fix ABI3 wheels warnings (#2445) --- .github/workflows/build.yml | 2 +- Makefile | 4 +++ psutil/_psutil_common.c | 2 +- psutil/_psutil_common.h | 2 +- psutil/_psutil_posix.c | 6 ++-- psutil/arch/bsd/proc.c | 2 +- psutil/arch/freebsd/cpu.c | 18 +++++++----- psutil/arch/freebsd/mem.c | 47 ++++++++++++++++++------------ psutil/arch/freebsd/proc.c | 17 ++++++----- psutil/arch/linux/net.c | 4 +-- psutil/arch/netbsd/proc.c | 6 ++-- psutil/arch/openbsd/socks.c | 2 +- psutil/arch/osx/cpu.c | 2 +- psutil/arch/osx/proc.c | 17 ++++++----- psutil/arch/windows/proc.c | 20 ++++++------- psutil/arch/windows/proc_handles.c | 18 +++++++----- psutil/arch/windows/proc_info.c | 8 ++--- psutil/arch/windows/proc_utils.c | 6 ++-- psutil/arch/windows/security.c | 12 ++++---- psutil/arch/windows/services.c | 22 +++++++------- psutil/arch/windows/sys.c | 14 ++++++--- psutil/arch/windows/wmi.c | 4 +-- setup.py | 2 +- 23 files changed, 133 insertions(+), 104 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12f8dcdf05..29753ef82d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,7 +119,7 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install ruff==0.4.4 black rstcheck toml-sort sphinx + python3 -m pip install ruff black rstcheck toml-sort sphinx make lint-all # upload weels as a single artefact diff --git a/Makefile b/Makefile index 473ba6d3ab..64fba604e2 100644 --- a/Makefile +++ b/Makefile @@ -271,6 +271,10 @@ download-wheels-appveyor: ## Download latest wheels hosted on appveyor. $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels_appveyor.py ${MAKE} print-dist +create-wheels: ## Create .whl files + $(PYTHON_ENV_VARS) $(PYTHON) setup.py bdist_wheel + ${MAKE} check-wheels + check-sdist: ## Check sanity of source distribution. $(PYTHON_ENV_VARS) $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv $(PYTHON_ENV_VARS) build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 2f2edca260..d16816972e 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -89,7 +89,7 @@ PyErr_SetFromWindowsErr(int winerr) { * message. */ PyObject * -PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { +psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { char fullmsg[1024]; #ifdef PSUTIL_WINDOWS diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 20c03ebd7c..2cdfa9d4d6 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -97,7 +97,7 @@ static const int PSUTIL_CONN_NONE = 128; PyObject* AccessDenied(const char *msg); PyObject* NoSuchProcess(const char *msg); -PyObject* PyErr_SetFromOSErrnoWithSyscall(const char *syscall); +PyObject* psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); // ==================================================================== // --- Global utils diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 8ced7beaac..5df15530f3 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -148,7 +148,7 @@ psutil_pid_exists(pid_t pid) { void psutil_raise_for_pid(long pid, char *syscall) { if (errno != 0) - PyErr_SetFromOSErrnoWithSyscall(syscall); + psutil_PyErr_SetFromOSErrnoWithSyscall(syscall); else if (psutil_pid_exists(pid) == 0) NoSuchProcess(syscall); else @@ -470,14 +470,14 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) { - PyErr_SetFromOSErrnoWithSyscall("socket(SOCK_DGRAM)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("socket(SOCK_DGRAM)"); goto error; } PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); ret = ioctl(sock, SIOCGIFFLAGS, &ifr); if (ret == -1) { - PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCGIFFLAGS)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCGIFFLAGS)"); goto error; } diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index e64cf80dc1..5d353ff35e 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -380,7 +380,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { #endif default: sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); - PyErr_SetFromOSErrnoWithSyscall(errbuf); + psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); break; } goto error; diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 9fa1a7dbe6..29a5ec5758 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -42,7 +42,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { size = sizeof(maxcpus); if (sysctlbyname("kern.smp.maxcpus", &maxcpus, &size, NULL, 0) < 0) { Py_DECREF(py_retlist); - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('kern.smp.maxcpus')"); } long cpu_time[maxcpus][CPUSTATES]; @@ -52,14 +52,16 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { mib[1] = HW_NCPU; len = sizeof(ncpu); if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_NCPU)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_NCPU)"); goto error; } // per-cpu info size = sizeof(cpu_time); if (sysctlbyname("kern.cp_times", &cpu_time, &size, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('kern.smp.maxcpus')"); + psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('kern.smp.maxcpus')" + ); goto error; } @@ -126,23 +128,23 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { size_t size = sizeof(v_soft); if (sysctlbyname("vm.stats.sys.v_soft", &v_soft, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('vm.stats.sys.v_soft')"); } if (sysctlbyname("vm.stats.sys.v_intr", &v_intr, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('vm.stats.sys.v_intr')"); } if (sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('vm.stats.sys.v_syscall')"); } if (sysctlbyname("vm.stats.sys.v_trap", &v_trap, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('vm.stats.sys.v_trap')"); } if (sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('vm.stats.sys.v_swtch')"); } diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 3326f63a1d..0482ef72df 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -39,36 +39,44 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { size_t buffers_size = sizeof(buffers); if (sysctlbyname("hw.physmem", &total, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('hw.physmem')"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('hw.physmem')" + ); } if (sysctlbyname("vm.stats.vm.v_active_count", &active, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('vm.stats.vm.v_active_count')"); } if (sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( + return psutil_PyErr_SetFromOSErrnoWithSyscall( "sysctlbyname('vm.stats.vm.v_inactive_count')"); } if (sysctlbyname("vm.stats.vm.v_wire_count", &wired, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_wire_count')"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_wire_count')" + ); } // https://github.com/giampaolo/psutil/issues/997 if (sysctlbyname("vm.stats.vm.v_cache_count", &cached, &size, NULL, 0)) { cached = 0; } if (sysctlbyname("vm.stats.vm.v_free_count", &free, &size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_free_count')"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_free_count')" + ); } if (sysctlbyname("vfs.bufspace", &buffers, &buffers_size, NULL, 0)) { - return PyErr_SetFromOSErrnoWithSyscall("sysctlbyname('vfs.bufspace')"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vfs.bufspace')" + ); } size = sizeof(vm); if (sysctl(mib, 2, &vm, &size, NULL, 0) != 0) { - return PyErr_SetFromOSErrnoWithSyscall("sysctl(CTL_VM | VM_METER)"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctl(CTL_VM | VM_METER)" + ); } return Py_BuildValue("KKKKKKKK", @@ -109,20 +117,24 @@ psutil_swap_mem(PyObject *self, PyObject *args) { kvm_close(kd); if (sysctlbyname("vm.stats.vm.v_swapin", &swapin, &size, NULL, 0) == -1) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_swapin)'"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_swapin)'" + ); } if (sysctlbyname("vm.stats.vm.v_swapout", &swapout, &size, NULL, 0) == -1){ - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_swapout)'"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_swapout)'" + ); } if (sysctlbyname("vm.stats.vm.v_vnodein", &nodein, &size, NULL, 0) == -1) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_vnodein)'"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_vnodein)'" + ); } if (sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, &size, NULL, 0) == -1) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_vnodeout)'"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('vm.stats.vm.v_vnodeout)'" + ); } return Py_BuildValue( @@ -135,4 +147,3 @@ psutil_swap_mem(PyObject *self, PyObject *args) { nodein + nodeout // swap out ); } - diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 6528ece452..a81128b518 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -45,7 +45,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { size = sizeof(struct kinfo_proc); if (sysctl((int *)mib, 4, proc, &size, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_PID)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_PID)"); return -1; } @@ -92,7 +92,7 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { // Call sysctl with a NULL buffer in order to get buffer length. err = sysctl(name, 3, NULL, &length, NULL, 0); if (err == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl (null buffer)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl (null buffer)"); return 1; } @@ -120,7 +120,7 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { } } - PyErr_SetFromOSErrnoWithSyscall("sysctl()"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl()"); return 1; } else { @@ -177,7 +177,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { size = argmax; if (sysctl(mib, 4, procargs, &size, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGS)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGS)"); goto error; } @@ -238,8 +238,9 @@ psutil_proc_exe(PyObject *self, PyObject *args) { return PyUnicode_DecodeFSDefault(""); } else { - return \ - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_PATHNAME)"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctl(KERN_PROC_PATHNAME)" + ); } } if (size == 0 || strlen(pathname) == 0) { @@ -300,7 +301,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { size = 0; error = sysctl(mib, 4, NULL, &size, NULL, 0); if (error == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); goto error; } if (size == 0) { @@ -316,7 +317,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { error = sysctl(mib, 4, kip, &size, NULL, 0); if (error == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); goto error; } if (size == 0) { diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index d193e94087..6d2785ee63 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -71,7 +71,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) - return PyErr_SetFromOSErrnoWithSyscall("socket()"); + return psutil_PyErr_SetFromOSErrnoWithSyscall("socket()"); PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // duplex and speed @@ -102,7 +102,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { speed = 0; } else { - PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCETHTOOL)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCETHTOOL)"); goto error; } } diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 4cd43c4c7f..7613b54594 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -352,7 +352,9 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { st = sysctl(mib, __arraycount(mib), NULL, &len, NULL, 0); if (st == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV) get size"); + psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctl(KERN_PROC_ARGV) get size" + ); goto error; } @@ -382,7 +384,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { } } else { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV)"); goto error; } } diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c index 69daa447b0..05849aa1d8 100644 --- a/psutil/arch/openbsd/socks.c +++ b/psutil/arch/openbsd/socks.c @@ -67,7 +67,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { ikf = kvm_getfiles(kd, KERN_FILE_BYPID, -1, sizeof(*ikf), &cnt); if (! ikf) { - PyErr_SetFromOSErrnoWithSyscall("kvm_getfiles"); + psutil_PyErr_SetFromOSErrnoWithSyscall("kvm_getfiles"); goto error; } diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 4196083ecc..d2f8b1855f 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -243,7 +243,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { mib[1] = HW_CPU_FREQ; if (sysctl(mib, 2, &curr, &len, NULL, 0) < 0) - return PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); + return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 2cdb9911c0..136311ecee 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -77,7 +77,7 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { while (lim-- > 0) { size = 0; if (sysctl((int *)mib, 3, NULL, &size, NULL, 0) == -1) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); return 1; } size2 = size + (size >> 3); // add some @@ -100,7 +100,7 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { err = errno; free(ptr); if (err != ENOMEM) { - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); return 1; } } @@ -132,7 +132,7 @@ psutil_sysctl_argmax() { if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) return argmax; - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); return 0; } @@ -164,7 +164,7 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); return 1; } - PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); return 1; } return 0; @@ -186,7 +186,7 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { // now read the data from sysctl if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { // raise an exception and throw errno as the error - PyErr_SetFromOSErrnoWithSyscall("sysctl"); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl"); return -1; } @@ -605,8 +605,9 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { len = sizeof(cpu_type); if (sysctlbyname("sysctl.proc_cputype", &cpu_type, &len, NULL, 0) != 0) { - return PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('sysctl.proc_cputype')"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctlbyname('sysctl.proc_cputype')" + ); } // Roughly based on libtop_update_vm_regions in @@ -977,7 +978,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { // check for inet_ntop failures if (errno != 0) { - PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); goto error; } diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index af3df267ac..05fb502557 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -116,7 +116,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { // https://github.com/giampaolo/psutil/issues/1099 // http://bugs.python.org/issue14252 if (GetLastError() != ERROR_ACCESS_DENIED) { - PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); + psutil_PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); return NULL; } } @@ -151,7 +151,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { Py_RETURN_NONE; } else { - PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); return NULL; } } @@ -163,7 +163,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // handle return code if (retVal == WAIT_FAILED) { - PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); + psutil_PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); CloseHandle(hProcess); return NULL; } @@ -185,7 +185,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // process is gone so we can get its process exit code. The PID // may still stick around though but we'll handle that from Python. if (GetExitCodeProcess(hProcess, &ExitCode) == 0) { - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + psutil_PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); CloseHandle(hProcess); return NULL; } @@ -598,7 +598,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThreadSnap == INVALID_HANDLE_VALUE) { - PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); + psutil_PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); goto error; } @@ -606,7 +606,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { te32.dwSize = sizeof(THREADENTRY32); if (! Thread32First(hThreadSnap, &te32)) { - PyErr_SetFromOSErrnoWithSyscall("Thread32First"); + psutil_PyErr_SetFromOSErrnoWithSyscall("Thread32First"); goto error; } @@ -626,7 +626,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { rc = GetThreadTimes(hThread, &ftDummy, &ftDummy, &ftKernel, &ftUser); if (rc == 0) { - PyErr_SetFromOSErrnoWithSyscall("GetThreadTimes"); + psutil_PyErr_SetFromOSErrnoWithSyscall("GetThreadTimes"); goto error; } @@ -702,7 +702,7 @@ _psutil_user_token_from_pid(DWORD pid) { return NULL; if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { - PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); goto error; } @@ -721,7 +721,7 @@ _psutil_user_token_from_pid(DWORD pid) { continue; } else { - PyErr_SetFromOSErrnoWithSyscall("GetTokenInformation"); + psutil_PyErr_SetFromOSErrnoWithSyscall("GetTokenInformation"); goto error; } } @@ -797,7 +797,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { goto error; } else { - PyErr_SetFromOSErrnoWithSyscall("LookupAccountSidW"); + psutil_PyErr_SetFromOSErrnoWithSyscall("LookupAccountSidW"); goto error; } } diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 30e7cd2d84..01ef6a425e 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -156,7 +156,7 @@ psutil_threaded_get_filename(HANDLE hFile) { hThread = CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE)psutil_get_filename, &hFile, 0, NULL); if (hThread == NULL) { - PyErr_SetFromOSErrnoWithSyscall("CreateThread"); + psutil_PyErr_SetFromOSErrnoWithSyscall("CreateThread"); return 1; } @@ -168,7 +168,7 @@ psutil_threaded_get_filename(HANDLE hFile) { psutil_debug( "get handle name thread timed out after %i ms", THREAD_TIMEOUT); if (TerminateThread(hThread, 0) == 0) { - PyErr_SetFromOSErrnoWithSyscall("TerminateThread"); + psutil_PyErr_SetFromOSErrnoWithSyscall("TerminateThread"); CloseHandle(hThread); return 1; } @@ -179,26 +179,28 @@ psutil_threaded_get_filename(HANDLE hFile) { if (dwWait == WAIT_FAILED) { psutil_debug("WaitForSingleObject -> WAIT_FAILED"); if (TerminateThread(hThread, 0) == 0) { - PyErr_SetFromOSErrnoWithSyscall( - "WaitForSingleObject -> WAIT_FAILED -> TerminateThread"); + psutil_PyErr_SetFromOSErrnoWithSyscall( + "WaitForSingleObject -> WAIT_FAILED -> TerminateThread" + ); CloseHandle(hThread); return 1; } - PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); + psutil_PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); CloseHandle(hThread); return 1; } if (GetExitCodeThread(hThread, &threadRetValue) == 0) { if (TerminateThread(hThread, 0) == 0) { - PyErr_SetFromOSErrnoWithSyscall( - "GetExitCodeThread (failed) -> TerminateThread"); + psutil_PyErr_SetFromOSErrnoWithSyscall( + "GetExitCodeThread (failed) -> TerminateThread" + ); CloseHandle(hThread); return 1; } CloseHandle(hThread); - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeThread"); + psutil_PyErr_SetFromOSErrnoWithSyscall("GetExitCodeThread"); return 1; } CloseHandle(hThread); diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 5d16b8133d..9e0caf3442 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -41,7 +41,7 @@ psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { MEMORY_BASIC_INFORMATION info; if (!VirtualQueryEx(hProcess, src, &info, sizeof(info))) { - PyErr_SetFromOSErrnoWithSyscall("VirtualQueryEx"); + psutil_PyErr_SetFromOSErrnoWithSyscall("VirtualQueryEx"); return -1; } @@ -67,7 +67,7 @@ psutil_convert_winerr(ULONG err, char* syscall) { AccessDenied(fullmsg); } else { - PyErr_SetFromOSErrnoWithSyscall(syscall); + psutil_PyErr_SetFromOSErrnoWithSyscall(syscall); } } @@ -226,7 +226,7 @@ psutil_get_process_data(DWORD pid, // 32 bit case. Check if the target is also 32 bit. if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || !IsWow64Process(hProcess, &theyAreWow64)) { - PyErr_SetFromOSErrnoWithSyscall("IsWow64Process"); + psutil_PyErr_SetFromOSErrnoWithSyscall("IsWow64Process"); goto error; } @@ -594,7 +594,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(data, &nArgs); if (szArglist == NULL) { - PyErr_SetFromOSErrnoWithSyscall("CommandLineToArgvW"); + psutil_PyErr_SetFromOSErrnoWithSyscall("CommandLineToArgvW"); goto error; } diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index 77b6dbf1e8..1ebb76c448 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -103,7 +103,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { } return NULL; } - PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); return NULL; } @@ -129,7 +129,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { SetLastError(0); return hProcess; } - PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + psutil_PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); CloseHandle(hProcess); return NULL; } @@ -151,7 +151,7 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { hProcess = OpenProcess(access, FALSE, pid); if ((hProcess == NULL) && (GetLastError() == ERROR_ACCESS_DENIED)) { - PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); return NULL; } diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index 7e400a2541..07d2399849 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -21,7 +21,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { DWORD cbPrevious = sizeof(TOKEN_PRIVILEGES); if (! LookupPrivilegeValue(NULL, Privilege, &luid)) { - PyErr_SetFromOSErrnoWithSyscall("LookupPrivilegeValue"); + psutil_PyErr_SetFromOSErrnoWithSyscall("LookupPrivilegeValue"); return 1; } @@ -38,7 +38,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { &tpPrevious, &cbPrevious)) { - PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); + psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); return 1; } @@ -60,7 +60,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { NULL, NULL)) { - PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); + psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); return 1; } @@ -79,18 +79,18 @@ psutil_get_thisproc_token() { if (GetLastError() == ERROR_NO_TOKEN) { if (! ImpersonateSelf(SecurityImpersonation)) { - PyErr_SetFromOSErrnoWithSyscall("ImpersonateSelf"); + psutil_PyErr_SetFromOSErrnoWithSyscall("ImpersonateSelf"); return NULL; } if (! OpenProcessToken( me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { - PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); return NULL; } } else { - PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); return NULL; } } diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index fa3e646e51..4931e9d66d 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -24,12 +24,12 @@ psutil_get_service_handler(char *service_name, DWORD scm_access, DWORD access) sc = OpenSCManager(NULL, NULL, scm_access); if (sc == NULL) { - PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); return NULL; } hService = OpenService(sc, service_name, access); if (hService == NULL) { - PyErr_SetFromOSErrnoWithSyscall("OpenService"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenService"); CloseServiceHandle(sc); return NULL; } @@ -113,7 +113,7 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { sc = OpenSCManager(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE); if (sc == NULL) { - PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); return NULL; } @@ -211,13 +211,13 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { bytesNeeded = 0; QueryServiceConfigW(hService, NULL, 0, &bytesNeeded); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); + psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); goto error; } qsc = (QUERY_SERVICE_CONFIGW *)malloc(bytesNeeded); ok = QueryServiceConfigW(hService, qsc, bytesNeeded, &bytesNeeded); if (ok == 0) { - PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); + psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); goto error; } @@ -303,7 +303,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { return Py_BuildValue("s", ""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); + psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); goto error; } ssp = (SERVICE_STATUS_PROCESS *)HeapAlloc( @@ -317,7 +317,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { ok = QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)ssp, bytesNeeded, &bytesNeeded); if (ok == 0) { - PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); + psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); goto error; } @@ -375,7 +375,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { return Py_BuildValue("s", ""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); + psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); goto error; } @@ -383,7 +383,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { ok = QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, (LPBYTE)scd, bytesNeeded, &bytesNeeded); if (ok == 0) { - PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); + psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); goto error; } @@ -429,7 +429,7 @@ psutil_winservice_start(PyObject *self, PyObject *args) { } ok = StartService(hService, 0, NULL); if (ok == 0) { - PyErr_SetFromOSErrnoWithSyscall("StartService"); + psutil_PyErr_SetFromOSErrnoWithSyscall("StartService"); goto error; } @@ -466,7 +466,7 @@ psutil_winservice_stop(PyObject *self, PyObject *args) { ok = ControlService(hService, SERVICE_CONTROL_STOP, &ssp); Py_END_ALLOW_THREADS if (ok == 0) { - PyErr_SetFromOSErrnoWithSyscall("ControlService"); + psutil_PyErr_SetFromOSErrnoWithSyscall("ControlService"); goto error; } diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index 3e12e71b72..ada684f6f9 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -70,7 +70,7 @@ psutil_users(PyObject *self, PyObject *args) { // On Windows Nano server, the Wtsapi32 API can be present, but return WinError 120. return py_retlist; } - PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); + psutil_PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); goto error; } @@ -93,7 +93,9 @@ psutil_users(PyObject *self, PyObject *args) { bytes = 0; if (WTSQuerySessionInformationW(hServer, sessionId, WTSUserName, &buffer_user, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); + psutil_PyErr_SetFromOSErrnoWithSyscall( + "WTSQuerySessionInformationW" + ); goto error; } if (bytes <= 2) @@ -103,7 +105,9 @@ psutil_users(PyObject *self, PyObject *args) { bytes = 0; if (WTSQuerySessionInformationW(hServer, sessionId, WTSClientAddress, &buffer_addr, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); + psutil_PyErr_SetFromOSErrnoWithSyscall( + "WTSQuerySessionInformationW" + ); goto error; } @@ -130,7 +134,9 @@ psutil_users(PyObject *self, PyObject *args) { bytes = 0; if (WTSQuerySessionInformationW(hServer, sessionId, WTSSessionInfo, &buffer_info, &bytes) == 0) { - PyErr_SetFromOSErrnoWithSyscall("WTSQuerySessionInformationW"); + psutil_PyErr_SetFromOSErrnoWithSyscall( + "WTSQuerySessionInformationW" + ); goto error; } wts_info = (PWTSINFOW)buffer_info; diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index fc7a66529e..2cf7e8a59b 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -80,7 +80,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { event = CreateEventW(NULL, FALSE, FALSE, L"LoadUpdateEvent"); if (event == NULL) { - PyErr_SetFromOSErrnoWithSyscall("CreateEventW"); + psutil_PyErr_SetFromOSErrnoWithSyscall("CreateEventW"); return NULL; } @@ -100,7 +100,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { WT_EXECUTEDEFAULT); if (ret == 0) { - PyErr_SetFromOSErrnoWithSyscall("RegisterWaitForSingleObject"); + psutil_PyErr_SetFromOSErrnoWithSyscall("RegisterWaitForSingleObject"); return NULL; } diff --git a/setup.py b/setup.py index 23e4f17c7a..a88239c7cf 100755 --- a/setup.py +++ b/setup.py @@ -224,7 +224,7 @@ def get_winver(): # see: https://github.com/giampaolo/psutil/issues/348 ('PSAPI_VERSION', 1), ]) - + if Py_GIL_DISABLED: macros.append(('Py_GIL_DISABLED', 1)) From 42c7a241c1caf8d723dc360422db937d6036b40f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 1 Oct 2024 22:06:10 +0200 Subject: [PATCH 1127/1714] Use pytest, get rid of test/runner.py (#2447) See https://github.com/giampaolo/psutil/issues/2446. This PR: * gets rid of [psutil/tests/runner.py](https://github.com/giampaolo/psutil/blob/622bd442eef704627202282d5805e4b39358b897/psutil/tests/runner.py#L1) (less code to maintain) * use `pytest-xdist` to allow for parallel test execution * get rid of [concurrencytest](https://pypi.org/project/concurrencytest/) dep * update Github and Appveyor CI config * removes 400 lines of code Replacing `self.assert*` APIs will be done in a separate PR. --- .github/workflows/bsd.yml | 29 ++- .github/workflows/build.yml | 14 +- MANIFEST.in | 1 - Makefile | 98 ++++---- appveyor.yml | 2 +- docs/DEVGUIDE.rst | 2 +- make.bat | 7 - psutil/_psaix.py | 2 +- psutil/_psbsd.py | 2 +- psutil/_pslinux.py | 2 +- psutil/_psosx.py | 2 +- psutil/_pssunos.py | 2 +- psutil/_pswindows.py | 2 +- psutil/tests/__init__.py | 27 +-- psutil/tests/runner.py | 385 ------------------------------- psutil/tests/test_aix.py | 6 - psutil/tests/test_bsd.py | 6 - psutil/tests/test_connections.py | 15 +- psutil/tests/test_contracts.py | 6 - psutil/tests/test_linux.py | 28 +-- psutil/tests/test_memleaks.py | 12 - psutil/tests/test_misc.py | 6 - psutil/tests/test_osx.py | 6 - psutil/tests/test_posix.py | 14 +- psutil/tests/test_process.py | 30 +-- psutil/tests/test_process_all.py | 13 +- psutil/tests/test_sunos.py | 6 - psutil/tests/test_system.py | 6 - psutil/tests/test_testutils.py | 15 +- psutil/tests/test_unicode.py | 11 +- psutil/tests/test_windows.py | 6 - pyproject.toml | 6 - scripts/internal/winmake.py | 75 ++++-- 33 files changed, 205 insertions(+), 639 deletions(-) delete mode 100755 psutil/tests/runner.py diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 8182177c0f..cba937d00a 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -1,15 +1,22 @@ -# # Execute tests on *BSD platforms. Does not produce wheels. -# # Useful URLs: -# # https://github.com/vmactions/freebsd-vm -# # https://github.com/vmactions/openbsd-vm -# # https://github.com/vmactions/netbsd-vm +# Execute tests on *BSD platforms. Does not produce wheels. +# Useful URLs: +# https://github.com/vmactions/freebsd-vm +# https://github.com/vmactions/openbsd-vm +# https://github.com/vmactions/netbsd-vm + +on: [push, pull_request] +name: bsd-tests +concurrency: + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} + cancel-in-progress: true +jobs: + # here just so that I can comment the next jobs to temporarily disable them + empty-job: + if: false + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 -# on: [push, pull_request] -# name: bsd-tests -# concurrency: -# group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} -# cancel-in-progress: true -# jobs: # freebsd: # runs-on: ubuntu-22.04 # steps: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29753ef82d..448f51eda1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ concurrency: jobs: # Linux + macOS + Windows Python 3 py3: - name: "py3-${{ matrix.os }}-${{ matrix.arch }}" + name: "py3, ${{ matrix.os }}, ${{ matrix.arch }}" runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: @@ -57,6 +57,9 @@ jobs: env: CIBW_ARCHS: "${{ matrix.arch }}" CIBW_PRERELEASE_PYTHONS: True + CIBW_TEST_EXTRAS: test + CIBW_TEST_COMMAND: + make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" setup-dev-env install print-sysinfo test test-memleaks - name: Upload wheels uses: actions/upload-artifact@v4 @@ -73,7 +76,7 @@ jobs: # Linux + macOS + Python 2 py2: - name: py2-${{ matrix.os }} + name: py2, ${{ matrix.os }} runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: @@ -81,11 +84,10 @@ jobs: matrix: os: [ubuntu-latest, macos-12] env: - CIBW_TEST_COMMAND: - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/runner.py && - PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 python {project}/psutil/tests/test_memleaks.py - CIBW_TEST_EXTRAS: test CIBW_BUILD: 'cp27-*' + CIBW_TEST_EXTRAS: test + CIBW_TEST_COMMAND: + make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" setup-dev-env install print-sysinfo test test-memleaks steps: - uses: actions/checkout@v4 diff --git a/MANIFEST.in b/MANIFEST.in index bb60aa849b..43ee1cda0c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -153,7 +153,6 @@ include psutil/arch/windows/wmi.h include psutil/tests/README.rst include psutil/tests/__init__.py include psutil/tests/__main__.py -include psutil/tests/runner.py include psutil/tests/test_aix.py include psutil/tests/test_bsd.py include psutil/tests/test_connections.py diff --git a/Makefile b/Makefile index 64fba604e2..555f418266 100644 --- a/Makefile +++ b/Makefile @@ -2,49 +2,64 @@ # To use a specific Python version run: "make install PYTHON=python3.3" # You can set the variables below from the command line. -# Configurable. PYTHON = python3 PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 +PYTEST_ARGS = -v -s --tb=short ARGS = -TSCRIPT = psutil/tests/runner.py -# Internal. +# mandatory deps for running tests PY3_DEPS = \ - black \ - check-manifest \ - concurrencytest \ - coverage \ - packaging \ - pylint \ - pyperf \ - pypinfo \ - requests \ - rstcheck \ - ruff \ setuptools \ - sphinx_rtd_theme \ - teyit \ - toml-sort \ - twine \ - virtualenv \ - wheel + pytest \ + pytest-xdist + +# deps for local development +ifndef CIBUILDWHEEL + PY3_DEPS += \ + black \ + check-manifest \ + coverage \ + packaging \ + pylint \ + pyperf \ + pypinfo \ + pytest-cov \ + requests \ + rstcheck \ + ruff \ + setuptools \ + sphinx_rtd_theme \ + toml-sort \ + twine \ + virtualenv \ + wheel +endif + +# python 2 deps PY2_DEPS = \ futures \ ipaddress \ - mock + mock==1.0.1 \ + pytest==4.6.11 \ + pytest-xdist \ + setuptools + PY_DEPS = `$(PYTHON) -c \ "import sys; \ py3 = sys.version_info[0] == 3; \ py38 = sys.version_info[:2] >= (3, 8); \ py3_extra = ' abi3audit' if py38 else ''; \ print('$(PY3_DEPS)' + py3_extra if py3 else '$(PY2_DEPS)')"` + NUM_WORKERS = `$(PYTHON) -c "import os; print(os.cpu_count() or 1)"` + # "python3 setup.py build" can be parallelized on Python >= 3.6. BUILD_OPTS = `$(PYTHON) -c \ "import sys, os; \ py36 = sys.version_info[:2] >= (3, 6); \ cpus = os.cpu_count() or 1 if py36 else 1; \ print('--parallel %s' % cpus if cpus > 1 else '')"` + # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` @@ -123,8 +138,8 @@ install-pip: ## Install pip (no-op if already installed). setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON_ENV_VARS) $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip - $(PYTHON_ENV_VARS) $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade $(PY_DEPS) + $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip + $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade $(PY_DEPS) # =================================================================== # Tests @@ -132,65 +147,65 @@ setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). test: ## Run all tests. To run a specific test do "make test ARGS=psutil.tests.test_system.TestDiskAPIs" ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) --ignore=psutil/tests/test_memleaks.py $(ARGS) test-parallel: ## Run all tests in parallel. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) --parallel + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) --ignore=psutil/tests/test_memleaks.py -n auto --dist loadgroup $(ARGS) test-process: ## Run process-related API tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_process.py test-process-all: ## Run tests which iterate over all process PIDs. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_process_all.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_process_all.py test-system: ## Run system-related API tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_system.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_system.py test-misc: ## Run miscellaneous tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_misc.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_misc.py test-testutils: ## Run test utils tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_testutils.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_testutils.py test-unicode: ## Test APIs dealing with strings. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_unicode.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_unicode.py test-contracts: ## APIs sanity tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_contracts.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_contracts.py test-connections: ## Test psutil.net_connections() and Process.net_connections(). ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_connections.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_connections.py test-posix: ## POSIX specific tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_posix.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_posix.py test-platform: ## Run specific platform tests only. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) psutil/tests/test_memleaks.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_memleaks.py test-last-failed: ## Re-run tests which failed on last run ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) $(TSCRIPT) $(ARGS) --last-failed + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) --last-failed $(ARGS) test-coverage: ## Run test coverage. ${MAKE} build # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov - $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m unittest -v + $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest $(PYTEST_ARGS) --ignore=psutil/tests/test_memleaks.py $(ARGS) $(PYTHON) -m coverage report @echo "writing results to htmlcov/index.html" $(PYTHON) -m coverage html @@ -235,16 +250,12 @@ fix-black: fix-ruff: @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --fix $(ARGS) -fix-unittests: ## Fix unittest idioms. - @git ls-files '*test_*.py' | xargs $(PYTHON) -m teyit --show-stats - fix-toml: ## Fix pyproject.toml @git ls-files '*.toml' | xargs toml-sort fix-all: ## Run all code fixers. ${MAKE} fix-ruff ${MAKE} fix-black - ${MAKE} fix-unittests ${MAKE} fix-toml # =================================================================== @@ -351,6 +362,9 @@ print-downloads: ## Print PYPI download statistics print-hashes: ## Prints hashes of files in dist/ directory $(PYTHON) scripts/internal/print_hashes.py dist/ +print-sysinfo: ## Prints system info + $(PYTHON) -c "from psutil.tests import print_sysinfo; print_sysinfo()" + # =================================================================== # Misc # =================================================================== diff --git a/appveyor.yml b/appveyor.yml index 70a4daec25..13798581eb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -36,7 +36,6 @@ init: install: - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - - "%WITH_COMPILER% %PYTHON%/python.exe -m pip install --upgrade --user setuptools pip" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py setup-dev-env" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install" @@ -44,6 +43,7 @@ install: build: off test_script: + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py print-sysinfo" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test-memleaks" diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 744ddadf4b..6f552081c7 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -44,7 +44,7 @@ Once you have a compiler installed run: .. code-block:: bash - make test TSCRIPT=test_script.py # on UNIX + make test ARGS=test_script.py # on UNIX make test test_script.py # on Windows - Do not use ``sudo``. ``make install`` installs psutil as a limited user in diff --git a/make.bat b/make.bat index 2ac53d9ec2..ec4fc591ef 100644 --- a/make.bat +++ b/make.bat @@ -16,19 +16,12 @@ rem ...therefore it might not work on your Windows installation. rem rem To compile for a specific Python version run: rem set PYTHON=C:\Python34\python.exe & make.bat build -rem -rem To use a different test script: -rem set TSCRIPT=foo.py & make.bat test rem ========================================================================== if "%PYTHON%" == "" ( set PYTHON=python ) -if "%TSCRIPT%" == "" ( - set TSCRIPT=psutil\tests\runner.py -) - rem Needed to locate the .pypirc file and upload exes on PyPI. set HOME=%USERPROFILE% set PSUTIL_DEBUG=1 diff --git a/psutil/_psaix.py b/psutil/_psaix.py index f48425eb8a..0904449b3d 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -349,7 +349,7 @@ def wrapper(self, *args, **kwargs): class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] def __init__(self, pid): self.pid = pid diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index cf84207e79..deffe50b09 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -640,7 +640,7 @@ def wrap_exceptions_procfs(inst): class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_cache"] + __slots__ = ["_cache", "_name", "_ppid", "pid"] def __init__(self, pid): self.pid = pid diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 1671838815..7dfa1430ac 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1732,7 +1732,7 @@ def wrapper(self, *args, **kwargs): class Process: """Linux process implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] def __init__(self, pid): self.pid = pid diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 41263fd735..ed9b319de7 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -361,7 +361,7 @@ def wrapper(self, *args, **kwargs): class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_cache"] + __slots__ = ["_cache", "_name", "_ppid", "pid"] def __init__(self, pid): self.pid = pid diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 1c0b96e9e9..5536d35079 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -387,7 +387,7 @@ def wrapper(self, *args, **kwargs): class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] def __init__(self, pid): self.pid = pid diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 0ba511b901..7eb01b7e2c 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -762,7 +762,7 @@ def wrapper(self, *args, **kwargs): class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_cache"] + __slots__ = ["_cache", "_name", "_ppid", "pid"] def __init__(self, pid): self.pid = pid diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index dccb9e4b28..faa455bbd2 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -14,7 +14,6 @@ import errno import functools import gc -import inspect import os import platform import random @@ -87,7 +86,7 @@ "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", - "MACOS_12PLUS", "COVERAGE", 'AARCH64', "QEMU_USER", + "MACOS_12PLUS", "COVERAGE", 'AARCH64', "QEMU_USER", "PYTEST_PARALLEL", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', 'spawn_children_pair', @@ -128,6 +127,7 @@ GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ CI_TESTING = APPVEYOR or GITHUB_ACTIONS COVERAGE = 'COVERAGE_RUN' in os.environ +PYTEST_PARALLEL = "PYTEST_XDIST_WORKER" in os.environ # `make test-parallel` if LINUX and GITHUB_ACTIONS: with open('/proc/1/cmdline') as f: QEMU_USER = "/bin/qemu-" in f.read() @@ -521,7 +521,7 @@ def sh(cmd, **kwds): else: stdout, stderr = p.communicate() if p.returncode != 0: - raise RuntimeError(stderr) + raise RuntimeError(stdout + stderr) if stderr: warn(stderr) if stdout.endswith('\n'): @@ -1355,11 +1355,12 @@ def print_sysinfo(): 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 + # if WINDOWS: + # os.system("tasklist") + # elif which("ps"): + # os.system("ps aux") + # print("=" * 70, file=sys.stderr) # NOQA + sys.stdout.flush() @@ -1598,16 +1599,6 @@ def iter(ls): 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. diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py deleted file mode 100755 index 3b28b64f1c..0000000000 --- a/psutil/tests/runner.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/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/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index e7e0c8aa51..8e6121570d 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -129,9 +129,3 @@ def test_net_if_addrs_names(self): 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/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 8512b4f9ee..6112c11e17 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -624,9 +624,3 @@ def test_cpu_stats_ctx_switches(self): 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/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 4a0674d620..e1a24563d2 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -16,6 +16,8 @@ from socket import SOCK_DGRAM from socket import SOCK_STREAM +import pytest + import psutil from psutil import FREEBSD from psutil import LINUX @@ -38,7 +40,6 @@ from psutil.tests import filter_proc_net_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 @@ -55,7 +56,7 @@ def this_proc_net_connections(kind): return cons -@serialrun +@pytest.mark.xdist_group(name="serial") class ConnectionTestCase(PsutilTestCase): def setUp(self): self.assertEqual(this_proc_net_connections(kind='all'), []) @@ -102,7 +103,7 @@ def test_invalid_kind(self): self.assertRaises(ValueError, psutil.net_connections, kind='???') -@serialrun +@pytest.mark.xdist_group(name="serial") class TestUnconnectedSockets(ConnectionTestCase): """Tests sockets which are open but not connected to anything.""" @@ -198,7 +199,7 @@ def test_unix_udp(self): self.assertEqual(conn.status, psutil.CONN_NONE) -@serialrun +@pytest.mark.xdist_group(name="serial") class TestConnectedSocket(ConnectionTestCase): """Test socket pairs which are actually connected to each other. @@ -568,9 +569,3 @@ def test_net_connection_constants(self): 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/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index a5469ac8a2..9154c5c70f 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -341,9 +341,3 @@ def test_negative_signal(self): 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/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d8faac9684..e0c361a049 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -36,6 +36,7 @@ from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_RLIMIT from psutil.tests import PYPY +from psutil.tests import PYTEST_PARALLEL from psutil.tests import QEMU_USER from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM @@ -980,10 +981,12 @@ def open_mock(name, *args, **kwargs): @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) + + # XXX: fails too often. + # 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") @@ -1603,6 +1606,7 @@ def test_procfs_path(self): psutil.PROCFS_PATH = "/proc" @retry_on_failure() + @unittest.skipIf(PYTEST_PARALLEL, "skip if pytest-parallel") def test_issue_687(self): # In case of thread ID: # - pid_exists() is supposed to return False @@ -2126,9 +2130,13 @@ 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, "") + # de-activate guessing from cmdline() + with mock.patch( + 'psutil._pslinux.Process.cmdline', return_value=[] + ): + 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 @@ -2353,9 +2361,3 @@ 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/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 6506497c9c..8ef108e1a9 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -19,7 +19,6 @@ import functools import os import platform -import sys import unittest import psutil @@ -492,14 +491,3 @@ def test_win_service_get_status(self): 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 cprint - from psutil.tests.runner import run_from_name - - if QEMU_USER: - cprint("skipping %s tests under QEMU_USER" % __file__, "brown") - sys.exit(0) - - run_from_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 5c05d17643..66d28b220f 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -1069,9 +1069,3 @@ def test_battery(self): @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/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 1fe02d3e41..a45b3853d6 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -204,9 +204,3 @@ def test_sensors_battery(self): 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/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 941f0fac12..79c7252d0c 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -23,6 +23,7 @@ from psutil import OPENBSD from psutil import POSIX from psutil import SUNOS +from psutil.tests import AARCH64 from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import PYTHON_EXE from psutil.tests import QEMU_USER @@ -288,7 +289,10 @@ def test_exe(self): def test_cmdline(self): ps_cmdline = ps_args(self.pid) psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline()) - self.assertEqual(ps_cmdline, psutil_cmdline) + if AARCH64 and len(ps_cmdline) < len(psutil_cmdline): + self.assertTrue(psutil_cmdline.startswith(ps_cmdline)) + else: + 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 @@ -315,7 +319,7 @@ def test_pids(self): pids_psutil = psutil.pids() # on MACOS and OPENBSD ps doesn't show pid 0 - if MACOS or OPENBSD and 0 not in pids_ps: + 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 @@ -490,9 +494,3 @@ def test_getpagesize(self): 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/psutil/tests/test_process.py b/psutil/tests/test_process.py index 363474c78a..33419de1bf 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -312,8 +312,13 @@ def test_create_time(self): def test_terminal(self): terminal = psutil.Process().terminal() if terminal is not None: - tty = os.path.realpath(sh('tty')) - self.assertEqual(terminal, tty) + try: + tty = os.path.realpath(sh('tty')) + except RuntimeError: + # Note: happens if pytest is run without the `-s` opt. + raise unittest.SkipTest("can't rely on `tty` CLI") + else: + self.assertEqual(terminal, tty) @unittest.skipIf(not HAS_PROC_IO_COUNTERS, 'not supported') @skip_on_not_implemented(only_if=LINUX) @@ -1498,13 +1503,16 @@ def test_pid_0(self): @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) + exclude = ["PLAT", "HOME", "PYTEST_CURRENT_TEST", "PYTEST_VERSION"] if MACOS: - d.pop("__CF_USER_TEXT_ENCODING", None) - d.pop("VERSIONER_PYTHON_PREFER_32_BIT", None) - d.pop("VERSIONER_PYTHON_VERSION", None) + exclude.extend([ + "__CF_USER_TEXT_ENCODING", + "VERSIONER_PYTHON_PREFER_32_BIT", + "VERSIONER_PYTHON_VERSION", + "VERSIONER_PYTHON_VERSION", + ]) + for name in exclude: + d.pop(name, None) return dict([ ( k.replace("\r", "").replace("\n", ""), @@ -1706,9 +1714,3 @@ def test_kill_terminate(self): 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/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 48833a105c..1a55b87ac0 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -32,6 +32,7 @@ from psutil._compat import long from psutil._compat import unicode from psutil.tests import CI_TESTING +from psutil.tests import PYTEST_PARALLEL from psutil.tests import QEMU_USER from psutil.tests import VALID_PROC_STATUSES from psutil.tests import PsutilTestCase @@ -40,12 +41,11 @@ 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 +USE_PROC_POOL = LINUX and not CI_TESTING and not PYTEST_PARALLEL def proc_info(pid): @@ -97,7 +97,6 @@ def 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. @@ -321,7 +320,7 @@ def memory_full_info(self, ret, info): 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'): + 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 @@ -541,9 +540,3 @@ def check(pid): continue with self.subTest(pid=pid): check(pid) - - -if __name__ == '__main__': - from psutil.tests.runner import run_from_name - - run_from_name(__file__) diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index 5483522816..d7505f8085 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -37,9 +37,3 @@ def test_swap_memory(self): 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/psutil/tests/test_system.py b/psutil/tests/test_system.py index e228f6d32c..c6854699ab 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -980,9 +980,3 @@ def test_sensors_fans(self): 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/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index ef98a3abe9..f7ce52dd60 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -16,6 +16,8 @@ import subprocess import unittest +import pytest + import psutil import psutil.tests from psutil import FREEBSD @@ -46,7 +48,6 @@ 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 @@ -361,7 +362,7 @@ def test_create_sockets(self): self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2) -@serialrun +@pytest.mark.xdist_group(name="serial") class TestMemLeakClass(TestMemoryLeak): @retry_on_failure() def test_times(self): @@ -389,9 +390,9 @@ def fun(ls=ls): ls.append("x" * 124 * 1024) try: - # will consume around 30M in total + # will consume around 60M in total self.assertRaisesRegex( - AssertionError, "extra-mem", self.execute, fun, times=50 + AssertionError, "extra-mem", self.execute, fun, times=100 ) finally: del ls @@ -452,9 +453,3 @@ 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/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 45aeb4e926..af32b56d1d 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -79,6 +79,8 @@ import warnings from contextlib import closing +import pytest + import psutil from psutil import BSD from psutil import POSIX @@ -103,7 +105,6 @@ 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 @@ -178,7 +179,7 @@ def setUp(self): raise unittest.SkipTest("can't handle unicode str") -@serialrun +@pytest.mark.xdist_group(name="serial") @unittest.skipIf(ASCII_FS, "ASCII fs") @unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2") class TestFSAPIs(BaseUnicodeTest): @@ -368,9 +369,3 @@ def test_proc_environ(self): 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/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 7778cf4a66..592705f959 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -959,9 +959,3 @@ def test_win_service_get(self): 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/pyproject.toml b/pyproject.toml index 04ad6414d2..e1cde1b33c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,6 @@ ignore = [ ".github/workflows/*" = ["T201", "T203"] "psutil/_compat.py" = ["PLW0127"] # self-assigning-variable "psutil/tests/*" = ["EM101", "TRY003"] -"psutil/tests/runner.py" = ["T201", "T203"] "scripts/*" = ["T201", "T203"] "scripts/internal/*" = ["EM101", "T201", "T203", "TRY003"] "setup.py" = ["T201", "T203"] @@ -212,11 +211,6 @@ skip = [ "cp3{7,8,9,10,11,12}-*linux_{aarch64,ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures "pp*", ] -test-command = [ - "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/runner.py", - "env PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_SCRIPTS_DIR={project}/scripts python {project}/psutil/tests/test_memleaks.py", -] -test-extras = "test" [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 20126ba7cc..c8d3a0e880 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -29,27 +29,49 @@ APPVEYOR = bool(os.environ.get('APPVEYOR')) PYTHON = sys.executable if APPVEYOR else os.getenv('PYTHON', sys.executable) -RUNNER_PY = 'psutil\\tests\\runner.py' -GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" PY3 = sys.version_info[0] >= 3 +PYTEST_ARGS = "-v -s --tb=short" +if PY3: + PYTEST_ARGS += "-o " HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) PYPY = '__pypy__' in sys.builtin_module_names -DEPS = [ - "coverage", - "pdbpp", - "pip", - "pyperf", - "pyreadline", - "requests", - "setuptools", - "wheel", -] - -if sys.version_info[0] < 3: - DEPS.append('mock') - DEPS.append('ipaddress') - DEPS.append('enum34') +if PY3: + GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" +else: + GET_PIP_URL = "https://bootstrap.pypa.io/pip/2.7/get-pip.py" + + +# mandatory deps +if PY3: + DEPS = [ + "setuptools", + "pytest", + "pytest-xdist", + "wheel", + ] +else: + DEPS = [ + "enum34", + "futures", + "ipaddress", + "mock==1.0.1", + "pytest-xdist", + "pytest==4.6.11", + "setuptools", + "wheel", + ] + +# deps for local development +if not APPVEYOR: + DEPS += [ + "coverage", + "pdbpp", + "pyperf", + "pyreadline", + "requests", + "wheel", + ] if not PYPY: DEPS.append("pywin32") @@ -375,17 +397,17 @@ def setup_dev_env(): sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEPS))) -def test(name=RUNNER_PY): +def test(args=""): """Run tests.""" build() - sh("%s %s" % (PYTHON, name)) + sh("%s -m pytest %s %s" % (PYTHON, PYTEST_ARGS, args)) def coverage(): """Run coverage tests.""" # Note: coverage options are controlled by .coveragerc file build() - sh("%s -m coverage run %s" % (PYTHON, RUNNER_PY)) + sh("%s -m coverage run -m pytest %s" % (PYTHON, PYTEST_ARGS)) sh("%s -m coverage report" % PYTHON) sh("%s -m coverage html" % PYTHON) sh("%s -m webbrowser -t htmlcov/index.html" % PYTHON) @@ -448,13 +470,13 @@ def test_testutils(): def test_by_name(name): """Run test by name.""" build() - sh("%s -m unittest -v %s" % (PYTHON, name)) + test(name) def test_last_failed(): """Re-run tests which failed on last run.""" build() - sh("%s %s --last-failed" % (PYTHON, RUNNER_PY)) + test("--last-failed") def test_memleaks(): @@ -499,6 +521,14 @@ def print_api_speed(): sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) +def print_sysinfo(): + """Print system info.""" + build() + from psutil.tests import print_sysinfo + + print_sysinfo() + + def download_appveyor_wheels(): """Download appveyor wheels.""" sh( @@ -557,6 +587,7 @@ def parse_args(): sp.add_parser('install-pip', help="install pip") sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") + sp.add_parser('print-sysinfo', help="print system info") sp.add_parser('setup-dev-env', help="install deps") test = sp.add_parser('test', help="[ARG] run tests") test_by_name = sp.add_parser('test-by-name', help=" run test by name") From 3aff71de5dd3d12d303c18728b6cc8f4760aab16 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Oct 2024 16:18:40 +0200 Subject: [PATCH 1128/1714] Add `make install-sysdeps` target + re-enable BSD builds (#2448) * Add make install-sysdeps target. I want a standardized way to satisfy all system deps from a central point across all UNIX platforms. From now on CI config files should always rely on make install-sysdeps. * Rename setup-dev-env make target to install-pydeps (these are deps intended for running tests) * Re-enable BSD builds --- .github/workflows/bsd.yml | 94 +++++++++++------------------ .github/workflows/build.yml | 4 +- HISTORY.rst | 6 ++ INSTALL.rst | 40 +++++++++--- MANIFEST.in | 1 + Makefile | 47 +++++++++------ appveyor.yml | 2 +- docs/DEVGUIDE.rst | 4 +- docs/Makefile | 4 +- psutil/tests/README.rst | 2 +- psutil/tests/test_windows.py | 2 +- pyproject.toml | 3 - scripts/internal/install-sysdeps.sh | 62 +++++++++++++++++++ scripts/internal/winmake.py | 4 +- 14 files changed, 175 insertions(+), 100 deletions(-) create mode 100755 scripts/internal/install-sysdeps.sh diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index cba937d00a..433e1d7780 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -10,65 +10,41 @@ concurrency: group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} cancel-in-progress: true jobs: - # here just so that I can comment the next jobs to temporarily disable them - empty-job: - if: false - runs-on: ubuntu-22.04 + freebsd: + # if: false + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Run tests + uses: vmactions/freebsd-vm@v1 + with: + usesh: true + run: | + scripts/internal/install-sysdeps.sh + PIP_BREAK_SYSTEM_PACKAGES=1 gmake install-pydeps install print-sysinfo test test-memleaks -# freebsd: -# runs-on: ubuntu-22.04 -# steps: -# - uses: actions/checkout@v4 -# - name: Run tests -# uses: vmactions/freebsd-vm@v1 -# with: -# usesh: true -# prepare: | -# pkg install -y gcc python3 -# run: | -# set -e -x -# make install-pip -# python3 -m pip install --user setuptools -# make install -# make test -# make test-memleaks -# openbsd: -# runs-on: ubuntu-22.04 -# steps: -# - uses: actions/checkout@v4 -# - name: Run tests -# uses: vmactions/openbsd-vm@v1 -# with: -# usesh: true -# prepare: | -# set -e -# pkg_add gcc python3 -# run: | -# set -e -# make install-pip -# python3 -m pip install --user setuptools -# make install -# make test -# make test-memleaks -# netbsd: -# runs-on: ubuntu-22.04 -# steps: -# - uses: actions/checkout@v4 -# - name: Run tests -# uses: vmactions/netbsd-vm@v1 -# with: -# usesh: true -# prepare: | -# set -e -# /usr/sbin/pkg_add -v pkgin -# pkgin update -# pkgin -y install python311-* py311-setuptools-* gcc12-* -# run: | -# set -e -# make install-pip PYTHON=python3.11 -# python3.11 -m pip install --user setuptools -# make install PYTHON=python3.11 -# make test PYTHON=python3.11 -# make test-memleaks PYTHON=python3.11 + openbsd: + # if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests + uses: vmactions/openbsd-vm@v1 + with: + usesh: true + run: | + scripts/internal/install-sysdeps.sh + PIP_BREAK_SYSTEM_PACKAGES=1 gmake install-pydeps install print-sysinfo test test-memleaks + + netbsd: + # if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests + uses: vmactions/netbsd-vm@v1 + with: + usesh: true + run: | + scripts/internal/install-sysdeps.sh + PIP_BREAK_SYSTEM_PACKAGES=1 gmake PYTHON=python3.11 install-pydeps install print-sysinfo test test-memleaks diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 448f51eda1..b2bc54b889 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,7 +59,7 @@ jobs: CIBW_PRERELEASE_PYTHONS: True CIBW_TEST_EXTRAS: test CIBW_TEST_COMMAND: - make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" setup-dev-env install print-sysinfo test test-memleaks + make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps install print-sysinfo test test-memleaks - name: Upload wheels uses: actions/upload-artifact@v4 @@ -87,7 +87,7 @@ jobs: CIBW_BUILD: 'cp27-*' CIBW_TEST_EXTRAS: test CIBW_TEST_COMMAND: - make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" setup-dev-env install print-sysinfo test test-memleaks + make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps install print-sysinfo test test-memleaks steps: - uses: actions/checkout@v4 diff --git a/HISTORY.rst b/HISTORY.rst index 2aa985bfe1..7741888ffc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,12 @@ XXXX-XX-XX - 2427_: psutil (segfault) on import in the free-threaded (no GIL) version of Python 3.13. (patch by Sam Gross) +**Enhancements** + +- 2448_: add ``make install-sysdeps`` target to install all necessary system + dependencies (python-dev, gcc, etc.) on different UNIX flavors. Also rename + ``make setup-dev-env`` to ``make install-pydeps``. + 6.0.0 ====== diff --git a/INSTALL.rst b/INSTALL.rst index e3dab9cb41..c0812c74d1 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -12,10 +12,36 @@ install a C compiler. All you have to do is:: If wheels are not available for your platform or architecture, or you wish to build & install psutil from sources, keep reading. -Linux (build) -------------- +Compile psutil from sources +=========================== + +UNIX +---- + +On all UNIX systems you can use the +`install-sysdeps.sh `__ +script to install the system dependencies necessary to compile psutil. You can +invoke this script from the Makefile as:: + + make install-sysdeps + +If you're on a BSD platform you need to use ``gmake`` instead of ``make``:: + + gmake install-sysdeps + +After system deps are installed you can build & compile psutil with:: -Ubuntu / Debian:: + make build + make install + +...or this, which will fetch the latest source distribution from `PyPI `__:: + + pip install --no-binary :all: psutil + +Linux +----- + +Debian / Ubuntu:: sudo apt-get install gcc python3-dev pip install --no-binary :all: psutil @@ -30,10 +56,10 @@ Alpine:: sudo apk add gcc python3-dev musl-dev linux-headers pip install --no-binary :all: psutil -Windows (build) ---------------- +Windows +------- -In order to install psutil from sources on Windows you need Visual Studio +In order to build / install psutil from sources on Windows you need Visual Studio (MinGW is not supported). Here's a couple of guides describing how to do it: `link `__ and `link `__. @@ -96,7 +122,7 @@ Install pip ----------- Pip is shipped by default with Python 2.7.9+ and 3.4+. -If you don't have pip you can install with wget:: +If you don't have pip you can install it with wget:: wget https://bootstrap.pypa.io/get-pip.py -O - | python3 diff --git a/MANIFEST.in b/MANIFEST.in index 43ee1cda0c..3a381cce12 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -187,6 +187,7 @@ include scripts/internal/download_wheels_appveyor.py include scripts/internal/download_wheels_github.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py +include scripts/internal/install-sysdeps.sh include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py diff --git a/Makefile b/Makefile index 555f418266..b5733a20ae 100644 --- a/Makefile +++ b/Makefile @@ -14,25 +14,28 @@ PY3_DEPS = \ pytest-xdist # deps for local development -ifndef CIBUILDWHEEL - PY3_DEPS += \ - black \ - check-manifest \ - coverage \ - packaging \ - pylint \ - pyperf \ - pypinfo \ - pytest-cov \ - requests \ - rstcheck \ - ruff \ - setuptools \ - sphinx_rtd_theme \ - toml-sort \ - twine \ - virtualenv \ - wheel +ifdef LINUX + ifndef CIBUILDWHEEL + PY3_DEPS += \ + black \ + check-manifest \ + coverage \ + packaging \ + pylint \ + pyperf \ + pypinfo \ + pytest-cov \ + requests \ + rstcheck \ + ruff \ + setuptools \ + sphinx \ + sphinx_rtd_theme \ + toml-sort \ + twine \ + virtualenv \ + wheel + endif endif # python 2 deps @@ -117,6 +120,7 @@ uninstall: ## Uninstall this package via pip. install-pip: ## Install pip (no-op if already installed). @$(PYTHON) -c \ "import sys, ssl, os, pkgutil, tempfile, atexit; \ + print('pip already installed') if pkgutil.find_loader('pip') else None; \ sys.exit(0) if pkgutil.find_loader('pip') else None; \ PY3 = sys.version_info[0] == 3; \ pyexc = 'from urllib.request import urlopen' if PY3 else 'from urllib2 import urlopen'; \ @@ -135,7 +139,10 @@ install-pip: ## Install pip (no-op if already installed). f.close(); \ sys.exit(code);" -setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). +install-sysdeps: + scripts/internal/install-sysdeps.sh + +install-pydeps: ## Install GIT hooks, pip, test deps (also upgrades them). ${MAKE} install-git-hooks ${MAKE} install-pip $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip diff --git a/appveyor.yml b/appveyor.yml index 13798581eb..553dbf9cd5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -36,7 +36,7 @@ init: install: - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py setup-dev-env" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install-pydeps" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install" diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 6f552081c7..4a0b43aef3 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -12,7 +12,7 @@ Once you have a compiler installed run: .. code-block:: bash git clone git@github.com:giampaolo/psutil.git - make setup-dev-env # install useful dev libs (ruff, coverage, ...) + make install-pydeps # install useful dev libs (ruff, coverage, ...) make build make install make test @@ -118,7 +118,7 @@ Documentation ------------- - doc source code is written in a single file: ``docs/index.rst``. -- doc can be built with ``make setup-dev-env; cd docs; make html``. +- doc can be built with ``make install-pydeps; cd docs; make html``. - public doc is hosted at https://psutil.readthedocs.io. .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS diff --git a/docs/Makefile b/docs/Makefile index 860a2b0e2f..be7e058b02 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -229,6 +229,6 @@ dummy: @echo @echo "Build finished. Dummy builder generates no files." -.PHONY: setup-dev-env -setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). +.PHONY: install-pydeps +install-pydeps: ## Install GIT hooks, pip, test deps (also upgrades them). $(PYTHON) -m pip install --user --upgrade --trusted-host files.pythonhosted.org $(DEPS) diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index 9dca118674..a186581e34 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -9,6 +9,6 @@ Instructions for running tests As a "developer", if you have a copy of the source code and you wish to hack on psutil:: - make setup-dev-env # install missing third-party deps + make install-pydeps # install missing third-party deps make test # serial run make test-parallel # parallel run diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 592705f959..113ff0d556 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -47,7 +47,7 @@ import win32api # requires "pip install pywin32" import win32con import win32process - import wmi # requires "pip install wmi" / "make setup-dev-env" + import wmi # requires "pip install wmi" / "make install-pydeps" if WINDOWS: from psutil._pswindows import convert_oserror diff --git a/pyproject.toml b/pyproject.toml index e1cde1b33c..881a1c718f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -215,9 +215,6 @@ skip = [ [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] -[tool.cibuildwheel.linux] -before-all = "yum install -y net-tools" - [build-system] build-backend = "setuptools.build_meta" requires = ["setuptools>=43", "wheel"] diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh new file mode 100755 index 0000000000..9894722f09 --- /dev/null +++ b/scripts/internal/install-sysdeps.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +# Depending on the UNIX platform, install the necessary system dependencies to: +# * compile psutil +# * run those unit tests that rely on CLI tools (netstat, ps, etc.) +# NOTE: this script MUST be kept compatible with the `sh` shell. + +set -e + +UNAME_S=$(uname -s) + +case "$UNAME_S" in + Linux) + LINUX=true + if command -v apt > /dev/null 2>&1; then + HAS_APT=true + elif command -v yum > /dev/null 2>&1; then + HAS_YUM=true + elif command -v apk > /dev/null 2>&1; then + HAS_APK=true # musl linux + fi + ;; + FreeBSD) + FREEBSD=true + ;; + NetBSD) + NETBSD=true + ;; + OpenBSD) + OPENBSD=true + ;; +esac + +# Check if running as root +if [ "$(id -u)" -ne 0 ]; then + SUDO=sudo +fi + +# Function to install system dependencies +main() { + if [ $HAS_APT ]; then + $SUDO apt-get install -y python3-dev gcc + $SUDO apt-get install -y net-tools coreutils util-linux # for tests + elif [ $HAS_YUM ]; then + $SUDO yum install -y python3-devel gcc + $SUDO yum install -y net-tools coreutils util-linux # for tests + elif [ $HAS_APK ]; then + $SUDO apk add python3-dev gcc musl-dev linux-headers coreutils procps + elif [ $FREEBSD ]; then + $SUDO pkg install -y gmake python3 gcc + elif [ $NETBSD ]; then + $SUDO /usr/sbin/pkg_add -v pkgin + $SUDO pkgin update + $SUDO pkgin -y install gmake python311-* gcc12-* + elif [ $OPENBSD ]; then + $SUDO pkg_add gmake gcc python3 + else + echo "Unsupported platform: $UNAME_S" + fi +} + +main diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c8d3a0e880..13fb57a72c 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -390,7 +390,7 @@ def clean(): safe_rmtree("tmp") -def setup_dev_env(): +def install_pydeps(): """Install useful deps.""" install_pip() install_git_hooks() @@ -588,7 +588,7 @@ def parse_args(): sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") sp.add_parser('print-sysinfo', help="print system info") - sp.add_parser('setup-dev-env', help="install deps") + sp.add_parser('install-pydeps', help="install python deps") test = sp.add_parser('test', help="[ARG] run tests") test_by_name = sp.add_parser('test-by-name', help=" run test by name") sp.add_parser('test-connections', help="run connections tests") From 04f19eea9b0d765a5c088754ca29ff1352c8db22 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Oct 2024 22:40:58 +0200 Subject: [PATCH 1129/1714] Provide `test` and `dev` python dependencies (#2449) --- .github/workflows/bsd.yml | 9 ++--- .github/workflows/build.yml | 4 +- HISTORY.rst | 15 ++++--- INSTALL.rst | 23 +++++------ Makefile | 59 +++++++++------------------ appveyor.yml | 4 +- docs/DEVGUIDE.rst | 46 ++++++++++++--------- psutil/tests/test_windows.py | 2 +- scripts/internal/install-sysdeps.sh | 6 +-- scripts/internal/winmake.py | 56 +++++++++---------------- setup.py | 63 +++++++++++++++++++++++------ 11 files changed, 144 insertions(+), 143 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 433e1d7780..5358412a3b 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -20,8 +20,7 @@ jobs: with: usesh: true run: | - scripts/internal/install-sysdeps.sh - PIP_BREAK_SYSTEM_PACKAGES=1 gmake install-pydeps install print-sysinfo test test-memleaks + PIP_BREAK_SYSTEM_PACKAGES=1 make install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks openbsd: # if: false @@ -33,8 +32,7 @@ jobs: with: usesh: true run: | - scripts/internal/install-sysdeps.sh - PIP_BREAK_SYSTEM_PACKAGES=1 gmake install-pydeps install print-sysinfo test test-memleaks + PIP_BREAK_SYSTEM_PACKAGES=1 make install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks netbsd: # if: false @@ -46,5 +44,4 @@ jobs: with: usesh: true run: | - scripts/internal/install-sysdeps.sh - PIP_BREAK_SYSTEM_PACKAGES=1 gmake PYTHON=python3.11 install-pydeps install print-sysinfo test test-memleaks + PIP_BREAK_SYSTEM_PACKAGES=1 make PYTHON=python3.11 install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2bc54b889..356f4d0807 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,7 +59,7 @@ jobs: CIBW_PRERELEASE_PYTHONS: True CIBW_TEST_EXTRAS: test CIBW_TEST_COMMAND: - make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps install print-sysinfo test test-memleaks + make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks - name: Upload wheels uses: actions/upload-artifact@v4 @@ -87,7 +87,7 @@ jobs: CIBW_BUILD: 'cp27-*' CIBW_TEST_EXTRAS: test CIBW_TEST_COMMAND: - make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps install print-sysinfo test test-memleaks + make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks steps: - uses: actions/checkout@v4 diff --git a/HISTORY.rst b/HISTORY.rst index 7741888ffc..b9ef61deeb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,17 +5,20 @@ XXXX-XX-XX +**Enhancements** + +- 2448_: add ``make install-sysdeps`` target to install the necessary system + dependencies (python-dev, gcc, etc.) on all supported UNIX flavors. +- 2449_: add ``make install-pydeps-test`` and ``make install-pydeps-dev`` + targets. They can be used to install dependencies meant for running tests and + for local development. They can also be installed via ``pip install .[test]`` + and ``pip install .[dev]``. + **Bug fixes** - 2427_: psutil (segfault) on import in the free-threaded (no GIL) version of Python 3.13. (patch by Sam Gross) -**Enhancements** - -- 2448_: add ``make install-sysdeps`` target to install all necessary system - dependencies (python-dev, gcc, etc.) on different UNIX flavors. Also rename - ``make setup-dev-env`` to ``make install-pydeps``. - 6.0.0 ====== diff --git a/INSTALL.rst b/INSTALL.rst index c0812c74d1..a425400fd7 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -18,18 +18,14 @@ Compile psutil from sources UNIX ---- -On all UNIX systems you can use the -`install-sysdeps.sh `__ -script to install the system dependencies necessary to compile psutil. You can -invoke this script from the Makefile as:: +On all UNIX systems you can use the `install-sysdeps.sh +`__ +script. This will install the system dependencies necessary to compile psutil +from sources. You can invoke this script from the Makefile as:: make install-sysdeps -If you're on a BSD platform you need to use ``gmake`` instead of ``make``:: - - gmake install-sysdeps - -After system deps are installed you can build & compile psutil with:: +After system deps are installed, you can compile & install psutil with:: make build make install @@ -59,11 +55,10 @@ Alpine:: Windows ------- -In order to build / install psutil from sources on Windows you need Visual Studio -(MinGW is not supported). -Here's a couple of guides describing how to do it: `link `__ -and `link `__. -Once VS is installed do:: +In order to build / install psutil from sources on Windows you need to install +`Visua Studio 2017 `__ +or later (see cPython `devguide `__'s instructions). +MinGW is not supported. Once Visual Studio is installed do:: pip install --no-binary :all: psutil diff --git a/Makefile b/Makefile index b5733a20ae..5fc72a5fbd 100644 --- a/Makefile +++ b/Makefile @@ -13,31 +13,6 @@ PY3_DEPS = \ pytest \ pytest-xdist -# deps for local development -ifdef LINUX - ifndef CIBUILDWHEEL - PY3_DEPS += \ - black \ - check-manifest \ - coverage \ - packaging \ - pylint \ - pyperf \ - pypinfo \ - pytest-cov \ - requests \ - rstcheck \ - ruff \ - setuptools \ - sphinx \ - sphinx_rtd_theme \ - toml-sort \ - twine \ - virtualenv \ - wheel - endif -endif - # python 2 deps PY2_DEPS = \ futures \ @@ -64,12 +39,16 @@ BUILD_OPTS = `$(PYTHON) -c \ print('--parallel %s' % cpus if cpus > 1 else '')"` # In not in a virtualenv, add --user options for install commands. -INSTALL_OPTS = `$(PYTHON) -c \ +SETUP_INSTALL_OPTS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` +PIP_INSTALL_OPTS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help +# install git hook +_ := $(shell mkdir -p .git/hooks/ && ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit) + # =================================================================== # Install # =================================================================== @@ -110,8 +89,7 @@ build: ## Compile (in parallel) without installing. install: ## Install this package as current user in "edit" mode. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(INSTALL_OPTS) - $(PYTHON_ENV_VARS) $(PYTHON) -c "import psutil" # make sure it actually worked + $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(SETUP_INSTALL_OPTS) uninstall: ## Uninstall this package via pip. cd ..; $(PYTHON_ENV_VARS) $(PYTHON) -m pip uninstall -y -v psutil || true @@ -140,13 +118,22 @@ install-pip: ## Install pip (no-op if already installed). sys.exit(code);" install-sysdeps: - scripts/internal/install-sysdeps.sh + ./scripts/internal/install-sysdeps.sh + +install-pydeps-test: ## Install python deps necessary to run unit tests. + ${MAKE} install-pip + $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) pip # upgrade pip to latest version + $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` -install-pydeps: ## Install GIT hooks, pip, test deps (also upgrades them). +install-pydeps-dev: ## Install python deps meant for local development. ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip - $(PYTHON) -m pip install $(INSTALL_OPTS) --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade $(PY_DEPS) + $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) pip # upgrade pip to latest version + $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS + setup.DEV_DEPS))"` + +install-git-hooks: ## Install GIT pre-commit hook. + ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit # =================================================================== # Tests @@ -265,14 +252,6 @@ fix-all: ## Run all code fixers. ${MAKE} fix-black ${MAKE} fix-toml -# =================================================================== -# GIT -# =================================================================== - -install-git-hooks: ## Install GIT pre-commit hook. - ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit - chmod +x .git/hooks/pre-commit - # =================================================================== # Distribution # =================================================================== diff --git a/appveyor.yml b/appveyor.yml index 553dbf9cd5..7488400d91 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -36,14 +36,14 @@ init: install: - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install-pydeps" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install-pydeps-test" - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install" + - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py print-sysinfo" build: off test_script: - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py print-sysinfo" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test" - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test-memleaks" diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 4a0b43aef3..456714603f 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -12,32 +12,36 @@ Once you have a compiler installed run: .. code-block:: bash git clone git@github.com:giampaolo/psutil.git - make install-pydeps # install useful dev libs (ruff, coverage, ...) + make install-sysdeps # install gcc and python headers + make install-pydeps-test # install python deps necessary to run unit tests make build make install make test -- ``make`` (see `Makefile`_) is the designated tool to run tests, build, install - try new features you're developing, etc. This also includes Windows (see - `make.bat`_ ). - Some useful commands are: +- ``make`` (and the accompanying `Makefile`_) is the designated tool to build, + install, run tests and do pretty much anything that involves development. + This also includes Windows, meaning that you can run `make command` similarly + as if you were on UNIX (see `make.bat`_ and `winmake.py`_). Some useful + commands are: .. code-block:: bash - make test-parallel # faster - make test-memleaks - make test-coverage - make lint-all # Run Python and C linter - make fix-all # Fix linting errors + make clean # remove build files + make install-pydeps-dev # install dev deps (ruff, black, ...) + make test # run tests + make test-parallel # run tests in parallel (faster) + make test-memleaks # run memory leak tests + make test-coverage # run test coverage + make lint-all # run linters + make fix-all # fix linters errors make uninstall make help - - To run a specific unit test: .. code-block:: bash - make test ARGS=psutil.tests.test_system.TestDiskAPIs + make test ARGS=psutil/tests/test_system.py::TestDiskAPIs::test_disk_usage - If you're working on a new feature and you wish to compile & test it "on the fly" from a test script, this is a quick & dirty way to do it: @@ -48,7 +52,8 @@ Once you have a compiler installed run: make test test_script.py # on Windows - Do not use ``sudo``. ``make install`` installs psutil as a limited user in - "edit" mode, meaning you can edit psutil code on the fly while you develop. + "edit" / development mode, meaning you can edit psutil code on the fly while + you develop. - If you want to target a specific Python version: @@ -60,10 +65,12 @@ Once you have a compiler installed run: Coding style ------------ -- Oython code strictly follows `PEP-8`_ styling guides and this is enforced by - a commit GIT hook installed via ``make install-git-hooks`` which will reject - commits if code is not PEP-8 complieant. -- C code should follow `PEP-7`_ styling guides. +- Python code strictly follows `PEP-8`_ styling guide. In addition we use + ``black`` and ``ruff`` code formatters / linters. This is enforced by a GIT + commit hook, installed via ``make install-git-hooks``, which will reject the + commit if code is not compliant. +- C code should follow `PEP-7`_ styling guides, but things are more relaxed. +- Linters are enforced also for ``.rst`` and ``.toml`` files. Code organization ----------------- @@ -101,7 +108,7 @@ Make a pull request - Fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") - Git clone the fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git`` -- Create a branch:``git checkout -b new-feature`` +- Create a branch: ``git checkout -b new-feature`` - Commit your changes: ``git commit -am 'add some feature'`` - Push the branch: ``git push origin new-feature`` - Create a new PR via the GitHub web interface and sign-off your work (see @@ -118,13 +125,14 @@ Documentation ------------- - doc source code is written in a single file: ``docs/index.rst``. -- doc can be built with ``make install-pydeps; cd docs; make html``. +- doc can be built with ``make install-pydeps-dev; cd docs; make html``. - public doc is hosted at https://psutil.readthedocs.io. .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst .. _`make.bat`: https://github.com/giampaolo/psutil/blob/master/make.bat +.. _`winmake.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/winmake.py .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile .. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ .. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 113ff0d556..935df40be1 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -47,7 +47,7 @@ import win32api # requires "pip install pywin32" import win32con import win32process - import wmi # requires "pip install wmi" / "make install-pydeps" + import wmi # requires "pip install wmi" / "make install-pydeps-test" if WINDOWS: from psutil._pswindows import convert_oserror diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 9894722f09..003a7d0817 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -47,13 +47,13 @@ main() { elif [ $HAS_APK ]; then $SUDO apk add python3-dev gcc musl-dev linux-headers coreutils procps elif [ $FREEBSD ]; then - $SUDO pkg install -y gmake python3 gcc + $SUDO pkg install -y python3 gcc elif [ $NETBSD ]; then $SUDO /usr/sbin/pkg_add -v pkgin $SUDO pkgin update - $SUDO pkgin -y install gmake python311-* gcc12-* + $SUDO pkgin -y install python311-* gcc12-* elif [ $OPENBSD ]; then - $SUDO pkg_add gmake gcc python3 + $SUDO pkg_add gcc python3 else echo "Unsupported platform: $UNAME_S" fi diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 13fb57a72c..3995a46f2f 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -36,46 +36,18 @@ HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) PYPY = '__pypy__' in sys.builtin_module_names +WINDOWS = os.name == "nt" if PY3: GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" else: GET_PIP_URL = "https://bootstrap.pypa.io/pip/2.7/get-pip.py" +sys.path.insert(0, ROOT_DIR) # so that we can import setup.py -# mandatory deps -if PY3: - DEPS = [ - "setuptools", - "pytest", - "pytest-xdist", - "wheel", - ] -else: - DEPS = [ - "enum34", - "futures", - "ipaddress", - "mock==1.0.1", - "pytest-xdist", - "pytest==4.6.11", - "setuptools", - "wheel", - ] - -# deps for local development -if not APPVEYOR: - DEPS += [ - "coverage", - "pdbpp", - "pyperf", - "pyreadline", - "requests", - "wheel", - ] - -if not PYPY: - DEPS.append("pywin32") - DEPS.append("wmi") +import setup # NOQA + +TEST_DEPS = setup.TEST_DEPS +DEV_DEPS = setup.DEV_DEPS _cmds = {} if PY3: @@ -123,6 +95,8 @@ def stderr_handle(): def win_colorprint(s, color=LIGHTBLUE): + if not WINDOWS: + return print(s) color += 8 # bold handle = stderr_handle() SetConsoleTextAttribute = ctypes.windll.Kernel32.SetConsoleTextAttribute @@ -390,11 +364,18 @@ def clean(): safe_rmtree("tmp") -def install_pydeps(): +def install_pydeps_test(): + """Install useful deps.""" + install_pip() + install_git_hooks() + sh("%s -m pip install -U %s" % (PYTHON, " ".join(TEST_DEPS))) + + +def install_pydeps_dev(): """Install useful deps.""" install_pip() install_git_hooks() - sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEPS))) + sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEV_DEPS))) def test(args=""): @@ -585,10 +566,11 @@ def parse_args(): sp.add_parser('install', help="build + install in develop/edit mode") sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") sp.add_parser('install-pip', help="install pip") + sp.add_parser('install-pydeps-dev', help="install dev python deps") + sp.add_parser('install-pydeps-test', help="install python test deps") sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") sp.add_parser('print-sysinfo', help="print system info") - sp.add_parser('install-pydeps', help="install python deps") test = sp.add_parser('test', help="[ARG] run tests") test_by_name = sp.add_parser('test-by-name', help=" run test by name") sp.add_parser('test-connections', help="run connections tests") diff --git a/setup.py b/setup.py index a88239c7cf..126f667032 100755 --- a/setup.py +++ b/setup.py @@ -68,6 +68,52 @@ CP37_PLUS = PY37_PLUS and sys.implementation.name == "cpython" Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED") +# Test deps, installable via `pip install .[test]`. +if PY3: + TEST_DEPS = [ + "pytest", + "pytest-xdist", + "setuptools", + ] +else: + TEST_DEPS = [ + "futures", + "ipaddress", + "enum34", + "mock==1.0.1", + "pytest-xdist", + "pytest==4.6.11", + "setuptools", + ] +if WINDOWS and not PYPY: + TEST_DEPS.append("pywin32") + TEST_DEPS.append("wheel") + TEST_DEPS.append("wmi") + +# Development deps, installable via `pip install .[dev]`. +DEV_DEPS = [ + "black", + "check-manifest", + "coverage", + "packaging", + "pylint", + "pyperf", + "pypinfo", + "pytest-cov", + "requests", + "rstcheck", + "ruff", + "sphinx", + "sphinx_rtd_theme", + "toml-sort", + "twine", + "virtualenv", + "wheel", +] +if WINDOWS: + DEV_DEPS.append("pyreadline") + DEV_DEPS.append("pdbpp") + macros = [] if POSIX: macros.append(("PSUTIL_POSIX", 1)) @@ -88,19 +134,6 @@ sources.append('psutil/_psutil_posix.c') -extras_require = { - "test": [ - "enum34; python_version <= '3.4'", - "ipaddress; python_version < '3.0'", - "mock; python_version < '3.0'", - ] -} -if not PYPY: - extras_require['test'].extend( - ["pywin32; sys.platform == 'win32'", "wmi; sys.platform == 'win32'"] - ) - - def get_version(): INIT = os.path.join(HERE, 'psutil/__init__.py') with open(INIT) as f: @@ -512,6 +545,10 @@ def main(): ], ) if setuptools is not None: + extras_require = { + "dev": DEV_DEPS, + "test": TEST_DEPS, + } kwargs.update( python_requires=( ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" From f84ca7d5c4953313e3090cfecc7c907fe1257342 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Oct 2024 23:42:22 +0200 Subject: [PATCH 1130/1714] get rid of reference to old test runner + update test instructions --- psutil/tests/README.rst | 40 +++++++++++++++++++++++++++++++++------- psutil/tests/__main__.py | 4 ++-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index a186581e34..5c7336fb00 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -1,14 +1,40 @@ Instructions for running tests ============================== -* There are two ways of running tests. As a "user", if psutil is already - installed and you just want to test it works:: +Setup: + +.. code-block:: bash + + make install-pydeps-test # install pytest + +There are two ways of running tests. As a "user", if psutil is already +installed and you just want to test it works on your system: + +.. code-block:: bash python -m psutil.tests - As a "developer", if you have a copy of the source code and you wish to hack - on psutil:: +As a "developer", if you have a copy of the source code and you're working on +it: + +.. code-block:: bash + + make test + +You can run tests in parallel with: + +.. code-block:: bash + + make test-parallel + +You can run a specific test with: + +.. code-block:: bash + + make test ARGS=psutil.tests.test_system.TestDiskAPIs + +You can run memory leak tests with: + +.. code-block:: bash - make install-pydeps # install missing third-party deps - make test # serial run - make test-parallel # parallel run + make test-parallel diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index 2460abdb36..f43b751f91 100644 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -6,7 +6,7 @@ $ python -m psutil.tests. """ -from .runner import main +import pytest -main() +pytest.main(["-v", "-s", "--tb=short"]) From 7d2155530e515a25146229d22475e616e5606ab3 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Thu, 3 Oct 2024 10:25:20 +0200 Subject: [PATCH 1131/1714] use setup() options to pass py_limited_api to bdist_wheel & bump cibuildwheel (#2450) Signed-off-by: mayeut --- .github/workflows/build.yml | 2 +- pyproject.toml | 2 +- setup.py | 27 ++++++++------------------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 356f4d0807..a3290241e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: if: matrix.arch == 'aarch64' - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.19.2 + uses: pypa/cibuildwheel@v2.21.2 env: CIBW_ARCHS: "${{ matrix.arch }}" CIBW_PRERELEASE_PYTHONS: True diff --git a/pyproject.toml b/pyproject.toml index 881a1c718f..988ff9c019 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -217,4 +217,4 @@ archs = ["arm64", "x86_64"] [build-system] build-backend = "setuptools.build_meta" -requires = ["setuptools>=43", "wheel"] +requires = ["setuptools>=43"] diff --git a/setup.py b/setup.py index 126f667032..fb36da0bc4 100755 --- a/setup.py +++ b/setup.py @@ -31,15 +31,11 @@ from setuptools import Extension from setuptools import setup except ImportError: + if "CIBUILDWHEEL" in os.environ: + raise setuptools = None from distutils.core import Extension from distutils.core import setup - try: - from wheel.bdist_wheel import bdist_wheel - except ImportError: - if "CIBUILDWHEEL" in os.environ: - raise - bdist_wheel = None HERE = os.path.abspath(os.path.dirname(__file__)) @@ -153,16 +149,19 @@ def get_version(): # Py_LIMITED_API lets us create a single wheel which works with multiple # python versions, including unreleased ones. -if bdist_wheel and CP36_PLUS and (MACOS or LINUX) and not Py_GIL_DISABLED: +if setuptools and CP36_PLUS and (MACOS or LINUX) and not Py_GIL_DISABLED: py_limited_api = {"py_limited_api": True} + options = {"bdist_wheel": {"py_limited_api": "cp36"}} macros.append(('Py_LIMITED_API', '0x03060000')) -elif bdist_wheel and CP37_PLUS and WINDOWS and not Py_GIL_DISABLED: +elif setuptools and CP37_PLUS and WINDOWS and not Py_GIL_DISABLED: # PyErr_SetFromWindowsErr / PyErr_SetFromWindowsErrWithFilename are # part of the stable API/ABI starting with CPython 3.7 py_limited_api = {"py_limited_api": True} + options = {"bdist_wheel": {"py_limited_api": "cp37"}} macros.append(('Py_LIMITED_API', '0x03070000')) else: py_limited_api = {} + options = {} def get_long_description(): @@ -462,22 +461,11 @@ def get_sunos_update(): else: extensions = [ext] -cmdclass = {} -if py_limited_api: - - class bdist_wheel_abi3(bdist_wheel): - def get_tag(self): - python, _abi, plat = bdist_wheel.get_tag(self) - return python, "abi3", plat - - cmdclass["bdist_wheel"] = bdist_wheel_abi3 - def main(): kwargs = dict( name='psutil', version=VERSION, - cmdclass=cmdclass, description=__doc__.replace('\n', ' ').strip() if __doc__ else '', long_description=get_long_description(), long_description_content_type='text/x-rst', @@ -497,6 +485,7 @@ def main(): license='BSD-3-Clause', packages=['psutil', 'psutil.tests'], ext_modules=extensions, + options=options, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', From 00c0fe8004defe1119b00d6c4b53d8a814765c25 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 3 Oct 2024 12:16:05 +0200 Subject: [PATCH 1132/1714] Pytest: use bare `assert` statements (#2453) --- HISTORY.rst | 1 + Makefile | 2 +- psutil/tests/__init__.py | 56 +-- psutil/tests/test_aix.py | 60 +-- psutil/tests/test_bsd.py | 260 +++++------- psutil/tests/test_connections.py | 162 ++++---- psutil/tests/test_contracts.py | 134 +++--- psutil/tests/test_linux.py | 691 +++++++++++++++---------------- psutil/tests/test_misc.py | 487 +++++++++++----------- psutil/tests/test_osx.py | 60 +-- psutil/tests/test_posix.py | 78 ++-- psutil/tests/test_process.py | 603 ++++++++++++++------------- psutil/tests/test_process_all.py | 200 ++++----- psutil/tests/test_sunos.py | 8 +- psutil/tests/test_system.py | 373 +++++++++-------- psutil/tests/test_testutils.py | 140 ++++--- psutil/tests/test_unicode.py | 42 +- psutil/tests/test_windows.py | 325 +++++++-------- 18 files changed, 1778 insertions(+), 1904 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b9ef61deeb..4ca7a58941 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ XXXX-XX-XX **Enhancements** +- 2446_: use pytest instead of unittest. - 2448_: add ``make install-sysdeps`` target to install the necessary system dependencies (python-dev, gcc, etc.) on all supported UNIX flavors. - 2449_: add ``make install-pydeps-test`` and ``make install-pydeps-dev`` diff --git a/Makefile b/Makefile index 5fc72a5fbd..b0fee04311 100644 --- a/Makefile +++ b/Makefile @@ -242,7 +242,7 @@ fix-black: @git ls-files '*.py' | xargs $(PYTHON) -m black fix-ruff: - @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --fix $(ARGS) + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --fix --output-format=concise $(ARGS) fix-toml: ## Fix pyproject.toml @git ls-files '*.toml' | xargs toml-sort diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index faa455bbd2..224faaa7d0 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -36,6 +36,8 @@ from socket import AF_INET6 from socket import SOCK_STREAM +import pytest + import psutil from psutil import AIX from psutil import LINUX @@ -982,29 +984,29 @@ def pyrun(self, *args, **kwds): 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) + assert isinstance(exc, psutil.Error) + assert exc.pid == proc.pid + assert exc.name == proc._name if exc.name: - self.assertNotEqual(exc.name, "") + assert exc.name if isinstance(exc, psutil.ZombieProcess): - self.assertEqual(exc.ppid, proc._ppid) + assert exc.ppid == proc._ppid if exc.ppid is not None: - self.assertGreaterEqual(exc.ppid, 0) + assert exc.ppid >= 0 str(exc) repr(exc) def assertPidGone(self, pid): - with self.assertRaises(psutil.NoSuchProcess) as cm: + with pytest.raises(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 cm.value.pid == pid + assert cm.value.name is None assert not psutil.pid_exists(pid), pid - self.assertNotIn(pid, psutil.pids()) - self.assertNotIn(pid, [x.pid for x in psutil.process_iter()]) + assert pid not in psutil.pids() + assert pid not in [x.pid for x in psutil.process_iter()] def assertProcessGone(self, proc): self.assertPidGone(proc.pid) @@ -1030,21 +1032,21 @@ def assertProcessZombie(self, proc): 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) + assert proc == clone if not (OPENBSD or NETBSD): - self.assertEqual(hash(proc), hash(clone)) + assert hash(proc) == hash(clone) # Its status always be querable. - self.assertEqual(proc.status(), psutil.STATUS_ZOMBIE) + assert 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()]) + assert proc.pid in psutil.pids() + assert proc.pid in [x.pid for x in psutil.process_iter()] psutil._pmap = {} - self.assertIn(proc.pid, [x.pid for x in psutil.process_iter()]) + assert proc.pid in [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): @@ -1055,15 +1057,15 @@ def assertProcessZombie(self, proc): self._check_proc_exc(proc, exc) if LINUX: # https://github.com/giampaolo/psutil/pull/2288 - with self.assertRaises(psutil.ZombieProcess) as cm: + with pytest.raises(psutil.ZombieProcess) as cm: proc.cmdline() - self._check_proc_exc(proc, cm.exception) - with self.assertRaises(psutil.ZombieProcess) as cm: + self._check_proc_exc(proc, cm.value) + with pytest.raises(psutil.ZombieProcess) as cm: proc.exe() - self._check_proc_exc(proc, cm.exception) - with self.assertRaises(psutil.ZombieProcess) as cm: + self._check_proc_exc(proc, cm.value) + with pytest.raises(psutil.ZombieProcess) as cm: proc.memory_maps() - self._check_proc_exc(proc, cm.exception) + self._check_proc_exc(proc, cm.value) # Zombie cannot be signaled or terminated. proc.suspend() proc.resume() @@ -1071,10 +1073,10 @@ def assertProcessZombie(self, proc): 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()]) + assert proc.pid in psutil.pids() + assert proc.pid in [x.pid for x in psutil.process_iter()] psutil._pmap = {} - self.assertIn(proc.pid, [x.pid for x in psutil.process_iter()]) + assert proc.pid in [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( @@ -1188,7 +1190,7 @@ def _call_ntimes(self, fun, times): del x, ret gc.collect(generation=1) mem2 = self._get_mem() - self.assertEqual(gc.garbage, []) + assert gc.garbage == [] diff = mem2 - mem1 # can also be negative return diff diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 8e6121570d..5c80d1f8f2 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -26,9 +26,7 @@ def test_virtual_memory(self): re_pattern += r"(?P<%s>\S+)\s+" % (field,) matchobj = re.search(re_pattern, out) - self.assertIsNotNone( - matchobj, "svmon command returned unexpected output" - ) + assert matchobj is not None KB = 1024 total = int(matchobj.group("size")) * KB @@ -42,16 +40,10 @@ def test_virtual_memory(self): # 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 - ) + assert psutil_result.total == total + assert abs(psutil_result.used - used) < TOLERANCE_SYS_MEM + assert abs(psutil_result.available - available) < TOLERANCE_SYS_MEM + assert abs(psutil_result.free - free) < TOLERANCE_SYS_MEM def test_swap_memory(self): out = sh('/usr/sbin/lsps -a') @@ -67,16 +59,14 @@ def test_swap_memory(self): out, ) - self.assertIsNotNone( - matchobj, "lsps command returned unexpected output" - ) + assert matchobj is not None 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) + assert int(psutil_result.total / MB) == total_mb def test_cpu_stats(self): out = sh('/usr/bin/mpstat -a') @@ -90,42 +80,36 @@ def test_cpu_stats(self): re_pattern += r"(?P<%s>\S+)\s+" % (field,) matchobj = re.search(re_pattern, out) - self.assertIsNotNone( - matchobj, "mpstat command returned unexpected output" - ) + assert matchobj is not None # 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, + assert ( + abs(psutil_result.ctx_switches - int(matchobj.group("cs"))) + < CPU_STATS_TOLERANCE ) - self.assertAlmostEqual( - psutil_result.syscalls, - int(matchobj.group("sysc")), - delta=CPU_STATS_TOLERANCE, + assert ( + abs(psutil_result.syscalls - int(matchobj.group("sysc"))) + < CPU_STATS_TOLERANCE ) - self.assertAlmostEqual( - psutil_result.interrupts, - int(matchobj.group("dev")), - delta=CPU_STATS_TOLERANCE, + assert ( + abs(psutil_result.interrupts - int(matchobj.group("dev"))) + < CPU_STATS_TOLERANCE ) - self.assertAlmostEqual( - psutil_result.soft_interrupts, - int(matchobj.group("soft")), - delta=CPU_STATS_TOLERANCE, + assert ( + abs(psutil_result.soft_interrupts - int(matchobj.group("soft"))) + < 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) + assert 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) + assert ifconfig_names == psutil_names diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 6112c11e17..13c8774608 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -16,6 +16,8 @@ import time import unittest +import pytest + import psutil from psutil import BSD from psutil import FREEBSD @@ -93,7 +95,7 @@ def test_process_create_time(self): start_psutil = time.strftime( "%a %b %e %H:%M:%S %Y", time.localtime(start_psutil) ) - self.assertEqual(start_ps, start_psutil) + assert start_ps == start_psutil def test_disks(self): # test psutil.disk_usage() and psutil.disk_partitions() @@ -114,8 +116,8 @@ def df(path): 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) + assert part.device == dev + assert usage.total == total # 10 MB tolerance if abs(usage.free - free) > 10 * 1024 * 1024: raise self.fail("psutil=%s, df=%s" % (usage.free, free)) @@ -125,13 +127,13 @@ def df(path): @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) + assert 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) + assert num == psutil.virtual_memory().total @unittest.skipIf(not which('ifconfig'), "ifconfig cmd not available") def test_net_if_stats(self): @@ -141,11 +143,9 @@ def test_net_if_stats(self): except RuntimeError: pass else: - self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) + assert stats.isup == ('RUNNING' in out) if "mtu" in out: - self.assertEqual( - stats.mtu, int(re.findall(r'mtu (\d+)', out)[0]) - ) + assert stats.mtu == int(re.findall(r'mtu (\d+)', out)[0]) # ===================================================================== @@ -173,22 +173,19 @@ def test_memory_maps(self): 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) + assert "%s-%s" % (start, stop) == map.addr + assert int(res) == map.rss if not map.path.startswith('['): - self.assertEqual(fields[10], map.path) + assert 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] - ) + assert 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:]), + assert ' '.join(psutil.Process(self.pid).cmdline()) == ' '.join( + out.split('\n')[1].split()[2:] ) def test_uids_gids(self): @@ -197,12 +194,12 @@ def test_uids_gids(self): 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)) + assert uids.real == int(ruid) + assert uids.effective == int(euid) + assert uids.saved == int(suid) + assert gids.real == int(rgid) + assert gids.effective == int(egid) + assert gids.saved == int(sgid) @retry_on_failure() def test_ctx_switches(self): @@ -214,12 +211,12 @@ def test_ctx_switches(self): if ' voluntary context' in line: pstat_value = int(line.split()[-1]) psutil_value = p.num_ctx_switches().voluntary - self.assertEqual(pstat_value, psutil_value) + assert 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) + assert pstat_value == psutil_value tested.append(None) if len(tested) != 2: raise RuntimeError("couldn't find lines match in procstat out") @@ -234,12 +231,12 @@ def test_cpu_times(self): 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) + assert 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) + assert pstat_value == psutil_value tested.append(None) if len(tested) != 2: raise RuntimeError("couldn't find lines match in procstat out") @@ -268,7 +265,7 @@ def test_cpu_frequency_against_sysctl(self): sysctl_result = int(sysctl(sensor)) except RuntimeError: raise unittest.SkipTest("frequencies not supported by kernel") - self.assertEqual(psutil.cpu_freq().current, sysctl_result) + assert psutil.cpu_freq().current == sysctl_result sensor = "dev.cpu.0.freq_levels" sysctl_result = sysctl(sensor) @@ -277,136 +274,114 @@ def test_cpu_frequency_against_sysctl(self): # 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) + assert psutil.cpu_freq().max == max_freq + assert 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 - ) + assert abs(psutil.virtual_memory().active - syst) < 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 - ) + assert abs(psutil.virtual_memory().inactive - syst) < 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 - ) + assert abs(psutil.virtual_memory().wired - syst) < 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 - ) + assert abs(psutil.virtual_memory().cached - syst) < 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 - ) + assert abs(psutil.virtual_memory().free - syst) < 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 - ) + assert abs(psutil.virtual_memory().buffers - syst) < 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) + assert 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 - ) + assert abs(psutil.virtual_memory().active - num) < 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 - ) + assert abs(psutil.virtual_memory().inactive - num) < 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 - ) + assert abs(psutil.virtual_memory().wired - num) < 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 - ) + assert abs(psutil.virtual_memory().cached - num) < 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 - ) + assert abs(psutil.virtual_memory().free - num) < 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 - ) + assert abs(psutil.virtual_memory().buffers - num) < 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, + assert ( + abs( + psutil.cpu_stats().ctx_switches + - sysctl('vm.stats.sys.v_swtch') + ) + < 1000 ) def test_cpu_stats_interrupts(self): - self.assertAlmostEqual( - psutil.cpu_stats().interrupts, - sysctl('vm.stats.sys.v_intr'), - delta=1000, + assert ( + abs(psutil.cpu_stats().interrupts - sysctl('vm.stats.sys.v_intr')) + < 1000 ) def test_cpu_stats_soft_interrupts(self): - self.assertAlmostEqual( - psutil.cpu_stats().soft_interrupts, - sysctl('vm.stats.sys.v_soft'), - delta=1000, + assert ( + abs( + psutil.cpu_stats().soft_interrupts + - sysctl('vm.stats.sys.v_soft') + ) + < 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, + assert ( + abs(psutil.cpu_stats().syscalls - sysctl('vm.stats.sys.v_syscall')) + < 200000 ) # def test_cpu_stats_traps(self): @@ -417,21 +392,15 @@ def test_cpu_stats_syscalls(self): def test_swapmem_free(self): _total, _used, free = self.parse_swapinfo() - self.assertAlmostEqual( - psutil.swap_memory().free, free, delta=TOLERANCE_SYS_MEM - ) + assert abs(psutil.swap_memory().free - free) < 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 - ) + assert abs(psutil.swap_memory().used - used) < 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 - ) + assert abs(psutil.swap_memory().total - total) < TOLERANCE_SYS_MEM # --- others @@ -440,7 +409,7 @@ def test_boot_time(self): s = s[s.find(" sec = ") + 7 :] s = s[: s.find(',')] btime = int(s) - self.assertEqual(btime, psutil.boot_time()) + assert btime == psutil.boot_time() # --- sensors_battery @@ -458,37 +427,36 @@ def secs2hours(secs): metrics = psutil.sensors_battery() percent = int(fields['Remaining capacity:'].replace('%', '')) remaining_time = fields['Remaining time:'] - self.assertEqual(metrics.percent, percent) + assert metrics.percent == percent if remaining_time == 'unknown': - self.assertEqual(metrics.secsleft, psutil.POWER_TIME_UNLIMITED) + assert metrics.secsleft == psutil.POWER_TIME_UNLIMITED else: - self.assertEqual(secs2hours(metrics.secsleft), remaining_time) + assert 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") + assert psutil.sensors_battery().percent == sysctl( + "hw.acpi.battery.life" ) - self.assertEqual( - psutil.sensors_battery().power_plugged, - sysctl("hw.acpi.acline") == 1, + assert 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) + assert sysctl("hw.acpi.battery.time") == -1 else: - self.assertEqual(secsleft, sysctl("hw.acpi.battery.time") * 60) + assert 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): + with pytest.raises(RuntimeError): sysctl("hw.acpi.battery.life") sysctl("hw.acpi.battery.time") sysctl("hw.acpi.acline") - self.assertIsNone(psutil.sensors_battery()) + assert psutil.sensors_battery() is None # --- sensors_temperatures @@ -501,17 +469,19 @@ def test_sensors_temperatures_against_sysctl(self): sysctl_result = int(float(sysctl(sensor)[:-1])) except RuntimeError: raise unittest.SkipTest("temperatures not supported by kernel") - self.assertAlmostEqual( - psutil.sensors_temperatures()["coretemp"][cpu].current, - sysctl_result, - delta=10, + assert ( + abs( + psutil.sensors_temperatures()["coretemp"][cpu].current + - sysctl_result + ) + < 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, + assert ( + psutil.sensors_temperatures()["coretemp"][cpu].high + == sysctl_result ) @@ -526,7 +496,7 @@ 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) + assert sys_bt == psutil_bt # ===================================================================== @@ -547,57 +517,55 @@ def parse_meminfo(look_for): # --- virtual mem def test_vmem_total(self): - self.assertEqual( - psutil.virtual_memory().total, self.parse_meminfo("MemTotal:") - ) + assert 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, + assert ( + abs(psutil.virtual_memory().free - self.parse_meminfo("MemFree:")) + < TOLERANCE_SYS_MEM ) def test_vmem_buffers(self): - self.assertAlmostEqual( - psutil.virtual_memory().buffers, - self.parse_meminfo("Buffers:"), - delta=TOLERANCE_SYS_MEM, + assert ( + abs( + psutil.virtual_memory().buffers + - self.parse_meminfo("Buffers:") + ) + < TOLERANCE_SYS_MEM ) def test_vmem_shared(self): - self.assertAlmostEqual( - psutil.virtual_memory().shared, - self.parse_meminfo("MemShared:"), - delta=TOLERANCE_SYS_MEM, + assert ( + abs( + psutil.virtual_memory().shared + - self.parse_meminfo("MemShared:") + ) + < TOLERANCE_SYS_MEM ) def test_vmem_cached(self): - self.assertAlmostEqual( - psutil.virtual_memory().cached, - self.parse_meminfo("Cached:"), - delta=TOLERANCE_SYS_MEM, + assert ( + abs(psutil.virtual_memory().cached - self.parse_meminfo("Cached:")) + < TOLERANCE_SYS_MEM ) # --- swap mem def test_swapmem_total(self): - self.assertAlmostEqual( - psutil.swap_memory().total, - self.parse_meminfo("SwapTotal:"), - delta=TOLERANCE_SYS_MEM, + assert ( + abs(psutil.swap_memory().total - self.parse_meminfo("SwapTotal:")) + < TOLERANCE_SYS_MEM ) def test_swapmem_free(self): - self.assertAlmostEqual( - psutil.swap_memory().free, - self.parse_meminfo("SwapFree:"), - delta=TOLERANCE_SYS_MEM, + assert ( + abs(psutil.swap_memory().free - self.parse_meminfo("SwapFree:")) + < TOLERANCE_SYS_MEM ) def test_swapmem_used(self): smem = psutil.swap_memory() - self.assertEqual(smem.used, smem.total - smem.free) + assert smem.used == smem.total - smem.free # --- others @@ -609,9 +577,7 @@ def test_cpu_stats_interrupts(self): break else: raise ValueError("couldn't find line") - self.assertAlmostEqual( - psutil.cpu_stats().interrupts, interrupts, delta=1000 - ) + assert abs(psutil.cpu_stats().interrupts - interrupts) < 1000 def test_cpu_stats_ctx_switches(self): with open('/proc/stat', 'rb') as f: @@ -621,6 +587,4 @@ def test_cpu_stats_ctx_switches(self): break else: raise ValueError("couldn't find line") - self.assertAlmostEqual( - psutil.cpu_stats().ctx_switches, ctx_switches, delta=1000 - ) + assert abs(psutil.cpu_stats().ctx_switches - ctx_switches) < 1000 diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index e1a24563d2..783bf81456 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -59,11 +59,11 @@ def this_proc_net_connections(kind): @pytest.mark.xdist_group(name="serial") class ConnectionTestCase(PsutilTestCase): def setUp(self): - self.assertEqual(this_proc_net_connections(kind='all'), []) + assert this_proc_net_connections(kind='all') == [] def tearDown(self): # Make sure we closed all resources. - self.assertEqual(this_proc_net_connections(kind='all'), []) + assert this_proc_net_connections(kind='all') == [] def compare_procsys_connections(self, pid, proc_cons, kind='all'): """Given a process PID and its list of connections compare @@ -83,7 +83,7 @@ def compare_procsys_connections(self, pid, proc_cons, kind='all'): 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) + assert proc_cons == sys_cons class TestBasicOperations(ConnectionTestCase): @@ -99,8 +99,10 @@ def test_process(self): check_connection_ntuple(conn) def test_invalid_kind(self): - self.assertRaises(ValueError, this_proc_net_connections, kind='???') - self.assertRaises(ValueError, psutil.net_connections, kind='???') + with pytest.raises(ValueError): + this_proc_net_connections(kind='???') + with pytest.raises(ValueError): + psutil.net_connections(kind='???') @pytest.mark.xdist_group(name="serial") @@ -115,9 +117,9 @@ def get_conn_from_sock(self, sock): # so there may be more connections. return smap[sock.fileno()] else: - self.assertEqual(len(cons), 1) + assert len(cons) == 1 if cons[0].fd != -1: - self.assertEqual(smap[sock.fileno()].fd, sock.fileno()) + assert smap[sock.fileno()].fd == sock.fileno() return cons[0] def check_socket(self, sock): @@ -130,12 +132,10 @@ def check_socket(self, sock): # fd, family, type if conn.fd != -1: - self.assertEqual(conn.fd, sock.fileno()) - self.assertEqual(conn.family, sock.family) + assert conn.fd == sock.fileno() + assert conn.family == sock.family # see: http://bugs.python.org/issue30204 - self.assertEqual( - conn.type, sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) - ) + assert conn.type == sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) # local address laddr = sock.getsockname() @@ -144,7 +144,7 @@ def check_socket(self, sock): laddr = laddr.decode() if sock.family == AF_INET6: laddr = laddr[:2] - self.assertEqual(conn.laddr, laddr) + assert conn.laddr == laddr # XXX Solaris can't retrieve system-wide UNIX sockets if sock.family == AF_UNIX and HAS_NET_CONNECTIONS_UNIX: @@ -156,47 +156,47 @@ 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) + assert conn.raddr == () + assert 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) + assert conn.raddr == () + assert 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) + assert conn.raddr == () + assert 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) + assert conn.raddr == () + assert 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) + assert conn.raddr == "" # noqa + assert 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) + assert conn.raddr == "" # noqa + assert conn.status == psutil.CONN_NONE @pytest.mark.xdist_group(name="serial") @@ -210,13 +210,13 @@ class TestConnectedSocket(ConnectionTestCase): @unittest.skipIf(SUNOS, "unreliable on SUONS") def test_tcp(self): addr = ("127.0.0.1", 0) - self.assertEqual(this_proc_net_connections(kind='tcp4'), []) + assert this_proc_net_connections(kind='tcp4') == [] server, client = tcp_socketpair(AF_INET, addr=addr) try: cons = this_proc_net_connections(kind='tcp4') - self.assertEqual(len(cons), 2) - self.assertEqual(cons[0].status, psutil.CONN_ESTABLISHED) - self.assertEqual(cons[1].status, psutil.CONN_ESTABLISHED) + assert len(cons) == 2 + assert cons[0].status == psutil.CONN_ESTABLISHED + assert cons[1].status == psutil.CONN_ESTABLISHED # May not be fast enough to change state so it stays # commenteed. # client.close() @@ -239,17 +239,17 @@ def test_unix(self): # 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) + assert len(cons) == 2 if LINUX or FREEBSD or SUNOS or OPENBSD: # remote path is never set - self.assertEqual(cons[0].raddr, "") - self.assertEqual(cons[1].raddr, "") + assert cons[0].raddr == "" # noqa + assert cons[1].raddr == "" # noqa # one local address should though - self.assertEqual(testfn, cons[0].laddr or cons[1].laddr) + assert 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) + assert (cons[0].laddr or cons[1].laddr) == testfn finally: server.close() client.close() @@ -259,12 +259,12 @@ class TestFilters(ConnectionTestCase): def test_filters(self): def check(kind, families, types): for conn in this_proc_net_connections(kind=kind): - self.assertIn(conn.family, families) - self.assertIn(conn.type, types) + assert conn.family in families + assert conn.type in types if not SKIP_SYSCONS: for conn in psutil.net_connections(kind=kind): - self.assertIn(conn.family, families) - self.assertIn(conn.type, types) + assert conn.family in families + assert conn.type in types with create_sockets(): check( @@ -305,17 +305,17 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): "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) + assert conn.family == family + assert conn.type == type + assert conn.laddr == laddr + assert conn.raddr == raddr + assert conn.status == status for kind in all_kinds: cons = proc.net_connections(kind=kind) if kind in kinds: - self.assertNotEqual(cons, []) + assert cons != [] else: - self.assertEqual(cons, []) + assert cons == [] # compare against system-wide connections # XXX Solaris can't retrieve system-wide UNIX # sockets. @@ -375,7 +375,7 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): for p in psutil.Process().children(): cons = p.net_connections() - self.assertEqual(len(cons), 1) + assert len(cons) == 1 for conn in cons: # TCP v4 if p.pid == tcp4_proc.pid: @@ -430,59 +430,59 @@ def test_count(self): with create_sockets(): # tcp cons = this_proc_net_connections(kind='tcp') - self.assertEqual(len(cons), 2 if supports_ipv6() else 1) + assert 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) + assert conn.family in (AF_INET, AF_INET6) + assert conn.type == SOCK_STREAM # tcp4 cons = this_proc_net_connections(kind='tcp4') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET) - self.assertEqual(cons[0].type, SOCK_STREAM) + assert len(cons) == 1 + assert cons[0].family == AF_INET + assert cons[0].type == SOCK_STREAM # tcp6 if supports_ipv6(): cons = this_proc_net_connections(kind='tcp6') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET6) - self.assertEqual(cons[0].type, SOCK_STREAM) + assert len(cons) == 1 + assert cons[0].family == AF_INET6 + assert cons[0].type == SOCK_STREAM # udp cons = this_proc_net_connections(kind='udp') - self.assertEqual(len(cons), 2 if supports_ipv6() else 1) + assert 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) + assert conn.family in (AF_INET, AF_INET6) + assert conn.type == SOCK_DGRAM # udp4 cons = this_proc_net_connections(kind='udp4') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET) - self.assertEqual(cons[0].type, SOCK_DGRAM) + assert len(cons) == 1 + assert cons[0].family == AF_INET + assert cons[0].type == SOCK_DGRAM # udp6 if supports_ipv6(): cons = this_proc_net_connections(kind='udp6') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET6) - self.assertEqual(cons[0].type, SOCK_DGRAM) + assert len(cons) == 1 + assert cons[0].family == AF_INET6 + assert cons[0].type == SOCK_DGRAM # inet cons = this_proc_net_connections(kind='inet') - self.assertEqual(len(cons), 4 if supports_ipv6() else 2) + assert 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)) + assert conn.family in (AF_INET, AF_INET6) + assert conn.type in (SOCK_STREAM, SOCK_DGRAM) # inet6 if supports_ipv6(): cons = this_proc_net_connections(kind='inet6') - self.assertEqual(len(cons), 2) + assert len(cons) == 2 for conn in cons: - self.assertEqual(conn.family, AF_INET6) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) + assert conn.family == AF_INET6 + assert conn.type in (SOCK_STREAM, SOCK_DGRAM) # Skipped on BSD becayse by default the Python process # creates a UNIX socket to '/var/run/log'. if HAS_NET_CONNECTIONS_UNIX and not (FREEBSD or NETBSD): cons = this_proc_net_connections(kind='unix') - self.assertEqual(len(cons), 3) + assert len(cons) == 3 for conn in cons: - self.assertEqual(conn.family, AF_UNIX) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) + assert conn.family == AF_UNIX + assert conn.type in (SOCK_STREAM, SOCK_DGRAM) @unittest.skipIf(SKIP_SYSCONS, "requires root") @@ -492,9 +492,9 @@ class TestSystemWideConnections(ConnectionTestCase): def test_it(self): def check(cons, families, types_): for conn in cons: - self.assertIn(conn.family, families, msg=conn) + assert conn.family in families if conn.family != AF_UNIX: - self.assertIn(conn.type, types_, msg=conn) + assert conn.type in types_ check_connection_ntuple(conn) with create_sockets(): @@ -506,7 +506,7 @@ def check(cons, families, types_): continue families, types_ = groups cons = psutil.net_connections(kind) - self.assertEqual(len(cons), len(set(cons))) + assert len(cons) == len(set(cons)) check(cons, families, types_) @retry_on_failure() @@ -544,11 +544,9 @@ def test_multi_sockets_procs(self): 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 - ) + assert len([x for x in syscons if x.pid == pid]) == expected p = psutil.Process(pid) - self.assertEqual(len(p.net_connections('all')), expected) + assert len(p.net_connections('all')) == expected class TestMisc(PsutilTestCase): @@ -560,8 +558,8 @@ def test_net_connection_constants(self): num = getattr(psutil, name) str_ = str(num) assert str_.isupper(), str_ - self.assertNotIn(str, strs) - self.assertNotIn(num, ints) + assert str not in strs + assert num not in ints ints.append(num) strs.append(str_) if SUNOS: diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 9154c5c70f..5b975de605 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -49,7 +49,7 @@ class TestAvailConstantsAPIs(PsutilTestCase): def test_PROCFS_PATH(self): - self.assertEqual(hasattr(psutil, "PROCFS_PATH"), LINUX or SUNOS or AIX) + assert hasattr(psutil, "PROCFS_PATH") == (LINUX or SUNOS or AIX) def test_win_priority(self): ae = self.assertEqual @@ -111,36 +111,31 @@ def test_rlimit(self): class TestAvailSystemAPIs(PsutilTestCase): def test_win_service_iter(self): - self.assertEqual(hasattr(psutil, "win_service_iter"), WINDOWS) + assert hasattr(psutil, "win_service_iter") == WINDOWS def test_win_service_get(self): - self.assertEqual(hasattr(psutil, "win_service_get"), WINDOWS) + assert 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, + assert 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 - ) + assert hasattr(psutil, "sensors_temperatures") == (LINUX or FREEBSD) def test_sensors_fans(self): - self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX) + assert hasattr(psutil, "sensors_fans") == LINUX def test_battery(self): - self.assertEqual( - hasattr(psutil, "sensors_battery"), - LINUX or WINDOWS or FREEBSD or MACOS, + assert hasattr(psutil, "sensors_battery") == ( + LINUX or WINDOWS or FREEBSD or MACOS ) class TestAvailProcessAPIs(PsutilTestCase): def test_environ(self): - self.assertEqual( - hasattr(psutil.Process, "environ"), + assert hasattr(psutil.Process, "environ") == ( LINUX or MACOS or WINDOWS @@ -148,51 +143,50 @@ def test_environ(self): or SUNOS or FREEBSD or OPENBSD - or NETBSD, + or NETBSD ) def test_uids(self): - self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) + assert hasattr(psutil.Process, "uids") == POSIX def test_gids(self): - self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) + assert hasattr(psutil.Process, "uids") == POSIX def test_terminal(self): - self.assertEqual(hasattr(psutil.Process, "terminal"), POSIX) + assert hasattr(psutil.Process, "terminal") == POSIX def test_ionice(self): - self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) + assert 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) + assert 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)) + assert hasit == (not (MACOS or SUNOS)) def test_num_fds(self): - self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX) + assert hasattr(psutil.Process, "num_fds") == POSIX def test_num_handles(self): - self.assertEqual(hasattr(psutil.Process, "num_handles"), WINDOWS) + assert hasattr(psutil.Process, "num_handles") == WINDOWS def test_cpu_affinity(self): - self.assertEqual( - hasattr(psutil.Process, "cpu_affinity"), - LINUX or WINDOWS or FREEBSD, + assert 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 + assert 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)) + assert hasit == (not (OPENBSD or NETBSD or AIX or MACOS)) # =================================================================== @@ -213,9 +207,9 @@ def setUpClass(cls): def assert_ntuple_of_nums(self, nt, type_=float, gezero=True): assert is_namedtuple(nt) for n in nt: - self.assertIsInstance(n, type_) + assert isinstance(n, type_) if gezero: - self.assertGreaterEqual(n, 0) + assert n >= 0 def test_cpu_times(self): self.assert_ntuple_of_nums(psutil.cpu_times()) @@ -223,15 +217,15 @@ def test_cpu_times(self): 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) + assert isinstance(psutil.cpu_percent(interval=None), float) + assert isinstance(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) + assert isinstance(psutil.cpu_count(), int) # TODO: remove this once 1892 is fixed @unittest.skipIf( @@ -246,88 +240,88 @@ def test_cpu_freq(self): 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) + assert isinstance(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) + assert isinstance(disk.device, str) + assert isinstance(disk.mountpoint, str) + assert isinstance(disk.fstype, str) + assert isinstance(disk.opts, str) @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))) + assert 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) + assert isinstance(ifname, str) for addr in addrs: if enum is not None and not PYPY: - self.assertIsInstance(addr.family, enum.IntEnum) + assert isinstance(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))) + assert isinstance(addr.family, int) + assert isinstance(addr.address, str) + assert isinstance(addr.netmask, (str, type(None))) + assert isinstance(addr.broadcast, (str, type(None))) @unittest.skipIf(QEMU_USER, 'QEMU user not supported') 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) + assert isinstance(ifname, str) + assert isinstance(info.isup, bool) if enum is not None: - self.assertIsInstance(info.duplex, enum.IntEnum) + assert isinstance(info.duplex, enum.IntEnum) else: - self.assertIsInstance(info.duplex, int) - self.assertIsInstance(info.speed, int) - self.assertIsInstance(info.mtu, int) + assert isinstance(info.duplex, int) + assert isinstance(info.speed, int) + assert isinstance(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) + assert isinstance(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) + assert isinstance(name, str) for unit in units: - self.assertIsInstance(unit.label, str) - self.assertIsInstance(unit.current, (float, int, type(None))) + assert isinstance(unit.label, str) + assert isinstance(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) + assert isinstance(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))) + assert isinstance(unit.label, str) + assert isinstance(unit.current, (float, int, type(None))) + assert isinstance(unit.high, (float, int, type(None))) + assert isinstance(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) + assert isinstance(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))) + assert isinstance(user.name, str) + assert isinstance(user.terminal, (str, type(None))) + assert isinstance(user.host, (str, type(None))) + assert isinstance(user.pid, (int, type(None))) class TestProcessWaitType(PsutilTestCase): @@ -336,8 +330,8 @@ def test_negative_signal(self): p = psutil.Process(self.spawn_testproc().pid) p.terminate() code = p.wait() - self.assertEqual(code, -signal.SIGTERM) + assert code == -signal.SIGTERM if enum is not None: - self.assertIsInstance(code, enum.IntEnum) + assert isinstance(code, enum.IntEnum) else: - self.assertIsInstance(code, int) + assert isinstance(code, int) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index e0c361a049..fddce78d2c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -23,6 +23,8 @@ import unittest import warnings +import pytest + import psutil from psutil import LINUX from psutil._compat import PY3 @@ -272,7 +274,7 @@ class TestSystemVirtualMemoryAgainstFree(PsutilTestCase): def test_total(self): cli_value = free_physmem().total psutil_value = psutil.virtual_memory().total - self.assertEqual(cli_value, psutil_value) + assert cli_value == psutil_value @retry_on_failure() def test_used(self): @@ -290,17 +292,13 @@ def test_used(self): raise unittest.SkipTest("free version too recent") cli_value = free_physmem().used psutil_value = psutil.virtual_memory().used - self.assertAlmostEqual( - cli_value, psutil_value, delta=TOLERANCE_SYS_MEM - ) + assert abs(cli_value - psutil_value) < 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 - ) + assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM @retry_on_failure() def test_shared(self): @@ -309,12 +307,9 @@ def test_shared(self): 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), - ) + assert ( + abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + ), '%s %s \n%s' % (free_value, psutil_value, free.output) @retry_on_failure() def test_available(self): @@ -327,12 +322,9 @@ def test_available(self): 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), - ) + assert ( + abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + ), '%s %s \n%s' % (free_value, psutil_value, out) @unittest.skipIf(not LINUX, "LINUX only") @@ -340,9 +332,7 @@ 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 - ) + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM @retry_on_failure() def test_used(self): @@ -360,41 +350,31 @@ def test_used(self): raise unittest.SkipTest("free version too recent") vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used - self.assertAlmostEqual( - vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM - ) + assert abs(vmstat_value - psutil_value) < 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 - ) + assert abs(vmstat_value - psutil_value) < 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 - ) + assert abs(vmstat_value - psutil_value) < 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 - ) + assert abs(vmstat_value - psutil_value) < 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 - ) + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM @unittest.skipIf(not LINUX, "LINUX only") @@ -418,24 +398,22 @@ def test_warnings_on_misses(self): warnings.simplefilter("always") ret = psutil.virtual_memory() assert m.called - self.assertEqual(len(ws), 1) + assert 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) + assert "memory stats couldn't be determined" in str(w.message) + assert "cached" in str(w.message) + assert "shared" in str(w.message) + assert "active" in str(w.message) + assert "inactive" in str(w.message) + assert "buffers" in str(w.message) + assert "available" in str(w.message) + assert ret.cached == 0 + assert ret.active == 0 + assert ret.inactive == 0 + assert ret.shared == 0 + assert ret.buffers == 0 + assert ret.available == 0 + assert ret.slab == 0 @retry_on_failure() def test_avail_old_percent(self): @@ -451,7 +429,7 @@ def test_avail_old_percent(self): if b'MemAvailable:' in mems: b = mems[b'MemAvailable:'] diff_percent = abs(a - b) / a * 100 - self.assertLess(diff_percent, 15) + assert diff_percent < 15 def test_avail_old_comes_from_kernel(self): # Make sure "MemAvailable:" coluimn is used instead of relying @@ -475,10 +453,10 @@ def test_avail_old_comes_from_kernel(self): with warnings.catch_warnings(record=True) as ws: ret = psutil.virtual_memory() assert m.called - self.assertEqual(ret.available, 6574984 * 1024) + assert ret.available == 6574984 * 1024 w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", str(w.message) + assert "inactive memory stats couldn't be determined" in str( + w.message ) def test_avail_old_missing_fields(self): @@ -500,10 +478,10 @@ def test_avail_old_missing_fields(self): with warnings.catch_warnings(record=True) as ws: ret = psutil.virtual_memory() assert m.called - self.assertEqual(ret.available, 2057400 * 1024 + 4818144 * 1024) + assert ret.available == 2057400 * 1024 + 4818144 * 1024 w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", str(w.message) + assert "inactive memory stats couldn't be determined" in str( + w.message ) def test_avail_old_missing_zoneinfo(self): @@ -530,13 +508,11 @@ def test_avail_old_missing_zoneinfo(self): ): with warnings.catch_warnings(record=True) as ws: ret = psutil.virtual_memory() - self.assertEqual( - ret.available, 2057400 * 1024 + 4818144 * 1024 - ) + assert ret.available == 2057400 * 1024 + 4818144 * 1024 w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", - str(w.message), + assert ( + "inactive memory stats couldn't be determined" + in str(w.message) ) def test_virtual_memory_mocked(self): @@ -594,16 +570,16 @@ def test_virtual_memory_mocked(self): 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) + assert mem.total == 100 * 1024 + assert mem.free == 2 * 1024 + assert 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) + assert mem.cached == (5 + 23) * 1024 + assert mem.shared == 21 * 1024 + assert mem.active == 7 * 1024 + assert mem.inactive == 8 * 1024 + assert mem.slab == 22 * 1024 + assert mem.available == 3 * 1024 # ===================================================================== @@ -623,25 +599,19 @@ def meminfo_has_swap_info(): 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 - ) + assert abs(free_value - psutil_value) < 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 - ) + assert abs(free_value - psutil_value) < 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 - ) + assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM def test_missing_sin_sout(self): with mock.patch('psutil._common.open', create=True) as m: @@ -649,15 +619,14 @@ def test_missing_sin_sout(self): warnings.simplefilter("always") ret = psutil.swap_memory() assert m.called - self.assertEqual(len(ws), 1) + assert len(ws) == 1 w = ws[0] - self.assertIn( - "'sin' and 'sout' swap memory stats couldn't " - "be determined", - str(w.message), + assert ( + "'sin' and 'sout' swap memory stats couldn't be determined" + in str(w.message) ) - self.assertEqual(ret.sin, 0) - self.assertEqual(ret.sout, 0) + assert ret.sin == 0 + assert ret.sout == 0 def test_no_vmstat_mocked(self): # see https://github.com/giampaolo/psutil/issues/722 @@ -668,15 +637,15 @@ def test_no_vmstat_mocked(self): warnings.simplefilter("always") ret = psutil.swap_memory() assert m.called - self.assertEqual(len(ws), 1) + assert len(ws) == 1 w = ws[0] - self.assertIn( + assert ( "'sin' and 'sout' swap memory stats couldn't " - "be determined and were set to 0", - str(w.message), + "be determined and were set to 0" + in str(w.message) ) - self.assertEqual(ret.sin, 0) - self.assertEqual(ret.sout, 0) + assert ret.sin == 0 + assert ret.sout == 0 def test_meminfo_against_sysinfo(self): # Make sure the content of /proc/meminfo about swap memory @@ -692,8 +661,8 @@ def test_meminfo_against_sysinfo(self): _, _, _, _, 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) + assert swap.total == total + assert abs(swap.free - free) < TOLERANCE_SYS_MEM def test_emulate_meminfo_has_no_metrics(self): # Emulate a case where /proc/meminfo provides no swap metrics @@ -716,17 +685,17 @@ def test_fields(self): 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) + assert 'steal' in fields else: - self.assertNotIn('steal', fields) + assert 'steal' not in fields if kernel_ver_info >= (2, 6, 24): - self.assertIn('guest', fields) + assert 'guest' in fields else: - self.assertNotIn('guest', fields) + assert 'guest' not in fields if kernel_ver_info >= (3, 2, 0): - self.assertIn('guest_nice', fields) + assert 'guest_nice' in fields else: - self.assertNotIn('guest_nice', fields) + assert 'guest_nice' not in fields @unittest.skipIf(not LINUX, "LINUX only") @@ -740,7 +709,7 @@ def test_against_sysdev_cpu_online(self): value = f.read().strip() if "-" in str(value): value = int(value.split('-')[1]) + 1 - self.assertEqual(psutil.cpu_count(), value) + assert psutil.cpu_count() == value @unittest.skipIf( not os.path.exists("/sys/devices/system/cpu"), @@ -749,18 +718,18 @@ def test_against_sysdev_cpu_online(self): 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) + assert 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) + assert 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) + assert psutil.cpu_count(logical=True) == num def test_emulate_fallbacks(self): import psutil._pslinux @@ -771,16 +740,16 @@ def test_emulate_fallbacks(self): with mock.patch( 'psutil._pslinux.os.sysconf', side_effect=ValueError ) as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) + assert 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) + assert psutil._pslinux.cpu_count_logical() is None + assert m.call_count == 2 # /proc/stat should be the last one - self.assertEqual(m.call_args[0][0], '/proc/stat') + assert m.call_args[0][0] == '/proc/stat' # Let's push this a bit further and make sure /proc/cpuinfo # parsing works as expected. @@ -790,12 +759,12 @@ def test_emulate_fallbacks(self): with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) + assert 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 psutil._pslinux.cpu_count_logical() == original assert m.called @@ -809,7 +778,7 @@ def test_against_lscpu(self): if not line.startswith('#'): fields = line.split(',') core_ids.add(fields[1]) - self.assertEqual(psutil.cpu_count(logical=False), len(core_ids)) + assert psutil.cpu_count(logical=False) == len(core_ids) def test_method_2(self): meth_1 = psutil._pslinux.cpu_count_cores() @@ -817,12 +786,12 @@ def test_method_2(self): meth_2 = psutil._pslinux.cpu_count_cores() assert m.called if meth_1 is not None: - self.assertEqual(meth_1, meth_2) + assert 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 psutil._pslinux.cpu_count_cores() is None assert m1.called assert m2.called @@ -861,11 +830,11 @@ def path_exists_mock(path): reload_module(psutil._pslinux) ret = psutil.cpu_freq() assert ret, ret - self.assertEqual(ret.max, 0.0) - self.assertEqual(ret.min, 0.0) + assert ret.max == 0.0 + assert ret.min == 0.0 for freq in psutil.cpu_freq(percpu=True): - self.assertEqual(freq.max, 0.0) - self.assertEqual(freq.min, 0.0) + assert freq.max == 0.0 + assert freq.min == 0.0 finally: reload_module(psutil._pslinux) reload_module(psutil) @@ -895,13 +864,13 @@ def open_mock(name, *args, **kwargs): 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) + assert 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) + assert freq.min == 600.0 if freq.max != 0.0: - self.assertEqual(freq.max, 700.0) + assert freq.max == 700.0 @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_emulate_multi_cpu(self): @@ -944,16 +913,16 @@ def open_mock(name, *args, **kwargs): 'psutil._pslinux.cpu_count_logical', return_value=2 ): freq = psutil.cpu_freq(percpu=True) - self.assertEqual(freq[0].current, 100.0) + assert freq[0].current == 100.0 if freq[0].min != 0.0: - self.assertEqual(freq[0].min, 200.0) + assert 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) + assert freq[0].max == 300.0 + assert freq[1].current == 400.0 if freq[1].min != 0.0: - self.assertEqual(freq[1].min, 500.0) + assert freq[1].min == 500.0 if freq[1].max != 0.0: - self.assertEqual(freq[1].max, 600.0) + assert freq[1].max == 600.0 @unittest.skipIf(not HAS_CPU_FREQ, "not supported") def test_emulate_no_scaling_cur_freq_file(self): @@ -976,7 +945,7 @@ def open_mock(name, *args, **kwargs): 'psutil._pslinux.cpu_count_logical', return_value=1 ): freq = psutil.cpu_freq() - self.assertEqual(freq.current, 200) + assert freq.current == 200 @unittest.skipIf(not LINUX, "LINUX only") @@ -991,7 +960,7 @@ class TestSystemCPUStats(PsutilTestCase): def test_interrupts(self): vmstat_value = vmstat("interrupts") psutil_value = psutil.cpu_stats().interrupts - self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) + assert abs(vmstat_value - psutil_value) < 500 @unittest.skipIf(not LINUX, "LINUX only") @@ -1002,9 +971,9 @@ def test_getloadavg(self): 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) + assert abs(float(proc_value[0]) - psutil_value[0]) < 1 + assert abs(float(proc_value[1]) - psutil_value[1]) < 1 + assert abs(float(proc_value[2]) - psutil_value[2]) < 1 # ===================================================================== @@ -1018,16 +987,14 @@ 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)) + assert 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)) + assert addr.address == get_ipv4_address(name) + assert addr.netmask == get_ipv4_netmask(name) if addr.broadcast is not None: - self.assertEqual( - addr.broadcast, get_ipv4_broadcast(name) - ) + assert addr.broadcast == get_ipv4_broadcast(name) else: - self.assertEqual(get_ipv4_broadcast(name), '0.0.0.0') + assert 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: @@ -1036,7 +1003,7 @@ def test_ips(self): # 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)) + assert address in get_ipv6_addresses(name) # XXX - not reliable when having virtual NICs installed by Docker. # @unittest.skipIf(not which('ip'), "'ip' utility not available") @@ -1065,15 +1032,15 @@ def test_against_ifconfig(self): 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]) + assert stats.isup == ('RUNNING' in out), out + assert 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())) + assert stats.mtu == int(f.read().strip()) @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") def test_flags(self): @@ -1091,7 +1058,7 @@ def test_flags(self): matches_found += 1 ifconfig_flags = set(match.group(2).lower().split(",")) psutil_flags = set(stats.flags.split(",")) - self.assertEqual(ifconfig_flags, psutil_flags) + assert ifconfig_flags == psutil_flags else: # ifconfig has a different output on CentOS 6 # let's try that @@ -1100,7 +1067,7 @@ def test_flags(self): matches_found += 1 ifconfig_flags = set(match.group(1).lower().split()) psutil_flags = set(stats.flags.split(",")) - self.assertEqual(ifconfig_flags, psutil_flags) + assert ifconfig_flags == psutil_flags if not matches_found: raise self.fail("no matches were found") @@ -1138,30 +1105,22 @@ def ifconfig(nic): ifconfig_ret = ifconfig(name) except RuntimeError: continue - self.assertAlmostEqual( - stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 10 - ) - self.assertAlmostEqual( - stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 10 - ) - self.assertAlmostEqual( - stats.packets_recv, ifconfig_ret['packets_recv'], delta=1024 - ) - self.assertAlmostEqual( - stats.packets_sent, ifconfig_ret['packets_sent'], delta=1024 + assert ( + abs(stats.bytes_recv - ifconfig_ret['bytes_recv']) < 1024 * 10 ) - self.assertAlmostEqual( - stats.errin, ifconfig_ret['errin'], delta=10 + assert ( + abs(stats.bytes_sent - ifconfig_ret['bytes_sent']) < 1024 * 10 ) - self.assertAlmostEqual( - stats.errout, ifconfig_ret['errout'], delta=10 + assert ( + abs(stats.packets_recv - ifconfig_ret['packets_recv']) < 1024 ) - self.assertAlmostEqual( - stats.dropin, ifconfig_ret['dropin'], delta=10 - ) - self.assertAlmostEqual( - stats.dropout, ifconfig_ret['dropout'], delta=10 + assert ( + abs(stats.packets_sent - ifconfig_ret['packets_sent']) < 1024 ) + assert abs(stats.errin - ifconfig_ret['errin']) < 10 + assert abs(stats.errout - ifconfig_ret['errout']) < 10 + assert abs(stats.dropin - ifconfig_ret['dropin']) < 10 + assert abs(stats.dropout - ifconfig_ret['dropout']) < 10 @unittest.skipIf(not LINUX, "LINUX only") @@ -1216,13 +1175,9 @@ def df(path): 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 - ) + assert usage.total == total + assert abs(usage.free - free) < TOLERANCE_DISK_USAGE + assert abs(usage.used - used) < TOLERANCE_DISK_USAGE def test_zfs_fs(self): # Test that ZFS partitions are returned. @@ -1248,7 +1203,7 @@ def test_zfs_fs(self): assert m1.called assert m2.called assert ret - self.assertEqual(ret[0].fstype, 'zfs') + assert ret[0].fstype == 'zfs' def test_emulate_realpath_fail(self): # See: https://github.com/giampaolo/psutil/issues/1307 @@ -1256,7 +1211,7 @@ def test_emulate_realpath_fail(self): with mock.patch( 'os.path.realpath', return_value='/non/existent' ) as m: - with self.assertRaises(FileNotFoundError): + with pytest.raises(FileNotFoundError): psutil.disk_partitions() assert m.called finally: @@ -1274,15 +1229,15 @@ def test_emulate_kernel_2_4(self): '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) + assert ret.read_count == 1 + assert ret.read_merged_count == 2 + assert ret.read_bytes == 3 * SECTOR_SIZE + assert ret.read_time == 4 + assert ret.write_count == 5 + assert ret.write_merged_count == 6 + assert ret.write_bytes == 7 * SECTOR_SIZE + assert ret.write_time == 8 + assert ret.busy_time == 10 def test_emulate_kernel_2_6_full(self): # Tests /proc/diskstats parsing format for 2.6 kernels, @@ -1294,15 +1249,15 @@ def test_emulate_kernel_2_6_full(self): '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) + assert ret.read_count == 1 + assert ret.read_merged_count == 2 + assert ret.read_bytes == 3 * SECTOR_SIZE + assert ret.read_time == 4 + assert ret.write_count == 5 + assert ret.write_merged_count == 6 + assert ret.write_bytes == 7 * SECTOR_SIZE + assert ret.write_time == 8 + assert ret.busy_time == 10 def test_emulate_kernel_2_6_limited(self): # Tests /proc/diskstats parsing format for 2.6 kernels, @@ -1315,16 +1270,16 @@ def test_emulate_kernel_2_6_limited(self): '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) + assert ret.read_count == 1 + assert ret.read_bytes == 2 * SECTOR_SIZE + assert ret.write_count == 3 + assert 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) + assert ret.read_merged_count == 0 + assert ret.read_time == 0 + assert ret.write_merged_count == 0 + assert ret.write_time == 0 + assert ret.busy_time == 0 def test_emulate_include_partitions(self): # Make sure that when perdisk=True disk partitions are returned, @@ -1339,11 +1294,11 @@ def test_emulate_include_partitions(self): '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) + assert len(ret) == 2 + assert ret['nvme0n1'].read_count == 1 + assert ret['nvme0n1p1'].read_count == 1 + assert ret['nvme0n1'].write_count == 5 + assert ret['nvme0n1p1'].write_count == 5 def test_emulate_exclude_partitions(self): # Make sure that when perdisk=False partitions (e.g. 'sda1', @@ -1358,7 +1313,7 @@ def test_emulate_exclude_partitions(self): 'psutil._pslinux.is_storage_device', return_value=False ): ret = psutil.disk_io_counters(perdisk=False, nowrap=False) - self.assertIsNone(ret) + assert ret is None def is_storage_device(name): return name == 'nvme0n1' @@ -1374,8 +1329,8 @@ def is_storage_device(name): 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) + assert ret.read_count == 1 + assert ret.write_count == 5 def test_emulate_use_sysfs(self): def exists(path): @@ -1386,7 +1341,7 @@ def exists(path): 'psutil._pslinux.os.path.exists', create=True, side_effect=exists ): wsysfs = psutil.disk_io_counters(perdisk=True) - self.assertEqual(len(wprocfs), len(wsysfs)) + assert len(wprocfs) == len(wsysfs) def test_emulate_not_impl(self): def exists(path): @@ -1395,7 +1350,8 @@ def exists(path): with mock.patch( 'psutil._pslinux.os.path.exists', create=True, side_effect=exists ): - self.assertRaises(NotImplementedError, psutil.disk_io_counters) + with pytest.raises(NotImplementedError): + psutil.disk_io_counters() @unittest.skipIf(not LINUX, "LINUX only") @@ -1410,19 +1366,21 @@ def test_call_methods(self): if os.path.exists("/proc/partitions"): finder.ask_proc_partitions() else: - self.assertRaises(FileNotFoundError, finder.ask_proc_partitions) + with pytest.raises(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) + with pytest.raises(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()) + assert finder.find() is not None a = b = c = None if os.path.exists("/proc/partitions"): @@ -1435,18 +1393,18 @@ def test_comparisons(self): base = a or b or c if base and a: - self.assertEqual(base, a) + assert base == a if base and b: - self.assertEqual(base, b) + assert base == b if base and c: - self.assertEqual(base, c) + assert 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) + assert psutil_value == findmnt_value def test_disk_partitions_mocked(self): with mock.patch( @@ -1456,10 +1414,10 @@ def test_disk_partitions_mocked(self): part = psutil.disk_partitions()[0] assert m.called if not GITHUB_ACTIONS: - self.assertNotEqual(part.device, "/dev/root") - self.assertEqual(part.device, RootFsDeviceFinder().find()) + assert part.device != "/dev/root" + assert part.device == RootFsDeviceFinder().find() else: - self.assertEqual(part.device, "/dev/root") + assert part.device == "/dev/root" # ===================================================================== @@ -1472,7 +1430,7 @@ 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)) + assert int(vmstat_value) == int(psutil_value) def test_no_procfs_on_import(self): my_procfs = self.get_testfn() @@ -1495,28 +1453,32 @@ def open_mock(name, *args, **kwargs): 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 - ) + with pytest.raises(IOError): + psutil.cpu_times() + with pytest.raises(IOError): + psutil.cpu_times(percpu=True) + with pytest.raises(IOError): + psutil.cpu_percent() + with pytest.raises(IOError): + psutil.cpu_percent(percpu=True) + with pytest.raises(IOError): + psutil.cpu_times_percent() + with pytest.raises(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) + assert psutil.cpu_percent() == 0 + assert 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) + assert 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) + assert sum(map(sum, per_cpu_times_percent)) == 0 # much user, very busy with open(os.path.join(my_procfs, 'stat'), 'w') as f: @@ -1524,17 +1486,17 @@ def open_mock(name, *args, **kwargs): 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 + assert psutil.cpu_percent() != 0 + assert sum(psutil.cpu_percent(percpu=True)) != 0 + assert sum(psutil.cpu_times_percent()) != 0 + assert ( + sum(map(sum, psutil.cpu_times_percent(percpu=True))) != 0 ) finally: shutil.rmtree(my_procfs) reload_module(psutil) - self.assertEqual(psutil.PROCFS_PATH, '/proc') + assert psutil.PROCFS_PATH == '/proc' def test_cpu_steal_decrease(self): # Test cumulative cpu stats decrease. We should ignore this. @@ -1566,42 +1528,52 @@ def test_cpu_steal_decrease(self): 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) + assert cpu_percent != 0 + assert sum(cpu_percent_percpu) != 0 + assert sum(cpu_times_percent) != 0 + assert sum(cpu_times_percent) != 100.0 + assert sum(map(sum, cpu_times_percent_percpu)) != 0 + assert sum(map(sum, cpu_times_percent_percpu)) != 100.0 + assert cpu_times_percent.steal == 0 + assert 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) + with pytest.raises(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")) + assert user.host not in (":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) + with pytest.raises(IOError): + psutil.virtual_memory() + with pytest.raises(IOError): + psutil.cpu_times() + with pytest.raises(IOError): + psutil.cpu_times(percpu=True) + with pytest.raises(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) + with pytest.raises(IOError): + psutil.net_connections() + with pytest.raises(IOError): + psutil.net_io_counters() + with pytest.raises(IOError): + psutil.net_if_stats() # self.assertRaises(IOError, psutil.disk_io_counters) - self.assertRaises(IOError, psutil.disk_partitions) - self.assertRaises(psutil.NoSuchProcess, psutil.Process) + with pytest.raises(IOError): + psutil.disk_partitions() + with pytest.raises(psutil.NoSuchProcess): + psutil.Process() finally: psutil.PROCFS_PATH = "/proc" @@ -1616,12 +1588,12 @@ def test_issue_687(self): with ThreadTask(): p = psutil.Process() threads = p.threads() - self.assertEqual(len(threads), 3 if QEMU_USER else 2) + assert len(threads) == (3 if QEMU_USER else 2) tid = sorted(threads, key=lambda x: x.id)[1].id - self.assertNotEqual(p.pid, tid) + assert p.pid != tid pt = psutil.Process(tid) pt.as_dict() - self.assertNotIn(tid, psutil.pids()) + assert tid not in psutil.pids() def test_pid_exists_no_proc_status(self): # Internally pid_exists relies on /proc/{pid}/status. @@ -1645,7 +1617,7 @@ 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) + assert abs(acpi_value - psutil_value) < 1 def test_emulate_power_plugged(self): # Pretend the AC power cable is connected. @@ -1658,9 +1630,10 @@ def open_mock(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 psutil.sensors_battery().power_plugged is True + assert ( + psutil.sensors_battery().secsleft + == psutil.POWER_TIME_UNLIMITED ) assert m.called @@ -1678,7 +1651,7 @@ def open_mock(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 psutil.sensors_battery().power_plugged is True assert m.called def test_emulate_power_not_plugged(self): @@ -1692,7 +1665,7 @@ def open_mock(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 psutil.sensors_battery().power_plugged is False assert m.called def test_emulate_power_not_plugged_2(self): @@ -1709,7 +1682,7 @@ def open_mock(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 psutil.sensors_battery().power_plugged is False assert m.called def test_emulate_power_undetermined(self): @@ -1729,7 +1702,7 @@ def open_mock(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 psutil.sensors_battery().power_plugged is None assert m.called def test_emulate_energy_full_0(self): @@ -1737,7 +1710,7 @@ def test_emulate_energy_full_0(self): with mock_open_content( {"/sys/class/power_supply/BAT0/energy_full": b"0"} ) as m: - self.assertEqual(psutil.sensors_battery().percent, 0) + assert psutil.sensors_battery().percent == 0 assert m.called def test_emulate_energy_full_not_avail(self): @@ -1754,7 +1727,7 @@ def test_emulate_energy_full_not_avail(self): with mock_open_content( {"/sys/class/power_supply/BAT0/capacity": b"88"} ): - self.assertEqual(psutil.sensors_battery().percent, 88) + assert psutil.sensors_battery().percent == 88 def test_emulate_no_power(self): # Emulate a case where /AC0/online file nor /BAT0/status exist. @@ -1768,7 +1741,7 @@ def test_emulate_no_power(self): "/sys/class/power_supply/BAT0/status", IOError(errno.ENOENT, ""), ): - self.assertIsNone(psutil.sensors_battery().power_plugged) + assert psutil.sensors_battery().power_plugged is None @unittest.skipIf(not LINUX, "LINUX only") @@ -1788,7 +1761,7 @@ def open_mock(name, *args, **kwargs): 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 psutil.sensors_battery() is not None assert mlistdir.called assert mopen.called @@ -1818,10 +1791,10 @@ def open_mock(name, *args, **kwargs): '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) + assert temp.label == 'label' + assert temp.current == 30.0 + assert temp.high == 40.0 + assert temp.critical == 50.0 def test_emulate_class_thermal(self): def open_mock(name, *args, **kwargs): @@ -1855,10 +1828,10 @@ def glob_mock(path): 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) + assert temp.label == '' # noqa + assert temp.current == 30.0 + assert temp.high == 50.0 + assert temp.critical == 50.0 @unittest.skipIf(not LINUX, "LINUX only") @@ -1881,8 +1854,8 @@ def open_mock(name, *args, **kwargs): '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) + assert fan.label == 'label' + assert fan.current == 2000 # ===================================================================== @@ -1897,13 +1870,12 @@ 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, + assert ( + abs(uss - sum([x.private_dirty + x.private_clean for x in maps])) + < 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) + assert abs(pss - sum([x.pss for x in maps])) < 4096 + assert abs(swap - sum([x.swap for x in maps])) < 4096 def test_parse_smaps_mocked(self): # See: https://github.com/giampaolo/psutil/issues/1222 @@ -1934,9 +1906,9 @@ def test_parse_smaps_mocked(self): 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) + assert uss == (6 + 7 + 14) * 1024 + assert pss == 3 * 1024 + assert swap == 15 * 1024 # On PYPY file descriptors are not closed fast enough. @unittest.skipIf(PYPY, "unreliable on PYPY") @@ -1954,25 +1926,25 @@ def get_test_file(fname): testfn = self.get_testfn() with open(testfn, "w"): - self.assertEqual(get_test_file(testfn).mode, "w") + assert get_test_file(testfn).mode == "w" with open(testfn): - self.assertEqual(get_test_file(testfn).mode, "r") + assert get_test_file(testfn).mode == "r" with open(testfn, "a"): - self.assertEqual(get_test_file(testfn).mode, "a") + assert get_test_file(testfn).mode == "a" with open(testfn, "r+"): - self.assertEqual(get_test_file(testfn).mode, "r+") + assert get_test_file(testfn).mode == "r+" with open(testfn, "w+"): - self.assertEqual(get_test_file(testfn).mode, "r+") + assert get_test_file(testfn).mode == "r+" with open(testfn, "a+"): - self.assertEqual(get_test_file(testfn).mode, "a+") + assert 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") + assert get_test_file(testfn).mode == "w" safe_rmpath(testfn) with open(testfn, "x+"): - self.assertEqual(get_test_file(testfn).mode, "r+") + assert get_test_file(testfn).mode == "r+" def test_open_files_file_gone(self): # simulates a file which gets deleted during open_files() @@ -1986,7 +1958,7 @@ def test_open_files_file_gone(self): 'psutil._pslinux.os.readlink', side_effect=OSError(errno.ENOENT, ""), ) as m: - self.assertEqual(p.open_files(), []) + assert p.open_files() == [] assert m.called # also simulate the case where os.readlink() returns EINVAL # in which case psutil is supposed to 'continue' @@ -1994,7 +1966,7 @@ def test_open_files_file_gone(self): 'psutil._pslinux.os.readlink', side_effect=OSError(errno.EINVAL, ""), ) as m: - self.assertEqual(p.open_files(), []) + assert p.open_files() == [] assert m.called def test_open_files_fd_gone(self): @@ -2010,7 +1982,7 @@ def test_open_files_fd_gone(self): with mock.patch( patch_point, side_effect=IOError(errno.ENOENT, "") ) as m: - self.assertEqual(p.open_files(), []) + assert p.open_files() == [] assert m.called def test_open_files_enametoolong(self): @@ -2027,7 +1999,7 @@ def test_open_files_enametoolong(self): patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") ) as m: with mock.patch("psutil._pslinux.debug"): - self.assertEqual(p.open_files(), []) + assert p.open_files() == [] assert m.called # --- mocked tests @@ -2036,7 +2008,7 @@ 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 psutil._pslinux.Process(os.getpid()).terminal() is None assert m.called # TODO: re-enable this test. @@ -2054,13 +2026,13 @@ def test_cmdline_mocked(self): with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar']) + assert 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 p.cmdline() == ['foo', 'bar', ''] assert m.called def test_cmdline_spaces_mocked(self): @@ -2070,13 +2042,13 @@ def test_cmdline_spaces_mocked(self): with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar']) + assert 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 p.cmdline() == ['foo', 'bar', ''] assert m.called def test_cmdline_mixed_separators(self): @@ -2087,15 +2059,15 @@ def test_cmdline_mixed_separators(self): with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar']) + assert 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") + assert psutil.Process().exe() == "/home/foo" + assert psutil.Process().cwd() == "/home/foo" def test_threads_mocked(self): # Test the case where os.listdir() returns a file (thread) @@ -2113,7 +2085,7 @@ def open_mock_1(name, *args, **kwargs): with mock.patch(patch_point, side_effect=open_mock_1) as m: ret = psutil.Process().threads() assert m.called - self.assertEqual(ret, []) + assert ret == [] # ...but if it bumps into something != ENOENT we want an # exception. @@ -2124,7 +2096,8 @@ def open_mock_2(name, *args, **kwargs): return orig_open(name, *args, **kwargs) with mock.patch(patch_point, side_effect=open_mock_2): - self.assertRaises(psutil.AccessDenied, psutil.Process().threads) + with pytest.raises(psutil.AccessDenied): + psutil.Process().threads() def test_exe_mocked(self): with mock.patch( @@ -2136,7 +2109,7 @@ def test_exe_mocked(self): ): ret = psutil.Process().exe() assert m.called - self.assertEqual(ret, "") + assert ret == "" # noqa def test_issue_1014(self): # Emulates a case where smaps file does not exist. In this case @@ -2145,7 +2118,7 @@ def test_issue_1014(self): '/proc/%s/smaps' % os.getpid(), IOError(errno.ENOENT, "") ) as m: p = psutil.Process() - with self.assertRaises(FileNotFoundError): + with pytest.raises(FileNotFoundError): p.memory_maps() assert m.called @@ -2162,12 +2135,12 @@ def test_rlimit_zombie(self): ) as m2: p = psutil.Process() p.name() - with self.assertRaises(psutil.ZombieProcess) as exc: + with pytest.raises(psutil.ZombieProcess) as cm: 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()) + assert cm.value.pid == p.pid + assert cm.value.name == p.name() def test_stat_file_parsing(self): args = [ @@ -2217,19 +2190,17 @@ def test_stat_file_parsing(self): 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() - ) + assert p.name() == 'cat' + assert p.status() == psutil.STATUS_ZOMBIE + assert p.ppid() == 1 + assert 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) + assert cpu.user == 2 / CLOCK_TICKS + assert cpu.system == 3 / CLOCK_TICKS + assert cpu.children_user == 4 / CLOCK_TICKS + assert cpu.children_system == 5 / CLOCK_TICKS + assert cpu.iowait == 7 / CLOCK_TICKS + assert p.cpu_num() == 6 def test_status_file_parsing(self): content = textwrap.dedent("""\ @@ -2242,18 +2213,18 @@ def test_status_file_parsing(self): 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) + assert p.num_ctx_switches().voluntary == 12 + assert p.num_ctx_switches().involuntary == 13 + assert p.num_threads() == 66 uids = p.uids() - self.assertEqual(uids.real, 1000) - self.assertEqual(uids.effective, 1001) - self.assertEqual(uids.saved, 1002) + assert uids.real == 1000 + assert uids.effective == 1001 + assert 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))) + assert gids.real == 1004 + assert gids.effective == 1005 + assert gids.saved == 1006 + assert p._proc._get_eligible_cpus() == list(range(8)) def test_net_connections_enametoolong(self): # Simulate a case where /proc/{pid}/fd/{fd} symlink points to @@ -2265,7 +2236,7 @@ def test_net_connections_enametoolong(self): ) as m: p = psutil.Process() with mock.patch("psutil._pslinux.debug"): - self.assertEqual(p.net_connections(), []) + assert p.net_connections() == [] assert m.called @@ -2298,47 +2269,45 @@ def read_status_file(self, linestart): def test_name(self): value = self.read_status_file("Name:") - self.assertEqual(self.proc.name(), value) + assert self.proc.name() == value @unittest.skipIf(QEMU_USER, "QEMU user not supported") 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) + assert self.proc.status() == value def test_ppid(self): value = self.read_status_file("PPid:") - self.assertEqual(self.proc.ppid(), value) + assert self.proc.ppid() == value def test_num_threads(self): value = self.read_status_file("Threads:") - self.assertEqual(self.proc.num_threads(), value) + assert 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) + assert 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) + assert 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) + assert self.proc.num_ctx_switches().voluntary == value value = self.read_status_file("nonvoluntary_ctxt_switches:") - self.assertEqual(self.proc.num_ctx_switches().involuntary, value) + assert 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)) - ) + assert self.proc.cpu_affinity() == list(range(min_, max_ + 1)) def test_cpu_affinity_eligible_cpus(self): value = self.read_status_file("Cpus_allowed_list:") @@ -2359,5 +2328,5 @@ def test_cpu_affinity_eligible_cpus(self): 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 psutil._psplatform.readlink("bar") == "foo" assert m.called diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 66d28b220f..adafea947c 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -18,6 +18,8 @@ import sys import unittest +import pytest + import psutil import psutil.tests from psutil import POSIX @@ -60,26 +62,24 @@ class TestSpecialMethods(PsutilTestCase): def test_check_pid_range(self): - with self.assertRaises(OverflowError): + with pytest.raises(OverflowError): psutil._psplatform.cext.check_pid_range(2**128) - with self.assertRaises(psutil.NoSuchProcess): + with pytest.raises(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) + assert "psutil.Process" in r + assert "pid=%s" % p.pid in r + assert "name='%s'" % str(p.name()) in r.replace("name=u'", "name='") + assert "status=" in r + assert "exitcode=" not in r p.terminate() p.wait() r = func(p) - self.assertIn("status='terminated'", r) - self.assertIn("exitcode=", r) + assert "status='terminated'" in r + assert "exitcode=" in r with mock.patch.object( psutil.Process, @@ -88,9 +88,9 @@ def test_process__repr__(self, func=repr): ): p = psutil.Process() r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("status='zombie'", r) - self.assertNotIn("name=", r) + assert "pid=%s" % p.pid in r + assert "status='zombie'" in r + assert "name=" not in r with mock.patch.object( psutil.Process, "name", @@ -98,9 +98,9 @@ def test_process__repr__(self, func=repr): ): p = psutil.Process() r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("terminated", r) - self.assertNotIn("name=", r) + assert "pid=%s" % p.pid in r + assert "terminated" in r + assert "name=" not in r with mock.patch.object( psutil.Process, "name", @@ -108,106 +108,104 @@ def test_process__repr__(self, func=repr): ): p = psutil.Process() r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertNotIn("name=", r) + assert "pid=%s" % p.pid in r + assert "name=" not in r def test_process__str__(self): self.test_process__repr__(func=str) def test_error__repr__(self): - self.assertEqual(repr(psutil.Error()), "psutil.Error()") + assert repr(psutil.Error()) == "psutil.Error()" def test_error__str__(self): - self.assertEqual(str(psutil.Error()), "") + assert str(psutil.Error()) == "" # noqa def test_no_such_process__repr__(self): - self.assertEqual( - repr(psutil.NoSuchProcess(321)), - "psutil.NoSuchProcess(pid=321, msg='process no longer exists')", + assert ( + 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')", + assert ( + 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)", + assert ( + 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')", + assert ( + 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")', + assert ( + 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')", + assert ( + 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)", + assert ( + 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')", + assert ( + 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')", + assert repr(psutil.AccessDenied(321)) == "psutil.AccessDenied(pid=321)" + assert ( + 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')", + assert str(psutil.AccessDenied(321)) == "(pid=321)" + assert ( + 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')", + assert ( + 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')", + assert ( + 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')", + assert str(psutil.TimeoutExpired(5)) == "timeout after 5 seconds" + assert ( + 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) + assert p1 == p2 p2._ident = (0, 0) - self.assertNotEqual(p1, p2) - self.assertNotEqual(p1, 'foo') + assert p1 != p2 + assert p1 != 'foo' def test_process__hash__(self): s = set([psutil.Process(), psutil.Process()]) - self.assertEqual(len(s), 1) + assert len(s) == 1 # =================================================================== @@ -247,18 +245,19 @@ def test__all__(self): # 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) + assert name in dir_psutil def test_version(self): - self.assertEqual( - '.'.join([str(x) for x in psutil.version_info]), psutil.__version__ + assert ( + '.'.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()) + assert 'foo' not in p.as_dict() def test_serialization(self): def check(ret): @@ -266,7 +265,7 @@ def check(ret): a = pickle.dumps(ret) b = pickle.loads(a) - self.assertEqual(ret, b) + assert ret == b # --- process APIs @@ -307,39 +306,39 @@ def check(ret): psutil.NoSuchProcess(pid=4567, name='name', msg='msg') ) ) - self.assertIsInstance(b, psutil.NoSuchProcess) - self.assertEqual(b.pid, 4567) - self.assertEqual(b.name, 'name') - self.assertEqual(b.msg, 'msg') + assert isinstance(b, psutil.NoSuchProcess) + assert b.pid == 4567 + assert b.name == 'name' + assert b.msg == 'msg' b = pickle.loads( pickle.dumps( psutil.ZombieProcess(pid=4567, name='name', ppid=42, msg='msg') ) ) - self.assertIsInstance(b, psutil.ZombieProcess) - self.assertEqual(b.pid, 4567) - self.assertEqual(b.ppid, 42) - self.assertEqual(b.name, 'name') - self.assertEqual(b.msg, 'msg') + assert isinstance(b, psutil.ZombieProcess) + assert b.pid == 4567 + assert b.ppid == 42 + assert b.name == 'name' + assert b.msg == 'msg' b = pickle.loads( pickle.dumps(psutil.AccessDenied(pid=123, name='name', msg='msg')) ) - self.assertIsInstance(b, psutil.AccessDenied) - self.assertEqual(b.pid, 123) - self.assertEqual(b.name, 'name') - self.assertEqual(b.msg, 'msg') + assert isinstance(b, psutil.AccessDenied) + assert b.pid == 123 + assert b.name == 'name' + assert b.msg == 'msg' b = pickle.loads( pickle.dumps( psutil.TimeoutExpired(seconds=33, pid=4567, name='name') ) ) - self.assertIsInstance(b, psutil.TimeoutExpired) - self.assertEqual(b.seconds, 33) - self.assertEqual(b.pid, 4567) - self.assertEqual(b.name, 'name') + assert isinstance(b, psutil.TimeoutExpired) + assert b.seconds == 33 + assert b.pid == 4567 + assert b.name == 'name' # # XXX: https://github.com/pypa/setuptools/pull/2896 # @unittest.skipIf(APPVEYOR, "temporarily disabled due to setuptools bug") @@ -367,7 +366,7 @@ def test_ad_on_process_creation(self): with mock.patch.object( psutil.Process, 'create_time', side_effect=ValueError ) as meth: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): psutil.Process() assert meth.called @@ -376,9 +375,9 @@ def test_sanity_version_check(self): with mock.patch( "psutil._psplatform.cext.version", return_value="0.0.0" ): - with self.assertRaises(ImportError) as cm: + with pytest.raises(ImportError) as cm: reload_module(psutil) - self.assertIn("version conflict", str(cm.exception).lower()) + assert "version conflict" in str(cm.value).lower() # =================================================================== @@ -396,32 +395,30 @@ def run_against(self, obj, expected_retval=None): # no args for _ in range(2): ret = obj() - self.assertEqual(self.calls, [((), {})]) + assert self.calls == [((), {})] if expected_retval is not None: - self.assertEqual(ret, expected_retval) + assert ret == expected_retval # with args for _ in range(2): ret = obj(1) - self.assertEqual(self.calls, [((), {}), ((1,), {})]) + assert self.calls == [((), {}), ((1,), {})] if expected_retval is not None: - self.assertEqual(ret, expected_retval) + assert ret == expected_retval # with args + kwargs for _ in range(2): ret = obj(1, bar=2) - self.assertEqual( - self.calls, [((), {}), ((1,), {}), ((1,), {'bar': 2})] - ) + assert self.calls == [((), {}), ((1,), {}), ((1,), {'bar': 2})] if expected_retval is not None: - self.assertEqual(ret, expected_retval) + assert ret == expected_retval # clear cache - self.assertEqual(len(self.calls), 3) + assert 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) + assert ret == expected_retval + assert len(self.calls) == 4 # docstring - self.assertEqual(obj.__doc__, "My docstring.") + assert obj.__doc__ == "My docstring." def test_function(self): @memoize @@ -446,7 +443,7 @@ def bar(self): baseclass = self self.run_against(Foo, expected_retval=None) - self.assertEqual(Foo().bar(), 22) + assert Foo().bar() == 22 def test_class_singleton(self): # @memoize can be used against classes to create singletons @@ -455,11 +452,11 @@ 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))) + assert Bar() is Bar() + assert id(Bar()) == id(Bar()) + assert id(Bar(1)) == id(Bar(1)) + assert id(Bar(1, foo=3)) == id(Bar(1, foo=3)) + assert id(Bar(1)) != id(Bar(2)) def test_staticmethod(self): class Foo: @@ -499,28 +496,28 @@ def foo(*args, **kwargs): for _ in range(2): ret = foo() expected = ((), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 1) + assert ret == expected + assert len(calls) == 1 # with args for _ in range(2): ret = foo(1) expected = ((1,), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 2) + assert ret == expected + assert 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) + assert ret == expected + assert len(calls) == 3 # clear cache foo.cache_clear() ret = foo() expected = ((), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 4) + assert ret == expected + assert len(calls) == 4 # docstring - self.assertEqual(foo.__doc__, "Foo docstring.") + assert foo.__doc__ == "Foo docstring." class TestCommonModule(PsutilTestCase): @@ -534,43 +531,42 @@ def foo(self): calls = [] f.foo() f.foo() - self.assertEqual(len(calls), 2) + assert len(calls) == 2 # activate calls = [] f.foo.cache_activate(f) f.foo() f.foo() - self.assertEqual(len(calls), 1) + assert len(calls) == 1 # deactivate calls = [] f.foo.cache_deactivate(f) f.foo() f.foo() - self.assertEqual(len(calls), 2) + assert 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"): ""} - ) + assert parse_environ_block("a=1\0") == {k("a"): "1"} + assert parse_environ_block("a=1\0b=2\0\0") == { + k("a"): "1", + k("b"): "2", + } + assert 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"}, - ) + assert 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"}) + assert parse_environ_block("xxx\0a=1\0") == {k("a"): "1"} + assert 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"}) + assert parse_environ_block("a=1\0b=2") == {k("a"): "1"} def test_supports_ipv6(self): self.addCleanup(supports_ipv6.cache_clear) @@ -604,7 +600,7 @@ def test_supports_ipv6(self): supports_ipv6.cache_clear() assert s.called else: - with self.assertRaises(socket.error): + with pytest.raises(socket.error): sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) try: sock.bind(("::1", 0)) @@ -618,11 +614,13 @@ def test_isfile_strict(self): with mock.patch( 'psutil._common.os.stat', side_effect=OSError(errno.EPERM, "foo") ): - self.assertRaises(OSError, isfile_strict, this_file) + with pytest.raises(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 pytest.raises(OSError): + isfile_strict(this_file) with mock.patch( 'psutil._common.os.stat', side_effect=OSError(errno.ENOENT, "foo") ): @@ -641,15 +639,15 @@ def test_debug(self): sys.stderr.flush() msg = f.getvalue() assert msg.startswith("psutil-debug"), msg - self.assertIn("hello", msg) - self.assertIn(__file__.replace('.pyc', '.py'), msg) + assert "hello" in msg + assert __file__.replace('.pyc', '.py') in 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) + assert "ignoring ValueError" in msg + assert "'this is an error'" in msg # supposed to use str(exc), because of extra info about file name with redirect_stderr(StringIO()) as f: @@ -657,19 +655,21 @@ def test_debug(self): exc.filename = "/foo" debug(exc) msg = f.getvalue() - self.assertIn("no such file", msg) - self.assertIn("/foo", msg) + assert "no such file" in msg + assert "/foo" in 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") + assert cat(testfn) == "foo" + assert bcat(testfn) == b"foo" + with pytest.raises(FileNotFoundError): + cat(testfn + '-invalid') + with pytest.raises(FileNotFoundError): + bcat(testfn + '-invalid') + assert cat(testfn + '-invalid', fallback="bar") == "bar" + assert bcat(testfn + '-invalid', fallback="bar") == "bar" # =================================================================== @@ -688,105 +688,89 @@ def setUp(self): def test_first_call(self): input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) + assert 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) + assert wrap_numbers(input, 'disk_io') == input + assert 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) + assert wrap_numbers(input, 'disk_io') == input input = {'disk1': nt(10, 15, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) + assert wrap_numbers(input, 'disk_io') == input input = {'disk1': nt(20, 25, 30)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) + assert wrap_numbers(input, 'disk_io') == input input = {'disk1': nt(20, 25, 30)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) + assert 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) + assert 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)} - ) + assert 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)} - ) + assert 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)} - ) + assert 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)} - ) + assert 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)} - ) + assert 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)} - ) + assert 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)} - ) + assert 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)} - ) + assert 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) + assert 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) + assert wrap_numbers(input, 'disk_io') == input input = {'disk1': nt(8, 8, 8)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) + assert 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) + assert 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)}, - ) + assert 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) + assert 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) + assert 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) + assert 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)}, - ) + assert wrap_numbers(input, 'disk_io') == { + 'disk1': nt(50, 50, 50), + 'disk2': nt(100, 100, 110), + } def test_real_data(self): d = { @@ -795,8 +779,8 @@ def test_real_data(self): '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) + assert wrap_numbers(d, 'disk_io') == d + assert wrap_numbers(d, 'disk_io') == d # decrease this ↓ d = { 'nvme0n1': (100, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), @@ -805,7 +789,7 @@ def test_real_data(self): 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348), } out = wrap_numbers(d, 'disk_io') - self.assertEqual(out['nvme0n1'][0], 400) + assert out['nvme0n1'][0] == 400 # --- cache tests @@ -813,9 +797,9 @@ 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': {}}) + assert cache[0] == {'disk_io': input} + assert cache[1] == {'disk_io': {}} + assert cache[2] == {'disk_io': {}} def test_cache_call_twice(self): input = {'disk1': nt(5, 5, 5)} @@ -823,12 +807,11 @@ def test_cache_call_twice(self): 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': {}}) + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0} + } + assert cache[2] == {'disk_io': {}} def test_cache_wrap(self): # let's say 100 is the threshold @@ -839,53 +822,46 @@ def test_cache_wrap(self): 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)])}}) + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100} + } + assert 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)])}} - ) + assert cache[1] == { + 'disk_io': { + ('disk1', 0): 0, + ('disk1', 1): 0, + ('disk1', 2): 100, + } + } + assert 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 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}) + assert 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)])}}) + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190} + } + assert cache[2] == {'disk_io': {'disk1': set([('disk1', 2)])}} def test_cache_changing_keys(self): input = {'disk1': nt(5, 5, 5)} @@ -893,19 +869,18 @@ def test_cache_changing_keys(self): 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': {}}) + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0} + } + assert 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(), ({}, {}, {})) + assert wrap_numbers.cache_info() == ({}, {}, {}) wrap_numbers.cache_clear('disk_io') wrap_numbers.cache_clear('?!?') @@ -917,18 +892,18 @@ def test_cache_clear_public_apis(self): 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) + assert 'psutil.disk_io_counters' in cache + assert 'psutil.net_io_counters' in 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) + assert 'psutil.net_io_counters' in cache + assert 'psutil.disk_io_counters' not in cache psutil.net_io_counters.cache_clear() caches = wrap_numbers.cache_info() - self.assertEqual(caches, ({}, {}, {})) + assert caches == ({}, {}, {}) # =================================================================== @@ -1039,7 +1014,7 @@ def test_iotop(self): def test_pidof(self): output = self.assert_stdout('pidof.py', psutil.Process().name()) - self.assertIn(str(os.getpid()), output) + assert str(os.getpid()) in output @unittest.skipIf(not WINDOWS, "WINDOWS only") def test_winservices(self): diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index a45b3853d6..d72bc76f44 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -67,12 +67,10 @@ def test_process_create_time(self): 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)) + assert hhmmss == time.strftime( + "%H:%M:%S", time.localtime(start_psutil) ) + assert year == time.strftime("%Y", time.localtime(start_psutil)) @unittest.skipIf(not MACOS, "MACOS only") @@ -100,24 +98,20 @@ def df(path): 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 - ) + assert part.device == dev + assert usage.total == total + assert abs(usage.free - free) < TOLERANCE_DISK_USAGE + assert abs(usage.used - used) < TOLERANCE_DISK_USAGE # --- cpu def test_cpu_count_logical(self): num = sysctl("sysctl hw.logicalcpu") - self.assertEqual(num, psutil.cpu_count(logical=True)) + assert 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)) + assert num == psutil.cpu_count(logical=False) # TODO: remove this once 1892 is fixed @unittest.skipIf( @@ -125,45 +119,39 @@ def test_cpu_count_cores(self): ) 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") - ) + assert freq.current * 1000 * 1000 == sysctl("sysctl hw.cpufrequency") + assert freq.min * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_min") + assert 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) + assert 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) + assert abs(psutil_val - vmstat_val) < 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) + assert abs(psutil_val - vmstat_val) < 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) + assert abs(psutil_val - vmstat_val) < 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) + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM # --- swap mem @@ -171,13 +159,13 @@ def test_vmem_wired(self): 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) + assert abs(psutil_val - vmstat_val) < 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) + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM # --- network @@ -188,10 +176,8 @@ def test_net_if_stats(self): 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]) - ) + assert stats.isup == ('RUNNING' in out), out + assert stats.mtu == int(re.findall(r'mtu (\d+)', out)[0]) # --- sensors_battery @@ -202,5 +188,5 @@ def test_sensors_battery(self): 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)) + assert psutil_result.power_plugged == power_plugged + assert psutil_result.percent == int(percent) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 79c7252d0c..727b16c28a 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -15,6 +15,8 @@ import time import unittest +import pytest + import psutil from psutil import AIX from psutil import BSD @@ -152,22 +154,22 @@ def tearDownClass(cls): def test_ppid(self): ppid_ps = ps('ppid', self.pid) ppid_psutil = psutil.Process(self.pid).ppid() - self.assertEqual(ppid_ps, ppid_psutil) + assert 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) + assert 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) + assert 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) + assert username_ps == username_psutil def test_username_no_resolution(self): # Emulate a case where the system can't resolve the uid to @@ -175,7 +177,7 @@ def test_username_no_resolution(self): # 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 p.username() == str(p.uids().real) assert fun.called @skip_on_access_denied() @@ -186,7 +188,7 @@ def test_rss_memory(self): 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) + assert rss_ps == rss_psutil @skip_on_access_denied() @retry_on_failure() @@ -196,7 +198,7 @@ def test_vsz_memory(self): 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) + assert vsz_ps == vsz_psutil def test_name(self): name_ps = ps_name(self.pid) @@ -210,7 +212,7 @@ def test_name(self): # ...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) + assert name_ps == name_psutil def test_name_long(self): # On UNIX the kernel truncates the name to the first 15 @@ -223,7 +225,7 @@ def test_name_long(self): "psutil._psplatform.Process.cmdline", return_value=cmdline ): p = psutil.Process() - self.assertEqual(p.name(), "long-program-name-extended") + assert p.name() == "long-program-name-extended" def test_name_long_cmdline_ad_exc(self): # Same as above but emulates a case where cmdline() raises @@ -236,7 +238,7 @@ def test_name_long_cmdline_ad_exc(self): side_effect=psutil.AccessDenied(0, ""), ): p = psutil.Process() - self.assertEqual(p.name(), "long-program-name") + assert p.name() == "long-program-name" def test_name_long_cmdline_nsp_exc(self): # Same as above but emulates a case where cmdline() raises NSP @@ -248,7 +250,8 @@ def test_name_long_cmdline_nsp_exc(self): side_effect=psutil.NoSuchProcess(0, ""), ): p = psutil.Process() - self.assertRaises(psutil.NoSuchProcess, p.name) + with pytest.raises(psutil.NoSuchProcess): + p.name() @unittest.skipIf(MACOS or BSD, 'ps -o start not available') def test_create_time(self): @@ -263,13 +266,13 @@ def test_create_time(self): 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]) + assert time_ps in [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) + assert ps_pathname == psutil_pathname except AssertionError: # certain platforms such as BSD are more accurate returning: # "/usr/local/bin/python2.7" @@ -278,7 +281,7 @@ def test_exe(self): # 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) + assert 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 @@ -290,9 +293,9 @@ def test_cmdline(self): ps_cmdline = ps_args(self.pid) psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline()) if AARCH64 and len(ps_cmdline) < len(psutil_cmdline): - self.assertTrue(psutil_cmdline.startswith(ps_cmdline)) + assert psutil_cmdline.startswith(ps_cmdline) else: - self.assertEqual(ps_cmdline, psutil_cmdline) + assert 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 @@ -304,7 +307,7 @@ def test_cmdline(self): def test_nice(self): ps_nice = ps('nice', self.pid) psutil_nice = psutil.Process().nice() - self.assertEqual(ps_nice, psutil_nice) + assert ps_nice == psutil_nice @unittest.skipIf(not POSIX, "POSIX only") @@ -355,11 +358,11 @@ def test_users(self): 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())) + assert 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]) + assert u.name == users[idx] + assert u.terminal == terminals[idx] if u.pid is not None: # None on OpenBSD psutil.Process(u.pid) @@ -400,7 +403,7 @@ def test_users_started(self): psutil_value = datetime.datetime.fromtimestamp( u.started ).strftime(tstamp) - self.assertEqual(psutil_value, started[idx]) + assert psutil_value == started[idx] def test_pid_exists_let_raise(self): # According to "man 2 kill" possible error values for kill @@ -409,7 +412,8 @@ def test_pid_exists_let_raise(self): with mock.patch( "psutil._psposix.os.kill", side_effect=OSError(errno.EBADF, "") ) as m: - self.assertRaises(OSError, psutil._psposix.pid_exists, os.getpid()) + with pytest.raises(OSError): + psutil._psposix.pid_exists(os.getpid()) assert m.called def test_os_waitpid_let_raise(self): @@ -418,7 +422,8 @@ def test_os_waitpid_let_raise(self): with mock.patch( "psutil._psposix.os.waitpid", side_effect=OSError(errno.EBADF, "") ) as m: - self.assertRaises(OSError, psutil._psposix.wait_pid, os.getpid()) + with pytest.raises(OSError): + psutil._psposix.wait_pid(os.getpid()) assert m.called def test_os_waitpid_eintr(self): @@ -426,12 +431,8 @@ def test_os_waitpid_eintr(self): 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, - ) + with pytest.raises(psutil._psposix.TimeoutExpired): + psutil._psposix.wait_pid(os.getpid(), timeout=0.01) assert m.called def test_os_waitpid_bad_ret_status(self): @@ -439,9 +440,8 @@ def test_os_waitpid_bad_ret_status(self): with mock.patch( "psutil._psposix.os.waitpid", return_value=(1, -1) ) as m: - self.assertRaises( - ValueError, psutil._psposix.wait_pid, os.getpid() - ) + with pytest.raises(ValueError): + psutil._psposix.wait_pid(os.getpid()) assert m.called # AIX can return '-' in df output instead of numbers, e.g. for /proc @@ -481,16 +481,16 @@ def df(device): 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) + assert abs(usage.total - total) < tolerance + assert abs(usage.used - used) < tolerance + assert abs(usage.free - free) < tolerance + assert abs(usage.percent - percent) < 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) + assert pagesize > 0 + assert pagesize == resource.getpagesize() + assert pagesize == mmap.PAGESIZE diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 33419de1bf..49a94b7a76 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -21,6 +21,8 @@ import types import unittest +import pytest + import psutil from psutil import AIX from psutil import BSD @@ -91,8 +93,8 @@ def spawn_psproc(self, *args, **kwargs): def test_pid(self): p = psutil.Process() - self.assertEqual(p.pid, os.getpid()) - with self.assertRaises(AttributeError): + assert p.pid == os.getpid() + with pytest.raises(AttributeError): p.pid = 33 def test_kill(self): @@ -100,9 +102,9 @@ def test_kill(self): p.kill() code = p.wait() if WINDOWS: - self.assertEqual(code, signal.SIGTERM) + assert code == signal.SIGTERM else: - self.assertEqual(code, -signal.SIGKILL) + assert code == -signal.SIGKILL self.assertProcessGone(p) def test_terminate(self): @@ -110,9 +112,9 @@ def test_terminate(self): p.terminate() code = p.wait() if WINDOWS: - self.assertEqual(code, signal.SIGTERM) + assert code == signal.SIGTERM else: - self.assertEqual(code, -signal.SIGTERM) + assert code == -signal.SIGTERM self.assertProcessGone(p) def test_send_signal(self): @@ -121,9 +123,9 @@ def test_send_signal(self): p.send_signal(sig) code = p.wait() if WINDOWS: - self.assertEqual(code, sig) + assert code == sig else: - self.assertEqual(code, -sig) + assert code == -sig self.assertProcessGone(p) @unittest.skipIf(not POSIX, "not POSIX") @@ -133,13 +135,15 @@ def test_send_signal_mocked(self): with mock.patch( 'psutil.os.kill', side_effect=OSError(errno.ESRCH, "") ): - self.assertRaises(psutil.NoSuchProcess, p.send_signal, sig) + with pytest.raises(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) + with pytest.raises(psutil.AccessDenied): + p.send_signal(sig) def test_wait_exited(self): # Test waitpid() + WIFEXITED -> WEXITSTATUS. @@ -147,25 +151,25 @@ def test_wait_exited(self): cmd = [PYTHON_EXE, "-c", "pass"] p = self.spawn_psproc(cmd) code = p.wait() - self.assertEqual(code, 0) + assert 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) + assert 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) + assert 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) + assert code == 5 self.assertProcessGone(p) @unittest.skipIf(NETBSD, "fails on NETBSD") @@ -175,27 +179,33 @@ def test_wait_stopped(self): # 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) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) p.send_signal(signal.SIGCONT) - self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) p.send_signal(signal.SIGTERM) - self.assertEqual(p.wait(), -signal.SIGTERM) - self.assertEqual(p.wait(), -signal.SIGTERM) + assert p.wait() == -signal.SIGTERM + assert p.wait() == -signal.SIGTERM else: p.suspend() - self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) p.resume() - self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) p.terminate() - self.assertEqual(p.wait(), signal.SIGTERM) - self.assertEqual(p.wait(), signal.SIGTERM) + assert p.wait() == signal.SIGTERM + assert 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) + with pytest.raises(psutil.TimeoutExpired): + child.wait(0.01) + with pytest.raises(psutil.TimeoutExpired): + grandchild.wait(0.01) # We also terminate the direct child otherwise the # grandchild will hang until the parent is gone. child.terminate() @@ -203,24 +213,28 @@ def test_wait_non_children(self): child_ret = child.wait() grandchild_ret = grandchild.wait() if POSIX: - self.assertEqual(child_ret, -signal.SIGTERM) + assert child_ret == -signal.SIGTERM # For processes which are not our children we're supposed # to get None. - self.assertEqual(grandchild_ret, None) + assert grandchild_ret is None else: - self.assertEqual(child_ret, signal.SIGTERM) - self.assertEqual(child_ret, signal.SIGTERM) + assert child_ret == signal.SIGTERM + assert 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) + with pytest.raises(psutil.TimeoutExpired): + p.wait(0.01) + with pytest.raises(psutil.TimeoutExpired): + p.wait(0) + with pytest.raises(ValueError): + p.wait(-1) def test_wait_timeout_nonblocking(self): p = self.spawn_psproc() - self.assertRaises(psutil.TimeoutExpired, p.wait, 0) + with pytest.raises(psutil.TimeoutExpired): + p.wait(0) p.kill() stop_at = time.time() + GLOBAL_TIMEOUT while time.time() < stop_at: @@ -232,9 +246,9 @@ def test_wait_timeout_nonblocking(self): else: raise self.fail('timeout') if POSIX: - self.assertEqual(code, -signal.SIGKILL) + assert code == -signal.SIGKILL else: - self.assertEqual(code, signal.SIGTERM) + assert code == signal.SIGTERM self.assertProcessGone(p) def test_cpu_percent(self): @@ -243,9 +257,9 @@ def test_cpu_percent(self): 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): + assert isinstance(percent, float) + assert percent >= 0.0 + with pytest.raises(ValueError): p.cpu_percent(interval=-1) def test_cpu_percent_numcpus_none(self): @@ -285,10 +299,10 @@ def test_cpu_times_2(self): def test_cpu_num(self): p = psutil.Process() num = p.cpu_num() - self.assertGreaterEqual(num, 0) + assert num >= 0 if psutil.cpu_count() == 1: - self.assertEqual(num, 0) - self.assertIn(p.cpu_num(), range(psutil.cpu_count())) + assert num == 0 + assert p.cpu_num() in range(psutil.cpu_count()) def test_create_time(self): p = self.spawn_psproc() @@ -318,7 +332,7 @@ def test_terminal(self): # Note: happens if pytest is run without the `-s` opt. raise unittest.SkipTest("can't rely on `tty` CLI") else: - self.assertEqual(terminal, tty) + assert terminal == tty @unittest.skipIf(not HAS_PROC_IO_COUNTERS, 'not supported') @skip_on_not_implemented(only_if=LINUX) @@ -330,14 +344,14 @@ def test_io_counters(self): 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) + assert io2.read_count > io1.read_count + assert io2.write_count == io1.write_count if LINUX: - self.assertGreater(io2.read_chars, io1.read_chars) - self.assertEqual(io2.write_chars, io1.write_chars) + assert io2.read_chars > io1.read_chars + assert io2.write_chars == io1.write_chars else: - self.assertGreaterEqual(io2.read_bytes, io1.read_bytes) - self.assertGreaterEqual(io2.write_bytes, io1.write_bytes) + assert io2.read_bytes >= io1.read_bytes + assert io2.write_bytes >= io1.write_bytes # test writes io1 = p.io_counters() @@ -347,21 +361,21 @@ def test_io_counters(self): 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) + assert io2.write_count >= io1.write_count + assert io2.write_bytes >= io1.write_bytes + assert io2.read_count >= io1.read_count + assert io2.read_bytes >= io1.read_bytes if LINUX: - self.assertGreater(io2.write_chars, io1.write_chars) - self.assertGreaterEqual(io2.read_chars, io1.read_chars) + assert io2.write_chars > io1.write_chars + assert 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) + assert io2[i] >= 0 + assert io2[i] >= 0 @unittest.skipIf(not HAS_IONICE, "not supported") @unittest.skipIf(not LINUX, "linux only") @@ -374,37 +388,37 @@ def cleanup(init): 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 + assert p.ionice()[0] == psutil.IOPRIO_CLASS_NONE + assert psutil.IOPRIO_CLASS_NONE == 0 + assert psutil.IOPRIO_CLASS_RT == 1 # high + assert psutil.IOPRIO_CLASS_BE == 2 # normal + assert 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 + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_IDLE, 0) + with pytest.raises(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)) + assert 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): + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_BE, 7) + with pytest.raises(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"): + with pytest.raises(ValueError, match="ioclass accepts no value"): p.ionice(psutil.IOPRIO_CLASS_NONE, 1) - with self.assertRaisesRegex(ValueError, "ioclass accepts no value"): + with pytest.raises(ValueError, match="ioclass accepts no value"): p.ionice(psutil.IOPRIO_CLASS_IDLE, 1) - with self.assertRaisesRegex( - ValueError, "'ioclass' argument must be specified" + with pytest.raises( + ValueError, match="'ioclass' argument must be specified" ): p.ionice(value=1) @@ -413,27 +427,27 @@ def cleanup(init): def test_ionice_win(self): p = psutil.Process() if not CI_TESTING: - self.assertEqual(p.ionice(), psutil.IOPRIO_NORMAL) + assert 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) + assert p.ionice() == psutil.IOPRIO_VERYLOW p.ionice(psutil.IOPRIO_LOW) - self.assertEqual(p.ionice(), psutil.IOPRIO_LOW) + assert p.ionice() == psutil.IOPRIO_LOW try: p.ionice(psutil.IOPRIO_HIGH) except psutil.AccessDenied: pass else: - self.assertEqual(p.ionice(), psutil.IOPRIO_HIGH) + assert p.ionice() == psutil.IOPRIO_HIGH # errs - with self.assertRaisesRegex( - TypeError, "value argument not accepted on Windows" + with pytest.raises( + TypeError, match="value argument not accepted on Windows" ): p.ionice(psutil.IOPRIO_NORMAL, value=1) - with self.assertRaisesRegex(ValueError, "is not a valid priority"): + with pytest.raises(ValueError, match="is not a valid priority"): p.ionice(psutil.IOPRIO_HIGH + 1) @unittest.skipIf(not HAS_RLIMIT, "not supported") @@ -445,32 +459,32 @@ def test_rlimit_get(self): assert names, names for name in names: value = getattr(psutil, name) - self.assertGreaterEqual(value, 0) + assert value >= 0 if name in dir(resource): - self.assertEqual(value, getattr(resource, name)) + assert 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)) + assert 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) + assert len(ret) == 2 + assert ret[0] >= -1 + assert 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)) + assert 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"): + with pytest.raises(ValueError, match="can't use prlimit"): psutil._psplatform.Process(0).rlimit(0) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) @unittest.skipIf(not HAS_RLIMIT, "not supported") @@ -484,15 +498,13 @@ def test_rlimit(self): f.write(b"X" * 1024) # write() or flush() doesn't always cause the exception # but close() will. - with self.assertRaises(IOError) as exc: + with pytest.raises(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 - ) + assert (exc.value.errno if PY3 else exc.value[0]) == errno.EFBIG finally: p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) + assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit_infinity(self): @@ -507,7 +519,7 @@ def test_rlimit_infinity(self): f.write(b"X" * 2048) finally: p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) + assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit_infinity_value(self): @@ -518,7 +530,7 @@ def test_rlimit_infinity_value(self): # conversion doesn't raise an error. p = psutil.Process() soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) - self.assertEqual(psutil.RLIM_INFINITY, hard) + assert hard == psutil.RLIM_INFINITY p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) def test_num_threads(self): @@ -536,13 +548,13 @@ def test_num_threads(self): with ThreadTask(): step2 = p.num_threads() - self.assertEqual(step2, step1 + 1) + assert 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) + assert p.num_handles() > 0 @unittest.skipIf(not HAS_THREADS, 'not supported') def test_threads(self): @@ -557,12 +569,12 @@ def test_threads(self): with ThreadTask(): step2 = p.threads() - self.assertEqual(len(step2), len(step1) + 1) + assert 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]) + assert athread.id == athread[0] + assert athread.user_time == athread[1] + assert athread.system_time == athread[2] @retry_on_failure() @skip_on_access_denied(only_if=MACOS) @@ -574,15 +586,16 @@ def test_threads_2(self): 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, + assert ( + abs(p.cpu_times().user - sum([x.user_time for x in p.threads()])) + < 0.1 ) - self.assertAlmostEqual( - p.cpu_times().system, - sum([x.system_time for x in p.threads()]), - delta=0.1, + assert ( + abs( + p.cpu_times().system + - sum([x.system_time for x in p.threads()]) + ) + < 0.1 ) @retry_on_failure() @@ -592,8 +605,8 @@ def test_memory_info(self): # 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) + assert rss1 > 0 + assert vms1 > 0 # step 2 - allocate some memory memarr = [None] * 1500000 @@ -602,19 +615,19 @@ def test_memory_info(self): 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) + assert rss2 > rss1 + assert vms2 >= vms1 # vms might be equal + assert percent2 > percent1 del memarr if WINDOWS: mem = p.memory_info() - self.assertEqual(mem.rss, mem.wset) - self.assertEqual(mem.vms, mem.pagefile) + assert mem.rss == mem.wset + assert mem.vms == mem.pagefile mem = p.memory_info() for name in mem._fields: - self.assertGreaterEqual(getattr(mem, name), 0) + assert getattr(mem, name) >= 0 def test_memory_full_info(self): p = psutil.Process() @@ -622,21 +635,21 @@ def test_memory_full_info(self): mem = p.memory_full_info() for name in mem._fields: value = getattr(mem, name) - self.assertGreaterEqual(value, 0, msg=(name, value)) + assert value >= 0 if name == 'vms' and OSX or LINUX: continue - self.assertLessEqual(value, total, msg=(name, value, total)) + assert value <= total if LINUX or WINDOWS or MACOS: - self.assertGreaterEqual(mem.uss, 0) + assert mem.uss >= 0 if LINUX: - self.assertGreaterEqual(mem.pss, 0) - self.assertGreaterEqual(mem.swap, 0) + assert mem.pss >= 0 + assert 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))) + assert len(maps) == len(set(maps)) ext_maps = p.memory_maps(grouped=False) for nt in maps: @@ -677,7 +690,7 @@ def test_memory_maps(self): if fname in ('addr', 'perms'): assert value, value else: - self.assertIsInstance(value, (int, long)) + assert isinstance(value, (int, long)) assert value >= 0, value @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") @@ -690,12 +703,13 @@ 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) + assert normpath(path) in libpaths def test_memory_percent(self): p = psutil.Process() p.memory_percent() - self.assertRaises(ValueError, p.memory_percent, memtype="?!?") + with pytest.raises(ValueError): + p.memory_percent(memtype="?!?") if LINUX or MACOS or WINDOWS: p.memory_percent(memtype='uss') @@ -713,12 +727,12 @@ def test_exe(self): p = self.spawn_psproc() exe = p.exe() try: - self.assertEqual(exe, PYTHON_EXE) + assert 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)) + assert normcase(exe) == normcase(PYTHON_EXE) else: # certain platforms such as BSD are more accurate returning: # "/usr/local/bin/python2.7" @@ -728,15 +742,13 @@ def test_exe(self): # an error. ver = "%s.%s" % (sys.version_info[0], sys.version_info[1]) try: - self.assertEqual( - exe.replace(ver, ''), PYTHON_EXE.replace(ver, '') - ) + assert 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') + assert out == 'hey' def test_cmdline(self): cmdline = [ @@ -756,19 +768,17 @@ def test_cmdline(self): # 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) + assert 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:]) - ) + assert ' '.join(p.cmdline()[1:]) == ' '.join(cmdline[1:]) return if QEMU_USER: - self.assertEqual(' '.join(p.cmdline()[2:]), ' '.join(cmdline)) + assert ' '.join(p.cmdline()[2:]) == ' '.join(cmdline) return - self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) + assert ' '.join(p.cmdline()) == ' '.join(cmdline) @unittest.skipIf(PYPY, "broken on PYPY") def test_long_cmdline(self): @@ -782,17 +792,17 @@ def test_long_cmdline(self): # XXX: for some reason the test process may turn into a # zombie (don't know why). try: - self.assertEqual(p.cmdline(), cmdline) + assert p.cmdline() == cmdline except psutil.ZombieProcess: raise unittest.SkipTest("OPENBSD: process turned into zombie") elif QEMU_USER: - self.assertEqual(p.cmdline()[2:], cmdline) + assert p.cmdline()[2:] == cmdline else: ret = p.cmdline() if NETBSD and ret == []: # https://github.com/giampaolo/psutil/issues/2250 raise unittest.SkipTest("OPENBSD: returned EBUSY") - self.assertEqual(ret, cmdline) + assert ret == cmdline def test_name(self): p = self.spawn_psproc() @@ -819,14 +829,14 @@ def test_long_name(self): # just compare the first 15 chars. Full explanation: # https://github.com/giampaolo/psutil/issues/2239 try: - self.assertEqual(p.name(), os.path.basename(pyexe)) + assert 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)) + assert p.name() == os.path.basename(pyexe) # XXX @unittest.skipIf(SUNOS, "broken on SUNOS") @@ -844,37 +854,37 @@ def test_prog_w_funky_name(self): "import time; [time.sleep(0.1) for x in range(100)]", ] 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)) + assert p.cmdline() == cmdline + assert p.name() == os.path.basename(pyexe) + assert 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()) + assert real == os.getuid() # os.geteuid() refers to "effective" uid - self.assertEqual(effective, os.geteuid()) + assert 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()) + assert 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()) + assert real == os.getgid() # os.geteuid() refers to "effective" uid - self.assertEqual(effective, os.getegid()) + assert 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()) + assert os.getresgid() == p.gids() def test_nice(self): def cleanup(init): @@ -884,7 +894,8 @@ def cleanup(init): pass p = psutil.Process() - self.assertRaises(TypeError, p.nice, "str") + with pytest.raises(TypeError): + p.nice("str") init = p.nice() self.addCleanup(cleanup, init) @@ -916,33 +927,35 @@ def cleanup(init): ): if new_prio == prio or highest_prio is None: highest_prio = prio - self.assertEqual(new_prio, highest_prio) + assert new_prio == highest_prio else: - self.assertEqual(new_prio, prio) + assert new_prio == prio else: try: if hasattr(os, "getpriority"): - self.assertEqual( - os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice() + assert ( + os.getpriority(os.PRIO_PROCESS, os.getpid()) + == p.nice() ) p.nice(1) - self.assertEqual(p.nice(), 1) + assert p.nice() == 1 if hasattr(os, "getpriority"): - self.assertEqual( - os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice() + assert ( + 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) + assert p.nice() == 0 except psutil.AccessDenied: pass @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_status(self): p = psutil.Process() - self.assertEqual(p.status(), psutil.STATUS_RUNNING) + assert p.status() == psutil.STATUS_RUNNING def test_username(self): p = self.spawn_psproc() @@ -955,15 +968,15 @@ def test_username(self): # 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) + assert username == getpass_user if 'USERDOMAIN' in os.environ: - self.assertEqual(domain, os.environ['USERDOMAIN']) + assert domain == os.environ['USERDOMAIN'] else: - self.assertEqual(username, getpass.getuser()) + assert username == getpass.getuser() def test_cwd(self): p = self.spawn_psproc() - self.assertEqual(p.cwd(), os.getcwd()) + assert p.cwd() == os.getcwd() def test_cwd_2(self): cmd = [ @@ -985,35 +998,32 @@ def test_cpu_affinity(self): 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))) + assert initial == list(os.sched_getaffinity(p.pid)) + assert 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]) + assert p.cpu_affinity() == [n] if hasattr(os, "sched_getaffinity"): - self.assertEqual( - p.cpu_affinity(), list(os.sched_getaffinity(p.pid)) - ) + assert 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()) + assert 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()) + assert p.cpu_affinity() == p._proc._get_eligible_cpus() else: - self.assertEqual(p.cpu_affinity(), all_cpus) + assert p.cpu_affinity() == all_cpus if hasattr(os, "sched_getaffinity"): - self.assertEqual( - p.cpu_affinity(), list(os.sched_getaffinity(p.pid)) - ) + assert p.cpu_affinity() == list(os.sched_getaffinity(p.pid)) - self.assertRaises(TypeError, p.cpu_affinity, 1) + with pytest.raises(TypeError): + p.cpu_affinity(1) p.cpu_affinity(initial) # it should work with all iterables, not only lists p.cpu_affinity(set(all_cpus)) @@ -1023,10 +1033,14 @@ def test_cpu_affinity(self): 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]) + with pytest.raises(ValueError): + p.cpu_affinity(invalid_cpu) + with pytest.raises(ValueError): + p.cpu_affinity(range(10000, 11000)) + with pytest.raises(TypeError): + p.cpu_affinity([0, "1"]) + with pytest.raises(ValueError): + p.cpu_affinity([0, -1]) @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') def test_cpu_affinity_all_combinations(self): @@ -1046,7 +1060,7 @@ def test_cpu_affinity_all_combinations(self): for combo in combos: p.cpu_affinity(combo) - self.assertEqual(sorted(p.cpu_affinity()), sorted(combo)) + assert sorted(p.cpu_affinity()) == sorted(combo) # TODO: #595 @unittest.skipIf(BSD, "broken on BSD") @@ -1056,18 +1070,18 @@ def test_open_files(self): p = psutil.Process() testfn = self.get_testfn() files = p.open_files() - self.assertNotIn(testfn, files) + assert testfn not 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)) filenames = [os.path.normcase(x.path) for x in files] - self.assertIn(os.path.normcase(testfn), filenames) + assert os.path.normcase(testfn) in filenames if LINUX: for file in files: if file.path == testfn: - self.assertEqual(file.position, 1024) + assert file.position == 1024 for file in files: assert os.path.isfile(file.path), file @@ -1084,7 +1098,7 @@ def test_open_files(self): break time.sleep(0.01) else: - self.assertIn(os.path.normcase(testfn), filenames) + assert os.path.normcase(testfn) in filenames for file in filenames: assert os.path.isfile(file), file @@ -1108,17 +1122,17 @@ def test_open_files_2(self): raise self.fail( "no file found; files=%s" % (repr(p.open_files())) ) - self.assertEqual(normcase(file.path), normcase(fileobj.name)) + assert normcase(file.path) == normcase(fileobj.name) if WINDOWS: - self.assertEqual(file.fd, -1) + assert file.fd == -1 else: - self.assertEqual(file.fd, fileobj.fileno()) + assert file.fd == fileobj.fileno() # test positions ntuple = p.open_files()[0] - self.assertEqual(ntuple[0], ntuple.path) - self.assertEqual(ntuple[1], ntuple.fd) + assert ntuple[0] == ntuple.path + assert ntuple[1] == ntuple.fd # test file is gone - self.assertNotIn(fileobj.name, p.open_files()) + assert fileobj.name not in p.open_files() @unittest.skipIf(not POSIX, 'POSIX only') def test_num_fds(self): @@ -1127,13 +1141,13 @@ def test_num_fds(self): start = p.num_fds() file = open(testfn, 'w') self.addCleanup(file.close) - self.assertEqual(p.num_fds(), start + 1) + assert p.num_fds() == start + 1 sock = socket.socket() self.addCleanup(sock.close) - self.assertEqual(p.num_fds(), start + 2) + assert p.num_fds() == start + 2 file.close() sock.close() - self.assertEqual(p.num_fds(), start) + assert p.num_fds() == start @skip_on_not_implemented(only_if=LINUX) @unittest.skipIf(OPENBSD or NETBSD, "not reliable on OPENBSD & NETBSD") @@ -1150,22 +1164,22 @@ def test_num_ctx_switches(self): def test_ppid(self): p = psutil.Process() if hasattr(os, 'getppid'): - self.assertEqual(p.ppid(), os.getppid()) + assert p.ppid() == os.getppid() p = self.spawn_psproc() - self.assertEqual(p.ppid(), os.getpid()) + assert p.ppid() == os.getpid() def test_parent(self): p = self.spawn_psproc() - self.assertEqual(p.parent().pid, os.getpid()) + assert p.parent().pid == os.getpid() lowest_pid = psutil.pids()[0] - self.assertIsNone(psutil.Process(lowest_pid).parent()) + assert psutil.Process(lowest_pid).parent() is None def test_parent_multi(self): parent = psutil.Process() child, grandchild = self.spawn_children_pair() - self.assertEqual(grandchild.parent(), child) - self.assertEqual(child.parent(), parent) + assert grandchild.parent() == child + assert child.parent() == parent @unittest.skipIf(QEMU_USER, "QEMU user not supported") @retry_on_failure() @@ -1173,14 +1187,14 @@ 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) + assert child.parents()[0] == parent + assert grandchild.parents()[0] == child + assert grandchild.parents()[1] == parent def test_children(self): parent = psutil.Process() - self.assertEqual(parent.children(), []) - self.assertEqual(parent.children(recursive=True), []) + assert parent.children() == [] + assert 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. @@ -1188,22 +1202,22 @@ def test_children(self): 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) + assert len(children) == 1 + assert children[0].pid == child.pid + assert 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]) + assert parent.children() == [child] + assert 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), []) + assert parent.children(recursive=True) == [] def test_children_duplicates(self): # find the process which has the highest number of children @@ -1223,20 +1237,20 @@ def test_children_duplicates(self): except psutil.AccessDenied: # windows pass else: - self.assertEqual(len(c), len(set(c))) + assert 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) + assert len(children) == 2 + assert children[0] == child + assert children[1] == grandchild # backward parents = grandchild.parents() - self.assertEqual(parents[0], child) - self.assertEqual(parents[1], parent) + assert parents[0] == child + assert parents[1] == parent def test_suspend_resume(self): p = self.spawn_psproc() @@ -1246,29 +1260,29 @@ def test_suspend_resume(self): break time.sleep(0.01) p.resume() - self.assertNotEqual(p.status(), psutil.STATUS_STOPPED) + assert p.status() != psutil.STATUS_STOPPED def test_invalid_pid(self): - self.assertRaises(TypeError, psutil.Process, "1") - self.assertRaises(ValueError, psutil.Process, -1) + with pytest.raises(TypeError): + psutil.Process("1") + with pytest.raises(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']) + assert sorted(d.keys()) == ['exe', 'name'] p = psutil.Process(min(psutil.pids())) d = p.as_dict(attrs=['net_connections'], ad_value='foo') if not isinstance(d['net_connections'], list): - self.assertEqual(d['net_connections'], 'foo') + assert d['net_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} - ) + assert p.as_dict(attrs=["nice"], ad_value=1) == {"nice": 1} # Test that NoSuchProcess bubbles up. with mock.patch( @@ -1276,7 +1290,8 @@ def test_as_dict(self): create=True, side_effect=psutil.NoSuchProcess(p.pid, "name"), ): - self.assertRaises(psutil.NoSuchProcess, p.as_dict, attrs=["nice"]) + with pytest.raises(psutil.NoSuchProcess): + p.as_dict(attrs=["nice"]) # Test that ZombieProcess is swallowed. with mock.patch( @@ -1284,9 +1299,7 @@ def test_as_dict(self): create=True, side_effect=psutil.ZombieProcess(p.pid, "name"), ): - self.assertEqual( - p.as_dict(attrs=["nice"], ad_value="foo"), {"nice": "foo"} - ) + assert p.as_dict(attrs=["nice"], ad_value="foo") == {"nice": "foo"} # By default APIs raising NotImplementedError are # supposed to be skipped. @@ -1294,17 +1307,17 @@ def test_as_dict(self): 'psutil.Process.nice', create=True, side_effect=NotImplementedError ): d = p.as_dict() - self.assertNotIn('nice', list(d.keys())) + assert 'nice' not in list(d.keys()) # ...unless the user explicitly asked for some attr. - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): p.as_dict(attrs=["nice"]) # errors - with self.assertRaises(TypeError): + with pytest.raises(TypeError): p.as_dict('name') - with self.assertRaises(ValueError): + with pytest.raises(ValueError): p.as_dict(['foo']) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): p.as_dict(['foo', 'bar']) def test_oneshot(self): @@ -1313,12 +1326,12 @@ def test_oneshot(self): with p.oneshot(): p.cpu_times() p.cpu_times() - self.assertEqual(m.call_count, 1) + assert 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) + assert m.call_count == 2 def test_oneshot_twice(self): # Test the case where the ctx manager is __enter__ed twice. @@ -1332,13 +1345,13 @@ def test_oneshot_twice(self): with p.oneshot(): p.cpu_times() p.cpu_times() - self.assertEqual(m1.call_count, 1) - self.assertEqual(m2.call_count, 1) + assert m1.call_count == 1 + assert 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) + assert m.call_count == 2 def test_oneshot_cache(self): # Make sure oneshot() cache is nonglobal. Instead it's @@ -1347,13 +1360,13 @@ def test_oneshot_cache(self): p1, p2 = self.spawn_children_pair() p1_ppid = p1.ppid() p2_ppid = p2.ppid() - self.assertNotEqual(p1_ppid, p2_ppid) + assert p1_ppid != p2_ppid with p1.oneshot(): - self.assertEqual(p1.ppid(), p1_ppid) - self.assertEqual(p2.ppid(), p2_ppid) + assert p1.ppid() == p1_ppid + assert p2.ppid() == p2_ppid with p2.oneshot(): - self.assertEqual(p1.ppid(), p1_ppid) - self.assertEqual(p2.ppid(), p2_ppid) + assert p1.ppid() == p1_ppid + assert p2.ppid() == p2_ppid def test_halfway_terminated_process(self): # Test that NoSuchProcess exception gets raised in case the @@ -1418,7 +1431,7 @@ def test_zombie_process_status_w_exc(self): "psutil._psplatform.Process.status", side_effect=psutil.ZombieProcess(0), ) as m: - self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) + assert p.status() == psutil.STATUS_ZOMBIE assert m.called def test_reused_pid(self): @@ -1433,52 +1446,64 @@ def test_reused_pid(self): p._ident = (p.pid, p.create_time() + 100) list(psutil.process_iter()) - self.assertIn(p.pid, psutil._pmap) + assert p.pid in psutil._pmap assert not p.is_running() # make sure is_running() removed PID from process_iter() # internal cache with redirect_stderr(StringIO()) as f: list(psutil.process_iter()) - self.assertIn( - "refreshing Process instance for reused PID %s" % p.pid, - f.getvalue(), + assert ( + "refreshing Process instance for reused PID %s" % p.pid + in f.getvalue() ) - self.assertNotIn(p.pid, psutil._pmap) + assert p.pid not in psutil._pmap 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) + with pytest.raises(psutil.NoSuchProcess, match=msg): + fun() - self.assertIn("terminated + PID reused", str(p)) - self.assertIn("terminated + PID reused", repr(p)) + assert "terminated + PID reused" in str(p) + assert "terminated + PID reused" in repr(p) - 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) + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.ppid() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.parent() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.parents() + with pytest.raises(psutil.NoSuchProcess, match=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) + with pytest.raises(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) + assert 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) + with pytest.raises(exc): + p.wait() + with pytest.raises(exc): + p.terminate() + with pytest.raises(exc): + p.suspend() + with pytest.raises(exc): + p.resume() + with pytest.raises(exc): + p.kill() + with pytest.raises(exc): + p.send_signal(signal.SIGTERM) # test all methods ns = process_namespace(p) @@ -1489,15 +1514,15 @@ def test_pid_0(self): pass else: if name in ("uids", "gids"): - self.assertEqual(ret.real, 0) + assert ret.real == 0 elif name == "username": user = 'NT AUTHORITY\\SYSTEM' if WINDOWS else 'root' - self.assertEqual(p.username(), user) + assert p.username() == user elif name == "name": assert name, name if not OPENBSD: - self.assertIn(0, psutil.pids()) + assert 0 in psutil.pids() assert psutil.pid_exists(0) @unittest.skipIf(not HAS_ENVIRON, "not supported") @@ -1526,7 +1551,7 @@ def clean_dict(d): d1 = clean_dict(p.environ()) d2 = clean_dict(os.environ.copy()) if not OSX and GITHUB_ACTIONS: - self.assertEqual(d1, d2) + assert d1 == d2 @unittest.skipIf(not HAS_ENVIRON, "not supported") @unittest.skipIf(not POSIX, "POSIX only") @@ -1560,7 +1585,7 @@ def test_weird_environ(self): wait_for_pid(p.pid) assert p.is_running() # Wait for process to exec or exit. - self.assertEqual(sproc.stderr.read(), b"") + assert sproc.stderr.read() == b"" if MACOS and CI_TESTING: try: env = p.environ() @@ -1570,9 +1595,9 @@ def test_weird_environ(self): return else: env = p.environ() - self.assertEqual(env, {"A": "1", "C": "3"}) + assert env == {"A": "1", "C": "3"} sproc.communicate() - self.assertEqual(sproc.returncode, 0) + assert sproc.returncode == 0 # =================================================================== @@ -1661,13 +1686,14 @@ def test_misc(self): proc.name() proc.cpu_times() proc.stdin # noqa - self.assertTrue(dir(proc)) - self.assertRaises(AttributeError, getattr, proc, 'foo') + assert dir(proc) + with pytest.raises(AttributeError): + proc.foo # noqa proc.terminate() if POSIX: - self.assertEqual(proc.wait(5), -signal.SIGTERM) + assert proc.wait(5) == -signal.SIGTERM else: - self.assertEqual(proc.wait(5), signal.SIGTERM) + assert proc.wait(5) == signal.SIGTERM def test_ctx_manager(self): with psutil.Popen( @@ -1681,7 +1707,7 @@ def test_ctx_manager(self): assert proc.stdout.closed assert proc.stderr.closed assert proc.stdin.closed - self.assertEqual(proc.returncode, 0) + assert proc.returncode == 0 def test_kill_terminate(self): # subprocess.Popen()'s terminate(), kill() and send_signal() do @@ -1700,17 +1726,14 @@ def test_kill_terminate(self): ) 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 - ) + with pytest.raises(psutil.NoSuchProcess): + proc.terminate() + with pytest.raises(psutil.NoSuchProcess): + proc.kill() + with pytest.raises(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, - ) + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.CTRL_C_EVENT) + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.CTRL_BREAK_EVENT) diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 1a55b87ac0..a6025390e1 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -16,6 +16,8 @@ import time import traceback +import pytest + import psutil from psutil import AIX from psutil import BSD @@ -155,13 +157,13 @@ def test_all(self): raise self.fail(''.join(failures)) def cmdline(self, ret, info): - self.assertIsInstance(ret, list) + assert isinstance(ret, list) for part in ret: - self.assertIsInstance(part, str) + assert isinstance(part, str) def exe(self, ret, info): - self.assertIsInstance(ret, (str, unicode)) - self.assertEqual(ret.strip(), ret) + assert isinstance(ret, (str, unicode)) + assert ret.strip() == ret if ret: if WINDOWS and not ret.endswith('.exe'): return # May be "Registry", "MemCompression", ... @@ -179,16 +181,16 @@ def exe(self, ret, info): raise def pid(self, ret, info): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) + assert isinstance(ret, int) + assert ret >= 0 def ppid(self, ret, info): - self.assertIsInstance(ret, (int, long)) - self.assertGreaterEqual(ret, 0) + assert isinstance(ret, (int, long)) + assert ret >= 0 proc_info(ret) def name(self, ret, info): - self.assertIsInstance(ret, (str, unicode)) + assert isinstance(ret, (str, unicode)) if WINDOWS and not ret and is_win_secure_system_proc(info['pid']): # https://github.com/giampaolo/psutil/issues/2338 return @@ -197,9 +199,9 @@ def name(self, ret, info): assert ret, repr(ret) def create_time(self, ret, info): - self.assertIsInstance(ret, float) + assert isinstance(ret, float) try: - self.assertGreaterEqual(ret, 0) + assert ret >= 0 except AssertionError: # XXX if OPENBSD and info['status'] == psutil.STATUS_ZOMBIE: @@ -215,45 +217,45 @@ def create_time(self, ret, info): def uids(self, ret, info): assert is_namedtuple(ret) for uid in ret: - self.assertIsInstance(uid, int) - self.assertGreaterEqual(uid, 0) + assert isinstance(uid, int) + assert 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) + assert isinstance(gid, int) if not MACOS and not NETBSD: - self.assertGreaterEqual(gid, 0) + assert gid >= 0 def username(self, ret, info): - self.assertIsInstance(ret, str) - self.assertEqual(ret.strip(), ret) + assert isinstance(ret, str) + assert ret.strip() == ret assert ret.strip() def status(self, ret, info): - self.assertIsInstance(ret, str) + assert isinstance(ret, str) assert ret, ret if QEMU_USER: # status does not work under qemu user return - self.assertNotEqual(ret, '?') # XXX - self.assertIn(ret, VALID_PROC_STATUSES) + assert ret != '?' # XXX + assert ret in VALID_PROC_STATUSES def io_counters(self, ret, info): assert is_namedtuple(ret) for field in ret: - self.assertIsInstance(field, (int, long)) + assert isinstance(field, (int, long)) if field != -1: - self.assertGreaterEqual(field, 0) + assert 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) + assert isinstance(ret.ioclass, int) + assert isinstance(ret.value, int) + assert ret.ioclass >= 0 + assert ret.value >= 0 else: # Windows, Cygwin choices = [ psutil.IOPRIO_VERYLOW, @@ -261,89 +263,89 @@ def ionice(self, ret, info): psutil.IOPRIO_NORMAL, psutil.IOPRIO_HIGH, ] - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) - self.assertIn(ret, choices) + assert isinstance(ret, int) + assert ret >= 0 + assert ret in choices def num_threads(self, ret, info): - self.assertIsInstance(ret, int) + assert isinstance(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) + assert ret >= 1 def threads(self, ret, info): - self.assertIsInstance(ret, list) + assert isinstance(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) + assert t.id >= 0 + assert t.user_time >= 0 + assert t.system_time >= 0 for field in t: - self.assertIsInstance(field, (int, float)) + assert isinstance(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) + assert isinstance(n, float) + assert n >= 0 # TODO: check ntuple fields def cpu_percent(self, ret, info): - self.assertIsInstance(ret, float) + assert isinstance(ret, float) assert 0.0 <= ret <= 100.0, ret def cpu_num(self, ret, info): - self.assertIsInstance(ret, int) + assert isinstance(ret, int) if FREEBSD and ret == -1: return - self.assertGreaterEqual(ret, 0) + assert ret >= 0 if psutil.cpu_count() == 1: - self.assertEqual(ret, 0) - self.assertIn(ret, list(range(psutil.cpu_count()))) + assert ret == 0 + assert ret in 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) + assert isinstance(value, (int, long)) + assert 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) + assert ret.peak_wset >= ret.wset + assert ret.peak_paged_pool >= ret.paged_pool + assert ret.peak_nonpaged_pool >= ret.nonpaged_pool + assert 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)) + assert isinstance(value, (int, long)) + assert value >= 0 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)) + assert value <= total, name if LINUX: - self.assertGreaterEqual(ret.pss, ret.uss) + assert ret.pss >= ret.uss def open_files(self, ret, info): - self.assertIsInstance(ret, list) + assert isinstance(ret, list) for f in ret: - self.assertIsInstance(f.fd, int) - self.assertIsInstance(f.path, str) - self.assertEqual(f.path.strip(), f.path) + assert isinstance(f.fd, int) + assert isinstance(f.path, str) + assert f.path.strip() == f.path if WINDOWS: - self.assertEqual(f.fd, -1) + assert 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) + assert isinstance(f.position, int) + assert isinstance(f.mode, str) + assert isinstance(f.flags, int) + assert f.position >= 0 + assert f.mode in ('r', 'w', 'a', 'r+', 'a+') + assert f.flags > 0 elif BSD and not f.path: # XXX see: https://github.com/giampaolo/psutil/issues/595 continue @@ -356,19 +358,19 @@ def open_files(self, ret, info): assert stat.S_ISREG(st.st_mode), f def num_fds(self, ret, info): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) + assert isinstance(ret, int) + assert ret >= 0 def net_connections(self, ret, info): with create_sockets(): - self.assertEqual(len(ret), len(set(ret))) + assert 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) + assert isinstance(ret, (str, unicode)) + assert ret.strip() == ret if ret: assert os.path.isabs(ret), ret try: @@ -383,31 +385,31 @@ def cwd(self, ret, info): assert stat.S_ISDIR(st.st_mode) def memory_percent(self, ret, info): - self.assertIsInstance(ret, float) + assert isinstance(ret, float) assert 0 <= ret <= 100, ret def is_running(self, ret, info): - self.assertIsInstance(ret, bool) + assert isinstance(ret, bool) def cpu_affinity(self, ret, info): - self.assertIsInstance(ret, list) - self.assertNotEqual(ret, []) + assert isinstance(ret, list) + assert ret != [] cpus = list(range(psutil.cpu_count())) for n in ret: - self.assertIsInstance(n, int) - self.assertIn(n, cpus) + assert isinstance(n, int) + assert n in cpus def terminal(self, ret, info): - self.assertIsInstance(ret, (str, type(None))) + assert isinstance(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) + assert isinstance(nt.addr, str) + assert isinstance(nt.perms, str) + assert isinstance(nt.path, str) for fname in nt._fields: value = getattr(nt, fname) if fname == 'path': @@ -422,15 +424,15 @@ def memory_maps(self, ret, info): if not WINDOWS: assert value, repr(value) else: - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0) + assert isinstance(value, (int, long)) + assert value >= 0 def num_handles(self, ret, info): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) + assert isinstance(ret, int) + assert ret >= 0 def nice(self, ret, info): - self.assertIsInstance(ret, int) + assert isinstance(ret, int) if POSIX: assert -20 <= ret <= 20, ret else: @@ -439,29 +441,29 @@ def nice(self, ret, info): for x in dir(psutil) if x.endswith('_PRIORITY_CLASS') ] - self.assertIn(ret, priorities) + assert ret in priorities if PY3: - self.assertIsInstance(ret, enum.IntEnum) + assert isinstance(ret, enum.IntEnum) else: - self.assertIsInstance(ret, int) + assert isinstance(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) + assert isinstance(value, (int, long)) + assert 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) + assert isinstance(ret, tuple) + assert len(ret) == 2 + assert ret[0] >= -1 + assert ret[1] >= -1 def environ(self, ret, info): - self.assertIsInstance(ret, dict) + assert isinstance(ret, dict) for k, v in ret.items(): - self.assertIsInstance(k, str) - self.assertIsInstance(v, str) + assert isinstance(k, str) + assert isinstance(v, str) class TestPidsRange(PsutilTestCase): @@ -515,16 +517,16 @@ def check(pid): if exists: psutil.Process(pid) if not WINDOWS: # see docstring - self.assertIn(pid, psutil.pids()) + assert pid in psutil.pids() else: # On OpenBSD thread IDs can be instantiated, # and oneshot() succeeds, but other APIs fail # with EINVAL. if not OPENBSD: - with self.assertRaises(psutil.NoSuchProcess): + with pytest.raises(psutil.NoSuchProcess): psutil.Process(pid) if not WINDOWS: # see docstring - self.assertNotIn(pid, psutil.pids()) + assert pid not in psutil.pids() except (psutil.Error, AssertionError): x -= 1 if x == 0: diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index d7505f8085..18ea18e6e4 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -30,10 +30,10 @@ def test_swap_memory(self): 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) + assert psutil_swap.total == total + assert psutil_swap.used == used + assert psutil_swap.free == free def test_cpu_count(self): out = sh("/usr/sbin/psrinfo") - self.assertEqual(psutil.cpu_count(), len(out.split('\n'))) + assert psutil.cpu_count() == len(out.split('\n')) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index c6854699ab..7403326bff 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -19,6 +19,8 @@ import time import unittest +import pytest + import psutil from psutil import AIX from psutil import BSD @@ -64,19 +66,18 @@ class TestProcessIter(PsutilTestCase): def test_pid_presence(self): - self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) + assert os.getpid() in [x.pid for x in psutil.process_iter()] sproc = self.spawn_testproc() - self.assertIn(sproc.pid, [x.pid for x in psutil.process_iter()]) + assert sproc.pid in [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 sproc.pid not in [x.pid for x in psutil.process_iter()] def test_no_duplicates(self): 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), + assert sorted(ls, key=lambda x: x.pid) == sorted( + set(ls), key=lambda x: x.pid ) def test_emulate_nsp(self): @@ -86,9 +87,7 @@ def test_emulate_nsp(self): 'psutil.Process.as_dict', side_effect=psutil.NoSuchProcess(os.getpid()), ): - self.assertEqual( - list(psutil.process_iter(attrs=["cpu_times"])), [] - ) + assert list(psutil.process_iter(attrs=["cpu_times"])) == [] psutil.process_iter.cache_clear() # repeat test without cache def test_emulate_access_denied(self): @@ -98,25 +97,25 @@ def test_emulate_access_denied(self): 'psutil.Process.as_dict', side_effect=psutil.AccessDenied(os.getpid()), ): - with self.assertRaises(psutil.AccessDenied): + with pytest.raises(psutil.AccessDenied): list(psutil.process_iter(attrs=["cpu_times"])) psutil.process_iter.cache_clear() # repeat test without cache def test_attrs(self): for p in psutil.process_iter(attrs=['pid']): - self.assertEqual(list(p.info.keys()), ['pid']) + assert list(p.info.keys()) == ['pid'] # yield again for p in psutil.process_iter(attrs=['pid']): - self.assertEqual(list(p.info.keys()), ['pid']) - with self.assertRaises(ValueError): + assert list(p.info.keys()) == ['pid'] + with pytest.raises(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 p.info['cpu_times'] is None + assert p.info['pid'] >= 0 assert m.called with mock.patch( "psutil._psplatform.Process.cpu_times", @@ -126,8 +125,8 @@ def test_attrs(self): 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 p.info['cpu_times'] is flag + assert p.info['pid'] >= 0 assert m.called def test_cache_clear(self): @@ -150,53 +149,55 @@ def callback(p): 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) + with pytest.raises(ValueError): + psutil.wait_procs(procs, timeout=-1) + with pytest.raises(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, []) + assert time.time() - t < 0.5 + assert gone == [] + assert len(alive) == 3 + assert pids == [] for p in alive: - self.assertFalse(hasattr(p, 'returncode')) + assert not 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) + assert len(gone) == 1 + assert len(alive) == 2 return gone, alive sproc3.terminate() gone, alive = test_1(procs, callback) - self.assertIn(sproc3.pid, [x.pid for x in gone]) + assert sproc3.pid in [x.pid for x in gone] if POSIX: - self.assertEqual(gone.pop().returncode, -signal.SIGTERM) + assert gone.pop().returncode == -signal.SIGTERM else: - self.assertEqual(gone.pop().returncode, 1) - self.assertEqual(pids, [sproc3.pid]) + assert gone.pop().returncode == 1 + assert pids == [sproc3.pid] for p in alive: - self.assertFalse(hasattr(p, 'returncode')) + assert not 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) + assert len(gone) == 3 + assert 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])) + assert set(pids) == set([sproc1.pid, sproc2.pid, sproc3.pid]) for p in gone: - self.assertTrue(hasattr(p, 'returncode')) + assert hasattr(p, 'returncode') @unittest.skipIf( PYPY and WINDOWS, "spawn_testproc() unreliable on PYPY + WINDOWS" @@ -212,13 +213,13 @@ def test_wait_procs_no_timeout(self): def test_pid_exists(self): sproc = self.spawn_testproc() - self.assertTrue(psutil.pid_exists(sproc.pid)) + assert 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()) + assert not psutil.pid_exists(sproc.pid) + assert not psutil.pid_exists(-1) + assert psutil.pid_exists(0) == (0 in psutil.pids()) def test_pid_exists_2(self): pids = psutil.pids() @@ -229,36 +230,36 @@ def test_pid_exists_2(self): # 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()) + assert pid not in psutil.pids() pids = range(max(pids) + 15000, max(pids) + 16000) for pid in pids: - self.assertFalse(psutil.pid_exists(pid), msg=pid) + assert not psutil.pid_exists(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()) + assert isinstance(bt, float) + assert bt > 0 + assert bt < time.time() @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") def test_users(self): users = psutil.users() - self.assertNotEqual(users, []) + assert 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))) + assert isinstance(user.name, str) + assert isinstance(user.terminal, (str, type(None))) if user.host is not None: - self.assertIsInstance(user.host, (str, type(None))) + assert isinstance(user.host, (str, type(None))) user.terminal # noqa user.host # noqa - self.assertGreater(user.started, 0.0) + assert user.started > 0.0 datetime.datetime.fromtimestamp(user.started) if WINDOWS or OPENBSD: - self.assertIsNone(user.pid) + assert user.pid is None else: psutil.Process(user.pid) @@ -284,7 +285,7 @@ def test_os_constants(self): "SUNOS", ] for name in names: - self.assertIsInstance(getattr(psutil, name), bool, msg=name) + assert isinstance(getattr(psutil, name), bool), name if os.name == 'posix': assert psutil.POSIX @@ -295,12 +296,9 @@ def test_os_constants(self): names.remove("LINUX") elif "bsd" in sys.platform.lower(): assert psutil.BSD - self.assertEqual( - [psutil.FREEBSD, psutil.OPENBSD, psutil.NETBSD].count( - True - ), - 1, - ) + assert [psutil.FREEBSD, psutil.OPENBSD, psutil.NETBSD].count( + True + ) == 1 names.remove("BSD") names.remove("FREEBSD") names.remove("OPENBSD") @@ -321,7 +319,7 @@ def test_os_constants(self): # assert all other constants are set to False for name in names: - self.assertFalse(getattr(psutil, name), msg=name) + assert not getattr(psutil, name), name class TestMemoryAPIs(PsutilTestCase): @@ -335,7 +333,7 @@ def test_virtual_memory(self): for name in mem._fields: value = getattr(mem, name) if name != 'percent': - self.assertIsInstance(value, (int, long)) + assert isinstance(value, (int, long)) if name != 'total': if not value >= 0: raise self.fail("%r < 0 (%s)" % (name, value)) @@ -347,8 +345,13 @@ def test_virtual_memory(self): def test_swap_memory(self): mem = psutil.swap_memory() - self.assertEqual( - mem._fields, ('total', 'used', 'free', 'percent', 'sin', 'sout') + assert mem._fields == ( + 'total', + 'used', + 'free', + 'percent', + 'sin', + 'sout', ) assert mem.total >= 0, mem @@ -366,9 +369,9 @@ def test_swap_memory(self): 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) + assert logical is not None + assert logical == len(psutil.cpu_times(percpu=True)) + assert logical >= 1 if os.path.exists("/proc/cpuinfo"): with open("/proc/cpuinfo") as fd: @@ -382,10 +385,10 @@ def test_cpu_count_cores(self): if cores is None: raise unittest.SkipTest("cpu_count_cores() is None") if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista - self.assertIsNone(cores) + assert cores is None else: - self.assertGreaterEqual(cores, 1) - self.assertGreaterEqual(logical, cores) + assert cores >= 1 + assert logical >= cores def test_cpu_count_none(self): # https://github.com/giampaolo/psutil/issues/1085 @@ -393,12 +396,12 @@ def test_cpu_count_none(self): with mock.patch( 'psutil._psplatform.cpu_count_logical', return_value=val ) as m: - self.assertIsNone(psutil.cpu_count()) + assert psutil.cpu_count() is None assert m.called with mock.patch( 'psutil._psplatform.cpu_count_cores', return_value=val ) as m: - self.assertIsNone(psutil.cpu_count(logical=False)) + assert psutil.cpu_count(logical=False) is None assert m.called def test_cpu_times(self): @@ -407,10 +410,10 @@ def test_cpu_times(self): times = psutil.cpu_times() sum(times) for cp_time in times: - self.assertIsInstance(cp_time, float) - self.assertGreaterEqual(cp_time, 0.0) + assert isinstance(cp_time, float) + assert cp_time >= 0.0 total += cp_time - self.assertAlmostEqual(total, sum(times), places=6) + assert round(abs(total - sum(times)), 6) == 0 str(times) # CPU times are always supposed to increase over time # or at least remain the same and that's because time @@ -446,14 +449,13 @@ def test_per_cpu_times(self): total = 0 sum(times) for cp_time in times: - self.assertIsInstance(cp_time, float) - self.assertGreaterEqual(cp_time, 0.0) + assert isinstance(cp_time, float) + assert cp_time >= 0.0 total += cp_time - self.assertAlmostEqual(total, sum(times), places=6) + assert round(abs(total - sum(times)), 6) == 0 str(times) - self.assertEqual( - len(psutil.cpu_times(percpu=True)[0]), - len(psutil.cpu_times(percpu=False)), + assert len(psutil.cpu_times(percpu=True)[0]) == len( + psutil.cpu_times(percpu=False) ) # Note: in theory CPU times are always supposed to increase over @@ -500,18 +502,17 @@ def test_cpu_times_comparison(self): 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, + assert ( + abs(getattr(base, field) - getattr(summed_values, field)) + < 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()) + assert isinstance(percent, float) + assert percent >= 0.0 + assert percent is not -0.0 + assert percent <= 100.0 * psutil.cpu_count() except AssertionError as err: raise AssertionError( "\n%s\nlast=%s\nnew=%s" @@ -524,18 +525,18 @@ def test_cpu_percent(self): new = psutil.cpu_percent(interval=None) self._test_cpu_percent(new, last, new) last = new - with self.assertRaises(ValueError): + with pytest.raises(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()) + assert 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): + with pytest.raises(ValueError): psutil.cpu_percent(interval=-1, percpu=True) def test_cpu_times_percent(self): @@ -546,12 +547,12 @@ def test_cpu_times_percent(self): self._test_cpu_percent(percent, last, new) self._test_cpu_percent(sum(new), last, new) last = new - with self.assertRaises(ValueError): + with pytest.raises(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()) + assert len(last) == psutil.cpu_count() for _ in range(100): new = psutil.cpu_times_percent(interval=None, percpu=True) for cpu in new: @@ -575,16 +576,18 @@ def test_per_cpu_times_percent_negative(self): 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'), + assert infos._fields == ( + 'ctx_switches', + 'interrupts', + 'soft_interrupts', + 'syscalls', ) for name in infos._fields: value = getattr(infos, name) - self.assertGreaterEqual(value, 0) + assert value >= 0 # on AIX, ctx_switches is always 0 if not AIX and name in ('ctx_switches', 'interrupts'): - self.assertGreater(value, 0) + assert value > 0 # TODO: remove this once 1892 is fixed @unittest.skipIf( @@ -594,13 +597,13 @@ def test_cpu_stats(self): def test_cpu_freq(self): def check_ls(ls): for nt in ls: - self.assertEqual(nt._fields, ('current', 'min', 'max')) + assert nt._fields == ('current', 'min', 'max') if nt.max != 0.0: - self.assertLessEqual(nt.current, nt.max) + assert nt.current <= nt.max for name in nt._fields: value = getattr(nt, name) - self.assertIsInstance(value, (int, long, float)) - self.assertGreaterEqual(value, 0) + assert isinstance(value, (int, long, float)) + assert value >= 0 ls = psutil.cpu_freq(percpu=True) if FREEBSD and not ls: @@ -610,22 +613,22 @@ def check_ls(ls): check_ls([psutil.cpu_freq(percpu=False)]) if LINUX: - self.assertEqual(len(ls), psutil.cpu_count()) + assert len(ls) == psutil.cpu_count() @unittest.skipIf(not HAS_GETLOADAVG, "not supported") def test_getloadavg(self): loadavg = psutil.getloadavg() - self.assertEqual(len(loadavg), 3) + assert len(loadavg) == 3 for load in loadavg: - self.assertIsInstance(load, float) - self.assertGreaterEqual(load, 0.0) + assert isinstance(load, float) + assert 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._fields == ('total', 'used', 'free', 'percent') assert usage.total > 0, usage assert usage.used > 0, usage @@ -637,26 +640,22 @@ def test_disk_usage(self): # 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 - ) + assert usage.total == shutil_usage.total + assert abs(usage.free - shutil_usage.free) < tolerance if not MACOS_12PLUS: # see https://github.com/giampaolo/psutil/issues/2147 - self.assertAlmostEqual( - usage.used, shutil_usage.used, delta=tolerance - ) + assert abs(usage.used - shutil_usage.used) < tolerance # if path does not exist OSError ENOENT is expected across # all platforms fname = self.get_testfn() - with self.assertRaises(FileNotFoundError): + with pytest.raises(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): + with pytest.raises(UnicodeEncodeError): psutil.disk_usage(UNICODE_SUFFIX) def test_disk_usage_bytes(self): @@ -664,14 +663,14 @@ def test_disk_usage_bytes(self): 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) + assert isinstance(nt.device, str) + assert isinstance(nt.mountpoint, str) + assert isinstance(nt.fstype, str) + assert isinstance(nt.opts, str) # all = False ls = psutil.disk_partitions(all=False) - self.assertTrue(ls, msg=ls) + assert ls for disk in ls: check_ntuple(disk) if WINDOWS and 'cdrom' in disk.opts: @@ -688,7 +687,7 @@ def check_ntuple(nt): # all = True ls = psutil.disk_partitions(all=True) - self.assertTrue(ls, msg=ls) + assert ls for disk in psutil.disk_partitions(all=True): check_ntuple(disk) if not WINDOWS and disk.mountpoint: @@ -718,7 +717,7 @@ def find_mount_point(path): for x in psutil.disk_partitions(all=True) if x.mountpoint ] - self.assertIn(mount, mounts) + assert mount in mounts @unittest.skipIf( LINUX and not os.path.exists('/proc/diskstats'), @@ -729,19 +728,19 @@ def find_mount_point(path): ) # 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) + assert nt[0] == nt.read_count + assert nt[1] == nt.write_count + assert nt[2] == nt.read_bytes + assert nt[3] == nt.write_bytes if not (OPENBSD or NETBSD): - self.assertEqual(nt[4], nt.read_time) - self.assertEqual(nt[5], nt.write_time) + assert nt[4] == nt.read_time + assert 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) + assert nt[6] == nt.read_merged_count + assert nt[7] == nt.write_merged_count + assert nt[8] == nt.busy_time elif FREEBSD: - self.assertEqual(nt[6], nt.busy_time) + assert nt[6] == nt.busy_time for name in nt._fields: assert getattr(nt, name) >= 0, nt @@ -750,7 +749,7 @@ def check_ntuple(nt): check_ntuple(ret) ret = psutil.disk_io_counters(perdisk=True) # make sure there are no duplicates - self.assertEqual(len(ret), len(set(ret))) + assert len(ret) == len(set(ret)) for key in ret: assert key, key check_ntuple(ret[key]) @@ -761,8 +760,8 @@ def test_disk_io_counters_no_disks(self): 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 psutil.disk_io_counters(perdisk=False) is None + assert psutil.disk_io_counters(perdisk=True) == {} assert m.called @@ -770,14 +769,14 @@ 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[0] == nt.bytes_sent + assert nt[1] == nt.bytes_recv + assert nt[2] == nt.packets_sent + assert nt[3] == nt.packets_recv + assert nt[4] == nt.errin + assert nt[5] == nt.errout + assert nt[6] == nt.dropin + assert nt[7] == nt.dropout assert nt.bytes_sent >= 0, nt assert nt.bytes_recv >= 0, nt assert nt.packets_sent >= 0, nt @@ -790,10 +789,10 @@ def check_ntuple(nt): ret = psutil.net_io_counters(pernic=False) check_ntuple(ret) ret = psutil.net_io_counters(pernic=True) - self.assertNotEqual(ret, []) + assert ret != [] for key in ret: - self.assertTrue(key) - self.assertIsInstance(key, str) + assert key + assert isinstance(key, str) check_ntuple(ret[key]) @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') @@ -803,8 +802,8 @@ def test_net_io_counters_no_nics(self): 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 psutil.net_io_counters(pernic=False) is None + assert psutil.net_io_counters(pernic=True) == {} assert m.called @unittest.skipIf(QEMU_USER, 'QEMU user not supported') @@ -821,16 +820,16 @@ def test_net_if_addrs(self): 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)) + assert isinstance(nic, str) + assert 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) + assert isinstance(addr.family, int) + assert isinstance(addr.address, str) + assert isinstance(addr.netmask, (str, type(None))) + assert isinstance(addr.broadcast, (str, type(None))) + assert addr.family in families if PY3 and not PYPY: - self.assertIsInstance(addr.family, enum.IntEnum) + assert isinstance(addr.family, enum.IntEnum) if nic_stats[nic].isup: # Do not test binding to addresses of interfaces # that are down @@ -865,17 +864,17 @@ def test_net_if_addrs(self): check_net_address(ip, addr.family) # broadcast and ptp addresses are mutually exclusive if addr.broadcast: - self.assertIsNone(addr.ptp) + assert addr.ptp is None elif addr.ptp: - self.assertIsNone(addr.broadcast) + assert addr.broadcast is None if BSD or MACOS or SUNOS: if hasattr(socket, "AF_LINK"): - self.assertEqual(psutil.AF_LINK, socket.AF_LINK) + assert psutil.AF_LINK == socket.AF_LINK elif LINUX: - self.assertEqual(psutil.AF_LINK, socket.AF_PACKET) + assert psutil.AF_LINK == socket.AF_PACKET elif WINDOWS: - self.assertEqual(psutil.AF_LINK, -1) + assert psutil.AF_LINK == -1 def test_net_if_addrs_mac_null_bytes(self): # Simulate that the underlying C function returns an incomplete @@ -891,9 +890,9 @@ def test_net_if_addrs_mac_null_bytes(self): addr = psutil.net_if_addrs()['em1'][0] assert m.called if POSIX: - self.assertEqual(addr.address, '06:3d:29:00:00:00') + assert addr.address == '06:3d:29:00:00:00' else: - self.assertEqual(addr.address, '06-3d-29-00-00-00') + assert addr.address == '06-3d-29-00-00-00' @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_net_if_stats(self): @@ -905,14 +904,14 @@ def test_net_if_stats(self): psutil.NIC_DUPLEX_UNKNOWN, ) for name, stats in nics.items(): - self.assertIsInstance(name, str) + assert isinstance(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) + assert isinstance(isup, bool) + assert duplex in all_duplexes + assert duplex in all_duplexes + assert speed >= 0 + assert mtu >= 0 + assert isinstance(flags, str) @unittest.skipIf( not (LINUX or BSD or MACOS), "LINUX or BSD or MACOS specific" @@ -924,7 +923,7 @@ def test_net_if_stats_enodev(self): side_effect=OSError(errno.ENODEV, ""), ) as m: ret = psutil.net_if_stats() - self.assertEqual(ret, {}) + assert ret == {} assert m.called @@ -933,15 +932,15 @@ class TestSensorsAPIs(PsutilTestCase): def test_sensors_temperatures(self): temps = psutil.sensors_temperatures() for name, entries in temps.items(): - self.assertIsInstance(name, str) + assert isinstance(name, str) for entry in entries: - self.assertIsInstance(entry.label, str) + assert isinstance(entry.label, str) if entry.current is not None: - self.assertGreaterEqual(entry.current, 0) + assert entry.current >= 0 if entry.high is not None: - self.assertGreaterEqual(entry.high, 0) + assert entry.high >= 0 if entry.critical is not None: - self.assertGreaterEqual(entry.critical, 0) + assert entry.critical >= 0 @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") def test_sensors_temperatures_fahreneit(self): @@ -951,32 +950,32 @@ def test_sensors_temperatures_fahreneit(self): ) 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) + assert temps.current == 122.0 + assert temps.high == 140.0 + assert 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) + assert ret.percent >= 0 + assert ret.percent <= 100 if ret.secsleft not in ( psutil.POWER_TIME_UNKNOWN, psutil.POWER_TIME_UNLIMITED, ): - self.assertGreaterEqual(ret.secsleft, 0) + assert ret.secsleft >= 0 else: if ret.secsleft == psutil.POWER_TIME_UNLIMITED: - self.assertTrue(ret.power_plugged) - self.assertIsInstance(ret.power_plugged, bool) + assert ret.power_plugged + assert isinstance(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) + assert isinstance(name, str) for entry in entries: - self.assertIsInstance(entry.label, str) - self.assertIsInstance(entry.current, (int, long)) - self.assertGreaterEqual(entry.current, 0) + assert isinstance(entry.label, str) + assert isinstance(entry.current, (int, long)) + assert entry.current >= 0 diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index f7ce52dd60..64f172c721 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -74,8 +74,8 @@ def foo(): return 1 queue = list(range(3)) - self.assertEqual(foo(), 1) - self.assertEqual(sleep.call_count, 3) + assert foo() == 1 + assert sleep.call_count == 3 @mock.patch('time.sleep') def test_retry_failure(self, sleep): @@ -88,8 +88,9 @@ def foo(): return 1 queue = list(range(6)) - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 5) + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 5 @mock.patch('time.sleep') def test_exception_arg(self, sleep): @@ -97,8 +98,9 @@ def test_exception_arg(self, sleep): def foo(): raise TypeError - self.assertRaises(TypeError, foo) - self.assertEqual(sleep.call_count, 0) + with pytest.raises(TypeError): + foo() + assert sleep.call_count == 0 @mock.patch('time.sleep') def test_no_interval_arg(self, sleep): @@ -108,8 +110,9 @@ def test_no_interval_arg(self, sleep): def foo(): 1 / 0 # noqa - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 0) + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 0 @mock.patch('time.sleep') def test_retries_arg(self, sleep): @@ -117,12 +120,14 @@ def test_retries_arg(self, sleep): def foo(): 1 / 0 # noqa - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 5) + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 5 @mock.patch('time.sleep') def test_retries_and_timeout_args(self, sleep): - self.assertRaises(ValueError, retry, retries=5, timeout=1) + with pytest.raises(ValueError): + retry(retries=5, timeout=1) class TestSyncTestUtils(PsutilTestCase): @@ -130,7 +135,8 @@ 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) + with pytest.raises(psutil.NoSuchProcess): + wait_for_pid(nopid) def test_wait_for_file(self): testfn = self.get_testfn() @@ -149,7 +155,8 @@ def test_wait_for_file_empty(self): 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) + with pytest.raises(IOError): + wait_for_file(testfn) def test_wait_for_file_no_delete(self): testfn = self.get_testfn() @@ -160,17 +167,17 @@ def test_wait_for_file_no_delete(self): def test_call_until(self): ret = call_until(lambda: 1, "ret == 1") - self.assertEqual(ret, 1) + assert ret == 1 class TestFSTestUtils(PsutilTestCase): def test_open_text(self): with open_text(__file__) as f: - self.assertEqual(f.mode, 'r') + assert f.mode == 'r' def test_open_binary(self): with open_binary(__file__) as f: - self.assertEqual(f.mode, 'rb') + assert f.mode == 'rb' def test_safe_mkdir(self): testfn = self.get_testfn() @@ -195,7 +202,7 @@ def test_safe_rmpath(self): with mock.patch( 'psutil.tests.os.stat', side_effect=OSError(errno.EINVAL, "") ) as m: - with self.assertRaises(OSError): + with pytest.raises(OSError): safe_rmpath(testfn) assert m.called @@ -204,8 +211,8 @@ 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) + assert os.getcwd() == os.path.join(base, testfn) + assert os.getcwd() == base class TestProcessUtils(PsutilTestCase): @@ -220,17 +227,17 @@ def test_reap_children(self): def test_spawn_children_pair(self): child, grandchild = self.spawn_children_pair() - self.assertNotEqual(child.pid, grandchild.pid) + assert child.pid != grandchild.pid assert child.is_running() assert grandchild.is_running() children = psutil.Process().children() - self.assertEqual(children, [child]) + assert 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) + assert len(children) == 2 + assert child in children + assert grandchild in children + assert child.ppid() == os.getpid() + assert grandchild.ppid() == child.pid terminate(child) assert not child.is_running() @@ -242,7 +249,7 @@ def test_spawn_children_pair(self): @unittest.skipIf(not POSIX, "POSIX only") def test_spawn_zombie(self): _parent, zombie = self.spawn_zombie() - self.assertEqual(zombie.status(), psutil.STATUS_ZOMBIE) + assert zombie.status() == psutil.STATUS_ZOMBIE def test_terminate(self): # by subprocess.Popen @@ -288,23 +295,23 @@ 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) + assert 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 sock.family == socket.AF_UNIX + assert sock.type == socket.SOCK_STREAM + assert 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) + assert sock.type == socket.SOCK_DGRAM def tcp_tcp_socketpair(self): addr = ("127.0.0.1", get_free_port()) @@ -313,9 +320,9 @@ def tcp_tcp_socketpair(self): 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) + assert server.getsockname() == addr + assert client.getpeername() == addr + assert client.getsockname() != addr @unittest.skipIf(not POSIX, "POSIX only") @unittest.skipIf( @@ -324,23 +331,23 @@ def tcp_tcp_socketpair(self): def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() - self.assertEqual( - filter_proc_net_connections(p.net_connections(kind='unix')), [] + assert ( + filter_proc_net_connections(p.net_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( + assert p.num_fds() - num_fds == 2 + assert ( len( filter_proc_net_connections(p.net_connections(kind='unix')) - ), - 2, + ) + == 2 ) - self.assertEqual(server.getsockname(), name) - self.assertEqual(client.getpeername(), name) + assert server.getsockname() == name + assert client.getpeername() == name finally: client.close() server.close() @@ -353,13 +360,13 @@ def test_create_sockets(self): 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) + assert fams[socket.AF_INET] >= 2 if supports_ipv6(): - self.assertGreaterEqual(fams[socket.AF_INET6], 2) + assert fams[socket.AF_INET6] >= 2 if POSIX and HAS_NET_CONNECTIONS_UNIX: - self.assertGreaterEqual(fams[socket.AF_UNIX], 2) - self.assertGreaterEqual(types[socket.SOCK_STREAM], 2) - self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2) + assert fams[socket.AF_UNIX] >= 2 + assert types[socket.SOCK_STREAM] >= 2 + assert types[socket.SOCK_DGRAM] >= 2 @pytest.mark.xdist_group(name="serial") @@ -371,14 +378,19 @@ def fun(): cnt = {'cnt': 0} self.execute(fun, times=10, warmup_times=15) - self.assertEqual(cnt['cnt'], 26) + assert 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) + with pytest.raises(ValueError): + self.execute(lambda: 0, times=0) + with pytest.raises(ValueError): + self.execute(lambda: 0, times=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, warmup_times=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, tolerance=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, retries=-1) @retry_on_failure() @unittest.skipIf(CI_TESTING, "skipped on CI") @@ -391,9 +403,8 @@ def fun(ls=ls): try: # will consume around 60M in total - self.assertRaisesRegex( - AssertionError, "extra-mem", self.execute, fun, times=100 - ) + with pytest.raises(AssertionError, match="extra-mem"): + self.execute(fun, times=100) finally: del ls @@ -405,9 +416,8 @@ def fun(): box = [] kind = "fd" if POSIX else "handle" - self.assertRaisesRegex( - AssertionError, "unclosed " + kind, self.execute, fun - ) + with pytest.raises(AssertionError, match="unclosed " + kind): + self.execute(fun) def test_tolerance(self): def fun(): @@ -418,20 +428,20 @@ def fun(): self.execute( fun, times=times, warmup_times=0, tolerance=200 * 1024 * 1024 ) - self.assertEqual(len(ls), times + 1) + assert 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): + with pytest.raises(ZeroDivisionError): self.execute_w_exc(OSError, fun_1) def fun_2(): pass - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.execute_w_exc(ZeroDivisionError, fun_2) @@ -441,12 +451,12 @@ def test_process_namespace(self): 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()) + assert 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()) + assert fun() == psutil.net_if_addrs() class TestOtherUtils(PsutilTestCase): diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index af32b56d1d..9177df5d7f 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -206,11 +206,9 @@ def test_proc_exe(self): subp = self.spawn_testproc(cmd) p = psutil.Process(subp.pid) exe = p.exe() - self.assertIsInstance(exe, str) + assert isinstance(exe, str) if self.expect_exact_path_match(): - self.assertEqual( - os.path.normcase(exe), os.path.normcase(self.funky_name) - ) + assert os.path.normcase(exe) == os.path.normcase(self.funky_name) def test_proc_name(self): cmd = [ @@ -220,9 +218,9 @@ def test_proc_name(self): ] subp = self.spawn_testproc(cmd) name = psutil.Process(subp.pid).name() - self.assertIsInstance(name, str) + assert isinstance(name, str) if self.expect_exact_path_match(): - self.assertEqual(name, os.path.basename(self.funky_name)) + assert name == os.path.basename(self.funky_name) def test_proc_cmdline(self): cmd = [ @@ -234,9 +232,9 @@ def test_proc_cmdline(self): p = psutil.Process(subp.pid) cmdline = p.cmdline() for part in cmdline: - self.assertIsInstance(part, str) + assert isinstance(part, str) if self.expect_exact_path_match(): - self.assertEqual(cmdline, cmd) + assert cmdline == cmd def test_proc_cwd(self): dname = self.funky_name + "2" @@ -245,9 +243,9 @@ def test_proc_cwd(self): with chdir(dname): p = psutil.Process() cwd = p.cwd() - self.assertIsInstance(p.cwd(), str) + assert isinstance(p.cwd(), str) if self.expect_exact_path_match(): - self.assertEqual(cwd, dname) + assert cwd == dname @unittest.skipIf(PYPY and WINDOWS, "fails on PYPY + WINDOWS") def test_proc_open_files(self): @@ -256,14 +254,12 @@ def test_proc_open_files(self): with open(self.funky_name, 'rb'): new = set(p.open_files()) path = (new - start).pop().path - self.assertIsInstance(path, str) + assert isinstance(path, str) if BSD and not path: # XXX - see https://github.com/giampaolo/psutil/issues/595 raise unittest.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) - ) + assert os.path.normcase(path) == os.path.normcase(self.funky_name) @unittest.skipIf(not POSIX, "POSIX only") def test_proc_net_connections(self): @@ -277,8 +273,8 @@ def test_proc_net_connections(self): raise unittest.SkipTest("not supported") with closing(sock): conn = psutil.Process().net_connections('unix')[0] - self.assertIsInstance(conn.laddr, str) - self.assertEqual(conn.laddr, name) + assert isinstance(conn.laddr, str) + assert conn.laddr == name @unittest.skipIf(not POSIX, "POSIX only") @unittest.skipIf(not HAS_NET_CONNECTIONS_UNIX, "can't list UNIX sockets") @@ -301,8 +297,8 @@ def find_sock(cons): with closing(sock): cons = psutil.net_connections(kind='unix') conn = find_sock(cons) - self.assertIsInstance(conn.laddr, str) - self.assertEqual(conn.laddr, name) + assert isinstance(conn.laddr, str) + assert conn.laddr == name def test_disk_usage(self): dname = self.funky_name + "2" @@ -326,9 +322,9 @@ def normpath(p): ] # ...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) + assert normpath(funky_path) in libpaths for path in libpaths: - self.assertIsInstance(path, str) + assert isinstance(path, str) @unittest.skipIf(CI_TESTING, "unreliable on CI") @@ -366,6 +362,6 @@ def test_proc_environ(self): 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) + assert isinstance(k, str) + assert isinstance(v, str) + assert env['FUNNY_ARG'] == self.funky_suffix diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 935df40be1..0a2683dd24 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -20,6 +20,8 @@ import unittest import warnings +import pytest + import psutil from psutil import WINDOWS from psutil._compat import FileNotFoundError @@ -110,37 +112,35 @@ 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()) + assert 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) + assert 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) + assert 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) + assert 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)) - ) + assert 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) + assert proc.CurrentClockSpeed == psutil.cpu_freq().current + assert proc.MaxClockSpeed == psutil.cpu_freq().max class TestSystemAPIs(WindowsTestCase): @@ -157,27 +157,24 @@ def test_nic_names(self): def test_total_phymem(self): w = wmi.WMI().Win32_ComputerSystem()[0] - self.assertEqual( - int(w.TotalPhysicalMemory), psutil.virtual_memory().total - ) + assert 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, + assert ( + abs(int(w.AvailableBytes) - psutil.virtual_memory().free) + < 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, + assert ( + 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) + assert psutil.swap_memory().free == 0 + assert psutil.swap_memory().used == 0 def test_percent_swapmem(self): if psutil.swap_memory().total > 0: @@ -186,11 +183,9 @@ def test_percent_swapmem(self): 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) + assert psutil.swap_memory().percent >= 0 + assert abs(psutil.swap_memory().percent - percentSwap) < 5 + assert psutil.swap_memory().percent <= 100 # @unittest.skipIf(wmi is None, "wmi module is not installed") # def test__UPTIME(self): @@ -212,7 +207,7 @@ def test_pids(self): 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) + assert wmi_pids == psutil_pids @retry_on_failure() def test_disks(self): @@ -233,9 +228,9 @@ def test_disks(self): except FileNotFoundError: # usually this is the floppy break - self.assertEqual(usage.total, int(wmi_part.Size)) + assert usage.total == int(wmi_part.Size) wmi_free = int(wmi_part.FreeSpace) - self.assertEqual(usage.free, wmi_free) + assert usage.free == wmi_free # 10 MB tolerance if abs(usage.free - wmi_free) > 10 * 1024 * 1024: raise self.fail( @@ -252,15 +247,11 @@ def test_disk_usage(self): 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 + assert abs(sys_value[0] - psutil_value.free) < TOLERANCE_DISK_USAGE + assert ( + abs(sys_value[1] - psutil_value.total) < TOLERANCE_DISK_USAGE ) + assert psutil_value.used == psutil_value.total - psutil_value.free def test_disk_partitions(self): sys_value = [ @@ -273,7 +264,7 @@ def test_disk_partitions(self): for x in psutil.disk_partitions(all=True) if not x.mountpoint.startswith('A:') ] - self.assertEqual(sys_value, psutil_value) + assert sys_value == psutil_value def test_net_if_stats(self): ps_names = set(cext.net_if_stats()) @@ -282,9 +273,9 @@ def test_net_if_stats(self): 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), + assert ps_names & wmi_names, "no common entries in %s, %s" % ( + ps_names, + wmi_names, ) def test_boot_time(self): @@ -295,18 +286,18 @@ def test_boot_time(self): ) psutil_dt = datetime.datetime.fromtimestamp(psutil.boot_time()) diff = abs((wmi_btime_dt - psutil_dt).total_seconds()) - self.assertLessEqual(diff, 5) + assert 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) + assert psutil.boot_time() == 5 with mock.patch('psutil._pswindows.cext.boot_time', return_value=4): - self.assertEqual(psutil.boot_time(), 5) + assert psutil.boot_time() == 5 with mock.patch('psutil._pswindows.cext.boot_time', return_value=6): - self.assertEqual(psutil.boot_time(), 5) + assert psutil.boot_time() == 5 with mock.patch('psutil._pswindows.cext.boot_time', return_value=333): - self.assertEqual(psutil.boot_time(), 333) + assert psutil.boot_time() == 333 # =================================================================== @@ -317,19 +308,18 @@ def test_boot_time_fluctuation(self): class TestSensorsBattery(WindowsTestCase): def test_has_battery(self): if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: - self.assertIsNotNone(psutil.sensors_battery()) + assert psutil.sensors_battery() is not None else: - self.assertIsNone(psutil.sensors_battery()) + assert psutil.sensors_battery() is None @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, + assert ( + abs(battery_psutil.percent - battery_wmi.EstimatedChargeRemaining) + < 1 ) @unittest.skipIf(not HAS_BATTERY, "no battery") @@ -339,24 +329,23 @@ def test_power_plugged(self): 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 - ) + assert 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 psutil.sensors_battery() is None 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 ( + psutil.sensors_battery().secsleft + == psutil.POWER_TIME_UNLIMITED ) assert m.called @@ -364,8 +353,9 @@ 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 ( + psutil.sensors_battery().secsleft + == psutil.POWER_TIME_UNLIMITED ) assert m.called @@ -374,8 +364,8 @@ def test_emulate_secs_left_unknown(self): "psutil._pswindows.cext.sensors_battery", return_value=(0, 0, 0, -1), ) as m: - self.assertEqual( - psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNKNOWN + assert ( + psutil.sensors_battery().secsleft == psutil.POWER_TIME_UNKNOWN ) assert m.called @@ -396,16 +386,17 @@ def tearDownClass(cls): def test_issue_24(self): p = psutil.Process(0) - self.assertRaises(psutil.AccessDenied, p.kill) + with pytest.raises(psutil.AccessDenied): + p.kill() def test_special_pid(self): p = psutil.Process(4) - self.assertEqual(p.name(), 'System') + assert 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) + assert p.create_time() >= 0.0 try: rss, _vms = p.memory_info()[:2] except psutil.AccessDenied: @@ -413,11 +404,12 @@ def test_special_pid(self): if platform.uname()[1] not in ('vista', 'win-7', 'win7'): raise else: - self.assertGreater(rss, 0) + assert rss > 0 def test_send_signal(self): p = psutil.Process(self.pid) - self.assertRaises(ValueError, p.send_signal, signal.SIGINT) + with pytest.raises(ValueError): + p.send_signal(signal.SIGINT) def test_num_handles_increment(self): p = psutil.Process(os.getpid()) @@ -426,9 +418,9 @@ def test_num_handles_increment(self): win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() ) after = p.num_handles() - self.assertEqual(after, before + 1) + assert after == before + 1 win32api.CloseHandle(handle) - self.assertEqual(p.num_handles(), before) + assert p.num_handles() == before def test_ctrl_signals(self): p = psutil.Process(self.spawn_testproc().pid) @@ -436,12 +428,10 @@ def test_ctrl_signals(self): 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 - ) + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(signal.CTRL_C_EVENT) + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(signal.CTRL_BREAK_EVENT) def test_username(self): name = win32api.GetUserNameEx(win32con.NameSamCompatible) @@ -450,7 +440,7 @@ def test_username(self): # 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) + assert psutil.Process().username() == name def test_cmdline(self): sys_value = re.sub('[ ]+', ' ', win32api.GetCommandLine()).strip() @@ -461,7 +451,7 @@ def test_cmdline(self): # 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) + assert sys_value == psutil_value # XXX - occasional failures @@ -485,7 +475,7 @@ def test_nice(self): self.addCleanup(win32api.CloseHandle, handle) sys_value = win32process.GetPriorityClass(handle) psutil_value = psutil.Process().nice() - self.assertEqual(psutil_value, sys_value) + assert psutil_value == sys_value def test_memory_info(self): handle = win32api.OpenProcess( @@ -494,30 +484,25 @@ def test_memory_info(self): 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 + assert sys_value['PeakWorkingSetSize'] == psutil_value.peak_wset + assert sys_value['WorkingSetSize'] == psutil_value.wset + assert ( + sys_value['QuotaPeakPagedPoolUsage'] + == psutil_value.peak_paged_pool ) - self.assertEqual( - sys_value['QuotaPeakNonPagedPoolUsage'], - psutil_value.peak_nonpaged_pool, + assert sys_value['QuotaPagedPoolUsage'] == psutil_value.paged_pool + assert ( + 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 + assert ( + sys_value['QuotaNonPagedPoolUsage'] == psutil_value.nonpaged_pool ) + assert sys_value['PagefileUsage'] == psutil_value.pagefile + assert sys_value['PeakPagefileUsage'] == psutil_value.peak_pagefile - self.assertEqual(psutil_value.rss, psutil_value.wset) - self.assertEqual(psutil_value.vms, psutil_value.pagefile) + assert psutil_value.rss == psutil_value.wset + assert psutil_value.vms == psutil_value.pagefile def test_wait(self): handle = win32api.OpenProcess( @@ -528,7 +513,7 @@ def test_wait(self): p.terminate() psutil_value = p.wait() sys_value = win32process.GetExitCodeProcess(handle) - self.assertEqual(psutil_value, sys_value) + assert psutil_value == sys_value def test_cpu_affinity(self): def from_bitmask(x): @@ -542,7 +527,7 @@ def from_bitmask(x): win32process.GetProcessAffinityMask(handle)[0] ) psutil_value = psutil.Process(self.pid).cpu_affinity() - self.assertEqual(psutil_value, sys_value) + assert psutil_value == sys_value def test_io_counters(self): handle = win32api.OpenProcess( @@ -551,24 +536,12 @@ def test_io_counters(self): 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'] - ) + assert psutil_value.read_count == sys_value['ReadOperationCount'] + assert psutil_value.write_count == sys_value['WriteOperationCount'] + assert psutil_value.read_bytes == sys_value['ReadTransferCount'] + assert psutil_value.write_bytes == sys_value['WriteTransferCount'] + assert psutil_value.other_count == sys_value['OtherOperationCount'] + assert psutil_value.other_bytes == sys_value['OtherTransferCount'] def test_num_handles(self): import ctypes @@ -586,7 +559,7 @@ def test_num_handles(self): ) sys_value = hndcnt.value psutil_value = psutil.Process(self.pid).num_handles() - self.assertEqual(psutil_value, sys_value) + assert psutil_value == sys_value def test_error_partial_copy(self): # https://github.com/giampaolo/psutil/issues/875 @@ -595,15 +568,17 @@ def test_error_partial_copy(self): 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) + with pytest.raises(psutil.AccessDenied): + p.cwd() + assert 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) + with pytest.raises(psutil.NoSuchProcess): + proc.exe() class TestProcessWMI(WindowsTestCase): @@ -620,7 +595,7 @@ def tearDownClass(cls): def test_name(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] p = psutil.Process(self.pid) - self.assertEqual(p.name(), w.Caption) + assert p.name() == w.Caption # This fail on github because using virtualenv for test environment @unittest.skipIf(GITHUB_ACTIONS, "unreliable path on GITHUB_ACTIONS") @@ -629,26 +604,26 @@ def test_exe(self): 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()) + assert 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('"', '')) + assert ' '.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) + assert 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)) + assert rss == int(w.WorkingSetSize) @retry_on_failure() def test_memory_vms(self): @@ -670,7 +645,7 @@ def test_create_time(self): psutil_create = time.strftime( "%Y%m%d%H%M%S", time.localtime(p.create_time()) ) - self.assertEqual(wmic_create, psutil_create) + assert wmic_create == psutil_create # --- @@ -702,11 +677,11 @@ def test_memory_info(self): side_effect=OSError(errno.EPERM, "msg"), ) as fun: mem_2 = psutil.Process(self.pid).memory_info() - self.assertEqual(len(mem_1), len(mem_2)) + assert 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 mem_1[i] >= 0 + assert mem_2[i] >= 0 + assert abs(mem_1[i] - mem_2[i]) < 512 assert fun.called def test_create_time(self): @@ -715,7 +690,7 @@ def test_create_time(self): "psutil._psplatform.cext.proc_times", side_effect=OSError(errno.EPERM, "msg"), ) as fun: - self.assertEqual(psutil.Process(self.pid).create_time(), ctime) + assert psutil.Process(self.pid).create_time() == ctime assert fun.called def test_cpu_times(self): @@ -726,12 +701,8 @@ def test_cpu_times(self): ) 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 - ) + assert abs(cpu_times_1.user - cpu_times_2.user) < 0.01 + assert abs(cpu_times_1.system - cpu_times_2.system) < 0.01 def test_io_counters(self): io_counters_1 = psutil.Process(self.pid).io_counters() @@ -741,9 +712,7 @@ def test_io_counters(self): ) 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 abs(io_counters_1[i] - io_counters_2[i]) < 5 assert fun.called def test_num_handles(self): @@ -752,9 +721,7 @@ def test_num_handles(self): "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 psutil.Process(self.pid).num_handles() == num_handles assert fun.called def test_cmdline(self): @@ -769,7 +736,7 @@ def test_cmdline(self): ): raise else: - self.assertEqual(a, b) + assert a == b @unittest.skipIf(not WINDOWS, "WINDOWS only") @@ -831,27 +798,27 @@ def tearDown(self): 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) + assert len(p.cmdline()) == 3 + assert 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) + assert len(p.cmdline()) == 3 + assert p.cmdline()[1:] == self.test_args def test_cwd_32(self): p = psutil.Process(self.proc32.pid) - self.assertEqual(p.cwd(), os.getcwd()) + assert p.cwd() == os.getcwd() def test_cwd_64(self): p = psutil.Process(self.proc64.pid) - self.assertEqual(p.cwd(), os.getcwd()) + assert 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())) + assert "THINK_OF_A_NUMBER" in e + assert e["THINK_OF_A_NUMBER"] == str(os.getpid()) def test_environ_64(self): p = psutil.Process(self.proc64.pid) @@ -890,27 +857,27 @@ def test_win_service_iter(self): ]) 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) + assert isinstance(data['name'], str) + assert data['name'].strip() + assert isinstance(data['display_name'], str) + assert isinstance(data['username'], str) + assert data['status'] in 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) + assert isinstance(data['binpath'], str) + assert isinstance(data['username'], str) + assert isinstance(data['start_type'], str) + assert data['start_type'] in valid_start_types + assert data['status'] in valid_statuses + assert isinstance(data['description'], str) pid = serv.pid() if pid is not None: p = psutil.Process(pid) - self.assertTrue(p.is_running()) + assert p.is_running() # win_service_get s = psutil.win_service_get(serv.name()) # test __eq__ - self.assertEqual(serv, s) + assert serv == s def test_win_service_get(self): ERROR_SERVICE_DOES_NOT_EXIST = ( @@ -919,9 +886,9 @@ def test_win_service_get(self): ERROR_ACCESS_DENIED = psutil._psplatform.cext.ERROR_ACCESS_DENIED name = next(psutil.win_service_iter()).name() - with self.assertRaises(psutil.NoSuchProcess) as cm: + with pytest.raises(psutil.NoSuchProcess) as cm: psutil.win_service_get(name + '???') - self.assertEqual(cm.exception.name, name + '???') + assert cm.value.name == name + '???' # test NoSuchProcess service = psutil.win_service_get(name) @@ -933,11 +900,13 @@ def test_win_service_get(self): with mock.patch( "psutil._psplatform.cext.winservice_query_status", side_effect=exc ): - self.assertRaises(psutil.NoSuchProcess, service.status) + with pytest.raises(psutil.NoSuchProcess): + service.status() with mock.patch( "psutil._psplatform.cext.winservice_query_config", side_effect=exc ): - self.assertRaises(psutil.NoSuchProcess, service.username) + with pytest.raises(psutil.NoSuchProcess): + service.username() # test AccessDenied if PY3: @@ -948,14 +917,16 @@ def test_win_service_get(self): with mock.patch( "psutil._psplatform.cext.winservice_query_status", side_effect=exc ): - self.assertRaises(psutil.AccessDenied, service.status) + with pytest.raises(psutil.AccessDenied): + service.status() with mock.patch( "psutil._psplatform.cext.winservice_query_config", side_effect=exc ): - self.assertRaises(psutil.AccessDenied, service.username) + with pytest.raises(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)) + assert service.name() in str(service) + assert service.display_name() in str(service) + assert service.name() in repr(service) + assert service.display_name() in repr(service) From 8b71dce40b11f35d76d081088b909491318fb3d7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 3 Oct 2024 20:31:24 +0200 Subject: [PATCH 1133/1714] refact Makefile and add scripts/internal/install_pip.py --- MANIFEST.in | 1 + Makefile | 81 ++++++++------------------------- psutil/tests/test_posix.py | 41 +++++++++-------- scripts/internal/install_pip.py | 58 +++++++++++++++++++++++ 4 files changed, 100 insertions(+), 81 deletions(-) create mode 100755 scripts/internal/install_pip.py diff --git a/MANIFEST.in b/MANIFEST.in index 3a381cce12..91e5e2accb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -188,6 +188,7 @@ include scripts/internal/download_wheels_github.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py include scripts/internal/install-sysdeps.sh +include scripts/internal/install_pip.py include scripts/internal/print_access_denied.py include scripts/internal/print_announce.py include scripts/internal/print_api_speed.py diff --git a/Makefile b/Makefile index b0fee04311..cbf20cb066 100644 --- a/Makefile +++ b/Makefile @@ -2,46 +2,24 @@ # To use a specific Python version run: "make install PYTHON=python3.3" # You can set the variables below from the command line. +# Configurable PYTHON = python3 -PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 -PYTEST_ARGS = -v -s --tb=short ARGS = -# mandatory deps for running tests -PY3_DEPS = \ - setuptools \ - pytest \ - pytest-xdist - -# python 2 deps -PY2_DEPS = \ - futures \ - ipaddress \ - mock==1.0.1 \ - pytest==4.6.11 \ - pytest-xdist \ - setuptools - -PY_DEPS = `$(PYTHON) -c \ - "import sys; \ - py3 = sys.version_info[0] == 3; \ - py38 = sys.version_info[:2] >= (3, 8); \ - py3_extra = ' abi3audit' if py38 else ''; \ - print('$(PY3_DEPS)' + py3_extra if py3 else '$(PY2_DEPS)')"` - -NUM_WORKERS = `$(PYTHON) -c "import os; print(os.cpu_count() or 1)"` - # "python3 setup.py build" can be parallelized on Python >= 3.6. -BUILD_OPTS = `$(PYTHON) -c \ +SETUP_BUILD_EXT_ARGS = `$(PYTHON) -c \ "import sys, os; \ py36 = sys.version_info[:2] >= (3, 6); \ cpus = os.cpu_count() or 1 if py36 else 1; \ print('--parallel %s' % cpus if cpus > 1 else '')"` # In not in a virtualenv, add --user options for install commands. -SETUP_INSTALL_OPTS = `$(PYTHON) -c \ +SETUP_INSTALL_ARGS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` -PIP_INSTALL_OPTS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade + +PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade +PYTEST_ARGS = -v -s --tb=short +PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help @@ -84,52 +62,33 @@ build: ## Compile (in parallel) without installing. @# "build_ext -i" copies compiled *.so files in ./psutil directory in order @# to allow "import psutil" when using the interactive interpreter from @# within this directory. - $(PYTHON_ENV_VARS) $(PYTHON) setup.py build_ext -i $(BUILD_OPTS) + $(PYTHON_ENV_VARS) $(PYTHON) setup.py build_ext -i $(SETUP_BUILD_EXT_ARGS) $(PYTHON_ENV_VARS) $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(SETUP_INSTALL_OPTS) + $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(SETUP_INSTALL_ARGS) uninstall: ## Uninstall this package via pip. cd ..; $(PYTHON_ENV_VARS) $(PYTHON) -m pip uninstall -y -v psutil || true $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/purge_installation.py install-pip: ## Install pip (no-op if already installed). - @$(PYTHON) -c \ - "import sys, ssl, os, pkgutil, tempfile, atexit; \ - print('pip already installed') if pkgutil.find_loader('pip') else None; \ - sys.exit(0) if pkgutil.find_loader('pip') else None; \ - PY3 = sys.version_info[0] == 3; \ - pyexc = 'from urllib.request import urlopen' if PY3 else 'from urllib2 import urlopen'; \ - exec(pyexc); \ - ctx = ssl._create_unverified_context() if hasattr(ssl, '_create_unverified_context') else None; \ - url = 'https://bootstrap.pypa.io/pip/2.7/get-pip.py' if not PY3 else 'https://bootstrap.pypa.io/get-pip.py'; \ - kw = dict(context=ctx) if ctx else {}; \ - req = urlopen(url, **kw); \ - data = req.read(); \ - f = tempfile.NamedTemporaryFile(suffix='.py'); \ - atexit.register(f.close); \ - f.write(data); \ - f.flush(); \ - print('downloaded %s' % f.name); \ - code = os.system('%s %s --user --upgrade' % (sys.executable, f.name)); \ - f.close(); \ - sys.exit(code);" + $(PYTHON) scripts/internal/install_pip.py install-sysdeps: ./scripts/internal/install-sysdeps.sh install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) pip # upgrade pip to latest version - $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip # upgrade pip to latest version + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` install-pydeps-dev: ## Install python deps meant for local development. ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) pip # upgrade pip to latest version - $(PYTHON) -m pip install $(PIP_INSTALL_OPTS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS + setup.DEV_DEPS))"` + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip # upgrade pip to latest version + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS + setup.DEV_DEPS))"` install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit @@ -216,7 +175,7 @@ black: ## Python files linting (via black) @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe _pylint: ## Python pylint (not mandatory, just run it from time to time) - @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=${NUM_WORKERS} + @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=0 lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py @@ -287,20 +246,20 @@ pre-release: ## Check if we're ready to produce a new release. ${MAKE} sdist ${MAKE} check-sdist ${MAKE} install - $(PYTHON) -c \ + @$(PYTHON) -c \ "import requests, sys; \ from packaging.version import parse; \ from psutil import __version__; \ res = requests.get('https://pypi.org/pypi/psutil/json', timeout=5); \ versions = sorted(res.json()['releases'], key=parse, reverse=True); \ sys.exit('version %r already exists on PYPI' % __version__) if __version__ in versions else 0" - $(PYTHON) -c \ + @$(PYTHON) -c \ "from psutil import __version__ as ver; \ doc = open('docs/index.rst').read(); \ history = open('HISTORY.rst').read(); \ - assert ver in doc, '%r not in docs/index.rst' % ver; \ - assert ver in history, '%r not in HISTORY.rst' % ver; \ - assert 'XXXX' not in history, 'XXXX in HISTORY.rst';" + assert ver in doc, '%r not found in docs/index.rst' % ver; \ + assert ver in history, '%r not found in HISTORY.rst' % ver; \ + assert 'XXXX' not in history, 'XXXX found in HISTORY.rst';" ${MAKE} download-wheels-github ${MAKE} download-wheels-appveyor ${MAKE} check-wheels diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 727b16c28a..dd2d179dda 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -137,6 +137,22 @@ def ps_vsz(pid): return ps(field, pid) +def df(device): + try: + out = sh("df -k %s" % device).strip() + except RuntimeError as err: + if "device busy" in str(err).lower(): + raise unittest.SkipTest("df returned EBUSY") + raise + line = out.split('\n')[1] + fields = line.split() + sys_total = int(fields[1]) * 1024 + sys_used = int(fields[2]) * 1024 + sys_free = int(fields[3]) * 1024 + sys_percent = float(fields[4].replace('%', '')) + return (sys_total, sys_used, sys_free, sys_percent) + + @unittest.skipIf(not POSIX, "POSIX only") class TestProcess(PsutilTestCase): """Compare psutil results against 'ps' command line utility (mainly).""" @@ -448,26 +464,11 @@ def test_os_waitpid_bad_ret_status(self): @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 unittest.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) + sys_total, sys_used, sys_free, sys_percent = df(part.device) except RuntimeError as err: # see: # https://travis-ci.org/giampaolo/psutil/jobs/138338464 @@ -481,10 +482,10 @@ def df(device): continue raise else: - assert abs(usage.total - total) < tolerance - assert abs(usage.used - used) < tolerance - assert abs(usage.free - free) < tolerance - assert abs(usage.percent - percent) < 1 + assert abs(usage.total - sys_total) < tolerance + assert abs(usage.used - sys_used) < tolerance + assert abs(usage.free - sys_free) < tolerance + assert abs(usage.percent - sys_percent) <= 1 @unittest.skipIf(not POSIX, "POSIX only") diff --git a/scripts/internal/install_pip.py b/scripts/internal/install_pip.py new file mode 100755 index 0000000000..44557529ab --- /dev/null +++ b/scripts/internal/install_pip.py @@ -0,0 +1,58 @@ +#!/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. + +import sys + + +try: + import pip # noqa: F401 +except ImportError: + pass +else: + print("pip already installed") + sys.exit(0) + +import os +import ssl +import tempfile + + +PY3 = sys.version_info[0] >= 3 +if PY3: + from urllib.request import urlopen + + URL = "https://bootstrap.pypa.io/get-pip.py" + +else: + from urllib2 import urlopen + + URL = "https://bootstrap.pypa.io/pip/2.7/get-pip.py" + + +def main(): + ssl_context = ( + ssl._create_unverified_context() + if hasattr(ssl, "_create_unverified_context") + else None + ) + with tempfile.NamedTemporaryFile(suffix=".py") as f: + print("downloading %s into %s" % (URL, f.name)) + kwargs = dict(context=ssl_context) if ssl_context else {} + req = urlopen(URL, **kwargs) + data = req.read() + req.close() + + f.write(data) + f.flush() + print("download finished, installing pip") + + code = os.system("%s %s --user --upgrade" % (sys.executable, f.name)) + + sys.exit(code) + + +if __name__ == "__main__": + main() From 1054b5e5aaccf7f85b209898abceeab8d9702eb3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 5 Oct 2024 09:46:47 +0200 Subject: [PATCH 1134/1714] remove unittest.TestCase ovverride (no longer useful after pytest) --- psutil/tests/__init__.py | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 224faaa7d0..86bd6dca7a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -915,42 +915,7 @@ def get_testfn(suffix="", dir=None): # =================================================================== -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): +class PsutilTestCase(unittest.TestCase): """Test class providing auto-cleanup wrappers on top of process test utilities. """ From 70b6787e432417c114daf50e3026e532da411fc6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 5 Oct 2024 09:49:57 +0200 Subject: [PATCH 1135/1714] fix #2455 [Linux]: fix ``IndexError`` if /proc/pid/stat has no field 40 (blkio_ticks). --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4ca7a58941..d70cee6c98 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,8 @@ XXXX-XX-XX - 2427_: psutil (segfault) on import in the free-threaded (no GIL) version of Python 3.13. (patch by Sam Gross) +- 2455_, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and + field 40 (blkio_ticks) is missing. 6.0.0 ====== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7dfa1430ac..d7e76c4f57 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1796,7 +1796,12 @@ def _parse_stat_file(self): ret['children_stime'] = fields[14] ret['create_time'] = fields[19] ret['cpu_num'] = fields[36] - ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks' + try: + ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks' + except IndexError: + # https://github.com/giampaolo/psutil/issues/2455 + debug("can't get blkio_ticks, set iowait to 0") + ret['blkio_ticks'] = 0 return ret From 809dd5a457bc92e4ab9739d5be265a5bccd19de3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Oct 2024 00:52:23 +0200 Subject: [PATCH 1136/1714] add unittest2 dep for python 2 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index fb36da0bc4..949716a5b8 100755 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ "pytest-xdist", "pytest==4.6.11", "setuptools", + "unittest2", ] if WINDOWS and not PYPY: TEST_DEPS.append("pywin32") From 4e85bee505038a691eaf89d12609bf9e8ca3c5c0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Oct 2024 01:19:21 +0200 Subject: [PATCH 1137/1714] change call_until() signature so that it can be used with lambda --- psutil/tests/__init__.py | 10 ++++------ psutil/tests/test_linux.py | 6 +++--- psutil/tests/test_process.py | 7 ++++--- psutil/tests/test_testutils.py | 3 +-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 86bd6dca7a..46c25e430e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -472,7 +472,7 @@ def spawn_zombie(): zpid = int(conn.recv(1024)) _pids_started.add(zpid) zombie = psutil.Process(zpid) - call_until(zombie.status, "ret == psutil.STATUS_ZOMBIE") + call_until(lambda: zombie.status() == psutil.STATUS_ZOMBIE) return (parent, zombie) finally: conn.close() @@ -792,12 +792,10 @@ def wait_for_file(fname, delete=True, empty=False): timeout=GLOBAL_TIMEOUT, interval=0.001, ) -def call_until(fun, expr): - """Keep calling function for timeout secs and exit if eval() - expression is True. - """ +def call_until(fun): + """Keep calling function until it evaluates to True.""" ret = fun() - assert eval(expr) # noqa + assert ret return ret diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index fddce78d2c..aa51739b1e 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1953,7 +1953,7 @@ def test_open_files_file_gone(self): 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)) + call_until(lambda: len(p.open_files()) != len(files)) with mock.patch( 'psutil._pslinux.os.readlink', side_effect=OSError(errno.ENOENT, ""), @@ -1977,7 +1977,7 @@ def test_open_files_fd_gone(self): 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)) + call_until(lambda: len(p.open_files()) != len(files)) patch_point = 'builtins.open' if PY3 else '__builtin__.open' with mock.patch( patch_point, side_effect=IOError(errno.ENOENT, "") @@ -1993,7 +1993,7 @@ def test_open_files_enametoolong(self): 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)) + call_until(lambda: len(p.open_files()) != len(files)) patch_point = 'psutil._pslinux.os.readlink' with mock.patch( patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 49a94b7a76..a339ac2d3d 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -988,7 +988,7 @@ def test_cwd_2(self): ), ] p = self.spawn_psproc(cmd) - call_until(p.cwd, "ret == os.path.dirname(os.getcwd())") + call_until(lambda: p.cwd() == os.path.dirname(os.getcwd())) @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') def test_cpu_affinity(self): @@ -1075,7 +1075,8 @@ def test_open_files(self): 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)) + call_until(lambda: len(p.open_files()) != len(files)) + files = p.open_files() filenames = [os.path.normcase(x.path) for x in files] assert os.path.normcase(testfn) in filenames if LINUX: @@ -1399,7 +1400,7 @@ def assert_raises_nsp(fun, fun_name): p.terminate() p.wait() if WINDOWS: # XXX - call_until(psutil.pids, "%s not in ret" % p.pid) + call_until(lambda: p.pid not in psutil.pids()) self.assertProcessGone(p) ns = process_namespace(p) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 64f172c721..90644a5294 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -166,8 +166,7 @@ def test_wait_for_file_no_delete(self): assert os.path.exists(testfn) def test_call_until(self): - ret = call_until(lambda: 1, "ret == 1") - assert ret == 1 + call_until(lambda: 1) class TestFSTestUtils(PsutilTestCase): From 80af8793ddef10c4094c2c533da422663fedf092 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Oct 2024 02:04:20 +0200 Subject: [PATCH 1138/1714] revert change which broke python 2 tests --- psutil/tests/__init__.py | 22 ++++++++++++++++++++-- psutil/tests/test_testutils.py | 2 +- setup.py | 1 - 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 46c25e430e..e4667ab692 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -913,9 +913,27 @@ def get_testfn(suffix="", dir=None): # =================================================================== -class PsutilTestCase(unittest.TestCase): +class TestCase(unittest.TestCase): + # ...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. + test utilities. All test classes should derive from this one, even + if we use pytest. """ def get_testfn(self, suffix="", dir=None): diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 90644a5294..d74df153ae 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -398,7 +398,7 @@ def test_leak_mem(self): ls = [] def fun(ls=ls): - ls.append("x" * 124 * 1024) + ls.append("x" * 248 * 1024) try: # will consume around 60M in total diff --git a/setup.py b/setup.py index 949716a5b8..fb36da0bc4 100755 --- a/setup.py +++ b/setup.py @@ -80,7 +80,6 @@ "pytest-xdist", "pytest==4.6.11", "setuptools", - "unittest2", ] if WINDOWS and not PYPY: TEST_DEPS.append("pywin32") From 4649a8870107873efca51f068b40e814ef38f0bb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Oct 2024 10:41:36 +0200 Subject: [PATCH 1139/1714] Run tests without pytests (#2456) --- HISTORY.rst | 6 ++- docs/DEVGUIDE.rst | 8 +++ psutil/__init__.py | 2 +- psutil/tests/README.rst | 23 ++++----- psutil/tests/__init__.py | 78 +++++++++++++++++++++++++++- psutil/tests/__main__.py | 2 +- psutil/tests/test_bsd.py | 3 +- psutil/tests/test_connections.py | 3 +- psutil/tests/test_linux.py | 3 +- psutil/tests/test_misc.py | 24 +++++---- psutil/tests/test_posix.py | 3 +- psutil/tests/test_process.py | 8 +-- psutil/tests/test_process_all.py | 3 +- psutil/tests/test_system.py | 3 +- psutil/tests/test_testutils.py | 87 +++++++++++++++++++++++++++++++- psutil/tests/test_unicode.py | 3 +- psutil/tests/test_windows.py | 3 +- setup.py | 1 + 18 files changed, 212 insertions(+), 51 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d70cee6c98..c854ef80be 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -6.0.1 (IN DEVELOPMENT) +6.1.0 (IN DEVELOPMENT) ====================== XXXX-XX-XX @@ -14,6 +14,10 @@ XXXX-XX-XX targets. They can be used to install dependencies meant for running tests and for local development. They can also be installed via ``pip install .[test]`` and ``pip install .[dev]``. +- 2456_: allow to run tests via ``python3 -m psutil.tests`` even if ``pytest`` + module is not installed. This is useful for production environments that + don't have pytest installed, but still want to be able to test psutil + installation. **Bug fixes** diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 456714603f..563c9b8839 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -18,6 +18,14 @@ Once you have a compiler installed run: make install make test +- If you don't have the source code, and just want to test psutil installation. + This will work also if ``pytest`` module is not installed (e.g. production + environments) by using unittest's test runner: + +.. code-block:: bash + + python3 -m psutil.tests + - ``make`` (and the accompanying `Makefile`_) is the designated tool to build, install, run tests and do pretty much anything that involves development. This also includes Windows, meaning that you can run `make command` similarly diff --git a/psutil/__init__.py b/psutil/__init__.py index c1e038f3ed..763a5f00ec 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -214,7 +214,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "6.0.1" +__version__ = "6.1.0" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index 5c7336fb00..8e5b6e7d84 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -1,40 +1,35 @@ Instructions for running tests ============================== -Setup: +There are 2 ways of running tests. If you have the source code: .. code-block:: bash make install-pydeps-test # install pytest + make test -There are two ways of running tests. As a "user", if psutil is already -installed and you just want to test it works on your system: +If you don't have the source code, and just want to test psutil installation. +This will work also if ``pytest`` module is not installed (e.g. production +environments) by using unittest's test runner: .. code-block:: bash python -m psutil.tests -As a "developer", if you have a copy of the source code and you're working on -it: - -.. code-block:: bash - - make test - -You can run tests in parallel with: +To run tests in parallel (faster): .. code-block:: bash make test-parallel -You can run a specific test with: +Run a specific test: .. code-block:: bash make test ARGS=psutil.tests.test_system.TestDiskAPIs -You can run memory leak tests with: +Test C extension memory leaks: .. code-block:: bash - make test-parallel + make test-memleaks diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index e4667ab692..dd31392c02 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -36,7 +36,11 @@ from socket import AF_INET6 from socket import SOCK_STREAM -import pytest + +try: + import pytest +except ImportError: + pytest = None import psutil from psutil import AIX @@ -71,6 +75,8 @@ if PY3: import enum else: + import unittest2 as unittest + enum = None if POSIX: @@ -913,6 +919,76 @@ def get_testfn(suffix="", dir=None): # =================================================================== +class fake_pytest: + """A class that mimics some basic pytest APIs. This is meant for + when unit tests are run in production, where pytest may not be + installed. Still, the user can test psutil installation via: + + $ python3 -m psutil.tests + """ + + @staticmethod + def main(*args, **kw): # noqa ARG004 + """Mimics pytest.main(). It has the same effect as running + `python3 -m unittest -v` from the project root directory. + """ + suite = unittest.TestLoader().discover(HERE) + unittest.TextTestRunner(verbosity=2).run(suite) + warnings.warn( + "Fake pytest module was used. Test results may be inaccurate.", + UserWarning, + stacklevel=1, + ) + return suite + + @staticmethod + def raises(exc, match=None): + """Mimics `pytest.raises`.""" + + class ExceptionInfo: + _exc = None + + @property + def value(self): + return self._exc + + @contextlib.contextmanager + def context(exc, match=None): + einfo = ExceptionInfo() + try: + yield einfo + except exc as err: + if match and not re.search(match, str(err)): + msg = '"{}" does not match "{}"'.format(match, str(err)) + raise AssertionError(msg) + einfo._exc = err + else: + raise AssertionError("%r not raised" % exc) + + return context(exc, match=match) + + @staticmethod + def warns(warning, match=None): + """Mimics `pytest.warns`.""" + if match: + return unittest.TestCase().assertWarnsRegex(warning, match) + return unittest.TestCase().assertWarns(warning) + + class mark: + class xdist_group: + """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" + + def __init__(self, name=None): + pass + + def __call__(self, cls_or_meth): + return cls_or_meth + + +if pytest is None: + pytest = fake_pytest + + class TestCase(unittest.TestCase): # ...otherwise multiprocessing.Pool complains if not PY3: diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index f43b751f91..ce6fc24c7f 100644 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -6,7 +6,7 @@ $ python -m psutil.tests. """ -import pytest +from psutil.tests import pytest pytest.main(["-v", "-s", "--tb=short"]) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 13c8774608..3af505b608 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -16,8 +16,6 @@ import time import unittest -import pytest - import psutil from psutil import BSD from psutil import FREEBSD @@ -26,6 +24,7 @@ from psutil.tests import HAS_BATTERY from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase +from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import spawn_testproc diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 783bf81456..7f29900b27 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -16,8 +16,6 @@ from socket import SOCK_DGRAM from socket import SOCK_STREAM -import pytest - import psutil from psutil import FREEBSD from psutil import LINUX @@ -38,6 +36,7 @@ from psutil.tests import check_connection_ntuple from psutil.tests import create_sockets from psutil.tests import filter_proc_net_connections +from psutil.tests import pytest from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import skip_on_access_denied diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index aa51739b1e..788fe9914b 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -23,8 +23,6 @@ import unittest import warnings -import pytest - import psutil from psutil import LINUX from psutil._compat import PY3 @@ -46,6 +44,7 @@ from psutil.tests import ThreadTask from psutil.tests import call_until from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import reload_module from psutil.tests import retry_on_failure from psutil.tests import safe_rmpath diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index adafea947c..8ddaec28ad 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -18,8 +18,6 @@ import sys import unittest -import pytest - import psutil import psutil.tests from psutil import POSIX @@ -50,6 +48,7 @@ from psutil.tests import PsutilTestCase from psutil.tests import mock from psutil.tests import process_namespace +from psutil.tests import pytest from psutil.tests import reload_module from psutil.tests import sh from psutil.tests import system_namespace @@ -634,26 +633,29 @@ def test_debug(self): else: from StringIO import StringIO - with redirect_stderr(StringIO()) as f: - debug("hello") - sys.stderr.flush() + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + debug("hello") + sys.stderr.flush() msg = f.getvalue() assert msg.startswith("psutil-debug"), msg assert "hello" in msg assert __file__.replace('.pyc', '.py') in msg # supposed to use repr(exc) - with redirect_stderr(StringIO()) as f: - debug(ValueError("this is an error")) + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + debug(ValueError("this is an error")) msg = f.getvalue() assert "ignoring ValueError" in msg assert "'this is an error'" in 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) + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + exc = OSError(2, "no such file") + exc.filename = "/foo" + debug(exc) msg = f.getvalue() assert "no such file" in msg assert "/foo" in msg diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index dd2d179dda..5c2c86c9ea 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -15,8 +15,6 @@ import time import unittest -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -31,6 +29,7 @@ from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import skip_on_access_denied diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a339ac2d3d..b3e6569eed 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -21,8 +21,6 @@ import types import unittest -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -65,6 +63,7 @@ from psutil.tests import create_py_exe from psutil.tests import mock from psutil.tests import process_namespace +from psutil.tests import pytest from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh @@ -1452,8 +1451,9 @@ def test_reused_pid(self): # make sure is_running() removed PID from process_iter() # internal cache - with redirect_stderr(StringIO()) as f: - list(psutil.process_iter()) + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + list(psutil.process_iter()) assert ( "refreshing Process instance for reused PID %s" % p.pid in f.getvalue() diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index a6025390e1..1550046bad 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -16,8 +16,6 @@ import time import traceback -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -43,6 +41,7 @@ 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 pytest # Cuts the time in half, but (e.g.) on macOS the process pool stays diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 7403326bff..298d2d9e40 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -19,8 +19,6 @@ import time import unittest -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -56,6 +54,7 @@ from psutil.tests import check_net_address from psutil.tests import enum from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import retry_on_failure diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index d74df153ae..7293aa010c 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -14,9 +14,9 @@ import socket import stat import subprocess +import textwrap import unittest - -import pytest +import warnings import psutil import psutil.tests @@ -29,6 +29,7 @@ from psutil.tests import CI_TESTING from psutil.tests import COVERAGE from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import HERE from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV from psutil.tests import PsutilTestCase @@ -38,11 +39,13 @@ from psutil.tests import call_until from psutil.tests import chdir from psutil.tests import create_sockets +from psutil.tests import fake_pytest from psutil.tests import filter_proc_net_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 pytest from psutil.tests import reap_children from psutil.tests import retry from psutil.tests import retry_on_failure @@ -167,6 +170,7 @@ def test_wait_for_file_no_delete(self): def test_call_until(self): call_until(lambda: 1) + # TODO: test for timeout class TestFSTestUtils(PsutilTestCase): @@ -444,6 +448,85 @@ def fun_2(): self.execute_w_exc(ZeroDivisionError, fun_2) +class TestFakePytest(PsutilTestCase): + def test_raises(self): + with fake_pytest.raises(ZeroDivisionError) as cm: + 1 / 0 # noqa + assert isinstance(cm.value, ZeroDivisionError) + + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("foo") + + try: + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("bar") + except AssertionError as err: + assert str(err) == '"foo" does not match "bar"' + else: + raise self.fail("exception not raised") + + def test_mark(self): + @fake_pytest.mark.xdist_group(name="serial") + def foo(): + return 1 + + assert foo() == 1 + + @fake_pytest.mark.xdist_group(name="serial") + class Foo: + def bar(self): + return 1 + + assert Foo().bar() == 1 + + def test_main(self): + tmpdir = self.get_testfn(dir=HERE) + os.mkdir(tmpdir) + with open(os.path.join(tmpdir, "__init__.py"), "w"): + pass + with open(os.path.join(tmpdir, "test_file.py"), "w") as f: + f.write(textwrap.dedent("""\ + import unittest + + class TestCase(unittest.TestCase): + def test_passed(self): + pass + """).lstrip()) + with mock.patch.object(psutil.tests, "HERE", tmpdir): + with self.assertWarnsRegex( + UserWarning, "Fake pytest module was used" + ): + suite = fake_pytest.main() + assert suite.countTestCases() == 1 + + def test_warns(self): + # success + with fake_pytest.warns(UserWarning): + warnings.warn("foo", UserWarning, stacklevel=1) + + # failure + try: + with fake_pytest.warns(UserWarning): + warnings.warn("foo", DeprecationWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + # match success + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("foo", UserWarning, stacklevel=1) + + # match failure + try: + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("bar", UserWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + class TestTestingUtils(PsutilTestCase): def test_process_namespace(self): p = psutil.Process() diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 9177df5d7f..cc7a5475b0 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -79,8 +79,6 @@ import warnings from contextlib import closing -import pytest - import psutil from psutil import BSD from psutil import POSIX @@ -103,6 +101,7 @@ from psutil.tests import copyload_shared_lib from psutil.tests import create_py_exe from psutil.tests import get_testfn +from psutil.tests import pytest from psutil.tests import safe_mkdir from psutil.tests import safe_rmpath from psutil.tests import skip_on_access_denied diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 0a2683dd24..7b5ba7ba11 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -20,8 +20,6 @@ import unittest import warnings -import pytest - import psutil from psutil import WINDOWS from psutil._compat import FileNotFoundError @@ -37,6 +35,7 @@ from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import spawn_testproc diff --git a/setup.py b/setup.py index fb36da0bc4..949716a5b8 100755 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ "pytest-xdist", "pytest==4.6.11", "setuptools", + "unittest2", ] if WINDOWS and not PYPY: TEST_DEPS.append("pywin32") From 0a71d0e404ddaab32b5222351fd2292802e3b131 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 9 Oct 2024 21:10:01 +0200 Subject: [PATCH 1140/1714] enable ruff cache (faster) --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index cbf20cb066..7a38ee57f2 100644 --- a/Makefile +++ b/Makefile @@ -169,7 +169,7 @@ test-coverage: ## Run test coverage. # =================================================================== ruff: ## Run ruff linter. - @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --output-format=concise + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --output-format=concise black: ## Python files linting (via black) @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe @@ -201,7 +201,7 @@ fix-black: @git ls-files '*.py' | xargs $(PYTHON) -m black fix-ruff: - @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --no-cache --fix --output-format=concise $(ARGS) + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --fix --output-format=concise $(ARGS) fix-toml: ## Fix pyproject.toml @git ls-files '*.toml' | xargs toml-sort From f65fe4451a26205e3c0f9a97e7e49902bf275c3b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 14 Oct 2024 20:58:52 +0200 Subject: [PATCH 1141/1714] [OpenBSD] Process `num_fds()` and `open_files()` may raise NSP for PID 0 (#2460) This is weird, because of all the sudden the tests below started failing. This is despite PID 0 exists. The syscall failing with `ESRCH` is `kinfo_getfile`: https://github.com/giampaolo/psutil/blob/f8b929b0754a4abbdadfdcb8aae382ddee8a13f9/psutil/arch/openbsd/proc.c#L57 We therefore return "null" fallback values instead of raising `NoSuchProcess`. ``` =================================== FAILURES =================================== ___________________________ TestProcess.test_as_dict ___________________________ psutil/_psbsd.py:604: in wrapper return fun(self, *args, **kwargs) psutil/_psbsd.py:914: in open_files rawlist = cext.proc_open_files(self.pid) E ProcessLookupError: [Errno 3] No such process (originated from sysctl(kinfo_file) (1/2)) During handling of the above exception, another exception occurred: psutil/tests/test_process.py:1309: in test_as_dict d = p.as_dict() psutil/__init__.py:558: in as_dict ret = meth() psutil/__init__.py:1199: in open_files return self._proc.open_files() psutil/_psbsd.py:609: in wrapper raise NoSuchProcess(self.pid, self._name) E psutil.NoSuchProcess: process no longer exists (pid=0) ____________________________ TestProcess.test_pid_0 ____________________________ psutil/_psbsd.py:604: in wrapper return fun(self, *args, **kwargs) psutil/_psbsd.py:914: in open_files rawlist = cext.proc_open_files(self.pid) E ProcessLookupError: [Errno 3] No such process (originated from sysctl(kinfo_file) (1/2)) During handling of the above exception, another exception occurred: psutil/tests/test_process.py:1513: in test_pid_0 ret = fun() psutil/__init__.py:1199: in open_files return self._proc.open_files() psutil/_psbsd.py:609: in wrapper raise NoSuchProcess(self.pid, self._name) E psutil.NoSuchProcess: process no longer exists (pid=0) ________________________ TestFetchAllProcesses.test_all ________________________ psutil/_psbsd.py:604: in wrapper return fun(self, *args, **kwargs) psutil/_psbsd.py:914: in open_files rawlist = cext.proc_open_files(self.pid) E ProcessLookupError: [Errno 3] No such process (originated from sysctl(kinfo_file) (1/2)) During handling of the above exception, another exception occurred: psutil/tests/test_process_all.py:93: in proc_info info[fun_name] = fun() psutil/__init__.py:1199: in open_files return self._proc.open_files() psutil/_psbsd.py:609: in wrapper raise NoSuchProcess(self.pid, self._name) E psutil.NoSuchProcess: process no longer exists (pid=0, name='swapper') During handling of the above exception, another exception occurred: psutil/tests/test_process_all.py:135: in test_all for info in self.iter_proc_info(): psutil/tests/test_process_all.py:130: in iter_proc_info ls.append(proc_info(pid)) psutil/tests/test_process_all.py:95: in proc_info check_exception(exc, proc, name, ppid) psutil/tests/test_process_all.py:65: in check_exception tcase.assertProcessGone(proc) psutil/tests/__init__.py:1069: in assertProcessGone self.assertPidGone(proc.pid) psutil/tests/__init__.py:1057: in assertPidGone with pytest.raises(psutil.NoSuchProcess) as cm: E Failed: DID NOT RAISE ``` --- HISTORY.rst | 3 +++ INSTALL.rst | 4 ++-- psutil/arch/bsd/proc.c | 11 ++++++++++- psutil/arch/openbsd/proc.c | 19 +++++++++++++++---- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c854ef80be..6ed9bd3da6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,6 +25,9 @@ XXXX-XX-XX Python 3.13. (patch by Sam Gross) - 2455_, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and field 40 (blkio_ticks) is missing. +- 2460_, [OpenBSD]: `Process.num_fds()`_ and `Process.open_files()`_ may fail + with `NoSuchProcess`_ for PID 0. Instead, we now return "null" values (0 and + [] respectively). 6.0.0 ====== diff --git a/INSTALL.rst b/INSTALL.rst index a425400fd7..8e06d400ea 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -82,7 +82,7 @@ OpenBSD :: - export PKG_PATH=http://ftp.eu.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ + export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ pkg_add -v python3 gcc pip install psutil @@ -93,7 +93,7 @@ Assuming Python 3.11 (the most recent at the time of writing): :: - export PKG_PATH="http://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" + export PKG_PATH="https://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" pkg_add -v pkgin pkgin install python311-* gcc12-* py311-setuptools-* py311-pip-* python3.11 -m pip install psutil diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 5d353ff35e..959b28ee06 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -442,8 +442,17 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { errno = 0; freep = kinfo_getfile(pid, &cnt); + if (freep == NULL) { -#if !defined(PSUTIL_OPENBSD) +#if defined(PSUTIL_OPENBSD) + if ((pid == 0) && (errno == ESRCH)) { + psutil_debug( + "open_files() returned ESRCH for PID 0; forcing `return []`" + ); + PyErr_Clear(); + return py_retlist; + } +#else psutil_raise_for_pid(pid, "kinfo_getfile()"); #endif goto error; diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 0881ccd555..bf4015be41 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -41,7 +41,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { ret = sysctl((int*)mib, 6, proc, &size, NULL, 0); if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_proc)"); return -1; } // sysctl stores 0 in the size if we can't find the process information. @@ -69,7 +69,7 @@ kinfo_getfile(pid_t pid, int* cnt) { /* get the size of what would be returned */ if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (1/2)"); return NULL; } if ((kf = malloc(len)) == NULL) { @@ -79,7 +79,7 @@ kinfo_getfile(pid_t pid, int* cnt) { mib[5] = (int)(len / sizeof(struct kinfo_file)); if (sysctl(mib, 6, kf, &len, NULL, 0) < 0) { free(kf); - PyErr_SetFromErrno(PyExc_OSError); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (2/2)"); return NULL; } @@ -288,8 +288,19 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { return NULL; freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) + + if (freep == NULL) { +#if defined(PSUTIL_OPENBSD) + if ((pid == 0) && (errno == ESRCH)) { + psutil_debug( + "num_fds() returned ESRCH for PID 0; forcing `return 0`" + ); + PyErr_Clear(); + return Py_BuildValue("i", 0); + } +#endif return NULL; + } free(freep); return Py_BuildValue("i", cnt); From 223938f0409b0d040809333587f6a1027784c9e2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 15 Oct 2024 08:38:29 +0200 Subject: [PATCH 1142/1714] Use `@pytest.mark.skipif` decorator instead of `@unittest.skipIf` (#2459) This is the 4th (and hopefully last) step of #2446. Similarly to #2456, we emulate @pytest.mark.skipif via unittest if pytest is not installed. --- psutil/tests/__init__.py | 8 +- psutil/tests/test_aix.py | 4 +- psutil/tests/test_bsd.py | 44 ++++++----- psutil/tests/test_connections.py | 17 ++--- psutil/tests/test_contracts.py | 29 +++---- psutil/tests/test_linux.py | 126 ++++++++++++++++++------------- psutil/tests/test_memleaks.py | 72 +++++++++--------- psutil/tests/test_misc.py | 32 ++++---- psutil/tests/test_osx.py | 12 +-- psutil/tests/test_posix.py | 23 +++--- psutil/tests/test_process.py | 110 ++++++++++++++------------- psutil/tests/test_sunos.py | 4 +- psutil/tests/test_system.py | 62 ++++++++------- psutil/tests/test_testutils.py | 15 ++-- psutil/tests/test_unicode.py | 28 ++++--- psutil/tests/test_windows.py | 30 ++++---- 16 files changed, 335 insertions(+), 281 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index dd31392c02..b2047ab70e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -975,6 +975,12 @@ def warns(warning, match=None): return unittest.TestCase().assertWarns(warning) class mark: + + @staticmethod + def skipif(condition, reason=""): + """Mimics `@pytest.mark.skipif` decorator.""" + return unittest.skipIf(condition, reason) + class xdist_group: """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" @@ -1150,7 +1156,7 @@ def assertProcessZombie(self, proc): # self.assertEqual(proc.ppid(), os.getpid()) -@unittest.skipIf(PYPY, "unreliable on PYPY") +@pytest.mark.skipif(PYPY, reason="unreliable on PYPY") class TestMemoryLeak(PsutilTestCase): """Test framework class for detecting function memory leaks, typically functions implemented in C which forgot to free() memory diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 5c80d1f8f2..68d467b185 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -9,15 +9,15 @@ """AIX specific tests.""" import re -import unittest import psutil from psutil import AIX from psutil.tests import PsutilTestCase +from psutil.tests import pytest from psutil.tests import sh -@unittest.skipIf(not AIX, "AIX only") +@pytest.mark.skipif(not AIX, reason="AIX only") class AIXSpecificTestCase(PsutilTestCase): def test_virtual_memory(self): out = sh('/usr/bin/svmon -O unit=KB') diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 3af505b608..c836dfd5c4 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -74,7 +74,7 @@ def muse(field): # ===================================================================== -@unittest.skipIf(not BSD, "BSD only") +@pytest.mark.skipif(not BSD, reason="BSD only") class BSDTestCase(PsutilTestCase): """Generic tests common to all BSD variants.""" @@ -86,7 +86,7 @@ def setUpClass(cls): def tearDownClass(cls): terminate(cls.pid) - @unittest.skipIf(NETBSD, "-o lstart doesn't work on NETBSD") + @pytest.mark.skipif(NETBSD, reason="-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() @@ -123,18 +123,22 @@ def df(path): 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") + @pytest.mark.skipif(not which('sysctl'), reason="sysctl cmd not available") def test_cpu_count_logical(self): syst = sysctl("hw.ncpu") assert 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 + @pytest.mark.skipif(not which('sysctl'), reason="sysctl cmd not available") + @pytest.mark.skipif( + NETBSD, reason="skipped on NETBSD" # we check /proc/meminfo + ) def test_virtual_memory_total(self): num = sysctl('hw.physmem') assert num == psutil.virtual_memory().total - @unittest.skipIf(not which('ifconfig'), "ifconfig cmd not available") + @pytest.mark.skipif( + not which('ifconfig'), reason="ifconfig cmd not available" + ) def test_net_if_stats(self): for name, stats in psutil.net_if_stats().items(): try: @@ -152,7 +156,7 @@ def test_net_if_stats(self): # ===================================================================== -@unittest.skipIf(not FREEBSD, "FREEBSD only") +@pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") class FreeBSDPsutilTestCase(PsutilTestCase): @classmethod def setUpClass(cls): @@ -241,7 +245,7 @@ def test_cpu_times(self): raise RuntimeError("couldn't find lines match in procstat out") -@unittest.skipIf(not FREEBSD, "FREEBSD only") +@pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") class FreeBSDSystemTestCase(PsutilTestCase): @staticmethod def parse_swapinfo(): @@ -310,42 +314,42 @@ def test_vmem_buffers(self): # --- virtual_memory(); tests against muse - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") def test_muse_vmem_total(self): num = muse('Total') assert psutil.virtual_memory().total == num - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") @retry_on_failure() def test_muse_vmem_active(self): num = muse('Active') assert abs(psutil.virtual_memory().active - num) < TOLERANCE_SYS_MEM - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") @retry_on_failure() def test_muse_vmem_inactive(self): num = muse('Inactive') assert abs(psutil.virtual_memory().inactive - num) < TOLERANCE_SYS_MEM - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") @retry_on_failure() def test_muse_vmem_wired(self): num = muse('Wired') assert abs(psutil.virtual_memory().wired - num) < TOLERANCE_SYS_MEM - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") @retry_on_failure() def test_muse_vmem_cached(self): num = muse('Cache') assert abs(psutil.virtual_memory().cached - num) < TOLERANCE_SYS_MEM - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") @retry_on_failure() def test_muse_vmem_free(self): num = muse('Free') assert abs(psutil.virtual_memory().free - num) < TOLERANCE_SYS_MEM - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") @retry_on_failure() def test_muse_vmem_buffers(self): num = muse('Buffer') @@ -412,7 +416,7 @@ def test_boot_time(self): # --- sensors_battery - @unittest.skipIf(not HAS_BATTERY, "no battery") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_sensors_battery(self): def secs2hours(secs): m, _s = divmod(secs, 60) @@ -432,7 +436,7 @@ def secs2hours(secs): else: assert secs2hours(metrics.secsleft) == remaining_time - @unittest.skipIf(not HAS_BATTERY, "no battery") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_sensors_battery_against_sysctl(self): assert psutil.sensors_battery().percent == sysctl( "hw.acpi.battery.life" @@ -446,7 +450,7 @@ def test_sensors_battery_against_sysctl(self): else: assert secsleft == sysctl("hw.acpi.battery.time") * 60 - @unittest.skipIf(HAS_BATTERY, "has battery") + @pytest.mark.skipif(HAS_BATTERY, reason="has battery") def test_sensors_battery_no_battery(self): # If no battery is present one of these calls is supposed # to fail, see: @@ -489,7 +493,7 @@ def test_sensors_temperatures_against_sysctl(self): # ===================================================================== -@unittest.skipIf(not OPENBSD, "OPENBSD only") +@pytest.mark.skipif(not OPENBSD, reason="OPENBSD only") class OpenBSDTestCase(PsutilTestCase): def test_boot_time(self): s = sysctl('kern.boottime') @@ -503,7 +507,7 @@ def test_boot_time(self): # ===================================================================== -@unittest.skipIf(not NETBSD, "NETBSD only") +@pytest.mark.skipif(not NETBSD, reason="NETBSD only") class NetBSDTestCase(PsutilTestCase): @staticmethod def parse_meminfo(look_for): diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 7f29900b27..c9256a17f8 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -9,7 +9,6 @@ import os import socket import textwrap -import unittest from contextlib import closing from socket import AF_INET from socket import AF_INET6 @@ -86,7 +85,7 @@ def compare_procsys_connections(self, pid, proc_cons, kind='all'): class TestBasicOperations(ConnectionTestCase): - @unittest.skipIf(SKIP_SYSCONS, "requires root") + @pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") def test_system(self): with create_sockets(): for conn in psutil.net_connections(kind='all'): @@ -158,7 +157,7 @@ def test_tcp_v4(self): assert conn.raddr == () assert conn.status == psutil.CONN_LISTEN - @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") + @pytest.mark.skipif(not supports_ipv6(), reason="IPv6 not supported") def test_tcp_v6(self): addr = ("::1", 0) with closing(bind_socket(AF_INET6, SOCK_STREAM, addr=addr)) as sock: @@ -173,7 +172,7 @@ def test_udp_v4(self): assert conn.raddr == () assert conn.status == psutil.CONN_NONE - @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") + @pytest.mark.skipif(not supports_ipv6(), reason="IPv6 not supported") def test_udp_v6(self): addr = ("::1", 0) with closing(bind_socket(AF_INET6, SOCK_DGRAM, addr=addr)) as sock: @@ -181,7 +180,7 @@ def test_udp_v6(self): assert conn.raddr == () assert conn.status == psutil.CONN_NONE - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_unix_tcp(self): testfn = self.get_testfn() with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock: @@ -189,7 +188,7 @@ def test_unix_tcp(self): assert conn.raddr == "" # noqa assert conn.status == psutil.CONN_NONE - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_unix_udp(self): testfn = self.get_testfn() with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock: @@ -206,7 +205,7 @@ class TestConnectedSocket(ConnectionTestCase): # On SunOS, even after we close() it, the server socket stays around # in TIME_WAIT state. - @unittest.skipIf(SUNOS, "unreliable on SUONS") + @pytest.mark.skipif(SUNOS, reason="unreliable on SUONS") def test_tcp(self): addr = ("127.0.0.1", 0) assert this_proc_net_connections(kind='tcp4') == [] @@ -226,7 +225,7 @@ def test_tcp(self): server.close() client.close() - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_unix(self): testfn = self.get_testfn() server, client = unix_socketpair(testfn) @@ -484,7 +483,7 @@ def test_count(self): assert conn.type in (SOCK_STREAM, SOCK_DGRAM) -@unittest.skipIf(SKIP_SYSCONS, "requires root") +@pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") class TestSystemWideConnections(ConnectionTestCase): """Tests for net_connections().""" diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 5b975de605..2f58cd1725 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -37,6 +37,7 @@ from psutil.tests import enum from psutil.tests import is_namedtuple from psutil.tests import kernel_version +from psutil.tests import pytest # =================================================================== @@ -74,8 +75,9 @@ def test_linux_ioprio_windows(self): ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) - @unittest.skipIf( - GITHUB_ACTIONS and LINUX, "unsupported on GITHUB_ACTIONS + LINUX" + @pytest.mark.skipif( + GITHUB_ACTIONS and LINUX, + reason="unsupported on GITHUB_ACTIONS + LINUX", ) def test_rlimit(self): ae = self.assertEqual @@ -158,8 +160,9 @@ def test_terminal(self): def test_ionice(self): assert hasattr(psutil.Process, "ionice") == (LINUX or WINDOWS) - @unittest.skipIf( - GITHUB_ACTIONS and LINUX, "unsupported on GITHUB_ACTIONS + LINUX" + @pytest.mark.skipif( + GITHUB_ACTIONS and LINUX, + reason="unsupported on GITHUB_ACTIONS + LINUX", ) def test_rlimit(self): assert hasattr(psutil.Process, "rlimit") == (LINUX or FREEBSD) @@ -228,10 +231,10 @@ def test_cpu_count(self): assert isinstance(psutil.cpu_count(), int) # TODO: remove this once 1892 is fixed - @unittest.skipIf( - MACOS and platform.machine() == 'arm64', "skipped due to #1892" + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" ) - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): if psutil.cpu_freq() is None: raise unittest.SkipTest("cpu_freq() returns None") @@ -251,7 +254,7 @@ def test_disk_partitions(self): assert isinstance(disk.fstype, str) assert isinstance(disk.opts, str) - @unittest.skipIf(SKIP_SYSCONS, "requires root") + @pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") def test_net_connections(self): with create_sockets(): ret = psutil.net_connections('all') @@ -272,7 +275,7 @@ def test_net_if_addrs(self): assert isinstance(addr.netmask, (str, type(None))) assert isinstance(addr.broadcast, (str, type(None))) - @unittest.skipIf(QEMU_USER, 'QEMU user not supported') + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_stats(self): # Duplicate of test_system.py. Keep it anyway. for ifname, info in psutil.net_if_stats().items(): @@ -285,13 +288,13 @@ def test_net_if_stats(self): assert isinstance(info.speed, int) assert isinstance(info.mtu, int) - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="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): assert isinstance(ifname, str) - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") def test_sensors_fans(self): # Duplicate of test_system.py. Keep it anyway. for name, units in psutil.sensors_fans().items(): @@ -300,7 +303,7 @@ def test_sensors_fans(self): assert isinstance(unit.label, str) assert isinstance(unit.current, (float, int, type(None))) - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") def test_sensors_temperatures(self): # Duplicate of test_system.py. Keep it anyway. for name, units in psutil.sensors_temperatures().items(): @@ -325,7 +328,7 @@ def test_users(self): class TestProcessWaitType(PsutilTestCase): - @unittest.skipIf(not POSIX, "not POSIX") + @pytest.mark.skipif(not POSIX, reason="not POSIX") def test_negative_signal(self): p = psutil.Process(self.spawn_testproc().pid) p.terminate() diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 788fe9914b..72f9378909 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -268,7 +268,7 @@ def open_mock(name, *args, **kwargs): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemVirtualMemoryAgainstFree(PsutilTestCase): def test_total(self): cli_value = free_physmem().total @@ -326,7 +326,7 @@ def test_available(self): ), '%s %s \n%s' % (free_value, psutil_value, out) -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemVirtualMemoryAgainstVmstat(PsutilTestCase): def test_total(self): vmstat_value = vmstat('total memory') * 1024 @@ -376,7 +376,7 @@ def test_inactive(self): assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemVirtualMemoryMocks(PsutilTestCase): def test_warnings_on_misses(self): # Emulate a case where /proc/meminfo provides few info. @@ -586,7 +586,7 @@ def test_virtual_memory_mocked(self): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemSwapMemory(PsutilTestCase): @staticmethod def meminfo_has_swap_info(): @@ -677,7 +677,7 @@ def test_emulate_meminfo_has_no_metrics(self): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUTimes(PsutilTestCase): def test_fields(self): fields = psutil.cpu_times()._fields @@ -697,11 +697,11 @@ def test_fields(self): assert 'guest_nice' not in fields -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUCountLogical(PsutilTestCase): - @unittest.skipIf( + @pytest.mark.skipif( not os.path.exists("/sys/devices/system/cpu/online"), - "/sys/devices/system/cpu/online does not exist", + reason="/sys/devices/system/cpu/online does not exist", ) def test_against_sysdev_cpu_online(self): with open("/sys/devices/system/cpu/online") as f: @@ -710,21 +710,25 @@ def test_against_sysdev_cpu_online(self): value = int(value.split('-')[1]) + 1 assert psutil.cpu_count() == value - @unittest.skipIf( + @pytest.mark.skipif( not os.path.exists("/sys/devices/system/cpu"), - "/sys/devices/system/cpu does not exist", + reason="/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]) assert psutil.cpu_count() == count - @unittest.skipIf(not which("nproc"), "nproc utility not available") + @pytest.mark.skipif( + not which("nproc"), reason="nproc utility not available" + ) def test_against_nproc(self): num = int(sh("nproc --all")) assert psutil.cpu_count(logical=True) == num - @unittest.skipIf(not which("lscpu"), "lscpu utility not available") + @pytest.mark.skipif( + not which("lscpu"), reason="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('#')]) @@ -767,9 +771,11 @@ def test_emulate_fallbacks(self): assert m.called -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUCountCores(PsutilTestCase): - @unittest.skipIf(not which("lscpu"), "lscpu utility not available") + @pytest.mark.skipif( + not which("lscpu"), reason="lscpu utility not available" + ) def test_against_lscpu(self): out = sh("lscpu -p") core_ids = set() @@ -795,9 +801,9 @@ def test_emulate_none(self): assert m2.called -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUFrequency(PsutilTestCase): - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_emulate_use_second_file(self): # https://github.com/giampaolo/psutil/issues/981 def path_exists_mock(path): @@ -812,8 +818,10 @@ def path_exists_mock(path): ): assert psutil.cpu_freq() - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - @unittest.skipIf(AARCH64, "aarch64 does not report mhz in /proc/cpuinfo") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + @pytest.mark.skipif( + AARCH64, reason="aarch64 does not report mhz in /proc/cpuinfo" + ) def test_emulate_use_cpuinfo(self): # Emulate a case where /sys/devices/system/cpu/cpufreq* does not # exist and /proc/cpuinfo is used instead. @@ -838,7 +846,7 @@ def path_exists_mock(path): reload_module(psutil._pslinux) reload_module(psutil) - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_emulate_data(self): def open_mock(name, *args, **kwargs): if name.endswith('/scaling_cur_freq') and name.startswith( @@ -871,7 +879,7 @@ def open_mock(name, *args, **kwargs): if freq.max != 0.0: assert freq.max == 700.0 - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_emulate_multi_cpu(self): def open_mock(name, *args, **kwargs): n = name @@ -923,7 +931,7 @@ def open_mock(name, *args, **kwargs): if freq[1].max != 0.0: assert freq[1].max == 600.0 - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="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): @@ -947,7 +955,7 @@ def open_mock(name, *args, **kwargs): assert freq.current == 200 -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUStats(PsutilTestCase): # XXX: fails too often. @@ -962,9 +970,9 @@ def test_interrupts(self): assert abs(vmstat_value - psutil_value) < 500 -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestLoadAvg(PsutilTestCase): - @unittest.skipIf(not HAS_GETLOADAVG, "not supported") + @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") def test_getloadavg(self): psutil_value = psutil.getloadavg() with open("/proc/loadavg") as f: @@ -980,7 +988,7 @@ def test_getloadavg(self): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemNetIfAddrs(PsutilTestCase): def test_ips(self): for name, addrs in psutil.net_if_addrs().items(): @@ -1005,7 +1013,7 @@ def test_ips(self): assert address in get_ipv6_addresses(name) # XXX - not reliable when having virtual NICs installed by Docker. - # @unittest.skipIf(not which('ip'), "'ip' utility not available") + # @pytest.mark.skipif(not which('ip'), reason="'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] @@ -1020,10 +1028,12 @@ def test_ips(self): # pprint.pformat(nics), out)) -@unittest.skipIf(not LINUX, "LINUX only") -@unittest.skipIf(QEMU_USER, "QEMU user not supported") +@pytest.mark.skipif(not LINUX, reason="LINUX only") +@pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") class TestSystemNetIfStats(PsutilTestCase): - @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") + @pytest.mark.skipif( + not which("ifconfig"), reason="ifconfig utility not available" + ) def test_against_ifconfig(self): for name, stats in psutil.net_if_stats().items(): try: @@ -1041,7 +1051,9 @@ def test_mtu(self): with open("/sys/class/net/%s/mtu" % name) as f: assert stats.mtu == int(f.read().strip()) - @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") + @pytest.mark.skipif( + not which("ifconfig"), reason="ifconfig utility not available" + ) def test_flags(self): # first line looks like this: # "eth0: flags=4163 mtu 1500" @@ -1072,9 +1084,11 @@ def test_flags(self): raise self.fail("no matches were found") -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemNetIOCounters(PsutilTestCase): - @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") + @pytest.mark.skipif( + not which("ifconfig"), reason="ifconfig utility not available" + ) @retry_on_failure() def test_against_ifconfig(self): def ifconfig(nic): @@ -1122,7 +1136,7 @@ def ifconfig(nic): assert abs(stats.dropout - ifconfig_ret['dropout']) < 10 -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemNetConnections(PsutilTestCase): @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) @@ -1153,9 +1167,11 @@ def test_emulate_unix(self): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemDiskPartitions(PsutilTestCase): - @unittest.skipIf(not hasattr(os, 'statvfs'), "os.statvfs() not available") + @pytest.mark.skipif( + not hasattr(os, 'statvfs'), reason="os.statvfs() not available" + ) @skip_on_not_implemented() def test_against_df(self): # test psutil.disk_usage() and psutil.disk_partitions() @@ -1217,7 +1233,7 @@ def test_emulate_realpath_fail(self): psutil.PROCFS_PATH = "/proc" -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemDiskIoCounters(PsutilTestCase): def test_emulate_kernel_2_4(self): # Tests /proc/diskstats parsing format for 2.4 kernels, see: @@ -1353,7 +1369,7 @@ def exists(path): psutil.disk_io_counters() -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestRootFsDeviceFinder(PsutilTestCase): def setUp(self): dev = os.stat("/").st_dev @@ -1376,7 +1392,7 @@ def test_call_methods(self): finder.ask_sys_dev_block() finder.ask_sys_class_block() - @unittest.skipIf(GITHUB_ACTIONS, "unsupported on GITHUB_ACTIONS") + @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") def test_comparisons(self): finder = RootFsDeviceFinder() assert finder.find() is not None @@ -1398,8 +1414,10 @@ def test_comparisons(self): if base and c: assert base == c - @unittest.skipIf(not which("findmnt"), "findmnt utility not available") - @unittest.skipIf(GITHUB_ACTIONS, "unsupported on GITHUB_ACTIONS") + @pytest.mark.skipif( + not which("findmnt"), reason="findmnt utility not available" + ) + @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") def test_against_findmnt(self): psutil_value = RootFsDeviceFinder().find() findmnt_value = sh("findmnt -o SOURCE -rn /") @@ -1424,7 +1442,7 @@ def test_disk_partitions_mocked(self): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestMisc(PsutilTestCase): def test_boot_time(self): vmstat_value = vmstat('boot time') @@ -1577,7 +1595,7 @@ def test_procfs_path(self): psutil.PROCFS_PATH = "/proc" @retry_on_failure() - @unittest.skipIf(PYTEST_PARALLEL, "skip if pytest-parallel") + @pytest.mark.skipif(PYTEST_PARALLEL, reason="skip if pytest-parallel") def test_issue_687(self): # In case of thread ID: # - pid_exists() is supposed to return False @@ -1608,10 +1626,10 @@ def test_pid_exists_no_proc_status(self): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") -@unittest.skipIf(not HAS_BATTERY, "no battery") +@pytest.mark.skipif(not LINUX, reason="LINUX only") +@pytest.mark.skipif(not HAS_BATTERY, reason="no battery") class TestSensorsBattery(PsutilTestCase): - @unittest.skipIf(not which("acpi"), "acpi utility not available") + @pytest.mark.skipif(not which("acpi"), reason="acpi utility not available") def test_percent(self): out = sh("acpi -b") acpi_value = int(out.split(",")[1].strip().replace('%', '')) @@ -1743,7 +1761,7 @@ def test_emulate_no_power(self): assert psutil.sensors_battery().power_plugged is None -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSensorsBatteryEmulated(PsutilTestCase): def test_it(self): def open_mock(name, *args, **kwargs): @@ -1765,7 +1783,7 @@ def open_mock(name, *args, **kwargs): assert mopen.called -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSensorsTemperatures(PsutilTestCase): def test_emulate_class_hwmon(self): def open_mock(name, *args, **kwargs): @@ -1833,7 +1851,7 @@ def glob_mock(path): assert temp.critical == 50.0 -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSensorsFans(PsutilTestCase): def test_emulate_data(self): def open_mock(name, *args, **kwargs): @@ -1862,7 +1880,7 @@ def open_mock(name, *args, **kwargs): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestProcess(PsutilTestCase): @retry_on_failure() def test_parse_smaps_vs_memory_maps(self): @@ -1910,7 +1928,7 @@ def test_parse_smaps_mocked(self): assert swap == 15 * 1024 # On PYPY file descriptors are not closed fast enough. - @unittest.skipIf(PYPY, "unreliable on PYPY") + @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") def test_open_files_mode(self): def get_test_file(fname): p = psutil.Process() @@ -2121,7 +2139,7 @@ def test_issue_1014(self): p.memory_maps() assert m.called - @unittest.skipIf(not HAS_RLIMIT, "not supported") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") def test_rlimit_zombie(self): # Emulate a case where rlimit() raises ENOSYS, which may # happen in case of zombie process: @@ -2239,7 +2257,7 @@ def test_net_connections_enametoolong(self): assert m.called -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="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). @@ -2270,7 +2288,7 @@ def test_name(self): value = self.read_status_file("Name:") assert self.proc.name() == value - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_status(self): value = self.read_status_file("State:") value = value[value.find('(') + 1 : value.rfind(')')] @@ -2323,7 +2341,7 @@ def test_cpu_affinity_eligible_cpus(self): # ===================================================================== -@unittest.skipIf(not LINUX, "LINUX only") +@pytest.mark.skipif(not LINUX, reason="LINUX only") class TestUtils(PsutilTestCase): def test_readlink(self): with mock.patch("os.readlink", return_value="foo (deleted)") as m: diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 8ef108e1a9..e249ca5169 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -19,7 +19,6 @@ import functools import os import platform -import unittest import psutil import psutil._common @@ -48,6 +47,7 @@ from psutil.tests import create_sockets from psutil.tests import get_testfn from psutil.tests import process_namespace +from psutil.tests import pytest from psutil.tests import skip_on_access_denied from psutil.tests import spawn_testproc from psutil.tests import system_namespace @@ -112,12 +112,12 @@ def test_exe(self): def test_ppid(self): self.execute(self.proc.ppid) - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") @fewtimes_if_linux() def test_uids(self): self.execute(self.proc.uids) - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") @fewtimes_if_linux() def test_gids(self): self.execute(self.proc.gids) @@ -133,11 +133,11 @@ def test_nice_set(self): niceness = thisproc.nice() self.execute(lambda: self.proc.nice(niceness)) - @unittest.skipIf(not HAS_IONICE, "not supported") + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") def test_ionice(self): self.execute(self.proc.ionice) - @unittest.skipIf(not HAS_IONICE, "not supported") + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") def test_ionice_set(self): if WINDOWS: value = thisproc.ionice() @@ -147,12 +147,12 @@ def test_ionice_set(self): 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") + @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") @fewtimes_if_linux() def test_io_counters(self): self.execute(self.proc.io_counters) - @unittest.skipIf(POSIX, "worthless on POSIX") + @pytest.mark.skipif(POSIX, reason="worthless on POSIX") def test_username(self): # always open 1 handle on Windows (only once) psutil.Process().username() @@ -167,11 +167,11 @@ def test_create_time(self): def test_num_threads(self): self.execute(self.proc.num_threads) - @unittest.skipIf(not WINDOWS, "WINDOWS only") + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") def test_num_handles(self): self.execute(self.proc.num_handles) - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") @fewtimes_if_linux() def test_num_fds(self): self.execute(self.proc.num_fds) @@ -190,7 +190,7 @@ def test_cpu_times(self): self.execute(self.proc.cpu_times) @fewtimes_if_linux() - @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") + @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") def test_cpu_num(self): self.execute(self.proc.cpu_num) @@ -202,7 +202,7 @@ def test_memory_info(self): def test_memory_full_info(self): self.execute(self.proc.memory_full_info) - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") @fewtimes_if_linux() def test_terminal(self): self.execute(self.proc.terminal) @@ -215,11 +215,11 @@ def test_resume(self): def test_cwd(self): self.execute(self.proc.cwd) - @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") def test_cpu_affinity(self): self.execute(self.proc.cpu_affinity) - @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_set(self): affinity = thisproc.cpu_affinity() self.execute(lambda: self.proc.cpu_affinity(affinity)) @@ -230,18 +230,18 @@ def test_open_files(self): with open(get_testfn(), 'w'): self.execute(self.proc.open_files) - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="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") + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @pytest.mark.skipif(not HAS_RLIMIT, reason="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") + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") def test_rlimit_set(self): limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) @@ -250,7 +250,7 @@ def test_rlimit_set(self): @fewtimes_if_linux() # Windows implementation is based on a single system-wide # function (tested later). - @unittest.skipIf(WINDOWS, "worthless on WINDOWS") + @pytest.mark.skipif(WINDOWS, reason="worthless on WINDOWS") def test_net_connections(self): # TODO: UNIX sockets are temporarily implemented by parsing # 'pfiles' cmd output; we don't want that part of the code to @@ -259,11 +259,11 @@ def test_net_connections(self): kind = 'inet' if SUNOS else 'all' self.execute(lambda: self.proc.net_connections(kind)) - @unittest.skipIf(not HAS_ENVIRON, "not supported") + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") def test_environ(self): self.execute(self.proc.environ) - @unittest.skipIf(not WINDOWS, "WINDOWS only") + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") def test_proc_info(self): self.execute(lambda: cext.proc_info(os.getpid())) @@ -322,7 +322,7 @@ def call(): self.execute(call) -@unittest.skipIf(not WINDOWS, "WINDOWS only") +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") class TestProcessDualImplementation(TestMemoryLeak): def test_cmdline_peb_true(self): self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True)) @@ -367,14 +367,14 @@ def test_cpu_stats(self): @fewtimes_if_linux() # TODO: remove this once 1892 is fixed - @unittest.skipIf( - MACOS and platform.machine() == 'arm64', "skipped due to #1892" + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" ) - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): self.execute(psutil.cpu_freq) - @unittest.skipIf(not WINDOWS, "WINDOWS only") + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") def test_getloadavg(self): psutil.getloadavg() self.execute(psutil.getloadavg) @@ -385,7 +385,7 @@ 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)") + @pytest.mark.skipif(SUNOS, reason="worthless on SUNOS (uses a subprocess)") def test_swap_memory(self): self.execute(psutil.swap_memory) @@ -399,13 +399,13 @@ def test_disk_usage(self): times = FEW_TIMES if POSIX else self.times self.execute(lambda: psutil.disk_usage('.'), times=times) - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_disk_partitions(self): self.execute(psutil.disk_partitions) - @unittest.skipIf( + @pytest.mark.skipif( LINUX and not os.path.exists('/proc/diskstats'), - '/proc/diskstats not available on this Linux version', + reason="/proc/diskstats not available on this Linux version", ) @fewtimes_if_linux() def test_disk_io_counters(self): @@ -420,12 +420,12 @@ def test_pids(self): # --- net @fewtimes_if_linux() - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="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") + @pytest.mark.skipif(MACOS and os.getuid() != 0, reason="need root access") def test_net_connections(self): # always opens and handle on Windows() (once) psutil.net_connections(kind='all') @@ -437,24 +437,24 @@ def test_net_if_addrs(self): tolerance = 80 * 1024 if WINDOWS else self.tolerance self.execute(psutil.net_if_addrs, tolerance=tolerance) - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_stats(self): self.execute(psutil.net_if_stats) # --- sensors @fewtimes_if_linux() - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") def test_sensors_battery(self): self.execute(psutil.sensors_battery) @fewtimes_if_linux() - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") def test_sensors_temperatures(self): self.execute(psutil.sensors_temperatures) @fewtimes_if_linux() - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") def test_sensors_fans(self): self.execute(psutil.sensors_fans) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 8ddaec28ad..3a4275c80a 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -340,7 +340,9 @@ def check(ret): assert b.name == 'name' # # XXX: https://github.com/pypa/setuptools/pull/2896 - # @unittest.skipIf(APPVEYOR, "temporarily disabled due to setuptools bug") + # @pytest.mark.skipif(APPVEYOR, + # reason="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): @@ -886,7 +888,7 @@ def test_cache_clear(self): wrap_numbers.cache_clear('disk_io') wrap_numbers.cache_clear('?!?') - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") def test_cache_clear_public_apis(self): if not psutil.disk_io_counters() or not psutil.net_io_counters(): raise unittest.SkipTest("no disks or NICs available") @@ -913,8 +915,8 @@ def test_cache_clear_public_apis(self): # =================================================================== -@unittest.skipIf( - not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory" +@pytest.mark.skipif( + not os.path.exists(SCRIPTS_DIR), reason="can't locate scripts directory" ) class TestScripts(PsutilTestCase): """Tests for scripts in the "scripts" directory.""" @@ -955,7 +957,7 @@ def test_coverage(self): % os.path.join(SCRIPTS_DIR, name) ) - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_executable(self): for root, dirs, files in os.walk(SCRIPTS_DIR): for file in files: @@ -976,7 +978,7 @@ def test_meminfo(self): def test_procinfo(self): self.assert_stdout('procinfo.py', str(os.getpid())) - @unittest.skipIf(CI_TESTING and not psutil.users(), "no users") + @pytest.mark.skipif(CI_TESTING and not psutil.users(), reason="no users") def test_who(self): self.assert_stdout('who.py') @@ -989,11 +991,11 @@ def test_pstree(self): def test_netstat(self): self.assert_stdout('netstat.py') - @unittest.skipIf(QEMU_USER, 'QEMU user not supported') + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_ifconfig(self): self.assert_stdout('ifconfig.py') - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") def test_pmap(self): self.assert_stdout('pmap.py', str(os.getpid())) @@ -1018,31 +1020,31 @@ def test_pidof(self): output = self.assert_stdout('pidof.py', psutil.Process().name()) assert str(os.getpid()) in output - @unittest.skipIf(not WINDOWS, "WINDOWS only") + @pytest.mark.skipif(not WINDOWS, reason="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") + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") def test_temperatures(self): if not psutil.sensors_temperatures(): raise unittest.SkipTest("no temperatures") self.assert_stdout('temperatures.py') - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") def test_fans(self): if not psutil.sensors_fans(): raise unittest.SkipTest("no fans") self.assert_stdout('fans.py') - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - @unittest.skipIf(not HAS_BATTERY, "no battery") + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="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") + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_sensors(self): self.assert_stdout('sensors.py') diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index d72bc76f44..a70cdf6415 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -9,7 +9,6 @@ import platform import re import time -import unittest import psutil from psutil import MACOS @@ -18,6 +17,7 @@ from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase +from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import spawn_testproc @@ -51,7 +51,7 @@ def vm_stat(field): return int(re.search(r'\d+', line).group(0)) * getpagesize() -@unittest.skipIf(not MACOS, "MACOS only") +@pytest.mark.skipif(not MACOS, reason="MACOS only") class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): @@ -73,7 +73,7 @@ def test_process_create_time(self): assert year == time.strftime("%Y", time.localtime(start_psutil)) -@unittest.skipIf(not MACOS, "MACOS only") +@pytest.mark.skipif(not MACOS, reason="MACOS only") class TestSystemAPIs(PsutilTestCase): # --- disk @@ -114,8 +114,8 @@ def test_cpu_count_cores(self): assert num == psutil.cpu_count(logical=False) # TODO: remove this once 1892 is fixed - @unittest.skipIf( - MACOS and platform.machine() == 'arm64', "skipped due to #1892" + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" ) def test_cpu_freq(self): freq = psutil.cpu_freq() @@ -181,7 +181,7 @@ def test_net_if_stats(self): # --- sensors_battery - @unittest.skipIf(not HAS_BATTERY, "no battery") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_sensors_battery(self): out = sh("pmset -g batt") percent = re.search(r"(\d+)%", out).group(1) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 5c2c86c9ea..ba430154d5 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -152,7 +152,7 @@ def df(device): return (sys_total, sys_used, sys_free, sys_percent) -@unittest.skipIf(not POSIX, "POSIX only") +@pytest.mark.skipif(not POSIX, reason="POSIX only") class TestProcess(PsutilTestCase): """Compare psutil results against 'ps' command line utility (mainly).""" @@ -268,7 +268,7 @@ def test_name_long_cmdline_nsp_exc(self): with pytest.raises(psutil.NoSuchProcess): p.name() - @unittest.skipIf(MACOS or BSD, 'ps -o start not available') + @pytest.mark.skipif(MACOS or BSD, reason="ps -o start not available") def test_create_time(self): time_ps = ps('start', self.pid) time_psutil = psutil.Process(self.pid).create_time() @@ -317,15 +317,15 @@ def test_cmdline(self): # 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") + @pytest.mark.skipif(SUNOS, reason="not reliable on SUNOS") + @pytest.mark.skipif(AIX, reason="not reliable on AIX") def test_nice(self): ps_nice = ps('nice', self.pid) psutil_nice = psutil.Process().nice() assert ps_nice == psutil_nice -@unittest.skipIf(not POSIX, "POSIX only") +@pytest.mark.skipif(not POSIX, reason="POSIX only") class TestSystemAPIs(PsutilTestCase): """Test some system APIs.""" @@ -349,9 +349,9 @@ def test_pids(self): # 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") + @pytest.mark.skipif(SUNOS, reason="unreliable on SUNOS") + @pytest.mark.skipif(not which('ifconfig'), reason="no ifconfig cmd") + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") def test_nic_names(self): output = sh("ifconfig -a") for nic in psutil.net_io_counters(pernic=True): @@ -364,7 +364,8 @@ def test_nic_names(self): % (nic, output) ) - # @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") + # @pytest.mark.skipif(CI_TESTING and not psutil.users(), + # reason="unreliable on CI") @retry_on_failure() def test_users(self): out = sh("who -u") @@ -460,7 +461,7 @@ def test_os_waitpid_bad_ret_status(self): assert m.called # AIX can return '-' in df output instead of numbers, e.g. for /proc - @unittest.skipIf(AIX, "unreliable on AIX") + @pytest.mark.skipif(AIX, reason="unreliable on AIX") @retry_on_failure() def test_disk_usage(self): tolerance = 4 * 1024 * 1024 # 4MB @@ -487,7 +488,7 @@ def test_disk_usage(self): assert abs(usage.percent - sys_percent) <= 1 -@unittest.skipIf(not POSIX, "POSIX only") +@pytest.mark.skipif(not POSIX, reason="POSIX only") class TestMisc(PsutilTestCase): def test_getpagesize(self): pagesize = getpagesize() diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b3e6569eed..a0e5199ec0 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -127,7 +127,7 @@ def test_send_signal(self): assert code == -sig self.assertProcessGone(p) - @unittest.skipIf(not POSIX, "not POSIX") + @pytest.mark.skipif(not POSIX, reason="not POSIX") def test_send_signal_mocked(self): sig = signal.SIGTERM p = self.spawn_psproc() @@ -171,7 +171,7 @@ def test_wait_exited(self): assert code == 5 self.assertProcessGone(p) - @unittest.skipIf(NETBSD, "fails on NETBSD") + @pytest.mark.skipif(NETBSD, reason="fails on NETBSD") def test_wait_stopped(self): p = self.spawn_psproc() if POSIX: @@ -267,7 +267,7 @@ def test_cpu_percent_numcpus_none(self): psutil.Process().cpu_percent() assert m.called - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_cpu_times(self): times = psutil.Process().cpu_times() assert times.user >= 0.0, times @@ -280,7 +280,7 @@ def test_cpu_times(self): for name in times._fields: time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_cpu_times_2(self): user_time, kernel_time = psutil.Process().cpu_times()[:2] utime, ktime = os.times()[:2] @@ -294,7 +294,7 @@ def test_cpu_times_2(self): 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") + @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") def test_cpu_num(self): p = psutil.Process() num = p.cpu_num() @@ -321,7 +321,7 @@ def test_create_time(self): # 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') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_terminal(self): terminal = psutil.Process().terminal() if terminal is not None: @@ -333,7 +333,7 @@ def test_terminal(self): else: assert terminal == tty - @unittest.skipIf(not HAS_PROC_IO_COUNTERS, 'not supported') + @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") @skip_on_not_implemented(only_if=LINUX) def test_io_counters(self): p = psutil.Process() @@ -376,8 +376,8 @@ def test_io_counters(self): assert io2[i] >= 0 assert io2[i] >= 0 - @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(not LINUX, "linux only") + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not LINUX, reason="linux only") def test_ionice_linux(self): def cleanup(init): ioclass, value = init @@ -421,8 +421,10 @@ def cleanup(init): ): p.ionice(value=1) - @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(not WINDOWS, 'not supported on this win version') + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif( + not WINDOWS, reason="not supported on this win version" + ) def test_ionice_win(self): p = psutil.Process() if not CI_TESTING: @@ -449,7 +451,7 @@ def test_ionice_win(self): with pytest.raises(ValueError, match="is not a valid priority"): p.ionice(psutil.IOPRIO_HIGH + 1) - @unittest.skipIf(not HAS_RLIMIT, "not supported") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") def test_rlimit_get(self): import resource @@ -473,7 +475,7 @@ def test_rlimit_get(self): assert ret[0] >= -1 assert ret[1] >= -1 - @unittest.skipIf(not HAS_RLIMIT, "not supported") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") def test_rlimit_set(self): p = self.spawn_psproc() p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) @@ -486,7 +488,7 @@ def test_rlimit_set(self): with pytest.raises(ValueError): p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) - @unittest.skipIf(not HAS_RLIMIT, "not supported") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") def test_rlimit(self): p = psutil.Process() testfn = self.get_testfn() @@ -505,7 +507,7 @@ def test_rlimit(self): p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) - @unittest.skipIf(not HAS_RLIMIT, "not supported") + @pytest.mark.skipif(not HAS_RLIMIT, reason="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. @@ -520,7 +522,7 @@ def test_rlimit_infinity(self): p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) - @unittest.skipIf(not HAS_RLIMIT, "not supported") + @pytest.mark.skipif(not HAS_RLIMIT, reason="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 @@ -549,13 +551,13 @@ def test_num_threads(self): step2 = p.num_threads() assert step2 == step1 + 1 - @unittest.skipIf(not WINDOWS, 'WINDOWS only') + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") def test_num_handles(self): # a better test is done later into test/_windows.py p = psutil.Process() assert p.num_handles() > 0 - @unittest.skipIf(not HAS_THREADS, 'not supported') + @pytest.mark.skipif(not HAS_THREADS, reason="not supported") def test_threads(self): p = psutil.Process() if OPENBSD: @@ -577,7 +579,7 @@ def test_threads(self): @retry_on_failure() @skip_on_access_denied(only_if=MACOS) - @unittest.skipIf(not HAS_THREADS, 'not supported') + @pytest.mark.skipif(not HAS_THREADS, reason="not supported") def test_threads_2(self): p = self.spawn_psproc() if OPENBSD: @@ -644,7 +646,7 @@ def test_memory_full_info(self): assert mem.pss >= 0 assert mem.swap >= 0 - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") def test_memory_maps(self): p = psutil.Process() maps = p.memory_maps() @@ -692,7 +694,7 @@ def test_memory_maps(self): assert isinstance(value, (int, long)) assert value >= 0, value - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") def test_memory_maps_lists_lib(self): # Make sure a newly loaded shared lib is listed. p = psutil.Process() @@ -721,7 +723,7 @@ def test_is_running(self): assert not p.is_running() assert not p.is_running() - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_exe(self): p = self.spawn_psproc() exe = p.exe() @@ -779,7 +781,7 @@ def test_cmdline(self): return assert ' '.join(p.cmdline()) == ' '.join(cmdline) - @unittest.skipIf(PYPY, "broken on PYPY") + @pytest.mark.skipif(PYPY, reason="broken on PYPY") def test_long_cmdline(self): cmdline = [PYTHON_EXE] cmdline.extend(["-v"] * 50) @@ -809,8 +811,8 @@ def test_name(self): pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) - @unittest.skipIf(PYPY or QEMU_USER, "unreliable on PYPY") - @unittest.skipIf(QEMU_USER, "unreliable on QEMU user") + @pytest.mark.skipif(PYPY or QEMU_USER, reason="unreliable on PYPY") + @pytest.mark.skipif(QEMU_USER, reason="unreliable on QEMU user") def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix="0123456789" * 2)) cmdline = [ @@ -838,10 +840,10 @@ def test_long_name(self): assert 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") - @unittest.skipIf(QEMU_USER, "broken on QEMU user") + @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") + @pytest.mark.skipif(AIX, reason="broken on AIX") + @pytest.mark.skipif(PYPY, reason="broken on PYPY") + @pytest.mark.skipif(QEMU_USER, reason="broken on QEMU user") def test_prog_w_funky_name(self): # Test that name(), exe() and cmdline() correctly handle programs # with funky chars such as spaces and ")", see: @@ -857,7 +859,7 @@ def test_prog_w_funky_name(self): assert p.name() == os.path.basename(pyexe) assert os.path.normcase(p.exe()) == os.path.normcase(pyexe) - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_uids(self): p = psutil.Process() real, effective, _saved = p.uids() @@ -871,7 +873,7 @@ def test_uids(self): if hasattr(os, "getresuid"): assert os.getresuid() == p.uids() - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_gids(self): p = psutil.Process() real, effective, _saved = p.gids() @@ -951,7 +953,7 @@ def cleanup(init): except psutil.AccessDenied: pass - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_status(self): p = psutil.Process() assert p.status() == psutil.STATUS_RUNNING @@ -989,7 +991,7 @@ def test_cwd_2(self): p = self.spawn_psproc(cmd) call_until(lambda: p.cwd() == os.path.dirname(os.getcwd())) - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") def test_cpu_affinity(self): p = psutil.Process() initial = p.cpu_affinity() @@ -1028,7 +1030,7 @@ def test_cpu_affinity(self): p.cpu_affinity(set(all_cpus)) p.cpu_affinity(tuple(all_cpus)) - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_errs(self): p = self.spawn_psproc() invalid_cpu = [len(psutil.cpu_times(percpu=True)) + 10] @@ -1041,7 +1043,7 @@ def test_cpu_affinity_errs(self): with pytest.raises(ValueError): p.cpu_affinity([0, -1]) - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_all_combinations(self): p = psutil.Process() initial = p.cpu_affinity() @@ -1062,9 +1064,9 @@ def test_cpu_affinity_all_combinations(self): assert sorted(p.cpu_affinity()) == sorted(combo) # TODO: #595 - @unittest.skipIf(BSD, "broken on BSD") + @pytest.mark.skipif(BSD, reason="broken on BSD") # can't find any process file on Appveyor - @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") + @pytest.mark.skipif(APPVEYOR, reason="unreliable on APPVEYOR") def test_open_files(self): p = psutil.Process() testfn = self.get_testfn() @@ -1103,9 +1105,9 @@ def test_open_files(self): assert os.path.isfile(file), file # TODO: #595 - @unittest.skipIf(BSD, "broken on BSD") + @pytest.mark.skipif(BSD, reason="broken on BSD") # can't find any process file on Appveyor - @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") + @pytest.mark.skipif(APPVEYOR, reason="unreliable on APPVEYOR") def test_open_files_2(self): # test fd and path fields p = psutil.Process() @@ -1134,7 +1136,7 @@ def test_open_files_2(self): # test file is gone assert fileobj.name not in p.open_files() - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_num_fds(self): p = psutil.Process() testfn = self.get_testfn() @@ -1150,7 +1152,9 @@ def test_num_fds(self): assert p.num_fds() == start @skip_on_not_implemented(only_if=LINUX) - @unittest.skipIf(OPENBSD or NETBSD, "not reliable on OPENBSD & NETBSD") + @pytest.mark.skipif( + OPENBSD or NETBSD, reason="not reliable on OPENBSD & NETBSD" + ) def test_num_ctx_switches(self): p = psutil.Process() before = sum(p.num_ctx_switches()) @@ -1181,7 +1185,7 @@ def test_parent_multi(self): assert grandchild.parent() == child assert child.parent() == parent - @unittest.skipIf(QEMU_USER, "QEMU user not supported") + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") @retry_on_failure() def test_parents(self): parent = psutil.Process() @@ -1406,12 +1410,12 @@ def assert_raises_nsp(fun, fun_name): for fun, name in ns.iter(ns.all): assert_raises_nsp(fun, name) - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process(self): _parent, zombie = self.spawn_zombie() self.assertProcessZombie(zombie) - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process_is_running_w_exc(self): # Emulate a case where internally is_running() raises # ZombieProcess. @@ -1422,7 +1426,7 @@ def test_zombie_process_is_running_w_exc(self): assert p.is_running() assert m.called - @unittest.skipIf(not POSIX, 'POSIX only') + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process_status_w_exc(self): # Emulate a case where internally status() raises # ZombieProcess. @@ -1526,7 +1530,7 @@ def test_pid_0(self): assert 0 in psutil.pids() assert psutil.pid_exists(0) - @unittest.skipIf(not HAS_ENVIRON, "not supported") + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") def test_environ(self): def clean_dict(d): exclude = ["PLAT", "HOME", "PYTEST_CURRENT_TEST", "PYTEST_VERSION"] @@ -1554,13 +1558,15 @@ def clean_dict(d): if not OSX and GITHUB_ACTIONS: assert d1 == d2 - @unittest.skipIf(not HAS_ENVIRON, "not supported") - @unittest.skipIf(not POSIX, "POSIX only") - @unittest.skipIf( + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( MACOS_11PLUS, - "macOS 11+ can't get another process environment, issue #2084", + reason="macOS 11+ can't get another process environment, issue #2084", + ) + @pytest.mark.skipif( + NETBSD, reason="sometimes fails on `assert is_running()`" ) - @unittest.skipIf(NETBSD, "sometimes fails on `assert is_running()`") def test_weird_environ(self): # environment variables can contain values without an equals sign code = textwrap.dedent(""" @@ -1652,7 +1658,7 @@ def test_nice(self): else: raise self.fail("exception not raised") - @unittest.skipIf(1, "causes problem as root") + @pytest.mark.skipif(True, reason="causes problem as root") def test_zombie_process(self): pass diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index 18ea18e6e4..b9638ec44b 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -7,15 +7,15 @@ """Sun OS specific tests.""" import os -import unittest import psutil from psutil import SUNOS from psutil.tests import PsutilTestCase +from psutil.tests import pytest from psutil.tests import sh -@unittest.skipIf(not SUNOS, "SUNOS only") +@pytest.mark.skipif(not SUNOS, reason="SUNOS only") class SunOSSpecificTestCase(PsutilTestCase): def test_swap_memory(self): out = sh('env PATH=/usr/sbin:/sbin:%s swap -l' % os.environ['PATH']) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 298d2d9e40..46cc15d4ea 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -136,8 +136,9 @@ def test_cache_clear(self): class TestProcessAPIs(PsutilTestCase): - @unittest.skipIf( - PYPY and WINDOWS, "spawn_testproc() unreliable on PYPY + WINDOWS" + @pytest.mark.skipif( + PYPY and WINDOWS, + reason="spawn_testproc() unreliable on PYPY + WINDOWS", ) def test_wait_procs(self): def callback(p): @@ -198,8 +199,9 @@ def test_2(procs, callback): for p in gone: assert hasattr(p, 'returncode') - @unittest.skipIf( - PYPY and WINDOWS, "spawn_testproc() unreliable on PYPY + WINDOWS" + @pytest.mark.skipif( + PYPY and WINDOWS, + reason="spawn_testproc() unreliable on PYPY + WINDOWS", ) def test_wait_procs_no_timeout(self): sproc1 = self.spawn_testproc() @@ -242,7 +244,9 @@ def test_boot_time(self): assert bt > 0 assert bt < time.time() - @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") + @pytest.mark.skipif( + CI_TESTING and not psutil.users(), reason="unreliable on CI" + ) def test_users(self): users = psutil.users() assert users != [] @@ -491,7 +495,9 @@ def test_per_cpu_times_2(self): if difference >= 0.05: return - @unittest.skipIf(CI_TESTING and OPENBSD, "unreliable on OPENBSD + CI") + @pytest.mark.skipif( + CI_TESTING and OPENBSD, reason="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 @@ -589,10 +595,10 @@ def test_cpu_stats(self): assert value > 0 # TODO: remove this once 1892 is fixed - @unittest.skipIf( - MACOS and platform.machine() == 'arm64', "skipped due to #1892" + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" ) - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): def check_ls(ls): for nt in ls: @@ -614,7 +620,7 @@ def check_ls(ls): if LINUX: assert len(ls) == psutil.cpu_count() - @unittest.skipIf(not HAS_GETLOADAVG, "not supported") + @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") def test_getloadavg(self): loadavg = psutil.getloadavg() assert len(loadavg) == 3 @@ -624,7 +630,9 @@ def test_getloadavg(self): class TestDiskAPIs(PsutilTestCase): - @unittest.skipIf(PYPY and not IS_64BIT, "unreliable on PYPY32 + 32BIT") + @pytest.mark.skipif( + PYPY and not IS_64BIT, reason="unreliable on PYPY32 + 32BIT" + ) def test_disk_usage(self): usage = psutil.disk_usage(os.getcwd()) assert usage._fields == ('total', 'used', 'free', 'percent') @@ -651,7 +659,7 @@ def test_disk_usage(self): with pytest.raises(FileNotFoundError): psutil.disk_usage(fname) - @unittest.skipIf(not ASCII_FS, "not an ASCII fs") + @pytest.mark.skipif(not ASCII_FS, reason="not an ASCII fs") def test_disk_usage_unicode(self): # See: https://github.com/giampaolo/psutil/issues/416 with pytest.raises(UnicodeEncodeError): @@ -718,12 +726,12 @@ def find_mount_point(path): ] assert mount in mounts - @unittest.skipIf( + @pytest.mark.skipif( LINUX and not os.path.exists('/proc/diskstats'), - '/proc/diskstats not available on this linux version', + reason="/proc/diskstats not available on this linux version", ) - @unittest.skipIf( - CI_TESTING and not psutil.disk_io_counters(), "unreliable on CI" + @pytest.mark.skipif( + CI_TESTING and not psutil.disk_io_counters(), reason="unreliable on CI" ) # no visible disks def test_disk_io_counters(self): def check_ntuple(nt): @@ -765,7 +773,7 @@ def test_disk_io_counters_no_disks(self): class TestNetAPIs(PsutilTestCase): - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") def test_net_io_counters(self): def check_ntuple(nt): assert nt[0] == nt.bytes_sent @@ -794,7 +802,7 @@ def check_ntuple(nt): assert isinstance(key, str) check_ntuple(ret[key]) - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="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 @@ -805,7 +813,7 @@ def test_net_io_counters_no_nics(self): assert psutil.net_io_counters(pernic=True) == {} assert m.called - @unittest.skipIf(QEMU_USER, 'QEMU user not supported') + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_addrs(self): nics = psutil.net_if_addrs() assert nics, nics @@ -893,7 +901,7 @@ def test_net_if_addrs_mac_null_bytes(self): else: assert addr.address == '06-3d-29-00-00-00' - @unittest.skipIf(QEMU_USER, 'QEMU user not supported') + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_stats(self): nics = psutil.net_if_stats() assert nics, nics @@ -912,8 +920,8 @@ def test_net_if_stats(self): assert mtu >= 0 assert isinstance(flags, str) - @unittest.skipIf( - not (LINUX or BSD or MACOS), "LINUX or BSD or MACOS specific" + @pytest.mark.skipif( + not (LINUX or BSD or MACOS), reason="LINUX or BSD or MACOS specific" ) def test_net_if_stats_enodev(self): # See: https://github.com/giampaolo/psutil/issues/1279 @@ -927,7 +935,7 @@ def test_net_if_stats_enodev(self): class TestSensorsAPIs(PsutilTestCase): - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") def test_sensors_temperatures(self): temps = psutil.sensors_temperatures() for name, entries in temps.items(): @@ -941,7 +949,7 @@ def test_sensors_temperatures(self): if entry.critical is not None: assert entry.critical >= 0 - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") def test_sensors_temperatures_fahreneit(self): d = {'coretemp': [('label', 50.0, 60.0, 70.0)]} with mock.patch( @@ -953,8 +961,8 @@ def test_sensors_temperatures_fahreneit(self): assert temps.high == 140.0 assert temps.critical == 158.0 - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - @unittest.skipIf(not HAS_BATTERY, "no battery") + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_sensors_battery(self): ret = psutil.sensors_battery() assert ret.percent >= 0 @@ -969,7 +977,7 @@ def test_sensors_battery(self): assert ret.power_plugged assert isinstance(ret.power_plugged, bool) - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") def test_sensors_fans(self): fans = psutil.sensors_fans() for name, entries in fans.items(): diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 7293aa010c..2f8005d54b 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -15,7 +15,6 @@ import stat import subprocess import textwrap -import unittest import warnings import psutil @@ -249,7 +248,7 @@ def test_spawn_children_pair(self): terminate(grandchild) assert not grandchild.is_running() - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_spawn_zombie(self): _parent, zombie = self.spawn_zombie() assert zombie.status() == psutil.STATUS_ZOMBIE @@ -300,7 +299,7 @@ def bind_socket(self): with contextlib.closing(bind_socket(addr=('', port))) as s: assert s.getsockname()[1] == port - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_bind_unix_socket(self): name = self.get_testfn() sock = bind_unix_socket(name) @@ -327,9 +326,9 @@ def tcp_tcp_socketpair(self): assert client.getpeername() == addr assert client.getsockname() != addr - @unittest.skipIf(not POSIX, "POSIX only") - @unittest.skipIf( - NETBSD or FREEBSD, "/var/run/log UNIX socket opened by default" + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + NETBSD or FREEBSD, reason="/var/run/log UNIX socket opened by default" ) def test_unix_socketpair(self): p = psutil.Process() @@ -396,8 +395,8 @@ def test_param_err(self): self.execute(lambda: 0, retries=-1) @retry_on_failure() - @unittest.skipIf(CI_TESTING, "skipped on CI") - @unittest.skipIf(COVERAGE, "skipped during test coverage") + @pytest.mark.skipif(CI_TESTING, reason="skipped on CI") + @pytest.mark.skipif(COVERAGE, reason="skipped during test coverage") def test_leak_mem(self): ls = [] diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index cc7a5475b0..89447dd575 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -179,8 +179,8 @@ def setUp(self): @pytest.mark.xdist_group(name="serial") -@unittest.skipIf(ASCII_FS, "ASCII fs") -@unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2") +@pytest.mark.skipif(ASCII_FS, reason="ASCII fs") +@pytest.mark.skipif(PYPY and not PY3, reason="too much trouble on PYPY2") class TestFSAPIs(BaseUnicodeTest): """Test FS APIs with a funky, valid, UTF8 path name.""" @@ -246,7 +246,7 @@ def test_proc_cwd(self): if self.expect_exact_path_match(): assert cwd == dname - @unittest.skipIf(PYPY and WINDOWS, "fails on PYPY + WINDOWS") + @pytest.mark.skipif(PYPY and WINDOWS, reason="fails on PYPY + WINDOWS") def test_proc_open_files(self): p = psutil.Process() start = set(p.open_files()) @@ -260,7 +260,7 @@ def test_proc_open_files(self): if self.expect_exact_path_match(): assert os.path.normcase(path) == os.path.normcase(self.funky_name) - @unittest.skipIf(not POSIX, "POSIX only") + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_proc_net_connections(self): name = self.get_testfn(suffix=self.funky_suffix) try: @@ -275,8 +275,10 @@ def test_proc_net_connections(self): assert isinstance(conn.laddr, str) assert conn.laddr == name - @unittest.skipIf(not POSIX, "POSIX only") - @unittest.skipIf(not HAS_NET_CONNECTIONS_UNIX, "can't list UNIX sockets") + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets" + ) @skip_on_access_denied() def test_net_connections(self): def find_sock(cons): @@ -305,9 +307,11 @@ def test_disk_usage(self): 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") + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif( + not PY3, reason="ctypes does not support unicode on PY2" + ) + @pytest.mark.skipif(PYPY, reason="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. @@ -326,7 +330,7 @@ def normpath(p): assert isinstance(path, str) -@unittest.skipIf(CI_TESTING, "unreliable on CI") +@pytest.mark.skipif(CI_TESTING, reason="unreliable on CI") class TestFSAPIsWithInvalidPath(TestFSAPIs): """Test FS APIs with a funky, invalid path name.""" @@ -347,8 +351,8 @@ class TestNonFSAPIS(BaseUnicodeTest): funky_suffix = UNICODE_SUFFIX if PY3 else 'è' - @unittest.skipIf(not HAS_ENVIRON, "not supported") - @unittest.skipIf(PYPY and WINDOWS, "segfaults on PYPY + WINDOWS") + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(PYPY and WINDOWS, reason="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 diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 7b5ba7ba11..da1da930b8 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -57,10 +57,12 @@ cext = psutil._psplatform.cext -@unittest.skipIf(not WINDOWS, "WINDOWS only") -@unittest.skipIf(PYPY, "pywin32 not available on PYPY") +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +@pytest.mark.skipif(PYPY, reason="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") +@pytest.mark.skipif( + GITHUB_ACTIONS and not PY3, reason="pywin32 broken on GITHUB + PY2" +) class WindowsTestCase(PsutilTestCase): pass @@ -103,9 +105,9 @@ def wmic(path, what, converter=int): class TestCpuAPIs(WindowsTestCase): - @unittest.skipIf( + @pytest.mark.skipif( 'NUMBER_OF_PROCESSORS' not in os.environ, - 'NUMBER_OF_PROCESSORS env var is not available', + reason="NUMBER_OF_PROCESSORS env var is not available", ) def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): # Will likely fail on many-cores systems: @@ -186,7 +188,7 @@ def test_percent_swapmem(self): assert abs(psutil.swap_memory().percent - percentSwap) < 5 assert psutil.swap_memory().percent <= 100 - # @unittest.skipIf(wmi is None, "wmi module is not installed") + # @pytest.mark.skipif(wmi is None, reason="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. @@ -198,7 +200,7 @@ def test_percent_swapmem(self): # time.localtime(p.create_time())) # Note: this test is not very reliable - @unittest.skipIf(APPVEYOR, "test not relieable on appveyor") + @pytest.mark.skipif(APPVEYOR, reason="test not relieable on appveyor") @retry_on_failure() def test_pids(self): # Note: this test might fail if the OS is starting/killing @@ -311,7 +313,7 @@ def test_has_battery(self): else: assert psutil.sensors_battery() is None - @unittest.skipIf(not HAS_BATTERY, "no battery") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_percent(self): w = wmi.WMI() battery_wmi = w.query('select * from Win32_Battery')[0] @@ -321,7 +323,7 @@ def test_percent(self): < 1 ) - @unittest.skipIf(not HAS_BATTERY, "no battery") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_power_plugged(self): w = wmi.WMI() battery_wmi = w.query('select * from Win32_Battery')[0] @@ -597,7 +599,9 @@ def test_name(self): assert p.name() == w.Caption # This fail on github because using virtualenv for test environment - @unittest.skipIf(GITHUB_ACTIONS, "unreliable path on GITHUB_ACTIONS") + @pytest.mark.skipif( + GITHUB_ACTIONS, reason="unreliable path on GITHUB_ACTIONS" + ) def test_exe(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] p = psutil.Process(self.pid) @@ -650,7 +654,7 @@ def test_create_time(self): # --- -@unittest.skipIf(not WINDOWS, "WINDOWS only") +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") class TestDualProcessImplementation(PsutilTestCase): """Certain APIs on Windows have 2 internal implementations, one based on documented Windows APIs, another one based @@ -738,7 +742,7 @@ def test_cmdline(self): assert a == b -@unittest.skipIf(not WINDOWS, "WINDOWS only") +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") class RemoteProcessTestCase(PsutilTestCase): """Certain functions require calling ReadProcessMemory. This trivially works when called on the current process. @@ -832,7 +836,7 @@ def test_environ_64(self): # =================================================================== -@unittest.skipIf(not WINDOWS, "WINDOWS only") +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") class TestServices(PsutilTestCase): def test_win_service_iter(self): valid_statuses = set([ From b19d5bd1871c96a6b3dcc70d0c9965dcadb74878 Mon Sep 17 00:00:00 2001 From: Aleksey Lobanov Date: Tue, 15 Oct 2024 11:32:00 +0300 Subject: [PATCH 1143/1714] AIX: improve open_files() regexp speed (#2457) --- CREDITS | 5 +++++ HISTORY.rst | 2 ++ psutil/_psaix.py | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 543536370a..35f4b6b4ae 100644 --- a/CREDITS +++ b/CREDITS @@ -835,3 +835,8 @@ I: 2272 N: Sam Gross W: https://github.com/colesbury I: 2401, 2427 + +N: Aleksey Lobanov +C: Russia +E: alex_github@likemath.ru +W: https://github.com/AlekseyLobanov diff --git a/HISTORY.rst b/HISTORY.rst index 6ed9bd3da6..e7687066d4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -25,6 +25,8 @@ XXXX-XX-XX Python 3.13. (patch by Sam Gross) - 2455_, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and field 40 (blkio_ticks) is missing. +- 2457_, [AIX]: significantly improve the speed of `Process.open_files()`_ for + some edge cases. - 2460_, [OpenBSD]: `Process.num_fds()`_ and `Process.open_files()`_ may fail with `NoSuchProcess`_ for PID 0. Instead, we now return "null" values (0 and [] respectively). diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 0904449b3d..2ccc638bce 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -539,7 +539,7 @@ def open_files(self): ) if "no such process" in stderr.lower(): raise NoSuchProcess(self.pid, self._name) - procfiles = re.findall(r"(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout) + procfiles = re.findall(r"(\d+): S_IFREG.*name:(.*)\n", stdout) retlist = [] for fd, path in procfiles: path = path.strip() From b1a759399a461996fef72a1f888cc8a60535d500 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 15 Oct 2024 11:15:25 +0200 Subject: [PATCH 1144/1714] Use `pytest.skip` instead of `unittest.SkipTest` (#2461) --- psutil/tests/__init__.py | 13 +++++++---- psutil/tests/test_bsd.py | 5 ++--- psutil/tests/test_contracts.py | 3 +-- psutil/tests/test_linux.py | 17 +++++++------- psutil/tests/test_misc.py | 11 +++++---- psutil/tests/test_posix.py | 9 ++++---- psutil/tests/test_process.py | 19 ++++++++-------- psutil/tests/test_system.py | 7 +++--- psutil/tests/test_testutils.py | 41 ++++++++++++++++++++++++++++++++++ psutil/tests/test_unicode.py | 9 ++++---- psutil/tests/test_windows.py | 7 +++--- 11 files changed, 89 insertions(+), 52 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b2047ab70e..dd3d536472 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -104,7 +104,7 @@ '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', + 'is_win_secure_system_proc', 'fake_pytest', # fs utils 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', # os @@ -878,7 +878,7 @@ 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") + raise pytest.skip("gcc is not installed") if c_code is None: c_code = textwrap.dedent(""" #include @@ -974,6 +974,11 @@ def warns(warning, match=None): return unittest.TestCase().assertWarnsRegex(warning, match) return unittest.TestCase().assertWarns(warning) + @staticmethod + def skip(reason=""): + """Mimics `unittest.SkipTest`.""" + raise unittest.SkipTest(reason) + class mark: @staticmethod @@ -1689,7 +1694,7 @@ def wrapper(*args, **kwargs): if only_if is not None: if not only_if: raise - raise unittest.SkipTest("raises AccessDenied") + raise pytest.skip("raises AccessDenied") return wrapper @@ -1712,7 +1717,7 @@ def wrapper(*args, **kwargs): "%r was skipped because it raised NotImplementedError" % fun.__name__ ) - raise unittest.SkipTest(msg) + raise pytest.skip(msg) return wrapper diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index c836dfd5c4..2fd1015d73 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -14,7 +14,6 @@ import os import re import time -import unittest import psutil from psutil import BSD @@ -267,7 +266,7 @@ def test_cpu_frequency_against_sysctl(self): try: sysctl_result = int(sysctl(sensor)) except RuntimeError: - raise unittest.SkipTest("frequencies not supported by kernel") + raise pytest.skip("frequencies not supported by kernel") assert psutil.cpu_freq().current == sysctl_result sensor = "dev.cpu.0.freq_levels" @@ -471,7 +470,7 @@ def test_sensors_temperatures_against_sysctl(self): try: sysctl_result = int(float(sysctl(sensor)[:-1])) except RuntimeError: - raise unittest.SkipTest("temperatures not supported by kernel") + raise pytest.skip("temperatures not supported by kernel") assert ( abs( psutil.sensors_temperatures()["coretemp"][cpu].current diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 2f58cd1725..c0ec6a8f7e 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -11,7 +11,6 @@ import platform import signal -import unittest import psutil from psutil import AIX @@ -237,7 +236,7 @@ def test_cpu_count(self): @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): if psutil.cpu_freq() is None: - raise unittest.SkipTest("cpu_freq() returns None") + raise pytest.skip("cpu_freq() returns None") self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int, long)) def test_disk_io_counters(self): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 72f9378909..cdcb468e10 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -20,7 +20,6 @@ import struct import textwrap import time -import unittest import warnings import psutil @@ -216,7 +215,7 @@ def vmstat(stat): def get_free_version_info(): out = sh(["free", "-V"]).strip() if 'UNKNOWN' in out: - raise unittest.SkipTest("can't determine free version") + raise pytest.skip("can't determine free version") return tuple(map(int, re.findall(r'\d+', out.split()[-1]))) @@ -286,9 +285,9 @@ def test_used(self): # https://gitlab.com/procps-ng/procps/commit/ # 2184e90d2e7cdb582f9a5b706b47015e56707e4d if get_free_version_info() < (3, 3, 12): - raise unittest.SkipTest("free version too old") + raise pytest.skip("free version too old") if get_free_version_info() >= (4, 0, 0): - raise unittest.SkipTest("free version too recent") + raise pytest.skip("free version too recent") cli_value = free_physmem().used psutil_value = psutil.virtual_memory().used assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM @@ -304,7 +303,7 @@ def test_shared(self): free = free_physmem() free_value = free.shared if free_value == 0: - raise unittest.SkipTest("free does not support 'shared' column") + raise pytest.skip("free does not support 'shared' column") psutil_value = psutil.virtual_memory().shared assert ( abs(free_value - psutil_value) < TOLERANCE_SYS_MEM @@ -317,7 +316,7 @@ def test_available(self): out = sh(["free", "-b"]) lines = out.split('\n') if 'available' not in lines[0]: - raise unittest.SkipTest("free does not support 'available' column") + raise pytest.skip("free does not support 'available' column") else: free_value = int(lines[1].split()[-1]) psutil_value = psutil.virtual_memory().available @@ -344,9 +343,9 @@ def test_used(self): # https://gitlab.com/procps-ng/procps/commit/ # 2184e90d2e7cdb582f9a5b706b47015e56707e4d if get_free_version_info() < (3, 3, 12): - raise unittest.SkipTest("free version too old") + raise pytest.skip("free version too old") if get_free_version_info() >= (4, 0, 0): - raise unittest.SkipTest("free version too recent") + raise pytest.skip("free version too recent") vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM @@ -651,7 +650,7 @@ def test_meminfo_against_sysinfo(self): # 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") + raise pytest.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 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 3a4275c80a..ad1922f8e6 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -16,7 +16,6 @@ import socket import stat import sys -import unittest import psutil import psutil.tests @@ -346,7 +345,7 @@ def check(ret): # def test_setup_script(self): # setup_py = os.path.join(ROOT_DIR, 'setup.py') # if CI_TESTING and not os.path.exists(setup_py): - # raise unittest.SkipTest("can't find setup.py") + # raise pytest.skip("can't find setup.py") # module = import_module_by_path(setup_py) # self.assertRaises(SystemExit, module.setup) # self.assertEqual(module.get_version(), psutil.__version__) @@ -891,7 +890,7 @@ def test_cache_clear(self): @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") def test_cache_clear_public_apis(self): if not psutil.disk_io_counters() or not psutil.net_io_counters(): - raise unittest.SkipTest("no disks or NICs available") + raise pytest.skip("no disks or NICs available") psutil.disk_io_counters() psutil.net_io_counters() caches = wrap_numbers.cache_info() @@ -1001,7 +1000,7 @@ def test_pmap(self): def test_procsmem(self): if 'uss' not in psutil.Process().memory_full_info()._fields: - raise unittest.SkipTest("not supported") + raise pytest.skip("not supported") self.assert_stdout('procsmem.py') def test_killall(self): @@ -1030,13 +1029,13 @@ def test_cpu_distribution(self): @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") def test_temperatures(self): if not psutil.sensors_temperatures(): - raise unittest.SkipTest("no temperatures") + raise pytest.skip("no temperatures") self.assert_stdout('temperatures.py') @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") def test_fans(self): if not psutil.sensors_fans(): - raise unittest.SkipTest("no fans") + raise pytest.skip("no fans") self.assert_stdout('fans.py') @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index ba430154d5..6f7f9790fc 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -13,7 +13,6 @@ import re import subprocess import time -import unittest import psutil from psutil import AIX @@ -141,7 +140,7 @@ def df(device): out = sh("df -k %s" % device).strip() except RuntimeError as err: if "device busy" in str(err).lower(): - raise unittest.SkipTest("df returned EBUSY") + raise pytest.skip("df returned EBUSY") raise line = out.split('\n')[1] fields = line.split() @@ -370,7 +369,7 @@ def test_nic_names(self): def test_users(self): out = sh("who -u") if not out.strip(): - raise unittest.SkipTest("no users on this system") + raise pytest.skip("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] @@ -386,7 +385,7 @@ def test_users(self): def test_users_started(self): out = sh("who -u") if not out.strip(): - raise unittest.SkipTest("no users on this system") + raise pytest.skip("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) @@ -410,7 +409,7 @@ def test_users_started(self): started = [x.capitalize() for x in started] if not tstamp: - raise unittest.SkipTest( + raise pytest.skip( "cannot interpret tstamp in who output\n%s" % (out) ) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a0e5199ec0..b8f06a46e9 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -19,7 +19,6 @@ import textwrap import time import types -import unittest import psutil from psutil import AIX @@ -329,7 +328,7 @@ def test_terminal(self): tty = os.path.realpath(sh('tty')) except RuntimeError: # Note: happens if pytest is run without the `-s` opt. - raise unittest.SkipTest("can't rely on `tty` CLI") + raise pytest.skip("can't rely on `tty` CLI") else: assert terminal == tty @@ -543,7 +542,7 @@ def test_num_threads(self): try: step1 = p.num_threads() except psutil.AccessDenied: - raise unittest.SkipTest("on OpenBSD this requires root access") + raise pytest.skip("on OpenBSD this requires root access") else: step1 = p.num_threads() @@ -564,7 +563,7 @@ def test_threads(self): try: step1 = p.threads() except psutil.AccessDenied: - raise unittest.SkipTest("on OpenBSD this requires root access") + raise pytest.skip("on OpenBSD this requires root access") else: step1 = p.threads() @@ -586,7 +585,7 @@ def test_threads_2(self): try: p.threads() except psutil.AccessDenied: - raise unittest.SkipTest("on OpenBSD this requires root access") + raise pytest.skip("on OpenBSD this requires root access") assert ( abs(p.cpu_times().user - sum([x.user_time for x in p.threads()])) < 0.1 @@ -761,7 +760,7 @@ def test_cmdline(self): if NETBSD and p.cmdline() == []: # https://github.com/giampaolo/psutil/issues/2250 - raise unittest.SkipTest("OPENBSD: returned EBUSY") + raise pytest.skip("OPENBSD: returned EBUSY") # XXX - most of the times the underlying sysctl() call on Net # and Open BSD returns a truncated string. @@ -795,14 +794,14 @@ def test_long_cmdline(self): try: assert p.cmdline() == cmdline except psutil.ZombieProcess: - raise unittest.SkipTest("OPENBSD: process turned into zombie") + raise pytest.skip("OPENBSD: process turned into zombie") elif QEMU_USER: assert p.cmdline()[2:] == cmdline else: ret = p.cmdline() if NETBSD and ret == []: # https://github.com/giampaolo/psutil/issues/2250 - raise unittest.SkipTest("OPENBSD: returned EBUSY") + raise pytest.skip("OPENBSD: returned EBUSY") assert ret == cmdline def test_name(self): @@ -968,7 +967,7 @@ def test_username(self): # 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') + raise pytest.skip('running as service account') assert username == getpass_user if 'USERDOMAIN' in os.environ: assert domain == os.environ['USERDOMAIN'] @@ -1234,7 +1233,7 @@ def test_children_duplicates(self): # 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 unittest.SkipTest("PID 0") + raise pytest.skip("PID 0") p = psutil.Process(pid) try: c = p.children(recursive=True) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 46cc15d4ea..7d6695408a 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -17,7 +17,6 @@ import socket import sys import time -import unittest import psutil from psutil import AIX @@ -380,13 +379,13 @@ def test_cpu_count_logical(self): 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") + raise pytest.skip("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 unittest.SkipTest("cpu_count_cores() is None") + raise pytest.skip("cpu_count_cores() is None") if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista assert cores is None else: @@ -612,7 +611,7 @@ def check_ls(ls): ls = psutil.cpu_freq(percpu=True) if FREEBSD and not ls: - raise unittest.SkipTest("returns empty list on FreeBSD") + raise pytest.skip("returns empty list on FreeBSD") assert ls, ls check_ls([psutil.cpu_freq(percpu=False)]) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 2f8005d54b..e11c8f783d 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -15,6 +15,7 @@ import stat import subprocess import textwrap +import unittest import warnings import psutil @@ -25,6 +26,7 @@ from psutil._common import open_binary from psutil._common import open_text from psutil._common import supports_ipv6 +from psutil._compat import PY3 from psutil.tests import CI_TESTING from psutil.tests import COVERAGE from psutil.tests import HAS_NET_CONNECTIONS_UNIX @@ -448,6 +450,13 @@ def fun_2(): class TestFakePytest(PsutilTestCase): + def run_test_class(self, klass): + suite = unittest.TestSuite() + suite.addTest(klass) + runner = unittest.TextTestRunner() + result = runner.run(suite) + return result + def test_raises(self): with fake_pytest.raises(ZeroDivisionError) as cm: 1 / 0 # noqa @@ -478,6 +487,38 @@ def bar(self): assert Foo().bar() == 1 + def test_skipif(self): + class TestCase(unittest.TestCase): + @fake_pytest.mark.skipif(True, reason="reason") + def foo(self): + assert 1 == 1 # noqa + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 1 + assert result.skipped[0][1] == "reason" + + class TestCase(unittest.TestCase): + @fake_pytest.mark.skipif(False, reason="reason") + def foo(self): + assert 1 == 1 # noqa + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 0 + + @pytest.mark.skipif(not PY3, reason="not PY3") + def test_skip(self): + class TestCase(unittest.TestCase): + def foo(self): + fake_pytest.skip("reason") + assert 1 == 0 # noqa + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 1 + assert result.skipped[0][1] == "reason" + def test_main(self): tmpdir = self.get_testfn(dir=HERE) os.mkdir(tmpdir) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 89447dd575..1bea46b5db 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -75,7 +75,6 @@ import os import shutil import traceback -import unittest import warnings from contextlib import closing @@ -175,7 +174,7 @@ def setUpClass(cls): def setUp(self): super().setUp() if self.skip_tests: - raise unittest.SkipTest("can't handle unicode str") + raise pytest.skip("can't handle unicode str") @pytest.mark.xdist_group(name="serial") @@ -256,7 +255,7 @@ def test_proc_open_files(self): assert isinstance(path, str) if BSD and not path: # XXX - see https://github.com/giampaolo/psutil/issues/595 - raise unittest.SkipTest("open_files on BSD is broken") + raise pytest.skip("open_files on BSD is broken") if self.expect_exact_path_match(): assert os.path.normcase(path) == os.path.normcase(self.funky_name) @@ -269,7 +268,7 @@ def test_proc_net_connections(self): if PY3: raise else: - raise unittest.SkipTest("not supported") + raise pytest.skip("not supported") with closing(sock): conn = psutil.Process().net_connections('unix')[0] assert isinstance(conn.laddr, str) @@ -294,7 +293,7 @@ def find_sock(cons): if PY3: raise else: - raise unittest.SkipTest("not supported") + raise pytest.skip("not supported") with closing(sock): cons = psutil.net_connections(kind='unix') conn = find_sock(cons) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index da1da930b8..b11b0e7067 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -17,7 +17,6 @@ import subprocess import sys import time -import unittest import warnings import psutil @@ -74,7 +73,7 @@ def powershell(cmd): "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") """ if not which("powershell.exe"): - raise unittest.SkipTest("powershell.exe not available") + raise pytest.skip("powershell.exe not available") cmdline = ( 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive ' + '-NoProfile -WindowStyle Hidden -Command "%s"' % cmd @@ -440,7 +439,7 @@ def test_username(self): # 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') + raise pytest.skip('running as service account') assert psutil.Process().username() == name def test_cmdline(self): @@ -775,7 +774,7 @@ def setUp(self): other_python = self.find_other_interpreter() if other_python is None: - raise unittest.SkipTest( + raise pytest.skip( "could not find interpreter with opposite bitness" ) if IS_64BIT: From 567438cd3eb4334486d88ba90aa14c65755b61cd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Oct 2024 21:00:17 +0200 Subject: [PATCH 1145/1714] [Windows] speed up `process_iter()` (#2444) This is based on https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555. On Windows, we now determine process unique identity by using process' fast create time method. This has more chances to fail with `AccessDenied` for ADMIN owned processes, but it shouldn't matter because if we have no rights to get process ctime we'll also have no rights to accidentally `kill()` the wrong process PID anyway. This should drastically speedup `process_iter()` when used for retrieving process info one time instead of in a loop (e.g. htop like apps). --- HISTORY.rst | 6 ++++- psutil/__init__.py | 47 ++++++++++++++++++++++++++---------- psutil/_pswindows.py | 4 ++- psutil/tests/test_misc.py | 15 +++++++++--- psutil/tests/test_process.py | 3 ++- 5 files changed, 56 insertions(+), 19 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e7687066d4..bc4625f38b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,10 @@ XXXX-XX-XX **Enhancements** +- 2366_, [Windows]: drastically speedup `process_iter()`_. We now determine + process unique identity by using process "fast" create time method. This + will considerably speedup those apps which use `process_iter()`_ only once, + e.g. to look for a process with a certain name. - 2446_: use pytest instead of unittest. - 2448_: add ``make install-sysdeps`` target to install the necessary system dependencies (python-dev, gcc, etc.) on all supported UNIX flavors. @@ -25,7 +29,7 @@ XXXX-XX-XX Python 3.13. (patch by Sam Gross) - 2455_, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and field 40 (blkio_ticks) is missing. -- 2457_, [AIX]: significantly improve the speed of `Process.open_files()`_ for +- 2457_, [AIX]: significantly improve the speed of `Process.open_files()`_ for some edge cases. - 2460_, [OpenBSD]: `Process.num_fds()`_ and `Process.open_files()`_ may fail with `NoSuchProcess`_ for PID 0. Instead, we now return "null" values (0 and diff --git a/psutil/__init__.py b/psutil/__init__.py index 763a5f00ec..aabf715921 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -350,13 +350,13 @@ def _init(self, pid, _ignore_nsp=False): 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 + self._ident = (self.pid, None) try: - self.create_time() + self._ident = self._get_ident() except AccessDenied: - # We should never get here as AFAIK we're able to get - # process creation time on all platforms even as a - # limited user. + # This should happen on Windows only, since we use the fast + # create time method. AFAIK, on all other platforms we are + # able to get create time for all PIDs. pass except ZombieProcess: # Zombies can still be queried by this class (although @@ -368,11 +368,32 @@ def _init(self, pid, _ignore_nsp=False): raise NoSuchProcess(pid, msg=msg) else: self._gone = True - # 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 _get_ident(self): + """Return a (pid, uid) tuple which is supposed to identify a + Process instance univocally over time. The PID alone is not + enough, as it can be assigned to a new process after this one + terminates, so we add process creation time to the mix. We need + this in order to prevent killing the wrong process later on. + This is also known as PID reuse or PID recycling problem. + + The reliability of this strategy mostly depends on + create_time() precision, which is 0.01 secs on Linux. The + assumption is that, after a process terminates, the kernel + won't reuse the same PID after such a short period of time + (0.01 secs). Technically this is inherently racy, but + practically it should be good enough. + """ + if WINDOWS: + # Use create_time() fast method in order to speedup + # `process_iter()`. This means we'll get AccessDenied for + # most ADMIN processes, but that's fine since it means + # we'll also get AccessDenied on kill(). + # https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555 + self._create_time = self._proc.create_time(fast_only=True) + return (self.pid, self._create_time) + else: + return (self.pid, self.create_time()) def __str__(self): info = collections.OrderedDict() @@ -417,10 +438,10 @@ def __eq__(self, other): # (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 + pid1, ident1 = self._ident + pid2, ident2 = other._ident if pid1 == pid2: - if ctime1 and not ctime2: + if ident1 and not ident2: try: return self.status() == STATUS_ZOMBIE except Error: diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 7eb01b7e2c..e4550c3914 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -982,7 +982,7 @@ def username(self): return py2_strencode(domain) + '\\' + py2_strencode(user) @wrap_exceptions - def create_time(self): + def create_time(self, fast_only=False): # Note: proc_times() not put under oneshot() 'cause create_time() # is already cached by the main Process class. try: @@ -990,6 +990,8 @@ def create_time(self): return created except OSError as err: if is_permission_err(err): + if fast_only: + raise debug("attempting create_time() fallback (slower)") return self._proc_info()[pinfo_map['create_time']] raise diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index ad1922f8e6..ca189a97b7 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -354,22 +354,31 @@ 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 + psutil.Process, '_get_ident', side_effect=psutil.AccessDenied ) as meth: psutil.Process() assert meth.called + with mock.patch.object( - psutil.Process, 'create_time', side_effect=psutil.ZombieProcess(1) + psutil.Process, '_get_ident', side_effect=psutil.ZombieProcess(1) ) as meth: psutil.Process() assert meth.called + with mock.patch.object( - psutil.Process, 'create_time', side_effect=ValueError + psutil.Process, '_get_ident', side_effect=ValueError ) as meth: with pytest.raises(ValueError): psutil.Process() assert meth.called + with mock.patch.object( + psutil.Process, '_get_ident', side_effect=psutil.NoSuchProcess(1) + ) as meth: + with self.assertRaises(psutil.NoSuchProcess): + psutil.Process() + assert meth.called + def test_sanity_version_check(self): # see: https://github.com/giampaolo/psutil/issues/564 with mock.patch( diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b8f06a46e9..0aa0b5a0d3 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -14,6 +14,7 @@ import signal import socket import stat +import string import subprocess import sys import textwrap @@ -813,7 +814,7 @@ def test_name(self): @pytest.mark.skipif(PYPY or QEMU_USER, reason="unreliable on PYPY") @pytest.mark.skipif(QEMU_USER, reason="unreliable on QEMU user") def test_long_name(self): - pyexe = create_py_exe(self.get_testfn(suffix="0123456789" * 2)) + pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) cmdline = [ pyexe, "-c", From fb68f9fae3b398899d87161746884ebb2a2613c0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 17 Oct 2024 23:31:31 +0200 Subject: [PATCH 1146/1714] pre release --- HISTORY.rst | 6 +++--- docs/index.rst | 4 ++++ setup.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index bc4625f38b..7623a135a7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -6.1.0 (IN DEVELOPMENT) -====================== +6.1.0 +===== -XXXX-XX-XX +2024-10-17 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index d9fe1c03cd..3d24d735fa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2672,6 +2672,10 @@ PyPy3. Timeline ======== +- 2024-10-17: + `6.1.0 `__ - + `what's new `__ - + `diff `__ - 2024-06-18: `6.0.0 `__ - `what's new `__ - diff --git a/setup.py b/setup.py index 949716a5b8..a7b7e81701 100755 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ # Development deps, installable via `pip install .[dev]`. DEV_DEPS = [ + "abi3audit", "black", "check-manifest", "coverage", From 8e89a05d11834705c9df781bb2ccf956516c12fb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 21 Oct 2024 01:40:39 +0200 Subject: [PATCH 1147/1714] update winmake.py --- Makefile | 6 +++--- scripts/internal/winmake.py | 39 ++++--------------------------------- 2 files changed, 7 insertions(+), 38 deletions(-) diff --git a/Makefile b/Makefile index 7a38ee57f2..b48019cc9b 100644 --- a/Makefile +++ b/Makefile @@ -171,7 +171,7 @@ test-coverage: ## Run test coverage. ruff: ## Run ruff linter. @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --output-format=concise -black: ## Python files linting (via black) +black: ## Run black formatter. @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe _pylint: ## Python pylint (not mandatory, just run it from time to time) @@ -180,10 +180,10 @@ _pylint: ## Python pylint (not mandatory, just run it from time to time) lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py -lint-rst: ## Run C linter. +lint-rst: ## Run linter for .rst files. @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml -lint-toml: ## Linter for pyproject.toml +lint-toml: ## Run linter for pyproject.toml. @git ls-files '*.toml' | xargs toml-sort --check lint-all: ## Run all linters diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 3995a46f2f..eb87a19c65 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -21,26 +21,19 @@ import os import shutil import site -import ssl import subprocess import sys -import tempfile APPVEYOR = bool(os.environ.get('APPVEYOR')) PYTHON = sys.executable if APPVEYOR else os.getenv('PYTHON', sys.executable) PY3 = sys.version_info[0] >= 3 PYTEST_ARGS = "-v -s --tb=short" -if PY3: - PYTEST_ARGS += "-o " HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) PYPY = '__pypy__' in sys.builtin_module_names WINDOWS = os.name == "nt" -if PY3: - GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -else: - GET_PIP_URL = "https://bootstrap.pypa.io/pip/2.7/get-pip.py" + sys.path.insert(0, ROOT_DIR) # so that we can import setup.py @@ -259,31 +252,7 @@ def upload_wheels(): def install_pip(): """Install pip.""" - try: - sh('%s -c "import pip"' % PYTHON) - except SystemExit: - if PY3: - from urllib.request import urlopen - else: - from urllib2 import urlopen - - if hasattr(ssl, '_create_unverified_context'): - ctx = ssl._create_unverified_context() - else: - ctx = None - kw = dict(context=ctx) if ctx else {} - safe_print("downloading %s" % GET_PIP_URL) - req = urlopen(GET_PIP_URL, **kw) - data = req.read() - - tfile = os.path.join(tempfile.gettempdir(), 'get-pip.py') - with open(tfile, 'wb') as f: - f.write(data) - - try: - sh('%s %s --user' % (PYTHON, tfile)) - finally: - os.remove(tfile) + sh('%s %s' % (PYTHON, os.path.join(HERE, "install_pip.py"))) def install(): @@ -368,14 +337,14 @@ def install_pydeps_test(): """Install useful deps.""" install_pip() install_git_hooks() - sh("%s -m pip install -U %s" % (PYTHON, " ".join(TEST_DEPS))) + sh("%s -m pip install --user -U %s" % (PYTHON, " ".join(TEST_DEPS))) def install_pydeps_dev(): """Install useful deps.""" install_pip() install_git_hooks() - sh("%s -m pip install -U %s" % (PYTHON, " ".join(DEV_DEPS))) + sh("%s -m pip install --user -U %s" % (PYTHON, " ".join(DEV_DEPS))) def test(args=""): From 3d12e67a9e4af3328660d4b3a81966555d7c98bf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Nov 2024 18:37:35 +0100 Subject: [PATCH 1148/1714] fix syntax warning #2468 --- Makefile | 4 ++-- psutil/tests/test_system.py | 1 - scripts/internal/winmake.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index b48019cc9b..79ad8c8c65 100644 --- a/Makefile +++ b/Makefile @@ -81,13 +81,13 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip # upgrade pip to latest version + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip setuptools $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` install-pydeps-dev: ## Install python deps meant for local development. ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip # upgrade pip to latest version + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip setuptools $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS + setup.DEV_DEPS))"` install-git-hooks: ## Install GIT pre-commit hook. diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 7d6695408a..68074ae14e 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -515,7 +515,6 @@ def _test_cpu_percent(self, percent, last_ret, new_ret): try: assert isinstance(percent, float) assert percent >= 0.0 - assert percent is not -0.0 assert percent <= 100.0 * psutil.cpu_count() except AssertionError as err: raise AssertionError( diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index eb87a19c65..82a309397e 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -31,7 +31,6 @@ PYTEST_ARGS = "-v -s --tb=short" HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) -PYPY = '__pypy__' in sys.builtin_module_names WINDOWS = os.name == "nt" From 687695c289a84585b20f2037eee84fa11ca49b81 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 11 Nov 2024 10:33:14 +0100 Subject: [PATCH 1149/1714] fix #2470: `users()`_ may return "localhost" instead of the actual IP address of the user logged in. --- HISTORY.rst | 8 ++++++++ psutil/arch/linux/users.c | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7623a135a7..f69375e45a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,13 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +6.1.1 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +- 2470_, [Linux]: `users()`_ may return "localhost" instead of the actual IP + address of the user logged in. + 6.1.0 ===== diff --git a/psutil/arch/linux/users.c b/psutil/arch/linux/users.c index 546e29ee98..663d0f2a45 100644 --- a/psutil/arch/linux/users.c +++ b/psutil/arch/linux/users.c @@ -33,7 +33,7 @@ psutil_users(PyObject *self, PyObject *args) { py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); if (! py_tty) goto error; - if (strcmp(ut->ut_host, ":0") || strcmp(ut->ut_host, ":0.0")) + if (strcmp(ut->ut_host, ":0") == 0 || strcmp(ut->ut_host, ":0.0") == 0) py_hostname = PyUnicode_DecodeFSDefault("localhost"); else py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); From 66892b20b4b7d43fba919454cb7935b40ec53501 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Nov 2024 21:21:07 +0100 Subject: [PATCH 1150/1714] remove dead code --- .github/workflows/bsd.yml | 2 +- .github/workflows/issues.py | 5 --- psutil/_compat.py | 2 +- psutil/tests/test_aix.py | 39 +++++++++++++++++--- psutil/tests/test_linux.py | 5 --- scripts/internal/bench_oneshot_2.py | 4 -- scripts/internal/download_wheels_appveyor.py | 2 - scripts/internal/git_pre_commit.py | 1 - 8 files changed, 35 insertions(+), 25 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 5358412a3b..c405e82283 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -35,7 +35,7 @@ jobs: PIP_BREAK_SYSTEM_PACKAGES=1 make install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks netbsd: - # if: false + if: false # XXX: disabled runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index eddaa7f998..5e8c2026f2 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -144,11 +144,6 @@ def has_label(issue, label): return label in assigned -def has_os_label(issue): - labels = set([x.name for x in issue.labels]) - return any(x in labels for x in OS_LABELS) - - def get_repo(): repo = os.environ['GITHUB_REPOSITORY'] token = os.environ['GITHUB_TOKEN'] diff --git a/psutil/_compat.py b/psutil/_compat.py index 6070c2a044..9631a7c50e 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -225,7 +225,7 @@ def FileExistsError(inst): "CacheInfo", ["hits", "misses", "maxsize", "currsize"] ) - class _HashedSeq(list): + class _HashedSeq(list): # noqa: FURB189 __slots__ = ('hashvalue',) def __init__(self, tup, hash=hash): diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 68d467b185..2b0f849be7 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -22,7 +22,15 @@ 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(): + for field in [ + "size", + "inuse", + "free", + "pin", + "virtual", + "available", + "mmode", + ]: re_pattern += r"(?P<%s>\S+)\s+" % (field,) matchobj = re.search(re_pattern, out) @@ -72,11 +80,30 @@ 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(): + 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", + ]: re_pattern += r"(?P<%s>\S+)\s+" % (field,) matchobj = re.search(re_pattern, out) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index cdcb468e10..dcfd60fbe1 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -11,7 +11,6 @@ import collections import contextlib import errno -import glob import io import os import re @@ -61,15 +60,11 @@ 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 # ===================================================================== diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index fe5151d3ed..41c9cbb89b 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -31,10 +31,6 @@ def call_oneshot(): fun() -def add_cmdline_args(cmd, args): - cmd.append(args.benchmark) - - def main(): runner = pyperf.Runner() diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py index 0e6490b395..154faeed75 100755 --- a/scripts/internal/download_wheels_appveyor.py +++ b/scripts/internal/download_wheels_appveyor.py @@ -37,12 +37,10 @@ def download_file(url): local_fname = os.path.join('dist', local_fname) os.makedirs('dist', exist_ok=True) r = requests.get(url, stream=True, timeout=TIMEOUT) - tot_bytes = 0 with open(local_fname, 'wb') as f: for chunk in r.iter_content(chunk_size=16384): if chunk: # filter out keep-alive new chunks f.write(chunk) - tot_bytes += len(chunk) return local_fname diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 84c38cccd0..1441a1454f 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -20,7 +20,6 @@ PYTHON = sys.executable PY3 = sys.version_info[0] >= 3 -THIS_SCRIPT = os.path.realpath(__file__) def term_supports_colors(): From 83590ba4d047d4a4c18a4d389d171fb0b1bf2eb2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Nov 2024 15:39:45 +0100 Subject: [PATCH 1151/1714] update issues.py crawler to detect docker --- .github/workflows/issues.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 5e8c2026f2..9566fa1194 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -62,6 +62,11 @@ "/dev/pts", "posix", ], "pypy": ["pypy"], + "docker": ["docker", "docker-compose"], + "vm": [ + "docker", "docker-compose", "vmware", "lxc", "hyperv", "virtualpc", + "virtualbox", "bhyve", "openvz", "lxc", "xen", "kvm", "qemu", "heroku", + ], # types "enhancement": ["enhancement"], "memleak": ["memory leak", "leaks memory", "memleak", "mem leak"], @@ -88,10 +93,10 @@ "continuous integration", "unittest", "pytest", "unit test", ], # critical errors - "priority-high": [ + "critical": [ "WinError", "WindowsError", "RuntimeError", "ZeroDivisionError", - "SystemError", "MemoryError", "core dumped", - "segfault", "segmentation fault", + "SystemError", "MemoryError", "core dump", "segfault", + "segmentation fault", ], } From d0473528984c1d5f6d97d30bea9aaf79fa04d4f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Nov 2024 16:09:33 +0100 Subject: [PATCH 1152/1714] Add Vulture (dead code detector) (#2471) --- .github/workflows/issues.py | 11 ++++++++--- HISTORY.rst | 6 ++++++ Makefile | 11 ++++++++--- psutil/tests/test_testutils.py | 2 +- pyproject.toml | 12 ++++++++++++ setup.py | 7 +++++-- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 5e8c2026f2..9566fa1194 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -62,6 +62,11 @@ "/dev/pts", "posix", ], "pypy": ["pypy"], + "docker": ["docker", "docker-compose"], + "vm": [ + "docker", "docker-compose", "vmware", "lxc", "hyperv", "virtualpc", + "virtualbox", "bhyve", "openvz", "lxc", "xen", "kvm", "qemu", "heroku", + ], # types "enhancement": ["enhancement"], "memleak": ["memory leak", "leaks memory", "memleak", "mem leak"], @@ -88,10 +93,10 @@ "continuous integration", "unittest", "pytest", "unit test", ], # critical errors - "priority-high": [ + "critical": [ "WinError", "WindowsError", "RuntimeError", "ZeroDivisionError", - "SystemError", "MemoryError", "core dumped", - "segfault", "segmentation fault", + "SystemError", "MemoryError", "core dump", "segfault", + "segmentation fault", ], } diff --git a/HISTORY.rst b/HISTORY.rst index f69375e45a..8d636de3ac 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,12 @@ XXXX-XX-XX +**Enhancements** + +- 2471_: use Vulture CLI tool to detect dead code. + +**Bug fixes** + - 2470_, [Linux]: `users()`_ may return "localhost" instead of the actual IP address of the user logged in. diff --git a/Makefile b/Makefile index 79ad8c8c65..8ddb7c42c4 100644 --- a/Makefile +++ b/Makefile @@ -174,9 +174,6 @@ ruff: ## Run ruff linter. black: ## Run black formatter. @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe -_pylint: ## Python pylint (not mandatory, just run it from time to time) - @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=0 - lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py @@ -193,6 +190,14 @@ lint-all: ## Run all linters ${MAKE} lint-rst ${MAKE} lint-toml +# --- not mandatory linters (just run from time to time) + +pylint: ## Python pylint + @git ls-files '*.py' | xargs $(PYTHON) -m pylint --rcfile=pyproject.toml --jobs=0 $(ARGS) + +vulture: ## Find unused code + @git ls-files '*.py' | xargs $(PYTHON) -m vulture $(ARGS) + # =================================================================== # Fixers # =================================================================== diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index e11c8f783d..1c83a94c65 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -317,7 +317,7 @@ def test_bind_unix_socket(self): with contextlib.closing(sock): assert sock.type == socket.SOCK_DGRAM - def tcp_tcp_socketpair(self): + def test_tcp_socketpair(self): addr = ("127.0.0.1", get_free_port()) server, client = tcp_socketpair(socket.AF_INET, addr=addr) with contextlib.closing(server): diff --git a/pyproject.toml b/pyproject.toml index 988ff9c019..35b5a31c05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,6 +189,18 @@ disable = [ "wrong-import-position", ] +[tool.vulture] +exclude = [ + "docs/conf.py", + "psutil/_compat.py", + "scripts/internal/winmake.py", +] +ignore_decorators = [ + "@_common.deprecated_method", + "@atexit.register", + "@pytest.fixture", +] + [tool.rstcheck] ignore_messages = [ "Duplicate explicit target name", diff --git a/setup.py b/setup.py index a7b7e81701..727677c494 100755 --- a/setup.py +++ b/setup.py @@ -64,7 +64,8 @@ CP37_PLUS = PY37_PLUS and sys.implementation.name == "cpython" Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED") -# Test deps, installable via `pip install .[test]`. +# Test deps, installable via `pip install .[test]` or +# `make install-pydeps-test`. if PY3: TEST_DEPS = [ "pytest", @@ -87,7 +88,8 @@ TEST_DEPS.append("wheel") TEST_DEPS.append("wmi") -# Development deps, installable via `pip install .[dev]`. +# Development deps, installable via `pip install .[dev]` or +# `make install-pydeps-dev`. DEV_DEPS = [ "abi3audit", "black", @@ -106,6 +108,7 @@ "toml-sort", "twine", "virtualenv", + "vulture", "wheel", ] if WINDOWS: From e15f203c5a18d2799a9c505958395b678f382b00 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Nov 2024 17:11:10 +0100 Subject: [PATCH 1153/1714] update winmake.py --- pyproject.toml | 1 + scripts/internal/winmake.py | 130 ++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35b5a31c05..18bfbfa281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -193,6 +193,7 @@ disable = [ exclude = [ "docs/conf.py", "psutil/_compat.py", + "psutil/tests/", "scripts/internal/winmake.py", ] ignore_decorators = [ diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 82a309397e..d4ec49ef6c 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -28,7 +28,7 @@ APPVEYOR = bool(os.environ.get('APPVEYOR')) PYTHON = sys.executable if APPVEYOR else os.getenv('PYTHON', sys.executable) PY3 = sys.version_info[0] >= 3 -PYTEST_ARGS = "-v -s --tb=short" +PYTEST_ARGS = ["-v", "-s", "--tb=short"] HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) WINDOWS = os.name == "nt" @@ -100,39 +100,16 @@ def win_colorprint(s, color=LIGHTBLUE): def sh(cmd, nolog=False): + assert isinstance(cmd, list), repr(cmd) if not nolog: - safe_print("cmd: " + cmd) - p = subprocess.Popen( # noqa S602 - cmd, shell=True, env=os.environ, cwd=os.getcwd() + safe_print("cmd: %s" % cmd) + return subprocess.check_output( + cmd, env=os.environ, universal_newlines=True ) - p.communicate() - if p.returncode != 0: - sys.exit(p.returncode) def rm(pattern, directory=False): """Recursively remove a file or dir by pattern.""" - - def safe_remove(path): - try: - os.remove(path) - except OSError as err: - if err.errno != errno.ENOENT: - raise - else: - safe_print("rm %s" % path) - - def safe_rmtree(path): - def onerror(fun, path, excinfo): - exc = excinfo[1] - if exc.errno != errno.ENOENT: - raise # noqa: PLE0704 - - existed = os.path.isdir(path) - shutil.rmtree(path, onerror=onerror) - if existed: - safe_print("rmdir -f %s" % path) - if "*" not in pattern: if directory: safe_rmtree(pattern) @@ -166,14 +143,9 @@ def safe_remove(path): def safe_rmtree(path): - def onerror(fun, path, excinfo): - exc = excinfo[1] - if exc.errno != errno.ENOENT: - raise # noqa: PLE0704 - existed = os.path.isdir(path) - shutil.rmtree(path, onerror=onerror) - if existed: + shutil.rmtree(path, ignore_errors=True) + if existed and not os.path.isdir(path): safe_print("rmdir -f %s" % path) @@ -202,7 +174,7 @@ def build(): """Build / compile.""" # Make sure setuptools is installed (needed for 'develop' / # edit mode). - sh('%s -c "import setuptools"' % PYTHON) + sh([PYTHON, "-c", "import setuptools"]) # "build_ext -i" copies compiled *.pyd files in ./psutil directory in # order to allow "import psutil" when using the interactive interpreter @@ -233,31 +205,31 @@ def build(): p.wait() # Make sure it actually worked. - sh('%s -c "import psutil"' % PYTHON) + sh([PYTHON, "-c", "import psutil"]) win_colorprint("build + import successful", GREEN) def wheel(): """Create wheel file.""" build() - sh("%s setup.py bdist_wheel" % PYTHON) + sh([PYTHON, "setup.py", "bdist_wheel"]) def upload_wheels(): """Upload wheel files on PyPI.""" build() - sh("%s -m twine upload dist/*.whl" % PYTHON) + sh([PYTHON, "-m", "twine", "upload", "dist/*.whl"]) def install_pip(): """Install pip.""" - sh('%s %s' % (PYTHON, os.path.join(HERE, "install_pip.py"))) + sh([PYTHON, os.path.join(HERE, "install_pip.py")]) def install(): """Install in develop / edit mode.""" build() - sh("%s setup.py develop" % PYTHON) + sh([PYTHON, "setup.py", "develop"]) def uninstall(): @@ -278,7 +250,7 @@ def uninstall(): except ImportError: break else: - sh("%s -m pip uninstall -y psutil" % PYTHON) + sh([PYTHON, "-m", "pip", "uninstall", "-y", "psutil"]) finally: os.chdir(here) @@ -336,102 +308,111 @@ def install_pydeps_test(): """Install useful deps.""" install_pip() install_git_hooks() - sh("%s -m pip install --user -U %s" % (PYTHON, " ".join(TEST_DEPS))) + sh([PYTHON, "-m", "pip", "install", "--user", "-U"] + TEST_DEPS) def install_pydeps_dev(): """Install useful deps.""" install_pip() install_git_hooks() - sh("%s -m pip install --user -U %s" % (PYTHON, " ".join(DEV_DEPS))) + sh([PYTHON, "-m", "pip", "install", "--user", "-U"] + DEV_DEPS) -def test(args=""): +def test(args=None): """Run tests.""" + if args: + assert isinstance(args, list), args build() - sh("%s -m pytest %s %s" % (PYTHON, PYTEST_ARGS, args)) + cmd = [PYTHON, "-m", "pytest"] + PYTEST_ARGS + if args: + cmd.extend(args) + sh(cmd) + + +def test_parallel(): + test(["-n", "auto", "--dist", "loadgroup"]) def coverage(): """Run coverage tests.""" # Note: coverage options are controlled by .coveragerc file build() - sh("%s -m coverage run -m pytest %s" % (PYTHON, PYTEST_ARGS)) - sh("%s -m coverage report" % PYTHON) - sh("%s -m coverage html" % PYTHON) - sh("%s -m webbrowser -t htmlcov/index.html" % PYTHON) + sh([PYTHON, "-m", "coverage", "run", "-m", "pytest"] + PYTEST_ARGS) + sh([PYTHON, "-m", "coverage", "report"]) + sh([PYTHON, "-m", "coverage", "html"]) + sh([PYTHON, "-m", "webbrowser", "-t", "htmlcov/index.html"]) def test_process(): """Run process tests.""" build() - sh("%s psutil\\tests\\test_process.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_process.py"]) def test_process_all(): """Run process all tests.""" build() - sh("%s psutil\\tests\\test_process_all.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_process_all.py"]) def test_system(): """Run system tests.""" build() - sh("%s psutil\\tests\\test_system.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_system.py"]) def test_platform(): """Run windows only tests.""" build() - sh("%s psutil\\tests\\test_windows.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_windows.py"]) def test_misc(): """Run misc tests.""" build() - sh("%s psutil\\tests\\test_misc.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_misc.py"]) def test_unicode(): """Run unicode tests.""" build() - sh("%s psutil\\tests\\test_unicode.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_unicode.py"]) def test_connections(): """Run connections tests.""" build() - sh("%s psutil\\tests\\test_connections.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_connections.py"]) def test_contracts(): """Run contracts tests.""" build() - sh("%s psutil\\tests\\test_contracts.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_contracts.py"]) def test_testutils(): """Run test utilities tests.""" build() - sh("%s psutil\\tests\\test_testutils.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_testutils.py"]) def test_by_name(name): """Run test by name.""" build() - test(name) + test([name]) def test_last_failed(): """Re-run tests which failed on last run.""" build() - test("--last-failed") + test(["--last-failed"]) def test_memleaks(): """Run memory leaks tests.""" build() - sh("%s psutil\\tests\\test_memleaks.py" % PYTHON) + sh([PYTHON, "psutil\\tests\\test_memleaks.py"]) def install_git_hooks(): @@ -450,24 +431,24 @@ def install_git_hooks(): def bench_oneshot(): """Benchmarks for oneshot() ctx manager (see #799).""" - sh("%s -Wa scripts\\internal\\bench_oneshot.py" % PYTHON) + sh([PYTHON, "scripts\\internal\\bench_oneshot.py"]) def bench_oneshot_2(): """Same as above but using perf module (supposed to be more precise).""" - sh("%s -Wa scripts\\internal\\bench_oneshot_2.py" % PYTHON) + sh([PYTHON, "scripts\\internal\\bench_oneshot_2.py"]) def print_access_denied(): """Print AD exceptions raised by all Process methods.""" build() - sh("%s -Wa scripts\\internal\\print_access_denied.py" % PYTHON) + sh([PYTHON, "scripts\\internal\\print_access_denied.py"]) def print_api_speed(): """Benchmark all API calls.""" build() - sh("%s -Wa scripts\\internal\\print_api_speed.py" % PYTHON) + sh([PYTHON, "scripts\\internal\\print_api_speed.py"]) def print_sysinfo(): @@ -480,10 +461,14 @@ def print_sysinfo(): def download_appveyor_wheels(): """Download appveyor wheels.""" - sh( - "%s -Wa scripts\\internal\\download_wheels_appveyor.py " - "--user giampaolo --project psutil" % PYTHON - ) + sh([ + PYTHON, + "scripts\\internal\\download_wheels_appveyor.py", + "--user", + "giampaolo", + "--project", + "psutil", + ]) def generate_manifest(): @@ -503,13 +488,9 @@ def get_python(path): path = path.replace('.', '') vers = ( '27', - '27-32', '27-64', - '310-32', '310-64', - '311-32', '311-64', - '312-32', '312-64', ) for v in vers: @@ -539,6 +520,7 @@ def parse_args(): sp.add_parser('print-access-denied', help="print AD exceptions") sp.add_parser('print-api-speed', help="benchmark all API calls") sp.add_parser('print-sysinfo', help="print system info") + sp.add_parser('test-parallel', help="run tests in parallel") test = sp.add_parser('test', help="[ARG] run tests") test_by_name = sp.add_parser('test-by-name', help=" run test by name") sp.add_parser('test-connections', help="run connections tests") From 0866e1ef20587dae62c1cf108d6fe69e1425817a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 20 Nov 2024 18:28:06 +0100 Subject: [PATCH 1154/1714] update winmake.py --- scripts/internal/winmake.py | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index d4ec49ef6c..ad0382f206 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -103,9 +103,10 @@ def sh(cmd, nolog=False): assert isinstance(cmd, list), repr(cmd) if not nolog: safe_print("cmd: %s" % cmd) - return subprocess.check_output( - cmd, env=os.environ, universal_newlines=True - ) + p = subprocess.Popen(cmd, env=os.environ, universal_newlines=True) + p.communicate() # print stdout/stderr in real time + if p.returncode != 0: + sys.exit(p.returncode) def rm(pattern, directory=False): @@ -318,15 +319,10 @@ def install_pydeps_dev(): sh([PYTHON, "-m", "pip", "install", "--user", "-U"] + DEV_DEPS) -def test(args=None): +def test(): """Run tests.""" - if args: - assert isinstance(args, list), args build() - cmd = [PYTHON, "-m", "pytest"] + PYTEST_ARGS - if args: - cmd.extend(args) - sh(cmd) + sh([PYTHON, "-m", "pytest"] + PYTEST_ARGS) def test_parallel(): @@ -397,12 +393,6 @@ def test_testutils(): sh([PYTHON, "psutil\\tests\\test_testutils.py"]) -def test_by_name(name): - """Run test by name.""" - build() - test([name]) - - def test_last_failed(): """Re-run tests which failed on last run.""" build() @@ -566,16 +556,10 @@ def main(): fname = args.command.replace('-', '_') fun = getattr(sys.modules[__name__], fname) # err if fun not defined - funargs = [] - # mandatory args - if args.command in ('test-by-name', 'test-script'): - if not args.arg: - sys.exit('command needs an argument') - funargs = [args.arg] - # optional args if args.command == 'test' and args.arg: - funargs = [args.arg] - fun(*funargs) + sh([PYTHON, args.arg]) # test a script + else: + fun() if __name__ == '__main__': From 13a336bd6e19f00999b52f83a0a7cf1d7dcf03de Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Nov 2024 19:49:02 +0100 Subject: [PATCH 1155/1714] fix #2418 / Linux: fix race condition --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 7 ++++++- psutil/tests/test_linux.py | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8d636de3ac..3a549e1ac7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,8 @@ XXXX-XX-XX **Bug fixes** +- 2418_, [Linux]: fix race condition in case /proc/PID/stat does not exist, but + /proc/PID does, resulting in FileNotFoundError. - 2470_, [Linux]: `users()`_ may return "localhost" instead of the actual IP address of the user logged in. diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index d7e76c4f57..e1e9bb076f 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1722,7 +1722,12 @@ def wrapper(self, *args, **kwargs): raise NoSuchProcess(self.pid, self._name) except FileNotFoundError: self._raise_if_zombie() - if not os.path.exists("%s/%s" % (self._procfs_path, self.pid)): + # /proc/PID directory may still exist, but the files within + # it may not, indicating the process is gone, see: + # https://github.com/giampaolo/psutil/issues/2418 + if not os.path.exists( + "%s/%s/stat" % (self._procfs_path, self.pid) + ): raise NoSuchProcess(self.pid, self._name) raise diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index dcfd60fbe1..03c03cf149 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -2133,6 +2133,15 @@ def test_issue_1014(self): p.memory_maps() assert m.called + def test_issue_2418(self): + p = psutil.Process() + with mock_open_exception( + '/proc/%s/statm' % os.getpid(), FileNotFoundError + ): + with mock.patch("os.path.exists", return_value=False): + with pytest.raises(psutil.NoSuchProcess): + p.memory_info() + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") def test_rlimit_zombie(self): # Emulate a case where rlimit() raises ENOSYS, which may From b5ea67e17b2cb07c74a89d16b9e58e77f6eaf4e8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Dec 2024 18:01:32 +0100 Subject: [PATCH 1156/1714] fix winmake.py test-parallel --- scripts/internal/winmake.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index ad0382f206..a0b085280e 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -319,10 +319,15 @@ def install_pydeps_dev(): sh([PYTHON, "-m", "pip", "install", "--user", "-U"] + DEV_DEPS) -def test(): +def test(args=None): """Run tests.""" build() - sh([PYTHON, "-m", "pytest"] + PYTEST_ARGS) + args = args or [] + sh( + [PYTHON, "-m", "pytest", "--ignore=psutil/tests/test_memleaks.py"] + + PYTEST_ARGS + + args + ) def test_parallel(): From 560c524cfd503ea111147fc0c0170507953ff321 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Wed, 18 Dec 2024 21:57:43 +0100 Subject: [PATCH 1157/1714] chore: bump cibuildwheel to 2.22.0, move to macos-13 (#2479) --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3290241e6..4975adc7a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: - {os: ubuntu-latest, arch: x86_64} - {os: ubuntu-latest, arch: i686} - {os: ubuntu-latest, arch: aarch64} - - {os: macos-12, arch: x86_64} + - {os: macos-13, arch: x86_64} - {os: macos-14, arch: arm64} - {os: windows-2019, arch: AMD64} - {os: windows-2019, arch: x86} @@ -53,10 +53,10 @@ jobs: if: matrix.arch == 'aarch64' - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.21.2 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_ARCHS: "${{ matrix.arch }}" - CIBW_PRERELEASE_PYTHONS: True + CIBW_ENABLE: "cpython-prerelease" CIBW_TEST_EXTRAS: test CIBW_TEST_COMMAND: make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks @@ -82,7 +82,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-12] + os: [ubuntu-latest, macos-13] env: CIBW_BUILD: 'cp27-*' CIBW_TEST_EXTRAS: test From 45934bb75a6247bc9cdf32d5aff9fdc1c603ac19 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Dec 2024 10:33:39 +0100 Subject: [PATCH 1158/1714] try to fix some flaky tests --- psutil/__init__.py | 2 +- psutil/tests/test_linux.py | 32 +++++++++++++++----------------- psutil/tests/test_process.py | 3 ++- psutil/tests/test_unicode.py | 3 +++ 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index aabf715921..96aa195d20 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -214,7 +214,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "6.1.0" +__version__ = "6.1.1" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 03c03cf149..ea9dff952f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1195,24 +1195,22 @@ def test_zfs_fs(self): 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") + return + + # 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._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 - assert ret[0].fstype == 'zfs' + '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 + assert ret[0].fstype == 'zfs' def test_emulate_realpath_fail(self): # See: https://github.com/giampaolo/psutil/issues/1307 diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0aa0b5a0d3..fa819643db 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -843,7 +843,8 @@ def test_long_name(self): @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") @pytest.mark.skipif(AIX, reason="broken on AIX") @pytest.mark.skipif(PYPY, reason="broken on PYPY") - @pytest.mark.skipif(QEMU_USER, reason="broken on QEMU user") + @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") + @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") def test_prog_w_funky_name(self): # Test that name(), exe() and cmdline() correctly handle programs # with funky chars such as spaces and ")", see: diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 1bea46b5db..c03aabd8fd 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -80,6 +80,7 @@ import psutil from psutil import BSD +from psutil import MACOS from psutil import POSIX from psutil import WINDOWS from psutil._compat import PY3 @@ -195,6 +196,7 @@ def expect_exact_path_match(self): # --- + @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") def test_proc_exe(self): cmd = [ self.funky_name, @@ -220,6 +222,7 @@ def test_proc_name(self): if self.expect_exact_path_match(): assert name == os.path.basename(self.funky_name) + @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") def test_proc_cmdline(self): cmd = [ self.funky_name, From c0e1eb1dde786eaaa34317abe09371ab6505fdeb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Dec 2024 10:47:25 +0100 Subject: [PATCH 1159/1714] try to fix some flaky tests 2 --- psutil/tests/test_process.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index fa819643db..34add6515c 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -813,6 +813,7 @@ def test_name(self): @pytest.mark.skipif(PYPY or QEMU_USER, reason="unreliable on PYPY") @pytest.mark.skipif(QEMU_USER, reason="unreliable on QEMU user") + @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) cmdline = [ @@ -845,6 +846,7 @@ def test_long_name(self): @pytest.mark.skipif(PYPY, reason="broken on PYPY") @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") + @retry_on_failure def test_prog_w_funky_name(self): # Test that name(), exe() and cmdline() correctly handle programs # with funky chars such as spaces and ")", see: From 1f3458be3139edd63d93e4a665240c90ad083c26 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Dec 2024 10:49:35 +0100 Subject: [PATCH 1160/1714] try to fix some flaky tests 2 --- psutil/tests/test_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 34add6515c..84cc5bebd9 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -846,7 +846,7 @@ def test_long_name(self): @pytest.mark.skipif(PYPY, reason="broken on PYPY") @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") - @retry_on_failure + @retry_on_failure() def test_prog_w_funky_name(self): # Test that name(), exe() and cmdline() correctly handle programs # with funky chars such as spaces and ")", see: From 8162905ae66292c7ebb1883a8c91d3e0d9a9c5c3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Dec 2024 11:58:20 +0100 Subject: [PATCH 1161/1714] disable flafy test + pre-release --- HISTORY.rst | 6 ++--- docs/index.rst | 4 ++++ psutil/tests/test_process.py | 43 ++++++++++++++++++------------------ 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3a549e1ac7..99b7f0769d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -6.1.1 (IN DEVELOPMENT) -====================== +6.1.1 +===== -XXXX-XX-XX +2024-12-19 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 3d24d735fa..a9b9dcc66f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2672,6 +2672,10 @@ PyPy3. Timeline ======== +- 2024-12-19: + `6.1.1 `__ - + `what's new `__ - + `diff `__ - 2024-10-17: `6.1.0 `__ - `what's new `__ - diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 84cc5bebd9..dec62baf7b 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -30,7 +30,6 @@ 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 @@ -840,27 +839,27 @@ def test_long_name(self): else: assert p.name() == os.path.basename(pyexe) - # XXX - @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") - @pytest.mark.skipif(AIX, reason="broken on AIX") - @pytest.mark.skipif(PYPY, reason="broken on PYPY") - @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") - @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") - @retry_on_failure() - 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(0.1) for x in range(100)]", - ] - p = self.spawn_psproc(cmdline) - assert p.cmdline() == cmdline - assert p.name() == os.path.basename(pyexe) - assert os.path.normcase(p.exe()) == os.path.normcase(pyexe) + # XXX: fails too often + # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") + # @pytest.mark.skipif(AIX, reason="broken on AIX") + # @pytest.mark.skipif(PYPY, reason="broken on PYPY") + # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") + # @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") + # @retry_on_failure() + # 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(0.1) for x in range(100)]", + # ] + # p = self.spawn_psproc(cmdline) + # assert p.cmdline() == cmdline + # assert p.name() == os.path.basename(pyexe) + # assert os.path.normcase(p.exe()) == os.path.normcase(pyexe) @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_uids(self): From 1a6340785242730cfc87fedb31700cb47f9c531c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Dec 2024 14:17:36 +0100 Subject: [PATCH 1162/1714] use a `set` literal when testing for membership --- psutil/__init__.py | 2 +- psutil/_common.py | 8 ++++---- psutil/_compat.py | 2 +- psutil/_psbsd.py | 2 +- psutil/_pslinux.py | 8 ++++---- psutil/_pssunos.py | 6 +++--- psutil/_pswindows.py | 24 ++++++++++++------------ psutil/tests/__init__.py | 18 +++++++++--------- psutil/tests/test_connections.py | 14 +++++++------- psutil/tests/test_linux.py | 2 +- psutil/tests/test_misc.py | 4 ++-- psutil/tests/test_posix.py | 2 +- psutil/tests/test_process.py | 12 ++++++------ psutil/tests/test_process_all.py | 4 ++-- psutil/tests/test_system.py | 8 ++++---- psutil/tests/test_windows.py | 4 ++-- pyproject.toml | 1 - scripts/top.py | 2 +- 18 files changed, 61 insertions(+), 62 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 96aa195d20..5648339382 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -414,7 +414,7 @@ def __str__(self): except AccessDenied: pass - if self._exitcode not in (_SENTINEL, None): + 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) diff --git a/psutil/_common.py b/psutil/_common.py index 9fd7b0cfb8..4b99b093e3 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -547,7 +547,7 @@ def isfile_strict(path): try: st = os.stat(path) except OSError as err: - if err.errno in (errno.EPERM, errno.EACCES): + if err.errno in {errno.EPERM, errno.EACCES}: raise return False else: @@ -562,7 +562,7 @@ def path_exists_strict(path): try: os.stat(path) except OSError as err: - if err.errno in (errno.EPERM, errno.EACCES): + if err.errno in {errno.EPERM, errno.EACCES}: raise return False else: @@ -639,12 +639,12 @@ def socktype_to_enum(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 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): + 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 diff --git a/psutil/_compat.py b/psutil/_compat.py index 9631a7c50e..92e01713c3 100644 --- a/psutil/_compat.py +++ b/psutil/_compat.py @@ -180,7 +180,7 @@ def ProcessLookupError(inst): @_instance_checking_exception(EnvironmentError) def PermissionError(inst): - return getattr(inst, 'errno', _SENTINEL) in (errno.EACCES, errno.EPERM) + return getattr(inst, 'errno', _SENTINEL) in {errno.EACCES, errno.EPERM} @_instance_checking_exception(EnvironmentError) def InterruptedError(inst): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index deffe50b09..77b99c0fd8 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -958,7 +958,7 @@ def cpu_affinity_set(self, cpus): # <> - if err.errno in (errno.EINVAL, errno.EDEADLK): + if err.errno in {errno.EINVAL, errno.EDEADLK}: for cpu in cpus: if cpu not in allcpus: raise ValueError( diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index e1e9bb076f..dd2a76baef 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1040,7 +1040,7 @@ def retrieve(self, kind, pid=None): ret = set() 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): + if family in {socket.AF_INET, socket.AF_INET6}: ls = self.process_inet( path, family, type_, inodes, filter_pid=pid ) @@ -1358,7 +1358,7 @@ def disk_partitions(all=False): device, mountpoint, fstype, opts = partition if device == 'none': device = '' - if device in ("/dev/root", "rootfs"): + if device in {"/dev/root", "rootfs"}: device = RootFsDeviceFinder().find() or device if not all: if not device or fstype not in fstypes: @@ -1589,7 +1589,7 @@ def multi_bcat(*paths): status = cat(root + "/status", fallback="").strip().lower() if status == "discharging": power_plugged = False - elif status in ("charging", "full"): + elif status in {"charging", "full"}: power_plugged = True # Seconds left. @@ -2256,7 +2256,7 @@ def ionice_get(self): def ionice_set(self, ioclass, value): if value is None: value = 0 - if value and ioclass in (IOPRIO_CLASS_IDLE, IOPRIO_CLASS_NONE): + 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" diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 5536d35079..a38d939d79 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -283,7 +283,7 @@ def net_connections(kind, _pid=-1): if type_ not in types: continue # TODO: refactor and use _common.conn_to_ntuple. - if fam in (AF_INET, AF_INET6): + if fam in {AF_INET, AF_INET6}: if laddr: laddr = _common.addr(*laddr) if raddr: @@ -476,7 +476,7 @@ def nice_get(self): @wrap_exceptions def nice_set(self, value): - if self.pid in (2, 3): + if self.pid in {2, 3}: # Special case PIDs: internally setpriority(3) return ESRCH # (no such process), no matter what. # The process actually exists though, as it has a name, @@ -678,7 +678,7 @@ def net_connections(self, kind='inet'): os.stat('%s/%s' % (self._procfs_path, self.pid)) # UNIX sockets - if kind in ('all', 'unix'): + if kind in {'all', 'unix'}: ret.extend([ _common.pconn(*conn) for conn in self._get_unix_sockets(self.pid) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e4550c3914..dfadbf4933 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -575,10 +575,10 @@ def _wrap_exceptions(self): % self._name ) raise AccessDenied(pid=None, name=self._name, msg=msg) - elif err.winerror in ( + 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: @@ -697,15 +697,15 @@ def as_dict(self): 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): + 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). - return getattr(exc, "winerror", -1) in ( + return getattr(exc, "winerror", -1) in { cext.ERROR_ACCESS_DENIED, cext.ERROR_PRIVILEGE_NOT_HELD, - ) + } def convert_oserror(exc, pid=None, name=None): @@ -919,10 +919,10 @@ def send_signal(self, sig): if sig == signal.SIGTERM: cext.proc_kill(self.pid) # py >= 2.7 - elif sig in ( + elif sig in { getattr(signal, "CTRL_C_EVENT", object()), getattr(signal, "CTRL_BREAK_EVENT", object()), - ): + }: os.kill(self.pid, sig) else: msg = ( @@ -976,7 +976,7 @@ def wait(self, timeout=None): @wrap_exceptions def username(self): - if self.pid in (0, 4): + if self.pid in {0, 4}: return 'NT AUTHORITY\\SYSTEM' domain, user = cext.proc_username(self.pid) return py2_strencode(domain) + '\\' + py2_strencode(user) @@ -1034,7 +1034,7 @@ def resume(self): @wrap_exceptions @retry_error_partial_copy def cwd(self): - if self.pid in (0, 4): + if self.pid in {0, 4}: raise AccessDenied(self.pid, self._name) # return a normalized pathname since the native C function appends # "\\" at the and of the path @@ -1043,7 +1043,7 @@ def cwd(self): @wrap_exceptions def open_files(self): - if self.pid in (0, 4): + if self.pid in {0, 4}: return [] ret = set() # Filenames come in in native format like: @@ -1087,12 +1087,12 @@ def ionice_set(self, ioclass, value): if value: msg = "value argument not accepted on Windows" raise TypeError(msg) - if ioclass not in ( + 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) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index dd3d536472..f2cceeac55 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -205,7 +205,7 @@ def macos_version(): 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') +ASCII_FS = sys.getfilesystemencoding().lower() in {'ascii', 'us-ascii'} # --- paths @@ -1739,11 +1739,11 @@ def get_free_port(host='127.0.0.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): + 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'): + if os.name not in {'nt', 'cygwin'}: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(addr) if type == socket.SOCK_STREAM: @@ -1874,7 +1874,7 @@ def check_connection_ntuple(conn): def check_ntuple(conn): has_pid = len(conn) == 7 - assert len(conn) in (6, 7), len(conn) + 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 @@ -1885,7 +1885,7 @@ def check_ntuple(conn): assert conn[6] == conn.pid, conn.pid def check_family(conn): - assert conn.family in (AF_INET, AF_INET6, AF_UNIX), conn.family + 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: @@ -1908,11 +1908,11 @@ def check_family(conn): 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 ( + assert conn.type in { socket.SOCK_STREAM, socket.SOCK_DGRAM, SOCK_SEQPACKET, - ), conn.type + }, conn.type if enum is not None: assert isinstance(conn.type, enum.IntEnum), conn else: @@ -1923,7 +1923,7 @@ def check_type(conn): 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): + if conn.family in {AF_INET, AF_INET6}: assert isinstance(addr, tuple), type(addr) if not addr: continue @@ -1939,7 +1939,7 @@ def check_status(conn): 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: + 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 diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index c9256a17f8..bca12ff4b6 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -49,7 +49,7 @@ def this_proc_net_connections(kind): cons = psutil.Process().net_connections(kind=kind) - if kind in ("all", "unix"): + if kind in {"all", "unix"}: return filter_proc_net_connections(cons) return cons @@ -430,7 +430,7 @@ def test_count(self): cons = this_proc_net_connections(kind='tcp') assert len(cons) == (2 if supports_ipv6() else 1) for conn in cons: - assert conn.family in (AF_INET, AF_INET6) + assert conn.family in {AF_INET, AF_INET6} assert conn.type == SOCK_STREAM # tcp4 cons = this_proc_net_connections(kind='tcp4') @@ -447,7 +447,7 @@ def test_count(self): cons = this_proc_net_connections(kind='udp') assert len(cons) == (2 if supports_ipv6() else 1) for conn in cons: - assert conn.family in (AF_INET, AF_INET6) + assert conn.family in {AF_INET, AF_INET6} assert conn.type == SOCK_DGRAM # udp4 cons = this_proc_net_connections(kind='udp4') @@ -464,15 +464,15 @@ def test_count(self): cons = this_proc_net_connections(kind='inet') assert len(cons) == (4 if supports_ipv6() else 2) for conn in cons: - assert conn.family in (AF_INET, AF_INET6) - assert conn.type in (SOCK_STREAM, SOCK_DGRAM) + assert conn.family in {AF_INET, AF_INET6} + assert conn.type in {SOCK_STREAM, SOCK_DGRAM} # inet6 if supports_ipv6(): cons = this_proc_net_connections(kind='inet6') assert len(cons) == 2 for conn in cons: assert conn.family == AF_INET6 - assert conn.type in (SOCK_STREAM, SOCK_DGRAM) + assert conn.type in {SOCK_STREAM, SOCK_DGRAM} # Skipped on BSD becayse by default the Python process # creates a UNIX socket to '/var/run/log'. if HAS_NET_CONNECTIONS_UNIX and not (FREEBSD or NETBSD): @@ -480,7 +480,7 @@ def test_count(self): assert len(cons) == 3 for conn in cons: assert conn.family == AF_UNIX - assert conn.type in (SOCK_STREAM, SOCK_DGRAM) + assert conn.type in {SOCK_STREAM, SOCK_DGRAM} @pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index ea9dff952f..15eaf5e2ed 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1556,7 +1556,7 @@ def test_users(self): # Make sure the C extension converts ':0' and ':0.0' to # 'localhost'. for user in psutil.users(): - assert user.host not in (":0", ":0.0") + assert user.host not in {":0", ":0.0"} def test_procfs_path(self): tdir = self.get_testfn() diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index ca189a97b7..cf98f8b4bf 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -215,14 +215,14 @@ class TestMisc(PsutilTestCase): def test__all__(self): dir_psutil = dir(psutil) for name in dir_psutil: - if name in ( + if name in { 'debug', 'long', 'tests', 'test', 'PermissionError', 'ProcessLookupError', - ): + }: continue if not name.startswith('_'): try: diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 6f7f9790fc..12afa61927 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -280,7 +280,7 @@ def test_create_time(self): round_time_psutil_tstamp = datetime.datetime.fromtimestamp( round_time_psutil ).strftime("%H:%M:%S") - assert time_ps in [time_psutil_tstamp, round_time_psutil_tstamp] + assert time_ps in {time_psutil_tstamp, round_time_psutil_tstamp} def test_exe(self): ps_pathname = ps_name(self.pid) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index dec62baf7b..bdf860a102 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -687,7 +687,7 @@ def test_memory_maps(self): value = getattr(nt, fname) if fname == 'path': continue - if fname in ('addr', 'perms'): + if fname in {'addr', 'perms'}: assert value, value else: assert isinstance(value, (int, long)) @@ -923,11 +923,11 @@ def cleanup(init): # even if the function succeeds. For higher # priorities, we match either the expected # value or the highest so far. - if prio in ( + 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 assert new_prio == highest_prio @@ -1390,12 +1390,12 @@ def assert_raises_nsp(fun, fun_name): except psutil.NoSuchProcess: pass except psutil.AccessDenied: - if OPENBSD and fun_name in ('threads', 'num_threads'): + 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'): + if WINDOWS and fun_name in {'exe', 'name'}: return raise self.fail( "%r didn't raise NSP and returned %r instead" % (fun, ret) @@ -1520,7 +1520,7 @@ def test_pid_0(self): except psutil.AccessDenied: pass else: - if name in ("uids", "gids"): + if name in {"uids", "gids"}: assert ret.real == 0 elif name == "username": user = 'NT AUTHORITY\\SYSTEM' if WINDOWS else 'root' diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 1550046bad..780ea194b6 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -321,7 +321,7 @@ def memory_full_info(self, ret, info): value = getattr(ret, name) assert isinstance(value, (int, long)) assert value >= 0 - if LINUX or (OSX and name in ('vms', 'data')): + 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 @@ -343,7 +343,7 @@ def open_files(self, ret, info): assert isinstance(f.mode, str) assert isinstance(f.flags, int) assert f.position >= 0 - assert f.mode in ('r', 'w', 'a', 'r+', 'a+') + assert f.mode in {'r', 'w', 'a', 'r+', 'a+'} assert f.flags > 0 elif BSD and not f.path: # XXX see: https://github.com/giampaolo/psutil/issues/595 diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 68074ae14e..e4dfe79d60 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -589,7 +589,7 @@ def test_cpu_stats(self): value = getattr(infos, name) assert value >= 0 # on AIX, ctx_switches is always 0 - if not AIX and name in ('ctx_switches', 'interrupts'): + if not AIX and name in {'ctx_switches', 'interrupts'}: assert value > 0 # TODO: remove this once 1892 is fixed @@ -703,7 +703,7 @@ def check_ntuple(nt): continue # http://mail.python.org/pipermail/python-dev/ # 2012-June/120787.html - if err.errno not in (errno.EPERM, errno.EACCES): + if err.errno not in {errno.EPERM, errno.EACCES}: raise else: assert os.path.exists(disk.mountpoint), disk @@ -965,10 +965,10 @@ def test_sensors_battery(self): ret = psutil.sensors_battery() assert ret.percent >= 0 assert ret.percent <= 100 - if ret.secsleft not in ( + if ret.secsleft not in { psutil.POWER_TIME_UNKNOWN, psutil.POWER_TIME_UNLIMITED, - ): + }: assert ret.secsleft >= 0 else: if ret.secsleft == psutil.POWER_TIME_UNLIMITED: diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index b11b0e7067..161e2f35d8 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -401,7 +401,7 @@ def test_special_pid(self): 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'): + if platform.uname()[1] not in {'vista', 'win-7', 'win7'}: raise else: assert rss > 0 @@ -637,7 +637,7 @@ def test_memory_vms(self): # 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): + if vms not in {wmi_usage, wmi_usage * 1024}: raise self.fail("wmi=%s, psutil=%s" % (wmi_usage, vms)) def test_create_time(self): diff --git a/pyproject.toml b/pyproject.toml index 18bfbfa281..429c4a7882 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,6 @@ ignore = [ "PLR1704", # Redefining argument with the local name `type_` "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable "PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation - "PLR6201", # Use a `set` literal when testing for membership "PLR6301", # Method `x` could be a function, class method, or static method "PLW0603", # Using the global statement to update `lineno` is discouraged "PLW1514", # `open` in text mode without explicit `encoding` argument diff --git a/scripts/top.py b/scripts/top.py index c0687ae1fe..db206e8965 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -161,7 +161,7 @@ def get_dashes(perc): for x, y in procs_status.items(): if y: st.append("%s=%s" % (x, y)) - st.sort(key=lambda x: x[:3] in ('run', 'sle'), reverse=1) + st.sort(key=lambda x: x[:3] in {'run', 'sle'}, reverse=1) printl(" Processes: %s (%s)" % (num_procs, ', '.join(st))) # load average, uptime uptime = datetime.datetime.now() - datetime.datetime.fromtimestamp( From 4ba6ad065234f12b1a8fe80f88fa8bb737093313 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Dec 2024 16:57:46 +0100 Subject: [PATCH 1163/1714] ruff: enable PLR5501 (Use `elif` instead of `else` then `if`, to reduce indentation) --- psutil/_pslinux.py | 7 +++---- psutil/_pswindows.py | 7 +++---- psutil/tests/test_posix.py | 7 +++---- psutil/tests/test_process.py | 15 +++++++-------- psutil/tests/test_system.py | 5 ++--- pyproject.toml | 1 - 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index dd2a76baef..c9c97f6ec4 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -706,11 +706,10 @@ def cpu_count_cores(): except KeyError: pass current_info = {} - else: + elif line.startswith((b'physical id', b'cpu cores')): # ongoing section - if line.startswith((b'physical id', b'cpu cores')): - key, value = line.split(b'\t:', 1) - current_info[key] = int(value) + key, value = line.split(b'\t:', 1) + current_info[key] = int(value) result = sum(mapping.values()) return result or None # mimic os.cpu_count() diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index dfadbf4933..e39ba711fa 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -218,11 +218,10 @@ def py2_strencode(s): """ if PY3: return s + if isinstance(s, str): + return s else: - if isinstance(s, str): - return s - else: - return s.encode(ENCODING, ENCODING_ERRS) + return s.encode(ENCODING, ENCODING_ERRS) @memoize diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 12afa61927..551eaec50c 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -56,11 +56,10 @@ def ps(fmt, pid=None): if pid is not None: cmd.extend(['-p', str(pid)]) + elif SUNOS or AIX: + cmd.append('-A') else: - if SUNOS or AIX: - cmd.append('-A') - else: - cmd.append('ax') + cmd.append('ax') if SUNOS: fmt = fmt.replace("start", "stime") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index bdf860a102..76dcbbf32a 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -671,17 +671,16 @@ def test_memory_maps(self): data = f.read() if "%s (deleted)" % nt.path not in data: raise - else: + elif '64' not in os.path.basename(nt.path): # 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 + 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) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index e4dfe79d60..0b69ada78f 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -970,9 +970,8 @@ def test_sensors_battery(self): psutil.POWER_TIME_UNLIMITED, }: assert ret.secsleft >= 0 - else: - if ret.secsleft == psutil.POWER_TIME_UNLIMITED: - assert ret.power_plugged + elif ret.secsleft == psutil.POWER_TIME_UNLIMITED: + assert ret.power_plugged assert isinstance(ret.power_plugged, bool) @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") diff --git a/pyproject.toml b/pyproject.toml index 429c4a7882..fea968df43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,6 @@ ignore = [ "PLR1702", # Too many nested blocks (x > y) "PLR1704", # Redefining argument with the local name `type_` "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable - "PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation "PLR6301", # Method `x` could be a function, class method, or static method "PLW0603", # Using the global statement to update `lineno` is discouraged "PLW1514", # `open` in text mode without explicit `encoding` argument From fbb6d9ce98f930d3d101b7df5a4f4d0f1d2b35a3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Dec 2024 19:23:38 +0100 Subject: [PATCH 1164/1714] cause appveyor run --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 7488400d91..31f4de6755 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# Build: 3 (bump this up by 1 to force an appveyor run) +# Build: 4 (bump this up by 1 to force an appveyor run) os: Visual Studio 2015 # avoid 2 builds when pushing on PRs From e95762885e7ca6eab73b92706f7f5c19bcb55d0d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 11:05:28 +0100 Subject: [PATCH 1165/1714] Drop Python 2.7 support (#2481) About dropping Python 2.7 support, 3 years ago [I stated](https://github.com/giampaolo/psutil/issues/2014#issuecomment-969263432): > [...] not a chance, for many years to come. [Python 2.7] currently represents 7-10% of total downloads, aka around 70k /100k downloads per day 3 years later (and to my surprise) **downloads for Python 2.7 dropped to 0.36%**. These are downloads per month: ``` $ pypinfo --percent psutil pyversion Served from cache: False Data processed: 4.65 GiB Data billed: 4.65 GiB Estimated cost: $0.03 | python_version | percent | download_count | | -------------- | ------- | -------------- | | 3.10 | 23.84% | 26,354,506 | | 3.8 | 18.87% | 20,862,015 | | 3.7 | 17.38% | 19,217,960 | | 3.9 | 17.00% | 18,798,843 | | 3.11 | 13.63% | 15,066,706 | | 3.12 | 7.01% | 7,754,751 | | 3.13 | 1.15% | 1,267,008 | | 3.6 | 0.73% | 803,189 | | 2.7 | 0.36% | 402,111 | | 3.5 | 0.03% | 28,656 | | Total | | 110,555,745 | ``` According to [pypistats.org](https://archive.is/wip/knzql) it's 0.28% of the total, and around 15.000 downloads per day. Maintaining 2.7 support has become increasingly difficult, but still possible. E.g. we can still run tests by using [old PYPI backports](https://github.com/giampaolo/psutil/blob/fbb6d9ce98f930d3d101b7df5a4f4d0f1d2b35a3/setup.py#L76-L85). GitHub Actions can still be [tweaked](https://github.com/giampaolo/psutil/blob/fbb6d9ce98f930d3d101b7df5a4f4d0f1d2b35a3/.github/workflows/build.yml#L77-L112) to run tests and produce wheels on Linux and macOS. Not Windows though, for which we have to use a separate service (Appveyor). Still, the amount of hacks in psutil source code necessary to support Python 2.7 piled up over the years, and became quite big. Some disadvantages that come to mind: * (high) having to maintain various python compatibility layers (e.g. [psutil/_compat.py](https://github.com/giampaolo/psutil/blob/fbb6d9ce98f930d3d101b7df5a4f4d0f1d2b35a3/psutil/_compat.py#L1)) + all the compromises that come with it (extra imports, extra code, str vs. unicode differences, etc.) * (medium) having to maintain a C compatibility layer (`#if PY_MAJOR_VERSION <= 3`, etc.) * (medium) inability to use modern language features, especially f-strings * (low) inability to freely use `enum`s, which creates a difference on how CONSTANTS are exposed in terms of API * (medium) having to install a specific version of pip and other (outdated) [deps](https://github.com/giampaolo/psutil/blob/fbb6d9ce98f930d3d101b7df5a4f4d0f1d2b35a3/setup.py#L76-L85) * (high) relying on third-party Appveyor CI service, just to run tests on python 2.7 and produce wheels, when we could rely on a single CI service instead (GitHub) * (high) soon I want to distribute wheels via GitHub instead of manually via `twine`, so that'll be a problem (CC @potiuk) * (high) gradual lack of support from third-party libraries and services * (medium) 4 extra CI jobs which are run on every commit (Linux, macOS, Windows 32-bit, Windows 64-bit) * (medium) the distribution of 7 wheels specific for Python 2.7. From last release: * psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl * psutil-6.1.1-cp27-none-win32.whl * psutil-6.1.1-cp27-none-win_amd64.whl * psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl * psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl * psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl * psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl * etc. As such I decided to finally **drop support for Python 2.7**. Current psutil 6.1.1 release will still support Python 2.7, but next 7.0.0 will not. We can still make a promise that the 6.1.* line (EDIT: see [python2 branch](https://github.com/giampaolo/psutil/tree/python2)) will keep supporting Python 2.7 and will **receive critical bug-fixes only** (no new features). In 7.0.0 we can keep the [setup.py](https://github.com/giampaolo/psutil/blob/fbb6d9ce98f930d3d101b7df5a4f4d0f1d2b35a3/setup.py) script compatible with Python 2.7 in terms of syntax, so that it can emit an informative error message on pip install. E.g. the user will see something like this: ``` $ pip2 install psutil As of version 7.0.0 psutil no longer supports Python 2.7. Latest version supporting Python 2.7 is psutil 6.1.X. Install it with: "pip2 install psutil==6.1.*". ``` Related tickets: * 2017-06: https://github.com/giampaolo/psutil/issues/1053 * 2022-04: https://github.com/giampaolo/psutil/pull/2099 * 2023-04: https://github.com/giampaolo/psutil/pull/2246 * https://github.com/giampaolo/pyftpdlib/pull/635 --- .github/workflows/build.yml | 57 +-- .github/workflows/issues.py | 5 +- HISTORY.rst | 14 + INSTALL.rst | 1 - MANIFEST.in | 6 +- Makefile | 21 +- README.rst | 11 +- appveyor.yml | 83 --- docs/conf.py | 2 - docs/index.rst | 18 +- make.bat | 1 - psutil/__init__.py | 71 +-- psutil/_common.py | 128 ++--- psutil/_compat.py | 477 ------------------ psutil/_psaix.py | 25 +- psutil/_psbsd.py | 8 +- psutil/_pslinux.py | 170 ++----- psutil/_psosx.py | 2 - psutil/_psposix.py | 57 +-- psutil/_pssunos.py | 21 +- psutil/_psutil_aix.c | 28 +- psutil/_psutil_bsd.c | 46 +- psutil/_psutil_common.c | 9 - psutil/_psutil_common.h | 21 +- psutil/_psutil_linux.c | 51 +- psutil/_psutil_osx.c | 45 +- psutil/_psutil_posix.c | 47 +- psutil/_psutil_sunos.c | 27 +- psutil/_psutil_windows.c | 35 +- psutil/_pswindows.py | 155 ++---- psutil/arch/freebsd/proc.c | 4 - psutil/arch/linux/proc.c | 12 - psutil/arch/osx/disk.c | 6 - psutil/arch/windows/disk.c | 29 +- psutil/arch/windows/net.c | 11 +- psutil/arch/windows/proc.c | 4 - psutil/tests/__init__.py | 173 ++----- psutil/tests/test_bsd.py | 19 +- psutil/tests/test_connections.py | 5 +- psutil/tests/test_contracts.py | 22 +- psutil/tests/test_linux.py | 253 ++++------ psutil/tests/test_memleaks.py | 3 - psutil/tests/test_misc.py | 52 +- psutil/tests/test_posix.py | 9 +- psutil/tests/test_process.py | 70 +-- psutil/tests/test_process_all.py | 27 +- psutil/tests/test_system.py | 27 +- psutil/tests/test_testutils.py | 28 +- psutil/tests/test_unicode.py | 75 +-- psutil/tests/test_windows.py | 55 +- pyproject.toml | 24 +- scripts/battery.py | 1 - scripts/cpu_distribution.py | 5 +- scripts/fans.py | 1 - scripts/ifconfig.py | 1 - .../internal/appveyor_run_with_compiler.cmd | 89 ---- scripts/internal/bench_oneshot.py | 2 - scripts/internal/check_broken_links.py | 3 +- scripts/internal/clinter.py | 1 - ...ad_wheels_github.py => download_wheels.py} | 17 - scripts/internal/download_wheels_appveyor.py | 115 ----- scripts/internal/generate_manifest.py | 2 +- scripts/internal/git_pre_commit.py | 9 +- scripts/internal/install_pip.py | 12 +- scripts/internal/print_access_denied.py | 2 - scripts/internal/print_announce.py | 3 +- scripts/internal/print_api_speed.py | 2 - scripts/internal/print_downloads.py | 3 +- scripts/internal/print_timeline.py | 2 +- scripts/internal/test_python2_setup_py.py | 39 ++ scripts/internal/winmake.py | 31 +- scripts/iotop.py | 4 +- scripts/pidof.py | 1 - scripts/pmap.py | 10 +- scripts/procsmem.py | 1 - scripts/ps.py | 4 +- scripts/pstree.py | 1 - scripts/sensors.py | 2 - scripts/temperatures.py | 2 - setup.py | 85 ++-- 80 files changed, 709 insertions(+), 2291 deletions(-) delete mode 100644 appveyor.yml delete mode 100644 psutil/_compat.py delete mode 100644 scripts/internal/appveyor_run_with_compiler.cmd rename scripts/internal/{download_wheels_github.py => download_wheels.py} (78%) delete mode 100755 scripts/internal/download_wheels_appveyor.py create mode 100755 scripts/internal/test_python2_setup_py.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4975adc7a3..f609ee6513 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,7 @@ # Runs CI tests and generates wheels on the following platforms: -# -# * Linux (py2 and py3) -# * macOS (py2 and py3) -# * Windows (py3, py2 is done by appveyor) +# * Linux +# * macOS +# * Windows # # Useful URLs: # * https://github.com/pypa/cibuildwheel @@ -16,9 +15,10 @@ concurrency: group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} cancel-in-progress: true jobs: - # Linux + macOS + Windows Python 3 - py3: - name: "py3, ${{ matrix.os }}, ${{ matrix.arch }}" + + # Run tests on Linux, macOS, Windows + tests: + name: "tests, ${{ matrix.os }}, ${{ matrix.arch }}" runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: @@ -64,7 +64,7 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-py3-${{ matrix.os }}-${{ matrix.arch }} + name: wheels-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse - name: Generate .tar.gz @@ -74,42 +74,19 @@ jobs: python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - # Linux + macOS + Python 2 - py2: - name: py2, ${{ matrix.os }} - runs-on: ${{ matrix.os }} + # Test python 2.7 fallback installation message produced by setup.py + py2-fallback: + name: py2.7 setup.py check + runs-on: ubuntu-24.04 timeout-minutes: 20 strategy: fail-fast: false - matrix: - os: [ubuntu-latest, macos-13] - env: - CIBW_BUILD: 'cp27-*' - CIBW_TEST_EXTRAS: test - CIBW_TEST_COMMAND: - make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks - steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Create wheels + run tests - uses: pypa/cibuildwheel@v1.12.0 - - - name: Upload wheels - uses: actions/upload-artifact@v4 + - uses: LizardByte/setup-python-action@master with: - name: wheels-py2-${{ matrix.os }} - path: wheelhouse - - - name: Generate .tar.gz - if: matrix.os == 'ubuntu-latest' - run: | - make generate-manifest - python setup.py sdist - mv dist/psutil*.tar.gz wheelhouse/ + python-version: '2.7' + - run: python scripts/internal/test_python2_setup_py.py # Run linters linters: @@ -124,9 +101,9 @@ jobs: python3 -m pip install ruff black rstcheck toml-sort sphinx make lint-all - # upload weels as a single artefact + # Produce wheels as artifacts. upload-wheels: - needs: [py2, py3] + needs: [tests] runs-on: ubuntu-latest steps: - uses: actions/upload-artifact/merge@v4 diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 9566fa1194..5efe6fc324 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -9,7 +9,6 @@ on the situation. """ -from __future__ import print_function import functools import json @@ -42,7 +41,7 @@ "windows", "win32", "WinError", "WindowsError", "win10", "win7", "win ", "mingw", "msys", "studio", "microsoft", "make.bat", "CloseHandle", "GetLastError", "NtQuery", "DLL", "MSVC", "TCHAR", - "WCHAR", ".bat", "OpenProcess", "TerminateProcess", "appveyor", + "WCHAR", ".bat", "OpenProcess", "TerminateProcess", "windows error", "NtWow64", "NTSTATUS", "Visual Studio", ], "macos": [ @@ -89,7 +88,7 @@ ], # tests "tests": [ - " test ", "tests", "travis", "coverage", "cirrus", "appveyor", + " test ", "tests", "travis", "coverage", "cirrus", "continuous integration", "unittest", "pytest", "unit test", ], # critical errors diff --git a/HISTORY.rst b/HISTORY.rst index 99b7f0769d..33df44fc68 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,19 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.0.0 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +**Enhancements** + +- 2480_: Dropped Python 2.7 support. + +**Compatibility notes** + +- 2480_: Python 2.7 is no longer supported. Latest version supporting Python + 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. + 6.1.1 ===== diff --git a/INSTALL.rst b/INSTALL.rst index 8e06d400ea..86972586c4 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -116,7 +116,6 @@ Troubleshooting Install pip ----------- -Pip is shipped by default with Python 2.7.9+ and 3.4+. If you don't have pip you can install it with wget:: wget https://bootstrap.pypa.io/get-pip.py -O - | python3 diff --git a/MANIFEST.in b/MANIFEST.in index 91e5e2accb..5ec1cdd9e9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -24,7 +24,6 @@ include docs/requirements.txt include make.bat include psutil/__init__.py include psutil/_common.py -include psutil/_compat.py include psutil/_psaix.py include psutil/_psbsd.py include psutil/_pslinux.py @@ -177,14 +176,12 @@ include scripts/fans.py include scripts/free.py include scripts/ifconfig.py include scripts/internal/README -include scripts/internal/appveyor_run_with_compiler.cmd include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/check_broken_links.py include scripts/internal/clinter.py include scripts/internal/convert_readme.py -include scripts/internal/download_wheels_appveyor.py -include scripts/internal/download_wheels_github.py +include scripts/internal/download_wheels.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py include scripts/internal/install-sysdeps.sh @@ -197,6 +194,7 @@ include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_timeline.py include scripts/internal/purge_installation.py +include scripts/internal/test_python2_setup_py.py include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py diff --git a/Makefile b/Makefile index 8ddb7c42c4..2c7d050ce9 100644 --- a/Makefile +++ b/Makefile @@ -6,13 +6,6 @@ PYTHON = python3 ARGS = -# "python3 setup.py build" can be parallelized on Python >= 3.6. -SETUP_BUILD_EXT_ARGS = `$(PYTHON) -c \ - "import sys, os; \ - py36 = sys.version_info[:2] >= (3, 6); \ - cpus = os.cpu_count() or 1 if py36 else 1; \ - print('--parallel %s' % cpus if cpus > 1 else '')"` - # In not in a virtualenv, add --user options for install commands. SETUP_INSTALL_ARGS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` @@ -55,6 +48,7 @@ clean: ## Remove all build files. dist/ \ docs/_build/ \ htmlcov/ \ + pytest-cache* \ wheelhouse .PHONY: build @@ -62,7 +56,7 @@ build: ## Compile (in parallel) without installing. @# "build_ext -i" copies compiled *.so files in ./psutil directory in order @# to allow "import psutil" when using the interactive interpreter from @# within this directory. - $(PYTHON_ENV_VARS) $(PYTHON) setup.py build_ext -i $(SETUP_BUILD_EXT_ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) setup.py build_ext -i --parallel 4 $(PYTHON_ENV_VARS) $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. @@ -224,12 +218,8 @@ sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist -download-wheels-github: ## Download latest wheels hosted on github. - $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels_github.py --tokenfile=~/.github.token - ${MAKE} print-dist - -download-wheels-appveyor: ## Download latest wheels hosted on appveyor. - $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels_appveyor.py +download-wheels: ## Download latest wheels hosted on github. + $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token ${MAKE} print-dist create-wheels: ## Create .whl files @@ -265,8 +255,7 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in doc, '%r not found in docs/index.rst' % ver; \ assert ver in history, '%r not found in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX found in HISTORY.rst';" - ${MAKE} download-wheels-github - ${MAKE} download-wheels-appveyor + ${MAKE} download-wheels ${MAKE} check-wheels ${MAKE} print-hashes ${MAKE} print-dist diff --git a/README.rst b/README.rst index 3fc6e601b1..16c756c50e 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ | |downloads| |stars| |forks| |contributors| |coverage| | |version| |py-versions| |packages| |license| -| |github-actions-wheels| |github-actions-bsd| |appveyor| |doc| |twitter| |tidelift| +| |github-actions-wheels| |github-actions-bsd| |doc| |twitter| |tidelift| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://pepy.tech/project/psutil @@ -26,10 +26,6 @@ :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests :alt: FreeBSD, NetBSD, OpenBSD -.. |appveyor| image:: https://img.shields.io/appveyor/build/giampaolo/psutil/master.svg?maxAge=3600&label=Windows%20(py2) - :target: https://ci.appveyor.com/project/giampaolo/psutil - :alt: Windows (Appveyor) - .. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master :target: https://coveralls.io/github/giampaolo/psutil?branch=master :alt: Test coverage (coverall.io) @@ -98,8 +94,9 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are **2.7**, **3.6+** and -`PyPy `__. +Supported Python versions are cPython 3.6+ and `PyPy `__. +Latest psutil version supporting Python 2.7 is +`psutil 6.1.1 `__. Funding ======= diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 31f4de6755..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,83 +0,0 @@ -# Build: 4 (bump this up by 1 to force an appveyor run) - -os: Visual Studio 2015 -# avoid 2 builds when pushing on PRs -skip_branch_with_pr: true -# avoid build on new GIT tag -skip_tags: true -matrix: - # stop build on first failure - fast_finish: true -environment: - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script interpreter - # See: http://stackoverflow.com/a/13751649/163740 - WITH_COMPILER: "cmd /E:ON /V:ON /C .\\scripts\\internal\\appveyor_run_with_compiler.cmd" - PYTHONWARNINGS: always - PYTHONUNBUFFERED: 1 - PSUTIL_DEBUG: 1 - matrix: - # 32 bits - - - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7.x" - PYTHON_ARCH: "32" - - # 64 bits - - - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.x" - PYTHON_ARCH: "64" - - -init: - - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" - -install: - - "%WITH_COMPILER% %PYTHON%/python.exe -m pip --version" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install-pydeps-test" - - "%WITH_COMPILER% %PYTHON%/python.exe -m pip freeze" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py install" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py print-sysinfo" - -build: off - -test_script: - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py test-memleaks" - -after_test: - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/winmake.py wheel" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_hashes.py dist" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_access_denied.py" - - "%WITH_COMPILER% %PYTHON%/python.exe scripts/internal/print_api_speed.py" - -artifacts: - - path: dist\* - -cache: - - '%LOCALAPPDATA%\pip\Cache' - -# on_success: -# - might want to upload the content of dist/*.whl to a public wheelhouse - -skip_commits: - message: skip-appveyor - -# run build only if one of the following files is modified on commit -only_commits: - files: - - .ci/appveyor/* - - appveyor.yml - - psutil/__init__.py - - psutil/_common.py - - psutil/_compat.py - - psutil/_psutil_common.* - - psutil/_psutil_windows.* - - psutil/_pswindows.py - - psutil/arch/windows/* - - psutil/tests/* - - scripts/* - - scripts/internal/* - - setup.py diff --git a/docs/conf.py b/docs/conf.py index 3ebc64178c..213aadfe07 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/docs/index.rst b/docs/index.rst index a9b9dcc66f..443c46eeda 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -224,8 +224,8 @@ CPU .. function:: cpu_count(logical=True) - Return the number of logical CPUs in the system (same as `os.cpu_count`_ - in Python 3.4) or ``None`` if undetermined. + Return the number of logical CPUs in the system (same as `os.cpu_count`_) + or ``None`` if undetermined. "logical CPUs" means the number of physical cores multiplied by the number of threads that can run on each core (this is known as Hyper Threading). If *logical* is ``False`` return the number of physical cores only, or @@ -1203,7 +1203,7 @@ Process class >>> import psutil >>> psutil.Process().exe() - '/usr/bin/python2.7' + '/usr/bin/python3' .. method:: cmdline() @@ -2637,6 +2637,18 @@ On Windows: set PSUTIL_DEBUG=1 python.exe script.py psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) +Python 2.7 +========== + +Latest version spporting Python 2.7 is `psutil 6.1.1 `__. +The 6.1.X serie may receive critical bug-fixes but no new features. It will +be maintained in the dedicated +`python2 `__ branch. +To install it: + +:: + + $ python2 -m pip install psutil==6.1.* Security ======== diff --git a/make.bat b/make.bat index ec4fc591ef..97af61c3a0 100644 --- a/make.bat +++ b/make.bat @@ -10,7 +10,6 @@ rem make install rem make test rem rem This script is modeled after my Windows installation which uses: -rem - Visual studio 2008 for Python 2.7 rem - Visual studio 2010 for Python 3.4+ rem ...therefore it might not work on your Windows installation. rem diff --git a/psutil/__init__.py b/psutil/__init__.py index 5648339382..490e7b394b 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -17,10 +15,9 @@ - Sun Solaris - AIX -Works with Python versions 2.7 and 3.6+. +Supported Python versions are cPython 3.6+ and PyPy. """ -from __future__ import division import collections import contextlib @@ -28,6 +25,7 @@ import functools import os import signal +import socket import subprocess import sys import threading @@ -88,11 +86,6 @@ from ._common import debug 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 if LINUX: @@ -214,7 +207,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "6.1.1" +__version__ = "7.0.0" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) @@ -287,7 +280,7 @@ def _pprint_secs(secs): # ===================================================================== -class Process(object): # noqa: UP004 +class Process: """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. @@ -322,9 +315,6 @@ def _init(self, pid, _ignore_nsp=False): if pid is None: pid = os.getpid() else: - if not _PY3 and not isinstance(pid, (int, long)): - msg = "pid must be an integer (got %r)" % pid - raise TypeError(msg) if pid < 0: msg = "pid must be a positive integer (got %s)" % pid raise ValueError(msg) @@ -1358,11 +1348,12 @@ def wait(self, timeout=None): # 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 +_as_dict_attrnames = { + 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', 'connections', 'oneshot'}]) + 'memory_info_ex', 'connections', 'oneshot'} +} # fmt: on @@ -1448,7 +1439,7 @@ def __getattribute__(self, name): def wait(self, timeout=None): if self.__subproc.returncode is not None: return self.__subproc.returncode - ret = super(Popen, self).wait(timeout) # noqa + ret = super().wait(timeout) # noqa self.__subproc.returncode = ret return ret @@ -1586,9 +1577,7 @@ def wait_procs(procs, timeout=None, callback=None): def check_gone(proc, timeout): try: returncode = proc.wait(timeout=timeout) - except TimeoutExpired: - pass - except _SubprocessTimeoutExpired: + except (TimeoutExpired, subprocess.TimeoutExpired): pass else: if returncode is not None or not proc.is_running(): @@ -1646,7 +1635,7 @@ def check_gone(proc, timeout): def cpu_count(logical=True): """Return the number of logical CPUs in the system (same as - os.cpu_count() in Python 3.4). + os.cpu_count()). If *logical* is False return the number of physical cores only (e.g. hyper thread CPUs are excluded). @@ -2223,27 +2212,22 @@ def net_if_addrs(): Note: you can have more than one address of the same family associated with each interface. """ - has_enums = _PY3 - if has_enums: - import socket rawlist = _psplatform.net_if_addrs() rawlist.sort(key=lambda x: x[1]) # sort by family ret = collections.defaultdict(list) for name, fam, addr, mask, broadcast, ptp in rawlist: - if has_enums: - try: - fam = socket.AddressFamily(fam) - except ValueError: - if WINDOWS and fam == -1: - fam = _psplatform.AF_LINK - 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 - fam = _psplatform.AF_LINK + try: + fam = socket.AddressFamily(fam) + except ValueError: + if WINDOWS and fam == -1: + fam = _psplatform.AF_LINK + 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 + fam = _psplatform.AF_LINK if fam == _psplatform.AF_LINK: # The underlying C function may return an incomplete MAC # address in which case we fill it with null bytes, see: @@ -2405,8 +2389,9 @@ def _set_debug(value): def test(): # pragma: no cover + import shutil + from ._common import bytes2human - from ._compat import get_terminal_size today_day = datetime.date.today() # fmt: off @@ -2475,12 +2460,10 @@ def test(): # pragma: no cover cputime, cmdline, ) - print(line[: get_terminal_size()[0]]) # NOQA + print(line[: shutil.get_terminal_size()[0]]) # NOQA -del memoize_when_activated, division -if sys.version_info[0] < 3: - del num, x # noqa +del memoize_when_activated if __name__ == "__main__": test() diff --git a/psutil/_common.py b/psutil/_common.py index 4b99b093e3..ffa6dcef34 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -7,12 +7,9 @@ # Note: this module is imported by setup.py so it should not import # psutil or third-party modules. -from __future__ import division -from __future__ import print_function import collections -import contextlib -import errno +import enum import functools import os import socket @@ -36,14 +33,6 @@ AF_UNIX = None -# 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 - - PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG')) _DEFAULT = object() @@ -57,7 +46,7 @@ 'CONN_FIN_WAIT1', 'CONN_FIN_WAIT2', 'CONN_LAST_ACK', 'CONN_LISTEN', 'CONN_NONE', 'CONN_SYN_RECV', 'CONN_SYN_SENT', 'CONN_TIME_WAIT', # net constants - 'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN', + 'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN', # noqa: F822 # process status constants 'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED', 'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED', @@ -134,42 +123,29 @@ CONN_CLOSING = "CLOSING" CONN_NONE = "NONE" + # net_if_stats() -if enum is None: +class NicDuplex(enum.IntEnum): NIC_DUPLEX_FULL = 2 NIC_DUPLEX_HALF = 1 NIC_DUPLEX_UNKNOWN = 0 -else: - class NicDuplex(enum.IntEnum): - NIC_DUPLEX_FULL = 2 - NIC_DUPLEX_HALF = 1 - NIC_DUPLEX_UNKNOWN = 0 - globals().update(NicDuplex.__members__) +globals().update(NicDuplex.__members__) + # sensors_battery() -if enum is None: +class BatteryTime(enum.IntEnum): POWER_TIME_UNKNOWN = -1 POWER_TIME_UNLIMITED = -2 -else: - class BatteryTime(enum.IntEnum): - POWER_TIME_UNKNOWN = -1 - POWER_TIME_UNLIMITED = -2 - globals().update(BatteryTime.__members__) +globals().update(BatteryTime.__members__) # --- others ENCODING = sys.getfilesystemencoding() -if not PY3: - ENCODING_ERRS = "replace" -else: - try: - ENCODING_ERRS = sys.getfilesystemencodeerrors() # py 3.6 - except AttributeError: - ENCODING_ERRS = "surrogateescape" if POSIX else "replace" +ENCODING_ERRS = sys.getfilesystemencodeerrors() # =================================================================== @@ -391,26 +367,6 @@ def __reduce__(self): # =================================================================== -# 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: @@ -456,7 +412,7 @@ def wrapper(*args, **kwargs): try: ret = cache[key] = fun(*args, **kwargs) except Exception as err: # noqa: BLE001 - raise raise_from(err, None) + raise err from None return ret def cache_clear(): @@ -505,14 +461,14 @@ def wrapper(self): try: return fun(self) except Exception as err: # noqa: BLE001 - raise raise_from(err, None) + raise err from 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) + raise err from None try: self._cache[fun] = ret except AttributeError: @@ -546,9 +502,9 @@ def isfile_strict(path): """ try: st = os.stat(path) - except OSError as err: - if err.errno in {errno.EPERM, errno.EACCES}: - raise + except PermissionError: + raise + except OSError: return False else: return stat.S_ISREG(st.st_mode) @@ -561,9 +517,9 @@ def path_exists_strict(path): """ try: os.stat(path) - except OSError as err: - if err.errno in {errno.EPERM, errno.EACCES}: - raise + except PermissionError: + raise + except OSError: return False else: return True @@ -575,11 +531,10 @@ def supports_ipv6(): if not socket.has_ipv6 or AF_INET6 is None: return False try: - sock = socket.socket(AF_INET6, socket.SOCK_STREAM) - with contextlib.closing(sock): + with socket.socket(AF_INET6, socket.SOCK_STREAM) as sock: sock.bind(("::1", 0)) return True - except socket.error: + except OSError: return False @@ -615,26 +570,20 @@ def sockfam_to_enum(num): """Convert a numeric socket family value to an IntEnum member. If it's not a known member, return the numeric value itself. """ - if enum is None: + try: + return socket.AddressFamily(num) + except ValueError: return num - else: # pragma: no cover - try: - return socket.AddressFamily(num) - except ValueError: - return num def socktype_to_enum(num): """Convert a numeric socket type value to an IntEnum member. If it's not a known member, return the numeric value itself. """ - if enum is None: + try: + return socket.SocketKind(num) + except ValueError: return num - else: # pragma: no cover - try: - return socket.SocketKind(num) - except ValueError: - return num def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): @@ -789,8 +738,6 @@ def wrap_numbers(input_dict, name): # 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 @@ -800,13 +747,9 @@ def open_binary(fname): 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'). + """Open a file in text mode by using the proper FS encoding and + en/decoding error handlers. """ - 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 @@ -842,7 +785,7 @@ def cat(fname, fallback=_DEFAULT, _open=open_text): try: with _open(fname) as f: return f.read() - except (IOError, OSError): + except OSError: return fallback @@ -875,15 +818,8 @@ def get_procfs_path(): 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 +def decode(s): + return s.decode(encoding=ENCODING, errors=ENCODING_ERRS) # ===================================================================== @@ -984,7 +920,7 @@ def debug(msg): inspect.currentframe().f_back ) if isinstance(msg, Exception): - if isinstance(msg, (OSError, IOError, EnvironmentError)): + if isinstance(msg, OSError): # ...because str(exc) may contain info about the file name msg = "ignoring %s" % msg else: diff --git a/psutil/_compat.py b/psutil/_compat.py deleted file mode 100644 index 92e01713c3..0000000000 --- a/psutil/_compat.py +++ /dev/null @@ -1,477 +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. - -"""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 - - -# fmt: off -__all__ = [ - # constants - "PY3", - # builtins - "long", "range", "super", "unicode", "basestring", - # literals - "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 b(s): - return s.encode("latin-1") - -else: - long = long - range = xrange - unicode = unicode - basestring = basestring - - 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 - - -# py 3.2 functools.lru_cache -# Taken from: http://code.activestate.com/recipes/578078 -# Credit: Raymond Hettinger -try: - from functools import lru_cache -except ImportError: - try: - from threading import RLock - except ImportError: - from dummy_threading import RLock - - _CacheInfo = collections.namedtuple( - "CacheInfo", ["hits", "misses", "maxsize", "currsize"] - ) - - class _HashedSeq(list): # noqa: FURB189 - __slots__ = ('hashvalue',) - - def __init__(self, tup, hash=hash): - self[:] = tup - self.hashvalue = hash(tup) - - def __hash__(self): - return self.hashvalue - - 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()) - key += kwd_mark - for item in sorted_items: - key += item - if typed: - key += tuple(type(v) for v in args) - if kwds: - key += tuple(type(v) for k, v in sorted_items) - elif len(key) == 1 and type(key[0]) in fasttypes: - return key[0] - return _HashedSeq(key) - - def lru_cache(maxsize=100, typed=False): - """Least-recently-used cache decorator, see: - http://docs.python.org/3/library/functools.html#functools.lru_cache. - """ - - def decorating_function(user_function): - cache = {} - stats = [0, 0] - HITS, MISSES = 0, 1 - make_key = _make_key - cache_get = cache.get - _len = len - lock = RLock() - root = [] - root[:] = [root, root, None, None] - 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) - if result is not root: - stats[HITS] += 1 - return result - result = user_function(*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) - else: - key = args - lock.acquire() - try: - link = cache_get(key) - if link is not None: - (root,) = nonlocal_root - link_prev, link_next, key, result = link - link_prev[NEXT] = link_next - link_next[PREV] = link_prev - last = root[PREV] - last[NEXT] = root[PREV] = link - link[PREV] = last - link[NEXT] = root - stats[HITS] += 1 - return result - finally: - lock.release() - result = user_function(*args, **kwds) - lock.acquire() - try: - (root,) = nonlocal_root - if key in cache: - pass - elif _len(cache) >= maxsize: - oldroot = root - oldroot[KEY] = key - oldroot[RESULT] = result - root = nonlocal_root[0] = oldroot[NEXT] - oldkey = root[KEY] - root[KEY] = root[RESULT] = None - del cache[oldkey] - cache[key] = oldroot - else: - last = root[PREV] - link = [last, root, key, result] - last[NEXT] = root[PREV] = cache[key] = link - stats[MISSES] += 1 - finally: - lock.release() - return result - - def cache_info(): - """Report cache statistics.""" - lock.acquire() - try: - return _CacheInfo( - stats[HITS], stats[MISSES], maxsize, len(cache) - ) - finally: - lock.release() - - def cache_clear(): - """Clear the cache and cache statistics.""" - lock.acquire() - try: - cache.clear() - root = nonlocal_root[0] - root[:] = [root, root, None, None] - stats[:] = [0, 0] - finally: - lock.release() - - wrapper.__wrapped__ = user_function - wrapper.cache_info = cache_info - wrapper.cache_clear = cache_clear - return functools.update_wrapper(wrapper, user_function) - - return decorating_function - - -# python 3.3 -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 - file. - - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - 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) - ) - - if os.path.dirname(cmd): - if _access_check(cmd, mode): - return cmd - return None - - if path is None: - path = os.environ.get("PATH", os.defpath) - if not path: - return None - path = path.split(os.pathsep) - - if sys.platform == "win32": - if os.curdir not in path: - path.insert(0, os.curdir) - - pathext = os.environ.get("PATHEXT", "").split(os.pathsep) - if any(cmd.lower().endswith(ext.lower()) for ext in pathext): - files = [cmd] - else: - files = [cmd + ext for ext in pathext] - else: - files = [cmd] - - seen = set() - for dir in path: - normdir = os.path.normcase(dir) - if normdir not in seen: - seen.add(normdir) - for thefile in files: - name = os.path.join(dir, thefile) - 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/psutil/_psaix.py b/psutil/_psaix.py index 2ccc638bce..94a07a3a59 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -28,10 +28,6 @@ from ._common import get_procfs_path from ._common import memoize_when_activated from ._common import usage_percent -from ._compat import PY3 -from ._compat import FileNotFoundError -from ._compat import PermissionError -from ._compat import ProcessLookupError __extra__all__ = ["PROCFS_PATH"] @@ -148,10 +144,7 @@ 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() @@ -243,7 +236,7 @@ def net_connections(kind, _pid=-1): def net_if_stats(): """Get NIC stats (isup, duplex, speed, mtu).""" duplex_map = {"Full": NIC_DUPLEX_FULL, "Half": NIC_DUPLEX_HALF} - names = set([x[0] for x in net_if_addrs()]) + names = {x[0] for x in net_if_addrs()} ret = {} for name in names: mtu = cext_posix.net_if_mtu(name) @@ -260,10 +253,9 @@ def net_if_stats(): 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 @@ -533,10 +525,9 @@ def open_files(self): 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.*name:(.*)\n", stdout) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 77b99c0fd8..209e5476cd 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -28,10 +28,6 @@ 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 __extra__all__ = [] @@ -690,9 +686,11 @@ def exe(self): # master/base_paths_posix.cc # We try our best guess by using which against the first # cmdline arg (may return None). + import shutil + cmdline = self.cmdline() if cmdline: - return which(cmdline[0]) or "" + return shutil.which(cmdline[0]) or "" else: return "" diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index c9c97f6ec4..5d5595feb5 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -4,15 +4,16 @@ """Linux platform implementation.""" -from __future__ import division import base64 import collections +import enum import errno import functools import glob import os import re +import resource import socket import struct import sys @@ -24,6 +25,7 @@ from . import _psposix from . import _psutil_linux as cext from . import _psutil_posix as cext_posix +from ._common import ENCODING from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN @@ -44,18 +46,6 @@ 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 - - -if PY3: - import enum -else: - enum = None # fmt: off @@ -69,6 +59,11 @@ "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", ] + +if hasattr(resource, "prlimit"): + __extra__all__.extend( + [x for x in dir(cext) if x.startswith('RLIM') and x.isupper()] + ) # fmt: on @@ -102,29 +97,21 @@ # * https://lkml.org/lkml/2015/8/17/234 DISK_SECTOR_SIZE = 512 -if enum is None: - AF_LINK = socket.AF_PACKET -else: - AddressFamily = enum.IntEnum( - 'AddressFamily', {'AF_LINK': int(socket.AF_PACKET)} - ) - AF_LINK = AddressFamily.AF_LINK +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 -if enum is None: +class IOPriority(enum.IntEnum): IOPRIO_CLASS_NONE = 0 IOPRIO_CLASS_RT = 1 IOPRIO_CLASS_BE = 2 IOPRIO_CLASS_IDLE = 3 -else: - class IOPriority(enum.IntEnum): - IOPRIO_CLASS_NONE = 0 - IOPRIO_CLASS_RT = 1 - IOPRIO_CLASS_BE = 2 - IOPRIO_CLASS_IDLE = 3 - globals().update(IOPriority.__members__) +globals().update(IOPriority.__members__) # See: # https://github.com/torvalds/linux/blame/master/fs/proc/array.c @@ -211,7 +198,7 @@ class IOPriority(enum.IntEnum): def readlink(path): """Wrapper around os.readlink().""" - assert isinstance(path, basestring), path + assert isinstance(path, str), path path = os.readlink(path) # readlink() might return paths containing null bytes ('\x00') # resulting in "TypeError: must be encoded string without NULL @@ -294,58 +281,6 @@ def set_scputimes_ntuple(procfs_path): 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 # ===================================================================== @@ -396,7 +331,7 @@ def calculate_avail_vmem(mems): return fallback try: f = open_binary('%s/zoneinfo' % get_procfs_path()) - except IOError: + except OSError: return fallback # kernel 2.6.13 watermark_low = 0 @@ -572,7 +507,7 @@ def swap_memory(): # get pgin/pgouts try: f = open_binary("%s/vmstat" % get_procfs_path()) - except IOError as err: + except OSError as err: # see https://github.com/giampaolo/psutil/issues/722 msg = ( "'sin' and 'sout' swap memory stats couldn't " @@ -778,9 +713,7 @@ def cpu_freq(): # https://github.com/giampaolo/psutil/issues/1071 curr = bcat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) if curr is None: - online_path = ( - "/sys/devices/system/cpu/cpu{}/online".format(i) - ) + online_path = f"/sys/devices/system/cpu/cpu{i}/online" # if cpu core is offline, set to all zeroes if cat(online_path, fallback=None) == "0\n": ret.append(_common.scpufreq(0.0, 0.0, 0.0)) @@ -914,8 +847,7 @@ def decode_address(addr, family): # no end-points connected if not port: return () - if PY3: - ip = ip.encode('ascii') + ip = ip.encode('ascii') if family == socket.AF_INET: # see: https://github.com/giampaolo/psutil/issues/201 if LITTLE_ENDIAN: @@ -1311,17 +1243,17 @@ def find(self): if path is None: try: path = self.ask_proc_partitions() - except (IOError, OSError) as err: + except OSError as err: debug(err) if path is None: try: path = self.ask_sys_dev_block() - except (IOError, OSError) as err: + except OSError as err: debug(err) if path is None: try: path = self.ask_sys_class_block() - except (IOError, OSError) as err: + except OSError as err: debug(err) # We use exists() because the "/dev/*" part of the path is hard # coded, so we want to be sure. @@ -1392,7 +1324,7 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/971 # https://github.com/nicolargo/glances/issues/1060 basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*')) - basenames = sorted(set([x.split('_')[0] for x in basenames])) + basenames = sorted({x.split('_')[0] for x in basenames}) # Only add the coretemp hwmon entries if they're not already in # /sys/class/hwmon/ @@ -1413,7 +1345,7 @@ def sensors_temperatures(): current = float(bcat(path)) / 1000.0 path = os.path.join(os.path.dirname(base), 'name') unit_name = cat(path).strip() - except (IOError, OSError, ValueError): + except (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. @@ -1452,15 +1384,15 @@ def sensors_temperatures(): current = float(bcat(path)) / 1000.0 path = os.path.join(base, 'type') unit_name = cat(path).strip() - except (IOError, OSError, ValueError) as err: + except (OSError, ValueError) as err: debug(err) continue trip_paths = glob.glob(base + '/trip_point*') - trip_points = set([ + trip_points = { '_'.join(os.path.basename(p).split('_')[0:3]) for p in trip_paths - ]) + } critical = None high = None for trip_point in trip_points: @@ -1508,11 +1440,11 @@ def sensors_fans(): # https://github.com/giampaolo/psutil/issues/971 basenames = glob.glob('/sys/class/hwmon/hwmon*/device/fan*_*') - basenames = sorted(set([x.split('_')[0] for x in basenames])) + basenames = sorted({x.split("_")[0] for x in basenames}) for base in basenames: try: current = int(bcat(base + '_input')) - except (IOError, OSError) as err: + except OSError as err: debug(err) continue unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip() @@ -1648,7 +1580,8 @@ def boot_time(): def pids(): """Returns a list of PIDs currently running on the system.""" - return [int(x) for x in os.listdir(b(get_procfs_path())) if x.isdigit()] + path = get_procfs_path().encode(ENCODING) + return [int(x) for x in os.listdir(path) if x.isdigit()] def pid_exists(pid): @@ -1679,7 +1612,7 @@ def pid_exists(pid): # dealing with a process PID. return tgid == pid raise ValueError("'Tgid' line not found in %s" % path) - except (EnvironmentError, ValueError): + except (OSError, ValueError): return pid in pids() @@ -1706,7 +1639,7 @@ def ppid_map(): def wrap_exceptions(fun): - """Decorator which translates bare OSError and IOError exceptions + """Decorator which translates bare OSError and OSError exceptions into NoSuchProcess and AccessDenied. """ @@ -1753,7 +1686,7 @@ def _is_zombie(self): # exception. try: data = bcat("%s/%s/stat" % (self._procfs_path, self.pid)) - except (IOError, OSError): + except OSError: return False else: rpar = data.rfind(b')') @@ -1837,11 +1770,8 @@ def oneshot_exit(self): @wrap_exceptions def name(self): - name = self._parse_stat_file()['name'] - if PY3: - name = decode(name) # XXX - gets changed later and probably needs refactoring - return name + return decode(self._parse_stat_file()['name']) @wrap_exceptions def exe(self): @@ -1995,7 +1925,7 @@ def _parse_smaps_rollup(self): # compared to /proc/pid/smaps_rollup. uss = pss = swap = 0 with open_binary( - "{}/{}/smaps_rollup".format(self._procfs_path, self.pid) + f"{self._procfs_path}/{self.pid}/smaps_rollup" ) as f: for line in f: if line.startswith(b"Private_"): @@ -2020,8 +1950,6 @@ def _parse_smaps( # 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 - # Python 2 though. Figure out why. # # You might be tempted to calculate USS by subtracting # the "shared" value from the "resident" value in @@ -2106,8 +2034,7 @@ def get_blocks(lines, current_block): if not path: path = '[anon]' else: - if PY3: - path = decode(path) + path = decode(path) path = path.strip() if path.endswith(' (deleted)') and not path_exists_strict( path @@ -2152,9 +2079,7 @@ def num_ctx_switches( @wrap_exceptions def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')): - # Note: on Python 3 using a re is faster than iterating over file - # line by line. On Python 2 is the exact opposite, and iterating - # over a file on Python 3 is slower than on Python 2. + # Using a re is faster than iterating over file line by line. data = self._read_status_file() return int(_num_threads_re.findall(data)[0]) @@ -2247,22 +2172,24 @@ def cpu_affinity_set(self, cpus): @wrap_exceptions def ionice_get(self): ioclass, value = cext.proc_ioprio_get(self.pid) - if enum is not None: - ioclass = IOPriority(ioclass) + ioclass = IOPriority(ioclass) return _common.pionice(ioclass, value) @wrap_exceptions def ionice_set(self, ioclass, value): if value is None: value = 0 - if value and ioclass in {IOPRIO_CLASS_IDLE, IOPRIO_CLASS_NONE}: + if value and ioclass in { + IOPriority.IOPRIO_CLASS_IDLE, + IOPriority.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 prlimit is not None: + if hasattr(resource, "prlimit"): @wrap_exceptions def rlimit(self, resource_, limits=None): @@ -2275,7 +2202,7 @@ def rlimit(self, resource_, limits=None): try: if limits is None: # get - return prlimit(self.pid, resource_) + return resource.prlimit(self.pid, resource_) else: # set if len(limits) != 2: @@ -2284,7 +2211,7 @@ def rlimit(self, resource_, limits=None): + "tuple, got %s" % repr(limits) ) raise ValueError(msg) - prlimit(self.pid, resource_, limits) + resource.prlimit(self.pid, resource_, limits) except OSError as err: if err.errno == errno.ENOSYS: # I saw this happening on Travis: @@ -2295,8 +2222,7 @@ def rlimit(self, resource_, limits=None): @wrap_exceptions def status(self): letter = self._parse_stat_file()['status'] - if PY3: - letter = letter.decode() + letter = letter.decode() # XXX is '?' legit? (we're not supposed to return it anyway) return PROC_STATUSES.get(letter, '?') diff --git a/psutil/_psosx.py b/psutil/_psosx.py index ed9b319de7..c6078ea3d4 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -22,8 +22,6 @@ from ._common import memoize_when_activated from ._common import parse_environ_block from ._common import usage_percent -from ._compat import PermissionError -from ._compat import ProcessLookupError __extra__all__ = [] diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 42bdfa7ef6..e074819861 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -4,10 +4,10 @@ """Routines common to all posix systems.""" +import enum import glob import os import signal -import sys import time from ._common import MACOS @@ -15,25 +15,12 @@ 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 if MACOS: from . import _psutil_osx -if PY3: - import enum -else: - enum = None - - __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] @@ -59,23 +46,16 @@ def pid_exists(pid): return True -# 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 +Negsignal = enum.IntEnum( + 'Negsignal', {x.name: -x.value for x in signal.Signals} +) -else: # pragma: no cover - def negsig_to_enum(num): +def negsig_to_enum(num): + """Convert a negative signal value to an enum.""" + try: + return Negsignal(num) + except ValueError: return num @@ -181,24 +161,7 @@ def disk_usage(path): total and used disk space whereas "free" and "percent" represent the "free" and "used percent" user disk space. """ - if PY3: - st = os.statvfs(path) - 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 - try: - st = os.statvfs(path) - except UnicodeEncodeError: - if isinstance(path, unicode): - try: - path = path.encode(sys.getfilesystemencoding()) - except UnicodeEncodeError: - pass - st = os.statvfs(path) - else: - raise - + st = os.statvfs(path) # Total space which is only available to root (unless changed # at system level). total = st.f_blocks * st.f_frsize diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index a38d939d79..09a79fef42 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -18,6 +18,7 @@ from . import _psutil_posix as cext_posix from . import _psutil_sunos as cext from ._common import AF_INET6 +from ._common import ENCODING from ._common import AccessDenied from ._common import NoSuchProcess from ._common import ZombieProcess @@ -28,11 +29,6 @@ from ._common import sockfam_to_enum from ._common import socktype_to_enum 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 __extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] @@ -154,8 +150,7 @@ def swap_memory(): stdout=subprocess.PIPE, ) stdout, _ = p.communicate() - if PY3: - stdout = stdout.decode(sys.stdout.encoding) + stdout = stdout.decode(sys.stdout.encoding) if p.returncode != 0: raise RuntimeError("'swap -l' failed (retcode=%s)" % p.returncode) @@ -346,7 +341,8 @@ def users(): def pids(): """Returns a list of PIDs currently running on the system.""" - return [int(x) for x in os.listdir(b(get_procfs_path())) if x.isdigit()] + path = get_procfs_path().encode(ENCODING) + return [int(x) for x in os.listdir(path) if x.isdigit()] def pid_exists(pid): @@ -588,7 +584,7 @@ def threads(self): utime, stime = cext.query_process_thread( self.pid, tid, procfs_path ) - except EnvironmentError as err: + except OSError as err: if err.errno == errno.EOVERFLOW and not IS_64_BIT: # We may get here if we attempt to query a 64bit process # with a 32bit python. @@ -640,10 +636,9 @@ def _get_unix_sockets(self, pid): 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) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 42f921188e..958a2c0c21 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -58,6 +58,7 @@ #define TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) +#define INITERROR return NULL /* * Read a file content and fills a C structure with it. @@ -1030,18 +1031,12 @@ struct module_state { PyObject *error; }; -#if PY_MAJOR_VERSION >= 3 #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -#endif #ifdef __cplusplus extern "C" { #endif -#if PY_MAJOR_VERSION >= 3 - static int psutil_aix_traverse(PyObject *m, visitproc visit, void *arg) { Py_VISIT(GETSTATE(m)->error); @@ -1066,21 +1061,13 @@ static struct PyModuleDef moduledef = { NULL }; -#define INITERROR return NULL -PyMODINIT_FUNC PyInit__psutil_aix(void) - -#else -#define INITERROR return - -void init_psutil_aix(void) -#endif -{ -#if PY_MAJOR_VERSION >= 3 +PyMODINIT_FUNC +PyInit__psutil_aix(void) { PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_aix", PsutilMethods); -#endif + if (module == NULL) + INITERROR; + #ifdef Py_GIL_DISABLED PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); #endif @@ -1109,9 +1096,8 @@ void init_psutil_aix(void) if (module == NULL) INITERROR; -#if PY_MAJOR_VERSION >= 3 + return module; -#endif } #ifdef __cplusplus diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index facaba831f..503e96cacb 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -52,6 +52,9 @@ #endif +#define INITERR return NULL + + /* * define the psutil C module methods and initialize the module. */ @@ -112,34 +115,23 @@ static PyMethodDef mod_methods[] = { {NULL, NULL, 0, NULL} }; -#if PY_MAJOR_VERSION >= 3 - #define INITERR return NULL - - static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_psutil_bsd", - NULL, - -1, - mod_methods, - NULL, - NULL, - NULL, - NULL - }; - - PyObject *PyInit__psutil_bsd(void) -#else /* PY_MAJOR_VERSION */ - #define INITERR return - - void init_psutil_bsd(void) -#endif /* PY_MAJOR_VERSION */ -{ + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_bsd", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL +}; + +PyObject +*PyInit__psutil_bsd(void) { PyObject *v; -#if PY_MAJOR_VERSION >= 3 PyObject *mod = PyModule_Create(&moduledef); -#else - PyObject *mod = Py_InitModule("_psutil_bsd", mod_methods); -#endif if (mod == NULL) INITERR; @@ -210,7 +202,5 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; -#if PY_MAJOR_VERSION >= 3 return mod; -#endif } diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index d16816972e..81132fd3ee 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -68,15 +68,6 @@ PyErr_SetExcFromWindowsErrWithFilenameObject(PyObject *type, return PyErr_SetFromWindowsErrWithFilename(ierr, NULL); } #endif // !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) - - -// PyPy 2.7 -#if !defined(PyErr_SetFromWindowsErr) -PyObject * -PyErr_SetFromWindowsErr(int winerr) { - return PyErr_SetFromWindowsErrWithFilename(winerr, ""); -} -#endif // !defined(PyErr_SetFromWindowsErr) #endif // defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h index 2cdfa9d4d6..024452630f 100644 --- a/psutil/_psutil_common.h +++ b/psutil/_psutil_common.h @@ -23,14 +23,6 @@ static const int PSUTIL_CONN_NONE = 128; // --- Backward compatibility with missing Python.h APIs // ==================================================================== -#if PY_MAJOR_VERSION < 3 - // On Python 2 we just return a plain byte string, which is never - // supposed to raise decoding errors, see: - // https://github.com/giampaolo/psutil/issues/1040 - #define PyUnicode_DecodeFSDefault PyString_FromString - #define PyUnicode_DecodeFSDefaultAndSize PyString_FromStringAndSize -#endif - #if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) #if !defined(PyErr_SetFromWindowsErrWithFilename) PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, @@ -45,7 +37,6 @@ static const int PSUTIL_CONN_NONE = 128; // --- _Py_PARSE_PID // SIZEOF_INT|LONG is missing on Linux + PyPy (only?). -// SIZEOF_PID_T is missing on Windows + Python2. // In this case we guess it from setup.py. It's not 100% bullet proof, // If wrong we'll probably get compiler warnings. // FWIW on all UNIX platforms I've seen pid_t is defined as an int. @@ -60,8 +51,8 @@ static const int PSUTIL_CONN_NONE = 128; #define SIZEOF_PID_T PSUTIL_SIZEOF_PID_T // set as a macro in setup.py #endif -// _Py_PARSE_PID is Python 3 only, but since it's private make sure it's -// always present. +// _Py_PARSE_PID was added in Python 3, but since it's private we make +// sure it's always present. #ifndef _Py_PARSE_PID #if SIZEOF_PID_T == SIZEOF_INT #define _Py_PARSE_PID "i" @@ -75,14 +66,10 @@ static const int PSUTIL_CONN_NONE = 128; #endif #endif -// Python 2 or PyPy on Windows +// PyPy on Windows #ifndef PyLong_FromPid #if ((SIZEOF_PID_T == SIZEOF_INT) || (SIZEOF_PID_T == SIZEOF_LONG)) - #if PY_MAJOR_VERSION >= 3 - #define PyLong_FromPid PyLong_FromLong - #else - #define PyLong_FromPid PyInt_FromLong - #endif + #define PyLong_FromPid PyLong_FromLong #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG #define PyLong_FromPid PyLong_FromLongLong #else diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 46244c5792..fcd886bfb6 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -19,6 +19,13 @@ #include "arch/linux/proc.h" #include "arch/linux/users.h" +// May happen on old RedHat versions, see: +// https://github.com/giampaolo/psutil/issues/607 +#ifndef DUPLEX_UNKNOWN + #define DUPLEX_UNKNOWN 0xff +#endif + +#define INITERR return NULL static PyMethodDef mod_methods[] = { // --- per-process functions @@ -41,40 +48,24 @@ static PyMethodDef mod_methods[] = { {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; -// May happen on old RedHat versions, see: -// https://github.com/giampaolo/psutil/issues/607 -#ifndef DUPLEX_UNKNOWN - #define DUPLEX_UNKNOWN 0xff -#endif - -#if PY_MAJOR_VERSION >= 3 - #define INITERR return NULL - static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_psutil_linux", - NULL, - -1, - mod_methods, - NULL, - NULL, - NULL, - NULL - }; +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_linux", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL +}; - PyObject *PyInit__psutil_linux(void) -#else /* PY_MAJOR_VERSION */ - #define INITERR return - void init_psutil_linux(void) -#endif /* PY_MAJOR_VERSION */ -{ -#if PY_MAJOR_VERSION >= 3 +PyObject * +PyInit__psutil_linux(void) { PyObject *mod = PyModule_Create(&moduledef); -#else - PyObject *mod = Py_InitModule("_psutil_linux", mod_methods); -#endif if (mod == NULL) INITERR; @@ -91,7 +82,5 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; -#if PY_MAJOR_VERSION >= 3 return mod; -#endif } diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 09fa267a98..d9a486fe57 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -20,6 +20,8 @@ #include "arch/osx/sys.h" +#define INITERR return NULL + static PyMethodDef mod_methods[] = { // --- per-process functions {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, @@ -61,33 +63,22 @@ static PyMethodDef mod_methods[] = { }; -#if PY_MAJOR_VERSION >= 3 - #define INITERR return NULL - - static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_psutil_osx", - NULL, - -1, - mod_methods, - NULL, - NULL, - NULL, - NULL - }; - - PyObject *PyInit__psutil_osx(void) -#else /* PY_MAJOR_VERSION */ - #define INITERR return - - void init_psutil_osx(void) -#endif /* PY_MAJOR_VERSION */ -{ -#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_osx", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL +}; + + +PyObject * +PyInit__psutil_osx(void) { PyObject *mod = PyModule_Create(&moduledef); -#else - PyObject *mod = Py_InitModule("_psutil_osx", mod_methods); -#endif if (mod == NULL) INITERR; @@ -140,7 +131,5 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; -#if PY_MAJOR_VERSION >= 3 return mod; -#endif } diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 5df15530f3..d4d16e7fff 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -52,6 +52,9 @@ #include "_psutil_common.h" +#define INITERR return NULL + + // ==================================================================== // --- Utils // ==================================================================== @@ -434,11 +437,7 @@ append_flag(PyObject *py_retlist, const char * flag_name) { PyObject *py_str = NULL; -#if PY_MAJOR_VERSION >= 3 py_str = PyUnicode_FromString(flag_name); -#else - py_str = PyString_FromString(flag_name); -#endif if (! py_str) return 0; if (PyList_Append(py_retlist, py_str)) { @@ -883,33 +882,21 @@ static PyMethodDef mod_methods[] = { }; -#if PY_MAJOR_VERSION >= 3 - #define INITERR return NULL - - static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_psutil_posix", - NULL, - -1, - mod_methods, - NULL, - NULL, - NULL, - NULL - }; - - PyObject *PyInit__psutil_posix(void) -#else /* PY_MAJOR_VERSION */ - #define INITERR return +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_posix", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL +}; - void init_psutil_posix(void) -#endif /* PY_MAJOR_VERSION */ -{ -#if PY_MAJOR_VERSION >= 3 +PyObject * +PyInit__psutil_posix(void) { PyObject *mod = PyModule_Create(&moduledef); -#else - PyObject *mod = Py_InitModule("_psutil_posix", mod_methods); -#endif if (mod == NULL) INITERR; @@ -1022,9 +1009,7 @@ static PyMethodDef mod_methods[] = { if (mod == NULL) INITERR; -#if PY_MAJOR_VERSION >= 3 return mod; -#endif } #ifdef __cplusplus diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index d21f59c618..dde9f70158 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -59,6 +59,8 @@ #include "arch/solaris/environ.h" #define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) +#define INITERROR return NULL /* @@ -1671,13 +1673,6 @@ struct module_state { PyObject *error; }; -#if PY_MAJOR_VERSION >= 3 -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -#endif - -#if PY_MAJOR_VERSION >= 3 static int psutil_sunos_traverse(PyObject *m, visitproc visit, void *arg) { @@ -1703,21 +1698,10 @@ static struct PyModuleDef moduledef = { NULL }; -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_sunos(void) -#else -#define INITERROR return - -void init_psutil_sunos(void) -#endif -{ -#if PY_MAJOR_VERSION >= 3 +PyMODINIT_FUNC +PyInit__psutil_sunos(void) { PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_sunos", PsutilMethods); -#endif if (module == NULL) INITERROR; @@ -1766,7 +1750,6 @@ void init_psutil_sunos(void) if (module == NULL) INITERROR; -#if PY_MAJOR_VERSION >= 3 + return module; -#endif } diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 0c221bdc23..0af18f3e26 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -34,6 +34,10 @@ #include "arch/windows/wmi.h" +#define INITERROR return NULL +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) + + // ------------------------ Python init --------------------------- static PyMethodDef @@ -116,21 +120,15 @@ struct module_state { PyObject *error; }; -#if PY_MAJOR_VERSION >= 3 -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -static struct module_state _state; -#endif - -#if PY_MAJOR_VERSION >= 3 -static int psutil_windows_traverse(PyObject *m, visitproc visit, void *arg) { +static int +psutil_windows_traverse(PyObject *m, visitproc visit, void *arg) { Py_VISIT(GETSTATE(m)->error); return 0; } -static int psutil_windows_clear(PyObject *m) { +static int +psutil_windows_clear(PyObject *m) { Py_CLEAR(GETSTATE(m)->error); return 0; } @@ -147,21 +145,12 @@ static struct PyModuleDef moduledef = { NULL }; -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_windows(void) -#else -#define INITERROR return -void init_psutil_windows(void) -#endif -{ +PyMODINIT_FUNC +PyInit__psutil_windows(void) { struct module_state *st = NULL; -#if PY_MAJOR_VERSION >= 3 + PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_windows", PsutilMethods); -#endif if (module == NULL) INITERROR; @@ -283,7 +272,5 @@ void init_psutil_windows(void) PyModule_AddIntConstant( module, "WINDOWS_10", PSUTIL_WINDOWS_10); -#if PY_MAJOR_VERSION >= 3 return module; -#endif } diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e39ba711fa..be100493e1 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -5,7 +5,7 @@ """Windows platform implementation.""" import contextlib -import errno +import enum import functools import os import signal @@ -15,7 +15,6 @@ from . import _common from ._common import ENCODING -from ._common import ENCODING_ERRS from ._common import AccessDenied from ._common import NoSuchProcess from ._common import TimeoutExpired @@ -27,11 +26,6 @@ from ._common import memoize_when_activated from ._common import parse_environ_block from ._common import usage_percent -from ._compat import PY3 -from ._compat import long -from ._compat import lru_cache -from ._compat import range -from ._compat import unicode from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS from ._psutil_windows import HIGH_PRIORITY_CLASS @@ -58,10 +52,6 @@ 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 @@ -88,11 +78,8 @@ ERROR_PARTIAL_COPY = 299 PYPY = '__pypy__' in sys.builtin_module_names -if enum is None: - AF_LINK = -1 -else: - AddressFamily = enum.IntEnum('AddressFamily', {'AF_LINK': -1}) - AF_LINK = AddressFamily.AF_LINK +AddressFamily = enum.IntEnum('AddressFamily', {'AF_LINK': -1}) +AF_LINK = AddressFamily.AF_LINK TCP_STATUSES = { cext.MIB_TCP_STATE_ESTAB: _common.CONN_ESTABLISHED, @@ -110,32 +97,27 @@ cext.PSUTIL_CONN_NONE: _common.CONN_NONE, } -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 - HIGH_PRIORITY_CLASS = HIGH_PRIORITY_CLASS - IDLE_PRIORITY_CLASS = IDLE_PRIORITY_CLASS - NORMAL_PRIORITY_CLASS = NORMAL_PRIORITY_CLASS - REALTIME_PRIORITY_CLASS = REALTIME_PRIORITY_CLASS +class Priority(enum.IntEnum): + ABOVE_NORMAL_PRIORITY_CLASS = ABOVE_NORMAL_PRIORITY_CLASS + BELOW_NORMAL_PRIORITY_CLASS = BELOW_NORMAL_PRIORITY_CLASS + HIGH_PRIORITY_CLASS = HIGH_PRIORITY_CLASS + IDLE_PRIORITY_CLASS = IDLE_PRIORITY_CLASS + NORMAL_PRIORITY_CLASS = NORMAL_PRIORITY_CLASS + REALTIME_PRIORITY_CLASS = REALTIME_PRIORITY_CLASS - globals().update(Priority.__members__) -if enum is None: +globals().update(Priority.__members__) + + +class IOPriority(enum.IntEnum): 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__) +globals().update(IOPriority.__members__) pinfo_map = dict( num_handles=0, @@ -199,7 +181,7 @@ class IOPriority(enum.IntEnum): # ===================================================================== -@lru_cache(maxsize=512) +@functools.lru_cache(maxsize=512) def convert_dos_path(s): r"""Convert paths using native DOS format like: "\Device\HarddiskVolume1\Windows\systemew\file.txt" @@ -212,18 +194,6 @@ def convert_dos_path(s): return os.path.join(driveletter, remainder) -def py2_strencode(s): - """Encode a unicode string to a byte string by using the default fs - encoding + "replace" error handler. - """ - if PY3: - return s - if isinstance(s, str): - return s - else: - return s.encode(ENCODING, ENCODING_ERRS) - - @memoize def getpagesize(): return cext.getpagesize() @@ -282,7 +252,7 @@ def swap_memory(): def disk_usage(path): """Return disk usage associated with path.""" - if PY3 and isinstance(path, bytes): + if isinstance(path, bytes): # XXX: do we want to use "strict"? Probably yes, in order # to fail immediately. After all we are accepting input here... path = path.decode(ENCODING, errors="strict") @@ -408,9 +378,6 @@ def net_if_stats(): ret = {} rawdict = cext.net_if_stats() for name, items in rawdict.items(): - if not PY3: - assert isinstance(name, unicode), type(name) - name = py2_strencode(name) isup, duplex, speed, mtu = items if hasattr(_common, 'NicDuplex'): duplex = _common.NicDuplex(duplex) @@ -422,18 +389,12 @@ def net_io_counters(): """Return network I/O statistics for every network interface installed on the system as a dict of raw tuples. """ - ret = cext.net_io_counters() - return dict([(py2_strencode(k), v) for k, v in ret.items()]) + return cext.net_io_counters() def net_if_addrs(): """Return the addresses associated to each NIC.""" - ret = [] - for items in cext.net_if_addrs(): - items = list(items) - items[0] = py2_strencode(items[0]) - ret.append(items) - return ret + return cext.net_if_addrs() # ===================================================================== @@ -489,7 +450,6 @@ def users(): rawlist = cext.users() for item in rawlist: user, hostname, tstamp = item - user = py2_strencode(user) nt = _common.suser(user, None, hostname, tstamp, None) retlist.append(nt) return retlist @@ -503,7 +463,7 @@ def users(): def win_service_iter(): """Yields a list of WindowsService instances.""" for name, display_name in cext.winservice_enumerate(): - yield WindowsService(py2_strencode(name), py2_strencode(display_name)) + yield WindowsService(name, display_name) def win_service_get(name): @@ -547,10 +507,10 @@ def _query_config(self): ) # 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), + display_name=display_name, + binpath=binpath, + username=username, + start_type=start_type, ) def _query_status(self): @@ -628,7 +588,7 @@ def status(self): def description(self): """Service long description.""" - return py2_strencode(cext.winservice_query_descr(self.name())) + return cext.winservice_query_descr(self.name()) # utils @@ -696,12 +656,7 @@ def as_dict(self): 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). - return getattr(exc, "winerror", -1) in { + return isinstance(exc, PermissionError) or exc.winerror in { cext.ERROR_ACCESS_DENIED, cext.ERROR_PRIVILEGE_NOT_HELD, } @@ -712,7 +667,7 @@ def convert_oserror(exc, pid=None, name=None): assert isinstance(exc, OSError), exc if is_permission_err(exc): return AccessDenied(pid=pid, name=name) - if exc.errno == errno.ESRCH: + if isinstance(exc, ProcessLookupError): return NoSuchProcess(pid=pid, name=name) raise exc @@ -742,7 +697,7 @@ def wrapper(self, *args, **kwargs): for _ in range(times): # retries for roughly 1 second try: return fun(self, *args, **kwargs) - except WindowsError as _: + except OSError as _: err = _ if err.winerror == ERROR_PARTIAL_COPY: time.sleep(delay) @@ -750,8 +705,8 @@ def wrapper(self, *args, **kwargs): continue raise msg = ( - "{} retried {} times, converted to AccessDenied as it's still" - "returning {}".format(fun, times, err) + f"{fun} retried {times} times, converted to AccessDenied as it's " + f"still returning {err}" ) raise AccessDenied(pid=self.pid, name=self._name, msg=msg) @@ -805,7 +760,7 @@ def exe(self): if PYPY: try: exe = cext.proc_exe(self.pid) - except WindowsError as err: + except OSError 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: @@ -814,8 +769,6 @@ def exe(self): 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", ... @@ -827,26 +780,20 @@ def cmdline(self): # 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) + return 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) + return 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] + return cext.proc_cmdline(self.pid, use_peb=True) @wrap_exceptions @retry_error_partial_copy def environ(self): - ustr = cext.proc_environ(self.pid) - if ustr and not PY3: - assert isinstance(ustr, unicode), type(ustr) - return parse_environ_block(py2_strencode(ustr)) + s = cext.proc_environ(self.pid) + return parse_environ_block(s) def ppid(self): try: @@ -904,8 +851,6 @@ def memory_maps(self): else: for addr, perm, path, rss in raw: path = convert_dos_path(path) - if not PY3: - path = py2_strencode(path) addr = hex(addr) yield (addr, perm, path, rss) @@ -917,11 +862,7 @@ def kill(self): def send_signal(self, 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()), - }: + elif sig in {signal.CTRL_C_EVENT, signal.CTRL_BREAK_EVENT}: os.kill(self.pid, sig) else: msg = ( @@ -978,7 +919,7 @@ def username(self): if self.pid in {0, 4}: return 'NT AUTHORITY\\SYSTEM' domain, user = cext.proc_username(self.pid) - return py2_strencode(domain) + '\\' + py2_strencode(user) + return f"{domain}\\{user}" @wrap_exceptions def create_time(self, fast_only=False): @@ -1038,7 +979,7 @@ def cwd(self): # return a normalized pathname since the native C function appends # "\\" at the and of the path path = cext.proc_cwd(self.pid) - return py2_strencode(os.path.normpath(path)) + return os.path.normpath(path) @wrap_exceptions def open_files(self): @@ -1053,8 +994,6 @@ def open_files(self): for _file in raw_file_names: _file = convert_dos_path(_file) if isfile_strict(_file): - if not PY3: - _file = py2_strencode(_file) ntuple = _common.popenfile(_file, -1) ret.add(ntuple) return list(ret) @@ -1066,8 +1005,7 @@ def net_connections(self, kind='inet'): @wrap_exceptions def nice_get(self): value = cext.proc_priority_get(self.pid) - if enum is not None: - value = Priority(value) + value = Priority(value) return value @wrap_exceptions @@ -1077,8 +1015,7 @@ def nice_set(self, value): @wrap_exceptions def ionice_get(self): ret = cext.proc_io_priority_get(self.pid) - if enum is not None: - ret = IOPriority(ret) + ret = IOPriority(ret) return ret @wrap_exceptions @@ -1087,10 +1024,10 @@ def ionice_set(self, ioclass, value): msg = "value argument not accepted on Windows" raise TypeError(msg) if ioclass not in { - IOPRIO_VERYLOW, - IOPRIO_LOW, - IOPRIO_NORMAL, - IOPRIO_HIGH, + IOPriority.IOPRIO_VERYLOW, + IOPriority.IOPRIO_LOW, + IOPriority.IOPRIO_NORMAL, + IOPriority.IOPRIO_HIGH, }: raise ValueError("%s is not a valid priority" % ioclass) cext.proc_io_priority_set(self.pid, ioclass) @@ -1146,7 +1083,7 @@ def to_bitmask(ls): allcpus = list(range(len(per_cpu_times()))) for cpu in value: if cpu not in allcpus: - if not isinstance(cpu, (int, long)): + if not isinstance(cpu, int): raise TypeError( "invalid CPU %r; an integer is required" % cpu ) diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index a81128b518..5c6fab971a 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -611,11 +611,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { CPU_ZERO(&cpu_set); for (i = 0; i < seq_len; i++) { PyObject *item = PySequence_Fast_GET_ITEM(py_cpu_seq, i); -#if PY_MAJOR_VERSION >= 3 long value = PyLong_AsLong(item); -#else - long value = PyInt_AsLong(item); -#endif if (value == -1 || PyErr_Occurred()) goto error; CPU_SET(value, &cpu_set); diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index b58a3ce2a2..f8230ee75b 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -118,11 +118,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { cpucount_s = CPU_COUNT_S(setsize, mask); for (cpu = 0, count = cpucount_s; count; cpu++) { if (CPU_ISSET_S(cpu, setsize, mask)) { -#if PY_MAJOR_VERSION >= 3 PyObject *cpu_num = PyLong_FromLong(cpu); -#else - PyObject *cpu_num = PyInt_FromLong(cpu); -#endif if (cpu_num == NULL) goto error; if (PyList_Append(py_list, cpu_num)) { @@ -159,11 +155,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { if (!PySequence_Check(py_cpu_set)) { return PyErr_Format( PyExc_TypeError, -#if PY_MAJOR_VERSION >= 3 "sequence argument expected, got %R", Py_TYPE(py_cpu_set) -#else - "sequence argument expected, got %s", Py_TYPE(py_cpu_set)->tp_name -#endif ); } @@ -177,11 +169,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { if (!item) { return NULL; } -#if PY_MAJOR_VERSION >= 3 long value = PyLong_AsLong(item); -#else - long value = PyInt_AsLong(item); -#endif Py_XDECREF(item); if ((value == -1) || PyErr_Occurred()) { if (!PyErr_Occurred()) diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index d02cf794d5..e1a8f5a492 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -168,7 +168,6 @@ psutil_disk_usage_used(PyObject *self, PyObject *args) { PyObject *py_mount_point_bytes = NULL; char* mount_point; -#if PY_MAJOR_VERSION >= 3 if (!PyArg_ParseTuple(args, "O&O", PyUnicode_FSConverter, &py_mount_point_bytes, &py_default_value)) { return NULL; } @@ -177,11 +176,6 @@ psutil_disk_usage_used(PyObject *self, PyObject *args) { Py_XDECREF(py_mount_point_bytes); return NULL; } -#else - if (!PyArg_ParseTuple(args, "sO", &mount_point, &py_default_value)) { - return NULL; - } -#endif #ifdef ATTR_VOL_SPACEUSED /* Call getattrlist(ATTR_VOL_SPACEUSED) to get used space info. */ diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 0041745617..552929f38a 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -44,33 +44,6 @@ PyObject * psutil_disk_usage(PyObject *self, PyObject *args) { BOOL retval; ULARGE_INTEGER _, total, free; - -#if PY_MAJOR_VERSION <= 2 - char *path; - - if (PyArg_ParseTuple(args, "u", &path)) { - Py_BEGIN_ALLOW_THREADS - retval = GetDiskFreeSpaceExW((LPCWSTR)path, &_, &total, &free); - Py_END_ALLOW_THREADS - goto return_; - } - - // on Python 2 we also want to accept plain strings other - // than Unicode - PyErr_Clear(); // drop the argument parsing error - if (PyArg_ParseTuple(args, "s", &path)) { - Py_BEGIN_ALLOW_THREADS - retval = GetDiskFreeSpaceEx(path, &_, &total, &free); - Py_END_ALLOW_THREADS - goto return_; - } - - return NULL; - -return_: - if (retval == 0) - return PyErr_SetFromWindowsErrWithFilename(0, path); -#else PyObject *py_path; wchar_t *path; @@ -91,7 +64,7 @@ psutil_disk_usage(PyObject *self, PyObject *args) { if (retval == 0) return PyErr_SetExcFromWindowsErrWithFilenameObject(PyExc_OSError, 0, py_path); -#endif + return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); } diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 8d8f7d1c0a..9a5634b069 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -255,21 +255,14 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { continue; } -#if PY_MAJOR_VERSION >= 3 py_address = PyUnicode_FromString(buff_addr); -#else - py_address = PyString_FromString(buff_addr); -#endif if (py_address == NULL) goto error; if (netmaskIntRet != NULL) { -#if PY_MAJOR_VERSION >= 3 py_netmask = PyUnicode_FromString(buff_netmask); -#else - py_netmask = PyString_FromString(buff_netmask); -#endif - } else { + } + else { Py_INCREF(Py_None); py_netmask = Py_None; } diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 05fb502557..41fa9dda68 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -192,11 +192,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { CloseHandle(hProcess); -#if PY_MAJOR_VERSION >= 3 return PyLong_FromLong((long) ExitCode); -#else - return PyInt_FromLong((long) ExitCode); -#endif } diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f2cceeac55..13fed17787 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1,19 +1,19 @@ -# -*- 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 enum import errno import functools import gc +import importlib +import ipaddress import os import platform import random @@ -56,29 +56,8 @@ 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 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: - import unittest2 as unittest - - enum = None - if POSIX: from psutil._psposix import wait_pid @@ -86,7 +65,7 @@ # fmt: off __all__ = [ # constants - 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', + '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', @@ -131,9 +110,8 @@ 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 +CI_TESTING = GITHUB_ACTIONS COVERAGE = 'COVERAGE_RUN' in os.environ PYTEST_PARALLEL = "PYTEST_XDIST_WORKER" in os.environ # `make test-parallel` if LINUX and GITHUB_ACTIONS: @@ -199,13 +177,10 @@ def macos_version(): TESTFN_PREFIX = '$psutil-%s-' % os.getpid() else: TESTFN_PREFIX = '@psutil-%s-' % os.getpid() -UNICODE_SUFFIX = u"-ƒőő" +UNICODE_SUFFIX = "-ƒőő" # 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'} +INVALID_UNICODE_SUFFIX = b"f\xc0\x80".decode('utf8', 'surrogateescape') +ASCII_FS = sys.getfilesystemencoding().lower() in {"ascii", "us-ascii"} # --- paths @@ -273,7 +248,7 @@ def attempt(exe): exe = ( attempt(sys.executable) or attempt(os.path.realpath(sys.executable)) - or attempt(which("python%s.%s" % sys.version_info[:2])) + or attempt(shutil.which("python%s.%s" % sys.version_info[:2])) or attempt(psutil.Process().exe()) ) if not exe: @@ -458,13 +433,9 @@ def spawn_zombie(): time.sleep(3000) else: # this is the zombie process - s = socket.socket(socket.AF_UNIX) - with contextlib.closing(s): + with socket.socket(socket.AF_UNIX) as s: s.connect('%s') - if sys.version_info < (3, ): - pid = str(os.getpid()) - else: - pid = bytes(str(os.getpid()), 'ascii') + pid = bytes(str(os.getpid()), 'ascii') s.sendall(pid) """ % unix_file) tfile = None @@ -524,10 +495,7 @@ def sh(cmd, **kwds): 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() + stdout, stderr = p.communicate(timeout=GLOBAL_TIMEOUT) if p.returncode != 0: raise RuntimeError(stdout + stderr) if stderr: @@ -549,10 +517,7 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): """ def wait(proc, timeout): - if isinstance(proc, subprocess.Popen) and not PY3: - proc.wait() - else: - proc.wait(timeout) + proc.wait(timeout) if WINDOWS and isinstance(proc, subprocess.Popen): # Otherwise PID may still hang around. try: @@ -573,11 +538,12 @@ def sendsig(proc, sig): def term_subprocess_proc(proc, timeout): try: sendsig(proc, sig) + except ProcessLookupError: + pass except OSError as err: if WINDOWS and err.winerror == 6: # "invalid handle" pass - elif err.errno != errno.ESRCH: - raise + raise return wait(proc, timeout) def term_psutil_proc(proc, timeout): @@ -688,11 +654,7 @@ 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 + sp = wv.service_pack_major or 0 return (wv[0], wv[1], sp) @@ -749,10 +711,8 @@ def wrapper(*args, **kwargs): self.logfun(exc) self.sleep() continue - if PY3: - raise exc # noqa: PLE0704 - else: - raise # noqa: PLE0704 + + raise exc # noqa: PLE0704 # This way the user of the decorated function can change config # parameters. @@ -824,7 +784,7 @@ def retry_fun(fun): return fun() except FileNotFoundError: pass - except WindowsError as _: + except OSError as _: err = _ warn("ignoring %s" % (str(err))) time.sleep(0.01) @@ -877,7 +837,7 @@ def create_py_exe(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"): + if not shutil.which("gcc"): raise pytest.skip("gcc is not installed") if c_code is None: c_code = textwrap.dedent(""" @@ -959,7 +919,7 @@ def context(exc, match=None): yield einfo except exc as err: if match and not re.search(match, str(err)): - msg = '"{}" does not match "{}"'.format(match, str(err)) + msg = f'"{match}" does not match "{str(err)}"' raise AssertionError(msg) einfo._exc = err else: @@ -1000,24 +960,7 @@ def __call__(self, cls_or_meth): pytest = fake_pytest -class TestCase(unittest.TestCase): - # ...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): +class PsutilTestCase(unittest.TestCase): """Test class providing auto-cleanup wrappers on top of process test utilities. All test classes should derive from this one, even if we use pytest. @@ -1345,7 +1288,7 @@ def print_sysinfo(): info = collections.OrderedDict() # OS - if psutil.LINUX and which('lsb_release'): + if psutil.LINUX and shutil.which("lsb_release"): info['OS'] = sh('lsb_release -d -s') elif psutil.OSX: info['OS'] = 'Darwin %s' % platform.mac_ver()[0] @@ -1373,7 +1316,7 @@ def print_sysinfo(): # UNIX if psutil.POSIX: - if which('gcc'): + if shutil.which("gcc"): out = sh(['gcc', '--version']) info['gcc'] = str(out).split('\n')[0] else: @@ -1427,7 +1370,7 @@ def print_sysinfo(): # if WINDOWS: # os.system("tasklist") - # elif which("ps"): + # elif shutil.which("ps"): # os.system("ps aux") # print("=" * 70, file=sys.stderr) # NOQA @@ -1591,9 +1534,9 @@ def test_class_coverage(cls, test_class, ls): @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] != '_']) + this = {x[0] for x in cls.all} + ignored = {x[0] for x in cls.ignored} + klass = {x for x in dir(psutil.Process) if x[0] != '_'} leftout = (this | ignored) ^ klass if leftout: raise ValueError("uncovered Process class names: %r" % leftout) @@ -1732,7 +1675,7 @@ def wrapper(*args, **kwargs): # 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: + with socket.socket() as sock: sock.bind((host, 0)) return sock.getsockname()[1] @@ -1773,7 +1716,7 @@ 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: + with socket.socket(family, SOCK_STREAM) as ll: ll.bind(addr) ll.listen(5) addr = ll.getsockname() @@ -1846,22 +1789,15 @@ 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 + 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 @@ -1886,20 +1822,16 @@ def check_ntuple(conn): 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 + assert isinstance(conn.family, enum.IntEnum), 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): + with socket.socket(conn.family, conn.type) as s: try: s.bind((conn.laddr[0], 0)) - except socket.error as err: + except OSError as err: if err.errno != errno.EADDRNOTAVAIL: raise elif conn.family == AF_UNIX: @@ -1913,10 +1845,7 @@ def check_type(conn): 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 + assert isinstance(conn.type, enum.IntEnum), conn if conn.type == socket.SOCK_DGRAM: assert conn.status == psutil.CONN_NONE, conn.status @@ -1966,38 +1895,20 @@ def filter_proc_net_connections(cons): # =================================================================== -# --- compatibility +# --- import utils # =================================================================== 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) + 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 + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod # =================================================================== diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 2fd1015d73..8e53f4f3e7 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -9,10 +9,10 @@ """Tests specific to all BSD platforms.""" - import datetime import os import re +import shutil import time import psutil @@ -28,7 +28,6 @@ from psutil.tests import sh from psutil.tests import spawn_testproc from psutil.tests import terminate -from psutil.tests import which if BSD: @@ -36,7 +35,7 @@ PAGESIZE = getpagesize() # muse requires root privileges - MUSE_AVAILABLE = os.getuid() == 0 and which('muse') + MUSE_AVAILABLE = os.getuid() == 0 and shutil.which("muse") else: PAGESIZE = None MUSE_AVAILABLE = False @@ -122,12 +121,16 @@ def df(path): if abs(usage.used - used) > 10 * 1024 * 1024: raise self.fail("psutil=%s, df=%s" % (usage.used, used)) - @pytest.mark.skipif(not which('sysctl'), reason="sysctl cmd not available") + @pytest.mark.skipif( + not shutil.which("sysctl"), reason="sysctl cmd not available" + ) def test_cpu_count_logical(self): syst = sysctl("hw.ncpu") assert psutil.cpu_count(logical=True) == syst - @pytest.mark.skipif(not which('sysctl'), reason="sysctl cmd not available") + @pytest.mark.skipif( + not shutil.which("sysctl"), reason="sysctl cmd not available" + ) @pytest.mark.skipif( NETBSD, reason="skipped on NETBSD" # we check /proc/meminfo ) @@ -136,7 +139,7 @@ def test_virtual_memory_total(self): assert num == psutil.virtual_memory().total @pytest.mark.skipif( - not which('ifconfig'), reason="ifconfig cmd not available" + not shutil.which("ifconfig"), reason="ifconfig cmd not available" ) def test_net_if_stats(self): for name, stats in psutil.net_if_stats().items(): @@ -423,9 +426,7 @@ def secs2hours(secs): 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")] - ) + fields = {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:'] diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index bca12ff4b6..47f69fed5c 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -25,7 +25,6 @@ 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_NET_CONNECTIONS_UNIX from psutil.tests import SKIP_SYSCONS @@ -109,7 +108,7 @@ class TestUnconnectedSockets(ConnectionTestCase): def get_conn_from_sock(self, sock): cons = this_proc_net_connections(kind='all') - smap = dict([(c.fd, c) for c in cons]) + smap = {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. @@ -137,7 +136,7 @@ def check_socket(self, sock): # local address laddr = sock.getsockname() - if not laddr and PY3 and isinstance(laddr, bytes): + if not laddr and isinstance(laddr, bytes): # See: http://bugs.python.org/issue30205 laddr = laddr.decode() if sock.family == AF_INET6: diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index c0ec6a8f7e..7406d98ad0 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -22,13 +22,11 @@ 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 QEMU_USER from psutil.tests import SKIP_SYSCONS from psutil.tests import PsutilTestCase @@ -198,7 +196,6 @@ def test_memory_maps(self): 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. """ @@ -237,13 +234,13 @@ def test_cpu_count(self): def test_cpu_freq(self): if psutil.cpu_freq() is None: raise pytest.skip("cpu_freq() returns None") - self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int, long)) + self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int)) 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(): assert isinstance(k, str) - self.assert_ntuple_of_nums(v, type_=(int, long)) + self.assert_ntuple_of_nums(v, type_=int) def test_disk_partitions(self): # Duplicate of test_system.py. Keep it anyway. @@ -266,10 +263,7 @@ def test_net_if_addrs(self): for ifname, addrs in psutil.net_if_addrs().items(): assert isinstance(ifname, str) for addr in addrs: - if enum is not None and not PYPY: - assert isinstance(addr.family, enum.IntEnum) - else: - assert isinstance(addr.family, int) + assert isinstance(addr.family, enum.IntEnum) assert isinstance(addr.address, str) assert isinstance(addr.netmask, (str, type(None))) assert isinstance(addr.broadcast, (str, type(None))) @@ -280,10 +274,7 @@ def test_net_if_stats(self): for ifname, info in psutil.net_if_stats().items(): assert isinstance(ifname, str) assert isinstance(info.isup, bool) - if enum is not None: - assert isinstance(info.duplex, enum.IntEnum) - else: - assert isinstance(info.duplex, int) + assert isinstance(info.duplex, enum.IntEnum) assert isinstance(info.speed, int) assert isinstance(info.mtu, int) @@ -333,7 +324,4 @@ def test_negative_signal(self): p.terminate() code = p.wait() assert code == -signal.SIGTERM - if enum is not None: - assert isinstance(code, enum.IntEnum) - else: - assert isinstance(code, int) + assert isinstance(code, enum.IntEnum) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 15eaf5e2ed..ea96a0a739 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -6,7 +6,6 @@ """Linux specific tests.""" -from __future__ import division import collections import contextlib @@ -20,12 +19,10 @@ import textwrap import time import warnings +from unittest import mock import psutil from psutil import LINUX -from psutil._compat import PY3 -from psutil._compat import FileNotFoundError -from psutil._compat import basestring from psutil.tests import AARCH64 from psutil.tests import GITHUB_ACTIONS from psutil.tests import GLOBAL_TIMEOUT @@ -41,14 +38,12 @@ 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 pytest 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: @@ -73,11 +68,8 @@ 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): + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: return socket.inet_ntoa( fcntl.ioctl(s.fileno(), SIOCGIFADDR, struct.pack('256s', ifname))[ 20:24 @@ -88,11 +80,8 @@ def get_ipv4_address(ifname): 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): + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: return socket.inet_ntoa( fcntl.ioctl( s.fileno(), SIOCGIFNETMASK, struct.pack('256s', ifname) @@ -103,11 +92,8 @@ def get_ipv4_netmask(ifname): 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): + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: return socket.inet_ntoa( fcntl.ioctl( s.fileno(), SIOCGIFBRDADDR, struct.pack('256s', ifname) @@ -140,24 +126,12 @@ def get_ipv6_addresses(ifname): 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): + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as 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] + return ''.join(['%02x:' % char for char in info[18:24]])[:-1] def free_swap(): @@ -223,19 +197,15 @@ def mock_open_content(pairs): 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) + if isinstance(content, str): + return io.StringIO(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: + with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: yield m @@ -252,8 +222,7 @@ def open_mock(name, *args, **kwargs): 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: + with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: yield m @@ -495,10 +464,7 @@ def test_avail_old_missing_zoneinfo(self): 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 mock_open_exception("/proc/zoneinfo", FileNotFoundError): with warnings.catch_warnings(record=True) as ws: ret = psutil.virtual_memory() assert ret.available == 2057400 * 1024 + 4818144 * 1024 @@ -623,9 +589,7 @@ def test_missing_sin_sout(self): 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 mock_open_exception("/proc/vmstat", FileNotFoundError) as m: with warnings.catch_warnings(record=True) as ws: warnings.simplefilter("always") ret = psutil.swap_memory() @@ -714,14 +678,14 @@ def test_against_sysdev_cpu_num(self): assert psutil.cpu_count() == count @pytest.mark.skipif( - not which("nproc"), reason="nproc utility not available" + not shutil.which("nproc"), reason="nproc utility not available" ) def test_against_nproc(self): num = int(sh("nproc --all")) assert psutil.cpu_count(logical=True) == num @pytest.mark.skipif( - not which("lscpu"), reason="lscpu utility not available" + not shutil.which("lscpu"), reason="lscpu utility not available" ) def test_against_lscpu(self): out = sh("lscpu -p") @@ -768,7 +732,7 @@ def test_emulate_fallbacks(self): @pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUCountCores(PsutilTestCase): @pytest.mark.skipif( - not which("lscpu"), reason="lscpu utility not available" + not shutil.which("lscpu"), reason="lscpu utility not available" ) def test_against_lscpu(self): out = sh("lscpu -p") @@ -861,8 +825,7 @@ def open_mock(name, *args, **kwargs): 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("builtins.open", side_effect=open_mock): with mock.patch('os.path.exists', return_value=True): freq = psutil.cpu_freq() assert freq.current == 500.0 @@ -907,8 +870,7 @@ def open_mock(name, *args, **kwargs): 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("builtins.open", side_effect=open_mock): with mock.patch('os.path.exists', return_value=True): with mock.patch( 'psutil._pslinux.cpu_count_logical', return_value=2 @@ -930,7 +892,7 @@ 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, "") + raise FileNotFoundError elif name.endswith('/cpuinfo_cur_freq'): return io.BytesIO(b"200000") elif name == '/proc/cpuinfo': @@ -939,8 +901,7 @@ def open_mock(name, *args, **kwargs): 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("builtins.open", side_effect=open_mock): with mock.patch('os.path.exists', return_value=True): with mock.patch( 'psutil._pslinux.cpu_count_logical', return_value=1 @@ -1007,7 +968,8 @@ def test_ips(self): assert address in get_ipv6_addresses(name) # XXX - not reliable when having virtual NICs installed by Docker. - # @pytest.mark.skipif(not which('ip'), reason="'ip' utility not available") + # @pytest.mark.skipif(not shutil.which("ip"), + # reason="'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] @@ -1026,7 +988,7 @@ def test_ips(self): @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") class TestSystemNetIfStats(PsutilTestCase): @pytest.mark.skipif( - not which("ifconfig"), reason="ifconfig utility not available" + not shutil.which("ifconfig"), reason="ifconfig utility not available" ) def test_against_ifconfig(self): for name, stats in psutil.net_if_stats().items(): @@ -1046,7 +1008,7 @@ def test_mtu(self): assert stats.mtu == int(f.read().strip()) @pytest.mark.skipif( - not which("ifconfig"), reason="ifconfig utility not available" + not shutil.which("ifconfig"), reason="ifconfig utility not available" ) def test_flags(self): # first line looks like this: @@ -1081,7 +1043,7 @@ def test_flags(self): @pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemNetIOCounters(PsutilTestCase): @pytest.mark.skipif( - not which("ifconfig"), reason="ifconfig utility not available" + not shutil.which("ifconfig"), reason="ifconfig utility not available" ) @retry_on_failure() def test_against_ifconfig(self): @@ -1140,7 +1102,7 @@ def test_emulate_ipv6_unsupported(self, supports_ipv6, inet_ntop): s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.addCleanup(s.close) s.bind(("::1", 0)) - except socket.error: + except OSError: pass psutil.net_connections(kind='inet6') @@ -1198,7 +1160,7 @@ def test_zfs_fs(self): return # No ZFS partitions on this system. Let's fake one. - fake_file = io.StringIO(u"nodev\tzfs\n") + fake_file = io.StringIO("nodev\tzfs\n") with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m1: @@ -1407,7 +1369,7 @@ def test_comparisons(self): assert base == c @pytest.mark.skipif( - not which("findmnt"), reason="findmnt utility not available" + not shutil.which("findmnt"), reason="findmnt utility not available" ) @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") def test_against_findmnt(self): @@ -1455,24 +1417,23 @@ def test_no_procfs_on_import(self): def open_mock(name, *args, **kwargs): if name.startswith('/proc'): - raise IOError(errno.ENOENT, 'rejecting access for test') + raise FileNotFoundError return orig_open(name, *args, **kwargs) - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): + with mock.patch("builtins.open", side_effect=open_mock): reload_module(psutil) - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_times() - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_times(percpu=True) - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_percent() - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_percent(percpu=True) - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_times_percent() - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_times_percent(percpu=True) psutil.PROCFS_PATH = my_procfs @@ -1563,23 +1524,23 @@ def test_procfs_path(self): os.mkdir(tdir) try: psutil.PROCFS_PATH = tdir - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.virtual_memory() - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_times() - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.cpu_times(percpu=True) - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.boot_time() - # self.assertRaises(IOError, psutil.pids) - with pytest.raises(IOError): + # self.assertRaises(OSError, psutil.pids) + with pytest.raises(OSError): psutil.net_connections() - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.net_io_counters() - with pytest.raises(IOError): + with pytest.raises(OSError): psutil.net_if_stats() - # self.assertRaises(IOError, psutil.disk_io_counters) - with pytest.raises(IOError): + # self.assertRaises(OSError, psutil.disk_io_counters) + with pytest.raises(OSError): psutil.disk_partitions() with pytest.raises(psutil.NoSuchProcess): psutil.Process() @@ -1621,7 +1582,9 @@ def test_pid_exists_no_proc_status(self): @pytest.mark.skipif(not LINUX, reason="LINUX only") @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") class TestSensorsBattery(PsutilTestCase): - @pytest.mark.skipif(not which("acpi"), reason="acpi utility not available") + @pytest.mark.skipif( + not shutil.which("acpi"), reason="acpi utility not available" + ) def test_percent(self): out = sh("acpi -b") acpi_value = int(out.split(",")[1].strip().replace('%', '')) @@ -1637,8 +1600,7 @@ def open_mock(name, *args, **kwargs): 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 mock.patch("builtins.open", side_effect=open_mock) as m: assert psutil.sensors_battery().power_plugged is True assert ( psutil.sensors_battery().secsleft @@ -1651,15 +1613,14 @@ def test_emulate_power_plugged_2(self): # case code relies on /status file. def open_mock(name, *args, **kwargs): if name.endswith(('AC0/online', 'AC/online')): - raise IOError(errno.ENOENT, "") + raise FileNotFoundError elif name.endswith("/status"): - return io.StringIO(u"charging") + return io.StringIO("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: + with mock.patch("builtins.open", side_effect=open_mock) as m: assert psutil.sensors_battery().power_plugged is True assert m.called @@ -1672,8 +1633,7 @@ def open_mock(name, *args, **kwargs): 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 mock.patch("builtins.open", side_effect=open_mock) as m: assert psutil.sensors_battery().power_plugged is False assert m.called @@ -1682,15 +1642,14 @@ def test_emulate_power_not_plugged_2(self): # case code relies on /status file. def open_mock(name, *args, **kwargs): if name.endswith(('AC0/online', 'AC/online')): - raise IOError(errno.ENOENT, "") + raise FileNotFoundError elif name.endswith("/status"): - return io.StringIO(u"discharging") + return io.StringIO("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: + with mock.patch("builtins.open", side_effect=open_mock) as m: assert psutil.sensors_battery().power_plugged is False assert m.called @@ -1702,15 +1661,14 @@ def open_mock(name, *args, **kwargs): '/sys/class/power_supply/AC0/online', '/sys/class/power_supply/AC/online', )): - raise IOError(errno.ENOENT, "") + raise FileNotFoundError 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: + with mock.patch("builtins.open", side_effect=open_mock) as m: assert psutil.sensors_battery().power_plugged is None assert m.called @@ -1727,11 +1685,11 @@ def test_emulate_energy_full_not_avail(self): # Expected fallback on /capacity. with mock_open_exception( "/sys/class/power_supply/BAT0/energy_full", - IOError(errno.ENOENT, ""), + FileNotFoundError, ): with mock_open_exception( "/sys/class/power_supply/BAT0/charge_full", - IOError(errno.ENOENT, ""), + FileNotFoundError, ): with mock_open_content( {"/sys/class/power_supply/BAT0/capacity": b"88"} @@ -1741,14 +1699,14 @@ def test_emulate_energy_full_not_avail(self): 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, "") + "/sys/class/power_supply/AC/online", FileNotFoundError ): with mock_open_exception( - "/sys/class/power_supply/AC0/online", IOError(errno.ENOENT, "") + "/sys/class/power_supply/AC0/online", FileNotFoundError ): with mock_open_exception( "/sys/class/power_supply/BAT0/status", - IOError(errno.ENOENT, ""), + FileNotFoundError, ): assert psutil.sensors_battery().power_plugged is None @@ -1758,18 +1716,17 @@ class TestSensorsBatteryEmulated(PsutilTestCase): def test_it(self): def open_mock(name, *args, **kwargs): if name.endswith("/energy_now"): - return io.StringIO(u"60000000") + return io.StringIO("60000000") elif name.endswith("/power_now"): - return io.StringIO(u"0") + return io.StringIO("0") elif name.endswith("/energy_full"): - return io.StringIO(u"60000001") + return io.StringIO("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: + with mock.patch("builtins.open", side_effect=open_mock) as mopen: assert psutil.sensors_battery() is not None assert mlistdir.called assert mopen.called @@ -1780,9 +1737,9 @@ class TestSensorsTemperatures(PsutilTestCase): def test_emulate_class_hwmon(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): - return io.StringIO(u"name") + return io.StringIO("name") elif name.endswith('/temp1_label'): - return io.StringIO(u"label") + return io.StringIO("label") elif name.endswith('/temp1_input'): return io.BytesIO(b"30000") elif name.endswith('/temp1_max'): @@ -1793,8 +1750,7 @@ def open_mock(name, *args, **kwargs): 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("builtins.open", side_effect=open_mock): # Test case with /sys/class/hwmon with mock.patch( 'glob.glob', return_value=['/sys/class/hwmon/hwmon0/temp1'] @@ -1812,9 +1768,9 @@ def open_mock(name, *args, **kwargs): elif name.endswith('temp'): return io.BytesIO(b"30000") elif name.endswith('0_type'): - return io.StringIO(u"critical") + return io.StringIO("critical") elif name.endswith('type'): - return io.StringIO(u"name") + return io.StringIO("name") else: return orig_open(name, *args, **kwargs) @@ -1833,8 +1789,7 @@ def glob_mock(path): 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("builtins.open", side_effect=open_mock): with mock.patch('glob.glob', create=True, side_effect=glob_mock): temp = psutil.sensors_temperatures()['name'][0] assert temp.label == '' # noqa @@ -1848,17 +1803,16 @@ class TestSensorsFans(PsutilTestCase): def test_emulate_data(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): - return io.StringIO(u"name") + return io.StringIO("name") elif name.endswith('/fan1_label'): - return io.StringIO(u"label") + return io.StringIO("label") elif name.endswith('/fan1_input'): - return io.StringIO(u"2000") + return io.StringIO("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("builtins.open", side_effect=open_mock): with mock.patch( 'glob.glob', return_value=['/sys/class/hwmon/hwmon2/fan1'] ): @@ -1946,14 +1900,13 @@ def get_test_file(fname): assert get_test_file(testfn).mode == "r+" with open(testfn, "a+"): assert get_test_file(testfn).mode == "a+" - # note: "x" bit is not supported - if PY3: - safe_rmpath(testfn) - with open(testfn, "x"): - assert get_test_file(testfn).mode == "w" - safe_rmpath(testfn) - with open(testfn, "x+"): - assert get_test_file(testfn).mode == "r+" + + safe_rmpath(testfn) + with open(testfn, "x"): + assert get_test_file(testfn).mode == "w" + safe_rmpath(testfn) + with open(testfn, "x+"): + assert get_test_file(testfn).mode == "r+" def test_open_files_file_gone(self): # simulates a file which gets deleted during open_files() @@ -1965,7 +1918,7 @@ def test_open_files_file_gone(self): call_until(lambda: len(p.open_files()) != len(files)) with mock.patch( 'psutil._pslinux.os.readlink', - side_effect=OSError(errno.ENOENT, ""), + side_effect=FileNotFoundError, ) as m: assert p.open_files() == [] assert m.called @@ -1987,9 +1940,8 @@ def test_open_files_fd_gone(self): with open(self.get_testfn(), 'w'): # give the kernel some time to see the new file call_until(lambda: len(p.open_files()) != len(files)) - patch_point = 'builtins.open' if PY3 else '__builtin__.open' with mock.patch( - patch_point, side_effect=IOError(errno.ENOENT, "") + "builtins.open", side_effect=FileNotFoundError ) as m: assert p.open_files() == [] assert m.called @@ -2031,13 +1983,13 @@ def test_terminal_mocked(self): def test_cmdline_mocked(self): # see: https://github.com/giampaolo/psutil/issues/639 p = psutil.Process() - fake_file = io.StringIO(u'foo\x00bar\x00') + fake_file = io.StringIO('foo\x00bar\x00') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: assert p.cmdline() == ['foo', 'bar'] assert m.called - fake_file = io.StringIO(u'foo\x00bar\x00\x00') + fake_file = io.StringIO('foo\x00bar\x00\x00') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: @@ -2047,13 +1999,13 @@ def test_cmdline_mocked(self): def test_cmdline_spaces_mocked(self): # see: https://github.com/giampaolo/psutil/issues/1179 p = psutil.Process() - fake_file = io.StringIO(u'foo bar ') + fake_file = io.StringIO('foo bar ') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: assert p.cmdline() == ['foo', 'bar'] assert m.called - fake_file = io.StringIO(u'foo bar ') + fake_file = io.StringIO('foo bar ') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: @@ -2064,7 +2016,7 @@ 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') + fake_file = io.StringIO('foo\x20bar\x00') with mock.patch( 'psutil._common.open', return_value=fake_file, create=True ) as m: @@ -2085,13 +2037,12 @@ def test_threads_mocked(self): # of raising NSP. def open_mock_1(name, *args, **kwargs): if name.startswith('/proc/%s/task' % os.getpid()): - raise IOError(errno.ENOENT, "") + raise FileNotFoundError 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: + with mock.patch("builtins.open", side_effect=open_mock_1) as m: ret = psutil.Process().threads() assert m.called assert ret == [] @@ -2100,17 +2051,17 @@ def open_mock_1(name, *args, **kwargs): # exception. def open_mock_2(name, *args, **kwargs): if name.startswith('/proc/%s/task' % os.getpid()): - raise IOError(errno.EPERM, "") + raise PermissionError else: return orig_open(name, *args, **kwargs) - with mock.patch(patch_point, side_effect=open_mock_2): + with mock.patch("builtins.open", side_effect=open_mock_2): with pytest.raises(psutil.AccessDenied): psutil.Process().threads() def test_exe_mocked(self): with mock.patch( - 'psutil._pslinux.readlink', side_effect=OSError(errno.ENOENT, "") + 'psutil._pslinux.readlink', side_effect=FileNotFoundError ) as m: # de-activate guessing from cmdline() with mock.patch( @@ -2124,7 +2075,7 @@ 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, "") + '/proc/%s/smaps' % os.getpid(), FileNotFoundError ) as m: p = psutil.Process() with pytest.raises(FileNotFoundError): @@ -2146,7 +2097,7 @@ def test_rlimit_zombie(self): # 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, "") + "resource.prlimit", side_effect=OSError(errno.ENOSYS, "") ) as m1: with mock.patch( "psutil._pslinux.Process._is_zombie", return_value=True diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index e249ca5169..fd4cd09a48 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -14,7 +14,6 @@ because of how its JIT handles memory, so tests are skipped. """ -from __future__ import print_function import functools import os @@ -28,8 +27,6 @@ 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 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index cf98f8b4bf..6771dbf982 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -1,5 +1,4 @@ #!/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 @@ -9,13 +8,15 @@ import ast import collections -import errno +import contextlib +import io import json import os import pickle import socket import stat import sys +from unittest import mock import psutil import psutil.tests @@ -30,9 +31,6 @@ 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 CI_TESTING from psutil.tests import HAS_BATTERY from psutil.tests import HAS_MEMORY_MAPS @@ -45,7 +43,6 @@ from psutil.tests import QEMU_USER from psutil.tests import SCRIPTS_DIR from psutil.tests import PsutilTestCase -from psutil.tests import mock from psutil.tests import process_namespace from psutil.tests import pytest from psutil.tests import reload_module @@ -202,7 +199,7 @@ def test_process__eq__(self): assert p1 != 'foo' def test_process__hash__(self): - s = set([psutil.Process(), psutil.Process()]) + s = {psutil.Process(), psutil.Process()} assert len(s) == 1 @@ -217,7 +214,6 @@ def test__all__(self): for name in dir_psutil: if name in { 'debug', - 'long', 'tests', 'test', 'PermissionError', @@ -240,7 +236,7 @@ def test__all__(self): # 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 + # Can't do `from psutil import *` as it won't work # so we simply iterate over __all__. for name in psutil.__all__: assert name in dir_psutil @@ -338,10 +334,6 @@ def check(ret): assert b.pid == 4567 assert b.name == 'name' - # # XXX: https://github.com/pypa/setuptools/pull/2896 - # @pytest.mark.skipif(APPVEYOR, - # reason="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): @@ -587,7 +579,7 @@ def test_supports_ipv6(self): supports_ipv6.cache_clear() with mock.patch( - 'psutil._common.socket.socket', side_effect=socket.error + 'psutil._common.socket.socket', side_effect=OSError ) as s: assert not supports_ipv6() assert s.called @@ -609,7 +601,7 @@ def test_supports_ipv6(self): supports_ipv6.cache_clear() assert s.called else: - with pytest.raises(socket.error): + with pytest.raises(OSError): sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) try: sock.bind(("::1", 0)) @@ -620,31 +612,19 @@ 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") - ): + with mock.patch('psutil._common.os.stat', side_effect=PermissionError): with pytest.raises(OSError): isfile_strict(this_file) with mock.patch( - 'psutil._common.os.stat', side_effect=OSError(errno.EACCES, "foo") - ): - with pytest.raises(OSError): - isfile_strict(this_file) - with mock.patch( - 'psutil._common.os.stat', side_effect=OSError(errno.ENOENT, "foo") + 'psutil._common.os.stat', side_effect=FileNotFoundError ): 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 mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): - with redirect_stderr(StringIO()) as f: + with contextlib.redirect_stderr(io.StringIO()) as f: debug("hello") sys.stderr.flush() msg = f.getvalue() @@ -654,7 +634,7 @@ def test_debug(self): # supposed to use repr(exc) with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): - with redirect_stderr(StringIO()) as f: + with contextlib.redirect_stderr(io.StringIO()) as f: debug(ValueError("this is an error")) msg = f.getvalue() assert "ignoring ValueError" in msg @@ -662,7 +642,7 @@ def test_debug(self): # supposed to use str(exc), because of extra info about file name with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): - with redirect_stderr(StringIO()) as f: + with contextlib.redirect_stderr(io.StringIO()) as f: exc = OSError(2, "no such file") exc.filename = "/foo" debug(exc) @@ -838,7 +818,7 @@ def test_cache_wrap(self): assert cache[1] == { 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100} } - assert cache[2] == {'disk_io': {'disk1': set([('disk1', 2)])}} + assert cache[2] == {'disk_io': {'disk1': {('disk1', 2)}}} def check_cache_info(): cache = wrap_numbers.cache_info() @@ -849,7 +829,7 @@ def check_cache_info(): ('disk1', 2): 100, } } - assert cache[2] == {'disk_io': {'disk1': set([('disk1', 2)])}} + assert cache[2] == {'disk_io': {'disk1': {('disk1', 2)}}} # then it remains the same input = {'disk1': nt(100, 100, 10)} @@ -873,7 +853,7 @@ def check_cache_info(): assert cache[1] == { 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190} } - assert cache[2] == {'disk_io': {'disk1': set([('disk1', 2)])}} + assert cache[2] == {'disk_io': {'disk1': {('disk1', 2)}}} def test_cache_changing_keys(self): input = {'disk1': nt(5, 5, 5)} @@ -949,7 +929,7 @@ def assert_stdout(exe, *args, **kwargs): @staticmethod def assert_syntax(exe): exe = os.path.join(SCRIPTS_DIR, exe) - with open(exe, encoding="utf8") if PY3 else open(exe) as f: + with open(exe, encoding="utf8") as f: src = f.read() ast.parse(src) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 551eaec50c..6c8ac7f492 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -1,5 +1,4 @@ #!/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 @@ -11,8 +10,10 @@ import errno import os import re +import shutil import subprocess import time +from unittest import mock import psutil from psutil import AIX @@ -27,14 +28,12 @@ from psutil.tests import PYTHON_EXE from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase -from psutil.tests import mock from psutil.tests import pytest 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: @@ -288,7 +287,7 @@ def test_exe(self): assert ps_pathname == psutil_pathname except AssertionError: # certain platforms such as BSD are more accurate returning: - # "/usr/local/bin/python2.7" + # "/usr/local/bin/python3.7" # ...instead of: # "/usr/local/bin/python" # We do not want to consider this difference in accuracy @@ -348,7 +347,7 @@ def test_pids(self): # for some reason ifconfig -a does not report all interfaces # returned by psutil @pytest.mark.skipif(SUNOS, reason="unreliable on SUNOS") - @pytest.mark.skipif(not which('ifconfig'), reason="no ifconfig cmd") + @pytest.mark.skipif(not shutil.which("ifconfig"), reason="no ifconfig cmd") @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") def test_nic_names(self): output = sh("ifconfig -a") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 76dcbbf32a..35432b1dbd 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -7,8 +7,10 @@ """Tests for psutil.Process class.""" import collections +import contextlib import errno import getpass +import io import itertools import os import signal @@ -20,6 +22,7 @@ import textwrap import time import types +from unittest import mock import psutil from psutil import AIX @@ -32,12 +35,6 @@ from psutil import POSIX 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 redirect_stderr -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 @@ -60,7 +57,6 @@ 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 pytest from psutil.tests import reap_children @@ -130,16 +126,12 @@ def test_send_signal(self): def test_send_signal_mocked(self): sig = signal.SIGTERM p = self.spawn_psproc() - with mock.patch( - 'psutil.os.kill', side_effect=OSError(errno.ESRCH, "") - ): + with mock.patch('psutil.os.kill', side_effect=ProcessLookupError): with pytest.raises(psutil.NoSuchProcess): p.send_signal(sig) p = self.spawn_psproc() - with mock.patch( - 'psutil.os.kill', side_effect=OSError(errno.EPERM, "") - ): + with mock.patch('psutil.os.kill', side_effect=PermissionError): with pytest.raises(psutil.AccessDenied): p.send_signal(sig) @@ -354,10 +346,7 @@ def test_io_counters(self): # 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) + f.write(bytes("x" * 1000000, 'ascii')) io2 = p.io_counters() assert io2.write_count >= io1.write_count assert io2.write_bytes >= io1.write_bytes @@ -498,10 +487,10 @@ def test_rlimit(self): f.write(b"X" * 1024) # write() or flush() doesn't always cause the exception # but close() will. - with pytest.raises(IOError) as exc: + with pytest.raises(OSError) as exc: with open(testfn, "wb") as f: f.write(b"X" * 1025) - assert (exc.value.errno if PY3 else exc.value[0]) == errno.EFBIG + assert exc.value.errno == errno.EFBIG finally: p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) @@ -689,7 +678,7 @@ def test_memory_maps(self): if fname in {'addr', 'perms'}: assert value, value else: - assert isinstance(value, (int, long)) + assert isinstance(value, int) assert value >= 0, value @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") @@ -734,7 +723,7 @@ def test_exe(self): assert normcase(exe) == normcase(PYTHON_EXE) else: # certain platforms such as BSD are more accurate returning: - # "/usr/local/bin/python2.7" + # "/usr/local/bin/python3.7" # ...instead of: # "/usr/local/bin/python" # We do not want to consider this difference in accuracy @@ -811,7 +800,6 @@ def test_name(self): @pytest.mark.skipif(PYPY or QEMU_USER, reason="unreliable on PYPY") @pytest.mark.skipif(QEMU_USER, reason="unreliable on QEMU user") - @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) cmdline = [ @@ -842,9 +830,7 @@ def test_long_name(self): # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") # @pytest.mark.skipif(AIX, reason="broken on AIX") # @pytest.mark.skipif(PYPY, reason="broken on PYPY") - # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") - # @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") - # @retry_on_failure() + # @pytest.mark.skipif(QEMU_USER, reason="broken on QEMU user") # def test_prog_w_funky_name(self): # # Test that name(), exe() and cmdline() correctly handle programs # # with funky chars such as spaces and ")", see: @@ -868,9 +854,8 @@ def test_uids(self): assert real == os.getuid() # os.geteuid() refers to "effective" uid assert 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. + # No such thing as os.getsuid() ("saved" uid), but we have + # os.getresuid() which returns all of them. if hasattr(os, "getresuid"): assert os.getresuid() == p.uids() @@ -882,9 +867,8 @@ def test_gids(self): assert real == os.getgid() # os.geteuid() refers to "effective" uid assert 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. + # No such thing as os.getsgid() ("saved" gid), but we have + # os.getresgid() which returns all of them. if hasattr(os, "getresuid"): assert os.getresgid() == p.gids() @@ -1066,8 +1050,6 @@ def test_cpu_affinity_all_combinations(self): # TODO: #595 @pytest.mark.skipif(BSD, reason="broken on BSD") - # can't find any process file on Appveyor - @pytest.mark.skipif(APPVEYOR, reason="unreliable on APPVEYOR") def test_open_files(self): p = psutil.Process() testfn = self.get_testfn() @@ -1107,8 +1089,6 @@ def test_open_files(self): # TODO: #595 @pytest.mark.skipif(BSD, reason="broken on BSD") - # can't find any process file on Appveyor - @pytest.mark.skipif(APPVEYOR, reason="unreliable on APPVEYOR") def test_open_files_2(self): # test fd and path fields p = psutil.Process() @@ -1441,11 +1421,6 @@ def test_zombie_process_status_w_exc(self): def test_reused_pid(self): # Emulate a case where PID has been reused by another process. - if PY3: - from io import StringIO - else: - from StringIO import StringIO - subp = self.spawn_testproc() p = psutil.Process(subp.pid) p._ident = (p.pid, p.create_time() + 100) @@ -1457,7 +1432,7 @@ def test_reused_pid(self): # make sure is_running() removed PID from process_iter() # internal cache with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): - with redirect_stderr(StringIO()) as f: + with contextlib.redirect_stderr(io.StringIO()) as f: list(psutil.process_iter()) assert ( "refreshing Process instance for reused PID %s" % p.pid @@ -1544,13 +1519,12 @@ def clean_dict(d): ]) for name in exclude: d.pop(name, None) - return dict([ - ( - k.replace("\r", "").replace("\n", ""), - v.replace("\r", "").replace("\n", ""), - ) + return { + k.replace("\r", "").replace("\n", ""): v.replace( + "\r", "" + ).replace("\n", "") for k, v in d.items() - ]) + } self.maxDiff = None p = psutil.Process() @@ -1677,7 +1651,7 @@ def tearDownClass(cls): reap_children() def test_misc(self): - # XXX this test causes a ResourceWarning on Python 3 because + # XXX this test causes a ResourceWarning because # psutil.__subproc instance doesn't get properly freed. # Not sure what to do though. cmd = [ diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 780ea194b6..29f3f894e4 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -27,10 +27,6 @@ from psutil import OSX from psutil import POSIX from psutil import WINDOWS -from psutil._compat import PY3 -from psutil._compat import FileNotFoundError -from psutil._compat import long -from psutil._compat import unicode from psutil.tests import CI_TESTING from psutil.tests import PYTEST_PARALLEL from psutil.tests import QEMU_USER @@ -161,7 +157,7 @@ def cmdline(self, ret, info): assert isinstance(part, str) def exe(self, ret, info): - assert isinstance(ret, (str, unicode)) + assert isinstance(ret, str) assert ret.strip() == ret if ret: if WINDOWS and not ret.endswith('.exe'): @@ -184,12 +180,12 @@ def pid(self, ret, info): assert ret >= 0 def ppid(self, ret, info): - assert isinstance(ret, (int, long)) + assert isinstance(ret, int) assert ret >= 0 proc_info(ret) def name(self, ret, info): - assert isinstance(ret, (str, unicode)) + assert isinstance(ret, str) if WINDOWS and not ret and is_win_secure_system_proc(info['pid']): # https://github.com/giampaolo/psutil/issues/2338 return @@ -245,7 +241,7 @@ def status(self, ret, info): def io_counters(self, ret, info): assert is_namedtuple(ret) for field in ret: - assert isinstance(field, (int, long)) + assert isinstance(field, int) if field != -1: assert field >= 0 @@ -306,7 +302,7 @@ def cpu_num(self, ret, info): def memory_info(self, ret, info): assert is_namedtuple(ret) for value in ret: - assert isinstance(value, (int, long)) + assert isinstance(value, int) assert value >= 0 if WINDOWS: assert ret.peak_wset >= ret.wset @@ -319,7 +315,7 @@ def memory_full_info(self, ret, info): total = psutil.virtual_memory().total for name in ret._fields: value = getattr(ret, name) - assert isinstance(value, (int, long)) + assert isinstance(value, int) assert value >= 0 if LINUX or (OSX and name in {'vms', 'data'}): # On Linux there are processes (e.g. 'goa-daemon') whose @@ -368,7 +364,7 @@ def net_connections(self, ret, info): check_connection_ntuple(conn) def cwd(self, ret, info): - assert isinstance(ret, (str, unicode)) + assert isinstance(ret, str) assert ret.strip() == ret if ret: assert os.path.isabs(ret), ret @@ -423,7 +419,7 @@ def memory_maps(self, ret, info): if not WINDOWS: assert value, repr(value) else: - assert isinstance(value, (int, long)) + assert isinstance(value, int) assert value >= 0 def num_handles(self, ret, info): @@ -441,15 +437,12 @@ def nice(self, ret, info): if x.endswith('_PRIORITY_CLASS') ] assert ret in priorities - if PY3: - assert isinstance(ret, enum.IntEnum) - else: - assert isinstance(ret, int) + assert isinstance(ret, enum.IntEnum) def num_ctx_switches(self, ret, info): assert is_namedtuple(ret) for value in ret: - assert isinstance(value, (int, long)) + assert isinstance(value, int) assert value >= 0 def rlimit(self, ret, info): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 0b69ada78f..1e814b1e8f 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -6,8 +6,8 @@ """Tests for system APIS.""" -import contextlib import datetime +import enum import errno import os import platform @@ -17,6 +17,7 @@ import socket import sys import time +from unittest import mock import psutil from psutil import AIX @@ -29,9 +30,6 @@ 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 @@ -51,8 +49,6 @@ 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 pytest from psutil.tests import retry_on_failure @@ -194,7 +190,7 @@ def test_2(procs, callback): sproc1.terminate() sproc2.terminate() gone, alive = test_2(procs, callback) - assert set(pids) == set([sproc1.pid, sproc2.pid, sproc3.pid]) + assert set(pids) == {sproc1.pid, sproc2.pid, sproc3.pid} for p in gone: assert hasattr(p, 'returncode') @@ -335,7 +331,7 @@ def test_virtual_memory(self): for name in mem._fields: value = getattr(mem, name) if name != 'percent': - assert isinstance(value, (int, long)) + assert isinstance(value, int) if name != 'total': if not value >= 0: raise self.fail("%r < 0 (%s)" % (name, value)) @@ -605,7 +601,7 @@ def check_ls(ls): assert nt.current <= nt.max for name in nt._fields: value = getattr(nt, name) - assert isinstance(value, (int, long, float)) + assert isinstance(value, (int, float)) assert value >= 0 ls = psutil.cpu_freq(percpu=True) @@ -823,7 +819,7 @@ def test_net_if_addrs(self): # self.assertEqual(sorted(nics.keys()), # sorted(psutil.net_io_counters(pernic=True).keys())) - families = set([socket.AF_INET, socket.AF_INET6, psutil.AF_LINK]) + families = {socket.AF_INET, socket.AF_INET6, psutil.AF_LINK} for nic, addrs in nics.items(): assert isinstance(nic, str) assert len(set(addrs)) == len(addrs) @@ -833,14 +829,12 @@ def test_net_if_addrs(self): assert isinstance(addr.netmask, (str, type(None))) assert isinstance(addr.broadcast, (str, type(None))) assert addr.family in families - if PY3 and not PYPY: - assert isinstance(addr.family, enum.IntEnum) + assert isinstance(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): + with socket.socket(addr.family) as s: s.bind((addr.address, 0)) elif addr.family == socket.AF_INET6: info = socket.getaddrinfo( @@ -852,8 +846,7 @@ def test_net_if_addrs(self): socket.AI_PASSIVE, )[0] af, socktype, proto, _canonname, sa = info - s = socket.socket(af, socktype, proto) - with contextlib.closing(s): + with socket.socket(af, socktype, proto) as s: s.bind(sa) for ip in ( addr.address, @@ -981,5 +974,5 @@ def test_sensors_fans(self): assert isinstance(name, str) for entry in entries: assert isinstance(entry.label, str) - assert isinstance(entry.current, (int, long)) + assert isinstance(entry.current, int) assert entry.current >= 0 diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 1c83a94c65..e6c3afa858 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -1,5 +1,4 @@ #!/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 @@ -8,7 +7,6 @@ """Tests for testing utils (psutil.tests namespace).""" import collections -import contextlib import errno import os import socket @@ -17,6 +15,7 @@ import textwrap import unittest import warnings +from unittest import mock import psutil import psutil.tests @@ -26,7 +25,6 @@ from psutil._common import open_binary from psutil._common import open_text from psutil._common import supports_ipv6 -from psutil._compat import PY3 from psutil.tests import CI_TESTING from psutil.tests import COVERAGE from psutil.tests import HAS_NET_CONNECTIONS_UNIX @@ -44,7 +42,6 @@ from psutil.tests import filter_proc_net_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 pytest from psutil.tests import reap_children @@ -159,7 +156,7 @@ def test_wait_for_file_empty(self): def test_wait_for_file_no_file(self): testfn = self.get_testfn() with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): - with pytest.raises(IOError): + with pytest.raises(OSError): wait_for_file(testfn) def test_wait_for_file_no_delete(self): @@ -298,14 +295,13 @@ def test_terminate(self): class TestNetUtils(PsutilTestCase): def bind_socket(self): port = get_free_port() - with contextlib.closing(bind_socket(addr=('', port))) as s: + with bind_socket(addr=('', port)) as s: assert s.getsockname()[1] == port @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_bind_unix_socket(self): name = self.get_testfn() - sock = bind_unix_socket(name) - with contextlib.closing(sock): + with bind_unix_socket(name) as sock: assert sock.family == socket.AF_UNIX assert sock.type == socket.SOCK_STREAM assert sock.getsockname() == name @@ -313,20 +309,17 @@ def test_bind_unix_socket(self): 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): + with bind_unix_socket(name, type=socket.SOCK_DGRAM) as sock: assert sock.type == socket.SOCK_DGRAM def test_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. - assert server.getsockname() == addr - assert client.getpeername() == addr - assert client.getsockname() != addr + with server, client: + # Ensure they are connected and the positions are correct. + assert server.getsockname() == addr + assert client.getpeername() == addr + assert client.getsockname() != addr @pytest.mark.skipif(not POSIX, reason="POSIX only") @pytest.mark.skipif( @@ -507,7 +500,6 @@ def foo(self): assert result.wasSuccessful() assert len(result.skipped) == 0 - @pytest.mark.skipif(not PY3, reason="not PY3") def test_skip(self): class TestCase(unittest.TestCase): def foo(self): diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index c03aabd8fd..d8a8c4bfc5 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -1,5 +1,4 @@ #!/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 @@ -20,13 +19,8 @@ * 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") + * sys.getfilesystemencodeerrors() or "surrogatescape" on POSIX and + "replace" on Windows. For a detailed explanation of how psutil handles unicode see #1040. @@ -74,18 +68,13 @@ import os import shutil -import traceback import warnings from contextlib import closing import psutil from psutil import BSD -from psutil import MACOS from psutil import POSIX from psutil import WINDOWS -from psutil._compat import PY3 -from psutil._compat import super -from psutil.tests import APPVEYOR from psutil.tests import ASCII_FS from psutil.tests import CI_TESTING from psutil.tests import HAS_ENVIRON @@ -109,27 +98,6 @@ 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. @@ -142,7 +110,7 @@ def try_unicode(suffix): sproc = spawn_testproc(cmd=[testfn]) shutil.copyfile(testfn, testfn + '-2') safe_rmpath(testfn + '-2') - except (UnicodeEncodeError, IOError): + except (UnicodeEncodeError, OSError): return False else: return True @@ -180,23 +148,18 @@ def setUp(self): @pytest.mark.xdist_group(name="serial") @pytest.mark.skipif(ASCII_FS, reason="ASCII fs") -@pytest.mark.skipif(PYPY and not PY3, reason="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) + return self.funky_name in os.listdir(".") # --- - @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") def test_proc_exe(self): cmd = [ self.funky_name, @@ -222,7 +185,6 @@ def test_proc_name(self): if self.expect_exact_path_match(): assert name == os.path.basename(self.funky_name) - @pytest.mark.skipif(MACOS and not PY3, reason="broken MACOS + PY2") def test_proc_cmdline(self): cmd = [ self.funky_name, @@ -265,13 +227,7 @@ def test_proc_open_files(self): @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_proc_net_connections(self): name = self.get_testfn(suffix=self.funky_suffix) - try: - sock = bind_unix_socket(name) - except UnicodeEncodeError: - if PY3: - raise - else: - raise pytest.skip("not supported") + sock = bind_unix_socket(name) with closing(sock): conn = psutil.Process().net_connections('unix')[0] assert isinstance(conn.laddr, str) @@ -290,13 +246,7 @@ def find_sock(cons): 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 pytest.skip("not supported") + sock = bind_unix_socket(name) with closing(sock): cons = psutil.net_connections(kind='unix') conn = find_sock(cons) @@ -310,13 +260,8 @@ def test_disk_usage(self): psutil.disk_usage(dname) @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") - @pytest.mark.skipif( - not PY3, reason="ctypes does not support unicode on PY2" - ) @pytest.mark.skipif(PYPY, reason="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): @@ -339,7 +284,6 @@ class TestFSAPIsWithInvalidPath(TestFSAPIs): funky_suffix = INVALID_UNICODE_SUFFIX def expect_exact_path_match(self): - # Invalid unicode names are supposed to work on Python 2. return True @@ -351,16 +295,13 @@ def expect_exact_path_match(self): class TestNonFSAPIS(BaseUnicodeTest): """Unicode tests for non fs-related APIs.""" - funky_suffix = UNICODE_SUFFIX if PY3 else 'è' + funky_suffix = UNICODE_SUFFIX @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") @pytest.mark.skipif(PYPY and WINDOWS, reason="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). + # with fs paths. env = os.environ.copy() env['FUNNY_ARG'] = self.funky_suffix sproc = self.spawn_testproc(env=env) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 161e2f35d8..4e5d2484dd 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -1,5 +1,4 @@ #!/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 @@ -8,32 +7,27 @@ """Windows specific tests.""" import datetime -import errno import glob import os import platform import re +import shutil import signal import subprocess import sys import time import warnings +from unittest import mock 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 pytest from psutil.tests import retry_on_failure from psutil.tests import sh @@ -58,10 +52,6 @@ @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") @pytest.mark.skipif(PYPY, reason="pywin32 not available on PYPY") -# https://github.com/giampaolo/psutil/pull/1762#issuecomment-632892692 -@pytest.mark.skipif( - GITHUB_ACTIONS and not PY3, reason="pywin32 broken on GITHUB + PY2" -) class WindowsTestCase(PsutilTestCase): pass @@ -72,7 +62,7 @@ def powershell(cmd): >>> powershell( "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") """ - if not which("powershell.exe"): + if not shutil.which("powershell.exe"): raise pytest.skip("powershell.exe not available") cmdline = ( 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive ' @@ -199,13 +189,12 @@ def test_percent_swapmem(self): # time.localtime(p.create_time())) # Note: this test is not very reliable - @pytest.mark.skipif(APPVEYOR, reason="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]) + wmi_pids = {x.ProcessId for x in w} psutil_pids = set(psutil.pids()) assert wmi_pids == psutil_pids @@ -563,7 +552,7 @@ def test_num_handles(self): def test_error_partial_copy(self): # https://github.com/giampaolo/psutil/issues/875 - exc = WindowsError() + exc = OSError() exc.winerror = 299 with mock.patch("psutil._psplatform.cext.proc_cwd", side_effect=exc): with mock.patch("time.sleep") as m: @@ -676,7 +665,7 @@ 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"), + side_effect=PermissionError, ) as fun: mem_2 = psutil.Process(self.pid).memory_info() assert len(mem_1) == len(mem_2) @@ -690,7 +679,7 @@ 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"), + side_effect=PermissionError, ) as fun: assert psutil.Process(self.pid).create_time() == ctime assert fun.called @@ -699,7 +688,7 @@ 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"), + side_effect=PermissionError, ) as fun: cpu_times_2 = psutil.Process(self.pid).cpu_times() assert fun.called @@ -710,7 +699,7 @@ 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"), + side_effect=PermissionError, ) as fun: io_counters_2 = psutil.Process(self.pid).io_counters() for i in range(len(io_counters_1)): @@ -721,7 +710,7 @@ 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"), + side_effect=PermissionError, ) as fun: assert psutil.Process(self.pid).num_handles() == num_handles assert fun.called @@ -838,7 +827,7 @@ def test_environ_64(self): @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") class TestServices(PsutilTestCase): def test_win_service_iter(self): - valid_statuses = set([ + valid_statuses = { "running", "paused", "start", @@ -846,9 +835,9 @@ def test_win_service_iter(self): "continue", "stop", "stopped", - ]) - valid_start_types = set(["automatic", "manual", "disabled"]) - valid_statuses = set([ + } + valid_start_types = {"automatic", "manual", "disabled"} + valid_statuses = { "running", "paused", "start_pending", @@ -856,7 +845,7 @@ def test_win_service_iter(self): "continue_pending", "stop_pending", "stopped", - ]) + } for serv in psutil.win_service_iter(): data = serv.as_dict() assert isinstance(data['name'], str) @@ -894,11 +883,8 @@ def test_win_service_get(self): # 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) + exc = OSError(0, "msg", 0) + exc.winerror = ERROR_SERVICE_DOES_NOT_EXIST with mock.patch( "psutil._psplatform.cext.winservice_query_status", side_effect=exc ): @@ -911,11 +897,8 @@ def test_win_service_get(self): service.username() # test AccessDenied - if PY3: - args = (0, "msg", 0, ERROR_ACCESS_DENIED) - else: - args = (ERROR_ACCESS_DENIED, "msg") - exc = WindowsError(*args) + exc = OSError(0, "msg", 0) + exc.winerror = ERROR_ACCESS_DENIED with mock.patch( "psutil._psplatform.cext.winservice_query_status", side_effect=exc ): diff --git a/pyproject.toml b/pyproject.toml index fea968df43..b64adb6ff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,8 @@ ignore = [ "ARG001", # unused-function-argument "ARG002", # unused-method-argument "B007", # Loop control variable `x` not used within loop body - "B904", # Within an `except` clause, raise exceptions with `raise ... from err` (PYTHON2.7 COMPAT) - "B904", # Use `raise from` to specify exception cause (PYTHON2.7 COMPAT) - "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) + "B904", # Use `raise from` to specify exception cause + "C4", # flake8-comprehensions "C90", # mccabe (function `X` is too complex) "COM812", # Trailing comma missing "D", # pydocstyle @@ -44,14 +43,13 @@ ignore = [ "ERA001", # Found commented-out code "FBT", # flake8-boolean-trap (makes zero sense) "FIX", # Line contains TODO / XXX / ..., consider resolving the issue - "FLY", # flynt (PYTHON2.7 COMPAT) + "FLY002", # static-join-to-f-string / Consider {expression} instead of string join "FURB101", # `open` and `read` should be replaced by `Path(src).read_text()` "FURB103", # `open` and `write` should be replaced by `Path(src).write_text()` "FURB113", # Use `ls.extend(...)` instead of repeatedly calling `ls.append()` "FURB116", # [*] Replace `hex` call with `f"{start:x}"` "FURB118", # [*] Use `operator.add` instead of defining a lambda "FURB140", # [*] Use `itertools.starmap` instead of the generator - "FURB145", # [*] Prefer `copy` method over slicing (PYTHON2.7 COMPAT) "FURB192", # [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence "INP", # flake8-no-pep420 "N801", # Class name `async_chat` should use CapWords convention (ASYNCORE COMPAT) @@ -91,14 +89,7 @@ ignore = [ "TD", # all TODOs, XXXs, etc. "TRY300", # Consider moving this statement to an `else` block "TRY301", # Abstract `raise` to an inner function - "UP009", # [*] UTF-8 encoding declaration is unnecessary (PYTHON2.7 COMPAT) - "UP010", # [*] Unnecessary `__future__` import `print_function` for target Python version (PYTHON2.7 COMPAT) - "UP024", # [*] Replace aliased errors with `OSError` (PYTHON2.7 COMPAT) - "UP025", # [*] Remove unicode literals from strings (PYTHON2.7 COMPAT) - "UP028", # [*] Replace `yield` over `for` loop with `yield from` (PYTHON2.7 COMPAT) "UP031", # [*] Use format specifiers instead of percent format - "UP032", # [*] Use f-string instead of `format` call (PYTHON2.7 COMPAT) - "UP036", # Version block is outdated for minimum Python version (PYTHON2.7 COMPAT) ] [tool.ruff.lint.per-file-ignores] @@ -106,7 +97,6 @@ ignore = [ # EM101 == raw-string-in-exception # TRY003 == raise-vanilla-args ".github/workflows/*" = ["T201", "T203"] -"psutil/_compat.py" = ["PLW0127"] # self-assigning-variable "psutil/tests/*" = ["EM101", "TRY003"] "scripts/*" = ["T201", "T203"] "scripts/internal/*" = ["EM101", "T201", "T203", "TRY003"] @@ -119,7 +109,6 @@ lines-after-imports = 2 [tool.coverage.report] exclude_lines = [ - "enum.IntEnum", "except ImportError:", "globals().update", "if BSD", @@ -129,22 +118,16 @@ exclude_lines = [ "if MACOS", "if NETBSD", "if OPENBSD", - "if PY3:", "if SUNOS", "if WINDOWS", "if _WINDOWS:", "if __name__ == .__main__.:", - "if enum is None:", - "if enum is not None:", - "if has_enums:", "if ppid_map is None:", "if sys.platform.startswith", - "import enum", "pragma: no cover", "raise NotImplementedError", ] omit = [ - "psutil/_compat.py", "psutil/tests/*", "setup.py", ] @@ -190,7 +173,6 @@ disable = [ [tool.vulture] exclude = [ "docs/conf.py", - "psutil/_compat.py", "psutil/tests/", "scripts/internal/winmake.py", ] diff --git a/scripts/battery.py b/scripts/battery.py index 0595d1ad10..d9a783daa1 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -13,7 +13,6 @@ plugged in: no """ -from __future__ import print_function import sys diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index bfbb14b6ca..85495c0d3a 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -38,15 +38,14 @@ kwork """ -from __future__ import print_function import collections import os +import shutil import sys import time import psutil -from psutil._compat import get_terminal_size if not hasattr(psutil.Process, "cpu_num"): @@ -97,7 +96,7 @@ def main(): print("%-10s" % pname[:10], end="") print() curr_line += 1 - if curr_line >= get_terminal_size()[1]: + if curr_line >= shutil.get_terminal_size()[1]: break time.sleep(1) diff --git a/scripts/fans.py b/scripts/fans.py index a9a8b8e671..bfab434c49 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -11,7 +11,6 @@ cpu_fan 3200 RPM """ -from __future__ import print_function import sys diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index e23472ba9e..dd7684e873 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -42,7 +42,6 @@ broadcast : ff:ff:ff:ff:ff:ff """ -from __future__ import print_function import socket diff --git a/scripts/internal/appveyor_run_with_compiler.cmd b/scripts/internal/appveyor_run_with_compiler.cmd deleted file mode 100644 index 7965f865fe..0000000000 --- a/scripts/internal/appveyor_run_with_compiler.cmd +++ /dev/null @@ -1,89 +0,0 @@ -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) -:: -:: To build extensions for 64 bit Python 2, we need to configure environment -:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) -:: -:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific -:: environment configurations. -:: -:: Note: this script needs to be run with the /E:ON and /V:ON flags for the -:: cmd interpreter, at least for (SDK v7.0) -:: -:: More details at: -:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: http://stackoverflow.com/a/13751649/163740 -:: -:: Author: Olivier Grisel -:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ -:: -:: Notes about batch files for Python people: -:: -:: Quotes in values are literally part of the values: -:: SET FOO="bar" -:: FOO is now five characters long: " b a r " -:: If you don't want quotes, don't include them on the right-hand side. -:: -:: The CALL lines at the end of this file look redundant, but if you move them -:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y -:: case, I don't know why. - -@ECHO OFF - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf - -:: Extract the major and minor versions, and allow for the minor version to be -:: more than 9. This requires the version number to have two dots in it. -SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1% -IF "%PYTHON_VERSION:~3,1%" == "." ( - SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1% -) ELSE ( - SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2% -) - -:: Based on the Python version, determine what SDK version to use, and whether -:: to set the SDK for 64-bit. -IF %MAJOR_PYTHON_VERSION% == 2 ( - SET WINDOWS_SDK_VERSION="v7.0" - SET SET_SDK_64=Y -) ELSE ( - IF %MAJOR_PYTHON_VERSION% == 3 ( - SET WINDOWS_SDK_VERSION="v7.1" - IF %MINOR_PYTHON_VERSION% LEQ 4 ( - SET SET_SDK_64=Y - ) ELSE ( - SET SET_SDK_64=N - IF EXIST "%WIN_WDK%" ( - :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN "%WIN_WDK%" 0wdf - ) - ) - ) ELSE ( - ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" - EXIT 1 - ) -) - -IF %PYTHON_ARCH% == 64 ( - IF %SET_SDK_64% == Y ( - ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture - SET DISTUTILS_USE_SDK=1 - SET MSSdk=1 - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 - ) ELSE ( - ECHO Using default MSVC build environment for 64 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 - ) -) ELSE ( - ECHO Using default MSVC build environment for 32 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index b58224bb6e..43c279a30d 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -9,8 +9,6 @@ See: https://github.com/giampaolo/psutil/issues/799. """ -from __future__ import division -from __future__ import print_function import sys import textwrap diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 90cb258c33..ed84385ef4 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -38,7 +38,6 @@ Author: Himanshu Shekhar (2017) """ -from __future__ import print_function import argparse import concurrent.futures @@ -93,7 +92,7 @@ def sanitize_url(url): def find_urls(s): matches = REGEX.findall(s) or [] - return list(set([sanitize_url(x) for x in matches])) + return list({sanitize_url(x) for x in matches}) def parse_rst(fname): diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py index 516ca894ef..225140018a 100755 --- a/scripts/internal/clinter.py +++ b/scripts/internal/clinter.py @@ -6,7 +6,6 @@ """A super simple linter to check C syntax.""" -from __future__ import print_function import argparse import sys diff --git a/scripts/internal/download_wheels_github.py b/scripts/internal/download_wheels.py similarity index 78% rename from scripts/internal/download_wheels_github.py rename to scripts/internal/download_wheels.py index c6ecb5dda0..bd9e743908 100755 --- a/scripts/internal/download_wheels_github.py +++ b/scripts/internal/download_wheels.py @@ -22,14 +22,12 @@ import requests -from psutil import __version__ from psutil._common import bytes2human from psutil.tests import safe_rmpath USER = "giampaolo" PROJECT = "psutil" -PROJECT_VERSION = __version__ OUTFILE = "wheels-github.zip" TOKEN = "" TIMEOUT = 30 @@ -60,27 +58,12 @@ def download_zip(url): print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) -def rename_win27_wheels(): - # See: https://github.com/giampaolo/psutil/issues/810 - src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PROJECT_VERSION - dst = 'dist/psutil-%s-cp27-none-win32.whl' % PROJECT_VERSION - if os.path.exists(src): - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PROJECT_VERSION - dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PROJECT_VERSION - if os.path.exists(src): - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - - def run(): data = get_artifacts() download_zip(data['artifacts'][0]['archive_download_url']) os.makedirs('dist', exist_ok=True) with zipfile.ZipFile(OUTFILE, 'r') as zf: zf.extractall('dist') - rename_win27_wheels() def main(): diff --git a/scripts/internal/download_wheels_appveyor.py b/scripts/internal/download_wheels_appveyor.py deleted file mode 100755 index 154faeed75..0000000000 --- a/scripts/internal/download_wheels_appveyor.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/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. - -"""Script which downloads wheel files hosted on AppVeyor: -https://ci.appveyor.com/project/giampaolo/psutil -Re-adapted from the original recipe of Ibarra Corretge' -: -http://code.saghul.net/index.php/2015/09/09/. -""" - -from __future__ import print_function - -import concurrent.futures -import os -import sys - -import requests - -from psutil import __version__ -from psutil._common import bytes2human -from psutil._common import print_color - - -USER = "giampaolo" -PROJECT = "psutil" -PROJECT_VERSION = __version__ -BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7'] -TIMEOUT = 30 - - -def download_file(url): - local_fname = url.split('/')[-1] - local_fname = os.path.join('dist', local_fname) - os.makedirs('dist', exist_ok=True) - r = requests.get(url, stream=True, timeout=TIMEOUT) - with open(local_fname, 'wb') as f: - for chunk in r.iter_content(chunk_size=16384): - if chunk: # filter out keep-alive new chunks - f.write(chunk) - return local_fname - - -def get_file_urls(): - with requests.Session() as session: - data = session.get( - BASE_URL + '/projects/' + USER + '/' + PROJECT, timeout=TIMEOUT - ) - data = data.json() - - urls = [] - for job in (job['jobId'] for job in data['build']['jobs']): - job_url = BASE_URL + '/buildjobs/' + job + '/artifacts' - data = session.get(job_url, timeout=TIMEOUT) - data = data.json() - for item in data: - file_url = job_url + '/' + item['fileName'] - urls.append(file_url) - if not urls: - print_color("no artifacts found", 'red') - sys.exit(1) - else: - for url in sorted(urls, key=os.path.basename): - yield url - - -def rename_win27_wheels(): - # See: https://github.com/giampaolo/psutil/issues/810 - src = 'dist/psutil-%s-cp27-cp27m-win32.whl' % PROJECT_VERSION - dst = 'dist/psutil-%s-cp27-none-win32.whl' % PROJECT_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - src = 'dist/psutil-%s-cp27-cp27m-win_amd64.whl' % PROJECT_VERSION - dst = 'dist/psutil-%s-cp27-none-win_amd64.whl' % PROJECT_VERSION - print("rename: %s\n %s" % (src, dst)) - os.rename(src, dst) - - -def run(): - urls = get_file_urls() - completed = 0 - exc = None - with concurrent.futures.ThreadPoolExecutor() as e: - fut_to_url = {e.submit(download_file, url): url for url in urls} - for fut in concurrent.futures.as_completed(fut_to_url): - url = fut_to_url[fut] - try: - local_fname = fut.result() - except Exception: - print_color("error while downloading %s" % (url), 'red') - raise - else: - completed += 1 - print( - "downloaded %-45s %s" - % (local_fname, bytes2human(os.path.getsize(local_fname))) - ) - # 2 wheels (32 and 64 bit) per supported python version - expected = len(PY_VERSIONS) * 2 - if expected != completed: - return sys.exit("expected %s files, got %s" % (expected, completed)) - if exc: - return sys.exit() - rename_win27_wheels() - - -def main(): - run() - - -if __name__ == '__main__': - main() diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 9eeeb1a24b..1e261e5a42 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -12,7 +12,7 @@ SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') -SKIP_FILES = 'appveyor.yml' +SKIP_FILES = () SKIP_PREFIXES = ('.ci/', '.github/') diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 1441a1454f..de4b461e63 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -10,7 +10,6 @@ "make install-git-hooks". """ -from __future__ import print_function import os import shlex @@ -19,7 +18,6 @@ PYTHON = sys.executable -PY3 = sys.version_info[0] >= 3 def term_supports_colors(): @@ -74,11 +72,6 @@ def sh(cmd): return stdout -def open_text(path): - kw = {'encoding': 'utf8'} if PY3 else {} - return open(path, **kw) - - def git_commit_files(): out = sh(["git", "diff", "--cached", "--name-only"]) py_files = [ @@ -158,7 +151,7 @@ def main(): toml_sort(toml_files) if new_rm_mv: out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) - with open_text('MANIFEST.in') as f: + with open("MANIFEST.in", encoding="utf8") as f: if out.strip() != f.read().strip(): sys.exit( "some files were added, deleted or renamed; " diff --git a/scripts/internal/install_pip.py b/scripts/internal/install_pip.py index 44557529ab..9b1ee7a215 100755 --- a/scripts/internal/install_pip.py +++ b/scripts/internal/install_pip.py @@ -18,18 +18,10 @@ import os import ssl import tempfile +from urllib.request import urlopen -PY3 = sys.version_info[0] >= 3 -if PY3: - from urllib.request import urlopen - - URL = "https://bootstrap.pypa.io/get-pip.py" - -else: - from urllib2 import urlopen - - URL = "https://bootstrap.pypa.io/pip/2.7/get-pip.py" +URL = "https://bootstrap.pypa.io/get-pip.py" def main(): diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index f7017b04db..6bb0cdd081 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -44,8 +44,6 @@ Totals: access-denied=1744, calls=10020, processes=334 """ -from __future__ import division -from __future__ import print_function import time from collections import defaultdict diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 65e28e3511..8b62f2b713 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -49,8 +49,7 @@ line tools such as: ps, top, lsof, netstat, ifconfig, who, df, kill, free, \ nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap. It \ currently supports Linux, Windows, macOS, Sun Solaris, FreeBSD, OpenBSD, \ -NetBSD and AIX. Supported Python versions are 2.7 and 3.6+. PyPy is also \ -known to work. +NetBSD and AIX. Supported Python versions are cPython 3.6+ and PyPy. What's new ========== diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 786644b5a1..3fecbfbcd0 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -68,8 +68,6 @@ memory_maps 300 0.74281 """ -from __future__ import division -from __future__ import print_function import argparse import inspect diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 8afb7c1fde..ae62169073 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -11,7 +11,6 @@ * https://hugovk.github.io/top-pypi-packages/. """ -from __future__ import print_function import json import os @@ -117,7 +116,7 @@ def downloads_by_distro(): def print_row(left, right): if isinstance(right, int): - right = '{:,}'.format(right) + right = f'{right:,}' print(templ % (left, right)) diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index 706601943b..6dec932b19 100755 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -24,7 +24,7 @@ def sh(cmd): def get_tag_date(tag): - out = sh(r"git log -1 --format=%ai {}".format(tag)) + out = sh(f"git log -1 --format=%ai {tag}") return out.split(' ')[0] diff --git a/scripts/internal/test_python2_setup_py.py b/scripts/internal/test_python2_setup_py.py new file mode 100755 index 0000000000..6121844598 --- /dev/null +++ b/scripts/internal/test_python2_setup_py.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python2 + +# 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. + + +"""Invoke setup.py with Python 2.7, make sure it fails but not due to +SyntaxError, and that it prints a meaningful error message. +""" + +import os +import subprocess +import sys + + +ROOT_DIR = os.path.realpath( + os.path.join(os.path.dirname(__file__), "..", "..") +) + + +def main(): + if sys.version_info[:2] != (2, 7): + raise RuntimeError("this script is supposed to be run with python 2.7") + setup_py = os.path.join(ROOT_DIR, "setup.py") + p = subprocess.Popen( + [sys.executable, setup_py], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate() + assert p.wait() == 1 + assert not stdout, stdout + assert "psutil no longer supports Python 2.7" in stderr, stderr + assert "Latest version supporting Python 2.7 is" in stderr, stderr + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index a0b085280e..2e076f0df5 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -11,7 +11,6 @@ that they should be deemed illegal! """ -from __future__ import print_function import argparse import atexit @@ -25,9 +24,7 @@ import sys -APPVEYOR = bool(os.environ.get('APPVEYOR')) -PYTHON = sys.executable if APPVEYOR else os.getenv('PYTHON', sys.executable) -PY3 = sys.version_info[0] >= 3 +PYTHON = os.getenv('PYTHON', sys.executable) PYTEST_ARGS = ["-v", "-s", "--tb=short"] HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) @@ -42,8 +39,6 @@ DEV_DEPS = setup.DEV_DEPS _cmds = {} -if PY3: - basestring = str GREEN = 2 LIGHTBLUE = 3 @@ -61,9 +56,8 @@ def safe_print(text, file=sys.stdout): """Prints a (unicode) string to the console, encoded depending on the stdout/file encoding (eg. cp437 on Windows). This is to avoid encoding errors in case of funky path names. - Works with Python 2 and 3. """ - if not isinstance(text, basestring): + if not isinstance(text, str): return print(text, file=file) try: file.write(text) @@ -181,15 +175,13 @@ def build(): # order to allow "import psutil" when using the interactive interpreter # from within psutil root directory. cmd = [PYTHON, "setup.py", "build_ext", "-i"] - if sys.version_info[:2] >= (3, 6) and (os.cpu_count() or 1) > 1: + if os.cpu_count() or 1 > 1: # noqa: PLR0133 cmd += ['--parallel', str(os.cpu_count())] # Print coloured warnings in real time. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) try: for line in iter(p.stdout.readline, b''): - if PY3: - line = line.decode() - line = line.strip() + line = line.decode().strip() if 'warning' in line: win_colorprint(line, YELLOW) elif 'error' in line: @@ -454,18 +446,6 @@ def print_sysinfo(): print_sysinfo() -def download_appveyor_wheels(): - """Download appveyor wheels.""" - sh([ - PYTHON, - "scripts\\internal\\download_wheels_appveyor.py", - "--user", - "giampaolo", - "--project", - "psutil", - ]) - - def generate_manifest(): """Generate MANIFEST.in file.""" script = "scripts\\internal\\generate_manifest.py" @@ -482,8 +462,6 @@ def get_python(path): # try to look for a python installation given a shortcut name path = path.replace('.', '') vers = ( - '27', - '27-64', '310-64', '311-64', '312-64', @@ -504,7 +482,6 @@ def parse_args(): sp.add_parser('build', help="build") sp.add_parser('clean', help="deletes dev files") sp.add_parser('coverage', help="run coverage tests.") - sp.add_parser('download-appveyor-wheels', help="download wheels.") sp.add_parser('generate-manifest', help="generate MANIFEST.in file") sp.add_parser('help', help="print this help") sp.add_parser('install', help="build + install in develop/edit mode") diff --git a/scripts/iotop.py b/scripts/iotop.py index 23c3a376ee..2d92f4e2d6 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -71,7 +71,7 @@ def poll(interval): """ # first get a list of all processes and disk io counters procs = list(psutil.process_iter()) - for p in procs[:]: + for p in procs.copy(): try: p._before = p.io_counters() except psutil.Error: @@ -83,7 +83,7 @@ def poll(interval): time.sleep(interval) # then retrieve the same info again - for p in procs[:]: + for p in procs.copy(): with p.oneshot(): try: p._after = p.io_counters() diff --git a/scripts/pidof.py b/scripts/pidof.py index 662d5d6573..7ac8e0323f 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -11,7 +11,6 @@ 1140 1138 1136 1134 1133 1129 1127 1125 1121 1120 1119 """ -from __future__ import print_function import sys diff --git a/scripts/pmap.py b/scripts/pmap.py index 577e4b2947..719ce01341 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -9,9 +9,9 @@ $ python3 scripts/pmap.py 32402 Address RSS Mode Mapping -0000000000400000 1200K r-xp /usr/bin/python2.7 -0000000000838000 4K r--p /usr/bin/python2.7 -0000000000839000 304K rw-p /usr/bin/python2.7 +0000000000400000 1200K r-xp /usr/bin/python3.7 +0000000000838000 4K r--p /usr/bin/python3.7 +0000000000839000 304K rw-p /usr/bin/python3.7 00000000008ae000 68K rw-p [anon] 000000000275e000 5396K rw-p [heap] 00002b29bb1e0000 124K r-xp /lib/x86_64-linux-gnu/ld-2.17.so @@ -28,15 +28,15 @@ ... """ +import shutil import sys import psutil from psutil._common import bytes2human -from psutil._compat import get_terminal_size def safe_print(s): - s = s[: get_terminal_size()[0]] + s = s[: shutil.get_terminal_size()[0]] try: print(s) except UnicodeEncodeError: diff --git a/scripts/procsmem.py b/scripts/procsmem.py index eec5cd51a0..c8eaf34078 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -35,7 +35,6 @@ """ -from __future__ import print_function import sys diff --git a/scripts/ps.py b/scripts/ps.py index f07b568652..8478bbd1a8 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -32,11 +32,11 @@ """ import datetime +import shutil import time import psutil from psutil._common import bytes2human -from psutil._compat import get_terminal_size def main(): @@ -109,7 +109,7 @@ def main(): cputime, cmdline, ) - print(line[: get_terminal_size()[0]]) + print(line[: shutil.get_terminal_size()[0]]) if __name__ == '__main__': diff --git a/scripts/pstree.py b/scripts/pstree.py index e873e467d7..51e3ee6cbf 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -27,7 +27,6 @@ ... """ -from __future__ import print_function import collections import sys diff --git a/scripts/sensors.py b/scripts/sensors.py index 668cca0a22..861a47d799 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -1,5 +1,4 @@ #!/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 @@ -28,7 +27,6 @@ plugged in: yes """ -from __future__ import print_function import psutil diff --git a/scripts/temperatures.py b/scripts/temperatures.py index 118ebc2652..2253fef7f3 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -1,5 +1,4 @@ #!/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 @@ -22,7 +21,6 @@ Core 3 54.0 °C (high = 100.0 °C, critical = 100.0 °C) """ -from __future__ import print_function import sys diff --git a/setup.py b/setup.py index 727677c494..148bfcfdfb 100755 --- a/setup.py +++ b/setup.py @@ -4,9 +4,12 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Cross-platform lib for process and system monitoring in Python.""" +"""Cross-platform lib for process and system monitoring in Python. -from __future__ import print_function +NOTE: the syntax of this script MUST be kept compatible with Python 2.7. +""" + +from __future__ import print_function # noqa: UP010 import ast import contextlib @@ -21,9 +24,21 @@ import sys import sysconfig import tempfile +import textwrap import warnings +if sys.version_info[0] == 2: # noqa: UP036 + sys.exit(textwrap.dedent("""\ + As of version 7.0.0 psutil no longer supports Python 2.7, see: + https://github.com/giampaolo/psutil/issues/2480 + Latest version supporting Python 2.7 is psutil 6.1.X. + Install it with: + + python2 -m pip install psutil==6.1.*\ + """)) + + with warnings.catch_warnings(): warnings.simplefilter("ignore") try: @@ -37,9 +52,10 @@ from distutils.core import Extension from distutils.core import setup + HERE = os.path.abspath(os.path.dirname(__file__)) -# ...so we can import _common.py and _compat.py +# ...so we can import _common.py sys.path.insert(0, os.path.join(HERE, "psutil")) from _common import AIX # NOQA @@ -53,8 +69,6 @@ from _common import SUNOS # NOQA from _common import WINDOWS # NOQA from _common import hilite # NOQA -from _compat import PY3 # NOQA -from _compat import which # NOQA PYPY = '__pypy__' in sys.builtin_module_names @@ -66,23 +80,12 @@ # Test deps, installable via `pip install .[test]` or # `make install-pydeps-test`. -if PY3: - TEST_DEPS = [ - "pytest", - "pytest-xdist", - "setuptools", - ] -else: - TEST_DEPS = [ - "futures", - "ipaddress", - "enum34", - "mock==1.0.1", - "pytest-xdist", - "pytest==4.6.11", - "setuptools", - "unittest2", - ] +TEST_DEPS = [ + "pytest", + "pytest-xdist", + "setuptools", +] + if WINDOWS and not PYPY: TEST_DEPS.append("pywin32") TEST_DEPS.append("wheel") @@ -90,7 +93,7 @@ # Development deps, installable via `pip install .[dev]` or # `make install-pydeps-dev`. -DEV_DEPS = [ +DEV_DEPS = TEST_DEPS + [ "abi3audit", "black", "check-manifest", @@ -111,6 +114,7 @@ "vulture", "wheel", ] + if WINDOWS: DEV_DEPS.append("pyreadline") DEV_DEPS.append("pdbpp") @@ -121,7 +125,7 @@ if BSD: macros.append(("PSUTIL_BSD", 1)) -# Needed to determine _Py_PARSE_PID in case it's missing (Python 2, PyPy). +# Needed to determine _Py_PARSE_PID in case it's missing (PyPy). # Taken from Lib/test/test_fcntl.py. # XXX: not bullet proof as the (long long) case is missing. if struct.calcsize('l') <= 8: @@ -203,7 +207,7 @@ def write(self, s): def missdeps(cmdline): s = "psutil could not be installed from sources" - if not SUNOS and not which("gcc"): + if not SUNOS and not shutil.which("gcc"): s += " because gcc is not installed. " else: s += ". Perhaps Python header files are not installed. " @@ -518,8 +522,6 @@ def main(): 'Operating System :: POSIX :: SunOS/Solaris', 'Operating System :: POSIX', 'Programming Language :: C', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', @@ -545,7 +547,7 @@ def main(): } kwargs.update( python_requires=( - ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" ), extras_require=extras_require, zip_safe=False, @@ -563,19 +565,16 @@ def main(): ("build", "install", "sdist", "bdist", "develop") ) ): - py3 = "3" if PY3 else "" if LINUX: pyimpl = "pypy" if PYPY else "python" - if which('dpkg'): - missdeps( - "sudo apt-get install gcc %s%s-dev" % (pyimpl, py3) - ) - elif which('rpm'): - missdeps("sudo yum install gcc %s%s-devel" % (pyimpl, py3)) - elif which('apk'): + if shutil.which("dpkg"): + missdeps("sudo apt-get install gcc %s3-dev" % (pyimpl)) + elif shutil.which("rpm"): + missdeps("sudo yum install gcc %s-devel" % (pyimpl)) + elif shutil.which("apk"): missdeps( "sudo apk add gcc %s%s-dev musl-dev linux-headers" - % (pyimpl, py3) + % (pyimpl) ) elif MACOS: msg = ( @@ -584,14 +583,14 @@ def main(): ) print(hilite(msg, color="red"), file=sys.stderr) elif FREEBSD: - if which('pkg'): - missdeps("pkg install gcc python%s" % py3) - elif which('mport'): # MidnightBSD - missdeps("mport install gcc python%s" % py3) + if shutil.which("pkg"): + missdeps("pkg install gcc python3") + elif shutil.which("mport"): # MidnightBSD + missdeps("mport install gcc python3") elif OPENBSD: - missdeps("pkg_add -v gcc python%s" % py3) + missdeps("pkg_add -v gcc python3") elif NETBSD: - missdeps("pkgin install gcc python%s" % py3) + missdeps("pkgin install gcc python3") elif SUNOS: missdeps( "sudo ln -s /usr/bin/gcc /usr/local/bin/cc && " From 5d4be42bd309dc3c7e93d5d319577ec4b8027445 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 15:20:48 +0100 Subject: [PATCH 1166/1714] refact: check valid net_connections() kinds from a single point --- psutil/__init__.py | 11 ++++++++++- psutil/_common.py | 2 +- psutil/_psaix.py | 6 ------ psutil/_psbsd.py | 11 ----------- psutil/_pslinux.py | 5 ----- psutil/_psosx.py | 5 ----- psutil/_pssunos.py | 8 -------- psutil/_pswindows.py | 5 ----- 8 files changed, 11 insertions(+), 42 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 490e7b394b..c7d377f602 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -18,7 +18,6 @@ Supported Python versions are cPython 3.6+ and PyPy. """ - import collections import contextlib import datetime @@ -275,6 +274,14 @@ def _pprint_secs(secs): return datetime.datetime.fromtimestamp(secs).strftime(fmt) +def _check_conn_kind(kind): + """Check net_connections()'s `kind` parameter.""" + kinds = tuple(_common.conn_tmap) + if kind not in kinds: + msg = f"invalid kind argument {kind!r}; valid ones are: {kinds}" + raise ValueError(msg) + + # ===================================================================== # --- Process class # ===================================================================== @@ -1231,6 +1238,7 @@ def net_connections(self, kind='inet'): | all | the sum of all the possible families and protocols | +------------+----------------------------------------------------+ """ + _check_conn_kind(kind) return self._proc.net_connections(kind) @_common.deprecated_method(replacement="net_connections") @@ -2191,6 +2199,7 @@ def net_connections(kind='inet'): On macOS this function requires root privileges. """ + _check_conn_kind(kind) return _psplatform.net_connections(kind) diff --git a/psutil/_common.py b/psutil/_common.py index ffa6dcef34..a0986ca7b4 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -249,7 +249,7 @@ class BatteryTime(enum.IntEnum): "udp6": ([AF_INET6], [SOCK_DGRAM]), }) -if AF_UNIX is not None: +if AF_UNIX is not None and not SUNOS: conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])}) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 94a07a3a59..b774d8d427 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -204,12 +204,6 @@ def net_connections(kind, _pid=-1): """Return socket connections. If pid == -1 return system-wide connections (as opposed to connections opened by one process only). """ - 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])) - ) families, types = _common.conn_tmap[kind] rawlist = cext.net_connections(_pid) ret = [] diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 209e5476cd..0628facbe3 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -435,14 +435,8 @@ def net_if_stats(): def net_connections(kind): """System-wide network connections.""" - 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])) - ) families, types = conn_tmap[kind] ret = set() - if OPENBSD: rawlist = cext.net_connections(-1, families, types) elif NETBSD: @@ -820,11 +814,6 @@ def threads(self): @wrap_exceptions def net_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])) - ) families, types = conn_tmap[kind] ret = [] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 5d5595feb5..b67964cf6d 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -955,11 +955,6 @@ def process_unix(file, family, inodes, filter_pid=None): yield (fd, family, type_, path, raddr, status, pid) 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])) - ) self._procfs_path = get_procfs_path() if pid is not None: inodes = self.get_proc_inodes(pid) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index c6078ea3d4..3fdbcd6d74 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -500,11 +500,6 @@ def open_files(self): @wrap_exceptions def net_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])) - ) families, types = conn_tmap[kind] rawlist = cext.proc_net_connections(self.pid, families, types) ret = [] diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 09a79fef42..b741792f31 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -260,14 +260,6 @@ def net_connections(kind, _pid=-1): connections (as opposed to connections opened by one process only). Only INET sockets are returned (UNIX are not). """ - cmap = _common.conn_tmap.copy() - 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])) - ) families, types = _common.conn_tmap[kind] rawlist = cext.net_connections(_pid) ret = set() diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index be100493e1..e3c7db3ca6 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -349,11 +349,6 @@ def net_connections(kind, _pid=-1): """Return socket connections. If pid == -1 return system-wide 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])) - ) families, types = conn_tmap[kind] rawlist = cext.net_connections(_pid, families, types) ret = set() From fffaba38cb4457285d8c3fc49e5762242bb771fe Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 16:11:21 +0100 Subject: [PATCH 1167/1714] Use f-strings (#2483) --- .github/workflows/issues.py | 18 +-- docs/conf.py | 4 +- psutil/__init__.py | 43 ++--- psutil/_common.py | 26 +-- psutil/_psaix.py | 13 +- psutil/_psbsd.py | 27 ++-- psutil/_pslinux.py | 201 ++++++++++++------------ psutil/_psposix.py | 3 +- psutil/_pssunos.py | 47 +++--- psutil/_pswindows.py | 31 ++-- psutil/tests/__init__.py | 77 ++++----- psutil/tests/test_aix.py | 4 +- psutil/tests/test_bsd.py | 34 ++-- psutil/tests/test_connections.py | 6 +- psutil/tests/test_linux.py | 54 +++---- psutil/tests/test_misc.py | 20 +-- psutil/tests/test_osx.py | 6 +- psutil/tests/test_posix.py | 10 +- psutil/tests/test_process.py | 26 ++- psutil/tests/test_process_all.py | 4 +- psutil/tests/test_sunos.py | 2 +- psutil/tests/test_system.py | 5 +- psutil/tests/test_windows.py | 25 ++- pyproject.toml | 9 +- scripts/battery.py | 12 +- scripts/ifconfig.py | 10 +- scripts/internal/bench_oneshot.py | 13 +- scripts/internal/bench_oneshot_2.py | 4 +- scripts/internal/check_broken_links.py | 8 +- scripts/internal/clinter.py | 4 +- scripts/internal/download_wheels.py | 8 +- scripts/internal/git_pre_commit.py | 23 ++- scripts/internal/install_pip.py | 4 +- scripts/internal/print_access_denied.py | 2 +- scripts/internal/print_api_speed.py | 2 +- scripts/internal/print_dist.py | 8 +- scripts/internal/print_downloads.py | 24 +-- scripts/internal/print_hashes.py | 7 +- scripts/internal/winmake.py | 16 +- scripts/killall.py | 4 +- scripts/pidof.py | 2 +- scripts/pmap.py | 2 +- scripts/procinfo.py | 16 +- scripts/procsmem.py | 6 +- scripts/sensors.py | 8 +- scripts/top.py | 4 +- scripts/winservices.py | 4 +- 47 files changed, 438 insertions(+), 448 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 5efe6fc324..7fb369eb90 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -194,29 +194,29 @@ def get_issue(): def log(msg): if '\n' in msg or "\r\n" in msg: - print(">>>\n%s\n<<<" % msg, flush=True) + print(f">>>\n{msg}\n<<<", flush=True) else: - print(">>> %s <<<" % msg, flush=True) + print(f">>> {msg} <<<", flush=True) def add_label(issue, label): def should_add(issue, label): if has_label(issue, label): - log("already has label %r" % (label)) + log(f"already has label {label!r}") return False for left, right in ILLOGICAL_PAIRS: if label == left and has_label(issue, right): - log("already has label" % (label)) + log(f"already has label f{label}") return False return not has_label(issue, label) if not should_add(issue, label): - log("should not add label %r" % label) + log(f"should not add label {label!r}") return - log("add label %r" % label) + log(f"add label {label!r}") issue.add_to_labels(label) @@ -329,16 +329,16 @@ def on_new_pr(issue): def main(): issue = get_issue() stype = "PR" if is_pr(issue) else "issue" - log("running issue bot for %s %r" % (stype, issue)) + log(f"running issue bot for {stype} {issue!r}") if is_event_new_issue(): - log("created new issue %s" % issue) + log(f"created new issue {issue}") add_labels_from_text(issue, issue.title) if issue.body: add_labels_from_new_body(issue, issue.body) on_new_issue(issue) elif is_event_new_pr(): - log("created new PR %s" % issue) + log(f"created new PR {issue}") add_labels_from_text(issue, issue.title) if issue.body: add_labels_from_new_body(issue, issue.body) diff --git a/docs/conf.py b/docs/conf.py index 213aadfe07..604eeccb46 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -84,7 +84,7 @@ def get_version(): # General information about the project. project = PROJECT_NAME -copyright = '2009-%s, %s' % (THIS_YEAR, AUTHOR) +copyright = f"2009-{THIS_YEAR}, {AUTHOR}" author = AUTHOR # The version info for the project you're documenting, acts as replacement for @@ -267,7 +267,7 @@ def get_version(): # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = '%s-doc' % PROJECT_NAME +htmlhelp_basename = f"{PROJECT_NAME}-doc" # -- Options for LaTeX output --------------------------------------------- diff --git a/psutil/__init__.py b/psutil/__init__.py index c7d377f602..160c813d82 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -135,7 +135,8 @@ PROCFS_PATH = "/proc" else: # pragma: no cover - raise NotImplementedError('platform %s is not supported' % sys.platform) + msg = f"platform {sys.platform} is not supported" + raise NotImplementedError(msg) # fmt: off @@ -223,7 +224,7 @@ if int(__version__.replace('.', '')) != getattr( _psplatform.cext, 'version', None ): - msg = "version conflict: %r C extension " % _psplatform.cext.__file__ + msg = f"version conflict: {_psplatform.cext.__file__!r} C extension " msg += "module was built for another version of psutil" if hasattr(_psplatform.cext, 'version'): msg += " (%s instead of %s)" % ( @@ -231,7 +232,7 @@ __version__, ) else: - msg += " (different than %s)" % __version__ + msg += f" (different than {__version__})" msg += "; you may try to 'pip uninstall psutil', manually remove %s" % ( getattr( _psplatform.cext, @@ -323,12 +324,12 @@ def _init(self, pid, _ignore_nsp=False): pid = os.getpid() else: if pid < 0: - msg = "pid must be a positive integer (got %s)" % pid + msg = f"pid must be a positive integer (got {pid})" raise ValueError(msg) try: _psplatform.cext.check_pid_range(pid) except OverflowError: - msg = "process PID out of range (got %s)" % pid + msg = f"process PID out of range (got {pid})" raise NoSuchProcess(pid, msg=msg) self._pid = pid @@ -419,7 +420,7 @@ def __str__(self): return "%s.%s(%s)" % ( self.__class__.__module__, self.__class__.__name__, - ", ".join(["%s=%r" % (k, v) for k, v in info.items()]), + ", ".join([f"{k}={v!r}" for k, v in info.items()]), ) __repr__ = __str__ @@ -553,7 +554,7 @@ 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)): - msg = "invalid attrs type %s" % type(attrs) + msg = f"invalid attrs type {type(attrs)}" raise TypeError(msg) attrs = set(attrs) invalid_names = attrs - valid_names @@ -1046,7 +1047,7 @@ def cpu_percent(self, interval=None): """ blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - msg = "interval is not positive (got %r)" % interval + msg = f"interval is not positive (got {interval!r})" raise ValueError(msg) num_cpus = cpu_count() or 1 @@ -1156,9 +1157,9 @@ def memory_percent(self, memtype="rss"): """ valid_types = list(_psplatform.pfullmem._fields) if memtype not in valid_types: - msg = "invalid memtype %r; valid types are %r" % ( - memtype, - tuple(valid_types), + msg = ( + f"invalid memtype {memtype!r}; valid types are" + f" {tuple(valid_types)!r}" ) raise ValueError(msg) fun = ( @@ -1175,7 +1176,7 @@ def memory_percent(self, memtype="rss"): # we should never get here msg = ( "can't calculate process memory percent because total physical" - " system memory is not positive (%r)" % (total_phymem) + f" system memory is not positive ({total_phymem!r})" ) raise ValueError(msg) return (value / float(total_phymem)) * 100 @@ -1438,9 +1439,9 @@ def __getattribute__(self, name): try: return object.__getattribute__(self.__subproc, name) except AttributeError: - msg = "%s instance has no attribute '%s'" % ( - self.__class__.__name__, - name, + msg = ( + f"{self.__class__.__name__} instance has no attribute" + f" '{name}'" ) raise AttributeError(msg) @@ -1524,7 +1525,7 @@ def remove(pid): remove(pid) while _pids_reused: pid = _pids_reused.pop() - debug("refreshing Process instance for reused PID %s" % pid) + debug(f"refreshing Process instance for reused PID {pid}") remove(pid) try: ls = sorted(list(pmap.items()) + list(dict.fromkeys(new_pids).items())) @@ -1596,12 +1597,12 @@ def check_gone(proc, timeout): callback(proc) if timeout is not None and not timeout >= 0: - msg = "timeout must be a positive integer, got %s" % timeout + msg = f"timeout must be a positive integer, got {timeout}" raise ValueError(msg) gone = set() alive = set(procs) if callback is not None and not callable(callback): - msg = "callback %r is not a callable" % callback + msg = f"callback {callback!r} is not a callable" raise TypeError(msg) if timeout is not None: deadline = _timer() + timeout @@ -1801,7 +1802,7 @@ def cpu_percent(interval=None, percpu=False): tid = threading.current_thread().ident blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - msg = "interval is not positive (got %r)" % interval + msg = f"interval is not positive (got {interval})" raise ValueError(msg) def calculate(t1, t2): @@ -1861,7 +1862,7 @@ def cpu_times_percent(interval=None, percpu=False): tid = threading.current_thread().ident blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - msg = "interval is not positive (got %r)" % interval + msg = f"interval is not positive (got {interval!r})" raise ValueError(msg) def calculate(t1, t2): @@ -2243,7 +2244,7 @@ def net_if_addrs(): # https://github.com/giampaolo/psutil/issues/786 separator = ":" if POSIX else "-" while addr.count(separator) < 5: - addr += "%s00" % separator + addr += f"{separator}00" ret[name].append(_common.snicaddr(fam, addr, mask, broadcast, ptp)) return dict(ret) diff --git a/psutil/_common.py b/psutil/_common.py index a0986ca7b4..1e8bb7959f 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -289,8 +289,8 @@ def __str__(self): 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) + details = ", ".join([f"{k}={v!r}" for k, v in info.items()]) + return f"psutil.{self.__class__.__name__}({details})" class NoSuchProcess(Error): @@ -356,7 +356,7 @@ def __init__(self, seconds, pid=None, name=None): self.seconds = seconds self.pid = pid self.name = name - self.msg = "timeout after %s seconds" % seconds + self.msg = f"timeout after {seconds} seconds" def __reduce__(self): return (self.__class__, (self.seconds, self.pid, self.name)) @@ -863,13 +863,12 @@ def hilite(s, color=None, bold=False): # pragma: no cover try: color = colors[color] except KeyError: - raise ValueError( - "invalid color %r; choose between %s" % (list(colors.keys())) - ) + msg = f"invalid color {color!r}; choose amongst {list(colors.keys())}" + raise ValueError(msg) attr.append(color) if bold: attr.append('1') - return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) + return f"\x1b[{';'.join(attr)}m{s}\x1b[0m" def print_color( @@ -894,10 +893,11 @@ def print_color( try: color = colors[color] except KeyError: - raise ValueError( - "invalid color %r; choose between %r" - % (color, list(colors.keys())) + msg = ( + f"invalid color {color!r}; choose between" + f" {list(colors.keys())!r}" ) + raise ValueError(msg) if bold and color <= 7: color += 8 @@ -922,9 +922,9 @@ def debug(msg): if isinstance(msg, Exception): if isinstance(msg, OSError): # ...because str(exc) may contain info about the file name - msg = "ignoring %s" % msg + msg = f"ignoring {msg}" else: - msg = "ignoring %r" % msg + msg = f"ignoring {msg!r}" print( # noqa - "psutil-debug [%s:%s]> %s" % (fname, lineno, msg), file=sys.stderr + f"psutil-debug [{fname}:{lineno}]> {msg}", file=sys.stderr ) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index b774d8d427..2547e194a4 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -146,7 +146,8 @@ def cpu_count_cores(): stdout, stderr = p.communicate() 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)) + msg = f"{cmd!r} command error\n{stderr}" + raise RuntimeError(msg) processors = stdout.strip().splitlines() return len(processors) or None @@ -430,7 +431,7 @@ def threads(self): # is no longer there. if not retlist: # will raise NSP if process is gone - os.stat('%s/%s' % (self._procfs_path, self.pid)) + os.stat(f"{self._procfs_path}/{self.pid}") return retlist @wrap_exceptions @@ -443,7 +444,7 @@ def net_connections(self, kind='inet'): # is no longer there. if not ret: # will raise NSP if process is gone - os.stat('%s/%s' % (self._procfs_path, self.pid)) + os.stat(f"{self._procfs_path}/{self.pid}") return ret @wrap_exceptions @@ -489,10 +490,10 @@ def terminal(self): def cwd(self): procfs_path = self._procfs_path try: - result = os.readlink("%s/%s/cwd" % (procfs_path, self.pid)) + result = os.readlink(f"{procfs_path}/{self.pid}/cwd") return result.rstrip('/') except FileNotFoundError: - os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD + os.stat(f"{procfs_path}/{self.pid}") # raise NSP or AD return "" @wrap_exceptions @@ -539,7 +540,7 @@ def open_files(self): def num_fds(self): if self.pid == 0: # no /proc/0/fd return 0 - return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid))) + return len(os.listdir(f"{self._procfs_path}/{self.pid}/fd")) @wrap_exceptions def num_ctx_switches(self): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 0628facbe3..6f6eeb35f7 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -485,7 +485,7 @@ def sensors_temperatures(): current, high = cext.sensors_cpu_temperature(cpu) if high <= 0: high = None - name = "Core %s" % cpu + name = f"Core {cpu}" ret["coretemp"].append( _common.shwtemp(name, current, high, high) ) @@ -673,7 +673,7 @@ def exe(self): # /proc/0 dir exists but /proc/0/exe doesn't return "" with wrap_exceptions_procfs(self): - return os.readlink("/proc/%s/exe" % self.pid) + return os.readlink(f"/proc/{self.pid}/exe") else: # OpenBSD: exe cannot be determined; references: # https://chromium.googlesource.com/chromium/src/base/+/ @@ -708,7 +708,7 @@ def cmdline(self): 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) + debug(f"ignoring {err!r} and returning an empty list") return [] else: raise @@ -932,12 +932,11 @@ def cpu_affinity_set(self, cpus): # Pre-emptively check if CPUs are valid because the C # function has a weird behavior in case of invalid CPUs, # see: https://github.com/giampaolo/psutil/issues/586 - allcpus = tuple(range(len(per_cpu_times()))) + allcpus = set(range(len(per_cpu_times()))) for cpu in cpus: if cpu not in allcpus: - raise ValueError( - "invalid CPU #%i (choose between %s)" % (cpu, allcpus) - ) + msg = f"invalid CPU {cpu!r} (choose between {allcpus})" + raise ValueError(msg) try: cext.proc_cpu_affinity_set(self.pid, cpus) except OSError as err: @@ -948,10 +947,11 @@ def cpu_affinity_set(self, cpus): if err.errno in {errno.EINVAL, errno.EDEADLK}: for cpu in cpus: if cpu not in allcpus: - raise ValueError( - "invalid CPU #%i (choose between %s)" - % (cpu, allcpus) + msg = ( + f"invalid CPU {cpu!r} (choose between" + f" {allcpus})" ) + raise ValueError(msg) raise @wrap_exceptions @@ -964,9 +964,10 @@ def rlimit(self, resource, limits=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) + msg = ( + "second argument must be a (soft, hard) tuple, got" + f" {limits!r}" ) + raise ValueError(msg) soft, hard = limits return cext.proc_setrlimit(self.pid, resource, soft, hard) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b67964cf6d..531503d55e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -73,8 +73,8 @@ POWER_SUPPLY_PATH = "/sys/class/power_supply" -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_SMAPS = os.path.exists(f"/proc/{os.getpid()}/smaps") +HAS_PROC_SMAPS_ROLLUP = os.path.exists(f"/proc/{os.getpid()}/smaps_rollup") HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get") HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get") @@ -242,9 +242,9 @@ def is_storage_device(name): name = name.replace('/', '!') including_virtual = True if including_virtual: - path = "/sys/block/%s" % name + path = f"/sys/block/{name}" else: - path = "/sys/block/%s/device" % name + path = f"/sys/block/{name}/device" return os.access(path, os.F_OK) @@ -257,7 +257,7 @@ def set_scputimes_ntuple(procfs_path): Used by cpu_times() function. """ global scputimes - with open_binary('%s/stat' % procfs_path) as f: + with open_binary(f"{procfs_path}/stat") as f: values = f.readline().split()[1:] fields = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq'] vlen = len(values) @@ -277,7 +277,7 @@ def set_scputimes_ntuple(procfs_path): set_scputimes_ntuple("/proc") except Exception as err: # noqa: BLE001 # Don't want to crash at import time. - debug("ignoring exception on import: %r" % err) + debug(f"ignoring exception on import: {err!r}") scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) @@ -324,13 +324,12 @@ def calculate_avail_vmem(mems): slab_reclaimable = mems[b'SReclaimable:'] except KeyError as err: debug( - "%s is missing from /proc/meminfo; using an approximation for " - "calculating available memory" - % err.args[0] + f"{err.args[0]} is missing from /proc/meminfo; using an" + " approximation for calculating available memory" ) return fallback try: - f = open_binary('%s/zoneinfo' % get_procfs_path()) + f = open_binary(f"{get_procfs_path()}/zoneinfo") except OSError: return fallback # kernel 2.6.13 @@ -360,7 +359,7 @@ def virtual_memory(): """ missing_fields = [] mems = {} - with open_binary('%s/meminfo' % get_procfs_path()) as f: + with open_binary(f"{get_procfs_path()}/meminfo") as f: for line in f: fields = line.split() mems[fields[0]] = int(fields[1]) * 1024 @@ -486,7 +485,7 @@ def virtual_memory(): def swap_memory(): """Return swap memory metrics.""" mems = {} - with open_binary('%s/meminfo' % get_procfs_path()) as f: + with open_binary(f"{get_procfs_path()}/meminfo") as f: for line in f: fields = line.split() mems[fields[0]] = int(fields[1]) * 1024 @@ -506,12 +505,12 @@ def swap_memory(): percent = usage_percent(used, total, round_=1) # get pgin/pgouts try: - f = open_binary("%s/vmstat" % get_procfs_path()) + f = open_binary(f"{get_procfs_path()}/vmstat") except OSError 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) + f"be determined and were set to 0 ({err})" ) warnings.warn(msg, RuntimeWarning, stacklevel=2) sin = sout = 0 @@ -552,7 +551,7 @@ def cpu_times(): """ procfs_path = get_procfs_path() set_scputimes_ntuple(procfs_path) - with open_binary('%s/stat' % procfs_path) as f: + with open_binary(f"{procfs_path}/stat") as f: values = f.readline().split() fields = values[1 : len(scputimes._fields) + 1] fields = [float(x) / CLOCK_TICKS for x in fields] @@ -566,7 +565,7 @@ def per_cpu_times(): procfs_path = get_procfs_path() set_scputimes_ntuple(procfs_path) cpus = [] - with open_binary('%s/stat' % procfs_path) as f: + with open_binary(f"{procfs_path}/stat") as f: # get rid of the first line which refers to system wide CPU stats f.readline() for line in f: @@ -586,7 +585,7 @@ def cpu_count_logical(): except ValueError: # as a second fallback we try to parse /proc/cpuinfo num = 0 - with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + with open_binary(f"{get_procfs_path()}/cpuinfo") as f: for line in f: if line.lower().startswith(b'processor'): num += 1 @@ -596,7 +595,7 @@ def cpu_count_logical(): # try to parse /proc/stat as a last resort if num == 0: search = re.compile(r'cpu\d') - with open_text('%s/stat' % get_procfs_path()) as f: + with open_text(f"{get_procfs_path()}/stat") as f: for line in f: line = line.split(' ')[0] if search.match(line): @@ -629,7 +628,7 @@ def cpu_count_cores(): # Method #2 mapping = {} current_info = {} - with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + with open_binary(f"{get_procfs_path()}/cpuinfo") as f: for line in f: line = line.strip().lower() if not line: @@ -652,7 +651,7 @@ def cpu_count_cores(): def cpu_stats(): """Return various CPU stats as a named tuple.""" - with open_binary('%s/stat' % get_procfs_path()) as f: + with open_binary(f"{get_procfs_path()}/stat") as f: ctx_switches = None interrupts = None soft_interrupts = None @@ -678,7 +677,7 @@ def cpu_stats(): def _cpu_get_cpuinfo_freq(): """Return current CPU frequency from cpuinfo if available.""" ret = [] - with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + with open_binary(f"{get_procfs_path()}/cpuinfo") as f: for line in f: if line.lower().startswith(b'cpu mhz'): ret.append(float(line.split(b':', 1)[1])) @@ -784,9 +783,9 @@ def __init__(self): def get_proc_inodes(self, pid): inodes = defaultdict(list) - for fd in os.listdir("%s/%s/fd" % (self._procfs_path, pid)): + for fd in os.listdir(f"{self._procfs_path}/{pid}/fd"): try: - inode = readlink("%s/%s/fd/%s" % (self._procfs_path, pid, fd)) + inode = readlink(f"{self._procfs_path}/{pid}/fd/{fd}") except (FileNotFoundError, ProcessLookupError): # ENOENT == file which is gone in the meantime; # os.stat('/proc/%s' % self.pid) will be done later @@ -890,10 +889,11 @@ def process_inet(file, family, type_, inodes, filter_pid=None): line.split()[:10] ) except ValueError: - raise RuntimeError( - "error while parsing %s; malformed line %s %r" - % (file, lineno, line) + msg = ( + f"error while parsing {file}; malformed line" + f" {lineno} {line!r}" ) + raise RuntimeError(msg) if inode in inodes: # # We assume inet sockets are unique, so we error # # out if there are multiple references to the @@ -931,10 +931,10 @@ def process_unix(file, family, inodes, filter_pid=None): if ' ' not in line: # see: https://github.com/giampaolo/psutil/issues/766 continue - raise RuntimeError( - "error while parsing %s; malformed line %r" - % (file, line) + msg = ( + f"error while parsing {file}; malformed line {line!r}" ) + raise RuntimeError(msg) if inode in inodes: # noqa # With UNIX sockets we can have a single inode # referencing many file descriptors. @@ -965,7 +965,7 @@ def retrieve(self, kind, pid=None): inodes = self.get_all_inodes() ret = set() for proto_name, family, type_ in self.tmap[kind]: - path = "%s/net/%s" % (self._procfs_path, proto_name) + path = f"{self._procfs_path}/net/{proto_name}" if family in {socket.AF_INET, socket.AF_INET6}: ls = self.process_inet( path, family, type_, inodes, filter_pid=pid @@ -997,7 +997,7 @@ def net_io_counters(): """Return network I/O statistics for every network interface installed on the system as a dict of raw tuples. """ - with open_text("%s/net/dev" % get_procfs_path()) as f: + with open_text(f"{get_procfs_path()}/net/dev") as f: lines = f.readlines() retdict = {} for line in lines[2:]: @@ -1099,7 +1099,7 @@ def read_procfs(): # See: # https://www.kernel.org/doc/Documentation/iostats.txt # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats - with open_text("%s/diskstats" % get_procfs_path()) as f: + with open_text(f"{get_procfs_path()}/diskstats") as f: lines = f.readlines() for line in lines: fields = line.split() @@ -1122,7 +1122,8 @@ def read_procfs(): reads, rbytes, writes, wbytes = map(int, fields[3:]) rtime = wtime = reads_merged = writes_merged = busy_time = 0 else: - raise ValueError("not sure how to interpret line %r" % line) + msg = f"not sure how to interpret line {line!r}" + raise ValueError(msg) yield (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, writes_merged, busy_time) # fmt: on @@ -1142,16 +1143,16 @@ def read_sysfs(): wtime, reads_merged, writes_merged, busy_time) # fmt: on - if os.path.exists('%s/diskstats' % get_procfs_path()): + if os.path.exists(f"{get_procfs_path()}/diskstats"): gen = read_procfs() elif os.path.exists('/sys/block'): gen = read_sysfs() else: - raise NotImplementedError( - "%s/diskstats nor /sys/block filesystem are available on this " - "system" - % get_procfs_path() + msg = ( + f"{get_procfs_path()}/diskstats nor /sys/block are available on" + " this system" ) + raise NotImplementedError(msg) retdict = {} for entry in gen: @@ -1197,7 +1198,7 @@ def __init__(self): self.minor = os.minor(dev) def ask_proc_partitions(self): - with open_text("%s/partitions" % get_procfs_path()) as f: + with open_text(f"{get_procfs_path()}/partitions") as f: for line in f.readlines()[2:]: fields = line.split() if len(fields) < 4: # just for extra safety @@ -1207,19 +1208,19 @@ def ask_proc_partitions(self): name = fields[3] if major == self.major and minor == self.minor: if name: # just for extra safety - return "/dev/%s" % name + return f"/dev/{name}" def ask_sys_dev_block(self): - path = "/sys/dev/block/%s:%s/uevent" % (self.major, self.minor) + path = f"/sys/dev/block/{self.major}:{self.minor}/uevent" 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 + return f"/dev/{name}" def ask_sys_class_block(self): - needle = "%s:%s" % (self.major, self.minor) + needle = f"{self.major}:{self.minor}" files = glob.iglob("/sys/class/block/*/dev") for file in files: try: @@ -1231,7 +1232,7 @@ def ask_sys_class_block(self): data = f.read().strip() if data == needle: name = os.path.basename(os.path.dirname(file)) - return "/dev/%s" % name + return f"/dev/{name}" def find(self): path = None @@ -1261,7 +1262,7 @@ def disk_partitions(all=False): fstypes = set() procfs_path = get_procfs_path() if not all: - with open_text("%s/filesystems" % procfs_path) as f: + with open_text(f"{procfs_path}/filesystems") as f: for line in f: line = line.strip() if not line.startswith("nodev"): @@ -1276,7 +1277,7 @@ def disk_partitions(all=False): 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) + mounts_path = os.path.realpath(f"{procfs_path}/self/mounts") retlist = [] partitions = cext.disk_partitions(mounts_path) @@ -1558,14 +1559,15 @@ def users(): def boot_time(): """Return the system boot time expressed in seconds since the epoch.""" global BOOT_TIME - path = '%s/stat' % get_procfs_path() + path = f"{get_procfs_path()}/stat" with open_binary(path) as f: for line in f: if line.startswith(b'btime'): ret = float(line.strip().split()[1]) BOOT_TIME = ret return ret - raise RuntimeError("line 'btime' not found in %s" % path) + msg = f"line 'btime' not found in {path}" + raise RuntimeError(msg) # ===================================================================== @@ -1598,7 +1600,7 @@ def pid_exists(pid): # Note: already checked that this is faster than using a # regular expr. Also (a lot) faster than doing # 'return pid in pids()' - path = "%s/%s/status" % (get_procfs_path(), pid) + path = f"{get_procfs_path()}/{pid}/status" with open_binary(path) as f: for line in f: if line.startswith(b"Tgid:"): @@ -1606,7 +1608,8 @@ def pid_exists(pid): # If tgid and pid are the same then we're # dealing with a process PID. return tgid == pid - raise ValueError("'Tgid' line not found in %s" % path) + msg = f"'Tgid' line not found in {path}" + raise ValueError(msg) except (OSError, ValueError): return pid in pids() @@ -1619,7 +1622,7 @@ def ppid_map(): procfs_path = get_procfs_path() for pid in pids(): try: - with open_binary("%s/%s/stat" % (procfs_path, pid)) as f: + with open_binary(f"{procfs_path}/{pid}/stat") as f: data = f.read() except (FileNotFoundError, ProcessLookupError): # Note: we should be able to access /stat for all processes @@ -1652,9 +1655,7 @@ def wrapper(self, *args, **kwargs): # /proc/PID directory may still exist, but the files within # it may not, indicating the process is gone, see: # https://github.com/giampaolo/psutil/issues/2418 - if not os.path.exists( - "%s/%s/stat" % (self._procfs_path, self.pid) - ): + if not os.path.exists(f"{self._procfs_path}/{self.pid}/stat"): raise NoSuchProcess(self.pid, self._name) raise @@ -1680,7 +1681,7 @@ def _is_zombie(self): # 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)) + data = bcat(f"{self._procfs_path}/{self.pid}/stat") except OSError: return False else: @@ -1696,7 +1697,7 @@ 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)) + os.stat(f"{self._procfs_path}/{self.pid}") @wrap_exceptions @memoize_when_activated @@ -1709,7 +1710,7 @@ def _parse_stat_file(self): The return value is cached in case oneshot() ctx manager is in use. """ - data = bcat("%s/%s/stat" % (self._procfs_path, self.pid)) + data = bcat(f"{self._procfs_path}/{self.pid}/stat") # 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 occurrence of ")". @@ -1744,13 +1745,13 @@ def _read_status_file(self): The return value is cached in case oneshot() ctx manager is in use. """ - with open_binary("%s/%s/status" % (self._procfs_path, self.pid)) as f: + with open_binary(f"{self._procfs_path}/{self.pid}/status") 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)) as f: + with open_binary(f"{self._procfs_path}/{self.pid}/smaps") as f: return f.read().strip() def oneshot_enter(self): @@ -1771,19 +1772,19 @@ def name(self): @wrap_exceptions def exe(self): try: - return readlink("%s/%s/exe" % (self._procfs_path, self.pid)) + return readlink(f"{self._procfs_path}/{self.pid}/exe") 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)): + if os.path.lexists(f"{self._procfs_path}/{self.pid}"): return "" raise @wrap_exceptions def cmdline(self): - with open_text("%s/%s/cmdline" % (self._procfs_path, self.pid)) as f: + with open_text(f"{self._procfs_path}/{self.pid}/cmdline") as f: data = f.read() if not data: # may happen in case of zombie process @@ -1809,7 +1810,7 @@ def cmdline(self): @wrap_exceptions def environ(self): - with open_text("%s/%s/environ" % (self._procfs_path, self.pid)) as f: + with open_text(f"{self._procfs_path}/{self.pid}/environ") as f: data = f.read() return parse_environ_block(data) @@ -1823,11 +1824,11 @@ def terminal(self): return None # May not be available on old kernels. - if os.path.exists('/proc/%s/io' % os.getpid()): + if os.path.exists(f"/proc/{os.getpid()}/io"): @wrap_exceptions def io_counters(self): - fname = "%s/%s/io" % (self._procfs_path, self.pid) + fname = f"{self._procfs_path}/{self.pid}/io" fields = {} with open_binary(fname) as f: for line in f: @@ -1842,7 +1843,8 @@ def io_counters(self): else: fields[name] = int(value) if not fields: - raise RuntimeError("%s file was empty" % fname) + msg = f"{fname} file was empty" + raise RuntimeError(msg) try: return pio( fields[b'syscr'], # read syscalls @@ -1853,10 +1855,11 @@ def io_counters(self): 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) + msg = ( + f"{err.args[0]!r} field was not found in {fname}; found" + f" fields are {fields!r}" ) + raise ValueError(msg) @wrap_exceptions def cpu_times(self): @@ -1901,7 +1904,7 @@ def memory_info(self): # | data | data + stack | drs | DATA | # | dirty | dirty pages (unused in Linux 2.6) | dt | | # ============================================================ - with open_binary("%s/%s/statm" % (self._procfs_path, self.pid)) as f: + with open_binary(f"{self._procfs_path}/{self.pid}/statm") as f: vms, rss, shared, text, lib, data, dirty = ( int(x) * PAGESIZE for x in f.readline().split()[:7] ) @@ -2004,10 +2007,11 @@ def get_blocks(lines, current_block): # see issue #369 continue else: - raise ValueError( - "don't know how to interpret line %r" - % line + msg = ( + "don't know how to interpret line" + f" {line!r}" ) + raise ValueError(msg) yield (current_block.pop(), data) data = self._read_smaps_file() @@ -2055,7 +2059,7 @@ def get_blocks(lines, current_block): @wrap_exceptions def cwd(self): - return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) + return readlink(f"{self._procfs_path}/{self.pid}/cwd") @wrap_exceptions def num_ctx_switches( @@ -2064,11 +2068,13 @@ def num_ctx_switches( 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) + msg = ( + "'voluntary_ctxt_switches' and" + " 'nonvoluntary_ctxt_switches'lines were not found in" + f" {self._procfs_path}/{self.pid}/status; the kernel is" + " probably older than 2.6.23" ) + raise NotImplementedError(msg) else: return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) @@ -2080,16 +2086,12 @@ def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')): @wrap_exceptions def threads(self): - thread_ids = os.listdir("%s/%s/task" % (self._procfs_path, self.pid)) + thread_ids = os.listdir(f"{self._procfs_path}/{self.pid}/task") thread_ids.sort() retlist = [] hit_enoent = False for thread_id in thread_ids: - fname = "%s/%s/task/%s/stat" % ( - self._procfs_path, - self.pid, - thread_id, - ) + fname = f"{self._procfs_path}/{self.pid}/task/{thread_id}/stat" try: with open_binary(fname) as f: st = f.read().strip() @@ -2150,15 +2152,17 @@ def cpu_affinity_set(self, 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) + msg = ( + f"invalid CPU {cpu!r}; choose between" + f" {eligible_cpus!r}" ) + raise ValueError(msg) if cpu not in eligible_cpus: - raise ValueError( - "CPU number %r is not eligible; choose " - "between %s" % (cpu, eligible_cpus) + msg = ( + f"CPU number {cpu} is not eligible; choose" + f" between {eligible_cpus}" ) + raise ValueError(msg) raise # only starting from kernel 2.6.13 @@ -2178,7 +2182,8 @@ def ionice_set(self, ioclass, value): IOPriority.IOPRIO_CLASS_IDLE, IOPriority.IOPRIO_CLASS_NONE, }: - raise ValueError("%r ioclass accepts no value" % ioclass) + msg = f"{ioclass!r} ioclass accepts no value" + raise ValueError(msg) if value < 0 or value > 7: msg = "value not in 0-7 range" raise ValueError(msg) @@ -2203,7 +2208,7 @@ def rlimit(self, resource_, limits=None): if len(limits) != 2: msg = ( "second argument must be a (soft, hard) " - + "tuple, got %s" % repr(limits) + f"tuple, got {limits!r}" ) raise ValueError(msg) resource.prlimit(self.pid, resource_, limits) @@ -2224,10 +2229,10 @@ def status(self): @wrap_exceptions def open_files(self): retlist = [] - files = os.listdir("%s/%s/fd" % (self._procfs_path, self.pid)) + files = os.listdir(f"{self._procfs_path}/{self.pid}/fd") hit_enoent = False for fd in files: - file = "%s/%s/fd/%s" % (self._procfs_path, self.pid, fd) + file = f"{self._procfs_path}/{self.pid}/fd/{fd}" try: path = readlink(file) except (FileNotFoundError, ProcessLookupError): @@ -2250,11 +2255,7 @@ def open_files(self): # absolute path though. if path.startswith('/') and isfile_strict(path): # Get file position and flags. - file = "%s/%s/fdinfo/%s" % ( - self._procfs_path, - self.pid, - fd, - ) + file = f"{self._procfs_path}/{self.pid}/fdinfo/{fd}" try: with open_binary(file) as f: pos = int(f.readline().split()[1]) @@ -2281,7 +2282,7 @@ def net_connections(self, kind='inet'): @wrap_exceptions def num_fds(self): - return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid))) + return len(os.listdir(f"{self._procfs_path}/{self.pid}/fd")) @wrap_exceptions def ppid(self): diff --git a/psutil/_psposix.py b/psutil/_psposix.py index e074819861..fdfce147f3 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -151,7 +151,8 @@ def sleep(interval): # continue else: # Should never happen. - raise ValueError("unknown process exit status %r" % status) + msg = f"unknown process exit status {status!r}" + raise ValueError(msg) def disk_usage(path): diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index b741792f31..ac3345b7ac 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -143,7 +143,7 @@ def swap_memory(): p = subprocess.Popen( [ '/usr/bin/env', - 'PATH=/usr/sbin:/sbin:%s' % os.environ['PATH'], + f"PATH=/usr/sbin:/sbin:{os.environ['PATH']}", 'swap', '-l', ], @@ -152,7 +152,8 @@ def swap_memory(): stdout, _ = p.communicate() stdout = stdout.decode(sys.stdout.encoding) if p.returncode != 0: - raise RuntimeError("'swap -l' failed (retcode=%s)" % p.returncode) + msg = f"'swap -l' failed (retcode={p.returncode})" + raise RuntimeError(msg) lines = stdout.strip().split('\n')[1:] if not lines: @@ -239,7 +240,7 @@ def disk_partitions(all=False): continue except OSError as err: # https://github.com/giampaolo/psutil/issues/1674 - debug("skipping %r: %s" % (mountpoint, err)) + debug(f"skipping {mountpoint!r}: {err}") continue ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) @@ -387,7 +388,7 @@ 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)) + os.stat(f"{self._procfs_path}/{self.pid}") def oneshot_enter(self): self._proc_name_and_args.cache_activate(self) @@ -408,7 +409,7 @@ def _proc_name_and_args(self): @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) + f"{self._procfs_path}/{self.pid}/psinfo" ): raise AccessDenied(self.pid) ret = cext.proc_basic_info(self.pid, self._procfs_path) @@ -428,9 +429,7 @@ def name(self): @wrap_exceptions def exe(self): try: - return os.readlink( - "%s/%s/path/a.out" % (self._procfs_path, self.pid) - ) + return os.readlink(f"{self._procfs_path}/{self.pid}/path/a.out") except OSError: pass # continue and guess the exe name from the cmdline # Will be guessed later from cmdline but we want to explicitly @@ -527,9 +526,7 @@ def terminal(self): if tty != cext.PRNODEV: for x in (0, 1, 2, 255): try: - return os.readlink( - '%s/%d/path/%d' % (procfs_path, self.pid, x) - ) + return os.readlink(f"{procfs_path}/{self.pid}/path/{x}") except FileNotFoundError: hit_enoent = True continue @@ -544,9 +541,9 @@ def cwd(self): # Reference: http://goo.gl/55XgO procfs_path = self._procfs_path try: - return os.readlink("%s/%s/path/cwd" % (procfs_path, self.pid)) + return os.readlink(f"{procfs_path}/{self.pid}/path/cwd") except FileNotFoundError: - os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD + os.stat(f"{procfs_path}/{self.pid}") # raise NSP or AD return "" @wrap_exceptions @@ -568,7 +565,7 @@ def status(self): def threads(self): procfs_path = self._procfs_path ret = [] - tids = os.listdir('%s/%d/lwp' % (procfs_path, self.pid)) + tids = os.listdir(f"{procfs_path}/{self.pid}/lwp") hit_enoent = False for tid in tids: tid = int(tid) @@ -603,8 +600,8 @@ def open_files(self): retlist = [] hit_enoent = False procfs_path = self._procfs_path - pathdir = '%s/%d/path' % (procfs_path, self.pid) - for fd in os.listdir('%s/%d/fd' % (procfs_path, self.pid)): + pathdir = f"{procfs_path}/{self.pid}/path" + for fd in os.listdir(f"{procfs_path}/{self.pid}/fd"): path = os.path.join(pathdir, fd) if os.path.islink(path): try: @@ -636,7 +633,8 @@ def _get_unix_sockets(self, pid): raise AccessDenied(self.pid, self._name) if 'no such process' in stderr.lower(): raise NoSuchProcess(self.pid, self._name) - raise RuntimeError("%r command error\n%s" % (cmd, stderr)) + msg = f"{cmd!r} command error\n{stderr}" + raise RuntimeError(msg) lines = stdout.split('\n')[2:] for i, line in enumerate(lines): @@ -662,7 +660,7 @@ def net_connections(self, kind='inet'): # is no longer there. if not ret: # will raise NSP if process is gone - os.stat('%s/%s' % (self._procfs_path, self.pid)) + os.stat(f"{self._procfs_path}/{self.pid}") # UNIX sockets if kind in {'all', 'unix'}: @@ -678,9 +676,8 @@ def net_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 "{}-{}".format( + hex(start)[2:].strip('L'), hex(end)[2:].strip('L') ) procfs_path = self._procfs_path @@ -705,9 +702,7 @@ def toaddr(start, end): addr = toaddr(addr, addrsize) if not name.startswith('['): try: - name = os.readlink( - '%s/%s/path/%s' % (procfs_path, self.pid, name) - ) + name = os.readlink(f"{procfs_path}/{self.pid}/path/{name}") except OSError as err: if err.errno == errno.ENOENT: # sometimes the link may not be resolved by @@ -716,7 +711,7 @@ def toaddr(start, end): # unresolved link path. # This seems an inconsistency with /proc similar # to: http://goo.gl/55XgO - name = '%s/%s/path/%s' % (procfs_path, self.pid, name) + name = f"{procfs_path}/{self.pid}/path/{name}" hit_enoent = True else: raise @@ -727,7 +722,7 @@ def toaddr(start, end): @wrap_exceptions def num_fds(self): - return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid))) + return len(os.listdir(f"{self._procfs_path}/{self.pid}/fd")) @wrap_exceptions def num_ctx_switches(self): diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e3c7db3ca6..7a6558fe1a 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -476,14 +476,11 @@ def __init__(self, name, display_name): self._display_name = display_name def __str__(self): - details = "(name=%r, display_name=%r)" % ( - self._name, - self._display_name, - ) - return "%s%s" % (self.__class__.__name__, details) + details = f"(name={self._name!r}, display_name={self._display_name!r})" + return f"{self.__class__.__name__}{details}" def __repr__(self): - return "<%s at %s>" % (self.__str__(), id(self)) + return f"<{self.__str__()} at {id(self)}>" def __eq__(self, other): # Test for equality with another WindosService object based @@ -525,15 +522,15 @@ def _wrap_exceptions(self): except OSError as err: if is_permission_err(err): msg = ( - "service %r is not querable (not enough privileges)" - % self._name + f"service {self._name!r} is not querable (not enough" + " privileges)" ) 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 + msg = f"service {self._name!r} does not exist" raise NoSuchProcess(pid=None, name=self._name, msg=msg) else: raise @@ -759,7 +756,7 @@ def exe(self): # 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) + debug(f"{err!r} translated into AccessDenied") raise AccessDenied(self.pid, self._name) raise else: @@ -1024,7 +1021,8 @@ def ionice_set(self, ioclass, value): IOPriority.IOPRIO_NORMAL, IOPriority.IOPRIO_HIGH, }: - raise ValueError("%s is not a valid priority" % ioclass) + msg = f"{ioclass} is not a valid priority" + raise ValueError(msg) cext.proc_io_priority_set(self.pid, ioclass) @wrap_exceptions @@ -1066,7 +1064,8 @@ def from_bitmask(x): def cpu_affinity_set(self, value): def to_bitmask(ls): if not ls: - raise ValueError("invalid argument %r" % ls) + msg = f"invalid argument {ls!r}" + raise ValueError(msg) out = 0 for b in ls: out |= 2**b @@ -1079,11 +1078,11 @@ def to_bitmask(ls): for cpu in value: if cpu not in allcpus: if not isinstance(cpu, int): - raise TypeError( - "invalid CPU %r; an integer is required" % cpu - ) + msg = f"invalid CPU {cpu!r}; an integer is required" + raise TypeError(msg) else: - raise ValueError("invalid CPU %r" % cpu) + msg = f"invalid CPU {cpu!r}" + raise ValueError(msg) bitmask = to_bitmask(value) cext.proc_cpu_affinity_set(self.pid, bitmask) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 13fed17787..c852ead0a3 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -174,9 +174,9 @@ def macos_version(): # Disambiguate TESTFN for parallel testing. if os.name == 'java': # Jython disallows @ in module names - TESTFN_PREFIX = '$psutil-%s-' % os.getpid() + TESTFN_PREFIX = f"$psutil-{os.getpid()}-" else: - TESTFN_PREFIX = '@psutil-%s-' % os.getpid() + TESTFN_PREFIX = f"@psutil-{os.getpid()}-" UNICODE_SUFFIX = "-ƒőő" # An invalid unicode string. INVALID_UNICODE_SUFFIX = b"f\xc0\x80".decode('utf8', 'surrogateescape') @@ -289,7 +289,7 @@ def __init__(self): def __repr__(self): name = self.__class__.__name__ - return '<%s running=%s at %#x>' % (name, self._running, id(self)) + return f"<{name} running={self._running} at {id(self):#x}>" def __enter__(self): self.start() @@ -364,8 +364,8 @@ def spawn_testproc(cmd=None, **kwds): safe_rmpath(testfn) pyline = ( "import time;" - + "open(r'%s', 'w').close();" % testfn - + "[time.sleep(0.1) for x in range(100)];" # 10 secs + f"open(r'{testfn}', 'w').close();" + "[time.sleep(0.1) for x in range(100)];" # 10 secs ) cmd = [PYTHON_EXE, "-c", pyline] sproc = subprocess.Popen(cmd, **kwds) @@ -391,16 +391,16 @@ def spawn_children_pair(): tfile = None testfn = get_testfn(dir=os.getcwd()) try: - s = textwrap.dedent("""\ + s = textwrap.dedent(f"""\ import subprocess, os, sys, time s = "import os, time;" - s += "f = open('%s', 'w');" + s += "f = open('{os.path.basename(testfn)}', 'w');" s += "f.write(str(os.getpid()));" s += "f.close();" s += "[time.sleep(0.1) for x in range(100 * 6)];" - p = subprocess.Popen([r'%s', '-c', s]) + p = subprocess.Popen([r'{PYTHON_EXE}', '-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. @@ -426,7 +426,7 @@ def spawn_zombie(): """ assert psutil.POSIX unix_file = get_testfn() - src = textwrap.dedent("""\ + src = textwrap.dedent(f"""\ import os, sys, time, socket, contextlib child_pid = os.fork() if child_pid > 0: @@ -434,10 +434,10 @@ def spawn_zombie(): else: # this is the zombie process with socket.socket(socket.AF_UNIX) as s: - s.connect('%s') + s.connect('{unix_file}') pid = bytes(str(os.getpid()), 'ascii') s.sendall(pid) - """ % unix_file) + """) tfile = None sock = bind_unix_socket(unix_file) try: @@ -581,7 +581,7 @@ def flush_popen(proc): elif isinstance(p, subprocess.Popen): return term_subprocess_proc(p, wait_timeout) else: - raise TypeError("wrong type %r" % p) + raise TypeError(f"wrong type {p!r}") finally: if isinstance(p, (subprocess.Popen, psutil.Popen)): flush_popen(p) @@ -617,7 +617,7 @@ def reap_children(recursive=False): 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) + warn(f"couldn't terminate process {p!r}; attempting kill()") terminate(p, sig=signal.SIGKILL) @@ -638,7 +638,7 @@ def kernel_version(): else: break if not s: - raise ValueError("can't parse %r" % uname) + raise ValueError(f"can't parse {uname!r}") minor = 0 micro = 0 nums = s.split('.') @@ -786,7 +786,7 @@ def retry_fun(fun): pass except OSError as _: err = _ - warn("ignoring %s" % (str(err))) + warn(f"ignoring {err}") time.sleep(0.01) raise err @@ -919,11 +919,11 @@ def context(exc, match=None): yield einfo except exc as err: if match and not re.search(match, str(err)): - msg = f'"{match}" does not match "{str(err)}"' + msg = f'"{match}" does not match "{err}"' raise AssertionError(msg) einfo._exc = err else: - raise AssertionError("%r not raised" % exc) + raise AssertionError(f"{exc!r} not raised") return context(exc, match=match) @@ -1031,9 +1031,9 @@ def assertProcessGone(self, proc): except psutil.NoSuchProcess as exc: self._check_proc_exc(proc, exc) else: - msg = "Process.%s() didn't raise NSP and returned %r" % ( - name, - ret, + msg = ( + f"Process.{name}() didn't raise NSP and returned" + f" {ret!r}" ) raise AssertionError(msg) proc.wait(timeout=0) # assert not raise TimeoutExpired @@ -1179,15 +1179,16 @@ def _check_fds(self, 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 + msg = ( + f"negative diff {diff!r} (gc probably collected a" + " resource from a previous test)" ) + raise self.fail(msg) if diff > 0: type_ = "fd" if POSIX else "handle" if diff > 1: type_ += "s" - msg = "%s unclosed %s after calling %r" % (diff, type_, fun) + msg = f"{diff} unclosed {type_} after calling {fun!r}" raise self.fail(msg) def _call_ntimes(self, fun, times): @@ -1291,13 +1292,13 @@ def print_sysinfo(): if psutil.LINUX and shutil.which("lsb_release"): info['OS'] = sh('lsb_release -d -s') elif psutil.OSX: - info['OS'] = 'Darwin %s' % platform.mac_ver()[0] + info['OS'] = f"Darwin {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['OS'] = f"{platform.system()} {platform.version()}" info['arch'] = ', '.join( list(platform.architecture()) + [platform.machine()] ) @@ -1312,7 +1313,7 @@ def print_sysinfo(): ]) info['pip'] = getattr(pip, '__version__', 'not installed') if wheel is not None: - info['pip'] += " (wheel=%s)" % wheel.__version__ + info['pip'] += f" (wheel={wheel.__version__})" # UNIX if psutil.POSIX: @@ -1328,7 +1329,7 @@ def print_sysinfo(): # system info['fs-encoding'] = sys.getfilesystemencoding() lang = locale.getlocale() - info['lang'] = '%s, %s' % (lang[0], lang[1]) + info['lang'] = f"{lang[0]}, {lang[1]}" info['boot-time'] = datetime.datetime.fromtimestamp( psutil.boot_time() ).strftime("%Y-%m-%d %H:%M:%S") @@ -1526,9 +1527,9 @@ def test_class_coverage(cls, test_class, ls): 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, + msg = ( + f"{test_class.__class__.__name__!r} class should define a" + f" {meth_name!r} method" ) raise AttributeError(msg) @@ -1539,7 +1540,7 @@ def test(cls): klass = {x for x in dir(psutil.Process) if x[0] != '_'} leftout = (this | ignored) ^ klass if leftout: - raise ValueError("uncovered Process class names: %r" % leftout) + raise ValueError(f"uncovered Process class names: {leftout!r}") class system_namespace: @@ -1618,7 +1619,7 @@ def retry_on_failure(retries=NO_RETRIES): """ def logfun(exc): - print("%r, retrying" % exc, file=sys.stderr) # NOQA + print(f"{exc!r}, retrying", file=sys.stderr) # NOQA return retry( exception=AssertionError, timeout=None, retries=retries, logfun=logfun @@ -1657,8 +1658,8 @@ def wrapper(*args, **kwargs): if not only_if: raise msg = ( - "%r was skipped because it raised NotImplementedError" - % fun.__name__ + f"{fun.__name__!r} was skipped because it raised" + " NotImplementedError" ) raise pytest.skip(msg) @@ -1802,7 +1803,7 @@ def check_net_address(addr, family): 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) + raise ValueError(f"unknown family {family!r}") def check_connection_ntuple(conn): @@ -1888,7 +1889,7 @@ def filter_proc_net_connections(cons): for conn in cons: if POSIX and conn.family == socket.AF_UNIX: if MACOS and "/syslog" in conn.raddr: - debug("skipping %s" % str(conn)) + debug(f"skipping {conn}") continue new.append(conn) return new diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py index 2b0f849be7..10934c12d4 100755 --- a/psutil/tests/test_aix.py +++ b/psutil/tests/test_aix.py @@ -31,7 +31,7 @@ def test_virtual_memory(self): "available", "mmode", ]: - re_pattern += r"(?P<%s>\S+)\s+" % (field,) + re_pattern += rf"(?P<{field}>\S+)\s+" matchobj = re.search(re_pattern, out) assert matchobj is not None @@ -104,7 +104,7 @@ def test_cpu_stats(self): "S5rd", "sysc", ]: - re_pattern += r"(?P<%s>\S+)\s+" % (field,) + re_pattern += rf"(?P<{field}>\S+)\s+" matchobj = re.search(re_pattern, out) assert matchobj is not None diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 8e53f4f3e7..2786c34857 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -86,7 +86,7 @@ def tearDownClass(cls): @pytest.mark.skipif(NETBSD, reason="-o lstart doesn't work on NETBSD") def test_process_create_time(self): - output = sh("ps -o lstart -p %s" % self.pid) + output = sh(f"ps -o lstart -p {self.pid}") start_ps = output.replace('STARTED', '').strip() start_psutil = psutil.Process(self.pid).create_time() start_psutil = time.strftime( @@ -98,7 +98,7 @@ 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() + out = sh(f'df -k "{path}"').strip() lines = out.split('\n') lines.pop(0) line = lines.pop(0) @@ -117,9 +117,9 @@ def df(path): assert usage.total == total # 10 MB tolerance if abs(usage.free - free) > 10 * 1024 * 1024: - raise self.fail("psutil=%s, df=%s" % (usage.free, free)) + raise self.fail(f"psutil={usage.free}, df={free}") if abs(usage.used - used) > 10 * 1024 * 1024: - raise self.fail("psutil=%s, df=%s" % (usage.used, used)) + raise self.fail(f"psutil={usage.used}, df={used}") @pytest.mark.skipif( not shutil.which("sysctl"), reason="sysctl cmd not available" @@ -144,7 +144,7 @@ def test_virtual_memory_total(self): def test_net_if_stats(self): for name, stats in psutil.net_if_stats().items(): try: - out = sh("ifconfig %s" % name) + out = sh(f"ifconfig {name}") except RuntimeError: pass else: @@ -170,7 +170,7 @@ def tearDownClass(cls): @retry_on_failure() def test_memory_maps(self): - out = sh('procstat -v %s' % self.pid) + out = sh(f"procstat -v {self.pid}") maps = psutil.Process(self.pid).memory_maps(grouped=False) lines = out.split('\n')[1:] while lines: @@ -178,23 +178,23 @@ def test_memory_maps(self): fields = line.split() _, start, stop, _perms, res = fields[:5] map = maps.pop() - assert "%s-%s" % (start, stop) == map.addr + assert f"{start}-{stop}" == map.addr assert int(res) == map.rss if not map.path.startswith('['): assert fields[10] == map.path def test_exe(self): - out = sh('procstat -b %s' % self.pid) + out = sh(f"procstat -b {self.pid}") assert psutil.Process(self.pid).exe() == out.split('\n')[1].split()[-1] def test_cmdline(self): - out = sh('procstat -c %s' % self.pid) + out = sh(f"procstat -c {self.pid}") assert ' '.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) + out = sh(f"procstat -s {self.pid}") euid, ruid, suid, egid, rgid, sgid = out.split('\n')[1].split()[2:8] p = psutil.Process(self.pid) uids = p.uids() @@ -209,7 +209,7 @@ def test_uids_gids(self): @retry_on_failure() def test_ctx_switches(self): tested = [] - out = sh('procstat -r %s' % self.pid) + out = sh(f"procstat -r {self.pid}") p = psutil.Process(self.pid) for line in out.split('\n'): line = line.lower().strip() @@ -229,7 +229,7 @@ def test_ctx_switches(self): @retry_on_failure() def test_cpu_times(self): tested = [] - out = sh('procstat -r %s' % self.pid) + out = sh(f"procstat -r {self.pid}") p = psutil.Process(self.pid) for line in out.split('\n'): line = line.lower().strip() @@ -256,7 +256,7 @@ def parse_swapinfo(): parts = re.split(r'\s+', output) if not parts: - raise ValueError("Can't parse swapinfo: %s" % output) + raise ValueError(f"Can't parse swapinfo: {output}") # the size is in 1k units, so multiply by 1024 total, used, free = (int(p) * 1024 for p in parts[1:4]) @@ -423,7 +423,7 @@ def test_sensors_battery(self): def secs2hours(secs): m, _s = divmod(secs, 60) h, m = divmod(m, 60) - return "%d:%02d" % (h, m) + return f"{int(h)}:{int(m):02}" out = sh("acpiconf -i 0") fields = {x.split('\t')[0]: x.split('\t')[-1] for x in out.split("\n")} @@ -466,7 +466,7 @@ def test_sensors_battery_no_battery(self): 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 + sensor = f"dev.cpu.{cpu}.temperature" # sysctl returns a string in the format 46.0C try: sysctl_result = int(float(sysctl(sensor)[:-1])) @@ -480,7 +480,7 @@ def test_sensors_temperatures_against_sysctl(self): < 10 ) - sensor = "dev.cpu.%s.coretemp.tjmax" % cpu + sensor = f"dev.cpu.{cpu}.coretemp.tjmax" sysctl_result = int(float(sysctl(sensor)[:-1])) assert ( psutil.sensors_temperatures()["coretemp"][cpu].high @@ -515,7 +515,7 @@ def parse_meminfo(look_for): for line in f: if line.startswith(look_for): return int(line.split()[1]) * 1024 - raise ValueError("can't find %s" % look_for) + raise ValueError(f"can't find {look_for}") # --- virtual mem diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 47f69fed5c..a082f90158 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -522,14 +522,14 @@ def test_multi_sockets_procs(self): for _ in range(times): fname = self.get_testfn() fnames.append(fname) - src = textwrap.dedent("""\ + src = textwrap.dedent(f"""\ import time, os from psutil.tests import create_sockets with create_sockets(): - with open(r'%s', 'w') as f: + with open(r'{fname}', 'w') as f: f.write("hello") [time.sleep(0.1) for x in range(100)] - """ % fname) + """) sproc = self.pyrun(src) pids.append(sproc.pid) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index ea96a0a739..945e0f5353 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -110,7 +110,7 @@ def get_ipv6_addresses(ifname): all_fields.append(fields) if len(all_fields) == 0: - raise ValueError("could not find interface %r" % ifname) + raise ValueError(f"could not find interface {ifname!r}") for i in range(len(all_fields)): unformatted = all_fields[i][0] @@ -131,7 +131,7 @@ def get_mac_address(ifname): info = fcntl.ioctl( s.fileno(), SIOCGIFHWADDR, struct.pack('256s', ifname) ) - return ''.join(['%02x:' % char for char in info[18:24]])[:-1] + return "".join([f"{char:02x}:" for char in info[18:24]])[:-1] def free_swap(): @@ -145,9 +145,7 @@ def free_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) - ) + raise ValueError(f"can't find 'Swap' in 'free' output:\n{out}") def free_physmem(): @@ -167,9 +165,7 @@ def free_physmem(): '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) - ) + raise ValueError(f"can't find 'Mem' in 'free' output:\n{out}") def vmstat(stat): @@ -178,7 +174,7 @@ def vmstat(stat): line = line.strip() if stat in line: return int(line.split(' ')[0]) - raise ValueError("can't find %r in 'vmstat' output" % stat) + raise ValueError(f"can't find {stat!r} in 'vmstat' output") def get_free_version_info(): @@ -271,7 +267,7 @@ def test_shared(self): psutil_value = psutil.virtual_memory().shared assert ( abs(free_value - psutil_value) < TOLERANCE_SYS_MEM - ), '%s %s \n%s' % (free_value, psutil_value, free.output) + ), f"{free_value} {psutil_value} \n{free.output}" @retry_on_failure() def test_available(self): @@ -286,7 +282,7 @@ def test_available(self): psutil_value = psutil.virtual_memory().available assert ( abs(free_value - psutil_value) < TOLERANCE_SYS_MEM - ), '%s %s \n%s' % (free_value, psutil_value, out) + ), f"{free_value} {psutil_value} \n{out}" @pytest.mark.skipif(not LINUX, reason="LINUX only") @@ -993,7 +989,7 @@ class TestSystemNetIfStats(PsutilTestCase): def test_against_ifconfig(self): for name, stats in psutil.net_if_stats().items(): try: - out = sh("ifconfig %s" % name) + out = sh(f"ifconfig {name}") except RuntimeError: pass else: @@ -1004,7 +1000,7 @@ def test_against_ifconfig(self): def test_mtu(self): for name, stats in psutil.net_if_stats().items(): - with open("/sys/class/net/%s/mtu" % name) as f: + with open(f"/sys/class/net/{name}/mtu") as f: assert stats.mtu == int(f.read().strip()) @pytest.mark.skipif( @@ -1016,7 +1012,7 @@ def test_flags(self): matches_found = 0 for name, stats in psutil.net_if_stats().items(): try: - out = sh("ifconfig %s" % name) + out = sh(f"ifconfig {name}") except RuntimeError: pass else: @@ -1049,7 +1045,7 @@ class TestSystemNetIOCounters(PsutilTestCase): def test_against_ifconfig(self): def ifconfig(nic): ret = {} - out = sh("ifconfig %s" % nic) + out = sh(f"ifconfig {nic}") ret['packets_recv'] = int( re.findall(r'RX packets[: ](\d+)', out)[0] ) @@ -1133,7 +1129,7 @@ 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() + out = sh(f'df -P -B 1 "{path}"').strip() lines = out.split('\n') lines.pop(0) line = lines.pop(0) @@ -1337,9 +1333,7 @@ def test_call_methods(self): else: with pytest.raises(FileNotFoundError): finder.ask_proc_partitions() - if os.path.exists( - "/sys/dev/block/%s:%s/uevent" % (self.major, self.minor) - ): + if os.path.exists(f"/sys/dev/block/{self.major}:{self.minor}/uevent"): finder.ask_sys_dev_block() else: with pytest.raises(FileNotFoundError): @@ -1354,9 +1348,7 @@ def test_comparisons(self): 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) - ): + if os.path.exists(f"/sys/dev/block/{self.major}:{self.minor}/uevent"): b = finder.ask_sys_class_block() c = finder.ask_sys_dev_block() @@ -1865,7 +1857,7 @@ def test_parse_smaps_mocked(self): Locked: 19 kB VmFlags: rd ex """).encode() - with mock_open_content({"/proc/%s/smaps" % os.getpid(): content}) as m: + with mock_open_content({f"/proc/{os.getpid()}/smaps": content}) as m: p = psutil._pslinux.Process(os.getpid()) uss, pss, swap = p._parse_smaps() assert m.called @@ -2036,7 +2028,7 @@ def test_threads_mocked(self): # 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()): + if name.startswith(f"/proc/{os.getpid()}/task"): raise FileNotFoundError else: return orig_open(name, *args, **kwargs) @@ -2050,7 +2042,7 @@ def open_mock_1(name, *args, **kwargs): # ...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()): + if name.startswith(f"/proc/{os.getpid()}/task"): raise PermissionError else: return orig_open(name, *args, **kwargs) @@ -2075,7 +2067,7 @@ 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(), FileNotFoundError + f"/proc/{os.getpid()}/smaps", FileNotFoundError ) as m: p = psutil.Process() with pytest.raises(FileNotFoundError): @@ -2085,7 +2077,7 @@ def test_issue_1014(self): def test_issue_2418(self): p = psutil.Process() with mock_open_exception( - '/proc/%s/statm' % os.getpid(), FileNotFoundError + f"/proc/{os.getpid()}/statm", FileNotFoundError ): with mock.patch("os.path.exists", return_value=False): with pytest.raises(psutil.NoSuchProcess): @@ -2157,7 +2149,7 @@ def test_stat_file_parsing(self): "7", # delayacct_blkio_ticks ] content = " ".join(args).encode() - with mock_open_content({"/proc/%s/stat" % os.getpid(): content}): + with mock_open_content({f"/proc/{os.getpid()}/stat": content}): p = psutil.Process() assert p.name() == 'cat' assert p.status() == psutil.STATUS_ZOMBIE @@ -2180,7 +2172,7 @@ def test_status_file_parsing(self): Cpus_allowed_list:\t0-7 voluntary_ctxt_switches:\t12 nonvoluntary_ctxt_switches:\t13""").encode() - with mock_open_content({"/proc/%s/status" % os.getpid(): content}): + with mock_open_content({f"/proc/{os.getpid()}/status": content}): p = psutil.Process() assert p.num_ctx_switches().voluntary == 12 assert p.num_ctx_switches().involuntary == 13 @@ -2224,7 +2216,7 @@ def setUpClass(cls): def read_status_file(self, linestart): with psutil._psplatform.open_text( - '/proc/%s/status' % self.proc.pid + f"/proc/{self.proc.pid}/status" ) as f: for line in f: line = line.strip() @@ -2234,7 +2226,7 @@ def read_status_file(self, linestart): return int(value) except ValueError: return value - raise ValueError("can't find %r" % linestart) + raise ValueError(f"can't find {linestart!r}") def test_name(self): value = self.read_status_file("Name:") diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 6771dbf982..9d24bb32c1 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -66,8 +66,8 @@ def test_process__repr__(self, func=repr): p = psutil.Process(self.spawn_testproc().pid) r = func(p) assert "psutil.Process" in r - assert "pid=%s" % p.pid in r - assert "name='%s'" % str(p.name()) in r.replace("name=u'", "name='") + assert f"pid={p.pid}" in r + assert f"name='{p.name()}'" in r.replace("name=u'", "name='") assert "status=" in r assert "exitcode=" not in r p.terminate() @@ -83,7 +83,7 @@ def test_process__repr__(self, func=repr): ): p = psutil.Process() r = func(p) - assert "pid=%s" % p.pid in r + assert f"pid={p.pid}" in r assert "status='zombie'" in r assert "name=" not in r with mock.patch.object( @@ -93,7 +93,7 @@ def test_process__repr__(self, func=repr): ): p = psutil.Process() r = func(p) - assert "pid=%s" % p.pid in r + assert f"pid={p.pid}" in r assert "terminated" in r assert "name=" not in r with mock.patch.object( @@ -103,7 +103,7 @@ def test_process__repr__(self, func=repr): ): p = psutil.Process() r = func(p) - assert "pid=%s" % p.pid in r + assert f"pid={p.pid}" in r assert "name=" not in r def test_process__str__(self): @@ -232,7 +232,7 @@ def test__all__(self): fun.__doc__ is not None and 'deprecated' not in fun.__doc__.lower() ): - raise self.fail('%r not in psutil.__all__' % name) + raise self.fail(f"{name!r} not in psutil.__all__") # Import 'star' will break if __all__ is inconsistent, see: # https://github.com/giampaolo/psutil/issues/656 @@ -912,7 +912,7 @@ class TestScripts(PsutilTestCase): @staticmethod def assert_stdout(exe, *args, **kwargs): kwargs.setdefault("env", PYTHON_EXE_ENV) - exe = '%s' % os.path.join(SCRIPTS_DIR, exe) + exe = os.path.join(SCRIPTS_DIR, exe) cmd = [PYTHON_EXE, exe] for arg in args: cmd.append(arg) @@ -941,8 +941,8 @@ def test_coverage(self): 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) + "no test defined for" + f" {os.path.join(SCRIPTS_DIR, name)!r} script" ) @pytest.mark.skipif(not POSIX, reason="POSIX only") @@ -952,7 +952,7 @@ def test_executable(self): 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) + raise self.fail(f"{path!r} is not executable") def test_disk_usage(self): self.assert_stdout('disk_usage.py') diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index a70cdf6415..682012ecfd 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -62,7 +62,7 @@ def tearDownClass(cls): terminate(cls.pid) def test_process_create_time(self): - output = sh("ps -o lstart -p %s" % self.pid) + output = sh(f"ps -o lstart -p {self.pid}") start_ps = output.replace('STARTED', '').strip() hhmmss = start_ps.split(' ')[-2] year = start_ps.split(' ')[-1] @@ -83,7 +83,7 @@ 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() + out = sh(f'df -k "{path}"').strip() lines = out.split('\n') lines.pop(0) line = lines.pop(0) @@ -172,7 +172,7 @@ def test_swapmem_sout(self): def test_net_if_stats(self): for name, stats in psutil.net_if_stats().items(): try: - out = sh("ifconfig %s" % name) + out = sh(f"ifconfig {name}") except RuntimeError: pass else: diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 6c8ac7f492..93e6df6e3a 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -135,7 +135,7 @@ def ps_vsz(pid): def df(device): try: - out = sh("df -k %s" % device).strip() + out = sh(f"df -k {device}").strip() except RuntimeError as err: if "device busy" in str(err).lower(): raise pytest.skip("df returned EBUSY") @@ -357,8 +357,8 @@ def test_nic_names(self): break else: raise self.fail( - "couldn't find %s nic in 'ifconfig -a' output\n%s" - % (nic, output) + f"couldn't find {nic} nic in 'ifconfig -a'" + f" output\n{output}" ) # @pytest.mark.skipif(CI_TESTING and not psutil.users(), @@ -407,9 +407,7 @@ def test_users_started(self): started = [x.capitalize() for x in started] if not tstamp: - raise pytest.skip( - "cannot interpret tstamp in who output\n%s" % (out) - ) + raise pytest.skip(f"cannot interpret tstamp in who output\n{out}") with self.subTest(psutil=psutil.users(), who=out): for idx, u in enumerate(psutil.users()): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 35432b1dbd..1b6269a3d1 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -280,10 +280,10 @@ def test_cpu_times_2(self): # 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)) + raise self.fail(f"expected: {utime}, found: {user_time}") if (max([kernel_time, ktime]) - min([kernel_time, ktime])) > 0.1: - raise self.fail("expected: %s, found: %s" % (ktime, kernel_time)) + raise self.fail(f"expected: {ktime}, found: {kernel_time}") @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") def test_cpu_num(self): @@ -305,8 +305,8 @@ def test_create_time(self): difference = abs(create_time - now) if difference > 2: raise self.fail( - "expected: %s, found: %s, difference: %s" - % (now, create_time, difference) + f"expected: {now}, found: {create_time}, difference:" + f" {difference}" ) # make sure returned value can be pretty printed with strftime @@ -658,7 +658,7 @@ def test_memory_maps(self): # 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: + if f"{nt.path} (deleted)" not in data: raise elif '64' not in os.path.basename(nt.path): # XXX - On Windows we have this strange behavior with @@ -728,7 +728,7 @@ def test_exe(self): # "/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]) + ver = f"{sys.version_info[0]}.{sys.version_info[1]}" try: assert exe.replace(ver, '') == PYTHON_EXE.replace(ver, '') except AssertionError: @@ -1023,7 +1023,7 @@ def test_cpu_affinity_errs(self): p.cpu_affinity(invalid_cpu) with pytest.raises(ValueError): p.cpu_affinity(range(10000, 11000)) - with pytest.raises(TypeError): + with pytest.raises((TypeError, ValueError)): p.cpu_affinity([0, "1"]) with pytest.raises(ValueError): p.cpu_affinity([0, -1]) @@ -1072,8 +1072,8 @@ def test_open_files(self): # another process cmdline = ( - "import time; f = open(r'%s', 'r'); [time.sleep(0.1) for x in" - " range(100)];" % testfn + f"import time; f = open(r'{testfn}', 'r'); [time.sleep(0.1) for x" + " in range(100)];" ) p = self.spawn_psproc([PYTHON_EXE, "-c", cmdline]) @@ -1102,9 +1102,7 @@ def test_open_files_2(self): ): break else: - raise self.fail( - "no file found; files=%s" % (repr(p.open_files())) - ) + raise self.fail(f"no file found; files={p.open_files()!r}") assert normcase(file.path) == normcase(fileobj.name) if WINDOWS: assert file.fd == -1 @@ -1377,7 +1375,7 @@ def assert_raises_nsp(fun, fun_name): if WINDOWS and fun_name in {'exe', 'name'}: return raise self.fail( - "%r didn't raise NSP and returned %r instead" % (fun, ret) + f"{fun!r} didn't raise NSP and returned {ret!r} instead" ) p = self.spawn_psproc() @@ -1435,7 +1433,7 @@ def test_reused_pid(self): with contextlib.redirect_stderr(io.StringIO()) as f: list(psutil.process_iter()) assert ( - "refreshing Process instance for reused PID %s" % p.pid + f"refreshing Process instance for reused PID {p.pid}" in f.getvalue() ) assert p.pid not in psutil._pmap diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 29f3f894e4..24229979c7 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -142,7 +142,7 @@ def test_all(self): info, ) s += '-' * 70 - s += "\n%s" % traceback.format_exc() + s += f"\n{traceback.format_exc()}" s = "\n".join((" " * 4) + i for i in s.splitlines()) + "\n" failures.append(s) else: @@ -484,7 +484,7 @@ def tearDown(self): def test_it(self): def is_linux_tid(pid): try: - f = open("/proc/%s/status" % pid, "rb") + f = open(f"/proc/{pid}/status", "rb") except FileNotFoundError: return False else: diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py index b9638ec44b..b5d9d353b9 100755 --- a/psutil/tests/test_sunos.py +++ b/psutil/tests/test_sunos.py @@ -18,7 +18,7 @@ @pytest.mark.skipif(not SUNOS, reason="SUNOS only") class SunOSSpecificTestCase(PsutilTestCase): def test_swap_memory(self): - out = sh('env PATH=/usr/sbin:/sbin:%s swap -l' % os.environ['PATH']) + out = sh(f"env PATH=/usr/sbin:/sbin:{os.environ['PATH']} swap -l") lines = out.strip().split('\n')[1:] if not lines: raise ValueError('no swap device(s) configured') diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 1e814b1e8f..62d49bf611 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -334,11 +334,10 @@ def test_virtual_memory(self): assert isinstance(value, int) if name != 'total': if not value >= 0: - raise self.fail("%r < 0 (%s)" % (name, value)) + raise self.fail(f"{name!r} < 0 ({value})") if value > mem.total: raise self.fail( - "%r > total (total=%s, %s=%s)" - % (name, mem.total, name, value) + f"{name!r} > total (total={mem.total}, {name}={value})" ) def test_swap_memory(self): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 4e5d2484dd..667f1d6862 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -65,8 +65,8 @@ def powershell(cmd): if not shutil.which("powershell.exe"): raise pytest.skip("powershell.exe not available") cmdline = ( - 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive ' - + '-NoProfile -WindowStyle Hidden -Command "%s"' % cmd + "powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive " + f"-NoProfile -WindowStyle Hidden -Command \"{cmd}\"" # noqa: Q003 ) return sh(cmdline) @@ -77,7 +77,7 @@ def wmic(path, what, converter=int): >>> wmic("Win32_OperatingSystem", "FreePhysicalMemory") 2134124534 """ - out = sh("wmic path %s get %s" % (path, what)).strip() + out = sh(f"wmic path {path} get {what}").strip() data = "".join(out.splitlines()[1:]).strip() # get rid of the header if converter is not None: if "," in what: @@ -142,7 +142,7 @@ def test_nic_names(self): continue if nic not in out: raise self.fail( - "%r nic wasn't found in 'ipconfig /all' output" % nic + f"{nic!r} nic wasn't found in 'ipconfig /all' output" ) def test_total_phymem(self): @@ -222,12 +222,10 @@ def test_disks(self): assert 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) - ) + raise self.fail(f"psutil={usage.free}, wmi={wmi_free}") break else: - raise self.fail("can't find partition %s" % repr(ps_part)) + raise self.fail(f"can't find partition {ps_part!r}") @retry_on_failure() def test_disk_usage(self): @@ -262,10 +260,9 @@ def test_net_if_stats(self): for wmi_adapter in wmi_adapters: wmi_names.add(wmi_adapter.Name) wmi_names.add(wmi_adapter.NetConnectionID) - assert ps_names & wmi_names, "no common entries in %s, %s" % ( - ps_names, - wmi_names, - ) + assert ( + ps_names & wmi_names + ), f"no common entries in {ps_names}, {wmi_names}" def test_boot_time(self): wmi_os = wmi.WMI().Win32_OperatingSystem() @@ -606,7 +603,7 @@ 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) + username = f"{domain}\\{username}" assert p.username() == username @retry_on_failure() @@ -627,7 +624,7 @@ def test_memory_vms(self): # 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)) + raise self.fail(f"wmi={wmi_usage}, psutil={vms}") def test_create_time(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] diff --git a/pyproject.toml b/pyproject.toml index b64adb6ff0..a0d6df88c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,10 +96,11 @@ ignore = [ # T201 == print(), T203 == pprint() # EM101 == raw-string-in-exception # TRY003 == raise-vanilla-args -".github/workflows/*" = ["T201", "T203"] -"psutil/tests/*" = ["EM101", "TRY003"] -"scripts/*" = ["T201", "T203"] -"scripts/internal/*" = ["EM101", "T201", "T203", "TRY003"] +# EM102 == Exception must not use an f-string literal, assign to variable first +".github/workflows/*" = ["EM102", "T201", "T203"] +"psutil/tests/*" = ["EM101", "EM102", "TRY003"] +"scripts/*" = ["EM102", "T201", "T203"] +"scripts/internal/*" = ["EM101", "EM102", "T201", "T203", "TRY003"] "setup.py" = ["T201", "T203"] [tool.ruff.lint.isort] diff --git a/scripts/battery.py b/scripts/battery.py index d9a783daa1..b6d4679ea0 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -22,7 +22,7 @@ def secs2hours(secs): mm, ss = divmod(secs, 60) hh, mm = divmod(mm, 60) - return "%d:%02d:%02d" % (hh, mm, ss) + return f"{int(hh)}:{int(mm):02}:{int(ss):02}" def main(): @@ -32,16 +32,16 @@ def main(): if batt is None: return sys.exit("no battery is installed") - print("charge: %s%%" % round(batt.percent, 2)) + print(f"charge: {round(batt.percent, 2)}%") if batt.power_plugged: print( - "status: %s" - % ("charging" if batt.percent < 100 else "fully charged") + "status: " + f" {'charging' if batt.percent < 100 else 'fully charged'}" ) print("plugged in: yes") else: - print("left: %s" % secs2hours(batt.secsleft)) - print("status: %s" % "discharging") + print(f"left: {secs2hours(batt.secsleft)}") + print("status: discharging") print("plugged in: no") diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index dd7684e873..dfc5f5ae4e 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -66,7 +66,7 @@ def main(): stats = psutil.net_if_stats() io_counters = psutil.net_io_counters(pernic=True) for nic, addrs in psutil.net_if_addrs().items(): - print("%s:" % (nic)) + print(f"{nic}:") if nic in stats: st = stats[nic] print(" stats : ", end='') @@ -103,13 +103,13 @@ def main(): ) for addr in addrs: print(" %-4s" % af_map.get(addr.family, addr.family), end="") - print(" address : %s" % addr.address) + print(f" address : {addr.address}") if addr.broadcast: - print(" broadcast : %s" % addr.broadcast) + print(f" broadcast : {addr.broadcast}") if addr.netmask: - print(" netmask : %s" % addr.netmask) + print(f" netmask : {addr.netmask}") if addr.ptp: - print(" p2p : %s" % addr.ptp) + print(f" p2p : {addr.ptp}") print() diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 43c279a30d..299f9cea67 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -126,8 +126,9 @@ def call_oneshot(funs): def main(): print( - "%s methods involved on platform %r (%s iterations, psutil %s):" - % (len(names), sys.platform, ITERATIONS, psutil.__version__) + f"{len(names)} methods involved on platform" + f" {sys.platform!r} ({ITERATIONS} iterations, psutil" + f" {psutil.__version__}):" ) for name in sorted(names): print(" " + name) @@ -136,19 +137,19 @@ def main(): elapsed1 = timeit.timeit( "call_normal(funs)", setup=setup, number=ITERATIONS ) - print("normal: %.3f secs" % elapsed1) + print(f"normal: {elapsed1:.3f} secs") # "one shot" run elapsed2 = timeit.timeit( "call_oneshot(funs)", setup=setup, number=ITERATIONS ) - print("onshot: %.3f secs" % elapsed2) + print(f"onshot: {elapsed2:.3f} secs") # done if elapsed2 < elapsed1: - print("speedup: +%.2fx" % (elapsed1 / elapsed2)) + print(f"speedup: +{elapsed1 / elapsed2:.2f}x") elif elapsed2 > elapsed1: - print("slowdown: -%.2fx" % (elapsed2 / elapsed1)) + print(f"slowdown: -{elapsed2 / elapsed1:.2f}x") else: print("same speed") diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index 41c9cbb89b..1076dffc8b 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -37,8 +37,8 @@ def main(): args = runner.parse_args() if not args.worker: print( - "%s methods involved on platform %r (psutil %s):" - % (len(names), sys.platform, psutil.__version__) + f"{len(names)} methods involved on platform" + f" {sys.platform!r} (psutil {psutil.__version__}):" ) for name in sorted(names): print(" " + name) diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index ed84385ef4..408886817b 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -212,7 +212,7 @@ def parallel_validator(urls): } for fut in concurrent.futures.as_completed(fut_to_url): current += 1 - sys.stdout.write("\r%s / %s" % (current, total)) + sys.stdout.write(f"\r{current} / {total}") sys.stdout.flush() fname, url = fut_to_url[fut] try: @@ -220,7 +220,7 @@ def parallel_validator(urls): except Exception: # noqa: BLE001 fails.append((fname, url)) print() - print("warn: error while validating %s" % url, file=sys.stderr) + print(f"warn: error while validating {url}", file=sys.stderr) traceback.print_exc() else: if not ok: @@ -242,7 +242,7 @@ def main(): for fname in args.files: urls = get_urls(fname) if urls: - print("%4s %s" % (len(urls), fname)) + print(f"{len(urls):4} {fname}") for url in urls: all_urls.append((fname, url)) @@ -254,7 +254,7 @@ def main(): fname, url = fail print("%-30s: %s " % (fname, url)) print('-' * 20) - print("total: %s fails!" % len(fails)) + print(f"total: {len(fails)} fails!") sys.exit(1) diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py index 225140018a..4f0d99c31e 100755 --- a/scripts/internal/clinter.py +++ b/scripts/internal/clinter.py @@ -17,7 +17,7 @@ def warn(path, line, lineno, msg): global warned warned = True - print("%s:%s: %s" % (path, lineno, msg), file=sys.stderr) + print(f"{path}:{lineno}: {msg}", file=sys.stderr) def check_line(path, line, idx, lines): @@ -49,7 +49,7 @@ def check_line(path, line, idx, lines): keywords = ("if", "else", "while", "do", "enum", "for") for kw in keywords: if sls.startswith(kw + '('): - warn(path, line, lineno, "missing space between %r and '('" % kw) + warn(path, line, lineno, f"missing space between {kw!r} and '('") # eof if eof and not line.endswith('\n'): warn(path, line, lineno, "no blank line at EOF") diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py index bd9e743908..73cda80f1f 100755 --- a/scripts/internal/download_wheels.py +++ b/scripts/internal/download_wheels.py @@ -34,10 +34,10 @@ def get_artifacts(): - base_url = "https://api.github.com/repos/%s/%s" % (USER, PROJECT) + base_url = f"https://api.github.com/repos/{USER}/{PROJECT}" url = base_url + "/actions/artifacts" res = requests.get( - url=url, headers={"Authorization": "token %s" % TOKEN}, timeout=TIMEOUT + url=url, headers={"Authorization": f"token {TOKEN}"}, timeout=TIMEOUT ) res.raise_for_status() data = json.loads(res.content) @@ -47,7 +47,7 @@ def get_artifacts(): def download_zip(url): print("downloading: " + url) res = requests.get( - url=url, headers={"Authorization": "token %s" % TOKEN}, timeout=TIMEOUT + url=url, headers={"Authorization": f"token {TOKEN}"}, timeout=TIMEOUT ) res.raise_for_status() totbytes = 0 @@ -55,7 +55,7 @@ def download_zip(url): for chunk in res.iter_content(chunk_size=16384): f.write(chunk) totbytes += len(chunk) - print("got %s, size %s)" % (OUTFILE, bytes2human(totbytes))) + print(f"got {OUTFILE}, size {bytes2human(totbytes)})") def run(): diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index de4b461e63..10f6368da3 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -45,7 +45,7 @@ def hilite(s, ok=True, bold=False): attr.append('31') if bold: attr.append('1') - return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) + return f"\x1b[{';'.join(attr)}m{s}\x1b[0m" def exit(msg): @@ -97,7 +97,7 @@ def git_commit_files(): def black(files): - print("running black (%s)" % len(files)) + print(f"running black ({len(files)})") cmd = [PYTHON, "-m", "black", "--check", "--safe"] + files if subprocess.call(cmd) != 0: return exit( @@ -107,8 +107,15 @@ def black(files): def ruff(files): - print("running ruff (%s)" % len(files)) - cmd = [PYTHON, "-m", "ruff", "check", "--no-cache"] + files + print(f"running ruff ({len(files)})") + cmd = [ + PYTHON, + "-m", + "ruff", + "check", + "--no-cache", + "--output-format=concise", + ] + files if subprocess.call(cmd) != 0: return exit( "Python code didn't pass 'ruff' style check." @@ -117,7 +124,7 @@ def ruff(files): def c_linter(files): - print("running clinter (%s)" % len(files)) + print(f"running clinter ({len(files)})") # XXX: we should escape spaces and possibly other amenities here cmd = [PYTHON, "scripts/internal/clinter.py"] + files if subprocess.call(cmd) != 0: @@ -125,14 +132,14 @@ def c_linter(files): def toml_sort(files): - print("running toml linter (%s)" % len(files)) + print(f"running toml linter ({len(files)})") cmd = ["toml-sort", "--check"] + files if subprocess.call(cmd) != 0: - return sys.exit("%s didn't pass style check" % ' '.join(files)) + return sys.exit(f"{' '.join(files)} didn't pass style check") def rstcheck(files): - print("running rst linter (%s)" % len(files)) + print(f"running rst linter ({len(files)})") cmd = ["rstcheck", "--config=pyproject.toml"] + files if subprocess.call(cmd) != 0: return sys.exit("RST code didn't pass style check") diff --git a/scripts/internal/install_pip.py b/scripts/internal/install_pip.py index 9b1ee7a215..bca5d5fe7b 100755 --- a/scripts/internal/install_pip.py +++ b/scripts/internal/install_pip.py @@ -31,7 +31,7 @@ def main(): else None ) with tempfile.NamedTemporaryFile(suffix=".py") as f: - print("downloading %s into %s" % (URL, f.name)) + print(f"downloading {URL} into {f.name}") kwargs = dict(context=ssl_context) if ssl_context else {} req = urlopen(URL, **kwargs) data = req.read() @@ -41,7 +41,7 @@ def main(): f.flush() print("download finished, installing pip") - code = os.system("%s %s --user --upgrade" % (sys.executable, f.name)) + code = os.system(f"{sys.executable} {f.name} --user --upgrade") sys.exit(code) diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 6bb0cdd081..dbf96f2d80 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -78,7 +78,7 @@ def main(): for methname, ads in sorted(d.items(), key=lambda x: (x[1], x[0])): perc = (ads / tot_procs) * 100 outcome = "SUCCESS" if not ads else "ACCESS DENIED" - s = templ % (methname, ads, "%6.1f%%" % perc, outcome) + s = templ % (methname, ads, f"{perc:6.1f}%", outcome) print_color(s, "red" if ads else None) tot_perc = round((tot_ads / tot_calls) * 100, 1) print("-" * 50) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 3fecbfbcd0..673f51c899 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -95,7 +95,7 @@ def print_timings(): i = 0 while timings[:]: title, times, elapsed = timings.pop(0) - s = templ % (title, str(times), "%.5f" % elapsed) + s = templ % (title, str(times), f"{elapsed:.5f}") if i > len(timings) - 5: print_color(s, color="red") else: diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 1fb4b3f3aa..5e2a6e5989 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -54,7 +54,7 @@ def platform(self): else: return 'macos' else: - raise ValueError("unknown platform %r" % self.name) + raise ValueError(f"unknown platform {self.name!r}") def arch(self): if self.name.endswith(('x86_64.whl', 'amd64.whl')): @@ -106,14 +106,14 @@ def main(): elif path.endswith(".tar.gz"): pkg = Tarball(path) else: - raise ValueError("invalid package %r" % path) + raise ValueError(f"invalid package {path!r}") groups[pkg.platform()].append(pkg) tot_files = 0 tot_size = 0 templ = "%-120s %7s %8s %7s" for platf, pkgs in groups.items(): - ppn = "%s (%s)" % (platf, len(pkgs)) + ppn = f"{platf} ({len(pkgs)})" s = templ % (ppn, "size", "arch", "pyver") print_color('\n' + s, color=None, bold=True) for pkg in sorted(pkgs, key=lambda x: x.name): @@ -131,7 +131,7 @@ def main(): print_color(s, color='brown') print_color( - "\n\ntotals: files=%s, size=%s" % (tot_files, bytes2human(tot_size)), + f"\n\ntotals: files={tot_files}, size={bytes2human(tot_size)}", bold=True, ) diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index ae62169073..70afd4b831 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -68,7 +68,7 @@ def query(cmd): def top_packages(): global LAST_UPDATE ret = query( - "pypinfo --all --json --days %s --limit %s '' project" % (DAYS, LIMIT) + f"pypinfo --all --json --days {DAYS} --limit {LIMIT} '' project" ) LAST_UPDATE = ret['last_update'] return [(x['project'], x['download_count']) for x in ret['rows']] @@ -81,7 +81,7 @@ def ranking(): if name == PKGNAME: return i i += 1 - raise ValueError("can't find %s" % PKGNAME) + raise ValueError(f"can't find {PKGNAME}") def downloads(): @@ -89,23 +89,23 @@ def downloads(): for name, downloads in data: if name == PKGNAME: return downloads - raise ValueError("can't find %s" % PKGNAME) + raise ValueError(f"can't find {PKGNAME}") def downloads_pyver(): - return query("pypinfo --json --days %s %s pyversion" % (DAYS, PKGNAME)) + return query(f"pypinfo --json --days {DAYS} {PKGNAME} pyversion") def downloads_by_country(): - return query("pypinfo --json --days %s %s country" % (DAYS, PKGNAME)) + return query(f"pypinfo --json --days {DAYS} {PKGNAME} country") def downloads_by_system(): - return query("pypinfo --json --days %s %s system" % (DAYS, PKGNAME)) + return query(f"pypinfo --json --days {DAYS} {PKGNAME} system") def downloads_by_distro(): - return query("pypinfo --json --days %s %s distro" % (DAYS, PKGNAME)) + return query(f"pypinfo --json --days {DAYS} {PKGNAME} distro") # --- print @@ -116,7 +116,7 @@ def downloads_by_distro(): def print_row(left, right): if isinstance(right, int): - right = f'{right:,}' + right = f"{right:,}" print(templ % (left, right)) @@ -142,9 +142,9 @@ def main(): print("# Download stats") print() - s = "psutil download statistics of the last %s days (last update " % DAYS - s += "*%s*).\n" % LAST_UPDATE - s += "Generated via [pypistats.py](%s) script.\n" % GITHUB_SCRIPT_URL + s = f"psutil download statistics of the last {DAYS} days (last update " + s += f"*{LAST_UPDATE}*).\n" + s += f"Generated via [pypistats.py]({GITHUB_SCRIPT_URL}) script.\n" print(s) data = [ @@ -171,4 +171,4 @@ def main(): try: main() finally: - print("bytes billed: %s" % bytes_billed, file=sys.stderr) + print(f"bytes billed: {bytes_billed}", file=sys.stderr) diff --git a/scripts/internal/print_hashes.py b/scripts/internal/print_hashes.py index 5b9cfb2091..b8d8b365dc 100755 --- a/scripts/internal/print_hashes.py +++ b/scripts/internal/print_hashes.py @@ -31,12 +31,9 @@ def main(): if os.path.isfile(file): md5 = csum(file, "md5") sha256 = csum(file, "sha256") - print( - "%s\nmd5: %s\nsha256: %s\n" - % (os.path.basename(file), md5, sha256) - ) + print(f"{os.path.basename(file)}\nmd5: {md5}\nsha256: {sha256}\n") else: - print("skipping %r (not a file)" % file) + print(f"skipping {file!r} (not a file)") if __name__ == "__main__": diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 2e076f0df5..5413b827b6 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -96,7 +96,7 @@ def win_colorprint(s, color=LIGHTBLUE): def sh(cmd, nolog=False): assert isinstance(cmd, list), repr(cmd) if not nolog: - safe_print("cmd: %s" % cmd) + safe_print(f"cmd: {cmd}") p = subprocess.Popen(cmd, env=os.environ, universal_newlines=True) p.communicate() # print stdout/stderr in real time if p.returncode != 0: @@ -120,10 +120,10 @@ def rm(pattern, directory=False): for name in found: path = os.path.join(root, name) if directory: - safe_print("rmdir -f %s" % path) + safe_print(f"rmdir -f {path}") safe_rmtree(path) else: - safe_print("rm %s" % path) + safe_print(f"rm {path}") safe_remove(path) @@ -134,14 +134,14 @@ def safe_remove(path): if err.errno != errno.ENOENT: raise else: - safe_print("rm %s" % path) + safe_print(f"rm {path}") def safe_rmtree(path): existed = os.path.isdir(path) shutil.rmtree(path, ignore_errors=True) if existed and not os.path.isdir(path): - safe_print("rmdir -f %s" % path) + safe_print(f"rmdir -f {path}") def recursive_rm(*patterns): @@ -268,7 +268,7 @@ def uninstall(): if 'psutil' not in line: f.write(line) else: - print("removed line %r from %r" % (line, path)) + print(f"removed line {line!r} from {path!r}") def clean(): @@ -467,7 +467,7 @@ def get_python(path): '312-64', ) for v in vers: - pypath = r'C:\\python%s\python.exe' % v + pypath = rf"C:\\python{v}\python.exe" if path in pypath and os.path.isfile(pypath): return pypath @@ -531,7 +531,7 @@ def main(): PYTHON = get_python(args.python) if not PYTHON: return sys.exit( - "can't find any python installation matching %r" % args.python + f"can't find any python installation matching {args.python!r}" ) os.putenv('PYTHON', PYTHON) win_colorprint("using " + PYTHON) diff --git a/scripts/killall.py b/scripts/killall.py index 592b8d6e3b..532e8b15ce 100755 --- a/scripts/killall.py +++ b/scripts/killall.py @@ -14,7 +14,7 @@ def main(): if len(sys.argv) != 2: - sys.exit('usage: %s name' % __file__) + sys.exit(f"usage: {__file__} name") else: name = sys.argv[1] @@ -24,7 +24,7 @@ def main(): proc.kill() killed.append(proc.pid) if not killed: - sys.exit('%s: no process found' % name) + sys.exit(f"{name}: no process found") else: sys.exit(0) diff --git a/scripts/pidof.py b/scripts/pidof.py index 7ac8e0323f..7c3b93d8ab 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -32,7 +32,7 @@ def pidof(pgname): def main(): if len(sys.argv) != 2: - sys.exit('usage: %s pgname' % __file__) + sys.exit(f"usage: {__file__} pgname") else: pgname = sys.argv[1] pids = pidof(pgname) diff --git a/scripts/pmap.py b/scripts/pmap.py index 719ce01341..ae633a41b3 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -61,7 +61,7 @@ def main(): safe_print(line) print("-" * 31) print(templ % ("Total", bytes2human(total_rss), '', '')) - safe_print("PID = %s, name = %s" % (p.pid, p.name())) + safe_print(f"PID = {p.pid}, name = {p.name()}") if __name__ == '__main__': diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 24004a9606..963dd77da0 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -130,10 +130,10 @@ def str_ntuple(nt, convert_bytes=False): if nt == ACCESS_DENIED: return "" if not convert_bytes: - return ", ".join(["%s=%s" % (x, getattr(nt, x)) for x in nt._fields]) + return ", ".join([f"{x}={getattr(nt, x)}" for x in nt._fields]) else: return ", ".join( - ["%s=%s" % (x, bytes2human(getattr(nt, x))) for x in nt._fields] + [f"{x}={bytes2human(getattr(nt, x))}" for x in nt._fields] ) @@ -148,7 +148,7 @@ def run(pid, verbose=False): with proc.oneshot(): try: parent = proc.parent() - parent = '(%s)' % parent.name() if parent else '' + parent = f"({parent.name()})" if parent else "" except psutil.Error: parent = '' try: @@ -165,7 +165,7 @@ def run(pid, verbose=False): # here we go print_('pid', pinfo['pid']) print_('name', pinfo['name']) - print_('parent', '%s %s' % (pinfo['ppid'], parent)) + print_('parent', f"{pinfo['ppid']} {parent}") print_('exe', pinfo['exe']) print_('cwd', pinfo['cwd']) print_('cmdline', ' '.join(pinfo['cmdline'])) @@ -207,7 +207,7 @@ def run(pid, verbose=False): else: print_( "ionice", - "class=%s, value=%s" % (str(ionice.ioclass), ionice.value), + f"class={ionice.ioclass}, value={ionice.value}", ) print_('num-threads', pinfo['num_threads']) @@ -261,8 +261,8 @@ def run(pid, verbose=False): rip, rport = conn.raddr line = template % ( type, - "%s:%s" % (lip, lport), - "%s:%s" % (rip, rport), + f"{lip}:{lport}", + f"{rip}:{rport}", conn.status, ) print_('', line) @@ -277,7 +277,7 @@ def run(pid, verbose=False): print_("", "[...]") break print_('', template % thread) - print_('', "total=%s" % len(pinfo['threads'])) + print_('', f"total={len(pinfo['threads'])}") else: print_('threads', '') diff --git a/scripts/procsmem.py b/scripts/procsmem.py index c8eaf34078..17eee8fb0f 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -53,8 +53,8 @@ def convert_bytes(n): for s in reversed(symbols): if n >= prefix[s]: value = float(n) / prefix[s] - return '%.1f%s' % (value, s) - return "%sB" % n + return f"{value:.1f}{s}" + return f"{n}B" def main(): @@ -97,7 +97,7 @@ def main(): print(line) if ad_pids: print( - "warning: access denied for %s pids" % (len(ad_pids)), + f"warning: access denied for {len(ad_pids)} pids", file=sys.stderr, ) diff --git a/scripts/sensors.py b/scripts/sensors.py index 861a47d799..fe1286ba5c 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -34,7 +34,7 @@ def secs2hours(secs): mm, ss = divmod(secs, 60) hh, mm = divmod(mm, 60) - return "%d:%02d:%02d" % (hh, mm, ss) + return f"{int(hh)}:{int(mm):02}:{int(ss):02}" def main(): @@ -78,7 +78,7 @@ def main(): # Battery. if battery: print("Battery:") - print(" charge: %s%%" % round(battery.percent, 2)) + print(f" charge: {round(battery.percent, 2)}%") if battery.power_plugged: print( " status: %s" @@ -86,8 +86,8 @@ def main(): ) print(" plugged in: yes") else: - print(" left: %s" % secs2hours(battery.secsleft)) - print(" status: %s" % "discharging") + print(f" left: {secs2hours(battery.secsleft)}") + print(" status: discharging") print(" plugged in: no") diff --git a/scripts/top.py b/scripts/top.py index db206e8965..03f235ec36 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -160,9 +160,9 @@ def get_dashes(perc): st = [] for x, y in procs_status.items(): if y: - st.append("%s=%s" % (x, y)) + st.append(f"{x}={y}") st.sort(key=lambda x: x[:3] in {'run', 'sle'}, reverse=1) - printl(" Processes: %s (%s)" % (num_procs, ', '.join(st))) + printl(f" Processes: {num_procs} ({', '.join(st)})") # load average, uptime uptime = datetime.datetime.now() - datetime.datetime.fromtimestamp( psutil.boot_time() diff --git a/scripts/winservices.py b/scripts/winservices.py index 216d0a6529..6ff240c1d1 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -43,7 +43,7 @@ def main(): for service in psutil.win_service_iter(): info = service.as_dict() - print("%r (%r)" % (info['name'], info['display_name'])) + print(f"{info['name']!r} ({info['display_name']!r})") s = "status: %s, start: %s, username: %s, pid: %s" % ( info['status'], info['start_type'], @@ -51,7 +51,7 @@ def main(): info['pid'], ) print(s) - print("binpath: %s" % info['binpath']) + print(f"binpath: {info['binpath']}") print() From 93318fad1dac5b41be2f84064021c33b623def83 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 16:21:56 +0100 Subject: [PATCH 1168/1714] ruff: ignore specific python2 rules for setup.py --- pyproject.toml | 19 ++++++++++++++++--- setup.py | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a0d6df88c5..d6e87a5bd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,15 +93,28 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -# T201 == print(), T203 == pprint() # EM101 == raw-string-in-exception +# T201 == print(), T203 == pprint() # TRY003 == raise-vanilla-args -# EM102 == Exception must not use an f-string literal, assign to variable first ".github/workflows/*" = ["EM102", "T201", "T203"] "psutil/tests/*" = ["EM101", "EM102", "TRY003"] "scripts/*" = ["EM102", "T201", "T203"] "scripts/internal/*" = ["EM101", "EM102", "T201", "T203", "TRY003"] -"setup.py" = ["T201", "T203"] +"setup.py" = [ + "B904", # Use ` raise from` to specify exception cause (PYTHON2.7 COMPAT) + "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) + "FLY", # flynt (PYTHON2.7 COMPAT) + "FURB145", # [*] Prefer `copy` method over slicing (PYTHON2.7 COMPAT) + "T201", + "T203", + "UP009", # [*] UTF-8 encoding declaration is unnecessary (PYTHON2.7 COMPAT) + "UP010", # [*] Unnecessary `__future__` import `print_function` (PYTHON2.7 COMPAT) + "UP024", # [*] Replace aliased errors with `OSError` (PYTHON2.7 COMPAT) + "UP025", # [*] Remove unicode literals from strings (PYTHON2.7 COMPAT) + "UP028", # [*] Replace `yield` over `for` loop with `yield from` (PYTHON2.7 COMPAT) + "UP032", # [*] Use f-string instead of `format` call (PYTHON2.7 COMPAT) + "UP036", # Version block is outdated for minimum Python version +] [tool.ruff.lint.isort] # https://beta.ruff.rs/docs/settings/#isort diff --git a/setup.py b/setup.py index 148bfcfdfb..4cf5c7374e 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ NOTE: the syntax of this script MUST be kept compatible with Python 2.7. """ -from __future__ import print_function # noqa: UP010 +from __future__ import print_function import ast import contextlib @@ -28,7 +28,7 @@ import warnings -if sys.version_info[0] == 2: # noqa: UP036 +if sys.version_info[0] == 2: sys.exit(textwrap.dedent("""\ As of version 7.0.0 psutil no longer supports Python 2.7, see: https://github.com/giampaolo/psutil/issues/2480 From c9088a5d1318e57ba85ef8f901be08effc8c957c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 17:23:28 +0100 Subject: [PATCH 1169/1714] use str.format() instead of % / percent format --- psutil/__init__.py | 21 +++++++++------------ psutil/_common.py | 10 +++++----- psutil/_pslinux.py | 2 +- psutil/tests/__init__.py | 14 ++++++++------ psutil/tests/test_process_all.py | 12 +++++++----- psutil/tests/test_system.py | 5 +++-- pyproject.toml | 16 ++++++++++------ scripts/ifconfig.py | 9 +++------ scripts/internal/print_access_denied.py | 4 ++-- scripts/internal/print_dist.py | 2 +- scripts/iotop.py | 2 +- scripts/netstat.py | 4 ++-- scripts/procinfo.py | 2 +- scripts/top.py | 4 ++-- scripts/who.py | 2 +- scripts/winservices.py | 2 +- setup.py | 12 ++++++------ 17 files changed, 63 insertions(+), 60 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 160c813d82..eaed048b31 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -227,19 +227,16 @@ msg = f"version conflict: {_psplatform.cext.__file__!r} C extension " 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__, - ) + v = ".".join([x for x in str(_psplatform.cext.version)]) + msg += f" ({v} instead of {__version__})" else: msg += f" (different than {__version__})" - msg += "; you may try to 'pip uninstall psutil', manually remove %s" % ( - getattr( - _psplatform.cext, - "__file__", - "the existing psutil install directory", - ) + what = getattr( + _psplatform.cext, + "__file__", + "the existing psutil install directory", ) + msg += f"; you may try to 'pip uninstall psutil', manually remove {what}" msg += " or clean the virtual env somehow, then reinstall" raise ImportError(msg) @@ -417,7 +414,7 @@ def __str__(self): if self._create_time is not None: info['started'] = _pprint_secs(self._create_time) - return "%s.%s(%s)" % ( + return "{}.{}({})".format( self.__class__.__module__, self.__class__.__name__, ", ".join([f"{k}={v!r}" for k, v in info.items()]), @@ -559,7 +556,7 @@ def as_dict(self, attrs=None, ad_value=None): attrs = set(attrs) invalid_names = attrs - valid_names if invalid_names: - msg = "invalid attr name%s %s" % ( + msg = "invalid attr name{} {}".format( "s" if len(invalid_names) > 1 else "", ", ".join(map(repr, invalid_names)), ) diff --git a/psutil/_common.py b/psutil/_common.py index 1e8bb7959f..e7c6ba3dd7 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -279,8 +279,8 @@ 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()] + details = "({})".format( + ", ".join([f"{k}={v!r}" for k, v in info.items()]) ) else: details = None @@ -611,9 +611,9 @@ def deprecated_method(replacement): """ def outer(fun): - msg = "%s() is deprecated and will be removed; use %s() instead" % ( - fun.__name__, - replacement, + msg = ( + f"{fun.__name__}() is deprecated and will be removed; use" + f" {replacement}() instead" ) if fun.__doc__ is None: fun.__doc__ = msg diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 531503d55e..ad1e1eff29 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -461,7 +461,7 @@ def virtual_memory(): # Warn about missing metrics which are set to 0. if missing_fields: - msg = "%s memory stats couldn't be determined and %s set to 0" % ( + msg = "{} memory stats couldn't be determined and {} set to 0".format( ", ".join(missing_fields), "was" if len(missing_fields) == 1 else "were", ) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index c852ead0a3..10f4cd877f 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -248,7 +248,9 @@ def attempt(exe): exe = ( attempt(sys.executable) or attempt(os.path.realpath(sys.executable)) - or attempt(shutil.which("python%s.%s" % sys.version_info[:2])) + or attempt( + shutil.which("python{}.{}".format(*sys.version_info[:2])) + ) or attempt(psutil.Process().exe()) ) if not exe: @@ -1212,7 +1214,7 @@ def _check_mem(self, fun, times, retries, tolerance): 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" % ( + msg = "Run #{}: extra-mem={}, per-call={}, calls={}".format( idx, bytes2human(mem), bytes2human(mem / times), @@ -1343,17 +1345,17 @@ def print_sysinfo(): # metrics info['cpus'] = psutil.cpu_count() - info['loadavg'] = "%.1f%%, %.1f%%, %.1f%%" % ( - tuple([x / psutil.cpu_count() * 100 for x in psutil.getloadavg()]) + info['loadavg'] = "{:.1f}%, {:.1f}%, {:.1f}%".format( + *tuple([x / psutil.cpu_count() * 100 for x in psutil.getloadavg()]) ) mem = psutil.virtual_memory() - info['memory'] = "%s%%, used=%s, total=%s" % ( + info['memory'] = "{}%%, used={}, total={}".format( int(mem.percent), bytes2human(mem.used), bytes2human(mem.total), ) swap = psutil.swap_memory() - info['swap'] = "%s%%, used=%s, total=%s" % ( + info['swap'] = "{}%%, used={}, total={}".format( int(swap.percent), bytes2human(swap.used), bytes2human(swap.total), diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 24229979c7..8dd2946c17 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -135,11 +135,13 @@ def test_all(self): 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 += ( + "FAIL: name=test_{}, pid={}, ret={}\ninfo={}\n".format( + name, + info['pid'], + repr(value), + info, + ) ) s += '-' * 70 s += f"\n{traceback.format_exc()}" diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 62d49bf611..391dbbfda3 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -513,8 +513,9 @@ def _test_cpu_percent(self, percent, last_ret, new_ret): assert 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)) + "\n{}\nlast={}\nnew={}".format( + err, pprint.pformat(last_ret), pprint.pformat(new_ret) + ) ) def test_cpu_percent(self): diff --git a/pyproject.toml b/pyproject.toml index d6e87a5bd8..fa5cd07a16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,17 +89,21 @@ ignore = [ "TD", # all TODOs, XXXs, etc. "TRY300", # Consider moving this statement to an `else` block "TRY301", # Abstract `raise` to an inner function - "UP031", # [*] Use format specifiers instead of percent format + "UP032", # [*] Use f-string instead of `format` call ] [tool.ruff.lint.per-file-ignores] # EM101 == raw-string-in-exception -# T201 == print(), T203 == pprint() +# EM102 == f-string-in-exception +# EM103 == dot-format-in-exception +# T201 == print() +# T203 == pprint() # TRY003 == raise-vanilla-args -".github/workflows/*" = ["EM102", "T201", "T203"] -"psutil/tests/*" = ["EM101", "EM102", "TRY003"] -"scripts/*" = ["EM102", "T201", "T203"] -"scripts/internal/*" = ["EM101", "EM102", "T201", "T203", "TRY003"] +# UP031 == Use format specifiers instead of percent format +".github/workflows/*" = ["EM101", "EM102", "EM103", "T201", "T203"] +"psutil/tests/*" = ["EM101", "EM102", "EM103", "TRY003"] +"scripts/*" = ["EM101", "EM102", "EM103", "T201", "T203", "UP031"] +"scripts/internal/*" = ["EM101", "EM102", "EM103", "T201", "T203", "TRY003", "UP031"] "setup.py" = [ "B904", # Use ` raise from` to specify exception cause (PYTHON2.7 COMPAT) "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index dfc5f5ae4e..93a14b9525 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -71,8 +71,7 @@ def main(): st = stats[nic] print(" stats : ", end='') print( - "speed=%sMB, duplex=%s, mtu=%s, up=%s" - % ( + "speed={}MB, duplex={}, mtu={}, up={}".format( st.speed, duplex_map[st.duplex], st.mtu, @@ -83,8 +82,7 @@ def main(): io = io_counters[nic] print(" incoming : ", end='') print( - "bytes=%s, pkts=%s, errs=%s, drops=%s" - % ( + "bytes={}, pkts={}, errs={}, drops={}".format( bytes2human(io.bytes_recv), io.packets_recv, io.errin, @@ -93,8 +91,7 @@ def main(): ) print(" outgoing : ", end='') print( - "bytes=%s, pkts=%s, errs=%s, drops=%s" - % ( + "bytes={}, pkts={}, errs={}, drops={}".format( bytes2human(io.bytes_sent), io.packets_sent, io.errout, diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index dbf96f2d80..4df2e63b4f 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -83,8 +83,8 @@ def main(): tot_perc = round((tot_ads / tot_calls) * 100, 1) print("-" * 50) print( - "Totals: access-denied=%s (%s%%), calls=%s, processes=%s, elapsed=%ss" - % (tot_ads, tot_perc, tot_calls, tot_procs, round(elapsed, 2)) + "Totals: access-denied={} ({}%%), calls={}, processes={}, elapsed={}s" + .format(tot_ads, tot_perc, tot_calls, tot_procs, round(elapsed, 2)) ) diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 5e2a6e5989..decd12c498 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -20,7 +20,7 @@ def __init__(self, path): self._name = os.path.basename(path) def __repr__(self): - return "<%s(name=%s, plat=%s, arch=%s, pyver=%s)>" % ( + return "<{}(name={}, plat={}, arch={}, pyver={})>".format( self.__class__.__name__, self.name, self.platform(), diff --git a/scripts/iotop.py b/scripts/iotop.py index 2d92f4e2d6..f56f828e58 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -118,7 +118,7 @@ def refresh_window(procs, disks_read, disks_write): templ = "%-5s %-7s %11s %11s %s" win.erase() - disks_tot = "Total DISK READ: %s | Total DISK WRITE: %s" % ( + disks_tot = "Total DISK READ: {} | Total DISK WRITE: {}".format( bytes2human(disks_read), bytes2human(disks_write), ) diff --git a/scripts/netstat.py b/scripts/netstat.py index b589009379..912bed9df7 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -51,10 +51,10 @@ def main(): for p in psutil.process_iter(['pid', 'name']): proc_names[p.info['pid']] = p.info['name'] for c in psutil.net_connections(kind='inet'): - laddr = "%s:%s" % (c.laddr) + laddr = "{}:{}".format(*c.laddr) raddr = "" if c.raddr: - raddr = "%s:%s" % (c.raddr) + raddr = "{}:{}".format(*c.raddr) name = proc_names.get(c.pid, '?') or '' line = templ % ( proto_map[(c.family, c.type)], diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 963dd77da0..afdda11b7b 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -172,7 +172,7 @@ def run(pid, verbose=False): print_('started', started) cpu_tot_time = datetime.timedelta(seconds=sum(pinfo['cpu_times'])) - cpu_tot_time = "%s:%s.%s" % ( + cpu_tot_time = "{}:{}.{}".format( cpu_tot_time.seconds // 60 % 60, str(cpu_tot_time.seconds % 60).zfill(2), str(cpu_tot_time.microseconds)[:2], diff --git a/scripts/top.py b/scripts/top.py index 03f235ec36..993f477807 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -168,7 +168,7 @@ def get_dashes(perc): psutil.boot_time() ) av1, av2, av3 = psutil.getloadavg() - line = " Load average: %.2f %.2f %.2f Uptime: %s" % ( + line = " Load average: {:.2f} {:.2f} {:.2f} Uptime: {}".format( av1, av2, av3, @@ -201,7 +201,7 @@ def refresh_window(procs, procs_status): # is expressed as: "mm:ss.ms" if p.dict['cpu_times'] is not None: ctime = datetime.timedelta(seconds=sum(p.dict['cpu_times'])) - ctime = "%s:%s.%s" % ( + ctime = "{}:{}.{}".format( ctime.seconds // 60 % 60, str(ctime.seconds % 60).zfill(2), str(ctime.microseconds)[:2], diff --git a/scripts/who.py b/scripts/who.py index 64a9481075..094c4fd1cc 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -25,7 +25,7 @@ def main(): user.name, user.terminal or '-', datetime.fromtimestamp(user.started).strftime("%Y-%m-%d %H:%M"), - "(%s)" % user.host if user.host else "", + f"({user.host or ''})", proc_name, ) print(line) diff --git a/scripts/winservices.py b/scripts/winservices.py index 6ff240c1d1..7df5894593 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -44,7 +44,7 @@ def main(): for service in psutil.win_service_iter(): info = service.as_dict() print(f"{info['name']!r} ({info['display_name']!r})") - s = "status: %s, start: %s, username: %s, pid: %s" % ( + s = "status: {}, start: {}, username: {}, pid: {}".format( info['status'], info['start_type'], info['username'], diff --git a/setup.py b/setup.py index 4cf5c7374e..6b68d511a0 100755 --- a/setup.py +++ b/setup.py @@ -212,7 +212,7 @@ def missdeps(cmdline): else: s += ". Perhaps Python header files are not installed. " s += "Try running:\n" - s += " %s" % cmdline + s += " {}".format(cmdline) print(hilite(s, color="red", bold=True), file=sys.stderr) @@ -429,7 +429,7 @@ def get_winver(): ) else: - sys.exit('platform %s is not supported' % sys.platform) + sys.exit("platform {} is not supported".format(sys.platform)) if POSIX: @@ -568,13 +568,13 @@ def main(): if LINUX: pyimpl = "pypy" if PYPY else "python" if shutil.which("dpkg"): - missdeps("sudo apt-get install gcc %s3-dev" % (pyimpl)) + missdeps("sudo apt-get install gcc {}3-dev".format(pyimpl)) elif shutil.which("rpm"): - missdeps("sudo yum install gcc %s-devel" % (pyimpl)) + missdeps("sudo yum install gcc {}3-devel".format(pyimpl)) elif shutil.which("apk"): missdeps( - "sudo apk add gcc %s%s-dev musl-dev linux-headers" - % (pyimpl) + "sudo apk add gcc {}3-dev musl-dev linux-headers" + .format(*pyimpl) ) elif MACOS: msg = ( From 508fb5da3b5b7ad740f82cabc2a01d8a64794b28 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 17:45:40 +0100 Subject: [PATCH 1170/1714] use str.format() instead of % / percent format --- pyproject.toml | 5 ++--- scripts/cpu_distribution.py | 6 +++--- scripts/fans.py | 4 +++- scripts/ifconfig.py | 3 ++- scripts/internal/check_broken_links.py | 2 +- scripts/internal/print_api_speed.py | 2 +- scripts/meminfo.py | 2 +- scripts/nettop.py | 3 +-- scripts/procinfo.py | 4 ++-- scripts/sensors.py | 7 ++++--- scripts/temperatures.py | 2 +- scripts/top.py | 8 +++++--- scripts/who.py | 2 +- 13 files changed, 27 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fa5cd07a16..fa63e600f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,11 +99,10 @@ ignore = [ # T201 == print() # T203 == pprint() # TRY003 == raise-vanilla-args -# UP031 == Use format specifiers instead of percent format ".github/workflows/*" = ["EM101", "EM102", "EM103", "T201", "T203"] "psutil/tests/*" = ["EM101", "EM102", "EM103", "TRY003"] -"scripts/*" = ["EM101", "EM102", "EM103", "T201", "T203", "UP031"] -"scripts/internal/*" = ["EM101", "EM102", "EM103", "T201", "T203", "TRY003", "UP031"] +"scripts/*" = ["EM101", "EM102", "EM103", "T201", "T203"] +"scripts/internal/*" = ["EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "setup.py" = [ "B904", # Use ` raise from` to specify exception cause (PYTHON2.7 COMPAT) "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index 85495c0d3a..ecb0b9c0d3 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -72,13 +72,13 @@ def main(): clean_screen() cpus_percent = psutil.cpu_percent(percpu=True) for i in range(num_cpus): - print("CPU %-6i" % i, end="") + print("CPU {:<6}".format(i), end="") if cpus_hidden: print(" (+ hidden)", end="") print() for _ in range(num_cpus): - print("%-10s" % cpus_percent.pop(0), end="") + print("{:<10}".format(cpus_percent.pop(0)), end="") print() # processes @@ -93,7 +93,7 @@ def main(): pname = procs[num].pop() except IndexError: pname = "" - print("%-10s" % pname[:10], end="") + print("{:<10}".format(pname[:10]), end="") print() curr_line += 1 if curr_line >= shutil.get_terminal_size()[1]: diff --git a/scripts/fans.py b/scripts/fans.py index bfab434c49..ca6a453bd8 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -27,7 +27,9 @@ def main(): for name, entries in fans.items(): print(name) for entry in entries: - print(" %-20s %s RPM" % (entry.label or name, entry.current)) + print( + " {:<20} {} RPM".format(entry.label or name, entry.current) + ) print() diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index 93a14b9525..3ab3c013b4 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -99,7 +99,8 @@ def main(): ) ) for addr in addrs: - print(" %-4s" % af_map.get(addr.family, addr.family), end="") + fam = " {:<4}".format(af_map.get(addr.family, addr.family)) + print(fam, end="") print(f" address : {addr.address}") if addr.broadcast: print(f" broadcast : {addr.broadcast}") diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 408886817b..2a51381d33 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -252,7 +252,7 @@ def main(): else: for fail in fails: fname, url = fail - print("%-30s: %s " % (fname, url)) + print("{:<30}: {} ".format(fname, url)) print('-' * 20) print(f"total: {len(fails)} fails!") sys.exit(1) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 673f51c899..5c155f4d3d 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -103,7 +103,7 @@ def print_timings(): def timecall(title, fun, *args, **kw): - print("%-50s" % title, end="") + print("{:<50}".format(title), end="") sys.stdout.flush() t = timer() for n in range(TIMES): diff --git a/scripts/meminfo.py b/scripts/meminfo.py index a13b7e00ba..6bee96998a 100755 --- a/scripts/meminfo.py +++ b/scripts/meminfo.py @@ -38,7 +38,7 @@ def pprint_ntuple(nt): value = getattr(nt, name) if name != 'percent': value = bytes2human(value) - print('%-10s : %7s' % (name.capitalize(), value)) + print('{:<10} : {:>7}'.format(name.capitalize(), value)) def main(): diff --git a/scripts/nettop.py b/scripts/nettop.py index eafcd0f5db..cda16bf07d 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -81,8 +81,7 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): # totals printl( - "total bytes: sent: %-10s received: %s" - % ( + "total bytes: sent: {:<10} received: {}".format( bytes2human(tot_after.bytes_sent), bytes2human(tot_after.bytes_recv), ) diff --git a/scripts/procinfo.py b/scripts/procinfo.py index afdda11b7b..5feb49bd3a 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -120,9 +120,9 @@ def print_(a, b): if sys.stdout.isatty() and psutil.POSIX: - fmt = '\x1b[1;32m%-13s\x1b[0m %s' % (a, b) + fmt = "\x1b[1;32m{:<13}\x1b[0m {}".format(a, b) else: - fmt = '%-11s %s' % (a, b) + fmt = "{:<11} {}".format(a, b) print(fmt) diff --git a/scripts/sensors.py b/scripts/sensors.py index fe1286ba5c..05aec894af 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -59,7 +59,7 @@ def main(): if name in temps: print(" Temperatures:") for entry in temps[name]: - s = " %-20s %s°C (high=%s°C, critical=%s°C)" % ( + s = " {:<20} {}°C (high={}°C, critical={}°C)".format( entry.label or name, entry.current, entry.high, @@ -71,8 +71,9 @@ def main(): print(" Fans:") for entry in fans[name]: print( - " %-20s %s RPM" - % (entry.label or name, entry.current) + " {:<20} {} RPM".format( + entry.label or name, entry.current + ) ) # Battery. diff --git a/scripts/temperatures.py b/scripts/temperatures.py index 2253fef7f3..6bc0787660 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -36,7 +36,7 @@ def main(): for name, entries in temps.items(): print(name) for entry in entries: - line = " %-20s %s °C (high = %s °C, critical = %s °C)" % ( + line = " {:<20} {} °C (high = {} °C, critical = %{} °C)".format( entry.label or name, entry.current, entry.high, diff --git a/scripts/top.py b/scripts/top.py index 993f477807..46fca7553f 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -129,13 +129,15 @@ def get_dashes(perc): percs = psutil.cpu_percent(interval=0, percpu=True) for cpu_num, perc in enumerate(percs): dashes, empty_dashes = get_dashes(perc) - line = " CPU%-2s [%s%s] %5s%%" % (cpu_num, dashes, empty_dashes, perc) + line = " CPU{:<2} [{}{}] {:>5}%".format( + cpu_num, dashes, empty_dashes, perc + ) printl(line, color=get_color(perc)) # memory usage mem = psutil.virtual_memory() dashes, empty_dashes = get_dashes(mem.percent) - line = " Mem [%s%s] %5s%% %6s / %s" % ( + line = " Mem [{}{}] {:>5}% {:>6} / {}".format( dashes, empty_dashes, mem.percent, @@ -147,7 +149,7 @@ def get_dashes(perc): # swap usage swap = psutil.swap_memory() dashes, empty_dashes = get_dashes(swap.percent) - line = " Swap [%s%s] %5s%% %6s / %s" % ( + line = " Swap [{}{}] {:>5}% {:>6} / {}".format( dashes, empty_dashes, swap.percent, diff --git a/scripts/who.py b/scripts/who.py index 094c4fd1cc..d459547234 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -21,7 +21,7 @@ def main(): users = psutil.users() for user in users: proc_name = psutil.Process(user.pid).name() if user.pid else "" - line = "%-12s %-10s %-10s %-14s %s" % ( + line = "{:<12} {:<10} {:<10} {:<14} {}".format( user.name, user.terminal or '-', datetime.fromtimestamp(user.started).strftime("%Y-%m-%d %H:%M"), From 318807fe54087cc274c0ea436d6cd6a7bd1d9100 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 18:00:21 +0100 Subject: [PATCH 1171/1714] remove psutil.test() ps-like function It could not be called anymore via: $ python3 -m psutil /usr/bin/python3: No module named psutil.__main__; 'psutil' is a package and cannot be directly executed ...plus code it's a duplicate of scripts/ps.py. --- psutil/__init__.py | 78 ------------------------------------- psutil/tests/test_system.py | 10 ----- 2 files changed, 88 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index eaed048b31..326ed29c51 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2395,82 +2395,4 @@ def _set_debug(value): _psplatform.cext.set_debug(bool(value)) -def test(): # pragma: no cover - import shutil - - from ._common import bytes2human - - today_day = datetime.date.today() - # 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: - ctime = ctime.strftime("%H:%M") - else: - ctime = ctime.strftime("%b%d") - else: - ctime = '' - 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] - 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, - nice, - status, - ctime, - cputime, - cmdline, - ) - print(line[: shutil.get_terminal_size()[0]]) # NOQA - - del memoize_when_activated - -if __name__ == "__main__": - test() diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 391dbbfda3..b66ff5fd9e 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -32,7 +32,6 @@ from psutil import WINDOWS 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 @@ -261,15 +260,6 @@ def test_users(self): 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", From c3effb7a7a12cbde6977d21b32de718e74dbe2ef Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 20:46:37 +0100 Subject: [PATCH 1172/1714] Test internal scripts (#2484) --- MANIFEST.in | 1 + Makefile | 4 + psutil/tests/test_misc.py | 161 ---------------- psutil/tests/test_scripts.py | 242 +++++++++++++++++++++++++ scripts/internal/bench_oneshot_2.py | 16 +- scripts/internal/git_pre_commit.py | 3 +- scripts/internal/purge_installation.py | 3 +- scripts/internal/winmake.py | 7 + 8 files changed, 267 insertions(+), 170 deletions(-) create mode 100755 psutil/tests/test_scripts.py diff --git a/MANIFEST.in b/MANIFEST.in index 5ec1cdd9e9..b60794b913 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -163,6 +163,7 @@ include psutil/tests/test_osx.py include psutil/tests/test_posix.py include psutil/tests/test_process.py include psutil/tests/test_process_all.py +include psutil/tests/test_scripts.py include psutil/tests/test_sunos.py include psutil/tests/test_system.py include psutil/tests/test_testutils.py diff --git a/Makefile b/Makefile index 2c7d050ce9..3e74e8c7d2 100644 --- a/Makefile +++ b/Makefile @@ -116,6 +116,10 @@ test-misc: ## Run miscellaneous tests. ${MAKE} build $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_misc.py +test-scripts: ## Run scripts tests. + ${MAKE} build + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_scripts.py + test-testutils: ## Run test utils tests. ${MAKE} build $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_testutils.py diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 9d24bb32c1..1c42b6022d 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -6,7 +6,6 @@ """Miscellaneous tests.""" -import ast import collections import contextlib import io @@ -14,13 +13,11 @@ import os import pickle import socket -import stat import sys from unittest import mock import psutil import psutil.tests -from psutil import POSIX from psutil import WINDOWS from psutil._common import bcat from psutil._common import cat @@ -31,22 +28,12 @@ from psutil._common import parse_environ_block from psutil._common import supports_ipv6 from psutil._common import wrap_numbers -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 QEMU_USER -from psutil.tests import SCRIPTS_DIR from psutil.tests import PsutilTestCase from psutil.tests import process_namespace from psutil.tests import pytest from psutil.tests import reload_module -from psutil.tests import sh from psutil.tests import system_namespace @@ -334,14 +321,6 @@ def check(ret): assert b.pid == 4567 assert b.name == 'name' - # def test_setup_script(self): - # setup_py = os.path.join(ROOT_DIR, 'setup.py') - # if CI_TESTING and not os.path.exists(setup_py): - # raise pytest.skip("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. @@ -896,143 +875,3 @@ def test_cache_clear_public_apis(self): psutil.net_io_counters.cache_clear() caches = wrap_numbers.cache_info() assert caches == ({}, {}, {}) - - -# =================================================================== -# --- Example script tests -# =================================================================== - - -@pytest.mark.skipif( - not os.path.exists(SCRIPTS_DIR), reason="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 = 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") 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" - f" {os.path.join(SCRIPTS_DIR, name)!r} script" - ) - - @pytest.mark.skipif(not POSIX, reason="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(f"{path!r} is not executable") - - 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())) - - @pytest.mark.skipif(CI_TESTING and not psutil.users(), reason="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') - - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") - def test_ifconfig(self): - self.assert_stdout('ifconfig.py') - - @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="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 pytest.skip("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()) - assert str(os.getpid()) in output - - @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") - def test_winservices(self): - self.assert_stdout('winservices.py') - - def test_cpu_distribution(self): - self.assert_syntax('cpu_distribution.py') - - @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") - def test_temperatures(self): - if not psutil.sensors_temperatures(): - raise pytest.skip("no temperatures") - self.assert_stdout('temperatures.py') - - @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") - def test_fans(self): - if not psutil.sensors_fans(): - raise pytest.skip("no fans") - self.assert_stdout('fans.py') - - @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") - @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") - def test_battery(self): - self.assert_stdout('battery.py') - - @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") - @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") - def test_sensors(self): - self.assert_stdout('sensors.py') diff --git a/psutil/tests/test_scripts.py b/psutil/tests/test_scripts.py new file mode 100755 index 0000000000..c354d2ad3b --- /dev/null +++ b/psutil/tests/test_scripts.py @@ -0,0 +1,242 @@ +#!/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. + +"""Test various scripts.""" + +import ast +import os +import shutil +import stat +import subprocess + +import pytest + +from psutil import POSIX +from psutil import WINDOWS +from psutil.tests import CI_TESTING +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_MEMORY_MAPS +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 QEMU_USER +from psutil.tests import ROOT_DIR +from psutil.tests import SCRIPTS_DIR +from psutil.tests import PsutilTestCase +from psutil.tests import import_module_by_path +from psutil.tests import psutil +from psutil.tests import sh + + +INTERNAL_SCRIPTS_DIR = os.path.join(SCRIPTS_DIR, "internal") +SETUP_PY = os.path.join(ROOT_DIR, 'setup.py') + + +# =================================================================== +# --- Tests scripts in scripts/ directory +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(SCRIPTS_DIR), + reason="can't find scripts/ directory", +) +class TestExampleScripts(PsutilTestCase): + @staticmethod + def assert_stdout(exe, *args, **kwargs): + kwargs.setdefault("env", PYTHON_EXE_ENV) + exe = 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") 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" + f" {os.path.join(SCRIPTS_DIR, name)!r} script" + ) + + @pytest.mark.skipif(not POSIX, reason="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(f"{path!r} is not executable") + + 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())) + + @pytest.mark.skipif(CI_TESTING and not psutil.users(), reason="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') + + @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") + def test_ifconfig(self): + self.assert_stdout('ifconfig.py') + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="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 pytest.skip("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()) + assert str(os.getpid()) in output + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_winservices(self): + self.assert_stdout('winservices.py') + + def test_cpu_distribution(self): + self.assert_syntax('cpu_distribution.py') + + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_temperatures(self): + if not psutil.sensors_temperatures(): + raise pytest.skip("no temperatures") + self.assert_stdout('temperatures.py') + + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_fans(self): + if not psutil.sensors_fans(): + raise pytest.skip("no fans") + self.assert_stdout('fans.py') + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_battery(self): + self.assert_stdout('battery.py') + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors(self): + self.assert_stdout('sensors.py') + + +# =================================================================== +# --- Tests scripts in scripts/internal/ directory +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(INTERNAL_SCRIPTS_DIR), + reason="can't find scripts/internal/ directory", +) +class TestInternalScripts(PsutilTestCase): + @staticmethod + def ls(): + for name in os.listdir(INTERNAL_SCRIPTS_DIR): + if name.endswith(".py"): + yield os.path.join(INTERNAL_SCRIPTS_DIR, name) + + def test_syntax_all(self): + for path in self.ls(): + with open(path, encoding="utf8") as f: + data = f.read() + ast.parse(data) + + @pytest.mark.skipif(CI_TESTING, reason="not on CI") + def test_import_all(self): + for path in self.ls(): + try: + import_module_by_path(path) + except SystemExit: + pass + + +# =================================================================== +# --- Tests for setup.py script +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(SETUP_PY), reason="can't find setup.py" +) +class TestSetupScript(PsutilTestCase): + def test_invocation(self): + module = import_module_by_path(SETUP_PY) + with pytest.raises(SystemExit): + module.setup() + assert module.get_version() == psutil.__version__ + + @pytest.mark.skipif( + not shutil.which("python2.7"), reason="python2.7 not installed" + ) + def test_python2(self): + # There's a duplicate of this test in scripts/internal + # directory, which is only executed by CI. We replicate it here + # to run it when developing locally. + p = subprocess.Popen( + [shutil.which("python2.7"), SETUP_PY], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = p.communicate() + assert p.wait() == 1 + assert not stdout + assert "psutil no longer supports Python 2.7" in stderr + assert "Latest version supporting Python 2.7 is" in stderr diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index 1076dffc8b..aa3ca78d11 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -11,27 +11,27 @@ import sys import pyperf # requires "pip install pyperf" -from bench_oneshot import names import psutil p = psutil.Process() -funs = [getattr(p, n) for n in names] -def call_normal(): +def call_normal(funs): for fun in funs: fun() -def call_oneshot(): +def call_oneshot(funs): with p.oneshot(): for fun in funs: fun() def main(): + from bench_oneshot import names + runner = pyperf.Runner() args = runner.parse_args() @@ -43,8 +43,10 @@ def main(): for name in sorted(names): print(" " + name) - runner.bench_func("normal", call_normal) - runner.bench_func("oneshot", call_oneshot) + funs = [getattr(p, n) for n in names] + runner.bench_func("normal", call_normal, funs) + runner.bench_func("oneshot", call_oneshot, funs) -main() +if __name__ == "__main__": + main() diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 10f6368da3..a47d7987ee 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -166,4 +166,5 @@ def main(): ) -main() +if __name__ == "__main__": + main() diff --git a/scripts/internal/purge_installation.py b/scripts/internal/purge_installation.py index 55b2f5c504..254adabe0e 100755 --- a/scripts/internal/purge_installation.py +++ b/scripts/internal/purge_installation.py @@ -38,4 +38,5 @@ def main(): rmpath(abspath) -main() +if __name__ == "__main__": + main() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 5413b827b6..1692c12534 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -366,6 +366,12 @@ def test_misc(): sh([PYTHON, "psutil\\tests\\test_misc.py"]) +def test_scripts(): + """Run scripts tests.""" + build() + sh([PYTHON, "psutil\\tests\\test_scripts.py"]) + + def test_unicode(): """Run unicode tests.""" build() @@ -502,6 +508,7 @@ def parse_args(): ) sp.add_parser('test-memleaks', help="run memory leaks tests") sp.add_parser('test-misc', help="run misc tests") + sp.add_parser('test-scripts', help="run scripts tests") sp.add_parser('test-platform', help="run windows only tests") sp.add_parser('test-process', help="run process tests") sp.add_parser('test-process-all', help="run process all tests") From 304f08f8b270b59138bad09fdb20f01160870bfd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Dec 2024 23:23:34 +0100 Subject: [PATCH 1173/1714] Use str.format() instead of % (#2485) --- psutil/_pslinux.py | 4 +-- psutil/tests/__init__.py | 2 +- psutil/tests/test_linux.py | 2 +- psutil/tests/test_system.py | 9 +++--- scripts/disk_usage.py | 10 ++++-- scripts/free.py | 10 +++--- scripts/internal/print_access_denied.py | 6 ++-- scripts/internal/print_api_speed.py | 6 ++-- scripts/internal/print_dist.py | 6 ++-- scripts/internal/print_downloads.py | 6 ++-- scripts/iotop.py | 6 ++-- scripts/netstat.py | 6 ++-- scripts/nettop.py | 12 ++++---- scripts/pmap.py | 8 ++--- scripts/procinfo.py | 41 +++++++++++++------------ scripts/procsmem.py | 6 ++-- scripts/ps.py | 8 ++--- scripts/sensors.py | 5 +-- scripts/top.py | 6 ++-- setup.py | 2 +- 20 files changed, 85 insertions(+), 76 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ad1e1eff29..81ec88bdaf 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -788,7 +788,7 @@ def get_proc_inodes(self, pid): inode = readlink(f"{self._procfs_path}/{pid}/fd/{fd}") except (FileNotFoundError, ProcessLookupError): # ENOENT == file which is gone in the meantime; - # os.stat('/proc/%s' % self.pid) will be done later + # os.stat(f"/proc/{self.pid}") will be done later # to force NSP (if it's the case) continue except OSError as err: @@ -2113,7 +2113,7 @@ def threads(self): @wrap_exceptions def nice_get(self): - # with open_text('%s/%s/stat' % (self._procfs_path, self.pid)) as f: + # with open_text(f"{self._procfs_path}/{self.pid}/stat") as f: # data = f.read() # return int(data.split()[18]) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 10f4cd877f..54f468f51a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1367,7 +1367,7 @@ def print_sysinfo(): print("=" * 70, file=sys.stderr) # NOQA for k, v in info.items(): - print("%-17s %s" % (k + ':', v), file=sys.stderr) # NOQA + print("{:<17} {}".format(k + ":", v), file=sys.stderr) # noqa: T201 print("=" * 70, file=sys.stderr) # NOQA sys.stdout.flush() diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 945e0f5353..741a152376 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -976,7 +976,7 @@ def test_ips(self): # found += 1 # name = line.split(':')[1].strip() # self.assertIn(name, nics) - # self.assertEqual(len(nics), found, msg="%s\n---\n%s" % ( + # self.assertEqual(len(nics), found, msg="{}\n---\n{}".format( # pprint.pformat(nics), out)) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index b66ff5fd9e..723aaa3379 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -416,8 +416,9 @@ def test_cpu_times(self): # 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)) + # self.assertGreaterEqual( + # new_t, last_t, + # msg="{} {}".format(new_t, last_t)) # last = new def test_cpu_times_time_increases(self): @@ -461,7 +462,7 @@ def test_per_cpu_times(self): # new_t = getattr(newcpu, field) # last_t = getattr(lastcpu, field) # self.assertGreaterEqual( - # new_t, last_t, msg="%s %s" % (lastcpu, newcpu)) + # new_t, last_t, msg="{} {}".format(lastcpu, newcpu)) # last = new def test_per_cpu_times_2(self): @@ -493,7 +494,7 @@ def test_cpu_times_comparison(self): with self.subTest(field=field, base=base, per_cpu=per_cpu): assert ( abs(getattr(base, field) - getattr(summed_values, field)) - < 1 + < 1.5 ) def _test_cpu_percent(self, percent, last_ret, new_ret): diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index c75c4f61d6..be391e28ab 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -23,8 +23,12 @@ def main(): - templ = "%-17s %8s %8s %8s %5s%% %9s %s" - print(templ % ("Device", "Total", "Used", "Free", "Use ", "Type", "Mount")) + templ = "{:<17} {:>8} {:>8} {:>8} {:>5}% {:>9} {}" + print( + templ.format( + "Device", "Total", "Used", "Free", "Use ", "Type", "Mount" + ) + ) for part in psutil.disk_partitions(all=False): if os.name == 'nt': if 'cdrom' in part.opts or not part.fstype: @@ -33,7 +37,7 @@ def main(): # partition or just hang. continue usage = psutil.disk_usage(part.mountpoint) - line = templ % ( + line = templ.format( part.device, bytes2human(usage.total), bytes2human(usage.used), diff --git a/scripts/free.py b/scripts/free.py index 332507284c..c891b6fc92 100755 --- a/scripts/free.py +++ b/scripts/free.py @@ -18,9 +18,11 @@ def main(): virt = psutil.virtual_memory() swap = psutil.swap_memory() - templ = "%-7s %10s %10s %10s %10s %10s %10s" - print(templ % ('', 'total', 'used', 'free', 'shared', 'buffers', 'cache')) - sect = templ % ( + templ = "{:<7} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}" + print( + templ.format("", "total", "used", "free", "shared", "buffers", "cache") + ) + sect = templ.format( 'Mem:', int(virt.total / 1024), int(virt.used / 1024), @@ -30,7 +32,7 @@ def main(): int(getattr(virt, 'cached', 0) / 1024), ) print(sect) - sect = templ % ( + sect = templ.format( 'Swap:', int(swap.total / 1024), int(swap.used / 1024), diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 4df2e63b4f..b92e9bfc24 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -72,13 +72,13 @@ def main(): elapsed = time.time() - start # print - templ = "%-20s %-5s %-9s %s" - s = templ % ("API", "AD", "Percent", "Outcome") + templ = "{:<20} {:<5} {:<9} {}" + s = templ.format("API", "AD", "Percent", "Outcome") print_color(s, color=None, bold=True) for methname, ads in sorted(d.items(), key=lambda x: (x[1], x[0])): perc = (ads / tot_procs) * 100 outcome = "SUCCESS" if not ads else "ACCESS DENIED" - s = templ % (methname, ads, f"{perc:6.1f}%", outcome) + s = templ.format(methname, ads, f"{perc:6.1f}%", outcome) print_color(s, "red" if ads else None) tot_perc = round((tot_ads / tot_calls) * 100, 1) print("-" * 50) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 5c155f4d3d..e421d83d8c 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -81,11 +81,11 @@ TIMES = 300 timings = [] -templ = "%-25s %10s %10s" +templ = "{:<25} {:>10} {:>10}" def print_header(what): - s = templ % (what, "NUM CALLS", "SECONDS") + s = templ.format(what, "NUM CALLS", "SECONDS") print_color(s, color=None, bold=True) print("-" * len(s)) @@ -95,7 +95,7 @@ def print_timings(): i = 0 while timings[:]: title, times, elapsed = timings.pop(0) - s = templ % (title, str(times), f"{elapsed:.5f}") + s = templ.format(title, str(times), f"{elapsed:.5f}") if i > len(timings) - 5: print_color(s, color="red") else: diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index decd12c498..3ff399e22c 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -111,15 +111,15 @@ def main(): tot_files = 0 tot_size = 0 - templ = "%-120s %7s %8s %7s" + templ = "{:<120} {:>7} {:>8} {:>7}" for platf, pkgs in groups.items(): ppn = f"{platf} ({len(pkgs)})" - s = templ % (ppn, "size", "arch", "pyver") + s = templ.format(ppn, "size", "arch", "pyver") print_color('\n' + s, color=None, bold=True) for pkg in sorted(pkgs, key=lambda x: x.name): tot_files += 1 tot_size += pkg.size() - s = templ % ( + s = templ.format( " " + pkg.name, bytes2human(pkg.size()), pkg.arch(), diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 70afd4b831..d8be58a28f 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -111,18 +111,18 @@ def downloads_by_distro(): # --- print -templ = "| %-30s | %15s |" +templ = "| {:<30} | {:>15} |" def print_row(left, right): if isinstance(right, int): right = f"{right:,}" - print(templ % (left, right)) + print(templ.format(left, right)) def print_header(left, right="Downloads"): print_row(left, right) - s = templ % ("-" * 30, "-" * 15) + s = templ.format("-" * 30, "-" * 15) print("|:" + s[2:-2] + ":|") diff --git a/scripts/iotop.py b/scripts/iotop.py index f56f828e58..93f46d157d 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -115,7 +115,7 @@ def poll(interval): def refresh_window(procs, disks_read, disks_write): """Print results on screen by using curses.""" curses.endwin() - templ = "%-5s %-7s %11s %11s %s" + templ = "{:<5} {:<7} {:>11} {:>11} {}" win.erase() disks_tot = "Total DISK READ: {} | Total DISK WRITE: {}".format( @@ -124,11 +124,11 @@ def refresh_window(procs, disks_read, disks_write): ) printl(disks_tot) - header = templ % ("PID", "USER", "DISK READ", "DISK WRITE", "COMMAND") + header = templ.format("PID", "USER", "DISK READ", "DISK WRITE", "COMMAND") printl(header, highlight=True) for p in procs: - line = templ % ( + line = templ.format( p.pid, p._username[:7], bytes2human(p._read_per_sec), diff --git a/scripts/netstat.py b/scripts/netstat.py index 912bed9df7..56924e6ae8 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -37,8 +37,8 @@ def main(): - templ = "%-5s %-30s %-30s %-13s %-6s %s" - header = templ % ( + templ = "{:<5} {:<30} {:<30} {:<13} {:<6} {}" + header = templ.format( "Proto", "Local address", "Remote address", @@ -56,7 +56,7 @@ def main(): if c.raddr: raddr = "{}:{}".format(*c.raddr) name = proc_names.get(c.pid, '?') or '' - line = templ % ( + line = templ.format( proto_map[(c.family, c.type)], laddr, raddr or AD, diff --git a/scripts/nettop.py b/scripts/nettop.py index cda16bf07d..222771edc0 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -95,27 +95,27 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): for name in nic_names: stats_before = pnic_before[name] stats_after = pnic_after[name] - templ = "%-15s %15s %15s" + templ = "{:<15s} {:>15} {:>15}" # fmt: off - printl(templ % (name, "TOTAL", "PER-SEC"), highlight=True) - printl(templ % ( + printl(templ.format(name, "TOTAL", "PER-SEC"), highlight=True) + printl(templ.format( "bytes-sent", bytes2human(stats_after.bytes_sent), bytes2human( stats_after.bytes_sent - stats_before.bytes_sent) + '/s', )) - printl(templ % ( + printl(templ.format( "bytes-recv", bytes2human(stats_after.bytes_recv), bytes2human( stats_after.bytes_recv - stats_before.bytes_recv) + '/s', )) - printl(templ % ( + printl(templ.format( "pkts-sent", stats_after.packets_sent, stats_after.packets_sent - stats_before.packets_sent, )) - printl(templ % ( + printl(templ.format( "pkts-recv", stats_after.packets_recv, stats_after.packets_recv - stats_before.packets_recv, diff --git a/scripts/pmap.py b/scripts/pmap.py index ae633a41b3..54d53bebd7 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -47,12 +47,12 @@ def main(): if len(sys.argv) != 2: sys.exit('usage: pmap ') p = psutil.Process(int(sys.argv[1])) - templ = "%-20s %10s %-7s %s" - print(templ % ("Address", "RSS", "Mode", "Mapping")) + templ = "{:<20} {:>10} {:<7} {}" + print(templ.format("Address", "RSS", "Mode", "Mapping")) total_rss = 0 for m in p.memory_maps(grouped=False): total_rss += m.rss - line = templ % ( + line = templ.format( m.addr.split('-')[0].zfill(16), bytes2human(m.rss), m.perms, @@ -60,7 +60,7 @@ def main(): ) safe_print(line) print("-" * 31) - print(templ % ("Total", bytes2human(total_rss), '', '')) + print(templ.format("Total", bytes2human(total_rss), "", "")) safe_print(f"PID = {p.pid}, name = {p.name()}") diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 5feb49bd3a..4a328d5062 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -221,13 +221,13 @@ def run(pid, verbose=False): if 'num_ctx_switches' in pinfo: print_("ctx-switches", str_ntuple(pinfo['num_ctx_switches'])) if pinfo['children']: - template = "%-6s %s" - print_("children", template % ("PID", "NAME")) + template = "{:<6} {}" + print_("children", template.format("PID", "NAME")) for child in pinfo['children']: try: - print_('', template % (child.pid, child.name())) + print_("", template.format(child.pid, child.name())) except psutil.AccessDenied: - print_('', template % (child.pid, "")) + print_("", template.format(child.pid, "")) except psutil.NoSuchProcess: pass @@ -242,10 +242,10 @@ def run(pid, verbose=False): print_('open-files', '') if pinfo['net_connections']: - template = '%-5s %-25s %-25s %s' + template = "{:<5} {:<25} {:<25} {}" print_( 'connections', - template % ('PROTO', 'LOCAL ADDR', 'REMOTE ADDR', 'STATUS'), + template.format("PROTO", "LOCAL ADDR", "REMOTE ADDR", "STATUS"), ) for conn in pinfo['net_connections']: if conn.type == socket.SOCK_STREAM: @@ -259,7 +259,7 @@ def run(pid, verbose=False): rip, rport = '*', '*' else: rip, rport = conn.raddr - line = template % ( + line = template.format( type, f"{lip}:{lport}", f"{rip}:{rport}", @@ -270,13 +270,13 @@ def run(pid, verbose=False): print_('connections', '') if pinfo['threads'] and len(pinfo['threads']) > 1: - template = "%-5s %12s %12s" - print_('threads', template % ("TID", "USER", "SYSTEM")) + template = "{:<5} {:>12} {:>12}" + print_("threads", template.format("TID", "USER", "SYSTEM")) for i, thread in enumerate(pinfo['threads']): if not verbose and i >= NON_VERBOSE_ITERATIONS: print_("", "[...]") break - print_('', template % thread) + print_("", template.format(*thread)) print_('', f"total={len(pinfo['threads'])}") else: print_('threads', '') @@ -292,8 +292,8 @@ def run(pid, verbose=False): else: resources.append((res_name, soft, hard)) if resources: - template = "%-12s %15s %15s" - print_("res-limits", template % ("RLIMIT", "SOFT", "HARD")) + template = "{:<12} {:>15} {:>15}" + print_("res-limits", template.format("RLIMIT", "SOFT", "HARD")) for res_name, soft, hard in resources: if soft == psutil.RLIM_INFINITY: soft = "infinity" @@ -301,28 +301,29 @@ def run(pid, verbose=False): hard = "infinity" print_( '', - template - % (RLIMITS_MAP.get(res_name, res_name), soft, hard), + template.format( + RLIMITS_MAP.get(res_name, res_name), soft, hard + ), ) if hasattr(proc, "environ") and pinfo['environ']: - template = "%-25s %s" - print_("environ", template % ("NAME", "VALUE")) + template = "{:<25} {}" + print_("environ", template.format("NAME", "VALUE")) for i, k in enumerate(sorted(pinfo['environ'])): if not verbose and i >= NON_VERBOSE_ITERATIONS: print_("", "[...]") break - print_("", template % (k, pinfo['environ'][k])) + print_("", template.format(k, pinfo["environ"][k])) if pinfo.get('memory_maps', None): - template = "%-8s %s" - print_("mem-maps", template % ("RSS", "PATH")) + template = "{:<8} {}" + print_("mem-maps", template.format("RSS", "PATH")) maps = sorted(pinfo['memory_maps'], key=lambda x: x.rss, reverse=True) for i, region in enumerate(maps): if not verbose and i >= NON_VERBOSE_ITERATIONS: print_("", "[...]") break - print_("", template % (bytes2human(region.rss), region.path)) + print_("", template.format(bytes2human(region.rss), region.path)) def main(): diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 17eee8fb0f..bf58f203da 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -80,12 +80,12 @@ def main(): procs.append(p) procs.sort(key=lambda p: p._uss) - templ = "%-7s %-7s %7s %7s %7s %7s %7s" - print(templ % ("PID", "User", "USS", "PSS", "Swap", "RSS", "Cmdline")) + templ = "{:<7} {:<7} {:>7} {:>7} {:>7} {:>7} {:>7}" + print(templ.format("PID", "User", "USS", "PSS", "Swap", "RSS", "Cmdline")) print("=" * 78) for p in procs[:86]: cmd = " ".join(p._info["cmdline"])[:50] if p._info["cmdline"] else "" - line = templ % ( + line = templ.format( p.pid, p._info["username"][:7] if p._info["username"] else "", convert_bytes(p._uss), diff --git a/scripts/ps.py b/scripts/ps.py index 8478bbd1a8..585eca2f46 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -42,11 +42,11 @@ def main(): today_day = datetime.date.today() # fmt: off - templ = "%-10s %5s %5s %7s %7s %5s %6s %6s %6s %s" + templ = "{:<10} {:>5} {:>5} {:>7} {:>7} {:>5} {:>6} {:>6} {:>6} {}" attrs = ['pid', 'memory_percent', 'name', 'cmdline', 'cpu_times', 'create_time', 'memory_info', 'status', 'nice', 'username'] - print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "NICE", - "STATUS", "START", "TIME", "CMDLINE")) + print(templ.format("USER", "PID", "%MEM", "VSZ", "RSS", "NICE", + "STATUS", "START", "TIME", "CMDLINE")) # fmt: on for p in psutil.process_iter(attrs, ad_value=None): if p.info['create_time']: @@ -97,7 +97,7 @@ def main(): cmdline = p.info['name'] status = p.info['status'][:5] if p.info['status'] else '' - line = templ % ( + line = templ.format( user, p.info['pid'], memp, diff --git a/scripts/sensors.py b/scripts/sensors.py index 05aec894af..ba7a9edca0 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -82,8 +82,9 @@ def main(): print(f" charge: {round(battery.percent, 2)}%") if battery.power_plugged: print( - " status: %s" - % ("charging" if battery.percent < 100 else "fully charged") + " status: {}".format( + "charging" if battery.percent < 100 else "fully charged" + ) ) print(" plugged in: yes") else: diff --git a/scripts/top.py b/scripts/top.py index 46fca7553f..f772bdf00b 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -182,9 +182,9 @@ def get_dashes(perc): def refresh_window(procs, procs_status): """Print results on screen by using curses.""" curses.endwin() - templ = "%-6s %-8s %4s %6s %6s %5s %5s %9s %2s" + templ = "{:<6} {:<8} {:>4} {:>6} {:>6} {:>5} {:>5} {:>9} {:>2}" win.erase() - header = templ % ( + header = templ.format( "PID", "USER", "NI", @@ -217,7 +217,7 @@ def refresh_window(procs, procs_status): if p.dict['cpu_percent'] is None: p.dict['cpu_percent'] = '' username = p.dict['username'][:8] if p.dict['username'] else '' - line = templ % ( + line = templ.format( p.pid, username, p.dict['nice'], diff --git a/setup.py b/setup.py index 6b68d511a0..864c25461e 100755 --- a/setup.py +++ b/setup.py @@ -247,7 +247,7 @@ def unix_can_compile(c_code): def get_winver(): maj, min = sys.getwindowsversion()[0:2] - return '0x0%s' % ((maj * 100) + min) + return "0x0{}".format((maj * 100) + min) if sys.getwindowsversion()[0] < 6: msg = "this Windows version is too old (< Windows Vista); " From 7890ad2917e471748dae4346312a6da9ed5f7e8f Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Sat, 21 Dec 2024 10:10:28 +0100 Subject: [PATCH 1174/1714] fix: python_requires>=3.6 (#2486) --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 864c25461e..03077624fd 100755 --- a/setup.py +++ b/setup.py @@ -546,9 +546,7 @@ def main(): "test": TEST_DEPS, } kwargs.update( - python_requires=( - "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - ), + python_requires=">=3.6", extras_require=extras_require, zip_safe=False, ) From 7403704d4cb05c6d477d908bc8e7cb52fd62ff9b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Dec 2024 15:04:18 +0100 Subject: [PATCH 1175/1714] Improved exceptions (`raise from ...`) (#2487) As part of the dropping of Python 2.7 support (#2480), we can now take advantage of chained exceptions machinery (`raise x from y` and `raise x from None`). In practical terms, this is what changes: # Shorter tracebacks When adding the full traceback info adds no value, we now shorten tracebacks if `raise X from None`. A similar (hackish) attempt was made in https://github.com/giampaolo/psutil/commit/633d8019, when we were still stuck with Python 2. The notable example is passing a PID that does not exist to the `Process` class: ```python psutil.Process(333) ``` Before we got: ``` Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1647, in wrapper return fun(self, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_common.py", line 464, in wrapper raise err from None File "/home/giampaolo/svn/psutil/psutil/_common.py", line 462, in wrapper return fun(self) ^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1713, in _parse_stat_file data = bcat(f"{self._procfs_path}/{self.pid}/stat") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_common.py", line 794, in bcat return cat(fname, fallback=fallback, _open=open_binary) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_common.py", line 782, in cat with _open(fname) as f: ^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_common.py", line 746, in open_binary return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: '/proc/341244/stat' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 350, in _init self._ident = self._get_ident() ^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 391, in _get_ident return (self.pid, self.create_time()) ^^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 773, in create_time self._create_time = self._proc.create_time() ^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1647, in wrapper return fun(self, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1885, in create_time ctime = float(self._parse_stat_file()['create_time']) ^^^^^^^^^^^^^^^^^^^^^^^ File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1659, in wrapper raise NoSuchProcess(pid, name) from err psutil.NoSuchProcess: process no longer exists (pid=341244) During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/giampaolo/svn/psutil/foo.py", line 5, in psutil.Process(341244) File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 317, in __init__ self._init(pid) File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 363, in _init raise NoSuchProcess(pid, msg=msg) psutil.NoSuchProcess: process PID not found (pid=341244) ``` Now we get: ``` Traceback (most recent call last): File "/home/giampaolo/svn/psutil/foo.py", line 5, in psutil.Process(341244) File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 317, in __init__ self._init(pid) File "/home/giampaolo/svn/psutil/psutil/__init__.py", line 363, in _init raise NoSuchProcess(pid, msg=msg) from None psutil.NoSuchProcess: process PID not found (pid=341244) ``` # Different wording for "translated" exceptions By "translated" I mean psutil's `NoSuchProcess`, `ZombieProcess` and `AccessDenied`. Given the following code: ```python import psutil from psutil.tests import spawn_testproc sproc = spawn_testproc() p = psutil.Process(sproc.pid) p.terminate() p.wait() p.name() ``` Before we got: ``` Traceback (most recent call last): [...] File "/home/giampaolo/svn/psutil/psutil/_common.py", line 746, in open_binary return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: '/proc/105496/stat' During handling of the above exception, another exception occurred: Traceback (most recent call last): [...] File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1659, in wrapper raise NoSuchProcess(pid, name) psutil.NoSuchProcess: process no longer exists (pid=105496) ``` Now we get: ``` Traceback (most recent call last): [...] File "/home/giampaolo/svn/psutil/psutil/_common.py", line 746, in open_binary return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: '/proc/105496/stat' The above exception was the direct cause of the following exception: Traceback (most recent call last): [...] File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1659, in wrapper raise NoSuchProcess(pid, name) from None psutil.NoSuchProcess: process no longer exists (pid=105496) ``` Diff: ```diff FileNotFoundError: [Errno 2] No such file or directory: '/proc/105496/stat' - During handling of the above exception, another exception occurred: + The above exception was the direct cause of the following exception: Traceback (most recent call last): [...] File "/home/giampaolo/svn/psutil/psutil/_pslinux.py", line 1659, in wrapper - raise NoSuchProcess(pid, name) + raise NoSuchProcess(pid, name) from None psutil.NoSuchProcess: process no longer exists (pid=105496) ``` --- psutil/__init__.py | 33 +++++++++++++++-------------- psutil/_common.py | 4 ++-- psutil/_psaix.py | 17 +++++++-------- psutil/_psbsd.py | 40 ++++++++++++++++++------------------ psutil/_pslinux.py | 32 ++++++++++++++--------------- psutil/_psosx.py | 13 ++++++------ psutil/_pssunos.py | 19 +++++++++-------- psutil/_pswindows.py | 24 +++++++++++----------- psutil/tests/test_process.py | 17 +++++++++++++++ pyproject.toml | 10 ++++----- 10 files changed, 114 insertions(+), 95 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 326ed29c51..0423473587 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -325,9 +325,9 @@ def _init(self, pid, _ignore_nsp=False): raise ValueError(msg) try: _psplatform.cext.check_pid_range(pid) - except OverflowError: - msg = f"process PID out of range (got {pid})" - raise NoSuchProcess(pid, msg=msg) + except OverflowError as err: + msg = "process PID out of range" + raise NoSuchProcess(pid, msg=msg) from err self._pid = pid self._name = None @@ -360,7 +360,7 @@ def _init(self, pid, _ignore_nsp=False): except NoSuchProcess: if not _ignore_nsp: msg = "process PID not found" - raise NoSuchProcess(pid, msg=msg) + raise NoSuchProcess(pid, msg=msg) from None else: self._gone = True @@ -1250,7 +1250,9 @@ def connections(self, kind="inet"): def _send_signal(self, sig): assert not self.pid < 0, self.pid self._raise_if_pid_reused() - if self.pid == 0: + + pid, ppid, name = self.pid, self._ppid, self._name + if pid == 0: # see "man 2 kill" msg = ( "preventing sending signal to process with PID 0 as it " @@ -1259,17 +1261,17 @@ def _send_signal(self, sig): ) raise ValueError(msg) try: - os.kill(self.pid, sig) - except ProcessLookupError: - if OPENBSD and pid_exists(self.pid): + os.kill(pid, sig) + except ProcessLookupError as err: + if OPENBSD and pid_exists(pid): # We do this because os.kill() lies in case of # zombie processes. - raise ZombieProcess(self.pid, self._name, self._ppid) + raise ZombieProcess(pid, name, ppid) from err else: self._gone = True - raise NoSuchProcess(self.pid, self._name) - except PermissionError: - raise AccessDenied(self.pid, self._name) + raise NoSuchProcess(pid, name) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err def send_signal(self, sig): """Send a signal *sig* to process pre-emptively checking @@ -1436,11 +1438,8 @@ def __getattribute__(self, name): try: return object.__getattribute__(self.__subproc, name) except AttributeError: - msg = ( - f"{self.__class__.__name__} instance has no attribute" - f" '{name}'" - ) - raise AttributeError(msg) + msg = f"{self.__class__!r} has no attribute {name!r}" + raise AttributeError(msg) from None def wait(self, timeout=None): if self.__subproc.returncode is not None: diff --git a/psutil/_common.py b/psutil/_common.py index e7c6ba3dd7..a08ce087a6 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -864,7 +864,7 @@ def hilite(s, color=None, bold=False): # pragma: no cover color = colors[color] except KeyError: msg = f"invalid color {color!r}; choose amongst {list(colors.keys())}" - raise ValueError(msg) + raise ValueError(msg) from None attr.append(color) if bold: attr.append('1') @@ -897,7 +897,7 @@ def print_color( f"invalid color {color!r}; choose between" f" {list(colors.keys())!r}" ) - raise ValueError(msg) + raise ValueError(msg) from None if bold and color <= 7: color += 8 diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 2547e194a4..39fa69d10c 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -317,18 +317,19 @@ def wrap_exceptions(fun): @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name try: return fun(self, *args, **kwargs) - except (FileNotFoundError, ProcessLookupError): + except (FileNotFoundError, ProcessLookupError) as err: # 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) + if not pid_exists(pid): + raise NoSuchProcess(pid, name) from err else: - raise ZombieProcess(self.pid, self._name, self._ppid) - except PermissionError: - raise AccessDenied(self.pid, self._name) + raise ZombieProcess(pid, name, ppid) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err return wrapper @@ -556,10 +557,10 @@ def wait(self, timeout=None): def io_counters(self): try: rc, wc, rb, wb = cext.proc_io_counters(self.pid) - except OSError: + except OSError as err: # 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 NoSuchProcess(self.pid, self._name) from err raise return _common.pio(rc, wc, rb, wb) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 6f6eeb35f7..84ad4c6031 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -590,21 +590,19 @@ def wrap_exceptions(fun): @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name try: return fun(self, *args, **kwargs) - except ProcessLookupError: - if is_zombie(self.pid): - raise ZombieProcess(self.pid, self._name, self._ppid) + except ProcessLookupError as err: + if is_zombie(pid): + raise ZombieProcess(pid, name, ppid) from err 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 + raise NoSuchProcess(pid, name) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err + except OSError as err: + if pid == 0 and 0 in pids(): + raise AccessDenied(pid, name) from err raise return wrapper @@ -613,18 +611,19 @@ def wrapper(self, *args, **kwargs): @contextlib.contextmanager def wrap_exceptions_procfs(inst): """Same as above, for routines relying on reading /proc fs.""" + pid, name, ppid = inst.pid, inst._name, inst._ppid try: yield - except (ProcessLookupError, FileNotFoundError): + except (ProcessLookupError, FileNotFoundError) as err: # 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 is_zombie(inst.pid): - raise ZombieProcess(inst.pid, inst._name, inst._ppid) + raise ZombieProcess(pid, name, ppid) from err else: - raise NoSuchProcess(inst.pid, inst._name) - except PermissionError: - raise AccessDenied(inst.pid, inst._name) + raise NoSuchProcess(pid, name) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err class Process: @@ -701,10 +700,11 @@ def cmdline(self): return cext.proc_cmdline(self.pid) except OSError as err: if err.errno == errno.EINVAL: + pid, name, ppid = self.pid, self._name, self._ppid if is_zombie(self.pid): - raise ZombieProcess(self.pid, self._name, self._ppid) + raise ZombieProcess(pid, name, ppid) from err elif not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name, self._ppid) + raise NoSuchProcess(pid, name, ppid) from err else: # XXX: this happens with unicode tests. It means the C # routine is unable to decode invalid unicode chars. @@ -951,7 +951,7 @@ def cpu_affinity_set(self, cpus): f"invalid CPU {cpu!r} (choose between" f" {allcpus})" ) - raise ValueError(msg) + raise ValueError(msg) from err raise @wrap_exceptions diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 81ec88bdaf..489000fdc2 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -870,9 +870,8 @@ def decode_address(addr, family): except ValueError: # see: https://github.com/giampaolo/psutil/issues/623 if not supports_ipv6(): - raise _Ipv6UnsupportedError - else: - raise + raise _Ipv6UnsupportedError from None + raise return _common.addr(ip, port) @staticmethod @@ -893,7 +892,7 @@ def process_inet(file, family, type_, inodes, filter_pid=None): f"error while parsing {file}; malformed line" f" {lineno} {line!r}" ) - raise RuntimeError(msg) + raise RuntimeError(msg) from None if inode in inodes: # # We assume inet sockets are unique, so we error # # out if there are multiple references to the @@ -934,7 +933,7 @@ def process_unix(file, family, inodes, filter_pid=None): msg = ( f"error while parsing {file}; malformed line {line!r}" ) - raise RuntimeError(msg) + raise RuntimeError(msg) # noqa: B904 if inode in inodes: # noqa # With UNIX sockets we can have a single inode # referencing many file descriptors. @@ -1643,20 +1642,21 @@ def wrap_exceptions(fun): @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, name = self.pid, self._name try: return fun(self, *args, **kwargs) - except PermissionError: - raise AccessDenied(self.pid, self._name) - except ProcessLookupError: + except PermissionError as err: + raise AccessDenied(pid, name) from err + except ProcessLookupError as err: self._raise_if_zombie() - raise NoSuchProcess(self.pid, self._name) - except FileNotFoundError: + raise NoSuchProcess(pid, name) from err + except FileNotFoundError as err: self._raise_if_zombie() # /proc/PID directory may still exist, but the files within # it may not, indicating the process is gone, see: # https://github.com/giampaolo/psutil/issues/2418 - if not os.path.exists(f"{self._procfs_path}/{self.pid}/stat"): - raise NoSuchProcess(self.pid, self._name) + if not os.path.exists(f"{self._procfs_path}/{pid}/stat"): + raise NoSuchProcess(pid, name) from err raise return wrapper @@ -1859,7 +1859,7 @@ def io_counters(self): f"{err.args[0]!r} field was not found in {fname}; found" f" fields are {fields!r}" ) - raise ValueError(msg) + raise ValueError(msg) from None @wrap_exceptions def cpu_times(self): @@ -2011,7 +2011,7 @@ def get_blocks(lines, current_block): "don't know how to interpret line" f" {line!r}" ) - raise ValueError(msg) + raise ValueError(msg) from None yield (current_block.pop(), data) data = self._read_smaps_file() @@ -2156,13 +2156,13 @@ def cpu_affinity_set(self, cpus): f"invalid CPU {cpu!r}; choose between" f" {eligible_cpus!r}" ) - raise ValueError(msg) + raise ValueError(msg) from None if cpu not in eligible_cpus: msg = ( f"CPU number {cpu} is not eligible; choose" f" between {eligible_cpus}" ) - raise ValueError(msg) + raise ValueError(msg) from err raise # only starting from kernel 2.6.13 diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 3fdbcd6d74..95cf3ae179 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -343,15 +343,16 @@ def wrap_exceptions(fun): @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name try: return fun(self, *args, **kwargs) - except ProcessLookupError: - if is_zombie(self.pid): - raise ZombieProcess(self.pid, self._name, self._ppid) + except ProcessLookupError as err: + if is_zombie(pid): + raise ZombieProcess(pid, name, ppid) from err else: - raise NoSuchProcess(self.pid, self._name) - except PermissionError: - raise AccessDenied(self.pid, self._name) + raise NoSuchProcess(pid, name) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err return wrapper diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index ac3345b7ac..331e33e1cd 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -350,22 +350,23 @@ def wrap_exceptions(fun): @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name try: return fun(self, *args, **kwargs) - except (FileNotFoundError, ProcessLookupError): + except (FileNotFoundError, ProcessLookupError) as err: # 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) + if not pid_exists(pid): + raise NoSuchProcess(pid, name) from err else: - raise ZombieProcess(self.pid, self._name, self._ppid) - except PermissionError: - raise AccessDenied(self.pid, self._name) - except OSError: - if self.pid == 0: + raise ZombieProcess(pid, name, ppid) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err + except OSError as err: + if pid == 0: if 0 in pids(): - raise AccessDenied(self.pid, self._name) + raise AccessDenied(pid, name) from err else: raise raise diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 7a6558fe1a..f02eba3574 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -48,7 +48,7 @@ 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) + raise RuntimeError(msg) from err else: raise @@ -520,18 +520,18 @@ def _wrap_exceptions(self): try: yield except OSError as err: + name = self._name if is_permission_err(err): msg = ( - f"service {self._name!r} is not querable (not enough" - " privileges)" + f"service {name!r} is not querable (not enough privileges)" ) - raise AccessDenied(pid=None, name=self._name, msg=msg) + raise AccessDenied(pid=None, name=name, msg=msg) from err elif err.winerror in { cext.ERROR_INVALID_NAME, cext.ERROR_SERVICE_DOES_NOT_EXIST, }: - msg = f"service {self._name!r} does not exist" - raise NoSuchProcess(pid=None, name=self._name, msg=msg) + msg = f"service {name!r} does not exist" + raise NoSuchProcess(pid=None, name=name, msg=msg) from err else: raise @@ -672,7 +672,7 @@ def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except OSError as err: - raise convert_oserror(err, pid=self.pid, name=self._name) + raise convert_oserror(err, pid=self.pid, name=self._name) from err return wrapper @@ -757,7 +757,7 @@ def exe(self): # (perhaps PyPy's JIT delaying garbage collection of files?). if err.errno == 24: debug(f"{err!r} translated into AccessDenied") - raise AccessDenied(self.pid, self._name) + raise AccessDenied(self.pid, self._name) from err raise else: exe = cext.proc_exe(self.pid) @@ -791,7 +791,7 @@ def ppid(self): try: return ppid_map()[self.pid] except KeyError: - raise NoSuchProcess(self.pid, self._name) + raise NoSuchProcess(self.pid, self._name) from None def _get_raw_meminfo(self): try: @@ -839,7 +839,7 @@ def memory_maps(self): except OSError as err: # XXX - can't use wrap_exceptions decorator as we're # returning a generator; probably needs refactoring. - raise convert_oserror(err, self.pid, self._name) + raise convert_oserror(err, self.pid, self._name) from err else: for addr, perm, path, rss in raw: path = convert_dos_path(path) @@ -879,9 +879,9 @@ def wait(self, timeout=None): # May also be None if OpenProcess() failed with # ERROR_INVALID_PARAMETER, meaning PID is already gone. exit_code = cext.proc_wait(self.pid, cext_timeout) - except cext.TimeoutExpired: + except cext.TimeoutExpired as err: # WaitForSingleObject() returned WAIT_TIMEOUT. Just raise. - raise TimeoutExpired(timeout, self.pid, self._name) + raise TimeoutExpired(timeout, self.pid, self._name) from err except cext.TimeoutAbandoned: # WaitForSingleObject() returned WAIT_ABANDONED, see: # https://github.com/giampaolo/psutil/issues/1224 diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 1b6269a3d1..b99b18d9cb 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1717,3 +1717,20 @@ def test_kill_terminate(self): proc.send_signal(signal.CTRL_C_EVENT) with pytest.raises(psutil.NoSuchProcess): proc.send_signal(signal.CTRL_BREAK_EVENT) + + def test__getattribute__(self): + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.terminate() + proc.wait() + with pytest.raises(AttributeError): + proc.foo # noqa: B018 diff --git a/pyproject.toml b/pyproject.toml index fa63e600f8..673072564a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ ignore = [ "ARG001", # unused-function-argument "ARG002", # unused-method-argument "B007", # Loop control variable `x` not used within loop body - "B904", # Use `raise from` to specify exception cause "C4", # flake8-comprehensions "C90", # mccabe (function `X` is too complex) "COM812", # Trailing comma missing @@ -99,10 +98,11 @@ ignore = [ # T201 == print() # T203 == pprint() # TRY003 == raise-vanilla-args -".github/workflows/*" = ["EM101", "EM102", "EM103", "T201", "T203"] -"psutil/tests/*" = ["EM101", "EM102", "EM103", "TRY003"] -"scripts/*" = ["EM101", "EM102", "EM103", "T201", "T203"] -"scripts/internal/*" = ["EM101", "EM102", "EM103", "T201", "T203", "TRY003"] +# "B904", # Use `raise from` to specify exception cause +".github/workflows/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] +"psutil/tests/*" = ["B904", "EM101", "EM102", "EM103", "TRY003"] +"scripts/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] +"scripts/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "setup.py" = [ "B904", # Use ` raise from` to specify exception cause (PYTHON2.7 COMPAT) "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) From 3ad810d7267db48195bee6127fd68284ae4925b0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Dec 2024 19:02:56 +0100 Subject: [PATCH 1176/1714] ruff: enable RET506 - [*] Unnecessary `else` after `raise` statement --- psutil/__init__.py | 8 +++----- psutil/_psaix.py | 3 +-- psutil/_psbsd.py | 14 ++++++-------- psutil/_pslinux.py | 6 ++---- psutil/_psosx.py | 3 +-- psutil/_pssunos.py | 6 ++---- psutil/_pswindows.py | 5 ++--- psutil/tests/test_linux.py | 26 ++++++++++---------------- psutil/tests/test_process.py | 11 +++++------ pyproject.toml | 1 + 10 files changed, 33 insertions(+), 50 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 0423473587..50b1f2a5cd 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -361,8 +361,7 @@ def _init(self, pid, _ignore_nsp=False): if not _ignore_nsp: msg = "process PID not found" raise NoSuchProcess(pid, msg=msg) from None - else: - self._gone = True + self._gone = True def _get_ident(self): """Return a (pid, uid) tuple which is supposed to identify a @@ -1267,9 +1266,8 @@ def _send_signal(self, sig): # We do this because os.kill() lies in case of # zombie processes. raise ZombieProcess(pid, name, ppid) from err - else: - self._gone = True - raise NoSuchProcess(pid, name) from err + self._gone = True + raise NoSuchProcess(pid, name) from err except PermissionError as err: raise AccessDenied(pid, name) from err diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 39fa69d10c..ba2725fdc1 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -326,8 +326,7 @@ def wrapper(self, *args, **kwargs): # process is gone in meantime. if not pid_exists(pid): raise NoSuchProcess(pid, name) from err - else: - raise ZombieProcess(pid, name, ppid) from err + raise ZombieProcess(pid, name, ppid) from err except PermissionError as err: raise AccessDenied(pid, name) from err diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 84ad4c6031..303b6ad083 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -596,8 +596,7 @@ def wrapper(self, *args, **kwargs): except ProcessLookupError as err: if is_zombie(pid): raise ZombieProcess(pid, name, ppid) from err - else: - raise NoSuchProcess(pid, name) from err + raise NoSuchProcess(pid, name) from err except PermissionError as err: raise AccessDenied(pid, name) from err except OSError as err: @@ -703,13 +702,12 @@ def cmdline(self): pid, name, ppid = self.pid, self._name, self._ppid if is_zombie(self.pid): raise ZombieProcess(pid, name, ppid) from err - elif not pid_exists(self.pid): + if not pid_exists(self.pid): raise NoSuchProcess(pid, name, ppid) from err - else: - # XXX: this happens with unicode tests. It means the C - # routine is unable to decode invalid unicode chars. - debug(f"ignoring {err!r} and returning an empty list") - return [] + # XXX: this happens with unicode tests. It means the C + # routine is unable to decode invalid unicode chars. + debug(f"ignoring {err!r} and returning an empty list") + return [] else: raise else: diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 489000fdc2..4a539e06ec 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1057,8 +1057,7 @@ def net_if_stats(): # https://github.com/giampaolo/psutil/issues/1279 if err.errno != errno.ENODEV: raise - else: - debug(err) + debug(err) else: output_flags = ','.join(flags) isup = 'running' in flags @@ -2075,8 +2074,7 @@ def num_ctx_switches( " probably older than 2.6.23" ) raise NotImplementedError(msg) - else: - return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) + return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) @wrap_exceptions def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')): diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 95cf3ae179..fa2c8b81d0 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -349,8 +349,7 @@ def wrapper(self, *args, **kwargs): except ProcessLookupError as err: if is_zombie(pid): raise ZombieProcess(pid, name, ppid) from err - else: - raise NoSuchProcess(pid, name) from err + raise NoSuchProcess(pid, name) from err except PermissionError as err: raise AccessDenied(pid, name) from err diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 331e33e1cd..fdc9e7e007 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -359,16 +359,14 @@ def wrapper(self, *args, **kwargs): # process is gone in meantime. if not pid_exists(pid): raise NoSuchProcess(pid, name) from err - else: - raise ZombieProcess(pid, name, ppid) from err + raise ZombieProcess(pid, name, ppid) from err except PermissionError as err: raise AccessDenied(pid, name) from err except OSError as err: if pid == 0: if 0 in pids(): raise AccessDenied(pid, name) from err - else: - raise + raise raise return wrapper diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index f02eba3574..a28180264b 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -1080,9 +1080,8 @@ def to_bitmask(ls): if not isinstance(cpu, int): msg = f"invalid CPU {cpu!r}; an integer is required" raise TypeError(msg) - else: - msg = f"invalid CPU {cpu!r}" - raise ValueError(msg) + msg = f"invalid CPU {cpu!r}" + raise ValueError(msg) bitmask = to_bitmask(value) cext.proc_cpu_affinity_set(self.pid, bitmask) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 741a152376..25e4cae114 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -214,8 +214,7 @@ def mock_open_exception(for_path, exc): def open_mock(name, *args, **kwargs): if name == for_path: raise exc - else: - return orig_open(name, *args, **kwargs) + return orig_open(name, *args, **kwargs) orig_open = open with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: @@ -277,12 +276,9 @@ def test_available(self): lines = out.split('\n') if 'available' not in lines[0]: raise pytest.skip("free does not support 'available' column") - else: - free_value = int(lines[1].split()[-1]) - psutil_value = psutil.virtual_memory().available - assert ( - abs(free_value - psutil_value) < TOLERANCE_SYS_MEM - ), f"{free_value} {psutil_value} \n{out}" + free_value = int(lines[1].split()[-1]) + psutil_value = psutil.virtual_memory().available + assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM @pytest.mark.skipif(not LINUX, reason="LINUX only") @@ -889,7 +885,7 @@ def test_emulate_no_scaling_cur_freq_file(self): def open_mock(name, *args, **kwargs): if name.endswith('/scaling_cur_freq'): raise FileNotFoundError - elif name.endswith('/cpuinfo_cur_freq'): + if name.endswith('/cpuinfo_cur_freq'): return io.BytesIO(b"200000") elif name == '/proc/cpuinfo': return io.BytesIO(b"cpu MHz : 200") @@ -1606,7 +1602,7 @@ def test_emulate_power_plugged_2(self): def open_mock(name, *args, **kwargs): if name.endswith(('AC0/online', 'AC/online')): raise FileNotFoundError - elif name.endswith("/status"): + if name.endswith("/status"): return io.StringIO("charging") else: return orig_open(name, *args, **kwargs) @@ -1635,7 +1631,7 @@ def test_emulate_power_not_plugged_2(self): def open_mock(name, *args, **kwargs): if name.endswith(('AC0/online', 'AC/online')): raise FileNotFoundError - elif name.endswith("/status"): + if name.endswith("/status"): return io.StringIO("discharging") else: return orig_open(name, *args, **kwargs) @@ -1654,7 +1650,7 @@ def open_mock(name, *args, **kwargs): '/sys/class/power_supply/AC/online', )): raise FileNotFoundError - elif name.startswith("/sys/class/power_supply/BAT0/status"): + if name.startswith("/sys/class/power_supply/BAT0/status"): return io.BytesIO(b"???") else: return orig_open(name, *args, **kwargs) @@ -2030,8 +2026,7 @@ def test_threads_mocked(self): def open_mock_1(name, *args, **kwargs): if name.startswith(f"/proc/{os.getpid()}/task"): raise FileNotFoundError - else: - return orig_open(name, *args, **kwargs) + return orig_open(name, *args, **kwargs) orig_open = open with mock.patch("builtins.open", side_effect=open_mock_1) as m: @@ -2044,8 +2039,7 @@ def open_mock_1(name, *args, **kwargs): def open_mock_2(name, *args, **kwargs): if name.startswith(f"/proc/{os.getpid()}/task"): raise PermissionError - else: - return orig_open(name, *args, **kwargs) + return orig_open(name, *args, **kwargs) with mock.patch("builtins.open", side_effect=open_mock_2): with pytest.raises(psutil.AccessDenied): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b99b18d9cb..fbc17a6b56 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -654,12 +654,11 @@ def test_memory_maps(self): 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 f"{nt.path} (deleted)" not in data: - raise + # https://github.com/giampaolo/psutil/issues/759 + with open_text('/proc/self/smaps') as f: + data = f.read() + if f"{nt.path} (deleted)" not in data: + raise elif '64' not in os.path.basename(nt.path): # XXX - On Windows we have this strange behavior with # 64 bit dlls: they are visible via explorer but cannot diff --git a/pyproject.toml b/pyproject.toml index 673072564a..af1cad24f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ select = [ "D301", # Use `r"""` if any backslashes in a docstring "D403", # [*] First word of the first line should be capitalized "PERF102", # [*] When using only the keys of a dict use the `keys()` method + "RET506", # [*] Unnecessary `else` after `raise` statement "RET507", # Unnecessary `elif` after `continue` statement "S113", # Probable use of requests call without timeout "S602", # `subprocess` call with `shell=True` identified, security issue From b42eccd7f152daadf31b61ce7711bb6385c8b840 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Dec 2024 19:04:50 +0100 Subject: [PATCH 1177/1714] ruff: add RET502 - [*] Do not implicitly `return None` in function able to return non-`None` value --- psutil/_psposix.py | 2 +- psutil/tests/test_system.py | 2 +- pyproject.toml | 1 + scripts/fans.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index fdfce147f3..88703fdbd2 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -119,7 +119,7 @@ def sleep(interval): # can't determine its exit status code. while _pid_exists(pid): interval = sleep(interval) - return + return None else: if retpid == 0: # WNOHANG flag was used and PID is still running. diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 723aaa3379..bcf2705454 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -478,7 +478,7 @@ def test_per_cpu_times_2(self): t1, t2 = psutil._cpu_busy_time(t1), psutil._cpu_busy_time(t2) difference = t2 - t1 if difference >= 0.05: - return + return None @pytest.mark.skipif( CI_TESTING and OPENBSD, reason="unreliable on OPENBSD + CI" diff --git a/pyproject.toml b/pyproject.toml index af1cad24f9..74d6157cb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ select = [ "D301", # Use `r"""` if any backslashes in a docstring "D403", # [*] First word of the first line should be capitalized "PERF102", # [*] When using only the keys of a dict use the `keys()` method + "RET502", # [*] Do not implicitly `return None` in function able to return non-`None` value "RET506", # [*] Unnecessary `else` after `raise` statement "RET507", # Unnecessary `elif` after `continue` statement "S113", # Probable use of requests call without timeout diff --git a/scripts/fans.py b/scripts/fans.py index ca6a453bd8..950da7bd21 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -23,7 +23,7 @@ def main(): fans = psutil.sensors_fans() if not fans: print("no fans detected") - return + return None for name, entries in fans.items(): print(name) for entry in entries: From 107daa15b2085823605fce25aa4fee261a071ad2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 23 Dec 2024 09:39:53 +0100 Subject: [PATCH 1178/1714] ruff: enable SIM115: Use context handler for opening files --- .github/workflows/issues.py | 7 ++++--- psutil/_common.py | 2 +- psutil/tests/__init__.py | 2 +- psutil/tests/test_process.py | 2 +- pyproject.toml | 1 - 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 7fb369eb90..1cdc4d996b 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -159,9 +159,10 @@ def get_repo(): @functools.lru_cache() def _get_event_data(): - ret = json.load(open(os.environ["GITHUB_EVENT_PATH"])) - pp(ret) - return ret + with open(open(os.environ["GITHUB_EVENT_PATH"])) as f: + ret = json.load(f) + pp(ret) + return ret def is_event_new_issue(): diff --git a/psutil/_common.py b/psutil/_common.py index a08ce087a6..a27351f4fd 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -753,7 +753,7 @@ def open_text(fname): # See: # https://github.com/giampaolo/psutil/issues/675 # https://github.com/giampaolo/psutil/pull/733 - fobj = open( + fobj = open( # noqa: SIM115 fname, buffering=FILE_READ_BUFFER_SIZE, encoding=ENCODING, diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 54f468f51a..9d8378134f 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -263,7 +263,7 @@ def attempt(exe): PYTHON_EXE, PYTHON_EXE_ENV = _get_py_exe() -DEVNULL = open(os.devnull, 'r+') +DEVNULL = open(os.devnull, 'r+') # noqa: SIM115 atexit.register(DEVNULL.close) VALID_PROC_STATUSES = [ diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index fbc17a6b56..64f71ff153 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1119,7 +1119,7 @@ def test_num_fds(self): p = psutil.Process() testfn = self.get_testfn() start = p.num_fds() - file = open(testfn, 'w') + file = open(testfn, 'w') # noqa: SIM115 self.addCleanup(file.close) assert p.num_fds() == start + 1 sock = socket.socket() diff --git a/pyproject.toml b/pyproject.toml index 74d6157cb4..5f8ef0691b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,6 @@ ignore = [ "S", # flake8-bandit "SIM102", # Use a single `if` statement instead of nested `if` statements "SIM105", # Use `contextlib.suppress(OSError)` instead of `try`-`except`-`pass` - "SIM115", # Use context handler for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements "SLF", # flake8-self "TD", # all TODOs, XXXs, etc. From 623e6e088e51d8c41e4344cd4980570f3d5e9cba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2024 13:13:27 +0100 Subject: [PATCH 1179/1714] New ruff rules (#2489) --- .github/workflows/issues.py | 2 +- psutil/__init__.py | 50 +++++----- psutil/_common.py | 12 +-- psutil/_psbsd.py | 8 +- psutil/_pslinux.py | 6 +- psutil/_pswindows.py | 8 +- psutil/tests/__init__.py | 16 ++-- psutil/tests/test_connections.py | 22 ++--- psutil/tests/test_linux.py | 11 ++- psutil/tests/test_misc.py | 2 +- psutil/tests/test_osx.py | 2 +- psutil/tests/test_process.py | 124 +++++++------------------ psutil/tests/test_process_all.py | 15 +-- psutil/tests/test_system.py | 6 +- psutil/tests/test_testutils.py | 24 ++--- psutil/tests/test_windows.py | 2 +- pyproject.toml | 17 ++-- scripts/internal/check_broken_links.py | 4 +- scripts/internal/convert_readme.py | 2 +- scripts/internal/print_downloads.py | 2 +- scripts/internal/print_timeline.py | 2 +- scripts/internal/winmake.py | 5 +- scripts/pidof.py | 6 +- setup.py | 22 ++--- 24 files changed, 162 insertions(+), 208 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 1cdc4d996b..21c720ca52 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -159,7 +159,7 @@ def get_repo(): @functools.lru_cache() def _get_event_data(): - with open(open(os.environ["GITHUB_EVENT_PATH"])) as f: + with open(os.environ["GITHUB_EVENT_PATH"]) as f: ret = json.load(f) pp(ret) return ret diff --git a/psutil/__init__.py b/psutil/__init__.py index 50b1f2a5cd..bc17aabf18 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -51,16 +51,16 @@ from ._common import CONN_SYN_RECV from ._common import CONN_SYN_SENT from ._common import CONN_TIME_WAIT -from ._common import FREEBSD # NOQA +from ._common import FREEBSD from ._common import LINUX from ._common import MACOS -from ._common import NETBSD # NOQA +from ._common import NETBSD 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 OPENBSD from ._common import OSX # deprecated alias -from ._common import POSIX # NOQA +from ._common import POSIX from ._common import POWER_TIME_UNKNOWN from ._common import POWER_TIME_UNLIMITED from ._common import STATUS_DEAD @@ -93,24 +93,24 @@ 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 + from ._pslinux import IOPRIO_CLASS_BE # noqa: F401 + from ._pslinux import IOPRIO_CLASS_IDLE # noqa: F401 + from ._pslinux import IOPRIO_CLASS_NONE # noqa: F401 + from ._pslinux import IOPRIO_CLASS_RT # noqa: F401 elif WINDOWS: from . import _pswindows as _psplatform - from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS # NOQA - from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS # NOQA - from ._psutil_windows import HIGH_PRIORITY_CLASS # NOQA - from ._psutil_windows import IDLE_PRIORITY_CLASS # NOQA - 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 + from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import HIGH_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import IDLE_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import NORMAL_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import REALTIME_PRIORITY_CLASS # noqa: F401 + from ._pswindows import CONN_DELETE_TCB # noqa: F401 + from ._pswindows import IOPRIO_HIGH # noqa: F401 + from ._pswindows import IOPRIO_LOW # noqa: F401 + from ._pswindows import IOPRIO_NORMAL # noqa: F401 + from ._pswindows import IOPRIO_VERYLOW # noqa: F401 elif MACOS: from . import _psosx as _psplatform @@ -120,8 +120,8 @@ elif SUNOS: from . import _pssunos as _psplatform - from ._pssunos import CONN_BOUND # NOQA - from ._pssunos import CONN_IDLE # NOQA + from ._pssunos import CONN_BOUND # noqa: F401 + from ._pssunos import CONN_IDLE # noqa: F401 # This is public writable API which is read from _pslinux.py and # _pssunos.py via sys.modules. @@ -1201,7 +1201,7 @@ def memory_maps(self, grouped=True): except KeyError: d[path] = nums nt = _psplatform.pmmap_grouped - return [nt(path, *d[path]) for path in d] # NOQA + return [nt(path, *d[path]) for path in d] else: nt = _psplatform.pmmap_ext return [nt(*x) for x in it] @@ -1442,7 +1442,7 @@ def __getattribute__(self, name): def wait(self, timeout=None): if self.__subproc.returncode is not None: return self.__subproc.returncode - ret = super().wait(timeout) # noqa + ret = super().wait(timeout) self.__subproc.returncode = ret return ret @@ -1536,7 +1536,7 @@ def remove(pid): _pmap = pmap -process_iter.cache_clear = lambda: _pmap.clear() # noqa +process_iter.cache_clear = lambda: _pmap.clear() # noqa: PLW0108 process_iter.cache_clear.__doc__ = "Clear process_iter() internal cache." @@ -1619,7 +1619,7 @@ def check_gone(proc, timeout): check_gone(proc, timeout) else: check_gone(proc, max_timeout) - alive = alive - gone # noqa PLR6104 + alive = alive - gone # noqa: PLR6104 if alive: # Last attempt over processes survived so far. diff --git a/psutil/_common.py b/psutil/_common.py index a27351f4fd..0c385fa6ae 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -269,9 +269,7 @@ 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: + if value or (name == "pid" and value == 0): info[name] = value return info @@ -876,9 +874,9 @@ def print_color( ): # pragma: no cover """Print a colorized version of string.""" if not term_supports_colors(): - print(s, file=file) # NOQA + print(s, file=file) elif POSIX: - print(hilite(s, color, bold), file=file) # NOQA + print(hilite(s, color, bold), file=file) else: import ctypes @@ -906,7 +904,7 @@ def print_color( handle = GetStdHandle(handle_id) SetConsoleTextAttribute(handle, color) try: - print(s, file=file) # NOQA + print(s, file=file) finally: SetConsoleTextAttribute(handle, DEFAULT_COLOR) @@ -925,6 +923,6 @@ def debug(msg): msg = f"ignoring {msg}" else: msg = f"ignoring {msg!r}" - print( # noqa + print( # noqa: T201 f"psutil-debug [{fname}:{lineno}]> {msg}", file=sys.stderr ) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 303b6ad083..13bd926fab 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -10,7 +10,7 @@ import os from collections import defaultdict from collections import namedtuple -from xml.etree import ElementTree # noqa ICN001 +from xml.etree import ElementTree # noqa: ICN001 from . import _common from . import _psposix @@ -192,8 +192,8 @@ def virtual_memory(): # #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 + # https://github.com/htop-dev/htop/blob/e7f447b/netbsd/NetBSDProcessList.c#L162 + # https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L135 used = active + wired avail = total - used else: @@ -202,7 +202,7 @@ def virtual_memory(): # * 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 + # * https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/freebsd/memory.c#L143 avail = inactive + cached + free used = active + wired + cached diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4a539e06ec..165125fdf5 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -934,7 +934,7 @@ def process_unix(file, family, inodes, filter_pid=None): f"error while parsing {file}; malformed line {line!r}" ) raise RuntimeError(msg) # noqa: B904 - if inode in inodes: # noqa + if inode in inodes: # noqa: SIM108, SIM401 # With UNIX sockets we can have a single inode # referencing many file descriptors. pairs = inodes[inode] @@ -1327,7 +1327,7 @@ def sensors_temperatures(): basenames2 = glob.glob( '/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*' ) - repl = re.compile('/sys/devices/platform/coretemp.*/hwmon/') + repl = re.compile(r"/sys/devices/platform/coretemp.*/hwmon/") for name in basenames2: altname = repl.sub('/sys/class/hwmon/', name) if altname not in basenames: @@ -1480,7 +1480,7 @@ def multi_bcat(*paths): # Get the first available battery. Usually this is "BAT0", except # some rare exceptions: # https://github.com/giampaolo/psutil/issues/1238 - root = os.path.join(POWER_SUPPLY_PATH, sorted(bats)[0]) + root = os.path.join(POWER_SUPPLY_PATH, min(bats)) # Base metrics. energy_now = multi_bcat(root + "/energy_now", root + "/charge_now") diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index a28180264b..69820ba419 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -983,10 +983,10 @@ def open_files(self): # Convert the first part in the corresponding drive letter # (e.g. "C:\") by using Windows's QueryDosDevice() raw_file_names = cext.proc_open_files(self.pid) - for _file in raw_file_names: - _file = convert_dos_path(_file) - if isfile_strict(_file): - ntuple = _common.popenfile(_file, -1) + for file in raw_file_names: + file = convert_dos_path(file) + if isfile_strict(file): + ntuple = _common.popenfile(file, -1) ret.add(ntuple) return list(ret) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 9d8378134f..3817f8b12a 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -707,14 +707,14 @@ def wrapper(*args, **kwargs): for _ in self: try: return fun(*args, **kwargs) - except self.exception as _: # NOQA + except self.exception as _: exc = _ if self.logfun is not None: self.logfun(exc) self.sleep() continue - raise exc # noqa: PLE0704 + raise exc # This way the user of the decorated function can change config # parameters. @@ -890,7 +890,7 @@ class fake_pytest: """ @staticmethod - def main(*args, **kw): # noqa ARG004 + def main(*args, **kw): # noqa: ARG004 """Mimics pytest.main(). It has the same effect as running `python3 -m unittest -v` from the project root directory. """ @@ -1228,7 +1228,7 @@ def _check_mem(self, fun, times, retries, tolerance): return else: if idx == 1: - print() # NOQA + print() # noqa: T201 self._log(msg) times += increase prev_mem = mem @@ -1365,17 +1365,17 @@ def print_sysinfo(): pinfo.pop('memory_maps', None) info['proc'] = pprint.pformat(pinfo) - print("=" * 70, file=sys.stderr) # NOQA + print("=" * 70, file=sys.stderr) # noqa: T201 for k, v in info.items(): print("{:<17} {}".format(k + ":", v), file=sys.stderr) # noqa: T201 - print("=" * 70, file=sys.stderr) # NOQA + print("=" * 70, file=sys.stderr) # noqa: T201 sys.stdout.flush() # if WINDOWS: # os.system("tasklist") # elif shutil.which("ps"): # os.system("ps aux") - # print("=" * 70, file=sys.stderr) # NOQA + # print("=" * 70, file=sys.stderr) sys.stdout.flush() @@ -1621,7 +1621,7 @@ def retry_on_failure(retries=NO_RETRIES): """ def logfun(exc): - print(f"{exc!r}, retrying", file=sys.stderr) # NOQA + print(f"{exc!r}, retrying", file=sys.stderr) # noqa: T201 return retry( exception=AssertionError, timeout=None, retries=retries, logfun=logfun diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index a082f90158..5ddeb855f2 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -184,7 +184,7 @@ 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) - assert conn.raddr == "" # noqa + assert conn.raddr == "" assert conn.status == psutil.CONN_NONE @pytest.mark.skipif(not POSIX, reason="POSIX only") @@ -192,7 +192,7 @@ 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) - assert conn.raddr == "" # noqa + assert conn.raddr == "" assert conn.status == psutil.CONN_NONE @@ -239,8 +239,8 @@ def test_unix(self): assert len(cons) == 2 if LINUX or FREEBSD or SUNOS or OPENBSD: # remote path is never set - assert cons[0].raddr == "" # noqa - assert cons[1].raddr == "" # noqa + assert cons[0].raddr == "" + assert cons[1].raddr == "" # one local address should though assert testfn == (cons[0].laddr or cons[1].laddr) else: @@ -356,14 +356,14 @@ def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): # 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 + tcp4_addr = eval(wait_for_file(testfile, delete=True)) udp4_proc = self.pyrun(udp4_template) - udp4_addr = eval(wait_for_file(testfile, delete=True)) # noqa + udp4_addr = eval(wait_for_file(testfile, delete=True)) if supports_ipv6(): tcp6_proc = self.pyrun(tcp6_template) - tcp6_addr = eval(wait_for_file(testfile, delete=True)) # noqa + tcp6_addr = eval(wait_for_file(testfile, delete=True)) udp6_proc = self.pyrun(udp6_template) - udp6_addr = eval(wait_for_file(testfile, delete=True)) # noqa + udp6_addr = eval(wait_for_file(testfile, delete=True)) else: tcp6_proc = None udp6_proc = None @@ -560,7 +560,7 @@ def test_net_connection_constants(self): ints.append(num) strs.append(str_) if SUNOS: - psutil.CONN_IDLE # noqa - psutil.CONN_BOUND # noqa + psutil.CONN_IDLE # noqa: B018 + psutil.CONN_BOUND # noqa: B018 if WINDOWS: - psutil.CONN_DELETE_TCB # noqa + psutil.CONN_DELETE_TCB # noqa: B018 diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 25e4cae114..3672bc9591 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1763,9 +1763,10 @@ def open_mock(name, *args, **kwargs): 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*_*': + if path in { + '/sys/class/hwmon/hwmon*/temp*_*', + '/sys/class/hwmon/hwmon*/device/temp*_*', + }: return [] elif path == '/sys/class/thermal/thermal_zone*': return ['/sys/class/thermal/thermal_zone0'] @@ -1780,7 +1781,7 @@ def glob_mock(path): with mock.patch("builtins.open", side_effect=open_mock): with mock.patch('glob.glob', create=True, side_effect=glob_mock): temp = psutil.sensors_temperatures()['name'][0] - assert temp.label == '' # noqa + assert temp.label == '' assert temp.current == 30.0 assert temp.high == 50.0 assert temp.critical == 50.0 @@ -2055,7 +2056,7 @@ def test_exe_mocked(self): ): ret = psutil.Process().exe() assert m.called - assert ret == "" # noqa + assert ret == "" def test_issue_1014(self): # Emulates a case where smaps file does not exist. In this case diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 1c42b6022d..848b9284e8 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -100,7 +100,7 @@ def test_error__repr__(self): assert repr(psutil.Error()) == "psutil.Error()" def test_error__str__(self): - assert str(psutil.Error()) == "" # noqa + assert str(psutil.Error()) == "" def test_no_such_process__repr__(self): assert ( diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 682012ecfd..62895ccdd8 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -185,7 +185,7 @@ def test_net_if_stats(self): 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) + drawing_from = re.search(r"Now drawing from '([^']+)'", out).group(1) power_plugged = drawing_from == "AC Power" psutil_result = psutil.sensors_battery() assert psutil_result.power_plugged == power_plugged diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 64f71ff153..1d81d498f5 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -21,7 +21,6 @@ import sys import textwrap import time -import types from unittest import mock import psutil @@ -625,7 +624,7 @@ def test_memory_full_info(self): for name in mem._fields: value = getattr(mem, name) assert value >= 0 - if name == 'vms' and OSX or LINUX: + if (name == "vms" and OSX) or LINUX: continue assert value <= total if LINUX or WINDOWS or MACOS: @@ -642,33 +641,38 @@ def test_memory_maps(self): ext_maps = p.memory_maps(grouped=False) for nt in maps: - if not nt.path.startswith('['): - if QEMU_USER and "/bin/qemu-" in nt.path: - continue - 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 - # https://github.com/giampaolo/psutil/issues/759 - with open_text('/proc/self/smaps') as f: - data = f.read() - if f"{nt.path} (deleted)" not in data: - raise - elif '64' not in os.path.basename(nt.path): - # 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?). - try: - st = os.stat(nt.path) - except FileNotFoundError: - pass - else: - assert stat.S_ISREG(st.st_mode), nt.path + if nt.path.startswith('['): + continue + if BSD and nt.path == "pvclock": + continue + if QEMU_USER and "/bin/qemu-" in nt.path: + continue + 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 + # https://github.com/giampaolo/psutil/issues/759 + with open_text('/proc/self/smaps') as f: + data = f.read() + if f"{nt.path} (deleted)" not in data: + raise + elif '64' not in os.path.basename(nt.path): + # 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?). + 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) @@ -1210,7 +1214,7 @@ def test_children_duplicates(self): 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] + pid = max(table.items(), key=lambda x: x[1])[0] if LINUX and pid == 0: raise pytest.skip("PID 0") p = psutil.Process(pid) @@ -1579,62 +1583,6 @@ def test_weird_environ(self): assert 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") - - @pytest.mark.skipif(True, reason="causes problem as root") - def test_zombie_process(self): - pass - - # =================================================================== # --- psutil.Popen tests # =================================================================== @@ -1664,10 +1612,10 @@ def test_misc(self): ) as proc: proc.name() proc.cpu_times() - proc.stdin # noqa + proc.stdin # noqa: B018 assert dir(proc) with pytest.raises(AttributeError): - proc.foo # noqa + proc.foo # noqa: B018 proc.terminate() if POSIX: assert proc.wait(5) == -signal.SIGTERM diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 8dd2946c17..cb7264d735 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -410,11 +410,14 @@ def memory_maps(self, ret, info): 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 + if value.startswith(("[", "anon_inode:")): # linux + continue + if BSD and value == "pvclock": # seen on FreeBSD + continue + 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': @@ -486,7 +489,7 @@ def tearDown(self): def test_it(self): def is_linux_tid(pid): try: - f = open(f"/proc/{pid}/status", "rb") + f = open(f"/proc/{pid}/status", "rb") # noqa: SIM115 except FileNotFoundError: return False else: diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index bcf2705454..f2eb40548c 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -251,8 +251,8 @@ def test_users(self): assert isinstance(user.terminal, (str, type(None))) if user.host is not None: assert isinstance(user.host, (str, type(None))) - user.terminal # noqa - user.host # noqa + user.terminal # noqa: B018 + user.host # noqa: B018 assert user.started > 0.0 datetime.datetime.fromtimestamp(user.started) if WINDOWS or OPENBSD: @@ -672,7 +672,7 @@ def check_ntuple(nt): else: # we cannot make any assumption about this, see: # http://goo.gl/p9c43 - disk.device # noqa + disk.device # noqa: B018 # on modern systems mount points can also be files assert os.path.exists(disk.mountpoint), disk assert disk.fstype, disk diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index e6c3afa858..85f20c14e7 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -71,7 +71,7 @@ def test_retry_success(self, sleep): def foo(): while queue: queue.pop() - 1 / 0 # noqa + 1 / 0 # noqa: B018 return 1 queue = list(range(3)) @@ -85,7 +85,7 @@ def test_retry_failure(self, sleep): def foo(): while queue: queue.pop() - 1 / 0 # noqa + 1 / 0 # noqa: B018 return 1 queue = list(range(6)) @@ -109,7 +109,7 @@ def test_no_interval_arg(self, sleep): @retry(retries=5, interval=None, logfun=None) def foo(): - 1 / 0 # noqa + 1 / 0 # noqa: B018 with pytest.raises(ZeroDivisionError): foo() @@ -119,7 +119,7 @@ def foo(): def test_retries_arg(self, sleep): @retry(retries=5, interval=1, logfun=None) def foo(): - 1 / 0 # noqa + 1 / 0 # noqa: B018 with pytest.raises(ZeroDivisionError): foo() @@ -407,7 +407,7 @@ def fun(ls=ls): def test_unclosed_files(self): def fun(): - f = open(__file__) + f = open(__file__) # noqa: SIM115 self.addCleanup(f.close) box.append(f) @@ -429,7 +429,7 @@ def fun(): def test_execute_w_exc(self): def fun_1(): - 1 / 0 # noqa + 1 / 0 # noqa: B018 self.execute_w_exc(ZeroDivisionError, fun_1) with pytest.raises(ZeroDivisionError): @@ -452,7 +452,7 @@ def run_test_class(self, klass): def test_raises(self): with fake_pytest.raises(ZeroDivisionError) as cm: - 1 / 0 # noqa + 1 / 0 # noqa: B018 assert isinstance(cm.value, ZeroDivisionError) with fake_pytest.raises(ValueError, match="foo") as cm: @@ -484,7 +484,7 @@ def test_skipif(self): class TestCase(unittest.TestCase): @fake_pytest.mark.skipif(True, reason="reason") def foo(self): - assert 1 == 1 # noqa + assert 1 == 1 # noqa: PLR0133 result = self.run_test_class(TestCase("foo")) assert result.wasSuccessful() @@ -494,7 +494,7 @@ def foo(self): class TestCase(unittest.TestCase): @fake_pytest.mark.skipif(False, reason="reason") def foo(self): - assert 1 == 1 # noqa + assert 1 == 1 # noqa: PLR0133 result = self.run_test_class(TestCase("foo")) assert result.wasSuccessful() @@ -504,7 +504,7 @@ def test_skip(self): class TestCase(unittest.TestCase): def foo(self): fake_pytest.skip("reason") - assert 1 == 0 # noqa + assert 1 == 0 # noqa: PLR0133 result = self.run_test_class(TestCase("foo")) assert result.wasSuccessful() @@ -564,12 +564,12 @@ 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] + fun = next(x for x in ns.iter(ns.getters) if x[1] == 'ppid')[0] assert 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] + fun = next(x for x in ns.iter(ns.getters) if x[1] == 'net_if_addrs')[0] assert fun() == psutil.net_if_addrs() diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 667f1d6862..9f54cf534c 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -429,7 +429,7 @@ def test_username(self): assert psutil.Process().username() == name def test_cmdline(self): - sys_value = re.sub('[ ]+', ' ', win32api.GetCommandLine()).strip() + sys_value = re.sub(r"[ ]+", " ", 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 diff --git a/pyproject.toml b/pyproject.toml index 5f8ef0691b..f6eb772d8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,14 +51,12 @@ ignore = [ "FURB116", # [*] Replace `hex` call with `f"{start:x}"` "FURB118", # [*] Use `operator.add` instead of defining a lambda "FURB140", # [*] Use `itertools.starmap` instead of the generator - "FURB192", # [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence "INP", # flake8-no-pep420 "N801", # Class name `async_chat` should use CapWords convention (ASYNCORE COMPAT) "N802", # Function name X should be lowercase. "N806", # Variable X in function should be lowercase. "N818", # Exception name `FooBar` should be named with an Error suffix "PERF", # Perflint - "PGH004", # Use specific rule codes when using `noqa` "PLC0415", # `import` should be at the top-level of a file "PLC2701", # Private name import `x` from external module `y` "PLR0904", # Too many public methods (x > y) @@ -79,8 +77,14 @@ ignore = [ "PTH", # flake8-use-pathlib "PYI", # flake8-pyi (python types stuff) "Q000", # Single quotes found but double quotes preferred - "RET", # flake8-return - "RUF", # Ruff-specific rules + "RET503", # Missing explicit `return` at the end of function able to return non-`None` value + "RET504", # Unnecessary assignment to `result` before `return` statement + "RET505", # [*] Unnecessary `else` after `return` statement + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "RUF022", # `__all__` is not sorted + "RUF028", # This suppression comment is invalid + "RUF031", # [*] Avoid parentheses for tuples in subscripts "S", # flake8-bandit "SIM102", # Use a single `if` statement instead of nested `if` statements "SIM105", # Use `contextlib.suppress(OSError)` instead of `try`-`except`-`pass` @@ -99,9 +103,10 @@ ignore = [ # T201 == print() # T203 == pprint() # TRY003 == raise-vanilla-args -# "B904", # Use `raise from` to specify exception cause +# B904 == Use `raise from` to specify exception cause +# PLC1901 == `x == ""` can be simplified to `not x` as an empty string is falsey ".github/workflows/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] -"psutil/tests/*" = ["B904", "EM101", "EM102", "EM103", "TRY003"] +"psutil/tests/*" = ["B904", "EM101", "EM102", "EM103", "PLC1901", "TRY003"] "scripts/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] "scripts/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "setup.py" = [ diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 2a51381d33..2c6852b9a5 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -122,7 +122,7 @@ def parse_py(fname): subidx = i + 1 while True: nextline = lines[subidx].strip() - if re.match('^# .+', nextline): + if re.match(r"^# .+", nextline): url += nextline[1:].strip() else: break @@ -143,7 +143,7 @@ def parse_c(fname): subidx = i + 1 while True: nextline = lines[subidx].strip() - if re.match('^// .+', nextline): + if re.match(r"^// .+", nextline): url += nextline[2:].strip() else: break diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index 0c4fade509..9aa75b9074 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -36,7 +36,7 @@ `Add your logo `__. -Example usages""" # noqa +Example usages""" def main(): diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index d8be58a28f..610a201822 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -18,7 +18,7 @@ import subprocess import sys -import pypinfo # NOQA +import pypinfo # noqa: F401 from psutil._common import memoize diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index 6dec932b19..940724129b 100755 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -14,7 +14,7 @@ - {date}: `{ver} `__ - `what's new `__ - - `diff `__""" # NOQA + `diff `__""" # noqa: E501 def sh(cmd): diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 1692c12534..41fb6e7469 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -33,7 +33,8 @@ sys.path.insert(0, ROOT_DIR) # so that we can import setup.py -import setup # NOQA +import setup # noqa: E402 + TEST_DEPS = setup.TEST_DEPS DEV_DEPS = setup.DEV_DEPS @@ -239,7 +240,7 @@ def uninstall(): os.chdir('C:\\') while True: try: - import psutil # NOQA + import psutil # noqa: F401 except ImportError: break else: diff --git a/scripts/pidof.py b/scripts/pidof.py index 7c3b93d8ab..c5af54c85e 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -21,10 +21,8 @@ def pidof(pgname): pids = [] for proc in psutil.process_iter(['name', 'cmdline']): # search for matches in the process name and cmdline - if ( - proc.info['name'] == pgname - or proc.info['cmdline'] - and proc.info['cmdline'][0] == pgname + if proc.info["name"] == pgname or ( + proc.info["cmdline"] and proc.info["cmdline"][0] == pgname ): pids.append(str(proc.pid)) return pids diff --git a/setup.py b/setup.py index 03077624fd..0014484a86 100755 --- a/setup.py +++ b/setup.py @@ -58,17 +58,17 @@ # ...so we can import _common.py sys.path.insert(0, os.path.join(HERE, "psutil")) -from _common import AIX # NOQA -from _common import BSD # NOQA -from _common import FREEBSD # NOQA -from _common import LINUX # NOQA -from _common import MACOS # NOQA -from _common import NETBSD # NOQA -from _common import OPENBSD # NOQA -from _common import POSIX # NOQA -from _common import SUNOS # NOQA -from _common import WINDOWS # NOQA -from _common import hilite # NOQA +from _common import AIX # noqa: E402 +from _common import BSD # noqa: E402 +from _common import FREEBSD # noqa: E402 +from _common import LINUX # noqa: E402 +from _common import MACOS # noqa: E402 +from _common import NETBSD # noqa: E402 +from _common import OPENBSD # noqa: E402 +from _common import POSIX # noqa: E402 +from _common import SUNOS # noqa: E402 +from _common import WINDOWS # noqa: E402 +from _common import hilite # noqa: E402 PYPY = '__pypy__' in sys.builtin_module_names From d68cedb8a29b06e318ace03fd3e617e58d0d1d4b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2024 13:15:45 +0100 Subject: [PATCH 1180/1714] remove deprecated memory_info_ex() --- psutil/__init__.py | 6 +----- psutil/tests/__init__.py | 1 - scripts/internal/print_api_speed.py | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index bc17aabf18..d5e2550d2d 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1121,10 +1121,6 @@ def memory_info(self): """ return self._proc.memory_info() - @_common.deprecated_method(replacement="memory_info") - def memory_info_ex(self): - return self.memory_info() - def memory_full_info(self): """This method returns the same information as memory_info(), plus, on some platform (Linux, macOS, Windows), also provides @@ -1358,7 +1354,7 @@ def wait(self, timeout=None): 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', 'connections', 'oneshot'} + 'connections', 'oneshot'} } # fmt: on diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 3817f8b12a..e37b84297d 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1424,7 +1424,6 @@ class process_namespace: ('children', (), {'recursive': True}), ('connections', (), {}), # deprecated ('is_running', (), {}), - ('memory_info_ex', (), {}), # deprecated ('oneshot', (), {}), ('parent', (), {}), ('parents', (), {}), diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index e421d83d8c..1fdbc83d39 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -191,7 +191,6 @@ def main(): 'as_dict', 'parent', 'parents', - 'memory_info_ex', 'oneshot', 'pid', 'rlimit', From 3be9efe569973a40fc3a0d55362c6b99eadec5e6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2024 13:20:25 +0100 Subject: [PATCH 1181/1714] update doc and history --- HISTORY.rst | 10 +++++++--- docs/index.rst | 7 ------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 33df44fc68..39bb66157e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,12 +7,16 @@ XXXX-XX-XX **Enhancements** -- 2480_: Dropped Python 2.7 support. +- 2480_: Python 2.7 is no longer supported. Latest version supporting Python + 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. +- 2490_: removed long deprecated ``Process.memory_info_ex()`` method. It was + deprecated in psutil 4.0.0, released 8 years ago. Substitute is + ``Process.memory_full_info()``. **Compatibility notes** -- 2480_: Python 2.7 is no longer supported. Latest version supporting Python - 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. +- 2480_: Python 2.7 is no longer supported. +- 2490_: removed long deprecated ``Process.memory_info_ex()`` method. 6.1.1 ===== diff --git a/docs/index.rst b/docs/index.rst index 443c46eeda..bde76d0b53 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1706,13 +1706,6 @@ Process class .. versionchanged:: 4.0.0 multiple fields are returned, not only `rss` and `vms`. - .. method:: memory_info_ex() - - Same as :meth:`memory_info` (deprecated). - - .. warning:: - deprecated in version 4.0.0; use :meth:`memory_info` instead. - .. method:: memory_full_info() This method returns the same information as :meth:`memory_info`, plus, on From f4b5498f30a94d703a351ac8241ac080a0f6cd28 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2024 13:29:06 +0100 Subject: [PATCH 1182/1714] relax test tolerance --- psutil/tests/test_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index f2eb40548c..acd3dc9ed1 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -494,7 +494,7 @@ def test_cpu_times_comparison(self): with self.subTest(field=field, base=base, per_cpu=per_cpu): assert ( abs(getattr(base, field) - getattr(summed_values, field)) - < 1.5 + < 2 ) def _test_cpu_percent(self, percent, last_ret, new_ret): From d9b2bac4c61a23f4d5a12e12ad05265a2a8fa901 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Dec 2024 13:59:36 +0100 Subject: [PATCH 1183/1714] relax test tolerance --- psutil/tests/test_system.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index acd3dc9ed1..167172f0eb 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -483,6 +483,7 @@ def test_per_cpu_times_2(self): @pytest.mark.skipif( CI_TESTING and OPENBSD, reason="unreliable on OPENBSD + CI" ) + @retry_on_failure(30) 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 From de778ff674c4a4e2a5d5cf5cf12d52eb1d619ebb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Dec 2024 16:58:41 +0100 Subject: [PATCH 1184/1714] more ruff rules --- psutil/__init__.py | 4 ++-- psutil/_pslinux.py | 10 +++++----- psutil/_pswindows.py | 2 +- psutil/tests/__init__.py | 14 +++++++++----- psutil/tests/test_linux.py | 12 ++++++------ psutil/tests/test_process.py | 15 +++++++-------- psutil/tests/test_process_all.py | 4 +--- psutil/tests/test_system.py | 2 +- psutil/tests/test_windows.py | 2 +- pyproject.toml | 7 ++----- scripts/internal/bench_oneshot.py | 3 +-- scripts/internal/check_broken_links.py | 3 +-- scripts/pidof.py | 15 +++++++-------- setup.py | 4 +--- 14 files changed, 45 insertions(+), 52 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index d5e2550d2d..cabfb13046 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -208,7 +208,7 @@ __author__ = "Giampaolo Rodola'" __version__ = "7.0.0" -version_info = tuple([int(num) for num in __version__.split('.')]) +version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) _TOTAL_PHYMEM = None @@ -227,7 +227,7 @@ msg = f"version conflict: {_psplatform.cext.__file__!r} C extension " msg += "module was built for another version of psutil" if hasattr(_psplatform.cext, 'version'): - v = ".".join([x for x in str(_psplatform.cext.version)]) + v = ".".join(list(str(_psplatform.cext.version))) msg += f" ({v} instead of {__version__})" else: msg += f" (different than {__version__})" diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 165125fdf5..0a966ed069 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -676,12 +676,12 @@ def cpu_stats(): def _cpu_get_cpuinfo_freq(): """Return current CPU frequency from cpuinfo if available.""" - ret = [] with open_binary(f"{get_procfs_path()}/cpuinfo") as f: - for line in f: - if line.lower().startswith(b'cpu mhz'): - ret.append(float(line.split(b':', 1)[1])) - return ret + return [ + float(line.split(b':', 1)[1]) + for line in f + if line.lower().startswith(b'cpu mhz') + ] if os.path.exists("/sys/devices/system/cpu/cpufreq/policy0") or os.path.exists( diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 69820ba419..e5af3c90f4 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -337,7 +337,7 @@ def getloadavg(): # Drop to 2 decimal points which is what Linux does raw_loads = cext.getloadavg() - return tuple([round(load, 2) for load in raw_loads]) + return tuple(round(load, 2) for load in raw_loads) # ===================================================================== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index e37b84297d..087eff09b6 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1346,7 +1346,7 @@ def print_sysinfo(): # metrics info['cpus'] = psutil.cpu_count() info['loadavg'] = "{:.1f}%, {:.1f}%, {:.1f}%".format( - *tuple([x / psutil.cpu_count() * 100 for x in psutil.getloadavg()]) + *tuple(x / psutil.cpu_count() * 100 for x in psutil.getloadavg()) ) mem = psutil.virtual_memory() info['memory'] = "{}%%, used={}, total={}".format( @@ -1766,11 +1766,15 @@ def create_sockets(): 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)) + socks.extend(( + bind_socket(socket.AF_INET, socket.SOCK_STREAM), + 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)) + socks.extend(( + bind_socket(socket.AF_INET6, socket.SOCK_STREAM), + bind_socket(socket.AF_INET6, socket.SOCK_DGRAM), + )) if POSIX and HAS_NET_CONNECTIONS_UNIX: fname1 = get_testfn() fname2 = get_testfn() diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 3672bc9591..4a7e66964f 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -114,9 +114,9 @@ def get_ipv6_addresses(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]) + groups = [ + unformatted[j : j + 4] for j in range(0, len(unformatted), 4) + ] formatted = ":".join(groups) packed = socket.inet_pton(socket.AF_INET6, formatted) all_fields[i] = socket.inet_ntop(socket.AF_INET6, packed) @@ -1823,11 +1823,11 @@ def test_parse_smaps_vs_memory_maps(self): uss, pss, swap = psutil._pslinux.Process(sproc.pid)._parse_smaps() maps = psutil.Process(sproc.pid).memory_maps(grouped=False) assert ( - abs(uss - sum([x.private_dirty + x.private_clean for x in maps])) + abs(uss - sum(x.private_dirty + x.private_clean for x in maps)) < 4096 ) - assert abs(pss - sum([x.pss for x in maps])) < 4096 - assert abs(swap - sum([x.swap for x in maps])) < 4096 + assert abs(pss - sum(x.pss for x in maps)) < 4096 + assert abs(swap - sum(x.swap for x in maps)) < 4096 def test_parse_smaps_mocked(self): # See: https://github.com/giampaolo/psutil/issues/1222 diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 1d81d498f5..c62e6de63e 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -575,14 +575,11 @@ def test_threads_2(self): except psutil.AccessDenied: raise pytest.skip("on OpenBSD this requires root access") assert ( - abs(p.cpu_times().user - sum([x.user_time for x in p.threads()])) + abs(p.cpu_times().user - sum(x.user_time for x in p.threads())) < 0.1 ) assert ( - abs( - p.cpu_times().system - - sum([x.system_time for x in p.threads()]) - ) + abs(p.cpu_times().system - sum(x.system_time for x in p.threads())) < 0.1 ) @@ -1043,9 +1040,11 @@ def test_cpu_affinity_all_combinations(self): 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)) + combos.extend( + list(subset) + for subset in itertools.combinations(initial, i) + if subset + ) for combo in combos: p.cpu_affinity(combo) diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index cb7264d735..4d641dfafb 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -121,9 +121,7 @@ def iter_proc_info(self): 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)) + ls = [proc_info(pid) for pid in psutil.pids()] return ls def test_all(self): diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 167172f0eb..b68a3388d6 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -68,7 +68,7 @@ def test_pid_presence(self): assert sproc.pid not in [x.pid for x in psutil.process_iter()] def test_no_duplicates(self): - ls = [x for x in psutil.process_iter()] + ls = list(psutil.process_iter()) assert sorted(ls, key=lambda x: x.pid) == sorted( set(ls), key=lambda x: x.pid ) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 9f54cf534c..c5c536b468 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -81,7 +81,7 @@ def wmic(path, what, converter=int): 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()]) + return tuple(converter(x) for x in data.split()) else: return converter(data) else: diff --git a/pyproject.toml b/pyproject.toml index f6eb772d8a..2827ca4d52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,7 @@ select = [ "D301", # Use `r"""` if any backslashes in a docstring "D403", # [*] First word of the first line should be capitalized "PERF102", # [*] When using only the keys of a dict use the `keys()` method - "RET502", # [*] Do not implicitly `return None` in function able to return non-`None` value - "RET506", # [*] Unnecessary `else` after `raise` statement - "RET507", # Unnecessary `elif` after `continue` statement + "PERF401", # Use a list comprehension to create a transformed list "S113", # Probable use of requests call without timeout "S602", # `subprocess` call with `shell=True` identified, security issue ] @@ -35,7 +33,7 @@ ignore = [ "ARG001", # unused-function-argument "ARG002", # unused-method-argument "B007", # Loop control variable `x` not used within loop body - "C4", # flake8-comprehensions + "C408", # Unnecessary dict() call "C90", # mccabe (function `X` is too complex) "COM812", # Trailing comma missing "D", # pydocstyle @@ -47,7 +45,6 @@ ignore = [ "FLY002", # static-join-to-f-string / Consider {expression} instead of string join "FURB101", # `open` and `read` should be replaced by `Path(src).read_text()` "FURB103", # `open` and `write` should be replaced by `Path(src).write_text()` - "FURB113", # Use `ls.extend(...)` instead of repeatedly calling `ls.append()` "FURB116", # [*] Replace `hex` call with `f"{start:x}"` "FURB118", # [*] Use `operator.add` instead of defining a lambda "FURB140", # [*] Use `itertools.starmap` instead of the generator diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 299f9cea67..f4a5eab98f 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -31,8 +31,7 @@ ] if psutil.POSIX: - names.append('uids') - names.append('username') + names.extend(('uids', 'username')) if psutil.LINUX: names += [ diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 2c6852b9a5..79cfd09a18 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -243,8 +243,7 @@ def main(): urls = get_urls(fname) if urls: print(f"{len(urls):4} {fname}") - for url in urls: - all_urls.append((fname, url)) + all_urls.extend((fname, url) for url in urls) fails = parallel_validator(all_urls) if not fails: diff --git a/scripts/pidof.py b/scripts/pidof.py index c5af54c85e..7feb464f3f 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -18,14 +18,13 @@ def pidof(pgname): - pids = [] - for proc in psutil.process_iter(['name', 'cmdline']): - # search for matches in the process name and cmdline - if proc.info["name"] == pgname or ( - proc.info["cmdline"] and proc.info["cmdline"][0] == pgname - ): - pids.append(str(proc.pid)) - return pids + # search for matches in the process name and cmdline + return [ + str(proc.pid) + for proc in psutil.process_iter(['name', 'cmdline']) + if proc.info["name"] == pgname + or (proc.info["cmdline"] and proc.info["cmdline"][0] == pgname) + ] def main(): diff --git a/setup.py b/setup.py index 0014484a86..13c3ae96b7 100755 --- a/setup.py +++ b/setup.py @@ -87,9 +87,7 @@ ] if WINDOWS and not PYPY: - TEST_DEPS.append("pywin32") - TEST_DEPS.append("wheel") - TEST_DEPS.append("wmi") + TEST_DEPS.extend(("pywin32", "wheel", "wmi")) # Development deps, installable via `pip install .[dev]` or # `make install-pydeps-dev`. From ed1d42549ba45f12d43792060f03eb091817efd9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 11 Jan 2025 21:54:41 +0100 Subject: [PATCH 1185/1714] some pylint fixes --- psutil/_pslinux.py | 8 ++------ psutil/tests/__init__.py | 2 +- psutil/tests/test_process.py | 6 +++--- psutil/tests/test_system.py | 8 ++++---- psutil/tests/test_testutils.py | 4 +--- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 0a966ed069..ce47b52ad3 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -2005,12 +2005,8 @@ def get_blocks(lines, current_block): if fields[0].startswith(b'VmFlags:'): # see issue #369 continue - else: - msg = ( - "don't know how to interpret line" - f" {line!r}" - ) - raise ValueError(msg) from None + msg = f"don't know how to interpret line {line!r}" + raise ValueError(msg) from None yield (current_block.pop(), data) data = self._read_smaps_file() diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 087eff09b6..89aeb2ac0d 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -2007,7 +2007,7 @@ def copyload_shared_lib(suffix=""): FreeLibrary.argtypes = [wintypes.HMODULE] ret = FreeLibrary(cfile._handle) if ret == 0: - WinError() + raise WinError() safe_rmpath(dst) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index c62e6de63e..ed1f0f93a9 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1178,8 +1178,8 @@ def test_parents(self): def test_children(self): parent = psutil.Process() - assert parent.children() == [] - assert parent.children(recursive=True) == [] + assert not parent.children() + assert not 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. @@ -1202,7 +1202,7 @@ def test_children_recursive(self): # children() to recursively find it. child.terminate() child.wait() - assert parent.children(recursive=True) == [] + assert not parent.children(recursive=True) def test_children_duplicates(self): # find the process which has the highest number of children diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index b68a3388d6..e2efca149f 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -80,7 +80,7 @@ def test_emulate_nsp(self): 'psutil.Process.as_dict', side_effect=psutil.NoSuchProcess(os.getpid()), ): - assert list(psutil.process_iter(attrs=["cpu_times"])) == [] + assert not list(psutil.process_iter(attrs=["cpu_times"])) psutil.process_iter.cache_clear() # repeat test without cache def test_emulate_access_denied(self): @@ -151,9 +151,9 @@ def callback(p): gone, alive = psutil.wait_procs(procs, timeout=0.01, callback=callback) assert time.time() - t < 0.5 - assert gone == [] + assert not gone assert len(alive) == 3 - assert pids == [] + assert not pids for p in alive: assert not hasattr(p, 'returncode') @@ -243,7 +243,7 @@ def test_boot_time(self): ) def test_users(self): users = psutil.users() - assert users != [] + assert users for user in users: with self.subTest(user=user): assert user.name diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 85f20c14e7..6db66e50eb 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -328,9 +328,7 @@ def test_tcp_socketpair(self): def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() - assert ( - filter_proc_net_connections(p.net_connections(kind='unix')) == [] - ) + assert not filter_proc_net_connections(p.net_connections(kind='unix')) name = self.get_testfn() server, client = unix_socketpair(name) try: From d4b37d9628e634c00a2990e488b1cc38ea6a41b5 Mon Sep 17 00:00:00 2001 From: Will H Date: Sun, 12 Jan 2025 17:17:43 +0000 Subject: [PATCH 1186/1714] Avoid segfault for processes that use hundreds of GBs of memory (#2497) --- CREDITS | 4 ++++ HISTORY.rst | 5 +++++ psutil/__init__.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 35f4b6b4ae..6d7dd113f5 100644 --- a/CREDITS +++ b/CREDITS @@ -840,3 +840,7 @@ N: Aleksey Lobanov C: Russia E: alex_github@likemath.ru W: https://github.com/AlekseyLobanov + +N: Will Hawes +W: https://github.com/wdh +I: 2496 diff --git a/HISTORY.rst b/HISTORY.rst index 39bb66157e..5b32811e5b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,11 @@ XXXX-XX-XX deprecated in psutil 4.0.0, released 8 years ago. Substitute is ``Process.memory_full_info()``. +**Bug fixes** + +- 2496_, [Linux]: Avoid segfault (a cPython bug) on ``Process.memory_maps()`` + for processes that use hundreds of GBs of memory. + **Compatibility notes** - 2480_: Python 2.7 is no longer supported. diff --git a/psutil/__init__.py b/psutil/__init__.py index cabfb13046..605138e226 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1193,7 +1193,7 @@ def memory_maps(self, grouped=True): path = tupl[2] nums = tupl[3:] try: - d[path] = map(lambda x, y: x + y, d[path], nums) + d[path] = list(map(lambda x, y: x + y, d[path], nums)) except KeyError: d[path] = nums nt = _psplatform.pmmap_grouped From a509e5aa1829c0268cd4d069ac340a5d9fb4fee8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Jan 2025 15:26:03 +0100 Subject: [PATCH 1187/1714] 669 windows broadcast addr (#2501) --- HISTORY.rst | 4 +++- docs/index.rst | 3 +++ psutil/__init__.py | 18 +++++++++++++++++- psutil/_common.py | 30 ++++++++++++++++++++++++++---- psutil/tests/test_system.py | 9 +++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5b32811e5b..cd7ad7e88e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ XXXX-XX-XX **Enhancements** +- 669_, [Windows]: `net_if_addrs()`_ also returns the ``broadcast`` address + instead of ``None``. - 2480_: Python 2.7 is no longer supported. Latest version supporting Python 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. - 2490_: removed long deprecated ``Process.memory_info_ex()`` method. It was @@ -15,7 +17,7 @@ XXXX-XX-XX **Bug fixes** -- 2496_, [Linux]: Avoid segfault (a cPython bug) on ``Process.memory_maps()`` +- 2496_, [Linux]: Avoid segfault (a cPython bug) on ``Process.memory_maps()`` for processes that use hundreds of GBs of memory. **Compatibility notes** diff --git a/docs/index.rst b/docs/index.rst index bde76d0b53..3fd122cc7c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -734,6 +734,9 @@ Network .. versionchanged:: 4.4.0 added support for *netmask* field on Windows which is no longer ``None``. + .. versionchanged:: 7.0.0 added support for *broadcast* field on Windows + which is no longer ``None``. + .. function:: net_if_stats() Return information about each NIC (network interface card) installed on the diff --git a/psutil/__init__.py b/psutil/__init__.py index 605138e226..cf4a58057f 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2228,6 +2228,7 @@ def net_if_addrs(): # We re-set the family here so that repr(family) # will show AF_LINK rather than AF_PACKET fam = _psplatform.AF_LINK + if fam == _psplatform.AF_LINK: # The underlying C function may return an incomplete MAC # address in which case we fill it with null bytes, see: @@ -2235,7 +2236,22 @@ def net_if_addrs(): separator = ":" if POSIX else "-" while addr.count(separator) < 5: addr += f"{separator}00" - ret[name].append(_common.snicaddr(fam, addr, mask, broadcast, ptp)) + + nt = _common.snicaddr(fam, addr, mask, broadcast, ptp) + + # On Windows broadcast is None, so we determine it via + # ipaddress module. + if WINDOWS and fam in {socket.AF_INET, socket.AF_INET6}: + try: + broadcast = _common.broadcast_addr(nt) + except Exception as err: # noqa: BLE001 + debug(err) + else: + if broadcast is not None: + nt._replace(broadcast=broadcast) + + ret[name].append(nt) + return dict(ret) diff --git a/psutil/_common.py b/psutil/_common.py index 0c385fa6ae..c8bcc76080 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -2,11 +2,11 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Common objects shared by __init__.py and _ps*.py modules.""" - -# Note: this module is imported by setup.py so it should not import -# psutil or third-party modules. +"""Common objects shared by __init__.py and _ps*.py modules. +Note: this module is imported by setup.py, so it should not import +psutil or third-party modules. +""" import collections import enum @@ -603,6 +603,28 @@ def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): return sconn(fd, fam, type_, laddr, raddr, status, pid) +def broadcast_addr(addr): + """Given the address ntuple returned by ``net_if_addrs()`` + calculates the broadcast address. + """ + import ipaddress + + if not addr.address or not addr.netmask: + return None + if addr.family == socket.AF_INET: + return str( + ipaddress.IPv4Network( + f"{addr.address}/{addr.netmask}", strict=False + ).broadcast_address + ) + if addr.family == socket.AF_INET6: + return str( + ipaddress.IPv6Network( + f"{addr.address}/{addr.netmask}", strict=False + ).broadcast_address + ) + + 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. diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index e2efca149f..e28e04b4a2 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -30,6 +30,7 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil._common import broadcast_addr from psutil.tests import ASCII_FS from psutil.tests import CI_TESTING from psutil.tests import GITHUB_ACTIONS @@ -858,6 +859,14 @@ def test_net_if_addrs(self): elif addr.ptp: assert addr.broadcast is None + # check broadcast address + if ( + addr.broadcast + and addr.netmask + and addr.family in {socket.AF_INET, socket.AF_INET6} + ): + assert addr.broadcast == broadcast_addr(addr) + if BSD or MACOS or SUNOS: if hasattr(socket, "AF_LINK"): assert psutil.AF_LINK == socket.AF_LINK From 08d7d43894e9d6916a6f83184dc491506857fbc2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 31 Jan 2025 17:18:43 +0100 Subject: [PATCH 1188/1714] pin black version to 24.X, because new 25.X breaks style Signed-off-by: Giampaolo Rodola --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 13c3ae96b7..df478e7428 100755 --- a/setup.py +++ b/setup.py @@ -93,7 +93,7 @@ # `make install-pydeps-dev`. DEV_DEPS = TEST_DEPS + [ "abi3audit", - "black", + "black==24.10.0", "check-manifest", "coverage", "packaging", From 9c114a513757446d3e2b13533f458bdfac2d8881 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 31 Jan 2025 17:22:38 +0100 Subject: [PATCH 1189/1714] [OSX] use `host_statistics64` to get memory metrics (#2502) We have lots of sporadic failures on OSX related to free virtual_memory(), whose value does not exactly match vm_stat CLI utility. With this PR we use the exact same approach of vm_stat CLI tool, whose source code is here: https://github.com/apple-open-source/macos/blob/master/system_cmds/vm_stat/vm_stat.c Hopefully this will reduce such sporadic failures. --- HISTORY.rst | 3 +++ psutil/_psosx.py | 3 +-- psutil/_psutil_osx.c | 1 + psutil/arch/osx/mem.c | 13 ++++++++----- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index cd7ad7e88e..d704cc4600 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,9 @@ XXXX-XX-XX - 2496_, [Linux]: Avoid segfault (a cPython bug) on ``Process.memory_maps()`` for processes that use hundreds of GBs of memory. +- 2502_, [macOS]: `virtual_memory()`_ now relies on ``host_statistics64`` + instead of ``host_statistics``. This is the same approach used by ``vm_stat`` + CLI tool, and should grant more accurate results. **Compatibility notes** diff --git a/psutil/_psosx.py b/psutil/_psosx.py index fa2c8b81d0..620497b30a 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -112,8 +112,7 @@ def virtual_memory(): """System virtual memory as a namedtuple.""" 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 + # https://github.com/zabbix/zabbix/blob/master/src/libs/zbxsysinfo/osx/memory.c # Also see: https://github.com/giampaolo/psutil/issues/1277 avail = inactive + free used = active + wired diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index d9a486fe57..b16103379b 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -7,6 +7,7 @@ */ #include +#include // needed for old macOS versions #include #include diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 53493065c2..8103876070 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -8,6 +8,9 @@ // from psutil/_psutil_osx.c in 2023. This is the GIT blame before the move: // https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c +// See: +// https://github.com/apple-open-source/macos/blob/master/system_cmds/vm_stat/vm_stat.c + #include #include #include @@ -17,12 +20,12 @@ static int -psutil_sys_vminfo(vm_statistics_data_t *vmstat) { +psutil_sys_vminfo(vm_statistics64_t vmstat) { kern_return_t ret; - mach_msg_type_number_t count = sizeof(*vmstat) / sizeof(integer_t); + unsigned int count = HOST_VM_INFO64_COUNT; mach_port_t mport = mach_host_self(); - ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)vmstat, &count); + ret = host_statistics64(mport, HOST_VM_INFO64, (host_info64_t)vmstat, &count); if (ret != KERN_SUCCESS) { PyErr_Format( PyExc_RuntimeError, @@ -46,7 +49,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { int mib[2]; uint64_t total; size_t len = sizeof(total); - vm_statistics_data_t vm; + vm_statistics64_data_t vm; long pagesize = psutil_getpagesize(); // physical mem mib[0] = CTL_HW; @@ -86,7 +89,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { int mib[2]; size_t size; struct xsw_usage totals; - vm_statistics_data_t vmstat; + vm_statistics64_data_t vmstat; long pagesize = psutil_getpagesize(); mib[0] = CTL_VM; From 1ba8667c89d80974c37594984183ce404e53dfd9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 31 Jan 2025 17:28:15 +0100 Subject: [PATCH 1190/1714] pin black version to 24.X, because new 25.X breaks style --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f609ee6513..ad48afee81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -98,7 +98,7 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install ruff black rstcheck toml-sort sphinx + python3 -m pip install ruff black==24.10.0 rstcheck toml-sort sphinx make lint-all # Produce wheels as artifacts. From 17e27801e6afe4a72d7eca9285e63f1babf822a0 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Sun, 2 Feb 2025 09:43:37 +0100 Subject: [PATCH 1191/1714] ci: build aarch64 wheel on GHA aarch64 runner (#2503) Signed-off-by: mayeut --- .github/workflows/build.yml | 11 ++--------- .gitignore | 1 + Makefile | 7 +++++++ psutil/tests/__init__.py | 7 +------ psutil/tests/test_contracts.py | 2 -- psutil/tests/test_linux.py | 12 ++++++++---- psutil/tests/test_memleaks.py | 3 --- psutil/tests/test_misc.py | 4 ---- psutil/tests/test_posix.py | 4 ---- psutil/tests/test_process.py | 17 +---------------- psutil/tests/test_process_all.py | 4 ---- psutil/tests/test_scripts.py | 2 -- psutil/tests/test_system.py | 10 +++++----- pyproject.toml | 4 +++- 14 files changed, 28 insertions(+), 60 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad48afee81..fe72d9a68c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: include: - {os: ubuntu-latest, arch: x86_64} - {os: ubuntu-latest, arch: i686} - - {os: ubuntu-latest, arch: aarch64} + - {os: ubuntu-24.04-arm, arch: aarch64} - {os: macos-13, arch: x86_64} - {os: macos-14, arch: arm64} - {os: windows-2019, arch: AMD64} @@ -48,18 +48,11 @@ jobs: with: python-version: 3.11 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - if: matrix.arch == 'aarch64' - - name: Create wheels + run tests uses: pypa/cibuildwheel@v2.22.0 env: CIBW_ARCHS: "${{ matrix.arch }}" - CIBW_ENABLE: "cpython-prerelease" - CIBW_TEST_EXTRAS: test - CIBW_TEST_COMMAND: - make -C {project} PYTHON="env python" PSUTIL_SCRIPTS_DIR="{project}/scripts" install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks + CIBW_ENABLE: "${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" - name: Upload wheels uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index ddafc64c6a..d9f3d671bc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ syntax: glob build/ dist/ wheelhouse/ +.tests/ diff --git a/Makefile b/Makefile index 3e74e8c7d2..c0acf3bb23 100644 --- a/Makefile +++ b/Makefile @@ -162,6 +162,13 @@ test-coverage: ## Run test coverage. $(PYTHON) -m coverage html $(PYTHON) -m webbrowser -t htmlcov/index.html +test-ci: + ${MAKE} install-sysdeps + mkdir -p .tests + cd .tests/ && $(PYTHON) -c "from psutil.tests import print_sysinfo; print_sysinfo()" + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest $(PYTEST_ARGS) --pyargs psutil.tests + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'test_memleaks.py'" $(PYTHON) -m pytest $(PYTEST_ARGS) --pyargs psutil.tests + # =================================================================== # Linters # =================================================================== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 89aeb2ac0d..5d4b3abb79 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -73,7 +73,7 @@ "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", - "MACOS_12PLUS", "COVERAGE", 'AARCH64', "QEMU_USER", "PYTEST_PARALLEL", + "MACOS_12PLUS", "COVERAGE", 'AARCH64', "PYTEST_PARALLEL", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', 'spawn_children_pair', @@ -114,11 +114,6 @@ CI_TESTING = GITHUB_ACTIONS COVERAGE = 'COVERAGE_RUN' in os.environ PYTEST_PARALLEL = "PYTEST_XDIST_WORKER" in os.environ # `make test-parallel` -if LINUX and GITHUB_ACTIONS: - with open('/proc/1/cmdline') as f: - QEMU_USER = "/bin/qemu-" in f.read() -else: - QEMU_USER = False # are we a 64 bit process? IS_64BIT = sys.maxsize > 2**32 AARCH64 = platform.machine() == "aarch64" diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 7406d98ad0..55f3a5ddb8 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -27,7 +27,6 @@ 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 QEMU_USER from psutil.tests import SKIP_SYSCONS from psutil.tests import PsutilTestCase from psutil.tests import create_sockets @@ -268,7 +267,6 @@ def test_net_if_addrs(self): assert isinstance(addr.netmask, (str, type(None))) assert isinstance(addr.broadcast, (str, type(None))) - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_stats(self): # Duplicate of test_system.py. Keep it anyway. for ifname, info in psutil.net_if_stats().items(): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 4a7e66964f..f4342d7aa3 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -12,6 +12,7 @@ import errno import io import os +import platform import re import shutil import socket @@ -32,7 +33,6 @@ from psutil.tests import HAS_RLIMIT from psutil.tests import PYPY from psutil.tests import PYTEST_PARALLEL -from psutil.tests import QEMU_USER from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase @@ -735,6 +735,9 @@ def test_against_lscpu(self): core_ids.add(fields[1]) assert psutil.cpu_count(logical=False) == len(core_ids) + @pytest.mark.skipif( + platform.machine() not in {"x86_64", "i686"}, reason="x86_64/i686 only" + ) def test_method_2(self): meth_1 = psutil._pslinux.cpu_count_cores() with mock.patch('glob.glob', return_value=[]) as m: @@ -754,6 +757,9 @@ def test_emulate_none(self): @pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUFrequency(PsutilTestCase): @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + @pytest.mark.skipif( + AARCH64, reason="aarch64 does not always expose frequency" + ) def test_emulate_use_second_file(self): # https://github.com/giampaolo/psutil/issues/981 def path_exists_mock(path): @@ -977,7 +983,6 @@ def test_ips(self): @pytest.mark.skipif(not LINUX, reason="LINUX only") -@pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") class TestSystemNetIfStats(PsutilTestCase): @pytest.mark.skipif( not shutil.which("ifconfig"), reason="ifconfig utility not available" @@ -1546,7 +1551,7 @@ def test_issue_687(self): with ThreadTask(): p = psutil.Process() threads = p.threads() - assert len(threads) == (3 if QEMU_USER else 2) + assert len(threads) == 2 tid = sorted(threads, key=lambda x: x.id)[1].id assert p.pid != tid pt = psutil.Process(tid) @@ -2227,7 +2232,6 @@ def test_name(self): value = self.read_status_file("Name:") assert self.proc.name() == value - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_status(self): value = self.read_status_file("State:") value = value[value.find('(') + 1 : value.rfind(')')] diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index fd4cd09a48..7f78fae67c 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -39,7 +39,6 @@ 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 QEMU_USER from psutil.tests import TestMemoryLeak from psutil.tests import create_sockets from psutil.tests import get_testfn @@ -396,7 +395,6 @@ def test_disk_usage(self): times = FEW_TIMES if POSIX else self.times self.execute(lambda: psutil.disk_usage('.'), times=times) - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_disk_partitions(self): self.execute(psutil.disk_partitions) @@ -434,7 +432,6 @@ def test_net_if_addrs(self): tolerance = 80 * 1024 if WINDOWS else self.tolerance self.execute(psutil.net_if_addrs, tolerance=tolerance) - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_stats(self): self.execute(psutil.net_if_stats) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 848b9284e8..c484264b91 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -29,7 +29,6 @@ from psutil._common import supports_ipv6 from psutil._common import wrap_numbers from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import process_namespace from psutil.tests import pytest @@ -269,9 +268,6 @@ def check(ret): for fun, name in ns.iter(ns.getters): if name in {"win_service_iter", "win_service_get"}: continue - if QEMU_USER and name == "net_if_stats": - # OSError: [Errno 38] ioctl(SIOCETHTOOL) not implemented - continue with self.subTest(name=name): try: ret = fun() diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 93e6df6e3a..a7844929e7 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -26,7 +26,6 @@ from psutil.tests import AARCH64 from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import PYTHON_EXE -from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import pytest from psutil.tests import retry_on_failure @@ -103,9 +102,6 @@ def ps_name(pid): if SUNOS: field = "comm" command = ps(field, pid).split() - if QEMU_USER: - assert "/bin/qemu-" in command[0] - return command[1] return command[0] diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index ed1f0f93a9..e85a551621 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -49,7 +49,6 @@ from psutil.tests import PYPY from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV -from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import ThreadTask from psutil.tests import call_until @@ -257,7 +256,6 @@ def test_cpu_percent_numcpus_none(self): psutil.Process().cpu_percent() assert m.called - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_cpu_times(self): times = psutil.Process().cpu_times() assert times.user >= 0.0, times @@ -270,7 +268,6 @@ def test_cpu_times(self): for name in times._fields: time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_cpu_times_2(self): user_time, kernel_time = psutil.Process().cpu_times()[:2] utime, ktime = os.times()[:2] @@ -642,8 +639,6 @@ def test_memory_maps(self): continue if BSD and nt.path == "pvclock": continue - if QEMU_USER and "/bin/qemu-" in nt.path: - continue assert os.path.isabs(nt.path), nt.path if POSIX: @@ -710,7 +705,6 @@ def test_is_running(self): assert not p.is_running() assert not p.is_running() - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_exe(self): p = self.spawn_psproc() exe = p.exe() @@ -763,9 +757,6 @@ def test_cmdline(self): if pyexe != PYTHON_EXE: assert ' '.join(p.cmdline()[1:]) == ' '.join(cmdline[1:]) return - if QEMU_USER: - assert ' '.join(p.cmdline()[2:]) == ' '.join(cmdline) - return assert ' '.join(p.cmdline()) == ' '.join(cmdline) @pytest.mark.skipif(PYPY, reason="broken on PYPY") @@ -783,8 +774,6 @@ def test_long_cmdline(self): assert p.cmdline() == cmdline except psutil.ZombieProcess: raise pytest.skip("OPENBSD: process turned into zombie") - elif QEMU_USER: - assert p.cmdline()[2:] == cmdline else: ret = p.cmdline() if NETBSD and ret == []: @@ -798,8 +787,7 @@ def test_name(self): pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) - @pytest.mark.skipif(PYPY or QEMU_USER, reason="unreliable on PYPY") - @pytest.mark.skipif(QEMU_USER, reason="unreliable on QEMU user") + @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) cmdline = [ @@ -830,7 +818,6 @@ def test_long_name(self): # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") # @pytest.mark.skipif(AIX, reason="broken on AIX") # @pytest.mark.skipif(PYPY, reason="broken on PYPY") - # @pytest.mark.skipif(QEMU_USER, reason="broken on QEMU user") # def test_prog_w_funky_name(self): # # Test that name(), exe() and cmdline() correctly handle programs # # with funky chars such as spaces and ")", see: @@ -938,7 +925,6 @@ def cleanup(init): except psutil.AccessDenied: pass - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_status(self): p = psutil.Process() assert p.status() == psutil.STATUS_RUNNING @@ -1166,7 +1152,6 @@ def test_parent_multi(self): assert grandchild.parent() == child assert child.parent() == parent - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") @retry_on_failure() def test_parents(self): parent = psutil.Process() diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 4d641dfafb..aaa3fa01df 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -29,7 +29,6 @@ from psutil import WINDOWS from psutil.tests import CI_TESTING from psutil.tests import PYTEST_PARALLEL -from psutil.tests import QEMU_USER from psutil.tests import VALID_PROC_STATUSES from psutil.tests import PsutilTestCase from psutil.tests import check_connection_ntuple @@ -232,9 +231,6 @@ def username(self, ret, info): def status(self, ret, info): assert isinstance(ret, str) assert ret, ret - if QEMU_USER: - # status does not work under qemu user - return assert ret != '?' # XXX assert ret in VALID_PROC_STATUSES diff --git a/psutil/tests/test_scripts.py b/psutil/tests/test_scripts.py index c354d2ad3b..de0ad2af74 100755 --- a/psutil/tests/test_scripts.py +++ b/psutil/tests/test_scripts.py @@ -24,7 +24,6 @@ from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV -from psutil.tests import QEMU_USER from psutil.tests import ROOT_DIR from psutil.tests import SCRIPTS_DIR from psutil.tests import PsutilTestCase @@ -117,7 +116,6 @@ def test_pstree(self): def test_netstat(self): self.assert_stdout('netstat.py') - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_ifconfig(self): self.assert_stdout('ifconfig.py') diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index e28e04b4a2..b961e1f891 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -31,6 +31,7 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import broadcast_addr +from psutil.tests import AARCH64 from psutil.tests import ASCII_FS from psutil.tests import CI_TESTING from psutil.tests import GITHUB_ACTIONS @@ -45,7 +46,6 @@ from psutil.tests import IS_64BIT from psutil.tests import MACOS_12PLUS from psutil.tests import PYPY -from psutil.tests import QEMU_USER from psutil.tests import UNICODE_SUFFIX from psutil.tests import PsutilTestCase from psutil.tests import check_net_address @@ -598,8 +598,10 @@ def check_ls(ls): assert value >= 0 ls = psutil.cpu_freq(percpu=True) - if FREEBSD and not ls: - raise pytest.skip("returns empty list on FreeBSD") + if (FREEBSD or AARCH64) and not ls: + raise pytest.skip( + "returns empty list on FreeBSD and Linux aarch64" + ) assert ls, ls check_ls([psutil.cpu_freq(percpu=False)]) @@ -800,7 +802,6 @@ def test_net_io_counters_no_nics(self): assert psutil.net_io_counters(pernic=True) == {} assert m.called - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_addrs(self): nics = psutil.net_if_addrs() assert nics, nics @@ -893,7 +894,6 @@ def test_net_if_addrs_mac_null_bytes(self): else: assert addr.address == '06-3d-29-00-00-00' - @pytest.mark.skipif(QEMU_USER, reason="QEMU user not supported") def test_net_if_stats(self): nics = psutil.net_if_stats() assert nics, nics diff --git a/pyproject.toml b/pyproject.toml index 2827ca4d52..d1268ad8e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,9 +221,11 @@ trailing_comma_inline_array = true skip = [ "*-musllinux*", "cp313-win*", # pywin32 is not available on cp313 yet - "cp3{7,8,9,10,11,12}-*linux_{aarch64,ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures + "cp3{7,8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures "pp*", ] +test-extras = ["test"] +test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" test-ci" [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] From eee09da72a3dbff60f438b6f8153e985c59d285d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Duval?= Date: Tue, 11 Feb 2025 15:53:49 +0100 Subject: [PATCH 1192/1714] [OSX] proc.c: Fix goo.gl link in comment for source reference (#2505) --- psutil/_common.py | 2 +- psutil/_pslinux.py | 2 +- psutil/_pssunos.py | 2 +- psutil/arch/osx/proc.c | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index c8bcc76080..4096c0a18c 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -815,7 +815,7 @@ def bcat(fname, fallback=_DEFAULT): def bytes2human(n, format="%(value).1f%(symbol)s"): - """Used by various scripts. See: http://goo.gl/zeJZl. + """Used by various scripts. See: https://code.activestate.com/recipes/578019-bytes-to-human-human-to-bytes-converter/?in=user-4178764. >>> bytes2human(10000) '9.8K' diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ce47b52ad3..9254a0b134 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1984,7 +1984,7 @@ def memory_full_info(self): 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: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/proc.txt?id=b76437579d1344b612cf1851ae610c636cec7db0. /proc/{PID}/smaps does not exist on kernels < 2.6.14 or if CONFIG_MMU kernel configuration option is not enabled. diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index fdc9e7e007..78d941cc5f 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -537,7 +537,7 @@ def cwd(self): # /proc/PID/path/cwd may not be resolved by readlink() even if # it exists (ls shows it). If that's the case and the process # is still alive return None (we can return None also on BSD). - # Reference: http://goo.gl/55XgO + # Reference: https://groups.google.com/g/comp.unix.solaris/c/tcqvhTNFCAs procfs_path = self._procfs_path try: return os.readlink(f"{procfs_path}/{self.pid}/path/cwd") diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 136311ecee..681642c3ad 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -763,7 +763,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { /* * Return process open files as a Python tuple. * References: - * - lsof source code: http://goo.gl/SYW79 and http://goo.gl/m78fd + * - lsof source code: https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 * - /usr/include/sys/proc_info.h */ PyObject * @@ -854,7 +854,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { * Return process TCP and UDP connections as a list of tuples. * Raises NSP in case of zombie process. * References: - * - lsof source code: http://goo.gl/SYW79 and http://goo.gl/wNrC0 + * - lsof source code: https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 * - /usr/include/sys/proc_info.h */ PyObject * From 16c091b3803d6f60c5e9d79f0696473fe82a0bc9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2025 10:37:39 +0100 Subject: [PATCH 1193/1714] test cpu_times() for process children --- psutil/tests/test_process.py | 39 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index e85a551621..6de4451999 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -12,6 +12,7 @@ import getpass import io import itertools +import multiprocessing import os import signal import socket @@ -269,17 +270,33 @@ def test_cpu_times(self): 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(f"expected: {utime}, found: {user_time}") - - if (max([kernel_time, ktime]) - min([kernel_time, ktime])) > 0.1: - raise self.fail(f"expected: {ktime}, found: {kernel_time}") + def waste_cpu(): + for x in range(100000): + x **= 2 + + while os.times().user < 0.2: + waste_cpu() + a = psutil.Process().cpu_times() + b = os.times() + self.assertAlmostEqual(a.user, b.user, delta=0.1) + self.assertAlmostEqual(a.system, b.system, delta=0.1) + + def test_cpu_times_3(self): + # same as above but for process children + def waste_cpu(): + while os.times().user < 0.2: + for x in range(100000): + x **= 2 + + proc = multiprocessing.Process(target=waste_cpu) + proc.start() + proc.join() + + a = psutil.Process().cpu_times() + b = os.times() + assert b.children_user >= 0.2 + self.assertAlmostEqual(a.children_user, b.children_user, delta=0.1) + self.assertAlmostEqual(a.children_system, b.children_system, delta=0.1) @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") def test_cpu_num(self): From 104bb3228bf6991f0a5bd466c6a8d8b4c2c629e0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2025 11:05:24 +0100 Subject: [PATCH 1194/1714] test cpu_times() for process children --- psutil/tests/test_process.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 6de4451999..b9397fd41f 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -288,13 +288,13 @@ def waste_cpu(): for x in range(100000): x **= 2 - proc = multiprocessing.Process(target=waste_cpu) - proc.start() - proc.join() + while os.times().children_user < 0.2: + proc = multiprocessing.Process(target=waste_cpu) + proc.start() + proc.join() a = psutil.Process().cpu_times() b = os.times() - assert b.children_user >= 0.2 self.assertAlmostEqual(a.children_user, b.children_user, delta=0.1) self.assertAlmostEqual(a.children_system, b.children_system, delta=0.1) From d6e28b7a83086ed6445666db895a6b7f889891e8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2025 15:08:53 +0100 Subject: [PATCH 1195/1714] try to fix tests --- psutil/_pslinux.py | 2 +- psutil/tests/test_osx.py | 5 +++++ psutil/tests/test_process.py | 27 +++++---------------------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 9254a0b134..8cc64e9a10 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -934,7 +934,7 @@ def process_unix(file, family, inodes, filter_pid=None): f"error while parsing {file}; malformed line {line!r}" ) raise RuntimeError(msg) # noqa: B904 - if inode in inodes: # noqa: SIM108, SIM401 + if inode in inodes: # noqa: SIM108 # With UNIX sockets we can have a single inode # referencing many file descriptors. pairs = inodes[inode] diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 62895ccdd8..050418c5f9 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -13,6 +13,7 @@ import psutil from psutil import MACOS from psutil import POSIX +from psutil.tests import CI_TESTING from psutil.tests import HAS_BATTERY from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM @@ -129,6 +130,10 @@ def test_vmem_total(self): sysctl_hwphymem = sysctl('sysctl hw.memsize') assert sysctl_hwphymem == psutil.virtual_memory().total + @pytest.mark.skipif( + CI_TESTING and MACOS and platform.machine() == 'arm64', + reason="skipped on MACOS + ARM64 + CI_TESTING", + ) @retry_on_failure() def test_vmem_free(self): vmstat_val = vm_stat("free") diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index b9397fd41f..9ba1ba0e3b 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -12,7 +12,6 @@ import getpass import io import itertools -import multiprocessing import os import signal import socket @@ -271,32 +270,16 @@ def test_cpu_times(self): def test_cpu_times_2(self): def waste_cpu(): - for x in range(100000): - x **= 2 - - while os.times().user < 0.2: - waste_cpu() - a = psutil.Process().cpu_times() - b = os.times() - self.assertAlmostEqual(a.user, b.user, delta=0.1) - self.assertAlmostEqual(a.system, b.system, delta=0.1) - - def test_cpu_times_3(self): - # same as above but for process children - def waste_cpu(): - while os.times().user < 0.2: + stop_at = os.times().user + 0.2 + while os.times().user < stop_at: for x in range(100000): x **= 2 - while os.times().children_user < 0.2: - proc = multiprocessing.Process(target=waste_cpu) - proc.start() - proc.join() - + waste_cpu() a = psutil.Process().cpu_times() b = os.times() - self.assertAlmostEqual(a.children_user, b.children_user, delta=0.1) - self.assertAlmostEqual(a.children_system, b.children_system, delta=0.1) + self.assertAlmostEqual(a.user, b.user, delta=0.1) + self.assertAlmostEqual(a.system, b.system, delta=0.1) @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") def test_cpu_num(self): From ea5b55605f857affa4e65fa27eb80f4f2bfebd63 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Feb 2025 22:53:57 +0100 Subject: [PATCH 1196/1714] pre-release --- HISTORY.rst | 6 +++--- docs/index.rst | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d704cc4600..f84aab61cc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.0.0 (IN DEVELOPMENT) -====================== +7.0.0 +===== -XXXX-XX-XX +2025-02-13 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 3fd122cc7c..6b084fddbc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2660,6 +2660,7 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= +* psutil 7.0.0 (2025-02): drop Python 2.7 * psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 * psutil 5.9.1 (2022-05): drop Python 2.6 * psutil 5.9.0 (2021-12): add **MidnightBSD** @@ -2680,6 +2681,10 @@ PyPy3. Timeline ======== +- 2025-02-13: + `7.0.0 `__ - + `what's new `__ - + `diff `__ - 2024-12-19: `6.1.1 `__ - `what's new `__ - From e66755386422e93eb50234ba8c20516dc2a0dc5c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2025 10:26:10 +0100 Subject: [PATCH 1197/1714] disable flaky test on OSX + ARM64 --- psutil/tests/test_osx.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 050418c5f9..780759b64a 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -140,6 +140,10 @@ def test_vmem_free(self): psutil_val = psutil.virtual_memory().free assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + @pytest.mark.skipif( + CI_TESTING and MACOS and platform.machine() == 'arm64', + reason="skipped on MACOS + ARM64 + CI_TESTING", + ) @retry_on_failure() def test_vmem_active(self): vmstat_val = vm_stat("active") From 94e66113c52d6480580d77ce25271032737e0d6b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2025 10:35:43 +0100 Subject: [PATCH 1198/1714] refact tests using AARCH64 --- psutil/tests/__init__.py | 2 +- psutil/tests/test_contracts.py | 6 ++---- psutil/tests/test_memleaks.py | 7 ++----- psutil/tests/test_osx.py | 10 ++++------ psutil/tests/test_system.py | 5 +---- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5d4b3abb79..5ff6ce889b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1569,7 +1569,7 @@ class system_namespace: ('virtual_memory', (), {}), ] if HAS_CPU_FREQ: - if MACOS and platform.machine() == 'arm64': # skipped due to #1892 + if MACOS and AARCH64: # skipped due to #1892 pass else: getters += [('cpu_freq', (), {'percpu': True})] diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 55f3a5ddb8..39f3f7628c 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -9,7 +9,6 @@ Some of these are duplicates of tests test_system.py and test_process.py. """ -import platform import signal import psutil @@ -22,6 +21,7 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil.tests import AARCH64 from psutil.tests import GITHUB_ACTIONS from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_NET_IO_COUNTERS @@ -226,9 +226,7 @@ def test_cpu_count(self): assert isinstance(psutil.cpu_count(), int) # TODO: remove this once 1892 is fixed - @pytest.mark.skipif( - MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" - ) + @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): if psutil.cpu_freq() is None: diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 7f78fae67c..e05f7324db 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -14,10 +14,8 @@ because of how its JIT handles memory, so tests are skipped. """ - import functools import os -import platform import psutil import psutil._common @@ -27,6 +25,7 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil.tests import AARCH64 from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_CPU_FREQ from psutil.tests import HAS_ENVIRON @@ -363,9 +362,7 @@ def test_cpu_stats(self): @fewtimes_if_linux() # TODO: remove this once 1892 is fixed - @pytest.mark.skipif( - MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" - ) + @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): self.execute(psutil.cpu_freq) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 780759b64a..4828d7c395 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -6,13 +6,13 @@ """macOS specific tests.""" -import platform import re import time import psutil from psutil import MACOS from psutil import POSIX +from psutil.tests import AARCH64 from psutil.tests import CI_TESTING from psutil.tests import HAS_BATTERY from psutil.tests import TOLERANCE_DISK_USAGE @@ -115,9 +115,7 @@ def test_cpu_count_cores(self): assert num == psutil.cpu_count(logical=False) # TODO: remove this once 1892 is fixed - @pytest.mark.skipif( - MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" - ) + @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") def test_cpu_freq(self): freq = psutil.cpu_freq() assert freq.current * 1000 * 1000 == sysctl("sysctl hw.cpufrequency") @@ -131,7 +129,7 @@ def test_vmem_total(self): assert sysctl_hwphymem == psutil.virtual_memory().total @pytest.mark.skipif( - CI_TESTING and MACOS and platform.machine() == 'arm64', + CI_TESTING and MACOS and AARCH64, reason="skipped on MACOS + ARM64 + CI_TESTING", ) @retry_on_failure() @@ -141,7 +139,7 @@ def test_vmem_free(self): assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM @pytest.mark.skipif( - CI_TESTING and MACOS and platform.machine() == 'arm64', + CI_TESTING and MACOS and AARCH64, reason="skipped on MACOS + ARM64 + CI_TESTING", ) @retry_on_failure() diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index b961e1f891..09c3ce50f4 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -10,7 +10,6 @@ import enum import errno import os -import platform import pprint import shutil import signal @@ -582,9 +581,7 @@ def test_cpu_stats(self): assert value > 0 # TODO: remove this once 1892 is fixed - @pytest.mark.skipif( - MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" - ) + @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): def check_ls(ls): From cc536afaf8e9030eae2e7c23612492b93df2b659 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2025 23:26:55 +0100 Subject: [PATCH 1199/1714] add pytest-subtests dep --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index df478e7428..8c7294f4c3 100755 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ # `make install-pydeps-test`. TEST_DEPS = [ "pytest", + "pytest-subtests", "pytest-xdist", "setuptools", ] From 16ea1ff61723ef80663c1e5350cf9b42f8e6f095 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Feb 2025 23:51:46 +0100 Subject: [PATCH 1200/1714] better identify AARCH64 --- psutil/tests/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 5ff6ce889b..03803c732b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -116,7 +116,8 @@ PYTEST_PARALLEL = "PYTEST_XDIST_WORKER" in os.environ # `make test-parallel` # are we a 64 bit process? IS_64BIT = sys.maxsize > 2**32 -AARCH64 = platform.machine() == "aarch64" +# apparently they're the same +AARCH64 = platform.machine() in {"aarch64", "arm64"} @memoize From b797bd7b4b42db0c80d3044d0676c11728d440aa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Feb 2025 00:05:37 +0100 Subject: [PATCH 1201/1714] small setup.py refact --- setup.py | 63 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/setup.py b/setup.py index 8c7294f4c3..1710c78ad0 100755 --- a/setup.py +++ b/setup.py @@ -215,6 +215,37 @@ def missdeps(cmdline): print(hilite(s, color="red", bold=True), file=sys.stderr) +def print_install_instructions(): + if LINUX: + pyimpl = "pypy" if PYPY else "python" + if shutil.which("dpkg"): + missdeps("sudo apt-get install gcc {}3-dev".format(pyimpl)) + elif shutil.which("rpm"): + missdeps("sudo yum install gcc {}3-devel".format(pyimpl)) + elif shutil.which("apk"): + missdeps( + "sudo apk add gcc {}3-dev musl-dev linux-headers".format( + *pyimpl + ) + ) + elif MACOS: + msg = "XCode (https://developer.apple.com/xcode/) is not installed" + print(hilite(msg, color="red"), file=sys.stderr) + elif FREEBSD: + if shutil.which("pkg"): + missdeps("pkg install gcc python3") + elif shutil.which("mport"): # MidnightBSD + missdeps("mport install gcc python3") + elif OPENBSD: + missdeps("pkg_add -v gcc python3") + elif NETBSD: + missdeps("pkgin install gcc python3") + elif SUNOS: + missdeps( + "sudo ln -s /usr/bin/gcc /usr/local/bin/cc && pkg install gcc" + ) + + def unix_can_compile(c_code): from distutils.errors import CompileError from distutils.unixccompiler import UnixCCompiler @@ -562,37 +593,7 @@ def main(): ("build", "install", "sdist", "bdist", "develop") ) ): - if LINUX: - pyimpl = "pypy" if PYPY else "python" - if shutil.which("dpkg"): - missdeps("sudo apt-get install gcc {}3-dev".format(pyimpl)) - elif shutil.which("rpm"): - missdeps("sudo yum install gcc {}3-devel".format(pyimpl)) - elif shutil.which("apk"): - missdeps( - "sudo apk add gcc {}3-dev musl-dev linux-headers" - .format(*pyimpl) - ) - elif MACOS: - msg = ( - "XCode (https://developer.apple.com/xcode/)" - " is not installed" - ) - print(hilite(msg, color="red"), file=sys.stderr) - elif FREEBSD: - if shutil.which("pkg"): - missdeps("pkg install gcc python3") - elif shutil.which("mport"): # MidnightBSD - missdeps("mport install gcc python3") - elif OPENBSD: - missdeps("pkg_add -v gcc python3") - elif NETBSD: - missdeps("pkgin install gcc python3") - elif SUNOS: - missdeps( - "sudo ln -s /usr/bin/gcc /usr/local/bin/cc && " - "pkg install gcc" - ) + print_install_instructions() if __name__ == '__main__': From 7f49fac348b931a6968881929f262e90efe32fce Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sat, 22 Feb 2025 13:08:46 +0000 Subject: [PATCH 1202/1714] consistent metadata in wheels (#2507) --- setup.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 1710c78ad0..437bc4e35f 100755 --- a/setup.py +++ b/setup.py @@ -85,11 +85,11 @@ "pytest-subtests", "pytest-xdist", "setuptools", + "pywin32 ; os_name == 'nt' and platform_python_implementation != 'PyPy'", + "wheel ; os_name == 'nt' and platform_python_implementation != 'PyPy'", + "wmi ; os_name == 'nt' and platform_python_implementation != 'PyPy'", ] -if WINDOWS and not PYPY: - TEST_DEPS.extend(("pywin32", "wheel", "wmi")) - # Development deps, installable via `pip install .[dev]` or # `make install-pydeps-dev`. DEV_DEPS = TEST_DEPS + [ @@ -112,12 +112,10 @@ "virtualenv", "vulture", "wheel", + "pyreadline ; os_name == 'nt'", + "pdbpp ; os_name == 'nt'", ] -if WINDOWS: - DEV_DEPS.append("pyreadline") - DEV_DEPS.append("pdbpp") - macros = [] if POSIX: macros.append(("PSUTIL_POSIX", 1)) From 70bacde04ca331b57f4889f0e9e2274a62ac9942 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Thu, 27 Feb 2025 16:04:25 +0000 Subject: [PATCH 1203/1714] More standard installation (#2508) Signed-off-by: David Hotham --- Makefile | 6 ++---- scripts/internal/winmake.py | 10 ++-------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index c0acf3bb23..db1a420d95 100644 --- a/Makefile +++ b/Makefile @@ -75,14 +75,12 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip setuptools - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test] install-pydeps-dev: ## Install python deps meant for local development. ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) pip setuptools - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS + setup.DEV_DEPS))"` + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test,dev] install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 41fb6e7469..cb925ae63b 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -33,12 +33,6 @@ sys.path.insert(0, ROOT_DIR) # so that we can import setup.py -import setup # noqa: E402 - - -TEST_DEPS = setup.TEST_DEPS -DEV_DEPS = setup.DEV_DEPS - _cmds = {} GREEN = 2 @@ -302,14 +296,14 @@ def install_pydeps_test(): """Install useful deps.""" install_pip() install_git_hooks() - sh([PYTHON, "-m", "pip", "install", "--user", "-U"] + TEST_DEPS) + sh([PYTHON, "-m", "pip", "install", "--user", "-U", "-e", ".[test]"]) def install_pydeps_dev(): """Install useful deps.""" install_pip() install_git_hooks() - sh([PYTHON, "-m", "pip", "install", "--user", "-U"] + DEV_DEPS) + sh([PYTHON, "-m", "pip", "install", "--user", "-U", "-e", ".[dev]"]) def test(args=None): From 3ee19e3cf9b6ce295d3544e556c78e543657b444 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 27 Feb 2025 17:54:06 +0100 Subject: [PATCH 1204/1714] run thread-related tests serially --- psutil/tests/test_linux.py | 9 +++++---- psutil/tests/test_process.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index f4342d7aa3..7d25fc841c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -32,7 +32,6 @@ from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_RLIMIT from psutil.tests import PYPY -from psutil.tests import PYTEST_PARALLEL from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase @@ -1541,17 +1540,19 @@ def test_procfs_path(self): psutil.PROCFS_PATH = "/proc" @retry_on_failure() - @pytest.mark.skipif(PYTEST_PARALLEL, reason="skip if pytest-parallel") + @pytest.mark.xdist_group(name="serial") 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 + + p = psutil.Process() + nthreads = len(p.threads()) with ThreadTask(): - p = psutil.Process() threads = p.threads() - assert len(threads) == 2 + assert len(threads) == nthreads + 1 tid = sorted(threads, key=lambda x: x.id)[1].id assert p.pid != tid pt = psutil.Process(tid) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 9ba1ba0e3b..563851673d 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -518,6 +518,7 @@ def test_rlimit_infinity_value(self): assert hard == psutil.RLIM_INFINITY p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + @pytest.mark.xdist_group(name="serial") 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, From 2121ef8ee97c2328f14cdb3f5dc58649d35ce4ec Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 4 Mar 2025 10:24:09 +0100 Subject: [PATCH 1205/1714] winame.py: reintroduce test-by-name --- scripts/internal/winmake.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index cb925ae63b..ec58a25b1c 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -217,7 +217,7 @@ def install_pip(): def install(): """Install in develop / edit mode.""" build() - sh([PYTHON, "setup.py", "develop"]) + sh([PYTHON, "setup.py", "develop", "--user"]) def uninstall(): @@ -317,6 +317,12 @@ def test(args=None): ) +def test_by_name(arg): + """Run specific test by name.""" + build() + sh([PYTHON, "-m", "pytest"] + PYTEST_ARGS + [arg]) + + def test_parallel(): test(["-n", "auto", "--dist", "loadgroup"]) @@ -542,6 +548,8 @@ def main(): fun = getattr(sys.modules[__name__], fname) # err if fun not defined if args.command == 'test' and args.arg: sh([PYTHON, args.arg]) # test a script + elif args.command == 'test-by-name': + test_by_name(args.arg) else: fun() From 74b707fc735b64ea159e118e26acc2ecfb78a719 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 4 Mar 2025 10:29:19 +0100 Subject: [PATCH 1206/1714] winmake.py: add test-by-regex target --- scripts/internal/winmake.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index ec58a25b1c..882729f972 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -320,7 +320,13 @@ def test(args=None): def test_by_name(arg): """Run specific test by name.""" build() - sh([PYTHON, "-m", "pytest"] + PYTEST_ARGS + [arg]) + print(" ".join([PYTHON, "-m", "pytest"] + PYTEST_ARGS + [arg])) + + +def test_by_regex(arg): + """Run specific test by name.""" + build() + sh([PYTHON, "-m", "pytest"] + PYTEST_ARGS + ["-k", arg]) def test_parallel(): @@ -502,6 +508,9 @@ def parse_args(): sp.add_parser('test-parallel', help="run tests in parallel") test = sp.add_parser('test', help="[ARG] run tests") test_by_name = sp.add_parser('test-by-name', help=" run test by name") + test_by_regex = sp.add_parser( + 'test-by-regex', help=" run test by regex" + ) sp.add_parser('test-connections', help="run connections tests") sp.add_parser('test-contracts', help="run contracts tests") sp.add_parser( @@ -520,7 +529,7 @@ def parse_args(): sp.add_parser('upload-wheels', help="upload wheel files on PyPI") sp.add_parser('wheel', help="create wheel file") - for p in (test, test_by_name): + for p in (test, test_by_name, test_by_regex): p.add_argument('arg', type=str, nargs='?', default="", help="arg") args = parser.parse_args() @@ -550,6 +559,8 @@ def main(): sh([PYTHON, args.arg]) # test a script elif args.command == 'test-by-name': test_by_name(args.arg) + elif args.command == 'test-by-regex': + test_by_regex(args.arg) else: fun() From e1a534202528da68fe61863c07d62403428d14f1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 4 Mar 2025 21:36:12 +0100 Subject: [PATCH 1207/1714] [Linux] `Process.cwd()` may raise `FileNotFoundError` (race condition) (#2515) --- HISTORY.rst | 10 ++++++++++ psutil/_pslinux.py | 34 +++++++++++++++++++++++----------- psutil/tests/test_linux.py | 9 +++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f84aab61cc..c05e4114f8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.1.0 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due + to a race condition. + 7.0.0 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 8cc64e9a10..73e76297dd 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -83,6 +83,7 @@ PAGESIZE = cext_posix.getpagesize() BOOT_TIME = None # set later LITTLE_ENDIAN = sys.byteorder == 'little' +UNSET = object() # "man iostat" states that sectors are equivalent with blocks and have # a size of 512 bytes. Despite this value can be queried at runtime @@ -1698,6 +1699,22 @@ def _raise_if_not_alive(self): # incorrect or incomplete result. os.stat(f"{self._procfs_path}/{self.pid}") + def _readlink(self, path, fallback=UNSET): + # * https://github.com/giampaolo/psutil/issues/503 + # os.readlink('/proc/pid/exe') may raise ESRCH (ProcessLookupError) + # instead of ENOENT (FileNotFoundError) when it races. + # * ENOENT may occur also if the path actually exists if PID is + # a low PID (~0-20 range). + # * https://github.com/giampaolo/psutil/issues/2514 + try: + return readlink(path) + except (FileNotFoundError, ProcessLookupError): + if os.path.lexists(f"{self._procfs_path}/{self.pid}"): + self._raise_if_zombie() + if fallback is not UNSET: + return fallback + raise + @wrap_exceptions @memoize_when_activated def _parse_stat_file(self): @@ -1770,16 +1787,9 @@ def name(self): @wrap_exceptions def exe(self): - try: - return readlink(f"{self._procfs_path}/{self.pid}/exe") - 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(f"{self._procfs_path}/{self.pid}"): - return "" - raise + return self._readlink( + f"{self._procfs_path}/{self.pid}/exe", fallback="" + ) @wrap_exceptions def cmdline(self): @@ -2054,7 +2064,9 @@ def get_blocks(lines, current_block): @wrap_exceptions def cwd(self): - return readlink(f"{self._procfs_path}/{self.pid}/cwd") + return self._readlink( + f"{self._procfs_path}/{self.pid}/cwd", fallback="" + ) @wrap_exceptions def num_ctx_switches( diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 7d25fc841c..836311e220 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -2064,6 +2064,15 @@ def test_exe_mocked(self): assert m.called assert ret == "" + def test_cwd_mocked(self): + # https://github.com/giampaolo/psutil/issues/2514 + with mock.patch( + 'psutil._pslinux.readlink', side_effect=FileNotFoundError + ) as m: + ret = psutil.Process().cwd() + assert m.called + assert 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. From 42e78a9f01fd298424b1e452a7453f48282fbf89 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Mar 2025 02:39:33 +0100 Subject: [PATCH 1208/1714] Check return code of C init module functions (#2516) --- psutil/_psutil_aix.c | 79 ++++++++++-------- psutil/_psutil_bsd.c | 112 ++++++++++++++----------- psutil/_psutil_linux.c | 24 +++--- psutil/_psutil_osx.c | 47 +++++------ psutil/_psutil_posix.c | 70 ++++++++++------ psutil/_psutil_sunos.c | 91 ++++++++++++-------- psutil/_psutil_windows.c | 173 ++++++++++++++++++--------------------- 7 files changed, 324 insertions(+), 272 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 958a2c0c21..8c73d71b00 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -58,7 +58,6 @@ #define TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) -#define INITERROR return NULL /* * Read a file content and fills a C structure with it. @@ -1064,40 +1063,56 @@ static struct PyModuleDef moduledef = { PyMODINIT_FUNC PyInit__psutil_aix(void) { - PyObject *module = PyModule_Create(&moduledef); - if (module == NULL) - INITERROR; + PyObject *mod = PyModule_Create(&moduledef); + if (mod == NULL) + return NULL; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); - - PyModule_AddIntConstant(module, "SIDL", SIDL); - PyModule_AddIntConstant(module, "SZOMB", SZOMB); - PyModule_AddIntConstant(module, "SACTIVE", SACTIVE); - PyModule_AddIntConstant(module, "SSWAP", SSWAP); - PyModule_AddIntConstant(module, "SSTOP", SSTOP); - - PyModule_AddIntConstant(module, "TCPS_CLOSED", TCPS_CLOSED); - PyModule_AddIntConstant(module, "TCPS_CLOSING", TCPS_CLOSING); - PyModule_AddIntConstant(module, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT); - PyModule_AddIntConstant(module, "TCPS_LISTEN", TCPS_LISTEN); - PyModule_AddIntConstant(module, "TCPS_ESTABLISHED", TCPS_ESTABLISHED); - PyModule_AddIntConstant(module, "TCPS_SYN_SENT", TCPS_SYN_SENT); - PyModule_AddIntConstant(module, "TCPS_SYN_RCVD", TCPS_SYN_RECEIVED); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2); - PyModule_AddIntConstant(module, "TCPS_LAST_ACK", TCPS_LAST_ACK); - PyModule_AddIntConstant(module, "TCPS_TIME_WAIT", TCPS_TIME_WAIT); - PyModule_AddIntConstant(module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); - - psutil_setup(); - - if (module == NULL) - INITERROR; - - return module; + + if (psutil_setup() != 0) + return NULL; + + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + return NULL; + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) + return NULL; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) + return NULL; + if (PyModule_AddIntConstant(mod, "SACTIVE", SACTIVE)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSWAP", SSWAP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_RCVD", TCPS_SYN_RECEIVED)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) + return NULL; + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) + return NULL; + + return mod; } #ifdef __cplusplus diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 503e96cacb..48298cde22 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -52,9 +52,6 @@ #endif -#define INITERR return NULL - - /* * define the psutil C module methods and initialize the module. */ @@ -133,74 +130,97 @@ PyObject PyObject *v; PyObject *mod = PyModule_Create(&moduledef); if (mod == NULL) - INITERR; + return NULL; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif - if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) INITERR; - // process status constants + if (psutil_setup() != 0) + return NULL; + + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + return NULL; + // process status constants #ifdef PSUTIL_FREEBSD - if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) INITERR; - if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) INITERR; - if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) INITERR; - if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) INITERR; - if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) INITERR; - if (PyModule_AddIntConstant(mod, "SWAIT", SWAIT)) INITERR; - if (PyModule_AddIntConstant(mod, "SLOCK", SLOCK)) INITERR; + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) + return NULL; + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) + return NULL; + if (PyModule_AddIntConstant(mod, "SWAIT", SWAIT)) + return NULL; + if (PyModule_AddIntConstant(mod, "SLOCK", SLOCK)) + return NULL; #elif PSUTIL_OPENBSD - if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) INITERR; - if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) INITERR; - if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) INITERR; - if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) INITERR; - if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) INITERR; // unused - if (PyModule_AddIntConstant(mod, "SDEAD", SDEAD)) INITERR; - if (PyModule_AddIntConstant(mod, "SONPROC", SONPROC)) INITERR; + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) + return NULL; + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) + return NULL; // unused + if (PyModule_AddIntConstant(mod, "SDEAD", SDEAD)) + return NULL; + if (PyModule_AddIntConstant(mod, "SONPROC", SONPROC)) + return NULL; #elif defined(PSUTIL_NETBSD) - if (PyModule_AddIntConstant(mod, "SIDL", LSIDL)) INITERR; - if (PyModule_AddIntConstant(mod, "SRUN", LSRUN)) INITERR; - if (PyModule_AddIntConstant(mod, "SSLEEP", LSSLEEP)) INITERR; - if (PyModule_AddIntConstant(mod, "SSTOP", LSSTOP)) INITERR; - if (PyModule_AddIntConstant(mod, "SZOMB", LSZOMB)) INITERR; + if (PyModule_AddIntConstant(mod, "SIDL", LSIDL)) + return NULL; + if (PyModule_AddIntConstant(mod, "SRUN", LSRUN)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSLEEP", LSSLEEP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSTOP", LSSTOP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SZOMB", LSZOMB)) + return NULL; #if __NetBSD_Version__ < 500000000 - if (PyModule_AddIntConstant(mod, "SDEAD", LSDEAD)) INITERR; + if (PyModule_AddIntConstant(mod, "SDEAD", LSDEAD)) + return NULL; #endif - if (PyModule_AddIntConstant(mod, "SONPROC", LSONPROC)) INITERR; + if (PyModule_AddIntConstant(mod, "SONPROC", LSONPROC)) + return NULL; // unique to NetBSD - if (PyModule_AddIntConstant(mod, "SSUSPENDED", LSSUSPENDED)) INITERR; + if (PyModule_AddIntConstant(mod, "SSUSPENDED", LSSUSPENDED)) + return NULL; #endif // connection status constants if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) - INITERR; - // PSUTIL_CONN_NONE - if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", 128)) INITERR; - - psutil_setup(); + return NULL; + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", 128)) + return NULL; - if (mod == NULL) - INITERR; return mod; } diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index fcd886bfb6..234680c7b5 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -25,8 +25,6 @@ #define DUPLEX_UNKNOWN 0xff #endif -#define INITERR return NULL - static PyMethodDef mod_methods[] = { // --- per-process functions #ifdef PSUTIL_HAVE_IOPRIO @@ -67,20 +65,24 @@ PyObject * PyInit__psutil_linux(void) { PyObject *mod = PyModule_Create(&moduledef); if (mod == NULL) - INITERR; + return NULL; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif - if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) INITERR; - if (PyModule_AddIntConstant(mod, "DUPLEX_HALF", DUPLEX_HALF)) INITERR; - if (PyModule_AddIntConstant(mod, "DUPLEX_FULL", DUPLEX_FULL)) INITERR; - if (PyModule_AddIntConstant(mod, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN)) INITERR; + if (psutil_setup() != 0) + return NULL; - psutil_setup(); + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + return NULL; + if (PyModule_AddIntConstant(mod, "DUPLEX_HALF", DUPLEX_HALF)) + return NULL; + if (PyModule_AddIntConstant(mod, "DUPLEX_FULL", DUPLEX_FULL)) + return NULL; + if (PyModule_AddIntConstant(mod, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN)) + return NULL; - if (mod == NULL) - INITERR; return mod; } diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index b16103379b..a9e116f5af 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -21,8 +21,6 @@ #include "arch/osx/sys.h" -#define INITERR return NULL - static PyMethodDef mod_methods[] = { // --- per-process functions {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, @@ -81,56 +79,55 @@ PyObject * PyInit__psutil_osx(void) { PyObject *mod = PyModule_Create(&moduledef); if (mod == NULL) - INITERR; + return NULL; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif if (psutil_setup() != 0) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) - INITERR; + return NULL; // process status constants, defined in: // http://fxr.watson.org/fxr/source/bsd/sys/proc.h?v=xnu-792.6.70#L149 if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) - INITERR; + return NULL; // connection status constants if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) - INITERR; + return NULL; if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) - INITERR; + return NULL; - if (mod == NULL) - INITERR; return mod; } diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index d4d16e7fff..f9f9da1051 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -52,9 +52,6 @@ #include "_psutil_common.h" -#define INITERR return NULL - - // ==================================================================== // --- Utils // ==================================================================== @@ -898,100 +895,121 @@ PyObject * PyInit__psutil_posix(void) { PyObject *mod = PyModule_Create(&moduledef); if (mod == NULL) - INITERR; + return NULL; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif #if defined(PSUTIL_BSD) || \ defined(PSUTIL_OSX) || \ defined(PSUTIL_SUNOS) || \ defined(PSUTIL_AIX) - if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) INITERR; + if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) + return NULL; #endif #if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) PyObject *v; #ifdef RLIMIT_AS - if (PyModule_AddIntConstant(mod, "RLIMIT_AS", RLIMIT_AS)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_AS", RLIMIT_AS)) + return NULL; #endif #ifdef RLIMIT_CORE - if (PyModule_AddIntConstant(mod, "RLIMIT_CORE", RLIMIT_CORE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_CORE", RLIMIT_CORE)) + return NULL; #endif #ifdef RLIMIT_CPU - if (PyModule_AddIntConstant(mod, "RLIMIT_CPU", RLIMIT_CPU)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_CPU", RLIMIT_CPU)) + return NULL; #endif #ifdef RLIMIT_DATA - if (PyModule_AddIntConstant(mod, "RLIMIT_DATA", RLIMIT_DATA)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_DATA", RLIMIT_DATA)) + return NULL; #endif #ifdef RLIMIT_FSIZE - if (PyModule_AddIntConstant(mod, "RLIMIT_FSIZE", RLIMIT_FSIZE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_FSIZE", RLIMIT_FSIZE)) + return NULL; #endif #ifdef RLIMIT_MEMLOCK - if (PyModule_AddIntConstant(mod, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK)) + return NULL; #endif #ifdef RLIMIT_NOFILE - if (PyModule_AddIntConstant(mod, "RLIMIT_NOFILE", RLIMIT_NOFILE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_NOFILE", RLIMIT_NOFILE)) + return NULL; #endif #ifdef RLIMIT_NPROC - if (PyModule_AddIntConstant(mod, "RLIMIT_NPROC", RLIMIT_NPROC)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_NPROC", RLIMIT_NPROC)) + return NULL; #endif #ifdef RLIMIT_RSS - if (PyModule_AddIntConstant(mod, "RLIMIT_RSS", RLIMIT_RSS)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_RSS", RLIMIT_RSS)) + return NULL; #endif #ifdef RLIMIT_STACK - if (PyModule_AddIntConstant(mod, "RLIMIT_STACK", RLIMIT_STACK)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_STACK", RLIMIT_STACK)) + return NULL; #endif // Linux specific #ifdef RLIMIT_LOCKS - if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) + return NULL; #endif #ifdef RLIMIT_MSGQUEUE - if (PyModule_AddIntConstant(mod, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE)) + return NULL; #endif #ifdef RLIMIT_NICE - if (PyModule_AddIntConstant(mod, "RLIMIT_NICE", RLIMIT_NICE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_NICE", RLIMIT_NICE)) + return NULL; #endif #ifdef RLIMIT_RTPRIO - if (PyModule_AddIntConstant(mod, "RLIMIT_RTPRIO", RLIMIT_RTPRIO)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_RTPRIO", RLIMIT_RTPRIO)) + return NULL; #endif #ifdef RLIMIT_RTTIME - if (PyModule_AddIntConstant(mod, "RLIMIT_RTTIME", RLIMIT_RTTIME)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_RTTIME", RLIMIT_RTTIME)) + return NULL; #endif #ifdef RLIMIT_SIGPENDING - if (PyModule_AddIntConstant(mod, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING)) + return NULL; #endif // Free specific #ifdef RLIMIT_SWAP - if (PyModule_AddIntConstant(mod, "RLIMIT_SWAP", RLIMIT_SWAP)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_SWAP", RLIMIT_SWAP)) + return NULL; #endif #ifdef RLIMIT_SBSIZE - if (PyModule_AddIntConstant(mod, "RLIMIT_SBSIZE", RLIMIT_SBSIZE)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_SBSIZE", RLIMIT_SBSIZE)) + return NULL; #endif #ifdef RLIMIT_NPTS - if (PyModule_AddIntConstant(mod, "RLIMIT_NPTS", RLIMIT_NPTS)) INITERR; + if (PyModule_AddIntConstant(mod, "RLIMIT_NPTS", RLIMIT_NPTS)) + return NULL; #endif #if defined(HAVE_LONG_LONG) @@ -1007,8 +1025,6 @@ PyInit__psutil_posix(void) { } #endif // defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) - if (mod == NULL) - INITERR; return mod; } diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index dde9f70158..1c8a8a31d3 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -60,7 +60,6 @@ #define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#define INITERROR return NULL /* @@ -1701,55 +1700,75 @@ static struct PyModuleDef moduledef = { PyMODINIT_FUNC PyInit__psutil_sunos(void) { - PyObject *module = PyModule_Create(&moduledef); + PyObject *mod = PyModule_Create(&moduledef); if (module == NULL) - INITERROR; + return NULL; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif if (psutil_setup() != 0) - INITERROR; - - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); + return NULL; - PyModule_AddIntConstant(module, "SSLEEP", SSLEEP); - PyModule_AddIntConstant(module, "SRUN", SRUN); - PyModule_AddIntConstant(module, "SZOMB", SZOMB); - PyModule_AddIntConstant(module, "SSTOP", SSTOP); - PyModule_AddIntConstant(module, "SIDL", SIDL); - PyModule_AddIntConstant(module, "SONPROC", SONPROC); + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) + return NULL; + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) + return NULL; + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) + return NULL; + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) + return NULL; + if (PyModule_AddIntConstant(mod, "SONPROC", SONPROC)) + return NULL; #ifdef SWAIT - PyModule_AddIntConstant(module, "SWAIT", SWAIT); + if (PyModule_AddIntConstant(mod, "SWAIT", SWAIT)) + return NULL; #else /* sys/proc.h started defining SWAIT somewhere * after Update 3 and prior to Update 5 included. */ - PyModule_AddIntConstant(module, "SWAIT", 0); + if (PyModule_AddIntConstant(mod, "SWAIT", 0)) + return NULL; #endif - - PyModule_AddIntConstant(module, "PRNODEV", PRNODEV); // for process tty - - PyModule_AddIntConstant(module, "TCPS_CLOSED", TCPS_CLOSED); - PyModule_AddIntConstant(module, "TCPS_CLOSING", TCPS_CLOSING); - PyModule_AddIntConstant(module, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT); - PyModule_AddIntConstant(module, "TCPS_LISTEN", TCPS_LISTEN); - PyModule_AddIntConstant(module, "TCPS_ESTABLISHED", TCPS_ESTABLISHED); - PyModule_AddIntConstant(module, "TCPS_SYN_SENT", TCPS_SYN_SENT); - PyModule_AddIntConstant(module, "TCPS_SYN_RCVD", TCPS_SYN_RCVD); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1); - PyModule_AddIntConstant(module, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2); - PyModule_AddIntConstant(module, "TCPS_LAST_ACK", TCPS_LAST_ACK); - PyModule_AddIntConstant(module, "TCPS_TIME_WAIT", TCPS_TIME_WAIT); + // for process tty + if (PyModule_AddIntConstant(mod, "PRNODEV", PRNODEV)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_SYN_RCVD", TCPS_SYN_RCVD)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) + return NULL; + if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) + return NULL; // sunos specific - PyModule_AddIntConstant(module, "TCPS_IDLE", TCPS_IDLE); + if (PyModule_AddIntConstant(mod, "TCPS_IDLE", TCPS_IDLE)) + return NULL; // sunos specific - PyModule_AddIntConstant(module, "TCPS_BOUND", TCPS_BOUND); - PyModule_AddIntConstant(module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); - - if (module == NULL) - INITERROR; + if (PyModule_AddIntConstant(mod, "TCPS_BOUND", TCPS_BOUND)) + return NULL; + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) + return NULL; - return module; + return mod; } diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 0af18f3e26..c622498948 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -34,7 +34,6 @@ #include "arch/windows/wmi.h" -#define INITERROR return NULL #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) @@ -150,127 +149,111 @@ PyMODINIT_FUNC PyInit__psutil_windows(void) { struct module_state *st = NULL; - PyObject *module = PyModule_Create(&moduledef); - if (module == NULL) - INITERROR; + PyObject *mod = PyModule_Create(&moduledef); + if (mod == NULL) + return NULL; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif if (psutil_setup() != 0) - INITERROR; + return NULL; if (psutil_set_se_debug() != 0) - INITERROR; + return NULL; - st = GETSTATE(module); + st = GETSTATE(mod); st->error = PyErr_NewException("_psutil_windows.Error", NULL, NULL); if (st->error == NULL) { - Py_DECREF(module); - INITERROR; + Py_DECREF(mod); + return NULL; } // Exceptions. TimeoutExpired = PyErr_NewException( "_psutil_windows.TimeoutExpired", NULL, NULL); Py_INCREF(TimeoutExpired); - PyModule_AddObject(module, "TimeoutExpired", TimeoutExpired); + PyModule_AddObject(mod, "TimeoutExpired", TimeoutExpired); TimeoutAbandoned = PyErr_NewException( "_psutil_windows.TimeoutAbandoned", NULL, NULL); Py_INCREF(TimeoutAbandoned); - PyModule_AddObject(module, "TimeoutAbandoned", TimeoutAbandoned); + PyModule_AddObject(mod, "TimeoutAbandoned", TimeoutAbandoned); // version constant - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + return NULL; // process status constants // http://msdn.microsoft.com/en-us/library/ms683211(v=vs.85).aspx - PyModule_AddIntConstant( - module, "ABOVE_NORMAL_PRIORITY_CLASS", ABOVE_NORMAL_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "BELOW_NORMAL_PRIORITY_CLASS", BELOW_NORMAL_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "HIGH_PRIORITY_CLASS", HIGH_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "IDLE_PRIORITY_CLASS", IDLE_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "NORMAL_PRIORITY_CLASS", NORMAL_PRIORITY_CLASS); - PyModule_AddIntConstant( - module, "REALTIME_PRIORITY_CLASS", REALTIME_PRIORITY_CLASS); + if (PyModule_AddIntConstant(mod, "ABOVE_NORMAL_PRIORITY_CLASS", ABOVE_NORMAL_PRIORITY_CLASS)) + return NULL; + if (PyModule_AddIntConstant(mod, "BELOW_NORMAL_PRIORITY_CLASS", BELOW_NORMAL_PRIORITY_CLASS)) + return NULL; + if (PyModule_AddIntConstant(mod, "HIGH_PRIORITY_CLASS", HIGH_PRIORITY_CLASS)) + return NULL; + if (PyModule_AddIntConstant(mod, "IDLE_PRIORITY_CLASS", IDLE_PRIORITY_CLASS)) + return NULL; + if (PyModule_AddIntConstant(mod, "NORMAL_PRIORITY_CLASS", NORMAL_PRIORITY_CLASS)) + return NULL; + if (PyModule_AddIntConstant(mod, "REALTIME_PRIORITY_CLASS", REALTIME_PRIORITY_CLASS)) + return NULL; // connection status constants // http://msdn.microsoft.com/en-us/library/cc669305.aspx - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_CLOSED", MIB_TCP_STATE_CLOSED); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_CLOSING", MIB_TCP_STATE_CLOSING); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_CLOSE_WAIT", MIB_TCP_STATE_CLOSE_WAIT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_LISTEN", MIB_TCP_STATE_LISTEN); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_ESTAB", MIB_TCP_STATE_ESTAB); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_SYN_SENT", MIB_TCP_STATE_SYN_SENT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_SYN_RCVD", MIB_TCP_STATE_SYN_RCVD); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_FIN_WAIT1", MIB_TCP_STATE_FIN_WAIT1); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_FIN_WAIT2", MIB_TCP_STATE_FIN_WAIT2); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_LAST_ACK", MIB_TCP_STATE_LAST_ACK); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT); - PyModule_AddIntConstant( - module, "MIB_TCP_STATE_DELETE_TCB", MIB_TCP_STATE_DELETE_TCB); - PyModule_AddIntConstant( - module, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE); - - // service status constants - /* - PyModule_AddIntConstant( - module, "SERVICE_CONTINUE_PENDING", SERVICE_CONTINUE_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_PAUSE_PENDING", SERVICE_PAUSE_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_PAUSED", SERVICE_PAUSED); - PyModule_AddIntConstant( - module, "SERVICE_RUNNING", SERVICE_RUNNING); - PyModule_AddIntConstant( - module, "SERVICE_START_PENDING", SERVICE_START_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_STOP_PENDING", SERVICE_STOP_PENDING); - PyModule_AddIntConstant( - module, "SERVICE_STOPPED", SERVICE_STOPPED); - */ + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_CLOSED", MIB_TCP_STATE_CLOSED)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_CLOSING", MIB_TCP_STATE_CLOSING)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_CLOSE_WAIT", MIB_TCP_STATE_CLOSE_WAIT)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_LISTEN", MIB_TCP_STATE_LISTEN)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_ESTAB", MIB_TCP_STATE_ESTAB)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_SYN_SENT", MIB_TCP_STATE_SYN_SENT)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_SYN_RCVD", MIB_TCP_STATE_SYN_RCVD)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_FIN_WAIT1", MIB_TCP_STATE_FIN_WAIT1)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_FIN_WAIT2", MIB_TCP_STATE_FIN_WAIT2)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_LAST_ACK", MIB_TCP_STATE_LAST_ACK)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT)) + return NULL; + if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_DELETE_TCB", MIB_TCP_STATE_DELETE_TCB)) + return NULL; + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) + return NULL; // ...for internal use in _psutil_windows.py - PyModule_AddIntConstant( - module, "INFINITE", INFINITE); - PyModule_AddIntConstant( - module, "ERROR_ACCESS_DENIED", ERROR_ACCESS_DENIED); - PyModule_AddIntConstant( - module, "ERROR_INVALID_NAME", ERROR_INVALID_NAME); - PyModule_AddIntConstant( - module, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST); - PyModule_AddIntConstant( - module, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD); - PyModule_AddIntConstant( - module, "WINVER", PSUTIL_WINVER); - PyModule_AddIntConstant( - module, "WINDOWS_VISTA", PSUTIL_WINDOWS_VISTA); - PyModule_AddIntConstant( - module, "WINDOWS_7", PSUTIL_WINDOWS_7); - PyModule_AddIntConstant( - module, "WINDOWS_8", PSUTIL_WINDOWS_8); - PyModule_AddIntConstant( - module, "WINDOWS_8_1", PSUTIL_WINDOWS_8_1); - PyModule_AddIntConstant( - module, "WINDOWS_10", PSUTIL_WINDOWS_10); - - return module; + if (PyModule_AddIntConstant(mod, "INFINITE", INFINITE)) + return NULL; + if (PyModule_AddIntConstant(mod, "ERROR_ACCESS_DENIED", ERROR_ACCESS_DENIED)) + return NULL; + if (PyModule_AddIntConstant(mod, "ERROR_INVALID_NAME", ERROR_INVALID_NAME)) + return NULL; + if (PyModule_AddIntConstant(mod, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST)) + return NULL; + if (PyModule_AddIntConstant(mod, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD)) + return NULL; + if (PyModule_AddIntConstant(mod, "WINVER", PSUTIL_WINVER)) + return NULL; + if (PyModule_AddIntConstant(mod, "WINDOWS_VISTA", PSUTIL_WINDOWS_VISTA)) + return NULL; + if (PyModule_AddIntConstant(mod, "WINDOWS_7", PSUTIL_WINDOWS_7)) + return NULL; + if (PyModule_AddIntConstant(mod, "WINDOWS_8", PSUTIL_WINDOWS_8)) + return NULL; + if (PyModule_AddIntConstant(mod, "WINDOWS_8_1", PSUTIL_WINDOWS_8_1)) + return NULL; + if (PyModule_AddIntConstant(mod, "WINDOWS_10", PSUTIL_WINDOWS_10)) + return NULL; + + return mod; } From a32dd1b71f60c101217c2354a0e98591150c0262 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Mar 2025 18:17:52 +0100 Subject: [PATCH 1209/1714] fix sunos compilation err --- psutil/_psutil_sunos.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 1c8a8a31d3..37a421307e 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1701,7 +1701,7 @@ static struct PyModuleDef moduledef = { PyMODINIT_FUNC PyInit__psutil_sunos(void) { PyObject *mod = PyModule_Create(&moduledef); - if (module == NULL) + if (mod == NULL) return NULL; #ifdef Py_GIL_DISABLED From 7d55c86d21481516579bc8cd55db577373d93ad6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Mar 2025 20:39:58 +0100 Subject: [PATCH 1210/1714] check return value of PyModule_AddObject --- psutil/_psutil_posix.c | 3 ++- psutil/_psutil_windows.c | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index f9f9da1051..53ffccdfbb 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -1021,7 +1021,8 @@ PyInit__psutil_posix(void) { v = PyLong_FromLong((long) RLIM_INFINITY); } if (v) { - PyModule_AddObject(mod, "RLIM_INFINITY", v); + if (PyModule_AddObject(mod, "RLIM_INFINITY", v)) + return NULL; } #endif // defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index c622498948..a2e9fd42cd 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -174,12 +174,14 @@ PyInit__psutil_windows(void) { TimeoutExpired = PyErr_NewException( "_psutil_windows.TimeoutExpired", NULL, NULL); Py_INCREF(TimeoutExpired); - PyModule_AddObject(mod, "TimeoutExpired", TimeoutExpired); + if (PyModule_AddObject(mod, "TimeoutExpired", TimeoutExpired)) + return NULL; TimeoutAbandoned = PyErr_NewException( "_psutil_windows.TimeoutAbandoned", NULL, NULL); Py_INCREF(TimeoutAbandoned); - PyModule_AddObject(mod, "TimeoutAbandoned", TimeoutAbandoned); + if (PyModule_AddObject(mod, "TimeoutAbandoned", TimeoutAbandoned)) + return NULL; // version constant if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) From 4a03f3623470d4f5d5396f05a38d05cbb0472d4e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Mar 2025 20:46:09 +0100 Subject: [PATCH 1211/1714] rm unused _psutil_windows.Error --- psutil/_psutil_windows.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index a2e9fd42cd..0bd14a51b0 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -147,8 +147,6 @@ static struct PyModuleDef moduledef = { PyMODINIT_FUNC PyInit__psutil_windows(void) { - struct module_state *st = NULL; - PyObject *mod = PyModule_Create(&moduledef); if (mod == NULL) return NULL; @@ -163,13 +161,6 @@ PyInit__psutil_windows(void) { if (psutil_set_se_debug() != 0) return NULL; - st = GETSTATE(mod); - st->error = PyErr_NewException("_psutil_windows.Error", NULL, NULL); - if (st->error == NULL) { - Py_DECREF(mod); - return NULL; - } - // Exceptions. TimeoutExpired = PyErr_NewException( "_psutil_windows.TimeoutExpired", NULL, NULL); From a17550784b0d3175da01cdb02cee1bc6b61637dc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Mar 2025 20:52:39 +0100 Subject: [PATCH 1212/1714] check return value of PyErr_NewException --- psutil/_psutil_windows.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 0bd14a51b0..4902f91487 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -161,15 +161,19 @@ PyInit__psutil_windows(void) { if (psutil_set_se_debug() != 0) return NULL; - // Exceptions. + // Exceptions TimeoutExpired = PyErr_NewException( "_psutil_windows.TimeoutExpired", NULL, NULL); + if (TimeoutExpired == NULL) + return NULL; Py_INCREF(TimeoutExpired); if (PyModule_AddObject(mod, "TimeoutExpired", TimeoutExpired)) return NULL; TimeoutAbandoned = PyErr_NewException( "_psutil_windows.TimeoutAbandoned", NULL, NULL); + if (TimeoutAbandoned == NULL) + return NULL; Py_INCREF(TimeoutAbandoned); if (PyModule_AddObject(mod, "TimeoutAbandoned", TimeoutAbandoned)) return NULL; From 65ef1d0f8f08c992b3c7259c424cf36269955ed7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 29 Mar 2025 20:19:59 +0100 Subject: [PATCH 1213/1714] fix #2528 / linux: turn PermissionError into AccessDenied on Process.children() --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c05e4114f8..1b445bf59a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due to a race condition. +- 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will + now raise `AccessDenied`_ instead. 7.0.0 ===== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 73e76297dd..59a61be3a4 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1624,9 +1624,9 @@ def ppid_map(): with open_binary(f"{procfs_path}/{pid}/stat") as f: data = f.read() 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. pass + except PermissionError as err: + raise AccessDenied(pid) from err else: rpar = data.rfind(b')') dset = data[rpar + 2 :].split() From 642bd8dccb06ceb7ad07e0290f49599be5124c9e Mon Sep 17 00:00:00 2001 From: Fabien Bousquet Date: Wed, 2 Apr 2025 17:09:29 +0200 Subject: [PATCH 1214/1714] MacOS: Fix to be able to build psutil on MacOS 11 (#2529) Signed-off-by: Fabien Bousquet --- CREDITS | 4 ++++ HISTORY.rst | 1 + psutil/arch/osx/cpu.c | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/CREDITS b/CREDITS index 6d7dd113f5..39485be58a 100644 --- a/CREDITS +++ b/CREDITS @@ -844,3 +844,7 @@ W: https://github.com/AlekseyLobanov N: Will Hawes W: https://github.com/wdh I: 2496 + +N: Fabien Bousquet +W: https://github.com/fafanoulele +I: 2473 diff --git a/HISTORY.rst b/HISTORY.rst index 1b445bf59a..ada5c62f38 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,7 @@ XXXX-XX-XX to a race condition. - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will now raise `AccessDenied`_ instead. +- 2473_, [macOS]: Fix build issue on macOS 11 and lower. 7.0.0 ===== diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index d2f8b1855f..769b1a8a1e 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -34,6 +34,10 @@ For reference, here's the git history with original implementations: #include "../../_psutil_common.h" #include "../../_psutil_posix.h" +// added in macOS 12 +#ifndef kIOMainPortDefault + #define kIOMainPortDefault 0 +#endif PyObject * psutil_cpu_count_logical(PyObject *self, PyObject *args) { From c3cc6e25f38e1f714f78cea9d5e019ce43d55417 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Apr 2025 19:20:37 +0200 Subject: [PATCH 1215/1714] Convert unittest idioms to pytest (#2530) --- psutil/tests/__init__.py | 11 +++-- psutil/tests/test_bsd.py | 4 -- psutil/tests/test_connections.py | 4 +- psutil/tests/test_contracts.py | 74 +++++++++++++++----------------- psutil/tests/test_linux.py | 15 +------ psutil/tests/test_misc.py | 2 +- psutil/tests/test_process.py | 4 +- psutil/tests/test_process_all.py | 6 +-- psutil/tests/test_system.py | 5 ++- psutil/tests/test_testutils.py | 2 +- psutil/tests/test_windows.py | 17 +++----- 11 files changed, 64 insertions(+), 80 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 03803c732b..f6d87a64ed 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1094,12 +1094,12 @@ def assertProcessZombie(self, proc): # __eq__ can't be relied upon because creation time may not be # querable. - # self.assertEqual(proc, psutil.Process(proc.pid)) + # assert 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()) + # assert proc == ppid(), os.getpid() @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") @@ -1263,7 +1263,12 @@ def execute_w_exc(self, exc, fun, **kwargs): """ def call(): - self.assertRaises(exc, fun) + try: + fun() + except exc: + pass + else: + raise self.fail(f"{fun} did not raise {exc}") self.execute(call, **kwargs) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 2786c34857..9ed1601502 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -389,10 +389,6 @@ def test_cpu_stats_syscalls(self): < 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): diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 5ddeb855f2..843fc46139 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -218,8 +218,8 @@ def test_tcp(self): # commenteed. # client.close() # cons = this_proc_net_connections(kind='all') - # self.assertEqual(len(cons), 1) - # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) + # assert len(cons) == 1 + # assert cons[0].status == psutil.CONN_CLOSE_WAIT finally: server.close() client.close() diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 39f3f7628c..3606da27be 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -49,62 +49,58 @@ def test_PROCFS_PATH(self): assert 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) + assert hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS") == WINDOWS + assert hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS") == WINDOWS + assert hasattr(psutil, "HIGH_PRIORITY_CLASS") == WINDOWS + assert hasattr(psutil, "IDLE_PRIORITY_CLASS") == WINDOWS + assert hasattr(psutil, "NORMAL_PRIORITY_CLASS") == WINDOWS + assert 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) + assert hasattr(psutil, "IOPRIO_CLASS_NONE") == LINUX + assert hasattr(psutil, "IOPRIO_CLASS_RT") == LINUX + assert hasattr(psutil, "IOPRIO_CLASS_BE") == LINUX + assert 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) + assert hasattr(psutil, "IOPRIO_HIGH") == WINDOWS + assert hasattr(psutil, "IOPRIO_NORMAL") == WINDOWS + assert hasattr(psutil, "IOPRIO_LOW") == WINDOWS + assert hasattr(psutil, "IOPRIO_VERYLOW") == WINDOWS @pytest.mark.skipif( GITHUB_ACTIONS and LINUX, reason="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) + assert hasattr(psutil, "RLIM_INFINITY") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_AS") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_CORE") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_CPU") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_DATA") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_FSIZE") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_MEMLOCK") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_NOFILE") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_NPROC") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_RSS") == LINUX or FREEBSD + assert hasattr(psutil, "RLIMIT_STACK") == LINUX or FREEBSD + + assert hasattr(psutil, "RLIMIT_LOCKS") == LINUX if POSIX: if kernel_version() >= (2, 6, 8): - ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX) + assert hasattr(psutil, "RLIMIT_MSGQUEUE") == LINUX if kernel_version() >= (2, 6, 12): - ae(hasattr(psutil, "RLIMIT_NICE"), LINUX) + assert hasattr(psutil, "RLIMIT_NICE") == LINUX if kernel_version() >= (2, 6, 12): - ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX) + assert hasattr(psutil, "RLIMIT_RTPRIO") == LINUX if kernel_version() >= (2, 6, 25): - ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX) + assert hasattr(psutil, "RLIMIT_RTTIME") == LINUX if kernel_version() >= (2, 6, 8): - ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX) + assert hasattr(psutil, "RLIMIT_SIGPENDING") == LINUX - ae(hasattr(psutil, "RLIMIT_SWAP"), FREEBSD) - ae(hasattr(psutil, "RLIMIT_SBSIZE"), FREEBSD) - ae(hasattr(psutil, "RLIMIT_NPTS"), FREEBSD) + assert hasattr(psutil, "RLIMIT_SWAP") == FREEBSD + assert hasattr(psutil, "RLIMIT_SBSIZE") == FREEBSD + assert hasattr(psutil, "RLIMIT_NPTS") == FREEBSD class TestAvailSystemAPIs(PsutilTestCase): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 836311e220..1fffa08eed 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -914,7 +914,7 @@ 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) + # assert abs(vmstat_value - psutil_value) < 500 def test_interrupts(self): vmstat_value = vmstat("interrupts") @@ -977,8 +977,7 @@ def test_ips(self): # found += 1 # name = line.split(':')[1].strip() # self.assertIn(name, nics) - # self.assertEqual(len(nics), found, msg="{}\n---\n{}".format( - # pprint.pformat(nics), out)) + # assert len(nics) == found @pytest.mark.skipif(not LINUX, reason="LINUX only") @@ -1524,14 +1523,12 @@ def test_procfs_path(self): psutil.cpu_times(percpu=True) with pytest.raises(OSError): psutil.boot_time() - # self.assertRaises(OSError, psutil.pids) with pytest.raises(OSError): psutil.net_connections() with pytest.raises(OSError): psutil.net_io_counters() with pytest.raises(OSError): psutil.net_if_stats() - # self.assertRaises(OSError, psutil.disk_io_counters) with pytest.raises(OSError): psutil.disk_partitions() with pytest.raises(psutil.NoSuchProcess): @@ -1967,14 +1964,6 @@ def test_terminal_mocked(self): assert psutil._pslinux.Process(os.getpid()).terminal() is None 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() diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index c484264b91..1da6995722 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -342,7 +342,7 @@ def test_ad_on_process_creation(self): with mock.patch.object( psutil.Process, '_get_ident', side_effect=psutil.NoSuchProcess(1) ) as meth: - with self.assertRaises(psutil.NoSuchProcess): + with pytest.raises(psutil.NoSuchProcess): psutil.Process() assert meth.called diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 563851673d..0f47c11504 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -278,8 +278,8 @@ def waste_cpu(): waste_cpu() a = psutil.Process().cpu_times() b = os.times() - self.assertAlmostEqual(a.user, b.user, delta=0.1) - self.assertAlmostEqual(a.system, b.system, delta=0.1) + assert abs(a.user - b.user) < 0.1 + assert abs(a.system - b.system) < 0.1 @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") def test_cpu_num(self): diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index aaa3fa01df..b7d3bcf514 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -48,14 +48,14 @@ def proc_info(pid): tcase = PsutilTestCase() def check_exception(exc, proc, name, ppid): - tcase.assertEqual(exc.pid, pid) + assert exc.pid == pid if exc.name is not None: - tcase.assertEqual(exc.name, name) + assert 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) + assert exc.ppid == ppid elif isinstance(exc, psutil.NoSuchProcess): tcase.assertProcessGone(proc) str(exc) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 09c3ce50f4..fb11259982 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -807,8 +807,9 @@ def test_net_if_addrs(self): # Not reliable on all platforms (net_if_addrs() reports more # interfaces). - # self.assertEqual(sorted(nics.keys()), - # sorted(psutil.net_io_counters(pernic=True).keys())) + # assert sorted(nics.keys()) == sorted( + # psutil.net_io_counters(pernic=True).keys() + # ) families = {socket.AF_INET, socket.AF_INET6, psutil.AF_LINK} for nic, addrs in nics.items(): diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 6db66e50eb..a47de6f349 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -523,7 +523,7 @@ def test_passed(self): pass """).lstrip()) with mock.patch.object(psutil.tests, "HERE", tmpdir): - with self.assertWarnsRegex( + with unittest.TestCase().assertWarnsRegex( UserWarning, "Fake pytest module was used" ): suite = fake_pytest.main() diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index c5c536b468..fefcebe5ae 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -442,17 +442,14 @@ def test_cmdline(self): # XXX - occasional failures # def test_cpu_times(self): - # handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - # win32con.FALSE, os.getpid()) + # 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) + # a = psutil.Process().cpu_times() + # b = win32process.GetProcessTimes(handle) + # assert abs(a.user - b['UserTime'] / 10000000.0) < 0.2 + # assert abs(a.user - b['KernelTime'] / 10000000.0) < 0.2 def test_nice(self): handle = win32api.OpenProcess( From f202cf7d60f16c59f0ea71c7dafd4088eab7b519 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Apr 2025 19:51:08 +0200 Subject: [PATCH 1216/1714] use fake pytest if tests were invoked via `python3 -m unittest` --- psutil/tests/__init__.py | 23 ++++++++++++++++++----- psutil/tests/test_testutils.py | 7 ++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f6d87a64ed..9633d57380 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -885,6 +885,17 @@ class fake_pytest: $ python3 -m psutil.tests """ + @staticmethod + def _warn_on_exit(): + def _warn_on_exit(): + warnings.warn( + "Fake pytest module was used. Test results may be inaccurate.", + UserWarning, + stacklevel=1, + ) + + atexit.register(_warn_on_exit) + @staticmethod def main(*args, **kw): # noqa: ARG004 """Mimics pytest.main(). It has the same effect as running @@ -892,11 +903,6 @@ def main(*args, **kw): # noqa: ARG004 """ suite = unittest.TestLoader().discover(HERE) unittest.TextTestRunner(verbosity=2).run(suite) - warnings.warn( - "Fake pytest module was used. Test results may be inaccurate.", - UserWarning, - stacklevel=1, - ) return suite @staticmethod @@ -955,7 +961,14 @@ def __call__(self, cls_or_meth): if pytest is None: + # pytest not installed + pytest = fake_pytest + fake_pytest._warn_on_exit() +elif "PYTEST_VERSION" not in os.environ: + # test run likely invoked via `python3 -m unittest` pytest = fake_pytest + sys.modules["pytest"] = fake_pytest + fake_pytest._warn_on_exit() class PsutilTestCase(unittest.TestCase): diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index a47de6f349..e2170416b1 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -523,11 +523,8 @@ def test_passed(self): pass """).lstrip()) with mock.patch.object(psutil.tests, "HERE", tmpdir): - with unittest.TestCase().assertWarnsRegex( - UserWarning, "Fake pytest module was used" - ): - suite = fake_pytest.main() - assert suite.countTestCases() == 1 + suite = fake_pytest.main() + assert suite.countTestCases() == 1 def test_warns(self): # success From 11cb0844f5dbdaec77f48dacc9904f1516062473 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Apr 2025 23:55:19 +0200 Subject: [PATCH 1217/1714] rm PYTEST_VERSION env check --- psutil/tests/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 9633d57380..16db3dabc9 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -964,11 +964,6 @@ def __call__(self, cls_or_meth): # pytest not installed pytest = fake_pytest fake_pytest._warn_on_exit() -elif "PYTEST_VERSION" not in os.environ: - # test run likely invoked via `python3 -m unittest` - pytest = fake_pytest - sys.modules["pytest"] = fake_pytest - fake_pytest._warn_on_exit() class PsutilTestCase(unittest.TestCase): From 1086c519e01ddf1e7cae882b4baca1d2547fd344 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 2 Apr 2025 23:34:52 +0200 Subject: [PATCH 1218/1714] Convert unittest idioms to pytest (round 2) (#2531) --- psutil/_common.py | 1 - psutil/tests/__init__.py | 27 +++++++++++++------- psutil/tests/test_bsd.py | 4 +-- psutil/tests/test_linux.py | 17 ++++++------ psutil/tests/test_misc.py | 11 +++----- psutil/tests/test_posix.py | 4 +-- psutil/tests/test_process.py | 44 +++++++++++++++----------------- psutil/tests/test_process_all.py | 12 ++++----- psutil/tests/test_scripts.py | 4 +-- psutil/tests/test_system.py | 15 +++++------ psutil/tests/test_testutils.py | 28 +++++++++++--------- psutil/tests/test_windows.py | 10 +++++--- 12 files changed, 89 insertions(+), 88 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 4096c0a18c..254468efd3 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -523,7 +523,6 @@ def path_exists_strict(path): return True -@memoize def supports_ipv6(): """Return True if IPv6 is supported on this platform.""" if not socket.has_ipv6 or AF_INET6 is None: diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 16db3dabc9..7b8734fe28 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -943,6 +943,11 @@ def skip(reason=""): """Mimics `unittest.SkipTest`.""" raise unittest.SkipTest(reason) + @staticmethod + def fail(reason=""): + """Mimics `pytest.fail`.""" + return unittest.TestCase().fail(reason) + class mark: @staticmethod @@ -960,6 +965,10 @@ def __call__(self, cls_or_meth): return cls_or_meth +# to make pytest.fail() exception catchable +fake_pytest.fail.Exception = AssertionError + + if pytest is None: # pytest not installed pytest = fake_pytest @@ -1013,7 +1022,7 @@ def _check_proc_exc(self, proc, exc): str(exc) repr(exc) - def assertPidGone(self, pid): + def assert_pid_gone(self, pid): with pytest.raises(psutil.NoSuchProcess) as cm: try: psutil.Process(pid) @@ -1025,8 +1034,8 @@ def assertPidGone(self, pid): assert pid not in psutil.pids() assert pid not in [x.pid for x in psutil.process_iter()] - def assertProcessGone(self, proc): - self.assertPidGone(proc.pid) + def assert_proc_gone(self, proc): + self.assert_pid_gone(proc.pid) ns = process_namespace(proc) for fun, name in ns.iter(ns.all, clear_cache=True): with self.subTest(proc=proc, name=name): @@ -1044,7 +1053,7 @@ def assertProcessGone(self, proc): raise AssertionError(msg) proc.wait(timeout=0) # assert not raise TimeoutExpired - def assertProcessZombie(self, proc): + def assert_proc_zombie(self, proc): # A zombie process should always be instantiable. clone = psutil.Process(proc.pid) # Cloned zombie on Open/NetBSD has null creation time, see: @@ -1098,7 +1107,7 @@ def assertProcessZombie(self, proc): # 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) + # assert proc.pid in descendants # __eq__ can't be relied upon because creation time may not be # querable. @@ -1189,13 +1198,13 @@ def _check_fds(self, fun): f"negative diff {diff!r} (gc probably collected a" " resource from a previous test)" ) - raise self.fail(msg) + raise pytest.fail(msg) if diff > 0: type_ = "fd" if POSIX else "handle" if diff > 1: type_ += "s" msg = f"{diff} unclosed {type_} after calling {fun!r}" - raise self.fail(msg) + raise pytest.fail(msg) def _call_ntimes(self, fun, times): """Get 2 distinct memory samples, before and after having @@ -1236,7 +1245,7 @@ def _check_mem(self, fun, times, retries, tolerance): self._log(msg) times += increase prev_mem = mem - raise self.fail(". ".join(messages)) + raise pytest.fail(". ".join(messages)) # --- @@ -1276,7 +1285,7 @@ def call(): except exc: pass else: - raise self.fail(f"{fun} did not raise {exc}") + raise pytest.fail(f"{fun} did not raise {exc}") self.execute(call, **kwargs) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 9ed1601502..e2a6b0b696 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -117,9 +117,9 @@ def df(path): assert usage.total == total # 10 MB tolerance if abs(usage.free - free) > 10 * 1024 * 1024: - raise self.fail(f"psutil={usage.free}, df={free}") + raise pytest.fail(f"psutil={usage.free}, df={free}") if abs(usage.used - used) > 10 * 1024 * 1024: - raise self.fail(f"psutil={usage.used}, df={used}") + raise pytest.fail(f"psutil={usage.used}, df={used}") @pytest.mark.skipif( not shutil.which("sysctl"), reason="sysctl cmd not available" diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 1fffa08eed..c716d63c3c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -976,7 +976,7 @@ def test_ips(self): # if re.search(r"^\d+:", line): # found += 1 # name = line.split(':')[1].strip() - # self.assertIn(name, nics) + # assert name in nics # assert len(nics) == found @@ -1032,7 +1032,7 @@ def test_flags(self): assert ifconfig_flags == psutil_flags if not matches_found: - raise self.fail("no matches were found") + raise pytest.fail("no matches were found") @pytest.mark.skipif(not LINUX, reason="LINUX only") @@ -1093,13 +1093,12 @@ class TestSystemNetConnections(PsutilTestCase): @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 OSError: - pass - psutil.net_connections(kind='inet6') + with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: + try: + s.bind(("::1", 0)) + except OSError: + pass + psutil.net_connections(kind='inet6') def test_emulate_unix(self): content = textwrap.dedent("""\ diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 1da6995722..4b330036c7 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -218,7 +218,9 @@ def test__all__(self): fun.__doc__ is not None and 'deprecated' not in fun.__doc__.lower() ): - raise self.fail(f"{name!r} not in psutil.__all__") + raise pytest.fail( + f"{name!r} not in psutil.__all__" + ) # Import 'star' will break if __all__ is inconsistent, see: # https://github.com/giampaolo/psutil/issues/656 @@ -545,35 +547,28 @@ def k(s): assert 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=OSError ) 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 pytest.raises(OSError): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index a7844929e7..e7b2bba2f3 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -338,7 +338,7 @@ def test_pids(self): 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)) + raise pytest.fail("difference: " + str(difference)) # for some reason ifconfig -a does not report all interfaces # returned by psutil @@ -352,7 +352,7 @@ def test_nic_names(self): if line.startswith(nic): break else: - raise self.fail( + raise pytest.fail( f"couldn't find {nic} nic in 'ifconfig -a'" f" output\n{output}" ) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 0f47c11504..91817f34e9 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -78,7 +78,7 @@ def spawn_psproc(self, *args, **kwargs): try: return psutil.Process(sproc.pid) except psutil.NoSuchProcess: - self.assertPidGone(sproc.pid) + self.assert_pid_gone(sproc.pid) raise # --- @@ -97,7 +97,7 @@ def test_kill(self): assert code == signal.SIGTERM else: assert code == -signal.SIGKILL - self.assertProcessGone(p) + self.assert_proc_gone(p) def test_terminate(self): p = self.spawn_psproc() @@ -107,7 +107,7 @@ def test_terminate(self): assert code == signal.SIGTERM else: assert code == -signal.SIGTERM - self.assertProcessGone(p) + self.assert_proc_gone(p) def test_send_signal(self): sig = signal.SIGKILL if POSIX else signal.SIGTERM @@ -118,7 +118,7 @@ def test_send_signal(self): assert code == sig else: assert code == -sig - self.assertProcessGone(p) + self.assert_proc_gone(p) @pytest.mark.skipif(not POSIX, reason="not POSIX") def test_send_signal_mocked(self): @@ -140,25 +140,25 @@ def test_wait_exited(self): p = self.spawn_psproc(cmd) code = p.wait() assert code == 0 - self.assertProcessGone(p) + self.assert_proc_gone(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() assert code == 1 - self.assertProcessGone(p) + self.assert_proc_gone(p) # via sys.exit() cmd = [PYTHON_EXE, "-c", "import sys; sys.exit(5);"] p = self.spawn_psproc(cmd) code = p.wait() assert code == 5 - self.assertProcessGone(p) + self.assert_proc_gone(p) # via os._exit() cmd = [PYTHON_EXE, "-c", "import os; os._exit(5);"] p = self.spawn_psproc(cmd) code = p.wait() assert code == 5 - self.assertProcessGone(p) + self.assert_proc_gone(p) @pytest.mark.skipif(NETBSD, reason="fails on NETBSD") def test_wait_stopped(self): @@ -232,12 +232,12 @@ def test_wait_timeout_nonblocking(self): except psutil.TimeoutExpired: pass else: - raise self.fail('timeout') + raise pytest.fail('timeout') if POSIX: assert code == -signal.SIGKILL else: assert code == signal.SIGTERM - self.assertProcessGone(p) + self.assert_proc_gone(p) def test_cpu_percent(self): p = psutil.Process() @@ -300,7 +300,7 @@ def test_create_time(self): # It will fail if the difference between the values is > 2s. difference = abs(create_time - now) if difference > 2: - raise self.fail( + raise pytest.fail( f"expected: {now}, found: {create_time}, difference:" f" {difference}" ) @@ -1091,7 +1091,7 @@ def test_open_files_2(self): ): break else: - raise self.fail(f"no file found; files={p.open_files()!r}") + raise pytest.fail(f"no file found; files={p.open_files()!r}") assert normcase(file.path) == normcase(fileobj.name) if WINDOWS: assert file.fd == -1 @@ -1109,14 +1109,10 @@ def test_num_fds(self): p = psutil.Process() testfn = self.get_testfn() start = p.num_fds() - file = open(testfn, 'w') # noqa: SIM115 - self.addCleanup(file.close) - assert p.num_fds() == start + 1 - sock = socket.socket() - self.addCleanup(sock.close) - assert p.num_fds() == start + 2 - file.close() - sock.close() + with open(testfn, 'w'): + assert p.num_fds() == start + 1 + with socket.socket(): + assert p.num_fds() == start + 2 assert p.num_fds() == start @skip_on_not_implemented(only_if=LINUX) @@ -1131,7 +1127,7 @@ def test_num_ctx_switches(self): after = sum(p.num_ctx_switches()) if after > before: return - raise self.fail("num ctx switches still the same after 2 iterations") + raise pytest.fail("num ctx switches still the same after 2 iterations") def test_ppid(self): p = psutil.Process() @@ -1362,7 +1358,7 @@ def assert_raises_nsp(fun, fun_name): # NtQuerySystemInformation succeeds even if process is gone. if WINDOWS and fun_name in {'exe', 'name'}: return - raise self.fail( + raise pytest.fail( f"{fun!r} didn't raise NSP and returned {ret!r} instead" ) @@ -1371,7 +1367,7 @@ def assert_raises_nsp(fun, fun_name): p.wait() if WINDOWS: # XXX call_until(lambda: p.pid not in psutil.pids()) - self.assertProcessGone(p) + self.assert_proc_gone(p) ns = process_namespace(p) for fun, name in ns.iter(ns.all): @@ -1380,7 +1376,7 @@ def assert_raises_nsp(fun, fun_name): @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process(self): _parent, zombie = self.spawn_zombie() - self.assertProcessZombie(zombie) + self.assert_proc_zombie(zombie) @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process_is_running_w_exc(self): diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index b7d3bcf514..aba8686a22 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -52,12 +52,12 @@ def check_exception(exc, proc, name, ppid): if exc.name is not None: assert exc.name == name if isinstance(exc, psutil.ZombieProcess): - tcase.assertProcessZombie(proc) + tcase.assert_proc_zombie(proc) if exc.ppid is not None: tcase.assertGreaterEqual(exc.ppid, 0) assert exc.ppid == ppid elif isinstance(exc, psutil.NoSuchProcess): - tcase.assertProcessGone(proc) + tcase.assert_proc_gone(proc) str(exc) repr(exc) @@ -71,12 +71,12 @@ def do_wait(): try: proc = psutil.Process(pid) except psutil.NoSuchProcess: - tcase.assertPidGone(pid) + tcase.assert_pid_gone(pid) return {} try: d = proc.as_dict(['ppid', 'name']) except psutil.NoSuchProcess: - tcase.assertProcessGone(proc) + tcase.assert_proc_gone(proc) else: name, ppid = d['name'], d['ppid'] info = {'pid': proc.pid} @@ -148,7 +148,7 @@ def test_all(self): if value not in (0, 0.0, [], None, '', {}): assert value, value if failures: - raise self.fail(''.join(failures)) + raise pytest.fail(''.join(failures)) def cmdline(self, ret, info): assert isinstance(ret, list) @@ -203,7 +203,7 @@ def create_time(self, ret, info): else: raise # this can't be taken for granted on all platforms - # self.assertGreaterEqual(ret, psutil.boot_time()) + # assert 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)) diff --git a/psutil/tests/test_scripts.py b/psutil/tests/test_scripts.py index de0ad2af74..5d2afbe28a 100755 --- a/psutil/tests/test_scripts.py +++ b/psutil/tests/test_scripts.py @@ -77,7 +77,7 @@ def test_coverage(self): if name.endswith('.py'): if 'test_' + os.path.splitext(name)[0] not in meths: # self.assert_stdout(name) - raise self.fail( + raise pytest.fail( "no test defined for" f" {os.path.join(SCRIPTS_DIR, name)!r} script" ) @@ -89,7 +89,7 @@ def test_executable(self): if file.endswith('.py'): path = os.path.join(root, file) if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]: - raise self.fail(f"{path!r} is not executable") + raise pytest.fail(f"{path!r} is not executable") def test_disk_usage(self): self.assert_stdout('disk_usage.py') diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index fb11259982..85219cf609 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -324,9 +324,9 @@ def test_virtual_memory(self): assert isinstance(value, int) if name != 'total': if not value >= 0: - raise self.fail(f"{name!r} < 0 ({value})") + raise pytest.fail(f"{name!r} < 0 ({value})") if value > mem.total: - raise self.fail( + raise pytest.fail( f"{name!r} > total (total={mem.total}, {name}={value})" ) @@ -416,9 +416,7 @@ def test_cpu_times(self): # for field in new._fields: # new_t = getattr(new, field) # last_t = getattr(last, field) - # self.assertGreaterEqual( - # new_t, last_t, - # msg="{} {}".format(new_t, last_t)) + # assert new_t >= last_t # last = new def test_cpu_times_time_increases(self): @@ -429,7 +427,7 @@ def test_cpu_times_time_increases(self): t2 = sum(psutil.cpu_times()) if t2 > t1: return - raise self.fail("time remained the same") + raise pytest.fail("time remained the same") def test_per_cpu_times(self): # Check type, value >= 0, str(). @@ -461,8 +459,7 @@ def test_per_cpu_times(self): # for field in newcpu._fields: # new_t = getattr(newcpu, field) # last_t = getattr(lastcpu, field) - # self.assertGreaterEqual( - # new_t, last_t, msg="{} {}".format(lastcpu, newcpu)) + # assert new_t >= last_t # last = new def test_per_cpu_times_2(self): @@ -472,7 +469,7 @@ def test_per_cpu_times_2(self): giveup_at = time.time() + GLOBAL_TIMEOUT while True: if time.time() >= giveup_at: - return self.fail("timeout") + return pytest.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) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index e2170416b1..b1e0de4991 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -256,12 +256,12 @@ def test_terminate(self): # by subprocess.Popen p = self.spawn_testproc() terminate(p) - self.assertPidGone(p.pid) + self.assert_pid_gone(p.pid) terminate(p) # by psutil.Process p = psutil.Process(self.spawn_testproc().pid) terminate(p) - self.assertPidGone(p.pid) + self.assert_pid_gone(p.pid) terminate(p) # by psutil.Popen cmd = [ @@ -276,20 +276,20 @@ def test_terminate(self): env=PYTHON_EXE_ENV, ) terminate(p) - self.assertPidGone(p.pid) + self.assert_pid_gone(p.pid) terminate(p) # by PID pid = self.spawn_testproc().pid terminate(pid) - self.assertPidGone(p.pid) + self.assert_pid_gone(p.pid) terminate(pid) # zombie if POSIX: parent, zombie = self.spawn_zombie() terminate(parent) terminate(zombie) - self.assertPidGone(parent.pid) - self.assertPidGone(zombie.pid) + self.assert_pid_gone(parent.pid) + self.assert_pid_gone(zombie.pid) class TestNetUtils(PsutilTestCase): @@ -398,7 +398,7 @@ def fun(ls=ls): try: # will consume around 60M in total - with pytest.raises(AssertionError, match="extra-mem"): + with pytest.raises(pytest.fail.Exception, match="extra-mem"): self.execute(fun, times=100) finally: del ls @@ -411,7 +411,7 @@ def fun(): box = [] kind = "fd" if POSIX else "handle" - with pytest.raises(AssertionError, match="unclosed " + kind): + with pytest.raises(pytest.fail.Exception, match="unclosed " + kind): self.execute(fun) def test_tolerance(self): @@ -436,7 +436,7 @@ def fun_1(): def fun_2(): pass - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): self.execute_w_exc(ZeroDivisionError, fun_2) @@ -462,7 +462,7 @@ def test_raises(self): except AssertionError as err: assert str(err) == '"foo" does not match "bar"' else: - raise self.fail("exception not raised") + raise pytest.fail("exception not raised") def test_mark(self): @fake_pytest.mark.xdist_group(name="serial") @@ -538,7 +538,7 @@ def test_warns(self): except AssertionError: pass else: - raise self.fail("exception not raised") + raise pytest.fail("exception not raised") # match success with fake_pytest.warns(UserWarning, match="foo"): @@ -551,7 +551,11 @@ def test_warns(self): except AssertionError: pass else: - raise self.fail("exception not raised") + raise pytest.fail("exception not raised") + + def test_fail(self): + with fake_pytest.raises(fake_pytest.fail.Exception): + raise fake_pytest.fail("reason") class TestTestingUtils(PsutilTestCase): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index fefcebe5ae..079bd6837e 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -141,7 +141,7 @@ def test_nic_names(self): if "pseudo-interface" in nic.replace(' ', '-').lower(): continue if nic not in out: - raise self.fail( + raise pytest.fail( f"{nic!r} nic wasn't found in 'ipconfig /all' output" ) @@ -222,10 +222,12 @@ def test_disks(self): assert usage.free == wmi_free # 10 MB tolerance if abs(usage.free - wmi_free) > 10 * 1024 * 1024: - raise self.fail(f"psutil={usage.free}, wmi={wmi_free}") + raise pytest.fail( + f"psutil={usage.free}, wmi={wmi_free}" + ) break else: - raise self.fail(f"can't find partition {ps_part!r}") + raise pytest.fail(f"can't find partition {ps_part!r}") @retry_on_failure() def test_disk_usage(self): @@ -621,7 +623,7 @@ def test_memory_vms(self): # returned instead. wmi_usage = int(w.PageFileUsage) if vms not in {wmi_usage, wmi_usage * 1024}: - raise self.fail(f"wmi={wmi_usage}, psutil={vms}") + raise pytest.fail(f"wmi={wmi_usage}, psutil={vms}") def test_create_time(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] From a513b7c85e77b73e789016ea8127735943aa6fc9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 3 Apr 2025 18:24:18 +0200 Subject: [PATCH 1219/1714] Remove wrong PID reuse assumption in Process `children()` and `parent()` (#2537, fix #2533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While skimming through the code I noticed: https://github.com/giampaolo/psutil/blob/1086c519e01ddf1e7cae882b4baca1d2547fd344/psutil/__init__.py#L973-L975 ...and: https://github.com/giampaolo/psutil/blob/1086c519e01ddf1e7cae882b4baca1d2547fd344/psutil/__init__.py#L999-L1002 This was introduced in https://github.com/giampaolo/psutil/commit/8628deb8 13 years ago! Here, the code assumes that a child process's PID must always be greater than its parent's PID, or else it means its PID has been reused. For example: * Parent = 10, Child = 11 → Considered a child * Parent = 10, Child = 9 → Not considered a child This assumption is incorrect. When PIDs are reused and exceed /proc/sys/kernel/pid_max, they restart from 0. Even if this weren’t the case, we cannot generalize this assumption across all operating systems. The PID reuse algorithm likely behaves differently on Linux, Windows, or other systems. The same wrong assumption applies to `Process.parent()` method, introduced in https://github.com/giampaolo/psutil/commit/7e06d4b6 12 years ago: https://github.com/giampaolo/psutil/blob/1086c519e01ddf1e7cae882b4baca1d2547fd344/psutil/__init__.py#L598-L601 --- HISTORY.rst | 6 +++++- psutil/__init__.py | 24 ++++++++---------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ada5c62f38..65b0b5f0a4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,11 +7,15 @@ XXXX-XX-XX **Bug fixes** +- 2473_, [macOS]: Fix build issue on macOS 11 and lower. - 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due to a race condition. - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will now raise `AccessDenied`_ instead. -- 2473_, [macOS]: Fix build issue on macOS 11 and lower. +- 2533_: `Process.children()`_ previously skipped all PIDs lower than the + parent PID, based on the incorrect assumption that a lower child PID + indicated PID reuse. However, this assumption was flawed, as PIDs can restart + from 0. The same problem also affected `Process.parent()`_. 7.0.0 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index cf4a58057f..874222427c 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -593,13 +593,9 @@ def parent(self): return None ppid = self.ppid() if ppid is not None: - ctime = self.create_time() try: - parent = Process(ppid) - if parent.create_time() <= ctime: - return parent - # ...else ppid has been reused by another process - except NoSuchProcess: + return Process(ppid) + except (NoSuchProcess, ZombieProcess): pass def parents(self): @@ -970,12 +966,10 @@ def children(self, recursive=False): if ppid == self.pid: try: child = Process(pid) - # if child happens to be older than its parent - # (self) it means child's PID has been reused - if self.create_time() <= child.create_time(): - ret.append(child) except (NoSuchProcess, ZombieProcess): pass + else: + ret.append(child) else: # Construct a {pid: [child pids]} dict reverse_ppid_map = collections.defaultdict(list) @@ -993,17 +987,15 @@ def children(self, recursive=False): # there's a cycle in the recorded process "tree". continue seen.add(pid) + for child_pid in reverse_ppid_map[pid]: try: child = Process(child_pid) - # if child happens to be older than its parent - # (self) it means child's PID has been reused - intime = self.create_time() <= child.create_time() - if intime: - ret.append(child) - stack.append(child_pid) except (NoSuchProcess, ZombieProcess): pass + else: + ret.append(child) + stack.append(child_pid) return ret def cpu_percent(self, interval=None): From 3cd222d1be028e94bbeb3c3394645b0f0b549a57 Mon Sep 17 00:00:00 2001 From: Andrew <47915356+kuzmich321@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:07:22 +0200 Subject: [PATCH 1220/1714] fix doc string (#2535) --- docs/index.rst | 2 +- psutil/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6b084fddbc..a32e29a462 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -935,7 +935,7 @@ Functions Every :class:`Process` instance is only created once, and then cached for the next time :func:`psutil.process_iter()` is called (if PID is still alive). - Cache can optionally be cleared via ``process_iter.clear_cache()``. + Cache can optionally be cleared via ``process_iter.cache_clear()``. *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. If *attrs* is specified :meth:`Process.as_dict()` result will be stored as a diff --git a/psutil/__init__.py b/psutil/__init__.py index 874222427c..958df662d3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1476,7 +1476,7 @@ def process_iter(attrs=None, ad_value=None): Every new Process instance is only created once and then cached into an internal table which is updated every time this is used. - Cache can optionally be cleared via `process_iter.clear_cache()`. + Cache can optionally be cleared via `process_iter.cache_clear()`. The sorting order in which processes are yielded is based on their PIDs. From 2975b218de46cd6fece44cafeb4de4a1cdf6a8ae Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 3 Apr 2025 22:34:24 +0200 Subject: [PATCH 1221/1714] add pytest-instafail plugin + move pytest config into pyproject.toml --- Makefile | 37 ++++++++++++++++++------------------- docs/index.rst | 2 +- psutil/__init__.py | 2 +- psutil/tests/__main__.py | 4 ++-- pyproject.toml | 4 ++++ scripts/internal/winmake.py | 8 +++----- setup.py | 1 + 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index db1a420d95..8faf6f7ca0 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,6 @@ SETUP_INSTALL_ARGS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade -PYTEST_ARGS = -v -s --tb=short PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 # if make is invoked with no arg, default to `make help` @@ -92,69 +91,69 @@ install-git-hooks: ## Install GIT pre-commit hook. test: ## Run all tests. To run a specific test do "make test ARGS=psutil.tests.test_system.TestDiskAPIs" ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) --ignore=psutil/tests/test_memleaks.py $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py $(ARGS) test-parallel: ## Run all tests in parallel. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) --ignore=psutil/tests/test_memleaks.py -n auto --dist loadgroup $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py -n auto --dist loadgroup $(ARGS) test-process: ## Run process-related API tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_process.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_process.py test-process-all: ## Run tests which iterate over all process PIDs. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_process_all.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_process_all.py test-system: ## Run system-related API tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_system.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_system.py test-misc: ## Run miscellaneous tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_misc.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_misc.py test-scripts: ## Run scripts tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_scripts.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_scripts.py test-testutils: ## Run test utils tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_testutils.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_testutils.py test-unicode: ## Test APIs dealing with strings. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_unicode.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_unicode.py test-contracts: ## APIs sanity tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_contracts.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_contracts.py test-connections: ## Test psutil.net_connections() and Process.net_connections(). ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_connections.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_connections.py test-posix: ## POSIX specific tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_posix.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_posix.py test-platform: ## Run specific platform tests only. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py test-memleaks: ## Memory leak tests. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) $(ARGS) psutil/tests/test_memleaks.py + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_memleaks.py test-last-failed: ## Re-run tests which failed on last run ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(PYTEST_ARGS) --last-failed $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --last-failed $(ARGS) test-coverage: ## Run test coverage. ${MAKE} build # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov - $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest $(PYTEST_ARGS) --ignore=psutil/tests/test_memleaks.py $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest --ignore=psutil/tests/test_memleaks.py $(ARGS) $(PYTHON) -m coverage report @echo "writing results to htmlcov/index.html" $(PYTHON) -m coverage html @@ -164,8 +163,8 @@ test-ci: ${MAKE} install-sysdeps mkdir -p .tests cd .tests/ && $(PYTHON) -c "from psutil.tests import print_sysinfo; print_sysinfo()" - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest $(PYTEST_ARGS) --pyargs psutil.tests - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'test_memleaks.py'" $(PYTHON) -m pytest $(PYTEST_ARGS) --pyargs psutil.tests + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests # =================================================================== # Linters diff --git a/docs/index.rst b/docs/index.rst index 6b084fddbc..a32e29a462 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -935,7 +935,7 @@ Functions Every :class:`Process` instance is only created once, and then cached for the next time :func:`psutil.process_iter()` is called (if PID is still alive). - Cache can optionally be cleared via ``process_iter.clear_cache()``. + Cache can optionally be cleared via ``process_iter.cache_clear()``. *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. If *attrs* is specified :meth:`Process.as_dict()` result will be stored as a diff --git a/psutil/__init__.py b/psutil/__init__.py index 874222427c..958df662d3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1476,7 +1476,7 @@ def process_iter(attrs=None, ad_value=None): Every new Process instance is only created once and then cached into an internal table which is updated every time this is used. - Cache can optionally be cleared via `process_iter.clear_cache()`. + Cache can optionally be cleared via `process_iter.cache_clear()`. The sorting order in which processes are yielded is based on their PIDs. diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index ce6fc24c7f..ab94163828 100644 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -3,10 +3,10 @@ # found in the LICENSE file. """Run unit tests. This is invoked by: -$ python -m psutil.tests. +$ python3 -m psutil.tests. """ from psutil.tests import pytest -pytest.main(["-v", "-s", "--tb=short"]) +pytest.main() diff --git a/pyproject.toml b/pyproject.toml index d1268ad8e0..15fb469472 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -230,6 +230,10 @@ test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/s [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] +[tool.pytest.ini_options] +addopts = "--verbose --capture=no --no-header --tb=short --instafail" +testpaths = ["psutil/tests/"] + [build-system] build-backend = "setuptools.build_meta" requires = ["setuptools>=43"] diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 882729f972..5c5d02e735 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -25,7 +25,6 @@ PYTHON = os.getenv('PYTHON', sys.executable) -PYTEST_ARGS = ["-v", "-s", "--tb=short"] HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) WINDOWS = os.name == "nt" @@ -312,7 +311,6 @@ def test(args=None): args = args or [] sh( [PYTHON, "-m", "pytest", "--ignore=psutil/tests/test_memleaks.py"] - + PYTEST_ARGS + args ) @@ -320,13 +318,13 @@ def test(args=None): def test_by_name(arg): """Run specific test by name.""" build() - print(" ".join([PYTHON, "-m", "pytest"] + PYTEST_ARGS + [arg])) + print(" ".join([PYTHON, "-m", "pytest"] + [arg])) def test_by_regex(arg): """Run specific test by name.""" build() - sh([PYTHON, "-m", "pytest"] + PYTEST_ARGS + ["-k", arg]) + sh([PYTHON, "-m", "pytest"] + ["-k", arg]) def test_parallel(): @@ -337,7 +335,7 @@ def coverage(): """Run coverage tests.""" # Note: coverage options are controlled by .coveragerc file build() - sh([PYTHON, "-m", "coverage", "run", "-m", "pytest"] + PYTEST_ARGS) + sh([PYTHON, "-m", "coverage", "run", "-m", "pytest"]) sh([PYTHON, "-m", "coverage", "report"]) sh([PYTHON, "-m", "coverage", "html"]) sh([PYTHON, "-m", "webbrowser", "-t", "htmlcov/index.html"]) diff --git a/setup.py b/setup.py index 437bc4e35f..2a45eee5b9 100755 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ # `make install-pydeps-test`. TEST_DEPS = [ "pytest", + "pytest-instafail", "pytest-subtests", "pytest-xdist", "setuptools", From 9ff21ddbffa69da685aea470f5464d1339b426d3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 4 Apr 2025 22:26:46 +0200 Subject: [PATCH 1222/1714] Pytest speedup when running a single test (#2538) Disable unused pytest plugins via `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1` env var, gaining around 0.12 secs when running an individual test via CLI. See blog post: https://gmpy.dev/blog/2025/speedup-pytest-startup --- Makefile | 4 ++-- psutil/tests/__init__.py | 33 +++++++++++++++++++++++++-------- pyproject.toml | 16 +++++++++++++++- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 8faf6f7ca0..25f3905f92 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ SETUP_INSTALL_ARGS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade -PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 +PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help @@ -95,7 +95,7 @@ test: ## Run all tests. To run a specific test do "make test ARGS=psutil.tests. test-parallel: ## Run all tests in parallel. ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py -n auto --dist loadgroup $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py -p xdist -n auto --dist loadgroup $(ARGS) test-process: ## Run process-related API tests. ${MAKE} build diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 7b8734fe28..f52a163df8 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -877,6 +877,30 @@ def get_testfn(suffix="", dir=None): # =================================================================== +def fake_xdist_group(*_args, **_kwargs): + """Mimics `@pytest.mark.xdist_group` decorator. No-op: it just + calls the test method or return the decorated class. + """ + + def wrapper(obj): + @functools.wraps(obj) + def inner(*args, **kwargs): + return obj(*args, **kwargs) + + return obj if isinstance(obj, type) else inner + + return wrapper + + +if not PYTEST_PARALLEL: + # Fake @pytest.mark.xdist_group decorator. This is here so that we + # can run pytest with PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 env var, and + # hence avoid loading pytest-xdist plugin (faster pytest startup). + # When we want to run parallel test we will explicitly enable + # pytest-xdist from the cmdline via `pytest -p xdist …`. + pytest.mark.xdist_group = fake_xdist_group + + class fake_pytest: """A class that mimics some basic pytest APIs. This is meant for when unit tests are run in production, where pytest may not be @@ -955,14 +979,7 @@ def skipif(condition, reason=""): """Mimics `@pytest.mark.skipif` decorator.""" return unittest.skipIf(condition, reason) - class xdist_group: - """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" - - def __init__(self, name=None): - pass - - def __call__(self, cls_or_meth): - return cls_or_meth + xdist_group = fake_xdist_group # to make pytest.fail() exception catchable diff --git a/pyproject.toml b/pyproject.toml index 15fb469472..ca64e8f02e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,7 +231,21 @@ test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/s archs = ["arm64", "x86_64"] [tool.pytest.ini_options] -addopts = "--verbose --capture=no --no-header --tb=short --instafail" +addopts = ''' + --verbose + --capture=no + --no-header + --tb=short + --strict-config + --strict-markers + --instafail + -p no:junitxml + -p no:doctest + -p no:nose + -p no:pastebin + -p instafail + -p subtests + ''' testpaths = ["psutil/tests/"] [build-system] From 680be3551a097b6bf03d0eec232aa707cc37a69f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 5 Apr 2025 12:48:48 +0200 Subject: [PATCH 1223/1714] have retry() decorator also catch pytest.fail.Exception + bump up ver --- HISTORY.rst | 2 +- Makefile | 1 - psutil/__init__.py | 2 +- psutil/tests/__init__.py | 9 ++++++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 65b0b5f0a4..90d90f5033 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.1.0 +7.0.1 ===== XXXX-XX-XX diff --git a/Makefile b/Makefile index 25f3905f92..19c47fa678 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,6 @@ clean: ## Remove all build files. dist/ \ docs/_build/ \ htmlcov/ \ - pytest-cache* \ wheelhouse .PHONY: build diff --git a/psutil/__init__.py b/psutil/__init__.py index 958df662d3..3cbb1917ab 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -207,7 +207,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.0.0" +__version__ = "7.0.1" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f52a163df8..b42f390333 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -751,7 +751,7 @@ def wait_for_file(fname, delete=True, empty=False): @retry( - exception=AssertionError, + exception=(AssertionError, pytest.fail.Exception), logfun=None, timeout=GLOBAL_TIMEOUT, interval=0.001, @@ -1044,7 +1044,7 @@ def assert_pid_gone(self, pid): try: psutil.Process(pid) except psutil.ZombieProcess: - raise AssertionError("wasn't supposed to raise ZombieProcess") + raise pytest.fail("wasn't supposed to raise ZombieProcess") assert cm.value.pid == pid assert cm.value.name is None assert not psutil.pid_exists(pid), pid @@ -1658,7 +1658,10 @@ def logfun(exc): print(f"{exc!r}, retrying", file=sys.stderr) # noqa: T201 return retry( - exception=AssertionError, timeout=None, retries=retries, logfun=logfun + exception=(AssertionError, pytest.fail.Exception), + timeout=None, + retries=retries, + logfun=logfun, ) From e40ec579e5ad7a61b7bfbf73c519413738243a28 Mon Sep 17 00:00:00 2001 From: Jonathan Kohler Date: Sat, 5 Apr 2025 04:39:54 -0700 Subject: [PATCH 1224/1714] Ensure consistent `Process._ident` value on Linux (#2527) Signed-off-by: Jonathan Kohler * Use 'monotonic' process starttime since boot to form unique process identity on Linux, since it is stable over changes to system time --- psutil/__init__.py | 6 ++++++ psutil/_pslinux.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/psutil/__init__.py b/psutil/__init__.py index 3cbb1917ab..2e1c0047e8 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -386,6 +386,12 @@ def _get_ident(self): # https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555 self._create_time = self._proc.create_time(fast_only=True) return (self.pid, self._create_time) + elif LINUX: + # Use 'monotonic' process starttime since boot to form unique + # process identity, since it is stable over changes to system + # time + create_monotonic = self._proc.create_monotonic() + return (self.pid, create_monotonic) else: return (self.pid, self.create_time()) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 59a61be3a4..17dc60742c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1900,6 +1900,11 @@ def create_time(self): bt = BOOT_TIME or boot_time() return (ctime / CLOCK_TICKS) + bt + @wrap_exceptions + def create_monotonic(self): + ctime = float(self._parse_stat_file()['create_time']) + return ctime / CLOCK_TICKS + @wrap_exceptions def memory_info(self): # ============================================================ From 0e7ff010f01cecfb43baa561ae2b84c5c65daac4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 5 Apr 2025 14:05:57 +0200 Subject: [PATCH 1225/1714] give @kohlerjl credits for #2526 and #2527 --- CREDITS | 4 ++++ HISTORY.rst | 4 ++++ psutil/__init__.py | 5 ++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CREDITS b/CREDITS index 39485be58a..25f2408e35 100644 --- a/CREDITS +++ b/CREDITS @@ -848,3 +848,7 @@ I: 2496 N: Fabien Bousquet W: https://github.com/fafanoulele I: 2473 + +N: Jonathan Kohler +W: https://github.com/kohlerjl +I: 2526, 2527 diff --git a/HISTORY.rst b/HISTORY.rst index 90d90f5033..766f38845d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,10 @@ XXXX-XX-XX - 2473_, [macOS]: Fix build issue on macOS 11 and lower. - 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due to a race condition. +- 2526_, [Linux]: `Process.create_time()`_, which is used to univocally + identify a process over time, is subject to system clock updates, and as such + can lead to `Process.is_running()`_ returning a wrong result. A monotonic + creation time is now used instead. (patch by Jonathan Kohler) - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will now raise `AccessDenied`_ instead. - 2533_: `Process.children()`_ previously skipped all PIDs lower than the diff --git a/psutil/__init__.py b/psutil/__init__.py index 2e1c0047e8..39c2881530 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -389,9 +389,8 @@ def _get_ident(self): elif LINUX: # Use 'monotonic' process starttime since boot to form unique # process identity, since it is stable over changes to system - # time - create_monotonic = self._proc.create_monotonic() - return (self.pid, create_monotonic) + # time. + return (self.pid, self._proc.create_monotonic()) else: return (self.pid, self.create_time()) From d15abdc6cc3ba8defff6e57a779298b5e382d12b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 5 Apr 2025 18:21:30 +0200 Subject: [PATCH 1226/1714] refactor create_time(self, monotonic=False) on Linux (re. #2526) --- psutil/__init__.py | 2 +- psutil/_pslinux.py | 36 ++++++++++++++++++++++-------------- psutil/tests/test_linux.py | 5 +++++ psutil/tests/test_process.py | 14 ++------------ 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 39c2881530..9cd85b7ae6 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -390,7 +390,7 @@ def _get_ident(self): # Use 'monotonic' process starttime since boot to form unique # process identity, since it is stable over changes to system # time. - return (self.pid, self._proc.create_monotonic()) + return (self.pid, self._proc.create_time(monotonic=True)) else: return (self.pid, self.create_time()) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 17dc60742c..f922ecc6f3 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1665,12 +1665,20 @@ def wrapper(self, *args, **kwargs): class Process: """Linux process implementation.""" - __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] + __slots__ = [ + "_cache", + "_ctime", + "_name", + "_ppid", + "_procfs_path", + "pid", + ] def __init__(self, pid): self.pid = pid self._name = None self._ppid = None + self._ctime = None self._procfs_path = get_procfs_path() def _is_zombie(self): @@ -1890,20 +1898,20 @@ def wait(self, timeout=None): return _psposix.wait_pid(self.pid, timeout, self._name) @wrap_exceptions - def create_time(self): - 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. - # Also use cached value if available. + def create_time(self, monotonic=False): + # Start time unit is expressed in jiffies (clock ticks per + # second). It never changes and is not affected by system clock + # updates. + if self._ctime is None: + self._ctime = ( + float(self._parse_stat_file()['create_time']) / CLOCK_TICKS + ) + if monotonic: + return self._ctime + # Add the uptime, returning seconds since the epoch (this is + # subject to system clock updates). bt = BOOT_TIME or boot_time() - return (ctime / CLOCK_TICKS) + bt - - @wrap_exceptions - def create_monotonic(self): - ctime = float(self._parse_stat_file()['create_time']) - return ctime / CLOCK_TICKS + return self._ctime + bt @wrap_exceptions def memory_info(self): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index c716d63c3c..d82945c954 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -2198,6 +2198,11 @@ def test_net_connections_enametoolong(self): assert p.net_connections() == [] assert m.called + def test_create_time_monotonic(self): + p = psutil.Process() + assert p._proc.create_time() != p._proc.create_time(monotonic=True) + assert p._get_ident()[1] == p._proc.create_time(monotonic=True) + @pytest.mark.skipif(not LINUX, reason="LINUX only") class TestProcessAgainstStatus(PsutilTestCase): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 91817f34e9..62956a3419 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -293,18 +293,8 @@ def test_cpu_num(self): 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 pytest.fail( - f"expected: {now}, found: {create_time}, difference:" - f" {difference}" - ) - + # Fail if the difference with current time is > 2s. + assert abs(p.create_time() - now) < 2 # make sure returned value can be pretty printed with strftime time.strftime("%Y %m %d %H:%M:%S", time.localtime(p.create_time())) From df4139eac984850ffbb04f930d3de0c02a7ef627 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Apr 2025 21:02:31 +0200 Subject: [PATCH 1227/1714] make pytest happy --- psutil/tests/test_process_all.py | 2 +- psutil/tests/test_testutils.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index aba8686a22..1b66eccd93 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -54,7 +54,7 @@ def check_exception(exc, proc, name, ppid): if isinstance(exc, psutil.ZombieProcess): tcase.assert_proc_zombie(proc) if exc.ppid is not None: - tcase.assertGreaterEqual(exc.ppid, 0) + assert exc.ppid >= 0 assert exc.ppid == ppid elif isinstance(exc, psutil.NoSuchProcess): tcase.assert_proc_gone(proc) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index b1e0de4991..425d9bcb99 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -460,7 +460,7 @@ def test_raises(self): with fake_pytest.raises(ValueError, match="foo") as cm: raise ValueError("bar") except AssertionError as err: - assert str(err) == '"foo" does not match "bar"' + assert str(err) == '"foo" does not match "bar"' # noqa: PT017 else: raise pytest.fail("exception not raised") diff --git a/pyproject.toml b/pyproject.toml index ca64e8f02e..1cfaa17614 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ ignore = [ "PLW0603", # Using the global statement to update `lineno` is discouraged "PLW1514", # `open` in text mode without explicit `encoding` argument "PLW2901", # `for` loop variable `x` overwritten by assignment target - "PT", # flake8-pytest-style + "PT028", # pytest-parameter-with-default-argument "PTH", # flake8-use-pathlib "PYI", # flake8-pyi (python types stuff) "Q000", # Single quotes found but double quotes preferred From 6a6ad17ae153081569235032b9a5a391540c1d33 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Apr 2025 12:42:18 -0700 Subject: [PATCH 1228/1714] fix #2540 / macOS: boot_time() is off by 45 secs due to a C precision issue. Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 1 + psutil/arch/osx/sys.c | 2 +- psutil/tests/test_osx.py | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 766f38845d..49b80ac000 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,7 @@ XXXX-XX-XX parent PID, based on the incorrect assumption that a lower child PID indicated PID reuse. However, this assumption was flawed, as PIDs can restart from 0. The same problem also affected `Process.parent()`_. +- 2540_, [macOS]: `boot_time()`_ is off by 45 seconds (C precision issue). 7.0.0 ===== diff --git a/psutil/arch/osx/sys.c b/psutil/arch/osx/sys.c index 4fe6642597..32878319cc 100644 --- a/psutil/arch/osx/sys.c +++ b/psutil/arch/osx/sys.c @@ -26,7 +26,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { if (sysctl(request, 2, &result, &result_len, NULL, 0) == -1) return PyErr_SetFromErrno(PyExc_OSError); boot_time = result.tv_sec; - return Py_BuildValue("f", (float)boot_time); + return Py_BuildValue("d", (double)boot_time); } diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 4828d7c395..8351bb6d21 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -197,3 +197,11 @@ def test_sensors_battery(self): psutil_result = psutil.sensors_battery() assert psutil_result.power_plugged == power_plugged assert psutil_result.percent == int(percent) + + # --- others + + def test_boot_time(self): + out = sh('sysctl kern.boottime') + a = float(re.search(r"sec\s*=\s*(\d+)", out).groups(0)[0]) + b = psutil.boot_time() + assert a == b From 59d534cbf0c758193344ef757f299fe9f128df4a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 6 Apr 2025 22:10:04 +0200 Subject: [PATCH 1229/1714] move fake_pytest up in the module --- psutil/tests/__init__.py | 219 +++++++++++++++++++-------------------- pyproject.toml | 1 + 2 files changed, 105 insertions(+), 115 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index b42f390333..c61db0dcd4 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -271,6 +271,110 @@ def attempt(exe): _pids_started = set() +# =================================================================== +# --- fake pytest +# =================================================================== + + +class fake_pytest: + """A class that mimics some basic pytest APIs. This is meant for + when unit tests are run in production, where pytest may not be + installed. Still, the user can test psutil installation via: + + $ python3 -m psutil.tests + """ + + @staticmethod + def _warn_on_exit(): + def _warn_on_exit(): + warnings.warn( + "Fake pytest module was used. Test results may be inaccurate.", + UserWarning, + stacklevel=1, + ) + + atexit.register(_warn_on_exit) + + @staticmethod + def main(*args, **kw): # noqa: ARG004 + """Mimics pytest.main(). It has the same effect as running + `python3 -m unittest -v` from the project root directory. + """ + suite = unittest.TestLoader().discover(HERE) + unittest.TextTestRunner(verbosity=2).run(suite) + return suite + + @staticmethod + def raises(exc, match=None): + """Mimics `pytest.raises`.""" + + class ExceptionInfo: + _exc = None + + @property + def value(self): + return self._exc + + @contextlib.contextmanager + def context(exc, match=None): + einfo = ExceptionInfo() + try: + yield einfo + except exc as err: + if match and not re.search(match, str(err)): + msg = f'"{match}" does not match "{err}"' + raise AssertionError(msg) + einfo._exc = err + else: + raise AssertionError(f"{exc!r} not raised") + + return context(exc, match=match) + + @staticmethod + def warns(warning, match=None): + """Mimics `pytest.warns`.""" + if match: + return unittest.TestCase().assertWarnsRegex(warning, match) + return unittest.TestCase().assertWarns(warning) + + @staticmethod + def skip(reason=""): + """Mimics `unittest.SkipTest`.""" + raise unittest.SkipTest(reason) + + @staticmethod + def fail(reason=""): + """Mimics `pytest.fail`.""" + return unittest.TestCase().fail(reason) + + class mark: + + @staticmethod + def skipif(condition, reason=""): + """Mimics `@pytest.mark.skipif` decorator.""" + return unittest.skipIf(condition, reason) + + class xdist_group: + """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" + + def __init__(self, name=None): + pass + + def __call__(self, cls_or_meth): + return cls_or_meth + + +# to make pytest.fail() exception catchable +fake_pytest.fail.Exception = AssertionError + + +if pytest is None: + pytest = fake_pytest + # monkey patch future `import pytest` statements + sys.modules["pytest"] = fake_pytest + fake_pytest._warn_on_exit() + + # =================================================================== # --- threads # =================================================================== @@ -877,121 +981,6 @@ def get_testfn(suffix="", dir=None): # =================================================================== -def fake_xdist_group(*_args, **_kwargs): - """Mimics `@pytest.mark.xdist_group` decorator. No-op: it just - calls the test method or return the decorated class. - """ - - def wrapper(obj): - @functools.wraps(obj) - def inner(*args, **kwargs): - return obj(*args, **kwargs) - - return obj if isinstance(obj, type) else inner - - return wrapper - - -if not PYTEST_PARALLEL: - # Fake @pytest.mark.xdist_group decorator. This is here so that we - # can run pytest with PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 env var, and - # hence avoid loading pytest-xdist plugin (faster pytest startup). - # When we want to run parallel test we will explicitly enable - # pytest-xdist from the cmdline via `pytest -p xdist …`. - pytest.mark.xdist_group = fake_xdist_group - - -class fake_pytest: - """A class that mimics some basic pytest APIs. This is meant for - when unit tests are run in production, where pytest may not be - installed. Still, the user can test psutil installation via: - - $ python3 -m psutil.tests - """ - - @staticmethod - def _warn_on_exit(): - def _warn_on_exit(): - warnings.warn( - "Fake pytest module was used. Test results may be inaccurate.", - UserWarning, - stacklevel=1, - ) - - atexit.register(_warn_on_exit) - - @staticmethod - def main(*args, **kw): # noqa: ARG004 - """Mimics pytest.main(). It has the same effect as running - `python3 -m unittest -v` from the project root directory. - """ - suite = unittest.TestLoader().discover(HERE) - unittest.TextTestRunner(verbosity=2).run(suite) - return suite - - @staticmethod - def raises(exc, match=None): - """Mimics `pytest.raises`.""" - - class ExceptionInfo: - _exc = None - - @property - def value(self): - return self._exc - - @contextlib.contextmanager - def context(exc, match=None): - einfo = ExceptionInfo() - try: - yield einfo - except exc as err: - if match and not re.search(match, str(err)): - msg = f'"{match}" does not match "{err}"' - raise AssertionError(msg) - einfo._exc = err - else: - raise AssertionError(f"{exc!r} not raised") - - return context(exc, match=match) - - @staticmethod - def warns(warning, match=None): - """Mimics `pytest.warns`.""" - if match: - return unittest.TestCase().assertWarnsRegex(warning, match) - return unittest.TestCase().assertWarns(warning) - - @staticmethod - def skip(reason=""): - """Mimics `unittest.SkipTest`.""" - raise unittest.SkipTest(reason) - - @staticmethod - def fail(reason=""): - """Mimics `pytest.fail`.""" - return unittest.TestCase().fail(reason) - - class mark: - - @staticmethod - def skipif(condition, reason=""): - """Mimics `@pytest.mark.skipif` decorator.""" - return unittest.skipIf(condition, reason) - - xdist_group = fake_xdist_group - - -# to make pytest.fail() exception catchable -fake_pytest.fail.Exception = AssertionError - - -if pytest is None: - # pytest not installed - pytest = fake_pytest - fake_pytest._warn_on_exit() - - class PsutilTestCase(unittest.TestCase): """Test class providing auto-cleanup wrappers on top of process test utilities. All test classes should derive from this one, even diff --git a/pyproject.toml b/pyproject.toml index 1cfaa17614..31f4b2509d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,6 +245,7 @@ addopts = ''' -p no:pastebin -p instafail -p subtests + -p xdist ''' testpaths = ["psutil/tests/"] From d7b2f188618de95c4af30168437d5d2fd4e5fa83 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Apr 2025 12:14:19 +0200 Subject: [PATCH 1230/1714] refact test-ci target --- .github/workflows/bsd.yml | 6 +++--- .github/workflows/build.yml | 2 +- Makefile | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index c405e82283..01e30ab4fe 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -20,7 +20,7 @@ jobs: with: usesh: true run: | - PIP_BREAK_SYSTEM_PACKAGES=1 make install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks + make test-ci openbsd: # if: false @@ -32,7 +32,7 @@ jobs: with: usesh: true run: | - PIP_BREAK_SYSTEM_PACKAGES=1 make install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks + make test-ci netbsd: if: false # XXX: disabled @@ -44,4 +44,4 @@ jobs: with: usesh: true run: | - PIP_BREAK_SYSTEM_PACKAGES=1 make PYTHON=python3.11 install-sysdeps install-pydeps-test install print-sysinfo test test-memleaks + make test-ci diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe72d9a68c..f835ee404d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,7 +91,7 @@ jobs: python-version: 3.x - name: 'Run linters' run: | - python3 -m pip install ruff black==24.10.0 rstcheck toml-sort sphinx + python3 -m pip install -U black==24.10.0 ruff rstcheck toml-sort sphinx make lint-all # Produce wheels as artifacts. diff --git a/Makefile b/Makefile index 19c47fa678..ed195bda53 100644 --- a/Makefile +++ b/Makefile @@ -158,12 +158,13 @@ test-coverage: ## Run test coverage. $(PYTHON) -m coverage html $(PYTHON) -m webbrowser -t htmlcov/index.html -test-ci: +test-ci: ## Run tests on GitHub CI. ${MAKE} install-sysdeps - mkdir -p .tests - cd .tests/ && $(PYTHON) -c "from psutil.tests import print_sysinfo; print_sysinfo()" - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests + PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test + ${MAKE} print-sysinfo + $(PYTHON) -m pip list + ${MAKE} test + ${MAKE} test-memleaks # =================================================================== # Linters From 697513a334269f7912e86912d9e9dc9bad6c79d4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Apr 2025 16:29:27 +0200 Subject: [PATCH 1231/1714] Revert changes introduced in #2533, #2537, a513b7c. Not sure what I was thinking. The assumption reported in the ticket description is wrong. What we were doing previously was not testing the PID (higher or lower) but the creation time, which was correct. I realized this created a regression thanks to a failure on windows: __________________ TestProcessUtils.test_spawn_children_pair __________________ psutil\tests\test_testutils.py:237: in test_spawn_children_pair assert len(children) == 2 E AssertionError: assert 6 == 2 E + where 6 = len([psutil.Process(pid=704, name='python.exe', status='running', started='12:45:55'), psutil.Process(pid=720, name='csrss.exe', status='running', started='12:24:19'), psutil.Process(pid=784, name='winlogon.exe', status='running', started='12:24:19'), psutil.Process(pid=3404, name='python.exe', status='running', started='12:45:55'), psutil.Process(pid=588, name='fontdrvhost.exe', status='running', started='12:24:19'), psutil.Process(pid=1412, name='dwm.exe', status='running', started='12:24:19')]) Signed-off-by: Giampaolo Rodola --- HISTORY.rst | 4 ---- psutil/__init__.py | 24 ++++++++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 49b80ac000..aa0a7edfe3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,10 +16,6 @@ XXXX-XX-XX creation time is now used instead. (patch by Jonathan Kohler) - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will now raise `AccessDenied`_ instead. -- 2533_: `Process.children()`_ previously skipped all PIDs lower than the - parent PID, based on the incorrect assumption that a lower child PID - indicated PID reuse. However, this assumption was flawed, as PIDs can restart - from 0. The same problem also affected `Process.parent()`_. - 2540_, [macOS]: `boot_time()`_ is off by 45 seconds (C precision issue). 7.0.0 diff --git a/psutil/__init__.py b/psutil/__init__.py index 9cd85b7ae6..80c5c225a3 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -598,9 +598,13 @@ def parent(self): return None ppid = self.ppid() if ppid is not None: + ctime = self.create_time() try: - return Process(ppid) - except (NoSuchProcess, ZombieProcess): + parent = Process(ppid) + if parent.create_time() <= ctime: + return parent + # ...else ppid has been reused by another process + except NoSuchProcess: pass def parents(self): @@ -971,10 +975,12 @@ def children(self, recursive=False): if ppid == self.pid: try: child = Process(pid) + # if child happens to be older than its parent + # (self) it means child's PID has been reused + if self.create_time() <= child.create_time(): + ret.append(child) except (NoSuchProcess, ZombieProcess): pass - else: - ret.append(child) else: # Construct a {pid: [child pids]} dict reverse_ppid_map = collections.defaultdict(list) @@ -992,15 +998,17 @@ def children(self, recursive=False): # there's a cycle in the recorded process "tree". continue seen.add(pid) - for child_pid in reverse_ppid_map[pid]: try: child = Process(child_pid) + # if child happens to be older than its parent + # (self) it means child's PID has been reused + intime = self.create_time() <= child.create_time() + if intime: + ret.append(child) + stack.append(child_pid) except (NoSuchProcess, ZombieProcess): pass - else: - ret.append(child) - stack.append(child_pid) return ret def cpu_percent(self, interval=None): From d1e316bd81857ef0925e5c66197b0333917a63d2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Apr 2025 17:44:28 +0200 Subject: [PATCH 1232/1714] rename spawn_testproc -> spawn_subproc --- psutil/tests/__init__.py | 18 +++++++++++++----- psutil/tests/test_bsd.py | 6 +++--- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 2 +- psutil/tests/test_memleaks.py | 4 ++-- psutil/tests/test_misc.py | 2 +- psutil/tests/test_osx.py | 4 ++-- psutil/tests/test_posix.py | 4 ++-- psutil/tests/test_process.py | 14 ++------------ psutil/tests/test_system.py | 20 ++++++++++---------- psutil/tests/test_testutils.py | 8 ++++---- psutil/tests/test_unicode.py | 12 ++++++------ psutil/tests/test_windows.py | 14 +++++++------- 13 files changed, 54 insertions(+), 56 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index c61db0dcd4..472a8055f4 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -75,7 +75,7 @@ "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", "MACOS_12PLUS", "COVERAGE", 'AARCH64', "PYTEST_PARALLEL", # subprocesses - 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', + 'pyrun', 'terminate', 'reap_children', 'spawn_subproc', 'spawn_zombie', 'spawn_children_pair', # threads 'ThreadTask', @@ -441,7 +441,7 @@ def wrapper(*args, **kwargs): @_reap_children_on_err -def spawn_testproc(cmd=None, **kwds): +def spawn_subproc(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. @@ -574,7 +574,7 @@ def pyrun(src, **kwds): try: with open(srcfile, "w") as f: f.write(src) - subp = spawn_testproc([PYTHON_EXE, f.name], **kwds) + subp = spawn_subproc([PYTHON_EXE, f.name], **kwds) wait_for_pid(subp.pid) return (subp, srcfile) except Exception: @@ -992,11 +992,19 @@ def get_testfn(self, suffix="", dir=None): self.addCleanup(safe_rmpath, fname) return fname - def spawn_testproc(self, *args, **kwds): - sproc = spawn_testproc(*args, **kwds) + def spawn_subproc(self, *args, **kwds): + sproc = spawn_subproc(*args, **kwds) self.addCleanup(terminate, sproc) return sproc + def spawn_psproc(self, *args, **kwargs): + sproc = self.spawn_subproc(*args, **kwargs) + try: + return psutil.Process(sproc.pid) + except psutil.NoSuchProcess: + self.assert_pid_gone(sproc.pid) + raise + def spawn_children_pair(self): child1, child2 = spawn_children_pair() self.addCleanup(terminate, child2) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index e2a6b0b696..e4ec5874cd 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -26,7 +26,7 @@ from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh -from psutil.tests import spawn_testproc +from psutil.tests import spawn_subproc from psutil.tests import terminate @@ -78,7 +78,7 @@ class BSDTestCase(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc().pid + cls.pid = spawn_subproc().pid @classmethod def tearDownClass(cls): @@ -162,7 +162,7 @@ def test_net_if_stats(self): class FreeBSDPsutilTestCase(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc().pid + cls.pid = spawn_subproc().pid @classmethod def tearDownClass(cls): diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 3606da27be..adb8019cdb 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -312,7 +312,7 @@ def test_users(self): class TestProcessWaitType(PsutilTestCase): @pytest.mark.skipif(not POSIX, reason="not POSIX") def test_negative_signal(self): - p = psutil.Process(self.spawn_testproc().pid) + p = psutil.Process(self.spawn_subproc().pid) p.terminate() code = p.wait() assert code == -signal.SIGTERM diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d82945c954..0d9caf2629 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1821,7 +1821,7 @@ def open_mock(name, *args, **kwargs): class TestProcess(PsutilTestCase): @retry_on_failure() def test_parse_smaps_vs_memory_maps(self): - sproc = self.spawn_testproc() + sproc = self.spawn_subproc() uss, pss, swap = psutil._pslinux.Process(sproc.pid)._parse_smaps() maps = psutil.Process(sproc.pid).memory_maps(grouped=False) assert ( diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index e05f7324db..8d91378be7 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -44,7 +44,7 @@ from psutil.tests import process_namespace from psutil.tests import pytest from psutil.tests import skip_on_access_denied -from psutil.tests import spawn_testproc +from psutil.tests import spawn_subproc from psutil.tests import system_namespace from psutil.tests import terminate @@ -273,7 +273,7 @@ class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @classmethod def setUpClass(cls): super().setUpClass() - cls.subp = spawn_testproc() + cls.subp = spawn_subproc() cls.proc = psutil.Process(cls.subp.pid) cls.proc.kill() cls.proc.wait() diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 4b330036c7..bc767d62a6 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -49,7 +49,7 @@ def test_check_pid_range(self): psutil.Process(2**128) def test_process__repr__(self, func=repr): - p = psutil.Process(self.spawn_testproc().pid) + p = psutil.Process(self.spawn_subproc().pid) r = func(p) assert "psutil.Process" in r assert f"pid={p.pid}" in r diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 8351bb6d21..0c20986087 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -21,7 +21,7 @@ from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh -from psutil.tests import spawn_testproc +from psutil.tests import spawn_subproc from psutil.tests import terminate @@ -56,7 +56,7 @@ def vm_stat(field): class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc().pid + cls.pid = spawn_subproc().pid @classmethod def tearDownClass(cls): diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index e7b2bba2f3..7c5db8c3d0 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -31,7 +31,7 @@ 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 spawn_subproc from psutil.tests import terminate @@ -151,7 +151,7 @@ class TestProcess(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc( + cls.pid = spawn_subproc( [PYTHON_EXE, "-E", "-O"], stdin=subprocess.PIPE ).pid diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 62956a3419..eb104acef2 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -73,16 +73,6 @@ 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.assert_pid_gone(sproc.pid) - raise - - # --- - def test_pid(self): p = psutil.Process() assert p.pid == os.getpid() @@ -1393,7 +1383,7 @@ def test_zombie_process_status_w_exc(self): def test_reused_pid(self): # Emulate a case where PID has been reused by another process. - subp = self.spawn_testproc() + subp = self.spawn_subproc() p = psutil.Process(subp.pid) p._ident = (p.pid, p.create_time() + 100) @@ -1532,7 +1522,7 @@ def test_weird_environ(self): } """) cexe = create_c_exe(self.get_testfn(), c_code=code) - sproc = self.spawn_testproc( + sproc = self.spawn_subproc( [cexe], stdin=subprocess.PIPE, stderr=subprocess.PIPE ) p = psutil.Process(sproc.pid) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 85219cf609..5239d4c40b 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -60,7 +60,7 @@ class TestProcessIter(PsutilTestCase): def test_pid_presence(self): assert os.getpid() in [x.pid for x in psutil.process_iter()] - sproc = self.spawn_testproc() + sproc = self.spawn_subproc() assert sproc.pid in [x.pid for x in psutil.process_iter()] p = psutil.Process(sproc.pid) p.kill() @@ -132,16 +132,16 @@ def test_cache_clear(self): class TestProcessAPIs(PsutilTestCase): @pytest.mark.skipif( PYPY and WINDOWS, - reason="spawn_testproc() unreliable on PYPY + WINDOWS", + reason="spawn_subproc() 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() + sproc1 = self.spawn_subproc() + sproc2 = self.spawn_subproc() + sproc3 = self.spawn_subproc() procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] with pytest.raises(ValueError): psutil.wait_procs(procs, timeout=-1) @@ -195,19 +195,19 @@ def test_2(procs, callback): @pytest.mark.skipif( PYPY and WINDOWS, - reason="spawn_testproc() unreliable on PYPY + WINDOWS", + reason="spawn_subproc() unreliable on PYPY + WINDOWS", ) def test_wait_procs_no_timeout(self): - sproc1 = self.spawn_testproc() - sproc2 = self.spawn_testproc() - sproc3 = self.spawn_testproc() + sproc1 = self.spawn_subproc() + sproc2 = self.spawn_subproc() + sproc3 = self.spawn_subproc() 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() + sproc = self.spawn_subproc() assert psutil.pid_exists(sproc.pid) p = psutil.Process(sproc.pid) p.kill() diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 425d9bcb99..9a7faa6d95 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -218,7 +218,7 @@ def test_chdir(self): class TestProcessUtils(PsutilTestCase): def test_reap_children(self): - subp = self.spawn_testproc() + subp = self.spawn_subproc() p = psutil.Process(subp.pid) assert p.is_running() reap_children() @@ -254,12 +254,12 @@ def test_spawn_zombie(self): def test_terminate(self): # by subprocess.Popen - p = self.spawn_testproc() + p = self.spawn_subproc() terminate(p) self.assert_pid_gone(p.pid) terminate(p) # by psutil.Process - p = psutil.Process(self.spawn_testproc().pid) + p = psutil.Process(self.spawn_subproc().pid) terminate(p) self.assert_pid_gone(p.pid) terminate(p) @@ -279,7 +279,7 @@ def test_terminate(self): self.assert_pid_gone(p.pid) terminate(p) # by PID - pid = self.spawn_testproc().pid + pid = self.spawn_subproc().pid terminate(pid) self.assert_pid_gone(p.pid) terminate(pid) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index d8a8c4bfc5..ee9282677c 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -94,7 +94,7 @@ from psutil.tests import safe_mkdir from psutil.tests import safe_rmpath from psutil.tests import skip_on_access_denied -from psutil.tests import spawn_testproc +from psutil.tests import spawn_subproc from psutil.tests import terminate @@ -107,7 +107,7 @@ def try_unicode(suffix): try: safe_rmpath(testfn) create_py_exe(testfn) - sproc = spawn_testproc(cmd=[testfn]) + sproc = spawn_subproc(cmd=[testfn]) shutil.copyfile(testfn, testfn + '-2') safe_rmpath(testfn + '-2') except (UnicodeEncodeError, OSError): @@ -166,7 +166,7 @@ def test_proc_exe(self): "-c", "import time; [time.sleep(0.1) for x in range(100)]", ] - subp = self.spawn_testproc(cmd) + subp = self.spawn_subproc(cmd) p = psutil.Process(subp.pid) exe = p.exe() assert isinstance(exe, str) @@ -179,7 +179,7 @@ def test_proc_name(self): "-c", "import time; [time.sleep(0.1) for x in range(100)]", ] - subp = self.spawn_testproc(cmd) + subp = self.spawn_subproc(cmd) name = psutil.Process(subp.pid).name() assert isinstance(name, str) if self.expect_exact_path_match(): @@ -191,7 +191,7 @@ def test_proc_cmdline(self): "-c", "import time; [time.sleep(0.1) for x in range(100)]", ] - subp = self.spawn_testproc(cmd) + subp = self.spawn_subproc(cmd) p = psutil.Process(subp.pid) cmdline = p.cmdline() for part in cmdline: @@ -304,7 +304,7 @@ def test_proc_environ(self): # with fs paths. env = os.environ.copy() env['FUNNY_ARG'] = self.funky_suffix - sproc = self.spawn_testproc(env=env) + sproc = self.spawn_subproc(env=env) p = psutil.Process(sproc.pid) env = p.environ() for k, v in env.items(): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 079bd6837e..8e4f400d63 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -31,7 +31,7 @@ from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh -from psutil.tests import spawn_testproc +from psutil.tests import spawn_subproc from psutil.tests import terminate @@ -366,7 +366,7 @@ def test_emulate_secs_left_unknown(self): class TestProcess(WindowsTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc().pid + cls.pid = spawn_subproc().pid @classmethod def tearDownClass(cls): @@ -411,7 +411,7 @@ def test_num_handles_increment(self): assert p.num_handles() == before def test_ctrl_signals(self): - p = psutil.Process(self.spawn_testproc().pid) + p = psutil.Process(self.spawn_subproc().pid) p.send_signal(signal.CTRL_C_EVENT) p.send_signal(signal.CTRL_BREAK_EVENT) p.kill() @@ -571,7 +571,7 @@ class TestProcessWMI(WindowsTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc().pid + cls.pid = spawn_subproc().pid @classmethod def tearDownClass(cls): @@ -651,7 +651,7 @@ class TestDualProcessImplementation(PsutilTestCase): @classmethod def setUpClass(cls): - cls.pid = spawn_testproc().pid + cls.pid = spawn_subproc().pid @classmethod def tearDownClass(cls): @@ -771,10 +771,10 @@ def setUp(self): env = os.environ.copy() env["THINK_OF_A_NUMBER"] = str(os.getpid()) - self.proc32 = self.spawn_testproc( + self.proc32 = self.spawn_subproc( [self.python32] + self.test_args, env=env, stdin=subprocess.PIPE ) - self.proc64 = self.spawn_testproc( + self.proc64 = self.spawn_subproc( [self.python64] + self.test_args, env=env, stdin=subprocess.PIPE ) From fa71912e600e9b439274b392741ac797085363f9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Apr 2025 20:05:01 +0200 Subject: [PATCH 1233/1714] Make `children()` and `parent()` aware of system clock updates (#2543) - fixes #2542 --- HISTORY.rst | 2 ++ psutil/__init__.py | 15 +++++++++++---- psutil/tests/test_process.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index aa0a7edfe3..cece7af690 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will now raise `AccessDenied`_ instead. - 2540_, [macOS]: `boot_time()`_ is off by 45 seconds (C precision issue). +- 2542_: if system clock is updated `Process.children()`_ and + `Process.parent()`_ may not be able to return the right information. 7.0.0 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index 80c5c225a3..06cbac0adf 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -598,10 +598,13 @@ def parent(self): return None ppid = self.ppid() if ppid is not None: - ctime = self.create_time() + # Get a fresh (non-cached) ctime in case the system clock + # was updated. TODO: use a monotonic ctime on platforms + # where it's supported. + proc_ctime = Process(self.pid).create_time() try: parent = Process(ppid) - if parent.create_time() <= ctime: + if parent.create_time() <= proc_ctime: return parent # ...else ppid has been reused by another process except NoSuchProcess: @@ -969,6 +972,10 @@ def children(self, recursive=False): """ self._raise_if_pid_reused() ppid_map = _ppid_map() + # Get a fresh (non-cached) ctime in case the system clock was + # updated. TODO: use a monotonic ctime on platforms where it's + # supported. + proc_ctime = Process(self.pid).create_time() ret = [] if not recursive: for pid, ppid in ppid_map.items(): @@ -977,7 +984,7 @@ def children(self, recursive=False): child = Process(pid) # if child happens to be older than its parent # (self) it means child's PID has been reused - if self.create_time() <= child.create_time(): + if proc_ctime <= child.create_time(): ret.append(child) except (NoSuchProcess, ZombieProcess): pass @@ -1003,7 +1010,7 @@ def children(self, recursive=False): child = Process(child_pid) # if child happens to be older than its parent # (self) it means child's PID has been reused - intime = self.create_time() <= child.create_time() + intime = proc_ctime <= child.create_time() if intime: ret.append(child) stack.append(child_pid) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index eb104acef2..7df07e3df9 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1123,6 +1123,18 @@ def test_parent(self): lowest_pid = psutil.pids()[0] assert psutil.Process(lowest_pid).parent() is None + def test_parent_mocked_ctime(self): + # Make sure we get a fresh copy of the ctime before processing + # parent().We make the assumption that the parent pid MUST have + # a creation time < than the child. If system clock is updated + # this assumption was broken. + # https://github.com/giampaolo/psutil/issues/2542 + p = self.spawn_psproc() + p.create_time() # trigger cache + assert p._create_time + p._create_time = 1 + assert p.parent().pid == os.getpid() + def test_parent_multi(self): parent = psutil.Process() child, grandchild = self.spawn_children_pair() @@ -1153,6 +1165,30 @@ def test_children(self): assert children[0].pid == child.pid assert children[0].ppid() == parent.pid + def test_children_mocked_ctime(self): + # Make sure we get a fresh copy of the ctime before processing + # children(). We make the assumption that process children MUST + # have a creation time > than the parent. If system clock is + # updated this assumption was broken. + # https://github.com/giampaolo/psutil/issues/2542 + parent = psutil.Process() + parent.create_time() # trigger cache + assert parent._create_time + parent._create_time += 100000 + + assert not parent.children() + assert not 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): + assert len(children) == 1 + assert children[0].pid == child.pid + assert 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). From 963b2a0fabfc6cae9a9f5122abe6fe19dd92701a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 7 Apr 2025 21:12:27 +0200 Subject: [PATCH 1234/1714] [Linux] make `Process.create_time()` aware of system clock updates (#2544, #2541) Don't cache `boot_time()` result when serving `Process.create_time()`. It doesn't work well with system clock updates. Instead just add a fresh `boot_time()` to the `create_time()`. This introduces a slowdown. Before: ``` $ python3 -m timeit -s "import psutil; psutil.Process().create_time()" "psutil.Process().create_time()" 50000 loops, best of 5: 9.03 usec per loop ``` Now: ``` $ python3 -m timeit -s "import psutil; psutil.Process().create_time()" "psutil.Process().create_time()" 10000 loops, best of 5: 31.2 usec per loop ``` On the bright side of things: on `Process()` instantiation we now use a monotonic `create_time()` (e40ec579), which is not subject to this slowdown. Also introduce a pretty ambitious test file which uses sudo to update system time. --- HISTORY.rst | 2 + MANIFEST.in | 1 + Makefile | 7 +- docs/index.rst | 11 ++- psutil/__init__.py | 16 ++-- psutil/_pslinux.py | 21 +++--- psutil/tests/__init__.py | 13 ++++ psutil/tests/test_sudo.py | 109 ++++++++++++++++++++++++++++ psutil/tests/test_unicode.py | 3 + pyproject.toml | 6 +- scripts/internal/install-sysdeps.sh | 2 + scripts/internal/winmake.py | 7 ++ 12 files changed, 175 insertions(+), 23 deletions(-) create mode 100755 psutil/tests/test_sudo.py diff --git a/HISTORY.rst b/HISTORY.rst index cece7af690..9791cb8d58 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will now raise `AccessDenied`_ instead. - 2540_, [macOS]: `boot_time()`_ is off by 45 seconds (C precision issue). +- 2541_, [Linux]: `Process.create_time()`_ does not reflect system clock + updates because it uses a cached version of `boot_time()`_. - 2542_: if system clock is updated `Process.children()`_ and `Process.parent()`_ may not be able to return the right information. diff --git a/MANIFEST.in b/MANIFEST.in index b60794b913..2a99ac1ea2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -164,6 +164,7 @@ include psutil/tests/test_posix.py include psutil/tests/test_process.py include psutil/tests/test_process_all.py include psutil/tests/test_scripts.py +include psutil/tests/test_sudo.py include psutil/tests/test_sunos.py include psutil/tests/test_system.py include psutil/tests/test_testutils.py diff --git a/Makefile b/Makefile index ed195bda53..dc6c028fac 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,9 @@ ARGS = # In not in a virtualenv, add --user options for install commands. SETUP_INSTALL_ARGS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` - PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 +SUDO = $(if $(filter $(OS),Windows_NT),,sudo) # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help @@ -158,6 +158,10 @@ test-coverage: ## Run test coverage. $(PYTHON) -m coverage html $(PYTHON) -m webbrowser -t htmlcov/index.html +test-sudo: ## Run tests requiring root privileges. + # Use unittest runner because pytest may not be installed as root. + $(SUDO) $(PYTHON_ENV_VARS) $(PYTHON) -m unittest -v psutil.tests.test_sudo + test-ci: ## Run tests on GitHub CI. ${MAKE} install-sysdeps PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test @@ -165,6 +169,7 @@ test-ci: ## Run tests on GitHub CI. $(PYTHON) -m pip list ${MAKE} test ${MAKE} test-memleaks + ${MAKE} test-sudo # =================================================================== # Linters diff --git a/docs/index.rst b/docs/index.rst index a32e29a462..143a461cc6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -868,8 +868,10 @@ Other system info .. function:: boot_time() - Return the system boot time expressed in seconds since the epoch. - Example: + Return the system boot time expressed in seconds since the epoch (seconds + since January 1, 1970, at midnight UTC). The returned value is based on the + system clock, which means it may be affected by changes such as manual + adjustments or time synchronization (e.g. NTP). .. code-block:: python @@ -1239,7 +1241,10 @@ Process class .. method:: create_time() The process creation time as a floating point number expressed in seconds - since the epoch. The return value is cached after first call. + since the epoch (seconds since January 1, 1970, at midnight UTC). The + return value, which is cached after first call, is based on the system + clock, which means it may be affected by changes such as manual adjustments + or time synchronization (e.g. NTP). >>> import psutil, datetime >>> p = psutil.Process() diff --git a/psutil/__init__.py b/psutil/__init__.py index 06cbac0adf..f0e69610f4 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -773,8 +773,11 @@ def username(self): def create_time(self): """The process creation time as a floating point number - expressed in seconds since the epoch. - The return value is cached after first call. + expressed in seconds since the epoch (seconds since January 1, + 1970, at midnight UTC). The return value, which is cached after + first call, is based on the system clock, which means it may be + affected by changes such as manual adjustments or time + synchronization (e.g. NTP). """ if self._create_time is None: self._create_time = self._proc.create_time() @@ -2364,9 +2367,12 @@ def sensors_battery(): def boot_time(): - """Return the system boot time expressed in seconds since the epoch.""" - # Note: we are not caching this because it is subject to - # system clock updates. + """Return the system boot time expressed in seconds since the epoch + (seconds since January 1, 1970, at midnight UTC). The returned + value is based on the system clock, which means it may be affected + by changes such as manual adjustments or time synchronization (e.g. + NTP). + """ return _psplatform.boot_time() diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index f922ecc6f3..4d34487dec 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -81,7 +81,6 @@ # Number of clock ticks per second CLOCK_TICKS = os.sysconf("SC_CLK_TCK") PAGESIZE = cext_posix.getpagesize() -BOOT_TIME = None # set later LITTLE_ENDIAN = sys.byteorder == 'little' UNSET = object() @@ -1557,14 +1556,11 @@ def users(): def boot_time(): """Return the system boot time expressed in seconds since the epoch.""" - global BOOT_TIME path = f"{get_procfs_path()}/stat" with open_binary(path) as f: for line in f: if line.startswith(b'btime'): - ret = float(line.strip().split()[1]) - BOOT_TIME = ret - return ret + return float(line.strip().split()[1]) msg = f"line 'btime' not found in {path}" raise RuntimeError(msg) @@ -1899,19 +1895,20 @@ def wait(self, timeout=None): @wrap_exceptions def create_time(self, monotonic=False): - # Start time unit is expressed in jiffies (clock ticks per - # second). It never changes and is not affected by system clock - # updates. + # The 'starttime' field in /proc/[pid]/stat is expressed in + # jiffies (clock ticks per second), a relative value which + # represents the number of clock ticks that have passed since + # the system booted until the process was created. It never + # changes and is unaffected by system clock updates. if self._ctime is None: self._ctime = ( float(self._parse_stat_file()['create_time']) / CLOCK_TICKS ) if monotonic: return self._ctime - # Add the uptime, returning seconds since the epoch (this is - # subject to system clock updates). - bt = BOOT_TIME or boot_time() - return self._ctime + bt + # Add the boot time, returning time expressed in seconds since + # the epoch. This is subject to system clock updates. + return self._ctime + boot_time() @wrap_exceptions def memory_info(self): diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 472a8055f4..3f791bcd78 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -987,6 +987,19 @@ class PsutilTestCase(unittest.TestCase): if we use pytest. """ + # Print a full path representation of the single unit test being + # run, similar to pytest output. Used only when running tests with + # the unittest runner. + def __str__(self): + fqmod = self.__class__.__module__ + if not fqmod.startswith('psutil.'): + fqmod = 'psutil.tests.' + fqmod + return "{}.{}.{}".format( + fqmod, + self.__class__.__name__, + self._testMethodName, + ) + def get_testfn(self, suffix="", dir=None): fname = get_testfn(suffix=suffix, dir=dir) self.addCleanup(safe_rmpath, fname) diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py new file mode 100755 index 0000000000..beae228f62 --- /dev/null +++ b/psutil/tests/test_sudo.py @@ -0,0 +1,109 @@ +#!/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 which are meant to be run as root. + +NOTE: keep this module compatible with unittest: we want to run this +file with the unittest runner, since pytest may not be installed for +the root user. +""" + +import datetime +import time +import unittest + +import psutil +from psutil import LINUX +from psutil import MACOS +from psutil import WINDOWS +from psutil.tests import PsutilTestCase + + +def get_systime(): + if hasattr(time, "clock_gettime") and hasattr(time, "CLOCK_REALTIME"): + return time.clock_gettime(time.CLOCK_REALTIME) + return time.time() + + +def set_systime(secs): # secs since the epoch + if hasattr(time, "clock_settime") and hasattr(time, "CLOCK_REALTIME"): + try: + time.clock_settime(time.CLOCK_REALTIME, secs) + except PermissionError: + raise unittest.SkipTest("needs root") + elif WINDOWS: + import pywintypes + import win32api + + dt = datetime.datetime.utcfromtimestamp(secs) + try: + win32api.SetSystemTime( + dt.year, + dt.month, + dt.isoweekday() % 7, + dt.day, + dt.hour, + dt.minute, + dt.second, + int(dt.microsecond / 1000), + ) + except pywintypes.error as err: + if err.winerror == 1314: + raise unittest.SkipTest("needs Administrator user") + raise + else: + raise unittest.SkipTest("setting systime not supported") + + +class TestUpdatedSystemTime(PsutilTestCase): + """Tests which update the system clock.""" + + def setUp(self): + self.time_updated = False + self.orig_time = get_systime() + + def tearDown(self): + if self.time_updated: + set_systime(self.orig_time) + if WINDOWS: + self.assertAlmostEqual(get_systime(), self.orig_time, delta=1) + else: + self.assertEqual(get_systime(), self.orig_time) + + def update_systime(self): + # set system time 1 hour later + set_systime(self.orig_time + 3600) + + @unittest.skipIf(WINDOWS, "broken on WINDOWS") # TODO: fix it + def test_boot_time(self): + # Test that boot_time() reflects system clock updates. + t1 = psutil.boot_time() + self.update_systime() + t2 = psutil.boot_time() + self.assertGreater(t2, t1) + diff = int(t2 - t1) + self.assertAlmostEqual(diff, 3600, delta=1) + + @unittest.skipIf(WINDOWS, "broken on WINDOWS") # TODO: fix it + @unittest.skipIf(MACOS, "broken on MACOS") # TODO: fix it + def test_proc_create_time(self): + # Test that Process.create_time() reflects system clock + # updates. On systems such as Linux this is added on top of the + # process monotonic time returned by the kernel. + t1 = psutil.Process().create_time() + self.update_systime() + t2 = psutil.Process().create_time() + self.assertGreater(t2, t1) + diff = int(t2 - t1) + self.assertAlmostEqual(diff, 3600, delta=1) + + @unittest.skipIf(not LINUX, "LINUX only") + def test_linux_monotonic_proc_time(self): + t1 = psutil.Process()._proc.create_time(monotonic=True) + self.update_systime() + time.sleep(0.05) + t2 = psutil.Process()._proc.create_time(monotonic=True) + self.assertEqual(t1, t2) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index ee9282677c..e84ed89a3a 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -73,6 +73,7 @@ import psutil from psutil import BSD +from psutil import MACOS from psutil import POSIX from psutil import WINDOWS from psutil.tests import ASCII_FS @@ -231,6 +232,8 @@ def test_proc_net_connections(self): with closing(sock): conn = psutil.Process().net_connections('unix')[0] assert isinstance(conn.laddr, str) + if not conn.laddr and MACOS and CI_TESTING: + raise pytest.skip("unreliable on OSX") assert conn.laddr == name @pytest.mark.skipif(not POSIX, reason="POSIX only") diff --git a/pyproject.toml b/pyproject.toml index 31f4b2509d..965649d0bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,16 +94,18 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] +# B904 == Use `raise from` to specify exception cause # EM101 == raw-string-in-exception # EM102 == f-string-in-exception # EM103 == dot-format-in-exception +# PLC1901 == `x == ""` can be simplified to `not x` as an empty string is falsey +# PT009 == Use a regular `assert` instead of unittest-style `self.assert*` # T201 == print() # T203 == pprint() # TRY003 == raise-vanilla-args -# B904 == Use `raise from` to specify exception cause -# PLC1901 == `x == ""` can be simplified to `not x` as an empty string is falsey ".github/workflows/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] "psutil/tests/*" = ["B904", "EM101", "EM102", "EM103", "PLC1901", "TRY003"] +"psutil/tests/test_sudo.py" = ["PT009"] "scripts/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] "scripts/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "setup.py" = [ diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 003a7d0817..77554e0441 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -41,9 +41,11 @@ main() { if [ $HAS_APT ]; then $SUDO apt-get install -y python3-dev gcc $SUDO apt-get install -y net-tools coreutils util-linux # for tests + $SUDO apt-get install -y sudo # for test-sudo elif [ $HAS_YUM ]; then $SUDO yum install -y python3-devel gcc $SUDO yum install -y net-tools coreutils util-linux # for tests + $SUDO yum install -y sudo # for test-sudo elif [ $HAS_APK ]; then $SUDO apk add python3-dev gcc musl-dev linux-headers coreutils procps elif [ $FREEBSD ]; then diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 5c5d02e735..d05dda0320 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -401,6 +401,12 @@ def test_testutils(): sh([PYTHON, "psutil\\tests\\test_testutils.py"]) +def test_sudo(): + """Run sudo utilities tests.""" + build() + sh([PYTHON, "-m", "unittest", "-v", "psutil\\tests\\test_sudo.py"]) + + def test_last_failed(): """Re-run tests which failed on last run.""" build() @@ -521,6 +527,7 @@ def parse_args(): sp.add_parser('test-process', help="run process tests") sp.add_parser('test-process-all', help="run process all tests") sp.add_parser('test-system', help="run system tests") + sp.add_parser('test-sudo', help="run sudo tests") sp.add_parser('test-unicode', help="run unicode tests") sp.add_parser('test-testutils', help="run test utils tests") sp.add_parser('uninstall', help="uninstall psutil") From 5e3fac1f74c0c6fb630f4d4a586790cd825fc8ad Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Tue, 8 Apr 2025 09:30:54 +0200 Subject: [PATCH 1235/1714] Fix MIB2_UDP_ENTRY handling for illumos (#2546) --- CREDITS | 4 ++++ HISTORY.rst | 2 ++ psutil/_psutil_sunos.c | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 25f2408e35..61b37498bf 100644 --- a/CREDITS +++ b/CREDITS @@ -852,3 +852,7 @@ I: 2473 N: Jonathan Kohler W: https://github.com/kohlerjl I: 2526, 2527 + +N: Marcel Telka +W: https://github.com/mtelka +I: 2545 diff --git a/HISTORY.rst b/HISTORY.rst index 9791cb8d58..f82c070fcd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,8 @@ XXXX-XX-XX updates because it uses a cached version of `boot_time()`_. - 2542_: if system clock is updated `Process.children()`_ and `Process.parent()`_ may not be able to return the right information. +- 2545_: [illumos]: Fix handling of MIB2_UDP_ENTRY in + `psutil_net_connections()`_. 7.0.0 ===== diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 37a421307e..6c33de8886 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -1326,7 +1326,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { } #endif // UDPv4 - else if (mibhdr.level == MIB2_UDP || mibhdr.level == MIB2_UDP_ENTRY) { + else if (mibhdr.level == MIB2_UDP && mibhdr.name == MIB2_UDP_ENTRY) { num_ent = mibhdr.len / sizeof(mib2_udpEntry_t); assert(num_ent * sizeof(mib2_udpEntry_t) == mibhdr.len); for (i = 0; i < num_ent; i++) { From 11fd4026d55b978b95ac6d15398b85c6c0c52bec Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 8 Apr 2025 10:13:03 +0200 Subject: [PATCH 1236/1714] fix #2548: remove unwanted text in PYPI description --- HISTORY.rst | 3 +-- setup.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f82c070fcd..261dd9bfad 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,8 +21,7 @@ XXXX-XX-XX updates because it uses a cached version of `boot_time()`_. - 2542_: if system clock is updated `Process.children()`_ and `Process.parent()`_ may not be able to return the right information. -- 2545_: [illumos]: Fix handling of MIB2_UDP_ENTRY in - `psutil_net_connections()`_. +- 2545_: [illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. 7.0.0 ===== diff --git a/setup.py b/setup.py index 2a45eee5b9..9758cddbf9 100755 --- a/setup.py +++ b/setup.py @@ -504,7 +504,7 @@ def main(): kwargs = dict( name='psutil', version=VERSION, - description=__doc__.replace('\n', ' ').strip() if __doc__ else '', + description="Cross-platform lib for process and system monitoring.", long_description=get_long_description(), long_description_content_type='text/x-rst', # fmt: off From ac059a4e69630cfafc4b730bb3bf1ab2d3892e06 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 9 Apr 2025 01:58:56 +0200 Subject: [PATCH 1237/1714] add adjust_for_clock_changes() fun --- psutil/_psosx.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 620497b30a..e253072f1c 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -18,6 +18,7 @@ from ._common import ZombieProcess from ._common import conn_tmap from ._common import conn_to_ntuple +from ._common import debug from ._common import isfile_strict from ._common import memoize_when_activated from ._common import parse_environ_block @@ -335,6 +336,44 @@ def is_zombie(pid): return False +try: + initial_boot_time = boot_time() +except Exception as err: # noqa: BLE001 + # Don't want to crash at import time. + debug(f"ignoring exception on import: {err!r}") + initial_boot_time = None + + +def adjust_for_clock_changes(monotonic_time): + """Adjust the given monotonic time for any system clock changes. + + If the system clock has been updated since the initial boot, this + function will adjust the provided time to ensure it reflects the + correct wall-clock time. + + Args: + monotonic_time (float): The time in seconds since the system boot. + + Returns: + float: The adjusted time accounting for any system clock changes. + """ + if initial_boot_time is None: + return monotonic_time + + current_boot_time = boot_time() + + # If boot time has not changed, return the input time unchanged. + if current_boot_time == initial_boot_time: + return monotonic_time + + # The system clock was updated, adjust the time accordingly. + time_diff = abs(initial_boot_time - current_boot_time) + if current_boot_time > initial_boot_time: + return monotonic_time + time_diff + else: + return monotonic_time - time_diff + + def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. @@ -471,7 +510,8 @@ def cpu_times(self): @wrap_exceptions def create_time(self): - return self._get_kinfo_proc()[kinfo_proc_map['ctime']] + monotonic_ctime = self._get_kinfo_proc()[kinfo_proc_map['ctime']] + return adjust_for_clock_changes(monotonic_ctime) @wrap_exceptions def num_ctx_switches(self): From 8d8961f033c3198a6d893753a9c9ebe60e3f0798 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 9 Apr 2025 09:57:41 +0200 Subject: [PATCH 1238/1714] better detect lack of cpu_freq() test support (re. #2550) --- psutil/tests/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 3f791bcd78..cb81c114a0 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -30,6 +30,7 @@ import textwrap import threading import time +import traceback import unittest import warnings from socket import AF_INET @@ -191,7 +192,6 @@ def macos_version(): # --- support HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") -HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") HAS_ENVIRON = hasattr(psutil.Process, "environ") HAS_GETLOADAVG = hasattr(psutil, "getloadavg") HAS_IONICE = hasattr(psutil.Process, "ionice") @@ -202,15 +202,24 @@ def macos_version(): 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 +try: + HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) +except Exception: # noqa: BLE001 + traceback.print_exc() + HAS_BATTERY = False +try: + HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") and bool(psutil.cpu_freq()) +except Exception: # noqa: BLE001 + # e.g. LINUX + (AARCH64 or RISCV64) + traceback.print_exc() + HAS_CPU_FREQ = False + + # --- misc From dd66d06dd57a1d3fe9dada4b601c1946772432f6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 9 Apr 2025 10:50:15 +0200 Subject: [PATCH 1239/1714] in case of test failure at import time, report the traceback atexit --- psutil/tests/__init__.py | 5 ++--- psutil/tests/test_process.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index cb81c114a0..afe45c65ef 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -210,13 +210,12 @@ def macos_version(): try: HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) except Exception: # noqa: BLE001 - traceback.print_exc() + atexit.register(functools.partial(print, traceback.format_exc())) HAS_BATTERY = False try: HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") and bool(psutil.cpu_freq()) except Exception: # noqa: BLE001 - # e.g. LINUX + (AARCH64 or RISCV64) - traceback.print_exc() + atexit.register(functools.partial(print, traceback.format_exc())) HAS_CPU_FREQ = False diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 7df07e3df9..53878bbffe 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -769,6 +769,7 @@ def test_name(self): assert pyexe.startswith(name), (pyexe, name) @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") + @retry_on_failure() def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) cmdline = [ From 8d472770818a7aab88cfac2926f5fac455d9e215 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 9 Apr 2025 12:04:08 +0200 Subject: [PATCH 1240/1714] winmake.py: fix test discovery which does not work on net mounted fs --- scripts/internal/winmake.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index d05dda0320..b0ee7759fa 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -318,7 +318,11 @@ def test(args=None): def test_by_name(arg): """Run specific test by name.""" build() - print(" ".join([PYTHON, "-m", "pytest"] + [arg])) + sh([ + PYTHON, + "-m", + "pytest", + ]) def test_by_regex(arg): @@ -344,61 +348,61 @@ def coverage(): def test_process(): """Run process tests.""" build() - sh([PYTHON, "psutil\\tests\\test_process.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_process.py"]) def test_process_all(): """Run process all tests.""" build() - sh([PYTHON, "psutil\\tests\\test_process_all.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_process_all.py"]) def test_system(): """Run system tests.""" build() - sh([PYTHON, "psutil\\tests\\test_system.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_system.py"]) def test_platform(): """Run windows only tests.""" build() - sh([PYTHON, "psutil\\tests\\test_windows.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_windows.py"]) def test_misc(): """Run misc tests.""" build() - sh([PYTHON, "psutil\\tests\\test_misc.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_misc.py"]) def test_scripts(): """Run scripts tests.""" build() - sh([PYTHON, "psutil\\tests\\test_scripts.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_scripts.py"]) def test_unicode(): """Run unicode tests.""" build() - sh([PYTHON, "psutil\\tests\\test_unicode.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_unicode.py"]) def test_connections(): """Run connections tests.""" build() - sh([PYTHON, "psutil\\tests\\test_connections.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_connections.py"]) def test_contracts(): """Run contracts tests.""" build() - sh([PYTHON, "psutil\\tests\\test_contracts.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_contracts.py"]) def test_testutils(): """Run test utilities tests.""" build() - sh([PYTHON, "psutil\\tests\\test_testutils.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_testutils.py"]) def test_sudo(): @@ -416,7 +420,7 @@ def test_last_failed(): def test_memleaks(): """Run memory leaks tests.""" build() - sh([PYTHON, "psutil\\tests\\test_memleaks.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_memleaks.py"]) def install_git_hooks(): From 5eead73d000c0f4c08f008b5532bbe388118fcb5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 9 Apr 2025 13:55:41 +0200 Subject: [PATCH 1241/1714] winmake.py: print err in case of permission error --- Makefile | 1 + scripts/internal/winmake.py | 21 +++++++++++---------- setup.py | 1 - 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index dc6c028fac..8771f733d8 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,7 @@ clean: ## Remove all build files. dist/ \ docs/_build/ \ htmlcov/ \ + pytest-cache-files* \ wheelhouse .PHONY: build diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index b0ee7759fa..c180de50b9 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -15,7 +15,6 @@ import argparse import atexit import ctypes -import errno import fnmatch import os import shutil @@ -124,16 +123,21 @@ def rm(pattern, directory=False): def safe_remove(path): try: os.remove(path) - except OSError as err: - if err.errno != errno.ENOENT: - raise + except FileNotFoundError: + pass + except PermissionError as err: + print(err) else: safe_print(f"rm {path}") def safe_rmtree(path): + def onerror(func, path, err): + if not issubclass(err[0], FileNotFoundError): + print(err[1]) + existed = os.path.isdir(path) - shutil.rmtree(path, ignore_errors=True) + shutil.rmtree(path, onerror=onerror) if existed and not os.path.isdir(path): safe_print(f"rmdir -f {path}") @@ -282,6 +286,7 @@ def clean(): "*__pycache__", ".coverage", ".failed-tests.txt", + "pytest-cache-files*", ) safe_rmtree("build") safe_rmtree(".coverage") @@ -318,11 +323,7 @@ def test(args=None): def test_by_name(arg): """Run specific test by name.""" build() - sh([ - PYTHON, - "-m", - "pytest", - ]) + sh([PYTHON, "-m", "pytest", arg]) def test_by_regex(arg): diff --git a/setup.py b/setup.py index 9758cddbf9..54aa9824c0 100755 --- a/setup.py +++ b/setup.py @@ -114,7 +114,6 @@ "vulture", "wheel", "pyreadline ; os_name == 'nt'", - "pdbpp ; os_name == 'nt'", ] macros = [] From d238ed81067e390ceb64ba4e60ab82f42fe9bf07 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Apr 2025 03:05:09 +0200 Subject: [PATCH 1242/1714] print debug msg if can't load (usually) optional win C API --- psutil/_psutil_common.c | 4 ++++ scripts/internal/winmake.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 81132fd3ee..653eb729ef 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -224,10 +224,12 @@ psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { FARPROC addr; if ((mod = GetModuleHandleA(libname)) == NULL) { + psutil_debug("%s -> %s not supported", libname, procname); PyErr_SetFromWindowsErrWithFilename(0, libname); return NULL; } if ((addr = GetProcAddress(mod, procname)) == NULL) { + psutil_debug("%s -> %s not supported", libname, procname); PyErr_SetFromWindowsErrWithFilename(0, procname); return NULL; } @@ -245,10 +247,12 @@ psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { mod = LoadLibraryA(libname); Py_END_ALLOW_THREADS if (mod == NULL) { + psutil_debug("%s -> %s not supported", libname, procname); PyErr_SetFromWindowsErrWithFilename(0, libname); return NULL; } if ((addr = GetProcAddress(mod, procname)) == NULL) { + psutil_debug("%s -> %s not supported", libname, procname); PyErr_SetFromWindowsErrWithFilename(0, procname); FreeLibrary(mod); return NULL; diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index c180de50b9..fb75a62b40 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -409,7 +409,7 @@ def test_testutils(): def test_sudo(): """Run sudo utilities tests.""" build() - sh([PYTHON, "-m", "unittest", "-v", "psutil\\tests\\test_sudo.py"]) + sh([PYTHON, "-m", "pytest", "-k", "test_sudo.py"]) def test_last_failed(): From acd30524a0671fddba84c6ebd047657cbec2a774 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Apr 2025 03:16:56 +0200 Subject: [PATCH 1243/1714] print debug msg if can't load (usually) optional win C API --- psutil/_psutil_common.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 653eb729ef..06ec78f241 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -219,18 +219,20 @@ CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; // A wrapper around GetModuleHandle and GetProcAddress. PVOID -psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { +psutil_GetProcAddress(LPCSTR libname, LPCSTR apiname) { HMODULE mod; FARPROC addr; if ((mod = GetModuleHandleA(libname)) == NULL) { - psutil_debug("%s -> %s not supported", libname, procname); + psutil_debug( + "%s module not supported (needed for %s)", libname, apiname + ); PyErr_SetFromWindowsErrWithFilename(0, libname); return NULL; } - if ((addr = GetProcAddress(mod, procname)) == NULL) { - psutil_debug("%s -> %s not supported", libname, procname); - PyErr_SetFromWindowsErrWithFilename(0, procname); + if ((addr = GetProcAddress(mod, apiname)) == NULL) { + psutil_debug("%s -> %s API not supported", libname, apiname); + PyErr_SetFromWindowsErrWithFilename(0, apiname); return NULL; } return addr; @@ -239,7 +241,7 @@ psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { // A wrapper around LoadLibrary and GetProcAddress. PVOID -psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { +psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR apiname) { HMODULE mod; FARPROC addr; @@ -247,13 +249,13 @@ psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { mod = LoadLibraryA(libname); Py_END_ALLOW_THREADS if (mod == NULL) { - psutil_debug("%s -> %s not supported", libname, procname); + psutil_debug("%s lib not supported (needed for %s)", libname, apiname); PyErr_SetFromWindowsErrWithFilename(0, libname); return NULL; } - if ((addr = GetProcAddress(mod, procname)) == NULL) { - psutil_debug("%s -> %s not supported", libname, procname); - PyErr_SetFromWindowsErrWithFilename(0, procname); + if ((addr = GetProcAddress(mod, apiname)) == NULL) { + psutil_debug("%s -> %s not supported", libname, apiname); + PyErr_SetFromWindowsErrWithFilename(0, apiname); FreeLibrary(mod); return NULL; } From ca03051e5ee70e696bdd1bb88dd951bc9b51db43 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 10 Apr 2025 17:50:00 +0200 Subject: [PATCH 1244/1714] [Windows] use `QueryInterruptTime` to determine `boot_time()` (#2554, fixes #2552) On Windows 7+ use QueryInterruptTime() instead of GetTickCount64. Reason: QueryInterruptTime() takes into account the time spent during suspend / hybernate. --- HISTORY.rst | 2 ++ Makefile | 2 +- psutil/_psutil_common.c | 4 ++++ psutil/_psutil_windows.c | 2 +- psutil/_pswindows.py | 6 ++++-- psutil/arch/windows/ntextapi.h | 6 ++++++ psutil/arch/windows/sys.c | 27 ++++++++++++++++---------- psutil/arch/windows/sys.h | 2 +- psutil/tests/test_sudo.py | 10 ++++------ psutil/tests/test_windows.py | 35 ++++++++++++++++++++++------------ 10 files changed, 63 insertions(+), 33 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 261dd9bfad..2e7c305369 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,8 @@ XXXX-XX-XX - 2542_: if system clock is updated `Process.children()`_ and `Process.parent()`_ may not be able to return the right information. - 2545_: [illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. +- 2552_, [Windows]: `boot_time()`_ didn't take into account the time spent + during suspend / hybernation. 7.0.0 ===== diff --git a/Makefile b/Makefile index 8771f733d8..59376705ad 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ install-git-hooks: ## Install GIT pre-commit hook. test: ## Run all tests. To run a specific test do "make test ARGS=psutil.tests.test_system.TestDiskAPIs" ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py --ignore=psutil/tests/test_sudo.py $(ARGS) test-parallel: ## Run all tests in parallel. ${MAKE} build diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 06ec78f241..dabf8a92df 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -346,6 +346,10 @@ psutil_loadlibs() { return 1; // --- Optional + + // minimum requirement: Win 7 + QueryInterruptTime = psutil_GetProcAddressFromLib( + "kernelbase.dll", "QueryInterruptTime"); // minimum requirement: Win 7 GetActiveProcessorCount = psutil_GetProcAddress( "kernel32", "GetActiveProcessorCount"); diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 4902f91487..a6b7420177 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -71,7 +71,7 @@ PsutilMethods[] = { {"proc_info", psutil_proc_info, METH_VARARGS}, // --- system-related functions - {"boot_time", psutil_boot_time, METH_VARARGS}, + {"uptime", psutil_uptime, METH_VARARGS}, {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, {"cpu_count_logical", psutil_cpu_count_logical, METH_VARARGS}, {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e5af3c90f4..7f5f8f0ebf 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -426,12 +426,14 @@ def sensors_battery(): def boot_time(): - """The system boot time expressed in seconds since the epoch.""" + """The system boot time expressed in seconds since the epoch. This + also includes the time spent during hybernate / suspend. + """ # This dirty hack is to adjust the precision of the returned # value which may have a 1 second fluctuation, see: # https://github.com/giampaolo/psutil/issues/1007 global _last_btime - ret = float(cext.boot_time()) + ret = time.time() - cext.uptime() if abs(ret - _last_btime) <= 1: return _last_btime else: diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index e0662fa044..9a1876de43 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -660,6 +660,12 @@ ULONGLONG (CALLBACK *_GetTickCount64) ( #define GetTickCount64 _GetTickCount64 +VOID(CALLBACK *_QueryInterruptTime) ( + PULONGLONG lpInterruptTime +); + +#define QueryInterruptTime _QueryInterruptTime + NTSTATUS (NTAPI *_NtQueryObject) ( HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index ada684f6f9..8fa03999ed 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -20,17 +20,24 @@ history before the move: #include "../../_psutil_common.h" -// Return a Python float representing the system uptime expressed in -// seconds since the epoch. +// The number of seconds passed since boot. This is a monotonic timer, +// not affected by system clock updates. On Windows 7+ it also includes +// the time spent during suspend / hybernate. PyObject * -psutil_boot_time(PyObject *self, PyObject *args) { - ULONGLONG upTime; - FILETIME fileTime; - - GetSystemTimeAsFileTime(&fileTime); - // Number of milliseconds that have elapsed since the system was started. - upTime = GetTickCount64() / 1000ull; - return Py_BuildValue("d", psutil_FiletimeToUnixTime(fileTime) - upTime); +psutil_uptime(PyObject *self, PyObject *args) { + double uptimeSeconds; + ULONGLONG interruptTime100ns = 0; + + if (QueryInterruptTime) { // Windows 7+ + QueryInterruptTime(&interruptTime100ns); + // Convert from 100-nanosecond to seconds. + uptimeSeconds = interruptTime100ns / 10000000.0; + } + else { + // Convert from milliseconds to seconds. + uptimeSeconds = (double)GetTickCount64() / 1000.0; + } + return Py_BuildValue("d", uptimeSeconds); } diff --git a/psutil/arch/windows/sys.h b/psutil/arch/windows/sys.h index 344ca21d42..83d143bd9d 100644 --- a/psutil/arch/windows/sys.h +++ b/psutil/arch/windows/sys.h @@ -6,5 +6,5 @@ #include -PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_uptime(PyObject *self, PyObject *args); PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py index beae228f62..e8a3ecc0af 100755 --- a/psutil/tests/test_sudo.py +++ b/psutil/tests/test_sudo.py @@ -64,20 +64,18 @@ class TestUpdatedSystemTime(PsutilTestCase): def setUp(self): self.time_updated = False self.orig_time = get_systime() + self.time_started = time.monotonic() def tearDown(self): if self.time_updated: - set_systime(self.orig_time) - if WINDOWS: - self.assertAlmostEqual(get_systime(), self.orig_time, delta=1) - else: - self.assertEqual(get_systime(), self.orig_time) + extra_t = time.monotonic() - self.time_started + set_systime(self.orig_time + extra_t) def update_systime(self): # set system time 1 hour later set_systime(self.orig_time + 3600) + self.time_updated = True - @unittest.skipIf(WINDOWS, "broken on WINDOWS") # TODO: fix it def test_boot_time(self): # Test that boot_time() reflects system clock updates. t1 = psutil.boot_time() diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 8e4f400d63..2c540afb65 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -6,6 +6,7 @@ """Windows specific tests.""" +import ctypes import datetime import glob import os @@ -274,18 +275,28 @@ def test_boot_time(self): ) psutil_dt = datetime.datetime.fromtimestamp(psutil.boot_time()) diff = abs((wmi_btime_dt - psutil_dt).total_seconds()) - assert 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): - assert psutil.boot_time() == 5 - with mock.patch('psutil._pswindows.cext.boot_time', return_value=4): - assert psutil.boot_time() == 5 - with mock.patch('psutil._pswindows.cext.boot_time', return_value=6): - assert psutil.boot_time() == 5 - with mock.patch('psutil._pswindows.cext.boot_time', return_value=333): - assert psutil.boot_time() == 333 + assert diff <= 5, (psutil_dt, wmi_btime_dt) + + def test_uptime_1(self): + # ...against QueryInterruptTime() (Windows 7+) + ULONGLONG = ctypes.c_ulonglong + + kernelbase = ctypes.WinDLL("kernelbase.dll") + QueryInterruptTime = kernelbase.QueryInterruptTime + QueryInterruptTime.argtypes = [ctypes.POINTER(ULONGLONG)] + QueryInterruptTime.restype = ctypes.c_bool + + interrupt_time_100ns = ULONGLONG(0) + assert QueryInterruptTime(ctypes.byref(interrupt_time_100ns)) + secs = interrupt_time_100ns.value / 10000000.0 + assert abs(cext.uptime() - secs) < 0.5 + + def test_uptime_2(self): + # ...against GetTickCount64() (Windows < 7, does not include + # time spent during suspend / hybernate). + ms = ctypes.windll.kernel32.GetTickCount64() + secs = ms / 1000.0 + assert abs(cext.uptime() - secs) < 0.5 # =================================================================== From cedc20a9a750f67adbd4386a6287284f5edd9204 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 11 Apr 2025 17:40:59 +0200 Subject: [PATCH 1245/1714] C files refactoring (#2556) Create a init.c file containing platform-specific C code, which is both executed on init and has all the utility objects used by plaform C modules. Also get rid of _psutil_common.c which contained code of all platforms. --- MANIFEST.in | 10 +- psutil/_psutil_aix.c | 2 +- psutil/_psutil_bsd.c | 2 +- psutil/_psutil_common.h | 167 ----------- psutil/_psutil_linux.c | 2 +- psutil/_psutil_osx.c | 4 +- psutil/_psutil_posix.c | 2 +- psutil/_psutil_sunos.c | 2 +- psutil/_psutil_windows.c | 5 +- psutil/arch/aix/net_connections.c | 3 +- psutil/arch/all/init.c | 119 ++++++++ psutil/arch/all/init.h | 100 +++++++ psutil/arch/bsd/init.c | 25 ++ psutil/arch/bsd/init.h | 7 + psutil/arch/bsd/proc.c | 2 +- psutil/arch/freebsd/cpu.c | 2 +- psutil/arch/freebsd/disk.c | 3 +- psutil/arch/freebsd/mem.c | 2 +- psutil/arch/freebsd/proc.c | 2 +- psutil/arch/freebsd/proc_socks.c | 2 +- psutil/arch/freebsd/sensors.c | 3 +- psutil/arch/freebsd/sys_socks.c | 3 +- psutil/arch/linux/disk.c | 2 +- psutil/arch/linux/mem.c | 2 +- psutil/arch/linux/net.c | 2 +- psutil/arch/linux/proc.c | 2 +- psutil/arch/linux/users.c | 2 +- psutil/arch/netbsd/mem.c | 2 +- psutil/arch/netbsd/proc.c | 2 +- psutil/arch/netbsd/socks.c | 2 +- psutil/arch/openbsd/proc.c | 2 +- psutil/arch/openbsd/socks.c | 2 +- psutil/arch/osx/cpu.c | 2 +- psutil/arch/osx/disk.c | 2 +- psutil/arch/osx/init.c | 21 ++ psutil/arch/osx/init.h | 11 + psutil/arch/osx/net.c | 2 +- psutil/arch/osx/proc.c | 2 +- psutil/arch/osx/sensors.c | 2 +- psutil/arch/osx/sys.c | 2 +- psutil/arch/windows/cpu.c | 2 +- psutil/arch/windows/disk.c | 2 +- .../{_psutil_common.c => arch/windows/init.c} | 278 +++++------------- psutil/arch/windows/init.h | 61 ++++ psutil/arch/windows/mem.c | 2 +- psutil/arch/windows/net.c | 2 +- psutil/arch/windows/proc.c | 2 +- psutil/arch/windows/proc_handles.c | 2 +- psutil/arch/windows/proc_info.c | 2 +- psutil/arch/windows/proc_utils.c | 2 +- psutil/arch/windows/security.c | 2 +- psutil/arch/windows/services.c | 2 +- psutil/arch/windows/socks.c | 2 +- psutil/arch/windows/sys.c | 2 +- psutil/arch/windows/wmi.c | 2 +- setup.py | 2 +- 56 files changed, 471 insertions(+), 429 deletions(-) delete mode 100644 psutil/_psutil_common.h create mode 100644 psutil/arch/all/init.c create mode 100644 psutil/arch/all/init.h create mode 100644 psutil/arch/bsd/init.c create mode 100644 psutil/arch/bsd/init.h create mode 100644 psutil/arch/osx/init.c create mode 100644 psutil/arch/osx/init.h rename psutil/{_psutil_common.c => arch/windows/init.c} (62%) create mode 100644 psutil/arch/windows/init.h diff --git a/MANIFEST.in b/MANIFEST.in index 2a99ac1ea2..36a947c75a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -32,8 +32,6 @@ include psutil/_psposix.py include psutil/_pssunos.py include psutil/_psutil_aix.c include psutil/_psutil_bsd.c -include psutil/_psutil_common.c -include psutil/_psutil_common.h include psutil/_psutil_linux.c include psutil/_psutil_osx.c include psutil/_psutil_posix.c @@ -48,10 +46,14 @@ include psutil/arch/aix/ifaddrs.h include psutil/arch/aix/net_connections.c include psutil/arch/aix/net_connections.h include psutil/arch/aix/net_kernel_structs.h +include psutil/arch/all/init.c +include psutil/arch/all/init.h include psutil/arch/bsd/cpu.c include psutil/arch/bsd/cpu.h include psutil/arch/bsd/disk.c include psutil/arch/bsd/disk.h +include psutil/arch/bsd/init.c +include psutil/arch/bsd/init.h include psutil/arch/bsd/net.c include psutil/arch/bsd/net.h include psutil/arch/bsd/proc.c @@ -106,6 +108,8 @@ include psutil/arch/osx/cpu.c include psutil/arch/osx/cpu.h include psutil/arch/osx/disk.c include psutil/arch/osx/disk.h +include psutil/arch/osx/init.c +include psutil/arch/osx/init.h include psutil/arch/osx/mem.c include psutil/arch/osx/mem.h include psutil/arch/osx/net.c @@ -124,6 +128,8 @@ include psutil/arch/windows/cpu.c include psutil/arch/windows/cpu.h include psutil/arch/windows/disk.c include psutil/arch/windows/disk.h +include psutil/arch/windows/init.c +include psutil/arch/windows/init.h include psutil/arch/windows/mem.c include psutil/arch/windows/mem.h include psutil/arch/windows/net.c diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 8c73d71b00..4b278da5c3 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -50,7 +50,7 @@ #include #include -#include "_psutil_common.h" +#include "arch/all/init.h" #include "_psutil_posix.h" #include "arch/aix/ifaddrs.h" #include "arch/aix/net_connections.h" diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 48298cde22..3f2973a0e9 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -21,7 +21,7 @@ #include // BSD version #include // for TCP connection states -#include "_psutil_common.h" +#include "arch/all/init.h" #include "_psutil_posix.h" #include "arch/bsd/cpu.h" #include "arch/bsd/disk.h" diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h deleted file mode 100644 index 024452630f..0000000000 --- a/psutil/_psutil_common.h +++ /dev/null @@ -1,167 +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. - */ - -#include - -// ==================================================================== -// --- Global vars / constants -// ==================================================================== - -extern int PSUTIL_DEBUG; -// a signaler for connections without an actual status -static const int PSUTIL_CONN_NONE = 128; - -// strncpy() variant which appends a null terminator. -#define PSUTIL_STRNCPY(dst, src, n) \ - strncpy(dst, src, n - 1); \ - dst[n - 1] = '\0' - -// ==================================================================== -// --- Backward compatibility with missing Python.h APIs -// ==================================================================== - -#if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) - #if !defined(PyErr_SetFromWindowsErrWithFilename) - PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, - const char *filename); - #endif - #if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) - PyObject *PyErr_SetExcFromWindowsErrWithFilenameObject( - PyObject *type, int ierr, PyObject *filename); - #endif -#endif - -// --- _Py_PARSE_PID - -// SIZEOF_INT|LONG is missing on Linux + PyPy (only?). -// In this case we guess it from setup.py. It's not 100% bullet proof, -// If wrong we'll probably get compiler warnings. -// FWIW on all UNIX platforms I've seen pid_t is defined as an int. -// _getpid() on Windows also returns an int. -#if !defined(SIZEOF_INT) - #define SIZEOF_INT 4 -#endif -#if !defined(SIZEOF_LONG) - #define SIZEOF_LONG 8 -#endif -#if !defined(SIZEOF_PID_T) - #define SIZEOF_PID_T PSUTIL_SIZEOF_PID_T // set as a macro in setup.py -#endif - -// _Py_PARSE_PID was added in Python 3, but since it's private we make -// sure it's always present. -#ifndef _Py_PARSE_PID - #if SIZEOF_PID_T == SIZEOF_INT - #define _Py_PARSE_PID "i" - #elif SIZEOF_PID_T == SIZEOF_LONG - #define _Py_PARSE_PID "l" - #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG - #define _Py_PARSE_PID "L" - #else - #error "_Py_PARSE_PID: sizeof(pid_t) is neither sizeof(int), " - "sizeof(long) or sizeof(long long)" - #endif -#endif - -// PyPy on Windows -#ifndef PyLong_FromPid - #if ((SIZEOF_PID_T == SIZEOF_INT) || (SIZEOF_PID_T == SIZEOF_LONG)) - #define PyLong_FromPid PyLong_FromLong - #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG - #define PyLong_FromPid PyLong_FromLongLong - #else - #error "PyLong_FromPid: sizeof(pid_t) is neither sizeof(int), " - "sizeof(long) or sizeof(long long)" - #endif -#endif - -// ==================================================================== -// --- Custom exceptions -// ==================================================================== - -PyObject* AccessDenied(const char *msg); -PyObject* NoSuchProcess(const char *msg); -PyObject* psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); - -// ==================================================================== -// --- Global utils -// ==================================================================== - -PyObject* psutil_check_pid_range(PyObject *self, PyObject *args); -PyObject* psutil_set_debug(PyObject *self, PyObject *args); -int psutil_setup(void); - - -// Print a debug message on stderr. -#define psutil_debug(...) do { \ - if (! PSUTIL_DEBUG) \ - break; \ - fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n");} while(0) - - -// ==================================================================== -// --- BSD -// ==================================================================== - -void convert_kvm_err(const char *syscall, char *errbuf); - -// ==================================================================== -// --- macOS -// ==================================================================== - -#ifdef PSUTIL_OSX - #include - - extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; -#endif - -// ==================================================================== -// --- Windows -// ==================================================================== - -#ifdef PSUTIL_WINDOWS - #include - // make it available to any file which includes this module - #include "arch/windows/ntextapi.h" - - extern int PSUTIL_WINVER; - extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; - extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; - - #define PSUTIL_WINDOWS_VISTA 60 - #define PSUTIL_WINDOWS_7 61 - #define PSUTIL_WINDOWS_8 62 - #define PSUTIL_WINDOWS_8_1 63 - #define PSUTIL_WINDOWS_10 100 - #define PSUTIL_WINDOWS_NEW MAXLONG - - #define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) - #define MALLOC_ZERO(x) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (x)) - #define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) - - #define _NT_FACILITY_MASK 0xfff - #define _NT_FACILITY_SHIFT 16 - #define _NT_FACILITY(status) \ - ((((ULONG)(status)) >> _NT_FACILITY_SHIFT) & _NT_FACILITY_MASK) - - #define NT_NTWIN32(status) (_NT_FACILITY(status) == FACILITY_WIN32) - #define WIN32_FROM_NTSTATUS(status) (((ULONG)(status)) & 0xffff) - - #define LO_T 1e-7 - #define HI_T 429.4967296 - - #ifndef AF_INET6 - #define AF_INET6 23 - #endif - - PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); - PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); - PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); - double psutil_FiletimeToUnixTime(FILETIME ft); - double psutil_LargeIntegerToUnixTime(LARGE_INTEGER li); -#endif diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 234680c7b5..9f910f0360 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -12,7 +12,7 @@ #include #include // DUPLEX_* -#include "_psutil_common.h" +#include "arch/all/init.h" #include "arch/linux/disk.h" #include "arch/linux/mem.h" #include "arch/linux/net.h" diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index a9e116f5af..2103bf052a 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -11,7 +11,7 @@ #include #include -#include "_psutil_common.h" +#include "arch/all/init.h" #include "arch/osx/cpu.h" #include "arch/osx/disk.h" #include "arch/osx/mem.h" @@ -88,6 +88,8 @@ PyInit__psutil_osx(void) { if (psutil_setup() != 0) return NULL; + if (psutil_setup_osx() != 0) + return NULL; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 53ffccdfbb..02b85ddcb9 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -49,7 +49,7 @@ #include #endif -#include "_psutil_common.h" +#include "arch/all/init.h" // ==================================================================== diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 6c33de8886..564adaafb7 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -53,7 +53,7 @@ #include // fabs() #include -#include "_psutil_common.h" +#include "arch/all/init.h" #include "_psutil_posix.h" #include "arch/solaris/environ.h" diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index a6b7420177..6774546dc5 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -17,9 +17,10 @@ #include #include -#include "_psutil_common.h" +#include "arch/all/init.h" #include "arch/windows/cpu.h" #include "arch/windows/disk.h" +#include "arch/windows/init.h" #include "arch/windows/mem.h" #include "arch/windows/net.h" #include "arch/windows/proc.h" @@ -158,6 +159,8 @@ PyInit__psutil_windows(void) { if (psutil_setup() != 0) return NULL; + if (psutil_setup_windows() != 0) + return NULL; if (psutil_set_se_debug() != 0) return NULL; diff --git a/psutil/arch/aix/net_connections.c b/psutil/arch/aix/net_connections.c index 69b4389201..71ad34341a 100644 --- a/psutil/arch/aix/net_connections.c +++ b/psutil/arch/aix/net_connections.c @@ -26,11 +26,12 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "net_kernel_structs.h" #include "net_connections.h" #include "common.h" + #define NO_SOCKET (PyObject *)(-1) static int diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c new file mode 100644 index 0000000000..b72dcbf6f8 --- /dev/null +++ b/psutil/arch/all/init.c @@ -0,0 +1,119 @@ +/* + * 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. + */ + +// Global names shared by all platforms. + +#include +#ifdef PSUTIL_WINDOWS +#include +#endif + + +int PSUTIL_DEBUG = 0; +int PSUTIL_CONN_NONE = 128; + + +// Set OSError(errno=ESRCH, strerror="No such process (originated from") +// Python exception. +PyObject * +NoSuchProcess(const char *syscall) { + PyObject *exc; + char msg[1024]; + + sprintf(msg, "assume no such process (originated from %s)", syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", ESRCH, msg); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); + return NULL; +} + + +// Set OSError(errno=EACCES, strerror="Permission denied" (originated from ...) +// Python exception. +PyObject * +AccessDenied(const char *syscall) { + PyObject *exc; + char msg[1024]; + + sprintf(msg, "assume access denied (originated from %s)", syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", EACCES, msg); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); + return NULL; +} + + +// Same as PyErr_SetFromErrno(0) but adds the syscall to the exception +// message. +PyObject * +psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { + char fullmsg[1024]; + +#ifdef PSUTIL_WINDOWS + DWORD dwLastError = GetLastError(); + sprintf(fullmsg, "(originated from %s)", syscall); + PyErr_SetFromWindowsErrWithFilename(dwLastError, fullmsg); +#else + PyObject *exc; + sprintf(fullmsg, "%s (originated from %s)", strerror(errno), syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", errno, fullmsg); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); +#endif + return NULL; +} + + +// Enable or disable PSUTIL_DEBUG messages. +PyObject * +psutil_set_debug(PyObject *self, PyObject *args) { + PyObject *value; + int x; + + if (!PyArg_ParseTuple(args, "O", &value)) + return NULL; + x = PyObject_IsTrue(value); + if (x < 0) { + return NULL; + } + else if (x == 0) { + PSUTIL_DEBUG = 0; + } + else { + PSUTIL_DEBUG = 1; + } + Py_RETURN_NONE; +} + + +// Raise OverflowError if Python int value overflowed when converting +// to pid_t. Raise ValueError if Python int value is negative. +// Otherwise, return None. +PyObject * +psutil_check_pid_range(PyObject *self, PyObject *args) { +#ifdef PSUTIL_WINDOWS + DWORD pid; +#else + pid_t pid; +#endif + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (pid < 0) { + PyErr_SetString(PyExc_ValueError, "pid must be a positive integer"); + return NULL; + } + Py_RETURN_NONE; +} + + +// Called on module import on all platforms. +int +psutil_setup(void) { + if (getenv("PSUTIL_DEBUG") != NULL) + PSUTIL_DEBUG = 1; + return 0; +} diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h new file mode 100644 index 0000000000..2a22c794ec --- /dev/null +++ b/psutil/arch/all/init.h @@ -0,0 +1,100 @@ +/* + * 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. + */ + +// Global names shared by all platforms. + +#include + +// We do this so that all .c files have to include only one header +// (ourselves, init.h). +#if defined(PSUTIL_WINDOWS) + #include "../../arch/windows/init.h" +#elif defined(PSUTIL_OSX) + #include "../../arch/osx/init.h" +#elif defined(PSUTIL_BSD) + #include "../../arch/bsd/init.h" +#endif + + +// print debug messages when set to 1 +extern int PSUTIL_DEBUG; +// a signaler for connections without an actual status +extern int PSUTIL_CONN_NONE; + + +// Print a debug message on stderr. +#define psutil_debug(...) do { \ + if (! PSUTIL_DEBUG) \ + break; \ + fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n");} while(0) + + +// strncpy() variant which appends a null terminator. +#define PSUTIL_STRNCPY(dst, src, n) \ + strncpy(dst, src, n - 1); \ + dst[n - 1] = '\0' + +// ==================================================================== +// --- Custom exceptions +// ==================================================================== + +PyObject* AccessDenied(const char *msg); +PyObject* NoSuchProcess(const char *msg); +PyObject* psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); + +// ==================================================================== +// --- Backward compatibility with missing Python.h APIs +// ==================================================================== + +// --- _Py_PARSE_PID + +// SIZEOF_INT|LONG is missing on Linux + PyPy (only?). +// In this case we guess it from setup.py. It's not 100% bullet proof, +// If wrong we'll probably get compiler warnings. +// FWIW on all UNIX platforms I've seen pid_t is defined as an int. +// _getpid() on Windows also returns an int. +#if !defined(SIZEOF_INT) + #define SIZEOF_INT 4 +#endif +#if !defined(SIZEOF_LONG) + #define SIZEOF_LONG 8 +#endif +#if !defined(SIZEOF_PID_T) + #define SIZEOF_PID_T PSUTIL_SIZEOF_PID_T // set as a macro in setup.py +#endif + +// _Py_PARSE_PID was added in Python 3, but since it's private we make +// sure it's always present. +#ifndef _Py_PARSE_PID + #if SIZEOF_PID_T == SIZEOF_INT + #define _Py_PARSE_PID "i" + #elif SIZEOF_PID_T == SIZEOF_LONG + #define _Py_PARSE_PID "l" + #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG + #define _Py_PARSE_PID "L" + #else + #error "_Py_PARSE_PID: sizeof(pid_t) is neither sizeof(int), " + "sizeof(long) or sizeof(long long)" + #endif +#endif + +// PyPy on Windows +#ifndef PyLong_FromPid + #if ((SIZEOF_PID_T == SIZEOF_INT) || (SIZEOF_PID_T == SIZEOF_LONG)) + #define PyLong_FromPid PyLong_FromLong + #elif defined(SIZEOF_LONG_LONG) && SIZEOF_PID_T == SIZEOF_LONG_LONG + #define PyLong_FromPid PyLong_FromLongLong + #else + #error "PyLong_FromPid: sizeof(pid_t) is neither sizeof(int), " + "sizeof(long) or sizeof(long long)" + #endif +#endif + +PyObject* psutil_set_debug(PyObject *self, PyObject *args); +PyObject* psutil_check_pid_range(PyObject *self, PyObject *args); +int psutil_setup(void); diff --git a/psutil/arch/bsd/init.c b/psutil/arch/bsd/init.c new file mode 100644 index 0000000000..9a965b72fd --- /dev/null +++ b/psutil/arch/bsd/init.c @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#include +#include + +#include "../../arch/all/init.h" +#include "init.h" + + +void +convert_kvm_err(const char *syscall, char *errbuf) { + char fullmsg[8192]; + + sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); + if (strstr(errbuf, "Permission denied") != NULL) + AccessDenied(fullmsg); + else if (strstr(errbuf, "Operation not permitted") != NULL) + AccessDenied(fullmsg); + else + PyErr_Format(PyExc_RuntimeError, fullmsg); +} diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h new file mode 100644 index 0000000000..14d9571e58 --- /dev/null +++ b/psutil/arch/bsd/init.h @@ -0,0 +1,7 @@ +/* + * 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. + */ + +void convert_kvm_err(const char *syscall, char *errbuf); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 959b28ee06..eb699ef087 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -16,7 +16,7 @@ #include // kinfo_getfile() #endif -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" #ifdef PSUTIL_FREEBSD #include "../../arch/freebsd/proc.h" diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 29a5ec5758..822d2267f2 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -20,7 +20,7 @@ For reference, here's the git history with original(ish) implementations: #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index 320b2bc819..3a44975ecc 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -8,7 +8,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" @@ -83,4 +83,3 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { free(stats.dinfo); return NULL; } - diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 0482ef72df..ecf1ce5c71 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -14,7 +14,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 5c6fab971a..791e021e57 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -21,7 +21,7 @@ #include // process open files, shared libs (kinfo_getvmmap), cwd #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index c84c3b04a8..07a40dabc1 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -19,7 +19,7 @@ #include // for inet_ntop() #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 08a882dd93..98e57c8353 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -16,7 +16,7 @@ For reference, here's the git history with original(ish) implementations: #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" @@ -79,4 +79,3 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } - diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 3887bd403a..212ff37e0a 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -26,9 +26,10 @@ #include // for struct xtcpcb #include // for inet_ntop() -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" + static struct xfile *psutil_xfiles; static int psutil_nxfiles; diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c index 692a7d5d47..0fb82a46fc 100644 --- a/psutil/arch/linux/disk.c +++ b/psutil/arch/linux/disk.c @@ -7,7 +7,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" // Return disk mounted partitions as a list of tuples including device, diff --git a/psutil/arch/linux/mem.c b/psutil/arch/linux/mem.c index 3b9b4fef3f..cacf1e7766 100644 --- a/psutil/arch/linux/mem.c +++ b/psutil/arch/linux/mem.c @@ -7,7 +7,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index 6d2785ee63..774d67f0f2 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -25,7 +25,7 @@ #define _LINUX_SYSINFO_H #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" // * defined in linux/ethtool.h but not always available (e.g. Android) diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index f8230ee75b..57a0f90885 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -9,8 +9,8 @@ #include #include +#include "../../arch/all/init.h" #include "proc.h" -#include "../../_psutil_common.h" #ifdef PSUTIL_HAVE_IOPRIO diff --git a/psutil/arch/linux/users.c b/psutil/arch/linux/users.c index 663d0f2a45..ff2b39e745 100644 --- a/psutil/arch/linux/users.c +++ b/psutil/arch/linux/users.c @@ -8,7 +8,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 456479ba1b..8fe92f5ec4 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -19,7 +19,7 @@ original(ish) implementations: #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 7613b54594..cc6c69d175 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -11,7 +11,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" #include "proc.h" diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index b3d6aac510..200b816836 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -21,7 +21,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index bf4015be41..ca48d7e168 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -13,7 +13,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c index 05849aa1d8..4c76a1c8b8 100644 --- a/psutil/arch/openbsd/socks.c +++ b/psutil/arch/openbsd/socks.c @@ -14,7 +14,7 @@ #include // INET6_ADDRSTRLEN, in6_addr #undef _KERNEL -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 769b1a8a1e..6d993647ae 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -31,7 +31,7 @@ For reference, here's the git history with original implementations: #include #endif -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" // added in macOS 12 diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index e1a8f5a492..b15fcd7c4a 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -18,7 +18,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" /* diff --git a/psutil/arch/osx/init.c b/psutil/arch/osx/init.c new file mode 100644 index 0000000000..af4d3b5390 --- /dev/null +++ b/psutil/arch/osx/init.c @@ -0,0 +1,21 @@ +/* + * 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. + */ + +#include +#include + +#include "../../arch/all/init.h" +#include "init.h" + + +struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; + +// Called on module import. +int +psutil_setup_osx(void) { + mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); + return 0; +} diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h new file mode 100644 index 0000000000..3c4dc6b04a --- /dev/null +++ b/psutil/arch/osx/init.h @@ -0,0 +1,11 @@ +/* + * 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. + */ + +#include + +extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; + +int psutil_setup_osx(void); diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index d365676ce1..6848f1a4fb 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -15,7 +15,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 681642c3ad..18e2b46554 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -29,7 +29,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "../../_psutil_posix.h" diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index 53626c2dc4..1015506cbf 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -16,7 +16,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/osx/sys.c b/psutil/arch/osx/sys.c index 32878319cc..6be5b809f4 100644 --- a/psutil/arch/osx/sys.c +++ b/psutil/arch/osx/sys.c @@ -12,7 +12,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 9d89e5bb6c..6d70ab37cc 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -8,7 +8,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" /* diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 552929f38a..224f968260 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -8,7 +8,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #ifndef _ARRAYSIZE diff --git a/psutil/_psutil_common.c b/psutil/arch/windows/init.c similarity index 62% rename from psutil/_psutil_common.c rename to psutil/arch/windows/init.c index dabf8a92df..19285db58a 100644 --- a/psutil/_psutil_common.c +++ b/psutil/arch/windows/init.c @@ -2,18 +2,20 @@ * 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. - * - * Routines common to all platforms. */ #include -#include "_psutil_common.h" +#include -// ==================================================================== -// --- Global vars -// ==================================================================== +#include "../../arch/all/init.h" +#include "init.h" +#include "ntextapi.h" -int PSUTIL_DEBUG = 0; + +// Needed to make these globally visible. +int PSUTIL_WINVER; +SYSTEM_INFO PSUTIL_SYSTEM_INFO; +CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; // ==================================================================== @@ -21,7 +23,7 @@ int PSUTIL_DEBUG = 0; // ==================================================================== // PyPy on Windows. Missing APIs added in PyPy 7.3.14. -#if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) +#if defined(PYPY_VERSION) #if !defined(PyErr_SetFromWindowsErrWithFilename) PyObject * PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { @@ -60,161 +62,37 @@ PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { #if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) PyObject * -PyErr_SetExcFromWindowsErrWithFilenameObject(PyObject *type, - int ierr, - PyObject *filename) { +PyErr_SetExcFromWindowsErrWithFilenameObject( + PyObject *type, int ierr, PyObject *filename) +{ // Original function is too complex. Just raise OSError without // filename. return PyErr_SetFromWindowsErrWithFilename(ierr, NULL); } #endif // !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) -#endif // defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) +#endif // defined(PYPY_VERSION) // ==================================================================== -// --- Custom exceptions +// --- Utils // ==================================================================== -/* - * Same as PyErr_SetFromErrno(0) but adds the syscall to the exception - * message. - */ -PyObject * -psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { +// Convert a NTSTATUS value to a Win32 error code and set the proper +// Python exception. +PVOID +psutil_SetFromNTStatusErr(NTSTATUS status, const char *syscall) { + ULONG err; char fullmsg[1024]; -#ifdef PSUTIL_WINDOWS - DWORD dwLastError = GetLastError(); - sprintf(fullmsg, "(originated from %s)", syscall); - PyErr_SetFromWindowsErrWithFilename(dwLastError, fullmsg); -#else - PyObject *exc; - sprintf(fullmsg, "%s (originated from %s)", strerror(errno), syscall); - exc = PyObject_CallFunction(PyExc_OSError, "(is)", errno, fullmsg); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); -#endif - return NULL; -} - - -/* - * Set OSError(errno=ESRCH, strerror="No such process (originated from") - * Python exception. - */ -PyObject * -NoSuchProcess(const char *syscall) { - PyObject *exc; - char msg[1024]; - - sprintf(msg, "assume no such process (originated from %s)", syscall); - exc = PyObject_CallFunction(PyExc_OSError, "(is)", ESRCH, msg); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); - return NULL; -} - - -/* - * Set OSError(errno=EACCES, strerror="Permission denied" (originated from ...) - * Python exception. - */ -PyObject * -AccessDenied(const char *syscall) { - PyObject *exc; - char msg[1024]; - - sprintf(msg, "assume access denied (originated from %s)", syscall); - exc = PyObject_CallFunction(PyExc_OSError, "(is)", EACCES, msg); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); - return NULL; -} - -/* - * Raise OverflowError if Python int value overflowed when converting to pid_t. - * Raise ValueError if Python int value is negative. - * Otherwise, return None. - */ -PyObject * -psutil_check_pid_range(PyObject *self, PyObject *args) { -#ifdef PSUTIL_WINDOWS - DWORD pid; -#else - pid_t pid; -#endif - - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (pid < 0) { - PyErr_SetString(PyExc_ValueError, "pid must be a positive integer"); - return NULL; - } - Py_RETURN_NONE; -} - -// Enable or disable PSUTIL_DEBUG messages. -PyObject * -psutil_set_debug(PyObject *self, PyObject *args) { - PyObject *value; - int x; - - if (!PyArg_ParseTuple(args, "O", &value)) - return NULL; - x = PyObject_IsTrue(value); - if (x < 0) { - return NULL; - } - else if (x == 0) { - PSUTIL_DEBUG = 0; - } - else { - PSUTIL_DEBUG = 1; - } - Py_RETURN_NONE; -} - - -// ============================================================================ -// Utility functions (BSD) -// ============================================================================ - -#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) -void -convert_kvm_err(const char *syscall, char *errbuf) { - char fullmsg[8192]; - - sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); - if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(fullmsg); - else if (strstr(errbuf, "Operation not permitted") != NULL) - AccessDenied(fullmsg); + if (NT_NTWIN32(status)) + err = WIN32_FROM_NTSTATUS(status); else - PyErr_Format(PyExc_RuntimeError, fullmsg); + err = RtlNtStatusToDosErrorNoTeb(status); + // if (GetLastError() != 0) + // err = GetLastError(); + sprintf(fullmsg, "(originated from %s)", syscall); + return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); } -#endif - -// ==================================================================== -// --- macOS -// ==================================================================== - -#ifdef PSUTIL_OSX -#include - -struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; -#endif - -// ==================================================================== -// --- Windows -// ==================================================================== - -#ifdef PSUTIL_WINDOWS -#include - -// Needed to make these globally visible. -int PSUTIL_WINVER; -SYSTEM_INFO PSUTIL_SYSTEM_INFO; -CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; // A wrapper around GetModuleHandle and GetProcAddress. @@ -265,26 +143,47 @@ psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR apiname) { } -/* - * Convert a NTSTATUS value to a Win32 error code and set the proper - * Python exception. - */ -PVOID -psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall) { - ULONG err; - char fullmsg[1024]; +// Convert the hi and lo parts of a FILETIME structure or a +// LARGE_INTEGER to a UNIX time. A FILETIME contains a 64-bit value +// representing the number of 100-nanosecond intervals since January 1, +// 1601 (UTC). A UNIX time is the number of seconds that have elapsed +// since the UNIX epoch, that is the time 00:00:00 UTC on 1 January +// 1970. +static double +_to_unix_time(ULONGLONG hiPart, ULONGLONG loPart) { + ULONGLONG ret; - if (NT_NTWIN32(Status)) - err = WIN32_FROM_NTSTATUS(Status); - else - err = RtlNtStatusToDosErrorNoTeb(Status); - // if (GetLastError() != 0) - // err = GetLastError(); - sprintf(fullmsg, "(originated from %s)", syscall); - return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); + // 100 nanosecond intervals since January 1, 1601. + ret = hiPart << 32; + ret += loPart; + // Change starting time to the Epoch (00:00:00 UTC, January 1, 1970). + ret -= 116444736000000000ull; + // Convert nano secs to secs. + return (double) ret / 10000000ull; +} + + +double +psutil_FiletimeToUnixTime(FILETIME ft) { + return _to_unix_time( + (ULONGLONG)ft.dwHighDateTime, (ULONGLONG)ft.dwLowDateTime + ); +} + + +double +psutil_LargeIntegerToUnixTime(LARGE_INTEGER li) { + return _to_unix_time( + (ULONGLONG)li.HighPart, (ULONGLONG)li.LowPart + ); } +// ==================================================================== +// --- Init / load libs +// ==================================================================== + + static int psutil_loadlibs() { // --- Mandatory @@ -396,59 +295,14 @@ psutil_set_winver() { } -/* - * Convert the hi and lo parts of a FILETIME structure or a LARGE_INTEGER - * to a UNIX time. - * A FILETIME contains a 64-bit value representing the number of - * 100-nanosecond intervals since January 1, 1601 (UTC). - * A UNIX time is the number of seconds that have elapsed since the - * UNIX epoch, that is the time 00:00:00 UTC on 1 January 1970. - */ -static double -_to_unix_time(ULONGLONG hiPart, ULONGLONG loPart) { - ULONGLONG ret; - - // 100 nanosecond intervals since January 1, 1601. - ret = hiPart << 32; - ret += loPart; - // Change starting time to the Epoch (00:00:00 UTC, January 1, 1970). - ret -= 116444736000000000ull; - // Convert nano secs to secs. - return (double) ret / 10000000ull; -} - - -double -psutil_FiletimeToUnixTime(FILETIME ft) { - return _to_unix_time((ULONGLONG)ft.dwHighDateTime, - (ULONGLONG)ft.dwLowDateTime); -} - - -double -psutil_LargeIntegerToUnixTime(LARGE_INTEGER li) { - return _to_unix_time((ULONGLONG)li.HighPart, - (ULONGLONG)li.LowPart); -} -#endif // PSUTIL_WINDOWS - - -// Called on module import on all platforms. +// Called on module import. int -psutil_setup(void) { - if (getenv("PSUTIL_DEBUG") != NULL) - PSUTIL_DEBUG = 1; - -#ifdef PSUTIL_WINDOWS +psutil_setup_windows(void) { if (psutil_loadlibs() != 0) return 1; if (psutil_set_winver() != 0) return 1; GetSystemInfo(&PSUTIL_SYSTEM_INFO); InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); -#endif -#ifdef PSUTIL_OSX - mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); -#endif return 0; } diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h new file mode 100644 index 0000000000..0a5f46809d --- /dev/null +++ b/psutil/arch/windows/init.h @@ -0,0 +1,61 @@ +/* + * 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. + */ + +#include +#include + +#include "ntextapi.h" + + +extern int PSUTIL_WINVER; +extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; +extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; + +#define PSUTIL_WINDOWS_VISTA 60 +#define PSUTIL_WINDOWS_7 61 +#define PSUTIL_WINDOWS_8 62 +#define PSUTIL_WINDOWS_8_1 63 +#define PSUTIL_WINDOWS_10 100 +#define PSUTIL_WINDOWS_NEW MAXLONG + +#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) +#define MALLOC_ZERO(x) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (x)) +#define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) + +#define _NT_FACILITY_MASK 0xfff +#define _NT_FACILITY_SHIFT 16 +#define _NT_FACILITY(status) \ + ((((ULONG)(status)) >> _NT_FACILITY_SHIFT) & _NT_FACILITY_MASK) + +#define NT_NTWIN32(status) (_NT_FACILITY(status) == FACILITY_WIN32) +#define WIN32_FROM_NTSTATUS(status) (((ULONG)(status)) & 0xffff) + +#define LO_T 1e-7 +#define HI_T 429.4967296 + +#ifndef AF_INET6 + #define AF_INET6 23 +#endif + +#if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) + #if !defined(PyErr_SetFromWindowsErrWithFilename) + PyObject *PyErr_SetFromWindowsErrWithFilename( + int ierr, const char *filename + ); + #endif + #if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) + PyObject *PyErr_SetExcFromWindowsErrWithFilenameObject( + PyObject *type, int ierr, PyObject *filename + ); + #endif +#endif + +PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); +PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); +PVOID psutil_SetFromNTStatusErr(NTSTATUS status, const char *syscall); +double psutil_FiletimeToUnixTime(FILETIME ft); +double psutil_LargeIntegerToUnixTime(LARGE_INTEGER li); +int psutil_setup_windows(void); diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c index 24dc15ad0e..03b85777d0 100644 --- a/psutil/arch/windows/mem.c +++ b/psutil/arch/windows/mem.c @@ -9,7 +9,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 9a5634b069..b9ca07dc0f 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -12,7 +12,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" static PIP_ADAPTER_ADDRESSES diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 41fa9dda68..4040641471 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -23,7 +23,7 @@ // Link with Iphlpapi.lib #pragma comment(lib, "IPHLPAPI.lib") -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "proc.h" #include "proc_info.h" #include "proc_handles.h" diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 01ef6a425e..703a0ba298 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -22,7 +22,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "proc_utils.h" diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 9e0caf3442..defdf78f1f 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -10,7 +10,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "proc_info.h" #include "proc_utils.h" diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index 1ebb76c448..63e1950efe 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -10,7 +10,7 @@ #include #include // EnumProcesses -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "proc_utils.h" diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index 07d2399849..d466be3f55 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -10,7 +10,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" static BOOL diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 4931e9d66d..9c7f1bee85 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -8,7 +8,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "services.h" diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 0dc77f2d9d..403964ad08 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -11,7 +11,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" #include "proc_utils.h" diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index 8fa03999ed..597b78d0b0 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -17,7 +17,7 @@ history before the move: #include #include "ntextapi.h" -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" // The number of seconds passed since boot. This is a monotonic timer, diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 2cf7e8a59b..5db4f24883 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -10,7 +10,7 @@ #include #include -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" // We use an exponentially weighted moving average, just like Unix systems do diff --git a/setup.py b/setup.py index 54aa9824c0..e3628a4bb2 100755 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ macros.append(('PSUTIL_SIZEOF_PID_T', '8')) # long -sources = ['psutil/_psutil_common.c'] +sources = ['psutil/arch/all/init.c'] if POSIX: sources.append('psutil/_psutil_posix.c') From 316a5617e60d3845d5c44d85654a9bea4071aca2 Mon Sep 17 00:00:00 2001 From: Julien Stephan Date: Tue, 15 Apr 2025 11:49:10 +0200 Subject: [PATCH 1246/1714] scripts: install-sysdeps: add apt-get update before installing packages (#2559) Without running apt-get update, apt-get install fails on minimal Debian/Ubuntu images due to missing or outdated package metadata. This fix adds the necessary apt-get update step to ensure that required packages like `gcc` and `python3-dev` can be installed successfully. Other package manager cases (yum, apk, etc.) remain unchanged, as they do not require an explicit update step in this context. Signed-off-by: Julien Stephan --- scripts/internal/install-sysdeps.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 77554e0441..0256901acd 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -39,6 +39,7 @@ fi # Function to install system dependencies main() { if [ $HAS_APT ]; then + $SUDO apt-get update $SUDO apt-get install -y python3-dev gcc $SUDO apt-get install -y net-tools coreutils util-linux # for tests $SUDO apt-get install -y sudo # for test-sudo From 5a605b2e0bfaa724454327aa553fb2a30cc0a8a7 Mon Sep 17 00:00:00 2001 From: Julien Stephan Date: Wed, 16 Apr 2025 11:12:46 +0200 Subject: [PATCH 1247/1714] linux: skip test_emulate_use_cpuinfo on riscv64 (#2561) Similar to aarch64, riscv64 does not report MHz in /proc/cpuinfo. Skip test_emulate_use_cpuinfo on riscv64 to avoid test failures. Signed-off-by: Julien Stephan --- psutil/tests/__init__.py | 1 + psutil/tests/test_linux.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index afe45c65ef..60d1845a55 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -119,6 +119,7 @@ IS_64BIT = sys.maxsize > 2**32 # apparently they're the same AARCH64 = platform.machine() in {"aarch64", "arm64"} +RISCV64 = platform.machine() == "riscv64" @memoize diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 0d9caf2629..fbe7733e4c 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -32,6 +32,7 @@ from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_RLIMIT from psutil.tests import PYPY +from psutil.tests import RISCV64 from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase @@ -775,7 +776,8 @@ def path_exists_mock(path): @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") @pytest.mark.skipif( - AARCH64, reason="aarch64 does not report mhz in /proc/cpuinfo" + AARCH64 or RISCV64, + reason=f"{platform.machine()} does not report mhz in /proc/cpuinfo", ) def test_emulate_use_cpuinfo(self): # Emulate a case where /sys/devices/system/cpu/cpufreq* does not From d461f4c0f0aad1a039c7d8bb724a4c7288ef2f39 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Thu, 17 Apr 2025 19:12:13 -0400 Subject: [PATCH 1248/1714] Fix typo in docs (#2563) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 143a461cc6..485fafa1ff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2062,7 +2062,7 @@ Process class positive integer >= 0 indicating the exit code. If the process was terminated by a signal return the negated value of the signal which caused the termination (e.g. ``-SIGTERM``). - If PID is not a children of `os.getpid`_ (current process) just wait until + If PID is not a child of `os.getpid`_ (current process) just wait until the process disappears and return ``None``. If PID does not exist return ``None`` immediately. From f2e2d8139d503eccc40281e0554f949780ef6546 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 28 Apr 2025 11:36:12 +0200 Subject: [PATCH 1249/1714] fix warning re. license in setup.py metadata --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e3628a4bb2..e03a91eab8 100755 --- a/setup.py +++ b/setup.py @@ -530,7 +530,6 @@ def main(): 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows :: Windows 10', 'Operating System :: Microsoft :: Windows :: Windows 7', From ee04dee3d3bffb1bd3750b6805dea2d11cb65f9a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 28 Apr 2025 14:02:59 +0200 Subject: [PATCH 1250/1714] [SunOS] big refactoring (#2566) --- MANIFEST.in | 20 +- psutil/_psutil_sunos.c | 1638 +----------------- psutil/arch/sunos/cpu.c | 139 ++ psutil/arch/sunos/cpu.h | 11 + psutil/arch/sunos/disk.c | 121 ++ psutil/arch/sunos/disk.h | 10 + psutil/arch/{solaris => sunos}/environ.c | 6 +- psutil/arch/{solaris => sunos}/environ.h | 2 + psutil/arch/sunos/mem.c | 105 ++ psutil/arch/sunos/mem.h | 9 + psutil/arch/sunos/net.c | 559 ++++++ psutil/arch/sunos/net.h | 11 + psutil/arch/sunos/proc.c | 613 +++++++ psutil/arch/sunos/proc.h | 20 + psutil/arch/sunos/sys.c | 94 + psutil/arch/sunos/sys.h | 10 + psutil/arch/{solaris => sunos}/v10/ifaddrs.c | 0 psutil/arch/{solaris => sunos}/v10/ifaddrs.h | 0 setup.py | 12 +- 19 files changed, 1740 insertions(+), 1640 deletions(-) create mode 100644 psutil/arch/sunos/cpu.c create mode 100644 psutil/arch/sunos/cpu.h create mode 100644 psutil/arch/sunos/disk.c create mode 100644 psutil/arch/sunos/disk.h rename psutil/arch/{solaris => sunos}/environ.c (98%) rename psutil/arch/{solaris => sunos}/environ.h (95%) create mode 100644 psutil/arch/sunos/mem.c create mode 100644 psutil/arch/sunos/mem.h create mode 100644 psutil/arch/sunos/net.c create mode 100644 psutil/arch/sunos/net.h create mode 100644 psutil/arch/sunos/proc.c create mode 100644 psutil/arch/sunos/proc.h create mode 100644 psutil/arch/sunos/sys.c create mode 100644 psutil/arch/sunos/sys.h rename psutil/arch/{solaris => sunos}/v10/ifaddrs.c (100%) rename psutil/arch/{solaris => sunos}/v10/ifaddrs.h (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 36a947c75a..2d59303420 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -120,10 +120,22 @@ include psutil/arch/osx/sensors.c include psutil/arch/osx/sensors.h include psutil/arch/osx/sys.c include psutil/arch/osx/sys.h -include psutil/arch/solaris/environ.c -include psutil/arch/solaris/environ.h -include psutil/arch/solaris/v10/ifaddrs.c -include psutil/arch/solaris/v10/ifaddrs.h +include psutil/arch/sunos/cpu.c +include psutil/arch/sunos/cpu.h +include psutil/arch/sunos/disk.c +include psutil/arch/sunos/disk.h +include psutil/arch/sunos/environ.c +include psutil/arch/sunos/environ.h +include psutil/arch/sunos/mem.c +include psutil/arch/sunos/mem.h +include psutil/arch/sunos/net.c +include psutil/arch/sunos/net.h +include psutil/arch/sunos/proc.c +include psutil/arch/sunos/proc.h +include psutil/arch/sunos/sys.c +include psutil/arch/sunos/sys.h +include psutil/arch/sunos/v10/ifaddrs.c +include psutil/arch/sunos/v10/ifaddrs.h include psutil/arch/windows/cpu.c include psutil/arch/windows/cpu.h include psutil/arch/windows/disk.c diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 564adaafb7..2585a6b667 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -23,1619 +23,19 @@ # undef _LARGEFILE64_SOURCE #endif -#include -#include -#include -#include -#include -#include // for MNTTAB -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#ifndef NEW_MIB_COMPLIANT -/* - * Solaris introduced NEW_MIB_COMPLIANT macro with Update 4. - * See https://github.com/giampaolo/psutil/issues/421 - * Prior to Update 4, one has to include mib2 by hand. - */ -#include -#endif -#include -#include -#include // fabs() -#include #include "arch/all/init.h" -#include "_psutil_posix.h" - -#include "arch/solaris/environ.h" - -#define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) - - -/* - * Read a file content and fills a C structure with it. - */ -static int -psutil_file_to_struct(char *path, void *fstruct, size_t size) { - int fd; - ssize_t nbytes; - fd = open(path, O_RDONLY); - if (fd == -1) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); - return 0; - } - nbytes = read(fd, fstruct, size); - if (nbytes == -1) { - close(fd); - PyErr_SetFromErrno(PyExc_OSError); - return 0; - } - if (nbytes != (ssize_t) size) { - close(fd); - PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch"); - return 0; - } - close(fd); - return nbytes; -} - - -/* - * Return process ppid, rss, vms, ctime, nice, nthreads, status and tty - * as a Python tuple. - */ -static PyObject * -psutil_proc_basic_info(PyObject *self, PyObject *args) { - int pid; - char path[1000]; - psinfo_t info; - const char *procfs_path; - - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - return NULL; - - sprintf(path, "%s/%i/psinfo", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) - return NULL; - return Py_BuildValue( - "ikkdiiikiiii", - info.pr_ppid, // parent pid - info.pr_rssize, // rss - info.pr_size, // vms - PSUTIL_TV2DOUBLE(info.pr_start), // create time - info.pr_lwp.pr_nice, // nice - info.pr_nlwp, // no. of threads - info.pr_lwp.pr_state, // status code - info.pr_ttydev, // tty nr - (int)info.pr_uid, // real user id - (int)info.pr_euid, // effective user id - (int)info.pr_gid, // real group id - (int)info.pr_egid // effective group id - ); -} - -/* - * Join array of C strings to C string with delimiter dm. - * Omit empty records. - */ -static int -cstrings_array_to_string(char **joined, char ** array, size_t count, char dm) { - size_t i; - size_t total_length = 0; - size_t item_length = 0; - char *result = NULL; - char *last = NULL; - - if (!array || !joined) - return 0; - - for (i=0; i 0) { - py_args = PyUnicode_DecodeFSDefault(argv_plain); - free(argv_plain); - } else if (joined < 0) { - goto error; - } - - psutil_free_cstrings_array(argv, argc); - } - } - - /* If we can't read process memory or can't decode the result - * then return args from /proc. */ - if (!py_args) { - PyErr_Clear(); - py_args = PyUnicode_DecodeFSDefault(info.pr_psargs); - } - - /* Both methods has been failed. */ - if (!py_args) - goto error; - - py_retlist = Py_BuildValue("OO", py_name, py_args); - if (!py_retlist) - goto error; - - Py_DECREF(py_name); - Py_DECREF(py_args); - return py_retlist; - -error: - Py_XDECREF(py_name); - Py_XDECREF(py_args); - Py_XDECREF(py_retlist); - return NULL; -} - - -/* - * Return process environ block. - */ -static PyObject * -psutil_proc_environ(PyObject *self, PyObject *args) { - int pid; - char path[1000]; - psinfo_t info; - const char *procfs_path; - char **env = NULL; - ssize_t env_count = -1; - char *dm; - int i = 0; - PyObject *py_envname = NULL; - PyObject *py_envval = NULL; - PyObject *py_retdict = PyDict_New(); - - if (! py_retdict) - return PyErr_NoMemory(); - - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - return NULL; - - sprintf(path, "%s/%i/psinfo", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) - goto error; - - if (! info.pr_envp) { - AccessDenied("/proc/pid/psinfo struct not set"); - goto error; - } - - env = psutil_read_raw_env(info, procfs_path, &env_count); - if (! env && env_count != 0) - goto error; - - for (i=0; i= 0) - psutil_free_cstrings_array(env, env_count); - - Py_XDECREF(py_envname); - Py_XDECREF(py_envval); - Py_XDECREF(py_retdict); - return NULL; -} - - -/* - * Return process user and system CPU times as a Python tuple. - */ -static PyObject * -psutil_proc_cpu_times(PyObject *self, PyObject *args) { - int pid; - char path[1000]; - pstatus_t info; - const char *procfs_path; - - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - return NULL; - sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) - return NULL; - // results are more precise than os.times() - return Py_BuildValue( - "(dddd)", - PSUTIL_TV2DOUBLE(info.pr_utime), - PSUTIL_TV2DOUBLE(info.pr_stime), - PSUTIL_TV2DOUBLE(info.pr_cutime), - PSUTIL_TV2DOUBLE(info.pr_cstime) - ); -} - - -/* - * Return what CPU the process is running on. - */ -static PyObject * -psutil_proc_cpu_num(PyObject *self, PyObject *args) { - int fd = -1; - int pid; - char path[1000]; - struct prheader header; - struct lwpsinfo *lwp = NULL; - int nent; - int size; - int proc_num; - ssize_t nbytes; - const char *procfs_path; - - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - return NULL; - - sprintf(path, "%s/%i/lpsinfo", procfs_path, pid); - fd = open(path, O_RDONLY); - if (fd == -1) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); - return NULL; - } - - // read header - nbytes = pread(fd, &header, sizeof(header), 0); - if (nbytes == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - if (nbytes != sizeof(header)) { - PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch"); - goto error; - } - - // malloc - nent = header.pr_nent; - size = header.pr_entsize * nent; - lwp = malloc(size); - if (lwp == NULL) { - PyErr_NoMemory(); - goto error; - } - - // read the rest - nbytes = pread(fd, lwp, size, sizeof(header)); - if (nbytes == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - if (nbytes != size) { - PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch"); - goto error; - } - - // done - proc_num = lwp->pr_onpro; - close(fd); - free(lwp); - return Py_BuildValue("i", proc_num); - -error: - if (fd != -1) - close(fd); - free(lwp); - return NULL; -} - - -/* - * Return process uids/gids as a Python tuple. - */ -static PyObject * -psutil_proc_cred(PyObject *self, PyObject *args) { - int pid; - char path[1000]; - prcred_t info; - const char *procfs_path; - - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - return NULL; - sprintf(path, "%s/%i/cred", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) - return NULL; - return Py_BuildValue("iiiiii", - info.pr_ruid, info.pr_euid, info.pr_suid, - info.pr_rgid, info.pr_egid, info.pr_sgid); -} - - -/* - * Return process voluntary and involuntary context switches as a Python tuple. - */ -static PyObject * -psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { - int pid; - char path[1000]; - prusage_t info; - const char *procfs_path; - - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - return NULL; - sprintf(path, "%s/%i/usage", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) - return NULL; - return Py_BuildValue("kk", info.pr_vctx, info.pr_ictx); -} - - -/* - * Process IO counters. - * - * Commented out and left here as a reminder. Apparently we cannot - * retrieve process IO stats because: - * - 'pr_ioch' is a sum of chars read and written, with no distinction - * - 'pr_inblk' and 'pr_oublk', which should be the number of bytes - * read and written, hardly increase and according to: - * http://www.brendangregg.com/Solaris/paper_diskubyp1.pdf - * ...they should be meaningless anyway. - * -static PyObject* -proc_io_counters(PyObject* self, PyObject* args) { - int pid; - char path[1000]; - prusage_t info; - const char *procfs_path; - - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - return NULL; - sprintf(path, "%s/%i/usage", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) - return NULL; - - // On Solaris we only have 'pr_ioch' which accounts for bytes read - // *and* written. - // 'pr_inblk' and 'pr_oublk' should be expressed in blocks of - // 8KB according to: - // http://www.brendangregg.com/Solaris/paper_diskubyp1.pdf (pag. 8) - return Py_BuildValue("kkkk", - info.pr_ioch, - info.pr_ioch, - info.pr_inblk, - info.pr_oublk); -} -*/ - - -/* - * Return information about a given process thread. - */ -static PyObject * -psutil_proc_query_thread(PyObject *self, PyObject *args) { - int pid, tid; - char path[1000]; - lwpstatus_t info; - const char *procfs_path; - - if (! PyArg_ParseTuple(args, "iis", &pid, &tid, &procfs_path)) - return NULL; - sprintf(path, "%s/%i/lwp/%i/lwpstatus", procfs_path, pid, tid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) - return NULL; - return Py_BuildValue("dd", - PSUTIL_TV2DOUBLE(info.pr_utime), - PSUTIL_TV2DOUBLE(info.pr_stime)); -} - - -/* - * Return information about system virtual memory. - */ -static PyObject * -psutil_swap_mem(PyObject *self, PyObject *args) { -// XXX (arghhh!) -// total/free swap mem: commented out as for some reason I can't -// manage to get the same results shown by "swap -l", despite the -// code below is exactly the same as: -// http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/ -// cmd/swap/swap.c -// We're going to parse "swap -l" output from Python (sigh!) - -/* - struct swaptable *st; - struct swapent *swapent; - int i; - struct stat64 statbuf; - char *path; - char fullpath[MAXPATHLEN+1]; - int num; - - if ((num = swapctl(SC_GETNSWP, NULL)) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - if (num == 0) { - PyErr_SetString(PyExc_RuntimeError, "no swap devices configured"); - return NULL; - } - if ((st = malloc(num * sizeof(swapent_t) + sizeof (int))) == NULL) { - PyErr_SetString(PyExc_RuntimeError, "malloc failed"); - return NULL; - } - if ((path = malloc(num * MAXPATHLEN)) == NULL) { - PyErr_SetString(PyExc_RuntimeError, "malloc failed"); - return NULL; - } - swapent = st->swt_ent; - for (i = 0; i < num; i++, swapent++) { - swapent->ste_path = path; - path += MAXPATHLEN; - } - st->swt_n = num; - if ((num = swapctl(SC_LIST, st)) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - swapent = st->swt_ent; - long t = 0, f = 0; - for (i = 0; i < num; i++, swapent++) { - int diskblks_per_page =(int)(sysconf(_SC_PAGESIZE) >> DEV_BSHIFT); - t += (long)swapent->ste_pages; - f += (long)swapent->ste_free; - } - - free(st); - return Py_BuildValue("(kk)", t, f); -*/ - - kstat_ctl_t *kc; - kstat_t *k; - cpu_stat_t *cpu; - int cpu_count = 0; - int flag = 0; - uint_t sin = 0; - uint_t sout = 0; - - kc = kstat_open(); - if (kc == NULL) - return PyErr_SetFromErrno(PyExc_OSError);; - - k = kc->kc_chain; - while (k != NULL) { - if ((strncmp(k->ks_name, "cpu_stat", 8) == 0) && \ - (kstat_read(kc, k, NULL) != -1) ) - { - flag = 1; - cpu = (cpu_stat_t *) k->ks_data; - sin += cpu->cpu_vminfo.pgswapin; // num pages swapped in - sout += cpu->cpu_vminfo.pgswapout; // num pages swapped out - } - cpu_count += 1; - k = k->ks_next; - } - kstat_close(kc); - if (!flag) { - PyErr_SetString(PyExc_RuntimeError, "no swap device was found"); - return NULL; - } - return Py_BuildValue("(II)", sin, sout); -} - - -/* - * Return users currently connected on the system. - */ -static PyObject * -psutil_users(PyObject *self, PyObject *args) { - struct utmpx *ut; - PyObject *py_tuple = NULL; - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_user_proc = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - setutxent(); - while (NULL != (ut = getutxent())) { - if (ut->ut_type == USER_PROCESS) - py_user_proc = Py_True; - else - py_user_proc = Py_False; - py_username = PyUnicode_DecodeFSDefault(ut->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); - if (! py_hostname) - goto error; - py_tuple = Py_BuildValue( - "(OOOdOi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut->ut_tv.tv_sec, // tstamp - py_user_proc, // (bool) user process - ut->ut_pid // process id - ); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - endutxent(); - - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - endutxent(); - return NULL; -} - - -/* - * Return disk mounted partitions as a list of tuples including device, - * mount point and filesystem type. - */ -static PyObject * -psutil_disk_partitions(PyObject *self, PyObject *args) { - FILE *file; - struct mnttab mt; - PyObject *py_dev = NULL; - PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - file = fopen(MNTTAB, "rb"); - if (file == NULL) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - while (getmntent(file, &mt) == 0) { - py_dev = PyUnicode_DecodeFSDefault(mt.mnt_special); - if (! py_dev) - goto error; - py_mountp = PyUnicode_DecodeFSDefault(mt.mnt_mountp); - if (! py_mountp) - goto error; - py_tuple = Py_BuildValue( - "(OOss)", - py_dev, // device - py_mountp, // mount point - mt.mnt_fstype, // fs type - mt.mnt_mntopts); // options - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_dev); - Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); - } - fclose(file); - return py_retlist; - -error: - Py_XDECREF(py_dev); - Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (file != NULL) - fclose(file); - return NULL; -} - - -/* - * Return system-wide CPU times. - */ -static PyObject * -psutil_per_cpu_times(PyObject *self, PyObject *args) { - kstat_ctl_t *kc; - kstat_t *ksp; - cpu_stat_t cs; - PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; - - if (py_retlist == NULL) - return NULL; - - kc = kstat_open(); - if (kc == NULL) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (ksp = kc->kc_chain; ksp != NULL; ksp = ksp->ks_next) { - if (strcmp(ksp->ks_module, "cpu_stat") == 0) { - if (kstat_read(kc, ksp, &cs) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - py_cputime = Py_BuildValue("ffff", - (float)cs.cpu_sysinfo.cpu[CPU_USER], - (float)cs.cpu_sysinfo.cpu[CPU_KERNEL], - (float)cs.cpu_sysinfo.cpu[CPU_IDLE], - (float)cs.cpu_sysinfo.cpu[CPU_WAIT]); - if (py_cputime == NULL) - goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_CLEAR(py_cputime); - } - } - - kstat_close(kc); - return py_retlist; - -error: - Py_XDECREF(py_cputime); - Py_DECREF(py_retlist); - if (kc != NULL) - kstat_close(kc); - return NULL; -} - - -/* - * Return disk IO statistics. - */ -static PyObject * -psutil_disk_io_counters(PyObject *self, PyObject *args) { - kstat_ctl_t *kc; - kstat_t *ksp; - kstat_io_t kio; - PyObject *py_retdict = PyDict_New(); - PyObject *py_disk_info = NULL; - - if (py_retdict == NULL) - return NULL; - kc = kstat_open(); - if (kc == NULL) { - PyErr_SetFromErrno(PyExc_OSError);; - goto error; - } - ksp = kc->kc_chain; - while (ksp != NULL) { - if (ksp->ks_type == KSTAT_TYPE_IO) { - if (strcmp(ksp->ks_class, "disk") == 0) { - if (kstat_read(kc, ksp, &kio) == -1) { - kstat_close(kc); - return PyErr_SetFromErrno(PyExc_OSError);; - } - py_disk_info = Py_BuildValue( - "(IIKKLL)", - kio.reads, - kio.writes, - kio.nread, - kio.nwritten, - kio.rtime / 1000 / 1000, // from nano to milli secs - kio.wtime / 1000 / 1000 // from nano to milli secs - ); - if (!py_disk_info) - goto error; - if (PyDict_SetItemString(py_retdict, ksp->ks_name, - py_disk_info)) - goto error; - Py_CLEAR(py_disk_info); - } - } - ksp = ksp->ks_next; - } - kstat_close(kc); - - return py_retdict; - -error: - Py_XDECREF(py_disk_info); - Py_DECREF(py_retdict); - if (kc != NULL) - kstat_close(kc); - return NULL; -} - - -/* - * Return process memory mappings. - */ -static PyObject * -psutil_proc_memory_maps(PyObject *self, PyObject *args) { - int pid; - int fd = -1; - char path[1000]; - char perms[10]; - const char *name; - struct stat st; - pstatus_t status; - - prxmap_t *xmap = NULL, *p; - off_t size; - size_t nread; - int nmap; - uintptr_t pr_addr_sz; - uintptr_t stk_base_sz, brk_base_sz; - const char *procfs_path; - - PyObject *py_tuple = NULL; - PyObject *py_path = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) - goto error; - - sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&status, sizeof(status))) - goto error; - - sprintf(path, "%s/%i/xmap", procfs_path, pid); - if (stat(path, &st) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - size = st.st_size; - - fd = open(path, O_RDONLY); - if (fd == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - xmap = (prxmap_t *)malloc(size); - if (xmap == NULL) { - PyErr_NoMemory(); - goto error; - } - - nread = pread(fd, xmap, size, 0); - nmap = nread / sizeof(prxmap_t); - p = xmap; - - while (nmap) { - nmap -= 1; - if (p == NULL) { - p += 1; - continue; - } - - perms[0] = '\0'; - pr_addr_sz = p->pr_vaddr + p->pr_size; - - // perms - sprintf(perms, "%c%c%c%c", p->pr_mflags & MA_READ ? 'r' : '-', - p->pr_mflags & MA_WRITE ? 'w' : '-', - p->pr_mflags & MA_EXEC ? 'x' : '-', - p->pr_mflags & MA_SHARED ? 's' : '-'); - - // name - if (strlen(p->pr_mapname) > 0) { - name = p->pr_mapname; - } - else { - if ((p->pr_mflags & MA_ISM) || (p->pr_mflags & MA_SHM)) { - name = "[shmid]"; - } - else { - stk_base_sz = status.pr_stkbase + status.pr_stksize; - brk_base_sz = status.pr_brkbase + status.pr_brksize; - - if ((pr_addr_sz > status.pr_stkbase) && - (p->pr_vaddr < stk_base_sz)) { - name = "[stack]"; - } - else if ((p->pr_mflags & MA_ANON) && \ - (pr_addr_sz > status.pr_brkbase) && \ - (p->pr_vaddr < brk_base_sz)) { - name = "[heap]"; - } - else { - name = "[anon]"; - } - } - } - - py_path = PyUnicode_DecodeFSDefault(name); - if (! py_path) - goto error; - py_tuple = Py_BuildValue( - "kksOkkk", - (unsigned long)p->pr_vaddr, - (unsigned long)pr_addr_sz, - perms, - py_path, - (unsigned long)p->pr_rss * p->pr_pagesize, - (unsigned long)p->pr_anon * p->pr_pagesize, - (unsigned long)p->pr_locked * p->pr_pagesize); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_path); - Py_CLEAR(py_tuple); - - // increment pointer - p += 1; - } - - close(fd); - free(xmap); - return py_retlist; - -error: - if (fd != -1) - close(fd); - Py_XDECREF(py_tuple); - Py_XDECREF(py_path); - Py_DECREF(py_retlist); - if (xmap != NULL) - free(xmap); - return NULL; -} - - -/* - * Return a list of tuples for network I/O statistics. - */ -static PyObject * -psutil_net_io_counters(PyObject *self, PyObject *args) { - kstat_ctl_t *kc = NULL; - kstat_t *ksp; - kstat_named_t *rbytes, *wbytes, *rpkts, *wpkts, *ierrs, *oerrs; - int ret; - int sock = -1; - struct lifreq ifr; - - PyObject *py_retdict = PyDict_New(); - PyObject *py_ifc_info = NULL; - - if (py_retdict == NULL) - return NULL; - kc = kstat_open(); - if (kc == NULL) - goto error; - - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - ksp = kc->kc_chain; - while (ksp != NULL) { - if (ksp->ks_type != KSTAT_TYPE_NAMED) - goto next; - if (strcmp(ksp->ks_class, "net") != 0) - goto next; - // skip 'lo' (localhost) because it doesn't have the statistics we need - // and it makes kstat_data_lookup() fail - if (strcmp(ksp->ks_module, "lo") == 0) - goto next; - - // check if this is a network interface by sending a ioctl - PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); - ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); - if (ret == -1) - goto next; - - if (kstat_read(kc, ksp, NULL) == -1) { - errno = 0; - goto next; - } - - rbytes = (kstat_named_t *)kstat_data_lookup(ksp, "rbytes"); - wbytes = (kstat_named_t *)kstat_data_lookup(ksp, "obytes"); - rpkts = (kstat_named_t *)kstat_data_lookup(ksp, "ipackets"); - wpkts = (kstat_named_t *)kstat_data_lookup(ksp, "opackets"); - ierrs = (kstat_named_t *)kstat_data_lookup(ksp, "ierrors"); - oerrs = (kstat_named_t *)kstat_data_lookup(ksp, "oerrors"); - - if ((rbytes == NULL) || (wbytes == NULL) || (rpkts == NULL) || - (wpkts == NULL) || (ierrs == NULL) || (oerrs == NULL)) - { - PyErr_SetString(PyExc_RuntimeError, "kstat_data_lookup() failed"); - goto error; - } - - if (rbytes->data_type == KSTAT_DATA_UINT64) - { - py_ifc_info = Py_BuildValue("(KKKKIIii)", - wbytes->value.ui64, - rbytes->value.ui64, - wpkts->value.ui64, - rpkts->value.ui64, - ierrs->value.ui32, - oerrs->value.ui32, - 0, // dropin not supported - 0 // dropout not supported - ); - } - else - { - py_ifc_info = Py_BuildValue("(IIIIIIii)", - wbytes->value.ui32, - rbytes->value.ui32, - wpkts->value.ui32, - rpkts->value.ui32, - ierrs->value.ui32, - oerrs->value.ui32, - 0, // dropin not supported - 0 // dropout not supported - ); - } - if (!py_ifc_info) - goto error; - if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_ifc_info)) - goto error; - Py_CLEAR(py_ifc_info); - goto next; - -next: - ksp = ksp->ks_next; - } - - kstat_close(kc); - close(sock); - return py_retdict; - -error: - Py_XDECREF(py_ifc_info); - Py_DECREF(py_retdict); - if (kc != NULL) - kstat_close(kc); - if (sock != -1) { - close(sock); - } - return NULL; -} - - -/* - * Return TCP and UDP connections opened by process. - * UNIX sockets are excluded. - * - * Thanks to: - * https://github.com/DavidGriffith/finx/blob/master/ - * nxsensor-3.5.0-1/src/sysdeps/solaris.c - * ...and: - * https://hg.java.net/hg/solaris~on-src/file/tip/usr/src/cmd/ - * cmd-inet/usr.bin/netstat/netstat.c - */ -static PyObject * -psutil_net_connections(PyObject *self, PyObject *args) { - long pid; - int sd = 0; - mib2_tcpConnEntry_t tp; - mib2_udpEntry_t ude; -#if defined(AF_INET6) - mib2_tcp6ConnEntry_t tp6; - mib2_udp6Entry_t ude6; -#endif - char buf[512]; - int i, flags, getcode, num_ent, state, ret; - char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; - int lport, rport; - int processed_pid; - int databuf_init = 0; - struct strbuf ctlbuf, databuf; - struct T_optmgmt_req tor = {0}; - struct T_optmgmt_ack toa = {0}; - struct T_error_ack tea = {0}; - struct opthdr mibhdr = {0}; - - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_laddr = NULL; - PyObject *py_raddr = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - - sd = open("/dev/arp", O_RDWR); - if (sd == -1) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, "/dev/arp"); - goto error; - } +#include "arch/sunos/cpu.h" +#include "arch/sunos/disk.h" +#include "arch/sunos/environ.h" +#include "arch/sunos/mem.h" +#include "arch/sunos/net.h" +#include "arch/sunos/proc.h" +#include "arch/sunos/sys.h" - ret = ioctl(sd, I_PUSH, "tcp"); - if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - ret = ioctl(sd, I_PUSH, "udp"); - if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - // - // OK, this mess is basically copied and pasted from nxsensor project - // which copied and pasted it from netstat source code, mibget() - // function. Also see: - // http://stackoverflow.com/questions/8723598/ - tor.PRIM_type = T_SVR4_OPTMGMT_REQ; - tor.OPT_offset = sizeof (struct T_optmgmt_req); - tor.OPT_length = sizeof (struct opthdr); - tor.MGMT_flags = T_CURRENT; - mibhdr.level = MIB2_IP; - mibhdr.name = 0; -#ifdef NEW_MIB_COMPLIANT - mibhdr.len = 1; -#else - mibhdr.len = 0; -#endif - memcpy(buf, &tor, sizeof tor); - memcpy(buf + tor.OPT_offset, &mibhdr, sizeof mibhdr); - - ctlbuf.buf = buf; - ctlbuf.len = tor.OPT_offset + tor.OPT_length; - flags = 0; // request to be sent in non-priority - - if (putmsg(sd, &ctlbuf, (struct strbuf *)0, flags) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - ctlbuf.maxlen = sizeof (buf); - for (;;) { - flags = 0; - getcode = getmsg(sd, &ctlbuf, (struct strbuf *)0, &flags); - memcpy(&toa, buf, sizeof toa); - memcpy(&tea, buf, sizeof tea); - - if (getcode != MOREDATA || - ctlbuf.len < (int)sizeof (struct T_optmgmt_ack) || - toa.PRIM_type != T_OPTMGMT_ACK || - toa.MGMT_flags != T_SUCCESS) - { - break; - } - if (ctlbuf.len >= (int)sizeof (struct T_error_ack) && - tea.PRIM_type == T_ERROR_ACK) - { - PyErr_SetString(PyExc_RuntimeError, "ERROR_ACK"); - goto error; - } - if (getcode == 0 && - ctlbuf.len >= (int)sizeof (struct T_optmgmt_ack) && - toa.PRIM_type == T_OPTMGMT_ACK && - toa.MGMT_flags == T_SUCCESS) - { - PyErr_SetString(PyExc_RuntimeError, "ERROR_T_OPTMGMT_ACK"); - goto error; - } - - memset(&mibhdr, 0x0, sizeof(mibhdr)); - memcpy(&mibhdr, buf + toa.OPT_offset, toa.OPT_length); - - databuf.maxlen = mibhdr.len; - databuf.len = 0; - databuf.buf = (char *)malloc((int)mibhdr.len); - if (!databuf.buf) { - PyErr_NoMemory(); - goto error; - } - databuf_init = 1; - - flags = 0; - getcode = getmsg(sd, (struct strbuf *)0, &databuf, &flags); - if (getcode < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - // TCPv4 - if (mibhdr.level == MIB2_TCP && mibhdr.name == MIB2_TCP_13) { - num_ent = mibhdr.len / sizeof(mib2_tcpConnEntry_t); - for (i = 0; i < num_ent; i++) { - memcpy(&tp, databuf.buf + i * sizeof tp, sizeof tp); -#ifdef NEW_MIB_COMPLIANT - processed_pid = tp.tcpConnCreationProcess; -#else - processed_pid = 0; -#endif - if (pid != -1 && processed_pid != pid) - continue; - // construct local/remote addresses - inet_ntop(AF_INET, &tp.tcpConnLocalAddress, lip, sizeof(lip)); - inet_ntop(AF_INET, &tp.tcpConnRemAddress, rip, sizeof(rip)); - lport = tp.tcpConnLocalPort; - rport = tp.tcpConnRemPort; - - // construct python tuple/list - py_laddr = Py_BuildValue("(si)", lip, lport); - if (!py_laddr) - goto error; - if (rport != 0) - py_raddr = Py_BuildValue("(si)", rip, rport); - else { - py_raddr = Py_BuildValue("()"); - } - if (!py_raddr) - goto error; - state = tp.tcpConnEntryInfo.ce_state; - - // add item - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET, SOCK_STREAM, - py_laddr, py_raddr, state, - processed_pid); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - } - } -#if defined(AF_INET6) - // TCPv6 - else if (mibhdr.level == MIB2_TCP6 && mibhdr.name == MIB2_TCP6_CONN) - { - num_ent = mibhdr.len / sizeof(mib2_tcp6ConnEntry_t); - - for (i = 0; i < num_ent; i++) { - memcpy(&tp6, databuf.buf + i * sizeof tp6, sizeof tp6); -#ifdef NEW_MIB_COMPLIANT - processed_pid = tp6.tcp6ConnCreationProcess; -#else - processed_pid = 0; -#endif - if (pid != -1 && processed_pid != pid) - continue; - // construct local/remote addresses - inet_ntop(AF_INET6, &tp6.tcp6ConnLocalAddress, lip, sizeof(lip)); - inet_ntop(AF_INET6, &tp6.tcp6ConnRemAddress, rip, sizeof(rip)); - lport = tp6.tcp6ConnLocalPort; - rport = tp6.tcp6ConnRemPort; - - // construct python tuple/list - py_laddr = Py_BuildValue("(si)", lip, lport); - if (!py_laddr) - goto error; - if (rport != 0) - py_raddr = Py_BuildValue("(si)", rip, rport); - else - py_raddr = Py_BuildValue("()"); - if (!py_raddr) - goto error; - state = tp6.tcp6ConnEntryInfo.ce_state; - - // add item - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET6, SOCK_STREAM, - py_laddr, py_raddr, state, processed_pid); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - } - } -#endif - // UDPv4 - else if (mibhdr.level == MIB2_UDP && mibhdr.name == MIB2_UDP_ENTRY) { - num_ent = mibhdr.len / sizeof(mib2_udpEntry_t); - assert(num_ent * sizeof(mib2_udpEntry_t) == mibhdr.len); - for (i = 0; i < num_ent; i++) { - memcpy(&ude, databuf.buf + i * sizeof ude, sizeof ude); -#ifdef NEW_MIB_COMPLIANT - processed_pid = ude.udpCreationProcess; -#else - processed_pid = 0; -#endif - if (pid != -1 && processed_pid != pid) - continue; - // XXX Very ugly hack! It seems we get here only the first - // time we bump into a UDPv4 socket. PID is a very high - // number (clearly impossible) and the address does not - // belong to any valid interface. Not sure what else - // to do other than skipping. - if (processed_pid > 131072) - continue; - inet_ntop(AF_INET, &ude.udpLocalAddress, lip, sizeof(lip)); - lport = ude.udpLocalPort; - py_laddr = Py_BuildValue("(si)", lip, lport); - if (!py_laddr) - goto error; - py_raddr = Py_BuildValue("()"); - if (!py_raddr) - goto error; - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET, SOCK_DGRAM, - py_laddr, py_raddr, PSUTIL_CONN_NONE, - processed_pid); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - } - } -#if defined(AF_INET6) - // UDPv6 - else if (mibhdr.level == MIB2_UDP6 || - mibhdr.level == MIB2_UDP6_ENTRY) - { - num_ent = mibhdr.len / sizeof(mib2_udp6Entry_t); - for (i = 0; i < num_ent; i++) { - memcpy(&ude6, databuf.buf + i * sizeof ude6, sizeof ude6); -#ifdef NEW_MIB_COMPLIANT - processed_pid = ude6.udp6CreationProcess; -#else - processed_pid = 0; -#endif - if (pid != -1 && processed_pid != pid) - continue; - inet_ntop(AF_INET6, &ude6.udp6LocalAddress, lip, sizeof(lip)); - lport = ude6.udp6LocalPort; - py_laddr = Py_BuildValue("(si)", lip, lport); - if (!py_laddr) - goto error; - py_raddr = Py_BuildValue("()"); - if (!py_raddr) - goto error; - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET6, SOCK_DGRAM, - py_laddr, py_raddr, PSUTIL_CONN_NONE, - processed_pid); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - } - } -#endif - free(databuf.buf); - } - - close(sd); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_XDECREF(py_laddr); - Py_XDECREF(py_raddr); - Py_DECREF(py_retlist); - if (databuf_init == 1) - free(databuf.buf); - if (sd != 0) - close(sd); - return NULL; -} - - -static PyObject * -psutil_boot_time(PyObject *self, PyObject *args) { - float boot_time = 0.0; - struct utmpx *ut; - - setutxent(); - while (NULL != (ut = getutxent())) { - if (ut->ut_type == BOOT_TIME) { - boot_time = (float)ut->ut_tv.tv_sec; - break; - } - } - endutxent(); - if (fabs(boot_time) < 0.000001) { - /* could not find BOOT_TIME in getutxent loop */ - PyErr_SetString(PyExc_RuntimeError, "can't determine boot time"); - return NULL; - } - return Py_BuildValue("f", boot_time); -} - - -/* - * Return the number of CPU cores on the system. - */ -static PyObject * -psutil_cpu_count_cores(PyObject *self, PyObject *args) { - kstat_ctl_t *kc; - kstat_t *ksp; - int ncpus = 0; - - kc = kstat_open(); - if (kc == NULL) - goto error; - ksp = kstat_lookup(kc, "cpu_info", -1, NULL); - if (ksp == NULL) - goto error; - - for (ksp = kc->kc_chain; ksp; ksp = ksp->ks_next) { - if (strcmp(ksp->ks_module, "cpu_info") != 0) - continue; - if (kstat_read(kc, ksp, NULL) == -1) - goto error; - ncpus += 1; - } - - kstat_close(kc); - if (ncpus > 0) - return Py_BuildValue("i", ncpus); - else - goto error; - -error: - // mimic os.cpu_count() - if (kc != NULL) - kstat_close(kc); - Py_RETURN_NONE; -} - - -/* - * Return stats about a particular network - * interface. References: - * https://github.com/dpaleino/wicd/blob/master/wicd/backends/be-ioctl.py - * http://www.i-scream.org/libstatgrab/ - */ -static PyObject* -psutil_net_if_stats(PyObject* self, PyObject* args) { - kstat_ctl_t *kc = NULL; - kstat_t *ksp; - kstat_named_t *knp; - int ret; - int sock = -1; - int duplex; - int speed; - - PyObject *py_retdict = PyDict_New(); - PyObject *py_ifc_info = NULL; - PyObject *py_is_up = NULL; - - if (py_retdict == NULL) - return NULL; - kc = kstat_open(); - if (kc == NULL) - goto error; - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (ksp = kc->kc_chain; ksp; ksp = ksp->ks_next) { - if (strcmp(ksp->ks_class, "net") == 0) { - struct lifreq ifr; - - kstat_read(kc, ksp, NULL); - if (ksp->ks_type != KSTAT_TYPE_NAMED) - continue; - if (strcmp(ksp->ks_class, "net") != 0) - continue; - - PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); - ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); - if (ret == -1) - continue; // not a network interface - - // is up? - if ((ifr.lifr_flags & IFF_UP) != 0) { - if ((knp = kstat_data_lookup(ksp, "link_up")) != NULL) { - if (knp->value.ui32 != 0u) - py_is_up = Py_True; - else - py_is_up = Py_False; - } - else { - py_is_up = Py_True; - } - } - else { - py_is_up = Py_False; - } - Py_INCREF(py_is_up); - - // duplex - duplex = 0; // unknown - if ((knp = kstat_data_lookup(ksp, "link_duplex")) != NULL) { - if (knp->value.ui32 == 1) - duplex = 1; // half - else if (knp->value.ui32 == 2) - duplex = 2; // full - } - - // speed - if ((knp = kstat_data_lookup(ksp, "ifspeed")) != NULL) - // expressed in bits per sec, we want mega bits per sec - speed = (int)knp->value.ui64 / 1000000; - else - speed = 0; - - // mtu - ret = ioctl(sock, SIOCGLIFMTU, &ifr); - if (ret == -1) - goto error; - - py_ifc_info = Py_BuildValue("(Oiii)", py_is_up, duplex, speed, - ifr.lifr_mtu); - if (!py_ifc_info) - goto error; - if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_ifc_info)) - goto error; - Py_CLEAR(py_ifc_info); - } - } - - close(sock); - kstat_close(kc); - return py_retdict; - -error: - Py_XDECREF(py_is_up); - Py_XDECREF(py_ifc_info); - Py_DECREF(py_retdict); - if (sock != -1) - close(sock); - if (kc != NULL) - kstat_close(kc); - PyErr_SetFromErrno(PyExc_OSError); - return NULL; -} - - -/* - * Return CPU statistics. - */ -static PyObject * -psutil_cpu_stats(PyObject *self, PyObject *args) { - kstat_ctl_t *kc; - kstat_t *ksp; - cpu_stat_t cs; - unsigned int ctx_switches = 0; - unsigned int interrupts = 0; - unsigned int traps = 0; - unsigned int syscalls = 0; - - kc = kstat_open(); - if (kc == NULL) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (ksp = kc->kc_chain; ksp != NULL; ksp = ksp->ks_next) { - if (strcmp(ksp->ks_module, "cpu_stat") == 0) { - if (kstat_read(kc, ksp, &cs) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - // voluntary + involuntary - ctx_switches += cs.cpu_sysinfo.pswitch + cs.cpu_sysinfo.inv_swtch; - interrupts += cs.cpu_sysinfo.intr; - traps += cs.cpu_sysinfo.trap; - syscalls += cs.cpu_sysinfo.syscall; - } - } - - kstat_close(kc); - return Py_BuildValue( - "IIII", ctx_switches, interrupts, syscalls, traps); - -error: - if (kc != NULL) - kstat_close(kc); - return NULL; -} - - -/* - * define the psutil C module methods and initialize the module. - */ -static PyMethodDef -PsutilMethods[] = { +static PyMethodDef mod_methods[] = { // --- process-related functions {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS}, {"proc_cpu_num", psutil_proc_cpu_num, METH_VARARGS}, @@ -1673,32 +73,20 @@ struct module_state { }; -static int -psutil_sunos_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - -static int -psutil_sunos_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "psutil_sunos", NULL, - sizeof(struct module_state), - PsutilMethods, + -1, + mod_methods, + NULL, + NULL, NULL, - psutil_sunos_traverse, - psutil_sunos_clear, NULL }; -PyMODINIT_FUNC +PyObject * PyInit__psutil_sunos(void) { PyObject *mod = PyModule_Create(&moduledef); if (mod == NULL) diff --git a/psutil/arch/sunos/cpu.c b/psutil/arch/sunos/cpu.c new file mode 100644 index 0000000000..03837e54ea --- /dev/null +++ b/psutil/arch/sunos/cpu.c @@ -0,0 +1,139 @@ +/* + * 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. + */ + +#include + +#include +#include + +#include "../../arch/all/init.h" + + +// System-wide CPU times. +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + kstat_ctl_t *kc; + kstat_t *ksp; + cpu_stat_t cs; + PyObject *py_retlist = PyList_New(0); + PyObject *py_cputime = NULL; + + if (py_retlist == NULL) + return NULL; + + kc = kstat_open(); + if (kc == NULL) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (ksp = kc->kc_chain; ksp != NULL; ksp = ksp->ks_next) { + if (strcmp(ksp->ks_module, "cpu_stat") == 0) { + if (kstat_read(kc, ksp, &cs) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + py_cputime = Py_BuildValue("ffff", + (float)cs.cpu_sysinfo.cpu[CPU_USER], + (float)cs.cpu_sysinfo.cpu[CPU_KERNEL], + (float)cs.cpu_sysinfo.cpu[CPU_IDLE], + (float)cs.cpu_sysinfo.cpu[CPU_WAIT]); + if (py_cputime == NULL) + goto error; + if (PyList_Append(py_retlist, py_cputime)) + goto error; + Py_CLEAR(py_cputime); + } + } + + kstat_close(kc); + return py_retlist; + +error: + Py_XDECREF(py_cputime); + Py_DECREF(py_retlist); + if (kc != NULL) + kstat_close(kc); + return NULL; +} + + +// Return the number of CPU cores on the system. +PyObject * +psutil_cpu_count_cores(PyObject *self, PyObject *args) { + kstat_ctl_t *kc; + kstat_t *ksp; + int ncpus = 0; + + kc = kstat_open(); + if (kc == NULL) + goto error; + ksp = kstat_lookup(kc, "cpu_info", -1, NULL); + if (ksp == NULL) + goto error; + + for (ksp = kc->kc_chain; ksp; ksp = ksp->ks_next) { + if (strcmp(ksp->ks_module, "cpu_info") != 0) + continue; + if (kstat_read(kc, ksp, NULL) == -1) + goto error; + ncpus += 1; + } + + kstat_close(kc); + if (ncpus > 0) + return Py_BuildValue("i", ncpus); + else + goto error; + +error: + // mimic os.cpu_count() + if (kc != NULL) + kstat_close(kc); + Py_RETURN_NONE; +} + + +// Return CPU statistics. +PyObject * +psutil_cpu_stats(PyObject *self, PyObject *args) { + kstat_ctl_t *kc; + kstat_t *ksp; + cpu_stat_t cs; + unsigned int ctx_switches = 0; + unsigned int interrupts = 0; + unsigned int traps = 0; + unsigned int syscalls = 0; + + kc = kstat_open(); + if (kc == NULL) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (ksp = kc->kc_chain; ksp != NULL; ksp = ksp->ks_next) { + if (strcmp(ksp->ks_module, "cpu_stat") == 0) { + if (kstat_read(kc, ksp, &cs) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + // voluntary + involuntary + ctx_switches += cs.cpu_sysinfo.pswitch + cs.cpu_sysinfo.inv_swtch; + interrupts += cs.cpu_sysinfo.intr; + traps += cs.cpu_sysinfo.trap; + syscalls += cs.cpu_sysinfo.syscall; + } + } + + kstat_close(kc); + return Py_BuildValue( + "IIII", ctx_switches, interrupts, syscalls, traps); + +error: + if (kc != NULL) + kstat_close(kc); + return NULL; +} diff --git a/psutil/arch/sunos/cpu.h b/psutil/arch/sunos/cpu.h new file mode 100644 index 0000000000..5c4309bd93 --- /dev/null +++ b/psutil/arch/sunos/cpu.h @@ -0,0 +1,11 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/disk.c b/psutil/arch/sunos/disk.c new file mode 100644 index 0000000000..c81acd832f --- /dev/null +++ b/psutil/arch/sunos/disk.c @@ -0,0 +1,121 @@ +/* + * 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. + */ + +#include + +#include +#include +#include + +#include "../../arch/all/init.h" + + +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + kstat_ctl_t *kc; + kstat_t *ksp; + kstat_io_t kio; + PyObject *py_retdict = PyDict_New(); + PyObject *py_disk_info = NULL; + + if (py_retdict == NULL) + return NULL; + kc = kstat_open(); + if (kc == NULL) { + PyErr_SetFromErrno(PyExc_OSError);; + goto error; + } + ksp = kc->kc_chain; + while (ksp != NULL) { + if (ksp->ks_type == KSTAT_TYPE_IO) { + if (strcmp(ksp->ks_class, "disk") == 0) { + if (kstat_read(kc, ksp, &kio) == -1) { + kstat_close(kc); + return PyErr_SetFromErrno(PyExc_OSError);; + } + py_disk_info = Py_BuildValue( + "(IIKKLL)", + kio.reads, + kio.writes, + kio.nread, + kio.nwritten, + kio.rtime / 1000 / 1000, // from nano to milli secs + kio.wtime / 1000 / 1000 // from nano to milli secs + ); + if (!py_disk_info) + goto error; + if (PyDict_SetItemString(py_retdict, ksp->ks_name, + py_disk_info)) + goto error; + Py_CLEAR(py_disk_info); + } + } + ksp = ksp->ks_next; + } + kstat_close(kc); + + return py_retdict; + +error: + Py_XDECREF(py_disk_info); + Py_DECREF(py_retdict); + if (kc != NULL) + kstat_close(kc); + return NULL; +} + + +PyObject * +psutil_disk_partitions(PyObject *self, PyObject *args) { + FILE *file; + struct mnttab mt; + PyObject *py_dev = NULL; + PyObject *py_mountp = NULL; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + file = fopen(MNTTAB, "rb"); + if (file == NULL) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + while (getmntent(file, &mt) == 0) { + py_dev = PyUnicode_DecodeFSDefault(mt.mnt_special); + if (! py_dev) + goto error; + py_mountp = PyUnicode_DecodeFSDefault(mt.mnt_mountp); + if (! py_mountp) + goto error; + py_tuple = Py_BuildValue( + "(OOss)", + py_dev, // device + py_mountp, // mount point + mt.mnt_fstype, // fs type + mt.mnt_mntopts); // options + if (py_tuple == NULL) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); + Py_CLEAR(py_tuple); + } + fclose(file); + return py_retlist; + +error: + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + if (file != NULL) + fclose(file); + return NULL; +} diff --git a/psutil/arch/sunos/disk.h b/psutil/arch/sunos/disk.h new file mode 100644 index 0000000000..5364b96b64 --- /dev/null +++ b/psutil/arch/sunos/disk.h @@ -0,0 +1,10 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/sunos/environ.c similarity index 98% rename from psutil/arch/solaris/environ.c rename to psutil/arch/sunos/environ.c index a2c7938556..6531b61959 100644 --- a/psutil/arch/solaris/environ.c +++ b/psutil/arch/sunos/environ.c @@ -10,17 +10,13 @@ #include -#if !defined(_LP64) && _FILE_OFFSET_BITS == 64 - #undef _FILE_OFFSET_BITS - #undef _LARGEFILE64_SOURCE -#endif - #include #include #include #include #include +#include "../../arch/all/init.h" #include "environ.h" diff --git a/psutil/arch/solaris/environ.h b/psutil/arch/sunos/environ.h similarity index 95% rename from psutil/arch/solaris/environ.h rename to psutil/arch/sunos/environ.h index 1d2039d3ad..e106ade6b6 100644 --- a/psutil/arch/solaris/environ.h +++ b/psutil/arch/sunos/environ.h @@ -4,6 +4,8 @@ * license that can be found in the LICENSE file. */ +#include + #ifndef PROCESS_AS_UTILS_H #define PROCESS_AS_UTILS_H diff --git a/psutil/arch/sunos/mem.c b/psutil/arch/sunos/mem.c new file mode 100644 index 0000000000..04b4470124 --- /dev/null +++ b/psutil/arch/sunos/mem.c @@ -0,0 +1,105 @@ +/* + * 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. + */ + +#include + +#include +#include +#include + +#include "mem.h" + + +PyObject * +psutil_swap_mem(PyObject *self, PyObject *args) { +// XXX (arghhh!) +// total/free swap mem: commented out as for some reason I can't +// manage to get the same results shown by "swap -l", despite the +// code below is exactly the same as: +// http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/ +// cmd/swap/swap.c +// We're going to parse "swap -l" output from Python (sigh!) + +/* + struct swaptable *st; + struct swapent *swapent; + int i; + struct stat64 statbuf; + char *path; + char fullpath[MAXPATHLEN+1]; + int num; + + if ((num = swapctl(SC_GETNSWP, NULL)) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + if (num == 0) { + PyErr_SetString(PyExc_RuntimeError, "no swap devices configured"); + return NULL; + } + if ((st = malloc(num * sizeof(swapent_t) + sizeof (int))) == NULL) { + PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + return NULL; + } + if ((path = malloc(num * MAXPATHLEN)) == NULL) { + PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + return NULL; + } + swapent = st->swt_ent; + for (i = 0; i < num; i++, swapent++) { + swapent->ste_path = path; + path += MAXPATHLEN; + } + st->swt_n = num; + if ((num = swapctl(SC_LIST, st)) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + swapent = st->swt_ent; + long t = 0, f = 0; + for (i = 0; i < num; i++, swapent++) { + int diskblks_per_page =(int)(sysconf(_SC_PAGESIZE) >> DEV_BSHIFT); + t += (long)swapent->ste_pages; + f += (long)swapent->ste_free; + } + + free(st); + return Py_BuildValue("(kk)", t, f); +*/ + + kstat_ctl_t *kc; + kstat_t *k; + cpu_stat_t *cpu; + int cpu_count = 0; + int flag = 0; + uint_t sin = 0; + uint_t sout = 0; + + kc = kstat_open(); + if (kc == NULL) + return PyErr_SetFromErrno(PyExc_OSError);; + + k = kc->kc_chain; + while (k != NULL) { + if ((strncmp(k->ks_name, "cpu_stat", 8) == 0) && \ + (kstat_read(kc, k, NULL) != -1) ) + { + flag = 1; + cpu = (cpu_stat_t *) k->ks_data; + sin += cpu->cpu_vminfo.pgswapin; // num pages swapped in + sout += cpu->cpu_vminfo.pgswapout; // num pages swapped out + } + cpu_count += 1; + k = k->ks_next; + } + kstat_close(kc); + if (!flag) { + PyErr_SetString(PyExc_RuntimeError, "no swap device was found"); + return NULL; + } + return Py_BuildValue("(II)", sin, sout); +} diff --git a/psutil/arch/sunos/mem.h b/psutil/arch/sunos/mem.h new file mode 100644 index 0000000000..724cb586db --- /dev/null +++ b/psutil/arch/sunos/mem.h @@ -0,0 +1,9 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c new file mode 100644 index 0000000000..0bde5e3c8b --- /dev/null +++ b/psutil/arch/sunos/net.c @@ -0,0 +1,559 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +PyObject * +psutil_net_io_counters(PyObject *self, PyObject *args) { + kstat_ctl_t *kc = NULL; + kstat_t *ksp; + kstat_named_t *rbytes, *wbytes, *rpkts, *wpkts, *ierrs, *oerrs; + int ret; + int sock = -1; + struct lifreq ifr; + + PyObject *py_retdict = PyDict_New(); + PyObject *py_ifc_info = NULL; + + if (py_retdict == NULL) + return NULL; + kc = kstat_open(); + if (kc == NULL) + goto error; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + ksp = kc->kc_chain; + while (ksp != NULL) { + if (ksp->ks_type != KSTAT_TYPE_NAMED) + goto next; + if (strcmp(ksp->ks_class, "net") != 0) + goto next; + // skip 'lo' (localhost) because it doesn't have the statistics we need + // and it makes kstat_data_lookup() fail + if (strcmp(ksp->ks_module, "lo") == 0) + goto next; + + // check if this is a network interface by sending a ioctl + PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); + ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); + if (ret == -1) + goto next; + + if (kstat_read(kc, ksp, NULL) == -1) { + errno = 0; + goto next; + } + + rbytes = (kstat_named_t *)kstat_data_lookup(ksp, "rbytes"); + wbytes = (kstat_named_t *)kstat_data_lookup(ksp, "obytes"); + rpkts = (kstat_named_t *)kstat_data_lookup(ksp, "ipackets"); + wpkts = (kstat_named_t *)kstat_data_lookup(ksp, "opackets"); + ierrs = (kstat_named_t *)kstat_data_lookup(ksp, "ierrors"); + oerrs = (kstat_named_t *)kstat_data_lookup(ksp, "oerrors"); + + if ((rbytes == NULL) || (wbytes == NULL) || (rpkts == NULL) || + (wpkts == NULL) || (ierrs == NULL) || (oerrs == NULL)) + { + PyErr_SetString(PyExc_RuntimeError, "kstat_data_lookup() failed"); + goto error; + } + + if (rbytes->data_type == KSTAT_DATA_UINT64) + { + py_ifc_info = Py_BuildValue("(KKKKIIii)", + wbytes->value.ui64, + rbytes->value.ui64, + wpkts->value.ui64, + rpkts->value.ui64, + ierrs->value.ui32, + oerrs->value.ui32, + 0, // dropin not supported + 0 // dropout not supported + ); + } + else + { + py_ifc_info = Py_BuildValue("(IIIIIIii)", + wbytes->value.ui32, + rbytes->value.ui32, + wpkts->value.ui32, + rpkts->value.ui32, + ierrs->value.ui32, + oerrs->value.ui32, + 0, // dropin not supported + 0 // dropout not supported + ); + } + if (!py_ifc_info) + goto error; + if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_ifc_info)) + goto error; + Py_CLEAR(py_ifc_info); + goto next; + +next: + ksp = ksp->ks_next; + } + + kstat_close(kc); + close(sock); + return py_retdict; + +error: + Py_XDECREF(py_ifc_info); + Py_DECREF(py_retdict); + if (kc != NULL) + kstat_close(kc); + if (sock != -1) { + close(sock); + } + return NULL; +} + + +// Return stats about a particular network interface. Refs: +// * https://github.com/dpaleino/wicd/blob/master/wicd/backends/be-ioctl.py +// * http://www.i-scream.org/libstatgrab/ +PyObject* +psutil_net_if_stats(PyObject* self, PyObject* args) { + kstat_ctl_t *kc = NULL; + kstat_t *ksp; + kstat_named_t *knp; + int ret; + int sock = -1; + int duplex; + int speed; + + PyObject *py_retdict = PyDict_New(); + PyObject *py_ifc_info = NULL; + PyObject *py_is_up = NULL; + + if (py_retdict == NULL) + return NULL; + kc = kstat_open(); + if (kc == NULL) + goto error; + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (ksp = kc->kc_chain; ksp; ksp = ksp->ks_next) { + if (strcmp(ksp->ks_class, "net") == 0) { + struct lifreq ifr; + + kstat_read(kc, ksp, NULL); + if (ksp->ks_type != KSTAT_TYPE_NAMED) + continue; + if (strcmp(ksp->ks_class, "net") != 0) + continue; + + PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); + ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); + if (ret == -1) + continue; // not a network interface + + // is up? + if ((ifr.lifr_flags & IFF_UP) != 0) { + if ((knp = kstat_data_lookup(ksp, "link_up")) != NULL) { + if (knp->value.ui32 != 0u) + py_is_up = Py_True; + else + py_is_up = Py_False; + } + else { + py_is_up = Py_True; + } + } + else { + py_is_up = Py_False; + } + Py_INCREF(py_is_up); + + // duplex + duplex = 0; // unknown + if ((knp = kstat_data_lookup(ksp, "link_duplex")) != NULL) { + if (knp->value.ui32 == 1) + duplex = 1; // half + else if (knp->value.ui32 == 2) + duplex = 2; // full + } + + // speed + if ((knp = kstat_data_lookup(ksp, "ifspeed")) != NULL) + // expressed in bits per sec, we want mega bits per sec + speed = (int)knp->value.ui64 / 1000000; + else + speed = 0; + + // mtu + ret = ioctl(sock, SIOCGLIFMTU, &ifr); + if (ret == -1) + goto error; + + py_ifc_info = Py_BuildValue("(Oiii)", py_is_up, duplex, speed, + ifr.lifr_mtu); + if (!py_ifc_info) + goto error; + if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_ifc_info)) + goto error; + Py_CLEAR(py_ifc_info); + } + } + + close(sock); + kstat_close(kc); + return py_retdict; + +error: + Py_XDECREF(py_is_up); + Py_XDECREF(py_ifc_info); + Py_DECREF(py_retdict); + if (sock != -1) + close(sock); + if (kc != NULL) + kstat_close(kc); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + + +/* + * Return TCP and UDP connections opened by process. + * UNIX sockets are excluded. + * + * Thanks to: + * https://github.com/DavidGriffith/finx/blob/master/ + * nxsensor-3.5.0-1/src/sysdeps/solaris.c + * ...and: + * https://hg.java.net/hg/solaris~on-src/file/tip/usr/src/cmd/ + * cmd-inet/usr.bin/netstat/netstat.c + */ +PyObject * +psutil_net_connections(PyObject *self, PyObject *args) { + long pid; + int sd = 0; + mib2_tcpConnEntry_t tp; + mib2_udpEntry_t ude; +#if defined(AF_INET6) + mib2_tcp6ConnEntry_t tp6; + mib2_udp6Entry_t ude6; +#endif + char buf[512]; + int i, flags, getcode, num_ent, state, ret; + char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; + int lport, rport; + int processed_pid; + int databuf_init = 0; + struct strbuf ctlbuf, databuf; + struct T_optmgmt_req tor = {0}; + struct T_optmgmt_ack toa = {0}; + struct T_error_ack tea = {0}; + struct opthdr mibhdr = {0}; + + PyObject *py_retlist = PyList_New(0); + PyObject *py_tuple = NULL; + PyObject *py_laddr = NULL; + PyObject *py_raddr = NULL; + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, "l", &pid)) + goto error; + + sd = open("/dev/arp", O_RDWR); + if (sd == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, "/dev/arp"); + goto error; + } + + ret = ioctl(sd, I_PUSH, "tcp"); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + ret = ioctl(sd, I_PUSH, "udp"); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + // + // OK, this mess is basically copied and pasted from nxsensor project + // which copied and pasted it from netstat source code, mibget() + // function. Also see: + // http://stackoverflow.com/questions/8723598/ + tor.PRIM_type = T_SVR4_OPTMGMT_REQ; + tor.OPT_offset = sizeof (struct T_optmgmt_req); + tor.OPT_length = sizeof (struct opthdr); + tor.MGMT_flags = T_CURRENT; + mibhdr.level = MIB2_IP; + mibhdr.name = 0; + +#ifdef NEW_MIB_COMPLIANT + mibhdr.len = 1; +#else + mibhdr.len = 0; +#endif + memcpy(buf, &tor, sizeof tor); + memcpy(buf + tor.OPT_offset, &mibhdr, sizeof mibhdr); + + ctlbuf.buf = buf; + ctlbuf.len = tor.OPT_offset + tor.OPT_length; + flags = 0; // request to be sent in non-priority + + if (putmsg(sd, &ctlbuf, (struct strbuf *)0, flags) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + ctlbuf.maxlen = sizeof (buf); + for (;;) { + flags = 0; + getcode = getmsg(sd, &ctlbuf, (struct strbuf *)0, &flags); + memcpy(&toa, buf, sizeof toa); + memcpy(&tea, buf, sizeof tea); + + if (getcode != MOREDATA || + ctlbuf.len < (int)sizeof (struct T_optmgmt_ack) || + toa.PRIM_type != T_OPTMGMT_ACK || + toa.MGMT_flags != T_SUCCESS) + { + break; + } + if (ctlbuf.len >= (int)sizeof (struct T_error_ack) && + tea.PRIM_type == T_ERROR_ACK) + { + PyErr_SetString(PyExc_RuntimeError, "ERROR_ACK"); + goto error; + } + if (getcode == 0 && + ctlbuf.len >= (int)sizeof (struct T_optmgmt_ack) && + toa.PRIM_type == T_OPTMGMT_ACK && + toa.MGMT_flags == T_SUCCESS) + { + PyErr_SetString(PyExc_RuntimeError, "ERROR_T_OPTMGMT_ACK"); + goto error; + } + + memset(&mibhdr, 0x0, sizeof(mibhdr)); + memcpy(&mibhdr, buf + toa.OPT_offset, toa.OPT_length); + + databuf.maxlen = mibhdr.len; + databuf.len = 0; + databuf.buf = (char *)malloc((int)mibhdr.len); + if (!databuf.buf) { + PyErr_NoMemory(); + goto error; + } + databuf_init = 1; + + flags = 0; + getcode = getmsg(sd, (struct strbuf *)0, &databuf, &flags); + if (getcode < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + // TCPv4 + if (mibhdr.level == MIB2_TCP && mibhdr.name == MIB2_TCP_13) { + num_ent = mibhdr.len / sizeof(mib2_tcpConnEntry_t); + for (i = 0; i < num_ent; i++) { + memcpy(&tp, databuf.buf + i * sizeof tp, sizeof tp); +#ifdef NEW_MIB_COMPLIANT + processed_pid = tp.tcpConnCreationProcess; +#else + processed_pid = 0; +#endif + if (pid != -1 && processed_pid != pid) + continue; + // construct local/remote addresses + inet_ntop(AF_INET, &tp.tcpConnLocalAddress, lip, sizeof(lip)); + inet_ntop(AF_INET, &tp.tcpConnRemAddress, rip, sizeof(rip)); + lport = tp.tcpConnLocalPort; + rport = tp.tcpConnRemPort; + + // construct python tuple/list + py_laddr = Py_BuildValue("(si)", lip, lport); + if (!py_laddr) + goto error; + if (rport != 0) + py_raddr = Py_BuildValue("(si)", rip, rport); + else { + py_raddr = Py_BuildValue("()"); + } + if (!py_raddr) + goto error; + state = tp.tcpConnEntryInfo.ce_state; + + // add item + py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET, SOCK_STREAM, + py_laddr, py_raddr, state, + processed_pid); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + } + } +#if defined(AF_INET6) + // TCPv6 + else if (mibhdr.level == MIB2_TCP6 && mibhdr.name == MIB2_TCP6_CONN) + { + num_ent = mibhdr.len / sizeof(mib2_tcp6ConnEntry_t); + + for (i = 0; i < num_ent; i++) { + memcpy(&tp6, databuf.buf + i * sizeof tp6, sizeof tp6); +#ifdef NEW_MIB_COMPLIANT + processed_pid = tp6.tcp6ConnCreationProcess; +#else + processed_pid = 0; +#endif + if (pid != -1 && processed_pid != pid) + continue; + // construct local/remote addresses + inet_ntop(AF_INET6, &tp6.tcp6ConnLocalAddress, lip, sizeof(lip)); + inet_ntop(AF_INET6, &tp6.tcp6ConnRemAddress, rip, sizeof(rip)); + lport = tp6.tcp6ConnLocalPort; + rport = tp6.tcp6ConnRemPort; + + // construct python tuple/list + py_laddr = Py_BuildValue("(si)", lip, lport); + if (!py_laddr) + goto error; + if (rport != 0) + py_raddr = Py_BuildValue("(si)", rip, rport); + else + py_raddr = Py_BuildValue("()"); + if (!py_raddr) + goto error; + state = tp6.tcp6ConnEntryInfo.ce_state; + + // add item + py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET6, SOCK_STREAM, + py_laddr, py_raddr, state, processed_pid); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + } + } +#endif + // UDPv4 + else if (mibhdr.level == MIB2_UDP && mibhdr.name == MIB2_UDP_ENTRY) { + num_ent = mibhdr.len / sizeof(mib2_udpEntry_t); + assert(num_ent * sizeof(mib2_udpEntry_t) == mibhdr.len); + for (i = 0; i < num_ent; i++) { + memcpy(&ude, databuf.buf + i * sizeof ude, sizeof ude); +#ifdef NEW_MIB_COMPLIANT + processed_pid = ude.udpCreationProcess; +#else + processed_pid = 0; +#endif + if (pid != -1 && processed_pid != pid) + continue; + // XXX Very ugly hack! It seems we get here only the first + // time we bump into a UDPv4 socket. PID is a very high + // number (clearly impossible) and the address does not + // belong to any valid interface. Not sure what else + // to do other than skipping. + if (processed_pid > 131072) + continue; + inet_ntop(AF_INET, &ude.udpLocalAddress, lip, sizeof(lip)); + lport = ude.udpLocalPort; + py_laddr = Py_BuildValue("(si)", lip, lport); + if (!py_laddr) + goto error; + py_raddr = Py_BuildValue("()"); + if (!py_raddr) + goto error; + py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET, SOCK_DGRAM, + py_laddr, py_raddr, PSUTIL_CONN_NONE, + processed_pid); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + } + } +#if defined(AF_INET6) + // UDPv6 + else if (mibhdr.level == MIB2_UDP6 || + mibhdr.level == MIB2_UDP6_ENTRY) + { + num_ent = mibhdr.len / sizeof(mib2_udp6Entry_t); + for (i = 0; i < num_ent; i++) { + memcpy(&ude6, databuf.buf + i * sizeof ude6, sizeof ude6); +#ifdef NEW_MIB_COMPLIANT + processed_pid = ude6.udp6CreationProcess; +#else + processed_pid = 0; +#endif + if (pid != -1 && processed_pid != pid) + continue; + inet_ntop(AF_INET6, &ude6.udp6LocalAddress, lip, sizeof(lip)); + lport = ude6.udp6LocalPort; + py_laddr = Py_BuildValue("(si)", lip, lport); + if (!py_laddr) + goto error; + py_raddr = Py_BuildValue("()"); + if (!py_raddr) + goto error; + py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET6, SOCK_DGRAM, + py_laddr, py_raddr, PSUTIL_CONN_NONE, + processed_pid); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + } + } +#endif + free(databuf.buf); + } + + close(sd); + return py_retlist; + +error: + Py_XDECREF(py_tuple); + Py_XDECREF(py_laddr); + Py_XDECREF(py_raddr); + Py_DECREF(py_retlist); + if (databuf_init == 1) + free(databuf.buf); + if (sd != 0) + close(sd); + return NULL; +} diff --git a/psutil/arch/sunos/net.h b/psutil/arch/sunos/net.h new file mode 100644 index 0000000000..97bfd0af57 --- /dev/null +++ b/psutil/arch/sunos/net.h @@ -0,0 +1,11 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c new file mode 100644 index 0000000000..39bb9a52be --- /dev/null +++ b/psutil/arch/sunos/proc.c @@ -0,0 +1,613 @@ +/* + * 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. + */ + +#include + +#include +#include + +#include "../../arch/all/init.h" +#include "proc.h" +#include "environ.h" + + +// Read a file content and fills a C structure with it. +int +psutil_file_to_struct(char *path, void *fstruct, size_t size) { + int fd; + ssize_t nbytes; + fd = open(path, O_RDONLY); + if (fd == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + return 0; + } + nbytes = read(fd, fstruct, size); + if (nbytes == -1) { + close(fd); + PyErr_SetFromErrno(PyExc_OSError); + return 0; + } + if (nbytes != (ssize_t) size) { + close(fd); + PyErr_SetString( + PyExc_RuntimeError, "read() file structure size mismatch"); + return 0; + } + close(fd); + return nbytes; +} + + +/* + * Return process ppid, rss, vms, ctime, nice, nthreads, status and tty + * as a Python tuple. + */ +PyObject * +psutil_proc_basic_info(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + psinfo_t info; + const char *procfs_path; + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + + sprintf(path, "%s/%i/psinfo", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + return NULL; + return Py_BuildValue( + "ikkdiiikiiii", + info.pr_ppid, // parent pid + info.pr_rssize, // rss + info.pr_size, // vms + PSUTIL_TV2DOUBLE(info.pr_start), // create time + info.pr_lwp.pr_nice, // nice + info.pr_nlwp, // no. of threads + info.pr_lwp.pr_state, // status code + info.pr_ttydev, // tty nr + (int)info.pr_uid, // real user id + (int)info.pr_euid, // effective user id + (int)info.pr_gid, // real group id + (int)info.pr_egid // effective group id + ); +} + + +/* + * Join array of C strings to C string with delimiter dm. + * Omit empty records. + */ +static int +cstrings_array_to_string(char **joined, char ** array, size_t count, char dm) { + size_t i; + size_t total_length = 0; + size_t item_length = 0; + char *result = NULL; + char *last = NULL; + + if (!array || !joined) + return 0; + + for (i=0; i 0) { + py_args = PyUnicode_DecodeFSDefault(argv_plain); + free(argv_plain); + } else if (joined < 0) { + goto error; + } + + psutil_free_cstrings_array(argv, argc); + } + } + + /* If we can't read process memory or can't decode the result + * then return args from /proc. */ + if (!py_args) { + PyErr_Clear(); + py_args = PyUnicode_DecodeFSDefault(info.pr_psargs); + } + + /* Both methods has been failed. */ + if (!py_args) + goto error; + + py_retlist = Py_BuildValue("OO", py_name, py_args); + if (!py_retlist) + goto error; + + Py_DECREF(py_name); + Py_DECREF(py_args); + return py_retlist; + +error: + Py_XDECREF(py_name); + Py_XDECREF(py_args); + Py_XDECREF(py_retlist); + return NULL; +} + + +/* + * Return process environ block. + */ +PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + psinfo_t info; + const char *procfs_path; + char **env = NULL; + ssize_t env_count = -1; + char *dm; + int i = 0; + PyObject *py_envname = NULL; + PyObject *py_envval = NULL; + PyObject *py_retdict = PyDict_New(); + + if (! py_retdict) + return PyErr_NoMemory(); + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + + sprintf(path, "%s/%i/psinfo", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + goto error; + + if (! info.pr_envp) { + AccessDenied("/proc/pid/psinfo struct not set"); + goto error; + } + + env = psutil_read_raw_env(info, procfs_path, &env_count); + if (! env && env_count != 0) + goto error; + + for (i=0; i= 0) + psutil_free_cstrings_array(env, env_count); + + Py_XDECREF(py_envname); + Py_XDECREF(py_envval); + Py_XDECREF(py_retdict); + return NULL; +} + + +/* + * Return process user and system CPU times as a Python tuple. + */ +PyObject * +psutil_proc_cpu_times(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + pstatus_t info; + const char *procfs_path; + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + sprintf(path, "%s/%i/status", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + return NULL; + // results are more precise than os.times() + return Py_BuildValue( + "(dddd)", + PSUTIL_TV2DOUBLE(info.pr_utime), + PSUTIL_TV2DOUBLE(info.pr_stime), + PSUTIL_TV2DOUBLE(info.pr_cutime), + PSUTIL_TV2DOUBLE(info.pr_cstime) + ); +} + + +/* + * Return what CPU the process is running on. + */ +PyObject * +psutil_proc_cpu_num(PyObject *self, PyObject *args) { + int fd = -1; + int pid; + char path[1000]; + struct prheader header; + struct lwpsinfo *lwp = NULL; + int nent; + int size; + int proc_num; + ssize_t nbytes; + const char *procfs_path; + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + + sprintf(path, "%s/%i/lpsinfo", procfs_path, pid); + fd = open(path, O_RDONLY); + if (fd == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + return NULL; + } + + // read header + nbytes = pread(fd, &header, sizeof(header), 0); + if (nbytes == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + if (nbytes != sizeof(header)) { + PyErr_SetString( + PyExc_RuntimeError, "read() file structure size mismatch"); + goto error; + } + + // malloc + nent = header.pr_nent; + size = header.pr_entsize * nent; + lwp = malloc(size); + if (lwp == NULL) { + PyErr_NoMemory(); + goto error; + } + + // read the rest + nbytes = pread(fd, lwp, size, sizeof(header)); + if (nbytes == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + if (nbytes != size) { + PyErr_SetString( + PyExc_RuntimeError, "read() file structure size mismatch"); + goto error; + } + + // done + proc_num = lwp->pr_onpro; + close(fd); + free(lwp); + return Py_BuildValue("i", proc_num); + +error: + if (fd != -1) + close(fd); + free(lwp); + return NULL; +} + + +/* + * Return process uids/gids as a Python tuple. + */ +PyObject * +psutil_proc_cred(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + prcred_t info; + const char *procfs_path; + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + sprintf(path, "%s/%i/cred", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + return NULL; + return Py_BuildValue("iiiiii", + info.pr_ruid, info.pr_euid, info.pr_suid, + info.pr_rgid, info.pr_egid, info.pr_sgid); +} + + +/* + * Return process voluntary and involuntary context switches as a Python tuple. + */ +PyObject * +psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + prusage_t info; + const char *procfs_path; + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + sprintf(path, "%s/%i/usage", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + return NULL; + return Py_BuildValue("kk", info.pr_vctx, info.pr_ictx); +} + + +/* + * Process IO counters. + * + * Commented out and left here as a reminder. Apparently we cannot + * retrieve process IO stats because: + * - 'pr_ioch' is a sum of chars read and written, with no distinction + * - 'pr_inblk' and 'pr_oublk', which should be the number of bytes + * read and written, hardly increase and according to: + * http://www.brendangregg.com/Solaris/paper_diskubyp1.pdf + * ...they should be meaningless anyway. + * +PyObject* +proc_io_counters(PyObject* self, PyObject* args) { + int pid; + char path[1000]; + prusage_t info; + const char *procfs_path; + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + sprintf(path, "%s/%i/usage", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + return NULL; + + // On Solaris we only have 'pr_ioch' which accounts for bytes read + // *and* written. + // 'pr_inblk' and 'pr_oublk' should be expressed in blocks of + // 8KB according to: + // http://www.brendangregg.com/Solaris/paper_diskubyp1.pdf (pag. 8) + return Py_BuildValue("kkkk", + info.pr_ioch, + info.pr_ioch, + info.pr_inblk, + info.pr_oublk); +} +*/ + + +/* + * Return information about a given process thread. + */ +PyObject * +psutil_proc_query_thread(PyObject *self, PyObject *args) { + int pid, tid; + char path[1000]; + lwpstatus_t info; + const char *procfs_path; + + if (! PyArg_ParseTuple(args, "iis", &pid, &tid, &procfs_path)) + return NULL; + sprintf(path, "%s/%i/lwp/%i/lwpstatus", procfs_path, pid, tid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + return NULL; + return Py_BuildValue("dd", + PSUTIL_TV2DOUBLE(info.pr_utime), + PSUTIL_TV2DOUBLE(info.pr_stime)); +} + + +/* + * Return process memory mappings. + */ +PyObject * +psutil_proc_memory_maps(PyObject *self, PyObject *args) { + int pid; + int fd = -1; + char path[1000]; + char perms[10]; + const char *name; + struct stat st; + pstatus_t status; + + prxmap_t *xmap = NULL, *p; + off_t size; + size_t nread; + int nmap; + uintptr_t pr_addr_sz; + uintptr_t stk_base_sz, brk_base_sz; + const char *procfs_path; + + PyObject *py_tuple = NULL; + PyObject *py_path = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + goto error; + + sprintf(path, "%s/%i/status", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&status, sizeof(status))) + goto error; + + sprintf(path, "%s/%i/xmap", procfs_path, pid); + if (stat(path, &st) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + size = st.st_size; + + fd = open(path, O_RDONLY); + if (fd == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + xmap = (prxmap_t *)malloc(size); + if (xmap == NULL) { + PyErr_NoMemory(); + goto error; + } + + nread = pread(fd, xmap, size, 0); + nmap = nread / sizeof(prxmap_t); + p = xmap; + + while (nmap) { + nmap -= 1; + if (p == NULL) { + p += 1; + continue; + } + + perms[0] = '\0'; + pr_addr_sz = p->pr_vaddr + p->pr_size; + + // perms + sprintf(perms, "%c%c%c%c", p->pr_mflags & MA_READ ? 'r' : '-', + p->pr_mflags & MA_WRITE ? 'w' : '-', + p->pr_mflags & MA_EXEC ? 'x' : '-', + p->pr_mflags & MA_SHARED ? 's' : '-'); + + // name + if (strlen(p->pr_mapname) > 0) { + name = p->pr_mapname; + } + else { + if ((p->pr_mflags & MA_ISM) || (p->pr_mflags & MA_SHM)) { + name = "[shmid]"; + } + else { + stk_base_sz = status.pr_stkbase + status.pr_stksize; + brk_base_sz = status.pr_brkbase + status.pr_brksize; + + if ((pr_addr_sz > status.pr_stkbase) && + (p->pr_vaddr < stk_base_sz)) { + name = "[stack]"; + } + else if ((p->pr_mflags & MA_ANON) && \ + (pr_addr_sz > status.pr_brkbase) && \ + (p->pr_vaddr < brk_base_sz)) { + name = "[heap]"; + } + else { + name = "[anon]"; + } + } + } + + py_path = PyUnicode_DecodeFSDefault(name); + if (! py_path) + goto error; + py_tuple = Py_BuildValue( + "kksOkkk", + (unsigned long)p->pr_vaddr, + (unsigned long)pr_addr_sz, + perms, + py_path, + (unsigned long)p->pr_rss * p->pr_pagesize, + (unsigned long)p->pr_anon * p->pr_pagesize, + (unsigned long)p->pr_locked * p->pr_pagesize); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_path); + Py_CLEAR(py_tuple); + + // increment pointer + p += 1; + } + + close(fd); + free(xmap); + return py_retlist; + +error: + if (fd != -1) + close(fd); + Py_XDECREF(py_tuple); + Py_XDECREF(py_path); + Py_DECREF(py_retlist); + if (xmap != NULL) + free(xmap); + return NULL; +} diff --git a/psutil/arch/sunos/proc.h b/psutil/arch/sunos/proc.h new file mode 100644 index 0000000000..874f360e58 --- /dev/null +++ b/psutil/arch/sunos/proc.h @@ -0,0 +1,20 @@ +/* + * 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. + */ + +#include + +#define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) +int psutil_file_to_struct(char *path, void *fstruct, size_t size); + +PyObject *psutil_proc_basic_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_num(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_cred(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_name_and_args(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_ctx_switches(PyObject *self, PyObject *args); +PyObject *psutil_proc_query_thread(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/sys.c b/psutil/arch/sunos/sys.c new file mode 100644 index 0000000000..50b85c9bf6 --- /dev/null +++ b/psutil/arch/sunos/sys.c @@ -0,0 +1,94 @@ +/* + * 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. + */ + +#include + +#include + +#include "../../arch/all/init.h" + + +PyObject * +psutil_boot_time(PyObject *self, PyObject *args) { + float boot_time = 0.0; + struct utmpx *ut; + + setutxent(); + while (NULL != (ut = getutxent())) { + if (ut->ut_type == BOOT_TIME) { + boot_time = (float)ut->ut_tv.tv_sec; + break; + } + } + endutxent(); + if (fabs(boot_time) < 0.000001) { + /* could not find BOOT_TIME in getutxent loop */ + PyErr_SetString(PyExc_RuntimeError, "can't determine boot time"); + return NULL; + } + return Py_BuildValue("f", boot_time); +} + + +PyObject * +psutil_users(PyObject *self, PyObject *args) { + struct utmpx *ut; + PyObject *py_tuple = NULL; + PyObject *py_username = NULL; + PyObject *py_tty = NULL; + PyObject *py_hostname = NULL; + PyObject *py_user_proc = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + setutxent(); + while (NULL != (ut = getutxent())) { + if (ut->ut_type == USER_PROCESS) + py_user_proc = Py_True; + else + py_user_proc = Py_False; + py_username = PyUnicode_DecodeFSDefault(ut->ut_user); + if (! py_username) + goto error; + py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); + if (! py_tty) + goto error; + py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); + if (! py_hostname) + goto error; + py_tuple = Py_BuildValue( + "(OOOdOi)", + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)ut->ut_tv.tv_sec, // tstamp + py_user_proc, // (bool) user process + ut->ut_pid // process id + ); + if (py_tuple == NULL) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); + } + endutxent(); + + return py_retlist; + +error: + Py_XDECREF(py_username); + Py_XDECREF(py_tty); + Py_XDECREF(py_hostname); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + endutxent(); + return NULL; +} diff --git a/psutil/arch/sunos/sys.h b/psutil/arch/sunos/sys.h new file mode 100644 index 0000000000..9fc6510c6d --- /dev/null +++ b/psutil/arch/sunos/sys.h @@ -0,0 +1,10 @@ +/* + * 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. + */ + +#include + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/arch/solaris/v10/ifaddrs.c b/psutil/arch/sunos/v10/ifaddrs.c similarity index 100% rename from psutil/arch/solaris/v10/ifaddrs.c rename to psutil/arch/sunos/v10/ifaddrs.c diff --git a/psutil/arch/solaris/v10/ifaddrs.h b/psutil/arch/sunos/v10/ifaddrs.h similarity index 100% rename from psutil/arch/solaris/v10/ifaddrs.h rename to psutil/arch/sunos/v10/ifaddrs.h diff --git a/setup.py b/setup.py index e03a91eab8..34cb176240 100755 --- a/setup.py +++ b/setup.py @@ -423,12 +423,12 @@ def get_winver(): macros.append(("PSUTIL_SUNOS", 1)) ext = Extension( 'psutil._psutil_sunos', - sources=sources - + [ - 'psutil/_psutil_sunos.c', - 'psutil/arch/solaris/v10/ifaddrs.c', - 'psutil/arch/solaris/environ.c', - ], + sources=( + sources + + ["psutil/_psutil_sunos.c"] + + glob.glob("psutil/arch/sunos/*.c") + + glob.glob("psutil/arch/sunos/v10/*.c") + ), define_macros=macros, libraries=['kstat', 'nsl', 'socket'], # fmt: off From c8f0bb44ab7e4d8f807bcd2640536f2c1433719a Mon Sep 17 00:00:00 2001 From: Julien Stephan Date: Tue, 29 Apr 2025 20:09:19 +0200 Subject: [PATCH 1251/1714] linux: fix smaps parsing crash on empty VmFlags: lines (#2560) On some systems (observed at least on riscv64 under qemu emulation), /proc//smaps contains lines like: VmFlags: without any value. The current parser assumed at least two fields per line, causing an IndexError during memory_maps() processing. This patch defensively skips lines with less than 2 fields, which matches existing logic for ignoring unhandled VmFlags lines. Signed-off-by: Julien Stephan --- psutil/_pslinux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4d34487dec..06364ccba0 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -2021,7 +2021,7 @@ def get_blocks(lines, current_block): else: try: data[fields[0]] = int(fields[1]) * 1024 - except ValueError: + except (ValueError, IndexError): if fields[0].startswith(b'VmFlags:'): # see issue #369 continue From df3702ac80dbee2491995127349b45e603822563 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 29 Apr 2025 20:12:01 +0200 Subject: [PATCH 1252/1714] give CREDITS to @justeph for #2560. --- CREDITS | 4 ++++ HISTORY.rst | 3 +++ 2 files changed, 7 insertions(+) diff --git a/CREDITS b/CREDITS index 61b37498bf..d3eabf5cab 100644 --- a/CREDITS +++ b/CREDITS @@ -856,3 +856,7 @@ I: 2526, 2527 N: Marcel Telka W: https://github.com/mtelka I: 2545 + +N: Julien Stephan +W: https://github.com/justeph +I: 2560 diff --git a/HISTORY.rst b/HISTORY.rst index 2e7c305369..586230d089 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,6 +24,9 @@ XXXX-XX-XX - 2545_: [illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. - 2552_, [Windows]: `boot_time()`_ didn't take into account the time spent during suspend / hybernation. +- 2560_, [Linux]: ``Process.memory_maps()`` may crash with `IndexError` on + RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien + Stephan) 7.0.0 ===== From 4861f50edc7e07d00ce24e4f643e24ef3494c7fd Mon Sep 17 00:00:00 2001 From: Ben Peddell Date: Wed, 30 Apr 2025 04:14:30 +1000 Subject: [PATCH 1253/1714] Handle alternate NT path forms (#2495) * Handle alternate NT path forms `convert_dos_path` does not currently handle UNC paths or paths of the form `\??\X:` (as returned by Wine). This results in the `psutil/tests/test_process.py::TestProcess::test_exe` test failing in the following cases: * Python is run from a network share; or * The tests are performed under Wine on a drive other than the one Python is installed on. Add handling for the following NT path forms: * `\\server\share` * `\Device\Mup\server\share` -> `\\server\share` * `\??\UNC\server\share` -> `\\server\share` * `\??\X:` -> `X:` Signed-off-by: Ben Peddell * Add tests for _pswindows.convert_dos_path * Test that the following return r"C:\Windows\Temp" (where r"\Device\HarddiskVolume1" is mounted on "C:"): - convert_dos_path(r"\??\C:\Windows\Temp") - convert_dos_path(r"\Device\HarddiskVolume1\Windows\Temp") * Test that the following return r"\\localhost\C$\Windows\Temp": - convert_dos_path(r"\\localhost\C$\Windows\Temp") - convert_dos_path(r"\??\UNC\localhost\C$\Windows\Temp") - convert_dos_path(r"\Device\Mup\localhost\C$\Windows\Temp") Signed-off-by: Ben Peddell --- psutil/_pswindows.py | 13 +++++++++++-- psutil/tests/test_windows.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 7f5f8f0ebf..12d54cb81e 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -184,12 +184,21 @@ class IOPriority(enum.IntEnum): @functools.lru_cache(maxsize=512) def convert_dos_path(s): r"""Convert paths using native DOS format like: - "\Device\HarddiskVolume1\Windows\systemew\file.txt" + "\Device\HarddiskVolume1\Windows\systemew\file.txt" or + "\??\C:\Windows\systemew\file.txt" into: "C:\Windows\systemew\file.txt". """ + if s.startswith('\\\\'): + return s rawdrive = '\\'.join(s.split('\\')[:3]) - driveletter = cext.QueryDosDevice(rawdrive) + if rawdrive in {"\\??\\UNC", "\\Device\\Mup"}: + rawdrive = '\\'.join(s.split('\\')[:4]) + driveletter = "\\\\" + s.split('\\')[3] + elif rawdrive.startswith('\\??\\'): + driveletter = s.split('\\')[2] + else: + driveletter = cext.QueryDosDevice(rawdrive) remainder = s[len(rawdrive) :] return os.path.join(driveletter, remainder) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 2c540afb65..77e9d92efd 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -256,6 +256,38 @@ def test_disk_partitions(self): ] assert sys_value == psutil_value + def test_convert_dos_path_drive(self): + winpath = 'C:\\Windows\\Temp' + driveletter = 'C:' + # Mocked NT device path for C: + devicepath = '\\Device\\HarddiskVolume1' + + # Path returned by RtlDosPathNameToNtPathName + ntpath1 = '\\??\\C:\\Windows\\Temp' + # Mocked normalized NT path + ntpath2 = '\\Device\\HarddiskVolume1\\Windows\\Temp' + + devices = {devicepath: driveletter} + + with mock.patch( + 'psutil._pswindows.cext.QueryDosDevice', side_effect=devices.get + ) as m: + assert psutil._pswindows.convert_dos_path(ntpath1) == winpath + assert psutil._pswindows.convert_dos_path(ntpath2) == winpath + assert m.called + + def test_convert_dos_path_unc(self): + # UNC path + winpath = '\\\\localhost\\C$\\Windows\\Temp' + # Path returned by RtlDosPathNameToNtPathName + ntpath1 = '\\??\\UNC\\localhost\\C$\\Windows\\Temp' + # Normalized NT path + ntpath2 = '\\Device\\Mup\\localhost\\C$\\Windows\\Temp' + + assert psutil._pswindows.convert_dos_path(winpath) == winpath + assert psutil._pswindows.convert_dos_path(ntpath1) == winpath + assert psutil._pswindows.convert_dos_path(ntpath2) == winpath + def test_net_if_stats(self): ps_names = set(cext.net_if_stats()) wmi_adapters = wmi.WMI().Win32_NetworkAdapter() From e0a9eeafa54fca45dda64b4edf115560bb96a5cf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 29 Apr 2025 20:21:34 +0200 Subject: [PATCH 1254/1714] give CREDITS to @klightspeed for #2494 and #2495 --- CREDITS | 4 ++++ HISTORY.rst | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index d3eabf5cab..d552d10d84 100644 --- a/CREDITS +++ b/CREDITS @@ -860,3 +860,7 @@ I: 2545 N: Julien Stephan W: https://github.com/justeph I: 2560 + +N: Ben Peddell +W: https://github.com/klightspeed +I: 2494 diff --git a/HISTORY.rst b/HISTORY.rst index 586230d089..813a87ebf4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,6 +8,11 @@ XXXX-XX-XX **Bug fixes** - 2473_, [macOS]: Fix build issue on macOS 11 and lower. +- 2494_, [Windows]: All APIs dealing with paths, such as + `Process.memory_maps()`_, `Process.exe()`_ and `Process.open_files()`_ does + not properly handle UNC paths. Paths such as ``\\??\\C:\\Windows\\Temp`` and + ``'\\Device\\HarddiskVolume1\\Windows\\Temp'`` are now converted to + ``C:\\Windows\\Temp``. (patch by Ben Peddell) - 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due to a race condition. - 2526_, [Linux]: `Process.create_time()`_, which is used to univocally @@ -24,7 +29,7 @@ XXXX-XX-XX - 2545_: [illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. - 2552_, [Windows]: `boot_time()`_ didn't take into account the time spent during suspend / hybernation. -- 2560_, [Linux]: ``Process.memory_maps()`` may crash with `IndexError` on +- 2560_, [Linux]: `Process.memory_maps()`_ may crash with `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien Stephan) From fe4742a68b244d51bf1a60aff450f59f67aa7c22 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Tue, 29 Apr 2025 20:34:55 +0200 Subject: [PATCH 1255/1714] Fix typo SUONS -> SUNOS (#2469) --- psutil/tests/test_connections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 843fc46139..c693fb845d 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -204,7 +204,7 @@ class TestConnectedSocket(ConnectionTestCase): # On SunOS, even after we close() it, the server socket stays around # in TIME_WAIT state. - @pytest.mark.skipif(SUNOS, reason="unreliable on SUONS") + @pytest.mark.skipif(SUNOS, reason="unreliable on SUNOS") def test_tcp(self): addr = ("127.0.0.1", 0) assert this_proc_net_connections(kind='tcp4') == [] From b7b27e0c73c6b546815129005184f2bb9f2bce70 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 30 Apr 2025 00:42:04 +0200 Subject: [PATCH 1256/1714] setup.py: use more modern code to silence stdout/stderr --- setup.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/setup.py b/setup.py index 34cb176240..d090bdf288 100755 --- a/setup.py +++ b/setup.py @@ -186,20 +186,10 @@ def get_long_description(): @contextlib.contextmanager -def silenced_output(stream_name): - class DummyFile(io.BytesIO): - # see: https://github.com/giampaolo/psutil/issues/678 - errors = "ignore" - - def write(self, s): - pass - - orig = getattr(sys, stream_name) - try: - setattr(sys, stream_name, DummyFile()) - yield - finally: - setattr(sys, stream_name, orig) +def silenced_output(): + with contextlib.redirect_stdout(io.StringIO()): + with contextlib.redirect_stderr(io.StringIO()): + yield def missdeps(cmdline): @@ -259,9 +249,9 @@ def unix_can_compile(c_code): # https://github.com/giampaolo/psutil/pull/1568 if os.getenv('CC'): compiler.set_executable('compiler_so', os.getenv('CC')) - with silenced_output('stderr'): - with silenced_output('stdout'): - compiler.compile([f.name], output_dir=tempdir) + with silenced_output(): + compiler.compile([f.name], output_dir=tempdir) + compiler.compile([f.name], output_dir=tempdir) except CompileError: return False else: From af615b2197ece879f129d89c48b6f3980f0faf75 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 30 Apr 2025 00:45:12 +0200 Subject: [PATCH 1257/1714] setup.py: update classifiers --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d090bdf288..abbacfcb89 100755 --- a/setup.py +++ b/setup.py @@ -516,18 +516,19 @@ def main(): classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', - 'Environment :: Win32 (MS Windows)', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: System Administrators', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows :: Windows 10', + 'Operating System :: Microsoft :: Windows :: Windows 11', 'Operating System :: Microsoft :: Windows :: Windows 7', 'Operating System :: Microsoft :: Windows :: Windows 8', 'Operating System :: Microsoft :: Windows :: Windows 8.1', 'Operating System :: Microsoft :: Windows :: Windows Server 2003', 'Operating System :: Microsoft :: Windows :: Windows Server 2008', 'Operating System :: Microsoft :: Windows :: Windows Vista', + 'Operating System :: Microsoft :: Windows', 'Operating System :: Microsoft', 'Operating System :: OS Independent', 'Operating System :: POSIX :: AIX', @@ -546,7 +547,6 @@ def main(): 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries', 'Topic :: System :: Benchmark', - 'Topic :: System :: Hardware :: Hardware Drivers', 'Topic :: System :: Hardware', 'Topic :: System :: Monitoring', 'Topic :: System :: Networking :: Monitoring :: Hardware Watchdog', From ec7d3e76611ae3ba5ca70f977485fdb25ac5f8e7 Mon Sep 17 00:00:00 2001 From: Ben Peddell Date: Wed, 30 Apr 2025 21:49:49 +1000 Subject: [PATCH 1258/1714] Fix UNC path under Python 3.10 and older (#2568) Python 3.10 and older only treat full `\\host\share` UNC paths as being rooted at the path of that share, while Python 3.11 and newer also treat `\\host` UNC paths as being rooted at the path of the host. Extract the `\\host\share` of an NT UNC path as the drive letter rather than only the `\\host` part of the path. Fixes: de54f54 Handle alternate NT path forms Signed-off-by: Ben Peddell --- psutil/_pswindows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 12d54cb81e..022b83bf31 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -193,8 +193,8 @@ def convert_dos_path(s): return s rawdrive = '\\'.join(s.split('\\')[:3]) if rawdrive in {"\\??\\UNC", "\\Device\\Mup"}: - rawdrive = '\\'.join(s.split('\\')[:4]) - driveletter = "\\\\" + s.split('\\')[3] + rawdrive = '\\'.join(s.split('\\')[:5]) + driveletter = '\\\\' + '\\'.join(s.split('\\')[3:5]) elif rawdrive.startswith('\\??\\'): driveletter = s.split('\\')[2] else: From 077d9c66e18ce9bacd93de98490a1b3999e67eb8 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Sat, 3 May 2025 16:48:23 +0300 Subject: [PATCH 1259/1714] Migrate to new datetime API (#2569) Signed-off-by: Emmanuel Ferdman --- psutil/tests/test_sudo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py index e8a3ecc0af..051e7da579 100755 --- a/psutil/tests/test_sudo.py +++ b/psutil/tests/test_sudo.py @@ -38,7 +38,7 @@ def set_systime(secs): # secs since the epoch import pywintypes import win32api - dt = datetime.datetime.utcfromtimestamp(secs) + dt = datetime.datetime.fromtimestamp(secs, datetime.timezone.utc) try: win32api.SetSystemTime( dt.year, From c3495244d47b9d5f1c50f389d5c92770d1350b12 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 May 2025 17:03:43 +0200 Subject: [PATCH 1260/1714] [FreeBSD] drop support for FreeBSD 8 #2571 (#2572) --- HISTORY.rst | 9 ++++ docs/index.rst | 1 + psutil/_psbsd.py | 93 ++++++++------------------------- psutil/_psutil_bsd.c | 16 +++--- psutil/arch/bsd/proc.c | 6 --- psutil/arch/bsd/sys.c | 16 ++---- psutil/arch/freebsd/mem.c | 4 -- psutil/arch/freebsd/proc.c | 4 -- psutil/arch/freebsd/sys_socks.c | 3 -- 9 files changed, 43 insertions(+), 109 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 813a87ebf4..1ced829e59 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,11 @@ XXXX-XX-XX +**Enhancements** + +- 2571_, [FreeBSD]: Dropped support for FreeBSD 8 and earlier. FreeBSD 8 was + maintained from 2009 to 2013. + **Bug fixes** - 2473_, [macOS]: Fix build issue on macOS 11 and lower. @@ -33,6 +38,10 @@ XXXX-XX-XX RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien Stephan) +**Compatibility notes** + +- 2571_: Dropped support for FreeBSD 8 and earlier. + 7.0.0 ===== diff --git a/docs/index.rst b/docs/index.rst index 485fafa1ff..e5582e4d6b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2665,6 +2665,7 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= +* psutil 7.0.1 (XXXX-XX): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 * psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 * psutil 5.9.1 (2022-05): drop Python 2.6 diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 13bd926fab..bf1d811377 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -98,10 +98,7 @@ 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, @@ -240,36 +237,14 @@ def cpu_times(): return scputimes(user, nice, system, idle, irq) -if HAS_PER_CPU_TIMES: - - def per_cpu_times(): - """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. - # On FreeBSD < 8 we cannot gather per-cpu information, see: - # https://github.com/giampaolo/psutil/issues/226 - # If num cpus > 1, on first call we return single cpu times to avoid a - # crash at psutil import time. - # Next calls will fail with NotImplementedError - def per_cpu_times(): - """Return system CPU times as a namedtuple.""" - if cpu_count_logical() == 1: - return [cpu_times()] - if per_cpu_times.__called__: - msg = "supported only starting from FreeBSD 8" - raise NotImplementedError(msg) - per_cpu_times.__called__ = True - return [cpu_times()] - - per_cpu_times.__called__ = False +def per_cpu_times(): + """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 def cpu_count_logical(): @@ -785,7 +760,7 @@ def create_time(self): @wrap_exceptions def num_threads(self): if HAS_PROC_NUM_THREADS: - # FreeBSD + # FreeBSD / NetBSD return cext.proc_num_threads(self.pid) else: return len(self.threads()) @@ -870,14 +845,7 @@ def cwd(self): # it into None if OPENBSD and self.pid == 0: 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) - else: - raise NotImplementedError( - "supported only starting from FreeBSD 8" if FREEBSD else "" - ) + return cext.proc_cwd(self.pid) nt_mmap_grouped = namedtuple( 'mmap', 'path rss, private, ref_count, shadow_count' @@ -886,36 +854,19 @@ def cwd(self): '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 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 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: - self._assert_alive() - return ret + @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: - num_fds = _not_implemented + @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: + self._assert_alive() + return ret # --- FreeBSD only APIs diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 3f2973a0e9..34f951629b 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -59,30 +59,25 @@ static PyMethodDef mod_methods[] = { // --- per-process functions {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, - {"proc_name", psutil_proc_name, METH_VARARGS}, - {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS}, - {"proc_threads", psutil_proc_threads, METH_VARARGS}, -#if defined(PSUTIL_FREEBSD) - {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, -#endif {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, -#if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) + {"proc_environ", psutil_proc_environ, METH_VARARGS}, + {"proc_name", psutil_proc_name, METH_VARARGS}, {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, + {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS}, {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, -#endif + {"proc_threads", psutil_proc_threads, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) {"proc_num_threads", psutil_proc_num_threads, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) - {"cpu_topology", psutil_cpu_topology, METH_VARARGS}, {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, {"proc_exe", psutil_proc_exe, METH_VARARGS}, {"proc_getrlimit", psutil_proc_getrlimit, METH_VARARGS}, {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, + {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, {"proc_setrlimit", psutil_proc_setrlimit, METH_VARARGS}, #endif - {"proc_environ", psutil_proc_environ, METH_VARARGS}, // --- system-related functions {"boot_time", psutil_boot_time, METH_VARARGS}, @@ -102,6 +97,7 @@ static PyMethodDef mod_methods[] = { {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) + {"cpu_topology", psutil_cpu_topology, METH_VARARGS}, {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS}, #endif diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index eb699ef087..d11b7e055f 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -334,11 +334,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { // On NetBSD, we cannot call kvm_getenvv2() for a zombie process. // To make unittest suite happy, return an empty environment. #if defined(PSUTIL_FREEBSD) -#if (defined(__FreeBSD_version) && __FreeBSD_version >= 700000) if (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) { -#else - if ((p)->ki_flag & P_SYSTEM) { -#endif #elif defined(PSUTIL_NETBSD) if ((p)->p_stat == SZOMB) { #elif defined(PSUTIL_OPENBSD) @@ -417,7 +413,6 @@ psutil_proc_environ(PyObject *self, PyObject *args) { * utility has the same problem see: * https://github.com/giampaolo/psutil/issues/595 */ -#if (defined(__FreeBSD_version) && __FreeBSD_version >= 800000) || PSUTIL_OPENBSD || defined(PSUTIL_NETBSD) PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { pid_t pid; @@ -500,4 +495,3 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { free(freep); return NULL; } -#endif diff --git a/psutil/arch/bsd/sys.c b/psutil/arch/bsd/sys.c index 5911f7a53d..64ffd2b1e9 100644 --- a/psutil/arch/bsd/sys.c +++ b/psutil/arch/bsd/sys.c @@ -8,15 +8,9 @@ #include #include #include // OS version -#ifdef PSUTIL_FREEBSD - #if __FreeBSD_version < 900000 - #include - #else - #include - #endif -#elif PSUTIL_NETBSD +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) #include -#elif PSUTIL_OPENBSD +#elif defined(PSUTIL_OPENBSD) #include #endif @@ -48,7 +42,7 @@ psutil_users(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; -#if (defined(__FreeBSD_version) && (__FreeBSD_version < 900000)) || PSUTIL_OPENBSD +#if defined(PSUTIL_OPENBSD) struct utmp ut; FILE *fp; @@ -78,7 +72,7 @@ psutil_users(PyObject *self, PyObject *args) { py_tty, // tty py_hostname, // hostname (double)ut.ut_time, // start time -#if defined(PSUTIL_OPENBSD) || (defined(__FreeBSD_version) && __FreeBSD_version < 900000) +#if defined(PSUTIL_OPENBSD) -1 // process id (set to None later) #else ut.ut_pid // TODO: use PyLong_FromPid @@ -114,7 +108,7 @@ psutil_users(PyObject *self, PyObject *args) { py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); if (! py_hostname) goto error; -#ifdef PSUTIL_OPENBSD +#if defined(PSUTIL_OPENBSD) py_pid = Py_BuildValue("i", -1); // set to None later #else py_pid = PyLong_FromPid(utx->ut_pid); diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index ecf1ce5c71..8b08eeb3a6 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -31,11 +31,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { struct vmtotal vm; int mib[] = {CTL_VM, VM_METER}; long pagesize = psutil_getpagesize(); -#if __FreeBSD_version > 702101 long buffers; -#else - int buffers; -#endif size_t buffers_size = sizeof(buffers); if (sysctlbyname("hw.physmem", &total, &size, NULL, 0)) { diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 791e021e57..b03b43e6f6 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -349,7 +349,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -#if defined(__FreeBSD_version) && __FreeBSD_version >= 701000 PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { pid_t pid; @@ -397,10 +396,8 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { free(freep); return NULL; } -#endif -#if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 PyObject * psutil_proc_num_fds(PyObject *self, PyObject *args) { pid_t pid; @@ -424,7 +421,6 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { return Py_BuildValue("i", cnt); } -#endif PyObject * diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 212ff37e0a..9ddf796703 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -17,9 +17,6 @@ #include #include #include -#if defined(__FreeBSD_version) && __FreeBSD_version < 800000 -#include -#endif #include // for xinpcb struct #include #include From 4b14ca66ebc02f6b1870e68f91ff460426998d33 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 May 2025 17:25:13 +0200 Subject: [PATCH 1261/1714] test sudo: add test for process ident --- psutil/tests/test_sudo.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py index 051e7da579..fd46469e45 100755 --- a/psutil/tests/test_sudo.py +++ b/psutil/tests/test_sudo.py @@ -94,10 +94,16 @@ def test_proc_create_time(self): t1 = psutil.Process().create_time() self.update_systime() t2 = psutil.Process().create_time() - self.assertGreater(t2, t1) diff = int(t2 - t1) self.assertAlmostEqual(diff, 3600, delta=1) + def test_proc_ident(self): + p1 = psutil.Process() + self.update_systime() + p2 = psutil.Process() + self.assertEqual(p1._get_ident(), p2._get_ident()) + self.assertEqual(p1, p2) + @unittest.skipIf(not LINUX, "LINUX only") def test_linux_monotonic_proc_time(self): t1 = psutil.Process()._proc.create_time(monotonic=True) From 16499b0ee25dc5a288617e8d9e9d795c5c2ce5d2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 May 2025 23:34:25 +0200 Subject: [PATCH 1262/1714] setup.py: properly detect missing Python.h --- INSTALL.rst | 14 +++++-- scripts/internal/install-sysdeps.sh | 14 ++++--- setup.py | 57 ++++++++++++++++------------- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 86972586c4..45711e4ebc 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -47,6 +47,11 @@ RedHat / CentOS:: sudo yum install gcc python3-devel pip install --no-binary :all: psutil +Arch:: + + sudo pacman -S cmake gcc python3 + pip install --no-binary :all: psutil + Alpine:: sudo apk add gcc python3-dev musl-dev linux-headers @@ -62,11 +67,14 @@ MinGW is not supported. Once Visual Studio is installed do:: pip install --no-binary :all: psutil -macOS (build) -------------- +macOS +----- + +Install Xcode first: -Install `Xcode `__ then run:: +:: + xcode-select --install pip install --no-binary :all: psutil FreeBSD diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 0256901acd..444e7aa108 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -11,13 +11,14 @@ UNAME_S=$(uname -s) case "$UNAME_S" in Linux) - LINUX=true if command -v apt > /dev/null 2>&1; then - HAS_APT=true + HAS_APT=true # debian / ubuntu elif command -v yum > /dev/null 2>&1; then - HAS_YUM=true + HAS_YUM=true # redhat / centos + elif command -v pacman > /dev/null 2>&1; then + HAS_PACMAN=true # arch elif command -v apk > /dev/null 2>&1; then - HAS_APK=true # musl linux + HAS_APK=true # musl fi ;; FreeBSD) @@ -47,8 +48,10 @@ main() { $SUDO yum install -y python3-devel gcc $SUDO yum install -y net-tools coreutils util-linux # for tests $SUDO yum install -y sudo # for test-sudo + elif [ $HAS_PACMAN ]; then + $SUDO pacman -S --noconfirm python gcc sudo net-tools coreutils util-linux elif [ $HAS_APK ]; then - $SUDO apk add python3-dev gcc musl-dev linux-headers coreutils procps + $SUDO apk add --no-confirm python3-dev gcc musl-dev linux-headers coreutils procps elif [ $FREEBSD ]; then $SUDO pkg install -y python3 gcc elif [ $NETBSD ]; then @@ -59,6 +62,7 @@ main() { $SUDO pkg_add gcc python3 else echo "Unsupported platform: $UNAME_S" + return 1 fi } diff --git a/setup.py b/setup.py index abbacfcb89..080c22ec8d 100755 --- a/setup.py +++ b/setup.py @@ -192,46 +192,53 @@ def silenced_output(): yield -def missdeps(cmdline): - s = "psutil could not be installed from sources" - if not SUNOS and not shutil.which("gcc"): - s += " because gcc is not installed. " - else: - s += ". Perhaps Python header files are not installed. " - s += "Try running:\n" - s += " {}".format(cmdline) - print(hilite(s, color="red", bold=True), file=sys.stderr) +def has_python_h(): + include_dir = sysconfig.get_path("include") + return os.path.exists(os.path.join(include_dir, "Python.h")) -def print_install_instructions(): +def get_sysdeps(): if LINUX: pyimpl = "pypy" if PYPY else "python" if shutil.which("dpkg"): - missdeps("sudo apt-get install gcc {}3-dev".format(pyimpl)) + return "sudo apt-get install gcc {}3-dev".format(pyimpl) elif shutil.which("rpm"): - missdeps("sudo yum install gcc {}3-devel".format(pyimpl)) + return "sudo yum install gcc {}3-devel".format(pyimpl) + elif shutil.which("pacman"): + return "sudo pacman -S gcc python" elif shutil.which("apk"): - missdeps( - "sudo apk add gcc {}3-dev musl-dev linux-headers".format( - *pyimpl - ) + return "sudo apk add gcc {}3-dev musl-dev linux-headers".format( + *pyimpl ) elif MACOS: - msg = "XCode (https://developer.apple.com/xcode/) is not installed" - print(hilite(msg, color="red"), file=sys.stderr) + return "xcode-select --install" elif FREEBSD: if shutil.which("pkg"): - missdeps("pkg install gcc python3") + return "pkg install gcc python3" elif shutil.which("mport"): # MidnightBSD - missdeps("mport install gcc python3") + return "mport install gcc python3" elif OPENBSD: - missdeps("pkg_add -v gcc python3") + return "pkg_add -v gcc python3" elif NETBSD: - missdeps("pkgin install gcc python3") + return "pkgin install gcc python3" elif SUNOS: - missdeps( - "sudo ln -s /usr/bin/gcc /usr/local/bin/cc && pkg install gcc" - ) + return "pkg install gcc" + + +def print_install_instructions(): + reasons = [] + if not shutil.which("gcc"): + reasons.append("gcc is not installed.") + if not has_python_h(): + reasons.append("Python header files are not installed.") + if reasons: + sysdeps = get_sysdeps() + if sysdeps: + s = "psutil could not be compiled from sources. " + s += " ".join(reasons) + s += " Try running:\n" + s += " {}".format(sysdeps) + print(hilite(s, color="red", bold=True), file=sys.stderr) def unix_can_compile(c_code): From 99334e60d33f7cc261ef57be2e0fafa9574b8cdd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 May 2025 23:42:29 +0200 Subject: [PATCH 1263/1714] skip failing test on CI for now --- INSTALL.rst | 2 +- psutil/tests/test_sudo.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/INSTALL.rst b/INSTALL.rst index 45711e4ebc..8fb731b408 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -49,7 +49,7 @@ RedHat / CentOS:: Arch:: - sudo pacman -S cmake gcc python3 + sudo pacman -S cmake gcc python pip install --no-binary :all: psutil Alpine:: diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py index fd46469e45..b28e41dea0 100755 --- a/psutil/tests/test_sudo.py +++ b/psutil/tests/test_sudo.py @@ -19,6 +19,7 @@ from psutil import LINUX from psutil import MACOS from psutil import WINDOWS +from psutil.tests import CI_TESTING from psutil.tests import PsutilTestCase @@ -97,6 +98,7 @@ def test_proc_create_time(self): diff = int(t2 - t1) self.assertAlmostEqual(diff, 3600, delta=1) + @unittest.skipIf(CI_TESTING, "skipped for now") # TODO: fix it def test_proc_ident(self): p1 = psutil.Process() self.update_systime() From 742e9bb4434831ca60d7d784959887b8b82a1a23 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 8 May 2025 23:51:20 +0200 Subject: [PATCH 1264/1714] install-sysdeps.sh: don't error out if platform is not supported --- scripts/internal/install-sysdeps.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 444e7aa108..6c58f3831f 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -61,8 +61,7 @@ main() { elif [ $OPENBSD ]; then $SUDO pkg_add gcc python3 else - echo "Unsupported platform: $UNAME_S" - return 1 + echo "Unsupported platform '$UNAME_S'. Ignoring." fi } From 9b199cce618fe2da6e6419e53e23714f8edcc94e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 9 May 2025 23:30:32 +0200 Subject: [PATCH 1265/1714] fix sudo test by copy env vars for root user --- Makefile | 2 +- psutil/tests/__init__.py | 19 ++++++++++--------- psutil/tests/test_sudo.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 59376705ad..7c43b15eca 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ SETUP_INSTALL_ARGS = `$(PYTHON) -c \ "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 -SUDO = $(if $(filter $(OS),Windows_NT),,sudo) +SUDO = $(if $(filter $(OS),Windows_NT),,sudo -E) # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 60d1845a55..ff6856ed9f 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -4,7 +4,6 @@ """Test utilities.""" - import atexit import contextlib import ctypes @@ -1414,25 +1413,27 @@ def print_sysinfo(): bytes2human(swap.used), bytes2human(swap.total), ) + + # constants + constants = sorted( + [k for k, v in globals().items() if k.isupper() and v is True] + ) + info['constants'] = "\n ".join(constants) + + # processes info['pids'] = len(psutil.pids()) pinfo = psutil.Process().as_dict() pinfo.pop('memory_maps', None) + pinfo["environ"] = {k: os.environ[k] for k in sorted(os.environ)} info['proc'] = pprint.pformat(pinfo) + # print print("=" * 70, file=sys.stderr) # noqa: T201 for k, v in info.items(): print("{:<17} {}".format(k + ":", v), file=sys.stderr) # noqa: T201 print("=" * 70, file=sys.stderr) # noqa: T201 sys.stdout.flush() - # if WINDOWS: - # os.system("tasklist") - # elif shutil.which("ps"): - # os.system("ps aux") - # print("=" * 70, file=sys.stderr) - - sys.stdout.flush() - def is_win_secure_system_proc(pid): # see: https://github.com/giampaolo/psutil/issues/2338 diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py index b28e41dea0..24d1cffb3f 100755 --- a/psutil/tests/test_sudo.py +++ b/psutil/tests/test_sudo.py @@ -98,7 +98,7 @@ def test_proc_create_time(self): diff = int(t2 - t1) self.assertAlmostEqual(diff, 3600, delta=1) - @unittest.skipIf(CI_TESTING, "skipped for now") # TODO: fix it + @unittest.skipIf(CI_TESTING, "skipped on CI for now") # TODO: fix it def test_proc_ident(self): p1 = psutil.Process() self.update_systime() From dcff914c42942261bca13faf7a57228b9fd2c48e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 21 May 2025 13:32:02 +0200 Subject: [PATCH 1266/1714] Introduce dprint formatter (mostly for .yml files) (#2575) Let's introduce dprint to format .yml and .md files. dprint is a CLI tool similar to black and ruff format, extremely fast and overall awesome. --- .dprint.jsonc | 30 +++++++++ .github/ISSUE_TEMPLATE/bug.md | 12 ++-- .github/ISSUE_TEMPLATE/enhancement.md | 5 +- .github/PULL_REQUEST_TEMPLATE.md | 8 +-- .github/workflows/bsd.yml | 2 +- .github/workflows/build.yml | 93 +++++++++++++-------------- .github/workflows/issues.yml | 30 ++++----- CONTRIBUTING.md | 36 +++++------ HISTORY.rst | 1 + MANIFEST.in | 1 + Makefile | 17 +++++ SECURITY.md | 6 +- docs/.readthedocs.yaml | 2 +- scripts/internal/git_pre_commit.py | 10 +++ 14 files changed, 154 insertions(+), 99 deletions(-) create mode 100644 .dprint.jsonc diff --git a/.dprint.jsonc b/.dprint.jsonc new file mode 100644 index 0000000000..ccfcf2ddf4 --- /dev/null +++ b/.dprint.jsonc @@ -0,0 +1,30 @@ +{ + "markdown": { + "lineWidth": 79, + "textWrap": "always", + }, + "json": { + "indentWidth": 4, + "associations": [ + "**/*.json", + "**/*.jsonc", + ], + }, + "yaml": { + "associations": [ + "**/*.yml", + "**/*.yaml", + ], + }, + "excludes": [ + "**/*-lock.json", + ".github/ISSUE_TEMPLATE/bug.md", + ".github/ISSUE_TEMPLATE/enhancement.md", + ".github/PULL_REQUEST_TEMPLATE.md", + ], + "plugins": [ + "https://plugins.dprint.dev/markdown-0.18.0.wasm", + "https://plugins.dprint.dev/json-0.20.0.wasm", + "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm", + ], +} diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 24d01efa7a..7db9f45e83 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -3,15 +3,15 @@ name: Bug about: Report a bug title: "[OS] title" labels: 'bug' - --- + ## Summary -* OS: { type-or-version } -* Architecture: { 64bit, 32bit, ARM, PowerPC, s390 } -* Psutil version: { pip3 show psutil } -* Python version: { python3 -V } -* Type: { core, doc, performance, scripts, tests, wheels, new-api, installation } +- OS: { type-or-version } +- Architecture: { 64bit, 32bit, ARM, PowerPC, s390 } +- Psutil version: { pip3 show psutil } +- Python version: { python3 -V } +- Type: { core, doc, performance, scripts, tests, wheels, new-api, installation } ## Description diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 2f7d75a565..32b20a93fe 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -3,13 +3,12 @@ name: Enhancement about: Propose an enhancement labels: 'enhancement' title: "[OS] title" - --- ## Summary -* OS: { type-or-version } -* Type: { core, doc, performance, scripts, tests, wheels, new-api } +- OS: { type-or-version } +- Type: { core, doc, performance, scripts, tests, wheels, new-api } ## Description diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e8bbb2a4c5..dd506cb9fb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,9 @@ ## Summary -* OS: { type-or-version } -* Bug fix: { yes/no } -* Type: { core, doc, performance, scripts, tests, wheels, new-api } -* Fixes: { comma-separated list of issues fixed by this PR, if any } +- OS: { type-or-version } +- Bug fix: { yes/no } +- Type: { core, doc, performance, scripts, tests, wheels, new-api } +- Fixes: { comma-separated list of issues fixed by this PR, if any } ## Description diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 01e30ab4fe..a0406b9d2c 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -35,7 +35,7 @@ jobs: make test-ci netbsd: - if: false # XXX: disabled + if: false # XXX: disabled runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f835ee404d..4c0fe80988 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,47 +25,47 @@ jobs: fail-fast: false matrix: include: - - {os: ubuntu-latest, arch: x86_64} - - {os: ubuntu-latest, arch: i686} - - {os: ubuntu-24.04-arm, arch: aarch64} - - {os: macos-13, arch: x86_64} - - {os: macos-14, arch: arm64} - - {os: windows-2019, arch: AMD64} - - {os: windows-2019, arch: x86} + - { os: ubuntu-latest, arch: x86_64 } + - { os: ubuntu-latest, arch: i686 } + - { os: ubuntu-24.04-arm, arch: aarch64 } + - { os: macos-13, arch: x86_64 } + - { os: macos-14, arch: arm64 } + - { os: windows-2019, arch: AMD64 } + - { os: windows-2019, arch: x86 } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - # see https://cibuildwheel.pypa.io/en/stable/faq/#macos-building-cpython-38-wheels-on-arm64 - - name: "Install python 3.8 universal2 on macOS arm64" - if: runner.os == 'macOS' && runner.arch == 'ARM64' - uses: actions/setup-python@v5 - env: + # see https://cibuildwheel.pypa.io/en/stable/faq/#macos-building-cpython-38-wheels-on-arm64 + - name: "Install python 3.8 universal2 on macOS arm64" + if: runner.os == 'macOS' && runner.arch == 'ARM64' + uses: actions/setup-python@v5 + env: PIP_DISABLE_PIP_VERSION_CHECK: 1 - with: - python-version: 3.8 + with: + python-version: 3.8 - - uses: actions/setup-python@v5 - with: - python-version: 3.11 + - uses: actions/setup-python@v5 + with: + python-version: 3.11 - - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.22.0 - env: - CIBW_ARCHS: "${{ matrix.arch }}" - CIBW_ENABLE: "${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" + - name: Create wheels + run tests + uses: pypa/cibuildwheel@v2.22.0 + env: + CIBW_ARCHS: "${{ matrix.arch }}" + CIBW_ENABLE: "${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-${{ matrix.os }}-${{ matrix.arch }} - path: wheelhouse + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }}-${{ matrix.arch }} + path: wheelhouse - - name: Generate .tar.gz - if: matrix.os == 'ubuntu-latest' - run: | - make generate-manifest - python setup.py sdist - mv dist/psutil*.tar.gz wheelhouse/ + - name: Generate .tar.gz + if: matrix.os == 'ubuntu-latest' + run: | + make generate-manifest + python setup.py sdist + mv dist/psutil*.tar.gz wheelhouse/ # Test python 2.7 fallback installation message produced by setup.py py2-fallback: @@ -75,24 +75,23 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v4 - - uses: LizardByte/setup-python-action@master - with: - python-version: '2.7' - - run: python scripts/internal/test_python2_setup_py.py + - uses: actions/checkout@v4 + - uses: LizardByte/setup-python-action@master + with: + python-version: "2.7" + - run: python scripts/internal/test_python2_setup_py.py # Run linters linters: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - name: 'Run linters' - run: | - python3 -m pip install -U black==24.10.0 ruff rstcheck toml-sort sphinx - make lint-all + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: "Run linters" + run: | + make lint-ci # Produce wheels as artifacts. upload-wheels: diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index a9f665eb6b..037f1c9680 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -12,20 +12,20 @@ jobs: build: runs-on: ubuntu-latest steps: - # install python - - uses: actions/checkout@v4 - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' + # install python + - uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" - # install deps - - name: Install deps - run: python3 -m pip install PyGithub + # install deps + - name: Install deps + run: python3 -m pip install PyGithub - # run - - name: Run - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PYTHONUNBUFFERED=1 PYTHONWARNINGS=always python3 .github/workflows/issues.py + # run + - name: Run + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PYTHONUNBUFFERED=1 PYTHONWARNINGS=always python3 .github/workflows/issues.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 250398f8a8..b8da409f06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,35 +1,33 @@ -Contributing to psutil project -============================== +# Contributing to psutil project -Issues ------- +## Issues -* The issue tracker is for reporting problems or proposing enhancements related +- The issue tracker is for reporting problems or proposing enhancements related to the **program code**. -* Please do not open issues **asking for support**. Instead, use the forum at: +- Please do not open issues **asking for support**. Instead, use the forum at: https://groups.google.com/g/psutil. -* Before submitting a new issue, **search** if there are existing issues for +- Before submitting a new issue, **search** if there are existing issues for the same topic. -* **Be clear** in describing what the problem is and try to be accurate in +- **Be clear** in describing what the problem is and try to be accurate in editing the default issue **template**. There is a bot which automatically assigns **labels** based on issue's title and body format. Labels help - keeping the issues properly organized and searchable (by OS, issue type, etc.). -* When reporting a malfunction, consider enabling + keeping the issues properly organized and searchable (by OS, issue type, + etc.). +- When reporting a malfunction, consider enabling [debug mode](https://psutil.readthedocs.io/en/latest/#debug-mode) first. -* To report a **security vulnerability**, use the - [Tidelift security contact](https://tidelift.com/security). - Tidelift will coordinate the fix and the disclosure of the reported problem. +- To report a **security vulnerability**, use the + [Tidelift security contact](https://tidelift.com/security). Tidelift will + coordinate the fix and the disclosure of the reported problem. -Pull Requests -------------- +## Pull Requests -* The PR system is for fixing bugs or make enhancements related to the +- The PR system is for fixing bugs or make enhancements related to the **program code**. -* If you wish to implement a new feature or add support for a new platform it's +- If you wish to implement a new feature or add support for a new platform it's better to **discuss it first**, either on the issue tracker, the forum or via private email. -* In order to get acquainted with the code base and tooling, take a look at the +- In order to get acquainted with the code base and tooling, take a look at the **[Development Guide](https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst)**. -* If you can, remember to update +- If you can, remember to update [HISTORY.rst](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) and [CREDITS](https://github.com/giampaolo/psutil/blob/master/CREDITS) file. diff --git a/HISTORY.rst b/HISTORY.rst index 1ced829e59..69f0b909e5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,7 @@ XXXX-XX-XX - 2571_, [FreeBSD]: Dropped support for FreeBSD 8 and earlier. FreeBSD 8 was maintained from 2009 to 2013. +- 2575_: introduced `dprint` CLI tool to format .yml and .md files. **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index 2d59303420..c01c4e05e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +include .dprint.jsonc include .gitignore include CONTRIBUTING.md include CREDITS diff --git a/Makefile b/Makefile index 7c43b15eca..2e307eb836 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ SETUP_INSTALL_ARGS = `$(PYTHON) -c \ PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 SUDO = $(if $(filter $(OS),Windows_NT),,sudo -E) +DPRINT = ~/.dprint/bin/dprint # if make is invoked with no arg, default to `make help` .DEFAULT_GOAL := help @@ -71,6 +72,9 @@ install-pip: ## Install pip (no-op if already installed). install-sysdeps: ./scripts/internal/install-sysdeps.sh + curl -fsSL https://dprint.dev/install.sh | sh + $(DPRINT) upgrade # update dprint + $(DPRINT) config update -y # update plugins install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip @@ -172,6 +176,11 @@ test-ci: ## Run tests on GitHub CI. ${MAKE} test-memleaks ${MAKE} test-sudo +lint-ci: ## Run all linters on GitHub CI. + python3 -m pip install -U black==24.10.0 ruff rstcheck toml-sort sphinx + curl -fsSL https://dprint.dev/install.sh | sh + ${MAKE} lint-all + # =================================================================== # Linters # =================================================================== @@ -182,6 +191,9 @@ ruff: ## Run ruff linter. black: ## Run black formatter. @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe +dprint: + @$(DPRINT) check --list-different + lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py @@ -194,6 +206,7 @@ lint-toml: ## Run linter for pyproject.toml. lint-all: ## Run all linters ${MAKE} black ${MAKE} ruff + ${MAKE} dprint ${MAKE} lint-c ${MAKE} lint-rst ${MAKE} lint-toml @@ -219,10 +232,14 @@ fix-ruff: fix-toml: ## Fix pyproject.toml @git ls-files '*.toml' | xargs toml-sort +fix-dprint: + @$(DPRINT) fmt + fix-all: ## Run all code fixers. ${MAKE} fix-ruff ${MAKE} fix-black ${MAKE} fix-toml + ${MAKE} fix-dprint # =================================================================== # Distribution diff --git a/SECURITY.md b/SECURITY.md index 21dc174c7c..fda8e36955 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,6 +5,6 @@ it privately. **Do not disclose it as a public issue**. This gives me time to fix the issue before public exposure, reducing the chance that an exploit will be used before a patch is released. -To report a security vulnerability use the [Tidelift security -contact](https://tidelift.com/security). Tidelift will coordinate the fix and -the disclosure of the reported problem. +To report a security vulnerability use the +[Tidelift security contact](https://tidelift.com/security). Tidelift will +coordinate the fix and the disclosure of the reported problem. diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index 44ffd9687f..03ab738f20 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -17,4 +17,4 @@ sphinx: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: docs/requirements.txt + - requirements: docs/requirements.txt diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index a47d7987ee..17359fe76a 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -145,6 +145,13 @@ def rstcheck(files): return sys.exit("RST code didn't pass style check") +def dprint(): + print("running dprint") + cmd = ["dprint", "check", "--list-different"] + if subprocess.call(cmd) != 0: + return sys.exit("code didn't pass dprint check") + + def main(): py_files, c_files, rst_files, toml_files, new_rm_mv = git_commit_files() if py_files: @@ -156,6 +163,9 @@ def main(): rstcheck(rst_files) if toml_files: toml_sort(toml_files) + + dprint() + if new_rm_mv: out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) with open("MANIFEST.in", encoding="utf8") as f: From 550966b4d05e399755cd623d421d0a3748784db5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 27 May 2025 23:47:10 +0200 Subject: [PATCH 1267/1714] upgrade black to latest ver --- .github/workflows/issues.py | 1 - Makefile | 5 +---- docs/conf.py | 1 - psutil/__init__.py | 2 -- psutil/_common.py | 1 - psutil/_psaix.py | 1 - psutil/_psbsd.py | 1 - psutil/_pslinux.py | 1 - psutil/_psosx.py | 1 - psutil/_psposix.py | 1 - psutil/_pssunos.py | 1 - psutil/_pswindows.py | 1 - psutil/tests/__init__.py | 2 -- psutil/tests/__main__.py | 1 - psutil/tests/test_bsd.py | 1 - psutil/tests/test_connections.py | 1 - psutil/tests/test_contracts.py | 1 - psutil/tests/test_linux.py | 1 - psutil/tests/test_memleaks.py | 1 - psutil/tests/test_misc.py | 1 - psutil/tests/test_osx.py | 1 - psutil/tests/test_posix.py | 1 - psutil/tests/test_process.py | 7 +++---- psutil/tests/test_process_all.py | 1 - psutil/tests/test_scripts.py | 1 - psutil/tests/test_system.py | 1 - psutil/tests/test_testutils.py | 1 - psutil/tests/test_windows.py | 1 - pyproject.toml | 1 - scripts/cpu_distribution.py | 1 - scripts/ifconfig.py | 1 - scripts/internal/bench_oneshot.py | 1 - scripts/internal/bench_oneshot_2.py | 1 - scripts/internal/check_broken_links.py | 1 - scripts/internal/clinter.py | 1 - scripts/internal/convert_readme.py | 1 - scripts/internal/download_wheels.py | 1 - scripts/internal/generate_manifest.py | 1 - scripts/internal/git_pre_commit.py | 1 - scripts/internal/install_pip.py | 2 -- scripts/internal/print_announce.py | 1 - scripts/internal/print_api_speed.py | 1 - scripts/internal/print_downloads.py | 1 - scripts/internal/print_timeline.py | 1 - scripts/internal/purge_installation.py | 1 - scripts/internal/test_python2_setup_py.py | 1 - scripts/internal/winmake.py | 1 - scripts/iotop.py | 2 -- scripts/netstat.py | 1 - scripts/nettop.py | 2 -- scripts/procinfo.py | 1 - scripts/procsmem.py | 1 - scripts/top.py | 2 -- scripts/winservices.py | 1 - setup.py | 4 +--- 55 files changed, 5 insertions(+), 69 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 21c720ca52..c184e092d9 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -18,7 +18,6 @@ from github import Github - ROOT_DIR = os.path.realpath( os.path.join(os.path.dirname(__file__), '..', '..') ) diff --git a/Makefile b/Makefile index 2e307eb836..6c5bae932b 100644 --- a/Makefile +++ b/Makefile @@ -72,9 +72,6 @@ install-pip: ## Install pip (no-op if already installed). install-sysdeps: ./scripts/internal/install-sysdeps.sh - curl -fsSL https://dprint.dev/install.sh | sh - $(DPRINT) upgrade # update dprint - $(DPRINT) config update -y # update plugins install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip @@ -177,7 +174,7 @@ test-ci: ## Run tests on GitHub CI. ${MAKE} test-sudo lint-ci: ## Run all linters on GitHub CI. - python3 -m pip install -U black==24.10.0 ruff rstcheck toml-sort sphinx + python3 -m pip install -U black ruff rstcheck toml-sort sphinx curl -fsSL https://dprint.dev/install.sh | sh ${MAKE} lint-all diff --git a/docs/conf.py b/docs/conf.py index 604eeccb46..645fd6a8cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,6 @@ import datetime import os - PROJECT_NAME = "psutil" AUTHOR = "Giampaolo Rodola" THIS_YEAR = str(datetime.datetime.now().year) diff --git a/psutil/__init__.py b/psutil/__init__.py index f0e69610f4..b0b274a1b5 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -30,7 +30,6 @@ import threading import time - try: import pwd except ImportError: @@ -86,7 +85,6 @@ from ._common import memoize_when_activated from ._common import wrap_numbers as _wrap_numbers - if LINUX: # This is public API and it will be retrieved from _pslinux.py # via sys.modules. diff --git a/psutil/_common.py b/psutil/_common.py index 254468efd3..750d66a039 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -22,7 +22,6 @@ from socket import SOCK_DGRAM from socket import SOCK_STREAM - try: from socket import AF_INET6 except ImportError: diff --git a/psutil/_psaix.py b/psutil/_psaix.py index ba2725fdc1..0db127e6a3 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -29,7 +29,6 @@ from ._common import memoize_when_activated from ._common import usage_percent - __extra__all__ = ["PROCFS_PATH"] diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index bf1d811377..f185cc2e3b 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -29,7 +29,6 @@ from ._common import memoize_when_activated from ._common import usage_percent - __extra__all__ = [] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 06364ccba0..ac0d1d6f2b 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -47,7 +47,6 @@ from ._common import supports_ipv6 from ._common import usage_percent - # fmt: off __extra__all__ = [ 'PROCFS_PATH', diff --git a/psutil/_psosx.py b/psutil/_psosx.py index e253072f1c..37b2ba81eb 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -24,7 +24,6 @@ from ._common import parse_environ_block from ._common import usage_percent - __extra__all__ = [] diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 88703fdbd2..83f1acdbe3 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -16,7 +16,6 @@ from ._common import sdiskusage from ._common import usage_percent - if MACOS: from . import _psutil_osx diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 78d941cc5f..d9625df340 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -30,7 +30,6 @@ from ._common import socktype_to_enum from ._common import usage_percent - __extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 022b83bf31..077a01e4ce 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -33,7 +33,6 @@ from ._psutil_windows import NORMAL_PRIORITY_CLASS from ._psutil_windows import REALTIME_PRIORITY_CLASS - try: from . import _psutil_windows as cext except ImportError as err: diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ff6856ed9f..f54973ee2b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -36,7 +36,6 @@ from socket import AF_INET6 from socket import SOCK_STREAM - try: import pytest except ImportError: @@ -57,7 +56,6 @@ from psutil._common import print_color from psutil._common import supports_ipv6 - if POSIX: from psutil._psposix import wait_pid diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index ab94163828..d3ef8db582 100644 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -8,5 +8,4 @@ from psutil.tests import pytest - pytest.main() diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index e4ec5874cd..e5597e699c 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -29,7 +29,6 @@ from psutil.tests import spawn_subproc from psutil.tests import terminate - if BSD: from psutil._psutil_posix import getpagesize diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index c693fb845d..8d693bc9bf 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -42,7 +42,6 @@ from psutil.tests import unix_socketpair from psutil.tests import wait_for_file - SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index adb8019cdb..ad13f6c0eb 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -35,7 +35,6 @@ from psutil.tests import kernel_version from psutil.tests import pytest - # =================================================================== # --- APIs availability # =================================================================== diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index fbe7733e4c..d7e54dd7d0 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -45,7 +45,6 @@ from psutil.tests import sh from psutil.tests import skip_on_not_implemented - if LINUX: from psutil._pslinux import CLOCK_TICKS from psutil._pslinux import RootFsDeviceFinder diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 8d91378be7..5f64632d23 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -48,7 +48,6 @@ from psutil.tests import system_namespace from psutil.tests import terminate - cext = psutil._psplatform.cext thisproc = psutil.Process() FEW_TIMES = 5 diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index bc767d62a6..cfbfb3f0da 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -35,7 +35,6 @@ from psutil.tests import reload_module from psutil.tests import system_namespace - # =================================================================== # --- Test classes' repr(), str(), ... # =================================================================== diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 0c20986087..b7f532bd98 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -24,7 +24,6 @@ from psutil.tests import spawn_subproc from psutil.tests import terminate - if POSIX: from psutil._psutil_posix import getpagesize diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 7c5db8c3d0..f3e74b148b 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -34,7 +34,6 @@ from psutil.tests import spawn_subproc from psutil.tests import terminate - if POSIX: import mmap import resource diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 53878bbffe..937d291428 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -64,7 +64,6 @@ from psutil.tests import skip_on_not_implemented from psutil.tests import wait_for_pid - # =================================================================== # --- psutil.Process class tests # =================================================================== @@ -1519,9 +1518,9 @@ def clean_dict(d): for name in exclude: d.pop(name, None) return { - k.replace("\r", "").replace("\n", ""): v.replace( - "\r", "" - ).replace("\n", "") + k.replace("\r", "").replace("\n", ""): ( + v.replace("\r", "").replace("\n", "") + ) for k, v in d.items() } diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 1b66eccd93..2dccf2e1b4 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -38,7 +38,6 @@ from psutil.tests import process_namespace from psutil.tests import pytest - # 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 and not PYTEST_PARALLEL diff --git a/psutil/tests/test_scripts.py b/psutil/tests/test_scripts.py index 5d2afbe28a..48fb6eb3b9 100755 --- a/psutil/tests/test_scripts.py +++ b/psutil/tests/test_scripts.py @@ -31,7 +31,6 @@ from psutil.tests import psutil from psutil.tests import sh - INTERNAL_SCRIPTS_DIR = os.path.join(SCRIPTS_DIR, "internal") SETUP_PY = os.path.join(ROOT_DIR, 'setup.py') diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 5239d4c40b..b517b64176 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -51,7 +51,6 @@ from psutil.tests import pytest from psutil.tests import retry_on_failure - # =================================================================== # --- System-related API tests # =================================================================== diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 9a7faa6d95..5472651da5 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -56,7 +56,6 @@ from psutil.tests import wait_for_file from psutil.tests import wait_for_pid - # =================================================================== # --- Unit tests for test utilities. # =================================================================== diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 77e9d92efd..4a325347a2 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -35,7 +35,6 @@ from psutil.tests import spawn_subproc from psutil.tests import terminate - if WINDOWS and not PYPY: with warnings.catch_warnings(): warnings.simplefilter("ignore") diff --git a/pyproject.toml b/pyproject.toml index 965649d0bd..1e55315d31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,6 @@ ignore = [ [tool.ruff.lint.isort] # https://beta.ruff.rs/docs/settings/#isort force-single-line = true # one import per line -lines-after-imports = 2 [tool.coverage.report] exclude_lines = [ diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index ecb0b9c0d3..c88514f2e3 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -47,7 +47,6 @@ import psutil - if not hasattr(psutil.Process, "cpu_num"): sys.exit("platform not supported") diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index 3ab3c013b4..425a347826 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -48,7 +48,6 @@ import psutil from psutil._common import bytes2human - af_map = { socket.AF_INET: 'IPv4', socket.AF_INET6: 'IPv6', diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index f4a5eab98f..ee665fd81b 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -16,7 +16,6 @@ import psutil - ITERATIONS = 1000 # The list of Process methods which gets collected in one shot and diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index aa3ca78d11..4520683b69 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -14,7 +14,6 @@ import psutil - p = psutil.Process() diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/check_broken_links.py index 79cfd09a18..759ace0a78 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/check_broken_links.py @@ -49,7 +49,6 @@ import requests - HERE = os.path.abspath(os.path.dirname(__file__)) REGEX = re.compile( r'(?:http|ftp|https)?://' diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py index 4f0d99c31e..6c0428f848 100755 --- a/scripts/internal/clinter.py +++ b/scripts/internal/clinter.py @@ -10,7 +10,6 @@ import argparse import sys - warned = False diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index 9aa75b9074..c82c1a8ca6 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -11,7 +11,6 @@ import argparse import re - summary = """\ Quick links =========== diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py index 73cda80f1f..08e9618822 100755 --- a/scripts/internal/download_wheels.py +++ b/scripts/internal/download_wheels.py @@ -25,7 +25,6 @@ from psutil._common import bytes2human from psutil.tests import safe_rmpath - USER = "giampaolo" PROJECT = "psutil" OUTFILE = "wheels-github.zip" diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 1e261e5a42..1ed6e5fe91 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -10,7 +10,6 @@ import shlex import subprocess - SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') SKIP_FILES = () SKIP_PREFIXES = ('.ci/', '.github/') diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 17359fe76a..3d1baf9252 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -16,7 +16,6 @@ import subprocess import sys - PYTHON = sys.executable diff --git a/scripts/internal/install_pip.py b/scripts/internal/install_pip.py index bca5d5fe7b..e74f0ec2fd 100755 --- a/scripts/internal/install_pip.py +++ b/scripts/internal/install_pip.py @@ -6,7 +6,6 @@ import sys - try: import pip # noqa: F401 except ImportError: @@ -20,7 +19,6 @@ import tempfile from urllib.request import urlopen - URL = "https://bootstrap.pypa.io/get-pip.py" diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 8b62f2b713..4d9bc00a76 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -16,7 +16,6 @@ from psutil import __version__ - HERE = os.path.abspath(os.path.dirname(__file__)) ROOT = os.path.realpath(os.path.join(HERE, '..', '..')) HISTORY = os.path.join(ROOT, 'HISTORY.rst') diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 1fdbc83d39..6d394cdb95 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -78,7 +78,6 @@ import psutil from psutil._common import print_color - TIMES = 300 timings = [] templ = "{:<25} {:>10} {:>10}" diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 610a201822..ddbb7591f8 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -22,7 +22,6 @@ from psutil._common import memoize - AUTH_FILE = os.path.expanduser("~/.pypinfo.json") PKGNAME = 'psutil' DAYS = 30 diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py index 940724129b..54bfefe1d4 100755 --- a/scripts/internal/print_timeline.py +++ b/scripts/internal/print_timeline.py @@ -9,7 +9,6 @@ import shlex import subprocess - entry = """\ - {date}: `{ver} `__ - diff --git a/scripts/internal/purge_installation.py b/scripts/internal/purge_installation.py index 254adabe0e..7842900f0a 100755 --- a/scripts/internal/purge_installation.py +++ b/scripts/internal/purge_installation.py @@ -14,7 +14,6 @@ import shutil import site - PKGNAME = "psutil" diff --git a/scripts/internal/test_python2_setup_py.py b/scripts/internal/test_python2_setup_py.py index 6121844598..016dcfaa03 100755 --- a/scripts/internal/test_python2_setup_py.py +++ b/scripts/internal/test_python2_setup_py.py @@ -13,7 +13,6 @@ import subprocess import sys - ROOT_DIR = os.path.realpath( os.path.join(os.path.dirname(__file__), "..", "..") ) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index fb75a62b40..ae90353200 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -22,7 +22,6 @@ import subprocess import sys - PYTHON = os.getenv('PYTHON', sys.executable) HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) diff --git a/scripts/iotop.py b/scripts/iotop.py index 93f46d157d..056fecc6e2 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -32,7 +32,6 @@ import sys import time - try: import curses except ImportError: @@ -41,7 +40,6 @@ import psutil from psutil._common import bytes2human - win = curses.initscr() lineno = 0 diff --git a/scripts/netstat.py b/scripts/netstat.py index 56924e6ae8..5e8615f261 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -25,7 +25,6 @@ import psutil - AD = "-" AF_INET6 = getattr(socket, 'AF_INET6', object()) proto_map = { diff --git a/scripts/nettop.py b/scripts/nettop.py index 222771edc0..de24bff335 100755 --- a/scripts/nettop.py +++ b/scripts/nettop.py @@ -33,7 +33,6 @@ import sys import time - try: import curses except ImportError: @@ -42,7 +41,6 @@ import psutil from psutil._common import bytes2human - lineno = 0 win = curses.initscr() diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 4a328d5062..3f3afeeede 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -93,7 +93,6 @@ import psutil from psutil._common import bytes2human - ACCESS_DENIED = '' NON_VERBOSE_ITERATIONS = 4 RLIMITS_MAP = { diff --git a/scripts/procsmem.py b/scripts/procsmem.py index bf58f203da..cb7431db11 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -40,7 +40,6 @@ import psutil - if not (psutil.LINUX or psutil.MACOS or psutil.WINDOWS): sys.exit("platform not supported") diff --git a/scripts/top.py b/scripts/top.py index f772bdf00b..25395870c0 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -36,7 +36,6 @@ import sys import time - try: import curses except ImportError: @@ -45,7 +44,6 @@ import psutil from psutil._common import bytes2human - win = curses.initscr() lineno = 0 colors_map = dict(green=3, red=10, yellow=4) diff --git a/scripts/winservices.py b/scripts/winservices.py index 7df5894593..972e675cb8 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -35,7 +35,6 @@ import psutil - if os.name != 'nt': sys.exit("platform not supported (Windows only)") diff --git a/setup.py b/setup.py index 080c22ec8d..8054b6b1f2 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ import textwrap import warnings - if sys.version_info[0] == 2: sys.exit(textwrap.dedent("""\ As of version 7.0.0 psutil no longer supports Python 2.7, see: @@ -70,7 +69,6 @@ from _common import WINDOWS # noqa: E402 from _common import hilite # noqa: E402 - PYPY = '__pypy__' in sys.builtin_module_names PY36_PLUS = sys.version_info[:2] >= (3, 6) PY37_PLUS = sys.version_info[:2] >= (3, 7) @@ -95,7 +93,7 @@ # `make install-pydeps-dev`. DEV_DEPS = TEST_DEPS + [ "abi3audit", - "black==24.10.0", + "black", "check-manifest", "coverage", "packaging", From 69200e7973fb0759f10c8635658294327dd74ed9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 28 May 2025 00:53:32 +0200 Subject: [PATCH 1268/1714] fix doc CSS formatting which was completely messed up --- docs/conf.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 645fd6a8cc..fd0fe294aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -368,10 +368,10 @@ def get_version(): # texinfo_no_detailmenu = False -html_context = { - 'css_files': [ - 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', - 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', - '_static/css/custom.css', - ] -} +html_static_path = ['_static'] + +html_css_files = [ + 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', + 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', + 'css/custom.css', +] From bf660c09d150a8790e1b774338a7592666a584ba Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 28 May 2025 23:51:42 +0200 Subject: [PATCH 1269/1714] [NetBSD] `Process.create_time()`: account for time diff in case of system clock update (#2579) --- HISTORY.rst | 6 ++++-- psutil/__init__.py | 6 +++++- psutil/_psbsd.py | 32 ++++++++++++++++++++++++++++++-- psutil/tests/test_sudo.py | 4 ++++ 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 69f0b909e5..eda15b6432 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -32,12 +32,14 @@ XXXX-XX-XX updates because it uses a cached version of `boot_time()`_. - 2542_: if system clock is updated `Process.children()`_ and `Process.parent()`_ may not be able to return the right information. -- 2545_: [illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. +- 2545_: [Illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. - 2552_, [Windows]: `boot_time()`_ didn't take into account the time spent - during suspend / hybernation. + during suspend / hibernation. - 2560_, [Linux]: `Process.memory_maps()`_ may crash with `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien Stephan) +- 2578_, [NetBSD]: `Process.create_time()`_ did not reflect system clock + updates. We now include the diff based on initial `boot_time()`_. **Compatibility notes** diff --git a/psutil/__init__.py b/psutil/__init__.py index b0b274a1b5..77c428d1b4 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -375,7 +375,11 @@ def _get_ident(self): won't reuse the same PID after such a short period of time (0.01 secs). Technically this is inherently racy, but practically it should be good enough. + + NOTE: unreliable on FreeBSD and OpenBSD as ctime is subject to + system clock updates. """ + if WINDOWS: # Use create_time() fast method in order to speedup # `process_iter()`. This means we'll get AccessDenied for @@ -384,7 +388,7 @@ def _get_ident(self): # https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555 self._create_time = self._proc.create_time(fast_only=True) return (self.pid, self._create_time) - elif LINUX: + elif LINUX or NETBSD: # Use 'monotonic' process starttime since boot to form unique # process identity, since it is stable over changes to system # time. diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index f185cc2e3b..0273b1fa9a 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -479,6 +479,30 @@ def boot_time(): return cext.boot_time() +if NETBSD: + + try: + INIT_BOOT_TIME = boot_time() + except Exception as err: # noqa: BLE001 + # Don't want to crash at import time. + debug(f"ignoring exception on import: {err!r}") + INIT_BOOT_TIME = 0 + + def adjust_proc_create_time(ctime): + """Account for system clock updates.""" + if INIT_BOOT_TIME == 0: + return ctime + + diff = INIT_BOOT_TIME - boot_time() + if diff == 0 or abs(diff) < 1: + return ctime + + debug("system clock was updated; adjusting process create_time()") + if diff < 0: + return ctime - diff + return ctime + diff + + def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] @@ -753,8 +777,12 @@ def memory_info(self): memory_full_info = memory_info @wrap_exceptions - def create_time(self): - return self.oneshot()[kinfo_proc_map['create_time']] + def create_time(self, monotonic=False): + ctime = self.oneshot()[kinfo_proc_map['create_time']] + if NETBSD and not monotonic: + # NetBSD: ctime subject to system clock updates. + ctime = adjust_proc_create_time(ctime) + return ctime @wrap_exceptions def num_threads(self): diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py index 24d1cffb3f..41be186def 100755 --- a/psutil/tests/test_sudo.py +++ b/psutil/tests/test_sudo.py @@ -16,8 +16,10 @@ import unittest import psutil +from psutil import FREEBSD from psutil import LINUX from psutil import MACOS +from psutil import OPENBSD from psutil import WINDOWS from psutil.tests import CI_TESTING from psutil.tests import PsutilTestCase @@ -99,6 +101,8 @@ def test_proc_create_time(self): self.assertAlmostEqual(diff, 3600, delta=1) @unittest.skipIf(CI_TESTING, "skipped on CI for now") # TODO: fix it + @unittest.skipIf(OPENBSD, "broken on OPENBSD") # TODO: fix it + @unittest.skipIf(FREEBSD, "broken on FREEBSD") # TODO: fix it def test_proc_ident(self): p1 = psutil.Process() self.update_systime() From f285dea2709380c144dd7f0ff4fe920b0648dbcc Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Thu, 29 May 2025 21:51:01 +0200 Subject: [PATCH 1270/1714] ci: add Window ARM64 wheels (#2581) Signed-off-by: mayeut --- .github/workflows/build.yml | 3 ++- Makefile | 9 +++++++++ psutil/tests/__init__.py | 2 +- psutil/tests/test_windows.py | 5 +++++ pyproject.toml | 4 ++-- scripts/winservices.py | 5 +++++ 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c0fe80988..eb078038ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,6 +32,7 @@ jobs: - { os: macos-14, arch: arm64 } - { os: windows-2019, arch: AMD64 } - { os: windows-2019, arch: x86 } + - { os: windows-11-arm, arch: ARM64 } steps: - uses: actions/checkout@v4 @@ -49,7 +50,7 @@ jobs: python-version: 3.11 - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.3 env: CIBW_ARCHS: "${{ matrix.arch }}" CIBW_ENABLE: "${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" diff --git a/Makefile b/Makefile index 6c5bae932b..a57c4fe280 100644 --- a/Makefile +++ b/Makefile @@ -173,6 +173,15 @@ test-ci: ## Run tests on GitHub CI. ${MAKE} test-memleaks ${MAKE} test-sudo +test-cibuildwheel: ## Run tests from cibuildwheel. + # testing the wheels means we can't use other test targets which are rebuilding the python extensions + # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed + ${MAKE} install-sysdeps + mkdir -p .tests + cd .tests/ && python -c "from psutil.tests import print_sysinfo; print_sysinfo()" + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k test_memleaks.py" $(PYTHON) -m pytest --pyargs psutil.tests + lint-ci: ## Run all linters on GitHub CI. python3 -m pip install -U black ruff rstcheck toml-sort sphinx curl -fsSL https://dprint.dev/install.sh | sh diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f54973ee2b..ffaaec99fe 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -115,7 +115,7 @@ # are we a 64 bit process? IS_64BIT = sys.maxsize > 2**32 # apparently they're the same -AARCH64 = platform.machine() in {"aarch64", "arm64"} +AARCH64 = platform.machine().lower() in {"aarch64", "arm64"} RISCV64 = platform.machine() == "riscv64" diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 4a325347a2..e9042fcd49 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -885,6 +885,11 @@ def test_win_service_iter(self): "stopped", } for serv in psutil.win_service_iter(): + if serv.name() == "WaaSMedicSvc": + # known issue in Windows 11 reading the description + # https://learn.microsoft.com/en-us/answers/questions/1320388/in-windows-11-version-22h2-there-it-shows-(failed + # https://github.com/giampaolo/psutil/issues/2383 + continue data = serv.as_dict() assert isinstance(data['name'], str) assert data['name'].strip() diff --git a/pyproject.toml b/pyproject.toml index 1e55315d31..82de944ab3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,12 +221,12 @@ trailing_comma_inline_array = true [tool.cibuildwheel] skip = [ "*-musllinux*", - "cp313-win*", # pywin32 is not available on cp313 yet "cp3{7,8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures "pp*", ] test-extras = ["test"] -test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" test-ci" +test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" test-cibuildwheel" +test-skip = "cp39-win_arm64" # pywin32 is not available on cp39 ARM64 [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] diff --git a/scripts/winservices.py b/scripts/winservices.py index 972e675cb8..79d7c9c6e8 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -41,6 +41,11 @@ def main(): for service in psutil.win_service_iter(): + if service.name() == "WaaSMedicSvc": + # known issue in Windows 11 reading the description + # https://learn.microsoft.com/en-us/answers/questions/1320388/in-windows-11-version-22h2-there-it-shows-(failed + # https://github.com/giampaolo/psutil/issues/2383 + continue info = service.as_dict() print(f"{info['name']!r} ({info['display_name']!r})") s = "status: {}, start: {}, username: {}, pid: {}".format( From 54a84963641a8bedb520a54365bda2c2eb76cad4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 29 May 2025 21:54:04 +0200 Subject: [PATCH 1271/1714] update HISTORY.rst, #2581, @mayeut --- CREDITS | 2 +- HISTORY.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index d552d10d84..49ee86c3db 100644 --- a/CREDITS +++ b/CREDITS @@ -783,7 +783,7 @@ I: 1956 N: Matthieu Darbois W: https://github.com/mayeut -I: 2039, 2142, 2147, 2153, 2040, 2102, 2216, 2246, 2252 +I: 2039, 2142, 2147, 2153, 2040, 2102, 2216, 2246, 2252, 2581 N: Hugo van Kemenade W: https://github.com/hugovk diff --git a/HISTORY.rst b/HISTORY.rst index eda15b6432..b4679de439 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ XXXX-XX-XX **Enhancements** +- 2581_, [Windows]: publish ARM64 wheels. (patch by Matthieu Darbois) - 2571_, [FreeBSD]: Dropped support for FreeBSD 8 and earlier. FreeBSD 8 was maintained from 2009 to 2013. - 2575_: introduced `dprint` CLI tool to format .yml and .md files. From 3d21a43a47ab6f3c4a08d235d2a9a55d4adae9b1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 29 May 2025 23:57:19 +0200 Subject: [PATCH 1272/1714] OSX: adjust `Process.create_time()` for system clock updates + use monotonic time for proc ident (#2582) --- HISTORY.rst | 6 ++-- psutil/__init__.py | 2 +- psutil/_psosx.py | 69 +++++++++++++++++++--------------------------- 3 files changed, 31 insertions(+), 46 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b4679de439..ddbc6a2eef 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,8 +29,8 @@ XXXX-XX-XX - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will now raise `AccessDenied`_ instead. - 2540_, [macOS]: `boot_time()`_ is off by 45 seconds (C precision issue). -- 2541_, [Linux]: `Process.create_time()`_ does not reflect system clock - updates because it uses a cached version of `boot_time()`_. +- 2541_, 2570_, 2578_ [Linux], [macOS], [NetBSD]: `Process.create_time()`_ does + not reflect system clock updates. - 2542_: if system clock is updated `Process.children()`_ and `Process.parent()`_ may not be able to return the right information. - 2545_: [Illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. @@ -39,8 +39,6 @@ XXXX-XX-XX - 2560_, [Linux]: `Process.memory_maps()`_ may crash with `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien Stephan) -- 2578_, [NetBSD]: `Process.create_time()`_ did not reflect system clock - updates. We now include the diff based on initial `boot_time()`_. **Compatibility notes** diff --git a/psutil/__init__.py b/psutil/__init__.py index 77c428d1b4..2a47b57b78 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -388,7 +388,7 @@ def _get_ident(self): # https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555 self._create_time = self._proc.create_time(fast_only=True) return (self.pid, self._create_time) - elif LINUX or NETBSD: + elif LINUX or NETBSD or OSX: # Use 'monotonic' process starttime since boot to form unique # process identity, since it is stable over changes to system # time. diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 37b2ba81eb..04f226c9b8 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -288,6 +288,29 @@ def boot_time(): return cext.boot_time() +try: + INIT_BOOT_TIME = boot_time() +except Exception as err: # noqa: BLE001 + # Don't want to crash at import time. + debug(f"ignoring exception on import: {err!r}") + INIT_BOOT_TIME = 0 + + +def adjust_proc_create_time(ctime): + """Account for system clock updates.""" + if INIT_BOOT_TIME == 0: + return ctime + + diff = INIT_BOOT_TIME - boot_time() + if diff == 0 or abs(diff) < 1: + return ctime + + debug("system clock was updated; adjusting process create_time()") + if diff < 0: + return ctime - diff + return ctime + diff + + def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] @@ -335,44 +358,6 @@ def is_zombie(pid): return False -try: - initial_boot_time = boot_time() -except Exception as err: # noqa: BLE001 - # Don't want to crash at import time. - debug(f"ignoring exception on import: {err!r}") - initial_boot_time = None - - -def adjust_for_clock_changes(monotonic_time): - """Adjust the given monotonic time for any system clock changes. - - If the system clock has been updated since the initial boot, this - function will adjust the provided time to ensure it reflects the - correct wall-clock time. - - Args: - monotonic_time (float): The time in seconds since the system boot. - - Returns: - float: The adjusted time accounting for any system clock changes. - """ - if initial_boot_time is None: - return monotonic_time - - current_boot_time = boot_time() - - # If boot time has not changed, return the input time unchanged. - if current_boot_time == initial_boot_time: - return monotonic_time - - # The system clock was updated, adjust the time accordingly. - time_diff = abs(initial_boot_time - current_boot_time) - if current_boot_time > initial_boot_time: - return monotonic_time + time_diff - else: - return monotonic_time - time_diff - - def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. @@ -508,9 +493,11 @@ def cpu_times(self): ) @wrap_exceptions - def create_time(self): - monotonic_ctime = self._get_kinfo_proc()[kinfo_proc_map['ctime']] - return adjust_for_clock_changes(monotonic_ctime) + def create_time(self, monotonic=False): + ctime = self._get_kinfo_proc()[kinfo_proc_map['ctime']] + if not monotonic: + ctime = adjust_proc_create_time(ctime) + return ctime @wrap_exceptions def num_ctx_switches(self): From e7754af7400c0836a721befaba0f54c402c4ecb4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 14 Jun 2025 12:13:58 +0200 Subject: [PATCH 1273/1714] make flaky test_long_cmdline() more robust --- psutil/tests/test_process.py | 9 +++++++-- psutil/tests/test_sudo.py | 2 -- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 937d291428..6e6fef5964 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -747,15 +747,20 @@ def test_long_cmdline(self): ["-c", "import time; [time.sleep(0.1) for x in range(100)]"] ) p = self.spawn_psproc(cmdline) + + # XXX - flaky test: exclude the python exe which, for some + # reason, and only sometimes, on OSX appears different. + cmdline = cmdline[1:] + if OPENBSD: # XXX: for some reason the test process may turn into a # zombie (don't know why). try: - assert p.cmdline() == cmdline + assert p.cmdline()[1:] == cmdline except psutil.ZombieProcess: raise pytest.skip("OPENBSD: process turned into zombie") else: - ret = p.cmdline() + ret = p.cmdline()[1:] if NETBSD and ret == []: # https://github.com/giampaolo/psutil/issues/2250 raise pytest.skip("OPENBSD: returned EBUSY") diff --git a/psutil/tests/test_sudo.py b/psutil/tests/test_sudo.py index 41be186def..034b763faf 100755 --- a/psutil/tests/test_sudo.py +++ b/psutil/tests/test_sudo.py @@ -18,7 +18,6 @@ import psutil from psutil import FREEBSD from psutil import LINUX -from psutil import MACOS from psutil import OPENBSD from psutil import WINDOWS from psutil.tests import CI_TESTING @@ -89,7 +88,6 @@ def test_boot_time(self): self.assertAlmostEqual(diff, 3600, delta=1) @unittest.skipIf(WINDOWS, "broken on WINDOWS") # TODO: fix it - @unittest.skipIf(MACOS, "broken on MACOS") # TODO: fix it def test_proc_create_time(self): # Test that Process.create_time() reflects system clock # updates. On systems such as Linux this is added on top of the From 0e921c1cab637aaf3c4194daf3533c0e0b146e7d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 14 Jun 2025 14:30:37 +0200 Subject: [PATCH 1274/1714] fix compilation err on PYPY --- psutil/arch/all/init.c | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c index b72dcbf6f8..891e017c7f 100644 --- a/psutil/arch/all/init.c +++ b/psutil/arch/all/init.c @@ -11,6 +11,7 @@ #include #endif +#include "init.h" int PSUTIL_DEBUG = 0; int PSUTIL_CONN_NONE = 128; From 081312c70a6ba30815e22024135e7cda189bf5a3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 14 Jun 2025 14:36:13 +0200 Subject: [PATCH 1275/1714] re-enable some old PYPY tests --- psutil/tests/test_linux.py | 3 --- psutil/tests/test_process.py | 21 --------------------- psutil/tests/test_system.py | 4 ---- psutil/tests/test_unicode.py | 1 - 4 files changed, 29 deletions(-) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d7e54dd7d0..d04df0b2c4 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -31,7 +31,6 @@ 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 RISCV64 from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM @@ -1865,8 +1864,6 @@ def test_parse_smaps_mocked(self): assert pss == 3 * 1024 assert swap == 15 * 1024 - # On PYPY file descriptors are not closed fast enough. - @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") def test_open_files_mode(self): def get_test_file(fname): p = psutil.Process() diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 6e6fef5964..e47443e35e 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -739,7 +739,6 @@ def test_cmdline(self): return assert ' '.join(p.cmdline()) == ' '.join(cmdline) - @pytest.mark.skipif(PYPY, reason="broken on PYPY") def test_long_cmdline(self): cmdline = [PYTHON_EXE] cmdline.extend(["-v"] * 50) @@ -772,7 +771,6 @@ def test_name(self): pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) - @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") @retry_on_failure() def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) @@ -800,25 +798,6 @@ def test_long_name(self): else: assert p.name() == os.path.basename(pyexe) - # XXX: fails too often - # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") - # @pytest.mark.skipif(AIX, reason="broken on AIX") - # @pytest.mark.skipif(PYPY, reason="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(0.1) for x in range(100)]", - # ] - # p = self.spawn_psproc(cmdline) - # assert p.cmdline() == cmdline - # assert p.name() == os.path.basename(pyexe) - # assert os.path.normcase(p.exe()) == os.path.normcase(pyexe) - @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_uids(self): p = psutil.Process() diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index b517b64176..027516f624 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -42,7 +42,6 @@ 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 @@ -612,9 +611,6 @@ def test_getloadavg(self): class TestDiskAPIs(PsutilTestCase): - @pytest.mark.skipif( - PYPY and not IS_64BIT, reason="unreliable on PYPY32 + 32BIT" - ) def test_disk_usage(self): usage = psutil.disk_usage(os.getcwd()) assert usage._fields == ('total', 'used', 'free', 'percent') diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index e84ed89a3a..79e7581959 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -263,7 +263,6 @@ def test_disk_usage(self): psutil.disk_usage(dname) @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") - @pytest.mark.skipif(PYPY, reason="unstable on PYPY") def test_memory_maps(self): with copyload_shared_lib(suffix=self.funky_suffix) as funky_path: From 6ee112e359893d74d91d2b5546b27f0cf48c6a77 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Mon, 23 Jun 2025 19:21:11 +0200 Subject: [PATCH 1276/1714] Remove global vars from FreeBSD net_connections() (#2589) Signed-off-by: Lysandros Nikolaou --- psutil/arch/netbsd/socks.c | 66 ++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index 200b816836..c019f3a094 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -33,9 +33,7 @@ struct kif { int has_buf; }; -// kinfo_file results list -SLIST_HEAD(kifhead, kif) kihead = SLIST_HEAD_INITIALIZER(kihead); - +SLIST_HEAD(kifhead, kif); // kinfo_pcb results struct kpcb { @@ -45,53 +43,38 @@ struct kpcb { int has_buf; }; -// kinfo_pcb results list -SLIST_HEAD(kpcbhead, kpcb) kpcbhead = SLIST_HEAD_INITIALIZER(kpcbhead); - - -// Initialize kinfo_file results list. -static void -psutil_kiflist_init(void) { - SLIST_INIT(&kihead); -} +SLIST_HEAD(kpcbhead, kpcb); // Clear kinfo_file results list. static void -psutil_kiflist_clear(void) { - while (!SLIST_EMPTY(&kihead)) { - struct kif *kif = SLIST_FIRST(&kihead); +psutil_kiflist_clear(struct kifhead *kifhead) { + while (!SLIST_EMPTY(kifhead)) { + struct kif *kif = SLIST_FIRST(kifhead); if (kif->has_buf == 1) free(kif->buf); free(kif); - SLIST_REMOVE_HEAD(&kihead, kifs); + SLIST_REMOVE_HEAD(kifhead, kifs); } } -// Initialize kinof_pcb result list. -static void -psutil_kpcblist_init(void) { - SLIST_INIT(&kpcbhead); -} - - // Clear kinof_pcb result list. static void -psutil_kpcblist_clear(void) { - while (!SLIST_EMPTY(&kpcbhead)) { - struct kpcb *kpcb = SLIST_FIRST(&kpcbhead); +psutil_kpcblist_clear(struct kpcbhead *kpcbhead) { + while (!SLIST_EMPTY(kpcbhead)) { + struct kpcb *kpcb = SLIST_FIRST(kpcbhead); if (kpcb->has_buf == 1) free(kpcb->buf); free(kpcb); - SLIST_REMOVE_HEAD(&kpcbhead, kpcbs); + SLIST_REMOVE_HEAD(kpcbhead, kpcbs); } } // Get all open files including socket. static int -psutil_get_files(void) { +psutil_get_files(struct kifhead *kifhead) { size_t len; size_t j; int mib[6]; @@ -142,7 +125,7 @@ psutil_get_files(void) { kif->has_buf = 0; kif->buf = NULL; } - SLIST_INSERT_HEAD(&kihead, kif, kifs); + SLIST_INSERT_HEAD(kifhead, kif, kifs); } } else { @@ -160,7 +143,7 @@ psutil_get_files(void) { // Get open sockets. static int -psutil_get_sockets(const char *name) { +psutil_get_sockets(const char *name, struct kpcbhead *kpcbhead) { size_t namelen; int mib[8]; struct kinfo_pcb *pcb = NULL; @@ -212,7 +195,7 @@ psutil_get_sockets(const char *name) { kpcb->has_buf = 0; kpcb->buf = NULL; } - SLIST_INSERT_HEAD(&kpcbhead, kpcb, kpcbs); + SLIST_INSERT_HEAD(kpcbhead, kpcb, kpcbs); } } else { @@ -337,12 +320,17 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } - psutil_kiflist_init(); - psutil_kpcblist_init(); + // kinfo_file results list + struct kifhead kifhead; + SLIST_INIT(&kifhead); + + // kinfo_pcb results list + struct kpcbhead kpcbhead; + SLIST_INIT(&kpcbhead); - if (psutil_get_files() != 0) + if (psutil_get_files(&kifhead) != 0) goto error; - if (psutil_get_info(kind) != 0) + if (psutil_get_info(kind, &kpcbhead) != 0) goto error; struct kif *k; @@ -446,13 +434,13 @@ psutil_net_connections(PyObject *self, PyObject *args) { } } - psutil_kiflist_clear(); - psutil_kpcblist_clear(); + psutil_kiflist_clear(&kifhead); + psutil_kpcblist_clear(&kpcbhead); return py_retlist; error: - psutil_kiflist_clear(); - psutil_kpcblist_clear(); + psutil_kiflist_clear(&kifhead); + psutil_kpcblist_clear(&kpcbhead); Py_DECREF(py_retlist); Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); From 028f3eb2b5e90fd60d7f87963a1ae9e3c86722d7 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Wed, 25 Jun 2025 17:26:14 +0200 Subject: [PATCH 1277/1714] Fix thread-safety issues in getloadavg on Windows (#2591) Lock around reading & writing of global state in `getloadavg` on Windows. Signed-off-by: Lysandros Nikolaou --- psutil/_pswindows.py | 26 ++++++++++++++++++-------- psutil/arch/windows/wmi.c | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 077a01e4ce..c6c8fffa4b 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -10,6 +10,7 @@ import os import signal import sys +import threading import time from collections import namedtuple @@ -330,22 +331,31 @@ def cpu_freq(): return [_common.scpufreq(float(curr), min_, float(max_))] -_loadavg_inititialized = False +_loadavg_initialized = False +_lock = threading.Lock() + + +def _getloadavg_impl(): + # Drop to 2 decimal points which is what Linux does + raw_loads = cext.getloadavg() + return tuple(round(load, 2) for load in raw_loads) 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 + global _loadavg_initialized - if not _loadavg_inititialized: - cext.init_loadavg_counter() - _loadavg_inititialized = True + if _loadavg_initialized: + return _getloadavg_impl() - # Drop to 2 decimal points which is what Linux does - raw_loads = cext.getloadavg() - return tuple(round(load, 2) for load in raw_loads) + with _lock: + if not _loadavg_initialized: + cext.init_loadavg_counter() + _loadavg_initialized = True + + return _getloadavg_impl() # ===================================================================== diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 5db4f24883..04ba3ee4c5 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -31,6 +31,15 @@ double load_avg_1m = 0; double load_avg_5m = 0; double load_avg_15m = 0; +#ifdef Py_GIL_DISABLED + static PyMutex mutex; + #define MUTEX_LOCK(m) PyMutex_Lock(m) + #define MUTEX_UNLOCK(m) PyMutex_Unlock(m) +#else + #define MUTEX_LOCK(m) + #define MUTEX_UNLOCK(m) +#endif + VOID CALLBACK LoadAvgCallback(PVOID hCounter, BOOLEAN timedOut) { PDH_FMT_COUNTERVALUE displayValue; @@ -45,12 +54,14 @@ VOID CALLBACK LoadAvgCallback(PVOID hCounter, BOOLEAN timedOut) { } currentLoad = displayValue.doubleValue; + MUTEX_LOCK(&mutex); load_avg_1m = load_avg_1m * LOADAVG_FACTOR_1F + currentLoad * \ (1.0 - LOADAVG_FACTOR_1F); load_avg_5m = load_avg_5m * LOADAVG_FACTOR_5F + currentLoad * \ (1.0 - LOADAVG_FACTOR_5F); load_avg_15m = load_avg_15m * LOADAVG_FACTOR_15F + currentLoad * \ (1.0 - LOADAVG_FACTOR_15F); + MUTEX_UNLOCK(&mutex); } @@ -116,5 +127,10 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { */ PyObject * psutil_get_loadavg(PyObject *self, PyObject *args) { - return Py_BuildValue("(ddd)", load_avg_1m, load_avg_5m, load_avg_15m); + MUTEX_LOCK(&mutex); + double load_avg_1m_l = load_avg_1m; + double load_avg_5m_l = load_avg_5m; + double load_avg_15m_l = load_avg_15m; + MUTEX_UNLOCK(&mutex); + return Py_BuildValue("(ddd)", load_avg_1m_l, load_avg_5m_l, load_avg_15m_l); } From d6a8a72e35ce1f6cb7e9a14bcd0952881fbab3b1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 Jun 2025 21:53:00 +0200 Subject: [PATCH 1278/1714] stabilize some tests --- psutil/tests/__init__.py | 4 ++-- psutil/tests/test_misc.py | 2 +- psutil/tests/test_posix.py | 37 +++++++++++++++++++++----------- psutil/tests/test_process_all.py | 3 +-- psutil/tests/test_unicode.py | 2 +- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ffaaec99fe..6465be8cad 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1071,7 +1071,7 @@ def assert_proc_gone(self, proc): self.assert_pid_gone(proc.pid) ns = process_namespace(proc) for fun, name in ns.iter(ns.all, clear_cache=True): - with self.subTest(proc=proc, name=name): + with self.subTest(proc=str(proc), name=name): try: ret = fun() except psutil.ZombieProcess: @@ -1109,7 +1109,7 @@ def assert_proc_zombie(self, proc): # 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): + with self.subTest(proc=str(proc), name=name): try: fun() except (psutil.ZombieProcess, psutil.AccessDenied) as exc: diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index cfbfb3f0da..5236d661f6 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -255,7 +255,7 @@ def check(ret): ns = process_namespace(proc) for fun, name in ns.iter(ns.getters, clear_cache=True): - with self.subTest(proc=proc, name=name): + with self.subTest(proc=str(proc), name=name): try: ret = fun() except psutil.Error: diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index f3e74b148b..c220524bd6 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -356,23 +356,36 @@ def test_nic_names(self): f" output\n{output}" ) - # @pytest.mark.skipif(CI_TESTING and not psutil.users(), - # reason="unreliable on CI") @retry_on_failure() def test_users(self): out = sh("who -u") if not out.strip(): raise pytest.skip("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] - assert len(users) == len(psutil.users()) - with self.subTest(psutil=psutil.users(), who=out): - for idx, u in enumerate(psutil.users()): - assert u.name == users[idx] - assert u.terminal == terminals[idx] - if u.pid is not None: # None on OpenBSD - psutil.Process(u.pid) + + susers = [] + for line in out.splitlines(): + user = line.split()[0] + terminal = line.split()[1] + if LINUX or MACOS: + try: + pid = int(line.split()[-2]) + except ValueError: + pid = int(line.split()[-1]) + susers.append((user, terminal, pid)) + else: + susers.append((user, terminal)) + + if LINUX or MACOS: + pusers = [(u.name, u.terminal, u.pid) for u in psutil.users()] + else: + pusers = [(u.name, u.terminal) for u in psutil.users()] + + assert len(susers) == len(pusers) + assert sorted(susers) == sorted(pusers) + + for user in psutil.users(): + if user.pid is not None: + assert user.pid > 0 @retry_on_failure() def test_users_started(self): diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 2dccf2e1b4..f7d1e32e03 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -530,5 +530,4 @@ def check(pid): # Process class and is querable like a PID (process # ID). Skip it. continue - with self.subTest(pid=pid): - check(pid) + check(pid) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index e84ed89a3a..881081d4ab 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -287,7 +287,7 @@ class TestFSAPIsWithInvalidPath(TestFSAPIs): funky_suffix = INVALID_UNICODE_SUFFIX def expect_exact_path_match(self): - return True + return not MACOS # =================================================================== From 990f7812d3b8737434dbe97aaae9c75a831f99ce Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 3 Jul 2025 15:19:39 +0200 Subject: [PATCH 1279/1714] Move global state in FreeBSD net_connections implementation into function (#2588) Signed-off-by: Lysandros Nikolaou --- psutil/arch/freebsd/sys_socks.c | 67 ++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 9ddf796703..00efb501d7 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -27,40 +27,35 @@ #include "../../_psutil_posix.h" -static struct xfile *psutil_xfiles; -static int psutil_nxfiles; - - static int -psutil_populate_xfiles(void) { - size_t len; +psutil_populate_xfiles(struct xfile **psutil_xfiles, int *psutil_nxfiles) { + struct xfile *new_psutil_xfiles; + size_t len = sizeof(struct xfile); - if ((psutil_xfiles = malloc(len = sizeof *psutil_xfiles)) == NULL) { - PyErr_NoMemory(); - return 0; - } - while (sysctlbyname("kern.file", psutil_xfiles, &len, 0, 0) == -1) { + while (sysctlbyname("kern.file", *psutil_xfiles, &len, 0, 0) == -1) { if (errno != ENOMEM) { PyErr_SetFromErrno(0); return 0; } len *= 2; - if ((psutil_xfiles = realloc(psutil_xfiles, len)) == NULL) { + new_psutil_xfiles = realloc(*psutil_xfiles, len); + if (new_psutil_xfiles == NULL) { PyErr_NoMemory(); return 0; } + *psutil_xfiles = new_psutil_xfiles; } - if (len > 0 && psutil_xfiles->xf_size != sizeof *psutil_xfiles) { + if (len > 0 && (*psutil_xfiles)->xf_size != sizeof(struct xfile)) { PyErr_Format(PyExc_RuntimeError, "struct xfile size mismatch"); return 0; } - psutil_nxfiles = len / sizeof *psutil_xfiles; + *psutil_nxfiles = len / sizeof(struct xfile); return 1; } static struct xfile * -psutil_get_file_from_sock(kvaddr_t sock) { +psutil_get_file_from_sock(kvaddr_t sock, struct xfile *psutil_xfiles, int psutil_nxfiles) { struct xfile *xf; int n; @@ -76,7 +71,8 @@ psutil_get_file_from_sock(kvaddr_t sock) { // https://github.com/freebsd/freebsd/blob/master/usr.bin/sockstat/sockstat.c static int psutil_gather_inet( - int proto, int include_v4, int include_v6, PyObject *py_retlist) + int proto, int include_v4, int include_v6, PyObject *py_retlist, + struct xfile *psutil_xfiles, int psutil_nxfiles) { struct xinpgen *xig, *exig; struct xinpcb *xip; @@ -186,7 +182,7 @@ psutil_gather_inet( char lip[200], rip[200]; - xf = psutil_get_file_from_sock(so->xso_so); + xf = psutil_get_file_from_sock(so->xso_so, psutil_xfiles, psutil_nxfiles); if (xf == NULL) continue; lport = ntohs(inp->inp_lport); @@ -243,7 +239,8 @@ psutil_gather_inet( static int -psutil_gather_unix(int proto, PyObject *py_retlist) { +psutil_gather_unix(int proto, PyObject *py_retlist, + struct xfile *psutil_xfiles, int psutil_nxfiles) { struct xunpgen *xug, *exug; struct xunpcb *xup; const char *varname = NULL; @@ -308,7 +305,7 @@ psutil_gather_unix(int proto, PyObject *py_retlist) { if (xup->xu_len != sizeof *xup) goto error; - xf = psutil_get_file_from_sock(xup->xu_socket.xso_so); + xf = psutil_get_file_from_sock(xup->xu_socket.xso_so, psutil_xfiles, psutil_nxfiles); if (xf == NULL) continue; @@ -363,7 +360,8 @@ psutil_int_in_seq(int value, PyObject *py_seq) { PyObject* psutil_net_connections(PyObject* self, PyObject* args) { - int include_v4, include_v6, include_unix, include_tcp, include_udp; + int include_v4, include_v6, include_unix, include_tcp, include_udp, psutil_nxfiles; + struct xfile *psutil_xfiles; PyObject *py_af_filter = NULL; PyObject *py_type_filter = NULL; PyObject *py_retlist = PyList_New(0); @@ -389,38 +387,47 @@ psutil_net_connections(PyObject* self, PyObject* args) { if ((include_udp = psutil_int_in_seq(SOCK_DGRAM, py_type_filter)) == -1) goto error; - if (psutil_populate_xfiles() != 1) + psutil_xfiles = malloc(sizeof(struct xfile)); + if (psutil_xfiles == NULL) { + PyErr_NoMemory(); goto error; + } + + if (psutil_populate_xfiles(&psutil_xfiles, &psutil_nxfiles) != 1) + goto error_free_psutil_xfiles; // TCP if (include_tcp == 1) { if (psutil_gather_inet( - IPPROTO_TCP, include_v4, include_v6, py_retlist) == 0) + IPPROTO_TCP, include_v4, include_v6, py_retlist, + psutil_xfiles, psutil_nxfiles) == 0) { - goto error; + goto error_free_psutil_xfiles; } } // UDP if (include_udp == 1) { if (psutil_gather_inet( - IPPROTO_UDP, include_v4, include_v6, py_retlist) == 0) + IPPROTO_UDP, include_v4, include_v6, py_retlist, + psutil_xfiles, psutil_nxfiles) == 0) { - goto error; + goto error_free_psutil_xfiles; } } // UNIX if (include_unix == 1) { - if (psutil_gather_unix(SOCK_STREAM, py_retlist) == 0) - goto error; - if (psutil_gather_unix(SOCK_DGRAM, py_retlist) == 0) - goto error; + if (psutil_gather_unix(SOCK_STREAM, py_retlist, psutil_xfiles, psutil_nxfiles) == 0) + goto error_free_psutil_xfiles; + if (psutil_gather_unix(SOCK_DGRAM, py_retlist, psutil_xfiles, psutil_nxfiles) == 0) + goto error_free_psutil_xfiles; } free(psutil_xfiles); return py_retlist; +error_free_psutil_xfiles: + free(psutil_xfiles); error: Py_DECREF(py_retlist); - free(psutil_xfiles); return NULL; } From 2317a6d254a83a9ee140b44e8ca86b01aef2dee0 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:12:20 -0400 Subject: [PATCH 1280/1714] CI: upgrade LizardByte/setup-python-action (#2602) LizardByte/setup-python-action is deprecated and has moved to LizardByte/actions. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb078038ff..6a801cc0e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: fail-fast: false steps: - uses: actions/checkout@v4 - - uses: LizardByte/setup-python-action@master + - uses: LizardByte/actions/actions/setup_python@master with: python-version: "2.7" - run: python scripts/internal/test_python2_setup_py.py From f8e2b202dc731589e07fa567d35fc7fc67afa2fa Mon Sep 17 00:00:00 2001 From: Irene Sheen Date: Fri, 1 Aug 2025 13:17:30 +0530 Subject: [PATCH 1281/1714] Fix negative value for psutil.sensors_battery().secsleft (#2606) --- HISTORY.rst | 2 ++ psutil/_pslinux.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index ddbc6a2eef..fd1cccff49 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -39,6 +39,8 @@ XXXX-XX-XX - 2560_, [Linux]: `Process.memory_maps()`_ may crash with `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien Stephan) +- 2605_, [Linux]: `psutil.sensors_battery()` reports a negative amount for + seconds left. **Compatibility notes** diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ac0d1d6f2b..232a701406 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1524,7 +1524,7 @@ def multi_bcat(*paths): secsleft = _common.POWER_TIME_UNLIMITED elif energy_now is not None and power_now is not None: try: - secsleft = int(energy_now / power_now * 3600) + secsleft = int(energy_now / abs(power_now) * 3600) except ZeroDivisionError: secsleft = _common.POWER_TIME_UNKNOWN elif time_to_empty is not None: From 7797fc1f57df4e230d18c11da1f0dcdf99bdcfb4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Aug 2025 16:00:58 +0200 Subject: [PATCH 1282/1714] progress --- .github/workflows/build.yml | 104 ++++++++++++++++++------------------ psutil/tests/test_system.py | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a801cc0e4..6d00ac3b5a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,58 +68,58 @@ jobs: python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - # Test python 2.7 fallback installation message produced by setup.py - py2-fallback: - name: py2.7 setup.py check - runs-on: ubuntu-24.04 - timeout-minutes: 20 - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v4 - - uses: LizardByte/actions/actions/setup_python@master - with: - python-version: "2.7" - - run: python scripts/internal/test_python2_setup_py.py + # # Test python 2.7 fallback installation message produced by setup.py + # py2-fallback: + # name: py2.7 setup.py check + # runs-on: ubuntu-24.04 + # timeout-minutes: 20 + # strategy: + # fail-fast: false + # steps: + # - uses: actions/checkout@v4 + # - uses: LizardByte/actions/actions/setup_python@master + # with: + # python-version: "2.7" + # - run: python scripts/internal/test_python2_setup_py.py - # Run linters - linters: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - name: "Run linters" - run: | - make lint-ci + # # Run linters + # linters: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v5 + # with: + # python-version: 3.x + # - name: "Run linters" + # run: | + # make lint-ci - # Produce wheels as artifacts. - upload-wheels: - needs: [tests] - runs-on: ubuntu-latest - steps: - - uses: actions/upload-artifact/merge@v4 - with: - name: wheels - pattern: wheels-* - separate-directories: false - delete-merged: true + # # Produce wheels as artifacts. + # upload-wheels: + # needs: [tests] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/upload-artifact/merge@v4 + # with: + # name: wheels + # pattern: wheels-* + # separate-directories: false + # delete-merged: true - # Check sanity of .tar.gz + wheel files - check-dist: - needs: [upload-wheels] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - uses: actions/download-artifact@v4 - with: - name: wheels - path: wheelhouse - - run: | - python scripts/internal/print_hashes.py wheelhouse/ - pipx run twine check --strict wheelhouse/* - pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl + # # Check sanity of .tar.gz + wheel files + # check-dist: + # needs: [upload-wheels] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v5 + # with: + # python-version: 3.x + # - uses: actions/download-artifact@v4 + # with: + # name: wheels + # path: wheelhouse + # - run: | + # python scripts/internal/print_hashes.py wheelhouse/ + # pipx run twine check --strict wheelhouse/* + # pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 027516f624..1a674cb8d3 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -476,7 +476,7 @@ def test_per_cpu_times_2(self): return None @pytest.mark.skipif( - CI_TESTING and OPENBSD, reason="unreliable on OPENBSD + CI" + (CI_TESTING and OPENBSD) or MACOS, reason="unreliable on OPENBSD + CI" ) @retry_on_failure(30) def test_cpu_times_comparison(self): From 7029817c28d20c047f9e9de08d81db66f34a1553 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Aug 2025 16:17:47 +0200 Subject: [PATCH 1283/1714] revert prev commit done by accident --- .github/workflows/build.yml | 104 ++++++++++++++++++------------------ psutil/tests/test_system.py | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a801cc0e4..6d00ac3b5a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,58 +68,58 @@ jobs: python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - # Test python 2.7 fallback installation message produced by setup.py - py2-fallback: - name: py2.7 setup.py check - runs-on: ubuntu-24.04 - timeout-minutes: 20 - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v4 - - uses: LizardByte/actions/actions/setup_python@master - with: - python-version: "2.7" - - run: python scripts/internal/test_python2_setup_py.py + # # Test python 2.7 fallback installation message produced by setup.py + # py2-fallback: + # name: py2.7 setup.py check + # runs-on: ubuntu-24.04 + # timeout-minutes: 20 + # strategy: + # fail-fast: false + # steps: + # - uses: actions/checkout@v4 + # - uses: LizardByte/actions/actions/setup_python@master + # with: + # python-version: "2.7" + # - run: python scripts/internal/test_python2_setup_py.py - # Run linters - linters: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - name: "Run linters" - run: | - make lint-ci + # # Run linters + # linters: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v5 + # with: + # python-version: 3.x + # - name: "Run linters" + # run: | + # make lint-ci - # Produce wheels as artifacts. - upload-wheels: - needs: [tests] - runs-on: ubuntu-latest - steps: - - uses: actions/upload-artifact/merge@v4 - with: - name: wheels - pattern: wheels-* - separate-directories: false - delete-merged: true + # # Produce wheels as artifacts. + # upload-wheels: + # needs: [tests] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/upload-artifact/merge@v4 + # with: + # name: wheels + # pattern: wheels-* + # separate-directories: false + # delete-merged: true - # Check sanity of .tar.gz + wheel files - check-dist: - needs: [upload-wheels] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - uses: actions/download-artifact@v4 - with: - name: wheels - path: wheelhouse - - run: | - python scripts/internal/print_hashes.py wheelhouse/ - pipx run twine check --strict wheelhouse/* - pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl + # # Check sanity of .tar.gz + wheel files + # check-dist: + # needs: [upload-wheels] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v5 + # with: + # python-version: 3.x + # - uses: actions/download-artifact@v4 + # with: + # name: wheels + # path: wheelhouse + # - run: | + # python scripts/internal/print_hashes.py wheelhouse/ + # pipx run twine check --strict wheelhouse/* + # pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 027516f624..1a674cb8d3 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -476,7 +476,7 @@ def test_per_cpu_times_2(self): return None @pytest.mark.skipif( - CI_TESTING and OPENBSD, reason="unreliable on OPENBSD + CI" + (CI_TESTING and OPENBSD) or MACOS, reason="unreliable on OPENBSD + CI" ) @retry_on_failure(30) def test_cpu_times_comparison(self): From ed4948235d0eddbfae8a36f38b0238bfba4cc903 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Aug 2025 16:44:59 +0200 Subject: [PATCH 1284/1714] [Windows] skip service description() on ERROR_NOT_FOUND (#2608) --- HISTORY.rst | 2 ++ psutil/arch/windows/services.c | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index fd1cccff49..154c55f2df 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -41,6 +41,8 @@ XXXX-XX-XX Stephan) - 2605_, [Linux]: `psutil.sensors_battery()` reports a negative amount for seconds left. +- 2607_, [Windows]: ``WindowsService.description()`` method may fail with + ``ERROR_NOT_FOUND``. Now it returns an empty string instead. **Compatibility notes** diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 9c7f1bee85..2339f289a7 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -368,12 +368,17 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { bytesNeeded = 0; QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, &bytesNeeded); - if (GetLastError() == ERROR_MUI_FILE_NOT_FOUND) { - // Also services.msc fails in the same manner, so we return an + + if ((GetLastError() == ERROR_NOT_FOUND) || + (GetLastError() == ERROR_MUI_FILE_NOT_FOUND)) + { + // E.g. services.msc fails in this manner, so we return an // empty string. + psutil_debug("set empty string for NOT_FOUND service description"); CloseServiceHandle(hService); return Py_BuildValue("s", ""); } + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); goto error; From f4bc10b140c6254f7430d99c0219cb0cc4434b07 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 5 Aug 2025 17:47:45 +0300 Subject: [PATCH 1285/1714] Call GetExtended[Tcp|Udp]Table twice under free-threaded build (#2590) * Call GetExtended[Tcp|Udp]Table twice under free-threaded build To avoid using global state for the size of the allocated tcp/udp table, we fall back to the slower path of calling the `GetExtended[Tcp|Udp]Table` APIs twice, once to get the size of the table we need to allocate, and once to actually populate the table. Signed-off-by: Lysandros Nikolaou --- psutil/arch/windows/socks.c | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 403964ad08..5b646173c6 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -18,9 +18,6 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) #define STATUS_UNSUCCESSFUL 0xC0000001 -ULONG g_TcpTableSize = 0; -ULONG g_UdpTableSize = 0; - // Note about GetExtended[Tcp|Udp]Table syscalls: due to other processes // being active on the machine, it's possible that the size of the table @@ -38,13 +35,9 @@ static PVOID __GetExtendedTcpTable(ULONG family) { ULONG size; TCP_TABLE_CLASS class = TCP_TABLE_OWNER_PID_ALL; - size = g_TcpTableSize; - if (size == 0) { - GetExtendedTcpTable(NULL, &size, FALSE, family, class, 0); - // reserve 25% more space - size = size + (size / 2 / 2); - g_TcpTableSize = size; - } + GetExtendedTcpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space to be sure + size = size + (size / 2 / 2); table = malloc(size); if (table == NULL) { @@ -59,7 +52,6 @@ static PVOID __GetExtendedTcpTable(ULONG family) { free(table); if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { psutil_debug("GetExtendedTcpTable: retry with different bufsize"); - g_TcpTableSize = 0; return __GetExtendedTcpTable(family); } @@ -74,13 +66,9 @@ static PVOID __GetExtendedUdpTable(ULONG family) { ULONG size; UDP_TABLE_CLASS class = UDP_TABLE_OWNER_PID; - size = g_UdpTableSize; - if (size == 0) { - GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); - // reserve 25% more space - size = size + (size / 2 / 2); - g_UdpTableSize = size; - } + GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space to be sure + size = size + (size / 2 / 2); table = malloc(size); if (table == NULL) { @@ -95,7 +83,6 @@ static PVOID __GetExtendedUdpTable(ULONG family) { free(table); if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { psutil_debug("GetExtendedUdpTable: retry with different bufsize"); - g_UdpTableSize = 0; return __GetExtendedUdpTable(family); } From 9e2fa1b8d35d64bbb596356293c6ea21eb49a591 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Aug 2025 18:20:16 +0200 Subject: [PATCH 1286/1714] check mach_timebase_info return value --- psutil/arch/osx/init.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/psutil/arch/osx/init.c b/psutil/arch/osx/init.c index af4d3b5390..65e81f40d3 100644 --- a/psutil/arch/osx/init.c +++ b/psutil/arch/osx/init.c @@ -16,6 +16,12 @@ struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; // Called on module import. int psutil_setup_osx(void) { - mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); + kern_return_t ret; + + ret = mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); + if (ret != KERN_SUCCESS) { + psutil_PyErr_SetFromOSErrnoWithSyscall("mach_timebase_info"); + return 1; + } return 0; } From 91cf137d59d23f9b7eea1038dc0a130f5412cf90 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 5 Aug 2025 18:30:22 +0200 Subject: [PATCH 1287/1714] OSX: fix segfault risk in in psutil_cpu_count_logical() where sysctlbyname() is called with the wrong last argument (2 instead of 0), which causes undefined behavior because it tries to write two bytes to the destination buffer instead of reading. Signed-off-by: Giampaolo Rodola --- psutil/arch/osx/cpu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 6d993647ae..d087719ee4 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -44,7 +44,7 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { int num; size_t size = sizeof(int); - if (sysctlbyname("hw.logicalcpu", &num, &size, NULL, 2)) + if (sysctlbyname("hw.logicalcpu", &num, &size, NULL, 0)) Py_RETURN_NONE; // mimic os.cpu_count() else return Py_BuildValue("i", num); From c0c4c5b4e21b95a1b6967dfff985383f9976a6e6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 12:12:54 +0200 Subject: [PATCH 1288/1714] osx: correct casting of types on Py_BuildValue --- psutil/arch/osx/cpu.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index d087719ee4..776abfae7e 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -215,10 +215,10 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { IOObjectRelease(entry); return Py_BuildValue( - "IKK", - curr / 1000 / 1000, - min / 1000 / 1000, - max / 1000 / 1000 + "KKK", + (unsigned long long)(curr / 1000 / 1000), + (unsigned long long)(min / 1000 / 1000), + (unsigned long long)(max / 1000 / 1000) ); error: @@ -256,10 +256,10 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); return Py_BuildValue( - "IKK", - curr / 1000 / 1000, - min / 1000 / 1000, - max / 1000 / 1000); + "KKK", + (unsigned long long)(curr / 1000 / 1000), + (unsigned long long)(min / 1000 / 1000), + (unsigned long long)(max / 1000 / 1000)); } #endif From 4881397946bcce266ee22984d6f7b28be8098f60 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 12:48:34 +0200 Subject: [PATCH 1289/1714] osx: add missing mach_port_deallocate() in case of err --- psutil/arch/osx/cpu.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 776abfae7e..97b3e20ac8 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -73,6 +73,7 @@ psutil_cpu_times(PyObject *self, PyObject *args) { error = host_statistics(host_port, HOST_CPU_LOAD_INFO, (host_info_t)&r_load, &count); if (error != KERN_SUCCESS) { + mach_port_deallocate(mach_task_self(), host_port); return PyErr_Format( PyExc_RuntimeError, "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", @@ -99,6 +100,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); if (ret != KERN_SUCCESS) { + mach_port_deallocate(mach_task_self(), mport); PyErr_Format( PyExc_RuntimeError, "host_statistics(HOST_VM_INFO) failed: %s", @@ -282,6 +284,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count); if (error != KERN_SUCCESS) { + mach_port_deallocate(mach_task_self(), host_port); PyErr_Format( PyExc_RuntimeError, "host_processor_info(PROCESSOR_CPU_LOAD_INFO) syscall failed: %s", @@ -302,8 +305,10 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { ); if (!py_cputime) goto error; - if (PyList_Append(py_retlist, py_cputime)) + if (PyList_Append(py_retlist, py_cputime)) { + Py_DECREF(py_cputime); goto error; + } Py_CLEAR(py_cputime); } From e9a8920ceb62ade5211e73eceb17ba12f3110a75 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 13:01:25 +0200 Subject: [PATCH 1290/1714] osx / cpu_stats() deprecated HOST_VM_INFO host_statistics(HOST_VM_INFO) no longer returns a struct vmmeter on modern macOS. Correct structure is vm_statistics_data_t (from ), but the code assumes BSD vmmeter. Fields like v_swtch, v_syscall, v_trap don't exist in vm_statistics_data_t, so this function is wrong on modern macOS and will return garbage or segfault. Signed-off-by: Giampaolo Rodola --- psutil/arch/osx/cpu.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 97b3e20ac8..923b99e4c2 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -93,12 +93,13 @@ psutil_cpu_times(PyObject *self, PyObject *args) { PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { - struct vmmeter vmstat; kern_return_t ret; - mach_msg_type_number_t count = sizeof(vmstat) / sizeof(integer_t); + mach_msg_type_number_t count; mach_port_t mport = mach_host_self(); + struct vmmeter vm32; - ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); + count = HOST_VM_INFO_COUNT; + ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vm32, &count); if (ret != KERN_SUCCESS) { mach_port_deallocate(mach_task_self(), mport); PyErr_Format( @@ -107,15 +108,15 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { mach_error_string(ret)); return NULL; } - mach_port_deallocate(mach_task_self(), mport); + mach_port_deallocate(mach_task_self(), mport); return Py_BuildValue( "IIIII", - vmstat.v_swtch, // ctx switches - vmstat.v_intr, // interrupts - vmstat.v_soft, // software interrupts - vmstat.v_syscall, // syscalls - vmstat.v_trap // traps + vm32.v_swtch, // ctx switches + vm32.v_intr, // interrupts + vm32.v_soft, // software interrupts + vm32.v_syscall, // syscalls + vm32.v_trap // traps ); } From 64463612c6aea6089448bb71f89e7edd2018d402 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 13:12:36 +0200 Subject: [PATCH 1291/1714] osx / mem.c: use btter conversion of C types (unsigned long long) --- psutil/arch/osx/mem.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 8103876070..3c39a1cc72 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -22,7 +22,7 @@ static int psutil_sys_vminfo(vm_statistics64_t vmstat) { kern_return_t ret; - unsigned int count = HOST_VM_INFO64_COUNT; + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; mach_port_t mport = mach_host_self(); ret = host_statistics64(mport, HOST_VM_INFO64, (host_info64_t)vmstat, &count); @@ -71,7 +71,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return Py_BuildValue( "KKKKKK", - total, + (unsigned long long) total, (unsigned long long) vm.active_count * pagesize, // active (unsigned long long) vm.inactive_count * pagesize, // inactive (unsigned long long) vm.wire_count * pagesize, // wired @@ -107,10 +107,10 @@ psutil_swap_mem(PyObject *self, PyObject *args) { return NULL; return Py_BuildValue( - "LLLKK", - totals.xsu_total, - totals.xsu_used, - totals.xsu_avail, - (unsigned long long)vmstat.pageins * pagesize, - (unsigned long long)vmstat.pageouts * pagesize); + "KKKKK", + (unsigned long long) totals.xsu_total, + (unsigned long long) totals.xsu_used, + (unsigned long long) totals.xsu_avail, + (unsigned long long) vmstat.pageins * pagesize, + (unsigned long long) vmstat.pageouts * pagesize); } From 7a4a85f0d228238e734a357343bdcef9679a383b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 13:17:56 +0200 Subject: [PATCH 1292/1714] osx: add missing CFRelease() call to cleanup resources --- psutil/arch/osx/cpu.c | 1 + psutil/arch/osx/mem.c | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 923b99e4c2..311119507a 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -145,6 +145,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { } status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); + CFRelease(matching); if (status != KERN_SUCCESS) { PyErr_Format( PyExc_RuntimeError, "IOServiceGetMatchingServices call failed" diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 3c39a1cc72..b5097f61c1 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -29,8 +29,9 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { if (ret != KERN_SUCCESS) { PyErr_Format( PyExc_RuntimeError, - "host_statistics(HOST_VM_INFO) syscall failed: %s", - mach_error_string(ret)); + "host_statistics64(HOST_VM_INFO64) syscall failed: %s", + mach_error_string(ret) + ); return 0; } mach_port_deallocate(mach_task_self(), mport); From 825b344b171ae6f170ca14fdecdc18d53d3ee614 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 13:30:47 +0200 Subject: [PATCH 1293/1714] osx: better error check on sysctl() + always mach_port_deallocate() --- psutil/arch/osx/mem.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index b5097f61c1..d222ad6ee3 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -26,6 +26,7 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { mach_port_t mport = mach_host_self(); ret = host_statistics64(mport, HOST_VM_INFO64, (host_info64_t)vmstat, &count); + mach_port_deallocate(mach_task_self(), mport); if (ret != KERN_SUCCESS) { PyErr_Format( PyExc_RuntimeError, @@ -34,7 +35,6 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { ); return 0; } - mach_port_deallocate(mach_task_self(), mport); return 1; } @@ -57,12 +57,8 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { mib[1] = HW_MEMSIZE; // This is also available as sysctlbyname("hw.memsize"). - if (sysctl(mib, 2, &total, &len, NULL, 0)) { - if (errno != 0) - PyErr_SetFromErrno(PyExc_OSError); - else - PyErr_Format( - PyExc_RuntimeError, "sysctl(HW_MEMSIZE) syscall failed"); + if (sysctl(mib, 2, &total, &len, NULL, 0) == -1) { + PyErr_SetFromErrno(PyExc_OSError); return NULL; } From ec7a69f9a48c119be100a7242e1a5ed33aaf7194 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 13:20:07 +0200 Subject: [PATCH 1294/1714] chore: upgrade from win 2019 to win 2025 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d00ac3b5a..72d42d3d74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,8 +30,8 @@ jobs: - { os: ubuntu-24.04-arm, arch: aarch64 } - { os: macos-13, arch: x86_64 } - { os: macos-14, arch: arm64 } - - { os: windows-2019, arch: AMD64 } - - { os: windows-2019, arch: x86 } + - { os: windows-2025, arch: AMD64 } + - { os: windows-2025, arch: x86 } - { os: windows-11-arm, arch: ARM64 } steps: - uses: actions/checkout@v4 From 9d6f5d8a40325630a35d8782dc4c57cd87d12bfb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 15:03:48 +0200 Subject: [PATCH 1295/1714] osx: always check mach_host_self() ret val --- psutil/arch/osx/cpu.c | 39 ++++++++++++++++++++++++++++++--------- psutil/arch/osx/mem.c | 17 ++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 311119507a..3e759f53d5 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -68,18 +68,25 @@ psutil_cpu_times(PyObject *self, PyObject *args) { mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT; kern_return_t error; host_cpu_load_info_data_t r_load; + mach_port_t mport; - mach_port_t host_port = mach_host_self(); - error = host_statistics(host_port, HOST_CPU_LOAD_INFO, + mport = mach_host_self(); + if (mport == MACH_PORT_NULL) { + PyErr_SetString(PyExc_RuntimeError, + "mach_host_self() returned MACH_PORT_NULL"); + return NULL; + } + + error = host_statistics(mport, HOST_CPU_LOAD_INFO, (host_info_t)&r_load, &count); if (error != KERN_SUCCESS) { - mach_port_deallocate(mach_task_self(), host_port); + mach_port_deallocate(mach_task_self(), mport); return PyErr_Format( PyExc_RuntimeError, "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", mach_error_string(error)); } - mach_port_deallocate(mach_task_self(), host_port); + mach_port_deallocate(mach_task_self(), mport); return Py_BuildValue( "(dddd)", @@ -95,9 +102,16 @@ PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { kern_return_t ret; mach_msg_type_number_t count; - mach_port_t mport = mach_host_self(); + mach_port_t mport; struct vmmeter vm32; + mport = mach_host_self(); + if (mport == MACH_PORT_NULL) { + PyErr_SetString(PyExc_RuntimeError, + "mach_host_self() returned MACH_PORT_NULL"); + return NULL; + } + count = HOST_VM_INFO_COUNT; ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vm32, &count); if (ret != KERN_SUCCESS) { @@ -276,24 +290,31 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { kern_return_t error; processor_cpu_load_info_data_t *cpu_load_info = NULL; int ret; + mach_port_t mport; PyObject *py_retlist = PyList_New(0); PyObject *py_cputime = NULL; if (py_retlist == NULL) return NULL; - mach_port_t host_port = mach_host_self(); - error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, + mport = mach_host_self(); + if (mport == MACH_PORT_NULL) { + PyErr_SetString(PyExc_RuntimeError, + "mach_host_self() returned MACH_PORT_NULL"); + goto error; + } + + error = host_processor_info(mport, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count); if (error != KERN_SUCCESS) { - mach_port_deallocate(mach_task_self(), host_port); + mach_port_deallocate(mach_task_self(), mport); PyErr_Format( PyExc_RuntimeError, "host_processor_info(PROCESSOR_CPU_LOAD_INFO) syscall failed: %s", mach_error_string(error)); goto error; } - mach_port_deallocate(mach_task_self(), host_port); + mach_port_deallocate(mach_task_self(), mport); cpu_load_info = (processor_cpu_load_info_data_t *) info_array; diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index d222ad6ee3..0aebbf233c 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -23,7 +23,14 @@ static int psutil_sys_vminfo(vm_statistics64_t vmstat) { kern_return_t ret; mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; - mach_port_t mport = mach_host_self(); + mach_port_t mport; + + mport = mach_host_self(); + if (mport == MACH_PORT_NULL) { + PyErr_SetString(PyExc_RuntimeError, + "mach_host_self() returned MACH_PORT_NULL"); + return 1; + } ret = host_statistics64(mport, HOST_VM_INFO64, (host_info64_t)vmstat, &count); mach_port_deallocate(mach_task_self(), mport); @@ -33,9 +40,9 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { "host_statistics64(HOST_VM_INFO64) syscall failed: %s", mach_error_string(ret) ); - return 0; + return 1; } - return 1; + return 0; } @@ -63,7 +70,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { } // vm - if (!psutil_sys_vminfo(&vm)) + if (psutil_sys_vminfo(&vm) != 0) return NULL; return Py_BuildValue( @@ -100,7 +107,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { PyExc_RuntimeError, "sysctl(VM_SWAPUSAGE) syscall failed"); return NULL; } - if (!psutil_sys_vminfo(&vmstat)) + if (psutil_sys_vminfo(&vmstat) != 0) return NULL; return Py_BuildValue( From 5abab5832d43b2edf03f5f08fbf725bba750c2c7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 15:46:22 +0200 Subject: [PATCH 1296/1714] OSX: fix `cpu_freq()` segfault (#2610) --- HISTORY.rst | 1 + psutil/arch/osx/cpu.c | 97 ++++++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 154c55f2df..7b468e8c76 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -43,6 +43,7 @@ XXXX-XX-XX seconds left. - 2607_, [Windows]: ``WindowsService.description()`` method may fail with ``ERROR_NOT_FOUND``. Now it returns an empty string instead. +- 2610:, [macOS], [CRITICAL]: fix `cpu_freq()`_ segfault on ARM architectures. **Compatibility notes** diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 3e759f53d5..7707b184f9 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -137,29 +137,32 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { #if defined(__arm64__) || defined(__aarch64__) PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { - uint32_t min; - uint32_t curr; - uint32_t pMin; - uint32_t eMin; - uint32_t max; kern_return_t status; - CFDictionaryRef matching = NULL; - CFTypeRef pCoreRef = NULL; - CFTypeRef eCoreRef = NULL; io_iterator_t iter = 0; io_registry_entry_t entry = 0; + CFTypeRef pCoreRef = NULL; + CFTypeRef eCoreRef = NULL; + CFDictionaryRef matching; + size_t pCoreLength; io_name_t name; + uint32_t pMin = 0; + uint32_t eMin = 0; + uint32_t min = 0; + uint32_t max = 0; + uint32_t curr = 0; + + // Get matching service for Apple ARM I/O device. matching = IOServiceMatching("AppleARMIODevice"); - if (matching == 0) { + if (matching == NULL) { return PyErr_Format( PyExc_RuntimeError, - "IOServiceMatching call failed, 'AppleARMIODevice' not found" + "IOServiceMatching failed: 'AppleARMIODevice' not found" ); } + // IOServiceGetMatchingServices consumes matching; do NOT CFRelease it. status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); - CFRelease(matching); if (status != KERN_SUCCESS) { PyErr_Format( PyExc_RuntimeError, "IOServiceGetMatchingServices call failed" @@ -167,16 +170,14 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { goto error; } + // Find the 'pmgr' entry. while ((entry = IOIteratorNext(iter)) != 0) { status = IORegistryEntryGetName(entry, name); - if (status != KERN_SUCCESS) { - IOObjectRelease(entry); - continue; - } - if (strcmp(name, "pmgr") == 0) { + if (status == KERN_SUCCESS && strcmp(name, "pmgr") == 0) { break; } IOObjectRelease(entry); + entry = 0; } if (entry == 0) { @@ -187,50 +188,52 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { goto error; } + // Get performance and efficiency core data. pCoreRef = IORegistryEntryCreateCFProperty( - entry, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, 0); - if (pCoreRef == NULL) { - PyErr_Format( - PyExc_RuntimeError, "'voltage-states5-sram' property not found"); - goto error; - } - - eCoreRef = IORegistryEntryCreateCFProperty( - entry, CFSTR("voltage-states1-sram"), kCFAllocatorDefault, 0); - if (eCoreRef == NULL) { - PyErr_Format( - PyExc_RuntimeError, "'voltage-states1-sram' property not found"); - goto error; - } - - size_t pCoreLength = CFDataGetLength(pCoreRef); - size_t eCoreLength = CFDataGetLength(eCoreRef); - if (pCoreLength < 8) { - PyErr_Format( + entry, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, 0 + ); + if (pCoreRef == NULL || + CFGetTypeID(pCoreRef) != CFDataGetTypeID() || + CFDataGetLength(pCoreRef) < 8) + { + PyErr_SetString( PyExc_RuntimeError, - "expected 'voltage-states5-sram' buffer to have at least size 8" + "'voltage-states5-sram' is missing or invalid" ); goto error; } - if (eCoreLength < 4) { - PyErr_Format( + + eCoreRef = IORegistryEntryCreateCFProperty( + entry, CFSTR("voltage-states1-sram"), kCFAllocatorDefault, 0 + ); + if (eCoreRef == NULL || + CFGetTypeID(eCoreRef) != CFDataGetTypeID() || + CFDataGetLength(eCoreRef) < 4) + { + PyErr_SetString( PyExc_RuntimeError, - "expected 'voltage-states1-sram' buffer to have at least size 4" + "'voltage-states1-sram' is missing or invalid" ); goto error; } - CFDataGetBytes(pCoreRef, CFRangeMake(0, 4), (UInt8 *) &pMin); - CFDataGetBytes(eCoreRef, CFRangeMake(0, 4), (UInt8 *) &eMin); - CFDataGetBytes(pCoreRef, CFRangeMake(pCoreLength - 8, 4), (UInt8 *) &max); + // Extract values safely. + pCoreLength = CFDataGetLength(pCoreRef); + CFDataGetBytes(pCoreRef, CFRangeMake(0, 4), (UInt8 *)&pMin); + CFDataGetBytes(eCoreRef, CFRangeMake(0, 4), (UInt8 *)&eMin); + CFDataGetBytes(pCoreRef, CFRangeMake(pCoreLength - 8, 4), (UInt8 *)&max); - min = pMin < eMin ? pMin : eMin; + min = (pMin < eMin) ? pMin : eMin; curr = max; - CFRelease(pCoreRef); - CFRelease(eCoreRef); - IOObjectRelease(iter); - IOObjectRelease(entry); + if (pCoreRef) + CFRelease(pCoreRef); + if (eCoreRef) + CFRelease(eCoreRef); + if (entry) + IOObjectRelease(entry); + if (iter) + IOObjectRelease(iter); return Py_BuildValue( "KKK", From 712b2bc141304fb121052f9c0b974209f9a70501 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Aug 2025 16:25:29 +0200 Subject: [PATCH 1297/1714] osx: cast to (unsigned long long) + use Py_CLEAR where needed --- psutil/arch/osx/net.c | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index 6848f1a4fb..759abf87c5 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -69,24 +69,23 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { py_ifc_info = Py_BuildValue( "(KKKKKKKi)", - if2m->ifm_data.ifi_obytes, - if2m->ifm_data.ifi_ibytes, - if2m->ifm_data.ifi_opackets, - if2m->ifm_data.ifi_ipackets, - if2m->ifm_data.ifi_ierrors, - if2m->ifm_data.ifi_oerrors, - if2m->ifm_data.ifi_iqdrops, + (unsigned long long)if2m->ifm_data.ifi_obytes, + (unsigned long long)if2m->ifm_data.ifi_ibytes, + (unsigned long long)if2m->ifm_data.ifi_opackets, + (unsigned long long)if2m->ifm_data.ifi_ipackets, + (unsigned long long)if2m->ifm_data.ifi_ierrors, + (unsigned long long)if2m->ifm_data.ifi_oerrors, + (unsigned long long)if2m->ifm_data.ifi_iqdrops, 0); // dropout not supported if (!py_ifc_info) goto error; - if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) + if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) { + Py_CLEAR(py_ifc_info); goto error; + } Py_CLEAR(py_ifc_info); } - else { - continue; - } } free(buf); From 9ece8c1247b1bce1081167bd7c484c0e02dc9ba7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Aug 2025 12:58:14 +0200 Subject: [PATCH 1298/1714] Create arch/posix folder (#2613) --- MANIFEST.in | 5 + psutil/_psutil_posix.c | 819 +-------------------------------------- psutil/_psutil_posix.h | 2 - psutil/arch/all/init.h | 3 + psutil/arch/osx/net.c | 21 +- psutil/arch/posix/init.h | 7 + psutil/arch/posix/net.c | 688 ++++++++++++++++++++++++++++++++ psutil/arch/posix/net.h | 15 + psutil/arch/posix/proc.c | 120 ++++++ psutil/arch/posix/proc.h | 11 + setup.py | 1 + 11 files changed, 862 insertions(+), 830 deletions(-) create mode 100644 psutil/arch/posix/init.h create mode 100644 psutil/arch/posix/net.c create mode 100644 psutil/arch/posix/net.h create mode 100644 psutil/arch/posix/proc.c create mode 100644 psutil/arch/posix/proc.h diff --git a/MANIFEST.in b/MANIFEST.in index c01c4e05e3..8f2edde2f0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -121,6 +121,11 @@ include psutil/arch/osx/sensors.c include psutil/arch/osx/sensors.h include psutil/arch/osx/sys.c include psutil/arch/osx/sys.h +include psutil/arch/posix/init.h +include psutil/arch/posix/net.c +include psutil/arch/posix/net.h +include psutil/arch/posix/proc.c +include psutil/arch/posix/proc.h include psutil/arch/sunos/cpu.c include psutil/arch/sunos/cpu.h include psutil/arch/sunos/disk.c diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 02b85ddcb9..ff145f9ec1 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -7,49 +7,12 @@ */ #include -#include -#include #include -#include -#include #include -#include -#include -#include - -#ifdef PSUTIL_SUNOS10 - #include "arch/solaris/v10/ifaddrs.h" -#elif PSUTIL_AIX - #include "arch/aix/ifaddrs.h" -#else - #include -#endif - -#if defined(PSUTIL_LINUX) - #include - #include - #include -#endif -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - #include - #include - #include - #include - #include - #include -#endif -#if defined(PSUTIL_SUNOS) - #include - #include -#endif -#if defined(PSUTIL_AIX) - #include -#endif -#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) - #include -#endif #include "arch/all/init.h" +#include "arch/posix/proc.h" +#include "arch/posix/net.h" // ==================================================================== @@ -83,84 +46,6 @@ psutil_getpagesize(void) { } -/* - * Check if PID exists. Return values: - * 1: exists - * 0: does not exist - * -1: error (Python exception is set) - */ -int -psutil_pid_exists(pid_t pid) { - int ret; - - // No negative PID exists, plus -1 is an alias for sending signal - // too all processes except system ones. Not what we want. - if (pid < 0) - return 0; - - // As per "man 2 kill" PID 0 is an alias for sending the signal to - // every process in the process group of the calling process. - // Not what we want. Some platforms have PID 0, some do not. - // We decide that at runtime. - if (pid == 0) { -#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) - return 0; -#else - return 1; -#endif - } - - ret = kill(pid , 0); - if (ret == 0) - return 1; - else { - if (errno == ESRCH) { - // ESRCH == No such process - return 0; - } - else if (errno == EPERM) { - // EPERM clearly indicates there's a process to deny - // access to. - return 1; - } - 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. - PyErr_SetFromErrno(PyExc_OSError); - return -1; - } - } -} - - -/* - * Utility used for those syscalls which do not return a meaningful - * error that we can translate into an exception which makes sense. - * As such, we'll have to guess. - * On UNIX, if errno is set, we return that one (OSError). - * Else, if PID does not exist we assume the syscall failed because - * of that so we raise NoSuchProcess. - * If none of this is true we giveup and raise RuntimeError(msg). - * This will always set a Python exception and return NULL. - */ -void -psutil_raise_for_pid(long pid, char *syscall) { - if (errno != 0) - psutil_PyErr_SetFromOSErrnoWithSyscall(syscall); - else if (psutil_pid_exists(pid) == 0) - NoSuchProcess(syscall); - else - PyErr_Format(PyExc_RuntimeError, "%s syscall failed", syscall); -} - - -// ==================================================================== -// --- Python wrappers -// ==================================================================== - - // Exposed so we can test it against Python's stdlib. static PyObject * psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { @@ -168,702 +53,6 @@ psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { } -/* - * Given a PID return process priority as a Python integer. - */ -static PyObject * -psutil_posix_getpriority(PyObject *self, PyObject *args) { - pid_t pid; - int priority; - errno = 0; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - -#ifdef PSUTIL_OSX - priority = getpriority(PRIO_PROCESS, (id_t)pid); -#else - priority = getpriority(PRIO_PROCESS, pid); -#endif - if (errno != 0) - return PyErr_SetFromErrno(PyExc_OSError); - return Py_BuildValue("i", priority); -} - - -/* - * Given a PID and a value change process priority. - */ -static PyObject * -psutil_posix_setpriority(PyObject *self, PyObject *args) { - pid_t pid; - int priority; - int retval; - - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) - return NULL; - -#ifdef PSUTIL_OSX - retval = setpriority(PRIO_PROCESS, (id_t)pid, priority); -#else - retval = setpriority(PRIO_PROCESS, pid, priority); -#endif - if (retval == -1) - return PyErr_SetFromErrno(PyExc_OSError); - Py_RETURN_NONE; -} - - -/* - * Translate a sockaddr struct into a Python string. - * Return None if address family is not AF_INET* or AF_PACKET. - */ -static PyObject * -psutil_convert_ipaddr(struct sockaddr *addr, int family) { - char buf[NI_MAXHOST]; - int err; - int addrlen; - size_t n; - size_t len; - const char *data; - char *ptr; - - if (addr == NULL) { - Py_INCREF(Py_None); - return Py_None; - } - else if (family == AF_INET || family == AF_INET6) { - if (family == AF_INET) - addrlen = sizeof(struct sockaddr_in); - else - addrlen = sizeof(struct sockaddr_in6); - err = getnameinfo(addr, addrlen, buf, sizeof(buf), NULL, 0, - NI_NUMERICHOST); - if (err != 0) { - // XXX we get here on FreeBSD when processing 'lo' / AF_INET6 - // broadcast. Not sure what to do other than returning None. - // ifconfig does not show anything BTW. - // PyErr_Format(PyExc_RuntimeError, gai_strerror(err)); - // return NULL; - Py_INCREF(Py_None); - return Py_None; - } - else { - return Py_BuildValue("s", buf); - } - } -#ifdef PSUTIL_LINUX - else if (family == AF_PACKET) { - struct sockaddr_ll *lladdr = (struct sockaddr_ll *)addr; - len = lladdr->sll_halen; - data = (const char *)lladdr->sll_addr; - } -#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - else if (addr->sa_family == AF_LINK) { - // Note: prior to Python 3.4 socket module does not expose - // AF_LINK so we'll do. - struct sockaddr_dl *dladdr = (struct sockaddr_dl *)addr; - len = dladdr->sdl_alen; - data = LLADDR(dladdr); - } -#endif - else { - // unknown family - Py_INCREF(Py_None); - return Py_None; - } - - // AF_PACKET or AF_LINK - if (len > 0) { - ptr = buf; - for (n = 0; n < len; ++n) { - sprintf(ptr, "%02x:", data[n] & 0xff); - ptr += 3; - } - *--ptr = '\0'; - return Py_BuildValue("s", buf); - } - else { - Py_INCREF(Py_None); - return Py_None; - } -} - - -/* - * Return NICs information a-la ifconfig as a list of tuples. - * TODO: on Solaris we won't get any MAC address. - */ -static PyObject* -psutil_net_if_addrs(PyObject* self, PyObject* args) { - struct ifaddrs *ifaddr, *ifa; - int family; - - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_address = NULL; - PyObject *py_netmask = NULL; - PyObject *py_broadcast = NULL; - PyObject *py_ptp = NULL; - - if (py_retlist == NULL) - return NULL; - if (getifaddrs(&ifaddr) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { - if (!ifa->ifa_addr) - continue; - family = ifa->ifa_addr->sa_family; - py_address = psutil_convert_ipaddr(ifa->ifa_addr, family); - // If the primary address can't be determined just skip it. - // I've never seen this happen on Linux but I did on FreeBSD. - if (py_address == Py_None) - continue; - if (py_address == NULL) - goto error; - py_netmask = psutil_convert_ipaddr(ifa->ifa_netmask, family); - if (py_netmask == NULL) - goto error; - - if (ifa->ifa_flags & IFF_BROADCAST) { - py_broadcast = psutil_convert_ipaddr(ifa->ifa_broadaddr, family); - Py_INCREF(Py_None); - py_ptp = Py_None; - } - else if (ifa->ifa_flags & IFF_POINTOPOINT) { - py_ptp = psutil_convert_ipaddr(ifa->ifa_dstaddr, family); - Py_INCREF(Py_None); - py_broadcast = Py_None; - } - else { - Py_INCREF(Py_None); - Py_INCREF(Py_None); - py_broadcast = Py_None; - py_ptp = Py_None; - } - - if ((py_broadcast == NULL) || (py_ptp == NULL)) - goto error; - py_tuple = Py_BuildValue( - "(siOOOO)", - ifa->ifa_name, - family, - py_address, - py_netmask, - py_broadcast, - py_ptp - ); - - if (! py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); - Py_CLEAR(py_address); - Py_CLEAR(py_netmask); - Py_CLEAR(py_broadcast); - Py_CLEAR(py_ptp); - } - - freeifaddrs(ifaddr); - return py_retlist; - -error: - if (ifaddr != NULL) - freeifaddrs(ifaddr); - Py_DECREF(py_retlist); - Py_XDECREF(py_tuple); - Py_XDECREF(py_address); - Py_XDECREF(py_netmask); - Py_XDECREF(py_broadcast); - Py_XDECREF(py_ptp); - return NULL; -} - - -/* - * Return NIC MTU. References: - * http://www.i-scream.org/libstatgrab/ - */ -static PyObject * -psutil_net_if_mtu(PyObject *self, PyObject *args) { - char *nic_name; - int sock = -1; - int ret; -#ifdef PSUTIL_SUNOS10 - struct lifreq lifr; -#else - struct ifreq ifr; -#endif - - if (! PyArg_ParseTuple(args, "s", &nic_name)) - return NULL; - - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock == -1) - goto error; - -#ifdef PSUTIL_SUNOS10 - PSUTIL_STRNCPY(lifr.lifr_name, nic_name, sizeof(lifr.lifr_name)); - ret = ioctl(sock, SIOCGIFMTU, &lifr); -#else - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); - ret = ioctl(sock, SIOCGIFMTU, &ifr); -#endif - if (ret == -1) - goto error; - close(sock); - -#ifdef PSUTIL_SUNOS10 - return Py_BuildValue("i", lifr.lifr_mtu); -#else - return Py_BuildValue("i", ifr.ifr_mtu); -#endif - -error: - if (sock != -1) - close(sock); - return PyErr_SetFromErrno(PyExc_OSError); -} - -static int -append_flag(PyObject *py_retlist, const char * flag_name) -{ - PyObject *py_str = NULL; - - py_str = PyUnicode_FromString(flag_name); - if (! py_str) - return 0; - if (PyList_Append(py_retlist, py_str)) { - Py_DECREF(py_str); - return 0; - } - Py_CLEAR(py_str); - - return 1; -} - -/* - * Get all of the NIC flags and return them. - */ -static PyObject * -psutil_net_if_flags(PyObject *self, PyObject *args) { - char *nic_name; - int sock = -1; - int ret; - struct ifreq ifr; - PyObject *py_retlist = PyList_New(0); - short int flags; - - if (py_retlist == NULL) - return NULL; - - if (! PyArg_ParseTuple(args, "s", &nic_name)) - goto error; - - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("socket(SOCK_DGRAM)"); - goto error; - } - - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); - ret = ioctl(sock, SIOCGIFFLAGS, &ifr); - if (ret == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCGIFFLAGS)"); - goto error; - } - - close(sock); - sock = -1; - - flags = ifr.ifr_flags & 0xFFFF; - - // Linux/glibc IFF flags: https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/gnu/net/if.h;h=251418f82331c0426e58707fe4473d454893b132;hb=HEAD - // macOS IFF flags: https://opensource.apple.com/source/xnu/xnu-792/bsd/net/if.h.auto.html - // AIX IFF flags: https://www.ibm.com/support/pages/how-hexadecimal-flags-displayed-ifconfig-are-calculated - // FreeBSD IFF flags: https://www.freebsd.org/cgi/man.cgi?query=if_allmulti&apropos=0&sektion=0&manpath=FreeBSD+10-current&format=html - -#ifdef IFF_UP - // Available in (at least) Linux, macOS, AIX, BSD - if (flags & IFF_UP) - if (!append_flag(py_retlist, "up")) - goto error; -#endif -#ifdef IFF_BROADCAST - // Available in (at least) Linux, macOS, AIX, BSD - if (flags & IFF_BROADCAST) - if (!append_flag(py_retlist, "broadcast")) - goto error; -#endif -#ifdef IFF_DEBUG - // Available in (at least) Linux, macOS, BSD - if (flags & IFF_DEBUG) - if (!append_flag(py_retlist, "debug")) - goto error; -#endif -#ifdef IFF_LOOPBACK - // Available in (at least) Linux, macOS, BSD - if (flags & IFF_LOOPBACK) - if (!append_flag(py_retlist, "loopback")) - goto error; -#endif -#ifdef IFF_POINTOPOINT - // Available in (at least) Linux, macOS, BSD - if (flags & IFF_POINTOPOINT) - if (!append_flag(py_retlist, "pointopoint")) - goto error; -#endif -#ifdef IFF_NOTRAILERS - // Available in (at least) Linux, macOS, AIX - if (flags & IFF_NOTRAILERS) - if (!append_flag(py_retlist, "notrailers")) - goto error; -#endif -#ifdef IFF_RUNNING - // Available in (at least) Linux, macOS, AIX, BSD - if (flags & IFF_RUNNING) - if (!append_flag(py_retlist, "running")) - goto error; -#endif -#ifdef IFF_NOARP - // Available in (at least) Linux, macOS, BSD - if (flags & IFF_NOARP) - if (!append_flag(py_retlist, "noarp")) - goto error; -#endif -#ifdef IFF_PROMISC - // Available in (at least) Linux, macOS, BSD - if (flags & IFF_PROMISC) - if (!append_flag(py_retlist, "promisc")) - goto error; -#endif -#ifdef IFF_ALLMULTI - // Available in (at least) Linux, macOS, BSD - if (flags & IFF_ALLMULTI) - if (!append_flag(py_retlist, "allmulti")) - goto error; -#endif -#ifdef IFF_MASTER - // Available in (at least) Linux - if (flags & IFF_MASTER) - if (!append_flag(py_retlist, "master")) - goto error; -#endif -#ifdef IFF_SLAVE - // Available in (at least) Linux - if (flags & IFF_SLAVE) - if (!append_flag(py_retlist, "slave")) - goto error; -#endif -#ifdef IFF_MULTICAST - // Available in (at least) Linux, macOS, BSD - if (flags & IFF_MULTICAST) - if (!append_flag(py_retlist, "multicast")) - goto error; -#endif -#ifdef IFF_PORTSEL - // Available in (at least) Linux - if (flags & IFF_PORTSEL) - if (!append_flag(py_retlist, "portsel")) - goto error; -#endif -#ifdef IFF_AUTOMEDIA - // Available in (at least) Linux - if (flags & IFF_AUTOMEDIA) - if (!append_flag(py_retlist, "automedia")) - goto error; -#endif -#ifdef IFF_DYNAMIC - // Available in (at least) Linux - if (flags & IFF_DYNAMIC) - if (!append_flag(py_retlist, "dynamic")) - goto error; -#endif -#ifdef IFF_OACTIVE - // Available in (at least) macOS, BSD - if (flags & IFF_OACTIVE) - if (!append_flag(py_retlist, "oactive")) - goto error; -#endif -#ifdef IFF_SIMPLEX - // Available in (at least) macOS, AIX, BSD - if (flags & IFF_SIMPLEX) - if (!append_flag(py_retlist, "simplex")) - goto error; -#endif -#ifdef IFF_LINK0 - // Available in (at least) macOS, BSD - if (flags & IFF_LINK0) - if (!append_flag(py_retlist, "link0")) - goto error; -#endif -#ifdef IFF_LINK1 - // Available in (at least) macOS, BSD - if (flags & IFF_LINK1) - if (!append_flag(py_retlist, "link1")) - goto error; -#endif -#ifdef IFF_LINK2 - // Available in (at least) macOS, BSD - if (flags & IFF_LINK2) - if (!append_flag(py_retlist, "link2")) - goto error; -#endif -#ifdef IFF_D2 - // Available in (at least) AIX - if (flags & IFF_D2) - if (!append_flag(py_retlist, "d2")) - goto error; -#endif - - return py_retlist; - -error: - Py_DECREF(py_retlist); - if (sock != -1) - close(sock); - return NULL; -} - - -/* - * Inspect NIC flags, returns a bool indicating whether the NIC is - * running. References: - * http://www.i-scream.org/libstatgrab/ - */ -static PyObject * -psutil_net_if_is_running(PyObject *self, PyObject *args) { - char *nic_name; - int sock = -1; - int ret; - struct ifreq ifr; - - if (! PyArg_ParseTuple(args, "s", &nic_name)) - return NULL; - - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock == -1) - goto error; - - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); - ret = ioctl(sock, SIOCGIFFLAGS, &ifr); - if (ret == -1) - goto error; - - close(sock); - if ((ifr.ifr_flags & IFF_RUNNING) != 0) - return Py_BuildValue("O", Py_True); - else - return Py_BuildValue("O", Py_False); - -error: - if (sock != -1) - close(sock); - return PyErr_SetFromErrno(PyExc_OSError); -} - - -/* - * net_if_stats() macOS/BSD implementation. - */ -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - -int psutil_get_nic_speed(int ifm_active) { - // Determine NIC speed. Taken from: - // http://www.i-scream.org/libstatgrab/ - // Assuming only ETHER devices - switch(IFM_TYPE(ifm_active)) { - case IFM_ETHER: - switch(IFM_SUBTYPE(ifm_active)) { -#if defined(IFM_HPNA_1) && ((!defined(IFM_10G_LR)) \ - || (IFM_10G_LR != IFM_HPNA_1)) - // HomePNA 1.0 (1Mb/s) - case(IFM_HPNA_1): - return 1; -#endif - // 10 Mbit - case(IFM_10_T): // 10BaseT - RJ45 - case(IFM_10_2): // 10Base2 - Thinnet - case(IFM_10_5): // 10Base5 - AUI - case(IFM_10_STP): // 10BaseT over shielded TP - case(IFM_10_FL): // 10baseFL - Fiber - return 10; - // 100 Mbit - case(IFM_100_TX): // 100BaseTX - RJ45 - case(IFM_100_FX): // 100BaseFX - Fiber - case(IFM_100_T4): // 100BaseT4 - 4 pair cat 3 - case(IFM_100_VG): // 100VG-AnyLAN - case(IFM_100_T2): // 100BaseT2 - return 100; - // 1000 Mbit - case(IFM_1000_SX): // 1000BaseSX - multi-mode fiber - case(IFM_1000_LX): // 1000baseLX - single-mode fiber - case(IFM_1000_CX): // 1000baseCX - 150ohm STP -#if defined(IFM_1000_TX) && !defined(PSUTIL_OPENBSD) - #define HAS_CASE_IFM_1000_TX 1 - // FreeBSD 4 and others (but NOT OpenBSD) -> #define IFM_1000_T in net/if_media.h - case(IFM_1000_TX): -#endif -#ifdef IFM_1000_FX - case(IFM_1000_FX): -#endif -#if defined(IFM_1000_T) && (!HAS_CASE_IFM_1000_TX || IFM_1000_T != IFM_1000_TX) - case(IFM_1000_T): -#endif - return 1000; -#if defined(IFM_10G_SR) || defined(IFM_10G_LR) || defined(IFM_10G_CX4) \ - || defined(IFM_10G_T) -#ifdef IFM_10G_SR - case(IFM_10G_SR): -#endif -#ifdef IFM_10G_LR - case(IFM_10G_LR): -#endif -#ifdef IFM_10G_CX4 - case(IFM_10G_CX4): -#endif -#ifdef IFM_10G_TWINAX - case(IFM_10G_TWINAX): -#endif -#ifdef IFM_10G_TWINAX_LONG - case(IFM_10G_TWINAX_LONG): -#endif -#ifdef IFM_10G_T - case(IFM_10G_T): -#endif - return 10000; -#endif -#if defined(IFM_2500_SX) -#ifdef IFM_2500_SX - case(IFM_2500_SX): -#endif - return 2500; -#endif // any 2.5GBit stuff... - // We don't know what it is - default: - return 0; - } - break; - -#ifdef IFM_TOKEN - case IFM_TOKEN: - switch(IFM_SUBTYPE(ifm_active)) { - case IFM_TOK_STP4: // Shielded twisted pair 4m - DB9 - case IFM_TOK_UTP4: // Unshielded twisted pair 4m - RJ45 - return 4; - case IFM_TOK_STP16: // Shielded twisted pair 16m - DB9 - case IFM_TOK_UTP16: // Unshielded twisted pair 16m - RJ45 - return 16; -#if defined(IFM_TOK_STP100) || defined(IFM_TOK_UTP100) -#ifdef IFM_TOK_STP100 - case IFM_TOK_STP100: // Shielded twisted pair 100m - DB9 -#endif -#ifdef IFM_TOK_UTP100 - case IFM_TOK_UTP100: // Unshielded twisted pair 100m - RJ45 -#endif - return 100; -#endif - // We don't know what it is - default: - return 0; - } - break; -#endif - -#ifdef IFM_FDDI - case IFM_FDDI: - switch(IFM_SUBTYPE(ifm_active)) { - // We don't know what it is - default: - return 0; - } - break; -#endif - case IFM_IEEE80211: - switch(IFM_SUBTYPE(ifm_active)) { - case IFM_IEEE80211_FH1: // Frequency Hopping 1Mbps - case IFM_IEEE80211_DS1: // Direct Sequence 1Mbps - return 1; - case IFM_IEEE80211_FH2: // Frequency Hopping 2Mbps - case IFM_IEEE80211_DS2: // Direct Sequence 2Mbps - return 2; - case IFM_IEEE80211_DS5: // Direct Sequence 5Mbps - return 5; - case IFM_IEEE80211_DS11: // Direct Sequence 11Mbps - return 11; - case IFM_IEEE80211_DS22: // Direct Sequence 22Mbps - return 22; - // We don't know what it is - default: - return 0; - } - break; - - default: - return 0; - } -} - - -/* - * Return stats about a particular network interface. - * References: - * http://www.i-scream.org/libstatgrab/ - */ -static PyObject * -psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { - char *nic_name; - int sock = -1; - int ret; - int duplex; - int speed; - struct ifreq ifr; - struct ifmediareq ifmed; - - if (! PyArg_ParseTuple(args, "s", &nic_name)) - return NULL; - - sock = socket(AF_INET, SOCK_DGRAM, 0); - if (sock == -1) - return PyErr_SetFromErrno(PyExc_OSError); - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); - - // speed / duplex - memset(&ifmed, 0, sizeof(struct ifmediareq)); - strlcpy(ifmed.ifm_name, nic_name, sizeof(ifmed.ifm_name)); - ret = ioctl(sock, SIOCGIFMEDIA, (caddr_t)&ifmed); - if (ret == -1) { - speed = 0; - duplex = 0; - } - else { - speed = psutil_get_nic_speed(ifmed.ifm_active); - if ((ifmed.ifm_active | IFM_FDX) == ifmed.ifm_active) - duplex = 2; - else if ((ifmed.ifm_active | IFM_HDX) == ifmed.ifm_active) - duplex = 1; - else - duplex = 0; - } - - close(sock); - return Py_BuildValue("[ii]", duplex, speed); -} -#endif // net_if_stats() macOS/BSD implementation - - -#ifdef __cplusplus -extern "C" { -#endif - - -/* - * define the psutil C module methods and initialize the module. - */ static PyMethodDef mod_methods[] = { {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS}, {"getpriority", psutil_posix_getpriority, METH_VARARGS}, @@ -1028,7 +217,3 @@ PyInit__psutil_posix(void) { return mod; } - -#ifdef __cplusplus -} -#endif diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h index 5a37e48b15..ef0679fa32 100644 --- a/psutil/_psutil_posix.h +++ b/psutil/_psutil_posix.h @@ -5,5 +5,3 @@ */ long psutil_getpagesize(void); -int psutil_pid_exists(pid_t pid); -void psutil_raise_for_pid(pid_t pid, char *msg); diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 2a22c794ec..a18b2dfaae 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -18,6 +18,9 @@ #include "../../arch/bsd/init.h" #endif +#if defined(PSUTIL_POSIX) + #include "../../arch/posix/init.h" +#endif // print debug messages when set to 1 extern int PSUTIL_DEBUG; diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index 6848f1a4fb..759abf87c5 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -69,24 +69,23 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { py_ifc_info = Py_BuildValue( "(KKKKKKKi)", - if2m->ifm_data.ifi_obytes, - if2m->ifm_data.ifi_ibytes, - if2m->ifm_data.ifi_opackets, - if2m->ifm_data.ifi_ipackets, - if2m->ifm_data.ifi_ierrors, - if2m->ifm_data.ifi_oerrors, - if2m->ifm_data.ifi_iqdrops, + (unsigned long long)if2m->ifm_data.ifi_obytes, + (unsigned long long)if2m->ifm_data.ifi_ibytes, + (unsigned long long)if2m->ifm_data.ifi_opackets, + (unsigned long long)if2m->ifm_data.ifi_ipackets, + (unsigned long long)if2m->ifm_data.ifi_ierrors, + (unsigned long long)if2m->ifm_data.ifi_oerrors, + (unsigned long long)if2m->ifm_data.ifi_iqdrops, 0); // dropout not supported if (!py_ifc_info) goto error; - if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) + if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) { + Py_CLEAR(py_ifc_info); goto error; + } Py_CLEAR(py_ifc_info); } - else { - continue; - } } free(buf); diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h new file mode 100644 index 0000000000..ba0f6af5c2 --- /dev/null +++ b/psutil/arch/posix/init.h @@ -0,0 +1,7 @@ +/* + * 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. + */ + +#include "proc.h" diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c new file mode 100644 index 0000000000..c0b67214d8 --- /dev/null +++ b/psutil/arch/posix/net.c @@ -0,0 +1,688 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef PSUTIL_SUNOS10 + #include "arch/solaris/v10/ifaddrs.h" +#elif PSUTIL_AIX + #include "arch/aix/ifaddrs.h" +#else + #include +#endif + +#if defined(PSUTIL_LINUX) + #include + #include + #include +#endif +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) + #include + #include + #include + #include + #include + #include +#endif +#if defined(PSUTIL_SUNOS) + #include + #include +#endif +#if defined(PSUTIL_AIX) + #include +#endif + +#include "../../arch/all/init.h" + + +/* + * Translate a sockaddr struct into a Python string. + * Return None if address family is not AF_INET* or AF_PACKET. + */ +PyObject * +psutil_convert_ipaddr(struct sockaddr *addr, int family) { + char buf[NI_MAXHOST]; + int err; + int addrlen; + size_t n; + size_t len; + const char *data; + char *ptr; + + if (addr == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + else if (family == AF_INET || family == AF_INET6) { + if (family == AF_INET) + addrlen = sizeof(struct sockaddr_in); + else + addrlen = sizeof(struct sockaddr_in6); + err = getnameinfo(addr, addrlen, buf, sizeof(buf), NULL, 0, + NI_NUMERICHOST); + if (err != 0) { + // XXX we get here on FreeBSD when processing 'lo' / AF_INET6 + // broadcast. Not sure what to do other than returning None. + // ifconfig does not show anything BTW. + // PyErr_Format(PyExc_RuntimeError, gai_strerror(err)); + // return NULL; + Py_INCREF(Py_None); + return Py_None; + } + else { + return Py_BuildValue("s", buf); + } + } +#ifdef PSUTIL_LINUX + else if (family == AF_PACKET) { + struct sockaddr_ll *lladdr = (struct sockaddr_ll *)addr; + len = lladdr->sll_halen; + data = (const char *)lladdr->sll_addr; + } +#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) + else if (addr->sa_family == AF_LINK) { + // Note: prior to Python 3.4 socket module does not expose + // AF_LINK so we'll do. + struct sockaddr_dl *dladdr = (struct sockaddr_dl *)addr; + len = dladdr->sdl_alen; + data = LLADDR(dladdr); + } +#endif + else { + // unknown family + Py_INCREF(Py_None); + return Py_None; + } + + // AF_PACKET or AF_LINK + if (len > 0) { + ptr = buf; + for (n = 0; n < len; ++n) { + sprintf(ptr, "%02x:", data[n] & 0xff); + ptr += 3; + } + *--ptr = '\0'; + return Py_BuildValue("s", buf); + } + else { + Py_INCREF(Py_None); + return Py_None; + } +} + + +/* + * Return NICs information a-la ifconfig as a list of tuples. + * TODO: on Solaris we won't get any MAC address. + */ +PyObject* +psutil_net_if_addrs(PyObject* self, PyObject* args) { + struct ifaddrs *ifaddr, *ifa; + int family; + + PyObject *py_retlist = PyList_New(0); + PyObject *py_tuple = NULL; + PyObject *py_address = NULL; + PyObject *py_netmask = NULL; + PyObject *py_broadcast = NULL; + PyObject *py_ptp = NULL; + + if (py_retlist == NULL) + return NULL; + if (getifaddrs(&ifaddr) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + family = ifa->ifa_addr->sa_family; + py_address = psutil_convert_ipaddr(ifa->ifa_addr, family); + // If the primary address can't be determined just skip it. + // I've never seen this happen on Linux but I did on FreeBSD. + if (py_address == Py_None) + continue; + if (py_address == NULL) + goto error; + py_netmask = psutil_convert_ipaddr(ifa->ifa_netmask, family); + if (py_netmask == NULL) + goto error; + + if (ifa->ifa_flags & IFF_BROADCAST) { + py_broadcast = psutil_convert_ipaddr(ifa->ifa_broadaddr, family); + Py_INCREF(Py_None); + py_ptp = Py_None; + } + else if (ifa->ifa_flags & IFF_POINTOPOINT) { + py_ptp = psutil_convert_ipaddr(ifa->ifa_dstaddr, family); + Py_INCREF(Py_None); + py_broadcast = Py_None; + } + else { + Py_INCREF(Py_None); + Py_INCREF(Py_None); + py_broadcast = Py_None; + py_ptp = Py_None; + } + + if ((py_broadcast == NULL) || (py_ptp == NULL)) + goto error; + py_tuple = Py_BuildValue( + "(siOOOO)", + ifa->ifa_name, + family, + py_address, + py_netmask, + py_broadcast, + py_ptp + ); + + if (! py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + Py_CLEAR(py_address); + Py_CLEAR(py_netmask); + Py_CLEAR(py_broadcast); + Py_CLEAR(py_ptp); + } + + freeifaddrs(ifaddr); + return py_retlist; + +error: + if (ifaddr != NULL) + freeifaddrs(ifaddr); + Py_DECREF(py_retlist); + Py_XDECREF(py_tuple); + Py_XDECREF(py_address); + Py_XDECREF(py_netmask); + Py_XDECREF(py_broadcast); + Py_XDECREF(py_ptp); + return NULL; +} + + +/* + * Return NIC MTU. References: + * http://www.i-scream.org/libstatgrab/ + */ +PyObject * +psutil_net_if_mtu(PyObject *self, PyObject *args) { + char *nic_name; + int sock = -1; + int ret; +#ifdef PSUTIL_SUNOS10 + struct lifreq lifr; +#else + struct ifreq ifr; +#endif + + if (! PyArg_ParseTuple(args, "s", &nic_name)) + return NULL; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) + goto error; + +#ifdef PSUTIL_SUNOS10 + PSUTIL_STRNCPY(lifr.lifr_name, nic_name, sizeof(lifr.lifr_name)); + ret = ioctl(sock, SIOCGIFMTU, &lifr); +#else + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + ret = ioctl(sock, SIOCGIFMTU, &ifr); +#endif + if (ret == -1) + goto error; + close(sock); + +#ifdef PSUTIL_SUNOS10 + return Py_BuildValue("i", lifr.lifr_mtu); +#else + return Py_BuildValue("i", ifr.ifr_mtu); +#endif + +error: + if (sock != -1) + close(sock); + return PyErr_SetFromErrno(PyExc_OSError); +} + +static int +append_flag(PyObject *py_retlist, const char * flag_name) +{ + PyObject *py_str = NULL; + + py_str = PyUnicode_FromString(flag_name); + if (! py_str) + return 0; + if (PyList_Append(py_retlist, py_str)) { + Py_DECREF(py_str); + return 0; + } + Py_CLEAR(py_str); + + return 1; +} + +/* + * Get all of the NIC flags and return them. + */ +PyObject * +psutil_net_if_flags(PyObject *self, PyObject *args) { + char *nic_name; + int sock = -1; + int ret; + struct ifreq ifr; + PyObject *py_retlist = PyList_New(0); + short int flags; + + if (py_retlist == NULL) + return NULL; + + if (! PyArg_ParseTuple(args, "s", &nic_name)) + goto error; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) { + psutil_PyErr_SetFromOSErrnoWithSyscall("socket(SOCK_DGRAM)"); + goto error; + } + + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + ret = ioctl(sock, SIOCGIFFLAGS, &ifr); + if (ret == -1) { + psutil_PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCGIFFLAGS)"); + goto error; + } + + close(sock); + sock = -1; + + flags = ifr.ifr_flags & 0xFFFF; + + // Linux/glibc IFF flags: https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/gnu/net/if.h;h=251418f82331c0426e58707fe4473d454893b132;hb=HEAD + // macOS IFF flags: https://opensource.apple.com/source/xnu/xnu-792/bsd/net/if.h.auto.html + // AIX IFF flags: https://www.ibm.com/support/pages/how-hexadecimal-flags-displayed-ifconfig-are-calculated + // FreeBSD IFF flags: https://www.freebsd.org/cgi/man.cgi?query=if_allmulti&apropos=0&sektion=0&manpath=FreeBSD+10-current&format=html + +#ifdef IFF_UP + // Available in (at least) Linux, macOS, AIX, BSD + if (flags & IFF_UP) + if (!append_flag(py_retlist, "up")) + goto error; +#endif +#ifdef IFF_BROADCAST + // Available in (at least) Linux, macOS, AIX, BSD + if (flags & IFF_BROADCAST) + if (!append_flag(py_retlist, "broadcast")) + goto error; +#endif +#ifdef IFF_DEBUG + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_DEBUG) + if (!append_flag(py_retlist, "debug")) + goto error; +#endif +#ifdef IFF_LOOPBACK + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_LOOPBACK) + if (!append_flag(py_retlist, "loopback")) + goto error; +#endif +#ifdef IFF_POINTOPOINT + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_POINTOPOINT) + if (!append_flag(py_retlist, "pointopoint")) + goto error; +#endif +#ifdef IFF_NOTRAILERS + // Available in (at least) Linux, macOS, AIX + if (flags & IFF_NOTRAILERS) + if (!append_flag(py_retlist, "notrailers")) + goto error; +#endif +#ifdef IFF_RUNNING + // Available in (at least) Linux, macOS, AIX, BSD + if (flags & IFF_RUNNING) + if (!append_flag(py_retlist, "running")) + goto error; +#endif +#ifdef IFF_NOARP + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_NOARP) + if (!append_flag(py_retlist, "noarp")) + goto error; +#endif +#ifdef IFF_PROMISC + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_PROMISC) + if (!append_flag(py_retlist, "promisc")) + goto error; +#endif +#ifdef IFF_ALLMULTI + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_ALLMULTI) + if (!append_flag(py_retlist, "allmulti")) + goto error; +#endif +#ifdef IFF_MASTER + // Available in (at least) Linux + if (flags & IFF_MASTER) + if (!append_flag(py_retlist, "master")) + goto error; +#endif +#ifdef IFF_SLAVE + // Available in (at least) Linux + if (flags & IFF_SLAVE) + if (!append_flag(py_retlist, "slave")) + goto error; +#endif +#ifdef IFF_MULTICAST + // Available in (at least) Linux, macOS, BSD + if (flags & IFF_MULTICAST) + if (!append_flag(py_retlist, "multicast")) + goto error; +#endif +#ifdef IFF_PORTSEL + // Available in (at least) Linux + if (flags & IFF_PORTSEL) + if (!append_flag(py_retlist, "portsel")) + goto error; +#endif +#ifdef IFF_AUTOMEDIA + // Available in (at least) Linux + if (flags & IFF_AUTOMEDIA) + if (!append_flag(py_retlist, "automedia")) + goto error; +#endif +#ifdef IFF_DYNAMIC + // Available in (at least) Linux + if (flags & IFF_DYNAMIC) + if (!append_flag(py_retlist, "dynamic")) + goto error; +#endif +#ifdef IFF_OACTIVE + // Available in (at least) macOS, BSD + if (flags & IFF_OACTIVE) + if (!append_flag(py_retlist, "oactive")) + goto error; +#endif +#ifdef IFF_SIMPLEX + // Available in (at least) macOS, AIX, BSD + if (flags & IFF_SIMPLEX) + if (!append_flag(py_retlist, "simplex")) + goto error; +#endif +#ifdef IFF_LINK0 + // Available in (at least) macOS, BSD + if (flags & IFF_LINK0) + if (!append_flag(py_retlist, "link0")) + goto error; +#endif +#ifdef IFF_LINK1 + // Available in (at least) macOS, BSD + if (flags & IFF_LINK1) + if (!append_flag(py_retlist, "link1")) + goto error; +#endif +#ifdef IFF_LINK2 + // Available in (at least) macOS, BSD + if (flags & IFF_LINK2) + if (!append_flag(py_retlist, "link2")) + goto error; +#endif +#ifdef IFF_D2 + // Available in (at least) AIX + if (flags & IFF_D2) + if (!append_flag(py_retlist, "d2")) + goto error; +#endif + + return py_retlist; + +error: + Py_DECREF(py_retlist); + if (sock != -1) + close(sock); + return NULL; +} + + +/* + * Inspect NIC flags, returns a bool indicating whether the NIC is + * running. References: + * http://www.i-scream.org/libstatgrab/ + */ +PyObject * +psutil_net_if_is_running(PyObject *self, PyObject *args) { + char *nic_name; + int sock = -1; + int ret; + struct ifreq ifr; + + if (! PyArg_ParseTuple(args, "s", &nic_name)) + return NULL; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) + goto error; + + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + ret = ioctl(sock, SIOCGIFFLAGS, &ifr); + if (ret == -1) + goto error; + + close(sock); + if ((ifr.ifr_flags & IFF_RUNNING) != 0) + return Py_BuildValue("O", Py_True); + else + return Py_BuildValue("O", Py_False); + +error: + if (sock != -1) + close(sock); + return PyErr_SetFromErrno(PyExc_OSError); +} + + + +/* + * net_if_stats() macOS/BSD implementation. + */ +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) + +int psutil_get_nic_speed(int ifm_active) { + // Determine NIC speed. Taken from: + // http://www.i-scream.org/libstatgrab/ + // Assuming only ETHER devices + switch(IFM_TYPE(ifm_active)) { + case IFM_ETHER: + switch(IFM_SUBTYPE(ifm_active)) { +#if defined(IFM_HPNA_1) && ((!defined(IFM_10G_LR)) \ + || (IFM_10G_LR != IFM_HPNA_1)) + // HomePNA 1.0 (1Mb/s) + case(IFM_HPNA_1): + return 1; +#endif + // 10 Mbit + case(IFM_10_T): // 10BaseT - RJ45 + case(IFM_10_2): // 10Base2 - Thinnet + case(IFM_10_5): // 10Base5 - AUI + case(IFM_10_STP): // 10BaseT over shielded TP + case(IFM_10_FL): // 10baseFL - Fiber + return 10; + // 100 Mbit + case(IFM_100_TX): // 100BaseTX - RJ45 + case(IFM_100_FX): // 100BaseFX - Fiber + case(IFM_100_T4): // 100BaseT4 - 4 pair cat 3 + case(IFM_100_VG): // 100VG-AnyLAN + case(IFM_100_T2): // 100BaseT2 + return 100; + // 1000 Mbit + case(IFM_1000_SX): // 1000BaseSX - multi-mode fiber + case(IFM_1000_LX): // 1000baseLX - single-mode fiber + case(IFM_1000_CX): // 1000baseCX - 150ohm STP +#if defined(IFM_1000_TX) && !defined(PSUTIL_OPENBSD) + #define HAS_CASE_IFM_1000_TX 1 + // FreeBSD 4 and others (but NOT OpenBSD) -> #define IFM_1000_T in net/if_media.h + case(IFM_1000_TX): +#endif +#ifdef IFM_1000_FX + case(IFM_1000_FX): +#endif +#if defined(IFM_1000_T) && (!HAS_CASE_IFM_1000_TX || IFM_1000_T != IFM_1000_TX) + case(IFM_1000_T): +#endif + return 1000; +#if defined(IFM_10G_SR) || defined(IFM_10G_LR) || defined(IFM_10G_CX4) \ + || defined(IFM_10G_T) +#ifdef IFM_10G_SR + case(IFM_10G_SR): +#endif +#ifdef IFM_10G_LR + case(IFM_10G_LR): +#endif +#ifdef IFM_10G_CX4 + case(IFM_10G_CX4): +#endif +#ifdef IFM_10G_TWINAX + case(IFM_10G_TWINAX): +#endif +#ifdef IFM_10G_TWINAX_LONG + case(IFM_10G_TWINAX_LONG): +#endif +#ifdef IFM_10G_T + case(IFM_10G_T): +#endif + return 10000; +#endif +#if defined(IFM_2500_SX) +#ifdef IFM_2500_SX + case(IFM_2500_SX): +#endif + return 2500; +#endif // any 2.5GBit stuff... + // We don't know what it is + default: + return 0; + } + break; + +#ifdef IFM_TOKEN + case IFM_TOKEN: + switch(IFM_SUBTYPE(ifm_active)) { + case IFM_TOK_STP4: // Shielded twisted pair 4m - DB9 + case IFM_TOK_UTP4: // Unshielded twisted pair 4m - RJ45 + return 4; + case IFM_TOK_STP16: // Shielded twisted pair 16m - DB9 + case IFM_TOK_UTP16: // Unshielded twisted pair 16m - RJ45 + return 16; +#if defined(IFM_TOK_STP100) || defined(IFM_TOK_UTP100) +#ifdef IFM_TOK_STP100 + case IFM_TOK_STP100: // Shielded twisted pair 100m - DB9 +#endif +#ifdef IFM_TOK_UTP100 + case IFM_TOK_UTP100: // Unshielded twisted pair 100m - RJ45 +#endif + return 100; +#endif + // We don't know what it is + default: + return 0; + } + break; +#endif + +#ifdef IFM_FDDI + case IFM_FDDI: + switch(IFM_SUBTYPE(ifm_active)) { + // We don't know what it is + default: + return 0; + } + break; +#endif + case IFM_IEEE80211: + switch(IFM_SUBTYPE(ifm_active)) { + case IFM_IEEE80211_FH1: // Frequency Hopping 1Mbps + case IFM_IEEE80211_DS1: // Direct Sequence 1Mbps + return 1; + case IFM_IEEE80211_FH2: // Frequency Hopping 2Mbps + case IFM_IEEE80211_DS2: // Direct Sequence 2Mbps + return 2; + case IFM_IEEE80211_DS5: // Direct Sequence 5Mbps + return 5; + case IFM_IEEE80211_DS11: // Direct Sequence 11Mbps + return 11; + case IFM_IEEE80211_DS22: // Direct Sequence 22Mbps + return 22; + // We don't know what it is + default: + return 0; + } + break; + + default: + return 0; + } +} + + +/* + * Return stats about a particular network interface. + * References: + * http://www.i-scream.org/libstatgrab/ + */ +PyObject * +psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { + char *nic_name; + int sock = -1; + int ret; + int duplex; + int speed; + struct ifreq ifr; + struct ifmediareq ifmed; + + if (! PyArg_ParseTuple(args, "s", &nic_name)) + return NULL; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) + return PyErr_SetFromErrno(PyExc_OSError); + PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + + // speed / duplex + memset(&ifmed, 0, sizeof(struct ifmediareq)); + strlcpy(ifmed.ifm_name, nic_name, sizeof(ifmed.ifm_name)); + ret = ioctl(sock, SIOCGIFMEDIA, (caddr_t)&ifmed); + if (ret == -1) { + speed = 0; + duplex = 0; + } + else { + speed = psutil_get_nic_speed(ifmed.ifm_active); + if ((ifmed.ifm_active | IFM_FDX) == ifmed.ifm_active) + duplex = 2; + else if ((ifmed.ifm_active | IFM_HDX) == ifmed.ifm_active) + duplex = 1; + else + duplex = 0; + } + + close(sock); + return Py_BuildValue("[ii]", duplex, speed); +} +#endif // net_if_stats() macOS/BSD implementation diff --git a/psutil/arch/posix/net.h b/psutil/arch/posix/net.h new file mode 100644 index 0000000000..d65ecc3575 --- /dev/null +++ b/psutil/arch/posix/net.h @@ -0,0 +1,15 @@ +/* + * 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. + */ + +#include + +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); +#endif +PyObject *psutil_net_if_addrs(PyObject* self, PyObject* args); +PyObject *psutil_net_if_flags(PyObject *self, PyObject *args); +PyObject *psutil_net_if_is_running(PyObject *self, PyObject *args); +PyObject *psutil_net_if_mtu(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/proc.c b/psutil/arch/posix/proc.c new file mode 100644 index 0000000000..70aa735efd --- /dev/null +++ b/psutil/arch/posix/proc.c @@ -0,0 +1,120 @@ +/* + * 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. + */ + + +#include +#include + +#include "../../arch/all/init.h" + +// Check if PID exists. Return values: +// 1: exists +// 0: does not exist +// -1: error (Python exception is set) +int +psutil_pid_exists(pid_t pid) { + int ret; + + // No negative PID exists, plus -1 is an alias for sending signal + // too all processes except system ones. Not what we want. + if (pid < 0) + return 0; + + // As per "man 2 kill" PID 0 is an alias for sending the signal to + // every process in the process group of the calling process. + // Not what we want. Some platforms have PID 0, some do not. + // We decide that at runtime. + if (pid == 0) { +#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + return 0; +#else + return 1; +#endif + } + + ret = kill(pid , 0); + if (ret == 0) + return 1; + else { + if (errno == ESRCH) { + // ESRCH == No such process + return 0; + } + else if (errno == EPERM) { + // EPERM clearly indicates there's a process to deny + // access to. + return 1; + } + 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. + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + } +} + + +// Utility used for those syscalls which do not return a meaningful +// error that we can translate into an exception which makes sense. As +// such, we'll have to guess. On UNIX, if errno is set, we return that +// one (OSError). Else, if PID does not exist we assume the syscall +// failed because of that so we raise NoSuchProcess. If none of this is +// true we giveup and raise RuntimeError(msg). This will always set a +// Python exception and return NULL. +void +psutil_raise_for_pid(pid_t pid, char *syscall) { + if (errno != 0) + psutil_PyErr_SetFromOSErrnoWithSyscall(syscall); + else if (psutil_pid_exists(pid) == 0) + NoSuchProcess(syscall); + else + PyErr_Format(PyExc_RuntimeError, "%s syscall failed", syscall); +} + + +// Get PID priority. +PyObject * +psutil_posix_getpriority(PyObject *self, PyObject *args) { + pid_t pid; + int priority; + errno = 0; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + +#ifdef PSUTIL_OSX + priority = getpriority(PRIO_PROCESS, (id_t)pid); +#else + priority = getpriority(PRIO_PROCESS, pid); +#endif + if (errno != 0) + return PyErr_SetFromErrno(PyExc_OSError); + return Py_BuildValue("i", priority); +} + + +// Set PID priority. +PyObject * +psutil_posix_setpriority(PyObject *self, PyObject *args) { + pid_t pid; + int priority; + int retval; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) + return NULL; + +#ifdef PSUTIL_OSX + retval = setpriority(PRIO_PROCESS, (id_t)pid, priority); +#else + retval = setpriority(PRIO_PROCESS, pid, priority); +#endif + if (retval == -1) + return PyErr_SetFromErrno(PyExc_OSError); + Py_RETURN_NONE; +} diff --git a/psutil/arch/posix/proc.h b/psutil/arch/posix/proc.h new file mode 100644 index 0000000000..b70f24aabc --- /dev/null +++ b/psutil/arch/posix/proc.h @@ -0,0 +1,11 @@ +/* + * 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. + */ + +int psutil_pid_exists(pid_t pid); +void psutil_raise_for_pid(pid_t pid, char *msg); + +PyObject *psutil_posix_getpriority(PyObject *self, PyObject *args); +PyObject *psutil_posix_setpriority(PyObject *self, PyObject *args); diff --git a/setup.py b/setup.py index 8054b6b1f2..303cdb3101 100755 --- a/setup.py +++ b/setup.py @@ -132,6 +132,7 @@ sources = ['psutil/arch/all/init.c'] if POSIX: sources.append('psutil/_psutil_posix.c') + sources.extend(glob.glob("psutil/arch/posix/*.c")) def get_version(): From 01111d7850f0cfe8fcffe9372cd4ffd87cdfcb0a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Aug 2025 16:41:35 +0200 Subject: [PATCH 1299/1714] rm win test_uptime_1: it's flaky due to ctypes --- psutil/tests/test_windows.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index e9042fcd49..8950840876 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -308,21 +308,7 @@ def test_boot_time(self): diff = abs((wmi_btime_dt - psutil_dt).total_seconds()) assert diff <= 5, (psutil_dt, wmi_btime_dt) - def test_uptime_1(self): - # ...against QueryInterruptTime() (Windows 7+) - ULONGLONG = ctypes.c_ulonglong - - kernelbase = ctypes.WinDLL("kernelbase.dll") - QueryInterruptTime = kernelbase.QueryInterruptTime - QueryInterruptTime.argtypes = [ctypes.POINTER(ULONGLONG)] - QueryInterruptTime.restype = ctypes.c_bool - - interrupt_time_100ns = ULONGLONG(0) - assert QueryInterruptTime(ctypes.byref(interrupt_time_100ns)) - secs = interrupt_time_100ns.value / 10000000.0 - assert abs(cext.uptime() - secs) < 0.5 - - def test_uptime_2(self): + def test_uptime(self): # ...against GetTickCount64() (Windows < 7, does not include # time spent during suspend / hybernate). ms = ctypes.windll.kernel32.GetTickCount64() From 064fb39d45615e1fdd3b5063ad99aa057291dc01 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 9 Aug 2025 14:52:59 +0200 Subject: [PATCH 1300/1714] Refact `psutil_users()` C code (#2616) ...move it under the arch/posix namespace in order to avoid code repetition. --- MANIFEST.in | 6 +- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 5 +- psutil/_pslinux.py | 2 +- psutil/_psosx.py | 2 +- psutil/_pssunos.py | 2 +- psutil/_psutil_aix.c | 66 ------------- psutil/_psutil_bsd.c | 3 + psutil/_psutil_linux.c | 2 - psutil/_psutil_osx.c | 1 - psutil/_psutil_posix.c | 3 + psutil/_psutil_posix.h | 1 + psutil/_psutil_sunos.c | 1 - psutil/arch/bsd/sys.c | 125 ------------------------- psutil/arch/bsd/sys.h | 1 - psutil/arch/openbsd/users.c | 67 +++++++++++++ psutil/arch/{linux => openbsd}/users.h | 0 psutil/arch/osx/sys.c | 59 ------------ psutil/arch/osx/sys.h | 1 - psutil/arch/posix/init.h | 3 + psutil/arch/{linux => posix}/users.c | 56 +++++++++-- psutil/arch/posix/users.h | 11 +++ psutil/arch/sunos/sys.c | 61 ------------ psutil/arch/sunos/sys.h | 1 - 24 files changed, 146 insertions(+), 335 deletions(-) create mode 100644 psutil/arch/openbsd/users.c rename psutil/arch/{linux => openbsd}/users.h (100%) rename psutil/arch/{linux => posix}/users.c (74%) create mode 100644 psutil/arch/posix/users.h diff --git a/MANIFEST.in b/MANIFEST.in index 8f2edde2f0..6c96e43152 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -83,8 +83,6 @@ include psutil/arch/linux/net.c include psutil/arch/linux/net.h include psutil/arch/linux/proc.c include psutil/arch/linux/proc.h -include psutil/arch/linux/users.c -include psutil/arch/linux/users.h include psutil/arch/netbsd/cpu.c include psutil/arch/netbsd/cpu.h include psutil/arch/netbsd/disk.c @@ -105,6 +103,8 @@ include psutil/arch/openbsd/proc.c include psutil/arch/openbsd/proc.h include psutil/arch/openbsd/socks.c include psutil/arch/openbsd/socks.h +include psutil/arch/openbsd/users.c +include psutil/arch/openbsd/users.h include psutil/arch/osx/cpu.c include psutil/arch/osx/cpu.h include psutil/arch/osx/disk.c @@ -126,6 +126,8 @@ include psutil/arch/posix/net.c include psutil/arch/posix/net.h include psutil/arch/posix/proc.c include psutil/arch/posix/proc.h +include psutil/arch/posix/users.c +include psutil/arch/posix/users.h include psutil/arch/sunos/cpu.c include psutil/arch/sunos/cpu.h include psutil/arch/sunos/disk.c diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 0db127e6a3..5d5d1edaa2 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -278,7 +278,7 @@ def boot_time(): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext.users() + rawlist = cext_posix.users() localhost = (':0.0', ':0') for item in rawlist: user, tty, hostname, tstamp, user_process, pid = item diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 0273b1fa9a..e71e6cdd4f 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -506,12 +506,9 @@ def adjust_proc_create_time(ctime): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext.users() + rawlist = cext.users() if OPENBSD else cext_posix.users() for item in rawlist: user, tty, hostname, tstamp, pid = item - if pid == -1: - assert OPENBSD - pid = None if tty == '~': continue # reboot or shutdown nt = _common.suser(user, tty or None, hostname, tstamp, pid) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 232a701406..115cbb2f26 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1545,7 +1545,7 @@ def multi_bcat(*paths): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext.users() + rawlist = cext_posix.users() for item in rawlist: user, tty, hostname, tstamp, pid = item nt = _common.suser(user, tty or None, hostname, tstamp, pid) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 04f226c9b8..18e19a8801 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -314,7 +314,7 @@ def adjust_proc_create_time(ctime): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext.users() + rawlist = cext_posix.users() for item in rawlist: user, tty, hostname, tstamp, pid = item if tty == '~': diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index d9625df340..1cd0430ff3 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -310,7 +310,7 @@ def boot_time(): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext.users() + rawlist = cext_posix.users() localhost = (':0.0', ':0') for item in rawlist: user, tty, hostname, tstamp, user_process, pid = item diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 4b278da5c3..4727c2d966 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -465,71 +465,6 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { } -/* - * Return users currently connected on the system. - */ -static PyObject * -psutil_users(PyObject *self, PyObject *args) { - struct utmpx *ut; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_user_proc = NULL; - - if (py_retlist == NULL) - return NULL; - - setutxent(); - while (NULL != (ut = getutxent())) { - if (ut->ut_type == USER_PROCESS) - py_user_proc = Py_True; - else - py_user_proc = Py_False; - py_username = PyUnicode_DecodeFSDefault(ut->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); - if (! py_hostname) - goto error; - py_tuple = Py_BuildValue( - "(OOOdOi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut->ut_tv.tv_sec, // tstamp - py_user_proc, // (bool) user process - ut->ut_pid // process id - ); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - endutxent(); - - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (ut != NULL) - endutxent(); - return NULL; -} - - /* * Return disk mounted partitions as a list of tuples including device, * mount point and filesystem type. @@ -1009,7 +944,6 @@ PsutilMethods[] = {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"swap_mem", psutil_swap_mem, METH_VARARGS}, - {"users", psutil_users, METH_VARARGS}, {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, #if defined(CURR_VERSION_NETINTERFACE) && CURR_VERSION_NETINTERFACE >= 3 {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 34f951629b..57ff49be04 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -43,6 +43,7 @@ #include "arch/openbsd/mem.h" #include "arch/openbsd/proc.h" #include "arch/openbsd/socks.h" + #include "arch/openbsd/users.h" #elif PSUTIL_NETBSD #include "arch/netbsd/cpu.h" #include "arch/netbsd/disk.h" @@ -91,7 +92,9 @@ static PyMethodDef mod_methods[] = { {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"pids", psutil_pids, METH_VARARGS}, {"swap_mem", psutil_swap_mem, METH_VARARGS}, +#if defined(PSUTIL_OPENBSD) {"users", psutil_users, METH_VARARGS}, +#endif {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 9f910f0360..421d9c283e 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -17,7 +17,6 @@ #include "arch/linux/mem.h" #include "arch/linux/net.h" #include "arch/linux/proc.h" -#include "arch/linux/users.h" // May happen on old RedHat versions, see: // https://github.com/giampaolo/psutil/issues/607 @@ -37,7 +36,6 @@ static PyMethodDef mod_methods[] = { #endif // --- system related functions {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, - {"users", psutil_users, METH_VARARGS}, {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, // --- linux specific {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS}, diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 2103bf052a..0e8b87b49d 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -51,7 +51,6 @@ static PyMethodDef mod_methods[] = { {"pids", psutil_pids, METH_VARARGS}, {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, {"swap_mem", psutil_swap_mem, METH_VARARGS}, - {"users", psutil_users, METH_VARARGS}, {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, // --- others diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index ff145f9ec1..4b08a8877b 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -63,6 +63,9 @@ static PyMethodDef mod_methods[] = { {"setpriority", psutil_posix_setpriority, METH_VARARGS}, #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, +#endif +#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) + {"users", psutil_users, METH_VARARGS}, #endif {NULL, NULL, 0, NULL} }; diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h index ef0679fa32..21b96600ed 100644 --- a/psutil/_psutil_posix.h +++ b/psutil/_psutil_posix.h @@ -5,3 +5,4 @@ */ long psutil_getpagesize(void); +PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 2585a6b667..54b40651c6 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -58,7 +58,6 @@ static PyMethodDef mod_methods[] = { {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"swap_mem", psutil_swap_mem, METH_VARARGS}, - {"users", psutil_users, METH_VARARGS}, // --- others {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, diff --git a/psutil/arch/bsd/sys.c b/psutil/arch/bsd/sys.c index 64ffd2b1e9..9af40e7403 100644 --- a/psutil/arch/bsd/sys.c +++ b/psutil/arch/bsd/sys.c @@ -28,128 +28,3 @@ psutil_boot_time(PyObject *self, PyObject *args) { return PyErr_SetFromErrno(PyExc_OSError); return Py_BuildValue("d", (double)boottime.tv_sec); } - - -PyObject * -psutil_users(PyObject *self, PyObject *args) { - PyObject *py_retlist = PyList_New(0); - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_tuple = NULL; - PyObject *py_pid = NULL; - - if (py_retlist == NULL) - return NULL; - -#if defined(PSUTIL_OPENBSD) - struct utmp ut; - FILE *fp; - - Py_BEGIN_ALLOW_THREADS - fp = fopen(_PATH_UTMP, "r"); - Py_END_ALLOW_THREADS - if (fp == NULL) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, _PATH_UTMP); - goto error; - } - - while (fread(&ut, sizeof(ut), 1, fp) == 1) { - if (*ut.ut_name == '\0') - continue; - py_username = PyUnicode_DecodeFSDefault(ut.ut_name); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(ut.ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(ut.ut_host); - if (! py_hostname) - goto error; - py_tuple = Py_BuildValue( - "(OOOdi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut.ut_time, // start time -#if defined(PSUTIL_OPENBSD) - -1 // process id (set to None later) -#else - ut.ut_pid // TODO: use PyLong_FromPid -#endif - ); - if (!py_tuple) { - fclose(fp); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - fclose(fp); - goto error; - } - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - - fclose(fp); -#else - struct utmpx *utx; - setutxent(); - while ((utx = getutxent()) != NULL) { - if (utx->ut_type != USER_PROCESS) - continue; - py_username = PyUnicode_DecodeFSDefault(utx->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(utx->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); - if (! py_hostname) - goto error; -#if defined(PSUTIL_OPENBSD) - py_pid = Py_BuildValue("i", -1); // set to None later -#else - py_pid = PyLong_FromPid(utx->ut_pid); -#endif - if (! py_pid) - goto error; - - py_tuple = Py_BuildValue( - "(OOOdO)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)utx->ut_tv.tv_sec, // start time - py_pid // process id - ); - - if (!py_tuple) { - endutxent(); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - endutxent(); - goto error; - } - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - Py_CLEAR(py_pid); - } - - endutxent(); -#endif - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - return NULL; -} diff --git a/psutil/arch/bsd/sys.h b/psutil/arch/bsd/sys.h index 344ca21d42..5ec2d6bc4a 100644 --- a/psutil/arch/bsd/sys.h +++ b/psutil/arch/bsd/sys.h @@ -7,4 +7,3 @@ #include PyObject *psutil_boot_time(PyObject *self, PyObject *args); -PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/users.c b/psutil/arch/openbsd/users.c new file mode 100644 index 0000000000..e2496543d7 --- /dev/null +++ b/psutil/arch/openbsd/users.c @@ -0,0 +1,67 @@ +#include +#include + +PyObject * +psutil_users(PyObject *self, PyObject *args) { + PyObject *py_retlist = PyList_New(0); + PyObject *py_username = NULL; + PyObject *py_tty = NULL; + PyObject *py_hostname = NULL; + PyObject *py_tuple = NULL; + + if (py_retlist == NULL) + return NULL; + + struct utmp ut; + FILE *fp; + + Py_BEGIN_ALLOW_THREADS + fp = fopen(_PATH_UTMP, "r"); + Py_END_ALLOW_THREADS + if (fp == NULL) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, _PATH_UTMP); + goto error; + } + + while (fread(&ut, sizeof(ut), 1, fp) == 1) { + if (*ut.ut_name == '\0') + continue; + py_username = PyUnicode_DecodeFSDefault(ut.ut_name); + if (! py_username) + goto error; + py_tty = PyUnicode_DecodeFSDefault(ut.ut_line); + if (! py_tty) + goto error; + py_hostname = PyUnicode_DecodeFSDefault(ut.ut_host); + if (! py_hostname) + goto error; + py_tuple = Py_BuildValue( + "(OOOdi)", + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)ut.ut_time, // start time + Py_None // pid + ); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_username); + Py_CLEAR(py_tty); + Py_CLEAR(py_hostname); + Py_CLEAR(py_tuple); + } + + fclose(fp); + return py_retlist; + +error: + fclose(fp); + Py_XDECREF(py_username); + Py_XDECREF(py_tty); + Py_XDECREF(py_hostname); + Py_XDECREF(py_tuple); + Py_DECREF(py_retlist); + return NULL; +} diff --git a/psutil/arch/linux/users.h b/psutil/arch/openbsd/users.h similarity index 100% rename from psutil/arch/linux/users.h rename to psutil/arch/openbsd/users.h diff --git a/psutil/arch/osx/sys.c b/psutil/arch/osx/sys.c index 6be5b809f4..6f528f0217 100644 --- a/psutil/arch/osx/sys.c +++ b/psutil/arch/osx/sys.c @@ -10,7 +10,6 @@ #include #include -#include #include "../../arch/all/init.h" @@ -28,61 +27,3 @@ psutil_boot_time(PyObject *self, PyObject *args) { boot_time = result.tv_sec; return Py_BuildValue("d", (double)boot_time); } - - -PyObject * -psutil_users(PyObject *self, PyObject *args) { - struct utmpx *utx; - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - while ((utx = getutxent()) != NULL) { - if (utx->ut_type != USER_PROCESS) - continue; - py_username = PyUnicode_DecodeFSDefault(utx->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(utx->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(utx->ut_host); - if (! py_hostname) - goto error; - py_tuple = Py_BuildValue( - "(OOOdi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)utx->ut_tv.tv_sec, // start time - utx->ut_pid // process id - ); - if (!py_tuple) { - endutxent(); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - endutxent(); - goto error; - } - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - - endutxent(); - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - return NULL; -} diff --git a/psutil/arch/osx/sys.h b/psutil/arch/osx/sys.h index 344ca21d42..5ec2d6bc4a 100644 --- a/psutil/arch/osx/sys.h +++ b/psutil/arch/osx/sys.h @@ -7,4 +7,3 @@ #include PyObject *psutil_boot_time(PyObject *self, PyObject *args); -PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index ba0f6af5c2..6586e44df1 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -5,3 +5,6 @@ */ #include "proc.h" +#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) + #include "users.h" +#endif diff --git a/psutil/arch/linux/users.c b/psutil/arch/posix/users.c similarity index 74% rename from psutil/arch/linux/users.c rename to psutil/arch/posix/users.c index ff2b39e745..15fca3d5ca 100644 --- a/psutil/arch/linux/users.c +++ b/psutil/arch/posix/users.c @@ -4,35 +4,75 @@ * found in the LICENSE file. */ +#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) + #include -#include #include +#if defined(PSUTIL_LINUX) + #include +#else + #include +#endif + #include "../../arch/all/init.h" +static void +setup() { + #if defined(PSUTIL_LINUX) + setutent(); + #else + setutxent(); + #endif +} + + +static void +teardown() { + #if defined(PSUTIL_LINUX) + endutent(); + #else + endutxent(); + #endif +} + + PyObject * psutil_users(PyObject *self, PyObject *args) { +#if defined(PSUTIL_LINUX) struct utmp *ut; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; +#else + struct utmpx *ut; +#endif PyObject *py_username = NULL; PyObject *py_tty = NULL; PyObject *py_hostname = NULL; + PyObject *py_tuple = NULL; + PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) return NULL; - setutent(); - while (NULL != (ut = getutent())) { + + setup(); + +#if defined(PSUTIL_LINUX) + while ((ut = getutent()) != NULL) { +#else + while ((ut = getutxent()) != NULL) { +#endif if (ut->ut_type != USER_PROCESS) continue; py_tuple = NULL; + py_username = PyUnicode_DecodeFSDefault(ut->ut_user); if (! py_username) goto error; + py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); if (! py_tty) goto error; + if (strcmp(ut->ut_host, ":0") == 0 || strcmp(ut->ut_host, ":0.0") == 0) py_hostname = PyUnicode_DecodeFSDefault("localhost"); else @@ -57,15 +97,17 @@ psutil_users(PyObject *self, PyObject *args) { Py_CLEAR(py_hostname); Py_CLEAR(py_tuple); } - endutent(); + + teardown(); return py_retlist; error: + teardown(); Py_XDECREF(py_username); Py_XDECREF(py_tty); Py_XDECREF(py_hostname); Py_XDECREF(py_tuple); Py_DECREF(py_retlist); - endutent(); return NULL; } +#endif // !defined(PLATFORMS…) diff --git a/psutil/arch/posix/users.h b/psutil/arch/posix/users.h new file mode 100644 index 0000000000..0f373d0639 --- /dev/null +++ b/psutil/arch/posix/users.h @@ -0,0 +1,11 @@ +/* + * 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. + */ + +#include + +#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) +PyObject *psutil_users(PyObject* self, PyObject* args); +#endif diff --git a/psutil/arch/sunos/sys.c b/psutil/arch/sunos/sys.c index 50b85c9bf6..85f5a8fa9f 100644 --- a/psutil/arch/sunos/sys.c +++ b/psutil/arch/sunos/sys.c @@ -31,64 +31,3 @@ psutil_boot_time(PyObject *self, PyObject *args) { } return Py_BuildValue("f", boot_time); } - - -PyObject * -psutil_users(PyObject *self, PyObject *args) { - struct utmpx *ut; - PyObject *py_tuple = NULL; - PyObject *py_username = NULL; - PyObject *py_tty = NULL; - PyObject *py_hostname = NULL; - PyObject *py_user_proc = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - setutxent(); - while (NULL != (ut = getutxent())) { - if (ut->ut_type == USER_PROCESS) - py_user_proc = Py_True; - else - py_user_proc = Py_False; - py_username = PyUnicode_DecodeFSDefault(ut->ut_user); - if (! py_username) - goto error; - py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); - if (! py_tty) - goto error; - py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); - if (! py_hostname) - goto error; - py_tuple = Py_BuildValue( - "(OOOdOi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut->ut_tv.tv_sec, // tstamp - py_user_proc, // (bool) user process - ut->ut_pid // process id - ); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_username); - Py_CLEAR(py_tty); - Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); - } - endutxent(); - - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tty); - Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - endutxent(); - return NULL; -} diff --git a/psutil/arch/sunos/sys.h b/psutil/arch/sunos/sys.h index 9fc6510c6d..9695dd7009 100644 --- a/psutil/arch/sunos/sys.h +++ b/psutil/arch/sunos/sys.h @@ -7,4 +7,3 @@ #include PyObject *psutil_boot_time(PyObject *self, PyObject *args); -PyObject *psutil_users(PyObject *self, PyObject *args); From e7d619f82cc971a2184c4d498488de87b3d4a91e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 9 Aug 2025 15:01:13 +0200 Subject: [PATCH 1301/1714] On linux, just use utmpx.h instead of utmp.h --- psutil/arch/posix/users.c | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c index 15fca3d5ca..706ee57032 100644 --- a/psutil/arch/posix/users.c +++ b/psutil/arch/posix/users.c @@ -9,42 +9,26 @@ #include #include -#if defined(PSUTIL_LINUX) - #include -#else - #include -#endif +#include #include "../../arch/all/init.h" static void setup() { - #if defined(PSUTIL_LINUX) - setutent(); - #else - setutxent(); - #endif + setutxent(); } static void teardown() { - #if defined(PSUTIL_LINUX) - endutent(); - #else - endutxent(); - #endif + endutxent(); } PyObject * psutil_users(PyObject *self, PyObject *args) { -#if defined(PSUTIL_LINUX) - struct utmp *ut; -#else struct utmpx *ut; -#endif PyObject *py_username = NULL; PyObject *py_tty = NULL; PyObject *py_hostname = NULL; @@ -56,11 +40,7 @@ psutil_users(PyObject *self, PyObject *args) { setup(); -#if defined(PSUTIL_LINUX) - while ((ut = getutent()) != NULL) { -#else while ((ut = getutxent()) != NULL) { -#endif if (ut->ut_type != USER_PROCESS) continue; py_tuple = NULL; From ad518298e9fd4c70ec3af6a7d5939bb3ea1def34 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 9 Aug 2025 17:48:15 +0200 Subject: [PATCH 1302/1714] do linux --- psutil/_psutil_linux.c | 8 ++------ psutil/arch/all/init.h | 4 +++- psutil/arch/linux/disk.c | 4 ++-- psutil/arch/linux/mem.c | 4 ++-- psutil/arch/linux/net.c | 4 ++-- psutil/arch/linux/proc.c | 5 ++--- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 421d9c283e..d2efab0a4a 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -6,18 +6,14 @@ * Linux-specific functions. */ +#include "arch/all/init.h" + #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif #include #include // DUPLEX_* -#include "arch/all/init.h" -#include "arch/linux/disk.h" -#include "arch/linux/mem.h" -#include "arch/linux/net.h" -#include "arch/linux/proc.h" - // May happen on old RedHat versions, see: // https://github.com/giampaolo/psutil/issues/607 #ifndef DUPLEX_UNKNOWN diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index a18b2dfaae..8f50d49792 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -10,7 +10,9 @@ // We do this so that all .c files have to include only one header // (ourselves, init.h). -#if defined(PSUTIL_WINDOWS) +#if defined(PSUTIL_LINUX) + #include "../../arch/linux/init.h" +#elif defined(PSUTIL_WINDOWS) #include "../../arch/windows/init.h" #elif defined(PSUTIL_OSX) #include "../../arch/osx/init.h" diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c index 0fb82a46fc..fb6726af98 100644 --- a/psutil/arch/linux/disk.c +++ b/psutil/arch/linux/disk.c @@ -4,11 +4,11 @@ * found in the LICENSE file. */ +#include "../../arch/all/init.h" + #include #include -#include "../../arch/all/init.h" - // Return disk mounted partitions as a list of tuples including device, // mount point and filesystem type. diff --git a/psutil/arch/linux/mem.c b/psutil/arch/linux/mem.c index cacf1e7766..8bdd7ada15 100644 --- a/psutil/arch/linux/mem.c +++ b/psutil/arch/linux/mem.c @@ -4,11 +4,11 @@ * found in the LICENSE file. */ +#include "../../arch/all/init.h" + #include #include -#include "../../arch/all/init.h" - PyObject * psutil_linux_sysinfo(PyObject *self, PyObject *args) { diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index 774d67f0f2..efab18e6c8 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -4,6 +4,8 @@ * found in the LICENSE file. */ +#include "../../arch/all/init.h" + #include #include #include @@ -25,8 +27,6 @@ #define _LINUX_SYSINFO_H #include -#include "../../arch/all/init.h" - // * defined in linux/ethtool.h but not always available (e.g. Android) // * #ifdef check needed for old kernels, see: diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index 57a0f90885..c641543148 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -4,14 +4,13 @@ * found in the LICENSE file. */ +#include "../../arch/all/init.h" + #include #include #include #include -#include "../../arch/all/init.h" -#include "proc.h" - #ifdef PSUTIL_HAVE_IOPRIO enum { From caeeae128b21444fbde6297cefbcbb1aefa70e4c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 9 Aug 2025 23:43:33 +0200 Subject: [PATCH 1303/1714] centralize logic in init.h --- MANIFEST.in | 4 +- psutil/_psutil_bsd.c | 1 - psutil/_psutil_posix.c | 39 +----------------- psutil/arch/all/init.h | 20 ++++++--- psutil/arch/bsd/proc.c | 1 - psutil/arch/freebsd/cpu.c | 1 - psutil/arch/freebsd/disk.c | 1 - psutil/arch/freebsd/mem.c | 2 - psutil/arch/freebsd/proc.c | 1 - psutil/arch/freebsd/proc_socks.c | 1 - psutil/arch/freebsd/sensors.c | 1 - psutil/arch/freebsd/sys_socks.c | 1 - psutil/{_psutil_posix.h => arch/linux/init.h} | 6 ++- psutil/arch/netbsd/mem.c | 1 - psutil/arch/netbsd/proc.c | 1 - psutil/arch/netbsd/socks.c | 1 - psutil/arch/openbsd/cpu.c | 2 + psutil/arch/openbsd/disk.c | 2 + psutil/arch/openbsd/init.h | 12 ++++++ psutil/arch/openbsd/mem.c | 2 +- psutil/arch/openbsd/proc.c | 1 - psutil/arch/openbsd/socks.c | 1 - psutil/arch/openbsd/socks.h | 2 + psutil/arch/openbsd/users.c | 10 +++++ psutil/arch/osx/cpu.c | 1 - psutil/arch/osx/mem.c | 2 +- psutil/arch/osx/proc.c | 1 - psutil/arch/posix/init.c | 41 +++++++++++++++++++ psutil/arch/posix/init.h | 4 ++ 29 files changed, 97 insertions(+), 66 deletions(-) rename psutil/{_psutil_posix.h => arch/linux/init.h} (66%) create mode 100644 psutil/arch/openbsd/init.h create mode 100644 psutil/arch/posix/init.c diff --git a/MANIFEST.in b/MANIFEST.in index 6c96e43152..a701f6f2bd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -36,7 +36,6 @@ include psutil/_psutil_bsd.c include psutil/_psutil_linux.c include psutil/_psutil_osx.c include psutil/_psutil_posix.c -include psutil/_psutil_posix.h include psutil/_psutil_sunos.c include psutil/_psutil_windows.c include psutil/_pswindows.py @@ -77,6 +76,7 @@ include psutil/arch/freebsd/sys_socks.c include psutil/arch/freebsd/sys_socks.h include psutil/arch/linux/disk.c include psutil/arch/linux/disk.h +include psutil/arch/linux/init.h include psutil/arch/linux/mem.c include psutil/arch/linux/mem.h include psutil/arch/linux/net.c @@ -97,6 +97,7 @@ include psutil/arch/openbsd/cpu.c include psutil/arch/openbsd/cpu.h include psutil/arch/openbsd/disk.c include psutil/arch/openbsd/disk.h +include psutil/arch/openbsd/init.h include psutil/arch/openbsd/mem.c include psutil/arch/openbsd/mem.h include psutil/arch/openbsd/proc.c @@ -121,6 +122,7 @@ include psutil/arch/osx/sensors.c include psutil/arch/osx/sensors.h include psutil/arch/osx/sys.c include psutil/arch/osx/sys.h +include psutil/arch/posix/init.c include psutil/arch/posix/init.h include psutil/arch/posix/net.c include psutil/arch/posix/net.h diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 57ff49be04..ebf5ae47fa 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -22,7 +22,6 @@ #include // for TCP connection states #include "arch/all/init.h" -#include "_psutil_posix.h" #include "arch/bsd/cpu.h" #include "arch/bsd/disk.h" #include "arch/bsd/net.h" diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 4b08a8877b..349456e505 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -11,48 +11,11 @@ #include #include "arch/all/init.h" +#include "arch/posix/init.h" #include "arch/posix/proc.h" #include "arch/posix/net.h" -// ==================================================================== -// --- Utils -// ==================================================================== - - -/* - * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: - * - * > In SUSv2 the getpagesize() call is labeled LEGACY, and in POSIX.1-2001 - * > it has been dropped. - * > Portable applications should employ sysconf(_SC_PAGESIZE) instead - * > of getpagesize(). - * > Most systems allow the synonym _SC_PAGE_SIZE for _SC_PAGESIZE. - * > Whether getpagesize() is present as a Linux system call depends on the - * > architecture. - */ -long -psutil_getpagesize(void) { -#ifdef _SC_PAGESIZE - // recommended POSIX - return sysconf(_SC_PAGESIZE); -#elif _SC_PAGE_SIZE - // alias - return sysconf(_SC_PAGE_SIZE); -#else - // legacy - return (long) getpagesize(); -#endif -} - - -// Exposed so we can test it against Python's stdlib. -static PyObject * -psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { - return Py_BuildValue("l", psutil_getpagesize()); -} - - static PyMethodDef mod_methods[] = { {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS}, {"getpriority", psutil_posix_getpriority, METH_VARARGS}, diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 8f50d49792..6483eec2da 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -10,18 +10,26 @@ // We do this so that all .c files have to include only one header // (ourselves, init.h). + +#if defined(PSUTIL_POSIX) + #include "../../arch/posix/init.h" +#endif +#if defined(PSUTIL_BSD) + #include "../../arch/bsd/init.h" +#endif + #if defined(PSUTIL_LINUX) #include "../../arch/linux/init.h" #elif defined(PSUTIL_WINDOWS) #include "../../arch/windows/init.h" #elif defined(PSUTIL_OSX) #include "../../arch/osx/init.h" -#elif defined(PSUTIL_BSD) - #include "../../arch/bsd/init.h" -#endif - -#if defined(PSUTIL_POSIX) - #include "../../arch/posix/init.h" +#elif defined(PSUTIL_FREEBSD) + #include "../../arch/freebsd/init.h" +#elif defined(PSUTIL_OPENSBD) + #include "../../arch/openbsd/init.h" +#elif defined(PSUTIL_NETBSD) + #include "../../arch/netbsd/init.h" #endif // print debug messages when set to 1 diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index d11b7e055f..6799d0db1e 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -17,7 +17,6 @@ #endif #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #ifdef PSUTIL_FREEBSD #include "../../arch/freebsd/proc.h" #elif PSUTIL_OPENBSD diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 822d2267f2..d735e4b65a 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -21,7 +21,6 @@ For reference, here's the git history with original(ish) implementations: #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" PyObject * diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index 3a44975ecc..16f289f3c4 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -9,7 +9,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // convert a bintime struct to milliseconds diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 8b08eeb3a6..70aeafbf3a 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -15,8 +15,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" - #ifndef _PATH_DEVNULL #define _PATH_DEVNULL "/dev/null" diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index b03b43e6f6..a0381e0995 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -22,7 +22,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 07a40dabc1..d56487aee9 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -20,7 +20,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // The tcplist fetching and walking is borrowed from netstat/inet.c. diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 98e57c8353..96738dd7c3 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -17,7 +17,6 @@ For reference, here's the git history with original(ish) implementations: #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define DECIKELVIN_2_CELSIUS(t) (t - 2731) / 10 diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 00efb501d7..f8d09ab3ef 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -24,7 +24,6 @@ #include // for inet_ntop() #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" static int diff --git a/psutil/_psutil_posix.h b/psutil/arch/linux/init.h similarity index 66% rename from psutil/_psutil_posix.h rename to psutil/arch/linux/init.h index 21b96600ed..cef9ae2651 100644 --- a/psutil/_psutil_posix.h +++ b/psutil/arch/linux/init.h @@ -4,5 +4,7 @@ * found in the LICENSE file. */ -long psutil_getpagesize(void); -PyObject *psutil_users(PyObject *self, PyObject *args); +#include "proc.h" +#include "disk.h" +#include "mem.h" +#include "net.h" diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 8fe92f5ec4..2f8228a6c1 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -20,7 +20,6 @@ original(ish) implementations: #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // Virtual memory stats, taken from: diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index cc6c69d175..4a109b59d2 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -12,7 +12,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #include "proc.h" diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index c019f3a094..8e565923c5 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -22,7 +22,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // kinfo_file results diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c index 0691fd1ff0..2454be7f7d 100644 --- a/psutil/arch/openbsd/cpu.c +++ b/psutil/arch/openbsd/cpu.c @@ -9,6 +9,8 @@ #include #include // for CPUSTATES & CP_* +#include "../../arch/all/init.h" + PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { diff --git a/psutil/arch/openbsd/disk.c b/psutil/arch/openbsd/disk.c index e533a082c0..bb4724e632 100644 --- a/psutil/arch/openbsd/disk.c +++ b/psutil/arch/openbsd/disk.c @@ -9,6 +9,8 @@ #include #include +#include "../../arch/all/init.h" + PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h new file mode 100644 index 0000000000..eacb61dd36 --- /dev/null +++ b/psutil/arch/openbsd/init.h @@ -0,0 +1,12 @@ +/* + * 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. + */ + +#include "cpu.h" +#include "disk.h" +#include "mem.h" +#include "proc.h" +#include "socks.h" +#include "users.h" diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index c6cab6189c..4cd1282e84 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -12,7 +12,7 @@ #include #include -#include "../../_psutil_posix.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index ca48d7e168..f97b7b0b7f 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -14,7 +14,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) // #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c index 4c76a1c8b8..d3bfd39616 100644 --- a/psutil/arch/openbsd/socks.c +++ b/psutil/arch/openbsd/socks.c @@ -15,7 +15,6 @@ #undef _KERNEL #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" PyObject * diff --git a/psutil/arch/openbsd/socks.h b/psutil/arch/openbsd/socks.h index 90b678bbb8..bd365f7350 100644 --- a/psutil/arch/openbsd/socks.h +++ b/psutil/arch/openbsd/socks.h @@ -5,4 +5,6 @@ * found in the LICENSE file. */ +#include + PyObject *psutil_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/openbsd/users.c b/psutil/arch/openbsd/users.c index e2496543d7..5475a91261 100644 --- a/psutil/arch/openbsd/users.c +++ b/psutil/arch/openbsd/users.c @@ -1,6 +1,16 @@ +/* + * 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. + */ + #include #include +#include "../../arch/all/init.h" + + PyObject * psutil_users(PyObject *self, PyObject *args) { PyObject *py_retlist = PyList_New(0); diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 7707b184f9..a9c92d82a2 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -32,7 +32,6 @@ For reference, here's the git history with original implementations: #endif #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // added in macOS 12 #ifndef kIOMainPortDefault diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 0aebbf233c..34c874afff 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -16,7 +16,7 @@ #include #include -#include "../../_psutil_posix.h" +#include "../../arch/all/init.h" static int diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 18e2b46554..7af297d827 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -30,7 +30,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/arch/posix/init.c b/psutil/arch/posix/init.c new file mode 100644 index 0000000000..b3818a27ef --- /dev/null +++ b/psutil/arch/posix/init.c @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#include +#include +#include + +/* + * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: + * + * > In SUSv2 the getpagesize() call is labeled LEGACY, and in POSIX.1-2001 + * > it has been dropped. + * > Portable applications should employ sysconf(_SC_PAGESIZE) instead + * > of getpagesize(). + * > Most systems allow the synonym _SC_PAGE_SIZE for _SC_PAGESIZE. + * > Whether getpagesize() is present as a Linux system call depends on the + * > architecture. + */ +long +psutil_getpagesize(void) { +#ifdef _SC_PAGESIZE + // recommended POSIX + return sysconf(_SC_PAGESIZE); +#elif _SC_PAGE_SIZE + // alias + return sysconf(_SC_PAGE_SIZE); +#else + // legacy + return (long) getpagesize(); +#endif +} + + +// Exposed so we can test it against Python's stdlib. +PyObject * +psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { + return Py_BuildValue("l", psutil_getpagesize()); +} diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 6586e44df1..43916eb27c 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -8,3 +8,7 @@ #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) #include "users.h" #endif + +long psutil_getpagesize(void); +PyObject *psutil_getpagesize_pywrapper(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); From 0c3897ec234711fbe7a3e23d5d51627039a1d932 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 00:16:37 +0200 Subject: [PATCH 1304/1714] bsd: rm all .h files --- MANIFEST.in | 5 ----- psutil/_psutil_bsd.c | 6 +----- psutil/arch/bsd/cpu.h | 10 ---------- psutil/arch/bsd/disk.h | 9 --------- psutil/arch/bsd/init.h | 13 +++++++++++++ psutil/arch/bsd/net.h | 9 --------- psutil/arch/bsd/proc.h | 13 ------------- psutil/arch/bsd/sys.h | 9 --------- psutil/arch/linux/init.h | 2 +- 9 files changed, 15 insertions(+), 61 deletions(-) delete mode 100644 psutil/arch/bsd/cpu.h delete mode 100644 psutil/arch/bsd/disk.h delete mode 100644 psutil/arch/bsd/net.h delete mode 100644 psutil/arch/bsd/proc.h delete mode 100644 psutil/arch/bsd/sys.h diff --git a/MANIFEST.in b/MANIFEST.in index a701f6f2bd..ed24f00bfb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -49,17 +49,12 @@ include psutil/arch/aix/net_kernel_structs.h include psutil/arch/all/init.c include psutil/arch/all/init.h include psutil/arch/bsd/cpu.c -include psutil/arch/bsd/cpu.h include psutil/arch/bsd/disk.c -include psutil/arch/bsd/disk.h include psutil/arch/bsd/init.c include psutil/arch/bsd/init.h include psutil/arch/bsd/net.c -include psutil/arch/bsd/net.h include psutil/arch/bsd/proc.c -include psutil/arch/bsd/proc.h include psutil/arch/bsd/sys.c -include psutil/arch/bsd/sys.h include psutil/arch/freebsd/cpu.c include psutil/arch/freebsd/cpu.h include psutil/arch/freebsd/disk.c diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index ebf5ae47fa..83bdc7f466 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -22,11 +22,7 @@ #include // for TCP connection states #include "arch/all/init.h" -#include "arch/bsd/cpu.h" -#include "arch/bsd/disk.h" -#include "arch/bsd/net.h" -#include "arch/bsd/proc.h" -#include "arch/bsd/sys.h" +#include "arch/bsd/init.h" #ifdef PSUTIL_FREEBSD #include "arch/freebsd/cpu.h" diff --git a/psutil/arch/bsd/cpu.h b/psutil/arch/bsd/cpu.h deleted file mode 100644 index 9c5d297fdd..0000000000 --- a/psutil/arch/bsd/cpu.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); -PyObject *psutil_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/disk.h b/psutil/arch/bsd/disk.h deleted file mode 100644 index 628907a9a7..0000000000 --- a/psutil/arch/bsd/disk.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 14d9571e58..5e1d94d854 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -4,4 +4,17 @@ * found in the LICENSE file. */ +#include + void convert_kvm_err(const char *syscall, char *errbuf); + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_name(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/net.h b/psutil/arch/bsd/net.h deleted file mode 100644 index 99079523c1..0000000000 --- a/psutil/arch/bsd/net.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.h b/psutil/arch/bsd/proc.h deleted file mode 100644 index 2ed8e42d63..0000000000 --- a/psutil/arch/bsd/proc.h +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_pids(PyObject *self, PyObject *args); -PyObject *psutil_proc_environ(PyObject *self, PyObject *args); -PyObject *psutil_proc_name(PyObject *self, PyObject *args); -PyObject *psutil_proc_oneshot_info(PyObject *self, PyObject *args); -PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/sys.h b/psutil/arch/bsd/sys.h deleted file mode 100644 index 5ec2d6bc4a..0000000000 --- a/psutil/arch/bsd/sys.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_boot_time(PyObject *self, PyObject *args); diff --git a/psutil/arch/linux/init.h b/psutil/arch/linux/init.h index cef9ae2651..1f6b824925 100644 --- a/psutil/arch/linux/init.h +++ b/psutil/arch/linux/init.h @@ -4,7 +4,7 @@ * found in the LICENSE file. */ -#include "proc.h" #include "disk.h" #include "mem.h" #include "net.h" +#include "proc.h" From e2caaa6104b7d9197c29d3ec58b20faafae88883 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 00:39:09 +0200 Subject: [PATCH 1305/1714] do openbsd --- MANIFEST.in | 5 ----- psutil/_psutil_bsd.c | 7 +------ psutil/arch/openbsd/cpu.h | 12 ------------ psutil/arch/openbsd/disk.h | 10 ---------- psutil/arch/openbsd/init.h | 20 ++++++++++++++------ psutil/arch/openbsd/mem.h | 11 ----------- psutil/arch/openbsd/proc.h | 5 ----- psutil/arch/openbsd/socks.h | 10 ---------- psutil/arch/openbsd/users.h | 9 --------- 9 files changed, 15 insertions(+), 74 deletions(-) delete mode 100644 psutil/arch/openbsd/cpu.h delete mode 100644 psutil/arch/openbsd/disk.h delete mode 100644 psutil/arch/openbsd/mem.h delete mode 100644 psutil/arch/openbsd/socks.h delete mode 100644 psutil/arch/openbsd/users.h diff --git a/MANIFEST.in b/MANIFEST.in index ed24f00bfb..6cd6f31391 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -89,18 +89,13 @@ include psutil/arch/netbsd/proc.h include psutil/arch/netbsd/socks.c include psutil/arch/netbsd/socks.h include psutil/arch/openbsd/cpu.c -include psutil/arch/openbsd/cpu.h include psutil/arch/openbsd/disk.c -include psutil/arch/openbsd/disk.h include psutil/arch/openbsd/init.h include psutil/arch/openbsd/mem.c -include psutil/arch/openbsd/mem.h include psutil/arch/openbsd/proc.c include psutil/arch/openbsd/proc.h include psutil/arch/openbsd/socks.c -include psutil/arch/openbsd/socks.h include psutil/arch/openbsd/users.c -include psutil/arch/openbsd/users.h include psutil/arch/osx/cpu.c include psutil/arch/osx/cpu.h include psutil/arch/osx/disk.c diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 83bdc7f466..ff28ded957 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -33,12 +33,7 @@ #include "arch/freebsd/sensors.h" #include "arch/freebsd/sys_socks.h" #elif PSUTIL_OPENBSD - #include "arch/openbsd/cpu.h" - #include "arch/openbsd/disk.h" - #include "arch/openbsd/mem.h" - #include "arch/openbsd/proc.h" - #include "arch/openbsd/socks.h" - #include "arch/openbsd/users.h" + #include "arch/openbsd/init.h" #elif PSUTIL_NETBSD #include "arch/netbsd/cpu.h" #include "arch/netbsd/disk.h" diff --git a/psutil/arch/openbsd/cpu.h b/psutil/arch/openbsd/cpu.h deleted file mode 100644 index 07bf95fd82..0000000000 --- a/psutil/arch/openbsd/cpu.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_cpu_freq(PyObject* self, PyObject* args); -PyObject *psutil_cpu_stats(PyObject* self, PyObject* args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/disk.h b/psutil/arch/openbsd/disk.h deleted file mode 100644 index b6348dd316..0000000000 --- a/psutil/arch/openbsd/disk.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject* self, PyObject* args); diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index eacb61dd36..125d7d8618 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -4,9 +4,17 @@ * found in the LICENSE file. */ -#include "cpu.h" -#include "disk.h" -#include "mem.h" -#include "proc.h" -#include "socks.h" -#include "users.h" +#include + +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/mem.h b/psutil/arch/openbsd/mem.h deleted file mode 100644 index 1de3f3c434..0000000000 --- a/psutil/arch/openbsd/mem.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/proc.h b/psutil/arch/openbsd/proc.h index a577e5f1c2..76f78c74d4 100644 --- a/psutil/arch/openbsd/proc.h +++ b/psutil/arch/openbsd/proc.h @@ -13,8 +13,3 @@ int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); char **_psutil_get_argv(pid_t pid); - -PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); -PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/socks.h b/psutil/arch/openbsd/socks.h deleted file mode 100644 index bd365f7350..0000000000 --- a/psutil/arch/openbsd/socks.h +++ /dev/null @@ -1,10 +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. - */ - -#include - -PyObject *psutil_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/openbsd/users.h b/psutil/arch/openbsd/users.h deleted file mode 100644 index ba2735d1d2..0000000000 --- a/psutil/arch/openbsd/users.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_users(PyObject* self, PyObject* args); From 3de0d53a36e0b5d70c104027ccc65f57cd66ba96 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 00:42:41 +0200 Subject: [PATCH 1306/1714] refine psutil/_psutil_posix.c --- psutil/_psutil_posix.c | 3 --- psutil/arch/posix/init.h | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 349456e505..a4d69473f1 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -11,9 +11,6 @@ #include #include "arch/all/init.h" -#include "arch/posix/init.h" -#include "arch/posix/proc.h" -#include "arch/posix/net.h" static PyMethodDef mod_methods[] = { diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 43916eb27c..11b2798b88 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -4,6 +4,7 @@ * found in the LICENSE file. */ +#include "net.h" #include "proc.h" #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) #include "users.h" From addbf958c8106388b00652d28b125e571fe5f3cb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 01:00:17 +0200 Subject: [PATCH 1307/1714] bsd: rm openbsd/proc.h (must be refactored) --- MANIFEST.in | 1 - psutil/_psutil_linux.c | 4 ++-- psutil/arch/bsd/proc.c | 2 +- psutil/arch/openbsd/init.h | 7 +++++++ psutil/arch/openbsd/proc.h | 15 --------------- 5 files changed, 10 insertions(+), 19 deletions(-) delete mode 100644 psutil/arch/openbsd/proc.h diff --git a/MANIFEST.in b/MANIFEST.in index 6cd6f31391..5de58677d8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -93,7 +93,6 @@ include psutil/arch/openbsd/disk.c include psutil/arch/openbsd/init.h include psutil/arch/openbsd/mem.c include psutil/arch/openbsd/proc.c -include psutil/arch/openbsd/proc.h include psutil/arch/openbsd/socks.c include psutil/arch/openbsd/users.c include psutil/arch/osx/cpu.c diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index d2efab0a4a..01b8ce9b0e 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -6,14 +6,14 @@ * Linux-specific functions. */ -#include "arch/all/init.h" - #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif #include #include // DUPLEX_* +#include "arch/all/init.h" + // May happen on old RedHat versions, see: // https://github.com/giampaolo/psutil/issues/607 #ifndef DUPLEX_UNKNOWN diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 6799d0db1e..ad6949e87e 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -20,7 +20,7 @@ #ifdef PSUTIL_FREEBSD #include "../../arch/freebsd/proc.h" #elif PSUTIL_OPENBSD - #include "../../arch/openbsd/proc.h" + #include "../../arch/openbsd/init.h" // TODO: refactor this #elif PSUTIL_NETBSD #include "../../arch/netbsd/proc.h" #endif diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index 125d7d8618..9a842991b7 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -6,6 +6,13 @@ #include +// TODO: move / refactor this stuff. It does not belong in here. +typedef struct kinfo_proc kinfo_proc; +int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); +struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); +int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); +char **_psutil_get_argv(pid_t pid); + PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/proc.h b/psutil/arch/openbsd/proc.h deleted file mode 100644 index 76f78c74d4..0000000000 --- a/psutil/arch/openbsd/proc.h +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -typedef struct kinfo_proc kinfo_proc; - -int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); -struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); -int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); -char **_psutil_get_argv(pid_t pid); From 5fa15bb0a1bd253e8bd9a029c25550350c518964 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 09:39:53 +0200 Subject: [PATCH 1308/1714] do osx --- MANIFEST.in | 7 ------- psutil/_psutil_osx.c | 8 +------- psutil/arch/osx/cpu.h | 14 -------------- psutil/arch/osx/disk.h | 11 ----------- psutil/arch/osx/init.h | 29 +++++++++++++++++++++++++++++ psutil/arch/osx/mem.h | 10 ---------- psutil/arch/osx/net.h | 9 --------- psutil/arch/osx/proc.h | 21 --------------------- psutil/arch/osx/sensors.h | 9 --------- psutil/arch/osx/sys.h | 9 --------- 10 files changed, 30 insertions(+), 97 deletions(-) delete mode 100644 psutil/arch/osx/cpu.h delete mode 100644 psutil/arch/osx/disk.h delete mode 100644 psutil/arch/osx/mem.h delete mode 100644 psutil/arch/osx/net.h delete mode 100644 psutil/arch/osx/proc.h delete mode 100644 psutil/arch/osx/sensors.h delete mode 100644 psutil/arch/osx/sys.h diff --git a/MANIFEST.in b/MANIFEST.in index 5de58677d8..2600aa6219 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -96,21 +96,14 @@ include psutil/arch/openbsd/proc.c include psutil/arch/openbsd/socks.c include psutil/arch/openbsd/users.c include psutil/arch/osx/cpu.c -include psutil/arch/osx/cpu.h include psutil/arch/osx/disk.c -include psutil/arch/osx/disk.h include psutil/arch/osx/init.c include psutil/arch/osx/init.h include psutil/arch/osx/mem.c -include psutil/arch/osx/mem.h include psutil/arch/osx/net.c -include psutil/arch/osx/net.h include psutil/arch/osx/proc.c -include psutil/arch/osx/proc.h include psutil/arch/osx/sensors.c -include psutil/arch/osx/sensors.h include psutil/arch/osx/sys.c -include psutil/arch/osx/sys.h include psutil/arch/posix/init.c include psutil/arch/posix/init.h include psutil/arch/posix/net.c diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 0e8b87b49d..e1bb372b71 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -12,13 +12,7 @@ #include #include "arch/all/init.h" -#include "arch/osx/cpu.h" -#include "arch/osx/disk.h" -#include "arch/osx/mem.h" -#include "arch/osx/net.h" -#include "arch/osx/proc.h" -#include "arch/osx/sensors.h" -#include "arch/osx/sys.h" +#include "arch/osx/init.h" static PyMethodDef mod_methods[] = { diff --git a/psutil/arch/osx/cpu.h b/psutil/arch/osx/cpu.h deleted file mode 100644 index 6cf92f82b3..0000000000 --- a/psutil/arch/osx/cpu.h +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); -PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); -PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); -PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_cpu_times(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/disk.h b/psutil/arch/osx/disk.h deleted file mode 100644 index 88ca9a28b1..0000000000 --- a/psutil/arch/osx/disk.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); -PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 3c4dc6b04a..4000bcd1fa 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -4,8 +4,37 @@ * found in the LICENSE file. */ +#include #include extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; int psutil_setup_osx(void); + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); +PyObject *psutil_proc_name(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/mem.h b/psutil/arch/osx/mem.h deleted file mode 100644 index dc4cd74388..0000000000 --- a/psutil/arch/osx/mem.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/net.h b/psutil/arch/osx/net.h deleted file mode 100644 index 99079523c1..0000000000 --- a/psutil/arch/osx/net.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/proc.h b/psutil/arch/osx/proc.h deleted file mode 100644 index f18f5f1fd6..0000000000 --- a/psutil/arch/osx/proc.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_pids(PyObject *self, PyObject *args); -PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); -PyObject *psutil_proc_environ(PyObject *self, PyObject *args); -PyObject *psutil_proc_exe(PyObject *self, PyObject *args); -PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); -PyObject *psutil_proc_name(PyObject *self, PyObject *args); -PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); -PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); -PyObject *psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/sensors.h b/psutil/arch/osx/sensors.h deleted file mode 100644 index edace25d3d..0000000000 --- a/psutil/arch/osx/sensors.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/sys.h b/psutil/arch/osx/sys.h deleted file mode 100644 index 5ec2d6bc4a..0000000000 --- a/psutil/arch/osx/sys.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_boot_time(PyObject *self, PyObject *args); From 2b342f9799c24c8e37dcbe8c69e93dcb0f1e703f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 09:56:49 +0200 Subject: [PATCH 1309/1714] do linux --- MANIFEST.in | 3 --- psutil/arch/linux/disk.h | 9 --------- psutil/arch/linux/init.h | 27 +++++++++++++++++++++++---- psutil/arch/linux/mem.c | 4 ++-- psutil/arch/linux/mem.h | 9 --------- psutil/arch/linux/net.c | 4 ++-- psutil/arch/linux/net.h | 9 --------- psutil/arch/linux/proc.c | 5 +++-- psutil/arch/linux/proc.h | 20 -------------------- psutil/arch/openbsd/init.h | 3 ++- 10 files changed, 32 insertions(+), 61 deletions(-) delete mode 100644 psutil/arch/linux/disk.h delete mode 100644 psutil/arch/linux/mem.h delete mode 100644 psutil/arch/linux/net.h diff --git a/MANIFEST.in b/MANIFEST.in index 2600aa6219..98b1455340 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -70,12 +70,9 @@ include psutil/arch/freebsd/sensors.h include psutil/arch/freebsd/sys_socks.c include psutil/arch/freebsd/sys_socks.h include psutil/arch/linux/disk.c -include psutil/arch/linux/disk.h include psutil/arch/linux/init.h include psutil/arch/linux/mem.c -include psutil/arch/linux/mem.h include psutil/arch/linux/net.c -include psutil/arch/linux/net.h include psutil/arch/linux/proc.c include psutil/arch/linux/proc.h include psutil/arch/netbsd/cpu.c diff --git a/psutil/arch/linux/disk.h b/psutil/arch/linux/disk.h deleted file mode 100644 index 90a86d611b..0000000000 --- a/psutil/arch/linux/disk.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_disk_partitions(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/init.h b/psutil/arch/linux/init.h index 1f6b824925..e21193a77f 100644 --- a/psutil/arch/linux/init.h +++ b/psutil/arch/linux/init.h @@ -4,7 +4,26 @@ * found in the LICENSE file. */ -#include "disk.h" -#include "mem.h" -#include "net.h" -#include "proc.h" +#include +#include // __NR_* +#include // CPU_ALLOC + +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_linux_sysinfo(PyObject *self, PyObject *args); +PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); + +// Linux >= 2.6.13 +#if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) + #define PSUTIL_HAVE_IOPRIO + + PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); + PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); +#endif + +// Should exist starting from CentOS 6 (year 2011). +#ifdef CPU_ALLOC + #define PSUTIL_HAVE_CPU_AFFINITY + + PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); + PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +#endif diff --git a/psutil/arch/linux/mem.c b/psutil/arch/linux/mem.c index 8bdd7ada15..cacf1e7766 100644 --- a/psutil/arch/linux/mem.c +++ b/psutil/arch/linux/mem.c @@ -4,11 +4,11 @@ * found in the LICENSE file. */ -#include "../../arch/all/init.h" - #include #include +#include "../../arch/all/init.h" + PyObject * psutil_linux_sysinfo(PyObject *self, PyObject *args) { diff --git a/psutil/arch/linux/mem.h b/psutil/arch/linux/mem.h deleted file mode 100644 index 582d3e0314..0000000000 --- a/psutil/arch/linux/mem.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_linux_sysinfo(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index efab18e6c8..2488d9ab27 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -4,8 +4,6 @@ * found in the LICENSE file. */ -#include "../../arch/all/init.h" - #include #include #include @@ -14,6 +12,8 @@ #include #include +#include "../../arch/all/init.h" + // see: https://github.com/giampaolo/psutil/issues/659 #ifdef PSUTIL_ETHTOOL_MISSING_TYPES #include diff --git a/psutil/arch/linux/net.h b/psutil/arch/linux/net.h deleted file mode 100644 index 55095c06c9..0000000000 --- a/psutil/arch/linux/net.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_net_if_duplex_speed(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index c641543148..e85fbf549d 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -4,13 +4,14 @@ * found in the LICENSE file. */ -#include "../../arch/all/init.h" - #include #include #include #include +#include "../../arch/all/init.h" +#include "init.h" + #ifdef PSUTIL_HAVE_IOPRIO enum { diff --git a/psutil/arch/linux/proc.h b/psutil/arch/linux/proc.h index 94a84c62ec..86708f4b82 100644 --- a/psutil/arch/linux/proc.h +++ b/psutil/arch/linux/proc.h @@ -3,23 +3,3 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ - -#include -#include // __NR_* -#include // CPU_ALLOC - -// Linux >= 2.6.13 -#if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) - #define PSUTIL_HAVE_IOPRIO - - PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); - PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); -#endif - -// Should exist starting from CentOS 6 (year 2011). -#ifdef CPU_ALLOC - #define PSUTIL_HAVE_CPU_AFFINITY - - PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); - PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); -#endif diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index 9a842991b7..326bc6435e 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ From d9639c7a4caf3bf485d9ed3e5d15b1b7cf1804b0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 10:05:02 +0200 Subject: [PATCH 1310/1714] do freebsd --- MANIFEST.in | 8 +------ psutil/_psutil_bsd.c | 8 +------ psutil/arch/bsd/proc.c | 2 +- psutil/arch/freebsd/cpu.h | 12 ----------- psutil/arch/freebsd/disk.h | 9 -------- psutil/arch/freebsd/init.h | 37 ++++++++++++++++++++++++++++++++ psutil/arch/freebsd/mem.h | 10 --------- psutil/arch/freebsd/proc.h | 24 --------------------- psutil/arch/freebsd/proc_socks.h | 9 -------- psutil/arch/freebsd/sensors.h | 10 --------- psutil/arch/freebsd/sys_socks.h | 10 --------- 11 files changed, 40 insertions(+), 99 deletions(-) delete mode 100644 psutil/arch/freebsd/cpu.h delete mode 100644 psutil/arch/freebsd/disk.h create mode 100644 psutil/arch/freebsd/init.h delete mode 100644 psutil/arch/freebsd/mem.h delete mode 100644 psutil/arch/freebsd/proc.h delete mode 100644 psutil/arch/freebsd/proc_socks.h delete mode 100644 psutil/arch/freebsd/sensors.h delete mode 100644 psutil/arch/freebsd/sys_socks.h diff --git a/MANIFEST.in b/MANIFEST.in index 98b1455340..ecce0dee5b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -56,19 +56,13 @@ include psutil/arch/bsd/net.c include psutil/arch/bsd/proc.c include psutil/arch/bsd/sys.c include psutil/arch/freebsd/cpu.c -include psutil/arch/freebsd/cpu.h include psutil/arch/freebsd/disk.c -include psutil/arch/freebsd/disk.h +include psutil/arch/freebsd/init.h include psutil/arch/freebsd/mem.c -include psutil/arch/freebsd/mem.h include psutil/arch/freebsd/proc.c -include psutil/arch/freebsd/proc.h include psutil/arch/freebsd/proc_socks.c -include psutil/arch/freebsd/proc_socks.h include psutil/arch/freebsd/sensors.c -include psutil/arch/freebsd/sensors.h include psutil/arch/freebsd/sys_socks.c -include psutil/arch/freebsd/sys_socks.h include psutil/arch/linux/disk.c include psutil/arch/linux/init.h include psutil/arch/linux/mem.c diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index ff28ded957..cfa8403eb9 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -25,13 +25,7 @@ #include "arch/bsd/init.h" #ifdef PSUTIL_FREEBSD - #include "arch/freebsd/cpu.h" - #include "arch/freebsd/disk.h" - #include "arch/freebsd/mem.h" - #include "arch/freebsd/proc.h" - #include "arch/freebsd/proc_socks.h" - #include "arch/freebsd/sensors.h" - #include "arch/freebsd/sys_socks.h" + #include "arch/freebsd/init.h" #elif PSUTIL_OPENBSD #include "arch/openbsd/init.h" #elif PSUTIL_NETBSD diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index ad6949e87e..ea7c56fca6 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -18,7 +18,7 @@ #include "../../arch/all/init.h" #ifdef PSUTIL_FREEBSD - #include "../../arch/freebsd/proc.h" + #include "../../arch/freebsd/init.h" // TODO: refactor this #elif PSUTIL_OPENBSD #include "../../arch/openbsd/init.h" // TODO: refactor this #elif PSUTIL_NETBSD diff --git a/psutil/arch/freebsd/cpu.h b/psutil/arch/freebsd/cpu.h deleted file mode 100644 index b78c439197..0000000000 --- a/psutil/arch/freebsd/cpu.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_cpu_freq(PyObject* self, PyObject* args); -PyObject *psutil_cpu_stats(PyObject* self, PyObject* args); -PyObject *psutil_cpu_topology(PyObject* self, PyObject* args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/disk.h b/psutil/arch/freebsd/disk.h deleted file mode 100644 index 9e29f664b0..0000000000 --- a/psutil/arch/freebsd/disk.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/init.h b/psutil/arch/freebsd/init.h new file mode 100644 index 0000000000..842f1419a7 --- /dev/null +++ b/psutil/arch/freebsd/init.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Jay Loden. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +typedef struct kinfo_proc kinfo_proc; + +// TODO: move this stuff. Does not belong here +int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); +int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); + +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_topology(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_getrlimit(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_setrlimit(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); +PyObject *psutil_sensors_cpu_temperature(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/mem.h b/psutil/arch/freebsd/mem.h deleted file mode 100644 index e7dcfc570a..0000000000 --- a/psutil/arch/freebsd/mem.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_swap_mem(PyObject* self, PyObject* args); -PyObject *psutil_virtual_mem(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/proc.h b/psutil/arch/freebsd/proc.h deleted file mode 100644 index f24535eb7d..0000000000 --- a/psutil/arch/freebsd/proc.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -typedef struct kinfo_proc kinfo_proc; - -int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); -int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); - -PyObject* psutil_proc_cmdline(PyObject* self, PyObject* args); -PyObject* psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args); -PyObject* psutil_proc_cpu_affinity_set(PyObject* self, PyObject* args); -PyObject* psutil_proc_cwd(PyObject* self, PyObject* args); -PyObject* psutil_proc_exe(PyObject* self, PyObject* args); -PyObject* psutil_proc_getrlimit(PyObject* self, PyObject* args); -PyObject* psutil_proc_memory_maps(PyObject* self, PyObject* args); -PyObject* psutil_proc_num_fds(PyObject* self, PyObject* args); -PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); -PyObject* psutil_proc_setrlimit(PyObject* self, PyObject* args); -PyObject* psutil_proc_threads(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/proc_socks.h b/psutil/arch/freebsd/proc_socks.h deleted file mode 100644 index edc29b7aaf..0000000000 --- a/psutil/arch/freebsd/proc_socks.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject* psutil_proc_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/sensors.h b/psutil/arch/freebsd/sensors.h deleted file mode 100644 index e5c4107bf4..0000000000 --- a/psutil/arch/freebsd/sensors.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject* psutil_sensors_battery(PyObject* self, PyObject* args); -PyObject* psutil_sensors_cpu_temperature(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/sys_socks.h b/psutil/arch/freebsd/sys_socks.h deleted file mode 100644 index 7524792655..0000000000 --- a/psutil/arch/freebsd/sys_socks.h +++ /dev/null @@ -1,10 +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. - */ - -#include - -PyObject* psutil_net_connections(PyObject* self, PyObject* args); From 3db5cf3bc8fc4719d4f2db0e20f479283d533bd5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 16:07:38 +0200 Subject: [PATCH 1311/1714] do netbsd --- MANIFEST.in | 6 +----- psutil/_psutil_bsd.c | 6 +----- psutil/arch/bsd/proc.c | 2 +- psutil/arch/netbsd/cpu.c | 2 ++ psutil/arch/netbsd/cpu.h | 11 ----------- psutil/arch/netbsd/disk.c | 2 ++ psutil/arch/netbsd/disk.h | 10 ---------- psutil/arch/netbsd/init.h | 32 ++++++++++++++++++++++++++++++++ psutil/arch/netbsd/mem.h | 11 ----------- psutil/arch/netbsd/proc.c | 1 - psutil/arch/netbsd/proc.h | 23 ----------------------- psutil/arch/netbsd/socks.h | 10 ---------- 12 files changed, 39 insertions(+), 77 deletions(-) delete mode 100644 psutil/arch/netbsd/cpu.h delete mode 100644 psutil/arch/netbsd/disk.h create mode 100644 psutil/arch/netbsd/init.h delete mode 100644 psutil/arch/netbsd/mem.h delete mode 100644 psutil/arch/netbsd/proc.h delete mode 100644 psutil/arch/netbsd/socks.h diff --git a/MANIFEST.in b/MANIFEST.in index ecce0dee5b..e362d32680 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -70,15 +70,11 @@ include psutil/arch/linux/net.c include psutil/arch/linux/proc.c include psutil/arch/linux/proc.h include psutil/arch/netbsd/cpu.c -include psutil/arch/netbsd/cpu.h include psutil/arch/netbsd/disk.c -include psutil/arch/netbsd/disk.h +include psutil/arch/netbsd/init.h include psutil/arch/netbsd/mem.c -include psutil/arch/netbsd/mem.h include psutil/arch/netbsd/proc.c -include psutil/arch/netbsd/proc.h include psutil/arch/netbsd/socks.c -include psutil/arch/netbsd/socks.h include psutil/arch/openbsd/cpu.c include psutil/arch/openbsd/disk.c include psutil/arch/openbsd/init.h diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index cfa8403eb9..75baab1f94 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -29,11 +29,7 @@ #elif PSUTIL_OPENBSD #include "arch/openbsd/init.h" #elif PSUTIL_NETBSD - #include "arch/netbsd/cpu.h" - #include "arch/netbsd/disk.h" - #include "arch/netbsd/mem.h" - #include "arch/netbsd/proc.h" - #include "arch/netbsd/socks.h" + #include "arch/netbsd/init.h" #endif diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index ea7c56fca6..5dd07d7e14 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -22,7 +22,7 @@ #elif PSUTIL_OPENBSD #include "../../arch/openbsd/init.h" // TODO: refactor this #elif PSUTIL_NETBSD - #include "../../arch/netbsd/proc.h" + #include "../../arch/netbsd/init.h" // TODO: refactor this #endif diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c index 3601e2e4a0..1b93a7ae49 100644 --- a/psutil/arch/netbsd/cpu.c +++ b/psutil/arch/netbsd/cpu.c @@ -10,6 +10,8 @@ #include #include +#include "../../arch/all/init.h" + /* CPU related functions. Original code was refactored and moved from diff --git a/psutil/arch/netbsd/cpu.h b/psutil/arch/netbsd/cpu.h deleted file mode 100644 index 6c86103250..0000000000 --- a/psutil/arch/netbsd/cpu.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/disk.c b/psutil/arch/netbsd/disk.c index 5481f65df6..18e88889b8 100644 --- a/psutil/arch/netbsd/disk.c +++ b/psutil/arch/netbsd/disk.c @@ -17,6 +17,8 @@ original(ish) implementations: #include #include +#include "../../arch/all/init.h" + PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { diff --git a/psutil/arch/netbsd/disk.h b/psutil/arch/netbsd/disk.h deleted file mode 100644 index 77691c0dfc..0000000000 --- a/psutil/arch/netbsd/disk.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h new file mode 100644 index 0000000000..7c4e2ba40f --- /dev/null +++ b/psutil/arch/netbsd/init.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola'. + * Copyright (c) 2015, Ryo ONODERA. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +typedef struct kinfo_proc2 kinfo_proc; + +// TODO: refactor this. Does not belong here. +struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); +int psutil_kinfo_proc(pid_t pid, kinfo_proc *proc); +int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); +char *psutil_get_cmd_args(pid_t pid, size_t *argsize); + +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *, PyObject *); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *, PyObject *); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/mem.h b/psutil/arch/netbsd/mem.h deleted file mode 100644 index 1de3f3c434..0000000000 --- a/psutil/arch/netbsd/mem.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 4a109b59d2..501b395297 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -12,7 +12,6 @@ #include #include "../../arch/all/init.h" -#include "proc.h" #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) diff --git a/psutil/arch/netbsd/proc.h b/psutil/arch/netbsd/proc.h deleted file mode 100644 index 138ff6ebd2..0000000000 --- a/psutil/arch/netbsd/proc.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -typedef struct kinfo_proc2 kinfo_proc; - -int psutil_kinfo_proc(pid_t pid, kinfo_proc *proc); -struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); -int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); -char *psutil_get_cmd_args(pid_t pid, size_t *argsize); - -PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); -PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject* psutil_proc_exe(PyObject* self, PyObject* args); -PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); diff --git a/psutil/arch/netbsd/socks.h b/psutil/arch/netbsd/socks.h deleted file mode 100644 index 9c2a87d4d7..0000000000 --- a/psutil/arch/netbsd/socks.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola'. - * Copyright (c) 2015, Ryo ONODERA. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -PyObject *psutil_net_connections(PyObject *, PyObject *); -PyObject *psutil_proc_net_connections(PyObject *, PyObject *); From 285db8ecd9d09901bfdf02a3bb613ff9bab0e2ae Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 16:18:42 +0200 Subject: [PATCH 1312/1714] Revert "Remove global vars from FreeBSD net_connections() (#2589)" (#2617) This reverts commit 6ee112e359893d74d91d2b5546b27f0cf48c6a77. --- psutil/arch/netbsd/socks.c | 66 ++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index c019f3a094..200b816836 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -33,7 +33,9 @@ struct kif { int has_buf; }; -SLIST_HEAD(kifhead, kif); +// kinfo_file results list +SLIST_HEAD(kifhead, kif) kihead = SLIST_HEAD_INITIALIZER(kihead); + // kinfo_pcb results struct kpcb { @@ -43,38 +45,53 @@ struct kpcb { int has_buf; }; -SLIST_HEAD(kpcbhead, kpcb); +// kinfo_pcb results list +SLIST_HEAD(kpcbhead, kpcb) kpcbhead = SLIST_HEAD_INITIALIZER(kpcbhead); + + +// Initialize kinfo_file results list. +static void +psutil_kiflist_init(void) { + SLIST_INIT(&kihead); +} // Clear kinfo_file results list. static void -psutil_kiflist_clear(struct kifhead *kifhead) { - while (!SLIST_EMPTY(kifhead)) { - struct kif *kif = SLIST_FIRST(kifhead); +psutil_kiflist_clear(void) { + while (!SLIST_EMPTY(&kihead)) { + struct kif *kif = SLIST_FIRST(&kihead); if (kif->has_buf == 1) free(kif->buf); free(kif); - SLIST_REMOVE_HEAD(kifhead, kifs); + SLIST_REMOVE_HEAD(&kihead, kifs); } } +// Initialize kinof_pcb result list. +static void +psutil_kpcblist_init(void) { + SLIST_INIT(&kpcbhead); +} + + // Clear kinof_pcb result list. static void -psutil_kpcblist_clear(struct kpcbhead *kpcbhead) { - while (!SLIST_EMPTY(kpcbhead)) { - struct kpcb *kpcb = SLIST_FIRST(kpcbhead); +psutil_kpcblist_clear(void) { + while (!SLIST_EMPTY(&kpcbhead)) { + struct kpcb *kpcb = SLIST_FIRST(&kpcbhead); if (kpcb->has_buf == 1) free(kpcb->buf); free(kpcb); - SLIST_REMOVE_HEAD(kpcbhead, kpcbs); + SLIST_REMOVE_HEAD(&kpcbhead, kpcbs); } } // Get all open files including socket. static int -psutil_get_files(struct kifhead *kifhead) { +psutil_get_files(void) { size_t len; size_t j; int mib[6]; @@ -125,7 +142,7 @@ psutil_get_files(struct kifhead *kifhead) { kif->has_buf = 0; kif->buf = NULL; } - SLIST_INSERT_HEAD(kifhead, kif, kifs); + SLIST_INSERT_HEAD(&kihead, kif, kifs); } } else { @@ -143,7 +160,7 @@ psutil_get_files(struct kifhead *kifhead) { // Get open sockets. static int -psutil_get_sockets(const char *name, struct kpcbhead *kpcbhead) { +psutil_get_sockets(const char *name) { size_t namelen; int mib[8]; struct kinfo_pcb *pcb = NULL; @@ -195,7 +212,7 @@ psutil_get_sockets(const char *name, struct kpcbhead *kpcbhead) { kpcb->has_buf = 0; kpcb->buf = NULL; } - SLIST_INSERT_HEAD(kpcbhead, kpcb, kpcbs); + SLIST_INSERT_HEAD(&kpcbhead, kpcb, kpcbs); } } else { @@ -320,17 +337,12 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } - // kinfo_file results list - struct kifhead kifhead; - SLIST_INIT(&kifhead); - - // kinfo_pcb results list - struct kpcbhead kpcbhead; - SLIST_INIT(&kpcbhead); + psutil_kiflist_init(); + psutil_kpcblist_init(); - if (psutil_get_files(&kifhead) != 0) + if (psutil_get_files() != 0) goto error; - if (psutil_get_info(kind, &kpcbhead) != 0) + if (psutil_get_info(kind) != 0) goto error; struct kif *k; @@ -434,13 +446,13 @@ psutil_net_connections(PyObject *self, PyObject *args) { } } - psutil_kiflist_clear(&kifhead); - psutil_kpcblist_clear(&kpcbhead); + psutil_kiflist_clear(); + psutil_kpcblist_clear(); return py_retlist; error: - psutil_kiflist_clear(&kifhead); - psutil_kpcblist_clear(&kpcbhead); + psutil_kiflist_clear(); + psutil_kpcblist_clear(); Py_DECREF(py_retlist); Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); From cf7c4a3e399c3043d92bee612a91e0ff31f59583 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 17:04:54 +0200 Subject: [PATCH 1313/1714] do windows --- MANIFEST.in | 14 ----- psutil/arch/windows/cpu.h | 14 ----- psutil/arch/windows/disk.h | 12 ----- psutil/arch/windows/init.c | 1 - psutil/arch/windows/init.h | 85 ++++++++++++++++++++++++++++-- psutil/arch/windows/mem.h | 11 ---- psutil/arch/windows/net.h | 11 ---- psutil/arch/windows/proc.c | 4 -- psutil/arch/windows/proc.h | 34 ------------ psutil/arch/windows/proc_handles.c | 1 - psutil/arch/windows/proc_handles.h | 10 ---- psutil/arch/windows/proc_info.c | 2 - psutil/arch/windows/proc_info.h | 24 --------- psutil/arch/windows/proc_utils.c | 1 - psutil/arch/windows/proc_utils.h | 12 ----- psutil/arch/windows/security.h | 13 ----- psutil/arch/windows/sensors.c | 2 + psutil/arch/windows/sensors.h | 9 ---- psutil/arch/windows/services.c | 1 - psutil/arch/windows/services.h | 17 ------ psutil/arch/windows/socks.c | 1 - psutil/arch/windows/socks.h | 9 ---- psutil/arch/windows/sys.h | 10 ---- psutil/arch/windows/wmi.h | 10 ---- 24 files changed, 84 insertions(+), 224 deletions(-) delete mode 100644 psutil/arch/windows/cpu.h delete mode 100644 psutil/arch/windows/disk.h delete mode 100644 psutil/arch/windows/mem.h delete mode 100644 psutil/arch/windows/net.h delete mode 100644 psutil/arch/windows/proc.h delete mode 100644 psutil/arch/windows/proc_handles.h delete mode 100644 psutil/arch/windows/proc_info.h delete mode 100644 psutil/arch/windows/proc_utils.h delete mode 100644 psutil/arch/windows/security.h delete mode 100644 psutil/arch/windows/sensors.h delete mode 100644 psutil/arch/windows/services.h delete mode 100644 psutil/arch/windows/socks.h delete mode 100644 psutil/arch/windows/sys.h delete mode 100644 psutil/arch/windows/wmi.h diff --git a/MANIFEST.in b/MANIFEST.in index e362d32680..180849e147 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -116,36 +116,22 @@ include psutil/arch/sunos/sys.h include psutil/arch/sunos/v10/ifaddrs.c include psutil/arch/sunos/v10/ifaddrs.h include psutil/arch/windows/cpu.c -include psutil/arch/windows/cpu.h include psutil/arch/windows/disk.c -include psutil/arch/windows/disk.h include psutil/arch/windows/init.c include psutil/arch/windows/init.h include psutil/arch/windows/mem.c -include psutil/arch/windows/mem.h include psutil/arch/windows/net.c -include psutil/arch/windows/net.h include psutil/arch/windows/ntextapi.h include psutil/arch/windows/proc.c -include psutil/arch/windows/proc.h include psutil/arch/windows/proc_handles.c -include psutil/arch/windows/proc_handles.h include psutil/arch/windows/proc_info.c -include psutil/arch/windows/proc_info.h include psutil/arch/windows/proc_utils.c -include psutil/arch/windows/proc_utils.h include psutil/arch/windows/security.c -include psutil/arch/windows/security.h include psutil/arch/windows/sensors.c -include psutil/arch/windows/sensors.h include psutil/arch/windows/services.c -include psutil/arch/windows/services.h include psutil/arch/windows/socks.c -include psutil/arch/windows/socks.h include psutil/arch/windows/sys.c -include psutil/arch/windows/sys.h include psutil/arch/windows/wmi.c -include psutil/arch/windows/wmi.h include psutil/tests/README.rst include psutil/tests/__init__.py include psutil/tests/__main__.py diff --git a/psutil/arch/windows/cpu.h b/psutil/arch/windows/cpu.h deleted file mode 100644 index 1ef3ff1f04..0000000000 --- a/psutil/arch/windows/cpu.h +++ /dev/null @@ -1,14 +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. - */ - -#include - -PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); -PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); -PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); -PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_cpu_times(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/disk.h b/psutil/arch/windows/disk.h deleted file mode 100644 index 28bed22b54..0000000000 --- a/psutil/arch/windows/disk.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); -PyObject *psutil_disk_usage(PyObject *self, PyObject *args); -PyObject *psutil_QueryDosDevice(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/init.c b/psutil/arch/windows/init.c index 19285db58a..55f779ffee 100644 --- a/psutil/arch/windows/init.c +++ b/psutil/arch/windows/init.c @@ -8,7 +8,6 @@ #include #include "../../arch/all/init.h" -#include "init.h" #include "ntextapi.h" diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index 0a5f46809d..a60ee8255c 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -6,6 +6,7 @@ #include #include +#include #include "ntextapi.h" @@ -33,6 +34,13 @@ extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define NT_NTWIN32(status) (_NT_FACILITY(status) == FACILITY_WIN32) #define WIN32_FROM_NTSTATUS(status) (((ULONG)(status)) & 0xffff) +#define PSUTIL_FIRST_PROCESS(Processes) ( \ + (PSYSTEM_PROCESS_INFORMATION)(Processes)) +#define PSUTIL_NEXT_PROCESS(Process) ( \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ + (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) + #define LO_T 1e-7 #define HI_T 429.4967296 @@ -53,9 +61,80 @@ extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #endif #endif -PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); -PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); -PVOID psutil_SetFromNTStatusErr(NTSTATUS status, const char *syscall); double psutil_FiletimeToUnixTime(FILETIME ft); double psutil_LargeIntegerToUnixTime(LARGE_INTEGER li); int psutil_setup_windows(void); +PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); +PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); +PVOID psutil_SetFromNTStatusErr(NTSTATUS status, const char *syscall); + +PyObject *TimeoutExpired; +PyObject *TimeoutAbandoned; + + +DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); +HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code); +HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); +int psutil_assert_pid_exists(DWORD pid, char *err); +int psutil_assert_pid_not_exists(DWORD pid, char *err); +int psutil_pid_is_running(DWORD pid); +int psutil_set_se_debug(); +SC_HANDLE psutil_get_service_handle(char service_name, DWORD scm_access, DWORD access); + +int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_disk_usage(PyObject *self, PyObject *args); +PyObject *psutil_get_loadavg(); +PyObject *psutil_get_open_files(DWORD pid, HANDLE hProcess); +PyObject *psutil_getpagesize(PyObject *self, PyObject *args); +PyObject *psutil_init_loadavg_counter(); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); +PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_pid_exists(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_ppid_map(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict); +PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_is_suspended(PyObject *self, PyObject *args); +PyObject *psutil_proc_kill(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_suspend_or_resume(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_username(PyObject *self, PyObject *args); +PyObject *psutil_proc_wait(PyObject *self, PyObject *args); +PyObject *psutil_QueryDosDevice(PyObject *self, PyObject *args); +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); +PyObject *psutil_swap_percent(PyObject *self, PyObject *args); +PyObject *psutil_uptime(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); +PyObject *psutil_winservice_enumerate(PyObject *self, PyObject *args); +PyObject *psutil_winservice_query_config(PyObject *self, PyObject *args); +PyObject *psutil_winservice_query_descr(PyObject *self, PyObject *args); +PyObject *psutil_winservice_query_status(PyObject *self, PyObject *args); +PyObject *psutil_winservice_start(PyObject *self, PyObject *args); +PyObject *psutil_winservice_stop(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/mem.h b/psutil/arch/windows/mem.h deleted file mode 100644 index 48d3dadee6..0000000000 --- a/psutil/arch/windows/mem.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_getpagesize(PyObject *self, PyObject *args); -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_percent(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/net.h b/psutil/arch/windows/net.h deleted file mode 100644 index 7a6158d13b..0000000000 --- a/psutil/arch/windows/net.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); -PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); -PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 4040641471..55fc4bc6df 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -24,10 +24,6 @@ #pragma comment(lib, "IPHLPAPI.lib") #include "../../arch/all/init.h" -#include "proc.h" -#include "proc_info.h" -#include "proc_handles.h" -#include "proc_utils.h" // Raised by Process.wait(). diff --git a/psutil/arch/windows/proc.h b/psutil/arch/windows/proc.h deleted file mode 100644 index ba119f16ba..0000000000 --- a/psutil/arch/windows/proc.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *TimeoutExpired; -PyObject *TimeoutAbandoned; - -PyObject *psutil_pid_exists(PyObject *self, PyObject *args); -PyObject *psutil_pids(PyObject *self, PyObject *args); -PyObject *psutil_ppid_map(PyObject *self, PyObject *args); -PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); -PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); -PyObject *psutil_proc_exe(PyObject *self, PyObject *args); -PyObject *psutil_proc_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_proc_io_priority_get(PyObject *self, PyObject *args); -PyObject *psutil_proc_io_priority_set(PyObject *self, PyObject *args); -PyObject *psutil_proc_is_suspended(PyObject *self, PyObject *args); -PyObject *psutil_proc_kill(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_info(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); -PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); -PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); -PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); -PyObject *psutil_proc_suspend_or_resume(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_proc_times(PyObject *self, PyObject *args); -PyObject *psutil_proc_username(PyObject *self, PyObject *args); -PyObject *psutil_proc_wait(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 703a0ba298..34cd54e1f0 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -23,7 +23,6 @@ #include #include "../../arch/all/init.h" -#include "proc_utils.h" #define THREAD_TIMEOUT 100 // ms diff --git a/psutil/arch/windows/proc_handles.h b/psutil/arch/windows/proc_handles.h deleted file mode 100644 index d1be3152d5..0000000000 --- a/psutil/arch/windows/proc_handles.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include -#include - -PyObject* psutil_get_open_files(DWORD pid, HANDLE hProcess); diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index defdf78f1f..c7acd24c33 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -11,8 +11,6 @@ #include #include "../../arch/all/init.h" -#include "proc_info.h" -#include "proc_utils.h" #ifndef _WIN64 diff --git a/psutil/arch/windows/proc_info.h b/psutil/arch/windows/proc_info.h deleted file mode 100644 index b7795451dc..0000000000 --- a/psutil/arch/windows/proc_info.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include -#include - -#include "ntextapi.h" - -#define PSUTIL_FIRST_PROCESS(Processes) ( \ - (PSYSTEM_PROCESS_INFORMATION)(Processes)) -#define PSUTIL_NEXT_PROCESS(Process) ( \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ - (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) - -int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, - PVOID *retBuffer); -PyObject* psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict); -PyObject* psutil_proc_cwd(PyObject *self, PyObject *args); -PyObject* psutil_proc_environ(PyObject *self, PyObject *args); -PyObject* psutil_proc_info(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index 63e1950efe..778e71d2a7 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -11,7 +11,6 @@ #include // EnumProcesses #include "../../arch/all/init.h" -#include "proc_utils.h" DWORD * diff --git a/psutil/arch/windows/proc_utils.h b/psutil/arch/windows/proc_utils.h deleted file mode 100644 index dca7c991a5..0000000000 --- a/psutil/arch/windows/proc_utils.h +++ /dev/null @@ -1,12 +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. - */ - -DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); -HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); -HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code); -int psutil_pid_is_running(DWORD pid); -int psutil_assert_pid_exists(DWORD pid, char *err); -int psutil_assert_pid_not_exists(DWORD pid, char *err); diff --git a/psutil/arch/windows/security.h b/psutil/arch/windows/security.h deleted file mode 100644 index 8d4ddb00d4..0000000000 --- a/psutil/arch/windows/security.h +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - * - * Security related functions for Windows platform (Set privileges such as - * SeDebug), as well as security helper functions. - */ - -#include - -int psutil_set_se_debug(); - diff --git a/psutil/arch/windows/sensors.c b/psutil/arch/windows/sensors.c index fbe2c2fe9f..8bb636c44e 100644 --- a/psutil/arch/windows/sensors.c +++ b/psutil/arch/windows/sensors.c @@ -7,6 +7,8 @@ #include #include +#include "../../arch/all/init.h" + // Added in https://github.com/giampaolo/psutil/commit/109f873 in 2017. // Moved in here in 2023. diff --git a/psutil/arch/windows/sensors.h b/psutil/arch/windows/sensors.h deleted file mode 100644 index edace25d3d..0000000000 --- a/psutil/arch/windows/sensors.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 2339f289a7..141bc1aaed 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -9,7 +9,6 @@ #include #include "../../arch/all/init.h" -#include "services.h" // ================================================================== diff --git a/psutil/arch/windows/services.h b/psutil/arch/windows/services.h deleted file mode 100644 index ebcfa5ef59..0000000000 --- a/psutil/arch/windows/services.h +++ /dev/null @@ -1,17 +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. - */ - -#include -#include - -SC_HANDLE psutil_get_service_handle( - char service_name, DWORD scm_access, DWORD access); -PyObject *psutil_winservice_enumerate(PyObject *self, PyObject *args); -PyObject *psutil_winservice_query_config(PyObject *self, PyObject *args); -PyObject *psutil_winservice_query_status(PyObject *self, PyObject *args); -PyObject *psutil_winservice_query_descr(PyObject *self, PyObject *args); -PyObject *psutil_winservice_start(PyObject *self, PyObject *args); -PyObject *psutil_winservice_stop(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 5b646173c6..8267c6e352 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -12,7 +12,6 @@ #include #include "../../arch/all/init.h" -#include "proc_utils.h" #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) diff --git a/psutil/arch/windows/socks.h b/psutil/arch/windows/socks.h deleted file mode 100644 index cd9ba58dcb..0000000000 --- a/psutil/arch/windows/socks.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Jeff Tang. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_net_connections(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/sys.h b/psutil/arch/windows/sys.h deleted file mode 100644 index 83d143bd9d..0000000000 --- a/psutil/arch/windows/sys.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_uptime(PyObject *self, PyObject *args); -PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/wmi.h b/psutil/arch/windows/wmi.h deleted file mode 100644 index 311242a393..0000000000 --- a/psutil/arch/windows/wmi.h +++ /dev/null @@ -1,10 +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. - */ - -#include - -PyObject* psutil_init_loadavg_counter(); -PyObject* psutil_get_loadavg(); From 46943addd49d53a0b03da2b0038781b9be9bbf5d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 17:08:22 +0200 Subject: [PATCH 1314/1714] fix win compilation err --- psutil/_psutil_windows.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 6774546dc5..ed3026183a 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -18,21 +18,7 @@ #include #include "arch/all/init.h" -#include "arch/windows/cpu.h" -#include "arch/windows/disk.h" #include "arch/windows/init.h" -#include "arch/windows/mem.h" -#include "arch/windows/net.h" -#include "arch/windows/proc.h" -#include "arch/windows/proc_handles.h" -#include "arch/windows/proc_info.h" -#include "arch/windows/proc_utils.h" -#include "arch/windows/security.h" -#include "arch/windows/sensors.h" -#include "arch/windows/services.h" -#include "arch/windows/socks.h" -#include "arch/windows/sys.h" -#include "arch/windows/wmi.h" #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) From ae3ddde8e5340b5f5a8595715c8f26884933eca9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 17:20:39 +0200 Subject: [PATCH 1315/1714] C refact: remove .h files (#2618) --- MANIFEST.in | 53 ++-------------- psutil/_psutil_bsd.c | 28 ++------- psutil/_psutil_linux.c | 4 -- psutil/_psutil_osx.c | 8 +-- psutil/_psutil_posix.c | 40 ------------ psutil/_psutil_posix.h | 8 --- psutil/_psutil_windows.c | 14 ----- psutil/arch/all/init.h | 24 +++++--- psutil/arch/bsd/cpu.h | 10 --- psutil/arch/bsd/disk.h | 9 --- psutil/arch/bsd/init.h | 13 ++++ psutil/arch/bsd/net.h | 9 --- psutil/arch/bsd/proc.c | 7 +-- psutil/arch/bsd/proc.h | 13 ---- psutil/arch/bsd/sys.h | 9 --- psutil/arch/freebsd/cpu.c | 1 - psutil/arch/freebsd/cpu.h | 12 ---- psutil/arch/freebsd/disk.c | 1 - psutil/arch/freebsd/disk.h | 9 --- psutil/arch/freebsd/init.h | 37 +++++++++++ psutil/arch/freebsd/mem.c | 2 - psutil/arch/freebsd/mem.h | 10 --- psutil/arch/freebsd/proc.c | 1 - psutil/arch/freebsd/proc.h | 24 -------- psutil/arch/freebsd/proc_socks.c | 1 - psutil/arch/freebsd/proc_socks.h | 9 --- psutil/arch/freebsd/sensors.c | 1 - psutil/arch/freebsd/sensors.h | 10 --- psutil/arch/freebsd/sys_socks.c | 1 - psutil/arch/freebsd/sys_socks.h | 10 --- psutil/arch/linux/disk.c | 4 +- psutil/arch/linux/disk.h | 9 --- psutil/arch/linux/init.h | 29 +++++++++ psutil/arch/linux/mem.h | 9 --- psutil/arch/linux/net.c | 4 +- psutil/arch/linux/net.h | 9 --- psutil/arch/linux/proc.c | 2 +- psutil/arch/linux/proc.h | 20 ------ psutil/arch/netbsd/cpu.c | 2 + psutil/arch/netbsd/cpu.h | 11 ---- psutil/arch/netbsd/disk.c | 2 + psutil/arch/netbsd/disk.h | 10 --- psutil/arch/netbsd/init.h | 32 ++++++++++ psutil/arch/netbsd/mem.c | 1 - psutil/arch/netbsd/mem.h | 11 ---- psutil/arch/netbsd/proc.c | 2 - psutil/arch/netbsd/proc.h | 23 ------- psutil/arch/netbsd/socks.c | 1 - psutil/arch/netbsd/socks.h | 10 --- psutil/arch/openbsd/cpu.c | 2 + psutil/arch/openbsd/cpu.h | 12 ---- psutil/arch/openbsd/disk.c | 2 + psutil/arch/openbsd/disk.h | 10 --- psutil/arch/openbsd/{proc.h => init.h} | 14 ++++- psutil/arch/openbsd/mem.c | 2 +- psutil/arch/openbsd/mem.h | 11 ---- psutil/arch/openbsd/proc.c | 1 - psutil/arch/openbsd/socks.c | 1 - psutil/arch/openbsd/socks.h | 8 --- psutil/arch/openbsd/users.c | 10 +++ psutil/arch/openbsd/users.h | 9 --- psutil/arch/osx/cpu.c | 1 - psutil/arch/osx/cpu.h | 14 ----- psutil/arch/osx/disk.h | 11 ---- psutil/arch/osx/init.h | 29 +++++++++ psutil/arch/osx/mem.c | 2 +- psutil/arch/osx/mem.h | 10 --- psutil/arch/osx/net.h | 9 --- psutil/arch/osx/proc.c | 1 - psutil/arch/osx/proc.h | 21 ------- psutil/arch/osx/sensors.h | 9 --- psutil/arch/osx/sys.h | 9 --- psutil/arch/posix/init.c | 41 +++++++++++++ psutil/arch/posix/init.h | 5 ++ psutil/arch/windows/cpu.h | 14 ----- psutil/arch/windows/disk.h | 12 ---- psutil/arch/windows/init.c | 1 - psutil/arch/windows/init.h | 85 +++++++++++++++++++++++++- psutil/arch/windows/mem.h | 11 ---- psutil/arch/windows/net.h | 11 ---- psutil/arch/windows/proc.c | 4 -- psutil/arch/windows/proc.h | 34 ----------- psutil/arch/windows/proc_handles.c | 1 - psutil/arch/windows/proc_handles.h | 10 --- psutil/arch/windows/proc_info.c | 2 - psutil/arch/windows/proc_info.h | 24 -------- psutil/arch/windows/proc_utils.c | 1 - psutil/arch/windows/proc_utils.h | 12 ---- psutil/arch/windows/security.h | 13 ---- psutil/arch/windows/sensors.c | 2 + psutil/arch/windows/sensors.h | 9 --- psutil/arch/windows/services.c | 1 - psutil/arch/windows/services.h | 17 ------ psutil/arch/windows/socks.c | 1 - psutil/arch/windows/socks.h | 9 --- psutil/arch/windows/sys.h | 10 --- psutil/arch/windows/wmi.h | 10 --- 97 files changed, 336 insertions(+), 771 deletions(-) delete mode 100644 psutil/_psutil_posix.h delete mode 100644 psutil/arch/bsd/cpu.h delete mode 100644 psutil/arch/bsd/disk.h delete mode 100644 psutil/arch/bsd/net.h delete mode 100644 psutil/arch/bsd/proc.h delete mode 100644 psutil/arch/bsd/sys.h delete mode 100644 psutil/arch/freebsd/cpu.h delete mode 100644 psutil/arch/freebsd/disk.h create mode 100644 psutil/arch/freebsd/init.h delete mode 100644 psutil/arch/freebsd/mem.h delete mode 100644 psutil/arch/freebsd/proc.h delete mode 100644 psutil/arch/freebsd/proc_socks.h delete mode 100644 psutil/arch/freebsd/sensors.h delete mode 100644 psutil/arch/freebsd/sys_socks.h delete mode 100644 psutil/arch/linux/disk.h create mode 100644 psutil/arch/linux/init.h delete mode 100644 psutil/arch/linux/mem.h delete mode 100644 psutil/arch/linux/net.h delete mode 100644 psutil/arch/netbsd/cpu.h delete mode 100644 psutil/arch/netbsd/disk.h create mode 100644 psutil/arch/netbsd/init.h delete mode 100644 psutil/arch/netbsd/mem.h delete mode 100644 psutil/arch/netbsd/proc.h delete mode 100644 psutil/arch/netbsd/socks.h delete mode 100644 psutil/arch/openbsd/cpu.h delete mode 100644 psutil/arch/openbsd/disk.h rename psutil/arch/openbsd/{proc.h => init.h} (56%) delete mode 100644 psutil/arch/openbsd/mem.h delete mode 100644 psutil/arch/openbsd/socks.h delete mode 100644 psutil/arch/openbsd/users.h delete mode 100644 psutil/arch/osx/cpu.h delete mode 100644 psutil/arch/osx/disk.h delete mode 100644 psutil/arch/osx/mem.h delete mode 100644 psutil/arch/osx/net.h delete mode 100644 psutil/arch/osx/proc.h delete mode 100644 psutil/arch/osx/sensors.h delete mode 100644 psutil/arch/osx/sys.h create mode 100644 psutil/arch/posix/init.c delete mode 100644 psutil/arch/windows/cpu.h delete mode 100644 psutil/arch/windows/disk.h delete mode 100644 psutil/arch/windows/mem.h delete mode 100644 psutil/arch/windows/net.h delete mode 100644 psutil/arch/windows/proc.h delete mode 100644 psutil/arch/windows/proc_handles.h delete mode 100644 psutil/arch/windows/proc_info.h delete mode 100644 psutil/arch/windows/proc_utils.h delete mode 100644 psutil/arch/windows/security.h delete mode 100644 psutil/arch/windows/sensors.h delete mode 100644 psutil/arch/windows/services.h delete mode 100644 psutil/arch/windows/socks.h delete mode 100644 psutil/arch/windows/sys.h delete mode 100644 psutil/arch/windows/wmi.h diff --git a/MANIFEST.in b/MANIFEST.in index 6c96e43152..180849e147 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -36,7 +36,6 @@ include psutil/_psutil_bsd.c include psutil/_psutil_linux.c include psutil/_psutil_osx.c include psutil/_psutil_posix.c -include psutil/_psutil_posix.h include psutil/_psutil_sunos.c include psutil/_psutil_windows.c include psutil/_pswindows.py @@ -50,77 +49,49 @@ include psutil/arch/aix/net_kernel_structs.h include psutil/arch/all/init.c include psutil/arch/all/init.h include psutil/arch/bsd/cpu.c -include psutil/arch/bsd/cpu.h include psutil/arch/bsd/disk.c -include psutil/arch/bsd/disk.h include psutil/arch/bsd/init.c include psutil/arch/bsd/init.h include psutil/arch/bsd/net.c -include psutil/arch/bsd/net.h include psutil/arch/bsd/proc.c -include psutil/arch/bsd/proc.h include psutil/arch/bsd/sys.c -include psutil/arch/bsd/sys.h include psutil/arch/freebsd/cpu.c -include psutil/arch/freebsd/cpu.h include psutil/arch/freebsd/disk.c -include psutil/arch/freebsd/disk.h +include psutil/arch/freebsd/init.h include psutil/arch/freebsd/mem.c -include psutil/arch/freebsd/mem.h include psutil/arch/freebsd/proc.c -include psutil/arch/freebsd/proc.h include psutil/arch/freebsd/proc_socks.c -include psutil/arch/freebsd/proc_socks.h include psutil/arch/freebsd/sensors.c -include psutil/arch/freebsd/sensors.h include psutil/arch/freebsd/sys_socks.c -include psutil/arch/freebsd/sys_socks.h include psutil/arch/linux/disk.c -include psutil/arch/linux/disk.h +include psutil/arch/linux/init.h include psutil/arch/linux/mem.c -include psutil/arch/linux/mem.h include psutil/arch/linux/net.c -include psutil/arch/linux/net.h include psutil/arch/linux/proc.c include psutil/arch/linux/proc.h include psutil/arch/netbsd/cpu.c -include psutil/arch/netbsd/cpu.h include psutil/arch/netbsd/disk.c -include psutil/arch/netbsd/disk.h +include psutil/arch/netbsd/init.h include psutil/arch/netbsd/mem.c -include psutil/arch/netbsd/mem.h include psutil/arch/netbsd/proc.c -include psutil/arch/netbsd/proc.h include psutil/arch/netbsd/socks.c -include psutil/arch/netbsd/socks.h include psutil/arch/openbsd/cpu.c -include psutil/arch/openbsd/cpu.h include psutil/arch/openbsd/disk.c -include psutil/arch/openbsd/disk.h +include psutil/arch/openbsd/init.h include psutil/arch/openbsd/mem.c -include psutil/arch/openbsd/mem.h include psutil/arch/openbsd/proc.c -include psutil/arch/openbsd/proc.h include psutil/arch/openbsd/socks.c -include psutil/arch/openbsd/socks.h include psutil/arch/openbsd/users.c -include psutil/arch/openbsd/users.h include psutil/arch/osx/cpu.c -include psutil/arch/osx/cpu.h include psutil/arch/osx/disk.c -include psutil/arch/osx/disk.h include psutil/arch/osx/init.c include psutil/arch/osx/init.h include psutil/arch/osx/mem.c -include psutil/arch/osx/mem.h include psutil/arch/osx/net.c -include psutil/arch/osx/net.h include psutil/arch/osx/proc.c -include psutil/arch/osx/proc.h include psutil/arch/osx/sensors.c -include psutil/arch/osx/sensors.h include psutil/arch/osx/sys.c -include psutil/arch/osx/sys.h +include psutil/arch/posix/init.c include psutil/arch/posix/init.h include psutil/arch/posix/net.c include psutil/arch/posix/net.h @@ -145,36 +116,22 @@ include psutil/arch/sunos/sys.h include psutil/arch/sunos/v10/ifaddrs.c include psutil/arch/sunos/v10/ifaddrs.h include psutil/arch/windows/cpu.c -include psutil/arch/windows/cpu.h include psutil/arch/windows/disk.c -include psutil/arch/windows/disk.h include psutil/arch/windows/init.c include psutil/arch/windows/init.h include psutil/arch/windows/mem.c -include psutil/arch/windows/mem.h include psutil/arch/windows/net.c -include psutil/arch/windows/net.h include psutil/arch/windows/ntextapi.h include psutil/arch/windows/proc.c -include psutil/arch/windows/proc.h include psutil/arch/windows/proc_handles.c -include psutil/arch/windows/proc_handles.h include psutil/arch/windows/proc_info.c -include psutil/arch/windows/proc_info.h include psutil/arch/windows/proc_utils.c -include psutil/arch/windows/proc_utils.h include psutil/arch/windows/security.c -include psutil/arch/windows/security.h include psutil/arch/windows/sensors.c -include psutil/arch/windows/sensors.h include psutil/arch/windows/services.c -include psutil/arch/windows/services.h include psutil/arch/windows/socks.c -include psutil/arch/windows/socks.h include psutil/arch/windows/sys.c -include psutil/arch/windows/sys.h include psutil/arch/windows/wmi.c -include psutil/arch/windows/wmi.h include psutil/tests/README.rst include psutil/tests/__init__.py include psutil/tests/__main__.py diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 57ff49be04..75baab1f94 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -22,34 +22,14 @@ #include // for TCP connection states #include "arch/all/init.h" -#include "_psutil_posix.h" -#include "arch/bsd/cpu.h" -#include "arch/bsd/disk.h" -#include "arch/bsd/net.h" -#include "arch/bsd/proc.h" -#include "arch/bsd/sys.h" +#include "arch/bsd/init.h" #ifdef PSUTIL_FREEBSD - #include "arch/freebsd/cpu.h" - #include "arch/freebsd/disk.h" - #include "arch/freebsd/mem.h" - #include "arch/freebsd/proc.h" - #include "arch/freebsd/proc_socks.h" - #include "arch/freebsd/sensors.h" - #include "arch/freebsd/sys_socks.h" + #include "arch/freebsd/init.h" #elif PSUTIL_OPENBSD - #include "arch/openbsd/cpu.h" - #include "arch/openbsd/disk.h" - #include "arch/openbsd/mem.h" - #include "arch/openbsd/proc.h" - #include "arch/openbsd/socks.h" - #include "arch/openbsd/users.h" + #include "arch/openbsd/init.h" #elif PSUTIL_NETBSD - #include "arch/netbsd/cpu.h" - #include "arch/netbsd/disk.h" - #include "arch/netbsd/mem.h" - #include "arch/netbsd/proc.h" - #include "arch/netbsd/socks.h" + #include "arch/netbsd/init.h" #endif diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 421d9c283e..01b8ce9b0e 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -13,10 +13,6 @@ #include // DUPLEX_* #include "arch/all/init.h" -#include "arch/linux/disk.h" -#include "arch/linux/mem.h" -#include "arch/linux/net.h" -#include "arch/linux/proc.h" // May happen on old RedHat versions, see: // https://github.com/giampaolo/psutil/issues/607 diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 0e8b87b49d..e1bb372b71 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -12,13 +12,7 @@ #include #include "arch/all/init.h" -#include "arch/osx/cpu.h" -#include "arch/osx/disk.h" -#include "arch/osx/mem.h" -#include "arch/osx/net.h" -#include "arch/osx/proc.h" -#include "arch/osx/sensors.h" -#include "arch/osx/sys.h" +#include "arch/osx/init.h" static PyMethodDef mod_methods[] = { diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c index 4b08a8877b..a4d69473f1 100644 --- a/psutil/_psutil_posix.c +++ b/psutil/_psutil_posix.c @@ -11,46 +11,6 @@ #include #include "arch/all/init.h" -#include "arch/posix/proc.h" -#include "arch/posix/net.h" - - -// ==================================================================== -// --- Utils -// ==================================================================== - - -/* - * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: - * - * > In SUSv2 the getpagesize() call is labeled LEGACY, and in POSIX.1-2001 - * > it has been dropped. - * > Portable applications should employ sysconf(_SC_PAGESIZE) instead - * > of getpagesize(). - * > Most systems allow the synonym _SC_PAGE_SIZE for _SC_PAGESIZE. - * > Whether getpagesize() is present as a Linux system call depends on the - * > architecture. - */ -long -psutil_getpagesize(void) { -#ifdef _SC_PAGESIZE - // recommended POSIX - return sysconf(_SC_PAGESIZE); -#elif _SC_PAGE_SIZE - // alias - return sysconf(_SC_PAGE_SIZE); -#else - // legacy - return (long) getpagesize(); -#endif -} - - -// Exposed so we can test it against Python's stdlib. -static PyObject * -psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { - return Py_BuildValue("l", psutil_getpagesize()); -} static PyMethodDef mod_methods[] = { diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h deleted file mode 100644 index 21b96600ed..0000000000 --- a/psutil/_psutil_posix.h +++ /dev/null @@ -1,8 +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. - */ - -long psutil_getpagesize(void); -PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 6774546dc5..ed3026183a 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -18,21 +18,7 @@ #include #include "arch/all/init.h" -#include "arch/windows/cpu.h" -#include "arch/windows/disk.h" #include "arch/windows/init.h" -#include "arch/windows/mem.h" -#include "arch/windows/net.h" -#include "arch/windows/proc.h" -#include "arch/windows/proc_handles.h" -#include "arch/windows/proc_info.h" -#include "arch/windows/proc_utils.h" -#include "arch/windows/security.h" -#include "arch/windows/sensors.h" -#include "arch/windows/services.h" -#include "arch/windows/socks.h" -#include "arch/windows/sys.h" -#include "arch/windows/wmi.h" #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index a18b2dfaae..6483eec2da 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -10,17 +10,27 @@ // We do this so that all .c files have to include only one header // (ourselves, init.h). -#if defined(PSUTIL_WINDOWS) - #include "../../arch/windows/init.h" -#elif defined(PSUTIL_OSX) - #include "../../arch/osx/init.h" -#elif defined(PSUTIL_BSD) - #include "../../arch/bsd/init.h" -#endif #if defined(PSUTIL_POSIX) #include "../../arch/posix/init.h" #endif +#if defined(PSUTIL_BSD) + #include "../../arch/bsd/init.h" +#endif + +#if defined(PSUTIL_LINUX) + #include "../../arch/linux/init.h" +#elif defined(PSUTIL_WINDOWS) + #include "../../arch/windows/init.h" +#elif defined(PSUTIL_OSX) + #include "../../arch/osx/init.h" +#elif defined(PSUTIL_FREEBSD) + #include "../../arch/freebsd/init.h" +#elif defined(PSUTIL_OPENSBD) + #include "../../arch/openbsd/init.h" +#elif defined(PSUTIL_NETBSD) + #include "../../arch/netbsd/init.h" +#endif // print debug messages when set to 1 extern int PSUTIL_DEBUG; diff --git a/psutil/arch/bsd/cpu.h b/psutil/arch/bsd/cpu.h deleted file mode 100644 index 9c5d297fdd..0000000000 --- a/psutil/arch/bsd/cpu.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); -PyObject *psutil_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/disk.h b/psutil/arch/bsd/disk.h deleted file mode 100644 index 628907a9a7..0000000000 --- a/psutil/arch/bsd/disk.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 14d9571e58..5e1d94d854 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -4,4 +4,17 @@ * found in the LICENSE file. */ +#include + void convert_kvm_err(const char *syscall, char *errbuf); + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_name(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/net.h b/psutil/arch/bsd/net.h deleted file mode 100644 index 99079523c1..0000000000 --- a/psutil/arch/bsd/net.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index d11b7e055f..5dd07d7e14 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -17,13 +17,12 @@ #endif #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #ifdef PSUTIL_FREEBSD - #include "../../arch/freebsd/proc.h" + #include "../../arch/freebsd/init.h" // TODO: refactor this #elif PSUTIL_OPENBSD - #include "../../arch/openbsd/proc.h" + #include "../../arch/openbsd/init.h" // TODO: refactor this #elif PSUTIL_NETBSD - #include "../../arch/netbsd/proc.h" + #include "../../arch/netbsd/init.h" // TODO: refactor this #endif diff --git a/psutil/arch/bsd/proc.h b/psutil/arch/bsd/proc.h deleted file mode 100644 index 2ed8e42d63..0000000000 --- a/psutil/arch/bsd/proc.h +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_pids(PyObject *self, PyObject *args); -PyObject *psutil_proc_environ(PyObject *self, PyObject *args); -PyObject *psutil_proc_name(PyObject *self, PyObject *args); -PyObject *psutil_proc_oneshot_info(PyObject *self, PyObject *args); -PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/sys.h b/psutil/arch/bsd/sys.h deleted file mode 100644 index 5ec2d6bc4a..0000000000 --- a/psutil/arch/bsd/sys.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_boot_time(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 822d2267f2..d735e4b65a 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -21,7 +21,6 @@ For reference, here's the git history with original(ish) implementations: #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" PyObject * diff --git a/psutil/arch/freebsd/cpu.h b/psutil/arch/freebsd/cpu.h deleted file mode 100644 index b78c439197..0000000000 --- a/psutil/arch/freebsd/cpu.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_cpu_freq(PyObject* self, PyObject* args); -PyObject *psutil_cpu_stats(PyObject* self, PyObject* args); -PyObject *psutil_cpu_topology(PyObject* self, PyObject* args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index 3a44975ecc..16f289f3c4 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -9,7 +9,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // convert a bintime struct to milliseconds diff --git a/psutil/arch/freebsd/disk.h b/psutil/arch/freebsd/disk.h deleted file mode 100644 index 9e29f664b0..0000000000 --- a/psutil/arch/freebsd/disk.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/init.h b/psutil/arch/freebsd/init.h new file mode 100644 index 0000000000..842f1419a7 --- /dev/null +++ b/psutil/arch/freebsd/init.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Jay Loden. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +typedef struct kinfo_proc kinfo_proc; + +// TODO: move this stuff. Does not belong here +int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); +int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); + +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_topology(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_getrlimit(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_setrlimit(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); +PyObject *psutil_sensors_cpu_temperature(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 8b08eeb3a6..70aeafbf3a 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -15,8 +15,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" - #ifndef _PATH_DEVNULL #define _PATH_DEVNULL "/dev/null" diff --git a/psutil/arch/freebsd/mem.h b/psutil/arch/freebsd/mem.h deleted file mode 100644 index e7dcfc570a..0000000000 --- a/psutil/arch/freebsd/mem.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_swap_mem(PyObject* self, PyObject* args); -PyObject *psutil_virtual_mem(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index b03b43e6f6..a0381e0995 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -22,7 +22,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/arch/freebsd/proc.h b/psutil/arch/freebsd/proc.h deleted file mode 100644 index f24535eb7d..0000000000 --- a/psutil/arch/freebsd/proc.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -typedef struct kinfo_proc kinfo_proc; - -int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); -int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); - -PyObject* psutil_proc_cmdline(PyObject* self, PyObject* args); -PyObject* psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args); -PyObject* psutil_proc_cpu_affinity_set(PyObject* self, PyObject* args); -PyObject* psutil_proc_cwd(PyObject* self, PyObject* args); -PyObject* psutil_proc_exe(PyObject* self, PyObject* args); -PyObject* psutil_proc_getrlimit(PyObject* self, PyObject* args); -PyObject* psutil_proc_memory_maps(PyObject* self, PyObject* args); -PyObject* psutil_proc_num_fds(PyObject* self, PyObject* args); -PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); -PyObject* psutil_proc_setrlimit(PyObject* self, PyObject* args); -PyObject* psutil_proc_threads(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 07a40dabc1..d56487aee9 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -20,7 +20,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // The tcplist fetching and walking is borrowed from netstat/inet.c. diff --git a/psutil/arch/freebsd/proc_socks.h b/psutil/arch/freebsd/proc_socks.h deleted file mode 100644 index edc29b7aaf..0000000000 --- a/psutil/arch/freebsd/proc_socks.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject* psutil_proc_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 98e57c8353..96738dd7c3 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -17,7 +17,6 @@ For reference, here's the git history with original(ish) implementations: #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define DECIKELVIN_2_CELSIUS(t) (t - 2731) / 10 diff --git a/psutil/arch/freebsd/sensors.h b/psutil/arch/freebsd/sensors.h deleted file mode 100644 index e5c4107bf4..0000000000 --- a/psutil/arch/freebsd/sensors.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject* psutil_sensors_battery(PyObject* self, PyObject* args); -PyObject* psutil_sensors_cpu_temperature(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 00efb501d7..f8d09ab3ef 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -24,7 +24,6 @@ #include // for inet_ntop() #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" static int diff --git a/psutil/arch/freebsd/sys_socks.h b/psutil/arch/freebsd/sys_socks.h deleted file mode 100644 index 7524792655..0000000000 --- a/psutil/arch/freebsd/sys_socks.h +++ /dev/null @@ -1,10 +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. - */ - -#include - -PyObject* psutil_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c index 0fb82a46fc..fb6726af98 100644 --- a/psutil/arch/linux/disk.c +++ b/psutil/arch/linux/disk.c @@ -4,11 +4,11 @@ * found in the LICENSE file. */ +#include "../../arch/all/init.h" + #include #include -#include "../../arch/all/init.h" - // Return disk mounted partitions as a list of tuples including device, // mount point and filesystem type. diff --git a/psutil/arch/linux/disk.h b/psutil/arch/linux/disk.h deleted file mode 100644 index 90a86d611b..0000000000 --- a/psutil/arch/linux/disk.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_disk_partitions(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/init.h b/psutil/arch/linux/init.h new file mode 100644 index 0000000000..e21193a77f --- /dev/null +++ b/psutil/arch/linux/init.h @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#include +#include // __NR_* +#include // CPU_ALLOC + +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_linux_sysinfo(PyObject *self, PyObject *args); +PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); + +// Linux >= 2.6.13 +#if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) + #define PSUTIL_HAVE_IOPRIO + + PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); + PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); +#endif + +// Should exist starting from CentOS 6 (year 2011). +#ifdef CPU_ALLOC + #define PSUTIL_HAVE_CPU_AFFINITY + + PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); + PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +#endif diff --git a/psutil/arch/linux/mem.h b/psutil/arch/linux/mem.h deleted file mode 100644 index 582d3e0314..0000000000 --- a/psutil/arch/linux/mem.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_linux_sysinfo(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index 774d67f0f2..2488d9ab27 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -12,6 +12,8 @@ #include #include +#include "../../arch/all/init.h" + // see: https://github.com/giampaolo/psutil/issues/659 #ifdef PSUTIL_ETHTOOL_MISSING_TYPES #include @@ -25,8 +27,6 @@ #define _LINUX_SYSINFO_H #include -#include "../../arch/all/init.h" - // * defined in linux/ethtool.h but not always available (e.g. Android) // * #ifdef check needed for old kernels, see: diff --git a/psutil/arch/linux/net.h b/psutil/arch/linux/net.h deleted file mode 100644 index 55095c06c9..0000000000 --- a/psutil/arch/linux/net.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_net_if_duplex_speed(PyObject* self, PyObject* args); diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index 57a0f90885..e85fbf549d 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -10,7 +10,7 @@ #include #include "../../arch/all/init.h" -#include "proc.h" +#include "init.h" #ifdef PSUTIL_HAVE_IOPRIO diff --git a/psutil/arch/linux/proc.h b/psutil/arch/linux/proc.h index 94a84c62ec..86708f4b82 100644 --- a/psutil/arch/linux/proc.h +++ b/psutil/arch/linux/proc.h @@ -3,23 +3,3 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ - -#include -#include // __NR_* -#include // CPU_ALLOC - -// Linux >= 2.6.13 -#if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) - #define PSUTIL_HAVE_IOPRIO - - PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); - PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); -#endif - -// Should exist starting from CentOS 6 (year 2011). -#ifdef CPU_ALLOC - #define PSUTIL_HAVE_CPU_AFFINITY - - PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); - PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); -#endif diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c index 3601e2e4a0..1b93a7ae49 100644 --- a/psutil/arch/netbsd/cpu.c +++ b/psutil/arch/netbsd/cpu.c @@ -10,6 +10,8 @@ #include #include +#include "../../arch/all/init.h" + /* CPU related functions. Original code was refactored and moved from diff --git a/psutil/arch/netbsd/cpu.h b/psutil/arch/netbsd/cpu.h deleted file mode 100644 index 6c86103250..0000000000 --- a/psutil/arch/netbsd/cpu.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/disk.c b/psutil/arch/netbsd/disk.c index 5481f65df6..18e88889b8 100644 --- a/psutil/arch/netbsd/disk.c +++ b/psutil/arch/netbsd/disk.c @@ -17,6 +17,8 @@ original(ish) implementations: #include #include +#include "../../arch/all/init.h" + PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { diff --git a/psutil/arch/netbsd/disk.h b/psutil/arch/netbsd/disk.h deleted file mode 100644 index 77691c0dfc..0000000000 --- a/psutil/arch/netbsd/disk.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h new file mode 100644 index 0000000000..7c4e2ba40f --- /dev/null +++ b/psutil/arch/netbsd/init.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola'. + * Copyright (c) 2015, Ryo ONODERA. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include + +typedef struct kinfo_proc2 kinfo_proc; + +// TODO: refactor this. Does not belong here. +struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); +int psutil_kinfo_proc(pid_t pid, kinfo_proc *proc); +int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); +char *psutil_get_cmd_args(pid_t pid, size_t *argsize); + +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *, PyObject *); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *, PyObject *); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 8fe92f5ec4..2f8228a6c1 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -20,7 +20,6 @@ original(ish) implementations: #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // Virtual memory stats, taken from: diff --git a/psutil/arch/netbsd/mem.h b/psutil/arch/netbsd/mem.h deleted file mode 100644 index 1de3f3c434..0000000000 --- a/psutil/arch/netbsd/mem.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index cc6c69d175..501b395297 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -12,8 +12,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" -#include "proc.h" #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) diff --git a/psutil/arch/netbsd/proc.h b/psutil/arch/netbsd/proc.h deleted file mode 100644 index 138ff6ebd2..0000000000 --- a/psutil/arch/netbsd/proc.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -typedef struct kinfo_proc2 kinfo_proc; - -int psutil_kinfo_proc(pid_t pid, kinfo_proc *proc); -struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); -int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); -char *psutil_get_cmd_args(pid_t pid, size_t *argsize); - -PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); -PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject* psutil_proc_exe(PyObject* self, PyObject* args); -PyObject* psutil_proc_num_threads(PyObject* self, PyObject* args); diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index 200b816836..ab61d38ee3 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -22,7 +22,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // kinfo_file results diff --git a/psutil/arch/netbsd/socks.h b/psutil/arch/netbsd/socks.h deleted file mode 100644 index 9c2a87d4d7..0000000000 --- a/psutil/arch/netbsd/socks.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola'. - * Copyright (c) 2015, Ryo ONODERA. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -PyObject *psutil_net_connections(PyObject *, PyObject *); -PyObject *psutil_proc_net_connections(PyObject *, PyObject *); diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c index 0691fd1ff0..2454be7f7d 100644 --- a/psutil/arch/openbsd/cpu.c +++ b/psutil/arch/openbsd/cpu.c @@ -9,6 +9,8 @@ #include #include // for CPUSTATES & CP_* +#include "../../arch/all/init.h" + PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { diff --git a/psutil/arch/openbsd/cpu.h b/psutil/arch/openbsd/cpu.h deleted file mode 100644 index 07bf95fd82..0000000000 --- a/psutil/arch/openbsd/cpu.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_cpu_freq(PyObject* self, PyObject* args); -PyObject *psutil_cpu_stats(PyObject* self, PyObject* args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/disk.c b/psutil/arch/openbsd/disk.c index e533a082c0..bb4724e632 100644 --- a/psutil/arch/openbsd/disk.c +++ b/psutil/arch/openbsd/disk.c @@ -9,6 +9,8 @@ #include #include +#include "../../arch/all/init.h" + PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { diff --git a/psutil/arch/openbsd/disk.h b/psutil/arch/openbsd/disk.h deleted file mode 100644 index b6348dd316..0000000000 --- a/psutil/arch/openbsd/disk.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject* self, PyObject* args); diff --git a/psutil/arch/openbsd/proc.h b/psutil/arch/openbsd/init.h similarity index 56% rename from psutil/arch/openbsd/proc.h rename to psutil/arch/openbsd/init.h index a577e5f1c2..326bc6435e 100644 --- a/psutil/arch/openbsd/proc.h +++ b/psutil/arch/openbsd/init.h @@ -7,14 +7,22 @@ #include +// TODO: move / refactor this stuff. It does not belong in here. typedef struct kinfo_proc kinfo_proc; - int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); char **_psutil_get_argv(pid_t pid); +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index c6cab6189c..4cd1282e84 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -12,7 +12,7 @@ #include #include -#include "../../_psutil_posix.h" +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/openbsd/mem.h b/psutil/arch/openbsd/mem.h deleted file mode 100644 index 1de3f3c434..0000000000 --- a/psutil/arch/openbsd/mem.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. - * All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index ca48d7e168..f97b7b0b7f 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -14,7 +14,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) // #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c index 4c76a1c8b8..d3bfd39616 100644 --- a/psutil/arch/openbsd/socks.c +++ b/psutil/arch/openbsd/socks.c @@ -15,7 +15,6 @@ #undef _KERNEL #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" PyObject * diff --git a/psutil/arch/openbsd/socks.h b/psutil/arch/openbsd/socks.h deleted file mode 100644 index 90b678bbb8..0000000000 --- a/psutil/arch/openbsd/socks.h +++ /dev/null @@ -1,8 +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. - */ - -PyObject *psutil_net_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/openbsd/users.c b/psutil/arch/openbsd/users.c index e2496543d7..5475a91261 100644 --- a/psutil/arch/openbsd/users.c +++ b/psutil/arch/openbsd/users.c @@ -1,6 +1,16 @@ +/* + * 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. + */ + #include #include +#include "../../arch/all/init.h" + + PyObject * psutil_users(PyObject *self, PyObject *args) { PyObject *py_retlist = PyList_New(0); diff --git a/psutil/arch/openbsd/users.h b/psutil/arch/openbsd/users.h deleted file mode 100644 index ba2735d1d2..0000000000 --- a/psutil/arch/openbsd/users.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_users(PyObject* self, PyObject* args); diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 7707b184f9..a9c92d82a2 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -32,7 +32,6 @@ For reference, here's the git history with original implementations: #endif #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" // added in macOS 12 #ifndef kIOMainPortDefault diff --git a/psutil/arch/osx/cpu.h b/psutil/arch/osx/cpu.h deleted file mode 100644 index 6cf92f82b3..0000000000 --- a/psutil/arch/osx/cpu.h +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); -PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); -PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); -PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_cpu_times(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/disk.h b/psutil/arch/osx/disk.h deleted file mode 100644 index 88ca9a28b1..0000000000 --- a/psutil/arch/osx/disk.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); -PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 3c4dc6b04a..4000bcd1fa 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -4,8 +4,37 @@ * found in the LICENSE file. */ +#include #include extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; int psutil_setup_osx(void); + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); +PyObject *psutil_proc_name(PyObject *self, PyObject *args); +PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 0aebbf233c..34c874afff 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -16,7 +16,7 @@ #include #include -#include "../../_psutil_posix.h" +#include "../../arch/all/init.h" static int diff --git a/psutil/arch/osx/mem.h b/psutil/arch/osx/mem.h deleted file mode 100644 index dc4cd74388..0000000000 --- a/psutil/arch/osx/mem.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/net.h b/psutil/arch/osx/net.h deleted file mode 100644 index 99079523c1..0000000000 --- a/psutil/arch/osx/net.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 18e2b46554..7af297d827 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -30,7 +30,6 @@ #include #include "../../arch/all/init.h" -#include "../../_psutil_posix.h" #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) diff --git a/psutil/arch/osx/proc.h b/psutil/arch/osx/proc.h deleted file mode 100644 index f18f5f1fd6..0000000000 --- a/psutil/arch/osx/proc.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_pids(PyObject *self, PyObject *args); -PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); -PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); -PyObject *psutil_proc_environ(PyObject *self, PyObject *args); -PyObject *psutil_proc_exe(PyObject *self, PyObject *args); -PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); -PyObject *psutil_proc_name(PyObject *self, PyObject *args); -PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); -PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); -PyObject *psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/sensors.h b/psutil/arch/osx/sensors.h deleted file mode 100644 index edace25d3d..0000000000 --- a/psutil/arch/osx/sensors.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/sys.h b/psutil/arch/osx/sys.h deleted file mode 100644 index 5ec2d6bc4a..0000000000 --- a/psutil/arch/osx/sys.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_boot_time(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/init.c b/psutil/arch/posix/init.c new file mode 100644 index 0000000000..b3818a27ef --- /dev/null +++ b/psutil/arch/posix/init.c @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#include +#include +#include + +/* + * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: + * + * > In SUSv2 the getpagesize() call is labeled LEGACY, and in POSIX.1-2001 + * > it has been dropped. + * > Portable applications should employ sysconf(_SC_PAGESIZE) instead + * > of getpagesize(). + * > Most systems allow the synonym _SC_PAGE_SIZE for _SC_PAGESIZE. + * > Whether getpagesize() is present as a Linux system call depends on the + * > architecture. + */ +long +psutil_getpagesize(void) { +#ifdef _SC_PAGESIZE + // recommended POSIX + return sysconf(_SC_PAGESIZE); +#elif _SC_PAGE_SIZE + // alias + return sysconf(_SC_PAGE_SIZE); +#else + // legacy + return (long) getpagesize(); +#endif +} + + +// Exposed so we can test it against Python's stdlib. +PyObject * +psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { + return Py_BuildValue("l", psutil_getpagesize()); +} diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 6586e44df1..11b2798b88 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -4,7 +4,12 @@ * found in the LICENSE file. */ +#include "net.h" #include "proc.h" #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) #include "users.h" #endif + +long psutil_getpagesize(void); +PyObject *psutil_getpagesize_pywrapper(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/cpu.h b/psutil/arch/windows/cpu.h deleted file mode 100644 index 1ef3ff1f04..0000000000 --- a/psutil/arch/windows/cpu.h +++ /dev/null @@ -1,14 +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. - */ - -#include - -PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); -PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); -PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); -PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_cpu_times(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/disk.h b/psutil/arch/windows/disk.h deleted file mode 100644 index 28bed22b54..0000000000 --- a/psutil/arch/windows/disk.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); -PyObject *psutil_disk_usage(PyObject *self, PyObject *args); -PyObject *psutil_QueryDosDevice(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/init.c b/psutil/arch/windows/init.c index 19285db58a..55f779ffee 100644 --- a/psutil/arch/windows/init.c +++ b/psutil/arch/windows/init.c @@ -8,7 +8,6 @@ #include #include "../../arch/all/init.h" -#include "init.h" #include "ntextapi.h" diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index 0a5f46809d..a60ee8255c 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -6,6 +6,7 @@ #include #include +#include #include "ntextapi.h" @@ -33,6 +34,13 @@ extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define NT_NTWIN32(status) (_NT_FACILITY(status) == FACILITY_WIN32) #define WIN32_FROM_NTSTATUS(status) (((ULONG)(status)) & 0xffff) +#define PSUTIL_FIRST_PROCESS(Processes) ( \ + (PSYSTEM_PROCESS_INFORMATION)(Processes)) +#define PSUTIL_NEXT_PROCESS(Process) ( \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ + (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ + ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) + #define LO_T 1e-7 #define HI_T 429.4967296 @@ -53,9 +61,80 @@ extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #endif #endif -PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); -PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); -PVOID psutil_SetFromNTStatusErr(NTSTATUS status, const char *syscall); double psutil_FiletimeToUnixTime(FILETIME ft); double psutil_LargeIntegerToUnixTime(LARGE_INTEGER li); int psutil_setup_windows(void); +PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); +PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); +PVOID psutil_SetFromNTStatusErr(NTSTATUS status, const char *syscall); + +PyObject *TimeoutExpired; +PyObject *TimeoutAbandoned; + + +DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); +HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code); +HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); +int psutil_assert_pid_exists(DWORD pid, char *err); +int psutil_assert_pid_not_exists(DWORD pid, char *err); +int psutil_pid_is_running(DWORD pid); +int psutil_set_se_debug(); +SC_HANDLE psutil_get_service_handle(char service_name, DWORD scm_access, DWORD access); + +int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); +PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_disk_usage(PyObject *self, PyObject *args); +PyObject *psutil_get_loadavg(); +PyObject *psutil_get_open_files(DWORD pid, HANDLE hProcess); +PyObject *psutil_getpagesize(PyObject *self, PyObject *args); +PyObject *psutil_init_loadavg_counter(); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); +PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_pid_exists(PyObject *self, PyObject *args); +PyObject *psutil_pids(PyObject *self, PyObject *args); +PyObject *psutil_ppid_map(PyObject *self, PyObject *args); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict); +PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_io_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_is_suspended(PyObject *self, PyObject *args); +PyObject *psutil_proc_kill(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); +PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); +PyObject *psutil_proc_suspend_or_resume(PyObject *self, PyObject *args); +PyObject *psutil_proc_threads(PyObject *self, PyObject *args); +PyObject *psutil_proc_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_username(PyObject *self, PyObject *args); +PyObject *psutil_proc_wait(PyObject *self, PyObject *args); +PyObject *psutil_QueryDosDevice(PyObject *self, PyObject *args); +PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); +PyObject *psutil_swap_percent(PyObject *self, PyObject *args); +PyObject *psutil_uptime(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); +PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); +PyObject *psutil_winservice_enumerate(PyObject *self, PyObject *args); +PyObject *psutil_winservice_query_config(PyObject *self, PyObject *args); +PyObject *psutil_winservice_query_descr(PyObject *self, PyObject *args); +PyObject *psutil_winservice_query_status(PyObject *self, PyObject *args); +PyObject *psutil_winservice_start(PyObject *self, PyObject *args); +PyObject *psutil_winservice_stop(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/mem.h b/psutil/arch/windows/mem.h deleted file mode 100644 index 48d3dadee6..0000000000 --- a/psutil/arch/windows/mem.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_getpagesize(PyObject *self, PyObject *args); -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); -PyObject *psutil_swap_percent(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/net.h b/psutil/arch/windows/net.h deleted file mode 100644 index 7a6158d13b..0000000000 --- a/psutil/arch/windows/net.h +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); -PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); -PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 4040641471..55fc4bc6df 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -24,10 +24,6 @@ #pragma comment(lib, "IPHLPAPI.lib") #include "../../arch/all/init.h" -#include "proc.h" -#include "proc_info.h" -#include "proc_handles.h" -#include "proc_utils.h" // Raised by Process.wait(). diff --git a/psutil/arch/windows/proc.h b/psutil/arch/windows/proc.h deleted file mode 100644 index ba119f16ba..0000000000 --- a/psutil/arch/windows/proc.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *TimeoutExpired; -PyObject *TimeoutAbandoned; - -PyObject *psutil_pid_exists(PyObject *self, PyObject *args); -PyObject *psutil_pids(PyObject *self, PyObject *args); -PyObject *psutil_ppid_map(PyObject *self, PyObject *args); -PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); -PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); -PyObject *psutil_proc_exe(PyObject *self, PyObject *args); -PyObject *psutil_proc_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_proc_io_priority_get(PyObject *self, PyObject *args); -PyObject *psutil_proc_io_priority_set(PyObject *self, PyObject *args); -PyObject *psutil_proc_is_suspended(PyObject *self, PyObject *args); -PyObject *psutil_proc_kill(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_info(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); -PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); -PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); -PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); -PyObject *psutil_proc_suspend_or_resume(PyObject *self, PyObject *args); -PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_proc_times(PyObject *self, PyObject *args); -PyObject *psutil_proc_username(PyObject *self, PyObject *args); -PyObject *psutil_proc_wait(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 703a0ba298..34cd54e1f0 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -23,7 +23,6 @@ #include #include "../../arch/all/init.h" -#include "proc_utils.h" #define THREAD_TIMEOUT 100 // ms diff --git a/psutil/arch/windows/proc_handles.h b/psutil/arch/windows/proc_handles.h deleted file mode 100644 index d1be3152d5..0000000000 --- a/psutil/arch/windows/proc_handles.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include -#include - -PyObject* psutil_get_open_files(DWORD pid, HANDLE hProcess); diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index defdf78f1f..c7acd24c33 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -11,8 +11,6 @@ #include #include "../../arch/all/init.h" -#include "proc_info.h" -#include "proc_utils.h" #ifndef _WIN64 diff --git a/psutil/arch/windows/proc_info.h b/psutil/arch/windows/proc_info.h deleted file mode 100644 index b7795451dc..0000000000 --- a/psutil/arch/windows/proc_info.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include -#include - -#include "ntextapi.h" - -#define PSUTIL_FIRST_PROCESS(Processes) ( \ - (PSYSTEM_PROCESS_INFORMATION)(Processes)) -#define PSUTIL_NEXT_PROCESS(Process) ( \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ - (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) - -int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, - PVOID *retBuffer); -PyObject* psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict); -PyObject* psutil_proc_cwd(PyObject *self, PyObject *args); -PyObject* psutil_proc_environ(PyObject *self, PyObject *args); -PyObject* psutil_proc_info(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index 63e1950efe..778e71d2a7 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -11,7 +11,6 @@ #include // EnumProcesses #include "../../arch/all/init.h" -#include "proc_utils.h" DWORD * diff --git a/psutil/arch/windows/proc_utils.h b/psutil/arch/windows/proc_utils.h deleted file mode 100644 index dca7c991a5..0000000000 --- a/psutil/arch/windows/proc_utils.h +++ /dev/null @@ -1,12 +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. - */ - -DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); -HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); -HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code); -int psutil_pid_is_running(DWORD pid); -int psutil_assert_pid_exists(DWORD pid, char *err); -int psutil_assert_pid_not_exists(DWORD pid, char *err); diff --git a/psutil/arch/windows/security.h b/psutil/arch/windows/security.h deleted file mode 100644 index 8d4ddb00d4..0000000000 --- a/psutil/arch/windows/security.h +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - * - * Security related functions for Windows platform (Set privileges such as - * SeDebug), as well as security helper functions. - */ - -#include - -int psutil_set_se_debug(); - diff --git a/psutil/arch/windows/sensors.c b/psutil/arch/windows/sensors.c index fbe2c2fe9f..8bb636c44e 100644 --- a/psutil/arch/windows/sensors.c +++ b/psutil/arch/windows/sensors.c @@ -7,6 +7,8 @@ #include #include +#include "../../arch/all/init.h" + // Added in https://github.com/giampaolo/psutil/commit/109f873 in 2017. // Moved in here in 2023. diff --git a/psutil/arch/windows/sensors.h b/psutil/arch/windows/sensors.h deleted file mode 100644 index edace25d3d..0000000000 --- a/psutil/arch/windows/sensors.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 2339f289a7..141bc1aaed 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -9,7 +9,6 @@ #include #include "../../arch/all/init.h" -#include "services.h" // ================================================================== diff --git a/psutil/arch/windows/services.h b/psutil/arch/windows/services.h deleted file mode 100644 index ebcfa5ef59..0000000000 --- a/psutil/arch/windows/services.h +++ /dev/null @@ -1,17 +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. - */ - -#include -#include - -SC_HANDLE psutil_get_service_handle( - char service_name, DWORD scm_access, DWORD access); -PyObject *psutil_winservice_enumerate(PyObject *self, PyObject *args); -PyObject *psutil_winservice_query_config(PyObject *self, PyObject *args); -PyObject *psutil_winservice_query_status(PyObject *self, PyObject *args); -PyObject *psutil_winservice_query_descr(PyObject *self, PyObject *args); -PyObject *psutil_winservice_start(PyObject *self, PyObject *args); -PyObject *psutil_winservice_stop(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 5b646173c6..8267c6e352 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -12,7 +12,6 @@ #include #include "../../arch/all/init.h" -#include "proc_utils.h" #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) diff --git a/psutil/arch/windows/socks.h b/psutil/arch/windows/socks.h deleted file mode 100644 index cd9ba58dcb..0000000000 --- a/psutil/arch/windows/socks.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Jeff Tang. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ - -#include - -PyObject *psutil_net_connections(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/sys.h b/psutil/arch/windows/sys.h deleted file mode 100644 index 83d143bd9d..0000000000 --- a/psutil/arch/windows/sys.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2009, Jay Loden, 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. - */ - -#include - -PyObject *psutil_uptime(PyObject *self, PyObject *args); -PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/wmi.h b/psutil/arch/windows/wmi.h deleted file mode 100644 index 311242a393..0000000000 --- a/psutil/arch/windows/wmi.h +++ /dev/null @@ -1,10 +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. - */ - -#include - -PyObject* psutil_init_loadavg_counter(); -PyObject* psutil_get_loadavg(); From d42f29735d176013159ef386f07eb7f5efed2ca7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 17:21:10 +0200 Subject: [PATCH 1316/1714] rm more .h files --- MANIFEST.in | 1 - psutil/arch/linux/disk.c | 4 ++-- psutil/arch/linux/proc.c | 1 - psutil/arch/linux/proc.h | 5 ----- 4 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 psutil/arch/linux/proc.h diff --git a/MANIFEST.in b/MANIFEST.in index 180849e147..f4386431c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -68,7 +68,6 @@ include psutil/arch/linux/init.h include psutil/arch/linux/mem.c include psutil/arch/linux/net.c include psutil/arch/linux/proc.c -include psutil/arch/linux/proc.h include psutil/arch/netbsd/cpu.c include psutil/arch/netbsd/disk.c include psutil/arch/netbsd/init.h diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c index fb6726af98..0fb82a46fc 100644 --- a/psutil/arch/linux/disk.c +++ b/psutil/arch/linux/disk.c @@ -4,11 +4,11 @@ * found in the LICENSE file. */ -#include "../../arch/all/init.h" - #include #include +#include "../../arch/all/init.h" + // Return disk mounted partitions as a list of tuples including device, // mount point and filesystem type. diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index e85fbf549d..e33755743c 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -10,7 +10,6 @@ #include #include "../../arch/all/init.h" -#include "init.h" #ifdef PSUTIL_HAVE_IOPRIO diff --git a/psutil/arch/linux/proc.h b/psutil/arch/linux/proc.h deleted file mode 100644 index 86708f4b82..0000000000 --- a/psutil/arch/linux/proc.h +++ /dev/null @@ -1,5 +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. - */ From 209c6a26c9d762e5cf823647b7bfef57a55da5ee Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 18:04:27 +0200 Subject: [PATCH 1317/1714] rm duplication of kinfo_getfile() --- psutil/arch/bsd/init.h | 4 ++++ psutil/arch/bsd/proc.c | 37 +++++++++++++++++++++++++++++++++++++ psutil/arch/netbsd/init.h | 1 - psutil/arch/netbsd/proc.c | 31 ------------------------------- psutil/arch/openbsd/init.h | 1 - psutil/arch/openbsd/proc.c | 35 ----------------------------------- 6 files changed, 41 insertions(+), 68 deletions(-) diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 5e1d94d854..8870e69ecc 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -8,6 +8,10 @@ void convert_kvm_err(const char *syscall, char *errbuf); +#if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) +struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); +#endif + PyObject *psutil_boot_time(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); PyObject *psutil_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 5dd07d7e14..b9ee21c0a6 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -34,6 +34,43 @@ #endif +// Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an +// int as arg and returns an array with cnt struct kinfo_file. +#if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) +struct kinfo_file * +kinfo_getfile(pid_t pid, int* cnt) { + int mib[6]; + size_t len; + struct kinfo_file* kf; + mib[0] = CTL_KERN; + mib[1] = KERN_FILE; + mib[2] = KERN_FILE_BYPID; + mib[3] = pid; + mib[4] = sizeof(struct kinfo_file); + mib[5] = 0; + + /* get the size of what would be returned */ + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (1/2)"); + return NULL; + } + if ((kf = malloc(len)) == NULL) { + PyErr_NoMemory(); + return NULL; + } + mib[5] = (int)(len / sizeof(struct kinfo_file)); + if (sysctl(mib, 6, kf, &len, NULL, 0) < 0) { + free(kf); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (2/2)"); + return NULL; + } + + *cnt = (int)(len / sizeof(struct kinfo_file)); + return kf; +} +#endif // defined(PLATFORMS…) + + /* * Return a Python list of all the PIDs running on the system. */ diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h index 7c4e2ba40f..fb0d4ad5f8 100644 --- a/psutil/arch/netbsd/init.h +++ b/psutil/arch/netbsd/init.h @@ -11,7 +11,6 @@ typedef struct kinfo_proc2 kinfo_proc; // TODO: refactor this. Does not belong here. -struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); int psutil_kinfo_proc(pid_t pid, kinfo_proc *proc); int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); char *psutil_get_cmd_args(pid_t pid, size_t *argsize); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 501b395297..6b650482bf 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -51,38 +51,7 @@ psutil_kinfo_proc(pid_t pid, kinfo_proc *proc) { } -struct kinfo_file * -kinfo_getfile(pid_t pid, int* cnt) { - // Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an - // int as arg and returns an array with cnt struct kinfo_file. - int mib[6]; - size_t len; - struct kinfo_file* kf; - mib[0] = CTL_KERN; - mib[1] = KERN_FILE2; - mib[2] = KERN_FILE_BYPID; - mib[3] = (int) pid; - mib[4] = sizeof(struct kinfo_file); - mib[5] = 0; - - // get the size of what would be returned - if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - if ((kf = malloc(len)) == NULL) { - PyErr_NoMemory(); - return NULL; - } - mib[5] = (int)(len / sizeof(struct kinfo_file)); - if (sysctl(mib, 6, kf, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - *cnt = (int)(len / sizeof(struct kinfo_file)); - return kf; -} PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index 326bc6435e..098573e95a 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -10,7 +10,6 @@ // TODO: move / refactor this stuff. It does not belong in here. typedef struct kinfo_proc kinfo_proc; int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); -struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt); int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); char **_psutil_get_argv(pid_t pid); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index f97b7b0b7f..a47ceea98a 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -52,41 +52,6 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { } -struct kinfo_file * -kinfo_getfile(pid_t pid, int* cnt) { - // Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an - // int as arg and returns an array with cnt struct kinfo_file. - int mib[6]; - size_t len; - struct kinfo_file* kf; - mib[0] = CTL_KERN; - mib[1] = KERN_FILE; - mib[2] = KERN_FILE_BYPID; - mib[3] = pid; - mib[4] = sizeof(struct kinfo_file); - mib[5] = 0; - - /* get the size of what would be returned */ - if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (1/2)"); - return NULL; - } - if ((kf = malloc(len)) == NULL) { - PyErr_NoMemory(); - return NULL; - } - mib[5] = (int)(len / sizeof(struct kinfo_file)); - if (sysctl(mib, 6, kf, &len, NULL, 0) < 0) { - free(kf); - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (2/2)"); - return NULL; - } - - *cnt = (int)(len / sizeof(struct kinfo_file)); - return kf; -} - - // ============================================================================ // APIS // ============================================================================ From ca8be418ebb0c062134c956f290c99c7ff97d43f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 19:01:59 +0200 Subject: [PATCH 1318/1714] attempt to fix osx segfault --- psutil/arch/openbsd/init.h | 1 - psutil/arch/osx/cpu.c | 29 ++++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index 098573e95a..3ffcb44473 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -11,7 +11,6 @@ typedef struct kinfo_proc kinfo_proc; int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); -char **_psutil_get_argv(pid_t pid); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index a9c92d82a2..8062769c59 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -102,7 +102,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { kern_return_t ret; mach_msg_type_number_t count; mach_port_t mport; - struct vmmeter vm32; + struct vmmeter vmstat; mport = mach_host_self(); if (mport == MACH_PORT_NULL) { @@ -112,7 +112,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } count = HOST_VM_INFO_COUNT; - ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vm32, &count); + ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); if (ret != KERN_SUCCESS) { mach_port_deallocate(mach_task_self(), mport); PyErr_Format( @@ -125,11 +125,16 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { mach_port_deallocate(mach_task_self(), mport); return Py_BuildValue( "IIIII", - vm32.v_swtch, // ctx switches - vm32.v_intr, // interrupts - vm32.v_soft, // software interrupts - vm32.v_syscall, // syscalls - vm32.v_trap // traps + vmstat.v_swtch, // context switches + vmstat.v_intr, // interrupts + vmstat.v_soft, // software interrupts +#if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) \ + && __MAC_OS_X_VERSION_MIN_REQUIRED__ >= 120000 + 0, +#else + vmstat.v_syscall, // system calls (if available) +#endif + vmstat.v_trap // traps ); } @@ -269,9 +274,11 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { if (sysctl(mib, 2, &curr, &len, NULL, 0) < 0) return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); + size = sizeof(min); if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); + size = sizeof(max); if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); @@ -287,7 +294,7 @@ PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { natural_t cpu_count; natural_t i; - processor_info_array_t info_array; + processor_info_array_t info_array = NULL; mach_msg_type_number_t info_count; kern_return_t error; processor_cpu_load_info_data_t *cpu_load_info = NULL; @@ -338,7 +345,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, - info_count * sizeof(int)); + info_count * sizeof(integer_t)); if (ret != KERN_SUCCESS) PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); return py_retlist; @@ -346,9 +353,9 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { error: Py_XDECREF(py_cputime); Py_DECREF(py_retlist); - if (cpu_load_info != NULL) { + if (info_array != NULL) { ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, - info_count * sizeof(int)); + info_count * sizeof(integer_t)); if (ret != KERN_SUCCESS) PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); } From e22618575b0761cceff57144e7ef1462a0d4d3b1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 19:17:04 +0200 Subject: [PATCH 1319/1714] osx: fix potential mem leaks in disk_io_counters() --- psutil/arch/osx/disk.c | 279 ++++++++++++++++++++++------------------- 1 file changed, 150 insertions(+), 129 deletions(-) diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index b15fcd7c4a..93a4e82529 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -213,165 +213,186 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { CFDictionaryRef stats_dict; io_registry_entry_t parent; io_registry_entry_t disk; - io_iterator_t disk_list; + io_iterator_t disk_list = 0; PyObject *py_disk_info = NULL; PyObject *py_retdict = PyDict_New(); if (py_retdict == NULL) return NULL; - // Get list of disks - if (IOServiceGetMatchingServices(kIOMasterPortDefault, - IOServiceMatching(kIOMediaClass), - &disk_list) != kIOReturnSuccess) { - PyErr_SetString( - PyExc_RuntimeError, "unable to get the list of disks."); + if (IOServiceGetMatchingServices( + kIOMasterPortDefault, + IOServiceMatching(kIOMediaClass), + &disk_list) != kIOReturnSuccess) + { + PyErr_SetString(PyExc_RuntimeError, "unable to get the list of disks"); goto error; } - // Iterate over disks while ((disk = IOIteratorNext(disk_list)) != 0) { py_disk_info = NULL; parent_dict = NULL; props_dict = NULL; stats_dict = NULL; + parent = 0; - if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) - != kIOReturnSuccess) { - PyErr_SetString(PyExc_RuntimeError, - "unable to get the disk's parent."); + if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) != kIOReturnSuccess) { + PyErr_SetString(PyExc_RuntimeError, "unable to get the disk's parent"); IOObjectRelease(disk); goto error; } - if (IOObjectConformsTo(parent, "IOBlockStorageDriver")) { - if (IORegistryEntryCreateCFProperties( - disk, - (CFMutableDictionaryRef *) &parent_dict, - kCFAllocatorDefault, - kNilOptions - ) != kIOReturnSuccess) - { - PyErr_SetString(PyExc_RuntimeError, - "unable to get the parent's properties."); - IOObjectRelease(disk); - IOObjectRelease(parent); - goto error; - } - - if (IORegistryEntryCreateCFProperties( - parent, - (CFMutableDictionaryRef *) &props_dict, - kCFAllocatorDefault, - kNilOptions - ) != kIOReturnSuccess) - { - PyErr_SetString(PyExc_RuntimeError, - "unable to get the disk properties."); + if (!IOObjectConformsTo(parent, "IOBlockStorageDriver")) { + IOObjectRelease(parent); + IOObjectRelease(disk); + continue; + } + + if (IORegistryEntryCreateCFProperties(disk, + (CFMutableDictionaryRef *)&parent_dict, + kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) + { + PyErr_SetString( + PyExc_RuntimeError, "unable to get the parent's properties" + ); + IOObjectRelease(parent); + IOObjectRelease(disk); + goto error; + } + + if (IORegistryEntryCreateCFProperties(parent, + (CFMutableDictionaryRef *)&props_dict, + kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) + { + PyErr_SetString( + PyExc_RuntimeError, "unable to get the disk properties" + ); + if (parent_dict) CFRelease(parent_dict); + IOObjectRelease(parent); + IOObjectRelease(disk); + goto error; + } + + CFStringRef disk_name_ref = (CFStringRef)CFDictionaryGetValue( + parent_dict, CFSTR(kIOBSDNameKey) + ); + if (disk_name_ref == NULL) { + PyErr_SetString(PyExc_RuntimeError, "unable to get disk name"); + if (parent_dict) + CFRelease(parent_dict); + if (props_dict) CFRelease(props_dict); - IOObjectRelease(disk); - IOObjectRelease(parent); - goto error; - } - - const int kMaxDiskNameSize = 64; - CFStringRef disk_name_ref = (CFStringRef)CFDictionaryGetValue( - parent_dict, CFSTR(kIOBSDNameKey)); - char disk_name[kMaxDiskNameSize]; - - CFStringGetCString(disk_name_ref, - disk_name, - kMaxDiskNameSize, - CFStringGetSystemEncoding()); - - stats_dict = (CFDictionaryRef)CFDictionaryGetValue( - props_dict, CFSTR(kIOBlockStorageDriverStatisticsKey)); - - if (stats_dict == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "Unable to get disk stats."); - goto error; - } - - CFNumberRef number; - int64_t reads = 0; - int64_t writes = 0; - int64_t read_bytes = 0; - int64_t write_bytes = 0; - int64_t read_time = 0; - int64_t write_time = 0; - - // Get disk reads/writes - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsReadsKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &reads); - } - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsWritesKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &writes); - } - - // Get disk bytes read/written - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsBytesReadKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &read_bytes); - } - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsBytesWrittenKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &write_bytes); - } - - // Get disk time spent reading/writing (nanoseconds) - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsTotalReadTimeKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &read_time); - } - if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, - CFSTR(kIOBlockStorageDriverStatisticsTotalWriteTimeKey)))) - { - CFNumberGetValue(number, kCFNumberSInt64Type, &write_time); - } - - // Read/Write time on macOS comes back in nanoseconds and in psutil - // we've standardized on milliseconds so do the conversion. - py_disk_info = Py_BuildValue( - "(KKKKKK)", - reads, - writes, - read_bytes, - write_bytes, - read_time / 1000 / 1000, - write_time / 1000 / 1000); - if (!py_disk_info) - goto error; - if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) - goto error; - Py_CLEAR(py_disk_info); + IOObjectRelease(parent); + IOObjectRelease(disk); + goto error; + } - CFRelease(parent_dict); + const int kMaxDiskNameSize = 64; + char disk_name[kMaxDiskNameSize]; + if (!CFStringGetCString(disk_name_ref, disk_name, kMaxDiskNameSize, + CFStringGetSystemEncoding())) { + PyErr_SetString( + PyExc_RuntimeError, "unable to convert disk name to C string" + ); + if (parent_dict) + CFRelease(parent_dict); + if (props_dict) + CFRelease(props_dict); IOObjectRelease(parent); - CFRelease(props_dict); IOObjectRelease(disk); + goto error; } - } - IOObjectRelease (disk_list); + stats_dict = (CFDictionaryRef)CFDictionaryGetValue( + props_dict, CFSTR(kIOBlockStorageDriverStatisticsKey) + ); + if (stats_dict == NULL) { + PyErr_SetString(PyExc_RuntimeError, "unable to get disk stats"); + if (parent_dict) + CFRelease(parent_dict); + if (props_dict) + CFRelease(props_dict); + IOObjectRelease(parent); + IOObjectRelease(disk); + goto error; + } + + CFNumberRef number; + int64_t reads = 0, writes = 0, read_bytes = 0, write_bytes = 0; + int64_t read_time = 0, write_time = 0; + + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsReadsKey)))) + CFNumberGetValue(number, kCFNumberSInt64Type, &reads); + + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsWritesKey)))) + CFNumberGetValue(number, kCFNumberSInt64Type, &writes); + + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsBytesReadKey)))) + CFNumberGetValue(number, kCFNumberSInt64Type, &read_bytes); + + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsBytesWrittenKey)))) + CFNumberGetValue(number, kCFNumberSInt64Type, &write_bytes); + + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsTotalReadTimeKey)))) + CFNumberGetValue(number, kCFNumberSInt64Type, &read_time); + + if ((number = (CFNumberRef)CFDictionaryGetValue( + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsTotalWriteTimeKey)))) + CFNumberGetValue(number, kCFNumberSInt64Type, &write_time); + + py_disk_info = Py_BuildValue( + "(KKKKKK)", + (unsigned long long)reads, + (unsigned long long)writes, + (unsigned long long)read_bytes, + (unsigned long long)write_bytes, + (unsigned long long)(read_time / 1000 / 1000), + (unsigned long long)(write_time / 1000 / 1000) + ); + + if (!py_disk_info) { + if (parent_dict) + CFRelease(parent_dict); + if (props_dict) + CFRelease(props_dict); + IOObjectRelease(parent); + IOObjectRelease(disk); + goto error; + } + + if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) { + Py_CLEAR(py_disk_info); + if (parent_dict) + CFRelease(parent_dict); + if (props_dict) + CFRelease(props_dict); + IOObjectRelease(parent); + IOObjectRelease(disk); + goto error; + } + + Py_CLEAR(py_disk_info); + + if (parent_dict) + CFRelease(parent_dict); + IOObjectRelease(parent); + if (props_dict) + CFRelease(props_dict); + IOObjectRelease(disk); + } + IOObjectRelease(disk_list); return py_retdict; error: Py_XDECREF(py_disk_info); Py_DECREF(py_retdict); + if (disk_list != 0) + IOObjectRelease(disk_list); return NULL; } From 774487769542ee19836d6d3af62202339c0c2c12 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 19:39:30 +0200 Subject: [PATCH 1320/1714] add debugging prints --- psutil/arch/osx/net.c | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index 759abf87c5..a95c1ab581 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -55,17 +55,42 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { lim = buf + len; for (next = buf; next < lim; ) { + // Check we have enough space for if_msghdr + if ((size_t)(lim - next) < sizeof(struct if_msghdr)) { + // buffer too small for header + psutil_debug("struct xfile size mismatch"); + } + ifm = (struct if_msghdr *)next; + + if (ifm->ifm_msglen == 0 || next + ifm->ifm_msglen > lim) { + psutil_debug("ifm_msglen size mismatch"); + } + next += ifm->ifm_msglen; if (ifm->ifm_type == RTM_IFINFO2) { py_ifc_info = NULL; struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; + + if ((char *)if2m + sizeof(struct if_msghdr2) > lim) { + psutil_debug("if_msghdr2 + sockaddr_dl mismatch"); + } + struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); + + if ((char *)sdl + sizeof(struct sockaddr_dl) > lim) { + psutil_debug("not enough buffer for sockaddr_dl"); + } + char ifc_name[32]; + size_t namelen = sdl->sdl_nlen; + + if (namelen >= sizeof(ifc_name)) + namelen = sizeof(ifc_name) - 1; - strncpy(ifc_name, sdl->sdl_data, sdl->sdl_nlen); - ifc_name[sdl->sdl_nlen] = 0; + strncpy(ifc_name, sdl->sdl_data, namelen); + ifc_name[namelen] = '\0'; py_ifc_info = Py_BuildValue( "(KKKKKKKi)", @@ -80,6 +105,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { if (!py_ifc_info) goto error; + if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) { Py_CLEAR(py_ifc_info); goto error; From 60d01c34d7c10ce7741a472fa483066798aa5db6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 19:46:59 +0200 Subject: [PATCH 1321/1714] osx: pass a better size to vm_deallocate() when cleaning up --- psutil/arch/osx/proc.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 7af297d827..5983027cb8 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -735,8 +735,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { Py_CLEAR(py_tuple); } - ret = vm_deallocate(task, (vm_address_t)thread_list, - thread_count * sizeof(int)); + ret = vm_deallocate( + task, (vm_address_t)thread_list, thread_count * sizeof(thread_act_t) + ); + if (ret != KERN_SUCCESS) PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); @@ -750,8 +752,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (thread_list != NULL) { - ret = vm_deallocate(task, (vm_address_t)thread_list, - thread_count * sizeof(int)); + ret = vm_deallocate( + task, (vm_address_t)thread_list, thread_count * sizeof(thread_act_t) + ); + if (ret != KERN_SUCCESS) PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); } From 5e4ed3d973ab277632ea406c036428911ca08b6a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 19:49:27 +0200 Subject: [PATCH 1322/1714] osx: more defensive error checking in psutil/arch/osx/sensors.c --- psutil/arch/osx/sensors.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index 1015506cbf..e50909456a 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -55,24 +55,33 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { power_sources_information = IOPSGetPowerSourceDescription( power_info, CFArrayGetValueAtIndex(power_sources_list, 0)); + if (!power_sources_information) { + PyErr_SetString( + PyExc_RuntimeError, "Failed to get power source description" + ); + goto error; + } - capacity_ref = (CFNumberRef) CFDictionaryGetValue( + capacity_ref = (CFNumberRef)CFDictionaryGetValue( power_sources_information, CFSTR(kIOPSCurrentCapacityKey)); - if (!CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) { + if (!capacity_ref || !CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) { PyErr_SetString(PyExc_RuntimeError, "No battery capacity information in power sources info"); goto error; } - ps_state_ref = (CFStringRef) CFDictionaryGetValue( + ps_state_ref = (CFStringRef)CFDictionaryGetValue( power_sources_information, CFSTR(kIOPSPowerSourceStateKey)); + if (!ps_state_ref) { + PyErr_SetString(PyExc_RuntimeError, "power source state info missing"); + goto error; + } is_power_plugged = CFStringCompare( - ps_state_ref, CFSTR(kIOPSACPowerValue), 0) - == kCFCompareEqualTo; + ps_state_ref, CFSTR(kIOPSACPowerValue), 0) == kCFCompareEqualTo; - time_to_empty_ref = (CFNumberRef) CFDictionaryGetValue( + time_to_empty_ref = (CFNumberRef)CFDictionaryGetValue( power_sources_information, CFSTR(kIOPSTimeToEmptyKey)); - if (!CFNumberGetValue(time_to_empty_ref, + if (!time_to_empty_ref || !CFNumberGetValue(time_to_empty_ref, kCFNumberIntType, &time_to_empty)) { /* This value is recommended for non-Apple power sources, so it's not * an error if it doesn't exist. We'll return -1 for "unknown" */ From 3583d4af48713788042e83b8cf126f10ff231458 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 19:51:27 +0200 Subject: [PATCH 1323/1714] osx / boot_time(): check sysctl() size mismatch --- psutil/arch/osx/sys.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/psutil/arch/osx/sys.c b/psutil/arch/osx/sys.c index 6f528f0217..927b7184d7 100644 --- a/psutil/arch/osx/sys.c +++ b/psutil/arch/osx/sys.c @@ -19,11 +19,19 @@ psutil_boot_time(PyObject *self, PyObject *args) { // fetch sysctl "kern.boottime" static int request[2] = { CTL_KERN, KERN_BOOTTIME }; struct timeval result; - size_t result_len = sizeof result; + size_t result_len = sizeof(result); time_t boot_time = 0; - if (sysctl(request, 2, &result, &result_len, NULL, 0) == -1) + if (sysctl(request, 2, &result, &result_len, NULL, 0) == -1) { return PyErr_SetFromErrno(PyExc_OSError); + } + + if (result_len != sizeof(result)) { + PyErr_SetString(PyExc_RuntimeError, "sysctl size mismatch"); + return NULL; + } + boot_time = result.tv_sec; + return Py_BuildValue("d", (double)boot_time); } From 25d2529a986651d1922c5ac9e7e74ec49a32d68f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 10 Aug 2025 19:53:53 +0200 Subject: [PATCH 1324/1714] update HISTORY.rst --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 7b468e8c76..e0d3d76f9e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -39,6 +39,8 @@ XXXX-XX-XX - 2560_, [Linux]: `Process.memory_maps()`_ may crash with `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien Stephan) +- 2586_, [macOS], [CRITICAL]: fixed different places in C code which can + trigger a segfault. - 2605_, [Linux]: `psutil.sensors_battery()` reports a negative amount for seconds left. - 2607_, [Windows]: ``WindowsService.description()`` method may fail with From 5b82572e576e0c0bd2d39a1323401f46fd7eca32 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Aug 2025 12:34:46 +0200 Subject: [PATCH 1325/1714] BSD / OSX: sysctl() wrapper (#2619) --- MANIFEST.in | 1 + psutil/arch/bsd/cpu.c | 3 + psutil/arch/bsd/disk.c | 1 + psutil/arch/bsd/net.c | 2 + psutil/arch/bsd/sys.c | 2 + psutil/arch/freebsd/cpu.c | 79 +++++++++++-------------- psutil/arch/freebsd/mem.c | 106 ++++++++++++++-------------------- psutil/arch/freebsd/proc.c | 8 +-- psutil/arch/freebsd/sensors.c | 10 ++-- psutil/arch/netbsd/cpu.c | 22 ++----- psutil/arch/netbsd/mem.c | 24 ++++---- psutil/arch/openbsd/cpu.c | 27 +++------ psutil/arch/openbsd/mem.c | 16 ++--- psutil/arch/osx/cpu.c | 32 +++++----- psutil/arch/osx/mem.c | 19 ++---- psutil/arch/osx/proc.c | 27 ++------- psutil/arch/osx/sys.c | 13 +---- psutil/arch/posix/init.h | 8 +++ psutil/arch/posix/sysctl.c | 78 +++++++++++++++++++++++++ 19 files changed, 234 insertions(+), 244 deletions(-) create mode 100644 psutil/arch/posix/sysctl.c diff --git a/MANIFEST.in b/MANIFEST.in index f4386431c5..c33d589566 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -96,6 +96,7 @@ include psutil/arch/posix/net.c include psutil/arch/posix/net.h include psutil/arch/posix/proc.c include psutil/arch/posix/proc.h +include psutil/arch/posix/sysctl.c include psutil/arch/posix/users.c include psutil/arch/posix/users.h include psutil/arch/sunos/cpu.c diff --git a/psutil/arch/bsd/cpu.c b/psutil/arch/bsd/cpu.c index 69325c636f..7681005e12 100644 --- a/psutil/arch/bsd/cpu.c +++ b/psutil/arch/bsd/cpu.c @@ -8,6 +8,9 @@ #include #include #include +#include + +#include "../../arch/all/init.h" PyObject * diff --git a/psutil/arch/bsd/disk.c b/psutil/arch/bsd/disk.c index bc1cf86329..2f381da202 100644 --- a/psutil/arch/bsd/disk.c +++ b/psutil/arch/bsd/disk.c @@ -17,6 +17,7 @@ #include #endif +#include "../../arch/all/init.h" PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { diff --git a/psutil/arch/bsd/net.c b/psutil/arch/bsd/net.c index c2fcc06632..deb905fc66 100644 --- a/psutil/arch/bsd/net.c +++ b/psutil/arch/bsd/net.c @@ -11,6 +11,8 @@ #include #include +#include "../../arch/all/init.h" + PyObject * psutil_net_io_counters(PyObject *self, PyObject *args) { diff --git a/psutil/arch/bsd/sys.c b/psutil/arch/bsd/sys.c index 9af40e7403..9237a6ea03 100644 --- a/psutil/arch/bsd/sys.c +++ b/psutil/arch/bsd/sys.c @@ -14,6 +14,8 @@ #include #endif +#include "../../arch/all/init.h" + // Return a Python float indicating the system boot time expressed in // seconds since the epoch. diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index d735e4b65a..201db3fc34 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -28,43 +28,35 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { int maxcpus; int mib[2]; int ncpu; - size_t len; size_t size; - int i; PyObject *py_retlist = PyList_New(0); PyObject *py_cputime = NULL; if (py_retlist == NULL) return NULL; - // retrieve maxcpus value - size = sizeof(maxcpus); - if (sysctlbyname("kern.smp.maxcpus", &maxcpus, &size, NULL, 0) < 0) { - Py_DECREF(py_retlist); - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('kern.smp.maxcpus')"); - } - long cpu_time[maxcpus][CPUSTATES]; - - // retrieve the number of cpus + // retrieve the number of CPUs currently online mib[0] = CTL_HW; mib[1] = HW_NCPU; - len = sizeof(ncpu); - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_NCPU)"); + if (psutil_sysctl_fixed(mib, 2, &ncpu, sizeof(ncpu)) != 0) { goto error; } - // per-cpu info - size = sizeof(cpu_time); - if (sysctlbyname("kern.cp_times", &cpu_time, &size, NULL, 0) == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('kern.smp.maxcpus')" - ); + // allocate buffer dynamically based on actual CPU count + long (*cpu_time)[CPUSTATES] = malloc(ncpu * sizeof(*cpu_time)); + if (!cpu_time) { + PyErr_NoMemory(); goto error; } - for (i = 0; i < ncpu; i++) { + // get per-cpu times using ncpu count + size = ncpu * sizeof(*cpu_time); + if (psutil_sysctlbyname_fixed("kern.cp_times", cpu_time, size) == -1) { + free(cpu_time); + goto error; + } + + for (int i = 0; i < ncpu; i++) { py_cputime = Py_BuildValue( "(ddddd)", (double)cpu_time[i][CP_USER] / CLOCKS_PER_SEC, @@ -72,13 +64,19 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { (double)cpu_time[i][CP_SYS] / CLOCKS_PER_SEC, (double)cpu_time[i][CP_IDLE] / CLOCKS_PER_SEC, (double)cpu_time[i][CP_INTR] / CLOCKS_PER_SEC); - if (!py_cputime) + if (!py_cputime) { + free(cpu_time); goto error; - if (PyList_Append(py_retlist, py_cputime)) + } + if (PyList_Append(py_retlist, py_cputime)) { + Py_DECREF(py_cputime); + free(cpu_time); goto error; + } Py_DECREF(py_cputime); } + free(cpu_time); return py_retlist; error: @@ -126,26 +124,16 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { unsigned int v_swtch; size_t size = sizeof(v_soft); - if (sysctlbyname("vm.stats.sys.v_soft", &v_soft, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_soft')"); - } - if (sysctlbyname("vm.stats.sys.v_intr", &v_intr, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_intr')"); - } - if (sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_syscall')"); - } - if (sysctlbyname("vm.stats.sys.v_trap", &v_trap, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_trap')"); - } - if (sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.sys.v_swtch')"); - } + if (psutil_sysctlbyname_fixed("vm.stats.sys.v_soft", &v_soft, size) != 0) + return NULL; + if (psutil_sysctlbyname_fixed("vm.stats.sys.v_intr", &v_intr, size) != 0) + return NULL; + if (psutil_sysctlbyname_fixed("vm.stats.sys.v_syscall", &v_syscall, size) != 0) + return NULL; + if (psutil_sysctlbyname_fixed("vm.stats.sys.v_trap", &v_trap, size) != 0) + return NULL; + if (psutil_sysctlbyname_fixed("vm.stats.sys.v_swtch", &v_swtch, size) != 0) + return NULL; return Py_BuildValue( "IIIII", @@ -173,9 +161,10 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "i", &core)) return NULL; + // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ sprintf(sensor, "dev.cpu.%d.freq", core); - if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed(sensor, ¤t, size) != 0) goto error; size = sizeof(available_freq_levels); diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 70aeafbf3a..6da0e521be 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -23,54 +23,46 @@ PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { - unsigned long total; - unsigned int active, inactive, wired, cached, free; - size_t size = sizeof(total); - struct vmtotal vm; - int mib[] = {CTL_VM, VM_METER}; - long pagesize = psutil_getpagesize(); + unsigned long total; + unsigned int active, inactive, wired, cached, free; long buffers; - size_t buffers_size = sizeof(buffers); + struct vmtotal vm; + int mib[] = {CTL_VM, VM_METER}; + long pagesize = psutil_getpagesize(); - if (sysctlbyname("hw.physmem", &total, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('hw.physmem')" - ); - } - if (sysctlbyname("vm.stats.vm.v_active_count", &active, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_active_count')"); - } - if (sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, &size, NULL, 0)) - { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_inactive_count')"); - } - if (sysctlbyname("vm.stats.vm.v_wire_count", &wired, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_wire_count')" - ); - } - // https://github.com/giampaolo/psutil/issues/997 - if (sysctlbyname("vm.stats.vm.v_cache_count", &cached, &size, NULL, 0)) { + size_t size_ul = sizeof(total); + size_t size_ui = sizeof(active); + size_t size_long = sizeof(buffers); + size_t size_vm = sizeof(vm); + + PyObject *ret = NULL; + + if (psutil_sysctlbyname_fixed("hw.physmem", &total, size_ul) != 0) + return NULL; + + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_active_count", &active, size_ui) != 0) + return NULL; + + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_inactive_count", &inactive, size_ui) != 0) + return NULL; + + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_wire_count", &wired, size_ui) != 0) + return NULL; + + // Optional; ignore error if not available + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_cache_count", &cached, size_ui) != 0) { + PyErr_Clear(); cached = 0; } - if (sysctlbyname("vm.stats.vm.v_free_count", &free, &size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_free_count')" - ); - } - if (sysctlbyname("vfs.bufspace", &buffers, &buffers_size, NULL, 0)) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vfs.bufspace')" - ); - } - size = sizeof(vm); - if (sysctl(mib, 2, &vm, &size, NULL, 0) != 0) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctl(CTL_VM | VM_METER)" - ); + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_free_count", &free, size_ui) != 0) + return NULL; + + if (psutil_sysctlbyname_fixed("vfs.bufspace", &buffers, size_long) != 0) + return NULL; + + if (psutil_sysctl_fixed(mib, 2, &vm, size_vm) != 0) { + return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(CTL_VM | VM_METER)"); } return Py_BuildValue("KKKKKKKK", @@ -110,26 +102,14 @@ psutil_swap_mem(PyObject *self, PyObject *args) { kvm_close(kd); - if (sysctlbyname("vm.stats.vm.v_swapin", &swapin, &size, NULL, 0) == -1) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_swapin)'" - ); - } - if (sysctlbyname("vm.stats.vm.v_swapout", &swapout, &size, NULL, 0) == -1){ - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_swapout)'" - ); - } - if (sysctlbyname("vm.stats.vm.v_vnodein", &nodein, &size, NULL, 0) == -1) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_vnodein)'" - ); - } - if (sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, &size, NULL, 0) == -1) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('vm.stats.vm.v_vnodeout)'" - ); - } + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_swapin", &swapin, size) != 0) + return NULL; + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_swapout", &swapout, size) != 0) + return NULL; + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_vnodein", &nodein, size) != 0) + return NULL; + if (psutil_sysctlbyname_fixed("vm.stats.vm.v_vnodeout", &nodeout, size) != 0) + return NULL; return Py_BuildValue( "(KKKII)", diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index a0381e0995..92a760eaad 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -153,12 +153,8 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; - // Get the maximum process arguments size. - mib[0] = CTL_KERN; - mib[1] = KERN_ARGMAX; - - size = sizeof(argmax); - if (sysctl(mib, 2, &argmax, &size, NULL, 0) == -1) + argmax = psutil_sysctl_argmax(); + if (! argmax) goto error; // Allocate space for the arguments. diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 96738dd7c3..ab8f184c36 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -29,11 +29,11 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { int power_plugged; size_t size = sizeof(percent); - if (sysctlbyname("hw.acpi.battery.life", &percent, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed("hw.acpi.battery.life", &percent, size) != 0) goto error; - if (sysctlbyname("hw.acpi.battery.time", &minsleft, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed("hw.acpi.battery.time", &minsleft, size) != 0) goto error; - if (sysctlbyname("hw.acpi.acline", &power_plugged, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed("hw.acpi.acline", &power_plugged, size) != 0) goto error; return Py_BuildValue("iii", percent, minsleft, power_plugged); @@ -59,13 +59,13 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "i", &core)) return NULL; sprintf(sensor, "dev.cpu.%d.temperature", core); - if (sysctlbyname(sensor, ¤t, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed(sensor, ¤t, size) != 0) goto error; current = DECIKELVIN_2_CELSIUS(current); // Return -273 in case of failure. sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); - if (sysctlbyname(sensor, &tjmax, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed(sensor, &tjmax, size) != 0) tjmax = 0; tjmax = DECIKELVIN_2_CELSIUS(tjmax); diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c index 1b93a7ae49..9b44038d74 100644 --- a/psutil/arch/netbsd/cpu.c +++ b/psutil/arch/netbsd/cpu.c @@ -25,16 +25,11 @@ original(ish) implementations: PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { - size_t size; struct uvmexp_sysctl uv; int uvmexp_mib[] = {CTL_VM, VM_UVMEXP2}; - size = sizeof(uv); - if (sysctl(uvmexp_mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(uvmexp_mib, 2, &uv, sizeof(uv)) != 0) return NULL; - } - return Py_BuildValue( "IIIIIII", uv.swtch, // ctx switches @@ -60,14 +55,13 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; + // retrieve the number of cpus mib[0] = CTL_HW; mib[1] = HW_NCPU; - len = sizeof(ncpu); - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(mib, 2, &ncpu, sizeof(ncpu)) != 0) goto error; - } + uint64_t cpu_time[CPUSTATES]; for (i = 0; i < ncpu; i++) { @@ -75,12 +69,8 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { mib[0] = CTL_KERN; mib[1] = KERN_CP_TIME; mib[2] = i; - size = sizeof(cpu_time); - if (sysctl(mib, 3, &cpu_time, &size, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - + if (psutil_sysctl_fixed(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) + goto error; py_cputime = Py_BuildValue( "(ddddd)", (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 2f8228a6c1..c04d6f4171 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -32,11 +32,8 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { int mib[] = {CTL_VM, VM_UVMEXP2}; long long cached; - size = sizeof(uv); - if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(mib, 2, &uv, sizeof(uv)) != 0) return NULL; - } // Note: zabbix does not include anonpages, but that doesn't match the // "Cached" value in /proc/meminfo. @@ -93,18 +90,17 @@ psutil_swap_mem(PyObject *self, PyObject *args) { size_t size = sizeof(total); struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; - size = sizeof(uv); - if (sysctl(mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(mib, 2, &uv, sizeof(uv)) != 0) goto error; - } - return Py_BuildValue("(LLLll)", - swap_total, - (swap_total - swap_free), - swap_free, - (long) uv.pgswapin * pagesize, // swap in - (long) uv.pgswapout * pagesize); // swap out + return Py_BuildValue( + "(LLLll)", + swap_total, + (swap_total - swap_free), + swap_free, + (long) uv.pgswapin * pagesize, // swap in + (long) uv.pgswapout * pagesize // swap out + ); error: free(swdev); diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c index 2454be7f7d..3289fcfb98 100644 --- a/psutil/arch/openbsd/cpu.c +++ b/psutil/arch/openbsd/cpu.c @@ -17,7 +17,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { int mib[3]; int ncpu; size_t len; - size_t size; int i; PyObject *py_retlist = PyList_New(0); PyObject *py_cputime = NULL; @@ -28,22 +27,18 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // retrieve the number of cpus mib[0] = CTL_HW; mib[1] = HW_NCPU; - len = sizeof(ncpu); - if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(mib, 2, &ncpu, sizeof(ncpu)) != 0) goto error; - } + uint64_t cpu_time[CPUSTATES]; for (i = 0; i < ncpu; i++) { mib[0] = CTL_KERN; mib[1] = KERN_CPTIME2; mib[2] = i; - size = sizeof(cpu_time); - if (sysctl(mib, 3, &cpu_time, &size, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } + + if (psutil_sysctl_fixed(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) + goto error; py_cputime = Py_BuildValue( "(ddddd)", @@ -70,15 +65,11 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { - size_t size; struct uvmexp uv; int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; - size = sizeof(uv); - if (sysctl(uvmexp_mib, 2, &uv, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(uvmexp_mib, 2, &uv, sizeof(uv)) !=0) return NULL; - } return Py_BuildValue( "IIIIIII", @@ -96,16 +87,12 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { int freq; - size_t size; int mib[2] = {CTL_HW, HW_CPUSPEED}; // On VirtualBox I get "sysctl hw.cpuspeed=2593" (never changing), // which appears to be expressed in Mhz. - size = sizeof(freq); - if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(mib, 2, &freq, sizeof(freq)) != 0) return NULL; - } return Py_BuildValue("i", freq); } diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index 4cd1282e84..821d476766 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -29,28 +29,20 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { long pagesize = psutil_getpagesize(); size = sizeof(total_physmem); - if (sysctl(physmem_mib, 2, &total_physmem, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(physmem_mib, 2, &total_physmem, size) != 0) return NULL; - } size = sizeof(uvmexp); - if (sysctl(uvmexp_mib, 2, &uvmexp, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(uvmexp_mib, 2, &uvmexp, size) != 0) return NULL; - } size = sizeof(bcstats); - if (sysctl(bcstats_mib, 3, &bcstats, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(bcstats_mib, 3, &bcstats, size) != 0) return NULL; - } size = sizeof(vmdata); - if (sysctl(vmmeter_mib, 2, &vmdata, &size, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(vmmeter_mib, 2, &vmdata, size) != 0) return NULL; - } return Py_BuildValue("KKKKKKKK", // Note: many programs calculate total memory as diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 8062769c59..c701b07859 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -41,9 +41,8 @@ For reference, here's the git history with original implementations: PyObject * psutil_cpu_count_logical(PyObject *self, PyObject *args) { int num; - size_t size = sizeof(int); - if (sysctlbyname("hw.logicalcpu", &num, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed("hw.logicalcpu", &num, sizeof(num)) != 0) Py_RETURN_NONE; // mimic os.cpu_count() else return Py_BuildValue("i", num); @@ -53,9 +52,8 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { PyObject * psutil_cpu_count_cores(PyObject *self, PyObject *args) { int num; - size_t size = sizeof(int); - if (sysctlbyname("hw.physicalcpu", &num, &size, NULL, 0)) + if (psutil_sysctlbyname_fixed("hw.physicalcpu", &num, sizeof(num)) != 0) Py_RETURN_NONE; // mimic os.cpu_count() else return Py_BuildValue("i", num); @@ -261,26 +259,30 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { unsigned int curr; - int64_t min = 0; - int64_t max = 0; + int64_t min; + int64_t max; int mib[2]; - size_t len = sizeof(curr); - size_t size = sizeof(min); // also available as "hw.cpufrequency" but it's deprecated mib[0] = CTL_HW; mib[1] = HW_CPU_FREQ; - if (sysctl(mib, 2, &curr, &len, NULL, 0) < 0) + if (psutil_sysctl_fixed(mib, 2, &curr, sizeof(curr)) < 0) return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); - size = sizeof(min); - if (sysctlbyname("hw.cpufrequency_min", &min, &size, NULL, 0)) - psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); + if (psutil_sysctlbyname_fixed( + "hw.cpufrequency_min", &min, sizeof(min)) != 0) + { + min = 0; + psutil_debug("sysctlbyname('hw.cpufrequency_min') failed (set to 0)"); + } - size = sizeof(max); - if (sysctlbyname("hw.cpufrequency_max", &max, &size, NULL, 0)) - psutil_debug("sysctl('hw.cpufrequency_min') failed (set to 0)"); + if (psutil_sysctlbyname_fixed( + "hw.cpufrequency_max", &max, sizeof(max)) != 0) + { + max = 0; + psutil_debug("sysctlbyname('hw.cpufrequency_max') failed (set to 0)"); + } return Py_BuildValue( "KKK", diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 34c874afff..4774858a5a 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -56,7 +56,6 @@ PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { int mib[2]; uint64_t total; - size_t len = sizeof(total); vm_statistics64_data_t vm; long pagesize = psutil_getpagesize(); // physical mem @@ -64,10 +63,8 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { mib[1] = HW_MEMSIZE; // This is also available as sysctlbyname("hw.memsize"). - if (sysctl(mib, 2, &total, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(mib, 2, &total, sizeof(total)) != 0) return NULL; - } // vm if (psutil_sys_vminfo(&vm) != 0) @@ -91,22 +88,16 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { int mib[2]; - size_t size; struct xsw_usage totals; vm_statistics64_data_t vmstat; long pagesize = psutil_getpagesize(); mib[0] = CTL_VM; mib[1] = VM_SWAPUSAGE; - size = sizeof(totals); - if (sysctl(mib, 2, &totals, &size, NULL, 0) == -1) { - if (errno != 0) - PyErr_SetFromErrno(PyExc_OSError); - else - PyErr_Format( - PyExc_RuntimeError, "sysctl(VM_SWAPUSAGE) syscall failed"); - return NULL; - } + + if (psutil_sysctl_fixed(mib, 2, &totals, sizeof(totals)) != 0) + return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); + if (psutil_sys_vminfo(&vmstat) != 0) return NULL; diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 5983027cb8..2b04fe46f7 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -119,23 +119,6 @@ psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { } -// Read the maximum argument size for processes -static int -psutil_sysctl_argmax() { - int argmax; - int mib[2]; - size_t size = sizeof(argmax); - - mib[0] = CTL_KERN; - mib[1] = KERN_ARGMAX; - - if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) - return argmax; - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); - return 0; -} - - // Read process argument space. static int psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { @@ -584,7 +567,6 @@ psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { PyObject * psutil_proc_memory_uss(PyObject *self, PyObject *args) { pid_t pid; - size_t len; cpu_type_t cpu_type; size_t private_pages = 0; mach_vm_size_t size = 0; @@ -602,11 +584,10 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { if (psutil_task_for_pid(pid, &task) != 0) return NULL; - len = sizeof(cpu_type); - if (sysctlbyname("sysctl.proc_cputype", &cpu_type, &len, NULL, 0) != 0) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctlbyname('sysctl.proc_cputype')" - ); + if (psutil_sysctlbyname_fixed( + "sysctl.proc_cputype", &cpu_type, sizeof(cpu_type)) != 0) + { + return NULL; } // Roughly based on libtop_update_vm_regions in diff --git a/psutil/arch/osx/sys.c b/psutil/arch/osx/sys.c index 927b7184d7..3846056fd6 100644 --- a/psutil/arch/osx/sys.c +++ b/psutil/arch/osx/sys.c @@ -17,21 +17,12 @@ PyObject * psutil_boot_time(PyObject *self, PyObject *args) { // fetch sysctl "kern.boottime" - static int request[2] = { CTL_KERN, KERN_BOOTTIME }; + int mib[2] = {CTL_KERN, KERN_BOOTTIME}; struct timeval result; - size_t result_len = sizeof(result); time_t boot_time = 0; - if (sysctl(request, 2, &result, &result_len, NULL, 0) == -1) { - return PyErr_SetFromErrno(PyExc_OSError); - } - - if (result_len != sizeof(result)) { - PyErr_SetString(PyExc_RuntimeError, "sysctl size mismatch"); + if (psutil_sysctl_fixed(mib, 2, &result, sizeof(result)) == -1) return NULL; - } - boot_time = result.tv_sec; - return Py_BuildValue("d", (double)boot_time); } diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 11b2798b88..47cfb7067e 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -10,6 +10,14 @@ #include "users.h" #endif +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) + #include + + int psutil_sysctl_fixed(int *mib, u_int miblen, void *buf, size_t buflen); + int psutil_sysctlbyname_fixed(const char *name, void *buf, size_t buflen); + int psutil_sysctl_argmax(); +#endif + long psutil_getpagesize(void); PyObject *psutil_getpagesize_pywrapper(PyObject *self, PyObject *args); PyObject *psutil_users(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c new file mode 100644 index 0000000000..aa0e7e4e56 --- /dev/null +++ b/psutil/arch/posix/sysctl.c @@ -0,0 +1,78 @@ +/* + * 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. + */ + +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#include +#include +#include + +#include "../../arch/all/init.h" + + +// A thin wrapper on top of sysctl(). +int +psutil_sysctl_fixed(int *mib, u_int miblen, void *buf, size_t buflen) { + size_t len = buflen; + + if (sysctl(mib, miblen, buf, &len, NULL, 0) == -1) { + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl()"); + return -1; + } + + if (len != buflen) { + PyErr_SetString(PyExc_RuntimeError, "sysctl() size mismatch"); + return -1; + } + + return 0; +} + + +#if !defined(PSUTIL_OPENBSD) +// A thin wrapper on top of sysctlbyname(). +int +psutil_sysctlbyname_fixed(const char *name, void *buf, size_t buflen) { + size_t len = buflen; + char errbuf[256]; + + if (sysctlbyname(name, buf, &len, NULL, 0) == -1) { + snprintf(errbuf, sizeof(errbuf), "sysctlbyname('%s')", name); + psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + return -1; + } + + if (len != buflen) { + snprintf( + errbuf, + sizeof(errbuf), + "sysctlbyname('%s') size mismatch: returned %zu, expected %zu", + name, + len, + buflen + ); + PyErr_SetString(PyExc_RuntimeError, errbuf); + return -1; + } + + return 0; +} +#endif // !PSUTIL_OPENBSD + + +// Get the maximum process arguments size. +int +psutil_sysctl_argmax() { + int argmax; + int mib[2] = {CTL_KERN, KERN_ARGMAX}; + + if (psutil_sysctl_fixed(mib, 2, &argmax, sizeof(argmax)) != 0) { + PyErr_Clear(); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); + return 0; + } + return argmax; +} +#endif // defined(PLATFORMS…) From 344356f9e750a2516c2b506c90074a40f4b9a6e5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Aug 2025 14:21:11 +0200 Subject: [PATCH 1326/1714] openbsd / users(): return None instead of negative integer for user PID --- psutil/arch/openbsd/users.c | 2 +- psutil/tests/test_contracts.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/arch/openbsd/users.c b/psutil/arch/openbsd/users.c index 5475a91261..5afc25a094 100644 --- a/psutil/arch/openbsd/users.c +++ b/psutil/arch/openbsd/users.c @@ -46,7 +46,7 @@ psutil_users(PyObject *self, PyObject *args) { if (! py_hostname) goto error; py_tuple = Py_BuildValue( - "(OOOdi)", + "(OOOdO)", py_username, // username py_tty, // tty py_hostname, // hostname diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index ad13f6c0eb..aa3a4fef3c 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -306,6 +306,8 @@ def test_users(self): assert isinstance(user.terminal, (str, type(None))) assert isinstance(user.host, (str, type(None))) assert isinstance(user.pid, (int, type(None))) + if isinstance(user.pid, int): + assert user.pid > 0 class TestProcessWaitType(PsutilTestCase): From 9ac9bc7e1481a74d61def6ee0567a9b87a54b23d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Aug 2025 17:10:30 +0200 Subject: [PATCH 1327/1714] use @serial around one flaky test --- psutil/tests/test_process.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index e47443e35e..d3341ebbef 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1069,6 +1069,7 @@ def test_open_files_2(self): assert fileobj.name not in p.open_files() @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.xdist_group(name="serial") def test_num_fds(self): p = psutil.Process() testfn = self.get_testfn() From 24074de2f9a35310954e493893e4e5aa7df32c2c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Aug 2025 21:29:05 +0200 Subject: [PATCH 1328/1714] [OSX, BSD] wrapper around malloc / dynamic sysctl() (#2620) --- psutil/arch/freebsd/proc.c | 90 ++++++-------------------------------- psutil/arch/netbsd/disk.c | 24 ++++------ psutil/arch/openbsd/disk.c | 17 ++----- psutil/arch/openbsd/proc.c | 27 ++++-------- psutil/arch/osx/net.c | 31 ++++--------- psutil/arch/osx/proc.c | 81 +++++++--------------------------- psutil/arch/posix/init.h | 1 + psutil/arch/posix/sysctl.c | 64 +++++++++++++++++++++++++++ 8 files changed, 122 insertions(+), 213 deletions(-) diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 92a760eaad..bc21933d5a 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -72,67 +72,25 @@ static void psutil_remove_spaces(char *str) { // APIS // ============================================================================ -int -psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { - // Returns a list of all BSD processes on the system. This routine - // allocates the list and puts it in *procList and a count of the - // number of entries in *procCount. You are responsible for freeing - // this list. On success returns 0, else 1 with exception set. - int err; - struct kinfo_proc *buf = NULL; - int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; + +int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; size_t length = 0; - size_t max_length = 12 * 1024 * 1024; // 12MB + char *buf = NULL; assert(procList != NULL); assert(*procList == NULL); assert(procCount != NULL); - // Call sysctl with a NULL buffer in order to get buffer length. - err = sysctl(name, 3, NULL, &length, NULL, 0); - if (err == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl (null buffer)"); + if (psutil_sysctl_malloc(mib, 4, &buf, &length) != 0) { return 1; } - while (1) { - // Allocate an appropriately sized buffer based on the results - // from the previous call. - buf = malloc(length); - if (buf == NULL) { - PyErr_NoMemory(); - return 1; - } - - // Call sysctl again with the new buffer. - err = sysctl(name, 3, buf, &length, NULL, 0); - if (err == -1) { - free(buf); - if (errno == ENOMEM) { - // Sometimes the first sysctl() suggested size is not enough, - // so we dynamically increase it until it's big enough : - // https://github.com/giampaolo/psutil/issues/2093 - psutil_debug("errno=ENOMEM, length=%zu; retrying", length); - length *= 2; - if (length < max_length) { - continue; - } - } - - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl()"); - return 1; - } - else { - break; - } - } - - *procList = buf; + *procList = (struct kinfo_proc *)buf; *procCount = length / sizeof(struct kinfo_proc); return 0; } - /* * Borrowed from psi Python System Information project * Based on code from ps. @@ -141,40 +99,24 @@ PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; int mib[4]; - int argmax; - size_t size = sizeof(argmax); char *procargs = NULL; + size_t size = 0; size_t pos = 0; PyObject *py_retlist = PyList_New(0); PyObject *py_arg = NULL; if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - goto error; - - argmax = psutil_sysctl_argmax(); - if (! argmax) - goto error; - - // Allocate space for the arguments. - procargs = (char *)malloc(argmax); - if (procargs == NULL) { - PyErr_NoMemory(); + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; - } - // Make a sysctl() call to get the raw argument space of the process. mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_ARGS; mib[3] = pid; - size = argmax; - if (sysctl(mib, 4, procargs, &size, NULL, 0) == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGS)"); + if (psutil_sysctl_malloc(mib, 4, &procargs, &size) != 0) goto error; - } // args are returned as a flattened string with \0 separators between // arguments add each string to the list then step forward to the next @@ -187,7 +129,8 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (PyList_Append(py_retlist, py_arg)) goto error; Py_DECREF(py_arg); - pos = pos + strlen(&procargs[pos]) + 1; + py_arg = NULL; + pos += strlen(&procargs[pos]) + 1; } } @@ -196,13 +139,12 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { error: Py_XDECREF(py_arg); - Py_DECREF(py_retlist); + Py_XDECREF(py_retlist); if (procargs != NULL) free(procargs); return NULL; } - /* * Return process pathname executable. * Thanks to Robert N. M. Watson: @@ -632,9 +574,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { PyObject * psutil_proc_getrlimit(PyObject *self, PyObject *args) { pid_t pid; - int ret; int resource; - size_t len; int name[5]; struct rlimit rlp; @@ -646,11 +586,9 @@ psutil_proc_getrlimit(PyObject *self, PyObject *args) { name[2] = KERN_PROC_RLIMIT; name[3] = pid; name[4] = resource; - len = sizeof(rlp); - ret = sysctl(name, 5, &rlp, &len, NULL, 0); - if (ret == -1) - return PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_fixed(name, 5, &rlp, sizeof(rlp)) != 0) + return NULL; #if defined(HAVE_LONG_LONG) return Py_BuildValue("LL", diff --git a/psutil/arch/netbsd/disk.c b/psutil/arch/netbsd/disk.c index 18e88889b8..421be47f7a 100644 --- a/psutil/arch/netbsd/disk.c +++ b/psutil/arch/netbsd/disk.c @@ -22,33 +22,25 @@ original(ish) implementations: PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { - int i, dk_ndrive, mib[3]; - size_t len; + int i, dk_ndrive; + int mib[3]; struct io_sysctl *stats = NULL; + size_t stats_len; PyObject *py_disk_info = NULL; PyObject *py_retdict = PyDict_New(); if (py_retdict == NULL) return NULL; + mib[0] = CTL_HW; mib[1] = HW_IOSTATS; mib[2] = sizeof(struct io_sysctl); - len = 0; - if (sysctl(mib, 3, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - dk_ndrive = (int)(len / sizeof(struct io_sysctl)); - stats = malloc(len); - if (stats == NULL) { - PyErr_NoMemory(); + // Use helper to allocate and fill buffer + if (psutil_sysctl_malloc(mib, 3, (char **)&stats, &stats_len) == -1) goto error; - } - if (sysctl(mib, 3, stats, &len, NULL, 0) < 0 ) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } + + dk_ndrive = (int)(stats_len / sizeof(struct io_sysctl)); for (i = 0; i < dk_ndrive; i++) { py_disk_info = Py_BuildValue( diff --git a/psutil/arch/openbsd/disk.c b/psutil/arch/openbsd/disk.c index bb4724e632..36d980d70f 100644 --- a/psutil/arch/openbsd/disk.c +++ b/psutil/arch/openbsd/disk.c @@ -25,22 +25,11 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { mib[0] = CTL_HW; mib[1] = HW_DISKSTATS; - len = 0; - if (sysctl(mib, 2, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - dk_ndrive = (int)(len / sizeof(struct diskstats)); - stats = malloc(len); - if (stats == NULL) { - PyErr_NoMemory(); - goto error; - } - if (sysctl(mib, 2, stats, &len, NULL, 0) < 0 ) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_malloc(mib, 2, (char **)&stats, &len) != 0) goto error; - } + + dk_ndrive = (int)(len / sizeof(struct diskstats)); for (i = 0; i < dk_ndrive; i++) { py_disk_info = Py_BuildValue( diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index a47ceea98a..8b320f5eee 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -111,9 +111,10 @@ PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; int mib[4]; + char *argv_buf = NULL; + size_t argv_len = 0; char **argv = NULL; char **p; - size_t argv_size = 128; PyObject *py_retlist = PyList_New(0); PyObject *py_arg = NULL; @@ -127,22 +128,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { mib[2] = pid; mib[3] = KERN_PROC_ARGV; - // Loop and reallocate until we have enough space to fit argv. - for (;; argv_size *= 2) { - if (argv_size >= 8192) { - PyErr_SetString(PyExc_RuntimeError, - "can't allocate enough space for KERN_PROC_ARGV"); - goto error; - } - if ((argv = realloc(argv, argv_size)) == NULL) - continue; - if (sysctl(mib, 4, argv, &argv_size, NULL, 0) == 0) - break; - if (errno == ENOMEM) - continue; - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_malloc(mib, 4, &argv_buf, &argv_len) == -1) goto error; - } + + argv = (char **)argv_buf; for (p = argv; *p != NULL; p++) { py_arg = PyUnicode_DecodeFSDefault(*p); @@ -153,12 +142,12 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { Py_DECREF(py_arg); } - free(argv); + free(argv_buf); return py_retlist; error: - if (argv != NULL) - free(argv); + if (argv_buf != NULL) + free(argv_buf); Py_XDECREF(py_arg); Py_DECREF(py_retlist); return NULL; diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index a95c1ab581..c11a925b98 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -23,41 +23,28 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { char *buf = NULL, *lim, *next; struct if_msghdr *ifm; int mib[6]; - mib[0] = CTL_NET; // networking subsystem - mib[1] = PF_ROUTE; // type of information - mib[2] = 0; // protocol (IPPROTO_xxx) - mib[3] = 0; // address family - mib[4] = NET_RT_IFLIST2; // operation - mib[5] = 0; - size_t len; + size_t len = 0; PyObject *py_ifc_info = NULL; PyObject *py_retdict = PyDict_New(); if (py_retdict == NULL) return NULL; - if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - buf = malloc(len); - if (buf == NULL) { - PyErr_NoMemory(); - goto error; - } + mib[0] = CTL_NET; // networking subsystem + mib[1] = PF_ROUTE; // type of information + mib[2] = 0; // protocol (IPPROTO_xxx) + mib[3] = 0; // address family + mib[4] = NET_RT_IFLIST2; // operation + mib[5] = 0; - if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_malloc(mib, 6, &buf, &len) != 0) goto error; - } lim = buf + len; for (next = buf; next < lim; ) { - // Check we have enough space for if_msghdr + // Check we have enough space for if_msghdr. if ((size_t)(lim - next) < sizeof(struct if_msghdr)) { - // buffer too small for header psutil_debug("struct xfile size mismatch"); } diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 2b04fe46f7..8e879a8121 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -40,85 +40,34 @@ typedef struct kinfo_proc kinfo_proc; // --- utils // ==================================================================== -/* - * Returns a list of all BSD processes on the system. This routine - * allocates the list and puts it in *procList and a count of the - * number of entries in *procCount. You are responsible for freeing - * this list (use "free" from System framework). - * On success, the function returns 0. - * On error, the function returns a BSD errno value. - */ + static int -psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { +psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { int mib[3]; - size_t size, size2; - void *ptr; - int err; - int lim = 8; // some limit + size_t len = 0; + char *buf = NULL; mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_ALL; + *procList = NULL; *procCount = 0; - /* - * We start by calling sysctl with ptr == NULL and size == 0. - * That will succeed, and set size to the appropriate length. - * We then allocate a buffer of at least that size and call - * sysctl with that buffer. If that succeeds, we're done. - * If that call fails with ENOMEM, we throw the buffer away - * and try again. - * Note that the loop calls sysctl with NULL again. This is - * is necessary because the ENOMEM failure case sets size to - * the amount of data returned, not the amount of data that - * could have been returned. - */ - while (lim-- > 0) { - size = 0; - if (sysctl((int *)mib, 3, NULL, &size, NULL, 0) == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); - return 1; - } - size2 = size + (size >> 3); // add some - if (size2 > size) { - ptr = malloc(size2); - if (ptr == NULL) - ptr = malloc(size); - else - size = size2; - } - else { - ptr = malloc(size); - } - if (ptr == NULL) { - PyErr_NoMemory(); - return 1; - } + if (psutil_sysctl_malloc(mib, 3, &buf, &len) != 0) + return 1; - if (sysctl((int *)mib, 3, ptr, &size, NULL, 0) == -1) { - err = errno; - free(ptr); - if (err != ENOMEM) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ALL)"); - return 1; - } - } - else { - *procList = (kinfo_proc *)ptr; - *procCount = size / sizeof(kinfo_proc); - if (procCount <= 0) { - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); - return 1; - } - return 0; // success - } + *procList = (struct kinfo_proc *)buf; + *procCount = len / sizeof(struct kinfo_proc); + + if (*procCount == 0) { + free(buf); + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + return 1; } - PyErr_Format(PyExc_RuntimeError, "couldn't collect PIDs list"); - return 1; + return 0; } - // Read process argument space. static int psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 47cfb7067e..4f13b826bc 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -14,6 +14,7 @@ #include int psutil_sysctl_fixed(int *mib, u_int miblen, void *buf, size_t buflen); + int psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen); int psutil_sysctlbyname_fixed(const char *name, void *buf, size_t buflen); int psutil_sysctl_argmax(); #endif diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index aa0e7e4e56..f202ade1f8 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -12,6 +12,9 @@ #include "../../arch/all/init.h" +#define MAX_RETRIES 10 + + // A thin wrapper on top of sysctl(). int psutil_sysctl_fixed(int *mib, u_int miblen, void *buf, size_t buflen) { @@ -30,6 +33,66 @@ psutil_sysctl_fixed(int *mib, u_int miblen, void *buf, size_t buflen) { return 0; } +// Allocate buffer for sysctl with retry on ENOMEM or buffer size mismatch. +// The caller is responsible for freeing the memory. +int +psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { + size_t needed = 0; + char *buffer = NULL; + int ret; + int max_retries = MAX_RETRIES; + + // First query to determine required size + ret = sysctl(mib, miblen, NULL, &needed, NULL, 0); + if (ret == -1) { + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl() malloc 1/3"); + return -1; + } + + while (max_retries-- > 0) { + // zero-initialize buffer to prevent uninitialized bytes + buffer = calloc(1, needed); + if (buffer == NULL) { + PyErr_NoMemory(); + return -1; + } + + size_t len = needed; + ret = sysctl(mib, miblen, buffer, &len, NULL, 0); + + if (ret == 0) { + // Success: return buffer and length + *buf = buffer; + *buflen = len; + return 0; + } + + // Handle buffer too small + if (errno == ENOMEM) { + free(buffer); + buffer = NULL; + + // Re-query needed size for next attempt + if (sysctl(mib, miblen, NULL, &needed, NULL, 0) == -1) { + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl() malloc 2/3"); + return -1; + } + + psutil_debug("psutil_sysctl_malloc() retry"); + continue; + } + + // Other errors: clean up and give up + free(buffer); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl() malloc 3/3"); + return -1; + } + + PyErr_SetString( + PyExc_RuntimeError, "sysctl() buffer allocation retry limit exceeded" + ); + return -1; +} #if !defined(PSUTIL_OPENBSD) // A thin wrapper on top of sysctlbyname(). @@ -73,6 +136,7 @@ psutil_sysctl_argmax() { psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); return 0; } + return argmax; } #endif // defined(PLATFORMS…) From b79581745573c204a0cabd9b367c6316a5d55307 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Aug 2025 16:48:18 +0200 Subject: [PATCH 1329/1714] bsd/osx: define psutil_sysctlbyname_malloc() helper fun --- psutil/arch/bsd/cpu.c | 23 +++++----- psutil/arch/freebsd/cpu.c | 47 ++++++++----------- psutil/arch/freebsd/mem.c | 24 +++++----- psutil/arch/freebsd/proc.c | 2 +- psutil/arch/freebsd/sensors.c | 10 ++--- psutil/arch/netbsd/cpu.c | 6 +-- psutil/arch/netbsd/mem.c | 4 +- psutil/arch/openbsd/cpu.c | 8 ++-- psutil/arch/openbsd/mem.c | 8 ++-- psutil/arch/osx/cpu.c | 14 +++--- psutil/arch/osx/mem.c | 4 +- psutil/arch/osx/proc.c | 2 +- psutil/arch/osx/sys.c | 2 +- psutil/arch/posix/init.h | 5 ++- psutil/arch/posix/sysctl.c | 85 +++++++++++++++++++++++++++++++++-- 15 files changed, 155 insertions(+), 89 deletions(-) diff --git a/psutil/arch/bsd/cpu.c b/psutil/arch/bsd/cpu.c index 7681005e12..48daef1834 100644 --- a/psutil/arch/bsd/cpu.c +++ b/psutil/arch/bsd/cpu.c @@ -41,18 +41,19 @@ psutil_cpu_times(PyObject *self, PyObject *args) { int ret; #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) - ret = sysctlbyname("kern.cp_time", &cpu_time, &size, NULL, 0); + ret = psutil_sysctlbyname("kern.cp_time", &cpu_time, size); #elif PSUTIL_OPENBSD int mib[] = {CTL_KERN, KERN_CPTIME}; - ret = sysctl(mib, 2, &cpu_time, &size, NULL, 0); + ret = psutil_sysctl(mib, 2, &cpu_time, size); #endif - if (ret == -1) - return PyErr_SetFromErrno(PyExc_OSError); - return Py_BuildValue("(ddddd)", - (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC - ); + if (ret != 0) + return NULL; + return Py_BuildValue( + "(ddddd)", + (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC + ); } diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 201db3fc34..9e534dd02d 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -38,7 +38,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // retrieve the number of CPUs currently online mib[0] = CTL_HW; mib[1] = HW_NCPU; - if (psutil_sysctl_fixed(mib, 2, &ncpu, sizeof(ncpu)) != 0) { + if (psutil_sysctl(mib, 2, &ncpu, sizeof(ncpu)) != 0) { goto error; } @@ -51,7 +51,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // get per-cpu times using ncpu count size = ncpu * sizeof(*cpu_time); - if (psutil_sysctlbyname_fixed("kern.cp_times", cpu_time, size) == -1) { + if (psutil_sysctlbyname("kern.cp_times", cpu_time, size) == -1) { free(cpu_time); goto error; } @@ -88,30 +88,20 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { PyObject * psutil_cpu_topology(PyObject *self, PyObject *args) { - void *topology = NULL; + char *topology = NULL; size_t size = 0; PyObject *py_str; - if (sysctlbyname("kern.sched.topology_spec", NULL, &size, NULL, 0)) - goto error; - - topology = malloc(size); - if (!topology) { - PyErr_NoMemory(); - return NULL; + if (psutil_sysctlbyname_malloc( + "kern.sched.topology_spec", &topology, &size) != 0) + { + psutil_debug("ignore sysctlbyname('kern.sched.topology_spec') error"); + Py_RETURN_NONE; } - if (sysctlbyname("kern.sched.topology_spec", topology, &size, NULL, 0)) - goto error; - py_str = Py_BuildValue("s", topology); free(topology); return py_str; - -error: - if (topology != NULL) - free(topology); - Py_RETURN_NONE; } @@ -124,15 +114,15 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { unsigned int v_swtch; size_t size = sizeof(v_soft); - if (psutil_sysctlbyname_fixed("vm.stats.sys.v_soft", &v_soft, size) != 0) + if (psutil_sysctlbyname("vm.stats.sys.v_soft", &v_soft, size) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.sys.v_intr", &v_intr, size) != 0) + if (psutil_sysctlbyname("vm.stats.sys.v_intr", &v_intr, size) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.sys.v_syscall", &v_syscall, size) != 0) + if (psutil_sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, size) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.sys.v_trap", &v_trap, size) != 0) + if (psutil_sysctlbyname("vm.stats.sys.v_trap", &v_trap, size) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.sys.v_swtch", &v_swtch, size) != 0) + if (psutil_sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, size) != 0) return NULL; return Py_BuildValue( @@ -157,21 +147,22 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { int core; char sensor[26]; char available_freq_levels[1000]; - size_t size = sizeof(current); + size_t size; if (! PyArg_ParseTuple(args, "i", &core)) return NULL; // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ + size = sizeof(current); sprintf(sensor, "dev.cpu.%d.freq", core); - if (psutil_sysctlbyname_fixed(sensor, ¤t, size) != 0) + if (psutil_sysctlbyname(sensor, ¤t, size) != 0) goto error; - size = sizeof(available_freq_levels); - // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ // In case of failure, an empty string is returned. + size = sizeof(available_freq_levels); sprintf(sensor, "dev.cpu.%d.freq_levels", core); - sysctlbyname(sensor, &available_freq_levels, &size, NULL, 0); + if (psutil_sysctlbyname(sensor, &available_freq_levels, size) != 0) + psutil_debug("cpu freq levels failed (ignored)"); return Py_BuildValue("is", current, available_freq_levels); diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 6da0e521be..dac470b63d 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -37,31 +37,31 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { PyObject *ret = NULL; - if (psutil_sysctlbyname_fixed("hw.physmem", &total, size_ul) != 0) + if (psutil_sysctlbyname("hw.physmem", &total, size_ul) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_active_count", &active, size_ui) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_active_count", &active, size_ui) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_inactive_count", &inactive, size_ui) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, size_ui) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_wire_count", &wired, size_ui) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_wire_count", &wired, size_ui) != 0) return NULL; // Optional; ignore error if not available - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_cache_count", &cached, size_ui) != 0) { + if (psutil_sysctlbyname("vm.stats.vm.v_cache_count", &cached, size_ui) != 0) { PyErr_Clear(); cached = 0; } - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_free_count", &free, size_ui) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_free_count", &free, size_ui) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vfs.bufspace", &buffers, size_long) != 0) + if (psutil_sysctlbyname("vfs.bufspace", &buffers, size_long) != 0) return NULL; - if (psutil_sysctl_fixed(mib, 2, &vm, size_vm) != 0) { + if (psutil_sysctl(mib, 2, &vm, size_vm) != 0) { return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(CTL_VM | VM_METER)"); } @@ -102,13 +102,13 @@ psutil_swap_mem(PyObject *self, PyObject *args) { kvm_close(kd); - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_swapin", &swapin, size) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_swapin", &swapin, size) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_swapout", &swapout, size) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_swapout", &swapout, size) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_vnodein", &nodein, size) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_vnodein", &nodein, size) != 0) return NULL; - if (psutil_sysctlbyname_fixed("vm.stats.vm.v_vnodeout", &nodeout, size) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, size) != 0) return NULL; return Py_BuildValue( diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index bc21933d5a..c3e18df403 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -587,7 +587,7 @@ psutil_proc_getrlimit(PyObject *self, PyObject *args) { name[3] = pid; name[4] = resource; - if (psutil_sysctl_fixed(name, 5, &rlp, sizeof(rlp)) != 0) + if (psutil_sysctl(name, 5, &rlp, sizeof(rlp)) != 0) return NULL; #if defined(HAVE_LONG_LONG) diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index ab8f184c36..af0326c5a5 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -29,11 +29,11 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { int power_plugged; size_t size = sizeof(percent); - if (psutil_sysctlbyname_fixed("hw.acpi.battery.life", &percent, size) != 0) + if (psutil_sysctlbyname("hw.acpi.battery.life", &percent, size) != 0) goto error; - if (psutil_sysctlbyname_fixed("hw.acpi.battery.time", &minsleft, size) != 0) + if (psutil_sysctlbyname("hw.acpi.battery.time", &minsleft, size) != 0) goto error; - if (psutil_sysctlbyname_fixed("hw.acpi.acline", &power_plugged, size) != 0) + if (psutil_sysctlbyname("hw.acpi.acline", &power_plugged, size) != 0) goto error; return Py_BuildValue("iii", percent, minsleft, power_plugged); @@ -59,13 +59,13 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, "i", &core)) return NULL; sprintf(sensor, "dev.cpu.%d.temperature", core); - if (psutil_sysctlbyname_fixed(sensor, ¤t, size) != 0) + if (psutil_sysctlbyname(sensor, ¤t, size) != 0) goto error; current = DECIKELVIN_2_CELSIUS(current); // Return -273 in case of failure. sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); - if (psutil_sysctlbyname_fixed(sensor, &tjmax, size) != 0) + if (psutil_sysctlbyname(sensor, &tjmax, size) != 0) tjmax = 0; tjmax = DECIKELVIN_2_CELSIUS(tjmax); diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c index 9b44038d74..f87485b06a 100644 --- a/psutil/arch/netbsd/cpu.c +++ b/psutil/arch/netbsd/cpu.c @@ -28,7 +28,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { struct uvmexp_sysctl uv; int uvmexp_mib[] = {CTL_VM, VM_UVMEXP2}; - if (psutil_sysctl_fixed(uvmexp_mib, 2, &uv, sizeof(uv)) != 0) + if (psutil_sysctl(uvmexp_mib, 2, &uv, sizeof(uv)) != 0) return NULL; return Py_BuildValue( "IIIIIII", @@ -59,7 +59,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // retrieve the number of cpus mib[0] = CTL_HW; mib[1] = HW_NCPU; - if (psutil_sysctl_fixed(mib, 2, &ncpu, sizeof(ncpu)) != 0) + if (psutil_sysctl(mib, 2, &ncpu, sizeof(ncpu)) != 0) goto error; uint64_t cpu_time[CPUSTATES]; @@ -69,7 +69,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { mib[0] = CTL_KERN; mib[1] = KERN_CP_TIME; mib[2] = i; - if (psutil_sysctl_fixed(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) + if (psutil_sysctl(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) goto error; py_cputime = Py_BuildValue( "(ddddd)", diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index c04d6f4171..47d5361a40 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -32,7 +32,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { int mib[] = {CTL_VM, VM_UVMEXP2}; long long cached; - if (psutil_sysctl_fixed(mib, 2, &uv, sizeof(uv)) != 0) + if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) return NULL; // Note: zabbix does not include anonpages, but that doesn't match the @@ -90,7 +90,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { size_t size = sizeof(total); struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; - if (psutil_sysctl_fixed(mib, 2, &uv, sizeof(uv)) != 0) + if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) goto error; return Py_BuildValue( diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c index 3289fcfb98..7dcdded15d 100644 --- a/psutil/arch/openbsd/cpu.c +++ b/psutil/arch/openbsd/cpu.c @@ -27,7 +27,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // retrieve the number of cpus mib[0] = CTL_HW; mib[1] = HW_NCPU; - if (psutil_sysctl_fixed(mib, 2, &ncpu, sizeof(ncpu)) != 0) + if (psutil_sysctl(mib, 2, &ncpu, sizeof(ncpu)) != 0) goto error; uint64_t cpu_time[CPUSTATES]; @@ -37,7 +37,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { mib[1] = KERN_CPTIME2; mib[2] = i; - if (psutil_sysctl_fixed(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) + if (psutil_sysctl(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) goto error; py_cputime = Py_BuildValue( @@ -68,7 +68,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { struct uvmexp uv; int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; - if (psutil_sysctl_fixed(uvmexp_mib, 2, &uv, sizeof(uv)) !=0) + if (psutil_sysctl(uvmexp_mib, 2, &uv, sizeof(uv)) !=0) return NULL; return Py_BuildValue( @@ -91,7 +91,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { // On VirtualBox I get "sysctl hw.cpuspeed=2593" (never changing), // which appears to be expressed in Mhz. - if (psutil_sysctl_fixed(mib, 2, &freq, sizeof(freq)) != 0) + if (psutil_sysctl(mib, 2, &freq, sizeof(freq)) != 0) return NULL; return Py_BuildValue("i", freq); diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index 821d476766..2a5a797436 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -29,19 +29,19 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { long pagesize = psutil_getpagesize(); size = sizeof(total_physmem); - if (psutil_sysctl_fixed(physmem_mib, 2, &total_physmem, size) != 0) + if (psutil_sysctl(physmem_mib, 2, &total_physmem, size) != 0) return NULL; size = sizeof(uvmexp); - if (psutil_sysctl_fixed(uvmexp_mib, 2, &uvmexp, size) != 0) + if (psutil_sysctl(uvmexp_mib, 2, &uvmexp, size) != 0) return NULL; size = sizeof(bcstats); - if (psutil_sysctl_fixed(bcstats_mib, 3, &bcstats, size) != 0) + if (psutil_sysctl(bcstats_mib, 3, &bcstats, size) != 0) return NULL; size = sizeof(vmdata); - if (psutil_sysctl_fixed(vmmeter_mib, 2, &vmdata, size) != 0) + if (psutil_sysctl(vmmeter_mib, 2, &vmdata, size) != 0) return NULL; return Py_BuildValue("KKKKKKKK", diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index c701b07859..0536540279 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -42,7 +42,7 @@ PyObject * psutil_cpu_count_logical(PyObject *self, PyObject *args) { int num; - if (psutil_sysctlbyname_fixed("hw.logicalcpu", &num, sizeof(num)) != 0) + if (psutil_sysctlbyname("hw.logicalcpu", &num, sizeof(num)) != 0) Py_RETURN_NONE; // mimic os.cpu_count() else return Py_BuildValue("i", num); @@ -53,7 +53,7 @@ PyObject * psutil_cpu_count_cores(PyObject *self, PyObject *args) { int num; - if (psutil_sysctlbyname_fixed("hw.physicalcpu", &num, sizeof(num)) != 0) + if (psutil_sysctlbyname("hw.physicalcpu", &num, sizeof(num)) != 0) Py_RETURN_NONE; // mimic os.cpu_count() else return Py_BuildValue("i", num); @@ -267,19 +267,15 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { mib[0] = CTL_HW; mib[1] = HW_CPU_FREQ; - if (psutil_sysctl_fixed(mib, 2, &curr, sizeof(curr)) < 0) + if (psutil_sysctl(mib, 2, &curr, sizeof(curr)) < 0) return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); - if (psutil_sysctlbyname_fixed( - "hw.cpufrequency_min", &min, sizeof(min)) != 0) - { + if (psutil_sysctlbyname("hw.cpufrequency_min", &min, sizeof(min)) != 0) { min = 0; psutil_debug("sysctlbyname('hw.cpufrequency_min') failed (set to 0)"); } - if (psutil_sysctlbyname_fixed( - "hw.cpufrequency_max", &max, sizeof(max)) != 0) - { + if (psutil_sysctlbyname("hw.cpufrequency_max", &max, sizeof(max)) != 0) { max = 0; psutil_debug("sysctlbyname('hw.cpufrequency_max') failed (set to 0)"); } diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 4774858a5a..825c65af31 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -63,7 +63,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { mib[1] = HW_MEMSIZE; // This is also available as sysctlbyname("hw.memsize"). - if (psutil_sysctl_fixed(mib, 2, &total, sizeof(total)) != 0) + if (psutil_sysctl(mib, 2, &total, sizeof(total)) != 0) return NULL; // vm @@ -95,7 +95,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { mib[0] = CTL_VM; mib[1] = VM_SWAPUSAGE; - if (psutil_sysctl_fixed(mib, 2, &totals, sizeof(totals)) != 0) + if (psutil_sysctl(mib, 2, &totals, sizeof(totals)) != 0) return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); if (psutil_sys_vminfo(&vmstat) != 0) diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 8e879a8121..c36609df3f 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -533,7 +533,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { if (psutil_task_for_pid(pid, &task) != 0) return NULL; - if (psutil_sysctlbyname_fixed( + if (psutil_sysctlbyname( "sysctl.proc_cputype", &cpu_type, sizeof(cpu_type)) != 0) { return NULL; diff --git a/psutil/arch/osx/sys.c b/psutil/arch/osx/sys.c index 3846056fd6..3fafcac2b5 100644 --- a/psutil/arch/osx/sys.c +++ b/psutil/arch/osx/sys.c @@ -21,7 +21,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { struct timeval result; time_t boot_time = 0; - if (psutil_sysctl_fixed(mib, 2, &result, sizeof(result)) == -1) + if (psutil_sysctl(mib, 2, &result, sizeof(result)) == -1) return NULL; boot_time = result.tv_sec; return Py_BuildValue("d", (double)boot_time); diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 4f13b826bc..5562de76c0 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -13,9 +13,10 @@ #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) #include - int psutil_sysctl_fixed(int *mib, u_int miblen, void *buf, size_t buflen); + int psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen); int psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen); - int psutil_sysctlbyname_fixed(const char *name, void *buf, size_t buflen); + int psutil_sysctlbyname(const char *name, void *buf, size_t buflen); + int psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen); int psutil_sysctl_argmax(); #endif diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index f202ade1f8..a2d0c40955 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -12,12 +12,12 @@ #include "../../arch/all/init.h" -#define MAX_RETRIES 10 +static const int MAX_RETRIES = 10; // A thin wrapper on top of sysctl(). int -psutil_sysctl_fixed(int *mib, u_int miblen, void *buf, size_t buflen) { +psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen) { size_t len = buflen; if (sysctl(mib, miblen, buf, &len, NULL, 0) == -1) { @@ -94,10 +94,11 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { return -1; } + #if !defined(PSUTIL_OPENBSD) // A thin wrapper on top of sysctlbyname(). int -psutil_sysctlbyname_fixed(const char *name, void *buf, size_t buflen) { +psutil_sysctlbyname(const char *name, void *buf, size_t buflen) { size_t len = buflen; char errbuf[256]; @@ -122,6 +123,82 @@ psutil_sysctlbyname_fixed(const char *name, void *buf, size_t buflen) { return 0; } + + +// Allocate buffer for sysctlbyname with retry on ENOMEM or size mismatch. +// The caller is responsible for freeing the memory. +int +psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { + int ret; + int max_retries = MAX_RETRIES; + size_t needed = 0; + size_t len = 0; + char *buffer = NULL; + char errbuf[256]; + + // First query to determine required size. + ret = sysctlbyname(name, NULL, &needed, NULL, 0); + if (ret == -1) { + snprintf(errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 1/3", name); + psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + return -1; + } + + while (max_retries-- > 0) { + // Zero-initialize buffer to prevent uninitialized bytes. + buffer = calloc(1, needed); + if (buffer == NULL) { + PyErr_NoMemory(); + return -1; + } + + len = needed; + ret = sysctlbyname(name, buffer, &len, NULL, 0); + if (ret == 0) { + // Success: return buffer and actual length. + *buf = buffer; + *buflen = len; + return 0; + } + + // Handle buffer too small. Re-query and retry. + if (errno == ENOMEM) { + free(buffer); + buffer = NULL; + + if (sysctlbyname(name, NULL, &needed, NULL, 0) == -1) { + snprintf( + errbuf, + sizeof(errbuf), + "sysctlbyname('%s') malloc 2/3", + name + ); + psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + return -1; + } + + psutil_debug("psutil_sysctlbyname_malloc() retry"); + continue; + } + + // Other errors: clean up and give up. + free(buffer); + snprintf( + errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 3/3", name + ); + psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + return -1; + } + + snprintf( + errbuf, + sizeof(errbuf), + "sysctlbyname('%s') buffer allocation retry limit exceeded", + name + ); + PyErr_SetString(PyExc_RuntimeError, errbuf); + return -1; +} #endif // !PSUTIL_OPENBSD @@ -131,7 +208,7 @@ psutil_sysctl_argmax() { int argmax; int mib[2] = {CTL_KERN, KERN_ARGMAX}; - if (psutil_sysctl_fixed(mib, 2, &argmax, sizeof(argmax)) != 0) { + if (psutil_sysctl(mib, 2, &argmax, sizeof(argmax)) != 0) { PyErr_Clear(); psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); return 0; From b66f11dec155a0a3ac1843f4f02c6d7a5ed2647b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Aug 2025 17:35:13 +0200 Subject: [PATCH 1330/1714] define PSUTIL_HAS_* constants to check for features availability --- .github/workflows/build.yml | 104 ++++++++++++++++++------------------ HISTORY.rst | 2 +- MANIFEST.in | 3 -- psutil/_psutil_linux.c | 4 +- psutil/arch/bsd/init.h | 4 +- psutil/arch/bsd/proc.c | 4 +- psutil/arch/linux/init.h | 4 +- psutil/arch/linux/proc.c | 13 +++-- psutil/arch/osx/cpu.c | 4 +- psutil/arch/posix/init.h | 34 +++++++++--- psutil/arch/posix/net.c | 8 ++- psutil/arch/posix/net.h | 15 ------ psutil/arch/posix/proc.h | 11 ---- psutil/arch/posix/sysctl.c | 44 +++++++-------- psutil/arch/posix/users.c | 6 +-- psutil/arch/posix/users.h | 11 ---- 16 files changed, 126 insertions(+), 145 deletions(-) delete mode 100644 psutil/arch/posix/net.h delete mode 100644 psutil/arch/posix/proc.h delete mode 100644 psutil/arch/posix/users.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72d42d3d74..24f80b0957 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,58 +68,58 @@ jobs: python setup.py sdist mv dist/psutil*.tar.gz wheelhouse/ - # # Test python 2.7 fallback installation message produced by setup.py - # py2-fallback: - # name: py2.7 setup.py check - # runs-on: ubuntu-24.04 - # timeout-minutes: 20 - # strategy: - # fail-fast: false - # steps: - # - uses: actions/checkout@v4 - # - uses: LizardByte/actions/actions/setup_python@master - # with: - # python-version: "2.7" - # - run: python scripts/internal/test_python2_setup_py.py + # Test python 2.7 fallback installation message produced by setup.py + py2-fallback: + name: py2.7 setup.py check + runs-on: ubuntu-24.04 + timeout-minutes: 20 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + - uses: LizardByte/actions/actions/setup_python@master + with: + python-version: "2.7" + - run: python scripts/internal/test_python2_setup_py.py - # # Run linters - # linters: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-python@v5 - # with: - # python-version: 3.x - # - name: "Run linters" - # run: | - # make lint-ci + # Run linters + linters: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: "Run linters" + run: | + make lint-ci - # # Produce wheels as artifacts. - # upload-wheels: - # needs: [tests] - # runs-on: ubuntu-latest - # steps: - # - uses: actions/upload-artifact/merge@v4 - # with: - # name: wheels - # pattern: wheels-* - # separate-directories: false - # delete-merged: true + # Produce wheels as artifacts. + upload-wheels: + needs: [tests] + runs-on: ubuntu-latest + steps: + - uses: actions/upload-artifact/merge@v4 + with: + name: wheels + pattern: wheels-* + separate-directories: false + delete-merged: true - # # Check sanity of .tar.gz + wheel files - # check-dist: - # needs: [upload-wheels] - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-python@v5 - # with: - # python-version: 3.x - # - uses: actions/download-artifact@v4 - # with: - # name: wheels - # path: wheelhouse - # - run: | - # python scripts/internal/print_hashes.py wheelhouse/ - # pipx run twine check --strict wheelhouse/* - # pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl + # Check sanity of .tar.gz + wheel files + check-dist: + needs: [upload-wheels] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - uses: actions/download-artifact@v4 + with: + name: wheels + path: wheelhouse + - run: | + python scripts/internal/print_hashes.py wheelhouse/ + pipx run twine check --strict wheelhouse/* + pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl diff --git a/HISTORY.rst b/HISTORY.rst index e0d3d76f9e..6a1c8fa92f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -621,7 +621,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - 1684_, [Linux]: `disk_io_counters()`_ may raise ``ValueError`` on systems not having ``/proc/diskstats``. - 1695_, [Linux]: could not compile on kernels <= 2.6.13 due to - ``PSUTIL_HAVE_IOPRIO`` not being defined. (patch by Anselm Kruis) + ``PSUTIL_HAS_IOPRIO`` not being defined. (patch by Anselm Kruis) 5.6.7 ===== diff --git a/MANIFEST.in b/MANIFEST.in index c33d589566..d63e5d6ec9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -93,12 +93,9 @@ include psutil/arch/osx/sys.c include psutil/arch/posix/init.c include psutil/arch/posix/init.h include psutil/arch/posix/net.c -include psutil/arch/posix/net.h include psutil/arch/posix/proc.c -include psutil/arch/posix/proc.h include psutil/arch/posix/sysctl.c include psutil/arch/posix/users.c -include psutil/arch/posix/users.h include psutil/arch/sunos/cpu.c include psutil/arch/sunos/cpu.h include psutil/arch/sunos/disk.c diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 01b8ce9b0e..1d9d974cd1 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -22,11 +22,11 @@ static PyMethodDef mod_methods[] = { // --- per-process functions -#ifdef PSUTIL_HAVE_IOPRIO +#ifdef PSUTIL_HAS_IOPRIO {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS}, {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS}, #endif -#ifdef PSUTIL_HAVE_CPU_AFFINITY +#ifdef PSUTIL_HAS_CPU_AFFINITY {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, #endif diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 8870e69ecc..714910a699 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -9,7 +9,9 @@ void convert_kvm_err(const char *syscall, char *errbuf); #if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) -struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); + #define PSUTIL_HAS_KINFO_GETFILE + + struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); #endif PyObject *psutil_boot_time(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index b9ee21c0a6..8e600df04f 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -36,7 +36,7 @@ // Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an // int as arg and returns an array with cnt struct kinfo_file. -#if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) +#ifdef PSUTIL_HAS_KINFO_GETFILE struct kinfo_file * kinfo_getfile(pid_t pid, int* cnt) { int mib[6]; @@ -68,7 +68,7 @@ kinfo_getfile(pid_t pid, int* cnt) { *cnt = (int)(len / sizeof(struct kinfo_file)); return kf; } -#endif // defined(PLATFORMS…) +#endif // PSUTIL_HAS_KINFO_GETFILE /* diff --git a/psutil/arch/linux/init.h b/psutil/arch/linux/init.h index e21193a77f..77c83c6b35 100644 --- a/psutil/arch/linux/init.h +++ b/psutil/arch/linux/init.h @@ -14,7 +14,7 @@ PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); // Linux >= 2.6.13 #if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) - #define PSUTIL_HAVE_IOPRIO + #define PSUTIL_HAS_IOPRIO PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); @@ -22,7 +22,7 @@ PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); // Should exist starting from CentOS 6 (year 2011). #ifdef CPU_ALLOC - #define PSUTIL_HAVE_CPU_AFFINITY + #define PSUTIL_HAS_CPU_AFFINITY PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index e33755743c..9024c81842 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -4,15 +4,14 @@ * found in the LICENSE file. */ +#include "../../arch/all/init.h" + #include #include #include #include -#include "../../arch/all/init.h" - - -#ifdef PSUTIL_HAVE_IOPRIO +#ifdef PSUTIL_HAS_IOPRIO enum { IOPRIO_WHO_PROCESS = 1, }; @@ -71,10 +70,10 @@ psutil_proc_ioprio_set(PyObject *self, PyObject *args) { return PyErr_SetFromErrno(PyExc_OSError); Py_RETURN_NONE; } -#endif // PSUTIL_HAVE_IOPRIO +#endif // PSUTIL_HAS_IOPRIO -#ifdef PSUTIL_HAVE_CPU_AFFINITY +#ifdef PSUTIL_HAS_CPU_AFFINITY // Return process CPU affinity as a Python list. PyObject * @@ -185,4 +184,4 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { Py_RETURN_NONE; } -#endif // PSUTIL_HAVE_CPU_AFFINITY +#endif // PSUTIL_HAS_CPU_AFFINITY diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 0536540279..c58892cf52 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -27,8 +27,8 @@ For reference, here's the git history with original implementations: #include #include #if defined(__arm64__) || defined(__aarch64__) -#include -#include + #include + #include #endif #include "../../arch/all/init.h" diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 5562de76c0..34687aac13 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -4,22 +4,44 @@ * found in the LICENSE file. */ -#include "net.h" -#include "proc.h" #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) - #include "users.h" + #define PSUTIL_HAS_POSIX_USERS + + PyObject *psutil_users(PyObject *self, PyObject *args); #endif #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) #include + #define PSUTIL_HAS_SYSCTL + int psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen); int psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen); - int psutil_sysctlbyname(const char *name, void *buf, size_t buflen); - int psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen); int psutil_sysctl_argmax(); + + #if !defined(PSUTIL_OPENBSD) + + #define PSUTIL_HAS_SYSCTLBYNAME + + int psutil_sysctlbyname(const char *name, void *buf, size_t buflen); + int psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen); + #endif #endif +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) + #define PSUTIL_HAS_NET_IF_DUPLEX_SPEED + + PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); +#endif + +int psutil_pid_exists(pid_t pid); long psutil_getpagesize(void); +void psutil_raise_for_pid(pid_t pid, char *msg); + PyObject *psutil_getpagesize_pywrapper(PyObject *self, PyObject *args); -PyObject *psutil_users(PyObject *self, PyObject *args); +PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); +PyObject *psutil_net_if_flags(PyObject *self, PyObject *args); +PyObject *psutil_net_if_is_running(PyObject *self, PyObject *args); +PyObject *psutil_net_if_mtu(PyObject *self, PyObject *args); +PyObject *psutil_posix_getpriority(PyObject *self, PyObject *args); +PyObject *psutil_posix_setpriority(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index c0b67214d8..abee189678 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -499,10 +499,8 @@ psutil_net_if_is_running(PyObject *self, PyObject *args) { -/* - * net_if_stats() macOS/BSD implementation. - */ -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +// net_if_stats() macOS/BSD implementation. +#ifdef PSUTIL_HAS_NET_IF_DUPLEX_SPEED int psutil_get_nic_speed(int ifm_active) { // Determine NIC speed. Taken from: @@ -685,4 +683,4 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { close(sock); return Py_BuildValue("[ii]", duplex, speed); } -#endif // net_if_stats() macOS/BSD implementation +#endif // PSUTIL_HAS_NET_IF_DUPLEX_SPEED diff --git a/psutil/arch/posix/net.h b/psutil/arch/posix/net.h deleted file mode 100644 index d65ecc3575..0000000000 --- a/psutil/arch/posix/net.h +++ /dev/null @@ -1,15 +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. - */ - -#include - -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) -PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); -#endif -PyObject *psutil_net_if_addrs(PyObject* self, PyObject* args); -PyObject *psutil_net_if_flags(PyObject *self, PyObject *args); -PyObject *psutil_net_if_is_running(PyObject *self, PyObject *args); -PyObject *psutil_net_if_mtu(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/proc.h b/psutil/arch/posix/proc.h deleted file mode 100644 index b70f24aabc..0000000000 --- a/psutil/arch/posix/proc.h +++ /dev/null @@ -1,11 +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. - */ - -int psutil_pid_exists(pid_t pid); -void psutil_raise_for_pid(pid_t pid, char *msg); - -PyObject *psutil_posix_getpriority(PyObject *self, PyObject *args); -PyObject *psutil_posix_setpriority(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index a2d0c40955..c4de3b3524 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -4,13 +4,13 @@ * found in the LICENSE file. */ -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#include "../../arch/all/init.h" + +#ifdef PSUTIL_HAS_SYSCTL #include #include #include -#include "../../arch/all/init.h" - static const int MAX_RETRIES = 10; @@ -95,7 +95,23 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { } -#if !defined(PSUTIL_OPENBSD) +// Get the maximum process arguments size. +int +psutil_sysctl_argmax() { + int argmax; + int mib[2] = {CTL_KERN, KERN_ARGMAX}; + + if (psutil_sysctl(mib, 2, &argmax, sizeof(argmax)) != 0) { + PyErr_Clear(); + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); + return 0; + } + + return argmax; +} + + +#ifdef PSUTIL_HAS_SYSCTLBYNAME // A thin wrapper on top of sysctlbyname(). int psutil_sysctlbyname(const char *name, void *buf, size_t buflen) { @@ -199,21 +215,5 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { PyErr_SetString(PyExc_RuntimeError, errbuf); return -1; } -#endif // !PSUTIL_OPENBSD - - -// Get the maximum process arguments size. -int -psutil_sysctl_argmax() { - int argmax; - int mib[2] = {CTL_KERN, KERN_ARGMAX}; - - if (psutil_sysctl(mib, 2, &argmax, sizeof(argmax)) != 0) { - PyErr_Clear(); - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); - return 0; - } - - return argmax; -} -#endif // defined(PLATFORMS…) +#endif // PSUTIL_HAS_SYSCTLBYNAME +#endif // PSUTIL_HAS_SYSCTL diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c index 706ee57032..5406ac3e47 100644 --- a/psutil/arch/posix/users.c +++ b/psutil/arch/posix/users.c @@ -4,14 +4,14 @@ * found in the LICENSE file. */ -#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) +#include "../../arch/all/init.h" +#ifdef PSUTIL_HAS_POSIX_USERS #include #include #include -#include "../../arch/all/init.h" static void @@ -90,4 +90,4 @@ psutil_users(PyObject *self, PyObject *args) { Py_DECREF(py_retlist); return NULL; } -#endif // !defined(PLATFORMS…) +#endif // PSUTIL_HAS_POSIX_USERS diff --git a/psutil/arch/posix/users.h b/psutil/arch/posix/users.h deleted file mode 100644 index 0f373d0639..0000000000 --- a/psutil/arch/posix/users.h +++ /dev/null @@ -1,11 +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. - */ - -#include - -#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) -PyObject *psutil_users(PyObject* self, PyObject* args); -#endif From 80f20eebf8784b2230d952e21902525f64f409bc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Aug 2025 18:09:03 +0200 Subject: [PATCH 1331/1714] osx: preemptively check whether cpu_freq() is available on ARM64 to avoid sporadic CI failures --- psutil/_psutil_osx.c | 1 + psutil/arch/osx/cpu.c | 46 +++++++++++++++++++++++++++++++++++++++++- psutil/arch/osx/init.h | 1 + 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index e1bb372b71..c389e3a599 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -40,6 +40,7 @@ static PyMethodDef mod_methods[] = { {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS}, {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, {"disk_usage_used", psutil_disk_usage_used, METH_VARARGS}, + {"has_cpu_freq", psutil_has_cpu_freq, METH_VARARGS}, {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"pids", psutil_pids, METH_VARARGS}, diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index c58892cf52..8c79487fcf 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -136,7 +136,46 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { ); } + #if defined(__arm64__) || defined(__aarch64__) +// Needed because on GitHub CI sometimes (but not all the times) +// "AppleARMIODevice" is not available. +PyObject * +psutil_has_cpu_freq(PyObject *self, PyObject *args) { + kern_return_t status; + io_iterator_t iter = 0; + io_registry_entry_t entry = 0; + CFDictionaryRef matching; + io_name_t name; + int found = 0; + + matching = IOServiceMatching("AppleARMIODevice"); + if (matching == NULL) + Py_RETURN_FALSE; + + status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); + if (status != KERN_SUCCESS) + Py_RETURN_FALSE; + + while ((entry = IOIteratorNext(iter)) != 0) { + status = IORegistryEntryGetName(entry, name); + if (status == KERN_SUCCESS && strcmp(name, "pmgr") == 0) { + found = 1; + IOObjectRelease(entry); + break; + } + IOObjectRelease(entry); + } + + IOObjectRelease(iter); + + if (found) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + + PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { kern_return_t status; @@ -255,7 +294,12 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { IOObjectRelease(entry); return NULL; } -#else +#else // not ARM64 / ARCH64 +PyObject * +psutil_has_cpu_freq(PyObject *self, PyObject *args) { + Py_RETURN_TRUE; +} + PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { unsigned int curr; diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 4000bcd1fa..27acbcaf43 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -20,6 +20,7 @@ PyObject *psutil_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); +PyObject *psutil_has_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_pids(PyObject *self, PyObject *args); From 0282c5603dbb01189f5c2d5147ec5249421ab124 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Aug 2025 18:12:49 +0200 Subject: [PATCH 1332/1714] refact: provide psutil_find_pmgr_entry() helper static fun --- psutil/arch/osx/cpu.c | 96 ++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 61 deletions(-) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 8c79487fcf..af50d61abc 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -138,10 +138,15 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { #if defined(__arm64__) || defined(__aarch64__) + +// Helper to locate the 'pmgr' entry in AppleARMIODevice. Returns 0 on +// failure, nonzero on success, and stores the found entry in +// *out_entry. Caller is responsible for IOObjectRelease(*out_entry). // Needed because on GitHub CI sometimes (but not all the times) // "AppleARMIODevice" is not available. -PyObject * -psutil_has_cpu_freq(PyObject *self, PyObject *args) { +static int +psutil_find_pmgr_entry(io_registry_entry_t *out_entry) { + kern_return_t status; io_iterator_t iter = 0; io_registry_entry_t entry = 0; @@ -149,19 +154,20 @@ psutil_has_cpu_freq(PyObject *self, PyObject *args) { io_name_t name; int found = 0; + *out_entry = 0; + matching = IOServiceMatching("AppleARMIODevice"); if (matching == NULL) - Py_RETURN_FALSE; + return 0; status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); if (status != KERN_SUCCESS) - Py_RETURN_FALSE; + return 0; while ((entry = IOIteratorNext(iter)) != 0) { status = IORegistryEntryGetName(entry, name); if (status == KERN_SUCCESS && strcmp(name, "pmgr") == 0) { found = 1; - IOObjectRelease(entry); break; } IOObjectRelease(entry); @@ -169,74 +175,48 @@ psutil_has_cpu_freq(PyObject *self, PyObject *args) { IOObjectRelease(iter); - if (found) + if (found) { + *out_entry = entry; // caller must release + return 1; + } + + return 0; +} + +// Python wrapper: return True/False. +PyObject * +psutil_has_cpu_freq(PyObject *self, PyObject *args) { + io_registry_entry_t entry = 0; + int ok = psutil_find_pmgr_entry(&entry); + if (entry != 0) + IOObjectRelease(entry); + if (ok) Py_RETURN_TRUE; - else - Py_RETURN_FALSE; + Py_RETURN_FALSE; } PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { - kern_return_t status; - io_iterator_t iter = 0; io_registry_entry_t entry = 0; CFTypeRef pCoreRef = NULL; CFTypeRef eCoreRef = NULL; - CFDictionaryRef matching; size_t pCoreLength; - io_name_t name; - - uint32_t pMin = 0; - uint32_t eMin = 0; - uint32_t min = 0; - uint32_t max = 0; - uint32_t curr = 0; + uint32_t pMin = 0, eMin = 0, min = 0, max = 0, curr = 0; - // Get matching service for Apple ARM I/O device. - matching = IOServiceMatching("AppleARMIODevice"); - if (matching == NULL) { + if (!psutil_find_pmgr_entry(&entry)) { return PyErr_Format( - PyExc_RuntimeError, - "IOServiceMatching failed: 'AppleARMIODevice' not found" - ); - } - - // IOServiceGetMatchingServices consumes matching; do NOT CFRelease it. - status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); - if (status != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, "IOServiceGetMatchingServices call failed" - ); - goto error; - } - - // Find the 'pmgr' entry. - while ((entry = IOIteratorNext(iter)) != 0) { - status = IORegistryEntryGetName(entry, name); - if (status == KERN_SUCCESS && strcmp(name, "pmgr") == 0) { - break; - } - IOObjectRelease(entry); - entry = 0; - } - - if (entry == 0) { - PyErr_Format( PyExc_RuntimeError, "'pmgr' entry was not found in AppleARMIODevice service" ); - goto error; } - // Get performance and efficiency core data. pCoreRef = IORegistryEntryCreateCFProperty( entry, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, 0 ); if (pCoreRef == NULL || CFGetTypeID(pCoreRef) != CFDataGetTypeID() || - CFDataGetLength(pCoreRef) < 8) - { + CFDataGetLength(pCoreRef) < 8) { PyErr_SetString( PyExc_RuntimeError, "'voltage-states5-sram' is missing or invalid" @@ -249,8 +229,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { ); if (eCoreRef == NULL || CFGetTypeID(eCoreRef) != CFDataGetTypeID() || - CFDataGetLength(eCoreRef) < 4) - { + CFDataGetLength(eCoreRef) < 4) { PyErr_SetString( PyExc_RuntimeError, "'voltage-states1-sram' is missing or invalid" @@ -258,7 +237,6 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { goto error; } - // Extract values safely. pCoreLength = CFDataGetLength(pCoreRef); CFDataGetBytes(pCoreRef, CFRangeMake(0, 4), (UInt8 *)&pMin); CFDataGetBytes(eCoreRef, CFRangeMake(0, 4), (UInt8 *)&eMin); @@ -273,8 +251,6 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { CFRelease(eCoreRef); if (entry) IOObjectRelease(entry); - if (iter) - IOObjectRelease(iter); return Py_BuildValue( "KKK", @@ -284,13 +260,11 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { ); error: - if (pCoreRef != NULL) + if (pCoreRef) CFRelease(pCoreRef); - if (eCoreRef != NULL) + if (eCoreRef) CFRelease(eCoreRef); - if (iter != 0) - IOObjectRelease(iter); - if (entry != 0) + if (entry) IOObjectRelease(entry); return NULL; } From c1d73193f848f906b640484b4c50302d8735a250 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Aug 2025 18:31:03 +0200 Subject: [PATCH 1333/1714] make ruff happy + actually check whether cpu_freq() is available on OSX --- psutil/_common.py | 6 +++--- psutil/_psosx.py | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 750d66a039..51c798b4c6 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -408,7 +408,7 @@ def wrapper(*args, **kwargs): except KeyError: try: ret = cache[key] = fun(*args, **kwargs) - except Exception as err: # noqa: BLE001 + except Exception as err: raise err from None return ret @@ -457,14 +457,14 @@ def wrapper(self): # case 2: we never entered oneshot() ctx try: return fun(self) - except Exception as err: # noqa: BLE001 + except Exception as err: raise err from 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 + except Exception as err: raise err from None try: self._cache[fun] = ret diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 18e19a8801..be51991099 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -170,14 +170,16 @@ def cpu_stats(): ) -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. - """ - curr, min_, max_ = cext.cpu_freq() - return [_common.scpufreq(curr, min_, max_)] +if cext.has_cpu_freq(): # not always available on ARM64 + + 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. + """ + curr, min_, max_ = cext.cpu_freq() + return [_common.scpufreq(curr, min_, max_)] # ===================================================================== From d2be6ba9a8f22d66fea9305a7682d7d64aac7b57 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Aug 2025 18:49:49 +0200 Subject: [PATCH 1334/1714] skip flaky test on osx / arch64 --- psutil/tests/test_contracts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index aa3a4fef3c..0e7aaca0b1 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -109,6 +109,7 @@ def test_win_service_iter(self): def test_win_service_get(self): assert hasattr(psutil, "win_service_get") == WINDOWS + @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") def test_cpu_freq(self): assert hasattr(psutil, "cpu_freq") == ( LINUX or MACOS or WINDOWS or FREEBSD or OPENBSD From 48516a929e23dff8a8d12c1a7a1f2f17e33a5691 Mon Sep 17 00:00:00 2001 From: "Isaac K. Ko" Date: Fri, 15 Aug 2025 01:54:17 +0900 Subject: [PATCH 1335/1714] Fix calculation of mem used metric for linux (#2612) In newer versions of procps, used memory is calculated as, used = total - avail --- psutil/_pslinux.py | 8 ++------ psutil/tests/test_linux.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 115cbb2f26..b5095d40b6 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -420,12 +420,6 @@ def virtual_memory(): except KeyError: slab = 0 - used = total - free - cached - buffers - if used < 0: - # May be symptomatic of running within a LCX container where such - # values will be dramatically distorted over those of the host. - used = total - free - # - starting from 4.4.0 we match free's "available" column. # Before 4.4.0 we calculated it as (free + buffers + cached) # which matched htop. @@ -456,6 +450,8 @@ def virtual_memory(): # 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L764 avail = free + used = total - avail + percent = usage_percent((total - avail), total, round_=1) # Warn about missing metrics which are set to 0. diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index d04df0b2c4..5e325e5564 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -241,10 +241,8 @@ def test_used(self): # memory. # https://gitlab.com/procps-ng/procps/commit/ # 2184e90d2e7cdb582f9a5b706b47015e56707e4d - if get_free_version_info() < (3, 3, 12): + if get_free_version_info() < (4, 0, 0): raise pytest.skip("free version too old") - if get_free_version_info() >= (4, 0, 0): - raise pytest.skip("free version too recent") cli_value = free_physmem().used psutil_value = psutil.virtual_memory().used assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM @@ -296,10 +294,8 @@ def test_used(self): # memory. # https://gitlab.com/procps-ng/procps/commit/ # 2184e90d2e7cdb582f9a5b706b47015e56707e4d - if get_free_version_info() < (3, 3, 12): + if get_free_version_info() < (4, 0, 0): raise pytest.skip("free version too old") - if get_free_version_info() >= (4, 0, 0): - raise pytest.skip("free version too recent") vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM From 9c7c46b413809814e477c2a255ca7017a7f43395 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Aug 2025 18:56:51 +0200 Subject: [PATCH 1336/1714] give CREDITS to @kyet for #2604 --- CREDITS | 4 ++++ HISTORY.rst | 4 +++- psutil/__init__.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CREDITS b/CREDITS index 49ee86c3db..ae6fbb3322 100644 --- a/CREDITS +++ b/CREDITS @@ -864,3 +864,7 @@ I: 2560 N: Ben Peddell W: https://github.com/klightspeed I: 2494 + +N Isaac K. Ko +W: https://github.com/kyet +I: 2604 diff --git a/HISTORY.rst b/HISTORY.rst index 6a1c8fa92f..b52e30f5ab 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.0.1 +7.1.0 ===== XXXX-XX-XX @@ -41,6 +41,8 @@ XXXX-XX-XX Stephan) - 2586_, [macOS], [CRITICAL]: fixed different places in C code which can trigger a segfault. +- 2604_, [Linux]: `virtual_memory()`_ "used" memory does not match recent + versions of ``free`` CLI utility. (patch by Isaac K. Ko) - 2605_, [Linux]: `psutil.sensors_battery()` reports a negative amount for seconds left. - 2607_, [Windows]: ``WindowsService.description()`` method may fail with diff --git a/psutil/__init__.py b/psutil/__init__.py index 2a47b57b78..c27b3c7921 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -205,7 +205,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.0.1" +__version__ = "7.1.0" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) From c06fde4cafa49bbdc1e886f2efae5ac34128f740 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Thu, 14 Aug 2025 19:52:32 +0200 Subject: [PATCH 1337/1714] [illumos] fix zombie process test with no ctime (#2594) --- psutil/__init__.py | 12 ++++++------ psutil/tests/__init__.py | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index c27b3c7921..23d47ebd27 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -433,12 +433,12 @@ 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__. + if OPENBSD or NETBSD or SUNOS: # pragma: no cover + # Zombie processes on Open/NetBSD/illumos/Solaris 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, ident1 = self._ident pid2, ident2 = other._ident if pid1 == pid2: diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 6465be8cad..7aa03330ad 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1089,10 +1089,12 @@ def assert_proc_gone(self, proc): def assert_proc_zombie(self, proc): # A zombie process should always be instantiable. clone = psutil.Process(proc.pid) - # Cloned zombie on Open/NetBSD has null creation time, see: + # Cloned zombie on Open/NetBSD/illumos/Solaris has null creation + # time, see: # https://github.com/giampaolo/psutil/issues/2287 + # https://github.com/giampaolo/psutil/issues/2593 assert proc == clone - if not (OPENBSD or NETBSD): + if not (OPENBSD or NETBSD or SUNOS): assert hash(proc) == hash(clone) # Its status always be querable. assert proc.status() == psutil.STATUS_ZOMBIE From 2cd73360d757fe03bed84d847648ff912ba6601b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Aug 2025 14:00:56 +0200 Subject: [PATCH 1338/1714] netbsd / openbsd: use psutil_sysctl_malloc() helper to emulate kinfo_getfile() Signed-off-by: Giampaolo Rodola --- psutil/arch/bsd/init.h | 2 +- psutil/arch/bsd/proc.c | 25 +++++++------------------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 714910a699..7220ccc44f 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -9,7 +9,7 @@ void convert_kvm_err(const char *syscall, char *errbuf); #if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) - #define PSUTIL_HAS_KINFO_GETFILE + #define PSUTIL_HASNT_KINFO_GETFILE struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); #endif diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 8e600df04f..56427ae7cc 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -36,12 +36,13 @@ // Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an // int as arg and returns an array with cnt struct kinfo_file. -#ifdef PSUTIL_HAS_KINFO_GETFILE +#ifdef PSUTIL_HASNT_KINFO_GETFILE struct kinfo_file * -kinfo_getfile(pid_t pid, int* cnt) { +kinfo_getfile(pid_t pid, int *cnt) { int mib[6]; size_t len; - struct kinfo_file* kf; + struct kinfo_file *kf; + mib[0] = CTL_KERN; mib[1] = KERN_FILE; mib[2] = KERN_FILE_BYPID; @@ -49,26 +50,14 @@ kinfo_getfile(pid_t pid, int* cnt) { mib[4] = sizeof(struct kinfo_file); mib[5] = 0; - /* get the size of what would be returned */ - if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (1/2)"); - return NULL; - } - if ((kf = malloc(len)) == NULL) { - PyErr_NoMemory(); + kf = (struct kinfo_file *)psutil_sysctl_malloc(mib, 6, &len); + if (kf == NULL) return NULL; - } - mib[5] = (int)(len / sizeof(struct kinfo_file)); - if (sysctl(mib, 6, kf, &len, NULL, 0) < 0) { - free(kf); - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_file) (2/2)"); - return NULL; - } *cnt = (int)(len / sizeof(struct kinfo_file)); return kf; } -#endif // PSUTIL_HAS_KINFO_GETFILE +#endif // PSUTIL_HASNT_KINFO_GETFILE /* From 357285c5ff31ef8bf1b9c347904062e8d7e818d0 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 22 Aug 2025 03:12:00 -0400 Subject: [PATCH 1339/1714] Fix alpine python dev package name (#2621) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 303cdb3101..20decd219c 100755 --- a/setup.py +++ b/setup.py @@ -207,7 +207,7 @@ def get_sysdeps(): return "sudo pacman -S gcc python" elif shutil.which("apk"): return "sudo apk add gcc {}3-dev musl-dev linux-headers".format( - *pyimpl + pyimpl ) elif MACOS: return "xcode-select --install" From fb39025bee43a1750a4fd5d08342fc42438c224e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 24 Aug 2025 15:54:50 +0200 Subject: [PATCH 1340/1714] BSD: port sysctl() to new sysctl() helper(s) (#2623) --- psutil/arch/bsd/net.c | 49 ++++++++++++-------------------- psutil/arch/bsd/proc.c | 6 ++-- psutil/arch/bsd/sys.c | 5 ++-- psutil/arch/freebsd/proc.c | 32 ++++----------------- psutil/arch/netbsd/proc.c | 54 ++++++++++++------------------------ psutil/tests/test_unicode.py | 5 ++++ 6 files changed, 50 insertions(+), 101 deletions(-) diff --git a/psutil/arch/bsd/net.c b/psutil/arch/bsd/net.c index deb905fc66..5f44d8e7b7 100644 --- a/psutil/arch/bsd/net.c +++ b/psutil/arch/bsd/net.c @@ -33,25 +33,12 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { mib[4] = NET_RT_IFLIST; // operation mib[5] = 0; - if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_malloc(mib, 6, &buf, &len) != 0) goto error; - } - - buf = malloc(len); - if (buf == NULL) { - PyErr_NoMemory(); - goto error; - } - - if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } lim = buf + len; - for (next = buf; next < lim; ) { + for (next = buf; next < lim;) { py_ifc_info = NULL; ifm = (struct if_msghdr *)next; next += ifm->ifm_msglen; @@ -62,7 +49,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { char ifc_name[32]; strncpy(ifc_name, sdl->sdl_data, sdl->sdl_nlen); - ifc_name[sdl->sdl_nlen] = 0; + ifc_name[sdl->sdl_nlen] = '\0'; + // XXX: ignore usbus interfaces: // http://lists.freebsd.org/pipermail/freebsd-current/ // 2011-October/028752.html @@ -70,29 +58,27 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { if (strncmp(ifc_name, "usbus", 5) == 0) continue; - py_ifc_info = Py_BuildValue("(kkkkkkki)", - if2m->ifm_data.ifi_obytes, - if2m->ifm_data.ifi_ibytes, - if2m->ifm_data.ifi_opackets, - if2m->ifm_data.ifi_ipackets, - if2m->ifm_data.ifi_ierrors, - if2m->ifm_data.ifi_oerrors, - if2m->ifm_data.ifi_iqdrops, + py_ifc_info = Py_BuildValue( + "(kkkkkkki)", + if2m->ifm_data.ifi_obytes, + if2m->ifm_data.ifi_ibytes, + if2m->ifm_data.ifi_opackets, + if2m->ifm_data.ifi_ipackets, + if2m->ifm_data.ifi_ierrors, + if2m->ifm_data.ifi_oerrors, + if2m->ifm_data.ifi_iqdrops, #ifdef _IFI_OQDROPS - if2m->ifm_data.ifi_oqdrops + if2m->ifm_data.ifi_oqdrops #else - 0 + 0 #endif ); if (!py_ifc_info) goto error; - if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info)) + if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info) != 0) goto error; Py_CLEAR(py_ifc_info); } - else { - continue; - } } free(buf); @@ -101,7 +87,6 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { error: Py_XDECREF(py_ifc_info); Py_DECREF(py_retdict); - if (buf != NULL) - free(buf); + free(buf); return NULL; } diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 56427ae7cc..32354dfd55 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -41,7 +41,7 @@ struct kinfo_file * kinfo_getfile(pid_t pid, int *cnt) { int mib[6]; size_t len; - struct kinfo_file *kf; + struct kinfo_file *kf = NULL; mib[0] = CTL_KERN; mib[1] = KERN_FILE; @@ -50,9 +50,9 @@ kinfo_getfile(pid_t pid, int *cnt) { mib[4] = sizeof(struct kinfo_file); mib[5] = 0; - kf = (struct kinfo_file *)psutil_sysctl_malloc(mib, 6, &len); - if (kf == NULL) + if (psutil_sysctl_malloc(mib, 6, (char **)&kf, &len) != 0) { return NULL; + } *cnt = (int)(len / sizeof(struct kinfo_file)); return kf; diff --git a/psutil/arch/bsd/sys.c b/psutil/arch/bsd/sys.c index 9237a6ea03..02c04a70c5 100644 --- a/psutil/arch/bsd/sys.c +++ b/psutil/arch/bsd/sys.c @@ -24,9 +24,8 @@ psutil_boot_time(PyObject *self, PyObject *args) { // fetch sysctl "kern.boottime" static int request[2] = { CTL_KERN, KERN_BOOTTIME }; struct timeval boottime; - size_t len = sizeof(boottime); - if (sysctl(request, 2, &boottime, &len, NULL, 0) == -1) - return PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl(request, 2, &boottime, sizeof(boottime)) != 0) + return NULL; return Py_BuildValue("d", (double)boottime.tv_sec); } diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index c3e18df403..d973728067 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -218,9 +218,8 @@ psutil_proc_threads(PyObject *self, PyObject *args) { int mib[4]; struct kinfo_proc *kip = NULL; struct kinfo_proc *kipp = NULL; - int error; unsigned int i; - size_t size; + size_t size = 0; PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; @@ -235,34 +234,16 @@ psutil_proc_threads(PyObject *self, PyObject *args) { mib[2] = KERN_PROC_PID | KERN_PROC_INC_THREAD; mib[3] = pid; - size = 0; - error = sysctl(mib, 4, NULL, &size, NULL, 0); - if (error == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); - goto error; - } - if (size == 0) { - NoSuchProcess("sysctl (size = 0)"); - goto error; - } - - kip = malloc(size); - if (kip == NULL) { - PyErr_NoMemory(); + if (psutil_sysctl_malloc(mib, 4, (char **)&kip, &size) != 0) goto error; - } - error = sysctl(mib, 4, kip, &size, NULL, 0); - if (error == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_INC_THREAD)"); - goto error; - } + // subtle check: size == 0 means no such process if (size == 0) { NoSuchProcess("sysctl (size = 0)"); goto error; } - for (i = 0; i < size / sizeof(*kipp); i++) { + for (i = 0; i < size / sizeof(*kip); i++) { kipp = &kip[i]; py_tuple = Py_BuildValue("Idd", kipp->ki_tid, @@ -274,18 +255,17 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; Py_DECREF(py_tuple); } + free(kip); return py_retlist; error: Py_XDECREF(py_tuple); Py_DECREF(py_retlist); - if (kip != NULL) - free(kip); + free(kip); return NULL; } - PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { pid_t pid; diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 6b650482bf..ebc25581e2 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -187,6 +187,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { mib[3] = sizeof(struct kinfo_lwp); mib[4] = 0; + // first query size st = sysctl(mib, 5, NULL, &size, NULL, 0); if (st == -1) { PyErr_SetFromErrno(PyExc_OSError); @@ -197,16 +198,11 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } + // set slot count for NetBSD KERN_LWP mib[4] = size / sizeof(size_t); - kl = malloc(size); - if (kl == NULL) { - PyErr_NoMemory(); - goto error; - } - st = sysctl(mib, 5, kl, &size, NULL, 0); - if (st == -1) { - PyErr_SetFromErrno(PyExc_OSError); + if (psutil_sysctl_malloc( + mib, __arraycount(mib), (char **)&kl, &size) != 0) { goto error; } if (size == 0) { @@ -216,20 +212,22 @@ psutil_proc_threads(PyObject *self, PyObject *args) { nlwps = (int)(size / sizeof(struct kinfo_lwp)); for (i = 0; i < nlwps; i++) { - if ((&kl[i])->l_stat == LSIDL || (&kl[i])->l_stat == LSZOMB) + if (kl[i].l_stat == LSIDL || kl[i].l_stat == LSZOMB) continue; - // XXX: we return 2 "user" times because the struct does not provide - // any "system" time. - py_tuple = Py_BuildValue("idd", - (&kl[i])->l_lid, - PSUTIL_KPT2DOUBLE((&kl[i])->l_rtime), - PSUTIL_KPT2DOUBLE((&kl[i])->l_rtime)); + // XXX: return 2 "user" times, no "system" time available + py_tuple = Py_BuildValue( + "idd", + kl[i].l_lid, + PSUTIL_KPT2DOUBLE(kl[i].l_rtime), + PSUTIL_KPT2DOUBLE(kl[i].l_rtime) + ); if (py_tuple == NULL) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; Py_DECREF(py_tuple); } + free(kl); return py_retlist; @@ -299,7 +297,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; int mib[4]; int st; - int attempt; + int attempt = 0; int max_attempts = 50; size_t len = 0; size_t pos = 0; @@ -317,23 +315,9 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { mib[2] = pid; mib[3] = KERN_PROC_ARGV; - st = sysctl(mib, __arraycount(mib), NULL, &len, NULL, 0); - if (st == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctl(KERN_PROC_ARGV) get size" - ); - goto error; - } - - procargs = (char *)malloc(len); - if (procargs == NULL) { - PyErr_NoMemory(); - goto error; - } - while (1) { - st = sysctl(mib, __arraycount(mib), procargs, &len, NULL, 0); - if (st == -1) { + if (psutil_sysctl_malloc( + mib, __arraycount(mib), (char **)&procargs, &len) != 0) { if (errno == EBUSY) { // Usually happens with TestProcess.test_long_cmdline. See: // https://github.com/giampaolo/psutil/issues/2250 @@ -346,14 +330,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { psutil_debug( "proc %zu cmdline(): return [] due to EBUSY", pid ); - free(procargs); return py_retlist; } } - else { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_ARGV)"); - goto error; - } + goto error; } break; } diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index f2c0ecacd9..8533c04e1a 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -74,6 +74,8 @@ import psutil from psutil import BSD from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD from psutil import POSIX from psutil import WINDOWS from psutil.tests import ASCII_FS @@ -212,6 +214,9 @@ def test_proc_cwd(self): assert cwd == dname @pytest.mark.skipif(PYPY and WINDOWS, reason="fails on PYPY + WINDOWS") + @pytest.mark.skipif( + NETBSD or OPENBSD, reason="broken on NETBSD or OPENBSD" + ) def test_proc_open_files(self): p = psutil.Process() start = set(p.open_files()) From 13b711fc3ffa384d481e03881e46ae48009c4eeb Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 2 Sep 2025 11:11:03 +0300 Subject: [PATCH 1341/1714] Lock around uses of getutent/setutent/endutent (#2615) --- psutil/_psutil_aix.c | 2 ++ psutil/arch/all/init.c | 4 ++++ psutil/arch/all/init.h | 8 ++++++++ psutil/arch/posix/users.c | 3 +++ psutil/arch/sunos/sys.c | 2 ++ 5 files changed, 19 insertions(+) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 4727c2d966..ca05615551 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -653,6 +653,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { float boot_time = 0.0; struct utmpx *ut; + UTXENT_MUTEX_LOCK(); setutxent(); while (NULL != (ut = getutxent())) { if (ut->ut_type == BOOT_TIME) { @@ -661,6 +662,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { } } endutxent(); + UTXENT_MUTEX_UNLOCK(); if (boot_time == 0.0) { /* could not find BOOT_TIME in getutxent loop */ PyErr_SetString(PyExc_RuntimeError, "can't determine boot time"); diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c index 891e017c7f..a4a67d6eca 100644 --- a/psutil/arch/all/init.c +++ b/psutil/arch/all/init.c @@ -16,6 +16,10 @@ int PSUTIL_DEBUG = 0; int PSUTIL_CONN_NONE = 128; +#ifdef Py_GIL_DISABLED +PyMutex utxent_lock = {0}; +#endif + // Set OSError(errno=ESRCH, strerror="No such process (originated from") // Python exception. diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 6483eec2da..d8904e93b5 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -37,6 +37,14 @@ extern int PSUTIL_DEBUG; // a signaler for connections without an actual status extern int PSUTIL_CONN_NONE; +#ifdef Py_GIL_DISABLED + extern PyMutex utxent_lock; + #define UTXENT_MUTEX_LOCK() PyMutex_Lock(&utxent_lock) + #define UTXENT_MUTEX_UNLOCK() PyMutex_Unlock(&utxent_lock) +#else + #define UTXENT_MUTEX_LOCK() + #define UTXENT_MUTEX_UNLOCK() +#endif // Print a debug message on stderr. #define psutil_debug(...) do { \ diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c index 5406ac3e47..bbaa60e911 100644 --- a/psutil/arch/posix/users.c +++ b/psutil/arch/posix/users.c @@ -38,6 +38,7 @@ psutil_users(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; + UTXENT_MUTEX_LOCK(); setup(); while ((ut = getutxent()) != NULL) { @@ -79,10 +80,12 @@ psutil_users(PyObject *self, PyObject *args) { } teardown(); + UTXENT_MUTEX_UNLOCK(); return py_retlist; error: teardown(); + UTXENT_MUTEX_UNLOCK(); Py_XDECREF(py_username); Py_XDECREF(py_tty); Py_XDECREF(py_hostname); diff --git a/psutil/arch/sunos/sys.c b/psutil/arch/sunos/sys.c index 85f5a8fa9f..d9f3ae4bfb 100644 --- a/psutil/arch/sunos/sys.c +++ b/psutil/arch/sunos/sys.c @@ -16,6 +16,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { float boot_time = 0.0; struct utmpx *ut; + UTXENT_MUTEX_LOCK(); setutxent(); while (NULL != (ut = getutxent())) { if (ut->ut_type == BOOT_TIME) { @@ -24,6 +25,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { } } endutxent(); + UTXENT_MUTEX_UNLOCK(); if (fabs(boot_time) < 0.000001) { /* could not find BOOT_TIME in getutxent loop */ PyErr_SetString(PyExc_RuntimeError, "can't determine boot time"); From 8773698006d387db90b3982d78fd4ff6b2ebe71c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 2 Sep 2025 21:57:31 +0200 Subject: [PATCH 1342/1714] small refact around UTXENT_MUTEX_LOCK --- psutil/_common.py | 6 +++--- psutil/arch/posix/users.c | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 51c798b4c6..750d66a039 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -408,7 +408,7 @@ def wrapper(*args, **kwargs): except KeyError: try: ret = cache[key] = fun(*args, **kwargs) - except Exception as err: + except Exception as err: # noqa: BLE001 raise err from None return ret @@ -457,14 +457,14 @@ def wrapper(self): # case 2: we never entered oneshot() ctx try: return fun(self) - except Exception as err: + except Exception as err: # noqa: BLE001 raise err from 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: + except Exception as err: # noqa: BLE001 raise err from None try: self._cache[fun] = ret diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c index bbaa60e911..04e7a8481e 100644 --- a/psutil/arch/posix/users.c +++ b/psutil/arch/posix/users.c @@ -16,6 +16,7 @@ static void setup() { + UTXENT_MUTEX_LOCK(); setutxent(); } @@ -23,6 +24,7 @@ setup() { static void teardown() { endutxent(); + UTXENT_MUTEX_UNLOCK(); } @@ -38,7 +40,6 @@ psutil_users(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - UTXENT_MUTEX_LOCK(); setup(); while ((ut = getutxent()) != NULL) { @@ -80,12 +81,10 @@ psutil_users(PyObject *self, PyObject *args) { } teardown(); - UTXENT_MUTEX_UNLOCK(); return py_retlist; error: teardown(); - UTXENT_MUTEX_UNLOCK(); Py_XDECREF(py_username); Py_XDECREF(py_tty); Py_XDECREF(py_hostname); From ef72dcd076f1cec94ba50ec30b5dbebb272845d0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 3 Sep 2025 00:28:08 +0200 Subject: [PATCH 1343/1714] revert #2590 (Call GetExtended[Tcp|Udp]Table twice under free-threaded build) it causes a segfault --- psutil/arch/windows/socks.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 8267c6e352..1d35de0423 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -17,6 +17,9 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) #define STATUS_UNSUCCESSFUL 0xC0000001 +ULONG g_TcpTableSize = 0; +ULONG g_UdpTableSize = 0; + // Note about GetExtended[Tcp|Udp]Table syscalls: due to other processes // being active on the machine, it's possible that the size of the table @@ -34,9 +37,13 @@ static PVOID __GetExtendedTcpTable(ULONG family) { ULONG size; TCP_TABLE_CLASS class = TCP_TABLE_OWNER_PID_ALL; - GetExtendedTcpTable(NULL, &size, FALSE, family, class, 0); - // reserve 25% more space to be sure - size = size + (size / 2 / 2); + size = g_TcpTableSize; + if (size == 0) { + GetExtendedTcpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space + size = size + (size / 2 / 2); + g_TcpTableSize = size; + } table = malloc(size); if (table == NULL) { @@ -51,6 +58,7 @@ static PVOID __GetExtendedTcpTable(ULONG family) { free(table); if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { psutil_debug("GetExtendedTcpTable: retry with different bufsize"); + g_TcpTableSize = 0; return __GetExtendedTcpTable(family); } @@ -65,9 +73,13 @@ static PVOID __GetExtendedUdpTable(ULONG family) { ULONG size; UDP_TABLE_CLASS class = UDP_TABLE_OWNER_PID; - GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); - // reserve 25% more space to be sure - size = size + (size / 2 / 2); + size = g_UdpTableSize; + if (size == 0) { + GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space + size = size + (size / 2 / 2); + g_UdpTableSize = size; + } table = malloc(size); if (table == NULL) { @@ -82,6 +94,7 @@ static PVOID __GetExtendedUdpTable(ULONG family) { free(table); if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { psutil_debug("GetExtendedUdpTable: retry with different bufsize"); + g_UdpTableSize = 0; return __GetExtendedUdpTable(family); } From dcbfb81e863fa2b8688ccd8da27b09b539404688 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 3 Sep 2025 01:35:35 +0200 Subject: [PATCH 1344/1714] [Windows] fix unicode issues around service APIs (#2626) --- HISTORY.rst | 2 + psutil/_common.py | 6 +- psutil/arch/windows/services.c | 199 ++++++++++++++++++++++----------- 3 files changed, 140 insertions(+), 67 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b52e30f5ab..273bad90ff 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,8 @@ XXXX-XX-XX not properly handle UNC paths. Paths such as ``\\??\\C:\\Windows\\Temp`` and ``'\\Device\\HarddiskVolume1\\Windows\\Temp'`` are now converted to ``C:\\Windows\\Temp``. (patch by Ben Peddell) +- 2506_, [Windows]: Windows service APIs had issues with unicode services using + special characters in their name. - 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due to a race condition. - 2526_, [Linux]: `Process.create_time()`_, which is used to univocally diff --git a/psutil/_common.py b/psutil/_common.py index 750d66a039..51c798b4c6 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -408,7 +408,7 @@ def wrapper(*args, **kwargs): except KeyError: try: ret = cache[key] = fun(*args, **kwargs) - except Exception as err: # noqa: BLE001 + except Exception as err: raise err from None return ret @@ -457,14 +457,14 @@ def wrapper(self): # case 2: we never entered oneshot() ctx try: return fun(self) - except Exception as err: # noqa: BLE001 + except Exception as err: raise err from 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 + except Exception as err: raise err from None try: self._cache[fun] = ret diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 141bc1aaed..8ea99a8daf 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -15,28 +15,69 @@ // utils // ================================================================== + SC_HANDLE -psutil_get_service_handler(char *service_name, DWORD scm_access, DWORD access) +psutil_get_service_handler( + const wchar_t *service_name, + DWORD scm_access, + DWORD access) { SC_HANDLE sc = NULL; SC_HANDLE hService = NULL; - sc = OpenSCManager(NULL, NULL, scm_access); + sc = OpenSCManagerW(NULL, NULL, scm_access); if (sc == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenSCManagerW"); return NULL; } - hService = OpenService(sc, service_name, access); + + hService = OpenServiceW(sc, service_name, access); if (hService == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenService"); + psutil_PyErr_SetFromOSErrnoWithSyscall("OpenServiceW"); CloseServiceHandle(sc); return NULL; } + CloseServiceHandle(sc); return hService; } +// helper: parse args, convert to wchar, and open service +// returns NULL on error. On success, fills *service_name_out. +static SC_HANDLE +psutil_get_service_from_args( + PyObject *args, + DWORD scm_access, + DWORD access, + wchar_t **service_name_out) +{ + PyObject *py_service_name = NULL; + wchar_t *service_name = NULL; + Py_ssize_t wlen; + SC_HANDLE hService = NULL; + + if (!PyArg_ParseTuple(args, "U", &py_service_name)) { + return NULL; + } + + service_name = PyUnicode_AsWideCharString(py_service_name, &wlen); + if (service_name == NULL) { + return NULL; + } + + hService = psutil_get_service_handler(service_name, scm_access, access); + if (hService == NULL) { + PyMem_Free(service_name); + return NULL; + } + + *service_name_out = service_name; + return hService; +} + + + // XXX - expose these as constants? static const char * get_startup_string(DWORD startup) { @@ -188,7 +229,7 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { */ PyObject * psutil_winservice_query_config(PyObject *self, PyObject *args) { - char *service_name; + wchar_t *service_name = NULL; SC_HANDLE hService = NULL; BOOL ok; DWORD bytesNeeded = 0; @@ -198,12 +239,14 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { PyObject *py_unicode_binpath = NULL; PyObject *py_unicode_username = NULL; - if (!PyArg_ParseTuple(args, "s", &service_name)) - return NULL; - hService = psutil_get_service_handler( - service_name, SC_MANAGER_ENUMERATE_SERVICE, SERVICE_QUERY_CONFIG); + hService = psutil_get_service_from_args( + args, + SC_MANAGER_ENUMERATE_SERVICE, + SERVICE_QUERY_CONFIG, + &service_name + ); if (hService == NULL) - goto error; + return NULL; // First call to QueryServiceConfigW() is necessary to get the // right size. @@ -213,9 +256,15 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); goto error; } + qsc = (QUERY_SERVICE_CONFIGW *)malloc(bytesNeeded); + if (qsc == NULL) { + PyErr_NoMemory(); + goto error; + } + ok = QueryServiceConfigW(hService, qsc, bytesNeeded, &bytesNeeded); - if (ok == 0) { + if (!ok) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); goto error; } @@ -255,6 +304,7 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { Py_DECREF(py_unicode_username); free(qsc); CloseServiceHandle(hService); + PyMem_Free(service_name); return py_tuple; error: @@ -262,10 +312,12 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { Py_XDECREF(py_unicode_binpath); Py_XDECREF(py_unicode_username); Py_XDECREF(py_tuple); - if (hService != NULL) + if (hService) CloseServiceHandle(hService); - if (qsc != NULL) + if (qsc) free(qsc); + if (service_name) + PyMem_Free(service_name); return NULL; } @@ -277,19 +329,21 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { */ PyObject * psutil_winservice_query_status(PyObject *self, PyObject *args) { - char *service_name; + wchar_t *service_name = NULL; SC_HANDLE hService = NULL; BOOL ok; DWORD bytesNeeded = 0; - SERVICE_STATUS_PROCESS *ssp = NULL; + SERVICE_STATUS_PROCESS *ssp = NULL; PyObject *py_tuple = NULL; - if (!PyArg_ParseTuple(args, "s", &service_name)) - return NULL; - hService = psutil_get_service_handler( - service_name, SC_MANAGER_ENUMERATE_SERVICE, SERVICE_QUERY_STATUS); + hService = psutil_get_service_from_args( + args, + SC_MANAGER_ENUMERATE_SERVICE, + SERVICE_QUERY_STATUS, + &service_name + ); if (hService == NULL) - goto error; + return NULL; // First call to QueryServiceStatusEx() is necessary to get the // right size. @@ -299,14 +353,15 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { // Also services.msc fails in the same manner, so we return an // empty string. CloseServiceHandle(hService); + PyMem_Free(service_name); return Py_BuildValue("s", ""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); goto error; } - ssp = (SERVICE_STATUS_PROCESS *)HeapAlloc( - GetProcessHeap(), 0, bytesNeeded); + + ssp = (SERVICE_STATUS_PROCESS *)HeapAlloc(GetProcessHeap(), 0, bytesNeeded); if (ssp == NULL) { PyErr_NoMemory(); goto error; @@ -315,7 +370,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { // Actual call. ok = QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)ssp, bytesNeeded, &bytesNeeded); - if (ok == 0) { + if (!ok) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); goto error; } @@ -330,43 +385,39 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { CloseServiceHandle(hService); HeapFree(GetProcessHeap(), 0, ssp); + PyMem_Free(service_name); return py_tuple; error: Py_XDECREF(py_tuple); - if (hService != NULL) + if (hService) CloseServiceHandle(hService); - if (ssp != NULL) + if (ssp) HeapFree(GetProcessHeap(), 0, ssp); + if (service_name) + PyMem_Free(service_name); return NULL; } - -/* - * Get service description. - */ PyObject * psutil_winservice_query_descr(PyObject *self, PyObject *args) { - ENUM_SERVICE_STATUS_PROCESSW *lpService = NULL; BOOL ok; DWORD bytesNeeded = 0; SC_HANDLE hService = NULL; SERVICE_DESCRIPTIONW *scd = NULL; - char *service_name; + wchar_t *service_name = NULL; PyObject *py_retstr = NULL; - if (!PyArg_ParseTuple(args, "s", &service_name)) - return NULL; - hService = psutil_get_service_handler( - service_name, SC_MANAGER_ENUMERATE_SERVICE, SERVICE_QUERY_CONFIG); + hService = psutil_get_service_from_args( + args, + SC_MANAGER_ENUMERATE_SERVICE, + SERVICE_QUERY_CONFIG, + &service_name + ); if (hService == NULL) - goto error; + return NULL; - // This first call to QueryServiceConfig2W() is necessary in order - // to get the right size. - bytesNeeded = 0; - QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, - &bytesNeeded); + QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, &bytesNeeded); if ((GetLastError() == ERROR_NOT_FOUND) || (GetLastError() == ERROR_MUI_FILE_NOT_FOUND)) @@ -375,6 +426,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { // empty string. psutil_debug("set empty string for NOT_FOUND service description"); CloseServiceHandle(hService); + PyMem_Free(service_name); return Py_BuildValue("s", ""); } @@ -384,9 +436,14 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { } scd = (SERVICE_DESCRIPTIONW *)malloc(bytesNeeded); + if (scd == NULL) { + PyErr_NoMemory(); + goto error; + } + ok = QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, (LPBYTE)scd, bytesNeeded, &bytesNeeded); - if (ok == 0) { + if (!ok) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); goto error; } @@ -396,20 +453,24 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { } else { py_retstr = PyUnicode_FromWideChar( - scd->lpDescription, wcslen(scd->lpDescription)); + scd->lpDescription, wcslen(scd->lpDescription)); } + if (!py_retstr) goto error; free(scd); CloseServiceHandle(hService); + PyMem_Free(service_name); return py_retstr; error: - if (hService != NULL) + if (hService) CloseServiceHandle(hService); - if (lpService != NULL) - free(lpService); + if (scd) + free(scd); + if (service_name) + PyMem_Free(service_name); return NULL; } @@ -420,29 +481,34 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { */ PyObject * psutil_winservice_start(PyObject *self, PyObject *args) { - char *service_name; BOOL ok; SC_HANDLE hService = NULL; + wchar_t *service_name = NULL; - if (!PyArg_ParseTuple(args, "s", &service_name)) + hService = psutil_get_service_from_args( + args, + SC_MANAGER_ALL_ACCESS, + SERVICE_START, + &service_name + ); + if (hService == NULL) return NULL; - hService = psutil_get_service_handler( - service_name, SC_MANAGER_ALL_ACCESS, SERVICE_START); - if (hService == NULL) { - goto error; - } + ok = StartService(hService, 0, NULL); - if (ok == 0) { + if (!ok) { psutil_PyErr_SetFromOSErrnoWithSyscall("StartService"); goto error; } CloseServiceHandle(hService); + PyMem_Free(service_name); Py_RETURN_NONE; error: - if (hService != NULL) + if (hService) CloseServiceHandle(hService); + if (service_name) + PyMem_Free(service_name); return NULL; } @@ -453,32 +519,37 @@ psutil_winservice_start(PyObject *self, PyObject *args) { */ PyObject * psutil_winservice_stop(PyObject *self, PyObject *args) { - char *service_name; + wchar_t *service_name = NULL; BOOL ok; SC_HANDLE hService = NULL; SERVICE_STATUS ssp; - if (!PyArg_ParseTuple(args, "s", &service_name)) - return NULL; - hService = psutil_get_service_handler( - service_name, SC_MANAGER_ALL_ACCESS, SERVICE_STOP); + hService = psutil_get_service_from_args( + args, + SC_MANAGER_ALL_ACCESS, + SERVICE_STOP, + &service_name + ); if (hService == NULL) - goto error; + return NULL; // Note: this can hang for 30 secs. Py_BEGIN_ALLOW_THREADS ok = ControlService(hService, SERVICE_CONTROL_STOP, &ssp); Py_END_ALLOW_THREADS - if (ok == 0) { + if (!ok) { psutil_PyErr_SetFromOSErrnoWithSyscall("ControlService"); goto error; } CloseServiceHandle(hService); + PyMem_Free(service_name); Py_RETURN_NONE; error: - if (hService != NULL) + if (hService) CloseServiceHandle(hService); + if (service_name) + PyMem_Free(service_name); return NULL; } From df0d4e2637917377bd5971f1745e881b75f2c6e2 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 4 Sep 2025 15:30:42 +0300 Subject: [PATCH 1345/1714] Call GetExtended[Tcp|Udp]Table twice under free-threaded build (round 2) (#2627) This time, initialize `size` properly so that it doesn't SEGFAULT, in case it's got a garbage value when first defined. --- psutil/arch/windows/socks.c | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 1d35de0423..09bee6820b 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -17,33 +17,23 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) #define STATUS_UNSUCCESSFUL 0xC0000001 -ULONG g_TcpTableSize = 0; -ULONG g_UdpTableSize = 0; - - // Note about GetExtended[Tcp|Udp]Table syscalls: due to other processes // being active on the machine, it's possible that the size of the table // increases between the moment we query the size and the moment we query // the data. Therefore we retry if that happens. See: // https://github.com/giampaolo/psutil/pull/1335 // https://github.com/giampaolo/psutil/issues/1294 -// A global and ever increasing size is used in order to avoid calling -// GetExtended[Tcp|Udp]Table twice per call (faster). static PVOID __GetExtendedTcpTable(ULONG family) { DWORD err; PVOID table; - ULONG size; + ULONG size = 0; TCP_TABLE_CLASS class = TCP_TABLE_OWNER_PID_ALL; - size = g_TcpTableSize; - if (size == 0) { - GetExtendedTcpTable(NULL, &size, FALSE, family, class, 0); - // reserve 25% more space - size = size + (size / 2 / 2); - g_TcpTableSize = size; - } + GetExtendedTcpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space to be sure + size = size + (size / 2 / 2); table = malloc(size); if (table == NULL) { @@ -58,7 +48,6 @@ static PVOID __GetExtendedTcpTable(ULONG family) { free(table); if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { psutil_debug("GetExtendedTcpTable: retry with different bufsize"); - g_TcpTableSize = 0; return __GetExtendedTcpTable(family); } @@ -70,16 +59,12 @@ static PVOID __GetExtendedTcpTable(ULONG family) { static PVOID __GetExtendedUdpTable(ULONG family) { DWORD err; PVOID table; - ULONG size; + ULONG size = 0; UDP_TABLE_CLASS class = UDP_TABLE_OWNER_PID; - size = g_UdpTableSize; - if (size == 0) { - GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); - // reserve 25% more space - size = size + (size / 2 / 2); - g_UdpTableSize = size; - } + GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space + size = size + (size / 2 / 2); table = malloc(size); if (table == NULL) { @@ -94,7 +79,6 @@ static PVOID __GetExtendedUdpTable(ULONG family) { free(table); if (err == ERROR_INSUFFICIENT_BUFFER || err == STATUS_UNSUCCESSFUL) { psutil_debug("GetExtendedUdpTable: retry with different bufsize"); - g_UdpTableSize = 0; return __GetExtendedUdpTable(family); } From 83e4f7fdd76051dd8e0eeed99e2bf5fc881a2960 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 4 Sep 2025 17:06:11 +0200 Subject: [PATCH 1346/1714] OSX: disable failing test --- psutil/tests/test_process.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index d3341ebbef..264f3ed639 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1577,6 +1577,7 @@ class TestPopen(PsutilTestCase): def tearDownClass(cls): reap_children() + @pytest.mark.skipif(MACOS and GITHUB_ACTIONS, reason="hangs on OSX + CI") def test_misc(self): # XXX this test causes a ResourceWarning because # psutil.__subproc instance doesn't get properly freed. From 3a4c01638bc61c77f43a05aea5bc6e1ddc81c6c4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 17 Sep 2025 17:15:41 +0200 Subject: [PATCH 1347/1714] Stop publishing wheels for Python 3.6 and 3.7 (#2629) This will speed up the CI test run considerably. 8 platforms * 2 python versions = 16 test runs which are not executed and (if I'm not mistaken) 16 files which we'll no longer publish on PYPI. Note: Python 3.6 and 3.7 are still supported, but on those psutil can only be installed from sources. --- .github/workflows/build.yml | 23 ++++++++++------------- HISTORY.rst | 6 +++++- docs/index.rst | 3 ++- pyproject.toml | 2 ++ 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24f80b0957..1e992bd83c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: tests: name: "tests, ${{ matrix.os }}, ${{ matrix.arch }}" runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -34,14 +34,12 @@ jobs: - { os: windows-2025, arch: x86 } - { os: windows-11-arm, arch: ARM64 } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - # see https://cibuildwheel.pypa.io/en/stable/faq/#macos-building-cpython-38-wheels-on-arm64 - - name: "Install python 3.8 universal2 on macOS arm64" + # Only install Python 3.8 on macOS ARM64 for universal2 support + - name: "Install Python 3.8 on macOS ARM64" if: runner.os == 'macOS' && runner.arch == 'ARM64' uses: actions/setup-python@v5 - env: - PIP_DISABLE_PIP_VERSION_CHECK: 1 with: python-version: 3.8 @@ -49,7 +47,7 @@ jobs: with: python-version: 3.11 - - name: Create wheels + run tests + - name: Build wheels + run tests uses: pypa/cibuildwheel@v2.23.3 env: CIBW_ARCHS: "${{ matrix.arch }}" @@ -61,7 +59,7 @@ jobs: name: wheels-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse - - name: Generate .tar.gz + - name: Generate source distribution (.tar.gz) if: matrix.os == 'ubuntu-latest' run: | make generate-manifest @@ -76,17 +74,16 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: LizardByte/actions/actions/setup_python@master with: - python-version: "2.7" + python-version: 2.7 - run: python scripts/internal/test_python2_setup_py.py - # Run linters linters: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x @@ -111,7 +108,7 @@ jobs: needs: [upload-wheels] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/HISTORY.rst b/HISTORY.rst index 273bad90ff..9480031890 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,8 @@ XXXX-XX-XX - 2571_, [FreeBSD]: Dropped support for FreeBSD 8 and earlier. FreeBSD 8 was maintained from 2009 to 2013. - 2575_: introduced `dprint` CLI tool to format .yml and .md files. +- 2629_: stop publishing wheels for Python 3.6 and 3.7. Just provide + installation from sources. **Bug fixes** @@ -53,7 +55,9 @@ XXXX-XX-XX **Compatibility notes** -- 2571_: Dropped support for FreeBSD 8 and earlier. +- 2571_: dropped support for FreeBSD 8 and earlier. +- 2629_: stop publishing wheels for Python 3.6 and 3.7. Just provide + installation from sources. 7.0.0 ===== diff --git a/docs/index.rst b/docs/index.rst index e5582e4d6b..b6a9373e53 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2665,7 +2665,8 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= -* psutil 7.0.1 (XXXX-XX): drop **FreeBSD 8** +* psutil 7.1.0 (XXXX-XX): drop wheels for Python 3.6 and 3.7 +* psutil 7.1.0 (XXXX-XX): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 * psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 * psutil 5.9.1 (2022-05): drop Python 2.6 diff --git a/pyproject.toml b/pyproject.toml index 82de944ab3..51e26c13df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,6 +221,8 @@ trailing_comma_inline_array = true [tool.cibuildwheel] skip = [ "*-musllinux*", + "cp36-*", + "cp37-*", "cp3{7,8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures "pp*", ] From fb75b28226ec8e6b9e5b9eefa05a62de579cc114 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 17 Sep 2025 18:56:42 +0200 Subject: [PATCH 1348/1714] Chore: skip CI tests except on Python 3.8 and 3.13 This way tests will run a lot faster. Also disable tests OSX + py3.13 because it hangs. --- .github/workflows/bsd.yml | 6 +++--- pyproject.toml | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index a0406b9d2c..411a451050 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -14,7 +14,7 @@ jobs: # if: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run tests uses: vmactions/freebsd-vm@v1 with: @@ -26,7 +26,7 @@ jobs: # if: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run tests uses: vmactions/openbsd-vm@v1 with: @@ -38,7 +38,7 @@ jobs: if: false # XXX: disabled runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run tests uses: vmactions/netbsd-vm@v1 with: diff --git a/pyproject.toml b/pyproject.toml index 51e26c13df..8bc9cc2a44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -226,9 +226,12 @@ skip = [ "cp3{7,8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures "pp*", ] +test-skip = [ + "cp313-macosx*", # Hangs on macOS for some reason + "cp3{6,7,8,9,10,11,12}-*", # Only run tests for minor (3.8) and major (3.13) Python versions +] test-extras = ["test"] test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" test-cibuildwheel" -test-skip = "cp39-win_arm64" # pywin32 is not available on cp39 ARM64 [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] From 229e2de232860fe22e2c2884c20b8979c643d5c6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 17 Sep 2025 19:22:02 +0200 Subject: [PATCH 1349/1714] Pre-release --- HISTORY.rst | 2 +- docs/index.rst | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9480031890..d44a101bc6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 7.1.0 ===== -XXXX-XX-XX +2025-09-17 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index b6a9373e53..7acd79787d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2665,8 +2665,8 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= -* psutil 7.1.0 (XXXX-XX): drop wheels for Python 3.6 and 3.7 -* psutil 7.1.0 (XXXX-XX): drop **FreeBSD 8** +* psutil 7.1.0 (2025-09): drop wheels for Python 3.6 and 3.7 +* psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 * psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 * psutil 5.9.1 (2022-05): drop Python 2.6 @@ -2688,6 +2688,10 @@ PyPy3. Timeline ======== +- 2025-09-17: + `7.0.0 `__ - + `what's new `__ - + `diff `__ - 2025-02-13: `7.0.0 `__ - `what's new `__ - From 0d18187e79b349e577fadabd3589f8fdbf99bf5a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 17 Sep 2025 22:06:05 +0200 Subject: [PATCH 1350/1714] Revert HISTORY notes about #2629. We still do publish 3.6 and 3.7 wheels. --- HISTORY.rst | 4 ---- docs/index.rst | 1 - 2 files changed, 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d44a101bc6..c66939db09 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,8 +11,6 @@ - 2571_, [FreeBSD]: Dropped support for FreeBSD 8 and earlier. FreeBSD 8 was maintained from 2009 to 2013. - 2575_: introduced `dprint` CLI tool to format .yml and .md files. -- 2629_: stop publishing wheels for Python 3.6 and 3.7. Just provide - installation from sources. **Bug fixes** @@ -56,8 +54,6 @@ **Compatibility notes** - 2571_: dropped support for FreeBSD 8 and earlier. -- 2629_: stop publishing wheels for Python 3.6 and 3.7. Just provide - installation from sources. 7.0.0 ===== diff --git a/docs/index.rst b/docs/index.rst index 7acd79787d..ac7b9d49ec 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2665,7 +2665,6 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= -* psutil 7.1.0 (2025-09): drop wheels for Python 3.6 and 3.7 * psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 * psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 From 599f90e70706c467dbceb70215c0d82cd452519d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 19 Sep 2025 17:47:12 +0200 Subject: [PATCH 1351/1714] Move scripts/internal/print_sysinfo.py (#2631) --- MANIFEST.in | 1 + Makefile | 4 +- psutil/tests/__init__.py | 112 +-------------------- scripts/internal/print_sysinfo.py | 157 ++++++++++++++++++++++++++++++ scripts/internal/winmake.py | 5 +- 5 files changed, 162 insertions(+), 117 deletions(-) create mode 100755 scripts/internal/print_sysinfo.py diff --git a/MANIFEST.in b/MANIFEST.in index d63e5d6ec9..05a5540d4d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -174,6 +174,7 @@ include scripts/internal/print_api_speed.py include scripts/internal/print_dist.py include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py +include scripts/internal/print_sysinfo.py include scripts/internal/print_timeline.py include scripts/internal/purge_installation.py include scripts/internal/test_python2_setup_py.py diff --git a/Makefile b/Makefile index a57c4fe280..08c3f435c3 100644 --- a/Makefile +++ b/Makefile @@ -177,8 +177,8 @@ test-cibuildwheel: ## Run tests from cibuildwheel. # testing the wheels means we can't use other test targets which are rebuilding the python extensions # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed ${MAKE} install-sysdeps + ${MAKE} print-sysinfo mkdir -p .tests - cd .tests/ && python -c "from psutil.tests import print_sysinfo; print_sysinfo()" cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k test_memleaks.py" $(PYTHON) -m pytest --pyargs psutil.tests @@ -339,7 +339,7 @@ print-hashes: ## Prints hashes of files in dist/ directory $(PYTHON) scripts/internal/print_hashes.py dist/ print-sysinfo: ## Prints system info - $(PYTHON) -c "from psutil.tests import print_sysinfo; print_sysinfo()" + $(PYTHON) scripts/internal/print_sysinfo.py # =================================================================== # Misc diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 7aa03330ad..462a3115c7 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -80,7 +80,7 @@ # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', - 'process_namespace', 'system_namespace', 'print_sysinfo', + 'process_namespace', 'system_namespace', 'is_win_secure_system_proc', 'fake_pytest', # fs utils 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', @@ -1325,116 +1325,6 @@ def call(): 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 shutil.which("lsb_release"): - info['OS'] = sh('lsb_release -d -s') - elif psutil.OSX: - info['OS'] = f"Darwin {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'] = f"{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'] += f" (wheel={wheel.__version__})" - - # UNIX - if psutil.POSIX: - if shutil.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'] = f"{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}%".format( - *tuple(x / psutil.cpu_count() * 100 for x in psutil.getloadavg()) - ) - mem = psutil.virtual_memory() - info['memory'] = "{}%%, used={}, total={}".format( - int(mem.percent), - bytes2human(mem.used), - bytes2human(mem.total), - ) - swap = psutil.swap_memory() - info['swap'] = "{}%%, used={}, total={}".format( - int(swap.percent), - bytes2human(swap.used), - bytes2human(swap.total), - ) - - # constants - constants = sorted( - [k for k, v in globals().items() if k.isupper() and v is True] - ) - info['constants'] = "\n ".join(constants) - - # processes - info['pids'] = len(psutil.pids()) - pinfo = psutil.Process().as_dict() - pinfo.pop('memory_maps', None) - pinfo["environ"] = {k: os.environ[k] for k in sorted(os.environ)} - info['proc'] = pprint.pformat(pinfo) - - # print - print("=" * 70, file=sys.stderr) # noqa: T201 - for k, v in info.items(): - print("{:<17} {}".format(k + ":", v), file=sys.stderr) # noqa: T201 - print("=" * 70, file=sys.stderr) # noqa: T201 - sys.stdout.flush() - - def is_win_secure_system_proc(pid): # see: https://github.com/giampaolo/psutil/issues/2338 @memoize diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py new file mode 100755 index 0000000000..eb85032db8 --- /dev/null +++ b/scripts/internal/print_sysinfo.py @@ -0,0 +1,157 @@ +#!/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. + +"""Print system information. Run before CI test run.""" + +import collections +import datetime +import getpass +import importlib.util +import locale +import os +import platform +import pprint +import shlex +import shutil +import subprocess +import sys + +import psutil +from psutil._common import bytes2human + +try: + import pip +except ImportError: + pip = None +try: + import wheel +except ImportError: + wheel = None + + +HERE = os.path.realpath(os.path.abspath(os.path.dirname(__file__))) + + +def sh(cmd): + if isinstance(cmd, str): + cmd = shlex.split(cmd) + return subprocess.check_output(cmd, universal_newlines=True).strip() + + +def import_module_by_path(path): + name = os.path.splitext(os.path.basename(path))[0] + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +tests_init = os.path.realpath( + os.path.join(HERE, "..", "..", "psutil", "tests", "__init__.py") +) + +tests_init_mod = import_module_by_path(tests_init) + + +def main(): + info = collections.OrderedDict() + + # OS + if psutil.LINUX and shutil.which("lsb_release"): + info['OS'] = sh('lsb_release -d -s') + elif psutil.OSX: + info['OS'] = f"Darwin {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'] = f"{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'] += f" (wheel={wheel.__version__})" + + # UNIX + if psutil.POSIX: + if shutil.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'] = f"{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'] = tests_init_mod.PYTHON_EXE + info['hostname'] = platform.node() + info['PID'] = os.getpid() + + # metrics + info['cpus'] = psutil.cpu_count() + info['loadavg'] = "{:.1f}%, {:.1f}%, {:.1f}%".format( + *tuple(x / psutil.cpu_count() * 100 for x in psutil.getloadavg()) + ) + mem = psutil.virtual_memory() + info['memory'] = "{}%%, used={}, total={}".format( + int(mem.percent), + bytes2human(mem.used), + bytes2human(mem.total), + ) + swap = psutil.swap_memory() + info['swap'] = "{}%%, used={}, total={}".format( + int(swap.percent), + bytes2human(swap.used), + bytes2human(swap.total), + ) + + # constants + constants = sorted([ + x + for x in dir(tests_init_mod) + if x.isupper() and getattr(tests_init_mod, x) is True + ]) + info['constants'] = "\n ".join(constants) + + # processes + info['pids'] = len(psutil.pids()) + pinfo = psutil.Process().as_dict() + pinfo.pop('memory_maps', None) + pinfo["environ"] = {k: os.environ[k] for k in sorted(os.environ)} + info['proc'] = pprint.pformat(pinfo) + + # print + print("=" * 70, file=sys.stderr) + for k, v in info.items(): + print("{:<17} {}".format(k + ":", v), file=sys.stderr) + print("=" * 70, file=sys.stderr) + sys.stdout.flush() + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index ae90353200..86d19e4ed8 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -461,10 +461,7 @@ def print_api_speed(): def print_sysinfo(): """Print system info.""" - build() - from psutil.tests import print_sysinfo - - print_sysinfo() + sh([PYTHON, "scripts\\internal\\print_sysinfo.py"]) def generate_manifest(): From d622c43420fee9174760f09b3889043da0b6dd07 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Fri, 19 Sep 2025 17:53:31 +0200 Subject: [PATCH 1352/1714] Tests that needs UNIX sockets should be properly marked for skip (#2592) --- psutil/tests/test_connections.py | 3 +++ psutil/tests/test_testutils.py | 3 +++ psutil/tests/test_unicode.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 8d693bc9bf..6a9778e0a4 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -224,6 +224,9 @@ def test_tcp(self): client.close() @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets" + ) def test_unix(self): testfn = self.get_testfn() server, client = unix_socketpair(testfn) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 5472651da5..2bdb2614f9 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -324,6 +324,9 @@ def test_tcp_socketpair(self): @pytest.mark.skipif( NETBSD or FREEBSD, reason="/var/run/log UNIX socket opened by default" ) + @pytest.mark.skipif( + not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets" + ) def test_unix_socketpair(self): p = psutil.Process() num_fds = p.num_fds() diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 8533c04e1a..070efe4db5 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -231,6 +231,9 @@ def test_proc_open_files(self): assert os.path.normcase(path) == os.path.normcase(self.funky_name) @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets" + ) def test_proc_net_connections(self): name = self.get_testfn(suffix=self.funky_suffix) sock = bind_unix_socket(name) From 0482bf6418fb9761d01a431d38a940becaed684f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 19 Sep 2025 19:17:29 +0200 Subject: [PATCH 1353/1714] #2475: document net_connections() on Linux may return incomplete results --- docs/index.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index ac7b9d49ec..4b29f6f8d7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -664,6 +664,11 @@ Network pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status='SYN_SENT', pid=None) ...] + .. warning:: + On Linux, retrieving some connections requires root privileges. If psutil is + not run as root, those connections are silently skipped instead of raising + ``PermissionError``. That means the returned list may be incomplete. + .. note:: (macOS and AIX) :class:`psutil.AccessDenied` is always raised unless running as root. This is a limitation of the OS and ``lsof`` does the same. @@ -1963,6 +1968,12 @@ Process class pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status='ESTABLISHED'), pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status='SYN_SENT')] + .. warning:: + On Linux, retrieving connections for certain processes requires root + privileges. If psutil is not run as root, those connections are silently + skipped instead of raising :class:`psutil.AccessDenied`. That means + the returned list may be incomplete. + .. note:: (Solaris) UNIX sockets are not supported. From fb0792eb48465438708575f96d7ec997240a98cf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 20 Sep 2025 22:07:20 +0200 Subject: [PATCH 1354/1714] SunOS: rm / move .h files (#2633) --- psutil/_psutil_sunos.c | 7 ------- psutil/arch/all/init.h | 2 ++ psutil/arch/sunos/cpu.c | 2 -- psutil/arch/sunos/cpu.h | 11 ---------- psutil/arch/sunos/disk.c | 2 -- psutil/arch/sunos/disk.h | 10 --------- psutil/arch/sunos/environ.c | 1 - psutil/arch/sunos/environ.h | 21 ------------------- psutil/arch/sunos/init.h | 41 +++++++++++++++++++++++++++++++++++++ psutil/arch/sunos/mem.c | 2 -- psutil/arch/sunos/mem.h | 9 -------- psutil/arch/sunos/net.h | 11 ---------- psutil/arch/sunos/proc.c | 6 +++--- psutil/arch/sunos/proc.h | 20 ------------------ psutil/arch/sunos/sys.h | 9 -------- 15 files changed, 46 insertions(+), 108 deletions(-) delete mode 100644 psutil/arch/sunos/cpu.h delete mode 100644 psutil/arch/sunos/disk.h delete mode 100644 psutil/arch/sunos/environ.h create mode 100644 psutil/arch/sunos/init.h delete mode 100644 psutil/arch/sunos/mem.h delete mode 100644 psutil/arch/sunos/net.h delete mode 100644 psutil/arch/sunos/proc.h delete mode 100644 psutil/arch/sunos/sys.h diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 54b40651c6..f6239403a4 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -26,13 +26,6 @@ #include #include "arch/all/init.h" -#include "arch/sunos/cpu.h" -#include "arch/sunos/disk.h" -#include "arch/sunos/environ.h" -#include "arch/sunos/mem.h" -#include "arch/sunos/net.h" -#include "arch/sunos/proc.h" -#include "arch/sunos/sys.h" static PyMethodDef mod_methods[] = { diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index d8904e93b5..dc7f0fa3af 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -30,6 +30,8 @@ #include "../../arch/openbsd/init.h" #elif defined(PSUTIL_NETBSD) #include "../../arch/netbsd/init.h" +#elif defined(PSUTIL_SUNOS) + #include "../../arch/sunos/init.h" #endif // print debug messages when set to 1 diff --git a/psutil/arch/sunos/cpu.c b/psutil/arch/sunos/cpu.c index 03837e54ea..946bffd00f 100644 --- a/psutil/arch/sunos/cpu.c +++ b/psutil/arch/sunos/cpu.c @@ -9,8 +9,6 @@ #include #include -#include "../../arch/all/init.h" - // System-wide CPU times. PyObject * diff --git a/psutil/arch/sunos/cpu.h b/psutil/arch/sunos/cpu.h deleted file mode 100644 index 5c4309bd93..0000000000 --- a/psutil/arch/sunos/cpu.h +++ /dev/null @@ -1,11 +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. - */ - -#include - -PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); -PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/disk.c b/psutil/arch/sunos/disk.c index c81acd832f..f736984c76 100644 --- a/psutil/arch/sunos/disk.c +++ b/psutil/arch/sunos/disk.c @@ -10,8 +10,6 @@ #include #include -#include "../../arch/all/init.h" - PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { diff --git a/psutil/arch/sunos/disk.h b/psutil/arch/sunos/disk.h deleted file mode 100644 index 5364b96b64..0000000000 --- a/psutil/arch/sunos/disk.h +++ /dev/null @@ -1,10 +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. - */ - -#include - -PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/environ.c b/psutil/arch/sunos/environ.c index 6531b61959..458c7f2225 100644 --- a/psutil/arch/sunos/environ.c +++ b/psutil/arch/sunos/environ.c @@ -17,7 +17,6 @@ #include #include "../../arch/all/init.h" -#include "environ.h" #define STRING_SEARCH_BUF_SIZE 512 diff --git a/psutil/arch/sunos/environ.h b/psutil/arch/sunos/environ.h deleted file mode 100644 index e106ade6b6..0000000000 --- a/psutil/arch/sunos/environ.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2009, Giampaolo Rodola', Oleksii Shevchuk. - * All rights reserved. Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file. - */ - -#include - -#ifndef PROCESS_AS_UTILS_H -#define PROCESS_AS_UTILS_H - -char ** -psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count); - -char ** -psutil_read_raw_env(psinfo_t info, const char *procfs_path, ssize_t *count); - -void -psutil_free_cstrings_array(char **array, size_t count); - -#endif // PROCESS_AS_UTILS_H diff --git a/psutil/arch/sunos/init.h b/psutil/arch/sunos/init.h new file mode 100644 index 0000000000..52ac0139b3 --- /dev/null +++ b/psutil/arch/sunos/init.h @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#include +#include + +#ifndef PROCESS_AS_UTILS_H +#define PROCESS_AS_UTILS_H + +char ** +psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count); + +char ** +psutil_read_raw_env(psinfo_t info, const char *procfs_path, ssize_t *count); + +void +psutil_free_cstrings_array(char **array, size_t count); +#endif // PROCESS_AS_UTILS_H + +PyObject *psutil_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); +PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); +PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); +PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); +PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_basic_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_num(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_times(PyObject *self, PyObject *args); +PyObject *psutil_proc_cred(PyObject *self, PyObject *args); +PyObject *psutil_proc_environ(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); +PyObject *psutil_proc_name_and_args(PyObject *self, PyObject *args); +PyObject *psutil_proc_num_ctx_switches(PyObject *self, PyObject *args); +PyObject *psutil_proc_query_thread(PyObject *self, PyObject *args); +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/mem.c b/psutil/arch/sunos/mem.c index 04b4470124..c4bf97189b 100644 --- a/psutil/arch/sunos/mem.c +++ b/psutil/arch/sunos/mem.c @@ -10,8 +10,6 @@ #include #include -#include "mem.h" - PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { diff --git a/psutil/arch/sunos/mem.h b/psutil/arch/sunos/mem.h deleted file mode 100644 index 724cb586db..0000000000 --- a/psutil/arch/sunos/mem.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/net.h b/psutil/arch/sunos/net.h deleted file mode 100644 index 97bfd0af57..0000000000 --- a/psutil/arch/sunos/net.h +++ /dev/null @@ -1,11 +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. - */ - -#include - -PyObject *psutil_net_connections(PyObject *self, PyObject *args); -PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); -PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index 39bb9a52be..ad27c1dfc8 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -10,12 +10,12 @@ #include #include "../../arch/all/init.h" -#include "proc.h" -#include "environ.h" + +#define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) // Read a file content and fills a C structure with it. -int +static int psutil_file_to_struct(char *path, void *fstruct, size_t size) { int fd; ssize_t nbytes; diff --git a/psutil/arch/sunos/proc.h b/psutil/arch/sunos/proc.h deleted file mode 100644 index 874f360e58..0000000000 --- a/psutil/arch/sunos/proc.h +++ /dev/null @@ -1,20 +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. - */ - -#include - -#define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) -int psutil_file_to_struct(char *path, void *fstruct, size_t size); - -PyObject *psutil_proc_basic_info(PyObject *self, PyObject *args); -PyObject *psutil_proc_cpu_num(PyObject *self, PyObject *args); -PyObject *psutil_proc_cpu_times(PyObject *self, PyObject *args); -PyObject *psutil_proc_cred(PyObject *self, PyObject *args); -PyObject *psutil_proc_environ(PyObject *self, PyObject *args); -PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); -PyObject *psutil_proc_name_and_args(PyObject *self, PyObject *args); -PyObject *psutil_proc_num_ctx_switches(PyObject *self, PyObject *args); -PyObject *psutil_proc_query_thread(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/sys.h b/psutil/arch/sunos/sys.h deleted file mode 100644 index 9695dd7009..0000000000 --- a/psutil/arch/sunos/sys.h +++ /dev/null @@ -1,9 +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. - */ - -#include - -PyObject *psutil_boot_time(PyObject *self, PyObject *args); From 0217662c7e3381612e1a15181b02fe3d06894272 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Fri, 3 Oct 2025 16:01:33 +0300 Subject: [PATCH 1355/1714] fix: add missing heahder file _psutil_posix.h (#2640) --- psutil/_psutil_posix.h | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 psutil/_psutil_posix.h diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h new file mode 100644 index 0000000000..5a37e48b15 --- /dev/null +++ b/psutil/_psutil_posix.h @@ -0,0 +1,9 @@ +/* + * 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. + */ + +long psutil_getpagesize(void); +int psutil_pid_exists(pid_t pid); +void psutil_raise_for_pid(pid_t pid, char *msg); From cfc02a17787e3fd43f46fecfb81f4b212eeb61a1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 7 Oct 2025 15:48:04 +0200 Subject: [PATCH 1356/1714] Fix #2641, SunOS: fix compilation err due to missing C include. --- HISTORY.rst | 10 ++++++++++ psutil/_psutil_sunos.c | 1 + 2 files changed, 11 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index c66939db09..ef8557e361 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* + +7.1.1 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 2641_, [SunOS]: cannot compile psutil from sources due to missing C include. + 7.1.0 ===== diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index f6239403a4..eac3544be4 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -24,6 +24,7 @@ #endif #include +#include #include "arch/all/init.h" From 13e7d91f90410cd1cb26ecdbf9eb1e338e99ebf9 Mon Sep 17 00:00:00 2001 From: Ben Raz Date: Sun, 12 Oct 2025 10:48:13 +0300 Subject: [PATCH 1357/1714] Avoid SunOS cmdline unnecessary join + split (#2643) --- CREDITS | 4 ++ HISTORY.rst | 1 + psutil/_pssunos.py | 2 +- psutil/arch/sunos/proc.c | 138 ++++++++++++++++----------------------- 4 files changed, 61 insertions(+), 84 deletions(-) diff --git a/CREDITS b/CREDITS index ae6fbb3322..32bdac45b2 100644 --- a/CREDITS +++ b/CREDITS @@ -868,3 +868,7 @@ I: 2494 N Isaac K. Ko W: https://github.com/kyet I: 2604 + +N: Ben Raz +W: https://github.com/ben9923 +I: 2357 diff --git a/HISTORY.rst b/HISTORY.rst index ef8557e361..1c07396705 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,7 @@ XXXX-XX-XX **Bug fixes** - 2641_, [SunOS]: cannot compile psutil from sources due to missing C include. +- 2357_, [SunOS]: `Process.cmdline()`_ does not handle spaces properly. (patch by Ben Raz) 7.1.0 ===== diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 1cd0430ff3..d1d88bdcf7 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -438,7 +438,7 @@ def exe(self): @wrap_exceptions def cmdline(self): - return self._proc_name_and_args()[1].split(' ') + return self._proc_name_and_args()[1] @wrap_exceptions def environ(self): diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index ad27c1dfc8..f892353f0b 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -76,59 +76,6 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { } -/* - * Join array of C strings to C string with delimiter dm. - * Omit empty records. - */ -static int -cstrings_array_to_string(char **joined, char ** array, size_t count, char dm) { - size_t i; - size_t total_length = 0; - size_t item_length = 0; - char *result = NULL; - char *last = NULL; - - if (!array || !joined) - return 0; - - for (i=0; i 0) { - py_args = PyUnicode_DecodeFSDefault(argv_plain); - free(argv_plain); - } else if (joined < 0) { - goto error; + /* SunOS truncates arguments to length PRARGSZ and has them space-separated. + * The only way to retrieve full properly-split command line is to parse process memory */ + argv = psutil_read_raw_args(info, procfs_path, &argc); + if (argv) { + py_args_list = PyList_New(argc); + if (!py_args_list) + goto error; + + // iterate through arguments + for (i = 0; i < argc; i++) { + py_arg = PyUnicode_DecodeFSDefault(argv[i]); + if (!py_arg) { + Py_DECREF(py_args_list); + py_args_list = NULL; + break; } - psutil_free_cstrings_array(argv, argc); + if (PyList_SetItem(py_args_list, i, py_arg)) + goto error; + + py_arg = NULL; } + + psutil_free_cstrings_array(argv, argc); } /* If we can't read process memory or can't decode the result * then return args from /proc. */ - if (!py_args) { + if (!py_args_list) { PyErr_Clear(); - py_args = PyUnicode_DecodeFSDefault(info.pr_psargs); - } + py_args_str = PyUnicode_DecodeFSDefault(info.pr_psargs); + if (!py_args_str) + goto error; - /* Both methods has been failed. */ - if (!py_args) - goto error; + py_sep = PyUnicode_FromString(" "); + if (!py_sep) + goto error; - py_retlist = Py_BuildValue("OO", py_name, py_args); - if (!py_retlist) + py_args_list = PyUnicode_Split(py_args_str, py_sep, -1); + if (!py_args_list) + goto error; + + Py_XDECREF(py_sep); + Py_XDECREF(py_args_str); + } + + py_rettuple = Py_BuildValue("OO", py_name, py_args_list); + if (!py_rettuple) goto error; Py_DECREF(py_name); - Py_DECREF(py_args); - return py_retlist; + Py_DECREF(py_args_list); + + return py_rettuple; error: + psutil_free_cstrings_array(argv, argc); Py_XDECREF(py_name); - Py_XDECREF(py_args); - Py_XDECREF(py_retlist); + Py_XDECREF(py_args_list); + Py_XDECREF(py_sep); + Py_XDECREF(py_arg); + Py_XDECREF(py_args_str); + Py_XDECREF(py_rettuple); return NULL; } From d46b6bec6585b854c8b987032a4d47409d2d1d67 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Oct 2025 06:00:09 +0200 Subject: [PATCH 1358/1714] Rm dead files from MANIFEST.in + refact Makefile --- MANIFEST.in | 12 ++--- Makefile | 53 ++++++++++--------- ...k_broken_links.py => find_broken_links.py} | 0 scripts/internal/generate_manifest.py | 2 + 4 files changed, 33 insertions(+), 34 deletions(-) rename scripts/internal/{check_broken_links.py => find_broken_links.py} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 05a5540d4d..7fb192e65a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -36,6 +36,7 @@ include psutil/_psutil_bsd.c include psutil/_psutil_linux.c include psutil/_psutil_osx.c include psutil/_psutil_posix.c +include psutil/_psutil_posix.h include psutil/_psutil_sunos.c include psutil/_psutil_windows.c include psutil/_pswindows.py @@ -97,19 +98,13 @@ include psutil/arch/posix/proc.c include psutil/arch/posix/sysctl.c include psutil/arch/posix/users.c include psutil/arch/sunos/cpu.c -include psutil/arch/sunos/cpu.h include psutil/arch/sunos/disk.c -include psutil/arch/sunos/disk.h include psutil/arch/sunos/environ.c -include psutil/arch/sunos/environ.h +include psutil/arch/sunos/init.h include psutil/arch/sunos/mem.c -include psutil/arch/sunos/mem.h include psutil/arch/sunos/net.c -include psutil/arch/sunos/net.h include psutil/arch/sunos/proc.c -include psutil/arch/sunos/proc.h include psutil/arch/sunos/sys.c -include psutil/arch/sunos/sys.h include psutil/arch/sunos/v10/ifaddrs.c include psutil/arch/sunos/v10/ifaddrs.h include psutil/arch/windows/cpu.c @@ -160,10 +155,10 @@ include scripts/ifconfig.py include scripts/internal/README include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py -include scripts/internal/check_broken_links.py include scripts/internal/clinter.py include scripts/internal/convert_readme.py include scripts/internal/download_wheels.py +include scripts/internal/find_broken_links.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py include scripts/internal/install-sysdeps.sh @@ -196,3 +191,4 @@ include scripts/top.py include scripts/who.py include scripts/winservices.py include setup.py +recursive-exclude docs/_static * diff --git a/Makefile b/Makefile index 08c3f435c3..c29996a06c 100644 --- a/Makefile +++ b/Makefile @@ -251,32 +251,37 @@ fix-all: ## Run all code fixers. # Distribution # =================================================================== -sdist: ## Create tar.gz source distribution. - ${MAKE} generate-manifest - $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist - -download-wheels: ## Download latest wheels hosted on github. - $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token - ${MAKE} print-dist - -create-wheels: ## Create .whl files - $(PYTHON_ENV_VARS) $(PYTHON) setup.py bdist_wheel - ${MAKE} check-wheels - check-sdist: ## Check sanity of source distribution. $(PYTHON_ENV_VARS) $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv $(PYTHON_ENV_VARS) build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz $(PYTHON_ENV_VARS) build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" $(PYTHON) -m twine check --strict dist/*.tar.gz +check-manifest: ## Check sanity of MANIFEST.in file. + $(PYTHON) -m check_manifest -v $(ARGS) + check-wheels: ## Check sanity of wheels. $(PYTHON) -m abi3audit --verbose --strict dist/*-abi3-*.whl $(PYTHON) -m twine check --strict dist/*.whl +check-distribution: + ${MAKE} check-sdist + ${MAKE} check-manifest + ${MAKE} check-wheels + +generate-manifest: ## Generates MANIFEST.in file. + $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in + +sdist: ## Create tar.gz source distribution. + ${MAKE} generate-manifest + $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist + +create-wheels: ## Create .whl files + $(PYTHON_ENV_VARS) $(PYTHON) setup.py bdist_wheel + pre-release: ## Check if we're ready to produce a new release. ${MAKE} clean ${MAKE} sdist - ${MAKE} check-sdist ${MAKE} install @$(PYTHON) -c \ "import requests, sys; \ @@ -293,22 +298,18 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in history, '%r not found in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX found in HISTORY.rst';" ${MAKE} download-wheels - ${MAKE} check-wheels + ${MAKE} check-distribution ${MAKE} print-hashes ${MAKE} print-dist release: ## Upload a new release. - ${MAKE} check-sdist - ${MAKE} check-wheels $(PYTHON) -m twine upload dist/*.tar.gz $(PYTHON) -m twine upload dist/*.whl ${MAKE} git-tag-release -generate-manifest: ## Generates MANIFEST.in file. - $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in - -print-dist: ## Print downloaded wheels / tar.gs - $(PYTHON) scripts/internal/print_dist.py +download-wheels: ## Download latest wheels hosted on github. + $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token + ${MAKE} print-dist git-tag-release: ## Git-tag a new release. git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` @@ -341,6 +342,9 @@ print-hashes: ## Prints hashes of files in dist/ directory print-sysinfo: ## Prints system info $(PYTHON) scripts/internal/print_sysinfo.py +print-dist: ## Print downloaded wheels / tar.gz + $(PYTHON) scripts/internal/print_dist.py + # =================================================================== # Misc # =================================================================== @@ -356,11 +360,8 @@ bench-oneshot-2: ## Same as above but using perf module (supposed to be more pr ${MAKE} build $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot_2.py -check-broken-links: ## Look for broken links in source files. - git ls-files | xargs $(PYTHON) -Wa scripts/internal/check_broken_links.py - -check-manifest: ## Inspect MANIFEST.in file. - $(PYTHON) -m check_manifest -v $(ARGS) +find-broken-links: ## Look for broken links in source files. + git ls-files | xargs $(PYTHON) -Wa scripts/internal/find_broken_links.py help: ## Display callable targets. @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/find_broken_links.py similarity index 100% rename from scripts/internal/check_broken_links.py rename to scripts/internal/find_broken_links.py diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 1ed6e5fe91..090324a93e 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -35,6 +35,8 @@ def main(): for file in sorted(files): print("include " + file) + print("recursive-exclude docs/_static *") + if __name__ == '__main__': main() From 4250b8e9e3520516766f30716daf113558cf61fd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Oct 2025 06:18:03 +0200 Subject: [PATCH 1359/1714] Make ruff happy --- .github/workflows/build.yml | 1 + psutil/tests/test_memleaks.py | 1 - psutil/tests/test_misc.py | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e992bd83c..7c76d1b081 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,6 +80,7 @@ jobs: python-version: 2.7 - run: python scripts/internal/test_python2_setup_py.py + # Run linters. linters: runs-on: ubuntu-latest steps: diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 5f64632d23..306b1c32f6 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -18,7 +18,6 @@ import os import psutil -import psutil._common from psutil import LINUX from psutil import MACOS from psutil import OPENBSD diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 5236d661f6..997bd18ebf 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -17,7 +17,6 @@ from unittest import mock import psutil -import psutil.tests from psutil import WINDOWS from psutil._common import bcat from psutil._common import cat From a2091b950f65c66583f5a67fbb5d030671141f63 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Oct 2025 12:51:25 +0200 Subject: [PATCH 1360/1714] Centralize distribution sanity check into Makefile --- .github/workflows/build.yml | 14 +++++++------- Makefile | 16 ++++++++++------ scripts/internal/print_hashes.py | 6 +++++- setup.py | 3 ++- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c76d1b081..b05da3568f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,9 +62,7 @@ jobs: - name: Generate source distribution (.tar.gz) if: matrix.os == 'ubuntu-latest' run: | - make generate-manifest - python setup.py sdist - mv dist/psutil*.tar.gz wheelhouse/ + make sdist # Test python 2.7 fallback installation message produced by setup.py py2-fallback: @@ -104,7 +102,7 @@ jobs: separate-directories: false delete-merged: true - # Check sanity of .tar.gz + wheel files + # Check sanity of the package distribution (.tar.gz and *.whl files) check-dist: needs: [upload-wheels] runs-on: ubuntu-latest @@ -118,6 +116,8 @@ jobs: name: wheels path: wheelhouse - run: | - python scripts/internal/print_hashes.py wheelhouse/ - pipx run twine check --strict wheelhouse/* - pipx run abi3audit --verbose --strict wheelhouse/*-abi3-*.whl + python3 -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit + make sdist + mv wheelhouse/* dist/ + make print-hashes + make check-distribution diff --git a/Makefile b/Makefile index c29996a06c..001e02e521 100644 --- a/Makefile +++ b/Makefile @@ -251,22 +251,26 @@ fix-all: ## Run all code fixers. # Distribution # =================================================================== +check-manifest: ## Check sanity of MANIFEST.in file. + $(PYTHON) -m check_manifest -v + +check-pyproject: ## Check sanity of pyproject.toml file. + $(PYTHON) -m validate_pyproject -v pyproject.toml + check-sdist: ## Check sanity of source distribution. $(PYTHON_ENV_VARS) $(PYTHON) -m virtualenv --clear --no-wheel --quiet build/venv $(PYTHON_ENV_VARS) build/venv/bin/python -m pip install -v --isolated --quiet dist/*.tar.gz $(PYTHON_ENV_VARS) build/venv/bin/python -c "import os; os.chdir('build/venv'); import psutil" $(PYTHON) -m twine check --strict dist/*.tar.gz -check-manifest: ## Check sanity of MANIFEST.in file. - $(PYTHON) -m check_manifest -v $(ARGS) - check-wheels: ## Check sanity of wheels. $(PYTHON) -m abi3audit --verbose --strict dist/*-abi3-*.whl $(PYTHON) -m twine check --strict dist/*.whl -check-distribution: - ${MAKE} check-sdist +check-distribution: ## Run all sanity checks re. to the package distribution. ${MAKE} check-manifest + ${MAKE} check-pyproject + ${MAKE} check-sdist ${MAKE} check-wheels generate-manifest: ## Generates MANIFEST.in file. @@ -337,7 +341,7 @@ print-downloads: ## Print PYPI download statistics $(PYTHON) scripts/internal/print_downloads.py print-hashes: ## Prints hashes of files in dist/ directory - $(PYTHON) scripts/internal/print_hashes.py dist/ + $(PYTHON) scripts/internal/print_hashes.py print-sysinfo: ## Prints system info $(PYTHON) scripts/internal/print_sysinfo.py diff --git a/scripts/internal/print_hashes.py b/scripts/internal/print_hashes.py index b8d8b365dc..1161268d46 100755 --- a/scripts/internal/print_hashes.py +++ b/scripts/internal/print_hashes.py @@ -23,7 +23,11 @@ def csum(file, kind): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - 'dir', type=str, help='directory containing tar.gz or wheel files' + "dir", + type=str, + nargs="?", + help="directory containing tar.gz or wheel files", + default="dist/", ) args = parser.parse_args() for name in sorted(os.listdir(args.dir)): diff --git a/setup.py b/setup.py index 20decd219c..abcc5aa627 100755 --- a/setup.py +++ b/setup.py @@ -100,6 +100,7 @@ "pylint", "pyperf", "pypinfo", + "pyreadline ; os_name == 'nt'", "pytest-cov", "requests", "rstcheck", @@ -108,10 +109,10 @@ "sphinx_rtd_theme", "toml-sort", "twine", + "validate-pyproject[all]", "virtualenv", "vulture", "wheel", - "pyreadline ; os_name == 'nt'", ] macros = [] From c3f655136628a2f88b5c16fb9dd61cedb79faeea Mon Sep 17 00:00:00 2001 From: Eli Wenig <83802262+elisw93@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:41:58 +0900 Subject: [PATCH 1361/1714] avoid raising the result of pytest.skip/pytest.fail (#2638) --- psutil/tests/__init__.py | 18 ++++++++-------- psutil/tests/test_bsd.py | 8 +++---- psutil/tests/test_contracts.py | 2 +- psutil/tests/test_linux.py | 26 +++++++++++------------ psutil/tests/test_misc.py | 4 ++-- psutil/tests/test_posix.py | 12 +++++------ psutil/tests/test_process.py | 36 +++++++++++++++++--------------- psutil/tests/test_process_all.py | 2 +- psutil/tests/test_scripts.py | 10 ++++----- psutil/tests/test_system.py | 14 ++++++------- psutil/tests/test_testutils.py | 6 +++--- psutil/tests/test_unicode.py | 6 +++--- psutil/tests/test_windows.py | 14 ++++++------- 13 files changed, 80 insertions(+), 78 deletions(-) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 462a3115c7..ba2f9e5985 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -946,7 +946,7 @@ 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 shutil.which("gcc"): - raise pytest.skip("gcc is not installed") + return pytest.skip("gcc is not installed") if c_code is None: c_code = textwrap.dedent(""" #include @@ -1060,7 +1060,7 @@ def assert_pid_gone(self, pid): try: psutil.Process(pid) except psutil.ZombieProcess: - raise pytest.fail("wasn't supposed to raise ZombieProcess") + return pytest.fail("wasn't supposed to raise ZombieProcess") assert cm.value.pid == pid assert cm.value.name is None assert not psutil.pid_exists(pid), pid @@ -1233,13 +1233,13 @@ def _check_fds(self, fun): f"negative diff {diff!r} (gc probably collected a" " resource from a previous test)" ) - raise pytest.fail(msg) + return pytest.fail(msg) if diff > 0: type_ = "fd" if POSIX else "handle" if diff > 1: type_ += "s" msg = f"{diff} unclosed {type_} after calling {fun!r}" - raise pytest.fail(msg) + return pytest.fail(msg) def _call_ntimes(self, fun, times): """Get 2 distinct memory samples, before and after having @@ -1273,14 +1273,14 @@ def _check_mem(self, fun, times, retries, tolerance): if success: if idx > 1: self._log(msg) - return + return None else: if idx == 1: print() # noqa: T201 self._log(msg) times += increase prev_mem = mem - raise pytest.fail(". ".join(messages)) + return pytest.fail(". ".join(messages)) # --- @@ -1320,7 +1320,7 @@ def call(): except exc: pass else: - raise pytest.fail(f"{fun} did not raise {exc}") + return pytest.fail(f"{fun} did not raise {exc}") self.execute(call, **kwargs) @@ -1587,7 +1587,7 @@ def wrapper(*args, **kwargs): if only_if is not None: if not only_if: raise - raise pytest.skip("raises AccessDenied") + return pytest.skip("raises AccessDenied") return wrapper @@ -1610,7 +1610,7 @@ def wrapper(*args, **kwargs): f"{fun.__name__!r} was skipped because it raised" " NotImplementedError" ) - raise pytest.skip(msg) + return pytest.skip(msg) return wrapper diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index e5597e699c..78542b15a3 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -116,9 +116,9 @@ def df(path): assert usage.total == total # 10 MB tolerance if abs(usage.free - free) > 10 * 1024 * 1024: - raise pytest.fail(f"psutil={usage.free}, df={free}") + return pytest.fail(f"psutil={usage.free}, df={free}") if abs(usage.used - used) > 10 * 1024 * 1024: - raise pytest.fail(f"psutil={usage.used}, df={used}") + return pytest.fail(f"psutil={usage.used}, df={used}") @pytest.mark.skipif( not shutil.which("sysctl"), reason="sysctl cmd not available" @@ -268,7 +268,7 @@ def test_cpu_frequency_against_sysctl(self): try: sysctl_result = int(sysctl(sensor)) except RuntimeError: - raise pytest.skip("frequencies not supported by kernel") + return pytest.skip("frequencies not supported by kernel") assert psutil.cpu_freq().current == sysctl_result sensor = "dev.cpu.0.freq_levels" @@ -466,7 +466,7 @@ def test_sensors_temperatures_against_sysctl(self): try: sysctl_result = int(float(sysctl(sensor)[:-1])) except RuntimeError: - raise pytest.skip("temperatures not supported by kernel") + return pytest.skip("temperatures not supported by kernel") assert ( abs( psutil.sensors_temperatures()["coretemp"][cpu].current diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 0e7aaca0b1..f174bdb917 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -226,7 +226,7 @@ def test_cpu_count(self): @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): if psutil.cpu_freq() is None: - raise pytest.skip("cpu_freq() returns None") + return pytest.skip("cpu_freq() returns None") self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int)) def test_disk_io_counters(self): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 5e325e5564..8db2b9f652 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -178,7 +178,7 @@ def vmstat(stat): def get_free_version_info(): out = sh(["free", "-V"]).strip() if 'UNKNOWN' in out: - raise pytest.skip("can't determine free version") + return pytest.skip("can't determine free version") return tuple(map(int, re.findall(r'\d+', out.split()[-1]))) @@ -237,12 +237,12 @@ def test_used(self): # This got changed in: # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e - # Newer versions of procps are using yet another way to compute used - # memory. + # Newer versions of procps (>=4.0.1) are using yet another way to + # compute used memory. # https://gitlab.com/procps-ng/procps/commit/ # 2184e90d2e7cdb582f9a5b706b47015e56707e4d - if get_free_version_info() < (4, 0, 0): - raise pytest.skip("free version too old") + if get_free_version_info() < (4, 0, 1): + return pytest.skip("free version too old") cli_value = free_physmem().used psutil_value = psutil.virtual_memory().used assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM @@ -258,7 +258,7 @@ def test_shared(self): free = free_physmem() free_value = free.shared if free_value == 0: - raise pytest.skip("free does not support 'shared' column") + return pytest.skip("free does not support 'shared' column") psutil_value = psutil.virtual_memory().shared assert ( abs(free_value - psutil_value) < TOLERANCE_SYS_MEM @@ -271,7 +271,7 @@ def test_available(self): out = sh(["free", "-b"]) lines = out.split('\n') if 'available' not in lines[0]: - raise pytest.skip("free does not support 'available' column") + return pytest.skip("free does not support 'available' column") free_value = int(lines[1].split()[-1]) psutil_value = psutil.virtual_memory().available assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM @@ -290,12 +290,12 @@ def test_used(self): # This got changed in: # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e - # Newer versions of procps are using yet another way to compute used - # memory. + # Newer versions of procps (>=4.0.1) are using yet another way to + # compute used memory. # https://gitlab.com/procps-ng/procps/commit/ # 2184e90d2e7cdb582f9a5b706b47015e56707e4d - if get_free_version_info() < (4, 0, 0): - raise pytest.skip("free version too old") + if get_free_version_info() < (4, 0, 1): + return pytest.skip("free version too old") vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM @@ -595,7 +595,7 @@ def test_meminfo_against_sysinfo(self): # matches sysinfo() syscall, see: # https://github.com/giampaolo/psutil/issues/1015 if not self.meminfo_has_swap_info(): - raise pytest.skip("/proc/meminfo has no swap metrics") + return pytest.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 @@ -1028,7 +1028,7 @@ def test_flags(self): assert ifconfig_flags == psutil_flags if not matches_found: - raise pytest.fail("no matches were found") + return pytest.fail("no matches were found") @pytest.mark.skipif(not LINUX, reason="LINUX only") diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 997bd18ebf..0ecae2f3b7 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -216,7 +216,7 @@ def test__all__(self): fun.__doc__ is not None and 'deprecated' not in fun.__doc__.lower() ): - raise pytest.fail( + return pytest.fail( f"{name!r} not in psutil.__all__" ) @@ -847,7 +847,7 @@ def test_cache_clear(self): @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") def test_cache_clear_public_apis(self): if not psutil.disk_io_counters() or not psutil.net_io_counters(): - raise pytest.skip("no disks or NICs available") + return pytest.skip("no disks or NICs available") psutil.disk_io_counters() psutil.net_io_counters() caches = wrap_numbers.cache_info() diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index c220524bd6..69df690089 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -133,7 +133,7 @@ def df(device): out = sh(f"df -k {device}").strip() except RuntimeError as err: if "device busy" in str(err).lower(): - raise pytest.skip("df returned EBUSY") + return pytest.skip("df returned EBUSY") raise line = out.split('\n')[1] fields = line.split() @@ -337,7 +337,7 @@ def test_pids(self): 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 pytest.fail("difference: " + str(difference)) + return pytest.fail("difference: " + str(difference)) # for some reason ifconfig -a does not report all interfaces # returned by psutil @@ -351,7 +351,7 @@ def test_nic_names(self): if line.startswith(nic): break else: - raise pytest.fail( + return pytest.fail( f"couldn't find {nic} nic in 'ifconfig -a'" f" output\n{output}" ) @@ -360,7 +360,7 @@ def test_nic_names(self): def test_users(self): out = sh("who -u") if not out.strip(): - raise pytest.skip("no users on this system") + return pytest.skip("no users on this system") susers = [] for line in out.splitlines(): @@ -391,7 +391,7 @@ def test_users(self): def test_users_started(self): out = sh("who -u") if not out.strip(): - raise pytest.skip("no users on this system") + return pytest.skip("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) @@ -415,7 +415,7 @@ def test_users_started(self): started = [x.capitalize() for x in started] if not tstamp: - raise pytest.skip(f"cannot interpret tstamp in who output\n{out}") + return pytest.skip(f"cannot interpret tstamp in who output\n{out}") with self.subTest(psutil=psutil.users(), who=out): for idx, u in enumerate(psutil.users()): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 264f3ed639..4b2d77fc73 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -221,7 +221,7 @@ def test_wait_timeout_nonblocking(self): except psutil.TimeoutExpired: pass else: - raise pytest.fail('timeout') + return pytest.fail('timeout') if POSIX: assert code == -signal.SIGKILL else: @@ -295,7 +295,7 @@ def test_terminal(self): tty = os.path.realpath(sh('tty')) except RuntimeError: # Note: happens if pytest is run without the `-s` opt. - raise pytest.skip("can't rely on `tty` CLI") + return pytest.skip("can't rely on `tty` CLI") else: assert terminal == tty @@ -507,7 +507,7 @@ def test_num_threads(self): try: step1 = p.num_threads() except psutil.AccessDenied: - raise pytest.skip("on OpenBSD this requires root access") + return pytest.skip("on OpenBSD this requires root access") else: step1 = p.num_threads() @@ -528,7 +528,7 @@ def test_threads(self): try: step1 = p.threads() except psutil.AccessDenied: - raise pytest.skip("on OpenBSD this requires root access") + return pytest.skip("on OpenBSD this requires root access") else: step1 = p.threads() @@ -550,7 +550,7 @@ def test_threads_2(self): try: p.threads() except psutil.AccessDenied: - raise pytest.skip("on OpenBSD this requires root access") + return pytest.skip("on OpenBSD this requires root access") assert ( abs(p.cpu_times().user - sum(x.user_time for x in p.threads())) < 0.1 @@ -722,7 +722,7 @@ def test_cmdline(self): if NETBSD and p.cmdline() == []: # https://github.com/giampaolo/psutil/issues/2250 - raise pytest.skip("OPENBSD: returned EBUSY") + return pytest.skip("OPENBSD: returned EBUSY") # XXX - most of the times the underlying sysctl() call on Net # and Open BSD returns a truncated string. @@ -736,7 +736,7 @@ def test_cmdline(self): pyexe = p.cmdline()[0] if pyexe != PYTHON_EXE: assert ' '.join(p.cmdline()[1:]) == ' '.join(cmdline[1:]) - return + return None assert ' '.join(p.cmdline()) == ' '.join(cmdline) def test_long_cmdline(self): @@ -757,12 +757,12 @@ def test_long_cmdline(self): try: assert p.cmdline()[1:] == cmdline except psutil.ZombieProcess: - raise pytest.skip("OPENBSD: process turned into zombie") + return pytest.skip("OPENBSD: process turned into zombie") else: ret = p.cmdline()[1:] if NETBSD and ret == []: # https://github.com/giampaolo/psutil/issues/2250 - raise pytest.skip("OPENBSD: returned EBUSY") + return pytest.skip("OPENBSD: returned EBUSY") assert ret == cmdline def test_name(self): @@ -904,7 +904,7 @@ def test_username(self): # 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 pytest.skip('running as service account') + return pytest.skip('running as service account') assert username == getpass_user if 'USERDOMAIN' in os.environ: assert domain == os.environ['USERDOMAIN'] @@ -1055,7 +1055,7 @@ def test_open_files_2(self): ): break else: - raise pytest.fail(f"no file found; files={p.open_files()!r}") + return pytest.fail(f"no file found; files={p.open_files()!r}") assert normcase(file.path) == normcase(fileobj.name) if WINDOWS: assert file.fd == -1 @@ -1091,8 +1091,10 @@ def test_num_ctx_switches(self): time.sleep(0.05) # this shall ensure a context switch happens after = sum(p.num_ctx_switches()) if after > before: - return - raise pytest.fail("num ctx switches still the same after 2 iterations") + return None + return pytest.fail( + "num ctx switches still the same after 2 iterations" + ) def test_ppid(self): p = psutil.Process() @@ -1198,7 +1200,7 @@ def test_children_duplicates(self): # this is the one, now let's make sure there are no duplicates pid = max(table.items(), key=lambda x: x[1])[0] if LINUX and pid == 0: - raise pytest.skip("PID 0") + return pytest.skip("PID 0") p = psutil.Process(pid) try: c = p.children(recursive=True) @@ -1353,13 +1355,13 @@ def assert_raises_nsp(fun, fun_name): pass except psutil.AccessDenied: if OPENBSD and fun_name in {'threads', 'num_threads'}: - return + return None raise else: # NtQuerySystemInformation succeeds even if process is gone. if WINDOWS and fun_name in {'exe', 'name'}: - return - raise pytest.fail( + return None + return pytest.fail( f"{fun!r} didn't raise NSP and returned {ret!r} instead" ) diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index f7d1e32e03..3398c47ee3 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -147,7 +147,7 @@ def test_all(self): if value not in (0, 0.0, [], None, '', {}): assert value, value if failures: - raise pytest.fail(''.join(failures)) + return pytest.fail(''.join(failures)) def cmdline(self, ret, info): assert isinstance(ret, list) diff --git a/psutil/tests/test_scripts.py b/psutil/tests/test_scripts.py index 48fb6eb3b9..40fc591519 100755 --- a/psutil/tests/test_scripts.py +++ b/psutil/tests/test_scripts.py @@ -76,7 +76,7 @@ def test_coverage(self): if name.endswith('.py'): if 'test_' + os.path.splitext(name)[0] not in meths: # self.assert_stdout(name) - raise pytest.fail( + return pytest.fail( "no test defined for" f" {os.path.join(SCRIPTS_DIR, name)!r} script" ) @@ -88,7 +88,7 @@ def test_executable(self): if file.endswith('.py'): path = os.path.join(root, file) if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]: - raise pytest.fail(f"{path!r} is not executable") + return pytest.fail(f"{path!r} is not executable") def test_disk_usage(self): self.assert_stdout('disk_usage.py') @@ -124,7 +124,7 @@ def test_pmap(self): def test_procsmem(self): if 'uss' not in psutil.Process().memory_full_info()._fields: - raise pytest.skip("not supported") + return pytest.skip("not supported") self.assert_stdout('procsmem.py') def test_killall(self): @@ -153,13 +153,13 @@ def test_cpu_distribution(self): @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") def test_temperatures(self): if not psutil.sensors_temperatures(): - raise pytest.skip("no temperatures") + return pytest.skip("no temperatures") self.assert_stdout('temperatures.py') @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") def test_fans(self): if not psutil.sensors_fans(): - raise pytest.skip("no fans") + return pytest.skip("no fans") self.assert_stdout('fans.py') @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 1a674cb8d3..bd56e1718c 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -322,9 +322,9 @@ def test_virtual_memory(self): assert isinstance(value, int) if name != 'total': if not value >= 0: - raise pytest.fail(f"{name!r} < 0 ({value})") + return pytest.fail(f"{name!r} < 0 ({value})") if value > mem.total: - raise pytest.fail( + return pytest.fail( f"{name!r} > total (total={mem.total}, {name}={value})" ) @@ -362,13 +362,13 @@ def test_cpu_count_logical(self): with open("/proc/cpuinfo") as fd: cpuinfo_data = fd.read() if "physical id" not in cpuinfo_data: - raise pytest.skip("cpuinfo doesn't include physical id") + return pytest.skip("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 pytest.skip("cpu_count_cores() is None") + return pytest.skip("cpu_count_cores() is None") if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista assert cores is None else: @@ -424,8 +424,8 @@ def test_cpu_times_time_increases(self): while time.time() < stop_at: t2 = sum(psutil.cpu_times()) if t2 > t1: - return - raise pytest.fail("time remained the same") + return None + return pytest.fail("time remained the same") def test_per_cpu_times(self): # Check type, value >= 0, str(). @@ -591,7 +591,7 @@ def check_ls(ls): ls = psutil.cpu_freq(percpu=True) if (FREEBSD or AARCH64) and not ls: - raise pytest.skip( + return pytest.skip( "returns empty list on FreeBSD and Linux aarch64" ) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 2bdb2614f9..14a241f29c 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -464,7 +464,7 @@ def test_raises(self): except AssertionError as err: assert str(err) == '"foo" does not match "bar"' # noqa: PT017 else: - raise pytest.fail("exception not raised") + return pytest.fail("exception not raised") def test_mark(self): @fake_pytest.mark.xdist_group(name="serial") @@ -540,7 +540,7 @@ def test_warns(self): except AssertionError: pass else: - raise pytest.fail("exception not raised") + return pytest.fail("exception not raised") # match success with fake_pytest.warns(UserWarning, match="foo"): @@ -553,7 +553,7 @@ def test_warns(self): except AssertionError: pass else: - raise pytest.fail("exception not raised") + return pytest.fail("exception not raised") def test_fail(self): with fake_pytest.raises(fake_pytest.fail.Exception): diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 070efe4db5..8c8e3d605e 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -146,7 +146,7 @@ def setUpClass(cls): def setUp(self): super().setUp() if self.skip_tests: - raise pytest.skip("can't handle unicode str") + return pytest.skip("can't handle unicode str") @pytest.mark.xdist_group(name="serial") @@ -226,7 +226,7 @@ def test_proc_open_files(self): assert isinstance(path, str) if BSD and not path: # XXX - see https://github.com/giampaolo/psutil/issues/595 - raise pytest.skip("open_files on BSD is broken") + return pytest.skip("open_files on BSD is broken") if self.expect_exact_path_match(): assert os.path.normcase(path) == os.path.normcase(self.funky_name) @@ -241,7 +241,7 @@ def test_proc_net_connections(self): conn = psutil.Process().net_connections('unix')[0] assert isinstance(conn.laddr, str) if not conn.laddr and MACOS and CI_TESTING: - raise pytest.skip("unreliable on OSX") + return pytest.skip("unreliable on OSX") assert conn.laddr == name @pytest.mark.skipif(not POSIX, reason="POSIX only") diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 8950840876..88c5fb754d 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -63,7 +63,7 @@ def powershell(cmd): "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") """ if not shutil.which("powershell.exe"): - raise pytest.skip("powershell.exe not available") + return pytest.skip("powershell.exe not available") cmdline = ( "powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive " f"-NoProfile -WindowStyle Hidden -Command \"{cmd}\"" # noqa: Q003 @@ -141,7 +141,7 @@ def test_nic_names(self): if "pseudo-interface" in nic.replace(' ', '-').lower(): continue if nic not in out: - raise pytest.fail( + return pytest.fail( f"{nic!r} nic wasn't found in 'ipconfig /all' output" ) @@ -222,12 +222,12 @@ def test_disks(self): assert usage.free == wmi_free # 10 MB tolerance if abs(usage.free - wmi_free) > 10 * 1024 * 1024: - raise pytest.fail( + return pytest.fail( f"psutil={usage.free}, wmi={wmi_free}" ) break else: - raise pytest.fail(f"can't find partition {ps_part!r}") + return pytest.fail(f"can't find partition {ps_part!r}") @retry_on_failure() def test_disk_usage(self): @@ -455,7 +455,7 @@ def test_username(self): # 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 pytest.skip('running as service account') + return pytest.skip('running as service account') assert psutil.Process().username() == name def test_cmdline(self): @@ -651,7 +651,7 @@ def test_memory_vms(self): # returned instead. wmi_usage = int(w.PageFileUsage) if vms not in {wmi_usage, wmi_usage * 1024}: - raise pytest.fail(f"wmi={wmi_usage}, psutil={vms}") + return pytest.fail(f"wmi={wmi_usage}, psutil={vms}") def test_create_time(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] @@ -787,7 +787,7 @@ def setUp(self): other_python = self.find_other_interpreter() if other_python is None: - raise pytest.skip( + return pytest.skip( "could not find interpreter with opposite bitness" ) if IS_64BIT: From 567b6c993a06769bcfcbd9fe19e3fe092fb37bcb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 15 Oct 2025 18:04:10 +0200 Subject: [PATCH 1362/1714] CI: rename ci-* Makefile target --- .dprint.jsonc | 2 +- .github/workflows/bsd.yml | 6 ++-- .github/workflows/build.yml | 8 ++--- Makefile | 61 ++++++++++++++++++++++--------------- pyproject.toml | 2 +- 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/.dprint.jsonc b/.dprint.jsonc index ccfcf2ddf4..3500bd32d1 100644 --- a/.dprint.jsonc +++ b/.dprint.jsonc @@ -23,7 +23,7 @@ ".github/PULL_REQUEST_TEMPLATE.md", ], "plugins": [ - "https://plugins.dprint.dev/markdown-0.18.0.wasm", + "https://plugins.dprint.dev/markdown-0.19.0.wasm", "https://plugins.dprint.dev/json-0.20.0.wasm", "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm", ], diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 411a451050..b41d7216aa 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -20,7 +20,7 @@ jobs: with: usesh: true run: | - make test-ci + make ci-test openbsd: # if: false @@ -32,7 +32,7 @@ jobs: with: usesh: true run: | - make test-ci + make ci-test netbsd: if: false # XXX: disabled @@ -44,4 +44,4 @@ jobs: with: usesh: true run: | - make test-ci + make ci-test diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b05da3568f..eef99e427d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,7 +88,7 @@ jobs: python-version: 3.x - name: "Run linters" run: | - make lint-ci + make ci-lint # Produce wheels as artifacts. upload-wheels: @@ -116,8 +116,4 @@ jobs: name: wheels path: wheelhouse - run: | - python3 -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit - make sdist - mv wheelhouse/* dist/ - make print-hashes - make check-distribution + make ci-check-dist diff --git a/Makefile b/Makefile index 001e02e521..a701e10f30 100644 --- a/Makefile +++ b/Makefile @@ -164,29 +164,6 @@ test-sudo: ## Run tests requiring root privileges. # Use unittest runner because pytest may not be installed as root. $(SUDO) $(PYTHON_ENV_VARS) $(PYTHON) -m unittest -v psutil.tests.test_sudo -test-ci: ## Run tests on GitHub CI. - ${MAKE} install-sysdeps - PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test - ${MAKE} print-sysinfo - $(PYTHON) -m pip list - ${MAKE} test - ${MAKE} test-memleaks - ${MAKE} test-sudo - -test-cibuildwheel: ## Run tests from cibuildwheel. - # testing the wheels means we can't use other test targets which are rebuilding the python extensions - # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed - ${MAKE} install-sysdeps - ${MAKE} print-sysinfo - mkdir -p .tests - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k test_memleaks.py" $(PYTHON) -m pytest --pyargs psutil.tests - -lint-ci: ## Run all linters on GitHub CI. - python3 -m pip install -U black ruff rstcheck toml-sort sphinx - curl -fsSL https://dprint.dev/install.sh | sh - ${MAKE} lint-all - # =================================================================== # Linters # =================================================================== @@ -247,6 +224,40 @@ fix-all: ## Run all code fixers. ${MAKE} fix-toml ${MAKE} fix-dprint +# =================================================================== +# CI jobs +# =================================================================== + +ci-lint: ## Run all linters on GitHub CI. + $(PYTHON) -m pip install -U black ruff rstcheck toml-sort sphinx + curl -fsSL https://dprint.dev/install.sh | sh + ${MAKE} lint-all + +ci-test: ## Run tests on GitHub CI. Used by BSD runners. + ${MAKE} install-sysdeps + PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test + ${MAKE} print-sysinfo + $(PYTHON) -m pip list + ${MAKE} test + ${MAKE} test-memleaks + ${MAKE} test-sudo + +ci-test-cibuildwheel: ## Run tests from cibuildwheel. + # testing the wheels means we can't use other test targets which are rebuilding the python extensions + # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed + ${MAKE} install-sysdeps + ${MAKE} print-sysinfo + mkdir -p .tests + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests + cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k test_memleaks.py" $(PYTHON) -m pytest --pyargs psutil.tests + +ci-check-dist: ## Run all sanity checks re. to the package distribution. + $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit + make sdist + mv wheelhouse/* dist/ + make print-hashes + make check-dist + # =================================================================== # Distribution # =================================================================== @@ -267,7 +278,7 @@ check-wheels: ## Check sanity of wheels. $(PYTHON) -m abi3audit --verbose --strict dist/*-abi3-*.whl $(PYTHON) -m twine check --strict dist/*.whl -check-distribution: ## Run all sanity checks re. to the package distribution. +check-dist: ## Run all sanity checks re. to the package distribution. ${MAKE} check-manifest ${MAKE} check-pyproject ${MAKE} check-sdist @@ -302,7 +313,7 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in history, '%r not found in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX found in HISTORY.rst';" ${MAKE} download-wheels - ${MAKE} check-distribution + ${MAKE} check-dist ${MAKE} print-hashes ${MAKE} print-dist diff --git a/pyproject.toml b/pyproject.toml index 8bc9cc2a44..08c2e747fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,7 +231,7 @@ test-skip = [ "cp3{6,7,8,9,10,11,12}-*", # Only run tests for minor (3.8) and major (3.13) Python versions ] test-extras = ["test"] -test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" test-cibuildwheel" +test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" ci-test-cibuildwheel" [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] From c4dd48a7788bcc4a680edef6eb28198e53710076 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 18 Oct 2025 17:46:03 +0200 Subject: [PATCH 1363/1714] Rename psutil_get_proc_list() to _psutil_pids() --- psutil/arch/bsd/proc.c | 31 ++++++++++++++++++------ psutil/arch/freebsd/init.h | 5 ++-- psutil/arch/freebsd/proc.c | 13 ++++------ psutil/arch/netbsd/init.h | 7 +++--- psutil/arch/netbsd/proc.c | 41 ++++++++++++-------------------- psutil/arch/openbsd/init.h | 2 +- psutil/arch/openbsd/proc.c | 2 +- psutil/arch/osx/proc.c | 22 ++++++++--------- psutil/arch/windows/init.h | 2 +- psutil/arch/windows/proc.c | 3 ++- psutil/arch/windows/proc_utils.c | 6 ++--- 11 files changed, 67 insertions(+), 67 deletions(-) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 32354dfd55..3d83ae2f93 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -65,8 +65,13 @@ kinfo_getfile(pid_t pid, int *cnt) { */ PyObject * psutil_pids(PyObject *self, PyObject *args) { - kinfo_proc *proclist = NULL; - kinfo_proc *orig_address = NULL; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 *proclist = NULL; + struct kinfo_proc2 *orig_address = NULL; +#else + struct kinfo_proc *proclist = NULL; + struct kinfo_proc *orig_address = NULL; +#endif size_t num_processes; size_t idx; PyObject *py_retlist = PyList_New(0); @@ -75,7 +80,7 @@ psutil_pids(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (psutil_get_proc_list(&proclist, &num_processes) != 0) + if (_psutil_pids(&proclist, &num_processes) != 0) goto error; if (num_processes > 0) { @@ -83,7 +88,7 @@ psutil_pids(PyObject *self, PyObject *args) { for (idx = 0; idx < num_processes; idx++) { #ifdef PSUTIL_FREEBSD py_pid = PyLong_FromPid(proclist->ki_pid); -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +#else py_pid = PyLong_FromPid(proclist->p_pid); #endif if (!py_pid) @@ -120,7 +125,11 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { long memdata; long memstack; int oncpu; - kinfo_proc kp; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kp; +#else + struct kinfo_proc kp; +#endif long pagesize = psutil_getpagesize(); char str[1000]; PyObject *py_name; @@ -287,7 +296,11 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { PyObject * psutil_proc_name(PyObject *self, PyObject *args) { pid_t pid; - kinfo_proc kp; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kp; +#else + struct kinfo_proc kp; +#endif char str[1000]; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) @@ -448,7 +461,11 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { char *path; struct kinfo_file *freep = NULL; struct kinfo_file *kif; - kinfo_proc kipp; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kipp; +#else + struct kinfo_proc kipp; +#endif PyObject *py_tuple = NULL; PyObject *py_path = NULL; PyObject *py_retlist = PyList_New(0); diff --git a/psutil/arch/freebsd/init.h b/psutil/arch/freebsd/init.h index 842f1419a7..58d97ab6fe 100644 --- a/psutil/arch/freebsd/init.h +++ b/psutil/arch/freebsd/init.h @@ -6,11 +6,10 @@ */ #include - -typedef struct kinfo_proc kinfo_proc; +#include // TODO: move this stuff. Does not belong here -int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); +int _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count); int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index d973728067..9e2d436b0f 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -73,21 +73,16 @@ static void psutil_remove_spaces(char *str) { // ============================================================================ -int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { +int _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; size_t length = 0; char *buf = NULL; - assert(procList != NULL); - assert(*procList == NULL); - assert(procCount != NULL); - - if (psutil_sysctl_malloc(mib, 4, &buf, &length) != 0) { + if (psutil_sysctl_malloc(mib, 4, &buf, &length) != 0) return 1; - } - *procList = (struct kinfo_proc *)buf; - *procCount = length / sizeof(struct kinfo_proc); + *proc_list = (struct kinfo_proc *)buf; + *proc_count = length / sizeof(struct kinfo_proc); return 0; } diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h index fb0d4ad5f8..da41064b55 100644 --- a/psutil/arch/netbsd/init.h +++ b/psutil/arch/netbsd/init.h @@ -7,12 +7,11 @@ */ #include - -typedef struct kinfo_proc2 kinfo_proc; +#include // TODO: refactor this. Does not belong here. -int psutil_kinfo_proc(pid_t pid, kinfo_proc *proc); -int psutil_get_proc_list(kinfo_proc **procList, size_t *procCount); +int psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc); +int _psutil_pids(struct kinfo_proc2 **proc_list, size_t *proc_count); char *psutil_get_cmd_args(pid_t pid, size_t *argsize); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index ebc25581e2..a1cd2c9bf8 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -24,11 +24,11 @@ int -psutil_kinfo_proc(pid_t pid, kinfo_proc *proc) { +psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc) { // Fills a kinfo_proc struct based on process pid. int ret; int mib[6]; - size_t size = sizeof(kinfo_proc); + size_t size = sizeof(struct kinfo_proc2); mib[0] = CTL_KERN; mib[1] = KERN_PROC2; @@ -154,9 +154,9 @@ psutil_proc_exe(PyObject *self, PyObject *args) { PyObject * psutil_proc_num_threads(PyObject *self, PyObject *args) { - // Return number of threads used by process as a Python integer. long pid; - kinfo_proc kp; + struct kinfo_proc2 kp; + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) @@ -241,53 +241,42 @@ psutil_proc_threads(PyObject *self, PyObject *args) { int -psutil_get_proc_list(kinfo_proc **procList, size_t *procCount) { - // Returns a list of all BSD processes on the system. This routine - // allocates the list and puts it in *procList and a count of the - // number of entries in *procCount. You are responsible for freeing - // this list (use "free" from System framework). - // On success, the function returns 0. - // On error, the function returns a BSD errno value. - kinfo_proc *result; +_psutil_pids(struct kinfo_proc2 **proc_list, size_t *proc_count) { + struct kinfo_proc2 *result; // Declaring name as const requires us to cast it when passing it to // sysctl because the prototype doesn't include the const modifier. char errbuf[_POSIX2_LINE_MAX]; int cnt; kvm_t *kd; - - assert( procList != NULL); - assert(*procList == NULL); - assert(procCount != NULL); + size_t mlen; kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); - if (kd == NULL) { PyErr_Format( - PyExc_RuntimeError, "kvm_openfiles() syscall failed: %s", errbuf); + PyExc_RuntimeError, "kvm_openfiles() syscall failed: %s", errbuf + ); return 1; } - result = kvm_getproc2(kd, KERN_PROC_ALL, 0, sizeof(kinfo_proc), &cnt); + result = kvm_getproc2(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt); if (result == NULL) { PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() syscall failed"); kvm_close(kd); return 1; } - *procCount = (size_t)cnt; - - size_t mlen = cnt * sizeof(kinfo_proc); + *proc_count = (size_t)cnt; - if ((*procList = malloc(mlen)) == NULL) { + mlen = cnt * sizeof(struct kinfo_proc2); + if ((*proc_list = malloc(mlen)) == NULL) { PyErr_NoMemory(); kvm_close(kd); return 1; } - memcpy(*procList, result, mlen); - assert(*procList != NULL); + memcpy(*proc_list, result, mlen); + assert(*proc_list != NULL); kvm_close(kd); - return 0; } diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index 3ffcb44473..e21b301e1c 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -10,7 +10,7 @@ // TODO: move / refactor this stuff. It does not belong in here. typedef struct kinfo_proc kinfo_proc; int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); -int psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount); +int _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 8b320f5eee..87cc57c0b3 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -57,7 +57,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // ============================================================================ int -psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { +_psutil_pids(struct kinfo_proc **procList, size_t *procCount) { // Returns a list of all BSD processes on the system. This routine // allocates the list and puts it in *procList and a count of the // number of entries in *procCount. You are responsible for freeing diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index c36609df3f..57629a1fc8 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -33,7 +33,6 @@ #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) -typedef struct kinfo_proc kinfo_proc; // ==================================================================== @@ -42,7 +41,7 @@ typedef struct kinfo_proc kinfo_proc; static int -psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { +_psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { int mib[3]; size_t len = 0; char *buf = NULL; @@ -50,16 +49,16 @@ psutil_get_proc_list(struct kinfo_proc **procList, size_t *procCount) { mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_ALL; - *procList = NULL; - *procCount = 0; + *proc_list = NULL; + *proc_count = 0; if (psutil_sysctl_malloc(mib, 3, &buf, &len) != 0) return 1; - *procList = (struct kinfo_proc *)buf; - *procCount = len / sizeof(struct kinfo_proc); + *proc_list = (struct kinfo_proc *)buf; + *proc_count = len / sizeof(struct kinfo_proc); - if (*procCount == 0) { + if (*proc_count == 0) { free(buf); PyErr_Format(PyExc_RuntimeError, "no PIDs found"); return 1; @@ -176,8 +175,9 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) err = task_for_pid(mach_task_self(), pid, task); if (err != KERN_SUCCESS) { - if (psutil_pid_exists(pid) == 0) + if (psutil_pid_exists(pid) == 0) { NoSuchProcess("task_for_pid"); + } // Now done in Python. // else if (psutil_is_zombie(pid) == 1) // PyErr_SetString(ZombieProcessError, @@ -271,8 +271,8 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { */ PyObject * psutil_pids(PyObject *self, PyObject *args) { - kinfo_proc *proclist = NULL; - kinfo_proc *orig_address = NULL; + struct kinfo_proc *proclist = NULL; + struct kinfo_proc *orig_address = NULL; size_t num_processes; size_t idx; PyObject *py_pid = NULL; @@ -281,7 +281,7 @@ psutil_pids(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (psutil_get_proc_list(&proclist, &num_processes) != 0) + if (_psutil_pids(&proclist, &num_processes) != 0) goto error; // save the address of proclist so we can free it later diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index a60ee8255c..ffe275b895 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -72,7 +72,7 @@ PyObject *TimeoutExpired; PyObject *TimeoutAbandoned; -DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); +DWORD* _psutil_pids(DWORD *numberOfReturnedPIDs); HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code); HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); int psutil_assert_pid_exists(DWORD pid, char *err); diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 55fc4bc6df..02fe5a3744 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -62,7 +62,8 @@ psutil_pids(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - proclist = psutil_get_pids(&numberOfReturnedPIDs); + + proclist = _psutil_pids(&numberOfReturnedPIDs); if (proclist == NULL) goto error; diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index 778e71d2a7..e558afcf64 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -14,7 +14,7 @@ DWORD * -psutil_get_pids(DWORD *numberOfReturnedPIDs) { +_psutil_pids(DWORD *numberOfReturnedPIDs) { // Win32 SDK says the only way to know if our process array // wasn't large enough is to check the returned size and make // sure that it doesn't match the size of the array. @@ -59,9 +59,9 @@ psutil_pid_in_pids(DWORD pid) { DWORD numberOfReturnedPIDs; DWORD i; - proclist = psutil_get_pids(&numberOfReturnedPIDs); + proclist = _psutil_pids(&numberOfReturnedPIDs); if (proclist == NULL) { - psutil_debug("psutil_get_pids() failed"); + psutil_debug("_psutil_pids() failed"); return -1; } for (i = 0; i < numberOfReturnedPIDs; i++) { From 8aca550dd16ec6b78d0160471bda4d24fe7e4d21 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 19 Oct 2025 13:47:50 +0200 Subject: [PATCH 1364/1714] [SunOS] add CI test runner for SunOS (#2646) --- .github/workflows/sunos.yml | 39 +++++++++++++++++++++++++++++ HISTORY.rst | 4 +++ Makefile | 2 +- scripts/internal/install-sysdeps.sh | 6 +++++ scripts/internal/install_pip.py | 5 +++- 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/sunos.yml diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml new file mode 100644 index 0000000000..bd022c5688 --- /dev/null +++ b/.github/workflows/sunos.yml @@ -0,0 +1,39 @@ +# Execute tests on SunOS +# https://github.com/vmactions/solaris-vm + +name: sunos-tests +on: + push: + # only run this job if the following files are modified + paths: &sunos_paths + - ".github/workflows/sunos.yml" + - "psutil/_pssunos.py" + - "psutil/_psutil_posix.c" + - "psutil/_psutil_sunos.c" + - "psutil/arch/all/**" + - "psutil/arch/posix/**" + - "psutil/arch/sunos/**" + - "setup.py" + pull_request: + paths: *sunos_paths +concurrency: + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} + cancel-in-progress: true +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run tests + id: test + uses: vmactions/solaris-vm@v1 + with: + release: "11.4-gcc" + usesh: true + run: | + set -x + python3 setup.py build_ext -i --parallel 4 + python3 -c "import psutil" + python3 scripts/internal/install_pip.py + python3 -m pip install --break-system-packages --user pytest pytest-instafail pytest-subtests pytest-xdist + python3 -m pytest psutil/tests/test_memleaks.py diff --git a/HISTORY.rst b/HISTORY.rst index 1c07396705..b317855842 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,10 @@ XXXX-XX-XX +**Enhancements** + +* 2646_, [SunOS]: add CI test runner for SunOS. + **Bug fixes** - 2641_, [SunOS]: cannot compile psutil from sources due to missing C include. diff --git a/Makefile b/Makefile index a701e10f30..7581120158 100644 --- a/Makefile +++ b/Makefile @@ -175,7 +175,7 @@ black: ## Run black formatter. @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe dprint: - @$(DPRINT) check --list-different + @$(DPRINT) check lint-c: ## Run C linter. @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 6c58f3831f..714d3b47a1 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -30,6 +30,10 @@ case "$UNAME_S" in OpenBSD) OPENBSD=true ;; + SunOS) + SUNOS=true + ;; + esac # Check if running as root @@ -60,6 +64,8 @@ main() { $SUDO pkgin -y install python311-* gcc12-* elif [ $OPENBSD ]; then $SUDO pkg_add gcc python3 + elif [ $SUNOS ]; then + $SUDO pkg install developer/gcc else echo "Unsupported platform '$UNAME_S'. Ignoring." fi diff --git a/scripts/internal/install_pip.py b/scripts/internal/install_pip.py index e74f0ec2fd..34bda5775d 100755 --- a/scripts/internal/install_pip.py +++ b/scripts/internal/install_pip.py @@ -39,7 +39,10 @@ def main(): f.flush() print("download finished, installing pip") - code = os.system(f"{sys.executable} {f.name} --user --upgrade") + code = os.system( + f"{sys.executable} {f.name} --user --upgrade" + " --break-system-packages" + ) sys.exit(code) From 49b56c2ae1299e1157ce8126ae8e3aad36750e37 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 19 Oct 2025 14:26:57 +0200 Subject: [PATCH 1365/1714] [SunOS] Drop SunOS 10 support (#2647) --- HISTORY.rst | 8 +- MANIFEST.in | 2 - docs/index.rst | 1 + psutil/__init__.py | 2 +- psutil/_psutil_sunos.c | 1 + psutil/arch/posix/net.c | 17 +---- psutil/arch/sunos/net.c | 20 ----- psutil/arch/sunos/v10/ifaddrs.c | 125 -------------------------------- psutil/arch/sunos/v10/ifaddrs.h | 26 ------- setup.py | 22 ------ 10 files changed, 11 insertions(+), 213 deletions(-) delete mode 100644 psutil/arch/sunos/v10/ifaddrs.c delete mode 100644 psutil/arch/sunos/v10/ifaddrs.h diff --git a/HISTORY.rst b/HISTORY.rst index b317855842..1a9709bf9c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,12 +8,18 @@ XXXX-XX-XX **Enhancements** +* 2645_, [SunOS]: dropped support for SunOS 10. * 2646_, [SunOS]: add CI test runner for SunOS. **Bug fixes** - 2641_, [SunOS]: cannot compile psutil from sources due to missing C include. -- 2357_, [SunOS]: `Process.cmdline()`_ does not handle spaces properly. (patch by Ben Raz) +- 2357_, [SunOS]: `Process.cmdline()`_ does not handle spaces properly. (patch + by Ben Raz) + +**Compatibility notes** + +* 2645_: SunOS 10 is no longer supported. 7.1.0 ===== diff --git a/MANIFEST.in b/MANIFEST.in index 7fb192e65a..ce9091ddc3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -105,8 +105,6 @@ include psutil/arch/sunos/mem.c include psutil/arch/sunos/net.c include psutil/arch/sunos/proc.c include psutil/arch/sunos/sys.c -include psutil/arch/sunos/v10/ifaddrs.c -include psutil/arch/sunos/v10/ifaddrs.h include psutil/arch/windows/cpu.c include psutil/arch/windows/disk.c include psutil/arch/windows/init.c diff --git a/docs/index.rst b/docs/index.rst index 4b29f6f8d7..d7c39f9515 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2676,6 +2676,7 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= +* psutil 7.1.1 (2025-10): drop **SunOS 10** * psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 * psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 diff --git a/psutil/__init__.py b/psutil/__init__.py index 23d47ebd27..5798a54708 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -205,7 +205,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.1.0" +__version__ = "7.1.1" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index eac3544be4..97da6a8f92 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -15,6 +15,7 @@ */ #define _STRUCTURED_PROC 1 +#define NEW_MIB_COMPLIANT 1 #include diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index abee189678..62ea184aef 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -13,9 +13,7 @@ #include #include -#ifdef PSUTIL_SUNOS10 - #include "arch/solaris/v10/ifaddrs.h" -#elif PSUTIL_AIX +#ifdef PSUTIL_AIX #include "arch/aix/ifaddrs.h" #else #include @@ -224,11 +222,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { char *nic_name; int sock = -1; int ret; -#ifdef PSUTIL_SUNOS10 - struct lifreq lifr; -#else struct ifreq ifr; -#endif if (! PyArg_ParseTuple(args, "s", &nic_name)) return NULL; @@ -237,22 +231,13 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { if (sock == -1) goto error; -#ifdef PSUTIL_SUNOS10 - PSUTIL_STRNCPY(lifr.lifr_name, nic_name, sizeof(lifr.lifr_name)); - ret = ioctl(sock, SIOCGIFMTU, &lifr); -#else PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); ret = ioctl(sock, SIOCGIFMTU, &ifr); -#endif if (ret == -1) goto error; close(sock); -#ifdef PSUTIL_SUNOS10 - return Py_BuildValue("i", lifr.lifr_mtu); -#else return Py_BuildValue("i", ifr.ifr_mtu); -#endif error: if (sock != -1) diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c index 0bde5e3c8b..1d1432b6db 100644 --- a/psutil/arch/sunos/net.c +++ b/psutil/arch/sunos/net.c @@ -317,11 +317,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { mibhdr.level = MIB2_IP; mibhdr.name = 0; -#ifdef NEW_MIB_COMPLIANT mibhdr.len = 1; -#else - mibhdr.len = 0; -#endif memcpy(buf, &tor, sizeof tor); memcpy(buf + tor.OPT_offset, &mibhdr, sizeof mibhdr); @@ -387,11 +383,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { num_ent = mibhdr.len / sizeof(mib2_tcpConnEntry_t); for (i = 0; i < num_ent; i++) { memcpy(&tp, databuf.buf + i * sizeof tp, sizeof tp); -#ifdef NEW_MIB_COMPLIANT processed_pid = tp.tcpConnCreationProcess; -#else - processed_pid = 0; -#endif if (pid != -1 && processed_pid != pid) continue; // construct local/remote addresses @@ -432,11 +424,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { for (i = 0; i < num_ent; i++) { memcpy(&tp6, databuf.buf + i * sizeof tp6, sizeof tp6); -#ifdef NEW_MIB_COMPLIANT processed_pid = tp6.tcp6ConnCreationProcess; -#else - processed_pid = 0; -#endif if (pid != -1 && processed_pid != pid) continue; // construct local/remote addresses @@ -474,11 +462,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { assert(num_ent * sizeof(mib2_udpEntry_t) == mibhdr.len); for (i = 0; i < num_ent; i++) { memcpy(&ude, databuf.buf + i * sizeof ude, sizeof ude); -#ifdef NEW_MIB_COMPLIANT processed_pid = ude.udpCreationProcess; -#else - processed_pid = 0; -#endif if (pid != -1 && processed_pid != pid) continue; // XXX Very ugly hack! It seems we get here only the first @@ -514,11 +498,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { num_ent = mibhdr.len / sizeof(mib2_udp6Entry_t); for (i = 0; i < num_ent; i++) { memcpy(&ude6, databuf.buf + i * sizeof ude6, sizeof ude6); -#ifdef NEW_MIB_COMPLIANT processed_pid = ude6.udp6CreationProcess; -#else - processed_pid = 0; -#endif if (pid != -1 && processed_pid != pid) continue; inet_ntop(AF_INET6, &ude6.udp6LocalAddress, lip, sizeof(lip)); diff --git a/psutil/arch/sunos/v10/ifaddrs.c b/psutil/arch/sunos/v10/ifaddrs.c deleted file mode 100644 index 3719c8c18a..0000000000 --- a/psutil/arch/sunos/v10/ifaddrs.c +++ /dev/null @@ -1,125 +0,0 @@ -/* References: - * https://lists.samba.org/archive/samba-technical/2009-February/063079.html - * http://stackoverflow.com/questions/4139405/#4139811 - * https://github.com/steve-o/openpgm/blob/master/openpgm/pgm/getifaddrs.c - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ifaddrs.h" - -#define MAX(x,y) ((x)>(y)?(x):(y)) -#define SIZE(p) MAX((p).ss_len,sizeof(p)) - - -static struct sockaddr * -sa_dup (struct sockaddr_storage *sa1) -{ - struct sockaddr *sa2; - size_t sz = sizeof(struct sockaddr_storage); - sa2 = (struct sockaddr *) calloc(1,sz); - memcpy(sa2,sa1,sz); - return(sa2); -} - - -void freeifaddrs (struct ifaddrs *ifp) -{ - if (NULL == ifp) return; - free(ifp->ifa_name); - free(ifp->ifa_addr); - free(ifp->ifa_netmask); - free(ifp->ifa_dstaddr); - freeifaddrs(ifp->ifa_next); - free(ifp); -} - - -int getifaddrs (struct ifaddrs **ifap) -{ - int sd = -1; - char *ccp, *ecp; - struct lifconf ifc; - struct lifreq *ifr; - struct lifnum lifn; - struct ifaddrs *cifa = NULL; /* current */ - struct ifaddrs *pifa = NULL; /* previous */ - const size_t IFREQSZ = sizeof(struct lifreq); - - sd = socket(AF_INET, SOCK_STREAM, 0); - if (sd < 0) - goto error; - - ifc.lifc_buf = NULL; - *ifap = NULL; - /* find how much memory to allocate for the SIOCGLIFCONF call */ - lifn.lifn_family = AF_UNSPEC; - lifn.lifn_flags = 0; - if (ioctl(sd, SIOCGLIFNUM, &lifn) < 0) - goto error; - - /* Sun and Apple code likes to pad the interface count here in case interfaces - * are coming up between calls */ - lifn.lifn_count += 4; - - ifc.lifc_family = AF_UNSPEC; - ifc.lifc_len = lifn.lifn_count * sizeof(struct lifreq); - ifc.lifc_buf = calloc(1, ifc.lifc_len); - if (ioctl(sd, SIOCGLIFCONF, &ifc) < 0) - goto error; - - ccp = (char *)ifc.lifc_req; - ecp = ccp + ifc.lifc_len; - - while (ccp < ecp) { - - ifr = (struct lifreq *) ccp; - cifa = (struct ifaddrs *) calloc(1, sizeof(struct ifaddrs)); - cifa->ifa_next = NULL; - cifa->ifa_name = strdup(ifr->lifr_name); - - if (pifa == NULL) *ifap = cifa; /* first one */ - else pifa->ifa_next = cifa; - - if (ioctl(sd, SIOCGLIFADDR, ifr, IFREQSZ) < 0) - goto error; - cifa->ifa_addr = sa_dup(&ifr->lifr_addr); - - if (ioctl(sd, SIOCGLIFNETMASK, ifr, IFREQSZ) < 0) - goto error; - cifa->ifa_netmask = sa_dup(&ifr->lifr_addr); - - cifa->ifa_flags = 0; - cifa->ifa_dstaddr = NULL; - - if (0 == ioctl(sd, SIOCGLIFFLAGS, ifr)) /* optional */ - cifa->ifa_flags = ifr->lifr_flags; - - if (ioctl(sd, SIOCGLIFDSTADDR, ifr, IFREQSZ) < 0) { - if (0 == ioctl(sd, SIOCGLIFBRDADDR, ifr, IFREQSZ)) - cifa->ifa_dstaddr = sa_dup(&ifr->lifr_addr); - } - else cifa->ifa_dstaddr = sa_dup(&ifr->lifr_addr); - - pifa = cifa; - ccp += IFREQSZ; - } - free(ifc.lifc_buf); - close(sd); - return 0; -error: - if (ifc.lifc_buf != NULL) - free(ifc.lifc_buf); - if (sd != -1) - close(sd); - freeifaddrs(*ifap); - return (-1); -} diff --git a/psutil/arch/sunos/v10/ifaddrs.h b/psutil/arch/sunos/v10/ifaddrs.h deleted file mode 100644 index 0953a9b99a..0000000000 --- a/psutil/arch/sunos/v10/ifaddrs.h +++ /dev/null @@ -1,26 +0,0 @@ -/* Reference: https://lists.samba.org/archive/samba-technical/2009-February/063079.html */ - - -#ifndef __IFADDRS_H__ -#define __IFADDRS_H__ - -#include -#include - -#undef ifa_dstaddr -#undef ifa_broadaddr -#define ifa_broadaddr ifa_dstaddr - -struct ifaddrs { - struct ifaddrs *ifa_next; - char *ifa_name; - unsigned int ifa_flags; - struct sockaddr *ifa_addr; - struct sockaddr *ifa_netmask; - struct sockaddr *ifa_dstaddr; -}; - -extern int getifaddrs(struct ifaddrs **); -extern void freeifaddrs(struct ifaddrs *); - -#endif diff --git a/setup.py b/setup.py index abcc5aa627..50848ceb9f 100755 --- a/setup.py +++ b/setup.py @@ -16,8 +16,6 @@ import glob import io import os -import platform -import re import shutil import struct import subprocess @@ -424,7 +422,6 @@ def get_winver(): sources + ["psutil/_psutil_sunos.c"] + glob.glob("psutil/arch/sunos/*.c") - + glob.glob("psutil/arch/sunos/v10/*.c") ), define_macros=macros, libraries=['kstat', 'nsl', 'socket'], @@ -468,26 +465,7 @@ def get_winver(): # fmt: on ) if SUNOS: - - def get_sunos_update(): - # See https://serverfault.com/q/524883 - # for an explanation of Solaris /etc/release - with open('/etc/release') as f: - update = re.search(r'(?<=s10s_u)[0-9]{1,2}', f.readline()) - return int(update.group(0)) if update else 0 - posix_extension.libraries.append('socket') - if platform.release() == '5.10': - # Detect Solaris 5.10, update >= 4, see: - # https://github.com/giampaolo/psutil/pull/1638 - if get_sunos_update() >= 4: - # MIB compliance starts with SunOS 5.10 Update 4: - posix_extension.define_macros.append(('NEW_MIB_COMPLIANT', 1)) - posix_extension.sources.append('psutil/arch/solaris/v10/ifaddrs.c') - posix_extension.define_macros.append(('PSUTIL_SUNOS10', 1)) - else: - # Other releases are by default considered to be new mib compliant. - posix_extension.define_macros.append(('NEW_MIB_COMPLIANT', 1)) elif AIX: posix_extension.sources.append('psutil/arch/aix/ifaddrs.c') From 03c5c69fe6326c0857881aa6a253314785bd95b3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 19 Oct 2025 14:49:35 +0200 Subject: [PATCH 1366/1714] setup.py: link external libs also when compiling _psutil_posix.c ext --- setup.py | 66 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/setup.py b/setup.py index 50848ceb9f..f1439c9534 100755 --- a/setup.py +++ b/setup.py @@ -113,7 +113,13 @@ "wheel", ] +# External libraries to link against. +libraries = [] + +# The pre-processor macros that are passed to the C compiler when +# building the extension. macros = [] + if POSIX: macros.append(("PSUTIL_POSIX", 1)) if BSD: @@ -278,6 +284,17 @@ def get_winver(): msg += "2000, XP and 2003 server" raise RuntimeError(msg) + libraries.extend([ + "advapi32", + "kernel32", + "netapi32", + "pdh", + "PowrProf", + "psapi", + "shell32", + "ws2_32", + ]) + macros.append(("PSUTIL_WINDOWS", 1)) macros.extend([ # be nice to mingw, see: @@ -300,16 +317,7 @@ def get_winver(): + glob.glob("psutil/arch/windows/*.c") ), define_macros=macros, - libraries=[ - "psapi", - "kernel32", - "advapi32", - "shell32", - "netapi32", - "ws2_32", - "PowrProf", - "pdh", - ], + libraries=libraries, # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"], # fmt: off @@ -341,7 +349,9 @@ def get_winver(): ) elif FREEBSD: + libraries.extend(["devstat"]) macros.append(("PSUTIL_FREEBSD", 1)) + ext = Extension( 'psutil._psutil_bsd', sources=( @@ -351,7 +361,7 @@ def get_winver(): + glob.glob("psutil/arch/freebsd/*.c") ), define_macros=macros, - libraries=["devstat"], + libraries=libraries, # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -359,7 +369,9 @@ def get_winver(): ) elif OPENBSD: + libraries.extend(["kvm"]) macros.append(("PSUTIL_OPENBSD", 1)) + ext = Extension( 'psutil._psutil_bsd', sources=( @@ -369,7 +381,7 @@ def get_winver(): + glob.glob("psutil/arch/openbsd/*.c") ), define_macros=macros, - libraries=["kvm"], + libraries=libraries, # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -377,7 +389,9 @@ def get_winver(): ) elif NETBSD: + libraries.extend(["kvm"]) macros.append(("PSUTIL_NETBSD", 1)) + ext = Extension( 'psutil._psutil_bsd', sources=( @@ -387,7 +401,7 @@ def get_winver(): + glob.glob("psutil/arch/netbsd/*.c") ), define_macros=macros, - libraries=["kvm"], + libraries=libraries, # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -415,7 +429,9 @@ def get_winver(): ) elif SUNOS: + libraries.extend(["kstat", "nsl", "socket"]) macros.append(("PSUTIL_SUNOS", 1)) + ext = Extension( 'psutil._psutil_sunos', sources=( @@ -424,7 +440,7 @@ def get_winver(): + glob.glob("psutil/arch/sunos/*.c") ), define_macros=macros, - libraries=['kstat', 'nsl', 'socket'], + libraries=libraries, # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -432,17 +448,17 @@ def get_winver(): ) elif AIX: + libraries.extend(["perfstat"]) macros.append(("PSUTIL_AIX", 1)) + ext = Extension( 'psutil._psutil_aix', - sources=sources - + [ - 'psutil/_psutil_aix.c', - 'psutil/arch/aix/net_connections.c', - 'psutil/arch/aix/common.c', - 'psutil/arch/aix/ifaddrs.c', - ], - libraries=['perfstat'], + sources=( + sources + + ["psutil/_psutil_aix.c"] + + glob.glob("psutil/arch/aix/*.c") + ), + libraries=libraries, define_macros=macros, # fmt: off # python 2.7 compatibility requires no comma @@ -457,6 +473,7 @@ def get_winver(): if POSIX: posix_extension = Extension( 'psutil._psutil_posix', + libraries=libraries, define_macros=macros, sources=sources, # fmt: off @@ -464,11 +481,6 @@ def get_winver(): **py_limited_api # fmt: on ) - if SUNOS: - posix_extension.libraries.append('socket') - elif AIX: - posix_extension.sources.append('psutil/arch/aix/ifaddrs.c') - extensions = [ext, posix_extension] else: extensions = [ext] From 7a0756f0adf9c7191b01281f868aa1ca83488f43 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 19 Oct 2025 15:50:23 +0200 Subject: [PATCH 1367/1714] Upgrade actions/setup-python@v6 --- .github/workflows/bsd.yml | 2 +- .github/workflows/build.yml | 10 +++++----- .github/workflows/issues.yml | 2 +- .github/workflows/sunos.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index b41d7216aa..2a7e9d3fff 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -5,7 +5,7 @@ # https://github.com/vmactions/netbsd-vm on: [push, pull_request] -name: bsd-tests +name: bsd concurrency: group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} cancel-in-progress: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eef99e427d..a5b00037a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: # Run tests on Linux, macOS, Windows tests: - name: "tests, ${{ matrix.os }}, ${{ matrix.arch }}" + name: "${{ matrix.os }}, ${{ matrix.arch }}" runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: @@ -39,11 +39,11 @@ jobs: # Only install Python 3.8 on macOS ARM64 for universal2 support - name: "Install Python 3.8 on macOS ARM64" if: runner.os == 'macOS' && runner.arch == 'ARM64' - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.8 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.11 @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: "Run linters" @@ -108,7 +108,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - uses: actions/download-artifact@v4 diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 037f1c9680..3ce6b4957e 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -15,7 +15,7 @@ jobs: # install python - uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml index bd022c5688..4539270110 100644 --- a/.github/workflows/sunos.yml +++ b/.github/workflows/sunos.yml @@ -1,7 +1,7 @@ # Execute tests on SunOS # https://github.com/vmactions/solaris-vm -name: sunos-tests +name: sunos on: push: # only run this job if the following files are modified From a07e87a1336678cbcc929116cd826137779cf654 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 19 Oct 2025 17:42:10 +0200 Subject: [PATCH 1368/1714] Pre release --- HISTORY.rst | 2 +- docs/index.rst | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1a9709bf9c..25264a7a79 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,7 +4,7 @@ 7.1.1 ===== -XXXX-XX-XX +2025-10-19 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index d7c39f9515..32b5f0da22 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2699,13 +2699,17 @@ PyPy3. Timeline ======== +- 2025-10-19: + `7.1.1 `__ - + `what's new `__ - + `diff `__ - 2025-09-17: - `7.0.0 `__ - - `what's new `__ - + `7.1.0 `__ - + `what's new `__ - `diff `__ - 2025-02-13: `7.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2024-12-19: `6.1.1 `__ - From fd4f5e5db6aee2f44b582803bd1567e3cf8f7a2c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 20 Oct 2025 23:08:51 +0200 Subject: [PATCH 1369/1714] CI: refact cibuildwheel test runs --- .github/workflows/build.yml | 11 +++-------- HISTORY.rst | 5 +++++ Makefile | 13 +++++-------- psutil/__init__.py | 2 +- psutil/tests/__init__.py | 18 +++++++++++------- psutil/tests/test_process.py | 10 +++++----- psutil/tests/test_system.py | 2 -- psutil/tests/test_windows.py | 12 ++++++------ pyproject.toml | 25 ++++++++++++++++--------- scripts/internal/install-sysdeps.sh | 2 +- scripts/internal/print_sysinfo.py | 25 +++++++++++++------------ 11 files changed, 66 insertions(+), 59 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5b00037a8..555004a48c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: tests: name: "${{ matrix.os }}, ${{ matrix.arch }}" runs-on: ${{ matrix.os }} - timeout-minutes: 20 + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -48,7 +48,7 @@ jobs: python-version: 3.11 - name: Build wheels + run tests - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.2.1 env: CIBW_ARCHS: "${{ matrix.arch }}" CIBW_ENABLE: "${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" @@ -59,16 +59,11 @@ jobs: name: wheels-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse - - name: Generate source distribution (.tar.gz) - if: matrix.os == 'ubuntu-latest' - run: | - make sdist - # Test python 2.7 fallback installation message produced by setup.py py2-fallback: name: py2.7 setup.py check runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 15 strategy: fail-fast: false steps: diff --git a/HISTORY.rst b/HISTORY.rst index 25264a7a79..1386f2edb7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,11 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.1.2 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + 7.1.1 ===== diff --git a/Makefile b/Makefile index 7581120158..0ed7d88943 100644 --- a/Makefile +++ b/Makefile @@ -75,12 +75,12 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test] + $(PYTHON) -m pip install -U $(PIP_INSTALL_ARGS) -e .[test] install-pydeps-dev: ## Install python deps meant for local development. ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test,dev] + $(PYTHON) -m pip install -U $(PIP_INSTALL_ARGS) -e .[test,dev] install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit @@ -238,9 +238,7 @@ ci-test: ## Run tests on GitHub CI. Used by BSD runners. PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test ${MAKE} print-sysinfo $(PYTHON) -m pip list - ${MAKE} test - ${MAKE} test-memleaks - ${MAKE} test-sudo + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest psutil/tests/ ci-test-cibuildwheel: ## Run tests from cibuildwheel. # testing the wheels means we can't use other test targets which are rebuilding the python extensions @@ -248,14 +246,13 @@ ci-test-cibuildwheel: ## Run tests from cibuildwheel. ${MAKE} install-sysdeps ${MAKE} print-sysinfo mkdir -p .tests - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k 'not test_memleaks.py'" $(PYTHON) -m pytest --pyargs psutil.tests - cd .tests/ && $(PYTHON_ENV_VARS) PYTEST_ADDOPTS="-k test_memleaks.py" $(PYTHON) -m pytest --pyargs psutil.tests + cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs psutil.tests ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit make sdist mv wheelhouse/* dist/ - make print-hashes + make print-dist make check-dist # =================================================================== diff --git a/psutil/__init__.py b/psutil/__init__.py index 5798a54708..0533369143 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -205,7 +205,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.1.1" +__version__ = "7.1.2" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index ba2f9e5985..f5e41cd0f1 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1056,13 +1056,16 @@ def _check_proc_exc(self, proc, exc): repr(exc) def assert_pid_gone(self, pid): - with pytest.raises(psutil.NoSuchProcess) as cm: - try: - psutil.Process(pid) - except psutil.ZombieProcess: - return pytest.fail("wasn't supposed to raise ZombieProcess") - assert cm.value.pid == pid - assert cm.value.name is None + try: + proc = psutil.Process(pid) + except psutil.ZombieProcess: + raise AssertionError("wasn't supposed to raise ZombieProcess") + except psutil.NoSuchProcess as exc: + assert exc.pid == pid # noqa: PT017 + assert exc.name is None # noqa: PT017 + else: + raise AssertionError(f"did not raise NoSuchProcess ({proc})") + assert not psutil.pid_exists(pid), pid assert pid not in psutil.pids() assert pid not in [x.pid for x in psutil.process_iter()] @@ -1155,6 +1158,7 @@ def assert_proc_zombie(self, proc): @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") +@pytest.mark.xdist_group(name="serial") class TestMemoryLeak(PsutilTestCase): """Test framework class for detecting function memory leaks, typically functions implemented in C which forgot to free() memory diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 4b2d77fc73..fb0767c4bb 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1494,16 +1494,16 @@ def test_pid_0(self): @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") def test_environ(self): def clean_dict(d): - exclude = ["PLAT", "HOME", "PYTEST_CURRENT_TEST", "PYTEST_VERSION"] + exclude = {"PLAT", "HOME"} if MACOS: - exclude.extend([ + exclude.update([ "__CF_USER_TEXT_ENCODING", "VERSIONER_PYTHON_PREFER_32_BIT", "VERSIONER_PYTHON_VERSION", - "VERSIONER_PYTHON_VERSION", ]) - for name in exclude: - d.pop(name, None) + for name in list(d.keys()): + if name in exclude or name.startswith("PYTEST_"): + d.pop(name) return { k.replace("\r", "").replace("\n", ""): ( v.replace("\r", "").replace("\n", "") diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index bd56e1718c..bcb45647f9 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -582,8 +582,6 @@ def test_cpu_freq(self): def check_ls(ls): for nt in ls: assert nt._fields == ('current', 'min', 'max') - if nt.max != 0.0: - assert nt.current <= nt.max for name in nt._fields: value = getattr(nt, name) assert isinstance(value, (int, float)) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 88c5fb754d..203766fa70 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -461,12 +461,12 @@ def test_username(self): def test_cmdline(self): sys_value = re.sub(r"[ ]+", " ", 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) + # 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('"', "") + psutil_value = psutil_value.replace('"', "") assert sys_value == psutil_value # XXX - occasional failures diff --git a/pyproject.toml b/pyproject.toml index 08c2e747fb..ea262f0664 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,21 +221,28 @@ trailing_comma_inline_array = true [tool.cibuildwheel] skip = [ "*-musllinux*", - "cp36-*", - "cp37-*", - "cp3{7,8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures - "pp*", + "cp314t*", + "cp3{8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures ] -test-skip = [ - "cp313-macosx*", # Hangs on macOS for some reason - "cp3{6,7,8,9,10,11,12}-*", # Only run tests for minor (3.8) and major (3.13) Python versions -] -test-extras = ["test"] +test-extras = ["test"] # same as doing `pip install .[test]` test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" ci-test-cibuildwheel" [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] +# Run tests on Python 3.13. On all other Python versions do a lightweight +# import test. +[[tool.cibuildwheel.overrides]] +select = "cp3{8,9,10,11,12,13t,14}-* cp313-macosx*" +test-extras = [] +test-command = "python -c \"import psutil; print(psutil.__version__)\"" + +# In macOS use Python 3.8: higher Python version hang for some reason. +[[tool.cibuildwheel.overrides]] +select = "cp38-macosx*" +test-extras = ["test"] +test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" ci-test-cibuildwheel" + [tool.pytest.ini_options] addopts = ''' --verbose diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 714d3b47a1..cda9807a51 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -50,7 +50,7 @@ main() { $SUDO apt-get install -y sudo # for test-sudo elif [ $HAS_YUM ]; then $SUDO yum install -y python3-devel gcc - $SUDO yum install -y net-tools coreutils util-linux # for tests + $SUDO yum install -y net-tools coreutils-single util-linux # for tests $SUDO yum install -y sudo # for test-sudo elif [ $HAS_PACMAN ]; then $SUDO pacman -S --noconfirm python gcc sudo net-tools coreutils util-linux diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py index eb85032db8..0ccd2c609a 100755 --- a/scripts/internal/print_sysinfo.py +++ b/scripts/internal/print_sysinfo.py @@ -13,7 +13,6 @@ import locale import os import platform -import pprint import shlex import shutil import subprocess @@ -59,6 +58,13 @@ def import_module_by_path(path): def main(): info = collections.OrderedDict() + # python + info['python'] = ', '.join([ + platform.python_implementation(), + platform.python_version(), + platform.python_compiler(), + ]) + # OS if psutil.LINUX and shutil.which("lsb_release"): info['OS'] = sh('lsb_release -d -s') @@ -76,12 +82,7 @@ def main(): if psutil.POSIX: info['kernel'] = platform.uname()[2] - # python - info['python'] = ', '.join([ - platform.python_implementation(), - platform.python_version(), - platform.python_compiler(), - ]) + # pip info['pip'] = getattr(pip, '__version__', 'not installed') if wheel is not None: info['pip'] += f" (wheel={wheel.__version__})" @@ -139,11 +140,11 @@ def main(): info['constants'] = "\n ".join(constants) # processes - info['pids'] = len(psutil.pids()) - pinfo = psutil.Process().as_dict() - pinfo.pop('memory_maps', None) - pinfo["environ"] = {k: os.environ[k] for k in sorted(os.environ)} - info['proc'] = pprint.pformat(pinfo) + # info['pids'] = len(psutil.pids()) + # pinfo = psutil.Process().as_dict() + # pinfo.pop('memory_maps', None) + # pinfo["environ"] = {k: os.environ[k] for k in sorted(os.environ)} + # info['proc'] = pprint.pformat(pinfo) # print print("=" * 70, file=sys.stderr) From 4c54215395428f5ad43f363483635243e9a84f93 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 21 Oct 2025 11:01:43 +0200 Subject: [PATCH 1370/1714] C: always use return -1; to indicate error --- psutil/arch/freebsd/proc.c | 2 +- psutil/arch/freebsd/sys_socks.c | 26 ++++++++++++------------ psutil/arch/netbsd/proc.c | 6 +++--- psutil/arch/openbsd/proc.c | 6 +++--- psutil/arch/osx/cpu.c | 2 +- psutil/arch/osx/init.c | 2 +- psutil/arch/osx/mem.c | 4 ++-- psutil/arch/osx/proc.c | 17 ++++++++-------- psutil/arch/windows/init.c | 32 +++++++++++++++--------------- psutil/arch/windows/proc.c | 10 +++++----- psutil/arch/windows/proc_handles.c | 20 +++++++++---------- psutil/arch/windows/proc_info.c | 8 ++++---- psutil/arch/windows/security.c | 10 +++++----- 13 files changed, 72 insertions(+), 73 deletions(-) diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 9e2d436b0f..0215c2c7b5 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -79,7 +79,7 @@ int _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { char *buf = NULL; if (psutil_sysctl_malloc(mib, 4, &buf, &length) != 0) - return 1; + return -1; *proc_list = (struct kinfo_proc *)buf; *proc_count = length / sizeof(struct kinfo_proc); diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index f8d09ab3ef..7e677a4959 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -34,22 +34,22 @@ psutil_populate_xfiles(struct xfile **psutil_xfiles, int *psutil_nxfiles) { while (sysctlbyname("kern.file", *psutil_xfiles, &len, 0, 0) == -1) { if (errno != ENOMEM) { PyErr_SetFromErrno(0); - return 0; + return -1; } len *= 2; new_psutil_xfiles = realloc(*psutil_xfiles, len); if (new_psutil_xfiles == NULL) { PyErr_NoMemory(); - return 0; + return -1; } *psutil_xfiles = new_psutil_xfiles; } if (len > 0 && (*psutil_xfiles)->xf_size != sizeof(struct xfile)) { PyErr_Format(PyExc_RuntimeError, "struct xfile size mismatch"); - return 0; + return -1; } *psutil_nxfiles = len / sizeof(struct xfile); - return 1; + return 0; } @@ -226,14 +226,14 @@ psutil_gather_inet( } free(buf); - return 1; + return 0; error: Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); free(buf); - return 0; + return -1; } @@ -333,13 +333,13 @@ psutil_gather_unix(int proto, PyObject *py_retlist, } free(buf); - return 1; + return 0; error: Py_XDECREF(py_tuple); Py_XDECREF(py_lpath); free(buf); - return 0; + return -1; } @@ -392,14 +392,14 @@ psutil_net_connections(PyObject* self, PyObject* args) { goto error; } - if (psutil_populate_xfiles(&psutil_xfiles, &psutil_nxfiles) != 1) + if (psutil_populate_xfiles(&psutil_xfiles, &psutil_nxfiles) != 0) goto error_free_psutil_xfiles; // TCP if (include_tcp == 1) { if (psutil_gather_inet( IPPROTO_TCP, include_v4, include_v6, py_retlist, - psutil_xfiles, psutil_nxfiles) == 0) + psutil_xfiles, psutil_nxfiles) != 0) { goto error_free_psutil_xfiles; } @@ -408,16 +408,16 @@ psutil_net_connections(PyObject* self, PyObject* args) { if (include_udp == 1) { if (psutil_gather_inet( IPPROTO_UDP, include_v4, include_v6, py_retlist, - psutil_xfiles, psutil_nxfiles) == 0) + psutil_xfiles, psutil_nxfiles) != 0) { goto error_free_psutil_xfiles; } } // UNIX if (include_unix == 1) { - if (psutil_gather_unix(SOCK_STREAM, py_retlist, psutil_xfiles, psutil_nxfiles) == 0) + if (psutil_gather_unix(SOCK_STREAM, py_retlist, psutil_xfiles, psutil_nxfiles) != 0) goto error_free_psutil_xfiles; - if (psutil_gather_unix(SOCK_DGRAM, py_retlist, psutil_xfiles, psutil_nxfiles) == 0) + if (psutil_gather_unix(SOCK_DGRAM, py_retlist, psutil_xfiles, psutil_nxfiles) != 0) goto error_free_psutil_xfiles; } diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index a1cd2c9bf8..f13e4f9cd4 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -255,14 +255,14 @@ _psutil_pids(struct kinfo_proc2 **proc_list, size_t *proc_count) { PyErr_Format( PyExc_RuntimeError, "kvm_openfiles() syscall failed: %s", errbuf ); - return 1; + return -1; } result = kvm_getproc2(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt); if (result == NULL) { PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() syscall failed"); kvm_close(kd); - return 1; + return -1; } *proc_count = (size_t)cnt; @@ -271,7 +271,7 @@ _psutil_pids(struct kinfo_proc2 **proc_list, size_t *proc_count) { if ((*proc_list = malloc(mlen)) == NULL) { PyErr_NoMemory(); kvm_close(kd); - return 1; + return -1; } memcpy(*proc_list, result, mlen); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 87cc57c0b3..a60c8e9065 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -78,14 +78,14 @@ _psutil_pids(struct kinfo_proc **procList, size_t *procCount) { kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); if (! kd) { convert_kvm_err("kvm_openfiles", errbuf); - return 1; + return -1; } result = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt); if (result == NULL) { PyErr_Format(PyExc_RuntimeError, "kvm_getprocs syscall failed"); kvm_close(kd); - return 1; + return -1; } *procCount = (size_t)cnt; @@ -95,7 +95,7 @@ _psutil_pids(struct kinfo_proc **procList, size_t *procCount) { if ((*procList = malloc(mlen)) == NULL) { PyErr_NoMemory(); kvm_close(kd); - return 1; + return -1; } memcpy(*procList, result, mlen); diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index af50d61abc..6160b2ad51 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -177,7 +177,7 @@ psutil_find_pmgr_entry(io_registry_entry_t *out_entry) { if (found) { *out_entry = entry; // caller must release - return 1; + return -1; } return 0; diff --git a/psutil/arch/osx/init.c b/psutil/arch/osx/init.c index 65e81f40d3..7120e8ab0e 100644 --- a/psutil/arch/osx/init.c +++ b/psutil/arch/osx/init.c @@ -21,7 +21,7 @@ psutil_setup_osx(void) { ret = mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); if (ret != KERN_SUCCESS) { psutil_PyErr_SetFromOSErrnoWithSyscall("mach_timebase_info"); - return 1; + return -1; } return 0; } diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 825c65af31..c77649d8eb 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -29,7 +29,7 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { if (mport == MACH_PORT_NULL) { PyErr_SetString(PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL"); - return 1; + return -1; } ret = host_statistics64(mport, HOST_VM_INFO64, (host_info64_t)vmstat, &count); @@ -40,7 +40,7 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { "host_statistics64(HOST_VM_INFO64) syscall failed: %s", mach_error_string(ret) ); - return 1; + return -1; } return 0; } diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 57629a1fc8..79b748ff32 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -53,7 +53,7 @@ _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { *proc_count = 0; if (psutil_sysctl_malloc(mib, 3, &buf, &len) != 0) - return 1; + return -1; *proc_list = (struct kinfo_proc *)buf; *proc_count = len / sizeof(struct kinfo_proc); @@ -61,7 +61,7 @@ _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { if (*proc_count == 0) { free(buf); PyErr_Format(PyExc_RuntimeError, "no PIDs found"); - return 1; + return -1; } return 0; @@ -79,23 +79,23 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { if (psutil_pid_exists(pid) == 0) { NoSuchProcess("psutil_pid_exists -> 0"); - return 1; + return -1; } // In case of zombie process we'll get EINVAL. We translate it // to NSP and _psosx.py will translate it to ZP. if (errno == EINVAL) { psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to NSP"); NoSuchProcess("sysctl(KERN_PROCARGS2) -> EINVAL"); - return 1; + return -1; } // There's nothing we can do other than raising AD. if (errno == EIO) { psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); - return 1; + return -1; } psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); - return 1; + return -1; } return 0; } @@ -168,8 +168,7 @@ psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 */ static int -psutil_task_for_pid(pid_t pid, mach_port_t *task) -{ +psutil_task_for_pid(pid_t pid, mach_port_t *task) { // See: https://github.com/giampaolo/psutil/issues/1181 kern_return_t err = KERN_SUCCESS; @@ -189,7 +188,7 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) (long)pid, err, errno, mach_error_string(err)); AccessDenied("task_for_pid"); } - return 1; + return -1; } return 0; } diff --git a/psutil/arch/windows/init.c b/psutil/arch/windows/init.c index 55f779ffee..b410d5670f 100644 --- a/psutil/arch/windows/init.c +++ b/psutil/arch/windows/init.c @@ -189,59 +189,59 @@ psutil_loadlibs() { NtQuerySystemInformation = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQuerySystemInformation"); if (! NtQuerySystemInformation) - return 1; + return -1; NtQueryInformationProcess = psutil_GetProcAddress( "ntdll.dll", "NtQueryInformationProcess"); if (! NtQueryInformationProcess) - return 1; + return -1; NtSetInformationProcess = psutil_GetProcAddress( "ntdll.dll", "NtSetInformationProcess"); if (! NtSetInformationProcess) - return 1; + return -1; NtQueryObject = psutil_GetProcAddressFromLib( "ntdll.dll", "NtQueryObject"); if (! NtQueryObject) - return 1; + return -1; RtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( "ntdll.dll", "RtlIpv4AddressToStringA"); if (! RtlIpv4AddressToStringA) - return 1; + return -1; GetExtendedTcpTable = psutil_GetProcAddressFromLib( "iphlpapi.dll", "GetExtendedTcpTable"); if (! GetExtendedTcpTable) - return 1; + return -1; GetExtendedUdpTable = psutil_GetProcAddressFromLib( "iphlpapi.dll", "GetExtendedUdpTable"); if (! GetExtendedUdpTable) - return 1; + return -1; RtlGetVersion = psutil_GetProcAddressFromLib( "ntdll.dll", "RtlGetVersion"); if (! RtlGetVersion) - return 1; + return -1; NtSuspendProcess = psutil_GetProcAddressFromLib( "ntdll", "NtSuspendProcess"); if (! NtSuspendProcess) - return 1; + return -1; NtResumeProcess = psutil_GetProcAddressFromLib( "ntdll", "NtResumeProcess"); if (! NtResumeProcess) - return 1; + return -1; NtQueryVirtualMemory = psutil_GetProcAddressFromLib( "ntdll", "NtQueryVirtualMemory"); if (! NtQueryVirtualMemory) - return 1; + return -1; RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( "ntdll", "RtlNtStatusToDosErrorNoTeb"); if (! RtlNtStatusToDosErrorNoTeb) - return 1; + return -1; GetTickCount64 = psutil_GetProcAddress( "kernel32", "GetTickCount64"); if (! GetTickCount64) - return 1; + return -1; RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( "ntdll.dll", "RtlIpv6AddressToStringA"); if (! RtlIpv6AddressToStringA) - return 1; + return -1; // --- Optional @@ -298,9 +298,9 @@ psutil_set_winver() { int psutil_setup_windows(void) { if (psutil_loadlibs() != 0) - return 1; + return -1; if (psutil_set_winver() != 0) - return 1; + return -1; GetSystemInfo(&PSUTIL_SYSTEM_INFO); InitializeCriticalSection(&PSUTIL_CRITICAL_SECTION); return 0; diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 02fe5a3744..22f8c95b36 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -425,7 +425,7 @@ psutil_GetProcWsetInformation( buffer = MALLOC_ZERO(bufferSize); if (! buffer) { PyErr_NoMemory(); - return 1; + return -1; } while ((status = NtQueryVirtualMemory( @@ -442,12 +442,12 @@ psutil_GetProcWsetInformation( if (bufferSize > 256 * 1024 * 1024) { PyErr_SetString(PyExc_RuntimeError, "NtQueryVirtualMemory bufsize is too large"); - return 1; + return -1; } buffer = MALLOC_ZERO(bufferSize); if (! buffer) { PyErr_NoMemory(); - return 1; + return -1; } } @@ -464,7 +464,7 @@ psutil_GetProcWsetInformation( status, "NtQueryVirtualMemory(MemoryWorkingSetInformation)"); } HeapFree(GetProcessHeap(), 0, buffer); - return 1; + return -1; } *wSetInfo = (PMEMORY_WORKING_SET_INFORMATION)buffer; @@ -1056,7 +1056,7 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - if (! psutil_get_proc_info(pid, &process, &buffer)) + if (psutil_get_proc_info(pid, &process, &buffer) != 0) return NULL; for (i = 0; i < process->NumberOfThreads; i++) { if (process->Threads[i].ThreadState != Waiting || diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 34cd54e1f0..471944c643 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -41,7 +41,7 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { buffer = MALLOC_ZERO(bufferSize); if (buffer == NULL) { PyErr_NoMemory(); - return 1; + return -1; } while ((status = NtQuerySystemInformation( @@ -59,20 +59,20 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { PyErr_SetString( PyExc_RuntimeError, "SystemExtendedHandleInformation buffer too big"); - return 1; + return -1; } buffer = MALLOC_ZERO(bufferSize); if (buffer == NULL) { PyErr_NoMemory(); - return 1; + return -1; } } if (! NT_SUCCESS(status)) { psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); FREE(buffer); - return 1; + return -1; } *handles = (PSYSTEM_HANDLE_INFORMATION_EX)buffer; @@ -156,7 +156,7 @@ psutil_threaded_get_filename(HANDLE hFile) { NULL, 0, (LPTHREAD_START_ROUTINE)psutil_get_filename, &hFile, 0, NULL); if (hThread == NULL) { psutil_PyErr_SetFromOSErrnoWithSyscall("CreateThread"); - return 1; + return -1; } // Wait for the worker thread to finish. @@ -169,7 +169,7 @@ psutil_threaded_get_filename(HANDLE hFile) { if (TerminateThread(hThread, 0) == 0) { psutil_PyErr_SetFromOSErrnoWithSyscall("TerminateThread"); CloseHandle(hThread); - return 1; + return -1; } CloseHandle(hThread); return 0; @@ -182,11 +182,11 @@ psutil_threaded_get_filename(HANDLE hFile) { "WaitForSingleObject -> WAIT_FAILED -> TerminateThread" ); CloseHandle(hThread); - return 1; + return -1; } psutil_PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); CloseHandle(hThread); - return 1; + return -1; } if (GetExitCodeThread(hThread, &threadRetValue) == 0) { @@ -195,12 +195,12 @@ psutil_threaded_get_filename(HANDLE hFile) { "GetExitCodeThread (failed) -> TerminateThread" ); CloseHandle(hThread); - return 1; + return -1; } CloseHandle(hThread); psutil_PyErr_SetFromOSErrnoWithSyscall("GetExitCodeThread"); - return 1; + return -1; } CloseHandle(hThread); return threadRetValue; diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index c7acd24c33..75bd17c7bb 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -699,7 +699,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { * We use this as a fallback when faster functions fail with access * denied. This is slower because it iterates over all processes * but it doesn't require any privilege (also work for PID 0). - * On success return 1, else 0 with Python exception already set. + * On success return 0, else -1 with Python exception already set. */ int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, @@ -752,7 +752,7 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, if ((ULONG_PTR)process->UniqueProcessId == pid) { *retProcess = process; *retBuffer = buffer; - return 1; + return 0; } } while ((process = PSUTIL_NEXT_PROCESS(process))); @@ -762,7 +762,7 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, error: if (buffer != NULL) free(buffer); - return 0; + return -1; } @@ -794,7 +794,7 @@ psutil_proc_info(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - if (! psutil_get_proc_info(pid, &process, &buffer)) + if (psutil_get_proc_info(pid, &process, &buffer) != 0) return NULL; for (i = 0; i < process->NumberOfThreads; i++) diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index d466be3f55..8beeb7cb8e 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -22,7 +22,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { if (! LookupPrivilegeValue(NULL, Privilege, &luid)) { psutil_PyErr_SetFromOSErrnoWithSyscall("LookupPrivilegeValue"); - return 1; + return -1; } // first pass. get current privilege setting @@ -39,7 +39,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { &cbPrevious)) { psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); - return 1; + return -1; } // Second pass. Set privilege based on previous setting. @@ -61,7 +61,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { NULL)) { psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); - return 1; + return -1; } return 0; @@ -122,13 +122,13 @@ psutil_set_se_debug() { HANDLE hToken; if ((hToken = psutil_get_thisproc_token()) == NULL) { - // "return 1;" to get an exception + // "return -1;" to get an exception psutil_print_err(); return 0; } if (psutil_set_privilege(hToken, SE_DEBUG_NAME, TRUE) != 0) { - // "return 1;" to get an exception + // "return -1;" to get an exception psutil_print_err(); } From 6ccd75dd9fa3da5caca1505e12ec63fc04457b66 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 21 Oct 2025 12:36:37 +0200 Subject: [PATCH 1371/1714] Remove _psutil_posix.c (#2651) --- .github/workflows/issues.py | 2 +- .github/workflows/sunos.yml | 1 - MANIFEST.in | 2 - psutil/__init__.py | 6 +- psutil/_psaix.py | 17 ++-- psutil/_psbsd.py | 19 ++-- psutil/_pslinux.py | 20 ++-- psutil/_psosx.py | 19 ++-- psutil/_pssunos.py | 11 +-- psutil/_psutil_aix.c | 5 +- psutil/_psutil_bsd.c | 4 + psutil/_psutil_linux.c | 4 + psutil/_psutil_osx.c | 4 + psutil/_psutil_posix.c | 182 ------------------------------------ psutil/_psutil_posix.h | 9 -- psutil/_psutil_sunos.c | 4 + psutil/arch/posix/init.c | 172 ++++++++++++++++++++++++++++++++++ psutil/arch/posix/init.h | 6 +- psutil/arch/posix/proc.c | 4 +- psutil/tests/test_bsd.py | 4 +- psutil/tests/test_osx.py | 9 +- psutil/tests/test_posix.py | 4 +- psutil/tests/test_system.py | 2 +- setup.py | 59 ++++-------- 24 files changed, 262 insertions(+), 307 deletions(-) delete mode 100644 psutil/_psutil_posix.c delete mode 100644 psutil/_psutil_posix.h diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index c184e092d9..0f8bd08d48 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -56,7 +56,7 @@ "sunos": ["sunos", "solaris"], "wsl": ["wsl"], "unix": [ - "psposix", "_psutil_posix", "waitpid", "statvfs", "/dev/tty", + "psposix", "waitpid", "statvfs", "/dev/tty", "/dev/pts", "posix", ], "pypy": ["pypy"], diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml index 4539270110..2facfe364c 100644 --- a/.github/workflows/sunos.yml +++ b/.github/workflows/sunos.yml @@ -8,7 +8,6 @@ on: paths: &sunos_paths - ".github/workflows/sunos.yml" - "psutil/_pssunos.py" - - "psutil/_psutil_posix.c" - "psutil/_psutil_sunos.c" - "psutil/arch/all/**" - "psutil/arch/posix/**" diff --git a/MANIFEST.in b/MANIFEST.in index ce9091ddc3..006fbe30fe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -35,8 +35,6 @@ include psutil/_psutil_aix.c include psutil/_psutil_bsd.c include psutil/_psutil_linux.c include psutil/_psutil_osx.c -include psutil/_psutil_posix.c -include psutil/_psutil_posix.h include psutil/_psutil_sunos.c include psutil/_psutil_windows.c include psutil/_pswindows.py diff --git a/psutil/__init__.py b/psutil/__init__.py index 0533369143..b289c16170 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -192,13 +192,11 @@ # 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): + for _name in dir(_psplatform.cext): if _name.startswith('RLIM') and _name.isupper(): - _globals[_name] = getattr(_psutil_posix, _name) + _globals[_name] = getattr(_psplatform.cext, _name) __all__.append(_name) del _globals, _name diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 5d5d1edaa2..a84abe3f53 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -17,7 +17,6 @@ from . import _common from . import _psposix from . import _psutil_aix as cext -from . import _psutil_posix as cext_posix from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN @@ -41,8 +40,8 @@ HAS_NET_IO_COUNTERS = hasattr(cext, "net_io_counters") HAS_PROC_IO_COUNTERS = hasattr(cext, "proc_io_counters") -PAGE_SIZE = cext_posix.getpagesize() -AF_LINK = cext_posix.AF_LINK +PAGE_SIZE = cext.getpagesize() +AF_LINK = cext.AF_LINK PROC_STATUSES = { cext.SIDL: _common.STATUS_IDLE, @@ -194,7 +193,7 @@ def disk_partitions(all=False): # ===================================================================== -net_if_addrs = cext_posix.net_if_addrs +net_if_addrs = cext.net_if_addrs if HAS_NET_IO_COUNTERS: net_io_counters = cext.net_io_counters @@ -233,8 +232,8 @@ def net_if_stats(): names = {x[0] for x in net_if_addrs()} ret = {} for name in names: - mtu = cext_posix.net_if_mtu(name) - flags = cext_posix.net_if_flags(name) + mtu = cext.net_if_mtu(name) + flags = cext.net_if_flags(name) # try to get speed and duplex # TODO: rewrite this in C (entstat forks, so use truss -f to follow. @@ -278,7 +277,7 @@ def boot_time(): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext_posix.users() + rawlist = cext.users() localhost = (':0.0', ':0') for item in rawlist: user, tty, hostname, tstamp, user_process, pid = item @@ -448,11 +447,11 @@ def net_connections(self, kind='inet'): @wrap_exceptions def nice_get(self): - return cext_posix.getpriority(self.pid) + return cext.proc_priority_get(self.pid) @wrap_exceptions def nice_set(self, value): - return cext_posix.setpriority(self.pid, value) + return cext.proc_priority_set(self.pid, value) @wrap_exceptions def ppid(self): diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index e71e6cdd4f..e96a29914e 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -15,7 +15,6 @@ from . import _common from . import _psposix from . import _psutil_bsd as cext -from . import _psutil_posix as cext_posix from ._common import FREEBSD from ._common import NETBSD from ._common import OPENBSD @@ -94,8 +93,8 @@ cext.PSUTIL_CONN_NONE: _common.CONN_NONE, } -PAGESIZE = cext_posix.getpagesize() -AF_LINK = cext_posix.AF_LINK +PAGESIZE = cext.getpagesize() +AF_LINK = cext.AF_LINK HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads") @@ -380,7 +379,7 @@ def disk_partitions(all=False): net_io_counters = cext.net_io_counters -net_if_addrs = cext_posix.net_if_addrs +net_if_addrs = cext.net_if_addrs def net_if_stats(): @@ -389,9 +388,9 @@ def net_if_stats(): ret = {} for name in names: try: - mtu = cext_posix.net_if_mtu(name) - flags = cext_posix.net_if_flags(name) - duplex, speed = cext_posix.net_if_duplex_speed(name) + mtu = cext.net_if_mtu(name) + flags = cext.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: @@ -506,7 +505,7 @@ def adjust_proc_create_time(ctime): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext.users() if OPENBSD else cext_posix.users() + rawlist = cext.users() for item in rawlist: user, tty, hostname, tstamp, pid = item if tty == '~': @@ -840,11 +839,11 @@ def wait(self, timeout=None): @wrap_exceptions def nice_get(self): - return cext_posix.getpriority(self.pid) + return cext.proc_priority_get(self.pid) @wrap_exceptions def nice_set(self, value): - return cext_posix.setpriority(self.pid, value) + return cext.proc_priority_set(self.pid, value) @wrap_exceptions def status(self): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b5095d40b6..821c4eb717 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -24,7 +24,6 @@ from . import _common from . import _psposix from . import _psutil_linux as cext -from . import _psutil_posix as cext_posix from ._common import ENCODING from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF @@ -58,11 +57,6 @@ "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", ] - -if hasattr(resource, "prlimit"): - __extra__all__.extend( - [x for x in dir(cext) if x.startswith('RLIM') and x.isupper()] - ) # fmt: on @@ -79,7 +73,7 @@ # Number of clock ticks per second CLOCK_TICKS = os.sysconf("SC_CLK_TCK") -PAGESIZE = cext_posix.getpagesize() +PAGESIZE = cext.getpagesize() LITTLE_ENDIAN = sys.byteorder == 'little' UNSET = object() @@ -734,7 +728,7 @@ def cpu_freq(): # ===================================================================== -net_if_addrs = cext_posix.net_if_addrs +net_if_addrs = cext.net_if_addrs class _Ipv6UnsupportedError(Exception): @@ -1045,8 +1039,8 @@ def net_if_stats(): ret = {} for name in names: try: - mtu = cext_posix.net_if_mtu(name) - flags = cext_posix.net_if_flags(name) + mtu = cext.net_if_mtu(name) + flags = cext.net_if_flags(name) duplex, speed = cext.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 @@ -1541,7 +1535,7 @@ def multi_bcat(*paths): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext_posix.users() + rawlist = cext.users() for item in rawlist: user, tty, hostname, tstamp, pid = item nt = _common.suser(user, tty or None, hostname, tstamp, pid) @@ -2129,11 +2123,11 @@ def nice_get(self): # return int(data.split()[18]) # Use C implementation - return cext_posix.getpriority(self.pid) + return cext.proc_priority_get(self.pid) @wrap_exceptions def nice_set(self, value): - return cext_posix.setpriority(self.pid, value) + return cext.proc_priority_set(self.pid, value) # starting from CentOS 6. if HAS_CPU_AFFINITY: diff --git a/psutil/_psosx.py b/psutil/_psosx.py index be51991099..28ec6b4f7e 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -12,7 +12,6 @@ from . import _common from . import _psposix from . import _psutil_osx as cext -from . import _psutil_posix as cext_posix from ._common import AccessDenied from ._common import NoSuchProcess from ._common import ZombieProcess @@ -32,8 +31,8 @@ # ===================================================================== -PAGESIZE = cext_posix.getpagesize() -AF_LINK = cext_posix.AF_LINK +PAGESIZE = cext.getpagesize() +AF_LINK = cext.AF_LINK TCP_STATUSES = { cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, @@ -235,7 +234,7 @@ def sensors_battery(): net_io_counters = cext.net_io_counters -net_if_addrs = cext_posix.net_if_addrs +net_if_addrs = cext.net_if_addrs def net_connections(kind='inet'): @@ -262,9 +261,9 @@ def net_if_stats(): ret = {} for name in names: try: - mtu = cext_posix.net_if_mtu(name) - flags = cext_posix.net_if_flags(name) - duplex, speed = cext_posix.net_if_duplex_speed(name) + mtu = cext.net_if_mtu(name) + flags = cext.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: @@ -316,7 +315,7 @@ def adjust_proc_create_time(ctime): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext_posix.users() + rawlist = cext.users() for item in rawlist: user, tty, hostname, tstamp, pid = item if tty == '~': @@ -550,11 +549,11 @@ def wait(self, timeout=None): @wrap_exceptions def nice_get(self): - return cext_posix.getpriority(self.pid) + return cext.proc_priority_get(self.pid) @wrap_exceptions def nice_set(self, value): - return cext_posix.setpriority(self.pid, value) + return cext.proc_priority_set(self.pid, value) @wrap_exceptions def status(self): diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index d1d88bdcf7..e3268932f6 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -15,7 +15,6 @@ from . import _common from . import _psposix -from . import _psutil_posix as cext_posix from . import _psutil_sunos as cext from ._common import AF_INET6 from ._common import ENCODING @@ -38,8 +37,8 @@ # ===================================================================== -PAGE_SIZE = cext_posix.getpagesize() -AF_LINK = cext_posix.AF_LINK +PAGE_SIZE = cext.getpagesize() +AF_LINK = cext.AF_LINK IS_64_BIT = sys.maxsize > 2**32 CONN_IDLE = "IDLE" @@ -252,7 +251,7 @@ def disk_partitions(all=False): net_io_counters = cext.net_io_counters -net_if_addrs = cext_posix.net_if_addrs +net_if_addrs = cext.net_if_addrs def net_connections(kind, _pid=-1): @@ -310,7 +309,7 @@ def boot_time(): def users(): """Return currently connected users as a list of namedtuples.""" retlist = [] - rawlist = cext_posix.users() + rawlist = cext.users() localhost = (':0.0', ':0') for item in rawlist: user, tty, hostname, tstamp, user_process, pid = item @@ -467,7 +466,7 @@ def nice_set(self, value): # The process actually exists though, as it has a name, # creation time, etc. raise AccessDenied(self.pid, self._name) - return cext_posix.setpriority(self.pid, value) + return cext.proc_priority_set(self.pid, value) @wrap_exceptions def ppid(self): diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index ca05615551..a7b6b80b60 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -51,7 +51,6 @@ #include #include "arch/all/init.h" -#include "_psutil_posix.h" #include "arch/aix/ifaddrs.h" #include "arch/aix/net_connections.h" #include "arch/aix/common.h" @@ -1010,6 +1009,10 @@ PyInit__psutil_aix(void) { if (psutil_setup() != 0) return NULL; + if (psutil_posix_add_constants(mod) != 0) + return NULL; + if (psutil_posix_add_methods(mod) != 0) + return NULL; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 75baab1f94..c4011506da 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -118,6 +118,10 @@ PyObject if (psutil_setup() != 0) return NULL; + if (psutil_posix_add_constants(mod) != 0) + return NULL; + if (psutil_posix_add_methods(mod) != 0) + return NULL; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 1d9d974cd1..2cfb4e47c4 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -68,6 +68,10 @@ PyInit__psutil_linux(void) { if (psutil_setup() != 0) return NULL; + if (psutil_posix_add_constants(mod) != 0) + return NULL; + if (psutil_posix_add_methods(mod) != 0) + return NULL; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index c389e3a599..ae79bddea1 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -84,6 +84,10 @@ PyInit__psutil_osx(void) { return NULL; if (psutil_setup_osx() != 0) return NULL; + if (psutil_posix_add_constants(mod) != 0) + return NULL; + if (psutil_posix_add_methods(mod) != 0) + return NULL; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c deleted file mode 100644 index a4d69473f1..0000000000 --- a/psutil/_psutil_posix.c +++ /dev/null @@ -1,182 +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. - * - * Functions specific to all POSIX compliant platforms. - */ - -#include -#include -#include - -#include "arch/all/init.h" - - -static PyMethodDef mod_methods[] = { - {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS}, - {"getpriority", psutil_posix_getpriority, METH_VARARGS}, - {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, - {"net_if_flags", psutil_net_if_flags, METH_VARARGS}, - {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS}, - {"net_if_mtu", psutil_net_if_mtu, METH_VARARGS}, - {"setpriority", psutil_posix_setpriority, METH_VARARGS}, -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, -#endif -#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) - {"users", psutil_users, METH_VARARGS}, -#endif - {NULL, NULL, 0, NULL} -}; - - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_psutil_posix", - NULL, - -1, - mod_methods, - NULL, - NULL, - NULL, - NULL -}; - -PyObject * -PyInit__psutil_posix(void) { - PyObject *mod = PyModule_Create(&moduledef); - if (mod == NULL) - return NULL; - -#ifdef Py_GIL_DISABLED - if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) - return NULL; -#endif - -#if defined(PSUTIL_BSD) || \ - defined(PSUTIL_OSX) || \ - defined(PSUTIL_SUNOS) || \ - defined(PSUTIL_AIX) - if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) - return NULL; -#endif - -#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) - PyObject *v; - -#ifdef RLIMIT_AS - if (PyModule_AddIntConstant(mod, "RLIMIT_AS", RLIMIT_AS)) - return NULL; -#endif - -#ifdef RLIMIT_CORE - if (PyModule_AddIntConstant(mod, "RLIMIT_CORE", RLIMIT_CORE)) - return NULL; -#endif - -#ifdef RLIMIT_CPU - if (PyModule_AddIntConstant(mod, "RLIMIT_CPU", RLIMIT_CPU)) - return NULL; -#endif - -#ifdef RLIMIT_DATA - if (PyModule_AddIntConstant(mod, "RLIMIT_DATA", RLIMIT_DATA)) - return NULL; -#endif - -#ifdef RLIMIT_FSIZE - if (PyModule_AddIntConstant(mod, "RLIMIT_FSIZE", RLIMIT_FSIZE)) - return NULL; -#endif - -#ifdef RLIMIT_MEMLOCK - if (PyModule_AddIntConstant(mod, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK)) - return NULL; -#endif - -#ifdef RLIMIT_NOFILE - if (PyModule_AddIntConstant(mod, "RLIMIT_NOFILE", RLIMIT_NOFILE)) - return NULL; -#endif - -#ifdef RLIMIT_NPROC - if (PyModule_AddIntConstant(mod, "RLIMIT_NPROC", RLIMIT_NPROC)) - return NULL; -#endif - -#ifdef RLIMIT_RSS - if (PyModule_AddIntConstant(mod, "RLIMIT_RSS", RLIMIT_RSS)) - return NULL; -#endif - -#ifdef RLIMIT_STACK - if (PyModule_AddIntConstant(mod, "RLIMIT_STACK", RLIMIT_STACK)) - return NULL; -#endif - -// Linux specific - -#ifdef RLIMIT_LOCKS - if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) - return NULL; -#endif - -#ifdef RLIMIT_MSGQUEUE - if (PyModule_AddIntConstant(mod, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE)) - return NULL; -#endif - -#ifdef RLIMIT_NICE - if (PyModule_AddIntConstant(mod, "RLIMIT_NICE", RLIMIT_NICE)) - return NULL; -#endif - -#ifdef RLIMIT_RTPRIO - if (PyModule_AddIntConstant(mod, "RLIMIT_RTPRIO", RLIMIT_RTPRIO)) - return NULL; -#endif - -#ifdef RLIMIT_RTTIME - if (PyModule_AddIntConstant(mod, "RLIMIT_RTTIME", RLIMIT_RTTIME)) - return NULL; -#endif - -#ifdef RLIMIT_SIGPENDING - if (PyModule_AddIntConstant(mod, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING)) - return NULL; -#endif - -// Free specific - -#ifdef RLIMIT_SWAP - if (PyModule_AddIntConstant(mod, "RLIMIT_SWAP", RLIMIT_SWAP)) - return NULL; -#endif - -#ifdef RLIMIT_SBSIZE - if (PyModule_AddIntConstant(mod, "RLIMIT_SBSIZE", RLIMIT_SBSIZE)) - return NULL; -#endif - -#ifdef RLIMIT_NPTS - if (PyModule_AddIntConstant(mod, "RLIMIT_NPTS", RLIMIT_NPTS)) - return NULL; -#endif - -#if defined(HAVE_LONG_LONG) - if (sizeof(RLIM_INFINITY) > sizeof(long)) { - v = PyLong_FromLongLong((PY_LONG_LONG) RLIM_INFINITY); - } else -#endif - { - v = PyLong_FromLong((long) RLIM_INFINITY); - } - if (v) { - if (PyModule_AddObject(mod, "RLIM_INFINITY", v)) - return NULL; - } -#endif // defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) - - return mod; -} diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h deleted file mode 100644 index 5a37e48b15..0000000000 --- a/psutil/_psutil_posix.h +++ /dev/null @@ -1,9 +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. - */ - -long psutil_getpagesize(void); -int psutil_pid_exists(pid_t pid); -void psutil_raise_for_pid(pid_t pid, char *msg); diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 97da6a8f92..084ba0a718 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -93,6 +93,10 @@ PyInit__psutil_sunos(void) { if (psutil_setup() != 0) return NULL; + if (psutil_posix_add_constants(mod) != 0) + return NULL; + if (psutil_posix_add_methods(mod) != 0) + return NULL; if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; diff --git a/psutil/arch/posix/init.c b/psutil/arch/posix/init.c index b3818a27ef..bd2eebbd97 100644 --- a/psutil/arch/posix/init.c +++ b/psutil/arch/posix/init.c @@ -8,6 +8,8 @@ #include #include +#include "init.h" + /* * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: * @@ -39,3 +41,173 @@ PyObject * psutil_getpagesize_pywrapper(PyObject *self, PyObject *args) { return Py_BuildValue("l", psutil_getpagesize()); } + + +// POSIX-only methods. +static PyMethodDef posix_methods[] = { + {"getpagesize", psutil_getpagesize_pywrapper, METH_VARARGS}, + {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, + {"net_if_flags", psutil_net_if_flags, METH_VARARGS}, + {"net_if_is_running", psutil_net_if_is_running, METH_VARARGS}, + {"net_if_mtu", psutil_net_if_mtu, METH_VARARGS}, + {"proc_priority_get", psutil_proc_priority_get, METH_VARARGS}, + {"proc_priority_set", psutil_proc_priority_set, METH_VARARGS}, +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) + {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, +#endif +#if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) + {"users", psutil_users, METH_VARARGS}, +#endif + {NULL, NULL, 0, NULL} +}; + + +// Add methods to a module. +int +psutil_posix_add_methods(PyObject *mod) { + for (int i = 0; posix_methods[i].ml_name != NULL; i++) { + PyObject *f = PyCFunction_NewEx(&posix_methods[i], NULL, mod); + if (!f) { + return -1; + } + if (PyModule_AddObject(mod, posix_methods[i].ml_name, f)) { + Py_DECREF(f); + return -1; + } + } + return 0; +} + + +// Add constants to a module. +int +psutil_posix_add_constants(PyObject *mod) { + if (!mod) + return -1; + +#if defined(PSUTIL_BSD) || \ + defined(PSUTIL_OSX) || \ + defined(PSUTIL_SUNOS) || \ + defined(PSUTIL_AIX) + if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) + return -1; +#endif + +#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + PyObject *v; + +#ifdef RLIMIT_AS + if (PyModule_AddIntConstant(mod, "RLIMIT_AS", RLIMIT_AS)) + return -1; +#endif + +#ifdef RLIMIT_CORE + if (PyModule_AddIntConstant(mod, "RLIMIT_CORE", RLIMIT_CORE)) + return -1; +#endif + +#ifdef RLIMIT_CPU + if (PyModule_AddIntConstant(mod, "RLIMIT_CPU", RLIMIT_CPU)) + return -1; +#endif + +#ifdef RLIMIT_DATA + if (PyModule_AddIntConstant(mod, "RLIMIT_DATA", RLIMIT_DATA)) + return -1; +#endif + +#ifdef RLIMIT_FSIZE + if (PyModule_AddIntConstant(mod, "RLIMIT_FSIZE", RLIMIT_FSIZE)) + return -1; +#endif + +#ifdef RLIMIT_MEMLOCK + if (PyModule_AddIntConstant(mod, "RLIMIT_MEMLOCK", RLIMIT_MEMLOCK)) + return -1; +#endif + +#ifdef RLIMIT_NOFILE + if (PyModule_AddIntConstant(mod, "RLIMIT_NOFILE", RLIMIT_NOFILE)) + return -1; +#endif + +#ifdef RLIMIT_NPROC + if (PyModule_AddIntConstant(mod, "RLIMIT_NPROC", RLIMIT_NPROC)) + return -1; +#endif + +#ifdef RLIMIT_RSS + if (PyModule_AddIntConstant(mod, "RLIMIT_RSS", RLIMIT_RSS)) + return -1; +#endif + +#ifdef RLIMIT_STACK + if (PyModule_AddIntConstant(mod, "RLIMIT_STACK", RLIMIT_STACK)) + return -1; +#endif + +// Linux specific + +#ifdef RLIMIT_LOCKS + if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) + return -1; +#endif + +#ifdef RLIMIT_MSGQUEUE + if (PyModule_AddIntConstant(mod, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE)) + return -1; +#endif + +#ifdef RLIMIT_NICE + if (PyModule_AddIntConstant(mod, "RLIMIT_NICE", RLIMIT_NICE)) + return -1; +#endif + +#ifdef RLIMIT_RTPRIO + if (PyModule_AddIntConstant(mod, "RLIMIT_RTPRIO", RLIMIT_RTPRIO)) + return -1; +#endif + +#ifdef RLIMIT_RTTIME + if (PyModule_AddIntConstant(mod, "RLIMIT_RTTIME", RLIMIT_RTTIME)) + return -1; +#endif + +#ifdef RLIMIT_SIGPENDING + if (PyModule_AddIntConstant(mod, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING)) + return -1; +#endif + +// Free specific + +#ifdef RLIMIT_SWAP + if (PyModule_AddIntConstant(mod, "RLIMIT_SWAP", RLIMIT_SWAP)) + return -1; +#endif + +#ifdef RLIMIT_SBSIZE + if (PyModule_AddIntConstant(mod, "RLIMIT_SBSIZE", RLIMIT_SBSIZE)) + return -1; +#endif + +#ifdef RLIMIT_NPTS + if (PyModule_AddIntConstant(mod, "RLIMIT_NPTS", RLIMIT_NPTS)) + return -1; +#endif + +#if defined(HAVE_LONG_LONG) + if (sizeof(RLIM_INFINITY) > sizeof(long)) { + v = PyLong_FromLongLong((PY_LONG_LONG) RLIM_INFINITY); + } else +#endif + { + v = PyLong_FromLong((long) RLIM_INFINITY); + } + if (v) { + if (PyModule_AddObject(mod, "RLIM_INFINITY", v)) + return -1; + } +#endif // defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + + return 0; +} diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 34687aac13..dde363dcd3 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -37,11 +37,13 @@ int psutil_pid_exists(pid_t pid); long psutil_getpagesize(void); void psutil_raise_for_pid(pid_t pid, char *msg); +int psutil_posix_add_constants(PyObject *mod); +int psutil_posix_add_methods(PyObject *mod); PyObject *psutil_getpagesize_pywrapper(PyObject *self, PyObject *args); PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); PyObject *psutil_net_if_flags(PyObject *self, PyObject *args); PyObject *psutil_net_if_is_running(PyObject *self, PyObject *args); PyObject *psutil_net_if_mtu(PyObject *self, PyObject *args); -PyObject *psutil_posix_getpriority(PyObject *self, PyObject *args); -PyObject *psutil_posix_setpriority(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); diff --git a/psutil/arch/posix/proc.c b/psutil/arch/posix/proc.c index 70aa735efd..b0ddf7b0e6 100644 --- a/psutil/arch/posix/proc.c +++ b/psutil/arch/posix/proc.c @@ -80,7 +80,7 @@ psutil_raise_for_pid(pid_t pid, char *syscall) { // Get PID priority. PyObject * -psutil_posix_getpriority(PyObject *self, PyObject *args) { +psutil_proc_priority_get(PyObject *self, PyObject *args) { pid_t pid; int priority; errno = 0; @@ -101,7 +101,7 @@ psutil_posix_getpriority(PyObject *self, PyObject *args) { // Set PID priority. PyObject * -psutil_posix_setpriority(PyObject *self, PyObject *args) { +psutil_proc_priority_set(PyObject *self, PyObject *args) { pid_t pid; int priority; int retval; diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 78542b15a3..6727bdffd1 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -30,9 +30,7 @@ from psutil.tests import terminate if BSD: - from psutil._psutil_posix import getpagesize - - PAGESIZE = getpagesize() + PAGESIZE = psutil._psplatform.cext.getpagesize() # muse requires root privileges MUSE_AVAILABLE = os.getuid() == 0 and shutil.which("muse") else: diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index b7f532bd98..8c7d1e93a9 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -11,7 +11,6 @@ import psutil from psutil import MACOS -from psutil import POSIX from psutil.tests import AARCH64 from psutil.tests import CI_TESTING from psutil.tests import HAS_BATTERY @@ -24,9 +23,6 @@ from psutil.tests import spawn_subproc 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 @@ -48,7 +44,10 @@ def vm_stat(field): break else: raise ValueError("line not found") - return int(re.search(r'\d+', line).group(0)) * getpagesize() + return ( + int(re.search(r'\d+', line).group(0)) + * psutil._psplatform.cext.getpagesize() + ) @pytest.mark.skipif(not MACOS, reason="MACOS only") diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 69df690089..26227cd398 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -38,8 +38,6 @@ 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 @@ -494,7 +492,7 @@ def test_disk_usage(self): @pytest.mark.skipif(not POSIX, reason="POSIX only") class TestMisc(PsutilTestCase): def test_getpagesize(self): - pagesize = getpagesize() + pagesize = psutil._psplatform.cext.getpagesize() assert pagesize > 0 assert pagesize == resource.getpagesize() assert pagesize == mmap.PAGESIZE diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index bcb45647f9..de3b360a5e 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -906,7 +906,7 @@ def test_net_if_stats(self): def test_net_if_stats_enodev(self): # See: https://github.com/giampaolo/psutil/issues/1279 with mock.patch( - 'psutil._psutil_posix.net_if_mtu', + 'psutil._psplatform.cext.net_if_mtu', side_effect=OSError(errno.ENODEV, ""), ) as m: ret = psutil.net_if_stats() diff --git a/setup.py b/setup.py index f1439c9534..b4bdf57bb8 100755 --- a/setup.py +++ b/setup.py @@ -113,9 +113,6 @@ "wheel", ] -# External libraries to link against. -libraries = [] - # The pre-processor macros that are passed to the C compiler when # building the extension. macros = [] @@ -136,7 +133,6 @@ sources = ['psutil/arch/all/init.c'] if POSIX: - sources.append('psutil/_psutil_posix.c') sources.extend(glob.glob("psutil/arch/posix/*.c")) @@ -284,17 +280,6 @@ def get_winver(): msg += "2000, XP and 2003 server" raise RuntimeError(msg) - libraries.extend([ - "advapi32", - "kernel32", - "netapi32", - "pdh", - "PowrProf", - "psapi", - "shell32", - "ws2_32", - ]) - macros.append(("PSUTIL_WINDOWS", 1)) macros.extend([ # be nice to mingw, see: @@ -317,7 +302,16 @@ def get_winver(): + glob.glob("psutil/arch/windows/*.c") ), define_macros=macros, - libraries=libraries, + libraries=[ + "advapi32", + "kernel32", + "netapi32", + "pdh", + "PowrProf", + "psapi", + "shell32", + "ws2_32", + ], # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"], # fmt: off @@ -349,7 +343,6 @@ def get_winver(): ) elif FREEBSD: - libraries.extend(["devstat"]) macros.append(("PSUTIL_FREEBSD", 1)) ext = Extension( @@ -361,7 +354,7 @@ def get_winver(): + glob.glob("psutil/arch/freebsd/*.c") ), define_macros=macros, - libraries=libraries, + libraries=["devstat"], # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -369,7 +362,6 @@ def get_winver(): ) elif OPENBSD: - libraries.extend(["kvm"]) macros.append(("PSUTIL_OPENBSD", 1)) ext = Extension( @@ -381,7 +373,7 @@ def get_winver(): + glob.glob("psutil/arch/openbsd/*.c") ), define_macros=macros, - libraries=libraries, + libraries=["kvm"], # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -389,7 +381,6 @@ def get_winver(): ) elif NETBSD: - libraries.extend(["kvm"]) macros.append(("PSUTIL_NETBSD", 1)) ext = Extension( @@ -401,7 +392,7 @@ def get_winver(): + glob.glob("psutil/arch/netbsd/*.c") ), define_macros=macros, - libraries=libraries, + libraries=["kvm"], # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -429,7 +420,6 @@ def get_winver(): ) elif SUNOS: - libraries.extend(["kstat", "nsl", "socket"]) macros.append(("PSUTIL_SUNOS", 1)) ext = Extension( @@ -440,7 +430,7 @@ def get_winver(): + glob.glob("psutil/arch/sunos/*.c") ), define_macros=macros, - libraries=libraries, + libraries=["kstat", "nsl", "socket"], # fmt: off # python 2.7 compatibility requires no comma **py_limited_api @@ -448,7 +438,6 @@ def get_winver(): ) elif AIX: - libraries.extend(["perfstat"]) macros.append(("PSUTIL_AIX", 1)) ext = Extension( @@ -458,7 +447,7 @@ def get_winver(): + ["psutil/_psutil_aix.c"] + glob.glob("psutil/arch/aix/*.c") ), - libraries=libraries, + libraries=["perfstat"], define_macros=macros, # fmt: off # python 2.7 compatibility requires no comma @@ -470,22 +459,6 @@ def get_winver(): sys.exit("platform {} is not supported".format(sys.platform)) -if POSIX: - posix_extension = Extension( - 'psutil._psutil_posix', - libraries=libraries, - define_macros=macros, - sources=sources, - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on - ) - extensions = [ext, posix_extension] -else: - extensions = [ext] - - def main(): kwargs = dict( name='psutil', @@ -508,7 +481,7 @@ def main(): platforms='Platform Independent', license='BSD-3-Clause', packages=['psutil', 'psutil.tests'], - ext_modules=extensions, + ext_modules=[ext], options=options, classifiers=[ 'Development Status :: 5 - Production/Stable', From 047b09c070841e4743eb23d4063b4f19b2397433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 21 Oct 2025 18:59:50 +0200 Subject: [PATCH 1372/1714] Ensure exit code from fake_pytest.main() is propagated (#2652) --- psutil/tests/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index d3ef8db582..d20e48bc5f 100644 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -6,6 +6,8 @@ $ python3 -m psutil.tests. """ +import sys + from psutil.tests import pytest -pytest.main() +sys.exit(pytest.main()) From 0bd5f1bc8ce3ed2a8e38f61bff31a9e0d2887e1f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 21 Oct 2025 22:23:36 +0200 Subject: [PATCH 1373/1714] [macOS] fix zombie recognition logic for proc cmdline() and environ() (#2654) --- HISTORY.rst | 5 +++ psutil/_psosx.py | 12 ++--- psutil/_psutil_osx.c | 3 +- psutil/_psutil_windows.c | 2 - psutil/arch/osx/init.h | 1 + psutil/arch/osx/proc.c | 97 +++++++++++++++++++++++++++------------- psutil/arch/posix/init.c | 16 ++++++- psutil/arch/posix/init.h | 2 + psutil/tests/__init__.py | 19 ++++---- 9 files changed, 103 insertions(+), 54 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1386f2edb7..f4d2aa5453 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,11 @@ XXXX-XX-XX +**Bug fixes** + +* 2650_, [macOS]: `Process.cmdline()`_ and `Process.environ()`_ may incorrectly + raise `NoSuchProcess`_ instead of `ZombieProcess`_. + 7.1.1 ===== diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 28ec6b4f7e..e1030b654b 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -351,14 +351,6 @@ 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. @@ -370,11 +362,13 @@ def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except ProcessLookupError as err: - if is_zombie(pid): + if cext.proc_is_zombie(pid): raise ZombieProcess(pid, name, ppid) from err raise NoSuchProcess(pid, name) from err except PermissionError as err: raise AccessDenied(pid, name) from err + except cext.ZombieProcessError as err: + raise ZombieProcess(pid, name, ppid) from err return wrapper diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index ae79bddea1..dbb33efc0b 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -18,13 +18,14 @@ static PyMethodDef mod_methods[] = { // --- per-process functions {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, - {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, {"proc_environ", psutil_proc_environ, METH_VARARGS}, {"proc_exe", psutil_proc_exe, METH_VARARGS}, + {"proc_is_zombie", psutil_proc_is_zombie, METH_VARARGS}, {"proc_kinfo_oneshot", psutil_proc_kinfo_oneshot, METH_VARARGS}, {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, {"proc_name", psutil_proc_name, METH_VARARGS}, + {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS}, diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index ed3026183a..618d759203 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -155,7 +155,6 @@ PyInit__psutil_windows(void) { "_psutil_windows.TimeoutExpired", NULL, NULL); if (TimeoutExpired == NULL) return NULL; - Py_INCREF(TimeoutExpired); if (PyModule_AddObject(mod, "TimeoutExpired", TimeoutExpired)) return NULL; @@ -163,7 +162,6 @@ PyInit__psutil_windows(void) { "_psutil_windows.TimeoutAbandoned", NULL, NULL); if (TimeoutAbandoned == NULL) return NULL; - Py_INCREF(TimeoutAbandoned); if (PyModule_AddObject(mod, "TimeoutAbandoned", TimeoutAbandoned)) return NULL; diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 27acbcaf43..7822151e8a 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -28,6 +28,7 @@ PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_is_zombie(PyObject *self, PyObject *args); PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 79b748ff32..664f408d4f 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -67,6 +67,49 @@ _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { return 0; } + +static int +psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { + int mib[4]; + size_t len; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = pid; + + // fetch the info with sysctl() + len = sizeof(struct kinfo_proc); + + // now read the data from sysctl + if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { + // raise an exception and throw errno as the error + psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl"); + return -1; + } + + // sysctl succeeds but len is zero, happens when process has gone away + if (len == 0) { + NoSuchProcess("sysctl(kinfo_proc), len == 0"); + return -1; + } + return 0; +} + + +static int +is_zombie(size_t pid) { + struct kinfo_proc kp; + + if (psutil_get_kinfo_proc(pid, &kp) == -1) { + PyErr_Clear(); + return 0; + } + if (kp.kp_proc.p_stat == SZOMB) + return 1; + return 0; +} + + // Read process argument space. static int psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { @@ -81,14 +124,18 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { NoSuchProcess("psutil_pid_exists -> 0"); return -1; } - // In case of zombie process we'll get EINVAL. We translate it - // to NSP and _psosx.py will translate it to ZP. + + if (is_zombie(pid) == 1) { + PyErr_SetString(ZombieProcessError, ""); + return -1; + } + if (errno == EINVAL) { psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to NSP"); - NoSuchProcess("sysctl(KERN_PROCARGS2) -> EINVAL"); + AccessDenied("sysctl(KERN_PROCARGS2) -> EINVAL"); return -1; } - // There's nothing we can do other than raising AD. + if (errno == EIO) { psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); @@ -101,34 +148,6 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { } -static int -psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { - int mib[4]; - size_t len; - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PID; - mib[3] = pid; - - // fetch the info with sysctl() - len = sizeof(struct kinfo_proc); - - // now read the data from sysctl - if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { - // raise an exception and throw errno as the error - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl"); - return -1; - } - - // sysctl succeeds but len is zero, happens when process has gone away - if (len == 0) { - NoSuchProcess("sysctl(kinfo_proc), len == 0"); - return -1; - } - return 0; -} - - /* * A wrapper around proc_pidinfo(). * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c @@ -307,6 +326,20 @@ psutil_pids(PyObject *self, PyObject *args) { } +// Return True if PID is a zombie else False, including if PID does not +// exist or the underlying function fails. +PyObject * +psutil_proc_is_zombie(PyObject *self, PyObject *args) { + pid_t pid; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (is_zombie(pid) == 1) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + /* * Return multiple process info as a Python tuple in one shot by * using sysctl() and filling up a kinfo_proc struct. diff --git a/psutil/arch/posix/init.c b/psutil/arch/posix/init.c index bd2eebbd97..8a95a3151a 100644 --- a/psutil/arch/posix/init.c +++ b/psutil/arch/posix/init.c @@ -10,6 +10,8 @@ #include "init.h" +PyObject *ZombieProcessError = NULL;; + /* * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: * @@ -62,7 +64,7 @@ static PyMethodDef posix_methods[] = { }; -// Add methods to a module. +// Add POSIX methods to main OS module. int psutil_posix_add_methods(PyObject *mod) { for (int i = 0; posix_methods[i].ml_name != NULL; i++) { @@ -75,11 +77,21 @@ psutil_posix_add_methods(PyObject *mod) { return -1; } } + + // custom exception + ZombieProcessError = PyErr_NewException( + "_psutil_posix.ZombieProcessError", NULL, NULL + ); + if (ZombieProcessError == NULL) + return -1; + if (PyModule_AddObject(mod, "ZombieProcessError", ZombieProcessError)) + return -1; + return 0; } -// Add constants to a module. +// Add POSIX constants to main OS module. int psutil_posix_add_constants(PyObject *mod) { if (!mod) diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index dde363dcd3..82c9abadbc 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -4,6 +4,8 @@ * found in the LICENSE file. */ +extern PyObject *ZombieProcessError; + #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) #define PSUTIL_HAS_POSIX_USERS diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index f5e41cd0f1..01a8d013f2 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1090,6 +1090,15 @@ def assert_proc_gone(self, proc): proc.wait(timeout=0) # assert not raise TimeoutExpired def assert_proc_zombie(self, proc): + def assert_in_pids(proc): + if MACOS: + # Even ps does not show zombie PIDs for some reason. Weird... + return + assert proc.pid in psutil.pids() + assert proc.pid in [x.pid for x in psutil.process_iter()] + psutil._pmap = {} + assert proc.pid in [x.pid for x in psutil.process_iter()] + # A zombie process should always be instantiable. clone = psutil.Process(proc.pid) # Cloned zombie on Open/NetBSD/illumos/Solaris has null creation @@ -1107,10 +1116,7 @@ def assert_proc_zombie(self, proc): # as_dict() shouldn't crash. proc.as_dict() # It should show up in pids() and process_iter(). - assert proc.pid in psutil.pids() - assert proc.pid in [x.pid for x in psutil.process_iter()] - psutil._pmap = {} - assert proc.pid in [x.pid for x in psutil.process_iter()] + assert_in_pids(proc) # Call all methods. ns = process_namespace(proc) for fun, name in ns.iter(ns.all, clear_cache=True): @@ -1137,10 +1143,7 @@ def assert_proc_zombie(self, proc): proc.kill() assert proc.is_running() assert psutil.pid_exists(proc.pid) - assert proc.pid in psutil.pids() - assert proc.pid in [x.pid for x in psutil.process_iter()] - psutil._pmap = {} - assert proc.pid in [x.pid for x in psutil.process_iter()] + assert_in_pids(proc) # Its parent should 'see' it (edit: not true on BSD and MACOS). # descendants = [x.pid for x in psutil.Process().children( From e1449d614b836ac37ad37c0741f897592dae22ee Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 22 Oct 2025 14:32:04 +0200 Subject: [PATCH 1374/1714] CI: various speedups --- .github/workflows/build.yml | 14 +++----------- Makefile | 7 ++++--- psutil/tests/__init__.py | 16 +++++----------- psutil/tests/test_system.py | 18 ++++++++---------- pyproject.toml | 4 ++-- scripts/internal/install-sysdeps.sh | 11 +++-------- 6 files changed, 25 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 555004a48c..2cf1cb36cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,6 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: - fail-fast: false matrix: include: - { os: ubuntu-latest, arch: x86_64 } @@ -36,16 +35,11 @@ jobs: steps: - uses: actions/checkout@v5 - # Only install Python 3.8 on macOS ARM64 for universal2 support - - name: "Install Python 3.8 on macOS ARM64" - if: runner.os == 'macOS' && runner.arch == 'ARM64' + # Install Python 3.8 on macOS ARM64 for universal2 support, else 3.11 + - name: Install Python uses: actions/setup-python@v6 with: - python-version: 3.8 - - - uses: actions/setup-python@v6 - with: - python-version: 3.11 + python-version: ${{ runner.os == 'macOS' && runner.arch == 'ARM64' && '3.8' || '3.11' }} - name: Build wheels + run tests uses: pypa/cibuildwheel@v3.2.1 @@ -64,8 +58,6 @@ jobs: name: py2.7 setup.py check runs-on: ubuntu-24.04 timeout-minutes: 15 - strategy: - fail-fast: false steps: - uses: actions/checkout@v5 - uses: LizardByte/actions/actions/setup_python@master diff --git a/Makefile b/Makefile index 0ed7d88943..e58f3fa576 100644 --- a/Makefile +++ b/Makefile @@ -250,10 +250,11 @@ ci-test-cibuildwheel: ## Run tests from cibuildwheel. ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit - make sdist + ${MAKE} sdist mv wheelhouse/* dist/ - make print-dist - make check-dist + ${MAKE} check-dist + ${MAKE} install + ${MAKE} print-dist # =================================================================== # Distribution diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 01a8d013f2..0cade49cce 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -166,12 +166,8 @@ def macos_version(): # --- file names -# Disambiguate TESTFN for parallel testing. -if os.name == 'java': - # Jython disallows @ in module names - TESTFN_PREFIX = f"$psutil-{os.getpid()}-" -else: - TESTFN_PREFIX = f"@psutil-{os.getpid()}-" +# Disambiguate TESTFN with PID for parallel testing. +TESTFN_PREFIX = f"@psutil-{os.getpid()}-" UNICODE_SUFFIX = "-ƒőő" # An invalid unicode string. INVALID_UNICODE_SUFFIX = b"f\xc0\x80".decode('utf8', 'surrogateescape') @@ -179,12 +175,10 @@ def macos_version(): # --- 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') +ROOT_DIR = os.environ.get("PSUTIL_ROOT_DIR") or os.path.realpath( + os.path.join(os.path.dirname(__file__), "..", "..") ) +SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') HERE = os.path.realpath(os.path.dirname(__file__)) # --- support diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index de3b360a5e..0cd56422c0 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -612,22 +612,20 @@ class TestDiskAPIs(PsutilTestCase): def test_disk_usage(self): usage = psutil.disk_usage(os.getcwd()) assert 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 - assert usage.total == shutil_usage.total - assert abs(usage.free - shutil_usage.free) < tolerance - if not MACOS_12PLUS: - # see https://github.com/giampaolo/psutil/issues/2147 - assert abs(usage.used - shutil_usage.used) < tolerance + + shutil_usage = shutil.disk_usage(os.getcwd()) + tolerance = 5 * 1024 * 1024 # 5MB + assert usage.total == shutil_usage.total + assert abs(usage.free - shutil_usage.free) < tolerance + if not MACOS_12PLUS: + # see https://github.com/giampaolo/psutil/issues/2147 + assert abs(usage.used - shutil_usage.used) < tolerance # if path does not exist OSError ENOENT is expected across # all platforms diff --git a/pyproject.toml b/pyproject.toml index ea262f0664..36f3f4dd30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -225,7 +225,7 @@ skip = [ "cp3{8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures ] test-extras = ["test"] # same as doing `pip install .[test]` -test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" ci-test-cibuildwheel" +test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci-test-cibuildwheel" [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] @@ -241,7 +241,7 @@ test-command = "python -c \"import psutil; print(psutil.__version__)\"" [[tool.cibuildwheel.overrides]] select = "cp38-macosx*" test-extras = ["test"] -test-command = "make -C {project} PYTHON=python PSUTIL_SCRIPTS_DIR=\"{project}/scripts\" ci-test-cibuildwheel" +test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci-test-cibuildwheel" [tool.pytest.ini_options] addopts = ''' diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index cda9807a51..ae6e341f1f 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -44,16 +44,11 @@ fi # Function to install system dependencies main() { if [ $HAS_APT ]; then - $SUDO apt-get update - $SUDO apt-get install -y python3-dev gcc - $SUDO apt-get install -y net-tools coreutils util-linux # for tests - $SUDO apt-get install -y sudo # for test-sudo + $SUDO apt-get install -y python3-dev gcc net-tools coreutils util-linux sudo elif [ $HAS_YUM ]; then - $SUDO yum install -y python3-devel gcc - $SUDO yum install -y net-tools coreutils-single util-linux # for tests - $SUDO yum install -y sudo # for test-sudo + $SUDO yum install -y python3-devel gcc net-tools coreutils-single util-linux sudo elif [ $HAS_PACMAN ]; then - $SUDO pacman -S --noconfirm python gcc sudo net-tools coreutils util-linux + $SUDO pacman -S --noconfirm python gcc net-tools coreutils util-linux sudo elif [ $HAS_APK ]; then $SUDO apk add --no-confirm python3-dev gcc musl-dev linux-headers coreutils procps elif [ $FREEBSD ]; then From c7f13dd05ffedf51afb9c566947e6e838fda673c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:06:42 +0200 Subject: [PATCH 1375/1714] Refact `psutil_pids()` C code (#2656) psutil_pids() is now a single python wrapper which calls _psutil_pids(), which returns an array, and builds a Python list with that array. --- MANIFEST.in | 6 +++ psutil/arch/all/init.h | 16 +++++--- psutil/arch/all/pids.c | 54 ++++++++++++++++++++++++ psutil/arch/bsd/init.h | 1 - psutil/arch/bsd/proc.c | 52 ------------------------ psutil/arch/freebsd/init.h | 2 +- psutil/arch/freebsd/pids.c | 56 +++++++++++++++++++++++++ psutil/arch/freebsd/proc.c | 14 ------- psutil/arch/netbsd/init.h | 2 +- psutil/arch/netbsd/pids.c | 59 +++++++++++++++++++++++++++ psutil/arch/netbsd/proc.c | 41 ------------------- psutil/arch/openbsd/init.h | 2 +- psutil/arch/openbsd/pids.c | 61 ++++++++++++++++++++++++++++ psutil/arch/openbsd/proc.c | 50 ----------------------- psutil/arch/osx/init.h | 2 +- psutil/arch/osx/pids.c | 56 +++++++++++++++++++++++++ psutil/arch/osx/proc.c | 70 -------------------------------- psutil/arch/windows/init.h | 2 +- psutil/arch/windows/pids.c | 45 ++++++++++++++++++++ psutil/arch/windows/proc.c | 40 ------------------ psutil/arch/windows/proc_utils.c | 59 +++++---------------------- setup.py | 4 +- 22 files changed, 364 insertions(+), 330 deletions(-) create mode 100644 psutil/arch/all/pids.c create mode 100644 psutil/arch/freebsd/pids.c create mode 100644 psutil/arch/netbsd/pids.c create mode 100644 psutil/arch/openbsd/pids.c create mode 100644 psutil/arch/osx/pids.c create mode 100644 psutil/arch/windows/pids.c diff --git a/MANIFEST.in b/MANIFEST.in index 006fbe30fe..9fbe1fff8c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -47,6 +47,7 @@ include psutil/arch/aix/net_connections.h include psutil/arch/aix/net_kernel_structs.h include psutil/arch/all/init.c include psutil/arch/all/init.h +include psutil/arch/all/pids.c include psutil/arch/bsd/cpu.c include psutil/arch/bsd/disk.c include psutil/arch/bsd/init.c @@ -58,6 +59,7 @@ include psutil/arch/freebsd/cpu.c include psutil/arch/freebsd/disk.c include psutil/arch/freebsd/init.h include psutil/arch/freebsd/mem.c +include psutil/arch/freebsd/pids.c include psutil/arch/freebsd/proc.c include psutil/arch/freebsd/proc_socks.c include psutil/arch/freebsd/sensors.c @@ -71,12 +73,14 @@ include psutil/arch/netbsd/cpu.c include psutil/arch/netbsd/disk.c include psutil/arch/netbsd/init.h include psutil/arch/netbsd/mem.c +include psutil/arch/netbsd/pids.c include psutil/arch/netbsd/proc.c include psutil/arch/netbsd/socks.c include psutil/arch/openbsd/cpu.c include psutil/arch/openbsd/disk.c include psutil/arch/openbsd/init.h include psutil/arch/openbsd/mem.c +include psutil/arch/openbsd/pids.c include psutil/arch/openbsd/proc.c include psutil/arch/openbsd/socks.c include psutil/arch/openbsd/users.c @@ -86,6 +90,7 @@ include psutil/arch/osx/init.c include psutil/arch/osx/init.h include psutil/arch/osx/mem.c include psutil/arch/osx/net.c +include psutil/arch/osx/pids.c include psutil/arch/osx/proc.c include psutil/arch/osx/sensors.c include psutil/arch/osx/sys.c @@ -110,6 +115,7 @@ include psutil/arch/windows/init.h include psutil/arch/windows/mem.c include psutil/arch/windows/net.c include psutil/arch/windows/ntextapi.h +include psutil/arch/windows/pids.c include psutil/arch/windows/proc.c include psutil/arch/windows/proc_handles.c include psutil/arch/windows/proc_info.c diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index dc7f0fa3af..f52b1fc83a 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -26,7 +26,7 @@ #include "../../arch/osx/init.h" #elif defined(PSUTIL_FREEBSD) #include "../../arch/freebsd/init.h" -#elif defined(PSUTIL_OPENSBD) +#elif defined(PSUTIL_OPENBSD) #include "../../arch/openbsd/init.h" #elif defined(PSUTIL_NETBSD) #include "../../arch/netbsd/init.h" @@ -66,9 +66,9 @@ extern int PSUTIL_CONN_NONE; // --- Custom exceptions // ==================================================================== -PyObject* AccessDenied(const char *msg); -PyObject* NoSuchProcess(const char *msg); -PyObject* psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); +PyObject *AccessDenied(const char *msg); +PyObject *NoSuchProcess(const char *msg); +PyObject *psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); // ==================================================================== // --- Backward compatibility with missing Python.h APIs @@ -118,6 +118,10 @@ PyObject* psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); #endif #endif -PyObject* psutil_set_debug(PyObject *self, PyObject *args); -PyObject* psutil_check_pid_range(PyObject *self, PyObject *args); +#if defined(PSUTIL_WINDOWS) || defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +PyObject *psutil_pids(PyObject *self, PyObject *args); +#endif + +PyObject *psutil_set_debug(PyObject *self, PyObject *args); +PyObject *psutil_check_pid_range(PyObject *self, PyObject *args); int psutil_setup(void); diff --git a/psutil/arch/all/pids.c b/psutil/arch/all/pids.c new file mode 100644 index 0000000000..d88c10dd6b --- /dev/null +++ b/psutil/arch/all/pids.c @@ -0,0 +1,54 @@ +/* + * 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. + */ + +#if defined(PSUTIL_WINDOWS) || defined(PSUTIL_BSD) || defined(PSUTIL_OSX) +#include + +#include "init.h" + + +PyObject * +psutil_pids(PyObject *self, PyObject *args) { +#ifdef PSUTIL_WINDOWS + DWORD *pids_array = NULL; +#else + pid_t *pids_array = NULL; +#endif + int pids_count = 0; + int i; + PyObject *py_retlist = PyList_New(0); + PyObject *py_pid = NULL; + + if (!py_retlist) + return NULL; + + if (_psutil_pids(&pids_array, &pids_count) != 0) + goto error; + + if (pids_count == 0) { + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + goto error; + } + + for (i = 0; i < pids_count; i++) { + py_pid = PyLong_FromPid(pids_array[i]); + if (!py_pid) + goto error; + if (PyList_Append(py_retlist, py_pid)) + goto error; + Py_CLEAR(py_pid); + } + + free(pids_array); + return py_retlist; + +error: + Py_XDECREF(py_pid); + Py_DECREF(py_retlist); + free(pids_array); + return NULL; +} +#endif diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 7220ccc44f..f9ea974afd 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -19,7 +19,6 @@ PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); PyObject *psutil_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); -PyObject *psutil_pids(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); PyObject *psutil_proc_oneshot_info(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 3d83ae2f93..77f1d1ee75 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -60,58 +60,6 @@ kinfo_getfile(pid_t pid, int *cnt) { #endif // PSUTIL_HASNT_KINFO_GETFILE -/* - * Return a Python list of all the PIDs running on the system. - */ -PyObject * -psutil_pids(PyObject *self, PyObject *args) { -#ifdef PSUTIL_NETBSD - struct kinfo_proc2 *proclist = NULL; - struct kinfo_proc2 *orig_address = NULL; -#else - struct kinfo_proc *proclist = NULL; - struct kinfo_proc *orig_address = NULL; -#endif - size_t num_processes; - size_t idx; - PyObject *py_retlist = PyList_New(0); - PyObject *py_pid = NULL; - - if (py_retlist == NULL) - return NULL; - - if (_psutil_pids(&proclist, &num_processes) != 0) - goto error; - - if (num_processes > 0) { - orig_address = proclist; // save so we can free it after we're done - for (idx = 0; idx < num_processes; idx++) { -#ifdef PSUTIL_FREEBSD - py_pid = PyLong_FromPid(proclist->ki_pid); -#else - py_pid = PyLong_FromPid(proclist->p_pid); -#endif - if (!py_pid) - goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_CLEAR(py_pid); - proclist++; - } - free(orig_address); - } - - return py_retlist; - -error: - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - if (orig_address != NULL) - free(orig_address); - return NULL; -} - - /* * Collect different info about a process in one shot and return * them as a big Python tuple. diff --git a/psutil/arch/freebsd/init.h b/psutil/arch/freebsd/init.h index 58d97ab6fe..f4009ee49e 100644 --- a/psutil/arch/freebsd/init.h +++ b/psutil/arch/freebsd/init.h @@ -8,8 +8,8 @@ #include #include +int _psutil_pids(pid_t **pids_array, int *pids_count); // TODO: move this stuff. Does not belong here -int _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count); int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/pids.c b/psutil/arch/freebsd/pids.c new file mode 100644 index 0000000000..00ad666517 --- /dev/null +++ b/psutil/arch/freebsd/pids.c @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +int +_psutil_pids(pid_t **pids_array, int *pids_count) { + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}; + size_t len = 0; + char *buf = NULL; + struct kinfo_proc *proc_list = NULL; + size_t num_procs = 0; + + *pids_array = NULL; + *pids_count = 0; + + if (psutil_sysctl_malloc(mib, 4, &buf, &len) != 0) + return -1; + + if (len == 0) { + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + goto error; + } + + proc_list = (struct kinfo_proc *)buf; + num_procs = len / sizeof(struct kinfo_proc); + + *pids_array = malloc(num_procs * sizeof(pid_t)); + if (!*pids_array) { + PyErr_NoMemory(); + goto error; + } + + for (size_t i = 0; i < num_procs; i++) { + (*pids_array)[i] = proc_list[i].ki_pid; // FreeBSD PID field + } + + *pids_count = (int)num_procs; + free(buf); + return 0; + +error: + if (buf != NULL) + free(buf); + return -1; +} diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 0215c2c7b5..3c3b94f394 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -72,20 +72,6 @@ static void psutil_remove_spaces(char *str) { // APIS // ============================================================================ - -int _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { - int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; - size_t length = 0; - char *buf = NULL; - - if (psutil_sysctl_malloc(mib, 4, &buf, &length) != 0) - return -1; - - *proc_list = (struct kinfo_proc *)buf; - *proc_count = length / sizeof(struct kinfo_proc); - return 0; -} - /* * Borrowed from psi Python System Information project * Based on code from ps. diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h index da41064b55..d79183a6c2 100644 --- a/psutil/arch/netbsd/init.h +++ b/psutil/arch/netbsd/init.h @@ -9,9 +9,9 @@ #include #include +int _psutil_pids(pid_t **pids_array, int *pids_count); // TODO: refactor this. Does not belong here. int psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc); -int _psutil_pids(struct kinfo_proc2 **proc_list, size_t *proc_count); char *psutil_get_cmd_args(pid_t pid, size_t *argsize); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/pids.c b/psutil/arch/netbsd/pids.c new file mode 100644 index 0000000000..d08f3bd1af --- /dev/null +++ b/psutil/arch/netbsd/pids.c @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#include +#include +#include +#include + + +int +_psutil_pids(pid_t **pids_array, int *pids_count) { + char errbuf[_POSIX2_LINE_MAX]; + kvm_t *kd; + struct kinfo_proc2 *proc_list = NULL; + struct kinfo_proc2 *result; + int cnt; + size_t i; + + *pids_array = NULL; + *pids_count = 0; + + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); + if (kd == NULL) { + PyErr_Format(PyExc_RuntimeError, "kvm_openfiles() failed: %s", errbuf); + return -1; + } + + result = kvm_getproc2(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt); + if (result == NULL) { + PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() failed"); + kvm_close(kd); + return -1; + } + + if (cnt == 0) { + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + kvm_close(kd); + return -1; + } + + *pids_array = malloc(cnt * sizeof(pid_t)); + if (!*pids_array) { + PyErr_NoMemory(); + kvm_close(kd); + return -1; + } + + for (i = 0; i < (size_t)cnt; i++) { + (*pids_array)[i] = result[i].p_pid; + } + + *pids_count = cnt; + kvm_close(kd); + return 0; +} diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index f13e4f9cd4..9bb27a3f95 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -240,47 +240,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -int -_psutil_pids(struct kinfo_proc2 **proc_list, size_t *proc_count) { - struct kinfo_proc2 *result; - // Declaring name as const requires us to cast it when passing it to - // sysctl because the prototype doesn't include the const modifier. - char errbuf[_POSIX2_LINE_MAX]; - int cnt; - kvm_t *kd; - size_t mlen; - - kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); - if (kd == NULL) { - PyErr_Format( - PyExc_RuntimeError, "kvm_openfiles() syscall failed: %s", errbuf - ); - return -1; - } - - result = kvm_getproc2(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt); - if (result == NULL) { - PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() syscall failed"); - kvm_close(kd); - return -1; - } - - *proc_count = (size_t)cnt; - - mlen = cnt * sizeof(struct kinfo_proc2); - if ((*proc_list = malloc(mlen)) == NULL) { - PyErr_NoMemory(); - kvm_close(kd); - return -1; - } - - memcpy(*proc_list, result, mlen); - assert(*proc_list != NULL); - kvm_close(kd); - return 0; -} - - PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index e21b301e1c..d6f0336eea 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -7,10 +7,10 @@ #include +int _psutil_pids(pid_t **pids_array, int *pids_count); // TODO: move / refactor this stuff. It does not belong in here. typedef struct kinfo_proc kinfo_proc; int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); -int _psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/pids.c b/psutil/arch/openbsd/pids.c new file mode 100644 index 0000000000..0b3ed44849 --- /dev/null +++ b/psutil/arch/openbsd/pids.c @@ -0,0 +1,61 @@ +/* + * 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. + */ + +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +int +_psutil_pids(pid_t **pids_array, int *pids_count) { + char errbuf[_POSIX2_LINE_MAX]; + kvm_t *kd; + struct kinfo_proc *proc_list = NULL; + struct kinfo_proc *result; + int cnt; + size_t i; + + *pids_array = NULL; + *pids_count = 0; + + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); + if (kd == NULL) { + PyErr_Format(PyExc_RuntimeError, "kvm_openfiles() failed: %s", errbuf); + return -1; + } + + result = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt); + if (result == NULL) { + PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() failed"); + kvm_close(kd); + return -1; + } + + if (cnt == 0) { + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + kvm_close(kd); + return -1; + } + + *pids_array = malloc(cnt * sizeof(pid_t)); + if (!*pids_array) { + PyErr_NoMemory(); + kvm_close(kd); + return -1; + } + + for (i = 0; i < (size_t)cnt; i++) { + (*pids_array)[i] = result[i].p_pid; + } + + *pids_count = cnt; + kvm_close(kd); + return 0; +} diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index a60c8e9065..8c97009362 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -56,56 +56,6 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // APIS // ============================================================================ -int -_psutil_pids(struct kinfo_proc **procList, size_t *procCount) { - // Returns a list of all BSD processes on the system. This routine - // allocates the list and puts it in *procList and a count of the - // number of entries in *procCount. You are responsible for freeing - // this list (use "free" from System framework). - // On success, the function returns 0. - // On error, the function returns a BSD errno value. - struct kinfo_proc *result; - // Declaring name as const requires us to cast it when passing it to - // sysctl because the prototype doesn't include the const modifier. - char errbuf[_POSIX2_LINE_MAX]; - int cnt; - kvm_t *kd; - - assert(procList != NULL); - assert(*procList == NULL); - assert(procCount != NULL); - - kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); - if (! kd) { - convert_kvm_err("kvm_openfiles", errbuf); - return -1; - } - - result = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt); - if (result == NULL) { - PyErr_Format(PyExc_RuntimeError, "kvm_getprocs syscall failed"); - kvm_close(kd); - return -1; - } - - *procCount = (size_t)cnt; - - size_t mlen = cnt * sizeof(struct kinfo_proc); - - if ((*procList = malloc(mlen)) == NULL) { - PyErr_NoMemory(); - kvm_close(kd); - return -1; - } - - memcpy(*procList, result, mlen); - assert(*procList != NULL); - kvm_close(kd); - - return 0; -} - - // TODO: refactor this (it's clunky) PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 7822151e8a..4a13f73e0d 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -10,6 +10,7 @@ extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; int psutil_setup_osx(void); +int _psutil_pids(pid_t **pids_array, int *pids_count); PyObject *psutil_boot_time(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); @@ -23,7 +24,6 @@ PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); PyObject *psutil_has_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); -PyObject *psutil_pids(PyObject *self, PyObject *args); PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/pids.c b/psutil/arch/osx/pids.c new file mode 100644 index 0000000000..2c3ddaa82b --- /dev/null +++ b/psutil/arch/osx/pids.c @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +int +_psutil_pids(pid_t **pids_array, int *pids_count) { + int mib[3] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL}; + size_t len = 0; + char *buf = NULL; + struct kinfo_proc *proc_list = NULL; + size_t num_procs = 0; + + *pids_array = NULL; + *pids_count = 0; + + if (psutil_sysctl_malloc(mib, 3, &buf, &len) != 0) + return -1; + + if (len == 0) { + PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + goto error; + } + + proc_list = (struct kinfo_proc *)buf; + num_procs = len / sizeof(struct kinfo_proc); + + *pids_array = malloc(num_procs * sizeof(pid_t)); + if (!*pids_array) { + PyErr_NoMemory(); + goto error; + } + + for (size_t i = 0; i < num_procs; i++) { + (*pids_array)[i] = proc_list[i].kp_proc.p_pid; + } + + *pids_count = (int)num_procs; + free(buf); + return 0; + +error: + if (buf != NULL) + free(buf); + return -1; +} diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 664f408d4f..d7b7373157 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -40,34 +40,6 @@ // ==================================================================== -static int -_psutil_pids(struct kinfo_proc **proc_list, size_t *proc_count) { - int mib[3]; - size_t len = 0; - char *buf = NULL; - - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_ALL; - *proc_list = NULL; - *proc_count = 0; - - if (psutil_sysctl_malloc(mib, 3, &buf, &len) != 0) - return -1; - - *proc_list = (struct kinfo_proc *)buf; - *proc_count = len / sizeof(struct kinfo_proc); - - if (*proc_count == 0) { - free(buf); - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); - return -1; - } - - return 0; -} - - static int psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { int mib[4]; @@ -284,48 +256,6 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { // ==================================================================== -/* - * Return a Python list of all the PIDs running on the system. - */ -PyObject * -psutil_pids(PyObject *self, PyObject *args) { - struct kinfo_proc *proclist = NULL; - struct kinfo_proc *orig_address = NULL; - size_t num_processes; - size_t idx; - PyObject *py_pid = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - if (_psutil_pids(&proclist, &num_processes) != 0) - goto error; - - // save the address of proclist so we can free it later - orig_address = proclist; - for (idx = 0; idx < num_processes; idx++) { - py_pid = PyLong_FromPid(proclist->kp_proc.p_pid); - if (! py_pid) - goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_CLEAR(py_pid); - proclist++; - } - free(orig_address); - - return py_retlist; - -error: - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - if (orig_address != NULL) - free(orig_address); - return NULL; -} - - // Return True if PID is a zombie else False, including if PID does not // exist or the underlying function fails. PyObject * diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index ffe275b895..3f2db7df9f 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -72,7 +72,7 @@ PyObject *TimeoutExpired; PyObject *TimeoutAbandoned; -DWORD* _psutil_pids(DWORD *numberOfReturnedPIDs); +int _psutil_pids(DWORD **pids_array, int *pids_count); HANDLE psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code); HANDLE psutil_handle_from_pid(DWORD pid, DWORD dwDesiredAccess); int psutil_assert_pid_exists(DWORD pid, char *err); diff --git a/psutil/arch/windows/pids.c b/psutil/arch/windows/pids.c new file mode 100644 index 0000000000..6f4b4ee6a2 --- /dev/null +++ b/psutil/arch/windows/pids.c @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include + +int +_psutil_pids(DWORD **pids_array, int *pids_count) { + DWORD *proc_array = NULL; + DWORD proc_array_bytes; + int proc_array_sz = 0; + DWORD enum_return_bytes = 0; + + *pids_array = NULL; + *pids_count = 0; + + do { + proc_array_sz += 1024; + if (proc_array != NULL) + free(proc_array); + + proc_array_bytes = proc_array_sz * sizeof(DWORD); + proc_array = malloc(proc_array_bytes); + if (proc_array == NULL) { + PyErr_NoMemory(); + return -1; + } + + if (!EnumProcesses(proc_array, proc_array_bytes, &enum_return_bytes)) { + free(proc_array); + PyErr_SetFromWindowsErr(0); + return -1; + } + + // Retry if our buffer was too small. + } while (enum_return_bytes == proc_array_bytes); + + *pids_count = (int)(enum_return_bytes / sizeof(DWORD)); + *pids_array = proc_array; + return 0; +} diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 22f8c95b36..c62897bb41 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -49,46 +49,6 @@ psutil_pid_exists(PyObject *self, PyObject *args) { } -/* - * Return a Python list of all the PIDs running on the system. - */ -PyObject * -psutil_pids(PyObject *self, PyObject *args) { - DWORD *proclist = NULL; - DWORD numberOfReturnedPIDs; - DWORD i; - PyObject *py_pid = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - proclist = _psutil_pids(&numberOfReturnedPIDs); - if (proclist == NULL) - goto error; - - for (i = 0; i < numberOfReturnedPIDs; i++) { - py_pid = PyLong_FromPid(proclist[i]); - if (!py_pid) - goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_CLEAR(py_pid); - } - - // free C array allocated for PIDs - free(proclist); - return py_retlist; - -error: - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - if (proclist != NULL) - free(proclist); - return NULL; -} - - /* * Kill a process given its PID. */ diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index e558afcf64..d272e91251 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -13,64 +13,25 @@ #include "../../arch/all/init.h" -DWORD * -_psutil_pids(DWORD *numberOfReturnedPIDs) { - // Win32 SDK says the only way to know if our process array - // wasn't large enough is to check the returned size and make - // sure that it doesn't match the size of the array. - // If it does we allocate a larger array and try again - - // Stores the actual array - DWORD *procArray = NULL; - DWORD procArrayByteSz; - int procArraySz = 0; - - // Stores the byte size of the returned array from enumprocesses - DWORD enumReturnSz = 0; - - do { - procArraySz += 1024; - if (procArray != NULL) - free(procArray); - procArrayByteSz = procArraySz * sizeof(DWORD); - procArray = malloc(procArrayByteSz); - if (procArray == NULL) { - PyErr_NoMemory(); - return NULL; - } - if (! EnumProcesses(procArray, procArrayByteSz, &enumReturnSz)) { - free(procArray); - PyErr_SetFromWindowsErr(0); - return NULL; - } - } while (enumReturnSz == procArraySz * sizeof(DWORD)); - - // The number of elements is the returned size / size of each element - *numberOfReturnedPIDs = enumReturnSz / sizeof(DWORD); - - return procArray; -} - // Return 1 if PID exists, 0 if not, -1 on error. int psutil_pid_in_pids(DWORD pid) { - DWORD *proclist = NULL; - DWORD numberOfReturnedPIDs; - DWORD i; + DWORD *pids_array = NULL; + int pids_count = 0; + int i; - proclist = _psutil_pids(&numberOfReturnedPIDs); - if (proclist == NULL) { - psutil_debug("_psutil_pids() failed"); + if (_psutil_pids(&pids_array, &pids_count) != 0) return -1; - } - for (i = 0; i < numberOfReturnedPIDs; i++) { - if (proclist[i] == pid) { - free(proclist); + + for (i = 0; i < pids_count; i++) { + if (pids_array[i] == pid) { + free(pids_array); return 1; } } - free(proclist); + + free(pids_array); return 0; } diff --git a/setup.py b/setup.py index b4bdf57bb8..9a4430f6d6 100755 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ macros.append(('PSUTIL_SIZEOF_PID_T', '8')) # long -sources = ['psutil/arch/all/init.c'] +sources = glob.glob("psutil/arch/all/*.c") if POSIX: sources.extend(glob.glob("psutil/arch/posix/*.c")) @@ -321,7 +321,7 @@ def get_winver(): ) elif MACOS: - macros.append(("PSUTIL_OSX", 1)) + macros.extend([("PSUTIL_OSX", 1), ("PSUTIL_MACOS", 1)]) ext = Extension( 'psutil._psutil_osx', sources=( From dc77246d4f96e48ac5fb6eea244ecd1b10d63419 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:12:49 +0200 Subject: [PATCH 1376/1714] Silence some verbose test --- psutil/tests/test_testutils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 14a241f29c..a5aa75acdf 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -8,6 +8,7 @@ import collections import errno +import io import os import socket import stat @@ -446,7 +447,8 @@ class TestFakePytest(PsutilTestCase): def run_test_class(self, klass): suite = unittest.TestSuite() suite.addTest(klass) - runner = unittest.TextTestRunner() + # silence output + runner = unittest.TextTestRunner(stream=io.StringIO()) result = runner.run(suite) return result From 03717661254f7d4dab4582e22a4363af08025f06 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:16:41 +0200 Subject: [PATCH 1377/1714] Silence more output --- psutil/tests/test_testutils.py | 9 +++++++-- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index a5aa75acdf..a8814139a0 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -7,6 +7,7 @@ """Tests for testing utils (psutil.tests namespace).""" import collections +import contextlib import errno import io import os @@ -402,7 +403,10 @@ def fun(ls=ls): try: # will consume around 60M in total with pytest.raises(pytest.fail.Exception, match="extra-mem"): - self.execute(fun, times=100) + with contextlib.redirect_stdout( + io.StringIO() + ), contextlib.redirect_stderr(io.StringIO()): + self.execute(fun, times=100) finally: del ls @@ -527,7 +531,8 @@ def test_passed(self): pass """).lstrip()) with mock.patch.object(psutil.tests, "HERE", tmpdir): - suite = fake_pytest.main() + with contextlib.redirect_stderr(io.StringIO()): + suite = fake_pytest.main() assert suite.countTestCases() == 1 def test_warns(self): diff --git a/pyproject.toml b/pyproject.toml index 36f3f4dd30..a34580d5a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -252,6 +252,7 @@ addopts = ''' --strict-config --strict-markers --instafail + --no-subtests-reports -p no:junitxml -p no:doctest -p no:nose From 1504c3ddebaebd1717faa3b3202afee953ede305 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:19:46 +0200 Subject: [PATCH 1378/1714] Pin pytest-subtests>=0.15 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a4430f6d6..a94d726775 100755 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ TEST_DEPS = [ "pytest", "pytest-instafail", - "pytest-subtests", + "pytest-subtests>=0.15", "pytest-xdist", "setuptools", "pywin32 ; os_name == 'nt' and platform_python_implementation != 'PyPy'", From bd00af9e271be1978b26f1c5d42db6125c098135 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:24:22 +0200 Subject: [PATCH 1379/1714] Make install-pydeps-test before starting tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e58f3fa576..89fbeb4b24 100644 --- a/Makefile +++ b/Makefile @@ -237,13 +237,13 @@ ci-test: ## Run tests on GitHub CI. Used by BSD runners. ${MAKE} install-sysdeps PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test ${MAKE} print-sysinfo - $(PYTHON) -m pip list $(PYTHON_ENV_VARS) $(PYTHON) -m pytest psutil/tests/ ci-test-cibuildwheel: ## Run tests from cibuildwheel. # testing the wheels means we can't use other test targets which are rebuilding the python extensions # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed ${MAKE} install-sysdeps + PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test ${MAKE} print-sysinfo mkdir -p .tests cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs psutil.tests From bfb35bd4cd765d7ba420fff8f455266e67894578 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:24:51 +0200 Subject: [PATCH 1380/1714] Make install-pydeps-test before starting tests --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 89fbeb4b24..35ac61d74f 100644 --- a/Makefile +++ b/Makefile @@ -75,12 +75,12 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip - $(PYTHON) -m pip install -U $(PIP_INSTALL_ARGS) -e .[test] + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test] install-pydeps-dev: ## Install python deps meant for local development. ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install -U $(PIP_INSTALL_ARGS) -e .[test,dev] + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test,dev] install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit From b891009572adac8178038667c313eff029c78ec2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:30:23 +0200 Subject: [PATCH 1381/1714] Giveup on -no-subtests-reports --- pyproject.toml | 1 - setup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a34580d5a1..36f3f4dd30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -252,7 +252,6 @@ addopts = ''' --strict-config --strict-markers --instafail - --no-subtests-reports -p no:junitxml -p no:doctest -p no:nose diff --git a/setup.py b/setup.py index a94d726775..9a4430f6d6 100755 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ TEST_DEPS = [ "pytest", "pytest-instafail", - "pytest-subtests>=0.15", + "pytest-subtests", "pytest-xdist", "setuptools", "pywin32 ; os_name == 'nt' and platform_python_implementation != 'PyPy'", From 31bbc777e9d41c40ac4dcfb675ebe92ff40bb33f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 02:31:34 +0200 Subject: [PATCH 1382/1714] Silence some test output --- Makefile | 6 +++--- psutil/tests/test_testutils.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index e58f3fa576..35ac61d74f 100644 --- a/Makefile +++ b/Makefile @@ -75,12 +75,12 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. ${MAKE} install-pip - $(PYTHON) -m pip install -U $(PIP_INSTALL_ARGS) -e .[test] + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test] install-pydeps-dev: ## Install python deps meant for local development. ${MAKE} install-git-hooks ${MAKE} install-pip - $(PYTHON) -m pip install -U $(PIP_INSTALL_ARGS) -e .[test,dev] + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test,dev] install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit @@ -237,13 +237,13 @@ ci-test: ## Run tests on GitHub CI. Used by BSD runners. ${MAKE} install-sysdeps PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test ${MAKE} print-sysinfo - $(PYTHON) -m pip list $(PYTHON_ENV_VARS) $(PYTHON) -m pytest psutil/tests/ ci-test-cibuildwheel: ## Run tests from cibuildwheel. # testing the wheels means we can't use other test targets which are rebuilding the python extensions # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed ${MAKE} install-sysdeps + PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test ${MAKE} print-sysinfo mkdir -p .tests cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs psutil.tests diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 14a241f29c..a8814139a0 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -7,7 +7,9 @@ """Tests for testing utils (psutil.tests namespace).""" import collections +import contextlib import errno +import io import os import socket import stat @@ -401,7 +403,10 @@ def fun(ls=ls): try: # will consume around 60M in total with pytest.raises(pytest.fail.Exception, match="extra-mem"): - self.execute(fun, times=100) + with contextlib.redirect_stdout( + io.StringIO() + ), contextlib.redirect_stderr(io.StringIO()): + self.execute(fun, times=100) finally: del ls @@ -446,7 +451,8 @@ class TestFakePytest(PsutilTestCase): def run_test_class(self, klass): suite = unittest.TestSuite() suite.addTest(klass) - runner = unittest.TextTestRunner() + # silence output + runner = unittest.TextTestRunner(stream=io.StringIO()) result = runner.run(suite) return result @@ -525,7 +531,8 @@ def test_passed(self): pass """).lstrip()) with mock.patch.object(psutil.tests, "HERE", tmpdir): - suite = fake_pytest.main() + with contextlib.redirect_stderr(io.StringIO()): + suite = fake_pytest.main() assert suite.countTestCases() == 1 def test_warns(self): From bd4955ee7600292bf87c1dc43f13ae686235f486 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 13:25:06 +0200 Subject: [PATCH 1383/1714] Change link to download stats --- README.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 16c756c50e..9eb011aa0c 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ | |downloads| |stars| |forks| |contributors| |coverage| -| |version| |py-versions| |packages| |license| +| |version| |packages| |license| | |github-actions-wheels| |github-actions-bsd| |doc| |twitter| |tidelift| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg - :target: https://pepy.tech/project/psutil + :target: https://clickpy.clickhouse.com/dashboard/psutil :alt: Downloads .. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg @@ -38,9 +38,6 @@ :target: https://pypi.org/project/psutil :alt: Latest version -.. |py-versions| image:: https://img.shields.io/pypi/pyversions/psutil.svg - :alt: Supported Python versions - .. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg :target: https://repology.org/metapackage/python:psutil/versions :alt: Binary packages From 86cfc2e0ee2f2a4e665fb40cfad3ad01662e8a0e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 13:30:39 +0200 Subject: [PATCH 1384/1714] Update generate_manifest.py so that it does not unnecessarily overwrite MANIFEST.in --- Makefile | 2 +- scripts/internal/generate_manifest.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 35ac61d74f..0724dd7dd5 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ check-dist: ## Run all sanity checks re. to the package distribution. ${MAKE} check-wheels generate-manifest: ## Generates MANIFEST.in file. - $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in + $(PYTHON) scripts/internal/generate_manifest.py sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 090324a93e..d9444ad896 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -13,6 +13,12 @@ SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') SKIP_FILES = () SKIP_PREFIXES = ('.ci/', '.github/') +FILE = "MANIFEST.in" + + +def cat(path): + with open(path) as f: + return f.read() def sh(cmd): @@ -23,6 +29,7 @@ def sh(cmd): def main(): files = set() + text = [] for file in sh("git ls-files").split('\n'): if ( file.startswith(SKIP_PREFIXES) @@ -33,9 +40,16 @@ def main(): files.add(file) for file in sorted(files): - print("include " + file) + text.append(f"include {file}") # noqa: PERF401 + + text.append("recursive-exclude docs/_static *") # noqa: FURB113 + text.append("") + text = "\n".join(text) - print("recursive-exclude docs/_static *") + if cat(FILE) != text: + with open(FILE, "w") as f: + f.write(text) + print(f"{FILE} was updated") if __name__ == '__main__': From d70efbf42583c20b7e813b2fe7da89a10938d96d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 13:51:16 +0200 Subject: [PATCH 1385/1714] Revert prev commit --- Makefile | 2 +- scripts/internal/generate_manifest.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 35ac61d74f..0724dd7dd5 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ check-dist: ## Run all sanity checks re. to the package distribution. ${MAKE} check-wheels generate-manifest: ## Generates MANIFEST.in file. - $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in + $(PYTHON) scripts/internal/generate_manifest.py sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 090324a93e..d9444ad896 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -13,6 +13,12 @@ SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') SKIP_FILES = () SKIP_PREFIXES = ('.ci/', '.github/') +FILE = "MANIFEST.in" + + +def cat(path): + with open(path) as f: + return f.read() def sh(cmd): @@ -23,6 +29,7 @@ def sh(cmd): def main(): files = set() + text = [] for file in sh("git ls-files").split('\n'): if ( file.startswith(SKIP_PREFIXES) @@ -33,9 +40,16 @@ def main(): files.add(file) for file in sorted(files): - print("include " + file) + text.append(f"include {file}") # noqa: PERF401 + + text.append("recursive-exclude docs/_static *") # noqa: FURB113 + text.append("") + text = "\n".join(text) - print("recursive-exclude docs/_static *") + if cat(FILE) != text: + with open(FILE, "w") as f: + f.write(text) + print(f"{FILE} was updated") if __name__ == '__main__': From d59cda34ade843bbc8faea50a34fc979b0c0e8ef Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 22:07:42 +0200 Subject: [PATCH 1386/1714] Fix #2657: rm python 32-bit CI jobs + wheels --- .github/workflows/build.yml | 2 -- HISTORY.rst | 13 +++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2cf1cb36cf..45989a4706 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,12 +25,10 @@ jobs: matrix: include: - { os: ubuntu-latest, arch: x86_64 } - - { os: ubuntu-latest, arch: i686 } - { os: ubuntu-24.04-arm, arch: aarch64 } - { os: macos-13, arch: x86_64 } - { os: macos-14, arch: arm64 } - { os: windows-2025, arch: AMD64 } - - { os: windows-2025, arch: x86 } - { os: windows-11-arm, arch: ARM64 } steps: - uses: actions/checkout@v5 diff --git a/HISTORY.rst b/HISTORY.rst index f4d2aa5453..9a7546b50e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,11 +6,20 @@ XXXX-XX-XX +**Enhancements** + +- 2657_, [Linux], [Windows]: no longer publish wheels for Python 32 bit. + **Bug fixes** * 2650_, [macOS]: `Process.cmdline()`_ and `Process.environ()`_ may incorrectly raise `NoSuchProcess`_ instead of `ZombieProcess`_. +**Compatibility notes** + +- 2657_: Linux and Windows wheels for Python 32 bit are no longer uploaded, + meaning pip will install psutil from sources. + 7.1.1 ===== @@ -18,8 +27,8 @@ XXXX-XX-XX **Enhancements** -* 2645_, [SunOS]: dropped support for SunOS 10. -* 2646_, [SunOS]: add CI test runner for SunOS. +- 2645_, [SunOS]: dropped support for SunOS 10. +- 2646_, [SunOS]: add CI test runner for SunOS. **Bug fixes** From f77c823072813c950e4f46b281642812073ba987 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 23 Oct 2025 22:38:35 +0200 Subject: [PATCH 1387/1714] Build wheels for cp313t (#2609) --- .github/workflows/build.yml | 2 +- psutil/tests/test_process.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45989a4706..03c850c9f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: uses: pypa/cibuildwheel@v3.2.1 env: CIBW_ARCHS: "${{ matrix.arch }}" - CIBW_ENABLE: "${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" + CIBW_ENABLE: "cpython-freethreading ${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" - name: Upload wheels uses: actions/upload-artifact@v4 diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index fb0767c4bb..c473c190d7 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -768,6 +768,8 @@ def test_long_cmdline(self): def test_name(self): p = self.spawn_psproc() name = p.name().lower() + if name.endswith("t"): # in the free-threaded build + name = name[:-1] pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) From f33513e206efe1db84bb69470fef071e8f8b58c3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 23 Oct 2025 22:58:11 +0200 Subject: [PATCH 1388/1714] Give credits for #2565 / @lysnikolaou --- HISTORY.rst | 4 +++- docs/index.rst | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9a7546b50e..08de1372d8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,7 +8,9 @@ XXXX-XX-XX **Enhancements** -- 2657_, [Linux], [Windows]: no longer publish wheels for Python 32 bit. +- 2657_, [Linux], [Windows]: stop publishing wheels for Python 32 bit. + 2565_: produce wheels for free-thread cPython 3.13 (patch by Lysandros + Nikolaou) **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index 32b5f0da22..648229672a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2676,6 +2676,8 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= +* psutil 7.1.2 (XXXX-XX): publish wheels for free-threaded Python +* psutil 7.1.2 (XXXX-XX): unpublish wheels for cPython 32-bit * psutil 7.1.1 (2025-10): drop **SunOS 10** * psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 From 24fc7dbde26e4c93f534c9a31863eb1ed25d162f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Oct 2025 00:00:55 +0200 Subject: [PATCH 1389/1714] macos, fix #2658: don't double-free in case of environ() error --- psutil/arch/osx/proc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index d7b7373157..9b3ca73566 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -1126,7 +1126,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { goto error; } - while (*arg_ptr != '\0' && arg_ptr < arg_end) { + while (arg_ptr < arg_end && *arg_ptr != '\0') { char *s = memchr(arg_ptr + 1, '\0', arg_end - arg_ptr); if (s == NULL) break; @@ -1158,6 +1158,6 @@ psutil_proc_environ(PyObject *self, PyObject *args) { if (procargs != NULL) free(procargs); if (procenv != NULL) - free(procargs); + free(procenv); return NULL; } From 439358dedf908a52e5ae512d3d2908af0dfdb2e4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Oct 2025 00:02:14 +0200 Subject: [PATCH 1390/1714] Update HISTORY.rst --- HISTORY.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 08de1372d8..e4b2bf696b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,8 +14,10 @@ XXXX-XX-XX **Bug fixes** -* 2650_, [macOS]: `Process.cmdline()`_ and `Process.environ()`_ may incorrectly +- 2650_, [macOS]: `Process.cmdline()`_ and `Process.environ()`_ may incorrectly raise `NoSuchProcess`_ instead of `ZombieProcess`_. +- 2658_, [macOS]: double ``free()`` in `Process.environ()`_ when it fails + internally. This posed a risk of segfault. **Compatibility notes** From 68585cae2b4b9a88df7a000de177e56443bd6b86 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Oct 2025 18:18:47 +0200 Subject: [PATCH 1391/1714] [macOS] various fixes to prevent potential segfaults (#2660) --- Makefile | 2 +- psutil/arch/osx/cpu.c | 200 +++++++++++--------------- psutil/arch/osx/disk.c | 65 +++------ psutil/arch/osx/net.c | 12 +- psutil/arch/osx/proc.c | 68 +++++---- psutil/arch/osx/sensors.c | 11 +- scripts/internal/generate_manifest.py | 18 +-- 7 files changed, 156 insertions(+), 220 deletions(-) diff --git a/Makefile b/Makefile index 0724dd7dd5..35ac61d74f 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ check-dist: ## Run all sanity checks re. to the package distribution. ${MAKE} check-wheels generate-manifest: ## Generates MANIFEST.in file. - $(PYTHON) scripts/internal/generate_manifest.py + $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in sdist: ## Create tar.gz source distribution. ${MAKE} generate-manifest diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 6160b2ad51..c2153b7641 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -43,9 +43,8 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { int num; if (psutil_sysctlbyname("hw.logicalcpu", &num, sizeof(num)) != 0) - Py_RETURN_NONE; // mimic os.cpu_count() - else - return Py_BuildValue("i", num); + Py_RETURN_NONE; + return Py_BuildValue("i", num); } @@ -54,9 +53,8 @@ psutil_cpu_count_cores(PyObject *self, PyObject *args) { int num; if (psutil_sysctlbyname("hw.physicalcpu", &num, sizeof(num)) != 0) - Py_RETURN_NONE; // mimic os.cpu_count() - else - return Py_BuildValue("i", num); + Py_RETURN_NONE; + return Py_BuildValue("i", num); } @@ -65,9 +63,8 @@ psutil_cpu_times(PyObject *self, PyObject *args) { mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT; kern_return_t error; host_cpu_load_info_data_t r_load; - mach_port_t mport; + mach_port_t mport = mach_host_self(); - mport = mach_host_self(); if (mport == MACH_PORT_NULL) { PyErr_SetString(PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL"); @@ -81,10 +78,11 @@ psutil_cpu_times(PyObject *self, PyObject *args) { return PyErr_Format( PyExc_RuntimeError, "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", - mach_error_string(error)); + mach_error_string(error) + ); } - mach_port_deallocate(mach_task_self(), mport); + mach_port_deallocate(mach_task_self(), mport); return Py_BuildValue( "(dddd)", (double)r_load.cpu_ticks[CPU_STATE_USER] / CLK_TCK, @@ -98,41 +96,40 @@ psutil_cpu_times(PyObject *self, PyObject *args) { PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { kern_return_t ret; - mach_msg_type_number_t count; - mach_port_t mport; + mach_msg_type_number_t count = HOST_VM_INFO_COUNT; + mach_port_t mport = mach_host_self(); struct vmmeter vmstat; - mport = mach_host_self(); if (mport == MACH_PORT_NULL) { PyErr_SetString(PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL"); return NULL; } - count = HOST_VM_INFO_COUNT; ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); if (ret != KERN_SUCCESS) { mach_port_deallocate(mach_task_self(), mport); PyErr_Format( PyExc_RuntimeError, "host_statistics(HOST_VM_INFO) failed: %s", - mach_error_string(ret)); + mach_error_string(ret) + ); return NULL; } mach_port_deallocate(mach_task_self(), mport); return Py_BuildValue( "IIIII", - vmstat.v_swtch, // context switches - vmstat.v_intr, // interrupts - vmstat.v_soft, // software interrupts + vmstat.v_swtch, + vmstat.v_intr, + vmstat.v_soft, #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) \ && __MAC_OS_X_VERSION_MIN_REQUIRED__ >= 120000 0, #else - vmstat.v_syscall, // system calls (if available) + vmstat.v_syscall, #endif - vmstat.v_trap // traps + vmstat.v_trap ); } @@ -146,27 +143,23 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { // "AppleARMIODevice" is not available. static int psutil_find_pmgr_entry(io_registry_entry_t *out_entry) { - kern_return_t status; - io_iterator_t iter = 0; - io_registry_entry_t entry = 0; - CFDictionaryRef matching; - io_name_t name; + io_iterator_t iter = IO_OBJECT_NULL; + io_registry_entry_t entry = IO_OBJECT_NULL; + CFDictionaryRef matching = IOServiceMatching("AppleARMIODevice"); int found = 0; - *out_entry = 0; - - matching = IOServiceMatching("AppleARMIODevice"); - if (matching == NULL) + if (!out_entry || !matching) return 0; status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); - if (status != KERN_SUCCESS) + if (status != KERN_SUCCESS || iter == IO_OBJECT_NULL) return 0; - while ((entry = IOIteratorNext(iter)) != 0) { - status = IORegistryEntryGetName(entry, name); - if (status == KERN_SUCCESS && strcmp(name, "pmgr") == 0) { + while ((entry = IOIteratorNext(iter)) != IO_OBJECT_NULL) { + io_name_t name; + if (IORegistryEntryGetName(entry, name) == KERN_SUCCESS && + strcmp(name, "pmgr") == 0) { found = 1; break; } @@ -176,19 +169,18 @@ psutil_find_pmgr_entry(io_registry_entry_t *out_entry) { IOObjectRelease(iter); if (found) { - *out_entry = entry; // caller must release - return -1; + *out_entry = entry; + return 1; } - return 0; } // Python wrapper: return True/False. PyObject * psutil_has_cpu_freq(PyObject *self, PyObject *args) { - io_registry_entry_t entry = 0; + io_registry_entry_t entry = IO_OBJECT_NULL; int ok = psutil_find_pmgr_entry(&entry); - if (entry != 0) + if (entry != IO_OBJECT_NULL) IOObjectRelease(entry); if (ok) Py_RETURN_TRUE; @@ -198,43 +190,35 @@ psutil_has_cpu_freq(PyObject *self, PyObject *args) { PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { - io_registry_entry_t entry = 0; + io_registry_entry_t entry = IO_OBJECT_NULL; CFTypeRef pCoreRef = NULL; CFTypeRef eCoreRef = NULL; - size_t pCoreLength; + size_t pCoreLength = 0; uint32_t pMin = 0, eMin = 0, min = 0, max = 0, curr = 0; if (!psutil_find_pmgr_entry(&entry)) { return PyErr_Format( PyExc_RuntimeError, - "'pmgr' entry was not found in AppleARMIODevice service" + "'pmgr' entry not found in AppleARMIODevice" ); } pCoreRef = IORegistryEntryCreateCFProperty( entry, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, 0 ); - if (pCoreRef == NULL || - CFGetTypeID(pCoreRef) != CFDataGetTypeID() || - CFDataGetLength(pCoreRef) < 8) { - PyErr_SetString( - PyExc_RuntimeError, - "'voltage-states5-sram' is missing or invalid" - ); - goto error; - } - eCoreRef = IORegistryEntryCreateCFProperty( entry, CFSTR("voltage-states1-sram"), kCFAllocatorDefault, 0 ); - if (eCoreRef == NULL || - CFGetTypeID(eCoreRef) != CFDataGetTypeID() || - CFDataGetLength(eCoreRef) < 4) { - PyErr_SetString( - PyExc_RuntimeError, - "'voltage-states1-sram' is missing or invalid" - ); - goto error; + + if (!pCoreRef || + !eCoreRef || + CFGetTypeID(pCoreRef) != CFDataGetTypeID() || + CFGetTypeID(eCoreRef) != CFDataGetTypeID() || + CFDataGetLength(pCoreRef) < 8 || + CFDataGetLength(eCoreRef) < 4) + { + PyErr_SetString(PyExc_RuntimeError, "invalid CPU frequency data"); + goto cleanup; } pCoreLength = CFDataGetLength(pCoreRef); @@ -245,30 +229,27 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { min = (pMin < eMin) ? pMin : eMin; curr = max; +cleanup: if (pCoreRef) CFRelease(pCoreRef); if (eCoreRef) CFRelease(eCoreRef); - if (entry) + if (entry != IO_OBJECT_NULL) IOObjectRelease(entry); + if (PyErr_Occurred()) + return NULL; + return Py_BuildValue( "KKK", (unsigned long long)(curr / 1000 / 1000), (unsigned long long)(min / 1000 / 1000), (unsigned long long)(max / 1000 / 1000) ); - -error: - if (pCoreRef) - CFRelease(pCoreRef); - if (eCoreRef) - CFRelease(eCoreRef); - if (entry) - IOObjectRelease(entry); - return NULL; } + #else // not ARM64 / ARCH64 + PyObject * psutil_has_cpu_freq(PyObject *self, PyObject *args) { Py_RETURN_TRUE; @@ -277,13 +258,8 @@ psutil_has_cpu_freq(PyObject *self, PyObject *args) { PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { unsigned int curr; - int64_t min; - int64_t max; - int mib[2]; - - // also available as "hw.cpufrequency" but it's deprecated - mib[0] = CTL_HW; - mib[1] = HW_CPU_FREQ; + int64_t min = 0, max = 0; + int mib[2] = {CTL_HW, HW_CPU_FREQ}; if (psutil_sysctl(mib, 2, &curr, sizeof(curr)) < 0) return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); @@ -302,78 +278,70 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { "KKK", (unsigned long long)(curr / 1000 / 1000), (unsigned long long)(min / 1000 / 1000), - (unsigned long long)(max / 1000 / 1000)); + (unsigned long long)(max / 1000 / 1000) + ); } -#endif + +#endif // ARM64 PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { - natural_t cpu_count; - natural_t i; + natural_t cpu_count = 0; + mach_msg_type_number_t info_count = 0; + processor_cpu_load_info_data_t *cpu_load_info = NULL; processor_info_array_t info_array = NULL; - mach_msg_type_number_t info_count; kern_return_t error; - processor_cpu_load_info_data_t *cpu_load_info = NULL; - int ret; - mach_port_t mport; + mach_port_t mport = mach_host_self(); PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; - if (py_retlist == NULL) + if (!py_retlist) return NULL; - mport = mach_host_self(); if (mport == MACH_PORT_NULL) { - PyErr_SetString(PyExc_RuntimeError, - "mach_host_self() returned MACH_PORT_NULL"); + PyErr_SetString(PyExc_RuntimeError, "mach_host_self() returned NULL"); goto error; } - error = host_processor_info(mport, PROCESSOR_CPU_LOAD_INFO, - &cpu_count, &info_array, &info_count); - if (error != KERN_SUCCESS) { - mach_port_deallocate(mach_task_self(), mport); - PyErr_Format( - PyExc_RuntimeError, - "host_processor_info(PROCESSOR_CPU_LOAD_INFO) syscall failed: %s", - mach_error_string(error)); + error = host_processor_info(mport, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count); + mach_port_deallocate(mach_task_self(), mport); + + if (error != KERN_SUCCESS || !info_array) { + PyErr_Format(PyExc_RuntimeError, "host_processor_info failed: %s", mach_error_string(error)); goto error; } - mach_port_deallocate(mach_task_self(), mport); - cpu_load_info = (processor_cpu_load_info_data_t *) info_array; + cpu_load_info = (processor_cpu_load_info_data_t *)info_array; - for (i = 0; i < cpu_count; i++) { - py_cputime = Py_BuildValue( + for (natural_t i = 0; i < cpu_count; i++) { + PyObject *py_cputime = Py_BuildValue( "(dddd)", (double)cpu_load_info[i].cpu_ticks[CPU_STATE_USER] / CLK_TCK, (double)cpu_load_info[i].cpu_ticks[CPU_STATE_NICE] / CLK_TCK, (double)cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM] / CLK_TCK, (double)cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE] / CLK_TCK ); - if (!py_cputime) + if (!py_cputime) { goto error; + } if (PyList_Append(py_retlist, py_cputime)) { Py_DECREF(py_cputime); - goto error; } - Py_CLEAR(py_cputime); + Py_DECREF(py_cputime); } - ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, - info_count * sizeof(integer_t)); - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); + vm_deallocate( + mach_task_self(), + (vm_address_t)info_array, + info_count * sizeof(integer_t) + ); return py_retlist; error: - Py_XDECREF(py_cputime); - Py_DECREF(py_retlist); - if (info_array != NULL) { - ret = vm_deallocate(mach_task_self(), (vm_address_t)info_array, - info_count * sizeof(integer_t)); - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); - } + Py_XDECREF(py_retlist); + vm_deallocate( + mach_task_self(), + (vm_address_t)info_array, + info_count * sizeof(integer_t) + ); return NULL; } diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index 93a4e82529..569c63f217 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -208,12 +208,12 @@ psutil_disk_usage_used(PyObject *self, PyObject *args) { */ PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { - CFDictionaryRef parent_dict; - CFDictionaryRef props_dict; - CFDictionaryRef stats_dict; - io_registry_entry_t parent; - io_registry_entry_t disk; - io_iterator_t disk_list = 0; + CFDictionaryRef parent_dict = NULL; + CFDictionaryRef props_dict = NULL; + CFDictionaryRef stats_dict = NULL; + io_registry_entry_t parent = IO_OBJECT_NULL; + io_registry_entry_t disk = IO_OBJECT_NULL; + io_iterator_t disk_list = IO_OBJECT_NULL; PyObject *py_disk_info = NULL; PyObject *py_retdict = PyDict_New(); @@ -234,11 +234,10 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { parent_dict = NULL; props_dict = NULL; stats_dict = NULL; - parent = 0; + parent = IO_OBJECT_NULL; if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) != kIOReturnSuccess) { PyErr_SetString(PyExc_RuntimeError, "unable to get the disk's parent"); - IOObjectRelease(disk); goto error; } @@ -255,8 +254,6 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { PyErr_SetString( PyExc_RuntimeError, "unable to get the parent's properties" ); - IOObjectRelease(parent); - IOObjectRelease(disk); goto error; } @@ -267,9 +264,6 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { PyErr_SetString( PyExc_RuntimeError, "unable to get the disk properties" ); - if (parent_dict) CFRelease(parent_dict); - IOObjectRelease(parent); - IOObjectRelease(disk); goto error; } @@ -278,12 +272,6 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { ); if (disk_name_ref == NULL) { PyErr_SetString(PyExc_RuntimeError, "unable to get disk name"); - if (parent_dict) - CFRelease(parent_dict); - if (props_dict) - CFRelease(props_dict); - IOObjectRelease(parent); - IOObjectRelease(disk); goto error; } @@ -294,12 +282,6 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { PyErr_SetString( PyExc_RuntimeError, "unable to convert disk name to C string" ); - if (parent_dict) - CFRelease(parent_dict); - if (props_dict) - CFRelease(props_dict); - IOObjectRelease(parent); - IOObjectRelease(disk); goto error; } @@ -308,12 +290,6 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { ); if (stats_dict == NULL) { PyErr_SetString(PyExc_RuntimeError, "unable to get disk stats"); - if (parent_dict) - CFRelease(parent_dict); - if (props_dict) - CFRelease(props_dict); - IOObjectRelease(parent); - IOObjectRelease(disk); goto error; } @@ -355,24 +331,11 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { (unsigned long long)(write_time / 1000 / 1000) ); - if (!py_disk_info) { - if (parent_dict) - CFRelease(parent_dict); - if (props_dict) - CFRelease(props_dict); - IOObjectRelease(parent); - IOObjectRelease(disk); + if (!py_disk_info) goto error; - } if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) { Py_CLEAR(py_disk_info); - if (parent_dict) - CFRelease(parent_dict); - if (props_dict) - CFRelease(props_dict); - IOObjectRelease(parent); - IOObjectRelease(disk); goto error; } @@ -380,9 +343,9 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (parent_dict) CFRelease(parent_dict); - IOObjectRelease(parent); if (props_dict) CFRelease(props_dict); + IOObjectRelease(parent); IOObjectRelease(disk); } @@ -392,7 +355,15 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { error: Py_XDECREF(py_disk_info); Py_DECREF(py_retdict); - if (disk_list != 0) + if (parent_dict) + CFRelease(parent_dict); + if (props_dict) + CFRelease(props_dict); + if (parent != IO_OBJECT_NULL) + IOObjectRelease(parent); + if (disk != IO_OBJECT_NULL) + IOObjectRelease(disk); + if (disk_list != IO_OBJECT_NULL) IOObjectRelease(disk_list); return NULL; } diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index c11a925b98..652180c18f 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -45,13 +45,15 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { for (next = buf; next < lim; ) { // Check we have enough space for if_msghdr. if ((size_t)(lim - next) < sizeof(struct if_msghdr)) { - psutil_debug("struct xfile size mismatch"); + psutil_debug("struct xfile size mismatch (skip entry)"); + break; } ifm = (struct if_msghdr *)next; if (ifm->ifm_msglen == 0 || next + ifm->ifm_msglen > lim) { - psutil_debug("ifm_msglen size mismatch"); + psutil_debug("ifm_msglen size mismatch (skip entry)"); + break; } next += ifm->ifm_msglen; @@ -61,13 +63,15 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; if ((char *)if2m + sizeof(struct if_msghdr2) > lim) { - psutil_debug("if_msghdr2 + sockaddr_dl mismatch"); + psutil_debug("if_msghdr2 + sockaddr_dl mismatch (skip entry)"); + continue; } struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); if ((char *)sdl + sizeof(struct sockaddr_dl) > lim) { - psutil_debug("not enough buffer for sockaddr_dl"); + psutil_debug("not enough buffer for sockaddr_dl (skip entry)"); + continue; } char ifc_name[32]; diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 9b3ca73566..770fc401f8 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -1068,22 +1068,23 @@ psutil_proc_environ(PyObject *self, PyObject *args) { char *arg_end; char *env_start; size_t argmax; + size_t env_len; PyObject *py_ret = NULL; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - // special case for PID 0 (kernel_task) where cmdline cannot be fetched + // PID 0 (kernel_task) has no cmdline. if (pid == 0) goto empty; - // read argmax and allocate memory for argument space. + // Allocate buffer for process args. argmax = psutil_sysctl_argmax(); - if (! argmax) + if (argmax == 0) goto error; procargs = (char *)malloc(argmax); - if (NULL == procargs) { + if (procargs == NULL) { PyErr_NoMemory(); goto error; } @@ -1091,56 +1092,66 @@ psutil_proc_environ(PyObject *self, PyObject *args) { if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) goto error; - arg_end = &procargs[argmax]; - // copy the number of arguments to nargs + arg_end = procargs + argmax; + + // Copy nargs. memcpy(&nargs, procargs, sizeof(nargs)); // skip executable path arg_ptr = procargs + sizeof(nargs); arg_ptr = memchr(arg_ptr, '\0', arg_end - arg_ptr); - - if (arg_ptr == NULL || arg_ptr == arg_end) { - psutil_debug( - "(arg_ptr == NULL || arg_ptr == arg_end); set environ to empty"); + if (arg_ptr == NULL || arg_ptr >= arg_end) goto empty; - } - // skip ahead to the first argument - for (; arg_ptr < arg_end; arg_ptr++) { - if (*arg_ptr != '\0') - break; - } + // Skip null bytes until first argument. + while (arg_ptr < arg_end && *arg_ptr == '\0') + arg_ptr++; - // iterate through arguments + // Skip arguments. while (arg_ptr < arg_end && nargs > 0) { if (*arg_ptr++ == '\0') nargs--; } - // build an environment variable block + if (arg_ptr >= arg_end) + goto empty; + env_start = arg_ptr; - procenv = calloc(1, arg_end - arg_ptr); + // Compute maximum possible environment length. + env_len = (size_t)(arg_end - env_start); + if (env_len == 0) + goto empty; + + procenv = (char *)calloc(1, env_len); if (procenv == NULL) { PyErr_NoMemory(); goto error; } while (arg_ptr < arg_end && *arg_ptr != '\0') { - char *s = memchr(arg_ptr + 1, '\0', arg_end - arg_ptr); + // Find the next NUL terminator. + size_t rem = (size_t)(arg_end - arg_ptr); + char *s = memchr(arg_ptr, '\0', rem); if (s == NULL) break; - memcpy(procenv + (arg_ptr - env_start), arg_ptr, s - arg_ptr); + + size_t copy_len = (size_t)(s - arg_ptr); + size_t offset = (size_t)(arg_ptr - env_start); + if (offset + copy_len >= env_len) + break; // prevent overflow. + + memcpy(procenv + offset, arg_ptr, copy_len); arg_ptr = s + 1; } - py_ret = PyUnicode_DecodeFSDefaultAndSize( - procenv, arg_ptr - env_start + 1); - if (!py_ret) { - // XXX: don't want to free() this as per: - // https://github.com/giampaolo/psutil/issues/926 - // It sucks but not sure what else to do. - procargs = NULL; + size_t used = (size_t)(arg_ptr - env_start); + if (used >= env_len) + used = env_len - 1; + + py_ret = PyUnicode_DecodeFSDefaultAndSize(procenv, (Py_ssize_t)used); + if (py_ret == NULL) { + procargs = NULL; // don't double free; see psutil issue #926. goto error; } @@ -1149,6 +1160,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { return py_ret; empty: + psutil_debug("set environ to empty"); if (procargs != NULL) free(procargs); return Py_BuildValue("s", ""); diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index e50909456a..ea73d52035 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -33,7 +33,6 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { int is_power_plugged; power_info = IOPSCopyPowerSourcesInfo(); - if (!power_info) { PyErr_SetString(PyExc_RuntimeError, "IOPSCopyPowerSourcesInfo() syscall failed"); @@ -47,8 +46,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { goto error; } - /* Should only get one source. But in practice, check for > 0 sources */ - if (!CFArrayGetCount(power_sources_list)) { + if (CFArrayGetCount(power_sources_list) == 0) { PyErr_SetString(PyExc_NotImplementedError, "no battery"); goto error; } @@ -90,16 +88,13 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { time_to_empty = -1; } - py_tuple = Py_BuildValue("Iii", - capacity, time_to_empty, is_power_plugged); - if (!py_tuple) { + py_tuple = Py_BuildValue("Iii", capacity, time_to_empty, is_power_plugged); + if (!py_tuple) goto error; - } CFRelease(power_info); CFRelease(power_sources_list); /* Caller should NOT release power_sources_information */ - return py_tuple; error: diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index d9444ad896..090324a93e 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -13,12 +13,6 @@ SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') SKIP_FILES = () SKIP_PREFIXES = ('.ci/', '.github/') -FILE = "MANIFEST.in" - - -def cat(path): - with open(path) as f: - return f.read() def sh(cmd): @@ -29,7 +23,6 @@ def sh(cmd): def main(): files = set() - text = [] for file in sh("git ls-files").split('\n'): if ( file.startswith(SKIP_PREFIXES) @@ -40,16 +33,9 @@ def main(): files.add(file) for file in sorted(files): - text.append(f"include {file}") # noqa: PERF401 - - text.append("recursive-exclude docs/_static *") # noqa: FURB113 - text.append("") - text = "\n".join(text) + print("include " + file) - if cat(FILE) != text: - with open(FILE, "w") as f: - f.write(text) - print(f"{FILE} was updated") + print("recursive-exclude docs/_static *") if __name__ == '__main__': From 9d1ba7529f68275697d5bcd1b0785ebd02743683 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 24 Oct 2025 21:56:02 +0200 Subject: [PATCH 1392/1714] Update pyproject.toml --- .github/workflows/bsd.yml | 17 ++++++++++++++++- .github/workflows/build.yml | 4 ++-- psutil/tests/test_process.py | 13 ------------- pyproject.toml | 13 +++++++------ 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 2a7e9d3fff..05694fbe85 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -4,7 +4,22 @@ # https://github.com/vmactions/openbsd-vm # https://github.com/vmactions/netbsd-vm -on: [push, pull_request] +on: + push: + # only run this job if the following files are modified + paths: &bsd_paths + - ".github/workflows/bsd.yml" + - "psutil/_psbsd.py" + - "psutil/_psutil_bsd.c" + - "psutil/arch/bsd/**" + - "psutil/arch/freebsd/**" + - "psutil/arch/netbsd/**" + - "psutil/arch/openbsd/**" + - "psutil/arch/posix/**" + - "psutil/tests/test_bsd.py" + - "setup.py" + pull_request: + paths: *bsd_paths name: bsd concurrency: group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03c850c9f6..ead62ac505 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,8 +26,8 @@ jobs: include: - { os: ubuntu-latest, arch: x86_64 } - { os: ubuntu-24.04-arm, arch: aarch64 } - - { os: macos-13, arch: x86_64 } - - { os: macos-14, arch: arm64 } + - { os: macos-15, arch: x86_64 } + - { os: macos-15, arch: arm64 } - { os: windows-2025, arch: AMD64 } - { os: windows-11-arm, arch: ARM64 } steps: diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index c473c190d7..f7b1c07b9e 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -257,19 +257,6 @@ def test_cpu_times(self): for name in times._fields: time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) - def test_cpu_times_2(self): - def waste_cpu(): - stop_at = os.times().user + 0.2 - while os.times().user < stop_at: - for x in range(100000): - x **= 2 - - waste_cpu() - a = psutil.Process().cpu_times() - b = os.times() - assert abs(a.user - b.user) < 0.1 - assert abs(a.system - b.system) < 0.1 - @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") def test_cpu_num(self): p = psutil.Process() diff --git a/pyproject.toml b/pyproject.toml index 36f3f4dd30..f1b4146fc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -227,22 +227,23 @@ skip = [ test-extras = ["test"] # same as doing `pip install .[test]` test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci-test-cibuildwheel" -[tool.cibuildwheel.macos] -archs = ["arm64", "x86_64"] - # Run tests on Python 3.13. On all other Python versions do a lightweight # import test. [[tool.cibuildwheel.overrides]] -select = "cp3{8,9,10,11,12,13t,14}-* cp313-macosx*" +select = "cp3{8,9,10,11,12,13t,14}-* cp313-macosx" test-extras = [] test-command = "python -c \"import psutil; print(psutil.__version__)\"" -# In macOS use Python 3.8: higher Python version hang for some reason. [[tool.cibuildwheel.overrides]] -select = "cp38-macosx*" +select = "cp38-macosx*" # In macOS use Python 3.8: higher Python version hang for some reason. test-extras = ["test"] test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci-test-cibuildwheel" +[[tool.cibuildwheel.overrides]] +select = "cp3*macosx*arm64" # XXX: skip due to segfault: https://github.com/giampaolo/psutil/issues/2661 +test-extras = ["test"] +test-command = "python -c \"import psutil; print(psutil.__version__)\"" + [tool.pytest.ini_options] addopts = ''' --verbose From 742c9b560ee01bea051438a0d7a205b709b6d144 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Oct 2025 01:53:04 +0200 Subject: [PATCH 1393/1714] [macOS] lots of fixes (#2662) - should fix macOS segfault #2661 This is an attempt to fix https://github.com/giampaolo/psutil/issues/2661. - Add `psutil_badargs()` to check invalid args passed to C utility functions. - Use `INET6_ADDRSTRLEN` for IP address buffers instead of hard-coded size 200. - Same for `IFNAMSIZ` for network interface names. - Improve mach port and VM memory handling on macOS: - Ensure `mach_port_deallocate()` is always called. - Avoid infinite loops in `psutil_proc_memory_uss()` by tracking previous addresses. - Reset `info_count` before each `mach_vm_region()` call. - Refactor `psutil_proc_pidinfo()` to check for truncated return sizes. - Fix `psutil_proc_threads()` cleanup to correctly deallocate memory and ports in error paths. - Check for zombie in `psutil_task_for_pid()` - Enhance `psutil_proc_net_connections()`: - Check `inet_ntop()` return value instead of relying on errno. - Use correct buffer lengths for interface names and IP addresses. - Add error handling for PySequence_Contains() returning -1. - Improve `psutil_users()` to handle non-null-terminated `ut_host` strings safely using `strnlen()` and copying to a local buffer. - Fix minor macOS syscall issues: - Correct `psutil_swap_mem()` to report proper sysctl identifiers. - Ensure attribute buffers are initialized for `psutil_disk_usage_used()`. - Minor style and safety improvements: - Avoid potential double decrefs and ensure proper Py_XDECREF/Py_DECREF usage. - Fix unused variable warnings and redundant comments. --- psutil/arch/all/init.c | 10 ++ psutil/arch/all/init.h | 5 +- psutil/arch/freebsd/proc_socks.c | 2 +- psutil/arch/freebsd/sys_socks.c | 2 +- psutil/arch/osx/cpu.c | 36 +++-- psutil/arch/osx/disk.c | 2 + psutil/arch/osx/mem.c | 7 +- psutil/arch/osx/net.c | 13 +- psutil/arch/osx/proc.c | 226 +++++++++++++++++++------------ psutil/arch/posix/init.h | 2 +- psutil/arch/posix/net.c | 2 +- psutil/arch/posix/sysctl.c | 36 ++++- psutil/arch/posix/users.c | 20 ++- pyproject.toml | 7 +- 14 files changed, 234 insertions(+), 136 deletions(-) diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c index a4a67d6eca..82e7c4b654 100644 --- a/psutil/arch/all/init.c +++ b/psutil/arch/all/init.c @@ -115,6 +115,16 @@ psutil_check_pid_range(PyObject *self, PyObject *args) { } +// Use it when invalid args are passed to a C function. +int +psutil_badargs(const char *funcname) { + PyErr_Format( + PyExc_RuntimeError, "%s() invalid args passed to function", funcname + ); + return -1; +} + + // Called on module import on all platforms. int psutil_setup(void) { diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index f52b1fc83a..97fd457170 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -118,10 +118,11 @@ PyObject *psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); #endif #endif +int psutil_badargs(const char *funcname); +int psutil_setup(void); + #if defined(PSUTIL_WINDOWS) || defined(PSUTIL_BSD) || defined(PSUTIL_OSX) PyObject *psutil_pids(PyObject *self, PyObject *args); #endif - PyObject *psutil_set_debug(PyObject *self, PyObject *args); PyObject *psutil_check_pid_range(PyObject *self, PyObject *args); -int psutil_setup(void); diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index d56487aee9..47e46ea08e 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -227,7 +227,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { for (i = 0; i < cnt; i++) { int lport, rport, state; - char lip[200], rip[200]; + char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; char path[PATH_MAX]; int inseq; py_tuple = NULL; diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 7e677a4959..a56ed06a8a 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -179,7 +179,7 @@ psutil_gather_inet( if ((inp->inp_vflag & INP_IPV6) && (include_v6 == 0)) continue; - char lip[200], rip[200]; + char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; xf = psutil_get_file_from_sock(so->xso_so, psutil_xfiles, psutil_nxfiles); if (xf == NULL) diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index c2153b7641..e556037d57 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -73,8 +73,9 @@ psutil_cpu_times(PyObject *self, PyObject *args) { error = host_statistics(mport, HOST_CPU_LOAD_INFO, (host_info_t)&r_load, &count); + mach_port_deallocate(mach_task_self(), mport); + if (error != KERN_SUCCESS) { - mach_port_deallocate(mach_task_self(), mport); return PyErr_Format( PyExc_RuntimeError, "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", @@ -82,7 +83,6 @@ psutil_cpu_times(PyObject *self, PyObject *args) { ); } - mach_port_deallocate(mach_task_self(), mport); return Py_BuildValue( "(dddd)", (double)r_load.cpu_ticks[CPU_STATE_USER] / CLK_TCK, @@ -107,8 +107,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); + mach_port_deallocate(mach_task_self(), mport); + if (ret != KERN_SUCCESS) { - mach_port_deallocate(mach_task_self(), mport); PyErr_Format( PyExc_RuntimeError, "host_statistics(HOST_VM_INFO) failed: %s", @@ -117,7 +118,6 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { return NULL; } - mach_port_deallocate(mach_task_self(), mport); return Py_BuildValue( "IIIII", vmstat.v_swtch, @@ -197,10 +197,11 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { uint32_t pMin = 0, eMin = 0, min = 0, max = 0, curr = 0; if (!psutil_find_pmgr_entry(&entry)) { - return PyErr_Format( + PyErr_SetString( PyExc_RuntimeError, "'pmgr' entry not found in AppleARMIODevice" ); + return NULL; } pCoreRef = IORegistryEntryCreateCFProperty( @@ -237,9 +238,6 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { if (entry != IO_OBJECT_NULL) IOObjectRelease(entry); - if (PyErr_Occurred()) - return NULL; - return Py_BuildValue( "KKK", (unsigned long long)(curr / 1000 / 1000), @@ -302,11 +300,17 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { goto error; } - error = host_processor_info(mport, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count); + error = host_processor_info( + mport, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count + ); mach_port_deallocate(mach_task_self(), mport); if (error != KERN_SUCCESS || !info_array) { - PyErr_Format(PyExc_RuntimeError, "host_processor_info failed: %s", mach_error_string(error)); + PyErr_Format( + PyExc_RuntimeError, + "host_processor_info failed: %s", + mach_error_string(error) + ); goto error; } @@ -325,6 +329,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } if (PyList_Append(py_retlist, py_cputime)) { Py_DECREF(py_cputime); + goto error; } Py_DECREF(py_cputime); } @@ -338,10 +343,11 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { error: Py_XDECREF(py_retlist); - vm_deallocate( - mach_task_self(), - (vm_address_t)info_array, - info_count * sizeof(integer_t) - ); + if (info_array) + vm_deallocate( + mach_task_self(), + (vm_address_t)info_array, + info_count * sizeof(integer_t) + ); return NULL; } diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index 569c63f217..052d19996b 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -188,6 +188,8 @@ psutil_disk_usage_used(PyObject *self, PyObject *args) { attrs.bitmapcount = ATTR_BIT_MAP_COUNT; attrs.volattr = ATTR_VOL_INFO | ATTR_VOL_SPACEUSED; + attrbuf.size = sizeof(attrbuf); + Py_BEGIN_ALLOW_THREADS ret = getattrlist(mount_point, &attrs, &attrbuf, sizeof(attrbuf), 0); Py_END_ALLOW_THREADS diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index c77649d8eb..cbac711a9b 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -89,14 +89,14 @@ PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { int mib[2]; struct xsw_usage totals; - vm_statistics64_data_t vmstat; + vm_statistics64_data_t vmstat; long pagesize = psutil_getpagesize(); mib[0] = CTL_VM; mib[1] = VM_SWAPUSAGE; if (psutil_sysctl(mib, 2, &totals, sizeof(totals)) != 0) - return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); + return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(VM_SWAPUSAGE)"); if (psutil_sys_vminfo(&vmstat) != 0) return NULL; @@ -107,5 +107,6 @@ psutil_swap_mem(PyObject *self, PyObject *args) { (unsigned long long) totals.xsu_used, (unsigned long long) totals.xsu_avail, (unsigned long long) vmstat.pageins * pagesize, - (unsigned long long) vmstat.pageouts * pagesize); + (unsigned long long) vmstat.pageouts * pagesize + ); } diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index 652180c18f..93be69e541 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -43,9 +43,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { lim = buf + len; for (next = buf; next < lim; ) { - // Check we have enough space for if_msghdr. if ((size_t)(lim - next) < sizeof(struct if_msghdr)) { - psutil_debug("struct xfile size mismatch (skip entry)"); + psutil_debug("struct if_msghdr size mismatch (skip entry)"); break; } @@ -74,13 +73,12 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { continue; } - char ifc_name[32]; + char ifc_name[IFNAMSIZ]; size_t namelen = sdl->sdl_nlen; + if (namelen >= IFNAMSIZ) + namelen = IFNAMSIZ - 1; - if (namelen >= sizeof(ifc_name)) - namelen = sizeof(ifc_name) - 1; - - strncpy(ifc_name, sdl->sdl_data, namelen); + memcpy(ifc_name, sdl->sdl_data, namelen); ifc_name[namelen] = '\0'; py_ifc_info = Py_BuildValue( @@ -101,6 +99,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { Py_CLEAR(py_ifc_info); goto error; } + Py_CLEAR(py_ifc_info); } } diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 770fc401f8..43c91ffb73 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -49,10 +49,11 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { mib[2] = KERN_PROC_PID; mib[3] = pid; - // fetch the info with sysctl() + if (pid < 0 || !kp) + return psutil_badargs("psutil_get_kinfo_proc"); + len = sizeof(struct kinfo_proc); - // now read the data from sysctl if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { // raise an exception and throw errno as the error psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl"); @@ -72,6 +73,9 @@ static int is_zombie(size_t pid) { struct kinfo_proc kp; + if (pid < 0) + return psutil_badargs("is_zombie"); + if (psutil_get_kinfo_proc(pid, &kp) == -1) { PyErr_Clear(); return 0; @@ -91,6 +95,9 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { mib[1] = KERN_PROCARGS2; mib[2] = pid; + if (pid < 0 || !procargs || !argmax || *argmax == 0) + return psutil_badargs("psutil_sysctl_procargs"); + if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { if (psutil_pid_exists(pid) == 0) { NoSuchProcess("psutil_pid_exists -> 0"); @@ -103,7 +110,7 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { } if (errno == EINVAL) { - psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to NSP"); + psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to AD"); AccessDenied("sysctl(KERN_PROCARGS2) -> EINVAL"); return -1; } @@ -127,20 +134,28 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { */ static int psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { - errno = 0; int ret; + if (pid < 0 || !pti || size <= 0) + return psutil_badargs("psutil_proc_pidinfo"); + + errno = 0; ret = proc_pidinfo(pid, flavor, arg, pti, size); if (ret <= 0) { psutil_raise_for_pid(pid, "proc_pidinfo()"); - return 0; + return -1; } - if ((unsigned long)ret < sizeof(pti)) { + + // check for truncated return size + if (ret < size) { psutil_raise_for_pid( - pid, "proc_pidinfo() return size < sizeof(struct_pointer)"); - return 0; + pid, + "proc_pidinfo() returned less data than requested buffer size" + ); + return -1; } - return ret; + + return 0; } @@ -152,26 +167,28 @@ psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { * - for PIDs != getpid() or PIDs which are not members of the procmod * it requires root * As such we can only guess what the heck went wrong and fail either - * with NoSuchProcess or giveup with AccessDenied. - * Here's some history: + * with NoSuchProcess or give up with AccessDenied. + * References: * https://github.com/giampaolo/psutil/issues/1181 * https://github.com/giampaolo/psutil/issues/1209 * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 */ static int psutil_task_for_pid(pid_t pid, mach_port_t *task) { - // See: https://github.com/giampaolo/psutil/issues/1181 - kern_return_t err = KERN_SUCCESS; + kern_return_t err; + + if (pid < 0 || !task) + return psutil_badargs("psutil_task_for_pid"); err = task_for_pid(mach_task_self(), pid, task); if (err != KERN_SUCCESS) { if (psutil_pid_exists(pid) == 0) { NoSuchProcess("task_for_pid"); } - // Now done in Python. - // else if (psutil_is_zombie(pid) == 1) - // PyErr_SetString(ZombieProcessError, - // "task_for_pid -> psutil_is_zombie -> 1"); + else if (is_zombie(pid) == 1) { + PyErr_SetString(ZombieProcessError, + "task_for_pid -> psutil_is_zombie -> 1"); + } else { psutil_debug( "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " @@ -181,6 +198,7 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) { } return -1; } + return 0; } @@ -196,6 +214,11 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { int max_size = 24 * 1024 * 1024; // 24M struct proc_fdinfo *fds_pointer = NULL; + if (pid < 0 || num_fds == NULL) { + psutil_badargs("psutil_proc_list_fds"); + return NULL; + } + errno = 0; ret = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); if (ret <= 0) { @@ -282,8 +305,8 @@ PyObject * psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { pid_t pid; struct kinfo_proc kp; - PyObject *py_name; - PyObject *py_retlist; + PyObject *py_name = NULL; + PyObject *py_retlist = NULL; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; @@ -295,11 +318,12 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { // Likely a decoding error. We don't want to fail the whole // operation. The python module may retry with proc_name(). PyErr_Clear(); + Py_INCREF(Py_None); py_name = Py_None; } py_retlist = Py_BuildValue( - _Py_PARSE_PID "llllllidiO", + _Py_PARSE_PID "llllllldiO", kp.kp_eproc.e_ppid, // (pid_t) ppid (long)kp.kp_eproc.e_pcred.p_ruid, // (long) real uid (long)kp.kp_eproc.e_ucred.cr_uid, // (long) effective uid @@ -307,16 +331,13 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { (long)kp.kp_eproc.e_pcred.p_rgid, // (long) real gid (long)kp.kp_eproc.e_ucred.cr_groups[0], // (long) effective gid (long)kp.kp_eproc.e_pcred.p_svgid, // (long) saved gid - kp.kp_eproc.e_tdev, // (int) tty nr + (long long)kp.kp_eproc.e_tdev, // (long long) tty nr PSUTIL_TV2DOUBLE(kp.kp_proc.p_starttime), // (double) create time (int)kp.kp_proc.p_stat, // (int) status py_name // (pystr) name ); - if (py_retlist != NULL) { - // XXX shall we decref() also in case of Py_BuildValue() error? - Py_DECREF(py_name); - } + Py_DECREF(py_name); return py_retlist; } @@ -338,7 +359,7 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) <= 0) + if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) != 0) return NULL; total_user = pti.pti_total_user * PSUTIL_MACH_TIMEBASE_INFO.numer; @@ -380,10 +401,10 @@ psutil_proc_name(PyObject *self, PyObject *args) { return NULL; if (psutil_get_kinfo_proc(pid, &kp) == -1) return NULL; + return PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); } - /* * Return process current working directory. * Raises NSP in case of zombie process. @@ -397,7 +418,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { return NULL; if (psutil_proc_pidinfo( - pid, PROC_PIDVNODEPATHINFO, 0, &pathinfo, sizeof(pathinfo)) <= 0) + pid, PROC_PIDVNODEPATHINFO, 0, &pathinfo, sizeof(pathinfo)) != 0) { return NULL; } @@ -481,13 +502,14 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { cpu_type_t cpu_type; size_t private_pages = 0; mach_vm_size_t size = 0; - mach_msg_type_number_t info_count = VM_REGION_TOP_INFO_COUNT; + mach_msg_type_number_t info_count; kern_return_t kr; long pagesize = psutil_getpagesize(); - mach_vm_address_t addr = MACH_VM_MIN_ADDRESS; + mach_vm_address_t addr; mach_port_t task = MACH_PORT_NULL; vm_region_top_info_data_t info; mach_port_t object_name; + mach_vm_address_t prev_addr; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; @@ -503,7 +525,10 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { // Roughly based on libtop_update_vm_regions in // http://www.opensource.apple.com/source/top/top-100.1.2/libtop.c - for (addr = 0; ; addr += size) { + for (addr = MACH_VM_MIN_ADDRESS; ; addr += size) { + prev_addr = addr; + info_count = VM_REGION_TOP_INFO_COUNT; // reset before each call + kr = mach_vm_region( task, &addr, &size, VM_REGION_TOP_INFO, (vm_region_info_t)&info, &info_count, &object_name); @@ -514,10 +539,17 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { else if (kr != KERN_SUCCESS) { PyErr_Format( PyExc_RuntimeError, - "mach_vm_region(VM_REGION_TOP_INFO) syscall failed"); + "mach_vm_region(VM_REGION_TOP_INFO) syscall failed" + ); + mach_port_deallocate(mach_task_self(), task); return NULL; } + if (size == 0 || addr < prev_addr) { + psutil_debug("prevent infinite loop"); + break; + } + if (psutil_in_shared_region(addr, cpu_type) && info.share_mode != SM_PRIVATE) { continue; @@ -557,9 +589,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { pid_t pid; - int err, ret; kern_return_t kr; - unsigned int info_count = TASK_BASIC_INFO_COUNT; mach_port_t task = MACH_PORT_NULL; struct task_basic_info tasks_info; thread_act_port_array_t thread_list = NULL; @@ -579,12 +609,11 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (psutil_task_for_pid(pid, &task) != 0) goto error; - info_count = TASK_BASIC_INFO_COUNT; - err = task_info(task, TASK_BASIC_INFO, (task_info_t)&tasks_info, - &info_count); - if (err != KERN_SUCCESS) { - // errcode 4 is "invalid argument" (access denied) - if (err == 4) { + // Get basic task info (optional, ignored if access denied) + mach_msg_type_number_t info_count = TASK_BASIC_INFO_COUNT; + kr = task_info(task, TASK_BASIC_INFO, (task_info_t)&tasks_info, &info_count); + if (kr != KERN_SUCCESS) { + if (kr == KERN_INVALID_ARGUMENT) { AccessDenied("task_info(TASK_BASIC_INFO)"); } else { @@ -595,8 +624,8 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } - err = task_threads(task, &thread_list, &thread_count); - if (err != KERN_SUCCESS) { + kr = task_threads(task, &thread_list, &thread_count); + if (kr != KERN_SUCCESS) { PyErr_Format(PyExc_RuntimeError, "task_threads() syscall failed"); goto error; } @@ -627,30 +656,28 @@ psutil_proc_threads(PyObject *self, PyObject *args) { Py_CLEAR(py_tuple); } - ret = vm_deallocate( - task, (vm_address_t)thread_list, thread_count * sizeof(thread_act_t) - ); - - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); - - mach_port_deallocate(mach_task_self(), task); + if (thread_list != NULL) { + vm_deallocate(mach_task_self(), (vm_address_t)thread_list, + thread_count * sizeof(thread_act_t)); + } + if (task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), task); + } return py_retlist; error: - if (task != MACH_PORT_NULL) - mach_port_deallocate(mach_task_self(), task); Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (thread_list != NULL) { - ret = vm_deallocate( - task, (vm_address_t)thread_list, thread_count * sizeof(thread_act_t) - ); + Py_XDECREF(py_retlist); - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); + if (thread_list != NULL) { + vm_deallocate(mach_task_self(), (vm_address_t)thread_list, + thread_count * sizeof(thread_act_t)); + } + if (task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), task); } + return NULL; } @@ -761,6 +788,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { struct proc_fdinfo *fds_pointer = NULL; struct proc_fdinfo *fdp_pointer; struct socket_fdinfo si; + const char* ntopret; PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; @@ -796,12 +824,11 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { fdp_pointer = &fds_pointer[i]; if (fdp_pointer->proc_fdtype == PROX_FDTYPE_SOCKET) { - errno = 0; nb = proc_pidfdinfo(pid, fdp_pointer->proc_fd, PROC_PIDFDSOCKETINFO, &si, sizeof(si)); // --- errors checking - if ((nb <= 0) || (nb < sizeof(si)) || (errno != 0)) { + if ((nb <= 0) || (nb < sizeof(si))) { if (errno == EBADF) { // let's assume socket has been closed psutil_debug("proc_pidfdinfo(PROC_PIDFDSOCKETINFO) -> " @@ -825,8 +852,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { // int fd, family, type, lport, rport, state; - // TODO: use INET6_ADDRSTRLEN instead of 200 - char lip[200], rip[200]; + char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; int inseq; PyObject *py_family; PyObject *py_type; @@ -839,42 +865,63 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { py_family = PyLong_FromLong((long)family); inseq = PySequence_Contains(py_af_filter, py_family); Py_DECREF(py_family); + if (inseq == -1) + goto error; if (inseq == 0) continue; + py_type = PyLong_FromLong((long)type); inseq = PySequence_Contains(py_type_filter, py_type); Py_DECREF(py_type); + if (inseq == -1) + goto error; if (inseq == 0) continue; if ((family == AF_INET) || (family == AF_INET6)) { if (family == AF_INET) { - inet_ntop(AF_INET, - &si.psi.soi_proto.pri_tcp.tcpsi_ini. \ - insi_laddr.ina_46.i46a_addr4, - lip, - sizeof(lip)); - inet_ntop(AF_INET, - &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_faddr. \ - ina_46.i46a_addr4, - rip, - sizeof(rip)); + ntopret = inet_ntop( + AF_INET, + &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_laddr.ina_46.i46a_addr4, + lip, + sizeof(lip) + ); + if (!ntopret) { + psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + goto error; + } + ntopret = inet_ntop( + AF_INET, + &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_faddr.ina_46.i46a_addr4, + rip, + sizeof(rip) + ); + if (!ntopret) { + psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + goto error; + } } else { - inet_ntop(AF_INET6, - &si.psi.soi_proto.pri_tcp.tcpsi_ini. \ - insi_laddr.ina_6, - lip, sizeof(lip)); - inet_ntop(AF_INET6, - &si.psi.soi_proto.pri_tcp.tcpsi_ini. \ - insi_faddr.ina_6, - rip, sizeof(rip)); - } - - // check for inet_ntop failures - if (errno != 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); - goto error; + ntopret = inet_ntop( + AF_INET6, + &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_laddr.ina_6, + lip, + sizeof(lip) + ); + if (!ntopret) { + psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + goto error; + } + ntopret = inet_ntop( + AF_INET6, + &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_faddr.ina_6, + rip, + sizeof(rip) + ); + if (!ntopret) { + psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + goto error; + } } lport = ntohs(si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_lport); @@ -894,7 +941,6 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (!py_raddr) goto error; - // construct the python list py_tuple = Py_BuildValue( "(iiiNNi)", fd, family, type, py_laddr, py_raddr, state); if (!py_tuple) @@ -912,7 +958,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { si.psi.soi_proto.pri_un.unsi_caddr.ua_sun.sun_path); if (!py_raddr) goto error; - // construct the python list + py_tuple = Py_BuildValue( "(iiiOOi)", fd, family, type, @@ -991,7 +1037,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { // read argmax and allocate memory for argument space. argmax = psutil_sysctl_argmax(); - if (! argmax) + if (argmax == 0) goto error; procargs = (char *)malloc(argmax); diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 82c9abadbc..3e323ca149 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -19,7 +19,7 @@ extern PyObject *ZombieProcessError; int psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen); int psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen); - int psutil_sysctl_argmax(); + size_t psutil_sysctl_argmax(); #if !defined(PSUTIL_OPENBSD) diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index 62ea184aef..b1dd38bdea 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -246,7 +246,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { } static int -append_flag(PyObject *py_retlist, const char * flag_name) +append_flag(PyObject *py_retlist, const char *flag_name) { PyObject *py_str = NULL; diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index c4de3b3524..fbe1b561cb 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -20,6 +20,9 @@ int psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen) { size_t len = buflen; + if (!mib || miblen == 0 || !buf || buflen == 0) + return psutil_badargs("psutil_sysctl"); + if (sysctl(mib, miblen, buf, &len, NULL, 0) == -1) { psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl()"); return -1; @@ -33,6 +36,7 @@ psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen) { return 0; } + // Allocate buffer for sysctl with retry on ENOMEM or buffer size mismatch. // The caller is responsible for freeing the memory. int @@ -42,6 +46,9 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { int ret; int max_retries = MAX_RETRIES; + if (!mib || miblen == 0 || !buf || !buflen) + return psutil_badargs("psutil_sysctl_malloc"); + // First query to determine required size ret = sysctl(mib, miblen, NULL, &needed, NULL, 0); if (ret == -1) { @@ -49,6 +56,10 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { return -1; } + if (needed == 0) { + psutil_debug("psutil_sysctl_malloc() size = 0"); + } + while (max_retries-- > 0) { // zero-initialize buffer to prevent uninitialized bytes buffer = calloc(1, needed); @@ -95,19 +106,24 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { } -// Get the maximum process arguments size. -int +// Get the maximum process arguments size. Return 0 on error. +size_t psutil_sysctl_argmax() { int argmax; int mib[2] = {CTL_KERN, KERN_ARGMAX}; if (psutil_sysctl(mib, 2, &argmax, sizeof(argmax)) != 0) { - PyErr_Clear(); - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_ARGMAX)"); return 0; } - return argmax; + if (argmax <= 0) { + PyErr_SetString( + PyExc_RuntimeError, "sysctl(KERN_ARGMAX) return <= 0" + ); + return 0; + } + + return (size_t)argmax; } @@ -118,6 +134,9 @@ psutil_sysctlbyname(const char *name, void *buf, size_t buflen) { size_t len = buflen; char errbuf[256]; + if (!name || !buf || buflen == 0) + return psutil_badargs("psutil_sysctlbyname"); + if (sysctlbyname(name, buf, &len, NULL, 0) == -1) { snprintf(errbuf, sizeof(errbuf), "sysctlbyname('%s')", name); psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); @@ -152,6 +171,9 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { char *buffer = NULL; char errbuf[256]; + if (!name || !buf || !buflen) + return psutil_badargs("psutil_sysctlbyname_malloc"); + // First query to determine required size. ret = sysctlbyname(name, NULL, &needed, NULL, 0); if (ret == -1) { @@ -160,6 +182,10 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { return -1; } + if (needed == 0) { + psutil_debug("psutil_sysctlbyname_malloc() size = 0"); + } + while (max_retries-- > 0) { // Zero-initialize buffer to prevent uninitialized bytes. buffer = calloc(1, needed); diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c index 04e7a8481e..df33820e0a 100644 --- a/psutil/arch/posix/users.c +++ b/psutil/arch/posix/users.c @@ -31,6 +31,7 @@ teardown() { PyObject * psutil_users(PyObject *self, PyObject *args) { struct utmpx *ut; + size_t host_len; PyObject *py_username = NULL; PyObject *py_tty = NULL; PyObject *py_hostname = NULL; @@ -55,11 +56,22 @@ psutil_users(PyObject *self, PyObject *args) { if (! py_tty) goto error; - if (strcmp(ut->ut_host, ":0") == 0 || strcmp(ut->ut_host, ":0.0") == 0) + host_len = strnlen(ut->ut_host, sizeof(ut->ut_host)); + if (host_len == 0 || + (strcmp(ut->ut_host, ":0") == 0) || + (strcmp(ut->ut_host, ":0.0") == 0)) + { py_hostname = PyUnicode_DecodeFSDefault("localhost"); - else - py_hostname = PyUnicode_DecodeFSDefault(ut->ut_host); - if (! py_hostname) + } + else { + // ut_host might not be null-terminated if the hostname is + // very long, so we do it. + char hostbuf[sizeof(ut->ut_host)]; + memcpy(hostbuf, ut->ut_host, host_len); + hostbuf[host_len] = '\0'; + py_hostname = PyUnicode_DecodeFSDefault(hostbuf); + } + if (!py_hostname) goto error; py_tuple = Py_BuildValue( diff --git a/pyproject.toml b/pyproject.toml index f1b4146fc2..8419b52a12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -230,7 +230,7 @@ test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci # Run tests on Python 3.13. On all other Python versions do a lightweight # import test. [[tool.cibuildwheel.overrides]] -select = "cp3{8,9,10,11,12,13t,14}-* cp313-macosx" +select = "cp3{8,9,10,11,12,13t,14}-* cp313-macosx*" test-extras = [] test-command = "python -c \"import psutil; print(psutil.__version__)\"" @@ -239,11 +239,6 @@ select = "cp38-macosx*" # In macOS use Python 3.8: higher Python version hang f test-extras = ["test"] test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci-test-cibuildwheel" -[[tool.cibuildwheel.overrides]] -select = "cp3*macosx*arm64" # XXX: skip due to segfault: https://github.com/giampaolo/psutil/issues/2661 -test-extras = ["test"] -test-command = "python -c \"import psutil; print(psutil.__version__)\"" - [tool.pytest.ini_options] addopts = ''' --verbose From 3ed64173b6592344200b27d284fb23c5fd70f450 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Oct 2025 01:58:55 +0200 Subject: [PATCH 1394/1714] BSD: kinfo_getfile() check args + INT_MAX --- psutil/arch/bsd/proc.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 77f1d1ee75..8a9ee1db31 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -36,9 +36,15 @@ // Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an // int as arg and returns an array with cnt struct kinfo_file. +// Caller is responsible for freeing the returned pointer with free(). #ifdef PSUTIL_HASNT_KINFO_GETFILE struct kinfo_file * kinfo_getfile(pid_t pid, int *cnt) { + if (pid < 0 || !cnt) { + psutil_badargs("kinfo_getfile"); + return NULL; + } + int mib[6]; size_t len; struct kinfo_file *kf = NULL; @@ -54,6 +60,14 @@ kinfo_getfile(pid_t pid, int *cnt) { return NULL; } + // Calculate number of entries and check for overflow + if (len / sizeof(struct kinfo_file) > INT_MAX) { + psutil_debug("exceeded INT_MAX") + free(kf); + errno = EOVERFLOW; + return NULL; + } + *cnt = (int)(len / sizeof(struct kinfo_file)); return kf; } From a6ca9e14c68069c1e40f1c10cece36ca0fa491c8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Oct 2025 02:12:02 +0200 Subject: [PATCH 1395/1714] BSD: Py_INCREF None value + use psutil_badargs() --- psutil/arch/bsd/proc.c | 23 +++++++++++------------ psutil/arch/freebsd/proc.c | 3 +++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 8a9ee1db31..90b1a0d22a 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -93,10 +93,10 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { struct kinfo_proc kp; #endif long pagesize = psutil_getpagesize(); - char str[1000]; - PyObject *py_name; - PyObject *py_ppid; - PyObject *py_retlist; + char name_buf[256]; // buffer for process name + PyObject *py_name = NULL; + PyObject *py_ppid = NULL; + PyObject *py_retlist = NULL; if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; @@ -105,18 +105,17 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { // Process #ifdef PSUTIL_FREEBSD - sprintf(str, "%s", kp.ki_comm); + snprintf(name_buf, sizeof(name_buf), "%s", kp.ki_comm); #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - sprintf(str, "%s", kp.p_comm); + snprintf(name_buf, sizeof(name_buf), "%s", kp.p_comm); #endif - py_name = PyUnicode_DecodeFSDefault(str); - if (! py_name) { - // Likely a decoding error. We don't want to fail the whole - // operation. The python module may retry with proc_name(). + py_name = PyUnicode_DecodeFSDefault(name_buf); + if (!py_name) { + // If decoding fails, fall back to None safely PyErr_Clear(); py_name = Py_None; + Py_INCREF(py_name); } - // Py_INCREF(py_name); // Calculate memory. #ifdef PSUTIL_FREEBSD @@ -165,7 +164,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) py_ppid = PyLong_FromPid(kp.p_ppid); #else - py_ppid = Py_BuildfValue(-1); + py_ppid = Py_BuildValue("i", -1); #endif if (! py_ppid) return NULL; diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 3c3b94f394..c1bb5ce672 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -42,6 +42,9 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { mib[2] = KERN_PROC_PID; mib[3] = pid; + if (pid < 0 || !proc) + psutil_badargs("psutil_kinfo_proc") + size = sizeof(struct kinfo_proc); if (sysctl((int *)mib, 4, proc, &size, NULL, 0) == -1) { psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_PID)"); From cbc0ba9025ec22b3be9f59b9e5bc761484c913b8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Oct 2025 02:18:28 +0200 Subject: [PATCH 1396/1714] C: add missing ; --- psutil/arch/freebsd/proc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index c1bb5ce672..e475601ab5 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -43,7 +43,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { mib[3] = pid; if (pid < 0 || !proc) - psutil_badargs("psutil_kinfo_proc") + psutil_badargs("psutil_kinfo_proc"); size = sizeof(struct kinfo_proc); if (sysctl((int *)mib, 4, proc, &size, NULL, 0) == -1) { From 7dfd0ed34fe70ffd879ae62d21aabd4a8ed06d6f Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Sat, 25 Oct 2025 11:48:34 +0200 Subject: [PATCH 1397/1714] Build 3.14t wheels (#2659) --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8419b52a12..682f0052cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,7 +221,6 @@ trailing_comma_inline_array = true [tool.cibuildwheel] skip = [ "*-musllinux*", - "cp314t*", "cp3{8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures ] test-extras = ["test"] # same as doing `pip install .[test]` @@ -230,7 +229,7 @@ test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci # Run tests on Python 3.13. On all other Python versions do a lightweight # import test. [[tool.cibuildwheel.overrides]] -select = "cp3{8,9,10,11,12,13t,14}-* cp313-macosx*" +select = "cp3{8,9,10,11,12,13t,14,14t}-* cp313-macosx*" test-extras = [] test-command = "python -c \"import psutil; print(psutil.__version__)\"" From c948ef07e46b114a61492c9d207c741339fceeb2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Oct 2025 12:00:27 +0200 Subject: [PATCH 1398/1714] Update HISTORY.rst --- HISTORY.rst | 10 +++++----- docs/index.rst | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e4b2bf696b..17db0c3258 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,9 +8,10 @@ XXXX-XX-XX **Enhancements** -- 2657_, [Linux], [Windows]: stop publishing wheels for Python 32 bit. - 2565_: produce wheels for free-thread cPython 3.13 (patch by Lysandros - Nikolaou) +- 2657_: stop publishing prebuilt Linux and Windows wheels for 32-bit Python. + 32-bit CPython is still supported, but psutil must now be built from source. + 2565_: produce wheels for free-thread cPython 3.13 and 3.14 (patch by + Lysandros Nikolaou) **Bug fixes** @@ -21,8 +22,7 @@ XXXX-XX-XX **Compatibility notes** -- 2657_: Linux and Windows wheels for Python 32 bit are no longer uploaded, - meaning pip will install psutil from sources. +- 2657_: stop publishing prebuilt Linux and Windows wheels for 32-bit Python. 7.1.1 ===== diff --git a/docs/index.rst b/docs/index.rst index 648229672a..371264043a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2677,7 +2677,8 @@ Platforms support history ========================= * psutil 7.1.2 (XXXX-XX): publish wheels for free-threaded Python -* psutil 7.1.2 (XXXX-XX): unpublish wheels for cPython 32-bit +* psutil 7.1.2 (XXXX-XX): no longer publish wheels for 32-bit Python + (**Linux** and **Windows**) * psutil 7.1.1 (2025-10): drop **SunOS 10** * psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 From 73b821a9a1713976e2164ce071f1b3e8a5dabd98 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 25 Oct 2025 17:11:35 +0200 Subject: [PATCH 1399/1714] Update HISTORY.rst --- HISTORY.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 17db0c3258..fbb083812b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,10 +1,10 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.1.2 (IN DEVELOPMENT) -====================== +7.1.2 +===== -XXXX-XX-XX +2025-10-25 **Enhancements** @@ -19,6 +19,8 @@ XXXX-XX-XX raise `NoSuchProcess`_ instead of `ZombieProcess`_. - 2658_, [macOS]: double ``free()`` in `Process.environ()`_ when it fails internally. This posed a risk of segfault. +- 2662_, [macOS]: massive C code cleanup to guard against possible segfaults + which were (not so) sporadically spotted on CI. **Compatibility notes** From d05b8742ae903821318b4c1a8d68b1cf06f91a8b Mon Sep 17 00:00:00 2001 From: Pierre Labastie Date: Sun, 26 Oct 2025 18:35:15 +0100 Subject: [PATCH 1400/1714] test_process_all.py: enforce the "fork" multiprocessing method (#2666) There are three method for launching processes in the multiprocessing module. "fork" is the only one that does not launch an additional "resource_tracker" process. This process ignores SIGINT and SIGTERM, so that it is not ended by "reap_children", which messes other tests (and may lead to hanging). Fixes #2663 --- psutil/tests/test_process_all.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index 3398c47ee3..a742391c7a 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -103,6 +103,13 @@ 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: + # The 'fork' method is the only one that does not + # create a "resource_tracker" process. The problem + # when creating this process is that it ignores + # SIGTERM and SIGINT, and this makes "reap_children" + # hang... The following code should run on python-3.4 + # and later. + multiprocessing.set_start_method('fork') self.pool = multiprocessing.Pool() def tearDown(self): From a0bc1b3b39c21198eda90491a66cca87fb030b38 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 26 Oct 2025 20:29:28 +0100 Subject: [PATCH 1401/1714] Format C code and enforce it via `clang-format` (#2667) Enforce `clang-format` on all C and header files. It is now the mandatory formatting style for all C sources. --- .clang-format | 69 +++++++ .dprint.jsonc | 1 + MANIFEST.in | 2 +- Makefile | 11 +- psutil/_psutil_aix.c | 242 ++++++++++++---------- psutil/_psutil_bsd.c | 44 ++-- psutil/_psutil_linux.c | 6 +- psutil/_psutil_osx.c | 3 +- psutil/_psutil_sunos.c | 6 +- psutil/_psutil_windows.c | 106 +++++++--- psutil/arch/aix/common.c | 30 +-- psutil/arch/aix/common.h | 29 +-- psutil/arch/aix/ifaddrs.c | 33 +-- psutil/arch/aix/ifaddrs.h | 10 +- psutil/arch/aix/net_connections.c | 104 ++++++---- psutil/arch/aix/net_connections.h | 4 +- psutil/arch/aix/net_kernel_structs.h | 106 +++++----- psutil/arch/all/init.h | 18 +- psutil/arch/bsd/disk.c | 30 +-- psutil/arch/bsd/init.h | 11 +- psutil/arch/bsd/net.c | 12 +- psutil/arch/bsd/proc.c | 157 +++++++------- psutil/arch/bsd/sys.c | 6 +- psutil/arch/freebsd/cpu.c | 11 +- psutil/arch/freebsd/disk.c | 36 ++-- psutil/arch/freebsd/mem.c | 44 ++-- psutil/arch/freebsd/proc.c | 119 ++++++----- psutil/arch/freebsd/proc_socks.c | 143 ++++++++----- psutil/arch/freebsd/sensors.c | 2 +- psutil/arch/freebsd/sys_socks.c | 130 ++++++++---- psutil/arch/linux/disk.c | 18 +- psutil/arch/linux/init.h | 16 +- psutil/arch/linux/mem.c | 6 +- psutil/arch/linux/net.c | 23 +-- psutil/arch/linux/proc.c | 16 +- psutil/arch/netbsd/mem.c | 17 +- psutil/arch/netbsd/pids.c | 4 +- psutil/arch/netbsd/proc.c | 27 +-- psutil/arch/netbsd/socks.c | 65 +++--- psutil/arch/openbsd/cpu.c | 5 +- psutil/arch/openbsd/mem.c | 19 +- psutil/arch/openbsd/pids.c | 4 +- psutil/arch/openbsd/proc.c | 31 +-- psutil/arch/openbsd/socks.c | 20 +- psutil/arch/openbsd/users.c | 14 +- psutil/arch/osx/cpu.c | 38 ++-- psutil/arch/osx/disk.c | 97 ++++++--- psutil/arch/osx/mem.c | 33 +-- psutil/arch/osx/net.c | 15 +- psutil/arch/osx/proc.c | 248 +++++++++++++--------- psutil/arch/osx/sensors.c | 47 +++-- psutil/arch/posix/init.c | 21 +- psutil/arch/posix/init.h | 8 +- psutil/arch/posix/net.c | 144 +++++++------ psutil/arch/posix/proc.c | 6 +- psutil/arch/posix/sysctl.c | 8 +- psutil/arch/posix/users.c | 20 +- psutil/arch/sunos/cpu.c | 15 +- psutil/arch/sunos/disk.c | 25 ++- psutil/arch/sunos/environ.c | 68 +++--- psutil/arch/sunos/init.h | 15 +- psutil/arch/sunos/mem.c | 125 +++++------ psutil/arch/sunos/net.c | 165 ++++++++------- psutil/arch/sunos/proc.c | 149 ++++++++------ psutil/arch/windows/cpu.c | 126 ++++++------ psutil/arch/windows/disk.c | 91 +++++--- psutil/arch/windows/init.c | 116 ++++++----- psutil/arch/windows/init.h | 51 +++-- psutil/arch/windows/mem.c | 18 +- psutil/arch/windows/net.c | 106 ++++++---- psutil/arch/windows/ntextapi.h | 2 + psutil/arch/windows/proc.c | 294 ++++++++++++++------------ psutil/arch/windows/proc_handles.c | 58 +++--- psutil/arch/windows/proc_info.c | 297 ++++++++++++++------------- psutil/arch/windows/proc_utils.c | 1 - psutil/arch/windows/security.c | 33 ++- psutil/arch/windows/services.c | 111 +++++----- psutil/arch/windows/socks.c | 104 +++++----- psutil/arch/windows/sys.c | 63 +++--- psutil/arch/windows/wmi.c | 37 ++-- pyproject.toml | 1 + scripts/internal/clinter.py | 80 -------- scripts/internal/git_pre_commit.py | 39 ++-- 83 files changed, 2616 insertions(+), 2069 deletions(-) create mode 100644 .clang-format delete mode 100755 scripts/internal/clinter.py diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..1c910a3b75 --- /dev/null +++ b/.clang-format @@ -0,0 +1,69 @@ +# Re-adapted from: https://gist.github.com/JPHutchins/6ef33a52cc92fc4a71996b32b11724b4 +# clang-format doc: https://clang.llvm.org/docs/ClangFormatStyleOptions.html + +BasedOnStyle: Google +AlignAfterOpenBracket: BlockIndent +AlignEscapedNewlines: DontAlign +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: MultiLine + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: true + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyNamespace: false + SplitEmptyRecord: false +BitFieldColonSpacing: After +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BreakStringLiterals: true +ColumnLimit: 79 +DerivePointerAlignment: false +IndentCaseBlocks: true +IndentCaseLabels: true +IndentWidth: 4 +MaxEmptyLinesToKeep: 2 +PointerAlignment: Right +SortIncludes: false +SpaceBeforeParens: ControlStatementsExceptControlMacros +UseTab: Never + +# Force fun return type and fun definition to stay on 2 different lines: +# static int +# foo() { +# printf(); +# } +AlwaysBreakAfterReturnType: TopLevelDefinitions + +# Prevents: +# foo = +# Bar(...) +PenaltyBreakAssignment: 400 +PenaltyBreakBeforeFirstCallParameter: 0 + +# Handle macros with no `;` at EOL, so that they don't include the next line +# into them. +StatementMacros: + - Py_BEGIN_ALLOW_THREADS + - Py_END_ALLOW_THREADS diff --git a/.dprint.jsonc b/.dprint.jsonc index 3500bd32d1..99ab830286 100644 --- a/.dprint.jsonc +++ b/.dprint.jsonc @@ -14,6 +14,7 @@ "associations": [ "**/*.yml", "**/*.yaml", + "**/.clang-format", ], }, "excludes": [ diff --git a/MANIFEST.in b/MANIFEST.in index 9fbe1fff8c..0aba3d60ff 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +include .clang-format include .dprint.jsonc include .gitignore include CONTRIBUTING.md @@ -157,7 +158,6 @@ include scripts/ifconfig.py include scripts/internal/README include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py -include scripts/internal/clinter.py include scripts/internal/convert_readme.py include scripts/internal/download_wheels.py include scripts/internal/find_broken_links.py diff --git a/Makefile b/Makefile index 35ac61d74f..81f3b2614e 100644 --- a/Makefile +++ b/Makefile @@ -178,7 +178,8 @@ dprint: @$(DPRINT) check lint-c: ## Run C linter. - @git ls-files '*.c' '*.h' | xargs $(PYTHON) scripts/internal/clinter.py +# @git ls-files '*.c' '*.h' | xargs clang-format --dry-run --Werror # serial exec + @git ls-files '*.c' '*.h' | xargs -P0 -I{} clang-format --dry-run --Werror {} lint-rst: ## Run linter for .rst files. @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml @@ -189,8 +190,8 @@ lint-toml: ## Run linter for pyproject.toml. lint-all: ## Run all linters ${MAKE} black ${MAKE} ruff - ${MAKE} dprint ${MAKE} lint-c + ${MAKE} dprint ${MAKE} lint-rst ${MAKE} lint-toml @@ -212,6 +213,10 @@ fix-black: fix-ruff: @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --fix --output-format=concise $(ARGS) +fix-c: +# @git ls-files '*.c' '*.h' | xargs clang-format -i # serial exec + @git ls-files '*.c' '*.h' | xargs -P0 -I{} clang-format -i {} # parallel exec + fix-toml: ## Fix pyproject.toml @git ls-files '*.toml' | xargs toml-sort @@ -231,6 +236,8 @@ fix-all: ## Run all code fixers. ci-lint: ## Run all linters on GitHub CI. $(PYTHON) -m pip install -U black ruff rstcheck toml-sort sphinx curl -fsSL https://dprint.dev/install.sh | sh + $(DPRINT) --version + clang-format --version ${MAKE} lint-all ci-test: ## Run tests on GitHub CI. Used by BSD runners. diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index a7b6b80b60..6253361dcc 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -56,7 +56,7 @@ #include "arch/aix/common.h" -#define TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) +#define TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) /* * Read a file content and fills a C structure with it. @@ -98,37 +98,40 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { pstatus_t status; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/psinfo", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; if (info.pr_nlwp == 0 && info.pr_lwp.pr_lwpid == 0) { // From the /proc docs: "If the process is a zombie, the pr_nlwp // and pr_lwp.pr_lwpid flags are zero." status.pr_stat = SZOMB; - } else if (info.pr_flag & SEXIT) { + } + else if (info.pr_flag & SEXIT) { // "exiting" processes don't have /proc//status // There are other "exiting" processes that 'ps' shows as "active" status.pr_stat = SACTIVE; - } else { + } + else { sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&status, sizeof(status))) + if (!psutil_file_to_struct(path, (void *)&status, sizeof(status))) return NULL; } - return Py_BuildValue("KKKdiiiK", - (unsigned long long) info.pr_ppid, // parent pid - (unsigned long long) info.pr_rssize, // rss - (unsigned long long) info.pr_size, // vms - TV2DOUBLE(info.pr_start), // create time - (int) info.pr_lwp.pr_nice, // nice - (int) info.pr_nlwp, // no. of threads - (int) status.pr_stat, // status code - (unsigned long long)info.pr_ttydev // tty nr - ); + return Py_BuildValue( + "KKKdiiiK", + (unsigned long long)info.pr_ppid, // parent pid + (unsigned long long)info.pr_rssize, // rss + (unsigned long long)info.pr_size, // vms + TV2DOUBLE(info.pr_start), // create time + (int)info.pr_lwp.pr_nice, // nice + (int)info.pr_nlwp, // no. of threads + (int)status.pr_stat, // status code + (unsigned long long)info.pr_ttydev // tty nr + ); } @@ -142,10 +145,10 @@ psutil_proc_name(PyObject *self, PyObject *args) { psinfo_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/psinfo", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return PyUnicode_DecodeFSDefaultAndSize(info.pr_fname, PRFNSZ); @@ -253,8 +256,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { separator = strchr(curvar, '='); if (separator != NULL) { py_key = PyUnicode_DecodeFSDefaultAndSize( - curvar, - (Py_ssize_t)(separator - curvar) + curvar, (Py_ssize_t)(separator - curvar) ); if (!py_key) goto error; @@ -300,7 +302,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (!PyArg_ParseTuple(args, "l", &pid)) goto error; /* Get the count of threads */ @@ -311,16 +313,18 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } /* Allocate enough memory */ - threadt = (perfstat_thread_t *)calloc(thread_count, - sizeof(perfstat_thread_t)); + threadt = (perfstat_thread_t *)calloc( + thread_count, sizeof(perfstat_thread_t) + ); if (threadt == NULL) { PyErr_NoMemory(); goto error; } strcpy(id.name, ""); - rc = perfstat_thread(&id, threadt, sizeof(perfstat_thread_t), - thread_count); + rc = perfstat_thread( + &id, threadt, sizeof(perfstat_thread_t), thread_count + ); if (rc <= 0) { PyErr_SetFromErrno(PyExc_OSError); goto error; @@ -330,10 +334,9 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (threadt[i].pid != pid) continue; - py_tuple = Py_BuildValue("Idd", - threadt[i].tid, - threadt[i].ucpu_time, - threadt[i].scpu_time); + py_tuple = Py_BuildValue( + "Idd", threadt[i].tid, threadt[i].ucpu_time, threadt[i].scpu_time + ); if (py_tuple == NULL) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -362,7 +365,7 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { int rc; perfstat_process_t procinfo; perfstat_id_t id; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (!PyArg_ParseTuple(args, "l", &pid)) return NULL; snprintf(id.name, sizeof(id.name), "%ld", pid); @@ -372,11 +375,13 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { return NULL; } - return Py_BuildValue("(KKKK)", - procinfo.inOps, // XXX always 0 - procinfo.outOps, - procinfo.inBytes, // XXX always 0 - procinfo.outBytes); + return Py_BuildValue( + "(KKKK)", + procinfo.inOps, // XXX always 0 + procinfo.outOps, + procinfo.inBytes, // XXX always 0 + procinfo.outBytes + ); } #endif @@ -392,17 +397,19 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { pstatus_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; // results are more precise than os.times() - return Py_BuildValue("dddd", - TV2DOUBLE(info.pr_utime), - TV2DOUBLE(info.pr_stime), - TV2DOUBLE(info.pr_cutime), - TV2DOUBLE(info.pr_cstime)); + return Py_BuildValue( + "dddd", + TV2DOUBLE(info.pr_utime), + TV2DOUBLE(info.pr_stime), + TV2DOUBLE(info.pr_cutime), + TV2DOUBLE(info.pr_cstime) + ); } @@ -416,14 +423,20 @@ psutil_proc_cred(PyObject *self, PyObject *args) { prcred_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/cred", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; - return Py_BuildValue("iiiiii", - info.pr_ruid, info.pr_euid, info.pr_suid, - info.pr_rgid, info.pr_egid, info.pr_sgid); + return Py_BuildValue( + "iiiiii", + info.pr_ruid, + info.pr_euid, + info.pr_suid, + info.pr_rgid, + info.pr_egid, + info.pr_sgid + ); } @@ -439,7 +452,7 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { struct procentry64 *processes = (struct procentry64 *)NULL; struct procentry64 *p; - if (! PyArg_ParseTuple(args, "i", &requested_pid)) + if (!PyArg_ParseTuple(args, "i", &requested_pid)) return NULL; processes = psutil_read_process_table(&np); @@ -451,9 +464,11 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { pid = p->pi_pid; if (requested_pid != pid) continue; - py_tuple = Py_BuildValue("LL", - (long long) p->pi_ru.ru_nvcsw, /* voluntary context switches */ - (long long) p->pi_ru.ru_nivcsw); /* involuntary */ + py_tuple = Py_BuildValue( + "LL", + (long long)p->pi_ru.ru_nvcsw, /* voluntary context switches */ + (long long)p->pi_ru.ru_nivcsw + ); /* involuntary */ free(processes); return py_tuple; } @@ -471,7 +486,7 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { static PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { FILE *file = NULL; - struct mntent * mt = NULL; + struct mntent *mt = NULL; PyObject *py_dev = NULL; PyObject *py_mountp = NULL; PyObject *py_tuple = NULL; @@ -488,17 +503,18 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { mt = getmntent(file); while (mt != NULL) { py_dev = PyUnicode_DecodeFSDefault(mt->mnt_fsname); - if (! py_dev) + if (!py_dev) goto error; py_mountp = PyUnicode_DecodeFSDefault(mt->mnt_dir); - if (! py_mountp) + if (!py_mountp) goto error; py_tuple = Py_BuildValue( "(OOss)", - py_dev, // device - py_mountp, // mount point - mt->mnt_type, // fs type - mt->mnt_opts); // options + py_dev, // device + py_mountp, // mount point + mt->mnt_type, // fs type + mt->mnt_opts // options + ); if (py_tuple == NULL) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -541,7 +557,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { /* check how many perfstat_netinterface_t structures are available */ tot = perfstat_netinterface( - NULL, NULL, sizeof(perfstat_netinterface_t), 0); + NULL, NULL, sizeof(perfstat_netinterface_t), 0 + ); if (tot == 0) { // no network interfaces - return empty dict return py_retdict; @@ -550,31 +567,34 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_OSError); goto error; } - statp = (perfstat_netinterface_t *) - malloc(tot * sizeof(perfstat_netinterface_t)); + statp = (perfstat_netinterface_t *)malloc( + tot * sizeof(perfstat_netinterface_t) + ); if (statp == NULL) { PyErr_NoMemory(); goto error; } strcpy(first.name, FIRST_NETINTERFACE); - tot = perfstat_netinterface(&first, statp, - sizeof(perfstat_netinterface_t), tot); + tot = perfstat_netinterface( + &first, statp, sizeof(perfstat_netinterface_t), tot + ); if (tot < 0) { PyErr_SetFromErrno(PyExc_OSError); goto error; } for (i = 0; i < tot; i++) { - py_ifc_info = Py_BuildValue("(KKKKKKKK)", - statp[i].obytes, /* number of bytes sent on interface */ - statp[i].ibytes, /* number of bytes received on interface */ - statp[i].opackets, /* number of packets sent on interface */ - statp[i].ipackets, /* number of packets received on interface */ - statp[i].ierrors, /* number of input errors on interface */ - statp[i].oerrors, /* number of output errors on interface */ - statp[i].if_iqdrops, /* Dropped on input, this interface */ - statp[i].xmitdrops /* number of packets not transmitted */ - ); + py_ifc_info = Py_BuildValue( + "(KKKKKKKK)", + statp[i].obytes, // bytes sent + statp[i].ibytes, // bytes received + statp[i].opackets, // packets sent + statp[i].ipackets, // packets received + statp[i].ierrors, // input errors + statp[i].oerrors, // output errors + statp[i].if_iqdrop s, // dropped on input + statp[i].xmitdrops // not transmitted + ); if (!py_ifc_info) goto error; if (PyDict_SetItemString(py_retdict, statp[i].name, py_ifc_info)) @@ -596,8 +616,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { #endif -static PyObject* -psutil_net_if_stats(PyObject* self, PyObject* args) { +static PyObject * +psutil_net_if_stats(PyObject *self, PyObject *args) { char *nic_name; int sock = 0; int ret; @@ -606,7 +626,7 @@ psutil_net_if_stats(PyObject* self, PyObject* args) { PyObject *py_is_up = NULL; PyObject *py_retlist = NULL; - if (! PyArg_ParseTuple(args, "s", &nic_name)) + if (!PyArg_ParseTuple(args, "s", &nic_name)) return NULL; sock = socket(AF_INET, SOCK_DGRAM, 0); @@ -695,13 +715,13 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { /* get the number of cpus in ncpu */ ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); - if (ncpu <= 0){ + if (ncpu <= 0) { PyErr_SetFromErrno(PyExc_OSError); goto error; } /* allocate enough memory to hold the ncpu structures */ - cpu = (perfstat_cpu_t *) malloc(ncpu * sizeof(perfstat_cpu_t)); + cpu = (perfstat_cpu_t *)malloc(ncpu * sizeof(perfstat_cpu_t)); if (cpu == NULL) { PyErr_NoMemory(); goto error; @@ -721,7 +741,8 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { (double)cpu[i].user / ticks, (double)cpu[i].sys / ticks, (double)cpu[i].idle / ticks, - (double)cpu[i].wait / ticks); + (double)cpu[i].wait / ticks + ); if (!py_cputime) goto error; if (PyList_Append(py_retlist, py_cputime)) @@ -762,16 +783,14 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { } /* Allocate enough memory */ - diskt = (perfstat_disk_t *)calloc(disk_count, - sizeof(perfstat_disk_t)); + diskt = (perfstat_disk_t *)calloc(disk_count, sizeof(perfstat_disk_t)); if (diskt == NULL) { PyErr_NoMemory(); goto error; } strcpy(id.name, FIRST_DISK); - rc = perfstat_disk(&id, diskt, sizeof(perfstat_disk_t), - disk_count); + rc = perfstat_disk(&id, diskt, sizeof(perfstat_disk_t), disk_count); if (rc <= 0) { PyErr_SetFromErrno(PyExc_OSError); goto error; @@ -785,12 +804,11 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { diskt[i].rblks * diskt[i].bsize, diskt[i].wblks * diskt[i].bsize, diskt[i].rserv / 1000 / 1000, // from nano to milli secs - diskt[i].wserv / 1000 / 1000 // from nano to milli secs + diskt[i].wserv / 1000 / 1000 // from nano to milli secs ); if (py_disk_info == NULL) goto error; - if (PyDict_SetItemString(py_retdict, diskt[i].name, - py_disk_info)) + if (PyDict_SetItemString(py_retdict, diskt[i].name, py_disk_info)) goto error; Py_DECREF(py_disk_info); } @@ -816,18 +834,20 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { perfstat_memory_total_t memory; rc = perfstat_memory_total( - NULL, &memory, sizeof(perfstat_memory_total_t), 1); - if (rc <= 0){ + NULL, &memory, sizeof(perfstat_memory_total_t), 1 + ); + if (rc <= 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } - return Py_BuildValue("KKKKK", - (unsigned long long) memory.real_total * pagesize, - (unsigned long long) memory.real_avail * pagesize, - (unsigned long long) memory.real_free * pagesize, - (unsigned long long) memory.real_pinned * pagesize, - (unsigned long long) memory.real_inuse * pagesize + return Py_BuildValue( + "KKKKK", + (unsigned long long)memory.real_total * pagesize, + (unsigned long long)memory.real_avail * pagesize, + (unsigned long long)memory.real_free * pagesize, + (unsigned long long)memory.real_pinned * pagesize, + (unsigned long long)memory.real_inuse * pagesize ); } @@ -842,17 +862,19 @@ psutil_swap_mem(PyObject *self, PyObject *args) { perfstat_memory_total_t memory; rc = perfstat_memory_total( - NULL, &memory, sizeof(perfstat_memory_total_t), 1); - if (rc <= 0){ + NULL, &memory, sizeof(perfstat_memory_total_t), 1 + ); + if (rc <= 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } - return Py_BuildValue("KKKK", - (unsigned long long) memory.pgsp_total * pagesize, - (unsigned long long) memory.pgsp_free * pagesize, - (unsigned long long) memory.pgins * pagesize, - (unsigned long long) memory.pgouts * pagesize + return Py_BuildValue( + "KKKK", + (unsigned long long)memory.pgsp_total * pagesize, + (unsigned long long)memory.pgsp_free * pagesize, + (unsigned long long)memory.pgins * pagesize, + (unsigned long long)memory.pgouts * pagesize ); } @@ -874,13 +896,13 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { /* get the number of cpus in ncpu */ ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); - if (ncpu <= 0){ + if (ncpu <= 0) { PyErr_SetFromErrno(PyExc_OSError); goto error; } /* allocate enough memory to hold the ncpu structures */ - cpu = (perfstat_cpu_t *) malloc(ncpu * sizeof(perfstat_cpu_t)); + cpu = (perfstat_cpu_t *)malloc(ncpu * sizeof(perfstat_cpu_t)); if (cpu == NULL) { PyErr_NoMemory(); goto error; @@ -903,13 +925,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { free(cpu); - return Py_BuildValue( - "KKKK", - cswitches, - devintrs, - softintrs, - syscall - ); + return Py_BuildValue("KKKK", cswitches, devintrs, softintrs, syscall); error: if (cpu != NULL) @@ -921,9 +937,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { /* * define the psutil C module methods and initialize the module. */ -static PyMethodDef -PsutilMethods[] = -{ +static PyMethodDef PsutilMethods[] = { // --- process-related functions {"proc_args", psutil_proc_args, METH_VARARGS}, {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS}, @@ -965,7 +979,7 @@ struct module_state { PyObject *error; }; -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) +#define GETSTATE(m) ((struct module_state *)PyModule_GetState(m)) #ifdef __cplusplus extern "C" { diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index c4011506da..02373b6cc2 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -19,17 +19,17 @@ #include #include #include // BSD version -#include // for TCP connection states +#include // for TCP connection states #include "arch/all/init.h" #include "arch/bsd/init.h" #ifdef PSUTIL_FREEBSD - #include "arch/freebsd/init.h" +#include "arch/freebsd/init.h" #elif PSUTIL_OPENBSD - #include "arch/openbsd/init.h" +#include "arch/openbsd/init.h" #elif PSUTIL_NETBSD - #include "arch/netbsd/init.h" +#include "arch/netbsd/init.h" #endif @@ -77,7 +77,7 @@ static PyMethodDef mod_methods[] = { #endif {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) - {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, + {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) {"cpu_topology", psutil_cpu_topology, METH_VARARGS}, @@ -104,12 +104,12 @@ static struct PyModuleDef moduledef = { NULL }; -PyObject -*PyInit__psutil_bsd(void) { +PyObject * +PyInit__psutil_bsd(void) { PyObject *v; PyObject *mod = PyModule_Create(&moduledef); if (mod == NULL) - return NULL; + return NULL; #ifdef Py_GIL_DISABLED if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) @@ -126,7 +126,7 @@ PyObject if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; - // process status constants + // process status constants #ifdef PSUTIL_FREEBSD if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) return NULL; @@ -142,7 +142,7 @@ PyObject return NULL; if (PyModule_AddIntConstant(mod, "SLOCK", SLOCK)) return NULL; -#elif PSUTIL_OPENBSD +#elif PSUTIL_OPENBSD if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) return NULL; if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) @@ -152,7 +152,7 @@ PyObject if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) return NULL; if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) - return NULL; // unused + return NULL; // unused if (PyModule_AddIntConstant(mod, "SDEAD", SDEAD)) return NULL; if (PyModule_AddIntConstant(mod, "SONPROC", SONPROC)) @@ -181,27 +181,27 @@ PyObject // connection status constants if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) - return NULL; + return NULL; if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", 128)) return NULL; diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 2cfb4e47c4..2b6589c2cf 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -7,7 +7,7 @@ */ #ifndef _GNU_SOURCE - #define _GNU_SOURCE 1 +#define _GNU_SOURCE 1 #endif #include #include // DUPLEX_* @@ -17,11 +17,11 @@ // May happen on old RedHat versions, see: // https://github.com/giampaolo/psutil/issues/607 #ifndef DUPLEX_UNKNOWN - #define DUPLEX_UNKNOWN 0xff +#define DUPLEX_UNKNOWN 0xff #endif static PyMethodDef mod_methods[] = { - // --- per-process functions +// --- per-process functions #ifdef PSUTIL_HAS_IOPRIO {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS}, {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS}, diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index dbb33efc0b..7e345010be 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -28,7 +28,8 @@ static PyMethodDef mod_methods[] = { {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, - {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS}, + {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS + }, {"proc_threads", psutil_proc_threads, METH_VARARGS}, // --- system-related functions diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 084ba0a718..6cdc83d57e 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -12,7 +12,7 @@ /* fix compilation issue on SunOS 5.10, see: * https://github.com/giampaolo/psutil/issues/421 * https://github.com/giampaolo/psutil/issues/1077 -*/ + */ #define _STRUCTURED_PROC 1 #define NEW_MIB_COMPLIANT 1 @@ -20,8 +20,8 @@ #include #if !defined(_LP64) && _FILE_OFFSET_BITS == 64 -# undef _FILE_OFFSET_BITS -# undef _LARGEFILE64_SOURCE +#undef _FILE_OFFSET_BITS +#undef _LARGEFILE64_SOURCE #endif #include diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 618d759203..1608ac2d1f 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -21,15 +21,15 @@ #include "arch/windows/init.h" -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) +#define GETSTATE(m) ((struct module_state *)PyModule_GetState(m)) // ------------------------ Python init --------------------------- -static PyMethodDef -PsutilMethods[] = { +static PyMethodDef PsutilMethods[] = { // --- per-process functions - {"proc_cmdline", (PyCFunction)(void(*)(void))psutil_proc_cmdline, + {"proc_cmdline", + (PyCFunction)(void (*)(void))psutil_proc_cmdline, METH_VARARGS | METH_KEYWORDS}, {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, @@ -70,7 +70,9 @@ PsutilMethods[] = { {"getloadavg", (PyCFunction)psutil_get_loadavg, METH_VARARGS}, {"getpagesize", psutil_getpagesize, METH_VARARGS}, {"swap_percent", psutil_swap_percent, METH_VARARGS}, - {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, METH_VARARGS}, + {"init_loadavg_counter", + (PyCFunction)psutil_init_loadavg_counter, + METH_VARARGS}, {"net_connections", psutil_net_connections, METH_VARARGS}, {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, @@ -152,14 +154,16 @@ PyInit__psutil_windows(void) { // Exceptions TimeoutExpired = PyErr_NewException( - "_psutil_windows.TimeoutExpired", NULL, NULL); + "_psutil_windows.TimeoutExpired", NULL, NULL + ); if (TimeoutExpired == NULL) return NULL; if (PyModule_AddObject(mod, "TimeoutExpired", TimeoutExpired)) return NULL; TimeoutAbandoned = PyErr_NewException( - "_psutil_windows.TimeoutAbandoned", NULL, NULL); + "_psutil_windows.TimeoutAbandoned", NULL, NULL + ); if (TimeoutAbandoned == NULL) return NULL; if (PyModule_AddObject(mod, "TimeoutAbandoned", TimeoutAbandoned)) @@ -171,46 +175,84 @@ PyInit__psutil_windows(void) { // process status constants // http://msdn.microsoft.com/en-us/library/ms683211(v=vs.85).aspx - if (PyModule_AddIntConstant(mod, "ABOVE_NORMAL_PRIORITY_CLASS", ABOVE_NORMAL_PRIORITY_CLASS)) + if (PyModule_AddIntConstant( + mod, "ABOVE_NORMAL_PRIORITY_CLASS", ABOVE_NORMAL_PRIORITY_CLASS + )) return NULL; - if (PyModule_AddIntConstant(mod, "BELOW_NORMAL_PRIORITY_CLASS", BELOW_NORMAL_PRIORITY_CLASS)) + if (PyModule_AddIntConstant( + mod, "BELOW_NORMAL_PRIORITY_CLASS", BELOW_NORMAL_PRIORITY_CLASS + )) return NULL; - if (PyModule_AddIntConstant(mod, "HIGH_PRIORITY_CLASS", HIGH_PRIORITY_CLASS)) + if (PyModule_AddIntConstant( + mod, "HIGH_PRIORITY_CLASS", HIGH_PRIORITY_CLASS + )) return NULL; - if (PyModule_AddIntConstant(mod, "IDLE_PRIORITY_CLASS", IDLE_PRIORITY_CLASS)) + if (PyModule_AddIntConstant( + mod, "IDLE_PRIORITY_CLASS", IDLE_PRIORITY_CLASS + )) return NULL; - if (PyModule_AddIntConstant(mod, "NORMAL_PRIORITY_CLASS", NORMAL_PRIORITY_CLASS)) + if (PyModule_AddIntConstant( + mod, "NORMAL_PRIORITY_CLASS", NORMAL_PRIORITY_CLASS + )) return NULL; - if (PyModule_AddIntConstant(mod, "REALTIME_PRIORITY_CLASS", REALTIME_PRIORITY_CLASS)) + if (PyModule_AddIntConstant( + mod, "REALTIME_PRIORITY_CLASS", REALTIME_PRIORITY_CLASS + )) return NULL; // connection status constants // http://msdn.microsoft.com/en-us/library/cc669305.aspx - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_CLOSED", MIB_TCP_STATE_CLOSED)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_CLOSED", MIB_TCP_STATE_CLOSED + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_CLOSING", MIB_TCP_STATE_CLOSING)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_CLOSING", MIB_TCP_STATE_CLOSING + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_CLOSE_WAIT", MIB_TCP_STATE_CLOSE_WAIT)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_CLOSE_WAIT", MIB_TCP_STATE_CLOSE_WAIT + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_LISTEN", MIB_TCP_STATE_LISTEN)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_LISTEN", MIB_TCP_STATE_LISTEN + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_ESTAB", MIB_TCP_STATE_ESTAB)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_ESTAB", MIB_TCP_STATE_ESTAB + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_SYN_SENT", MIB_TCP_STATE_SYN_SENT)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_SYN_SENT", MIB_TCP_STATE_SYN_SENT + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_SYN_RCVD", MIB_TCP_STATE_SYN_RCVD)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_SYN_RCVD", MIB_TCP_STATE_SYN_RCVD + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_FIN_WAIT1", MIB_TCP_STATE_FIN_WAIT1)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_FIN_WAIT1", MIB_TCP_STATE_FIN_WAIT1 + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_FIN_WAIT2", MIB_TCP_STATE_FIN_WAIT2)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_FIN_WAIT2", MIB_TCP_STATE_FIN_WAIT2 + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_LAST_ACK", MIB_TCP_STATE_LAST_ACK)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_LAST_ACK", MIB_TCP_STATE_LAST_ACK + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_TIME_WAIT", MIB_TCP_STATE_TIME_WAIT + )) return NULL; - if (PyModule_AddIntConstant(mod, "MIB_TCP_STATE_DELETE_TCB", MIB_TCP_STATE_DELETE_TCB)) + if (PyModule_AddIntConstant( + mod, "MIB_TCP_STATE_DELETE_TCB", MIB_TCP_STATE_DELETE_TCB + )) return NULL; if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) return NULL; @@ -218,13 +260,19 @@ PyInit__psutil_windows(void) { // ...for internal use in _psutil_windows.py if (PyModule_AddIntConstant(mod, "INFINITE", INFINITE)) return NULL; - if (PyModule_AddIntConstant(mod, "ERROR_ACCESS_DENIED", ERROR_ACCESS_DENIED)) + if (PyModule_AddIntConstant( + mod, "ERROR_ACCESS_DENIED", ERROR_ACCESS_DENIED + )) return NULL; if (PyModule_AddIntConstant(mod, "ERROR_INVALID_NAME", ERROR_INVALID_NAME)) return NULL; - if (PyModule_AddIntConstant(mod, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST)) + if (PyModule_AddIntConstant( + mod, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST + )) return NULL; - if (PyModule_AddIntConstant(mod, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD)) + if (PyModule_AddIntConstant( + mod, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD + )) return NULL; if (PyModule_AddIntConstant(mod, "WINVER", PSUTIL_WINVER)) return NULL; diff --git a/psutil/arch/aix/common.c b/psutil/arch/aix/common.c index 945cbd978b..b0d0de3997 100644 --- a/psutil/arch/aix/common.c +++ b/psutil/arch/aix/common.c @@ -13,10 +13,11 @@ /* psutil_kread() - read from kernel memory */ int psutil_kread( - int Kd, /* kernel memory file descriptor */ - KA_T addr, /* kernel memory address */ - char *buf, /* buffer to receive data */ - size_t len) { /* length to read */ + int Kd, // kernel memory file descriptor + KA_T addr, // kernel memory address + char *buf, // buffer to receive data + size_t len // length to read +) { int br; if (lseek64(Kd, (off64_t)addr, L_SET) == (off64_t)-1) { @@ -29,22 +30,23 @@ psutil_kread( return 1; } if (br != len) { - PyErr_SetString(PyExc_RuntimeError, - "size mismatch when reading kernel memory fd"); + PyErr_SetString( + PyExc_RuntimeError, "size mismatch when reading kernel memory fd" + ); return 1; } return 0; } struct procentry64 * -psutil_read_process_table(int * num) { +psutil_read_process_table(int *num) { size_t msz; pid32_t pid = 0; struct procentry64 *processes = (struct procentry64 *)NULL; struct procentry64 *p; - int Np = 0; /* number of processes allocated in 'processes' */ - int np = 0; /* number of processes read into 'processes' */ - int i; /* number of processes read in current iteration */ + int Np = 0; // number of processes allocated in 'processes' + int np = 0; // number of processes read into 'processes' + int i; // number of processes read in current iteration msz = (size_t)(PROCSIZE * PROCINFO_INCR); processes = (struct procentry64 *)malloc(msz); @@ -54,9 +56,11 @@ psutil_read_process_table(int * num) { } Np = PROCINFO_INCR; p = processes; - while ((i = getprocs64(p, PROCSIZE, (struct fdsinfo64 *)NULL, 0, &pid, - PROCINFO_INCR)) - == PROCINFO_INCR) { + while ((i = getprocs64( + p, PROCSIZE, (struct fdsinfo64 *)NULL, 0, &pid, PROCINFO_INCR + )) + == PROCINFO_INCR) + { np += PROCINFO_INCR; if (np >= Np) { msz = (size_t)(PROCSIZE * (Np + PROCINFO_INCR)); diff --git a/psutil/arch/aix/common.h b/psutil/arch/aix/common.h index b677d8c29e..a4b8662f59 100644 --- a/psutil/arch/aix/common.h +++ b/psutil/arch/aix/common.h @@ -10,22 +10,23 @@ #include -#define PROCINFO_INCR (256) -#define PROCSIZE (sizeof(struct procentry64)) -#define FDSINFOSIZE (sizeof(struct fdsinfo64)) -#define KMEM "/dev/kmem" +#define PROCINFO_INCR (256) +#define PROCSIZE (sizeof(struct procentry64)) +#define FDSINFOSIZE (sizeof(struct fdsinfo64)) +#define KMEM "/dev/kmem" -typedef u_longlong_t KA_T; +typedef u_longlong_t KA_T; -/* psutil_kread() - read from kernel memory */ -int psutil_kread(int Kd, /* kernel memory file descriptor */ - KA_T addr, /* kernel memory address */ - char *buf, /* buffer to receive data */ - size_t len); /* length to read */ +// psutil_kread() - read from kernel memory +int psutil_kread( + int Kd, // kernel memory file descriptor + KA_T addr, // kernel memory address + char *buf, // buffer to receive data + size_t len // length to read +); -struct procentry64 * -psutil_read_process_table( - int * num /* out - number of processes read */ +struct procentry64 *psutil_read_process_table( + int *num // out - number of processes read ); -#endif /* __PSUTIL_AIX_COMMON_H__ */ +#endif // __PSUTIL_AIX_COMMON_H__ diff --git a/psutil/arch/aix/ifaddrs.c b/psutil/arch/aix/ifaddrs.c index 1480b60fab..af765c9186 100644 --- a/psutil/arch/aix/ifaddrs.c +++ b/psutil/arch/aix/ifaddrs.c @@ -20,16 +20,15 @@ #include "ifaddrs.h" -#define MAX(x,y) ((x)>(y)?(x):(y)) -#define SIZE(p) MAX((p).sa_len,sizeof(p)) +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#define SIZE(p) MAX((p).sa_len, sizeof(p)) static struct sockaddr * -sa_dup(struct sockaddr *sa1) -{ +sa_dup(struct sockaddr *sa1) { struct sockaddr *sa2; size_t sz = sa1->sa_len; - sa2 = (struct sockaddr *) calloc(1, sz); + sa2 = (struct sockaddr *)calloc(1, sz); if (sa2 == NULL) return NULL; memcpy(sa2, sa1, sz); @@ -37,9 +36,10 @@ sa_dup(struct sockaddr *sa1) } -void freeifaddrs(struct ifaddrs *ifp) -{ - if (NULL == ifp) return; +void +freeifaddrs(struct ifaddrs *ifp) { + if (NULL == ifp) + return; free(ifp->ifa_name); free(ifp->ifa_addr); free(ifp->ifa_netmask); @@ -49,8 +49,8 @@ void freeifaddrs(struct ifaddrs *ifp) } -int getifaddrs(struct ifaddrs **ifap) -{ +int +getifaddrs(struct ifaddrs **ifap) { int sd, ifsize; char *ccp, *ecp; struct ifconf ifc; @@ -70,7 +70,7 @@ int getifaddrs(struct ifaddrs **ifap) if (ioctl(sd, SIOCGSIZIFCONF, (caddr_t)&ifsize) < 0) goto error; - ifc.ifc_req = (struct ifreq *) calloc(1, ifsize); + ifc.ifc_req = (struct ifreq *)calloc(1, ifsize); if (ifc.ifc_req == NULL) goto error; ifc.ifc_len = ifsize; @@ -82,19 +82,20 @@ int getifaddrs(struct ifaddrs **ifap) ecp = ccp + ifsize; while (ccp < ecp) { - - ifr = (struct ifreq *) ccp; + ifr = (struct ifreq *)ccp; ifsize = sizeof(ifr->ifr_name) + SIZE(ifr->ifr_addr); fam = ifr->ifr_addr.sa_family; if (fam == AF_INET || fam == AF_INET6) { - cifa = (struct ifaddrs *) calloc(1, sizeof(struct ifaddrs)); + cifa = (struct ifaddrs *)calloc(1, sizeof(struct ifaddrs)); if (cifa == NULL) goto error; cifa->ifa_next = NULL; - if (pifa == NULL) *ifap = cifa; /* first one */ - else pifa->ifa_next = cifa; + if (pifa == NULL) + *ifap = cifa; // first one + else + pifa->ifa_next = cifa; cifa->ifa_name = strdup(ifr->ifr_name); if (cifa->ifa_name == NULL) diff --git a/psutil/arch/aix/ifaddrs.h b/psutil/arch/aix/ifaddrs.h index e15802bf7b..fe4126762b 100644 --- a/psutil/arch/aix/ifaddrs.h +++ b/psutil/arch/aix/ifaddrs.h @@ -16,14 +16,14 @@ #include #include -#undef ifa_dstaddr -#undef ifa_broadaddr +#undef ifa_dstaddr +#undef ifa_broadaddr #define ifa_broadaddr ifa_dstaddr struct ifaddrs { - struct ifaddrs *ifa_next; - char *ifa_name; - unsigned int ifa_flags; + struct ifaddrs *ifa_next; + char *ifa_name; + unsigned int ifa_flags; struct sockaddr *ifa_addr; struct sockaddr *ifa_netmask; struct sockaddr *ifa_dstaddr; diff --git a/psutil/arch/aix/net_connections.c b/psutil/arch/aix/net_connections.c index 71ad34341a..41d8ab173e 100644 --- a/psutil/arch/aix/net_connections.c +++ b/psutil/arch/aix/net_connections.c @@ -11,7 +11,7 @@ * - lib/prfp.c:process_file * - dialects/aix/dsock.c:process_socket * - dialects/aix/dproc.c:get_kernel_access -*/ + */ #include #include @@ -32,15 +32,10 @@ #include "common.h" -#define NO_SOCKET (PyObject *)(-1) +#define NO_SOCKET (PyObject *)(-1) static int -read_unp_addr( - int Kd, - KA_T unp_addr, - char *buf, - size_t buflen -) { +read_unp_addr(int Kd, KA_T unp_addr, char *buf, size_t buflen) { struct sockaddr_un *ua = (struct sockaddr_un *)NULL; struct sockaddr_un un; struct mbuf64 mb; @@ -54,8 +49,8 @@ read_unp_addr( if ((uo + sizeof(struct sockaddr)) <= sizeof(mb)) ua = (struct sockaddr_un *)((char *)&mb + uo); else { - if (psutil_kread(Kd, (KA_T)mb.m_hdr.mh_data, - (char *)&un, sizeof(un))) { + if (psutil_kread(Kd, (KA_T)mb.m_hdr.mh_data, (char *)&un, sizeof(un))) + { return 1; } ua = &un; @@ -85,8 +80,8 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { char laddr_str[INET6_ADDRSTRLEN]; char raddr_str[INET6_ADDRSTRLEN]; struct unpcb64 unp; - char unix_laddr_str[PATH_MAX] = { 0 }; - char unix_raddr_str[PATH_MAX] = { 0 }; + char unix_laddr_str[PATH_MAX] = {0}; + char unix_raddr_str[PATH_MAX] = {0}; /* Read file structure */ if (psutil_kread(Kd, fp, (char *)&f, sizeof(f))) { @@ -96,7 +91,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { return NO_SOCKET; } - if (psutil_kread(Kd, (KA_T) f.f_data, (char *) &s, sizeof(s))) { + if (psutil_kread(Kd, (KA_T)f.f_data, (char *)&s, sizeof(s))) { return NULL; } @@ -127,15 +122,16 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { PyErr_SetString(PyExc_RuntimeError, "invalid socket PCB"); return NULL; } - if (psutil_kread(Kd, (KA_T) s.so_pcb, (char *) &inp, sizeof(inp))) { + if (psutil_kread(Kd, (KA_T)s.so_pcb, (char *)&inp, sizeof(inp))) { return NULL; } if (p.pr_protocol == IPPROTO_TCP) { /* If this is a TCP socket, read its control block */ if (inp.inp_ppcb - && !psutil_kread(Kd, (KA_T)inp.inp_ppcb, - (char *)&t, sizeof(t))) + && !psutil_kread( + Kd, (KA_T)inp.inp_ppcb, (char *)&t, sizeof(t) + )) state = t.t_state; } @@ -149,7 +145,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { if (fam == AF_INET) { laddr = (unsigned char *)&inp.inp_laddr; if (inp.inp_faddr.s_addr != INADDR_ANY || inp.inp_fport != 0) { - raddr = (unsigned char *)&inp.inp_faddr; + raddr = (unsigned char *)&inp.inp_faddr; rport = (int)ntohs(inp.inp_fport); } } @@ -159,48 +155,77 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { if (raddr != NULL) { inet_ntop(fam, raddr, raddr_str, sizeof(raddr_str)); - return Py_BuildValue("(iii(si)(si)ii)", fd, fam, - s.so_type, laddr_str, lport, raddr_str, - rport, state, pid); + return Py_BuildValue( + "(iii(si)(si)ii)", + fd, + fam, + s.so_type, + laddr_str, + lport, + raddr_str, + rport, + state, + pid + ); } else { - return Py_BuildValue("(iii(si)()ii)", fd, fam, - s.so_type, laddr_str, lport, state, - pid); + return Py_BuildValue( + "(iii(si)()ii)", + fd, + fam, + s.so_type, + laddr_str, + lport, + state, + pid + ); } } if (fam == AF_UNIX) { - if (psutil_kread(Kd, (KA_T) s.so_pcb, (char *)&unp, sizeof(unp))) { + if (psutil_kread(Kd, (KA_T)s.so_pcb, (char *)&unp, sizeof(unp))) { return NULL; } - if ((KA_T) f.f_data != (KA_T) unp.unp_socket) { + if ((KA_T)f.f_data != (KA_T)unp.unp_socket) { PyErr_SetString(PyExc_RuntimeError, "unp_socket mismatch"); return NULL; } if (unp.unp_addr) { - if (read_unp_addr(Kd, unp.unp_addr, unix_laddr_str, - sizeof(unix_laddr_str))) { + if (read_unp_addr( + Kd, unp.unp_addr, unix_laddr_str, sizeof(unix_laddr_str) + )) + { return NULL; } } if (unp.unp_conn) { - if (psutil_kread(Kd, (KA_T) unp.unp_conn, (char *)&unp, - sizeof(unp))) { + if (psutil_kread( + Kd, (KA_T)unp.unp_conn, (char *)&unp, sizeof(unp) + )) + { return NULL; } - if (read_unp_addr(Kd, unp.unp_addr, unix_raddr_str, - sizeof(unix_raddr_str))) { + if (read_unp_addr( + Kd, unp.unp_addr, unix_raddr_str, sizeof(unix_raddr_str) + )) + { return NULL; } } - return Py_BuildValue("(iiissii)", fd, d.dom_family, - s.so_type, unix_laddr_str, unix_raddr_str, PSUTIL_CONN_NONE, - pid); + return Py_BuildValue( + "(iiissii)", + fd, + d.dom_family, + s.so_type, + unix_laddr_str, + unix_raddr_str, + PSUTIL_CONN_NONE, + pid + ); } return NO_SOCKET; } @@ -217,11 +242,11 @@ psutil_net_connections(PyObject *self, PyObject *args) { pid32_t requested_pid; pid32_t pid; struct procentry64 *processes = (struct procentry64 *)NULL; - /* the process table */ + // the process table if (py_retlist == NULL) goto error; - if (! PyArg_ParseTuple(args, "i", &requested_pid)) + if (!PyArg_ParseTuple(args, "i", &requested_pid)) goto error; Kd = open(KMEM, O_RDONLY, 0); @@ -249,9 +274,10 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } } - if (getprocs64((struct procentry64 *)NULL, PROCSIZE, fds, FDSINFOSIZE, - &pid, 1) - != 1) + if (getprocs64( + (struct procentry64 *)NULL, PROCSIZE, fds, FDSINFOSIZE, &pid, 1 + ) + != 1) continue; /* loop over file descriptors */ diff --git a/psutil/arch/aix/net_connections.h b/psutil/arch/aix/net_connections.h index d57ee42847..c1f10173a5 100644 --- a/psutil/arch/aix/net_connections.h +++ b/psutil/arch/aix/net_connections.h @@ -10,6 +10,6 @@ #include -PyObject* psutil_net_connections(PyObject *self, PyObject *args); +PyObject *psutil_net_connections(PyObject *self, PyObject *args); -#endif /* __NET_CONNECTIONS_H__ */ +#endif // __NET_CONNECTIONS_H__ diff --git a/psutil/arch/aix/net_kernel_structs.h b/psutil/arch/aix/net_kernel_structs.h index 7e22a1639a..7382280274 100644 --- a/psutil/arch/aix/net_kernel_structs.h +++ b/psutil/arch/aix/net_kernel_structs.h @@ -13,7 +13,7 @@ * and unused data truncated. */ #ifdef __64BIT__ -/* In case we're in a 64 bit process after all */ +// In case we're in a 64 bit process after all #include #include #include @@ -31,81 +31,79 @@ #define tcpcb64 tcpcb #define unpcb64 unpcb #define mbuf64 mbuf -#else /* __64BIT__ */ - struct file64 { +#else // __64BIT__ +struct file64 { int f_flag; int f_count; int f_options; int f_type; u_longlong_t f_data; - }; +}; - struct socket64 { - short so_type; /* generic type, see socket.h */ - short so_options; /* from socket call, see socket.h */ - ushort so_linger; /* time to linger while closing */ - short so_state; /* internal state flags SS_*, below */ - u_longlong_t so_pcb; /* protocol control block */ - u_longlong_t so_proto; /* protocol handle */ - }; +struct socket64 { + short so_type; // generic type, see socket.h + short so_options; // from socket call, see socket.h + ushort so_linger; // time to linger while closing + short so_state; // internal state flags SS_*, below + u_longlong_t so_pcb; // protocol control block + u_longlong_t so_proto; // protocol handle +}; - struct protosw64 { - short pr_type; /* socket type used for */ - u_longlong_t pr_domain; /* domain protocol a member of */ - short pr_protocol; /* protocol number */ - short pr_flags; /* see below */ - }; +struct protosw64 { + short pr_type; // socket type used for + u_longlong_t pr_domain; // domain protocol a member of + short pr_protocol; // protocol number + short pr_flags; // see below +}; - struct inpcb64 { - u_longlong_t inp_next,inp_prev; - /* pointers to other pcb's */ - u_longlong_t inp_head; /* pointer back to chain of inpcb's - for this protocol */ - u_int32_t inp_iflowinfo; /* input flow label */ - u_short inp_fport; /* foreign port */ - u_int16_t inp_fatype; /* foreign address type */ - union in_addr_6 inp_faddr_6; /* foreign host table entry */ - u_int32_t inp_oflowinfo; /* output flow label */ - u_short inp_lport; /* local port */ - u_int16_t inp_latype; /* local address type */ - union in_addr_6 inp_laddr_6; /* local host table entry */ - u_longlong_t inp_socket; /* back pointer to socket */ - u_longlong_t inp_ppcb; /* pointer to per-protocol pcb */ +struct inpcb64 { + u_longlong_t inp_next, inp_prev; + // pointers to other pcb's + u_longlong_t + inp_head; // pointer back to chain of inpcb's for this protocol + u_int32_t inp_iflowinfo; // input flow label + u_short inp_fport; // foreign port + u_int16_t inp_fatype; // foreign address type + union in_addr_6 inp_faddr_6; // foreign host table entry + u_int32_t inp_oflowinfo; // output flow label + u_short inp_lport; // local port + u_int16_t inp_latype; // local address type + union in_addr_6 inp_laddr_6; // local host table entry + u_longlong_t inp_socket; // back pointer to socket + u_longlong_t inp_ppcb; // pointer to per-protocol pcb u_longlong_t space_rt; - struct sockaddr_in6 spare_dst; - u_longlong_t inp_ifa; /* interface address to use */ - int inp_flags; /* generic IP/datagram flags */ + struct sockaddr_in6 spare_dst; + u_longlong_t inp_ifa; // interface address to use + int inp_flags; // generic IP/datagram flags }; struct tcpcb64 { u_longlong_t seg__next; u_longlong_t seg__prev; - short t_state; /* state of this connection */ + short t_state; // state of this connection }; struct unpcb64 { - u_longlong_t unp_socket; /* pointer back to socket */ - u_longlong_t unp_vnode; /* if associated with file */ - ino_t unp_vno; /* fake vnode number */ - u_longlong_t unp_conn; /* control block of connected socket */ - u_longlong_t unp_refs; /* referencing socket linked list */ - u_longlong_t unp_nextref; /* link in unp_refs list */ - u_longlong_t unp_addr; /* bound address of socket */ + u_longlong_t unp_socket; // pointer back to socket + u_longlong_t unp_vnode; // if associated with file + ino_t unp_vno; // fake vnode number + u_longlong_t unp_conn; // control block of connected socket + u_longlong_t unp_refs; // referencing socket linked list + u_longlong_t unp_nextref; // link in unp_refs list + u_longlong_t unp_addr; // bound address of socket }; -struct m_hdr64 -{ - u_longlong_t mh_next; /* next buffer in chain */ - u_longlong_t mh_nextpkt; /* next chain in queue/record */ - long mh_len; /* amount of data in this mbuf */ - u_longlong_t mh_data; /* location of data */ +struct m_hdr64 { + u_longlong_t mh_next; // next buffer in chain + u_longlong_t mh_nextpkt; // next chain in queue/record + long mh_len; // amount of data in this mbuf + u_longlong_t mh_data; // location of data }; -struct mbuf64 -{ +struct mbuf64 { struct m_hdr64 m_hdr; }; -#define m_len m_hdr.mh_len +#define m_len m_hdr.mh_len -#endif /* __64BIT__ */ +#endif // __64BIT__ diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 97fd457170..dfa21ab218 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -11,6 +11,7 @@ // We do this so that all .c files have to include only one header // (ourselves, init.h). +// clang-format off #if defined(PSUTIL_POSIX) #include "../../arch/posix/init.h" #endif @@ -47,14 +48,17 @@ extern int PSUTIL_CONN_NONE; #define UTXENT_MUTEX_LOCK() #define UTXENT_MUTEX_UNLOCK() #endif +// clang-format on // Print a debug message on stderr. -#define psutil_debug(...) do { \ - if (! PSUTIL_DEBUG) \ - break; \ - fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n");} while(0) +#define psutil_debug(...) \ + do { \ + if (!PSUTIL_DEBUG) \ + break; \ + fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while (0) // strncpy() variant which appends a null terminator. @@ -76,6 +80,7 @@ PyObject *psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); // --- _Py_PARSE_PID +// clang-format off // SIZEOF_INT|LONG is missing on Linux + PyPy (only?). // In this case we guess it from setup.py. It's not 100% bullet proof, // If wrong we'll probably get compiler warnings. @@ -117,6 +122,7 @@ PyObject *psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); "sizeof(long) or sizeof(long long)" #endif #endif +// clang-format on int psutil_badargs(const char *funcname); int psutil_setup(void); diff --git a/psutil/arch/bsd/disk.c b/psutil/arch/bsd/disk.c index 2f381da202..46a7101323 100644 --- a/psutil/arch/bsd/disk.c +++ b/psutil/arch/bsd/disk.c @@ -7,14 +7,14 @@ #include #include #if PSUTIL_NETBSD - // getvfsstat() - #include - #include +// getvfsstat() +#include +#include #else - // getfsstat() - #include - #include - #include +// getfsstat() +#include +#include +#include #endif #include "../../arch/all/init.h" @@ -151,16 +151,18 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",nodevmtime", sizeof(opts)); #endif py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); - if (! py_dev) + if (!py_dev) goto error; py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); - if (! py_mountp) + if (!py_mountp) goto error; - py_tuple = Py_BuildValue("(OOss)", - py_dev, // device - py_mountp, // mount point - fs[i].f_fstypename, // fs type - opts); // options + py_tuple = Py_BuildValue( + "(OOss)", + py_dev, // device + py_mountp, // mount point + fs[i].f_fstypename, // fs type + opts // options + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index f9ea974afd..7d051caee3 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -6,14 +6,13 @@ #include -void convert_kvm_err(const char *syscall, char *errbuf); - -#if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) - #define PSUTIL_HASNT_KINFO_GETFILE - - struct kinfo_file *kinfo_getfile(pid_t pid, int* cnt); +#if defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +#define PSUTIL_HASNT_KINFO_GETFILE +struct kinfo_file *kinfo_getfile(pid_t pid, int *cnt); #endif +void convert_kvm_err(const char *syscall, char *errbuf); + PyObject *psutil_boot_time(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); PyObject *psutil_cpu_times(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/net.c b/psutil/arch/bsd/net.c index 5f44d8e7b7..0c2c46734d 100644 --- a/psutil/arch/bsd/net.c +++ b/psutil/arch/bsd/net.c @@ -26,11 +26,11 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { if (py_retdict == NULL) return NULL; - mib[0] = CTL_NET; // networking subsystem - mib[1] = PF_ROUTE; // type of information - mib[2] = 0; // protocol (IPPROTO_xxx) - mib[3] = 0; // address family - mib[4] = NET_RT_IFLIST; // operation + mib[0] = CTL_NET; // networking subsystem + mib[1] = PF_ROUTE; // type of information + mib[2] = 0; // protocol (IPPROTO_xxx) + mib[3] = 0; // address family + mib[4] = NET_RT_IFLIST; // operation mib[5] = 0; if (psutil_sysctl_malloc(mib, 6, &buf, &len) != 0) @@ -72,7 +72,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { #else 0 #endif - ); + ); if (!py_ifc_info) goto error; if (PyDict_SetItemString(py_retdict, ifc_name, py_ifc_info) != 0) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 90b1a0d22a..bfa39ddb08 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -12,25 +12,25 @@ #include #include // VREG #ifdef PSUTIL_FREEBSD - #include // kinfo_proc, kinfo_file, KF_* - #include // kinfo_getfile() +#include // kinfo_proc, kinfo_file, KF_* +#include // kinfo_getfile() #endif #include "../../arch/all/init.h" #ifdef PSUTIL_FREEBSD - #include "../../arch/freebsd/init.h" // TODO: refactor this +#include "../../arch/freebsd/init.h" // TODO: refactor this #elif PSUTIL_OPENBSD - #include "../../arch/openbsd/init.h" // TODO: refactor this +#include "../../arch/openbsd/init.h" // TODO: refactor this #elif PSUTIL_NETBSD - #include "../../arch/netbsd/init.h" // TODO: refactor this +#include "../../arch/netbsd/init.h" // TODO: refactor this #endif // convert a timeval struct to a double #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) -#if defined(PSUTIL_OPENBSD) || defined (PSUTIL_NETBSD) - #define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) +#if defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +#define PSUTIL_KPT2DOUBLE(t) (t##_sec + t##_usec / 1000000.0) #endif @@ -62,7 +62,7 @@ kinfo_getfile(pid_t pid, int *cnt) { // Calculate number of entries and check for overflow if (len / sizeof(struct kinfo_file) > INT_MAX) { - psutil_debug("exceeded INT_MAX") + psutil_debug("exceeded INT_MAX"); free(kf); errno = EOVERFLOW; return NULL; @@ -98,12 +98,12 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { PyObject *py_ppid = NULL; PyObject *py_retlist = NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; - // Process + // Process #ifdef PSUTIL_FREEBSD snprintf(name_buf, sizeof(name_buf), "%s", kp.ki_comm); #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) @@ -126,20 +126,20 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { memstack = (long)kp.ki_ssize * pagesize; #else rss = (long)kp.p_vm_rssize * pagesize; - #ifdef PSUTIL_OPENBSD - // VMS, this is how ps determines it on OpenBSD: - // https://github.com/openbsd/src/blob/ - // 588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L505 - vms = (long)(kp.p_vm_dsize + kp.p_vm_ssize + kp.p_vm_tsize) * pagesize; - #elif PSUTIL_NETBSD - // VMS, this is how top determines it on NetBSD: - // https://github.com/IIJ-NetBSD/netbsd-src/blob/master/external/ - // bsd/top/dist/machine/m_netbsd.c - vms = (long)kp.p_vm_msize * pagesize; - #endif - memtext = (long)kp.p_vm_tsize * pagesize; - memdata = (long)kp.p_vm_dsize * pagesize; - memstack = (long)kp.p_vm_ssize * pagesize; +#ifdef PSUTIL_OPENBSD + // VMS, this is how ps determines it on OpenBSD: + // https://github.com/openbsd/src/blob/ + // 588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L505 + vms = (long)(kp.p_vm_dsize + kp.p_vm_ssize + kp.p_vm_tsize) * pagesize; +#elif PSUTIL_NETBSD + // VMS, this is how top determines it on NetBSD: + // https://github.com/IIJ-NetBSD/netbsd-src/blob/master/external/ + // bsd/top/dist/machine/m_netbsd.c + vms = (long)kp.p_vm_msize * pagesize; +#endif + memtext = (long)kp.p_vm_tsize * pagesize; + memdata = (long)kp.p_vm_dsize * pagesize; + memstack = (long)kp.p_vm_ssize * pagesize; #endif #ifdef PSUTIL_FREEBSD @@ -166,7 +166,7 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { #else py_ppid = Py_BuildValue("i", -1); #endif - if (! py_ppid) + if (!py_ppid) return NULL; // Return a single big tuple with all process info. @@ -177,58 +177,58 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { "(OillllllidllllddddlllllbO)", #endif #ifdef PSUTIL_FREEBSD - py_ppid, // (pid_t) ppid - (int)kp.ki_stat, // (int) status + py_ppid, // (pid_t) ppid + (int)kp.ki_stat, // (int) status // UIDs - (long)kp.ki_ruid, // (long) real uid - (long)kp.ki_uid, // (long) effective uid - (long)kp.ki_svuid, // (long) saved uid + (long)kp.ki_ruid, // (long) real uid + (long)kp.ki_uid, // (long) effective uid + (long)kp.ki_svuid, // (long) saved uid // GIDs - (long)kp.ki_rgid, // (long) real gid - (long)kp.ki_groups[0], // (long) effective gid - (long)kp.ki_svuid, // (long) saved gid + (long)kp.ki_rgid, // (long) real gid + (long)kp.ki_groups[0], // (long) effective gid + (long)kp.ki_svuid, // (long) saved gid // - kp.ki_tdev, // (int or long long) tty nr - PSUTIL_TV2DOUBLE(kp.ki_start), // (double) create time + kp.ki_tdev, // (int or long long) tty nr + PSUTIL_TV2DOUBLE(kp.ki_start), // (double) create time // ctx switches - kp.ki_rusage.ru_nvcsw, // (long) ctx switches (voluntary) - kp.ki_rusage.ru_nivcsw, // (long) ctx switches (unvoluntary) + kp.ki_rusage.ru_nvcsw, // (long) ctx switches (voluntary) + kp.ki_rusage.ru_nivcsw, // (long) ctx switches (unvoluntary) // IO count - kp.ki_rusage.ru_inblock, // (long) read io count - kp.ki_rusage.ru_oublock, // (long) write io count + kp.ki_rusage.ru_inblock, // (long) read io count + kp.ki_rusage.ru_oublock, // (long) write io count // CPU times: convert from micro seconds to seconds. - PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_utime), // (double) user time - PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_stime), // (double) sys time + PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_utime), // (double) user time + PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_stime), // (double) sys time PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_utime), // (double) children utime PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_stime), // (double) children stime // memory - rss, // (long) rss - vms, // (long) vms - memtext, // (long) mem text - memdata, // (long) mem data - memstack, // (long) mem stack + rss, // (long) rss + vms, // (long) vms + memtext, // (long) mem text + memdata, // (long) mem data + memstack, // (long) mem stack // others - oncpu, // (int) the CPU we are on + oncpu, // (int) the CPU we are on #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_ppid, // (pid_t) ppid - (int)kp.p_stat, // (int) status + py_ppid, // (pid_t) ppid + (int)kp.p_stat, // (int) status // UIDs - (long)kp.p_ruid, // (long) real uid - (long)kp.p_uid, // (long) effective uid - (long)kp.p_svuid, // (long) saved uid + (long)kp.p_ruid, // (long) real uid + (long)kp.p_uid, // (long) effective uid + (long)kp.p_svuid, // (long) saved uid // GIDs - (long)kp.p_rgid, // (long) real gid - (long)kp.p_groups[0], // (long) effective gid - (long)kp.p_svuid, // (long) saved gid + (long)kp.p_rgid, // (long) real gid + (long)kp.p_groups[0], // (long) effective gid + (long)kp.p_svuid, // (long) saved gid // - kp.p_tdev, // (int) tty nr + kp.p_tdev, // (int) tty nr PSUTIL_KPT2DOUBLE(kp.p_ustart), // (double) create time // ctx switches - kp.p_uru_nvcsw, // (long) ctx switches (voluntary) - kp.p_uru_nivcsw, // (long) ctx switches (unvoluntary) + kp.p_uru_nvcsw, // (long) ctx switches (voluntary) + kp.p_uru_nivcsw, // (long) ctx switches (unvoluntary) // IO count - kp.p_uru_inblock, // (long) read io count - kp.p_uru_oublock, // (long) write io count + kp.p_uru_inblock, // (long) read io count + kp.p_uru_oublock, // (long) write io count // CPU times: convert from micro seconds to seconds. PSUTIL_KPT2DOUBLE(kp.p_uutime), // (double) user time PSUTIL_KPT2DOUBLE(kp.p_ustime), // (double) sys time @@ -237,15 +237,15 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch utime kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch stime // memory - rss, // (long) rss - vms, // (long) vms - memtext, // (long) mem text - memdata, // (long) mem data - memstack, // (long) mem stack + rss, // (long) rss + vms, // (long) vms + memtext, // (long) mem text + memdata, // (long) mem data + memstack, // (long) mem stack // others - oncpu, // (int) the CPU we are on + oncpu, // (int) the CPU we are on #endif - py_name // (pystr) name + py_name // (pystr) name ); Py_DECREF(py_name); @@ -264,7 +264,7 @@ psutil_proc_name(PyObject *self, PyObject *args) { #endif char str[1000]; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -283,7 +283,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { int i, cnt = -1; long pid; char *s, **envs, errbuf[_POSIX2_LINE_MAX]; - PyObject *py_value=NULL, *py_retdict=NULL; + PyObject *py_value = NULL, *py_retdict = NULL; kvm_t *kd; #ifdef PSUTIL_NETBSD struct kinfo_proc2 *p; @@ -367,9 +367,12 @@ psutil_proc_environ(PyObject *self, PyObject *args) { // failure for certain processes ( e.g. try // "sudo procstat -e ".) // Map the error condition to 'AccessDenied'. - sprintf(errbuf, - "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM", - pid, p->ki_uid); + sprintf( + errbuf, + "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM", + pid, + p->ki_uid + ); AccessDenied(errbuf); break; #endif @@ -406,7 +409,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } - /* +/* * Return files opened by process as a list of (path, fd) tuples. * TODO: this is broken as it may report empty paths. 'procstat' * utility has the same problem see: @@ -433,7 +436,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_kinfo_proc(pid, &kipp) == -1) goto error; @@ -460,8 +463,8 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { kif = &freep[i]; #ifdef PSUTIL_FREEBSD - regular = (kif->kf_type == KF_TYPE_VNODE) && \ - (kif->kf_vnode_type == KF_VTYPE_VREG); + regular = (kif->kf_type == KF_TYPE_VNODE) + && (kif->kf_vnode_type == KF_VTYPE_VREG); fd = kif->kf_fd; path = kif->kf_path; #elif PSUTIL_OPENBSD @@ -477,7 +480,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { #endif if (regular == 1) { py_path = PyUnicode_DecodeFSDefault(path); - if (! py_path) + if (!py_path) goto error; py_tuple = Py_BuildValue("(Oi)", py_path, fd); if (py_tuple == NULL) diff --git a/psutil/arch/bsd/sys.c b/psutil/arch/bsd/sys.c index 02c04a70c5..287e1cb0f0 100644 --- a/psutil/arch/bsd/sys.c +++ b/psutil/arch/bsd/sys.c @@ -9,9 +9,9 @@ #include #include // OS version #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) - #include +#include #elif defined(PSUTIL_OPENBSD) - #include +#include #endif #include "../../arch/all/init.h" @@ -22,7 +22,7 @@ PyObject * psutil_boot_time(PyObject *self, PyObject *args) { // fetch sysctl "kern.boottime" - static int request[2] = { CTL_KERN, KERN_BOOTTIME }; + static int request[2] = {CTL_KERN, KERN_BOOTTIME}; struct timeval boottime; if (psutil_sysctl(request, 2, &boottime, sizeof(boottime)) != 0) diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 9e534dd02d..2cd96be673 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -43,7 +43,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } // allocate buffer dynamically based on actual CPU count - long (*cpu_time)[CPUSTATES] = malloc(ncpu * sizeof(*cpu_time)); + long(*cpu_time)[CPUSTATES] = malloc(ncpu * sizeof(*cpu_time)); if (!cpu_time) { PyErr_NoMemory(); goto error; @@ -63,7 +63,8 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { (double)cpu_time[i][CP_NICE] / CLOCKS_PER_SEC, (double)cpu_time[i][CP_SYS] / CLOCKS_PER_SEC, (double)cpu_time[i][CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_INTR] / CLOCKS_PER_SEC); + (double)cpu_time[i][CP_INTR] / CLOCKS_PER_SEC + ); if (!py_cputime) { free(cpu_time); goto error; @@ -93,7 +94,9 @@ psutil_cpu_topology(PyObject *self, PyObject *args) { PyObject *py_str; if (psutil_sysctlbyname_malloc( - "kern.sched.topology_spec", &topology, &size) != 0) + "kern.sched.topology_spec", &topology, &size + ) + != 0) { psutil_debug("ignore sysctlbyname('kern.sched.topology_spec') error"); Py_RETURN_NONE; @@ -149,7 +152,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { char available_freq_levels[1000]; size_t size; - if (! PyArg_ParseTuple(args, "i", &core)) + if (!PyArg_ParseTuple(args, "i", &core)) return NULL; // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index 16f289f3c4..f5cf4bbe49 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -12,8 +12,9 @@ // convert a bintime struct to milliseconds -#define PSUTIL_BT2MSEC(bt) (bt.sec * 1000 + (((uint64_t) 1000000000 * (uint32_t) \ - (bt.frac >> 32) ) >> 32 ) / 1000000) +#define PSUTIL_BT2MSEC(bt) \ + (bt.sec * 1000 \ + + (((uint64_t)1000000000 * (uint32_t)(bt.frac >> 32)) >> 32) / 1000000) PyObject * @@ -27,8 +28,9 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (py_retdict == NULL) return NULL; if (devstat_checkversion(NULL) < 0) { - PyErr_Format(PyExc_RuntimeError, - "devstat_checkversion() syscall failed"); + PyErr_Format( + PyExc_RuntimeError, "devstat_checkversion() syscall failed" + ); goto error; } @@ -49,20 +51,26 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { struct devstat current; char disk_name[128]; current = stats.dinfo->devices[i]; - snprintf(disk_name, sizeof(disk_name), "%s%d", - current.device_name, - current.unit_number); + snprintf( + disk_name, + sizeof(disk_name), + "%s%d", + current.device_name, + current.unit_number + ); py_disk_info = Py_BuildValue( "(KKKKLLL)", - current.operations[DEVSTAT_READ], // no reads + current.operations[DEVSTAT_READ], // no reads current.operations[DEVSTAT_WRITE], // no writes - current.bytes[DEVSTAT_READ], // bytes read - current.bytes[DEVSTAT_WRITE], // bytes written - (long long) PSUTIL_BT2MSEC(current.duration[DEVSTAT_READ]), // r time - (long long) PSUTIL_BT2MSEC(current.duration[DEVSTAT_WRITE]), // w time - (long long) PSUTIL_BT2MSEC(current.busy_time) // busy time - ); // finished transactions + current.bytes[DEVSTAT_READ], // bytes read + current.bytes[DEVSTAT_WRITE], // bytes written + (long long)PSUTIL_BT2MSEC(current.duration[DEVSTAT_READ] + ), // r time + (long long)PSUTIL_BT2MSEC(current.duration[DEVSTAT_WRITE] + ), // w time + (long long)PSUTIL_BT2MSEC(current.busy_time) // busy time + ); // finished transactions if (!py_disk_info) goto error; if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index dac470b63d..85013205d2 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -17,7 +17,7 @@ #include "../../arch/all/init.h" #ifndef _PATH_DEVNULL - #define _PATH_DEVNULL "/dev/null" +#define _PATH_DEVNULL "/dev/null" #endif @@ -40,17 +40,21 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { if (psutil_sysctlbyname("hw.physmem", &total, size_ul) != 0) return NULL; - if (psutil_sysctlbyname("vm.stats.vm.v_active_count", &active, size_ui) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_active_count", &active, size_ui) + != 0) return NULL; - if (psutil_sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, size_ui) != 0) + if (psutil_sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, size_ui) + != 0) return NULL; if (psutil_sysctlbyname("vm.stats.vm.v_wire_count", &wired, size_ui) != 0) return NULL; // Optional; ignore error if not available - if (psutil_sysctlbyname("vm.stats.vm.v_cache_count", &cached, size_ui) != 0) { + if (psutil_sysctlbyname("vm.stats.vm.v_cache_count", &cached, size_ui) + != 0) + { PyErr_Clear(); cached = 0; } @@ -62,18 +66,21 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return NULL; if (psutil_sysctl(mib, 2, &vm, size_vm) != 0) { - return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(CTL_VM | VM_METER)"); + return psutil_PyErr_SetFromOSErrnoWithSyscall( + "sysctl(CTL_VM | VM_METER)" + ); } - return Py_BuildValue("KKKKKKKK", - (unsigned long long) total, - (unsigned long long) free * pagesize, - (unsigned long long) active * pagesize, - (unsigned long long) inactive * pagesize, - (unsigned long long) wired * pagesize, - (unsigned long long) cached * pagesize, - (unsigned long long) buffers, - (unsigned long long) (vm.t_vmshr + vm.t_rmshr) * pagesize // shared + return Py_BuildValue( + "KKKKKKKK", + (unsigned long long)total, + (unsigned long long)free * pagesize, + (unsigned long long)active * pagesize, + (unsigned long long)inactive * pagesize, + (unsigned long long)wired * pagesize, + (unsigned long long)cached * pagesize, + (unsigned long long)buffers, + (unsigned long long)(vm.t_vmshr + vm.t_rmshr) * pagesize // shared ); } @@ -95,8 +102,9 @@ psutil_swap_mem(PyObject *self, PyObject *args) { if (kvm_getswapinfo(kd, kvmsw, 1, 0) < 0) { kvm_close(kd); - PyErr_SetString(PyExc_RuntimeError, - "kvm_getswapinfo() syscall failed"); + PyErr_SetString( + PyExc_RuntimeError, "kvm_getswapinfo() syscall failed" + ); return NULL; } @@ -115,8 +123,8 @@ psutil_swap_mem(PyObject *self, PyObject *args) { "(KKKII)", (unsigned long long)kvmsw[0].ksw_total * pagesize, // total (unsigned long long)kvmsw[0].ksw_used * pagesize, // used - (unsigned long long)kvmsw[0].ksw_total * pagesize - // free - kvmsw[0].ksw_used * pagesize, + (unsigned long long)kvmsw[0].ksw_total * pagesize - // free + kvmsw[0].ksw_used * pagesize, swapin + swapout, // swap in nodein + nodeout // swap out ); diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index e475601ab5..1dde788449 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -24,7 +24,7 @@ #include "../../arch/all/init.h" -#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) +#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) // ============================================================================ @@ -61,7 +61,8 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // remove spaces from string -static void psutil_remove_spaces(char *str) { +static void +psutil_remove_spaces(char *str) { char *p1 = str; char *p2 = str; do @@ -143,7 +144,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { int ret; size_t size; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; mib[0] = CTL_KERN; @@ -183,7 +184,7 @@ psutil_proc_num_threads(PyObject *self, PyObject *args) { // Return number of threads used by process as a Python integer. pid_t pid; struct kinfo_proc kp; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -209,7 +210,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; // we need to re-query for thread information, so don't use *kipp @@ -229,10 +230,12 @@ psutil_proc_threads(PyObject *self, PyObject *args) { for (i = 0; i < size / sizeof(*kip); i++) { kipp = &kip[i]; - py_tuple = Py_BuildValue("Idd", - kipp->ki_tid, - PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_utime), - PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_stime)); + py_tuple = Py_BuildValue( + "Idd", + kipp->ki_tid, + PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_utime), + PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_stime) + ); if (py_tuple == NULL) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -260,7 +263,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { int i, cnt; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_kinfo_proc(pid, &kipp) == -1) goto error; @@ -307,7 +310,7 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { struct kinfo_file *freep; struct kinfo_proc kipp; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kipp) == -1) return NULL; @@ -344,7 +347,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_kinfo_proc(pid, &kp) == -1) goto error; @@ -360,15 +363,30 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { kve = &freep[i]; addr[0] = '\0'; perms[0] = '\0'; - sprintf(addr, "%#*jx-%#*jx", ptrwidth, (uintmax_t)kve->kve_start, - ptrwidth, (uintmax_t)kve->kve_end); + sprintf( + addr, + "%#*jx-%#*jx", + ptrwidth, + (uintmax_t)kve->kve_start, + ptrwidth, + (uintmax_t)kve->kve_end + ); psutil_remove_spaces(addr); - strlcat(perms, kve->kve_protection & KVME_PROT_READ ? "r" : "-", - sizeof(perms)); - strlcat(perms, kve->kve_protection & KVME_PROT_WRITE ? "w" : "-", - sizeof(perms)); - strlcat(perms, kve->kve_protection & KVME_PROT_EXEC ? "x" : "-", - sizeof(perms)); + strlcat( + perms, + kve->kve_protection & KVME_PROT_READ ? "r" : "-", + sizeof(perms) + ); + strlcat( + perms, + kve->kve_protection & KVME_PROT_WRITE ? "w" : "-", + sizeof(perms) + ); + strlcat( + perms, + kve->kve_protection & KVME_PROT_EXEC ? "x" : "-", + sizeof(perms) + ); if (strlen(kve->kve_path) == 0) { switch (kve->kve_type) { @@ -411,16 +429,18 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { } py_path = PyUnicode_DecodeFSDefault(path); - if (! py_path) + if (!py_path) goto error; - py_tuple = Py_BuildValue("ssOiiii", - addr, // "start-end" address - perms, // "rwx" permissions - py_path, // path - kve->kve_resident, // rss + py_tuple = Py_BuildValue( + "ssOiiii", + addr, // "start-end" address + perms, // "rwx" permissions + py_path, // path + kve->kve_resident, // rss kve->kve_private_resident, // private - kve->kve_ref_count, // ref count - kve->kve_shadow_count); // shadow count + kve->kve_ref_count, // ref count + kve->kve_shadow_count // shadow count + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -441,8 +461,8 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { } -PyObject* -psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args) { +PyObject * +psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { // Get process CPU affinity. // Reference: // http://sources.freebsd.org/RELENG_9/src/usr.bin/cpuset/cpuset.c @@ -450,13 +470,14 @@ psutil_proc_cpu_affinity_get(PyObject* self, PyObject* args) { int ret; int i; cpuset_t mask; - PyObject* py_retlist; - PyObject* py_cpu_num; + PyObject *py_retlist; + PyObject *py_cpu_num; if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; - ret = cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, - sizeof(mask), &mask); + ret = cpuset_getaffinity( + CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, sizeof(mask), &mask + ); if (ret != 0) return PyErr_SetFromErrno(PyExc_OSError); @@ -515,8 +536,9 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { } // set affinity - ret = cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, - sizeof(cpu_set), &cpu_set); + ret = cpuset_setaffinity( + CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, sizeof(cpu_set), &cpu_set + ); if (ret != 0) { PyErr_SetFromErrno(PyExc_OSError); goto error; @@ -542,7 +564,7 @@ psutil_proc_getrlimit(PyObject *self, PyObject *args) { int name[5]; struct rlimit rlp; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &resource)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &resource)) return NULL; name[0] = CTL_KERN; @@ -555,13 +577,11 @@ psutil_proc_getrlimit(PyObject *self, PyObject *args) { return NULL; #if defined(HAVE_LONG_LONG) - return Py_BuildValue("LL", - (PY_LONG_LONG) rlp.rlim_cur, - (PY_LONG_LONG) rlp.rlim_max); + return Py_BuildValue( + "LL", (PY_LONG_LONG)rlp.rlim_cur, (PY_LONG_LONG)rlp.rlim_max + ); #else - return Py_BuildValue("ll", - (long) rlp.rlim_cur, - (long) rlp.rlim_max); + return Py_BuildValue("ll", (long)rlp.rlim_cur, (long)rlp.rlim_max); #endif } @@ -580,8 +600,9 @@ psutil_proc_setrlimit(PyObject *self, PyObject *args) { PyObject *py_soft = NULL; PyObject *py_hard = NULL; - if (! PyArg_ParseTuple( - args, _Py_PARSE_PID "iOO", &pid, &resource, &py_soft, &py_hard)) + if (!PyArg_ParseTuple( + args, _Py_PARSE_PID "iOO", &pid, &resource, &py_soft, &py_hard + )) return NULL; name[0] = CTL_KERN; @@ -592,17 +613,17 @@ psutil_proc_setrlimit(PyObject *self, PyObject *args) { #if defined(HAVE_LONG_LONG) new.rlim_cur = PyLong_AsLongLong(py_soft); - if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) + if (new.rlim_cur == (rlim_t)-1 && PyErr_Occurred()) return NULL; new.rlim_max = PyLong_AsLongLong(py_hard); - if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) + if (new.rlim_max == (rlim_t)-1 && PyErr_Occurred()) return NULL; #else new.rlim_cur = PyLong_AsLong(py_soft); - if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred()) + if (new.rlim_cur == (rlim_t)-1 && PyErr_Occurred()) return NULL; new.rlim_max = PyLong_AsLong(py_hard); - if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred()) + if (new.rlim_max == (rlim_t)-1 && PyErr_Occurred()) return NULL; #endif newp = &new; diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 47e46ea08e..7b63f5c73c 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -10,13 +10,13 @@ #include #include #include -#include // for struct xsocket +#include // for struct xsocket #include #include -#include // for xinpcb struct +#include // for xinpcb struct #include -#include // for struct xtcpcb -#include // for inet_ntop() +#include // for struct xtcpcb +#include // for inet_ntop() #include #include "../../arch/all/init.h" @@ -90,12 +90,19 @@ psutil_sockaddr_addrlen(int family) { static int -psutil_sockaddr_matches(int family, int port, void *pcb_addr, - struct sockaddr_storage *ss) { +psutil_sockaddr_matches( + int family, int port, void *pcb_addr, struct sockaddr_storage *ss +) { if (psutil_sockaddr_port(family, ss) != port) return (0); - return (memcmp(psutil_sockaddr_addr(family, ss), pcb_addr, - psutil_sockaddr_addrlen(family)) == 0); + return ( + memcmp( + psutil_sockaddr_addr(family, ss), + pcb_addr, + psutil_sockaddr_addrlen(family) + ) + == 0 + ); } @@ -115,9 +122,9 @@ psutil_search_tcplist(char *buf, struct kinfo_file *kif) { oxig = xig = (struct xinpgen *)buf; for (xig = (struct xinpgen *)((char *)xig + xig->xig_len); - xig->xig_len > sizeof(struct xinpgen); - xig = (struct xinpgen *)((char *)xig + xig->xig_len)) { - + xig->xig_len > sizeof(struct xinpgen); + xig = (struct xinpgen *)((char *)xig + xig->xig_len)) + { #if __FreeBSD_version >= 1200026 tp = (struct xtcpcb *)xig; inp = &tp->xt_inp; @@ -128,43 +135,60 @@ psutil_search_tcplist(char *buf, struct kinfo_file *kif) { so = &((struct xtcpcb *)xig)->xt_socket; #endif - if (so->so_type != kif->kf_sock_type || - so->xso_family != kif->kf_sock_domain || - so->xso_protocol != kif->kf_sock_protocol) + if (so->so_type != kif->kf_sock_type + || so->xso_family != kif->kf_sock_domain + || so->xso_protocol != kif->kf_sock_protocol) continue; if (kif->kf_sock_domain == AF_INET) { if (!psutil_sockaddr_matches( - AF_INET, inp->inp_lport, &inp->inp_laddr, + AF_INET, + inp->inp_lport, + &inp->inp_laddr, #if __FreeBSD_version < 1200031 - &kif->kf_sa_local)) + &kif->kf_sa_local + )) #else - &kif->kf_un.kf_sock.kf_sa_local)) + &kif->kf_un.kf_sock.kf_sa_local + )) #endif continue; if (!psutil_sockaddr_matches( - AF_INET, inp->inp_fport, &inp->inp_faddr, + AF_INET, + inp->inp_fport, + &inp->inp_faddr, #if __FreeBSD_version < 1200031 - &kif->kf_sa_peer)) + &kif->kf_sa_peer + )) #else - &kif->kf_un.kf_sock.kf_sa_peer)) + &kif->kf_un.kf_sock.kf_sa_peer + )) #endif continue; - } else { + } + else { if (!psutil_sockaddr_matches( - AF_INET6, inp->inp_lport, &inp->in6p_laddr, + AF_INET6, + inp->inp_lport, + &inp->in6p_laddr, #if __FreeBSD_version < 1200031 - &kif->kf_sa_local)) + &kif->kf_sa_local + )) #else - &kif->kf_un.kf_sock.kf_sa_local)) + &kif->kf_un.kf_sock.kf_sa_local + )) #endif continue; if (!psutil_sockaddr_matches( - AF_INET6, inp->inp_fport, &inp->in6p_faddr, + AF_INET6, + inp->inp_fport, + &inp->in6p_faddr, #if __FreeBSD_version < 1200031 - &kif->kf_sa_peer)) + &kif->kf_sa_peer + )) #else - &kif->kf_un.kf_sock.kf_sa_peer)) + &kif->kf_un.kf_sock.kf_sa_peer + )) #endif continue; } @@ -175,7 +199,6 @@ psutil_search_tcplist(char *buf, struct kinfo_file *kif) { } - PyObject * psutil_proc_net_connections(PyObject *self, PyObject *args) { // Return connections opened by process. @@ -202,8 +225,9 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, - &py_af_filter, &py_type_filter)) + if (!PyArg_ParseTuple( + args, _Py_PARSE_PID "OO", &pid, &py_af_filter, &py_type_filter + )) { goto error; } @@ -248,8 +272,9 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (inseq == 0) continue; // IPv4 / IPv6 socket - if ((kif->kf_sock_domain == AF_INET) || - (kif->kf_sock_domain == AF_INET6)) { + if ((kif->kf_sock_domain == AF_INET) + || (kif->kf_sock_domain == AF_INET6)) + { // fill status state = PSUTIL_CONN_NONE; if (kif->kf_sock_type == SOCK_STREAM) { @@ -261,35 +286,49 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { // build addr and port inet_ntop( kif->kf_sock_domain, - psutil_sockaddr_addr(kif->kf_sock_domain, + psutil_sockaddr_addr( + kif->kf_sock_domain, #if __FreeBSD_version < 1200031 - &kif->kf_sa_local), + &kif->kf_sa_local + ), #else - &kif->kf_un.kf_sock.kf_sa_local), + &kif->kf_un.kf_sock.kf_sa_local + ), #endif lip, - sizeof(lip)); + sizeof(lip) + ); inet_ntop( kif->kf_sock_domain, - psutil_sockaddr_addr(kif->kf_sock_domain, + psutil_sockaddr_addr( + kif->kf_sock_domain, #if __FreeBSD_version < 1200031 - &kif->kf_sa_peer), + &kif->kf_sa_peer + ), #else - &kif->kf_un.kf_sock.kf_sa_peer), + &kif->kf_un.kf_sock.kf_sa_peer + ), #endif rip, - sizeof(rip)); - lport = htons(psutil_sockaddr_port(kif->kf_sock_domain, + sizeof(rip) + ); + lport = htons(psutil_sockaddr_port( + kif->kf_sock_domain, #if __FreeBSD_version < 1200031 - &kif->kf_sa_local)); + &kif->kf_sa_local + )); #else - &kif->kf_un.kf_sock.kf_sa_local)); + &kif->kf_un.kf_sock.kf_sa_local + )); #endif - rport = htons(psutil_sockaddr_port(kif->kf_sock_domain, + rport = htons(psutil_sockaddr_port( + kif->kf_sock_domain, #if __FreeBSD_version < 1200031 - &kif->kf_sa_peer)); + &kif->kf_sa_peer + )); #else - &kif->kf_un.kf_sock.kf_sa_peer)); + &kif->kf_un.kf_sock.kf_sa_peer + )); #endif // construct python tuple/list @@ -328,12 +367,16 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { sun = (struct sockaddr_un *)&kif->kf_un.kf_sock.kf_sa_local; #endif snprintf( - path, sizeof(path), "%.*s", - (int)(sun->sun_len - (sizeof(*sun) - sizeof(sun->sun_path))), - sun->sun_path); + path, + sizeof(path), + "%.*s", + (int)(sun->sun_len - (sizeof(*sun) - sizeof(sun->sun_path)) + ), + sun->sun_path + ); py_laddr = PyUnicode_DecodeFSDefault(path); - if (! py_laddr) + if (!py_laddr) goto error; py_tuple = Py_BuildValue( diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index af0326c5a5..089ba93a0b 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -56,7 +56,7 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { char sensor[26]; size_t size = sizeof(current); - if (! PyArg_ParseTuple(args, "i", &core)) + if (!PyArg_ParseTuple(args, "i", &core)) return NULL; sprintf(sensor, "dev.cpu.%d.temperature", core); if (psutil_sysctlbyname(sensor, ¤t, size) != 0) diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index a56ed06a8a..6c02f0e08e 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -13,15 +13,15 @@ #include #include #include -#include // for struct xsocket +#include // for struct xsocket #include #include #include -#include // for xinpcb struct +#include // for xinpcb struct #include #include -#include // for struct xtcpcb -#include // for inet_ntop() +#include // for struct xtcpcb +#include // for inet_ntop() #include "../../arch/all/init.h" @@ -54,7 +54,9 @@ psutil_populate_xfiles(struct xfile **psutil_xfiles, int *psutil_nxfiles) { static struct xfile * -psutil_get_file_from_sock(kvaddr_t sock, struct xfile *psutil_xfiles, int psutil_nxfiles) { +psutil_get_file_from_sock( + kvaddr_t sock, struct xfile *psutil_xfiles, int psutil_nxfiles +) { struct xfile *xf; int n; @@ -70,9 +72,13 @@ psutil_get_file_from_sock(kvaddr_t sock, struct xfile *psutil_xfiles, int psutil // https://github.com/freebsd/freebsd/blob/master/usr.bin/sockstat/sockstat.c static int psutil_gather_inet( - int proto, int include_v4, int include_v6, PyObject *py_retlist, - struct xfile *psutil_xfiles, int psutil_nxfiles) -{ + int proto, + int include_v4, + int include_v6, + PyObject *py_retlist, + struct xfile *psutil_xfiles, + int psutil_nxfiles +) { struct xinpgen *xig, *exig; struct xinpcb *xip; struct xtcpcb *xtp; @@ -140,8 +146,9 @@ psutil_gather_inet( case IPPROTO_TCP: xtp = (struct xtcpcb *)xig; if (xtp->xt_len != sizeof *xtp) { - PyErr_Format(PyExc_RuntimeError, - "struct xtcpcb size mismatch"); + PyErr_Format( + PyExc_RuntimeError, "struct xtcpcb size mismatch" + ); goto error; } inp = &xtp->xt_inp; @@ -156,8 +163,9 @@ psutil_gather_inet( case IPPROTO_UDP: xip = (struct xinpcb *)xig; if (xip->xi_len != sizeof *xip) { - PyErr_Format(PyExc_RuntimeError, - "struct xinpcb size mismatch"); + PyErr_Format( + PyExc_RuntimeError, "struct xinpcb size mismatch" + ); goto error; } #if __FreeBSD_version >= 1200026 @@ -181,7 +189,9 @@ psutil_gather_inet( char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; - xf = psutil_get_file_from_sock(so->xso_so, psutil_xfiles, psutil_nxfiles); + xf = psutil_get_file_from_sock( + so->xso_so, psutil_xfiles, psutil_nxfiles + ); if (xf == NULL) continue; lport = ntohs(inp->inp_lport); @@ -210,13 +220,13 @@ psutil_gather_inet( goto error; py_tuple = Py_BuildValue( "iiiNNi" _Py_PARSE_PID, - xf->xf_fd, // fd - family, // family - type, // type + xf->xf_fd, // fd + family, // family + type, // type py_laddr, // laddr py_raddr, // raddr - status, // status - xf->xf_pid // pid + status, // status + xf->xf_pid // pid ); if (!py_tuple) goto error; @@ -238,8 +248,12 @@ psutil_gather_inet( static int -psutil_gather_unix(int proto, PyObject *py_retlist, - struct xfile *psutil_xfiles, int psutil_nxfiles) { +psutil_gather_unix( + int proto, + PyObject *py_retlist, + struct xfile *psutil_xfiles, + int psutil_nxfiles +) { struct xunpgen *xug, *exug; struct xunpcb *xup; const char *varname = NULL; @@ -286,8 +300,7 @@ psutil_gather_unix(int proto, PyObject *py_retlist, bufsize *= 2; } xug = (struct xunpgen *)buf; - exug = (struct xunpgen *)(void *) - ((char *)buf + len - sizeof *exug); + exug = (struct xunpgen *)(void *)((char *)buf + len - sizeof *exug); if (xug->xug_len != sizeof *xug || exug->xug_len != sizeof *exug) { PyErr_Format(PyExc_RuntimeError, "struct xinpgen size mismatch"); goto error; @@ -304,26 +317,34 @@ psutil_gather_unix(int proto, PyObject *py_retlist, if (xup->xu_len != sizeof *xup) goto error; - xf = psutil_get_file_from_sock(xup->xu_socket.xso_so, psutil_xfiles, psutil_nxfiles); + xf = psutil_get_file_from_sock( + xup->xu_socket.xso_so, psutil_xfiles, psutil_nxfiles + ); if (xf == NULL) continue; sun = (struct sockaddr_un *)&xup->xu_addr; - snprintf(path, sizeof(path), "%.*s", - (int)(sun->sun_len - (sizeof(*sun) - sizeof(sun->sun_path))), - sun->sun_path); + snprintf( + path, + sizeof(path), + "%.*s", + (int)(sun->sun_len - (sizeof(*sun) - sizeof(sun->sun_path))), + sun->sun_path + ); py_lpath = PyUnicode_DecodeFSDefault(path); - if (! py_lpath) + if (!py_lpath) goto error; - py_tuple = Py_BuildValue("(iiiOsii)", - xf->xf_fd, // fd - AF_UNIX, // family - proto, // type - py_lpath, // lpath - "", // rath + py_tuple = Py_BuildValue( + "(iiiOsii)", + xf->xf_fd, // fd + AF_UNIX, // family + proto, // type + py_lpath, // lpath + "", // rath PSUTIL_CONN_NONE, // status - xf->xf_pid); // pid + xf->xf_pid // pid + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -357,9 +378,10 @@ psutil_int_in_seq(int value, PyObject *py_seq) { } -PyObject* -psutil_net_connections(PyObject* self, PyObject* args) { - int include_v4, include_v6, include_unix, include_tcp, include_udp, psutil_nxfiles; +PyObject * +psutil_net_connections(PyObject *self, PyObject *args) { + int include_v4, include_v6, include_unix, include_tcp, include_udp, + psutil_nxfiles; struct xfile *psutil_xfiles; PyObject *py_af_filter = NULL; PyObject *py_type_filter = NULL; @@ -367,7 +389,7 @@ psutil_net_connections(PyObject* self, PyObject* args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "OO", &py_af_filter, &py_type_filter)) { + if (!PyArg_ParseTuple(args, "OO", &py_af_filter, &py_type_filter)) { goto error; } if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { @@ -398,8 +420,14 @@ psutil_net_connections(PyObject* self, PyObject* args) { // TCP if (include_tcp == 1) { if (psutil_gather_inet( - IPPROTO_TCP, include_v4, include_v6, py_retlist, - psutil_xfiles, psutil_nxfiles) != 0) + IPPROTO_TCP, + include_v4, + include_v6, + py_retlist, + psutil_xfiles, + psutil_nxfiles + ) + != 0) { goto error_free_psutil_xfiles; } @@ -407,17 +435,29 @@ psutil_net_connections(PyObject* self, PyObject* args) { // UDP if (include_udp == 1) { if (psutil_gather_inet( - IPPROTO_UDP, include_v4, include_v6, py_retlist, - psutil_xfiles, psutil_nxfiles) != 0) + IPPROTO_UDP, + include_v4, + include_v6, + py_retlist, + psutil_xfiles, + psutil_nxfiles + ) + != 0) { goto error_free_psutil_xfiles; } } // UNIX if (include_unix == 1) { - if (psutil_gather_unix(SOCK_STREAM, py_retlist, psutil_xfiles, psutil_nxfiles) != 0) - goto error_free_psutil_xfiles; - if (psutil_gather_unix(SOCK_DGRAM, py_retlist, psutil_xfiles, psutil_nxfiles) != 0) + if (psutil_gather_unix( + SOCK_STREAM, py_retlist, psutil_xfiles, psutil_nxfiles + ) + != 0) + goto error_free_psutil_xfiles; + if (psutil_gather_unix( + SOCK_DGRAM, py_retlist, psutil_xfiles, psutil_nxfiles + ) + != 0) goto error_free_psutil_xfiles; } diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c index fb6726af98..d55da11b33 100644 --- a/psutil/arch/linux/disk.c +++ b/psutil/arch/linux/disk.c @@ -43,17 +43,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { goto error; } py_dev = PyUnicode_DecodeFSDefault(entry->mnt_fsname); - if (! py_dev) + if (!py_dev) goto error; py_mountp = PyUnicode_DecodeFSDefault(entry->mnt_dir); - if (! py_mountp) + if (!py_mountp) goto error; - py_tuple = Py_BuildValue("(OOss)", - py_dev, // device - py_mountp, // mount point - entry->mnt_type, // fs type - entry->mnt_opts); // options - if (! py_tuple) + py_tuple = Py_BuildValue( + "(OOss)", + py_dev, // device + py_mountp, // mount point + entry->mnt_type, // fs type + entry->mnt_opts // options + ); + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; diff --git a/psutil/arch/linux/init.h b/psutil/arch/linux/init.h index 77c83c6b35..19a909125b 100644 --- a/psutil/arch/linux/init.h +++ b/psutil/arch/linux/init.h @@ -6,7 +6,7 @@ #include #include // __NR_* -#include // CPU_ALLOC +#include // CPU_ALLOC PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); PyObject *psutil_linux_sysinfo(PyObject *self, PyObject *args); @@ -14,16 +14,14 @@ PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); // Linux >= 2.6.13 #if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) - #define PSUTIL_HAS_IOPRIO - - PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); - PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); +#define PSUTIL_HAS_IOPRIO +PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); #endif // Should exist starting from CentOS 6 (year 2011). #ifdef CPU_ALLOC - #define PSUTIL_HAS_CPU_AFFINITY - - PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); - PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); +#define PSUTIL_HAS_CPU_AFFINITY +PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); #endif diff --git a/psutil/arch/linux/mem.c b/psutil/arch/linux/mem.c index cacf1e7766..8fb1d37103 100644 --- a/psutil/arch/linux/mem.c +++ b/psutil/arch/linux/mem.c @@ -21,9 +21,9 @@ psutil_linux_sysinfo(PyObject *self, PyObject *args) { "(kkkkkkI)", info.totalram, // total info.freeram, // free - info.bufferram, // buffer - info.sharedram, // shared - info.totalswap, // swap tot + info.bufferram, // buffer + info.sharedram, // shared + info.totalswap, // swap tot info.freeswap, // swap free info.mem_unit // multiplier ); diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index 2488d9ab27..8b1eadac2c 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -16,18 +16,17 @@ // see: https://github.com/giampaolo/psutil/issues/659 #ifdef PSUTIL_ETHTOOL_MISSING_TYPES - #include - typedef __u64 u64; - typedef __u32 u32; - typedef __u16 u16; - typedef __u8 u8; +#include +typedef __u64 u64; +typedef __u32 u32; +typedef __u16 u16; +typedef __u8 u8; #endif // Avoid redefinition of struct sysinfo with musl libc. #define _LINUX_SYSINFO_H #include - // * defined in linux/ethtool.h but not always available (e.g. Android) // * #ifdef check needed for old kernels, see: // https://github.com/giampaolo/psutil/issues/2164 @@ -43,19 +42,19 @@ psutil_ethtool_cmd_speed(const struct ethtool_cmd *ecmd) { // May happen on old RedHat versions, see: // https://github.com/giampaolo/psutil/issues/607 #ifndef DUPLEX_UNKNOWN - #define DUPLEX_UNKNOWN 0xff +#define DUPLEX_UNKNOWN 0xff #endif + // https://github.com/giampaolo/psutil/pull/2156 #ifndef SPEED_UNKNOWN - #define SPEED_UNKNOWN -1 +#define SPEED_UNKNOWN -1 #endif - // References: // * https://github.com/dpaleino/wicd/blob/master/wicd/backends/be-ioctl.py // * http://www.i-scream.org/libstatgrab/ -PyObject* -psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { +PyObject * +psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { char *nic_name; int sock = 0; int ret; @@ -66,7 +65,7 @@ psutil_net_if_duplex_speed(PyObject* self, PyObject* args) { struct ethtool_cmd ethcmd; PyObject *py_retlist = NULL; - if (! PyArg_ParseTuple(args, "s", &nic_name)) + if (!PyArg_ParseTuple(args, "s", &nic_name)) return NULL; sock = socket(AF_INET, SOCK_DGRAM, 0); diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index 9024c81842..f0458e6c10 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -33,14 +33,13 @@ ioprio_set(int which, int who, int ioprio) { #define IOPRIO_PRIO_DATA(mask) ((mask) & IOPRIO_PRIO_MASK) #define IOPRIO_PRIO_VALUE(class, data) (((class) << IOPRIO_CLASS_SHIFT) | data) - // Return a (ioclass, iodata) Python tuple representing process I/O // priority. PyObject * psutil_proc_ioprio_get(PyObject *self, PyObject *args) { pid_t pid; int ioprio, ioclass, iodata; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; ioprio = ioprio_get(IOPRIO_WHO_PROCESS, pid); if (ioprio == -1) @@ -60,8 +59,7 @@ psutil_proc_ioprio_set(PyObject *self, PyObject *args) { int ioprio, ioclass, iodata; int retval; - if (! PyArg_ParseTuple( - args, _Py_PARSE_PID "ii", &pid, &ioclass, &iodata)) { + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "ii", &pid, &ioclass, &iodata)) { return NULL; } ioprio = IOPRIO_PRIO_VALUE(ioclass, iodata); @@ -102,8 +100,11 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { if (errno != EINVAL) return PyErr_SetFromErrno(PyExc_OSError); if (ncpus > INT_MAX / 2) { - PyErr_SetString(PyExc_OverflowError, "could not allocate " - "a large enough CPU set"); + PyErr_SetString( + PyExc_OverflowError, + "could not allocate " + "a large enough CPU set" + ); return NULL; } ncpus = ncpus * 2; @@ -153,7 +154,8 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { if (!PySequence_Check(py_cpu_set)) { return PyErr_Format( PyExc_TypeError, - "sequence argument expected, got %R", Py_TYPE(py_cpu_set) + "sequence argument expected, got %R", + Py_TYPE(py_cpu_set) ); } diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 47d5361a40..2b2604f0fe 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -41,11 +41,11 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { cached = (uv.filepages + uv.execpages + uv.anonpages) << uv.pageshift; return Py_BuildValue( "LLLLLL", - (long long) uv.npages << uv.pageshift, // total - (long long) uv.free << uv.pageshift, // free - (long long) uv.active << uv.pageshift, // active - (long long) uv.inactive << uv.pageshift, // inactive - (long long) uv.wired << uv.pageshift, // wired + (long long)uv.npages << uv.pageshift, // total + (long long)uv.free << uv.pageshift, // free + (long long)uv.active << uv.pageshift, // active + (long long)uv.inactive << uv.pageshift, // inactive + (long long)uv.wired << uv.pageshift, // wired cached // cached ); } @@ -80,7 +80,8 @@ psutil_swap_mem(PyObject *self, PyObject *args) { for (i = 0; i < nswap; i++) { if (swdev[i].se_flags & SWF_ENABLE) { swap_total += (uint64_t)swdev[i].se_nblks * DEV_BSIZE; - swap_free += (uint64_t)(swdev[i].se_nblks - swdev[i].se_inuse) * DEV_BSIZE; + swap_free += (uint64_t)(swdev[i].se_nblks - swdev[i].se_inuse) + * DEV_BSIZE; } } free(swdev); @@ -98,8 +99,8 @@ psutil_swap_mem(PyObject *self, PyObject *args) { swap_total, (swap_total - swap_free), swap_free, - (long) uv.pgswapin * pagesize, // swap in - (long) uv.pgswapout * pagesize // swap out + (long)uv.pgswapin * pagesize, // swap in + (long)uv.pgswapout * pagesize // swap out ); error: diff --git a/psutil/arch/netbsd/pids.c b/psutil/arch/netbsd/pids.c index d08f3bd1af..ea7abb3749 100644 --- a/psutil/arch/netbsd/pids.c +++ b/psutil/arch/netbsd/pids.c @@ -29,7 +29,9 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { return -1; } - result = kvm_getproc2(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt); + result = kvm_getproc2( + kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt + ); if (result == NULL) { PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() failed"); kvm_close(kd); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 9bb27a3f95..2ce73f7d55 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -14,7 +14,7 @@ #include "../../arch/all/init.h" -#define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) +#define PSUTIL_KPT2DOUBLE(t) (t##_sec + t##_usec / 1000000.0) #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -37,7 +37,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc) { mib[4] = size; mib[5] = 1; - ret = sysctl((int*)mib, 6, proc, &size, NULL, 0); + ret = sysctl((int *)mib, 6, proc, &size, NULL, 0); if (ret == -1) { PyErr_SetFromErrno(PyExc_OSError); return -1; @@ -51,8 +51,6 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc) { } - - PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { long pid; @@ -60,11 +58,11 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { char path[MAXPATHLEN]; size_t pathlen = sizeof path; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; #ifdef KERN_PROC_CWD - int name[] = { CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; + int name[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) NoSuchProcess("sysctl -> ENOENT"); @@ -157,7 +155,7 @@ psutil_proc_num_threads(PyObject *self, PyObject *args) { long pid; struct kinfo_proc2 kp; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; @@ -178,7 +176,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; mib[0] = CTL_KERN; @@ -201,8 +199,8 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // set slot count for NetBSD KERN_LWP mib[4] = size / sizeof(size_t); - if (psutil_sysctl_malloc( - mib, __arraycount(mib), (char **)&kl, &size) != 0) { + if (psutil_sysctl_malloc(mib, __arraycount(mib), (char **)&kl, &size) != 0) + { goto error; } if (size == 0) { @@ -255,7 +253,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; mib[0] = CTL_KERN; @@ -265,7 +263,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { while (1) { if (psutil_sysctl_malloc( - mib, __arraycount(mib), (char **)&procargs, &len) != 0) { + mib, __arraycount(mib), (char **)&procargs, &len + ) + != 0) + { if (errno == EBUSY) { // Usually happens with TestProcess.test_long_cmdline. See: // https://github.com/giampaolo/psutil/issues/2250 @@ -317,7 +318,7 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { struct kinfo_file *freep; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; errno = 0; diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index ab61d38ee3..e67d2e8eaf 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -332,7 +332,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "s", &pid, &kind)) { + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "s", &pid, &kind)) { goto error; } @@ -354,37 +354,41 @@ psutil_net_connections(PyObject *self, PyObject *args) { continue; // IPv4 or IPv6 - if ((kp->kpcb->ki_family == AF_INET) || - (kp->kpcb->ki_family == AF_INET6)) { - + if ((kp->kpcb->ki_family == AF_INET) + || (kp->kpcb->ki_family == AF_INET6)) + { if (kp->kpcb->ki_family == AF_INET) { // IPv4 - struct sockaddr_in *sin_src = - (struct sockaddr_in *)&kp->kpcb->ki_src; - struct sockaddr_in *sin_dst = - (struct sockaddr_in *)&kp->kpcb->ki_dst; + struct sockaddr_in *sin_src = (struct sockaddr_in *)&kp + ->kpcb->ki_src; + struct sockaddr_in *sin_dst = (struct sockaddr_in *)&kp + ->kpcb->ki_dst; // source addr and port - inet_ntop(AF_INET, &sin_src->sin_addr, laddr, - sizeof(laddr)); + inet_ntop( + AF_INET, &sin_src->sin_addr, laddr, sizeof(laddr) + ); lport = ntohs(sin_src->sin_port); // remote addr and port - inet_ntop(AF_INET, &sin_dst->sin_addr, raddr, - sizeof(raddr)); + inet_ntop( + AF_INET, &sin_dst->sin_addr, raddr, sizeof(raddr) + ); rport = ntohs(sin_dst->sin_port); } else { // IPv6 - struct sockaddr_in6 *sin6_src = - (struct sockaddr_in6 *)&kp->kpcb->ki_src; - struct sockaddr_in6 *sin6_dst = - (struct sockaddr_in6 *)&kp->kpcb->ki_dst; + struct sockaddr_in6 *sin6_src = (struct sockaddr_in6 *)&kp + ->kpcb->ki_src; + struct sockaddr_in6 *sin6_dst = (struct sockaddr_in6 *)&kp + ->kpcb->ki_dst; // local addr and port - inet_ntop(AF_INET6, &sin6_src->sin6_addr, laddr, - sizeof(laddr)); + inet_ntop( + AF_INET6, &sin6_src->sin6_addr, laddr, sizeof(laddr) + ); lport = ntohs(sin6_src->sin6_port); // remote addr and port - inet_ntop(AF_INET6, &sin6_dst->sin6_addr, raddr, - sizeof(raddr)); + inet_ntop( + AF_INET6, &sin6_dst->sin6_addr, raddr, sizeof(raddr) + ); rport = ntohs(sin6_dst->sin6_port); } @@ -396,29 +400,29 @@ psutil_net_connections(PyObject *self, PyObject *args) { // build addr tuple py_laddr = Py_BuildValue("(si)", laddr, lport); - if (! py_laddr) + if (!py_laddr) goto error; if (rport != 0) py_raddr = Py_BuildValue("(si)", raddr, rport); else py_raddr = Py_BuildValue("()"); - if (! py_raddr) + if (!py_raddr) goto error; } else if (kp->kpcb->ki_family == AF_UNIX) { // UNIX sockets - struct sockaddr_un *sun_src = - (struct sockaddr_un *)&kp->kpcb->ki_src; - struct sockaddr_un *sun_dst = - (struct sockaddr_un *)&kp->kpcb->ki_dst; + struct sockaddr_un *sun_src = (struct sockaddr_un *)&kp->kpcb + ->ki_src; + struct sockaddr_un *sun_dst = (struct sockaddr_un *)&kp->kpcb + ->ki_dst; strcpy(laddr, sun_src->sun_path); strcpy(raddr, sun_dst->sun_path); status = PSUTIL_CONN_NONE; py_laddr = PyUnicode_DecodeFSDefault(laddr); - if (! py_laddr) + if (!py_laddr) goto error; py_raddr = PyUnicode_DecodeFSDefault(raddr); - if (! py_raddr) + if (!py_raddr) goto error; } else { @@ -434,8 +438,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_laddr, py_raddr, status, - k->kif->ki_pid); - if (! py_tuple) + k->kif->ki_pid + ); + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c index 7dcdded15d..a14116ef3a 100644 --- a/psutil/arch/openbsd/cpu.c +++ b/psutil/arch/openbsd/cpu.c @@ -46,7 +46,8 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC); + (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC + ); if (!py_cputime) goto error; if (PyList_Append(py_retlist, py_cputime)) @@ -68,7 +69,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { struct uvmexp uv; int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; - if (psutil_sysctl(uvmexp_mib, 2, &uv, sizeof(uv)) !=0) + if (psutil_sysctl(uvmexp_mib, 2, &uv, sizeof(uv)) != 0) return NULL; return Py_BuildValue( diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index 2a5a797436..9fab2779d4 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -44,19 +44,20 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { if (psutil_sysctl(vmmeter_mib, 2, &vmdata, size) != 0) return NULL; - return Py_BuildValue("KKKKKKKK", + return Py_BuildValue( + "KKKKKKKK", // Note: many programs calculate total memory as // "uvmexp.npages * pagesize" but this is incorrect and does not // match "sysctl | grep hw.physmem". - (unsigned long long) total_physmem, - (unsigned long long) uvmexp.free * pagesize, - (unsigned long long) uvmexp.active * pagesize, - (unsigned long long) uvmexp.inactive * pagesize, - (unsigned long long) uvmexp.wired * pagesize, + (unsigned long long)total_physmem, + (unsigned long long)uvmexp.free * pagesize, + (unsigned long long)uvmexp.active * pagesize, + (unsigned long long)uvmexp.inactive * pagesize, + (unsigned long long)uvmexp.wired * pagesize, // this is how "top" determines it - (unsigned long long) bcstats.numbufpages * pagesize, // cached - (unsigned long long) 0, // buffers - (unsigned long long) vmdata.t_vmshr + vmdata.t_rmshr // shared + (unsigned long long)bcstats.numbufpages * pagesize, // cached + (unsigned long long)0, // buffers + (unsigned long long)vmdata.t_vmshr + vmdata.t_rmshr // shared ); } diff --git a/psutil/arch/openbsd/pids.c b/psutil/arch/openbsd/pids.c index 0b3ed44849..48554041a5 100644 --- a/psutil/arch/openbsd/pids.c +++ b/psutil/arch/openbsd/pids.c @@ -31,7 +31,9 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { return -1; } - result = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt); + result = kvm_getprocs( + kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt + ); if (result == NULL) { PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() failed"); kvm_close(kd); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 8c97009362..9951e944db 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -15,7 +15,7 @@ #include "../../arch/all/init.h" -#define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) +#define PSUTIL_KPT2DOUBLE(t) (t##_sec + t##_usec / 1000000.0) // #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) @@ -38,7 +38,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { mib[4] = size; mib[5] = 1; - ret = sysctl((int*)mib, 6, proc, &size, NULL, 0); + ret = sysctl((int *)mib, 6, proc, &size, NULL, 0); if (ret == -1) { psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_proc)"); return -1; @@ -70,7 +70,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; mib[0] = CTL_KERN; @@ -120,26 +120,30 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; kd = kvm_openfiles(0, 0, 0, O_RDONLY, errbuf); - if (! kd) { + if (!kd) { // Usually fails due to EPERM against /dev/mem. We retry with // KVM_NO_FILES which apparently has the same effect. // https://stackoverflow.com/questions/22369736/ psutil_debug("kvm_openfiles(O_RDONLY) failed"); kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); - if (! kd) { + if (!kd) { convert_kvm_err("kvm_openfiles()", errbuf); goto error; } } kp = kvm_getprocs( - kd, KERN_PROC_PID | KERN_PROC_SHOW_THREADS | KERN_PROC_KTHREAD, pid, - sizeof(*kp), &nentries); - if (! kp) { + kd, + KERN_PROC_PID | KERN_PROC_SHOW_THREADS | KERN_PROC_KTHREAD, + pid, + sizeof(*kp), + &nentries + ); + if (!kp) { if (strstr(errbuf, "Permission denied") != NULL) AccessDenied("kvm_getprocs"); else @@ -155,7 +159,8 @@ psutil_proc_threads(PyObject *self, PyObject *args) { _Py_PARSE_PID "dd", kp[i].p_tid, PSUTIL_KPT2DOUBLE(kp[i].p_uutime), - PSUTIL_KPT2DOUBLE(kp[i].p_ustime)); + PSUTIL_KPT2DOUBLE(kp[i].p_ustime) + ); if (py_tuple == NULL) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -184,7 +189,7 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { struct kinfo_file *freep; struct kinfo_proc kipp; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kipp) == -1) @@ -220,12 +225,12 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { char path[MAXPATHLEN]; size_t pathlen = sizeof path; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_kinfo_proc(pid, &kp) == -1) return NULL; - int name[] = { CTL_KERN, KERN_PROC_CWD, pid }; + int name[] = {CTL_KERN, KERN_PROC_CWD, pid}; if (sysctl(name, 3, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) { psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c index d3bfd39616..574bdc6eee 100644 --- a/psutil/arch/openbsd/socks.c +++ b/psutil/arch/openbsd/socks.c @@ -49,8 +49,10 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, - &py_type_filter)) { + if (!PyArg_ParseTuple( + args, _Py_PARSE_PID "OO", &pid, &py_af_filter, &py_type_filter + )) + { goto error; } if (!PySequence_Check(py_af_filter) || !PySequence_Check(py_type_filter)) { @@ -59,13 +61,13 @@ psutil_net_connections(PyObject *self, PyObject *args) { } kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); - if (! kd) { + if (!kd) { convert_kvm_err("kvm_openfiles", errbuf); goto error; } ikf = kvm_getfiles(kd, KERN_FILE_BYPID, -1, sizeof(*ikf), &cnt); - if (! ikf) { + if (!ikf) { psutil_PyErr_SetFromOSErrnoWithSyscall("kvm_getfiles"); goto error; } @@ -108,7 +110,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { // local addr inet_ntop(kif->so_family, &kif->inp_laddru, lip, sizeof(lip)); py_laddr = Py_BuildValue("(si)", lip, lport); - if (! py_laddr) + if (!py_laddr) goto error; // remote addr @@ -119,7 +121,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { else { py_raddr = Py_BuildValue("()"); } - if (! py_raddr) + if (!py_raddr) goto error; // populate tuple and list @@ -133,7 +135,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { state, kif->p_pid ); - if (! py_tuple) + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; @@ -142,7 +144,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { // UNIX socket else if (kif->so_family == AF_UNIX) { py_lpath = PyUnicode_DecodeFSDefault(kif->unp_path); - if (! py_lpath) + if (!py_lpath) goto error; py_tuple = Py_BuildValue( @@ -155,7 +157,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { PSUTIL_CONN_NONE, kif->p_pid ); - if (! py_tuple) + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; diff --git a/psutil/arch/openbsd/users.c b/psutil/arch/openbsd/users.c index 5afc25a094..8fa1eb6cd7 100644 --- a/psutil/arch/openbsd/users.c +++ b/psutil/arch/openbsd/users.c @@ -37,21 +37,21 @@ psutil_users(PyObject *self, PyObject *args) { if (*ut.ut_name == '\0') continue; py_username = PyUnicode_DecodeFSDefault(ut.ut_name); - if (! py_username) + if (!py_username) goto error; py_tty = PyUnicode_DecodeFSDefault(ut.ut_line); - if (! py_tty) + if (!py_tty) goto error; py_hostname = PyUnicode_DecodeFSDefault(ut.ut_host); - if (! py_hostname) + if (!py_hostname) goto error; py_tuple = Py_BuildValue( "(OOOdO)", - py_username, // username - py_tty, // tty - py_hostname, // hostname + py_username, // username + py_tty, // tty + py_hostname, // hostname (double)ut.ut_time, // start time - Py_None // pid + Py_None // pid ); if (!py_tuple) goto error; diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index e556037d57..37932a4a1e 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -27,15 +27,15 @@ For reference, here's the git history with original implementations: #include #include #if defined(__arm64__) || defined(__aarch64__) - #include - #include +#include +#include #endif #include "../../arch/all/init.h" // added in macOS 12 #ifndef kIOMainPortDefault - #define kIOMainPortDefault 0 +#define kIOMainPortDefault 0 #endif PyObject * @@ -66,13 +66,15 @@ psutil_cpu_times(PyObject *self, PyObject *args) { mach_port_t mport = mach_host_self(); if (mport == MACH_PORT_NULL) { - PyErr_SetString(PyExc_RuntimeError, - "mach_host_self() returned MACH_PORT_NULL"); + PyErr_SetString( + PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL" + ); return NULL; } - error = host_statistics(mport, HOST_CPU_LOAD_INFO, - (host_info_t)&r_load, &count); + error = host_statistics( + mport, HOST_CPU_LOAD_INFO, (host_info_t)&r_load, &count + ); mach_port_deallocate(mach_task_self(), mport); if (error != KERN_SUCCESS) { @@ -101,8 +103,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { struct vmmeter vmstat; if (mport == MACH_PORT_NULL) { - PyErr_SetString(PyExc_RuntimeError, - "mach_host_self() returned MACH_PORT_NULL"); + PyErr_SetString( + PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL" + ); return NULL; } @@ -158,8 +161,9 @@ psutil_find_pmgr_entry(io_registry_entry_t *out_entry) { while ((entry = IOIteratorNext(iter)) != IO_OBJECT_NULL) { io_name_t name; - if (IORegistryEntryGetName(entry, name) == KERN_SUCCESS && - strcmp(name, "pmgr") == 0) { + if (IORegistryEntryGetName(entry, name) == KERN_SUCCESS + && strcmp(name, "pmgr") == 0) + { found = 1; break; } @@ -198,8 +202,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { if (!psutil_find_pmgr_entry(&entry)) { PyErr_SetString( - PyExc_RuntimeError, - "'pmgr' entry not found in AppleARMIODevice" + PyExc_RuntimeError, "'pmgr' entry not found in AppleARMIODevice" ); return NULL; } @@ -211,12 +214,9 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { entry, CFSTR("voltage-states1-sram"), kCFAllocatorDefault, 0 ); - if (!pCoreRef || - !eCoreRef || - CFGetTypeID(pCoreRef) != CFDataGetTypeID() || - CFGetTypeID(eCoreRef) != CFDataGetTypeID() || - CFDataGetLength(pCoreRef) < 8 || - CFDataGetLength(eCoreRef) < 4) + if (!pCoreRef || !eCoreRef || CFGetTypeID(pCoreRef) != CFDataGetTypeID() + || CFGetTypeID(eCoreRef) != CFDataGetTypeID() + || CFDataGetLength(pCoreRef) < 8 || CFDataGetLength(eCoreRef) < 4) { PyErr_SetString(PyExc_RuntimeError, "invalid CPU frequency data"); goto cleanup; diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index 052d19996b..a84dfd7239 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -114,7 +114,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",force", sizeof(opts)); if (flags & MNT_CMDFLAGS) strlcat(opts, ",cmdflags", sizeof(opts)); - // requires macOS >= 10.5 + // requires macOS >= 10.5 #ifdef MNT_QUARANTINE if (flags & MNT_QUARANTINE) strlcat(opts, ",quarantine", sizeof(opts)); @@ -128,17 +128,18 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { strlcat(opts, ",noatime", sizeof(opts)); #endif py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); - if (! py_dev) + if (!py_dev) goto error; py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); - if (! py_mountp) + if (!py_mountp) goto error; py_tuple = Py_BuildValue( "(OOss)", - py_dev, // device - py_mountp, // mount point - fs[i].f_fstypename, // fs type - opts); // options + py_dev, // device + py_mountp, // mount point + fs[i].f_fstypename, // fs type + opts // options + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -166,9 +167,16 @@ PyObject * psutil_disk_usage_used(PyObject *self, PyObject *args) { PyObject *py_default_value; PyObject *py_mount_point_bytes = NULL; - char* mount_point; - - if (!PyArg_ParseTuple(args, "O&O", PyUnicode_FSConverter, &py_mount_point_bytes, &py_default_value)) { + char *mount_point; + + if (!PyArg_ParseTuple( + args, + "O&O", + PyUnicode_FSConverter, + &py_mount_point_bytes, + &py_default_value + )) + { return NULL; } mount_point = PyBytes_AsString(py_mount_point_bytes); @@ -197,7 +205,9 @@ psutil_disk_usage_used(PyObject *self, PyObject *args) { Py_XDECREF(py_mount_point_bytes); return PyLong_FromUnsignedLongLong(attrbuf.spaceused); } - psutil_debug("getattrlist(ATTR_VOL_SPACEUSED) failed, fall-back to default value"); + psutil_debug( + "getattrlist(ATTR_VOL_SPACEUSED) failed, fall-back to default value" + ); #endif Py_XDECREF(py_mount_point_bytes); Py_INCREF(py_default_value); @@ -223,9 +233,9 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { return NULL; if (IOServiceGetMatchingServices( - kIOMasterPortDefault, - IOServiceMatching(kIOMediaClass), - &disk_list) != kIOReturnSuccess) + kIOMasterPortDefault, IOServiceMatching(kIOMediaClass), &disk_list + ) + != kIOReturnSuccess) { PyErr_SetString(PyExc_RuntimeError, "unable to get the list of disks"); goto error; @@ -238,8 +248,12 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { stats_dict = NULL; parent = IO_OBJECT_NULL; - if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) != kIOReturnSuccess) { - PyErr_SetString(PyExc_RuntimeError, "unable to get the disk's parent"); + if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) + != kIOReturnSuccess) + { + PyErr_SetString( + PyExc_RuntimeError, "unable to get the disk's parent" + ); goto error; } @@ -249,9 +263,13 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { continue; } - if (IORegistryEntryCreateCFProperties(disk, + if (IORegistryEntryCreateCFProperties( + disk, (CFMutableDictionaryRef *)&parent_dict, - kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) + kCFAllocatorDefault, + kNilOptions + ) + != kIOReturnSuccess) { PyErr_SetString( PyExc_RuntimeError, "unable to get the parent's properties" @@ -259,9 +277,13 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { goto error; } - if (IORegistryEntryCreateCFProperties(parent, + if (IORegistryEntryCreateCFProperties( + parent, (CFMutableDictionaryRef *)&props_dict, - kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) + kCFAllocatorDefault, + kNilOptions + ) + != kIOReturnSuccess) { PyErr_SetString( PyExc_RuntimeError, "unable to get the disk properties" @@ -269,9 +291,8 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { goto error; } - CFStringRef disk_name_ref = (CFStringRef)CFDictionaryGetValue( - parent_dict, CFSTR(kIOBSDNameKey) - ); + CFStringRef disk_name_ref = (CFStringRef + )CFDictionaryGetValue(parent_dict, CFSTR(kIOBSDNameKey)); if (disk_name_ref == NULL) { PyErr_SetString(PyExc_RuntimeError, "unable to get disk name"); goto error; @@ -279,8 +300,13 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { const int kMaxDiskNameSize = 64; char disk_name[kMaxDiskNameSize]; - if (!CFStringGetCString(disk_name_ref, disk_name, kMaxDiskNameSize, - CFStringGetSystemEncoding())) { + if (!CFStringGetCString( + disk_name_ref, + disk_name, + kMaxDiskNameSize, + CFStringGetSystemEncoding() + )) + { PyErr_SetString( PyExc_RuntimeError, "unable to convert disk name to C string" ); @@ -300,27 +326,36 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { int64_t read_time = 0, write_time = 0; if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, CFSTR(kIOBlockStorageDriverStatisticsReadsKey)))) + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsReadsKey) + ))) CFNumberGetValue(number, kCFNumberSInt64Type, &reads); if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, CFSTR(kIOBlockStorageDriverStatisticsWritesKey)))) + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsWritesKey) + ))) CFNumberGetValue(number, kCFNumberSInt64Type, &writes); if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, CFSTR(kIOBlockStorageDriverStatisticsBytesReadKey)))) + stats_dict, CFSTR(kIOBlockStorageDriverStatisticsBytesReadKey) + ))) CFNumberGetValue(number, kCFNumberSInt64Type, &read_bytes); if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, CFSTR(kIOBlockStorageDriverStatisticsBytesWrittenKey)))) + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsBytesWrittenKey) + ))) CFNumberGetValue(number, kCFNumberSInt64Type, &write_bytes); if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, CFSTR(kIOBlockStorageDriverStatisticsTotalReadTimeKey)))) + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsTotalReadTimeKey) + ))) CFNumberGetValue(number, kCFNumberSInt64Type, &read_time); if ((number = (CFNumberRef)CFDictionaryGetValue( - stats_dict, CFSTR(kIOBlockStorageDriverStatisticsTotalWriteTimeKey)))) + stats_dict, + CFSTR(kIOBlockStorageDriverStatisticsTotalWriteTimeKey) + ))) CFNumberGetValue(number, kCFNumberSInt64Type, &write_time); py_disk_info = Py_BuildValue( diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index cbac711a9b..dc9b013ffe 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -27,12 +27,15 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { mport = mach_host_self(); if (mport == MACH_PORT_NULL) { - PyErr_SetString(PyExc_RuntimeError, - "mach_host_self() returned MACH_PORT_NULL"); + PyErr_SetString( + PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL" + ); return -1; } - ret = host_statistics64(mport, HOST_VM_INFO64, (host_info64_t)vmstat, &count); + ret = host_statistics64( + mport, HOST_VM_INFO64, (host_info64_t)vmstat, &count + ); mach_port_deallocate(mach_task_self(), mport); if (ret != KERN_SUCCESS) { PyErr_Format( @@ -54,7 +57,7 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { */ PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { - int mib[2]; + int mib[2]; uint64_t total; vm_statistics64_data_t vm; long pagesize = psutil_getpagesize(); @@ -72,12 +75,12 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return Py_BuildValue( "KKKKKK", - (unsigned long long) total, - (unsigned long long) vm.active_count * pagesize, // active - (unsigned long long) vm.inactive_count * pagesize, // inactive - (unsigned long long) vm.wire_count * pagesize, // wired - (unsigned long long) vm.free_count * pagesize, // free - (unsigned long long) vm.speculative_count * pagesize // speculative + (unsigned long long)total, + (unsigned long long)vm.active_count * pagesize, // active + (unsigned long long)vm.inactive_count * pagesize, // inactive + (unsigned long long)vm.wire_count * pagesize, // wired + (unsigned long long)vm.free_count * pagesize, // free + (unsigned long long)vm.speculative_count * pagesize // speculative ); } @@ -103,10 +106,10 @@ psutil_swap_mem(PyObject *self, PyObject *args) { return Py_BuildValue( "KKKKK", - (unsigned long long) totals.xsu_total, - (unsigned long long) totals.xsu_used, - (unsigned long long) totals.xsu_avail, - (unsigned long long) vmstat.pageins * pagesize, - (unsigned long long) vmstat.pageouts * pagesize + (unsigned long long)totals.xsu_total, + (unsigned long long)totals.xsu_used, + (unsigned long long)totals.xsu_avail, + (unsigned long long)vmstat.pageins * pagesize, + (unsigned long long)vmstat.pageouts * pagesize ); } diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c index 93be69e541..12847320fc 100644 --- a/psutil/arch/osx/net.c +++ b/psutil/arch/osx/net.c @@ -30,11 +30,11 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { if (py_retdict == NULL) return NULL; - mib[0] = CTL_NET; // networking subsystem - mib[1] = PF_ROUTE; // type of information - mib[2] = 0; // protocol (IPPROTO_xxx) - mib[3] = 0; // address family - mib[4] = NET_RT_IFLIST2; // operation + mib[0] = CTL_NET; // networking subsystem + mib[1] = PF_ROUTE; // type of information + mib[2] = 0; // protocol (IPPROTO_xxx) + mib[3] = 0; // address family + mib[4] = NET_RT_IFLIST2; // operation mib[5] = 0; if (psutil_sysctl_malloc(mib, 6, &buf, &len) != 0) @@ -42,7 +42,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { lim = buf + len; - for (next = buf; next < lim; ) { + for (next = buf; next < lim;) { if ((size_t)(lim - next) < sizeof(struct if_msghdr)) { psutil_debug("struct if_msghdr size mismatch (skip entry)"); break; @@ -90,7 +90,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { (unsigned long long)if2m->ifm_data.ifi_ierrors, (unsigned long long)if2m->ifm_data.ifi_oerrors, (unsigned long long)if2m->ifm_data.ifi_iqdrops, - 0); // dropout not supported + 0 + ); // dropout not supported if (!py_ifc_info) goto error; diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 43c91ffb73..3b35c46bc8 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -73,9 +73,6 @@ static int is_zombie(size_t pid) { struct kinfo_proc kp; - if (pid < 0) - return psutil_badargs("is_zombie"); - if (psutil_get_kinfo_proc(pid, &kp) == -1) { PyErr_Clear(); return 0; @@ -149,8 +146,7 @@ psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { // check for truncated return size if (ret < size) { psutil_raise_for_pid( - pid, - "proc_pidinfo() returned less data than requested buffer size" + pid, "proc_pidinfo() returned less data than requested buffer size" ); return -1; } @@ -186,14 +182,19 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) { NoSuchProcess("task_for_pid"); } else if (is_zombie(pid) == 1) { - PyErr_SetString(ZombieProcessError, - "task_for_pid -> psutil_is_zombie -> 1"); + PyErr_SetString( + ZombieProcessError, "task_for_pid -> psutil_is_zombie -> 1" + ); } else { psutil_debug( "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " "setting AccessDenied()", - (long)pid, err, errno, mach_error_string(err)); + (long)pid, + err, + errno, + mach_error_string(err) + ); AccessDenied("task_for_pid"); } return -1; @@ -207,7 +208,7 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) { * A wrapper around proc_pidinfo(PROC_PIDLISTFDS), which dynamically sets * the buffer size. */ -static struct proc_fdinfo* +static struct proc_fdinfo * psutil_proc_list_fds(pid_t pid, int *num_fds) { int ret; int fds_size = 0; @@ -231,8 +232,10 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { while (ret > fds_size) { fds_size += PROC_PIDLISTFD_SIZE * 32; if (fds_size > max_size) { - PyErr_Format(PyExc_RuntimeError, - "prevent malloc() to allocate > 24M"); + PyErr_Format( + PyExc_RuntimeError, + "prevent malloc() to allocate > 24M" + ); goto error; } } @@ -285,7 +288,7 @@ PyObject * psutil_proc_is_zombie(PyObject *self, PyObject *args) { pid_t pid; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (is_zombie(pid) == 1) Py_RETURN_TRUE; @@ -308,13 +311,13 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { PyObject *py_name = NULL; PyObject *py_retlist = NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_get_kinfo_proc(pid, &kp) == -1) return NULL; py_name = PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); - if (! py_name) { + if (!py_name) { // Likely a decoding error. We don't want to fail the whole // operation. The python module may retry with proc_name(). PyErr_Clear(); @@ -324,17 +327,17 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { py_retlist = Py_BuildValue( _Py_PARSE_PID "llllllldiO", - kp.kp_eproc.e_ppid, // (pid_t) ppid - (long)kp.kp_eproc.e_pcred.p_ruid, // (long) real uid - (long)kp.kp_eproc.e_ucred.cr_uid, // (long) effective uid - (long)kp.kp_eproc.e_pcred.p_svuid, // (long) saved uid - (long)kp.kp_eproc.e_pcred.p_rgid, // (long) real gid - (long)kp.kp_eproc.e_ucred.cr_groups[0], // (long) effective gid - (long)kp.kp_eproc.e_pcred.p_svgid, // (long) saved gid - (long long)kp.kp_eproc.e_tdev, // (long long) tty nr + kp.kp_eproc.e_ppid, // (pid_t) ppid + (long)kp.kp_eproc.e_pcred.p_ruid, // (long) real uid + (long)kp.kp_eproc.e_ucred.cr_uid, // (long) effective uid + (long)kp.kp_eproc.e_pcred.p_svuid, // (long) saved uid + (long)kp.kp_eproc.e_pcred.p_rgid, // (long) real gid + (long)kp.kp_eproc.e_ucred.cr_groups[0], // (long) effective gid + (long)kp.kp_eproc.e_pcred.p_svgid, // (long) saved gid + (long long)kp.kp_eproc.e_tdev, // (long long) tty nr PSUTIL_TV2DOUBLE(kp.kp_proc.p_starttime), // (double) create time - (int)kp.kp_proc.p_stat, // (int) status - py_name // (pystr) name + (int)kp.kp_proc.p_stat, // (int) status + py_name // (pystr) name ); Py_DECREF(py_name); @@ -357,7 +360,7 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { uint64_t total_user; uint64_t total_system; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) != 0) return NULL; @@ -369,22 +372,22 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { return Py_BuildValue( "(ddKKkkkk)", - (float)total_user / 1000000000.0, // (float) cpu user time - (float)total_system / 1000000000.0, // (float) cpu sys time + (float)total_user / 1000000000.0, // (float) cpu user time + (float)total_system / 1000000000.0, // (float) cpu sys time // Note about memory: determining other mem stats on macOS is a mess: // http://www.opensource.apple.com/source/top/top-67/libtop.c?txt // I just give up. // struct proc_regioninfo pri; // psutil_proc_pidinfo(pid, PROC_PIDREGIONINFO, 0, &pri, sizeof(pri)) pti.pti_resident_size, // (uns long long) rss - pti.pti_virtual_size, // (uns long long) vms - pti.pti_faults, // (uns long) number of page faults (pages) - pti.pti_pageins, // (uns long) number of actual pageins (pages) - pti.pti_threadnum, // (uns long) num threads + pti.pti_virtual_size, // (uns long long) vms + pti.pti_faults, // (uns long) number of page faults (pages) + pti.pti_pageins, // (uns long) number of actual pageins (pages) + pti.pti_threadnum, // (uns long) num threads // Unvoluntary value seems not to be available; // pti.pti_csw probably refers to the sum of the two; // getrusage() numbers seems to confirm this theory. - pti.pti_csw // (uns long) voluntary ctx switches + pti.pti_csw // (uns long) voluntary ctx switches ); } @@ -397,7 +400,7 @@ psutil_proc_name(PyObject *self, PyObject *args) { pid_t pid; struct kinfo_proc kp; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_get_kinfo_proc(pid, &kp) == -1) return NULL; @@ -414,11 +417,13 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { pid_t pid; struct proc_vnodepathinfo pathinfo; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_proc_pidinfo( - pid, PROC_PIDVNODEPATHINFO, 0, &pathinfo, sizeof(pathinfo)) != 0) + pid, PROC_PIDVNODEPATHINFO, 0, &pathinfo, sizeof(pathinfo) + ) + != 0) { return NULL; } @@ -436,7 +441,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { char buf[PATH_MAX]; int ret; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; errno = 0; ret = proc_pidpath(pid, &buf, sizeof(buf)); @@ -511,27 +516,33 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { mach_port_t object_name; mach_vm_address_t prev_addr; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_task_for_pid(pid, &task) != 0) return NULL; - if (psutil_sysctlbyname( - "sysctl.proc_cputype", &cpu_type, sizeof(cpu_type)) != 0) + if (psutil_sysctlbyname("sysctl.proc_cputype", &cpu_type, sizeof(cpu_type)) + != 0) { return NULL; } // Roughly based on libtop_update_vm_regions in // http://www.opensource.apple.com/source/top/top-100.1.2/libtop.c - for (addr = MACH_VM_MIN_ADDRESS; ; addr += size) { + for (addr = MACH_VM_MIN_ADDRESS;; addr += size) { prev_addr = addr; info_count = VM_REGION_TOP_INFO_COUNT; // reset before each call kr = mach_vm_region( - task, &addr, &size, VM_REGION_TOP_INFO, (vm_region_info_t)&info, - &info_count, &object_name); + task, + &addr, + &size, + VM_REGION_TOP_INFO, + (vm_region_info_t)&info, + &info_count, + &object_name + ); if (kr == KERN_INVALID_ADDRESS) { // Done iterating VM regions. break; @@ -550,8 +561,9 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { break; } - if (psutil_in_shared_region(addr, cpu_type) && - info.share_mode != SM_PRIVATE) { + if (psutil_in_shared_region(addr, cpu_type) + && info.share_mode != SM_PRIVATE) + { continue; } @@ -603,7 +615,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (psutil_task_for_pid(pid, &task) != 0) @@ -611,15 +623,18 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // Get basic task info (optional, ignored if access denied) mach_msg_type_number_t info_count = TASK_BASIC_INFO_COUNT; - kr = task_info(task, TASK_BASIC_INFO, (task_info_t)&tasks_info, &info_count); + kr = task_info( + task, TASK_BASIC_INFO, (task_info_t)&tasks_info, &info_count + ); if (kr != KERN_SUCCESS) { if (kr == KERN_INVALID_ARGUMENT) { AccessDenied("task_info(TASK_BASIC_INFO)"); } else { // otherwise throw a runtime error with appropriate error code - PyErr_Format(PyExc_RuntimeError, - "task_info(TASK_BASIC_INFO) syscall failed"); + PyErr_Format( + PyExc_RuntimeError, "task_info(TASK_BASIC_INFO) syscall failed" + ); } goto error; } @@ -632,11 +647,17 @@ psutil_proc_threads(PyObject *self, PyObject *args) { for (j = 0; j < thread_count; j++) { thread_info_count = THREAD_INFO_MAX; - kr = thread_info(thread_list[j], THREAD_BASIC_INFO, - (thread_info_t)thinfo_basic, &thread_info_count); + kr = thread_info( + thread_list[j], + THREAD_BASIC_INFO, + (thread_info_t)thinfo_basic, + &thread_info_count + ); if (kr != KERN_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, - "thread_info(THREAD_BASIC_INFO) syscall failed"); + PyErr_Format( + PyExc_RuntimeError, + "thread_info(THREAD_BASIC_INFO) syscall failed" + ); goto error; } @@ -644,10 +665,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { py_tuple = Py_BuildValue( "Iff", j + 1, - basic_info_th->user_time.seconds + \ - (float)basic_info_th->user_time.microseconds / 1000000.0, - basic_info_th->system_time.seconds + \ - (float)basic_info_th->system_time.microseconds / 1000000.0 + basic_info_th->user_time.seconds + + (float)basic_info_th->user_time.microseconds / 1000000.0, + basic_info_th->system_time.seconds + + (float)basic_info_th->system_time.microseconds / 1000000.0 ); if (!py_tuple) goto error; @@ -657,8 +678,11 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } if (thread_list != NULL) { - vm_deallocate(mach_task_self(), (vm_address_t)thread_list, - thread_count * sizeof(thread_act_t)); + vm_deallocate( + mach_task_self(), + (vm_address_t)thread_list, + thread_count * sizeof(thread_act_t) + ); } if (task != MACH_PORT_NULL) { mach_port_deallocate(mach_task_self(), task); @@ -671,8 +695,11 @@ psutil_proc_threads(PyObject *self, PyObject *args) { Py_XDECREF(py_retlist); if (thread_list != NULL) { - vm_deallocate(mach_task_self(), (vm_address_t)thread_list, - thread_count * sizeof(thread_act_t)); + vm_deallocate( + mach_task_self(), + (vm_address_t)thread_list, + thread_count * sizeof(thread_act_t) + ); } if (task != MACH_PORT_NULL) { mach_port_deallocate(mach_task_self(), task); @@ -684,9 +711,9 @@ psutil_proc_threads(PyObject *self, PyObject *args) { /* * Return process open files as a Python tuple. - * References: - * - lsof source code: https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 - * - /usr/include/sys/proc_info.h + * See lsof source code: + * https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 + * ...and /usr/include/sys/proc_info.h */ PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { @@ -704,7 +731,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; // see: https://github.com/giampaolo/psutil/issues/2116 @@ -720,11 +747,13 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { if (fdp_pointer->proc_fdtype == PROX_FDTYPE_VNODE) { errno = 0; - nb = proc_pidfdinfo((pid_t)pid, - fdp_pointer->proc_fd, - PROC_PIDFDVNODEPATHINFO, - &vi, - sizeof(vi)); + nb = proc_pidfdinfo( + (pid_t)pid, + fdp_pointer->proc_fd, + PROC_PIDFDVNODEPATHINFO, + &vi, + sizeof(vi) + ); // --- errors checking if ((nb <= 0) || nb < sizeof(vi)) { @@ -735,7 +764,8 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { } else { psutil_raise_for_pid( - pid, "proc_pidinfo(PROC_PIDFDVNODEPATHINFO)"); + pid, "proc_pidinfo(PROC_PIDFDVNODEPATHINFO)" + ); goto error; } } @@ -743,12 +773,11 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { // --- construct python list py_path = PyUnicode_DecodeFSDefault(vi.pvip.vip_path); - if (! py_path) + if (!py_path) goto error; py_tuple = Py_BuildValue( - "(Oi)", - py_path, - (int)fdp_pointer->proc_fd); + "(Oi)", py_path, (int)fdp_pointer->proc_fd + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -775,9 +804,9 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { /* * Return process TCP and UDP connections as a list of tuples. * Raises NSP in case of zombie process. - * References: - * - lsof source code: https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 - * - /usr/include/sys/proc_info.h + * See lsof source code: + * https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 + * ...and /usr/include/sys/proc_info.h */ PyObject * psutil_proc_net_connections(PyObject *self, PyObject *args) { @@ -788,7 +817,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { struct proc_fdinfo *fds_pointer = NULL; struct proc_fdinfo *fdp_pointer; struct socket_fdinfo si; - const char* ntopret; + const char *ntopret; PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; @@ -799,8 +828,10 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, - &py_type_filter)) { + if (!PyArg_ParseTuple( + args, _Py_PARSE_PID "OO", &pid, &py_af_filter, &py_type_filter + )) + { goto error; } @@ -824,27 +855,37 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { fdp_pointer = &fds_pointer[i]; if (fdp_pointer->proc_fdtype == PROX_FDTYPE_SOCKET) { - nb = proc_pidfdinfo(pid, fdp_pointer->proc_fd, - PROC_PIDFDSOCKETINFO, &si, sizeof(si)); + nb = proc_pidfdinfo( + pid, + fdp_pointer->proc_fd, + PROC_PIDFDSOCKETINFO, + &si, + sizeof(si) + ); // --- errors checking if ((nb <= 0) || (nb < sizeof(si))) { if (errno == EBADF) { // let's assume socket has been closed - psutil_debug("proc_pidfdinfo(PROC_PIDFDSOCKETINFO) -> " - "EBADF (ignored)"); + psutil_debug( + "proc_pidfdinfo(PROC_PIDFDSOCKETINFO) -> " + "EBADF (ignored)" + ); continue; } else if (errno == EOPNOTSUPP) { // may happen sometimes, see: // https://github.com/giampaolo/psutil/issues/1512 - psutil_debug("proc_pidfdinfo(PROC_PIDFDSOCKETINFO) -> " - "EOPNOTSUPP (ignored)"); + psutil_debug( + "proc_pidfdinfo(PROC_PIDFDSOCKETINFO) -> " + "EOPNOTSUPP (ignored)" + ); continue; } else { psutil_raise_for_pid( - pid, "proc_pidinfo(PROC_PIDFDSOCKETINFO)"); + pid, "proc_pidinfo(PROC_PIDFDSOCKETINFO)" + ); goto error; } } @@ -882,7 +923,8 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (family == AF_INET) { ntopret = inet_ntop( AF_INET, - &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_laddr.ina_46.i46a_addr4, + &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_laddr.ina_46 + .i46a_addr4, lip, sizeof(lip) ); @@ -892,7 +934,8 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { } ntopret = inet_ntop( AF_INET, - &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_faddr.ina_46.i46a_addr4, + &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_faddr.ina_46 + .i46a_addr4, rip, sizeof(rip) ); @@ -942,7 +985,8 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { goto error; py_tuple = Py_BuildValue( - "(iiiNNi)", fd, family, type, py_laddr, py_raddr, state); + "(iiiNNi)", fd, family, type, py_laddr, py_raddr, state + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -951,20 +995,25 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { } else if (family == AF_UNIX) { py_laddr = PyUnicode_DecodeFSDefault( - si.psi.soi_proto.pri_un.unsi_addr.ua_sun.sun_path); + si.psi.soi_proto.pri_un.unsi_addr.ua_sun.sun_path + ); if (!py_laddr) goto error; py_raddr = PyUnicode_DecodeFSDefault( - si.psi.soi_proto.pri_un.unsi_caddr.ua_sun.sun_path); + si.psi.soi_proto.pri_un.unsi_caddr.ua_sun.sun_path + ); if (!py_raddr) goto error; py_tuple = Py_BuildValue( "(iiiOOi)", - fd, family, type, + fd, + family, + type, py_laddr, py_raddr, - PSUTIL_CONN_NONE); + PSUTIL_CONN_NONE + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -1000,7 +1049,7 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { int num_fds; struct proc_fdinfo *fds_pointer; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; fds_pointer = psutil_proc_list_fds(pid, &num_fds); @@ -1028,7 +1077,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; // special case for PID 0 (kernel_task) where cmdline cannot be fetched @@ -1073,7 +1122,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { while (arg_ptr < arg_end && nargs > 0) { if (*arg_ptr++ == '\0') { py_arg = PyUnicode_DecodeFSDefault(curr_arg); - if (! py_arg) + if (!py_arg) goto error; if (PyList_Append(py_retlist, py_arg)) goto error; @@ -1103,7 +1152,8 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { // * target process is not cs_restricted // * SIP is off // * caller has an entitlement -// See: https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/kern/kern_sysctl.c#L1315-L1321 +// See: +// https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/kern/kern_sysctl.c#L1315-L1321 PyObject * psutil_proc_environ(PyObject *self, PyObject *args) { pid_t pid; @@ -1117,7 +1167,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { size_t env_len; PyObject *py_ret = NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; // PID 0 (kernel_task) has no cmdline. diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index ea73d52035..a4f9abea80 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -28,21 +28,23 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { CFNumberRef capacity_ref = NULL; CFNumberRef time_to_empty_ref = NULL; CFStringRef ps_state_ref = NULL; - uint32_t capacity; /* units are percent */ - int time_to_empty; /* units are minutes */ + uint32_t capacity; // units are percent + int time_to_empty; // units are minutes int is_power_plugged; power_info = IOPSCopyPowerSourcesInfo(); if (!power_info) { - PyErr_SetString(PyExc_RuntimeError, - "IOPSCopyPowerSourcesInfo() syscall failed"); + PyErr_SetString( + PyExc_RuntimeError, "IOPSCopyPowerSourcesInfo() syscall failed" + ); goto error; } power_sources_list = IOPSCopyPowerSourcesList(power_info); if (!power_sources_list) { - PyErr_SetString(PyExc_RuntimeError, - "IOPSCopyPowerSourcesList() syscall failed"); + PyErr_SetString( + PyExc_RuntimeError, "IOPSCopyPowerSourcesList() syscall failed" + ); goto error; } @@ -52,7 +54,8 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { } power_sources_information = IOPSGetPowerSourceDescription( - power_info, CFArrayGetValueAtIndex(power_sources_list, 0)); + power_info, CFArrayGetValueAtIndex(power_sources_list, 0) + ); if (!power_sources_information) { PyErr_SetString( PyExc_RuntimeError, "Failed to get power source description" @@ -61,26 +64,38 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { } capacity_ref = (CFNumberRef)CFDictionaryGetValue( - power_sources_information, CFSTR(kIOPSCurrentCapacityKey)); - if (!capacity_ref || !CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) { - PyErr_SetString(PyExc_RuntimeError, - "No battery capacity information in power sources info"); + power_sources_information, CFSTR(kIOPSCurrentCapacityKey) + ); + if (!capacity_ref + || !CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) + { + PyErr_SetString( + PyExc_RuntimeError, + "No battery capacity information in power sources info" + ); goto error; } ps_state_ref = (CFStringRef)CFDictionaryGetValue( - power_sources_information, CFSTR(kIOPSPowerSourceStateKey)); + power_sources_information, CFSTR(kIOPSPowerSourceStateKey) + ); if (!ps_state_ref) { PyErr_SetString(PyExc_RuntimeError, "power source state info missing"); goto error; } is_power_plugged = CFStringCompare( - ps_state_ref, CFSTR(kIOPSACPowerValue), 0) == kCFCompareEqualTo; + ps_state_ref, CFSTR(kIOPSACPowerValue), 0 + ) + == kCFCompareEqualTo; time_to_empty_ref = (CFNumberRef)CFDictionaryGetValue( - power_sources_information, CFSTR(kIOPSTimeToEmptyKey)); - if (!time_to_empty_ref || !CFNumberGetValue(time_to_empty_ref, - kCFNumberIntType, &time_to_empty)) { + power_sources_information, CFSTR(kIOPSTimeToEmptyKey) + ); + if (!time_to_empty_ref + || !CFNumberGetValue( + time_to_empty_ref, kCFNumberIntType, &time_to_empty + )) + { /* This value is recommended for non-Apple power sources, so it's not * an error if it doesn't exist. We'll return -1 for "unknown" */ /* A value of -1 indicates "Still Calculating the Time" also for diff --git a/psutil/arch/posix/init.c b/psutil/arch/posix/init.c index 8a95a3151a..0c7072aa33 100644 --- a/psutil/arch/posix/init.c +++ b/psutil/arch/posix/init.c @@ -10,7 +10,7 @@ #include "init.h" -PyObject *ZombieProcessError = NULL;; +PyObject *ZombieProcessError = NULL; /* * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: @@ -33,7 +33,7 @@ psutil_getpagesize(void) { return sysconf(_SC_PAGE_SIZE); #else // legacy - return (long) getpagesize(); + return (long)getpagesize(); #endif } @@ -97,10 +97,8 @@ psutil_posix_add_constants(PyObject *mod) { if (!mod) return -1; -#if defined(PSUTIL_BSD) || \ - defined(PSUTIL_OSX) || \ - defined(PSUTIL_SUNOS) || \ - defined(PSUTIL_AIX) +#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) || defined(PSUTIL_SUNOS) \ + || defined(PSUTIL_AIX) if (PyModule_AddIntConstant(mod, "AF_LINK", AF_LINK)) return -1; #endif @@ -158,7 +156,7 @@ psutil_posix_add_constants(PyObject *mod) { return -1; #endif -// Linux specific + // Linux specific #ifdef RLIMIT_LOCKS if (PyModule_AddIntConstant(mod, "RLIMIT_LOCKS", RLIMIT_LOCKS)) @@ -190,7 +188,7 @@ psutil_posix_add_constants(PyObject *mod) { return -1; #endif -// Free specific + // Free specific #ifdef RLIMIT_SWAP if (PyModule_AddIntConstant(mod, "RLIMIT_SWAP", RLIMIT_SWAP)) @@ -209,11 +207,12 @@ psutil_posix_add_constants(PyObject *mod) { #if defined(HAVE_LONG_LONG) if (sizeof(RLIM_INFINITY) > sizeof(long)) { - v = PyLong_FromLongLong((PY_LONG_LONG) RLIM_INFINITY); - } else + v = PyLong_FromLongLong((PY_LONG_LONG)RLIM_INFINITY); + } + else #endif { - v = PyLong_FromLong((long) RLIM_INFINITY); + v = PyLong_FromLong((long)RLIM_INFINITY); } if (v) { if (PyModule_AddObject(mod, "RLIM_INFINITY", v)) diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 3e323ca149..e1968fbe97 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -6,25 +6,21 @@ extern PyObject *ZombieProcessError; +// clang-format off #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) #define PSUTIL_HAS_POSIX_USERS - PyObject *psutil_users(PyObject *self, PyObject *args); #endif #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) #include - #define PSUTIL_HAS_SYSCTL - int psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen); int psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen); size_t psutil_sysctl_argmax(); #if !defined(PSUTIL_OPENBSD) - #define PSUTIL_HAS_SYSCTLBYNAME - int psutil_sysctlbyname(const char *name, void *buf, size_t buflen); int psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen); #endif @@ -32,9 +28,9 @@ extern PyObject *ZombieProcessError; #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) #define PSUTIL_HAS_NET_IF_DUPLEX_SPEED - PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); #endif +// clang-format on int psutil_pid_exists(pid_t pid); long psutil_getpagesize(void); diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index b1dd38bdea..9cb38c92dc 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -14,30 +14,33 @@ #include #ifdef PSUTIL_AIX - #include "arch/aix/ifaddrs.h" +#include "arch/aix/ifaddrs.h" #else - #include +#include #endif #if defined(PSUTIL_LINUX) - #include - #include - #include +#include +#include +#include #endif + #if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - #include - #include - #include - #include - #include - #include +#include +#include +#include +#include +#include +#include #endif + #if defined(PSUTIL_SUNOS) - #include - #include +#include +#include #endif + #if defined(PSUTIL_AIX) - #include +#include #endif #include "../../arch/all/init.h" @@ -66,8 +69,9 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { addrlen = sizeof(struct sockaddr_in); else addrlen = sizeof(struct sockaddr_in6); - err = getnameinfo(addr, addrlen, buf, sizeof(buf), NULL, 0, - NI_NUMERICHOST); + err = getnameinfo( + addr, addrlen, buf, sizeof(buf), NULL, 0, NI_NUMERICHOST + ); if (err != 0) { // XXX we get here on FreeBSD when processing 'lo' / AF_INET6 // broadcast. Not sure what to do other than returning None. @@ -123,8 +127,8 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { * Return NICs information a-la ifconfig as a list of tuples. * TODO: on Solaris we won't get any MAC address. */ -PyObject* -psutil_net_if_addrs(PyObject* self, PyObject* args) { +PyObject * +psutil_net_if_addrs(PyObject *self, PyObject *args) { struct ifaddrs *ifaddr, *ifa; int family; @@ -186,7 +190,7 @@ psutil_net_if_addrs(PyObject* self, PyObject* args) { py_ptp ); - if (! py_tuple) + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; @@ -224,7 +228,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { int ret; struct ifreq ifr; - if (! PyArg_ParseTuple(args, "s", &nic_name)) + if (!PyArg_ParseTuple(args, "s", &nic_name)) return NULL; sock = socket(AF_INET, SOCK_DGRAM, 0); @@ -246,12 +250,11 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { } static int -append_flag(PyObject *py_retlist, const char *flag_name) -{ +append_flag(PyObject *py_retlist, const char *flag_name) { PyObject *py_str = NULL; py_str = PyUnicode_FromString(flag_name); - if (! py_str) + if (!py_str) return 0; if (PyList_Append(py_retlist, py_str)) { Py_DECREF(py_str); @@ -277,7 +280,7 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "s", &nic_name)) + if (!PyArg_ParseTuple(args, "s", &nic_name)) goto error; sock = socket(AF_INET, SOCK_DGRAM, 0); @@ -298,10 +301,14 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { flags = ifr.ifr_flags & 0xFFFF; - // Linux/glibc IFF flags: https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/gnu/net/if.h;h=251418f82331c0426e58707fe4473d454893b132;hb=HEAD - // macOS IFF flags: https://opensource.apple.com/source/xnu/xnu-792/bsd/net/if.h.auto.html - // AIX IFF flags: https://www.ibm.com/support/pages/how-hexadecimal-flags-displayed-ifconfig-are-calculated - // FreeBSD IFF flags: https://www.freebsd.org/cgi/man.cgi?query=if_allmulti&apropos=0&sektion=0&manpath=FreeBSD+10-current&format=html + // Linux/glibc IFF flags: + // https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/gnu/net/if.h;h=251418f82331c0426e58707fe4473d454893b132;hb=HEAD + // macOS IFF flags: + // https://opensource.apple.com/source/xnu/xnu-792/bsd/net/if.h.auto.html + // AIX IFF flags: + // https://www.ibm.com/support/pages/how-hexadecimal-flags-displayed-ifconfig-are-calculated + // FreeBSD IFF flags: + // https://www.freebsd.org/cgi/man.cgi?query=if_allmulti&apropos=0&sektion=0&manpath=FreeBSD+10-current&format=html #ifdef IFF_UP // Available in (at least) Linux, macOS, AIX, BSD @@ -458,7 +465,7 @@ psutil_net_if_is_running(PyObject *self, PyObject *args) { int ret; struct ifreq ifr; - if (! PyArg_ParseTuple(args, "s", &nic_name)) + if (!PyArg_ParseTuple(args, "s", &nic_name)) return NULL; sock = socket(AF_INET, SOCK_DGRAM, 0); @@ -483,82 +490,83 @@ psutil_net_if_is_running(PyObject *self, PyObject *args) { } - // net_if_stats() macOS/BSD implementation. #ifdef PSUTIL_HAS_NET_IF_DUPLEX_SPEED -int psutil_get_nic_speed(int ifm_active) { +int +psutil_get_nic_speed(int ifm_active) { // Determine NIC speed. Taken from: // http://www.i-scream.org/libstatgrab/ // Assuming only ETHER devices - switch(IFM_TYPE(ifm_active)) { + switch (IFM_TYPE(ifm_active)) { case IFM_ETHER: - switch(IFM_SUBTYPE(ifm_active)) { -#if defined(IFM_HPNA_1) && ((!defined(IFM_10G_LR)) \ - || (IFM_10G_LR != IFM_HPNA_1)) + switch (IFM_SUBTYPE(ifm_active)) { +#if defined(IFM_HPNA_1) \ + && ((!defined(IFM_10G_LR)) || (IFM_10G_LR != IFM_HPNA_1)) // HomePNA 1.0 (1Mb/s) - case(IFM_HPNA_1): + case (IFM_HPNA_1): return 1; #endif // 10 Mbit - case(IFM_10_T): // 10BaseT - RJ45 - case(IFM_10_2): // 10Base2 - Thinnet - case(IFM_10_5): // 10Base5 - AUI - case(IFM_10_STP): // 10BaseT over shielded TP - case(IFM_10_FL): // 10baseFL - Fiber + case (IFM_10_T): // 10BaseT - RJ45 + case (IFM_10_2): // 10Base2 - Thinnet + case (IFM_10_5): // 10Base5 - AUI + case (IFM_10_STP): // 10BaseT over shielded TP + case (IFM_10_FL): // 10baseFL - Fiber return 10; // 100 Mbit - case(IFM_100_TX): // 100BaseTX - RJ45 - case(IFM_100_FX): // 100BaseFX - Fiber - case(IFM_100_T4): // 100BaseT4 - 4 pair cat 3 - case(IFM_100_VG): // 100VG-AnyLAN - case(IFM_100_T2): // 100BaseT2 + case (IFM_100_TX): // 100BaseTX - RJ45 + case (IFM_100_FX): // 100BaseFX - Fiber + case (IFM_100_T4): // 100BaseT4 - 4 pair cat 3 + case (IFM_100_VG): // 100VG-AnyLAN + case (IFM_100_T2): // 100BaseT2 return 100; // 1000 Mbit - case(IFM_1000_SX): // 1000BaseSX - multi-mode fiber - case(IFM_1000_LX): // 1000baseLX - single-mode fiber - case(IFM_1000_CX): // 1000baseCX - 150ohm STP + case (IFM_1000_SX): // 1000BaseSX - multi-mode fiber + case (IFM_1000_LX): // 1000baseLX - single-mode fiber + case (IFM_1000_CX): // 1000baseCX - 150ohm STP #if defined(IFM_1000_TX) && !defined(PSUTIL_OPENBSD) - #define HAS_CASE_IFM_1000_TX 1 - // FreeBSD 4 and others (but NOT OpenBSD) -> #define IFM_1000_T in net/if_media.h - case(IFM_1000_TX): +#define HAS_CASE_IFM_1000_TX 1 + // FreeBSD 4 and others (but NOT OpenBSD) -> #define IFM_1000_T + // in net/if_media.h + case (IFM_1000_TX): #endif #ifdef IFM_1000_FX - case(IFM_1000_FX): + case (IFM_1000_FX): #endif #if defined(IFM_1000_T) && (!HAS_CASE_IFM_1000_TX || IFM_1000_T != IFM_1000_TX) - case(IFM_1000_T): + case (IFM_1000_T): #endif return 1000; #if defined(IFM_10G_SR) || defined(IFM_10G_LR) || defined(IFM_10G_CX4) \ - || defined(IFM_10G_T) + || defined(IFM_10G_T) #ifdef IFM_10G_SR - case(IFM_10G_SR): + case (IFM_10G_SR): #endif #ifdef IFM_10G_LR - case(IFM_10G_LR): + case (IFM_10G_LR): #endif #ifdef IFM_10G_CX4 - case(IFM_10G_CX4): + case (IFM_10G_CX4): #endif #ifdef IFM_10G_TWINAX - case(IFM_10G_TWINAX): + case (IFM_10G_TWINAX): #endif #ifdef IFM_10G_TWINAX_LONG - case(IFM_10G_TWINAX_LONG): + case (IFM_10G_TWINAX_LONG): #endif #ifdef IFM_10G_T - case(IFM_10G_T): + case (IFM_10G_T): #endif return 10000; #endif #if defined(IFM_2500_SX) #ifdef IFM_2500_SX - case(IFM_2500_SX): + case (IFM_2500_SX): #endif return 2500; -#endif // any 2.5GBit stuff... - // We don't know what it is +#endif // any 2.5GBit stuff... + // We don't know what it is default: return 0; } @@ -566,7 +574,7 @@ int psutil_get_nic_speed(int ifm_active) { #ifdef IFM_TOKEN case IFM_TOKEN: - switch(IFM_SUBTYPE(ifm_active)) { + switch (IFM_SUBTYPE(ifm_active)) { case IFM_TOK_STP4: // Shielded twisted pair 4m - DB9 case IFM_TOK_UTP4: // Unshielded twisted pair 4m - RJ45 return 4; @@ -591,7 +599,7 @@ int psutil_get_nic_speed(int ifm_active) { #ifdef IFM_FDDI case IFM_FDDI: - switch(IFM_SUBTYPE(ifm_active)) { + switch (IFM_SUBTYPE(ifm_active)) { // We don't know what it is default: return 0; @@ -599,7 +607,7 @@ int psutil_get_nic_speed(int ifm_active) { break; #endif case IFM_IEEE80211: - switch(IFM_SUBTYPE(ifm_active)) { + switch (IFM_SUBTYPE(ifm_active)) { case IFM_IEEE80211_FH1: // Frequency Hopping 1Mbps case IFM_IEEE80211_DS1: // Direct Sequence 1Mbps return 1; @@ -639,7 +647,7 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { struct ifreq ifr; struct ifmediareq ifmed; - if (! PyArg_ParseTuple(args, "s", &nic_name)) + if (!PyArg_ParseTuple(args, "s", &nic_name)) return NULL; sock = socket(AF_INET, SOCK_DGRAM, 0); diff --git a/psutil/arch/posix/proc.c b/psutil/arch/posix/proc.c index b0ddf7b0e6..800e56dde6 100644 --- a/psutil/arch/posix/proc.c +++ b/psutil/arch/posix/proc.c @@ -35,7 +35,7 @@ psutil_pid_exists(pid_t pid) { #endif } - ret = kill(pid , 0); + ret = kill(pid, 0); if (ret == 0) return 1; else { @@ -85,7 +85,7 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { int priority; errno = 0; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; #ifdef PSUTIL_OSX @@ -106,7 +106,7 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { int priority; int retval; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) return NULL; #ifdef PSUTIL_OSX diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index fbe1b561cb..4edc21450c 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -117,9 +117,7 @@ psutil_sysctl_argmax() { } if (argmax <= 0) { - PyErr_SetString( - PyExc_RuntimeError, "sysctl(KERN_ARGMAX) return <= 0" - ); + PyErr_SetString(PyExc_RuntimeError, "sysctl(KERN_ARGMAX) return <= 0"); return 0; } @@ -177,7 +175,9 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { // First query to determine required size. ret = sysctlbyname(name, NULL, &needed, NULL, 0); if (ret == -1) { - snprintf(errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 1/3", name); + snprintf( + errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 1/3", name + ); psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); return -1; } diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c index df33820e0a..8c75bf3308 100644 --- a/psutil/arch/posix/users.c +++ b/psutil/arch/posix/users.c @@ -13,7 +13,6 @@ #include - static void setup() { UTXENT_MUTEX_LOCK(); @@ -49,17 +48,16 @@ psutil_users(PyObject *self, PyObject *args) { py_tuple = NULL; py_username = PyUnicode_DecodeFSDefault(ut->ut_user); - if (! py_username) + if (!py_username) goto error; py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); - if (! py_tty) + if (!py_tty) goto error; host_len = strnlen(ut->ut_host, sizeof(ut->ut_host)); - if (host_len == 0 || - (strcmp(ut->ut_host, ":0") == 0) || - (strcmp(ut->ut_host, ":0.0") == 0)) + if (host_len == 0 || (strcmp(ut->ut_host, ":0") == 0) + || (strcmp(ut->ut_host, ":0.0") == 0)) { py_hostname = PyUnicode_DecodeFSDefault("localhost"); } @@ -76,13 +74,13 @@ psutil_users(PyObject *self, PyObject *args) { py_tuple = Py_BuildValue( "OOOd" _Py_PARSE_PID, - py_username, // username - py_tty, // tty - py_hostname, // hostname + py_username, // username + py_tty, // tty + py_hostname, // hostname (double)ut->ut_tv.tv_sec, // tstamp - ut->ut_pid // process id + ut->ut_pid // process id ); - if (! py_tuple) + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; diff --git a/psutil/arch/sunos/cpu.c b/psutil/arch/sunos/cpu.c index 946bffd00f..361b741558 100644 --- a/psutil/arch/sunos/cpu.c +++ b/psutil/arch/sunos/cpu.c @@ -34,11 +34,13 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_OSError); goto error; } - py_cputime = Py_BuildValue("ffff", - (float)cs.cpu_sysinfo.cpu[CPU_USER], - (float)cs.cpu_sysinfo.cpu[CPU_KERNEL], - (float)cs.cpu_sysinfo.cpu[CPU_IDLE], - (float)cs.cpu_sysinfo.cpu[CPU_WAIT]); + py_cputime = Py_BuildValue( + "ffff", + (float)cs.cpu_sysinfo.cpu[CPU_USER], + (float)cs.cpu_sysinfo.cpu[CPU_KERNEL], + (float)cs.cpu_sysinfo.cpu[CPU_IDLE], + (float)cs.cpu_sysinfo.cpu[CPU_WAIT] + ); if (py_cputime == NULL) goto error; if (PyList_Append(py_retlist, py_cputime)) @@ -127,8 +129,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } kstat_close(kc); - return Py_BuildValue( - "IIII", ctx_switches, interrupts, syscalls, traps); + return Py_BuildValue("IIII", ctx_switches, interrupts, syscalls, traps); error: if (kc != NULL) diff --git a/psutil/arch/sunos/disk.c b/psutil/arch/sunos/disk.c index f736984c76..2d37d8d317 100644 --- a/psutil/arch/sunos/disk.c +++ b/psutil/arch/sunos/disk.c @@ -23,7 +23,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { return NULL; kc = kstat_open(); if (kc == NULL) { - PyErr_SetFromErrno(PyExc_OSError);; + PyErr_SetFromErrno(PyExc_OSError); goto error; } ksp = kc->kc_chain; @@ -32,7 +32,8 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (strcmp(ksp->ks_class, "disk") == 0) { if (kstat_read(kc, ksp, &kio) == -1) { kstat_close(kc); - return PyErr_SetFromErrno(PyExc_OSError);; + return PyErr_SetFromErrno(PyExc_OSError); + ; } py_disk_info = Py_BuildValue( "(IIKKLL)", @@ -41,12 +42,13 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { kio.nread, kio.nwritten, kio.rtime / 1000 / 1000, // from nano to milli secs - kio.wtime / 1000 / 1000 // from nano to milli secs + kio.wtime / 1000 / 1000 // from nano to milli secs ); if (!py_disk_info) goto error; - if (PyDict_SetItemString(py_retdict, ksp->ks_name, - py_disk_info)) + if (PyDict_SetItemString( + py_retdict, ksp->ks_name, py_disk_info + )) goto error; Py_CLEAR(py_disk_info); } @@ -86,17 +88,18 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { while (getmntent(file, &mt) == 0) { py_dev = PyUnicode_DecodeFSDefault(mt.mnt_special); - if (! py_dev) + if (!py_dev) goto error; py_mountp = PyUnicode_DecodeFSDefault(mt.mnt_mountp); - if (! py_mountp) + if (!py_mountp) goto error; py_tuple = Py_BuildValue( "(OOss)", - py_dev, // device - py_mountp, // mount point - mt.mnt_fstype, // fs type - mt.mnt_mntopts); // options + py_dev, // device + py_mountp, // mount point + mt.mnt_fstype, // fs type + mt.mnt_mntopts // options + ); if (py_tuple == NULL) goto error; if (PyList_Append(py_retlist, py_tuple)) diff --git a/psutil/arch/sunos/environ.c b/psutil/arch/sunos/environ.c index 458c7f2225..2e0767b32d 100644 --- a/psutil/arch/sunos/environ.c +++ b/psutil/arch/sunos/environ.c @@ -54,7 +54,7 @@ open_address_space(pid_t pid, const char *procfs_path) { static size_t read_offt(int fd, off_t offset, char *buf, size_t buf_size) { size_t to_read = buf_size; - size_t stored = 0; + size_t stored = 0; int r; while (to_read) { @@ -70,7 +70,7 @@ read_offt(int fd, off_t offset, char *buf, size_t buf_size) { return stored; - error: +error: PyErr_SetFromErrno(PyExc_OSError); return -1; } @@ -82,7 +82,7 @@ read_offt(int fd, off_t offset, char *buf, size_t buf_size) { * @param fd a file descriptor of opened address space. * @param offset an offset in specified file descriptor. * @return allocated null-terminated string or NULL in case of error. -*/ + */ static char * read_cstring_offt(int fd, off_t offset) { int r; @@ -108,8 +108,8 @@ read_cstring_offt(int fd, off_t offset) { break; } else { - for (i=0; i= 0 && count) *count = env_count; if (env_count > 0) - result = read_cstrings_block( - as, info.pr_envp, ptr_size, env_count); + result = read_cstrings_block(as, info.pr_envp, ptr_size, env_count); close(as); return result; @@ -391,7 +391,7 @@ psutil_free_cstrings_array(char **array, size_t count) { if (!array) return; - for (i=0; iswt_ent; - for (i = 0; i < num; i++, swapent++) { - swapent->ste_path = path; - path += MAXPATHLEN; - } - st->swt_n = num; - if ((num = swapctl(SC_LIST, st)) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } + if ((num = swapctl(SC_GETNSWP, NULL)) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + if (num == 0) { + PyErr_SetString(PyExc_RuntimeError, "no swap devices configured"); + return NULL; + } + if ((st = malloc(num * sizeof(swapent_t) + sizeof (int))) == NULL) { + PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + return NULL; + } + if ((path = malloc(num * MAXPATHLEN)) == NULL) { + PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + return NULL; + } + swapent = st->swt_ent; + for (i = 0; i < num; i++, swapent++) { + swapent->ste_path = path; + path += MAXPATHLEN; + } + st->swt_n = num; + if ((num = swapctl(SC_LIST, st)) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } - swapent = st->swt_ent; - long t = 0, f = 0; - for (i = 0; i < num; i++, swapent++) { - int diskblks_per_page =(int)(sysconf(_SC_PAGESIZE) >> DEV_BSHIFT); - t += (long)swapent->ste_pages; - f += (long)swapent->ste_free; - } + swapent = st->swt_ent; + long t = 0, f = 0; + for (i = 0; i < num; i++, swapent++) { + int diskblks_per_page =(int)(sysconf(_SC_PAGESIZE) >> DEV_BSHIFT); + t += (long)swapent->ste_pages; + f += (long)swapent->ste_free; + } - free(st); - return Py_BuildValue("(kk)", t, f); -*/ + free(st); + return Py_BuildValue("(kk)", t, f); + */ kstat_ctl_t *kc; - kstat_t *k; - cpu_stat_t *cpu; - int cpu_count = 0; - int flag = 0; - uint_t sin = 0; - uint_t sout = 0; + kstat_t *k; + cpu_stat_t *cpu; + int cpu_count = 0; + int flag = 0; + uint_t sin = 0; + uint_t sout = 0; kc = kstat_open(); if (kc == NULL) - return PyErr_SetFromErrno(PyExc_OSError);; + return PyErr_SetFromErrno(PyExc_OSError); + ; k = kc->kc_chain; while (k != NULL) { - if ((strncmp(k->ks_name, "cpu_stat", 8) == 0) && \ - (kstat_read(kc, k, NULL) != -1) ) + if ((strncmp(k->ks_name, "cpu_stat", 8) == 0) + && (kstat_read(kc, k, NULL) != -1)) { flag = 1; - cpu = (cpu_stat_t *) k->ks_data; - sin += cpu->cpu_vminfo.pgswapin; // num pages swapped in + cpu = (cpu_stat_t *)k->ks_data; + sin += cpu->cpu_vminfo.pgswapin; // num pages swapped in sout += cpu->cpu_vminfo.pgswapout; // num pages swapped out } cpu_count += 1; diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c index 1d1432b6db..c1b3b008d0 100644 --- a/psutil/arch/sunos/net.c +++ b/psutil/arch/sunos/net.c @@ -27,7 +27,7 @@ PyObject * psutil_net_io_counters(PyObject *self, PyObject *args) { - kstat_ctl_t *kc = NULL; + kstat_ctl_t *kc = NULL; kstat_t *ksp; kstat_named_t *rbytes, *wbytes, *rpkts, *wpkts, *ierrs, *oerrs; int ret; @@ -78,38 +78,38 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { ierrs = (kstat_named_t *)kstat_data_lookup(ksp, "ierrors"); oerrs = (kstat_named_t *)kstat_data_lookup(ksp, "oerrors"); - if ((rbytes == NULL) || (wbytes == NULL) || (rpkts == NULL) || - (wpkts == NULL) || (ierrs == NULL) || (oerrs == NULL)) + if ((rbytes == NULL) || (wbytes == NULL) || (rpkts == NULL) + || (wpkts == NULL) || (ierrs == NULL) || (oerrs == NULL)) { PyErr_SetString(PyExc_RuntimeError, "kstat_data_lookup() failed"); goto error; } - if (rbytes->data_type == KSTAT_DATA_UINT64) - { - py_ifc_info = Py_BuildValue("(KKKKIIii)", - wbytes->value.ui64, - rbytes->value.ui64, - wpkts->value.ui64, - rpkts->value.ui64, - ierrs->value.ui32, - oerrs->value.ui32, - 0, // dropin not supported - 0 // dropout not supported - ); + if (rbytes->data_type == KSTAT_DATA_UINT64) { + py_ifc_info = Py_BuildValue( + "(KKKKIIii)", + wbytes->value.ui64, + rbytes->value.ui64, + wpkts->value.ui64, + rpkts->value.ui64, + ierrs->value.ui32, + oerrs->value.ui32, + 0, // dropin not supported + 0 // dropout not supported + ); } - else - { - py_ifc_info = Py_BuildValue("(IIIIIIii)", - wbytes->value.ui32, - rbytes->value.ui32, - wpkts->value.ui32, - rpkts->value.ui32, - ierrs->value.ui32, - oerrs->value.ui32, - 0, // dropin not supported - 0 // dropout not supported - ); + else { + py_ifc_info = Py_BuildValue( + "(IIIIIIii)", + wbytes->value.ui32, + rbytes->value.ui32, + wpkts->value.ui32, + rpkts->value.ui32, + ierrs->value.ui32, + oerrs->value.ui32, + 0, // dropin not supported + 0 // dropout not supported + ); } if (!py_ifc_info) goto error; @@ -118,7 +118,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { Py_CLEAR(py_ifc_info); goto next; -next: + next: ksp = ksp->ks_next; } @@ -141,8 +141,8 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { // Return stats about a particular network interface. Refs: // * https://github.com/dpaleino/wicd/blob/master/wicd/backends/be-ioctl.py // * http://www.i-scream.org/libstatgrab/ -PyObject* -psutil_net_if_stats(PyObject* self, PyObject* args) { +PyObject * +psutil_net_if_stats(PyObject *self, PyObject *args) { kstat_ctl_t *kc = NULL; kstat_t *ksp; kstat_named_t *knp; @@ -219,8 +219,9 @@ psutil_net_if_stats(PyObject* self, PyObject* args) { if (ret == -1) goto error; - py_ifc_info = Py_BuildValue("(Oiii)", py_is_up, duplex, speed, - ifr.lifr_mtu); + py_ifc_info = Py_BuildValue( + "(Oiii)", py_is_up, duplex, speed, ifr.lifr_mtu + ); if (!py_ifc_info) goto error; if (PyDict_SetItemString(py_retdict, ksp->ks_name, py_ifc_info)) @@ -262,10 +263,10 @@ psutil_net_connections(PyObject *self, PyObject *args) { long pid; int sd = 0; mib2_tcpConnEntry_t tp; - mib2_udpEntry_t ude; + mib2_udpEntry_t ude; #if defined(AF_INET6) mib2_tcp6ConnEntry_t tp6; - mib2_udp6Entry_t ude6; + mib2_udp6Entry_t ude6; #endif char buf[512]; int i, flags, getcode, num_ent, state, ret; @@ -276,8 +277,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct strbuf ctlbuf, databuf; struct T_optmgmt_req tor = {0}; struct T_optmgmt_ack toa = {0}; - struct T_error_ack tea = {0}; - struct opthdr mibhdr = {0}; + struct T_error_ack tea = {0}; + struct opthdr mibhdr = {0}; PyObject *py_retlist = PyList_New(0); PyObject *py_tuple = NULL; @@ -286,7 +287,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) + if (!PyArg_ParseTuple(args, "l", &pid)) goto error; sd = open("/dev/arp", O_RDWR); @@ -311,13 +312,13 @@ psutil_net_connections(PyObject *self, PyObject *args) { // function. Also see: // http://stackoverflow.com/questions/8723598/ tor.PRIM_type = T_SVR4_OPTMGMT_REQ; - tor.OPT_offset = sizeof (struct T_optmgmt_req); - tor.OPT_length = sizeof (struct opthdr); + tor.OPT_offset = sizeof(struct T_optmgmt_req); + tor.OPT_length = sizeof(struct opthdr); tor.MGMT_flags = T_CURRENT; mibhdr.level = MIB2_IP; - mibhdr.name = 0; + mibhdr.name = 0; - mibhdr.len = 1; + mibhdr.len = 1; memcpy(buf, &tor, sizeof tor); memcpy(buf + tor.OPT_offset, &mibhdr, sizeof mibhdr); @@ -330,30 +331,27 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; } - ctlbuf.maxlen = sizeof (buf); + ctlbuf.maxlen = sizeof(buf); for (;;) { flags = 0; getcode = getmsg(sd, &ctlbuf, (struct strbuf *)0, &flags); memcpy(&toa, buf, sizeof toa); memcpy(&tea, buf, sizeof tea); - if (getcode != MOREDATA || - ctlbuf.len < (int)sizeof (struct T_optmgmt_ack) || - toa.PRIM_type != T_OPTMGMT_ACK || - toa.MGMT_flags != T_SUCCESS) + if (getcode != MOREDATA + || ctlbuf.len < (int)sizeof(struct T_optmgmt_ack) + || toa.PRIM_type != T_OPTMGMT_ACK || toa.MGMT_flags != T_SUCCESS) { break; } - if (ctlbuf.len >= (int)sizeof (struct T_error_ack) && - tea.PRIM_type == T_ERROR_ACK) + if (ctlbuf.len >= (int)sizeof(struct T_error_ack) + && tea.PRIM_type == T_ERROR_ACK) { PyErr_SetString(PyExc_RuntimeError, "ERROR_ACK"); goto error; } - if (getcode == 0 && - ctlbuf.len >= (int)sizeof (struct T_optmgmt_ack) && - toa.PRIM_type == T_OPTMGMT_ACK && - toa.MGMT_flags == T_SUCCESS) + if (getcode == 0 && ctlbuf.len >= (int)sizeof(struct T_optmgmt_ack) + && toa.PRIM_type == T_OPTMGMT_ACK && toa.MGMT_flags == T_SUCCESS) { PyErr_SetString(PyExc_RuntimeError, "ERROR_T_OPTMGMT_ACK"); goto error; @@ -406,9 +404,16 @@ psutil_net_connections(PyObject *self, PyObject *args) { state = tp.tcpConnEntryInfo.ce_state; // add item - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET, SOCK_STREAM, - py_laddr, py_raddr, state, - processed_pid); + py_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET, + SOCK_STREAM, + py_laddr, + py_raddr, + state, + processed_pid + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -418,8 +423,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { } #if defined(AF_INET6) // TCPv6 - else if (mibhdr.level == MIB2_TCP6 && mibhdr.name == MIB2_TCP6_CONN) - { + else if (mibhdr.level == MIB2_TCP6 && mibhdr.name == MIB2_TCP6_CONN) { num_ent = mibhdr.len / sizeof(mib2_tcp6ConnEntry_t); for (i = 0; i < num_ent; i++) { @@ -428,7 +432,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (pid != -1 && processed_pid != pid) continue; // construct local/remote addresses - inet_ntop(AF_INET6, &tp6.tcp6ConnLocalAddress, lip, sizeof(lip)); + inet_ntop( + AF_INET6, &tp6.tcp6ConnLocalAddress, lip, sizeof(lip) + ); inet_ntop(AF_INET6, &tp6.tcp6ConnRemAddress, rip, sizeof(rip)); lport = tp6.tcp6ConnLocalPort; rport = tp6.tcp6ConnRemPort; @@ -446,8 +452,16 @@ psutil_net_connections(PyObject *self, PyObject *args) { state = tp6.tcp6ConnEntryInfo.ce_state; // add item - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET6, SOCK_STREAM, - py_laddr, py_raddr, state, processed_pid); + py_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_STREAM, + py_laddr, + py_raddr, + state, + processed_pid + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -480,9 +494,16 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_raddr = Py_BuildValue("()"); if (!py_raddr) goto error; - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET, SOCK_DGRAM, - py_laddr, py_raddr, PSUTIL_CONN_NONE, - processed_pid); + py_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET, + SOCK_DGRAM, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE, + processed_pid + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -492,9 +513,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { } #if defined(AF_INET6) // UDPv6 - else if (mibhdr.level == MIB2_UDP6 || - mibhdr.level == MIB2_UDP6_ENTRY) - { + else if (mibhdr.level == MIB2_UDP6 || mibhdr.level == MIB2_UDP6_ENTRY) + { num_ent = mibhdr.len / sizeof(mib2_udp6Entry_t); for (i = 0; i < num_ent; i++) { memcpy(&ude6, databuf.buf + i * sizeof ude6, sizeof ude6); @@ -509,9 +529,16 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_raddr = Py_BuildValue("()"); if (!py_raddr) goto error; - py_tuple = Py_BuildValue("(iiiNNiI)", -1, AF_INET6, SOCK_DGRAM, - py_laddr, py_raddr, PSUTIL_CONN_NONE, - processed_pid); + py_tuple = Py_BuildValue( + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_DGRAM, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE, + processed_pid + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index f892353f0b..c354d2c058 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -30,10 +30,11 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { PyErr_SetFromErrno(PyExc_OSError); return 0; } - if (nbytes != (ssize_t) size) { + if (nbytes != (ssize_t)size) { close(fd); PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch"); + PyExc_RuntimeError, "read() file structure size mismatch" + ); return 0; } close(fd); @@ -52,27 +53,27 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { psinfo_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/psinfo", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return Py_BuildValue( "ikkdiiikiiii", - info.pr_ppid, // parent pid - info.pr_rssize, // rss - info.pr_size, // vms + info.pr_ppid, // parent pid + info.pr_rssize, // rss + info.pr_size, // vms PSUTIL_TV2DOUBLE(info.pr_start), // create time - info.pr_lwp.pr_nice, // nice - info.pr_nlwp, // no. of threads - info.pr_lwp.pr_state, // status code - info.pr_ttydev, // tty nr - (int)info.pr_uid, // real user id - (int)info.pr_euid, // effective user id - (int)info.pr_gid, // real group id - (int)info.pr_egid // effective group id - ); + info.pr_lwp.pr_nice, // nice + info.pr_nlwp, // no. of threads + info.pr_lwp.pr_state, // status code + info.pr_ttydev, // tty nr + (int)info.pr_uid, // real user id + (int)info.pr_euid, // effective user id + (int)info.pr_gid, // real group id + (int)info.pr_egid // effective group id + ); } @@ -95,18 +96,19 @@ psutil_proc_name_and_args(PyObject *self, PyObject *args) { PyObject *py_args_list = NULL; PyObject *py_rettuple = NULL; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/psinfo", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; py_name = PyUnicode_DecodeFSDefault(info.pr_fname); if (!py_name) goto error; - /* SunOS truncates arguments to length PRARGSZ and has them space-separated. - * The only way to retrieve full properly-split command line is to parse process memory */ + // SunOS truncates arguments to length PRARGSZ and has them + // space-separated. The only way to retrieve full properly-split + // command line is to parse process memory. argv = psutil_read_raw_args(info, procfs_path, &argc); if (argv) { py_args_list = PyList_New(argc); @@ -189,41 +191,41 @@ psutil_proc_environ(PyObject *self, PyObject *args) { PyObject *py_envval = NULL; PyObject *py_retdict = PyDict_New(); - if (! py_retdict) + if (!py_retdict) return PyErr_NoMemory(); - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/psinfo", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) goto error; - if (! info.pr_envp) { + if (!info.pr_envp) { AccessDenied("/proc/pid/psinfo struct not set"); goto error; } env = psutil_read_raw_env(info, procfs_path, &env_count); - if (! env && env_count != 0) + if (!env && env_count != 0) goto error; - for (i=0; i= 0) psutil_free_cstrings_array(env, env_count); @@ -257,18 +259,18 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { pstatus_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; // results are more precise than os.times() return Py_BuildValue( "(dddd)", - PSUTIL_TV2DOUBLE(info.pr_utime), - PSUTIL_TV2DOUBLE(info.pr_stime), - PSUTIL_TV2DOUBLE(info.pr_cutime), - PSUTIL_TV2DOUBLE(info.pr_cstime) + PSUTIL_TV2DOUBLE(info.pr_utime), + PSUTIL_TV2DOUBLE(info.pr_stime), + PSUTIL_TV2DOUBLE(info.pr_cutime), + PSUTIL_TV2DOUBLE(info.pr_cstime) ); } @@ -289,7 +291,7 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { ssize_t nbytes; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/lpsinfo", procfs_path, pid); @@ -307,7 +309,8 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { } if (nbytes != sizeof(header)) { PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch"); + PyExc_RuntimeError, "read() file structure size mismatch" + ); goto error; } @@ -328,7 +331,8 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { } if (nbytes != size) { PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch"); + PyExc_RuntimeError, "read() file structure size mismatch" + ); goto error; } @@ -356,14 +360,20 @@ psutil_proc_cred(PyObject *self, PyObject *args) { prcred_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/cred", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; - return Py_BuildValue("iiiiii", - info.pr_ruid, info.pr_euid, info.pr_suid, - info.pr_rgid, info.pr_egid, info.pr_sgid); + return Py_BuildValue( + "iiiiii", + info.pr_ruid, + info.pr_euid, + info.pr_suid, + info.pr_rgid, + info.pr_egid, + info.pr_sgid + ); } @@ -377,10 +387,10 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { prusage_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; sprintf(path, "%s/%i/usage", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return Py_BuildValue("kk", info.pr_vctx, info.pr_ictx); } @@ -434,14 +444,14 @@ psutil_proc_query_thread(PyObject *self, PyObject *args) { lwpstatus_t info; const char *procfs_path; - if (! PyArg_ParseTuple(args, "iis", &pid, &tid, &procfs_path)) + if (!PyArg_ParseTuple(args, "iis", &pid, &tid, &procfs_path)) return NULL; sprintf(path, "%s/%i/lwp/%i/lwpstatus", procfs_path, pid, tid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; - return Py_BuildValue("dd", - PSUTIL_TV2DOUBLE(info.pr_utime), - PSUTIL_TV2DOUBLE(info.pr_stime)); + return Py_BuildValue( + "dd", PSUTIL_TV2DOUBLE(info.pr_utime), PSUTIL_TV2DOUBLE(info.pr_stime) + ); } @@ -472,11 +482,11 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) goto error; sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&status, sizeof(status))) + if (!psutil_file_to_struct(path, (void *)&status, sizeof(status))) goto error; sprintf(path, "%s/%i/xmap", procfs_path, pid); @@ -514,10 +524,14 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { pr_addr_sz = p->pr_vaddr + p->pr_size; // perms - sprintf(perms, "%c%c%c%c", p->pr_mflags & MA_READ ? 'r' : '-', - p->pr_mflags & MA_WRITE ? 'w' : '-', - p->pr_mflags & MA_EXEC ? 'x' : '-', - p->pr_mflags & MA_SHARED ? 's' : '-'); + sprintf( + perms, + "%c%c%c%c", + p->pr_mflags & MA_READ ? 'r' : '-', + p->pr_mflags & MA_WRITE ? 'w' : '-', + p->pr_mflags & MA_EXEC ? 'x' : '-', + p->pr_mflags & MA_SHARED ? 's' : '-' + ); // name if (strlen(p->pr_mapname) > 0) { @@ -531,13 +545,15 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { stk_base_sz = status.pr_stkbase + status.pr_stksize; brk_base_sz = status.pr_brkbase + status.pr_brksize; - if ((pr_addr_sz > status.pr_stkbase) && - (p->pr_vaddr < stk_base_sz)) { + if ((pr_addr_sz > status.pr_stkbase) + && (p->pr_vaddr < stk_base_sz)) + { name = "[stack]"; } - else if ((p->pr_mflags & MA_ANON) && \ - (pr_addr_sz > status.pr_brkbase) && \ - (p->pr_vaddr < brk_base_sz)) { + else if ((p->pr_mflags & MA_ANON) + && (pr_addr_sz > status.pr_brkbase) + && (p->pr_vaddr < brk_base_sz)) + { name = "[heap]"; } else { @@ -547,7 +563,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { } py_path = PyUnicode_DecodeFSDefault(name); - if (! py_path) + if (!py_path) goto error; py_tuple = Py_BuildValue( "kksOkkk", @@ -557,7 +573,8 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { py_path, (unsigned long)p->pr_rss * p->pr_pagesize, (unsigned long)p->pr_anon * p->pr_pagesize, - (unsigned long)p->pr_locked * p->pr_pagesize); + (unsigned long)p->pr_locked * p->pr_pagesize + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 6d70ab37cc..1723d843d7 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -27,13 +27,16 @@ psutil_get_num_cpus(int fail_on_err) { } } else { - psutil_debug("GetActiveProcessorCount() not available; " - "using GetSystemInfo()"); + psutil_debug( + "GetActiveProcessorCount() not available; " + "using GetSystemInfo()" + ); ncpus = (unsigned int)PSUTIL_SYSTEM_INFO.dwNumberOfProcessors; if ((ncpus <= 0) && (fail_on_err == 1)) { PyErr_SetString( PyExc_RuntimeError, - "GetSystemInfo() failed to retrieve CPU count"); + "GetSystemInfo() failed to retrieve CPU count" + ); } } return ncpus; @@ -55,12 +58,12 @@ psutil_cpu_times(PyObject *self, PyObject *args) { return NULL; } - idle = (double)((HI_T * idle_time.dwHighDateTime) + \ - (LO_T * idle_time.dwLowDateTime)); - user = (double)((HI_T * user_time.dwHighDateTime) + \ - (LO_T * user_time.dwLowDateTime)); - kernel = (double)((HI_T * kernel_time.dwHighDateTime) + \ - (LO_T * kernel_time.dwLowDateTime)); + idle = (double)((HI_T * idle_time.dwHighDateTime) + + (LO_T * idle_time.dwLowDateTime)); + user = (double)((HI_T * user_time.dwHighDateTime) + + (LO_T * user_time.dwLowDateTime)); + kernel = (double)((HI_T * kernel_time.dwHighDateTime) + + (LO_T * kernel_time.dwLowDateTime)); // Kernel time includes idle time. // We return only busy kernel time subtracting idle time from @@ -93,8 +96,9 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // allocates an array of _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION // structures, one per processor - sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *)malloc( + ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION) + ); if (sppi == NULL) { PyErr_NoMemory(); goto error; @@ -105,8 +109,9 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { SystemProcessorPerformanceInformation, sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { + NULL + ); + if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( status, "NtQuerySystemInformation(SystemProcessorPerformanceInformation)" @@ -119,28 +124,23 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { idle = user = kernel = interrupt = dpc = 0; for (i = 0; i < ncpus; i++) { py_tuple = NULL; - user = (double)((HI_T * sppi[i].UserTime.HighPart) + - (LO_T * sppi[i].UserTime.LowPart)); - idle = (double)((HI_T * sppi[i].IdleTime.HighPart) + - (LO_T * sppi[i].IdleTime.LowPart)); - kernel = (double)((HI_T * sppi[i].KernelTime.HighPart) + - (LO_T * sppi[i].KernelTime.LowPart)); - interrupt = (double)((HI_T * sppi[i].InterruptTime.HighPart) + - (LO_T * sppi[i].InterruptTime.LowPart)); - dpc = (double)((HI_T * sppi[i].DpcTime.HighPart) + - (LO_T * sppi[i].DpcTime.LowPart)); + user = (double)((HI_T * sppi[i].UserTime.HighPart) + + (LO_T * sppi[i].UserTime.LowPart)); + idle = (double)((HI_T * sppi[i].IdleTime.HighPart) + + (LO_T * sppi[i].IdleTime.LowPart)); + kernel = (double)((HI_T * sppi[i].KernelTime.HighPart) + + (LO_T * sppi[i].KernelTime.LowPart)); + interrupt = (double)((HI_T * sppi[i].InterruptTime.HighPart) + + (LO_T * sppi[i].InterruptTime.LowPart)); + dpc = (double)((HI_T * sppi[i].DpcTime.HighPart) + + (LO_T * sppi[i].DpcTime.LowPart)); // kernel time includes idle time on windows // we return only busy kernel time subtracting // idle time from kernel time systemt = kernel - idle; py_tuple = Py_BuildValue( - "(ddddd)", - user, - systemt, - idle, - interrupt, - dpc + "(ddddd)", user, systemt, idle, interrupt, dpc ); if (!py_tuple) goto error; @@ -200,23 +200,24 @@ psutil_cpu_count_cores(PyObject *self, PyObject *args) { } while (1) { - rc = GetLogicalProcessorInformationEx( - RelationAll, buffer, &length); + rc = GetLogicalProcessorInformationEx(RelationAll, buffer, &length); if (rc == FALSE) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { if (buffer) { free(buffer); } - buffer = \ - (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)malloc(length); + buffer = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX + )malloc(length); if (NULL == buffer) { PyErr_NoMemory(); return NULL; } } else { - psutil_debug("GetLogicalProcessorInformationEx() returned %u", - GetLastError()); + psutil_debug( + "GetLogicalProcessorInformationEx() returned %u", + GetLastError() + ); goto return_none; } } @@ -229,8 +230,8 @@ psutil_cpu_count_cores(PyObject *self, PyObject *args) { while (offset < length) { // Advance ptr by the size of the previous // SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX struct. - ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) \ - (((char*)ptr) + prev_processor_info_size); + ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX + *)(((char *)ptr) + prev_processor_info_size); if (ptr->Relationship == RelationProcessorCore) { ncpus += 1; @@ -278,8 +279,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { goto error; // get syscalls / ctx switches - spi = (_SYSTEM_PERFORMANCE_INFORMATION *) \ - malloc(ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION)); + spi = (_SYSTEM_PERFORMANCE_INFORMATION *)malloc( + ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION) + ); if (spi == NULL) { PyErr_NoMemory(); goto error; @@ -288,16 +290,19 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { SystemPerformanceInformation, spi, ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { + NULL + ); + if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( - status, "NtQuerySystemInformation(SystemPerformanceInformation)"); + status, "NtQuerySystemInformation(SystemPerformanceInformation)" + ); goto error; } // get DPCs - InterruptInformation = \ - malloc(sizeof(_SYSTEM_INTERRUPT_INFORMATION) * ncpus); + InterruptInformation = malloc( + sizeof(_SYSTEM_INTERRUPT_INFORMATION) * ncpus + ); if (InterruptInformation == NULL) { PyErr_NoMemory(); goto error; @@ -307,10 +312,12 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { SystemInterruptInformation, InterruptInformation, ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { + NULL + ); + if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( - status, "NtQuerySystemInformation(SystemInterruptInformation)"); + status, "NtQuerySystemInformation(SystemInterruptInformation)" + ); goto error; } for (i = 0; i < ncpus; i++) { @@ -318,8 +325,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } // get interrupts - sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *)malloc( + ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION) + ); if (sppi == NULL) { PyErr_NoMemory(); goto error; @@ -329,11 +337,13 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { SystemProcessorPerformanceInformation, sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), - NULL); - if (! NT_SUCCESS(status)) { + NULL + ); + if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( status, - "NtQuerySystemInformation(SystemProcessorPerformanceInformation)"); + "NtQuerySystemInformation(SystemProcessorPerformanceInformation)" + ); goto error; } @@ -384,18 +394,18 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { // Allocate size. size = ncpus * sizeof(PROCESSOR_POWER_INFORMATION); - pBuffer = (BYTE*)LocalAlloc(LPTR, size); - if (! pBuffer) { + pBuffer = (BYTE *)LocalAlloc(LPTR, size); + if (!pBuffer) { PyErr_SetFromWindowsErr(0); return NULL; } // Syscall. - ret = CallNtPowerInformation( - ProcessorInformation, NULL, 0, pBuffer, size); + ret = CallNtPowerInformation(ProcessorInformation, NULL, 0, pBuffer, size); if (ret != 0) { - PyErr_SetString(PyExc_RuntimeError, - "CallNtPowerInformation syscall failed"); + PyErr_SetString( + PyExc_RuntimeError, "CallNtPowerInformation syscall failed" + ); goto error; } diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 224f968260..fcb1775c61 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -12,10 +12,11 @@ #ifndef _ARRAYSIZE -#define _ARRAYSIZE(a) (sizeof(a)/sizeof(a[0])) +#define _ARRAYSIZE(a) (sizeof(a) / sizeof(a[0])) #endif -static char *psutil_get_drive_type(int type) { +static char * +psutil_get_drive_type(int type) { switch (type) { case DRIVE_FIXED: return "fixed"; @@ -63,7 +64,9 @@ psutil_disk_usage(PyObject *self, PyObject *args) { PyMem_Free(path); if (retval == 0) - return PyErr_SetExcFromWindowsErrWithFilenameObject(PyExc_OSError, 0, py_path); + return PyErr_SetExcFromWindowsErrWithFilenameObject( + PyExc_OSError, 0, py_path + ); return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); } @@ -96,8 +99,15 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { for (devNum = 0; devNum <= 32; ++devNum) { py_tuple = NULL; sprintf_s(szDevice, MAX_PATH, "\\\\.\\PhysicalDrive%d", devNum); - hDevice = CreateFile(szDevice, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, OPEN_EXISTING, 0, NULL); + hDevice = CreateFile( + szDevice, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + 0, + NULL + ); if (hDevice == INVALID_HANDLE_VALUE) continue; @@ -107,8 +117,15 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { while (1) { i += 1; ret = DeviceIoControl( - hDevice, IOCTL_DISK_PERFORMANCE, NULL, 0, &diskPerformance, - ioctrlSize, &dwSize, NULL); + hDevice, + IOCTL_DISK_PERFORMANCE, + NULL, + 0, + &diskPerformance, + ioctrlSize, + &dwSize, + NULL + ); if (ret != 0) break; // OK! if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { @@ -124,14 +141,20 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { // 1364/job/ascpdi271b06jle3 // Assume it means we're dealing with some exotic disk // and go on. - psutil_debug("DeviceIoControl -> ERROR_INVALID_FUNCTION; " - "ignore PhysicalDrive%i", devNum); + psutil_debug( + "DeviceIoControl -> ERROR_INVALID_FUNCTION; " + "ignore PhysicalDrive%i", + devNum + ); goto next; } else if (GetLastError() == ERROR_NOT_SUPPORTED) { // Again, let's assume we're dealing with some exotic disk. - psutil_debug("DeviceIoControl -> ERROR_NOT_SUPPORTED; " - "ignore PhysicalDrive%i", devNum); + psutil_debug( + "DeviceIoControl -> ERROR_NOT_SUPPORTED; " + "ignore PhysicalDrive%i", + devNum + ); goto next; } // XXX: it seems we should also catch ERROR_INVALID_PARAMETER: @@ -155,17 +178,16 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { diskPerformance.BytesWritten, // convert to ms: // https://github.com/giampaolo/psutil/issues/1012 - (unsigned long long) - (diskPerformance.ReadTime.QuadPart) / 10000000, - (unsigned long long) - (diskPerformance.WriteTime.QuadPart) / 10000000); + (unsigned long long)(diskPerformance.ReadTime.QuadPart) / 10000000, + (unsigned long long)(diskPerformance.WriteTime.QuadPart) / 10000000 + ); if (!py_tuple) goto error; if (PyDict_SetItemString(py_retdict, szDeviceDisplay, py_tuple)) goto error; Py_CLEAR(py_tuple); -next: + next: CloseHandle(hDevice); } @@ -197,8 +219,8 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { unsigned int old_mode = 0; char opts[50]; HANDLE mp_h; - BOOL mp_flag= TRUE; - LPTSTR fs_type[MAX_PATH + 1] = { 0 }; + BOOL mp_flag = TRUE; + LPTSTR fs_type[MAX_PATH + 1] = {0}; DWORD pflags = 0; DWORD lpMaximumComponentLength = 0; // max file name PyObject *py_all; @@ -213,7 +235,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { // see https://github.com/giampaolo/psutil/issues/264 old_mode = SetErrorMode(SEM_FAILCRITICALERRORS); - if (! PyArg_ParseTuple(args, "O", &py_all)) + if (!PyArg_ParseTuple(args, "O", &py_all)) goto error; all = PyObject_IsTrue(py_all); @@ -237,16 +259,16 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { // by default we only show hard drives and cd-roms if (all == 0) { - if ((type == DRIVE_UNKNOWN) || - (type == DRIVE_NO_ROOT_DIR) || - (type == DRIVE_REMOTE) || - (type == DRIVE_RAMDISK)) { + if ((type == DRIVE_UNKNOWN) || (type == DRIVE_NO_ROOT_DIR) + || (type == DRIVE_REMOTE) || (type == DRIVE_RAMDISK)) + { goto next; } // floppy disk: skip it by default as it introduces a // considerable slowdown. - if ((type == DRIVE_REMOVABLE) && - (strcmp(drive_letter, "A:\\") == 0)) { + if ((type == DRIVE_REMOVABLE) + && (strcmp(drive_letter, "A:\\") == 0)) + { goto next; } } @@ -259,7 +281,8 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { &lpMaximumComponentLength, &pflags, (LPTSTR)fs_type, - _ARRAYSIZE(fs_type)); + _ARRAYSIZE(fs_type) + ); if (ret == 0) { // We might get here in case of a floppy hard drive, in // which case the error is (21, "device not ready"). @@ -282,7 +305,8 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { // (checks first to know if we can even have mount points) if (pflags & FILE_SUPPORTS_REPARSE_POINTS) { mp_h = FindFirstVolumeMountPoint( - drive_letter, mp_buf, MAX_PATH); + drive_letter, mp_buf, MAX_PATH + ); if (mp_h != INVALID_HANDLE_VALUE) { mp_flag = TRUE; while (mp_flag) { @@ -294,12 +318,13 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { "(ssss)", drive_letter, mp_path, - fs_type, // typically "NTFS" + fs_type, // typically "NTFS" opts ); - if (!py_tuple || - PyList_Append(py_retlist, py_tuple) == -1) { + if (!py_tuple + || PyList_Append(py_retlist, py_tuple) == -1) + { FindVolumeMountPointClose(mp_h); goto error; } @@ -308,11 +333,11 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { // Continue looking for more mount points mp_flag = FindNextVolumeMountPoint( - mp_h, mp_buf, MAX_PATH); + mp_h, mp_buf, MAX_PATH + ); } FindVolumeMountPointClose(mp_h); } - } } @@ -334,7 +359,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { Py_CLEAR(py_tuple); goto next; -next: + next: drive_letter = strchr(drive_letter, 0) + 1; } diff --git a/psutil/arch/windows/init.c b/psutil/arch/windows/init.c index b410d5670f..b4f41d7b44 100644 --- a/psutil/arch/windows/init.c +++ b/psutil/arch/windows/init.c @@ -13,8 +13,8 @@ // Needed to make these globally visible. int PSUTIL_WINVER; -SYSTEM_INFO PSUTIL_SYSTEM_INFO; -CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; +SYSTEM_INFO PSUTIL_SYSTEM_INFO; +CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; // ==================================================================== @@ -32,12 +32,14 @@ PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { if (winerr == 0) winerr = GetLastError(); if (filename == NULL) { - py_exc = PyObject_CallFunction(PyExc_OSError, "(is)", winerr, - strerror(winerr)); + py_exc = PyObject_CallFunction( + PyExc_OSError, "(is)", winerr, strerror(winerr) + ); } else { - py_exc = PyObject_CallFunction(PyExc_OSError, "(iss)", winerr, - strerror(winerr), filename); + py_exc = PyObject_CallFunction( + PyExc_OSError, "(iss)", winerr, strerror(winerr), filename + ); } if (py_exc == NULL) return NULL; @@ -62,13 +64,13 @@ PyErr_SetFromWindowsErrWithFilename(int winerr, const char *filename) { #if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) PyObject * PyErr_SetExcFromWindowsErrWithFilenameObject( - PyObject *type, int ierr, PyObject *filename) -{ + PyObject *type, int ierr, PyObject *filename +) { // Original function is too complex. Just raise OSError without // filename. return PyErr_SetFromWindowsErrWithFilename(ierr, NULL); } -#endif // !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) +#endif // !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) #endif // defined(PYPY_VERSION) @@ -125,7 +127,7 @@ psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR apiname) { Py_BEGIN_ALLOW_THREADS mod = LoadLibraryA(libname); Py_END_ALLOW_THREADS - if (mod == NULL) { + if (mod == NULL) { psutil_debug("%s lib not supported (needed for %s)", libname, apiname); PyErr_SetFromWindowsErrWithFilename(0, libname); return NULL; @@ -158,7 +160,7 @@ _to_unix_time(ULONGLONG hiPart, ULONGLONG loPart) { // Change starting time to the Epoch (00:00:00 UTC, January 1, 1970). ret -= 116444736000000000ull; // Convert nano secs to secs. - return (double) ret / 10000000ull; + return (double)ret / 10000000ull; } @@ -172,9 +174,7 @@ psutil_FiletimeToUnixTime(FILETIME ft) { double psutil_LargeIntegerToUnixTime(LARGE_INTEGER li) { - return _to_unix_time( - (ULONGLONG)li.HighPart, (ULONGLONG)li.LowPart - ); + return _to_unix_time((ULONGLONG)li.HighPart, (ULONGLONG)li.LowPart); } @@ -187,80 +187,92 @@ static int psutil_loadlibs() { // --- Mandatory NtQuerySystemInformation = psutil_GetProcAddressFromLib( - "ntdll.dll", "NtQuerySystemInformation"); - if (! NtQuerySystemInformation) + "ntdll.dll", "NtQuerySystemInformation" + ); + if (!NtQuerySystemInformation) return -1; NtQueryInformationProcess = psutil_GetProcAddress( - "ntdll.dll", "NtQueryInformationProcess"); - if (! NtQueryInformationProcess) + "ntdll.dll", "NtQueryInformationProcess" + ); + if (!NtQueryInformationProcess) return -1; NtSetInformationProcess = psutil_GetProcAddress( - "ntdll.dll", "NtSetInformationProcess"); - if (! NtSetInformationProcess) + "ntdll.dll", "NtSetInformationProcess" + ); + if (!NtSetInformationProcess) return -1; - NtQueryObject = psutil_GetProcAddressFromLib( - "ntdll.dll", "NtQueryObject"); - if (! NtQueryObject) + NtQueryObject = psutil_GetProcAddressFromLib("ntdll.dll", "NtQueryObject"); + if (!NtQueryObject) return -1; RtlIpv4AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv4AddressToStringA"); - if (! RtlIpv4AddressToStringA) + "ntdll.dll", "RtlIpv4AddressToStringA" + ); + if (!RtlIpv4AddressToStringA) return -1; GetExtendedTcpTable = psutil_GetProcAddressFromLib( - "iphlpapi.dll", "GetExtendedTcpTable"); - if (! GetExtendedTcpTable) + "iphlpapi.dll", "GetExtendedTcpTable" + ); + if (!GetExtendedTcpTable) return -1; GetExtendedUdpTable = psutil_GetProcAddressFromLib( - "iphlpapi.dll", "GetExtendedUdpTable"); - if (! GetExtendedUdpTable) + "iphlpapi.dll", "GetExtendedUdpTable" + ); + if (!GetExtendedUdpTable) return -1; - RtlGetVersion = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlGetVersion"); - if (! RtlGetVersion) + RtlGetVersion = psutil_GetProcAddressFromLib("ntdll.dll", "RtlGetVersion"); + if (!RtlGetVersion) return -1; NtSuspendProcess = psutil_GetProcAddressFromLib( - "ntdll", "NtSuspendProcess"); - if (! NtSuspendProcess) + "ntdll", "NtSuspendProcess" + ); + if (!NtSuspendProcess) return -1; - NtResumeProcess = psutil_GetProcAddressFromLib( - "ntdll", "NtResumeProcess"); - if (! NtResumeProcess) + NtResumeProcess = psutil_GetProcAddressFromLib("ntdll", "NtResumeProcess"); + if (!NtResumeProcess) return -1; NtQueryVirtualMemory = psutil_GetProcAddressFromLib( - "ntdll", "NtQueryVirtualMemory"); - if (! NtQueryVirtualMemory) + "ntdll", "NtQueryVirtualMemory" + ); + if (!NtQueryVirtualMemory) return -1; RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( - "ntdll", "RtlNtStatusToDosErrorNoTeb"); - if (! RtlNtStatusToDosErrorNoTeb) + "ntdll", "RtlNtStatusToDosErrorNoTeb" + ); + if (!RtlNtStatusToDosErrorNoTeb) return -1; - GetTickCount64 = psutil_GetProcAddress( - "kernel32", "GetTickCount64"); - if (! GetTickCount64) + GetTickCount64 = psutil_GetProcAddress("kernel32", "GetTickCount64"); + if (!GetTickCount64) return -1; RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( - "ntdll.dll", "RtlIpv6AddressToStringA"); - if (! RtlIpv6AddressToStringA) + "ntdll.dll", "RtlIpv6AddressToStringA" + ); + if (!RtlIpv6AddressToStringA) return -1; // --- Optional // minimum requirement: Win 7 QueryInterruptTime = psutil_GetProcAddressFromLib( - "kernelbase.dll", "QueryInterruptTime"); + "kernelbase.dll", "QueryInterruptTime" + ); // minimum requirement: Win 7 GetActiveProcessorCount = psutil_GetProcAddress( - "kernel32", "GetActiveProcessorCount"); + "kernel32", "GetActiveProcessorCount" + ); // minimum requirement: Win 7 GetLogicalProcessorInformationEx = psutil_GetProcAddressFromLib( - "kernel32", "GetLogicalProcessorInformationEx"); + "kernel32", "GetLogicalProcessorInformationEx" + ); // minimum requirements: Windows Server Core WTSEnumerateSessionsW = psutil_GetProcAddressFromLib( - "wtsapi32.dll", "WTSEnumerateSessionsW"); + "wtsapi32.dll", "WTSEnumerateSessionsW" + ); WTSQuerySessionInformationW = psutil_GetProcAddressFromLib( - "wtsapi32.dll", "WTSQuerySessionInformationW"); + "wtsapi32.dll", "WTSQuerySessionInformationW" + ); WTSFreeMemory = psutil_GetProcAddressFromLib( - "wtsapi32.dll", "WTSFreeMemory"); + "wtsapi32.dll", "WTSFreeMemory" + ); PyErr_Clear(); return 0; diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index 3f2db7df9f..fcf928289b 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -12,8 +12,8 @@ extern int PSUTIL_WINVER; -extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; -extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; +extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; +extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define PSUTIL_WINDOWS_VISTA 60 #define PSUTIL_WINDOWS_7 61 @@ -34,31 +34,33 @@ extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define NT_NTWIN32(status) (_NT_FACILITY(status) == FACILITY_WIN32) #define WIN32_FROM_NTSTATUS(status) (((ULONG)(status)) & 0xffff) -#define PSUTIL_FIRST_PROCESS(Processes) ( \ - (PSYSTEM_PROCESS_INFORMATION)(Processes)) -#define PSUTIL_NEXT_PROCESS(Process) ( \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ - (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) +#define PSUTIL_FIRST_PROCESS(Processes) \ + ((PSYSTEM_PROCESS_INFORMATION)(Processes)) + +#define PSUTIL_NEXT_PROCESS(Process) \ + (((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset \ + ? (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) \ + + ((PSYSTEM_PROCESS_INFORMATION)(Process \ + )) \ + ->NextEntryOffset) \ + : NULL) #define LO_T 1e-7 #define HI_T 429.4967296 #ifndef AF_INET6 - #define AF_INET6 23 +#define AF_INET6 23 #endif #if defined(PSUTIL_WINDOWS) && defined(PYPY_VERSION) - #if !defined(PyErr_SetFromWindowsErrWithFilename) - PyObject *PyErr_SetFromWindowsErrWithFilename( - int ierr, const char *filename - ); - #endif - #if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) - PyObject *PyErr_SetExcFromWindowsErrWithFilenameObject( - PyObject *type, int ierr, PyObject *filename - ); - #endif +#if !defined(PyErr_SetFromWindowsErrWithFilename) +PyObject *PyErr_SetFromWindowsErrWithFilename(int ierr, const char *filename); +#endif +#if !defined(PyErr_SetExcFromWindowsErrWithFilenameObject) +PyObject *PyErr_SetExcFromWindowsErrWithFilenameObject( + PyObject *type, int ierr, PyObject *filename +); +#endif #endif double psutil_FiletimeToUnixTime(FILETIME ft); @@ -79,9 +81,14 @@ int psutil_assert_pid_exists(DWORD pid, char *err); int psutil_assert_pid_not_exists(DWORD pid, char *err); int psutil_pid_is_running(DWORD pid); int psutil_set_se_debug(); -SC_HANDLE psutil_get_service_handle(char service_name, DWORD scm_access, DWORD access); +SC_HANDLE psutil_get_service_handle( + char service_name, DWORD scm_access, DWORD access +); + +int psutil_get_proc_info( + DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer +); -int psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer); PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); @@ -102,7 +109,7 @@ PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_pid_exists(PyObject *self, PyObject *args); PyObject *psutil_pids(PyObject *self, PyObject *args); PyObject *psutil_ppid_map(PyObject *self, PyObject *args); -PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict); +PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kw); PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c index 03b85777d0..b792d11f06 100644 --- a/psutil/arch/windows/mem.c +++ b/psutil/arch/windows/mem.c @@ -25,7 +25,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { unsigned long long totalPhys, availPhys, totalSys, availSys, pageSize; PERFORMANCE_INFORMATION perfInfo; - if (! GetPerformanceInfo(&perfInfo, sizeof(PERFORMANCE_INFORMATION))) { + if (!GetPerformanceInfo(&perfInfo, sizeof(PERFORMANCE_INFORMATION))) { PyErr_SetFromWindowsErr(0); return NULL; } @@ -35,12 +35,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { availPhys = perfInfo.PhysicalAvailable * pageSize; totalSys = perfInfo.CommitLimit * pageSize; availSys = totalSys - perfInfo.CommitTotal * pageSize; - return Py_BuildValue( - "(LLLL)", - totalPhys, - availPhys, - totalSys, - availSys); + return Py_BuildValue("(LLLL)", totalPhys, availPhys, totalSys, availSys); } @@ -65,7 +60,8 @@ psutil_swap_percent(PyObject *self, PyObject *args) { PdhCloseQuery(hQuery); PyErr_Format( PyExc_RuntimeError, - "PdhAddEnglishCounterW failed. Performance counters may be disabled." + "PdhAddEnglishCounterW failed. Performance counters may be " + "disabled." ); return NULL; } @@ -78,11 +74,13 @@ psutil_swap_percent(PyObject *self, PyObject *args) { } else { s = PdhGetFormattedCounterValue( - (PDH_HCOUNTER)hCounter, PDH_FMT_DOUBLE, 0, &counterValue); + (PDH_HCOUNTER)hCounter, PDH_FMT_DOUBLE, 0, &counterValue + ); if (s != ERROR_SUCCESS) { PdhCloseQuery(hQuery); PyErr_Format( - PyExc_RuntimeError, "PdhGetFormattedCounterValue failed"); + PyExc_RuntimeError, "PdhGetFormattedCounterValue failed" + ); return NULL; } percentUsage = counterValue.doubleValue; diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index b9ca07dc0f..1b71a42192 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -21,10 +21,11 @@ psutil_get_nic_addresses(void) { PIP_ADAPTER_ADDRESSES buffer; if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, NULL, &bufferLength) - != ERROR_BUFFER_OVERFLOW) + != ERROR_BUFFER_OVERFLOW) { - PyErr_SetString(PyExc_RuntimeError, - "GetAdaptersAddresses() syscall failed."); + PyErr_SetString( + PyExc_RuntimeError, "GetAdaptersAddresses() syscall failed." + ); return NULL; } @@ -36,11 +37,12 @@ psutil_get_nic_addresses(void) { memset(buffer, 0, bufferLength); if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, buffer, &bufferLength) - != ERROR_SUCCESS) + != ERROR_SUCCESS) { free(buffer); - PyErr_SetString(PyExc_RuntimeError, - "GetAdaptersAddresses() syscall failed."); + PyErr_SetString( + PyExc_RuntimeError, "GetAdaptersAddresses() syscall failed." + ); return NULL; } @@ -72,7 +74,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { py_nic_name = NULL; py_nic_info = NULL; - pIfRow = (MIB_IF_ROW2 *) malloc(sizeof(MIB_IF_ROW2)); + pIfRow = (MIB_IF_ROW2 *)malloc(sizeof(MIB_IF_ROW2)); if (pIfRow == NULL) { PyErr_NoMemory(); goto error; @@ -82,8 +84,10 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { pIfRow->InterfaceIndex = pCurrAddresses->IfIndex; dwRetVal = GetIfEntry2(pIfRow); if (dwRetVal != NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, - "GetIfEntry() or GetIfEntry2() syscalls failed."); + PyErr_SetString( + PyExc_RuntimeError, + "GetIfEntry() or GetIfEntry2() syscalls failed." + ); goto error; } @@ -96,13 +100,14 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { pIfRow->InErrors, pIfRow->OutErrors, pIfRow->InDiscards, - pIfRow->OutDiscards); + pIfRow->OutDiscards + ); if (!py_nic_info) goto error; py_nic_name = PyUnicode_FromWideChar( - pCurrAddresses->FriendlyName, - wcslen(pCurrAddresses->FriendlyName)); + pCurrAddresses->FriendlyName, wcslen(pCurrAddresses->FriendlyName) + ); if (py_nic_name == NULL) goto error; @@ -172,8 +177,8 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { netmaskIntRet = NULL; py_nic_name = NULL; py_nic_name = PyUnicode_FromWideChar( - pCurrAddresses->FriendlyName, - wcslen(pCurrAddresses->FriendlyName)); + pCurrAddresses->FriendlyName, wcslen(pCurrAddresses->FriendlyName) + ); if (py_nic_name == NULL) goto error; @@ -181,14 +186,22 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { if (pCurrAddresses->PhysicalAddressLength != 0) { ptr = buff_macaddr; *ptr = '\0'; - for (i = 0; i < (int) pCurrAddresses->PhysicalAddressLength; i++) { + for (i = 0; i < (int)pCurrAddresses->PhysicalAddressLength; i++) { if (i == (pCurrAddresses->PhysicalAddressLength - 1)) { - sprintf_s(ptr, _countof(buff_macaddr), "%.2X\n", - (int)pCurrAddresses->PhysicalAddress[i]); + sprintf_s( + ptr, + _countof(buff_macaddr), + "%.2X\n", + (int)pCurrAddresses->PhysicalAddress[i] + ); } else { - sprintf_s(ptr, _countof(buff_macaddr), "%.2X-", - (int)pCurrAddresses->PhysicalAddress[i]); + sprintf_s( + ptr, + _countof(buff_macaddr), + "%.2X-", + (int)pCurrAddresses->PhysicalAddress[i] + ); } ptr += 3; } @@ -210,7 +223,7 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { Py_None, // broadcast (not supported) Py_None // ptp (not supported on Windows) ); - if (! py_tuple) + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; @@ -223,29 +236,42 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { for (i = 0; pUnicast != NULL; i++) { family = pUnicast->Address.lpSockaddr->sa_family; if (family == AF_INET) { - struct sockaddr_in *sa_in = (struct sockaddr_in *) - pUnicast->Address.lpSockaddr; - intRet = inet_ntop(AF_INET, &(sa_in->sin_addr), buff_addr, - sizeof(buff_addr)); + struct sockaddr_in *sa_in = (struct sockaddr_in *)pUnicast + ->Address.lpSockaddr; + intRet = inet_ntop( + AF_INET, + &(sa_in->sin_addr), + buff_addr, + sizeof(buff_addr) + ); if (!intRet) goto error; netmask_bits = pUnicast->OnLinkPrefixLength; dwRetVal = ConvertLengthToIpv4Mask( - netmask_bits, &converted_netmask); + netmask_bits, &converted_netmask + ); if (dwRetVal == NO_ERROR) { in_netmask.s_addr = converted_netmask; netmaskIntRet = inet_ntop( - AF_INET, &in_netmask, buff_netmask, - sizeof(buff_netmask)); + AF_INET, + &in_netmask, + buff_netmask, + sizeof(buff_netmask) + ); if (!netmaskIntRet) goto error; } } else if (family == AF_INET6) { struct sockaddr_in6 *sa_in6 = (struct sockaddr_in6 *) - pUnicast->Address.lpSockaddr; - intRet = inet_ntop(AF_INET6, &(sa_in6->sin6_addr), - buff_addr, sizeof(buff_addr)); + pUnicast->Address + .lpSockaddr; + intRet = inet_ntop( + AF_INET6, + &(sa_in6->sin6_addr), + buff_addr, + sizeof(buff_addr) + ); if (!intRet) goto error; } @@ -279,7 +305,7 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { Py_None // ptp (not supported on Windows) ); - if (! py_tuple) + if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; @@ -337,7 +363,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { if (pAddresses == NULL) goto error; - pIfTable = (MIB_IFTABLE *) malloc(sizeof (MIB_IFTABLE)); + pIfTable = (MIB_IFTABLE *)malloc(sizeof(MIB_IFTABLE)); if (pIfTable == NULL) { PyErr_NoMemory(); goto error; @@ -345,7 +371,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { dwSize = sizeof(MIB_IFTABLE); if (GetIfTable(pIfTable, &dwSize, FALSE) == ERROR_INSUFFICIENT_BUFFER) { free(pIfTable); - pIfTable = (MIB_IFTABLE *) malloc(dwSize); + pIfTable = (MIB_IFTABLE *)malloc(dwSize); if (pIfTable == NULL) { PyErr_NoMemory(); goto error; @@ -358,8 +384,8 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { goto error; } - for (i = 0; i < (int) pIfTable->dwNumEntries; i++) { - pIfRow = (MIB_IFROW *) & pIfTable->table[i]; + for (i = 0; i < (int)pIfTable->dwNumEntries; i++) { + pIfRow = (MIB_IFROW *)&pIfTable->table[i]; // GetIfTable is not able to give us NIC with "friendly names" // so we determine them via GetAdapterAddresses() which @@ -372,7 +398,8 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { if (lstrcmp(descr, pIfRow->bDescr) == 0) { py_nic_name = PyUnicode_FromWideChar( pCurrAddresses->FriendlyName, - wcslen(pCurrAddresses->FriendlyName)); + wcslen(pCurrAddresses->FriendlyName) + ); if (py_nic_name == NULL) goto error; ifname_found = 1; @@ -388,9 +415,10 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { } // is up? - if ((pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_CONNECTED || - pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_OPERATIONAL) && - pIfRow->dwAdminStatus == 1 ) { + if ((pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_CONNECTED + || pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_OPERATIONAL) + && pIfRow->dwAdminStatus == 1) + { py_is_up = Py_True; } else { diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 9a1876de43..e42b81d47a 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -5,6 +5,7 @@ * Define Windows structs and constants which are considered private. */ +// clang-format off #if !defined(__NTEXTAPI_H__) #define __NTEXTAPI_H__ #include @@ -711,3 +712,4 @@ ULONG (WINAPI *_RtlNtStatusToDosErrorNoTeb) ( #define RtlNtStatusToDosErrorNoTeb _RtlNtStatusToDosErrorNoTeb #endif // __NTEXTAPI_H__ +// clang-format on diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index c62897bb41..451cf072a9 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -9,7 +9,7 @@ * psutil/_psutil_windows.c in 2023. For reference, here's the GIT blame * history before the move: * https://github.com/giampaolo/psutil/blame/59504a5/psutil/_psutil_windows.c -*/ + */ // Fixes clash between winsock2.h and windows.h #define WIN32_LEAN_AND_MEAN @@ -39,12 +39,12 @@ psutil_pid_exists(PyObject *self, PyObject *args) { DWORD pid; int status; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; status = psutil_pid_is_running(pid); if (-1 == status) - return NULL; // exception raised in psutil_pid_is_running() + return NULL; // exception raised in psutil_pid_is_running() return PyBool_FromLong(status); } @@ -58,7 +58,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { DWORD pid; DWORD access = PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (pid == 0) return AccessDenied("automatically set for PID 0"); @@ -68,7 +68,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { return NULL; } - if (! TerminateProcess(hProcess, SIGTERM)) { + if (!TerminateProcess(hProcess, SIGTERM)) { // ERROR_ACCESS_DENIED may happen if the process already died. See: // https://github.com/giampaolo/psutil/issues/1099 // http://bugs.python.org/issue14252 @@ -94,13 +94,14 @@ psutil_proc_wait(PyObject *self, PyObject *args) { DWORD pid; long timeout; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "l", &pid, &timeout)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "l", &pid, &timeout)) return NULL; if (pid == 0) return AccessDenied("automatically set for PID 0"); - hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, - FALSE, pid); + hProcess = OpenProcess( + SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, pid + ); if (hProcess == NULL) { if (GetLastError() == ERROR_INVALID_PARAMETER) { // no such process; we do not want to raise NSP but @@ -125,15 +126,17 @@ psutil_proc_wait(PyObject *self, PyObject *args) { return NULL; } if (retVal == WAIT_TIMEOUT) { - PyErr_SetString(TimeoutExpired, - "WaitForSingleObject() returned WAIT_TIMEOUT"); + PyErr_SetString( + TimeoutExpired, "WaitForSingleObject() returned WAIT_TIMEOUT" + ); CloseHandle(hProcess); return NULL; } if (retVal == WAIT_ABANDONED) { psutil_debug("WaitForSingleObject() -> WAIT_ABANDONED"); - PyErr_SetString(TimeoutAbandoned, - "WaitForSingleObject() returned WAIT_ABANDONED"); + PyErr_SetString( + TimeoutAbandoned, "WaitForSingleObject() returned WAIT_ABANDONED" + ); CloseHandle(hProcess); return NULL; } @@ -149,7 +152,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { CloseHandle(hProcess); - return PyLong_FromLong((long) ExitCode); + return PyLong_FromLong((long)ExitCode); } @@ -158,18 +161,18 @@ psutil_proc_wait(PyObject *self, PyObject *args) { */ PyObject * psutil_proc_times(PyObject *self, PyObject *args) { - DWORD pid; - HANDLE hProcess; - FILETIME ftCreate, ftExit, ftKernel, ftUser; + DWORD pid; + HANDLE hProcess; + FILETIME ftCreate, ftExit, ftKernel, ftUser; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; - if (! GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { + if (!GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser)) { if (GetLastError() == ERROR_ACCESS_DENIED) { // usually means the process has died so we throw a NoSuchProcess // here @@ -194,13 +197,12 @@ psutil_proc_times(PyObject *self, PyObject *args) { * below from Python's Modules/posixmodule.c */ return Py_BuildValue( - "(ddd)", - (double)(ftUser.dwHighDateTime * HI_T + \ - ftUser.dwLowDateTime * LO_T), - (double)(ftKernel.dwHighDateTime * HI_T + \ - ftKernel.dwLowDateTime * LO_T), - psutil_FiletimeToUnixTime(ftCreate) - ); + "(ddd)", + (double)(ftUser.dwHighDateTime * HI_T + ftUser.dwLowDateTime * LO_T), + (double)(ftKernel.dwHighDateTime * HI_T + ftKernel.dwLowDateTime * LO_T + ), + psutil_FiletimeToUnixTime(ftCreate) + ); } @@ -215,11 +217,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { DWORD pid; NTSTATUS status; PVOID buffer = NULL; - ULONG bufferSize = 0x104 * 2; // WIN_MAX_PATH * sizeof(wchar_t) + ULONG bufferSize = 0x104 * 2; // WIN_MAX_PATH * sizeof(wchar_t) SYSTEM_PROCESS_ID_INFORMATION processIdInfo; PyObject *py_exe; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (pid == 0) @@ -231,7 +233,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { return NoSuchProcess("psutil_pid_is_running -> 0"); buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { + if (!buffer) { PyErr_NoMemory(); return NULL; } @@ -245,10 +247,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { SystemProcessIdInformation, &processIdInfo, sizeof(SYSTEM_PROCESS_ID_INFORMATION), - NULL); + NULL + ); - if ((status == STATUS_INFO_LENGTH_MISMATCH) && - (processIdInfo.ImageName.MaximumLength <= bufferSize)) + if ((status == STATUS_INFO_LENGTH_MISMATCH) + && (processIdInfo.ImageName.MaximumLength <= bufferSize)) { // Required length was NOT stored in MaximumLength (WOW64 issue). ULONG maxBufferSize = 0x7FFF * 2; // NTFS_MAX_PATH * sizeof(wchar_t) @@ -257,7 +260,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { bufferSize *= 2; FREE(buffer); buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { + if (!buffer) { PyErr_NoMemory(); return NULL; } @@ -269,15 +272,16 @@ psutil_proc_exe(PyObject *self, PyObject *args) { SystemProcessIdInformation, &processIdInfo, sizeof(SYSTEM_PROCESS_ID_INFORMATION), - NULL); - } while ((status == STATUS_INFO_LENGTH_MISMATCH) && - (bufferSize <= maxBufferSize)); + NULL + ); + } while ((status == STATUS_INFO_LENGTH_MISMATCH) + && (bufferSize <= maxBufferSize)); } else if (status == STATUS_INFO_LENGTH_MISMATCH) { // Required length is stored in MaximumLength. FREE(buffer); buffer = MALLOC_ZERO(processIdInfo.ImageName.MaximumLength); - if (! buffer) { + if (!buffer) { PyErr_NoMemory(); return NULL; } @@ -288,10 +292,11 @@ psutil_proc_exe(PyObject *self, PyObject *args) { SystemProcessIdInformation, &processIdInfo, sizeof(SYSTEM_PROCESS_ID_INFORMATION), - NULL); + NULL + ); } - if (! NT_SUCCESS(status)) { + if (!NT_SUCCESS(status)) { FREE(buffer); if (psutil_pid_is_running(pid) == 0) NoSuchProcess("psutil_pid_is_running -> 0"); @@ -305,8 +310,9 @@ psutil_proc_exe(PyObject *self, PyObject *args) { py_exe = Py_BuildValue("s", ""); } else { - py_exe = PyUnicode_FromWideChar(processIdInfo.ImageName.Buffer, - processIdInfo.ImageName.Length / 2); + py_exe = PyUnicode_FromWideChar( + processIdInfo.ImageName.Buffer, processIdInfo.ImageName.Length / 2 + ); } FREE(buffer); return py_exe; @@ -322,15 +328,17 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { DWORD pid; PROCESS_MEMORY_COUNTERS_EX cnt; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; - if (! GetProcessMemoryInfo(hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, - sizeof(cnt))) { + if (!GetProcessMemoryInfo( + hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, sizeof(cnt) + )) + { PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); return NULL; @@ -353,11 +361,12 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { (unsigned long long)cnt.QuotaNonPagedPoolUsage, (unsigned long long)cnt.PagefileUsage, (unsigned long long)cnt.PeakPagefileUsage, - (unsigned long long)cnt.PrivateUsage); + (unsigned long long)cnt.PrivateUsage + ); #else return Py_BuildValue( "(kIIIIIIIII)", - cnt.PageFaultCount, // unsigned long + cnt.PageFaultCount, // unsigned long (unsigned int)cnt.PeakWorkingSetSize, (unsigned int)cnt.WorkingSetSize, (unsigned int)cnt.QuotaPeakPagedPoolUsage, @@ -366,46 +375,48 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { (unsigned int)cnt.QuotaNonPagedPoolUsage, (unsigned int)cnt.PagefileUsage, (unsigned int)cnt.PeakPagefileUsage, - (unsigned int)cnt.PrivateUsage); + (unsigned int)cnt.PrivateUsage + ); #endif } static int psutil_GetProcWsetInformation( - DWORD pid, - HANDLE hProcess, - PMEMORY_WORKING_SET_INFORMATION *wSetInfo) -{ + DWORD pid, HANDLE hProcess, PMEMORY_WORKING_SET_INFORMATION *wSetInfo +) { NTSTATUS status; PVOID buffer; SIZE_T bufferSize; bufferSize = 0x8000; buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { + if (!buffer) { PyErr_NoMemory(); return -1; } while ((status = NtQueryVirtualMemory( - hProcess, - NULL, - MemoryWorkingSetInformation, - buffer, - bufferSize, - NULL)) == STATUS_INFO_LENGTH_MISMATCH) + hProcess, + NULL, + MemoryWorkingSetInformation, + buffer, + bufferSize, + NULL + )) + == STATUS_INFO_LENGTH_MISMATCH) { FREE(buffer); bufferSize *= 2; // Fail if we're resizing the buffer to something very large. if (bufferSize > 256 * 1024 * 1024) { - PyErr_SetString(PyExc_RuntimeError, - "NtQueryVirtualMemory bufsize is too large"); + PyErr_SetString( + PyExc_RuntimeError, "NtQueryVirtualMemory bufsize is too large" + ); return -1; } buffer = MALLOC_ZERO(bufferSize); - if (! buffer) { + if (!buffer) { PyErr_NoMemory(); return -1; } @@ -421,7 +432,8 @@ psutil_GetProcWsetInformation( else { PyErr_Clear(); psutil_SetFromNTStatusErr( - status, "NtQueryVirtualMemory(MemoryWorkingSetInformation)"); + status, "NtQueryVirtualMemory(MemoryWorkingSetInformation)" + ); } HeapFree(GetProcessHeap(), 0, buffer); return -1; @@ -446,7 +458,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { PMEMORY_WORKING_SET_INFORMATION wsInfo; ULONG_PTR i; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION); if (hProcess == NULL) @@ -472,8 +484,9 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { // This is what we do: count shared pages that only one process // is using as private (USS). - if (!wsInfo->WorkingSetInfo[i].Shared || - wsInfo->WorkingSetInfo[i].ShareCount <= 1) { + if (!wsInfo->WorkingSetInfo[i].Shared + || wsInfo->WorkingSetInfo[i].ShareCount <= 1) + { wsCounters.NumberOfPrivatePages++; } } @@ -494,9 +507,9 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { NTSTATUS status; HANDLE hProcess; DWORD access = PROCESS_SUSPEND_RESUME | PROCESS_QUERY_LIMITED_INFORMATION; - PyObject* suspend; + PyObject *suspend; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) return NULL; hProcess = psutil_handle_from_pid(pid, access); @@ -508,7 +521,7 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { else status = NtResumeProcess(hProcess); - if (! NT_SUCCESS(status)) { + if (!NT_SUCCESS(status)) { CloseHandle(hProcess); return psutil_SetFromNTStatusErr(status, "NtSuspend|ResumeProcess"); } @@ -532,7 +545,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; if (pid == 0) { // raise AD instead of returning 0 as procexp is able to @@ -558,7 +571,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // Fill in the size of the structure before using it te32.dwSize = sizeof(THREADENTRY32); - if (! Thread32First(hThreadSnap, &te32)) { + if (!Thread32First(hThreadSnap, &te32)) { psutil_PyErr_SetFromOSErrnoWithSyscall("Thread32First"); goto error; } @@ -569,15 +582,17 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (te32.th32OwnerProcessID == pid) { py_tuple = NULL; hThread = NULL; - hThread = OpenThread(THREAD_QUERY_INFORMATION, - FALSE, te32.th32ThreadID); + hThread = OpenThread( + THREAD_QUERY_INFORMATION, FALSE, te32.th32ThreadID + ); if (hThread == NULL) { // thread has disappeared on us continue; } - rc = GetThreadTimes(hThread, &ftDummy, &ftDummy, &ftKernel, - &ftUser); + rc = GetThreadTimes( + hThread, &ftDummy, &ftDummy, &ftKernel, &ftUser + ); if (rc == 0) { psutil_PyErr_SetFromOSErrnoWithSyscall("GetThreadTimes"); goto error; @@ -595,10 +610,11 @@ psutil_proc_threads(PyObject *self, PyObject *args) { py_tuple = Py_BuildValue( "kdd", te32.th32ThreadID, - (double)(ftUser.dwHighDateTime * HI_T + \ - ftUser.dwLowDateTime * LO_T), - (double)(ftKernel.dwHighDateTime * HI_T + \ - ftKernel.dwLowDateTime * LO_T)); + (double)(ftUser.dwHighDateTime * HI_T + + ftUser.dwLowDateTime * LO_T), + (double)(ftKernel.dwHighDateTime * HI_T + + ftKernel.dwLowDateTime * LO_T) + ); if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) @@ -630,7 +646,7 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { DWORD access = PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION; PyObject *py_retlist; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; processHandle = psutil_handle_from_pid(pid, access); @@ -666,8 +682,9 @@ _psutil_user_token_from_pid(DWORD pid) { PyErr_NoMemory(); goto error; } - if (!GetTokenInformation(hToken, TokenUser, userToken, bufferSize, - &bufferSize)) + if (!GetTokenInformation( + hToken, TokenUser, userToken, bufferSize, &bufferSize + )) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { free(userToken); @@ -710,7 +727,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { PyObject *py_domain = NULL; PyObject *py_tuple = NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; userToken = _psutil_user_token_from_pid(pid); if (userToken == NULL) @@ -728,8 +745,15 @@ psutil_proc_username(PyObject *self, PyObject *args) { PyErr_NoMemory(); goto error; } - if (!LookupAccountSidW(NULL, userToken->User.Sid, userName, &nameSize, - domainName, &domainNameSize, &nameUse)) + if (!LookupAccountSidW( + NULL, + userToken->User.Sid, + userName, + &nameSize, + domainName, + &domainNameSize, + &nameUse + )) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { free(userName); @@ -758,13 +782,13 @@ psutil_proc_username(PyObject *self, PyObject *args) { } py_domain = PyUnicode_FromWideChar(domainName, wcslen(domainName)); - if (! py_domain) + if (!py_domain) goto error; py_username = PyUnicode_FromWideChar(userName, wcslen(userName)); - if (! py_username) + if (!py_username) goto error; py_tuple = Py_BuildValue("OO", py_domain, py_username); - if (! py_tuple) + if (!py_tuple) goto error; Py_DECREF(py_domain); Py_DECREF(py_username); @@ -797,7 +821,7 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { DWORD priority; HANDLE hProcess; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -826,7 +850,7 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &priority)) return NULL; hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) @@ -854,7 +878,7 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { DWORD IoPriority; NTSTATUS status; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); @@ -862,15 +886,11 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { return NULL; status = NtQueryInformationProcess( - hProcess, - ProcessIoPriority, - &IoPriority, - sizeof(DWORD), - NULL + hProcess, ProcessIoPriority, &IoPriority, sizeof(DWORD), NULL ); CloseHandle(hProcess); - if (! NT_SUCCESS(status)) + if (!NT_SUCCESS(status)) return psutil_SetFromNTStatusErr(status, "NtQueryInformationProcess"); return Py_BuildValue("i", IoPriority); } @@ -887,7 +907,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { NTSTATUS status; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &prio)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &prio)) return NULL; hProcess = psutil_handle_from_pid(pid, access); @@ -895,14 +915,11 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { return NULL; status = NtSetInformationProcess( - hProcess, - ProcessIoPriority, - (PVOID)&prio, - sizeof(DWORD) + hProcess, ProcessIoPriority, (PVOID)&prio, sizeof(DWORD) ); CloseHandle(hProcess); - if (! NT_SUCCESS(status)) + if (!NT_SUCCESS(status)) return psutil_SetFromNTStatusErr(status, "NtSetInformationProcess"); Py_RETURN_NONE; } @@ -917,26 +934,28 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { HANDLE hProcess; IO_COUNTERS IoCounters; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; - if (! GetProcessIoCounters(hProcess, &IoCounters)) { + if (!GetProcessIoCounters(hProcess, &IoCounters)) { PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); return NULL; } CloseHandle(hProcess); - return Py_BuildValue("(KKKKKK)", - IoCounters.ReadOperationCount, - IoCounters.WriteOperationCount, - IoCounters.ReadTransferCount, - IoCounters.WriteTransferCount, - IoCounters.OtherOperationCount, - IoCounters.OtherTransferCount); + return Py_BuildValue( + "(KKKKKK)", + IoCounters.ReadOperationCount, + IoCounters.WriteOperationCount, + IoCounters.ReadTransferCount, + IoCounters.WriteTransferCount, + IoCounters.OtherOperationCount, + IoCounters.OtherTransferCount + ); } @@ -950,7 +969,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { DWORD_PTR proc_mask; DWORD_PTR system_mask; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) { @@ -982,9 +1001,9 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { DWORD_PTR mask; #ifdef _WIN64 - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "K", &pid, &mask)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "K", &pid, &mask)) #else - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "k", &pid, &mask)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "k", &pid, &mask)) #endif { return NULL; @@ -1014,13 +1033,13 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { PSYSTEM_PROCESS_INFORMATION process; PVOID buffer; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_get_proc_info(pid, &process, &buffer) != 0) return NULL; for (i = 0; i < process->NumberOfThreads; i++) { - if (process->Threads[i].ThreadState != Waiting || - process->Threads[i].WaitReason != Suspended) + if (process->Threads[i].ThreadState != Waiting + || process->Threads[i].WaitReason != Suspended) { free(buffer); Py_RETURN_FALSE; @@ -1040,12 +1059,12 @@ psutil_proc_num_handles(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD handleCount; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) return NULL; - if (! GetProcessHandleCount(hProcess, &handleCount)) { + if (!GetProcessHandleCount(hProcess, &handleCount)) { PyErr_SetFromWindowsErr(0); CloseHandle(hProcess); return NULL; @@ -1055,7 +1074,8 @@ psutil_proc_num_handles(PyObject *self, PyObject *args) { } -static char *get_region_protection_string(ULONG protection) { +static char * +get_region_protection_string(ULONG protection) { switch (protection & 0xff) { case PAGE_NOACCESS: return ""; @@ -1098,7 +1118,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) goto error; hProcess = psutil_handle_from_pid(pid, access); if (NULL == hProcess) @@ -1107,31 +1127,35 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { maxAddr = PSUTIL_SYSTEM_INFO.lpMaximumApplicationAddress; baseAddress = NULL; - while (VirtualQueryEx(hProcess, baseAddress, &basicInfo, - sizeof(MEMORY_BASIC_INFORMATION))) + while (VirtualQueryEx( + hProcess, baseAddress, &basicInfo, sizeof(MEMORY_BASIC_INFORMATION) + )) { py_tuple = NULL; if (baseAddress > maxAddr) break; - if (GetMappedFileNameW(hProcess, baseAddress, mappedFileName, - sizeof(mappedFileName))) + if (GetMappedFileNameW( + hProcess, baseAddress, mappedFileName, sizeof(mappedFileName) + )) { - py_str = PyUnicode_FromWideChar(mappedFileName, - wcslen(mappedFileName)); + py_str = PyUnicode_FromWideChar( + mappedFileName, wcslen(mappedFileName) + ); if (py_str == NULL) goto error; #ifdef _WIN64 - py_tuple = Py_BuildValue( - "(KsOI)", - (unsigned long long)baseAddress, + py_tuple = Py_BuildValue( + "(KsOI)", + (unsigned long long)baseAddress, #else - py_tuple = Py_BuildValue( - "(ksOI)", - (unsigned long)baseAddress, + py_tuple = Py_BuildValue( + "(ksOI)", + (unsigned long)baseAddress, #endif - get_region_protection_string(basicInfo.Protect), - py_str, - basicInfo.RegionSize); + get_region_protection_string(basicInfo.Protect), + py_str, + basicInfo.RegionSize + ); if (!py_tuple) goto error; diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 471944c643..09b0ee0779 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -45,11 +45,9 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { } while ((status = NtQuerySystemInformation( - SystemExtendedHandleInformation, - buffer, - bufferSize, - NULL - )) == STATUS_INFO_LENGTH_MISMATCH) + SystemExtendedHandleInformation, buffer, bufferSize, NULL + )) + == STATUS_INFO_LENGTH_MISMATCH) { FREE(buffer); bufferSize *= 2; @@ -58,7 +56,8 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { if (bufferSize > 256 * 1024 * 1024) { PyErr_SetString( PyExc_RuntimeError, - "SystemExtendedHandleInformation buffer too big"); + "SystemExtendedHandleInformation buffer too big" + ); return -1; } @@ -69,7 +68,7 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { } } - if (! NT_SUCCESS(status)) { + if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); FREE(buffer); return -1; @@ -82,7 +81,7 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { static int psutil_get_filename(LPVOID lpvParam) { - HANDLE hFile = *((HANDLE*)lpvParam); + HANDLE hFile = *((HANDLE *)lpvParam); NTSTATUS status; ULONG bufferSize; ULONG attempts = 8; @@ -111,10 +110,10 @@ psutil_get_filename(LPVOID lpvParam) { globalFileName, bufferSize, &bufferSize - ); - if (status == STATUS_BUFFER_OVERFLOW || - status == STATUS_INFO_LENGTH_MISMATCH || - status == STATUS_BUFFER_TOO_SMALL) + ); + if (status == STATUS_BUFFER_OVERFLOW + || status == STATUS_INFO_LENGTH_MISMATCH + || status == STATUS_BUFFER_TOO_SMALL) { FREE(globalFileName); globalFileName = MALLOC_ZERO(bufferSize); @@ -128,7 +127,7 @@ psutil_get_filename(LPVOID lpvParam) { } } while (--attempts); - if (! NT_SUCCESS(status)) { + if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); FREE(globalFileName); globalFileName = NULL; @@ -153,7 +152,8 @@ psutil_threaded_get_filename(HANDLE hFile) { DWORD threadRetValue; hThread = CreateThread( - NULL, 0, (LPTHREAD_START_ROUTINE)psutil_get_filename, &hFile, 0, NULL); + NULL, 0, (LPTHREAD_START_ROUTINE)psutil_get_filename, &hFile, 0, NULL + ); if (hThread == NULL) { psutil_PyErr_SetFromOSErrnoWithSyscall("CreateThread"); return -1; @@ -165,7 +165,8 @@ psutil_threaded_get_filename(HANDLE hFile) { // If the thread hangs, kill it and cleanup. if (dwWait == WAIT_TIMEOUT) { psutil_debug( - "get handle name thread timed out after %i ms", THREAD_TIMEOUT); + "get handle name thread timed out after %i ms", THREAD_TIMEOUT + ); if (TerminateThread(hThread, 0) == 0) { psutil_PyErr_SetFromOSErrnoWithSyscall("TerminateThread"); CloseHandle(hThread); @@ -209,13 +210,14 @@ psutil_threaded_get_filename(HANDLE hFile) { PyObject * psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { - PSYSTEM_HANDLE_INFORMATION_EX handlesList = NULL; - PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX hHandle = NULL; - HANDLE hFile = NULL; - ULONG i = 0; - BOOLEAN errorOccurred = FALSE; - PyObject* py_path = NULL; - PyObject* py_retlist = PyList_New(0);; + PSYSTEM_HANDLE_INFORMATION_EX handlesList = NULL; + PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX hHandle = NULL; + HANDLE hFile = NULL; + ULONG i = 0; + BOOLEAN errorOccurred = FALSE; + PyObject *py_path = NULL; + PyObject *py_retlist = PyList_New(0); + ; if (!py_retlist) return NULL; @@ -231,14 +233,15 @@ psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { hHandle = &handlesList->Handles[i]; if ((ULONG_PTR)hHandle->UniqueProcessId != dwPid) continue; - if (! DuplicateHandle( + if (!DuplicateHandle( hProcess, hHandle->HandleValue, GetCurrentProcess(), &hFile, 0, TRUE, - DUPLICATE_SAME_ACCESS)) + DUPLICATE_SAME_ACCESS + )) { // Will fail if not a regular file; just skip it. continue; @@ -249,9 +252,10 @@ psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { goto error; if ((globalFileName != NULL) && (globalFileName->Length > 0)) { - py_path = PyUnicode_FromWideChar(globalFileName->Buffer, - wcslen(globalFileName->Buffer)); - if (! py_path) + py_path = PyUnicode_FromWideChar( + globalFileName->Buffer, wcslen(globalFileName->Buffer) + ); + if (!py_path) goto error; if (PyList_Append(py_retlist, py_path)) goto error; diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 75bd17c7bb..15fc65b0db 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -14,20 +14,24 @@ #ifndef _WIN64 -typedef NTSTATUS (NTAPI *__NtQueryInformationProcess)( +typedef NTSTATUS(NTAPI *__NtQueryInformationProcess)( HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, DWORD ProcessInformationLength, - PDWORD ReturnLength); + PDWORD ReturnLength +); #endif -#define PSUTIL_FIRST_PROCESS(Processes) ( \ - (PSYSTEM_PROCESS_INFORMATION)(Processes)) -#define PSUTIL_NEXT_PROCESS(Process) ( \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset ? \ - (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) + \ - ((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset) : NULL) +#define PSUTIL_FIRST_PROCESS(Processes) \ + ((PSYSTEM_PROCESS_INFORMATION)(Processes)) +#define PSUTIL_NEXT_PROCESS(Process) \ + (((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset \ + ? (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) \ + + ((PSYSTEM_PROCESS_INFORMATION)(Process \ + )) \ + ->NextEntryOffset) \ + : NULL) /* @@ -43,7 +47,7 @@ psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { return -1; } - *psize = info.RegionSize - ((char*)src - (char*)info.BaseAddress); + *psize = info.RegionSize - ((char *)src - (char *)info.BaseAddress); return 0; } @@ -56,10 +60,10 @@ enum psutil_process_data_kind { static void -psutil_convert_winerr(ULONG err, char* syscall) { +psutil_convert_winerr(ULONG err, char *syscall) { char fullmsg[8192]; - if (err == ERROR_NOACCESS) { + if (err == ERROR_NOACCESS) { sprintf(fullmsg, "%s -> ERROR_NOACCESS", syscall); psutil_debug(fullmsg); AccessDenied(fullmsg); @@ -71,7 +75,7 @@ psutil_convert_winerr(ULONG err, char* syscall) { static void -psutil_convert_ntstatus_err(NTSTATUS status, char* syscall) { +psutil_convert_ntstatus_err(NTSTATUS status, char *syscall) { ULONG err; if (NT_NTWIN32(status)) @@ -83,7 +87,7 @@ psutil_convert_ntstatus_err(NTSTATUS status, char* syscall) { static void -psutil_giveup_with_ad(NTSTATUS status, char* syscall) { +psutil_giveup_with_ad(NTSTATUS status, char *syscall) { ULONG err; char fullmsg[8192]; @@ -105,10 +109,9 @@ psutil_giveup_with_ad(NTSTATUS status, char* syscall) { * -1 is returned, and an appropriate Python exception is set. */ static int -psutil_get_process_data(DWORD pid, - enum psutil_process_data_kind kind, - WCHAR **pdata, - SIZE_T *psize) { +psutil_get_process_data( + DWORD pid, enum psutil_process_data_kind kind, WCHAR **pdata, SIZE_T *psize +) { /* This function is quite complex because there are several cases to be considered: @@ -157,15 +160,13 @@ psutil_get_process_data(DWORD pid, /* 64 bit case. Check if the target is a 32 bit process running in WoW64 * mode. */ status = NtQueryInformationProcess( - hProcess, - ProcessWow64Information, - &ppeb32, - sizeof(LPVOID), - NULL); + hProcess, ProcessWow64Information, &ppeb32, sizeof(LPVOID), NULL + ); if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( - status, "NtQueryInformationProcess(ProcessWow64Information)"); + status, "NtQueryInformationProcess(ProcessWow64Information)" + ); goto error; } @@ -175,7 +176,8 @@ psutil_get_process_data(DWORD pid, RTL_USER_PROCESS_PARAMETERS32 procParameters32; // read PEB - if (!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) { + if (!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) + { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); @@ -183,11 +185,13 @@ psutil_get_process_data(DWORD pid, } // read process parameters - if (!ReadProcessMemory(hProcess, - UlongToPtr(peb32.ProcessParameters), - &procParameters32, - sizeof(procParameters32), - NULL)) + if (!ReadProcessMemory( + hProcess, + UlongToPtr(peb32.ProcessParameters), + &procParameters32, + sizeof(procParameters32), + NULL + )) { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 @@ -208,7 +212,8 @@ psutil_get_process_data(DWORD pid, src = UlongToPtr(procParameters32.env); break; } - } else + } + else #else // #ifdef _WIN64 // 32 bit process. In here we may run into a lot of errors, e.g.: // * [Error 0] The operation completed successfully @@ -222,8 +227,9 @@ psutil_get_process_data(DWORD pid, // in case of any error from NtWow64* APIs we raise AccessDenied. // 32 bit case. Check if the target is also 32 bit. - if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || - !IsWow64Process(hProcess, &theyAreWow64)) { + if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) + || !IsWow64Process(hProcess, &theyAreWow64)) + { psutil_PyErr_SetFromOSErrnoWithSyscall("IsWow64Process"); goto error; } @@ -235,9 +241,9 @@ psutil_get_process_data(DWORD pid, RTL_USER_PROCESS_PARAMETERS64 procParameters64; if (NtWow64QueryInformationProcess64 == NULL) { - NtWow64QueryInformationProcess64 = \ - psutil_GetProcAddressFromLib( - "ntdll.dll", "NtWow64QueryInformationProcess64"); + NtWow64QueryInformationProcess64 = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtWow64QueryInformationProcess64" + ); if (NtWow64QueryInformationProcess64 == NULL) { PyErr_Clear(); AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); @@ -245,9 +251,9 @@ psutil_get_process_data(DWORD pid, } } if (NtWow64ReadVirtualMemory64 == NULL) { - NtWow64ReadVirtualMemory64 = \ - psutil_GetProcAddressFromLib( - "ntdll.dll", "NtWow64ReadVirtualMemory64"); + NtWow64ReadVirtualMemory64 = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtWow64ReadVirtualMemory64" + ); if (NtWow64ReadVirtualMemory64 == NULL) { PyErr_Clear(); AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); @@ -256,11 +262,8 @@ psutil_get_process_data(DWORD pid, } status = NtWow64QueryInformationProcess64( - hProcess, - ProcessBasicInformation, - &pbi64, - sizeof(pbi64), - NULL); + hProcess, ProcessBasicInformation, &pbi64, sizeof(pbi64), NULL + ); if (!NT_SUCCESS(status)) { /* psutil_convert_ntstatus_err( @@ -269,43 +272,42 @@ psutil_get_process_data(DWORD pid, */ psutil_giveup_with_ad( status, - "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); + "NtWow64QueryInformationProcess64(ProcessBasicInformation)" + ); goto error; } // read peb status = NtWow64ReadVirtualMemory64( - hProcess, - pbi64.PebBaseAddress, - &peb64, - sizeof(peb64), - NULL); + hProcess, pbi64.PebBaseAddress, &peb64, sizeof(peb64), NULL + ); if (!NT_SUCCESS(status)) { /* psutil_convert_ntstatus_err( status, "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)"); */ psutil_giveup_with_ad( - status, - "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)"); + status, "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)" + ); goto error; } // read process parameters status = NtWow64ReadVirtualMemory64( - hProcess, - peb64.ProcessParameters, - &procParameters64, - sizeof(procParameters64), - NULL); + hProcess, + peb64.ProcessParameters, + &procParameters64, + sizeof(procParameters64), + NULL + ); if (!NT_SUCCESS(status)) { /* psutil_convert_ntstatus_err( status, "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)"); */ psutil_giveup_with_ad( - status, - "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)"); + status, "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)" + ); goto error; } @@ -322,7 +324,8 @@ psutil_get_process_data(DWORD pid, src64 = procParameters64.env; break; } - } else + } + else #endif /* Target process is of the same bitness as us. */ { @@ -331,25 +334,21 @@ psutil_get_process_data(DWORD pid, RTL_USER_PROCESS_PARAMETERS_ procParameters; status = NtQueryInformationProcess( - hProcess, - ProcessBasicInformation, - &pbi, - sizeof(pbi), - NULL); + hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL + ); if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( - status, "NtQueryInformationProcess(ProcessBasicInformation)"); + status, "NtQueryInformationProcess(ProcessBasicInformation)" + ); goto error; } // read peb - if (!ReadProcessMemory(hProcess, - pbi.PebBaseAddress, - &peb, - sizeof(peb), - NULL)) + if (!ReadProcessMemory( + hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL + )) { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 @@ -358,11 +357,13 @@ psutil_get_process_data(DWORD pid, } // read process parameters - if (!ReadProcessMemory(hProcess, - peb.ProcessParameters, - &procParameters, - sizeof(procParameters), - NULL)) + if (!ReadProcessMemory( + hProcess, + peb.ProcessParameters, + &procParameters, + sizeof(procParameters), + NULL + )) { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 @@ -393,7 +394,7 @@ psutil_get_process_data(DWORD pid, } else #endif - if (psutil_get_process_region_size(hProcess, src, &size) != 0) + if (psutil_get_process_region_size(hProcess, src, &size) != 0) goto error; } @@ -406,19 +407,19 @@ psutil_get_process_data(DWORD pid, #ifndef _WIN64 if (weAreWow64 && !theyAreWow64) { status = NtWow64ReadVirtualMemory64( - hProcess, - src64, - buffer, - size, - NULL); + hProcess, src64, buffer, size, NULL + ); if (!NT_SUCCESS(status)) { - // psutil_convert_ntstatus_err(status, "NtWow64ReadVirtualMemory64"); + // psutil_convert_ntstatus_err(status, + // "NtWow64ReadVirtualMemory64"); psutil_giveup_with_ad(status, "NtWow64ReadVirtualMemory64"); goto error; } - } else + } + else #endif - if (!ReadProcessMemory(hProcess, src, buffer, size, NULL)) { + if (!ReadProcessMemory(hProcess, src, buffer, size, NULL)) + { // May fail with ERROR_PARTIAL_COPY, see: // https://github.com/giampaolo/psutil/issues/875 psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); @@ -451,15 +452,14 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { HANDLE hProcess = NULL; ULONG bufLen = 0; NTSTATUS status; - char * buffer = NULL; - WCHAR * bufWchar = NULL; + char *buffer = NULL; + WCHAR *bufWchar = NULL; PUNICODE_STRING tmp = NULL; size_t size; int ProcessCommandLineInformation = 60; if (PSUTIL_WINVER < PSUTIL_WINDOWS_8_1) { - PyErr_SetString( - PyExc_RuntimeError, "requires Windows 8.1+"); + PyErr_SetString(PyExc_RuntimeError, "requires Windows 8.1+"); goto error; } @@ -469,24 +469,24 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { // get the right buf size status = NtQueryInformationProcess( - hProcess, - ProcessCommandLineInformation, - NULL, - 0, - &bufLen); + hProcess, ProcessCommandLineInformation, NULL, 0, &bufLen + ); // https://github.com/giampaolo/psutil/issues/1501 if (status == STATUS_NOT_FOUND) { - AccessDenied("NtQueryInformationProcess(ProcessBasicInformation) -> " - "STATUS_NOT_FOUND"); + AccessDenied( + "NtQueryInformationProcess(ProcessBasicInformation) -> " + "STATUS_NOT_FOUND" + ); goto error; } - if (status != STATUS_BUFFER_OVERFLOW && \ - status != STATUS_BUFFER_TOO_SMALL && \ - status != STATUS_INFO_LENGTH_MISMATCH) { + if (status != STATUS_BUFFER_OVERFLOW && status != STATUS_BUFFER_TOO_SMALL + && status != STATUS_INFO_LENGTH_MISMATCH) + { psutil_SetFromNTStatusErr( - status, "NtQueryInformationProcess(ProcessBasicInformation)"); + status, "NtQueryInformationProcess(ProcessBasicInformation)" + ); goto error; } @@ -499,15 +499,12 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { // get the cmdline status = NtQueryInformationProcess( - hProcess, - ProcessCommandLineInformation, - buffer, - bufLen, - &bufLen + hProcess, ProcessCommandLineInformation, buffer, bufLen, &bufLen ); if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( - status, "NtQueryInformationProcess(ProcessCommandLineInformation)"); + status, "NtQueryInformationProcess(ProcessCommandLineInformation)" + ); goto error; } @@ -557,8 +554,9 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { PyObject *py_unicode = NULL; static char *keywords[] = {"pid", "use_peb", NULL}; - if (! PyArg_ParseTupleAndKeywords(args, kwdict, _Py_PARSE_PID "|O", - keywords, &pid, &py_usepeb)) + if (!PyArg_ParseTupleAndKeywords( + args, kwdict, _Py_PARSE_PID "|O", keywords, &pid, &py_usepeb + )) { return NULL; } @@ -602,8 +600,9 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { if (py_retlist == NULL) goto error; for (i = 0; i < nArgs; i++) { - py_unicode = PyUnicode_FromWideChar(szArglist[i], - wcslen(szArglist[i])); + py_unicode = PyUnicode_FromWideChar( + szArglist[i], wcslen(szArglist[i]) + ); if (py_unicode == NULL) goto error; PyList_SetItem(py_retlist, i, py_unicode); @@ -633,7 +632,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { SIZE_T size; int pid_return; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; pid_return = psutil_pid_is_running(pid); @@ -668,7 +667,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { int pid_return; PyObject *ret = NULL; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if ((pid == 0) || (pid == 4)) return Py_BuildValue("s", ""); @@ -702,8 +701,9 @@ psutil_proc_environ(PyObject *self, PyObject *args) { * On success return 0, else -1 with Python exception already set. */ int -psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, - PVOID *retBuffer) { +psutil_get_proc_info( + DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer +) { static ULONG initialBufferSize = 0x4000; NTSTATUS status; PVOID buffer; @@ -719,12 +719,10 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, while (TRUE) { status = NtQuerySystemInformation( - SystemProcessInformation, - buffer, - bufferSize, - &bufferSize); - if (status == STATUS_BUFFER_TOO_SMALL || - status == STATUS_INFO_LENGTH_MISMATCH) + SystemProcessInformation, buffer, bufferSize, &bufferSize + ); + if (status == STATUS_BUFFER_TOO_SMALL + || status == STATUS_INFO_LENGTH_MISMATCH) { free(buffer); buffer = malloc(bufferSize); @@ -738,9 +736,10 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, } } - if (! NT_SUCCESS(status)) { + if (!NT_SUCCESS(status)) { psutil_SetFromNTStatusErr( - status, "NtQuerySystemInformation(SystemProcessInformation)"); + status, "NtQuerySystemInformation(SystemProcessInformation)" + ); goto error; } @@ -792,17 +791,17 @@ psutil_proc_info(PyObject *self, PyObject *args) { double create_time; PyObject *py_retlist; - if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_get_proc_info(pid, &process, &buffer) != 0) return NULL; for (i = 0; i < process->NumberOfThreads; i++) ctx_switches += process->Threads[i].ContextSwitches; - user_time = (double)process->UserTime.HighPart * HI_T + \ - (double)process->UserTime.LowPart * LO_T; - kernel_time = (double)process->KernelTime.HighPart * HI_T + \ - (double)process->KernelTime.LowPart * LO_T; + user_time = (double)process->UserTime.HighPart * HI_T + + (double)process->UserTime.LowPart * LO_T; + kernel_time = (double)process->KernelTime.HighPart * HI_T + + (double)process->KernelTime.LowPart * LO_T; // Convert the LARGE_INTEGER union to a Unix time. // It's the best I could find by googling and borrowing code here @@ -817,34 +816,36 @@ psutil_proc_info(PyObject *self, PyObject *args) { py_retlist = Py_BuildValue( #if defined(_WIN64) - "kkdddkKKKKKK" "kKKKKKKKKK", + "kkdddkKKKKKK" + "kKKKKKKKKK", #else - "kkdddkKKKKKK" "kIIIIIIIII", + "kkdddkKKKKKK" + "kIIIIIIIII", #endif - process->HandleCount, // num handles - ctx_switches, // num ctx switches - user_time, // cpu user time - kernel_time, // cpu kernel time - create_time, // create time - process->NumberOfThreads, // num threads + process->HandleCount, // num handles + ctx_switches, // num ctx switches + user_time, // cpu user time + kernel_time, // cpu kernel time + create_time, // create time + process->NumberOfThreads, // num threads // IO counters - process->ReadOperationCount.QuadPart, // io rcount + process->ReadOperationCount.QuadPart, // io rcount process->WriteOperationCount.QuadPart, // io wcount - process->ReadTransferCount.QuadPart, // io rbytes - process->WriteTransferCount.QuadPart, // io wbytes + process->ReadTransferCount.QuadPart, // io rbytes + process->WriteTransferCount.QuadPart, // io wbytes process->OtherOperationCount.QuadPart, // io others count - process->OtherTransferCount.QuadPart, // io others bytes + process->OtherTransferCount.QuadPart, // io others bytes // memory - process->PageFaultCount, // num page faults - process->PeakWorkingSetSize, // peak wset - process->WorkingSetSize, // wset - process->QuotaPeakPagedPoolUsage, // peak paged pool - process->QuotaPagedPoolUsage, // paged pool - process->QuotaPeakNonPagedPoolUsage, // peak non paged pool - process->QuotaNonPagedPoolUsage, // non paged pool - process->PagefileUsage, // pagefile - process->PeakPagefileUsage, // peak pagefile - process->PrivatePageCount // private + process->PageFaultCount, // num page faults + process->PeakWorkingSetSize, // peak wset + process->WorkingSetSize, // wset + process->QuotaPeakPagedPoolUsage, // peak paged pool + process->QuotaPagedPoolUsage, // paged pool + process->QuotaPeakNonPagedPoolUsage, // peak non paged pool + process->QuotaNonPagedPoolUsage, // non paged pool + process->PagefileUsage, // pagefile + process->PeakPagefileUsage, // peak pagefile + process->PrivatePageCount // private ); free(buffer); diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index d272e91251..f2acd1b197 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -13,7 +13,6 @@ #include "../../arch/all/init.h" - // Return 1 if PID exists, 0 if not, -1 on error. int psutil_pid_in_pids(DWORD pid) { diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index 8beeb7cb8e..627d8ac12d 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -20,7 +20,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { TOKEN_PRIVILEGES tpPrevious; DWORD cbPrevious = sizeof(TOKEN_PRIVILEGES); - if (! LookupPrivilegeValue(NULL, Privilege, &luid)) { + if (!LookupPrivilegeValue(NULL, Privilege, &luid)) { psutil_PyErr_SetFromOSErrnoWithSyscall("LookupPrivilegeValue"); return -1; } @@ -30,13 +30,14 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = 0; - if (! AdjustTokenPrivileges( + if (!AdjustTokenPrivileges( hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &tpPrevious, - &cbPrevious)) + &cbPrevious + )) { psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); return -1; @@ -52,13 +53,9 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { tpPrevious.Privileges[0].Attributes ^= (SE_PRIVILEGE_ENABLED & tpPrevious.Privileges[0].Attributes); - if (! AdjustTokenPrivileges( - hToken, - FALSE, - &tpPrevious, - cbPrevious, - NULL, - NULL)) + if (!AdjustTokenPrivileges( + hToken, FALSE, &tpPrevious, cbPrevious, NULL, NULL + )) { psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); return -1; @@ -73,17 +70,16 @@ psutil_get_thisproc_token() { HANDLE hToken = NULL; HANDLE me = GetCurrentProcess(); - if (! OpenProcessToken( - me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) + if (!OpenProcessToken(me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { - if (GetLastError() == ERROR_NO_TOKEN) - { - if (! ImpersonateSelf(SecurityImpersonation)) { + if (GetLastError() == ERROR_NO_TOKEN) { + if (!ImpersonateSelf(SecurityImpersonation)) { psutil_PyErr_SetFromOSErrnoWithSyscall("ImpersonateSelf"); return NULL; } - if (! OpenProcessToken( - me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) + if (!OpenProcessToken( + me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken + )) { psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); return NULL; @@ -101,7 +97,8 @@ psutil_get_thisproc_token() { static void psutil_print_err() { - char *msg = "psutil module couldn't set SE DEBUG mode for this process; " \ + char *msg = + "psutil module couldn't set SE DEBUG mode for this process; " "please file an issue against psutil bug tracker"; psutil_debug(msg); if (GetLastError() != ERROR_ACCESS_DENIED) diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 8ea99a8daf..dfb8075c78 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -18,10 +18,8 @@ SC_HANDLE psutil_get_service_handler( - const wchar_t *service_name, - DWORD scm_access, - DWORD access) -{ + const wchar_t *service_name, DWORD scm_access, DWORD access +) { SC_HANDLE sc = NULL; SC_HANDLE hService = NULL; @@ -47,11 +45,8 @@ psutil_get_service_handler( // returns NULL on error. On success, fills *service_name_out. static SC_HANDLE psutil_get_service_from_args( - PyObject *args, - DWORD scm_access, - DWORD access, - wchar_t **service_name_out) -{ + PyObject *args, DWORD scm_access, DWORD access, wchar_t **service_name_out +) { PyObject *py_service_name = NULL; wchar_t *service_name = NULL; Py_ssize_t wlen; @@ -77,7 +72,6 @@ psutil_get_service_from_args( } - // XXX - expose these as constants? static const char * get_startup_string(DWORD startup) { @@ -88,14 +82,12 @@ get_startup_string(DWORD startup) { return "manual"; case SERVICE_DISABLED: return "disabled"; -/* // drivers only (since we use EnumServicesStatusEx() with // SERVICE_WIN32) - case SERVICE_BOOT_START: - return "boot-start"; - case SERVICE_SYSTEM_START: - return "system-start"; -*/ + // case SERVICE_BOOT_START: + // return "boot-start"; + // case SERVICE_SYSTEM_START: + // return "system-start"; default: return "unknown"; } @@ -168,26 +160,29 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { &bytesNeeded, &srvCount, &resumeHandle, - NULL); + NULL + ); if (ok || (GetLastError() != ERROR_MORE_DATA)) break; if (lpService) free(lpService); dwBytes = bytesNeeded; - lpService = (ENUM_SERVICE_STATUS_PROCESSW*)malloc(dwBytes); + lpService = (ENUM_SERVICE_STATUS_PROCESSW *)malloc(dwBytes); } for (i = 0; i < srvCount; i++) { // Get unicode name / display name. py_name = NULL; py_name = PyUnicode_FromWideChar( - lpService[i].lpServiceName, wcslen(lpService[i].lpServiceName)); + lpService[i].lpServiceName, wcslen(lpService[i].lpServiceName) + ); if (py_name == NULL) goto error; py_display_name = NULL; py_display_name = PyUnicode_FromWideChar( - lpService[i].lpDisplayName, wcslen(lpService[i].lpDisplayName)); + lpService[i].lpDisplayName, wcslen(lpService[i].lpDisplayName) + ); if (py_display_name == NULL) goto error; @@ -240,10 +235,7 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { PyObject *py_unicode_username = NULL; hService = psutil_get_service_from_args( - args, - SC_MANAGER_ENUMERATE_SERVICE, - SERVICE_QUERY_CONFIG, - &service_name + args, SC_MANAGER_ENUMERATE_SERVICE, SERVICE_QUERY_CONFIG, &service_name ); if (hService == NULL) return NULL; @@ -271,19 +263,22 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { // Get unicode display name. py_unicode_display_name = PyUnicode_FromWideChar( - qsc->lpDisplayName, wcslen(qsc->lpDisplayName)); + qsc->lpDisplayName, wcslen(qsc->lpDisplayName) + ); if (py_unicode_display_name == NULL) goto error; // Get unicode bin path. py_unicode_binpath = PyUnicode_FromWideChar( - qsc->lpBinaryPathName, wcslen(qsc->lpBinaryPathName)); + qsc->lpBinaryPathName, wcslen(qsc->lpBinaryPathName) + ); if (py_unicode_binpath == NULL) goto error; // Get unicode username. py_unicode_username = PyUnicode_FromWideChar( - qsc->lpServiceStartName, wcslen(qsc->lpServiceStartName)); + qsc->lpServiceStartName, wcslen(qsc->lpServiceStartName) + ); if (py_unicode_username == NULL) goto error; @@ -337,18 +332,16 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { PyObject *py_tuple = NULL; hService = psutil_get_service_from_args( - args, - SC_MANAGER_ENUMERATE_SERVICE, - SERVICE_QUERY_STATUS, - &service_name + args, SC_MANAGER_ENUMERATE_SERVICE, SERVICE_QUERY_STATUS, &service_name ); if (hService == NULL) return NULL; // First call to QueryServiceStatusEx() is necessary to get the // right size. - QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, NULL, 0, - &bytesNeeded); + QueryServiceStatusEx( + hService, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded + ); if (GetLastError() == ERROR_MUI_FILE_NOT_FOUND) { // Also services.msc fails in the same manner, so we return an // empty string. @@ -361,24 +354,29 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { goto error; } - ssp = (SERVICE_STATUS_PROCESS *)HeapAlloc(GetProcessHeap(), 0, bytesNeeded); + ssp = (SERVICE_STATUS_PROCESS *)HeapAlloc( + GetProcessHeap(), 0, bytesNeeded + ); if (ssp == NULL) { PyErr_NoMemory(); goto error; } // Actual call. - ok = QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)ssp, - bytesNeeded, &bytesNeeded); + ok = QueryServiceStatusEx( + hService, + SC_STATUS_PROCESS_INFO, + (LPBYTE)ssp, + bytesNeeded, + &bytesNeeded + ); if (!ok) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); goto error; } py_tuple = Py_BuildValue( - "(sk)", - get_state_string(ssp->dwCurrentState), - ssp->dwProcessId + "(sk)", get_state_string(ssp->dwCurrentState), ssp->dwProcessId ); if (py_tuple == NULL) goto error; @@ -409,18 +407,17 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { PyObject *py_retstr = NULL; hService = psutil_get_service_from_args( - args, - SC_MANAGER_ENUMERATE_SERVICE, - SERVICE_QUERY_CONFIG, - &service_name + args, SC_MANAGER_ENUMERATE_SERVICE, SERVICE_QUERY_CONFIG, &service_name ); if (hService == NULL) return NULL; - QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, &bytesNeeded); + QueryServiceConfig2W( + hService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, &bytesNeeded + ); - if ((GetLastError() == ERROR_NOT_FOUND) || - (GetLastError() == ERROR_MUI_FILE_NOT_FOUND)) + if ((GetLastError() == ERROR_NOT_FOUND) + || (GetLastError() == ERROR_MUI_FILE_NOT_FOUND)) { // E.g. services.msc fails in this manner, so we return an // empty string. @@ -441,8 +438,13 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { goto error; } - ok = QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, - (LPBYTE)scd, bytesNeeded, &bytesNeeded); + ok = QueryServiceConfig2W( + hService, + SERVICE_CONFIG_DESCRIPTION, + (LPBYTE)scd, + bytesNeeded, + &bytesNeeded + ); if (!ok) { psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); goto error; @@ -453,7 +455,8 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { } else { py_retstr = PyUnicode_FromWideChar( - scd->lpDescription, wcslen(scd->lpDescription)); + scd->lpDescription, wcslen(scd->lpDescription) + ); } if (!py_retstr) @@ -486,10 +489,7 @@ psutil_winservice_start(PyObject *self, PyObject *args) { wchar_t *service_name = NULL; hService = psutil_get_service_from_args( - args, - SC_MANAGER_ALL_ACCESS, - SERVICE_START, - &service_name + args, SC_MANAGER_ALL_ACCESS, SERVICE_START, &service_name ); if (hService == NULL) return NULL; @@ -525,10 +525,7 @@ psutil_winservice_stop(PyObject *self, PyObject *args) { SERVICE_STATUS ssp; hService = psutil_get_service_from_args( - args, - SC_MANAGER_ALL_ACCESS, - SERVICE_STOP, - &service_name + args, SC_MANAGER_ALL_ACCESS, SERVICE_STOP, &service_name ); if (hService == NULL) return NULL; diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 09bee6820b..2b434034cd 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -25,7 +25,8 @@ // https://github.com/giampaolo/psutil/issues/1294 -static PVOID __GetExtendedTcpTable(ULONG family) { +static PVOID +__GetExtendedTcpTable(ULONG family) { DWORD err; PVOID table; ULONG size = 0; @@ -56,7 +57,8 @@ static PVOID __GetExtendedTcpTable(ULONG family) { } -static PVOID __GetExtendedUdpTable(ULONG family) { +static PVOID +__GetExtendedUdpTable(ULONG family) { DWORD err; PVOID table; ULONG size = 0; @@ -89,8 +91,8 @@ static PVOID __GetExtendedUdpTable(ULONG family) { #define psutil_conn_decref_objs() \ Py_DECREF(_AF_INET); \ - Py_DECREF(_AF_INET6);\ - Py_DECREF(_SOCK_STREAM);\ + Py_DECREF(_AF_INET6); \ + Py_DECREF(_SOCK_STREAM); \ Py_DECREF(_SOCK_DGRAM); @@ -99,7 +101,7 @@ static PVOID __GetExtendedUdpTable(ULONG family) { */ PyObject * psutil_net_connections(PyObject *self, PyObject *args) { - static long null_address[4] = { 0, 0, 0, 0 }; + static long null_address[4] = {0, 0, 0, 0}; DWORD pid; int pid_return; PVOID table = NULL; @@ -122,8 +124,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { PyObject *_SOCK_STREAM = PyLong_FromLong((long)SOCK_STREAM); PyObject *_SOCK_DGRAM = PyLong_FromLong((long)SOCK_DGRAM); - if (! PyArg_ParseTuple(args, _Py_PARSE_PID "OO", &pid, &py_af_filter, - &py_type_filter)) + if (!PyArg_ParseTuple( + args, _Py_PARSE_PID "OO", &pid, &py_af_filter, &py_type_filter + )) { goto error; } @@ -154,8 +157,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { // TCP IPv4 - if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1)) + if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) + && (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1)) { table = NULL; py_conn_tuple = NULL; @@ -173,8 +176,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { } } - if (tcp4Table->table[i].dwLocalAddr != 0 || - tcp4Table->table[i].dwLocalPort != 0) + if (tcp4Table->table[i].dwLocalAddr != 0 + || tcp4Table->table[i].dwLocalPort != 0) { struct in_addr addr; @@ -183,7 +186,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, - BYTESWAP_USHORT(tcp4Table->table[i].dwLocalPort)); + BYTESWAP_USHORT(tcp4Table->table[i].dwLocalPort) + ); } else { py_addr_tuple_local = PyTuple_New(0); @@ -194,9 +198,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { // On Windows <= XP, remote addr is filled even if socket // is in LISTEN mode in which case we just ignore it. - if ((tcp4Table->table[i].dwRemoteAddr != 0 || - tcp4Table->table[i].dwRemotePort != 0) && - (tcp4Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) + if ((tcp4Table->table[i].dwRemoteAddr != 0 + || tcp4Table->table[i].dwRemotePort != 0) + && (tcp4Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) { struct in_addr addr; @@ -205,10 +209,10 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_remote = Py_BuildValue( "(si)", addressBufferRemote, - BYTESWAP_USHORT(tcp4Table->table[i].dwRemotePort)); + BYTESWAP_USHORT(tcp4Table->table[i].dwRemotePort) + ); } - else - { + else { py_addr_tuple_remote = PyTuple_New(0); } @@ -223,7 +227,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local, py_addr_tuple_remote, tcp4Table->table[i].dwState, - tcp4Table->table[i].dwOwningPid); + tcp4Table->table[i].dwOwningPid + ); if (!py_conn_tuple) goto error; if (PyList_Append(py_retlist, py_conn_tuple)) @@ -236,9 +241,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { } // TCP IPv6 - if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1) && - (RtlIpv6AddressToStringA != NULL)) + if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) + && (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1) + && (RtlIpv6AddressToStringA != NULL)) { table = NULL; py_conn_tuple = NULL; @@ -249,16 +254,15 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (table == NULL) goto error; tcp6Table = table; - for (i = 0; i < tcp6Table->dwNumEntries; i++) - { + for (i = 0; i < tcp6Table->dwNumEntries; i++) { if (pid != -1) { if (tcp6Table->table[i].dwOwningPid != pid) { continue; } } - if (memcmp(tcp6Table->table[i].ucLocalAddr, null_address, 16) - != 0 || tcp6Table->table[i].dwLocalPort != 0) + if (memcmp(tcp6Table->table[i].ucLocalAddr, null_address, 16) != 0 + || tcp6Table->table[i].dwLocalPort != 0) { struct in6_addr addr; @@ -267,7 +271,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, - BYTESWAP_USHORT(tcp6Table->table[i].dwLocalPort)); + BYTESWAP_USHORT(tcp6Table->table[i].dwLocalPort) + ); } else { py_addr_tuple_local = PyTuple_New(0); @@ -279,9 +284,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { // On Windows <= XP, remote addr is filled even if socket // is in LISTEN mode in which case we just ignore it. if ((memcmp(tcp6Table->table[i].ucRemoteAddr, null_address, 16) - != 0 || - tcp6Table->table[i].dwRemotePort != 0) && - (tcp6Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) + != 0 + || tcp6Table->table[i].dwRemotePort != 0) + && (tcp6Table->table[i].dwState != MIB_TCP_STATE_LISTEN)) { struct in6_addr addr; @@ -290,7 +295,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_remote = Py_BuildValue( "(si)", addressBufferRemote, - BYTESWAP_USHORT(tcp6Table->table[i].dwRemotePort)); + BYTESWAP_USHORT(tcp6Table->table[i].dwRemotePort) + ); } else { py_addr_tuple_remote = PyTuple_New(0); @@ -307,7 +313,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local, py_addr_tuple_remote, tcp6Table->table[i].dwState, - tcp6Table->table[i].dwOwningPid); + tcp6Table->table[i].dwOwningPid + ); if (!py_conn_tuple) goto error; if (PyList_Append(py_retlist, py_conn_tuple)) @@ -321,8 +328,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { // UDP IPv4 - if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1)) + if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) + && (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1)) { table = NULL; py_conn_tuple = NULL; @@ -332,16 +339,15 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (table == NULL) goto error; udp4Table = table; - for (i = 0; i < udp4Table->dwNumEntries; i++) - { + for (i = 0; i < udp4Table->dwNumEntries; i++) { if (pid != -1) { if (udp4Table->table[i].dwOwningPid != pid) { continue; } } - if (udp4Table->table[i].dwLocalAddr != 0 || - udp4Table->table[i].dwLocalPort != 0) + if (udp4Table->table[i].dwLocalAddr != 0 + || udp4Table->table[i].dwLocalPort != 0) { struct in_addr addr; @@ -350,7 +356,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, - BYTESWAP_USHORT(udp4Table->table[i].dwLocalPort)); + BYTESWAP_USHORT(udp4Table->table[i].dwLocalPort) + ); } else { py_addr_tuple_local = PyTuple_New(0); @@ -367,7 +374,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local, PyTuple_New(0), PSUTIL_CONN_NONE, - udp4Table->table[i].dwOwningPid); + udp4Table->table[i].dwOwningPid + ); if (!py_conn_tuple) goto error; if (PyList_Append(py_retlist, py_conn_tuple)) @@ -381,9 +389,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { // UDP IPv6 - if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) && - (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1) && - (RtlIpv6AddressToStringA != NULL)) + if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) + && (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1) + && (RtlIpv6AddressToStringA != NULL)) { table = NULL; py_conn_tuple = NULL; @@ -400,8 +408,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { } } - if (memcmp(udp6Table->table[i].ucLocalAddr, null_address, 16) - != 0 || udp6Table->table[i].dwLocalPort != 0) + if (memcmp(udp6Table->table[i].ucLocalAddr, null_address, 16) != 0 + || udp6Table->table[i].dwLocalPort != 0) { struct in6_addr addr; @@ -410,7 +418,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local = Py_BuildValue( "(si)", addressBufferLocal, - BYTESWAP_USHORT(udp6Table->table[i].dwLocalPort)); + BYTESWAP_USHORT(udp6Table->table[i].dwLocalPort) + ); } else { py_addr_tuple_local = PyTuple_New(0); @@ -427,7 +436,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_addr_tuple_local, PyTuple_New(0), PSUTIL_CONN_NONE, - udp6Table->table[i].dwOwningPid); + udp6Table->table[i].dwOwningPid + ); if (!py_conn_tuple) goto error; if (PyList_Append(py_retlist, py_conn_tuple)) diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index 597b78d0b0..111772d9ac 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -9,8 +9,10 @@ System related functions. Original code moved in here from psutil/_psutil_windows.c in 2023. For reference, here's the GIT blame history before the move: -* boot_time(): https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L51-L60 -* users(): https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L1103-L1244 +- boot_time(): + https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L51-L60 +- users(): + https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L1103-L1244 */ #include @@ -63,18 +65,19 @@ psutil_users(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - if (WTSEnumerateSessionsW == NULL || - WTSQuerySessionInformationW == NULL || - WTSFreeMemory == NULL) { - // If we don't run in an environment that is a Remote Desktop Services environment - // the Wtsapi32 proc might not be present. - // https://docs.microsoft.com/en-us/windows/win32/termserv/run-time-linking-to-wtsapi32-dll - return py_retlist; + if (WTSEnumerateSessionsW == NULL || WTSQuerySessionInformationW == NULL + || WTSFreeMemory == NULL) + { + // If we don't run in an environment that is a Remote Desktop Services + // environment the Wtsapi32 proc might not be present. + // https://docs.microsoft.com/en-us/windows/win32/termserv/run-time-linking-to-wtsapi32-dll + return py_retlist; } if (WTSEnumerateSessionsW(hServer, 0, 1, &sessions, &count) == 0) { if (ERROR_CALL_NOT_IMPLEMENTED == GetLastError()) { - // On Windows Nano server, the Wtsapi32 API can be present, but return WinError 120. + // On Windows Nano server, the Wtsapi32 API can be present, but + // return WinError 120. return py_retlist; } psutil_PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); @@ -98,8 +101,11 @@ psutil_users(PyObject *self, PyObject *args) { // username bytes = 0; - if (WTSQuerySessionInformationW(hServer, sessionId, WTSUserName, - &buffer_user, &bytes) == 0) { + if (WTSQuerySessionInformationW( + hServer, sessionId, WTSUserName, &buffer_user, &bytes + ) + == 0) + { psutil_PyErr_SetFromOSErrnoWithSyscall( "WTSQuerySessionInformationW" ); @@ -110,8 +116,11 @@ psutil_users(PyObject *self, PyObject *args) { // address bytes = 0; - if (WTSQuerySessionInformationW(hServer, sessionId, WTSClientAddress, - &buffer_addr, &bytes) == 0) { + if (WTSQuerySessionInformationW( + hServer, sessionId, WTSClientAddress, &buffer_addr, &bytes + ) + == 0) + { psutil_PyErr_SetFromOSErrnoWithSyscall( "WTSQuerySessionInformationW" ); @@ -120,14 +129,17 @@ psutil_users(PyObject *self, PyObject *args) { address = (PWTS_CLIENT_ADDRESS)buffer_addr; if (address->AddressFamily == 2) { // AF_INET == 2 - sprintf_s(address_str, - _countof(address_str), - "%u.%u.%u.%u", - // The IP address is offset by two bytes from the start of the Address member of the WTS_CLIENT_ADDRESS structure. - address->Address[2], - address->Address[3], - address->Address[4], - address->Address[5]); + sprintf_s( + address_str, + _countof(address_str), + "%u.%u.%u.%u", + // The IP address is offset by two bytes from the start of the + // Address member of the WTS_CLIENT_ADDRESS structure. + address->Address[2], + address->Address[3], + address->Address[4], + address->Address[5] + ); py_address = Py_BuildValue("s", address_str); if (!py_address) goto error; @@ -139,8 +151,11 @@ psutil_users(PyObject *self, PyObject *args) { // login time bytes = 0; - if (WTSQuerySessionInformationW(hServer, sessionId, WTSSessionInfo, - &buffer_info, &bytes) == 0) { + if (WTSQuerySessionInformationW( + hServer, sessionId, WTSSessionInfo, &buffer_info, &bytes + ) + == 0) + { psutil_PyErr_SetFromOSErrnoWithSyscall( "WTSQuerySessionInformationW" ); diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 04ba3ee4c5..0d4d9124df 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -21,8 +21,8 @@ // // This formula comes from linux's include/linux/sched/loadavg.h // https://github.com/torvalds/linux/blob/345671ea0f9258f410eb057b9ced9cefbbe5dc78/include/linux/sched/loadavg.h#L20-L23 -#define LOADAVG_FACTOR_1F 0.9200444146293232478931553241 -#define LOADAVG_FACTOR_5F 0.9834714538216174894737477501 +#define LOADAVG_FACTOR_1F 0.9200444146293232478931553241 +#define LOADAVG_FACTOR_5F 0.9834714538216174894737477501 #define LOADAVG_FACTOR_15F 0.9944598480048967508795473394 // The time interval in seconds between taking load counts, same as Linux #define SAMPLING_INTERVAL 5 @@ -31,6 +31,7 @@ double load_avg_1m = 0; double load_avg_5m = 0; double load_avg_15m = 0; +// clang-format off #ifdef Py_GIL_DISABLED static PyMutex mutex; #define MUTEX_LOCK(m) PyMutex_Lock(m) @@ -39,15 +40,18 @@ double load_avg_15m = 0; #define MUTEX_LOCK(m) #define MUTEX_UNLOCK(m) #endif +// clang-format on -VOID CALLBACK LoadAvgCallback(PVOID hCounter, BOOLEAN timedOut) { +VOID CALLBACK +LoadAvgCallback(PVOID hCounter, BOOLEAN timedOut) { PDH_FMT_COUNTERVALUE displayValue; double currentLoad; PDH_STATUS err; err = PdhGetFormattedCounterValue( - (PDH_HCOUNTER)hCounter, PDH_FMT_DOUBLE, 0, &displayValue); + (PDH_HCOUNTER)hCounter, PDH_FMT_DOUBLE, 0, &displayValue + ); // Skip updating the load if we can't get the value successfully if (err != ERROR_SUCCESS) { return; @@ -55,12 +59,12 @@ VOID CALLBACK LoadAvgCallback(PVOID hCounter, BOOLEAN timedOut) { currentLoad = displayValue.doubleValue; MUTEX_LOCK(&mutex); - load_avg_1m = load_avg_1m * LOADAVG_FACTOR_1F + currentLoad * \ - (1.0 - LOADAVG_FACTOR_1F); - load_avg_5m = load_avg_5m * LOADAVG_FACTOR_5F + currentLoad * \ - (1.0 - LOADAVG_FACTOR_5F); - load_avg_15m = load_avg_15m * LOADAVG_FACTOR_15F + currentLoad * \ - (1.0 - LOADAVG_FACTOR_15F); + load_avg_1m = load_avg_1m * LOADAVG_FACTOR_1F + + currentLoad * (1.0 - LOADAVG_FACTOR_1F); + load_avg_5m = load_avg_5m * LOADAVG_FACTOR_5F + + currentLoad * (1.0 - LOADAVG_FACTOR_5F); + load_avg_15m = load_avg_15m * LOADAVG_FACTOR_15F + + currentLoad * (1.0 - LOADAVG_FACTOR_15F); MUTEX_UNLOCK(&mutex); } @@ -84,7 +88,8 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { if (s != ERROR_SUCCESS) { PyErr_Format( PyExc_RuntimeError, - "PdhAddEnglishCounterW failed. Performance counters may be disabled." + "PdhAddEnglishCounterW failed. Performance counters may be " + "disabled." ); return NULL; } @@ -105,10 +110,10 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { &waitHandle, event, (WAITORTIMERCALLBACK)LoadAvgCallback, - (PVOID) - hCounter, + (PVOID)hCounter, INFINITE, - WT_EXECUTEDEFAULT); + WT_EXECUTEDEFAULT + ); if (ret == 0) { psutil_PyErr_SetFromOSErrnoWithSyscall("RegisterWaitForSingleObject"); @@ -132,5 +137,7 @@ psutil_get_loadavg(PyObject *self, PyObject *args) { double load_avg_5m_l = load_avg_5m; double load_avg_15m_l = load_avg_15m; MUTEX_UNLOCK(&mutex); - return Py_BuildValue("(ddd)", load_avg_1m_l, load_avg_5m_l, load_avg_15m_l); + return Py_BuildValue( + "(ddd)", load_avg_1m_l, load_avg_5m_l, load_avg_15m_l + ); } diff --git a/pyproject.toml b/pyproject.toml index 682f0052cf..35f93c8205 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ ignore = [ "PTH", # flake8-use-pathlib "PYI", # flake8-pyi (python types stuff) "Q000", # Single quotes found but double quotes preferred + "RET502", # Do not implicitly `return None` in function able to return non-`None` value "RET503", # Missing explicit `return` at the end of function able to return non-`None` value "RET504", # Unnecessary assignment to `result` before `return` statement "RET505", # [*] Unnecessary `else` after `return` statement diff --git a/scripts/internal/clinter.py b/scripts/internal/clinter.py deleted file mode 100755 index 6c0428f848..0000000000 --- a/scripts/internal/clinter.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/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. - -"""A super simple linter to check C syntax.""" - - -import argparse -import sys - -warned = False - - -def warn(path, line, lineno, msg): - global warned - warned = True - print(f"{path}:{lineno}: {msg}", file=sys.stderr) - - -def check_line(path, line, idx, lines): - s = line - lineno = idx + 1 - eof = lineno == len(lines) - if s.endswith(' \n'): - warn(path, line, lineno, "extra space at EOL") - elif '\t' in line: - warn(path, line, lineno, "line has a tab") - elif s.endswith('\r\n'): - warn(path, line, lineno, "Windows line ending") - # end of global block, e.g. "}newfunction...": - elif s == "}\n": - if not eof: - nextline = lines[idx + 1] - # "#" is a pre-processor line - if ( - nextline != '\n' - and nextline.strip()[0] != '#' - and nextline.strip()[:2] != '*/' - ): - warn(path, line, lineno, "expected 1 blank line") - - sls = s.lstrip() - if sls.startswith('//') and sls[2] != ' ' and line.strip() != '//': - warn(path, line, lineno, "no space after // comment") - # e.g. "if(..." after keywords - keywords = ("if", "else", "while", "do", "enum", "for") - for kw in keywords: - if sls.startswith(kw + '('): - warn(path, line, lineno, f"missing space between {kw!r} and '('") - # eof - if eof and not line.endswith('\n'): - warn(path, line, lineno, "no blank line at EOF") - - ss = s.strip() - if ss.startswith(("printf(", "printf (")): - if not ss.endswith(("// NOQA", "// NOQA")): - warn(path, line, lineno, "printf() statement") - - -def process(path): - with open(path) as f: - lines = f.readlines() - for idx, line in enumerate(lines): - check_line(path, line, idx, lines) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('paths', nargs='+', help='path(s) to a file(s)') - args = parser.parse_args() - for path in args.paths: - process(path) - if warned: - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 3d1baf9252..053ee01bce 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -10,13 +10,14 @@ "make install-git-hooks". """ - import os import shlex +import shutil import subprocess import sys PYTHON = sys.executable +LINUX = sys.platform.startswith("linux") def term_supports_colors(): @@ -95,6 +96,9 @@ def git_commit_files(): return (py_files, c_files, rst_files, toml_files, new_rm_mv) +# --- linters + + def black(files): print(f"running black ({len(files)})") cmd = [PYTHON, "-m", "black", "--check", "--safe"] + files @@ -122,12 +126,14 @@ def ruff(files): ) -def c_linter(files): - print(f"running clinter ({len(files)})") - # XXX: we should escape spaces and possibly other amenities here - cmd = [PYTHON, "scripts/internal/clinter.py"] + files +def clang_format(files): + if not LINUX and not shutil.which("clang-format"): + print("clang-format not installed; skip lint check") + return + print("running clang-format") + cmd = ["clang-format", "--dry-run", "--Werror"] + files if subprocess.call(cmd) != 0: - return sys.exit("C code didn't pass style check") + return sys.exit("code didn't pass clang-format check") def toml_sort(files): @@ -151,13 +157,24 @@ def dprint(): return sys.exit("code didn't pass dprint check") +def lint_manifest(): + print("running MANIFEST.in check") + out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) + with open("MANIFEST.in", encoding="utf8") as f: + if out.strip() != f.read().strip(): + sys.exit( + "some files were added, deleted or renamed; " + "run 'make generate-manifest' and commit again" + ) + + def main(): py_files, c_files, rst_files, toml_files, new_rm_mv = git_commit_files() if py_files: black(py_files) ruff(py_files) if c_files: - c_linter(c_files) + clang_format(c_files) if rst_files: rstcheck(rst_files) if toml_files: @@ -166,13 +183,7 @@ def main(): dprint() if new_rm_mv: - out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) - with open("MANIFEST.in", encoding="utf8") as f: - if out.strip() != f.read().strip(): - sys.exit( - "some files were added, deleted or renamed; " - "run 'make generate-manifest' and commit again" - ) + lint_manifest() if __name__ == "__main__": From 3fdbb1929292319f0535dee73b1f1738b070aca1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 26 Oct 2025 20:31:02 +0100 Subject: [PATCH 1402/1714] Update HISTORY.rst + bump up ver --- HISTORY.rst | 11 +++++++++++ psutil/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index fbb083812b..9834fa4efc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,16 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +*Bug tracker at https://github.com/giampaolo/psutil/issues* + +7.1.3 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +**Enhancements** + +- 2667_: enforce `clang-format` on all C and header files. It is now the + mandatory formatting style for all C sources. 7.1.2 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index b289c16170..9251770911 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -203,7 +203,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.1.2" +__version__ = "7.1.3" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) From 8bbd12d06359e9f447a60aa33f4efbf5009e8323 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 26 Oct 2025 22:07:45 +0100 Subject: [PATCH 1403/1714] Move psutil_check_pid_range() in arch/all/pids.c --- HISTORY.rst | 2 -- psutil/arch/all/init.c | 21 --------------------- psutil/arch/all/pids.c | 23 ++++++++++++++++++++++- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9834fa4efc..8d867a2388 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,5 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -*Bug tracker at https://github.com/giampaolo/psutil/issues* - 7.1.3 (IN DEVELOPMENT) ====================== diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c index 82e7c4b654..eb254e5c24 100644 --- a/psutil/arch/all/init.c +++ b/psutil/arch/all/init.c @@ -94,27 +94,6 @@ psutil_set_debug(PyObject *self, PyObject *args) { } -// Raise OverflowError if Python int value overflowed when converting -// to pid_t. Raise ValueError if Python int value is negative. -// Otherwise, return None. -PyObject * -psutil_check_pid_range(PyObject *self, PyObject *args) { -#ifdef PSUTIL_WINDOWS - DWORD pid; -#else - pid_t pid; -#endif - - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (pid < 0) { - PyErr_SetString(PyExc_ValueError, "pid must be a positive integer"); - return NULL; - } - Py_RETURN_NONE; -} - - // Use it when invalid args are passed to a C function. int psutil_badargs(const char *funcname) { diff --git a/psutil/arch/all/pids.c b/psutil/arch/all/pids.c index d88c10dd6b..1b23ea34a9 100644 --- a/psutil/arch/all/pids.c +++ b/psutil/arch/all/pids.c @@ -4,12 +4,33 @@ * found in the LICENSE file. */ -#if defined(PSUTIL_WINDOWS) || defined(PSUTIL_BSD) || defined(PSUTIL_OSX) #include #include "init.h" +// Raise OverflowError if Python int value overflowed when converting +// to pid_t. Raise ValueError if Python int value is negative. +// Otherwise, return None. +PyObject * +psutil_check_pid_range(PyObject *self, PyObject *args) { +#ifdef PSUTIL_WINDOWS + DWORD pid; +#else + pid_t pid; +#endif + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (pid < 0) { + PyErr_SetString(PyExc_ValueError, "pid must be a positive integer"); + return NULL; + } + Py_RETURN_NONE; +} + + +#if defined(PSUTIL_WINDOWS) || defined(PSUTIL_BSD) || defined(PSUTIL_OSX) PyObject * psutil_pids(PyObject *self, PyObject *args) { #ifdef PSUTIL_WINDOWS From 2696f5bc5456cbfba1352de56c5d9e82d51b64fe Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 00:07:46 +0100 Subject: [PATCH 1404/1714] Rm duplicated Windows code --- Makefile | 8 +++----- psutil/arch/windows/init.h | 11 ----------- psutil/arch/windows/proc_info.c | 1 + 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 81f3b2614e..412f5d023c 100644 --- a/Makefile +++ b/Makefile @@ -174,13 +174,12 @@ ruff: ## Run ruff linter. black: ## Run black formatter. @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe -dprint: - @$(DPRINT) check - lint-c: ## Run C linter. -# @git ls-files '*.c' '*.h' | xargs clang-format --dry-run --Werror # serial exec @git ls-files '*.c' '*.h' | xargs -P0 -I{} clang-format --dry-run --Werror {} +dprint: + @$(DPRINT) check + lint-rst: ## Run linter for .rst files. @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml @@ -214,7 +213,6 @@ fix-ruff: @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --fix --output-format=concise $(ARGS) fix-c: -# @git ls-files '*.c' '*.h' | xargs clang-format -i # serial exec @git ls-files '*.c' '*.h' | xargs -P0 -I{} clang-format -i {} # parallel exec fix-toml: ## Fix pyproject.toml diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index fcf928289b..342a35560f 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -34,17 +34,6 @@ extern CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; #define NT_NTWIN32(status) (_NT_FACILITY(status) == FACILITY_WIN32) #define WIN32_FROM_NTSTATUS(status) (((ULONG)(status)) & 0xffff) -#define PSUTIL_FIRST_PROCESS(Processes) \ - ((PSYSTEM_PROCESS_INFORMATION)(Processes)) - -#define PSUTIL_NEXT_PROCESS(Process) \ - (((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset \ - ? (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) \ - + ((PSYSTEM_PROCESS_INFORMATION)(Process \ - )) \ - ->NextEntryOffset) \ - : NULL) - #define LO_T 1e-7 #define HI_T 429.4967296 diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 15fc65b0db..e370fc44b0 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -25,6 +25,7 @@ typedef NTSTATUS(NTAPI *__NtQueryInformationProcess)( #define PSUTIL_FIRST_PROCESS(Processes) \ ((PSYSTEM_PROCESS_INFORMATION)(Processes)) + #define PSUTIL_NEXT_PROCESS(Process) \ (((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset \ ? (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) \ From b5c38556b5df744d9d416cbcd5a7a7560accc103 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 12:09:28 +0100 Subject: [PATCH 1405/1714] `psutil_oserror()` and `psutil_oserror_wsyscall()` utils (#2668) --- psutil/_psutil_aix.c | 38 +++++++++++++++--------------- psutil/arch/aix/common.c | 4 ++-- psutil/arch/all/init.c | 14 ++++++++++- psutil/arch/all/init.h | 3 ++- psutil/arch/bsd/disk.c | 4 ++-- psutil/arch/bsd/proc.c | 2 +- psutil/arch/freebsd/cpu.c | 2 +- psutil/arch/freebsd/mem.c | 4 +--- psutil/arch/freebsd/proc.c | 12 ++++------ psutil/arch/freebsd/proc_socks.c | 6 ++--- psutil/arch/freebsd/sensors.c | 4 ++-- psutil/arch/freebsd/sys_socks.c | 6 ++--- psutil/arch/linux/mem.c | 2 +- psutil/arch/linux/net.c | 4 ++-- psutil/arch/linux/proc.c | 8 +++---- psutil/arch/netbsd/mem.c | 4 ++-- psutil/arch/netbsd/pids.c | 2 ++ psutil/arch/netbsd/proc.c | 12 +++++----- psutil/arch/netbsd/socks.c | 10 ++++---- psutil/arch/openbsd/mem.c | 4 ++-- psutil/arch/openbsd/proc.c | 4 ++-- psutil/arch/openbsd/socks.c | 2 +- psutil/arch/osx/cpu.c | 2 +- psutil/arch/osx/disk.c | 4 ++-- psutil/arch/osx/init.c | 2 +- psutil/arch/osx/mem.c | 2 +- psutil/arch/osx/proc.c | 12 +++++----- psutil/arch/posix/net.c | 12 +++++----- psutil/arch/posix/proc.c | 8 +++---- psutil/arch/posix/sysctl.c | 16 ++++++------- psutil/arch/sunos/cpu.c | 8 +++---- psutil/arch/sunos/disk.c | 6 ++--- psutil/arch/sunos/environ.c | 10 ++++---- psutil/arch/sunos/mem.c | 6 ++--- psutil/arch/sunos/net.c | 14 +++++------ psutil/arch/sunos/proc.c | 10 ++++---- psutil/arch/windows/cpu.c | 6 ++--- psutil/arch/windows/disk.c | 4 ++-- psutil/arch/windows/mem.c | 2 +- psutil/arch/windows/pids.c | 5 +++- psutil/arch/windows/proc.c | 38 +++++++++++++++--------------- psutil/arch/windows/proc_handles.c | 12 +++++----- psutil/arch/windows/proc_info.c | 8 +++---- psutil/arch/windows/proc_utils.c | 6 ++--- psutil/arch/windows/security.c | 12 +++++----- psutil/arch/windows/sensors.c | 2 +- psutil/arch/windows/services.c | 22 ++++++++--------- psutil/arch/windows/sys.c | 14 ++++------- psutil/arch/windows/wmi.c | 4 ++-- 49 files changed, 203 insertions(+), 195 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 6253361dcc..00d9cc1be6 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -73,7 +73,7 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { nbytes = read(fd, fstruct, size); if (nbytes <= 0) { close(fd); - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return 0; } if (nbytes != size) { @@ -183,7 +183,7 @@ psutil_proc_args(PyObject *self, PyObject *args) { procbuf.pi_pid = pid; ret = getargs(&procbuf, sizeof(procbuf), argbuf, ARG_MAX); if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -244,7 +244,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { procbuf.pi_pid = pid; ret = getevars(&procbuf, sizeof(procbuf), envbuf, ARG_MAX); if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -308,7 +308,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { /* Get the count of threads */ thread_count = perfstat_thread(NULL, NULL, sizeof(perfstat_thread_t), 0); if (thread_count <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -326,7 +326,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { &id, threadt, sizeof(perfstat_thread_t), thread_count ); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -371,7 +371,7 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { snprintf(id.name, sizeof(id.name), "%ld", pid); rc = perfstat_process(&id, &procinfo, sizeof(perfstat_process_t), 1); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -497,7 +497,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { file = setmntent(MNTTAB, "rb"); if (file == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } mt = getmntent(file); @@ -564,7 +564,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { return py_retdict; } if (tot < 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } statp = (perfstat_netinterface_t *)malloc( @@ -579,7 +579,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { &first, statp, sizeof(perfstat_netinterface_t), tot ); if (tot < 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -662,7 +662,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { Py_XDECREF(py_is_up); if (sock != 0) close(sock); - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -709,14 +709,14 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { /* get the number of ticks per second */ ticks = sysconf(_SC_CLK_TCK); if (ticks < 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } /* get the number of cpus in ncpu */ ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); if (ncpu <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -731,7 +731,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { rc = perfstat_cpu(&id, cpu, sizeof(perfstat_cpu_t), ncpu); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -778,7 +778,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { /* Get the count of disks */ disk_count = perfstat_disk(NULL, NULL, sizeof(perfstat_disk_t), 0); if (disk_count <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -792,7 +792,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { strcpy(id.name, FIRST_DISK); rc = perfstat_disk(&id, diskt, sizeof(perfstat_disk_t), disk_count); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -837,7 +837,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { NULL, &memory, sizeof(perfstat_memory_total_t), 1 ); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -865,7 +865,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { NULL, &memory, sizeof(perfstat_memory_total_t), 1 ); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -897,7 +897,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { /* get the number of cpus in ncpu */ ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); if (ncpu <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -912,7 +912,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { rc = perfstat_cpu(&id, cpu, sizeof(perfstat_cpu_t), ncpu); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/aix/common.c b/psutil/arch/aix/common.c index b0d0de3997..88302c6444 100644 --- a/psutil/arch/aix/common.c +++ b/psutil/arch/aix/common.c @@ -21,12 +21,12 @@ psutil_kread( int br; if (lseek64(Kd, (off64_t)addr, L_SET) == (off64_t)-1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return 1; } br = read(Kd, buf, len); if (br == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return 1; } if (br != len) { diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c index eb254e5c24..f702fd1769 100644 --- a/psutil/arch/all/init.c +++ b/psutil/arch/all/init.c @@ -51,10 +51,22 @@ AccessDenied(const char *syscall) { } +// Same as PyErr_SetFromErrno(0). +PyObject * +psutil_oserror(void) { +#ifdef PSUTIL_WINDOWS + PyErr_SetFromWindowsErr(0); +#else + PyErr_SetFromErrno(PyExc_OSError); +#endif + return NULL; +} + + // Same as PyErr_SetFromErrno(0) but adds the syscall to the exception // message. PyObject * -psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { +psutil_oserror_wsyscall(const char *syscall) { char fullmsg[1024]; #ifdef PSUTIL_WINDOWS diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index dfa21ab218..d8e20ad4d7 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -72,7 +72,8 @@ extern int PSUTIL_CONN_NONE; PyObject *AccessDenied(const char *msg); PyObject *NoSuchProcess(const char *msg); -PyObject *psutil_PyErr_SetFromOSErrnoWithSyscall(const char *syscall); +PyObject *psutil_oserror(void); +PyObject *psutil_oserror_wsyscall(const char *syscall); // ==================================================================== // --- Backward compatibility with missing Python.h APIs diff --git a/psutil/arch/bsd/disk.c b/psutil/arch/bsd/disk.c index 46a7101323..fbd0fded00 100644 --- a/psutil/arch/bsd/disk.c +++ b/psutil/arch/bsd/disk.c @@ -48,7 +48,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { #endif Py_END_ALLOW_THREADS if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -67,7 +67,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { #endif Py_END_ALLOW_THREADS if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index bfa39ddb08..de13245b98 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -378,7 +378,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { #endif default: sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); - psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + psutil_oserror_wsyscall(errbuf); break; } goto error; diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 2cd96be673..c936abdedc 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -173,6 +173,6 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { if (errno == ENOENT) PyErr_SetString(PyExc_NotImplementedError, "unable to read frequency"); else - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 85013205d2..2ccb44650f 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -66,9 +66,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { return NULL; if (psutil_sysctl(mib, 2, &vm, size_vm) != 0) { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctl(CTL_VM | VM_METER)" - ); + return psutil_oserror_wsyscall("sysctl(CTL_VM | VM_METER)"); } return Py_BuildValue( diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 1dde788449..262b4c1748 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -47,7 +47,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { size = sizeof(struct kinfo_proc); if (sysctl((int *)mib, 4, proc, &size, NULL, 0) == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROC_PID)"); + psutil_oserror_wsyscall("sysctl(KERN_PROC_PID)"); return -1; } @@ -160,9 +160,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { return PyUnicode_DecodeFSDefault(""); } else { - return psutil_PyErr_SetFromOSErrnoWithSyscall( - "sysctl(KERN_PROC_PATHNAME)" - ); + return psutil_oserror_wsyscall("sysctl(KERN_PROC_PATHNAME)"); } } if (size == 0 || strlen(pathname) == 0) { @@ -479,7 +477,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, sizeof(mask), &mask ); if (ret != 0) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -540,7 +538,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, sizeof(cpu_set), &cpu_set ); if (ret != 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -629,6 +627,6 @@ psutil_proc_setrlimit(PyObject *self, PyObject *args) { newp = &new; ret = sysctl(name, 5, NULL, 0, newp, sizeof(*newp)); if (ret == -1) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); Py_RETURN_NONE; } diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 7b63f5c73c..91acea812e 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -30,7 +30,7 @@ psutil_fetch_tcplist(void) { for (;;) { if (sysctlbyname("net.inet.tcp.pcblist", NULL, &len, NULL, 0) < 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } buf = malloc(len); @@ -40,7 +40,7 @@ psutil_fetch_tcplist(void) { } if (sysctlbyname("net.inet.tcp.pcblist", buf, &len, NULL, 0) < 0) { free(buf); - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } return buf; @@ -245,7 +245,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { tcplist = psutil_fetch_tcplist(); if (tcplist == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 089ba93a0b..594c6cda09 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -42,7 +42,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { if (errno == ENOENT) PyErr_SetString(PyExc_NotImplementedError, "no battery"); else - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -75,6 +75,6 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { if (errno == ENOENT) PyErr_SetString(PyExc_NotImplementedError, "no temperature sensors"); else - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 6c02f0e08e..2c35deb6a2 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -33,7 +33,7 @@ psutil_populate_xfiles(struct xfile **psutil_xfiles, int *psutil_nxfiles) { while (sysctlbyname("kern.file", *psutil_xfiles, &len, 0, 0) == -1) { if (errno != ENOMEM) { - PyErr_SetFromErrno(0); + psutil_oserror(); return -1; } len *= 2; @@ -121,7 +121,7 @@ psutil_gather_inet( if (sysctlbyname(varname, buf, &len, NULL, 0) == 0) break; if (errno != ENOMEM) { - PyErr_SetFromErrno(0); + psutil_oserror(); goto error; } bufsize *= 2; @@ -294,7 +294,7 @@ psutil_gather_unix( if (sysctlbyname(varname, buf, &len, NULL, 0) == 0) break; if (errno != ENOMEM) { - PyErr_SetFromErrno(0); + psutil_oserror(); goto error; } bufsize *= 2; diff --git a/psutil/arch/linux/mem.c b/psutil/arch/linux/mem.c index 8fb1d37103..044e6042d4 100644 --- a/psutil/arch/linux/mem.c +++ b/psutil/arch/linux/mem.c @@ -15,7 +15,7 @@ psutil_linux_sysinfo(PyObject *self, PyObject *args) { struct sysinfo info; if (sysinfo(&info) != 0) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); // note: boot time might also be determined from here return Py_BuildValue( "(kkkkkkI)", diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index 8b1eadac2c..cbc6c57ad3 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -70,7 +70,7 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) - return psutil_PyErr_SetFromOSErrnoWithSyscall("socket()"); + return psutil_oserror_wsyscall("socket()"); PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // duplex and speed @@ -101,7 +101,7 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { speed = 0; } else { - psutil_PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCETHTOOL)"); + psutil_oserror_wsyscall("ioctl(SIOCETHTOOL)"); goto error; } } diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index f0458e6c10..be7bcd0de6 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -43,7 +43,7 @@ psutil_proc_ioprio_get(PyObject *self, PyObject *args) { return NULL; ioprio = ioprio_get(IOPRIO_WHO_PROCESS, pid); if (ioprio == -1) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); ioclass = IOPRIO_PRIO_CLASS(ioprio); iodata = IOPRIO_PRIO_DATA(ioprio); return Py_BuildValue("ii", ioclass, iodata); @@ -65,7 +65,7 @@ psutil_proc_ioprio_set(PyObject *self, PyObject *args) { ioprio = IOPRIO_PRIO_VALUE(ioclass, iodata); retval = ioprio_set(IOPRIO_WHO_PROCESS, pid, ioprio); if (retval == -1) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); Py_RETURN_NONE; } #endif // PSUTIL_HAS_IOPRIO @@ -98,7 +98,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { break; CPU_FREE(mask); if (errno != EINVAL) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); if (ncpus > INT_MAX / 2) { PyErr_SetString( PyExc_OverflowError, @@ -181,7 +181,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { len = sizeof(cpu_set); if (sched_setaffinity(pid, len, &cpu_set)) { - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); } Py_RETURN_NONE; diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 2b2604f0fe..14338588ef 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -66,12 +66,12 @@ psutil_swap_mem(PyObject *self, PyObject *args) { swdev = calloc(nswap, sizeof(*swdev)); if (swdev == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } if (swapctl(SWAP_STATS, swdev, nswap) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/netbsd/pids.c b/psutil/arch/netbsd/pids.c index ea7abb3749..a5034b9c37 100644 --- a/psutil/arch/netbsd/pids.c +++ b/psutil/arch/netbsd/pids.c @@ -10,6 +10,8 @@ #include #include +#include "../../arch/all/init.h" + int _psutil_pids(pid_t **pids_array, int *pids_count) { diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 2ce73f7d55..bae5440d1f 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -39,7 +39,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc) { ret = sysctl((int *)mib, 6, proc, &size, NULL, 0); if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return -1; } // sysctl stores 0 in the size if we can't find the process information. @@ -67,7 +67,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { if (errno == ENOENT) NoSuchProcess("sysctl -> ENOENT"); else - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } #else @@ -83,7 +83,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { if (errno == ENOENT) NoSuchProcess("readlink -> ENOENT"); else - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } path[len] = '\0'; @@ -123,13 +123,13 @@ psutil_proc_exe(PyObject *self, PyObject *args) { size = sizeof(pathname); error = sysctl(mib, 4, NULL, &size, NULL, 0); if (error == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } error = sysctl(mib, 4, pathname, &size, NULL, 0); if (error == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } if (size == 0 || strlen(pathname) == 0) { @@ -188,7 +188,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // first query size st = sysctl(mib, 5, NULL, &size, NULL, 0); if (st == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } if (size == 0) { diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index e67d2e8eaf..668bd89e14 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -105,7 +105,7 @@ psutil_get_files(void) { mib[5] = 0; if (sysctl(mib, 6, NULL, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -118,7 +118,7 @@ psutil_get_files(void) { } if (sysctl(mib, 6, buf + offset, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -169,12 +169,12 @@ psutil_get_sockets(const char *name) { memset(mib, 0, sizeof(mib)); if (sysctlnametomib(name, mib, &namelen) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } if (sysctl(mib, __arraycount(mib), NULL, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -188,7 +188,7 @@ psutil_get_sockets(const char *name) { mib[7] = len / sizeof(*pcb); if (sysctl(mib, __arraycount(mib), pcb, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index 9fab2779d4..e2d5535e39 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -69,7 +69,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { int nswap, i; if ((nswap = swapctl(SWAP_NSWAP, 0, 0)) == 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -79,7 +79,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } if (swapctl(SWAP_STATS, swdev, nswap) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 9951e944db..268cdd2b7e 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -40,7 +40,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { ret = sysctl((int *)mib, 6, proc, &size, NULL, 0); if (ret == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(kinfo_proc)"); + psutil_oserror_wsyscall("sysctl(kinfo_proc)"); return -1; } // sysctl stores 0 in the size if we can't find the process information. @@ -237,7 +237,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { return Py_BuildValue("s", ""); } else { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } } diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c index 574bdc6eee..ab4ad51cac 100644 --- a/psutil/arch/openbsd/socks.c +++ b/psutil/arch/openbsd/socks.c @@ -68,7 +68,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { ikf = kvm_getfiles(kd, KERN_FILE_BYPID, -1, sizeof(*ikf), &cnt); if (!ikf) { - psutil_PyErr_SetFromOSErrnoWithSyscall("kvm_getfiles"); + psutil_oserror_wsyscall("kvm_getfiles"); goto error; } diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 37932a4a1e..e81033e05a 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -260,7 +260,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { int mib[2] = {CTL_HW, HW_CPU_FREQ}; if (psutil_sysctl(mib, 2, &curr, sizeof(curr)) < 0) - return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(HW_CPU_FREQ)"); + return psutil_oserror_wsyscall("sysctl(HW_CPU_FREQ)"); if (psutil_sysctlbyname("hw.cpufrequency_min", &min, sizeof(min)) != 0) { min = 0; diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index a84dfd7239..9341ee5f70 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -46,7 +46,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { num = getfsstat(NULL, 0, MNT_NOWAIT); Py_END_ALLOW_THREADS if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -61,7 +61,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { num = getfsstat(fs, len, MNT_NOWAIT); Py_END_ALLOW_THREADS if (num == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/osx/init.c b/psutil/arch/osx/init.c index 7120e8ab0e..1f4aab463f 100644 --- a/psutil/arch/osx/init.c +++ b/psutil/arch/osx/init.c @@ -20,7 +20,7 @@ psutil_setup_osx(void) { ret = mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); if (ret != KERN_SUCCESS) { - psutil_PyErr_SetFromOSErrnoWithSyscall("mach_timebase_info"); + psutil_oserror_wsyscall("mach_timebase_info"); return -1; } return 0; diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index dc9b013ffe..7f1741a222 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -99,7 +99,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { mib[1] = VM_SWAPUSAGE; if (psutil_sysctl(mib, 2, &totals, sizeof(totals)) != 0) - return psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(VM_SWAPUSAGE)"); + return psutil_oserror_wsyscall("sysctl(VM_SWAPUSAGE)"); if (psutil_sys_vminfo(&vmstat) != 0) return NULL; diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 3b35c46bc8..83418b2f07 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -56,7 +56,7 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { // raise an exception and throw errno as the error - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl"); + psutil_oserror_wsyscall("sysctl"); return -1; } @@ -117,7 +117,7 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); return -1; } - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl(KERN_PROCARGS2)"); + psutil_oserror_wsyscall("sysctl(KERN_PROCARGS2)"); return -1; } return 0; @@ -929,7 +929,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { sizeof(lip) ); if (!ntopret) { - psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + psutil_oserror_wsyscall("inet_ntop()"); goto error; } ntopret = inet_ntop( @@ -940,7 +940,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { sizeof(rip) ); if (!ntopret) { - psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + psutil_oserror_wsyscall("inet_ntop()"); goto error; } } @@ -952,7 +952,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { sizeof(lip) ); if (!ntopret) { - psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + psutil_oserror_wsyscall("inet_ntop()"); goto error; } ntopret = inet_ntop( @@ -962,7 +962,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { sizeof(rip) ); if (!ntopret) { - psutil_PyErr_SetFromOSErrnoWithSyscall("inet_ntop()"); + psutil_oserror_wsyscall("inet_ntop()"); goto error; } } diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index 9cb38c92dc..4ce1755bb3 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -142,7 +142,7 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; if (getifaddrs(&ifaddr) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -246,7 +246,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { error: if (sock != -1) close(sock); - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); } static int @@ -285,14 +285,14 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("socket(SOCK_DGRAM)"); + psutil_oserror_wsyscall("socket(SOCK_DGRAM)"); goto error; } PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); ret = ioctl(sock, SIOCGIFFLAGS, &ifr); if (ret == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("ioctl(SIOCGIFFLAGS)"); + psutil_oserror_wsyscall("ioctl(SIOCGIFFLAGS)"); goto error; } @@ -486,7 +486,7 @@ psutil_net_if_is_running(PyObject *self, PyObject *args) { error: if (sock != -1) close(sock); - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); } @@ -652,7 +652,7 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); // speed / duplex diff --git a/psutil/arch/posix/proc.c b/psutil/arch/posix/proc.c index 800e56dde6..46b01c1d3d 100644 --- a/psutil/arch/posix/proc.c +++ b/psutil/arch/posix/proc.c @@ -53,7 +53,7 @@ psutil_pid_exists(pid_t pid) { // (EINVAL, EPERM, ESRCH) therefore we should never get // here. If we do let's be explicit in considering this // an error. - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return -1; } } @@ -70,7 +70,7 @@ psutil_pid_exists(pid_t pid) { void psutil_raise_for_pid(pid_t pid, char *syscall) { if (errno != 0) - psutil_PyErr_SetFromOSErrnoWithSyscall(syscall); + psutil_oserror_wsyscall(syscall); else if (psutil_pid_exists(pid) == 0) NoSuchProcess(syscall); else @@ -94,7 +94,7 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { priority = getpriority(PRIO_PROCESS, pid); #endif if (errno != 0) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); return Py_BuildValue("i", priority); } @@ -115,6 +115,6 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { retval = setpriority(PRIO_PROCESS, pid, priority); #endif if (retval == -1) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); Py_RETURN_NONE; } diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index 4edc21450c..67e0df7d9d 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -24,7 +24,7 @@ psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen) { return psutil_badargs("psutil_sysctl"); if (sysctl(mib, miblen, buf, &len, NULL, 0) == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl()"); + psutil_oserror_wsyscall("sysctl()"); return -1; } @@ -52,7 +52,7 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { // First query to determine required size ret = sysctl(mib, miblen, NULL, &needed, NULL, 0); if (ret == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl() malloc 1/3"); + psutil_oserror_wsyscall("sysctl() malloc 1/3"); return -1; } @@ -85,7 +85,7 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { // Re-query needed size for next attempt if (sysctl(mib, miblen, NULL, &needed, NULL, 0) == -1) { - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl() malloc 2/3"); + psutil_oserror_wsyscall("sysctl() malloc 2/3"); return -1; } @@ -95,7 +95,7 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { // Other errors: clean up and give up free(buffer); - psutil_PyErr_SetFromOSErrnoWithSyscall("sysctl() malloc 3/3"); + psutil_oserror_wsyscall("sysctl() malloc 3/3"); return -1; } @@ -137,7 +137,7 @@ psutil_sysctlbyname(const char *name, void *buf, size_t buflen) { if (sysctlbyname(name, buf, &len, NULL, 0) == -1) { snprintf(errbuf, sizeof(errbuf), "sysctlbyname('%s')", name); - psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + psutil_oserror_wsyscall(errbuf); return -1; } @@ -178,7 +178,7 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { snprintf( errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 1/3", name ); - psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + psutil_oserror_wsyscall(errbuf); return -1; } @@ -215,7 +215,7 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { "sysctlbyname('%s') malloc 2/3", name ); - psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + psutil_oserror_wsyscall(errbuf); return -1; } @@ -228,7 +228,7 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { snprintf( errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 3/3", name ); - psutil_PyErr_SetFromOSErrnoWithSyscall(errbuf); + psutil_oserror_wsyscall(errbuf); return -1; } diff --git a/psutil/arch/sunos/cpu.c b/psutil/arch/sunos/cpu.c index 361b741558..a1780dd766 100644 --- a/psutil/arch/sunos/cpu.c +++ b/psutil/arch/sunos/cpu.c @@ -24,14 +24,14 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { kc = kstat_open(); if (kc == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } for (ksp = kc->kc_chain; ksp != NULL; ksp = ksp->ks_next) { if (strcmp(ksp->ks_module, "cpu_stat") == 0) { if (kstat_read(kc, ksp, &cs) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } py_cputime = Py_BuildValue( @@ -110,14 +110,14 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { kc = kstat_open(); if (kc == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } for (ksp = kc->kc_chain; ksp != NULL; ksp = ksp->ks_next) { if (strcmp(ksp->ks_module, "cpu_stat") == 0) { if (kstat_read(kc, ksp, &cs) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } // voluntary + involuntary diff --git a/psutil/arch/sunos/disk.c b/psutil/arch/sunos/disk.c index 2d37d8d317..7b08c6df3a 100644 --- a/psutil/arch/sunos/disk.c +++ b/psutil/arch/sunos/disk.c @@ -23,7 +23,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { return NULL; kc = kstat_open(); if (kc == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } ksp = kc->kc_chain; @@ -32,7 +32,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (strcmp(ksp->ks_class, "disk") == 0) { if (kstat_read(kc, ksp, &kio) == -1) { kstat_close(kc); - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); ; } py_disk_info = Py_BuildValue( @@ -82,7 +82,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { file = fopen(MNTTAB, "rb"); if (file == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/sunos/environ.c b/psutil/arch/sunos/environ.c index 2e0767b32d..d6c533f4c7 100644 --- a/psutil/arch/sunos/environ.c +++ b/psutil/arch/sunos/environ.c @@ -36,7 +36,7 @@ open_address_space(pid_t pid, const char *procfs_path) { snprintf(proc_path, PATH_MAX, "%s/%i/as", procfs_path, pid); fd = open(proc_path, O_RDONLY); if (fd < 0) - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return fd; } @@ -71,7 +71,7 @@ read_offt(int fd, off_t offset, char *buf, size_t buf_size) { return stored; error: - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return -1; } @@ -93,7 +93,7 @@ read_cstring_offt(int fd, off_t offset) { char *result = NULL; if (lseek(fd, offset, SEEK_SET) == (off_t)-1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -101,7 +101,7 @@ read_cstring_offt(int fd, off_t offset) { for (;;) { r = read(fd, buf, sizeof(buf)); if (r == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } else if (r == 0) { @@ -273,7 +273,7 @@ search_pointers_vector_size_offt(int fd, off_t offt, size_t ptr_size) { return count; error: - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return -1; } diff --git a/psutil/arch/sunos/mem.c b/psutil/arch/sunos/mem.c index b78f14c11d..eedf6f68c7 100644 --- a/psutil/arch/sunos/mem.c +++ b/psutil/arch/sunos/mem.c @@ -31,7 +31,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { int num; if ((num = swapctl(SC_GETNSWP, NULL)) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } if (num == 0) { @@ -53,7 +53,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } st->swt_n = num; if ((num = swapctl(SC_LIST, st)) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -79,7 +79,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { kc = kstat_open(); if (kc == NULL) - return PyErr_SetFromErrno(PyExc_OSError); + return psutil_oserror(); ; k = kc->kc_chain; diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c index c1b3b008d0..e38c86b614 100644 --- a/psutil/arch/sunos/net.c +++ b/psutil/arch/sunos/net.c @@ -45,7 +45,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -162,7 +162,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { goto error; sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -242,7 +242,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { close(sock); if (kc != NULL) kstat_close(kc); - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return NULL; } @@ -298,12 +298,12 @@ psutil_net_connections(PyObject *self, PyObject *args) { ret = ioctl(sd, I_PUSH, "tcp"); if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } ret = ioctl(sd, I_PUSH, "udp"); if (ret == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } // @@ -327,7 +327,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { flags = 0; // request to be sent in non-priority if (putmsg(sd, &ctlbuf, (struct strbuf *)0, flags) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -372,7 +372,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { flags = 0; getcode = getmsg(sd, (struct strbuf *)0, &databuf, &flags); if (getcode < 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index c354d2c058..3f04004e34 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -27,7 +27,7 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { nbytes = read(fd, fstruct, size); if (nbytes == -1) { close(fd); - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); return 0; } if (nbytes != (ssize_t)size) { @@ -304,7 +304,7 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { // read header nbytes = pread(fd, &header, sizeof(header), 0); if (nbytes == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } if (nbytes != sizeof(header)) { @@ -326,7 +326,7 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { // read the rest nbytes = pread(fd, lwp, size, sizeof(header)); if (nbytes == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } if (nbytes != size) { @@ -491,7 +491,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { sprintf(path, "%s/%i/xmap", procfs_path, pid); if (stat(path, &st) == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } @@ -499,7 +499,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { fd = open(path, O_RDONLY); if (fd == -1) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 1723d843d7..455bd272a8 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -23,7 +23,7 @@ psutil_get_num_cpus(int fail_on_err) { if (GetActiveProcessorCount != NULL) { ncpus = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); if ((ncpus == 0) && (fail_on_err == 1)) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); } } else { @@ -54,7 +54,7 @@ psutil_cpu_times(PyObject *self, PyObject *args) { FILETIME idle_time, kernel_time, user_time; if (!GetSystemTimes(&idle_time, &kernel_time, &user_time)) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); return NULL; } @@ -396,7 +396,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { size = ncpus * sizeof(PROCESSOR_POWER_INFORMATION); pBuffer = (BYTE *)LocalAlloc(LPTR, size); if (!pBuffer) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); return NULL; } diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index fcb1775c61..0641d12e47 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -165,7 +165,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { // XXX: we can also bump into ERROR_MORE_DATA in which case // (quoting doc) we're supposed to retry with a bigger buffer // and specify a new "starting point", whatever it means. - PyErr_SetFromWindowsErr(0); + psutil_oserror(); goto error; } @@ -244,7 +244,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { Py_END_ALLOW_THREADS if (num_bytes == 0) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); goto error; } diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c index b792d11f06..e8889f7dfb 100644 --- a/psutil/arch/windows/mem.c +++ b/psutil/arch/windows/mem.c @@ -26,7 +26,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { PERFORMANCE_INFORMATION perfInfo; if (!GetPerformanceInfo(&perfInfo, sizeof(PERFORMANCE_INFORMATION))) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); return NULL; } // values are size_t, widen (if needed) to long long diff --git a/psutil/arch/windows/pids.c b/psutil/arch/windows/pids.c index 6f4b4ee6a2..8101aaeee8 100644 --- a/psutil/arch/windows/pids.c +++ b/psutil/arch/windows/pids.c @@ -8,6 +8,9 @@ #include #include +#include "../../arch/all/init.h" + + int _psutil_pids(DWORD **pids_array, int *pids_count) { DWORD *proc_array = NULL; @@ -32,7 +35,7 @@ _psutil_pids(DWORD **pids_array, int *pids_count) { if (!EnumProcesses(proc_array, proc_array_bytes, &enum_return_bytes)) { free(proc_array); - PyErr_SetFromWindowsErr(0); + psutil_oserror(); return -1; } diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 451cf072a9..687daee81a 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -73,7 +73,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { // https://github.com/giampaolo/psutil/issues/1099 // http://bugs.python.org/issue14252 if (GetLastError() != ERROR_ACCESS_DENIED) { - psutil_PyErr_SetFromOSErrnoWithSyscall("TerminateProcess"); + psutil_oserror_wsyscall("TerminateProcess"); return NULL; } } @@ -109,7 +109,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { Py_RETURN_NONE; } else { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + psutil_oserror_wsyscall("OpenProcess"); return NULL; } } @@ -121,7 +121,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // handle return code if (retVal == WAIT_FAILED) { - psutil_PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); + psutil_oserror_wsyscall("WaitForSingleObject"); CloseHandle(hProcess); return NULL; } @@ -145,7 +145,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { // process is gone so we can get its process exit code. The PID // may still stick around though but we'll handle that from Python. if (GetExitCodeProcess(hProcess, &ExitCode) == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + psutil_oserror_wsyscall("GetExitCodeProcess"); CloseHandle(hProcess); return NULL; } @@ -179,7 +179,7 @@ psutil_proc_times(PyObject *self, PyObject *args) { NoSuchProcess("GetProcessTimes -> ERROR_ACCESS_DENIED"); } else { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); } CloseHandle(hProcess); return NULL; @@ -339,7 +339,7 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, sizeof(cnt) )) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); CloseHandle(hProcess); return NULL; } @@ -564,7 +564,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThreadSnap == INVALID_HANDLE_VALUE) { - psutil_PyErr_SetFromOSErrnoWithSyscall("CreateToolhelp32Snapshot"); + psutil_oserror_wsyscall("CreateToolhelp32Snapshot"); goto error; } @@ -572,7 +572,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { te32.dwSize = sizeof(THREADENTRY32); if (!Thread32First(hThreadSnap, &te32)) { - psutil_PyErr_SetFromOSErrnoWithSyscall("Thread32First"); + psutil_oserror_wsyscall("Thread32First"); goto error; } @@ -594,7 +594,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { hThread, &ftDummy, &ftDummy, &ftKernel, &ftUser ); if (rc == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall("GetThreadTimes"); + psutil_oserror_wsyscall("GetThreadTimes"); goto error; } @@ -671,7 +671,7 @@ _psutil_user_token_from_pid(DWORD pid) { return NULL; if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + psutil_oserror_wsyscall("OpenProcessToken"); goto error; } @@ -691,7 +691,7 @@ _psutil_user_token_from_pid(DWORD pid) { continue; } else { - psutil_PyErr_SetFromOSErrnoWithSyscall("GetTokenInformation"); + psutil_oserror_wsyscall("GetTokenInformation"); goto error; } } @@ -774,7 +774,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { goto error; } else { - psutil_PyErr_SetFromOSErrnoWithSyscall("LookupAccountSidW"); + psutil_oserror_wsyscall("LookupAccountSidW"); goto error; } } @@ -830,7 +830,7 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { priority = GetPriorityClass(hProcess); if (priority == 0) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); CloseHandle(hProcess); return NULL; } @@ -858,7 +858,7 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { retval = SetPriorityClass(hProcess, priority); if (retval == 0) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); CloseHandle(hProcess); return NULL; } @@ -941,7 +941,7 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { return NULL; if (!GetProcessIoCounters(hProcess, &IoCounters)) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); CloseHandle(hProcess); return NULL; } @@ -976,7 +976,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { return NULL; } if (GetProcessAffinityMask(hProcess, &proc_mask, &system_mask) == 0) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); CloseHandle(hProcess); return NULL; } @@ -1013,7 +1013,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { return NULL; if (SetProcessAffinityMask(hProcess, mask) == 0) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); CloseHandle(hProcess); return NULL; } @@ -1065,7 +1065,7 @@ psutil_proc_num_handles(PyObject *self, PyObject *args) { if (NULL == hProcess) return NULL; if (!GetProcessHandleCount(hProcess, &handleCount)) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); CloseHandle(hProcess); return NULL; } @@ -1196,7 +1196,7 @@ psutil_ppid_map(PyObject *self, PyObject *args) { return NULL; handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (handle == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); Py_DECREF(py_retdict); return NULL; } diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 09b0ee0779..8ca6967abf 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -155,7 +155,7 @@ psutil_threaded_get_filename(HANDLE hFile) { NULL, 0, (LPTHREAD_START_ROUTINE)psutil_get_filename, &hFile, 0, NULL ); if (hThread == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("CreateThread"); + psutil_oserror_wsyscall("CreateThread"); return -1; } @@ -168,7 +168,7 @@ psutil_threaded_get_filename(HANDLE hFile) { "get handle name thread timed out after %i ms", THREAD_TIMEOUT ); if (TerminateThread(hThread, 0) == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall("TerminateThread"); + psutil_oserror_wsyscall("TerminateThread"); CloseHandle(hThread); return -1; } @@ -179,20 +179,20 @@ psutil_threaded_get_filename(HANDLE hFile) { if (dwWait == WAIT_FAILED) { psutil_debug("WaitForSingleObject -> WAIT_FAILED"); if (TerminateThread(hThread, 0) == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall( + psutil_oserror_wsyscall( "WaitForSingleObject -> WAIT_FAILED -> TerminateThread" ); CloseHandle(hThread); return -1; } - psutil_PyErr_SetFromOSErrnoWithSyscall("WaitForSingleObject"); + psutil_oserror_wsyscall("WaitForSingleObject"); CloseHandle(hThread); return -1; } if (GetExitCodeThread(hThread, &threadRetValue) == 0) { if (TerminateThread(hThread, 0) == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall( + psutil_oserror_wsyscall( "GetExitCodeThread (failed) -> TerminateThread" ); CloseHandle(hThread); @@ -200,7 +200,7 @@ psutil_threaded_get_filename(HANDLE hFile) { } CloseHandle(hThread); - psutil_PyErr_SetFromOSErrnoWithSyscall("GetExitCodeThread"); + psutil_oserror_wsyscall("GetExitCodeThread"); return -1; } CloseHandle(hThread); diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index e370fc44b0..a7d91e578e 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -44,7 +44,7 @@ psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { MEMORY_BASIC_INFORMATION info; if (!VirtualQueryEx(hProcess, src, &info, sizeof(info))) { - psutil_PyErr_SetFromOSErrnoWithSyscall("VirtualQueryEx"); + psutil_oserror_wsyscall("VirtualQueryEx"); return -1; } @@ -70,7 +70,7 @@ psutil_convert_winerr(ULONG err, char *syscall) { AccessDenied(fullmsg); } else { - psutil_PyErr_SetFromOSErrnoWithSyscall(syscall); + psutil_oserror_wsyscall(syscall); } } @@ -231,7 +231,7 @@ psutil_get_process_data( if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || !IsWow64Process(hProcess, &theyAreWow64)) { - psutil_PyErr_SetFromOSErrnoWithSyscall("IsWow64Process"); + psutil_oserror_wsyscall("IsWow64Process"); goto error; } @@ -591,7 +591,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(data, &nArgs); if (szArglist == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("CommandLineToArgvW"); + psutil_oserror_wsyscall("CommandLineToArgvW"); goto error; } diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index f2acd1b197..6067c6fad1 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -62,7 +62,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { } return NULL; } - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + psutil_oserror_wsyscall("OpenProcess"); return NULL; } @@ -88,7 +88,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { SetLastError(0); return hProcess; } - psutil_PyErr_SetFromOSErrnoWithSyscall("GetExitCodeProcess"); + psutil_oserror_wsyscall("GetExitCodeProcess"); CloseHandle(hProcess); return NULL; } @@ -110,7 +110,7 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { hProcess = OpenProcess(access, FALSE, pid); if ((hProcess == NULL) && (GetLastError() == ERROR_ACCESS_DENIED)) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcess"); + psutil_oserror_wsyscall("OpenProcess"); return NULL; } diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index 627d8ac12d..b00483747b 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -21,7 +21,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { DWORD cbPrevious = sizeof(TOKEN_PRIVILEGES); if (!LookupPrivilegeValue(NULL, Privilege, &luid)) { - psutil_PyErr_SetFromOSErrnoWithSyscall("LookupPrivilegeValue"); + psutil_oserror_wsyscall("LookupPrivilegeValue"); return -1; } @@ -39,7 +39,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { &cbPrevious )) { - psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); + psutil_oserror_wsyscall("AdjustTokenPrivileges"); return -1; } @@ -57,7 +57,7 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) { hToken, FALSE, &tpPrevious, cbPrevious, NULL, NULL )) { - psutil_PyErr_SetFromOSErrnoWithSyscall("AdjustTokenPrivileges"); + psutil_oserror_wsyscall("AdjustTokenPrivileges"); return -1; } @@ -74,19 +74,19 @@ psutil_get_thisproc_token() { { if (GetLastError() == ERROR_NO_TOKEN) { if (!ImpersonateSelf(SecurityImpersonation)) { - psutil_PyErr_SetFromOSErrnoWithSyscall("ImpersonateSelf"); + psutil_oserror_wsyscall("ImpersonateSelf"); return NULL; } if (!OpenProcessToken( me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken )) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + psutil_oserror_wsyscall("OpenProcessToken"); return NULL; } } else { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenProcessToken"); + psutil_oserror_wsyscall("OpenProcessToken"); return NULL; } } diff --git a/psutil/arch/windows/sensors.c b/psutil/arch/windows/sensors.c index 8bb636c44e..6b599659f0 100644 --- a/psutil/arch/windows/sensors.c +++ b/psutil/arch/windows/sensors.c @@ -17,7 +17,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { SYSTEM_POWER_STATUS sps; if (GetSystemPowerStatus(&sps) == 0) { - PyErr_SetFromWindowsErr(0); + psutil_oserror(); return NULL; } return Py_BuildValue( diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index dfb8075c78..59178c5363 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -25,13 +25,13 @@ psutil_get_service_handler( sc = OpenSCManagerW(NULL, NULL, scm_access); if (sc == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenSCManagerW"); + psutil_oserror_wsyscall("OpenSCManagerW"); return NULL; } hService = OpenServiceW(sc, service_name, access); if (hService == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenServiceW"); + psutil_oserror_wsyscall("OpenServiceW"); CloseServiceHandle(sc); return NULL; } @@ -145,7 +145,7 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { sc = OpenSCManager(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE); if (sc == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("OpenSCManager"); + psutil_oserror_wsyscall("OpenSCManager"); return NULL; } @@ -245,7 +245,7 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { bytesNeeded = 0; QueryServiceConfigW(hService, NULL, 0, &bytesNeeded); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); + psutil_oserror_wsyscall("QueryServiceConfigW"); goto error; } @@ -257,7 +257,7 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { ok = QueryServiceConfigW(hService, qsc, bytesNeeded, &bytesNeeded); if (!ok) { - psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfigW"); + psutil_oserror_wsyscall("QueryServiceConfigW"); goto error; } @@ -350,7 +350,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { return Py_BuildValue("s", ""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); + psutil_oserror_wsyscall("QueryServiceStatusEx"); goto error; } @@ -371,7 +371,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { &bytesNeeded ); if (!ok) { - psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceStatusEx"); + psutil_oserror_wsyscall("QueryServiceStatusEx"); goto error; } @@ -428,7 +428,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); + psutil_oserror_wsyscall("QueryServiceConfig2W"); goto error; } @@ -446,7 +446,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { &bytesNeeded ); if (!ok) { - psutil_PyErr_SetFromOSErrnoWithSyscall("QueryServiceConfig2W"); + psutil_oserror_wsyscall("QueryServiceConfig2W"); goto error; } @@ -496,7 +496,7 @@ psutil_winservice_start(PyObject *self, PyObject *args) { ok = StartService(hService, 0, NULL); if (!ok) { - psutil_PyErr_SetFromOSErrnoWithSyscall("StartService"); + psutil_oserror_wsyscall("StartService"); goto error; } @@ -535,7 +535,7 @@ psutil_winservice_stop(PyObject *self, PyObject *args) { ok = ControlService(hService, SERVICE_CONTROL_STOP, &ssp); Py_END_ALLOW_THREADS if (!ok) { - psutil_PyErr_SetFromOSErrnoWithSyscall("ControlService"); + psutil_oserror_wsyscall("ControlService"); goto error; } diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index 111772d9ac..612dd2ebb7 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -80,7 +80,7 @@ psutil_users(PyObject *self, PyObject *args) { // return WinError 120. return py_retlist; } - psutil_PyErr_SetFromOSErrnoWithSyscall("WTSEnumerateSessionsW"); + psutil_oserror_wsyscall("WTSEnumerateSessionsW"); goto error; } @@ -106,9 +106,7 @@ psutil_users(PyObject *self, PyObject *args) { ) == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall( - "WTSQuerySessionInformationW" - ); + psutil_oserror_wsyscall("WTSQuerySessionInformationW"); goto error; } if (bytes <= 2) @@ -121,9 +119,7 @@ psutil_users(PyObject *self, PyObject *args) { ) == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall( - "WTSQuerySessionInformationW" - ); + psutil_oserror_wsyscall("WTSQuerySessionInformationW"); goto error; } @@ -156,9 +152,7 @@ psutil_users(PyObject *self, PyObject *args) { ) == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall( - "WTSQuerySessionInformationW" - ); + psutil_oserror_wsyscall("WTSQuerySessionInformationW"); goto error; } wts_info = (PWTSINFOW)buffer_info; diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 0d4d9124df..8a234abb10 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -96,7 +96,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { event = CreateEventW(NULL, FALSE, FALSE, L"LoadUpdateEvent"); if (event == NULL) { - psutil_PyErr_SetFromOSErrnoWithSyscall("CreateEventW"); + psutil_oserror_wsyscall("CreateEventW"); return NULL; } @@ -116,7 +116,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { ); if (ret == 0) { - psutil_PyErr_SetFromOSErrnoWithSyscall("RegisterWaitForSingleObject"); + psutil_oserror_wsyscall("RegisterWaitForSingleObject"); return NULL; } From 5f054b12993272b30ff16a696afec360da38d16a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 15:52:25 +0100 Subject: [PATCH 1406/1714] Add errors.c utility functions (#2670) --- MANIFEST.in | 1 + psutil/_psutil_aix.c | 8 +-- psutil/arch/aix/common.c | 7 ++- psutil/arch/aix/ifaddrs.c | 1 + psutil/arch/aix/net_connections.c | 8 +-- psutil/arch/all/errors.c | 98 ++++++++++++++++++++++++++++++ psutil/arch/all/init.c | 76 ----------------------- psutil/arch/all/init.h | 56 +++++++++-------- psutil/arch/all/pids.c | 2 +- psutil/arch/bsd/init.c | 6 +- psutil/arch/bsd/proc.c | 10 +-- psutil/arch/freebsd/disk.c | 6 +- psutil/arch/freebsd/mem.c | 6 +- psutil/arch/freebsd/pids.c | 2 +- psutil/arch/freebsd/proc.c | 6 +- psutil/arch/freebsd/sys_socks.c | 16 ++--- psutil/arch/linux/disk.c | 2 +- psutil/arch/netbsd/pids.c | 6 +- psutil/arch/netbsd/proc.c | 12 ++-- psutil/arch/openbsd/pids.c | 6 +- psutil/arch/openbsd/proc.c | 6 +- psutil/arch/osx/cpu.c | 31 +++------- psutil/arch/osx/disk.c | 22 +++---- psutil/arch/osx/mem.c | 7 +-- psutil/arch/osx/pids.c | 2 +- psutil/arch/osx/proc.c | 35 +++++------ psutil/arch/osx/sensors.c | 17 ++---- psutil/arch/posix/net.c | 2 +- psutil/arch/posix/proc.c | 4 +- psutil/arch/posix/sysctl.c | 12 ++-- psutil/arch/sunos/cpu.c | 2 + psutil/arch/sunos/disk.c | 2 + psutil/arch/sunos/environ.c | 6 +- psutil/arch/sunos/mem.c | 10 +-- psutil/arch/sunos/net.c | 6 +- psutil/arch/sunos/proc.c | 14 ++--- psutil/arch/sunos/sys.c | 2 +- psutil/arch/windows/cpu.c | 9 +-- psutil/arch/windows/mem.c | 9 +-- psutil/arch/windows/net.c | 13 ++-- psutil/arch/windows/proc.c | 26 ++++---- psutil/arch/windows/proc_handles.c | 3 +- psutil/arch/windows/proc_info.c | 26 ++++---- psutil/arch/windows/proc_utils.c | 10 +-- psutil/arch/windows/socks.c | 6 +- psutil/arch/windows/wmi.c | 7 +-- 46 files changed, 298 insertions(+), 326 deletions(-) create mode 100644 psutil/arch/all/errors.c diff --git a/MANIFEST.in b/MANIFEST.in index 0aba3d60ff..bd7ae3e934 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -46,6 +46,7 @@ include psutil/arch/aix/ifaddrs.h include psutil/arch/aix/net_connections.c include psutil/arch/aix/net_connections.h include psutil/arch/aix/net_kernel_structs.h +include psutil/arch/all/errors.c include psutil/arch/all/init.c include psutil/arch/all/init.h include psutil/arch/all/pids.c diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 00d9cc1be6..ab62918080 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -78,7 +78,7 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { } if (nbytes != size) { close(fd); - PyErr_SetString(PyExc_RuntimeError, "structure size mismatch"); + psutil_runtime_error("psutil_file_to_struct() size mismatch"); return 0; } close(fd); @@ -475,7 +475,7 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { /* finished iteration without finding requested pid */ free(processes); - return NoSuchProcess("psutil_read_process_table (no PID found)"); + return psutil_oserror_nsp("psutil_read_process_table (no PID found)"); } @@ -683,8 +683,8 @@ psutil_boot_time(PyObject *self, PyObject *args) { endutxent(); UTXENT_MUTEX_UNLOCK(); if (boot_time == 0.0) { - /* could not find BOOT_TIME in getutxent loop */ - PyErr_SetString(PyExc_RuntimeError, "can't determine boot time"); + // could not find BOOT_TIME in getutxent loop + psutil_runtime_error("can't determine boot time"); return NULL; } return Py_BuildValue("f", boot_time); diff --git a/psutil/arch/aix/common.c b/psutil/arch/aix/common.c index 88302c6444..5f8ed67317 100644 --- a/psutil/arch/aix/common.c +++ b/psutil/arch/aix/common.c @@ -8,8 +8,11 @@ #include #include #include + +#include "../../arch/all/init.h" #include "common.h" + /* psutil_kread() - read from kernel memory */ int psutil_kread( @@ -30,9 +33,7 @@ psutil_kread( return 1; } if (br != len) { - PyErr_SetString( - PyExc_RuntimeError, "size mismatch when reading kernel memory fd" - ); + psutil_runtime_error("size mismatch when reading kernel memory fd"); return 1; } return 0; diff --git a/psutil/arch/aix/ifaddrs.c b/psutil/arch/aix/ifaddrs.c index af765c9186..e3f0eba3b5 100644 --- a/psutil/arch/aix/ifaddrs.c +++ b/psutil/arch/aix/ifaddrs.c @@ -19,6 +19,7 @@ #include #include "ifaddrs.h" +#include "../../arch/all/init.h" #define MAX(x, y) ((x) > (y) ? (x) : (y)) #define SIZE(p) MAX((p).sa_len, sizeof(p)) diff --git a/psutil/arch/aix/net_connections.c b/psutil/arch/aix/net_connections.c index 41d8ab173e..2ab139b600 100644 --- a/psutil/arch/aix/net_connections.c +++ b/psutil/arch/aix/net_connections.c @@ -100,7 +100,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { } if (!s.so_proto) { - PyErr_SetString(PyExc_RuntimeError, "invalid socket protocol handle"); + psutil_runtime_error("invalid socket protocol handle"); return NULL; } if (psutil_kread(Kd, (KA_T)s.so_proto, (char *)&p, sizeof(p))) { @@ -108,7 +108,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { } if (!p.pr_domain) { - PyErr_SetString(PyExc_RuntimeError, "invalid socket protocol domain"); + psutil_runtime_error("invalid socket protocol domain"); return NULL; } if (psutil_kread(Kd, (KA_T)p.pr_domain, (char *)&d, sizeof(d))) { @@ -119,7 +119,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { if (fam == AF_INET || fam == AF_INET6) { /* Read protocol control block */ if (!s.so_pcb) { - PyErr_SetString(PyExc_RuntimeError, "invalid socket PCB"); + psutil_runtime_error("invalid socket PCB"); return NULL; } if (psutil_kread(Kd, (KA_T)s.so_pcb, (char *)&inp, sizeof(inp))) { @@ -188,7 +188,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { return NULL; } if ((KA_T)f.f_data != (KA_T)unp.unp_socket) { - PyErr_SetString(PyExc_RuntimeError, "unp_socket mismatch"); + psutil_runtime_error("unp_socket mismatch"); return NULL; } diff --git a/psutil/arch/all/errors.c b/psutil/arch/all/errors.c new file mode 100644 index 0000000000..bef0bf6131 --- /dev/null +++ b/psutil/arch/all/errors.c @@ -0,0 +1,98 @@ +/* + * 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. + */ + +#include +#include +#include +#if defined(PSUTIL_WINDOWS) +#include +#endif + +#define MSG_SIZE 512 + + +// Set OSError() based on errno (UNIX) or GetLastError() (Windows). +PyObject * +psutil_oserror(void) { +#ifdef PSUTIL_WINDOWS + PyErr_SetFromWindowsErr(GetLastError()); +#else + PyErr_SetFromErrno(PyExc_OSError); +#endif + return NULL; +} + + +// Same as above, but adds the syscall to the exception message. On +// Windows this is achieved by setting the `filename` attribute of the +// OSError object. +PyObject * +psutil_oserror_wsyscall(const char *syscall) { + char msg[MSG_SIZE]; + +#ifdef PSUTIL_WINDOWS + DWORD err = GetLastError(); + sprintf(msg, "(originated from %s)", syscall); + PyErr_SetFromWindowsErrWithFilename(err, msg); +#else + PyObject *exc; + sprintf(msg, "%s (originated from %s)", strerror(errno), syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", errno, msg); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); +#endif + return NULL; +} + + +// Set OSError(errno=ESRCH) ("No such process"). +PyObject * +psutil_oserror_nsp(const char *syscall) { + PyObject *exc; + char msg[MSG_SIZE]; + + sprintf(msg, "force no such process (originated from %s)", syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", ESRCH, msg); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); + return NULL; +} + + +// Set OSError(errno=EACCES) ("Permission denied"). +PyObject * +psutil_oserror_ad(const char *syscall) { + PyObject *exc; + char msg[MSG_SIZE]; + + sprintf(msg, "force permission denied (originated from %s)", syscall); + exc = PyObject_CallFunction(PyExc_OSError, "(is)", EACCES, msg); + PyErr_SetObject(PyExc_OSError, exc); + Py_XDECREF(exc); + return NULL; +} + + +// Set RuntimeError with formatted `msg` and optional arguments. +PyObject * +psutil_runtime_error(const char *msg, ...) { + va_list args; + + va_start(args, msg); + PyErr_FormatV(PyExc_RuntimeError, msg, args); + va_end(args); + return NULL; +} + + +// Use it when invalid args are passed to a C function. +int +psutil_badargs(const char *funcname) { + PyErr_Format( + PyExc_RuntimeError, "%s() invalid args passed to function", funcname + ); + return -1; +} diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c index f702fd1769..b2647aaa6e 100644 --- a/psutil/arch/all/init.c +++ b/psutil/arch/all/init.c @@ -7,9 +7,6 @@ // Global names shared by all platforms. #include -#ifdef PSUTIL_WINDOWS -#include -#endif #include "init.h" @@ -21,69 +18,6 @@ PyMutex utxent_lock = {0}; #endif -// Set OSError(errno=ESRCH, strerror="No such process (originated from") -// Python exception. -PyObject * -NoSuchProcess(const char *syscall) { - PyObject *exc; - char msg[1024]; - - sprintf(msg, "assume no such process (originated from %s)", syscall); - exc = PyObject_CallFunction(PyExc_OSError, "(is)", ESRCH, msg); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); - return NULL; -} - - -// Set OSError(errno=EACCES, strerror="Permission denied" (originated from ...) -// Python exception. -PyObject * -AccessDenied(const char *syscall) { - PyObject *exc; - char msg[1024]; - - sprintf(msg, "assume access denied (originated from %s)", syscall); - exc = PyObject_CallFunction(PyExc_OSError, "(is)", EACCES, msg); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); - return NULL; -} - - -// Same as PyErr_SetFromErrno(0). -PyObject * -psutil_oserror(void) { -#ifdef PSUTIL_WINDOWS - PyErr_SetFromWindowsErr(0); -#else - PyErr_SetFromErrno(PyExc_OSError); -#endif - return NULL; -} - - -// Same as PyErr_SetFromErrno(0) but adds the syscall to the exception -// message. -PyObject * -psutil_oserror_wsyscall(const char *syscall) { - char fullmsg[1024]; - -#ifdef PSUTIL_WINDOWS - DWORD dwLastError = GetLastError(); - sprintf(fullmsg, "(originated from %s)", syscall); - PyErr_SetFromWindowsErrWithFilename(dwLastError, fullmsg); -#else - PyObject *exc; - sprintf(fullmsg, "%s (originated from %s)", strerror(errno), syscall); - exc = PyObject_CallFunction(PyExc_OSError, "(is)", errno, fullmsg); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); -#endif - return NULL; -} - - // Enable or disable PSUTIL_DEBUG messages. PyObject * psutil_set_debug(PyObject *self, PyObject *args) { @@ -106,16 +40,6 @@ psutil_set_debug(PyObject *self, PyObject *args) { } -// Use it when invalid args are passed to a C function. -int -psutil_badargs(const char *funcname) { - PyErr_Format( - PyExc_RuntimeError, "%s() invalid args passed to function", funcname - ); - return -1; -} - - // Called on module import on all platforms. int psutil_setup(void) { diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index d8e20ad4d7..3288090ab0 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -50,30 +50,6 @@ extern int PSUTIL_CONN_NONE; #endif // clang-format on -// Print a debug message on stderr. -#define psutil_debug(...) \ - do { \ - if (!PSUTIL_DEBUG) \ - break; \ - fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ - } while (0) - - -// strncpy() variant which appends a null terminator. -#define PSUTIL_STRNCPY(dst, src, n) \ - strncpy(dst, src, n - 1); \ - dst[n - 1] = '\0' - -// ==================================================================== -// --- Custom exceptions -// ==================================================================== - -PyObject *AccessDenied(const char *msg); -PyObject *NoSuchProcess(const char *msg); -PyObject *psutil_oserror(void); -PyObject *psutil_oserror_wsyscall(const char *syscall); // ==================================================================== // --- Backward compatibility with missing Python.h APIs @@ -125,9 +101,41 @@ PyObject *psutil_oserror_wsyscall(const char *syscall); #endif // clang-format on +// ==================================================================== +// --- Internal utils +// ==================================================================== + +// Print a debug message to stderr, including where it originated from +// within the C code (file path + lineno). +#define psutil_debug(...) \ + do { \ + if (!PSUTIL_DEBUG) \ + break; \ + fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while (0) + + +// strncpy() variant which appends a null terminator. +#define PSUTIL_STRNCPY(dst, src, n) \ + strncpy(dst, src, n - 1); \ + dst[n - 1] = '\0' + + +PyObject *psutil_oserror(void); +PyObject *psutil_oserror_ad(const char *msg); +PyObject *psutil_oserror_nsp(const char *msg); +PyObject *psutil_oserror_wsyscall(const char *syscall); +PyObject *psutil_runtime_error(const char *msg, ...); + int psutil_badargs(const char *funcname); int psutil_setup(void); +// ==================================================================== +// --- Exposed to Python +// ==================================================================== + #if defined(PSUTIL_WINDOWS) || defined(PSUTIL_BSD) || defined(PSUTIL_OSX) PyObject *psutil_pids(PyObject *self, PyObject *args); #endif diff --git a/psutil/arch/all/pids.c b/psutil/arch/all/pids.c index 1b23ea34a9..74b1f309d0 100644 --- a/psutil/arch/all/pids.c +++ b/psutil/arch/all/pids.c @@ -50,7 +50,7 @@ psutil_pids(PyObject *self, PyObject *args) { goto error; if (pids_count == 0) { - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + psutil_runtime_error("no PIDs found"); goto error; } diff --git a/psutil/arch/bsd/init.c b/psutil/arch/bsd/init.c index 9a965b72fd..a4831b5614 100644 --- a/psutil/arch/bsd/init.c +++ b/psutil/arch/bsd/init.c @@ -17,9 +17,9 @@ convert_kvm_err(const char *syscall, char *errbuf) { sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(fullmsg); + psutil_oserror_ad(fullmsg); else if (strstr(errbuf, "Operation not permitted") != NULL) - AccessDenied(fullmsg); + psutil_oserror_ad(fullmsg); else - PyErr_Format(PyExc_RuntimeError, fullmsg); + psutil_runtime_error(fullmsg); } diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index de13245b98..4554de2bb7 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -316,11 +316,11 @@ psutil_proc_environ(PyObject *self, PyObject *args) { p = kvm_getproc2(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); #endif if (!p) { - NoSuchProcess("kvm_getprocs"); + psutil_oserror_nsp("kvm_getprocs"); goto error; } if (cnt <= 0) { - NoSuchProcess(cnt < 0 ? kvm_geterr(kd) : "kvm_getprocs: cnt==0"); + psutil_oserror_nsp(cnt < 0 ? kvm_geterr(kd) : "kvm_getprocs: cnt==0"); goto error; } @@ -356,10 +356,10 @@ psutil_proc_environ(PyObject *self, PyObject *args) { kvm_close(kd); return py_retdict; case EPERM: - AccessDenied("kvm_getenvv -> EPERM"); + psutil_oserror_ad("kvm_getenvv -> EPERM"); break; case ESRCH: - NoSuchProcess("kvm_getenvv -> ESRCH"); + psutil_oserror_nsp("kvm_getenvv -> ESRCH"); break; #if defined(PSUTIL_FREEBSD) case ENOMEM: @@ -373,7 +373,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { pid, p->ki_uid ); - AccessDenied(errbuf); + psutil_oserror_ad(errbuf); break; #endif default: diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index f5cf4bbe49..e7b9cdcc62 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -28,9 +28,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (py_retdict == NULL) return NULL; if (devstat_checkversion(NULL) < 0) { - PyErr_Format( - PyExc_RuntimeError, "devstat_checkversion() syscall failed" - ); + psutil_runtime_error("devstat_checkversion() syscall failed"); goto error; } @@ -42,7 +40,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { bzero(stats.dinfo, sizeof(struct devinfo)); if (devstat_getdevs(NULL, &stats) == -1) { - PyErr_Format(PyExc_RuntimeError, "devstat_getdevs() syscall failed"); + psutil_runtime_error("devstat_getdevs() syscall failed"); goto error; } diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 2ccb44650f..25a48425cd 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -94,15 +94,13 @@ psutil_swap_mem(PyObject *self, PyObject *args) { kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open failed"); if (kd == NULL) { - PyErr_SetString(PyExc_RuntimeError, "kvm_open() syscall failed"); + psutil_runtime_error("kvm_open() syscall failed"); return NULL; } if (kvm_getswapinfo(kd, kvmsw, 1, 0) < 0) { kvm_close(kd); - PyErr_SetString( - PyExc_RuntimeError, "kvm_getswapinfo() syscall failed" - ); + psutil_runtime_error("kvm_getswapinfo() syscall failed"); return NULL; } diff --git a/psutil/arch/freebsd/pids.c b/psutil/arch/freebsd/pids.c index 00ad666517..051eb66b87 100644 --- a/psutil/arch/freebsd/pids.c +++ b/psutil/arch/freebsd/pids.c @@ -28,7 +28,7 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { return -1; if (len == 0) { - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + psutil_runtime_error("no PIDs found"); goto error; } diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 262b4c1748..1536a62bde 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -53,7 +53,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { // sysctl stores 0 in the size if we can't find the process information. if (size == 0) { - NoSuchProcess("sysctl (size = 0)"); + psutil_oserror_nsp("sysctl (size = 0)"); return -1; } return 0; @@ -168,7 +168,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (ret == -1) return NULL; else if (ret == 0) - return NoSuchProcess("psutil_pid_exists -> 0"); + return psutil_oserror_nsp("psutil_pid_exists -> 0"); else strcpy(pathname, ""); } @@ -222,7 +222,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // subtle check: size == 0 means no such process if (size == 0) { - NoSuchProcess("sysctl (size = 0)"); + psutil_oserror_nsp("sysctl (size = 0)"); goto error; } diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 2c35deb6a2..e2249902c4 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -45,7 +45,7 @@ psutil_populate_xfiles(struct xfile **psutil_xfiles, int *psutil_nxfiles) { *psutil_xfiles = new_psutil_xfiles; } if (len > 0 && (*psutil_xfiles)->xf_size != sizeof(struct xfile)) { - PyErr_Format(PyExc_RuntimeError, "struct xfile size mismatch"); + psutil_runtime_error("struct xfile size mismatch"); return -1; } *psutil_nxfiles = len / sizeof(struct xfile); @@ -129,7 +129,7 @@ psutil_gather_inet( xig = (struct xinpgen *)buf; exig = (struct xinpgen *)(void *)((char *)buf + len - sizeof *exig); if (xig->xig_len != sizeof *xig || exig->xig_len != sizeof *exig) { - PyErr_Format(PyExc_RuntimeError, "struct xinpgen size mismatch"); + psutil_runtime_error("struct xinpgen size mismatch"); goto error; } } while (xig->xig_gen != exig->xig_gen && retry--); @@ -146,9 +146,7 @@ psutil_gather_inet( case IPPROTO_TCP: xtp = (struct xtcpcb *)xig; if (xtp->xt_len != sizeof *xtp) { - PyErr_Format( - PyExc_RuntimeError, "struct xtcpcb size mismatch" - ); + psutil_runtime_error("struct xtcpcb size mismatch"); goto error; } inp = &xtp->xt_inp; @@ -163,9 +161,7 @@ psutil_gather_inet( case IPPROTO_UDP: xip = (struct xinpcb *)xig; if (xip->xi_len != sizeof *xip) { - PyErr_Format( - PyExc_RuntimeError, "struct xinpcb size mismatch" - ); + psutil_runtime_error("struct xinpcb size mismatch"); goto error; } #if __FreeBSD_version >= 1200026 @@ -177,7 +173,7 @@ psutil_gather_inet( status = PSUTIL_CONN_NONE; break; default: - PyErr_Format(PyExc_RuntimeError, "invalid proto"); + psutil_runtime_error("invalid proto"); goto error; } @@ -302,7 +298,7 @@ psutil_gather_unix( xug = (struct xunpgen *)buf; exug = (struct xunpgen *)(void *)((char *)buf + len - sizeof *exug); if (xug->xug_len != sizeof *xug || exug->xug_len != sizeof *exug) { - PyErr_Format(PyExc_RuntimeError, "struct xinpgen size mismatch"); + psutil_runtime_error("struct xinpgen size mismatch"); goto error; } } while (xug->xug_gen != exug->xug_gen && retry--); diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c index d55da11b33..81a608cd10 100644 --- a/psutil/arch/linux/disk.c +++ b/psutil/arch/linux/disk.c @@ -39,7 +39,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { while ((entry = getmntent(file))) { if (entry == NULL) { - PyErr_Format(PyExc_RuntimeError, "getmntent() syscall failed"); + psutil_runtime_error("getmntent() syscall failed"); goto error; } py_dev = PyUnicode_DecodeFSDefault(entry->mnt_fsname); diff --git a/psutil/arch/netbsd/pids.c b/psutil/arch/netbsd/pids.c index a5034b9c37..ebf5127f06 100644 --- a/psutil/arch/netbsd/pids.c +++ b/psutil/arch/netbsd/pids.c @@ -27,7 +27,7 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); if (kd == NULL) { - PyErr_Format(PyExc_RuntimeError, "kvm_openfiles() failed: %s", errbuf); + psutil_runtime_error("kvm_openfiles() failed: %s", errbuf); return -1; } @@ -35,13 +35,13 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt ); if (result == NULL) { - PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() failed"); + psutil_runtime_error("kvm_getproc2() failed"); kvm_close(kd); return -1; } if (cnt == 0) { - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + psutil_runtime_error("no PIDs found"); kvm_close(kd); return -1; } diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index bae5440d1f..ebdf98fa08 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -44,7 +44,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc) { } // sysctl stores 0 in the size if we can't find the process information. if (size == 0) { - NoSuchProcess("sysctl (size = 0)"); + psutil_oserror_nsp("sysctl (size = 0)"); return -1; } return 0; @@ -65,7 +65,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { int name[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) - NoSuchProcess("sysctl -> ENOENT"); + psutil_oserror_nsp("sysctl -> ENOENT"); else psutil_oserror(); return NULL; @@ -81,7 +81,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { free(buf); if (len == -1) { if (errno == ENOENT) - NoSuchProcess("readlink -> ENOENT"); + psutil_oserror_nsp("readlink -> ENOENT"); else psutil_oserror(); return NULL; @@ -137,7 +137,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (ret == -1) return NULL; else if (ret == 0) - return NoSuchProcess("psutil_pid_exists -> 0"); + return psutil_oserror_nsp("psutil_pid_exists -> 0"); else strcpy(pathname, ""); } @@ -192,7 +192,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } if (size == 0) { - NoSuchProcess("sysctl (size = 0)"); + psutil_oserror_nsp("sysctl (size = 0)"); goto error; } @@ -204,7 +204,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { goto error; } if (size == 0) { - NoSuchProcess("sysctl (size = 0)"); + psutil_oserror_nsp("sysctl (size = 0)"); goto error; } diff --git a/psutil/arch/openbsd/pids.c b/psutil/arch/openbsd/pids.c index 48554041a5..be7c493fb8 100644 --- a/psutil/arch/openbsd/pids.c +++ b/psutil/arch/openbsd/pids.c @@ -27,7 +27,7 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); if (kd == NULL) { - PyErr_Format(PyExc_RuntimeError, "kvm_openfiles() failed: %s", errbuf); + psutil_runtime_error("kvm_openfiles() failed: %s", errbuf); return -1; } @@ -35,13 +35,13 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt ); if (result == NULL) { - PyErr_Format(PyExc_RuntimeError, "kvm_getproc2() failed"); + psutil_runtime_error("kvm_getproc2() failed"); kvm_close(kd); return -1; } if (cnt == 0) { - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + psutil_runtime_error("no PIDs found"); kvm_close(kd); return -1; } diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 268cdd2b7e..4c4ade7552 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -45,7 +45,7 @@ psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { } // sysctl stores 0 in the size if we can't find the process information. if (size == 0) { - NoSuchProcess("sysctl (size = 0)"); + psutil_oserror_nsp("sysctl (size = 0)"); return -1; } return 0; @@ -145,9 +145,9 @@ psutil_proc_threads(PyObject *self, PyObject *args) { ); if (!kp) { if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied("kvm_getprocs"); + psutil_oserror_ad("kvm_getprocs"); else - PyErr_Format(PyExc_RuntimeError, "kvm_getprocs() syscall failed"); + psutil_runtime_error("kvm_getprocs() syscall failed"); goto error; } diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index e81033e05a..56accf167c 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -66,9 +66,7 @@ psutil_cpu_times(PyObject *self, PyObject *args) { mach_port_t mport = mach_host_self(); if (mport == MACH_PORT_NULL) { - PyErr_SetString( - PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL" - ); + psutil_runtime_error("mach_host_self() returned MACH_PORT_NULL"); return NULL; } @@ -78,8 +76,7 @@ psutil_cpu_times(PyObject *self, PyObject *args) { mach_port_deallocate(mach_task_self(), mport); if (error != KERN_SUCCESS) { - return PyErr_Format( - PyExc_RuntimeError, + return psutil_runtime_error( "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", mach_error_string(error) ); @@ -103,9 +100,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { struct vmmeter vmstat; if (mport == MACH_PORT_NULL) { - PyErr_SetString( - PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL" - ); + psutil_runtime_error("mach_host_self() returned MACH_PORT_NULL"); return NULL; } @@ -113,10 +108,8 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { mach_port_deallocate(mach_task_self(), mport); if (ret != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, - "host_statistics(HOST_VM_INFO) failed: %s", - mach_error_string(ret) + psutil_runtime_error( + "host_statistics(HOST_VM_INFO) failed: %s", mach_error_string(ret) ); return NULL; } @@ -201,9 +194,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { uint32_t pMin = 0, eMin = 0, min = 0, max = 0, curr = 0; if (!psutil_find_pmgr_entry(&entry)) { - PyErr_SetString( - PyExc_RuntimeError, "'pmgr' entry not found in AppleARMIODevice" - ); + psutil_runtime_error("'pmgr' entry not found in AppleARMIODevice"); return NULL; } @@ -218,7 +209,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { || CFGetTypeID(eCoreRef) != CFDataGetTypeID() || CFDataGetLength(pCoreRef) < 8 || CFDataGetLength(eCoreRef) < 4) { - PyErr_SetString(PyExc_RuntimeError, "invalid CPU frequency data"); + psutil_runtime_error("invalid CPU frequency data"); goto cleanup; } @@ -296,7 +287,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { return NULL; if (mport == MACH_PORT_NULL) { - PyErr_SetString(PyExc_RuntimeError, "mach_host_self() returned NULL"); + psutil_runtime_error("mach_host_self() returned NULL"); goto error; } @@ -306,10 +297,8 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { mach_port_deallocate(mach_task_self(), mport); if (error != KERN_SUCCESS || !info_array) { - PyErr_Format( - PyExc_RuntimeError, - "host_processor_info failed: %s", - mach_error_string(error) + psutil_runtime_error( + "host_processor_info failed: %s", mach_error_string(error) ); goto error; } diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index 9341ee5f70..ae3283a212 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -237,7 +237,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { ) != kIOReturnSuccess) { - PyErr_SetString(PyExc_RuntimeError, "unable to get the list of disks"); + psutil_runtime_error("unable to get the list of disks"); goto error; } @@ -251,9 +251,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) != kIOReturnSuccess) { - PyErr_SetString( - PyExc_RuntimeError, "unable to get the disk's parent" - ); + psutil_runtime_error("unable to get the disk's parent"); goto error; } @@ -271,9 +269,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { ) != kIOReturnSuccess) { - PyErr_SetString( - PyExc_RuntimeError, "unable to get the parent's properties" - ); + psutil_runtime_error("unable to get the parent's properties"); goto error; } @@ -285,16 +281,14 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { ) != kIOReturnSuccess) { - PyErr_SetString( - PyExc_RuntimeError, "unable to get the disk properties" - ); + psutil_runtime_error("unable to get the disk properties"); goto error; } CFStringRef disk_name_ref = (CFStringRef )CFDictionaryGetValue(parent_dict, CFSTR(kIOBSDNameKey)); if (disk_name_ref == NULL) { - PyErr_SetString(PyExc_RuntimeError, "unable to get disk name"); + psutil_runtime_error("unable to get disk name"); goto error; } @@ -307,9 +301,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { CFStringGetSystemEncoding() )) { - PyErr_SetString( - PyExc_RuntimeError, "unable to convert disk name to C string" - ); + psutil_runtime_error("unable to convert disk name to C string"); goto error; } @@ -317,7 +309,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { props_dict, CFSTR(kIOBlockStorageDriverStatisticsKey) ); if (stats_dict == NULL) { - PyErr_SetString(PyExc_RuntimeError, "unable to get disk stats"); + psutil_runtime_error("unable to get disk stats"); goto error; } diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 7f1741a222..b222b566a6 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -27,9 +27,7 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { mport = mach_host_self(); if (mport == MACH_PORT_NULL) { - PyErr_SetString( - PyExc_RuntimeError, "mach_host_self() returned MACH_PORT_NULL" - ); + psutil_runtime_error("mach_host_self() returned MACH_PORT_NULL"); return -1; } @@ -38,8 +36,7 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { ); mach_port_deallocate(mach_task_self(), mport); if (ret != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, + psutil_runtime_error( "host_statistics64(HOST_VM_INFO64) syscall failed: %s", mach_error_string(ret) ); diff --git a/psutil/arch/osx/pids.c b/psutil/arch/osx/pids.c index 2c3ddaa82b..debd8af4d6 100644 --- a/psutil/arch/osx/pids.c +++ b/psutil/arch/osx/pids.c @@ -28,7 +28,7 @@ _psutil_pids(pid_t **pids_array, int *pids_count) { return -1; if (len == 0) { - PyErr_Format(PyExc_RuntimeError, "no PIDs found"); + psutil_runtime_error("no PIDs found"); goto error; } diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 83418b2f07..a8ebd7aadd 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -62,7 +62,7 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { // sysctl succeeds but len is zero, happens when process has gone away if (len == 0) { - NoSuchProcess("sysctl(kinfo_proc), len == 0"); + psutil_oserror_nsp("sysctl(kinfo_proc), len == 0"); return -1; } return 0; @@ -97,7 +97,7 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { if (psutil_pid_exists(pid) == 0) { - NoSuchProcess("psutil_pid_exists -> 0"); + psutil_oserror_nsp("psutil_pid_exists -> 0"); return -1; } @@ -108,13 +108,13 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { if (errno == EINVAL) { psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to AD"); - AccessDenied("sysctl(KERN_PROCARGS2) -> EINVAL"); + psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> EINVAL"); return -1; } if (errno == EIO) { psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); - AccessDenied("sysctl(KERN_PROCARGS2) -> EIO"); + psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> EIO"); return -1; } psutil_oserror_wsyscall("sysctl(KERN_PROCARGS2)"); @@ -179,7 +179,7 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) { err = task_for_pid(mach_task_self(), pid, task); if (err != KERN_SUCCESS) { if (psutil_pid_exists(pid) == 0) { - NoSuchProcess("task_for_pid"); + psutil_oserror_nsp("task_for_pid"); } else if (is_zombie(pid) == 1) { PyErr_SetString( @@ -189,13 +189,13 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) { else { psutil_debug( "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " - "setting AccessDenied()", + "setting EACCES", (long)pid, err, errno, mach_error_string(err) ); - AccessDenied("task_for_pid"); + psutil_oserror_ad("task_for_pid"); } return -1; } @@ -232,10 +232,7 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { while (ret > fds_size) { fds_size += PROC_PIDLISTFD_SIZE * 32; if (fds_size > max_size) { - PyErr_Format( - PyExc_RuntimeError, - "prevent malloc() to allocate > 24M" - ); + psutil_runtime_error("prevent malloc() to allocate > 24M"); goto error; } } @@ -447,7 +444,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { ret = proc_pidpath(pid, &buf, sizeof(buf)); if (ret == 0) { if (pid == 0) { - AccessDenied("automatically set for PID 0"); + psutil_oserror_ad("automatically set for PID 0"); return NULL; } else if (errno == ENOENT) { @@ -548,8 +545,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { break; } else if (kr != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, + psutil_runtime_error( "mach_vm_region(VM_REGION_TOP_INFO) syscall failed" ); mach_port_deallocate(mach_task_self(), task); @@ -628,20 +624,18 @@ psutil_proc_threads(PyObject *self, PyObject *args) { ); if (kr != KERN_SUCCESS) { if (kr == KERN_INVALID_ARGUMENT) { - AccessDenied("task_info(TASK_BASIC_INFO)"); + psutil_oserror_ad("task_info(TASK_BASIC_INFO)"); } else { // otherwise throw a runtime error with appropriate error code - PyErr_Format( - PyExc_RuntimeError, "task_info(TASK_BASIC_INFO) syscall failed" - ); + psutil_runtime_error("task_info(TASK_BASIC_INFO) syscall failed"); } goto error; } kr = task_threads(task, &thread_list, &thread_count); if (kr != KERN_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, "task_threads() syscall failed"); + psutil_runtime_error("task_threads() syscall failed"); goto error; } @@ -654,8 +648,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { &thread_info_count ); if (kr != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, + psutil_runtime_error( "thread_info(THREAD_BASIC_INFO) syscall failed" ); goto error; diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index a4f9abea80..c4b2afeed9 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -34,17 +34,13 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { power_info = IOPSCopyPowerSourcesInfo(); if (!power_info) { - PyErr_SetString( - PyExc_RuntimeError, "IOPSCopyPowerSourcesInfo() syscall failed" - ); + psutil_runtime_error("IOPSCopyPowerSourcesInfo() syscall failed"); goto error; } power_sources_list = IOPSCopyPowerSourcesList(power_info); if (!power_sources_list) { - PyErr_SetString( - PyExc_RuntimeError, "IOPSCopyPowerSourcesList() syscall failed" - ); + psutil_runtime_error("IOPSCopyPowerSourcesList() syscall failed"); goto error; } @@ -57,9 +53,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { power_info, CFArrayGetValueAtIndex(power_sources_list, 0) ); if (!power_sources_information) { - PyErr_SetString( - PyExc_RuntimeError, "Failed to get power source description" - ); + psutil_runtime_error("Failed to get power source description"); goto error; } @@ -69,8 +63,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { if (!capacity_ref || !CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) { - PyErr_SetString( - PyExc_RuntimeError, + psutil_runtime_error( "No battery capacity information in power sources info" ); goto error; @@ -80,7 +73,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { power_sources_information, CFSTR(kIOPSPowerSourceStateKey) ); if (!ps_state_ref) { - PyErr_SetString(PyExc_RuntimeError, "power source state info missing"); + psutil_runtime_error("power source state info missing"); goto error; } is_power_plugged = CFStringCompare( diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index 4ce1755bb3..1e05eb8d9f 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -76,7 +76,7 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { // XXX we get here on FreeBSD when processing 'lo' / AF_INET6 // broadcast. Not sure what to do other than returning None. // ifconfig does not show anything BTW. - // PyErr_Format(PyExc_RuntimeError, gai_strerror(err)); + // psutil_runtime_error(gai_strerror(err)); // return NULL; Py_INCREF(Py_None); return Py_None; diff --git a/psutil/arch/posix/proc.c b/psutil/arch/posix/proc.c index 46b01c1d3d..5671077cb0 100644 --- a/psutil/arch/posix/proc.c +++ b/psutil/arch/posix/proc.c @@ -72,9 +72,9 @@ psutil_raise_for_pid(pid_t pid, char *syscall) { if (errno != 0) psutil_oserror_wsyscall(syscall); else if (psutil_pid_exists(pid) == 0) - NoSuchProcess(syscall); + psutil_oserror_nsp(syscall); else - PyErr_Format(PyExc_RuntimeError, "%s syscall failed", syscall); + psutil_runtime_error("%s syscall failed", syscall); } diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index 67e0df7d9d..d1ff1a58d9 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -29,7 +29,7 @@ psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen) { } if (len != buflen) { - PyErr_SetString(PyExc_RuntimeError, "sysctl() size mismatch"); + psutil_runtime_error("sysctl() size mismatch"); return -1; } @@ -99,9 +99,7 @@ psutil_sysctl_malloc(int *mib, u_int miblen, char **buf, size_t *buflen) { return -1; } - PyErr_SetString( - PyExc_RuntimeError, "sysctl() buffer allocation retry limit exceeded" - ); + psutil_runtime_error("sysctl() buffer allocation retry limit exceeded"); return -1; } @@ -117,7 +115,7 @@ psutil_sysctl_argmax() { } if (argmax <= 0) { - PyErr_SetString(PyExc_RuntimeError, "sysctl(KERN_ARGMAX) return <= 0"); + psutil_runtime_error("sysctl(KERN_ARGMAX) return <= 0"); return 0; } @@ -150,7 +148,7 @@ psutil_sysctlbyname(const char *name, void *buf, size_t buflen) { len, buflen ); - PyErr_SetString(PyExc_RuntimeError, errbuf); + psutil_runtime_error(errbuf); return -1; } @@ -238,7 +236,7 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { "sysctlbyname('%s') buffer allocation retry limit exceeded", name ); - PyErr_SetString(PyExc_RuntimeError, errbuf); + psutil_runtime_error(errbuf); return -1; } #endif // PSUTIL_HAS_SYSCTLBYNAME diff --git a/psutil/arch/sunos/cpu.c b/psutil/arch/sunos/cpu.c index a1780dd766..e0ca022ebc 100644 --- a/psutil/arch/sunos/cpu.c +++ b/psutil/arch/sunos/cpu.c @@ -9,6 +9,8 @@ #include #include +#include "../../arch/all/init.h" + // System-wide CPU times. PyObject * diff --git a/psutil/arch/sunos/disk.c b/psutil/arch/sunos/disk.c index 7b08c6df3a..d5ced15a30 100644 --- a/psutil/arch/sunos/disk.c +++ b/psutil/arch/sunos/disk.c @@ -10,6 +10,8 @@ #include #include +#include "../../arch/all/init.h" + PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { diff --git a/psutil/arch/sunos/environ.c b/psutil/arch/sunos/environ.c index d6c533f4c7..5da8ab225d 100644 --- a/psutil/arch/sunos/environ.c +++ b/psutil/arch/sunos/environ.c @@ -262,7 +262,7 @@ search_pointers_vector_size_offt(int fd, off_t offt, size_t ptr_size) { break; if (r != ptr_size) { - PyErr_SetString(PyExc_RuntimeError, "pointer block is truncated"); + psutil_runtime_error("pointer block is truncated"); return -1; } @@ -303,9 +303,7 @@ psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count) { } if (!(info.pr_argv && info.pr_argc)) { - PyErr_SetString( - PyExc_RuntimeError, "process doesn't have arguments block" - ); + psutil_runtime_error("process doesn't have arguments block"); return NULL; } diff --git a/psutil/arch/sunos/mem.c b/psutil/arch/sunos/mem.c index eedf6f68c7..089b703e80 100644 --- a/psutil/arch/sunos/mem.c +++ b/psutil/arch/sunos/mem.c @@ -10,6 +10,8 @@ #include #include +#include "../../arch/all/init.h" + PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { @@ -35,15 +37,15 @@ psutil_swap_mem(PyObject *self, PyObject *args) { return NULL; } if (num == 0) { - PyErr_SetString(PyExc_RuntimeError, "no swap devices configured"); + psutil_runtime_error("no swap devices configured"); return NULL; } if ((st = malloc(num * sizeof(swapent_t) + sizeof (int))) == NULL) { - PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + psutil_runtime_error("malloc failed"); return NULL; } if ((path = malloc(num * MAXPATHLEN)) == NULL) { - PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + psutil_runtime_error("malloc failed"); return NULL; } swapent = st->swt_ent; @@ -97,7 +99,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } kstat_close(kc); if (!flag) { - PyErr_SetString(PyExc_RuntimeError, "no swap device was found"); + psutil_runtime_error("no swap device was found"); return NULL; } return Py_BuildValue("(II)", sin, sout); diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c index e38c86b614..052ac0e0c2 100644 --- a/psutil/arch/sunos/net.c +++ b/psutil/arch/sunos/net.c @@ -81,7 +81,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { if ((rbytes == NULL) || (wbytes == NULL) || (rpkts == NULL) || (wpkts == NULL) || (ierrs == NULL) || (oerrs == NULL)) { - PyErr_SetString(PyExc_RuntimeError, "kstat_data_lookup() failed"); + psutil_runtime_error("kstat_data_lookup() failed"); goto error; } @@ -347,13 +347,13 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (ctlbuf.len >= (int)sizeof(struct T_error_ack) && tea.PRIM_type == T_ERROR_ACK) { - PyErr_SetString(PyExc_RuntimeError, "ERROR_ACK"); + psutil_runtime_error("ERROR_ACK"); goto error; } if (getcode == 0 && ctlbuf.len >= (int)sizeof(struct T_optmgmt_ack) && toa.PRIM_type == T_OPTMGMT_ACK && toa.MGMT_flags == T_SUCCESS) { - PyErr_SetString(PyExc_RuntimeError, "ERROR_T_OPTMGMT_ACK"); + psutil_runtime_error("ERROR_T_OPTMGMT_ACK"); goto error; } diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index 3f04004e34..9a000e4fa1 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -32,9 +32,7 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { } if (nbytes != (ssize_t)size) { close(fd); - PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch" - ); + psutil_runtime_error("read() file structure size mismatch"); return 0; } close(fd); @@ -202,7 +200,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { goto error; if (!info.pr_envp) { - AccessDenied("/proc/pid/psinfo struct not set"); + psutil_oserror_ad("/proc/pid/psinfo struct not set"); goto error; } @@ -308,9 +306,7 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { goto error; } if (nbytes != sizeof(header)) { - PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch" - ); + psutil_runtime_error("read() file structure size mismatch"); goto error; } @@ -330,9 +326,7 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { goto error; } if (nbytes != size) { - PyErr_SetString( - PyExc_RuntimeError, "read() file structure size mismatch" - ); + psutil_runtime_error("read() file structure size mismatch"); goto error; } diff --git a/psutil/arch/sunos/sys.c b/psutil/arch/sunos/sys.c index d9f3ae4bfb..e0d147debd 100644 --- a/psutil/arch/sunos/sys.c +++ b/psutil/arch/sunos/sys.c @@ -28,7 +28,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { UTXENT_MUTEX_UNLOCK(); if (fabs(boot_time) < 0.000001) { /* could not find BOOT_TIME in getutxent loop */ - PyErr_SetString(PyExc_RuntimeError, "can't determine boot time"); + psutil_runtime_error("can't determine boot time"); return NULL; } return Py_BuildValue("f", boot_time); diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index 455bd272a8..c4a5f7f531 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -33,10 +33,7 @@ psutil_get_num_cpus(int fail_on_err) { ); ncpus = (unsigned int)PSUTIL_SYSTEM_INFO.dwNumberOfProcessors; if ((ncpus <= 0) && (fail_on_err == 1)) { - PyErr_SetString( - PyExc_RuntimeError, - "GetSystemInfo() failed to retrieve CPU count" - ); + psutil_runtime_error("GetSystemInfo failed to retrieve CPU count"); } } return ncpus; @@ -403,9 +400,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { // Syscall. ret = CallNtPowerInformation(ProcessorInformation, NULL, 0, pBuffer, size); if (ret != 0) { - PyErr_SetString( - PyExc_RuntimeError, "CallNtPowerInformation syscall failed" - ); + psutil_runtime_error("CallNtPowerInformation syscall failed"); goto error; } diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c index e8889f7dfb..058838de96 100644 --- a/psutil/arch/windows/mem.c +++ b/psutil/arch/windows/mem.c @@ -51,15 +51,14 @@ psutil_swap_percent(PyObject *self, PyObject *args) { double percentUsage; if ((PdhOpenQueryW(NULL, 0, &hQuery)) != ERROR_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, "PdhOpenQueryW failed"); + psutil_runtime_error("PdhOpenQueryW failed"); return NULL; } s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); if (s != ERROR_SUCCESS) { PdhCloseQuery(hQuery); - PyErr_Format( - PyExc_RuntimeError, + psutil_runtime_error( "PdhAddEnglishCounterW failed. Performance counters may be " "disabled." ); @@ -78,9 +77,7 @@ psutil_swap_percent(PyObject *self, PyObject *args) { ); if (s != ERROR_SUCCESS) { PdhCloseQuery(hQuery); - PyErr_Format( - PyExc_RuntimeError, "PdhGetFormattedCounterValue failed" - ); + psutil_runtime_error("PdhGetFormattedCounterValue failed"); return NULL; } percentUsage = counterValue.doubleValue; diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 1b71a42192..5a68fc7e10 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -23,9 +23,7 @@ psutil_get_nic_addresses(void) { if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, NULL, &bufferLength) != ERROR_BUFFER_OVERFLOW) { - PyErr_SetString( - PyExc_RuntimeError, "GetAdaptersAddresses() syscall failed." - ); + psutil_runtime_error("GetAdaptersAddresses() syscall failed."); return NULL; } @@ -40,9 +38,7 @@ psutil_get_nic_addresses(void) { != ERROR_SUCCESS) { free(buffer); - PyErr_SetString( - PyExc_RuntimeError, "GetAdaptersAddresses() syscall failed." - ); + psutil_runtime_error("GetAdaptersAddresses() syscall failed."); return NULL; } @@ -84,8 +80,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { pIfRow->InterfaceIndex = pCurrAddresses->IfIndex; dwRetVal = GetIfEntry2(pIfRow); if (dwRetVal != NO_ERROR) { - PyErr_SetString( - PyExc_RuntimeError, + psutil_runtime_error( "GetIfEntry() or GetIfEntry2() syscalls failed." ); goto error; @@ -380,7 +375,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { // Make a second call to GetIfTable to get the actual // data we want. if ((dwRetVal = GetIfTable(pIfTable, &dwSize, FALSE)) != NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "GetIfTable() syscall failed"); + psutil_runtime_error("GetIfTable() syscall failed"); goto error; } diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 687daee81a..ff22340fbf 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -61,7 +61,7 @@ psutil_proc_kill(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (pid == 0) - return AccessDenied("automatically set for PID 0"); + return psutil_oserror_ad("automatically set for PID 0"); hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) { @@ -97,7 +97,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, _Py_PARSE_PID "l", &pid, &timeout)) return NULL; if (pid == 0) - return AccessDenied("automatically set for PID 0"); + return psutil_oserror_ad("automatically set for PID 0"); hProcess = OpenProcess( SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, pid @@ -176,7 +176,7 @@ psutil_proc_times(PyObject *self, PyObject *args) { if (GetLastError() == ERROR_ACCESS_DENIED) { // usually means the process has died so we throw a NoSuchProcess // here - NoSuchProcess("GetProcessTimes -> ERROR_ACCESS_DENIED"); + psutil_oserror_nsp("GetProcessTimes -> ERROR_ACCESS_DENIED"); } else { psutil_oserror(); @@ -225,12 +225,12 @@ psutil_proc_exe(PyObject *self, PyObject *args) { return NULL; if (pid == 0) - return AccessDenied("automatically set for PID 0"); + return psutil_oserror_ad("automatically set for PID 0"); // ...because NtQuerySystemInformation can succeed for terminated // processes. if (psutil_pid_is_running(pid) == 0) - return NoSuchProcess("psutil_pid_is_running -> 0"); + return psutil_oserror_nsp("psutil_pid_is_running -> 0"); buffer = MALLOC_ZERO(bufferSize); if (!buffer) { @@ -299,7 +299,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (!NT_SUCCESS(status)) { FREE(buffer); if (psutil_pid_is_running(pid) == 0) - NoSuchProcess("psutil_pid_is_running -> 0"); + psutil_oserror_nsp("psutil_pid_is_running -> 0"); else psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); return NULL; @@ -410,9 +410,7 @@ psutil_GetProcWsetInformation( bufferSize *= 2; // Fail if we're resizing the buffer to something very large. if (bufferSize > 256 * 1024 * 1024) { - PyErr_SetString( - PyExc_RuntimeError, "NtQueryVirtualMemory bufsize is too large" - ); + psutil_runtime_error("NtQueryVirtualMemory bufsize is too large"); return -1; } buffer = MALLOC_ZERO(bufferSize); @@ -424,10 +422,10 @@ psutil_GetProcWsetInformation( if (!NT_SUCCESS(status)) { if (status == STATUS_ACCESS_DENIED) { - AccessDenied("NtQueryVirtualMemory -> STATUS_ACCESS_DENIED"); + psutil_oserror_ad("NtQueryVirtualMemory -> STATUS_ACCESS_DENIED"); } else if (psutil_pid_is_running(pid) == 0) { - NoSuchProcess("psutil_pid_is_running -> 0"); + psutil_oserror_nsp("psutil_pid_is_running -> 0"); } else { PyErr_Clear(); @@ -550,13 +548,13 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (pid == 0) { // raise AD instead of returning 0 as procexp is able to // retrieve useful information somehow - AccessDenied("forced for PID 0"); + psutil_oserror_ad("forced for PID 0"); goto error; } pid_return = psutil_pid_is_running(pid); if (pid_return == 0) { - NoSuchProcess("psutil_pid_is_running -> 0"); + psutil_oserror_nsp("psutil_pid_is_running -> 0"); goto error; } if (pid_return == -1) @@ -770,7 +768,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { // name. It also occurs for SIDs that have no corresponding // account name, such as a logon SID that identifies a logon // session. - AccessDenied("LookupAccountSidW -> ERROR_NONE_MAPPED"); + psutil_oserror_ad("LookupAccountSidW -> ERROR_NONE_MAPPED"); goto error; } else { diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index 8ca6967abf..ab996938ef 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -54,8 +54,7 @@ psutil_enum_handles(PSYSTEM_HANDLE_INFORMATION_EX *handles) { // Fail if we're resizing the buffer to something very large. if (bufferSize > 256 * 1024 * 1024) { - PyErr_SetString( - PyExc_RuntimeError, + psutil_runtime_error( "SystemExtendedHandleInformation buffer too big" ); return -1; diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index a7d91e578e..ba22a011b0 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -67,7 +67,7 @@ psutil_convert_winerr(ULONG err, char *syscall) { if (err == ERROR_NOACCESS) { sprintf(fullmsg, "%s -> ERROR_NOACCESS", syscall); psutil_debug(fullmsg); - AccessDenied(fullmsg); + psutil_oserror_ad(fullmsg); } else { psutil_oserror_wsyscall(syscall); @@ -98,7 +98,7 @@ psutil_giveup_with_ad(NTSTATUS status, char *syscall) { err = RtlNtStatusToDosErrorNoTeb(status); sprintf(fullmsg, "%s -> %lu (%s)", syscall, err, strerror(err)); psutil_debug(fullmsg); - AccessDenied(fullmsg); + psutil_oserror_ad(fullmsg); } @@ -247,7 +247,9 @@ psutil_get_process_data( ); if (NtWow64QueryInformationProcess64 == NULL) { PyErr_Clear(); - AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); + psutil_oserror_ad( + "can't query 64-bit process in 32-bit-WoW mode" + ); goto error; } } @@ -257,7 +259,9 @@ psutil_get_process_data( ); if (NtWow64ReadVirtualMemory64 == NULL) { PyErr_Clear(); - AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); + psutil_oserror_ad( + "can't query 64-bit process in 32-bit-WoW mode" + ); goto error; } } @@ -390,7 +394,7 @@ psutil_get_process_data( if (kind == KIND_ENVIRON) { #ifndef _WIN64 if (weAreWow64 && !theyAreWow64) { - AccessDenied("can't query 64-bit process in 32-bit-WoW mode"); + psutil_oserror_ad("can't query 64-bit process in 32-bit-WoW mode"); goto error; } else @@ -460,7 +464,7 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { int ProcessCommandLineInformation = 60; if (PSUTIL_WINVER < PSUTIL_WINDOWS_8_1) { - PyErr_SetString(PyExc_RuntimeError, "requires Windows 8.1+"); + psutil_runtime_error("requires Windows 8.1+"); goto error; } @@ -475,7 +479,7 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { // https://github.com/giampaolo/psutil/issues/1501 if (status == STATUS_NOT_FOUND) { - AccessDenied( + psutil_oserror_ad( "NtQueryInformationProcess(ProcessBasicInformation) -> " "STATUS_NOT_FOUND" ); @@ -566,7 +570,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running -> 0"); + return psutil_oserror_nsp("psutil_pid_is_running -> 0"); if (pid_return == -1) return NULL; @@ -638,7 +642,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running -> 0"); + return psutil_oserror_nsp("psutil_pid_is_running -> 0"); if (pid_return == -1) return NULL; @@ -675,7 +679,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) - return NoSuchProcess("psutil_pid_is_running -> 0"); + return psutil_oserror_nsp("psutil_pid_is_running -> 0"); if (pid_return == -1) return NULL; @@ -756,7 +760,7 @@ psutil_get_proc_info( } } while ((process = PSUTIL_NEXT_PROCESS(process))); - NoSuchProcess("NtQuerySystemInformation (no PID found)"); + psutil_oserror_nsp("NtQuerySystemInformation (no PID found)"); goto error; error: diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index 6067c6fad1..43ad7b6531 100644 --- a/psutil/arch/windows/proc_utils.c +++ b/psutil/arch/windows/proc_utils.c @@ -46,7 +46,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { if (GetLastError() == ERROR_INVALID_PARAMETER) { // Yeah, this is the actual error code in case of // "no such process". - NoSuchProcess("OpenProcess -> ERROR_INVALID_PARAMETER"); + psutil_oserror_nsp("OpenProcess -> ERROR_INVALID_PARAMETER"); return NULL; } if (GetLastError() == ERROR_SUCCESS) { @@ -54,11 +54,11 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { // https://github.com/giampaolo/psutil/issues/1877 if (psutil_pid_in_pids(pid) == 1) { psutil_debug("OpenProcess -> ERROR_SUCCESS turned into AD"); - AccessDenied("OpenProcess -> ERROR_SUCCESS"); + psutil_oserror_ad("OpenProcess -> ERROR_SUCCESS"); } else { psutil_debug("OpenProcess -> ERROR_SUCCESS turned into NSP"); - NoSuchProcess("OpenProcess -> ERROR_SUCCESS"); + psutil_oserror_nsp("OpenProcess -> ERROR_SUCCESS"); } return NULL; } @@ -79,7 +79,7 @@ psutil_check_phandle(HANDLE hProcess, DWORD pid, int check_exit_code) { return hProcess; } CloseHandle(hProcess); - NoSuchProcess("GetExitCodeProcess != STILL_ACTIVE"); + psutil_oserror_nsp("GetExitCodeProcess != STILL_ACTIVE"); return NULL; } @@ -104,7 +104,7 @@ psutil_handle_from_pid(DWORD pid, DWORD access) { if (pid == 0) { // otherwise we'd get NoSuchProcess - return AccessDenied("automatically set for PID 0"); + return psutil_oserror_ad("automatically set for PID 0"); } hProcess = OpenProcess(access, FALSE, pid); diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 2b434034cd..ae9007f4aa 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -52,7 +52,7 @@ __GetExtendedTcpTable(ULONG family) { return __GetExtendedTcpTable(family); } - PyErr_SetString(PyExc_RuntimeError, "GetExtendedTcpTable failed"); + psutil_runtime_error("GetExtendedTcpTable failed"); return NULL; } @@ -84,7 +84,7 @@ __GetExtendedUdpTable(ULONG family) { return __GetExtendedUdpTable(family); } - PyErr_SetString(PyExc_RuntimeError, "GetExtendedUdpTable failed"); + psutil_runtime_error("GetExtendedUdpTable failed"); return NULL; } @@ -141,7 +141,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { pid_return = psutil_pid_is_running(pid); if (pid_return == 0) { psutil_conn_decref_objs(); - return NoSuchProcess("psutil_pid_is_running"); + return psutil_oserror_nsp("psutil_pid_is_running"); } else if (pid_return == -1) { psutil_conn_decref_objs(); diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index 8a234abb10..c98db34130 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -80,14 +80,13 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { HANDLE waitHandle; if ((PdhOpenQueryW(NULL, 0, &hQuery)) != ERROR_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, "PdhOpenQueryW failed"); + psutil_runtime_error("PdhOpenQueryW failed"); return NULL; } s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); if (s != ERROR_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, + psutil_runtime_error( "PdhAddEnglishCounterW failed. Performance counters may be " "disabled." ); @@ -102,7 +101,7 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { s = PdhCollectQueryDataEx(hQuery, SAMPLING_INTERVAL, event); if (s != ERROR_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, "PdhCollectQueryDataEx failed"); + psutil_runtime_error("PdhCollectQueryDataEx failed"); return NULL; } From bacb51ba191fec55bcd9abb0d0be2812469be775 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 18:11:37 +0100 Subject: [PATCH 1407/1714] Full rewrite of winmake.py - no longer use argparse; use a hand-made simple parser instead (@clicmd) - allow to call multple Makefile targets serially, as a one-liner, as if we were on POSIX - introduce colorama as a third-party dep to print colors --- make.bat | 13 +- scripts/internal/winmake.py | 435 +++++++++++------------------------- setup.py | 1 + 3 files changed, 132 insertions(+), 317 deletions(-) diff --git a/make.bat b/make.bat index 97af61c3a0..3589b91bf8 100644 --- a/make.bat +++ b/make.bat @@ -9,20 +9,19 @@ rem make build rem make install rem make test rem -rem This script is modeled after my Windows installation which uses: -rem - Visual studio 2010 for Python 3.4+ -rem ...therefore it might not work on your Windows installation. -rem rem To compile for a specific Python version run: rem set PYTHON=C:\Python34\python.exe & make.bat build +rem +rem To run a specific test: +rem set ARGS=psutil/tests/test_system.py::TestMemoryAPIs::test_virtual_memory && make.bat test rem ========================================================================== if "%PYTHON%" == "" ( set PYTHON=python ) -rem Needed to locate the .pypirc file and upload exes on PyPI. -set HOME=%USERPROFILE% +set PYTHONWARNINGS=always set PSUTIL_DEBUG=1 +set PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 -%PYTHON% scripts\internal\winmake.py %1 %2 %3 %4 %5 %6 +%PYTHON% scripts\internal\winmake.py %1 %2 %3 %4 %5 %6 %7 %8 %9 diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 86d19e4ed8..f3d356cfbb 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -9,39 +9,43 @@ This is supposed to be invoked by "make.bat" and not used directly. This was originally written as a bat file but they suck so much that they should be deemed illegal! -""" +To run a specific test: + set ARGS=psutil/tests/test_system.py && make.bat test +""" -import argparse -import atexit -import ctypes import fnmatch import os +import shlex import shutil import site import subprocess import sys +import colorama + +# configurable via CLI invocation PYTHON = os.getenv('PYTHON', sys.executable) +ARGS = shlex.split(os.getenv("ARGS", "")) + HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) WINDOWS = os.name == "nt" +colorama.init(autoreset=True) +sys.path.insert(0, ROOT_DIR) # so we can import setup.py -sys.path.insert(0, ROOT_DIR) # so that we can import setup.py +# =================================================================== +# utils +# =================================================================== _cmds = {} -GREEN = 2 -LIGHTBLUE = 3 -YELLOW = 6 -RED = 4 -DEFAULT_COLOR = 7 - -# =================================================================== -# utils -# =================================================================== +def clicmd(fun): + """Mark a function to be invoked as a make target.""" + _cmds[fun.__name__.replace("_", "-")] = fun + return fun def safe_print(text, file=sys.stdout): @@ -52,71 +56,40 @@ def safe_print(text, file=sys.stdout): if not isinstance(text, str): return print(text, file=file) try: - file.write(text) + print(text, file=file) except UnicodeEncodeError: bytes_string = text.encode(file.encoding, 'backslashreplace') if hasattr(file, 'buffer'): file.buffer.write(bytes_string) else: text = bytes_string.decode(file.encoding, 'strict') - file.write(text) - file.write("\n") - - -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(s, color=LIGHTBLUE): - if not WINDOWS: - return print(s) - color += 8 # bold - handle = stderr_handle() - SetConsoleTextAttribute = ctypes.windll.Kernel32.SetConsoleTextAttribute - SetConsoleTextAttribute(handle, color) - try: - print(s) - finally: - SetConsoleTextAttribute(handle, DEFAULT_COLOR) + print(text, file=file) + + +def color(s, c): + return c + s -def sh(cmd, nolog=False): +red = lambda s: color(s, colorama.Fore.RED) # noqa: E731 +yellow = lambda s: color(s, colorama.Fore.YELLOW) # noqa: E731 +green = lambda s: color(s, colorama.Fore.GREEN) # noqa: E731 +white = lambda s: color(s, colorama.Fore.WHITE) # noqa: E731 + + +def sh(cmd): + """Run a shell command, exit on failure.""" assert isinstance(cmd, list), repr(cmd) - if not nolog: - safe_print(f"cmd: {cmd}") - p = subprocess.Popen(cmd, env=os.environ, universal_newlines=True) - p.communicate() # print stdout/stderr in real time - if p.returncode != 0: - sys.exit(p.returncode) - - -def rm(pattern, directory=False): - """Recursively remove a file or dir by pattern.""" - if "*" not in pattern: - if directory: - safe_rmtree(pattern) - else: - safe_remove(pattern) - return + safe_print(f"$ {' '.join(cmd)}") + result = subprocess.run( + cmd, + env=os.environ, + text=True, + check=False, + ) - for root, dirs, files in os.walk('.'): - root = os.path.normpath(root) - if root.startswith('.git/'): - continue - found = fnmatch.filter(dirs if directory else files, pattern) - for name in found: - path = os.path.join(root, name) - if directory: - safe_print(f"rmdir -f {path}") - safe_rmtree(path) - else: - safe_print(f"rm {path}") - safe_remove(path) + if result.returncode: + print(red(f"command failed with exit code {result.returncode}")) + sys.exit(result.returncode) def safe_remove(path): @@ -131,9 +104,9 @@ def safe_remove(path): def safe_rmtree(path): - def onerror(func, path, err): - if not issubclass(err[0], FileNotFoundError): - print(err[1]) + def onerror(func, path, exc): + if not issubclass(exc[0], FileNotFoundError): + safe_print(exc[1]) existed = os.path.isdir(path) shutil.rmtree(path, onerror=onerror) @@ -141,20 +114,26 @@ def onerror(func, path, err): safe_print(f"rmdir -f {path}") -def recursive_rm(*patterns): - """Recursively remove a file or matching a list of patterns.""" +def safe_rmpath(path): + """Remove a file or directory at a given path.""" + if os.path.isdir(path): + safe_rmtree(path) + else: + safe_remove(path) + + +def remove_recursive_patterns(*patterns): + """Recursively remove files or directories matching the given + patterns from root. + """ for root, dirs, files in os.walk('.'): root = os.path.normpath(root) if root.startswith('.git/'): continue - for file in files: - for pattern in patterns: - if fnmatch.fnmatch(file, pattern): - safe_remove(os.path.join(root, file)) - for dir in dirs: + for name in dirs + files: for pattern in patterns: - if fnmatch.fnmatch(dir, pattern): - safe_rmtree(os.path.join(root, dir)) + if fnmatch.fnmatch(name, pattern): + safe_rmpath(os.path.join(root, name)) # =================================================================== @@ -162,33 +141,37 @@ def recursive_rm(*patterns): # =================================================================== +@clicmd def build(): """Build / compile.""" - # Make sure setuptools is installed (needed for 'develop' / - # edit mode). - sh([PYTHON, "-c", "import setuptools"]) - # "build_ext -i" copies compiled *.pyd files in ./psutil directory in # order to allow "import psutil" when using the interactive interpreter # from within psutil root directory. cmd = [PYTHON, "setup.py", "build_ext", "-i"] - if os.cpu_count() or 1 > 1: # noqa: PLR0133 + if (os.cpu_count() or 1) > 1: cmd += ['--parallel', str(os.cpu_count())] # Print coloured warnings in real time. - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) try: - for line in iter(p.stdout.readline, b''): - line = line.decode().strip() + for line in iter(p.stdout.readline, ''): + line = line.strip() if 'warning' in line: - win_colorprint(line, YELLOW) - elif 'error' in line: - win_colorprint(line, RED) + print(yellow(line)) + elif ( + 'error' in line + and "errors.c" not in line + and "errors.obj" not in line + and "errors.o" not in line + ): + print(red(line)) else: print(line) # retcode = p.poll() p.communicate() if p.returncode: - win_colorprint("failure", RED) + print(red("failure")) sys.exit(p.returncode) finally: p.terminate() @@ -196,32 +179,23 @@ def build(): # Make sure it actually worked. sh([PYTHON, "-c", "import psutil"]) - win_colorprint("build + import successful", GREEN) - - -def wheel(): - """Create wheel file.""" - build() - sh([PYTHON, "setup.py", "bdist_wheel"]) - - -def upload_wheels(): - """Upload wheel files on PyPI.""" - build() - sh([PYTHON, "-m", "twine", "upload", "dist/*.whl"]) + print(green("build + import successful")) +@clicmd def install_pip(): """Install pip.""" sh([PYTHON, os.path.join(HERE, "install_pip.py")]) +@clicmd def install(): """Install in develop / edit mode.""" build() sh([PYTHON, "setup.py", "develop", "--user"]) +@clicmd def uninstall(): """Uninstall psutil.""" # Uninstalling psutil on Windows seems to be tricky. @@ -247,7 +221,7 @@ def uninstall(): for dir in site.getsitepackages(): for name in os.listdir(dir): if name.startswith('psutil'): - rm(os.path.join(dir, name)) + safe_rmpath(os.path.join(dir, name)) elif name == 'easy-install.pth': # easy_install can add a line (installation path) into # easy-install.pth; that line alters sys.path. @@ -268,9 +242,10 @@ def uninstall(): print(f"removed line {line!r} from {path!r}") +@clicmd def clean(): - """Deletes dev files.""" - recursive_rm( + """Delete build files.""" + remove_recursive_patterns( "$testfn*", "*.bak", "*.core", @@ -287,57 +262,48 @@ def clean(): ".failed-tests.txt", "pytest-cache-files*", ) - safe_rmtree("build") - safe_rmtree(".coverage") - safe_rmtree("dist") - safe_rmtree("docs/_build") - safe_rmtree("htmlcov") - safe_rmtree("tmp") + for path in ( + "build", + ".coverage", + "dist", + "docs/_build", + "htmlcov", + "tmp", + ): + safe_rmpath(path) +@clicmd def install_pydeps_test(): - """Install useful deps.""" install_pip() install_git_hooks() sh([PYTHON, "-m", "pip", "install", "--user", "-U", "-e", ".[test]"]) +@clicmd def install_pydeps_dev(): - """Install useful deps.""" install_pip() install_git_hooks() sh([PYTHON, "-m", "pip", "install", "--user", "-U", "-e", ".[dev]"]) -def test(args=None): +@clicmd +def test(args=ARGS): """Run tests.""" - build() - args = args or [] sh( [PYTHON, "-m", "pytest", "--ignore=psutil/tests/test_memleaks.py"] + args ) -def test_by_name(arg): - """Run specific test by name.""" - build() - sh([PYTHON, "-m", "pytest", arg]) - - -def test_by_regex(arg): - """Run specific test by name.""" - build() - sh([PYTHON, "-m", "pytest"] + ["-k", arg]) - - +@clicmd def test_parallel(): - test(["-n", "auto", "--dist", "loadgroup"]) + """Run tests in parallel.""" + test(args=["-n", "auto", "--dist", "loadgroup"]) +@clicmd def coverage(): - """Run coverage tests.""" - # Note: coverage options are controlled by .coveragerc file build() sh([PYTHON, "-m", "coverage", "run", "-m", "pytest"]) sh([PYTHON, "-m", "coverage", "report"]) @@ -345,86 +311,38 @@ def coverage(): sh([PYTHON, "-m", "webbrowser", "-t", "htmlcov/index.html"]) +@clicmd def test_process(): - """Run process tests.""" build() sh([PYTHON, "-m", "pytest", "-k", "test_process.py"]) -def test_process_all(): - """Run process all tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_process_all.py"]) - - +@clicmd def test_system(): - """Run system tests.""" build() sh([PYTHON, "-m", "pytest", "-k", "test_system.py"]) +@clicmd def test_platform(): - """Run windows only tests.""" build() sh([PYTHON, "-m", "pytest", "-k", "test_windows.py"]) -def test_misc(): - """Run misc tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_misc.py"]) - - -def test_scripts(): - """Run scripts tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_scripts.py"]) - - -def test_unicode(): - """Run unicode tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_unicode.py"]) - - -def test_connections(): - """Run connections tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_connections.py"]) - - -def test_contracts(): - """Run contracts tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_contracts.py"]) - - -def test_testutils(): - """Run test utilities tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_testutils.py"]) - - -def test_sudo(): - """Run sudo utilities tests.""" - build() - sh([PYTHON, "-m", "pytest", "-k", "test_sudo.py"]) - - +@clicmd def test_last_failed(): - """Re-run tests which failed on last run.""" build() - test(["--last-failed"]) + test(args=["--last-failed"]) +@clicmd def test_memleaks(): - """Run memory leaks tests.""" build() sh([PYTHON, "-m", "pytest", "-k", "test_memleaks.py"]) +@clicmd def install_git_hooks(): - """Install GIT pre-commit hook.""" if os.path.isdir('.git'): src = os.path.join( ROOT_DIR, "scripts", "internal", "git_pre_commit.py" @@ -432,38 +350,11 @@ def install_git_hooks(): dst = os.path.realpath( os.path.join(ROOT_DIR, ".git", "hooks", "pre-commit") ) - with open(src) as s: - with open(dst, "w") as d: - d.write(s.read()) - - -def bench_oneshot(): - """Benchmarks for oneshot() ctx manager (see #799).""" - sh([PYTHON, "scripts\\internal\\bench_oneshot.py"]) - - -def bench_oneshot_2(): - """Same as above but using perf module (supposed to be more precise).""" - sh([PYTHON, "scripts\\internal\\bench_oneshot_2.py"]) - - -def print_access_denied(): - """Print AD exceptions raised by all Process methods.""" - build() - sh([PYTHON, "scripts\\internal\\print_access_denied.py"]) - - -def print_api_speed(): - """Benchmark all API calls.""" - build() - sh([PYTHON, "scripts\\internal\\print_api_speed.py"]) - - -def print_sysinfo(): - """Print system info.""" - sh([PYTHON, "scripts\\internal\\print_sysinfo.py"]) + with open(src) as s, open(dst, "w") as d: + d.write(s.read()) +@clicmd def generate_manifest(): """Generate MANIFEST.in file.""" script = "scripts\\internal\\generate_manifest.py" @@ -472,102 +363,26 @@ def generate_manifest(): f.write(out) -def get_python(path): - if not path: - return sys.executable - if os.path.isabs(path): - return path - # try to look for a python installation given a shortcut name - path = path.replace('.', '') - vers = ( - '310-64', - '311-64', - '312-64', - ) - for v in vers: - pypath = rf"C:\\python{v}\python.exe" - if path in pypath and os.path.isfile(pypath): - return pypath - - def parse_args(): - parser = argparse.ArgumentParser() - # option shared by all commands - parser.add_argument('-p', '--python', help="use python executable path") - sp = parser.add_subparsers(dest='command', title='targets') - sp.add_parser('bench-oneshot', help="benchmarks for oneshot()") - sp.add_parser('bench-oneshot_2', help="benchmarks for oneshot() (perf)") - sp.add_parser('build', help="build") - sp.add_parser('clean', help="deletes dev files") - sp.add_parser('coverage', help="run coverage tests.") - sp.add_parser('generate-manifest', help="generate MANIFEST.in file") - sp.add_parser('help', help="print this help") - sp.add_parser('install', help="build + install in develop/edit mode") - sp.add_parser('install-git-hooks', help="install GIT pre-commit hook") - sp.add_parser('install-pip', help="install pip") - sp.add_parser('install-pydeps-dev', help="install dev python deps") - sp.add_parser('install-pydeps-test', help="install python test deps") - sp.add_parser('print-access-denied', help="print AD exceptions") - sp.add_parser('print-api-speed', help="benchmark all API calls") - sp.add_parser('print-sysinfo', help="print system info") - sp.add_parser('test-parallel', help="run tests in parallel") - test = sp.add_parser('test', help="[ARG] run tests") - test_by_name = sp.add_parser('test-by-name', help=" run test by name") - test_by_regex = sp.add_parser( - 'test-by-regex', help=" run test by regex" - ) - sp.add_parser('test-connections', help="run connections tests") - sp.add_parser('test-contracts', help="run contracts tests") - sp.add_parser( - 'test-last-failed', help="re-run tests which failed on last run" - ) - sp.add_parser('test-memleaks', help="run memory leaks tests") - sp.add_parser('test-misc', help="run misc tests") - sp.add_parser('test-scripts', help="run scripts tests") - sp.add_parser('test-platform', help="run windows only tests") - sp.add_parser('test-process', help="run process tests") - sp.add_parser('test-process-all', help="run process all tests") - sp.add_parser('test-system', help="run system tests") - sp.add_parser('test-sudo', help="run sudo tests") - sp.add_parser('test-unicode', help="run unicode tests") - sp.add_parser('test-testutils', help="run test utils tests") - sp.add_parser('uninstall', help="uninstall psutil") - sp.add_parser('upload-wheels', help="upload wheel files on PyPI") - sp.add_parser('wheel', help="create wheel file") - - for p in (test, test_by_name, test_by_regex): - p.add_argument('arg', type=str, nargs='?', default="", help="arg") - - args = parser.parse_args() + if len(sys.argv) <= 1: + # print commands and exit + for name in sorted(_cmds): + doc = _cmds[name].__doc__ or "" + print(f"{green(name):<30} {white(doc)}") + return sys.exit(0) - if not args.command or args.command == 'help': - parser.print_help(sys.stderr) - sys.exit(1) + funcs = [] + for cmd in sys.argv[1:]: + if cmd not in _cmds: + return sys.exit(f"winmake: no target '{cmd}'") + funcs.append(_cmds[cmd]) - return args + return funcs # the 'commands' to execute serially def main(): - global PYTHON - args = parse_args() - # set python exe - PYTHON = get_python(args.python) - if not PYTHON: - return sys.exit( - f"can't find any python installation matching {args.python!r}" - ) - os.putenv('PYTHON', PYTHON) - win_colorprint("using " + PYTHON) - - fname = args.command.replace('-', '_') - fun = getattr(sys.modules[__name__], fname) # err if fun not defined - if args.command == 'test' and args.arg: - sh([PYTHON, args.arg]) # test a script - elif args.command == 'test-by-name': - test_by_name(args.arg) - elif args.command == 'test-by-regex': - test_by_regex(args.arg) - else: + os.environ["PYTHON"] = PYTHON # propagate it to subprocesses + for fun in parse_args(): fun() diff --git a/setup.py b/setup.py index 9a4430f6d6..3020019178 100755 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ "abi3audit", "black", "check-manifest", + "colorama ; os_name == 'nt'", "coverage", "packaging", "pylint", From d99c7e39cc3aecf0f5a561209d62a517cdd71fb7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 18:18:15 +0100 Subject: [PATCH 1408/1714] winmake.py: update docstring --- scripts/internal/winmake.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index f3d356cfbb..4b9ea8cd6b 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -4,13 +4,14 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +"""Shortcuts for various tasks, emulating UNIX "make" command on +Windows. This script is supposed to be invoked via "make.bat" and not +used directly. Like on POSIX, you can run multiple targets serially: -"""Shortcuts for various tasks, emulating UNIX "make" on Windows. -This is supposed to be invoked by "make.bat" and not used directly. -This was originally written as a bat file but they suck so much -that they should be deemed illegal! + make.bat clean build test To run a specific test: + set ARGS=psutil/tests/test_system.py && make.bat test """ @@ -30,10 +31,8 @@ HERE = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) -WINDOWS = os.name == "nt" colorama.init(autoreset=True) -sys.path.insert(0, ROOT_DIR) # so we can import setup.py # =================================================================== # utils From 0ef9575e94591ddcb14e28d12c8bbd1226bf7a9c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 19:22:12 +0100 Subject: [PATCH 1409/1714] clang-format: align escaped newlines --- .clang-format | 1 - psutil/arch/all/init.h | 14 +++++++------- psutil/arch/freebsd/disk.c | 2 +- psutil/arch/windows/proc_info.c | 10 +++++----- psutil/arch/windows/socks.c | 6 +++--- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.clang-format b/.clang-format index 1c910a3b75..3867ab07fa 100644 --- a/.clang-format +++ b/.clang-format @@ -3,7 +3,6 @@ BasedOnStyle: Google AlignAfterOpenBracket: BlockIndent -AlignEscapedNewlines: DontAlign AlignTrailingComments: false AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 3288090ab0..9066237a88 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -107,19 +107,19 @@ extern int PSUTIL_CONN_NONE; // Print a debug message to stderr, including where it originated from // within the C code (file path + lineno). -#define psutil_debug(...) \ - do { \ - if (!PSUTIL_DEBUG) \ - break; \ +#define psutil_debug(...) \ + do { \ + if (!PSUTIL_DEBUG) \ + break; \ fprintf(stderr, "psutil-debug [%s:%d]> ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ } while (0) // strncpy() variant which appends a null terminator. #define PSUTIL_STRNCPY(dst, src, n) \ - strncpy(dst, src, n - 1); \ + strncpy(dst, src, n - 1); \ dst[n - 1] = '\0' diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index e7b9cdcc62..51e38b98b8 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -13,7 +13,7 @@ // convert a bintime struct to milliseconds #define PSUTIL_BT2MSEC(bt) \ - (bt.sec * 1000 \ + (bt.sec * 1000 \ + (((uint64_t)1000000000 * (uint32_t)(bt.frac >> 32)) >> 32) / 1000000) diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index ba22a011b0..5473cae0fc 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -26,12 +26,12 @@ typedef NTSTATUS(NTAPI *__NtQueryInformationProcess)( #define PSUTIL_FIRST_PROCESS(Processes) \ ((PSYSTEM_PROCESS_INFORMATION)(Processes)) -#define PSUTIL_NEXT_PROCESS(Process) \ - (((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset \ - ? (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) \ +#define PSUTIL_NEXT_PROCESS(Process) \ + (((PSYSTEM_PROCESS_INFORMATION)(Process))->NextEntryOffset \ + ? (PSYSTEM_PROCESS_INFORMATION)((PCHAR)(Process) \ + ((PSYSTEM_PROCESS_INFORMATION)(Process \ - )) \ - ->NextEntryOffset) \ + )) \ + ->NextEntryOffset) \ : NULL) diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index ae9007f4aa..12c2fb153b 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -90,9 +90,9 @@ __GetExtendedUdpTable(ULONG family) { #define psutil_conn_decref_objs() \ - Py_DECREF(_AF_INET); \ - Py_DECREF(_AF_INET6); \ - Py_DECREF(_SOCK_STREAM); \ + Py_DECREF(_AF_INET); \ + Py_DECREF(_AF_INET6); \ + Py_DECREF(_SOCK_STREAM); \ Py_DECREF(_SOCK_DGRAM); From 2df2fc2a9abce135748f9afd94c6582040ee3204 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 19:37:54 +0100 Subject: [PATCH 1410/1714] Move psutil_pid_exists into psutil/arch/posix/pids.c --- MANIFEST.in | 1 + psutil/arch/freebsd/proc.c | 4 ++- psutil/arch/posix/pids.c | 54 ++++++++++++++++++++++++++++++++++++++ psutil/arch/posix/proc.c | 49 ---------------------------------- 4 files changed, 58 insertions(+), 50 deletions(-) create mode 100644 psutil/arch/posix/pids.c diff --git a/MANIFEST.in b/MANIFEST.in index bd7ae3e934..037dd4c847 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -99,6 +99,7 @@ include psutil/arch/osx/sys.c include psutil/arch/posix/init.c include psutil/arch/posix/init.h include psutil/arch/posix/net.c +include psutil/arch/posix/pids.c include psutil/arch/posix/proc.c include psutil/arch/posix/sysctl.c include psutil/arch/posix/users.c diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 1536a62bde..351f690f05 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -165,8 +165,10 @@ psutil_proc_exe(PyObject *self, PyObject *args) { } if (size == 0 || strlen(pathname) == 0) { ret = psutil_pid_exists(pid); - if (ret == -1) + if (ret == -1) { + psutil_oserror(); return NULL; + } else if (ret == 0) return psutil_oserror_nsp("psutil_pid_exists -> 0"); else diff --git a/psutil/arch/posix/pids.c b/psutil/arch/posix/pids.c new file mode 100644 index 0000000000..91f60bdabf --- /dev/null +++ b/psutil/arch/posix/pids.c @@ -0,0 +1,54 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "../../arch/all/init.h" + + +// Check if PID exists. Return values: +// 1: exists +// 0: does not exist +// -1: error (Python exception is set) +int +psutil_pid_exists(pid_t pid) { + int ret; + + // No negative PID exists, plus -1 is an alias for sending signal + // too all processes except system ones. Not what we want. + if (pid < 0) + return 0; + + // As per "man 2 kill" PID 0 is an alias for sending the signal to + // every process in the process group of the calling process. Not + // what we want. Some platforms have PID 0, some do not. We decide + // that at runtime. + if (pid == 0) { +#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + return 0; +#else + return 1; +#endif + } + + ret = kill(pid, 0); + if (ret == 0) + return 1; + + // ESRCH == No such process + if (errno == ESRCH) + return 0; + + // EPERM clearly indicates there's a process to deny access to. + if (errno == EPERM) + return 1; + + // According to "man 2 kill" possible error values are (EINVAL, + // EPERM, ESRCH) therefore we should never get here. + return -1; +} diff --git a/psutil/arch/posix/proc.c b/psutil/arch/posix/proc.c index 5671077cb0..227390c65f 100644 --- a/psutil/arch/posix/proc.c +++ b/psutil/arch/posix/proc.c @@ -10,55 +10,6 @@ #include "../../arch/all/init.h" -// Check if PID exists. Return values: -// 1: exists -// 0: does not exist -// -1: error (Python exception is set) -int -psutil_pid_exists(pid_t pid) { - int ret; - - // No negative PID exists, plus -1 is an alias for sending signal - // too all processes except system ones. Not what we want. - if (pid < 0) - return 0; - - // As per "man 2 kill" PID 0 is an alias for sending the signal to - // every process in the process group of the calling process. - // Not what we want. Some platforms have PID 0, some do not. - // We decide that at runtime. - if (pid == 0) { -#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) - return 0; -#else - return 1; -#endif - } - - ret = kill(pid, 0); - if (ret == 0) - return 1; - else { - if (errno == ESRCH) { - // ESRCH == No such process - return 0; - } - else if (errno == EPERM) { - // EPERM clearly indicates there's a process to deny - // access to. - return 1; - } - 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. - psutil_oserror(); - return -1; - } - } -} - // Utility used for those syscalls which do not return a meaningful // error that we can translate into an exception which makes sense. As From df97f0ac58a800c31b453ac03d71be6e821e6662 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 27 Oct 2025 19:46:58 +0100 Subject: [PATCH 1411/1714] Refactor out has_pid_zero() fun --- psutil/arch/posix/pids.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/psutil/arch/posix/pids.c b/psutil/arch/posix/pids.c index 91f60bdabf..368c53c91a 100644 --- a/psutil/arch/posix/pids.c +++ b/psutil/arch/posix/pids.c @@ -11,6 +11,16 @@ #include "../../arch/all/init.h" +static int +has_pid_zero(void) { +#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) + return 0; +#else + return 1; +#endif +} + + // Check if PID exists. Return values: // 1: exists // 0: does not exist @@ -28,13 +38,8 @@ psutil_pid_exists(pid_t pid) { // every process in the process group of the calling process. Not // what we want. Some platforms have PID 0, some do not. We decide // that at runtime. - if (pid == 0) { -#if defined(PSUTIL_LINUX) || defined(PSUTIL_FREEBSD) - return 0; -#else - return 1; -#endif - } + if (pid == 0) + return has_pid_zero(); ret = kill(pid, 0); if (ret == 0) From ce4aec639aedc7869472877f7c56f622d5baff74 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Oct 2025 14:48:43 +0100 Subject: [PATCH 1412/1714] BSD: move / refactor `psutil_kinfo_proc()` (#2671) Since code is almost identical amongst BSD variants let's move this utility function into arch/bsd/proc.c and adjust the differences. --- psutil/arch/bsd/init.h | 2 ++ psutil/arch/bsd/proc.c | 37 +++++++++++++++++++++++++++++++++++++ psutil/arch/freebsd/init.h | 2 -- psutil/arch/freebsd/proc.c | 28 ---------------------------- psutil/arch/netbsd/init.h | 3 --- psutil/arch/netbsd/proc.c | 33 --------------------------------- psutil/arch/openbsd/init.h | 3 --- psutil/arch/openbsd/proc.c | 37 ------------------------------------- 8 files changed, 39 insertions(+), 106 deletions(-) diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 7d051caee3..1fa85b1b42 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -5,12 +5,14 @@ */ #include +#include #if defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) #define PSUTIL_HASNT_KINFO_GETFILE struct kinfo_file *kinfo_getfile(pid_t pid, int *cnt); #endif +int psutil_kinfo_proc(pid_t pid, void *proc); void convert_kvm_err(const char *syscall, char *errbuf); PyObject *psutil_boot_time(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 4554de2bb7..97a400c230 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -34,6 +34,43 @@ #endif +// Fills a kinfo_proc or kinfo_proc2 struct based on process PID. +int +psutil_kinfo_proc(pid_t pid, void *proc) { +#if defined(PSUTIL_FREEBSD) + size_t size = sizeof(struct kinfo_proc); + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; + int len = 4; +#elif defined(PSUTIL_OPENBSD) + size_t size = sizeof(struct kinfo_proc); + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, (int)size, 1}; + int len = 6; +#elif defined(PSUTIL_NETBSD) + size_t size = sizeof(struct kinfo_proc2); + int mib[] = {CTL_KERN, KERN_PROC2, KERN_PROC_PID, pid, (int)size, 1}; + int len = 6; +#else +#error "unsupported BSD variant" +#endif + + if (pid < 0 || proc == NULL) + psutil_badargs("psutil_kinfo_proc"); + + if (sysctl(mib, len, proc, &size, NULL, 0) == -1) { + psutil_oserror_wsyscall("sysctl(kinfo_proc)"); + return -1; + } + + // sysctl stores 0 in size if the process doesn't exist. + if (size == 0) { + psutil_oserror_nsp("sysctl(kinfo_proc), size = 0"); + return -1; + } + + return 0; +} + + // Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an // int as arg and returns an array with cnt struct kinfo_file. // Caller is responsible for freeing the returned pointer with free(). diff --git a/psutil/arch/freebsd/init.h b/psutil/arch/freebsd/init.h index f4009ee49e..8b187cc822 100644 --- a/psutil/arch/freebsd/init.h +++ b/psutil/arch/freebsd/init.h @@ -9,8 +9,6 @@ #include int _psutil_pids(pid_t **pids_array, int *pids_count); -// TODO: move this stuff. Does not belong here -int psutil_kinfo_proc(const pid_t pid, struct kinfo_proc *proc); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 351f690f05..b00c0be9ad 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -32,34 +32,6 @@ // ============================================================================ -int -psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { - // Fills a kinfo_proc struct based on process pid. - int mib[4]; - size_t size; - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PID; - mib[3] = pid; - - if (pid < 0 || !proc) - psutil_badargs("psutil_kinfo_proc"); - - size = sizeof(struct kinfo_proc); - if (sysctl((int *)mib, 4, proc, &size, NULL, 0) == -1) { - psutil_oserror_wsyscall("sysctl(KERN_PROC_PID)"); - return -1; - } - - // sysctl stores 0 in the size if we can't find the process information. - if (size == 0) { - psutil_oserror_nsp("sysctl (size = 0)"); - return -1; - } - return 0; -} - - // remove spaces from string static void psutil_remove_spaces(char *str) { diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h index d79183a6c2..eeeb3cb172 100644 --- a/psutil/arch/netbsd/init.h +++ b/psutil/arch/netbsd/init.h @@ -10,9 +10,6 @@ #include int _psutil_pids(pid_t **pids_array, int *pids_count); -// TODO: refactor this. Does not belong here. -int psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc); -char *psutil_get_cmd_args(pid_t pid, size_t *argsize); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index ebdf98fa08..cda47406ca 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -18,39 +18,6 @@ #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) -// ============================================================================ -// Utility functions -// ============================================================================ - - -int -psutil_kinfo_proc(pid_t pid, struct kinfo_proc2 *proc) { - // Fills a kinfo_proc struct based on process pid. - int ret; - int mib[6]; - size_t size = sizeof(struct kinfo_proc2); - - mib[0] = CTL_KERN; - mib[1] = KERN_PROC2; - mib[2] = KERN_PROC_PID; - mib[3] = pid; - mib[4] = size; - mib[5] = 1; - - ret = sysctl((int *)mib, 6, proc, &size, NULL, 0); - if (ret == -1) { - psutil_oserror(); - return -1; - } - // sysctl stores 0 in the size if we can't find the process information. - if (size == 0) { - psutil_oserror_nsp("sysctl (size = 0)"); - return -1; - } - return 0; -} - - PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { long pid; diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index d6f0336eea..120143dd6d 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -8,9 +8,6 @@ #include int _psutil_pids(pid_t **pids_array, int *pids_count); -// TODO: move / refactor this stuff. It does not belong in here. -typedef struct kinfo_proc kinfo_proc; -int psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 4c4ade7552..74ffacc1ea 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -19,43 +19,6 @@ // #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) -// ============================================================================ -// Utility functions -// ============================================================================ - - -int -psutil_kinfo_proc(pid_t pid, struct kinfo_proc *proc) { - // Fills a kinfo_proc struct based on process pid. - int ret; - int mib[6]; - size_t size = sizeof(struct kinfo_proc); - - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PID; - mib[3] = pid; - mib[4] = size; - mib[5] = 1; - - ret = sysctl((int *)mib, 6, proc, &size, NULL, 0); - if (ret == -1) { - psutil_oserror_wsyscall("sysctl(kinfo_proc)"); - return -1; - } - // sysctl stores 0 in the size if we can't find the process information. - if (size == 0) { - psutil_oserror_nsp("sysctl (size = 0)"); - return -1; - } - return 0; -} - - -// ============================================================================ -// APIS -// ============================================================================ - // TODO: refactor this (it's clunky) PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { From adf42a3ede01af4fa84ff9d07cdc0d37890527d3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Oct 2025 16:35:06 +0100 Subject: [PATCH 1413/1714] BSD / OSX: increase chances to recognize zombie processes (#2672) `psutil_raise_for_pid()` is a utility function which we used for those syscalls that do not return an error that we can directly translate into an exception. E.g. they don't even set errno. So we have to guess. Let's increase this guessing strategy, and check if PID is zombie from within `psutil_raise_for_pid()` itself. This will increase the chances for psutil to raise `ZombieProcess` instead of `AccessDenied`, `OSError` or `RuntimeError`. `psutil_raise_for_pid()` is used by a lot of public process APIs on OSX and BSD. A non extensive list include (but there are more): - open_files() - cwd() - num_fds() - memory_maps() - net_connections() - exe() --- HISTORY.rst | 2 ++ psutil/_psbsd.py | 18 ++++++------------ psutil/arch/bsd/init.h | 1 + psutil/arch/bsd/proc.c | 26 ++++++++++++++++++++++++++ psutil/arch/osx/init.h | 2 +- psutil/arch/osx/proc.c | 22 ++++------------------ psutil/arch/posix/init.c | 3 +++ psutil/arch/posix/init.h | 9 ++++++++- psutil/arch/posix/proc.c | 36 ++++++++++++++++++++++++++++-------- psutil/tests/test_process.py | 3 +++ 10 files changed, 82 insertions(+), 40 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8d867a2388..a074050dc1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ XXXX-XX-XX - 2667_: enforce `clang-format` on all C and header files. It is now the mandatory formatting style for all C sources. +- 2672_, [macOS], [BSD]: increase the chances to recognize zombie processes and + raise the appropriate exception (`ZombieProcess`_). 7.1.2 ===== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index e96a29914e..ea58c76cad 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -569,14 +569,6 @@ def pid_exists(pid): 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. @@ -588,15 +580,17 @@ def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except ProcessLookupError as err: - if is_zombie(pid): + if cext.proc_is_zombie(pid): raise ZombieProcess(pid, name, ppid) from err raise NoSuchProcess(pid, name) from err except PermissionError as err: raise AccessDenied(pid, name) from err + except cext.ZombieProcessError as err: + raise ZombieProcess(pid, name, ppid) from err except OSError as err: if pid == 0 and 0 in pids(): raise AccessDenied(pid, name) from err - raise + raise err from None return wrapper @@ -611,7 +605,7 @@ def wrap_exceptions_procfs(inst): # 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 is_zombie(inst.pid): + if cext.proc_is_zombie(inst.pid): raise ZombieProcess(pid, name, ppid) from err else: raise NoSuchProcess(pid, name) from err @@ -694,7 +688,7 @@ def cmdline(self): except OSError as err: if err.errno == errno.EINVAL: pid, name, ppid = self.pid, self._name, self._ppid - if is_zombie(self.pid): + if cext.proc_is_zombie(self.pid): raise ZombieProcess(pid, name, ppid) from err if not pid_exists(self.pid): raise NoSuchProcess(pid, name, ppid) from err diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 1fa85b1b42..72e3feb3e0 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -14,6 +14,7 @@ struct kinfo_file *kinfo_getfile(pid_t pid, int *cnt); int psutil_kinfo_proc(pid_t pid, void *proc); void convert_kvm_err(const char *syscall, char *errbuf); +int is_zombie(size_t pid); PyObject *psutil_boot_time(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 97a400c230..b14e5fc663 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -111,6 +111,32 @@ kinfo_getfile(pid_t pid, int *cnt) { #endif // PSUTIL_HASNT_KINFO_GETFILE +int +is_zombie(size_t pid) { +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kp; +#else + struct kinfo_proc kp; +#endif + if (psutil_kinfo_proc(pid, &kp) == -1) { + errno = 0; + PyErr_Clear(); + return 0; + } + +#if defined(PSUTIL_FREEBSD) + return kp.ki_stat == SZOMB; +#elif defined(PSUTIL_OPENBSD) + // According to /usr/include/sys/proc.h SZOMB is unused. + // test_zombie_process() shows that SDEAD is the right + // equivalent. + return ((kp.p_stat == SZOMB) || (kp.p_stat == SDEAD)); +#else + return kp.p_stat == SZOMB; +#endif +} + + /* * Collect different info about a process in one shot and return * them as a big Python tuple. diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 4a13f73e0d..4921bee97e 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -11,6 +11,7 @@ extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; int psutil_setup_osx(void); int _psutil_pids(pid_t **pids_array, int *pids_count); +int is_zombie(size_t pid); PyObject *psutil_boot_time(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); @@ -28,7 +29,6 @@ PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_exe(PyObject *self, PyObject *args); -PyObject *psutil_proc_is_zombie(PyObject *self, PyObject *args); PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index a8ebd7aadd..621c68a654 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -69,17 +69,17 @@ psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { } -static int +// Return 1 if PID a zombie, else 0 (including on error). +int is_zombie(size_t pid) { struct kinfo_proc kp; if (psutil_get_kinfo_proc(pid, &kp) == -1) { + errno = 0; PyErr_Clear(); return 0; } - if (kp.kp_proc.p_stat == SZOMB) - return 1; - return 0; + return kp.kp_proc.p_stat == SZOMB; } @@ -279,20 +279,6 @@ psutil_proc_list_fds(pid_t pid, int *num_fds) { // ==================================================================== -// Return True if PID is a zombie else False, including if PID does not -// exist or the underlying function fails. -PyObject * -psutil_proc_is_zombie(PyObject *self, PyObject *args) { - pid_t pid; - - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (is_zombie(pid) == 1) - Py_RETURN_TRUE; - Py_RETURN_FALSE; -} - - /* * Return multiple process info as a Python tuple in one shot by * using sysctl() and filling up a kinfo_proc struct. diff --git a/psutil/arch/posix/init.c b/psutil/arch/posix/init.c index 0c7072aa33..503ae49659 100644 --- a/psutil/arch/posix/init.c +++ b/psutil/arch/posix/init.c @@ -59,6 +59,9 @@ static PyMethodDef posix_methods[] = { #endif #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) {"users", psutil_users, METH_VARARGS}, +#endif +#if defined(PSUTIL_OSX) || defined(PSUTIL_BSD) + {"proc_is_zombie", psutil_proc_is_zombie, METH_VARARGS}, #endif {NULL, NULL, 0, NULL} }; diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index e1968fbe97..28158bdb71 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -32,11 +32,15 @@ extern PyObject *ZombieProcessError; #endif // clang-format on +// --- internal utils + int psutil_pid_exists(pid_t pid); long psutil_getpagesize(void); -void psutil_raise_for_pid(pid_t pid, char *msg); int psutil_posix_add_constants(PyObject *mod); int psutil_posix_add_methods(PyObject *mod); +PyObject *psutil_raise_for_pid(pid_t pid, char *msg); + +// --- Python wrappers PyObject *psutil_getpagesize_pywrapper(PyObject *self, PyObject *args); PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); @@ -45,3 +49,6 @@ PyObject *psutil_net_if_is_running(PyObject *self, PyObject *args); PyObject *psutil_net_if_mtu(PyObject *self, PyObject *args); PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); +#if defined(PSUTIL_OSX) || defined(PSUTIL_BSD) +PyObject *psutil_proc_is_zombie(PyObject *self, PyObject *args); +#endif diff --git a/psutil/arch/posix/proc.c b/psutil/arch/posix/proc.c index 227390c65f..f8cd7e6a92 100644 --- a/psutil/arch/posix/proc.c +++ b/psutil/arch/posix/proc.c @@ -4,28 +4,48 @@ * found in the LICENSE file. */ - +#include #include +#include #include #include "../../arch/all/init.h" +#if defined(PSUTIL_OSX) || defined(PSUTIL_BSD) +// Return True if PID is a zombie else False, including if PID does not +// exist or the underlying function fails (never raise exception). +PyObject * +psutil_proc_is_zombie(PyObject *self, PyObject *args) { + pid_t pid; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (is_zombie(pid) == 1) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} +#endif + + // Utility used for those syscalls which do not return a meaningful -// error that we can translate into an exception which makes sense. As -// such, we'll have to guess. On UNIX, if errno is set, we return that -// one (OSError). Else, if PID does not exist we assume the syscall -// failed because of that so we raise NoSuchProcess. If none of this is -// true we giveup and raise RuntimeError(msg). This will always set a -// Python exception and return NULL. -void +// error that we can directly translate into an exception which makes +// sense. As such we'll have to guess, e.g. if errno is set or if PID +// does not exist. If reason can't be determined raise RuntimeError. +PyObject * psutil_raise_for_pid(pid_t pid, char *syscall) { if (errno != 0) psutil_oserror_wsyscall(syscall); else if (psutil_pid_exists(pid) == 0) psutil_oserror_nsp(syscall); +#if defined(PSUTIL_OSX) || defined(PSUTIL_BSD) + else if (is_zombie(pid)) + PyErr_SetString(ZombieProcessError, ""); +#endif else psutil_runtime_error("%s syscall failed", syscall); + return NULL; } diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index f7b1c07b9e..fa03150e3e 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -1369,6 +1369,9 @@ def assert_raises_nsp(fun, fun_name): def test_zombie_process(self): _parent, zombie = self.spawn_zombie() self.assert_proc_zombie(zombie) + if hasattr(psutil._psplatform.cext, "proc_is_zombie"): + assert not psutil._psplatform.cext.proc_is_zombie(os.getpid()) + assert psutil._psplatform.cext.proc_is_zombie(zombie.pid) @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process_is_running_w_exc(self): From 58f2da724134b7c4c388304f0fb645513facc757 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 28 Oct 2025 21:05:03 +0100 Subject: [PATCH 1414/1714] Remove unused .h includes --- psutil/arch/bsd/cpu.c | 2 +- psutil/arch/bsd/disk.c | 8 +++----- psutil/arch/bsd/init.c | 4 ++-- psutil/arch/bsd/init.h | 3 +++ psutil/arch/bsd/proc.c | 15 --------------- psutil/arch/bsd/sys.c | 7 ------- psutil/arch/freebsd/disk.c | 1 - psutil/arch/freebsd/init.h | 2 +- psutil/arch/freebsd/mem.c | 3 +-- psutil/arch/freebsd/proc.c | 17 +---------------- psutil/arch/linux/proc.c | 14 ++++++++++++-- psutil/arch/netbsd/init.h | 2 +- psutil/arch/netbsd/proc.c | 4 ---- psutil/arch/netbsd/socks.c | 8 -------- psutil/arch/openbsd/proc.c | 3 --- psutil/arch/osx/proc.c | 4 ---- psutil/arch/posix/init.h | 7 +++++++ psutil/arch/sunos/proc.c | 2 -- 18 files changed, 32 insertions(+), 74 deletions(-) diff --git a/psutil/arch/bsd/cpu.c b/psutil/arch/bsd/cpu.c index 48daef1834..4cced2d9c9 100644 --- a/psutil/arch/bsd/cpu.c +++ b/psutil/arch/bsd/cpu.c @@ -7,8 +7,8 @@ #include #include #include -#include #include +#include // CP_* on OpenBSD #include "../../arch/all/init.h" diff --git a/psutil/arch/bsd/disk.c b/psutil/arch/bsd/disk.c index fbd0fded00..0cfa7133ca 100644 --- a/psutil/arch/bsd/disk.c +++ b/psutil/arch/bsd/disk.c @@ -5,13 +5,10 @@ */ #include -#include -#if PSUTIL_NETBSD -// getvfsstat() +#if PSUTIL_NETBSD // getvfsstat() #include #include -#else -// getfsstat() +#else // getfsstat() #include #include #include @@ -19,6 +16,7 @@ #include "../../arch/all/init.h" + PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { int num; diff --git a/psutil/arch/bsd/init.c b/psutil/arch/bsd/init.c index a4831b5614..ad4dc1f1f7 100644 --- a/psutil/arch/bsd/init.c +++ b/psutil/arch/bsd/init.c @@ -6,14 +6,14 @@ #include #include +#include #include "../../arch/all/init.h" -#include "init.h" void convert_kvm_err(const char *syscall, char *errbuf) { - char fullmsg[8192]; + char fullmsg[512]; sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); if (strstr(errbuf, "Permission denied") != NULL) diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 72e3feb3e0..9a5bc10bb8 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -7,6 +7,9 @@ #include #include + +#define PSUTIL_KPT2DOUBLE(t) (t##_sec + t##_usec / 1000000.0) + #if defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) #define PSUTIL_HASNT_KINFO_GETFILE struct kinfo_file *kinfo_getfile(pid_t pid, int *cnt); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index b14e5fc663..f994d612da 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -17,21 +17,6 @@ #endif #include "../../arch/all/init.h" -#ifdef PSUTIL_FREEBSD -#include "../../arch/freebsd/init.h" // TODO: refactor this -#elif PSUTIL_OPENBSD -#include "../../arch/openbsd/init.h" // TODO: refactor this -#elif PSUTIL_NETBSD -#include "../../arch/netbsd/init.h" // TODO: refactor this -#endif - - -// convert a timeval struct to a double -#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) - -#if defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) -#define PSUTIL_KPT2DOUBLE(t) (t##_sec + t##_usec / 1000000.0) -#endif // Fills a kinfo_proc or kinfo_proc2 struct based on process PID. diff --git a/psutil/arch/bsd/sys.c b/psutil/arch/bsd/sys.c index 287e1cb0f0..8205dd2af5 100644 --- a/psutil/arch/bsd/sys.c +++ b/psutil/arch/bsd/sys.c @@ -6,13 +6,6 @@ #include #include -#include -#include // OS version -#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) -#include -#elif defined(PSUTIL_OPENBSD) -#include -#endif #include "../../arch/all/init.h" diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index 51e38b98b8..6e15cd23f9 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -5,7 +5,6 @@ */ #include -#include #include #include "../../arch/all/init.h" diff --git a/psutil/arch/freebsd/init.h b/psutil/arch/freebsd/init.h index 8b187cc822..e4a388052a 100644 --- a/psutil/arch/freebsd/init.h +++ b/psutil/arch/freebsd/init.h @@ -6,7 +6,7 @@ */ #include -#include +#include int _psutil_pids(pid_t **pids_array, int *pids_count); diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 25a48425cd..4246087d72 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -6,11 +6,10 @@ #include -#include +#include #include #include #include -#include #include #include diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index b00c0be9ad..ec06ada2a3 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -5,28 +5,13 @@ */ #include -#include -#include -#include -#include -#include -#include #include -#include #include -#include -#include -#include -#include -#include // process open files, shared libs (kinfo_getvmmap), cwd -#include +#include #include "../../arch/all/init.h" -#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) - - // ============================================================================ // Utility functions // ============================================================================ diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index be7bcd0de6..f608bffdc5 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -11,6 +11,12 @@ #include #include + +// ==================================================================== +// --- process priority (niceness) +// ==================================================================== + + #ifdef PSUTIL_HAS_IOPRIO enum { IOPRIO_WHO_PROCESS = 1, @@ -71,9 +77,13 @@ psutil_proc_ioprio_set(PyObject *self, PyObject *args) { #endif // PSUTIL_HAS_IOPRIO -#ifdef PSUTIL_HAS_CPU_AFFINITY +// ==================================================================== +// --- process CPU affinity +// ==================================================================== + -// Return process CPU affinity as a Python list. +#ifdef PSUTIL_HAS_CPU_AFFINITY +// Return process CPU affinity as a list of integers. PyObject * psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { int cpu, ncpus, count, cpucount_s; diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h index eeeb3cb172..d10626b020 100644 --- a/psutil/arch/netbsd/init.h +++ b/psutil/arch/netbsd/init.h @@ -7,7 +7,7 @@ */ #include -#include +#include int _psutil_pids(pid_t **pids_array, int *pids_count); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index cda47406ca..09b5ed5c52 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -14,10 +14,6 @@ #include "../../arch/all/init.h" -#define PSUTIL_KPT2DOUBLE(t) (t##_sec + t##_usec / 1000000.0) -#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) - - PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { long pid; diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index 668bd89e14..abb315e9c6 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -7,19 +7,11 @@ */ #include -#include -#include -#include #include #include -#include -#include -#include -#include #include #include #include -#include #include "../../arch/all/init.h" diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 74ffacc1ea..09f3055eda 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -15,9 +15,6 @@ #include "../../arch/all/init.h" -#define PSUTIL_KPT2DOUBLE(t) (t##_sec + t##_usec / 1000000.0) -// #define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) - // TODO: refactor this (it's clunky) PyObject * diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 621c68a654..8f01dfbf17 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -11,7 +11,6 @@ // https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/arch/osx/process_info.c #include -#include #include #include #include @@ -32,9 +31,6 @@ #include "../../arch/all/init.h" -#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) - - // ==================================================================== // --- utils // ==================================================================== diff --git a/psutil/arch/posix/init.h b/psutil/arch/posix/init.h index 28158bdb71..ec4391a664 100644 --- a/psutil/arch/posix/init.h +++ b/psutil/arch/posix/init.h @@ -6,6 +6,13 @@ extern PyObject *ZombieProcessError; +// convert a timeval struct to a double +#ifdef PSUTIL_SUNOS +#define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) +#else +#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) +#endif + // clang-format off #if !defined(PSUTIL_OPENBSD) && !defined(PSUTIL_AIX) #define PSUTIL_HAS_POSIX_USERS diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index 9a000e4fa1..3fbc0ddb18 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -11,8 +11,6 @@ #include "../../arch/all/init.h" -#define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) - // Read a file content and fills a C structure with it. static int From 169ad9fb9541a46afdc920f248b84e570951cd73 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 14:09:09 +0100 Subject: [PATCH 1415/1714] Makefile: use macro to avoid code repetition when running tests --- Makefile | 61 ++++++++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/Makefile b/Makefile index 412f5d023c..bd0393f004 100644 --- a/Makefile +++ b/Makefile @@ -90,65 +90,60 @@ install-git-hooks: ## Install GIT pre-commit hook. # Tests # =================================================================== -test: ## Run all tests. To run a specific test do "make test ARGS=psutil.tests.test_system.TestDiskAPIs" - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py --ignore=psutil/tests/test_sudo.py $(ARGS) +define run_test + $(MAKE) build + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(1) $(ARGS) +endef + +test: ## Run all tests. + # To run a specific test do `make test ARGS=psutil.tests.test_system.TestDiskAPIs` + $(call run_test, --ignore=psutil/tests/test_memleaks.py --ignore=psutil/tests/test_sudo.py) test-parallel: ## Run all tests in parallel. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --ignore=psutil/tests/test_memleaks.py -p xdist -n auto --dist loadgroup $(ARGS) + $(call run_test, --ignore=psutil/tests/test_memleaks.py -p xdist -n auto --dist loadgroup) test-process: ## Run process-related API tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_process.py + $(call run_test, psutil/tests/test_process.py) test-process-all: ## Run tests which iterate over all process PIDs. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_process_all.py + $(call run_test, psutil/tests/test_process_all.py) test-system: ## Run system-related API tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_system.py + $(call run_test, psutil/tests/test_system.py) test-misc: ## Run miscellaneous tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_misc.py + $(call run_test, psutil/tests/test_misc.py) test-scripts: ## Run scripts tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_scripts.py + $(call run_test, psutil/tests/test_scripts.py) test-testutils: ## Run test utils tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_testutils.py + $(call run_test, psutil/tests/test_testutils.py) test-unicode: ## Test APIs dealing with strings. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_unicode.py + $(call run_test, psutil/tests/test_unicode.py) test-contracts: ## APIs sanity tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_contracts.py + $(call run_test, psutil/tests/test_contracts.py) test-connections: ## Test psutil.net_connections() and Process.net_connections(). - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_connections.py + $(call run_test, psutil/tests/test_connections.py) test-posix: ## POSIX specific tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_posix.py + $(call run_test, psutil/tests/test_posix.py) test-platform: ## Run specific platform tests only. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py + $(call run_test, psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py) test-memleaks: ## Memory leak tests. - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(ARGS) psutil/tests/test_memleaks.py + $(call run_test, psutil/tests/test_memleaks.py) + +test-sudo: ## Run tests requiring root privileges. + # Use unittest runner because pytest may not be installed as root. + $(SUDO) $(PYTHON_ENV_VARS) $(PYTHON) -m unittest -v psutil.tests.test_sudo test-last-failed: ## Re-run tests which failed on last run - ${MAKE} build - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --last-failed $(ARGS) + $(call run_test, --last-failed) test-coverage: ## Run test coverage. ${MAKE} build @@ -160,10 +155,6 @@ test-coverage: ## Run test coverage. $(PYTHON) -m coverage html $(PYTHON) -m webbrowser -t htmlcov/index.html -test-sudo: ## Run tests requiring root privileges. - # Use unittest runner because pytest may not be installed as root. - $(SUDO) $(PYTHON_ENV_VARS) $(PYTHON) -m unittest -v psutil.tests.test_sudo - # =================================================================== # Linters # =================================================================== From fbb4b69d197498160e38470545dbd53710eab9b7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 14:11:30 +0100 Subject: [PATCH 1416/1714] Makefile: use $(MAKE) instead of ${MAKE} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $(...) is the conventional, portable, and recommended form in GNU make’s own documentation and examples. --- Makefile | 88 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index bd0393f004..25d3a8e970 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ build: ## Compile (in parallel) without installing. $(PYTHON_ENV_VARS) $(PYTHON) -c "import psutil" # make sure it actually worked install: ## Install this package as current user in "edit" mode. - ${MAKE} build + $(MAKE) build $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(SETUP_INSTALL_ARGS) uninstall: ## Uninstall this package via pip. @@ -74,12 +74,12 @@ install-sysdeps: ./scripts/internal/install-sysdeps.sh install-pydeps-test: ## Install python deps necessary to run unit tests. - ${MAKE} install-pip + $(MAKE) install-pip $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test] install-pydeps-dev: ## Install python deps meant for local development. - ${MAKE} install-git-hooks - ${MAKE} install-pip + $(MAKE) install-git-hooks + $(MAKE) install-pip $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test,dev] install-git-hooks: ## Install GIT pre-commit hook. @@ -146,7 +146,7 @@ test-last-failed: ## Re-run tests which failed on last run $(call run_test, --last-failed) test-coverage: ## Run test coverage. - ${MAKE} build + $(MAKE) build # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest --ignore=psutil/tests/test_memleaks.py $(ARGS) @@ -178,12 +178,12 @@ lint-toml: ## Run linter for pyproject.toml. @git ls-files '*.toml' | xargs toml-sort --check lint-all: ## Run all linters - ${MAKE} black - ${MAKE} ruff - ${MAKE} lint-c - ${MAKE} dprint - ${MAKE} lint-rst - ${MAKE} lint-toml + $(MAKE) black + $(MAKE) ruff + $(MAKE) lint-c + $(MAKE) dprint + $(MAKE) lint-rst + $(MAKE) lint-toml # --- not mandatory linters (just run from time to time) @@ -213,10 +213,10 @@ fix-dprint: @$(DPRINT) fmt fix-all: ## Run all code fixers. - ${MAKE} fix-ruff - ${MAKE} fix-black - ${MAKE} fix-toml - ${MAKE} fix-dprint + $(MAKE) fix-ruff + $(MAKE) fix-black + $(MAKE) fix-toml + $(MAKE) fix-dprint # =================================================================== # CI jobs @@ -227,30 +227,30 @@ ci-lint: ## Run all linters on GitHub CI. curl -fsSL https://dprint.dev/install.sh | sh $(DPRINT) --version clang-format --version - ${MAKE} lint-all + $(MAKE) lint-all ci-test: ## Run tests on GitHub CI. Used by BSD runners. - ${MAKE} install-sysdeps - PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test - ${MAKE} print-sysinfo + $(MAKE) install-sysdeps + PIP_BREAK_SYSTEM_PACKAGES=1 $(MAKE) install-pydeps-test + $(MAKE) print-sysinfo $(PYTHON_ENV_VARS) $(PYTHON) -m pytest psutil/tests/ ci-test-cibuildwheel: ## Run tests from cibuildwheel. # testing the wheels means we can't use other test targets which are rebuilding the python extensions # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed - ${MAKE} install-sysdeps - PIP_BREAK_SYSTEM_PACKAGES=1 ${MAKE} install-pydeps-test - ${MAKE} print-sysinfo + $(MAKE) install-sysdeps + PIP_BREAK_SYSTEM_PACKAGES=1 $(MAKE) install-pydeps-test + $(MAKE) print-sysinfo mkdir -p .tests cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs psutil.tests ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit - ${MAKE} sdist + $(MAKE) sdist mv wheelhouse/* dist/ - ${MAKE} check-dist - ${MAKE} install - ${MAKE} print-dist + $(MAKE) check-dist + $(MAKE) install + $(MAKE) print-dist # =================================================================== # Distribution @@ -273,25 +273,25 @@ check-wheels: ## Check sanity of wheels. $(PYTHON) -m twine check --strict dist/*.whl check-dist: ## Run all sanity checks re. to the package distribution. - ${MAKE} check-manifest - ${MAKE} check-pyproject - ${MAKE} check-sdist - ${MAKE} check-wheels + $(MAKE) check-manifest + $(MAKE) check-pyproject + $(MAKE) check-sdist + $(MAKE) check-wheels generate-manifest: ## Generates MANIFEST.in file. $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in sdist: ## Create tar.gz source distribution. - ${MAKE} generate-manifest + $(MAKE) generate-manifest $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist create-wheels: ## Create .whl files $(PYTHON_ENV_VARS) $(PYTHON) setup.py bdist_wheel pre-release: ## Check if we're ready to produce a new release. - ${MAKE} clean - ${MAKE} sdist - ${MAKE} install + $(MAKE) clean + $(MAKE) sdist + $(MAKE) install @$(PYTHON) -c \ "import requests, sys; \ from packaging.version import parse; \ @@ -306,19 +306,19 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in doc, '%r not found in docs/index.rst' % ver; \ assert ver in history, '%r not found in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX found in HISTORY.rst';" - ${MAKE} download-wheels - ${MAKE} check-dist - ${MAKE} print-hashes - ${MAKE} print-dist + $(MAKE) download-wheels + $(MAKE) check-dist + $(MAKE) print-hashes + $(MAKE) print-dist release: ## Upload a new release. $(PYTHON) -m twine upload dist/*.tar.gz $(PYTHON) -m twine upload dist/*.whl - ${MAKE} git-tag-release + $(MAKE) git-tag-release download-wheels: ## Download latest wheels hosted on github. $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token - ${MAKE} print-dist + $(MAKE) print-dist git-tag-release: ## Git-tag a new release. git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` @@ -335,11 +335,11 @@ print-timeline: ## Print releases' timeline. @$(PYTHON) scripts/internal/print_timeline.py print-access-denied: ## Print AD exceptions - ${MAKE} build + $(MAKE) build @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls - ${MAKE} build + $(MAKE) build @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) print-downloads: ## Print PYPI download statistics @@ -362,11 +362,11 @@ grep-todos: ## Look for TODOs in the source files. git grep -EIn "TODO|FIXME|XXX" bench-oneshot: ## Benchmarks for oneshot() ctx manager (see #799). - ${MAKE} build + $(MAKE) build $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot.py bench-oneshot-2: ## Same as above but using perf module (supposed to be more precise) - ${MAKE} build + $(MAKE) build $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot_2.py find-broken-links: ## Look for broken links in source files. From b27d43ffa016f6c58bec8ca53436f57efbe67085 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 14:15:24 +0100 Subject: [PATCH 1417/1714] Makefile: make help: use awk instead of grep + handle targets with numbers and dots --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 25d3a8e970..6fcc5eb84d 100644 --- a/Makefile +++ b/Makefile @@ -373,4 +373,4 @@ find-broken-links: ## Look for broken links in source files. git ls-files | xargs $(PYTHON) -Wa scripts/internal/find_broken_links.py help: ## Display callable targets. - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + @awk -F':.*?## ' '/^[a-zA-Z0-9_.-]+:.*?## / {printf "\033[36m%-24s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort From fb17cee728089627def5fe41588c4bffc89c87d1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 14:22:44 +0100 Subject: [PATCH 1418/1714] Makefile: avoid unneeded var --- Makefile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 6fcc5eb84d..a09493716e 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,6 @@ PYTHON = python3 ARGS = -# In not in a virtualenv, add --user options for install commands. -SETUP_INSTALL_ARGS = `$(PYTHON) -c \ - "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 SUDO = $(if $(filter $(OS),Windows_NT),,sudo -E) @@ -61,7 +58,9 @@ build: ## Compile (in parallel) without installing. install: ## Install this package as current user in "edit" mode. $(MAKE) build - $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(SETUP_INSTALL_ARGS) + # If not in a virtualenv, add --user to the install command. + $(PYTHON_ENV_VARS) $(PYTHON) setup.py develop $(SETUP_INSTALL_ARGS) `$(PYTHON) -c \ + "import sys; print('' if hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix else '--user')"` uninstall: ## Uninstall this package via pip. cd ..; $(PYTHON_ENV_VARS) $(PYTHON) -m pip uninstall -y -v psutil || true From 07cb1fab8e332147e2885cd064d9035e4e6fe9af Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 14:26:03 +0100 Subject: [PATCH 1419/1714] (IMPORTANT) make: do not build before tests; do it manually --- Makefile | 6 ------ scripts/internal/winmake.py | 6 ------ 2 files changed, 12 deletions(-) diff --git a/Makefile b/Makefile index a09493716e..145f88f026 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,6 @@ install-git-hooks: ## Install GIT pre-commit hook. # =================================================================== define run_test - $(MAKE) build $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(1) $(ARGS) endef @@ -145,7 +144,6 @@ test-last-failed: ## Re-run tests which failed on last run $(call run_test, --last-failed) test-coverage: ## Run test coverage. - $(MAKE) build # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest --ignore=psutil/tests/test_memleaks.py $(ARGS) @@ -334,11 +332,9 @@ print-timeline: ## Print releases' timeline. @$(PYTHON) scripts/internal/print_timeline.py print-access-denied: ## Print AD exceptions - $(MAKE) build @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls - $(MAKE) build @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) print-downloads: ## Print PYPI download statistics @@ -361,11 +357,9 @@ grep-todos: ## Look for TODOs in the source files. git grep -EIn "TODO|FIXME|XXX" bench-oneshot: ## Benchmarks for oneshot() ctx manager (see #799). - $(MAKE) build $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot.py bench-oneshot-2: ## Same as above but using perf module (supposed to be more precise) - $(MAKE) build $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot_2.py find-broken-links: ## Look for broken links in source files. diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 4b9ea8cd6b..5d4cee4868 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -303,7 +303,6 @@ def test_parallel(): @clicmd def coverage(): - build() sh([PYTHON, "-m", "coverage", "run", "-m", "pytest"]) sh([PYTHON, "-m", "coverage", "report"]) sh([PYTHON, "-m", "coverage", "html"]) @@ -312,31 +311,26 @@ def coverage(): @clicmd def test_process(): - build() sh([PYTHON, "-m", "pytest", "-k", "test_process.py"]) @clicmd def test_system(): - build() sh([PYTHON, "-m", "pytest", "-k", "test_system.py"]) @clicmd def test_platform(): - build() sh([PYTHON, "-m", "pytest", "-k", "test_windows.py"]) @clicmd def test_last_failed(): - build() test(args=["--last-failed"]) @clicmd def test_memleaks(): - build() sh([PYTHON, "-m", "pytest", "-k", "test_memleaks.py"]) From 618cbfda1e338998e94f3ce12fbc9737bcfb0f74 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 18:48:16 +0100 Subject: [PATCH 1420/1714] New psutil/arch/bsd/proc_utils.c and psutil/arch/osx/proc_utils.c --- MANIFEST.in | 3 + Makefile | 46 +++---- docs/STATS.md | 5 + psutil/arch/bsd/proc.c | 111 +-------------- psutil/arch/bsd/proc_utils.c | 122 +++++++++++++++++ psutil/arch/osx/init.h | 11 ++ psutil/arch/osx/proc.c | 244 --------------------------------- psutil/arch/osx/proc_utils.c | 255 +++++++++++++++++++++++++++++++++++ 8 files changed, 421 insertions(+), 376 deletions(-) create mode 100644 docs/STATS.md create mode 100644 psutil/arch/bsd/proc_utils.c create mode 100644 psutil/arch/osx/proc_utils.c diff --git a/MANIFEST.in b/MANIFEST.in index 037dd4c847..5f8e39cf19 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,6 +15,7 @@ include docs/DEVGUIDE.rst include docs/DEVNOTES include docs/Makefile include docs/README +include docs/STATS.md include docs/_static/copybutton.js include docs/_static/css/custom.css include docs/_static/favicon.ico @@ -56,6 +57,7 @@ include psutil/arch/bsd/init.c include psutil/arch/bsd/init.h include psutil/arch/bsd/net.c include psutil/arch/bsd/proc.c +include psutil/arch/bsd/proc_utils.c include psutil/arch/bsd/sys.c include psutil/arch/freebsd/cpu.c include psutil/arch/freebsd/disk.c @@ -94,6 +96,7 @@ include psutil/arch/osx/mem.c include psutil/arch/osx/net.c include psutil/arch/osx/pids.c include psutil/arch/osx/proc.c +include psutil/arch/osx/proc_utils.c include psutil/arch/osx/sensors.c include psutil/arch/osx/sys.c include psutil/arch/posix/init.c diff --git a/Makefile b/Makefile index 145f88f026..23831dcbc3 100644 --- a/Makefile +++ b/Makefile @@ -89,59 +89,57 @@ install-git-hooks: ## Install GIT pre-commit hook. # Tests # =================================================================== -define run_test - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(1) $(ARGS) -endef +RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest test: ## Run all tests. - # To run a specific test do `make test ARGS=psutil.tests.test_system.TestDiskAPIs` - $(call run_test, --ignore=psutil/tests/test_memleaks.py --ignore=psutil/tests/test_sudo.py) + # To run a specific test do `make test ARGS=psutil/tests/test_process.py::TestProcess::test_cmdline` + $(RUN_TEST) --ignore=psutil/tests/test_memleaks.py --ignore=psutil/tests/test_sudo.py $(ARGS) test-parallel: ## Run all tests in parallel. - $(call run_test, --ignore=psutil/tests/test_memleaks.py -p xdist -n auto --dist loadgroup) + $(RUN_TEST) --ignore=psutil/tests/test_memleaks.py -p xdist -n auto --dist loadgroup $(ARGS) test-process: ## Run process-related API tests. - $(call run_test, psutil/tests/test_process.py) + $(RUN_TEST) psutil/tests/test_process.py $(ARGS) test-process-all: ## Run tests which iterate over all process PIDs. - $(call run_test, psutil/tests/test_process_all.py) + $(RUN_TEST) psutil/tests/test_process_all.py $(ARGS) test-system: ## Run system-related API tests. - $(call run_test, psutil/tests/test_system.py) + $(RUN_TEST) psutil/tests/test_system.py $(ARGS) test-misc: ## Run miscellaneous tests. - $(call run_test, psutil/tests/test_misc.py) + $(RUN_TEST) psutil/tests/test_misc.py $(ARGS) test-scripts: ## Run scripts tests. - $(call run_test, psutil/tests/test_scripts.py) + $(RUN_TEST) psutil/tests/test_scripts.py $(ARGS) test-testutils: ## Run test utils tests. - $(call run_test, psutil/tests/test_testutils.py) + $(RUN_TEST) psutil/tests/test_testutils.py $(ARGS) test-unicode: ## Test APIs dealing with strings. - $(call run_test, psutil/tests/test_unicode.py) + $(RUN_TEST) psutil/tests/test_unicode.py $(ARGS) test-contracts: ## APIs sanity tests. - $(call run_test, psutil/tests/test_contracts.py) + $(RUN_TEST) psutil/tests/test_contracts.py $(ARGS) test-connections: ## Test psutil.net_connections() and Process.net_connections(). - $(call run_test, psutil/tests/test_connections.py) + $(RUN_TEST) psutil/tests/test_connections.py $(ARGS) test-posix: ## POSIX specific tests. - $(call run_test, psutil/tests/test_posix.py) + $(RUN_TEST) psutil/tests/test_posix.py $(ARGS) test-platform: ## Run specific platform tests only. - $(call run_test, psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py) + $(RUN_TEST) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) test-memleaks: ## Memory leak tests. - $(call run_test, psutil/tests/test_memleaks.py) + $(RUN_TEST) psutil/tests/test_memleaks.py $(ARGS) test-sudo: ## Run tests requiring root privileges. # Use unittest runner because pytest may not be installed as root. $(SUDO) $(PYTHON_ENV_VARS) $(PYTHON) -m unittest -v psutil.tests.test_sudo test-last-failed: ## Re-run tests which failed on last run - $(call run_test, --last-failed) + $(RUN_TEST) --last-failed $(ARGS) test-coverage: ## Run test coverage. # Note: coverage options are controlled by .coveragerc file @@ -314,7 +312,7 @@ release: ## Upload a new release. $(MAKE) git-tag-release download-wheels: ## Download latest wheels hosted on github. - $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token + $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token $(MAKE) print-dist git-tag-release: ## Git-tag a new release. @@ -332,10 +330,10 @@ print-timeline: ## Print releases' timeline. @$(PYTHON) scripts/internal/print_timeline.py print-access-denied: ## Print AD exceptions - @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_access_denied.py + $(PYTHON) scripts/internal/print_access_denied.py print-api-speed: ## Benchmark all API calls - @$(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) + $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) print-downloads: ## Print PYPI download statistics $(PYTHON) scripts/internal/print_downloads.py @@ -357,10 +355,10 @@ grep-todos: ## Look for TODOs in the source files. git grep -EIn "TODO|FIXME|XXX" bench-oneshot: ## Benchmarks for oneshot() ctx manager (see #799). - $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot.py + $(PYTHON) scripts/internal/bench_oneshot.py bench-oneshot-2: ## Same as above but using perf module (supposed to be more precise) - $(PYTHON_ENV_VARS) $(PYTHON) scripts/internal/bench_oneshot_2.py + $(PYTHON) scripts/internal/bench_oneshot_2.py find-broken-links: ## Look for broken links in source files. git ls-files | xargs $(PYTHON) -Wa scripts/internal/find_broken_links.py diff --git a/docs/STATS.md b/docs/STATS.md new file mode 100644 index 0000000000..5b87ecf364 --- /dev/null +++ b/docs/STATS.md @@ -0,0 +1,5 @@ +# Download statistics + +- https://pepy.tech/projects/psutil +- https://clickpy.clickhouse.com/dashboard/psutil +- https://pypistats.org/packages/psutil diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index f994d612da..1f9f86ce76 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -10,118 +10,13 @@ #include #include #include -#include // VREG -#ifdef PSUTIL_FREEBSD -#include // kinfo_proc, kinfo_file, KF_* -#include // kinfo_getfile() -#endif +#include +#include +#include #include "../../arch/all/init.h" -// Fills a kinfo_proc or kinfo_proc2 struct based on process PID. -int -psutil_kinfo_proc(pid_t pid, void *proc) { -#if defined(PSUTIL_FREEBSD) - size_t size = sizeof(struct kinfo_proc); - int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; - int len = 4; -#elif defined(PSUTIL_OPENBSD) - size_t size = sizeof(struct kinfo_proc); - int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, (int)size, 1}; - int len = 6; -#elif defined(PSUTIL_NETBSD) - size_t size = sizeof(struct kinfo_proc2); - int mib[] = {CTL_KERN, KERN_PROC2, KERN_PROC_PID, pid, (int)size, 1}; - int len = 6; -#else -#error "unsupported BSD variant" -#endif - - if (pid < 0 || proc == NULL) - psutil_badargs("psutil_kinfo_proc"); - - if (sysctl(mib, len, proc, &size, NULL, 0) == -1) { - psutil_oserror_wsyscall("sysctl(kinfo_proc)"); - return -1; - } - - // sysctl stores 0 in size if the process doesn't exist. - if (size == 0) { - psutil_oserror_nsp("sysctl(kinfo_proc), size = 0"); - return -1; - } - - return 0; -} - - -// Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an -// int as arg and returns an array with cnt struct kinfo_file. -// Caller is responsible for freeing the returned pointer with free(). -#ifdef PSUTIL_HASNT_KINFO_GETFILE -struct kinfo_file * -kinfo_getfile(pid_t pid, int *cnt) { - if (pid < 0 || !cnt) { - psutil_badargs("kinfo_getfile"); - return NULL; - } - - int mib[6]; - size_t len; - struct kinfo_file *kf = NULL; - - mib[0] = CTL_KERN; - mib[1] = KERN_FILE; - mib[2] = KERN_FILE_BYPID; - mib[3] = pid; - mib[4] = sizeof(struct kinfo_file); - mib[5] = 0; - - if (psutil_sysctl_malloc(mib, 6, (char **)&kf, &len) != 0) { - return NULL; - } - - // Calculate number of entries and check for overflow - if (len / sizeof(struct kinfo_file) > INT_MAX) { - psutil_debug("exceeded INT_MAX"); - free(kf); - errno = EOVERFLOW; - return NULL; - } - - *cnt = (int)(len / sizeof(struct kinfo_file)); - return kf; -} -#endif // PSUTIL_HASNT_KINFO_GETFILE - - -int -is_zombie(size_t pid) { -#ifdef PSUTIL_NETBSD - struct kinfo_proc2 kp; -#else - struct kinfo_proc kp; -#endif - if (psutil_kinfo_proc(pid, &kp) == -1) { - errno = 0; - PyErr_Clear(); - return 0; - } - -#if defined(PSUTIL_FREEBSD) - return kp.ki_stat == SZOMB; -#elif defined(PSUTIL_OPENBSD) - // According to /usr/include/sys/proc.h SZOMB is unused. - // test_zombie_process() shows that SDEAD is the right - // equivalent. - return ((kp.p_stat == SZOMB) || (kp.p_stat == SDEAD)); -#else - return kp.p_stat == SZOMB; -#endif -} - - /* * Collect different info about a process in one shot and return * them as a big Python tuple. diff --git a/psutil/arch/bsd/proc_utils.c b/psutil/arch/bsd/proc_utils.c new file mode 100644 index 0000000000..b63f08e5f5 --- /dev/null +++ b/psutil/arch/bsd/proc_utils.c @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +// Fills a kinfo_proc or kinfo_proc2 struct based on process PID. +int +psutil_kinfo_proc(pid_t pid, void *proc) { +#if defined(PSUTIL_FREEBSD) + size_t size = sizeof(struct kinfo_proc); + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; + int len = 4; +#elif defined(PSUTIL_OPENBSD) + size_t size = sizeof(struct kinfo_proc); + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, (int)size, 1}; + int len = 6; +#elif defined(PSUTIL_NETBSD) + size_t size = sizeof(struct kinfo_proc2); + int mib[] = {CTL_KERN, KERN_PROC2, KERN_PROC_PID, pid, (int)size, 1}; + int len = 6; +#else +#error "unsupported BSD variant" +#endif + + if (pid < 0 || proc == NULL) + psutil_badargs("psutil_kinfo_proc"); + + if (sysctl(mib, len, proc, &size, NULL, 0) == -1) { + psutil_oserror_wsyscall("sysctl(kinfo_proc)"); + return -1; + } + + // sysctl stores 0 in size if the process doesn't exist. + if (size == 0) { + psutil_oserror_nsp("sysctl(kinfo_proc), size = 0"); + return -1; + } + + return 0; +} + + +// Mimic's FreeBSD kinfo_file call, taking a pid and a ptr to an +// int as arg and returns an array with cnt struct kinfo_file. +// Caller is responsible for freeing the returned pointer with free(). +#ifdef PSUTIL_HASNT_KINFO_GETFILE +struct kinfo_file * +kinfo_getfile(pid_t pid, int *cnt) { + if (pid < 0 || !cnt) { + psutil_badargs("kinfo_getfile"); + return NULL; + } + + int mib[6]; + size_t len; + struct kinfo_file *kf = NULL; + + mib[0] = CTL_KERN; + mib[1] = KERN_FILE; + mib[2] = KERN_FILE_BYPID; + mib[3] = pid; + mib[4] = sizeof(struct kinfo_file); + mib[5] = 0; + + if (psutil_sysctl_malloc(mib, 6, (char **)&kf, &len) != 0) { + return NULL; + } + + // Calculate number of entries and check for overflow + if (len / sizeof(struct kinfo_file) > INT_MAX) { + psutil_debug("exceeded INT_MAX"); + free(kf); + errno = EOVERFLOW; + return NULL; + } + + *cnt = (int)(len / sizeof(struct kinfo_file)); + return kf; +} +#endif // PSUTIL_HASNT_KINFO_GETFILE + + +int +is_zombie(size_t pid) { +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kp; +#else + struct kinfo_proc kp; +#endif + if (psutil_kinfo_proc(pid, &kp) == -1) { + errno = 0; + PyErr_Clear(); + return 0; + } + +#if defined(PSUTIL_FREEBSD) + return kp.ki_stat == SZOMB; +#elif defined(PSUTIL_OPENBSD) + // According to /usr/include/sys/proc.h SZOMB is unused. + // test_zombie_process() shows that SDEAD is the right + // equivalent. + return ((kp.p_stat == SZOMB) || (kp.p_stat == SDEAD)); +#else + return kp.p_stat == SZOMB; +#endif +} diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 4921bee97e..d36e26f2ca 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -5,6 +5,9 @@ */ #include +#include +#include +#include #include extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; @@ -13,6 +16,14 @@ int psutil_setup_osx(void); int _psutil_pids(pid_t **pids_array, int *pids_count); int is_zombie(size_t pid); +int psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp); +int psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax); +int psutil_proc_pidinfo( + pid_t pid, int flavor, uint64_t arg, void *pti, int size +); +int psutil_task_for_pid(pid_t pid, mach_port_t *task); +struct proc_fdinfo *psutil_proc_list_fds(pid_t pid, int *num_fds); + PyObject *psutil_boot_time(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 8f01dfbf17..44a0f1e8a8 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -31,250 +31,6 @@ #include "../../arch/all/init.h" -// ==================================================================== -// --- utils -// ==================================================================== - - -static int -psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { - int mib[4]; - size_t len; - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PID; - mib[3] = pid; - - if (pid < 0 || !kp) - return psutil_badargs("psutil_get_kinfo_proc"); - - len = sizeof(struct kinfo_proc); - - if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { - // raise an exception and throw errno as the error - psutil_oserror_wsyscall("sysctl"); - return -1; - } - - // sysctl succeeds but len is zero, happens when process has gone away - if (len == 0) { - psutil_oserror_nsp("sysctl(kinfo_proc), len == 0"); - return -1; - } - return 0; -} - - -// Return 1 if PID a zombie, else 0 (including on error). -int -is_zombie(size_t pid) { - struct kinfo_proc kp; - - if (psutil_get_kinfo_proc(pid, &kp) == -1) { - errno = 0; - PyErr_Clear(); - return 0; - } - return kp.kp_proc.p_stat == SZOMB; -} - - -// Read process argument space. -static int -psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { - int mib[3]; - - mib[0] = CTL_KERN; - mib[1] = KERN_PROCARGS2; - mib[2] = pid; - - if (pid < 0 || !procargs || !argmax || *argmax == 0) - return psutil_badargs("psutil_sysctl_procargs"); - - if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { - if (psutil_pid_exists(pid) == 0) { - psutil_oserror_nsp("psutil_pid_exists -> 0"); - return -1; - } - - if (is_zombie(pid) == 1) { - PyErr_SetString(ZombieProcessError, ""); - return -1; - } - - if (errno == EINVAL) { - psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to AD"); - psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> EINVAL"); - return -1; - } - - if (errno == EIO) { - psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); - psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> EIO"); - return -1; - } - psutil_oserror_wsyscall("sysctl(KERN_PROCARGS2)"); - return -1; - } - return 0; -} - - -/* - * A wrapper around proc_pidinfo(). - * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c - * Returns 0 on failure. - */ -static int -psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { - int ret; - - if (pid < 0 || !pti || size <= 0) - return psutil_badargs("psutil_proc_pidinfo"); - - errno = 0; - ret = proc_pidinfo(pid, flavor, arg, pti, size); - if (ret <= 0) { - psutil_raise_for_pid(pid, "proc_pidinfo()"); - return -1; - } - - // check for truncated return size - if (ret < size) { - psutil_raise_for_pid( - pid, "proc_pidinfo() returned less data than requested buffer size" - ); - return -1; - } - - return 0; -} - - -/* - * A wrapper around task_for_pid() which sucks big time: - * - it's not documented - * - errno is set only sometimes - * - sometimes errno is ENOENT (?!?) - * - for PIDs != getpid() or PIDs which are not members of the procmod - * it requires root - * As such we can only guess what the heck went wrong and fail either - * with NoSuchProcess or give up with AccessDenied. - * References: - * https://github.com/giampaolo/psutil/issues/1181 - * https://github.com/giampaolo/psutil/issues/1209 - * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 - */ -static int -psutil_task_for_pid(pid_t pid, mach_port_t *task) { - kern_return_t err; - - if (pid < 0 || !task) - return psutil_badargs("psutil_task_for_pid"); - - err = task_for_pid(mach_task_self(), pid, task); - if (err != KERN_SUCCESS) { - if (psutil_pid_exists(pid) == 0) { - psutil_oserror_nsp("task_for_pid"); - } - else if (is_zombie(pid) == 1) { - PyErr_SetString( - ZombieProcessError, "task_for_pid -> psutil_is_zombie -> 1" - ); - } - else { - psutil_debug( - "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " - "setting EACCES", - (long)pid, - err, - errno, - mach_error_string(err) - ); - psutil_oserror_ad("task_for_pid"); - } - return -1; - } - - return 0; -} - - -/* - * A wrapper around proc_pidinfo(PROC_PIDLISTFDS), which dynamically sets - * the buffer size. - */ -static struct proc_fdinfo * -psutil_proc_list_fds(pid_t pid, int *num_fds) { - int ret; - int fds_size = 0; - int max_size = 24 * 1024 * 1024; // 24M - struct proc_fdinfo *fds_pointer = NULL; - - if (pid < 0 || num_fds == NULL) { - psutil_badargs("psutil_proc_list_fds"); - return NULL; - } - - errno = 0; - ret = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); - if (ret <= 0) { - psutil_raise_for_pid(pid, "proc_pidinfo(PROC_PIDLISTFDS) 1/2"); - goto error; - } - - while (1) { - if (ret > fds_size) { - while (ret > fds_size) { - fds_size += PROC_PIDLISTFD_SIZE * 32; - if (fds_size > max_size) { - psutil_runtime_error("prevent malloc() to allocate > 24M"); - goto error; - } - } - - if (fds_pointer != NULL) { - free(fds_pointer); - } - fds_pointer = malloc(fds_size); - - if (fds_pointer == NULL) { - PyErr_NoMemory(); - goto error; - } - } - - errno = 0; - ret = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds_pointer, fds_size); - if (ret <= 0) { - psutil_raise_for_pid(pid, "proc_pidinfo(PROC_PIDLISTFDS) 2/2"); - goto error; - } - - if (ret + (int)PROC_PIDLISTFD_SIZE >= fds_size) { - psutil_debug("PROC_PIDLISTFDS: make room for 1 extra fd"); - ret = fds_size + (int)PROC_PIDLISTFD_SIZE; - continue; - } - - break; - } - - *num_fds = (ret / (int)PROC_PIDLISTFD_SIZE); - return fds_pointer; - -error: - if (fds_pointer != NULL) - free(fds_pointer); - return NULL; -} - - -// ==================================================================== -// --- Python APIs -// ==================================================================== - - /* * Return multiple process info as a Python tuple in one shot by * using sysctl() and filling up a kinfo_proc struct. diff --git a/psutil/arch/osx/proc_utils.c b/psutil/arch/osx/proc_utils.c new file mode 100644 index 0000000000..8cb49ec231 --- /dev/null +++ b/psutil/arch/osx/proc_utils.c @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2009, Jay Loden, 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +int +psutil_get_kinfo_proc(pid_t pid, struct kinfo_proc *kp) { + int mib[4]; + size_t len; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = pid; + + if (pid < 0 || !kp) + return psutil_badargs("psutil_get_kinfo_proc"); + + len = sizeof(struct kinfo_proc); + + if (sysctl(mib, 4, kp, &len, NULL, 0) == -1) { + // raise an exception and throw errno as the error + psutil_oserror_wsyscall("sysctl"); + return -1; + } + + // sysctl succeeds but len is zero, happens when process has gone away + if (len == 0) { + psutil_oserror_nsp("sysctl(kinfo_proc), len == 0"); + return -1; + } + return 0; +} + + +// Return 1 if PID a zombie, else 0 (including on error). +int +is_zombie(size_t pid) { + struct kinfo_proc kp; + + if (psutil_get_kinfo_proc(pid, &kp) == -1) { + errno = 0; + PyErr_Clear(); + return 0; + } + return kp.kp_proc.p_stat == SZOMB; +} + + +// Read process argument space. +int +psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { + int mib[3]; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROCARGS2; + mib[2] = pid; + + if (pid < 0 || !procargs || !argmax || *argmax == 0) + return psutil_badargs("psutil_sysctl_procargs"); + + if (sysctl(mib, 3, procargs, argmax, NULL, 0) < 0) { + if (psutil_pid_exists(pid) == 0) { + psutil_oserror_nsp("psutil_pid_exists -> 0"); + return -1; + } + + if (is_zombie(pid) == 1) { + PyErr_SetString(ZombieProcessError, ""); + return -1; + } + + if (errno == EINVAL) { + psutil_debug("sysctl(KERN_PROCARGS2) -> EINVAL translated to AD"); + psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> EINVAL"); + return -1; + } + + if (errno == EIO) { + psutil_debug("sysctl(KERN_PROCARGS2) -> EIO translated to AD"); + psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> EIO"); + return -1; + } + psutil_oserror_wsyscall("sysctl(KERN_PROCARGS2)"); + return -1; + } + return 0; +} + + +/* + * A wrapper around proc_pidinfo(). + * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c + * Returns 0 on failure. + */ +int +psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { + int ret; + + if (pid < 0 || !pti || size <= 0) + return psutil_badargs("psutil_proc_pidinfo"); + + errno = 0; + ret = proc_pidinfo(pid, flavor, arg, pti, size); + if (ret <= 0) { + psutil_raise_for_pid(pid, "proc_pidinfo()"); + return -1; + } + + // check for truncated return size + if (ret < size) { + psutil_raise_for_pid( + pid, "proc_pidinfo() returned less data than requested buffer size" + ); + return -1; + } + + return 0; +} + + +/* + * A wrapper around task_for_pid() which sucks big time: + * - it's not documented + * - errno is set only sometimes + * - sometimes errno is ENOENT (?!?) + * - for PIDs != getpid() or PIDs which are not members of the procmod + * it requires root + * As such we can only guess what the heck went wrong and fail either + * with NoSuchProcess or give up with AccessDenied. + * References: + * https://github.com/giampaolo/psutil/issues/1181 + * https://github.com/giampaolo/psutil/issues/1209 + * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 + */ +int +psutil_task_for_pid(pid_t pid, mach_port_t *task) { + kern_return_t err; + + if (pid < 0 || !task) + return psutil_badargs("psutil_task_for_pid"); + + err = task_for_pid(mach_task_self(), pid, task); + if (err != KERN_SUCCESS) { + if (psutil_pid_exists(pid) == 0) { + psutil_oserror_nsp("task_for_pid"); + } + else if (is_zombie(pid) == 1) { + PyErr_SetString( + ZombieProcessError, "task_for_pid -> psutil_is_zombie -> 1" + ); + } + else { + psutil_debug( + "task_for_pid() failed (pid=%ld, err=%i, errno=%i, msg='%s'); " + "setting EACCES", + (long)pid, + err, + errno, + mach_error_string(err) + ); + psutil_oserror_ad("task_for_pid"); + } + return -1; + } + + return 0; +} + + +/* + * A wrapper around proc_pidinfo(PROC_PIDLISTFDS), which dynamically sets + * the buffer size. + */ +struct proc_fdinfo * +psutil_proc_list_fds(pid_t pid, int *num_fds) { + int ret; + int fds_size = 0; + int max_size = 24 * 1024 * 1024; // 24M + struct proc_fdinfo *fds_pointer = NULL; + + if (pid < 0 || num_fds == NULL) { + psutil_badargs("psutil_proc_list_fds"); + return NULL; + } + + errno = 0; + ret = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); + if (ret <= 0) { + psutil_raise_for_pid(pid, "proc_pidinfo(PROC_PIDLISTFDS) 1/2"); + goto error; + } + + while (1) { + if (ret > fds_size) { + while (ret > fds_size) { + fds_size += PROC_PIDLISTFD_SIZE * 32; + if (fds_size > max_size) { + psutil_runtime_error("prevent malloc() to allocate > 24M"); + goto error; + } + } + + if (fds_pointer != NULL) { + free(fds_pointer); + } + fds_pointer = malloc(fds_size); + + if (fds_pointer == NULL) { + PyErr_NoMemory(); + goto error; + } + } + + errno = 0; + ret = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds_pointer, fds_size); + if (ret <= 0) { + psutil_raise_for_pid(pid, "proc_pidinfo(PROC_PIDLISTFDS) 2/2"); + goto error; + } + + if (ret + (int)PROC_PIDLISTFD_SIZE >= fds_size) { + psutil_debug("PROC_PIDLISTFDS: make room for 1 extra fd"); + ret = fds_size + (int)PROC_PIDLISTFD_SIZE; + continue; + } + + break; + } + + *num_fds = (ret / (int)PROC_PIDLISTFD_SIZE); + return fds_pointer; + +error: + if (fds_pointer != NULL) + free(fds_pointer); + return NULL; +} From f35a1a22d195bf74cf713e4b2a2936910cb1c719 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 19:30:26 +0100 Subject: [PATCH 1421/1714] Fix disk usage reporting on Windows for large drives - Correct psutil_disk_usage() to use "KK" in Py_BuildValue, ensuring ULARGE_INTEGER.QuadPart values are returned correctly. - Previously using "LL" could truncate values on 32-bit platforms, potentially reporting incorrect total/free space for drives larger than 4GB. --- psutil/arch/windows/disk.c | 17 ++++++++--------- scripts/internal/winmake.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 0641d12e47..9026e4ffdb 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -38,15 +38,13 @@ psutil_get_drive_type(int type) { } -/* - * Return path's disk total and free as a Python tuple. - */ +// Return path's disk total and free as a Python tuple. PyObject * psutil_disk_usage(PyObject *self, PyObject *args) { - BOOL retval; - ULARGE_INTEGER _, total, free; PyObject *py_path; - wchar_t *path; + wchar_t *path = NULL; + ULARGE_INTEGER total, free, avail; + BOOL retval; if (!PyArg_ParseTuple(args, "U", &py_path)) { return NULL; @@ -58,17 +56,18 @@ psutil_disk_usage(PyObject *self, PyObject *args) { } Py_BEGIN_ALLOW_THREADS - retval = GetDiskFreeSpaceExW(path, &_, &total, &free); + retval = GetDiskFreeSpaceExW(path, &avail, &total, &free); Py_END_ALLOW_THREADS PyMem_Free(path); - if (retval == 0) + if (retval == 0) { return PyErr_SetExcFromWindowsErrWithFilenameObject( PyExc_OSError, 0, py_path ); + } - return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); + return Py_BuildValue("(KK)", total.QuadPart, free.QuadPart); } diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 5d4cee4868..acfe114c3c 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -97,7 +97,7 @@ def safe_remove(path): except FileNotFoundError: pass except PermissionError as err: - print(err) + print(red(err)) else: safe_print(f"rm {path}") From 96085b501278dec99e9294848285a18643bf8e86 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 19:32:18 +0100 Subject: [PATCH 1422/1714] Calculate "used" disk space in C, not in python --- psutil/_pswindows.py | 3 +-- psutil/arch/windows/disk.c | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index c6c8fffa4b..7d343f26bb 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -265,8 +265,7 @@ def disk_usage(path): # XXX: do we want to use "strict"? Probably yes, in order # to fail immediately. After all we are accepting input here... path = path.decode(ENCODING, errors="strict") - total, free = cext.disk_usage(path) - used = total - free + total, used, free = cext.disk_usage(path) percent = usage_percent(used, total, round_=1) return _common.sdiskusage(total, used, free, percent) diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 9026e4ffdb..ce0e3bd689 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -38,13 +38,14 @@ psutil_get_drive_type(int type) { } -// Return path's disk total and free as a Python tuple. +// Return path's disk total, used, and free as a Python tuple. PyObject * psutil_disk_usage(PyObject *self, PyObject *args) { PyObject *py_path; wchar_t *path = NULL; ULARGE_INTEGER total, free, avail; BOOL retval; + ULONGLONG used; if (!PyArg_ParseTuple(args, "U", &py_path)) { return NULL; @@ -67,7 +68,8 @@ psutil_disk_usage(PyObject *self, PyObject *args) { ); } - return Py_BuildValue("(KK)", total.QuadPart, free.QuadPart); + used = total.QuadPart - free.QuadPart; + return Py_BuildValue("(KKK)", total.QuadPart, used, free.QuadPart); } From ac4a1f16a490d996db6fdd35f9570ab0fc1390f5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 19:34:15 +0100 Subject: [PATCH 1423/1714] Fix BSD compilation err --- psutil/arch/bsd/proc.c | 2 ++ psutil/arch/windows/disk.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 1f9f86ce76..aa946b20f9 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -11,8 +11,10 @@ #include #include #include +#ifdef PSUTIL_FREEBSD #include #include +#endif #include "../../arch/all/init.h" diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index ce0e3bd689..daf56687d8 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -38,7 +38,7 @@ psutil_get_drive_type(int type) { } -// Return path's disk total, used, and free as a Python tuple. +// Return path's disk total, used, and free space. PyObject * psutil_disk_usage(PyObject *self, PyObject *args) { PyObject *py_path; From 2f4ebb5992011cc6ccedf220b18d6c1f22cc236e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 19:37:17 +0100 Subject: [PATCH 1424/1714] Update HISTORY.rst --- HISTORY.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index a074050dc1..25da2b85e7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,12 @@ XXXX-XX-XX - 2672_, [macOS], [BSD]: increase the chances to recognize zombie processes and raise the appropriate exception (`ZombieProcess`_). +**Bug fixes** + +- 2674_, [Windows]: `disk_usage()`_ could truncate values on 32-bit platforms, + potentially reporting incorrect total/free/used space for drives larger than + 4GB. + 7.1.2 ===== From c185f74b2b0866c4ba64abe334e9fc94e83fc4f5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 19:37:25 +0100 Subject: [PATCH 1425/1714] [Windows] fix incorrect `disk_usage()` on 32-bit Python for large drives (#2674) * Fix disk usage reporting on Windows for large drives - Correct psutil_disk_usage() to use "KK" in Py_BuildValue, ensuring ULARGE_INTEGER.QuadPart values are returned correctly. - Previously using "LL" could truncate values on 32-bit platforms, potentially reporting incorrect total/free space for drives larger than 4GB. * Calculate "used" disk space in C, not in python * Fix BSD compilation err --- HISTORY.rst | 6 ++++++ psutil/_pswindows.py | 3 +-- psutil/arch/bsd/proc.c | 2 ++ psutil/arch/windows/disk.c | 19 ++++++++++--------- scripts/internal/winmake.py | 2 +- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a074050dc1..25da2b85e7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,12 @@ XXXX-XX-XX - 2672_, [macOS], [BSD]: increase the chances to recognize zombie processes and raise the appropriate exception (`ZombieProcess`_). +**Bug fixes** + +- 2674_, [Windows]: `disk_usage()`_ could truncate values on 32-bit platforms, + potentially reporting incorrect total/free/used space for drives larger than + 4GB. + 7.1.2 ===== diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index c6c8fffa4b..7d343f26bb 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -265,8 +265,7 @@ def disk_usage(path): # XXX: do we want to use "strict"? Probably yes, in order # to fail immediately. After all we are accepting input here... path = path.decode(ENCODING, errors="strict") - total, free = cext.disk_usage(path) - used = total - free + total, used, free = cext.disk_usage(path) percent = usage_percent(used, total, round_=1) return _common.sdiskusage(total, used, free, percent) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 1f9f86ce76..aa946b20f9 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -11,8 +11,10 @@ #include #include #include +#ifdef PSUTIL_FREEBSD #include #include +#endif #include "../../arch/all/init.h" diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 0641d12e47..daf56687d8 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -38,15 +38,14 @@ psutil_get_drive_type(int type) { } -/* - * Return path's disk total and free as a Python tuple. - */ +// Return path's disk total, used, and free space. PyObject * psutil_disk_usage(PyObject *self, PyObject *args) { - BOOL retval; - ULARGE_INTEGER _, total, free; PyObject *py_path; - wchar_t *path; + wchar_t *path = NULL; + ULARGE_INTEGER total, free, avail; + BOOL retval; + ULONGLONG used; if (!PyArg_ParseTuple(args, "U", &py_path)) { return NULL; @@ -58,17 +57,19 @@ psutil_disk_usage(PyObject *self, PyObject *args) { } Py_BEGIN_ALLOW_THREADS - retval = GetDiskFreeSpaceExW(path, &_, &total, &free); + retval = GetDiskFreeSpaceExW(path, &avail, &total, &free); Py_END_ALLOW_THREADS PyMem_Free(path); - if (retval == 0) + if (retval == 0) { return PyErr_SetExcFromWindowsErrWithFilenameObject( PyExc_OSError, 0, py_path ); + } - return Py_BuildValue("(LL)", total.QuadPart, free.QuadPart); + used = total.QuadPart - free.QuadPart; + return Py_BuildValue("(KKK)", total.QuadPart, used, free.QuadPart); } diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 5d4cee4868..acfe114c3c 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -97,7 +97,7 @@ def safe_remove(path): except FileNotFoundError: pass except PermissionError as err: - print(err) + print(red(err)) else: safe_print(f"rm {path}") From 143844a7176acb6b6518d278049c1aece3ac3e23 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 29 Oct 2025 20:20:10 +0100 Subject: [PATCH 1426/1714] Windows / net_io_counters improvements - replace malloc + free with a local ifRow variable on the stack. - zeroes out ifRow before use. - limit string length to IF_MAX_STRING_SIZE instead of wcslen() (safer) --- psutil/arch/windows/net.c | 52 ++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 5a68fc7e10..e550c43392 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -52,7 +52,7 @@ psutil_get_nic_addresses(void) { PyObject * psutil_net_io_counters(PyObject *self, PyObject *args) { DWORD dwRetVal = 0; - MIB_IF_ROW2 *pIfRow = NULL; + MIB_IF_ROW2 ifRow; PIP_ADAPTER_ADDRESSES pAddresses = NULL; PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; PyObject *py_retdict = PyDict_New(); @@ -64,54 +64,52 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { pAddresses = psutil_get_nic_addresses(); if (pAddresses == NULL) goto error; + pCurrAddresses = pAddresses; while (pCurrAddresses) { - py_nic_name = NULL; py_nic_info = NULL; + py_nic_name = NULL; - pIfRow = (MIB_IF_ROW2 *)malloc(sizeof(MIB_IF_ROW2)); - if (pIfRow == NULL) { - PyErr_NoMemory(); - goto error; - } + SecureZeroMemory(&ifRow, sizeof(ifRow)); + ifRow.InterfaceIndex = pCurrAddresses->IfIndex; - SecureZeroMemory((PVOID)pIfRow, sizeof(MIB_IF_ROW2)); - pIfRow->InterfaceIndex = pCurrAddresses->IfIndex; - dwRetVal = GetIfEntry2(pIfRow); + dwRetVal = GetIfEntry2(&ifRow); if (dwRetVal != NO_ERROR) { psutil_runtime_error( - "GetIfEntry() or GetIfEntry2() syscalls failed." + "GetIfEntry2() syscall failed for interface %lu", + (unsigned long)ifRow.InterfaceIndex ); goto error; } py_nic_info = Py_BuildValue( "(KKKKKKKK)", - pIfRow->OutOctets, - pIfRow->InOctets, - (pIfRow->OutUcastPkts + pIfRow->OutNUcastPkts), - (pIfRow->InUcastPkts + pIfRow->InNUcastPkts), - pIfRow->InErrors, - pIfRow->OutErrors, - pIfRow->InDiscards, - pIfRow->OutDiscards + ifRow.OutOctets, + ifRow.InOctets, + ifRow.OutUcastPkts + ifRow.OutNUcastPkts, + ifRow.InUcastPkts + ifRow.InNUcastPkts, + ifRow.InErrors, + ifRow.OutErrors, + ifRow.InDiscards, + ifRow.OutDiscards ); if (!py_nic_info) goto error; py_nic_name = PyUnicode_FromWideChar( - pCurrAddresses->FriendlyName, wcslen(pCurrAddresses->FriendlyName) + pCurrAddresses->FriendlyName, + wcsnlen(pCurrAddresses->FriendlyName, IF_MAX_STRING_SIZE) ); - - if (py_nic_name == NULL) + if (!py_nic_name) goto error; + if (PyDict_SetItem(py_retdict, py_nic_name, py_nic_info)) goto error; - Py_CLEAR(py_nic_name); + Py_CLEAR(py_nic_info); + Py_CLEAR(py_nic_name); - free(pIfRow); pCurrAddresses = pCurrAddresses->Next; } @@ -119,13 +117,11 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { return py_retdict; error: - Py_XDECREF(py_nic_name); Py_XDECREF(py_nic_info); + Py_XDECREF(py_nic_name); Py_DECREF(py_retdict); - if (pAddresses != NULL) + if (pAddresses) free(pAddresses); - if (pIfRow != NULL) - free(pIfRow); return NULL; } From df607b1885fddcf0070fee1c1e90f3c5afcc84dd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Oct 2025 01:10:56 +0100 Subject: [PATCH 1427/1714] Silence output of some tests + fix NetBSD build --- psutil/arch/bsd/proc_utils.c | 4 +++- psutil/tests/test_scripts.py | 14 ++++++++------ scripts/procsmem.py | 5 +---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/psutil/arch/bsd/proc_utils.c b/psutil/arch/bsd/proc_utils.c index b63f08e5f5..969d2a38bb 100644 --- a/psutil/arch/bsd/proc_utils.c +++ b/psutil/arch/bsd/proc_utils.c @@ -11,10 +11,12 @@ #include #include #include -#include #include #include #include +#ifdef PSUTIL_FREEBSD +#include +#endif #include "../../arch/all/init.h" diff --git a/psutil/tests/test_scripts.py b/psutil/tests/test_scripts.py index 40fc591519..6ef78b6303 100755 --- a/psutil/tests/test_scripts.py +++ b/psutil/tests/test_scripts.py @@ -14,6 +14,7 @@ import pytest +from psutil import LINUX from psutil import POSIX from psutil import WINDOWS from psutil.tests import CI_TESTING @@ -46,14 +47,13 @@ ) class TestExampleScripts(PsutilTestCase): @staticmethod - def assert_stdout(exe, *args, **kwargs): - kwargs.setdefault("env", PYTHON_EXE_ENV) + def assert_stdout(exe, *args): + env = PYTHON_EXE_ENV.copy() + env.pop("PSUTIL_DEBUG") # avoid spamming to stderr exe = os.path.join(SCRIPTS_DIR, exe) - cmd = [PYTHON_EXE, exe] - for arg in args: - cmd.append(arg) + cmd = [PYTHON_EXE, exe, *args] try: - out = sh(cmd, **kwargs).strip() + out = sh(cmd, env=env).strip() except RuntimeError as err: if 'AccessDenied' in str(err): return str(err) @@ -195,6 +195,8 @@ def test_syntax_all(self): data = f.read() ast.parse(data) + # don't care about other platforms, this is really just for myself + @pytest.mark.skipif(not LINUX, reason="not on LINUX") @pytest.mark.skipif(CI_TESTING, reason="not on CI") def test_import_all(self): for path in self.ls(): diff --git a/scripts/procsmem.py b/scripts/procsmem.py index cb7431db11..0b5f5c1fc8 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -95,10 +95,7 @@ def main(): ) print(line) if ad_pids: - print( - f"warning: access denied for {len(ad_pids)} pids", - file=sys.stderr, - ) + print(f"warning: access denied for {len(ad_pids)} pids") if __name__ == '__main__': From 1ba3cf53a89d0e097848933a4e751e7cbce40be7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Oct 2025 03:05:47 +0100 Subject: [PATCH 1428/1714] Fix #2675 / macOS: convert process status() to a meaningful value --- HISTORY.rst | 2 ++ psutil/arch/osx/proc.c | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 25da2b85e7..87f18a8ff0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,8 @@ XXXX-XX-XX - 2674_, [Windows]: `disk_usage()`_ could truncate values on 32-bit platforms, potentially reporting incorrect total/free/used space for drives larger than 4GB. +- 2675_, [macOS]: `Process.status()`_ incorrectly returns "running" for 99% + of the processes. 7.1.2 ===== diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 44a0f1e8a8..758f5ed15d 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -31,6 +31,34 @@ #include "../../arch/all/init.h" +// macOS is apparently the only UNIX where the process "base" status +// (running, idle, etc.) is unreliable and must be guessed from flags: +// https://github.com/giampaolo/psutil/issues/2675 +static int +convert_status(struct extern_proc *p, struct eproc *e) { + int flag = p->p_flag; + int eflag = e->e_flag; + + // zombies and stopped + if (p->p_stat == SZOMB) + return SZOMB; + if (p->p_stat == SSTOP) + return SSTOP; + + if (flag & P_SYSTEM) + return SIDL; // system idle + if (flag & P_WEXIT) + return SIDL; // waiting to exit + if (flag & P_PPWAIT) + return SIDL; // parent waiting + if (eflag & EPROC_SLEADER) + return SSLEEP; // session leader treated as sleeping + + // Default: 99% is SRUN (running) + return p->p_stat; +} + + /* * Return multiple process info as a Python tuple in one shot by * using sysctl() and filling up a kinfo_proc struct. @@ -42,6 +70,7 @@ PyObject * psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { pid_t pid; + int status; struct kinfo_proc kp; PyObject *py_name = NULL; PyObject *py_retlist = NULL; @@ -60,6 +89,8 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { py_name = Py_None; } + status = convert_status(&kp.kp_proc, &kp.kp_eproc); + py_retlist = Py_BuildValue( _Py_PARSE_PID "llllllldiO", kp.kp_eproc.e_ppid, // (pid_t) ppid @@ -71,7 +102,7 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { (long)kp.kp_eproc.e_pcred.p_svgid, // (long) saved gid (long long)kp.kp_eproc.e_tdev, // (long long) tty nr PSUTIL_TV2DOUBLE(kp.kp_proc.p_starttime), // (double) create time - (int)kp.kp_proc.p_stat, // (int) status + status, // (int) status py_name // (pystr) name ); From 262977f655037071ce0d0d884f8d8c7ec8f3ca16 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Oct 2025 14:47:41 +0100 Subject: [PATCH 1429/1714] Replace unsafe `sprintf/snprintf/sprintf_s` calls with `str_format()` (#2676) Fixes #2669. Introduce a new cross-platform string formatting utility str_format() Wraps vsnprintf on POSIX and _vsnprintf_s on Windows. Always null-terminates buffers and safely handles truncation. Updated Windows code to remove _s variants in favor of str_format(). --- MANIFEST.in | 1 + psutil/_psutil_aix.c | 10 +++---- psutil/arch/all/errors.c | 19 +++++++++++--- psutil/arch/all/init.h | 1 + psutil/arch/all/str.c | 45 ++++++++++++++++++++++++++++++++ psutil/arch/bsd/init.c | 4 ++- psutil/arch/bsd/proc.c | 15 ++++++----- psutil/arch/freebsd/cpu.c | 4 +-- psutil/arch/freebsd/disk.c | 2 +- psutil/arch/freebsd/proc.c | 3 ++- psutil/arch/freebsd/proc_socks.c | 2 +- psutil/arch/freebsd/sensors.c | 4 +-- psutil/arch/freebsd/sys_socks.c | 2 +- psutil/arch/netbsd/proc.c | 13 ++++----- psutil/arch/posix/net.c | 2 +- psutil/arch/posix/sysctl.c | 12 ++++----- psutil/arch/sunos/environ.c | 2 +- psutil/arch/sunos/proc.c | 27 ++++++++++--------- psutil/arch/windows/disk.c | 4 +-- psutil/arch/windows/init.c | 2 +- psutil/arch/windows/net.c | 2 +- psutil/arch/windows/proc_info.c | 8 +++--- 22 files changed, 125 insertions(+), 59 deletions(-) create mode 100644 psutil/arch/all/str.c diff --git a/MANIFEST.in b/MANIFEST.in index 5f8e39cf19..450201d440 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -51,6 +51,7 @@ include psutil/arch/all/errors.c include psutil/arch/all/init.c include psutil/arch/all/init.h include psutil/arch/all/pids.c +include psutil/arch/all/str.c include psutil/arch/bsd/cpu.c include psutil/arch/bsd/disk.c include psutil/arch/bsd/init.c diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index ab62918080..8ac4908a5b 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -101,7 +101,7 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/psinfo", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; @@ -116,7 +116,7 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { status.pr_stat = SACTIVE; } else { - sprintf(path, "%s/%i/status", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/status", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&status, sizeof(status))) return NULL; } @@ -147,7 +147,7 @@ psutil_proc_name(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/psinfo", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; @@ -399,7 +399,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/status", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/status", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; // results are more precise than os.times() @@ -425,7 +425,7 @@ psutil_proc_cred(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/cred", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/cred", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return Py_BuildValue( diff --git a/psutil/arch/all/errors.c b/psutil/arch/all/errors.c index bef0bf6131..039b8f98a2 100644 --- a/psutil/arch/all/errors.c +++ b/psutil/arch/all/errors.c @@ -11,6 +11,8 @@ #include #endif +#include "init.h" + #define MSG_SIZE 512 @@ -35,11 +37,13 @@ psutil_oserror_wsyscall(const char *syscall) { #ifdef PSUTIL_WINDOWS DWORD err = GetLastError(); - sprintf(msg, "(originated from %s)", syscall); + str_format(msg, sizeof(msg), "(originated from %s)", syscall); PyErr_SetFromWindowsErrWithFilename(err, msg); #else PyObject *exc; - sprintf(msg, "%s (originated from %s)", strerror(errno), syscall); + str_format( + msg, sizeof(msg), "%s (originated from %s)", strerror(errno), syscall + ); exc = PyObject_CallFunction(PyExc_OSError, "(is)", errno, msg); PyErr_SetObject(PyExc_OSError, exc); Py_XDECREF(exc); @@ -54,7 +58,9 @@ psutil_oserror_nsp(const char *syscall) { PyObject *exc; char msg[MSG_SIZE]; - sprintf(msg, "force no such process (originated from %s)", syscall); + str_format( + msg, sizeof(msg), "force no such process (originated from %s)", syscall + ); exc = PyObject_CallFunction(PyExc_OSError, "(is)", ESRCH, msg); PyErr_SetObject(PyExc_OSError, exc); Py_XDECREF(exc); @@ -68,7 +74,12 @@ psutil_oserror_ad(const char *syscall) { PyObject *exc; char msg[MSG_SIZE]; - sprintf(msg, "force permission denied (originated from %s)", syscall); + str_format( + msg, + sizeof(msg), + "force permission denied (originated from %s)", + syscall + ); exc = PyObject_CallFunction(PyExc_OSError, "(is)", EACCES, msg); PyErr_SetObject(PyExc_OSError, exc); Py_XDECREF(exc); diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 9066237a88..de2060490f 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -129,6 +129,7 @@ PyObject *psutil_oserror_nsp(const char *msg); PyObject *psutil_oserror_wsyscall(const char *syscall); PyObject *psutil_runtime_error(const char *msg, ...); +int str_format(char *buf, size_t size, const char *fmt, ...); int psutil_badargs(const char *funcname); int psutil_setup(void); diff --git a/psutil/arch/all/str.c b/psutil/arch/all/str.c new file mode 100644 index 0000000000..8dce9f8ae1 --- /dev/null +++ b/psutil/arch/all/str.c @@ -0,0 +1,45 @@ +/* + * 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. + */ + +// String utilities. + +#include +#include +#include + +#include "init.h" + + +// Safely formats a string into a buffer. Writes a printf-style +// formatted string into `buf` of size `size`, always null-terminating +// if size > 0. Returns the number of characters written (excluding the +// null terminator) on success, or -1 if the buffer is too small or an +// error occurs. +int +str_format(char *buf, size_t size, const char *fmt, ...) { + va_list args; + int ret; + + if (size == 0) { + psutil_debug("str_format: invalid arg 'size' = 0"); + return -1; + } + + va_start(args, fmt); +#if defined(PSUTIL_WINDOWS) + ret = _vsnprintf_s(buf, size, _TRUNCATE, fmt, args); +#else + ret = vsnprintf(buf, size, fmt, args); +#endif + va_end(args); + + if (ret < 0 || (size_t)ret >= size) { + psutil_debug("str_format: error in format '%s'", fmt); + buf[size - 1] = '\0'; + return -1; + } + return ret; +} diff --git a/psutil/arch/bsd/init.c b/psutil/arch/bsd/init.c index ad4dc1f1f7..d3f00f5f20 100644 --- a/psutil/arch/bsd/init.c +++ b/psutil/arch/bsd/init.c @@ -15,7 +15,9 @@ void convert_kvm_err(const char *syscall, char *errbuf) { char fullmsg[512]; - sprintf(fullmsg, "(originated from %s: %s)", syscall, errbuf); + str_format( + fullmsg, sizeof(fullmsg), "(originated from %s: %s)", syscall, errbuf + ); if (strstr(errbuf, "Permission denied") != NULL) psutil_oserror_ad(fullmsg); else if (strstr(errbuf, "Operation not permitted") != NULL) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index aa946b20f9..893b4c92c9 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -50,9 +50,9 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { // Process #ifdef PSUTIL_FREEBSD - snprintf(name_buf, sizeof(name_buf), "%s", kp.ki_comm); + str_format(name_buf, sizeof(name_buf), "%s", kp.ki_comm); #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - snprintf(name_buf, sizeof(name_buf), "%s", kp.p_comm); + str_format(name_buf, sizeof(name_buf), "%s", kp.p_comm); #endif py_name = PyUnicode_DecodeFSDefault(name_buf); if (!py_name) { @@ -215,9 +215,9 @@ psutil_proc_name(PyObject *self, PyObject *args) { return NULL; #ifdef PSUTIL_FREEBSD - sprintf(str, "%s", kp.ki_comm); + str_format(str, sizeof(str), "%s", kp.ki_comm); #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - sprintf(str, "%s", kp.p_comm); + str_format(str, sizeof(str), "%s", kp.p_comm); #endif return PyUnicode_DecodeFSDefault(str); } @@ -312,8 +312,9 @@ psutil_proc_environ(PyObject *self, PyObject *args) { // failure for certain processes ( e.g. try // "sudo procstat -e ".) // Map the error condition to 'AccessDenied'. - sprintf( + str_format( errbuf, + sizeof(errbuf), "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM", pid, p->ki_uid @@ -322,7 +323,9 @@ psutil_proc_environ(PyObject *self, PyObject *args) { break; #endif default: - sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); + str_format( + errbuf, sizeof(errbuf), "kvm_getenvv(pid=%ld)", pid + ); psutil_oserror_wsyscall(errbuf); break; } diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index c936abdedc..bf1730f13e 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -157,13 +157,13 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ size = sizeof(current); - sprintf(sensor, "dev.cpu.%d.freq", core); + str_format(sensor, sizeof(sensor), "dev.cpu.%d.freq", core); if (psutil_sysctlbyname(sensor, ¤t, size) != 0) goto error; // In case of failure, an empty string is returned. size = sizeof(available_freq_levels); - sprintf(sensor, "dev.cpu.%d.freq_levels", core); + str_format(sensor, sizeof(sensor), "dev.cpu.%d.freq_levels", core); if (psutil_sysctlbyname(sensor, &available_freq_levels, size) != 0) psutil_debug("cpu freq levels failed (ignored)"); diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c index 6e15cd23f9..8a46d74f54 100644 --- a/psutil/arch/freebsd/disk.c +++ b/psutil/arch/freebsd/disk.c @@ -48,7 +48,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { struct devstat current; char disk_name[128]; current = stats.dinfo->devices[i]; - snprintf( + str_format( disk_name, sizeof(disk_name), "%s%d", diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index ec06ada2a3..ebab0ad266 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -320,8 +320,9 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { kve = &freep[i]; addr[0] = '\0'; perms[0] = '\0'; - sprintf( + str_format( addr, + sizeof(addr), "%#*jx-%#*jx", ptrwidth, (uintmax_t)kve->kve_start, diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 91acea812e..1623ff5eff 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -366,7 +366,7 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { #else sun = (struct sockaddr_un *)&kif->kf_un.kf_sock.kf_sa_local; #endif - snprintf( + str_format( path, sizeof(path), "%.*s", diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 594c6cda09..60035014ad 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -58,13 +58,13 @@ psutil_sensors_cpu_temperature(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "i", &core)) return NULL; - sprintf(sensor, "dev.cpu.%d.temperature", core); + str_format(sensor, sizeof(sensor), "dev.cpu.%d.temperature", core); if (psutil_sysctlbyname(sensor, ¤t, size) != 0) goto error; current = DECIKELVIN_2_CELSIUS(current); // Return -273 in case of failure. - sprintf(sensor, "dev.cpu.%d.coretemp.tjmax", core); + str_format(sensor, sizeof(sensor), "dev.cpu.%d.coretemp.tjmax", core); if (psutil_sysctlbyname(sensor, &tjmax, size) != 0) tjmax = 0; tjmax = DECIKELVIN_2_CELSIUS(tjmax); diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index e2249902c4..c89a2a129f 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -320,7 +320,7 @@ psutil_gather_unix( continue; sun = (struct sockaddr_un *)&xup->xu_addr; - snprintf( + str_format( path, sizeof(path), "%.*s", diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 09b5ed5c52..d86fe15e6a 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -24,7 +24,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; -#ifdef KERN_PROC_CWD +#ifdef KERN_PROC_CWD // available since NetBSD 99.43 int name[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) @@ -34,14 +34,11 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { return NULL; } #else - char *buf; - if (asprintf(&buf, "/proc/%d/cwd", (int)pid) < 0) { - PyErr_NoMemory(); - return NULL; - } + char buf[32]; + ssize_t len; - ssize_t len = readlink(buf, path, sizeof(path) - 1); - free(buf); + str_format(buf, sizeof(buf), "/proc/%d/cwd", (int)pid); + len = readlink(buf, path, sizeof(path) - 1); if (len == -1) { if (errno == ENOENT) psutil_oserror_nsp("readlink -> ENOENT"); diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index 1e05eb8d9f..833c5f2dcd 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -110,7 +110,7 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { if (len > 0) { ptr = buf; for (n = 0; n < len; ++n) { - sprintf(ptr, "%02x:", data[n] & 0xff); + str_format(ptr, sizeof(ptr), "%02x:", data[n] & 0xff); ptr += 3; } *--ptr = '\0'; diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index d1ff1a58d9..dbec5159db 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -134,13 +134,13 @@ psutil_sysctlbyname(const char *name, void *buf, size_t buflen) { return psutil_badargs("psutil_sysctlbyname"); if (sysctlbyname(name, buf, &len, NULL, 0) == -1) { - snprintf(errbuf, sizeof(errbuf), "sysctlbyname('%s')", name); + str_format(errbuf, sizeof(errbuf), "sysctlbyname('%s')", name); psutil_oserror_wsyscall(errbuf); return -1; } if (len != buflen) { - snprintf( + str_format( errbuf, sizeof(errbuf), "sysctlbyname('%s') size mismatch: returned %zu, expected %zu", @@ -173,7 +173,7 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { // First query to determine required size. ret = sysctlbyname(name, NULL, &needed, NULL, 0); if (ret == -1) { - snprintf( + str_format( errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 1/3", name ); psutil_oserror_wsyscall(errbuf); @@ -207,7 +207,7 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { buffer = NULL; if (sysctlbyname(name, NULL, &needed, NULL, 0) == -1) { - snprintf( + str_format( errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 2/3", @@ -223,14 +223,14 @@ psutil_sysctlbyname_malloc(const char *name, char **buf, size_t *buflen) { // Other errors: clean up and give up. free(buffer); - snprintf( + str_format( errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 3/3", name ); psutil_oserror_wsyscall(errbuf); return -1; } - snprintf( + str_format( errbuf, sizeof(errbuf), "sysctlbyname('%s') buffer allocation retry limit exceeded", diff --git a/psutil/arch/sunos/environ.c b/psutil/arch/sunos/environ.c index 5da8ab225d..d3b0c42239 100644 --- a/psutil/arch/sunos/environ.c +++ b/psutil/arch/sunos/environ.c @@ -33,7 +33,7 @@ open_address_space(pid_t pid, const char *procfs_path) { int fd; char proc_path[PATH_MAX]; - snprintf(proc_path, PATH_MAX, "%s/%i/as", procfs_path, pid); + str_format(proc_path, PATH_MAX, "%s/%i/as", procfs_path, pid); fd = open(proc_path, O_RDONLY); if (fd < 0) psutil_oserror(); diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index 3fbc0ddb18..c186702181 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -52,7 +52,7 @@ psutil_proc_basic_info(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/psinfo", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return Py_BuildValue( @@ -94,7 +94,7 @@ psutil_proc_name_and_args(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/psinfo", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; @@ -193,7 +193,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/psinfo", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) goto error; @@ -257,7 +257,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/status", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/status", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; // results are more precise than os.times() @@ -290,7 +290,7 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/lpsinfo", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/lpsinfo", procfs_path, pid); fd = open(path, O_RDONLY); if (fd == -1) { PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); @@ -354,7 +354,7 @@ psutil_proc_cred(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/cred", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/cred", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return Py_BuildValue( @@ -381,7 +381,7 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/usage", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/usage", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return Py_BuildValue("kk", info.pr_vctx, info.pr_ictx); @@ -408,7 +408,7 @@ proc_io_counters(PyObject* self, PyObject* args) { if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/usage", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/usage", procfs_path, pid); if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; @@ -438,7 +438,9 @@ psutil_proc_query_thread(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "iis", &pid, &tid, &procfs_path)) return NULL; - sprintf(path, "%s/%i/lwp/%i/lwpstatus", procfs_path, pid, tid); + str_format( + path, sizeof(path), "%s/%i/lwp/%i/lwpstatus", procfs_path, pid, tid + ); if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) return NULL; return Py_BuildValue( @@ -477,11 +479,11 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) goto error; - sprintf(path, "%s/%i/status", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/status", procfs_path, pid); if (!psutil_file_to_struct(path, (void *)&status, sizeof(status))) goto error; - sprintf(path, "%s/%i/xmap", procfs_path, pid); + str_format(path, sizeof(path), "%s/%i/xmap", procfs_path, pid); if (stat(path, &st) == -1) { psutil_oserror(); goto error; @@ -516,8 +518,9 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { pr_addr_sz = p->pr_vaddr + p->pr_size; // perms - sprintf( + str_format( perms, + sizeof(perms), "%c%c%c%c", p->pr_mflags & MA_READ ? 'r' : '-', p->pr_mflags & MA_WRITE ? 'w' : '-', diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index daf56687d8..ff9c0e3885 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -99,7 +99,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { // in the alphabet (from A:\ to Z:\). for (devNum = 0; devNum <= 32; ++devNum) { py_tuple = NULL; - sprintf_s(szDevice, MAX_PATH, "\\\\.\\PhysicalDrive%d", devNum); + str_format(szDevice, MAX_PATH, "\\\\.\\PhysicalDrive%d", devNum); hDevice = CreateFile( szDevice, 0, @@ -170,7 +170,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { goto error; } - sprintf_s(szDeviceDisplay, MAX_PATH, "PhysicalDrive%i", devNum); + str_format(szDeviceDisplay, MAX_PATH, "PhysicalDrive%i", devNum); py_tuple = Py_BuildValue( "(IILLKK)", diskPerformance.ReadCount, diff --git a/psutil/arch/windows/init.c b/psutil/arch/windows/init.c index b4f41d7b44..544b9110e0 100644 --- a/psutil/arch/windows/init.c +++ b/psutil/arch/windows/init.c @@ -91,7 +91,7 @@ psutil_SetFromNTStatusErr(NTSTATUS status, const char *syscall) { err = RtlNtStatusToDosErrorNoTeb(status); // if (GetLastError() != 0) // err = GetLastError(); - sprintf(fullmsg, "(originated from %s)", syscall); + str_format(fullmsg, sizeof(fullmsg), "(originated from %s)", syscall); return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); } diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index e550c43392..491095061e 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -385,7 +385,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { ifname_found = 0; pCurrAddresses = pAddresses; while (pCurrAddresses) { - sprintf_s(descr, MAX_PATH, "%wS", pCurrAddresses->Description); + str_format(descr, MAX_PATH, "%wS", pCurrAddresses->Description); if (lstrcmp(descr, pIfRow->bDescr) == 0) { py_nic_name = PyUnicode_FromWideChar( pCurrAddresses->FriendlyName, diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 5473cae0fc..6dca9cd2ee 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -65,7 +65,7 @@ psutil_convert_winerr(ULONG err, char *syscall) { char fullmsg[8192]; if (err == ERROR_NOACCESS) { - sprintf(fullmsg, "%s -> ERROR_NOACCESS", syscall); + str_format(fullmsg, sizeof(fullmsg), "%s -> ERROR_NOACCESS", syscall); psutil_debug(fullmsg); psutil_oserror_ad(fullmsg); } @@ -90,13 +90,15 @@ psutil_convert_ntstatus_err(NTSTATUS status, char *syscall) { static void psutil_giveup_with_ad(NTSTATUS status, char *syscall) { ULONG err; - char fullmsg[8192]; + char fullmsg[2048]; if (NT_NTWIN32(status)) err = WIN32_FROM_NTSTATUS(status); else err = RtlNtStatusToDosErrorNoTeb(status); - sprintf(fullmsg, "%s -> %lu (%s)", syscall, err, strerror(err)); + str_format( + fullmsg, sizeof(fullmsg), "%s -> %lu (%s)", syscall, err, strerror(err) + ); psutil_debug(fullmsg); psutil_oserror_ad(fullmsg); } From 7d821358201eb39278fa604ea04745e9fbcae27e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Oct 2025 14:55:34 +0100 Subject: [PATCH 1430/1714] NetBSD: refact proc cwd() --- psutil/arch/netbsd/proc.c | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index d86fe15e6a..36d22a98f0 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -17,39 +17,36 @@ PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { long pid; - char path[MAXPATHLEN]; - size_t pathlen = sizeof path; if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; #ifdef KERN_PROC_CWD // available since NetBSD 99.43 int name[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD}; - if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) { - if (errno == ENOENT) - psutil_oserror_nsp("sysctl -> ENOENT"); - else - psutil_oserror(); - return NULL; - } + size_t pathlen = sizeof(path); + + if (sysctl(name, 4, path, &pathlen, NULL, 0) != 0) + goto error; #else char buf[32]; ssize_t len; str_format(buf, sizeof(buf), "/proc/%d/cwd", (int)pid); len = readlink(buf, path, sizeof(path) - 1); - if (len == -1) { - if (errno == ENOENT) - psutil_oserror_nsp("readlink -> ENOENT"); - else - psutil_oserror(); - return NULL; - } + if (len == -1) + goto error; path[len] = '\0'; #endif return PyUnicode_DecodeFSDefault(path); + +error: + if (errno == ENOENT) + psutil_oserror_nsp("sysctl -> ENOENT"); + else + psutil_oserror(); + return NULL; } From fd05690da26faedb7b01753fb56f6323b1bdda30 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Oct 2025 15:13:30 +0100 Subject: [PATCH 1431/1714] [Windows] fix MAC address string construction in `net_if_addrs()` (#2677) Previously, the MAC address buffer was incorrectly updated using a fixed increment (ptr += 3) and sprintf_s, which could overflow or misformat the string if the MAC length or formatting changed. Also, the final '\n' was inserted unnecessarily. --- HISTORY.rst | 5 +++++ psutil/arch/windows/net.c | 26 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 87f18a8ff0..cc5121f756 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -19,6 +19,11 @@ XXXX-XX-XX 4GB. - 2675_, [macOS]: `Process.status()`_ incorrectly returns "running" for 99% of the processes. +- 2677_, [Windows]: fix MAC address string construction in `net_if_addrs()`_. + Previously, the MAC address buffer was incorrectly updated using a fixed + increment and `sprintf_s`, which could overflow or misformat the + string if the MAC length or formatting changed. Also, the final '\n' was + inserted unnecessarily. 7.1.2 ===== diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 491095061e..8e9a8186da 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -142,6 +142,8 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { DWORD dwRetVal = 0; ULONG converted_netmask; UINT netmask_bits; + int n; + size_t remaining; struct in_addr in_netmask; PIP_ADAPTER_ADDRESSES pAddresses = NULL; PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; @@ -176,27 +178,31 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { // MAC address if (pCurrAddresses->PhysicalAddressLength != 0) { ptr = buff_macaddr; - *ptr = '\0'; - for (i = 0; i < (int)pCurrAddresses->PhysicalAddressLength; i++) { - if (i == (pCurrAddresses->PhysicalAddressLength - 1)) { - sprintf_s( + remaining = sizeof(buff_macaddr); + for (i = 0; i < pCurrAddresses->PhysicalAddressLength; i++) { + if (i == pCurrAddresses->PhysicalAddressLength - 1) { + n = str_format( ptr, - _countof(buff_macaddr), - "%.2X\n", + remaining, + "%.2X", (int)pCurrAddresses->PhysicalAddress[i] ); } else { - sprintf_s( + n = str_format( ptr, - _countof(buff_macaddr), + remaining, "%.2X-", (int)pCurrAddresses->PhysicalAddress[i] ); } - ptr += 3; + if (n < 0) { // error or truncated + psutil_runtime_error("str_format() error"); + break; + } + ptr += n; + remaining -= n; } - *--ptr = '\0'; py_mac_address = Py_BuildValue("s", buff_macaddr); if (py_mac_address == NULL) From 0a7465c354688061847a44f17df182ccce14e362 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Oct 2025 15:22:14 +0100 Subject: [PATCH 1432/1714] Windows: use str_format() in psutil_users(). --- psutil/arch/windows/sys.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index 612dd2ebb7..4271a1a168 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -125,9 +125,9 @@ psutil_users(PyObject *self, PyObject *args) { address = (PWTS_CLIENT_ADDRESS)buffer_addr; if (address->AddressFamily == 2) { // AF_INET == 2 - sprintf_s( + str_format( address_str, - _countof(address_str), + sizeof(address_str), "%u.%u.%u.%u", // The IP address is offset by two bytes from the start of the // Address member of the WTS_CLIENT_ADDRESS structure. From 6822ba73e56a9ff289dadc3cd6d804aa13c3e869 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 30 Oct 2025 19:14:25 +0100 Subject: [PATCH 1433/1714] Replace `strlcat/strlcpy` with safe `str_copy/str_append` (#2678) This unifies string handling across platforms and reduces unsafe usage of strncpy/strlcat/strlcpy/strcpy, improving robustness. --- HISTORY.rst | 4 +++ psutil/_psutil_aix.c | 2 +- psutil/arch/all/init.h | 9 ++--- psutil/arch/all/str.c | 64 +++++++++++++++++++++++++++++++++--- psutil/arch/bsd/disk.c | 56 +++++++++++++++---------------- psutil/arch/freebsd/proc.c | 20 +++++------ psutil/arch/linux/net.c | 3 +- psutil/arch/netbsd/proc.c | 2 +- psutil/arch/netbsd/socks.c | 4 +-- psutil/arch/osx/disk.c | 50 ++++++++++++++-------------- psutil/arch/osx/proc_utils.c | 1 - psutil/arch/posix/net.c | 10 +++--- psutil/arch/sunos/mem.c | 1 - psutil/arch/sunos/net.c | 4 +-- psutil/arch/windows/disk.c | 22 ++++++++----- psutil/tests/test_osx.py | 2 ++ 16 files changed, 157 insertions(+), 97 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index cc5121f756..ed11b86ebf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,10 @@ XXXX-XX-XX mandatory formatting style for all C sources. - 2672_, [macOS], [BSD]: increase the chances to recognize zombie processes and raise the appropriate exception (`ZombieProcess`_). +- 2676_, 2678_: replace unsafe `sprintf` / `snprintf` / `sprintf_s` calls with + `str_format()`. Replace `strlcat` / `strlcpy` with safe `str_copy` / + `str_append`. This unifies string handling across platforms and reduces + unsafe usage of standard string functions, improving robustness. **Bug fixes** diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 8ac4908a5b..978dfb9e77 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -633,7 +633,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { if (sock == -1) goto error; - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); // is up? ret = ioctl(sock, SIOCGIFFLAGS, &ifr); diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index de2060490f..6377d92b3b 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -117,19 +117,16 @@ extern int PSUTIL_CONN_NONE; } while (0) -// strncpy() variant which appends a null terminator. -#define PSUTIL_STRNCPY(dst, src, n) \ - strncpy(dst, src, n - 1); \ - dst[n - 1] = '\0' - - PyObject *psutil_oserror(void); PyObject *psutil_oserror_ad(const char *msg); PyObject *psutil_oserror_nsp(const char *msg); PyObject *psutil_oserror_wsyscall(const char *syscall); PyObject *psutil_runtime_error(const char *msg, ...); +int str_append(char *dst, size_t dst_size, const char *src); +int str_copy(char *dst, size_t dst_size, const char *src); int str_format(char *buf, size_t size, const char *fmt, ...); + int psutil_badargs(const char *funcname); int psutil_setup(void); diff --git a/psutil/arch/all/str.c b/psutil/arch/all/str.c index 8dce9f8ae1..9baf0c7e1f 100644 --- a/psutil/arch/all/str.c +++ b/psutil/arch/all/str.c @@ -9,10 +9,19 @@ #include #include #include +#include #include "init.h" +static int +_error(const char *msg) { + // print debug msg because we never check str_*() return value. + psutil_debug("%s", msg); + return -1; +} + + // Safely formats a string into a buffer. Writes a printf-style // formatted string into `buf` of size `size`, always null-terminating // if size > 0. Returns the number of characters written (excluding the @@ -23,10 +32,8 @@ str_format(char *buf, size_t size, const char *fmt, ...) { va_list args; int ret; - if (size == 0) { - psutil_debug("str_format: invalid arg 'size' = 0"); - return -1; - } + if (size == 0) + return _error("str_format: invalid arg 'size' = 0"); va_start(args, fmt); #if defined(PSUTIL_WINDOWS) @@ -43,3 +50,52 @@ str_format(char *buf, size_t size, const char *fmt, ...) { } return ret; } + + +// Safely copy `src` to `dst`, always null-terminating. Replaces unsafe +// strcpy/strncpy. +int +str_copy(char *dst, size_t dst_size, const char *src) { + if (dst_size == 0) + return _error("str_copy: invalid arg 'dst_size' = 0"); + +#if defined(PSUTIL_WINDOWS) + if (strcpy_s(dst, dst_size, src) != 0) + return _error("str_copy: strcpy_s failed"); +#else + strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; +#endif + return 0; +} + + +// Safely append `src` to `dst`, always null-terminating. Returns 0 on +// success, -1 on truncation. +int +str_append(char *dst, size_t dst_size, const char *src) { + size_t dst_len; + + if (!dst || !src || dst_size == 0) + return _error("str_append: invalid arg"); + +#if defined(PSUTIL_WINDOWS) + dst_len = strnlen_s(dst, dst_size); + if (dst_len >= dst_size - 1) + return _error("str_append: destination full or truncated"); + if (strcat_s(dst, dst_size, src) != 0) + return _error("str_append: strcat_s failed"); +#elif defined(PSUTIL_MACOS) || defined(PSUTIL_BSD) + dst_len = strlcat(dst, src, dst_size); + if (dst_len >= dst_size) + return _error("str_append: truncated"); +#else + dst_len = strnlen(dst, dst_size); + if (dst_len >= dst_size - 1) + return _error("str_append: destination full or truncated"); + strncat(dst, src, dst_size - dst_len - 1); + dst[dst_size - 1] = '\0'; +#endif + + return 0; +} diff --git a/psutil/arch/bsd/disk.c b/psutil/arch/bsd/disk.c index 0cfa7133ca..7d6d000b38 100644 --- a/psutil/arch/bsd/disk.c +++ b/psutil/arch/bsd/disk.c @@ -80,73 +80,71 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { // see sys/mount.h if (flags & MNT_RDONLY) - strlcat(opts, "ro", sizeof(opts)); + str_append(opts, sizeof(opts), "ro"); else - strlcat(opts, "rw", sizeof(opts)); + str_append(opts, sizeof(opts), "rw"); if (flags & MNT_SYNCHRONOUS) - strlcat(opts, ",sync", sizeof(opts)); + str_append(opts, sizeof(opts), ",sync"); if (flags & MNT_NOEXEC) - strlcat(opts, ",noexec", sizeof(opts)); + str_append(opts, sizeof(opts), ",noexec"); if (flags & MNT_NOSUID) - strlcat(opts, ",nosuid", sizeof(opts)); + str_append(opts, sizeof(opts), ",nosuid"); if (flags & MNT_ASYNC) - strlcat(opts, ",async", sizeof(opts)); + str_append(opts, sizeof(opts), ",async"); if (flags & MNT_NOATIME) - strlcat(opts, ",noatime", sizeof(opts)); + str_append(opts, sizeof(opts), ",noatime"); if (flags & MNT_SOFTDEP) - strlcat(opts, ",softdep", sizeof(opts)); + str_append(opts, sizeof(opts), ",softdep"); #ifdef PSUTIL_FREEBSD if (flags & MNT_UNION) - strlcat(opts, ",union", sizeof(opts)); + str_append(opts, sizeof(opts), ",union"); if (flags & MNT_SUIDDIR) - strlcat(opts, ",suiddir", sizeof(opts)); - if (flags & MNT_SOFTDEP) - strlcat(opts, ",softdep", sizeof(opts)); + str_append(opts, sizeof(opts), ",suiddir"); if (flags & MNT_NOSYMFOLLOW) - strlcat(opts, ",nosymfollow", sizeof(opts)); + str_append(opts, sizeof(opts), ",nosymfollow"); #ifdef MNT_GJOURNAL if (flags & MNT_GJOURNAL) - strlcat(opts, ",gjournal", sizeof(opts)); + str_append(opts, sizeof(opts), ",gjournal"); #endif if (flags & MNT_MULTILABEL) - strlcat(opts, ",multilabel", sizeof(opts)); + str_append(opts, sizeof(opts), ",multilabel"); if (flags & MNT_ACLS) - strlcat(opts, ",acls", sizeof(opts)); + str_append(opts, sizeof(opts), ",acls"); if (flags & MNT_NOCLUSTERR) - strlcat(opts, ",noclusterr", sizeof(opts)); + str_append(opts, sizeof(opts), ",noclusterr"); if (flags & MNT_NOCLUSTERW) - strlcat(opts, ",noclusterw", sizeof(opts)); + str_append(opts, sizeof(opts), ",noclusterw"); #ifdef MNT_NFS4ACLS if (flags & MNT_NFS4ACLS) - strlcat(opts, ",nfs4acls", sizeof(opts)); + str_append(opts, sizeof(opts), ",nfs4acls"); #endif #elif PSUTIL_NETBSD if (flags & MNT_NODEV) - strlcat(opts, ",nodev", sizeof(opts)); + str_append(opts, sizeof(opts), ",nodev"); if (flags & MNT_UNION) - strlcat(opts, ",union", sizeof(opts)); + str_append(opts, sizeof(opts), ",union"); if (flags & MNT_NOCOREDUMP) - strlcat(opts, ",nocoredump", sizeof(opts)); + str_append(opts, sizeof(opts), ",nocoredump"); #ifdef MNT_RELATIME if (flags & MNT_RELATIME) - strlcat(opts, ",relatime", sizeof(opts)); + str_append(opts, sizeof(opts), ",relatime"); #endif if (flags & MNT_IGNORE) - strlcat(opts, ",ignore", sizeof(opts)); + str_append(opts, sizeof(opts), ",ignore"); #ifdef MNT_DISCARD if (flags & MNT_DISCARD) - strlcat(opts, ",discard", sizeof(opts)); + str_append(opts, sizeof(opts), ",discard"); #endif #ifdef MNT_EXTATTR if (flags & MNT_EXTATTR) - strlcat(opts, ",extattr", sizeof(opts)); + str_append(opts, sizeof(opts), ",extattr"); #endif if (flags & MNT_LOG) - strlcat(opts, ",log", sizeof(opts)); + str_append(opts, sizeof(opts), ",log"); if (flags & MNT_SYMPERM) - strlcat(opts, ",symperm", sizeof(opts)); + str_append(opts, sizeof(opts), ",symperm"); if (flags & MNT_NODEVMTIME) - strlcat(opts, ",nodevmtime", sizeof(opts)); + str_append(opts, sizeof(opts), ",nodevmtime"); #endif py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); if (!py_dev) diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index ebab0ad266..2e3637c8ab 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -129,7 +129,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { else if (ret == 0) return psutil_oserror_nsp("psutil_pid_exists -> 0"); else - strcpy(pathname, ""); + str_copy(pathname, sizeof(pathname), ""); } return PyUnicode_DecodeFSDefault(pathname); @@ -330,20 +330,20 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { (uintmax_t)kve->kve_end ); psutil_remove_spaces(addr); - strlcat( + str_append( perms, - kve->kve_protection & KVME_PROT_READ ? "r" : "-", - sizeof(perms) + sizeof(perms), + kve->kve_protection & KVME_PROT_READ ? "r" : "-" ); - strlcat( + str_append( perms, - kve->kve_protection & KVME_PROT_WRITE ? "w" : "-", - sizeof(perms) + sizeof(perms), + kve->kve_protection & KVME_PROT_WRITE ? "w" : "-" ); - strlcat( + str_append( perms, - kve->kve_protection & KVME_PROT_EXEC ? "x" : "-", - sizeof(perms) + sizeof(perms), + kve->kve_protection & KVME_PROT_EXEC ? "x" : "-" ); if (strlen(kve->kve_path) == 0) { diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index cbc6c57ad3..6daaa43138 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -71,7 +71,8 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) return psutil_oserror_wsyscall("socket()"); - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); + // duplex and speed memset(ðcmd, 0, sizeof ethcmd); diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 36d22a98f0..6f8d3a478f 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -96,7 +96,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { else if (ret == 0) return psutil_oserror_nsp("psutil_pid_exists -> 0"); else - strcpy(pathname, ""); + str_copy(pathname, sizeof(pathname), ""); } return PyUnicode_DecodeFSDefault(pathname); diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index abb315e9c6..349493b237 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -407,8 +407,8 @@ psutil_net_connections(PyObject *self, PyObject *args) { ->ki_src; struct sockaddr_un *sun_dst = (struct sockaddr_un *)&kp->kpcb ->ki_dst; - strcpy(laddr, sun_src->sun_path); - strcpy(raddr, sun_dst->sun_path); + str_copy(laddr, sizeof(sun_src->sun_path), sun_src->sun_path); + str_copy(raddr, sizeof(sun_dst->sun_path), sun_dst->sun_path); status = PSUTIL_CONN_NONE; py_laddr = PyUnicode_DecodeFSDefault(laddr); if (!py_laddr) diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index ae3283a212..c08ef24c7c 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -71,61 +71,61 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { // see sys/mount.h if (flags & MNT_RDONLY) - strlcat(opts, "ro", sizeof(opts)); + str_append(opts, sizeof(opts), "ro"); else - strlcat(opts, "rw", sizeof(opts)); + str_append(opts, sizeof(opts), "rw"); if (flags & MNT_SYNCHRONOUS) - strlcat(opts, ",sync", sizeof(opts)); + str_append(opts, sizeof(opts), ",sync"); if (flags & MNT_NOEXEC) - strlcat(opts, ",noexec", sizeof(opts)); + str_append(opts, sizeof(opts), ",noexec"); if (flags & MNT_NOSUID) - strlcat(opts, ",nosuid", sizeof(opts)); + str_append(opts, sizeof(opts), ",nosuid"); if (flags & MNT_UNION) - strlcat(opts, ",union", sizeof(opts)); + str_append(opts, sizeof(opts), ",union"); if (flags & MNT_ASYNC) - strlcat(opts, ",async", sizeof(opts)); + str_append(opts, sizeof(opts), ",async"); if (flags & MNT_EXPORTED) - strlcat(opts, ",exported", sizeof(opts)); + str_append(opts, sizeof(opts), ",exported"); if (flags & MNT_LOCAL) - strlcat(opts, ",local", sizeof(opts)); + str_append(opts, sizeof(opts), ",local"); if (flags & MNT_QUOTA) - strlcat(opts, ",quota", sizeof(opts)); + str_append(opts, sizeof(opts), ",quota"); if (flags & MNT_ROOTFS) - strlcat(opts, ",rootfs", sizeof(opts)); + str_append(opts, sizeof(opts), ",rootfs"); if (flags & MNT_DOVOLFS) - strlcat(opts, ",dovolfs", sizeof(opts)); + str_append(opts, sizeof(opts), ",dovolfs"); if (flags & MNT_DONTBROWSE) - strlcat(opts, ",dontbrowse", sizeof(opts)); + str_append(opts, sizeof(opts), ",dontbrowse"); if (flags & MNT_IGNORE_OWNERSHIP) - strlcat(opts, ",ignore-ownership", sizeof(opts)); + str_append(opts, sizeof(opts), ",ignore-ownership"); if (flags & MNT_AUTOMOUNTED) - strlcat(opts, ",automounted", sizeof(opts)); + str_append(opts, sizeof(opts), ",automounted"); if (flags & MNT_JOURNALED) - strlcat(opts, ",journaled", sizeof(opts)); + str_append(opts, sizeof(opts), ",journaled"); if (flags & MNT_NOUSERXATTR) - strlcat(opts, ",nouserxattr", sizeof(opts)); + str_append(opts, sizeof(opts), ",nouserxattr"); if (flags & MNT_DEFWRITE) - strlcat(opts, ",defwrite", sizeof(opts)); + str_append(opts, sizeof(opts), ",defwrite"); if (flags & MNT_UPDATE) - strlcat(opts, ",update", sizeof(opts)); + str_append(opts, sizeof(opts), ",update"); if (flags & MNT_RELOAD) - strlcat(opts, ",reload", sizeof(opts)); + str_append(opts, sizeof(opts), ",reload"); if (flags & MNT_FORCE) - strlcat(opts, ",force", sizeof(opts)); + str_append(opts, sizeof(opts), ",force"); if (flags & MNT_CMDFLAGS) - strlcat(opts, ",cmdflags", sizeof(opts)); + str_append(opts, sizeof(opts), ",cmdflags"); // requires macOS >= 10.5 #ifdef MNT_QUARANTINE if (flags & MNT_QUARANTINE) - strlcat(opts, ",quarantine", sizeof(opts)); + str_append(opts, sizeof(opts), ",quarantine"); #endif #ifdef MNT_MULTILABEL if (flags & MNT_MULTILABEL) - strlcat(opts, ",multilabel", sizeof(opts)); + str_append(opts, sizeof(opts), ",multilabel"); #endif #ifdef MNT_NOATIME if (flags & MNT_NOATIME) - strlcat(opts, ",noatime", sizeof(opts)); + str_append(opts, sizeof(opts), ",noatime"); #endif py_dev = PyUnicode_DecodeFSDefault(fs[i].f_mntfromname); if (!py_dev) diff --git a/psutil/arch/osx/proc_utils.c b/psutil/arch/osx/proc_utils.c index 8cb49ec231..93e5e91f37 100644 --- a/psutil/arch/osx/proc_utils.c +++ b/psutil/arch/osx/proc_utils.c @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index 833c5f2dcd..ee8def6ef1 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -235,7 +235,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { if (sock == -1) goto error; - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); ret = ioctl(sock, SIOCGIFMTU, &ifr); if (ret == -1) goto error; @@ -289,7 +289,7 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { goto error; } - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); ret = ioctl(sock, SIOCGIFFLAGS, &ifr); if (ret == -1) { psutil_oserror_wsyscall("ioctl(SIOCGIFFLAGS)"); @@ -472,7 +472,7 @@ psutil_net_if_is_running(PyObject *self, PyObject *args) { if (sock == -1) goto error; - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); ret = ioctl(sock, SIOCGIFFLAGS, &ifr); if (ret == -1) goto error; @@ -653,11 +653,11 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) return psutil_oserror(); - PSUTIL_STRNCPY(ifr.ifr_name, nic_name, sizeof(ifr.ifr_name)); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); // speed / duplex memset(&ifmed, 0, sizeof(struct ifmediareq)); - strlcpy(ifmed.ifm_name, nic_name, sizeof(ifmed.ifm_name)); + str_copy(ifmed.ifm_name, sizeof(ifmed.ifm_name), nic_name); ret = ioctl(sock, SIOCGIFMEDIA, (caddr_t)&ifmed); if (ret == -1) { speed = 0; diff --git a/psutil/arch/sunos/mem.c b/psutil/arch/sunos/mem.c index 089b703e80..a4e41482ae 100644 --- a/psutil/arch/sunos/mem.c +++ b/psutil/arch/sunos/mem.c @@ -7,7 +7,6 @@ #include #include -#include #include #include "../../arch/all/init.h" diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c index 052ac0e0c2..12353d4c75 100644 --- a/psutil/arch/sunos/net.c +++ b/psutil/arch/sunos/net.c @@ -61,7 +61,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { goto next; // check if this is a network interface by sending a ioctl - PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); + str_copy(ifr.lifr_name, sizeof(ifr.lifr_name), ksp->ks_name); ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); if (ret == -1) goto next; @@ -176,7 +176,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { if (strcmp(ksp->ks_class, "net") != 0) continue; - PSUTIL_STRNCPY(ifr.lifr_name, ksp->ks_name, sizeof(ifr.lifr_name)); + str_copy(ifr.lifr_name, sizeof(ifr.lifr_name), ksp->ks_name); ret = ioctl(sock, SIOCGLIFFLAGS, &ifr); if (ret == -1) continue; // not a network interface diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index ff9c0e3885..5310a92de0 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -289,18 +289,18 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { // which case the error is (21, "device not ready"). // Let's pretend it didn't happen as we already have // the drive name and type ('removable'). - strcat_s(opts, _countof(opts), ""); + str_append(opts, sizeof(opts), ""); SetLastError(0); } else { if (pflags & FILE_READ_ONLY_VOLUME) - strcat_s(opts, _countof(opts), "ro"); + str_append(opts, sizeof(opts), "ro"); else - strcat_s(opts, _countof(opts), "rw"); + str_append(opts, sizeof(opts), "rw"); if (pflags & FILE_VOLUME_IS_COMPRESSED) - strcat_s(opts, _countof(opts), ",compressed"); + str_append(opts, sizeof(opts), ",compressed"); if (pflags & FILE_READ_ONLY_VOLUME) - strcat_s(opts, _countof(opts), ",readonly"); + str_append(opts, sizeof(opts), ",readonly"); // Check for mount points on this volume and add/get info // (checks first to know if we can even have mount points) @@ -312,8 +312,12 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { mp_flag = TRUE; while (mp_flag) { // Append full mount path with drive letter - strcpy_s(mp_path, _countof(mp_path), drive_letter); - strcat_s(mp_path, _countof(mp_path), mp_buf); + str_copy( + mp_path, sizeof(mp_path), drive_letter + ); // initialize + str_append( + mp_path, sizeof(mp_path), mp_buf + ); // append mount point py_tuple = Py_BuildValue( "(ssss)", @@ -343,8 +347,8 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } if (strlen(opts) > 0) - strcat_s(opts, _countof(opts), ","); - strcat_s(opts, _countof(opts), psutil_get_drive_type(type)); + str_append(opts, sizeof(opts), ","); + str_append(opts, sizeof(opts), psutil_get_drive_type(type)); py_tuple = Py_BuildValue( "(ssss)", diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py index 8c7d1e93a9..33833c3ed0 100755 --- a/psutil/tests/test_osx.py +++ b/psutil/tests/test_osx.py @@ -146,6 +146,8 @@ def test_vmem_active(self): psutil_val = psutil.virtual_memory().active assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + # XXX: fails too often + @pytest.mark.skipif(CI_TESTING, reason="skipped on CI_TESTING") @retry_on_failure() def test_vmem_inactive(self): vmstat_val = vm_stat("inactive") From bcdede485b346f65ee8109b18e0a03d1cd3fc462 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Nov 2025 12:29:49 +0100 Subject: [PATCH 1434/1714] Introduce PSUTIL_TESTING mode. Terminate execution if str_* funcs fail --- Makefile | 2 +- psutil/arch/all/init.c | 3 +++ psutil/arch/all/init.h | 1 + psutil/arch/all/str.c | 13 ++++++++++--- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 23831dcbc3..37027dc838 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ PYTHON = python3 ARGS = PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade -PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 +PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_TESTING=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 SUDO = $(if $(filter $(OS),Windows_NT),,sudo -E) DPRINT = ~/.dprint/bin/dprint diff --git a/psutil/arch/all/init.c b/psutil/arch/all/init.c index b2647aaa6e..67c446b2b6 100644 --- a/psutil/arch/all/init.c +++ b/psutil/arch/all/init.c @@ -11,6 +11,7 @@ #include "init.h" int PSUTIL_DEBUG = 0; +int PSUTIL_TESTING = 0; int PSUTIL_CONN_NONE = 128; #ifdef Py_GIL_DISABLED @@ -45,5 +46,7 @@ int psutil_setup(void) { if (getenv("PSUTIL_DEBUG") != NULL) PSUTIL_DEBUG = 1; + if (getenv("PSUTIL_TESTING") != NULL) + PSUTIL_TESTING = 1; return 0; } diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 6377d92b3b..df0481f62c 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -39,6 +39,7 @@ extern int PSUTIL_DEBUG; // a signaler for connections without an actual status extern int PSUTIL_CONN_NONE; +extern int PSUTIL_TESTING; #ifdef Py_GIL_DISABLED extern PyMutex utxent_lock; diff --git a/psutil/arch/all/str.c b/psutil/arch/all/str.c index 9baf0c7e1f..f0826f5157 100644 --- a/psutil/arch/all/str.c +++ b/psutil/arch/all/str.c @@ -6,9 +6,10 @@ // String utilities. -#include #include #include +#include +#include #include #include "init.h" @@ -16,8 +17,14 @@ static int _error(const char *msg) { - // print debug msg because we never check str_*() return value. - psutil_debug("%s", msg); + if (PSUTIL_TESTING) { + printf("CRITICAL: %s\n", msg); + exit(EXIT_FAILURE); // terminate execution + } + else { + // Print debug msg because we never check str_*() return value. + psutil_debug("%s", msg); + } return -1; } From e4e100300c68c86b6bec4a28e5a0ed8035e7f647 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Nov 2025 12:37:21 +0100 Subject: [PATCH 1435/1714] psutil_kinfo_proc: return in case of badargs; also print to stderr in case of PSUTIL_TESTING --- psutil/arch/all/str.c | 3 ++- psutil/arch/bsd/proc_utils.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/psutil/arch/all/str.c b/psutil/arch/all/str.c index f0826f5157..c4b90962da 100644 --- a/psutil/arch/all/str.c +++ b/psutil/arch/all/str.c @@ -18,7 +18,8 @@ static int _error(const char *msg) { if (PSUTIL_TESTING) { - printf("CRITICAL: %s\n", msg); + fprintf(stderr, "CRITICAL: %s\n", msg); + fflush(stderr); exit(EXIT_FAILURE); // terminate execution } else { diff --git a/psutil/arch/bsd/proc_utils.c b/psutil/arch/bsd/proc_utils.c index 969d2a38bb..4b1023d0b4 100644 --- a/psutil/arch/bsd/proc_utils.c +++ b/psutil/arch/bsd/proc_utils.c @@ -41,7 +41,7 @@ psutil_kinfo_proc(pid_t pid, void *proc) { #endif if (pid < 0 || proc == NULL) - psutil_badargs("psutil_kinfo_proc"); + return psutil_badargs("psutil_kinfo_proc"); if (sysctl(mib, len, proc, &size, NULL, 0) == -1) { psutil_oserror_wsyscall("sysctl(kinfo_proc)"); From d42db80660b719c91b387b9c6400d43e7a7839bb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 1 Nov 2025 13:35:39 +0100 Subject: [PATCH 1436/1714] Update DEVGUIDE.rst --- docs/DEVGUIDE.rst | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 563c9b8839..bf03cdb731 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -28,9 +28,9 @@ Once you have a compiler installed run: - ``make`` (and the accompanying `Makefile`_) is the designated tool to build, install, run tests and do pretty much anything that involves development. - This also includes Windows, meaning that you can run `make command` similarly - as if you were on UNIX (see `make.bat`_ and `winmake.py`_). Some useful - commands are: + This also includes Windows, meaning that you can run `make.bat command` + similarly as if you were on UNIX (see `make.bat`_ and `winmake.py`_). Some + useful commands are: .. code-block:: bash @@ -49,15 +49,9 @@ Once you have a compiler installed run: .. code-block:: bash - make test ARGS=psutil/tests/test_system.py::TestDiskAPIs::test_disk_usage + make test ARGS=psutil/tests/test_system.py # UNIX -- If you're working on a new feature and you wish to compile & test it "on the - fly" from a test script, this is a quick & dirty way to do it: - -.. code-block:: bash - - make test ARGS=test_script.py # on UNIX - make test test_script.py # on Windows + set ARGS=psutil/tests/test_system.py && make test # Windows - Do not use ``sudo``. ``make install`` installs psutil as a limited user in "edit" / development mode, meaning you can edit psutil code on the fly while @@ -67,18 +61,27 @@ Once you have a compiler installed run: .. code-block:: bash - make test PYTHON=python3.5 # UNIX - make test -p C:\python35\python.exe # Windows + make test PYTHON=python3.8 # UNIX + + set PYTHON=C:\Python38\python.exe && make test # Windows Coding style ------------ -- Python code strictly follows `PEP-8`_ styling guide. In addition we use - ``black`` and ``ruff`` code formatters / linters. This is enforced by a GIT - commit hook, installed via ``make install-git-hooks``, which will reject the - commit if code is not compliant. -- C code should follow `PEP-7`_ styling guides, but things are more relaxed. -- Linters are enforced also for ``.rst`` and ``.toml`` files. +All style and formatting checks are automatically enforced both **locally on +each `git commit`** and **remotely via a GitHub Actions pipeline**. + +- A **Git commit hook**, installed with `make install-git-hooks`, runs all + formatters and linters before each commit. The commit is rejected if any + check fails. +- **Python** code follows the `PEP-8`_ style guide. We use `black` and `ruff` + for formatting and linting. +- **C** code generally follows the `PEP-7`_ style guide, with formatting + enforced by `clang-format`. +- **Other files** (`.rst`, `.toml`, `.md`, `.yml`) are also validated by + dedicated command-line linters. +- The **GitHub Actions pipeline** re-runs all these checks to ensure + consistency (via ``make lint-all``). Code organization ----------------- @@ -88,6 +91,7 @@ Code organization psutil/__init__.py # main psutil namespace ("import psutil") psutil/_ps{platform}.py # platform-specific python wrapper psutil/_psutil_{platform}.c # platform-specific C extension + psutil/arch/{platform}/*.c # platform-specific C extension psutil/tests/test_process|system.py # main test suite psutil/tests/test_{platform}.py # platform-specific test suite @@ -100,7 +104,7 @@ Typically, this is what you do: - Write the platform specific implementation in ``psutil/_ps{platform}.py`` (e.g. `psutil/_pslinux.py`_). - If the change requires C code, write the C implementation in - ``psutil/_psutil_{platform}.c`` (e.g. `psutil/_psutil_linux.c`_). + ``psutil/arch/{platform}/file.c`` (e.g. `psutil/arch/linux/cpu.c`). - Write a generic test in `psutil/tests/test_system.py`_ or `psutil/tests/test_process.py`_. - If possible, write a platform-specific test in From 5904ff94e3fd0fea3d0734a936834bef6e63369a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 2 Nov 2025 13:25:11 +0100 Subject: [PATCH 1437/1714] Pre-release --- HISTORY.rst | 7 ++++--- docs/index.rst | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ed11b86ebf..d840b743cb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.1.3 (IN DEVELOPMENT) -====================== +7.1.3 +===== -XXXX-XX-XX +2025-11-02 **Enhancements** @@ -28,6 +28,7 @@ XXXX-XX-XX increment and `sprintf_s`, which could overflow or misformat the string if the MAC length or formatting changed. Also, the final '\n' was inserted unnecessarily. +- 2679_, [OpenBSD], [NetBSD], [critical]: can't build due to C syntax error. 7.1.2 ===== diff --git a/docs/index.rst b/docs/index.rst index 371264043a..51d65d452c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2702,6 +2702,14 @@ PyPy3. Timeline ======== +- 2025-11-02: + `7.1.3 `__ - + `what's new `__ - + `diff `__ +- 2025-10-25: + `7.1.2 `__ - + `what's new `__ - + `diff `__ - 2025-10-19: `7.1.1 `__ - `what's new `__ - From 024ec81905237ca15e47bcf7895384e4c9f68003 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 3 Nov 2025 21:23:03 +0100 Subject: [PATCH 1438/1714] Move tests/ in root directory (#2680) --- .github/workflows/bsd.yml | 2 +- .github/workflows/sunos.yml | 2 +- HISTORY.rst | 15 ++++ MANIFEST.in | 41 ++++++----- Makefile | 38 +++++----- docs/DEVGUIDE.rst | 28 +++----- docs/index.rst | 2 +- make.bat | 2 +- psutil/__init__.py | 2 +- psutil/tests/README.rst | 35 ---------- psutil/tests/__main__.py | 13 ---- pyproject.toml | 10 +-- scripts/internal/download_wheels.py | 13 +++- scripts/internal/print_sysinfo.py | 2 +- scripts/internal/winmake.py | 17 +++-- setup.py | 2 +- tests/README.md | 41 +++++++++++ {psutil/tests => tests}/__init__.py | 20 ++++-- {psutil/tests => tests}/test_aix.py | 13 ++-- {psutil/tests => tests}/test_bsd.py | 17 ++--- {psutil/tests => tests}/test_connections.py | 39 ++++++----- {psutil/tests => tests}/test_contracts.py | 27 ++++---- {psutil/tests => tests}/test_linux.py | 39 ++++++----- {psutil/tests => tests}/test_memleaks.py | 45 ++++++------ {psutil/tests => tests}/test_misc.py | 13 ++-- {psutil/tests => tests}/test_osx.py | 23 +++--- {psutil/tests => tests}/test_posix.py | 21 +++--- {psutil/tests => tests}/test_process.py | 59 ++++++++-------- {psutil/tests => tests}/test_process_all.py | 23 +++--- {psutil/tests => tests}/test_scripts.py | 29 ++++---- {psutil/tests => tests}/test_sudo.py | 5 +- {psutil/tests => tests}/test_sunos.py | 7 +- {psutil/tests => tests}/test_system.py | 39 ++++++----- {psutil/tests => tests}/test_testutils.py | 77 +++++++++++---------- {psutil/tests => tests}/test_unicode.py | 43 ++++++------ {psutil/tests => tests}/test_windows.py | 27 ++++---- 36 files changed, 437 insertions(+), 394 deletions(-) delete mode 100644 psutil/tests/README.rst delete mode 100644 psutil/tests/__main__.py create mode 100644 tests/README.md rename {psutil/tests => tests}/__init__.py (99%) rename {psutil/tests => tests}/test_aix.py (93%) rename {psutil/tests => tests}/test_bsd.py (98%) rename {psutil/tests => tests}/test_connections.py (96%) rename {psutil/tests => tests}/test_contracts.py (95%) rename {psutil/tests => tests}/test_linux.py (99%) rename {psutil/tests => tests}/test_memleaks.py (94%) rename {psutil/tests => tests}/test_misc.py (99%) rename {psutil/tests => tests}/test_osx.py (93%) rename {psutil/tests => tests}/test_posix.py (97%) rename {psutil/tests => tests}/test_process.py (98%) rename {psutil/tests => tests}/test_process_all.py (97%) rename {psutil/tests => tests}/test_scripts.py (93%) rename {psutil/tests => tests}/test_sudo.py (98%) rename {psutil/tests => tests}/test_sunos.py (91%) rename {psutil/tests => tests}/test_system.py (97%) rename {psutil/tests => tests}/test_testutils.py (91%) rename {psutil/tests => tests}/test_unicode.py (92%) rename {psutil/tests => tests}/test_windows.py (98%) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 05694fbe85..036bf581b3 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -16,7 +16,7 @@ on: - "psutil/arch/netbsd/**" - "psutil/arch/openbsd/**" - "psutil/arch/posix/**" - - "psutil/tests/test_bsd.py" + - "tests/test_bsd.py" - "setup.py" pull_request: paths: *bsd_paths diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml index 2facfe364c..dd9dcb9d9a 100644 --- a/.github/workflows/sunos.yml +++ b/.github/workflows/sunos.yml @@ -35,4 +35,4 @@ jobs: python3 -c "import psutil" python3 scripts/internal/install_pip.py python3 -m pip install --break-system-packages --user pytest pytest-instafail pytest-subtests pytest-xdist - python3 -m pytest psutil/tests/test_memleaks.py + python3 -m pytest tests/test_memleaks.py diff --git a/HISTORY.rst b/HISTORY.rst index d840b743cb..aceb6a411b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,20 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.2.0 (IN DEVELOPMENT) +====================== + +XXXX-XX-XX + +**Enhancements** + +- 2680_: unit tests are no longer installed / part of the distribution. They + now live under `tests/` instead of `psutil/tests`. + +**Compatibility notes** + +- 2680_: `import psutil.tests` no longer works (but it was never documented to + begin with). + 7.1.3 ===== diff --git a/MANIFEST.in b/MANIFEST.in index 450201d440..3d9bd3aedd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -133,27 +133,6 @@ include psutil/arch/windows/services.c include psutil/arch/windows/socks.c include psutil/arch/windows/sys.c include psutil/arch/windows/wmi.c -include psutil/tests/README.rst -include psutil/tests/__init__.py -include psutil/tests/__main__.py -include psutil/tests/test_aix.py -include psutil/tests/test_bsd.py -include psutil/tests/test_connections.py -include psutil/tests/test_contracts.py -include psutil/tests/test_linux.py -include psutil/tests/test_memleaks.py -include psutil/tests/test_misc.py -include psutil/tests/test_osx.py -include psutil/tests/test_posix.py -include psutil/tests/test_process.py -include psutil/tests/test_process_all.py -include psutil/tests/test_scripts.py -include psutil/tests/test_sudo.py -include psutil/tests/test_sunos.py -include psutil/tests/test_system.py -include psutil/tests/test_testutils.py -include psutil/tests/test_unicode.py -include psutil/tests/test_windows.py include pyproject.toml include scripts/battery.py include scripts/cpu_distribution.py @@ -199,4 +178,24 @@ include scripts/top.py include scripts/who.py include scripts/winservices.py include setup.py +include tests/README.md +include tests/__init__.py +include tests/test_aix.py +include tests/test_bsd.py +include tests/test_connections.py +include tests/test_contracts.py +include tests/test_linux.py +include tests/test_memleaks.py +include tests/test_misc.py +include tests/test_osx.py +include tests/test_posix.py +include tests/test_process.py +include tests/test_process_all.py +include tests/test_scripts.py +include tests/test_sudo.py +include tests/test_sunos.py +include tests/test_system.py +include tests/test_testutils.py +include tests/test_unicode.py +include tests/test_windows.py recursive-exclude docs/_static * diff --git a/Makefile b/Makefile index 37027dc838..a24d8870db 100644 --- a/Makefile +++ b/Makefile @@ -92,51 +92,51 @@ install-git-hooks: ## Install GIT pre-commit hook. RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest test: ## Run all tests. - # To run a specific test do `make test ARGS=psutil/tests/test_process.py::TestProcess::test_cmdline` - $(RUN_TEST) --ignore=psutil/tests/test_memleaks.py --ignore=psutil/tests/test_sudo.py $(ARGS) + # To run a specific test do `make test ARGS=tests/test_process.py::TestProcess::test_cmdline` + $(RUN_TEST) --ignore=tests/test_memleaks.py --ignore=tests/test_sudo.py $(ARGS) test-parallel: ## Run all tests in parallel. - $(RUN_TEST) --ignore=psutil/tests/test_memleaks.py -p xdist -n auto --dist loadgroup $(ARGS) + $(RUN_TEST) --ignore=tests/test_memleaks.py -p xdist -n auto --dist loadgroup $(ARGS) test-process: ## Run process-related API tests. - $(RUN_TEST) psutil/tests/test_process.py $(ARGS) + $(RUN_TEST) tests/test_process.py $(ARGS) test-process-all: ## Run tests which iterate over all process PIDs. - $(RUN_TEST) psutil/tests/test_process_all.py $(ARGS) + $(RUN_TEST) tests/test_process_all.py $(ARGS) test-system: ## Run system-related API tests. - $(RUN_TEST) psutil/tests/test_system.py $(ARGS) + $(RUN_TEST) tests/test_system.py $(ARGS) test-misc: ## Run miscellaneous tests. - $(RUN_TEST) psutil/tests/test_misc.py $(ARGS) + $(RUN_TEST) tests/test_misc.py $(ARGS) test-scripts: ## Run scripts tests. - $(RUN_TEST) psutil/tests/test_scripts.py $(ARGS) + $(RUN_TEST) tests/test_scripts.py $(ARGS) test-testutils: ## Run test utils tests. - $(RUN_TEST) psutil/tests/test_testutils.py $(ARGS) + $(RUN_TEST) tests/test_testutils.py $(ARGS) test-unicode: ## Test APIs dealing with strings. - $(RUN_TEST) psutil/tests/test_unicode.py $(ARGS) + $(RUN_TEST) tests/test_unicode.py $(ARGS) test-contracts: ## APIs sanity tests. - $(RUN_TEST) psutil/tests/test_contracts.py $(ARGS) + $(RUN_TEST) tests/test_contracts.py $(ARGS) test-connections: ## Test psutil.net_connections() and Process.net_connections(). - $(RUN_TEST) psutil/tests/test_connections.py $(ARGS) + $(RUN_TEST) tests/test_connections.py $(ARGS) test-posix: ## POSIX specific tests. - $(RUN_TEST) psutil/tests/test_posix.py $(ARGS) + $(RUN_TEST) tests/test_posix.py $(ARGS) test-platform: ## Run specific platform tests only. - $(RUN_TEST) psutil/tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) + $(RUN_TEST) tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) test-memleaks: ## Memory leak tests. - $(RUN_TEST) psutil/tests/test_memleaks.py $(ARGS) + $(RUN_TEST) tests/test_memleaks.py $(ARGS) test-sudo: ## Run tests requiring root privileges. # Use unittest runner because pytest may not be installed as root. - $(SUDO) $(PYTHON_ENV_VARS) $(PYTHON) -m unittest -v psutil.tests.test_sudo + $(SUDO) $(PYTHON_ENV_VARS) $(PYTHON) -m unittest -v tests.test_sudo test-last-failed: ## Re-run tests which failed on last run $(RUN_TEST) --last-failed $(ARGS) @@ -144,7 +144,7 @@ test-last-failed: ## Re-run tests which failed on last run test-coverage: ## Run test coverage. # Note: coverage options are controlled by .coveragerc file rm -rf .coverage htmlcov - $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest --ignore=psutil/tests/test_memleaks.py $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest --ignore=tests/test_memleaks.py $(ARGS) $(PYTHON) -m coverage report @echo "writing results to htmlcov/index.html" $(PYTHON) -m coverage html @@ -228,7 +228,7 @@ ci-test: ## Run tests on GitHub CI. Used by BSD runners. $(MAKE) install-sysdeps PIP_BREAK_SYSTEM_PACKAGES=1 $(MAKE) install-pydeps-test $(MAKE) print-sysinfo - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest psutil/tests/ + $(PYTHON_ENV_VARS) $(PYTHON) -m pytest tests/ ci-test-cibuildwheel: ## Run tests from cibuildwheel. # testing the wheels means we can't use other test targets which are rebuilding the python extensions @@ -237,7 +237,7 @@ ci-test-cibuildwheel: ## Run tests from cibuildwheel. PIP_BREAK_SYSTEM_PACKAGES=1 $(MAKE) install-pydeps-test $(MAKE) print-sysinfo mkdir -p .tests - cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs psutil.tests + cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs ../tests ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index bf03cdb731..bd2b7b4b2b 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -18,14 +18,6 @@ Once you have a compiler installed run: make install make test -- If you don't have the source code, and just want to test psutil installation. - This will work also if ``pytest`` module is not installed (e.g. production - environments) by using unittest's test runner: - -.. code-block:: bash - - python3 -m psutil.tests - - ``make`` (and the accompanying `Makefile`_) is the designated tool to build, install, run tests and do pretty much anything that involves development. This also includes Windows, meaning that you can run `make.bat command` @@ -49,9 +41,9 @@ Once you have a compiler installed run: .. code-block:: bash - make test ARGS=psutil/tests/test_system.py # UNIX + make test ARGS=tests/test_system.py # UNIX - set ARGS=psutil/tests/test_system.py && make test # Windows + set ARGS=tests/test_system.py && make test # Windows - Do not use ``sudo``. ``make install`` installs psutil as a limited user in "edit" / development mode, meaning you can edit psutil code on the fly while @@ -92,8 +84,8 @@ Code organization psutil/_ps{platform}.py # platform-specific python wrapper psutil/_psutil_{platform}.c # platform-specific C extension psutil/arch/{platform}/*.c # platform-specific C extension - psutil/tests/test_process|system.py # main test suite - psutil/tests/test_{platform}.py # platform-specific test suite + tests/test_process|system.py # main test suite + tests/test_{platform}.py # platform-specific test suite Adding a new API ---------------- @@ -105,10 +97,10 @@ Typically, this is what you do: (e.g. `psutil/_pslinux.py`_). - If the change requires C code, write the C implementation in ``psutil/arch/{platform}/file.c`` (e.g. `psutil/arch/linux/cpu.c`). -- Write a generic test in `psutil/tests/test_system.py`_ or - `psutil/tests/test_process.py`_. +- Write a generic test in `tests/test_system.py`_ or + `tests/test_process.py`_. - If possible, write a platform-specific test in - ``psutil/tests/test_{platform}.py`` (e.g. `psutil/tests/test_linux.py`_). + ``tests/test_{platform}.py`` (e.g. `tests/test_linux.py`_). This usually means testing the return value of the new API against a system CLI tool. - Update the doc in ``docs/index.py``. @@ -151,6 +143,6 @@ Documentation .. _`psutil/__init__.py`: https://github.com/giampaolo/psutil/blob/master/psutil/__init__.py .. _`psutil/_pslinux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_pslinux.py .. _`psutil/_psutil_linux.c`: https://github.com/giampaolo/psutil/blob/master/psutil/_psutil_linux.c -.. _`psutil/tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_linux.py -.. _`psutil/tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_process.py -.. _`psutil/tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/psutil/tests/test_system.py +.. _`tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_linux.py +.. _`tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_process.py +.. _`tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_system.py diff --git a/docs/index.rst b/docs/index.rst index 51d65d452c..f11aa000d1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2624,7 +2624,7 @@ Running tests :: - $ python3 -m psutil.tests + $ make test Debug mode ========== diff --git a/make.bat b/make.bat index 3589b91bf8..c5748f95e6 100644 --- a/make.bat +++ b/make.bat @@ -13,7 +13,7 @@ rem To compile for a specific Python version run: rem set PYTHON=C:\Python34\python.exe & make.bat build rem rem To run a specific test: -rem set ARGS=psutil/tests/test_system.py::TestMemoryAPIs::test_virtual_memory && make.bat test +rem set ARGS=-k tests/test_system.py && make.bat test rem ========================================================================== if "%PYTHON%" == "" ( diff --git a/psutil/__init__.py b/psutil/__init__.py index 9251770911..ee595599cd 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -203,7 +203,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.1.3" +__version__ = "7.2.0" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst deleted file mode 100644 index 8e5b6e7d84..0000000000 --- a/psutil/tests/README.rst +++ /dev/null @@ -1,35 +0,0 @@ -Instructions for running tests -============================== - -There are 2 ways of running tests. If you have the source code: - -.. code-block:: bash - - make install-pydeps-test # install pytest - make test - -If you don't have the source code, and just want to test psutil installation. -This will work also if ``pytest`` module is not installed (e.g. production -environments) by using unittest's test runner: - -.. code-block:: bash - - python -m psutil.tests - -To run tests in parallel (faster): - -.. code-block:: bash - - make test-parallel - -Run a specific test: - -.. code-block:: bash - - make test ARGS=psutil.tests.test_system.TestDiskAPIs - -Test C extension memory leaks: - -.. code-block:: bash - - make test-memleaks diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py deleted file mode 100644 index d20e48bc5f..0000000000 --- a/psutil/tests/__main__.py +++ /dev/null @@ -1,13 +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. - -"""Run unit tests. This is invoked by: -$ python3 -m psutil.tests. -""" - -import sys - -from psutil.tests import pytest - -sys.exit(pytest.main()) diff --git a/pyproject.toml b/pyproject.toml index 35f93c8205..3e67c958d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,8 +105,8 @@ ignore = [ # T203 == pprint() # TRY003 == raise-vanilla-args ".github/workflows/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] -"psutil/tests/*" = ["B904", "EM101", "EM102", "EM103", "PLC1901", "TRY003"] -"psutil/tests/test_sudo.py" = ["PT009"] +"tests/*" = ["B904", "EM101", "EM102", "EM103", "PLC1901", "TRY003"] +"tests/test_sudo.py" = ["PT009"] "scripts/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] "scripts/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "setup.py" = [ @@ -150,8 +150,8 @@ exclude_lines = [ "raise NotImplementedError", ] omit = [ - "psutil/tests/*", "setup.py", + "tests/*", ] [tool.pylint.messages_control] @@ -195,8 +195,8 @@ disable = [ [tool.vulture] exclude = [ "docs/conf.py", - "psutil/tests/", "scripts/internal/winmake.py", + "tests/", ] ignore_decorators = [ "@_common.deprecated_method", @@ -256,7 +256,7 @@ addopts = ''' -p subtests -p xdist ''' -testpaths = ["psutil/tests/"] +testpaths = ["tests/"] [build-system] build-backend = "setuptools.build_meta" diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py index 08e9618822..589a329631 100755 --- a/scripts/internal/download_wheels.py +++ b/scripts/internal/download_wheels.py @@ -17,13 +17,13 @@ import argparse import json import os +import shutil import sys import zipfile import requests from psutil._common import bytes2human -from psutil.tests import safe_rmpath USER = "giampaolo" PROJECT = "psutil" @@ -32,6 +32,17 @@ TIMEOUT = 30 +def safe_rmpath(path): + """Convenience function for removing temporary test files or dirs.""" + if os.path.isdir(path): + shutil.rmtree(path) + else: + try: + os.remove(path) + except FileNotFoundError: + pass + + def get_artifacts(): base_url = f"https://api.github.com/repos/{USER}/{PROJECT}" url = base_url + "/actions/artifacts" diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py index 0ccd2c609a..c2889fac9e 100755 --- a/scripts/internal/print_sysinfo.py +++ b/scripts/internal/print_sysinfo.py @@ -49,7 +49,7 @@ def import_module_by_path(path): tests_init = os.path.realpath( - os.path.join(HERE, "..", "..", "psutil", "tests", "__init__.py") + os.path.join(HERE, "..", "..", "tests", "__init__.py") ) tests_init_mod = import_module_by_path(tests_init) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index acfe114c3c..84064d8028 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -12,7 +12,7 @@ To run a specific test: - set ARGS=psutil/tests/test_system.py && make.bat test + set ARGS=-k tests/test_system.py && make.bat test """ import fnmatch @@ -289,10 +289,17 @@ def install_pydeps_dev(): @clicmd def test(args=ARGS): """Run tests.""" - sh( - [PYTHON, "-m", "pytest", "--ignore=psutil/tests/test_memleaks.py"] - + args - ) + # Disable pytest cache on Windows because we usually get + # PermissionDenied on start. Related to network drives (e.g. Z:\). + sh([ + PYTHON, + "-m", + "pytest", + "-p", + "no:cacheprovider", + "--ignore=tests/test_memleaks.py", + *args, + ]) @clicmd diff --git a/setup.py b/setup.py index 3020019178..dbd6718041 100755 --- a/setup.py +++ b/setup.py @@ -481,7 +481,7 @@ def main(): url='https://github.com/giampaolo/psutil', platforms='Platform Independent', license='BSD-3-Clause', - packages=['psutil', 'psutil.tests'], + packages=['psutil'], ext_modules=[ext], options=options, classifiers=[ diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..13e1cda0ba --- /dev/null +++ b/tests/README.md @@ -0,0 +1,41 @@ +# Instructions for running tests + +Install deps (e.g. pytest): + +.. code-block:: bash + + make install-pydeps-test + +Run tests: + +.. code-block:: bash + + make test + +Run tests in parallel (faster): + +.. code-block:: bash + + make test-parallel + +Run a specific test: + +.. code-block:: bash + + make test ARGS=tests.test_system.TestDiskAPIs + +Test C extension memory leaks: + +.. code-block:: bash + + make test-memleaks + +# Running tests on Windows + +Same as above, just replace `make` with `make.bat`, e.g.: + +.. code-block:: bash + + make.bat install-pydeps-test + make.bat test + set ARGS=-k tests.test_system.TestDiskAPIs && make.bat test diff --git a/psutil/tests/__init__.py b/tests/__init__.py similarity index 99% rename from psutil/tests/__init__.py rename to tests/__init__.py index 0cade49cce..7f92b8ab27 100644 --- a/psutil/tests/__init__.py +++ b/tests/__init__.py @@ -176,7 +176,7 @@ def macos_version(): # --- paths ROOT_DIR = os.environ.get("PSUTIL_ROOT_DIR") or os.path.realpath( - os.path.join(os.path.dirname(__file__), "..", "..") + os.path.join(os.path.dirname(__file__), "..") ) SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') HERE = os.path.realpath(os.path.dirname(__file__)) @@ -279,9 +279,7 @@ def attempt(exe): class fake_pytest: """A class that mimics some basic pytest APIs. This is meant for when unit tests are run in production, where pytest may not be - installed. Still, the user can test psutil installation via: - - $ python3 -m psutil.tests + installed. """ @staticmethod @@ -458,8 +456,16 @@ def spawn_subproc(cmd=None, **kwds): # 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) + # CREATE_NO_WINDOW = 0x8000000 + # kwds.setdefault("creationflags", CREATE_NO_WINDOW) + + # New: hopefully this should achieve the same and not suppress + # stderr. + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + kwds.setdefault("startupinfo", startupinfo) + if cmd is None: testfn = get_testfn(dir=os.getcwd()) try: @@ -993,7 +999,7 @@ class PsutilTestCase(unittest.TestCase): def __str__(self): fqmod = self.__class__.__module__ if not fqmod.startswith('psutil.'): - fqmod = 'psutil.tests.' + fqmod + fqmod = 'tests.' + fqmod return "{}.{}.{}".format( fqmod, self.__class__.__name__, diff --git a/psutil/tests/test_aix.py b/tests/test_aix.py similarity index 93% rename from psutil/tests/test_aix.py rename to tests/test_aix.py index 10934c12d4..fcc50146f7 100755 --- a/psutil/tests/test_aix.py +++ b/tests/test_aix.py @@ -12,9 +12,10 @@ import psutil from psutil import AIX -from psutil.tests import PsutilTestCase -from psutil.tests import pytest -from psutil.tests import sh + +from . import PsutilTestCase +from . import pytest +from . import sh @pytest.mark.skipif(not AIX, reason="AIX only") @@ -44,9 +45,9 @@ def test_virtual_memory(self): 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 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 assert psutil_result.total == total assert abs(psutil_result.used - used) < TOLERANCE_SYS_MEM diff --git a/psutil/tests/test_bsd.py b/tests/test_bsd.py similarity index 98% rename from psutil/tests/test_bsd.py rename to tests/test_bsd.py index 6727bdffd1..f2169a2b2e 100755 --- a/psutil/tests/test_bsd.py +++ b/tests/test_bsd.py @@ -20,14 +20,15 @@ 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 pytest -from psutil.tests import retry_on_failure -from psutil.tests import sh -from psutil.tests import spawn_subproc -from psutil.tests import terminate + +from . import HAS_BATTERY +from . import TOLERANCE_SYS_MEM +from . import PsutilTestCase +from . import pytest +from . import retry_on_failure +from . import sh +from . import spawn_subproc +from . import terminate if BSD: PAGESIZE = psutil._psplatform.cext.getpagesize() diff --git a/psutil/tests/test_connections.py b/tests/test_connections.py similarity index 96% rename from psutil/tests/test_connections.py rename to tests/test_connections.py index 6a9778e0a4..55470060e8 100755 --- a/psutil/tests/test_connections.py +++ b/tests/test_connections.py @@ -25,22 +25,24 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import supports_ipv6 -from psutil.tests import AF_UNIX -from psutil.tests import HAS_NET_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_net_connections -from psutil.tests import pytest -from psutil.tests import reap_children -from psutil.tests import retry_on_failure -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 + +from . import AF_UNIX +from . import HAS_NET_CONNECTIONS_UNIX +from . import ROOT_DIR +from . import SKIP_SYSCONS +from . import PsutilTestCase +from . import bind_socket +from . import bind_unix_socket +from . import check_connection_ntuple +from . import create_sockets +from . import filter_proc_net_connections +from . import pytest +from . import reap_children +from . import retry_on_failure +from . import skip_on_access_denied +from . import tcp_socketpair +from . import unix_socketpair +from . import wait_for_file SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) @@ -525,8 +527,9 @@ def test_multi_sockets_procs(self): fname = self.get_testfn() fnames.append(fname) src = textwrap.dedent(f"""\ - import time, os - from psutil.tests import create_sockets + import time, os, sys + sys.path.insert(0, r'{ROOT_DIR}') + from tests import create_sockets with create_sockets(): with open(r'{fname}', 'w') as f: f.write("hello") diff --git a/psutil/tests/test_contracts.py b/tests/test_contracts.py similarity index 95% rename from psutil/tests/test_contracts.py rename to tests/test_contracts.py index f174bdb917..aaf0e383bd 100755 --- a/psutil/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -21,19 +21,20 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS -from psutil.tests import AARCH64 -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 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 -from psutil.tests import pytest + +from . import AARCH64 +from . import GITHUB_ACTIONS +from . import HAS_CPU_FREQ +from . import HAS_NET_IO_COUNTERS +from . import HAS_SENSORS_FANS +from . import HAS_SENSORS_TEMPERATURES +from . import SKIP_SYSCONS +from . import PsutilTestCase +from . import create_sockets +from . import enum +from . import is_namedtuple +from . import kernel_version +from . import pytest # =================================================================== # --- APIs availability diff --git a/psutil/tests/test_linux.py b/tests/test_linux.py similarity index 99% rename from psutil/tests/test_linux.py rename to tests/test_linux.py index 8db2b9f652..aec75aa13f 100755 --- a/psutil/tests/test_linux.py +++ b/tests/test_linux.py @@ -24,25 +24,26 @@ import psutil from psutil import LINUX -from psutil.tests import AARCH64 -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 RISCV64 -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 pytest -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 . import AARCH64 +from . import GITHUB_ACTIONS +from . import GLOBAL_TIMEOUT +from . import HAS_BATTERY +from . import HAS_CPU_FREQ +from . import HAS_GETLOADAVG +from . import HAS_RLIMIT +from . import RISCV64 +from . import TOLERANCE_DISK_USAGE +from . import TOLERANCE_SYS_MEM +from . import PsutilTestCase +from . import ThreadTask +from . import call_until +from . import pytest +from . import reload_module +from . import retry_on_failure +from . import safe_rmpath +from . import sh +from . import skip_on_not_implemented if LINUX: from psutil._pslinux import CLOCK_TICKS diff --git a/psutil/tests/test_memleaks.py b/tests/test_memleaks.py similarity index 94% rename from psutil/tests/test_memleaks.py rename to tests/test_memleaks.py index 306b1c32f6..3a31b8ee76 100755 --- a/psutil/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -24,28 +24,29 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS -from psutil.tests import AARCH64 -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 pytest -from psutil.tests import skip_on_access_denied -from psutil.tests import spawn_subproc -from psutil.tests import system_namespace -from psutil.tests import terminate + +from . import AARCH64 +from . import HAS_CPU_AFFINITY +from . import HAS_CPU_FREQ +from . import HAS_ENVIRON +from . import HAS_IONICE +from . import HAS_MEMORY_MAPS +from . import HAS_NET_IO_COUNTERS +from . import HAS_PROC_CPU_NUM +from . import HAS_PROC_IO_COUNTERS +from . import HAS_RLIMIT +from . import HAS_SENSORS_BATTERY +from . import HAS_SENSORS_FANS +from . import HAS_SENSORS_TEMPERATURES +from . import TestMemoryLeak +from . import create_sockets +from . import get_testfn +from . import process_namespace +from . import pytest +from . import skip_on_access_denied +from . import spawn_subproc +from . import system_namespace +from . import terminate cext = psutil._psplatform.cext thisproc = psutil.Process() diff --git a/psutil/tests/test_misc.py b/tests/test_misc.py similarity index 99% rename from psutil/tests/test_misc.py rename to tests/test_misc.py index 0ecae2f3b7..e9f2ee8c66 100755 --- a/psutil/tests/test_misc.py +++ b/tests/test_misc.py @@ -27,12 +27,13 @@ from psutil._common import parse_environ_block from psutil._common import supports_ipv6 from psutil._common import wrap_numbers -from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import PsutilTestCase -from psutil.tests import process_namespace -from psutil.tests import pytest -from psutil.tests import reload_module -from psutil.tests import system_namespace + +from . import HAS_NET_IO_COUNTERS +from . import PsutilTestCase +from . import process_namespace +from . import pytest +from . import reload_module +from . import system_namespace # =================================================================== # --- Test classes' repr(), str(), ... diff --git a/psutil/tests/test_osx.py b/tests/test_osx.py similarity index 93% rename from psutil/tests/test_osx.py rename to tests/test_osx.py index 33833c3ed0..a5bb79ce90 100755 --- a/psutil/tests/test_osx.py +++ b/tests/test_osx.py @@ -11,17 +11,18 @@ import psutil from psutil import MACOS -from psutil.tests import AARCH64 -from psutil.tests import CI_TESTING -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 pytest -from psutil.tests import retry_on_failure -from psutil.tests import sh -from psutil.tests import spawn_subproc -from psutil.tests import terminate + +from . import AARCH64 +from . import CI_TESTING +from . import HAS_BATTERY +from . import TOLERANCE_DISK_USAGE +from . import TOLERANCE_SYS_MEM +from . import PsutilTestCase +from . import pytest +from . import retry_on_failure +from . import sh +from . import spawn_subproc +from . import terminate def sysctl(cmdline): diff --git a/psutil/tests/test_posix.py b/tests/test_posix.py similarity index 97% rename from psutil/tests/test_posix.py rename to tests/test_posix.py index 26227cd398..2cc9dbd84c 100755 --- a/psutil/tests/test_posix.py +++ b/tests/test_posix.py @@ -23,16 +23,17 @@ from psutil import OPENBSD from psutil import POSIX from psutil import SUNOS -from psutil.tests import AARCH64 -from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import PYTHON_EXE -from psutil.tests import PsutilTestCase -from psutil.tests import pytest -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_subproc -from psutil.tests import terminate + +from . import AARCH64 +from . import HAS_NET_IO_COUNTERS +from . import PYTHON_EXE +from . import PsutilTestCase +from . import pytest +from . import retry_on_failure +from . import sh +from . import skip_on_access_denied +from . import spawn_subproc +from . import terminate if POSIX: import mmap diff --git a/psutil/tests/test_process.py b/tests/test_process.py similarity index 98% rename from psutil/tests/test_process.py rename to tests/test_process.py index fa03150e3e..76cb8d949b 100755 --- a/psutil/tests/test_process.py +++ b/tests/test_process.py @@ -34,35 +34,36 @@ from psutil import POSIX from psutil import WINDOWS from psutil._common import open_text -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 process_namespace -from psutil.tests import pytest -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 + +from . import CI_TESTING +from . import GITHUB_ACTIONS +from . import GLOBAL_TIMEOUT +from . import HAS_CPU_AFFINITY +from . import HAS_ENVIRON +from . import HAS_IONICE +from . import HAS_MEMORY_MAPS +from . import HAS_PROC_CPU_NUM +from . import HAS_PROC_IO_COUNTERS +from . import HAS_RLIMIT +from . import HAS_THREADS +from . import MACOS_11PLUS +from . import PYPY +from . import PYTHON_EXE +from . import PYTHON_EXE_ENV +from . import PsutilTestCase +from . import ThreadTask +from . import call_until +from . import copyload_shared_lib +from . import create_c_exe +from . import create_py_exe +from . import process_namespace +from . import pytest +from . import reap_children +from . import retry_on_failure +from . import sh +from . import skip_on_access_denied +from . import skip_on_not_implemented +from . import wait_for_pid # =================================================================== # --- psutil.Process class tests diff --git a/psutil/tests/test_process_all.py b/tests/test_process_all.py similarity index 97% rename from psutil/tests/test_process_all.py rename to tests/test_process_all.py index a742391c7a..62f19f7d48 100755 --- a/psutil/tests/test_process_all.py +++ b/tests/test_process_all.py @@ -27,16 +27,17 @@ from psutil import OSX from psutil import POSIX from psutil import WINDOWS -from psutil.tests import CI_TESTING -from psutil.tests import PYTEST_PARALLEL -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 pytest + +from . import CI_TESTING +from . import PYTEST_PARALLEL +from . import VALID_PROC_STATUSES +from . import PsutilTestCase +from . import check_connection_ntuple +from . import create_sockets +from . import is_namedtuple +from . import is_win_secure_system_proc +from . import process_namespace +from . import pytest # Cuts the time in half, but (e.g.) on macOS the process pool stays # alive after join() (multiprocessing bug?), messing up other tests. @@ -121,7 +122,7 @@ def tearDown(self): 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 + from tests.test_process_all import proc_info if USE_PROC_POOL: return self.pool.imap_unordered(proc_info, psutil.pids()) diff --git a/psutil/tests/test_scripts.py b/tests/test_scripts.py similarity index 93% rename from psutil/tests/test_scripts.py rename to tests/test_scripts.py index 6ef78b6303..cea5039eab 100755 --- a/psutil/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -17,20 +17,21 @@ from psutil import LINUX from psutil import POSIX from psutil import WINDOWS -from psutil.tests import CI_TESTING -from psutil.tests import HAS_BATTERY -from psutil.tests import HAS_MEMORY_MAPS -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 ROOT_DIR -from psutil.tests import SCRIPTS_DIR -from psutil.tests import PsutilTestCase -from psutil.tests import import_module_by_path -from psutil.tests import psutil -from psutil.tests import sh + +from . import CI_TESTING +from . import HAS_BATTERY +from . import HAS_MEMORY_MAPS +from . import HAS_SENSORS_BATTERY +from . import HAS_SENSORS_FANS +from . import HAS_SENSORS_TEMPERATURES +from . import PYTHON_EXE +from . import PYTHON_EXE_ENV +from . import ROOT_DIR +from . import SCRIPTS_DIR +from . import PsutilTestCase +from . import import_module_by_path +from . import psutil +from . import sh INTERNAL_SCRIPTS_DIR = os.path.join(SCRIPTS_DIR, "internal") SETUP_PY = os.path.join(ROOT_DIR, 'setup.py') diff --git a/psutil/tests/test_sudo.py b/tests/test_sudo.py similarity index 98% rename from psutil/tests/test_sudo.py rename to tests/test_sudo.py index 034b763faf..ec3f9ef5a9 100755 --- a/psutil/tests/test_sudo.py +++ b/tests/test_sudo.py @@ -20,8 +20,9 @@ from psutil import LINUX from psutil import OPENBSD from psutil import WINDOWS -from psutil.tests import CI_TESTING -from psutil.tests import PsutilTestCase + +from . import CI_TESTING +from . import PsutilTestCase def get_systime(): diff --git a/psutil/tests/test_sunos.py b/tests/test_sunos.py similarity index 91% rename from psutil/tests/test_sunos.py rename to tests/test_sunos.py index b5d9d353b9..2a5c5535e9 100755 --- a/psutil/tests/test_sunos.py +++ b/tests/test_sunos.py @@ -10,9 +10,10 @@ import psutil from psutil import SUNOS -from psutil.tests import PsutilTestCase -from psutil.tests import pytest -from psutil.tests import sh + +from . import PsutilTestCase +from . import pytest +from . import sh @pytest.mark.skipif(not SUNOS, reason="SUNOS only") diff --git a/psutil/tests/test_system.py b/tests/test_system.py similarity index 97% rename from psutil/tests/test_system.py rename to tests/test_system.py index 0cd56422c0..79325c7599 100755 --- a/psutil/tests/test_system.py +++ b/tests/test_system.py @@ -30,25 +30,26 @@ from psutil import SUNOS from psutil import WINDOWS from psutil._common import broadcast_addr -from psutil.tests import AARCH64 -from psutil.tests import ASCII_FS -from psutil.tests import CI_TESTING -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 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 pytest -from psutil.tests import retry_on_failure + +from . import AARCH64 +from . import ASCII_FS +from . import CI_TESTING +from . import GITHUB_ACTIONS +from . import GLOBAL_TIMEOUT +from . import HAS_BATTERY +from . import HAS_CPU_FREQ +from . import HAS_GETLOADAVG +from . import HAS_NET_IO_COUNTERS +from . import HAS_SENSORS_BATTERY +from . import HAS_SENSORS_FANS +from . import HAS_SENSORS_TEMPERATURES +from . import MACOS_12PLUS +from . import PYPY +from . import UNICODE_SUFFIX +from . import PsutilTestCase +from . import check_net_address +from . import pytest +from . import retry_on_failure # =================================================================== # --- System-related API tests diff --git a/psutil/tests/test_testutils.py b/tests/test_testutils.py similarity index 91% rename from psutil/tests/test_testutils.py rename to tests/test_testutils.py index a8814139a0..f36057de12 100755 --- a/psutil/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -4,7 +4,7 @@ # 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).""" +"""Tests for testing utils.""" import collections import contextlib @@ -20,43 +20,44 @@ from unittest import mock import psutil -import psutil.tests +import 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_NET_CONNECTIONS_UNIX -from psutil.tests import HERE -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 fake_pytest -from psutil.tests import filter_proc_net_connections -from psutil.tests import get_free_port -from psutil.tests import is_namedtuple -from psutil.tests import process_namespace -from psutil.tests import pytest -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 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 + +from . import CI_TESTING +from . import COVERAGE +from . import HAS_NET_CONNECTIONS_UNIX +from . import HERE +from . import PYTHON_EXE +from . import PYTHON_EXE_ENV +from . import PsutilTestCase +from . import TestMemoryLeak +from . import bind_socket +from . import bind_unix_socket +from . import call_until +from . import chdir +from . import create_sockets +from . import fake_pytest +from . import filter_proc_net_connections +from . import get_free_port +from . import is_namedtuple +from . import process_namespace +from . import pytest +from . import reap_children +from . import retry +from . import retry_on_failure +from . import safe_mkdir +from . import safe_rmpath +from . import system_namespace +from . import tcp_socketpair +from . import terminate +from . import unix_socketpair +from . import wait_for_file +from . import wait_for_pid # =================================================================== # --- Unit tests for test utilities. @@ -136,7 +137,7 @@ 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])): + with mock.patch('tests.retry.__iter__', return_value=iter([0])): with pytest.raises(psutil.NoSuchProcess): wait_for_pid(nopid) @@ -156,7 +157,7 @@ def test_wait_for_file_empty(self): def test_wait_for_file_no_file(self): testfn = self.get_testfn() - with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): + with mock.patch('tests.retry.__iter__', return_value=iter([0])): with pytest.raises(OSError): wait_for_file(testfn) @@ -202,7 +203,7 @@ def test_safe_rmpath(self): assert not os.path.exists(testfn) # test other exceptions are raised with mock.patch( - 'psutil.tests.os.stat', side_effect=OSError(errno.EINVAL, "") + 'tests.os.stat', side_effect=OSError(errno.EINVAL, "") ) as m: with pytest.raises(OSError): safe_rmpath(testfn) @@ -224,8 +225,8 @@ def test_reap_children(self): assert p.is_running() reap_children() assert not p.is_running() - assert not psutil.tests._pids_started - assert not psutil.tests._subprocesses_started + assert not tests._pids_started + assert not tests._subprocesses_started def test_spawn_children_pair(self): child, grandchild = self.spawn_children_pair() @@ -530,7 +531,7 @@ class TestCase(unittest.TestCase): def test_passed(self): pass """).lstrip()) - with mock.patch.object(psutil.tests, "HERE", tmpdir): + with mock.patch.object(tests, "HERE", tmpdir): with contextlib.redirect_stderr(io.StringIO()): suite = fake_pytest.main() assert suite.countTestCases() == 1 diff --git a/psutil/tests/test_unicode.py b/tests/test_unicode.py similarity index 92% rename from psutil/tests/test_unicode.py rename to tests/test_unicode.py index 8c8e3d605e..d9abaeb9ea 100755 --- a/psutil/tests/test_unicode.py +++ b/tests/test_unicode.py @@ -78,27 +78,28 @@ from psutil import OPENBSD from psutil import POSIX from psutil import WINDOWS -from psutil.tests import ASCII_FS -from psutil.tests import CI_TESTING -from psutil.tests import HAS_ENVIRON -from psutil.tests import HAS_MEMORY_MAPS -from psutil.tests import HAS_NET_CONNECTIONS_UNIX -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 pytest -from psutil.tests import safe_mkdir -from psutil.tests import safe_rmpath -from psutil.tests import skip_on_access_denied -from psutil.tests import spawn_subproc -from psutil.tests import terminate + +from . import ASCII_FS +from . import CI_TESTING +from . import HAS_ENVIRON +from . import HAS_MEMORY_MAPS +from . import HAS_NET_CONNECTIONS_UNIX +from . import INVALID_UNICODE_SUFFIX +from . import PYPY +from . import TESTFN_PREFIX +from . import UNICODE_SUFFIX +from . import PsutilTestCase +from . import bind_unix_socket +from . import chdir +from . import copyload_shared_lib +from . import create_py_exe +from . import get_testfn +from . import pytest +from . import safe_mkdir +from . import safe_rmpath +from . import skip_on_access_denied +from . import spawn_subproc +from . import terminate def try_unicode(suffix): diff --git a/psutil/tests/test_windows.py b/tests/test_windows.py similarity index 98% rename from psutil/tests/test_windows.py rename to tests/test_windows.py index 203766fa70..ae5991f844 100755 --- a/psutil/tests/test_windows.py +++ b/tests/test_windows.py @@ -22,18 +22,19 @@ import psutil from psutil import WINDOWS -from psutil.tests import GITHUB_ACTIONS -from psutil.tests import HAS_BATTERY -from psutil.tests import IS_64BIT -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 pytest -from psutil.tests import retry_on_failure -from psutil.tests import sh -from psutil.tests import spawn_subproc -from psutil.tests import terminate + +from . import GITHUB_ACTIONS +from . import HAS_BATTERY +from . import IS_64BIT +from . import PYPY +from . import TOLERANCE_DISK_USAGE +from . import TOLERANCE_SYS_MEM +from . import PsutilTestCase +from . import pytest +from . import retry_on_failure +from . import sh +from . import spawn_subproc +from . import terminate if WINDOWS and not PYPY: with warnings.catch_warnings(): @@ -129,7 +130,7 @@ def test_cpu_count_vs_cpu_times(self): def test_cpu_freq(self): w = wmi.WMI() proc = w.Win32_Processor()[0] - assert proc.CurrentClockSpeed == psutil.cpu_freq().current + assert abs(proc.CurrentClockSpeed - psutil.cpu_freq().current) < 100 assert proc.MaxClockSpeed == psutil.cpu_freq().max From 055ad0f1eb87280d32bd33e30fd21405866e83e6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 3 Nov 2025 21:26:00 +0100 Subject: [PATCH 1439/1714] Rm fake_pytest (re. to #2680) After unit tests are no longer installed (#2680) there's no point in keeping fake_pytest class. Let's just remove it. --- tests/__init__.py | 104 +-------------------------------- tests/test_testutils.py | 125 ---------------------------------------- 2 files changed, 1 insertion(+), 228 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 7f92b8ab27..376ea34fe9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -81,7 +81,7 @@ 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', 'process_namespace', 'system_namespace', - 'is_win_secure_system_proc', 'fake_pytest', + 'is_win_secure_system_proc', # fs utils 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', # os @@ -271,108 +271,6 @@ def attempt(exe): _pids_started = set() -# =================================================================== -# --- fake pytest -# =================================================================== - - -class fake_pytest: - """A class that mimics some basic pytest APIs. This is meant for - when unit tests are run in production, where pytest may not be - installed. - """ - - @staticmethod - def _warn_on_exit(): - def _warn_on_exit(): - warnings.warn( - "Fake pytest module was used. Test results may be inaccurate.", - UserWarning, - stacklevel=1, - ) - - atexit.register(_warn_on_exit) - - @staticmethod - def main(*args, **kw): # noqa: ARG004 - """Mimics pytest.main(). It has the same effect as running - `python3 -m unittest -v` from the project root directory. - """ - suite = unittest.TestLoader().discover(HERE) - unittest.TextTestRunner(verbosity=2).run(suite) - return suite - - @staticmethod - def raises(exc, match=None): - """Mimics `pytest.raises`.""" - - class ExceptionInfo: - _exc = None - - @property - def value(self): - return self._exc - - @contextlib.contextmanager - def context(exc, match=None): - einfo = ExceptionInfo() - try: - yield einfo - except exc as err: - if match and not re.search(match, str(err)): - msg = f'"{match}" does not match "{err}"' - raise AssertionError(msg) - einfo._exc = err - else: - raise AssertionError(f"{exc!r} not raised") - - return context(exc, match=match) - - @staticmethod - def warns(warning, match=None): - """Mimics `pytest.warns`.""" - if match: - return unittest.TestCase().assertWarnsRegex(warning, match) - return unittest.TestCase().assertWarns(warning) - - @staticmethod - def skip(reason=""): - """Mimics `unittest.SkipTest`.""" - raise unittest.SkipTest(reason) - - @staticmethod - def fail(reason=""): - """Mimics `pytest.fail`.""" - return unittest.TestCase().fail(reason) - - class mark: - - @staticmethod - def skipif(condition, reason=""): - """Mimics `@pytest.mark.skipif` decorator.""" - return unittest.skipIf(condition, reason) - - class xdist_group: - """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" - - def __init__(self, name=None): - pass - - def __call__(self, cls_or_meth): - return cls_or_meth - - -# to make pytest.fail() exception catchable -fake_pytest.fail.Exception = AssertionError - - -if pytest is None: - pytest = fake_pytest - # monkey patch future `import pytest` statements - sys.modules["pytest"] = fake_pytest - fake_pytest._warn_on_exit() - - # =================================================================== # --- threads # =================================================================== diff --git a/tests/test_testutils.py b/tests/test_testutils.py index f36057de12..1ba6fbb916 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -14,9 +14,6 @@ import socket import stat import subprocess -import textwrap -import unittest -import warnings from unittest import mock import psutil @@ -31,7 +28,6 @@ from . import CI_TESTING from . import COVERAGE from . import HAS_NET_CONNECTIONS_UNIX -from . import HERE from . import PYTHON_EXE from . import PYTHON_EXE_ENV from . import PsutilTestCase @@ -41,7 +37,6 @@ from . import call_until from . import chdir from . import create_sockets -from . import fake_pytest from . import filter_proc_net_connections from . import get_free_port from . import is_namedtuple @@ -448,126 +443,6 @@ def fun_2(): self.execute_w_exc(ZeroDivisionError, fun_2) -class TestFakePytest(PsutilTestCase): - def run_test_class(self, klass): - suite = unittest.TestSuite() - suite.addTest(klass) - # silence output - runner = unittest.TextTestRunner(stream=io.StringIO()) - result = runner.run(suite) - return result - - def test_raises(self): - with fake_pytest.raises(ZeroDivisionError) as cm: - 1 / 0 # noqa: B018 - assert isinstance(cm.value, ZeroDivisionError) - - with fake_pytest.raises(ValueError, match="foo") as cm: - raise ValueError("foo") - - try: - with fake_pytest.raises(ValueError, match="foo") as cm: - raise ValueError("bar") - except AssertionError as err: - assert str(err) == '"foo" does not match "bar"' # noqa: PT017 - else: - return pytest.fail("exception not raised") - - def test_mark(self): - @fake_pytest.mark.xdist_group(name="serial") - def foo(): - return 1 - - assert foo() == 1 - - @fake_pytest.mark.xdist_group(name="serial") - class Foo: - def bar(self): - return 1 - - assert Foo().bar() == 1 - - def test_skipif(self): - class TestCase(unittest.TestCase): - @fake_pytest.mark.skipif(True, reason="reason") - def foo(self): - assert 1 == 1 # noqa: PLR0133 - - result = self.run_test_class(TestCase("foo")) - assert result.wasSuccessful() - assert len(result.skipped) == 1 - assert result.skipped[0][1] == "reason" - - class TestCase(unittest.TestCase): - @fake_pytest.mark.skipif(False, reason="reason") - def foo(self): - assert 1 == 1 # noqa: PLR0133 - - result = self.run_test_class(TestCase("foo")) - assert result.wasSuccessful() - assert len(result.skipped) == 0 - - def test_skip(self): - class TestCase(unittest.TestCase): - def foo(self): - fake_pytest.skip("reason") - assert 1 == 0 # noqa: PLR0133 - - result = self.run_test_class(TestCase("foo")) - assert result.wasSuccessful() - assert len(result.skipped) == 1 - assert result.skipped[0][1] == "reason" - - def test_main(self): - tmpdir = self.get_testfn(dir=HERE) - os.mkdir(tmpdir) - with open(os.path.join(tmpdir, "__init__.py"), "w"): - pass - with open(os.path.join(tmpdir, "test_file.py"), "w") as f: - f.write(textwrap.dedent("""\ - import unittest - - class TestCase(unittest.TestCase): - def test_passed(self): - pass - """).lstrip()) - with mock.patch.object(tests, "HERE", tmpdir): - with contextlib.redirect_stderr(io.StringIO()): - suite = fake_pytest.main() - assert suite.countTestCases() == 1 - - def test_warns(self): - # success - with fake_pytest.warns(UserWarning): - warnings.warn("foo", UserWarning, stacklevel=1) - - # failure - try: - with fake_pytest.warns(UserWarning): - warnings.warn("foo", DeprecationWarning, stacklevel=1) - except AssertionError: - pass - else: - return pytest.fail("exception not raised") - - # match success - with fake_pytest.warns(UserWarning, match="foo"): - warnings.warn("foo", UserWarning, stacklevel=1) - - # match failure - try: - with fake_pytest.warns(UserWarning, match="foo"): - warnings.warn("bar", UserWarning, stacklevel=1) - except AssertionError: - pass - else: - return pytest.fail("exception not raised") - - def test_fail(self): - with fake_pytest.raises(fake_pytest.fail.Exception): - raise fake_pytest.fail("reason") - - class TestTestingUtils(PsutilTestCase): def test_process_namespace(self): p = psutil.Process() From d91da4a06e4aea5c4ca6d6341ce884535f80587e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 4 Nov 2025 20:24:34 +0100 Subject: [PATCH 1440/1714] TestMemoryLeak: track RSS, VMS, and USS memory instead of just USS --- tests/__init__.py | 99 ++++++++++++++++++++++++++--------------- tests/test_testutils.py | 6 ++- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 376ea34fe9..3a6ef9cb45 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1058,8 +1058,6 @@ def assert_in_pids(proc): # assert proc == ppid(), os.getpid() -@pytest.mark.skipif(PYPY, reason="unreliable on PYPY") -@pytest.mark.xdist_group(name="serial") class TestMemoryLeak(PsutilTestCase): """Test framework class for detecting function memory leaks, typically functions implemented in C which forgot to free() memory @@ -1073,12 +1071,9 @@ class TestMemoryLeak(PsutilTestCase): 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. + We currently check RSS, VMS and USS [1] memory. mallinfo() on Linux + and _heapwalk() on Windows should give even more precision [2], but + at the moment they are not implemented. PyPy appears to be completely unstable for this framework, probably because of its JIT, so tests on PYPY are skipped. @@ -1086,9 +1081,11 @@ class TestMemoryLeak(PsutilTestCase): Usage: class TestLeaks(psutil.tests.TestMemoryLeak): - def test_fun(self): self.execute(some_function) + + [1] https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python + [2] https://github.com/giampaolo/psutil/issues/1275 """ # Configurable class attrs. @@ -1097,6 +1094,7 @@ def test_fun(self): tolerance = 0 # memory retries = 10 if CI_TESTING else 5 verbose = True + _thisproc = psutil.Process() _psutil_debug_orig = bool(os.getenv('PSUTIL_DEBUG')) @@ -1108,11 +1106,19 @@ def setUpClass(cls): def tearDownClass(cls): psutil._set_debug(cls._psutil_debug_orig) + def _log(self, msg): + if self.verbose: + print_color(msg, color="yellow", file=sys.stderr) + + # --- getters + 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) + return { + "rss": mem.rss, + "vms": mem.vms, + "uss": getattr(mem, "uss", 0), + } def _get_num_fds(self): if POSIX: @@ -1120,9 +1126,7 @@ def _get_num_fds(self): else: return self._thisproc.num_handles() - def _log(self, msg): - if self.verbose: - print_color(msg, color="yellow", file=sys.stderr) + # --- checkers def _check_fds(self, fun): """Makes sure num_fds() (POSIX) or num_handles() (Windows) does @@ -1147,8 +1151,8 @@ def _check_fds(self, fun): return pytest.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. + """Get memory samples (rss, vms, uss) before and after calling + fun repeatedly, and return the diffs as a dict. """ gc.collect(generation=1) mem1 = self._get_mem() @@ -1158,34 +1162,53 @@ def _call_ntimes(self, fun, times): gc.collect(generation=1) mem2 = self._get_mem() assert gc.garbage == [] - diff = mem2 - mem1 # can also be negative - return diff + diffs = {k: mem2[k] - mem1[k] for k in mem1} + return diffs def _check_mem(self, fun, times, retries, tolerance): messages = [] - prev_mem = 0 + prev_mem = {} increase = times + b2h = functools.partial(bytes2human, format="%(value)i%(symbol)s") + for idx in range(1, retries + 1): - mem = self._call_ntimes(fun, times) - msg = "Run #{}: extra-mem={}, per-call={}, calls={}".format( - idx, - bytes2human(mem), - bytes2human(mem / times), - times, + diffs = self._call_ntimes(fun, times) + + # Filter only metrics with positive diffs (possible leaks). + positive_diffs = {k: v for k, v in diffs.items() if v > 0} + + # Build message only for those metrics. + if positive_diffs: + msg_parts = [ + f"{k}=+{b2h(v)}" for k, v in positive_diffs.items() + ] + msg = "Run #{}: {} (ncalls={}, avg-per-call=+{})".format( + idx, + ", ".join(msg_parts), + times, + b2h(sum(positive_diffs.values()) / times), + ) + if idx == 1: + msg = "\n" + msg + messages.append(msg) + self._log(msg) + + # Determine if memory stabilized or decreased. + success = all( + diffs.get(k, 0) <= tolerance + or diffs.get(k, 0) <= prev_mem.get(k, 0) + for k in diffs ) - messages.append(msg) - success = mem <= tolerance or mem <= prev_mem if success: - if idx > 1: - self._log(msg) + if idx > 1 and positive_diffs: + self._log("Memory stabilized (no further growth detected)") return None - else: - if idx == 1: - print() # noqa: T201 - self._log(msg) - times += increase - prev_mem = mem - return pytest.fail(". ".join(messages)) + + times += increase + prev_mem = diffs + + msg = "\n" + "\n".join(messages) + return pytest.fail(msg) # --- @@ -1202,6 +1225,7 @@ def execute( ) 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" @@ -1211,6 +1235,7 @@ def execute( 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) diff --git a/tests/test_testutils.py b/tests/test_testutils.py index 1ba6fbb916..b7fd5f1357 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -28,6 +28,7 @@ from . import CI_TESTING from . import COVERAGE from . import HAS_NET_CONNECTIONS_UNIX +from . import PYPY from . import PYTHON_EXE from . import PYTHON_EXE_ENV from . import PsutilTestCase @@ -364,6 +365,7 @@ def test_create_sockets(self): assert types[socket.SOCK_DGRAM] >= 2 +@pytest.mark.skipif(PYPY, reason="unreliable on PYPY") @pytest.mark.xdist_group(name="serial") class TestMemLeakClass(TestMemoryLeak): @retry_on_failure() @@ -398,7 +400,9 @@ def fun(ls=ls): try: # will consume around 60M in total - with pytest.raises(pytest.fail.Exception, match="extra-mem"): + with pytest.raises( + pytest.fail.Exception, match=rf"Run \#{TestMemoryLeak.retries}" + ): with contextlib.redirect_stdout( io.StringIO() ), contextlib.redirect_stderr(io.StringIO()): From 8d8fa5e2e991cfd39245c05bbaf460cbf7483d9b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Nov 2025 20:39:16 +0100 Subject: [PATCH 1441/1714] Make `psutil.test.MemoryLeakTestCase` public (#2682) This function has been used internally for a long time to check memory leaks in all psutil C functions, but it's generic enough that it's worth it to make it public and officially document it. MemoryLeakTestCase now lives under the new psutil.test namespace, which only defines this class. The leak detection logic now considers VMS, RSS, and USS instead of only USS, making it more reliable across different platforms and allocator behaviors. Next step is gonna be using also mallinfo on Linux (see #1275) to make it even more reliable. --- MANIFEST.in | 2 + README.rst | 11 ++ docs/index.rst | 75 ++++++++++++++ psutil/test/__init__.py | 7 ++ psutil/test/memleak.py | 220 ++++++++++++++++++++++++++++++++++++++++ tests/__init__.py | 206 +------------------------------------ tests/test_memleaks.py | 27 ++++- tests/test_testutils.py | 41 ++++---- 8 files changed, 363 insertions(+), 226 deletions(-) create mode 100644 psutil/test/__init__.py create mode 100644 psutil/test/memleak.py diff --git a/MANIFEST.in b/MANIFEST.in index 3d9bd3aedd..050d5d491f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -133,6 +133,8 @@ include psutil/arch/windows/services.c include psutil/arch/windows/socks.c include psutil/arch/windows/sys.c include psutil/arch/windows/wmi.c +include psutil/test/__init__.py +include psutil/test/memleak.py include pyproject.toml include scripts/battery.py include scripts/cpu_distribution.py diff --git a/README.rst b/README.rst index 9eb011aa0c..215cbcefef 100644 --- a/README.rst +++ b/README.rst @@ -454,6 +454,17 @@ Further process APIs >>> gone, alive = psutil.wait_procs(procs_list, timeout=3, callback=on_terminate) >>> +Detecting memory leaks in C functions +------------------------------------- + +.. code-block:: python + + from psutil.test import MemoryLeakTestCase + + class TestLeaks(MemoryLeakTestCase): + def test_fun(self): + self.execute(some_function) + Windows services ---------------- diff --git a/docs/index.rst b/docs/index.rst index f11aa000d1..96fcf9258a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2222,6 +2222,81 @@ Example code: 'status': 'stopped', 'username': 'NT AUTHORITY\\LocalService'} +Testing utilities +================= + +The ``psutil.test`` subpackage includes a helper class to assist in writing +memory-leak detection tests. + +.. class:: psutil.test.MemoryLeakTestCase + + A testing framework for detecting memory leaks in functions, typically those + implemented in C that forget to ``free()`` heap memory, call ``Py_DECREF`` on + Python objects, and so on. It works by comparing the process's memory usage + before and after repeatedly calling the target function. + + Detecting memory leaks reliably is inherently difficult (and probably + impossible) because of how the OS manages memory, garbage collection, and + caching. Memory usage may even decrease between runs. So this is not meant to + be bullet proof. To reduce false positives, when an increase in memory is + detected (mem > 0), the test is retried up to 5 times, increasing the + number of function calls each time. If memory continues to grow, the test is + considered a failure. + The test currently monitors RSS, VMS, and `USS `__ memory. + ``mallinfo()`` on Linux and ``_heapwalk()`` on Windows could provide + even more precise results (see `issue 1275 `__), + but these are not yet implemented. + + In addition it also ensures that the target function does not leak + file descriptors (UNIX) or handles (Windows). + + .. versionadded:: 7.2.0 + + .. warning:: + This class is experimental, meaning its API or internal algorithm may + change in the future. + + Usage example:: + + from psutil.test import MemoryLeakTestCase + + class TestLeaks(MemoryLeakTestCase): + def test_fun(self): + self.execute(some_function) + + Class attributes and methods: + + .. attribute:: times + :value: 200 + + Number of times to call the tested function in each iteration. + + .. attribute:: retries + :value: 5 + + Maximum number of retries if memory growth is detected. + + .. attribute:: warmup_times + :value: 10 + + Number of warm-up calls before measurements begin. + + .. attribute:: tolerance + :value: 0 + + Allowed memory difference (in bytes) before considering it a leak. + + .. attribute:: verbosity + :value: 1 + + 0 = no messages; 1 = print diagnostics when memory increases during the + test run. + + .. method:: execute(fun, *, times=None, warmup_times=None, retries=None, tolerance=None) + + Run a full leak test on a callable. If specified, the optional arguments + override the class attributes with the same name. + Constants ========= diff --git a/psutil/test/__init__.py b/psutil/test/__init__.py new file mode 100644 index 0000000000..33af57e4c1 --- /dev/null +++ b/psutil/test/__init__.py @@ -0,0 +1,7 @@ +# 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. + +from .memleak import MemoryLeakTestCase + +__all__ = ["MemoryLeakTestCase"] diff --git a/psutil/test/memleak.py b/psutil/test/memleak.py new file mode 100644 index 0000000000..0f9fa6e2e3 --- /dev/null +++ b/psutil/test/memleak.py @@ -0,0 +1,220 @@ +# 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 framework for detecting memory leaks in C functions.""" + +import functools +import gc +import os +import sys +import unittest + +import psutil +from psutil._common import POSIX +from psutil._common import bytes2human +from psutil._common import print_color + +thisproc = psutil.Process() + + +class MemoryLeakTestCase(unittest.TestCase): + """A testing framework for detecting memory leaks in functions, + typically those implemented in C that forget to `free()` heap + memory, call `Py_DECREF` on Python objects, and so on. It works by + comparing the process's memory usage before and after repeatedly + calling the target function. + + Detecting memory leaks reliably is inherently difficult (and + probably impossible) because of how the OS manages memory, garbage + collection, and caching. Memory usage may even decrease between + runs. So this is not meant to be bullet proof. To reduce false + positives, when an increase in memory is detected (mem > 0), the + test is retried up to 5 times, increasing the number of function + calls each time. If memory continues to grow, the test is + considered a failure. + + The test currently monitors RSS, VMS, and USS [1] memory. + `mallinfo()` on Linux and `_heapwalk()` on Windows could provide + even more precise results [2], but these are not yet implemented. + + In addition it also ensures that the target function does not leak + file descriptors (UNIX) or handles (Windows). + + Usage example: + + from psutil.test import MemoryLeakTestCase + + class TestLeaks(MemoryLeakTestCase): + def test_fun(self): + self.execute(some_function) + + NOTE - This class is experimental, meaning its API or internal + algorithm may change in the future. + + [1] https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python + [2] https://github.com/giampaolo/psutil/issues/1275 + """ + + # Number of times to call the tested function in each iteration. + times = 200 + # Maximum number of retries if memory growth is detected. + retries = 5 + # Number of warm-up calls before measurements begin. + warmup_times = 10 + # Allowed memory difference (in bytes) before considering it a leak. + tolerance = 0 + # 0 = no messages; 1 = print diagnostics when memory increases. + verbosity = 1 + + @classmethod + def setUpClass(cls): + cls._psutil_debug_orig = bool(os.getenv("PSUTIL_DEBUG")) + psutil._set_debug(False) # avoid spamming to stderr + + @classmethod + def tearDownClass(cls): + psutil._set_debug(cls._psutil_debug_orig) + + def _log(self, msg, level): + if level <= self.verbosity: + print_color(msg, color="yellow", file=sys.stderr) + + # --- getters + + def _get_mem(self): + mem = thisproc.memory_full_info() + return { + "rss": mem.rss, + "vms": mem.vms, + "uss": getattr(mem, "uss", 0), + } + + def _get_num_fds(self): + if POSIX: + return thisproc.num_fds() + else: + return thisproc.num_handles() + + # --- checkers + + 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: + msg = ( + f"negative diff {diff!r} (gc probably collected a" + " resource from a previous test)" + ) + return self.fail(msg) + if diff > 0: + type_ = "fd" if POSIX else "handle" + if diff > 1: + type_ += "s" + msg = f"{diff} unclosed {type_} after calling {fun!r}" + return self.fail(msg) + + def _call_ntimes(self, fun, times): + """Get memory samples (rss, vms, uss) before and after calling + fun repeatedly, and return the diffs as a dict. + """ + 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() + assert gc.garbage == [] + diffs = {k: mem2[k] - mem1[k] for k in mem1} + return diffs + + def _check_mem(self, fun, times, retries, tolerance): + prev = {} + messages = [] + b2h = functools.partial(bytes2human, format="%(value)i%(symbol)s") + + for idx in range(1, retries + 1): + diffs = self._call_ntimes(fun, times) + leaks = {k: v for k, v in diffs.items() if v > 0} + + if leaks: + parts = [f"{k}=+{b2h(v)}" for k, v in leaks.items()] + avg = b2h(sum(leaks.values()) / times) + msg = ( + f"Run #{idx}: {', '.join(parts)} " + f"(ncalls={times}, avg-per-call=+{avg})" + ) + if idx == 1: + msg = "\n" + msg + messages.append(msg) + self._log(msg, 1) + + stable = all( + diffs.get(k, 0) <= tolerance + or diffs.get(k, 0) <= prev.get(k, 0) + for k in diffs + ) + if stable: + if idx > 1 and leaks: + self._log( + "Memory stabilized (no further growth detected)", 1 + ) + return + + prev = diffs + times += times # double calls each retry + + msg = f"Memory kept increasing after {retries} runs." + "\n".join( + messages + ) + return self.fail(msg) + + # --- + + def call(self, fun): + return fun() + + def execute( + self, + fun, + *, + times=None, + warmup_times=None, + retries=None, + tolerance=None, + ): + """Run a full leak test on a callable. If specified, the + optional arguments override the class attributes with the same + name. + """ + 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 + + if times < 1: + msg = f"times must be >= 1 (got {times})" + raise ValueError(msg) + if warmup_times < 0: + msg = f"warmup_times must be >= 0 (got {warmup_times})" + raise ValueError(msg) + if retries < 0: + msg = f"retries must be >= 0 (got {retries})" + raise ValueError(msg) + if tolerance < 0: + msg = f"tolerance must be >= 0 (got {tolerance})" + raise ValueError(msg) + + self._call_ntimes(fun, warmup_times) # warm up + + self._check_fds(fun) + self._check_mem(fun, times=times, retries=retries, tolerance=tolerance) diff --git a/tests/__init__.py b/tests/__init__.py index 3a6ef9cb45..5d6f4f74f0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -10,7 +10,6 @@ import enum import errno import functools -import gc import importlib import ipaddress import os @@ -50,10 +49,8 @@ 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 if POSIX: @@ -79,8 +76,8 @@ 'ThreadTask', # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', - 'process_namespace', 'system_namespace', + 'retry_on_failure', 'PsutilTestCase', 'process_namespace', + 'system_namespace', 'is_win_secure_system_proc', # fs utils 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', @@ -95,7 +92,7 @@ # compat 'reload_module', 'import_module_by_path', # others - 'warn', 'copyload_shared_lib', 'is_namedtuple', + 'warn', 'copyload_shared_lib', 'is_namedtuple' ] # fmt: on @@ -1058,203 +1055,6 @@ def assert_in_pids(proc): # assert proc == ppid(), os.getpid() -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. - - We currently check RSS, VMS and USS [1] memory. mallinfo() on Linux - and _heapwalk() on Windows should give even more precision [2], but - at the moment they 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) - - [1] https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python - [2] https://github.com/giampaolo/psutil/issues/1275 - """ - - # 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 _log(self, msg): - if self.verbose: - print_color(msg, color="yellow", file=sys.stderr) - - # --- getters - - def _get_mem(self): - mem = self._thisproc.memory_full_info() - return { - "rss": mem.rss, - "vms": mem.vms, - "uss": getattr(mem, "uss", 0), - } - - def _get_num_fds(self): - if POSIX: - return self._thisproc.num_fds() - else: - return self._thisproc.num_handles() - - # --- checkers - - 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: - msg = ( - f"negative diff {diff!r} (gc probably collected a" - " resource from a previous test)" - ) - return pytest.fail(msg) - if diff > 0: - type_ = "fd" if POSIX else "handle" - if diff > 1: - type_ += "s" - msg = f"{diff} unclosed {type_} after calling {fun!r}" - return pytest.fail(msg) - - def _call_ntimes(self, fun, times): - """Get memory samples (rss, vms, uss) before and after calling - fun repeatedly, and return the diffs as a dict. - """ - 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() - assert gc.garbage == [] - diffs = {k: mem2[k] - mem1[k] for k in mem1} - return diffs - - def _check_mem(self, fun, times, retries, tolerance): - messages = [] - prev_mem = {} - increase = times - b2h = functools.partial(bytes2human, format="%(value)i%(symbol)s") - - for idx in range(1, retries + 1): - diffs = self._call_ntimes(fun, times) - - # Filter only metrics with positive diffs (possible leaks). - positive_diffs = {k: v for k, v in diffs.items() if v > 0} - - # Build message only for those metrics. - if positive_diffs: - msg_parts = [ - f"{k}=+{b2h(v)}" for k, v in positive_diffs.items() - ] - msg = "Run #{}: {} (ncalls={}, avg-per-call=+{})".format( - idx, - ", ".join(msg_parts), - times, - b2h(sum(positive_diffs.values()) / times), - ) - if idx == 1: - msg = "\n" + msg - messages.append(msg) - self._log(msg) - - # Determine if memory stabilized or decreased. - success = all( - diffs.get(k, 0) <= tolerance - or diffs.get(k, 0) <= prev_mem.get(k, 0) - for k in diffs - ) - if success: - if idx > 1 and positive_diffs: - self._log("Memory stabilized (no further growth detected)") - return None - - times += increase - prev_mem = diffs - - msg = "\n" + "\n".join(messages) - return pytest.fail(msg) - - # --- - - 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(): - try: - fun() - except exc: - pass - else: - return pytest.fail(f"{fun} did not raise {exc}") - - self.execute(call, **kwargs) - - def is_win_secure_system_proc(pid): # see: https://github.com/giampaolo/psutil/issues/2338 @memoize diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index 3a31b8ee76..2743780112 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -24,8 +24,10 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil.test import MemoryLeakTestCase from . import AARCH64 +from . import CI_TESTING from . import HAS_CPU_AFFINITY from . import HAS_CPU_FREQ from . import HAS_ENVIRON @@ -38,7 +40,6 @@ from . import HAS_SENSORS_BATTERY from . import HAS_SENSORS_FANS from . import HAS_SENSORS_TEMPERATURES -from . import TestMemoryLeak from . import create_sockets from . import get_testfn from . import process_namespace @@ -50,6 +51,9 @@ cext = psutil._psplatform.cext thisproc = psutil.Process() +if CI_TESTING: + MemoryLeakTestCase.retries *= 2 + FEW_TIMES = 5 @@ -81,11 +85,26 @@ def wrapper(self, *args, **kwargs): # =================================================================== -class TestProcessObjectLeaks(TestMemoryLeak): +class TestProcessObjectLeaks(MemoryLeakTestCase): """Test leaks of Process class methods.""" proc = thisproc + def execute_w_exc(self, exc, fun, **kwargs): + """Run MemoryLeakTestCase.execute() expecting fun() to raise + exc on every call. + """ + + def call(): + try: + fun() + except exc: + pass + else: + return self.fail(f"{fun} did not raise {exc}") + + self.execute(call, **kwargs) + def test_coverage(self): ns = process_namespace(None) ns.test_class_coverage(self, ns.getters + ns.setters) @@ -317,7 +336,7 @@ def call(): @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") -class TestProcessDualImplementation(TestMemoryLeak): +class TestProcessDualImplementation(MemoryLeakTestCase): def test_cmdline_peb_true(self): self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True)) @@ -330,7 +349,7 @@ def test_cmdline_peb_false(self): # =================================================================== -class TestModuleFunctionsLeaks(TestMemoryLeak): +class TestModuleFunctionsLeaks(MemoryLeakTestCase): """Test leaks of psutil module functions.""" def test_coverage(self): diff --git a/tests/test_testutils.py b/tests/test_testutils.py index b7fd5f1357..86f6386848 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -21,9 +21,11 @@ from psutil import FREEBSD from psutil import NETBSD from psutil import POSIX +from psutil import WINDOWS from psutil._common import open_binary from psutil._common import open_text from psutil._common import supports_ipv6 +from psutil.test import MemoryLeakTestCase from . import CI_TESTING from . import COVERAGE @@ -32,7 +34,6 @@ from . import PYTHON_EXE from . import PYTHON_EXE_ENV from . import PsutilTestCase -from . import TestMemoryLeak from . import bind_socket from . import bind_unix_socket from . import call_until @@ -367,7 +368,8 @@ def test_create_sockets(self): @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") @pytest.mark.xdist_group(name="serial") -class TestMemLeakClass(TestMemoryLeak): +class TestMemLeakClass(MemoryLeakTestCase): + @retry_on_failure() def test_times(self): def fun(): @@ -401,7 +403,8 @@ def fun(ls=ls): try: # will consume around 60M in total with pytest.raises( - pytest.fail.Exception, match=rf"Run \#{TestMemoryLeak.retries}" + AssertionError, + match=rf"Run \#{MemoryLeakTestCase.retries}", ): with contextlib.redirect_stdout( io.StringIO() @@ -414,11 +417,25 @@ def test_unclosed_files(self): def fun(): f = open(__file__) # noqa: SIM115 self.addCleanup(f.close) - box.append(f) + box.append(f) # prevent auto-gc box = [] kind = "fd" if POSIX else "handle" - with pytest.raises(pytest.fail.Exception, match="unclosed " + kind): + with pytest.raises(AssertionError, match="unclosed " + kind): + self.execute(fun) + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_unclosed_handles(self): + import win32api + import win32con + + def fun(): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + self.addCleanup(win32api.CloseHandle, handle) + + with pytest.raises(AssertionError, match="unclosed handle"): self.execute(fun) def test_tolerance(self): @@ -432,20 +449,6 @@ def fun(): ) assert len(ls) == times + 1 - def test_execute_w_exc(self): - def fun_1(): - 1 / 0 # noqa: B018 - - self.execute_w_exc(ZeroDivisionError, fun_1) - with pytest.raises(ZeroDivisionError): - self.execute_w_exc(OSError, fun_1) - - def fun_2(): - pass - - with pytest.raises(pytest.fail.Exception): - self.execute_w_exc(ZeroDivisionError, fun_2) - class TestTestingUtils(PsutilTestCase): def test_process_namespace(self): From aae47457f4fea6cd6d8e06463737e59eb5662cd0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 5 Nov 2025 22:25:20 +0100 Subject: [PATCH 1442/1714] test_memleaks.py: call self.execute() only once per test (avoid printing wrong stuff) --- tests/test_memleaks.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index 2743780112..3f6ea0ddf4 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -157,8 +157,11 @@ def test_ionice_set(self): 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) + + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + def test_ionice_set_badarg(self): + fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) + self.execute_w_exc(OSError, fun) @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") @fewtimes_if_linux() @@ -236,6 +239,9 @@ def test_cpu_affinity(self): def test_cpu_affinity_set(self): affinity = thisproc.cpu_affinity() self.execute(lambda: self.proc.cpu_affinity(affinity)) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity_set_badarg(self): self.execute_w_exc(ValueError, lambda: self.proc.cpu_affinity([-1])) @fewtimes_if_linux() @@ -258,6 +264,10 @@ def test_rlimit(self): def test_rlimit_set(self): limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) + + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_set_badarg(self): self.execute_w_exc((OSError, ValueError), lambda: self.proc.rlimit(-1)) @fewtimes_if_linux() From 931b0d816f4f1c667f7cd7eac347a535b01bf057 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 6 Nov 2025 01:50:17 +0100 Subject: [PATCH 1443/1714] memleak.py: improve output format --- psutil/test/memleak.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/psutil/test/memleak.py b/psutil/test/memleak.py index 0f9fa6e2e3..03b9531344 100644 --- a/psutil/test/memleak.py +++ b/psutil/test/memleak.py @@ -16,6 +16,20 @@ from psutil._common import print_color thisproc = psutil.Process() +b2h = functools.partial(bytes2human, format="%(value)i%(symbol)s") + + +def format_run_line(idx, diffs, times): + parts = [f"{k}={'+' + b2h(v):<6}" for k, v in diffs.items() if v > 0] + metrics = " | ".join(parts) + avg = "0B" + if parts: + first_key = next(k for k, v in diffs.items() if v > 0) + avg = b2h(diffs[first_key] // times) + s = f"Run #{idx:>2}: {metrics:<50} (calls={times:>5}, avg/call=+{avg})" + if idx == 1: + s = "\n" + s + return s class MemoryLeakTestCase(unittest.TestCase): @@ -138,23 +152,15 @@ def _call_ntimes(self, fun, times): def _check_mem(self, fun, times, retries, tolerance): prev = {} messages = [] - b2h = functools.partial(bytes2human, format="%(value)i%(symbol)s") for idx in range(1, retries + 1): diffs = self._call_ntimes(fun, times) leaks = {k: v for k, v in diffs.items() if v > 0} if leaks: - parts = [f"{k}=+{b2h(v)}" for k, v in leaks.items()] - avg = b2h(sum(leaks.values()) / times) - msg = ( - f"Run #{idx}: {', '.join(parts)} " - f"(ncalls={times}, avg-per-call=+{avg})" - ) - if idx == 1: - msg = "\n" + msg - messages.append(msg) - self._log(msg, 1) + line = format_run_line(idx, leaks, times) + messages.append(line) + self._log(line, 1) stable = all( diffs.get(k, 0) <= tolerance From 0071d5478e1cf54046f500c304d5f32beb6c52c9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Nov 2025 02:04:26 +0100 Subject: [PATCH 1444/1714] Skip test not for Windows Signed-off-by: Giampaolo Rodola --- tests/test_memleaks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index 3f6ea0ddf4..1741cad792 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -159,6 +159,7 @@ def test_ionice_set(self): self.execute(lambda: self.proc.ionice(psutil.IOPRIO_CLASS_NONE)) @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(WINDOWS, reason="not on WINDOWS") def test_ionice_set_badarg(self): fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) self.execute_w_exc(OSError, fun) From 5caf5f66e6f6bd260ea463a660ffdb2d570b321a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 7 Nov 2025 19:36:12 +0100 Subject: [PATCH 1445/1714] Move namedtuples in a new _ntuples.py module. --- MANIFEST.in | 1 + psutil/__init__.py | 34 ++-- psutil/_common.py | 94 +-------- psutil/_ntuples.py | 417 ++++++++++++++++++++++++++++++++++++++++ psutil/_psaix.py | 51 ++--- psutil/_psbsd.py | 83 ++------ psutil/_pslinux.py | 124 ++++-------- psutil/_psosx.py | 65 +++---- psutil/_psposix.py | 4 +- psutil/_pssunos.py | 74 +++---- psutil/_pswindows.py | 75 +++----- tests/test_testutils.py | 2 +- 12 files changed, 588 insertions(+), 436 deletions(-) create mode 100644 psutil/_ntuples.py diff --git a/MANIFEST.in b/MANIFEST.in index 050d5d491f..aecf381f7d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -27,6 +27,7 @@ include docs/requirements.txt include make.bat include psutil/__init__.py include psutil/_common.py +include psutil/_ntuples.py include psutil/_psaix.py include psutil/_psbsd.py include psutil/_pslinux.py diff --git a/psutil/__init__.py b/psutil/__init__.py index ee595599cd..af63773b70 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -36,6 +36,7 @@ pwd = None from . import _common +from . import _ntuples as _ntp from ._common import AIX from ._common import BSD from ._common import CONN_CLOSE @@ -1162,7 +1163,7 @@ def memory_percent(self, memtype="rss"): >>> psutil.Process().memory_info()._fields ('rss', 'vms', 'shared', 'text', 'lib', 'data', 'dirty', 'uss', 'pss') """ - valid_types = list(_psplatform.pfullmem._fields) + valid_types = list(_ntp.pfullmem._fields) if memtype not in valid_types: msg = ( f"invalid memtype {memtype!r}; valid types are" @@ -1171,7 +1172,7 @@ def memory_percent(self, memtype="rss"): raise ValueError(msg) fun = ( self.memory_info - if memtype in _psplatform.pmem._fields + if memtype in _ntp.pmem._fields else self.memory_full_info ) metrics = fun() @@ -1211,11 +1212,9 @@ def memory_maps(self, grouped=True): d[path] = list(map(lambda x, y: x + y, d[path], nums)) except KeyError: d[path] = nums - nt = _psplatform.pmmap_grouped - return [nt(path, *d[path]) for path in d] + return [_ntp.pmmap_grouped(path, *d[path]) for path in d] else: - nt = _psplatform.pmmap_ext - return [nt(*x) for x in it] + return [_ntp.pmmap_ext(*x) for x in it] def open_files(self): """Return files opened by process as a list of @@ -1751,7 +1750,7 @@ def _cpu_busy_time(times): def _cpu_times_deltas(t1, t2): assert t1._fields == t2._fields, (t1, t2) field_deltas = [] - for field in _psplatform.scputimes._fields: + for field in _ntp.scputimes._fields: field_delta = getattr(t2, field) - getattr(t1, field) # CPU times are always supposed to increase over time # or at least remain the same and that's because time @@ -1766,7 +1765,7 @@ def _cpu_times_deltas(t1, t2): # https://gitlab.com/procps-ng/procps/blob/v3.3.12/top/top.c#L5063 field_delta = max(0, field_delta) field_deltas.append(field_delta) - return _psplatform.scputimes(*field_deltas) + return _ntp.scputimes(*field_deltas) def cpu_percent(interval=None, percpu=False): @@ -1885,7 +1884,7 @@ def calculate(t1, t2): # make sure we don't return negative values or values over 100% field_perc = min(max(0.0, field_perc), 100.0) nums.append(field_perc) - return _psplatform.scputimes(*nums) + return _ntp.scputimes(*nums) # system-wide usage if not percpu: @@ -1955,7 +1954,7 @@ def cpu_freq(percpu=False): min_ = mins / num_cpus max_ = maxs / num_cpus - return _common.scpufreq(current, min_, max_) + return _ntp.scpufreq(current, min_, max_) __all__.append("cpu_freq") @@ -2114,13 +2113,12 @@ def disk_io_counters(perdisk=False, nowrap=True): return {} if perdisk else None if nowrap: rawdict = _wrap_numbers(rawdict, 'psutil.disk_io_counters') - nt = getattr(_psplatform, "sdiskio", _common.sdiskio) if perdisk: for disk, fields in rawdict.items(): - rawdict[disk] = nt(*fields) + rawdict[disk] = _ntp.sdiskio(*fields) return rawdict else: - return nt(*(sum(x) for x in zip(*rawdict.values()))) + return _ntp.sdiskio(*(sum(x) for x in zip(*rawdict.values()))) disk_io_counters.cache_clear = functools.partial( @@ -2167,10 +2165,10 @@ def net_io_counters(pernic=False, nowrap=True): rawdict = _wrap_numbers(rawdict, 'psutil.net_io_counters') if pernic: for nic, fields in rawdict.items(): - rawdict[nic] = _common.snetio(*fields) + rawdict[nic] = _ntp.snetio(*fields) return rawdict else: - return _common.snetio(*[sum(x) for x in zip(*rawdict.values())]) + return _ntp.snetio(*[sum(x) for x in zip(*rawdict.values())]) net_io_counters.cache_clear = functools.partial( @@ -2252,7 +2250,7 @@ def net_if_addrs(): while addr.count(separator) < 5: addr += f"{separator}00" - nt = _common.snicaddr(fam, addr, mask, broadcast, ptp) + nt = _ntp.snicaddr(fam, addr, mask, broadcast, ptp) # On Windows broadcast is None, so we determine it via # ipaddress module. @@ -2321,9 +2319,7 @@ def convert(n): elif critical and not high: high = critical - ret[name].append( - _common.shwtemp(label, current, high, critical) - ) + ret[name].append(_ntp.shwtemp(label, current, high, critical)) return dict(ret) diff --git a/psutil/_common.py b/psutil/_common.py index 51c798b4c6..0aa9335e9b 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -17,7 +17,6 @@ import sys import threading import warnings -from collections import namedtuple from socket import AF_INET from socket import SOCK_DGRAM from socket import SOCK_STREAM @@ -53,10 +52,6 @@ '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', - 'sdiskusage', 'snetio', 'snicaddr', 'snicstats', 'sswap', 'suser', # utility functions 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize', 'parse_environ_block', 'path_exists_strict', 'usage_percent', @@ -147,85 +142,6 @@ class BatteryTime(enum.IntEnum): ENCODING_ERRS = sys.getfilesystemencodeerrors() -# =================================================================== -# --- namedtuples -# =================================================================== - -# --- for system functions - -# fmt: off -# psutil.swap_memory() -sswap = namedtuple('sswap', ['total', 'used', 'free', 'percent', 'sin', - 'sout']) -# psutil.disk_usage() -sdiskusage = namedtuple('sdiskusage', ['total', 'used', 'free', 'percent']) -# psutil.disk_io_counters() -sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', - 'read_bytes', 'write_bytes', - 'read_time', 'write_time']) -# psutil.disk_partitions() -sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts']) -# psutil.net_io_counters() -snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv', - 'packets_sent', 'packets_recv', - 'errin', 'errout', - 'dropin', 'dropout']) -# psutil.users() -suser = namedtuple('suser', ['name', 'terminal', 'host', 'started', 'pid']) -# psutil.net_connections() -sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr', - 'status', 'pid']) -# psutil.net_if_addrs() -snicaddr = namedtuple('snicaddr', - ['family', 'address', 'netmask', 'broadcast', 'ptp']) -# psutil.net_if_stats() -snicstats = namedtuple('snicstats', - ['isup', 'duplex', 'speed', 'mtu', 'flags']) -# psutil.cpu_stats() -scpustats = namedtuple( - 'scpustats', ['ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']) -# psutil.cpu_freq() -scpufreq = namedtuple('scpufreq', ['current', 'min', 'max']) -# psutil.sensors_temperatures() -shwtemp = namedtuple( - 'shwtemp', ['label', 'current', 'high', 'critical']) -# psutil.sensors_battery() -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'] -) -# psutil.Process.open_files() -popenfile = namedtuple('popenfile', ['path', 'fd']) -# psutil.Process.threads() -pthread = namedtuple('pthread', ['id', 'user_time', 'system_time']) -# psutil.Process.uids() -puids = namedtuple('puids', ['real', 'effective', 'saved']) -# psutil.Process.gids() -pgids = namedtuple('pgids', ['real', 'effective', 'saved']) -# psutil.Process.io_counters() -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.net_connections() -pconn = namedtuple( - 'pconn', ['fd', 'family', 'type', 'laddr', 'raddr', 'status'] -) - -# psutil.net_connections() and psutil.Process.net_connections() -addr = namedtuple('addr', ['ip', 'port']) - - # =================================================================== # --- Process.net_connections() 'kind' parameter mapping # =================================================================== @@ -584,11 +500,13 @@ def socktype_to_enum(num): def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): """Convert a raw connection tuple to a proper ntuple.""" + from . import _ntuples as ntp + if fam in {socket.AF_INET, AF_INET6}: if laddr: - laddr = addr(*laddr) + laddr = ntp.addr(*laddr) if raddr: - raddr = addr(*raddr) + raddr = ntp.addr(*raddr) if type_ == socket.SOCK_STREAM and fam in {AF_INET, AF_INET6}: status = status_map.get(status, CONN_NONE) else: @@ -596,9 +514,9 @@ def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): fam = sockfam_to_enum(fam) type_ = socktype_to_enum(type_) if pid is None: - return pconn(fd, fam, type_, laddr, raddr, status) + return ntp.pconn(fd, fam, type_, laddr, raddr, status) else: - return sconn(fd, fam, type_, laddr, raddr, status, pid) + return ntp.sconn(fd, fam, type_, laddr, raddr, status, pid) def broadcast_addr(addr): diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py new file mode 100644 index 0000000000..dabf496b21 --- /dev/null +++ b/psutil/_ntuples.py @@ -0,0 +1,417 @@ +# 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. + +from collections import namedtuple as nt + +from ._common import AIX +from ._common import BSD +from ._common import FREEBSD +from ._common import LINUX +from ._common import MACOS +from ._common import SUNOS +from ._common import WINDOWS + +# =================================================================== +# --- system functions +# =================================================================== + +# psutil.swap_memory() +sswap = nt("sswap", ("total", "used", "free", "percent", "sin", "sout")) + +# psutil.disk_usage() +sdiskusage = nt("sdiskusage", ("total", "used", "free", "percent")) + +# psutil.disk_io_counters() +sdiskio = nt( + "sdiskio", + ( + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + ), +) + +# psutil.disk_partitions() +sdiskpart = nt("sdiskpart", ("device", "mountpoint", "fstype", "opts")) + +# psutil.net_io_counters() +snetio = nt( + "snetio", + ( + "bytes_sent", + "bytes_recv", + "packets_sent", + "packets_recv", + "errin", + "errout", + "dropin", + "dropout", + ), +) + +# psutil.users() +suser = nt("suser", ("name", "terminal", "host", "started", "pid")) + +# psutil.net_connections() +sconn = nt( + "sconn", ("fd", "family", "type", "laddr", "raddr", "status", "pid") +) + +# psutil.net_if_addrs() +snicaddr = nt("snicaddr", ("family", "address", "netmask", "broadcast", "ptp")) + +# psutil.net_if_stats() +snicstats = nt("snicstats", ("isup", "duplex", "speed", "mtu", "flags")) + +# psutil.cpu_stats() +scpustats = nt( + "scpustats", ("ctx_switches", "interrupts", "soft_interrupts", "syscalls") +) + +# psutil.cpu_freq() +scpufreq = nt("scpufreq", ("current", "min", "max")) + +# psutil.sensors_temperatures() +shwtemp = nt("shwtemp", ("label", "current", "high", "critical")) + +# psutil.sensors_battery() +sbattery = nt("sbattery", ("percent", "secsleft", "power_plugged")) + +# psutil.sensors_fans() +sfan = nt("sfan", ("label", "current")) + +# =================================================================== +# --- Process class +# =================================================================== + +# psutil.Process.cpu_times() +pcputimes = nt( + "pcputimes", ("user", "system", "children_user", "children_system") +) + +# psutil.Process.open_files() +popenfile = nt("popenfile", ("path", "fd")) + +# psutil.Process.threads() +pthread = nt("pthread", ("id", "user_time", "system_time")) + +# psutil.Process.uids() +puids = nt("puids", ("real", "effective", "saved")) + +# psutil.Process.gids() +pgids = nt("pgids", ("real", "effective", "saved")) + +# psutil.Process.io_counters() +pio = nt("pio", ("read_count", "write_count", "read_bytes", "write_bytes")) + +# psutil.Process.ionice() +pionice = nt("pionice", ("ioclass", "value")) + +# psutil.Process.ctx_switches() +pctxsw = nt("pctxsw", ("voluntary", "involuntary")) + +# psutil.Process.net_connections() +pconn = nt("pconn", ("fd", "family", "type", "laddr", "raddr", "status")) + +# psutil.net_connections() and psutil.Process.net_connections() +addr = nt("addr", ("ip", "port")) + +# =================================================================== +# --- Linux +# =================================================================== + +if LINUX: + + # This gets set from _pslinux.py + scputimes = None + + # psutil.virtual_memory() + svmem = nt( + "svmem", + ( + "total", + "available", + "percent", + "used", + "free", + "active", + "inactive", + "buffers", + "cached", + "shared", + "slab", + ), + ) + + # psutil.disk_io_counters() + sdiskio = nt( + "sdiskio", + ( + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + "busy_time", + ), + ) + + # psutil.Process().open_files() + popenfile = nt("popenfile", ("path", "fd", "position", "mode", "flags")) + + # psutil.Process().memory_info() + pmem = nt("pmem", ("rss", "vms", "shared", "text", "lib", "data", "dirty")) + + # psutil.Process().memory_full_info() + pfullmem = nt("pfullmem", pmem._fields + ("uss", "pss", "swap")) + + # psutil.Process().memory_maps(grouped=True) + pmmap_grouped = nt( + "pmmap_grouped", + ( + "path", + "rss", + "size", + "pss", + "shared_clean", + "shared_dirty", + "private_clean", + "private_dirty", + "referenced", + "anonymous", + "swap", + ), + ) + + # psutil.Process().memory_maps(grouped=False) + pmmap_ext = nt( + "pmmap_ext", "addr perms " + " ".join(pmmap_grouped._fields) + ) + + # psutil.Process.io_counters() + pio = nt( + "pio", + ( + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_chars", + "write_chars", + ), + ) + + # psutil.Process.cpu_times() + pcputimes = nt( + "pcputimes", + ("user", "system", "children_user", "children_system", "iowait"), + ) + +# =================================================================== +# --- Windows +# =================================================================== + +elif WINDOWS: + + # psutil.cpu_times() + scputimes = nt("scputimes", ("user", "system", "idle", "interrupt", "dpc")) + + # psutil.virtual_memory() + svmem = nt("svmem", ("total", "available", "percent", "used", "free")) + + # psutil.Process.memory_info() + pmem = nt( + "pmem", + ( + "rss", + "vms", + "num_page_faults", + "peak_wset", + "wset", + "peak_paged_pool", + "paged_pool", + "peak_nonpaged_pool", + "nonpaged_pool", + "pagefile", + "peak_pagefile", + "private", + ), + ) + + # psutil.Process.memory_full_info() + pfullmem = nt("pfullmem", pmem._fields + ("uss",)) + + # psutil.Process.memory_maps(grouped=True) + pmmap_grouped = nt("pmmap_grouped", ("path", "rss")) + + # psutil.Process.memory_maps(grouped=False) + pmmap_ext = nt( + "pmmap_ext", "addr perms " + " ".join(pmmap_grouped._fields) + ) + + # psutil.Process.io_counters() + pio = nt( + "pio", + ( + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "other_count", + "other_bytes", + ), + ) + +# =================================================================== +# --- macOS +# =================================================================== + +elif MACOS: + + # psutil.cpu_times() + scputimes = nt("scputimes", ("user", "nice", "system", "idle")) + + # psutil.virtual_memory() + svmem = nt( + "svmem", + ( + "total", + "available", + "percent", + "used", + "free", + "active", + "inactive", + "wired", + ), + ) + + # psutil.Process.memory_info() + pmem = nt("pmem", ("rss", "vms", "pfaults", "pageins")) + + # psutil.Process.memory_full_info() + pfullmem = nt("pfullmem", pmem._fields + ("uss",)) + +# =================================================================== +# --- BSD +# =================================================================== + +elif BSD: + + # psutil.virtual_memory() + svmem = nt( + "svmem", + ( + "total", + "available", + "percent", + "used", + "free", + "active", + "inactive", + "buffers", + "cached", + "shared", + "wired", + ), + ) + + # psutil.cpu_times() + scputimes = nt("scputimes", ("user", "nice", "system", "idle", "irq")) + + # psutil.Process.memory_info() + pmem = nt("pmem", ("rss", "vms", "text", "data", "stack")) + + # psutil.Process.memory_full_info() + pfullmem = pmem + + # psutil.Process.cpu_times() + pcputimes = nt( + "pcputimes", ("user", "system", "children_user", "children_system") + ) + + # psutil.Process.memory_maps(grouped=True) + pmmap_grouped = nt( + "pmmap_grouped", "path rss, private, ref_count, shadow_count" + ) + + # psutil.Process.memory_maps(grouped=False) + pmmap_ext = nt( + "pmmap_ext", "addr, perms path rss, private, ref_count, shadow_count" + ) + + # psutil.disk_io_counters() + if FREEBSD: + sdiskio = nt( + "sdiskio", + ( + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "busy_time", + ), + ) + else: + sdiskio = nt( + "sdiskio", + ("read_count", "write_count", "read_bytes", "write_bytes"), + ) + +# =================================================================== +# --- SunOS +# =================================================================== + +elif SUNOS: + + # psutil.cpu_times() + scputimes = nt("scputimes", ("user", "system", "idle", "iowait")) + + # psutil.cpu_times(percpu=True) + pcputimes = nt( + "pcputimes", ("user", "system", "children_user", "children_system") + ) + + # psutil.virtual_memory() + svmem = nt("svmem", ("total", "available", "percent", "used", "free")) + + # psutil.Process.memory_info() + pmem = nt("pmem", ("rss", "vms")) + + # psutil.Process.memory_full_info() + pfullmem = pmem + + # psutil.Process.memory_maps(grouped=True) + pmmap_grouped = nt("pmmap_grouped", ("path", "rss", "anonymous", "locked")) + + # psutil.Process.memory_maps(grouped=False) + pmmap_ext = nt( + "pmmap_ext", "addr perms " + " ".join(pmmap_grouped._fields) + ) + +# =================================================================== +# --- AIX +# =================================================================== + +elif AIX: + + # psutil.Process.memory_info() + pmem = nt("pmem", ("rss", "vms")) + + # psutil.Process.memory_full_info() + pfullmem = pmem + + # psutil.Process.cpu_times() + scputimes = nt("scputimes", ("user", "system", "idle", "iowait")) + + # psutil.virtual_memory() + svmem = nt("svmem", ("total", "available", "percent", "used", "free")) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index a84abe3f53..2dfeb7653f 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -12,9 +12,9 @@ import re import subprocess import sys -from collections import namedtuple from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_aix as cext from ._common import NIC_DUPLEX_FULL @@ -78,21 +78,6 @@ ) -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# psutil.Process.memory_info() -pmem = namedtuple('pmem', ['rss', 'vms']) -# psutil.Process.memory_full_info() -pfullmem = pmem -# psutil.Process.cpu_times() -scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait']) -# psutil.virtual_memory() -svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) - - # ===================================================================== # --- memory # ===================================================================== @@ -101,7 +86,7 @@ def virtual_memory(): total, avail, free, _pinned, inuse = cext.virtual_mem() percent = usage_percent((total - avail), total, round_=1) - return svmem(total, avail, percent, inuse, free) + return ntp.svmem(total, avail, percent, inuse, free) def swap_memory(): @@ -109,7 +94,7 @@ def swap_memory(): total, free, sin, sout = cext.swap_mem() used = total - free percent = usage_percent(used, total, round_=1) - return _common.sswap(total, used, free, percent, sin, sout) + return ntp.sswap(total, used, free, percent, sin, sout) # ===================================================================== @@ -120,13 +105,13 @@ def swap_memory(): def cpu_times(): """Return system-wide CPU times as a named tuple.""" ret = cext.per_cpu_times() - return scputimes(*[sum(x) for x in zip(*ret)]) + return ntp.scputimes(*[sum(x) for x in zip(*ret)]) def per_cpu_times(): """Return system per-CPU times as a list of named tuples.""" ret = cext.per_cpu_times() - return [scputimes(*x) for x in ret] + return [ntp.scputimes(*x) for x in ret] def cpu_count_logical(): @@ -153,9 +138,7 @@ def cpu_count_cores(): 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 - ) + return ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) # ===================================================================== @@ -183,7 +166,7 @@ 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) + ntuple = ntp.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist @@ -260,7 +243,7 @@ def net_if_stats(): output_flags = ','.join(flags) isup = 'running' in flags duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN) - ret[name] = _common.snicstats(isup, duplex, speed, mtu, output_flags) + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) return ret @@ -288,7 +271,7 @@ def users(): continue if hostname in localhost: hostname = 'localhost' - nt = _common.suser(user, tty, hostname, tstamp, pid) + nt = ntp.suser(user, tty, hostname, tstamp, pid) retlist.append(nt) return retlist @@ -420,7 +403,7 @@ def threads(self): rawlist = cext.proc_threads(self.pid) retlist = [] for thread_id, utime, stime in rawlist: - ntuple = _common.pthread(thread_id, utime, stime) + ntuple = ntp.pthread(thread_id, utime, stime) retlist.append(ntuple) # The underlying C implementation retrieves all OS threads # and filters them by PID. At this point we can't tell whether @@ -461,17 +444,17 @@ def ppid(self): @wrap_exceptions def uids(self): real, effective, saved, _, _, _ = self._proc_cred() - return _common.puids(real, effective, saved) + return ntp.puids(real, effective, saved) @wrap_exceptions def gids(self): _, _, _, real, effective, saved = self._proc_cred() - return _common.puids(real, effective, saved) + return ntp.puids(real, effective, saved) @wrap_exceptions def cpu_times(self): t = cext.proc_cpu_times(self.pid, self._procfs_path) - return _common.pcputimes(*t) + return ntp.pcputimes(*t) @wrap_exceptions def terminal(self): @@ -499,7 +482,7 @@ def memory_info(self): ret = self._proc_basic_info() rss = ret[proc_info_map['rss']] * 1024 vms = ret[proc_info_map['vms']] * 1024 - return pmem(rss, vms) + return ntp.pmem(rss, vms) memory_full_info = memory_info @@ -531,7 +514,7 @@ def open_files(self): path = path[1:] if path.lower() == "cannot be retrieved": continue - retlist.append(_common.popenfile(path, int(fd))) + retlist.append(ntp.popenfile(path, int(fd))) return retlist @wrap_exceptions @@ -542,7 +525,7 @@ def num_fds(self): @wrap_exceptions def num_ctx_switches(self): - return _common.pctxsw(*cext.proc_num_ctx_switches(self.pid)) + return ntp.pctxsw(*cext.proc_num_ctx_switches(self.pid)) @wrap_exceptions def wait(self, timeout=None): @@ -560,4 +543,4 @@ def io_counters(self): if not pid_exists(self.pid): raise NoSuchProcess(self.pid, self._name) from err raise - return _common.pio(rc, wc, rb, wb) + return ntp.pio(rc, wc, rb, wb) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index ea58c76cad..d9e6d12671 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -13,6 +13,7 @@ from xml.etree import ElementTree # noqa: ICN001 from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_bsd as cext from ._common import FREEBSD @@ -127,44 +128,6 @@ ) -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# fmt: off -# psutil.virtual_memory() -svmem = namedtuple( - 'svmem', ['total', 'available', 'percent', 'used', 'free', - 'active', 'inactive', 'buffers', 'cached', 'shared', 'wired']) -# psutil.cpu_times() -scputimes = namedtuple( - 'scputimes', ['user', 'nice', 'system', 'idle', 'irq']) -# psutil.Process.memory_info() -pmem = namedtuple('pmem', ['rss', 'vms', 'text', 'data', 'stack']) -# psutil.Process.memory_full_info() -pfullmem = pmem -# psutil.Process.cpu_times() -pcputimes = namedtuple('pcputimes', - ['user', 'system', 'children_user', 'children_system']) -# psutil.Process.memory_maps(grouped=True) -pmmap_grouped = namedtuple( - 'pmmap_grouped', 'path rss, private, ref_count, shadow_count') -# psutil.Process.memory_maps(grouped=False) -pmmap_ext = namedtuple( - 'pmmap_ext', 'addr, perms path rss, private, ref_count, shadow_count') -# psutil.disk_io_counters() -if FREEBSD: - sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', - 'read_bytes', 'write_bytes', - 'read_time', 'write_time', - 'busy_time']) -else: - sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', - 'read_bytes', 'write_bytes']) -# fmt: on - - # ===================================================================== # --- memory # ===================================================================== @@ -202,7 +165,7 @@ def virtual_memory(): used = active + wired + cached percent = usage_percent((total - avail), total, round_=1) - return svmem( + return ntp.svmem( total, avail, percent, @@ -221,7 +184,7 @@ def swap_memory(): """System swap memory as (total, used, free, sin, sout) namedtuple.""" total, used, free, sin, sout = cext.swap_mem() percent = usage_percent(used, total, round_=1) - return _common.sswap(total, used, free, percent, sin, sout) + return ntp.sswap(total, used, free, percent, sin, sout) # ===================================================================== @@ -232,7 +195,7 @@ def swap_memory(): def cpu_times(): """Return system per-CPU times as a namedtuple.""" user, nice, system, idle, irq = cext.cpu_times() - return scputimes(user, nice, system, idle, irq) + return ntp.scputimes(user, nice, system, idle, irq) def per_cpu_times(): @@ -240,7 +203,7 @@ def per_cpu_times(): ret = [] for cpu_t in cext.per_cpu_times(): user, nice, system, idle, irq = cpu_t - item = scputimes(user, nice, system, idle, irq) + item = ntp.scputimes(user, nice, system, idle, irq) ret.append(item) return ret @@ -314,7 +277,7 @@ def cpu_stats(): ctxsw, intrs, soft_intrs, syscalls, _traps, _faults, _forks = ( cext.cpu_stats() ) - return _common.scpustats(ctxsw, intrs, soft_intrs, syscalls) + return ntp.scpustats(ctxsw, intrs, soft_intrs, syscalls) if FREEBSD: @@ -340,14 +303,14 @@ def cpu_freq(): max_freq = int(available_freq.split(" ")[0].split("/")[0]) except (IndexError, ValueError): max_freq = None - ret.append(_common.scpufreq(current, min_freq, max_freq)) + ret.append(ntp.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)] + return [ntp.scpufreq(curr, 0.0, 0.0)] # ===================================================================== @@ -364,7 +327,7 @@ def disk_partitions(all=False): partitions = cext.disk_partitions() for partition in partitions: device, mountpoint, fstype, opts = partition - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + ntuple = ntp.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist @@ -400,9 +363,7 @@ def net_if_stats(): duplex = _common.NicDuplex(duplex) output_flags = ','.join(flags) isup = 'running' in flags - ret[name] = _common.snicstats( - isup, duplex, speed, mtu, output_flags - ) + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) return ret @@ -447,7 +408,7 @@ def sensors_battery(): secsleft = _common.POWER_TIME_UNKNOWN else: secsleft = minsleft * 60 - return _common.sbattery(percent, secsleft, power_plugged) + return ntp.sbattery(percent, secsleft, power_plugged) def sensors_temperatures(): """Return CPU cores temperatures if available, else an empty dict.""" @@ -459,9 +420,7 @@ def sensors_temperatures(): if high <= 0: high = None name = f"Core {cpu}" - ret["coretemp"].append( - _common.shwtemp(name, current, high, high) - ) + ret["coretemp"].append(ntp.shwtemp(name, current, high, high)) except NotImplementedError: pass @@ -510,7 +469,7 @@ def users(): user, tty, hostname, tstamp, pid = item if tty == '~': continue # reboot or shutdown - nt = _common.suser(user, tty or None, hostname, tstamp, pid) + nt = ntp.suser(user, tty or None, hostname, tstamp, pid) retlist.append(nt) return retlist @@ -722,7 +681,7 @@ def ppid(self): @wrap_exceptions def uids(self): rawtuple = self.oneshot() - return _common.puids( + return ntp.puids( rawtuple[kinfo_proc_map['real_uid']], rawtuple[kinfo_proc_map['effective_uid']], rawtuple[kinfo_proc_map['saved_uid']], @@ -731,7 +690,7 @@ def uids(self): @wrap_exceptions def gids(self): rawtuple = self.oneshot() - return _common.pgids( + return ntp.pgids( rawtuple[kinfo_proc_map['real_gid']], rawtuple[kinfo_proc_map['effective_gid']], rawtuple[kinfo_proc_map['saved_gid']], @@ -740,7 +699,7 @@ def gids(self): @wrap_exceptions def cpu_times(self): rawtuple = self.oneshot() - return _common.pcputimes( + return ntp.pcputimes( rawtuple[kinfo_proc_map['user_time']], rawtuple[kinfo_proc_map['sys_time']], rawtuple[kinfo_proc_map['ch_user_time']], @@ -756,7 +715,7 @@ def cpu_num(self): @wrap_exceptions def memory_info(self): rawtuple = self.oneshot() - return pmem( + return ntp.pmem( rawtuple[kinfo_proc_map['rss']], rawtuple[kinfo_proc_map['vms']], rawtuple[kinfo_proc_map['memtext']], @@ -785,7 +744,7 @@ def num_threads(self): @wrap_exceptions def num_ctx_switches(self): rawtuple = self.oneshot() - return _common.pctxsw( + return ntp.pctxsw( rawtuple[kinfo_proc_map['ctx_switches_vol']], rawtuple[kinfo_proc_map['ctx_switches_unvol']], ) @@ -796,7 +755,7 @@ def threads(self): rawlist = cext.proc_threads(self.pid) retlist = [] for thread_id, utime, stime in rawlist: - ntuple = _common.pthread(thread_id, utime, stime) + ntuple = ntp.pthread(thread_id, utime, stime) retlist.append(ntuple) if OPENBSD: self._assert_alive() @@ -848,7 +807,7 @@ def status(self): @wrap_exceptions def io_counters(self): rawtuple = self.oneshot() - return _common.pio( + return ntp.pio( rawtuple[kinfo_proc_map['read_io_count']], rawtuple[kinfo_proc_map['write_io_count']], -1, @@ -875,7 +834,7 @@ def cwd(self): 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] + return [ntp.popenfile(path, fd) for path, fd in rawlist] @wrap_exceptions def num_fds(self): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 821c4eb717..7126a5e0d4 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -22,6 +22,7 @@ from collections import namedtuple from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_linux as cext from ._common import ENCODING @@ -141,49 +142,6 @@ class IOPriority(enum.IntEnum): } -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# fmt: off -# psutil.virtual_memory() -svmem = namedtuple( - 'svmem', ['total', 'available', 'percent', 'used', 'free', - 'active', 'inactive', 'buffers', 'cached', 'shared', 'slab']) -# psutil.disk_io_counters() -sdiskio = namedtuple( - 'sdiskio', ['read_count', 'write_count', - 'read_bytes', 'write_bytes', - 'read_time', 'write_time', - 'read_merged_count', 'write_merged_count', - 'busy_time']) -# psutil.Process().open_files() -popenfile = namedtuple( - 'popenfile', ['path', 'fd', 'position', 'mode', 'flags']) -# psutil.Process().memory_info() -pmem = namedtuple('pmem', 'rss vms shared text lib data dirty') -# psutil.Process().memory_full_info() -pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', 'pss', 'swap')) -# psutil.Process().memory_maps(grouped=True) -pmmap_grouped = namedtuple( - 'pmmap_grouped', - ['path', 'rss', 'size', 'pss', 'shared_clean', 'shared_dirty', - 'private_clean', 'private_dirty', 'referenced', 'anonymous', 'swap']) -# psutil.Process().memory_maps(grouped=False) -pmmap_ext = namedtuple( - 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) -# psutil.Process.io_counters() -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 - - # ===================================================================== # --- utils # ===================================================================== @@ -242,14 +200,13 @@ def is_storage_device(name): @memoize -def set_scputimes_ntuple(procfs_path): - """Set a namedtuple of variable fields depending on the CPU times +def _scputimes_ntuple(procfs_path): + """Return a namedtuple of variable fields depending on the CPU times available on this Linux kernel version which may be: (user, nice, system, idle, iowait, irq, softirq, [steal, [guest, [guest_nice]]]) Used by cpu_times() function. """ - global scputimes with open_binary(f"{procfs_path}/stat") as f: values = f.readline().split()[1:] fields = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq'] @@ -263,15 +220,20 @@ def set_scputimes_ntuple(procfs_path): if vlen >= 10: # Linux >= 3.2.0 fields.append('guest_nice') - scputimes = namedtuple('scputimes', fields) + return namedtuple('scputimes', fields) +# Set it into _ntuples.py namespace. try: - set_scputimes_ntuple("/proc") + ntp.scputimes = _scputimes_ntuple("/proc") except Exception as err: # noqa: BLE001 # Don't want to crash at import time. debug(f"ignoring exception on import: {err!r}") - scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) + ntp.scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) + +# XXX: must be available also at this module level in order to be +# serialized (tests/test_misc.py::TestMisc::test_serialization). +scputimes = ntp.scputimes # ===================================================================== @@ -456,7 +418,7 @@ def virtual_memory(): ) warnings.warn(msg, RuntimeWarning, stacklevel=2) - return svmem( + return ntp.svmem( total, avail, percent, @@ -523,7 +485,7 @@ def swap_memory(): 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) + return ntp.sswap(total, used, free, percent, sin, sout) # ===================================================================== @@ -539,12 +501,11 @@ def cpu_times(): Last 3 fields may not be available on all Linux kernel versions. """ procfs_path = get_procfs_path() - set_scputimes_ntuple(procfs_path) with open_binary(f"{procfs_path}/stat") as f: values = f.readline().split() - fields = values[1 : len(scputimes._fields) + 1] + fields = values[1 : len(ntp.scputimes._fields) + 1] fields = [float(x) / CLOCK_TICKS for x in fields] - return scputimes(*fields) + return ntp.scputimes(*fields) def per_cpu_times(): @@ -552,7 +513,6 @@ def per_cpu_times(): for every CPU available on the system. """ procfs_path = get_procfs_path() - set_scputimes_ntuple(procfs_path) cpus = [] with open_binary(f"{procfs_path}/stat") as f: # get rid of the first line which refers to system wide CPU stats @@ -560,9 +520,9 @@ 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(ntp.scputimes._fields) + 1] fields = [float(x) / CLOCK_TICKS for x in fields] - entry = scputimes(*fields) + entry = ntp.scputimes(*fields) cpus.append(entry) return cpus @@ -658,9 +618,7 @@ def cpu_stats(): ): break syscalls = 0 - return _common.scpustats( - ctx_switches, interrupts, soft_interrupts, syscalls - ) + return ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) def _cpu_get_cpuinfo_freq(): @@ -704,14 +662,14 @@ def cpu_freq(): online_path = f"/sys/devices/system/cpu/cpu{i}/online" # if cpu core is offline, set to all zeroes if cat(online_path, fallback=None) == "0\n": - ret.append(_common.scpufreq(0.0, 0.0, 0.0)) + ret.append(ntp.scpufreq(0.0, 0.0, 0.0)) continue msg = "can't find current frequency file" raise NotImplementedError(msg) curr = int(curr) / 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_)) + ret.append(ntp.scpufreq(curr, min_, max_)) return ret else: @@ -720,7 +678,7 @@ 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()] + return [ntp.scpufreq(x, 0.0, 0.0) for x in _cpu_get_cpuinfo_freq()] # ===================================================================== @@ -861,7 +819,7 @@ def decode_address(addr, family): if not supports_ipv6(): raise _Ipv6UnsupportedError from None raise - return _common.addr(ip, port) + return ntp.addr(ip, port) @staticmethod def process_inet(file, family, type_, inodes, filter_pid=None): @@ -962,11 +920,9 @@ def retrieve(self, kind, pid=None): 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 = ntp.pconn(fd, family, type_, laddr, raddr, status) else: - conn = _common.sconn( + conn = ntp.sconn( fd, family, type_, laddr, raddr, status, bound_pid ) ret.add(conn) @@ -1050,7 +1006,7 @@ def net_if_stats(): else: output_flags = ','.join(flags) isup = 'running' in flags - ret[name] = _common.snicstats( + ret[name] = ntp.snicstats( isup, duplex_map[duplex], speed, mtu, output_flags ) return ret @@ -1277,7 +1233,7 @@ def disk_partitions(all=False): if not all: if not device or fstype not in fstypes: continue - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + ntuple = ntp.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist @@ -1432,7 +1388,7 @@ def sensors_fans(): continue 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)) + ret[unit_name].append(ntp.sfan(label, current)) return dict(ret) @@ -1524,7 +1480,7 @@ def multi_bcat(*paths): else: secsleft = _common.POWER_TIME_UNKNOWN - return _common.sbattery(percent, secsleft, power_plugged) + return ntp.sbattery(percent, secsleft, power_plugged) # ===================================================================== @@ -1538,7 +1494,7 @@ def users(): rawlist = cext.users() for item in rawlist: user, tty, hostname, tstamp, pid = item - nt = _common.suser(user, tty or None, hostname, tstamp, pid) + nt = ntp.suser(user, tty or None, hostname, tstamp, pid) retlist.append(nt) return retlist @@ -1848,7 +1804,7 @@ def io_counters(self): msg = f"{fname} file was empty" raise RuntimeError(msg) try: - return pio( + return ntp.pio( fields[b'syscr'], # read syscalls fields[b'syscw'], # write syscalls fields[b'read_bytes'], # read bytes @@ -1871,7 +1827,9 @@ def cpu_times(self): 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) + return ntp.pcputimes( + utime, stime, children_utime, children_stime, iowait + ) @wrap_exceptions def cpu_num(self): @@ -1916,7 +1874,7 @@ def memory_info(self): 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) + return ntp.pmem(rss, vms, shared, text, lib, data, dirty) if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS: @@ -1982,7 +1940,7 @@ def memory_full_info(self): else: uss, pss, swap = self._parse_smaps() basic_mem = self.memory_info() - return pfullmem(*basic_mem + (uss, pss, swap)) + return ntp.pfullmem(*basic_mem + (uss, pss, swap)) else: memory_full_info = memory_info @@ -2081,7 +2039,7 @@ def num_ctx_switches( " probably older than 2.6.23" ) raise NotImplementedError(msg) - return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) + return ntp.pctxsw(int(ctxsw[0]), int(ctxsw[1])) @wrap_exceptions def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')): @@ -2110,7 +2068,7 @@ def threads(self): values = st.split(b' ') utime = float(values[11]) / CLOCK_TICKS stime = float(values[12]) / CLOCK_TICKS - ntuple = _common.pthread(int(thread_id), utime, stime) + ntuple = ntp.pthread(int(thread_id), utime, stime) retlist.append(ntuple) if hit_enoent: self._raise_if_not_alive() @@ -2177,7 +2135,7 @@ def cpu_affinity_set(self, cpus): def ionice_get(self): ioclass, value = cext.proc_ioprio_get(self.pid) ioclass = IOPriority(ioclass) - return _common.pionice(ioclass, value) + return ntp.pionice(ioclass, value) @wrap_exceptions def ionice_set(self, ioclass, value): @@ -2271,7 +2229,7 @@ def open_files(self): hit_enoent = True else: mode = file_flags_to_mode(flags) - ntuple = popenfile( + ntuple = ntp.popenfile( path, int(fd), int(pos), mode, flags ) retlist.append(ntuple) @@ -2297,10 +2255,10 @@ def ppid(self): def uids(self, _uids_re=re.compile(br'Uid:\t(\d+)\t(\d+)\t(\d+)')): data = self._read_status_file() real, effective, saved = _uids_re.findall(data)[0] - return _common.puids(int(real), int(effective), int(saved)) + return ntp.puids(int(real), int(effective), int(saved)) @wrap_exceptions def gids(self, _gids_re=re.compile(br'Gid:\t(\d+)\t(\d+)\t(\d+)')): data = self._read_status_file() real, effective, saved = _gids_re.findall(data)[0] - return _common.pgids(int(real), int(effective), int(saved)) + return ntp.pgids(int(real), int(effective), int(saved)) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index e1030b654b..06e28f58f0 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -7,9 +7,9 @@ import errno import functools import os -from collections import namedtuple from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_osx as cext from ._common import AccessDenied @@ -83,25 +83,6 @@ ) -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# fmt: off -# psutil.cpu_times() -scputimes = namedtuple('scputimes', ['user', 'nice', 'system', 'idle']) -# psutil.virtual_memory() -svmem = namedtuple( - 'svmem', ['total', 'available', 'percent', 'used', 'free', - 'active', 'inactive', 'wired']) -# psutil.Process.memory_info() -pmem = namedtuple('pmem', ['rss', 'vms', 'pfaults', 'pageins']) -# psutil.Process.memory_full_info() -pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) -# fmt: on - - # ===================================================================== # --- memory # ===================================================================== @@ -119,14 +100,16 @@ def virtual_memory(): # cmdline utility. free -= speculative percent = usage_percent((total - avail), total, round_=1) - return svmem(total, avail, percent, used, free, active, inactive, wired) + return ntp.svmem( + total, avail, percent, used, free, active, inactive, wired + ) def swap_memory(): """Swap system memory as a (total, used, free, sin, sout) tuple.""" total, used, free, sin, sout = cext.swap_mem() percent = usage_percent(used, total, round_=1) - return _common.sswap(total, used, free, percent, sin, sout) + return ntp.sswap(total, used, free, percent, sin, sout) # ===================================================================== @@ -137,7 +120,7 @@ def swap_memory(): def cpu_times(): """Return system CPU times as a namedtuple.""" user, nice, system, idle = cext.cpu_times() - return scputimes(user, nice, system, idle) + return ntp.scputimes(user, nice, system, idle) def per_cpu_times(): @@ -145,7 +128,7 @@ def per_cpu_times(): ret = [] for cpu_t in cext.per_cpu_times(): user, nice, system, idle = cpu_t - item = scputimes(user, nice, system, idle) + item = ntp.scputimes(user, nice, system, idle) ret.append(item) return ret @@ -164,9 +147,7 @@ def cpu_stats(): ctx_switches, interrupts, soft_interrupts, syscalls, _traps = ( cext.cpu_stats() ) - return _common.scpustats( - ctx_switches, interrupts, soft_interrupts, syscalls - ) + return ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) if cext.has_cpu_freq(): # not always available on ARM64 @@ -178,7 +159,7 @@ def cpu_freq(): https://arstechnica.com/civis/viewtopic.php?f=19&t=465002. """ curr, min_, max_ = cext.cpu_freq() - return [_common.scpufreq(curr, min_, max_)] + return [ntp.scpufreq(curr, min_, max_)] # ===================================================================== @@ -201,7 +182,7 @@ 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) + ntuple = ntp.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist @@ -225,7 +206,7 @@ def sensors_battery(): secsleft = _common.POWER_TIME_UNKNOWN else: secsleft = minsleft * 60 - return _common.sbattery(percent, secsleft, power_plugged) + return ntp.sbattery(percent, secsleft, power_plugged) # ===================================================================== @@ -251,7 +232,7 @@ def net_connections(kind='inet'): if cons: for c in cons: c = list(c) + [pid] - ret.append(_common.sconn(*c)) + ret.append(ntp.sconn(*c)) return ret @@ -273,9 +254,7 @@ def net_if_stats(): duplex = _common.NicDuplex(duplex) output_flags = ','.join(flags) isup = 'running' in flags - ret[name] = _common.snicstats( - isup, duplex, speed, mtu, output_flags - ) + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) return ret @@ -322,7 +301,7 @@ def users(): continue # reboot or shutdown if not tstamp: continue - nt = _common.suser(user, tty or None, hostname or None, tstamp, pid) + nt = ntp.suser(user, tty or None, hostname or None, tstamp, pid) retlist.append(nt) return retlist @@ -436,7 +415,7 @@ def cwd(self): @wrap_exceptions def uids(self): rawtuple = self._get_kinfo_proc() - return _common.puids( + return ntp.puids( rawtuple[kinfo_proc_map['ruid']], rawtuple[kinfo_proc_map['euid']], rawtuple[kinfo_proc_map['suid']], @@ -445,7 +424,7 @@ def uids(self): @wrap_exceptions def gids(self): rawtuple = self._get_kinfo_proc() - return _common.puids( + return ntp.puids( rawtuple[kinfo_proc_map['rgid']], rawtuple[kinfo_proc_map['egid']], rawtuple[kinfo_proc_map['sgid']], @@ -463,7 +442,7 @@ def terminal(self): @wrap_exceptions def memory_info(self): rawtuple = self._get_pidtaskinfo() - return pmem( + return ntp.pmem( rawtuple[pidtaskinfo_map['rss']], rawtuple[pidtaskinfo_map['vms']], rawtuple[pidtaskinfo_map['pfaults']], @@ -474,12 +453,12 @@ 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 ntp.pfullmem(*basic_mem + (uss,)) @wrap_exceptions def cpu_times(self): rawtuple = self._get_pidtaskinfo() - return _common.pcputimes( + return ntp.pcputimes( rawtuple[pidtaskinfo_map['cpuutime']], rawtuple[pidtaskinfo_map['cpustime']], # children user / system times are not retrievable (set to 0) @@ -500,7 +479,7 @@ def num_ctx_switches(self): # getrusage() numbers seems to confirm this theory. # We set it to 0. vol = self._get_pidtaskinfo()[pidtaskinfo_map['volctxsw']] - return _common.pctxsw(vol, 0) + return ntp.pctxsw(vol, 0) @wrap_exceptions def num_threads(self): @@ -514,7 +493,7 @@ def open_files(self): rawlist = cext.proc_open_files(self.pid) for path, fd in rawlist: if isfile_strict(path): - ntuple = _common.popenfile(path, fd) + ntuple = ntp.popenfile(path, fd) files.append(ntuple) return files @@ -560,6 +539,6 @@ def threads(self): rawlist = cext.proc_threads(self.pid) retlist = [] for thread_id, utime, stime in rawlist: - ntuple = _common.pthread(thread_id, utime, stime) + ntuple = ntp.pthread(thread_id, utime, stime) retlist.append(ntuple) return retlist diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 83f1acdbe3..efdf1eb850 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -10,10 +10,10 @@ import signal import time +from . import _ntuples as ntp from ._common import MACOS from ._common import TimeoutExpired from ._common import memoize -from ._common import sdiskusage from ._common import usage_percent if MACOS: @@ -185,7 +185,7 @@ def disk_usage(path): # NB: the percentage is -5% than what shown by df due to # reserved blocks that we are currently not considering: # https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462 - return sdiskusage( + return ntp.sdiskusage( total=total, used=used, free=avail_to_user, percent=usage_percent_user ) diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index e3268932f6..89c6055c49 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -14,6 +14,7 @@ from socket import AF_INET from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_sunos as cext from ._common import AF_INET6 @@ -87,32 +88,6 @@ ) -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# psutil.cpu_times() -scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait']) -# psutil.cpu_times(percpu=True) -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'] -) -# psutil.Process.memory_maps(grouped=False) -pmmap_ext = namedtuple( - 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields) -) - - # ===================================================================== # --- memory # ===================================================================== @@ -126,7 +101,7 @@ def virtual_memory(): free = avail = os.sysconf('SC_AVPHYS_PAGES') * PAGE_SIZE used = total - free percent = usage_percent(used, total, round_=1) - return svmem(total, avail, percent, used, free) + return ntp.svmem(total, avail, percent, used, free) def swap_memory(): @@ -165,7 +140,7 @@ def swap_memory(): free += int(int(f) * 512) used = total - free percent = usage_percent(used, total, round_=1) - return _common.sswap( + return ntp.sswap( total, used, free, percent, sin * PAGE_SIZE, sout * PAGE_SIZE ) @@ -178,13 +153,13 @@ def swap_memory(): def cpu_times(): """Return system-wide CPU times as a named tuple.""" ret = cext.per_cpu_times() - return scputimes(*[sum(x) for x in zip(*ret)]) + return ntp.scputimes(*[sum(x) for x in zip(*ret)]) def per_cpu_times(): """Return system per-CPU times as a list of named tuples.""" ret = cext.per_cpu_times() - return [scputimes(*x) for x in ret] + return [ntp.scputimes(*x) for x in ret] def cpu_count_logical(): @@ -205,9 +180,7 @@ 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 ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) # ===================================================================== @@ -240,7 +213,7 @@ def disk_partitions(all=False): # https://github.com/giampaolo/psutil/issues/1674 debug(f"skipping {mountpoint!r}: {err}") continue - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + ntuple = ntp.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist @@ -271,16 +244,16 @@ def net_connections(kind, _pid=-1): # TODO: refactor and use _common.conn_to_ntuple. if fam in {AF_INET, AF_INET6}: if laddr: - laddr = _common.addr(*laddr) + laddr = ntp.addr(*laddr) if raddr: - raddr = _common.addr(*raddr) + raddr = ntp.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) + nt = ntp.sconn(fd, fam, type_, laddr, raddr, status, pid) else: - nt = _common.pconn(fd, fam, type_, laddr, raddr, status) + nt = ntp.pconn(fd, fam, type_, laddr, raddr, status) ret.add(nt) return list(ret) @@ -292,7 +265,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] = ntp.snicstats(isup, duplex, speed, mtu, '') return ret @@ -320,7 +293,7 @@ def users(): continue if hostname in localhost: hostname = 'localhost' - nt = _common.suser(user, tty, hostname, tstamp, pid) + nt = ntp.suser(user, tty, hostname, tstamp, pid) retlist.append(nt) return retlist @@ -481,7 +454,7 @@ def uids(self): real = self._proc_basic_info()[proc_info_map['uid']] effective = self._proc_basic_info()[proc_info_map['euid']] saved = None - return _common.puids(real, effective, saved) + return ntp.puids(real, effective, saved) @wrap_exceptions def gids(self): @@ -491,7 +464,7 @@ def gids(self): real = self._proc_basic_info()[proc_info_map['gid']] effective = self._proc_basic_info()[proc_info_map['egid']] saved = None - return _common.puids(real, effective, saved) + return ntp.puids(real, effective, saved) @wrap_exceptions def cpu_times(self): @@ -509,7 +482,7 @@ def cpu_times(self): times = (0.0, 0.0, 0.0, 0.0) else: raise - return _common.pcputimes(*times) + return ntp.pcputimes(*times) @wrap_exceptions def cpu_num(self): @@ -548,7 +521,7 @@ def memory_info(self): ret = self._proc_basic_info() rss = ret[proc_info_map['rss']] * 1024 vms = ret[proc_info_map['vms']] * 1024 - return pmem(rss, vms) + return ntp.pmem(rss, vms) memory_full_info = memory_info @@ -586,7 +559,7 @@ def threads(self): continue raise else: - nt = _common.pthread(tid, utime, stime) + nt = ntp.pthread(tid, utime, stime) ret.append(nt) if hit_enoent: self._assert_alive() @@ -608,7 +581,7 @@ def open_files(self): continue else: if isfile_strict(file): - retlist.append(_common.popenfile(file, int(fd))) + retlist.append(ntp.popenfile(file, int(fd))) if hit_enoent: self._assert_alive() return retlist @@ -661,10 +634,9 @@ def net_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( + [ntp.pconn(*conn) for conn in self._get_unix_sockets(self.pid)] + ) return ret nt_mmap_grouped = namedtuple('mmap', 'path rss anon locked') @@ -723,7 +695,7 @@ def num_fds(self): @wrap_exceptions def num_ctx_switches(self): - return _common.pctxsw( + return ntp.pctxsw( *cext.proc_num_ctx_switches(self.pid, self._procfs_path) ) diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 7d343f26bb..c22071965e 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -12,9 +12,9 @@ import sys import threading import time -from collections import namedtuple from . import _common +from . import _ntuples as ntp from ._common import ENCODING from ._common import AccessDenied from ._common import NoSuchProcess @@ -145,37 +145,6 @@ class IOPriority(enum.IntEnum): ) -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# fmt: off -# psutil.cpu_times() -scputimes = namedtuple('scputimes', - ['user', 'system', 'idle', 'interrupt', 'dpc']) -# psutil.virtual_memory() -svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) -# psutil.Process.memory_info() -pmem = namedtuple( - 'pmem', ['rss', 'vms', - 'num_page_faults', 'peak_wset', 'wset', 'peak_paged_pool', - 'paged_pool', 'peak_nonpaged_pool', 'nonpaged_pool', - 'pagefile', 'peak_pagefile', 'private']) -# psutil.Process.memory_full_info() -pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) -# psutil.Process.memory_maps(grouped=True) -pmmap_grouped = namedtuple('pmmap_grouped', ['path', 'rss']) -# psutil.Process.memory_maps(grouped=False) -pmmap_ext = namedtuple( - 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) -# psutil.Process.io_counters() -pio = namedtuple('pio', ['read_count', 'write_count', - 'read_bytes', 'write_bytes', - 'other_count', 'other_bytes']) -# fmt: on - - # ===================================================================== # --- utils # ===================================================================== @@ -222,7 +191,7 @@ def virtual_memory(): free = availphys used = total - avail percent = usage_percent((total - avail), total, round_=1) - return svmem(total, avail, percent, used, free) + return ntp.svmem(total, avail, percent, used, free) def swap_memory(): @@ -248,7 +217,7 @@ def swap_memory(): free = total - used percent = round(percentswap, 1) - return _common.sswap(total, used, free, percent, 0, 0) + return ntp.sswap(total, used, free, percent, 0, 0) # ===================================================================== @@ -267,13 +236,13 @@ def disk_usage(path): path = path.decode(ENCODING, errors="strict") total, used, free = cext.disk_usage(path) percent = usage_percent(used, total, round_=1) - return _common.sdiskusage(total, used, free, percent) + return ntp.sdiskusage(total, used, free, percent) def disk_partitions(all): """Return disk partitions.""" rawlist = cext.disk_partitions(all) - return [_common.sdiskpart(*x) for x in rawlist] + return [ntp.sdiskpart(*x) for x in rawlist] # ===================================================================== @@ -287,8 +256,10 @@ def cpu_times(): # Internally, GetSystemTimes() is used, and it doesn't return # 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( + percpu_summed = ntp.scputimes( + *[sum(n) for n in zip(*cext.per_cpu_times())] + ) + return ntp.scputimes( user, system, idle, percpu_summed.interrupt, percpu_summed.dpc ) @@ -297,7 +268,7 @@ def per_cpu_times(): """Return system per-CPU times as a list of named tuples.""" ret = [] for user, system, idle, interrupt, dpc in cext.per_cpu_times(): - item = scputimes(user, system, idle, interrupt, dpc) + item = ntp.scputimes(user, system, idle, interrupt, dpc) ret.append(item) return ret @@ -316,9 +287,7 @@ 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 ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) def cpu_freq(): @@ -327,7 +296,7 @@ def cpu_freq(): """ curr, max_ = cext.cpu_freq() min_ = 0.0 - return [_common.scpufreq(float(curr), min_, float(max_))] + return [ntp.scpufreq(float(curr), min_, float(max_))] _loadavg_initialized = False @@ -393,7 +362,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] = ntp.snicstats(isup, duplex, speed, mtu, '') return ret @@ -431,7 +400,7 @@ def sensors_battery(): elif secsleft == -1: secsleft = _common.POWER_TIME_UNKNOWN - return _common.sbattery(percent, secsleft, power_plugged) + return ntp.sbattery(percent, secsleft, power_plugged) # ===================================================================== @@ -464,7 +433,7 @@ def users(): rawlist = cext.users() for item in rawlist: user, hostname, tstamp = item - nt = _common.suser(user, None, hostname, tstamp, None) + nt = ntp.suser(user, None, hostname, tstamp, None) retlist.append(nt) return retlist @@ -843,14 +812,14 @@ def memory_info(self): t = self._get_raw_meminfo() rss = t[2] # wset vms = t[7] # pagefile - return pmem(*(rss, vms) + t) + return ntp.pmem(*(rss, vms) + t) @wrap_exceptions def memory_full_info(self): basic_mem = self.memory_info() uss = cext.proc_memory_uss(self.pid) uss *= getpagesize() - return pfullmem(*basic_mem + (uss,)) + return ntp.pfullmem(*basic_mem + (uss,)) def memory_maps(self): try: @@ -956,7 +925,7 @@ def threads(self): rawlist = cext.proc_threads(self.pid) retlist = [] for thread_id, utime, stime in rawlist: - ntuple = _common.pthread(thread_id, utime, stime) + ntuple = ntp.pthread(thread_id, utime, stime) retlist.append(ntuple) return retlist @@ -972,7 +941,7 @@ def cpu_times(self): 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) + return ntp.pcputimes(user, system, 0.0, 0.0) @wrap_exceptions def suspend(self): @@ -1005,7 +974,7 @@ def open_files(self): for file in raw_file_names: file = convert_dos_path(file) if isfile_strict(file): - ntuple = _common.popenfile(file, -1) + ntuple = ntp.popenfile(file, -1) ret.add(ntuple) return list(ret) @@ -1061,7 +1030,7 @@ def io_counters(self): info[pinfo_map['io_count_others']], info[pinfo_map['io_bytes_others']], ) - return pio(*ret) + return ntp.pio(*ret) @wrap_exceptions def status(self): @@ -1119,4 +1088,4 @@ def num_handles(self): def num_ctx_switches(self): ctx_switches = self._proc_info()[pinfo_map['ctx_switches']] # only voluntary ctx switches are supported - return _common.pctxsw(ctx_switches, 0) + return ntp.pctxsw(ctx_switches, 0) diff --git a/tests/test_testutils.py b/tests/test_testutils.py index 86f6386848..7231a73760 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -404,7 +404,7 @@ def fun(ls=ls): # will consume around 60M in total with pytest.raises( AssertionError, - match=rf"Run \#{MemoryLeakTestCase.retries}", + match=r"Memory kept increasing", ): with contextlib.redirect_stdout( io.StringIO() From c15b01945b10049ac82f20ea9e46a9d181638c18 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 8 Nov 2025 19:33:49 +0100 Subject: [PATCH 1446/1714] @retry_on_failure deco: call setUp / tearDown before retry --- tests/__init__.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 5d6f4f74f0..c12ce15e75 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1291,18 +1291,40 @@ def iter(ls): def retry_on_failure(retries=NO_RETRIES): """Decorator which runs a test function and retries N times before - actually failing. + giving up and failing. """ - def logfun(exc): - print(f"{exc!r}, retrying", file=sys.stderr) # noqa: T201 + def decorator(test_method): + @functools.wraps(test_method) + def wrapper(self, *args, **kwargs): + err = None + for attempt in range(retries): + try: + return test_method(self, *args, **kwargs) + except (AssertionError, pytest.fail.Exception) as _: + err = _ + prefix = "\n" if attempt == 0 else "" + short_err = str(err).split("\n")[0] + print( # noqa: T201 + f"{prefix}{short_err}, retrying" + f" {attempt + 1}/{retries} ...", + file=sys.stderr, + ) + if hasattr(self, "tearDown"): + self.tearDown() + if hasattr(self, "teardown_method"): + self.teardown_method() + if hasattr(self, "setUp"): + self.setUp() + if hasattr(self, "setup_method"): + self.setup_method() - return retry( - exception=(AssertionError, pytest.fail.Exception), - timeout=None, - retries=retries, - logfun=logfun, - ) + raise err + + return wrapper + + assert retries > 1, retries + return decorator def skip_on_access_denied(only_if=None): From 1de683b23f03eb8e7d6e8567622e32302af325e2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 10 Nov 2025 16:46:42 +0100 Subject: [PATCH 1447/1714] Make new black version happy --- .github/workflows/issues.py | 1 - psutil/_pslinux.py | 1 - psutil/_psutil_bsd.c | 4 +++- psutil/_psutil_linux.c | 4 ++-- psutil/_psutil_osx.c | 4 ++-- psutil/_psutil_sunos.c | 9 +++++---- psutil/_psutil_windows.c | 4 +++- psutil/arch/freebsd/mem.c | 1 - scripts/battery.py | 1 - scripts/cpu_distribution.py | 1 - scripts/fans.py | 1 - scripts/ifconfig.py | 1 - scripts/internal/bench_oneshot.py | 1 - scripts/internal/find_broken_links.py | 1 - scripts/internal/print_access_denied.py | 1 - scripts/internal/print_api_speed.py | 1 - scripts/internal/print_downloads.py | 1 - scripts/pidof.py | 1 - scripts/procsmem.py | 1 - scripts/pstree.py | 1 - scripts/sensors.py | 1 - scripts/temperatures.py | 1 - scripts/winservices.py | 1 - tests/test_linux.py | 1 - 24 files changed, 15 insertions(+), 29 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 0f8bd08d48..1d071ed5dc 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -9,7 +9,6 @@ on the situation. """ - import functools import json import os diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7126a5e0d4..fd1926af0c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -4,7 +4,6 @@ """Linux platform implementation.""" - import base64 import collections import enum diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 02373b6cc2..d04aa46258 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -4,7 +4,9 @@ * All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. - * + */ + +/* * Platform-specific module methods for FreeBSD and OpenBSD. * OpenBSD references: diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 2b6589c2cf..f66cae8f4b 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -2,10 +2,10 @@ * 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 functions. */ +// Linux-specific functions. + #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 7e345010be..29119cef7e 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -2,10 +2,10 @@ * Copyright (c) 2009, Jay Loden, 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 platform-specific module methods. */ +// macOS platform-specific module methods. + #include #include // needed for old macOS versions #include diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 6cdc83d57e..e8ca62a74a 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -2,14 +2,15 @@ * 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. - * + */ + +/* * Functions specific to Sun OS Solaris platforms. * * Thanks to Justin Venus who originally wrote a consistent part of * this in Cython which I later on translated in C. - */ - -/* fix compilation issue on SunOS 5.10, see: + * + * Fix compilation issue on SunOS 5.10, see: * https://github.com/giampaolo/psutil/issues/421 * https://github.com/giampaolo/psutil/issues/1077 */ diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 1608ac2d1f..a3d3667941 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2,7 +2,9 @@ * Copyright (c) 2009, Jay Loden, 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 platform-specific module methods for _psutil_windows. * * List of undocumented Windows NT APIs which are used in here and in diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 4246087d72..52644a8d3b 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -4,7 +4,6 @@ * found in the LICENSE file. */ - #include #include #include diff --git a/scripts/battery.py b/scripts/battery.py index b6d4679ea0..d73b8cb8b6 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -13,7 +13,6 @@ plugged in: no """ - import sys import psutil diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index c88514f2e3..4b39bddc2d 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -38,7 +38,6 @@ kwork """ - import collections import os import shutil diff --git a/scripts/fans.py b/scripts/fans.py index 950da7bd21..283542c6ca 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -11,7 +11,6 @@ cpu_fan 3200 RPM """ - import sys import psutil diff --git a/scripts/ifconfig.py b/scripts/ifconfig.py index 425a347826..2c3b7de5b4 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -42,7 +42,6 @@ broadcast : ff:ff:ff:ff:ff:ff """ - import socket import psutil diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index ee665fd81b..276ed72dda 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -9,7 +9,6 @@ See: https://github.com/giampaolo/psutil/issues/799. """ - import sys import textwrap import timeit diff --git a/scripts/internal/find_broken_links.py b/scripts/internal/find_broken_links.py index 759ace0a78..246c9c050f 100755 --- a/scripts/internal/find_broken_links.py +++ b/scripts/internal/find_broken_links.py @@ -38,7 +38,6 @@ Author: Himanshu Shekhar (2017) """ - import argparse import concurrent.futures import functools diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index b92e9bfc24..3834b0f187 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -44,7 +44,6 @@ Totals: access-denied=1744, calls=10020, processes=334 """ - import time from collections import defaultdict diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 6d394cdb95..f4eb395e26 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -68,7 +68,6 @@ memory_maps 300 0.74281 """ - import argparse import inspect import os diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index ddbb7591f8..6330793f67 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -11,7 +11,6 @@ * https://hugovk.github.io/top-pypi-packages/. """ - import json import os import shlex diff --git a/scripts/pidof.py b/scripts/pidof.py index 7feb464f3f..274541dc6d 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -11,7 +11,6 @@ 1140 1138 1136 1134 1133 1129 1127 1125 1121 1120 1119 """ - import sys import psutil diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 0b5f5c1fc8..c14f480d4a 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -35,7 +35,6 @@ """ - import sys import psutil diff --git a/scripts/pstree.py b/scripts/pstree.py index 51e3ee6cbf..694c815e72 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -27,7 +27,6 @@ ... """ - import collections import sys diff --git a/scripts/sensors.py b/scripts/sensors.py index ba7a9edca0..726b53d52c 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -27,7 +27,6 @@ plugged in: yes """ - import psutil diff --git a/scripts/temperatures.py b/scripts/temperatures.py index 6bc0787660..f3486b8817 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -21,7 +21,6 @@ Core 3 54.0 °C (high = 100.0 °C, critical = 100.0 °C) """ - import sys import psutil diff --git a/scripts/winservices.py b/scripts/winservices.py index 79d7c9c6e8..bb58287506 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -29,7 +29,6 @@ ... """ - import os import sys diff --git a/tests/test_linux.py b/tests/test_linux.py index aec75aa13f..33417e5062 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -6,7 +6,6 @@ """Linux specific tests.""" - import collections import contextlib import errno From a956ad98e5e6c923c597d9bd9aa5ee1d33b0616c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 12 Nov 2025 21:25:56 +0100 Subject: [PATCH 1448/1714] Refactor GIT hook script --- psutil/_common.py | 4 +- scripts/internal/git_pre_commit.py | 151 +++++++++++++---------------- 2 files changed, 71 insertions(+), 84 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 0aa9335e9b..1046a03d2a 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -772,11 +772,9 @@ def term_supports_colors(file=sys.stdout): # pragma: no cover assert file.isatty() curses.setupterm() - assert curses.tigetnum("colors") > 0 + return curses.tigetnum("colors") > 0 except Exception: # noqa: BLE001 return False - else: - return True def hilite(s, color=None, bold=False): # pragma: no cover diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 053ee01bce..08574ae46a 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -26,10 +26,9 @@ def term_supports_colors(): assert sys.stderr.isatty() curses.setupterm() - assert curses.tigetnum("colors") > 0 + return curses.tigetnum("colors") > 0 except Exception: # noqa: BLE001 return False - return True def hilite(s, ok=True, bold=False): @@ -40,16 +39,16 @@ def hilite(s, ok=True, bold=False): if ok is None: # no color pass elif ok: # green - attr.append('32') + attr.append("32") else: # red - attr.append('31') + attr.append("31") if bold: - attr.append('1') + attr.append("1") return f"\x1b[{';'.join(attr)}m{s}\x1b[0m" -def exit(msg): - print(hilite("commit aborted: " + msg, ok=False), file=sys.stderr) +def exit_with(msg): + print(hilite("Commit aborted. " + msg, ok=False), file=sys.stderr) sys.exit(1) @@ -67,119 +66,109 @@ def sh(cmd): raise RuntimeError(stderr) if stderr: print(stderr, file=sys.stderr) - if stdout.endswith('\n'): - stdout = stdout[:-1] - return stdout + return stdout.rstrip() def git_commit_files(): - out = sh(["git", "diff", "--cached", "--name-only"]) - py_files = [ - x for x in out.split('\n') if x.endswith('.py') and os.path.exists(x) - ] - c_files = [ - x - for x in out.split('\n') - if x.endswith(('.c', '.h')) and os.path.exists(x) - ] - rst_files = [ - x for x in out.split('\n') if x.endswith('.rst') and os.path.exists(x) - ] - toml_files = [ - x for x in out.split("\n") if x.endswith(".toml") and os.path.exists(x) + out = [ + f + for f in sh(["git", "diff", "--cached", "--name-only"]).splitlines() + if os.path.exists(f) ] + + py = [f for f in out if f.endswith(".py")] + c = [f for f in out if f.endswith((".c", ".h"))] + rst = [f for f in out if f.endswith(".rst")] + toml = [f for f in out if f.endswith(".toml")] + # XXX: we should escape spaces and possibly other amenities here new_rm_mv = sh( ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] - ) - # XXX: we should escape spaces and possibly other amenities here - new_rm_mv = new_rm_mv.split() - return (py_files, c_files, rst_files, toml_files, new_rm_mv) + ).split() + return py, c, rst, toml, new_rm_mv -# --- linters +def run_cmd(base_cmd, files, tool, fixer=""): + if not files: + return + cmd = base_cmd + files + if subprocess.call(cmd) != 0: + msg = f"'{tool}' failed." + if fixer: + msg += f" Try running '{fixer}'." + exit_with(msg) def black(files): - print(f"running black ({len(files)})") - cmd = [PYTHON, "-m", "black", "--check", "--safe"] + files - if subprocess.call(cmd) != 0: - return exit( - "Python code didn't pass 'ruff' style check." - "Try running 'make fix-ruff'." - ) + run_cmd( + [PYTHON, "-m", "black", "--check", "--safe"], + files, + "black", + fixer="fix-black", + ) def ruff(files): - print(f"running ruff ({len(files)})") - cmd = [ - PYTHON, - "-m", + run_cmd( + [ + PYTHON, + "-m", + "ruff", + "check", + "--no-cache", + "--output-format=concise", + ], + files, "ruff", - "check", - "--no-cache", - "--output-format=concise", - ] + files - if subprocess.call(cmd) != 0: - return exit( - "Python code didn't pass 'ruff' style check." - "Try running 'make fix-ruff'." - ) + fixer="fix-ruff", + ) def clang_format(files): if not LINUX and not shutil.which("clang-format"): - print("clang-format not installed; skip lint check") - return - print("running clang-format") - cmd = ["clang-format", "--dry-run", "--Werror"] + files - if subprocess.call(cmd) != 0: - return sys.exit("code didn't pass clang-format check") + return print("clang-format not installed; skip lint check") + run_cmd( + ["clang-format", "--dry-run", "--Werror"], + files, + "clang-format", + fixer="fix-c", + ) def toml_sort(files): - print(f"running toml linter ({len(files)})") - cmd = ["toml-sort", "--check"] + files - if subprocess.call(cmd) != 0: - return sys.exit(f"{' '.join(files)} didn't pass style check") + run_cmd(["toml-sort", "--check"], files, "toml-sort", fixer="fix-toml") def rstcheck(files): - print(f"running rst linter ({len(files)})") - cmd = ["rstcheck", "--config=pyproject.toml"] + files - if subprocess.call(cmd) != 0: - return sys.exit("RST code didn't pass style check") + run_cmd(["rstcheck", "--config=pyproject.toml"], files, "rstcheck") def dprint(): - print("running dprint") - cmd = ["dprint", "check", "--list-different"] - if subprocess.call(cmd) != 0: - return sys.exit("code didn't pass dprint check") + run_cmd( + ["dprint", "check", "--list-different"], + [], + "dprint", + fixer="fix-dprint", + ) def lint_manifest(): - print("running MANIFEST.in check") out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) with open("MANIFEST.in", encoding="utf8") as f: if out.strip() != f.read().strip(): - sys.exit( - "some files were added, deleted or renamed; " - "run 'make generate-manifest' and commit again" + exit_with( + "Some files were added, deleted or renamed. " + "Run 'make generate-manifest' and commit again." ) def main(): - py_files, c_files, rst_files, toml_files, new_rm_mv = git_commit_files() - if py_files: - black(py_files) - ruff(py_files) - if c_files: - clang_format(c_files) - if rst_files: - rstcheck(rst_files) - if toml_files: - toml_sort(toml_files) + py, c, rst, toml, new_rm_mv = git_commit_files() + black(py) + ruff(py) + clang_format(c) + rstcheck(rst) + toml_sort(toml) dprint() if new_rm_mv: From ec833eb642036032d44ef084313d9092de0276f7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Nov 2025 13:03:43 +0100 Subject: [PATCH 1449/1714] Make ruff happy --- scripts/internal/print_downloads.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index 6330793f67..be081144d1 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -74,11 +74,9 @@ def top_packages(): def ranking(): data = top_packages() - i = 1 - for name, downloads in data: + for i, (name, downloads) in enumerate(data, start=1): if name == PKGNAME: return i - i += 1 raise ValueError(f"can't find {PKGNAME}") From 331eb8f18b4cf5a4553efdfa251cf2dff0121684 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Nov 2025 23:28:24 +0100 Subject: [PATCH 1450/1714] [Linux] Add wheels for Linux musl (#2690) --- HISTORY.rst | 1 + docs/index.rst | 5 +++-- pyproject.toml | 5 ++--- scripts/internal/install-sysdeps.sh | 13 ++++++++----- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index aceb6a411b..0e94ab60fa 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ XXXX-XX-XX **Enhancements** +- 2403_, [Linux]: publish wheels for Linux musl. - 2680_: unit tests are no longer installed / part of the distribution. They now live under `tests/` instead of `psutil/tests`. diff --git a/docs/index.rst b/docs/index.rst index 96fcf9258a..e5bab9cff2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2751,8 +2751,9 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= -* psutil 7.1.2 (XXXX-XX): publish wheels for free-threaded Python -* psutil 7.1.2 (XXXX-XX): no longer publish wheels for 32-bit Python +* psutil 7.2.0 (XXXX-XX): publish wheels for Linux musl +* psutil 7.1.2 (2025-10): publish wheels for free-threaded Python +* psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (**Linux** and **Windows**) * psutil 7.1.1 (2025-10): drop **SunOS 10** * psutil 7.1.0 (2025-09): drop **FreeBSD 8** diff --git a/pyproject.toml b/pyproject.toml index 3e67c958d3..071dff1efc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,8 +221,7 @@ trailing_comma_inline_array = true [tool.cibuildwheel] skip = [ - "*-musllinux*", - "cp3{8,9,10,11,12}-*linux_{ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures + "cp3{8,9,10,11,12}-*linux_{ppc64le,s390x} cp3*t-musllinux*", ] test-extras = ["test"] # same as doing `pip install .[test]` test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci-test-cibuildwheel" @@ -230,7 +229,7 @@ test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci # Run tests on Python 3.13. On all other Python versions do a lightweight # import test. [[tool.cibuildwheel.overrides]] -select = "cp3{8,9,10,11,12,13t,14,14t}-* cp313-macosx*" +select = "cp3{8,9,10,11,12,13t,14,14t}-* cp313-macosx* *-musllinux*" test-extras = [] test-command = "python -c \"import psutil; print(psutil.__version__)\"" diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index ae6e341f1f..07ea0caf79 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -33,7 +33,6 @@ case "$UNAME_S" in SunOS) SUNOS=true ;; - esac # Check if running as root @@ -44,13 +43,17 @@ fi # Function to install system dependencies main() { if [ $HAS_APT ]; then - $SUDO apt-get install -y python3-dev gcc net-tools coreutils util-linux sudo + $SUDO apt-get install -y python3-dev gcc + $SUDO apt-get install -y net-tools coreutils util-linux sudo # for tests elif [ $HAS_YUM ]; then - $SUDO yum install -y python3-devel gcc net-tools coreutils-single util-linux sudo + $SUDO yum install -y python3-devel gcc + $SUDO yum install -y net-tools coreutils-single util-linux sudo # for tests elif [ $HAS_PACMAN ]; then - $SUDO pacman -S --noconfirm python gcc net-tools coreutils util-linux sudo + $SUDO pacman -S --noconfirm python gcc + $SUDO pacman -S --noconfirm net-tools coreutils util-linux sudo # for tests elif [ $HAS_APK ]; then - $SUDO apk add --no-confirm python3-dev gcc musl-dev linux-headers coreutils procps + $SUDO apk add --no-interactive python3-dev gcc musl-dev linux-headers + $SUDO apk add --no-interactive coreutils util-linux procps # for tests elif [ $FREEBSD ]; then $SUDO pkg install -y python3 gcc elif [ $NETBSD ]; then From 0fa61d172714359f6c6c522aeb721e85181229d6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 19 Nov 2025 23:32:16 +0100 Subject: [PATCH 1451/1714] Update doc about supported python versions --- docs/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e5bab9cff2..bd0f0fd006 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,8 +38,9 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are **2.7** and **3.6+**. -`PyPy `__ is also known to work. +Supported Python versions are cPython 3.6+ and `PyPy `__. +Latest psutil version supporting Python 2.7 is +`psutil 6.1.1 `__. The psutil documentation you're reading is distributed as a single HTML page. From 4ffcc935b6813541e0469d49bb6e9e00be57c610 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 20 Nov 2025 15:58:22 +0100 Subject: [PATCH 1452/1714] Rm pytest-subtests dep --- .github/workflows/sunos.yml | 2 +- pyproject.toml | 34 +++++++++++++++++----------------- setup.py | 1 - 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml index dd9dcb9d9a..dd319ebd07 100644 --- a/.github/workflows/sunos.yml +++ b/.github/workflows/sunos.yml @@ -34,5 +34,5 @@ jobs: python3 setup.py build_ext -i --parallel 4 python3 -c "import psutil" python3 scripts/internal/install_pip.py - python3 -m pip install --break-system-packages --user pytest pytest-instafail pytest-subtests pytest-xdist + python3 -m pip install --break-system-packages --user pytest pytest-instafail pytest-xdist python3 -m pytest tests/test_memleaks.py diff --git a/pyproject.toml b/pyproject.toml index 071dff1efc..a8638718ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -238,23 +238,23 @@ select = "cp38-macosx*" # In macOS use Python 3.8: higher Python version hang f test-extras = ["test"] test-command = "make -C {project} PYTHON=python PSUTIL_ROOT_DIR=\"{project}\" ci-test-cibuildwheel" -[tool.pytest.ini_options] -addopts = ''' - --verbose - --capture=no - --no-header - --tb=short - --strict-config - --strict-markers - --instafail - -p no:junitxml - -p no:doctest - -p no:nose - -p no:pastebin - -p instafail - -p subtests - -p xdist - ''' +[tool.pytest] +addopts = [ + "--capture=no", + "--instafail", + "--no-header", + "--strict-config", + "--strict-markers", + "--tb=short", + "--verbose", + "-p instafail", + "-p no:doctest", + "-p no:junitxml", + "-p no:nose", + "-p no:pastebin", + "-p xdist", +] +verbosity_subtests = "0" testpaths = ["tests/"] [build-system] diff --git a/setup.py b/setup.py index dbd6718041..812ff5157c 100755 --- a/setup.py +++ b/setup.py @@ -79,7 +79,6 @@ TEST_DEPS = [ "pytest", "pytest-instafail", - "pytest-subtests", "pytest-xdist", "setuptools", "pywin32 ; os_name == 'nt' and platform_python_implementation != 'PyPy'", From 1829c6213c54f8bb7558bb41629d6c5cc895accb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 22 Nov 2025 11:04:00 +0100 Subject: [PATCH 1453/1714] Fix #2684 / freebsd: compilation fails due to missing include. --- HISTORY.rst | 4 ++++ psutil/arch/freebsd/proc.c | 1 + 2 files changed, 5 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 0e94ab60fa..98eb7c918b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,10 @@ XXXX-XX-XX - 2680_: unit tests are no longer installed / part of the distribution. They now live under `tests/` instead of `psutil/tests`. +**Bug fixes** + +* 2684_, [FreeBSD], [critical]: compilation fails on FreeBSD 14 due to missing include. + **Compatibility notes** - 2680_: `import psutil.tests` no longer works (but it was never documented to diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 2e3637c8ab..39dd15b23b 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -5,6 +5,7 @@ */ #include +#include #include #include #include From 40e27872d534ed849245fdb0c4604ca678d5e9fc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 23 Nov 2025 00:08:41 +0100 Subject: [PATCH 1454/1714] Make ntuples work with subTest of new pytest release When running tests in parallel, the new pytest 9.X is unable to de/serialize nametuples when passed as: with self.subTest(foo=ntuple): ... --- tests/test_posix.py | 2 +- tests/test_system.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_posix.py b/tests/test_posix.py index 2cc9dbd84c..ab2c10c9f3 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -416,7 +416,7 @@ def test_users_started(self): if not tstamp: return pytest.skip(f"cannot interpret tstamp in who output\n{out}") - with self.subTest(psutil=psutil.users(), who=out): + with self.subTest(psutil=str(psutil.users()), who=out): for idx, u in enumerate(psutil.users()): psutil_value = datetime.datetime.fromtimestamp( u.started diff --git a/tests/test_system.py b/tests/test_system.py index 79325c7599..c00ab6fb96 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -244,7 +244,7 @@ def test_users(self): users = psutil.users() assert users for user in users: - with self.subTest(user=user): + with self.subTest(user=str(user)): assert user.name assert isinstance(user.name, str) assert isinstance(user.terminal, (str, type(None))) @@ -488,7 +488,9 @@ def test_cpu_times_comparison(self): 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): + with self.subTest( + field=field, base=str(base), per_cpu=str(per_cpu) + ): assert ( abs(getattr(base, field) - getattr(summed_values, field)) < 2 From 5e4f170df3421e00796aef85dd0d4680febf3686 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 24 Nov 2025 00:38:36 +0100 Subject: [PATCH 1455/1714] [Windows] net_if_stats memory leak (#2691) --- HISTORY.rst | 5 ++++- psutil/arch/windows/net.c | 20 ++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 98eb7c918b..05c8b2f2ea 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,7 +13,10 @@ XXXX-XX-XX **Bug fixes** -* 2684_, [FreeBSD], [critical]: compilation fails on FreeBSD 14 due to missing include. +* 2684_, [FreeBSD], [critical]: compilation fails on FreeBSD 14 due to missing + include. +* 2691_, [Windows]: fix memory leak in `net_if_stats()`_ memory leak due to + missing ``Py_CLEAR``. **Compatibility notes** diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 8e9a8186da..7f0c75c891 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -341,7 +341,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { int i; DWORD dwSize = 0; DWORD dwRetVal = 0; - MIB_IFTABLE *pIfTable; + MIB_IFTABLE *pIfTable = NULL; MIB_IFROW *pIfRow; PIP_ADAPTER_ADDRESSES pAddresses = NULL; PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; @@ -389,6 +389,9 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { // provides friendly names *and* descriptions and find the // ones that match. ifname_found = 0; + Py_CLEAR(py_nic_name); + Py_CLEAR(py_ifc_info); + pCurrAddresses = pAddresses; while (pCurrAddresses) { str_format(descr, MAX_PATH, "%wS", pCurrAddresses->Description); @@ -411,7 +414,7 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { continue; } - // is up? + Py_CLEAR(py_is_up); if ((pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_CONNECTED || pIfRow->dwOperStatus == MIB_IF_OPER_STATUS_OPERATIONAL) && pIfRow->dwAdminStatus == 1) @@ -432,24 +435,29 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { ); if (!py_ifc_info) goto error; + if (PyDict_SetItem(py_retdict, py_nic_name, py_ifc_info)) goto error; - Py_CLEAR(py_nic_name); + Py_CLEAR(py_ifc_info); + Py_CLEAR(py_nic_name); } free(pIfTable); free(pAddresses); + Py_CLEAR(py_nic_name); + Py_CLEAR(py_ifc_info); + Py_CLEAR(py_is_up); return py_retdict; error: Py_XDECREF(py_is_up); Py_XDECREF(py_ifc_info); Py_XDECREF(py_nic_name); - Py_DECREF(py_retdict); - if (pIfTable != NULL) + Py_XDECREF(py_retdict); + if (pIfTable) free(pIfTable); - if (pAddresses != NULL) + if (pAddresses) free(pAddresses); return NULL; } From aa90de91665c528983e0407cfd99a05e0bded803 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 24 Nov 2025 00:41:42 +0100 Subject: [PATCH 1456/1714] Windows / net_if_addrs(): use Py_CLEAR to eliminate risk of potential leaks --- psutil/arch/windows/net.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 7f0c75c891..5d41f23057 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -165,10 +165,16 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { pCurrAddresses = pAddresses; while (pCurrAddresses) { + Py_CLEAR(py_nic_name); + Py_CLEAR(py_tuple); + Py_CLEAR(py_address); + Py_CLEAR(py_mac_address); + Py_CLEAR(py_netmask); + pUnicast = pCurrAddresses->FirstUnicastAddress; netmaskIntRet = NULL; - py_nic_name = NULL; + py_nic_name = PyUnicode_FromWideChar( pCurrAddresses->FriendlyName, wcslen(pCurrAddresses->FriendlyName) ); @@ -224,6 +230,7 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; + Py_CLEAR(py_tuple); Py_CLEAR(py_mac_address); } @@ -278,6 +285,9 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { continue; } + Py_CLEAR(py_address); + Py_CLEAR(py_netmask); + py_address = PyUnicode_FromString(buff_addr); if (py_address == NULL) goto error; @@ -301,11 +311,11 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { Py_None, // broadcast (not supported) Py_None // ptp (not supported on Windows) ); - if (!py_tuple) goto error; if (PyList_Append(py_retlist, py_tuple)) goto error; + Py_CLEAR(py_tuple); Py_CLEAR(py_address); Py_CLEAR(py_netmask); @@ -313,6 +323,7 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { pUnicast = pUnicast->Next; } } + Py_CLEAR(py_nic_name); pCurrAddresses = pCurrAddresses->Next; } @@ -323,9 +334,10 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { error: if (pAddresses) free(pAddresses); - Py_DECREF(py_retlist); + Py_XDECREF(py_retlist); Py_XDECREF(py_tuple); Py_XDECREF(py_address); + Py_XDECREF(py_mac_address); Py_XDECREF(py_nic_name); Py_XDECREF(py_netmask); return NULL; From f99781a79c3f612bc30795b6e50ba07240ce3036 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 25 Nov 2025 16:20:39 +0100 Subject: [PATCH 1457/1714] Add low-level heap APIs (#2692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Title: New heap introspection APIs in psutil 7.2.0 Date: 2025-11-24 Tags: psutil, python, c, memory-leak Authors: Giampaolo Rodola Memory leaks in Python are often straightforward to diagnose. Just look at RSS, track Python object counts, follow reference graphs. But leaks inside **C extension modules** are another story. Traditional process memory metrics frequently fail to reveal them because Python's memory allocators sit above the platform's native heap. If something in an extension calls ``malloc()`` without a corresponding ``free()``, that memory often won't show up where you expect it. You have a leak, and **you don't know**. In here we introduce two new APIs for **C heap introspection**, designed specifically to catch these kinds of native leaks. They give you a window directly into the underlying platform allocator (e.g., glibc's malloc), letting you track how much memory the C layer is actually consuming. These C functions bypass Python entirely. They don't reflect Python object memory, arenas, pools, or anything managed by [pymalloc](https://docs.python.org/3/c-api/memory.html). Instead, they examine the allocator that C extensions actually use. If your RSS is flat but your C heap usage climbs, you now have a way to see it. ## Why native heap introspection matters Many Python projects rely on C extensions: psutil, NumPy, pandas, PIL, cryptography, lxml, psycopg, PyTorch, database adapters, custom in-house modules, etc., and even cPython itself, which implements many of its standard library modules in C. If any of these components mishandle memory at the C level, you get a leak that: * Doesn't show up in Python reference counts ([sys.getrefcount()](https://docs.python.org/dev/library/sys.html#sys.getrefcount)). * Doesn't show up in [tracemalloc](https://docs.python.org/3/library/tracemalloc.html). * Doesn't show up in Python's [gc](https://docs.python.org/dev/library/gc.html) stats. * May not even show up in RSS or [USS](https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python) due to allocator caching. psutil's new functions solve this by inspecting platform-native allocator state the same way Valgrind, glibc or jemalloc stats does. ## heap_info(): direct allocator statistics `heap_info()` exposes the following metrics: * ``heap_used``: total number of bytes currently allocated via ``malloc()`` (small allocations). * ``mmap_used``: total number of bytes currently allocated via ``mmap()`` or via large ``malloc()`` allocations. * ``heap_count``: (Windows only) number of private heaps created via ``HeapCreate()``. Example: ```python >>> import psutil >>> psutil.heap_info() pheap(heap_used=5177792, mmap_used=819200) ``` Reference for what contributes to each field: | Platform | Allocation type | Field affected | |----------------|------------------------------------------------------------------------------------------|----------------| | UNIX / Windows | small `malloc()` ≤128 KB without `free()` | `heap_used` | | UNIX / Windows | large `malloc()` >128 KB without `free()`, or `mmap()` without `munmap()` (UNIX) | `mmap_used` | | Windows | `HeapAlloc()` without `HeapFree()` | `heap_used` | | Windows | `VirtualAlloc()` without `VirtualFree()` | `mmap_used` | | Windows | `HeapCreate()` without `HeapDestroy()` | `heap_count` | ## heap_trim(): returning unused heap memory ``heap_trim()`` provides a cross-platform way to request that the underlying allocator free any unused memory it's holding in the heap (typically small `malloc()` allocations). In practice, modern allocators rarely comply, so this is not a general-purpose memory-reduction tool and won't meaningfully shrink RSS in real programs. Its primary value is in **leak detection tools**. Calling ``heap_trim()`` before taking measurements helps reduce allocator noise, giving you a cleaner baseline so that changes in `heap_used` come from the code you're testing, not from internal allocator caching or fragmentation. ## Real-world use: finding a C extension leak The workflow is simple: 1. Take a baseline snapshot of the heap. 1. Call the C extension hundreds or thousands of times. 1. Take another snapshot. 1. Compare. ```python import psutil psutil.heap_trim() # reduce noise before = psutil.heap_info() for _ in range(400): my_cext_function() after = psutil.heap_info() print("delta heap_used =", after.heap_used - before.heap_used) print("delta mmap_used =", after.mmap_used - before.mmap_used) ``` If `heap_used` or `mmap_used` values increase consistently, you've found a native leak. To reduce false positives, repeat the test multiple times, doubling the number of calls on each retry. This approach helps distinguish real leaks from random noise or transient allocations. That’s exactly what the [MemoryLeakTestCase](https://github.com/giampaolo/psutil/blob/d40164f1/psutil/test/memleak.py) class in psutil does: it runs the target function repeatedly, trims the allocator before each run, and tracks differences across retries. Memory that continues to grow after several retries is flagged as a leak. ## Under the hood * **[Linux](https://github.com/giampaolo/psutil/blob/d40164f1/psutil/arch/linux/heap.c)**: uses glibc's [mallinfo2()](https://man7.org/linux/man-pages/man3/mallinfo.3.html) to report ``uordblks`` (heap allocations) and ``hblkhd`` (mmap-backed blocks). * **[Windows](https://github.com/giampaolo/psutil/blob/d40164f1/psutil/arch/windows/heap.c)**: enumerates heaps and aggregates ``HeapAlloc`` / ``VirtualAlloc`` usage. * **[macOS](https://github.com/giampaolo/psutil/blob/d40164f1/psutil/arch/osx/heap.c)**: uses malloc zone statistics. * **[BSD](https://github.com/giampaolo/psutil/blob/d40164f1/psutil/arch/bsd/heap.c)**: uses jemalloc's arena and stats interfaces. ## Summary psutil 7.2.0 fills a long-standing observability gap: native-level memory leaks in C extensions are now visible directly from Python. You now have a simple method to **test C extensions for leaks**. This turns psutil into not just a monitoring library, but a practical debugging tool for Python projects that rely on native C extension modules. --- HISTORY.rst | 5 + MANIFEST.in | 5 + Makefile | 6 +- README.rst | 10 ++ docs/index.rst | 79 ++++++++- psutil/__init__.py | 46 ++++++ psutil/_ntuples.py | 12 ++ psutil/_psbsd.py | 6 + psutil/_pslinux.py | 6 + psutil/_psosx.py | 5 + psutil/_psutil_bsd.c | 4 + psutil/_psutil_linux.c | 7 + psutil/_psutil_osx.c | 2 + psutil/_psutil_windows.c | 2 + psutil/_pswindows.py | 5 + psutil/arch/bsd/heap.c | 90 ++++++++++ psutil/arch/bsd/init.h | 2 + psutil/arch/linux/heap.c | 89 ++++++++++ psutil/arch/linux/init.h | 8 + psutil/arch/osx/heap.c | 106 ++++++++++++ psutil/arch/osx/init.h | 2 + psutil/arch/windows/heap.c | 112 +++++++++++++ psutil/arch/windows/init.h | 2 + psutil/test/memleak.py | 189 ++++++++++++++++----- setup.py | 2 +- tests/__init__.py | 6 + tests/test_contracts.py | 16 ++ tests/test_heap.py | 325 +++++++++++++++++++++++++++++++++++++ tests/test_memleaks.py | 9 + tests/test_system.py | 16 ++ tests/test_testutils.py | 23 +-- 31 files changed, 1129 insertions(+), 68 deletions(-) create mode 100644 psutil/arch/bsd/heap.c create mode 100644 psutil/arch/linux/heap.c create mode 100644 psutil/arch/osx/heap.c create mode 100644 psutil/arch/windows/heap.c create mode 100755 tests/test_heap.py diff --git a/HISTORY.rst b/HISTORY.rst index 05c8b2f2ea..ddd8d1a8b2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,9 @@ XXXX-XX-XX **Enhancements** +- 1275_: new `heap_info()`_ and `heap_trim()`_ functions, providing direct + access to the platform's native C heap allocator. Useful to create tools to + detect memory leaks. - 2403_, [Linux]: publish wheels for Linux musl. - 2680_: unit tests are no longer installed / part of the distribution. They now live under `tests/` instead of `psutil/tests`. @@ -2842,6 +2845,8 @@ In most cases accessing the old names will work but it will cause a .. _`disk_partitions()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_partitions .. _`disk_usage()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_usage .. _`getloadavg()`: https://psutil.readthedocs.io/en/latest/#psutil.getloadavg +.. _`heap_info()`: https://psutil.readthedocs.io/en/latest/#psutil.heap_info +.. _`heap_trim()`: https://psutil.readthedocs.io/en/latest/#psutil.heap_trim .. _`net_connections()`: https://psutil.readthedocs.io/en/latest/#psutil.net_connections .. _`net_if_addrs()`: https://psutil.readthedocs.io/en/latest/#psutil.net_if_addrs .. _`net_if_stats()`: https://psutil.readthedocs.io/en/latest/#psutil.net_if_stats diff --git a/MANIFEST.in b/MANIFEST.in index aecf381f7d..0fcff8679d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -55,6 +55,7 @@ include psutil/arch/all/pids.c include psutil/arch/all/str.c include psutil/arch/bsd/cpu.c include psutil/arch/bsd/disk.c +include psutil/arch/bsd/heap.c include psutil/arch/bsd/init.c include psutil/arch/bsd/init.h include psutil/arch/bsd/net.c @@ -71,6 +72,7 @@ include psutil/arch/freebsd/proc_socks.c include psutil/arch/freebsd/sensors.c include psutil/arch/freebsd/sys_socks.c include psutil/arch/linux/disk.c +include psutil/arch/linux/heap.c include psutil/arch/linux/init.h include psutil/arch/linux/mem.c include psutil/arch/linux/net.c @@ -92,6 +94,7 @@ include psutil/arch/openbsd/socks.c include psutil/arch/openbsd/users.c include psutil/arch/osx/cpu.c include psutil/arch/osx/disk.c +include psutil/arch/osx/heap.c include psutil/arch/osx/init.c include psutil/arch/osx/init.h include psutil/arch/osx/mem.c @@ -118,6 +121,7 @@ include psutil/arch/sunos/proc.c include psutil/arch/sunos/sys.c include psutil/arch/windows/cpu.c include psutil/arch/windows/disk.c +include psutil/arch/windows/heap.c include psutil/arch/windows/init.c include psutil/arch/windows/init.h include psutil/arch/windows/mem.c @@ -187,6 +191,7 @@ include tests/test_aix.py include tests/test_bsd.py include tests/test_connections.py include tests/test_contracts.py +include tests/test_heap.py include tests/test_linux.py include tests/test_memleaks.py include tests/test_misc.py diff --git a/Makefile b/Makefile index a24d8870db..fdac6dcc95 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,9 @@ test-contracts: ## APIs sanity tests. test-connections: ## Test psutil.net_connections() and Process.net_connections(). $(RUN_TEST) tests/test_connections.py $(ARGS) +test-malloc: ## Test psutil.malloc_*() APIs. + $(RUN_TEST) tests/test_malloc.py $(ARGS) + test-posix: ## POSIX specific tests. $(RUN_TEST) tests/test_posix.py $(ARGS) @@ -237,7 +240,8 @@ ci-test-cibuildwheel: ## Run tests from cibuildwheel. PIP_BREAK_SYSTEM_PACKAGES=1 $(MAKE) install-pydeps-test $(MAKE) print-sysinfo mkdir -p .tests - cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs ../tests + cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs --ignore=../tests/test_memleaks.py ../tests + cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs ../tests/test_memleaks.py ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit diff --git a/README.rst b/README.rst index 215cbcefef..259f0a32fd 100644 --- a/README.rst +++ b/README.rst @@ -454,6 +454,16 @@ Further process APIs >>> gone, alive = psutil.wait_procs(procs_list, timeout=3, callback=on_terminate) >>> +Heap info +--------- + +.. code-block:: python + + >>> import psutil + >>> psutil.heap_info() + pheap(heap_used=5177792, mmap_used=819200) + >>> psutil.heap_trim() + Detecting memory leaks in C functions ------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index bd0f0fd006..c490c5a834 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2129,6 +2129,79 @@ Process class .. versionchanged:: 4.4.0 added context manager support +C heap introspection +==================== + +The following functions provide direct access to the platform's native C heap +allocator (such as glibc's ``malloc`` on Linux or ``jemalloc`` on BSD). They +are low-level interfaces intended for detecting memory leaks in C extensions, +which are usually not revealed via standard RSS/VMS metrics. These functions do +not reflect Python object memory; they operate solely on allocations made in C +via ``malloc()``, ``free()``, and related calls. + +The general idea behind these functions is straightforward: capture the state +of the C heap before and after repeatedly invoking a function implemented in a +C extension, and compare the results. If ``heap_used`` or ``mmap_used`` grows +steadily across iterations, the C code is likely retaining memory it should be +releasing. This provides an allocator-level way to spot native leaks that +Python's memory tracking misses. + +.. function:: heap_info() + + Return low-level heap statistics from the system's C allocator. On Linux, + this exposes ``uordblks`` and ``hblkhd`` fields from glibc's `mallinfo2`_. + Returns a namedtuple containing: + + - ``heap_used``: total number of bytes currently allocated via ``malloc()`` + (small allocations). + - ``mmap_used``: total number of bytes currently allocated via ``mmap()`` or + via large ``malloc()`` allocations. Always set to 0 on macOS. + - ``heap_count``: (Windows only) number of private heaps created via + ``HeapCreate()``. + + >>> import psutil + >>> psutil.heap_info() + pheap(heap_used=5177792, mmap_used=819200) + + These fields reflect how unreleased C allocations affect the heap: + + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Platform | Allocation type | Affected field | + +===============+====================================================================================+=================+ + | UNIX / glibc | small ``malloc()`` ≤128KB without ``free()`` | ``heap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | UNIX / glibc | large ``malloc()`` >128KB without ``free()`` , or ``mmap()`` without ``munmap()`` | ``mmap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Windows | ``HeapAlloc()`` without ``HeapFree()`` | ``heap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Windows | ``VirtualAlloc()`` without ``VirtualFree()`` | ``mmap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Windows | ``HeapCreate()`` without ``HeapDestroy()`` | ``heap_count`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + + .. versionadded:: 7.2.0 + + Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD + +.. function:: heap_trim() + + Request that the underlying allocator free any unused memory it's holding in + the heap (typically small ``malloc()`` allocations). + + In practice, modern allocators rarely comply, so this is not a + general-purpose memory-reduction tool and won't meaningfully shrink RSS in + real programs. Its primary value is in **leak detection tools**. + + Calling ``heap_trim()`` before taking measurements helps reduce allocator + noise, giving you a cleaner baseline so that changes in ``heap_used`` come + from the code you're testing, not from internal allocator caching or + fragmentation. Its effectiveness depends on allocator behavior and + fragmentation patterns. + + .. versionadded:: 7.2.0 + + Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD + Windows services ================ @@ -2244,9 +2317,8 @@ memory-leak detection tests. number of function calls each time. If memory continues to grow, the test is considered a failure. The test currently monitors RSS, VMS, and `USS `__ memory. - ``mallinfo()`` on Linux and ``_heapwalk()`` on Windows could provide - even more precise results (see `issue 1275 `__), - but these are not yet implemented. + On supported platforms, it also monitors **heap metrics** (``heap_used``, ``mmap_used`` from + :func:`heap_info`). In addition it also ensures that the target function does not leak file descriptors (UNIX) or handles (Windows). @@ -3203,6 +3275,7 @@ Timeline .. _`issue #1007`: https://github.com/giampaolo/psutil/issues/1007 .. _`issue #1040`: https://github.com/giampaolo/psutil/issues/1040 .. _`issue #883`: https://github.com/giampaolo/psutil/issues/883 +.. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html .. _`man prlimit`: https://linux.die.net/man/2/prlimit .. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py .. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py diff --git a/psutil/__init__.py b/psutil/__init__.py index af63773b70..d72cd18aa0 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -2405,6 +2405,52 @@ def win_service_get(name): return _psplatform.win_service_get(name) +# ===================================================================== +# --- malloc / heap +# ===================================================================== + + +# Linux + glibc, Windows, macOS, FreeBSD, NetBSD +if hasattr(_psplatform, "heap_info"): + + def heap_info(): + """Return low-level heap statistics from the C heap allocator + (glibc). + + - `heap_used`: the total number of bytes allocated via + malloc/free. These are typically allocations smaller than + MMAP_THRESHOLD. + + - `mmap_used`: the total number of bytes allocated via `mmap()` + or via large ``malloc()`` allocations. + + - `heap_count` (Windows only): number of private heaps created + via `HeapCreate()`. + """ + return _ntp.pheap(*_psplatform.heap_info()) + + def heap_trim(): + """Request that the underlying allocator free any unused memory + it's holding in the heap (typically small `malloc()` + allocations). + + In practice, modern allocators rarely comply, so this is not a + general-purpose memory-reduction tool and won't meaningfully + shrink RSS in real programs. Its primary value is in **leak + detection tools**. + + Calling `heap_trim()` before taking measurements helps reduce + allocator noise, giving you a cleaner baseline so that changes + in `heap_used` come from the code you're testing, not from + internal allocator caching or fragmentation. Its effectiveness + depends on allocator behavior and fragmentation patterns. + """ + _psplatform.heap_trim() + + __all__.append("heap_info") + __all__.append("heap_trim") + + # ===================================================================== diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index dabf496b21..f0ab039376 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -84,6 +84,18 @@ # psutil.sensors_fans() sfan = nt("sfan", ("label", "current")) +# psutil.heap_info() (mallinfo2 Linux struct) +if LINUX or WINDOWS or MACOS or BSD: + pheap = nt( + "pheap", + [ + "heap_used", # uordblks, memory allocated via malloc() + "mmap_used", # hblkhd, memory allocated via mmap() (large blocks) + ], + ) + if WINDOWS: + pheap = nt("pheap", pheap._fields + ("heap_count",)) + # =================================================================== # --- Process class # =================================================================== diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index d9e6d12671..172bf19499 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -187,6 +187,12 @@ def swap_memory(): return ntp.sswap(total, used, free, percent, sin, sout) +# malloc / heap functions (FreeBSD / NetBSD) +if hasattr(cext, "heap_info"): + heap_info = cext.heap_info + heap_trim = cext.heap_trim + + # ===================================================================== # --- CPU # ===================================================================== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index fd1926af0c..a98d351db3 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -487,6 +487,12 @@ def swap_memory(): return ntp.sswap(total, used, free, percent, sin, sout) +# malloc / heap functions; require glibc +if hasattr(cext, "heap_info"): + heap_info = cext.heap_info + heap_trim = cext.heap_trim + + # ===================================================================== # --- CPU # ===================================================================== diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 06e28f58f0..77fc65e94f 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -112,6 +112,11 @@ def swap_memory(): return ntp.sswap(total, used, free, percent, sin, sout) +# malloc / heap functions +heap_info = cext.heap_info +heap_trim = cext.heap_trim + + # ===================================================================== # --- CPU # ===================================================================== diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index d04aa46258..39e43fc318 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -74,6 +74,10 @@ static PyMethodDef mod_methods[] = { {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"pids", psutil_pids, METH_VARARGS}, {"swap_mem", psutil_swap_mem, METH_VARARGS}, +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) + {"heap_info", psutil_heap_info, METH_VARARGS}, + {"heap_trim", psutil_heap_trim, METH_VARARGS}, +#endif #if defined(PSUTIL_OPENBSD) {"users", psutil_users, METH_VARARGS}, #endif diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index f66cae8f4b..71e70024ee 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -33,6 +33,13 @@ static PyMethodDef mod_methods[] = { // --- system related functions {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS}, +#ifdef PSUTIL_HAS_HEAP_INFO + {"heap_info", psutil_heap_info, METH_VARARGS}, +#endif +#ifdef PSUTIL_HAS_HEAP_TRIM + {"heap_trim", psutil_heap_trim, METH_VARARGS}, +#endif + // --- linux specific {"linux_sysinfo", psutil_linux_sysinfo, METH_VARARGS}, // --- others diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 29119cef7e..d4cd62da94 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -43,6 +43,8 @@ static PyMethodDef mod_methods[] = { {"disk_partitions", psutil_disk_partitions, METH_VARARGS}, {"disk_usage_used", psutil_disk_usage_used, METH_VARARGS}, {"has_cpu_freq", psutil_has_cpu_freq, METH_VARARGS}, + {"heap_info", psutil_heap_info, METH_VARARGS}, + {"heap_trim", psutil_heap_trim, METH_VARARGS}, {"net_io_counters", psutil_net_io_counters, METH_VARARGS}, {"per_cpu_times", psutil_per_cpu_times, METH_VARARGS}, {"pids", psutil_pids, METH_VARARGS}, diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index a3d3667941..31f9800ff4 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -75,6 +75,8 @@ static PyMethodDef PsutilMethods[] = { {"init_loadavg_counter", (PyCFunction)psutil_init_loadavg_counter, METH_VARARGS}, + {"heap_info", psutil_heap_info, METH_VARARGS}, + {"heap_trim", psutil_heap_trim, METH_VARARGS}, {"net_connections", psutil_net_connections, METH_VARARGS}, {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS}, {"net_if_stats", psutil_net_if_stats, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index c22071965e..3ee1eafce0 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -220,6 +220,11 @@ def swap_memory(): return ntp.sswap(total, used, free, percent, 0, 0) +# malloc / heap functions +heap_info = cext.heap_info +heap_trim = cext.heap_trim + + # ===================================================================== # --- disk # ===================================================================== diff --git a/psutil/arch/bsd/heap.c b/psutil/arch/bsd/heap.c new file mode 100644 index 0000000000..a3d03a5d19 --- /dev/null +++ b/psutil/arch/bsd/heap.c @@ -0,0 +1,90 @@ +/* + * 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. + */ + +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) +#include +#include +#if defined(PSUTIL_FREEBSD) +#include +#else +#include +#endif + +#include "../../arch/all/init.h" + + +// Return low-level heap statistics from the C allocator. Return +// jemalloc heap stats via `mallctl()`. Mimics Linux `mallinfo2()`: +// - heap_used ~ stats.allocated (like `uordblks`) +// - mmap_used ~ stats.mapped (like `hblkhd`) +PyObject * +psutil_heap_info(PyObject *self, PyObject *args) { + uint64_t epoch = 0; + uint64_t allocated = 0, active = 0, mapped = 0; + size_t sz_epoch = sizeof(epoch); + size_t sz_val; + int ret; + + // Read current epoch + ret = mallctl("epoch", &epoch, &sz_epoch, NULL, 0); + if (ret != 0) + return psutil_oserror_wsyscall("mallctl('epoch')"); + + // Refresh stats + ret = mallctl("epoch", NULL, NULL, &epoch, sz_epoch); + if (ret != 0) + return psutil_oserror_wsyscall("mallctl('epoch') update"); + + // Read stats + sz_val = sizeof(allocated); + ret = mallctl("stats.allocated", &allocated, &sz_val, NULL, 0); + if (ret != 0) + return psutil_oserror_wsyscall("mallctl('stats.allocated')"); + + sz_val = sizeof(mapped); + ret = mallctl("stats.mapped", &mapped, &sz_val, NULL, 0); + if (ret != 0) + return psutil_oserror_wsyscall("mallctl('stats.mapped')"); + + return Py_BuildValue("KK", allocated, mapped); +} + + +// Release unused heap memory from all jemalloc arenas back to the OS. +// Aggressively purges free pages from all arenas (main + per-thread). +// More effective than Linux `heap_trim(0)`. +PyObject * +psutil_heap_trim(PyObject *self, PyObject *args) { + char cmd[32]; + int ret; + +#ifdef MALLCTL_ARENAS_ALL + // FreeBSD. MALLCTL_ARENAS_ALL is a magic number (4096) which means "all + // arenas". + str_format(cmd, sizeof(cmd), "arena.%u.purge", MALLCTL_ARENAS_ALL); + ret = mallctl(cmd, NULL, NULL, NULL, 0); + if (ret != 0) + return psutil_oserror(); +#else + // NetBSD. Iterate over all arenas. + unsigned narenas; + size_t sz = sizeof(narenas); + + ret = mallctl("arenas.narenas", &narenas, &sz, NULL, 0); + if (ret != 0) + return psutil_oserror_wsyscall("mallctl('arenas.narenas')"); + + for (unsigned i = 0; i < narenas; i++) { + str_format(cmd, sizeof(cmd), "arena.%u.purge", i); + ret = mallctl(cmd, NULL, NULL, NULL, 0); + if (ret != 0) + return psutil_oserror_wsyscall("mallctl('arena.{n}.purge')"); + } +#endif + + Py_RETURN_NONE; +} +#endif // PSUTIL_FREEBSD || PSUTIL_NETBSD diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 9a5bc10bb8..bde5fe6cea 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -23,6 +23,8 @@ PyObject *psutil_boot_time(PyObject *self, PyObject *args); PyObject *psutil_cpu_count_logical(PyObject *self, PyObject *args); PyObject *psutil_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); +PyObject *psutil_heap_info(PyObject *self, PyObject *args); +PyObject *psutil_heap_trim(PyObject *self, PyObject *args); PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); diff --git a/psutil/arch/linux/heap.c b/psutil/arch/linux/heap.c new file mode 100644 index 0000000000..d3ef444712 --- /dev/null +++ b/psutil/arch/linux/heap.c @@ -0,0 +1,89 @@ +/* + * 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. + */ + +#include "../../arch/all/init.h" + +#if defined(PSUTIL_HAS_HEAP_INFO) // Not available on MUSL / Alpine Linux +#include +#include +#include + + +// A copy of glibc's mallinfo2 layout. Allows compilation even if +// doesn't define mallinfo2. +struct my_mallinfo2 { + size_t arena; + size_t ordblks; + size_t smblks; + size_t hblks; + size_t hblkhd; + size_t usmblks; + size_t fsmblks; + size_t uordblks; + size_t fordblks; + size_t keepcost; +}; + + +// psutil_heap_info() -> (heap_used, mmap_used) +// Return low-level heap statistics from the C allocator (glibc). +PyObject * +psutil_heap_info(PyObject *self, PyObject *args) { + static int warned = 0; + void *handle = NULL; + void *fun = NULL; + unsigned long long uord, mmap; + + handle = dlopen("libc.so.6", RTLD_LAZY); + if (handle != NULL) { + fun = dlsym(handle, "mallinfo2"); + } + + // mallinfo2() appeared in glibc 2.33, February 2021. + if (fun != NULL) { + struct my_mallinfo2 m2; + + m2 = ((struct my_mallinfo2(*)(void))fun)(); + + uord = (unsigned long long)m2.uordblks; + mmap = (unsigned long long)m2.hblkhd; + } + + // mallinfo() is broken due to its fields that are 32-bit + // integers, meaning they overflow if process allocates more than + // 2GB in the heap. + else { + struct mallinfo m1; + + if (!warned) { + psutil_debug("WARNING: using deprecated mallinfo()"); + warned = 1; + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + m1 = mallinfo(); +#pragma GCC diagnostic pop + + uord = (unsigned long long)m1.uordblks; + mmap = (unsigned long long)m1.hblkhd; + } + + if (handle) + dlclose(handle); + + return Py_BuildValue("KK", uord, mmap); +} + + +// Release unused memory held by the allocator back to the OS. +PyObject * +psutil_heap_trim(PyObject *self, PyObject *args) { + // heap_trim returns 1 if some memory was released, else 0. + int ret = malloc_trim(0); + return PyBool_FromLong(ret); +} +#endif // PSUTIL_HAS_HEAP_INFO diff --git a/psutil/arch/linux/init.h b/psutil/arch/linux/init.h index 19a909125b..9002d9efda 100644 --- a/psutil/arch/linux/init.h +++ b/psutil/arch/linux/init.h @@ -25,3 +25,11 @@ PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); PyObject *psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args); PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); #endif + +// Does not exist on MUSL / Alpine Linux. +#if defined(__GLIBC__) +#define PSUTIL_HAS_HEAP_INFO +#define PSUTIL_HAS_HEAP_TRIM +PyObject *psutil_heap_trim(PyObject *self, PyObject *args); +PyObject *psutil_heap_info(PyObject *self, PyObject *args); +#endif diff --git a/psutil/arch/osx/heap.c b/psutil/arch/osx/heap.c new file mode 100644 index 0000000000..95238ba31b --- /dev/null +++ b/psutil/arch/osx/heap.c @@ -0,0 +1,106 @@ +/* + * 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. + */ + +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +static int +get_zones(malloc_zone_t ***out_zones, unsigned int *out_count) { + vm_address_t *raw = NULL; + unsigned int count = 0; + kern_return_t kr; + malloc_zone_t **zones; + malloc_zone_t *zone; + + *out_zones = NULL; + *out_count = 0; + + kr = malloc_get_all_zones(mach_task_self(), NULL, &raw, &count); + if (kr == KERN_SUCCESS && raw != NULL && count > 0) { + *out_zones = (malloc_zone_t **)raw; + *out_count = count; + return 1; // success + } + + psutil_debug("malloc_get_all_zones() failed; using malloc_default_zone()"); + + zones = (malloc_zone_t **)malloc(sizeof(malloc_zone_t *)); + if (!zones) { + PyErr_NoMemory(); + return -1; + } + + zone = malloc_default_zone(); + if (!zone) { + free(zones); + psutil_runtime_error("malloc_default_zone() failed"); + return -1; + } + + zones[0] = zone; + *out_zones = zones; + *out_count = 1; + return 0; // fallback, caller must free() +} + + +// psutil_heap_info() -> (heap_used, mmap_used) +// +// Return libmalloc heap stats via `malloc_zone_statistics()`. +// Compatible with macOS 10.6+ (Sierra and earlier). +// +// Mapping: +// - heap_used ~ size_in_use (live allocated bytes) +// - mmap_used ~ 0 (no direct stat) +PyObject * +psutil_heap_info(PyObject *self, PyObject *args) { + malloc_zone_t **zones = NULL; + unsigned int count = 0; + uint64_t heap_used = 0; + uint64_t mmap_used = 0; + int ok; + + ok = get_zones(&zones, &count); + if (ok == -1) + return NULL; + + for (unsigned int i = 0; i < count; i++) { + malloc_statistics_t stats = {0}; + malloc_zone_statistics(zones[i], &stats); + heap_used += (uint64_t)stats.size_in_use; + } + + if (!ok) + free(zones); + + return Py_BuildValue("KK", heap_used, mmap_used); +} + + +// Return unused heap memory back to the OS. +PyObject * +psutil_heap_trim(PyObject *self, PyObject *args) { + malloc_zone_t **zones = NULL; + unsigned int count = 0; + int ok; + + ok = get_zones(&zones, &count); + if (ok == -1) + return NULL; + + for (unsigned int i = 0; i < count; i++) + malloc_zone_pressure_relief(zones[i], 0); + + if (!ok) + free(zones); + + Py_RETURN_NONE; +} diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index d36e26f2ca..6ce2573870 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -34,6 +34,8 @@ PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); PyObject *psutil_disk_usage_used(PyObject *self, PyObject *args); PyObject *psutil_has_cpu_freq(PyObject *self, PyObject *args); +PyObject *psutil_heap_info(PyObject *self, PyObject *args); +PyObject *psutil_heap_trim(PyObject *self, PyObject *args); PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/heap.c b/psutil/arch/windows/heap.c new file mode 100644 index 0000000000..69570bd216 --- /dev/null +++ b/psutil/arch/windows/heap.c @@ -0,0 +1,112 @@ +/* + * 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. + */ + + +#include +#include +#include + +#include "../../arch/all/init.h" + + +// Returns a tuple with: +// +// - heap_used: sum of used blocks, like `uordblks` on Linux. Catches +// small `malloc()` without `free()` and small `HeapAlloc()` without +// `HeapFree()`. If bigger than some KB they go into `mmap_used`. +// +// - mmap_used: VirtualAlloc'd regions, like `hblkhd` on Linux. Catches +// `VirtualAlloc()` without `VirtualFree()`. +// +// - heap_count (Windows only): number of private heaps. Catches +// `HeapCreate()` without `HeapDestroy()`. +PyObject * +psutil_heap_info(PyObject *self, PyObject *args) { + MEMORY_BASIC_INFORMATION mbi; + LPVOID addr = NULL; + SIZE_T heap_used = 0; + SIZE_T mmap_used = 0; + DWORD heap_count; + DWORD written; + _HEAPINFO hinfo = {0}; + hinfo._pentry = NULL; + int status; + HANDLE *heaps = NULL; + + // Walk CRT heaps to measure heap used. + while ((status = _heapwalk(&hinfo)) == _HEAPOK) { + if (hinfo._useflag == _USEDENTRY) { + heap_used += hinfo._size; + } + } + if ((status != _HEAPEND) && (status != _HEAPOK)) + return psutil_oserror_wsyscall("_heapwalk"); + + // Get number of heaps (+ heap handles). + heap_count = GetProcessHeaps(0, NULL); // 1st: get count + if (heap_count == 0) + return psutil_oserror_wsyscall("GetProcessHeaps (1/2)"); + heaps = (HANDLE *)malloc(heap_count * sizeof(HANDLE)); + if (!heaps) { + PyErr_NoMemory(); + return NULL; + } + written = GetProcessHeaps(heap_count, heaps); // 2nd: get heaps handles + if (written == 0) { + free(heaps); + return psutil_oserror_wsyscall("GetProcessHeaps (2/2)"); + } + + // VirtualAlloc'd regions (large allocations / mmap|hblkhd equivalent). + while (VirtualQuery(addr, &mbi, sizeof(mbi)) == sizeof(mbi)) { + if (mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE + && (mbi.AllocationProtect & PAGE_READWRITE)) + { + int is_heap_region = 0; + for (DWORD i = 0; i < heap_count; i++) { + if (mbi.AllocationBase == heaps[i]) { + is_heap_region = 1; + break; + } + } + + if (!is_heap_region) { + mmap_used += mbi.RegionSize; + } + } + addr = (LPBYTE)mbi.BaseAddress + mbi.RegionSize; + } + + free(heaps); + + return Py_BuildValue( + "nnn", + (Py_ssize_t)heap_used, + (Py_ssize_t)mmap_used, + (Py_ssize_t)heap_count + ); +} + + +// Return unused heap memory back to the OS. Return the size of the +// largest committed free block in the heap, in bytes. Equivalent to +// Linux `heap_trim(0)`. +PyObject * +psutil_heap_trim(PyObject *self, PyObject *args) { + HANDLE hHeap = GetProcessHeap(); + SIZE_T largest_free; + + if (hHeap == NULL) + return psutil_oserror_wsyscall("GetProcessHeap"); + + largest_free = HeapCompact(hHeap, 0); + if (largest_free == 0) { + if (GetLastError() != NO_ERROR) { + return psutil_oserror_wsyscall("HeapCompact"); + } + } + return Py_BuildValue("K", (unsigned long long)largest_free); +} diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index 342a35560f..3689b22f2c 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -90,6 +90,8 @@ PyObject *psutil_get_loadavg(); PyObject *psutil_get_open_files(DWORD pid, HANDLE hProcess); PyObject *psutil_getpagesize(PyObject *self, PyObject *args); PyObject *psutil_init_loadavg_counter(); +PyObject *psutil_heap_info(PyObject *self, PyObject *args); +PyObject *psutil_heap_trim(PyObject *self, PyObject *args); PyObject *psutil_net_connections(PyObject *self, PyObject *args); PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); diff --git a/psutil/test/memleak.py b/psutil/test/memleak.py index 03b9531344..9eea9a299a 100644 --- a/psutil/test/memleak.py +++ b/psutil/test/memleak.py @@ -2,16 +2,68 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Test framework for detecting memory leaks in C functions.""" +"""A testing framework for detecting memory leaks in functions, +typically those implemented in C that forget to `free()` heap memory, +call `Py_DECREF` on Python objects, and so on. It works by comparing +the process's memory usage before and after repeatedly calling the +target function. + +Detecting memory leaks reliably is inherently difficult (and probably +impossible) because of how the OS manages memory, garbage collection, +and caching. Memory usage may even decrease between runs. So this is +not meant to be bullet proof. To reduce false positives, when an +increase in memory is detected (mem > 0), the test is retried up to 5 +times, increasing the number of function calls each time. If memory +continues to grow, the test is considered a failure. + +The test monitors RSS, VMS, and USS [1] memory. In addition, it also +monitors **heap metrics** (`heap_used`, `mmap_used` from +`psutil.heap_info()`). + +In other words, this is specifically designed to catch cases where a C +extension or other native code allocates memory via `malloc()` or +similar functions but fails to call `free()`, resulting in unreleased +memory that would otherwise remain in the process heap or in mapped +memory regions. + +The types of allocations this should catch include: + +- `malloc()` without a corresponding `free()` +- `mmap()` without `munmap()` +- `HeapAlloc()` without `HeapFree()` (Windows) +- `VirtualAlloc()` without `VirtualFree()` (Windows) +- `HeapCreate()` without `HeapDestroy()` (Windows) + +In addition it also ensures that the target function does not leak file +descriptors (UNIX) or handles (Windows) such as: + +- `open()` without a corresponding `close()` (UNIX) +- `CreateFile()` / `CreateProcess()` / ... without `CloseHandle()` + (Windows) + +Usage example: + + from psutil.test import MemoryLeakTestCase + + class TestLeaks(MemoryLeakTestCase): + def test_fun(self): + self.execute(some_function) + +NOTE - This class is experimental, meaning its API or internal +algorithm may change in the future. + +[1] https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python +[2] https://github.com/giampaolo/psutil/issues/1275 +""" import functools import gc import os -import sys import unittest import psutil from psutil._common import POSIX +from psutil._common import WINDOWS from psutil._common import bytes2human from psutil._common import print_color @@ -32,44 +84,30 @@ def format_run_line(idx, diffs, times): return s -class MemoryLeakTestCase(unittest.TestCase): - """A testing framework for detecting memory leaks in functions, - typically those implemented in C that forget to `free()` heap - memory, call `Py_DECREF` on Python objects, and so on. It works by - comparing the process's memory usage before and after repeatedly - calling the target function. - - Detecting memory leaks reliably is inherently difficult (and - probably impossible) because of how the OS manages memory, garbage - collection, and caching. Memory usage may even decrease between - runs. So this is not meant to be bullet proof. To reduce false - positives, when an increase in memory is detected (mem > 0), the - test is retried up to 5 times, increasing the number of function - calls each time. If memory continues to grow, the test is - considered a failure. - - The test currently monitors RSS, VMS, and USS [1] memory. - `mallinfo()` on Linux and `_heapwalk()` on Windows could provide - even more precise results [2], but these are not yet implemented. +def qualname(obj): + """Return a human-readable qualified name for a function, method or + class. + """ + return getattr(obj, "__qualname__", getattr(obj, "__name__", str(obj))) - In addition it also ensures that the target function does not leak - file descriptors (UNIX) or handles (Windows). - Usage example: +class MemoryLeakError(AssertionError): + """Raised when a memory leak is detected.""" - from psutil.test import MemoryLeakTestCase - class TestLeaks(MemoryLeakTestCase): - def test_fun(self): - self.execute(some_function) +class UnclosedFdError(AssertionError): + """Raised when an unclosed file descriptor (UNIX) or handle + (Windows) is detected. + """ - NOTE - This class is experimental, meaning its API or internal - algorithm may change in the future. - [1] https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python - [2] https://github.com/giampaolo/psutil/issues/1275 +class UnclosedHeapCreateError(AssertionError): + """Raised on Windows when test detects HeapCreate() without a + corresponding HeapDestroy(). """ + +class MemoryLeakTestCase(unittest.TestCase): # Number of times to call the tested function in each iteration. times = 200 # Maximum number of retries if memory growth is detected. @@ -80,6 +118,10 @@ def test_fun(self): tolerance = 0 # 0 = no messages; 1 = print diagnostics when memory increases. verbosity = 1 + # max number of calls per retry batch + max_calls_per_retry = 1600 + + __doc__ = __doc__ @classmethod def setUpClass(cls): @@ -92,16 +134,31 @@ def tearDownClass(cls): def _log(self, msg, level): if level <= self.verbosity: - print_color(msg, color="yellow", file=sys.stderr) + print_color(msg, color="yellow") + + def _heap_trim(self): + """Release unused memory held by the allocator back to the OS.""" + if hasattr(psutil, "heap_trim"): + psutil.heap_trim() + + def _warmup(self, fun, warmup_times): + self._call_ntimes(fun, warmup_times) # --- getters def _get_mem(self): mem = thisproc.memory_full_info() + heap_used = mmap_used = 0 + if hasattr(psutil, "heap_info"): + mallinfo = psutil.heap_info() + heap_used = mallinfo.heap_used + mmap_used = mallinfo.mmap_used return { + "heap": heap_used, + "mmap": mmap_used, + "uss": getattr(mem, "uss", 0), "rss": mem.rss, "vms": mem.vms, - "uss": getattr(mem, "uss", 0), } def _get_num_fds(self): @@ -113,32 +170,64 @@ def _get_num_fds(self): # --- checkers 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. + """Makes sure `num_fds()` (POSIX) or `num_handles()` (Windows) + do not increase after calling function 1 time. Used to + discover forgotten `close(2)` and `CloseHandle()`. """ + before = self._get_num_fds() self.call(fun) after = self._get_num_fds() diff = after - before + if diff < 0: msg = ( f"negative diff {diff!r} (gc probably collected a" " resource from a previous test)" ) - return self.fail(msg) + raise UnclosedFdError(msg) + if diff > 0: type_ = "fd" if POSIX else "handle" if diff > 1: type_ += "s" - msg = f"{diff} unclosed {type_} after calling {fun!r}" - return self.fail(msg) + msg = ( + f"detected {diff} unclosed {type_} after calling" + f" {qualname(fun)!r} 1 time" + ) + raise UnclosedFdError(msg) + + def _check_heap_count(self, fun): + """Windows only. Calls function once, and detects HeapCreate() + without a corresponding HeapDestroy(). + """ + if not WINDOWS: + return + + before = psutil.heap_info().heap_count + self.call(fun) + after = psutil.heap_info().heap_count + diff = after - before + + if diff < 0: + msg = f"negative diff {diff!r}" + raise UnclosedHeapCreateError(msg) + + if diff > 0: + msg = ( + f"detected {diff} HeapCreate() without a corresponding " + f" HeapDestroy() after calling {qualname(fun)!r} 1 time" + ) + raise UnclosedHeapCreateError(msg) def _call_ntimes(self, fun, times): """Get memory samples (rss, vms, uss) before and after calling fun repeatedly, and return the diffs as a dict. """ gc.collect(generation=1) + + self._heap_trim() + mem1 = self._get_mem() for x in range(times): ret = self.call(fun) @@ -175,12 +264,14 @@ def _check_mem(self, fun, times, retries, tolerance): return prev = diffs - times += times # double calls each retry + times *= 2 # double calls each retry + if self.max_calls_per_retry: + times = min(times, self.max_calls_per_retry) - msg = f"Memory kept increasing after {retries} runs." + "\n".join( + msg = f"memory kept increasing after {retries} runs" + "\n".join( messages ) - return self.fail(msg) + raise MemoryLeakError(msg) # --- @@ -195,6 +286,7 @@ def execute( warmup_times=None, retries=None, tolerance=None, + max_calls_per_retry=None, ): """Run a full leak test on a callable. If specified, the optional arguments override the class attributes with the same @@ -206,6 +298,7 @@ def execute( ) retries = retries if retries is not None else self.retries tolerance = tolerance if tolerance is not None else self.tolerance + max_calls_per_retry = max_calls_per_retry or self.max_calls_per_retry if times < 1: msg = f"times must be >= 1 (got {times})" @@ -219,8 +312,14 @@ def execute( if tolerance < 0: msg = f"tolerance must be >= 0 (got {tolerance})" raise ValueError(msg) + if max_calls_per_retry < 0: + msg = ( + f"max_calls_per_retry must be >= 0 (got {max_calls_per_retry})" + ) + raise ValueError(msg) - self._call_ntimes(fun, warmup_times) # warm up - + self._warmup(fun, warmup_times) self._check_fds(fun) + if WINDOWS: + self._check_heap_count(fun) self._check_mem(fun, times=times, retries=retries, tolerance=tolerance) diff --git a/setup.py b/setup.py index 812ff5157c..302301cdbd 100755 --- a/setup.py +++ b/setup.py @@ -392,7 +392,7 @@ def get_winver(): + glob.glob("psutil/arch/netbsd/*.c") ), define_macros=macros, - libraries=["kvm"], + libraries=["kvm", "jemalloc"], # fmt: off # python 2.7 compatibility requires no comma **py_limited_api diff --git a/tests/__init__.py b/tests/__init__.py index c12ce15e75..91e8faf333 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -184,6 +184,7 @@ def macos_version(): HAS_ENVIRON = hasattr(psutil.Process, "environ") HAS_GETLOADAVG = hasattr(psutil, "getloadavg") HAS_IONICE = hasattr(psutil.Process, "ionice") +HAS_HEAP_INFO = hasattr(psutil, "heap_info") HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") HAS_NET_CONNECTIONS_UNIX = POSIX and not SUNOS HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") @@ -1248,6 +1249,7 @@ class system_namespace: ('users', (), {}), ('virtual_memory', (), {}), ] + if HAS_CPU_FREQ: if MACOS and AARCH64: # skipped due to #1892 pass @@ -1261,6 +1263,10 @@ class system_namespace: getters += [('sensors_fans', (), {})] if HAS_SENSORS_BATTERY: getters += [('sensors_battery', (), {})] + if HAS_HEAP_INFO: + getters += [('heap_info', (), {})] + getters += [('heap_trim', (), {})] + if WINDOWS: getters += [('win_service_iter', (), {})] getters += [('win_service_get', ('alg',), {})] diff --git a/tests/test_contracts.py b/tests/test_contracts.py index aaf0e383bd..3097f45b7a 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -9,10 +9,12 @@ Some of these are duplicates of tests test_system.py and test_process.py. """ +import platform import signal import psutil from psutil import AIX +from psutil import BSD from psutil import FREEBSD from psutil import LINUX from psutil import MACOS @@ -127,6 +129,20 @@ def test_battery(self): LINUX or WINDOWS or FREEBSD or MACOS ) + def test_heap_info(self): + hasit = hasattr(psutil, "heap_info") + if LINUX: + assert hasit == bool(platform.libc_ver() != ("", "")) + else: + assert hasit == MACOS or WINDOWS or BSD + + def test_heap_trim(self): + hasit = hasattr(psutil, "heap_trim") + if LINUX: + assert hasit == bool(platform.libc_ver() != ("", "")) + else: + assert hasit == MACOS or WINDOWS or BSD + class TestAvailProcessAPIs(PsutilTestCase): def test_environ(self): diff --git a/tests/test_heap.py b/tests/test_heap.py new file mode 100755 index 0000000000..f17728cf7e --- /dev/null +++ b/tests/test_heap.py @@ -0,0 +1,325 @@ +#!/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.heap_info()`. + +This module deliberately creates **controlled memory leaks** by calling +low-level C allocation functions (`malloc()`, `HeapAlloc()`, +`VirtualAllocEx()`, etc.) **without** freeing them - exactly how +real-world memory leaks occur in native C extensions code. + +By bypassing Python's memory manager entirely (via `ctypes`), we +directly exercise the underlying system allocator: + +UNIX + +- Small `malloc()` allocations (≤128KB on glibc) without `free()` + increase `heap_used`. +- Large `malloc()` allocations without `free()` trigger `mmap()` and + increase `mmap_used`. + - Note: direct `mmap()` / `munmap()` via `ctypes` was attempted but + proved unreliable. + +Windows + +- `HeapAlloc()` without `HeapFree()` increases `heap_used`. +- `VirtualAllocEx()` without `VirtualFreeEx()` increases `mmap_used`. +- `HeapCreate()` without `HeapDestroy()` increases `heap_count`. + +These tests ensure that `psutil.heap_info()` detects unreleased +native memory across different allocators (glibc on Linux, +jemalloc on BSD/macOS, Windows CRT). +""" + +import ctypes +import gc + +import pytest + +import psutil +from psutil import LINUX +from psutil import MACOS +from psutil import POSIX +from psutil import WINDOWS + +from . import HAS_HEAP_INFO +from . import PsutilTestCase +from . import retry_on_failure + +HEAP_SIZE = 64 * 1024 # 64 KiB +MMAP_SIZE = 10 * 1024 * 1024 # 10 MiB (large enough to trigger mmap()) + + +# ===================================================================== +# --- Utils +# ===================================================================== + + +if POSIX: # noqa: SIM108 + libc = ctypes.CDLL(None) +else: + libc = ctypes.CDLL("msvcrt.dll") + + +def malloc(size): + """Allocate memory via malloc(). If passed a small size, usually + affects heap_used, else mmap_used (not on Windows). + """ + fun = libc.malloc + fun.argtypes = [ctypes.c_size_t] + fun.restype = ctypes.c_void_p + ptr = fun(size) + assert ptr, "malloc() failed" + return ptr + + +def free(ptr): + """Free malloc() memory.""" + fun = libc.free + fun.argtypes = [ctypes.c_void_p] + fun.restype = None + fun(ptr) + + +if WINDOWS: + from ctypes import wintypes + + import win32api + import win32con + import win32process + + kernel32 = ctypes.windll.kernel32 + HEAP_NO_SERIALIZE = 0x00000001 + + # --- for `heap_used` + + def GetProcessHeap(): + fun = kernel32.GetProcessHeap + fun.argtypes = [] + fun.restype = wintypes.HANDLE + heap = fun() + assert heap != 0, "GetProcessHeap failed" + return heap + + def HeapAlloc(heap, size): + fun = kernel32.HeapAlloc + fun.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.c_size_t] + fun.restype = ctypes.c_void_p + addr = fun(heap, 0, size) + assert addr, "HeapAlloc failed" + return addr + + def HeapFree(heap, addr): + fun = kernel32.HeapFree + fun.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.c_void_p] + fun.restype = wintypes.BOOL + assert fun(heap, 0, addr) != 0, "HeapFree failed" + + # --- for `mmap_used` + + def VirtualAllocEx(size): + return win32process.VirtualAllocEx( + win32api.GetCurrentProcess(), + 0, + size, + win32con.MEM_COMMIT | win32con.MEM_RESERVE, + win32con.PAGE_READWRITE, + ) + + def VirtualFreeEx(addr): + win32process.VirtualFreeEx( + win32api.GetCurrentProcess(), addr, 0, win32con.MEM_RELEASE + ) + + # --- for `heap_count` + + def HeapCreate(initial_size, max_size): + fun = kernel32.HeapCreate + fun.argtypes = [ + wintypes.DWORD, + ctypes.c_size_t, + ctypes.c_size_t, + ] + fun.restype = wintypes.HANDLE + heap = fun(HEAP_NO_SERIALIZE, initial_size, max_size) + assert heap != 0, "HeapCreate failed" + return heap + + def HeapDestroy(heap): + fun = kernel32.HeapDestroy + fun.argtypes = [wintypes.HANDLE] + fun.restype = wintypes.BOOL + assert fun(heap) != 0, "HeapDestroy failed" + + +# ===================================================================== +# --- Tests +# ===================================================================== + + +def trim_memory(): + gc.collect() + psutil.heap_trim() + + +def assert_within_percent(actual, expected, percent): + """Assert that `actual` is within `percent` tolerance of `expected`.""" + lower = expected * (1 - percent / 100) + upper = expected * (1 + percent / 100) + if not (lower <= actual <= upper): + raise AssertionError( + f"{actual} is not within {percent}% tolerance of expected" + f" {expected} (allowed range: {lower} - {upper})" + ) + + +@pytest.mark.skipif(not HAS_HEAP_INFO, reason="heap_info() not supported") +class HeapTestCase(PsutilTestCase): + def setUp(self): + trim_memory() + + @classmethod + def tearDownClass(cls): + trim_memory() + + +class TestHeap(HeapTestCase): + + # On Windows malloc() increases mmap_used + @pytest.mark.skipif(WINDOWS, reason="not on WINDOWS") + @retry_on_failure() + def test_heap_used(self): + """Test that a small malloc() allocation without free() + increases heap_used. + """ + size = HEAP_SIZE + + mem1 = psutil.heap_info() + ptr = malloc(size) + mem2 = psutil.heap_info() + + try: + # heap_used should increase (roughly) by the requested size + diff = mem2.heap_used - mem1.heap_used + assert diff > 0 + assert_within_percent(diff, size, percent=10) + + # mmap_used should not increase for small allocations, but + # sometimes it does. + diff = mem2.mmap_used - mem1.mmap_used + if diff != 0: + assert diff > 0 + assert_within_percent(diff, size, percent=10) + finally: + free(ptr) + + # assert we returned close to the baseline (mem1) after free() + trim_memory() + mem3 = psutil.heap_info() + assert_within_percent(mem3.heap_used, mem1.heap_used, percent=10) + assert_within_percent(mem3.mmap_used, mem1.mmap_used, percent=10) + + @pytest.mark.skipif(MACOS, reason="not supported") + @retry_on_failure() + def test_mmap_used(self): + """Test that a large malloc allocation increases mmap_used. + NOTE: `mmap()` / `munmap()` via ctypes proved to be unreliable. + """ + size = MMAP_SIZE + + mem1 = psutil.heap_info() + ptr = malloc(size) + mem2 = psutil.heap_info() + + try: + # mmap_used should increase (roughly) by the requested size + diff = mem2.mmap_used - mem1.mmap_used + assert diff > 0 + assert_within_percent(diff, size, percent=10) + + diff = mem2.heap_used - mem1.heap_used + if diff != 0: + if LINUX: + # heap_used should not increase significantly + assert diff >= 0 + assert_within_percent(diff, 0, percent=5) + else: + # On BSD jemalloc allocates big memory both into + # heap_used and mmap_used. + assert_within_percent(diff, size, percent=10) + + finally: + free(ptr) + + # assert we returned close to the baseline (mem1) after free() + trim_memory() + mem3 = psutil.heap_info() + assert_within_percent(mem3.heap_used, mem1.heap_used, percent=10) + assert_within_percent(mem3.mmap_used, mem1.mmap_used, percent=10) + + if WINDOWS: + assert mem1.heap_count == mem2.heap_count == mem3.heap_count + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +@pytest.mark.xdist_group(name="serial") +class TestHeapWindows(HeapTestCase): + + @retry_on_failure() + def test_heap_used(self): + """Test that HeapAlloc() without HeapFree() increases heap_used.""" + size = HEAP_SIZE + + mem1 = psutil.heap_info() + heap = GetProcessHeap() + addr = HeapAlloc(heap, size) + mem2 = psutil.heap_info() + + try: + assert mem2.heap_used - mem1.heap_used == size + finally: + HeapFree(heap, addr) + + trim_memory() + mem3 = psutil.heap_info() + assert mem3.heap_used == mem1.heap_used + + @retry_on_failure() + def test_mmap_used(self): + """Test that VirtualAllocEx() without VirtualFreeEx() increases + mmap_used. + """ + size = MMAP_SIZE + + mem1 = psutil.heap_info() + addr = VirtualAllocEx(size) + mem2 = psutil.heap_info() + + try: + assert mem2.mmap_used - mem1.mmap_used == size + finally: + VirtualFreeEx(addr) + + trim_memory() + mem3 = psutil.heap_info() + assert mem3.mmap_used == mem1.mmap_used + + @retry_on_failure() + def test_heap_count(self): + """Test that HeapCreate() without HeapDestroy() increases + heap_count. + """ + mem1 = psutil.heap_info() + heap = HeapCreate(HEAP_SIZE, 0) + mem2 = psutil.heap_info() + try: + assert mem2.heap_count == mem1.heap_count + 1 + finally: + HeapDestroy(heap) + + trim_memory() + mem3 = psutil.heap_info() + assert mem3.heap_count == mem1.heap_count diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index 1741cad792..2a3a5af29a 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -31,6 +31,7 @@ from . import HAS_CPU_AFFINITY from . import HAS_CPU_FREQ from . import HAS_ENVIRON +from . import HAS_HEAP_INFO from . import HAS_IONICE from . import HAS_MEMORY_MAPS from . import HAS_NET_IO_COUNTERS @@ -490,6 +491,14 @@ def test_users(self): def test_set_debug(self): self.execute(lambda: psutil._set_debug(False)) + @pytest.mark.skipif(not HAS_HEAP_INFO, reason="not supported") + def test_heap_info(self): + self.execute(psutil.heap_info) + + @pytest.mark.skipif(not HAS_HEAP_INFO, reason="not supported") + def test_heap_trim(self): + self.execute(psutil.heap_trim) + if WINDOWS: # --- win services diff --git a/tests/test_system.py b/tests/test_system.py index c00ab6fb96..f40bdbf832 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -39,6 +39,7 @@ from . import HAS_BATTERY from . import HAS_CPU_FREQ from . import HAS_GETLOADAVG +from . import HAS_HEAP_INFO from . import HAS_NET_IO_COUNTERS from . import HAS_SENSORS_BATTERY from . import HAS_SENSORS_FANS @@ -259,6 +260,21 @@ def test_users(self): else: psutil.Process(user.pid) + @pytest.mark.skipif(not HAS_HEAP_INFO, reason="not supported") + def test_heap_info(self): + m = psutil.heap_info() + assert m.heap_used > 0 + if MACOS: + assert m.mmap_used == 0 # not supported + else: + assert m.mmap_used > 0 + if WINDOWS: + assert m.heap_count >= 0 + + @pytest.mark.skipif(not HAS_HEAP_INFO, reason="not supported") + def test_heap_trim(self): + psutil.heap_trim() + def test_os_constants(self): names = [ "POSIX", diff --git a/tests/test_testutils.py b/tests/test_testutils.py index 7231a73760..91a8696391 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -26,6 +26,8 @@ from psutil._common import open_text from psutil._common import supports_ipv6 from psutil.test import MemoryLeakTestCase +from psutil.test.memleak import MemoryLeakError +from psutil.test.memleak import UnclosedFdError from . import CI_TESTING from . import COVERAGE @@ -369,16 +371,6 @@ def test_create_sockets(self): @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") @pytest.mark.xdist_group(name="serial") class TestMemLeakClass(MemoryLeakTestCase): - - @retry_on_failure() - def test_times(self): - def fun(): - cnt['cnt'] += 1 - - cnt = {'cnt': 0} - self.execute(fun, times=10, warmup_times=15) - assert cnt['cnt'] == 26 - def test_param_err(self): with pytest.raises(ValueError): self.execute(lambda: 0, times=0) @@ -402,10 +394,7 @@ def fun(ls=ls): try: # will consume around 60M in total - with pytest.raises( - AssertionError, - match=r"Memory kept increasing", - ): + with pytest.raises(MemoryLeakError): with contextlib.redirect_stdout( io.StringIO() ), contextlib.redirect_stderr(io.StringIO()): @@ -420,8 +409,7 @@ def fun(): box.append(f) # prevent auto-gc box = [] - kind = "fd" if POSIX else "handle" - with pytest.raises(AssertionError, match="unclosed " + kind): + with pytest.raises(UnclosedFdError): self.execute(fun) @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") @@ -435,7 +423,7 @@ def fun(): ) self.addCleanup(win32api.CloseHandle, handle) - with pytest.raises(AssertionError, match="unclosed handle"): + with pytest.raises(UnclosedFdError): self.execute(fun) def test_tolerance(self): @@ -447,7 +435,6 @@ def fun(): self.execute( fun, times=times, warmup_times=0, tolerance=200 * 1024 * 1024 ) - assert len(ls) == times + 1 class TestTestingUtils(PsutilTestCase): From f1a586c30063f218ea58de591ee6e956f5941e3a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 25 Nov 2025 20:52:46 +0100 Subject: [PATCH 1458/1714] Simplify term_supports_colors() and make it work with pytest (suddenly it doesn't) --- psutil/_common.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 1046a03d2a..33b6282253 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -765,16 +765,13 @@ def decode(s): @memoize def term_supports_colors(file=sys.stdout): # pragma: no cover - if os.name == 'nt': - return True + if not hasattr(file, "isatty") or not file.isatty(): + return False try: - import curses - - assert file.isatty() - curses.setupterm() - return curses.tigetnum("colors") > 0 + file.fileno() except Exception: # noqa: BLE001 return False + return True def hilite(s, color=None, bold=False): # pragma: no cover From 6e92e392981930fb037862dd329f95422e6074e7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 26 Nov 2025 14:39:01 +0100 Subject: [PATCH 1459/1714] Get rid of make.bat. Suggest Git Bash for developing on Windows. --- .github/workflows/issues.py | 2 +- INSTALL.rst | 19 +- MANIFEST.in | 2 - docs/DEVGUIDE.rst | 29 ++- make.bat | 27 --- pyproject.toml | 1 - scripts/internal/winmake.py | 390 ------------------------------------ setup.py | 2 +- 8 files changed, 36 insertions(+), 436 deletions(-) delete mode 100644 make.bat delete mode 100755 scripts/internal/winmake.py diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 1d071ed5dc..3bc00d3fe7 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -37,7 +37,7 @@ ], "windows": [ "windows", "win32", "WinError", "WindowsError", "win10", "win7", - "win ", "mingw", "msys", "studio", "microsoft", "make.bat", + "win ", "mingw", "msys", "studio", "microsoft", "CloseHandle", "GetLastError", "NtQuery", "DLL", "MSVC", "TCHAR", "WCHAR", ".bat", "OpenProcess", "TerminateProcess", "windows error", "NtWow64", "NTSTATUS", "Visual Studio", diff --git a/INSTALL.rst b/INSTALL.rst index 8fb731b408..ed7abf68f8 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -60,13 +60,24 @@ Alpine:: Windows ------- -In order to build / install psutil from sources on Windows you need to install -`Visua Studio 2017 `__ -or later (see cPython `devguide `__'s instructions). -MinGW is not supported. Once Visual Studio is installed do:: +- To build or install psutil from source on Windows, you need to have + **`Visual Studio 2017 `__** + or later installed. For detailed instructions, see the + `CPython Developer Guide `__. +- **MinGW is not supported** for building psutil on Windows. +- To build directly from the source tarball (.tar.gz) on PYPI, run:: pip install --no-binary :all: psutil +- If you want to clone psutil's GIT repository and build / develop locally, + first install: **`Git for Windows `__** + and launch a **Git Bash shell**. This provides a Unix-like environment where + ``make`` works. +- Once inside Git Bash, you can run the usual ``make`` commands:: + + make build + make install + macOS ----- diff --git a/MANIFEST.in b/MANIFEST.in index 0fcff8679d..e25466350c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -24,7 +24,6 @@ include docs/conf.py include docs/index.rst include docs/make.bat include docs/requirements.txt -include make.bat include psutil/__init__.py include psutil/_common.py include psutil/_ntuples.py @@ -167,7 +166,6 @@ include scripts/internal/print_sysinfo.py include scripts/internal/print_timeline.py include scripts/internal/purge_installation.py include scripts/internal/test_python2_setup_py.py -include scripts/internal/winmake.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index bd2b7b4b2b..dbcfcc366a 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -20,8 +20,6 @@ Once you have a compiler installed run: - ``make`` (and the accompanying `Makefile`_) is the designated tool to build, install, run tests and do pretty much anything that involves development. - This also includes Windows, meaning that you can run `make.bat command` - similarly as if you were on UNIX (see `make.bat`_ and `winmake.py`_). Some useful commands are: .. code-block:: bash @@ -41,9 +39,7 @@ Once you have a compiler installed run: .. code-block:: bash - make test ARGS=tests/test_system.py # UNIX - - set ARGS=tests/test_system.py && make test # Windows + make test ARGS=tests/test_system.py - Do not use ``sudo``. ``make install`` installs psutil as a limited user in "edit" / development mode, meaning you can edit psutil code on the fly while @@ -53,9 +49,21 @@ Once you have a compiler installed run: .. code-block:: bash - make test PYTHON=python3.8 # UNIX + make test PYTHON=python3.8 + +Windows +------- + +- The recommended way to develop on Windows is using ``make``, just like on + UNIX systems. +- First, install `Git for Windows`_ and launch a **Git Bash shell**. This + provides a Unix-like environment where ``make`` works. +- Once inside Git Bash, you can run the usual ``make`` commands: - set PYTHON=C:\Python38\python.exe && make test # Windows +.. code-block:: bash + + make build + make test-parallel Coding style ------------ @@ -132,11 +140,10 @@ Documentation - doc can be built with ``make install-pydeps-dev; cd docs; make html``. - public doc is hosted at https://psutil.readthedocs.io. -.. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md +.. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS +.. _`Git for Windows`: ` .. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst -.. _`make.bat`: https://github.com/giampaolo/psutil/blob/master/make.bat -.. _`winmake.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/winmake.py .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile .. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ .. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ @@ -146,3 +153,5 @@ Documentation .. _`tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_linux.py .. _`tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_process.py .. _`tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_system.py +.. _`tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_system.py +.. _`winmake.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/winmake.py diff --git a/make.bat b/make.bat deleted file mode 100644 index c5748f95e6..0000000000 --- a/make.bat +++ /dev/null @@ -1,27 +0,0 @@ -@echo off - -rem ========================================================================== -rem Shortcuts for various tasks, emulating UNIX "make" on Windows. -rem It is primarily intended as a shortcut for compiling / installing -rem psutil and running tests. E.g.: -rem -rem make build -rem make install -rem make test -rem -rem To compile for a specific Python version run: -rem set PYTHON=C:\Python34\python.exe & make.bat build -rem -rem To run a specific test: -rem set ARGS=-k tests/test_system.py && make.bat test -rem ========================================================================== - -if "%PYTHON%" == "" ( - set PYTHON=python -) - -set PYTHONWARNINGS=always -set PSUTIL_DEBUG=1 -set PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 - -%PYTHON% scripts\internal\winmake.py %1 %2 %3 %4 %5 %6 %7 %8 %9 diff --git a/pyproject.toml b/pyproject.toml index a8638718ae..80d36501b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,7 +195,6 @@ disable = [ [tool.vulture] exclude = [ "docs/conf.py", - "scripts/internal/winmake.py", "tests/", ] ignore_decorators = [ diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py deleted file mode 100755 index 84064d8028..0000000000 --- a/scripts/internal/winmake.py +++ /dev/null @@ -1,390 +0,0 @@ -#!/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. - -"""Shortcuts for various tasks, emulating UNIX "make" command on -Windows. This script is supposed to be invoked via "make.bat" and not -used directly. Like on POSIX, you can run multiple targets serially: - - make.bat clean build test - -To run a specific test: - - set ARGS=-k tests/test_system.py && make.bat test -""" - -import fnmatch -import os -import shlex -import shutil -import site -import subprocess -import sys - -import colorama - -# configurable via CLI invocation -PYTHON = os.getenv('PYTHON', sys.executable) -ARGS = shlex.split(os.getenv("ARGS", "")) - -HERE = os.path.abspath(os.path.dirname(__file__)) -ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) - -colorama.init(autoreset=True) - -# =================================================================== -# utils -# =================================================================== - -_cmds = {} - - -def clicmd(fun): - """Mark a function to be invoked as a make target.""" - _cmds[fun.__name__.replace("_", "-")] = fun - return fun - - -def safe_print(text, file=sys.stdout): - """Prints a (unicode) string to the console, encoded depending on - the stdout/file encoding (eg. cp437 on Windows). This is to avoid - encoding errors in case of funky path names. - """ - if not isinstance(text, str): - return print(text, file=file) - try: - print(text, file=file) - except UnicodeEncodeError: - bytes_string = text.encode(file.encoding, 'backslashreplace') - if hasattr(file, 'buffer'): - file.buffer.write(bytes_string) - else: - text = bytes_string.decode(file.encoding, 'strict') - print(text, file=file) - - -def color(s, c): - return c + s - - -red = lambda s: color(s, colorama.Fore.RED) # noqa: E731 -yellow = lambda s: color(s, colorama.Fore.YELLOW) # noqa: E731 -green = lambda s: color(s, colorama.Fore.GREEN) # noqa: E731 -white = lambda s: color(s, colorama.Fore.WHITE) # noqa: E731 - - -def sh(cmd): - """Run a shell command, exit on failure.""" - assert isinstance(cmd, list), repr(cmd) - safe_print(f"$ {' '.join(cmd)}") - result = subprocess.run( - cmd, - env=os.environ, - text=True, - check=False, - ) - - if result.returncode: - print(red(f"command failed with exit code {result.returncode}")) - sys.exit(result.returncode) - - -def safe_remove(path): - try: - os.remove(path) - except FileNotFoundError: - pass - except PermissionError as err: - print(red(err)) - else: - safe_print(f"rm {path}") - - -def safe_rmtree(path): - def onerror(func, path, exc): - if not issubclass(exc[0], FileNotFoundError): - safe_print(exc[1]) - - existed = os.path.isdir(path) - shutil.rmtree(path, onerror=onerror) - if existed and not os.path.isdir(path): - safe_print(f"rmdir -f {path}") - - -def safe_rmpath(path): - """Remove a file or directory at a given path.""" - if os.path.isdir(path): - safe_rmtree(path) - else: - safe_remove(path) - - -def remove_recursive_patterns(*patterns): - """Recursively remove files or directories matching the given - patterns from root. - """ - for root, dirs, files in os.walk('.'): - root = os.path.normpath(root) - if root.startswith('.git/'): - continue - for name in dirs + files: - for pattern in patterns: - if fnmatch.fnmatch(name, pattern): - safe_rmpath(os.path.join(root, name)) - - -# =================================================================== -# commands -# =================================================================== - - -@clicmd -def build(): - """Build / compile.""" - # "build_ext -i" copies compiled *.pyd files in ./psutil directory in - # order to allow "import psutil" when using the interactive interpreter - # from within psutil root directory. - cmd = [PYTHON, "setup.py", "build_ext", "-i"] - if (os.cpu_count() or 1) > 1: - cmd += ['--parallel', str(os.cpu_count())] - # Print coloured warnings in real time. - p = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True - ) - try: - for line in iter(p.stdout.readline, ''): - line = line.strip() - if 'warning' in line: - print(yellow(line)) - elif ( - 'error' in line - and "errors.c" not in line - and "errors.obj" not in line - and "errors.o" not in line - ): - print(red(line)) - else: - print(line) - # retcode = p.poll() - p.communicate() - if p.returncode: - print(red("failure")) - sys.exit(p.returncode) - finally: - p.terminate() - p.wait() - - # Make sure it actually worked. - sh([PYTHON, "-c", "import psutil"]) - print(green("build + import successful")) - - -@clicmd -def install_pip(): - """Install pip.""" - sh([PYTHON, os.path.join(HERE, "install_pip.py")]) - - -@clicmd -def install(): - """Install in develop / edit mode.""" - build() - sh([PYTHON, "setup.py", "develop", "--user"]) - - -@clicmd -def uninstall(): - """Uninstall psutil.""" - # Uninstalling psutil on Windows seems to be tricky. - # On "import psutil" tests may import a psutil version living in - # C:\PythonXY\Lib\site-packages which is not what we want, so - # we try both "pip uninstall psutil" and manually remove stuff - # from site-packages. - clean() - install_pip() - here = os.getcwd() - try: - os.chdir('C:\\') - while True: - try: - import psutil # noqa: F401 - except ImportError: - break - else: - sh([PYTHON, "-m", "pip", "uninstall", "-y", "psutil"]) - finally: - os.chdir(here) - - for dir in site.getsitepackages(): - for name in os.listdir(dir): - if name.startswith('psutil'): - safe_rmpath(os.path.join(dir, name)) - elif name == 'easy-install.pth': - # easy_install can add a line (installation path) into - # easy-install.pth; that line alters sys.path. - path = os.path.join(dir, name) - with open(path) as f: - lines = f.readlines() - hasit = False - for line in lines: - if 'psutil' in line: - hasit = True - break - if hasit: - with open(path, "w") as f: - for line in lines: - if 'psutil' not in line: - f.write(line) - else: - print(f"removed line {line!r} from {path!r}") - - -@clicmd -def clean(): - """Delete build files.""" - remove_recursive_patterns( - "$testfn*", - "*.bak", - "*.core", - "*.egg-info", - "*.orig", - "*.pyc", - "*.pyd", - "*.pyo", - "*.rej", - "*.so", - "*.~", - "*__pycache__", - ".coverage", - ".failed-tests.txt", - "pytest-cache-files*", - ) - for path in ( - "build", - ".coverage", - "dist", - "docs/_build", - "htmlcov", - "tmp", - ): - safe_rmpath(path) - - -@clicmd -def install_pydeps_test(): - install_pip() - install_git_hooks() - sh([PYTHON, "-m", "pip", "install", "--user", "-U", "-e", ".[test]"]) - - -@clicmd -def install_pydeps_dev(): - install_pip() - install_git_hooks() - sh([PYTHON, "-m", "pip", "install", "--user", "-U", "-e", ".[dev]"]) - - -@clicmd -def test(args=ARGS): - """Run tests.""" - # Disable pytest cache on Windows because we usually get - # PermissionDenied on start. Related to network drives (e.g. Z:\). - sh([ - PYTHON, - "-m", - "pytest", - "-p", - "no:cacheprovider", - "--ignore=tests/test_memleaks.py", - *args, - ]) - - -@clicmd -def test_parallel(): - """Run tests in parallel.""" - test(args=["-n", "auto", "--dist", "loadgroup"]) - - -@clicmd -def coverage(): - sh([PYTHON, "-m", "coverage", "run", "-m", "pytest"]) - sh([PYTHON, "-m", "coverage", "report"]) - sh([PYTHON, "-m", "coverage", "html"]) - sh([PYTHON, "-m", "webbrowser", "-t", "htmlcov/index.html"]) - - -@clicmd -def test_process(): - sh([PYTHON, "-m", "pytest", "-k", "test_process.py"]) - - -@clicmd -def test_system(): - sh([PYTHON, "-m", "pytest", "-k", "test_system.py"]) - - -@clicmd -def test_platform(): - sh([PYTHON, "-m", "pytest", "-k", "test_windows.py"]) - - -@clicmd -def test_last_failed(): - test(args=["--last-failed"]) - - -@clicmd -def test_memleaks(): - sh([PYTHON, "-m", "pytest", "-k", "test_memleaks.py"]) - - -@clicmd -def install_git_hooks(): - if os.path.isdir('.git'): - src = os.path.join( - ROOT_DIR, "scripts", "internal", "git_pre_commit.py" - ) - dst = os.path.realpath( - os.path.join(ROOT_DIR, ".git", "hooks", "pre-commit") - ) - with open(src) as s, open(dst, "w") as d: - d.write(s.read()) - - -@clicmd -def generate_manifest(): - """Generate MANIFEST.in file.""" - script = "scripts\\internal\\generate_manifest.py" - out = subprocess.check_output([PYTHON, script], text=True) - with open("MANIFEST.in", "w", newline="\n") as f: - f.write(out) - - -def parse_args(): - if len(sys.argv) <= 1: - # print commands and exit - for name in sorted(_cmds): - doc = _cmds[name].__doc__ or "" - print(f"{green(name):<30} {white(doc)}") - return sys.exit(0) - - funcs = [] - for cmd in sys.argv[1:]: - if cmd not in _cmds: - return sys.exit(f"winmake: no target '{cmd}'") - funcs.append(_cmds[cmd]) - - return funcs # the 'commands' to execute serially - - -def main(): - os.environ["PYTHON"] = PYTHON # propagate it to subprocesses - for fun in parse_args(): - fun() - - -if __name__ == '__main__': - main() diff --git a/setup.py b/setup.py index 302301cdbd..d4efc03aac 100755 --- a/setup.py +++ b/setup.py @@ -98,7 +98,7 @@ "pylint", "pyperf", "pypinfo", - "pyreadline ; os_name == 'nt'", + "pyreadline3 ; os_name == 'nt'", "pytest-cov", "requests", "rstcheck", From f4d37d022405df45b0d9257750bba288cc926de8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 26 Nov 2025 15:20:06 +0100 Subject: [PATCH 1460/1714] purge_installation.py: expand for windows --- scripts/internal/purge_installation.py | 44 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/scripts/internal/purge_installation.py b/scripts/internal/purge_installation.py index 7842900f0a..94c5f287d6 100755 --- a/scripts/internal/purge_installation.py +++ b/scripts/internal/purge_installation.py @@ -16,6 +16,8 @@ PKGNAME = "psutil" +locations = [site.getusersitepackages()] + site.getsitepackages() + def rmpath(path): if os.path.isdir(path): @@ -26,9 +28,7 @@ def rmpath(path): os.remove(path) -def main(): - locations = [site.getusersitepackages()] - locations += site.getsitepackages() +def purge(): for root in locations: if os.path.isdir(root): for name in os.listdir(root): @@ -37,5 +37,43 @@ def main(): rmpath(abspath) +def purge_windows(): + r"""Uninstalling psutil on Windows is more tricky. On "import + psutil" tests may import a psutil version living in + C:\PythonXY\Lib\site-packages which is not what we want, so other + than "pip uninstall psutil" we also manually remove stuff from + site-packages dirs. + """ + for dir in locations: + for name in os.listdir(dir): + path = os.path.join(dir, name) + if name.startswith(PKGNAME): + rmpath(path) + elif name == 'easy-install.pth': + # easy_install can add a line (installation path) into + # easy-install.pth; that line alters sys.path. + path = os.path.join(dir, name) + with open(path) as f: + lines = f.readlines() + hasit = False + for line in lines: + if PKGNAME in line: + hasit = True + break + if hasit: + with open(path, "w") as f: + for line in lines: + if PKGNAME not in line: + f.write(line) + else: + print(f"removed line {line!r} from {path!r}") + + +def main(): + purge() + if os.name == "nt": + purge_windows() + + if __name__ == "__main__": main() From b270dfaa3c11d0ab196fd89c909fd04b8fd2964a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 26 Nov 2025 18:34:20 +0100 Subject: [PATCH 1461/1714] memleak.py: allow execute(*args) params + trim mem more aggressively --- psutil/test/memleak.py | 71 +++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/psutil/test/memleak.py b/psutil/test/memleak.py index 9eea9a299a..56f5b9127b 100644 --- a/psutil/test/memleak.py +++ b/psutil/test/memleak.py @@ -58,7 +58,9 @@ def test_fun(self): import functools import gc +import logging import os +import sys import unittest import psutil @@ -118,8 +120,6 @@ class MemoryLeakTestCase(unittest.TestCase): tolerance = 0 # 0 = no messages; 1 = print diagnostics when memory increases. verbosity = 1 - # max number of calls per retry batch - max_calls_per_retry = 1600 __doc__ = __doc__ @@ -134,15 +134,36 @@ def tearDownClass(cls): def _log(self, msg, level): if level <= self.verbosity: - print_color(msg, color="yellow") + if WINDOWS: + # On Windows we use ctypes to add colors. Avoid that to + # not interfere with memory observations. + print(msg) # noqa: T201 + else: + print_color(msg, color="yellow") + # Force flush to not interfere with memory observations. + sys.stdout.flush() + + def _trim_mem(self): + """Release unused memory. Aims to stabilize memory measurements.""" + # flush standard streams + sys.stdout.flush() + sys.stderr.flush() + + # flush logging handlers + for handler in logging.root.handlers: + handler.flush() + + # full garbage collection + gc.collect() + assert gc.garbage == [] - def _heap_trim(self): - """Release unused memory held by the allocator back to the OS.""" + # release free heap memory back to the OS if hasattr(psutil, "heap_trim"): psutil.heap_trim() def _warmup(self, fun, warmup_times): - self._call_ntimes(fun, warmup_times) + for _ in range(warmup_times): + self.call(fun) # --- getters @@ -150,9 +171,9 @@ def _get_mem(self): mem = thisproc.memory_full_info() heap_used = mmap_used = 0 if hasattr(psutil, "heap_info"): - mallinfo = psutil.heap_info() - heap_used = mallinfo.heap_used - mmap_used = mallinfo.mmap_used + heap = psutil.heap_info() + heap_used = heap.heap_used + mmap_used = heap.mmap_used return { "heap": heap_used, "mmap": mmap_used, @@ -221,20 +242,18 @@ def _check_heap_count(self, fun): raise UnclosedHeapCreateError(msg) def _call_ntimes(self, fun, times): - """Get memory samples (rss, vms, uss) before and after calling - fun repeatedly, and return the diffs as a dict. + """Get memory samples before and after calling fun repeatedly, + and return the diffs as a dict. """ - gc.collect(generation=1) + self._trim_mem() + mem1 = self._get_mem() - self._heap_trim() + for _ in range(times): + self.call(fun) - mem1 = self._get_mem() - for x in range(times): - ret = self.call(fun) - del x, ret - gc.collect(generation=1) + self._trim_mem() mem2 = self._get_mem() - assert gc.garbage == [] + diffs = {k: mem2[k] - mem1[k] for k in mem1} return diffs @@ -265,8 +284,6 @@ def _check_mem(self, fun, times, retries, tolerance): prev = diffs times *= 2 # double calls each retry - if self.max_calls_per_retry: - times = min(times, self.max_calls_per_retry) msg = f"memory kept increasing after {retries} runs" + "\n".join( messages @@ -281,12 +298,11 @@ def call(self, fun): def execute( self, fun, - *, + *args, times=None, warmup_times=None, retries=None, tolerance=None, - max_calls_per_retry=None, ): """Run a full leak test on a callable. If specified, the optional arguments override the class attributes with the same @@ -298,7 +314,6 @@ def execute( ) retries = retries if retries is not None else self.retries tolerance = tolerance if tolerance is not None else self.tolerance - max_calls_per_retry = max_calls_per_retry or self.max_calls_per_retry if times < 1: msg = f"times must be >= 1 (got {times})" @@ -312,11 +327,9 @@ def execute( if tolerance < 0: msg = f"tolerance must be >= 0 (got {tolerance})" raise ValueError(msg) - if max_calls_per_retry < 0: - msg = ( - f"max_calls_per_retry must be >= 0 (got {max_calls_per_retry})" - ) - raise ValueError(msg) + + if args: + fun = functools.partial(fun, *args) self._warmup(fun, warmup_times) self._check_fds(fun) From 6eda0246faa556674e7060c37bc7b56f2f261f2f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 26 Nov 2025 20:14:29 +0100 Subject: [PATCH 1462/1714] Makefile: expand test-* targets --- Makefile | 24 ++++++++++++------------ pyproject.toml | 1 + tests/test_scripts.py | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index fdac6dcc95..73740f8afb 100644 --- a/Makefile +++ b/Makefile @@ -91,24 +91,24 @@ install-git-hooks: ## Install GIT pre-commit hook. RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest -test: ## Run all tests. +test: ## Run all tests (except memleak tests). # To run a specific test do `make test ARGS=tests/test_process.py::TestProcess::test_cmdline` - $(RUN_TEST) --ignore=tests/test_memleaks.py --ignore=tests/test_sudo.py $(ARGS) + $(RUN_TEST) --ignore=tests/test_memleaks.py $(ARGS) -test-parallel: ## Run all tests in parallel. +test-parallel: ## Run all tests (except memleak tests) in parallel. $(RUN_TEST) --ignore=tests/test_memleaks.py -p xdist -n auto --dist loadgroup $(ARGS) -test-process: ## Run process-related API tests. - $(RUN_TEST) tests/test_process.py $(ARGS) +test-process: ## Run process-related tests. + $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_process.py or test_proc or test_pid or Process or pids or pid_exists" $(ARGS) test-process-all: ## Run tests which iterate over all process PIDs. - $(RUN_TEST) tests/test_process_all.py $(ARGS) + $(RUN_TEST) -k test_process_all.py $(ARGS) test-system: ## Run system-related API tests. - $(RUN_TEST) tests/test_system.py $(ARGS) + $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_system.py or test_sys or System or disk or sensors or net_io_counters or net_if_addrs or net_if_stats or users or pids or win_service_ or boot_time" $(ARGS) test-misc: ## Run miscellaneous tests. - $(RUN_TEST) tests/test_misc.py $(ARGS) + $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_misc.py or Misc" $(ARGS) test-scripts: ## Run scripts tests. $(RUN_TEST) tests/test_scripts.py $(ARGS) @@ -123,13 +123,13 @@ test-contracts: ## APIs sanity tests. $(RUN_TEST) tests/test_contracts.py $(ARGS) test-connections: ## Test psutil.net_connections() and Process.net_connections(). - $(RUN_TEST) tests/test_connections.py $(ARGS) + $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_connections.py or net_" $(ARGS) -test-malloc: ## Test psutil.malloc_*() APIs. - $(RUN_TEST) tests/test_malloc.py $(ARGS) +test-heap: ## Test psutil.heap_*() APIs. + $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_heap.py or heap_" $(ARGS) test-posix: ## POSIX specific tests. - $(RUN_TEST) tests/test_posix.py $(ARGS) + $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_posix.py or posix_ or Posix" $(ARGS) test-platform: ## Run specific platform tests only. $(RUN_TEST) tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) diff --git a/pyproject.toml b/pyproject.toml index 80d36501b0..bd5b1e82e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -255,6 +255,7 @@ addopts = [ ] verbosity_subtests = "0" testpaths = ["tests/"] +python_files = ["test_*.py"] [build-system] build-backend = "setuptools.build_meta" diff --git a/tests/test_scripts.py b/tests/test_scripts.py index cea5039eab..2561e457a8 100755 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -50,7 +50,7 @@ class TestExampleScripts(PsutilTestCase): @staticmethod def assert_stdout(exe, *args): env = PYTHON_EXE_ENV.copy() - env.pop("PSUTIL_DEBUG") # avoid spamming to stderr + env.pop("PSUTIL_DEBUG", None) # avoid spamming to stderr exe = os.path.join(SCRIPTS_DIR, exe) cmd = [PYTHON_EXE, exe, *args] try: From 556158f55fc3b2f1271b48c66c46912323cf8bec Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 27 Nov 2025 13:33:57 +0100 Subject: [PATCH 1463/1714] Refact memleak.py --- psutil/test/memleak.py | 152 +++++++++++++++++++++++----------------- tests/test_testutils.py | 5 +- 2 files changed, 89 insertions(+), 68 deletions(-) diff --git a/psutil/test/memleak.py b/psutil/test/memleak.py index 56f5b9127b..5e15147426 100644 --- a/psutil/test/memleak.py +++ b/psutil/test/memleak.py @@ -93,22 +93,67 @@ def qualname(obj): return getattr(obj, "__qualname__", getattr(obj, "__name__", str(obj))) -class MemoryLeakError(AssertionError): - """Raised when a memory leak is detected.""" +# --- exceptions + + +class UnclosedResourceError(AssertionError): + """Base class for errors raised when some resource created during a + function call is left unclosed or unfreed afterward. + """ + + resource_name = "resource" # override in subclasses + + def __init__(self, count, fun_name): + self.count = count + self.fun_name = fun_name + name = self.resource_name + name += "s" if count > 1 else "" # pluralize + msg = ( + f"detected {count} unclosed {name} after calling {fun_name!r} 1" + " time" + ) + super().__init__(msg) + + +class UnclosedFdError(UnclosedResourceError): + """Raised when an unclosed file descriptor is detected after + calling function once. Used to detect forgotten close(). UNIX only. + """ + + resource_name = "file descriptor" -class UnclosedFdError(AssertionError): - """Raised when an unclosed file descriptor (UNIX) or handle - (Windows) is detected. +class UnclosedHandleError(UnclosedResourceError): + """Raised when an unclosed handle is detected after calling + function once. Used to detect forgotten CloseHandle(). + Windows only. """ + resource_name = "handle" + + +class UnclosedHeapCreateError(UnclosedResourceError): + """Raised when test detects HeapCreate() without a corresponding + HeapDestroy() after calling function once. Windows only. + """ + + resource_name = "HeapCreate() call" + + +class MemoryLeakError(AssertionError): + """Raised when a memory leak is detected after calling function + many times. Aims to detect: -class UnclosedHeapCreateError(AssertionError): - """Raised on Windows when test detects HeapCreate() without a - corresponding HeapDestroy(). + - `malloc()` without a corresponding `free()` + - `mmap()` without `munmap()` + - `HeapAlloc()` without `HeapFree()` (Windows) + - `VirtualAlloc()` without `VirtualFree()` (Windows) """ +# --- + + class MemoryLeakTestCase(unittest.TestCase): # Number of times to call the tested function in each iteration. times = 200 @@ -167,6 +212,13 @@ def _warmup(self, fun, warmup_times): # --- getters + def _get_oneshot(self): + return { + "num_fds": thisproc.num_fds() if POSIX else 0, + "num_handles": thisproc.num_handles() if WINDOWS else 0, + "heap_count": psutil.heap_info().heap_count if WINDOWS else 0, + } + def _get_mem(self): mem = thisproc.memory_full_info() heap_used = mmap_used = 0 @@ -182,64 +234,34 @@ def _get_mem(self): "vms": mem.vms, } - def _get_num_fds(self): - if POSIX: - return thisproc.num_fds() - else: - return thisproc.num_handles() - # --- checkers - def _check_fds(self, fun): - """Makes sure `num_fds()` (POSIX) or `num_handles()` (Windows) - do not increase after calling function 1 time. Used to - discover forgotten `close(2)` and `CloseHandle()`. - """ - - before = self._get_num_fds() - self.call(fun) - after = self._get_num_fds() - diff = after - before - - if diff < 0: - msg = ( - f"negative diff {diff!r} (gc probably collected a" - " resource from a previous test)" - ) - raise UnclosedFdError(msg) - - if diff > 0: - type_ = "fd" if POSIX else "handle" - if diff > 1: - type_ += "s" - msg = ( - f"detected {diff} unclosed {type_} after calling" - f" {qualname(fun)!r} 1 time" - ) - raise UnclosedFdError(msg) - - def _check_heap_count(self, fun): - """Windows only. Calls function once, and detects HeapCreate() - without a corresponding HeapDestroy(). - """ - if not WINDOWS: - return - - before = psutil.heap_info().heap_count + def _check_oneshot(self, fun): + before = self._get_oneshot() self.call(fun) - after = psutil.heap_info().heap_count - diff = after - before - - if diff < 0: - msg = f"negative diff {diff!r}" - raise UnclosedHeapCreateError(msg) - - if diff > 0: - msg = ( - f"detected {diff} HeapCreate() without a corresponding " - f" HeapDestroy() after calling {qualname(fun)!r} 1 time" - ) - raise UnclosedHeapCreateError(msg) + after = self._get_oneshot() + + for what, value_before in before.items(): + value_after = after[what] + diff = value_after - value_before + + if diff < 0: + msg = ( + f"WARNING: {what!r} decreased by {abs(diff)} after calling" + f" {qualname(fun)!r} 1 time" + ) + self._log(msg, 0) + + elif diff > 0: + mapping = { + "num_fds": UnclosedFdError, + "num_handles": UnclosedHandleError, + "heap_count": UnclosedHeapCreateError, + } + exc = mapping.get(what) + if exc is None: + raise ValueError(what) + raise exc(diff, qualname(fun)) def _call_ntimes(self, fun, times): """Get memory samples before and after calling fun repeatedly, @@ -331,8 +353,6 @@ def execute( if args: fun = functools.partial(fun, *args) + self._check_oneshot(fun) self._warmup(fun, warmup_times) - self._check_fds(fun) - if WINDOWS: - self._check_heap_count(fun) self._check_mem(fun, times=times, retries=retries, tolerance=tolerance) diff --git a/tests/test_testutils.py b/tests/test_testutils.py index 91a8696391..175d3f268b 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -28,6 +28,7 @@ from psutil.test import MemoryLeakTestCase from psutil.test.memleak import MemoryLeakError from psutil.test.memleak import UnclosedFdError +from psutil.test.memleak import UnclosedHandleError from . import CI_TESTING from . import COVERAGE @@ -402,7 +403,7 @@ def fun(ls=ls): finally: del ls - def test_unclosed_files(self): + def test_unclosed_fds(self): def fun(): f = open(__file__) # noqa: SIM115 self.addCleanup(f.close) @@ -423,7 +424,7 @@ def fun(): ) self.addCleanup(win32api.CloseHandle, handle) - with pytest.raises(UnclosedFdError): + with pytest.raises(UnclosedHandleError): self.execute(fun) def test_tolerance(self): From d5a1398f0860c04883ef881c4760f32d14ef4abf Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:50:37 -0800 Subject: [PATCH 1464/1714] Update cpu_count docs: clarify differences from os.cpu_count (#2696) - Note that psutil.cpu_count is not influenced by PYTHON_CPU_COUNT environment variable introduced in Python 3.13 - Mention that logical CPUs are what cloud providers call vCPUs Co-authored-by: Claude --- docs/index.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index c490c5a834..754b1bffc9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -225,10 +225,13 @@ CPU .. function:: cpu_count(logical=True) - Return the number of logical CPUs in the system (same as `os.cpu_count`_) + Return the number of logical CPUs in the system (similar to `os.cpu_count`_) or ``None`` if undetermined. + Unlike `os.cpu_count`_, this is not influenced by the ``PYTHON_CPU_COUNT`` + environment variable introduced in Python 3.13. "logical CPUs" means the number of physical cores multiplied by the number of threads that can run on each core (this is known as Hyper Threading). + This is what cloud providers often refer to as vCPUs. If *logical* is ``False`` return the number of physical cores only, or ``None`` if undetermined. On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return From ba507bd26bbfe5d1bdc45d606c8979f9e6f632cb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 17 Dec 2025 10:24:06 +0100 Subject: [PATCH 1465/1714] Fix various CI errors --- Makefile | 25 +++++++++++++++---------- docs/DEVGUIDE.rst | 1 - pyproject.toml | 2 ++ setup.py | 19 +++++++++++++------ tests/test_connections.py | 3 ++- tests/test_testutils.py | 1 + 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 73740f8afb..fd7f38aafa 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,7 @@ clean: ## Remove all build files. .failed-tests.txt \ .pytest_cache \ .ruff_cache/ \ + .tests \ build/ \ dist/ \ docs/_build/ \ @@ -74,12 +75,12 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. $(MAKE) install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test] + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` install-pydeps-dev: ## Install python deps meant for local development. $(MAKE) install-git-hooks $(MAKE) install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) -e .[test,dev] + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.DEV_DEPS))"` install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit @@ -229,19 +230,23 @@ ci-lint: ## Run all linters on GitHub CI. ci-test: ## Run tests on GitHub CI. Used by BSD runners. $(MAKE) install-sysdeps - PIP_BREAK_SYSTEM_PACKAGES=1 $(MAKE) install-pydeps-test + $(MAKE) install-pydeps-test + $(MAKE) build $(MAKE) print-sysinfo - $(PYTHON_ENV_VARS) $(PYTHON) -m pytest tests/ + $(MAKE) test + $(MAKE) test-memleaks -ci-test-cibuildwheel: ## Run tests from cibuildwheel. - # testing the wheels means we can't use other test targets which are rebuilding the python extensions - # we also need to run the tests from another folder for pytest not to use the sources but only what's been installed +ci-test-cibuildwheel: ## Run CI tests for the built wheels. $(MAKE) install-sysdeps - PIP_BREAK_SYSTEM_PACKAGES=1 $(MAKE) install-pydeps-test + $(MAKE) install-pydeps-test $(MAKE) print-sysinfo + # Tests must be run from a separate directory so pytest does not import + # from the source tree and instead exercises only the installed wheel. + rm -rf .tests tests/__pycache__ mkdir -p .tests - cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs --ignore=../tests/test_memleaks.py ../tests - cd .tests/ && $(PYTHON_ENV_VARS) $(PYTHON) -m pytest --pyargs ../tests/test_memleaks.py + cp -r tests .tests/ + cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) $(PYTHON) -m pytest -k "not test_memleaks.py" + cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) $(PYTHON) -m pytest -k "test_memleaks.py" ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index dbcfcc366a..11f4727655 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -153,5 +153,4 @@ Documentation .. _`tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_linux.py .. _`tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_process.py .. _`tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_system.py -.. _`tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_system.py .. _`winmake.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/winmake.py diff --git a/pyproject.toml b/pyproject.toml index bd5b1e82e0..d2a309a127 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -207,7 +207,9 @@ ignore_decorators = [ ignore_messages = [ "Duplicate explicit target name", "Duplicate implicit target name", + "Duplicate name \"development guide\" for external target", "Hyperlink target \".*?\" is not referenced", + "Target name overrides implicit target name \"development guide\"", ] [tool.tomlsort] diff --git a/setup.py b/setup.py index d4efc03aac..1f680b670e 100755 --- a/setup.py +++ b/setup.py @@ -81,10 +81,13 @@ "pytest-instafail", "pytest-xdist", "setuptools", - "pywin32 ; os_name == 'nt' and platform_python_implementation != 'PyPy'", - "wheel ; os_name == 'nt' and platform_python_implementation != 'PyPy'", - "wmi ; os_name == 'nt' and platform_python_implementation != 'PyPy'", ] +if WINDOWS and not hasattr(sys, "pypy_version_info"): + TEST_DEPS.extend([ + "pywin32", + "wheel", + "wmi", + ]) # Development deps, installable via `pip install .[dev]` or # `make install-pydeps-dev`. @@ -92,13 +95,11 @@ "abi3audit", "black", "check-manifest", - "colorama ; os_name == 'nt'", "coverage", "packaging", "pylint", "pyperf", "pypinfo", - "pyreadline3 ; os_name == 'nt'", "pytest-cov", "requests", "rstcheck", @@ -113,6 +114,12 @@ "wheel", ] +if WINDOWS: + DEV_DEPS.extend([ + "colorama", + "pyreadline3", + ]) + # The pre-processor macros that are passed to the C compiler when # building the extension. macros = [] @@ -480,7 +487,7 @@ def main(): url='https://github.com/giampaolo/psutil', platforms='Platform Independent', license='BSD-3-Clause', - packages=['psutil'], + packages=['psutil', 'psutil.test'], ext_modules=[ext], options=options, classifiers=[ diff --git a/tests/test_connections.py b/tests/test_connections.py index 55470060e8..3c1acb58de 100755 --- a/tests/test_connections.py +++ b/tests/test_connections.py @@ -528,7 +528,8 @@ def test_multi_sockets_procs(self): fnames.append(fname) src = textwrap.dedent(f"""\ import time, os, sys - sys.path.insert(0, r'{ROOT_DIR}') + if 'CIBUILDWHEEL' not in os.environ: + sys.path.insert(0, r'{ROOT_DIR}') from tests import create_sockets with create_sockets(): with open(r'{fname}', 'w') as f: diff --git a/tests/test_testutils.py b/tests/test_testutils.py index 175d3f268b..7c43229e29 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -403,6 +403,7 @@ def fun(ls=ls): finally: del ls + @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_unclosed_fds(self): def fun(): f = open(__file__) # noqa: SIM115 From ac56e6ad0b7c08755a5542afdacb668ad164fd09 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 18 Dec 2025 19:55:24 +0100 Subject: [PATCH 1466/1714] CI: don't cancel CI in progress on 1st failure --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ead62ac505..a644a5b989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,9 @@ on: [push, pull_request] name: build concurrency: - group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || '' }} + # Cancel build if a new one starts, but don't interrupt all jobs on the first + # failure. + group: build-${{ github.ref }} cancel-in-progress: true jobs: From 5085421cabed317d5e2f7164e6072b0be05ba4f1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 21 Dec 2025 13:29:42 +0100 Subject: [PATCH 1467/1714] Use external psleak module for memleak tests (#2698) ...and get rid of internal psutil.test.MemleakTestCase framework. --- MANIFEST.in | 2 - Makefile | 5 +- README.rst | 11 -- docs/index.rst | 74 --------- psutil/test/__init__.py | 7 - psutil/test/memleak.py | 358 ---------------------------------------- setup.py | 2 +- tests/test_memleaks.py | 151 ++++++----------- tests/test_testutils.py | 81 --------- 9 files changed, 57 insertions(+), 634 deletions(-) delete mode 100644 psutil/test/__init__.py delete mode 100644 psutil/test/memleak.py diff --git a/MANIFEST.in b/MANIFEST.in index e25466350c..04d553cf99 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -137,8 +137,6 @@ include psutil/arch/windows/services.c include psutil/arch/windows/socks.c include psutil/arch/windows/sys.c include psutil/arch/windows/wmi.c -include psutil/test/__init__.py -include psutil/test/memleak.py include pyproject.toml include scripts/battery.py include scripts/cpu_distribution.py diff --git a/Makefile b/Makefile index fd7f38aafa..59e14e689c 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,7 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. $(MAKE) install-pip PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install git+https://github.com/giampaolo/psleak.git install-pydeps-dev: ## Install python deps meant for local development. $(MAKE) install-git-hooks @@ -136,7 +137,7 @@ test-platform: ## Run specific platform tests only. $(RUN_TEST) tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) test-memleaks: ## Memory leak tests. - $(RUN_TEST) tests/test_memleaks.py $(ARGS) + PYTHONMALLOC=malloc $(RUN_TEST) -k test_memleaks.py $(ARGS) test-sudo: ## Run tests requiring root privileges. # Use unittest runner because pytest may not be installed as root. @@ -246,7 +247,7 @@ ci-test-cibuildwheel: ## Run CI tests for the built wheels. mkdir -p .tests cp -r tests .tests/ cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) $(PYTHON) -m pytest -k "not test_memleaks.py" - cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) $(PYTHON) -m pytest -k "test_memleaks.py" + cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) PYTHONMALLOC=malloc $(PYTHON) -m pytest -k "test_memleaks.py" ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit diff --git a/README.rst b/README.rst index 259f0a32fd..19bd9287c7 100644 --- a/README.rst +++ b/README.rst @@ -464,17 +464,6 @@ Heap info pheap(heap_used=5177792, mmap_used=819200) >>> psutil.heap_trim() -Detecting memory leaks in C functions -------------------------------------- - -.. code-block:: python - - from psutil.test import MemoryLeakTestCase - - class TestLeaks(MemoryLeakTestCase): - def test_fun(self): - self.execute(some_function) - Windows services ---------------- diff --git a/docs/index.rst b/docs/index.rst index 754b1bffc9..234711a017 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2299,80 +2299,6 @@ Example code: 'status': 'stopped', 'username': 'NT AUTHORITY\\LocalService'} -Testing utilities -================= - -The ``psutil.test`` subpackage includes a helper class to assist in writing -memory-leak detection tests. - -.. class:: psutil.test.MemoryLeakTestCase - - A testing framework for detecting memory leaks in functions, typically those - implemented in C that forget to ``free()`` heap memory, call ``Py_DECREF`` on - Python objects, and so on. It works by comparing the process's memory usage - before and after repeatedly calling the target function. - - Detecting memory leaks reliably is inherently difficult (and probably - impossible) because of how the OS manages memory, garbage collection, and - caching. Memory usage may even decrease between runs. So this is not meant to - be bullet proof. To reduce false positives, when an increase in memory is - detected (mem > 0), the test is retried up to 5 times, increasing the - number of function calls each time. If memory continues to grow, the test is - considered a failure. - The test currently monitors RSS, VMS, and `USS `__ memory. - On supported platforms, it also monitors **heap metrics** (``heap_used``, ``mmap_used`` from - :func:`heap_info`). - - In addition it also ensures that the target function does not leak - file descriptors (UNIX) or handles (Windows). - - .. versionadded:: 7.2.0 - - .. warning:: - This class is experimental, meaning its API or internal algorithm may - change in the future. - - Usage example:: - - from psutil.test import MemoryLeakTestCase - - class TestLeaks(MemoryLeakTestCase): - def test_fun(self): - self.execute(some_function) - - Class attributes and methods: - - .. attribute:: times - :value: 200 - - Number of times to call the tested function in each iteration. - - .. attribute:: retries - :value: 5 - - Maximum number of retries if memory growth is detected. - - .. attribute:: warmup_times - :value: 10 - - Number of warm-up calls before measurements begin. - - .. attribute:: tolerance - :value: 0 - - Allowed memory difference (in bytes) before considering it a leak. - - .. attribute:: verbosity - :value: 1 - - 0 = no messages; 1 = print diagnostics when memory increases during the - test run. - - .. method:: execute(fun, *, times=None, warmup_times=None, retries=None, tolerance=None) - - Run a full leak test on a callable. If specified, the optional arguments - override the class attributes with the same name. - Constants ========= diff --git a/psutil/test/__init__.py b/psutil/test/__init__.py deleted file mode 100644 index 33af57e4c1..0000000000 --- a/psutil/test/__init__.py +++ /dev/null @@ -1,7 +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. - -from .memleak import MemoryLeakTestCase - -__all__ = ["MemoryLeakTestCase"] diff --git a/psutil/test/memleak.py b/psutil/test/memleak.py deleted file mode 100644 index 5e15147426..0000000000 --- a/psutil/test/memleak.py +++ /dev/null @@ -1,358 +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. - -"""A testing framework for detecting memory leaks in functions, -typically those implemented in C that forget to `free()` heap memory, -call `Py_DECREF` on Python objects, and so on. It works by comparing -the process's memory usage before and after repeatedly calling the -target function. - -Detecting memory leaks reliably is inherently difficult (and probably -impossible) because of how the OS manages memory, garbage collection, -and caching. Memory usage may even decrease between runs. So this is -not meant to be bullet proof. To reduce false positives, when an -increase in memory is detected (mem > 0), the test is retried up to 5 -times, increasing the number of function calls each time. If memory -continues to grow, the test is considered a failure. - -The test monitors RSS, VMS, and USS [1] memory. In addition, it also -monitors **heap metrics** (`heap_used`, `mmap_used` from -`psutil.heap_info()`). - -In other words, this is specifically designed to catch cases where a C -extension or other native code allocates memory via `malloc()` or -similar functions but fails to call `free()`, resulting in unreleased -memory that would otherwise remain in the process heap or in mapped -memory regions. - -The types of allocations this should catch include: - -- `malloc()` without a corresponding `free()` -- `mmap()` without `munmap()` -- `HeapAlloc()` without `HeapFree()` (Windows) -- `VirtualAlloc()` without `VirtualFree()` (Windows) -- `HeapCreate()` without `HeapDestroy()` (Windows) - -In addition it also ensures that the target function does not leak file -descriptors (UNIX) or handles (Windows) such as: - -- `open()` without a corresponding `close()` (UNIX) -- `CreateFile()` / `CreateProcess()` / ... without `CloseHandle()` - (Windows) - -Usage example: - - from psutil.test import MemoryLeakTestCase - - class TestLeaks(MemoryLeakTestCase): - def test_fun(self): - self.execute(some_function) - -NOTE - This class is experimental, meaning its API or internal -algorithm may change in the future. - -[1] https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python -[2] https://github.com/giampaolo/psutil/issues/1275 -""" - -import functools -import gc -import logging -import os -import sys -import unittest - -import psutil -from psutil._common import POSIX -from psutil._common import WINDOWS -from psutil._common import bytes2human -from psutil._common import print_color - -thisproc = psutil.Process() -b2h = functools.partial(bytes2human, format="%(value)i%(symbol)s") - - -def format_run_line(idx, diffs, times): - parts = [f"{k}={'+' + b2h(v):<6}" for k, v in diffs.items() if v > 0] - metrics = " | ".join(parts) - avg = "0B" - if parts: - first_key = next(k for k, v in diffs.items() if v > 0) - avg = b2h(diffs[first_key] // times) - s = f"Run #{idx:>2}: {metrics:<50} (calls={times:>5}, avg/call=+{avg})" - if idx == 1: - s = "\n" + s - return s - - -def qualname(obj): - """Return a human-readable qualified name for a function, method or - class. - """ - return getattr(obj, "__qualname__", getattr(obj, "__name__", str(obj))) - - -# --- exceptions - - -class UnclosedResourceError(AssertionError): - """Base class for errors raised when some resource created during a - function call is left unclosed or unfreed afterward. - """ - - resource_name = "resource" # override in subclasses - - def __init__(self, count, fun_name): - self.count = count - self.fun_name = fun_name - name = self.resource_name - name += "s" if count > 1 else "" # pluralize - msg = ( - f"detected {count} unclosed {name} after calling {fun_name!r} 1" - " time" - ) - super().__init__(msg) - - -class UnclosedFdError(UnclosedResourceError): - """Raised when an unclosed file descriptor is detected after - calling function once. Used to detect forgotten close(). UNIX only. - """ - - resource_name = "file descriptor" - - -class UnclosedHandleError(UnclosedResourceError): - """Raised when an unclosed handle is detected after calling - function once. Used to detect forgotten CloseHandle(). - Windows only. - """ - - resource_name = "handle" - - -class UnclosedHeapCreateError(UnclosedResourceError): - """Raised when test detects HeapCreate() without a corresponding - HeapDestroy() after calling function once. Windows only. - """ - - resource_name = "HeapCreate() call" - - -class MemoryLeakError(AssertionError): - """Raised when a memory leak is detected after calling function - many times. Aims to detect: - - - `malloc()` without a corresponding `free()` - - `mmap()` without `munmap()` - - `HeapAlloc()` without `HeapFree()` (Windows) - - `VirtualAlloc()` without `VirtualFree()` (Windows) - """ - - -# --- - - -class MemoryLeakTestCase(unittest.TestCase): - # Number of times to call the tested function in each iteration. - times = 200 - # Maximum number of retries if memory growth is detected. - retries = 5 - # Number of warm-up calls before measurements begin. - warmup_times = 10 - # Allowed memory difference (in bytes) before considering it a leak. - tolerance = 0 - # 0 = no messages; 1 = print diagnostics when memory increases. - verbosity = 1 - - __doc__ = __doc__ - - @classmethod - def setUpClass(cls): - cls._psutil_debug_orig = bool(os.getenv("PSUTIL_DEBUG")) - psutil._set_debug(False) # avoid spamming to stderr - - @classmethod - def tearDownClass(cls): - psutil._set_debug(cls._psutil_debug_orig) - - def _log(self, msg, level): - if level <= self.verbosity: - if WINDOWS: - # On Windows we use ctypes to add colors. Avoid that to - # not interfere with memory observations. - print(msg) # noqa: T201 - else: - print_color(msg, color="yellow") - # Force flush to not interfere with memory observations. - sys.stdout.flush() - - def _trim_mem(self): - """Release unused memory. Aims to stabilize memory measurements.""" - # flush standard streams - sys.stdout.flush() - sys.stderr.flush() - - # flush logging handlers - for handler in logging.root.handlers: - handler.flush() - - # full garbage collection - gc.collect() - assert gc.garbage == [] - - # release free heap memory back to the OS - if hasattr(psutil, "heap_trim"): - psutil.heap_trim() - - def _warmup(self, fun, warmup_times): - for _ in range(warmup_times): - self.call(fun) - - # --- getters - - def _get_oneshot(self): - return { - "num_fds": thisproc.num_fds() if POSIX else 0, - "num_handles": thisproc.num_handles() if WINDOWS else 0, - "heap_count": psutil.heap_info().heap_count if WINDOWS else 0, - } - - def _get_mem(self): - mem = thisproc.memory_full_info() - heap_used = mmap_used = 0 - if hasattr(psutil, "heap_info"): - heap = psutil.heap_info() - heap_used = heap.heap_used - mmap_used = heap.mmap_used - return { - "heap": heap_used, - "mmap": mmap_used, - "uss": getattr(mem, "uss", 0), - "rss": mem.rss, - "vms": mem.vms, - } - - # --- checkers - - def _check_oneshot(self, fun): - before = self._get_oneshot() - self.call(fun) - after = self._get_oneshot() - - for what, value_before in before.items(): - value_after = after[what] - diff = value_after - value_before - - if diff < 0: - msg = ( - f"WARNING: {what!r} decreased by {abs(diff)} after calling" - f" {qualname(fun)!r} 1 time" - ) - self._log(msg, 0) - - elif diff > 0: - mapping = { - "num_fds": UnclosedFdError, - "num_handles": UnclosedHandleError, - "heap_count": UnclosedHeapCreateError, - } - exc = mapping.get(what) - if exc is None: - raise ValueError(what) - raise exc(diff, qualname(fun)) - - def _call_ntimes(self, fun, times): - """Get memory samples before and after calling fun repeatedly, - and return the diffs as a dict. - """ - self._trim_mem() - mem1 = self._get_mem() - - for _ in range(times): - self.call(fun) - - self._trim_mem() - mem2 = self._get_mem() - - diffs = {k: mem2[k] - mem1[k] for k in mem1} - return diffs - - def _check_mem(self, fun, times, retries, tolerance): - prev = {} - messages = [] - - for idx in range(1, retries + 1): - diffs = self._call_ntimes(fun, times) - leaks = {k: v for k, v in diffs.items() if v > 0} - - if leaks: - line = format_run_line(idx, leaks, times) - messages.append(line) - self._log(line, 1) - - stable = all( - diffs.get(k, 0) <= tolerance - or diffs.get(k, 0) <= prev.get(k, 0) - for k in diffs - ) - if stable: - if idx > 1 and leaks: - self._log( - "Memory stabilized (no further growth detected)", 1 - ) - return - - prev = diffs - times *= 2 # double calls each retry - - msg = f"memory kept increasing after {retries} runs" + "\n".join( - messages - ) - raise MemoryLeakError(msg) - - # --- - - def call(self, fun): - return fun() - - def execute( - self, - fun, - *args, - times=None, - warmup_times=None, - retries=None, - tolerance=None, - ): - """Run a full leak test on a callable. If specified, the - optional arguments override the class attributes with the same - name. - """ - 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 - - if times < 1: - msg = f"times must be >= 1 (got {times})" - raise ValueError(msg) - if warmup_times < 0: - msg = f"warmup_times must be >= 0 (got {warmup_times})" - raise ValueError(msg) - if retries < 0: - msg = f"retries must be >= 0 (got {retries})" - raise ValueError(msg) - if tolerance < 0: - msg = f"tolerance must be >= 0 (got {tolerance})" - raise ValueError(msg) - - if args: - fun = functools.partial(fun, *args) - - self._check_oneshot(fun) - self._warmup(fun, warmup_times) - self._check_mem(fun, times=times, retries=retries, tolerance=tolerance) diff --git a/setup.py b/setup.py index 1f680b670e..63a7e9d935 100755 --- a/setup.py +++ b/setup.py @@ -487,7 +487,7 @@ def main(): url='https://github.com/giampaolo/psutil', platforms='Platform Independent', license='BSD-3-Clause', - packages=['psutil', 'psutil.test'], + packages=['psutil'], ext_modules=[ext], options=options, classifiers=[ diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index 2a3a5af29a..52b501b53e 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -4,19 +4,15 @@ # 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. +"""Regression test suite for detecting memory leaks in the underlying C +extension. Requires https://github.com/giampaolo/psleak. """ import functools import os +from psleak import MemoryLeakTestCase + import psutil from psutil import LINUX from psutil import MACOS @@ -24,10 +20,8 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS -from psutil.test import MemoryLeakTestCase from . import AARCH64 -from . import CI_TESTING from . import HAS_CPU_AFFINITY from . import HAS_CPU_FREQ from . import HAS_ENVIRON @@ -52,34 +46,12 @@ cext = psutil._psplatform.cext thisproc = psutil.Process() -if CI_TESTING: - MemoryLeakTestCase.retries *= 2 - -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 +# try to minimize flaky failures +MemoryLeakTestCase.retries = 30 +TIMES = MemoryLeakTestCase.times +FEW_TIMES = int(TIMES / 10) # =================================================================== # Process class @@ -91,52 +63,32 @@ class TestProcessObjectLeaks(MemoryLeakTestCase): proc = thisproc - def execute_w_exc(self, exc, fun, **kwargs): - """Run MemoryLeakTestCase.execute() expecting fun() to raise - exc on every call. - """ - - def call(): - try: - fun() - except exc: - pass - else: - return self.fail(f"{fun} did not raise {exc}") - - self.execute(call, **kwargs) - 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): + if WINDOWS and self.proc.is_running(): + self.proc.cmdline() 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) @pytest.mark.skipif(not POSIX, reason="POSIX only") - @fewtimes_if_linux() def test_uids(self): self.execute(self.proc.uids) @pytest.mark.skipif(not POSIX, reason="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) @@ -163,10 +115,9 @@ def test_ionice_set(self): @pytest.mark.skipif(WINDOWS, reason="not on WINDOWS") def test_ionice_set_badarg(self): fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) - self.execute_w_exc(OSError, fun) + self.execute_w_exc(OSError, fun, retries=20) @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") - @fewtimes_if_linux() def test_io_counters(self): self.execute(self.proc.io_counters) @@ -176,11 +127,9 @@ def test_username(self): 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) @@ -190,38 +139,31 @@ def test_num_handles(self): self.execute(self.proc.num_handles) @pytest.mark.skipif(not POSIX, reason="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) + kw = {"times": 50} if WINDOWS else {} + self.execute(self.proc.threads, **kw) - @fewtimes_if_linux() def test_cpu_times(self): self.execute(self.proc.cpu_times) - @fewtimes_if_linux() @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="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) @pytest.mark.skipif(not POSIX, reason="POSIX only") - @fewtimes_if_linux() def test_terminal(self): self.execute(self.proc.terminal) @@ -229,7 +171,6 @@ 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) @@ -244,17 +185,19 @@ def test_cpu_affinity_set(self): @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_set_badarg(self): - self.execute_w_exc(ValueError, lambda: self.proc.cpu_affinity([-1])) + self.execute_w_exc( + ValueError, lambda: self.proc.cpu_affinity([-1]), retries=20 + ) - @fewtimes_if_linux() def test_open_files(self): + kw = {"times": 10, "retries": 30} if WINDOWS else {} with open(get_testfn(), 'w'): - self.execute(self.proc.open_files) + self.execute(self.proc.open_files, **kw) @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") - @fewtimes_if_linux() + @pytest.mark.skipif(LINUX, reason="too slow on LINUX") def test_memory_maps(self): - self.execute(self.proc.memory_maps) + self.execute(self.proc.memory_maps, times=60, retries=10) @pytest.mark.skipif(not LINUX, reason="LINUX only") @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") @@ -270,9 +213,10 @@ def test_rlimit_set(self): @pytest.mark.skipif(not LINUX, reason="LINUX only") @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") def test_rlimit_set_badarg(self): - self.execute_w_exc((OSError, ValueError), lambda: self.proc.rlimit(-1)) + self.execute_w_exc( + (OSError, ValueError), lambda: self.proc.rlimit(-1), retries=20 + ) - @fewtimes_if_linux() # Windows implementation is based on a single system-wide # function (tested later). @pytest.mark.skipif(WINDOWS, reason="worthless on WINDOWS") @@ -280,9 +224,10 @@ def test_net_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. + times = FEW_TIMES if LINUX else self.times with create_sockets(): kind = 'inet' if SUNOS else 'all' - self.execute(lambda: self.proc.net_connections(kind)) + self.execute(lambda: self.proc.net_connections(kind), times=times) @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") def test_environ(self): @@ -319,6 +264,9 @@ def call(self, fun): except psutil.NoSuchProcess: pass + def test_cpu_affinity_set_badarg(self): + raise pytest.skip("skip") + if WINDOWS: def test_kill(self): @@ -370,32 +318,27 @@ def test_coverage(self): # --- 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 @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): - self.execute(psutil.cpu_freq) + times = FEW_TIMES if LINUX else self.times + self.execute(psutil.cpu_freq, times=times) @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") def test_getloadavg(self): @@ -429,32 +372,39 @@ def test_disk_partitions(self): LINUX and not os.path.exists('/proc/diskstats'), reason="/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() @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") def test_net_io_counters(self): self.execute(lambda: psutil.net_io_counters(nowrap=False)) - @fewtimes_if_linux() @pytest.mark.skipif(MACOS and os.getuid() != 0, reason="need root access") def test_net_connections(self): # always opens and handle on Windows() (once) psutil.net_connections(kind='all') + times = FEW_TIMES if LINUX else self.times with create_sockets(): - self.execute(lambda: psutil.net_connections(kind='all')) + self.execute( + lambda: psutil.net_connections(kind='all'), times=times + ) def test_net_if_addrs(self): + if WINDOWS: + # Calling GetAdaptersAddresses() for the first time + # allocates internal OS handles. These handles persist for + # the lifetime of the process, causing psleak to report + # "unclosed handles". Call it here first to avoid false + # positives. + psutil.net_if_addrs() + # 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) @@ -464,28 +414,33 @@ def test_net_if_stats(self): # --- sensors - @fewtimes_if_linux() @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") def test_sensors_battery(self): self.execute(psutil.sensors_battery) - @fewtimes_if_linux() @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + @pytest.mark.skipif(LINUX, reason="too slow on LINUX") def test_sensors_temperatures(self): - self.execute(psutil.sensors_temperatures) + times = FEW_TIMES if LINUX else self.times + self.execute(psutil.sensors_temperatures, times=times) - @fewtimes_if_linux() @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") def test_sensors_fans(self): - self.execute(psutil.sensors_fans) + times = FEW_TIMES if LINUX else self.times + self.execute(psutil.sensors_fans, times=times) # --- others - @fewtimes_if_linux() def test_boot_time(self): self.execute(psutil.boot_time) def test_users(self): + if WINDOWS: + # The first time this is called it allocates internal OS + # handles. These handles persist for the lifetime of the + # process, causing psleak to report "unclosed handles". + # Call it here first to avoid false positives. + psutil.users() self.execute(psutil.users) def test_set_debug(self): diff --git a/tests/test_testutils.py b/tests/test_testutils.py index 7c43229e29..5a0787d245 100755 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -7,9 +7,7 @@ """Tests for testing utils.""" import collections -import contextlib import errno -import io import os import socket import stat @@ -21,19 +19,11 @@ from psutil import FREEBSD from psutil import NETBSD from psutil import POSIX -from psutil import WINDOWS from psutil._common import open_binary from psutil._common import open_text from psutil._common import supports_ipv6 -from psutil.test import MemoryLeakTestCase -from psutil.test.memleak import MemoryLeakError -from psutil.test.memleak import UnclosedFdError -from psutil.test.memleak import UnclosedHandleError -from . import CI_TESTING -from . import COVERAGE from . import HAS_NET_CONNECTIONS_UNIX -from . import PYPY from . import PYTHON_EXE from . import PYTHON_EXE_ENV from . import PsutilTestCase @@ -49,7 +39,6 @@ from . import pytest from . import reap_children from . import retry -from . import retry_on_failure from . import safe_mkdir from . import safe_rmpath from . import system_namespace @@ -369,76 +358,6 @@ def test_create_sockets(self): assert types[socket.SOCK_DGRAM] >= 2 -@pytest.mark.skipif(PYPY, reason="unreliable on PYPY") -@pytest.mark.xdist_group(name="serial") -class TestMemLeakClass(MemoryLeakTestCase): - def test_param_err(self): - with pytest.raises(ValueError): - self.execute(lambda: 0, times=0) - with pytest.raises(ValueError): - self.execute(lambda: 0, times=-1) - with pytest.raises(ValueError): - self.execute(lambda: 0, warmup_times=-1) - with pytest.raises(ValueError): - self.execute(lambda: 0, tolerance=-1) - with pytest.raises(ValueError): - self.execute(lambda: 0, retries=-1) - - @retry_on_failure() - @pytest.mark.skipif(CI_TESTING, reason="skipped on CI") - @pytest.mark.skipif(COVERAGE, reason="skipped during test coverage") - def test_leak_mem(self): - ls = [] - - def fun(ls=ls): - ls.append("x" * 248 * 1024) - - try: - # will consume around 60M in total - with pytest.raises(MemoryLeakError): - with contextlib.redirect_stdout( - io.StringIO() - ), contextlib.redirect_stderr(io.StringIO()): - self.execute(fun, times=100) - finally: - del ls - - @pytest.mark.skipif(not POSIX, reason="POSIX only") - def test_unclosed_fds(self): - def fun(): - f = open(__file__) # noqa: SIM115 - self.addCleanup(f.close) - box.append(f) # prevent auto-gc - - box = [] - with pytest.raises(UnclosedFdError): - self.execute(fun) - - @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") - def test_unclosed_handles(self): - import win32api - import win32con - - def fun(): - handle = win32api.OpenProcess( - win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() - ) - self.addCleanup(win32api.CloseHandle, handle) - - with pytest.raises(UnclosedHandleError): - 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 - ) - - class TestTestingUtils(PsutilTestCase): def test_process_namespace(self): p = psutil.Process() From 1a946cfe738045cecf031222cd5078da21946af4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 21 Dec 2025 17:27:03 +0100 Subject: [PATCH 1468/1714] Take psleak from PYPI Signed-off-by: Giampaolo Rodola --- Makefile | 4 +--- setup.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 59e14e689c..d75ec54fc6 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,6 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. $(MAKE) install-pip PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` - PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install git+https://github.com/giampaolo/psleak.git install-pydeps-dev: ## Install python deps meant for local development. $(MAKE) install-git-hooks @@ -146,8 +145,7 @@ test-sudo: ## Run tests requiring root privileges. test-last-failed: ## Re-run tests which failed on last run $(RUN_TEST) --last-failed $(ARGS) -test-coverage: ## Run test coverage. - # Note: coverage options are controlled by .coveragerc file +coverage: ## Run test coverage. rm -rf .coverage htmlcov $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest --ignore=tests/test_memleaks.py $(ARGS) $(PYTHON) -m coverage report diff --git a/setup.py b/setup.py index 63a7e9d935..eb03aafcff 100755 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ # Test deps, installable via `pip install .[test]` or # `make install-pydeps-test`. TEST_DEPS = [ + "psleak", "pytest", "pytest-instafail", "pytest-xdist", From 704e218db7da14e98a54f2aa9f93372d5900e0b4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 23 Dec 2025 21:26:14 +0100 Subject: [PATCH 1469/1714] Pre-release --- HISTORY.rst | 14 +++++++------- docs/index.rst | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ddd8d1a8b2..e30a77fc50 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,15 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.2.0 (IN DEVELOPMENT) -====================== +7.2.0 +===== -XXXX-XX-XX +2025-12-23 **Enhancements** - 1275_: new `heap_info()`_ and `heap_trim()`_ functions, providing direct - access to the platform's native C heap allocator. Useful to create tools to - detect memory leaks. + access to the platform's native C heap allocator (glibc, mimalloc, + libmalloc). Useful to create tools to detect memory leaks. - 2403_, [Linux]: publish wheels for Linux musl. - 2680_: unit tests are no longer installed / part of the distribution. They now live under `tests/` instead of `psutil/tests`. @@ -18,8 +18,8 @@ XXXX-XX-XX * 2684_, [FreeBSD], [critical]: compilation fails on FreeBSD 14 due to missing include. -* 2691_, [Windows]: fix memory leak in `net_if_stats()`_ memory leak due to - missing ``Py_CLEAR``. +* 2691_, [Windows]: fix memory leak in `net_if_stats()`_ due to missing + ``Py_CLEAR``. **Compatibility notes** diff --git a/docs/index.rst b/docs/index.rst index 234711a017..2c8ea02fc3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2753,7 +2753,7 @@ If you want to develop psutil take a look at the `DEVGUIDE.rst`_. Platforms support history ========================= -* psutil 7.2.0 (XXXX-XX): publish wheels for Linux musl +* psutil 7.2.0 (2025-12): publish wheels for Linux musl * psutil 7.1.2 (2025-10): publish wheels for free-threaded Python * psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (**Linux** and **Windows**) From 899ee4efa9c1943de14a5818853b6dc9c019eb4f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 24 Dec 2025 14:12:49 +0100 Subject: [PATCH 1470/1714] Mention psleak --- Makefile | 1 + README.rst | 3 +++ docs/index.rst | 10 +++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d75ec54fc6..a409773b8a 100644 --- a/Makefile +++ b/Makefile @@ -75,6 +75,7 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. $(MAKE) install-pip + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) setuptools PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` install-pydeps-dev: ## Install python deps meant for local development. diff --git a/README.rst b/README.rst index 19bd9287c7..10157cc9c1 100644 --- a/README.rst +++ b/README.rst @@ -464,6 +464,8 @@ Heap info pheap(heap_used=5177792, mmap_used=819200) >>> psutil.heap_trim() +See also `psleak `__ + Windows services ---------------- @@ -491,6 +493,7 @@ Projects using psutil Here's some I find particularly interesting: +- https://github.com/giampaolo/psleak - https://github.com/google/grr - https://github.com/facebook/osquery/ - https://github.com/nicolargo/glances diff --git a/docs/index.rst b/docs/index.rst index 2c8ea02fc3..d3fed1c96f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2149,6 +2149,9 @@ steadily across iterations, the C code is likely retaining memory it should be releasing. This provides an allocator-level way to spot native leaks that Python's memory tracking misses. +Checkout `psleak`_ project to see a practical example of how these APIs can be +used to detect memory leaks in C extensions. + .. function:: heap_info() Return low-level heap statistics from the system's C allocator. On Linux, @@ -2780,8 +2783,12 @@ PyPy3. Timeline ======== +- 2025-12-23: + `7.2.0 `__ - + `what's new `__ - + `diff `__ - 2025-11-02: - `7.1.3 `__ - + `7.1.3 `__ - `what's new `__ - `diff `__ - 2025-10-25: @@ -3225,6 +3232,7 @@ Timeline .. _`PROCESS_MEMORY_COUNTERS_EX`: https://docs.microsoft.com/en-us/windows/desktop/api/psapi/ns-psapi-_process_memory_counters_ex .. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py .. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`psleak`: https://github.com/giampaolo/psleak .. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit .. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit .. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py From 6130c19da2d01383befa0dfca2371a792f8881af Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 29 Dec 2025 08:45:54 +0100 Subject: [PATCH 1471/1714] Fix #2699 / BSD: flush internal jemalloc cache before returning metrics. This makes jemalloc detect allocations <= 1K. Originally emerged in psleak: https://github.com/giampaolo/psleak/issues/6 --- HISTORY.rst | 11 +++++++++++ psutil/__init__.py | 2 +- psutil/arch/bsd/heap.c | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index e30a77fc50..b4b743fbef 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,16 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.2.1 +===== + +2025-12-29 + +**Bug fixes** + +- 2699_, [FreeBSD], [NetBSD]: `heap_info()`_ does not detect small allocations + (<= 1K). In order to fix that, we now flush internal jemalloc cache before + fetching the metrics. + 7.2.0 ===== diff --git a/psutil/__init__.py b/psutil/__init__.py index d72cd18aa0..35298db766 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -204,7 +204,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.2.0" +__version__ = "7.2.1" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/arch/bsd/heap.c b/psutil/arch/bsd/heap.c index a3d03a5d19..fe7fc27d44 100644 --- a/psutil/arch/bsd/heap.c +++ b/psutil/arch/bsd/heap.c @@ -28,6 +28,14 @@ psutil_heap_info(PyObject *self, PyObject *args) { size_t sz_val; int ret; + // Flush per-thread tcache so small leaks become visible. + // Originates from https://github.com/giampaolo/psleak/issues/6. In + // there we had failures for small allocations, which disappeared + // after we added this. + ret = mallctl("thread.tcache.flush", NULL, NULL, NULL, 0); + if (ret != 0) + return psutil_oserror_wsyscall("mallctl('thread.tcache.flush')"); + // Read current epoch ret = mallctl("epoch", &epoch, &sz_epoch, NULL, 0); if (ret != 0) From 2cc496d659811e501c36dfb3aa5fc8ec1a5e4ace Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 30 Dec 2025 18:00:53 +0800 Subject: [PATCH 1472/1714] macOS: fix heap.c for < 10.7 (#2701) --- psutil/arch/osx/heap.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/psutil/arch/osx/heap.c b/psutil/arch/osx/heap.c index 95238ba31b..90c45cad02 100644 --- a/psutil/arch/osx/heap.c +++ b/psutil/arch/osx/heap.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "../../arch/all/init.h" @@ -96,8 +97,11 @@ psutil_heap_trim(PyObject *self, PyObject *args) { if (ok == -1) return NULL; +// malloc_zone_pressure_relief added in macOS 10.7. +#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 for (unsigned int i = 0; i < count; i++) malloc_zone_pressure_relief(zones[i], 0); +#endif if (!ok) free(zones); From d10d8498224fff153734d3988fe38560170841ff Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 30 Dec 2025 12:18:46 +0100 Subject: [PATCH 1473/1714] Give credits to @barracuda156 for @2694 --- .github/workflows/sunos.yml | 2 +- CREDITS | 5 +++++ HISTORY.rst | 10 ++++++++++ docs/index.rst | 4 ++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml index dd319ebd07..b399f31f25 100644 --- a/.github/workflows/sunos.yml +++ b/.github/workflows/sunos.yml @@ -34,5 +34,5 @@ jobs: python3 setup.py build_ext -i --parallel 4 python3 -c "import psutil" python3 scripts/internal/install_pip.py - python3 -m pip install --break-system-packages --user pytest pytest-instafail pytest-xdist + python3 -m pip install --break-system-packages --user pytest pytest-instafail pytest-xdist psleak python3 -m pytest tests/test_memleaks.py diff --git a/CREDITS b/CREDITS index 32bdac45b2..fdd1e755bc 100644 --- a/CREDITS +++ b/CREDITS @@ -872,3 +872,8 @@ I: 2604 N: Ben Raz W: https://github.com/ben9923 I: 2357 + +N: Sergey Fedorov +W: https://github.com/barracuda156 +C: Taiwan +I: 2694 diff --git a/HISTORY.rst b/HISTORY.rst index b4b743fbef..53491e57aa 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.2.2 (IN DEVELOPMENT) +====================== + +XXXX-XX + +**Bug fixes** + +- 2701_, [macOS]: fix compilation error on macOS < 10.7. (patch by Sergey + Fedorov) + 7.2.1 ===== diff --git a/docs/index.rst b/docs/index.rst index d3fed1c96f..990f9b5e41 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2783,6 +2783,10 @@ PyPy3. Timeline ======== +- 2025-12-29: + `7.2.1 `__ - + `what's new `__ - + `diff `__ - 2025-12-23: `7.2.0 `__ - `what's new `__ - From dd49bbe2271c4acbf24b75c89d1cae914744d6bf Mon Sep 17 00:00:00 2001 From: David Hotham Date: Tue, 30 Dec 2025 14:53:19 +0000 Subject: [PATCH 1474/1714] Static metadata (#2700) --- .github/workflows/sunos.yml | 2 +- Makefile | 6 +++--- setup.py | 17 +++++------------ 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml index b399f31f25..b58cd231b8 100644 --- a/.github/workflows/sunos.yml +++ b/.github/workflows/sunos.yml @@ -34,5 +34,5 @@ jobs: python3 setup.py build_ext -i --parallel 4 python3 -c "import psutil" python3 scripts/internal/install_pip.py - python3 -m pip install --break-system-packages --user pytest pytest-instafail pytest-xdist psleak + python3 -m pip install --break-system-packages --user .[test] python3 -m pytest tests/test_memleaks.py diff --git a/Makefile b/Makefile index a409773b8a..8fdee5efab 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PYTHON = python3 ARGS = -PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade +PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade --upgrade-strategy eager PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_TESTING=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 SUDO = $(if $(filter $(OS),Windows_NT),,sudo -E) DPRINT = ~/.dprint/bin/dprint @@ -76,12 +76,12 @@ install-sysdeps: install-pydeps-test: ## Install python deps necessary to run unit tests. $(MAKE) install-pip PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) setuptools - PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.TEST_DEPS))"` + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) .[test] install-pydeps-dev: ## Install python deps meant for local development. $(MAKE) install-git-hooks $(MAKE) install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) `$(PYTHON) -c "import setup; print(' '.join(setup.DEV_DEPS))"` + $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) .[dev] install-git-hooks: ## Install GIT pre-commit hook. ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit diff --git a/setup.py b/setup.py index eb03aafcff..9603b29ab0 100755 --- a/setup.py +++ b/setup.py @@ -82,13 +82,10 @@ "pytest-instafail", "pytest-xdist", "setuptools", + 'pywin32 ; os_name == "nt" and implementation_name != "pypy"', + 'wheel ; os_name == "nt" and implementation_name != "pypy"', + 'wmi ; os_name == "nt" and implementation_name != "pypy"', ] -if WINDOWS and not hasattr(sys, "pypy_version_info"): - TEST_DEPS.extend([ - "pywin32", - "wheel", - "wmi", - ]) # Development deps, installable via `pip install .[dev]` or # `make install-pydeps-dev`. @@ -113,14 +110,10 @@ "virtualenv", "vulture", "wheel", + 'colorama ; os_name == "nt"', + 'pyreadline3 ; os_name == "nt"', ] -if WINDOWS: - DEV_DEPS.extend([ - "colorama", - "pyreadline3", - ]) - # The pre-processor macros that are passed to the C compiler when # building the extension. macros = [] From 3a2fe4a5d43403da229437c137376e5a07902787 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 2 Jan 2026 12:05:03 +0100 Subject: [PATCH 1475/1714] Reorganize sponsors --- README.rst | 85 ++++++++++++++++++++++++++++++-------------------- docs/index.rst | 30 ++++-------------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/README.rst b/README.rst index 10157cc9c1..3cd5863e34 100644 --- a/README.rst +++ b/README.rst @@ -113,47 +113,21 @@ Sponsors .. raw:: html -
+
- +    - + + +    + +
- add your logo - -Supporters -========== - -.. raw:: html - -
- - - - - - - - - - - - - - - - -
- add your avatar - -Contributing -============ - -See `contributing guidelines `__. + add your logo Example usages ============== @@ -509,3 +483,46 @@ Portings - C: https://github.com/hamon-in/cpslib - Rust: https://github.com/rust-psutil/rust-psutil - Nim: https://github.com/johnscillieri/psutil-nim + + +Supporters +========== + +People who donated money over the years: + +.. raw:: html + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + add your avatar diff --git a/docs/index.rst b/docs/index.rst index 990f9b5e41..ec7db53978 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -64,39 +64,21 @@ Sponsors
- +    - + + +    + +

add your logo -Supporters ----------- - -.. raw:: html - -
- - - - - - - - - - - - -
-
- add your avatar - Install ======= From 95bb7d3bcd4cc8c7a586ac78f00b4424c3bed290 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 2 Jan 2026 14:57:46 +0100 Subject: [PATCH 1476/1714] Reformat README sponsors section --- README.rst | 32 +++++++++++++++---------- docs/index.rst | 38 ++++++++++++++++++------------ scripts/internal/convert_readme.py | 36 +++++++++++++++------------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/README.rst b/README.rst index 3cd5863e34..dfca029dae 100644 --- a/README.rst +++ b/README.rst @@ -113,19 +113,25 @@ Sponsors .. raw:: html -
- - - -    - - - -    - - - -
+ + + + + + +
+ + + + + + + + + + + +
add your logo diff --git a/docs/index.rst b/docs/index.rst index ec7db53978..2f535479b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -60,23 +60,31 @@ and have your logo displayed in here and psutil `doc - - - -    - - - -    - - - -
- -
+ + + + + + +
+ + + + + + + + + + + +
+ add your logo Install diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index c82c1a8ca6..fd19ef7fed 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -11,7 +11,7 @@ import argparse import re -summary = """\ +quick_links = """\ Quick links =========== @@ -25,28 +25,30 @@ - `What's new `_ """ -funding = """\ -Sponsors -======== - -.. image:: https://github.com/giampaolo/psutil/raw/master/docs/_static/tidelift-logo.png - :width: 200 - :alt: Alternative text - -`Add your logo `__. - -Example usages""" - def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('file', type=str) args = parser.parse_args() with open(args.file) as f: - data = f.read() - data = re.sub(r".. raw:: html\n+\s+
", summary, data) - data = re.sub(r"Sponsors\n========[\s\S]*?Example usages", funding, data) - print(data) + text = f.read() + + # Remove "Sponsors" section + pattern = re.compile( + r"^Sponsors\n=+\n.*?^Example usages\n=+", + re.DOTALL | re.MULTILINE, + ) + text = pattern.sub("Example usages\n==============", text) + + # Remove "Supporters" section + text = re.sub( + r"^Supporters\n=+\n[\s\S]*\Z", + "", + text, + flags=re.MULTILINE, + ) + + print(text) if __name__ == '__main__': From 77cfb0c6408055342fda77065088dbd5bd94ab49 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 6 Jan 2026 00:45:03 +0100 Subject: [PATCH 1477/1714] Update README.rst --- INSTALL.rst | 8 ++--- README.rst | 48 ++++++++++++++---------------- docs/index.rst | 31 ++++++++++--------- scripts/internal/convert_readme.py | 5 ++++ 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index ed7abf68f8..1619b7d341 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -61,17 +61,17 @@ Windows ------- - To build or install psutil from source on Windows, you need to have - **`Visual Studio 2017 `__** + `Visual Studio 2017 `__. or later installed. For detailed instructions, see the `CPython Developer Guide `__. -- **MinGW is not supported** for building psutil on Windows. +- MinGW is not supported for building psutil on Windows. - To build directly from the source tarball (.tar.gz) on PYPI, run:: pip install --no-binary :all: psutil - If you want to clone psutil's GIT repository and build / develop locally, - first install: **`Git for Windows `__** - and launch a **Git Bash shell**. This provides a Unix-like environment where + first install: `Git for Windows `__ + and launch a Git Bash shell. This provides a Unix-like environment where ``make`` works. - Once inside Git Bash, you can run the usual ``make`` commands:: diff --git a/README.rst b/README.rst index dfca029dae..742fc02d96 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -| |downloads| |stars| |forks| |contributors| |coverage| -| |version| |packages| |license| -| |github-actions-wheels| |github-actions-bsd| |doc| |twitter| |tidelift| +| |downloads| |stars| |forks| |contributors| |packages| +| |version| |license| |stackoverflow| |twitter| |tidelift| +| |github-actions-wheels| |github-actions-bsd| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://clickpy.clickhouse.com/dashboard/psutil @@ -18,6 +18,10 @@ :target: https://github.com/giampaolo/psutil/graphs/contributors :alt: Contributors +.. |stackoverflow| image:: https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg + :target: https://stackoverflow.com/questions/tagged/psutil + :alt: Stackoverflow + .. |github-actions-wheels| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml.svg?label=Linux%2C%20macOS%2C%20Windows :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild :alt: Linux, macOS, Windows @@ -26,14 +30,6 @@ :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests :alt: FreeBSD, NetBSD, OpenBSD -.. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master - :target: https://coveralls.io/github/giampaolo/psutil?branch=master - :alt: Test coverage (coverall.io) - -.. |doc| image:: https://readthedocs.org/projects/psutil/badge/?version=latest - :target: https://psutil.readthedocs.io/en/latest/ - :alt: Documentation Status - .. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi :target: https://pypi.org/project/psutil :alt: Latest version @@ -46,7 +42,7 @@ :target: https://github.com/giampaolo/psutil/blob/master/LICENSE :alt: License -.. |twitter| image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=flat&logo=twitter&logoColor=4FADFF +.. |twitter| image:: https://img.shields.io/twitter/follow/grodola?style=flat :target: https://twitter.com/grodola :alt: Twitter Follow @@ -95,19 +91,6 @@ Supported Python versions are cPython 3.6+ and `PyPy `__. Latest psutil version supporting Python 2.7 is `psutil 6.1.1 `__. -Funding -======= - -While psutil is free software and will always be, the project would benefit -immensely from some funding. -Keeping up with bug reports and maintenance has become hardly sustainable for -me alone in terms of time. -If you're a company that's making significant use of psutil you can consider -becoming a sponsor via `GitHub Sponsors `__, -`Open Collective `__ or -`PayPal `__ -and have your logo displayed in here and psutil `doc `__. - Sponsors ======== @@ -135,6 +118,21 @@ Sponsors add your logo +Funding +======= + +While psutil is free software and will always be, the project would benefit +immensely from some funding. +psutil is among the `top 100 `__ +most-downloaded Python packages, and keeping up with bug reports, user support, +and ongoing maintenance has become increasingly difficult to sustain as a +one-person effort. +If you're a company that's making significant use of psutil you can consider +becoming a sponsor via `GitHub `__, +`Open Collective `__ or +`PayPal `__. +Sponsors can have their logo displayed here and in the psutil `documentation `__. + Example usages ============== diff --git a/docs/index.rst b/docs/index.rst index 2f535479b4..8959d1021a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,22 +44,6 @@ Latest psutil version supporting Python 2.7 is The psutil documentation you're reading is distributed as a single HTML page. -Funding -======= - -While psutil is free software and will always be, the project would benefit -immensely from some funding. -Keeping up with bug reports and maintenance has become hardly sustainable for -me alone in terms of time. -If you're a company that's making significant use of psutil you can consider -becoming a sponsor via `GitHub `__, -`Open Collective `__ or -`PayPal `__ -and have your logo displayed in here and psutil `doc `__. - -Sponsors --------- - Sponsors ======== @@ -87,6 +71,21 @@ Sponsors add your logo +Funding +======= + +While psutil is free software and will always be, the project would benefit +immensely from some funding. +psutil is among the `top 100 `__ +most-downloaded Python packages, and keeping up with bug reports, user support, +and ongoing maintenance has become increasingly difficult to sustain as a +one-person effort. +If you're a company that's making significant use of psutil you can consider +becoming a sponsor via `GitHub `__, +`Open Collective `__ or +`PayPal `__. +Sponsors can have their logo displayed here and in the psutil `documentation `__. + Install ======= diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index fd19ef7fed..f909607487 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -33,6 +33,11 @@ def main(): with open(args.file) as f: text = f.read() + # Rewrite summary + text = re.sub( + r".. raw:: html\n+\s+
", quick_links, text + ) + # Remove "Sponsors" section pattern = re.compile( r"^Sponsors\n=+\n.*?^Example usages\n=+", From 9cc7c373bbf6d292da3998333b5c3f909f1d26da Mon Sep 17 00:00:00 2001 From: Julien Stephan Date: Mon, 12 Jan 2026 16:04:02 +0100 Subject: [PATCH 1478/1714] scripts: install-sysdeps: add procps-ng for RHEL-based distros (#2704) On Rocky Linux (RHEL-based distro), procps-ng is not installed by default. As a result, utilities like "free" and "vmstat" are missing, which causes several tests to fail. This commit ensures procps-ng is installed as part of system dependencies to prevent such failures. Signed-off-by: Julien Stephan --- scripts/internal/install-sysdeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 07ea0caf79..81547a1726 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -47,7 +47,7 @@ main() { $SUDO apt-get install -y net-tools coreutils util-linux sudo # for tests elif [ $HAS_YUM ]; then $SUDO yum install -y python3-devel gcc - $SUDO yum install -y net-tools coreutils-single util-linux sudo # for tests + $SUDO yum install -y net-tools coreutils-single util-linux sudo procps-ng # for tests elif [ $HAS_PACMAN ]; then $SUDO pacman -S --noconfirm python gcc $SUDO pacman -S --noconfirm net-tools coreutils util-linux sudo # for tests From 351518ac7bdd55a3245510af90cc4590a07d88e5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 14 Jan 2026 17:22:08 +0100 Subject: [PATCH 1479/1714] Update test_heap.py + set big MMAP_SIZE to avoid flaky failures --- .github/workflows/sunos.yml | 2 +- Makefile | 22 +++++++++++----------- pyproject.toml | 1 + tests/test_heap.py | 13 +++++++++---- tests/test_memleaks.py | 5 +++-- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml index b58cd231b8..0bc62f5a15 100644 --- a/.github/workflows/sunos.yml +++ b/.github/workflows/sunos.yml @@ -35,4 +35,4 @@ jobs: python3 -c "import psutil" python3 scripts/internal/install_pip.py python3 -m pip install --break-system-packages --user .[test] - python3 -m pytest tests/test_memleaks.py + PYTHONMALLOC=malloc PYTHONUNBUFFERED=1 python3 -m pytest tests/test_memleaks.py diff --git a/Makefile b/Makefile index 8fdee5efab..c1baeced4c 100644 --- a/Makefile +++ b/Makefile @@ -95,22 +95,22 @@ RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest test: ## Run all tests (except memleak tests). # To run a specific test do `make test ARGS=tests/test_process.py::TestProcess::test_cmdline` - $(RUN_TEST) --ignore=tests/test_memleaks.py $(ARGS) + $(RUN_TEST) $(ARGS) test-parallel: ## Run all tests (except memleak tests) in parallel. - $(RUN_TEST) --ignore=tests/test_memleaks.py -p xdist -n auto --dist loadgroup $(ARGS) + $(RUN_TEST) -p xdist -n auto --dist loadgroup $(ARGS) test-process: ## Run process-related tests. - $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_process.py or test_proc or test_pid or Process or pids or pid_exists" $(ARGS) + $(RUN_TEST) -k "test_process.py or test_proc or test_pid or Process or pids or pid_exists" $(ARGS) test-process-all: ## Run tests which iterate over all process PIDs. $(RUN_TEST) -k test_process_all.py $(ARGS) test-system: ## Run system-related API tests. - $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_system.py or test_sys or System or disk or sensors or net_io_counters or net_if_addrs or net_if_stats or users or pids or win_service_ or boot_time" $(ARGS) + $(RUN_TEST) -k "test_system.py or test_sys or System or disk or sensors or net_io_counters or net_if_addrs or net_if_stats or users or pids or win_service_ or boot_time" $(ARGS) test-misc: ## Run miscellaneous tests. - $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_misc.py or Misc" $(ARGS) + $(RUN_TEST) -k "test_misc.py or Misc" $(ARGS) test-scripts: ## Run scripts tests. $(RUN_TEST) tests/test_scripts.py $(ARGS) @@ -125,13 +125,13 @@ test-contracts: ## APIs sanity tests. $(RUN_TEST) tests/test_contracts.py $(ARGS) test-connections: ## Test psutil.net_connections() and Process.net_connections(). - $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_connections.py or net_" $(ARGS) + $(RUN_TEST) -k "test_connections.py or net_" $(ARGS) test-heap: ## Test psutil.heap_*() APIs. - $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_heap.py or heap_" $(ARGS) + $(RUN_TEST) -k "test_heap.py or heap_" $(ARGS) test-posix: ## POSIX specific tests. - $(RUN_TEST) --ignore=tests/test_memleaks.py -k "test_posix.py or posix_ or Posix" $(ARGS) + $(RUN_TEST) -k "test_posix.py or posix_ or Posix" $(ARGS) test-platform: ## Run specific platform tests only. $(RUN_TEST) tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) @@ -148,7 +148,7 @@ test-last-failed: ## Re-run tests which failed on last run coverage: ## Run test coverage. rm -rf .coverage htmlcov - $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest --ignore=tests/test_memleaks.py $(ARGS) + $(PYTHON_ENV_VARS) $(PYTHON) -m coverage run -m pytest $(ARGS) $(PYTHON) -m coverage report @echo "writing results to htmlcov/index.html" $(PYTHON) -m coverage html @@ -245,8 +245,8 @@ ci-test-cibuildwheel: ## Run CI tests for the built wheels. rm -rf .tests tests/__pycache__ mkdir -p .tests cp -r tests .tests/ - cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) $(PYTHON) -m pytest -k "not test_memleaks.py" - cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) PYTHONMALLOC=malloc $(PYTHON) -m pytest -k "test_memleaks.py" + cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) $(PYTHON) -m pytest + cd .tests/ && PYTHONPATH=$$(pwd) $(PYTHON_ENV_VARS) PYTHONMALLOC=malloc $(PYTHON) -m pytest -k test_memleaks.py ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit diff --git a/pyproject.toml b/pyproject.toml index d2a309a127..fba1d7e015 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ ignore = [ "RUF022", # `__all__` is not sorted "RUF028", # This suppression comment is invalid "RUF031", # [*] Avoid parentheses for tuples in subscripts + "RUF067", # `__init__` module should only contain docstrings and re-exports "S", # flake8-bandit "SIM102", # Use a single `if` statement instead of nested `if` statements "SIM105", # Use `contextlib.suppress(OSError)` instead of `try`-`except`-`pass` diff --git a/tests/test_heap.py b/tests/test_heap.py index f17728cf7e..b623697764 100755 --- a/tests/test_heap.py +++ b/tests/test_heap.py @@ -16,7 +16,7 @@ UNIX -- Small `malloc()` allocations (≤128KB on glibc) without `free()` +- Small `malloc()` allocations (≤ 128KB on glibc) without `free()` increase `heap_used`. - Large `malloc()` allocations without `free()` trigger `mmap()` and increase `mmap_used`. @@ -49,8 +49,13 @@ from . import PsutilTestCase from . import retry_on_failure -HEAP_SIZE = 64 * 1024 # 64 KiB -MMAP_SIZE = 10 * 1024 * 1024 # 10 MiB (large enough to trigger mmap()) +# Small allocation (64 KiB), below M_MMAP_THRESHOLD (128 KiB). +# Increases heap_used (uordblks) without triggering mmap(). +HEAP_SIZE = 64 * 1024 +# Large allocation (64 MiB), exceeds DEFAULT_MMAP_THRESHOLD_MAX (32 +# MiB). Forces malloc() to use mmap() internally and increases +# mmap_used (hblkhd). See `man mallopt`. +MMAP_SIZE = 64 * 1024 * 1024 # ===================================================================== @@ -222,7 +227,7 @@ def test_heap_used(self): assert_within_percent(mem3.heap_used, mem1.heap_used, percent=10) assert_within_percent(mem3.mmap_used, mem1.mmap_used, percent=10) - @pytest.mark.skipif(MACOS, reason="not supported") + @pytest.mark.skipif(MACOS, reason="not supported on MACOS") @retry_on_failure() def test_mmap_used(self): """Test that a large malloc allocation increases mmap_used. diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index 52b501b53e..bf42d276de 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -47,8 +47,9 @@ cext = psutil._psplatform.cext thisproc = psutil.Process() -# try to minimize flaky failures -MemoryLeakTestCase.retries = 30 + +MemoryLeakTestCase.retries = 30 # minimize false positives +MemoryLeakTestCase.verbosity = 1 TIMES = MemoryLeakTestCase.times FEW_TIMES = int(TIMES / 10) From 812ffcb07e2ef11f370513ecf443ea7273acf68f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 15 Jan 2026 01:45:31 +0100 Subject: [PATCH 1480/1714] Refactor wait_pid() and make it more readable --- psutil/_psposix.py | 82 +++++++++++++++++++++++-------------------- tests/test_process.py | 2 +- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index efdf1eb850..e01cbebd78 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -58,6 +58,34 @@ def negsig_to_enum(num): return num +def convert_exit_code(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) + if 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 + msg = f"unknown process exit status {status!r}" + raise ValueError(msg) + + def wait_pid( pid, timeout=None, @@ -82,24 +110,29 @@ def wait_pid( 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). + If timeout=None and process is still alive raise TimeoutExpired. + + If timeout=0 either return immediately or raise TimeoutExpired + (non-blocking). """ if pid <= 0: # see "man waitpid" msg = "can't wait for PID 0" raise ValueError(msg) + interval = 0.0001 flags = 0 + stop_at = None + if timeout is not None: flags |= os.WNOHANG - stop_at = _timer() + timeout + if timeout != 0: + stop_at = _timer() + timeout - def sleep(interval): + def sleep_or_timeout(interval): # Sleep for some time and return a new increased interval. - if timeout is not None: - if _timer() >= stop_at: - raise TimeoutExpired(timeout, pid=pid, name=proc_name) + if timeout == 0 or (stop_at is not None and _timer() >= stop_at): + raise TimeoutExpired(timeout, pid=pid, name=proc_name) _sleep(interval) return _min(interval * 2, 0.04) @@ -108,7 +141,7 @@ def sleep(interval): try: retpid, status = os.waitpid(pid, flags) except InterruptedError: - interval = sleep(interval) + interval = sleep_or_timeout(interval) except ChildProcessError: # This has two meanings: # - PID is not a child of os.getpid() in which case @@ -117,41 +150,14 @@ def sleep(interval): # In both cases we'll eventually return None as we # can't determine its exit status code. while _pid_exists(pid): - interval = sleep(interval) + interval = sleep_or_timeout(interval) return None else: if retpid == 0: # WNOHANG flag was used and PID is still running. - interval = sleep(interval) - continue - - 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 + interval = sleep_or_timeout(interval) else: - # Should never happen. - msg = f"unknown process exit status {status!r}" - raise ValueError(msg) + return convert_exit_code(status) def disk_usage(path): diff --git a/tests/test_process.py b/tests/test_process.py index 76cb8d949b..41ceabc407 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -206,7 +206,7 @@ def test_wait_timeout(self): p.wait(0.01) with pytest.raises(psutil.TimeoutExpired): p.wait(0) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="must be a positive integer"): p.wait(-1) def test_wait_timeout_nonblocking(self): From e75ea4d6a4e7dabb11a0d20047ba472dda9e7974 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 15 Jan 2026 01:50:53 +0100 Subject: [PATCH 1481/1714] wait_pid(): don't handle EINTR, since it's already done by os.waitpid() See: https://github.com/python/cpython/blob/69a9dd0187b/Modules/posixmod ule.c#L10645-L10649 --- psutil/_psposix.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index e01cbebd78..4e5d784c9a 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -59,6 +59,7 @@ def negsig_to_enum(num): def convert_exit_code(status): + """Convert a os.waitpid() status to an exit code.""" if os.WIFEXITED(status): # Process terminated normally by calling exit(3) or _exit(2), # or by returning from main(). The return value is the @@ -82,6 +83,8 @@ def convert_exit_code(status): # # with WCONTINUED flag. # interval = sleep(interval) # continue + + # Should never happen. msg = f"unknown process exit status {status!r}" raise ValueError(msg) @@ -140,8 +143,6 @@ def sleep_or_timeout(interval): while True: try: retpid, status = os.waitpid(pid, flags) - except InterruptedError: - interval = sleep_or_timeout(interval) except ChildProcessError: # This has two meanings: # - PID is not a child of os.getpid() in which case From 9c93c15bbba49749f4ef0143a094a094bf676d05 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 15 Jan 2026 03:03:02 +0100 Subject: [PATCH 1482/1714] Process.wait(): add more user error checks --- psutil/__init__.py | 23 +++++++++++++++++------ psutil/_psposix.py | 15 ++++++++------- tests/test_process.py | 12 ++++++++++-- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 35298db766..c38e851322 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1340,22 +1340,33 @@ def kill(self): self._proc.kill() def wait(self, timeout=None): - """Wait for process to terminate and, if process is a children + """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 + If the process is already terminated, immediately return None instead of raising NoSuchProcess. If *timeout* (in seconds) is specified and process is still - alive raise TimeoutExpired. + alive, raise TimeoutExpired. - To wait for multiple Process(es) use psutil.wait_procs(). + If *timeout=0* either return immediately or raise + TimeoutExpired (non-blocking). + + To wait for multiple Process objects use psutil.wait_procs(). """ - if timeout is not None and not timeout >= 0: - msg = "timeout must be a positive integer" + if self.pid == 0: + msg = "can't wait for PID 0" raise ValueError(msg) + if timeout is not None: + if not isinstance(timeout, (int, float)): + msg = f"timeout must be an int or float (got {type(timeout)})" + raise TypeError(msg) + if timeout < 0: + msg = f"timeout must be positive or zero (got {timeout})" + raise ValueError(msg) + if self._exitcode is not _SENTINEL: return self._exitcode self._exitcode = self._proc.wait(timeout) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 4e5d784c9a..cd6d8cce90 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -69,7 +69,7 @@ def convert_exit_code(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): + # if 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) @@ -78,7 +78,7 @@ def convert_exit_code(status): # # ignore SIGTERM. # interval = sleep(interval) # continue - # elif os.WIFCONTINUED(status): + # if os.WIFCONTINUED(status): # # Process was resumed via SIGCONT and waitpid() was called # # with WCONTINUED flag. # interval = sleep(interval) @@ -118,16 +118,17 @@ def wait_pid( If timeout=0 either return immediately or raise TimeoutExpired (non-blocking). """ - if pid <= 0: - # see "man waitpid" - msg = "can't wait for PID 0" - raise ValueError(msg) + # PID 0 passed to waitpid() waits for any child of the current + # process to change state. + assert pid > 0 interval = 0.0001 + max_interval = 0.04 flags = 0 stop_at = None if timeout is not None: + assert timeout >= 0 flags |= os.WNOHANG if timeout != 0: stop_at = _timer() + timeout @@ -137,7 +138,7 @@ def sleep_or_timeout(interval): if timeout == 0 or (stop_at is not None and _timer() >= stop_at): raise TimeoutExpired(timeout, pid=pid, name=proc_name) _sleep(interval) - return _min(interval * 2, 0.04) + return _min(interval * 2, max_interval) # See: https://linux.die.net/man/2/waitpid while True: diff --git a/tests/test_process.py b/tests/test_process.py index 41ceabc407..a585e1e8e5 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -206,8 +206,6 @@ def test_wait_timeout(self): p.wait(0.01) with pytest.raises(psutil.TimeoutExpired): p.wait(0) - with pytest.raises(ValueError, match="must be a positive integer"): - p.wait(-1) def test_wait_timeout_nonblocking(self): p = self.spawn_psproc() @@ -229,6 +227,16 @@ def test_wait_timeout_nonblocking(self): assert code == signal.SIGTERM self.assert_proc_gone(p) + def test_wait_errors(self): + p = psutil.Process() + with pytest.raises(TypeError, match="must be an int or float"): + p.wait("foo") + with pytest.raises(ValueError, match="must be positive or zero"): + p.wait(-1) + p._pid = 0 + with pytest.raises(ValueError, match="can't wait for PID 0"): + p.wait() + def test_cpu_percent(self): p = psutil.Process() p.cpu_percent(interval=0.001) From 81c49560acc26bce73856a8ce049341e0e5084ef Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 15 Jan 2026 20:40:25 +0100 Subject: [PATCH 1483/1714] Remove old waitpid() test re. to EINTR --- psutil/_psposix.py | 3 ++- tests/test_posix.py | 11 +---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index cd6d8cce90..012d401c43 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -113,7 +113,8 @@ def wait_pid( If PID does not exist at all return None immediately. - If timeout=None and process is still alive raise TimeoutExpired. + If timeout is specified and process is still alive raise + TimeoutExpired. If timeout=0 either return immediately or raise TimeoutExpired (non-blocking). diff --git a/tests/test_posix.py b/tests/test_posix.py index ab2c10c9f3..56483b7005 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -435,7 +435,7 @@ def test_pid_exists_let_raise(self): assert m.called def test_os_waitpid_let_raise(self): - # os.waitpid() is supposed to catch EINTR and ECHILD only. + # os.waitpid() is supposed to catch ECHILD only. # Test that any other errno results in an exception. with mock.patch( "psutil._psposix.os.waitpid", side_effect=OSError(errno.EBADF, "") @@ -444,15 +444,6 @@ def test_os_waitpid_let_raise(self): 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: - with pytest.raises(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( From 3692d8bdfca4e3fc86fe9f9cd5c91c221245c6c3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 16 Jan 2026 00:26:56 +0100 Subject: [PATCH 1484/1714] Add test for wait() on zombies --- tests/test_process.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_process.py b/tests/test_process.py index a585e1e8e5..0e561d9d37 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -237,6 +237,15 @@ def test_wait_errors(self): with pytest.raises(ValueError, match="can't wait for PID 0"): p.wait() + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_wait_zombie(self): + parent, zombie = self.spawn_zombie() + with pytest.raises(psutil.TimeoutExpired): + zombie.wait(0.001) + parent.terminate() + parent.wait() + zombie.wait() + def test_cpu_percent(self): p = psutil.Process() p.cpu_percent(interval=0.001) @@ -1376,11 +1385,14 @@ def assert_raises_nsp(fun, fun_name): @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process(self): - _parent, zombie = self.spawn_zombie() + parent, zombie = self.spawn_zombie() self.assert_proc_zombie(zombie) if hasattr(psutil._psplatform.cext, "proc_is_zombie"): assert not psutil._psplatform.cext.proc_is_zombie(os.getpid()) assert psutil._psplatform.cext.proc_is_zombie(zombie.pid) + parent.terminate() + parent.wait() + zombie.wait() @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process_is_running_w_exc(self): @@ -1459,7 +1471,7 @@ def test_pid_0(self): p = psutil.Process(0) exc = psutil.AccessDenied if WINDOWS else ValueError - with pytest.raises(exc): + with pytest.raises(ValueError): p.wait() with pytest.raises(exc): p.terminate() From 220fef39820247b8f3d4972941525f5062255637 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 16 Jan 2026 00:34:48 +0100 Subject: [PATCH 1485/1714] Dont pass proc_name to wait_pid() fun --- psutil/__init__.py | 8 +++++++- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 2 +- psutil/_pslinux.py | 2 +- psutil/_psosx.py | 2 +- psutil/_psposix.py | 3 +-- psutil/_pssunos.py | 2 +- 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index c38e851322..6e5821a855 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1369,7 +1369,13 @@ def wait(self, timeout=None): if self._exitcode is not _SENTINEL: return self._exitcode - self._exitcode = self._proc.wait(timeout) + + try: + self._exitcode = self._proc.wait(timeout) + except TimeoutExpired: + exc = TimeoutExpired(timeout, pid=self.pid, name=self._name) + raise exc from None + return self._exitcode diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 2dfeb7653f..1f628ad93c 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -529,7 +529,7 @@ def num_ctx_switches(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) if HAS_PROC_IO_COUNTERS: diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 172bf19499..a75cd1af13 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -794,7 +794,7 @@ def net_connections(self, kind='inet'): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions def nice_get(self): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index a98d351db3..dc37a8f236 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1843,7 +1843,7 @@ def cpu_num(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions def create_time(self, monotonic=False): diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 77fc65e94f..b14d5e023b 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -523,7 +523,7 @@ def num_fds(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions def nice_get(self): diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 012d401c43..5756268b60 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -92,7 +92,6 @@ def convert_exit_code(status): def wait_pid( pid, timeout=None, - proc_name=None, _waitpid=os.waitpid, _timer=getattr(time, 'monotonic', time.time), # noqa: B008 _min=min, @@ -137,7 +136,7 @@ def wait_pid( def sleep_or_timeout(interval): # Sleep for some time and return a new increased interval. if timeout == 0 or (stop_at is not None and _timer() >= stop_at): - raise TimeoutExpired(timeout, pid=pid, name=proc_name) + raise TimeoutExpired(timeout, pid=pid) _sleep(interval) return _min(interval * 2, max_interval) diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 89c6055c49..a0e70581f5 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -701,4 +701,4 @@ def num_ctx_switches(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) From 164dadd48931d1ed488872cdeaef7649094e443c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 16 Jan 2026 00:45:46 +0100 Subject: [PATCH 1486/1714] Avoid passing proc name to wait_pid(); re-raise TimeoutExpired instead --- psutil/__init__.py | 8 +++++++- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 2 +- psutil/_pslinux.py | 6 +++--- psutil/_psosx.py | 2 +- psutil/_psposix.py | 3 +-- psutil/_pssunos.py | 2 +- tests/test_process.py | 16 ++++++++++++++-- 8 files changed, 29 insertions(+), 12 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index c38e851322..9f0d35b0f1 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1369,7 +1369,13 @@ def wait(self, timeout=None): if self._exitcode is not _SENTINEL: return self._exitcode - self._exitcode = self._proc.wait(timeout) + + try: + self._exitcode = self._proc.wait(timeout) + except TimeoutExpired as err: + exc = TimeoutExpired(timeout, pid=self.pid, name=self._name) + raise exc from err + return self._exitcode diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 2dfeb7653f..1f628ad93c 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -529,7 +529,7 @@ def num_ctx_switches(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) if HAS_PROC_IO_COUNTERS: diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 172bf19499..a75cd1af13 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -794,7 +794,7 @@ def net_connections(self, kind='inet'): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions def nice_get(self): diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index a98d351db3..dc305b5955 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1582,8 +1582,8 @@ def ppid_map(): def wrap_exceptions(fun): - """Decorator which translates bare OSError and OSError exceptions - into NoSuchProcess and AccessDenied. + """Decorator which translates bare OSError exceptions into + NoSuchProcess and AccessDenied. """ @functools.wraps(fun) @@ -1843,7 +1843,7 @@ def cpu_num(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions def create_time(self, monotonic=False): diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 77fc65e94f..b14d5e023b 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -523,7 +523,7 @@ def num_fds(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions def nice_get(self): diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 012d401c43..5756268b60 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -92,7 +92,6 @@ def convert_exit_code(status): def wait_pid( pid, timeout=None, - proc_name=None, _waitpid=os.waitpid, _timer=getattr(time, 'monotonic', time.time), # noqa: B008 _min=min, @@ -137,7 +136,7 @@ def wait_pid( def sleep_or_timeout(interval): # Sleep for some time and return a new increased interval. if timeout == 0 or (stop_at is not None and _timer() >= stop_at): - raise TimeoutExpired(timeout, pid=pid, name=proc_name) + raise TimeoutExpired(timeout, pid=pid) _sleep(interval) return _min(interval * 2, max_interval) diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 89c6055c49..a0e70581f5 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -701,4 +701,4 @@ def num_ctx_switches(self): @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) diff --git a/tests/test_process.py b/tests/test_process.py index a585e1e8e5..0e561d9d37 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -237,6 +237,15 @@ def test_wait_errors(self): with pytest.raises(ValueError, match="can't wait for PID 0"): p.wait() + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_wait_zombie(self): + parent, zombie = self.spawn_zombie() + with pytest.raises(psutil.TimeoutExpired): + zombie.wait(0.001) + parent.terminate() + parent.wait() + zombie.wait() + def test_cpu_percent(self): p = psutil.Process() p.cpu_percent(interval=0.001) @@ -1376,11 +1385,14 @@ def assert_raises_nsp(fun, fun_name): @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process(self): - _parent, zombie = self.spawn_zombie() + parent, zombie = self.spawn_zombie() self.assert_proc_zombie(zombie) if hasattr(psutil._psplatform.cext, "proc_is_zombie"): assert not psutil._psplatform.cext.proc_is_zombie(os.getpid()) assert psutil._psplatform.cext.proc_is_zombie(zombie.pid) + parent.terminate() + parent.wait() + zombie.wait() @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_zombie_process_is_running_w_exc(self): @@ -1459,7 +1471,7 @@ def test_pid_0(self): p = psutil.Process(0) exc = psutil.AccessDenied if WINDOWS else ValueError - with pytest.raises(exc): + with pytest.raises(ValueError): p.wait() with pytest.raises(exc): p.terminate() From 629fef089373fbf58548645f0fcbf2e701e27537 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 16 Jan 2026 13:15:47 +0100 Subject: [PATCH 1487/1714] Move wait() tests --- tests/test_contracts.py | 11 -- tests/test_posix.py | 19 --- tests/test_process.py | 284 +++++++++++++++++++++++----------------- 3 files changed, 161 insertions(+), 153 deletions(-) diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 3097f45b7a..e60529134e 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -10,7 +10,6 @@ """ import platform -import signal import psutil from psutil import AIX @@ -326,13 +325,3 @@ def test_users(self): assert isinstance(user.pid, (int, type(None))) if isinstance(user.pid, int): assert user.pid > 0 - - -class TestProcessWaitType(PsutilTestCase): - @pytest.mark.skipif(not POSIX, reason="not POSIX") - def test_negative_signal(self): - p = psutil.Process(self.spawn_subproc().pid) - p.terminate() - code = p.wait() - assert code == -signal.SIGTERM - assert isinstance(code, enum.IntEnum) diff --git a/tests/test_posix.py b/tests/test_posix.py index 56483b7005..881a89b6c0 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -434,25 +434,6 @@ def test_pid_exists_let_raise(self): psutil._psposix.pid_exists(os.getpid()) assert m.called - def test_os_waitpid_let_raise(self): - # os.waitpid() is supposed to catch 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: - with pytest.raises(OSError): - psutil._psposix.wait_pid(os.getpid()) - 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: - with pytest.raises(ValueError): - psutil._psposix.wait_pid(os.getpid()) - assert m.called - # AIX can return '-' in df output instead of numbers, e.g. for /proc @pytest.mark.skipif(AIX, reason="unreliable on AIX") @retry_on_failure() diff --git a/tests/test_process.py b/tests/test_process.py index 0e561d9d37..f2863e8947 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -8,6 +8,7 @@ import collections import contextlib +import enum import errno import getpass import io @@ -123,129 +124,6 @@ def test_send_signal_mocked(self): with pytest.raises(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() - assert code == 0 - self.assert_proc_gone(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() - assert code == 1 - self.assert_proc_gone(p) - # via sys.exit() - cmd = [PYTHON_EXE, "-c", "import sys; sys.exit(5);"] - p = self.spawn_psproc(cmd) - code = p.wait() - assert code == 5 - self.assert_proc_gone(p) - # via os._exit() - cmd = [PYTHON_EXE, "-c", "import os; os._exit(5);"] - p = self.spawn_psproc(cmd) - code = p.wait() - assert code == 5 - self.assert_proc_gone(p) - - @pytest.mark.skipif(NETBSD, reason="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) - with pytest.raises(psutil.TimeoutExpired): - p.wait(timeout=0.001) - p.send_signal(signal.SIGCONT) - with pytest.raises(psutil.TimeoutExpired): - p.wait(timeout=0.001) - p.send_signal(signal.SIGTERM) - assert p.wait() == -signal.SIGTERM - assert p.wait() == -signal.SIGTERM - else: - p.suspend() - with pytest.raises(psutil.TimeoutExpired): - p.wait(timeout=0.001) - p.resume() - with pytest.raises(psutil.TimeoutExpired): - p.wait(timeout=0.001) - p.terminate() - assert p.wait() == signal.SIGTERM - assert 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() - with pytest.raises(psutil.TimeoutExpired): - child.wait(0.01) - with pytest.raises(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: - assert child_ret == -signal.SIGTERM - # For processes which are not our children we're supposed - # to get None. - assert grandchild_ret is None - else: - assert child_ret == signal.SIGTERM - assert child_ret == signal.SIGTERM - - def test_wait_timeout(self): - p = self.spawn_psproc() - p.name() - with pytest.raises(psutil.TimeoutExpired): - p.wait(0.01) - with pytest.raises(psutil.TimeoutExpired): - p.wait(0) - - def test_wait_timeout_nonblocking(self): - p = self.spawn_psproc() - with pytest.raises(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: - return pytest.fail('timeout') - if POSIX: - assert code == -signal.SIGKILL - else: - assert code == signal.SIGTERM - self.assert_proc_gone(p) - - def test_wait_errors(self): - p = psutil.Process() - with pytest.raises(TypeError, match="must be an int or float"): - p.wait("foo") - with pytest.raises(ValueError, match="must be positive or zero"): - p.wait(-1) - p._pid = 0 - with pytest.raises(ValueError, match="can't wait for PID 0"): - p.wait() - - @pytest.mark.skipif(not POSIX, reason="POSIX only") - def test_wait_zombie(self): - parent, zombie = self.spawn_zombie() - with pytest.raises(psutil.TimeoutExpired): - zombie.wait(0.001) - parent.terminate() - parent.wait() - zombie.wait() - def test_cpu_percent(self): p = psutil.Process() p.cpu_percent(interval=0.001) @@ -1580,6 +1458,166 @@ def test_weird_environ(self): assert sproc.returncode == 0 +# =================================================================== +# --- psutil.Process.wait tests +# =================================================================== + + +class TestProcessWait(PsutilTestCase): + + 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() + assert code == 0 + self.assert_proc_gone(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() + assert code == 1 + self.assert_proc_gone(p) + # via sys.exit() + cmd = [PYTHON_EXE, "-c", "import sys; sys.exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 5 + self.assert_proc_gone(p) + # via os._exit() + cmd = [PYTHON_EXE, "-c", "import os; os._exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 5 + self.assert_proc_gone(p) + + @pytest.mark.skipif(not POSIX, reason="not POSIX") + def test_wait_signaled(self): + p = psutil.Process(self.spawn_subproc().pid) + p.terminate() + code = p.wait() + assert code == -signal.SIGTERM + assert isinstance(code, enum.IntEnum) + + @pytest.mark.skipif(NETBSD, reason="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) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.send_signal(signal.SIGCONT) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.send_signal(signal.SIGTERM) + assert p.wait() == -signal.SIGTERM + assert p.wait() == -signal.SIGTERM + else: + p.suspend() + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.resume() + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.terminate() + assert p.wait() == signal.SIGTERM + assert 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() + with pytest.raises(psutil.TimeoutExpired): + child.wait(0.01) + with pytest.raises(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: + assert child_ret == -signal.SIGTERM + # For processes which are not our children we're supposed + # to get None. + assert grandchild_ret is None + else: + assert child_ret == signal.SIGTERM + assert child_ret == signal.SIGTERM + + def test_wait_timeout(self): + p = self.spawn_psproc() + p.name() + with pytest.raises(psutil.TimeoutExpired): + p.wait(0.01) + with pytest.raises(psutil.TimeoutExpired): + p.wait(0) + + def test_wait_timeout_nonblocking(self): + p = self.spawn_psproc() + with pytest.raises(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: + return pytest.fail('timeout') + if POSIX: + assert code == -signal.SIGKILL + else: + assert code == signal.SIGTERM + self.assert_proc_gone(p) + + def test_wait_errors(self): + p = psutil.Process() + with pytest.raises(TypeError, match="must be an int or float"): + p.wait("foo") + with pytest.raises(ValueError, match="must be positive or zero"): + p.wait(-1) + p._pid = 0 + with pytest.raises(ValueError, match="can't wait for PID 0"): + p.wait() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_wait_zombie(self): + parent, zombie = self.spawn_zombie() + with pytest.raises(psutil.TimeoutExpired): + zombie.wait(0.001) + parent.terminate() + parent.wait() + zombie.wait() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_os_waitpid_let_raise(self): + # os.waitpid() is supposed to catch 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: + with pytest.raises(OSError): + psutil._psposix.wait_pid(os.getpid()) + assert m.called + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + 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: + with pytest.raises(ValueError): + psutil._psposix.wait_pid(os.getpid()) + assert m.called + + # =================================================================== # --- psutil.Popen tests # =================================================================== From d7777d13e772a2463500039f7a09cd175b177869 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 16 Jan 2026 18:53:44 +0100 Subject: [PATCH 1488/1714] CI: run all jobs also in case of failure --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a644a5b989..fc3e1d0b5d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,7 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: + fail-fast: false matrix: include: - { os: ubuntu-latest, arch: x86_64 } From 77d0a4aed331d53e0683b4b941efd0f85f99ff8d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Jan 2026 02:55:19 +0100 Subject: [PATCH 1489/1714] Eliminate busy-loop in `Process.wait()` on Linux and BSD (#2706) Linux: Process.wait() now uses pidfd_open() + poll() instead of busy-loop polling. Requires Linux >= 5.3, Python >= 3.9. macOS/BSD: Process.wait() now uses kqueue() instead of busy-loop polling. Both implementations fall back to traditional busy loop polling if not available or on certain errors. --- .github/workflows/bsd.yml | 7 +- HISTORY.rst | 9 +++ docs/index.rst | 4 ++ psutil/_psposix.py | 139 ++++++++++++++++++++++++++++++++++++-- tests/test_process.py | 137 +++++++++++++++++++++++++++++++++++-- 5 files changed, 281 insertions(+), 15 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 036bf581b3..d3cb85212a 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -9,15 +9,20 @@ on: # only run this job if the following files are modified paths: &bsd_paths - ".github/workflows/bsd.yml" + - "psutil/__init__.py" + - "psutil/_common.py" + - "psutil/_ntuples.py" - "psutil/_psbsd.py" + - "psutil/_psposix.py" - "psutil/_psutil_bsd.c" - "psutil/arch/bsd/**" - "psutil/arch/freebsd/**" - "psutil/arch/netbsd/**" - "psutil/arch/openbsd/**" - "psutil/arch/posix/**" - - "tests/test_bsd.py" - "setup.py" + - "tests/test_bsd.py" + - "tests/test_posix.py" pull_request: paths: *bsd_paths name: bsd diff --git a/HISTORY.rst b/HISTORY.rst index 53491e57aa..0842fab070 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,15 @@ XXXX-XX +**Enhancements** + +- 2705_: [Linux]: `Process.wait()`_ now uses ``pidfd_open()`` + ``poll()`` for + waiting, resulting in no busy loop and faster response times. Requires + Linux >= 5.3 and Python >= 3.9. Falls back to traditional polling if + unavailable. +- 2705_: [macOS], [BSD]: `Process.wait()`_ now uses ``kqueue()`` for waiting, + resulting in no busy loop and faster response times. + **Bug fixes** - 2701_, [macOS]: fix compilation error on macOS < 10.7. (patch by Sergey diff --git a/docs/index.rst b/docs/index.rst index 8959d1021a..6ae2a0d577 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2093,6 +2093,10 @@ Process class .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it as a human readable `enum`_. + .. versionchanged:: 7.2.2 on Linux (>= 5.3, Python >= 3.9) and macOS/BSD, + use ``pidfd_open()`` and ``kqueue()`` respectively, instead of less + efficient busy-loop polling. + .. class:: Popen(*args, **kwargs) Same as `subprocess.Popen`_ but in addition it provides all diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 5756268b60..39f15d2adb 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -5,14 +5,17 @@ """Routines common to all posix systems.""" import enum +import errno import glob import os +import select import signal import time from . import _ntuples as ntp from ._common import MACOS from ._common import TimeoutExpired +from ._common import debug from ._common import memoize from ._common import usage_percent @@ -89,7 +92,7 @@ def convert_exit_code(status): raise ValueError(msg) -def wait_pid( +def wait_pid_posix( pid, timeout=None, _waitpid=os.waitpid, @@ -118,17 +121,12 @@ def wait_pid( If timeout=0 either return immediately or raise TimeoutExpired (non-blocking). """ - # PID 0 passed to waitpid() waits for any child of the current - # process to change state. - assert pid > 0 - interval = 0.0001 max_interval = 0.04 flags = 0 stop_at = None if timeout is not None: - assert timeout >= 0 flags |= os.WNOHANG if timeout != 0: stop_at = _timer() + timeout @@ -136,7 +134,7 @@ def wait_pid( def sleep_or_timeout(interval): # Sleep for some time and return a new increased interval. if timeout == 0 or (stop_at is not None and _timer() >= stop_at): - raise TimeoutExpired(timeout, pid=pid) + raise TimeoutExpired(timeout) _sleep(interval) return _min(interval * 2, max_interval) @@ -162,6 +160,133 @@ def sleep_or_timeout(interval): return convert_exit_code(status) +def _waitpid(pid, timeout): + """Wrapper around os.waitpid(). PID is supposed to be gone already, + it just returns the exit code. + """ + try: + retpid, status = os.waitpid(pid, 0) + except ChildProcessError: + # PID is not a child of os.getpid(). + return wait_pid_posix(pid, timeout) + else: + assert retpid != 0 + return convert_exit_code(status) + + +def wait_pid_pidfd_open(pid, timeout=None): + """Wait for PID to terminate using pidfd_open() + poll(). Linux >= + 5.3 + Python >= 3.9 only. + """ + try: + pidfd = os.pidfd_open(pid, 0) + except OSError as err: + if err.errno == errno.ESRCH: + # No such process. os.waitpid() may still be able to return + # the status code. + return wait_pid_posix(pid, timeout) + if err.errno in {errno.EMFILE, errno.ENFILE, errno.ENODEV}: + # EMFILE, ENFILE: too many open files + # ENODEV: anonymous inode filesystem not supported + debug(f"pidfd_open() failed ({err!r}); use fallback") + return wait_pid_posix(pid, timeout) + raise + + try: + # poll() / select() have the advantage of not requiring any + # extra file descriptor, contrary to epoll() / kqueue(). + # select() crashes if process opens > 1024 FDs, so we use + # poll(). + poller = select.poll() + poller.register(pidfd, select.POLLIN) + timeout_ms = None if timeout is None else int(timeout * 1000) + events = poller.poll(timeout_ms) # wait + + if not events: + raise TimeoutExpired(timeout) + return _waitpid(pid, timeout) + finally: + os.close(pidfd) + + +def wait_pid_kqueue(pid, timeout=None): + """Wait for PID to terminate using kqueue(). macOS and BSD only.""" + try: + kq = select.kqueue() + except OSError as err: + if err.errno in {errno.EMFILE, errno.ENFILE}: # too many open files + debug(f"kqueue() failed ({err!r}); use fallback") + return wait_pid_posix(pid, timeout) + raise + + try: + kev = select.kevent( + pid, + filter=select.KQ_FILTER_PROC, + flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT, + fflags=select.KQ_NOTE_EXIT, + ) + try: + events = kq.control([kev], 1, timeout) # wait + except OSError as err: + if err.errno in {errno.EACCES, errno.EPERM, errno.ESRCH}: + debug(f"kqueue.control() failed ({err!r}); use fallback") + return wait_pid_posix(pid, timeout) + raise + else: + if not events: + raise TimeoutExpired(timeout) + return _waitpid(pid, timeout) + finally: + kq.close() + + +@memoize +def can_use_pidfd(): + # Availability: Linux >= 5.3, Python >= 3.9 + if not hasattr(os, "pidfd_open"): + return False + try: + pidfd = os.pidfd_open(os.getpid(), 0) + except OSError as err: + # blocked by security policy like SECCOMP + debug(f"can't use pidfd_open() due to {err}") + return False + else: + os.close(pidfd) + return True + + +@memoize +def can_use_kqueue(): + names = ( + "kqueue", + "KQ_EV_ADD", + "KQ_EV_ONESHOT", + "KQ_FILTER_PROC", + "KQ_NOTE_EXIT", + ) + return all(hasattr(select, x) for x in names) + + +def wait_pid(pid, timeout=None): + # PID 0 passed to waitpid() waits for any child of the current + # process to change state. + assert pid > 0 + if timeout is not None: + assert timeout >= 0 + + if can_use_pidfd(): + return wait_pid_pidfd_open(pid, timeout) + elif can_use_kqueue(): + return wait_pid_kqueue(pid, timeout) + else: + return wait_pid_posix(pid, timeout) + + +wait_pid.__doc__ = wait_pid_posix.__doc__ + + def disk_usage(path): """Return disk usage associated with path. Note: UNIX usually reserves 5% disk space which is not accessible diff --git a/tests/test_process.py b/tests/test_process.py index f2863e8947..e6d1fe1db0 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -14,6 +14,8 @@ import io import itertools import os +import random +import select import signal import socket import stat @@ -1499,6 +1501,12 @@ def test_wait_signaled(self): code = p.wait() assert code == -signal.SIGTERM assert isinstance(code, enum.IntEnum) + # second call is cached + assert code == -signal.SIGTERM + # Call the underlying implementation. Done to exercise the + # poll/kqueue machinery on a gone PID. Also, waitpid() is + # supposed to fail with ESRCH. + assert p._proc.wait() is None @pytest.mark.skipif(NETBSD, reason="fails on NETBSD") def test_wait_stopped(self): @@ -1596,27 +1604,142 @@ def test_wait_zombie(self): parent.wait() zombie.wait() + # --- tests for wait_pid_posix() + @pytest.mark.skipif(not POSIX, reason="POSIX only") - def test_os_waitpid_let_raise(self): + def test_os_waitpid_error(self): # os.waitpid() is supposed to catch ECHILD only. # Test that any other errno results in an exception. with mock.patch( - "psutil._psposix.os.waitpid", side_effect=OSError(errno.EBADF, "") + "os.waitpid", side_effect=OSError(errno.EBADF, "") ) as m: with pytest.raises(OSError): - psutil._psposix.wait_pid(os.getpid()) + psutil._psposix.wait_pid_posix(os.getpid()) assert m.called @pytest.mark.skipif(not POSIX, reason="POSIX only") 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: + with mock.patch("os.waitpid", return_value=(1, -1)) as m: with pytest.raises(ValueError): - psutil._psposix.wait_pid(os.getpid()) + psutil._psposix.wait_pid_posix(os.getpid()) assert m.called + # --- tests for pidfd_open() and kqueue() + + def assert_wait_pid_errors(self, patch_target, wait_func, errors): + # Test that legitimate errors are caught and wait_pid_posix() + # fallback is used. + sproc = self.spawn_subproc() + sproc.terminate() + + errors = list(errors) + random.shuffle(errors) + for idx, err in enumerate(errors): + with mock.patch( + patch_target, + side_effect=OSError(err, os.strerror(err)), + ) as m: + # the second time waitpid() does not return the exit code + code = -signal.SIGTERM if idx == 0 else None + assert wait_func(sproc.pid) == code + assert m.called + + # illegitimate error + with mock.patch( + patch_target, + side_effect=OSError(errno.EBADF), + ): + with pytest.raises(OSError): + wait_func(sproc.pid) + + @pytest.mark.skipif( + not hasattr(os, "pidfd_open"), + reason="LINUX only" if not LINUX else "not supported", + ) + def test_pidfd_open_errors(self): + from psutil._psposix import wait_pid_pidfd_open + + self.assert_wait_pid_errors( + "os.pidfd_open", + wait_pid_pidfd_open, + [errno.ESRCH, errno.EMFILE, errno.ENFILE, errno.ENODEV], + ) + + @pytest.mark.skipif( + not hasattr(select, "kqueue"), reason="MACOS and BSD only" + ) + def test_kqueue_errors(self): + from psutil._psposix import wait_pid_kqueue + + self.assert_wait_pid_errors( + "select.kqueue", + wait_pid_kqueue, + [errno.EMFILE, errno.ENFILE], + ) + + def assert_wait_pid_race(self, patch_target, real_func): + # Kill process after patch_target succeeds but before wait() + # completes, then verify Process.wait() still works. + sproc = self.spawn_subproc() + psproc = psutil.Process(sproc.pid) + + def wrapper(*args, **kwargs): + ret = real_func(*args, **kwargs) + sproc.terminate() + sproc.wait() + return ret + + with mock.patch(patch_target, side_effect=wrapper) as m: + psproc.wait() + assert m.called + + @pytest.mark.skipif( + not hasattr(os, "pidfd_open"), + reason="LINUX only" if not LINUX else "not supported", + ) + def test_pidfd_open_race(self): + self.assert_wait_pid_race("os.pidfd_open", os.pidfd_open) + + @pytest.mark.skipif( + not hasattr(select, "kqueue"), reason="MACOS and BSD only" + ) + def test_kqueue_race(self): + self.assert_wait_pid_race("select.kqueue", select.kqueue) + + @pytest.mark.skipif( + not hasattr(select, "kqueue"), reason="MACOS and BSD only" + ) + def test_kqueue_control_errors(self): + # Test that kqueue.control() errors are caught and fallback is used. + from psutil._psposix import wait_pid_kqueue + + sproc = self.spawn_subproc() + sproc.terminate() + + errors = [errno.EACCES, errno.EPERM, errno.ESRCH] + random.shuffle(errors) + for idx, err in enumerate(errors): + kq_mock = mock.Mock() + kq_mock.control.side_effect = OSError(err, os.strerror(err)) + kq_mock.close = mock.Mock() + + with mock.patch("select.kqueue", return_value=kq_mock): + # the second time waitpid() does not return the exit code + code = -signal.SIGTERM if idx == 0 else None + assert wait_pid_kqueue(sproc.pid) == code + assert kq_mock.control.called + + # illegitimate error + kq_mock = mock.Mock() + kq_mock.control.side_effect = OSError( + errno.EBADF, os.strerror(errno.EBADF) + ) + kq_mock.close = mock.Mock() + with mock.patch("select.kqueue", return_value=kq_mock): + with pytest.raises(OSError): + wait_pid_kqueue(sproc.pid) + # =================================================================== # --- psutil.Popen tests From d7ce072aef56138167b4cb9e84ede860980ea1ae Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Jan 2026 12:42:23 +0100 Subject: [PATCH 1490/1714] Windows / username(): fix potential leak in case GetTokenInformation fails --- psutil/arch/windows/proc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index ff22340fbf..a2d56d63b1 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -686,6 +686,7 @@ _psutil_user_token_from_pid(DWORD pid) { { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { free(userToken); + userToken = NULL; continue; } else { @@ -701,6 +702,8 @@ _psutil_user_token_from_pid(DWORD pid) { return userToken; error: + if (userToken != NULL) + free(userToken); if (hProcess != NULL) CloseHandle(hProcess); if (hToken != NULL) From 7cc7923a1479fb1d32bb25d3e78b465e2695fbf7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Jan 2026 12:45:38 +0100 Subject: [PATCH 1491/1714] Windows / cmdline(): be more defensive in free()ing in case of error --- psutil/arch/windows/proc_info.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 6dca9cd2ee..601355aa8d 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -533,6 +533,8 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { error: if (buffer != NULL) free(buffer); + if (bufWchar != NULL) + free(bufWchar); if (hProcess != NULL) CloseHandle(hProcess); return -1; From 700b7e6a4171ae7c775679217205f1d97568ae00 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 17 Jan 2026 13:19:35 +0100 Subject: [PATCH 1492/1714] [macOS] fix potential leaks in error paths (#2707) ...affecting Process memory_full_info() and threads() --- HISTORY.rst | 2 ++ psutil/arch/osx/proc.c | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 0842fab070..d31dc46f2a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,8 @@ XXXX-XX - 2701_, [macOS]: fix compilation error on macOS < 10.7. (patch by Sergey Fedorov) +- 2707_, [macOS]: fix potential memory leaks in error paths of + `Process.memory_full_info()` and `Process.threads()`. 7.2.1 ===== diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 758f5ed15d..41b2849046 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -291,6 +291,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { if (psutil_sysctlbyname("sysctl.proc_cputype", &cpu_type, sizeof(cpu_type)) != 0) { + mach_port_deallocate(mach_task_self(), task); return NULL; } @@ -299,6 +300,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { for (addr = MACH_VM_MIN_ADDRESS;; addr += size) { prev_addr = addr; info_count = VM_REGION_TOP_INFO_COUNT; // reset before each call + object_name = MACH_PORT_NULL; kr = mach_vm_region( task, @@ -309,6 +311,12 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { &info_count, &object_name ); + + if (object_name != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), object_name); + object_name = MACH_PORT_NULL; + } + if (kr == KERN_INVALID_ADDRESS) { // Done iterating VM regions. break; @@ -439,15 +447,20 @@ psutil_proc_threads(PyObject *self, PyObject *args) { Py_CLEAR(py_tuple); } + // deallocate thread_list if it was allocated if (thread_list != NULL) { vm_deallocate( mach_task_self(), (vm_address_t)thread_list, thread_count * sizeof(thread_act_t) ); + thread_list = NULL; } + + // deallocate the task port if (task != MACH_PORT_NULL) { mach_port_deallocate(mach_task_self(), task); + task = MACH_PORT_NULL; } return py_retlist; @@ -462,9 +475,12 @@ psutil_proc_threads(PyObject *self, PyObject *args) { (vm_address_t)thread_list, thread_count * sizeof(thread_act_t) ); + thread_list = NULL; } + if (task != MACH_PORT_NULL) { mach_port_deallocate(mach_task_self(), task); + task = MACH_PORT_NULL; } return NULL; From 8b98c3effc20dcc4ed08c8f3d33e11ec0f5445b1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 18 Jan 2026 18:16:18 +0100 Subject: [PATCH 1493/1714] Pre-release --- HISTORY.rst | 6 +++--- docs/index.rst | 4 ++++ psutil/__init__.py | 7 ++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d31dc46f2a..f1bfc600d0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,9 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.2.2 (IN DEVELOPMENT) -====================== +7.2.2 +===== -XXXX-XX +2026-01-18 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index 6ae2a0d577..84dc2130c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2776,6 +2776,10 @@ PyPy3. Timeline ======== +- 2026-01-18: + `7.2.2 `__ - + `what's new `__ - + `diff `__ - 2025-12-29: `7.2.1 `__ - `what's new `__ - diff --git a/psutil/__init__.py b/psutil/__init__.py index 9f0d35b0f1..880e1a9ddf 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -204,7 +204,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.2.1" +__version__ = "7.2.2" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) @@ -1620,11 +1620,12 @@ def check_gone(proc, timeout): if timeout is not None and not timeout >= 0: msg = f"timeout must be a positive integer, got {timeout}" raise ValueError(msg) - gone = set() - alive = set(procs) if callback is not None and not callable(callback): msg = f"callback {callback!r} is not a callable" raise TypeError(msg) + + gone = set() + alive = set(procs) if timeout is not None: deadline = _timer() + timeout From a571717d6520d436273c8cb34ca871db4bfa508a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 20 Jan 2026 13:10:04 +0100 Subject: [PATCH 1494/1714] #2708, macos / cmdline / environ; raise AD instead of OSError(0) (#2709) The problem originates from sysctl(KERN_PROCARGS2) (macOS bug) --- .dprint.jsonc | 6 +++--- HISTORY.rst | 3 +++ psutil/_psposix.py | 1 + psutil/arch/osx/proc_utils.c | 6 ++++++ pyproject.toml | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.dprint.jsonc b/.dprint.jsonc index 99ab830286..538d4c3229 100644 --- a/.dprint.jsonc +++ b/.dprint.jsonc @@ -24,8 +24,8 @@ ".github/PULL_REQUEST_TEMPLATE.md", ], "plugins": [ - "https://plugins.dprint.dev/markdown-0.19.0.wasm", - "https://plugins.dprint.dev/json-0.20.0.wasm", - "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm", + "https://plugins.dprint.dev/markdown-0.20.0.wasm", + "https://plugins.dprint.dev/json-0.21.1.wasm", + "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm", ], } diff --git a/HISTORY.rst b/HISTORY.rst index f1bfc600d0..7111cd0dbb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,9 @@ Fedorov) - 2707_, [macOS]: fix potential memory leaks in error paths of `Process.memory_full_info()` and `Process.threads()`. +- 2708_, [macOS]: Process.cmdline()`_ and `Process.environ()`_ may fail with + ``OSError: [Errno 0] Undefined error`` (from ``sysctl(KERN_PROCARGS2)``). + They now raise `AccessDenied`_ instead. 7.2.1 ===== diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 39f15d2adb..2b1dd0062b 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -259,6 +259,7 @@ def can_use_pidfd(): @memoize def can_use_kqueue(): + # Availability: macOS, BSD names = ( "kqueue", "KQ_EV_ADD", diff --git a/psutil/arch/osx/proc_utils.c b/psutil/arch/osx/proc_utils.c index 93e5e91f37..16673e4dbb 100644 --- a/psutil/arch/osx/proc_utils.c +++ b/psutil/arch/osx/proc_utils.c @@ -97,6 +97,12 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> EIO"); return -1; } + if (errno == 0) { + // see: https://github.com/giampaolo/psutil/issues/2708 + psutil_debug("sysctl(KERN_PROCARGS2) -> errno 0"); + psutil_oserror_ad("sysctl(KERN_PROCARGS2) -> errno 0"); + return 0; + } psutil_oserror_wsyscall("sysctl(KERN_PROCARGS2)"); return -1; } diff --git a/pyproject.toml b/pyproject.toml index fba1d7e015..6d7f56b53c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ line-length = 79 skip-string-normalization = true # https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html preview = true -enable-unstable-feature = ["hug_parens_with_braces_and_square_brackets", "multiline_string_handling", "string_processing", "wrap_long_dict_values_in_parens"] +enable-unstable-feature = ["hug_parens_with_braces_and_square_brackets", "string_processing", "wrap_long_dict_values_in_parens"] [tool.ruff] # https://beta.ruff.rs/docs/settings/ From bb30943b0336a16f28437ec549c15a8ad2830cca Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 20 Jan 2026 17:00:08 +0100 Subject: [PATCH 1495/1714] Refact can_use_pidfd_open() and can_use_kqueue() --- psutil/_psposix.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 2b1dd0062b..c7ad2fdabd 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -242,15 +242,18 @@ def wait_pid_kqueue(pid, timeout=None): @memoize -def can_use_pidfd(): +def can_use_pidfd_open(): # Availability: Linux >= 5.3, Python >= 3.9 if not hasattr(os, "pidfd_open"): return False try: pidfd = os.pidfd_open(os.getpid(), 0) except OSError as err: - # blocked by security policy like SECCOMP - debug(f"can't use pidfd_open() due to {err}") + if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103 + # transitory 'too many open files' + return True + # likely blocked by security policy like SECCOMP (EPERM, + # EACCES, ENOSYS) return False else: os.close(pidfd) @@ -267,7 +270,27 @@ def can_use_kqueue(): "KQ_FILTER_PROC", "KQ_NOTE_EXIT", ) - return all(hasattr(select, x) for x in names) + if not all(hasattr(select, x) for x in names): + return False + kq = None + try: + kq = select.kqueue() + kev = select.kevent( + os.getpid(), + filter=select.KQ_FILTER_PROC, + flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT, + fflags=select.KQ_NOTE_EXIT, + ) + kq.control([kev], 1, 0) + return True + except OSError as err: + if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103 + # transitory 'too many open files' + return True + return False + finally: + if kq is not None: + kq.close() def wait_pid(pid, timeout=None): @@ -277,7 +300,7 @@ def wait_pid(pid, timeout=None): if timeout is not None: assert timeout >= 0 - if can_use_pidfd(): + if can_use_pidfd_open(): return wait_pid_pidfd_open(pid, timeout) elif can_use_kqueue(): return wait_pid_kqueue(pid, timeout) From de1cafa56f54e97ca557993155e76dd98877e136 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 22 Jan 2026 01:56:22 +0100 Subject: [PATCH 1496/1714] Update doc mentioning Process.wait() internal details --- docs/index.rst | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 84dc2130c8..a186426be2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2087,14 +2087,31 @@ Process class >>> p.wait() + .. note:: + + When ``timeout`` is not ``None`` and the platform supports it, an + efficient event-driven mechanism is used to wait for process termination: + + - Linux >= 5.3 with Python >= 3.9 uses `os.pidfd_open`_ + `select.poll`_ + - macOS and other BSD variants use `select.kqueue`_ + ``KQ_FILTER_PROC`` + + ``KQ_NOTE_EXIT`` + - Windows uses ``WaitForSingleObject`` + + If none of these mechanisms are available, the function falls back to a + busy loop (non-blocking call and short sleeps). + + .. versionchanged:: 3.15 + if *timeout* is not ``None``, use efficient event-driven implementation + on Linux >= 5.3 and macOS / BSD. + .. versionchanged:: 5.7.1 return value is cached (instead of returning ``None``). .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it as a human readable `enum`_. - .. versionchanged:: 7.2.2 on Linux (>= 5.3, Python >= 3.9) and macOS/BSD, - use ``pidfd_open()`` and ``kqueue()`` respectively, instead of less + .. versionchanged:: 7.2.2 on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, + use `os.pidfd_open`_ and `select.kqueue`_ respectively, instead of less efficient busy-loop polling. .. class:: Popen(*args, **kwargs) @@ -3227,6 +3244,7 @@ Timeline .. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY .. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC .. _`os.open`: https://docs.python.org/3/library/os.html#os.open +.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open .. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority .. _`os.times`: https://docs.python.org//library/os.html#os.times .. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py @@ -3236,6 +3254,8 @@ Timeline .. _`psleak`: https://github.com/giampaolo/psleak .. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit .. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit +.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue +.. _`select.poll`: https://docs.python.org//library/select.html#select.poll .. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py .. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. .. _`SetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-setpriorityclass From 76eaf9ae0f2868569c14b46f4165310885a40a15 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 22 Jan 2026 03:34:27 +0100 Subject: [PATCH 1497/1714] Try to add google analytics to doc --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index fd0fe294aa..7596db5428 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,6 +63,7 @@ def get_version(): 'sphinx.ext.imgmath', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', + 'sphinxcontrib.googleanalytics', ] # Add any paths that contain templates here, relative to this directory. @@ -375,3 +376,6 @@ def get_version(): 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', 'css/custom.css', ] + +googleanalytics_id = "G-G7374TFB11" +googleanalytics_enabled = True From 9dcbb7e60e650f0ab0cb52154b0a12c70f6a1e4c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 22 Jan 2026 03:36:13 +0100 Subject: [PATCH 1498/1714] Add sphinxcontrib-googleanalytics to requirements.txt --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 82133027c9..bd8714de8e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ sphinx sphinx_rtd_theme +sphinxcontrib-googleanalytics From 938ac647418f09e4e610b2c755741316713c5592 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 22 Jan 2026 03:50:28 +0100 Subject: [PATCH 1499/1714] Rm sphinxcontrib.googleanalytics; override layout.html --- MANIFEST.in | 1 + docs/_templates/layout.html | 12 ++++++++++++ docs/conf.py | 4 ---- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 docs/_templates/layout.html diff --git a/MANIFEST.in b/MANIFEST.in index 04d553cf99..89483408ca 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,6 +20,7 @@ include docs/_static/copybutton.js include docs/_static/css/custom.css include docs/_static/favicon.ico include docs/_static/sidebar.js +include docs/_templates/layout.html include docs/conf.py include docs/index.rst include docs/make.bat diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000000..1f595cbd46 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,12 @@ +{% extends "!layout.html" %} +{% block extrahead %} +{{ super() }} + + + +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 7596db5428..fd0fe294aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,7 +63,6 @@ def get_version(): 'sphinx.ext.imgmath', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', - 'sphinxcontrib.googleanalytics', ] # Add any paths that contain templates here, relative to this directory. @@ -376,6 +375,3 @@ def get_version(): 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', 'css/custom.css', ] - -googleanalytics_id = "G-G7374TFB11" -googleanalytics_enabled = True From 9eea97dd6f1d16ea33f5144c8925f1ce7a0688e1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 28 Jan 2026 19:14:48 +0100 Subject: [PATCH 1500/1714] Pre-release --- HISTORY.rst | 2 +- docs/index.rst | 2 +- psutil/__init__.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7111cd0dbb..5e68c723cf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 7.2.2 ===== -2026-01-18 +2026-01-28 **Enhancements** diff --git a/docs/index.rst b/docs/index.rst index a186426be2..1653c017dc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2793,7 +2793,7 @@ PyPy3. Timeline ======== -- 2026-01-18: +- 2026-01-28: `7.2.2 `__ - `what's new `__ - `diff `__ diff --git a/psutil/__init__.py b/psutil/__init__.py index 880e1a9ddf..f10598e9bc 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -149,8 +149,7 @@ "STATUS_RUNNING", "STATUS_IDLE", "STATUS_SLEEPING", "STATUS_DISK_SLEEP", "STATUS_STOPPED", "STATUS_TRACING_STOP", "STATUS_ZOMBIE", "STATUS_DEAD", - "STATUS_WAKING", "STATUS_LOCKED", "STATUS_WAITING", "STATUS_LOCKED", - "STATUS_PARKED", + "STATUS_WAKING", "STATUS_LOCKED", "STATUS_WAITING", "STATUS_PARKED", "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", From 3fe3c1c423fd6f8fb0cb6973fcdb673076d7948f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 28 Jan 2026 21:46:28 +0100 Subject: [PATCH 1501/1714] Add @codingjoe / Johannes Maron to list of supporters --- README.rst | 1 + docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 742fc02d96..068d68a617 100644 --- a/README.rst +++ b/README.rst @@ -497,6 +497,7 @@ People who donated money over the years: .. raw:: html
+ diff --git a/docs/index.rst b/docs/index.rst index 1653c017dc..a8a8cd3ad3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2100,7 +2100,7 @@ Process class If none of these mechanisms are available, the function falls back to a busy loop (non-blocking call and short sleeps). - .. versionchanged:: 3.15 + .. versionchanged:: 5.7.2 if *timeout* is not ``None``, use efficient event-driven implementation on Linux >= 5.3 and macOS / BSD. From 0195d11ed0f6106ff1371ff379bb47454130d770 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 7 Feb 2026 14:20:26 +0100 Subject: [PATCH 1502/1714] Fix #2715: catch all pidfd_open() errors on Linux --- HISTORY.rst | 10 ++++++++++ psutil/_psposix.py | 21 +++++++-------------- tests/test_process.py | 14 +++----------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5e68c723cf..275572e22c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,15 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.2.3 +===== + +XXXX-XX-XX + +**Bug fixes** + +- 2715_, [Linux]: ``wait_pid_pidfd_open()`` (from `Process.wait()`_) crashes + with ``EINVAL`` due to kernel race condition. + 7.2.2 ===== diff --git a/psutil/_psposix.py b/psutil/_psposix.py index c7ad2fdabd..11250424f9 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -181,16 +181,10 @@ def wait_pid_pidfd_open(pid, timeout=None): try: pidfd = os.pidfd_open(pid, 0) except OSError as err: - if err.errno == errno.ESRCH: - # No such process. os.waitpid() may still be able to return - # the status code. - return wait_pid_posix(pid, timeout) - if err.errno in {errno.EMFILE, errno.ENFILE, errno.ENODEV}: - # EMFILE, ENFILE: too many open files - # ENODEV: anonymous inode filesystem not supported - debug(f"pidfd_open() failed ({err!r}); use fallback") - return wait_pid_posix(pid, timeout) - raise + # ESRCH = no such process, EMFILE / ENFILE = too many open files + if err.errno not in {errno.ESRCH, errno.EMFILE, errno.ENFILE}: + debug(f"pidfd_open() failed unexpectedly ({err!r}); use fallback") + return wait_pid_posix(pid, timeout) try: # poll() / select() have the advantage of not requiring any @@ -214,10 +208,9 @@ def wait_pid_kqueue(pid, timeout=None): try: kq = select.kqueue() except OSError as err: - if err.errno in {errno.EMFILE, errno.ENFILE}: # too many open files - debug(f"kqueue() failed ({err!r}); use fallback") - return wait_pid_posix(pid, timeout) - raise + if err.errno not in {errno.EMFILE, errno.ENFILE}: + debug(f"kqueue() failed unexpectedly ({err!r}); use fallback") + return wait_pid_posix(pid, timeout) try: kev = select.kevent( diff --git a/tests/test_process.py b/tests/test_process.py index e6d1fe1db0..252a2e0b66 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1628,7 +1628,7 @@ def test_os_waitpid_bad_ret_status(self): # --- tests for pidfd_open() and kqueue() def assert_wait_pid_errors(self, patch_target, wait_func, errors): - # Test that legitimate errors are caught and wait_pid_posix() + # Test that all errors are caught and wait_pid_posix() # fallback is used. sproc = self.spawn_subproc() sproc.terminate() @@ -1645,14 +1645,6 @@ def assert_wait_pid_errors(self, patch_target, wait_func, errors): assert wait_func(sproc.pid) == code assert m.called - # illegitimate error - with mock.patch( - patch_target, - side_effect=OSError(errno.EBADF), - ): - with pytest.raises(OSError): - wait_func(sproc.pid) - @pytest.mark.skipif( not hasattr(os, "pidfd_open"), reason="LINUX only" if not LINUX else "not supported", @@ -1663,7 +1655,7 @@ def test_pidfd_open_errors(self): self.assert_wait_pid_errors( "os.pidfd_open", wait_pid_pidfd_open, - [errno.ESRCH, errno.EMFILE, errno.ENFILE, errno.ENODEV], + [errno.ESRCH, errno.EMFILE, errno.ENFILE, errno.EBADF], ) @pytest.mark.skipif( @@ -1675,7 +1667,7 @@ def test_kqueue_errors(self): self.assert_wait_pid_errors( "select.kqueue", wait_pid_kqueue, - [errno.EMFILE, errno.ENFILE], + [errno.EMFILE, errno.ENFILE, errno.EBADF], ) def assert_wait_pid_race(self, patch_target, real_func): From 36f1e18d2b2dc2c0aeefb7e117e3f413c978639b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 8 Feb 2026 15:08:58 +0100 Subject: [PATCH 1503/1714] Pre-release --- HISTORY.rst | 2 +- docs/index.rst | 6 +++++- psutil/__init__.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 275572e22c..5fcce93483 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ 7.2.3 ===== -XXXX-XX-XX +2026-02-08 **Bug fixes** diff --git a/docs/index.rst b/docs/index.rst index a8a8cd3ad3..06619ee9d1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2793,9 +2793,13 @@ PyPy3. Timeline ======== +- 2026-02-08: + `7.2.3 `__ - + `what's new `__ - + `diff `__ - 2026-01-28: `7.2.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-12-29: `7.2.1 `__ - diff --git a/psutil/__init__.py b/psutil/__init__.py index f10598e9bc..9ebbdfb91c 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -203,7 +203,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.2.2" +__version__ = "7.2.3" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) From 475373c6a52bb8932f46fac0a2b0230c7dcddb89 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 9 Feb 2026 15:48:55 +0100 Subject: [PATCH 1504/1714] Move things around --- .dprint.jsonc | 2 +- Makefile | 43 ++++++++++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/.dprint.jsonc b/.dprint.jsonc index 538d4c3229..f4afe95463 100644 --- a/.dprint.jsonc +++ b/.dprint.jsonc @@ -24,7 +24,7 @@ ".github/PULL_REQUEST_TEMPLATE.md", ], "plugins": [ - "https://plugins.dprint.dev/markdown-0.20.0.wasm", + "https://plugins.dprint.dev/markdown-0.21.1.wasm", "https://plugins.dprint.dev/json-0.21.1.wasm", "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm", ], diff --git a/Makefile b/Makefile index c1baeced4c..7993a9691a 100644 --- a/Makefile +++ b/Makefile @@ -250,7 +250,7 @@ ci-test-cibuildwheel: ## Run CI tests for the built wheels. ci-check-dist: ## Run all sanity checks re. to the package distribution. $(PYTHON) -m pip install -U setuptools virtualenv twine check-manifest validate-pyproject[all] abi3audit - $(MAKE) sdist + $(MAKE) create-sdist mv wheelhouse/* dist/ $(MAKE) check-dist $(MAKE) install @@ -260,6 +260,28 @@ ci-check-dist: ## Run all sanity checks re. to the package distribution. # Distribution # =================================================================== +# --- create + +generate-manifest: ## Generates MANIFEST.in file. + $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in + +create-sdist: ## Create tar.gz source distribution. + $(MAKE) generate-manifest + $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist + +create-wheels: ## Create .whl files + $(PYTHON_ENV_VARS) $(PYTHON) setup.py bdist_wheel + +download-wheels: ## Download latest wheels hosted on github. + $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token + $(MAKE) print-dist + +create-dist: ## Create .tar.gz + .whl distribution. + $(MAKE) create-sdist + $(MAKE) download-wheels + +# --- check + check-manifest: ## Check sanity of MANIFEST.in file. $(PYTHON) -m check_manifest -v @@ -282,19 +304,12 @@ check-dist: ## Run all sanity checks re. to the package distribution. $(MAKE) check-sdist $(MAKE) check-wheels -generate-manifest: ## Generates MANIFEST.in file. - $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in - -sdist: ## Create tar.gz source distribution. - $(MAKE) generate-manifest - $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist - -create-wheels: ## Create .whl files - $(PYTHON_ENV_VARS) $(PYTHON) setup.py bdist_wheel +# --- release pre-release: ## Check if we're ready to produce a new release. $(MAKE) clean - $(MAKE) sdist + $(MAKE) create-dist + $(MAKE) check-dist $(MAKE) install @$(PYTHON) -c \ "import requests, sys; \ @@ -310,8 +325,6 @@ pre-release: ## Check if we're ready to produce a new release. assert ver in doc, '%r not found in docs/index.rst' % ver; \ assert ver in history, '%r not found in HISTORY.rst' % ver; \ assert 'XXXX' not in history, 'XXXX found in HISTORY.rst';" - $(MAKE) download-wheels - $(MAKE) check-dist $(MAKE) print-hashes $(MAKE) print-dist @@ -320,10 +333,6 @@ release: ## Upload a new release. $(PYTHON) -m twine upload dist/*.whl $(MAKE) git-tag-release -download-wheels: ## Download latest wheels hosted on github. - $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token - $(MAKE) print-dist - git-tag-release: ## Git-tag a new release. git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` git push --follow-tags From fa7d4a7d7d6dc8e5db68598653d59d2480e090cb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 12 Feb 2026 12:47:32 +0100 Subject: [PATCH 1505/1714] Try to avoid .github/workflows/issues.yml crashing for external PRs (#2719) --- .github/workflows/issues.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 3ce6b4957e..2cfec564d3 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -4,26 +4,25 @@ name: issues on: issues: types: [opened] - pull_request: - typed: [opened] + pull_request_target: + types: [opened] issue_comment: types: [created] +permissions: + issues: write + pull-requests: write jobs: build: runs-on: ubuntu-latest steps: - # install python - - uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v6 with: python-version: "3.x" - # install deps - name: Install deps run: python3 -m pip install PyGithub - # run - name: Run env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From ad1214684dc8a13fb0b92725e5693223c83fc426 Mon Sep 17 00:00:00 2001 From: Konstantin Baikov Date: Thu, 12 Feb 2026 17:46:23 +0000 Subject: [PATCH 1506/1714] Re-enable tests after MacOs cpu_freq 1892 was fixed (#2717) --- tests/__init__.py | 5 +---- tests/test_contracts.py | 6 +++--- tests/test_memleaks.py | 3 --- tests/test_osx.py | 7 +++++-- tests/test_system.py | 2 -- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 91e8faf333..df80ecf8e7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1251,10 +1251,7 @@ class system_namespace: ] if HAS_CPU_FREQ: - if MACOS and AARCH64: # skipped due to #1892 - pass - else: - getters += [('cpu_freq', (), {'percpu': True})] + getters += [('cpu_freq', (), {'percpu': True})] if HAS_GETLOADAVG: getters += [('getloadavg', (), {})] if HAS_SENSORS_TEMPERATURES: diff --git a/tests/test_contracts.py b/tests/test_contracts.py index e60529134e..844132e9d3 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -111,7 +111,9 @@ def test_win_service_iter(self): def test_win_service_get(self): assert hasattr(psutil, "win_service_get") == WINDOWS - @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") + @pytest.mark.skipif( + MACOS and AARCH64 and not HAS_CPU_FREQ, reason="not supported" + ) def test_cpu_freq(self): assert hasattr(psutil, "cpu_freq") == ( LINUX or MACOS or WINDOWS or FREEBSD or OPENBSD @@ -237,8 +239,6 @@ def test_cpu_times_percent(self): def test_cpu_count(self): assert isinstance(psutil.cpu_count(), int) - # TODO: remove this once 1892 is fixed - @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): if psutil.cpu_freq() is None: diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index bf42d276de..2e44d81119 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -21,7 +21,6 @@ from psutil import SUNOS from psutil import WINDOWS -from . import AARCH64 from . import HAS_CPU_AFFINITY from . import HAS_CPU_FREQ from . import HAS_ENVIRON @@ -334,8 +333,6 @@ def test_per_cpu_times(self): def test_cpu_stats(self): self.execute(psutil.cpu_stats) - # TODO: remove this once 1892 is fixed - @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): times = FEW_TIMES if LINUX else self.times diff --git a/tests/test_osx.py b/tests/test_osx.py index a5bb79ce90..e7a6604943 100755 --- a/tests/test_osx.py +++ b/tests/test_osx.py @@ -15,6 +15,7 @@ from . import AARCH64 from . import CI_TESTING from . import HAS_BATTERY +from . import HAS_CPU_FREQ from . import TOLERANCE_DISK_USAGE from . import TOLERANCE_SYS_MEM from . import PsutilTestCase @@ -113,8 +114,10 @@ def test_cpu_count_cores(self): num = sysctl("sysctl hw.physicalcpu") assert num == psutil.cpu_count(logical=False) - # TODO: remove this once 1892 is fixed - @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") + @pytest.mark.skipif( + MACOS and AARCH64 and not HAS_CPU_FREQ, + reason="not available on MACOS + AARCH64", + ) def test_cpu_freq(self): freq = psutil.cpu_freq() assert freq.current * 1000 * 1000 == sysctl("sysctl hw.cpufrequency") diff --git a/tests/test_system.py b/tests/test_system.py index f40bdbf832..410230b8bc 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -594,8 +594,6 @@ def test_cpu_stats(self): if not AIX and name in {'ctx_switches', 'interrupts'}: assert value > 0 - # TODO: remove this once 1892 is fixed - @pytest.mark.skipif(MACOS and AARCH64, reason="skipped due to #1892") @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") def test_cpu_freq(self): def check_ls(ls): From 4e503f854c17dda05eed63323a03111bf81e98c3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Feb 2026 16:25:49 +0100 Subject: [PATCH 1507/1714] get_testfn(): avoid unnecessary while loop --- tests/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index df80ecf8e7..f53708508a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -870,12 +870,10 @@ def get_testfn(suffix="", dir=None): 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 + name = tempfile.mktemp(prefix=TESTFN_PREFIX, suffix=suffix, dir=dir) + path = os.path.realpath(name) # needed for OSX + atexit.register(safe_rmpath, path) + return path # =================================================================== From e5c4bb0a5b6f90fd72731c00d7fdb271cbdd1fd6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Feb 2026 12:06:31 +0100 Subject: [PATCH 1508/1714] Rename some HAS_* constants from test support module --- pyproject.toml | 2 +- tests/__init__.py | 45 ++++++++++++++++++++++-------------------- tests/test_linux.py | 4 ++-- tests/test_memleaks.py | 32 +++++++++++++++--------------- tests/test_process.py | 44 ++++++++++++++++++++--------------------- tests/test_scripts.py | 4 ++-- tests/test_unicode.py | 8 ++++---- 7 files changed, 71 insertions(+), 68 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6d7f56b53c..a2876e15f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,7 @@ ignore = [ # T203 == pprint() # TRY003 == raise-vanilla-args ".github/workflows/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] -"tests/*" = ["B904", "EM101", "EM102", "EM103", "PLC1901", "TRY003"] +"tests/*" = ["B904", "EM101", "EM102", "EM103", "PLC1901", "RUF069", "TRY003"] "tests/test_sudo.py" = ["PT009"] "scripts/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] "scripts/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] diff --git a/tests/__init__.py b/tests/__init__.py index f53708508a..54df554921 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -64,11 +64,12 @@ '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", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", - "MACOS_12PLUS", "COVERAGE", 'AARCH64', "PYTEST_PARALLEL", + "HAS_PROC_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_PROC_ENVIRON", + "HAS_PROC_IO_COUNTERS", "HAS_PROC_IONICE", "HAS_PROC_MEMORY_MAPS", + "HAS_PROC_CPU_NUM", "HAS_PROC_RLIMIT", "HAS_SENSORS_BATTERY", + "HAS_BATTERY", "HAS_SENSORS_FANS", "HAS_SENSORS_TEMPERATURES", + "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", "MACOS_12PLUS", "COVERAGE", + "AARCH64", "PYTEST_PARALLEL", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_subproc', 'spawn_zombie', 'spawn_children_pair', @@ -180,21 +181,23 @@ def macos_version(): # --- support -HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") -HAS_ENVIRON = hasattr(psutil.Process, "environ") HAS_GETLOADAVG = hasattr(psutil, "getloadavg") -HAS_IONICE = hasattr(psutil.Process, "ionice") HAS_HEAP_INFO = hasattr(psutil, "heap_info") -HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") HAS_NET_CONNECTIONS_UNIX = POSIX and not SUNOS 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") HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") -HAS_THREADS = hasattr(psutil.Process, "threads") + +HAS_PROC_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") +HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") +HAS_PROC_ENVIRON = hasattr(psutil.Process, "environ") +HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") +HAS_PROC_IONICE = hasattr(psutil.Process, "ionice") +HAS_PROC_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") +HAS_PROC_RLIMIT = hasattr(psutil.Process, "rlimit") +HAS_PROC_THREADS = hasattr(psutil.Process, "threads") + SKIP_SYSCONS = (MACOS or AIX) and os.getuid() != 0 try: @@ -1131,19 +1134,19 @@ class process_namespace: getters += [('num_fds', (), {})] if HAS_PROC_IO_COUNTERS: getters += [('io_counters', (), {})] - if HAS_IONICE: + if HAS_PROC_IONICE: getters += [('ionice', (), {})] - if HAS_RLIMIT: + if HAS_PROC_RLIMIT: getters += [('rlimit', (psutil.RLIMIT_NOFILE,), {})] - if HAS_CPU_AFFINITY: + if HAS_PROC_CPU_AFFINITY: getters += [('cpu_affinity', (), {})] if HAS_PROC_CPU_NUM: getters += [('cpu_num', (), {})] - if HAS_ENVIRON: + if HAS_PROC_ENVIRON: getters += [('environ', (), {})] if WINDOWS: getters += [('num_handles', (), {})] - if HAS_MEMORY_MAPS: + if HAS_PROC_MEMORY_MAPS: getters += [('memory_maps', (), {'grouped': False})] setters = [] @@ -1151,14 +1154,14 @@ class process_namespace: setters += [('nice', (0,), {})] else: setters += [('nice', (psutil.NORMAL_PRIORITY_CLASS,), {})] - if HAS_RLIMIT: + if HAS_PROC_RLIMIT: setters += [('rlimit', (psutil.RLIMIT_NOFILE, (1024, 4096)), {})] - if HAS_IONICE: + if HAS_PROC_IONICE: if LINUX: setters += [('ionice', (psutil.IOPRIO_CLASS_NONE, 0), {})] else: setters += [('ionice', (psutil.IOPRIO_NORMAL,), {})] - if HAS_CPU_AFFINITY: + if HAS_PROC_CPU_AFFINITY: setters += [('cpu_affinity', ([_get_eligible_cpu()],), {})] killers = [ diff --git a/tests/test_linux.py b/tests/test_linux.py index 33417e5062..db73815e5d 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -30,7 +30,7 @@ from . import HAS_BATTERY from . import HAS_CPU_FREQ from . import HAS_GETLOADAVG -from . import HAS_RLIMIT +from . import HAS_PROC_RLIMIT from . import RISCV64 from . import TOLERANCE_DISK_USAGE from . import TOLERANCE_SYS_MEM @@ -2075,7 +2075,7 @@ def test_issue_2418(self): with pytest.raises(psutil.NoSuchProcess): p.memory_info() - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit_zombie(self): # Emulate a case where rlimit() raises ENOSYS, which may # happen in case of zombie process: diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index 2e44d81119..ef72b1a84c 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -21,16 +21,16 @@ from psutil import SUNOS from psutil import WINDOWS -from . import HAS_CPU_AFFINITY from . import HAS_CPU_FREQ -from . import HAS_ENVIRON from . import HAS_HEAP_INFO -from . import HAS_IONICE -from . import HAS_MEMORY_MAPS from . import HAS_NET_IO_COUNTERS +from . import HAS_PROC_CPU_AFFINITY from . import HAS_PROC_CPU_NUM +from . import HAS_PROC_ENVIRON from . import HAS_PROC_IO_COUNTERS -from . import HAS_RLIMIT +from . import HAS_PROC_IONICE +from . import HAS_PROC_MEMORY_MAPS +from . import HAS_PROC_RLIMIT from . import HAS_SENSORS_BATTERY from . import HAS_SENSORS_FANS from . import HAS_SENSORS_TEMPERATURES @@ -99,11 +99,11 @@ def test_nice_set(self): niceness = thisproc.nice() self.execute(lambda: self.proc.nice(niceness)) - @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_IONICE, reason="not supported") def test_ionice(self): self.execute(self.proc.ionice) - @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_IONICE, reason="not supported") def test_ionice_set(self): if WINDOWS: value = thisproc.ionice() @@ -111,7 +111,7 @@ def test_ionice_set(self): else: self.execute(lambda: self.proc.ionice(psutil.IOPRIO_CLASS_NONE)) - @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_IONICE, reason="not supported") @pytest.mark.skipif(WINDOWS, reason="not on WINDOWS") def test_ionice_set_badarg(self): fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) @@ -174,16 +174,16 @@ def test_resume(self): def test_cwd(self): self.execute(self.proc.cwd) - @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_CPU_AFFINITY, reason="not supported") def test_cpu_affinity(self): self.execute(self.proc.cpu_affinity) - @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_set(self): affinity = thisproc.cpu_affinity() self.execute(lambda: self.proc.cpu_affinity(affinity)) - @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_set_badarg(self): self.execute_w_exc( ValueError, lambda: self.proc.cpu_affinity([-1]), retries=20 @@ -194,24 +194,24 @@ def test_open_files(self): with open(get_testfn(), 'w'): self.execute(self.proc.open_files, **kw) - @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_MEMORY_MAPS, reason="not supported") @pytest.mark.skipif(LINUX, reason="too slow on LINUX") def test_memory_maps(self): self.execute(self.proc.memory_maps, times=60, retries=10) @pytest.mark.skipif(not LINUX, reason="LINUX only") - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit(self): self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE)) @pytest.mark.skipif(not LINUX, reason="LINUX only") - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit_set(self): limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) @pytest.mark.skipif(not LINUX, reason="LINUX only") - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit_set_badarg(self): self.execute_w_exc( (OSError, ValueError), lambda: self.proc.rlimit(-1), retries=20 @@ -229,7 +229,7 @@ def test_net_connections(self): kind = 'inet' if SUNOS else 'all' self.execute(lambda: self.proc.net_connections(kind), times=times) - @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_ENVIRON, reason="not supported") def test_environ(self): self.execute(self.proc.environ) diff --git a/tests/test_process.py b/tests/test_process.py index 252a2e0b66..17473c7187 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -41,14 +41,14 @@ from . import CI_TESTING from . import GITHUB_ACTIONS from . import GLOBAL_TIMEOUT -from . import HAS_CPU_AFFINITY -from . import HAS_ENVIRON -from . import HAS_IONICE -from . import HAS_MEMORY_MAPS +from . import HAS_PROC_CPU_AFFINITY from . import HAS_PROC_CPU_NUM +from . import HAS_PROC_ENVIRON from . import HAS_PROC_IO_COUNTERS -from . import HAS_RLIMIT -from . import HAS_THREADS +from . import HAS_PROC_IONICE +from . import HAS_PROC_MEMORY_MAPS +from . import HAS_PROC_RLIMIT +from . import HAS_PROC_THREADS from . import MACOS_11PLUS from . import PYPY from . import PYTHON_EXE @@ -224,7 +224,7 @@ def test_io_counters(self): assert io2[i] >= 0 assert io2[i] >= 0 - @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_IONICE, reason="not supported") @pytest.mark.skipif(not LINUX, reason="linux only") def test_ionice_linux(self): def cleanup(init): @@ -269,7 +269,7 @@ def cleanup(init): ): p.ionice(value=1) - @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_IONICE, reason="not supported") @pytest.mark.skipif( not WINDOWS, reason="not supported on this win version" ) @@ -299,7 +299,7 @@ def test_ionice_win(self): with pytest.raises(ValueError, match="is not a valid priority"): p.ionice(psutil.IOPRIO_HIGH + 1) - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit_get(self): import resource @@ -323,7 +323,7 @@ def test_rlimit_get(self): assert ret[0] >= -1 assert ret[1] >= -1 - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit_set(self): p = self.spawn_psproc() p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) @@ -336,7 +336,7 @@ def test_rlimit_set(self): with pytest.raises(ValueError): p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit(self): p = psutil.Process() testfn = self.get_testfn() @@ -355,7 +355,7 @@ def test_rlimit(self): p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="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. @@ -370,7 +370,7 @@ def test_rlimit_infinity(self): p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) - @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="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 @@ -406,7 +406,7 @@ def test_num_handles(self): p = psutil.Process() assert p.num_handles() > 0 - @pytest.mark.skipif(not HAS_THREADS, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_THREADS, reason="not supported") def test_threads(self): p = psutil.Process() if OPENBSD: @@ -428,7 +428,7 @@ def test_threads(self): @retry_on_failure() @skip_on_access_denied(only_if=MACOS) - @pytest.mark.skipif(not HAS_THREADS, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_THREADS, reason="not supported") def test_threads_2(self): p = self.spawn_psproc() if OPENBSD: @@ -492,7 +492,7 @@ def test_memory_full_info(self): assert mem.pss >= 0 assert mem.swap >= 0 - @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_MEMORY_MAPS, reason="not supported") def test_memory_maps(self): p = psutil.Process() maps = p.memory_maps() @@ -541,7 +541,7 @@ def test_memory_maps(self): assert isinstance(value, int) assert value >= 0, value - @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_MEMORY_MAPS, reason="not supported") def test_memory_maps_lists_lib(self): # Make sure a newly loaded shared lib is listed. p = psutil.Process() @@ -814,7 +814,7 @@ def test_cwd_2(self): p = self.spawn_psproc(cmd) call_until(lambda: p.cwd() == os.path.dirname(os.getcwd())) - @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_CPU_AFFINITY, reason="not supported") def test_cpu_affinity(self): p = psutil.Process() initial = p.cpu_affinity() @@ -853,7 +853,7 @@ def test_cpu_affinity(self): p.cpu_affinity(set(all_cpus)) p.cpu_affinity(tuple(all_cpus)) - @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_errs(self): p = self.spawn_psproc() invalid_cpu = [len(psutil.cpu_times(percpu=True)) + 10] @@ -866,7 +866,7 @@ def test_cpu_affinity_errs(self): with pytest.raises(ValueError): p.cpu_affinity([0, -1]) - @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_CPU_AFFINITY, reason="not supported") def test_cpu_affinity_all_combinations(self): p = psutil.Process() initial = p.cpu_affinity() @@ -1384,7 +1384,7 @@ def test_pid_0(self): assert 0 in psutil.pids() assert psutil.pid_exists(0) - @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_ENVIRON, reason="not supported") def test_environ(self): def clean_dict(d): exclude = {"PLAT", "HOME"} @@ -1411,7 +1411,7 @@ def clean_dict(d): if not OSX and GITHUB_ACTIONS: assert d1 == d2 - @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_ENVIRON, reason="not supported") @pytest.mark.skipif(not POSIX, reason="POSIX only") @pytest.mark.skipif( MACOS_11PLUS, diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 2561e457a8..6cfee2ad1b 100755 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -20,7 +20,7 @@ from . import CI_TESTING from . import HAS_BATTERY -from . import HAS_MEMORY_MAPS +from . import HAS_PROC_MEMORY_MAPS from . import HAS_SENSORS_BATTERY from . import HAS_SENSORS_FANS from . import HAS_SENSORS_TEMPERATURES @@ -119,7 +119,7 @@ def test_netstat(self): def test_ifconfig(self): self.assert_stdout('ifconfig.py') - @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_MEMORY_MAPS, reason="not supported") def test_pmap(self): self.assert_stdout('pmap.py', str(os.getpid())) diff --git a/tests/test_unicode.py b/tests/test_unicode.py index d9abaeb9ea..d3a6e0e1eb 100755 --- a/tests/test_unicode.py +++ b/tests/test_unicode.py @@ -81,9 +81,9 @@ from . import ASCII_FS from . import CI_TESTING -from . import HAS_ENVIRON -from . import HAS_MEMORY_MAPS from . import HAS_NET_CONNECTIONS_UNIX +from . import HAS_PROC_ENVIRON +from . import HAS_PROC_MEMORY_MAPS from . import INVALID_UNICODE_SUFFIX from . import PYPY from . import TESTFN_PREFIX @@ -271,7 +271,7 @@ def test_disk_usage(self): safe_mkdir(dname) psutil.disk_usage(dname) - @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_MEMORY_MAPS, reason="not supported") def test_memory_maps(self): with copyload_shared_lib(suffix=self.funky_suffix) as funky_path: @@ -308,7 +308,7 @@ class TestNonFSAPIS(BaseUnicodeTest): funky_suffix = UNICODE_SUFFIX - @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(not HAS_PROC_ENVIRON, reason="not supported") @pytest.mark.skipif(PYPY and WINDOWS, reason="segfaults on PYPY + WINDOWS") def test_proc_environ(self): # Note: differently from others, this test does not deal From a2a259b22fe668a39c5d43b945f5ce39fe128711 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 27 Feb 2026 13:08:39 +0100 Subject: [PATCH 1509/1714] Try to enable openbsd ci --- .github/workflows/bsd.yml | 2 +- scripts/internal/install-sysdeps.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index d3cb85212a..3ad58bf42c 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -55,7 +55,7 @@ jobs: make ci-test netbsd: - if: false # XXX: disabled + # if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh index 81547a1726..89fa90f7a0 100755 --- a/scripts/internal/install-sysdeps.sh +++ b/scripts/internal/install-sysdeps.sh @@ -60,6 +60,9 @@ main() { $SUDO /usr/sbin/pkg_add -v pkgin $SUDO pkgin update $SUDO pkgin -y install python311-* gcc12-* + if [ ! -e /usr/pkg/bin/python3 ]; then + $SUDO ln -s /usr/pkg/bin/python3.11 /usr/pkg/bin/python3 + fi elif [ $OPENBSD ]; then $SUDO pkg_add gcc python3 elif [ $SUNOS ]; then From 399dfd4f0dc904c13e6cd0270782af809e4b7527 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Mar 2026 17:22:03 +0100 Subject: [PATCH 1510/1714] Add 2 POSIX tests based on resource module (ctx switches and cpu times) --- tests/test_posix.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_posix.py b/tests/test_posix.py index 881a89b6c0..0f39df6af5 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -315,6 +315,18 @@ def test_nice(self): psutil_nice = psutil.Process().nice() assert ps_nice == psutil_nice + def test_num_ctx_switches(self): + ru = resource.getrusage(resource.RUSAGE_SELF) + cws = psutil.Process().num_ctx_switches() + assert cws.voluntary == pytest.approx(ru.ru_nvcsw, abs=1) + assert cws.involuntary == pytest.approx(ru.ru_nivcsw, abs=1) + + def test_cpu_times(self): + ru = resource.getrusage(resource.RUSAGE_SELF) + cws = psutil.Process().cpu_times() + assert cws.user == pytest.approx(ru.ru_utime, abs=0.1) + assert cws.system == pytest.approx(ru.ru_stime, abs=0.1) + @pytest.mark.skipif(not POSIX, reason="POSIX only") class TestSystemAPIs(PsutilTestCase): From c35e4bdb009440cece40fbdcd9ba62331f6ec3b0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Mar 2026 17:30:12 +0100 Subject: [PATCH 1511/1714] Give more test tolerance --- tests/test_posix.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_posix.py b/tests/test_posix.py index 0f39df6af5..ed9ee3c065 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -315,17 +315,19 @@ def test_nice(self): psutil_nice = psutil.Process().nice() assert ps_nice == psutil_nice + @retry_on_failure() def test_num_ctx_switches(self): ru = resource.getrusage(resource.RUSAGE_SELF) cws = psutil.Process().num_ctx_switches() - assert cws.voluntary == pytest.approx(ru.ru_nvcsw, abs=1) - assert cws.involuntary == pytest.approx(ru.ru_nivcsw, abs=1) + assert cws.voluntary == pytest.approx(ru.ru_nvcsw, abs=10) + assert cws.involuntary == pytest.approx(ru.ru_nivcsw, abs=10) + @retry_on_failure() def test_cpu_times(self): ru = resource.getrusage(resource.RUSAGE_SELF) cws = psutil.Process().cpu_times() - assert cws.user == pytest.approx(ru.ru_utime, abs=0.1) - assert cws.system == pytest.approx(ru.ru_stime, abs=0.1) + assert cws.user == pytest.approx(ru.ru_utime, abs=0.3) + assert cws.system == pytest.approx(ru.ru_stime, abs=0.3) @pytest.mark.skipif(not POSIX, reason="POSIX only") From e44f9d16228139fb5c65f1c67e3b3d58e37d8d7f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Mar 2026 18:29:05 +0100 Subject: [PATCH 1512/1714] Fix #2726 / macOS: num ctx switches C type precision issue (#2727) --- HISTORY.rst | 2 ++ docs/index.rst | 5 +++++ psutil/arch/osx/proc.c | 8 ++++---- tests/test_posix.py | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 5fcce93483..bd02944de5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ - 2715_, [Linux]: ``wait_pid_pidfd_open()`` (from `Process.wait()`_) crashes with ``EINVAL`` due to kernel race condition. +- 2726_, [macOS]: `Process.num_ctx_switches()`_ return an unusual high number + due to a C type precision issue. 7.2.2 ===== diff --git a/docs/index.rst b/docs/index.rst index 06619ee9d1..02e9f3d9b5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1482,6 +1482,11 @@ Process class The number voluntary and involuntary context switches performed by this process (cumulative). + .. note:: + (macOS) *involuntary* value is always set to 0, while *voluntary* value + reflect the total number of context switches (voluntary + involuntary). + This is a limitation of the OS. + .. versionchanged:: 5.4.1 added AIX support .. method:: num_fds() diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 41b2849046..de7a0c1ca8 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -147,13 +147,13 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { // psutil_proc_pidinfo(pid, PROC_PIDREGIONINFO, 0, &pri, sizeof(pri)) pti.pti_resident_size, // (uns long long) rss pti.pti_virtual_size, // (uns long long) vms - pti.pti_faults, // (uns long) number of page faults (pages) - pti.pti_pageins, // (uns long) number of actual pageins (pages) - pti.pti_threadnum, // (uns long) num threads + (unsigned long)pti.pti_faults, // number of page faults (pages) + (unsigned long)pti.pti_pageins, // number of actual pageins (pages) + (unsigned long)pti.pti_threadnum, // num threads // Unvoluntary value seems not to be available; // pti.pti_csw probably refers to the sum of the two; // getrusage() numbers seems to confirm this theory. - pti.pti_csw // (uns long) voluntary ctx switches + (unsigned long)pti.pti_csw // voluntary ctx switches ); } diff --git a/tests/test_posix.py b/tests/test_posix.py index ed9ee3c065..0f80f0ad6b 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -316,6 +316,7 @@ def test_nice(self): assert ps_nice == psutil_nice @retry_on_failure() + @pytest.mark.skipif(MACOS, reason="pti_csw is total (vol+invol) on MACOS") def test_num_ctx_switches(self): ru = resource.getrusage(resource.RUSAGE_SELF) cws = psutil.Process().num_ctx_switches() From 54e6283a90c604dab896a2531f07cde301e0d174 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Mar 2026 18:55:42 +0100 Subject: [PATCH 1513/1714] Osx: fix proc CPU times calculation for x86_64 (arm64 was fine) (#2728) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On x86_64 macOS (Rosetta 2 on Apple Silicon CI), `mach_timebase_info()` returns `numer=1, denom=1` (Intel-normalized), but `proc_pidinfo(PROC_PIDTASKINFO)` goes directly to the kernel and returns `pti_total_user` in native ARM Mach ticks (24 MHz, needing ×41.67 to reach nanoseconds). So the conversion is a no-op when it should be ×41.67. Hence the 41.67× undercount. The fix: use `sysctlbyname("hw.tbfrequency")` instead of `mach_timebase_info`. The former always returns the true hardware timer rate (1 GHz on Intel, 24 MHz on Apple Silicon), bypassing Rosetta normalization. Should fix: https://github.com/giampaolo/psutil/issues/2411. --- psutil/arch/osx/init.c | 18 ++++++++++++------ psutil/arch/osx/init.h | 2 +- psutil/arch/osx/proc.c | 15 ++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/psutil/arch/osx/init.c b/psutil/arch/osx/init.c index 1f4aab463f..1d9621160b 100644 --- a/psutil/arch/osx/init.c +++ b/psutil/arch/osx/init.c @@ -5,22 +5,28 @@ */ #include -#include +#include #include "../../arch/all/init.h" #include "init.h" -struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; +uint64_t PSUTIL_HW_TBFREQUENCY; // Called on module import. int psutil_setup_osx(void) { - kern_return_t ret; + size_t size = sizeof(PSUTIL_HW_TBFREQUENCY); - ret = mach_timebase_info(&PSUTIL_MACH_TIMEBASE_INFO); - if (ret != KERN_SUCCESS) { - psutil_oserror_wsyscall("mach_timebase_info"); + // hw.tbfrequency gives the real hardware timer frequency regardless of + // whether we are running under Rosetta 2 (x86_64 on Apple Silicon). + // mach_timebase_info() is intercepted by Rosetta and returns numer=1, + // denom=1 for x86_64 processes, but proc_pidinfo() returns raw ARM Mach + // ticks, so mach_timebase_info gives a wrong conversion factor there. + if (sysctlbyname("hw.tbfrequency", &PSUTIL_HW_TBFREQUENCY, &size, NULL, 0) + != 0) + { + psutil_oserror_wsyscall("sysctlbyname('hw.tbfrequency')"); return -1; } return 0; diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 6ce2573870..2eb90a0d7c 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -10,7 +10,7 @@ #include #include -extern struct mach_timebase_info PSUTIL_MACH_TIMEBASE_INFO; +extern uint64_t PSUTIL_HW_TBFREQUENCY; int psutil_setup_osx(void); int _psutil_pids(pid_t **pids_array, int *pids_count); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index de7a0c1ca8..a3de41a697 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -123,23 +123,20 @@ PyObject * psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { pid_t pid; struct proc_taskinfo pti; - uint64_t total_user; - uint64_t total_system; if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) != 0) return NULL; - total_user = pti.pti_total_user * PSUTIL_MACH_TIMEBASE_INFO.numer; - total_user /= PSUTIL_MACH_TIMEBASE_INFO.denom; - total_system = pti.pti_total_system * PSUTIL_MACH_TIMEBASE_INFO.numer; - total_system /= PSUTIL_MACH_TIMEBASE_INFO.denom; - return Py_BuildValue( "(ddKKkkkk)", - (float)total_user / 1000000000.0, // (float) cpu user time - (float)total_system / 1000000000.0, // (float) cpu sys time + // pti_total_user/system are in Mach ticks; hw.tbfrequency gives + // ticks/second and is not intercepted by Rosetta 2, unlike + // mach_timebase_info() which returns numer=1/denom=1 for x86_64 + // processes on Apple Silicon, causing a 41.67x undercount there. + (double)pti.pti_total_user / PSUTIL_HW_TBFREQUENCY, // cpu user time + (double)pti.pti_total_system / PSUTIL_HW_TBFREQUENCY, // cpu sys time // Note about memory: determining other mem stats on macOS is a mess: // http://www.opensource.apple.com/source/top/top-67/libtop.c?txt // I just give up. From 32f1c83191d701511f208cb7d6a82f5d7b54a931 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Mar 2026 18:58:19 +0100 Subject: [PATCH 1514/1714] Enable POSIX ctx switches test on macOS --- psutil/arch/osx/proc.c | 6 +++--- tests/test_posix.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index a3de41a697..3165154e78 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -147,9 +147,9 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { (unsigned long)pti.pti_faults, // number of page faults (pages) (unsigned long)pti.pti_pageins, // number of actual pageins (pages) (unsigned long)pti.pti_threadnum, // num threads - // Unvoluntary value seems not to be available; - // pti.pti_csw probably refers to the sum of the two; - // getrusage() numbers seems to confirm this theory. + // Unvoluntary not available on macOS. `pti_csw` refers to the + // sum of voluntary + involuntary. getrusage() numbers confirm + // this theory. (unsigned long)pti.pti_csw // voluntary ctx switches ); } diff --git a/tests/test_posix.py b/tests/test_posix.py index 0f80f0ad6b..26ba5b4be3 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -316,12 +316,17 @@ def test_nice(self): assert ps_nice == psutil_nice @retry_on_failure() - @pytest.mark.skipif(MACOS, reason="pti_csw is total (vol+invol) on MACOS") def test_num_ctx_switches(self): ru = resource.getrusage(resource.RUSAGE_SELF) cws = psutil.Process().num_ctx_switches() - assert cws.voluntary == pytest.approx(ru.ru_nvcsw, abs=10) - assert cws.involuntary == pytest.approx(ru.ru_nivcsw, abs=10) + tol = 10 + if MACOS: + assert cws.voluntary + cws.involuntary == pytest.approx( + ru.ru_nvcsw + ru.ru_nivcsw, abs=tol * 2 + ) + else: + assert cws.voluntary == pytest.approx(ru.ru_nvcsw, abs=tol) + assert cws.involuntary == pytest.approx(ru.ru_nivcsw, abs=tol) @retry_on_failure() def test_cpu_times(self): From 7342838b01b6ddcff96aa4e8603f8cbd73417b1d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Mar 2026 19:02:12 +0100 Subject: [PATCH 1515/1714] Update HISTORY.rst --- HISTORY.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index bd02944de5..6238c5d584 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,16 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* +7.2.4 (IN DEVELOPMENT) +====================== + +**Bug fixes** + +- 2726_, [macOS]: `Process.num_ctx_switches()`_ return an unusual high number + due to a C type precision issue. +- 2411_ [macOS]: `Process.cpu_times()`_ and `Process.cpu_percent()`_ + calculation on macOS x86_64 (arm64 is fine) was highly inaccurate (41.67x + lower). + 7.2.3 ===== @@ -9,8 +20,6 @@ - 2715_, [Linux]: ``wait_pid_pidfd_open()`` (from `Process.wait()`_) crashes with ``EINVAL`` due to kernel race condition. -- 2726_, [macOS]: `Process.num_ctx_switches()`_ return an unusual high number - due to a C type precision issue. 7.2.2 ===== From 86e4546fc1b5832a57d5eca2ce55e6470e61ce2d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 1 Mar 2026 23:31:59 +0100 Subject: [PATCH 1516/1714] Implement `Process.page_faults()` (#2730) --- HISTORY.rst | 13 ++++++++++++- README.rst | 3 +++ docs/index.rst | 34 ++++++++++++++++++++++++++++------ psutil/__init__.py | 18 ++++++++++++++++++ psutil/_ntuples.py | 5 ++++- psutil/_psbsd.py | 12 +++++++++++- psutil/_pslinux.py | 7 +++++++ psutil/_psosx.py | 17 +++++++++++------ psutil/_pssunos.py | 5 +++++ psutil/_psutil_sunos.c | 1 + psutil/_psutil_windows.c | 1 + psutil/_pswindows.py | 5 +++++ psutil/arch/bsd/proc.c | 10 ++++++++-- psutil/arch/osx/proc.c | 11 +++++++++-- psutil/arch/sunos/init.h | 1 + psutil/arch/sunos/proc.c | 19 +++++++++++++++++++ psutil/arch/windows/init.h | 1 + psutil/arch/windows/ntextapi.h | 9 +++++---- psutil/arch/windows/proc.c | 27 +++++++++++++++++++++++++++ tests/__init__.py | 1 + tests/test_memleaks.py | 3 +++ tests/test_posix.py | 22 ++++++++++++++++++++++ tests/test_process.py | 6 ++++++ tests/test_process_all.py | 7 +++++++ tests/test_windows.py | 11 +++++++++++ 25 files changed, 226 insertions(+), 23 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6238c5d584..beeda9eceb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,8 +1,13 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.2.4 (IN DEVELOPMENT) +7.3.0 (IN DEVELOPMENT) ====================== +**Enhancements** + +- 2729_: New `Process.page_faults()`_ method, returning a ``(minor, major)`` + namedtuple. + **Bug fixes** - 2726_, [macOS]: `Process.num_ctx_switches()`_ return an unusual high number @@ -11,6 +16,11 @@ calculation on macOS x86_64 (arm64 is fine) was highly inaccurate (41.67x lower). +**Compatibility notes** + +- `Process.memory_info()`_ on macOS no longer returns *pfaults* and *pageins* + fields. Use `Process.page_faults()`_ method instead. + 7.2.3 ===== @@ -2961,6 +2971,7 @@ In most cases accessing the old names will work but it will cause a .. _`Process.num_threads()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_threads .. _`Process.oneshot()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.oneshot .. _`Process.open_files()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.open_files +.. _`Process.page_faults()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.page_faults .. _`Process.parent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.parent .. _`Process.parents()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.parents .. _`Process.pid`: https://psutil.readthedocs.io/en/latest/#psutil.Process.pid diff --git a/README.rst b/README.rst index 068d68a617..af904070ff 100644 --- a/README.rst +++ b/README.rst @@ -347,6 +347,9 @@ Process management pmmap_grouped(path='[stack]', rss=2465792, size=2494464, pss=2465792, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=2465792, referenced=2277376, anonymous=2465792, swap=0), ...] >>> + >>> p.page_faults() + ppagefaults(minor=5905, major=3) + >>> >>> p.io_counters() pio(read_count=478001, write_count=59371, read_bytes=700416, write_bytes=69632, read_chars=456232, write_chars=517543) >>> diff --git a/docs/index.rst b/docs/index.rst index 02e9f3d9b5..6ce2172154 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1653,9 +1653,9 @@ Process class +---------+---------+-------+---------+-----+------------------------------+ | vms | vms | vms | vms | vms | vms (alias for ``pagefile``) | +---------+---------+-------+---------+-----+------------------------------+ - | shared | pfaults | text | | | num_page_faults | + | shared | | text | | | num_page_faults | +---------+---------+-------+---------+-----+------------------------------+ - | text | pageins | data | | | peak_wset | + | text | | data | | | peak_wset | +---------+---------+-------+---------+-----+------------------------------+ | lib | | stack | | | wset | +---------+---------+-------+---------+-----+------------------------------+ @@ -1702,10 +1702,6 @@ Process class - **dirty** *(Linux)*: the number of dirty pages. - - **pfaults** *(macOS)*: number of page faults. - - - **pageins** *(macOS)*: number of actual pageins. - For on explanation of Windows fields rely on `PROCESS_MEMORY_COUNTERS_EX`_ structure doc. Example on Linux: @@ -1717,6 +1713,10 @@ Process class .. versionchanged:: 4.0.0 multiple fields are returned, not only `rss` and `vms`. + .. versionchanged:: + 7.3.0 macOS: *pfaults* and *pageins* are no longer returned. Use + :meth:`page_faults` method instead. + .. method:: memory_full_info() This method returns the same information as :meth:`memory_info`, plus, on @@ -1852,6 +1852,28 @@ Process class See also how to `kill a process tree <#kill-process-tree>`__ and `terminate my children <#terminate-my-children>`__. + .. method:: page_faults() + + Return the number of page faults for this process as a ``(minor, major)`` + namedtuple. + + - **minor** (a.k.a. *soft* faults): occur when a memory page is not + currently mapped into the process address space, but is already present + in physical RAM (e.g. a shared library loaded by another process). The + kernel resolves these without disk I/O. + - **major** (a.k.a. *hard* faults): occur when the page must be fetched + from disk. These are expensive because they stall the process until I/O + completes. + + Both counters are cumulative since process creation. Example:: + + >>> import psutil + >>> p = psutil.Process() + >>> p.page_faults() + ppagefaults(minor=5905, major=3) + + .. versionadded:: 7.3.0 + .. method:: open_files() Return regular files opened by process as a list of named tuples including diff --git a/psutil/__init__.py b/psutil/__init__.py index 9ebbdfb91c..c76b36e35e 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1215,6 +1215,24 @@ def memory_maps(self, grouped=True): else: return [_ntp.pmmap_ext(*x) for x in it] + def page_faults(self): + """Return the number of page faults for this process as a + (minor, major) namedtuple. + + - *minor* (a.k.a. *soft* faults): occur when a memory page is + not currently mapped into the process address space, but is + already present in physical RAM (e.g. a shared library page + loaded by another process). The kernel resolves these without + disk I/O. + + - *major* (a.k.a. *hard* faults): occur when the page must be + fetched from disk. These are expensive because they stall the + process until I/O completes. + + Both counters are cumulative since process creation. + """ + return self._proc.page_faults() + def open_files(self): """Return files opened by process as a list of (path, fd) namedtuples including the absolute file name diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index f0ab039376..25591f3c43 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -126,6 +126,9 @@ # psutil.Process.ctx_switches() pctxsw = nt("pctxsw", ("voluntary", "involuntary")) +# psutil.Process.page_faults() +ppagefaults = nt("ppagefaults", ("minor", "major")) + # psutil.Process.net_connections() pconn = nt("pconn", ("fd", "family", "type", "laddr", "raddr", "status")) @@ -306,7 +309,7 @@ ) # psutil.Process.memory_info() - pmem = nt("pmem", ("rss", "vms", "pfaults", "pageins")) + pmem = nt("pmem", ("rss", "vms")) # psutil.Process.memory_full_info() pfullmem = nt("pfullmem", pmem._fields + ("uss",)) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index a75cd1af13..3b294aa3ed 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -124,7 +124,9 @@ memdata=21, memstack=22, cpunum=23, - name=24, + min_faults=24, + maj_faults=25, + name=26, ) @@ -755,6 +757,14 @@ def num_ctx_switches(self): rawtuple[kinfo_proc_map['ctx_switches_unvol']], ) + @wrap_exceptions + def page_faults(self): + rawtuple = self.oneshot() + return ntp.ppagefaults( + rawtuple[kinfo_proc_map['min_faults']], + rawtuple[kinfo_proc_map['maj_faults']], + ) + @wrap_exceptions def threads(self): # Note: on OpenSBD this (/dev/mem) requires root access. diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index dc305b5955..69b7c3e1ab 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1693,6 +1693,8 @@ def _parse_stat_file(self): ret['status'] = fields[0] ret['ppid'] = fields[1] ret['ttynr'] = fields[4] + ret['minflt'] = fields[7] + ret['majflt'] = fields[9] ret['utime'] = fields[11] ret['stime'] = fields[12] ret['children_utime'] = fields[13] @@ -2024,6 +2026,11 @@ def get_blocks(lines, current_block): ls.append(item) return ls + @wrap_exceptions + def page_faults(self): + values = self._parse_stat_file() + return ntp.ppagefaults(int(values['minflt']), int(values['majflt'])) + @wrap_exceptions def cwd(self): return self._readlink( diff --git a/psutil/_psosx.py b/psutil/_psosx.py index b14d5e023b..deb05a3f76 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -76,8 +76,8 @@ cpustime=1, rss=2, vms=3, - pfaults=4, - pageins=5, + minor_faults=4, + major_faults=5, numthreads=6, volctxsw=7, ) @@ -448,10 +448,7 @@ def terminal(self): def memory_info(self): rawtuple = self._get_pidtaskinfo() return ntp.pmem( - rawtuple[pidtaskinfo_map['rss']], - rawtuple[pidtaskinfo_map['vms']], - rawtuple[pidtaskinfo_map['pfaults']], - rawtuple[pidtaskinfo_map['pageins']], + rawtuple[pidtaskinfo_map['rss']], rawtuple[pidtaskinfo_map['vms']] ) @wrap_exceptions @@ -460,6 +457,14 @@ def memory_full_info(self): uss = cext.proc_memory_uss(self.pid) return ntp.pfullmem(*basic_mem + (uss,)) + @wrap_exceptions + def page_faults(self): + rawtuple = self._get_pidtaskinfo() + return ntp.ppagefaults( + rawtuple[pidtaskinfo_map['minor_faults']], + rawtuple[pidtaskinfo_map['major_faults']], + ) + @wrap_exceptions def cpu_times(self): rawtuple = self._get_pidtaskinfo() diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index a0e70581f5..33a910948f 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -699,6 +699,11 @@ def num_ctx_switches(self): *cext.proc_num_ctx_switches(self.pid, self._procfs_path) ) + @wrap_exceptions + def page_faults(self): + ret = cext.proc_page_faults(self.pid, self._procfs_path) + return ntp.ppagefaults(*ret) + @wrap_exceptions def wait(self, timeout=None): return _psposix.wait_pid(self.pid, timeout) diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index e8ca62a74a..0eb9825bfe 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -41,6 +41,7 @@ static PyMethodDef mod_methods[] = { {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, {"proc_name_and_args", psutil_proc_name_and_args, METH_VARARGS}, {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS}, + {"proc_page_faults", psutil_proc_page_faults, METH_VARARGS}, {"query_process_thread", psutil_proc_query_thread, METH_VARARGS}, // --- system-related functions diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 31f9800ff4..509364dccf 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -48,6 +48,7 @@ static PyMethodDef PsutilMethods[] = { {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, {"proc_num_handles", psutil_proc_num_handles, METH_VARARGS}, {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, + {"proc_page_faults", psutil_proc_page_faults, METH_VARARGS}, {"proc_priority_get", psutil_proc_priority_get, METH_VARARGS}, {"proc_priority_set", psutil_proc_priority_set, METH_VARARGS}, {"proc_suspend_or_resume", psutil_proc_suspend_or_resume, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 3ee1eafce0..5003725d8c 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -826,6 +826,11 @@ def memory_full_info(self): uss *= getpagesize() return ntp.pfullmem(*basic_mem + (uss,)) + @wrap_exceptions + def page_faults(self): + ret = cext.proc_page_faults(self.pid) + return ntp.ppagefaults(*ret) + def memory_maps(self): try: raw = cext.proc_memory_maps(self.pid) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 893b4c92c9..4e347a41bb 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -117,9 +117,9 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { // Return a single big tuple with all process info. py_retlist = Py_BuildValue( #if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031 - "(OillllllLdllllddddlllllbO)", + "(OillllllLdllllddddlllllbllO)", #else - "(OillllllidllllddddlllllbO)", + "(OillllllidllllddddlllllbllO)", #endif #ifdef PSUTIL_FREEBSD py_ppid, // (pid_t) ppid @@ -154,6 +154,9 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { memstack, // (long) mem stack // others oncpu, // (int) the CPU we are on + // page faults + (long)kp.ki_rusage.ru_minflt, // (long) minor page faults + (long)kp.ki_rusage.ru_majflt, // (long) major page faults #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) py_ppid, // (pid_t) ppid (int)kp.p_stat, // (int) status @@ -189,6 +192,9 @@ psutil_proc_oneshot_info(PyObject *self, PyObject *args) { memstack, // (long) mem stack // others oncpu, // (int) the CPU we are on + // page faults + (long)kp.p_uru_minflt, // (long) minor page faults + (long)kp.p_uru_majflt, // (long) major page faults #endif py_name // (pystr) name ); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 3165154e78..65041b67ba 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -123,12 +123,19 @@ PyObject * psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { pid_t pid; struct proc_taskinfo pti; + unsigned long maj_faults; + unsigned long min_faults; if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) != 0) return NULL; + // matches getrusage().ru_majflt + maj_faults = (unsigned long)pti.pti_pageins; + // matches getrusage().ru_minflt + min_faults = (unsigned long)pti.pti_faults - maj_faults; + return Py_BuildValue( "(ddKKkkkk)", // pti_total_user/system are in Mach ticks; hw.tbfrequency gives @@ -144,8 +151,8 @@ psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { // psutil_proc_pidinfo(pid, PROC_PIDREGIONINFO, 0, &pri, sizeof(pri)) pti.pti_resident_size, // (uns long long) rss pti.pti_virtual_size, // (uns long long) vms - (unsigned long)pti.pti_faults, // number of page faults (pages) - (unsigned long)pti.pti_pageins, // number of actual pageins (pages) + min_faults, + maj_faults, (unsigned long)pti.pti_threadnum, // num threads // Unvoluntary not available on macOS. `pti_csw` refers to the // sum of voluntary + involuntary. getrusage() numbers confirm diff --git a/psutil/arch/sunos/init.h b/psutil/arch/sunos/init.h index 754a792c89..ba12c45aaf 100644 --- a/psutil/arch/sunos/init.h +++ b/psutil/arch/sunos/init.h @@ -38,5 +38,6 @@ PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); PyObject *psutil_proc_name_and_args(PyObject *self, PyObject *args); PyObject *psutil_proc_num_ctx_switches(PyObject *self, PyObject *args); +PyObject *psutil_proc_page_faults(PyObject *self, PyObject *args); PyObject *psutil_proc_query_thread(PyObject *self, PyObject *args); PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index c186702181..55f7dc9a7f 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -388,6 +388,25 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { } +/* + * Return process page faults as a (minor, major) tuple. + */ +PyObject * +psutil_proc_page_faults(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + prusage_t info; + const char *procfs_path; + + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + str_format(path, sizeof(path), "%s/%i/usage", procfs_path, pid); + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) + return NULL; + return Py_BuildValue("(kk)", info.pr_minf, info.pr_majf); +} + + /* * Process IO counters. * diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index 3689b22f2c..c6bd9e20e8 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -117,6 +117,7 @@ PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +PyObject *psutil_proc_page_faults(PyObject *self, PyObject *args); PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); PyObject *psutil_proc_priority_set(PyObject *self, PyObject *args); PyObject *psutil_proc_suspend_or_resume(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index e42b81d47a..038e2263a3 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -294,9 +294,10 @@ typedef struct _SYSTEM_THREAD_INFORMATION2 { typedef struct _SYSTEM_PROCESS_INFORMATION2 { ULONG NextEntryOffset; ULONG NumberOfThreads; - LARGE_INTEGER SpareLi1; - LARGE_INTEGER SpareLi2; - LARGE_INTEGER SpareLi3; + ULONGLONG WorkingSetPrivateSize; + ULONG HardFaultCount; + ULONG NumberOfThreadsHighWatermark; + ULONGLONG CycleTime; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; @@ -306,7 +307,7 @@ typedef struct _SYSTEM_PROCESS_INFORMATION2 { HANDLE InheritedFromUniqueProcessId; ULONG HandleCount; ULONG SessionId; - ULONG_PTR PageDirectoryBase; + ULONG_PTR UniqueProcessKey; SIZE_T PeakVirtualSize; SIZE_T VirtualSize; DWORD PageFaultCount; diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index a2d56d63b1..64d39f3561 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -1024,6 +1024,33 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { } +// Return process page faults as a (minor, major) tuple. Uses +// NtQuerySystemInformation(SystemProcessInformation) which returns +// SYSTEM_PROCESS_INFORMATION. PageFaultCount is the total (soft + +// hard), while HardFaultCount (available since Win7) tracks hard +// (major) faults only. Minor faults are derived by subtracting the +// two. +PyObject * +psutil_proc_page_faults(PyObject *self, PyObject *args) { + DWORD pid; + PSYSTEM_PROCESS_INFORMATION process; + PVOID buffer; + ULONG minor; + ULONG major; + PyObject *ret; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (psutil_get_proc_info(pid, &process, &buffer) != 0) + return NULL; + major = process->HardFaultCount; + minor = process->PageFaultCount - major; + ret = Py_BuildValue("(kk)", (unsigned long)minor, (unsigned long)major); + free(buffer); + return ret; +} + + /* * Return True if all process threads are in waiting/suspended state. */ diff --git a/tests/__init__.py b/tests/__init__.py index 54df554921..f4330df315 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1122,6 +1122,7 @@ class process_namespace: ('num_ctx_switches', (), {}), ('num_threads', (), {}), ('open_files', (), {}), + ('page_faults', (), {}), ('ppid', (), {}), ('status', (), {}), ('threads', (), {}), diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index ef72b1a84c..e809d3eb17 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -199,6 +199,9 @@ def test_open_files(self): def test_memory_maps(self): self.execute(self.proc.memory_maps, times=60, retries=10) + def test_page_faults(self): + self.execute(self.proc.page_faults) + @pytest.mark.skipif(not LINUX, reason="LINUX only") @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") def test_rlimit(self): diff --git a/tests/test_posix.py b/tests/test_posix.py index 26ba5b4be3..a801c670fc 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -320,6 +320,8 @@ def test_num_ctx_switches(self): ru = resource.getrusage(resource.RUSAGE_SELF) cws = psutil.Process().num_ctx_switches() tol = 10 + if "PYTEST_XDIST_WORKER_COUNT" in os.environ: + tol *= int(os.environ["PYTEST_XDIST_WORKER_COUNT"]) if MACOS: assert cws.voluntary + cws.involuntary == pytest.approx( ru.ru_nvcsw + ru.ru_nivcsw, abs=tol * 2 @@ -335,6 +337,26 @@ def test_cpu_times(self): assert cws.user == pytest.approx(ru.ru_utime, abs=0.3) assert cws.system == pytest.approx(ru.ru_stime, abs=0.3) + @retry_on_failure() + def test_page_faults(self): + ru = resource.getrusage(resource.RUSAGE_SELF) + pf = psutil.Process().page_faults() + tol = 5 + assert pf.minor == pytest.approx(ru.ru_minflt, abs=tol) + assert pf.major == pytest.approx(ru.ru_majflt, abs=tol) + + @pytest.mark.skipif(not LINUX and not MACOS, reason="Linux, macOS only") + def test_page_faults_minor_increase(self): + # Access 200 new anonymous pages; each first access triggers a + # minor fault. + p = psutil.Process() + pf_before = p.page_faults() + with mmap.mmap(-1, 200 * mmap.PAGESIZE) as m: + for i in range(0, 200 * mmap.PAGESIZE, mmap.PAGESIZE): + m[i : i + 1] = b'\x00' + pf_after = p.page_faults() + assert pf_after.minor > pf_before.minor + @pytest.mark.skipif(not POSIX, reason="POSIX only") class TestSystemAPIs(PsutilTestCase): diff --git a/tests/test_process.py b/tests/test_process.py index 17473c7187..b14c5a9964 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -561,6 +561,12 @@ def test_memory_percent(self): if LINUX or MACOS or WINDOWS: p.memory_percent(memtype='uss') + def test_page_faults(self): + p = psutil.Process() + pfaults = p.page_faults() + assert pfaults.minor > 0 + assert pfaults.major >= 0 + def test_is_running(self): p = self.spawn_psproc() assert p.is_running() diff --git a/tests/test_process_all.py b/tests/test_process_all.py index 62f19f7d48..63b0641de4 100755 --- a/tests/test_process_all.py +++ b/tests/test_process_all.py @@ -432,6 +432,13 @@ def num_handles(self, ret, info): assert isinstance(ret, int) assert ret >= 0 + def page_faults(self, ret, info): + assert is_namedtuple(ret) + assert isinstance(ret.minor, int) + assert isinstance(ret.major, int) + assert ret.minor >= 0 + assert ret.major >= 0 + def nice(self, ret, info): assert isinstance(ret, int) if POSIX: diff --git a/tests/test_windows.py b/tests/test_windows.py index ae5991f844..72136c7c71 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -594,6 +594,17 @@ def test_exe(self): with pytest.raises(psutil.NoSuchProcess): proc.exe() + @retry_on_failure() + def test_page_faults(self): + # memory_info() value comes from GetProcessMemoryInfo -> + # PageFaultCount. page_faults() value comes from + # NtQuerySystemInformation -> HardFaultCount / PageFaultCount + p = psutil.Process() + mem = p.memory_info() + pfaults = p.page_faults() + tol = 500 + assert mem.num_page_faults == pytest.approx(pfaults.minor, abs=tol) + class TestProcessWMI(WindowsTestCase): """Compare Process API results with WMI.""" From f04a426683ba04df5fc07f7d4bec30dc37250b83 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 02:40:29 +0100 Subject: [PATCH 1517/1714] Fix make test-platforms invocation on windows --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7993a9691a..889e1f0503 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ test-posix: ## POSIX specific tests. $(RUN_TEST) -k "test_posix.py or posix_ or Posix" $(ARGS) test-platform: ## Run specific platform tests only. - $(RUN_TEST) tests/test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) + $(RUN_TEST) -k test_`$(PYTHON) -c 'import psutil; print([x.lower() for x in ("LINUX", "BSD", "OSX", "SUNOS", "WINDOWS", "AIX") if getattr(psutil, x)][0])'`.py $(ARGS) test-memleaks: ## Memory leak tests. PYTHONMALLOC=malloc $(RUN_TEST) -k test_memleaks.py $(ARGS) From 6ea68e6f620023117184bb1c39195f0757cd20a4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 10:04:11 +0100 Subject: [PATCH 1518/1714] Doc: update Windows API URLs --- docs/index.rst | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6ce2172154..6bf5df9586 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1674,17 +1674,14 @@ Process class | | | | | | private | +---------+---------+-------+---------+-----+------------------------------+ - - **rss**: aka "Resident Set Size", this is the non-swapped physical - memory a process has used. - On UNIX it matches "top"'s RES column). - On Windows this is an alias for `wset` field and it matches "Mem Usage" - column of taskmgr.exe. + - **rss**: aka "Resident Set Size", this is the non-swapped physical memory + a process has used. On UNIX it matches ``top`` RES column. On Windows + this is an alias for `wset` field and maps to ``WorkingSetSize``. - **vms**: aka "Virtual Memory Size", this is the total amount of virtual - memory used by the process. - On UNIX it matches "top"'s VIRT column. - On Windows this is an alias for `pagefile` field and it matches - "Mem Usage" "VM Size" column of taskmgr.exe. + memory used by the process. On UNIX it matches ``top`` VIRT column. On + Windows this is an alias for `pagefile` field and maps to + ``PagefileUsage``. - **shared**: *(Linux)* memory that could be potentially shared with other processes. @@ -1711,7 +1708,7 @@ Process class pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, lib=0, data=9891840, dirty=0) .. versionchanged:: - 4.0.0 multiple fields are returned, not only `rss` and `vms`. + 4.0.0 multiple fields are returned, not only *rss* and *vms*. .. versionchanged:: 7.3.0 macOS: *pfaults* and *pageins* are no longer returned. Use @@ -3247,10 +3244,10 @@ Timeline .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py .. _`enum`: https://docs.python.org/3/library/enum.html#module-enum .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py -.. _`GetDriveType`: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypea -.. _`GetExitCodeProcess`: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess +.. _`GetDriveType`: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea +.. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ -.. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +.. _`GetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass .. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`hash`: https://docs.python.org/3/library/functions.html#hash .. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py @@ -3279,7 +3276,7 @@ Timeline .. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority .. _`os.times`: https://docs.python.org//library/os.html#os.times .. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`PROCESS_MEMORY_COUNTERS_EX`: https://docs.microsoft.com/en-us/windows/desktop/api/psapi/ns-psapi-_process_memory_counters_ex +.. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex .. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py .. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py .. _`psleak`: https://github.com/giampaolo/psleak @@ -3289,7 +3286,7 @@ Timeline .. _`select.poll`: https://docs.python.org//library/select.html#select.poll .. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py .. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. -.. _`SetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-setpriorityclass +.. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass .. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. .. _`signal module`: https://docs.python.org//library/signal.html .. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM @@ -3299,7 +3296,7 @@ Timeline .. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait .. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen .. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py -.. _`TerminateProcess`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-terminateprocess +.. _`TerminateProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess .. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident .. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread .. _`Tidelift security contact`: https://tidelift.com/security From 9c2bb13ba2d92978e44d3f85909f97eb22c63623 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Mon, 2 Mar 2026 11:55:14 +0200 Subject: [PATCH 1519/1714] net_if_duplex_speed: handle EBUSY from ioctl(SIOCETHTOOL) (#2732) On one of my riscv64 machines, a few netifs return EBUSY from the SIOCETHTOOL ioctl. Treat it like EOPNOTSUPP/EINVAL and return unknown duplex and speed 0 instead of raising an OSError. The offending interfaces are raising the same errors with `ethtool`: ``` $ ethtool end0 netlink error: Device or resource busy netlink error: Device or resource busy netlink error: Device or resource busy netlink error: Device or resource busy netlink error: Device or resource busy No data available ``` --- HISTORY.rst | 1 + psutil/arch/linux/net.c | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index beeda9eceb..cfb6e44f48 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,7 @@ - 2411_ [macOS]: `Process.cpu_times()`_ and `Process.cpu_percent()`_ calculation on macOS x86_64 (arm64 is fine) was highly inaccurate (41.67x lower). +- 2732_, [Linux]: net_if_duplex_speed: handle EBUSY from ioctl(SIOCETHTOOL). **Compatibility notes** diff --git a/psutil/arch/linux/net.c b/psutil/arch/linux/net.c index 6daaa43138..fdacdff903 100644 --- a/psutil/arch/linux/net.c +++ b/psutil/arch/linux/net.c @@ -93,11 +93,12 @@ psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { } } else { - if ((errno == EOPNOTSUPP) || (errno == EINVAL)) { + if ((errno == EOPNOTSUPP) || (errno == EINVAL) || (errno == EBUSY)) { // EOPNOTSUPP may occur in case of wi-fi cards. // For EINVAL see: // https://github.com/giampaolo/psutil/issues/797 // #issuecomment-202999532 + // EBUSY may occur with broken drivers or busy devices. duplex = DUPLEX_UNKNOWN; speed = 0; } From 316edcbe6cb2c4209165c5545b41f2908dfe140d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 12:36:22 +0100 Subject: [PATCH 1520/1714] Update windows proc mem doc including reference to original struct fields --- docs/index.rst | 59 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6bf5df9586..947502899b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1646,42 +1646,41 @@ Process class The "portable" fields available on all platforms are `rss` and `vms`. All numbers are expressed in bytes. - +---------+---------+-------+---------+-----+------------------------------+ - | Linux | macOS | BSD | Solaris | AIX | Windows | - +=========+=========+=======+=========+=====+==============================+ - | rss | rss | rss | rss | rss | rss (alias for ``wset``) | - +---------+---------+-------+---------+-----+------------------------------+ - | vms | vms | vms | vms | vms | vms (alias for ``pagefile``) | - +---------+---------+-------+---------+-----+------------------------------+ - | shared | | text | | | num_page_faults | - +---------+---------+-------+---------+-----+------------------------------+ - | text | | data | | | peak_wset | - +---------+---------+-------+---------+-----+------------------------------+ - | lib | | stack | | | wset | - +---------+---------+-------+---------+-----+------------------------------+ - | data | | | | | peak_paged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | dirty | | | | | paged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | peak_nonpaged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | nonpaged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | pagefile | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | peak_pagefile | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | private | - +---------+---------+-------+---------+-----+------------------------------+ + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | Linux | macOS | BSD | Solaris | AIX | Windows | + +=========+=========+==========+=========+=====+=============================================================+ + | rss | rss | rss | rss | rss | rss (alias for ``wset``, maps to ``WorkingSetSize``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | vms | vms | vms | vms | vms | vms (alias for ``pagefile``, maps to ``PagefileUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | shared | | text | | | num_page_faults (maps to ``PageFaultCount``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | text | | data | | | peak_wset (maps to ``PeakWorkingSetSize``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | lib | | stack | | | wset (maps to ``WorkingSetSize``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | data | | | | | peak_paged_pool (maps to ``QuotaPeakPagedPoolUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | dirty | | | | | paged_pool (maps to ``QuotaPagedPoolUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | | | | | | peak_nonpaged_pool (maps to ``QuotaPeakNonPagedPoolUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | | | | | | nonpaged_pool (maps to ``QuotaNonPagedPoolUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | | | | | | pagefile (maps to ``PagefileUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | | | | | | peak_pagefile (maps to ``PeakPagefileUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + | | | | | | private (maps to ``PrivateUsage``) | + +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - **rss**: aka "Resident Set Size", this is the non-swapped physical memory a process has used. On UNIX it matches ``top`` RES column. On Windows - this is an alias for `wset` field and maps to ``WorkingSetSize``. + this is an alias for `wset` field. - **vms**: aka "Virtual Memory Size", this is the total amount of virtual memory used by the process. On UNIX it matches ``top`` VIRT column. On - Windows this is an alias for `pagefile` field and maps to - ``PagefileUsage``. + Windows this is an alias for `pagefile` field. - **shared**: *(Linux)* memory that could be potentially shared with other processes. From 0c66e64370694856e26479af3bb789c0850af166 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 13:13:42 +0100 Subject: [PATCH 1521/1714] Windows: remove 2 separate Py_BuildValue paths via #ifdef(_WIN64) / #else --- psutil/arch/windows/proc.c | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 64d39f3561..4ed1b8cd00 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -345,14 +345,9 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { } CloseHandle(hProcess); - // PROCESS_MEMORY_COUNTERS values are defined as SIZE_T which on 64bits - // is an (unsigned long long) and on 32bits is an (unsigned int). - // "_WIN64" is defined if we're running a 64bit Python interpreter not - // exclusively if the *system* is 64bit. -#if defined(_WIN64) return Py_BuildValue( - "(kKKKKKKKKK)", - cnt.PageFaultCount, // unsigned long + "(KKKKKKKKKK)", + (unsigned long long)cnt.PageFaultCount, (unsigned long long)cnt.PeakWorkingSetSize, (unsigned long long)cnt.WorkingSetSize, (unsigned long long)cnt.QuotaPeakPagedPoolUsage, @@ -363,21 +358,6 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { (unsigned long long)cnt.PeakPagefileUsage, (unsigned long long)cnt.PrivateUsage ); -#else - return Py_BuildValue( - "(kIIIIIIIII)", - cnt.PageFaultCount, // unsigned long - (unsigned int)cnt.PeakWorkingSetSize, - (unsigned int)cnt.WorkingSetSize, - (unsigned int)cnt.QuotaPeakPagedPoolUsage, - (unsigned int)cnt.QuotaPagedPoolUsage, - (unsigned int)cnt.QuotaPeakNonPagedPoolUsage, - (unsigned int)cnt.QuotaNonPagedPoolUsage, - (unsigned int)cnt.PagefileUsage, - (unsigned int)cnt.PeakPagefileUsage, - (unsigned int)cnt.PrivateUsage - ); -#endif } From a64a03fd48189cefe1b180744bcd3cd8dc819e03 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 14:21:34 +0100 Subject: [PATCH 1522/1714] Windows: remove separate Py_BuildValue paths via #ifdef(_WIN64) / #else --- psutil/arch/windows/proc.c | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 4ed1b8cd00..016feefbce 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -963,11 +963,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { } CloseHandle(hProcess); -#ifdef _WIN64 return Py_BuildValue("K", (unsigned long long)proc_mask); -#else - return Py_BuildValue("k", (unsigned long)proc_mask); -#endif } @@ -981,14 +977,8 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; DWORD_PTR mask; -#ifdef _WIN64 if (!PyArg_ParseTuple(args, _Py_PARSE_PID "K", &pid, &mask)) -#else - if (!PyArg_ParseTuple(args, _Py_PARSE_PID "k", &pid, &mask)) -#endif - { return NULL; - } hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; @@ -1151,15 +1141,9 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { ); if (py_str == NULL) goto error; -#ifdef _WIN64 py_tuple = Py_BuildValue( "(KsOI)", (unsigned long long)baseAddress, -#else - py_tuple = Py_BuildValue( - "(ksOI)", - (unsigned long)baseAddress, -#endif get_region_protection_string(basicInfo.Protect), py_str, basicInfo.RegionSize From a70ae9b49ef3d1328788c9196321250f105563db Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 14:26:43 +0100 Subject: [PATCH 1523/1714] Windows: remove separate Py_BuildValue paths via #ifdef(_WIN64) / #else --- psutil/arch/windows/proc_info.c | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 601355aa8d..1ef505534e 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -824,13 +824,7 @@ psutil_proc_info(PyObject *self, PyObject *args) { } py_retlist = Py_BuildValue( -#if defined(_WIN64) - "kkdddkKKKKKK" - "kKKKKKKKKK", -#else - "kkdddkKKKKKK" - "kIIIIIIIII", -#endif + "kkdddkKKKKKKKKKKKKKKKK", process->HandleCount, // num handles ctx_switches, // num ctx switches user_time, // cpu user time @@ -844,17 +838,18 @@ psutil_proc_info(PyObject *self, PyObject *args) { process->WriteTransferCount.QuadPart, // io wbytes process->OtherOperationCount.QuadPart, // io others count process->OtherTransferCount.QuadPart, // io others bytes - // memory - process->PageFaultCount, // num page faults - process->PeakWorkingSetSize, // peak wset - process->WorkingSetSize, // wset - process->QuotaPeakPagedPoolUsage, // peak paged pool - process->QuotaPagedPoolUsage, // paged pool - process->QuotaPeakNonPagedPoolUsage, // peak non paged pool - process->QuotaNonPagedPoolUsage, // non paged pool - process->PagefileUsage, // pagefile - process->PeakPagefileUsage, // peak pagefile - process->PrivatePageCount // private + (unsigned long long)process->PageFaultCount, // num page faults + (unsigned long long)process->PeakWorkingSetSize, // peak wset + (unsigned long long)process->WorkingSetSize, // wset + (unsigned long long + )process->QuotaPeakPagedPoolUsage, // peak paged pool + (unsigned long long)process->QuotaPagedPoolUsage, // paged pool + (unsigned long long + )process->QuotaPeakNonPagedPoolUsage, // peak non paged pool + (unsigned long long)process->QuotaNonPagedPoolUsage, // non paged pool + (unsigned long long)process->PagefileUsage, // pagefile + (unsigned long long)process->PeakPagefileUsage, // peak pagefile + (unsigned long long)process->PrivatePageCount // private ); free(buffer); From 6c1f08f409bcf10e4484e673d207b7609cda65d6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 15:08:53 +0100 Subject: [PATCH 1524/1714] Rename oneshot() functions --- psutil/_psaix.py | 20 ++++++++--------- psutil/_psbsd.py | 2 +- psutil/_psosx.py | 40 ++++++++++++++++----------------- psutil/_pssunos.py | 30 ++++++++++++------------- psutil/_psutil_aix.c | 4 ++-- psutil/_psutil_bsd.c | 2 +- psutil/_psutil_osx.c | 7 +++--- psutil/_psutil_sunos.c | 2 +- psutil/_psutil_windows.c | 2 +- psutil/_pswindows.py | 20 ++++++++--------- psutil/arch/bsd/init.h | 2 +- psutil/arch/bsd/proc.c | 2 +- psutil/arch/osx/init.h | 4 ++-- psutil/arch/osx/proc.c | 4 ++-- psutil/arch/sunos/init.h | 2 +- psutil/arch/sunos/proc.c | 2 +- psutil/arch/windows/init.h | 4 ++-- psutil/arch/windows/proc_info.c | 2 +- 18 files changed, 76 insertions(+), 75 deletions(-) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 1f628ad93c..17f069b7e0 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -326,17 +326,17 @@ def __init__(self, pid): self._procfs_path = get_procfs_path() def oneshot_enter(self): - self._proc_basic_info.cache_activate(self) + self._proc_oneshot.cache_activate(self) self._proc_cred.cache_activate(self) def oneshot_exit(self): - self._proc_basic_info.cache_deactivate(self) + self._proc_oneshot.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) + def _proc_oneshot(self): + return cext.proc_oneshot(self.pid, self._procfs_path) @wrap_exceptions @memoize_when_activated @@ -390,11 +390,11 @@ def environ(self): @wrap_exceptions def create_time(self): - return self._proc_basic_info()[proc_info_map['create_time']] + return self._proc_oneshot()[proc_info_map['create_time']] @wrap_exceptions def num_threads(self): - return self._proc_basic_info()[proc_info_map['num_threads']] + return self._proc_oneshot()[proc_info_map['num_threads']] if HAS_THREADS: @@ -438,7 +438,7 @@ def nice_set(self, value): @wrap_exceptions def ppid(self): - self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + self._ppid = self._proc_oneshot()[proc_info_map['ppid']] return self._ppid @wrap_exceptions @@ -458,7 +458,7 @@ def cpu_times(self): @wrap_exceptions def terminal(self): - ttydev = self._proc_basic_info()[proc_info_map['ttynr']] + ttydev = self._proc_oneshot()[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) # try to match rdev of /dev/pts/* files ttydev @@ -479,7 +479,7 @@ def cwd(self): @wrap_exceptions def memory_info(self): - ret = self._proc_basic_info() + ret = self._proc_oneshot() rss = ret[proc_info_map['rss']] * 1024 vms = ret[proc_info_map['vms']] * 1024 return ntp.pmem(rss, vms) @@ -488,7 +488,7 @@ def memory_info(self): @wrap_exceptions def status(self): - code = self._proc_basic_info()[proc_info_map['status']] + code = self._proc_oneshot()[proc_info_map['status']] # XXX is '?' legit? (we're not supposed to return it anyway) return PROC_STATUSES.get(code, '?') diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 3b294aa3ed..afc22fa981 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -600,7 +600,7 @@ def _assert_alive(self): @memoize_when_activated def oneshot(self): """Retrieves multiple process info in one shot as a raw tuple.""" - ret = cext.proc_oneshot_info(self.pid) + ret = cext.proc_oneshot_kinfo(self.pid) assert len(ret) == len(kinfo_proc_map) return ret diff --git a/psutil/_psosx.py b/psutil/_psosx.py index deb05a3f76..63551a8364 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -369,31 +369,31 @@ def __init__(self, pid): @wrap_exceptions @memoize_when_activated - def _get_kinfo_proc(self): + def _oneshot_kinfo(self): # Note: should work with all PIDs without permission issues. - ret = cext.proc_kinfo_oneshot(self.pid) + ret = cext.proc_oneshot_kinfo(self.pid) assert len(ret) == len(kinfo_proc_map) return ret @wrap_exceptions @memoize_when_activated - def _get_pidtaskinfo(self): + def _oneshot_pidtaskinfo(self): # Note: should work for PIDs owned by user only. - ret = cext.proc_pidtaskinfo_oneshot(self.pid) + ret = cext.proc_oneshot_pidtaskinfo(self.pid) assert len(ret) == len(pidtaskinfo_map) return ret def oneshot_enter(self): - self._get_kinfo_proc.cache_activate(self) - self._get_pidtaskinfo.cache_activate(self) + self._oneshot_kinfo.cache_activate(self) + self._oneshot_pidtaskinfo.cache_activate(self) def oneshot_exit(self): - self._get_kinfo_proc.cache_deactivate(self) - self._get_pidtaskinfo.cache_deactivate(self) + self._oneshot_kinfo.cache_deactivate(self) + self._oneshot_pidtaskinfo.cache_deactivate(self) @wrap_exceptions def name(self): - name = self._get_kinfo_proc()[kinfo_proc_map['name']] + name = self._oneshot_kinfo()[kinfo_proc_map['name']] return name if name is not None else cext.proc_name(self.pid) @wrap_exceptions @@ -410,7 +410,7 @@ def environ(self): @wrap_exceptions def ppid(self): - self._ppid = self._get_kinfo_proc()[kinfo_proc_map['ppid']] + self._ppid = self._oneshot_kinfo()[kinfo_proc_map['ppid']] return self._ppid @wrap_exceptions @@ -419,7 +419,7 @@ def cwd(self): @wrap_exceptions def uids(self): - rawtuple = self._get_kinfo_proc() + rawtuple = self._oneshot_kinfo() return ntp.puids( rawtuple[kinfo_proc_map['ruid']], rawtuple[kinfo_proc_map['euid']], @@ -428,7 +428,7 @@ def uids(self): @wrap_exceptions def gids(self): - rawtuple = self._get_kinfo_proc() + rawtuple = self._oneshot_kinfo() return ntp.puids( rawtuple[kinfo_proc_map['rgid']], rawtuple[kinfo_proc_map['egid']], @@ -437,7 +437,7 @@ def gids(self): @wrap_exceptions def terminal(self): - tty_nr = self._get_kinfo_proc()[kinfo_proc_map['ttynr']] + tty_nr = self._oneshot_kinfo()[kinfo_proc_map['ttynr']] tmap = _psposix.get_terminal_map() try: return tmap[tty_nr] @@ -446,7 +446,7 @@ def terminal(self): @wrap_exceptions def memory_info(self): - rawtuple = self._get_pidtaskinfo() + rawtuple = self._oneshot_pidtaskinfo() return ntp.pmem( rawtuple[pidtaskinfo_map['rss']], rawtuple[pidtaskinfo_map['vms']] ) @@ -459,7 +459,7 @@ def memory_full_info(self): @wrap_exceptions def page_faults(self): - rawtuple = self._get_pidtaskinfo() + rawtuple = self._oneshot_pidtaskinfo() return ntp.ppagefaults( rawtuple[pidtaskinfo_map['minor_faults']], rawtuple[pidtaskinfo_map['major_faults']], @@ -467,7 +467,7 @@ def page_faults(self): @wrap_exceptions def cpu_times(self): - rawtuple = self._get_pidtaskinfo() + rawtuple = self._oneshot_pidtaskinfo() return ntp.pcputimes( rawtuple[pidtaskinfo_map['cpuutime']], rawtuple[pidtaskinfo_map['cpustime']], @@ -478,7 +478,7 @@ def cpu_times(self): @wrap_exceptions def create_time(self, monotonic=False): - ctime = self._get_kinfo_proc()[kinfo_proc_map['ctime']] + ctime = self._oneshot_kinfo()[kinfo_proc_map['ctime']] if not monotonic: ctime = adjust_proc_create_time(ctime) return ctime @@ -488,12 +488,12 @@ def num_ctx_switches(self): # Unvoluntary value seems not to be available; # getrusage() numbers seems to confirm this theory. # We set it to 0. - vol = self._get_pidtaskinfo()[pidtaskinfo_map['volctxsw']] + vol = self._oneshot_pidtaskinfo()[pidtaskinfo_map['volctxsw']] return ntp.pctxsw(vol, 0) @wrap_exceptions def num_threads(self): - return self._get_pidtaskinfo()[pidtaskinfo_map['numthreads']] + return self._oneshot_pidtaskinfo()[pidtaskinfo_map['numthreads']] @wrap_exceptions def open_files(self): @@ -540,7 +540,7 @@ def nice_set(self, value): @wrap_exceptions def status(self): - code = self._get_kinfo_proc()[kinfo_proc_map['status']] + code = self._oneshot_kinfo()[kinfo_proc_map['status']] # XXX is '?' legit? (we're not supposed to return it anyway) return PROC_STATUSES.get(code, '?') diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 33a910948f..61118e5d3c 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -361,13 +361,13 @@ def _assert_alive(self): os.stat(f"{self._procfs_path}/{self.pid}") def oneshot_enter(self): + self._oneshot.cache_activate(self) 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._oneshot.cache_deactivate(self) self._proc_name_and_args.cache_deactivate(self) - self._proc_basic_info.cache_deactivate(self) self._proc_cred.cache_deactivate(self) @wrap_exceptions @@ -377,12 +377,12 @@ def _proc_name_and_args(self): @wrap_exceptions @memoize_when_activated - def _proc_basic_info(self): + def _oneshot(self): if self.pid == 0 and not os.path.exists( f"{self._procfs_path}/{self.pid}/psinfo" ): raise AccessDenied(self.pid) - ret = cext.proc_basic_info(self.pid, self._procfs_path) + ret = cext.proc_oneshot(self.pid, self._procfs_path) assert len(ret) == len(proc_info_map) return ret @@ -418,18 +418,18 @@ def environ(self): @wrap_exceptions def create_time(self): - return self._proc_basic_info()[proc_info_map['create_time']] + return self._oneshot()[proc_info_map['create_time']] @wrap_exceptions def num_threads(self): - return self._proc_basic_info()[proc_info_map['num_threads']] + return self._oneshot()[proc_info_map['num_threads']] @wrap_exceptions def nice_get(self): # Note #1: getpriority(3) doesn't work for realtime processes. # Psinfo is what ps uses, see: # https://github.com/giampaolo/psutil/issues/1194 - return self._proc_basic_info()[proc_info_map['nice']] + return self._oneshot()[proc_info_map['nice']] @wrap_exceptions def nice_set(self, value): @@ -443,7 +443,7 @@ def nice_set(self, value): @wrap_exceptions def ppid(self): - self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + self._ppid = self._oneshot()[proc_info_map['ppid']] return self._ppid @wrap_exceptions @@ -451,8 +451,8 @@ def uids(self): try: real, effective, saved, _, _, _ = self._proc_cred() except AccessDenied: - real = self._proc_basic_info()[proc_info_map['uid']] - effective = self._proc_basic_info()[proc_info_map['euid']] + real = self._oneshot()[proc_info_map['uid']] + effective = self._oneshot()[proc_info_map['euid']] saved = None return ntp.puids(real, effective, saved) @@ -461,8 +461,8 @@ def gids(self): try: _, _, _, real, effective, saved = self._proc_cred() except AccessDenied: - real = self._proc_basic_info()[proc_info_map['gid']] - effective = self._proc_basic_info()[proc_info_map['egid']] + real = self._oneshot()[proc_info_map['gid']] + effective = self._oneshot()[proc_info_map['egid']] saved = None return ntp.puids(real, effective, saved) @@ -492,7 +492,7 @@ 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._oneshot()[proc_info_map['ttynr']]) if tty != cext.PRNODEV: for x in (0, 1, 2, 255): try: @@ -518,7 +518,7 @@ def cwd(self): @wrap_exceptions def memory_info(self): - ret = self._proc_basic_info() + ret = self._oneshot() rss = ret[proc_info_map['rss']] * 1024 vms = ret[proc_info_map['vms']] * 1024 return ntp.pmem(rss, vms) @@ -527,7 +527,7 @@ def memory_info(self): @wrap_exceptions def status(self): - code = self._proc_basic_info()[proc_info_map['status']] + code = self._oneshot()[proc_info_map['status']] # XXX is '?' legit? (we're not supposed to return it anyway) return PROC_STATUSES.get(code, '?') diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 978dfb9e77..e73a145d4b 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -91,7 +91,7 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { * as a Python tuple. */ static PyObject * -psutil_proc_basic_info(PyObject *self, PyObject *args) { +psutil_proc_oneshot(PyObject *self, PyObject *args) { int pid; char path[100]; psinfo_t info; @@ -940,11 +940,11 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { static PyMethodDef PsutilMethods[] = { // --- process-related functions {"proc_args", psutil_proc_args, METH_VARARGS}, - {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS}, {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS}, {"proc_cred", psutil_proc_cred, METH_VARARGS}, {"proc_environ", psutil_proc_environ, METH_VARARGS}, {"proc_name", psutil_proc_name, METH_VARARGS}, + {"proc_oneshot", psutil_proc_oneshot, METH_VARARGS}, #ifdef CURR_VERSION_THREAD {"proc_threads", psutil_proc_threads, METH_VARARGS}, #endif diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 39e43fc318..7053a2d93f 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -46,7 +46,7 @@ static PyMethodDef mod_methods[] = { {"proc_environ", psutil_proc_environ, METH_VARARGS}, {"proc_name", psutil_proc_name, METH_VARARGS}, {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, - {"proc_oneshot_info", psutil_proc_oneshot_info, METH_VARARGS}, + {"proc_oneshot_kinfo", psutil_proc_oneshot_kinfo, METH_VARARGS}, {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, {"proc_threads", psutil_proc_threads, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index d4cd62da94..4eb87ed202 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -17,20 +17,21 @@ static PyMethodDef mod_methods[] = { // --- per-process functions + // clang-format off {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, {"proc_environ", psutil_proc_environ, METH_VARARGS}, {"proc_exe", psutil_proc_exe, METH_VARARGS}, {"proc_is_zombie", psutil_proc_is_zombie, METH_VARARGS}, - {"proc_kinfo_oneshot", psutil_proc_kinfo_oneshot, METH_VARARGS}, {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, {"proc_name", psutil_proc_name, METH_VARARGS}, {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, {"proc_num_fds", psutil_proc_num_fds, METH_VARARGS}, + {"proc_oneshot_kinfo", psutil_proc_oneshot_kinfo, METH_VARARGS}, + {"proc_oneshot_pidtaskinfo", psutil_proc_oneshot_pidtaskinfo, METH_VARARGS}, {"proc_open_files", psutil_proc_open_files, METH_VARARGS}, - {"proc_pidtaskinfo_oneshot", psutil_proc_pidtaskinfo_oneshot, METH_VARARGS - }, {"proc_threads", psutil_proc_threads, METH_VARARGS}, + // clang-format on // --- system-related functions {"boot_time", psutil_boot_time, METH_VARARGS}, diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 0eb9825bfe..71291dc451 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -33,7 +33,6 @@ static PyMethodDef mod_methods[] = { // --- process-related functions - {"proc_basic_info", psutil_proc_basic_info, METH_VARARGS}, {"proc_cpu_num", psutil_proc_cpu_num, METH_VARARGS}, {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS}, {"proc_cred", psutil_proc_cred, METH_VARARGS}, @@ -41,6 +40,7 @@ static PyMethodDef mod_methods[] = { {"proc_memory_maps", psutil_proc_memory_maps, METH_VARARGS}, {"proc_name_and_args", psutil_proc_name_and_args, METH_VARARGS}, {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS}, + {"proc_oneshot", psutil_proc_oneshot, METH_VARARGS}, {"proc_page_faults", psutil_proc_page_faults, METH_VARARGS}, {"query_process_thread", psutil_proc_query_thread, METH_VARARGS}, diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 509364dccf..0a708557e5 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -58,7 +58,7 @@ static PyMethodDef PsutilMethods[] = { {"proc_wait", psutil_proc_wait, METH_VARARGS}, // --- alternative pinfo interface - {"proc_info", psutil_proc_info, METH_VARARGS}, + {"proc_info", psutil_proc_oneshot, METH_VARARGS}, // --- system-related functions {"uptime", psutil_uptime, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 5003725d8c..636ab9acaf 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -711,15 +711,15 @@ def __init__(self, pid): # --- oneshot() stuff def oneshot_enter(self): - self._proc_info.cache_activate(self) + self._oneshot.cache_activate(self) self.exe.cache_activate(self) def oneshot_exit(self): - self._proc_info.cache_deactivate(self) + self._oneshot.cache_deactivate(self) self.exe.cache_deactivate(self) @memoize_when_activated - def _proc_info(self): + def _oneshot(self): """Return multiple information about this process as a raw tuple. """ @@ -794,7 +794,7 @@ def _get_raw_meminfo(self): # TODO: the C ext can probably be refactored in order # to get this from cext.proc_info() debug("attempting memory_info() fallback (slower)") - info = self._proc_info() + info = self._oneshot() return ( info[pinfo_map['num_page_faults']], info[pinfo_map['peak_wset']], @@ -923,12 +923,12 @@ def create_time(self, fast_only=False): if fast_only: raise debug("attempting create_time() fallback (slower)") - return self._proc_info()[pinfo_map['create_time']] + return self._oneshot()[pinfo_map['create_time']] raise @wrap_exceptions def num_threads(self): - return self._proc_info()[pinfo_map['num_threads']] + return self._oneshot()[pinfo_map['num_threads']] @wrap_exceptions def threads(self): @@ -947,7 +947,7 @@ def cpu_times(self): if not is_permission_err(err): raise debug("attempting cpu_times() fallback (slower)") - info = self._proc_info() + info = self._oneshot() user = info[pinfo_map['user_time']] system = info[pinfo_map['kernel_time']] # Children user/system times are not retrievable (set to 0). @@ -1031,7 +1031,7 @@ def io_counters(self): if not is_permission_err(err): raise debug("attempting io_counters() fallback (slower)") - info = self._proc_info() + info = self._oneshot() ret = ( info[pinfo_map['io_rcount']], info[pinfo_map['io_wcount']], @@ -1091,11 +1091,11 @@ def num_handles(self): except OSError as err: if is_permission_err(err): debug("attempting num_handles() fallback (slower)") - return self._proc_info()[pinfo_map['num_handles']] + return self._oneshot()[pinfo_map['num_handles']] raise @wrap_exceptions def num_ctx_switches(self): - ctx_switches = self._proc_info()[pinfo_map['ctx_switches']] + ctx_switches = self._oneshot()[pinfo_map['ctx_switches']] # only voluntary ctx switches are supported return ntp.pctxsw(ctx_switches, 0) diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index bde5fe6cea..8a5658e91b 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -28,5 +28,5 @@ PyObject *psutil_heap_trim(PyObject *self, PyObject *args); PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); -PyObject *psutil_proc_oneshot_info(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args); PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 4e347a41bb..439e0f3f90 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -24,7 +24,7 @@ * them as a big Python tuple. */ PyObject * -psutil_proc_oneshot_info(PyObject *self, PyObject *args) { +psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { pid_t pid; long rss; long vms; diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index 2eb90a0d7c..ffdc791556 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -42,13 +42,13 @@ PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_exe(PyObject *self, PyObject *args); -PyObject *psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot_pidtaskinfo(PyObject *self, PyObject *args); PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); -PyObject *psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 65041b67ba..63fdbe88ef 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -68,7 +68,7 @@ convert_status(struct extern_proc *p, struct eproc *e) { * information. */ PyObject * -psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { +psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { pid_t pid; int status; struct kinfo_proc kp; @@ -120,7 +120,7 @@ psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { * processes. */ PyObject * -psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { +psutil_proc_oneshot_pidtaskinfo(PyObject *self, PyObject *args) { pid_t pid; struct proc_taskinfo pti; unsigned long maj_faults; diff --git a/psutil/arch/sunos/init.h b/psutil/arch/sunos/init.h index ba12c45aaf..67f80fe67a 100644 --- a/psutil/arch/sunos/init.h +++ b/psutil/arch/sunos/init.h @@ -30,7 +30,6 @@ PyObject *psutil_net_connections(PyObject *self, PyObject *args); PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); PyObject *psutil_net_io_counters(PyObject *self, PyObject *args); PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); -PyObject *psutil_proc_basic_info(PyObject *self, PyObject *args); PyObject *psutil_proc_cpu_num(PyObject *self, PyObject *args); PyObject *psutil_proc_cpu_times(PyObject *self, PyObject *args); PyObject *psutil_proc_cred(PyObject *self, PyObject *args); @@ -38,6 +37,7 @@ PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); PyObject *psutil_proc_name_and_args(PyObject *self, PyObject *args); PyObject *psutil_proc_num_ctx_switches(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_page_faults(PyObject *self, PyObject *args); PyObject *psutil_proc_query_thread(PyObject *self, PyObject *args); PyObject *psutil_swap_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index 55f7dc9a7f..041f8306ff 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -43,7 +43,7 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { * as a Python tuple. */ PyObject * -psutil_proc_basic_info(PyObject *self, PyObject *args) { +psutil_proc_oneshot(PyObject *self, PyObject *args) { int pid; char path[1000]; psinfo_t info; diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index c6bd9e20e8..12ab148ee1 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -89,9 +89,9 @@ PyObject *psutil_disk_usage(PyObject *self, PyObject *args); PyObject *psutil_get_loadavg(); PyObject *psutil_get_open_files(DWORD pid, HANDLE hProcess); PyObject *psutil_getpagesize(PyObject *self, PyObject *args); -PyObject *psutil_init_loadavg_counter(); PyObject *psutil_heap_info(PyObject *self, PyObject *args); PyObject *psutil_heap_trim(PyObject *self, PyObject *args); +PyObject *psutil_init_loadavg_counter(); PyObject *psutil_net_connections(PyObject *self, PyObject *args); PyObject *psutil_net_if_addrs(PyObject *self, PyObject *args); PyObject *psutil_net_if_stats(PyObject *self, PyObject *args); @@ -106,7 +106,6 @@ PyObject *psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_exe(PyObject *self, PyObject *args); -PyObject *psutil_proc_info(PyObject *self, PyObject *args); PyObject *psutil_proc_io_counters(PyObject *self, PyObject *args); PyObject *psutil_proc_io_priority_get(PyObject *self, PyObject *args); PyObject *psutil_proc_io_priority_set(PyObject *self, PyObject *args); @@ -116,6 +115,7 @@ PyObject *psutil_proc_memory_info(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_maps(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); PyObject *psutil_proc_num_handles(PyObject *self, PyObject *args); +PyObject *psutil_proc_oneshot(PyObject *self, PyObject *args); PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); PyObject *psutil_proc_page_faults(PyObject *self, PyObject *args); PyObject *psutil_proc_priority_get(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 1ef505534e..56717d881c 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -789,7 +789,7 @@ psutil_get_proc_info( * - memory_info() (fallback) */ PyObject * -psutil_proc_info(PyObject *self, PyObject *args) { +psutil_proc_oneshot(PyObject *self, PyObject *args) { DWORD pid; PSYSTEM_PROCESS_INFORMATION process; PVOID buffer; From 636b53f69cfc5a051815635997afdab3752770a9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 15:12:44 +0100 Subject: [PATCH 1525/1714] Rename oneshot() functions --- psutil/_psutil_windows.c | 2 +- psutil/_pswindows.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 0a708557e5..109b5f0085 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -58,7 +58,7 @@ static PyMethodDef PsutilMethods[] = { {"proc_wait", psutil_proc_wait, METH_VARARGS}, // --- alternative pinfo interface - {"proc_info", psutil_proc_oneshot, METH_VARARGS}, + {"proc_oneshot", psutil_proc_oneshot, METH_VARARGS}, // --- system-related functions {"uptime", psutil_uptime, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 636ab9acaf..deb25834a9 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -723,7 +723,7 @@ def _oneshot(self): """Return multiple information about this process as a raw tuple. """ - ret = cext.proc_info(self.pid) + ret = cext.proc_oneshot(self.pid) assert len(ret) == len(pinfo_map) return ret @@ -792,7 +792,7 @@ def _get_raw_meminfo(self): except OSError as err: if is_permission_err(err): # TODO: the C ext can probably be refactored in order - # to get this from cext.proc_info() + # to get this from cext.proc_oneshot() debug("attempting memory_info() fallback (slower)") info = self._oneshot() return ( From 12a9ab8b2411bda1663b85f23dd29d53e91d133c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 16:22:32 +0100 Subject: [PATCH 1526/1714] Add pydict_add() C util fun and return dicts on Windows --- MANIFEST.in | 1 + docs/index.rst | 6 +- psutil/_pswindows.py | 100 ++++++++++++++------------------ psutil/arch/all/init.h | 1 + psutil/arch/all/utils.c | 27 +++++++++ psutil/arch/windows/proc.c | 41 +++++++------ psutil/arch/windows/proc_info.c | 93 +++++++++++++++-------------- tests/test_memleaks.py | 8 +-- 8 files changed, 153 insertions(+), 124 deletions(-) create mode 100644 psutil/arch/all/utils.c diff --git a/MANIFEST.in b/MANIFEST.in index 89483408ca..dc4afc78d4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -53,6 +53,7 @@ include psutil/arch/all/init.c include psutil/arch/all/init.h include psutil/arch/all/pids.c include psutil/arch/all/str.c +include psutil/arch/all/utils.c include psutil/arch/bsd/cpu.c include psutil/arch/bsd/disk.c include psutil/arch/bsd/heap.c diff --git a/docs/index.rst b/docs/index.rst index 947502899b..7e88696b47 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1483,9 +1483,9 @@ Process class this process (cumulative). .. note:: - (macOS) *involuntary* value is always set to 0, while *voluntary* value - reflect the total number of context switches (voluntary + involuntary). - This is a limitation of the OS. + (Windows, macOS) *involuntary* value is always set to 0, while + *voluntary* value reflect the total number of context switches (voluntary + + involuntary). This is a limitation of the OS. .. versionchanged:: 5.4.1 added AIX support diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index deb25834a9..dd02c632e2 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -119,31 +119,6 @@ class IOPriority(enum.IntEnum): globals().update(IOPriority.__members__) -pinfo_map = dict( - num_handles=0, - ctx_switches=1, - user_time=2, - kernel_time=3, - create_time=4, - num_threads=5, - io_rcount=6, - io_wcount=7, - io_rbytes=8, - io_wbytes=9, - io_count_others=10, - io_bytes_others=11, - num_page_faults=12, - peak_wset=13, - wset=14, - peak_paged_pool=15, - paged_pool=16, - peak_non_paged_pool=17, - non_paged_pool=18, - pagefile=19, - peak_pagefile=20, - mem_private=21, -) - # ===================================================================== # --- utils @@ -721,11 +696,9 @@ def oneshot_exit(self): @memoize_when_activated def _oneshot(self): """Return multiple information about this process as a - raw tuple. + raw dict. """ - ret = cext.proc_oneshot(self.pid) - assert len(ret) == len(pinfo_map) - return ret + return cext.proc_oneshot(self.pid) def name(self): """Return process name, which on Windows is always the final @@ -795,18 +768,20 @@ def _get_raw_meminfo(self): # to get this from cext.proc_oneshot() debug("attempting memory_info() fallback (slower)") info = self._oneshot() - return ( - info[pinfo_map['num_page_faults']], - info[pinfo_map['peak_wset']], - info[pinfo_map['wset']], - info[pinfo_map['peak_paged_pool']], - info[pinfo_map['paged_pool']], - info[pinfo_map['peak_non_paged_pool']], - info[pinfo_map['non_paged_pool']], - info[pinfo_map['pagefile']], - info[pinfo_map['peak_pagefile']], - info[pinfo_map['mem_private']], - ) + return { + "PageFaultCount": info["PageFaultCount"], + "PeakWorkingSetSize": info["PeakWorkingSetSize"], + "WorkingSetSize": info["WorkingSetSize"], + "QuotaPeakPagedPoolUsage": info["QuotaPeakPagedPoolUsage"], + "QuotaPagedPoolUsage": info["QuotaPagedPoolUsage"], + "QuotaPeakNonPagedPoolUsage": info[ + "QuotaPeakNonPagedPoolUsage" + ], + "QuotaNonPagedPoolUsage": info["QuotaNonPagedPoolUsage"], + "PagefileUsage": info["PagefileUsage"], + "PeakPagefileUsage": info["PeakPagefileUsage"], + "PrivateUsage": info["PrivatePageCount"], + } raise @wrap_exceptions @@ -814,10 +789,21 @@ def memory_info(self): # on Windows RSS == WorkingSetSize and VSM == PagefileUsage. # Underlying C function returns fields of PROCESS_MEMORY_COUNTERS # struct. - t = self._get_raw_meminfo() - rss = t[2] # wset - vms = t[7] # pagefile - return ntp.pmem(*(rss, vms) + t) + d = self._get_raw_meminfo() + return ntp.pmem( + rss=d["WorkingSetSize"], + vms=d["PagefileUsage"], + num_page_faults=d["PageFaultCount"], + peak_wset=d["PeakWorkingSetSize"], + wset=d["WorkingSetSize"], + peak_paged_pool=d["QuotaPeakPagedPoolUsage"], + paged_pool=d["QuotaPagedPoolUsage"], + peak_nonpaged_pool=d["QuotaPeakNonPagedPoolUsage"], + nonpaged_pool=d["QuotaNonPagedPoolUsage"], + pagefile=d["PagefileUsage"], + peak_pagefile=d["PeakPagefileUsage"], + private=d["PrivateUsage"], + ) @wrap_exceptions def memory_full_info(self): @@ -923,12 +909,12 @@ def create_time(self, fast_only=False): if fast_only: raise debug("attempting create_time() fallback (slower)") - return self._oneshot()[pinfo_map['create_time']] + return self._oneshot()["create_time"] raise @wrap_exceptions def num_threads(self): - return self._oneshot()[pinfo_map['num_threads']] + return self._oneshot()["num_threads"] @wrap_exceptions def threads(self): @@ -948,8 +934,8 @@ def cpu_times(self): raise debug("attempting cpu_times() fallback (slower)") info = self._oneshot() - user = info[pinfo_map['user_time']] - system = info[pinfo_map['kernel_time']] + user = info["user_time"] + system = info["kernel_time"] # Children user/system times are not retrievable (set to 0). return ntp.pcputimes(user, system, 0.0, 0.0) @@ -1033,12 +1019,12 @@ def io_counters(self): debug("attempting io_counters() fallback (slower)") info = self._oneshot() 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']], + info["io_rcount"], + info["io_wcount"], + info["io_rbytes"], + info["io_wbytes"], + info["io_count_others"], + info["io_bytes_others"], ) return ntp.pio(*ret) @@ -1091,11 +1077,11 @@ def num_handles(self): except OSError as err: if is_permission_err(err): debug("attempting num_handles() fallback (slower)") - return self._oneshot()[pinfo_map['num_handles']] + return self._oneshot()["num_handles"] raise @wrap_exceptions def num_ctx_switches(self): - ctx_switches = self._oneshot()[pinfo_map['ctx_switches']] + ctx_switches = self._oneshot()["ctx_switches"] # only voluntary ctx switches are supported return ntp.pctxsw(ctx_switches, 0) diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index df0481f62c..2e607a59b3 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -130,6 +130,7 @@ int str_format(char *buf, size_t size, const char *fmt, ...); int psutil_badargs(const char *funcname); int psutil_setup(void); +int pydict_add(PyObject *dict, const char *key, const char *fmt, ...); // ==================================================================== // --- Exposed to Python diff --git a/psutil/arch/all/utils.c b/psutil/arch/all/utils.c new file mode 100644 index 0000000000..cb0dcf8613 --- /dev/null +++ b/psutil/arch/all/utils.c @@ -0,0 +1,27 @@ +#include +#include + + +// Build a Python object from a Py_BuildValue format string and set it +// as key in an existing dict. Returns 1 on success, 0 on failure with +// a Python exception set. +int +pydict_add(PyObject *dict, const char *key, const char *fmt, ...) { + int ret = 0; // 0 = failure + PyObject *obj = NULL; + va_list ap; + + va_start(ap, fmt); + obj = Py_VaBuildValue(fmt, ap); + va_end(ap); + + if (!obj) + return 0; + if (PyDict_SetItemString(dict, key, obj) < 0) + goto done; + ret = 1; // success + +done: + Py_DECREF(obj); + return ret; +} diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 016feefbce..93a034e0f1 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -320,20 +320,23 @@ psutil_proc_exe(PyObject *self, PyObject *args) { /* - * Return process memory information as a Python tuple. + * Return process memory information as a Python dict. */ PyObject * psutil_proc_memory_info(PyObject *self, PyObject *args) { HANDLE hProcess; DWORD pid; PROCESS_MEMORY_COUNTERS_EX cnt; + PyObject *dict = PyDict_New(); - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!dict) return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (NULL == hProcess) - return NULL; + goto error; if (!GetProcessMemoryInfo( hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, sizeof(cnt) @@ -341,23 +344,27 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) { { psutil_oserror(); CloseHandle(hProcess); - return NULL; + goto error; } CloseHandle(hProcess); - return Py_BuildValue( - "(KKKKKKKKKK)", - (unsigned long long)cnt.PageFaultCount, - (unsigned long long)cnt.PeakWorkingSetSize, - (unsigned long long)cnt.WorkingSetSize, - (unsigned long long)cnt.QuotaPeakPagedPoolUsage, - (unsigned long long)cnt.QuotaPagedPoolUsage, - (unsigned long long)cnt.QuotaPeakNonPagedPoolUsage, - (unsigned long long)cnt.QuotaNonPagedPoolUsage, - (unsigned long long)cnt.PagefileUsage, - (unsigned long long)cnt.PeakPagefileUsage, - (unsigned long long)cnt.PrivateUsage - ); + // clang-format off + if (!pydict_add(dict, "PageFaultCount", "K", (ULONGLONG)cnt.PageFaultCount)) goto error; + if (!pydict_add(dict, "PeakWorkingSetSize", "K", (ULONGLONG)cnt.PeakWorkingSetSize)) goto error; + if (!pydict_add(dict, "WorkingSetSize", "K", (ULONGLONG)cnt.WorkingSetSize)) goto error; + if (!pydict_add(dict, "QuotaPeakPagedPoolUsage", "K", (ULONGLONG)cnt.QuotaPeakPagedPoolUsage)) goto error; + if (!pydict_add(dict, "QuotaPagedPoolUsage", "K", (ULONGLONG)cnt.QuotaPagedPoolUsage)) goto error; + if (!pydict_add(dict, "QuotaPeakNonPagedPoolUsage", "K", (ULONGLONG)cnt.QuotaPeakNonPagedPoolUsage)) goto error; + if (!pydict_add(dict, "QuotaNonPagedPoolUsage", "K", (ULONGLONG)cnt.QuotaNonPagedPoolUsage)) goto error; + if (!pydict_add(dict, "PagefileUsage", "K", (ULONGLONG)cnt.PagefileUsage)) goto error; + if (!pydict_add(dict, "PeakPagefileUsage", "K", (ULONGLONG)cnt.PeakPagefileUsage)) goto error; + if (!pydict_add(dict, "PrivateUsage", "K", (ULONGLONG)cnt.PrivateUsage)) goto error; + // clang-format on + return dict; + +error: + Py_DECREF(dict); + return NULL; } diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 56717d881c..2b924f053e 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -778,7 +778,7 @@ psutil_get_proc_info( * Get various process information by using NtQuerySystemInformation. * We use this as a fallback when faster functions fail with access * denied. This is slower because it iterates over all processes. - * Returned tuple includes the following process info: + * Returned dict includes the following process info: * * - num_threads() * - ctx_switches() @@ -791,26 +791,29 @@ psutil_get_proc_info( PyObject * psutil_proc_oneshot(PyObject *self, PyObject *args) { DWORD pid; - PSYSTEM_PROCESS_INFORMATION process; - PVOID buffer; + PSYSTEM_PROCESS_INFORMATION proc; + PVOID buffer = NULL; ULONG i; ULONG ctx_switches = 0; double user_time; double kernel_time; double create_time; - PyObject *py_retlist; + PyObject *dict = PyDict_New(); - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) - return NULL; - if (psutil_get_proc_info(pid, &process, &buffer) != 0) + if (!dict) return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + if (psutil_get_proc_info(pid, &proc, &buffer) != 0) + goto error; + + for (i = 0; i < proc->NumberOfThreads; i++) + ctx_switches += proc->Threads[i].ContextSwitches; - for (i = 0; i < process->NumberOfThreads; i++) - ctx_switches += process->Threads[i].ContextSwitches; - user_time = (double)process->UserTime.HighPart * HI_T - + (double)process->UserTime.LowPart * LO_T; - kernel_time = (double)process->KernelTime.HighPart * HI_T - + (double)process->KernelTime.LowPart * LO_T; + user_time = (double)proc->UserTime.HighPart * HI_T + + (double)proc->UserTime.LowPart * LO_T; + kernel_time = (double)proc->KernelTime.HighPart * HI_T + + (double)proc->KernelTime.LowPart * LO_T; // Convert the LARGE_INTEGER union to a Unix time. // It's the best I could find by googling and borrowing code here @@ -820,38 +823,42 @@ psutil_proc_oneshot(PyObject *self, PyObject *args) { create_time = 0; } else { - create_time = psutil_LargeIntegerToUnixTime(process->CreateTime); + create_time = psutil_LargeIntegerToUnixTime(proc->CreateTime); } - py_retlist = Py_BuildValue( - "kkdddkKKKKKKKKKKKKKKKK", - process->HandleCount, // num handles - ctx_switches, // num ctx switches - user_time, // cpu user time - kernel_time, // cpu kernel time - create_time, // create time - process->NumberOfThreads, // num threads - // IO counters - process->ReadOperationCount.QuadPart, // io rcount - process->WriteOperationCount.QuadPart, // io wcount - process->ReadTransferCount.QuadPart, // io rbytes - process->WriteTransferCount.QuadPart, // io wbytes - process->OtherOperationCount.QuadPart, // io others count - process->OtherTransferCount.QuadPart, // io others bytes - (unsigned long long)process->PageFaultCount, // num page faults - (unsigned long long)process->PeakWorkingSetSize, // peak wset - (unsigned long long)process->WorkingSetSize, // wset - (unsigned long long - )process->QuotaPeakPagedPoolUsage, // peak paged pool - (unsigned long long)process->QuotaPagedPoolUsage, // paged pool - (unsigned long long - )process->QuotaPeakNonPagedPoolUsage, // peak non paged pool - (unsigned long long)process->QuotaNonPagedPoolUsage, // non paged pool - (unsigned long long)process->PagefileUsage, // pagefile - (unsigned long long)process->PeakPagefileUsage, // peak pagefile - (unsigned long long)process->PrivatePageCount // private - ); + // clang-format off + if (!pydict_add(dict, "num_handles", "k", proc->HandleCount)) goto error; + if (!pydict_add(dict, "ctx_switches", "k", ctx_switches)) goto error; + if (!pydict_add(dict, "user_time", "d", user_time)) goto error; + if (!pydict_add(dict, "kernel_time", "d", kernel_time)) goto error; + if (!pydict_add(dict, "create_time", "d", create_time)) goto error; + if (!pydict_add(dict, "num_threads", "k", proc->NumberOfThreads)) goto error; + // I/O + if (!pydict_add(dict, "io_rcount", "K", proc->ReadOperationCount.QuadPart)) goto error; + if (!pydict_add(dict, "io_wcount", "K", proc->WriteOperationCount.QuadPart)) goto error; + if (!pydict_add(dict, "io_rbytes", "K", proc->ReadTransferCount.QuadPart)) goto error; + if (!pydict_add(dict, "io_wbytes", "K", proc->WriteTransferCount.QuadPart)) goto error; + if (!pydict_add(dict, "io_count_others", "K", proc->OtherOperationCount.QuadPart)) goto error; + if (!pydict_add(dict, "io_bytes_others", "K", proc->OtherTransferCount.QuadPart)) goto error; + // proc memory + if (!pydict_add(dict, "PageFaultCount", "K", (ULONGLONG)proc->PageFaultCount)) goto error; + if (!pydict_add(dict, "PeakWorkingSetSize", "K", (ULONGLONG)proc->PeakWorkingSetSize)) goto error; + if (!pydict_add(dict, "WorkingSetSize", "K", (ULONGLONG)proc->WorkingSetSize)) goto error; + if (!pydict_add(dict, "QuotaPeakPagedPoolUsage", "K", (ULONGLONG)proc->QuotaPeakPagedPoolUsage)) goto error; + if (!pydict_add(dict, "QuotaPagedPoolUsage", "K", (ULONGLONG)proc->QuotaPagedPoolUsage)) goto error; + if (!pydict_add(dict, "QuotaPeakNonPagedPoolUsage", "K", (ULONGLONG)proc->QuotaPeakNonPagedPoolUsage)) goto error; + if (!pydict_add(dict, "QuotaNonPagedPoolUsage", "K", (ULONGLONG)proc->QuotaNonPagedPoolUsage)) goto error; + if (!pydict_add(dict, "PagefileUsage", "K", (ULONGLONG)proc->PagefileUsage)) goto error; + if (!pydict_add(dict, "PeakPagefileUsage", "K", (ULONGLONG)proc->PeakPagefileUsage)) goto error; + if (!pydict_add(dict, "PrivatePageCount", "K", (ULONGLONG)proc->PrivatePageCount)) goto error; + // clang-format on free(buffer); - return py_retlist; + return dict; + +error: + if (buffer != NULL) + free(buffer); + Py_DECREF(dict); + return NULL; } diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index e809d3eb17..f72391e297 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -237,8 +237,8 @@ def test_environ(self): self.execute(self.proc.environ) @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") - def test_proc_info(self): - self.execute(lambda: cext.proc_info(os.getpid())) + def test_proc_oneshot(self): + self.execute(lambda: cext.proc_oneshot(os.getpid())) class TestTerminatedProcessLeaks(TestProcessObjectLeaks): @@ -287,11 +287,11 @@ def test_resume(self): def test_wait(self): self.execute(self.proc.wait) - def test_proc_info(self): + def test_proc_oneshot(self): # test dual implementation def call(): try: - return cext.proc_info(self.proc.pid) + return cext.proc_oneshot(self.proc.pid) except ProcessLookupError: pass From 70041472b307d6d3792b9c6f52a71dda29b30275 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 16:47:12 +0100 Subject: [PATCH 1527/1714] Use pydict_add() on OSX --- psutil/_psosx.py | 87 ++++++++--------------------------- psutil/arch/osx/proc.c | 102 +++++++++++++++++++++++------------------ 2 files changed, 77 insertions(+), 112 deletions(-) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 63551a8364..bfe3f533dd 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -57,31 +57,6 @@ cext.SZOMB: _common.STATUS_ZOMBIE, } -kinfo_proc_map = dict( - ppid=0, - ruid=1, - euid=2, - suid=3, - rgid=4, - egid=5, - sgid=6, - ttynr=7, - ctime=8, - status=9, - name=10, -) - -pidtaskinfo_map = dict( - cpuutime=0, - cpustime=1, - rss=2, - vms=3, - minor_faults=4, - major_faults=5, - numthreads=6, - volctxsw=7, -) - # ===================================================================== # --- memory @@ -371,17 +346,13 @@ def __init__(self, pid): @memoize_when_activated def _oneshot_kinfo(self): # Note: should work with all PIDs without permission issues. - ret = cext.proc_oneshot_kinfo(self.pid) - assert len(ret) == len(kinfo_proc_map) - return ret + return cext.proc_oneshot_kinfo(self.pid) @wrap_exceptions @memoize_when_activated def _oneshot_pidtaskinfo(self): # Note: should work for PIDs owned by user only. - ret = cext.proc_oneshot_pidtaskinfo(self.pid) - assert len(ret) == len(pidtaskinfo_map) - return ret + return cext.proc_oneshot_pidtaskinfo(self.pid) def oneshot_enter(self): self._oneshot_kinfo.cache_activate(self) @@ -393,7 +364,7 @@ def oneshot_exit(self): @wrap_exceptions def name(self): - name = self._oneshot_kinfo()[kinfo_proc_map['name']] + name = self._oneshot_kinfo()["name"] return name if name is not None else cext.proc_name(self.pid) @wrap_exceptions @@ -410,7 +381,7 @@ def environ(self): @wrap_exceptions def ppid(self): - self._ppid = self._oneshot_kinfo()[kinfo_proc_map['ppid']] + self._ppid = self._oneshot_kinfo()["ppid"] return self._ppid @wrap_exceptions @@ -419,25 +390,17 @@ def cwd(self): @wrap_exceptions def uids(self): - rawtuple = self._oneshot_kinfo() - return ntp.puids( - rawtuple[kinfo_proc_map['ruid']], - rawtuple[kinfo_proc_map['euid']], - rawtuple[kinfo_proc_map['suid']], - ) + d = self._oneshot_kinfo() + return ntp.puids(d["ruid"], d["euid"], d["suid"]) @wrap_exceptions def gids(self): - rawtuple = self._oneshot_kinfo() - return ntp.puids( - rawtuple[kinfo_proc_map['rgid']], - rawtuple[kinfo_proc_map['egid']], - rawtuple[kinfo_proc_map['sgid']], - ) + d = self._oneshot_kinfo() + return ntp.puids(d["rgid"], d["egid"], d["sgid"]) @wrap_exceptions def terminal(self): - tty_nr = self._oneshot_kinfo()[kinfo_proc_map['ttynr']] + tty_nr = self._oneshot_kinfo()["ttynr"] tmap = _psposix.get_terminal_map() try: return tmap[tty_nr] @@ -446,10 +409,8 @@ def terminal(self): @wrap_exceptions def memory_info(self): - rawtuple = self._oneshot_pidtaskinfo() - return ntp.pmem( - rawtuple[pidtaskinfo_map['rss']], rawtuple[pidtaskinfo_map['vms']] - ) + d = self._oneshot_pidtaskinfo() + return ntp.pmem(d["rss"], d["vms"]) @wrap_exceptions def memory_full_info(self): @@ -459,26 +420,18 @@ def memory_full_info(self): @wrap_exceptions def page_faults(self): - rawtuple = self._oneshot_pidtaskinfo() - return ntp.ppagefaults( - rawtuple[pidtaskinfo_map['minor_faults']], - rawtuple[pidtaskinfo_map['major_faults']], - ) + d = self._oneshot_pidtaskinfo() + return ntp.ppagefaults(d["minor_faults"], d["major_faults"]) @wrap_exceptions def cpu_times(self): - rawtuple = self._oneshot_pidtaskinfo() - return ntp.pcputimes( - rawtuple[pidtaskinfo_map['cpuutime']], - rawtuple[pidtaskinfo_map['cpustime']], - # children user / system times are not retrievable (set to 0) - 0.0, - 0.0, - ) + d = self._oneshot_pidtaskinfo() + # children user / system times are not retrievable (set to 0) + return ntp.pcputimes(d["cpu_utime"], d["cpu_stime"], 0.0, 0.0) @wrap_exceptions def create_time(self, monotonic=False): - ctime = self._oneshot_kinfo()[kinfo_proc_map['ctime']] + ctime = self._oneshot_kinfo()["ctime"] if not monotonic: ctime = adjust_proc_create_time(ctime) return ctime @@ -488,12 +441,12 @@ def num_ctx_switches(self): # Unvoluntary value seems not to be available; # getrusage() numbers seems to confirm this theory. # We set it to 0. - vol = self._oneshot_pidtaskinfo()[pidtaskinfo_map['volctxsw']] + vol = self._oneshot_pidtaskinfo()["volctxsw"] return ntp.pctxsw(vol, 0) @wrap_exceptions def num_threads(self): - return self._oneshot_pidtaskinfo()[pidtaskinfo_map['numthreads']] + return self._oneshot_pidtaskinfo()["num_threads"] @wrap_exceptions def open_files(self): @@ -540,7 +493,7 @@ def nice_set(self, value): @wrap_exceptions def status(self): - code = self._oneshot_kinfo()[kinfo_proc_map['status']] + code = self._oneshot_kinfo()["status"] # XXX is '?' legit? (we're not supposed to return it anyway) return PROC_STATUSES.get(code, '?') diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 63fdbe88ef..2c836f6cd3 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -60,7 +60,7 @@ convert_status(struct extern_proc *p, struct eproc *e) { /* - * Return multiple process info as a Python tuple in one shot by + * Return multiple process info as a Python dict in one shot by * using sysctl() and filling up a kinfo_proc struct. * It should be possible to do this for all processes without * incurring into permission (EPERM) errors. @@ -73,12 +73,14 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { int status; struct kinfo_proc kp; PyObject *py_name = NULL; - PyObject *py_retlist = NULL; + PyObject *dict = PyDict_New(); - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!dict) return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; if (psutil_get_kinfo_proc(pid, &kp) == -1) - return NULL; + goto error; py_name = PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); if (!py_name) { @@ -91,28 +93,32 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { status = convert_status(&kp.kp_proc, &kp.kp_eproc); - py_retlist = Py_BuildValue( - _Py_PARSE_PID "llllllldiO", - kp.kp_eproc.e_ppid, // (pid_t) ppid - (long)kp.kp_eproc.e_pcred.p_ruid, // (long) real uid - (long)kp.kp_eproc.e_ucred.cr_uid, // (long) effective uid - (long)kp.kp_eproc.e_pcred.p_svuid, // (long) saved uid - (long)kp.kp_eproc.e_pcred.p_rgid, // (long) real gid - (long)kp.kp_eproc.e_ucred.cr_groups[0], // (long) effective gid - (long)kp.kp_eproc.e_pcred.p_svgid, // (long) saved gid - (long long)kp.kp_eproc.e_tdev, // (long long) tty nr - PSUTIL_TV2DOUBLE(kp.kp_proc.p_starttime), // (double) create time - status, // (int) status - py_name // (pystr) name - ); + // clang-format off + if (!pydict_add(dict, "ppid", _Py_PARSE_PID, kp.kp_eproc.e_ppid)) goto error; + if (!pydict_add(dict, "ruid", "l", (long)kp.kp_eproc.e_pcred.p_ruid)) goto error; + if (!pydict_add(dict, "euid", "l", (long)kp.kp_eproc.e_ucred.cr_uid)) goto error; + if (!pydict_add(dict, "suid", "l", (long)kp.kp_eproc.e_pcred.p_svuid)) goto error; + if (!pydict_add(dict, "rgid", "l", (long)kp.kp_eproc.e_pcred.p_rgid)) goto error; + if (!pydict_add(dict, "egid", "l", (long)kp.kp_eproc.e_ucred.cr_groups[0])) goto error; + if (!pydict_add(dict, "sgid", "l", (long)kp.kp_eproc.e_pcred.p_svgid)) goto error; + if (!pydict_add(dict, "ttynr", "l", (long)kp.kp_eproc.e_tdev)) goto error; + if (!pydict_add(dict, "ctime", "d", PSUTIL_TV2DOUBLE(kp.kp_proc.p_starttime))) goto error; + if (!pydict_add(dict, "status", "i", status)) goto error; + if (!pydict_add(dict, "name", "O", py_name)) goto error; + // clang-format on Py_DECREF(py_name); - return py_retlist; + return dict; + +error: + Py_XDECREF(py_name); + Py_DECREF(dict); + return NULL; } /* - * Return multiple process info as a Python tuple in one shot by + * Return multiple process info as a Python dict in one shot by * using proc_pidinfo(PROC_PIDTASKINFO) and filling a proc_taskinfo * struct. * Contrarily from proc_kinfo above this function will fail with @@ -125,40 +131,46 @@ psutil_proc_oneshot_pidtaskinfo(PyObject *self, PyObject *args) { struct proc_taskinfo pti; unsigned long maj_faults; unsigned long min_faults; + PyObject *dict = PyDict_New(); - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!dict) return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) != 0) - return NULL; + goto error; // matches getrusage().ru_majflt maj_faults = (unsigned long)pti.pti_pageins; // matches getrusage().ru_minflt min_faults = (unsigned long)pti.pti_faults - maj_faults; - return Py_BuildValue( - "(ddKKkkkk)", - // pti_total_user/system are in Mach ticks; hw.tbfrequency gives - // ticks/second and is not intercepted by Rosetta 2, unlike - // mach_timebase_info() which returns numer=1/denom=1 for x86_64 - // processes on Apple Silicon, causing a 41.67x undercount there. - (double)pti.pti_total_user / PSUTIL_HW_TBFREQUENCY, // cpu user time - (double)pti.pti_total_system / PSUTIL_HW_TBFREQUENCY, // cpu sys time - // Note about memory: determining other mem stats on macOS is a mess: - // http://www.opensource.apple.com/source/top/top-67/libtop.c?txt - // I just give up. - // struct proc_regioninfo pri; - // psutil_proc_pidinfo(pid, PROC_PIDREGIONINFO, 0, &pri, sizeof(pri)) - pti.pti_resident_size, // (uns long long) rss - pti.pti_virtual_size, // (uns long long) vms - min_faults, - maj_faults, - (unsigned long)pti.pti_threadnum, // num threads - // Unvoluntary not available on macOS. `pti_csw` refers to the - // sum of voluntary + involuntary. getrusage() numbers confirm - // this theory. - (unsigned long)pti.pti_csw // voluntary ctx switches - ); + // clang-format off + // pti_total_user/system are in Mach ticks; hw.tbfrequency gives + // ticks/second and is not intercepted by Rosetta 2, unlike + // mach_timebase_info() which returns numer=1/denom=1 for x86_64 + // processes on Apple Silicon, causing a 41.67x undercount there. + if (!pydict_add(dict, "cpu_utime", "d", (double)pti.pti_total_user / PSUTIL_HW_TBFREQUENCY)) goto error; + if (!pydict_add(dict, "cpu_stime", "d", (double)pti.pti_total_system / PSUTIL_HW_TBFREQUENCY)) goto error; + // Note about memory: determining other mem stats on macOS is a mess: + // http://www.opensource.apple.com/source/top/top-67/libtop.c?txt + // I just give up. + if (!pydict_add(dict, "rss", "K", pti.pti_resident_size)) goto error; + if (!pydict_add(dict, "vms", "K", pti.pti_virtual_size)) goto error; + if (!pydict_add(dict, "minor_faults", "k", min_faults)) goto error; + if (!pydict_add(dict, "major_faults", "k", maj_faults)) goto error; + if (!pydict_add(dict, "num_threads", "k", (unsigned long)pti.pti_threadnum)) goto error; + // Unvoluntary not available on macOS. `pti_csw` refers to the + // sum of voluntary + involuntary. getrusage() numbers confirm + // this theory. + if (!pydict_add(dict, "volctxsw", "k", (unsigned long)pti.pti_csw)) goto error; + // clang-format on + + return dict; + +error: + Py_DECREF(dict); + return NULL; } From 269d16b554e85755f5d7082bd588ece080d8cbf1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 17:05:44 +0100 Subject: [PATCH 1528/1714] Port BSD proc_oneshot() to pydict_add() --- psutil/_psbsd.py | 102 ++++++-------------------- psutil/arch/bsd/proc.c | 161 +++++++++++++++++------------------------ 2 files changed, 87 insertions(+), 176 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index afc22fa981..f5b2c9fe6e 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -99,36 +99,6 @@ HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads") -kinfo_proc_map = dict( - ppid=0, - status=1, - real_uid=2, - effective_uid=3, - saved_uid=4, - real_gid=5, - effective_gid=6, - saved_gid=7, - ttynr=8, - create_time=9, - ctx_switches_vol=10, - ctx_switches_unvol=11, - read_io_count=12, - write_io_count=13, - user_time=14, - sys_time=15, - ch_user_time=16, - ch_sys_time=17, - rss=18, - vms=19, - memtext=20, - memdata=21, - memstack=22, - cpunum=23, - min_faults=24, - maj_faults=25, - name=26, -) - # ===================================================================== # --- memory @@ -599,10 +569,8 @@ def _assert_alive(self): @wrap_exceptions @memoize_when_activated def oneshot(self): - """Retrieves multiple process info in one shot as a raw tuple.""" - ret = cext.proc_oneshot_kinfo(self.pid) - assert len(ret) == len(kinfo_proc_map) - return ret + """Retrieves multiple process info in one shot as a raw dict.""" + return cext.proc_oneshot_kinfo(self.pid) def oneshot_enter(self): self.oneshot.cache_activate(self) @@ -612,7 +580,7 @@ def oneshot_exit(self): @wrap_exceptions def name(self): - name = self.oneshot()[kinfo_proc_map['name']] + name = self.oneshot()["name"] return name if name is not None else cext.proc_name(self.pid) @wrap_exceptions @@ -674,7 +642,7 @@ def environ(self): @wrap_exceptions def terminal(self): - tty_nr = self.oneshot()[kinfo_proc_map['ttynr']] + tty_nr = self.oneshot()["ttynr"] tmap = _psposix.get_terminal_map() try: return tmap[tty_nr] @@ -683,59 +651,44 @@ def terminal(self): @wrap_exceptions def ppid(self): - self._ppid = self.oneshot()[kinfo_proc_map['ppid']] + self._ppid = self.oneshot()["ppid"] return self._ppid @wrap_exceptions def uids(self): - rawtuple = self.oneshot() - return ntp.puids( - rawtuple[kinfo_proc_map['real_uid']], - rawtuple[kinfo_proc_map['effective_uid']], - rawtuple[kinfo_proc_map['saved_uid']], - ) + d = self.oneshot() + return ntp.puids(d["real_uid"], d["effective_uid"], d["saved_uid"]) @wrap_exceptions def gids(self): - rawtuple = self.oneshot() - return ntp.pgids( - rawtuple[kinfo_proc_map['real_gid']], - rawtuple[kinfo_proc_map['effective_gid']], - rawtuple[kinfo_proc_map['saved_gid']], - ) + d = self.oneshot() + return ntp.pgids(d["real_gid"], d["effective_gid"], d["saved_gid"]) @wrap_exceptions def cpu_times(self): - rawtuple = self.oneshot() + d = self.oneshot() return ntp.pcputimes( - 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']], + d["user_time"], d["sys_time"], d["ch_user_time"], d["ch_sys_time"] ) if FREEBSD: @wrap_exceptions def cpu_num(self): - return self.oneshot()[kinfo_proc_map['cpunum']] + return self.oneshot()["cpunum"] @wrap_exceptions def memory_info(self): - rawtuple = self.oneshot() + d = self.oneshot() return ntp.pmem( - rawtuple[kinfo_proc_map['rss']], - rawtuple[kinfo_proc_map['vms']], - rawtuple[kinfo_proc_map['memtext']], - rawtuple[kinfo_proc_map['memdata']], - rawtuple[kinfo_proc_map['memstack']], + d["rss"], d["vms"], d["memtext"], d["memdata"], d["memstack"] ) memory_full_info = memory_info @wrap_exceptions def create_time(self, monotonic=False): - ctime = self.oneshot()[kinfo_proc_map['create_time']] + ctime = self.oneshot()["create_time"] if NETBSD and not monotonic: # NetBSD: ctime subject to system clock updates. ctime = adjust_proc_create_time(ctime) @@ -751,19 +704,13 @@ def num_threads(self): @wrap_exceptions def num_ctx_switches(self): - rawtuple = self.oneshot() - return ntp.pctxsw( - rawtuple[kinfo_proc_map['ctx_switches_vol']], - rawtuple[kinfo_proc_map['ctx_switches_unvol']], - ) + d = self.oneshot() + return ntp.pctxsw(d["ctx_switches_vol"], d["ctx_switches_unvol"]) @wrap_exceptions def page_faults(self): - rawtuple = self.oneshot() - return ntp.ppagefaults( - rawtuple[kinfo_proc_map['min_faults']], - rawtuple[kinfo_proc_map['maj_faults']], - ) + d = self.oneshot() + return ntp.ppagefaults(d["min_faults"], d["maj_faults"]) @wrap_exceptions def threads(self): @@ -816,19 +763,14 @@ def nice_set(self, value): @wrap_exceptions def status(self): - code = self.oneshot()[kinfo_proc_map['status']] + code = self.oneshot()["status"] # XXX is '?' legit? (we're not supposed to return it anyway) return PROC_STATUSES.get(code, '?') @wrap_exceptions def io_counters(self): - rawtuple = self.oneshot() - return ntp.pio( - rawtuple[kinfo_proc_map['read_io_count']], - rawtuple[kinfo_proc_map['write_io_count']], - -1, - -1, - ) + d = self.oneshot() + return ntp.pio(d["read_io_count"], d["write_io_count"], -1, -1) @wrap_exceptions def cwd(self): diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 439e0f3f90..f8fe4c7e4e 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -21,7 +21,7 @@ /* * Collect different info about a process in one shot and return - * them as a big Python tuple. + * them as a Python dict. */ PyObject * psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { @@ -40,15 +40,16 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { long pagesize = psutil_getpagesize(); char name_buf[256]; // buffer for process name PyObject *py_name = NULL; - PyObject *py_ppid = NULL; - PyObject *py_retlist = NULL; + PyObject *dict = PyDict_New(); - if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + if (!dict) return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; if (psutil_kinfo_proc(pid, &kp) == -1) - return NULL; + goto error; - // Process + // Process name #ifdef PSUTIL_FREEBSD str_format(name_buf, sizeof(name_buf), "%s", kp.ki_comm); #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) @@ -104,104 +105,72 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { oncpu = -1; #endif + // clang-format off #ifdef PSUTIL_FREEBSD - py_ppid = PyLong_FromPid(kp.ki_ppid); -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_ppid = PyLong_FromPid(kp.p_ppid); -#else - py_ppid = Py_BuildValue("i", -1); -#endif - if (!py_ppid) - return NULL; - - // Return a single big tuple with all process info. - py_retlist = Py_BuildValue( + if (!pydict_add(dict, "ppid", _Py_PARSE_PID, kp.ki_ppid)) goto error; + if (!pydict_add(dict, "status", "i", (int)kp.ki_stat)) goto error; + if (!pydict_add(dict, "real_uid", "l", (long)kp.ki_ruid)) goto error; + if (!pydict_add(dict, "effective_uid", "l", (long)kp.ki_uid)) goto error; + if (!pydict_add(dict, "saved_uid", "l", (long)kp.ki_svuid)) goto error; + if (!pydict_add(dict, "real_gid", "l", (long)kp.ki_rgid)) goto error; + if (!pydict_add(dict, "effective_gid", "l", (long)kp.ki_groups[0])) goto error; + if (!pydict_add(dict, "saved_gid", "l", (long)kp.ki_svuid)) goto error; #if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031 - "(OillllllLdllllddddlllllbllO)", + if (!pydict_add(dict, "ttynr", "L", kp.ki_tdev)) goto error; #else - "(OillllllidllllddddlllllbllO)", + if (!pydict_add(dict, "ttynr", "i", (int)kp.ki_tdev)) goto error; #endif -#ifdef PSUTIL_FREEBSD - py_ppid, // (pid_t) ppid - (int)kp.ki_stat, // (int) status - // UIDs - (long)kp.ki_ruid, // (long) real uid - (long)kp.ki_uid, // (long) effective uid - (long)kp.ki_svuid, // (long) saved uid - // GIDs - (long)kp.ki_rgid, // (long) real gid - (long)kp.ki_groups[0], // (long) effective gid - (long)kp.ki_svuid, // (long) saved gid - // - kp.ki_tdev, // (int or long long) tty nr - PSUTIL_TV2DOUBLE(kp.ki_start), // (double) create time - // ctx switches - kp.ki_rusage.ru_nvcsw, // (long) ctx switches (voluntary) - kp.ki_rusage.ru_nivcsw, // (long) ctx switches (unvoluntary) - // IO count - kp.ki_rusage.ru_inblock, // (long) read io count - kp.ki_rusage.ru_oublock, // (long) write io count - // CPU times: convert from micro seconds to seconds. - PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_utime), // (double) user time - PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_stime), // (double) sys time - PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_utime), // (double) children utime - PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_stime), // (double) children stime - // memory - rss, // (long) rss - vms, // (long) vms - memtext, // (long) mem text - memdata, // (long) mem data - memstack, // (long) mem stack - // others - oncpu, // (int) the CPU we are on - // page faults - (long)kp.ki_rusage.ru_minflt, // (long) minor page faults - (long)kp.ki_rusage.ru_majflt, // (long) major page faults + if (!pydict_add(dict, "create_time", "d", PSUTIL_TV2DOUBLE(kp.ki_start))) goto error; + if (!pydict_add(dict, "ctx_switches_vol", "l", kp.ki_rusage.ru_nvcsw)) goto error; + if (!pydict_add(dict, "ctx_switches_unvol", "l", kp.ki_rusage.ru_nivcsw)) goto error; + if (!pydict_add(dict, "read_io_count", "l", kp.ki_rusage.ru_inblock)) goto error; + if (!pydict_add(dict, "write_io_count", "l", kp.ki_rusage.ru_oublock)) goto error; + if (!pydict_add(dict, "user_time", "d", PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_utime))) goto error; + if (!pydict_add(dict, "sys_time", "d", PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_stime))) goto error; + if (!pydict_add(dict, "ch_user_time", "d", PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_utime))) goto error; + if (!pydict_add(dict, "ch_sys_time", "d", PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_stime))) goto error; + if (!pydict_add(dict, "min_faults", "l", (long)kp.ki_rusage.ru_minflt)) goto error; + if (!pydict_add(dict, "maj_faults", "l", (long)kp.ki_rusage.ru_majflt)) goto error; #elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_ppid, // (pid_t) ppid - (int)kp.p_stat, // (int) status - // UIDs - (long)kp.p_ruid, // (long) real uid - (long)kp.p_uid, // (long) effective uid - (long)kp.p_svuid, // (long) saved uid - // GIDs - (long)kp.p_rgid, // (long) real gid - (long)kp.p_groups[0], // (long) effective gid - (long)kp.p_svuid, // (long) saved gid - // - kp.p_tdev, // (int) tty nr - PSUTIL_KPT2DOUBLE(kp.p_ustart), // (double) create time - // ctx switches - kp.p_uru_nvcsw, // (long) ctx switches (voluntary) - kp.p_uru_nivcsw, // (long) ctx switches (unvoluntary) - // IO count - kp.p_uru_inblock, // (long) read io count - kp.p_uru_oublock, // (long) write io count - // CPU times: convert from micro seconds to seconds. - PSUTIL_KPT2DOUBLE(kp.p_uutime), // (double) user time - PSUTIL_KPT2DOUBLE(kp.p_ustime), // (double) sys time - // OpenBSD and NetBSD provide children user + system times summed - // together (no distinction). - kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch utime - kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0, // (double) ch stime - // memory - rss, // (long) rss - vms, // (long) vms - memtext, // (long) mem text - memdata, // (long) mem data - memstack, // (long) mem stack - // others - oncpu, // (int) the CPU we are on - // page faults - (long)kp.p_uru_minflt, // (long) minor page faults - (long)kp.p_uru_majflt, // (long) major page faults + if (!pydict_add(dict, "ppid", _Py_PARSE_PID, kp.p_ppid)) goto error; + if (!pydict_add(dict, "status", "i", (int)kp.p_stat)) goto error; + if (!pydict_add(dict, "real_uid", "l", (long)kp.p_ruid)) goto error; + if (!pydict_add(dict, "effective_uid", "l", (long)kp.p_uid)) goto error; + if (!pydict_add(dict, "saved_uid", "l", (long)kp.p_svuid)) goto error; + if (!pydict_add(dict, "real_gid", "l", (long)kp.p_rgid)) goto error; + if (!pydict_add(dict, "effective_gid", "l", (long)kp.p_groups[0])) goto error; + if (!pydict_add(dict, "saved_gid", "l", (long)kp.p_svuid)) goto error; + if (!pydict_add(dict, "ttynr", "i", (int)kp.p_tdev)) goto error; + if (!pydict_add(dict, "create_time", "d", PSUTIL_KPT2DOUBLE(kp.p_ustart))) goto error; + if (!pydict_add(dict, "ctx_switches_vol", "l", kp.p_uru_nvcsw)) goto error; + if (!pydict_add(dict, "ctx_switches_unvol", "l", kp.p_uru_nivcsw)) goto error; + if (!pydict_add(dict, "read_io_count", "l", kp.p_uru_inblock)) goto error; + if (!pydict_add(dict, "write_io_count", "l", kp.p_uru_oublock)) goto error; + if (!pydict_add(dict, "user_time", "d", PSUTIL_KPT2DOUBLE(kp.p_uutime))) goto error; + if (!pydict_add(dict, "sys_time", "d", PSUTIL_KPT2DOUBLE(kp.p_ustime))) goto error; + // OpenBSD and NetBSD provide children user + system times summed + // together (no distinction). + if (!pydict_add(dict, "ch_user_time", "d", kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0)) goto error; + if (!pydict_add(dict, "ch_sys_time", "d", kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0)) goto error; + if (!pydict_add(dict, "min_faults", "l", (long)kp.p_uru_minflt)) goto error; + if (!pydict_add(dict, "maj_faults", "l", (long)kp.p_uru_majflt)) goto error; #endif - py_name // (pystr) name - ); + if (!pydict_add(dict, "rss", "l", rss)) goto error; + if (!pydict_add(dict, "vms", "l", vms)) goto error; + if (!pydict_add(dict, "memtext", "l", memtext)) goto error; + if (!pydict_add(dict, "memdata", "l", memdata)) goto error; + if (!pydict_add(dict, "memstack", "l", memstack)) goto error; + if (!pydict_add(dict, "cpunum", "i", oncpu)) goto error; + if (!pydict_add(dict, "name", "O", py_name)) goto error; + // clang-format on Py_DECREF(py_name); - Py_DECREF(py_ppid); - return py_retlist; + return dict; + +error: + Py_XDECREF(py_name); + Py_DECREF(dict); + return NULL; } From 00512869b211329c25eccfb235f0dbcf3f662924 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 17:59:34 +0100 Subject: [PATCH 1529/1714] Remove unnecessary #ifdef __FreeBSD_version check --- psutil/arch/bsd/proc.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index f8fe4c7e4e..a3a0221bf4 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -115,11 +115,7 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { if (!pydict_add(dict, "real_gid", "l", (long)kp.ki_rgid)) goto error; if (!pydict_add(dict, "effective_gid", "l", (long)kp.ki_groups[0])) goto error; if (!pydict_add(dict, "saved_gid", "l", (long)kp.ki_svuid)) goto error; -#if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031 - if (!pydict_add(dict, "ttynr", "L", kp.ki_tdev)) goto error; -#else - if (!pydict_add(dict, "ttynr", "i", (int)kp.ki_tdev)) goto error; -#endif + if (!pydict_add(dict, "ttynr", "L", (unsigned long long)kp.ki_tdev)) goto error; if (!pydict_add(dict, "create_time", "d", PSUTIL_TV2DOUBLE(kp.ki_start))) goto error; if (!pydict_add(dict, "ctx_switches_vol", "l", kp.ki_rusage.ru_nvcsw)) goto error; if (!pydict_add(dict, "ctx_switches_unvol", "l", kp.ki_rusage.ru_nivcsw)) goto error; @@ -131,7 +127,8 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { if (!pydict_add(dict, "ch_sys_time", "d", PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_stime))) goto error; if (!pydict_add(dict, "min_faults", "l", (long)kp.ki_rusage.ru_minflt)) goto error; if (!pydict_add(dict, "maj_faults", "l", (long)kp.ki_rusage.ru_majflt)) goto error; -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +#else + // OpenBSD / NetBSD if (!pydict_add(dict, "ppid", _Py_PARSE_PID, kp.p_ppid)) goto error; if (!pydict_add(dict, "status", "i", (int)kp.p_stat)) goto error; if (!pydict_add(dict, "real_uid", "l", (long)kp.p_ruid)) goto error; @@ -155,6 +152,8 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { if (!pydict_add(dict, "min_faults", "l", (long)kp.p_uru_minflt)) goto error; if (!pydict_add(dict, "maj_faults", "l", (long)kp.p_uru_majflt)) goto error; #endif + + // all BSDs if (!pydict_add(dict, "rss", "l", rss)) goto error; if (!pydict_add(dict, "vms", "l", vms)) goto error; if (!pydict_add(dict, "memtext", "l", memtext)) goto error; @@ -162,8 +161,8 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { if (!pydict_add(dict, "memstack", "l", memstack)) goto error; if (!pydict_add(dict, "cpunum", "i", oncpu)) goto error; if (!pydict_add(dict, "name", "O", py_name)) goto error; - // clang-format on + // clang-format on Py_DECREF(py_name); return dict; From 81da7c63c299089266f978a601b9cb36ffb51716 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 18:21:09 +0100 Subject: [PATCH 1530/1714] test_windows.py refact: use shorter var names (ps - win) --- tests/test_windows.py | 110 ++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/tests/test_windows.py b/tests/test_windows.py index 72136c7c71..f2c8873fc8 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -108,9 +108,7 @@ def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): 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() - assert sys_value == psutil_value + assert psutil.cpu_count() == win32api.GetSystemInfo()[5] def test_cpu_count_logical_vs_wmi(self): w = wmi.WMI() @@ -235,26 +233,24 @@ 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) - assert abs(sys_value[0] - psutil_value.free) < TOLERANCE_DISK_USAGE - assert ( - abs(sys_value[1] - psutil_value.total) < TOLERANCE_DISK_USAGE - ) - assert psutil_value.used == psutil_value.total - psutil_value.free + win = win32api.GetDiskFreeSpaceEx(disk.mountpoint) + ps = psutil.disk_usage(disk.mountpoint) + assert abs(win[0] - ps.free) < TOLERANCE_DISK_USAGE + assert abs(win[1] - ps.total) < TOLERANCE_DISK_USAGE + assert ps.used == ps.total - ps.free def test_disk_partitions(self): - sys_value = [ + win = [ x + '\\' for x in win32api.GetLogicalDriveStrings().split("\\\x00") if x and not x.startswith('A:') ] - psutil_value = [ + ps = [ x.mountpoint for x in psutil.disk_partitions(all=True) if not x.mountpoint.startswith('A:') ] - assert sys_value == psutil_value + assert win == ps def test_convert_dos_path_drive(self): winpath = 'C:\\Windows\\Temp' @@ -460,15 +456,15 @@ def test_username(self): assert psutil.Process().username() == name def test_cmdline(self): - sys_value = re.sub(r"[ ]+", " ", win32api.GetCommandLine()).strip() - psutil_value = ' '.join(psutil.Process().cmdline()) + win = re.sub(r"[ ]+", " ", win32api.GetCommandLine()).strip() + ps = " ".join(psutil.Process().cmdline()) # 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. + # the first 2 quotes from win if not in ps. # A path to an executable will not contain quotes, so this is safe. - sys_value = sys_value.replace('"', "") - psutil_value = psutil_value.replace('"', "") - assert sys_value == psutil_value + win = win.replace('"', "") + ps = ps.replace('"', "") + assert win == ps # XXX - occasional failures @@ -487,36 +483,28 @@ def test_nice(self): win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() ) self.addCleanup(win32api.CloseHandle, handle) - sys_value = win32process.GetPriorityClass(handle) - psutil_value = psutil.Process().nice() - assert psutil_value == sys_value + win = win32process.GetPriorityClass(handle) + ps = psutil.Process().nice() + assert ps == win 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() - assert sys_value['PeakWorkingSetSize'] == psutil_value.peak_wset - assert sys_value['WorkingSetSize'] == psutil_value.wset - assert ( - sys_value['QuotaPeakPagedPoolUsage'] - == psutil_value.peak_paged_pool - ) - assert sys_value['QuotaPagedPoolUsage'] == psutil_value.paged_pool - assert ( - sys_value['QuotaPeakNonPagedPoolUsage'] - == psutil_value.peak_nonpaged_pool - ) - assert ( - sys_value['QuotaNonPagedPoolUsage'] == psutil_value.nonpaged_pool - ) - assert sys_value['PagefileUsage'] == psutil_value.pagefile - assert sys_value['PeakPagefileUsage'] == psutil_value.peak_pagefile - - assert psutil_value.rss == psutil_value.wset - assert psutil_value.vms == psutil_value.pagefile + win = win32process.GetProcessMemoryInfo(handle) + ps = psutil.Process(self.pid).memory_info() + assert ps.peak_wset == win['PeakWorkingSetSize'] + assert ps.wset == win['WorkingSetSize'] + assert ps.peak_paged_pool == win['QuotaPeakPagedPoolUsage'] + assert ps.paged_pool == win['QuotaPagedPoolUsage'] + assert ps.peak_nonpaged_pool == win['QuotaPeakNonPagedPoolUsage'] + assert ps.nonpaged_pool == win['QuotaNonPagedPoolUsage'] + assert ps.pagefile == win['PagefileUsage'] + assert ps.peak_pagefile == win['PeakPagefileUsage'] + + assert ps.rss == ps.wset + assert ps.vms == ps.pagefile def test_wait(self): handle = win32api.OpenProcess( @@ -525,9 +513,9 @@ def test_wait(self): self.addCleanup(win32api.CloseHandle, handle) p = psutil.Process(self.pid) p.terminate() - psutil_value = p.wait() - sys_value = win32process.GetExitCodeProcess(handle) - assert psutil_value == sys_value + ps = p.wait() + win = win32process.GetExitCodeProcess(handle) + assert ps == win def test_cpu_affinity(self): def from_bitmask(x): @@ -537,25 +525,23 @@ def from_bitmask(x): 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() - assert psutil_value == sys_value + win = from_bitmask(win32process.GetProcessAffinityMask(handle)[0]) + ps = psutil.Process(self.pid).cpu_affinity() + assert ps == win 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() - assert psutil_value.read_count == sys_value['ReadOperationCount'] - assert psutil_value.write_count == sys_value['WriteOperationCount'] - assert psutil_value.read_bytes == sys_value['ReadTransferCount'] - assert psutil_value.write_bytes == sys_value['WriteTransferCount'] - assert psutil_value.other_count == sys_value['OtherOperationCount'] - assert psutil_value.other_bytes == sys_value['OtherTransferCount'] + win = win32process.GetProcessIoCounters(handle) + ps = psutil.Process().io_counters() + assert ps.read_count == win['ReadOperationCount'] + assert ps.write_count == win['WriteOperationCount'] + assert ps.read_bytes == win['ReadTransferCount'] + assert ps.write_bytes == win['WriteTransferCount'] + assert ps.other_count == win['OtherOperationCount'] + assert ps.other_bytes == win['OtherTransferCount'] def test_num_handles(self): import ctypes @@ -571,9 +557,9 @@ def test_num_handles(self): ctypes.windll.kernel32.GetProcessHandleCount( handle, ctypes.byref(hndcnt) ) - sys_value = hndcnt.value - psutil_value = psutil.Process(self.pid).num_handles() - assert psutil_value == sys_value + win = hndcnt.value + ps = psutil.Process(self.pid).num_handles() + assert ps == win def test_error_partial_copy(self): # https://github.com/giampaolo/psutil/issues/875 From d2f38b1be06e4db1d23b830756f3d4602e267f48 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 18:56:16 +0100 Subject: [PATCH 1531/1714] test_windows.py refact: use OpenHandle utility fun --- tests/test_windows.py | 57 +++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/tests/test_windows.py b/tests/test_windows.py index f2c8873fc8..2f8ee57284 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -37,6 +37,9 @@ from . import terminate if WINDOWS and not PYPY: + import ctypes + import ctypes.wintypes + with warnings.catch_warnings(): warnings.simplefilter("ignore") import win32api # requires "pip install pywin32" @@ -54,7 +57,15 @@ @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") @pytest.mark.skipif(PYPY, reason="pywin32 not available on PYPY") class WindowsTestCase(PsutilTestCase): - pass + + def OpenProcess(self, pid=None): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, + win32con.FALSE, + pid or os.getpid(), + ) + self.addCleanup(win32api.CloseHandle, handle) + return handle def powershell(cmd): @@ -427,12 +438,9 @@ def test_send_signal(self): 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() - ) + self.OpenProcess() after = p.num_handles() assert after == before + 1 - win32api.CloseHandle(handle) assert p.num_handles() == before def test_ctrl_signals(self): @@ -469,30 +477,18 @@ def test_cmdline(self): # XXX - occasional failures # def test_cpu_times(self): - # handle = win32api.OpenProcess( - # win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() - # ) - # self.addCleanup(win32api.CloseHandle, handle) # a = psutil.Process().cpu_times() - # b = win32process.GetProcessTimes(handle) + # b = win32process.GetProcessTimes(self.OpenProcess()) # assert abs(a.user - b['UserTime'] / 10000000.0) < 0.2 # assert abs(a.user - b['KernelTime'] / 10000000.0) < 0.2 def test_nice(self): - handle = win32api.OpenProcess( - win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() - ) - self.addCleanup(win32api.CloseHandle, handle) - win = win32process.GetPriorityClass(handle) + win = win32process.GetPriorityClass(self.OpenProcess()) ps = psutil.Process().nice() assert ps == win def test_memory_info(self): - handle = win32api.OpenProcess( - win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid - ) - self.addCleanup(win32api.CloseHandle, handle) - win = win32process.GetProcessMemoryInfo(handle) + win = win32process.GetProcessMemoryInfo(self.OpenProcess(self.pid)) ps = psutil.Process(self.pid).memory_info() assert ps.peak_wset == win['PeakWorkingSetSize'] assert ps.wset == win['WorkingSetSize'] @@ -507,34 +503,24 @@ def test_memory_info(self): assert ps.vms == ps.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() ps = p.wait() - win = win32process.GetExitCodeProcess(handle) + win = win32process.GetExitCodeProcess(self.OpenProcess(self.pid)) assert ps == win 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 + win = from_bitmask( + win32process.GetProcessAffinityMask(self.OpenProcess(self.pid))[0] ) - self.addCleanup(win32api.CloseHandle, handle) - win = from_bitmask(win32process.GetProcessAffinityMask(handle)[0]) ps = psutil.Process(self.pid).cpu_affinity() assert ps == win def test_io_counters(self): - handle = win32api.OpenProcess( - win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() - ) - self.addCleanup(win32api.CloseHandle, handle) - win = win32process.GetProcessIoCounters(handle) + win = win32process.GetProcessIoCounters(self.OpenProcess()) ps = psutil.Process().io_counters() assert ps.read_count == win['ReadOperationCount'] assert ps.write_count == win['WriteOperationCount'] @@ -544,9 +530,6 @@ def test_io_counters(self): assert ps.other_bytes == win['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 From 7f2a5a7ee828b83d77d16aafe6dae655061160bc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 2 Mar 2026 19:29:08 +0100 Subject: [PATCH 1532/1714] Windows: disable pytest cache due to permission errs + fix win test --- Makefile | 3 ++- tests/test_windows.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 889e1f0503..f824bd46d2 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,8 @@ install-git-hooks: ## Install GIT pre-commit hook. # Tests # =================================================================== -RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest +# disable cache on Windows because it causes "Permission denied" errors +RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(if $(filter $(OS),Windows_NT),-p no:cacheprovider,) test: ## Run all tests (except memleak tests). # To run a specific test do `make test ARGS=tests/test_process.py::TestProcess::test_cmdline` diff --git a/tests/test_windows.py b/tests/test_windows.py index 2f8ee57284..8a3f8a9a14 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -438,9 +438,10 @@ def test_send_signal(self): def test_num_handles_increment(self): p = psutil.Process(os.getpid()) before = p.num_handles() - self.OpenProcess() + handle = self.OpenProcess() after = p.num_handles() assert after == before + 1 + win32api.CloseHandle(handle) assert p.num_handles() == before def test_ctrl_signals(self): From aa81dc8fc5098b4d3bcd0433e12e0fc1dddfa76d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 3 Mar 2026 09:32:18 +0100 Subject: [PATCH 1533/1714] Try to fix Makefile for BSD --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f824bd46d2..4bd9e8a05e 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,8 @@ install-git-hooks: ## Install GIT pre-commit hook. # =================================================================== # disable cache on Windows because it causes "Permission denied" errors -RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(if $(filter $(OS),Windows_NT),-p no:cacheprovider,) +_PYTEST_EXTRA != if [ "$$OS" = "Windows_NT" ]; then printf '%s' '-p no:cacheprovider'; fi +RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(_PYTEST_EXTRA) test: ## Run all tests (except memleak tests). # To run a specific test do `make test ARGS=tests/test_process.py::TestProcess::test_cmdline` From f7cd6cbc13e78512184a29ab00c4c632cd7b162d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 3 Mar 2026 10:02:46 +0100 Subject: [PATCH 1534/1714] Refact: avoid boilerplate when populating lists in C (#2734) --- psutil/_psutil_aix.c | 72 +++++++---------- psutil/arch/all/init.h | 5 +- psutil/arch/all/pids.c | 8 +- psutil/arch/all/utils.c | 50 +++++++++++- psutil/arch/bsd/disk.c | 24 +++--- psutil/arch/bsd/proc.c | 9 +-- psutil/arch/freebsd/cpu.c | 27 +++---- psutil/arch/freebsd/proc.c | 70 +++++++---------- psutil/arch/freebsd/proc_socks.c | 56 +++++++------ psutil/arch/freebsd/sys_socks.c | 61 +++++++-------- psutil/arch/linux/disk.c | 23 +++--- psutil/arch/linux/proc.c | 8 +- psutil/arch/netbsd/cpu.c | 25 +++--- psutil/arch/netbsd/proc.c | 31 +++----- psutil/arch/netbsd/socks.c | 31 ++++---- psutil/arch/openbsd/cpu.c | 25 +++--- psutil/arch/openbsd/proc.c | 29 +++---- psutil/arch/openbsd/socks.c | 62 +++++++-------- psutil/arch/openbsd/users.c | 25 +++--- psutil/arch/osx/cpu.c | 22 +++--- psutil/arch/osx/disk.c | 23 +++--- psutil/arch/osx/proc.c | 99 +++++++++++------------ psutil/arch/posix/net.c | 41 +++------- psutil/arch/posix/users.c | 26 +++--- psutil/arch/sunos/cpu.c | 23 +++--- psutil/arch/sunos/disk.c | 23 +++--- psutil/arch/sunos/net.c | 118 ++++++++++++++-------------- psutil/arch/sunos/proc.c | 29 +++---- psutil/arch/windows/cpu.c | 13 +-- psutil/arch/windows/disk.c | 44 +++++------ psutil/arch/windows/net.c | 58 ++++++-------- psutil/arch/windows/proc.c | 51 +++++------- psutil/arch/windows/proc_handles.c | 16 ++-- psutil/arch/windows/services.c | 8 +- psutil/arch/windows/socks.c | 122 ++++++++++++++--------------- psutil/arch/windows/sys.c | 22 +++--- 36 files changed, 620 insertions(+), 759 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index e73a145d4b..ccfc15c889 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -162,7 +162,6 @@ static PyObject * psutil_proc_args(PyObject *self, PyObject *args) { int pid; PyObject *py_retlist = PyList_New(0); - PyObject *py_arg = NULL; struct procsinfo procbuf; long arg_max; char *argbuf = NULL; @@ -192,12 +191,8 @@ psutil_proc_args(PyObject *self, PyObject *args) { * even if the buffer is not big enough (even though it is supposed * to be) so the following 'while' is safe */ while (*curarg != '\0') { - py_arg = PyUnicode_DecodeFSDefault(curarg); - if (!py_arg) + if (!pylist_append_obj(py_retlist, PyUnicode_DecodeFSDefault(curarg))) goto error; - if (PyList_Append(py_retlist, py_arg)) - goto error; - Py_DECREF(py_arg); curarg = strchr(curarg, '\0') + 1; } @@ -209,7 +204,6 @@ psutil_proc_args(PyObject *self, PyObject *args) { if (argbuf != NULL) free(argbuf); Py_XDECREF(py_retlist); - Py_XDECREF(py_arg); return NULL; } @@ -295,7 +289,6 @@ static PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { long pid; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; perfstat_thread_t *threadt = NULL; perfstat_id_t id; int i, rc, thread_count; @@ -334,20 +327,21 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (threadt[i].pid != pid) continue; - py_tuple = Py_BuildValue( - "Idd", threadt[i].tid, threadt[i].ucpu_time, threadt[i].scpu_time - ); - if (py_tuple == NULL) + if (!pylist_append_fmt( + py_retlist, + "Idd", + threadt[i].tid, + threadt[i].ucpu_time, + threadt[i].scpu_time + )) + { goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); + } } free(threadt); return py_retlist; error: - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (threadt != NULL) free(threadt); @@ -489,7 +483,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { struct mntent *mt = NULL; PyObject *py_dev = NULL; PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -508,20 +501,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { py_mountp = PyUnicode_DecodeFSDefault(mt->mnt_dir); if (!py_mountp) goto error; - py_tuple = Py_BuildValue( - "(OOss)", - py_dev, // device - py_mountp, // mount point - mt->mnt_type, // fs type - mt->mnt_opts // options - ); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OOss)", + py_dev, // device + py_mountp, // mount point + mt->mnt_type, // fs type + mt->mnt_opts // options + )) + { goto error; + } Py_CLEAR(py_dev); Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); mt = getmntent(file); } endmntent(file); @@ -530,7 +522,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { error: Py_XDECREF(py_dev); Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (file != NULL) endmntent(file); @@ -701,7 +692,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { perfstat_cpu_t *cpu = NULL; perfstat_id_t id; PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; if (py_retlist == NULL) return NULL; @@ -736,24 +726,22 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } for (i = 0; i < ncpu; i++) { - py_cputime = Py_BuildValue( - "(dddd)", - (double)cpu[i].user / ticks, - (double)cpu[i].sys / ticks, - (double)cpu[i].idle / ticks, - (double)cpu[i].wait / ticks - ); - if (!py_cputime) - goto error; - if (PyList_Append(py_retlist, py_cputime)) + if (!pylist_append_fmt( + py_retlist, + "(dddd)", + (double)cpu[i].user / ticks, + (double)cpu[i].sys / ticks, + (double)cpu[i].idle / ticks, + (double)cpu[i].wait / ticks + )) + { goto error; - Py_DECREF(py_cputime); + } } free(cpu); return py_retlist; error: - Py_XDECREF(py_cputime); Py_DECREF(py_retlist); if (cpu != NULL) free(cpu); diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index 2e607a59b3..b027b824c8 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -128,9 +128,12 @@ int str_append(char *dst, size_t dst_size, const char *src); int str_copy(char *dst, size_t dst_size, const char *src); int str_format(char *buf, size_t size, const char *fmt, ...); +int pydict_add(PyObject *dict, const char *key, const char *fmt, ...); +int pylist_append_fmt(PyObject *list, const char *fmt, ...); +int pylist_append_obj(PyObject *list, PyObject *obj); + int psutil_badargs(const char *funcname); int psutil_setup(void); -int pydict_add(PyObject *dict, const char *key, const char *fmt, ...); // ==================================================================== // --- Exposed to Python diff --git a/psutil/arch/all/pids.c b/psutil/arch/all/pids.c index 74b1f309d0..858aa09279 100644 --- a/psutil/arch/all/pids.c +++ b/psutil/arch/all/pids.c @@ -41,7 +41,6 @@ psutil_pids(PyObject *self, PyObject *args) { int pids_count = 0; int i; PyObject *py_retlist = PyList_New(0); - PyObject *py_pid = NULL; if (!py_retlist) return NULL; @@ -55,19 +54,14 @@ psutil_pids(PyObject *self, PyObject *args) { } for (i = 0; i < pids_count; i++) { - py_pid = PyLong_FromPid(pids_array[i]); - if (!py_pid) + if (!pylist_append_obj(py_retlist, PyLong_FromPid(pids_array[i]))) goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_CLEAR(py_pid); } free(pids_array); return py_retlist; error: - Py_XDECREF(py_pid); Py_DECREF(py_retlist); free(pids_array); return NULL; diff --git a/psutil/arch/all/utils.c b/psutil/arch/all/utils.c index cb0dcf8613..b0e4fbe4c6 100644 --- a/psutil/arch/all/utils.c +++ b/psutil/arch/all/utils.c @@ -2,9 +2,53 @@ #include -// Build a Python object from a Py_BuildValue format string and set it -// as key in an existing dict. Returns 1 on success, 0 on failure with -// a Python exception set. +// Build a Python object from a format string, append it to a list, +// then decref it. Eliminates the need for a temporary variable, a NULL +// check, and a Py_DECREF / Py_XDECREF at the error label. Returns 1 on +// success, 0 on failure with a Python exception set. +int +pylist_append_fmt(PyObject *list, const char *fmt, ...) { + int ret = 0; // 0 = failure + PyObject *obj = NULL; + va_list ap; + + va_start(ap, fmt); + obj = Py_VaBuildValue(fmt, ap); + va_end(ap); + + if (!obj) + return 0; + if (PyList_Append(list, obj) < 0) + goto done; + ret = 1; // success + +done: + Py_DECREF(obj); + return ret; +} + + +// Append a pre-built Python object to a list, then decref it. Same as +// pylist_append_fmt() but takes an already-built object instead of a +// format string. Returns 1 on success, 0 on failure with a Python +// exception set. +int +pylist_append_obj(PyObject *list, PyObject *obj) { + if (!obj) + return 0; + if (PyList_Append(list, obj) < 0) { + Py_DECREF(obj); + return 0; + } + Py_DECREF(obj); + return 1; +} + + +// Build a Python object from a format string, set it as a key in a +// dict, then decref it. Same idea as pylist_append_fmt() but for +// dicts. Returns 1 on success, 0 on failure with a Python exception +// set. int pydict_add(PyObject *dict, const char *key, const char *fmt, ...) { int ret = 0; // 0 = failure diff --git a/psutil/arch/bsd/disk.c b/psutil/arch/bsd/disk.c index 7d6d000b38..1d7efc9d6b 100644 --- a/psutil/arch/bsd/disk.c +++ b/psutil/arch/bsd/disk.c @@ -32,7 +32,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { PyObject *py_retlist = PyList_New(0); PyObject *py_dev = NULL; PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; if (py_retlist == NULL) return NULL; @@ -70,7 +69,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } for (i = 0; i < num; i++) { - py_tuple = NULL; opts[0] = 0; #ifdef PSUTIL_NETBSD flags = fs[i].f_flag; @@ -152,20 +150,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); if (!py_mountp) goto error; - py_tuple = Py_BuildValue( - "(OOss)", - py_dev, // device - py_mountp, // mount point - fs[i].f_fstypename, // fs type - opts // options - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OOss)", + py_dev, // device + py_mountp, // mount point + fs[i].f_fstypename, // fs type + opts // options + )) + { goto error; + } Py_CLEAR(py_dev); Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); } free(fs); @@ -174,7 +171,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { error: Py_XDECREF(py_dev); Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (fs != NULL) free(fs); diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index a3a0221bf4..2ab6c0c917 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -352,7 +352,6 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { #else struct kinfo_proc kipp; #endif - PyObject *py_tuple = NULL; PyObject *py_path = NULL; PyObject *py_retlist = PyList_New(0); @@ -404,20 +403,16 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { py_path = PyUnicode_DecodeFSDefault(path); if (!py_path) goto error; - py_tuple = Py_BuildValue("(Oi)", py_path, fd); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt(py_retlist, "(Oi)", py_path, fd)) goto error; Py_CLEAR(py_path); - Py_CLEAR(py_tuple); } } free(freep); return py_retlist; error: - Py_XDECREF(py_tuple); + Py_XDECREF(py_path); Py_DECREF(py_retlist); if (freep != NULL) free(freep); diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index bf1730f13e..623c699fb7 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -30,7 +30,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { int ncpu; size_t size; PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; if (py_retlist == NULL) return NULL; @@ -57,31 +56,25 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } for (int i = 0; i < ncpu; i++) { - py_cputime = Py_BuildValue( - "(ddddd)", - (double)cpu_time[i][CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[i][CP_INTR] / CLOCKS_PER_SEC - ); - if (!py_cputime) { + if (!pylist_append_fmt( + py_retlist, + "(ddddd)", + (double)cpu_time[i][CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[i][CP_INTR] / CLOCKS_PER_SEC + )) + { free(cpu_time); goto error; } - if (PyList_Append(py_retlist, py_cputime)) { - Py_DECREF(py_cputime); - free(cpu_time); - goto error; - } - Py_DECREF(py_cputime); } free(cpu_time); return py_retlist; error: - Py_XDECREF(py_cputime); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 39dd15b23b..c0306af0f5 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -46,7 +46,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { size_t size = 0; size_t pos = 0; PyObject *py_retlist = PyList_New(0); - PyObject *py_arg = NULL; if (py_retlist == NULL) return NULL; @@ -66,13 +65,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { // separator if (size > 0) { while (pos < size) { - py_arg = PyUnicode_DecodeFSDefault(&procargs[pos]); - if (!py_arg) + if (!pylist_append_obj( + py_retlist, PyUnicode_DecodeFSDefault(&procargs[pos]) + )) goto error; - if (PyList_Append(py_retlist, py_arg)) - goto error; - Py_DECREF(py_arg); - py_arg = NULL; pos += strlen(&procargs[pos]) + 1; } } @@ -81,7 +77,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_arg); Py_XDECREF(py_retlist); if (procargs != NULL) free(procargs); @@ -164,7 +159,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { unsigned int i; size_t size = 0; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; if (py_retlist == NULL) return NULL; @@ -188,24 +182,22 @@ psutil_proc_threads(PyObject *self, PyObject *args) { for (i = 0; i < size / sizeof(*kip); i++) { kipp = &kip[i]; - py_tuple = Py_BuildValue( - "Idd", - kipp->ki_tid, - PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_utime), - PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_stime) - ); - if (py_tuple == NULL) + if (!pylist_append_fmt( + py_retlist, + "Idd", + kipp->ki_tid, + PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_utime), + PSUTIL_TV2DOUBLE(kipp->ki_rusage.ru_stime) + )) + { goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); + } } free(kip); return py_retlist; error: - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); free(kip); return NULL; @@ -299,7 +291,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { struct kinfo_vmentry *freep = NULL; struct kinfo_vmentry *kve; ptrwidth = 2 * sizeof(void *); - PyObject *py_tuple = NULL; PyObject *py_path = NULL; PyObject *py_retlist = PyList_New(0); @@ -317,7 +308,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { goto error; } for (i = 0; i < cnt; i++) { - py_tuple = NULL; kve = &freep[i]; addr[0] = '\0'; perms[0] = '\0'; @@ -390,28 +380,27 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { py_path = PyUnicode_DecodeFSDefault(path); if (!py_path) goto error; - py_tuple = Py_BuildValue( - "ssOiiii", - addr, // "start-end" address - perms, // "rwx" permissions - py_path, // path - kve->kve_resident, // rss - kve->kve_private_resident, // private - kve->kve_ref_count, // ref count - kve->kve_shadow_count // shadow count - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "ssOiiii", + addr, // "start-end" address + perms, // "rwx" permissions + py_path, // path + kve->kve_resident, // rss + kve->kve_private_resident, // private + kve->kve_ref_count, // ref count + kve->kve_shadow_count // shadow count + )) + { goto error; + } Py_DECREF(py_path); - Py_DECREF(py_tuple); + py_path = NULL; } free(freep); return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_path); Py_DECREF(py_retlist); if (freep != NULL) @@ -430,7 +419,6 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { int i; cpuset_t mask; PyObject *py_retlist; - PyObject *py_cpu_num; if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; @@ -446,10 +434,7 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { for (i = 0; i < CPU_SETSIZE; i++) { if (CPU_ISSET(i, &mask)) { - py_cpu_num = Py_BuildValue("i", i); - if (py_cpu_num == NULL) - goto error; - if (PyList_Append(py_retlist, py_cpu_num)) + if (!pylist_append_fmt(py_retlist, "i", i)) goto error; } } @@ -457,7 +442,6 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_cpu_num); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 1623ff5eff..4b5dd5ff29 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -215,7 +215,6 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { #endif PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; PyObject *py_af_filter = NULL; @@ -254,7 +253,6 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; char path[PATH_MAX]; int inseq; - py_tuple = NULL; py_laddr = NULL; py_raddr = NULL; @@ -341,20 +339,21 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { py_raddr = Py_BuildValue("()"); if (!py_raddr) goto error; - py_tuple = Py_BuildValue( - "(iiiNNi)", - kif->kf_fd, - kif->kf_sock_domain, - kif->kf_sock_type, - py_laddr, - py_raddr, - state - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNi)", + kif->kf_fd, + kif->kf_sock_domain, + kif->kf_sock_type, + py_laddr, + py_raddr, + state + )) + { goto error; - Py_DECREF(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } // UNIX socket. // Note: remote path cannot be determined. @@ -379,21 +378,21 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (!py_laddr) goto error; - py_tuple = Py_BuildValue( - "(iiiOsi)", - kif->kf_fd, - kif->kf_sock_domain, - kif->kf_sock_type, - py_laddr, - "", // raddr can't be determined - PSUTIL_CONN_NONE - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiOsi)", + kif->kf_fd, + kif->kf_sock_domain, + kif->kf_sock_type, + py_laddr, + "", // raddr can't be determined + PSUTIL_CONN_NONE + )) + { goto error; - Py_DECREF(py_tuple); + } Py_DECREF(py_laddr); + py_laddr = NULL; } } } @@ -402,7 +401,6 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); Py_DECREF(py_retlist); diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index c89a2a129f..99c8d61860 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -94,7 +94,6 @@ psutil_gather_inet( int retry; int type; - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; @@ -214,28 +213,28 @@ psutil_gather_inet( py_raddr = Py_BuildValue("()"); if (!py_raddr) goto error; - py_tuple = Py_BuildValue( - "iiiNNi" _Py_PARSE_PID, - xf->xf_fd, // fd - family, // family - type, // type - py_laddr, // laddr - py_raddr, // raddr - status, // status - xf->xf_pid // pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "iiiNNi" _Py_PARSE_PID, + xf->xf_fd, // fd + family, // family + type, // type + py_laddr, // laddr + py_raddr, // raddr + status, // status + xf->xf_pid // pid + )) + { goto error; - Py_CLEAR(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } free(buf); return 0; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); free(buf); @@ -261,7 +260,6 @@ psutil_gather_unix( struct sockaddr_un *sun; char path[PATH_MAX]; - PyObject *py_tuple = NULL; PyObject *py_lpath = NULL; switch (proto) { @@ -331,29 +329,28 @@ psutil_gather_unix( if (!py_lpath) goto error; - py_tuple = Py_BuildValue( - "(iiiOsii)", - xf->xf_fd, // fd - AF_UNIX, // family - proto, // type - py_lpath, // lpath - "", // rath - PSUTIL_CONN_NONE, // status - xf->xf_pid // pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiOsii)", + xf->xf_fd, // fd + AF_UNIX, // family + proto, // type + py_lpath, // lpath + "", // rath + PSUTIL_CONN_NONE, // status + xf->xf_pid // pid + )) + { goto error; + } Py_DECREF(py_lpath); - Py_DECREF(py_tuple); + py_lpath = NULL; } free(buf); return 0; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_lpath); free(buf); return -1; diff --git a/psutil/arch/linux/disk.c b/psutil/arch/linux/disk.c index 81a608cd10..2fe1b4a8ef 100644 --- a/psutil/arch/linux/disk.c +++ b/psutil/arch/linux/disk.c @@ -19,7 +19,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { char *mtab_path; PyObject *py_dev = NULL; PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -48,20 +47,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { py_mountp = PyUnicode_DecodeFSDefault(entry->mnt_dir); if (!py_mountp) goto error; - py_tuple = Py_BuildValue( - "(OOss)", - py_dev, // device - py_mountp, // mount point - entry->mnt_type, // fs type - entry->mnt_opts // options - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OOss)", + py_dev, // device + py_mountp, // mount point + entry->mnt_type, // fs type + entry->mnt_opts // options + )) + { goto error; + } Py_CLEAR(py_dev); Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); } endmntent(file); return py_retlist; @@ -71,7 +69,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { endmntent(file); Py_XDECREF(py_dev); Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index f608bffdc5..efcaa26735 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -127,14 +127,8 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { cpucount_s = CPU_COUNT_S(setsize, mask); for (cpu = 0, count = cpucount_s; count; cpu++) { if (CPU_ISSET_S(cpu, setsize, mask)) { - PyObject *cpu_num = PyLong_FromLong(cpu); - if (cpu_num == NULL) + if (!pylist_append_obj(py_list, PyLong_FromLong(cpu))) goto error; - if (PyList_Append(py_list, cpu_num)) { - Py_DECREF(cpu_num); - goto error; - } - Py_DECREF(cpu_num); --count; } } diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c index f87485b06a..94eb85517f 100644 --- a/psutil/arch/netbsd/cpu.c +++ b/psutil/arch/netbsd/cpu.c @@ -50,7 +50,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { size_t len; size_t size; int i; - PyObject *py_cputime = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -71,25 +70,23 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { mib[2] = i; if (psutil_sysctl(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) goto error; - py_cputime = Py_BuildValue( - "(ddddd)", - (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC - ); - if (!py_cputime) + if (!pylist_append_fmt( + py_retlist, + "(ddddd)", + (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC + )) + { goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_DECREF(py_cputime); + } } return py_retlist; error: - Py_XDECREF(py_cputime); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 6f8d3a478f..1c191ba626 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -129,7 +129,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { size_t size; struct kinfo_lwp *kl = NULL; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; if (py_retlist == NULL) return NULL; @@ -170,24 +169,22 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (kl[i].l_stat == LSIDL || kl[i].l_stat == LSZOMB) continue; // XXX: return 2 "user" times, no "system" time available - py_tuple = Py_BuildValue( - "idd", - kl[i].l_lid, - PSUTIL_KPT2DOUBLE(kl[i].l_rtime), - PSUTIL_KPT2DOUBLE(kl[i].l_rtime) - ); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "idd", + kl[i].l_lid, + PSUTIL_KPT2DOUBLE(kl[i].l_rtime), + PSUTIL_KPT2DOUBLE(kl[i].l_rtime) + )) + { goto error; - Py_DECREF(py_tuple); + } } free(kl); return py_retlist; error: - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (kl != NULL) free(kl); @@ -206,7 +203,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { size_t pos = 0; char *procargs = NULL; PyObject *py_retlist = PyList_New(0); - PyObject *py_arg = NULL; if (py_retlist == NULL) return NULL; @@ -246,12 +242,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (len > 0) { while (pos < len) { - py_arg = PyUnicode_DecodeFSDefault(&procargs[pos]); - if (!py_arg) - goto error; - if (PyList_Append(py_retlist, py_arg)) + if (!pylist_append_obj( + py_retlist, PyUnicode_DecodeFSDefault(&procargs[pos]) + )) goto error; - Py_DECREF(py_arg); pos = pos + strlen(&procargs[pos]) + 1; } } @@ -260,7 +254,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_arg); Py_DECREF(py_retlist); if (procargs != NULL) free(procargs); diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index 349493b237..d383027cc0 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -316,7 +316,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { int32_t rport; int32_t status; pid_t pid; - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; PyObject *py_retlist = PyList_New(0); @@ -422,23 +421,24 @@ psutil_net_connections(PyObject *self, PyObject *args) { } // append tuple to list - py_tuple = Py_BuildValue( - "(iiiOOii)", - k->kif->ki_fd, - kp->kpcb->ki_family, - kp->kpcb->ki_type, - py_laddr, - py_raddr, - status, - k->kif->ki_pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiOOii)", + k->kif->ki_fd, + kp->kpcb->ki_family, + kp->kpcb->ki_type, + py_laddr, + py_raddr, + status, + k->kif->ki_pid + )) + { goto error; + } Py_DECREF(py_laddr); + py_laddr = NULL; Py_DECREF(py_raddr); - Py_DECREF(py_tuple); + py_raddr = NULL; } } @@ -450,7 +450,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { psutil_kiflist_clear(); psutil_kpcblist_clear(); Py_DECREF(py_retlist); - Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); return NULL; diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c index a14116ef3a..1e75ade27d 100644 --- a/psutil/arch/openbsd/cpu.c +++ b/psutil/arch/openbsd/cpu.c @@ -19,7 +19,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { size_t len; int i; PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; if (py_retlist == NULL) return NULL; @@ -40,25 +39,23 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { if (psutil_sysctl(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) goto error; - py_cputime = Py_BuildValue( - "(ddddd)", - (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, - (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, - (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, - (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC - ); - if (!py_cputime) + if (!pylist_append_fmt( + py_retlist, + "(ddddd)", + (double)cpu_time[CP_USER] / CLOCKS_PER_SEC, + (double)cpu_time[CP_NICE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_SYS] / CLOCKS_PER_SEC, + (double)cpu_time[CP_IDLE] / CLOCKS_PER_SEC, + (double)cpu_time[CP_INTR] / CLOCKS_PER_SEC + )) + { goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_DECREF(py_cputime); + } } return py_retlist; error: - Py_XDECREF(py_cputime); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 09f3055eda..1f44c1f921 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -26,7 +26,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { char **argv = NULL; char **p; PyObject *py_retlist = PyList_New(0); - PyObject *py_arg = NULL; if (py_retlist == NULL) return NULL; @@ -44,12 +43,8 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { argv = (char **)argv_buf; for (p = argv; *p != NULL; p++) { - py_arg = PyUnicode_DecodeFSDefault(*p); - if (!py_arg) + if (!pylist_append_obj(py_retlist, PyUnicode_DecodeFSDefault(*p))) goto error; - if (PyList_Append(py_retlist, py_arg)) - goto error; - Py_DECREF(py_arg); } free(argv_buf); @@ -58,7 +53,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { error: if (argv_buf != NULL) free(argv_buf); - Py_XDECREF(py_arg); Py_DECREF(py_retlist); return NULL; } @@ -76,7 +70,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { char errbuf[4096]; struct kinfo_proc *kp; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; if (py_retlist == NULL) return NULL; @@ -115,17 +108,16 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (kp[i].p_tid < 0) continue; if (kp[i].p_pid == pid) { - py_tuple = Py_BuildValue( - _Py_PARSE_PID "dd", - kp[i].p_tid, - PSUTIL_KPT2DOUBLE(kp[i].p_uutime), - PSUTIL_KPT2DOUBLE(kp[i].p_ustime) - ); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + _Py_PARSE_PID "dd", + kp[i].p_tid, + PSUTIL_KPT2DOUBLE(kp[i].p_uutime), + PSUTIL_KPT2DOUBLE(kp[i].p_ustime) + )) + { goto error; - Py_DECREF(py_tuple); + } } } @@ -133,7 +125,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (kd != NULL) kvm_close(kd); diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c index ab4ad51cac..d6221f4bac 100644 --- a/psutil/arch/openbsd/socks.c +++ b/psutil/arch/openbsd/socks.c @@ -37,7 +37,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct in6_addr laddr6; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; PyObject *py_lpath = NULL; @@ -74,7 +73,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { for (int i = 0; i < cnt; i++) { const struct kinfo_file *kif = ikf + i; - py_tuple = NULL; py_laddr = NULL; py_raddr = NULL; py_lpath = NULL; @@ -125,21 +123,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { goto error; // populate tuple and list - py_tuple = Py_BuildValue( - "(iiiNNil)", - kif->fd_fd, - kif->so_family, - kif->so_type, - py_laddr, - py_raddr, - state, - kif->p_pid - ); - if (!py_tuple) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNil)", + kif->fd_fd, + kif->so_family, + kif->so_type, + py_laddr, + py_raddr, + state, + kif->p_pid + )) + { goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } // UNIX socket else if (kif->so_family == AF_UNIX) { @@ -147,23 +146,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (!py_lpath) goto error; - py_tuple = Py_BuildValue( - "(iiiOsil)", - kif->fd_fd, - kif->so_family, - kif->so_type, - py_lpath, - "", // raddr - PSUTIL_CONN_NONE, - kif->p_pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiOsil)", + kif->fd_fd, + kif->so_family, + kif->so_type, + py_lpath, + "", // raddr + PSUTIL_CONN_NONE, + kif->p_pid + )) + { goto error; + } Py_DECREF(py_lpath); - Py_DECREF(py_tuple); - Py_INCREF(Py_None); + py_lpath = NULL; } } @@ -171,9 +169,9 @@ psutil_net_connections(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); + Py_XDECREF(py_lpath); Py_DECREF(py_retlist); if (kd != NULL) kvm_close(kd); diff --git a/psutil/arch/openbsd/users.c b/psutil/arch/openbsd/users.c index 8fa1eb6cd7..0b943b7161 100644 --- a/psutil/arch/openbsd/users.c +++ b/psutil/arch/openbsd/users.c @@ -17,7 +17,6 @@ psutil_users(PyObject *self, PyObject *args) { PyObject *py_username = NULL; PyObject *py_tty = NULL; PyObject *py_hostname = NULL; - PyObject *py_tuple = NULL; if (py_retlist == NULL) return NULL; @@ -45,22 +44,21 @@ psutil_users(PyObject *self, PyObject *args) { py_hostname = PyUnicode_DecodeFSDefault(ut.ut_host); if (!py_hostname) goto error; - py_tuple = Py_BuildValue( - "(OOOdO)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut.ut_time, // start time - Py_None // pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OOOdO)", + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)ut.ut_time, // start time + Py_None // pid + )) + { goto error; + } Py_CLEAR(py_username); Py_CLEAR(py_tty); Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); } fclose(fp); @@ -71,7 +69,6 @@ psutil_users(PyObject *self, PyObject *args) { Py_XDECREF(py_username); Py_XDECREF(py_tty); Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index 56accf167c..b152f2f19c 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -306,21 +306,17 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { cpu_load_info = (processor_cpu_load_info_data_t *)info_array; for (natural_t i = 0; i < cpu_count; i++) { - PyObject *py_cputime = Py_BuildValue( - "(dddd)", - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_USER] / CLK_TCK, - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_NICE] / CLK_TCK, - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM] / CLK_TCK, - (double)cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE] / CLK_TCK - ); - if (!py_cputime) { - goto error; - } - if (PyList_Append(py_retlist, py_cputime)) { - Py_DECREF(py_cputime); + if (!pylist_append_fmt( + py_retlist, + "(dddd)", + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_USER] / CLK_TCK, + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_NICE] / CLK_TCK, + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM] / CLK_TCK, + (double)cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE] / CLK_TCK + )) + { goto error; } - Py_DECREF(py_cputime); } vm_deallocate( diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index c08ef24c7c..0775890007 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -35,7 +35,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { struct statfs *fs = NULL; PyObject *py_dev = NULL; PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -133,20 +132,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { py_mountp = PyUnicode_DecodeFSDefault(fs[i].f_mntonname); if (!py_mountp) goto error; - py_tuple = Py_BuildValue( - "(OOss)", - py_dev, // device - py_mountp, // mount point - fs[i].f_fstypename, // fs type - opts // options - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OOss)", + py_dev, // device + py_mountp, // mount point + fs[i].f_fstypename, // fs type + opts // options + )) + { goto error; + } Py_CLEAR(py_dev); Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); } free(fs); @@ -155,7 +153,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { error: Py_XDECREF(py_dev); Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (fs != NULL) free(fs); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 2c836f6cd3..75f7792ff6 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -398,7 +398,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { thread_basic_info_t basic_info_th; mach_msg_type_number_t thread_count, thread_info_count, j; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -448,19 +447,19 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } basic_info_th = (thread_basic_info_t)thinfo_basic; - py_tuple = Py_BuildValue( - "Iff", - j + 1, - basic_info_th->user_time.seconds - + (float)basic_info_th->user_time.microseconds / 1000000.0, - basic_info_th->system_time.seconds - + (float)basic_info_th->system_time.microseconds / 1000000.0 - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "Iff", + j + 1, + basic_info_th->user_time.seconds + + (float)basic_info_th->user_time.microseconds / 1000000.0, + basic_info_th->system_time.seconds + + (float)basic_info_th->system_time.microseconds + / 1000000.0 + )) + { goto error; - Py_CLEAR(py_tuple); + } } // deallocate thread_list if it was allocated @@ -482,7 +481,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_retlist); if (thread_list != NULL) { @@ -519,7 +517,6 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { struct proc_fdinfo *fdp_pointer; struct vnode_fdinfowithpath vi; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_path = NULL; if (py_retlist == NULL) @@ -569,14 +566,12 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { py_path = PyUnicode_DecodeFSDefault(vi.pvip.vip_path); if (!py_path) goto error; - py_tuple = Py_BuildValue( - "(Oi)", py_path, (int)fdp_pointer->proc_fd - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, "(Oi)", py_path, (int)fdp_pointer->proc_fd + )) + { goto error; - Py_CLEAR(py_tuple); + } Py_CLEAR(py_path); // --- /construct python list } @@ -586,7 +581,6 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_path); Py_DECREF(py_retlist); if (fds_pointer != NULL) @@ -613,7 +607,6 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { struct socket_fdinfo si; const char *ntopret; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; PyObject *py_af_filter = NULL; @@ -643,7 +636,6 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { goto error; for (i = 0; i < num_fds; i++) { - py_tuple = NULL; py_laddr = NULL; py_raddr = NULL; fdp_pointer = &fds_pointer[i]; @@ -778,14 +770,21 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (!py_raddr) goto error; - py_tuple = Py_BuildValue( - "(iiiNNi)", fd, family, type, py_laddr, py_raddr, state - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNi)", + fd, + family, + type, + py_laddr, + py_raddr, + state + )) + { goto error; - Py_CLEAR(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } else if (family == AF_UNIX) { py_laddr = PyUnicode_DecodeFSDefault( @@ -799,20 +798,19 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { if (!py_raddr) goto error; - py_tuple = Py_BuildValue( - "(iiiOOi)", - fd, - family, - type, - py_laddr, - py_raddr, - PSUTIL_CONN_NONE - ); - if (!py_tuple) + if (!pylist_append_fmt( + py_retlist, + "(iiiOOi)", + fd, + family, + type, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE + )) + { goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_CLEAR(py_tuple); + } Py_CLEAR(py_laddr); Py_CLEAR(py_raddr); } @@ -823,7 +821,6 @@ psutil_proc_net_connections(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); Py_DECREF(py_retlist); @@ -867,7 +864,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { char *curr_arg; size_t argmax; PyObject *py_retlist = PyList_New(0); - PyObject *py_arg = NULL; if (py_retlist == NULL) return NULL; @@ -915,12 +911,10 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { curr_arg = arg_ptr; while (arg_ptr < arg_end && nargs > 0) { if (*arg_ptr++ == '\0') { - py_arg = PyUnicode_DecodeFSDefault(curr_arg); - if (!py_arg) - goto error; - if (PyList_Append(py_retlist, py_arg)) + if (!pylist_append_obj( + py_retlist, PyUnicode_DecodeFSDefault(curr_arg) + )) goto error; - Py_DECREF(py_arg); // iterate to next arg and decrement # of args curr_arg = arg_ptr; nargs--; @@ -931,7 +925,6 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_arg); Py_XDECREF(py_retlist); if (procargs != NULL) free(procargs); diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index ee8def6ef1..8d9513a004 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -133,7 +133,6 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { int family; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_address = NULL; PyObject *py_netmask = NULL; PyObject *py_broadcast = NULL; @@ -180,21 +179,19 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { if ((py_broadcast == NULL) || (py_ptp == NULL)) goto error; - py_tuple = Py_BuildValue( - "(siOOOO)", - ifa->ifa_name, - family, - py_address, - py_netmask, - py_broadcast, - py_ptp - ); - - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(siOOOO)", + ifa->ifa_name, + family, + py_address, + py_netmask, + py_broadcast, + py_ptp + )) + { goto error; - Py_CLEAR(py_tuple); + } Py_CLEAR(py_address); Py_CLEAR(py_netmask); Py_CLEAR(py_broadcast); @@ -208,7 +205,6 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { if (ifaddr != NULL) freeifaddrs(ifaddr); Py_DECREF(py_retlist); - Py_XDECREF(py_tuple); Py_XDECREF(py_address); Py_XDECREF(py_netmask); Py_XDECREF(py_broadcast); @@ -251,18 +247,7 @@ psutil_net_if_mtu(PyObject *self, PyObject *args) { static int append_flag(PyObject *py_retlist, const char *flag_name) { - PyObject *py_str = NULL; - - py_str = PyUnicode_FromString(flag_name); - if (!py_str) - return 0; - if (PyList_Append(py_retlist, py_str)) { - Py_DECREF(py_str); - return 0; - } - Py_CLEAR(py_str); - - return 1; + return pylist_append_obj(py_retlist, PyUnicode_FromString(flag_name)); } /* diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c index 8c75bf3308..bdf30864ce 100644 --- a/psutil/arch/posix/users.c +++ b/psutil/arch/posix/users.c @@ -34,7 +34,6 @@ psutil_users(PyObject *self, PyObject *args) { PyObject *py_username = NULL; PyObject *py_tty = NULL; PyObject *py_hostname = NULL; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -45,7 +44,6 @@ psutil_users(PyObject *self, PyObject *args) { while ((ut = getutxent()) != NULL) { if (ut->ut_type != USER_PROCESS) continue; - py_tuple = NULL; py_username = PyUnicode_DecodeFSDefault(ut->ut_user); if (!py_username) @@ -72,22 +70,21 @@ psutil_users(PyObject *self, PyObject *args) { if (!py_hostname) goto error; - py_tuple = Py_BuildValue( - "OOOd" _Py_PARSE_PID, - py_username, // username - py_tty, // tty - py_hostname, // hostname - (double)ut->ut_tv.tv_sec, // tstamp - ut->ut_pid // process id - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "OOOd" _Py_PARSE_PID, + py_username, // username + py_tty, // tty + py_hostname, // hostname + (double)ut->ut_tv.tv_sec, // tstamp + ut->ut_pid // process id + )) + { goto error; + } Py_CLEAR(py_username); Py_CLEAR(py_tty); Py_CLEAR(py_hostname); - Py_CLEAR(py_tuple); } teardown(); @@ -98,7 +95,6 @@ psutil_users(PyObject *self, PyObject *args) { Py_XDECREF(py_username); Py_XDECREF(py_tty); Py_XDECREF(py_hostname); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/sunos/cpu.c b/psutil/arch/sunos/cpu.c index e0ca022ebc..b1b08394fe 100644 --- a/psutil/arch/sunos/cpu.c +++ b/psutil/arch/sunos/cpu.c @@ -19,7 +19,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { kstat_t *ksp; cpu_stat_t cs; PyObject *py_retlist = PyList_New(0); - PyObject *py_cputime = NULL; if (py_retlist == NULL) return NULL; @@ -36,18 +35,17 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { psutil_oserror(); goto error; } - py_cputime = Py_BuildValue( - "ffff", - (float)cs.cpu_sysinfo.cpu[CPU_USER], - (float)cs.cpu_sysinfo.cpu[CPU_KERNEL], - (float)cs.cpu_sysinfo.cpu[CPU_IDLE], - (float)cs.cpu_sysinfo.cpu[CPU_WAIT] - ); - if (py_cputime == NULL) + if (!pylist_append_fmt( + py_retlist, + "ffff", + (float)cs.cpu_sysinfo.cpu[CPU_USER], + (float)cs.cpu_sysinfo.cpu[CPU_KERNEL], + (float)cs.cpu_sysinfo.cpu[CPU_IDLE], + (float)cs.cpu_sysinfo.cpu[CPU_WAIT] + )) + { goto error; - if (PyList_Append(py_retlist, py_cputime)) - goto error; - Py_CLEAR(py_cputime); + } } } @@ -55,7 +53,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_cputime); Py_DECREF(py_retlist); if (kc != NULL) kstat_close(kc); diff --git a/psutil/arch/sunos/disk.c b/psutil/arch/sunos/disk.c index d5ced15a30..98ea64693c 100644 --- a/psutil/arch/sunos/disk.c +++ b/psutil/arch/sunos/disk.c @@ -76,7 +76,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { struct mnttab mt; PyObject *py_dev = NULL; PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -95,20 +94,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { py_mountp = PyUnicode_DecodeFSDefault(mt.mnt_mountp); if (!py_mountp) goto error; - py_tuple = Py_BuildValue( - "(OOss)", - py_dev, // device - py_mountp, // mount point - mt.mnt_fstype, // fs type - mt.mnt_mntopts // options - ); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OOss)", + py_dev, // device + py_mountp, // mount point + mt.mnt_fstype, // fs type + mt.mnt_mntopts // options + )) + { goto error; + } Py_CLEAR(py_dev); Py_CLEAR(py_mountp); - Py_CLEAR(py_tuple); } fclose(file); return py_retlist; @@ -116,7 +114,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { error: Py_XDECREF(py_dev); Py_XDECREF(py_mountp); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (file != NULL) fclose(file); diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c index 12353d4c75..8033b80eea 100644 --- a/psutil/arch/sunos/net.c +++ b/psutil/arch/sunos/net.c @@ -281,7 +281,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { struct opthdr mibhdr = {0}; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; @@ -404,21 +403,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { state = tp.tcpConnEntryInfo.ce_state; // add item - py_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_STREAM, - py_laddr, - py_raddr, - state, - processed_pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET, + SOCK_STREAM, + py_laddr, + py_raddr, + state, + processed_pid + )) + { goto error; - Py_CLEAR(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } } #if defined(AF_INET6) @@ -452,21 +452,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { state = tp6.tcp6ConnEntryInfo.ce_state; // add item - py_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_STREAM, - py_laddr, - py_raddr, - state, - processed_pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_STREAM, + py_laddr, + py_raddr, + state, + processed_pid + )) + { goto error; - Py_CLEAR(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } } #endif @@ -494,21 +495,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_raddr = Py_BuildValue("()"); if (!py_raddr) goto error; - py_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_DGRAM, - py_laddr, - py_raddr, - PSUTIL_CONN_NONE, - processed_pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET, + SOCK_DGRAM, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE, + processed_pid + )) + { goto error; - Py_CLEAR(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } } #if defined(AF_INET6) @@ -529,21 +531,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { py_raddr = Py_BuildValue("()"); if (!py_raddr) goto error; - py_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_DGRAM, - py_laddr, - py_raddr, - PSUTIL_CONN_NONE, - processed_pid - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_DGRAM, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE, + processed_pid + )) + { goto error; - Py_CLEAR(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } } #endif @@ -554,7 +557,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); Py_DECREF(py_retlist); diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index 041f8306ff..a52bac0b89 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -489,7 +489,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { uintptr_t stk_base_sz, brk_base_sz; const char *procfs_path; - PyObject *py_tuple = NULL; PyObject *py_path = NULL; PyObject *py_retlist = PyList_New(0); @@ -579,22 +578,21 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { py_path = PyUnicode_DecodeFSDefault(name); if (!py_path) goto error; - py_tuple = Py_BuildValue( - "kksOkkk", - (unsigned long)p->pr_vaddr, - (unsigned long)pr_addr_sz, - perms, - py_path, - (unsigned long)p->pr_rss * p->pr_pagesize, - (unsigned long)p->pr_anon * p->pr_pagesize, - (unsigned long)p->pr_locked * p->pr_pagesize - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "kksOkkk", + (unsigned long)p->pr_vaddr, + (unsigned long)pr_addr_sz, + perms, + py_path, + (unsigned long)p->pr_rss * p->pr_pagesize, + (unsigned long)p->pr_anon * p->pr_pagesize, + (unsigned long)p->pr_locked * p->pr_pagesize + )) + { goto error; + } Py_CLEAR(py_path); - Py_CLEAR(py_tuple); // increment pointer p += 1; @@ -607,7 +605,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { error: if (fd != -1) close(fd); - Py_XDECREF(py_tuple); Py_XDECREF(py_path); Py_DECREF(py_retlist); if (xmap != NULL) diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index c4a5f7f531..a700644d02 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -80,7 +80,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; UINT i; unsigned int ncpus; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -120,7 +119,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // processor value idle = user = kernel = interrupt = dpc = 0; for (i = 0; i < ncpus; i++) { - py_tuple = NULL; user = (double)((HI_T * sppi[i].UserTime.HighPart) + (LO_T * sppi[i].UserTime.LowPart)); idle = (double)((HI_T * sppi[i].IdleTime.HighPart) @@ -136,21 +134,16 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { // we return only busy kernel time subtracting // idle time from kernel time systemt = kernel - idle; - py_tuple = Py_BuildValue( - "(ddddd)", user, systemt, idle, interrupt, dpc - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, "(ddddd)", user, systemt, idle, interrupt, dpc + )) goto error; - Py_CLEAR(py_tuple); } free(sppi); return py_retlist; error: - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (sppi) free(sppi); diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 5310a92de0..ace067a798 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -226,7 +226,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { DWORD lpMaximumComponentLength = 0; // max file name PyObject *py_all; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; if (py_retlist == NULL) { return NULL; @@ -250,7 +249,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } while (*drive_letter != 0) { - py_tuple = NULL; opts[0] = 0; fs_type[0] = 0; @@ -319,23 +317,19 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { mp_path, sizeof(mp_path), mp_buf ); // append mount point - py_tuple = Py_BuildValue( - "(ssss)", - drive_letter, - mp_path, - fs_type, // typically "NTFS" - opts - ); - - if (!py_tuple - || PyList_Append(py_retlist, py_tuple) == -1) + if (!pylist_append_fmt( + py_retlist, + "(ssss)", + drive_letter, + mp_path, + fs_type, // typically "NTFS" + opts + )) { FindVolumeMountPointClose(mp_h); goto error; } - Py_CLEAR(py_tuple); - // Continue looking for more mount points mp_flag = FindNextVolumeMountPoint( mp_h, mp_buf, MAX_PATH @@ -350,18 +344,17 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { str_append(opts, sizeof(opts), ","); str_append(opts, sizeof(opts), psutil_get_drive_type(type)); - py_tuple = Py_BuildValue( - "(ssss)", - drive_letter, - drive_letter, - fs_type, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS - opts - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(ssss)", + drive_letter, + drive_letter, + fs_type, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS + opts + )) + { goto error; - Py_CLEAR(py_tuple); + } goto next; next: @@ -373,7 +366,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { error: SetErrorMode(old_mode); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); return NULL; } diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 5d41f23057..0e0166a75a 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -150,7 +150,6 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_address = NULL; PyObject *py_mac_address = NULL; PyObject *py_nic_name = NULL; @@ -166,7 +165,6 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { while (pCurrAddresses) { Py_CLEAR(py_nic_name); - Py_CLEAR(py_tuple); Py_CLEAR(py_address); Py_CLEAR(py_mac_address); Py_CLEAR(py_netmask); @@ -214,24 +212,20 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { if (py_mac_address == NULL) goto error; - Py_INCREF(Py_None); - Py_INCREF(Py_None); - Py_INCREF(Py_None); - py_tuple = Py_BuildValue( - "(OiOOOO)", - py_nic_name, - -1, // this will be converted later to AF_LINK - py_mac_address, - Py_None, // netmask (not supported) - Py_None, // broadcast (not supported) - Py_None // ptp (not supported on Windows) - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OiOOOO)", + py_nic_name, + -1, // this will be converted later to AF_LINK + py_mac_address, + Py_None, // netmask (not supported) + Py_None, // broadcast (not supported) + Py_None // ptp (not supported on Windows) + )) + { goto error; + } - Py_CLEAR(py_tuple); Py_CLEAR(py_mac_address); } @@ -300,23 +294,20 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { py_netmask = Py_None; } - Py_INCREF(Py_None); - Py_INCREF(Py_None); - py_tuple = Py_BuildValue( - "(OiOOOO)", - py_nic_name, - family, - py_address, - py_netmask, - Py_None, // broadcast (not supported) - Py_None // ptp (not supported on Windows) - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(OiOOOO)", + py_nic_name, + family, + py_address, + py_netmask, + Py_None, // broadcast (not supported) + Py_None // ptp (not supported on Windows) + )) + { goto error; + } - Py_CLEAR(py_tuple); Py_CLEAR(py_address); Py_CLEAR(py_netmask); @@ -335,7 +326,6 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { if (pAddresses) free(pAddresses); Py_XDECREF(py_retlist); - Py_XDECREF(py_tuple); Py_XDECREF(py_address); Py_XDECREF(py_mac_address); Py_XDECREF(py_nic_name); diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 93a034e0f1..ed5d36f87b 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -525,7 +525,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { int rc; FILETIME ftDummy, ftKernel, ftUser; HANDLE hThreadSnap = NULL; - PyObject *py_tuple = NULL; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -565,7 +564,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // If the thread belongs to the process, increase the counter. do { if (te32.th32OwnerProcessID == pid) { - py_tuple = NULL; hThread = NULL; hThread = OpenThread( THREAD_QUERY_INFORMATION, FALSE, te32.th32ThreadID @@ -592,19 +590,18 @@ psutil_proc_threads(PyObject *self, PyObject *args) { * process has executed in user/kernel mode I borrowed the code * below from Python's Modules/posixmodule.c */ - py_tuple = Py_BuildValue( - "kdd", - te32.th32ThreadID, - (double)(ftUser.dwHighDateTime * HI_T - + ftUser.dwLowDateTime * LO_T), - (double)(ftKernel.dwHighDateTime * HI_T - + ftKernel.dwLowDateTime * LO_T) - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "kdd", + te32.th32ThreadID, + (double)(ftUser.dwHighDateTime * HI_T + + ftUser.dwLowDateTime * LO_T), + (double)(ftKernel.dwHighDateTime * HI_T + + ftKernel.dwLowDateTime * LO_T) + )) + { goto error; - Py_CLEAR(py_tuple); + } CloseHandle(hThread); } @@ -614,7 +611,6 @@ psutil_proc_threads(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (hThread != NULL) CloseHandle(hThread); @@ -1118,7 +1114,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { // required by GetMappedFileNameW DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_str = NULL; if (py_retlist == NULL) @@ -1136,7 +1131,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { hProcess, baseAddress, &basicInfo, sizeof(MEMORY_BASIC_INFORMATION) )) { - py_tuple = NULL; if (baseAddress > maxAddr) break; if (GetMappedFileNameW( @@ -1148,19 +1142,17 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { ); if (py_str == NULL) goto error; - py_tuple = Py_BuildValue( - "(KsOI)", - (unsigned long long)baseAddress, - get_region_protection_string(basicInfo.Protect), - py_str, - basicInfo.RegionSize - ); - - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(KsOI)", + (unsigned long long)baseAddress, + get_region_protection_string(basicInfo.Protect), + py_str, + basicInfo.RegionSize + )) + { goto error; - Py_CLEAR(py_tuple); + } Py_CLEAR(py_str); } baseAddress = (PCHAR)baseAddress + basicInfo.RegionSize; @@ -1170,7 +1162,6 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_str); Py_DECREF(py_retlist); if (hProcess != NULL) diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index ab996938ef..eabe70bf4d 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -214,7 +214,6 @@ psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { HANDLE hFile = NULL; ULONG i = 0; BOOLEAN errorOccurred = FALSE; - PyObject *py_path = NULL; PyObject *py_retlist = PyList_New(0); ; @@ -251,14 +250,13 @@ psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { goto error; if ((globalFileName != NULL) && (globalFileName->Length > 0)) { - py_path = PyUnicode_FromWideChar( - globalFileName->Buffer, wcslen(globalFileName->Buffer) - ); - if (!py_path) - goto error; - if (PyList_Append(py_retlist, py_path)) + if (!pylist_append_obj( + py_retlist, + PyUnicode_FromWideChar( + globalFileName->Buffer, wcslen(globalFileName->Buffer) + ) + )) goto error; - Py_CLEAR(py_path); // also sets to NULL } // Loop cleanup section. @@ -284,8 +282,6 @@ psutil_get_open_files(DWORD dwPid, HANDLE hProcess) { FREE(globalFileName); globalFileName = NULL; } - if (py_path != NULL) - Py_DECREF(py_path); if (handlesList != NULL) FREE(handlesList); diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 59178c5363..07c6571ed7 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -136,7 +136,6 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { DWORD dwBytes = 0; DWORD i; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; PyObject *py_name = NULL; PyObject *py_display_name = NULL; @@ -187,14 +186,10 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { goto error; // Construct the result. - py_tuple = Py_BuildValue("(OO)", py_name, py_display_name); - if (py_tuple == NULL) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt(py_retlist, "(OO)", py_name, py_display_name)) goto error; Py_DECREF(py_display_name); Py_DECREF(py_name); - Py_DECREF(py_tuple); } // Free resources. @@ -205,7 +200,6 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { error: Py_DECREF(py_name); Py_XDECREF(py_display_name); - Py_XDECREF(py_tuple); Py_DECREF(py_retlist); if (sc != NULL) CloseServiceHandle(sc); diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 12c2fb153b..29e67aa406 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -114,7 +114,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { CHAR addressBufferRemote[65]; PyObject *py_retlist = NULL; - PyObject *py_conn_tuple = NULL; PyObject *py_af_filter = NULL; PyObject *py_type_filter = NULL; PyObject *py_addr_tuple_local = NULL; @@ -161,7 +160,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { && (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1)) { table = NULL; - py_conn_tuple = NULL; py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; @@ -219,21 +217,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_addr_tuple_remote == NULL) goto error; - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_STREAM, - py_addr_tuple_local, - py_addr_tuple_remote, - tcp4Table->table[i].dwState, - tcp4Table->table[i].dwOwningPid - ); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET, + SOCK_STREAM, + py_addr_tuple_local, + py_addr_tuple_remote, + tcp4Table->table[i].dwState, + tcp4Table->table[i].dwOwningPid + )) + { goto error; - Py_CLEAR(py_conn_tuple); + } + py_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; } free(table); @@ -246,7 +245,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { && (RtlIpv6AddressToStringA != NULL)) { table = NULL; - py_conn_tuple = NULL; py_addr_tuple_local = NULL; py_addr_tuple_remote = NULL; @@ -305,21 +303,22 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_addr_tuple_remote == NULL) goto error; - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_STREAM, - py_addr_tuple_local, - py_addr_tuple_remote, - tcp6Table->table[i].dwState, - tcp6Table->table[i].dwOwningPid - ); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_STREAM, + py_addr_tuple_local, + py_addr_tuple_remote, + tcp6Table->table[i].dwState, + tcp6Table->table[i].dwOwningPid + )) + { goto error; - Py_CLEAR(py_conn_tuple); + } + py_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; } free(table); @@ -332,9 +331,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { && (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1)) { table = NULL; - py_conn_tuple = NULL; py_addr_tuple_local = NULL; - py_addr_tuple_remote = NULL; table = __GetExtendedUdpTable(AF_INET); if (table == NULL) goto error; @@ -366,21 +363,21 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_addr_tuple_local == NULL) goto error; - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET, - SOCK_DGRAM, - py_addr_tuple_local, - PyTuple_New(0), - PSUTIL_CONN_NONE, - udp4Table->table[i].dwOwningPid - ); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET, + SOCK_DGRAM, + py_addr_tuple_local, + PyTuple_New(0), + PSUTIL_CONN_NONE, + udp4Table->table[i].dwOwningPid + )) + { goto error; - Py_CLEAR(py_conn_tuple); + } + py_addr_tuple_local = NULL; } free(table); @@ -394,9 +391,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { && (RtlIpv6AddressToStringA != NULL)) { table = NULL; - py_conn_tuple = NULL; py_addr_tuple_local = NULL; - py_addr_tuple_remote = NULL; table = __GetExtendedUdpTable(AF_INET6); if (table == NULL) goto error; @@ -428,21 +423,21 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (py_addr_tuple_local == NULL) goto error; - py_conn_tuple = Py_BuildValue( - "(iiiNNiI)", - -1, - AF_INET6, - SOCK_DGRAM, - py_addr_tuple_local, - PyTuple_New(0), - PSUTIL_CONN_NONE, - udp6Table->table[i].dwOwningPid - ); - if (!py_conn_tuple) - goto error; - if (PyList_Append(py_retlist, py_conn_tuple)) + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_DGRAM, + py_addr_tuple_local, + PyTuple_New(0), + PSUTIL_CONN_NONE, + udp6Table->table[i].dwOwningPid + )) + { goto error; - Py_CLEAR(py_conn_tuple); + } + py_addr_tuple_local = NULL; } free(table); @@ -454,7 +449,6 @@ psutil_net_connections(PyObject *self, PyObject *args) { error: psutil_conn_decref_objs(); - Py_XDECREF(py_conn_tuple); Py_XDECREF(py_addr_tuple_local); Py_XDECREF(py_addr_tuple_remote); Py_DECREF(py_retlist); diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index 4271a1a168..fbd6706302 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -57,7 +57,6 @@ psutil_users(PyObject *self, PyObject *args) { PWTS_CLIENT_ADDRESS address; char address_str[50]; PWTSINFOW wts_info; - PyObject *py_tuple = NULL; PyObject *py_address = NULL; PyObject *py_username = NULL; PyObject *py_retlist = PyList_New(0); @@ -86,7 +85,6 @@ psutil_users(PyObject *self, PyObject *args) { for (i = 0; i < count; i++) { py_address = NULL; - py_tuple = NULL; sessionId = sessions[i].SessionId; if (buffer_user != NULL) WTSFreeMemory(buffer_user); @@ -161,19 +159,18 @@ psutil_users(PyObject *self, PyObject *args) { if (py_username == NULL) goto error; - py_tuple = Py_BuildValue( - "OOd", - py_username, - py_address, - psutil_LargeIntegerToUnixTime(wts_info->ConnectTime) - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) + if (!pylist_append_fmt( + py_retlist, + "OOd", + py_username, + py_address, + psutil_LargeIntegerToUnixTime(wts_info->ConnectTime) + )) + { goto error; + } Py_CLEAR(py_username); Py_CLEAR(py_address); - Py_CLEAR(py_tuple); } WTSFreeMemory(sessions); @@ -184,7 +181,6 @@ psutil_users(PyObject *self, PyObject *args) { error: Py_XDECREF(py_username); - Py_XDECREF(py_tuple); Py_XDECREF(py_address); Py_DECREF(py_retlist); From 592b1da414bbd1233161796c09fa0a2eb8b7e8fa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 3 Mar 2026 21:38:44 +0100 Subject: [PATCH 1535/1714] Reorganize `Process` memory methods (#2737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes: #2731 - Fixes: #2736 - Fixes: #2723 ## Description Reorganization of process memory APIs. - Add new `Process.memory_info_ex()` method, which extends `Process.memory_info()` with platform-specific metrics: - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, *swap*, *hugetlb* - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, *phys_footprint* - Windows: *virtual*, *peak_virtual* - Add new `Process.memory_footprint()` method, which returns *uss*, *pss* and *swap* metrics (what `Process.memory_full_info()` used to return, which is now **deprecated**). - macOS: `Process.memory_info()` no longer returns *pfaults* and *pageins* fields. Use `Process.page_faults()` method instead. - Windows: `Process.memory_info()` renamed several fields (old names are kept as deprecated aliases): *wset* → *rss*, *peak_wset* → *peak_rss*, *pagefile* → *vms*, *peak_pagefile* → *peak_vms*, *private* → *vms*. - `Process.memory_full_info()` is **deprecated**. Use the new `Process.memory_footprint()` instead. ## Breaking changes - macOS: `Process.memory_info()` no longer returns *pfaults* and *pageins* fields (use `Process.page_faults()` method instead). Code that now breaks: `rss, vms, pfaults, pageins = Process().memory_info()`. - Windows: `Process.memory_info()` renamed several fields and the named tuple is shorter. Old names are kept as deprecated aliases, but if you rely on indexing (e.g. `Process.memory_info()[3]`) you should update your code to use namedtuple attribute access instead (e.g. `Process.memory_info().rss`). --- HISTORY.rst | 40 ++++- README.rst | 8 +- docs/index.rst | 193 ++++++++++++++++++------ psutil/__init__.py | 91 ++++++++--- psutil/_common.py | 25 +++ psutil/_ntuples.py | 86 +++++++++-- psutil/_psaix.py | 2 - psutil/_psbsd.py | 9 +- psutil/_pslinux.py | 53 +++++-- psutil/_psosx.py | 9 +- psutil/_pssunos.py | 2 - psutil/_psutil_osx.c | 1 + psutil/_pswindows.py | 25 +-- psutil/arch/bsd/proc.c | 21 ++- psutil/arch/osx/init.h | 1 + psutil/arch/osx/proc.c | 55 +++++++ psutil/arch/windows/ntextapi.h | 70 ++++----- psutil/arch/windows/proc_info.c | 2 + scripts/internal/bench_oneshot.py | 6 +- scripts/internal/print_access_denied.py | 2 +- scripts/internal/print_api_speed.py | 3 +- scripts/procsmem.py | 6 +- tests/__init__.py | 35 ++++- tests/test_contracts.py | 4 + tests/test_linux.py | 4 + tests/test_memleaks.py | 9 +- tests/test_posix.py | 13 ++ tests/test_process.py | 92 ++++++++--- tests/test_process_all.py | 36 ++--- tests/test_scripts.py | 4 +- tests/test_system.py | 31 ++++ tests/test_windows.py | 27 +++- 32 files changed, 738 insertions(+), 227 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index cfb6e44f48..cefe56eb00 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,12 +1,36 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -7.3.0 (IN DEVELOPMENT) +8.0.0 (IN DEVELOPMENT) ====================== **Enhancements** - 2729_: New `Process.page_faults()`_ method, returning a ``(minor, major)`` namedtuple. +- 2731_: Reorganization of process memory APIs. + + - Add new `Process.memory_info_ex()`_ method, which extends + `Process.memory_info()`_ with platform-specific metrics: + + - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, + *swap*, *hugetlb* + - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, + *phys_footprint* + - Windows: *virtual*, *peak_virtual* + + - Add new `Process.memory_footprint()`_ method, which returns *uss*, *pss* + and *swap* metrics (what `Process.memory_full_info()`_ used to return, + which is now **deprecated**). + + - macOS: `Process.memory_info()`_ no longer returns *pfaults* and *pageins* + fields. Use `Process.page_faults()`_ method instead. + + - Windows: `Process.memory_info()`_ renamed several fields (old names are + kept as deprecated aliases): *wset* → *rss*, *peak_wset* → *peak_rss*, + *pagefile* → *vms*, *peak_pagefile* → *peak_vms*, *private* → *vms*. + + - `Process.memory_full_info()`_ is **deprecated**. Use the new + `Process.memory_footprint()`_ instead. **Bug fixes** @@ -19,8 +43,17 @@ **Compatibility notes** -- `Process.memory_info()`_ on macOS no longer returns *pfaults* and *pageins* - fields. Use `Process.page_faults()`_ method instead. +- See 2731_ description above. Changes which break backwards compatibility are: + + - macOS: `Process.memory_info()`_ no longer returns *pfaults* and *pageins* + fields (use `Process.page_faults()`_ method instead). Code that now breaks: + ``rss, vms, pfaults, pageins = Process().memory_info()``. + + - Windows: `Process.memory_info()`_ renamed several fields and the named + tuple is shorter. Old names are kept as deprecated aliases, but if you rely + on indexing (e.g. ``Process.memory_info()[3]``) you should update your code + to use namedtuple attribute access instead (e.g. + ``Process.memory_info().rss``). 7.2.3 ===== @@ -2958,6 +2991,7 @@ In most cases accessing the old names will work but it will cause a .. _`Process.ionice()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.ionice .. _`Process.is_running()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.is_running .. _`Process.kill()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.kill +.. _`Process.memory_footprint()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_footprint .. _`Process.memory_full_info()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_full_info .. _`Process.memory_info()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info .. _`Process.memory_info_ex()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info_ex diff --git a/README.rst b/README.rst index af904070ff..a8f401086f 100644 --- a/README.rst +++ b/README.rst @@ -335,9 +335,11 @@ Process management 1 >>> >>> p.memory_info() - pmem(rss=10915840, vms=67608576, shared=3313664, text=2310144, lib=0, data=7262208, dirty=0) - >>> p.memory_full_info() # "real" USS memory usage (Linux, macOS, Win only) - pfullmem(rss=10199040, vms=52133888, shared=3887104, text=2867200, lib=0, data=5967872, dirty=0, uss=6545408, pss=6872064, swap=0) + pmem(rss=3164160, vms=4410163, shared=897433, text=302694, lib=0, data=2422374, dirty=0) + >>> p.memory_info_ex() + pmem_ex(rss=3164160, vms=4410163, shared=897433, text=302694, lib=0, data=2422374, dirty=0, peak_rss=4172190, peak_vms=6399001, rss_anon=2266726, rss_file=897433, rss_shmem=0, swap=0, hugetlb=0) + >>> p.memory_footprint() # "real" USS memory usage + pfootprint(uss=2355200, pss=2483712, swap=0) >>> p.memory_percent() 0.7823 >>> p.memory_maps() diff --git a/docs/index.rst b/docs/index.rst index 7e88696b47..457ec77759 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -339,7 +339,33 @@ Memory Return statistics about system memory usage as a named tuple including the following fields, expressed in bytes. - Main metrics: + +-----------+-----------+-----------+-----------+-----------+-----------+ + | Linux | macOS | BSD | Windows | Solaris | AIX | + +===========+===========+===========+===========+===========+===========+ + | total | total | total | total | total | total | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | available | available | available | available | available | available | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | percent | percent | percent | percent | percent | percent | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | used | used | used | used | used | used | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | free | free | free | free | free | free | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | active | active | active | | | | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | inactive | inactive | inactive | | | | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | buffers | | buffers | | | | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | cached | | cached | | | | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | shared | | shared | | | | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | slab | | | | | | + +-----------+-----------+-----------+-----------+-----------+-----------+ + | | wired | wired | | | | + +-----------+-----------+-----------+-----------+-----------+-----------+ - **total**: total physical memory (exclusive swap). - **available**: the memory that can be given instantly to processes without @@ -348,9 +374,6 @@ Memory on the platform. It is supposed to be used to monitor actual memory usage in a cross platform fashion. - **percent**: the percentage usage calculated as ``(total - available) / total * 100``. - - Other metrics: - - **used**: memory used, calculated differently depending on the platform and designed for informational purposes only. **total - free** does not necessarily match **used**. @@ -358,15 +381,15 @@ Memory note that this doesn't reflect the actual memory available (use **available** instead). **total - used** does not necessarily match **free**. - - **active** *(UNIX)*: memory currently in use or very recently used, and so - it is in RAM. - - **inactive** *(UNIX)*: memory that is marked as not used. + - **active** *(Linux, macOS, BSD)*: memory currently in use or very recently + used, and so it is in RAM. + - **inactive** *(Linux, macOS, BSD)*: memory that is marked as not used. - **buffers** *(Linux, BSD)*: cache for things like file system metadata. - **cached** *(Linux, BSD)*: cache for various things. - **shared** *(Linux, BSD)*: memory that may be simultaneously accessed by multiple processes. - **slab** *(Linux)*: in-kernel data structures cache. - - **wired** *(BSD, macOS)*: memory that is marked to always stay in RAM. It is + - **wired** *(macOS, BSD)*: memory that is marked to always stay in RAM. It is never moved to disk. The sum of **used** and **available** does not necessarily equal **total**. @@ -383,7 +406,7 @@ Memory >>> mem svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304, slab=199348224) >>> - >>> THRESHOLD = 100 * 1024 * 1024 # 100MB + >>> THRESHOLD = 500 * 1024 * 1024 # 500MB >>> if mem.available <= THRESHOLD: ... print("warning") ... @@ -1162,9 +1185,11 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`gids` | | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_ctx_switches` | :meth:`exe` | :meth:`ppid` | :meth:`ppid` | | | + | :meth:`memory_info_ex` | :meth:`exe` | :meth:`ppid` | :meth:`ppid` | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`num_ctx_switches` | :meth:`name` | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_threads` | :meth:`name` | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | + | :meth:`num_threads` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`uids` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ @@ -1172,7 +1197,7 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | | | :meth:`username` | :meth:`username` | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`memory_full_info` | | | | | | + | :meth:`memory_footprint` | | | | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`memory_maps` | | | | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ @@ -1257,7 +1282,7 @@ Process class If *attrs* is specified it must be a list of strings reflecting available :class:`Process` class's attribute names. Here's a list of possible string values: - ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. + ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_footprint'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_info_ex'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. If *attrs* argument is not passed all public read only attributes are assumed. *ad_value* is the value which gets assigned to a dict key in case @@ -1273,7 +1298,7 @@ Process class >>> >>> # get a list of valid attrs names >>> list(psutil.Process().as_dict().keys()) - ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_full_info', 'memory_info', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] + ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] .. versionchanged:: 3.0.0 *ad_value* is used also when incurring into @@ -1649,38 +1674,33 @@ Process class +---------+---------+----------+---------+-----+-------------------------------------------------------------+ | Linux | macOS | BSD | Solaris | AIX | Windows | +=========+=========+==========+=========+=====+=============================================================+ - | rss | rss | rss | rss | rss | rss (alias for ``wset``, maps to ``WorkingSetSize``) | + | rss | rss | rss | rss | rss | rss (maps to ``WorkingSetSize``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | vms | vms | vms | vms | vms | vms (alias for ``pagefile``, maps to ``PagefileUsage``) | + | vms | vms | vms | vms | vms | vms (maps to ``PagefileUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ | shared | | text | | | num_page_faults (maps to ``PageFaultCount``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | text | | data | | | peak_wset (maps to ``PeakWorkingSetSize``) | + | text | | data | | | paged_pool (maps to ``QuotaPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | lib | | stack | | | wset (maps to ``WorkingSetSize``) | + | lib | | stack | | | nonpaged_pool (maps to ``QuotaNonPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | data | | | | | peak_paged_pool (maps to ``QuotaPeakPagedPoolUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | dirty | | | | | paged_pool (maps to ``QuotaPagedPoolUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | peak_nonpaged_pool (maps to ``QuotaPeakNonPagedPoolUsage``) | + | data | | peak_rss | | | peak_rss (maps to ``PeakWorkingSetSize``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | nonpaged_pool (maps to ``QuotaNonPagedPoolUsage``) | + | dirty | | | | | peak_vms (maps to ``PeakPagefileUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | pagefile (maps to ``PagefileUsage``) | + | | | | | | peak_paged_pool (maps to ``QuotaPeakPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | peak_pagefile (maps to ``PeakPagefileUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | private (maps to ``PrivateUsage``) | + | | | | | | peak_nonpaged_pool (maps to ``QuotaPeakNonPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - **rss**: aka "Resident Set Size", this is the non-swapped physical memory - a process has used. On UNIX it matches ``top`` RES column. On Windows - this is an alias for `wset` field. + a process is using. On UNIX it matches ``top`` RES column. - **vms**: aka "Virtual Memory Size", this is the total amount of virtual memory used by the process. On UNIX it matches ``top`` VIRT column. On - Windows this is an alias for `pagefile` field. + Windows it maps to ``Private``, which is not exactly the virtual address + space size (VMS) as intended on UNIX. For that, use ``virtual`` from + :meth:`memory_info_ex`. - **shared**: *(Linux)* memory that could be potentially shared with other processes. @@ -1691,15 +1711,21 @@ Process class executable code. This matches "top"'s CODE column). - **data** *(Linux, BSD)*: - aka DRS (data resident set) the amount of physical memory devoted to - other than executable code. It matches "top"'s DATA column). + aka DRS (Data Resident Set) the amount of physical memory devoted to + other than executable code. It matches "top"'s DATA column. - **lib** *(Linux)*: the memory used by shared libraries. - **dirty** *(Linux)*: the number of dirty pages. + - **peak_rss** *(BSD)*: aka "peak Resident Set Size" or "high water mark". + It's the highest amount of physical memory the process has ever used at + any point during its lifetime. Can be 0 for kernel PIDs. + For on explanation of Windows fields rely on `PROCESS_MEMORY_COUNTERS_EX`_ - structure doc. Example on Linux: + doc. + + Example on Linux: >>> import psutil >>> p = psutil.Process() @@ -1710,22 +1736,78 @@ Process class 4.0.0 multiple fields are returned, not only *rss* and *vms*. .. versionchanged:: - 7.3.0 macOS: *pfaults* and *pageins* are no longer returned. Use + 8.0.0 macOS: *pfaults* and *pageins* are no longer returned. Use :meth:`page_faults` method instead. - .. method:: memory_full_info() + .. versionchanged:: + 8.0.0 BSD: added *peak_rss* - This method returns the same information as :meth:`memory_info`, plus, on - some platform (Linux, macOS, Windows), also provides additional metrics - (USS, PSS and swap). - The additional metrics provide a better representation of "effective" - process memory consumption (in case of USS) as explained in detail in this + .. versionchanged:: + 8.0.0 Windows: renamed several fields (old names are kept as deprecated + aliases): *wset* → *rss*, *peak_wset* → *peak_rss*, *pagefile* and + *private* → *vms*, *peak_pagefile* → *peak_vms*. + + .. method:: memory_info_ex() + + Return a named tuple extending :meth:`memory_info` with additional + platform-specific memory metrics. On platforms where extra fields are not + implemented this returns the same result as :meth:`memory_info`. All + numbers are expressed in bytes. + + +-------------+----------------+--------------+ + | Linux | macOS | Windows | + +=============+================+==============+ + | peak_rss | peak_rss | virtual | + +-------------+----------------+--------------+ + | peak_vms | | peak_virtual | + +-------------+----------------+--------------+ + | rss_anon | rss_anon | | + +-------------+----------------+--------------+ + | rss_file | rss_file | | + +-------------+----------------+--------------+ + | rss_shmem | wired | | + +-------------+----------------+--------------+ + | swap | compressed | | + +-------------+----------------+--------------+ + | hugetlb | phys_footprint | | + +-------------+----------------+--------------+ + + - **peak_rss**: aka "peak Resident Set Size" or "high water mark". It's the + highest amount of physical memory the process has ever used at any point + during its lifetime. + - **peak_vms** *(Linux)*: aka "peak Virtual Memory Size". It's highest + amount of virtual memory the process has ever used at any point during + its lifetime. + - **rss_anon** *(Linux, macOS)*: anonymous resident memory (heap, stack, + etc.). On macOS this maps to ``task_vm_info.internal``. + - **rss_file** *(Linux, macOS)*: file-backed resident memory. On macOS this + maps to ``task_vm_info.external``. + - **rss_shmem** *(Linux)*: shared memory resident pages. + - **wired** *(macOS)*: memory that is marked to always stay in RAM. It is + never moved to disk. + - **swap** *(Linux)*: memory swapped out to disk. Equivalent to + ``memory_footprint().swap`` but faster, as it reads from + */proc/pid/status* instead of */proc/pid/smaps*. + - **hugetlb** *(Linux)*: memory backed by huge TLB pages. + - **phys_footprint** *(macOS)*: total physical memory footprint including + compressed pages; this is what Xcode's memory gauge shows. + - **virtual** *(Windows)*: total virtual address space size. Unlike ``vms`` + in :meth:`memory_info`, this is the true virtual memory size + (``VirtualSize`` from ``SYSTEM_PROCESS_INFORMATION``). + - **peak_virtual** *(Windows)*: peak virtual address space size + (``VirtualPeakSize`` from ``SYSTEM_PROCESS_INFORMATION``). + + .. versionadded:: 8.0.0 + + .. method:: memory_footprint() + + Return a named tuple with USS, PSS and swap memory metrics. + These provide a better representation of actual process memory + consumption as explained in detail in this `blog post `__. It does so by passing through the whole process address. As such it usually requires higher user privileges than - :meth:`memory_info` and is considerably slower. - On platforms where extra fields are not implemented this simply returns the - same metrics as :meth:`memory_info`. + :meth:`memory_info` or :meth:`memory_info_ex` and is considerably slower. - **uss** *(Linux, macOS, Windows)*: aka "Unique Set Size", this is the memory which is unique to a process @@ -1749,22 +1831,33 @@ Process class >>> import psutil >>> p = psutil.Process() - >>> p.memory_full_info() - pfullmem(rss=10199040, vms=52133888, shared=3887104, text=2867200, lib=0, data=5967872, dirty=0, uss=6545408, pss=6872064, swap=0) - >>> + >>> p.memory_footprint() + pfootprint(uss=6545408, pss=6872064, swap=0) See also `procsmem.py`_ for an example application. + .. versionadded:: 8.0.0 + + Availability: Linux, macOS, Windows + + .. method:: memory_full_info() + + This method returns the same information as :meth:`memory_info` plus + :meth:`memory_footprint` in a single named tuple. + .. versionadded:: 4.0.0 + .. warning:: + deprecated in version 8.0.0; use :meth:`memory_footprint` instead. + .. method:: memory_percent(memtype="rss") Compare process memory to total physical system memory and calculate process memory utilization as a percentage. *memtype* argument is a string that dictates what type of process memory you want to compare against. You can choose between the named tuple field - names returned by :meth:`memory_info` and :meth:`memory_full_info` - (defaults to ``"rss"``). + names returned by :meth:`memory_info`, :meth:`memory_info_ex` and + :meth:`memory_footprint` (defaults to ``"rss"``). .. versionchanged:: 4.0.0 added `memtype` parameter. @@ -1868,7 +1961,7 @@ Process class >>> p.page_faults() ppagefaults(minor=5905, major=3) - .. versionadded:: 7.3.0 + .. versionadded:: 8.0.0 .. method:: open_files() diff --git a/psutil/__init__.py b/psutil/__init__.py index c76b36e35e..d65e1f8a9e 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -29,6 +29,7 @@ import sys import threading import time +import warnings try: import pwd @@ -203,7 +204,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "7.2.3" +__version__ = "8.0.0" version_info = tuple(int(num) for num in __version__.split('.')) _timer = getattr(time, 'monotonic', time.time) @@ -558,7 +559,7 @@ def as_dict(self, attrs=None, ad_value=None): msg = f"invalid attrs type {type(attrs)}" raise TypeError(msg) attrs = set(attrs) - invalid_names = attrs - valid_names + invalid_names = attrs - valid_names - _as_dict_attrnames_deprecated if invalid_names: msg = "invalid attr name{} {}".format( "s" if len(invalid_names) > 1 else "", @@ -1136,21 +1137,52 @@ def memory_info(self): """ return self._proc.memory_info() + def memory_info_ex(self): + """Return a namedtuple extending memory_info() with extra + metrics. + + All numbers are expressed in bytes. + """ + base = self.memory_info() + if hasattr(self._proc, "memory_info_ex"): + extras = self._proc.memory_info_ex() + return _ntp.pmem_ex(**base._asdict(), **extras) + return base + + # Linux, macOS, Windows + if hasattr(_psplatform.Process, "memory_footprint"): + + def memory_footprint(self): + """Return a named tuple with USS, PSS and swap memory + metrics. These provide a better representation of + actual process memory usage. + + USS is the memory unique to a process and which would + be freed if the process was terminated right now. + + It does so by passing through the whole process address. As + such it usually requires higher user privileges than + memory_info() or memory_info_ex() and is considerably + slower. + """ + return self._proc.memory_footprint() + + # DEPRECATED def memory_full_info(self): - """This method returns the same information as memory_info(), - plus, on some platform (Linux, macOS, Windows), also provides - additional metrics (USS, PSS and swap). - The additional metrics provide a better representation of actual - process memory usage. - - Namely USS is the memory which is unique to a process and which - would be freed if the process was terminated right now. - - It does so by passing through the whole process address. - As such it usually requires higher user privileges than - memory_info() and is considerably slower. + """Return the same information as memory_info() plus + memory_footprint() in a single named tuple. + + DEPRECATED in 8.0.0. Use memory_footprint() instead. """ - return self._proc.memory_full_info() + msg = ( + "memory_full_info() is deprecated; use memory_footprint() instead" + ) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + basic_mem = self.memory_info() + if hasattr(self, "memory_footprint"): + fp = self.memory_footprint() + return _ntp.pfullmem(*basic_mem + fp) + return _ntp.pfullmem(*basic_mem) def memory_percent(self, memtype="rss"): """Compare process memory to total physical system memory and @@ -1162,18 +1194,29 @@ def memory_percent(self, memtype="rss"): >>> psutil.Process().memory_info()._fields ('rss', 'vms', 'shared', 'text', 'lib', 'data', 'dirty', 'uss', 'pss') """ - valid_types = list(_ntp.pfullmem._fields) + valid_types = list(_ntp.pmem._fields) + if hasattr(_ntp, "pmem_ex"): + valid_types += [ + f for f in _ntp.pmem_ex._fields if f not in valid_types + ] + if hasattr(_ntp, "pfootprint"): + valid_types += [ + f for f in _ntp.pfootprint._fields if f not in valid_types + ] if memtype not in valid_types: msg = ( f"invalid memtype {memtype!r}; valid types are" f" {tuple(valid_types)!r}" ) raise ValueError(msg) - fun = ( - self.memory_info - if memtype in _ntp.pmem._fields - else self.memory_full_info - ) + if memtype in _ntp.pmem._fields: + fun = self.memory_info + elif ( + hasattr(_ntp, "pfootprint") and memtype in _ntp.pfootprint._fields + ): + fun = self.memory_footprint + else: + fun = self.memory_info_ex metrics = fun() value = getattr(metrics, memtype) @@ -1402,10 +1445,14 @@ def wait(self, timeout=None): 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', - 'connections', 'oneshot'} + 'connections', 'memory_full_info', 'oneshot'} } # fmt: on +# Deprecated attrs: not returned by default but still accepted if +# explicitly requested via as_dict(attrs=[...]). +_as_dict_attrnames_deprecated = {'memory_full_info'} + # ===================================================================== # --- Popen class diff --git a/psutil/_common.py b/psutil/_common.py index 33b6282253..2e3dcac0b8 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -564,6 +564,31 @@ def inner(self, *args, **kwargs): return outer +class deprecated_property: + """A descriptor which can be used to mark a property as deprecated. + 'replacement' is the attribute name to use instead. Usage:: + + class Foo: + bar = deprecated_property("baz") + """ + + def __init__(self, replacement): + self.replacement = replacement + self._msg = None + + def __set_name__(self, owner, name): + self._msg = ( + f"{name} is deprecated and will be removed; use" + f" {self.replacement} instead" + ) + + def __get__(self, obj, objtype=None): + if obj is None: + return self + warnings.warn(self._msg, category=DeprecationWarning, stacklevel=2) + return getattr(obj, self.replacement) + + class _WrapNumbers: """Watches numbers so that they don't overflow and wrap (reset to zero). diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 25591f3c43..79b3d46e54 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -11,6 +11,7 @@ from ._common import MACOS from ._common import SUNOS from ._common import WINDOWS +from ._common import deprecated_property # =================================================================== # --- system functions @@ -184,6 +185,24 @@ # psutil.Process().memory_info() pmem = nt("pmem", ("rss", "vms", "shared", "text", "lib", "data", "dirty")) + # psutil.Process().memory_info_ex() + pmem_ex = nt( + "pmem_ex", + pmem._fields + + ( + "peak_rss", + "peak_vms", + "rss_anon", + "rss_file", + "rss_shmem", + "swap", + "hugetlb", + ), + ) + + # psutil.Process().memory_footprint() + pfootprint = nt("pfootprint", ("uss", "pss", "swap")) + # psutil.Process().memory_full_info() pfullmem = nt("pfullmem", pmem._fields + ("uss", "pss", "swap")) @@ -242,24 +261,56 @@ svmem = nt("svmem", ("total", "available", "percent", "used", "free")) # psutil.Process.memory_info() - pmem = nt( + _pmem = nt( "pmem", ( "rss", "vms", "num_page_faults", - "peak_wset", - "wset", - "peak_paged_pool", "paged_pool", - "peak_nonpaged_pool", "nonpaged_pool", - "pagefile", - "peak_pagefile", - "private", + "peak_rss", + "peak_vms", + "peak_paged_pool", + "peak_nonpaged_pool", ), ) + class pmem(_pmem): + __slots__ = () + + wset = deprecated_property(replacement="rss") + peak_wset = deprecated_property(replacement="peak_rss") + pagefile = deprecated_property(replacement="vms") + peak_pagefile = deprecated_property(replacement="peak_vms") + private = deprecated_property(replacement="vms") + + # psutil.Process.memory_info_ex() + _pmem_ex = nt( + "pmem_ex", + pmem._fields + ("virtual", "peak_virtual"), + ) + + class pmem_ex(pmem, _pmem_ex): + __slots__ = () + _fields = _pmem_ex._fields + __repr__ = _pmem_ex.__repr__ + + def __new__(cls, *args, **kwargs): + return _pmem_ex.__new__(cls, *args, **kwargs) + + @classmethod + def _make(cls, iterable): + result = tuple.__new__(cls, iterable) + n = len(cls._fields) + if len(result) != n: + msg = f"Expected {n} arguments, got {len(result)}" + raise TypeError(msg) + return result + + # psutil.Process.memory_footprint() + pfootprint = nt("pfootprint", ("uss",)) + # psutil.Process.memory_full_info() pfullmem = nt("pfullmem", pmem._fields + ("uss",)) @@ -311,6 +362,23 @@ # psutil.Process.memory_info() pmem = nt("pmem", ("rss", "vms")) + # psutil.Process.memory_info_ex() + pmem_ex = nt( + "pmem_ex", + pmem._fields + + ( + "peak_rss", + "rss_anon", + "rss_file", + "wired", + "compressed", + "phys_footprint", + ), + ) + + # psutil.Process.memory_footprint() + pfootprint = nt("pfootprint", ("uss",)) + # psutil.Process.memory_full_info() pfullmem = nt("pfullmem", pmem._fields + ("uss",)) @@ -342,7 +410,7 @@ scputimes = nt("scputimes", ("user", "nice", "system", "idle", "irq")) # psutil.Process.memory_info() - pmem = nt("pmem", ("rss", "vms", "text", "data", "stack")) + pmem = nt("pmem", ("rss", "vms", "text", "data", "stack", "peak_rss")) # psutil.Process.memory_full_info() pfullmem = pmem diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 17f069b7e0..720ae3e689 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -484,8 +484,6 @@ def memory_info(self): vms = ret[proc_info_map['vms']] * 1024 return ntp.pmem(rss, vms) - memory_full_info = memory_info - @wrap_exceptions def status(self): code = self._proc_oneshot()[proc_info_map['status']] diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index f5b2c9fe6e..91184d4b6b 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -681,11 +681,14 @@ def cpu_num(self): def memory_info(self): d = self.oneshot() return ntp.pmem( - d["rss"], d["vms"], d["memtext"], d["memdata"], d["memstack"] + rss=d["rss"], + vms=d["vms"], + text=d["memtext"], + data=d["memdata"], + stack=d["memstack"], + peak_rss=d["peak_rss"], ) - memory_full_info = memory_info - @wrap_exceptions def create_time(self, monotonic=False): ctime = self.oneshot()["create_time"] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 69b7c3e1ab..4e2483a260 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1883,6 +1883,37 @@ def memory_info(self): ) return ntp.pmem(rss, vms, shared, text, lib, data, dirty) + @wrap_exceptions + def memory_info_ex( + self, + _vmpeak_re=re.compile(br"VmPeak:\s+(\d+)"), + _vmhwm_re=re.compile(br"VmHWM:\s+(\d+)"), + _rssanon_re=re.compile(br"RssAnon:\s+(\d+)"), + _rssfile_re=re.compile(br"RssFile:\s+(\d+)"), + _rssshmem_re=re.compile(br"RssShmem:\s+(\d+)"), + _vmswap_re=re.compile(br"VmSwap:\s+(\d+)"), + _hugetlb_re=re.compile(br"HugetlbPages:\s+(\d+)"), + ): + # Read /proc/{pid}/status which provides peak RSS/VMS and a + # cheaper way to get swap (no smaps parsing needed). + # RssAnon/RssFile/RssShmem were added in Linux 4.5; + # VmSwap in 2.6.34; HugetlbPages in 4.4. + data = self._read_status_file() + + def parse(regex): + m = regex.search(data) + return int(m.group(1)) * 1024 if m else 0 + + return { + "peak_rss": parse(_vmhwm_re), + "peak_vms": parse(_vmpeak_re), + "rss_anon": parse(_rssanon_re), + "rss_file": parse(_rssfile_re), + "rss_shmem": parse(_rssshmem_re), + "swap": parse(_vmswap_re), + "hugetlb": parse(_hugetlb_re), + } + if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS: def _parse_smaps_rollup(self): @@ -1938,19 +1969,17 @@ def _parse_smaps( 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 ntp.pfullmem(*basic_mem + (uss, pss, swap)) + def memory_footprint(self): + def fetch(): + if HAS_PROC_SMAPS_ROLLUP: # faster + try: + return self._parse_smaps_rollup() + except (ProcessLookupError, FileNotFoundError): + pass + return self._parse_smaps() - else: - memory_full_info = memory_info + uss, pss, swap = fetch() + return ntp.pfootprint(uss, pss, swap) if HAS_PROC_SMAPS: diff --git a/psutil/_psosx.py b/psutil/_psosx.py index bfe3f533dd..25377bb246 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -413,10 +413,13 @@ def memory_info(self): return ntp.pmem(d["rss"], d["vms"]) @wrap_exceptions - def memory_full_info(self): - basic_mem = self.memory_info() + def memory_info_ex(self): + return cext.proc_memory_info_ex(self.pid) + + @wrap_exceptions + def memory_footprint(self): uss = cext.proc_memory_uss(self.pid) - return ntp.pfullmem(*basic_mem + (uss,)) + return ntp.pfootprint(uss) @wrap_exceptions def page_faults(self): diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 61118e5d3c..76e85966d4 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -523,8 +523,6 @@ def memory_info(self): vms = ret[proc_info_map['vms']] * 1024 return ntp.pmem(rss, vms) - memory_full_info = memory_info - @wrap_exceptions def status(self): code = self._oneshot()[proc_info_map['status']] diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 4eb87ed202..8d6ade90e5 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -23,6 +23,7 @@ static PyMethodDef mod_methods[] = { {"proc_environ", psutil_proc_environ, METH_VARARGS}, {"proc_exe", psutil_proc_exe, METH_VARARGS}, {"proc_is_zombie", psutil_proc_is_zombie, METH_VARARGS}, + {"proc_memory_info_ex", psutil_proc_memory_info_ex, METH_VARARGS}, {"proc_memory_uss", psutil_proc_memory_uss, METH_VARARGS}, {"proc_name", psutil_proc_name, METH_VARARGS}, {"proc_net_connections", psutil_proc_net_connections, METH_VARARGS}, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index dd02c632e2..7e8c29457c 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -786,7 +786,6 @@ def _get_raw_meminfo(self): @wrap_exceptions def memory_info(self): - # on Windows RSS == WorkingSetSize and VSM == PagefileUsage. # Underlying C function returns fields of PROCESS_MEMORY_COUNTERS # struct. d = self._get_raw_meminfo() @@ -794,23 +793,27 @@ def memory_info(self): rss=d["WorkingSetSize"], vms=d["PagefileUsage"], num_page_faults=d["PageFaultCount"], - peak_wset=d["PeakWorkingSetSize"], - wset=d["WorkingSetSize"], - peak_paged_pool=d["QuotaPeakPagedPoolUsage"], paged_pool=d["QuotaPagedPoolUsage"], - peak_nonpaged_pool=d["QuotaPeakNonPagedPoolUsage"], nonpaged_pool=d["QuotaNonPagedPoolUsage"], - pagefile=d["PagefileUsage"], - peak_pagefile=d["PeakPagefileUsage"], - private=d["PrivateUsage"], + peak_rss=d["PeakWorkingSetSize"], + peak_vms=d["PeakPagefileUsage"], + peak_paged_pool=d["QuotaPeakPagedPoolUsage"], + peak_nonpaged_pool=d["QuotaPeakNonPagedPoolUsage"], ) @wrap_exceptions - def memory_full_info(self): - basic_mem = self.memory_info() + def memory_info_ex(self): + d = self._oneshot() + return { + "virtual": d["VirtualSize"], + "peak_virtual": d["PeakVirtualSize"], + } + + @wrap_exceptions + def memory_footprint(self): uss = cext.proc_memory_uss(self.pid) uss *= getpagesize() - return ntp.pfullmem(*basic_mem + (uss,)) + return ntp.pfootprint(uss) @wrap_exceptions def page_faults(self): diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index 2ab6c0c917..b54779f24a 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -31,6 +31,7 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { long memtext; long memdata; long memstack; + long peak_rss; int oncpu; #ifdef PSUTIL_NETBSD struct kinfo_proc2 kp; @@ -70,8 +71,10 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { memtext = (long)kp.ki_tsize * pagesize; memdata = (long)kp.ki_dsize * pagesize; memstack = (long)kp.ki_ssize * pagesize; + peak_rss = kp.ki_rusage.ru_maxrss * 1024; // expressed in KB #else rss = (long)kp.p_vm_rssize * pagesize; + peak_rss = kp.p_uru_maxrss * 1024; // expressed in KB #ifdef PSUTIL_OPENBSD // VMS, this is how ps determines it on OpenBSD: // https://github.com/openbsd/src/blob/ @@ -88,6 +91,21 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { memstack = (long)kp.p_vm_ssize * pagesize; #endif + // kernel doesn't always update peak_rss atomically, so rss can + // briefly exceed it. Difference is almost always 16KB. peak_rss is + // 0 for kernel/root PIDs: we leave it as-is so the caller knows it + // can't be relied upon. + if ((peak_rss < rss && peak_rss != 0)) { + psutil_debug( + "pid: %ld, ru_maxrss (%ld KB) < rss (%ld KB); using rss as " + "peak_rss", + (long)pid, + peak_rss / 1024, + rss / 1024 + ); + peak_rss = rss; // use rss as peak_rss + } + #ifdef PSUTIL_FREEBSD // what CPU we're on; top was used as an example: // https://svnweb.freebsd.org/base/head/usr.bin/top/machine.c? @@ -106,6 +124,7 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { #endif // clang-format off + #ifdef PSUTIL_FREEBSD if (!pydict_add(dict, "ppid", _Py_PARSE_PID, kp.ki_ppid)) goto error; if (!pydict_add(dict, "status", "i", (int)kp.ki_stat)) goto error; @@ -152,13 +171,13 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { if (!pydict_add(dict, "min_faults", "l", (long)kp.p_uru_minflt)) goto error; if (!pydict_add(dict, "maj_faults", "l", (long)kp.p_uru_majflt)) goto error; #endif - // all BSDs if (!pydict_add(dict, "rss", "l", rss)) goto error; if (!pydict_add(dict, "vms", "l", vms)) goto error; if (!pydict_add(dict, "memtext", "l", memtext)) goto error; if (!pydict_add(dict, "memdata", "l", memdata)) goto error; if (!pydict_add(dict, "memstack", "l", memstack)) goto error; + if (!pydict_add(dict, "peak_rss", "l", peak_rss)) goto error; if (!pydict_add(dict, "cpunum", "i", oncpu)) goto error; if (!pydict_add(dict, "name", "O", py_name)) goto error; diff --git a/psutil/arch/osx/init.h b/psutil/arch/osx/init.h index ffdc791556..ad9b070534 100644 --- a/psutil/arch/osx/init.h +++ b/psutil/arch/osx/init.h @@ -42,6 +42,7 @@ PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_exe(PyObject *self, PyObject *args); +PyObject *psutil_proc_memory_info_ex(PyObject *self, PyObject *args); PyObject *psutil_proc_memory_uss(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 75f7792ff6..73d6c01e16 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -278,6 +279,60 @@ psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { } +/* + * Return extended memory info via task_info(TASK_VM_INFO). + */ +PyObject * +psutil_proc_memory_info_ex(PyObject *self, PyObject *args) { + pid_t pid; + mach_port_t task = MACH_PORT_NULL; + kern_return_t kr; + task_vm_info_data_t info; + mach_msg_type_number_t info_count = TASK_VM_INFO_COUNT; + PyObject *dict = PyDict_New(); + + if (!dict) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + if (psutil_task_for_pid(pid, &task) != 0) + goto error; + + // Fetch multiple metrics. Fails with access denied for any PID not + // owned by us. + kr = task_info(task, TASK_VM_INFO, (task_info_t)&info, &info_count); + mach_port_deallocate(mach_task_self(), task); + task = MACH_PORT_NULL; + if (kr != KERN_SUCCESS) { + psutil_runtime_error("task_info(TASK_VM_INFO) syscall failed"); + goto error; + } + + // Fetch wired memory. + uint64_t wired_size = 0; + struct rusage_info_v0 ri; + if (proc_pid_rusage(pid, RUSAGE_INFO_V0, (rusage_info_t *)&ri) == 0) + wired_size = ri.ri_wired_size; + else + psutil_debug("proc_pid_rusage() failed (pid=%i)", pid); + + // clang-format off + if (!pydict_add(dict, "peak_rss", "K", (unsigned long long)info.resident_size_peak)) goto error; + if (!pydict_add(dict, "rss_anon", "K", (unsigned long long)info.internal)) goto error; + if (!pydict_add(dict, "rss_file", "K", (unsigned long long)info.external)) goto error; + if (!pydict_add(dict, "wired", "K", (unsigned long long)wired_size)) goto error; + if (!pydict_add(dict, "compressed", "K", (unsigned long long)info.compressed)) goto error; + if (!pydict_add(dict, "phys_footprint", "K", (unsigned long long)info.phys_footprint)) goto error; + // clang-format on + + return dict; + +error: + Py_DECREF(dict); + return NULL; +} + + /* * Returns the USS (unique set size) of the process. Reference: * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 038e2263a3..f0d4ebf51c 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -292,41 +292,41 @@ typedef struct _SYSTEM_THREAD_INFORMATION2 { #define PSYSTEM_THREAD_INFORMATION PSYSTEM_THREAD_INFORMATION2 typedef struct _SYSTEM_PROCESS_INFORMATION2 { - ULONG NextEntryOffset; - ULONG NumberOfThreads; - ULONGLONG WorkingSetPrivateSize; - ULONG HardFaultCount; - ULONG NumberOfThreadsHighWatermark; - ULONGLONG CycleTime; - LARGE_INTEGER CreateTime; - LARGE_INTEGER UserTime; - LARGE_INTEGER KernelTime; - UNICODE_STRING ImageName; - LONG BasePriority; - HANDLE UniqueProcessId; - HANDLE InheritedFromUniqueProcessId; - ULONG HandleCount; - ULONG SessionId; - ULONG_PTR UniqueProcessKey; - SIZE_T PeakVirtualSize; - SIZE_T VirtualSize; - DWORD PageFaultCount; - SIZE_T PeakWorkingSetSize; - SIZE_T WorkingSetSize; - SIZE_T QuotaPeakPagedPoolUsage; - SIZE_T QuotaPagedPoolUsage; - SIZE_T QuotaPeakNonPagedPoolUsage; - SIZE_T QuotaNonPagedPoolUsage; - SIZE_T PagefileUsage; - SIZE_T PeakPagefileUsage; - SIZE_T PrivatePageCount; - LARGE_INTEGER ReadOperationCount; - LARGE_INTEGER WriteOperationCount; - LARGE_INTEGER OtherOperationCount; - LARGE_INTEGER ReadTransferCount; - LARGE_INTEGER WriteTransferCount; - LARGE_INTEGER OtherTransferCount; - SYSTEM_THREAD_INFORMATION Threads[1]; + ULONG NextEntryOffset; // The address of the previous item plus the value in the NextEntryOffset member. For the last item in the array, NextEntryOffset is 0. + ULONG NumberOfThreads; // The NumberOfThreads member contains the number of threads in the process. + ULONGLONG WorkingSetPrivateSize; // The total private memory that a process currently has allocated and is physically resident in memory. // since VISTA + ULONG HardFaultCount; // The total number of hard faults for data from disk rather than from in-memory pages. // since WIN7 + ULONG NumberOfThreadsHighWatermark; // The peak number of threads that were running at any given point in time, indicative of potential performance bottlenecks related to thread management. + ULONGLONG CycleTime; // The sum of the cycle time of all threads in the process. + LARGE_INTEGER CreateTime; // Number of 100-nanosecond intervals since the creation time of the process. Not updated during system timezone changes. + LARGE_INTEGER UserTime; // Number of 100-nanosecond intervals the process has executed in user mode. + LARGE_INTEGER KernelTime; // Number of 100-nanosecond intervals the process has executed in kernel mode. + UNICODE_STRING ImageName; // The file name of the executable image. + KPRIORITY BasePriority; // The starting priority of the process. + HANDLE UniqueProcessId; // The identifier of the process. + HANDLE InheritedFromUniqueProcessId; // The identifier of the process that created this process. Not updated and incorrectly refers to processes with recycled identifiers. + ULONG HandleCount; // The current number of open handles used by the process. + ULONG SessionId; // The identifier of the Remote Desktop Services session under which the specified process is running. + ULONG_PTR UniqueProcessKey; // since VISTA (requires SystemExtendedProcessInformation) + SIZE_T PeakVirtualSize; // The peak size, in bytes, of the virtual memory used by the process. + SIZE_T VirtualSize; // The current size, in bytes, of virtual memory used by the process. + ULONG PageFaultCount; // The total number of page faults for data that is not currently in memory. The value wraps around to zero on average 24 hours. + SIZE_T PeakWorkingSetSize; // The peak size, in kilobytes, of the working set of the process. + SIZE_T WorkingSetSize; // The number of pages visible to the process in physical memory. These pages are resident and available for use without triggering a page fault. + SIZE_T QuotaPeakPagedPoolUsage; // The peak quota charged to the process for pool usage, in bytes. + SIZE_T QuotaPagedPoolUsage; // The quota charged to the process for paged pool usage, in bytes. + SIZE_T QuotaPeakNonPagedPoolUsage; // The peak quota charged to the process for nonpaged pool usage, in bytes. + SIZE_T QuotaNonPagedPoolUsage; // The current quota charged to the process for nonpaged pool usage. + SIZE_T PagefileUsage; // The total number of bytes of page file storage in use by the process. + SIZE_T PeakPagefileUsage; // The maximum number of bytes of page-file storage used by the process. + SIZE_T PrivatePageCount; // The number of memory pages allocated for the use by the process. + LARGE_INTEGER ReadOperationCount; // The total number of read operations performed. + LARGE_INTEGER WriteOperationCount; // The total number of write operations performed. + LARGE_INTEGER OtherOperationCount; // The total number of I/O operations performed other than read and write operations. + LARGE_INTEGER ReadTransferCount; // The total number of bytes read during a read operation. + LARGE_INTEGER WriteTransferCount; // The total number of bytes written during a write operation. + LARGE_INTEGER OtherTransferCount; // The total number of bytes transferred during operations other than read and write operations. + SYSTEM_THREAD_INFORMATION Threads[1]; // This type is not defined in the structure but was added for convenience. } SYSTEM_PROCESS_INFORMATION2, *PSYSTEM_PROCESS_INFORMATION2; #define SYSTEM_PROCESS_INFORMATION SYSTEM_PROCESS_INFORMATION2 diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index 2b924f053e..ebf569cc93 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -851,6 +851,8 @@ psutil_proc_oneshot(PyObject *self, PyObject *args) { if (!pydict_add(dict, "PagefileUsage", "K", (ULONGLONG)proc->PagefileUsage)) goto error; if (!pydict_add(dict, "PeakPagefileUsage", "K", (ULONGLONG)proc->PeakPagefileUsage)) goto error; if (!pydict_add(dict, "PrivatePageCount", "K", (ULONGLONG)proc->PrivatePageCount)) goto error; + if (!pydict_add(dict, "VirtualSize", "K", (ULONGLONG)proc->VirtualSize)) goto error; + if (!pydict_add(dict, "PeakVirtualSize", "K", (ULONGLONG)proc->PeakVirtualSize)) goto error; // clang-format on free(buffer); diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 276ed72dda..8eee21a010 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -33,7 +33,7 @@ if psutil.LINUX: names += [ - # 'memory_full_info', + # 'memory_footprint', # 'memory_maps', 'cpu_num', 'cpu_times', @@ -51,7 +51,7 @@ 'cpu_times', 'gids', 'io_counters', - 'memory_full_info', + 'memory_footprint', 'memory_info', 'name', 'num_ctx_switches', @@ -66,7 +66,7 @@ names += [ 'cmdline', 'gids', - 'memory_full_info', + 'memory_footprint', 'memory_info', 'name', 'num_threads', diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 3834b0f187..48b1d3f011 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -32,7 +32,7 @@ threads 0 0.0% SUCCESS cpu_affinity 0 0.0% SUCCESS memory_maps 71 21.3% ACCESS DENIED -memory_full_info 71 21.3% ACCESS DENIED +memory_footprint 71 21.3% ACCESS DENIED exe 174 52.1% ACCESS DENIED environ 238 71.3% ACCESS DENIED num_fds 238 71.3% ACCESS DENIED diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index f4eb395e26..5842600c14 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -45,6 +45,7 @@ num_fds 300 0.00391 memory_info 300 0.00597 memory_percent 300 0.00648 +memory_info_ex 300 0.00701 io_counters 300 0.00707 name 300 0.00894 status 300 0.00900 @@ -64,7 +65,7 @@ open_files 300 0.01630 username 300 0.01655 environ 300 0.02250 -memory_full_info 300 0.07066 +memory_foorprint 300 0.07066 memory_maps 300 0.74281 """ diff --git a/scripts/procsmem.py b/scripts/procsmem.py index c14f480d4a..90b6cf8cbc 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -61,15 +61,15 @@ def main(): for p in psutil.process_iter(): with p.oneshot(): try: - mem = p.memory_full_info() - info = p.as_dict(["cmdline", "username"]) + mem = p.memory_footprint() + info = p.as_dict(["cmdline", "username", "memory_info"]) except psutil.AccessDenied: ad_pids.append(p.pid) except psutil.NoSuchProcess: pass else: p._uss = mem.uss - p._rss = mem.rss + p._rss = info["memory_info"].rss if not p._uss: continue p._pss = getattr(mem, "pss", "") diff --git a/tests/__init__.py b/tests/__init__.py index f4330df315..586c73fa0b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -42,6 +42,7 @@ import psutil from psutil import AIX +from psutil import BSD from psutil import LINUX from psutil import MACOS from psutil import NETBSD @@ -65,7 +66,8 @@ 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', 'IS_64BIT', "HAS_PROC_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_PROC_ENVIRON", - "HAS_PROC_IO_COUNTERS", "HAS_PROC_IONICE", "HAS_PROC_MEMORY_MAPS", + "HAS_PROC_IO_COUNTERS", "HAS_PROC_IONICE", + "HAS_PROC_MEMORY_FOOTPRINT", "HAS_PROC_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_PROC_RLIMIT", "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", "MACOS_12PLUS", "COVERAGE", @@ -194,6 +196,7 @@ def macos_version(): HAS_PROC_ENVIRON = hasattr(psutil.Process, "environ") HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") HAS_PROC_IONICE = hasattr(psutil.Process, "ionice") +HAS_PROC_MEMORY_FOOTPRINT = hasattr(psutil.Process, "memory_footprint") HAS_PROC_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") HAS_PROC_RLIMIT = hasattr(psutil.Process, "rlimit") HAS_PROC_THREADS = hasattr(psutil.Process, "threads") @@ -1056,6 +1059,28 @@ def assert_in_pids(proc): # rid of a zombie is to kill its parent. # assert proc == ppid(), os.getpid() + def check_proc_memory(self, nt): + # Check the ntuple returned by Process.memory_*() methods. + assert is_namedtuple(nt) + for value in nt: + assert isinstance(value, int) + assert value >= 0 + if hasattr(nt, "peak_rss"): + if BSD and nt.peak_rss == 0: + pass # kernel threads don't have rusage tracking + else: + # VmHWM (from /proc/pid/status) and ru_maxrss both + # track peak RSS but are synced independently. Allow 5% + # tolerance. + diff = nt.rss - nt.peak_rss + assert diff <= nt.rss * 0.05 + if hasattr(nt, "peak_vms"): + assert nt.peak_vms >= nt.vms + if hasattr(nt, "peak_paged_pool"): # Windows + assert nt.peak_paged_pool >= nt.paged_pool + if hasattr(nt, "peak_nonpaged_pool"): # Windows + assert nt.peak_nonpaged_pool >= nt.nonpaged_pool + def is_win_secure_system_proc(pid): # see: https://github.com/giampaolo/psutil/issues/2338 @@ -1101,6 +1126,7 @@ class process_namespace: ('children', (), {'recursive': True}), ('connections', (), {}), # deprecated ('is_running', (), {}), + ('memory_full_info', (), {}), # deprecated ('oneshot', (), {}), ('parent', (), {}), ('parents', (), {}), @@ -1114,8 +1140,8 @@ class process_namespace: ('create_time', (), {}), ('cwd', (), {}), ('exe', (), {}), - ('memory_full_info', (), {}), ('memory_info', (), {}), + ('memory_info_ex', (), {}), ('name', (), {}), ('net_connections', (), {'kind': 'all'}), ('nice', (), {}), @@ -1147,6 +1173,8 @@ class process_namespace: getters += [('environ', (), {})] if WINDOWS: getters += [('num_handles', (), {})] + if HAS_PROC_MEMORY_FOOTPRINT: + getters += [('memory_footprint', (), {})] if HAS_PROC_MEMORY_MAPS: getters += [('memory_maps', (), {'grouped': False})] @@ -1635,8 +1663,7 @@ def warn(msg): 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] is not tuple: + if tuple not in t.__mro__: return False f = getattr(t, '_fields', None) if not isinstance(f, tuple): diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 844132e9d3..f984247cea 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -201,6 +201,10 @@ def test_memory_maps(self): hasit = hasattr(psutil.Process, "memory_maps") assert hasit == (not (OPENBSD or NETBSD or AIX or MACOS)) + def test_memory_footprint(self): + hasit = hasattr(psutil.Process, "memory_footprint") + assert hasit == (LINUX or MACOS or WINDOWS) + # =================================================================== # --- API types diff --git a/tests/test_linux.py b/tests/test_linux.py index db73815e5d..cb517d5852 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -2197,6 +2197,10 @@ def test_create_time_monotonic(self): assert p._proc.create_time() != p._proc.create_time(monotonic=True) assert p._get_ident()[1] == p._proc.create_time(monotonic=True) + def test_memory_info_ex(self): + mem = psutil.Process().memory_info_ex() + assert mem.rss == mem.rss_anon + mem.rss_file + mem.rss_shmem + @pytest.mark.skipif(not LINUX, reason="LINUX only") class TestProcessAgainstStatus(PsutilTestCase): diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py index f72391e297..a727a53672 100755 --- a/tests/test_memleaks.py +++ b/tests/test_memleaks.py @@ -29,6 +29,7 @@ from . import HAS_PROC_ENVIRON from . import HAS_PROC_IO_COUNTERS from . import HAS_PROC_IONICE +from . import HAS_PROC_MEMORY_FOOTPRINT from . import HAS_PROC_MEMORY_MAPS from . import HAS_PROC_RLIMIT from . import HAS_SENSORS_BATTERY @@ -160,8 +161,12 @@ def test_cpu_num(self): def test_memory_info(self): self.execute(self.proc.memory_info) - def test_memory_full_info(self): - self.execute(self.proc.memory_full_info) + def test_memory_info_ex(self): + self.execute(self.proc.memory_info_ex) + + @pytest.mark.skipif(not HAS_PROC_MEMORY_FOOTPRINT, reason="not supported") + def test_memory_footprint(self): + self.execute(self.proc.memory_footprint) @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_terminal(self): diff --git a/tests/test_posix.py b/tests/test_posix.py index a801c670fc..efc4851e3e 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -357,6 +357,19 @@ def test_page_faults_minor_increase(self): pf_after = p.page_faults() assert pf_after.minor > pf_before.minor + def test_memory_peak_rss(self): + mem = psutil.Process().memory_info_ex() + if not hasattr(mem, "peak_rss"): + return pytest.skip("not supported") + ru = resource.getrusage(resource.RUSAGE_SELF) + # VmHWM (from /proc/pid/status) and ru_maxrss both track peak + # RSS but are synced independently. Allow 5% tolerance. + if MACOS: + rss_diff = abs(mem.peak_rss - ru.ru_maxrss) + else: + rss_diff = abs(mem.peak_rss - ru.ru_maxrss * 1024) + assert rss_diff <= mem.peak_rss * 0.05 + @pytest.mark.skipif(not POSIX, reason="POSIX only") class TestSystemAPIs(PsutilTestCase): diff --git a/tests/test_process.py b/tests/test_process.py index b14c5a9964..0225e9d6fa 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -46,6 +46,7 @@ from . import HAS_PROC_ENVIRON from . import HAS_PROC_IO_COUNTERS from . import HAS_PROC_IONICE +from . import HAS_PROC_MEMORY_FOOTPRINT from . import HAS_PROC_MEMORY_MAPS from . import HAS_PROC_RLIMIT from . import HAS_PROC_THREADS @@ -448,6 +449,7 @@ def test_threads_2(self): @retry_on_failure() def test_memory_info(self): p = psutil.Process() + self.check_proc_memory(p.memory_info()) # step 1 - get a base value to compare our results rss1, vms1 = p.memory_info()[:2] @@ -467,30 +469,80 @@ def test_memory_info(self): assert percent2 > percent1 del memarr - if WINDOWS: - mem = p.memory_info() - assert mem.rss == mem.wset - assert mem.vms == mem.pagefile - - mem = p.memory_info() - for name in mem._fields: - assert getattr(mem, name) >= 0 - - def test_memory_full_info(self): + def test_memory_info_ex(self): p = psutil.Process() + mem = p.memory_info_ex() + self.check_proc_memory(mem) total = psutil.virtual_memory().total - mem = p.memory_full_info() for name in mem._fields: - value = getattr(mem, name) - assert value >= 0 - if (name == "vms" and OSX) or LINUX: - continue - assert value <= total - if LINUX or WINDOWS or MACOS: - assert mem.uss >= 0 + if name != "vms": + value = getattr(mem, name) + assert value <= total + + def test_memory_info_ex_fields_order(self): + mem = psutil.Process().memory_info_ex() + common = ("rss", "vms") + assert mem._fields[:2] == common if LINUX: - assert mem.pss >= 0 - assert mem.swap >= 0 + assert mem._fields[2:] == ( + "shared", + "text", + "lib", + "data", + "dirty", + "peak_rss", + "peak_vms", + "rss_anon", + "rss_file", + "rss_shmem", + "swap", + "hugetlb", + ) + elif MACOS: + assert mem._fields[2:] == ( + "peak_rss", + "rss_anon", + "rss_file", + "wired", + "compressed", + "phys_footprint", + ) + elif WINDOWS: + assert mem._fields[2:] == ( + "num_page_faults", + "paged_pool", + "nonpaged_pool", + "peak_rss", + "peak_vms", + "peak_paged_pool", + "peak_nonpaged_pool", + "virtual", + "peak_virtual", + ) + else: + assert mem._fields == psutil.Process().memory_info_ex()._fields + + @pytest.mark.skipif(not HAS_PROC_MEMORY_FOOTPRINT, reason="not supported") + def test_memory_footprint(self): + p = psutil.Process() + mem = p.memory_footprint() + self.check_proc_memory(mem) + + def test_memory_full_info(self): + p = psutil.Process() + with pytest.warns(DeprecationWarning): + mem = p.memory_full_info() + # not returned by default + assert 'memory_full_info' not in p.as_dict() + # but explicitly requesting it should work + with pytest.warns(DeprecationWarning): + d = p.as_dict(attrs=['memory_full_info']) + assert 'memory_full_info' in d + # fields should be memory_info() + memory_footprint() (if avail) + expected = p.memory_info()._fields + if HAS_PROC_MEMORY_FOOTPRINT: + expected += p.memory_footprint()._fields + assert mem._fields == expected @pytest.mark.skipif(not HAS_PROC_MEMORY_MAPS, reason="not supported") def test_memory_maps(self): diff --git a/tests/test_process_all.py b/tests/test_process_all.py index 63b0641de4..c8853aa506 100755 --- a/tests/test_process_all.py +++ b/tests/test_process_all.py @@ -24,7 +24,6 @@ from psutil import MACOS from psutil import NETBSD from psutil import OPENBSD -from psutil import OSX from psutil import POSIX from psutil import WINDOWS @@ -139,13 +138,10 @@ def test_all(self): meth(value, info) except Exception: # noqa: BLE001 s = '\n' + '=' * 70 + '\n' - s += ( - "FAIL: name=test_{}, pid={}, ret={}\ninfo={}\n".format( - name, - info['pid'], - repr(value), - info, - ) + s += "FAIL: name=test_{}, pid={}, ret={}\n\n".format( + name, + info['pid'], + repr(value), ) s += '-' * 70 s += f"\n{traceback.format_exc()}" @@ -303,31 +299,17 @@ def cpu_num(self, ret, info): assert ret in list(range(psutil.cpu_count())) def memory_info(self, ret, info): - assert is_namedtuple(ret) - for value in ret: - assert isinstance(value, int) - assert value >= 0 - if WINDOWS: - assert ret.peak_wset >= ret.wset - assert ret.peak_paged_pool >= ret.paged_pool - assert ret.peak_nonpaged_pool >= ret.nonpaged_pool - assert ret.peak_pagefile >= ret.pagefile + self.check_proc_memory(ret) - def memory_full_info(self, ret, info): + def memory_info_ex(self, ret, info): + self.check_proc_memory(ret) + + def memory_footprint(self, ret, info): assert is_namedtuple(ret) - total = psutil.virtual_memory().total for name in ret._fields: value = getattr(ret, name) assert isinstance(value, int) assert value >= 0 - 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 - assert value <= total, name - - if LINUX: - assert ret.pss >= ret.uss def open_files(self, ret, info): assert isinstance(ret, list) diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 6cfee2ad1b..dda82ab29e 100755 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -20,6 +20,7 @@ from . import CI_TESTING from . import HAS_BATTERY +from . import HAS_PROC_MEMORY_FOOTPRINT from . import HAS_PROC_MEMORY_MAPS from . import HAS_SENSORS_BATTERY from . import HAS_SENSORS_FANS @@ -123,9 +124,8 @@ def test_ifconfig(self): def test_pmap(self): self.assert_stdout('pmap.py', str(os.getpid())) + @pytest.mark.skipif(not HAS_PROC_MEMORY_FOOTPRINT, reason="not supported") def test_procsmem(self): - if 'uss' not in psutil.Process().memory_full_info()._fields: - return pytest.skip("not supported") self.assert_stdout('procsmem.py') def test_killall(self): diff --git a/tests/test_system.py b/tests/test_system.py index 410230b8bc..0adfa4f055 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -345,6 +345,37 @@ def test_virtual_memory(self): f"{name!r} > total (total={mem.total}, {name}={value})" ) + def test_virtual_memory_fields_order(self): + mem = psutil.virtual_memory() + common = ("total", "available", "percent", "used", "free") + assert mem._fields[:5] == common + if LINUX: + assert mem._fields[5:] == ( + "active", + "inactive", + "buffers", + "cached", + "shared", + "slab", + ) + elif MACOS: + assert mem._fields[5:] == ( + "active", + "inactive", + "wired", + ) + elif BSD: + assert mem._fields[5:] == ( + "active", + "inactive", + "buffers", + "cached", + "shared", + "wired", + ) + elif WINDOWS or SUNOS or AIX: + assert mem._fields[5:] == () + def test_swap_memory(self): mem = psutil.swap_memory() assert mem._fields == ( diff --git a/tests/test_windows.py b/tests/test_windows.py index 8a3f8a9a14..7138e28977 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -491,17 +491,28 @@ def test_nice(self): def test_memory_info(self): win = win32process.GetProcessMemoryInfo(self.OpenProcess(self.pid)) ps = psutil.Process(self.pid).memory_info() - assert ps.peak_wset == win['PeakWorkingSetSize'] - assert ps.wset == win['WorkingSetSize'] - assert ps.peak_paged_pool == win['QuotaPeakPagedPoolUsage'] + + assert ps.rss == win['WorkingSetSize'] + assert ps.vms == win['PagefileUsage'] assert ps.paged_pool == win['QuotaPagedPoolUsage'] - assert ps.peak_nonpaged_pool == win['QuotaPeakNonPagedPoolUsage'] assert ps.nonpaged_pool == win['QuotaNonPagedPoolUsage'] - assert ps.pagefile == win['PagefileUsage'] - assert ps.peak_pagefile == win['PeakPagefileUsage'] + assert ps.peak_rss == win['PeakWorkingSetSize'] + assert ps.peak_vms == win['PeakPagefileUsage'] + assert ps.peak_paged_pool == win['QuotaPeakPagedPoolUsage'] + assert ps.peak_nonpaged_pool == win['QuotaPeakNonPagedPoolUsage'] - assert ps.rss == ps.wset - assert ps.vms == ps.pagefile + with pytest.warns(DeprecationWarning, match="wset is deprecated"): + assert ps.wset == ps.rss + with pytest.warns(DeprecationWarning, match="peak_wset is deprecated"): + assert ps.peak_wset == ps.peak_rss + with pytest.warns(DeprecationWarning, match="pagefile is deprecated"): + assert ps.pagefile == ps.vms + with pytest.warns(DeprecationWarning, match="private is deprecated"): + assert ps.private == ps.vms + with pytest.warns( + DeprecationWarning, match="peak_pagefile is deprecated" + ): + assert ps.peak_pagefile == ps.peak_vms def test_wait(self): p = psutil.Process(self.pid) From ea55b415805c4ac51bcc0c73cdfeb6d152437e88 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 00:53:55 +0100 Subject: [PATCH 1536/1714] [Linux] remove `lib` and `dirty` process memory metrics (#2740) --- HISTORY.rst | 34 +++++++++++++++++--------------- README.rst | 4 ++-- docs/index.rst | 45 +++++++++++++++++++++++++------------------ psutil/_ntuples.py | 18 ++++++++++++++++- psutil/_pslinux.py | 4 ++-- tests/test_process.py | 2 -- 6 files changed, 66 insertions(+), 41 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index cefe56eb00..86edb503bb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,7 +7,7 @@ - 2729_: New `Process.page_faults()`_ method, returning a ``(minor, major)`` namedtuple. -- 2731_: Reorganization of process memory APIs. +- 2731_, 2736_, 2723_, 2733_: Reorganization of process memory APIs. - Add new `Process.memory_info_ex()`_ method, which extends `Process.memory_info()`_ with platform-specific metrics: @@ -22,12 +22,19 @@ and *swap* metrics (what `Process.memory_full_info()`_ used to return, which is now **deprecated**). - - macOS: `Process.memory_info()`_ no longer returns *pfaults* and *pageins* - fields. Use `Process.page_faults()`_ method instead. + - `Process.memory_info()`_ named tuple changed: - - Windows: `Process.memory_info()`_ renamed several fields (old names are - kept as deprecated aliases): *wset* → *rss*, *peak_wset* → *peak_rss*, - *pagefile* → *vms*, *peak_pagefile* → *peak_vms*, *private* → *vms*. + - BSD: added *peak_rss*. + + - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated + aliases returning 0 and emitting `DeprecationWarning` are kept. + + - macOS: *pfaults* and *pageins* removed with **no + backward-compataliases**. Use `Process.page_faults()`_ instead. + + - Windows: renamed fields (old names kept as deprecated aliases): *wset* → + *rss*, *peak_wset* → *peak_rss*, *pagefile* / *private* → *vms*, + *peak_pagefile* → *peak_vms*. - `Process.memory_full_info()`_ is **deprecated**. Use the new `Process.memory_footprint()`_ instead. @@ -43,17 +50,14 @@ **Compatibility notes** -- See 2731_ description above. Changes which break backwards compatibility are: +Changes that break backwards compatibility: - - macOS: `Process.memory_info()`_ no longer returns *pfaults* and *pageins* - fields (use `Process.page_faults()`_ method instead). Code that now breaks: - ``rss, vms, pfaults, pageins = Process().memory_info()``. +- `Process.memory_info()`_: - - Windows: `Process.memory_info()`_ renamed several fields and the named - tuple is shorter. Old names are kept as deprecated aliases, but if you rely - on indexing (e.g. ``Process.memory_info()[3]``) you should update your code - to use namedtuple attribute access instead (e.g. - ``Process.memory_info().rss``). + - The returned named tuple changed size and field order. + Positional access (e.g. ``p.memory_info()[3]`` or ``a, b, c = + p.memory_info()``) may break or silently return the wrong field. Always use + attribute access instead (e.g. ``p.memory_info().rss``). 7.2.3 ===== diff --git a/README.rst b/README.rst index a8f401086f..72a01ab8db 100644 --- a/README.rst +++ b/README.rst @@ -335,9 +335,9 @@ Process management 1 >>> >>> p.memory_info() - pmem(rss=3164160, vms=4410163, shared=897433, text=302694, lib=0, data=2422374, dirty=0) + pmem(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374) >>> p.memory_info_ex() - pmem_ex(rss=3164160, vms=4410163, shared=897433, text=302694, lib=0, data=2422374, dirty=0, peak_rss=4172190, peak_vms=6399001, rss_anon=2266726, rss_file=897433, rss_shmem=0, swap=0, hugetlb=0) + pmem_ex(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374, peak_rss=4172190, peak_vms=6399001, rss_anon=2266726, rss_file=897433, rss_shmem=0, swap=0, hugetlb=0) >>> p.memory_footprint() # "real" USS memory usage pfootprint(uss=2355200, pss=2483712, swap=0) >>> p.memory_percent() diff --git a/docs/index.rst b/docs/index.rst index 457ec77759..1736878728 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1173,7 +1173,7 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`create_time` | :meth:`memory_info` | :meth:`memory_percent` | :meth:`create_time` | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`name` | :meth:`memory_maps` | :meth:`num_ctx_switches` | :meth:`gids` | :meth:`memory_info` | :meth:`memory_info` | + | :meth:`name` | :meth:`memory_info_ex` | :meth:`num_ctx_switches` | :meth:`gids` | :meth:`memory_info` | :meth:`memory_info` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`ppid` | :meth:`num_ctx_switches` | :meth:`num_threads` | :meth:`io_counters` | :meth:`memory_percent` | :meth:`memory_percent` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ @@ -1181,13 +1181,13 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`terminal` | :meth:`num_threads` | :meth:`create_time` | :meth:`memory_info` | :meth:`ppid` | :meth:`ppid` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | | :meth:`username` | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | + | | | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`gids` | | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | + | :meth:`gids` | :meth:`exe` | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`memory_info_ex` | :meth:`exe` | :meth:`ppid` | :meth:`ppid` | | | + | :meth:`memory_info_ex` | :meth:`name` | :meth:`ppid` | :meth:`ppid` | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_ctx_switches` | :meth:`name` | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | + | :meth:`num_ctx_switches` | | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`num_threads` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ @@ -1682,11 +1682,11 @@ Process class +---------+---------+----------+---------+-----+-------------------------------------------------------------+ | text | | data | | | paged_pool (maps to ``QuotaPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | lib | | stack | | | nonpaged_pool (maps to ``QuotaNonPagedPoolUsage``) | + | data | | stack | | | nonpaged_pool (maps to ``QuotaNonPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | data | | peak_rss | | | peak_rss (maps to ``PeakWorkingSetSize``) | + | | | peak_rss | | | peak_rss (maps to ``PeakWorkingSetSize``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | dirty | | | | | peak_vms (maps to ``PeakPagefileUsage``) | + | | | | | | peak_vms (maps to ``PeakPagefileUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ | | | | | | peak_paged_pool (maps to ``QuotaPeakPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ @@ -1714,10 +1714,6 @@ Process class aka DRS (Data Resident Set) the amount of physical memory devoted to other than executable code. It matches "top"'s DATA column. - - **lib** *(Linux)*: the memory used by shared libraries. - - - **dirty** *(Linux)*: the number of dirty pages. - - **peak_rss** *(BSD)*: aka "peak Resident Set Size" or "high water mark". It's the highest amount of physical memory the process has ever used at any point during its lifetime. Can be 0 for kernel PIDs. @@ -1730,22 +1726,33 @@ Process class >>> import psutil >>> p = psutil.Process() >>> p.memory_info() - pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, lib=0, data=9891840, dirty=0) + pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, data=9891840) .. versionchanged:: 4.0.0 multiple fields are returned, not only *rss* and *vms*. .. versionchanged:: - 8.0.0 macOS: *pfaults* and *pageins* are no longer returned. Use - :meth:`page_faults` method instead. + 8.0.0 Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). + Deprecated aliases returning 0 and emitting `DeprecationWarning` are + kept. + + .. versionchanged:: + 8.0.0 macOS: *pfaults* and *pageins* removed with **no backward-compat + aliases**. Use :meth:`page_faults` instead. .. versionchanged:: - 8.0.0 BSD: added *peak_rss* + 8.0.0 Windows: renamed fields (old names kept as deprecated aliases): + *wset* → *rss*, *peak_wset* → *peak_rss*, *pagefile* / *private* → + *vms*, *peak_pagefile* → *peak_vms*. .. versionchanged:: - 8.0.0 Windows: renamed several fields (old names are kept as deprecated - aliases): *wset* → *rss*, *peak_wset* → *peak_rss*, *pagefile* and - *private* → *vms*, *peak_pagefile* → *peak_vms*. + 8.0.0 BSD: added *peak_rss*. + + .. warning:: + in version 8.0.0 the named tuple changed size and field order. Positional + access (e.g. ``p.memory_info()[3]`` or ``a, b, c = p.memory_info()``) may + break or silently return the wrong field. Always use attribute access + instead (e.g. ``p.memory_info().rss``). .. method:: memory_info_ex() diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 79b3d46e54..45fa533a6a 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -2,6 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import warnings from collections import namedtuple as nt from ._common import AIX @@ -183,7 +184,22 @@ popenfile = nt("popenfile", ("path", "fd", "position", "mode", "flags")) # psutil.Process().memory_info() - pmem = nt("pmem", ("rss", "vms", "shared", "text", "lib", "data", "dirty")) + class pmem(nt("pmem", ("rss", "vms", "shared", "text", "data"))): + __slots__ = () + + @property + def lib(self): + # It has always been 0 since Linux 2.6. + msg = "'lib' field is deprecated and will be removed" + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return 0 + + @property + def dirty(self): + # It has always been 0 since Linux 2.6. + msg = "'dirty' field is deprecated and will be removed" + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return 0 # psutil.Process().memory_info_ex() pmem_ex = nt( diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 4e2483a260..24718ab842 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1878,10 +1878,10 @@ def memory_info(self): # | dirty | dirty pages (unused in Linux 2.6) | dt | | # ============================================================ with open_binary(f"{self._procfs_path}/{self.pid}/statm") as f: - vms, rss, shared, text, lib, data, dirty = ( + vms, rss, shared, text, _lib, data, _dirty = ( int(x) * PAGESIZE for x in f.readline().split()[:7] ) - return ntp.pmem(rss, vms, shared, text, lib, data, dirty) + return ntp.pmem(rss, vms, shared, text, data) @wrap_exceptions def memory_info_ex( diff --git a/tests/test_process.py b/tests/test_process.py index 0225e9d6fa..fc7ab2294b 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -487,9 +487,7 @@ def test_memory_info_ex_fields_order(self): assert mem._fields[2:] == ( "shared", "text", - "lib", "data", - "dirty", "peak_rss", "peak_vms", "rss_anon", From b32a6e33a108a7835c2d9124eae9cf4cf43c47eb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 02:24:32 +0100 Subject: [PATCH 1537/1714] Win: refact C virtual_memory() and expose GetPerformanceInfo() --- psutil/_psutil_windows.c | 4 ++-- psutil/_pswindows.py | 26 +++++++++++++------------ psutil/arch/windows/init.h | 1 + psutil/arch/windows/mem.c | 40 ++++++++++++++++++++++++++------------ 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 109b5f0085..d57c287e10 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -88,7 +88,6 @@ static PyMethodDef PsutilMethods[] = { {"ppid_map", psutil_ppid_map, METH_VARARGS}, {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, {"users", psutil_users, METH_VARARGS}, - {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, // --- windows services {"winservice_enumerate", psutil_winservice_enumerate, METH_VARARGS}, @@ -98,7 +97,8 @@ static PyMethodDef PsutilMethods[] = { {"winservice_start", psutil_winservice_start, METH_VARARGS}, {"winservice_stop", psutil_winservice_stop, METH_VARARGS}, - // --- windows API bindings + // --- direct Windows APIs + {"GetPerformanceInfo", psutil_GetPerformanceInfo, METH_VARARGS}, {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS}, // --- others diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 7e8c29457c..957eb3b233 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -159,11 +159,11 @@ def getpagesize(): def virtual_memory(): """System virtual memory as a namedtuple.""" - mem = cext.virtual_mem() - totphys, availphys, _totsys, _availsys = mem - total = totphys - avail = availphys - free = availphys + info = cext.GetPerformanceInfo() + page = info["PageSize"] + total = info["PhysicalTotal"] * page + avail = info["PhysicalAvailable"] * page + free = avail used = total - avail percent = usage_percent((total - avail), total, round_=1) return ntp.svmem(total, avail, percent, used, free) @@ -171,14 +171,16 @@ def virtual_memory(): def swap_memory(): """Swap system memory as a (total, used, free, sin, sout) tuple.""" - mem = cext.virtual_mem() - - 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 + info = cext.GetPerformanceInfo() + page = info["PageSize"] + total_phys = info["PhysicalTotal"] * page + # CommitLimit == Maximum pages that can be committed into RAM + + # page file (swap). In the context of swap, it's the total "system + # memory" (physical + virtual), thus substract the physical part + # from it to get the "total swap". + total_system = info["CommitLimit"] * page # physical + swap 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. diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index 12ab148ee1..8844dde292 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -130,6 +130,7 @@ PyObject *psutil_sensors_battery(PyObject *self, PyObject *args); PyObject *psutil_swap_percent(PyObject *self, PyObject *args); PyObject *psutil_uptime(PyObject *self, PyObject *args); PyObject *psutil_users(PyObject *self, PyObject *args); +PyObject *psutil_GetPerformanceInfo(PyObject *self, PyObject *args); PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); PyObject *psutil_winservice_enumerate(PyObject *self, PyObject *args); PyObject *psutil_winservice_query_config(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/mem.c b/psutil/arch/windows/mem.c index 058838de96..96ebeb0879 100644 --- a/psutil/arch/windows/mem.c +++ b/psutil/arch/windows/mem.c @@ -21,21 +21,37 @@ psutil_getpagesize(PyObject *self, PyObject *args) { PyObject * -psutil_virtual_mem(PyObject *self, PyObject *args) { - unsigned long long totalPhys, availPhys, totalSys, availSys, pageSize; - PERFORMANCE_INFORMATION perfInfo; +psutil_GetPerformanceInfo(PyObject *self, PyObject *args) { + PERFORMANCE_INFORMATION info; + PyObject *dict = PyDict_New(); - if (!GetPerformanceInfo(&perfInfo, sizeof(PERFORMANCE_INFORMATION))) { - psutil_oserror(); + if (!dict) return NULL; + if (!GetPerformanceInfo(&info, sizeof(PERFORMANCE_INFORMATION))) { + psutil_oserror(); + goto error; } - // values are size_t, widen (if needed) to long long - pageSize = perfInfo.PageSize; - totalPhys = perfInfo.PhysicalTotal * pageSize; - availPhys = perfInfo.PhysicalAvailable * pageSize; - totalSys = perfInfo.CommitLimit * pageSize; - availSys = totalSys - perfInfo.CommitTotal * pageSize; - return Py_BuildValue("(LLLL)", totalPhys, availPhys, totalSys, availSys); + + // clang-format off + if (!pydict_add(dict, "CommitTotal", "K", (ULONGLONG)info.CommitTotal)) goto error; + if (!pydict_add(dict, "CommitLimit", "K", (ULONGLONG)info.CommitLimit)) goto error; + if (!pydict_add(dict, "CommitPeak", "K", (ULONGLONG)info.CommitPeak)) goto error; + if (!pydict_add(dict, "PhysicalTotal", "K", (ULONGLONG)info.PhysicalTotal)) goto error; + if (!pydict_add(dict, "PhysicalAvailable", "K", (ULONGLONG)info.PhysicalAvailable)) goto error; + if (!pydict_add(dict, "KernelTotal", "K", (ULONGLONG)info.KernelTotal)) goto error; + if (!pydict_add(dict, "KernelPaged", "K", (ULONGLONG)info.KernelPaged)) goto error; + if (!pydict_add(dict, "KernelNonpaged", "K", (ULONGLONG)info.KernelNonpaged)) goto error; + if (!pydict_add(dict, "SystemCache", "K", (ULONGLONG)info.SystemCache)) goto error; + if (!pydict_add(dict, "PageSize", "K", (ULONGLONG)info.PageSize)) goto error; + // if (!pydict_add(dict, "HandleCount", "I", (unsigned int)info.HandleCount)) goto error; + // if (!pydict_add(dict, "ProcessCount", "I", (unsigned int)info.ProcessCount)) goto error; + // if (!pydict_add(dict, "ThreadCount", "I", (unsigned int)info.ThreadCount)) goto error; + // clang-format on + return dict; + +error: + Py_DECREF(dict); + return NULL; } From aaad3bf0f7efdf47aeac94186fd5d960481c72d9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 02:39:49 +0100 Subject: [PATCH 1538/1714] Win: use PrivateUsage instead of PagefileUsage for VMS memory ...in reality it returns exactly the same value, but PagefileUsage is the old deprecated name according to MS doc. --- docs/index.rst | 7 ++++--- psutil/_pswindows.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1736878728..f769f57a3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1676,7 +1676,7 @@ Process class +=========+=========+==========+=========+=====+=============================================================+ | rss | rss | rss | rss | rss | rss (maps to ``WorkingSetSize``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | vms | vms | vms | vms | vms | vms (maps to ``PagefileUsage``) | + | vms | vms | vms | vms | vms | vms (maps to ``PrivateUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ | shared | | text | | | num_page_faults (maps to ``PageFaultCount``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ @@ -1698,8 +1698,9 @@ Process class - **vms**: aka "Virtual Memory Size", this is the total amount of virtual memory used by the process. On UNIX it matches ``top`` VIRT column. On - Windows it maps to ``Private``, which is not exactly the virtual address - space size (VMS) as intended on UNIX. For that, use ``virtual`` from + Windows it maps to ``PrivateUsage``, which is not exactly the virtual + address space size (VMS) as intended on UNIX. For that, use ``virtual`` + from :meth:`memory_info_ex`. - **shared**: *(Linux)* diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 957eb3b233..b8e98562a0 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -782,7 +782,7 @@ def _get_raw_meminfo(self): "QuotaNonPagedPoolUsage": info["QuotaNonPagedPoolUsage"], "PagefileUsage": info["PagefileUsage"], "PeakPagefileUsage": info["PeakPagefileUsage"], - "PrivateUsage": info["PrivatePageCount"], + "PrivateUsage": info["PrivatePageCount"], # adjust name } raise @@ -793,7 +793,7 @@ def memory_info(self): d = self._get_raw_meminfo() return ntp.pmem( rss=d["WorkingSetSize"], - vms=d["PagefileUsage"], + vms=d["PrivateUsage"], num_page_faults=d["PageFaultCount"], paged_pool=d["QuotaPagedPoolUsage"], nonpaged_pool=d["QuotaNonPagedPoolUsage"], From 691bde75f9c7ceb3f1e33ffa12f27ec8bacc6f3a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 14:25:33 +0100 Subject: [PATCH 1539/1714] OSX / virtmem: do mem calculations in C + make it return a dict --- psutil/_psosx.py | 16 +++-------- psutil/arch/osx/mem.c | 62 +++++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 25377bb246..fe8b5407cf 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -65,19 +65,11 @@ def virtual_memory(): """System virtual memory as a namedtuple.""" - total, active, inactive, wired, free, speculative = cext.virtual_mem() - # This is how Zabbix calculate avail and used mem: - # https://github.com/zabbix/zabbix/blob/master/src/libs/zbxsysinfo/osx/memory.c - # Also see: https://github.com/giampaolo/psutil/issues/1277 - avail = inactive + free - 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 ntp.svmem( - total, avail, percent, used, free, active, inactive, wired + d = cext.virtual_mem() + d["percent"] = usage_percent( + (d["total"] - d["available"]), d["total"], round_=1 ) + return ntp.svmem(**d) def swap_memory(): diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index b222b566a6..b27935e68c 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -49,36 +49,58 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { /* * Return system virtual memory stats. * See: - * https://opensource.apple.com/source/system_cmds/system_cmds-790/ - * vm_stat.tproj/vm_stat.c.auto.html + * https://opensource.apple.com/source/system_cmds/system_cmds-790/vm_stat.tproj/vm_stat.c.auto.html */ PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { - int mib[2]; uint64_t total; + unsigned long long active, inactive, wired, free, _speculative; + unsigned long long available, used; + int mib[2] = {CTL_HW, HW_MEMSIZE}; vm_statistics64_data_t vm; long pagesize = psutil_getpagesize(); - // physical mem - mib[0] = CTL_HW; - mib[1] = HW_MEMSIZE; + PyObject *dict = PyDict_New(); - // This is also available as sysctlbyname("hw.memsize"). - if (psutil_sysctl(mib, 2, &total, sizeof(total)) != 0) + if (dict == NULL) return NULL; - // vm + // This is also available as sysctlbyname("hw.memsize"). + if (psutil_sysctl(mib, 2, &total, sizeof(total)) != 0) + goto error; if (psutil_sys_vminfo(&vm) != 0) - return NULL; - - return Py_BuildValue( - "KKKKKK", - (unsigned long long)total, - (unsigned long long)vm.active_count * pagesize, // active - (unsigned long long)vm.inactive_count * pagesize, // inactive - (unsigned long long)vm.wire_count * pagesize, // wired - (unsigned long long)vm.free_count * pagesize, // free - (unsigned long long)vm.speculative_count * pagesize // speculative - ); + goto error; + + active = (unsigned long long)vm.active_count * pagesize; + inactive = (unsigned long long)vm.inactive_count * pagesize; + wired = (unsigned long long)vm.wire_count * pagesize; + free = (unsigned long long)vm.free_count * pagesize; + _speculative = (unsigned long long)vm.speculative_count * pagesize; + + // This is how Zabbix calculates avail and used mem: + // https://github.com/zabbix/zabbix/blob/master/src/libs/zbxsysinfo/osx/memory.c + // Also see: https://github.com/giampaolo/psutil/issues/1277 + available = inactive + free; + used = active + wired; + + // This is NOT how Zabbix calculates free mem but it matches "free" + // CLI utility. + free -= _speculative; + + // clang-format off + if (!pydict_add(dict, "total", "K", (unsigned long long)total)) goto error; + if (!pydict_add(dict, "available", "K", available)) goto error; + if (!pydict_add(dict, "used", "K", used)) goto error; + if (!pydict_add(dict, "free", "K", free)) goto error; + if (!pydict_add(dict, "active", "K", active)) goto error; + if (!pydict_add(dict, "inactive", "K", inactive)) goto error; + if (!pydict_add(dict, "wired", "K", wired)) goto error; + // clang-format on + + return dict; + +error: + Py_DECREF(dict); + return NULL; } From 9d7b9a8b30ad769ae27711a864d668aa63886e81 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 14:34:03 +0100 Subject: [PATCH 1540/1714] OSX / swap mem: do calculations in C + make it return a dict --- psutil/_psosx.py | 6 +++--- psutil/arch/osx/mem.c | 33 +++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index fe8b5407cf..1a4b13c86f 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -74,9 +74,9 @@ def virtual_memory(): def swap_memory(): """Swap system memory as a (total, used, free, sin, sout) tuple.""" - total, used, free, sin, sout = cext.swap_mem() - percent = usage_percent(used, total, round_=1) - return ntp.sswap(total, used, free, percent, sin, sout) + d = cext.swap_mem() + d["percent"] = usage_percent(d["used"], d["total"], round_=1) + return ntp.sswap(**d) # malloc / heap functions diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index b27935e68c..7dc51e76ed 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -109,26 +109,31 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { */ PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { - int mib[2]; struct xsw_usage totals; vm_statistics64_data_t vmstat; long pagesize = psutil_getpagesize(); + int mib[2] = {CTL_VM, VM_SWAPUSAGE}; + PyObject *dict = PyDict_New(); - mib[0] = CTL_VM; - mib[1] = VM_SWAPUSAGE; + if (dict == NULL) + return NULL; if (psutil_sysctl(mib, 2, &totals, sizeof(totals)) != 0) - return psutil_oserror_wsyscall("sysctl(VM_SWAPUSAGE)"); - + goto error; if (psutil_sys_vminfo(&vmstat) != 0) - return NULL; + goto error; - return Py_BuildValue( - "KKKKK", - (unsigned long long)totals.xsu_total, - (unsigned long long)totals.xsu_used, - (unsigned long long)totals.xsu_avail, - (unsigned long long)vmstat.pageins * pagesize, - (unsigned long long)vmstat.pageouts * pagesize - ); + // clang-format off + if (!pydict_add(dict, "total", "K", (unsigned long long)totals.xsu_total)) goto error; + if (!pydict_add(dict, "used", "K", (unsigned long long)totals.xsu_used)) goto error; + if (!pydict_add(dict, "free", "K", (unsigned long long)totals.xsu_avail)) goto error; + if (!pydict_add(dict, "sin", "K", (unsigned long long)vmstat.pageins * pagesize)) goto error; + if (!pydict_add(dict, "sout", "K", (unsigned long long)vmstat.pageouts * pagesize)) goto error; + // clang-format on + + return dict; + +error: + Py_DECREF(dict); + return NULL; } From 5a63d81e3b49d6929ac17862823cfc56a8bd289d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 17:08:26 +0100 Subject: [PATCH 1541/1714] Doc: add a table showing implementation details of virtual_memory() --- docs/index.rst | 127 +++++++++++++++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 45 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f769f57a3b..6cddb9b927 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -339,40 +339,11 @@ Memory Return statistics about system memory usage as a named tuple including the following fields, expressed in bytes. - +-----------+-----------+-----------+-----------+-----------+-----------+ - | Linux | macOS | BSD | Windows | Solaris | AIX | - +===========+===========+===========+===========+===========+===========+ - | total | total | total | total | total | total | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | available | available | available | available | available | available | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | percent | percent | percent | percent | percent | percent | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | used | used | used | used | used | used | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | free | free | free | free | free | free | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | active | active | active | | | | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | inactive | inactive | inactive | | | | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | buffers | | buffers | | | | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | cached | | cached | | | | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | shared | | shared | | | | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | slab | | | | | | - +-----------+-----------+-----------+-----------+-----------+-----------+ - | | wired | wired | | | | - +-----------+-----------+-----------+-----------+-----------+-----------+ - - **total**: total physical memory (exclusive swap). - **available**: the memory that can be given instantly to processes without - the system going into swap. - This is calculated by summing different memory metrics that vary depending - on the platform. It is supposed to be used to monitor actual memory usage - in a cross platform fashion. + the system going into swap. This may be calculated by summing different + memory metrics that vary depending on the platform. It is supposed to be + used to monitor actual memory usage in a cross-platform fashion. - **percent**: the percentage usage calculated as ``(total - available) / total * 100``. - **used**: memory used, calculated differently depending on the platform and designed for informational purposes only. **total - free** does not @@ -385,21 +356,81 @@ Memory used, and so it is in RAM. - **inactive** *(Linux, macOS, BSD)*: memory that is marked as not used. - **buffers** *(Linux, BSD)*: cache for things like file system metadata. - - **cached** *(Linux, BSD)*: cache for various things. + - **cached** *(Linux, BSD)*: cache for files read from the disk (the page + cache) - **shared** *(Linux, BSD)*: memory that may be simultaneously accessed by multiple processes. - **slab** *(Linux)*: in-kernel data structures cache. - **wired** *(macOS, BSD)*: memory that is marked to always stay in RAM. It is never moved to disk. - The sum of **used** and **available** does not necessarily equal **total**. - On Windows **available** and **free** are the same. - See `meminfo.py`_ script providing an example on how to convert bytes in a - human readable form. - - .. note:: if you just want to know how much physical memory is left in a - cross platform fashion simply rely on **available** and **percent** - fields. + Follows a table showing implementation details. All info on Linux are retrieved from `/proc/meminfo`. + + .. list-table:: + :header-rows: 1 + + * - Field + - Linux + - macOS + - Windows + - FreeBSD + * - total + - ``MemTotal`` + - ``sysctl() hw.memsize`` + - ``GetPerformanceInfo() PhysicalTotal`` + - ``sysctl() hw.physmem`` + * - available + - ``MemAvailable`` + - ``host_statistics64() inactive + free`` + - ``GetPerformanceInfo() PhysicalAvailable`` + - ``inactive + cached + free`` + * - used + - ``total - available`` + - ``host_statistics64() active + wired`` + - ``total - available`` + - ``active + wired + cached`` + * - free + - ``MemFree`` + - ``host_statistics64() free - speculative`` + - same as ``available`` + - ``sysctl() vm.stats.vm.v_free_count`` + * - active + - ``Active`` + - ``host_statistics64() active`` + - + - ``sysctl() vm.stats.vm.v_active_count`` + * - inactive + - ``Inactive`` + - ``host_statistics64() inactive`` + - + - ``sysctl() vm.stats.vm.v_inactive_count`` + * - buffers + - ``Buffers`` + - + - + - ``sysctl() vfs.bufspace`` + * - cached + - ``Cached + SReclaimable`` + - + - + - ``sysctl() vm.stats.vm.v_cache_count`` + * - shared + - ``Shmem`` + - + - + - ``sysctl(CTL_VM/VM_METER) t_vmshr + t_rmshr`` + * - slab + - ``Slab`` + - + - + - + * - wired + - + - ``host_statistics64() wired`` + - + - ``sysctl() vm.stats.vm.v_wire_count`` + + Example on Linux: >>> import psutil >>> mem = psutil.virtual_memory() @@ -412,6 +443,13 @@ Memory ... >>> + .. note:: if you just want to know how much physical memory is left in a + cross platform fashion simply rely on **available** and **percent** + fields. + + .. note:: see `meminfo.py`_ script providing an example on how to convert + bytes in a human readable form. + .. versionchanged:: 4.2.0 added *shared* metric on Linux. .. versionchanged:: 5.4.4 added *slab* metric on Linux. @@ -1698,10 +1736,9 @@ Process class - **vms**: aka "Virtual Memory Size", this is the total amount of virtual memory used by the process. On UNIX it matches ``top`` VIRT column. On - Windows it maps to ``PrivateUsage``, which is not exactly the virtual - address space size (VMS) as intended on UNIX. For that, use ``virtual`` - from - :meth:`memory_info_ex`. + Windows it maps to ``PrivateUsage``, which is close to, but not exactly + the same as the VMS definition used on UNIX. For that, use ``virtual`` + from :meth:`memory_info_ex`. - **shared**: *(Linux)* memory that could be potentially shared with other processes. From e6c307ba8bdb1d9dc736e919cd429b08bcfe7d6d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 18:19:30 +0100 Subject: [PATCH 1542/1714] FreeBSD / mem.c: use shorter sbn() alias for psutil_sysctlbyname() --- psutil/arch/freebsd/mem.c | 60 ++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 52644a8d3b..5ebdd99279 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -19,6 +19,13 @@ #endif +// shorter alias for psutil_sysctlbyname() +static inline int +sbn(const char *name, void *oldp, size_t oldlen) { + return psutil_sysctlbyname(name, oldp, oldlen); +} + + PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { unsigned long total; @@ -28,44 +35,27 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { int mib[] = {CTL_VM, VM_METER}; long pagesize = psutil_getpagesize(); - size_t size_ul = sizeof(total); - size_t size_ui = sizeof(active); - size_t size_long = sizeof(buffers); - size_t size_vm = sizeof(vm); - - PyObject *ret = NULL; - - if (psutil_sysctlbyname("hw.physmem", &total, size_ul) != 0) + if (sbn("hw.physmem", &total, sizeof(total)) != 0) return NULL; - - if (psutil_sysctlbyname("vm.stats.vm.v_active_count", &active, size_ui) - != 0) + if (sbn("vm.stats.vm.v_active_count", &active, sizeof(active)) != 0) return NULL; - - if (psutil_sysctlbyname("vm.stats.vm.v_inactive_count", &inactive, size_ui) - != 0) + if (sbn("vm.stats.vm.v_inactive_count", &inactive, sizeof(inactive)) != 0) return NULL; - - if (psutil_sysctlbyname("vm.stats.vm.v_wire_count", &wired, size_ui) != 0) + if (sbn("vm.stats.vm.v_wire_count", &wired, sizeof(wired)) != 0) + return NULL; + if (sbn("vm.stats.vm.v_free_count", &free, sizeof(free)) != 0) + return NULL; + if (sbn("vfs.bufspace", &buffers, sizeof(buffers)) != 0) return NULL; // Optional; ignore error if not available - if (psutil_sysctlbyname("vm.stats.vm.v_cache_count", &cached, size_ui) - != 0) - { + if (sbn("vm.stats.vm.v_cache_count", &cached, sizeof(cached)) != 0) { PyErr_Clear(); cached = 0; } - if (psutil_sysctlbyname("vm.stats.vm.v_free_count", &free, size_ui) != 0) - return NULL; - - if (psutil_sysctlbyname("vfs.bufspace", &buffers, size_long) != 0) - return NULL; - - if (psutil_sysctl(mib, 2, &vm, size_vm) != 0) { + if (psutil_sysctl(mib, 2, &vm, sizeof(vm)) != 0) return psutil_oserror_wsyscall("sysctl(CTL_VM | VM_METER)"); - } return Py_BuildValue( "KKKKKKKK", @@ -86,8 +76,7 @@ psutil_swap_mem(PyObject *self, PyObject *args) { // Return swap memory stats (see 'swapinfo' cmdline tool) kvm_t *kd; struct kvm_swap kvmsw[1]; - unsigned int swapin, swapout, nodein, nodeout; - size_t size = sizeof(unsigned int); + unsigned int free, swapin, swapout, nodein, nodeout; long pagesize = psutil_getpagesize(); kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open failed"); @@ -104,21 +93,22 @@ psutil_swap_mem(PyObject *self, PyObject *args) { kvm_close(kd); - if (psutil_sysctlbyname("vm.stats.vm.v_swapin", &swapin, size) != 0) + if (sbn("vm.stats.vm.v_swapin", &swapin, sizeof(swapin)) != 0) return NULL; - if (psutil_sysctlbyname("vm.stats.vm.v_swapout", &swapout, size) != 0) + if (sbn("vm.stats.vm.v_swapout", &swapout, sizeof(swapout)) != 0) return NULL; - if (psutil_sysctlbyname("vm.stats.vm.v_vnodein", &nodein, size) != 0) + if (sbn("vm.stats.vm.v_vnodein", &nodein, sizeof(nodein)) != 0) return NULL; - if (psutil_sysctlbyname("vm.stats.vm.v_vnodeout", &nodeout, size) != 0) + if (sbn("vm.stats.vm.v_vnodeout", &nodeout, sizeof(nodeout)) != 0) return NULL; + free = (kvmsw[0].ksw_total - kvmsw[0].ksw_used); + return Py_BuildValue( "(KKKII)", (unsigned long long)kvmsw[0].ksw_total * pagesize, // total (unsigned long long)kvmsw[0].ksw_used * pagesize, // used - (unsigned long long)kvmsw[0].ksw_total * pagesize - // free - kvmsw[0].ksw_used * pagesize, + (unsigned long long)free * pagesize, // free swapin + swapout, // swap in nodein + nodeout // swap out ); From 570957abcd4a2df8df66c705c2953977dce5f108 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 22:03:19 +0100 Subject: [PATCH 1543/1714] FreeBSD, mem.c: return a dict, use intermediate _vars, do percent in C --- psutil/_psbsd.py | 49 +++++++++---- psutil/arch/all/init.h | 1 + psutil/arch/all/utils.c | 14 ++++ psutil/arch/freebsd/mem.c | 144 +++++++++++++++++++++++++------------- psutil/arch/osx/mem.c | 17 +++-- 5 files changed, 153 insertions(+), 72 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 91184d4b6b..81d2a8fde3 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -126,6 +126,22 @@ def virtual_memory(): # https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L135 used = active + wired avail = total - used + percent = usage_percent((total - avail), total, round_=1) + return ntp.svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + wired, + ) + elif FREEBSD: + return ntp.svmem(**mem) else: total, free, active, inactive, wired, cached, buffers, shared = mem # matches freebsd-memory CLI: @@ -136,25 +152,28 @@ def virtual_memory(): avail = inactive + cached + free used = active + wired + cached - percent = usage_percent((total - avail), total, round_=1) - return ntp.svmem( - total, - avail, - percent, - used, - free, - active, - inactive, - buffers, - cached, - shared, - wired, - ) + percent = usage_percent((total - avail), total, round_=1) + return ntp.svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + wired, + ) def swap_memory(): """System swap memory as (total, used, free, sin, sout) namedtuple.""" - total, used, free, sin, sout = cext.swap_mem() + mem = cext.swap_mem() + if FREEBSD: + return ntp.sswap(**mem) + total, used, free, sin, sout = mem percent = usage_percent(used, total, round_=1) return ntp.sswap(total, used, free, percent, sin, sout) diff --git a/psutil/arch/all/init.h b/psutil/arch/all/init.h index b027b824c8..61734fd5ae 100644 --- a/psutil/arch/all/init.h +++ b/psutil/arch/all/init.h @@ -134,6 +134,7 @@ int pylist_append_obj(PyObject *list, PyObject *obj); int psutil_badargs(const char *funcname); int psutil_setup(void); +double psutil_usage_percent(double used, double total, int round_); // ==================================================================== // --- Exposed to Python diff --git a/psutil/arch/all/utils.c b/psutil/arch/all/utils.c index b0e4fbe4c6..dda570ecaa 100644 --- a/psutil/arch/all/utils.c +++ b/psutil/arch/all/utils.c @@ -1,5 +1,6 @@ #include #include +#include // Build a Python object from a format string, append it to a list, @@ -69,3 +70,16 @@ pydict_add(PyObject *dict, const char *key, const char *fmt, ...) { Py_DECREF(obj); return ret; } + + +double +psutil_usage_percent(double used, double total, int round_) { + double ret; + + if (total == 0.0) + return 0.0; + ret = (used / total) * 100.0; + if (round_ >= 0) + ret = round(ret * pow(10, round_)) / pow(10, round_); + return ret; +} diff --git a/psutil/arch/freebsd/mem.c b/psutil/arch/freebsd/mem.c index 5ebdd99279..ac3a234db7 100644 --- a/psutil/arch/freebsd/mem.c +++ b/psutil/arch/freebsd/mem.c @@ -28,46 +28,79 @@ sbn(const char *name, void *oldp, size_t oldlen) { PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { - unsigned long total; - unsigned int active, inactive, wired, cached, free; - long buffers; + unsigned long _total; + long _buffers; + unsigned int _active, _inactive, _wired, _cached, _free; + unsigned long long total, buffers, active, inactive, wired, cached, free; + unsigned long long avail, used, shared; + double percent; struct vmtotal vm; int mib[] = {CTL_VM, VM_METER}; long pagesize = psutil_getpagesize(); + PyObject *dict = PyDict_New(); - if (sbn("hw.physmem", &total, sizeof(total)) != 0) - return NULL; - if (sbn("vm.stats.vm.v_active_count", &active, sizeof(active)) != 0) - return NULL; - if (sbn("vm.stats.vm.v_inactive_count", &inactive, sizeof(inactive)) != 0) - return NULL; - if (sbn("vm.stats.vm.v_wire_count", &wired, sizeof(wired)) != 0) - return NULL; - if (sbn("vm.stats.vm.v_free_count", &free, sizeof(free)) != 0) - return NULL; - if (sbn("vfs.bufspace", &buffers, sizeof(buffers)) != 0) + if (dict == NULL) return NULL; - // Optional; ignore error if not available - if (sbn("vm.stats.vm.v_cache_count", &cached, sizeof(cached)) != 0) { + if (sbn("hw.physmem", &_total, sizeof(_total)) != 0) + goto error; + if (sbn("vm.stats.vm.v_active_count", &_active, sizeof(_active)) != 0) + goto error; + if (sbn("vm.stats.vm.v_inactive_count", &_inactive, sizeof(_inactive)) + != 0) + goto error; + if (sbn("vm.stats.vm.v_wire_count", &_wired, sizeof(_wired)) != 0) + goto error; + if (sbn("vm.stats.vm.v_free_count", &_free, sizeof(_free)) != 0) + goto error; + if (sbn("vfs.bufspace", &_buffers, sizeof(_buffers)) != 0) + goto error; + + // Optional; ignore error if not avail + if (sbn("vm.stats.vm.v_cache_count", &_cached, sizeof(_cached)) != 0) { PyErr_Clear(); - cached = 0; + _cached = 0; } if (psutil_sysctl(mib, 2, &vm, sizeof(vm)) != 0) - return psutil_oserror_wsyscall("sysctl(CTL_VM | VM_METER)"); - - return Py_BuildValue( - "KKKKKKKK", - (unsigned long long)total, - (unsigned long long)free * pagesize, - (unsigned long long)active * pagesize, - (unsigned long long)inactive * pagesize, - (unsigned long long)wired * pagesize, - (unsigned long long)cached * pagesize, - (unsigned long long)buffers, - (unsigned long long)(vm.t_vmshr + vm.t_rmshr) * pagesize // shared - ); + goto error; + + total = (unsigned long long)_total; + buffers = (unsigned long long)_buffers; + free = (unsigned long long)_free * pagesize; + active = (unsigned long long)_active * pagesize; + inactive = (unsigned long long)_inactive * pagesize; + wired = (unsigned long long)_wired * pagesize; + cached = (unsigned long long)_cached * pagesize; + shared = (unsigned long long)(vm.t_vmshr + vm.t_rmshr) * pagesize; + + // 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 + avail = (inactive + cached + free); + used = (active + wired + cached); + percent = psutil_usage_percent((double)(total - avail), (double)total, 1); + + if (!(pydict_add(dict, "total", "K", total) + | pydict_add(dict, "available", "K", avail) + | pydict_add(dict, "percent", "d", percent) + | pydict_add(dict, "used", "K", used) + | pydict_add(dict, "free", "K", free) + | pydict_add(dict, "active", "K", active) + | pydict_add(dict, "inactive", "K", inactive) + | pydict_add(dict, "buffers", "K", buffers) + | pydict_add(dict, "cached", "K", cached) + | pydict_add(dict, "shared", "K", shared) + | pydict_add(dict, "wired", "K", wired))) + goto error; + + return dict; + +error: + Py_DECREF(dict); + return NULL; } @@ -76,40 +109,55 @@ psutil_swap_mem(PyObject *self, PyObject *args) { // Return swap memory stats (see 'swapinfo' cmdline tool) kvm_t *kd; struct kvm_swap kvmsw[1]; - unsigned int free, swapin, swapout, nodein, nodeout; + unsigned long long total, used, free; + unsigned int swapin, swapout, nodein, nodeout; long pagesize = psutil_getpagesize(); + double percent; + PyObject *dict = PyDict_New(); + + if (dict == NULL) + return NULL; kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open failed"); if (kd == NULL) { psutil_runtime_error("kvm_open() syscall failed"); - return NULL; + goto error; } if (kvm_getswapinfo(kd, kvmsw, 1, 0) < 0) { kvm_close(kd); psutil_runtime_error("kvm_getswapinfo() syscall failed"); - return NULL; + goto error; } kvm_close(kd); if (sbn("vm.stats.vm.v_swapin", &swapin, sizeof(swapin)) != 0) - return NULL; + goto error; if (sbn("vm.stats.vm.v_swapout", &swapout, sizeof(swapout)) != 0) - return NULL; + goto error; if (sbn("vm.stats.vm.v_vnodein", &nodein, sizeof(nodein)) != 0) - return NULL; + goto error; if (sbn("vm.stats.vm.v_vnodeout", &nodeout, sizeof(nodeout)) != 0) - return NULL; - - free = (kvmsw[0].ksw_total - kvmsw[0].ksw_used); - - return Py_BuildValue( - "(KKKII)", - (unsigned long long)kvmsw[0].ksw_total * pagesize, // total - (unsigned long long)kvmsw[0].ksw_used * pagesize, // used - (unsigned long long)free * pagesize, // free - swapin + swapout, // swap in - nodein + nodeout // swap out - ); + goto error; + + total = (unsigned long long)kvmsw[0].ksw_total * pagesize; + used = (unsigned long long)kvmsw[0].ksw_used * pagesize; + free = (unsigned long long)(kvmsw[0].ksw_total - kvmsw[0].ksw_used) + * pagesize; + percent = psutil_usage_percent((double)used, (double)total, 1); + + if (!(pydict_add(dict, "total", "K", total) + | pydict_add(dict, "used", "K", used) + | pydict_add(dict, "free", "K", free) + | pydict_add(dict, "percent", "d", percent) + | pydict_add(dict, "sin", "I", swapin + swapout) + | pydict_add(dict, "sout", "I", nodein + nodeout))) + goto error; + + return dict; + +error: + Py_DECREF(dict); + return NULL; } diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index 7dc51e76ed..a75f79aa01 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -86,15 +86,14 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { // CLI utility. free -= _speculative; - // clang-format off - if (!pydict_add(dict, "total", "K", (unsigned long long)total)) goto error; - if (!pydict_add(dict, "available", "K", available)) goto error; - if (!pydict_add(dict, "used", "K", used)) goto error; - if (!pydict_add(dict, "free", "K", free)) goto error; - if (!pydict_add(dict, "active", "K", active)) goto error; - if (!pydict_add(dict, "inactive", "K", inactive)) goto error; - if (!pydict_add(dict, "wired", "K", wired)) goto error; - // clang-format on + if (!(pydict_add(dict, "total", "K", (unsigned long long)total) + | pydict_add(dict, "available", "K", available) + | pydict_add(dict, "used", "K", used) + | pydict_add(dict, "free", "K", free) + | pydict_add(dict, "active", "K", active) + | pydict_add(dict, "inactive", "K", inactive) + | pydict_add(dict, "wired", "K", wired))) + goto error; return dict; From c85a399e2945e7f5a3ded0f860b9759ac0a5390a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 22:33:01 +0100 Subject: [PATCH 1544/1714] NetBSD: fix double `free()` in `swap_memory()` (#2744) --- HISTORY.rst | 1 + psutil/arch/netbsd/mem.c | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 86edb503bb..ccf5918a31 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -47,6 +47,7 @@ calculation on macOS x86_64 (arm64 is fine) was highly inaccurate (41.67x lower). - 2732_, [Linux]: net_if_duplex_speed: handle EBUSY from ioctl(SIOCETHTOOL). +- 2744_, [NetBSD]: fix possible double `free()` in `swap_memory()`_. **Compatibility notes** diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 14338588ef..beb181a87e 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -54,7 +54,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { uint64_t swap_total, swap_free; - struct swapent *swdev; + struct swapent *swdev = NULL; int nswap, i; long pagesize = psutil_getpagesize(); @@ -84,16 +84,15 @@ psutil_swap_mem(PyObject *self, PyObject *args) { * DEV_BSIZE; } } - free(swdev); // Get swap in/out - unsigned int total; - size_t size = sizeof(total); struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) goto error; + free(swdev); + return Py_BuildValue( "(LLLll)", swap_total, From e09dab3c212fb25543774625cbfc35b01e49668a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 22:54:31 +0100 Subject: [PATCH 1545/1714] NetBSD, mem.c: return a dict, use intermediate vars, do percent in C --- psutil/_psbsd.py | 2 +- psutil/arch/netbsd/mem.c | 44 ++++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 81d2a8fde3..fb34b3ec75 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -171,7 +171,7 @@ def virtual_memory(): def swap_memory(): """System swap memory as (total, used, free, sin, sout) namedtuple.""" mem = cext.swap_mem() - if FREEBSD: + if FREEBSD or NETBSD: return ntp.sswap(**mem) total, used, free, sin, sout = mem percent = usage_percent(used, total, round_=1) diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index beb181a87e..a7b2ee94eb 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -53,21 +53,28 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { - uint64_t swap_total, swap_free; + uint64_t swap_total = 0; + uint64_t swap_free = 0; + uint64_t swap_used = 0; + uint64_t sin = 0; + uint64_t sout = 0; + double percent = 0.0; struct swapent *swdev = NULL; int nswap, i; long pagesize = psutil_getpagesize(); + PyObject *dict = PyDict_New(); + + if (dict == NULL) + return NULL; nswap = swapctl(SWAP_NSWAP, 0, 0); - if (nswap == 0) { - // This means there's no swap partition. - return Py_BuildValue("(iiiii)", 0, 0, 0, 0, 0); - } + if (nswap == 0) // this means there's no swap partition + goto done; swdev = calloc(nswap, sizeof(*swdev)); if (swdev == NULL) { psutil_oserror(); - return NULL; + goto error; } if (swapctl(SWAP_STATS, swdev, nswap) == -1) { @@ -76,7 +83,6 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } // Total things up. - swap_total = swap_free = 0; for (i = 0; i < nswap; i++) { if (swdev[i].se_flags & SWF_ENABLE) { swap_total += (uint64_t)swdev[i].se_nblks * DEV_BSIZE; @@ -93,16 +99,24 @@ psutil_swap_mem(PyObject *self, PyObject *args) { free(swdev); - return Py_BuildValue( - "(LLLll)", - swap_total, - (swap_total - swap_free), - swap_free, - (long)uv.pgswapin * pagesize, // swap in - (long)uv.pgswapout * pagesize // swap out - ); + sin = (uint64_t)uv.pgswapin * pagesize; + sout = (uint64_t)uv.pgswapout * pagesize; + swap_used = swap_total - swap_free; + percent = psutil_usage_percent((double)swap_used, (double)swap_total, 1); + +done: + if (!(pydict_add(dict, "total", "K", swap_total) + | pydict_add(dict, "used", "K", swap_used) + | pydict_add(dict, "free", "K", swap_free) + | pydict_add(dict, "percent", "d", percent) + | pydict_add(dict, "sin", "K", sin) + | pydict_add(dict, "sout", "K", sout))) + goto error; + + return dict; error: + Py_DECREF(dict); free(swdev); return NULL; } From d149ff6fc7097690dc6dd67a499cb158d9e52afa Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 4 Mar 2026 23:24:46 +0100 Subject: [PATCH 1546/1714] NetBSD, OpenBSD: use common bsd/mem.c implementation for swap_memory() --- MANIFEST.in | 1 + psutil/_psbsd.py | 12 +++-- psutil/arch/bsd/init.h | 3 ++ psutil/arch/bsd/mem.c | 89 ++++++++++++++++++++++++++++++++++++++ psutil/arch/netbsd/init.h | 1 - psutil/arch/netbsd/mem.c | 72 ------------------------------ psutil/arch/openbsd/init.h | 1 - psutil/arch/openbsd/mem.c | 50 --------------------- 8 files changed, 98 insertions(+), 131 deletions(-) create mode 100644 psutil/arch/bsd/mem.c diff --git a/MANIFEST.in b/MANIFEST.in index dc4afc78d4..dbb8953aad 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -59,6 +59,7 @@ include psutil/arch/bsd/disk.c include psutil/arch/bsd/heap.c include psutil/arch/bsd/init.c include psutil/arch/bsd/init.h +include psutil/arch/bsd/mem.c include psutil/arch/bsd/net.c include psutil/arch/bsd/proc.c include psutil/arch/bsd/proc_utils.c diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index fb34b3ec75..06c2661937 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -169,13 +169,11 @@ def virtual_memory(): def swap_memory(): - """System swap memory as (total, used, free, sin, sout) namedtuple.""" - mem = cext.swap_mem() - if FREEBSD or NETBSD: - return ntp.sswap(**mem) - total, used, free, sin, sout = mem - percent = usage_percent(used, total, round_=1) - return ntp.sswap(total, used, free, percent, sin, sout) + """System swap memory as a (total, used, free, percent, sin, sout) + named tuple. sin and sout are always 0 on OpenBSD + """ + d = cext.swap_mem() + return ntp.sswap(**d) # malloc / heap functions (FreeBSD / NetBSD) diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h index 8a5658e91b..6cc86f46ca 100644 --- a/psutil/arch/bsd/init.h +++ b/psutil/arch/bsd/init.h @@ -30,3 +30,6 @@ PyObject *psutil_proc_environ(PyObject *self, PyObject *args); PyObject *psutil_proc_name(PyObject *self, PyObject *args); PyObject *psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args); PyObject *psutil_proc_open_files(PyObject *self, PyObject *args); +#if defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +PyObject *psutil_swap_mem(PyObject *self, PyObject *args); +#endif diff --git a/psutil/arch/bsd/mem.c b/psutil/arch/bsd/mem.c new file mode 100644 index 0000000000..2085f9e573 --- /dev/null +++ b/psutil/arch/bsd/mem.c @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2009, Giampaolo Rodola', Landry Breuil. + * All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "../../arch/all/init.h" + +#if defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) +#include +#include +#include +#include +#include + + +PyObject * +psutil_swap_mem(PyObject *self, PyObject *args) { + uint64_t swap_total = 0; + uint64_t swap_free = 0; + uint64_t swap_used = 0; + uint64_t sin = 0; + uint64_t sout = 0; + double percent = 0.0; + struct swapent *swdev = NULL; + int nswap, i; + long pagesize = psutil_getpagesize(); + PyObject *dict = PyDict_New(); + + if (dict == NULL) + return NULL; + + nswap = swapctl(SWAP_NSWAP, 0, 0); + if (nswap == 0) // this means there's no swap partition + goto done; + + swdev = calloc(nswap, sizeof(*swdev)); + if (swdev == NULL) { + psutil_oserror(); + goto error; + } + + if (swapctl(SWAP_STATS, swdev, nswap) == -1) { + psutil_oserror(); + goto error; + } + + // Total things up. + for (i = 0; i < nswap; i++) { + if (swdev[i].se_flags & SWF_ENABLE) { + swap_total += (uint64_t)swdev[i].se_nblks * DEV_BSIZE; + swap_free += (uint64_t)(swdev[i].se_nblks - swdev[i].se_inuse) + * DEV_BSIZE; + } + } + +#if defined(PSUTIL_NETBSD) + // Get swap in/out + struct uvmexp_sysctl uv; + int mib[] = {CTL_VM, VM_UVMEXP2}; + if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) + goto error; + sin = (uint64_t)uv.pgswapin * pagesize; + sout = (uint64_t)uv.pgswapout * pagesize; +#endif + + free(swdev); + + swap_used = swap_total - swap_free; + percent = psutil_usage_percent((double)swap_used, (double)swap_total, 1); + +done: + if (!(pydict_add(dict, "total", "K", swap_total) + | pydict_add(dict, "used", "K", swap_used) + | pydict_add(dict, "free", "K", swap_free) + | pydict_add(dict, "percent", "d", percent) + | pydict_add(dict, "sin", "K", sin) + | pydict_add(dict, "sout", "K", sout))) + goto error; + + return dict; + +error: + Py_DECREF(dict); + free(swdev); + return NULL; +} +#endif // PSUTIL_OPENBSD || PSUTIL_NETBSD diff --git a/psutil/arch/netbsd/init.h b/psutil/arch/netbsd/init.h index d10626b020..2949da7856 100644 --- a/psutil/arch/netbsd/init.h +++ b/psutil/arch/netbsd/init.h @@ -23,5 +23,4 @@ PyObject *psutil_proc_net_connections(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_num_threads(PyObject *self, PyObject *args); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index a7b2ee94eb..65e50f2919 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -15,7 +15,6 @@ original(ish) implementations: */ #include -#include #include #include @@ -49,74 +48,3 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { cached // cached ); } - - -PyObject * -psutil_swap_mem(PyObject *self, PyObject *args) { - uint64_t swap_total = 0; - uint64_t swap_free = 0; - uint64_t swap_used = 0; - uint64_t sin = 0; - uint64_t sout = 0; - double percent = 0.0; - struct swapent *swdev = NULL; - int nswap, i; - long pagesize = psutil_getpagesize(); - PyObject *dict = PyDict_New(); - - if (dict == NULL) - return NULL; - - nswap = swapctl(SWAP_NSWAP, 0, 0); - if (nswap == 0) // this means there's no swap partition - goto done; - - swdev = calloc(nswap, sizeof(*swdev)); - if (swdev == NULL) { - psutil_oserror(); - goto error; - } - - if (swapctl(SWAP_STATS, swdev, nswap) == -1) { - psutil_oserror(); - goto error; - } - - // Total things up. - for (i = 0; i < nswap; i++) { - if (swdev[i].se_flags & SWF_ENABLE) { - swap_total += (uint64_t)swdev[i].se_nblks * DEV_BSIZE; - swap_free += (uint64_t)(swdev[i].se_nblks - swdev[i].se_inuse) - * DEV_BSIZE; - } - } - - // Get swap in/out - struct uvmexp_sysctl uv; - int mib[] = {CTL_VM, VM_UVMEXP2}; - if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) - goto error; - - free(swdev); - - sin = (uint64_t)uv.pgswapin * pagesize; - sout = (uint64_t)uv.pgswapout * pagesize; - swap_used = swap_total - swap_free; - percent = psutil_usage_percent((double)swap_used, (double)swap_total, 1); - -done: - if (!(pydict_add(dict, "total", "K", swap_total) - | pydict_add(dict, "used", "K", swap_used) - | pydict_add(dict, "free", "K", swap_free) - | pydict_add(dict, "percent", "d", percent) - | pydict_add(dict, "sin", "K", sin) - | pydict_add(dict, "sout", "K", sout))) - goto error; - - return dict; - -error: - Py_DECREF(dict); - free(swdev); - return NULL; -} diff --git a/psutil/arch/openbsd/init.h b/psutil/arch/openbsd/init.h index 120143dd6d..8d6879fc0f 100644 --- a/psutil/arch/openbsd/init.h +++ b/psutil/arch/openbsd/init.h @@ -18,6 +18,5 @@ PyObject *psutil_proc_cmdline(PyObject *self, PyObject *args); PyObject *psutil_proc_cwd(PyObject *self, PyObject *args); PyObject *psutil_proc_num_fds(PyObject *self, PyObject *args); PyObject *psutil_proc_threads(PyObject *self, PyObject *args); -PyObject *psutil_swap_mem(PyObject *self, PyObject *args); PyObject *psutil_users(PyObject *self, PyObject *args); PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index e2d5535e39..666dd4f425 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "../../arch/all/init.h" @@ -60,52 +59,3 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { (unsigned long long)vmdata.t_vmshr + vmdata.t_rmshr // shared ); } - - -PyObject * -psutil_swap_mem(PyObject *self, PyObject *args) { - uint64_t swap_total, swap_free; - struct swapent *swdev; - int nswap, i; - - if ((nswap = swapctl(SWAP_NSWAP, 0, 0)) == 0) { - psutil_oserror(); - return NULL; - } - - if ((swdev = calloc(nswap, sizeof(*swdev))) == NULL) { - PyErr_NoMemory(); - return NULL; - } - - if (swapctl(SWAP_STATS, swdev, nswap) == -1) { - psutil_oserror(); - goto error; - } - - // Total things up. - swap_total = swap_free = 0; - for (i = 0; i < nswap; i++) { - if (swdev[i].se_flags & SWF_ENABLE) { - swap_free += (swdev[i].se_nblks - swdev[i].se_inuse); - swap_total += swdev[i].se_nblks; - } - } - - free(swdev); - return Py_BuildValue( - "(LLLII)", - swap_total * DEV_BSIZE, - (swap_total - swap_free) * DEV_BSIZE, - swap_free * DEV_BSIZE, - // swap in / swap out is not supported as the - // swapent struct does not provide any info - // about it. - 0, - 0 - ); - -error: - free(swdev); - return NULL; -} From a41365256b1032286f7d32fc92aca6bebdb5eb49 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 00:08:56 +0100 Subject: [PATCH 1547/1714] OpenBSD, mem.c: return a dict, use intermediate vars, do percent in C --- psutil/_psbsd.py | 26 +------------ psutil/arch/openbsd/mem.c | 81 +++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 06c2661937..292c73a819 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -140,32 +140,8 @@ def virtual_memory(): shared, wired, ) - elif FREEBSD: - return ntp.svmem(**mem) 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 - avail = inactive + cached + free - used = active + wired + cached - - percent = usage_percent((total - avail), total, round_=1) - return ntp.svmem( - total, - avail, - percent, - used, - free, - active, - inactive, - buffers, - cached, - shared, - wired, - ) + return ntp.svmem(**mem) def swap_memory(): diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index 666dd4f425..7ef2acaef7 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -16,46 +16,71 @@ PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { - int64_t total_physmem; + int64_t _total; + unsigned long long free, active, inactive, wired, cached, shared; + unsigned long long total, avail, used; + double percent; int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; int bcstats_mib[] = {CTL_VFS, VFS_GENERIC, VFS_BCACHESTAT}; int physmem_mib[] = {CTL_HW, HW_PHYSMEM64}; int vmmeter_mib[] = {CTL_VM, VM_METER}; - size_t size; struct uvmexp uvmexp; struct bcachestats bcstats; struct vmtotal vmdata; long pagesize = psutil_getpagesize(); + PyObject *dict = PyDict_New(); - size = sizeof(total_physmem); - if (psutil_sysctl(physmem_mib, 2, &total_physmem, size) != 0) + if (dict == NULL) return NULL; - size = sizeof(uvmexp); - if (psutil_sysctl(uvmexp_mib, 2, &uvmexp, size) != 0) - return NULL; + // Note: many programs calculate total memory as "uvmexp.npages * + // pagesize" but this is incorrect and does not match "sysctl | + // grep hw.physmem". + if (psutil_sysctl(physmem_mib, 2, &_total, sizeof(_total)) != 0) + goto error; + if (psutil_sysctl(uvmexp_mib, 2, &uvmexp, sizeof(uvmexp)) != 0) + goto error; + if (psutil_sysctl(bcstats_mib, 3, &bcstats, sizeof(bcstats)) != 0) + goto error; + if (psutil_sysctl(vmmeter_mib, 2, &vmdata, sizeof(vmdata)) != 0) + goto error; - size = sizeof(bcstats); - if (psutil_sysctl(bcstats_mib, 3, &bcstats, size) != 0) - return NULL; + total = (unsigned long long)_total; + free = (unsigned long long)uvmexp.free * pagesize; + active = (unsigned long long)uvmexp.active * pagesize; + inactive = (unsigned long long)uvmexp.inactive * pagesize; + wired = (unsigned long long)uvmexp.wired * pagesize; + shared = (unsigned long long)vmdata.t_vmshr + vmdata.t_rmshr; - size = sizeof(vmdata); - if (psutil_sysctl(vmmeter_mib, 2, &vmdata, size) != 0) - return NULL; + // this is how "top" determines cached mem + cached = (unsigned long long)bcstats.numbufpages * pagesize; + + // 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 + avail = inactive + cached + free; + used = active + wired + cached; + percent = psutil_usage_percent((double)(total - avail), (double)total, 1); + + if (!(pydict_add(dict, "total", "K", total) + | pydict_add(dict, "available", "K", avail) + | pydict_add(dict, "percent", "d", percent) + | pydict_add(dict, "used", "K", used) + | pydict_add(dict, "free", "K", free) + | pydict_add(dict, "active", "K", active) + | pydict_add(dict, "inactive", "K", inactive) + | pydict_add(dict, "buffers", "K", 0ULL) + | pydict_add(dict, "cached", "K", cached) + | pydict_add(dict, "shared", "K", shared) + | pydict_add(dict, "wired", "K", wired))) + goto error; + + return dict; - return Py_BuildValue( - "KKKKKKKK", - // Note: many programs calculate total memory as - // "uvmexp.npages * pagesize" but this is incorrect and does not - // match "sysctl | grep hw.physmem". - (unsigned long long)total_physmem, - (unsigned long long)uvmexp.free * pagesize, - (unsigned long long)uvmexp.active * pagesize, - (unsigned long long)uvmexp.inactive * pagesize, - (unsigned long long)uvmexp.wired * pagesize, - // this is how "top" determines it - (unsigned long long)bcstats.numbufpages * pagesize, // cached - (unsigned long long)0, // buffers - (unsigned long long)vmdata.t_vmshr + vmdata.t_rmshr // shared - ); +error: + Py_DECREF(dict); + return NULL; } From 77db1c2d42d5dec96b34ed408ebaf3ad58641a75 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 00:24:21 +0100 Subject: [PATCH 1548/1714] NetBSD, mem.c: return a dict, use intermediate vars, do percent in C --- psutil/_psbsd.py | 31 ++----------------- psutil/arch/netbsd/mem.c | 66 +++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 292c73a819..d495787c90 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -27,7 +27,6 @@ from ._common import debug from ._common import memoize from ._common import memoize_when_activated -from ._common import usage_percent __extra__all__ = [] @@ -108,38 +107,14 @@ def virtual_memory(): mem = cext.virtual_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: for line in f: if line.startswith(b'Buffers:'): - buffers = int(line.split()[1]) * 1024 + mem['buffers'] = int(line.split()[1]) * 1024 elif line.startswith(b'MemShared:'): - shared = int(line.split()[1]) * 1024 - # 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 - # https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L135 - used = active + wired - avail = total - used - percent = usage_percent((total - avail), total, round_=1) - return ntp.svmem( - total, - avail, - percent, - used, - free, - active, - inactive, - buffers, - cached, - shared, - wired, - ) + mem['shared'] = int(line.split()[1]) * 1024 + return ntp.svmem(**mem) else: return ntp.svmem(**mem) diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 65e50f2919..2cedfc4edb 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -22,29 +22,59 @@ original(ish) implementations: // Virtual memory stats, taken from: -// https://github.com/satterly/zabbix-stats/blob/master/src/libs/zbxsysinfo/ -// netbsd/memory.c +// https://github.com/satterly/zabbix-stats/blob/master/src/libs/zbxsysinfo/netbsd/memory.c PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { - size_t size; struct uvmexp_sysctl uv; int mib[] = {CTL_VM, VM_UVMEXP2}; - long long cached; + unsigned long long total, free, active, inactive, wired, cached; + unsigned long long used, avail; + double percent; + PyObject *dict = PyDict_New(); - if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) + if (dict == NULL) return NULL; - // Note: zabbix does not include anonpages, but that doesn't match the - // "Cached" value in /proc/meminfo. - // https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L182 - cached = (uv.filepages + uv.execpages + uv.anonpages) << uv.pageshift; - return Py_BuildValue( - "LLLLLL", - (long long)uv.npages << uv.pageshift, // total - (long long)uv.free << uv.pageshift, // free - (long long)uv.active << uv.pageshift, // active - (long long)uv.inactive << uv.pageshift, // inactive - (long long)uv.wired << uv.pageshift, // wired - cached // cached - ); + if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) + goto error; + + total = (unsigned long long)uv.npages << uv.pageshift; + free = (unsigned long long)uv.free << uv.pageshift; + active = (unsigned long long)uv.active << uv.pageshift; + inactive = (unsigned long long)uv.inactive << uv.pageshift; + wired = (unsigned long long)uv.wired << uv.pageshift; + + // Note: zabbix does not include anonpages, but that doesn't match + // the "Cached" value in /proc/meminfo. + // https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/ + // zbxsysinfo/netbsd/memory.c#L182 + cached = (unsigned long long)(uv.filepages + uv.execpages + uv.anonpages) + << uv.pageshift; + + // 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 + // https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L135 + used = active + wired; + avail = total - used; + percent = psutil_usage_percent((double)(total - avail), (double)total, 1); + + if (!(pydict_add(dict, "total", "K", total) + | pydict_add(dict, "available", "K", avail) + | pydict_add(dict, "percent", "d", percent) + | pydict_add(dict, "used", "K", used) + | pydict_add(dict, "free", "K", free) + | pydict_add(dict, "active", "K", active) + | pydict_add(dict, "inactive", "K", inactive) + | pydict_add(dict, "wired", "K", wired) + | pydict_add(dict, "cached", "K", cached))) + goto error; + + return dict; + +error: + Py_DECREF(dict); + return NULL; } From 55bd354eff77cc9ccec7e729e47fad3b1ba01ec8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 00:38:42 +0100 Subject: [PATCH 1549/1714] NetBSD, vmem: parse /proc/meminfo in C --- psutil/_psbsd.py | 14 ++------------ psutil/arch/netbsd/mem.c | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index d495787c90..282a5d8717 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -105,18 +105,8 @@ def virtual_memory(): - mem = cext.virtual_mem() - if NETBSD: - # On NetBSD buffers and shared mem is determined via /proc. - with open('/proc/meminfo', 'rb') as f: - for line in f: - if line.startswith(b'Buffers:'): - mem['buffers'] = int(line.split()[1]) * 1024 - elif line.startswith(b'MemShared:'): - mem['shared'] = int(line.split()[1]) * 1024 - return ntp.svmem(**mem) - else: - return ntp.svmem(**mem) + d = cext.virtual_mem() + return ntp.svmem(**d) def swap_memory(): diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 2cedfc4edb..6a6c9c675f 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -15,7 +15,10 @@ original(ish) implementations: */ #include +#include +#include #include +#include #include #include "../../arch/all/init.h" @@ -26,23 +29,46 @@ original(ish) implementations: PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { struct uvmexp_sysctl uv; - int mib[] = {CTL_VM, VM_UVMEXP2}; + struct vmtotal vmdata; + int uvmexp_mib[] = {CTL_VM, VM_UVMEXP2}; + int vmmeter_mib[] = {CTL_VM, VM_METER}; unsigned long long total, free, active, inactive, wired, cached; - unsigned long long used, avail; + unsigned long long buffers, shared, used, avail; double percent; + long pagesize = psutil_getpagesize(); + FILE *f; + char line[256]; PyObject *dict = PyDict_New(); if (dict == NULL) return NULL; - if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) + if (psutil_sysctl(uvmexp_mib, 2, &uv, sizeof(uv)) != 0) goto error; + if (psutil_sysctl(vmmeter_mib, 2, &vmdata, sizeof(vmdata)) != 0) + goto error; + + // get buffers from /proc/meminfo + buffers = 0; + f = fopen("/proc/meminfo", "r"); + if (f != NULL) { + while (fgets(line, sizeof(line), f) != NULL) { + if (strncmp(line, "Buffers:", 8) == 0) { + sscanf(line + 8, "%llu", &buffers); + break; + } + } + fclose(f); + buffers *= 1024; + } total = (unsigned long long)uv.npages << uv.pageshift; free = (unsigned long long)uv.free << uv.pageshift; active = (unsigned long long)uv.active << uv.pageshift; inactive = (unsigned long long)uv.inactive << uv.pageshift; wired = (unsigned long long)uv.wired << uv.pageshift; + // Also in /proc/meminfo as MemShared + shared = (unsigned long long)(vmdata.t_vmshr + vmdata.t_rmshr) * pagesize; // Note: zabbix does not include anonpages, but that doesn't match // the "Cached" value in /proc/meminfo. @@ -68,8 +94,10 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { | pydict_add(dict, "free", "K", free) | pydict_add(dict, "active", "K", active) | pydict_add(dict, "inactive", "K", inactive) + | pydict_add(dict, "buffers", "K", buffers) | pydict_add(dict, "wired", "K", wired) - | pydict_add(dict, "cached", "K", cached))) + | pydict_add(dict, "cached", "K", cached) + | pydict_add(dict, "shared", "K", shared))) goto error; return dict; From 043629a61697701ddaed8ff9266c75cfec1e3c16 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 01:28:54 +0100 Subject: [PATCH 1550/1714] Drastically improve `virtual_memory()` doc (#2745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System memory is notoriously hard to understand, and it gets even trickier in psutil, because psutil tries to provide cross-platform consistency by exposing a set of common metrics, but those metrics aren’t truly "common" across operating systems. Because of this, `virtual_memory()` documentation has historically been poor: it's vague, sometimes inaccurate, and doesn't clearly explain what each field actually represents. To improve it, I used Sonnet 4.6 with the following prompt: > Take a look at `virtual_memory()` in @docs/index.rst. Considering 1) the table below already shows implementation details on major platforms, and 2) all the code in the mem.c files in this project... provide a more accurate and descriptive explanation for each field (total, shared, buffers, etc.) so that the end user can understand what these metrics mean in practice. --- HISTORY.rst | 3 ++ docs/index.rst | 61 +++++++++++++++++++++++++--------------- psutil/arch/netbsd/mem.c | 2 +- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ccf5918a31..4bb154138f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,9 @@ - 2729_: New `Process.page_faults()`_ method, returning a ``(minor, major)`` namedtuple. +- 2745_: Drastically improve `virtual_memory()`_ docstring, which is now more + detailed, and includes a table with all the available metrics on each + platform. - 2731_, 2736_, 2723_, 2733_: Reorganization of process memory APIs. - Add new `Process.memory_info_ex()`_ method, which extends diff --git a/docs/index.rst b/docs/index.rst index 6cddb9b927..04f361096e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -340,29 +340,44 @@ Memory following fields, expressed in bytes. - **total**: total physical memory (exclusive swap). - - **available**: the memory that can be given instantly to processes without - the system going into swap. This may be calculated by summing different - memory metrics that vary depending on the platform. It is supposed to be - used to monitor actual memory usage in a cross-platform fashion. - - **percent**: the percentage usage calculated as ``(total - available) / total * 100``. - - **used**: memory used, calculated differently depending on the platform and - designed for informational purposes only. **total - free** does not - necessarily match **used**. - - **free**: memory not being used at all (zeroed) that is readily available; - note that this doesn't reflect the actual memory available (use - **available** instead). **total - used** does not necessarily match - **free**. - - **active** *(Linux, macOS, BSD)*: memory currently in use or very recently - used, and so it is in RAM. - - **inactive** *(Linux, macOS, BSD)*: memory that is marked as not used. - - **buffers** *(Linux, BSD)*: cache for things like file system metadata. - - **cached** *(Linux, BSD)*: cache for files read from the disk (the page - cache) - - **shared** *(Linux, BSD)*: memory that may be simultaneously accessed by - multiple processes. - - **slab** *(Linux)*: in-kernel data structures cache. - - **wired** *(macOS, BSD)*: memory that is marked to always stay in RAM. It is - never moved to disk. + - **available**: memory that can be given instantly to processes without the + system going into swap. It is calculated by summing different memory values + depending on the platform (on Linux it matches the ``MemAvailable`` kernel + field). This is the recommended field for monitoring actual memory usage + in a cross-platform fashion. + - **percent**: the percentage usage calculated as + ``(total - available) / total * 100``. + - **used**: memory in use, calculated differently depending on the platform + (see the table below). It is meant for informational purposes. Neither + ``total - free`` nor ``total - available`` necessarily equals ``used``. + - **free**: memory that is not in use at all (zeroed pages). This is + typically much lower than **available** because the OS keeps recently + freed memory as reclaimable cache (see **cached** and **buffers**) + rather than zeroing it immediately. Do not use this to check for + memory pressure; use **available** instead. + - **active** *(Linux, macOS, BSD)*: memory currently mapped by processes + or recently accessed, held in RAM. It is unlikely to be reclaimed unless + the system is under significant memory pressure. + - **inactive** *(Linux, macOS, BSD)*: memory not recently accessed. It + still holds valid data (file cache, old allocations) but is a candidate + for reclamation or swapping. On BSD systems it is counted in + **available**. + - **buffers** *(Linux, BSD)*: kernel buffer cache for raw block-device I/O + (e.g. disk blocks read before the filesystem processes them). Can be + reclaimed by the OS when needed. + - **cached** *(Linux, BSD)*: in-memory page cache for files read from disk. + The OS reclaims this memory automatically when processes need it, so a + large **cached** value is healthy and does not indicate memory pressure. + On Linux this includes ``SReclaimable`` (reclaimable slab memory). + - **shared** *(Linux, BSD)*: memory accessible by multiple processes + simultaneously, such as POSIX shared memory (``shm_open``) and + ``tmpfs``-backed files. On Linux this corresponds to ``Shmem`` in + ``/proc/meminfo`` and is already counted within **active** / **inactive**. + - **slab** *(Linux)*: memory used by the kernel's internal object caches + (e.g. inode and dentry caches). The reclaimable portion + (``SReclaimable``) is already included in **cached**. + - **wired** *(macOS, BSD)*: memory pinned in RAM by the kernel (e.g. kernel + code and critical data structures). It can never be moved to disk. Follows a table showing implementation details. All info on Linux are retrieved from `/proc/meminfo`. diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index 6a6c9c675f..dfeb7bac70 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -25,7 +25,7 @@ original(ish) implementations: // Virtual memory stats, taken from: -// https://github.com/satterly/zabbix-stats/blob/master/src/libs/zbxsysinfo/netbsd/memory.c +// https://github.com/zabbix/zabbix/blob/master/src/libs/zbxsysinfo/netbsd/memory.c PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { struct uvmexp_sysctl uv; From a26ff816efcf27825d364bb669e098db78a7c089 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 01:46:35 +0100 Subject: [PATCH 1551/1714] Improve swap_memory() doc (Sonnet 4.6) --- docs/index.rst | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 04f361096e..5906a89c0d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -474,16 +474,22 @@ Memory Return system swap memory statistics as a named tuple including the following fields: - * **total**: total swap memory in bytes - * **used**: used swap memory in bytes - * **free**: free swap memory in bytes - * **percent**: the percentage usage calculated as ``(total - available) / total * 100`` - * **sin**: the number of bytes the system has swapped in from disk - (cumulative) - * **sout**: the number of bytes the system has swapped out from disk - (cumulative) - - **sin** and **sout** on Windows are always set to ``0``. + * **total**: total swap space. On Windows this is derived as + ``CommitLimit - PhysicalTotal``, representing virtual memory backed by + the page file rather than the raw page-file size. + * **used**: swap space currently in use. + * **free**: swap space not in use (``total - used``). + * **percent**: swap usage as a percentage, calculated as + ``used / total * 100``. + * **sin**: number of bytes the system has paged *in* from disk (pages moved + from swap space back into RAM) since boot (cumulative). + * **sout**: number of bytes the system has paged *out* to disk (pages moved + from RAM into swap space) since boot (cumulative). A continuously + increasing **sout** is a sign of memory pressure. + + **sin** and **sout** are cumulative counters since boot; monitor their rate + of change rather than the absolute value to detect active swapping. + On Windows both are always ``0``. See `meminfo.py`_ script providing an example on how to convert bytes in a human readable form. From 0ccf5bf31abbcacf69e8dc57220b523ad52c5bc0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 02:00:14 +0100 Subject: [PATCH 1552/1714] Improve memory_info() doc (Sonnet 4.6) --- docs/index.rst | 70 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5906a89c0d..ce6a0cdb54 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1752,33 +1752,61 @@ Process class | | | | | | peak_nonpaged_pool (maps to ``QuotaPeakNonPagedPoolUsage``) | +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - - **rss**: aka "Resident Set Size", this is the non-swapped physical memory - a process is using. On UNIX it matches ``top`` RES column. + - **rss**: aka "Resident Set Size". The portion of physical memory + currently held by this process (code, data, stack, and mapped files + that are resident). Pages swapped out to disk are **not** counted. + On UNIX it matches the ``top`` RES column. - - **vms**: aka "Virtual Memory Size", this is the total amount of virtual - memory used by the process. On UNIX it matches ``top`` VIRT column. On - Windows it maps to ``PrivateUsage``, which is close to, but not exactly - the same as the VMS definition used on UNIX. For that, use ``virtual`` - from :meth:`memory_info_ex`. + - **vms**: aka "Virtual Memory Size". The total address space reserved by + the process, including pages not yet touched, pages in swap, and + memory-mapped files not yet accessed. Typically much larger than + **rss**. On UNIX it matches the ``top`` VIRT column. On Windows this + maps to ``PrivateUsage`` (private committed pages only), which differs + from the UNIX definition; use ``virtual`` from :meth:`memory_info_ex` + for the true virtual address space size. - - **shared**: *(Linux)* - memory that could be potentially shared with other processes. - This matches "top"'s SHR column). + - **shared** *(Linux)*: memory backed by a file or device (shared + libraries, mmap'd files, POSIX shared memory) that *could* be shared + with other processes. A page is counted here even if no other process + is currently mapping it. Matches ``top``'s SHR column. - - **text** *(Linux, BSD)*: - aka TRS (text resident set) the amount of memory devoted to - executable code. This matches "top"'s CODE column). + - **text** *(Linux, BSD)*: aka TRS (Text Resident Set). Resident memory + devoted to executable code. These pages are read-only and typically + shared across all processes running the same binary. Matches ``top``'s + CODE column. - - **data** *(Linux, BSD)*: - aka DRS (Data Resident Set) the amount of physical memory devoted to - other than executable code. It matches "top"'s DATA column. + - **data** *(Linux, BSD)*: aka DRS (Data Resident Set). On Linux this + covers the data **and** stack segments combined (from + ``/proc//statm``). On BSD it covers the data segment only (see + **stack**). Matches ``top``'s DATA column. - - **peak_rss** *(BSD)*: aka "peak Resident Set Size" or "high water mark". - It's the highest amount of physical memory the process has ever used at - any point during its lifetime. Can be 0 for kernel PIDs. + - **stack** *(BSD)*: size of the process stack segment. Reported + separately from **data** (unlike Linux where both are combined). - For on explanation of Windows fields rely on `PROCESS_MEMORY_COUNTERS_EX`_ - doc. + - **peak_rss** *(BSD, Windows)*: the highest RSS value (high water mark) + the process has ever reached. On BSD this may be ``0`` for kernel PIDs. + + - **num_page_faults** *(Windows)*: total page faults (soft + hard) since + the process started. A hard fault requires a disk read and indicates + memory pressure. + + - **paged_pool** *(Windows)*: kernel memory used for objects created by + this process (open file handles, registry keys, etc.) that the OS may + swap to disk under memory pressure. + + - **nonpaged_pool** *(Windows)*: kernel memory used for objects that must + stay in RAM at all times (I/O request packets, device driver buffers, + etc.). A large or growing value may indicate a driver memory leak. + + - **peak_vms** *(Windows)*: peak private committed (page-file-backed) + virtual memory. + + - **peak_paged_pool** *(Windows)*: peak paged-pool usage. + + - **peak_nonpaged_pool** *(Windows)*: peak non-paged-pool usage. + + For the full definitions of Windows fields see + `PROCESS_MEMORY_COUNTERS_EX`_. Example on Linux: From 36bd1b5214664672e7d1fcfd68a367553e3e76dc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 02:17:32 +0100 Subject: [PATCH 1553/1714] Improve memory_info_ex() doc (Sonnet 4.6) --- docs/index.rst | 51 ++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ce6a0cdb54..f1f8d0d70e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1866,30 +1866,33 @@ Process class | hugetlb | phys_footprint | | +-------------+----------------+--------------+ - - **peak_rss**: aka "peak Resident Set Size" or "high water mark". It's the - highest amount of physical memory the process has ever used at any point - during its lifetime. - - **peak_vms** *(Linux)*: aka "peak Virtual Memory Size". It's highest - amount of virtual memory the process has ever used at any point during - its lifetime. - - **rss_anon** *(Linux, macOS)*: anonymous resident memory (heap, stack, - etc.). On macOS this maps to ``task_vm_info.internal``. - - **rss_file** *(Linux, macOS)*: file-backed resident memory. On macOS this - maps to ``task_vm_info.external``. - - **rss_shmem** *(Linux)*: shared memory resident pages. - - **wired** *(macOS)*: memory that is marked to always stay in RAM. It is - never moved to disk. - - **swap** *(Linux)*: memory swapped out to disk. Equivalent to - ``memory_footprint().swap`` but faster, as it reads from - */proc/pid/status* instead of */proc/pid/smaps*. - - **hugetlb** *(Linux)*: memory backed by huge TLB pages. - - **phys_footprint** *(macOS)*: total physical memory footprint including - compressed pages; this is what Xcode's memory gauge shows. - - **virtual** *(Windows)*: total virtual address space size. Unlike ``vms`` - in :meth:`memory_info`, this is the true virtual memory size - (``VirtualSize`` from ``SYSTEM_PROCESS_INFORMATION``). - - **peak_virtual** *(Windows)*: peak virtual address space size - (``VirtualPeakSize`` from ``SYSTEM_PROCESS_INFORMATION``). + - **peak_rss** *(Linux, macOS)*: the highest RSS value (high water mark) + the process has reached since it started. + - **peak_vms** *(Linux)*: the highest VMS value the process has reached + since it started. + - **rss_anon** *(Linux, macOS)*: resident anonymous pages (heap, stack, + private mappings) not backed by any file, such as heap allocations, + stack, and private ``mmap(MAP_ANONYMOUS)`` regions. + - **rss_file** *(Linux, macOS)*: resident file-backed memory; pages mapped + from files (shared libraries, mmap'd files). + - **rss_shmem** *(Linux)*: resident shared memory pages (``tmpfs``, + ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals **rss**. + - **wired** *(macOS)*: memory pinned in RAM by the kernel on behalf of this + process; cannot be compressed or paged out. + - **swap** *(Linux)*: process memory currently in swap. Equivalent to + ``memory_footprint().swap`` but cheaper, as it reads from + ``/proc//status`` instead of ``/proc//smaps``. + - **compressed** *(macOS)*: pages held in the in-RAM memory compressor; not + counted in **rss**. A large value signals memory pressure but has not yet + triggered swapping. + - **hugetlb** *(Linux)*: resident memory backed by huge pages. + - **phys_footprint** *(macOS)*: total physical memory impact including + compressed pages. What Xcode and ``footprint(1)`` report; prefer this + over **rss** macOS memory monitoring. + - **virtual** *(Windows)*: true virtual address space size, including + reserved-but-uncommitted regions (unlike **vms** in + :meth:`memory_info`). + - **peak_virtual** *(Windows)*: peak virtual address space size. .. versionadded:: 8.0.0 From 5521099b657f3dcfc9bae65fd40ab714f224312a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 02:26:33 +0100 Subject: [PATCH 1554/1714] Improve memory_footprint() doc (Sonnet 4.6) --- docs/index.rst | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f1f8d0d70e..95b7f4cdf3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1898,31 +1898,27 @@ Process class .. method:: memory_footprint() - Return a named tuple with USS, PSS and swap memory metrics. - These provide a better representation of actual process memory - consumption as explained in detail in this + Return a named tuple with USS, PSS and swap memory metrics. These give + a more accurate picture of actual memory consumption than + :meth:`memory_info`, as explained in this `blog post `__. - It does so by passing through the whole process address. - As such it usually requires higher user privileges than - :meth:`memory_info` or :meth:`memory_info_ex` and is considerably slower. + It works by walking the full process address space, so it is + considerably slower than :meth:`memory_info` and may require elevated + privileges. - - **uss** *(Linux, macOS, Windows)*: - aka "Unique Set Size", this is the memory which is unique to a process - and which would be freed if the process was terminated right now. + - **uss** *(Linux, macOS, Windows)*: aka "Unique Set Size". This is the + memory which is unique to a process and which would be freed if the + process were terminated right now. The most representative metric for + actual memory usage. - **pss** *(Linux)*: aka "Proportional Set Size", is the amount of memory shared with other processes, accounted in a way that the amount is - divided evenly between the processes that share it. - I.e. if a process has 10 MBs all to itself and 10 MBs shared with - another process its PSS will be 15 MBs. + divided evenly between the processes that share it. I.e. if a process has + 10 MBs all to itself, and 10 MBs shared with another process, its PSS + will be 15 MBs. - - **swap** *(Linux)*: amount of memory that has been swapped out to disk. - - .. note:: - `uss` is probably the most representative metric for determining how - much memory is actually being used by a process. - It represents the amount of memory that would be freed if the process - was terminated right now. + - **swap** *(Linux)*: process memory currently in swap, counted per-mapping + (slower, but may be more accurate than ``memory_info_ex().swap``). Example on Linux: From f9ed4ec04136007e9761c1bcb257c660717e5bbf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 02:28:00 +0100 Subject: [PATCH 1555/1714] Improve memory_percent() doc (Sonnet 4.6) --- docs/index.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 95b7f4cdf3..cc572e7a2a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1945,12 +1945,11 @@ Process class .. method:: memory_percent(memtype="rss") - Compare process memory to total physical system memory and calculate - process memory utilization as a percentage. - *memtype* argument is a string that dictates what type of process memory - you want to compare against. You can choose between the named tuple field - names returned by :meth:`memory_info`, :meth:`memory_info_ex` and - :meth:`memory_footprint` (defaults to ``"rss"``). + Return process memory usage as a percentage of total physical memory + (``process.memory_info().rss / virtual_memory().total * 100``). + *memtype* can be any field name from :meth:`memory_info`, + :meth:`memory_info_ex`, or :meth:`memory_footprint` and controls which + memory value is used in the calculation (defaults to ``"rss"``). .. versionchanged:: 4.0.0 added `memtype` parameter. From 6ab020f01dfcd48617b221095e8a2edf8bc28b90 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 02:32:48 +0100 Subject: [PATCH 1556/1714] Improve memory_maps() doc (Sonnet 4.6) --- docs/index.rst | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index cc572e7a2a..666ca3d336 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1956,16 +1956,11 @@ Process class .. method:: memory_maps(grouped=True) Return process's mapped memory regions as a list of named tuples whose - fields are variable depending on the platform. - This method is useful to obtain a detailed representation of process - memory usage as explained - `here `__ - (the most important value is "private" memory). - If *grouped* is ``True`` the mapped regions with the same *path* are - grouped together and the different memory fields are summed. If *grouped* - is ``False`` each mapped region is shown as a single entity and the - named tuple will also include the mapped region's address space (*addr*) - and permission set (*perms*). + fields vary by platform (all values in bytes). If *grouped* is ``True`` + regions with the same *path* are merged and their numeric fields summed. + If *grouped* is ``False`` each region is listed individually and the + tuple also includes *addr* (address range) and *perms* (permission + string e.g. ``"r-xp"``). See `pmap.py`_ for an example application. +---------------+---------+--------------+-----------+ @@ -1992,6 +1987,31 @@ Process class | swap | | | | +---------------+---------+--------------+-----------+ + Linux fields (from ``/proc//smaps``): + + - **rss**: resident pages in this mapping. + - **size**: total virtual size; may far exceed **rss** for sparse or + reserved-but-unaccessed mappings. + - **pss**: proportional RSS. **rss** divided by the number of processes + sharing this mapping. Useful for fair per-process accounting. + - **shared_clean**: shared pages not modified (e.g. shared library code); + can be dropped from RAM without writing to swap. + - **shared_dirty**: shared pages that have been written to. + - **private_clean**: private unmodified pages; can be dropped without + writing to swap. + - **private_dirty**: private modified pages; must be written to swap + before they can be reclaimed. The key indicator of a mapping's real + memory cost. + - **referenced**: pages recently accessed. + - **anonymous**: pages not backed by a file (heap, stack allocations). + - **swap**: pages from this mapping currently in swap. + + FreeBSD fields: + + - **private**: pages in this mapping private to this process. + - **ref_count**: reference count on the VM object backing this mapping. + - **shadow_count**: depth of the copy-on-write shadow object chain. + >>> import psutil >>> p = psutil.Process() >>> p.memory_maps() @@ -2367,7 +2387,7 @@ steadily across iterations, the C code is likely retaining memory it should be releasing. This provides an allocator-level way to spot native leaks that Python's memory tracking misses. -Checkout `psleak`_ project to see a practical example of how these APIs can be +Check out `psleak`_ project to see a practical example of how these APIs can be used to detect memory leaks in C extensions. .. function:: heap_info() From 027bfba4483367d703bd83d5cec196d66a283ec7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 10:18:50 +0100 Subject: [PATCH 1557/1714] Update doc mentioning mem matches against system tools --- docs/index.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 666ca3d336..3166525082 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -379,6 +379,14 @@ Memory - **wired** *(macOS, BSD)*: memory pinned in RAM by the kernel (e.g. kernel code and critical data structures). It can never be moved to disk. + .. note:: + - On Linux, **total**, **free**, **used**, **shared**, and **available** + match the output of the ``free`` command. + - On macOS, **free**, **active**, **inactive**, and **wired** match + ``vm_stat`` output. + - On Windows, **total**, **used** ("In use"), and **available** match + the Task Manager (Performance > Memory tab). + Follows a table showing implementation details. All info on Linux are retrieved from `/proc/meminfo`. .. list-table:: From b43571f954105cbdd885525a77d0c03668fad7b5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 10:27:05 +0100 Subject: [PATCH 1558/1714] Shorten URLs in comments --- psutil/__init__.py | 7 ++----- psutil/_psbsd.py | 3 +-- psutil/_pslinux.py | 15 ++++----------- psutil/_pssunos.py | 3 +-- psutil/_pswindows.py | 7 ++----- tests/test_linux.py | 15 +++++---------- tests/test_system.py | 3 +-- 7 files changed, 16 insertions(+), 37 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index d65e1f8a9e..1a4ab098fe 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1805,9 +1805,7 @@ def _cpu_tot_time(times): # Htop does the same. References: # https://github.com/giampaolo/psutil/pull/940 # http://unix.stackexchange.com/questions/178045 - # https://github.com/torvalds/linux/blob/ - # 447976ef4fd09b1be88b316d1a81553f1aa7cd07/kernel/sched/ - # cputime.c#L158 + # https://github.com/torvalds/linux/blob/447976ef4/kernel/sched/cputime.c#L158 tot -= getattr(times, "guest", 0) # Linux 2.6.24+ tot -= getattr(times, "guest_nice", 0) # Linux 3.2.0+ return tot @@ -1823,8 +1821,7 @@ def _cpu_busy_time(times): # (waits for IO to complete). On Linux IO wait is *not* accounted # in "idle" time so we subtract it. Htop does the same. # References: - # https://github.com/torvalds/linux/blob/ - # 447976ef4fd09b1be88b316d1a81553f1aa7cd07/kernel/sched/cputime.c#L244 + # https://github.com/torvalds/linux/blob/447976ef4/kernel/sched/cputime.c#L244 busy -= getattr(times, "iowait", 0) return busy diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 282a5d8717..4ecb2e1843 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -555,8 +555,7 @@ def exe(self): return os.readlink(f"/proc/{self.pid}/exe") else: # OpenBSD: exe cannot be determined; references: - # https://chromium.googlesource.com/chromium/src/base/+/ - # master/base_paths_posix.cc + # https://chromium.googlesource.com/chromium/src/base/+/master/base_paths_posix.cc # We try our best guess by using which against the first # cmdline arg (may return None). import shutil diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 24718ab842..b4b7ddfd25 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -186,8 +186,7 @@ def is_storage_device(name): return True. """ # Re-adapted from iostat source code, see: - # https://github.com/sysstat/sysstat/blob/ - # 97912938cd476645b267280069e83b1c8dc0e1c7/common.c#L208 + # https://github.com/sysstat/sysstat/blob/97912938cd476/common.c#L208 # Some devices may have a slash in their name (e.g. cciss/c0d0...). name = name.replace('/', '!') including_virtual = True @@ -262,8 +261,7 @@ def calculate_avail_vmem(mems): * https://github.com/famzah/linux-memavailable-procfs/issues/2 """ # Note about "fallback" value. According to: - # https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ - # commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 + # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398 # ...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 @@ -338,8 +336,7 @@ def virtual_memory(): # "free" cmdline utility sums reclaimable to cached. # Older versions of procps used to add slab memory instead. # This got changed in: - # https://gitlab.com/procps-ng/procps/commit/ - # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # https://gitlab.com/procps-ng/procps/-/commit/05d751c4f cached += mems.get(b"SReclaimable:", 0) # since kernel 2.6.19 try: @@ -401,8 +398,7 @@ def virtual_memory(): # 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 + # https://gitlab.com/procps-ng/procps/blob/24fd2605c51fcc/proc/sysinfo.c#L764 avail = free used = total - avail @@ -1468,9 +1464,6 @@ def multi_bcat(*paths): power_plugged = True # Seconds left. - # Note to self: we may also calculate the charging ETA as per: - # https://github.com/thialfihar/dotfiles/blob/ - # 013937745fd9050c30146290e8f963d65c0179e6/bin/battery.py#L55 if power_plugged: secsleft = _common.POWER_TIME_UNLIMITED elif energy_now is not None and power_now is not None: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 76e85966d4..c7d1325ad1 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -109,8 +109,7 @@ def swap_memory(): sin, sout = cext.swap_mem() # XXX # we are supposed to get total/free by doing so: - # http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/ - # usr/src/cmd/swap/swap.c + # http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/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( diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index b8e98562a0..93016e23b0 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -368,8 +368,7 @@ def net_if_addrs(): def sensors_battery(): """Return battery information.""" # For constants meaning see: - # https://msdn.microsoft.com/en-us/library/windows/desktop/ - # aa373232(v=vs.85).aspx + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa373232(v=vs.85).aspx acline_status, flags, percent, secsleft = cext.sensors_battery() power_plugged = acline_status == 1 no_battery = bool(flags & 128) @@ -877,9 +876,7 @@ def wait(self, timeout=None): # We'll just rely on the internal polling and return None # when the PID disappears. Subprocess module does the same # (return None): - # https://github.com/python/cpython/blob/ - # be50a7b627d0aa37e08fa8e2d5568891f19903ce/ - # Lib/subprocess.py#L1193-L1194 + # https://github.com/python/cpython/blob/be50a7b627d0/Lib/subprocess.py#L1193-L1194 exit_code = None # At this point WaitForSingleObject() returned WAIT_OBJECT_0, diff --git a/tests/test_linux.py b/tests/test_linux.py index cb517d5852..5c66348d43 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -235,12 +235,10 @@ def test_total(self): 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 + # https://gitlab.com/procps-ng/procps/-/commit/05d751c4f07 # Newer versions of procps (>=4.0.1) are using yet another way to # compute used memory. - # https://gitlab.com/procps-ng/procps/commit/ - # 2184e90d2e7cdb582f9a5b706b47015e56707e4d + # https://gitlab.com/procps-ng/procps/-/commit/2184e90d2e7 if get_free_version_info() < (4, 0, 1): return pytest.skip("free version too old") cli_value = free_physmem().used @@ -288,12 +286,10 @@ def test_total(self): 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 + # https://gitlab.com/procps-ng/procps/-/commit/05d751c4f07 # Newer versions of procps (>=4.0.1) are using yet another way to # compute used memory. - # https://gitlab.com/procps-ng/procps/commit/ - # 2184e90d2e7cdb582f9a5b706b47015e56707e4d + # https://gitlab.com/procps-ng/procps/-/commit/2184e90d2e7 if get_free_version_info() < (4, 0, 1): return pytest.skip("free version too old") vmstat_value = vmstat('used memory') * 1024 @@ -1990,8 +1986,7 @@ def test_cmdline_spaces_mocked(self): assert m.called def test_cmdline_mixed_separators(self): - # https://github.com/giampaolo/psutil/issues/ - # 1179#issuecomment-552984549 + # https://github.com/giampaolo/psutil/issues/1179#issuecomment-552984549 p = psutil.Process() fake_file = io.StringIO('foo\x20bar\x00') with mock.patch( diff --git a/tests/test_system.py b/tests/test_system.py index 0adfa4f055..d756b22fd1 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -725,8 +725,7 @@ def check_ntuple(nt): 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 + # http://mail.python.org/pipermail/python-dev/2012-June/120787.html if err.errno not in {errno.EPERM, errno.EACCES}: raise else: From bcc75ba818bc54c53e7f8ad6897411161ce10a2d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 15:46:14 +0100 Subject: [PATCH 1559/1714] Move low-level proc memory metrics into memory_info_ex() These are: *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, *peak_nonpaged_pool* --- HISTORY.rst | 8 +-- docs/index.rst | 112 +++++++++++++++++++++--------------------- psutil/_ntuples.py | 54 +++++++++++--------- psutil/_pswindows.py | 24 +++++++-- tests/test_process.py | 8 +-- 5 files changed, 113 insertions(+), 93 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4bb154138f..e0fc1fedbc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -35,9 +35,11 @@ - macOS: *pfaults* and *pageins* removed with **no backward-compataliases**. Use `Process.page_faults()`_ instead. - - Windows: renamed fields (old names kept as deprecated aliases): *wset* → - *rss*, *peak_wset* → *peak_rss*, *pagefile* / *private* → *vms*, - *peak_pagefile* → *peak_vms*. + - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. + At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, + *peak_nonpaged_pool* were moved to `Process.memory_info_ex()`_. All these + old names still work but raise `DeprecationWarning`. - `Process.memory_full_info()`_ is **deprecated**. Use the new `Process.memory_footprint()`_ instead. diff --git a/docs/index.rst b/docs/index.rst index 3166525082..521749f577 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1738,32 +1738,28 @@ Process class The "portable" fields available on all platforms are `rss` and `vms`. All numbers are expressed in bytes. - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | Linux | macOS | BSD | Solaris | AIX | Windows | - +=========+=========+==========+=========+=====+=============================================================+ - | rss | rss | rss | rss | rss | rss (maps to ``WorkingSetSize``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | vms | vms | vms | vms | vms | vms (maps to ``PrivateUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | shared | | text | | | num_page_faults (maps to ``PageFaultCount``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | text | | data | | | paged_pool (maps to ``QuotaPagedPoolUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | data | | stack | | | nonpaged_pool (maps to ``QuotaNonPagedPoolUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | peak_rss | | | peak_rss (maps to ``PeakWorkingSetSize``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | peak_vms (maps to ``PeakPagefileUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | peak_paged_pool (maps to ``QuotaPeakPagedPoolUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ - | | | | | | peak_nonpaged_pool (maps to ``QuotaPeakNonPagedPoolUsage``) | - +---------+---------+----------+---------+-----+-------------------------------------------------------------+ + +---------+---------+----------+---------+-----+----------+ + | Linux | macOS | BSD | Solaris | AIX | Windows | + +=========+=========+==========+=========+=====+==========+ + | rss | rss | rss | rss | rss | rss | + +---------+---------+----------+---------+-----+----------+ + | vms | vms | vms | vms | vms | vms | + +---------+---------+----------+---------+-----+----------+ + | shared | | text | | | | + +---------+---------+----------+---------+-----+----------+ + | text | | data | | | | + +---------+---------+----------+---------+-----+----------+ + | data | | stack | | | | + +---------+---------+----------+---------+-----+----------+ + | | | peak_rss | | | peak_rss | + +---------+---------+----------+---------+-----+----------+ + | | | | | | peak_vms | + +---------+---------+----------+---------+-----+----------+ - **rss**: aka "Resident Set Size". The portion of physical memory - currently held by this process (code, data, stack, and mapped files - that are resident). Pages swapped out to disk are **not** counted. - On UNIX it matches the ``top`` RES column. + currently held by this process (code, data, stack, and mapped files that + are resident). Pages swapped out to disk are **not** counted. On UNIX it + matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. - **vms**: aka "Virtual Memory Size". The total address space reserved by the process, including pages not yet touched, pages in swap, and @@ -1793,25 +1789,14 @@ Process class - **peak_rss** *(BSD, Windows)*: the highest RSS value (high water mark) the process has ever reached. On BSD this may be ``0`` for kernel PIDs. + On Windows it maps to ``PeakWorkingSetSize``. - **num_page_faults** *(Windows)*: total page faults (soft + hard) since the process started. A hard fault requires a disk read and indicates memory pressure. - - **paged_pool** *(Windows)*: kernel memory used for objects created by - this process (open file handles, registry keys, etc.) that the OS may - swap to disk under memory pressure. - - - **nonpaged_pool** *(Windows)*: kernel memory used for objects that must - stay in RAM at all times (I/O request packets, device driver buffers, - etc.). A large or growing value may indicate a driver memory leak. - - **peak_vms** *(Windows)*: peak private committed (page-file-backed) - virtual memory. - - - **peak_paged_pool** *(Windows)*: peak paged-pool usage. - - - **peak_nonpaged_pool** *(Windows)*: peak non-paged-pool usage. + virtual memory. Maps to ``PeakPagefileUsage``. For the full definitions of Windows fields see `PROCESS_MEMORY_COUNTERS_EX`_. @@ -1836,9 +1821,11 @@ Process class aliases**. Use :meth:`page_faults` instead. .. versionchanged:: - 8.0.0 Windows: renamed fields (old names kept as deprecated aliases): - *wset* → *rss*, *peak_wset* → *peak_rss*, *pagefile* / *private* → - *vms*, *peak_pagefile* → *peak_vms*. + 8.0.0 Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. + At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, + *peak_nonpaged_pool* were moved to :meth:`memory_info_ex`. All these old + names still work but raise `DeprecationWarning`. .. versionchanged:: 8.0.0 BSD: added *peak_rss*. @@ -1856,23 +1843,23 @@ Process class implemented this returns the same result as :meth:`memory_info`. All numbers are expressed in bytes. - +-------------+----------------+--------------+ - | Linux | macOS | Windows | - +=============+================+==============+ - | peak_rss | peak_rss | virtual | - +-------------+----------------+--------------+ - | peak_vms | | peak_virtual | - +-------------+----------------+--------------+ - | rss_anon | rss_anon | | - +-------------+----------------+--------------+ - | rss_file | rss_file | | - +-------------+----------------+--------------+ - | rss_shmem | wired | | - +-------------+----------------+--------------+ - | swap | compressed | | - +-------------+----------------+--------------+ - | hugetlb | phys_footprint | | - +-------------+----------------+--------------+ + +-------------+----------------+--------------------+ + | Linux | macOS | Windows | + +=============+================+====================+ + | peak_rss | peak_rss | virtual | + +-------------+----------------+--------------------+ + | peak_vms | | peak_virtual | + +-------------+----------------+--------------------+ + | rss_anon | rss_anon | paged_pool | + +-------------+----------------+--------------------+ + | rss_file | rss_file | nonpaged_pool | + +-------------+----------------+--------------------+ + | rss_shmem | wired | peak_paged_pool | + +-------------+----------------+--------------------+ + | swap | compressed | peak_nonpaged_pool | + +-------------+----------------+--------------------+ + | hugetlb | phys_footprint | | + +-------------+----------------+--------------------+ - **peak_rss** *(Linux, macOS)*: the highest RSS value (high water mark) the process has reached since it started. @@ -1901,6 +1888,17 @@ Process class reserved-but-uncommitted regions (unlike **vms** in :meth:`memory_info`). - **peak_virtual** *(Windows)*: peak virtual address space size. + - **paged_pool** *(Windows)*: kernel memory used for objects created by + this process (open file handles, registry keys, etc.) that the OS may + swap to disk under memory pressure. + - **nonpaged_pool** *(Windows)*: kernel memory used for objects that must + stay in RAM at all times (I/O request packets, device driver buffers, + etc.). A large or growing value may indicate a driver memory leak. + - **peak_paged_pool** *(Windows)*: peak paged-pool usage. + - **peak_nonpaged_pool** *(Windows)*: peak non-paged-pool usage. + + For the full definitions of Windows fields see + `PROCESS_MEMORY_COUNTERS_EX`_. .. versionadded:: 8.0.0 diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 45fa533a6a..618817ef61 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -12,7 +12,6 @@ from ._common import MACOS from ._common import SUNOS from ._common import WINDOWS -from ._common import deprecated_property # =================================================================== # --- system functions @@ -277,36 +276,43 @@ def dirty(self): svmem = nt("svmem", ("total", "available", "percent", "used", "free")) # psutil.Process.memory_info() - _pmem = nt( - "pmem", - ( - "rss", - "vms", - "num_page_faults", + class pmem( # noqa: SLOT002 + nt("pmem", ("rss", "vms", "peak_rss", "peak_vms", "num_page_faults")) + ): + def __new__( + cls, rss, vms, peak_rss, peak_vms, num_page_faults, _deprecated + ): + inst = super().__new__( + cls, rss, vms, peak_rss, peak_vms, num_page_faults + ) + inst.__dict__['_deprecated'] = _deprecated + return inst + + def __getattr__(self, name): + dep = self.__dict__.get('_deprecated', {}) + if name in dep: + msg = ( + f"pmem.{name} is deprecated; use memory_info_ex() instead" + ) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return dep[name] + msg = f"{self.__class__.__name__} object has no attribute {name!r}" + raise AttributeError(msg) + + # psutil.Process.memory_info_ex() + _pmem_ex = nt( + "pmem_ex", + pmem._fields + + ( + "virtual", + "peak_virtual", "paged_pool", "nonpaged_pool", - "peak_rss", - "peak_vms", "peak_paged_pool", "peak_nonpaged_pool", ), ) - class pmem(_pmem): - __slots__ = () - - wset = deprecated_property(replacement="rss") - peak_wset = deprecated_property(replacement="peak_rss") - pagefile = deprecated_property(replacement="vms") - peak_pagefile = deprecated_property(replacement="peak_vms") - private = deprecated_property(replacement="vms") - - # psutil.Process.memory_info_ex() - _pmem_ex = nt( - "pmem_ex", - pmem._fields + ("virtual", "peak_virtual"), - ) - class pmem_ex(pmem, _pmem_ex): __slots__ = () _fields = _pmem_ex._fields diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 93016e23b0..e1a39a7829 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -793,21 +793,35 @@ def memory_info(self): return ntp.pmem( rss=d["WorkingSetSize"], vms=d["PrivateUsage"], - num_page_faults=d["PageFaultCount"], - paged_pool=d["QuotaPagedPoolUsage"], - nonpaged_pool=d["QuotaNonPagedPoolUsage"], peak_rss=d["PeakWorkingSetSize"], peak_vms=d["PeakPagefileUsage"], - peak_paged_pool=d["QuotaPeakPagedPoolUsage"], - peak_nonpaged_pool=d["QuotaPeakNonPagedPoolUsage"], + num_page_faults=d["PageFaultCount"], + _deprecated={ + # old aliases + "wset": d["WorkingSetSize"], # 'rss' + "peak_wset": d["PeakWorkingSetSize"], # 'peak_rss' + "pagefile": d["PrivateUsage"], # 'vms' + "private": d["PrivateUsage"], # 'vms' + "peak_pagefile": d["PeakPagefileUsage"], # 'vms' + # fields which were moved to memory_info_ex() + "paged_pool": d["QuotaPagedPoolUsage"], + "nonpaged_pool": d["QuotaNonPagedPoolUsage"], + "peak_paged_pool": d["QuotaPeakPagedPoolUsage"], + "peak_nonpaged_pool": d["QuotaPeakNonPagedPoolUsage"], + }, ) @wrap_exceptions def memory_info_ex(self): d = self._oneshot() + raw = self._get_raw_meminfo() return { "virtual": d["VirtualSize"], "peak_virtual": d["PeakVirtualSize"], + "paged_pool": raw["QuotaPagedPoolUsage"], + "nonpaged_pool": raw["QuotaNonPagedPoolUsage"], + "peak_paged_pool": raw["QuotaPeakPagedPoolUsage"], + "peak_nonpaged_pool": raw["QuotaPeakNonPagedPoolUsage"], } @wrap_exceptions diff --git a/tests/test_process.py b/tests/test_process.py index fc7ab2294b..26738c197f 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -507,15 +507,15 @@ def test_memory_info_ex_fields_order(self): ) elif WINDOWS: assert mem._fields[2:] == ( + "peak_rss", + "peak_vms", "num_page_faults", + "virtual", + "peak_virtual", "paged_pool", "nonpaged_pool", - "peak_rss", - "peak_vms", "peak_paged_pool", "peak_nonpaged_pool", - "virtual", - "peak_virtual", ) else: assert mem._fields == psutil.Process().memory_info_ex()._fields From 1fb66b324621c42cc5443d8c6fa083844198ba8d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 16:26:05 +0100 Subject: [PATCH 1560/1714] Fix win failing test + add new ones --- docs/index.rst | 46 +++++++++++++++++----------------- psutil/_ntuples.py | 21 ++++++++++++---- tests/__init__.py | 6 ----- tests/test_windows.py | 57 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 88 insertions(+), 42 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 521749f577..0182a2756f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1738,23 +1738,25 @@ Process class The "portable" fields available on all platforms are `rss` and `vms`. All numbers are expressed in bytes. - +---------+---------+----------+---------+-----+----------+ - | Linux | macOS | BSD | Solaris | AIX | Windows | - +=========+=========+==========+=========+=====+==========+ - | rss | rss | rss | rss | rss | rss | - +---------+---------+----------+---------+-----+----------+ - | vms | vms | vms | vms | vms | vms | - +---------+---------+----------+---------+-----+----------+ - | shared | | text | | | | - +---------+---------+----------+---------+-----+----------+ - | text | | data | | | | - +---------+---------+----------+---------+-----+----------+ - | data | | stack | | | | - +---------+---------+----------+---------+-----+----------+ - | | | peak_rss | | | peak_rss | - +---------+---------+----------+---------+-----+----------+ - | | | | | | peak_vms | - +---------+---------+----------+---------+-----+----------+ + +---------+---------+----------+---------+-----+-----------------+ + | Linux | macOS | BSD | Solaris | AIX | Windows | + +=========+=========+==========+=========+=====+=================+ + | rss | rss | rss | rss | rss | rss | + +---------+---------+----------+---------+-----+-----------------+ + | vms | vms | vms | vms | vms | vms | + +---------+---------+----------+---------+-----+-----------------+ + | shared | | text | | | | + +---------+---------+----------+---------+-----+-----------------+ + | text | | data | | | | + +---------+---------+----------+---------+-----+-----------------+ + | data | | stack | | | | + +---------+---------+----------+---------+-----+-----------------+ + | | | peak_rss | | | peak_rss | + +---------+---------+----------+---------+-----+-----------------+ + | | | | | | peak_vms | + +---------+---------+----------+---------+-----+-----------------+ + | | | | | | num_page_faults | + +---------+---------+----------+---------+-----+-----------------+ - **rss**: aka "Resident Set Size". The portion of physical memory currently held by this process (code, data, stack, and mapped files that @@ -1791,13 +1793,13 @@ Process class the process has ever reached. On BSD this may be ``0`` for kernel PIDs. On Windows it maps to ``PeakWorkingSetSize``. + - **peak_vms** *(Windows)*: peak private committed (page-file-backed) + virtual memory. Maps to ``PeakPagefileUsage``. + - **num_page_faults** *(Windows)*: total page faults (soft + hard) since the process started. A hard fault requires a disk read and indicates memory pressure. - - **peak_vms** *(Windows)*: peak private committed (page-file-backed) - virtual memory. Maps to ``PeakPagefileUsage``. - For the full definitions of Windows fields see `PROCESS_MEMORY_COUNTERS_EX`_. @@ -2062,7 +2064,7 @@ Process class .. method:: page_faults() Return the number of page faults for this process as a ``(minor, major)`` - namedtuple. + named tuple. - **minor** (a.k.a. *soft* faults): occur when a memory page is not currently mapped into the process address space, but is already present @@ -2400,7 +2402,7 @@ used to detect memory leaks in C extensions. Return low-level heap statistics from the system's C allocator. On Linux, this exposes ``uordblks`` and ``hblkhd`` fields from glibc's `mallinfo2`_. - Returns a namedtuple containing: + Returns a named tuple containing: - ``heap_used``: total number of bytes currently allocated via ``malloc()`` (small allocations). diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 618817ef61..f7b32dbb9c 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -280,20 +280,31 @@ class pmem( # noqa: SLOT002 nt("pmem", ("rss", "vms", "peak_rss", "peak_vms", "num_page_faults")) ): def __new__( - cls, rss, vms, peak_rss, peak_vms, num_page_faults, _deprecated + cls, + rss, + vms, + peak_rss, + peak_vms, + num_page_faults, + _deprecated=None, ): inst = super().__new__( cls, rss, vms, peak_rss, peak_vms, num_page_faults ) - inst.__dict__['_deprecated'] = _deprecated + inst.__dict__['_deprecated'] = _deprecated or {} return inst def __getattr__(self, name): dep = self.__dict__.get('_deprecated', {}) if name in dep: - msg = ( - f"pmem.{name} is deprecated; use memory_info_ex() instead" - ) + msg = f"pmem.{name} is deprecated" + if name in { + "paged_pool", + "nonpaged_pool", + "peak_paged_pool", + "peak_nonpaged_pool", + }: + msg += "; use memory_info_ex() instead" warnings.warn(msg, DeprecationWarning, stacklevel=2) return dep[name] msg = f"{self.__class__.__name__} object has no attribute {name!r}" diff --git a/tests/__init__.py b/tests/__init__.py index 586c73fa0b..e48d7a1ded 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1074,12 +1074,6 @@ def check_proc_memory(self, nt): # tolerance. diff = nt.rss - nt.peak_rss assert diff <= nt.rss * 0.05 - if hasattr(nt, "peak_vms"): - assert nt.peak_vms >= nt.vms - if hasattr(nt, "peak_paged_pool"): # Windows - assert nt.peak_paged_pool >= nt.paged_pool - if hasattr(nt, "peak_nonpaged_pool"): # Windows - assert nt.peak_nonpaged_pool >= nt.nonpaged_pool def is_win_secure_system_proc(pid): diff --git a/tests/test_windows.py b/tests/test_windows.py index 7138e28977..38dabc3246 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -491,16 +491,18 @@ def test_nice(self): def test_memory_info(self): win = win32process.GetProcessMemoryInfo(self.OpenProcess(self.pid)) ps = psutil.Process(self.pid).memory_info() + assert ps.rss == win["WorkingSetSize"] + assert ps.vms == win["PagefileUsage"] + assert ps.vms == win["PrivateUsage"] + assert ps.peak_rss == win["PeakWorkingSetSize"] + assert ps.peak_vms == win["PeakPagefileUsage"] + assert ps.num_page_faults == win["PageFaultCount"] + + def test_memory_info_deprecated_fields(self): + win = win32process.GetProcessMemoryInfo(self.OpenProcess(self.pid)) + ps = psutil.Process(self.pid).memory_info() - assert ps.rss == win['WorkingSetSize'] - assert ps.vms == win['PagefileUsage'] - assert ps.paged_pool == win['QuotaPagedPoolUsage'] - assert ps.nonpaged_pool == win['QuotaNonPagedPoolUsage'] - assert ps.peak_rss == win['PeakWorkingSetSize'] - assert ps.peak_vms == win['PeakPagefileUsage'] - assert ps.peak_paged_pool == win['QuotaPeakPagedPoolUsage'] - assert ps.peak_nonpaged_pool == win['QuotaPeakNonPagedPoolUsage'] - + # old aliases with pytest.warns(DeprecationWarning, match="wset is deprecated"): assert ps.wset == ps.rss with pytest.warns(DeprecationWarning, match="peak_wset is deprecated"): @@ -514,6 +516,43 @@ def test_memory_info(self): ): assert ps.peak_pagefile == ps.peak_vms + # fields moved to memory_info_ex() + with pytest.warns( + DeprecationWarning, match="paged_pool is deprecated" + ): + assert ps.paged_pool == win['QuotaPagedPoolUsage'] + with pytest.warns( + DeprecationWarning, match="nonpaged_pool is deprecated" + ): + assert ps.nonpaged_pool == win['QuotaNonPagedPoolUsage'] + with pytest.warns( + DeprecationWarning, match="peak_paged_pool is deprecated" + ): + assert ps.peak_paged_pool == win['QuotaPeakPagedPoolUsage'] + with pytest.warns( + DeprecationWarning, match="peak_nonpaged_pool is deprecated" + ): + assert ps.peak_nonpaged_pool == win['QuotaPeakNonPagedPoolUsage'] + + # test ntuple's __getattr__ override + with pytest.raises(AttributeError, match="foo"): + ps.foo # noqa: B018 + + def test_memory_info_ex(self): + win = win32process.GetProcessMemoryInfo(self.OpenProcess(self.pid)) + ps = psutil.Process(self.pid).memory_info_ex() + assert ps.paged_pool == win["QuotaPagedPoolUsage"] + assert ps.nonpaged_pool == win["QuotaNonPagedPoolUsage"] + assert ps.peak_paged_pool == win["QuotaPeakPagedPoolUsage"] + assert ps.peak_nonpaged_pool == win["QuotaPeakNonPagedPoolUsage"] + + assert ps.virtual > 0 + assert ps.peak_virtual > 0 + + assert ps.peak_virtual >= ps.virtual + assert ps.peak_paged_pool >= ps.paged_pool + assert ps.peak_nonpaged_pool >= ps.nonpaged_pool + def test_wait(self): p = psutil.Process(self.pid) p.terminate() From 3bc0bee8a2acc4199a4fea931960e9e706af6e52 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 16:41:31 +0100 Subject: [PATCH 1561/1714] Fix win failing test --- tests/test_windows.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_windows.py b/tests/test_windows.py index 38dabc3246..2a8c29cfb3 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -493,7 +493,6 @@ def test_memory_info(self): ps = psutil.Process(self.pid).memory_info() assert ps.rss == win["WorkingSetSize"] assert ps.vms == win["PagefileUsage"] - assert ps.vms == win["PrivateUsage"] assert ps.peak_rss == win["PeakWorkingSetSize"] assert ps.peak_vms == win["PeakPagefileUsage"] assert ps.num_page_faults == win["PageFaultCount"] From fa69d0f2634ab2a6fa0c7ebf3d60f9fa63fed3e1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 20:16:35 +0100 Subject: [PATCH 1562/1714] FreeBSD: memory_maps() rss and private fields were expressed in pages and not bytes --- HISTORY.rst | 3 +++ psutil/arch/freebsd/proc.c | 8 +++++--- tests/test_bsd.py | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e0fc1fedbc..2d16e10a8c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -53,6 +53,9 @@ lower). - 2732_, [Linux]: net_if_duplex_speed: handle EBUSY from ioctl(SIOCETHTOOL). - 2744_, [NetBSD]: fix possible double `free()` in `swap_memory()`_. +- 2746_, [FreeBSD]: `Process.memory_maps()`_, `rss` and `private` fields, are + erroneously reported in memory pages instead of bytes. Other platforms + (Linux, macOS, Windows) return bytes. **Compatibility notes** diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index c0306af0f5..731891fdbb 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -291,6 +291,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { struct kinfo_vmentry *freep = NULL; struct kinfo_vmentry *kve; ptrwidth = 2 * sizeof(void *); + long pagesize = psutil_getpagesize(); PyObject *py_path = NULL; PyObject *py_retlist = PyList_New(0); @@ -382,12 +383,13 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { goto error; if (!pylist_append_fmt( py_retlist, - "ssOiiii", + "ssOKKii", addr, // "start-end" address perms, // "rwx" permissions py_path, // path - kve->kve_resident, // rss - kve->kve_private_resident, // private + (unsigned long long)kve->kve_resident * pagesize, // rss + (unsigned long long)kve->kve_private_resident // private + * pagesize, kve->kve_ref_count, // ref count kve->kve_shadow_count // shadow count )) diff --git a/tests/test_bsd.py b/tests/test_bsd.py index f2169a2b2e..60b3e1dd78 100755 --- a/tests/test_bsd.py +++ b/tests/test_bsd.py @@ -157,7 +157,7 @@ def test_net_if_stats(self): @pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") -class FreeBSDPsutilTestCase(PsutilTestCase): +class FreeBSDTestCase(PsutilTestCase): @classmethod def setUpClass(cls): cls.pid = spawn_subproc().pid @@ -177,7 +177,7 @@ def test_memory_maps(self): _, start, stop, _perms, res = fields[:5] map = maps.pop() assert f"{start}-{stop}" == map.addr - assert int(res) == map.rss + assert int(res) * PAGESIZE == map.rss if not map.path.startswith('['): assert fields[10] == map.path From afca8a3883806227182bdefbcade223e2f881505 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 5 Mar 2026 21:39:47 +0100 Subject: [PATCH 1563/1714] Show VMS mem in scripts/procsmem.py --- scripts/procsmem.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/procsmem.py b/scripts/procsmem.py index 90b6cf8cbc..6ccb08b593 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -39,8 +39,8 @@ import psutil -if not (psutil.LINUX or psutil.MACOS or psutil.WINDOWS): - sys.exit("platform not supported") +if not hasattr(psutil.Process, "memory_footprint"): + sys.exit("can't retrieve USS memory on this platform") def convert_bytes(n): @@ -70,6 +70,7 @@ def main(): else: p._uss = mem.uss p._rss = info["memory_info"].rss + p._vms = info["memory_info"].vms if not p._uss: continue p._pss = getattr(mem, "pss", "") @@ -78,9 +79,12 @@ def main(): procs.append(p) procs.sort(key=lambda p: p._uss) - templ = "{:<7} {:<7} {:>7} {:>7} {:>7} {:>7} {:>7}" - print(templ.format("PID", "User", "USS", "PSS", "Swap", "RSS", "Cmdline")) - print("=" * 78) + templ = "{:<7} {:<7} {:>7} {:>7} {:>7} {:>7} {:>7} {}" + header = templ.format( + "PID", "User", "USS", "PSS", "Swap", "RSS", "VMS", "Cmdline" + ) + print(header) + print("=" * len(header)) for p in procs[:86]: cmd = " ".join(p._info["cmdline"])[:50] if p._info["cmdline"] else "" line = templ.format( @@ -90,6 +94,7 @@ def main(): convert_bytes(p._pss) if p._pss else "", convert_bytes(p._swap) if p._swap else "", convert_bytes(p._rss), + convert_bytes(p._vms), cmd, ) print(line) From d79a70c816efa49622358319087cb66fe5dad008 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 6 Mar 2026 01:41:01 +0100 Subject: [PATCH 1564/1714] Windows: remove num_page_faults from memory_info() (deprecate + alias) --- docs/index.rst | 15 +++++---------- psutil/_ntuples.py | 16 +++------------- psutil/_pswindows.py | 7 ++++--- psutil/arch/osx/proc.c | 4 ++-- tests/test_windows.py | 6 ++++++ 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 0182a2756f..f70dace259 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1755,8 +1755,6 @@ Process class +---------+---------+----------+---------+-----+-----------------+ | | | | | | peak_vms | +---------+---------+----------+---------+-----+-----------------+ - | | | | | | num_page_faults | - +---------+---------+----------+---------+-----+-----------------+ - **rss**: aka "Resident Set Size". The portion of physical memory currently held by this process (code, data, stack, and mapped files that @@ -1796,10 +1794,6 @@ Process class - **peak_vms** *(Windows)*: peak private committed (page-file-backed) virtual memory. Maps to ``PeakPagefileUsage``. - - **num_page_faults** *(Windows)*: total page faults (soft + hard) since - the process started. A hard fault requires a disk read and indicates - memory pressure. - For the full definitions of Windows fields see `PROCESS_MEMORY_COUNTERS_EX`_. @@ -1824,10 +1818,11 @@ Process class .. versionchanged:: 8.0.0 Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. - At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, - *peak_nonpaged_pool* were moved to :meth:`memory_info_ex`. All these old - names still work but raise `DeprecationWarning`. + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*, + *num_page_faults* → :meth:`page_faults` method. At the same time + *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, *peak_nonpaged_pool* + were moved to :meth:`memory_info_ex`. All these old names still work but + raise `DeprecationWarning`. .. versionchanged:: 8.0.0 BSD: added *peak_rss*. diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index f7b32dbb9c..9bc40780dd 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -277,20 +277,10 @@ def dirty(self): # psutil.Process.memory_info() class pmem( # noqa: SLOT002 - nt("pmem", ("rss", "vms", "peak_rss", "peak_vms", "num_page_faults")) + nt("pmem", ("rss", "vms", "peak_rss", "peak_vms")) ): - def __new__( - cls, - rss, - vms, - peak_rss, - peak_vms, - num_page_faults, - _deprecated=None, - ): - inst = super().__new__( - cls, rss, vms, peak_rss, peak_vms, num_page_faults - ) + def __new__(cls, rss, vms, peak_rss, peak_vms, _deprecated=None): + inst = super().__new__(cls, rss, vms, peak_rss, peak_vms) inst.__dict__['_deprecated'] = _deprecated or {} return inst diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e1a39a7829..cc1cd54548 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -795,7 +795,6 @@ def memory_info(self): vms=d["PrivateUsage"], peak_rss=d["PeakWorkingSetSize"], peak_vms=d["PeakPagefileUsage"], - num_page_faults=d["PageFaultCount"], _deprecated={ # old aliases "wset": d["WorkingSetSize"], # 'rss' @@ -808,6 +807,8 @@ def memory_info(self): "nonpaged_pool": d["QuotaNonPagedPoolUsage"], "peak_paged_pool": d["QuotaPeakPagedPoolUsage"], "peak_nonpaged_pool": d["QuotaPeakNonPagedPoolUsage"], + # moved to page_faults() + "num_page_faults": d["PageFaultCount"], }, ) @@ -832,8 +833,8 @@ def memory_footprint(self): @wrap_exceptions def page_faults(self): - ret = cext.proc_page_faults(self.pid) - return ntp.ppagefaults(*ret) + t = cext.proc_page_faults(self.pid) + return ntp.ppagefaults(*t) def memory_maps(self): try: diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 73d6c01e16..4375032e6b 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -141,9 +141,9 @@ psutil_proc_oneshot_pidtaskinfo(PyObject *self, PyObject *args) { if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) != 0) goto error; - // matches getrusage().ru_majflt + // Match getrusage() ru_majflt and ru_minflt. getrusage() source: + // https://github.com/apple/darwin-xnu/blob/2ff845c2e033/bsd/kern/kern_resource.c#L1263-L1265 maj_faults = (unsigned long)pti.pti_pageins; - // matches getrusage().ru_minflt min_faults = (unsigned long)pti.pti_faults - maj_faults; // clang-format off diff --git a/tests/test_windows.py b/tests/test_windows.py index 2a8c29cfb3..342b9527d2 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -533,6 +533,12 @@ def test_memory_info_deprecated_fields(self): ): assert ps.peak_nonpaged_pool == win['QuotaPeakNonPagedPoolUsage'] + # field moved to pages_fault() + with pytest.warns( + DeprecationWarning, match="num_page_faults is deprecated" + ): + assert ps.num_page_faults == win['PageFaultCount'] + # test ntuple's __getattr__ override with pytest.raises(AttributeError, match="foo"): ps.foo # noqa: B018 From 45ddb46481f8fd1969a7ac221c7f411ea7933b8d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 6 Mar 2026 01:51:22 +0100 Subject: [PATCH 1565/1714] Win: remove dead ntuple code --- psutil/_ntuples.py | 28 +++++++--------------------- tests/test_process.py | 1 - 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 9bc40780dd..aa7f2db3f7 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -285,8 +285,8 @@ def __new__(cls, rss, vms, peak_rss, peak_vms, _deprecated=None): return inst def __getattr__(self, name): - dep = self.__dict__.get('_deprecated', {}) - if name in dep: + depr = self.__dict__["_deprecated"] + if name in depr: msg = f"pmem.{name} is deprecated" if name in { "paged_pool", @@ -295,13 +295,16 @@ def __getattr__(self, name): "peak_nonpaged_pool", }: msg += "; use memory_info_ex() instead" + elif name == "num_page_faults": + msg += "; use page_faults() instead" warnings.warn(msg, DeprecationWarning, stacklevel=2) - return dep[name] + return depr[name] + msg = f"{self.__class__.__name__} object has no attribute {name!r}" raise AttributeError(msg) # psutil.Process.memory_info_ex() - _pmem_ex = nt( + pmem_ex = nt( "pmem_ex", pmem._fields + ( @@ -314,23 +317,6 @@ def __getattr__(self, name): ), ) - class pmem_ex(pmem, _pmem_ex): - __slots__ = () - _fields = _pmem_ex._fields - __repr__ = _pmem_ex.__repr__ - - def __new__(cls, *args, **kwargs): - return _pmem_ex.__new__(cls, *args, **kwargs) - - @classmethod - def _make(cls, iterable): - result = tuple.__new__(cls, iterable) - n = len(cls._fields) - if len(result) != n: - msg = f"Expected {n} arguments, got {len(result)}" - raise TypeError(msg) - return result - # psutil.Process.memory_footprint() pfootprint = nt("pfootprint", ("uss",)) diff --git a/tests/test_process.py b/tests/test_process.py index 26738c197f..aa457017c6 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -509,7 +509,6 @@ def test_memory_info_ex_fields_order(self): assert mem._fields[2:] == ( "peak_rss", "peak_vms", - "num_page_faults", "virtual", "peak_virtual", "paged_pool", From 33cb6a46092f11428305ef8326c43e78c852c373 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 6 Mar 2026 12:44:15 +0100 Subject: [PATCH 1566/1714] Standardize cpu_times() ntuple's first 3 fields --- HISTORY.rst | 13 +++++++++++++ docs/index.rst | 9 +++++++-- psutil/_ntuples.py | 4 ++-- psutil/_psbsd.py | 4 ++-- psutil/_pslinux.py | 22 +++++++++++++--------- psutil/_psosx.py | 4 ++-- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2d16e10a8c..63dd39ab99 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -44,6 +44,10 @@ - `Process.memory_full_info()`_ is **deprecated**. Use the new `Process.memory_footprint()`_ instead. +- 2747_: the field order of the returned named tuple was normalized on all + platforms, and the first 3 fields are now always ``user, system, idle``. See + compatibility notes below. + **Bug fixes** - 2726_, [macOS]: `Process.num_ctx_switches()`_ return an unusual high number @@ -61,6 +65,15 @@ Changes that break backwards compatibility: +- `cpu_times()`_: + + - On Linux, macOS and BSD the field order of the returned named tuple + changed: ``user, system, idle`` are now always the first 3 fields on all + platforms, with platform-specific fields (e.g. ``nice``) following. + Positional access (e.g. ``psutil.cpu_times()[1]``) may return the wrong + field. Always use attribute access instead (e.g. + ``psutil.cpu_times().system``). + - `Process.memory_info()`_: - The returned named tuple changed size and field order. diff --git a/docs/index.rst b/docs/index.rst index f70dace259..8803839262 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -107,7 +107,8 @@ CPU Return system CPU times as a named tuple. Every attribute represents the seconds the CPU has spent in the given mode. - The attributes availability varies depending on the platform: + The attributes availability varies depending on the platform. + Cross-platform fields: - **user**: time spent by normal processes executing in user mode; on Linux this also includes **guest** time @@ -143,10 +144,14 @@ CPU >>> import psutil >>> psutil.cpu_times() - scputimes(user=17411.7, nice=77.99, system=3797.02, idle=51266.57, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) + scputimes(user=17411.7, system=3797.02, idle=51266.57, nice=77.99, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. + .. versionchanged:: 8.0.0 field order standardized: *user*, *system*, + *idle* are now always the first 3 fields on all platforms. Before the first + 3 fields on Linux, macOS and BSD were `user, nice, system`. + .. warning:: CPU times are always supposed to increase over time, or at least remain the same, and that's because time cannot go backwards. diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index aa7f2db3f7..c7edcd60d2 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -351,7 +351,7 @@ def __getattr__(self, name): elif MACOS: # psutil.cpu_times() - scputimes = nt("scputimes", ("user", "nice", "system", "idle")) + scputimes = nt("scputimes", ("user", "system", "idle", "nice")) # psutil.virtual_memory() svmem = nt( @@ -416,7 +416,7 @@ def __getattr__(self, name): ) # psutil.cpu_times() - scputimes = nt("scputimes", ("user", "nice", "system", "idle", "irq")) + scputimes = nt("scputimes", ("user", "system", "idle", "nice", "irq")) # psutil.Process.memory_info() pmem = nt("pmem", ("rss", "vms", "text", "data", "stack", "peak_rss")) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 4ecb2e1843..0205a93c82 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -131,7 +131,7 @@ def swap_memory(): def cpu_times(): """Return system per-CPU times as a namedtuple.""" user, nice, system, idle, irq = cext.cpu_times() - return ntp.scputimes(user, nice, system, idle, irq) + return ntp.scputimes(user, system, idle, nice, irq) def per_cpu_times(): @@ -139,7 +139,7 @@ def per_cpu_times(): ret = [] for cpu_t in cext.per_cpu_times(): user, nice, system, idle, irq = cpu_t - item = ntp.scputimes(user, nice, system, idle, irq) + item = ntp.scputimes(user, system, idle, nice, irq) ret.append(item) return ret diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b4b7ddfd25..8c1cca515e 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -201,13 +201,13 @@ def is_storage_device(name): def _scputimes_ntuple(procfs_path): """Return a namedtuple of variable fields depending on the CPU times available on this Linux kernel version which may be: - (user, nice, system, idle, iowait, irq, softirq, [steal, [guest, + (user, system, idle, nice, iowait, irq, softirq, [steal, [guest, [guest_nice]]]) Used by cpu_times() function. """ with open_binary(f"{procfs_path}/stat") as f: values = f.readline().split()[1:] - fields = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq'] + fields = ['user', 'system', 'idle', 'nice', 'iowait', 'irq', 'softirq'] vlen = len(values) if vlen >= 8: # Linux >= 2.6.11 @@ -497,16 +497,19 @@ def swap_memory(): def cpu_times(): """Return a named tuple representing the following system-wide CPU times: - (user, nice, system, idle, iowait, irq, softirq [steal, [guest, + (user, system, idle, nice, iowait, irq, softirq [steal, [guest, [guest_nice]]]) Last 3 fields may not be available on all Linux kernel versions. """ procfs_path = get_procfs_path() with open_binary(f"{procfs_path}/stat") as f: values = f.readline().split() - fields = values[1 : len(ntp.scputimes._fields) + 1] - fields = [float(x) / CLOCK_TICKS for x in fields] - return ntp.scputimes(*fields) + nfields = len(ntp.scputimes._fields) + # /proc/stat order: user nice system idle [iowait irq softirq ...] + # ntuple order: user system idle nice [iowait irq softirq ...] + raw = [float(x) / CLOCK_TICKS for x in values[1 : nfields + 1]] + user, nice, system, idle = raw[0], raw[1], raw[2], raw[3] + return ntp.scputimes(user, system, idle, nice, *raw[4:]) def per_cpu_times(): @@ -515,15 +518,16 @@ def per_cpu_times(): """ procfs_path = get_procfs_path() cpus = [] + nfields = len(ntp.scputimes._fields) with open_binary(f"{procfs_path}/stat") as f: # get rid of the first line which refers to system wide CPU stats f.readline() for line in f: if line.startswith(b'cpu'): values = line.split() - fields = values[1 : len(ntp.scputimes._fields) + 1] - fields = [float(x) / CLOCK_TICKS for x in fields] - entry = ntp.scputimes(*fields) + raw = [float(x) / CLOCK_TICKS for x in values[1 : nfields + 1]] + user, nice, system, idle = raw[0], raw[1], raw[2], raw[3] + entry = ntp.scputimes(user, system, idle, nice, *raw[4:]) cpus.append(entry) return cpus diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 1a4b13c86f..68a29c6977 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -92,7 +92,7 @@ def swap_memory(): def cpu_times(): """Return system CPU times as a namedtuple.""" user, nice, system, idle = cext.cpu_times() - return ntp.scputimes(user, nice, system, idle) + return ntp.scputimes(user, system, idle, nice) def per_cpu_times(): @@ -100,7 +100,7 @@ def per_cpu_times(): ret = [] for cpu_t in cext.per_cpu_times(): user, nice, system, idle = cpu_t - item = ntp.scputimes(user, nice, system, idle) + item = ntp.scputimes(user, system, idle, nice) ret.append(item) return ret From b94e5dfc7639a5c739faed5dfd73c156d05c6f7f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 6 Mar 2026 20:42:18 +0100 Subject: [PATCH 1567/1714] #2748, Linux: drop old kernel checks and make CPU time fields constant. --- docs/index.rst | 27 ++++++++------- psutil/__init__.py | 10 +++--- psutil/_ntuples.py | 18 ++++++++-- psutil/_pslinux.py | 71 +++++++++++++--------------------------- psutil/_psutil_linux.c | 4 +-- psutil/arch/linux/init.h | 5 --- psutil/arch/linux/proc.c | 2 -- tests/test_linux.py | 20 ----------- 8 files changed, 59 insertions(+), 98 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 8803839262..42b6d61402 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -123,11 +123,11 @@ CPU accounted in **idle** time counter. - **irq** *(Linux, BSD)*: time spent for servicing hardware interrupts - **softirq** *(Linux)*: time spent for servicing software interrupts - - **steal** *(Linux 2.6.11+)*: time spent by other operating systems running + - **steal** *(Linux)*: time spent by other operating systems running in a virtualized environment - - **guest** *(Linux 2.6.24+)*: time spent running a virtual CPU for guest + - **guest** *(Linux)*: time spent running a virtual CPU for guest operating systems under the control of the Linux kernel - - **guest_nice** *(Linux 3.2.0+)*: time spent running a niced guest + - **guest_nice** *(Linux)*: time spent running a niced guest (virtual CPU for guest operating systems under the control of the Linux kernel) - **interrupt** *(Windows)*: time spent for servicing hardware interrupts ( @@ -346,9 +346,9 @@ Memory - **total**: total physical memory (exclusive swap). - **available**: memory that can be given instantly to processes without the - system going into swap. It is calculated by summing different memory values - depending on the platform (on Linux it matches the ``MemAvailable`` kernel - field). This is the recommended field for monitoring actual memory usage + system going into swap. On Linux it uses the ``MemAvailable`` field from + ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back to an + estimate. This is the recommended field for monitoring actual memory usage in a cross-platform fashion. - **percent**: the percentage usage calculated as ``(total - available) / total * 100``. @@ -1867,13 +1867,15 @@ Process class the process has reached since it started. - **peak_vms** *(Linux)*: the highest VMS value the process has reached since it started. - - **rss_anon** *(Linux, macOS)*: resident anonymous pages (heap, stack, - private mappings) not backed by any file, such as heap allocations, - stack, and private ``mmap(MAP_ANONYMOUS)`` regions. + - **rss_anon** *(Linux, macOS)*: resident anonymous pages (heap, + stack, private mappings) not backed by any file, such as heap + allocations, stack, and private ``mmap(MAP_ANONYMOUS)`` regions. Set to 0 + on Linux < 4.5. - **rss_file** *(Linux, macOS)*: resident file-backed memory; pages mapped - from files (shared libraries, mmap'd files). + from files (shared libraries, mmap'd files). Set to 0 on Linux < 4.5. - **rss_shmem** *(Linux)*: resident shared memory pages (``tmpfs``, - ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals **rss**. + ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals **rss**. Set to + 0 on Linux < 4.5. - **wired** *(macOS)*: memory pinned in RAM by the kernel on behalf of this process; cannot be compressed or paged out. - **swap** *(Linux)*: process memory currently in swap. Equivalent to @@ -1882,7 +1884,8 @@ Process class - **compressed** *(macOS)*: pages held in the in-RAM memory compressor; not counted in **rss**. A large value signals memory pressure but has not yet triggered swapping. - - **hugetlb** *(Linux)*: resident memory backed by huge pages. + - **hugetlb** *(Linux)*: resident memory backed by huge pages. Set to 0 on + Linux < 4.4. - **phys_footprint** *(macOS)*: total physical memory impact including compressed pages. What Xcode and ``footprint(1)`` report; prefer this over **rss** macOS memory monitoring. diff --git a/psutil/__init__.py b/psutil/__init__.py index 1a4ab098fe..ae1266a82c 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1764,9 +1764,9 @@ def cpu_times(percpu=False): - iowait (Linux) - irq (Linux, FreeBSD) - softirq (Linux) - - steal (Linux >= 2.6.11) - - guest (Linux >= 2.6.24) - - guest_nice (Linux >= 3.2.0) + - steal (Linux) + - guest (Linux) + - guest_nice (Linux) When *percpu* is True return a list of namedtuples for each CPU. First element of the list refers to first CPU, second element @@ -1806,8 +1806,8 @@ def _cpu_tot_time(times): # https://github.com/giampaolo/psutil/pull/940 # http://unix.stackexchange.com/questions/178045 # https://github.com/torvalds/linux/blob/447976ef4/kernel/sched/cputime.c#L158 - tot -= getattr(times, "guest", 0) # Linux 2.6.24+ - tot -= getattr(times, "guest_nice", 0) # Linux 3.2.0+ + tot -= times.guest + tot -= times.guest_nice return tot diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index c7edcd60d2..9a3e2d0664 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -142,8 +142,22 @@ if LINUX: - # This gets set from _pslinux.py - scputimes = None + # psutil.cpu_times() + scputimes = nt( + "scputimes", + ( + "user", + "system", + "idle", + "nice", + "iowait", + "irq", + "softirq", + "steal", + "guest", + "guest_nice", + ), + ) # psutil.virtual_memory() svmem = nt( diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 8c1cca515e..7897e77594 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -18,7 +18,6 @@ import sys import warnings from collections import defaultdict -from collections import namedtuple from . import _common from . import _ntuples as ntp @@ -37,7 +36,6 @@ 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 @@ -197,43 +195,6 @@ def is_storage_device(name): return os.access(path, os.F_OK) -@memoize -def _scputimes_ntuple(procfs_path): - """Return a namedtuple of variable fields depending on the CPU times - available on this Linux kernel version which may be: - (user, system, idle, nice, iowait, irq, softirq, [steal, [guest, - [guest_nice]]]) - Used by cpu_times() function. - """ - with open_binary(f"{procfs_path}/stat") as f: - values = f.readline().split()[1:] - fields = ['user', 'system', 'idle', 'nice', 'iowait', 'irq', 'softirq'] - vlen = len(values) - if vlen >= 8: - # Linux >= 2.6.11 - fields.append('steal') - if vlen >= 9: - # Linux >= 2.6.24 - fields.append('guest') - if vlen >= 10: - # Linux >= 3.2.0 - fields.append('guest_nice') - return namedtuple('scputimes', fields) - - -# Set it into _ntuples.py namespace. -try: - ntp.scputimes = _scputimes_ntuple("/proc") -except Exception as err: # noqa: BLE001 - # Don't want to crash at import time. - debug(f"ignoring exception on import: {err!r}") - ntp.scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) - -# XXX: must be available also at this module level in order to be -# serialized (tests/test_misc.py::TestMisc::test_serialization). -scputimes = ntp.scputimes - - # ===================================================================== # --- system memory # ===================================================================== @@ -495,21 +456,33 @@ def swap_memory(): def cpu_times(): - """Return a named tuple representing the following system-wide - CPU times: - (user, system, idle, nice, iowait, irq, softirq [steal, [guest, - [guest_nice]]]) - Last 3 fields may not be available on all Linux kernel versions. - """ + """Return a named tuple representing system-wide CPU times.""" + + def lsget(lst, idx, field_name): + try: + return lst[idx] + except IndexError: + debug(f"can't get {field_name} CPU time; set it to 0") + return 0 + procfs_path = get_procfs_path() with open_binary(f"{procfs_path}/stat") as f: values = f.readline().split() nfields = len(ntp.scputimes._fields) - # /proc/stat order: user nice system idle [iowait irq softirq ...] - # ntuple order: user system idle nice [iowait irq softirq ...] raw = [float(x) / CLOCK_TICKS for x in values[1 : nfields + 1]] - user, nice, system, idle = raw[0], raw[1], raw[2], raw[3] - return ntp.scputimes(user, system, idle, nice, *raw[4:]) + user, nice, system, idle = raw[:4] + return ntp.scputimes( + user, + system, + idle, + nice, + lsget(raw, 4, "iowait"), # Linux >= 2.5.41 + lsget(raw, 5, "irq"), # Linux >= 2.6.0 + lsget(raw, 6, "softirq"), # Linux >= 2.6.0 + lsget(raw, 7, "steal"), # Linux >= 2.6.11 + lsget(raw, 8, "guest"), # Linux >= 2.6.24 + lsget(raw, 9, "guest_nice"), # Linux >= 2.6.33 + ) def per_cpu_times(): diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index 71e70024ee..a8b5401477 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -21,11 +21,9 @@ #endif static PyMethodDef mod_methods[] = { -// --- per-process functions -#ifdef PSUTIL_HAS_IOPRIO + // --- per-process functions {"proc_ioprio_get", psutil_proc_ioprio_get, METH_VARARGS}, {"proc_ioprio_set", psutil_proc_ioprio_set, METH_VARARGS}, -#endif #ifdef PSUTIL_HAS_CPU_AFFINITY {"proc_cpu_affinity_get", psutil_proc_cpu_affinity_get, METH_VARARGS}, {"proc_cpu_affinity_set", psutil_proc_cpu_affinity_set, METH_VARARGS}, diff --git a/psutil/arch/linux/init.h b/psutil/arch/linux/init.h index 9002d9efda..60c45548ad 100644 --- a/psutil/arch/linux/init.h +++ b/psutil/arch/linux/init.h @@ -11,13 +11,8 @@ PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); PyObject *psutil_linux_sysinfo(PyObject *self, PyObject *args); PyObject *psutil_net_if_duplex_speed(PyObject *self, PyObject *args); - -// Linux >= 2.6.13 -#if defined(__NR_ioprio_get) && defined(__NR_ioprio_set) -#define PSUTIL_HAS_IOPRIO PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); -#endif // Should exist starting from CentOS 6 (year 2011). #ifdef CPU_ALLOC diff --git a/psutil/arch/linux/proc.c b/psutil/arch/linux/proc.c index efcaa26735..9e1bb3bd60 100644 --- a/psutil/arch/linux/proc.c +++ b/psutil/arch/linux/proc.c @@ -17,7 +17,6 @@ // ==================================================================== -#ifdef PSUTIL_HAS_IOPRIO enum { IOPRIO_WHO_PROCESS = 1, }; @@ -74,7 +73,6 @@ psutil_proc_ioprio_set(PyObject *self, PyObject *args) { return psutil_oserror(); Py_RETURN_NONE; } -#endif // PSUTIL_HAS_IOPRIO // ==================================================================== diff --git a/tests/test_linux.py b/tests/test_linux.py index 5c66348d43..ab39f64049 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -617,26 +617,6 @@ def test_emulate_meminfo_has_no_metrics(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="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): - assert 'steal' in fields - else: - assert 'steal' not in fields - if kernel_ver_info >= (2, 6, 24): - assert 'guest' in fields - else: - assert 'guest' not in fields - if kernel_ver_info >= (3, 2, 0): - assert 'guest_nice' in fields - else: - assert 'guest_nice' not in fields - - @pytest.mark.skipif(not LINUX, reason="LINUX only") class TestSystemCPUCountLogical(PsutilTestCase): @pytest.mark.skipif( From a6e84b23686c638cbda63724a7711e109ce3cf49 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 7 Mar 2026 01:13:22 +0100 Subject: [PATCH 1568/1714] Typed named tuples (#2751) Convert all named tuples in `psutil/_ntuples.py` from `collections.namedtuple()` to `typing.NamedTuple` classes with **field type annotations**. Platform-specific fields are expressed via `if PLATFORM:` blocks inside a single class definition, instead of scattering multiple per-platform `namedtuple()` declarations across separate sections. The type annotations (e.g. `pid: int | None`, `family: socket.AddressFamily`) make the classes self-documented. The 2 things together (`if PLATFORM:` + type annotations) basically turn this module into a readable API reference / overview. --- .github/workflows/bsd.yml | 3 +- HISTORY.rst | 19 +- docs/index.rst | 40 ++- psutil/_ntuples.py | 686 +++++++++++++++++++------------------- tests/__init__.py | 53 ++- tests/test_contracts.py | 50 +++ tests/test_process_all.py | 27 +- 7 files changed, 489 insertions(+), 389 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 3ad58bf42c..62f58c4e75 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -21,8 +21,7 @@ on: - "psutil/arch/openbsd/**" - "psutil/arch/posix/**" - "setup.py" - - "tests/test_bsd.py" - - "tests/test_posix.py" + - "tests/**" pull_request: paths: *bsd_paths name: bsd diff --git a/HISTORY.rst b/HISTORY.rst index 63dd39ab99..86f58d80f2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -44,9 +44,13 @@ - `Process.memory_full_info()`_ is **deprecated**. Use the new `Process.memory_footprint()`_ instead. -- 2747_: the field order of the returned named tuple was normalized on all - platforms, and the first 3 fields are now always ``user, system, idle``. See - compatibility notes below. +- 2747_: the field order of the named tuple returned by `cpu_times()`_ has been + normalized on all platforms, and the first 3 fields are now always ``user, + system, idle``. See compatibility notes below. +- 2751_: convert all named tuples in `psutil/_ntuples.py`_ from + ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type + annotations**. This makes the classes self-documenting, effectively turning + this module into a readable API reference. **Bug fixes** @@ -70,9 +74,8 @@ Changes that break backwards compatibility: - On Linux, macOS and BSD the field order of the returned named tuple changed: ``user, system, idle`` are now always the first 3 fields on all platforms, with platform-specific fields (e.g. ``nice``) following. - Positional access (e.g. ``psutil.cpu_times()[1]``) may return the wrong - field. Always use attribute access instead (e.g. - ``psutil.cpu_times().system``). + Positional access (e.g. ``cpu_times()[3]``) may silently return the wrong + field. Always use attribute access instead (e.g. ``cpu_times().idle``). - `Process.memory_info()`_: @@ -2990,7 +2993,6 @@ In most cases accessing the old names will work but it will cause a .. _`win_service_get()`: https://psutil.readthedocs.io/en/latest/#psutil.win_service_get .. _`win_service_iter()`: https://psutil.readthedocs.io/en/latest/#psutil.win_service_iter - .. _`Process`: https://psutil.readthedocs.io/en/latest/#psutil.Process .. _`psutil.Popen`: https://psutil.readthedocs.io/en/latest/#psutil.Popen @@ -2999,7 +3001,6 @@ In most cases accessing the old names will work but it will cause a .. _`TimeoutExpired`: https://psutil.readthedocs.io/en/latest/#psutil.TimeoutExpired .. _`ZombieProcess`: https://psutil.readthedocs.io/en/latest/#psutil.ZombieProcess - .. _`Process.as_dict()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.as_dict .. _`Process.children()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.children .. _`Process.cmdline()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.connections @@ -3049,7 +3050,6 @@ In most cases accessing the old names will work but it will cause a .. _`Process.username()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.username .. _`Process.wait()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.wait - .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py .. _`free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py @@ -3065,6 +3065,7 @@ In most cases accessing the old names will work but it will cause a .. _`pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py .. _`top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py +.. _`psutil/_ntuples.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_ntuples.py .. _1: https://github.com/giampaolo/psutil/issues/1 .. _2: https://github.com/giampaolo/psutil/issues/2 diff --git a/docs/index.rst b/docs/index.rst index 42b6d61402..c2d97d529d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -117,10 +117,10 @@ CPU Platform-specific fields: - - **nice** *(UNIX)*: time spent by niced (prioritized) processes executing in - user mode; on Linux this also includes **guest_nice** time - - **iowait** *(Linux)*: time spent waiting for I/O to complete. This is *not* - accounted in **idle** time counter. + - **nice** *(Linux, macOS, BSD)*: time spent by niced (prioritized) processes + executing in user mode; on Linux this also includes **guest_nice** time + - **iowait** *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete. + This is *not* accounted in **idle** time counter. - **irq** *(Linux, BSD)*: time spent for servicing hardware interrupts - **softirq** *(Linux)*: time spent for servicing software interrupts - **steal** *(Linux)*: time spent by other operating systems running @@ -148,15 +148,20 @@ CPU .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. - .. versionchanged:: 8.0.0 field order standardized: *user*, *system*, - *idle* are now always the first 3 fields on all platforms. Before the first - 3 fields on Linux, macOS and BSD were `user, nice, system`. + .. versionchanged:: 8.0.0 + ``cpu_times()`` field order was standardized: ``user``, ``system``, + ``idle`` are now always the first three fields. Previously on Linux, + macOS, and BSD the first three were ``user``, ``nice``, ``system``. + .. warning:: + in version 8.0.0 the named tuple changed field order. Positional access + (e.g. ``cpu_times()[3]``) may silently return the wrong field. Always use + attribute access instead (e.g. ``cpu_times().idle``). - .. warning:: - 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 `#1210 `__. + .. warning:: + 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 `#1210 + `__. .. function:: cpu_percent(interval=None, percpu=False) @@ -879,6 +884,15 @@ Sensors All temperatures are expressed in celsius unless *fahrenheit* is set to ``True``. If sensors are not supported by the OS an empty dict is returned. + Each named tuple includes 4 fields: + + - **label**: a string label for the sensor, if available, else ``""``. + - **current**: current temperature, or ``None`` if not available. + - **high**: temperature at which the system will throttle, or ``None`` + if not available. + - **critical**: temperature at which the system will shut down, or + ``None`` if not available. + Example:: >>> import psutil @@ -986,7 +1000,7 @@ Other system info - **name**: the name of the user. - **terminal**: the tty or pseudo-tty associated with the user, if any, else ``None``. - - **host**: the host name associated with the entry, if any. + - **host**: the host name associated with the entry, if any, else ``None``. - **started**: the creation time as a floating point number expressed in seconds since the epoch. - **pid**: the PID of the login process (like sshd, tmux, gdm-session-worker, diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 9a3e2d0664..9ae556ed20 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -2,203 +2,387 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +from __future__ import annotations + import warnings -from collections import namedtuple as nt +from collections import namedtuple +from typing import TYPE_CHECKING +from typing import NamedTuple + +if TYPE_CHECKING: + import socket from ._common import AIX from ._common import BSD from ._common import FREEBSD from ._common import LINUX from ._common import MACOS +from ._common import NETBSD +from ._common import OPENBSD from ._common import SUNOS from ._common import WINDOWS +from ._common import BatteryTime +from ._common import NicDuplex # =================================================================== # --- system functions # =================================================================== + # psutil.swap_memory() -sswap = nt("sswap", ("total", "used", "free", "percent", "sin", "sout")) +class sswap(NamedTuple): + total: int + used: int + free: int + percent: float + sin: int + sout: int + # psutil.disk_usage() -sdiskusage = nt("sdiskusage", ("total", "used", "free", "percent")) +class sdiskusage(NamedTuple): + total: int + used: int + free: int + percent: float + # psutil.disk_io_counters() -sdiskio = nt( - "sdiskio", - ( - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_time", - "write_time", - ), -) +class sdiskio(NamedTuple): + read_count: int + write_count: int + read_bytes: int + write_bytes: int + if not (NETBSD or OPENBSD): + read_time: int + write_time: int + if LINUX: + read_merged_count: int + write_merged_count: int + busy_time: int + if FREEBSD: + busy_time: int + # psutil.disk_partitions() -sdiskpart = nt("sdiskpart", ("device", "mountpoint", "fstype", "opts")) +class sdiskpart(NamedTuple): + device: str + mountpoint: str + fstype: str + opts: str + # psutil.net_io_counters() -snetio = nt( - "snetio", - ( - "bytes_sent", - "bytes_recv", - "packets_sent", - "packets_recv", - "errin", - "errout", - "dropin", - "dropout", - ), -) +class snetio(NamedTuple): + bytes_sent: int + bytes_recv: int + packets_sent: int + packets_recv: int + errin: int + errout: int + dropin: int + dropout: int + # psutil.users() -suser = nt("suser", ("name", "terminal", "host", "started", "pid")) +class suser(NamedTuple): + name: str + terminal: str | None + host: str | None + started: float + pid: int | None + + +# psutil.net_connections() and psutil.Process.net_connections() +class addr(NamedTuple): + ip: str + port: int + # psutil.net_connections() -sconn = nt( - "sconn", ("fd", "family", "type", "laddr", "raddr", "status", "pid") -) +class sconn(NamedTuple): + fd: int + family: socket.AddressFamily + type: socket.SocketKind + laddr: addr | tuple | str + raddr: addr | tuple | str + status: str + pid: int | None + # psutil.net_if_addrs() -snicaddr = nt("snicaddr", ("family", "address", "netmask", "broadcast", "ptp")) +class snicaddr(NamedTuple): + family: socket.AddressFamily + address: str + netmask: str | None + broadcast: str | None + ptp: str | None + # psutil.net_if_stats() -snicstats = nt("snicstats", ("isup", "duplex", "speed", "mtu", "flags")) +class snicstats(NamedTuple): + isup: bool + duplex: NicDuplex + speed: int + mtu: int + flags: str + + +# psutil.cpu_times() +class scputimes(NamedTuple): + user: float + system: float + idle: float + if LINUX or MACOS or BSD: + nice: float + if LINUX: + iowait: float + irq: float + softirq: float + steal: float + guest: float + guest_nice: float + if BSD: + irq: float + if SUNOS or AIX: + iowait: float + if WINDOWS: + interrupt: float + dpc: float + # psutil.cpu_stats() -scpustats = nt( - "scpustats", ("ctx_switches", "interrupts", "soft_interrupts", "syscalls") -) +class scpustats(NamedTuple): + ctx_switches: int + interrupts: int + soft_interrupts: int + syscalls: int + # psutil.cpu_freq() -scpufreq = nt("scpufreq", ("current", "min", "max")) +class scpufreq(NamedTuple): + current: float + min: float | None + max: float | None + # psutil.sensors_temperatures() -shwtemp = nt("shwtemp", ("label", "current", "high", "critical")) +class shwtemp(NamedTuple): + label: str + current: float | None + high: float | None + critical: float | None + # psutil.sensors_battery() -sbattery = nt("sbattery", ("percent", "secsleft", "power_plugged")) +class sbattery(NamedTuple): + percent: float + secsleft: int | BatteryTime + power_plugged: bool | None + # psutil.sensors_fans() -sfan = nt("sfan", ("label", "current")) +class sfan(NamedTuple): + label: str + current: int + -# psutil.heap_info() (mallinfo2 Linux struct) if LINUX or WINDOWS or MACOS or BSD: - pheap = nt( - "pheap", - [ - "heap_used", # uordblks, memory allocated via malloc() - "mmap_used", # hblkhd, memory allocated via mmap() (large blocks) - ], - ) - if WINDOWS: - pheap = nt("pheap", pheap._fields + ("heap_count",)) + + # psutil.heap_info() + class pheap(NamedTuple): + heap_used: int + mmap_used: int + if WINDOWS: + heap_count: int + + +# psutil.virtual_memory() +class svmem(NamedTuple): + total: int + available: int + percent: float + used: int + free: int + if LINUX: + active: int + inactive: int + buffers: int + cached: int + shared: int + slab: int + elif BSD: + active: int + inactive: int + buffers: int + cached: int + shared: int + wired: int + elif MACOS: + active: int + inactive: int + wired: int + # =================================================================== # --- Process class # =================================================================== + # psutil.Process.cpu_times() -pcputimes = nt( - "pcputimes", ("user", "system", "children_user", "children_system") -) +class pcputimes(NamedTuple): + user: float + system: float + children_user: float + children_system: float + if LINUX: + iowait: float + # psutil.Process.open_files() -popenfile = nt("popenfile", ("path", "fd")) +class popenfile(NamedTuple): + path: str + fd: int + if LINUX: + position: int + mode: str + flags: int + # psutil.Process.threads() -pthread = nt("pthread", ("id", "user_time", "system_time")) +class pthread(NamedTuple): + id: int + user_time: float + system_time: float + # psutil.Process.uids() -puids = nt("puids", ("real", "effective", "saved")) +class puids(NamedTuple): + real: int + effective: int + saved: int + # psutil.Process.gids() -pgids = nt("pgids", ("real", "effective", "saved")) +class pgids(NamedTuple): + real: int + effective: int + saved: int + # psutil.Process.io_counters() -pio = nt("pio", ("read_count", "write_count", "read_bytes", "write_bytes")) +class pio(NamedTuple): + read_count: int + write_count: int + read_bytes: int + write_bytes: int + if LINUX: + read_chars: int + write_chars: int + elif WINDOWS: + other_count: int + other_bytes: int + # psutil.Process.ionice() -pionice = nt("pionice", ("ioclass", "value")) +class pionice(NamedTuple): + ioclass: int + value: int + # psutil.Process.ctx_switches() -pctxsw = nt("pctxsw", ("voluntary", "involuntary")) +class pctxsw(NamedTuple): + voluntary: int + involuntary: int + # psutil.Process.page_faults() -ppagefaults = nt("ppagefaults", ("minor", "major")) +class ppagefaults(NamedTuple): + minor: int + major: int -# psutil.Process.net_connections() -pconn = nt("pconn", ("fd", "family", "type", "laddr", "raddr", "status")) -# psutil.net_connections() and psutil.Process.net_connections() -addr = nt("addr", ("ip", "port")) +# psutil.Process().memory_footprint() +if LINUX or MACOS or WINDOWS: -# =================================================================== -# --- Linux -# =================================================================== + class pfootprint(NamedTuple): + uss: int + if LINUX: + pss: int + swap: int -if LINUX: - # psutil.cpu_times() - scputimes = nt( - "scputimes", - ( - "user", - "system", - "idle", - "nice", - "iowait", - "irq", - "softirq", - "steal", - "guest", - "guest_nice", - ), - ) +# psutil.Process.net_connections() +class pconn(NamedTuple): + fd: int + family: socket.AddressFamily + type: socket.SocketKind + laddr: addr | tuple | str + raddr: addr | tuple | str + status: str + + +# psutil.Process.memory_maps(grouped=True) +class pmmap_grouped(NamedTuple): + path: str + rss: int + if LINUX: + size: int + pss: int + shared_clean: int + shared_dirty: int + private_clean: int + private_dirty: int + referenced: int + anonymous: int + swap: int + elif BSD: + private: int + ref_count: int + shadow_count: int + elif SUNOS: + anonymous: int + locked: int + + +# psutil.Process.memory_maps(grouped=False) +class pmmap_ext(NamedTuple): + addr: str + perms: str + path: str + rss: int + if LINUX: + size: int + pss: int + shared_clean: int + shared_dirty: int + private_clean: int + private_dirty: int + referenced: int + anonymous: int + swap: int + elif BSD: + private: int + ref_count: int + shadow_count: int + elif SUNOS: + anonymous: int + locked: int - # psutil.virtual_memory() - svmem = nt( - "svmem", - ( - "total", - "available", - "percent", - "used", - "free", - "active", - "inactive", - "buffers", - "cached", - "shared", - "slab", - ), - ) - # psutil.disk_io_counters() - sdiskio = nt( - "sdiskio", - ( - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_time", - "write_time", - "read_merged_count", - "write_merged_count", - "busy_time", - ), - ) +# =================================================================== +# --- Process memory_info() / memory_info_ex() / memory_full_info() +# =================================================================== - # psutil.Process().open_files() - popenfile = nt("popenfile", ("path", "fd", "position", "mode", "flags")) +if LINUX: # psutil.Process().memory_info() - class pmem(nt("pmem", ("rss", "vms", "shared", "text", "data"))): - __slots__ = () + class pmem(NamedTuple): + rss: int + vms: int + shared: int + text: int + data: int @property def lib(self): @@ -215,7 +399,7 @@ def dirty(self): return 0 # psutil.Process().memory_info_ex() - pmem_ex = nt( + pmem_ex = namedtuple( "pmem_ex", pmem._fields + ( @@ -229,73 +413,18 @@ def dirty(self): ), ) - # psutil.Process().memory_footprint() - pfootprint = nt("pfootprint", ("uss", "pss", "swap")) - # psutil.Process().memory_full_info() - pfullmem = nt("pfullmem", pmem._fields + ("uss", "pss", "swap")) - - # psutil.Process().memory_maps(grouped=True) - pmmap_grouped = nt( - "pmmap_grouped", - ( - "path", - "rss", - "size", - "pss", - "shared_clean", - "shared_dirty", - "private_clean", - "private_dirty", - "referenced", - "anonymous", - "swap", - ), - ) - - # psutil.Process().memory_maps(grouped=False) - pmmap_ext = nt( - "pmmap_ext", "addr perms " + " ".join(pmmap_grouped._fields) - ) - - # psutil.Process.io_counters() - pio = nt( - "pio", - ( - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_chars", - "write_chars", - ), - ) - - # psutil.Process.cpu_times() - pcputimes = nt( - "pcputimes", - ("user", "system", "children_user", "children_system", "iowait"), - ) - -# =================================================================== -# --- Windows -# =================================================================== + pfullmem = namedtuple("pfullmem", pmem._fields + ("uss", "pss", "swap")) elif WINDOWS: - # psutil.cpu_times() - scputimes = nt("scputimes", ("user", "system", "idle", "interrupt", "dpc")) - - # psutil.virtual_memory() - svmem = nt("svmem", ("total", "available", "percent", "used", "free")) - # psutil.Process.memory_info() class pmem( # noqa: SLOT002 - nt("pmem", ("rss", "vms", "peak_rss", "peak_vms")) + namedtuple("pmem", ("rss", "vms", "peak_rss", "peak_vms")) ): def __new__(cls, rss, vms, peak_rss, peak_vms, _deprecated=None): inst = super().__new__(cls, rss, vms, peak_rss, peak_vms) - inst.__dict__['_deprecated'] = _deprecated or {} + inst.__dict__["_deprecated"] = _deprecated or {} return inst def __getattr__(self, name): @@ -318,7 +447,7 @@ def __getattr__(self, name): raise AttributeError(msg) # psutil.Process.memory_info_ex() - pmem_ex = nt( + pmem_ex = namedtuple( "pmem_ex", pmem._fields + ( @@ -331,193 +460,50 @@ def __getattr__(self, name): ), ) - # psutil.Process.memory_footprint() - pfootprint = nt("pfootprint", ("uss",)) - # psutil.Process.memory_full_info() - pfullmem = nt("pfullmem", pmem._fields + ("uss",)) - - # psutil.Process.memory_maps(grouped=True) - pmmap_grouped = nt("pmmap_grouped", ("path", "rss")) - - # psutil.Process.memory_maps(grouped=False) - pmmap_ext = nt( - "pmmap_ext", "addr perms " + " ".join(pmmap_grouped._fields) - ) - - # psutil.Process.io_counters() - pio = nt( - "pio", - ( - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "other_count", - "other_bytes", - ), - ) - -# =================================================================== -# --- macOS -# =================================================================== + pfullmem = namedtuple("pfullmem", pmem._fields + ("uss",)) elif MACOS: - # psutil.cpu_times() - scputimes = nt("scputimes", ("user", "system", "idle", "nice")) - - # psutil.virtual_memory() - svmem = nt( - "svmem", - ( - "total", - "available", - "percent", - "used", - "free", - "active", - "inactive", - "wired", - ), - ) - # psutil.Process.memory_info() - pmem = nt("pmem", ("rss", "vms")) + class pmem(NamedTuple): + rss: int + vms: int # psutil.Process.memory_info_ex() - pmem_ex = nt( - "pmem_ex", - pmem._fields - + ( - "peak_rss", - "rss_anon", - "rss_file", - "wired", - "compressed", - "phys_footprint", - ), - ) - - # psutil.Process.memory_footprint() - pfootprint = nt("pfootprint", ("uss",)) + class pmem_ex(NamedTuple): + rss: int + vms: int + peak_rss: int + rss_anon: int + rss_file: int + wired: int + compressed: int + phys_footprint: int # psutil.Process.memory_full_info() - pfullmem = nt("pfullmem", pmem._fields + ("uss",)) - -# =================================================================== -# --- BSD -# =================================================================== + pfullmem = namedtuple("pfullmem", pmem._fields + ("uss",)) elif BSD: - # psutil.virtual_memory() - svmem = nt( - "svmem", - ( - "total", - "available", - "percent", - "used", - "free", - "active", - "inactive", - "buffers", - "cached", - "shared", - "wired", - ), - ) - - # psutil.cpu_times() - scputimes = nt("scputimes", ("user", "system", "idle", "nice", "irq")) - # psutil.Process.memory_info() - pmem = nt("pmem", ("rss", "vms", "text", "data", "stack", "peak_rss")) + class pmem(NamedTuple): + rss: int + vms: int + text: int + data: int + stack: int + peak_rss: int # psutil.Process.memory_full_info() pfullmem = pmem - # psutil.Process.cpu_times() - pcputimes = nt( - "pcputimes", ("user", "system", "children_user", "children_system") - ) - - # psutil.Process.memory_maps(grouped=True) - pmmap_grouped = nt( - "pmmap_grouped", "path rss, private, ref_count, shadow_count" - ) - - # psutil.Process.memory_maps(grouped=False) - pmmap_ext = nt( - "pmmap_ext", "addr, perms path rss, private, ref_count, shadow_count" - ) - - # psutil.disk_io_counters() - if FREEBSD: - sdiskio = nt( - "sdiskio", - ( - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_time", - "write_time", - "busy_time", - ), - ) - else: - sdiskio = nt( - "sdiskio", - ("read_count", "write_count", "read_bytes", "write_bytes"), - ) - -# =================================================================== -# --- SunOS -# =================================================================== - -elif SUNOS: - - # psutil.cpu_times() - scputimes = nt("scputimes", ("user", "system", "idle", "iowait")) - - # psutil.cpu_times(percpu=True) - pcputimes = nt( - "pcputimes", ("user", "system", "children_user", "children_system") - ) - - # psutil.virtual_memory() - svmem = nt("svmem", ("total", "available", "percent", "used", "free")) +elif SUNOS or AIX: # psutil.Process.memory_info() - pmem = nt("pmem", ("rss", "vms")) + class pmem(NamedTuple): + rss: int + vms: int # psutil.Process.memory_full_info() pfullmem = pmem - - # psutil.Process.memory_maps(grouped=True) - pmmap_grouped = nt("pmmap_grouped", ("path", "rss", "anonymous", "locked")) - - # psutil.Process.memory_maps(grouped=False) - pmmap_ext = nt( - "pmmap_ext", "addr perms " + " ".join(pmmap_grouped._fields) - ) - -# =================================================================== -# --- AIX -# =================================================================== - -elif AIX: - - # psutil.Process.memory_info() - pmem = nt("pmem", ("rss", "vms")) - - # psutil.Process.memory_full_info() - pfullmem = pmem - - # psutil.Process.cpu_times() - scputimes = nt("scputimes", ("user", "system", "idle", "iowait")) - - # psutil.virtual_memory() - svmem = nt("svmem", ("total", "available", "percent", "used", "free")) diff --git a/tests/__init__.py b/tests/__init__.py index e48d7a1ded..ef670e4ebc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -29,6 +29,8 @@ import threading import time import traceback +import types +import typing import unittest import warnings from socket import AF_INET @@ -41,6 +43,7 @@ pytest = None import psutil +import psutil._ntuples as ntuples from psutil import AIX from psutil import BSD from psutil import LINUX @@ -80,8 +83,7 @@ # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', 'retry_on_failure', 'PsutilTestCase', 'process_namespace', - 'system_namespace', - 'is_win_secure_system_proc', + 'system_namespace', 'check_ntuple_types', 'is_win_secure_system_proc', # fs utils 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', # os @@ -1061,7 +1063,7 @@ def assert_in_pids(proc): def check_proc_memory(self, nt): # Check the ntuple returned by Process.memory_*() methods. - assert is_namedtuple(nt) + check_ntuple_types(nt) for value in nt: assert isinstance(value, int) assert value >= 0 @@ -1170,6 +1172,7 @@ class process_namespace: if HAS_PROC_MEMORY_FOOTPRINT: getters += [('memory_footprint', (), {})] if HAS_PROC_MEMORY_MAPS: + getters += [('memory_maps', (), {'grouped': True})] getters += [('memory_maps', (), {'grouped': False})] setters = [] @@ -1260,12 +1263,15 @@ class system_namespace: ('cpu_stats', (), {}), ('cpu_times', (), {'percpu': False}), ('cpu_times', (), {'percpu': True}), + ('disk_io_counters', (), {'perdisk': False}), ('disk_io_counters', (), {'perdisk': True}), + ('disk_partitions', (), {'all': False}), ('disk_partitions', (), {'all': True}), ('disk_usage', (os.getcwd(),), {}), ('net_connections', (), {'kind': 'all'}), ('net_if_addrs', (), {}), ('net_if_stats', (), {}), + ('net_io_counters', (), {'pernic': False}), ('net_io_counters', (), {'pernic': True}), ('pid_exists', (os.getpid(),), {}), ('pids', (), {}), @@ -1275,6 +1281,7 @@ class system_namespace: ] if HAS_CPU_FREQ: + getters += [('cpu_freq', (), {'percpu': False})] getters += [('cpu_freq', (), {'percpu': True})] if HAS_GETLOADAVG: getters += [('getloadavg', (), {})] @@ -1518,6 +1525,45 @@ def create_sockets(): safe_rmpath(fname) +@functools.lru_cache(maxsize=None) +def _get_hints(cls): + try: + return typing.get_type_hints( + cls, globalns=vars(ntuples), localns={'socket': socket} + ) + except TypeError: + # Python < 3.10 can't evaluate "X | Y" union syntax. + return {} + + +def check_ntuple_types(nt): + """Uses type hints from _ntuples.py to verify field types. `nt` is + a named tuple returned by one of psutil APIs. + """ + assert is_namedtuple(nt) + hints = _get_hints(type(nt)) + if not hints: + return + for field in nt._fields: + if field not in hints: + # field is not annotated + continue + value = getattr(nt, field) + hint = hints[field] + if ( + hasattr(types, 'UnionType') and isinstance(hint, types.UnionType) + ) or getattr(hint, '__origin__', None) is typing.Union: + types_ = typing.get_args(hint) + elif isinstance(hint, type): + # For IntEnum hints (e.g. socket.AddressFamily), psutil may + # return a platform-specific IntEnum subclass rather than the + # annotated one, so we broaden the check to int. + types_ = (int,) if issubclass(hint, enum.IntEnum) else (hint,) + else: + continue + assert isinstance(value, types_), (value, types_) + + def check_net_address(addr, family): """Check a net address validity. Supported families are IPv4, IPv6 and MAC addresses. @@ -1606,6 +1652,7 @@ def check_status(conn): else: assert conn.status == psutil.CONN_NONE, conn.status + check_ntuple_types(conn) check_ntuple(conn) check_family(conn) check_type(conn) diff --git a/tests/test_contracts.py b/tests/test_contracts.py index f984247cea..c3ce8b5fef 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -31,11 +31,14 @@ from . import HAS_SENSORS_TEMPERATURES from . import SKIP_SYSCONS from . import PsutilTestCase +from . import check_ntuple_types from . import create_sockets from . import enum from . import is_namedtuple from . import kernel_version +from . import process_namespace from . import pytest +from . import system_namespace # =================================================================== # --- APIs availability @@ -329,3 +332,50 @@ def test_users(self): assert isinstance(user.pid, (int, type(None))) if isinstance(user.pid, int): assert user.pid > 0 + + +# =================================================================== +# --- namedtuple field types +# =================================================================== + + +class TestNtupleFieldTypes(PsutilTestCase): + """Check that namedtuple field values match the type annotations + defined in psutil/_ntuples.py. + """ + + def check_result(self, ret): + if is_namedtuple(ret): + check_ntuple_types(ret) + elif isinstance(ret, list): + for item in ret: + if is_namedtuple(item): + check_ntuple_types(item) + + def test_system_ntuple_types(self): + for fun, name in system_namespace.iter(system_namespace.getters): + try: + ret = fun() + except psutil.Error: + continue + with self.subTest(fun=str(fun)): + if isinstance(ret, dict): + for v in ret.values(): + if isinstance(v, list): + for item in v: + self.check_result(item) + else: + self.check_result(v) + else: + self.check_result(ret) + + def test_process_ntuple_types(self): + p = psutil.Process() + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters): + with self.subTest(fun=str(fun)): + try: + ret = fun() + except psutil.Error: + continue + self.check_result(ret) diff --git a/tests/test_process_all.py b/tests/test_process_all.py index c8853aa506..6b74c7037e 100755 --- a/tests/test_process_all.py +++ b/tests/test_process_all.py @@ -32,8 +32,8 @@ from . import VALID_PROC_STATUSES from . import PsutilTestCase from . import check_connection_ntuple +from . import check_ntuple_types from . import create_sockets -from . import is_namedtuple from . import is_win_secure_system_proc from . import process_namespace from . import pytest @@ -212,13 +212,13 @@ def create_time(self, ret, info): time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) def uids(self, ret, info): - assert is_namedtuple(ret) + check_ntuple_types(ret) for uid in ret: assert isinstance(uid, int) assert uid >= 0 def gids(self, ret, info): - assert is_namedtuple(ret) + check_ntuple_types(ret) # note: testing all gids as above seems not to be reliable for # gid == 30 (nodoby); not sure why. for gid in ret: @@ -238,7 +238,7 @@ def status(self, ret, info): assert ret in VALID_PROC_STATUSES def io_counters(self, ret, info): - assert is_namedtuple(ret) + check_ntuple_types(ret) for field in ret: assert isinstance(field, int) if field != -1: @@ -271,7 +271,7 @@ def num_threads(self, ret, info): def threads(self, ret, info): assert isinstance(ret, list) for t in ret: - assert is_namedtuple(t) + check_ntuple_types(t) assert t.id >= 0 assert t.user_time >= 0 assert t.system_time >= 0 @@ -279,7 +279,7 @@ def threads(self, ret, info): assert isinstance(field, (int, float)) def cpu_times(self, ret, info): - assert is_namedtuple(ret) + check_ntuple_types(ret) for n in ret: assert isinstance(n, float) assert n >= 0 @@ -305,7 +305,7 @@ def memory_info_ex(self, ret, info): self.check_proc_memory(ret) def memory_footprint(self, ret, info): - assert is_namedtuple(ret) + check_ntuple_types(ret) for name in ret._fields: value = getattr(ret, name) assert isinstance(value, int) @@ -314,6 +314,7 @@ def memory_footprint(self, ret, info): def open_files(self, ret, info): assert isinstance(ret, list) for f in ret: + check_ntuple_types(f) assert isinstance(f.fd, int) assert isinstance(f.path, str) assert f.path.strip() == f.path @@ -345,7 +346,6 @@ def net_connections(self, ret, info): with create_sockets(): assert len(ret) == len(set(ret)) for conn in ret: - assert is_namedtuple(conn) check_connection_ntuple(conn) def cwd(self, ret, info): @@ -387,8 +387,11 @@ def terminal(self, ret, info): def memory_maps(self, ret, info): for nt in ret: - assert isinstance(nt.addr, str) - assert isinstance(nt.perms, str) + check_ntuple_types(nt) + if hasattr(nt, "addr"): + assert isinstance(nt.addr, str) + if hasattr(nt, "perms"): + assert isinstance(nt.perms, str) assert isinstance(nt.path, str) for fname in nt._fields: value = getattr(nt, fname) @@ -415,7 +418,7 @@ def num_handles(self, ret, info): assert ret >= 0 def page_faults(self, ret, info): - assert is_namedtuple(ret) + check_ntuple_types(ret) assert isinstance(ret.minor, int) assert isinstance(ret.major, int) assert ret.minor >= 0 @@ -435,7 +438,7 @@ def nice(self, ret, info): assert isinstance(ret, enum.IntEnum) def num_ctx_switches(self, ret, info): - assert is_namedtuple(ret) + check_ntuple_types(ret) for value in ret: assert isinstance(value, int) assert value >= 0 From 2b654547b66298602a9b26ade5c7204a2e8a84c4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 8 Mar 2026 18:29:28 +0100 Subject: [PATCH 1569/1714] Fix #2754: make sensors_battery().percent return a float on all OSes --- HISTORY.rst | 4 +++- Makefile | 4 ++-- psutil/_pslinux.py | 2 +- psutil/arch/freebsd/sensors.c | 2 +- psutil/arch/osx/sensors.c | 4 +++- psutil/arch/windows/sensors.c | 4 ++-- tests/__init__.py | 2 +- tests/test_contracts.py | 4 ++-- tests/test_linux.py | 15 +++++++++++++++ 9 files changed, 30 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 86f58d80f2..d2e9f5b7a7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -51,6 +51,8 @@ ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type annotations**. This makes the classes self-documenting, effectively turning this module into a readable API reference. +- 2754_: standardize `sensors_battery()`_'s `percent` so that it returns a + `float` instead of `int` on all systems, not only Linux. **Bug fixes** @@ -275,7 +277,7 @@ Changes that break backwards compatibility: trigger a segfault. - 2604_, [Linux]: `virtual_memory()`_ "used" memory does not match recent versions of ``free`` CLI utility. (patch by Isaac K. Ko) -- 2605_, [Linux]: `psutil.sensors_battery()` reports a negative amount for +- 2605_, [Linux]: `sensors_battery()`_ reports a negative amount for seconds left. - 2607_, [Windows]: ``WindowsService.description()`` method may fail with ``ERROR_NOT_FOUND``. Now it returns an empty string instead. diff --git a/Makefile b/Makefile index 4bd9e8a05e..f0aada8b8c 100644 --- a/Makefile +++ b/Makefile @@ -91,8 +91,8 @@ install-git-hooks: ## Install GIT pre-commit hook. # Tests # =================================================================== -# disable cache on Windows because it causes "Permission denied" errors -_PYTEST_EXTRA != if [ "$$OS" = "Windows_NT" ]; then printf '%s' '-p no:cacheprovider'; fi +# Cache dir on Windows often causes "Permission denied" errors +_PYTEST_EXTRA != if [ "$$OS" = "Windows_NT" ]; then printf '%s' '-o cache_dir=/tmp/pytest-psutil-cache'; fi RUN_TEST = $(PYTHON_ENV_VARS) $(PYTHON) -m pytest $(_PYTEST_EXTRA) test: ## Run all tests (except memleak tests). diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7897e77594..b1b146a9d4 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -1419,7 +1419,7 @@ def multi_bcat(*paths): except ZeroDivisionError: percent = 0.0 else: - percent = int(cat(root + "/capacity", fallback=-1)) + percent = float(cat(root + "/capacity", fallback=-1)) if percent == -1: return None diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 60035014ad..698b50bf41 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -35,7 +35,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { goto error; if (psutil_sysctlbyname("hw.acpi.acline", &power_plugged, size) != 0) goto error; - return Py_BuildValue("iii", percent, minsleft, power_plugged); + return Py_BuildValue("dii", (double)percent, minsleft, power_plugged); error: // see: https://github.com/giampaolo/psutil/issues/1074 diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index c4b2afeed9..f012b2cab5 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -96,7 +96,9 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { time_to_empty = -1; } - py_tuple = Py_BuildValue("Iii", capacity, time_to_empty, is_power_plugged); + py_tuple = Py_BuildValue( + "dii", (double)capacity, time_to_empty, is_power_plugged + ); if (!py_tuple) goto error; diff --git a/psutil/arch/windows/sensors.c b/psutil/arch/windows/sensors.c index 6b599659f0..169324aa24 100644 --- a/psutil/arch/windows/sensors.c +++ b/psutil/arch/windows/sensors.c @@ -21,14 +21,14 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { return NULL; } return Py_BuildValue( - "iiiI", + "iidI", sps.ACLineStatus, // whether AC is connected: 0=no, 1=yes, 255=unknown // status flag: // 1, 2, 4 = high, low, critical // 8 = charging // 128 = no battery sps.BatteryFlag, - sps.BatteryLifePercent, // percent + (double)sps.BatteryLifePercent, // percent sps.BatteryLifeTime // remaining secs ); } diff --git a/tests/__init__.py b/tests/__init__.py index ef670e4ebc..c8a7c0ada2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1561,7 +1561,7 @@ def check_ntuple_types(nt): types_ = (int,) if issubclass(hint, enum.IntEnum) else (hint,) else: continue - assert isinstance(value, types_), (value, types_) + assert isinstance(value, types_), (field, value, types_) def check_net_address(addr, family): diff --git a/tests/test_contracts.py b/tests/test_contracts.py index c3ce8b5fef..a4c76ad162 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -358,7 +358,7 @@ def test_system_ntuple_types(self): ret = fun() except psutil.Error: continue - with self.subTest(fun=str(fun)): + with self.subTest(name=name, fun=str(fun)): if isinstance(ret, dict): for v in ret.values(): if isinstance(v, list): @@ -373,7 +373,7 @@ def test_process_ntuple_types(self): p = psutil.Process() ns = process_namespace(p) for fun, name in ns.iter(ns.getters): - with self.subTest(fun=str(fun)): + with self.subTest(name=name, fun=str(fun)): try: ret = fun() except psutil.Error: diff --git a/tests/test_linux.py b/tests/test_linux.py index ab39f64049..26746daa49 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -1658,6 +1658,21 @@ def test_emulate_energy_full_not_avail(self): ): assert psutil.sensors_battery().percent == 88 + @pytest.mark.skipif( + not os.path.isfile("/sys/class/power_supply/BAT0/capacity"), + reason="BAT /capacity file don't exist", + ) + def test_percent_against_capacity(self): + # Internally, if we have /energy_full, the percentage will be + # calculated by NOT reading the /capacity file, to get more + # accuracy. Check againt /capacity to make sure our percentage + # is calculated correctly. + with open("/sys/class/power_supply/BAT0/capacity") as f: + capacity = float(f.read()) + assert psutil.sensors_battery().percent == pytest.approx( + capacity, abs=1 + ) + def test_emulate_no_power(self): # Emulate a case where /AC0/online file nor /BAT0/status exist. with mock_open_exception( From 34a9e3245e89160ebb780dab6b2c45da048a9f79 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 8 Mar 2026 23:17:32 +0100 Subject: [PATCH 1570/1714] Enum constants (#2755) Problem ======= psutil exposes a number of named constants that identify process statuses (`STATUS_RUNNING`, `STATUS_ZOMBIE`, ...), connection states (`CONN_ESTABLISHED`, `CONN_NONE`, ...), etc. These constants have always been plain Python values, strings or integers, scattered across `_common.py`, `_pslinux.py`, `_pswindows.py`, etc. with no shared container. * This makes them hard to discover, impossible to enumerate programmatically, and invisible to static type checkers. * Code like `if p.status() == "runing":` fails silently due to a typo: a type checker cannot catch it because the return type is str. * There is no way to ask "give me all valid process statuses". Proposal ======== Introduce a new `psutil/_enums.py` module defining the following enum classes: - `ProcessStatus(StrEnum)`: groups `STATUS_*` constants - `ConnectionStatus(StrEnum)`: groups `CONN_*` constants - `ProcessPriority(IntEnum)`: groups `*_PRIORITY_CLASS` constants (Windows) - `ProcessIOPriorityClass(IntEnum)`: groups `IOPRIO_CLASS_*` (Linux) and `IOPRIO_*` (Windows) constants - ProcessRlimit(IntEnum): groups `RLIMIT_` constants (Linux, FreeBSD) - `NicDuplex(IntEnum)`: groups `NIC_DUPLEX_*` constants (was already an `IntEnum` in `_common.py`, now centralised) - `BatteryTime(IntEnum)`: groups `POWER_TIME_*` constants (same) All enum members are exported into the psutil top-level namespace under their existing names (`psutil.STATUS_RUNNING`, `psutil.CONN_ESTABLISHED`, etc.), so they are aliases of the enum members rather than independent values. Backward compatibility ====================== The change is designed to be as non-breaking as possible: - All existing top-level constants (`psutil.STATUS_RUNNING`, `psutil.CONN_ESTABLISHED`, etc.) stay, and are aliases for enum members rather than plain values. - `enum.StrEnum` subclasses `str`, so `p.status() == "running"` and `conn.status == "ESTABLISHED"` remain valid comparisons. - `enum.IntEnum` subclasses `int`, so `psutil.NIC_DUPLEX_FULL == 2` and similar comparisons continue to work. - `isinstance(p.status(), str)` still returns True. The only (minor) breaking change is that `repr()` of `STATUS_*` and `CONN_*` constants changes from `'running'` to ``, which may affect logging output or test assertions that check repr strings. --- HISTORY.rst | 33 +++++- MANIFEST.in | 1 + README.rst | 22 ++-- docs/index.rst | 138 +++++++++++++++++++++--- psutil/__init__.py | 115 +++++++------------- psutil/_common.py | 74 +------------ psutil/_enums.py | 149 ++++++++++++++++++++++++++ psutil/_ntuples.py | 13 ++- psutil/_psaix.py | 51 ++++----- psutil/_psbsd.py | 78 +++++++------- psutil/_pslinux.py | 105 ++++++++---------- psutil/_psosx.py | 46 ++++---- psutil/_pssunos.py | 65 ++++++----- psutil/_pswindows.py | 98 +++++------------ tests/__init__.py | 11 +- tests/test_contracts.py | 232 ++++++++++++++++++++++++++++++++-------- tests/test_misc.py | 2 + 17 files changed, 763 insertions(+), 470 deletions(-) create mode 100644 psutil/_enums.py diff --git a/HISTORY.rst b/HISTORY.rst index d2e9f5b7a7..bf3a63fb72 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -51,6 +51,11 @@ ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type annotations**. This makes the classes self-documenting, effectively turning this module into a readable API reference. +- 2753_: Introduce enum classes (`ProcessStatus`_, `ConnectionStatus`_, + `ProcessIOPriority`_, `ProcessPriority`_, `ProcessRlimit`_) grouping related + constants. The individual top-level constants (e.g. + ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases for + the corresponding enum members. - 2754_: standardize `sensors_battery()`_'s `percent` so that it returns a `float` instead of `int` on all systems, not only Linux. @@ -69,7 +74,9 @@ **Compatibility notes** -Changes that break backwards compatibility: +Changes that break backwards compatibility. + +Named tuples: - `cpu_times()`_: @@ -86,6 +93,24 @@ Changes that break backwards compatibility: p.memory_info()``) may break or silently return the wrong field. Always use attribute access instead (e.g. ``p.memory_info().rss``). +Enums: + +- `Process.status()`_ now returns a :class:`psutil.ProcessStatus` enum member + instead of a plain ``str``. Since :class:`psutil.ProcessStatus` is a + ``StrEnum``, it compares equal to its string value (e.g. + ``p.status() == "running"`` still works), but ``repr()`` and ``type()`` + differ. Use :data:`psutil.STATUS_RUNNING` and friends as before; + ``psutil.STATUS_RUNNING`` is now an alias for the enum member. + +- `net_connections()`_ and `Process.net_connections()`_: the ``status`` field + now returns a :class:`psutil.ConnectionStatus` enum member instead of a + plain ``str``. Same ``StrEnum`` compatibility rules as above apply. + +- ``RLIMIT_*`` / ``RLIM_*`` constants (Linux, FreeBSD): these are now members + of the :class:`psutil.ProcessRlimit` ``IntEnum`` instead of plain integers. + Since ``IntEnum`` compares equal to integers, existing code using them as + arguments to :meth:`Process.rlimit` is unaffected. + 7.2.3 ===== @@ -3052,6 +3077,12 @@ In most cases accessing the old names will work but it will cause a .. _`Process.username()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.username .. _`Process.wait()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.wait +.. _`ProcessStatus`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessStatus +.. _`ProcessPriority`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessPriority +.. _`ProcessIOPriority`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessIOPriority +.. _`ProcessRlimit`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessRlimit +.. _`ConnectionStatus`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ConnectionStatus + .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py .. _`free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py diff --git a/MANIFEST.in b/MANIFEST.in index dbb8953aad..337788531a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -27,6 +27,7 @@ include docs/make.bat include docs/requirements.txt include psutil/__init__.py include psutil/_common.py +include psutil/_enums.py include psutil/_ntuples.py include psutil/_psaix.py include psutil/_psbsd.py diff --git a/README.rst b/README.rst index 72a01ab8db..72bc447435 100644 --- a/README.rst +++ b/README.rst @@ -286,7 +286,7 @@ Process management >>> >>> p = psutil.Process(7055) >>> p - psutil.Process(pid=7055, name='python3', status='running', started='09:04:44') + psutil.Process(pid=7055, name='python3', status=, started='09:04:44') >>> p.pid 7055 >>> p.name() @@ -301,17 +301,17 @@ Process management >>> p.ppid() 7054 >>> p.parent() - psutil.Process(pid=4699, name='bash', status='sleeping', started='09:06:44') + psutil.Process(pid=4699, name='bash', status=, started='09:06:44') >>> p.parents() [psutil.Process(pid=4699, name='bash', started='09:06:44'), - psutil.Process(pid=4689, name='gnome-terminal-server', status='sleeping', started='0:06:44'), - psutil.Process(pid=1, name='systemd', status='sleeping', started='05:56:55')] + psutil.Process(pid=4689, name='gnome-terminal-server', status=, started='0:06:44'), + psutil.Process(pid=1, name='systemd', status=, started='05:56:55')] >>> p.children(recursive=True) - [psutil.Process(pid=29835, name='python3', status='sleeping', started='11:45:38'), - psutil.Process(pid=29836, name='python3', status='waking', started='11:43:39')] + [psutil.Process(pid=29835, name='python3', status=, started='11:45:38'), + psutil.Process(pid=29836, name='python3', status=, started='11:43:39')] >>> >>> p.status() - 'running' + >>> p.create_time() 1267551141.5019531 >>> p.terminal() @@ -360,8 +360,8 @@ Process management popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] >>> >>> p.net_connections(kind='tcp') - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')] + [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=)] >>> >>> p.threads() [pthread(id=5234, user_time=22.5, system_time=9.2891), @@ -380,7 +380,7 @@ Process management >>> >>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # IO priority (Win and Linux only) >>> p.ionice() - pionice(ioclass=, value=0) + pionice(ioclass=, value=0) >>> >>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits (Linux only) >>> p.rlimit(psutil.RLIMIT_NOFILE) @@ -392,7 +392,7 @@ Process management ...} >>> >>> p.as_dict() - {'status': 'running', 'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), 'pid': 5457, ...} + {'status': , 'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), 'pid': 5457, ...} >>> p.is_running() True >>> p.suspend() diff --git a/docs/index.rst b/docs/index.rst index c2d97d529d..6eb6a8904a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -751,10 +751,10 @@ Network >>> import psutil >>> psutil.net_connections() - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING', pid=2987), - pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status='ESTABLISHED', pid=None), - pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status='SYN_SENT', pid=None) + [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=, pid=1254), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=, pid=2987), + pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=, pid=None), + pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=, pid=None) ...] .. warning:: @@ -783,6 +783,9 @@ Network .. versionchanged:: 5.9.5 : OpenBSD: retrieve *laddr* path for AF_UNIX sockets (before it was an empty string). + .. versionchanged:: 8.0.0 *status* field is now a + :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + .. function:: net_if_addrs() Return the addresses associated to each NIC (network interface card) @@ -1410,9 +1413,13 @@ Process class .. method:: status() - The current process status as a string. The returned string is one of the + The current process status as a :class:`psutil.ProcessStatus` enum member. + The returned value is one of the `psutil.STATUS_* <#process-status-constants>`_ constants. + .. versionchanged:: 8.0.0 return value is now a :class:`psutil.ProcessStatus` + enum member instead of a plain ``str``. + .. method:: cwd() The process current working directory as an absolute path. If cwd cannot be @@ -1466,10 +1473,14 @@ Process class `SetPriorityClass`_ Windows APIs and *value* is one of the :data:`psutil.*_PRIORITY_CLASS ` constants reflecting the MSDN documentation. + The return value on Windows is a :class:`psutil.ProcessPriority` enum member. Example which increases process priority on Windows: >>> p.nice(psutil.HIGH_PRIORITY_CLASS) + .. versionchanged:: 8.0.0 on Windows, return value is now a + :class:`psutil.ProcessPriority` enum member. + .. method:: ionice(ioclass=None, value=None) Get or set process I/O niceness (priority). @@ -1511,12 +1522,14 @@ Process class ... p.ionice(psutil.IOPRIO_HIGH) ... >>> p.ionice() # get - pionice(ioclass=, value=7) + pionice(ioclass=, value=7) Availability: Linux, Windows Vista+ - .. versionchanged:: 5.6.2 Windows accepts new ``IOPRIO_*`` constants - including new ``IOPRIO_HIGH``. + .. versionchanged:: 5.6.2 Windows accepts new ``IOPRIO_*`` constants. + + .. versionchanged:: 8.0.0 *ioclass* is now a + :class:`psutil.ProcessIOPriority` enum member. .. method:: rlimit(resource, limits=None) @@ -2209,10 +2222,10 @@ Process class >>> p.name() 'firefox' >>> p.net_connections() - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING'), - pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status='ESTABLISHED'), - pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status='SYN_SENT')] + [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=), + pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=), + pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=)] .. warning:: On Linux, retrieving connections for certain processes requires root @@ -2240,6 +2253,9 @@ Process class .. versionchanged:: 6.0.0 : method renamed from `connections` to `net_connections`. + .. versionchanged:: 8.0.0 *status* field is now a + :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + .. method:: connections() Same as :meth:`net_connections` (deprecated). @@ -2568,6 +2584,71 @@ Example code: Constants ========= +The following enum classes group related constants and are useful for type +annotations and introspection. The individual constants (e.g. +:data:`psutil.STATUS_RUNNING`) are also accessible directly from the psutil +namespace as aliases for the enum members and should be preferred over +accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over +``psutil.ProcessStatus.STATUS_RUNNING``). + +.. class:: psutil.ProcessStatus + + `enum.StrEnum`_ collection of :data:`STATUS_* ` + constants. Returned by :meth:`psutil.Process.status()`. + + .. versionadded:: 8.0.0 + +.. class:: psutil.ProcessPriority + + `enum.IntEnum`_ collection of + :data:`*_PRIORITY_CLASS ` constants for + :meth:`psutil.Process.nice` on Windows. + + Availability: Windows + + .. versionadded:: 8.0.0 + +.. class:: psutil.ProcessIOPriority + + `enum.IntEnum`_ collection of I/O priority constants for + :meth:`psutil.Process.ionice`. On Linux: ``IOPRIO_CLASS_*`` constants. + On Windows: ``IOPRIO_*`` constants. + + Availability: Linux, Windows + + .. versionadded:: 8.0.0 + +.. class:: psutil.ProcessRlimit + + `enum.IntEnum`_ collection of :data:`RLIMIT_* ` + constants for :meth:`psutil.Process.rlimit`. + + Availability: Linux, FreeBSD + + .. versionadded:: 8.0.0 + +.. class:: psutil.ConnectionStatus + + `enum.StrEnum`_ collection of :data:`CONN_* ` + constants. Returned in the *status* field of + :func:`psutil.net_connections` and :meth:`psutil.Process.net_connections`. + + .. versionadded:: 8.0.0 + +.. class:: psutil.NicDuplex + + `enum.IntEnum`_ collection of :data:`NIC_DUPLEX_* ` + constants. Returned in the *duplex* field of :func:`psutil.net_if_stats`. + + .. versionadded:: 3.0.0 + +.. class:: psutil.BatteryTime + + `enum.IntEnum`_ collection of :data:`POWER_TIME_* ` + constants. May appear in the *secsleft* field of :func:`psutil.sensors_battery`. + + .. versionadded:: 5.1.0 + Operating system constants -------------------------- @@ -2635,9 +2716,12 @@ Process status constants .. data:: STATUS_SUSPENDED (NetBSD) Represent a process status. Returned by :meth:`psutil.Process.status()`. + These constants are members of the :class:`psutil.ProcessStatus` enum. .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) .. versionadded:: 5.4.7 ``STATUS_PARKED`` (Linux) + .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessStatus` + enum members (were plain strings). Process priority constants -------------------------- @@ -2653,9 +2737,13 @@ Process priority constants Represent the priority of a process on Windows (see `SetPriorityClass`_). They can be used in conjunction with :meth:`psutil.Process.nice()` to get or set process priority. + These constants are members of the :class:`psutil.ProcessPriority` enum. Availability: Windows + .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessPriority` + enum members (were plain integers). + .. _const-ioprio: .. data:: IOPRIO_CLASS_NONE .. data:: IOPRIO_CLASS_RT @@ -2665,6 +2753,8 @@ Process priority constants A set of integers representing the I/O priority of a process on Linux. They can be used in conjunction with :meth:`psutil.Process.ionice()` to get or set process I/O priority. + These constants are members of the :class:`psutil.ProcessIOPriority` + enum. *IOPRIO_CLASS_NONE* and *IOPRIO_CLASS_BE* (best effort) is the default for any process that hasn't set a specific I/O priority. *IOPRIO_CLASS_RT* (real time) means the process is given first access to the @@ -2677,6 +2767,10 @@ Process priority constants Availability: Linux + .. versionchanged:: 8.0.0 constants are now + :class:`psutil.ProcessIOPriority` enum members (previously + ``IOPriority`` enum). + .. data:: IOPRIO_VERYLOW .. data:: IOPRIO_LOW .. data:: IOPRIO_NORMAL @@ -2685,10 +2779,15 @@ Process priority constants A set of integers representing the I/O priority of a process on Windows. They can be used in conjunction with :meth:`psutil.Process.ionice()` to get or set process I/O priority. + These constants are members of the :class:`psutil.ProcessIOPriority` + enum. Availability: Windows .. versionadded:: 5.6.2 + .. versionchanged:: 8.0.0 constants are now + :class:`psutil.ProcessIOPriority` enum members (previously + ``IOPriority`` enum). Process resources constants --------------------------- @@ -2725,11 +2824,14 @@ FreeBSD specific: Constants used for getting and setting process resource limits to be used in conjunction with :meth:`psutil.Process.rlimit()`. See `resource.getrlimit`_ for further information. +These constants are members of the :class:`psutil.ProcessRlimit` enum. Availability: Linux, FreeBSD .. versionchanged:: 5.7.3 added FreeBSD support, added ``RLIMIT_SWAP``, ``RLIMIT_SBSIZE``, ``RLIMIT_NPTS``. +.. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessRlimit` + enum members (were plain integers). Connections constants --------------------- @@ -2754,6 +2856,10 @@ Connections constants A set of strings representing the status of a TCP connection. Returned by :meth:`psutil.Process.net_connections()` and :func:`psutil.net_connections` (`status` field). + These constants are members of the :class:`psutil.ConnectionStatus` enum. + + .. versionchanged:: 8.0.0 constants are now :class:`psutil.ConnectionStatus` + enum members (were plain strings). Hardware constants ------------------ @@ -2885,9 +2991,9 @@ Processes owned by user:: Processes actively running:: >>> pp([(p.pid, p.info) for p in psutil.process_iter(['name', 'status']) if p.info['status'] == psutil.STATUS_RUNNING]) - [(1150, {'name': 'Xorg', 'status': 'running'}), - (1776, {'name': 'unity-panel-service', 'status': 'running'}), - (20492, {'name': 'python', 'status': 'running'})] + [(1150, {'name': 'Xorg', 'status': }), + (1776, {'name': 'unity-panel-service', 'status': }), + (20492, {'name': 'python', 'status': })] Processes using log files:: @@ -3471,6 +3577,8 @@ Timeline .. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py .. _`DEVGUIDE.rst`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum +.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum .. _`enum`: https://docs.python.org/3/library/enum.html#module-enum .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py .. _`GetDriveType`: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea diff --git a/psutil/__init__.py b/psutil/__init__.py index ae1266a82c..0f1c51b69d 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -40,42 +40,13 @@ from . import _ntuples as _ntp from ._common import AIX from ._common import BSD -from ._common import CONN_CLOSE -from ._common import CONN_CLOSE_WAIT -from ._common import CONN_CLOSING -from ._common import CONN_ESTABLISHED -from ._common import CONN_FIN_WAIT1 -from ._common import CONN_FIN_WAIT2 -from ._common import CONN_LAST_ACK -from ._common import CONN_LISTEN -from ._common import CONN_NONE -from ._common import CONN_SYN_RECV -from ._common import CONN_SYN_SENT -from ._common import CONN_TIME_WAIT from ._common import FREEBSD from ._common import LINUX from ._common import MACOS from ._common import NETBSD -from ._common import NIC_DUPLEX_FULL -from ._common import NIC_DUPLEX_HALF -from ._common import NIC_DUPLEX_UNKNOWN from ._common import OPENBSD from ._common import OSX # deprecated alias from ._common import POSIX -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 @@ -86,6 +57,10 @@ from ._common import debug from ._common import memoize_when_activated from ._common import wrap_numbers as _wrap_numbers +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus if LINUX: # This is public API and it will be retrieved from _pslinux.py @@ -93,24 +68,13 @@ PROCFS_PATH = "/proc" from . import _pslinux as _psplatform - from ._pslinux import IOPRIO_CLASS_BE # noqa: F401 - from ._pslinux import IOPRIO_CLASS_IDLE # noqa: F401 - from ._pslinux import IOPRIO_CLASS_NONE # noqa: F401 - from ._pslinux import IOPRIO_CLASS_RT # noqa: F401 + from ._enums import ProcessIOPriority + from ._enums import ProcessRlimit elif WINDOWS: from . import _pswindows as _psplatform - from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS # noqa: F401 - from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS # noqa: F401 - from ._psutil_windows import HIGH_PRIORITY_CLASS # noqa: F401 - from ._psutil_windows import IDLE_PRIORITY_CLASS # noqa: F401 - from ._psutil_windows import NORMAL_PRIORITY_CLASS # noqa: F401 - from ._psutil_windows import REALTIME_PRIORITY_CLASS # noqa: F401 - from ._pswindows import CONN_DELETE_TCB # noqa: F401 - from ._pswindows import IOPRIO_HIGH # noqa: F401 - from ._pswindows import IOPRIO_LOW # noqa: F401 - from ._pswindows import IOPRIO_NORMAL # noqa: F401 - from ._pswindows import IOPRIO_VERYLOW # noqa: F401 + from ._enums import ProcessIOPriority + from ._enums import ProcessPriority elif MACOS: from . import _psosx as _psplatform @@ -118,10 +82,11 @@ elif BSD: from . import _psbsd as _psplatform + if FREEBSD: + from ._enums import ProcessRlimit + elif SUNOS: from . import _pssunos as _psplatform - from ._pssunos import CONN_BOUND # noqa: F401 - from ._pssunos import CONN_IDLE # noqa: F401 # This is public writable API which is read from _pslinux.py and # _pssunos.py via sys.modules. @@ -148,29 +113,11 @@ # constants "version_info", "__version__", - "STATUS_RUNNING", "STATUS_IDLE", "STATUS_SLEEPING", "STATUS_DISK_SLEEP", - "STATUS_STOPPED", "STATUS_TRACING_STOP", "STATUS_ZOMBIE", "STATUS_DEAD", - "STATUS_WAKING", "STATUS_LOCKED", "STATUS_WAITING", "STATUS_PARKED", - - "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", - "NIC_DUPLEX_FULL", "NIC_DUPLEX_HALF", "NIC_DUPLEX_UNKNOWN", - - "POWER_TIME_UNKNOWN", "POWER_TIME_UNLIMITED", - "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", @@ -187,19 +134,33 @@ ] # fmt: on - __all__.extend(_psplatform.__extra__all__) +_globals = globals() + + +def _export_enum(cls): + __all__.append(cls.__name__) + for name, member in cls.__members__.items(): + if name not in _globals: # noqa: F821 + _globals[name] = member # noqa: F821 + __all__.append(name) + + +# Populate global namespace with enums and CONSTANTs. +_export_enum(ProcessStatus) +_export_enum(ConnectionStatus) +_export_enum(NicDuplex) +_export_enum(BatteryTime) +if LINUX or WINDOWS: + _export_enum(ProcessIOPriority) +if WINDOWS: + _export_enum(ProcessPriority) +if LINUX or FREEBSD: + _export_enum(ProcessRlimit) +if LINUX or SUNOS or AIX: + __all__.append("PROCFS_PATH") -# Linux, FreeBSD -if hasattr(_psplatform.Process, "rlimit"): - # Populate global namespace with RLIM* constants. - _globals = globals() - _name = None - for _name in dir(_psplatform.cext): - if _name.startswith('RLIM') and _name.isupper(): - _globals[_name] = getattr(_psplatform.cext, _name) - __all__.append(_name) - del _globals, _name +del _globals, _export_enum AF_LINK = _psplatform.AF_LINK @@ -443,7 +404,7 @@ def __eq__(self, other): if pid1 == pid2: if ident1 and not ident2: try: - return self.status() == STATUS_ZOMBIE + return self.status() == ProcessStatus.STATUS_ZOMBIE except Error: pass return self._ident == other._ident @@ -752,7 +713,7 @@ def status(self): try: return self._proc.status() except ZombieProcess: - return STATUS_ZOMBIE + return ProcessStatus.STATUS_ZOMBIE def username(self): """The name of the user that owns the process. diff --git a/psutil/_common.py b/psutil/_common.py index 2e3dcac0b8..2c36ff0d6e 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -9,7 +9,6 @@ """ import collections -import enum import functools import os import socket @@ -39,17 +38,6 @@ # OS constants 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX', 'SUNOS', 'WINDOWS', - # connection constants - 'CONN_CLOSE', 'CONN_CLOSE_WAIT', 'CONN_CLOSING', 'CONN_ESTABLISHED', - 'CONN_FIN_WAIT1', 'CONN_FIN_WAIT2', 'CONN_LAST_ACK', 'CONN_LISTEN', - 'CONN_NONE', 'CONN_SYN_RECV', 'CONN_SYN_SENT', 'CONN_TIME_WAIT', - # net constants - 'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN', # noqa: F822 - # process status constants - 'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED', - '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', # utility functions @@ -81,63 +69,6 @@ SUNOS = sys.platform.startswith(("sunos", "solaris")) AIX = sys.platform.startswith("aix") - -# =================================================================== -# --- API constants -# =================================================================== - - -# Process.status() -STATUS_RUNNING = "running" -STATUS_SLEEPING = "sleeping" -STATUS_DISK_SLEEP = "disk-sleep" -STATUS_STOPPED = "stopped" -STATUS_TRACING_STOP = "tracing-stop" -STATUS_ZOMBIE = "zombie" -STATUS_DEAD = "dead" -STATUS_WAKE_KILL = "wake-kill" -STATUS_WAKING = "waking" -STATUS_IDLE = "idle" # Linux, macOS, FreeBSD -STATUS_LOCKED = "locked" # FreeBSD -STATUS_WAITING = "waiting" # FreeBSD -STATUS_SUSPENDED = "suspended" # NetBSD -STATUS_PARKED = "parked" # Linux - -# Process.net_connections() and psutil.net_connections() -CONN_ESTABLISHED = "ESTABLISHED" -CONN_SYN_SENT = "SYN_SENT" -CONN_SYN_RECV = "SYN_RECV" -CONN_FIN_WAIT1 = "FIN_WAIT1" -CONN_FIN_WAIT2 = "FIN_WAIT2" -CONN_TIME_WAIT = "TIME_WAIT" -CONN_CLOSE = "CLOSE" -CONN_CLOSE_WAIT = "CLOSE_WAIT" -CONN_LAST_ACK = "LAST_ACK" -CONN_LISTEN = "LISTEN" -CONN_CLOSING = "CLOSING" -CONN_NONE = "NONE" - - -# net_if_stats() -class NicDuplex(enum.IntEnum): - NIC_DUPLEX_FULL = 2 - NIC_DUPLEX_HALF = 1 - NIC_DUPLEX_UNKNOWN = 0 - - -globals().update(NicDuplex.__members__) - - -# sensors_battery() -class BatteryTime(enum.IntEnum): - POWER_TIME_UNKNOWN = -1 - POWER_TIME_UNLIMITED = -2 - - -globals().update(BatteryTime.__members__) - -# --- others - ENCODING = sys.getfilesystemencoding() ENCODING_ERRS = sys.getfilesystemencodeerrors() @@ -501,6 +432,7 @@ def socktype_to_enum(num): def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): """Convert a raw connection tuple to a proper ntuple.""" from . import _ntuples as ntp + from ._enums import ConnectionStatus if fam in {socket.AF_INET, AF_INET6}: if laddr: @@ -508,9 +440,9 @@ def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): if raddr: raddr = ntp.addr(*raddr) if type_ == socket.SOCK_STREAM and fam in {AF_INET, AF_INET6}: - status = status_map.get(status, CONN_NONE) + status = status_map.get(status, ConnectionStatus.CONN_NONE) else: - status = CONN_NONE # ignore whatever C returned to us + status = ConnectionStatus.CONN_NONE # ignore whatever C returned to us fam = sockfam_to_enum(fam) type_ = socktype_to_enum(type_) if pid is None: diff --git a/psutil/_enums.py b/psutil/_enums.py new file mode 100644 index 0000000000..6cde53c390 --- /dev/null +++ b/psutil/_enums.py @@ -0,0 +1,149 @@ +# 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. + +"""Enum containers backing psutil constants. + +This module groups constants used by psutil APIs into Enum classes. +These enums mainly act as containers for related values and are useful +for type annotations and introspection. + +In normal usage constants should be accessed directly from the psutil +namespace instead of importing these enums, e.g.: + + import psutil + if proc.status() == psutil.STATUS_RUNNING: + ... + +The top-level constants (e.g. ``psutil.STATUS_RUNNING``) are aliases of +the enum members defined here and represent the primary public API. +""" + +import enum + +from ._common import FREEBSD +from ._common import LINUX +from ._common import SUNOS +from ._common import WINDOWS + +if WINDOWS: + from . import _psutil_windows as cext +elif LINUX: + from . import _psutil_linux as cext +elif FREEBSD: + from . import _psutil_bsd as cext +else: + cext = None + +if hasattr(enum, "StrEnum"): # Python >= 3.11 + StrEnum = enum.StrEnum +else: + + # A backport of Python 3.11 StrEnum class for >= Python 3.8 + class StrEnum(str, enum.Enum): + def __new__(cls, *values): + value = str(*values) + member = str.__new__(cls, value) + member._value_ = value + return member + + __str__ = str.__str__ + + @staticmethod + def _generate_next_value_(name, _start, _count, _last_values): + return name.lower() + + +# psutil.Process.status() +class ProcessStatus(StrEnum): + STATUS_DEAD = "dead" + STATUS_DISK_SLEEP = "disk-sleep" + STATUS_IDLE = "idle" # Linux, macOS, FreeBSD + STATUS_LOCKED = "locked" # FreeBSD + STATUS_PARKED = "parked" # Linux + STATUS_RUNNING = "running" + STATUS_SLEEPING = "sleeping" + STATUS_STOPPED = "stopped" + STATUS_SUSPENDED = "suspended" # NetBSD + STATUS_TRACING_STOP = "tracing-stop" + STATUS_WAITING = "waiting" # FreeBSD + STATUS_WAKE_KILL = "wake-kill" + STATUS_WAKING = "waking" + STATUS_ZOMBIE = "zombie" + + +# psutil.Process.net_connections() and psutil.net_connections() +class ConnectionStatus(StrEnum): + CONN_CLOSE = "CLOSE" + CONN_CLOSE_WAIT = "CLOSE_WAIT" + CONN_CLOSING = "CLOSING" + CONN_ESTABLISHED = "ESTABLISHED" + CONN_FIN_WAIT1 = "FIN_WAIT1" + CONN_FIN_WAIT2 = "FIN_WAIT2" + CONN_LAST_ACK = "LAST_ACK" + CONN_LISTEN = "LISTEN" + CONN_NONE = "NONE" + CONN_SYN_RECV = "SYN_RECV" + CONN_SYN_SENT = "SYN_SENT" + CONN_TIME_WAIT = "TIME_WAIT" + if WINDOWS: + CONN_DELETE_TCB = "DELETE_TCB" + if SUNOS: + CONN_BOUND = "CONN_BOUND" + CONN_IDLE = "CONN_IDLE" + + +# psutil.net_if_stats() +class NicDuplex(enum.IntEnum): + NIC_DUPLEX_FULL = 2 + NIC_DUPLEX_HALF = 1 + NIC_DUPLEX_UNKNOWN = 0 + + +# psutil.sensors_battery() +class BatteryTime(enum.IntEnum): + POWER_TIME_UNKNOWN = -1 + POWER_TIME_UNLIMITED = -2 + + +if LINUX: + + # psutil.Process.ionice(ioclass=…) + class ProcessIOPriority(enum.IntEnum): + # ioprio_* constants http://linux.die.net/man/2/ioprio_get + IOPRIO_CLASS_NONE = 0 + IOPRIO_CLASS_RT = 1 + IOPRIO_CLASS_BE = 2 + IOPRIO_CLASS_IDLE = 3 + + +if WINDOWS: + + # psutil.Process.ionice(ioclass=…) + class ProcessIOPriority(enum.IntEnum): + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 + + # psutil.Process.nice() + class ProcessPriority(enum.IntEnum): + ABOVE_NORMAL_PRIORITY_CLASS = cext.ABOVE_NORMAL_PRIORITY_CLASS + BELOW_NORMAL_PRIORITY_CLASS = cext.BELOW_NORMAL_PRIORITY_CLASS + HIGH_PRIORITY_CLASS = cext.HIGH_PRIORITY_CLASS + IDLE_PRIORITY_CLASS = cext.IDLE_PRIORITY_CLASS + NORMAL_PRIORITY_CLASS = cext.NORMAL_PRIORITY_CLASS + REALTIME_PRIORITY_CLASS = cext.REALTIME_PRIORITY_CLASS + + +if LINUX or FREEBSD: + + # psutil.Process.rlimit() + ProcessRlimit = enum.IntEnum( + "ProcessRlimit", + ( + (name, getattr(cext, name)) + for name in dir(cext) + if name.startswith("RLIM") and name.isupper() + ), + ) diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 9ae556ed20..e76530a4d0 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -12,6 +12,11 @@ if TYPE_CHECKING: import socket + from ._enums import BatteryTime + from ._enums import ConnectionStatus + from ._enums import NicDuplex + from ._enums import ProcessIOPriority + from ._common import AIX from ._common import BSD from ._common import FREEBSD @@ -21,8 +26,6 @@ from ._common import OPENBSD from ._common import SUNOS from ._common import WINDOWS -from ._common import BatteryTime -from ._common import NicDuplex # =================================================================== # --- system functions @@ -106,7 +109,7 @@ class sconn(NamedTuple): type: socket.SocketKind laddr: addr | tuple | str raddr: addr | tuple | str - status: str + status: ConnectionStatus pid: int | None @@ -286,7 +289,7 @@ class pio(NamedTuple): # psutil.Process.ionice() class pionice(NamedTuple): - ioclass: int + ioclass: ProcessIOPriority value: int @@ -319,7 +322,7 @@ class pconn(NamedTuple): type: socket.SocketKind laddr: addr | tuple | str raddr: addr | tuple | str - status: str + status: ConnectionStatus # psutil.Process.memory_maps(grouped=True) diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 720ae3e689..5fcc367ae1 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -13,20 +13,20 @@ import subprocess import sys -from . import _common from . import _ntuples as ntp from . import _psposix from . import _psutil_aix as cext -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 conn_tmap from ._common import conn_to_ntuple from ._common import get_procfs_path from ._common import memoize_when_activated from ._common import usage_percent +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus __extra__all__ = ["PROCFS_PATH"] @@ -44,26 +44,26 @@ AF_LINK = cext.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.SSTOP: _common.STATUS_STOPPED, + cext.SIDL: ProcessStatus.STATUS_IDLE, + cext.SZOMB: ProcessStatus.STATUS_ZOMBIE, + cext.SACTIVE: ProcessStatus.STATUS_RUNNING, + cext.SSWAP: ProcessStatus.STATUS_RUNNING, # TODO what status is this? + cext.SSTOP: ProcessStatus.STATUS_STOPPED, } TCP_STATUSES = { - cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, - cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, - cext.TCPS_SYN_RCVD: _common.CONN_SYN_RECV, - cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, - cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, - cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, - cext.TCPS_CLOSED: _common.CONN_CLOSE, - cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, - cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, - cext.TCPS_LISTEN: _common.CONN_LISTEN, - cext.TCPS_CLOSING: _common.CONN_CLOSING, - cext.PSUTIL_CONN_NONE: _common.CONN_NONE, + cext.TCPS_ESTABLISHED: ConnectionStatus.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: ConnectionStatus.CONN_SYN_SENT, + cext.TCPS_SYN_RCVD: ConnectionStatus.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: ConnectionStatus.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: ConnectionStatus.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: ConnectionStatus.CONN_TIME_WAIT, + cext.TCPS_CLOSED: ConnectionStatus.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: ConnectionStatus.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: ConnectionStatus.CONN_LAST_ACK, + cext.TCPS_LISTEN: ConnectionStatus.CONN_LISTEN, + cext.TCPS_CLOSING: ConnectionStatus.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: ConnectionStatus.CONN_NONE, } proc_info_map = dict( @@ -186,7 +186,7 @@ def net_connections(kind, _pid=-1): """Return socket connections. If pid == -1 return system-wide connections (as opposed to connections opened by one process only). """ - families, types = _common.conn_tmap[kind] + families, types = conn_tmap[kind] rawlist = cext.net_connections(_pid) ret = [] for item in rawlist: @@ -211,7 +211,10 @@ def net_connections(kind, _pid=-1): def net_if_stats(): """Get NIC stats (isup, duplex, speed, mtu).""" - duplex_map = {"Full": NIC_DUPLEX_FULL, "Half": NIC_DUPLEX_HALF} + duplex_map = { + "Full": NicDuplex.NIC_DUPLEX_FULL, + "Half": NicDuplex.NIC_DUPLEX_HALF, + } names = {x[0] for x in net_if_addrs()} ret = {} for name in names: @@ -242,7 +245,7 @@ def net_if_stats(): output_flags = ','.join(flags) isup = 'running' in flags - duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN) + duplex = duplex_map.get(duplex, NicDuplex.NIC_DUPLEX_UNKNOWN) ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) return ret diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 0205a93c82..852f1dd9eb 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -12,7 +12,6 @@ from collections import namedtuple from xml.etree import ElementTree # noqa: ICN001 -from . import _common from . import _ntuples as ntp from . import _psposix from . import _psutil_bsd as cext @@ -27,6 +26,10 @@ from ._common import debug from ._common import memoize from ._common import memoize_when_activated +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus __extra__all__ = [] @@ -38,26 +41,26 @@ if FREEBSD: PROC_STATUSES = { - cext.SIDL: _common.STATUS_IDLE, - cext.SRUN: _common.STATUS_RUNNING, - cext.SSLEEP: _common.STATUS_SLEEPING, - cext.SSTOP: _common.STATUS_STOPPED, - cext.SZOMB: _common.STATUS_ZOMBIE, - cext.SWAIT: _common.STATUS_WAITING, - cext.SLOCK: _common.STATUS_LOCKED, + cext.SIDL: ProcessStatus.STATUS_IDLE, + cext.SRUN: ProcessStatus.STATUS_RUNNING, + cext.SSLEEP: ProcessStatus.STATUS_SLEEPING, + cext.SSTOP: ProcessStatus.STATUS_STOPPED, + cext.SZOMB: ProcessStatus.STATUS_ZOMBIE, + cext.SWAIT: ProcessStatus.STATUS_WAITING, + cext.SLOCK: ProcessStatus.STATUS_LOCKED, } elif OPENBSD: PROC_STATUSES = { - cext.SIDL: _common.STATUS_IDLE, - cext.SSLEEP: _common.STATUS_SLEEPING, - cext.SSTOP: _common.STATUS_STOPPED, + cext.SIDL: ProcessStatus.STATUS_IDLE, + cext.SSLEEP: ProcessStatus.STATUS_SLEEPING, + cext.SSTOP: ProcessStatus.STATUS_STOPPED, # According to /usr/include/sys/proc.h SZOMB is unused. # test_zombie_process() shows that SDEAD is the right # equivalent. Also it appears there's no equivalent of # psutil.STATUS_DEAD. SDEAD really means STATUS_ZOMBIE. - # cext.SZOMB: _common.STATUS_ZOMBIE, - cext.SDEAD: _common.STATUS_ZOMBIE, - cext.SZOMB: _common.STATUS_ZOMBIE, + # cext.SZOMB: ProcStatus.STATUS_ZOMBIE, + cext.SDEAD: ProcessStatus.STATUS_ZOMBIE, + cext.SZOMB: ProcessStatus.STATUS_ZOMBIE, # From http://www.eecs.harvard.edu/~margo/cs161/videos/proc.h.txt # OpenBSD has SRUN and SONPROC: SRUN indicates that a process # is runnable but *not* yet running, i.e. is on a run queue. @@ -65,32 +68,32 @@ # a CPU, i.e. it is no longer on a run queue. # As such we'll map SRUN to STATUS_WAKING and SONPROC to # STATUS_RUNNING - cext.SRUN: _common.STATUS_WAKING, - cext.SONPROC: _common.STATUS_RUNNING, + cext.SRUN: ProcessStatus.STATUS_WAKING, + cext.SONPROC: ProcessStatus.STATUS_RUNNING, } elif NETBSD: PROC_STATUSES = { - cext.SIDL: _common.STATUS_IDLE, - cext.SSLEEP: _common.STATUS_SLEEPING, - cext.SSTOP: _common.STATUS_STOPPED, - cext.SZOMB: _common.STATUS_ZOMBIE, - cext.SRUN: _common.STATUS_WAKING, - cext.SONPROC: _common.STATUS_RUNNING, + cext.SIDL: ProcessStatus.STATUS_IDLE, + cext.SSLEEP: ProcessStatus.STATUS_SLEEPING, + cext.SSTOP: ProcessStatus.STATUS_STOPPED, + cext.SZOMB: ProcessStatus.STATUS_ZOMBIE, + cext.SRUN: ProcessStatus.STATUS_WAKING, + cext.SONPROC: ProcessStatus.STATUS_RUNNING, } TCP_STATUSES = { - cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, - cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, - cext.TCPS_SYN_RECEIVED: _common.CONN_SYN_RECV, - cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, - cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, - cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, - cext.TCPS_CLOSED: _common.CONN_CLOSE, - cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, - cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, - cext.TCPS_LISTEN: _common.CONN_LISTEN, - cext.TCPS_CLOSING: _common.CONN_CLOSING, - cext.PSUTIL_CONN_NONE: _common.CONN_NONE, + cext.TCPS_ESTABLISHED: ConnectionStatus.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: ConnectionStatus.CONN_SYN_SENT, + cext.TCPS_SYN_RECEIVED: ConnectionStatus.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: ConnectionStatus.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: ConnectionStatus.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: ConnectionStatus.CONN_TIME_WAIT, + cext.TCPS_CLOSED: ConnectionStatus.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: ConnectionStatus.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: ConnectionStatus.CONN_LAST_ACK, + cext.TCPS_LISTEN: ConnectionStatus.CONN_LISTEN, + cext.TCPS_CLOSING: ConnectionStatus.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: ConnectionStatus.CONN_NONE, } PAGESIZE = cext.getpagesize() @@ -295,8 +298,7 @@ def net_if_stats(): if err.errno != errno.ENODEV: raise else: - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) + duplex = NicDuplex(duplex) output_flags = ','.join(flags) isup = 'running' in flags ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) @@ -339,9 +341,9 @@ def sensors_battery(): return None power_plugged = power_plugged == 1 if power_plugged: - secsleft = _common.POWER_TIME_UNLIMITED + secsleft = BatteryTime.POWER_TIME_UNLIMITED elif minsleft == -1: - secsleft = _common.POWER_TIME_UNKNOWN + secsleft = BatteryTime.POWER_TIME_UNKNOWN else: secsleft = minsleft * 60 return ntp.sbattery(percent, secsleft, power_plugged) diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index b1b146a9d4..82e2b5d891 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -19,14 +19,10 @@ import warnings from collections import defaultdict -from . import _common from . import _ntuples as ntp from . import _psposix from . import _psutil_linux as cext from ._common import ENCODING -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 @@ -41,21 +37,16 @@ from ._common import open_text from ._common import parse_environ_block from ._common import path_exists_strict +from ._common import socktype_to_enum from ._common import supports_ipv6 from ._common import usage_percent +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessIOPriority +from ._enums import ProcessStatus -# fmt: off -__extra__all__ = [ - 'PROCFS_PATH', - # io prio constants - "IOPRIO_CLASS_NONE", "IOPRIO_CLASS_RT", "IOPRIO_CLASS_BE", - "IOPRIO_CLASS_IDLE", - # 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", -] -# fmt: on +__extra__all__ = ['PROCFS_PATH'] # ===================================================================== @@ -94,48 +85,38 @@ AF_LINK = AddressFamily.AF_LINK -# ioprio_* constants http://linux.die.net/man/2/ioprio_get -class IOPriority(enum.IntEnum): - IOPRIO_CLASS_NONE = 0 - IOPRIO_CLASS_RT = 1 - IOPRIO_CLASS_BE = 2 - IOPRIO_CLASS_IDLE = 3 - - -globals().update(IOPriority.__members__) - # See: # https://github.com/torvalds/linux/blame/master/fs/proc/array.c # ...and (TASK_* constants): # https://github.com/torvalds/linux/blob/master/include/linux/sched.h PROC_STATUSES = { - "R": _common.STATUS_RUNNING, - "S": _common.STATUS_SLEEPING, - "D": _common.STATUS_DISK_SLEEP, - "T": _common.STATUS_STOPPED, - "t": _common.STATUS_TRACING_STOP, - "Z": _common.STATUS_ZOMBIE, - "X": _common.STATUS_DEAD, - "x": _common.STATUS_DEAD, - "K": _common.STATUS_WAKE_KILL, - "W": _common.STATUS_WAKING, - "I": _common.STATUS_IDLE, - "P": _common.STATUS_PARKED, + "R": ProcessStatus.STATUS_RUNNING, + "S": ProcessStatus.STATUS_SLEEPING, + "D": ProcessStatus.STATUS_DISK_SLEEP, + "T": ProcessStatus.STATUS_STOPPED, + "t": ProcessStatus.STATUS_TRACING_STOP, + "Z": ProcessStatus.STATUS_ZOMBIE, + "X": ProcessStatus.STATUS_DEAD, + "x": ProcessStatus.STATUS_DEAD, + "K": ProcessStatus.STATUS_WAKE_KILL, + "W": ProcessStatus.STATUS_WAKING, + "I": ProcessStatus.STATUS_IDLE, + "P": ProcessStatus.STATUS_PARKED, } # https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h TCP_STATUSES = { - "01": _common.CONN_ESTABLISHED, - "02": _common.CONN_SYN_SENT, - "03": _common.CONN_SYN_RECV, - "04": _common.CONN_FIN_WAIT1, - "05": _common.CONN_FIN_WAIT2, - "06": _common.CONN_TIME_WAIT, - "07": _common.CONN_CLOSE, - "08": _common.CONN_CLOSE_WAIT, - "09": _common.CONN_LAST_ACK, - "0A": _common.CONN_LISTEN, - "0B": _common.CONN_CLOSING, + "01": ConnectionStatus.CONN_ESTABLISHED, + "02": ConnectionStatus.CONN_SYN_SENT, + "03": ConnectionStatus.CONN_SYN_RECV, + "04": ConnectionStatus.CONN_FIN_WAIT1, + "05": ConnectionStatus.CONN_FIN_WAIT2, + "06": ConnectionStatus.CONN_TIME_WAIT, + "07": ConnectionStatus.CONN_CLOSE, + "08": ConnectionStatus.CONN_CLOSE_WAIT, + "09": ConnectionStatus.CONN_LAST_ACK, + "0A": ConnectionStatus.CONN_LISTEN, + "0B": ConnectionStatus.CONN_CLOSING, } @@ -834,7 +815,7 @@ def process_inet(file, family, type_, inodes, filter_pid=None): if type_ == socket.SOCK_STREAM: status = TCP_STATUSES[status] else: - status = _common.CONN_NONE + status = ConnectionStatus.CONN_NONE try: laddr = NetConnections.decode_address(laddr, family) raddr = NetConnections.decode_address(raddr, family) @@ -870,12 +851,12 @@ def process_unix(file, family, inodes, filter_pid=None): continue else: path = tokens[-1] if len(tokens) == 8 else '' - type_ = _common.socktype_to_enum(int(type_)) + type_ = 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/ raddr = "" - status = _common.CONN_NONE + status = ConnectionStatus.CONN_NONE yield (fd, family, type_, path, raddr, status, pid) def retrieve(self, kind, pid=None): @@ -965,9 +946,9 @@ def net_io_counters(): 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, + cext.DUPLEX_FULL: NicDuplex.NIC_DUPLEX_FULL, + cext.DUPLEX_HALF: NicDuplex.NIC_DUPLEX_HALF, + cext.DUPLEX_UNKNOWN: NicDuplex.NIC_DUPLEX_UNKNOWN, } names = net_io_counters().keys() ret = {} @@ -1442,18 +1423,18 @@ def multi_bcat(*paths): # Seconds left. if power_plugged: - secsleft = _common.POWER_TIME_UNLIMITED + secsleft = BatteryTime.POWER_TIME_UNLIMITED elif energy_now is not None and power_now is not None: try: secsleft = int(energy_now / abs(power_now) * 3600) except ZeroDivisionError: - secsleft = _common.POWER_TIME_UNKNOWN + secsleft = BatteryTime.POWER_TIME_UNKNOWN elif time_to_empty is not None: secsleft = int(time_to_empty * 60) if secsleft < 0: - secsleft = _common.POWER_TIME_UNKNOWN + secsleft = BatteryTime.POWER_TIME_UNKNOWN else: - secsleft = _common.POWER_TIME_UNKNOWN + secsleft = BatteryTime.POWER_TIME_UNKNOWN return ntp.sbattery(percent, secsleft, power_plugged) @@ -2145,7 +2126,7 @@ def cpu_affinity_set(self, cpus): @wrap_exceptions def ionice_get(self): ioclass, value = cext.proc_ioprio_get(self.pid) - ioclass = IOPriority(ioclass) + ioclass = ProcessIOPriority(ioclass) return ntp.pionice(ioclass, value) @wrap_exceptions @@ -2153,8 +2134,8 @@ def ionice_set(self, ioclass, value): if value is None: value = 0 if value and ioclass in { - IOPriority.IOPRIO_CLASS_IDLE, - IOPriority.IOPRIO_CLASS_NONE, + ProcessIOPriority.IOPRIO_CLASS_IDLE, + ProcessIOPriority.IOPRIO_CLASS_NONE, }: msg = f"{ioclass!r} ioclass accepts no value" raise ValueError(msg) diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 68a29c6977..2751ad4475 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -8,7 +8,6 @@ import functools import os -from . import _common from . import _ntuples as ntp from . import _psposix from . import _psutil_osx as cext @@ -22,6 +21,10 @@ from ._common import memoize_when_activated from ._common import parse_environ_block from ._common import usage_percent +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus __extra__all__ = [] @@ -35,26 +38,26 @@ AF_LINK = cext.AF_LINK TCP_STATUSES = { - cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, - cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, - cext.TCPS_SYN_RECEIVED: _common.CONN_SYN_RECV, - cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, - cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, - cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, - cext.TCPS_CLOSED: _common.CONN_CLOSE, - cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, - cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, - cext.TCPS_LISTEN: _common.CONN_LISTEN, - cext.TCPS_CLOSING: _common.CONN_CLOSING, - cext.PSUTIL_CONN_NONE: _common.CONN_NONE, + cext.TCPS_ESTABLISHED: ConnectionStatus.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: ConnectionStatus.CONN_SYN_SENT, + cext.TCPS_SYN_RECEIVED: ConnectionStatus.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: ConnectionStatus.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: ConnectionStatus.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: ConnectionStatus.CONN_TIME_WAIT, + cext.TCPS_CLOSED: ConnectionStatus.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: ConnectionStatus.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: ConnectionStatus.CONN_LAST_ACK, + cext.TCPS_LISTEN: ConnectionStatus.CONN_LISTEN, + cext.TCPS_CLOSING: ConnectionStatus.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: ConnectionStatus.CONN_NONE, } PROC_STATUSES = { - cext.SIDL: _common.STATUS_IDLE, - cext.SRUN: _common.STATUS_RUNNING, - cext.SSLEEP: _common.STATUS_SLEEPING, - cext.SSTOP: _common.STATUS_STOPPED, - cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SIDL: ProcessStatus.STATUS_IDLE, + cext.SRUN: ProcessStatus.STATUS_RUNNING, + cext.SSLEEP: ProcessStatus.STATUS_SLEEPING, + cext.SSTOP: ProcessStatus.STATUS_STOPPED, + cext.SZOMB: ProcessStatus.STATUS_ZOMBIE, } @@ -173,9 +176,9 @@ def sensors_battery(): return None power_plugged = power_plugged == 1 if power_plugged: - secsleft = _common.POWER_TIME_UNLIMITED + secsleft = BatteryTime.POWER_TIME_UNLIMITED elif minsleft == -1: - secsleft = _common.POWER_TIME_UNKNOWN + secsleft = BatteryTime.POWER_TIME_UNKNOWN else: secsleft = minsleft * 60 return ntp.sbattery(percent, secsleft, power_plugged) @@ -222,8 +225,7 @@ def net_if_stats(): if err.errno != errno.ENODEV: raise else: - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) + duplex = NicDuplex(duplex) output_flags = ','.join(flags) isup = 'running' in flags ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index c7d1325ad1..6c316006ae 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -13,7 +13,6 @@ from collections import namedtuple from socket import AF_INET -from . import _common from . import _ntuples as ntp from . import _psposix from . import _psutil_sunos as cext @@ -22,6 +21,7 @@ from ._common import AccessDenied from ._common import NoSuchProcess from ._common import ZombieProcess +from ._common import conn_tmap from ._common import debug from ._common import get_procfs_path from ._common import isfile_strict @@ -29,8 +29,11 @@ from ._common import sockfam_to_enum from ._common import socktype_to_enum from ._common import usage_percent +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus -__extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] +__extra__all__ = ["PROCFS_PATH"] # ===================================================================== @@ -42,34 +45,32 @@ AF_LINK = cext.AF_LINK IS_64_BIT = sys.maxsize > 2**32 -CONN_IDLE = "IDLE" -CONN_BOUND = "BOUND" PROC_STATUSES = { - cext.SSLEEP: _common.STATUS_SLEEPING, - cext.SRUN: _common.STATUS_RUNNING, - cext.SZOMB: _common.STATUS_ZOMBIE, - cext.SSTOP: _common.STATUS_STOPPED, - cext.SIDL: _common.STATUS_IDLE, - cext.SONPROC: _common.STATUS_RUNNING, # same as run - cext.SWAIT: _common.STATUS_WAITING, + cext.SSLEEP: ProcessStatus.STATUS_SLEEPING, + cext.SRUN: ProcessStatus.STATUS_RUNNING, + cext.SZOMB: ProcessStatus.STATUS_ZOMBIE, + cext.SSTOP: ProcessStatus.STATUS_STOPPED, + cext.SIDL: ProcessStatus.STATUS_IDLE, + cext.SONPROC: ProcessStatus.STATUS_RUNNING, # same as run + cext.SWAIT: ProcessStatus.STATUS_WAITING, } TCP_STATUSES = { - cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, - cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, - cext.TCPS_SYN_RCVD: _common.CONN_SYN_RECV, - cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, - cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, - cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, - cext.TCPS_CLOSED: _common.CONN_CLOSE, - cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, - cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, - cext.TCPS_LISTEN: _common.CONN_LISTEN, - cext.TCPS_CLOSING: _common.CONN_CLOSING, - cext.PSUTIL_CONN_NONE: _common.CONN_NONE, - cext.TCPS_IDLE: CONN_IDLE, # sunos specific - cext.TCPS_BOUND: CONN_BOUND, # sunos specific + cext.TCPS_ESTABLISHED: ConnectionStatus.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: ConnectionStatus.CONN_SYN_SENT, + cext.TCPS_SYN_RCVD: ConnectionStatus.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: ConnectionStatus.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: ConnectionStatus.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: ConnectionStatus.CONN_TIME_WAIT, + cext.TCPS_CLOSED: ConnectionStatus.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: ConnectionStatus.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: ConnectionStatus.CONN_LAST_ACK, + cext.TCPS_LISTEN: ConnectionStatus.CONN_LISTEN, + cext.TCPS_CLOSING: ConnectionStatus.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: ConnectionStatus.CONN_NONE, + cext.TCPS_IDLE: ConnectionStatus.CONN_IDLE, # sunos specific + cext.TCPS_BOUND: ConnectionStatus.CONN_BOUND, # sunos specific } proc_info_map = dict( @@ -231,7 +232,7 @@ def net_connections(kind, _pid=-1): connections (as opposed to connections opened by one process only). Only INET sockets are returned (UNIX are not). """ - families, types = _common.conn_tmap[kind] + families, types = conn_tmap[kind] rawlist = cext.net_connections(_pid) ret = set() for item in rawlist: @@ -262,8 +263,7 @@ def net_if_stats(): ret = cext.net_if_stats() for name, items in ret.items(): isup, duplex, speed, mtu = items - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) + duplex = NicDuplex(duplex) ret[name] = ntp.snicstats(isup, duplex, speed, mtu, '') return ret @@ -615,7 +615,14 @@ def _get_unix_sockets(self, pid): type = socket.SOCK_DGRAM else: type = -1 - yield (-1, socket.AF_UNIX, type, path, "", _common.CONN_NONE) + yield ( + -1, + socket.AF_UNIX, + type, + path, + "", + ConnectionStatus.CONN_NONE, + ) @wrap_exceptions def net_connections(self, kind='inet'): diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index cc1cd54548..0a2309a2a0 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -13,7 +13,6 @@ import threading import time -from . import _common from . import _ntuples as ntp from ._common import ENCODING from ._common import AccessDenied @@ -27,12 +26,12 @@ from ._common import memoize_when_activated from ._common import parse_environ_block from ._common import usage_percent -from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS -from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS -from ._psutil_windows import HIGH_PRIORITY_CLASS -from ._psutil_windows import IDLE_PRIORITY_CLASS -from ._psutil_windows import NORMAL_PRIORITY_CLASS -from ._psutil_windows import REALTIME_PRIORITY_CLASS +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessIOPriority +from ._enums import ProcessPriority +from ._enums import ProcessStatus try: from . import _psutil_windows as cext @@ -55,26 +54,13 @@ # 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", - # IO priority - "IOPRIO_VERYLOW", "IOPRIO_LOW", "IOPRIO_NORMAL", "IOPRIO_HIGH", - # others - "CONN_DELETE_TCB", "AF_LINK", -] -# fmt: on +__extra__all__ = ["win_service_iter", "win_service_get", "AF_LINK"] # ===================================================================== # --- globals # ===================================================================== -CONN_DELETE_TCB = "DELETE_TCB" ERROR_PARTIAL_COPY = 299 PYPY = '__pypy__' in sys.builtin_module_names @@ -82,44 +68,22 @@ AF_LINK = AddressFamily.AF_LINK TCP_STATUSES = { - cext.MIB_TCP_STATE_ESTAB: _common.CONN_ESTABLISHED, - cext.MIB_TCP_STATE_SYN_SENT: _common.CONN_SYN_SENT, - cext.MIB_TCP_STATE_SYN_RCVD: _common.CONN_SYN_RECV, - cext.MIB_TCP_STATE_FIN_WAIT1: _common.CONN_FIN_WAIT1, - cext.MIB_TCP_STATE_FIN_WAIT2: _common.CONN_FIN_WAIT2, - cext.MIB_TCP_STATE_TIME_WAIT: _common.CONN_TIME_WAIT, - cext.MIB_TCP_STATE_CLOSED: _common.CONN_CLOSE, - cext.MIB_TCP_STATE_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, - cext.MIB_TCP_STATE_LAST_ACK: _common.CONN_LAST_ACK, - cext.MIB_TCP_STATE_LISTEN: _common.CONN_LISTEN, - cext.MIB_TCP_STATE_CLOSING: _common.CONN_CLOSING, - cext.MIB_TCP_STATE_DELETE_TCB: CONN_DELETE_TCB, - cext.PSUTIL_CONN_NONE: _common.CONN_NONE, + cext.MIB_TCP_STATE_ESTAB: ConnectionStatus.CONN_ESTABLISHED, + cext.MIB_TCP_STATE_SYN_SENT: ConnectionStatus.CONN_SYN_SENT, + cext.MIB_TCP_STATE_SYN_RCVD: ConnectionStatus.CONN_SYN_RECV, + cext.MIB_TCP_STATE_FIN_WAIT1: ConnectionStatus.CONN_FIN_WAIT1, + cext.MIB_TCP_STATE_FIN_WAIT2: ConnectionStatus.CONN_FIN_WAIT2, + cext.MIB_TCP_STATE_TIME_WAIT: ConnectionStatus.CONN_TIME_WAIT, + cext.MIB_TCP_STATE_CLOSED: ConnectionStatus.CONN_CLOSE, + cext.MIB_TCP_STATE_CLOSE_WAIT: ConnectionStatus.CONN_CLOSE_WAIT, + cext.MIB_TCP_STATE_LAST_ACK: ConnectionStatus.CONN_LAST_ACK, + cext.MIB_TCP_STATE_LISTEN: ConnectionStatus.CONN_LISTEN, + cext.MIB_TCP_STATE_CLOSING: ConnectionStatus.CONN_CLOSING, + cext.MIB_TCP_STATE_DELETE_TCB: ConnectionStatus.CONN_DELETE_TCB, + cext.PSUTIL_CONN_NONE: ConnectionStatus.CONN_NONE, } -class Priority(enum.IntEnum): - ABOVE_NORMAL_PRIORITY_CLASS = ABOVE_NORMAL_PRIORITY_CLASS - BELOW_NORMAL_PRIORITY_CLASS = BELOW_NORMAL_PRIORITY_CLASS - HIGH_PRIORITY_CLASS = HIGH_PRIORITY_CLASS - IDLE_PRIORITY_CLASS = IDLE_PRIORITY_CLASS - NORMAL_PRIORITY_CLASS = NORMAL_PRIORITY_CLASS - REALTIME_PRIORITY_CLASS = REALTIME_PRIORITY_CLASS - - -globals().update(Priority.__members__) - - -class IOPriority(enum.IntEnum): - IOPRIO_VERYLOW = 0 - IOPRIO_LOW = 1 - IOPRIO_NORMAL = 2 - IOPRIO_HIGH = 3 - - -globals().update(IOPriority.__members__) - - # ===================================================================== # --- utils # ===================================================================== @@ -342,8 +306,7 @@ def net_if_stats(): rawdict = cext.net_if_stats() for name, items in rawdict.items(): isup, duplex, speed, mtu = items - if hasattr(_common, 'NicDuplex'): - duplex = _common.NicDuplex(duplex) + duplex = NicDuplex(duplex) ret[name] = ntp.snicstats(isup, duplex, speed, mtu, '') return ret @@ -377,9 +340,9 @@ def sensors_battery(): if no_battery: return None if power_plugged or charging: - secsleft = _common.POWER_TIME_UNLIMITED + secsleft = BatteryTime.POWER_TIME_UNLIMITED elif secsleft == -1: - secsleft = _common.POWER_TIME_UNKNOWN + secsleft = BatteryTime.POWER_TIME_UNKNOWN return ntp.sbattery(percent, secsleft, power_plugged) @@ -998,7 +961,7 @@ def net_connections(self, kind='inet'): @wrap_exceptions def nice_get(self): value = cext.proc_priority_get(self.pid) - value = Priority(value) + value = ProcessPriority(value) return value @wrap_exceptions @@ -1008,7 +971,7 @@ def nice_set(self, value): @wrap_exceptions def ionice_get(self): ret = cext.proc_io_priority_get(self.pid) - ret = IOPriority(ret) + ret = ProcessIOPriority(ret) return ret @wrap_exceptions @@ -1016,12 +979,7 @@ def ionice_set(self, ioclass, value): if value: msg = "value argument not accepted on Windows" raise TypeError(msg) - if ioclass not in { - IOPriority.IOPRIO_VERYLOW, - IOPriority.IOPRIO_LOW, - IOPriority.IOPRIO_NORMAL, - IOPriority.IOPRIO_HIGH, - }: + if ioclass not in ProcessIOPriority: msg = f"{ioclass} is not a valid priority" raise ValueError(msg) cext.proc_io_priority_set(self.pid, ioclass) @@ -1049,9 +1007,9 @@ def io_counters(self): def status(self): suspended = cext.proc_is_suspended(self.pid) if suspended: - return _common.STATUS_STOPPED + return ProcessStatus.STATUS_STOPPED else: - return _common.STATUS_RUNNING + return ProcessStatus.STATUS_RUNNING @wrap_exceptions def cpu_affinity_get(self): diff --git a/tests/__init__.py b/tests/__init__.py index c8a7c0ada2..77ef37d9e0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -53,6 +53,7 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil import _enums from psutil._common import debug from psutil._common import memoize from psutil._common import supports_ipv6 @@ -1528,8 +1529,16 @@ def create_sockets(): @functools.lru_cache(maxsize=None) def _get_hints(cls): try: + localns = { + name: obj + for name, obj in vars(_enums).items() + if isinstance(obj, type) and issubclass(obj, enum.Enum) + } + localns['socket'] = socket return typing.get_type_hints( - cls, globalns=vars(ntuples), localns={'socket': socket} + cls, + globalns=vars(ntuples), + localns=localns, ) except TypeError: # Python < 3.10 can't evaluate "X | Y" union syntax. diff --git a/tests/test_contracts.py b/tests/test_contracts.py index a4c76ad162..2b0c9aeb16 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -22,6 +22,10 @@ from psutil import POSIX from psutil import SUNOS from psutil import WINDOWS +from psutil import BatteryTime +from psutil import ConnectionStatus +from psutil import NicDuplex +from psutil import ProcessStatus from . import AARCH64 from . import GITHUB_ACTIONS @@ -49,62 +53,202 @@ class TestAvailConstantsAPIs(PsutilTestCase): + + def check_constants(self, names, are_avail): + for name in names: + with self.subTest(name=name): + # assert CONSTANT is/isn't in psutil namespace + assert hasattr(psutil, name) == are_avail + # assert CONSTANT is/isn't in psutil.__all__ + if are_avail: + assert name in psutil.__all__ + else: + assert name not in psutil.__all__ + def test_PROCFS_PATH(self): - assert hasattr(psutil, "PROCFS_PATH") == (LINUX or SUNOS or AIX) - - def test_win_priority(self): - assert hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS") == WINDOWS - assert hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS") == WINDOWS - assert hasattr(psutil, "HIGH_PRIORITY_CLASS") == WINDOWS - assert hasattr(psutil, "IDLE_PRIORITY_CLASS") == WINDOWS - assert hasattr(psutil, "NORMAL_PRIORITY_CLASS") == WINDOWS - assert hasattr(psutil, "REALTIME_PRIORITY_CLASS") == WINDOWS - - def test_linux_ioprio_linux(self): - assert hasattr(psutil, "IOPRIO_CLASS_NONE") == LINUX - assert hasattr(psutil, "IOPRIO_CLASS_RT") == LINUX - assert hasattr(psutil, "IOPRIO_CLASS_BE") == LINUX - assert hasattr(psutil, "IOPRIO_CLASS_IDLE") == LINUX - - def test_linux_ioprio_windows(self): - assert hasattr(psutil, "IOPRIO_HIGH") == WINDOWS - assert hasattr(psutil, "IOPRIO_NORMAL") == WINDOWS - assert hasattr(psutil, "IOPRIO_LOW") == WINDOWS - assert hasattr(psutil, "IOPRIO_VERYLOW") == WINDOWS + self.check_constants(("PROCFS_PATH",), LINUX or SUNOS or AIX) + + def test_proc_status(self): + names = ( + "STATUS_RUNNING", + "STATUS_SLEEPING", + "STATUS_DISK_SLEEP", + "STATUS_STOPPED", + "STATUS_TRACING_STOP", + "STATUS_ZOMBIE", + "STATUS_DEAD", + "STATUS_WAKE_KILL", + "STATUS_WAKING", + "STATUS_IDLE", + "STATUS_LOCKED", + "STATUS_WAITING", + "STATUS_SUSPENDED", + "STATUS_PARKED", + ) + self.check_constants(names, True) + assert sorted(ProcessStatus.__members__.keys()) == sorted(names) + + def test_proc_status_strenum(self): + mapping = ( + (psutil.STATUS_RUNNING, "running"), + (psutil.STATUS_SLEEPING, "sleeping"), + (psutil.STATUS_DISK_SLEEP, "disk-sleep"), + (psutil.STATUS_STOPPED, "stopped"), + (psutil.STATUS_TRACING_STOP, "tracing-stop"), + (psutil.STATUS_ZOMBIE, "zombie"), + (psutil.STATUS_DEAD, "dead"), + (psutil.STATUS_WAKE_KILL, "wake-kill"), + (psutil.STATUS_WAKING, "waking"), + (psutil.STATUS_IDLE, "idle"), + (psutil.STATUS_LOCKED, "locked"), + (psutil.STATUS_WAITING, "waiting"), + (psutil.STATUS_SUSPENDED, "suspended"), + (psutil.STATUS_PARKED, "parked"), + ) + for en, str_ in mapping: + assert en == str_ + assert str(en) == str_ + assert repr(en) != str_ + + def test_conn_status(self): + names = [ + "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", + ] + if WINDOWS: + names.append("CONN_DELETE_TCB") + if SUNOS: + names.extend(["CONN_IDLE", "CONN_BOUND"]) + + self.check_constants(names, True) + assert sorted(ConnectionStatus.__members__.keys()) == sorted(names) + + def test_conn_status_strenum(self): + mapping = ( + (psutil.CONN_ESTABLISHED, "ESTABLISHED"), + (psutil.CONN_SYN_SENT, "SYN_SENT"), + (psutil.CONN_SYN_RECV, "SYN_RECV"), + (psutil.CONN_FIN_WAIT1, "FIN_WAIT1"), + (psutil.CONN_FIN_WAIT2, "FIN_WAIT2"), + (psutil.CONN_TIME_WAIT, "TIME_WAIT"), + (psutil.CONN_CLOSE, "CLOSE"), + (psutil.CONN_CLOSE_WAIT, "CLOSE_WAIT"), + (psutil.CONN_LAST_ACK, "LAST_ACK"), + (psutil.CONN_LISTEN, "LISTEN"), + (psutil.CONN_CLOSING, "CLOSING"), + (psutil.CONN_NONE, "NONE"), + ) + for en, str_ in mapping: + assert en == str_ + assert str(en) == str_ + assert repr(en) != str_ + + def test_nic_duplex(self): + names = ("NIC_DUPLEX_FULL", "NIC_DUPLEX_HALF", "NIC_DUPLEX_UNKNOWN") + self.check_constants(names, True) + assert sorted(NicDuplex.__members__.keys()) == sorted(names) + + def test_battery_time(self): + names = ("POWER_TIME_UNKNOWN", "POWER_TIME_UNLIMITED") + self.check_constants(names, True) + assert sorted(BatteryTime.__members__.keys()) == sorted(names) + + def test_proc_ioprio_class_linux(self): + names = ( + "IOPRIO_CLASS_NONE", + "IOPRIO_CLASS_RT", + "IOPRIO_CLASS_BE", + "IOPRIO_CLASS_IDLE", + ) + self.check_constants(names, LINUX) + if LINUX: + assert sorted( + psutil.ProcessIOPriority.__members__.keys() + ) == sorted(names) + else: + not hasattr(psutil, "ProcessIOPriority") + + def test_proc_ioprio_value_windows(self): + names = ( + "IOPRIO_HIGH", + "IOPRIO_NORMAL", + "IOPRIO_LOW", + "IOPRIO_VERYLOW", + ) + self.check_constants(names, WINDOWS) + if WINDOWS: + assert sorted( + psutil.ProcessIOPriority.__members__.keys() + ) == sorted(names) + + def test_proc_priority_windows(self): + names = ( + "ABOVE_NORMAL_PRIORITY_CLASS", + "BELOW_NORMAL_PRIORITY_CLASS", + "HIGH_PRIORITY_CLASS", + "IDLE_PRIORITY_CLASS", + "NORMAL_PRIORITY_CLASS", + "REALTIME_PRIORITY_CLASS", + ) + self.check_constants(names, WINDOWS) + if WINDOWS: + assert sorted(psutil.ProcessPriority.__members__.keys()) == sorted( + names + ) + else: + not hasattr(psutil, "ProcessPriority") @pytest.mark.skipif( GITHUB_ACTIONS and LINUX, reason="unsupported on GITHUB_ACTIONS + LINUX", ) def test_rlimit(self): - assert hasattr(psutil, "RLIM_INFINITY") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_AS") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_CORE") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_CPU") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_DATA") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_FSIZE") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_MEMLOCK") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_NOFILE") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_NPROC") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_RSS") == LINUX or FREEBSD - assert hasattr(psutil, "RLIMIT_STACK") == LINUX or FREEBSD - - assert hasattr(psutil, "RLIMIT_LOCKS") == LINUX + names = ( + "RLIM_INFINITY", + "RLIMIT_AS", + "RLIMIT_CORE", + "RLIMIT_CPU", + "RLIMIT_DATA", + "RLIMIT_FSIZE", + "RLIMIT_MEMLOCK", + "RLIMIT_NOFILE", + "RLIMIT_NPROC", + "RLIMIT_RSS", + "RLIMIT_STACK", + ) + self.check_constants(names, LINUX or FREEBSD) + self.check_constants(("RLIMIT_LOCKS",), LINUX) + self.check_constants( + ("RLIMIT_SWAP", "RLIMIT_SBSIZE", "RLIMIT_NPTS"), FREEBSD + ) + if POSIX: if kernel_version() >= (2, 6, 8): - assert hasattr(psutil, "RLIMIT_MSGQUEUE") == LINUX + self.check_constants(("RLIMIT_MSGQUEUE",), LINUX) if kernel_version() >= (2, 6, 12): - assert hasattr(psutil, "RLIMIT_NICE") == LINUX - if kernel_version() >= (2, 6, 12): - assert hasattr(psutil, "RLIMIT_RTPRIO") == LINUX + self.check_constants(("RLIMIT_NICE", "RLIMIT_RTPRIO"), LINUX) if kernel_version() >= (2, 6, 25): - assert hasattr(psutil, "RLIMIT_RTTIME") == LINUX + self.check_constants(("RLIMIT_RTTIME",), LINUX) if kernel_version() >= (2, 6, 8): - assert hasattr(psutil, "RLIMIT_SIGPENDING") == LINUX - - assert hasattr(psutil, "RLIMIT_SWAP") == FREEBSD - assert hasattr(psutil, "RLIMIT_SBSIZE") == FREEBSD - assert hasattr(psutil, "RLIMIT_NPTS") == FREEBSD + self.check_constants(("RLIMIT_SIGPENDING",), LINUX) + + def test_enum_containers(self): + self.check_constants(("ProcessStatus",), True) + self.check_constants(("ProcessPriority",), WINDOWS) + self.check_constants(("ProcessIOPriority",), LINUX or WINDOWS) + self.check_constants(("ConnectionStatus",), True) + self.check_constants(("NicDuplex",), True) + self.check_constants(("BatteryTime",), True) class TestAvailSystemAPIs(PsutilTestCase): diff --git a/tests/test_misc.py b/tests/test_misc.py index e9f2ee8c66..fd608ebae8 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -196,6 +196,8 @@ def test_process__hash__(self): class TestMisc(PsutilTestCase): def test__all__(self): dir_psutil = dir(psutil) + # assert there's no duplicates + assert len(dir_psutil) == len(set(dir_psutil)) for name in dir_psutil: if name in { 'debug', From e42e82555fa1112fd31c2f046748ea1898158ddf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 9 Mar 2026 16:02:21 +0100 Subject: [PATCH 1571/1714] Assume getloadavg() is always available --- docs/index.rst | 2 -- psutil/__init__.py | 17 +++++++++-------- tests/__init__.py | 4 +--- tests/test_linux.py | 2 -- tests/test_system.py | 2 -- 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6eb6a8904a..4124e4da23 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -337,8 +337,6 @@ CPU >>> [x / psutil.cpu_count() * 100 for x in psutil.getloadavg()] [31.4, 38.9, 46.7] - Availability: Unix, Windows - .. versionadded:: 5.6.2 Memory diff --git a/psutil/__init__.py b/psutil/__init__.py index 0f1c51b69d..acf6bcb69c 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -125,7 +125,7 @@ "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", "getloadavg" + "cpu_stats", # "cpu_freq", "net_io_counters", "net_connections", "net_if_addrs", # network "net_if_stats", "disk_io_counters", "disk_partitions", "disk_usage", # disk @@ -1999,15 +1999,16 @@ def cpu_freq(percpu=False): __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. +def getloadavg(): + """Return the average system load over the last 1, 5 and 15 minutes + as a tuple. On Windows this is emulated by using a Windows API that + spawns a thread which keeps running in background and updates + results every 5 seconds, mimicking the UNIX behavior. + """ if hasattr(os, "getloadavg"): - getloadavg = os.getloadavg + return os.getloadavg() else: - getloadavg = _psplatform.getloadavg - - __all__.append("getloadavg") + return _psplatform.getloadavg() # Windows # ===================================================================== diff --git a/tests/__init__.py b/tests/__init__.py index 77ef37d9e0..2c3afb26cc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -186,7 +186,6 @@ def macos_version(): # --- support -HAS_GETLOADAVG = hasattr(psutil, "getloadavg") HAS_HEAP_INFO = hasattr(psutil, "heap_info") HAS_NET_CONNECTIONS_UNIX = POSIX and not SUNOS HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") @@ -1269,6 +1268,7 @@ class system_namespace: ('disk_partitions', (), {'all': False}), ('disk_partitions', (), {'all': True}), ('disk_usage', (os.getcwd(),), {}), + ('getloadavg', (), {}), ('net_connections', (), {'kind': 'all'}), ('net_if_addrs', (), {}), ('net_if_stats', (), {}), @@ -1284,8 +1284,6 @@ class system_namespace: if HAS_CPU_FREQ: getters += [('cpu_freq', (), {'percpu': False})] getters += [('cpu_freq', (), {'percpu': True})] - if HAS_GETLOADAVG: - getters += [('getloadavg', (), {})] if HAS_SENSORS_TEMPERATURES: getters += [('sensors_temperatures', (), {})] if HAS_SENSORS_FANS: diff --git a/tests/test_linux.py b/tests/test_linux.py index 26746daa49..3e8ce9e49c 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -29,7 +29,6 @@ from . import GLOBAL_TIMEOUT from . import HAS_BATTERY from . import HAS_CPU_FREQ -from . import HAS_GETLOADAVG from . import HAS_PROC_RLIMIT from . import RISCV64 from . import TOLERANCE_DISK_USAGE @@ -896,7 +895,6 @@ def test_interrupts(self): @pytest.mark.skipif(not LINUX, reason="LINUX only") class TestLoadAvg(PsutilTestCase): - @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") def test_getloadavg(self): psutil_value = psutil.getloadavg() with open("/proc/loadavg") as f: diff --git a/tests/test_system.py b/tests/test_system.py index d756b22fd1..492165b896 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -38,7 +38,6 @@ from . import GLOBAL_TIMEOUT from . import HAS_BATTERY from . import HAS_CPU_FREQ -from . import HAS_GETLOADAVG from . import HAS_HEAP_INFO from . import HAS_NET_IO_COUNTERS from . import HAS_SENSORS_BATTERY @@ -647,7 +646,6 @@ def check_ls(ls): if LINUX: assert len(ls) == psutil.cpu_count() - @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") def test_getloadavg(self): loadavg = psutil.getloadavg() assert len(loadavg) == 3 From acf958789f77eb988b759ecff1c5d482606ecf05 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 9 Mar 2026 16:17:05 +0100 Subject: [PATCH 1572/1714] Tests: introduce {Platform}TestCase base class to avoid repeating skipif decorators --- psutil/__init__.py | 2 +- tests/test_linux.py | 79 ++++++++++++++++++--------------------------- tests/test_osx.py | 9 ++++-- tests/test_posix.py | 12 ++++--- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index acf6bcb69c..b654050d06 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -125,7 +125,7 @@ "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", "getloadavg", # "cpu_freq", "net_io_counters", "net_connections", "net_if_addrs", # network "net_if_stats", "disk_io_counters", "disk_partitions", "disk_usage", # disk diff --git a/tests/test_linux.py b/tests/test_linux.py index 3e8ce9e49c..1b18c68038 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -57,6 +57,13 @@ SIOCGIFBRDADDR = 0x8919 if LINUX: SECTOR_SIZE = 512 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class LinuxTestCase(PsutilTestCase): + pass + + # ===================================================================== # --- utils # ===================================================================== @@ -223,8 +230,7 @@ def open_mock(name, *args, **kwargs): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemVirtualMemoryAgainstFree(PsutilTestCase): +class TestSystemVirtualMemoryAgainstFree(LinuxTestCase): def test_total(self): cli_value = free_physmem().total psutil_value = psutil.virtual_memory().total @@ -274,8 +280,7 @@ def test_available(self): assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemVirtualMemoryAgainstVmstat(PsutilTestCase): +class TestSystemVirtualMemoryAgainstVmstat(LinuxTestCase): def test_total(self): vmstat_value = vmstat('total memory') * 1024 psutil_value = psutil.virtual_memory().total @@ -320,8 +325,7 @@ def test_inactive(self): assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemVirtualMemoryMocks(PsutilTestCase): +class TestSystemVirtualMemoryMocks(LinuxTestCase): 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 @@ -527,8 +531,7 @@ def test_virtual_memory_mocked(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemSwapMemory(PsutilTestCase): +class TestSystemSwapMemory(LinuxTestCase): @staticmethod def meminfo_has_swap_info(): """Return True if /proc/meminfo provides swap metrics.""" @@ -616,8 +619,7 @@ def test_emulate_meminfo_has_no_metrics(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemCPUCountLogical(PsutilTestCase): +class TestSystemCPUCountLogical(LinuxTestCase): @pytest.mark.skipif( not os.path.exists("/sys/devices/system/cpu/online"), reason="/sys/devices/system/cpu/online does not exist", @@ -690,8 +692,7 @@ def test_emulate_fallbacks(self): assert m.called -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemCPUCountCores(PsutilTestCase): +class TestSystemCPUCountCores(LinuxTestCase): @pytest.mark.skipif( not shutil.which("lscpu"), reason="lscpu utility not available" ) @@ -723,8 +724,7 @@ def test_emulate_none(self): assert m2.called -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemCPUFrequency(PsutilTestCase): +class TestSystemCPUFrequency(LinuxTestCase): @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") @pytest.mark.skipif( AARCH64, reason="aarch64 does not always expose frequency" @@ -878,8 +878,7 @@ def open_mock(name, *args, **kwargs): assert freq.current == 200 -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemCPUStats(PsutilTestCase): +class TestSystemCPUStats(LinuxTestCase): # XXX: fails too often. # def test_ctx_switches(self): @@ -893,8 +892,7 @@ def test_interrupts(self): assert abs(vmstat_value - psutil_value) < 500 -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestLoadAvg(PsutilTestCase): +class TestLoadAvg(LinuxTestCase): def test_getloadavg(self): psutil_value = psutil.getloadavg() with open("/proc/loadavg") as f: @@ -910,8 +908,7 @@ def test_getloadavg(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemNetIfAddrs(PsutilTestCase): +class TestSystemNetIfAddrs(LinuxTestCase): def test_ips(self): for name, addrs in psutil.net_if_addrs().items(): for addr in addrs: @@ -950,8 +947,7 @@ def test_ips(self): # assert len(nics) == found -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemNetIfStats(PsutilTestCase): +class TestSystemNetIfStats(LinuxTestCase): @pytest.mark.skipif( not shutil.which("ifconfig"), reason="ifconfig utility not available" ) @@ -1005,8 +1001,7 @@ def test_flags(self): return pytest.fail("no matches were found") -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemNetIOCounters(PsutilTestCase): +class TestSystemNetIOCounters(LinuxTestCase): @pytest.mark.skipif( not shutil.which("ifconfig"), reason="ifconfig utility not available" ) @@ -1057,8 +1052,7 @@ def ifconfig(nic): assert abs(stats.dropout - ifconfig_ret['dropout']) < 10 -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemNetConnections(PsutilTestCase): +class TestSystemNetConnections(LinuxTestCase): @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): @@ -1087,8 +1081,7 @@ def test_emulate_unix(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemDiskPartitions(PsutilTestCase): +class TestSystemDiskPartitions(LinuxTestCase): @pytest.mark.skipif( not hasattr(os, 'statvfs'), reason="os.statvfs() not available" ) @@ -1151,8 +1144,7 @@ def test_emulate_realpath_fail(self): psutil.PROCFS_PATH = "/proc" -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSystemDiskIoCounters(PsutilTestCase): +class TestSystemDiskIoCounters(LinuxTestCase): def test_emulate_kernel_2_4(self): # Tests /proc/diskstats parsing format for 2.4 kernels, see: # https://github.com/giampaolo/psutil/issues/767 @@ -1287,8 +1279,7 @@ def exists(path): psutil.disk_io_counters() -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestRootFsDeviceFinder(PsutilTestCase): +class TestRootFsDeviceFinder(LinuxTestCase): def setUp(self): dev = os.stat("/").st_dev self.major = os.major(dev) @@ -1356,8 +1347,7 @@ def test_disk_partitions_mocked(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestMisc(PsutilTestCase): +class TestMisc(LinuxTestCase): def test_boot_time(self): vmstat_value = vmstat('boot time') psutil_value = psutil.boot_time() @@ -1539,9 +1529,8 @@ def test_pid_exists_no_proc_status(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") -class TestSensorsBattery(PsutilTestCase): +class TestSensorsBattery(LinuxTestCase): @pytest.mark.skipif( not shutil.which("acpi"), reason="acpi utility not available" ) @@ -1686,8 +1675,7 @@ def test_emulate_no_power(self): assert psutil.sensors_battery().power_plugged is None -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSensorsBatteryEmulated(PsutilTestCase): +class TestSensorsBatteryEmulated(LinuxTestCase): def test_it(self): def open_mock(name, *args, **kwargs): if name.endswith("/energy_now"): @@ -1707,8 +1695,7 @@ def open_mock(name, *args, **kwargs): assert mopen.called -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSensorsTemperatures(PsutilTestCase): +class TestSensorsTemperatures(LinuxTestCase): def test_emulate_class_hwmon(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): @@ -1774,8 +1761,7 @@ def glob_mock(path): assert temp.critical == 50.0 -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestSensorsFans(PsutilTestCase): +class TestSensorsFans(LinuxTestCase): def test_emulate_data(self): def open_mock(name, *args, **kwargs): if name.endswith('/name'): @@ -1802,8 +1788,7 @@ def open_mock(name, *args, **kwargs): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestProcess(PsutilTestCase): +class TestProcess(LinuxTestCase): @retry_on_failure() def test_parse_smaps_vs_memory_maps(self): sproc = self.spawn_subproc() @@ -2190,8 +2175,7 @@ def test_memory_info_ex(self): assert mem.rss == mem.rss_anon + mem.rss_file + mem.rss_shmem -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestProcessAgainstStatus(PsutilTestCase): +class TestProcessAgainstStatus(LinuxTestCase): """/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 @@ -2273,8 +2257,7 @@ def test_cpu_affinity_eligible_cpus(self): # ===================================================================== -@pytest.mark.skipif(not LINUX, reason="LINUX only") -class TestUtils(PsutilTestCase): +class TestUtils(LinuxTestCase): def test_readlink(self): with mock.patch("os.readlink", return_value="foo (deleted)") as m: assert psutil._psplatform.readlink("bar") == "foo" diff --git a/tests/test_osx.py b/tests/test_osx.py index e7a6604943..16e5589799 100755 --- a/tests/test_osx.py +++ b/tests/test_osx.py @@ -53,7 +53,11 @@ def vm_stat(field): @pytest.mark.skipif(not MACOS, reason="MACOS only") -class TestProcess(PsutilTestCase): +class MacosTestCase(PsutilTestCase): + pass + + +class TestProcess(MacosTestCase): @classmethod def setUpClass(cls): cls.pid = spawn_subproc().pid @@ -74,8 +78,7 @@ def test_process_create_time(self): assert year == time.strftime("%Y", time.localtime(start_psutil)) -@pytest.mark.skipif(not MACOS, reason="MACOS only") -class TestSystemAPIs(PsutilTestCase): +class TestSystemAPIs(MacosTestCase): # --- disk diff --git a/tests/test_posix.py b/tests/test_posix.py index efc4851e3e..acc3eb87e5 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -144,7 +144,11 @@ def df(device): @pytest.mark.skipif(not POSIX, reason="POSIX only") -class TestProcess(PsutilTestCase): +class PosixTestCase(PsutilTestCase): + pass + + +class TestProcess(PosixTestCase): """Compare psutil results against 'ps' command line utility (mainly).""" @classmethod @@ -371,8 +375,7 @@ def test_memory_peak_rss(self): assert rss_diff <= mem.peak_rss * 0.05 -@pytest.mark.skipif(not POSIX, reason="POSIX only") -class TestSystemAPIs(PsutilTestCase): +class TestSystemAPIs(PosixTestCase): """Test some system APIs.""" @retry_on_failure() @@ -517,8 +520,7 @@ def test_disk_usage(self): assert abs(usage.percent - sys_percent) <= 1 -@pytest.mark.skipif(not POSIX, reason="POSIX only") -class TestMisc(PsutilTestCase): +class TestMisc(PosixTestCase): def test_getpagesize(self): pagesize = psutil._psplatform.cext.getpagesize() assert pagesize > 0 From 0fb85fe5537e0d015015cad209db4c48064d92c2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 9 Mar 2026 23:05:38 +0100 Subject: [PATCH 1573/1714] Add type hints (#2756) Add inline type hints to all public APIs in psutil/__init__.py. Type checkers (mypy, pyright, etc.) can now statically verify code that uses psutil. No runtime behavior is changed; the annotations are purely informational. Fixes #1946. Continuation of #2751. Note: with this we implicitly drop support for Python 3.6, which is part of the upcoming 8.0.0 breaking release. --- HISTORY.rst | 6 + MANIFEST.in | 1 + Makefile | 3 + README.rst | 2 +- docs/index.rst | 3 +- psutil/__init__.py | 238 +++++++++++++++-------- psutil/_ntuples.py | 6 + psutil/_psosx.py | 2 +- psutil/_pswindows.py | 26 +-- scripts/internal/print_announce.py | 2 +- setup.py | 2 +- tests/__init__.py | 216 ++++++++++++++++----- tests/test_contracts.py | 50 ----- tests/test_process_all.py | 24 ++- tests/test_type_hints.py | 292 +++++++++++++++++++++++++++++ tests/test_windows.py | 3 +- 16 files changed, 663 insertions(+), 213 deletions(-) create mode 100755 tests/test_type_hints.py diff --git a/HISTORY.rst b/HISTORY.rst index bf3a63fb72..6416a49d77 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,10 @@ **Enhancements** +- 1946_: add inline type hints to all public APIs in `psutil/__init__.py`. + Type checkers (mypy, pyright, etc.) can now statically verify code that + uses psutil. No runtime behavior is changed; the annotations are purely + informational. - 2729_: New `Process.page_faults()`_ method, returning a ``(minor, major)`` namedtuple. - 2745_: Drastically improve `virtual_memory()`_ docstring, which is now more @@ -76,6 +80,8 @@ Changes that break backwards compatibility. +- Dropped support for Python 3.6. + Named tuples: - `cpu_times()`_: diff --git a/MANIFEST.in b/MANIFEST.in index 337788531a..a0cb105d76 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -204,6 +204,7 @@ include tests/test_sudo.py include tests/test_sunos.py include tests/test_system.py include tests/test_testutils.py +include tests/test_type_hints.py include tests/test_unicode.py include tests/test_windows.py recursive-exclude docs/_static * diff --git a/Makefile b/Makefile index f0aada8b8c..de25dbf846 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,9 @@ test-unicode: ## Test APIs dealing with strings. test-contracts: ## APIs sanity tests. $(RUN_TEST) tests/test_contracts.py $(ARGS) +test-type-hints: ## Test type hints + $(RUN_TEST) tests/test_type_hints.py $(ARGS) + test-connections: ## Test psutil.net_connections() and Process.net_connections(). $(RUN_TEST) -k "test_connections.py or net_" $(ARGS) diff --git a/README.rst b/README.rst index 72bc447435..c6de10976b 100644 --- a/README.rst +++ b/README.rst @@ -87,7 +87,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are cPython 3.6+ and `PyPy `__. +Supported Python versions are cPython 3.7+ and `PyPy `__. Latest psutil version supporting Python 2.7 is `psutil 6.1.1 `__. diff --git a/docs/index.rst b/docs/index.rst index 4124e4da23..0ad2f8c3b9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are cPython 3.6+ and `PyPy `__. +Supported Python versions are cPython 3.7+ and `PyPy `__. Latest psutil version supporting Python 2.7 is `psutil 6.1.1 `__. @@ -3127,6 +3127,7 @@ Platforms support history * psutil 7.1.2 (2025-10): publish wheels for free-threaded Python * psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (**Linux** and **Windows**) +* psutil 8.0.0 (XXXX-XX): drop Python 3.6 * psutil 7.1.1 (2025-10): drop **SunOS 10** * psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 diff --git a/psutil/__init__.py b/psutil/__init__.py index b654050d06..0f72cfe1fc 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -15,9 +15,11 @@ - Sun Solaris - AIX -Supported Python versions are cPython 3.6+ and PyPy. +Supported Python versions are cPython 3.7+ and PyPy. """ +from __future__ import annotations + import collections import contextlib import datetime @@ -30,6 +32,7 @@ import threading import time import warnings +from typing import TYPE_CHECKING as _TYPE_CHECKING try: import pwd @@ -62,6 +65,48 @@ from ._enums import NicDuplex from ._enums import ProcessStatus +if _TYPE_CHECKING: + from typing import Any + from typing import Callable + from typing import Generator + from typing import Iterator + + from ._ntuples import pconn + from ._ntuples import pcputimes + from ._ntuples import pctxsw + from ._ntuples import pfootprint + from ._ntuples import pfullmem + from ._ntuples import pgids + from ._ntuples import pheap + from ._ntuples import pio + from ._ntuples import pionice + from ._ntuples import pmem + from ._ntuples import pmem_ex + from ._ntuples import pmmap_ext + from ._ntuples import pmmap_grouped + from ._ntuples import popenfile + from ._ntuples import ppagefaults + from ._ntuples import pthread + from ._ntuples import puids + from ._ntuples import sbattery + from ._ntuples import sconn + from ._ntuples import scpufreq + from ._ntuples import scpustats + from ._ntuples import scputimes + from ._ntuples import sdiskio + from ._ntuples import sdiskpart + from ._ntuples import sdiskusage + from ._ntuples import sfan + from ._ntuples import shwtemp + from ._ntuples import snetio + from ._ntuples import snicaddr + from ._ntuples import snicstats + from ._ntuples import sswap + from ._ntuples import suser + from ._ntuples import svmem + from ._pswindows import WindowsService + + if LINUX: # This is public API and it will be retrieved from _pslinux.py # via sys.modules. @@ -271,7 +316,7 @@ class Process: is_running() before querying the process. """ - def __init__(self, pid=None): + def __init__(self, pid: int | None = None) -> None: self._init(pid) def _init(self, pid, _ignore_nsp=False): @@ -430,14 +475,14 @@ def _raise_if_pid_reused(self): raise NoSuchProcess(self.pid, self._name, msg=msg) @property - def pid(self): + def pid(self) -> int: """The process PID.""" return self._pid # --- utility methods @contextlib.contextmanager - def oneshot(self): + def oneshot(self) -> Generator[None, None, None]: """Utility context manager which considerably speeds up the retrieval of multiple process information at the same time. @@ -503,7 +548,9 @@ def oneshot(self): self.uids.cache_deactivate(self) self._proc.oneshot_exit() - def as_dict(self, attrs=None, ad_value=None): + def as_dict( + self, attrs: list[str] | None = None, ad_value: Any = None + ) -> dict[str, Any]: """Utility method returning process information as a hashable dictionary. If *attrs* is specified it must be a list of strings @@ -550,7 +597,7 @@ def as_dict(self, attrs=None, ad_value=None): retdict[name] = ret return retdict - def parent(self): + def parent(self) -> Process | None: """Return the parent process as a Process object pre-emptively checking whether PID has been reused. If no parent is known return None. @@ -572,7 +619,7 @@ def parent(self): except NoSuchProcess: pass - def parents(self): + def parents(self) -> list[Process]: """Return the parents of this process as a list of Process instances. If no parents are known return an empty list. """ @@ -583,7 +630,7 @@ def parents(self): proc = proc.parent() return parents - def is_running(self): + def is_running(self) -> bool: """Return whether this process is running. It also checks if PID has been reused by another process, in @@ -613,7 +660,7 @@ def is_running(self): # --- actual API @memoize_when_activated - def ppid(self): + def ppid(self) -> int: """The process parent PID. On Windows the return value is cached after first call. """ @@ -631,7 +678,7 @@ def ppid(self): self._ppid = self._ppid or self._proc.ppid() return self._ppid - def name(self): + def name(self) -> str: """The process name. The return value is cached after first call.""" # Process name is only cached on Windows as on POSIX it may # change, see: @@ -662,7 +709,7 @@ def name(self): self._proc._name = name return name - def exe(self): + def exe(self) -> str: """The process executable as an absolute path. May also be an empty string. The return value is cached after first call. @@ -704,18 +751,18 @@ def guess_it(fallback): self._exe = exe return self._exe - def cmdline(self): + def cmdline(self) -> list[str]: """The command line this process has been called with.""" return self._proc.cmdline() - def status(self): + def status(self) -> ProcessStatus | str: """The process current status as a STATUS_* constant.""" try: return self._proc.status() except ZombieProcess: return ProcessStatus.STATUS_ZOMBIE - def username(self): + def username(self) -> str: """The name of the user that owns the process. On UNIX this is calculated by using *real* process uid. """ @@ -733,7 +780,7 @@ def username(self): else: return self._proc.username() - def create_time(self): + def create_time(self) -> float: """The process creation time as a floating point number expressed in seconds since the epoch (seconds since January 1, 1970, at midnight UTC). The return value, which is cached after @@ -745,11 +792,11 @@ def create_time(self): self._create_time = self._proc.create_time() return self._create_time - def cwd(self): + def cwd(self) -> str: """Process current working directory as an absolute path.""" return self._proc.cwd() - def nice(self, value=None): + def nice(self, value: int | None = None) -> int | None: """Get or set process niceness (priority).""" if value is None: return self._proc.nice_get() @@ -760,25 +807,25 @@ def nice(self, value=None): if POSIX: @memoize_when_activated - def uids(self): + def uids(self) -> puids: """Return process UIDs as a (real, effective, saved) namedtuple. """ return self._proc.uids() - def gids(self): + def gids(self) -> pgids: """Return process GIDs as a (real, effective, saved) namedtuple. """ return self._proc.gids() - def terminal(self): + def terminal(self) -> str | None: """The terminal associated with this process, if any, else None. """ return self._proc.terminal() - def num_fds(self): + def num_fds(self) -> int: """Return the number of file descriptors opened by this process (POSIX only). """ @@ -787,7 +834,7 @@ def num_fds(self): # Linux, BSD, AIX and Windows only if hasattr(_psplatform.Process, "io_counters"): - def io_counters(self): + def io_counters(self) -> pio: """Return process I/O statistics as a (read_count, write_count, read_bytes, write_bytes) namedtuple. @@ -799,7 +846,9 @@ def io_counters(self): # Linux and Windows if hasattr(_psplatform.Process, "ionice_get"): - def ionice(self, ioclass=None, value=None): + def ionice( + self, ioclass: int | None = None, value: int | None = None + ) -> pionice | ProcessIOPriority | None: """Get or set process I/O niceness (priority). On Linux *ioclass* is one of the IOPRIO_CLASS_* constants. @@ -823,7 +872,11 @@ def ionice(self, ioclass=None, value=None): # Linux / FreeBSD only if hasattr(_psplatform.Process, "rlimit"): - def rlimit(self, resource, limits=None): + def rlimit( + self, + resource: int, + limits: tuple[int, int] | None = None, + ) -> tuple[int, int] | None: """Get or set process resource limits as a (soft, hard) tuple. @@ -840,7 +893,9 @@ def rlimit(self, resource, limits=None): # Windows, Linux and FreeBSD only if hasattr(_psplatform.Process, "cpu_affinity_get"): - def cpu_affinity(self, cpus=None): + def cpu_affinity( + self, cpus: list[int] | None = None + ) -> list[int] | None: """Get or set process CPU affinity. If specified, *cpus* must be a list of CPUs for which you want to set the affinity (e.g. [0, 1]). @@ -862,7 +917,7 @@ def cpu_affinity(self, cpus=None): # Linux, FreeBSD, SunOS if hasattr(_psplatform.Process, "cpu_num"): - def cpu_num(self): + def cpu_num(self) -> int: """Return what CPU this process is currently running on. The returned number should be <= psutil.cpu_count() and <= len(psutil.cpu_percent(percpu=True)). @@ -875,7 +930,7 @@ def cpu_num(self): # All platforms has it, but maybe not in the future. if hasattr(_psplatform.Process, "environ"): - def environ(self): + def environ(self) -> dict[str, str]: """The environment variables of the process as a dict. Note: this might not reflect changes made after the process started. """ @@ -883,25 +938,25 @@ def environ(self): if WINDOWS: - def num_handles(self): + def num_handles(self) -> int: """Return the number of handles opened by this process (Windows only). """ return self._proc.num_handles() - def num_ctx_switches(self): + def num_ctx_switches(self) -> pctxsw: """Return the number of voluntary and involuntary context switches performed by this process. """ return self._proc.num_ctx_switches() - def num_threads(self): + def num_threads(self) -> int: """Return the number of threads used by this process.""" return self._proc.num_threads() if hasattr(_psplatform.Process, "threads"): - def threads(self): + def threads(self) -> list[pthread]: """Return threads opened by process as a list of (id, user_time, system_time) namedtuples representing thread id and thread CPU times (user/system). @@ -909,7 +964,7 @@ def threads(self): """ return self._proc.threads() - def children(self, recursive=False): + def children(self, recursive: bool = False) -> list[Process]: """Return the children of this process as a list of Process instances, pre-emptively checking whether PID has been reused. If *recursive* is True return all the parent descendants. @@ -983,7 +1038,7 @@ def children(self, recursive=False): pass return ret - def cpu_percent(self, interval=None): + def cpu_percent(self, interval: float | None = None) -> float: """Return a float representing the current process CPU utilization as a percentage. @@ -1077,7 +1132,7 @@ def timer(): return round(single_cpu_percent, 1) @memoize_when_activated - def cpu_times(self): + def cpu_times(self) -> pcputimes: """Return a (user, system, children_user, children_system) namedtuple representing the accumulated process time, in seconds. @@ -1088,7 +1143,7 @@ def cpu_times(self): return self._proc.cpu_times() @memoize_when_activated - def memory_info(self): + def memory_info(self) -> pmem: """Return a namedtuple with variable fields depending on the platform, representing memory information about the process. @@ -1098,7 +1153,8 @@ def memory_info(self): """ return self._proc.memory_info() - def memory_info_ex(self): + @memoize_when_activated + def memory_info_ex(self) -> pmem_ex: """Return a namedtuple extending memory_info() with extra metrics. @@ -1113,7 +1169,7 @@ def memory_info_ex(self): # Linux, macOS, Windows if hasattr(_psplatform.Process, "memory_footprint"): - def memory_footprint(self): + def memory_footprint(self) -> pfootprint: """Return a named tuple with USS, PSS and swap memory metrics. These provide a better representation of actual process memory usage. @@ -1129,7 +1185,7 @@ def memory_footprint(self): return self._proc.memory_footprint() # DEPRECATED - def memory_full_info(self): + def memory_full_info(self) -> pfullmem: """Return the same information as memory_info() plus memory_footprint() in a single named tuple. @@ -1145,7 +1201,7 @@ def memory_full_info(self): return _ntp.pfullmem(*basic_mem + fp) return _ntp.pfullmem(*basic_mem) - def memory_percent(self, memtype="rss"): + def memory_percent(self, memtype: str = "rss") -> float: """Compare process memory to total physical system memory and calculate process memory utilization as a percentage. *memtype* argument is a string that dictates what type of @@ -1194,7 +1250,9 @@ def memory_percent(self, memtype="rss"): if hasattr(_psplatform.Process, "memory_maps"): - def memory_maps(self, grouped=True): + def memory_maps( + self, grouped: bool = True + ) -> list[pmmap_grouped] | list[pmmap_ext]: """Return process' mapped memory regions as a list of namedtuples whose fields are variable depending on the platform. @@ -1219,7 +1277,7 @@ def memory_maps(self, grouped=True): else: return [_ntp.pmmap_ext(*x) for x in it] - def page_faults(self): + def page_faults(self) -> ppagefaults: """Return the number of page faults for this process as a (minor, major) namedtuple. @@ -1237,14 +1295,14 @@ def page_faults(self): """ return self._proc.page_faults() - def open_files(self): + def open_files(self) -> list[popenfile]: """Return files opened by process as a list of (path, fd) namedtuples including the absolute file name and file descriptor number. """ return self._proc.open_files() - def net_connections(self, kind='inet'): + def net_connections(self, kind: str = "inet") -> list[pconn]: """Return socket connections opened by process as a list of (fd, family, type, laddr, raddr, status) namedtuples. The *kind* parameter filters for connections that match the @@ -1270,7 +1328,7 @@ def net_connections(self, kind='inet'): return self._proc.net_connections(kind) @_common.deprecated_method(replacement="net_connections") - def connections(self, kind="inet"): + def connections(self, kind="inet") -> list[pconn]: return self.net_connections(kind=kind) # --- signals @@ -1302,7 +1360,7 @@ def _send_signal(self, sig): except PermissionError as err: raise AccessDenied(pid, name) from err - def send_signal(self, sig): + def send_signal(self, sig: int) -> None: """Send a signal *sig* to process pre-emptively checking whether PID has been reused (see signal module constants) . On Windows only SIGTERM is valid and is treated as an alias @@ -1317,7 +1375,7 @@ def send_signal(self, sig): raise NoSuchProcess(self.pid, self._name, msg=msg) self._proc.send_signal(sig) - def suspend(self): + def suspend(self) -> None: """Suspend process execution with SIGSTOP pre-emptively checking whether PID has been reused. On Windows this has the effect of suspending all process threads. @@ -1328,7 +1386,7 @@ def suspend(self): self._raise_if_pid_reused() self._proc.suspend() - def resume(self): + def resume(self) -> None: """Resume process execution with SIGCONT pre-emptively checking whether PID has been reused. On Windows this has the effect of resuming all process threads. @@ -1339,7 +1397,7 @@ def resume(self): self._raise_if_pid_reused() self._proc.resume() - def terminate(self): + def terminate(self) -> None: """Terminate the process with SIGTERM pre-emptively checking whether PID has been reused. On Windows this is an alias for kill(). @@ -1350,7 +1408,7 @@ def terminate(self): self._raise_if_pid_reused() self._proc.kill() - def kill(self): + def kill(self) -> None: """Kill the current process with SIGKILL pre-emptively checking whether PID has been reused. """ @@ -1360,7 +1418,7 @@ def kill(self): self._raise_if_pid_reused() self._proc.kill() - def wait(self, timeout=None): + def wait(self, timeout: float | None = None) -> int | 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 @@ -1460,7 +1518,7 @@ def __init__(self, *args, **kwargs): def __dir__(self): return sorted(set(dir(Popen) + dir(subprocess.Popen))) - def __enter__(self): + def __enter__(self) -> Popen: if hasattr(self.__subproc, '__enter__'): self.__subproc.__enter__() return self @@ -1491,7 +1549,7 @@ def __getattribute__(self, name): msg = f"{self.__class__!r} has no attribute {name!r}" raise AttributeError(msg) from None - def wait(self, timeout=None): + def wait(self, timeout: float | None = None) -> int | None: if self.__subproc.returncode is not None: return self.__subproc.returncode ret = super().wait(timeout) @@ -1504,7 +1562,7 @@ def wait(self, timeout=None): # ===================================================================== -def pids(): +def pids() -> list[int]: """Return a list of current running PIDs.""" global _LOWEST_PID ret = sorted(_psplatform.pids()) @@ -1512,7 +1570,7 @@ def pids(): return ret -def pid_exists(pid): +def pid_exists(pid: int) -> bool: """Return True if given PID exists in the current process list. This is faster than doing "pid in psutil.pids()" and should be preferred. @@ -1534,7 +1592,9 @@ def pid_exists(pid): _pids_reused = set() -def process_iter(attrs=None, ad_value=None): +def process_iter( + attrs: list[str] | None = None, ad_value: Any = None +) -> Iterator[Process]: """Return a generator yielding a Process instance for all running processes. @@ -1592,7 +1652,11 @@ def remove(pid): process_iter.cache_clear.__doc__ = "Clear process_iter() internal cache." -def wait_procs(procs, timeout=None, callback=None): +def wait_procs( + procs: list[Process], + timeout: float | None = None, + callback: Callable[[Process], None] | None = None, +) -> tuple[list[Process], list[Process]]: """Convenience function which waits for a list of processes to terminate. @@ -1689,7 +1753,7 @@ def check_gone(proc, timeout): # ===================================================================== -def cpu_count(logical=True): +def cpu_count(logical: bool = True) -> int | None: """Return the number of logical CPUs in the system (same as os.cpu_count()). @@ -1712,7 +1776,7 @@ def cpu_count(logical=True): return ret -def cpu_times(percpu=False): +def cpu_times(percpu: bool = False) -> scputimes | list[scputimes]: """Return system-wide CPU times as a namedtuple. Every CPU time represents the seconds the CPU has spent in the given mode. The namedtuple's fields availability varies depending on the @@ -1808,7 +1872,9 @@ def _cpu_times_deltas(t1, t2): return _ntp.scputimes(*field_deltas) -def cpu_percent(interval=None, percpu=False): +def cpu_percent( + interval: float | None = None, percpu: bool = False +) -> float | list[float]: """Return a float representing the current system-wide CPU utilization as a percentage. @@ -1890,7 +1956,9 @@ def calculate(t1, t2): _last_per_cpu_times_2 = _last_per_cpu_times.copy() -def cpu_times_percent(interval=None, percpu=False): +def cpu_times_percent( + interval: float | None = None, percpu: bool = False +) -> scputimes | list[scputimes]: """Same as cpu_percent() but provides utilization percentages for each specific CPU time as is returned by cpu_times(). For instance, on Linux we'll get: @@ -1949,14 +2017,14 @@ def calculate(t1, t2): return ret -def cpu_stats(): +def cpu_stats() -> scpustats: """Return CPU statistics.""" return _psplatform.cpu_stats() if hasattr(_psplatform, "cpu_freq"): - def cpu_freq(percpu=False): + def cpu_freq(percpu: bool = False) -> scpufreq | list[scpufreq] | None: """Return CPU frequency as a namedtuple including current, min and max frequency expressed in Mhz. @@ -1999,7 +2067,7 @@ def cpu_freq(percpu=False): __all__.append("cpu_freq") -def getloadavg(): +def getloadavg() -> tuple[float, float, float]: """Return the average system load over the last 1, 5 and 15 minutes as a tuple. On Windows this is emulated by using a Windows API that spawns a thread which keeps running in background and updates @@ -2008,7 +2076,7 @@ def getloadavg(): if hasattr(os, "getloadavg"): return os.getloadavg() else: - return _psplatform.getloadavg() # Windows + return _psplatform.getloadavg() # ===================================================================== @@ -2016,7 +2084,7 @@ def getloadavg(): # ===================================================================== -def virtual_memory(): +def virtual_memory() -> svmem: """Return statistics about system memory usage as a namedtuple including the following fields, expressed in bytes: @@ -2075,7 +2143,7 @@ def virtual_memory(): return ret -def swap_memory(): +def swap_memory() -> sswap: """Return system swap memory statistics as a namedtuple including the following fields: @@ -2096,7 +2164,7 @@ def swap_memory(): # ===================================================================== -def disk_usage(path): +def disk_usage(path: str) -> sdiskusage: """Return disk usage statistics about the given *path* as a namedtuple including total, used and free space expressed in bytes plus the percentage usage. @@ -2104,7 +2172,7 @@ def disk_usage(path): return _psplatform.disk_usage(path) -def disk_partitions(all=False): +def disk_partitions(all: bool = False) -> list[sdiskpart]: """Return mounted partitions as a list of (device, mountpoint, fstype, opts) namedtuple. 'opts' field is a raw string separated by commas indicating mount @@ -2116,7 +2184,9 @@ def disk_partitions(all=False): return _psplatform.disk_partitions(all) -def disk_io_counters(perdisk=False, nowrap=True): +def disk_io_counters( + perdisk: bool = False, nowrap: bool = True +) -> sdiskio | dict[str, sdiskio]: """Return system disk I/O statistics as a namedtuple including the following fields: @@ -2173,7 +2243,9 @@ def disk_io_counters(perdisk=False, nowrap=True): # ===================================================================== -def net_io_counters(pernic=False, nowrap=True): +def net_io_counters( + pernic: bool = False, nowrap: bool = True +) -> snetio | dict[str, snetio] | None: """Return network I/O statistics as a namedtuple including the following fields: @@ -2218,7 +2290,7 @@ def net_io_counters(pernic=False, nowrap=True): net_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" -def net_connections(kind='inet'): +def net_connections(kind: str = 'inet') -> list[sconn]: """Return system-wide socket connections as a list of (fd, family, type, laddr, raddr, status, pid) namedtuples. In case of limited privileges 'fd' and 'pid' may be set to -1 @@ -2248,7 +2320,7 @@ def net_connections(kind='inet'): return _psplatform.net_connections(kind) -def net_if_addrs(): +def net_if_addrs() -> dict[str, list[snicaddr]]: """Return the addresses associated to each NIC (network interface card) installed on the system as a dictionary whose keys are the NIC names and value is a list of namedtuples for each address @@ -2309,7 +2381,7 @@ def net_if_addrs(): return dict(ret) -def net_if_stats(): +def net_if_stats() -> dict[str, snicstats]: """Return information about each NIC (network interface card) installed on the system as a dictionary whose keys are the NIC names and value is a namedtuple with the following fields: @@ -2332,7 +2404,9 @@ def net_if_stats(): # Linux, macOS if hasattr(_psplatform, "sensors_temperatures"): - def sensors_temperatures(fahrenheit=False): + def sensors_temperatures( + fahrenheit: bool = False, + ) -> dict[str, list[shwtemp]]: """Return hardware temperatures. Each entry is a namedtuple representing a certain hardware sensor (it may be a CPU, an hard disk or something else, depending on the OS and its @@ -2370,7 +2444,7 @@ def convert(n): # Linux if hasattr(_psplatform, "sensors_fans"): - def sensors_fans(): + def sensors_fans() -> dict[str, list[sfan]]: """Return fans speed. Each entry is a namedtuple representing a certain hardware sensor. All speed are expressed in RPM (rounds per minute). @@ -2383,7 +2457,7 @@ def sensors_fans(): # Linux, Windows, FreeBSD, macOS if hasattr(_psplatform, "sensors_battery"): - def sensors_battery(): + def sensors_battery() -> sbattery | None: """Return battery information. If no battery is installed returns None. @@ -2403,7 +2477,7 @@ def sensors_battery(): # ===================================================================== -def boot_time(): +def boot_time() -> float: """Return the system boot time expressed in seconds since the epoch (seconds since January 1, 1970, at midnight UTC). The returned value is based on the system clock, which means it may be affected @@ -2413,7 +2487,7 @@ def boot_time(): return _psplatform.boot_time() -def users(): +def users() -> list[suser]: """Return users currently connected on the system as a list of namedtuples including the following fields. @@ -2433,13 +2507,13 @@ def users(): if WINDOWS: - def win_service_iter(): + def win_service_iter() -> Iterator[WindowsService]: """Return a generator yielding a WindowsService instance for all Windows services installed. """ return _psplatform.win_service_iter() - def win_service_get(name): + def win_service_get(name) -> WindowsService: """Get a Windows service by *name*. Raise NoSuchProcess if no service with such name exists. """ @@ -2454,7 +2528,7 @@ def win_service_get(name): # Linux + glibc, Windows, macOS, FreeBSD, NetBSD if hasattr(_psplatform, "heap_info"): - def heap_info(): + def heap_info() -> pheap: """Return low-level heap statistics from the C heap allocator (glibc). @@ -2470,7 +2544,7 @@ def heap_info(): """ return _ntp.pheap(*_psplatform.heap_info()) - def heap_trim(): + def heap_trim() -> None: """Request that the underlying allocator free any unused memory it's holding in the heap (typically small `malloc()` allocations). diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index e76530a4d0..bb89865657 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -498,6 +498,9 @@ class pmem(NamedTuple): stack: int peak_rss: int + # psutil.Process.memory_info_ex() + pmem_ex = pmem + # psutil.Process.memory_full_info() pfullmem = pmem @@ -508,5 +511,8 @@ class pmem(NamedTuple): rss: int vms: int + # psutil.Process.memory_info_ex() + pmem_ex = pmem + # psutil.Process.memory_full_info() pfullmem = pmem diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 2751ad4475..207f1c81a2 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -390,7 +390,7 @@ def uids(self): @wrap_exceptions def gids(self): d = self._oneshot_kinfo() - return ntp.puids(d["rgid"], d["egid"], d["sgid"]) + return ntp.pgids(d["rgid"], d["egid"], d["sgid"]) @wrap_exceptions def terminal(self): diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 0a2309a2a0..81cac78bb1 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -4,6 +4,8 @@ """Windows platform implementation.""" +from __future__ import annotations + import contextlib import enum import functools @@ -403,7 +405,7 @@ def win_service_get(name): class WindowsService: # noqa: PLW1641 """Represents an installed Windows service.""" - def __init__(self, name, display_name): + def __init__(self, name: str, display_name: str): self._name = name self._display_name = display_name @@ -414,14 +416,14 @@ def __str__(self): def __repr__(self): return f"<{self.__str__()} at {id(self)}>" - def __eq__(self, other): + def __eq__(self, other: object): # Test for equality with another WindosService object based # on name. if not isinstance(other, WindowsService): return NotImplemented return self._name == other._name - def __ne__(self, other): + def __ne__(self, other: object): return not self == other def _query_config(self): @@ -469,30 +471,30 @@ def _wrap_exceptions(self): # config query - def name(self): + def name(self) -> str: """The service name. This string is how a service is referenced and can be passed to win_service_get() to get a new WindowsService instance. """ return self._name - def display_name(self): + def display_name(self) -> str: """The service display name. The value is cached when this class is instantiated. """ return self._display_name - def binpath(self): + def binpath(self) -> str: """The fully qualified path to the service binary/exe file as a string, including command line arguments. """ return self._query_config()['binpath'] - def username(self): + def username(self) -> str: """The name of the user that owns this service.""" return self._query_config()['username'] - def start_type(self): + def start_type(self) -> str: """A string which can either be "automatic", "manual" or "disabled". """ @@ -500,23 +502,23 @@ def start_type(self): # status query - def pid(self): + def pid(self) -> int | None: """The process PID, if any, else None. This can be passed to Process class to control the service's process. """ return self._query_status()['pid'] - def status(self): + def status(self) -> str: """Service status as a string.""" return self._query_status()['status'] - def description(self): + def description(self) -> str: """Service long description.""" return cext.winservice_query_descr(self.name()) # utils - def as_dict(self): + def as_dict(self) -> dict[str, str | int | None]: """Utility method retrieving all the information above as a dictionary. """ diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 4d9bc00a76..cd984de7c3 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -48,7 +48,7 @@ line tools such as: ps, top, lsof, netstat, ifconfig, who, df, kill, free, \ nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap. It \ currently supports Linux, Windows, macOS, Sun Solaris, FreeBSD, OpenBSD, \ -NetBSD and AIX. Supported Python versions are cPython 3.6+ and PyPy. +NetBSD and AIX. Supported Python versions are cPython 3.7+ and PyPy. What's new ========== diff --git a/setup.py b/setup.py index 9603b29ab0..feac788ed2 100755 --- a/setup.py +++ b/setup.py @@ -534,7 +534,7 @@ def main(): "test": TEST_DEPS, } kwargs.update( - python_requires=">=3.6", + python_requires=">=3.7", extras_require=extras_require, zip_safe=False, ) diff --git a/tests/__init__.py b/tests/__init__.py index 2c3afb26cc..dfb0701f0b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -84,7 +84,9 @@ # test utils 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', 'retry_on_failure', 'PsutilTestCase', 'process_namespace', - 'system_namespace', 'check_ntuple_types', 'is_win_secure_system_proc', + 'system_namespace', 'is_win_secure_system_proc', + # type hints + 'check_ntuple_type_hints', 'check_fun_type_hints', # fs utils 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', # os @@ -1063,7 +1065,7 @@ def assert_in_pids(proc): def check_proc_memory(self, nt): # Check the ntuple returned by Process.memory_*() methods. - check_ntuple_types(nt) + check_ntuple_type_hints(nt) for value in nt: assert isinstance(value, int) assert value >= 0 @@ -1524,53 +1526,6 @@ def create_sockets(): safe_rmpath(fname) -@functools.lru_cache(maxsize=None) -def _get_hints(cls): - try: - localns = { - name: obj - for name, obj in vars(_enums).items() - if isinstance(obj, type) and issubclass(obj, enum.Enum) - } - localns['socket'] = socket - return typing.get_type_hints( - cls, - globalns=vars(ntuples), - localns=localns, - ) - except TypeError: - # Python < 3.10 can't evaluate "X | Y" union syntax. - return {} - - -def check_ntuple_types(nt): - """Uses type hints from _ntuples.py to verify field types. `nt` is - a named tuple returned by one of psutil APIs. - """ - assert is_namedtuple(nt) - hints = _get_hints(type(nt)) - if not hints: - return - for field in nt._fields: - if field not in hints: - # field is not annotated - continue - value = getattr(nt, field) - hint = hints[field] - if ( - hasattr(types, 'UnionType') and isinstance(hint, types.UnionType) - ) or getattr(hint, '__origin__', None) is typing.Union: - types_ = typing.get_args(hint) - elif isinstance(hint, type): - # For IntEnum hints (e.g. socket.AddressFamily), psutil may - # return a platform-specific IntEnum subclass rather than the - # annotated one, so we broaden the check to int. - types_ = (int,) if issubclass(hint, enum.IntEnum) else (hint,) - else: - continue - assert isinstance(value, types_), (field, value, types_) - - def check_net_address(addr, family): """Check a net address validity. Supported families are IPv4, IPv6 and MAC addresses. @@ -1659,7 +1614,7 @@ def check_status(conn): else: assert conn.status == psutil.CONN_NONE, conn.status - check_ntuple_types(conn) + check_ntuple_type_hints(conn) check_ntuple(conn) check_family(conn) check_type(conn) @@ -1681,6 +1636,167 @@ def filter_proc_net_connections(cons): return new +# ===================================================================== +# --- type hints +# ===================================================================== + + +class TypeHintsChecker: + try: + UNION_TYPES = (typing.Union, types.UnionType) + except AttributeError: # Python < 3.10 + UNION_TYPES = (typing.Union,) + + @staticmethod + @functools.lru_cache(maxsize=None) + def _get_ntuple_hints(nt): + cls = type(nt) + try: + localns = { + name: obj + for name, obj in vars(_enums).items() + if isinstance(obj, type) and issubclass(obj, enum.Enum) + } + localns['socket'] = socket + return typing.get_type_hints( + cls, + globalns=vars(ntuples), + localns=localns, + ) + except TypeError: + # Python < 3.10 can't evaluate "X | Y" union syntax. + return {} + + @staticmethod + def _hint_to_types(hint): + """Flatten a type hint into a tuple of concrete types suitable + for isinstance(). Returns None if the hint cannot be checked. + """ + if not hasattr(typing, "get_origin") and sys.version_info[:2] <= ( + 3, + 7, + ): + return None + origin = typing.get_origin(hint) + if origin in TypeHintsChecker.UNION_TYPES: + result = [] + for arg in typing.get_args(hint): + inner = typing.get_origin(arg) + if inner is not None: + result.append(inner) + elif isinstance(arg, type): + result.append(arg) + return tuple(result) if result else None + if origin is not None: + return (origin,) + if isinstance(hint, type): + return (hint,) + return None + + @staticmethod + def check_ntuple_type_hints(nt): + """Uses type hints from _ntuples.py to verify field types. `nt` + is a named tuple returned by one of psutil APIs. + """ + assert is_namedtuple(nt) + hints = TypeHintsChecker._get_ntuple_hints(nt) + if not hints: + return + for field in nt._fields: + if field not in hints: + # field is not annotated + continue + value = getattr(nt, field) + types_ = TypeHintsChecker._hint_to_types(hints[field]) + if types_ is None: + continue + # For IntEnum hints (e.g. socket.AddressFamily), psutil may + # return a platform-specific IntEnum subclass rather than + # the annotated one, so we broaden the check to int. + types_ = tuple( + ( + int + if isinstance(t, type) and issubclass(t, enum.IntEnum) + else t + ) + for t in types_ + ) + assert isinstance(value, types_), (field, value, types_) + + @staticmethod + @functools.lru_cache(maxsize=None) + def _get_return_hint(fun): + """Get the 'return' type hint for a psutil API function or + method. Resolves annotation strings using a combined namespace + of psutil globals (Any, Generator, Process, ...) and ntuple + types (scputimes, svmem, pmem, ...). Returns None if hints + cannot be resolved or there is no return annotation. + """ + while hasattr(fun, 'func'): + fun = fun.func + # Build a namespace that can resolve all annotations. + psp = vars(psutil).get('_psplatform') + psp_ns = vars(psp) if psp is not None else {} + ns = { + **psp_ns, + **vars(psutil), + **vars(ntuples), + **vars(typing), + } + underlying = getattr(fun, '__func__', fun) + try: + hints = typing.get_type_hints(underlying, globalns=ns) + except TypeError: + # X | Y union syntax in annotations requires Python 3.10+ + # to evaluate. On older versions skip the check entirely. + if sys.version_info < (3, 10): + msg = f"skip X|Y type check on old python for {fun.__name__!r}" + warn(msg) + return None + else: + raise + return hints.get('return') + + @staticmethod + def _check_container_items(hint, value): + """For list[T] and dict[K, V] hints, verify element types.""" + origin = typing.get_origin(hint) + args = typing.get_args(hint) + if origin is list and args: + elem_types = TypeHintsChecker._hint_to_types(args[0]) + if elem_types: + for item in value: + assert isinstance(item, elem_types), (item, elem_types) + elif origin is dict and len(args) == 2: + key_types = TypeHintsChecker._hint_to_types(args[0]) + val_types = TypeHintsChecker._hint_to_types(args[1]) + for k, v in value.items(): + if key_types: + assert isinstance(k, key_types), (k, key_types) + if val_types: + assert isinstance(v, val_types), (v, val_types) + + @staticmethod + def check_fun_type_hints(fun, retval): + """Use the 'return' type hint of *fun* from psutil/__init__.py + to verify that *retval* is an instance of the annotated type. + """ + hint = TypeHintsChecker._get_return_hint(fun) + if hint is None: + if not hasattr(types, "UnionType"): + # added in python 3.10 + return + raise ValueError(f"no type hints defined for {fun}") + types_ = TypeHintsChecker._hint_to_types(hint) + assert types_, hint + assert isinstance(retval, types_), (fun, retval, types_) + TypeHintsChecker._check_container_items(hint, retval) + + +check_ntuple_type_hints = TypeHintsChecker.check_ntuple_type_hints +check_fun_type_hints = TypeHintsChecker.check_fun_type_hints + + # =================================================================== # --- import utils # =================================================================== diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 2b0c9aeb16..276e8d098e 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -35,14 +35,11 @@ from . import HAS_SENSORS_TEMPERATURES from . import SKIP_SYSCONS from . import PsutilTestCase -from . import check_ntuple_types from . import create_sockets from . import enum from . import is_namedtuple from . import kernel_version -from . import process_namespace from . import pytest -from . import system_namespace # =================================================================== # --- APIs availability @@ -476,50 +473,3 @@ def test_users(self): assert isinstance(user.pid, (int, type(None))) if isinstance(user.pid, int): assert user.pid > 0 - - -# =================================================================== -# --- namedtuple field types -# =================================================================== - - -class TestNtupleFieldTypes(PsutilTestCase): - """Check that namedtuple field values match the type annotations - defined in psutil/_ntuples.py. - """ - - def check_result(self, ret): - if is_namedtuple(ret): - check_ntuple_types(ret) - elif isinstance(ret, list): - for item in ret: - if is_namedtuple(item): - check_ntuple_types(item) - - def test_system_ntuple_types(self): - for fun, name in system_namespace.iter(system_namespace.getters): - try: - ret = fun() - except psutil.Error: - continue - with self.subTest(name=name, fun=str(fun)): - if isinstance(ret, dict): - for v in ret.values(): - if isinstance(v, list): - for item in v: - self.check_result(item) - else: - self.check_result(v) - else: - self.check_result(ret) - - def test_process_ntuple_types(self): - p = psutil.Process() - ns = process_namespace(p) - for fun, name in ns.iter(ns.getters): - with self.subTest(name=name, fun=str(fun)): - try: - ret = fun() - except psutil.Error: - continue - self.check_result(ret) diff --git a/tests/test_process_all.py b/tests/test_process_all.py index 6b74c7037e..f6e7e653a8 100755 --- a/tests/test_process_all.py +++ b/tests/test_process_all.py @@ -16,6 +16,8 @@ import time import traceback +import pytest + import psutil from psutil import AIX from psutil import BSD @@ -32,11 +34,12 @@ from . import VALID_PROC_STATUSES from . import PsutilTestCase from . import check_connection_ntuple -from . import check_ntuple_types +from . import check_fun_type_hints +from . import check_ntuple_type_hints from . import create_sockets +from . import is_namedtuple from . import is_win_secure_system_proc from . import process_namespace -from . import pytest # Cuts the time in half, but (e.g.) on macOS the process pool stays # alive after join() (multiprocessing bug?), messing up other tests. @@ -84,10 +87,15 @@ def do_wait(): # check_exception() in case of NSP. for fun, fun_name in ns.iter(ns.getters, clear_cache=False): try: - info[fun_name] = fun() + ret = fun() except psutil.Error as exc: check_exception(exc, proc, name, ppid) continue + else: + check_fun_type_hints(fun, ret) + if is_namedtuple(ret): + check_ntuple_type_hints(ret) + info[fun_name] = ret do_wait() return info @@ -212,13 +220,11 @@ def create_time(self, ret, info): time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) def uids(self, ret, info): - check_ntuple_types(ret) for uid in ret: assert isinstance(uid, int) assert uid >= 0 def gids(self, ret, info): - check_ntuple_types(ret) # note: testing all gids as above seems not to be reliable for # gid == 30 (nodoby); not sure why. for gid in ret: @@ -238,7 +244,6 @@ def status(self, ret, info): assert ret in VALID_PROC_STATUSES def io_counters(self, ret, info): - check_ntuple_types(ret) for field in ret: assert isinstance(field, int) if field != -1: @@ -271,7 +276,6 @@ def num_threads(self, ret, info): def threads(self, ret, info): assert isinstance(ret, list) for t in ret: - check_ntuple_types(t) assert t.id >= 0 assert t.user_time >= 0 assert t.system_time >= 0 @@ -279,7 +283,6 @@ def threads(self, ret, info): assert isinstance(field, (int, float)) def cpu_times(self, ret, info): - check_ntuple_types(ret) for n in ret: assert isinstance(n, float) assert n >= 0 @@ -305,7 +308,6 @@ def memory_info_ex(self, ret, info): self.check_proc_memory(ret) def memory_footprint(self, ret, info): - check_ntuple_types(ret) for name in ret._fields: value = getattr(ret, name) assert isinstance(value, int) @@ -314,7 +316,6 @@ def memory_footprint(self, ret, info): def open_files(self, ret, info): assert isinstance(ret, list) for f in ret: - check_ntuple_types(f) assert isinstance(f.fd, int) assert isinstance(f.path, str) assert f.path.strip() == f.path @@ -387,7 +388,6 @@ def terminal(self, ret, info): def memory_maps(self, ret, info): for nt in ret: - check_ntuple_types(nt) if hasattr(nt, "addr"): assert isinstance(nt.addr, str) if hasattr(nt, "perms"): @@ -418,7 +418,6 @@ def num_handles(self, ret, info): assert ret >= 0 def page_faults(self, ret, info): - check_ntuple_types(ret) assert isinstance(ret.minor, int) assert isinstance(ret.major, int) assert ret.minor >= 0 @@ -438,7 +437,6 @@ def nice(self, ret, info): assert isinstance(ret, enum.IntEnum) def num_ctx_switches(self, ret, info): - check_ntuple_types(ret) for value in ret: assert isinstance(value, int) assert value >= 0 diff --git a/tests/test_type_hints.py b/tests/test_type_hints.py new file mode 100755 index 0000000000..b199809b8b --- /dev/null +++ b/tests/test_type_hints.py @@ -0,0 +1,292 @@ +#!/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. + +from __future__ import annotations + +import functools +import sys +import types + +import pytest + +import psutil + +from . import PsutilTestCase +from . import check_fun_type_hints +from . import check_ntuple_type_hints +from . import is_namedtuple +from . import process_namespace +from . import system_namespace + + +@pytest.mark.skipif( + sys.version_info[:2] <= (3, 7), reason="not supported on Python <= 3.7" +) +class TypeHintTestCase(PsutilTestCase): + pass + + +# =================================================================== +# --- named tuples type hints +# =================================================================== + + +class TestTypeHintsNtuples(TypeHintTestCase): + """Check that namedtuple field values match the type annotations + defined in psutil/_ntuples.py. + """ + + def check_result(self, ret): + if is_namedtuple(ret): + check_ntuple_type_hints(ret) + elif isinstance(ret, list): + for item in ret: + if is_namedtuple(item): + check_ntuple_type_hints(item) + + def test_system_ntuple_types(self): + for fun, name in system_namespace.iter(system_namespace.getters): + try: + ret = fun() + except psutil.Error: + continue + with self.subTest(name=name, fun=str(fun)): + if isinstance(ret, dict): + for v in ret.values(): + if isinstance(v, list): + for item in v: + self.check_result(item) + else: + self.check_result(v) + else: + self.check_result(ret) + + def test_process_ntuple_types(self): + p = psutil.Process() + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters): + with self.subTest(name=name, fun=str(fun)): + try: + ret = fun() + except psutil.Error: + continue + self.check_result(ret) + + +# =================================================================== +# --- returned type hints +# =================================================================== + + +class TestTypeHintsReturned(TypeHintTestCase): + """Check that annotated return types in psutil/__init__.py match + the actual values returned at runtime. + """ + + def check(self, fun, name): + try: + ret = fun() + except psutil.Error: + return + check_fun_type_hints(fun, ret) + + def test_system_return_types(self): + for fun, name in system_namespace.iter(system_namespace.getters): + with self.subTest(name=name, fun=str(fun)): + self.check(fun, name) + + def test_process_return_types(self): + p = psutil.Process() + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters): + with self.subTest(name=name, fun=str(fun)): + self.check(fun, name) + + +# ===================================================================== +# --- Tests for check_ntuple_type_hints() test utility fun +# ===================================================================== + + +class TestCheckNtupleTypeHints(TypeHintTestCase): + def test_not_namedtuple(self): + # plain tuple is rejected + with pytest.raises(AssertionError): + check_ntuple_type_hints((1, 2, 3)) + + def test_ok(self): + # addr(ip: str, port: int) - correct types + from psutil._ntuples import addr + + check_ntuple_type_hints(addr('127.0.0.1', 80)) + + def test_wrong_type(self): + # ip should be str, passing int instead + from psutil._ntuples import addr + + with pytest.raises(AssertionError): + check_ntuple_type_hints(addr(127, 80)) + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_union_with_none(self): + # suser has terminal: str | None and host: str | None + from psutil._ntuples import suser + + check_ntuple_type_hints(suser('user', None, None, 1.0, None)) + check_ntuple_type_hints(suser('user', '/dev/tty1', 'host', 1.0, 1)) + with pytest.raises(AssertionError): + check_ntuple_type_hints(suser(1, None, None, 1.0, None)) + + def test_intenum_broadening(self): + # NicDuplex is an IntEnum; check_ntuple_type_hints broadens it to int + from psutil._ntuples import snicstats + + nt = snicstats(True, psutil.NIC_DUPLEX_FULL, 1000, 1500, '') + check_ntuple_type_hints(nt) + + +# ===================================================================== +# --- Tests for check_fun_type_hints() test utility fun +# ===================================================================== + + +class TestCheckFunTypeHints(TypeHintTestCase): + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_no_annotation(self): + def foo(): + return 1 + + with pytest.raises(ValueError, match="no type hints defined"): + check_fun_type_hints(foo, 1) + + def test_float(self): + def foo() -> float: + return 1.0 + + check_fun_type_hints(foo, 1.0) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + def test_bool(self): + def foo() -> bool: + return True + + check_fun_type_hints(foo, True) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_list(self): + def foo() -> list[int]: + return [1] + + check_fun_type_hints(foo, foo()) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_list_container(self): + def foo() -> list[str]: + pass + + check_fun_type_hints(foo, ["a", "b"]) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, ["a", 1]) + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_dict(self): + def foo() -> dict[str, int]: + return {'a': 1} + + check_fun_type_hints(foo, foo()) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_dict_container(self): + def foo() -> dict[str, str]: + pass + + check_fun_type_hints(foo, {"a": "a", "b": "b"}) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, {"a": "a", "1": 1}) + + def test_namedtuple(self): + from psutil._ntuples import addr + + def foo() -> addr: + return addr('127.0.0.1', 80) + + check_fun_type_hints(foo, addr('127.0.0.1', 80)) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_union_with_none(self): + def foo() -> int | None: + return 1 + + check_fun_type_hints(foo, 1) + check_fun_type_hints(foo, None) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + @pytest.mark.skipif( + not hasattr(types, "UnionType"), reason="Python 3.10+ only" + ) + def test_union_or_dict_or_none(self): + def foo() -> int | dict[str, int] | None: + return 1 + + check_fun_type_hints(foo, 1) + check_fun_type_hints(foo, {'a': 1}) + check_fun_type_hints(foo, None) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + def test_generator(self): + from typing import Generator + + def foo() -> Generator[int, None, None]: + yield 1 + + check_fun_type_hints(foo, foo()) + with pytest.raises(AssertionError): + check_fun_type_hints(foo, "str") + + def test_partial(self): + def foo(x) -> int: + return x + + fun = functools.partial(foo, 1) + check_fun_type_hints(fun, 1) + with pytest.raises(AssertionError): + check_fun_type_hints(fun, "str") + + def test_bound_method(self): + class MyClass: + def foo(self) -> int: + return 1 + + obj = MyClass() + check_fun_type_hints(obj.foo, 1) + with pytest.raises(AssertionError): + check_fun_type_hints(obj.foo, "str") diff --git a/tests/test_windows.py b/tests/test_windows.py index 342b9527d2..2f3d01fb81 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -495,7 +495,8 @@ def test_memory_info(self): assert ps.vms == win["PagefileUsage"] assert ps.peak_rss == win["PeakWorkingSetSize"] assert ps.peak_vms == win["PeakPagefileUsage"] - assert ps.num_page_faults == win["PageFaultCount"] + with pytest.warns(DeprecationWarning): + assert ps.num_page_faults == win["PageFaultCount"] def test_memory_info_deprecated_fields(self): win = win32process.GetProcessMemoryInfo(self.OpenProcess(self.pid)) From 1059a9e71edc0ea8b71a798885c88fb6e293dadd Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 10 Mar 2026 19:40:53 +0100 Subject: [PATCH 1574/1714] Split doc into sections (#2758) Split the documentation from a single-page HTML document into multiple sub-sections. Sections now include separate pages for API reference, installation, release timeline, FAQs, and more. Fixes #2757. --- .gitignore | 1 + HISTORY.rst | 3 + MANIFEST.in | 13 +- docs/DEVNOTES | 7 + docs/STATS.md | 5 - docs/_links.rst | 83 + docs/_static/css/custom.css | 404 +-- docs/api.rst | 2829 +++++++++++++++++++++ docs/auto-build.sh | 12 + docs/{DEVGUIDE.rst => devguide.rst} | 36 +- docs/faq.rst | 20 + docs/index.rst | 3604 +-------------------------- INSTALL.rst => docs/install.rst | 26 +- docs/make.bat | 242 -- docs/platform.rst | 45 + docs/recipes.rst | 158 ++ docs/timeline.rst | 415 +++ 17 files changed, 3679 insertions(+), 4224 deletions(-) delete mode 100644 docs/STATS.md create mode 100644 docs/_links.rst create mode 100644 docs/api.rst create mode 100644 docs/auto-build.sh rename docs/{DEVGUIDE.rst => devguide.rst} (84%) create mode 100644 docs/faq.rst rename INSTALL.rst => docs/install.rst (96%) delete mode 100644 docs/make.bat create mode 100644 docs/platform.rst create mode 100644 docs/recipes.rst create mode 100644 docs/timeline.rst diff --git a/.gitignore b/.gitignore index d9f3d671bc..3d8f05eaff 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ syntax: glob .idea/ .tox/ build/ +docs/_build/ dist/ wheelhouse/ .tests/ diff --git a/HISTORY.rst b/HISTORY.rst index 6416a49d77..8660f09b4c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -62,6 +62,9 @@ the corresponding enum members. - 2754_: standardize `sensors_battery()`_'s `percent` so that it returns a `float` instead of `int` on all systems, not only Linux. +- 2757_: split the documentation from a single-page HTML document into multiple + sub-sections. Sections now include separate pages for API reference, + installation, release timeline, FAQs, and more. **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index a0cb105d76..464b51e011 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,27 +4,32 @@ include .gitignore include CONTRIBUTING.md include CREDITS include HISTORY.rst -include INSTALL.rst include LICENSE include MANIFEST.in include Makefile include README.rst include SECURITY.md include docs/.readthedocs.yaml -include docs/DEVGUIDE.rst include docs/DEVNOTES include docs/Makefile include docs/README -include docs/STATS.md +include docs/_links.rst include docs/_static/copybutton.js include docs/_static/css/custom.css include docs/_static/favicon.ico include docs/_static/sidebar.js include docs/_templates/layout.html +include docs/api.rst +include docs/auto-build.sh include docs/conf.py +include docs/devguide.rst +include docs/faq.rst include docs/index.rst -include docs/make.bat +include docs/install.rst +include docs/platform.rst +include docs/recipes.rst include docs/requirements.txt +include docs/timeline.rst include psutil/__init__.py include psutil/_common.py include psutil/_enums.py diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 38ae74f821..d98971b5e2 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -127,3 +127,10 @@ RESOURCES - top: http://www.unixtop.org/ - oshi: https://github.com/oshi/oshi - netdata: https://github.com/netdata/netdata + +STATS +===== + +- https://pepy.tech/projects/psutil +- https://clickpy.clickhouse.com/dashboard/psutil +- https://pypistats.org/packages/psutil diff --git a/docs/STATS.md b/docs/STATS.md deleted file mode 100644 index 5b87ecf364..0000000000 --- a/docs/STATS.md +++ /dev/null @@ -1,5 +0,0 @@ -# Download statistics - -- https://pepy.tech/projects/psutil -- https://clickpy.clickhouse.com/dashboard/psutil -- https://pypistats.org/packages/psutil diff --git a/docs/_links.rst b/docs/_links.rst new file mode 100644 index 0000000000..7fe64f8214 --- /dev/null +++ b/docs/_links.rst @@ -0,0 +1,83 @@ +.. _`BPO-10784`: https://bugs.python.org/issue10784 +.. _`BPO-12442`: https://bugs.python.org/issue12442 +.. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 +.. _`DEVGUIDE.rst`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst +.. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ +.. _`Giampaolo Rodola`: https://gmpy.dev/about +.. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get +.. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt +.. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html +.. _`man prlimit`: https://linux.die.net/man/2/prlimit +.. _`psleak`: https://github.com/giampaolo/psleak +.. _`Tidelift security contact`: https://tidelift.com/security + +.. === docs.python.org + +.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 +.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET +.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX +.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum +.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum +.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum +.. _`hash`: https://docs.python.org/3/library/functions.html#hash +.. _`open`: https://docs.python.org/3/library/functions.html#open +.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count +.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg +.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid +.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority +.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid +.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid +.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY +.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC +.. _`os.open`: https://docs.python.org/3/library/os.html#os.open +.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open +.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority +.. _`os.times`: https://docs.python.org//library/os.html#os.times +.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit +.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit +.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue +.. _`select.poll`: https://docs.python.org//library/select.html#select.poll +.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. +.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. +.. _`signal module`: https://docs.python.org//library/signal.html +.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM +.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET +.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM +.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd +.. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait +.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen +.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident +.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread + +.. === scripts + +.. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py + +.. === Windows API + +.. _`GetDriveType`: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea +.. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess +.. _`GetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +.. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex +.. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass +.. _`TerminateProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess + +.. === Issues + +.. _`#1007`: https://github.com/giampaolo/psutil/issues/1007 +.. _`#1040`: https://github.com/giampaolo/psutil/issues/1040 +.. _`#883`: https://github.com/giampaolo/psutil/issues/883 diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 49da69dfb8..3652a848aa 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -1,6 +1,18 @@ -.wy-nav-content { - max-width: 100% !important; - padding: 15px !important; +@media (min-width: 1200px) { + .wy-nav-content { + max-width: 1000px; + padding-left: 20px !important; + } +} + +/* hide top navigation */ +.rst-content > div[role="navigation"] { + display: none; +} + +/* leave footer navigation visible */ +footer div[role="navigation"][aria-label="Footer"] { + display: block; } .rst-content dl:not(.docutils) { @@ -15,14 +27,14 @@ border-right:10px !important; } -.local-toc li ul li { - padding-left: 20px !important; -} - .rst-content ul p { margin-bottom: 0px !important; } +.rst-content ul { + margin-top: 0px !important; +} + .rst-content li { list-style: outside; margin-left: 15px; @@ -112,11 +124,6 @@ h1 { background: #eeffcc; } -.highlight-default, .highlight-python { - border-radius: 3px !important; - border: 1px solid #ac9 !important; -} - .highlight .c { color: #408090; font-style: italic @@ -126,15 +133,6 @@ h1 { background-color: grey !important } -.highlight { - border-radius: 3px !important; - -} - -div.highlight-default { - margin-bottom: 10px !important; -} - pre { padding: 5px !important; } @@ -184,367 +182,3 @@ div.body div.admonition, div.body div.impl-detail { .note code { background: #d6d6d6 !important; } - -/* ================================================================== */ -/* Syntax highlight like Python doc. -/* ================================================================== */ - -/* Comment */ -.highlight .err { - border: 1px solid #FF0000 -} - -/* Error */ -.highlight .k { - color: #007020; - font-weight: bold -} - -/* Keyword */ -.highlight .o { - color: #666666 -} - -/* Operator */ -.highlight .ch { - color: #408090; - font-style: italic -} - -/* Comment.Hashbang */ -.highlight .cm { - color: #408090; - font-style: italic -} - -/* Comment.Multiline */ -.highlight .cp { - color: #007020 -} - -/* Comment.Preproc */ -.highlight .cpf { - color: #408090; - font-style: italic -} - -/* Comment.PreprocFile */ -.highlight .c1 { - color: #408090; - font-style: italic -} - -/* Comment.Single */ -.highlight .cs { - color: #408090; - background-color: #fff0f0 -} - -/* Comment.Special */ -.highlight .gd { - color: #A00000 -} - -/* Generic.Deleted */ -.highlight .ge { - font-style: italic -} - -/* Generic.Emph */ -.highlight .gr { - color: #FF0000 -} - -/* Generic.Error */ -.highlight .gh { - color: #000080; - font-weight: bold -} - -/* Generic.Heading */ -.highlight .gi { - color: #00A000 -} - -/* Generic.Inserted */ -.highlight .go { - color: #333333 -} - -/* Generic.Output */ -.highlight .gp { - color: #c65d09; - font-weight: bold -} - -/* Generic.Prompt */ -.highlight .gs { - font-weight: bold -} - -/* Generic.Strong */ -.highlight .gu { - color: #800080; - font-weight: bold -} - -/* Generic.Subheading */ -.highlight .gt { - color: #0044DD -} - -/* Generic.Traceback */ -.highlight .kc { - color: #007020; - font-weight: bold -} - -/* Keyword.Constant */ -.highlight .kd { - color: #007020; - font-weight: bold -} - -/* Keyword.Declaration */ -.highlight .kn { - color: #007020; - font-weight: bold -} - -/* Keyword.Namespace */ -.highlight .kp { - color: #007020 -} - -/* Keyword.Pseudo */ -.highlight .kr { - color: #007020; - font-weight: bold -} - -/* Keyword.Reserved */ -.highlight .kt { - color: #902000 -} - -/* Keyword.Type */ -.highlight .m { - color: #208050 -} - -/* Literal.Number */ -.highlight .s { - color: #4070a0 -} - -/* Literal.String */ -.highlight .na { - color: #4070a0 -} - -/* Name.Attribute */ -.highlight .nb { - color: #007020 -} - -/* Name.Builtin */ -.highlight .nc { - color: #0e84b5; - font-weight: bold -} - -/* Name.Class */ -.highlight .no { - color: #60add5 -} - -/* Name.Constant */ -.highlight .nd { - color: #555555; - font-weight: bold -} - -/* Name.Decorator */ -.highlight .ni { - color: #d55537; - font-weight: bold -} - -/* Name.Entity */ -.highlight .ne { - color: #007020 -} - -/* Name.Exception */ -.highlight .nf { - color: #06287e -} - -/* Name.Function */ -.highlight .nl { - color: #002070; - font-weight: bold -} - -/* Name.Label */ -.highlight .nn { - color: #0e84b5; - font-weight: bold -} - -/* Name.Namespace */ -.highlight .nt { - color: #062873; - font-weight: bold -} - -/* Name.Tag */ -.highlight .nv { - color: #bb60d5 -} - -/* Name.Variable */ -.highlight .ow { - color: #007020; - font-weight: bold -} - -/* Operator.Word */ -.highlight .w { - color: #bbbbbb -} - -/* Text.Whitespace */ -.highlight .mb { - color: #208050 -} - -/* Literal.Number.Bin */ -.highlight .mf { - color: #208050 -} - -/* Literal.Number.Float */ -.highlight .mh { - color: #208050 -} - -/* Literal.Number.Hex */ -.highlight .mi { - color: #208050 -} - -/* Literal.Number.Integer */ -.highlight .mo { - color: #208050 -} - -/* Literal.Number.Oct */ -.highlight .sa { - color: #4070a0 -} - -/* Literal.String.Affix */ -.highlight .sb { - color: #4070a0 -} - -/* Literal.String.Backtick */ -.highlight .sc { - color: #4070a0 -} - -/* Literal.String.Char */ -.highlight .dl { - color: #4070a0 -} - -/* Literal.String.Delimiter */ -.highlight .sd { - color: #4070a0; - font-style: italic -} - -/* Literal.String.Doc */ -.highlight .s2 { - color: #4070a0 -} - -/* Literal.String.Double */ -.highlight .se { - color: #4070a0; - font-weight: bold -} - -/* Literal.String.Escape */ -.highlight .sh { - color: #4070a0 -} - -/* Literal.String.Heredoc */ -.highlight .si { - color: #70a0d0; - font-style: italic -} - -/* Literal.String.Interpol */ -.highlight .sx { - color: #c65d09 -} - -/* Literal.String.Other */ -.highlight .sr { - color: #235388 -} - -/* Literal.String.Regex */ -.highlight .s1 { - color: #4070a0 -} - -/* Literal.String.Single */ -.highlight .ss { - color: #517918 -} - -/* Literal.String.Symbol */ -.highlight .bp { - color: #007020 -} - -/* Name.Builtin.Pseudo */ -.highlight .fm { - color: #06287e -} - -/* Name.Function.Magic */ -.highlight .vc { - color: #bb60d5 -} - -/* Name.Variable.Class */ -.highlight .vg { - color: #bb60d5 -} - -/* Name.Variable.Global */ -.highlight .vi { - color: #bb60d5 -} - -/* Name.Variable.Instance */ -.highlight .vm { - color: #bb60d5 -} - -/* Name.Variable.Magic */ -.highlight .il { - color: #208050 -} - -.rst-content pre.literal-block, .rst-content div[class^='highlight'] { - border: 1px solid #e1e4e5; - padding: 0px; - overflow-x: auto; - margin: 1px 0 0px 0; -} diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000000..e691503d71 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,2829 @@ +.. include:: _links.rst +.. currentmodule:: psutil + +API reference +============= + +.. contents:: + :local: + :depth: 3 + +System related functions +------------------------ + +CPU +^^^ + +.. function:: cpu_times(percpu=False) + + Return system CPU times as a named tuple. + Every attribute represents the seconds the CPU has spent in the given mode. + The attributes availability varies depending on the platform. + Cross-platform fields: + + - **user**: time spent by normal processes executing in user mode; on Linux + this also includes **guest** time + - **system**: time spent by processes executing in kernel mode + - **idle**: time spent doing nothing + + Platform-specific fields: + + - **nice** *(Linux, macOS, BSD)*: time spent by niced (prioritized) processes + executing in user mode; on Linux this also includes **guest_nice** time + - **iowait** *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete. + This is *not* accounted in **idle** time counter. + - **irq** *(Linux, BSD)*: time spent for servicing hardware interrupts + - **softirq** *(Linux)*: time spent for servicing software interrupts + - **steal** *(Linux)*: time spent by other operating systems running + in a virtualized environment + - **guest** *(Linux)*: time spent running a virtual CPU for guest + operating systems under the control of the Linux kernel + - **guest_nice** *(Linux)*: time spent running a niced guest + (virtual CPU for guest operating systems under the control of the Linux + kernel) + - **interrupt** *(Windows)*: time spent for servicing hardware interrupts ( + similar to "irq" on UNIX) + - **dpc** *(Windows)*: time spent servicing deferred procedure calls (DPCs); + DPCs are interrupts that run at a lower priority than standard interrupts. + + When *percpu* is ``True`` return a list of named tuples for each logical CPU + on the system. + First element of the list refers to first CPU, second element to second CPU + and so on. + The order of the list is consistent across calls. + Example output on Linux: + + >>> import psutil + >>> psutil.cpu_times() + scputimes(user=17411.7, system=3797.02, idle=51266.57, nice=77.99, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) + + .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. + + .. versionchanged:: 8.0.0 + ``cpu_times()`` field order was standardized: ``user``, ``system``, + ``idle`` are now always the first three fields. Previously on Linux, + macOS, and BSD the first three were ``user``, ``nice``, ``system``. + .. warning:: + in version 8.0.0 the named tuple changed field order. Positional access + (e.g. ``cpu_times()[3]``) may silently return the wrong field. Always use + attribute access instead (e.g. ``cpu_times().idle``). + + .. warning:: + 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 `#1210 + `__. + +.. function:: cpu_percent(interval=None, percpu=False) + + Return a float representing the current system-wide CPU utilization as a + percentage. When *interval* is > ``0.0`` compares system CPU times elapsed + before and after the interval (blocking). + When *interval* is ``0.0`` or ``None`` compares system CPU times elapsed + since last call or module import, returning immediately. + That means the first time this is called it will return a meaningless ``0.0`` + value which you are supposed to ignore. + In this case it is recommended for accuracy that this function be called with + at least ``0.1`` seconds between calls. + When *percpu* is ``True`` returns a list of floats representing the + utilization as a percentage for each CPU. + First element of the list refers to first CPU, second element to second CPU + and so on. The order of the list is consistent across calls. + Internally this function maintains a global map (a dict) where each key is + the ID of the calling thread (`threading.get_ident`_). This means it can be + called from different threads, at different intervals, and still return + meaningful and independent results. + + >>> import psutil + >>> # blocking + >>> psutil.cpu_percent(interval=1) + 2.0 + >>> # non-blocking (percentage since last call) + >>> psutil.cpu_percent(interval=None) + 2.9 + >>> # blocking, per-cpu + >>> psutil.cpu_percent(interval=1, percpu=True) + [2.0, 1.0] + >>> + + .. warning:: + the first time this function is called with *interval* = ``0.0`` or ``None`` + it will return a meaningless ``0.0`` value which you are supposed to + ignore. + + .. versionchanged:: 5.9.6 function is now thread safe. + +.. function:: cpu_times_percent(interval=None, percpu=False) + + Same as :func:`cpu_percent()` but provides utilization percentages for each + specific CPU time as is returned by + :func:`psutil.cpu_times(percpu=True)`. + *interval* and + *percpu* arguments have the same meaning as in :func:`cpu_percent()`. + On Linux "guest" and "guest_nice" percentages are not accounted in "user" + and "user_nice" percentages. + + .. warning:: + the first time this function is called with *interval* = ``0.0`` or + ``None`` it will return a meaningless ``0.0`` value which you are supposed + to ignore. + + .. versionchanged:: + 4.1.0 two new *interrupt* and *dpc* fields are returned on Windows. + + .. versionchanged:: 5.9.6 function is now thread safe. + +.. function:: cpu_count(logical=True) + + Return the number of logical CPUs in the system (similar to `os.cpu_count`_) + or ``None`` if undetermined. + Unlike `os.cpu_count`_, this is not influenced by the ``PYTHON_CPU_COUNT`` + environment variable introduced in Python 3.13. + "logical CPUs" means the number of physical cores multiplied by the number + of threads that can run on each core (this is known as Hyper Threading). + This is what cloud providers often refer to as vCPUs. + If *logical* is ``False`` return the number of physical cores only, or + ``None`` if undetermined. + On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return + ``None``. + Example on a system having 2 cores + Hyper Threading: + + >>> import psutil + >>> psutil.cpu_count() + 4 + >>> psutil.cpu_count(logical=False) + 2 + + Note that ``psutil.cpu_count()`` may not necessarily be equivalent to the + actual number of CPUs the current process can use. + That can vary in case process CPU affinity has been changed, Linux cgroups + are being used or (in case of Windows) on systems using processor groups or + having more than 64 CPUs. + The number of usable CPUs can be obtained with: + + >>> len(psutil.Process().cpu_affinity()) + 1 + +.. function:: cpu_stats() + + Return various CPU statistics as a named tuple: + + - **ctx_switches**: + number of context switches (voluntary + involuntary) since boot. + - **interrupts**: + number of interrupts since boot. + - **soft_interrupts**: + number of software interrupts since boot. Always set to ``0`` on Windows + and SunOS. + - **syscalls**: number of system calls since boot. Always set to ``0`` on + Linux. + + Example (Linux): + + .. code-block:: python + + >>> import psutil + >>> psutil.cpu_stats() + scpustats(ctx_switches=20455687, interrupts=6598984, soft_interrupts=2134212, syscalls=0) + + .. versionadded:: 4.1.0 + + +.. function:: cpu_freq(percpu=False) + + Return CPU frequency as a named tuple including *current*, *min* and *max* + frequencies expressed in Mhz. On Linux *current* frequency reports the + real-time value, on all other platforms this usually represents the + nominal "fixed" value (never changing). If *percpu* is ``True`` and the + system supports per-cpu frequency retrieval (Linux and FreeBSD), a list of + frequencies is returned for each CPU, if not, a list with a single element + is returned. If *min* and *max* cannot be determined they are set to + ``0.0``. + + Example (Linux): + + .. code-block:: python + + >>> import psutil + >>> psutil.cpu_freq() + scpufreq(current=931.42925, min=800.0, max=3500.0) + >>> psutil.cpu_freq(percpu=True) + [scpufreq(current=2394.945, min=800.0, max=3500.0), + scpufreq(current=2236.812, min=800.0, max=3500.0), + scpufreq(current=1703.609, min=800.0, max=3500.0), + scpufreq(current=1754.289, min=800.0, max=3500.0)] + + Availability: Linux, macOS, Windows, FreeBSD, OpenBSD. *percpu* only + supported on Linux and FreeBSD. + + .. versionadded:: 5.1.0 + + .. versionchanged:: 5.5.1 added FreeBSD support. + + .. versionchanged:: 5.9.1 added OpenBSD support. + +.. function:: getloadavg() + + Return the average system load over the last 1, 5 and 15 minutes as a tuple. + The "load" represents the processes which are in a runnable state, either + using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). + On UNIX systems this relies on `os.getloadavg`_. On Windows this is emulated + by using a Windows API that spawns a thread which keeps running in + background and updates results every 5 seconds, mimicking the UNIX behavior. + Thus, on Windows, the first time this is called and for the next 5 seconds + it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. + The numbers returned only make sense if related to the number of CPU cores + installed on the system. So, for instance, a value of `3.14` on a system + with 10 logical CPUs means that the system load was 31.4% percent over the + last N minutes. + + .. code-block:: python + + >>> import psutil + >>> psutil.getloadavg() + (3.14, 3.89, 4.67) + >>> psutil.cpu_count() + 10 + >>> # percentage representation + >>> [x / psutil.cpu_count() * 100 for x in psutil.getloadavg()] + [31.4, 38.9, 46.7] + + .. versionadded:: 5.6.2 + +Memory +^^^^^^ + +.. function:: virtual_memory() + + Return statistics about system memory usage as a named tuple including the + following fields, expressed in bytes. + + - **total**: total physical memory (exclusive swap). + - **available**: memory that can be given instantly to processes without the + system going into swap. On Linux it uses the ``MemAvailable`` field from + ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back to an + estimate. This is the recommended field for monitoring actual memory usage + in a cross-platform fashion. + - **percent**: the percentage usage calculated as + ``(total - available) / total * 100``. + - **used**: memory in use, calculated differently depending on the platform + (see the table below). It is meant for informational purposes. Neither + ``total - free`` nor ``total - available`` necessarily equals ``used``. + - **free**: memory that is not in use at all (zeroed pages). This is + typically much lower than **available** because the OS keeps recently + freed memory as reclaimable cache (see **cached** and **buffers**) + rather than zeroing it immediately. Do not use this to check for + memory pressure; use **available** instead. + - **active** *(Linux, macOS, BSD)*: memory currently mapped by processes + or recently accessed, held in RAM. It is unlikely to be reclaimed unless + the system is under significant memory pressure. + - **inactive** *(Linux, macOS, BSD)*: memory not recently accessed. It + still holds valid data (file cache, old allocations) but is a candidate + for reclamation or swapping. On BSD systems it is counted in + **available**. + - **buffers** *(Linux, BSD)*: kernel buffer cache for raw block-device I/O + (e.g. disk blocks read before the filesystem processes them). Can be + reclaimed by the OS when needed. + - **cached** *(Linux, BSD)*: in-memory page cache for files read from disk. + The OS reclaims this memory automatically when processes need it, so a + large **cached** value is healthy and does not indicate memory pressure. + On Linux this includes ``SReclaimable`` (reclaimable slab memory). + - **shared** *(Linux, BSD)*: memory accessible by multiple processes + simultaneously, such as POSIX shared memory (``shm_open``) and + ``tmpfs``-backed files. On Linux this corresponds to ``Shmem`` in + ``/proc/meminfo`` and is already counted within **active** / **inactive**. + - **slab** *(Linux)*: memory used by the kernel's internal object caches + (e.g. inode and dentry caches). The reclaimable portion + (``SReclaimable``) is already included in **cached**. + - **wired** *(macOS, BSD)*: memory pinned in RAM by the kernel (e.g. kernel + code and critical data structures). It can never be moved to disk. + + .. note:: + - On Linux, **total**, **free**, **used**, **shared**, and **available** + match the output of the ``free`` command. + - On macOS, **free**, **active**, **inactive**, and **wired** match + ``vm_stat`` output. + - On Windows, **total**, **used** ("In use"), and **available** match + the Task Manager (Performance > Memory tab). + + Follows a table showing implementation details. All info on Linux are retrieved from `/proc/meminfo`. + + .. list-table:: + :header-rows: 1 + + * - Field + - Linux + - macOS + - Windows + - FreeBSD + * - total + - ``MemTotal`` + - ``sysctl() hw.memsize`` + - ``GetPerformanceInfo() PhysicalTotal`` + - ``sysctl() hw.physmem`` + * - available + - ``MemAvailable`` + - ``host_statistics64() inactive + free`` + - ``GetPerformanceInfo() PhysicalAvailable`` + - ``inactive + cached + free`` + * - used + - ``total - available`` + - ``host_statistics64() active + wired`` + - ``total - available`` + - ``active + wired + cached`` + * - free + - ``MemFree`` + - ``host_statistics64() free - speculative`` + - same as ``available`` + - ``sysctl() vm.stats.vm.v_free_count`` + * - active + - ``Active`` + - ``host_statistics64() active`` + - + - ``sysctl() vm.stats.vm.v_active_count`` + * - inactive + - ``Inactive`` + - ``host_statistics64() inactive`` + - + - ``sysctl() vm.stats.vm.v_inactive_count`` + * - buffers + - ``Buffers`` + - + - + - ``sysctl() vfs.bufspace`` + * - cached + - ``Cached + SReclaimable`` + - + - + - ``sysctl() vm.stats.vm.v_cache_count`` + * - shared + - ``Shmem`` + - + - + - ``sysctl(CTL_VM/VM_METER) t_vmshr + t_rmshr`` + * - slab + - ``Slab`` + - + - + - + * - wired + - + - ``host_statistics64() wired`` + - + - ``sysctl() vm.stats.vm.v_wire_count`` + + Example on Linux: + + >>> import psutil + >>> mem = psutil.virtual_memory() + >>> mem + svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304, slab=199348224) + >>> + >>> THRESHOLD = 500 * 1024 * 1024 # 500MB + >>> if mem.available <= THRESHOLD: + ... print("warning") + ... + >>> + + .. note:: if you just want to know how much physical memory is left in a + cross platform fashion simply rely on **available** and **percent** + fields. + + .. note:: see `meminfo.py`_ script providing an example on how to convert + bytes in a human readable form. + + .. versionchanged:: 4.2.0 added *shared* metric on Linux. + + .. versionchanged:: 5.4.4 added *slab* metric on Linux. + +.. function:: swap_memory() + + Return system swap memory statistics as a named tuple including the following + fields: + + * **total**: total swap space. On Windows this is derived as + ``CommitLimit - PhysicalTotal``, representing virtual memory backed by + the page file rather than the raw page-file size. + * **used**: swap space currently in use. + * **free**: swap space not in use (``total - used``). + * **percent**: swap usage as a percentage, calculated as + ``used / total * 100``. + * **sin**: number of bytes the system has paged *in* from disk (pages moved + from swap space back into RAM) since boot (cumulative). + * **sout**: number of bytes the system has paged *out* to disk (pages moved + from RAM into swap space) since boot (cumulative). A continuously + increasing **sout** is a sign of memory pressure. + + **sin** and **sout** are cumulative counters since boot; monitor their rate + of change rather than the absolute value to detect active swapping. + On Windows both are always ``0``. + See `meminfo.py`_ script providing an example on how to convert bytes in a + human readable form. + + >>> import psutil + >>> psutil.swap_memory() + sswap(total=2097147904L, used=886620160L, free=1210527744L, percent=42.3, sin=1050411008, sout=1906720768) + + .. versionchanged:: 5.2.3 on Linux this function relies on /proc fs instead + of sysinfo() syscall so that it can be used in conjunction with + :const:`psutil.PROCFS_PATH` in order to retrieve memory info about + Linux containers such as Docker and Heroku. + +Disks +^^^^^ + +.. function:: disk_partitions(all=False) + + Return all mounted disk partitions as a list of named tuples including device, + mount point and filesystem type, similarly to "df" command on UNIX. If *all* + parameter is ``False`` it tries to distinguish and return physical devices + only (e.g. hard disks, cd-rom drives, USB keys) and ignore all others + (e.g. pseudo, memory, duplicate, inaccessible filesystems). + Note that this may not be fully reliable on all systems (e.g. on BSD this + parameter is ignored). + See `disk_usage.py`_ script providing an example usage. + Returns a list of named tuples with the following fields: + + * **device**: the device path (e.g. ``"/dev/hda1"``). On Windows this is the + drive letter (e.g. ``"C:\\"``). + * **mountpoint**: the mount point path (e.g. ``"/"``). On Windows this is the + drive letter (e.g. ``"C:\\"``). + * **fstype**: the partition filesystem (e.g. ``"ext3"`` on UNIX or ``"NTFS"`` + on Windows). + * **opts**: a comma-separated string indicating different mount options for + the drive/partition. Platform-dependent. + + >>> import psutil + >>> psutil.disk_partitions() + [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), + sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] + + .. versionchanged:: 5.7.4 added *maxfile* and *maxpath* fields + + .. versionchanged:: 6.0.0 removed *maxfile* and *maxpath* fields + +.. function:: disk_usage(path) + + Return disk usage statistics about the partition which contains the given + *path* as a named tuple including **total**, **used** and **free** space + expressed in bytes, plus the **percentage** usage. + ``OSError`` is raised if *path* does not exist. + Starting from Python 3.3 this is also available as `shutil.disk_usage`_ + (see `BPO-12442`_). + See `disk_usage.py`_ script providing an example usage. + + >>> import psutil + >>> psutil.disk_usage('/') + sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) + + .. note:: + UNIX usually reserves 5% of the total disk space for the root user. + *total* and *used* fields on UNIX refer to the overall total and used + space, whereas *free* represents the space available for the **user** and + *percent* represents the **user** utilization (see + `source code `__). + That is why *percent* value may look 5% bigger than what you would expect + it to be. + Also note that both 4 values match "df" cmdline utility. + + .. versionchanged:: + 4.3.0 *percent* value takes root reserved space into account. + +.. function:: disk_io_counters(perdisk=False, nowrap=True) + + Return system-wide disk I/O statistics as a named tuple including the + following fields: + + - **read_count**: number of reads + - **write_count**: number of writes + - **read_bytes**: number of bytes read + - **write_bytes**: number of bytes written + + Platform-specific fields: + + - **read_time**: (all except *NetBSD* and *OpenBSD*) time spent reading from + disk (in milliseconds) + - **write_time**: (all except *NetBSD* and *OpenBSD*) time spent writing to disk + (in milliseconds) + - **busy_time**: (*Linux*, *FreeBSD*) time spent doing actual I/Os (in + milliseconds) + - **read_merged_count** (*Linux*): number of merged reads (see `iostats doc`_) + - **write_merged_count** (*Linux*): number of merged writes (see `iostats doc`_) + + If *perdisk* is ``True`` return the same information for every physical disk + installed on the system as a dictionary with partition names as the keys and + the named tuple described above as the values. + See `iotop.py`_ for an example application. + On some systems such as Linux, on a very busy or long-lived system, the + numbers returned by the kernel may overflow and wrap (restart from zero). + If *nowrap* is ``True`` psutil will detect and adjust those numbers across + function calls 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 *nowrap* + cache. + On Windows it may be necessary to issue ``diskperf -y`` command from cmd.exe + first in order to enable IO counters. + On diskless machines this function will return ``None`` or ``{}`` if + *perdisk* is ``True``. + + >>> import psutil + >>> psutil.disk_io_counters() + sdiskio(read_count=8141, write_count=2431, read_bytes=290203, write_bytes=537676, read_time=5868, write_time=94922) + >>> + >>> psutil.disk_io_counters(perdisk=True) + {'sda1': sdiskio(read_count=920, write_count=1, read_bytes=2933248, write_bytes=512, read_time=6016, write_time=4), + 'sda2': sdiskio(read_count=18707, write_count=8830, read_bytes=6060, write_bytes=3443, read_time=24585, write_time=1572), + 'sdb1': sdiskio(read_count=161, write_count=0, read_bytes=786432, write_bytes=0, read_time=44, write_time=0)} + + .. note:: + on Windows ``"diskperf -y"`` command may need to be executed first + otherwise this function won't find any disk. + + .. versionchanged:: + 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new + *nowrap* argument. + + .. versionchanged:: + 4.0.0 added *busy_time* (Linux, FreeBSD), *read_merged_count* and + *write_merged_count* (Linux) fields. + + .. versionchanged:: + 4.0.0 NetBSD no longer has *read_time* and *write_time* fields. + +Network +^^^^^^^ + +.. function:: net_io_counters(pernic=False, nowrap=True) + + Return system-wide network I/O statistics as a named tuple including the + following attributes: + + - **bytes_sent**: number of bytes sent + - **bytes_recv**: number of bytes received + - **packets_sent**: number of packets sent + - **packets_recv**: number of packets received + - **errin**: total number of errors while receiving + - **errout**: total number of errors while sending + - **dropin**: total number of incoming packets which were dropped + - **dropout**: total number of outgoing packets which were dropped (always 0 + on macOS and BSD) + + If *pernic* is ``True`` return the same information for every network + interface installed on the system as a dictionary with network interface + names as the keys and the named tuple described above as the values. + On some systems such as Linux, on a very busy or long-lived system, the + numbers returned by the kernel may overflow and wrap (restart from zero). + If *nowrap* is ``True`` psutil will detect and adjust those numbers across + function calls and add "old value" to "new value" so that the returned + numbers will always be increasing or remain the same, but never decrease. + ``net_io_counters.cache_clear()`` can be used to invalidate the *nowrap* + cache. + On machines with no network interfaces this function will return ``None`` or + ``{}`` if *pernic* is ``True``. + + >>> import psutil + >>> psutil.net_io_counters() + snetio(bytes_sent=14508483, bytes_recv=62749361, packets_sent=84311, packets_recv=94888, errin=0, errout=0, dropin=0, dropout=0) + >>> + >>> psutil.net_io_counters(pernic=True) + {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), + 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} + + Also see `nettop.py`_ and `ifconfig.py`_ for an example application. + + .. versionchanged:: + 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new + *nowrap* argument. + +.. function:: net_connections(kind='inet') + + Return system-wide socket connections as a list of named tuples. + Every named tuple provides 7 attributes: + + - **fd**: the socket file descriptor. If the connection refers to the current + process this may be passed to `socket.fromfd`_ + to obtain a usable socket object. + On Windows and SunOS this is always set to ``-1``. + - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. + - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or + `SOCK_SEQPACKET`_. + - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` + in case of AF_UNIX sockets. For UNIX sockets see notes below. + - **raddr**: the remote address as a ``(ip, port)`` named tuple or an + absolute ``path`` in case of UNIX sockets. + When the remote endpoint is not connected you'll get an empty tuple + (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. + - **status**: represents the status of a TCP connection. The return value + is one of the `psutil.CONN_* <#connections-constants>`_ constants + (a string). + For UDP and UNIX sockets this is always going to be + :const:`psutil.CONN_NONE`. + - **pid**: the PID of the process which opened the socket, if retrievable, + else ``None``. On some platforms (e.g. Linux) the availability of this + field changes depending on process privileges (root is needed). + + The *kind* parameter is a string which filters for connections matching the + following criteria: + + .. table:: + + +----------------+-----------------------------------------------------+ + | **Kind value** | **Connections using** | + +================+=====================================================+ + | ``"inet"`` | IPv4 and IPv6 | + +----------------+-----------------------------------------------------+ + | ``"inet4"`` | IPv4 | + +----------------+-----------------------------------------------------+ + | ``"inet6"`` | IPv6 | + +----------------+-----------------------------------------------------+ + | ``"tcp"`` | TCP | + +----------------+-----------------------------------------------------+ + | ``"tcp4"`` | TCP over IPv4 | + +----------------+-----------------------------------------------------+ + | ``"tcp6"`` | TCP over IPv6 | + +----------------+-----------------------------------------------------+ + | ``"udp"`` | UDP | + +----------------+-----------------------------------------------------+ + | ``"udp4"`` | UDP over IPv4 | + +----------------+-----------------------------------------------------+ + | ``"udp6"`` | UDP over IPv6 | + +----------------+-----------------------------------------------------+ + | ``"unix"`` | UNIX socket (both UDP and TCP protocols) | + +----------------+-----------------------------------------------------+ + | ``"all"`` | the sum of all the possible families and protocols | + +----------------+-----------------------------------------------------+ + + On macOS and AIX this function requires root privileges. + To get per-process connections use :meth:`Process.net_connections`. + Also, see `netstat.py`_ example script. + Example: + + >>> import psutil + >>> psutil.net_connections() + [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=, pid=1254), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=, pid=2987), + pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=, pid=None), + pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=, pid=None) + ...] + + .. warning:: + On Linux, retrieving some connections requires root privileges. If psutil is + not run as root, those connections are silently skipped instead of raising + ``PermissionError``. That means the returned list may be incomplete. + + .. note:: + (macOS and AIX) :class:`psutil.AccessDenied` is always raised unless running + as root. This is a limitation of the OS and ``lsof`` does the same. + + .. note:: + (Solaris) UNIX sockets are not supported. + + .. note:: + (Linux, FreeBSD, OpenBSD) *raddr* field for UNIX sockets is always set to + ``""`` (empty string). This is a limitation of the OS. + + .. versionadded:: 2.1.0 + + .. versionchanged:: 5.3.0 : socket "fd" is now set for real instead of being + ``-1``. + + .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. + + .. versionchanged:: 5.9.5 : OpenBSD: retrieve *laddr* path for AF_UNIX + sockets (before it was an empty string). + + .. versionchanged:: 8.0.0 *status* field is now a + :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + +.. function:: net_if_addrs() + + Return the addresses associated to each NIC (network interface card) + installed on the system as a dictionary whose keys are the NIC names and + value is a list of named tuples for each address assigned to the NIC. + Each named tuple includes 5 fields: + + - **family**: the address family, either `AF_INET`_ or `AF_INET6`_ + or :const:`psutil.AF_LINK`, which refers to a MAC address. + - **address**: the primary NIC address (always set). + - **netmask**: the netmask address (may be ``None``). + - **broadcast**: the broadcast address (may be ``None``). + - **ptp**: stands for "point to point"; it's the destination address on a + point to point interface (typically a VPN). *broadcast* and *ptp* are + mutually exclusive. May be ``None``. + + Example:: + + >>> import psutil + >>> psutil.net_if_addrs() + {'lo': [snicaddr(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), + snicaddr(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), + snicaddr(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], + 'wlan0': [snicaddr(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), + snicaddr(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), + snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} + >>> + + See also `nettop.py`_ and `ifconfig.py`_ for an example application. + + .. note:: + if you're interested in others families (e.g. AF_BLUETOOTH) you can use + the more powerful `netifaces `__ + extension. + + .. note:: + you can have more than one address of the same family associated with each + interface (that's why dict values are lists). + + .. note:: + *broadcast* and *ptp* are not supported on Windows and are always ``None``. + + .. versionadded:: 3.0.0 + + .. versionchanged:: 3.2.0 *ptp* field was added. + + .. versionchanged:: 4.4.0 added support for *netmask* field on Windows which + is no longer ``None``. + + .. versionchanged:: 7.0.0 added support for *broadcast* field on Windows + which is no longer ``None``. + +.. function:: net_if_stats() + + Return information about each NIC (network interface card) installed on the + system as a dictionary whose keys are the NIC names and value is a named tuple + with the following fields: + + - **isup**: a bool indicating whether the NIC is up and running (meaning + ethernet cable or Wi-Fi is connected). + - **duplex**: the duplex communication type; + it can be either :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or + :const:`NIC_DUPLEX_UNKNOWN`. + - **speed**: the NIC speed expressed in mega bits (MB), if it can't be + determined (e.g. 'localhost') it will be set to ``0``. + - **mtu**: NIC's maximum transmission unit expressed in bytes. + - **flags**: a string of comma-separated flags on the interface (may be an empty string). + Possible flags are: ``up``, ``broadcast``, ``debug``, ``loopback``, + ``pointopoint``, ``notrailers``, ``running``, ``noarp``, ``promisc``, + ``allmulti``, ``master``, ``slave``, ``multicast``, ``portsel``, + ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, + and ``d2`` (some flags are only available on certain platforms). + + Availability: UNIX + + Example: + + >>> import psutil + >>> psutil.net_if_stats() + {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast'), + 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running')} + + Also see `nettop.py`_ and `ifconfig.py`_ for an example application. + + .. versionadded:: 3.0.0 + + .. versionchanged:: 5.7.3 `isup` on UNIX also checks whether the NIC is running. + + .. versionchanged:: 5.9.3 *flags* field was added on POSIX. + +Sensors +^^^^^^^ + +.. function:: sensors_temperatures(fahrenheit=False) + + Return hardware temperatures. Each entry is a named tuple representing a + certain hardware temperature sensor (it may be a CPU, an hard disk or + something else, depending on the OS and its configuration). + All temperatures are expressed in celsius unless *fahrenheit* is set to + ``True``. + If sensors are not supported by the OS an empty dict is returned. + Each named tuple includes 4 fields: + + - **label**: a string label for the sensor, if available, else ``""``. + - **current**: current temperature, or ``None`` if not available. + - **high**: temperature at which the system will throttle, or ``None`` + if not available. + - **critical**: temperature at which the system will shut down, or + ``None`` if not available. + + Example:: + + >>> import psutil + >>> psutil.sensors_temperatures() + {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], + 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], + 'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), + shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0), + shwtemp(label='Core 1', current=52.0, high=100.0, critical=100.0), + shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), + shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} + + See also `temperatures.py`_ and `sensors.py`_ for an example application. + + Availability: Linux, FreeBSD + + .. versionadded:: 5.1.0 + + .. versionchanged:: 5.5.0 added FreeBSD support + +.. function:: sensors_fans() + + Return hardware fans speed. Each entry is a named tuple representing a + certain hardware sensor fan. + Fan speed is expressed in RPM (revolutions per minute). + If sensors are not supported by the OS an empty dict is returned. + Example:: + + >>> import psutil + >>> psutil.sensors_fans() + {'asus': [sfan(label='cpu_fan', current=3200)]} + + See also `fans.py`_ and `sensors.py`_ for an example application. + + Availability: Linux + + .. versionadded:: 5.2.0 + +.. function:: sensors_battery() + + Return battery status information as a named tuple including the following + values. If no battery is installed or metrics can't be determined ``None`` + is returned. + + - **percent**: battery power left as a percentage. + - **secsleft**: a rough approximation of how many seconds are left before the + battery runs out of power. + If the AC power cable is connected this is set to + :data:`psutil.POWER_TIME_UNLIMITED `. + If it can't be determined it is set to + :data:`psutil.POWER_TIME_UNKNOWN `. + - **power_plugged**: ``True`` if the AC power cable is connected, ``False`` + if not or ``None`` if it can't be determined. + + Example:: + + >>> import psutil + >>> + >>> def secs2hours(secs): + ... mm, ss = divmod(secs, 60) + ... hh, mm = divmod(mm, 60) + ... return "%d:%02d:%02d" % (hh, mm, ss) + ... + >>> battery = psutil.sensors_battery() + >>> battery + sbattery(percent=93, secsleft=16628, power_plugged=False) + >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) + charge = 93%, time left = 4:37:08 + + See also `battery.py`_ and `sensors.py`_ for an example application. + + Availability: Linux, Windows, FreeBSD + + .. versionadded:: 5.1.0 + + .. versionchanged:: 5.4.2 added macOS support + +---- + +Other system info +^^^^^^^^^^^^^^^^^ + +.. function:: boot_time() + + Return the system boot time expressed in seconds since the epoch (seconds + since January 1, 1970, at midnight UTC). The returned value is based on the + system clock, which means it may be affected by changes such as manual + adjustments or time synchronization (e.g. NTP). + + .. code-block:: python + + >>> import psutil, datetime + >>> psutil.boot_time() + 1389563460.0 + >>> datetime.datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S") + '2014-01-12 22:51:00' + + .. note:: + on Windows this function may return a time which is off by 1 second if it's + used across different processes (see issue `#1007`_). + +.. function:: users() + + Return users currently connected on the system as a list of named tuples + including the following fields: + + - **name**: the name of the user. + - **terminal**: the tty or pseudo-tty associated with the user, if any, + else ``None``. + - **host**: the host name associated with the entry, if any, else ``None``. + - **started**: the creation time as a floating point number expressed in + seconds since the epoch. + - **pid**: the PID of the login process (like sshd, tmux, gdm-session-worker, + ...). On Windows and OpenBSD this is always set to ``None``. + + Example:: + + >>> import psutil + >>> psutil.users() + [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), + suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] + + .. versionchanged:: + 5.3.0 added "pid" field + +---- + +Processes +--------- + +Functions +^^^^^^^^^ + +.. function:: pids() + + Return a sorted list of current running PIDs. + To iterate over all processes and avoid race conditions :func:`process_iter()` + should be preferred. + + >>> import psutil + >>> psutil.pids() + [1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, ..., 32498] + + .. versionchanged:: + 5.6.0 PIDs are returned in sorted order + +.. function:: process_iter(attrs=None, ad_value=None) + + Return an iterator yielding a :class:`Process` class instance for all running + processes on the local machine. + This should be preferred over :func:`psutil.pids()` to iterate over + processes, as retrieving info is safe from race conditions. + + Every :class:`Process` instance is only created once, and then cached for the + next time :func:`psutil.process_iter()` is called (if PID is still alive). + Cache can optionally be cleared via ``process_iter.cache_clear()``. + + *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. + If *attrs* is specified :meth:`Process.as_dict()` result will be stored as a + ``info`` attribute attached to the returned :class:`Process` instances. + If *attrs* is an empty list it will retrieve all process info (slow). + + Sorting order in which processes are returned is based on their PID. + + Example:: + + >>> import psutil + >>> for proc in psutil.process_iter(['pid', 'name', 'username']): + ... print(proc.info) + ... + {'name': 'systemd', 'pid': 1, 'username': 'root'} + {'name': 'kthreadd', 'pid': 2, 'username': 'root'} + {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} + ... + + A dict comprehensions to create a ``{pid: info, ...}`` data structure:: + + >>> import psutil + >>> procs = {p.pid: p.info for p in psutil.process_iter(['name', 'username'])} + >>> procs + {1: {'name': 'systemd', 'username': 'root'}, + 2: {'name': 'kthreadd', 'username': 'root'}, + 3: {'name': 'ksoftirqd/0', 'username': 'root'}, + ...} + + Clear internal cache:: + + >>> psutil.process_iter.cache_clear() + + .. versionchanged:: + 5.3.0 added "attrs" and "ad_value" parameters. + + .. versionchanged:: + 6.0.0 no longer checks whether each yielded process PID has been reused. + + .. versionchanged:: + 6.0.0 added ``psutil.process_iter.cache_clear()`` API. + +.. function:: pid_exists(pid) + + Check whether the given PID exists in the current process list. This is + faster than doing ``pid in psutil.pids()`` and should be preferred. + +.. function:: wait_procs(procs, timeout=None, callback=None) + + Convenience function which waits for a list of :class:`Process` instances to + terminate. Return a ``(gone, alive)`` tuple indicating which processes are + gone and which ones are still alive. The *gone* ones will have a new + *returncode* attribute indicating process exit status as returned by + :meth:`Process.wait`. + ``callback`` is a function which gets called when one of the processes being + waited on is terminated and a :class:`Process` instance is passed as callback + argument (the instance will also have a *returncode* attribute set). + This function will return as soon as all processes terminate or when + *timeout* (seconds) occurs. + Differently from :meth:`Process.wait` it will not raise + :class:`TimeoutExpired` if timeout occurs. + A typical use case may be: + + - send SIGTERM to a list of processes + - give them some time to terminate + - send SIGKILL to those ones which are still alive + + Example which terminates and waits all the children of this process:: + + import psutil + + def on_terminate(proc): + print("process {} terminated with exit code {}".format(proc, proc.returncode)) + + procs = psutil.Process().children() + for p in procs: + p.terminate() + gone, alive = psutil.wait_procs(procs, timeout=3, callback=on_terminate) + for p in alive: + p.kill() + +Exceptions +^^^^^^^^^^ + +.. class:: Error() + + Base exception class. All other exceptions inherit from this one. + +.. class:: NoSuchProcess(pid, name=None, msg=None) + + Raised by :class:`Process` class methods when no process with the given + *pid* is found in the current process list, or when a process no longer + exists. *name* is the name the process had before disappearing + and gets set only if :meth:`Process.name()` was previously called. + +.. class:: ZombieProcess(pid, name=None, ppid=None, msg=None) + + This may be raised by :class:`Process` class methods when querying a zombie + process on UNIX (Windows doesn't have zombie processes). + *name* and *ppid* attributes are available if :meth:`Process.name()` or + :meth:`Process.ppid()` methods were called before the process turned into a + zombie. + + .. note:: + + this is a subclass of :class:`NoSuchProcess` so if you're not interested + in retrieving zombies (e.g. when using :func:`process_iter()`) you can + ignore this exception and just catch :class:`NoSuchProcess`. + + .. versionadded:: 3.0.0 + +.. class:: AccessDenied(pid=None, name=None, msg=None) + + Raised by :class:`Process` class methods when permission to perform an + action is denied due to insufficient privileges. + *name* attribute is available if :meth:`Process.name()` was previously called. + +.. class:: TimeoutExpired(seconds, pid=None, name=None, msg=None) + + Raised by :meth:`Process.wait` method if timeout expires and the process is + still alive. + *name* attribute is available if :meth:`Process.name()` was previously called. + +Process class +^^^^^^^^^^^^^ + +.. class:: Process(pid=None) + + Represents an OS process with the given *pid*. + If *pid* is omitted current process *pid* (`os.getpid`_) is used. + Raise :class:`NoSuchProcess` if *pid* does not exist. + On Linux *pid* can also refer to a thread ID (the *id* field returned by + :meth:`threads` method). + When accessing methods of this class always be prepared to catch + :class:`NoSuchProcess` and :class:`AccessDenied` exceptions. + `hash`_ builtin can be used against instances of this class in order to + identify a process univocally over time (the hash is determined by mixing + process PID + creation time). As such it can also be used with `set`_. + + .. note:: + + In order to efficiently fetch more than one information about the process + at the same time, make sure to use either :meth:`oneshot` context manager + or :meth:`as_dict` utility method. + + .. note:: + + the way this class is bound to a process is via its **PID**. + That means that if the process terminates and the OS reuses its PID you may + inadvertently end up querying another process. To prevent this problem + you can use :meth:`is_running()` first. + The only methods which preemptively check whether PID has been reused + (via PID + creation time) are: + :meth:`nice` (set), + :meth:`ionice` (set), + :meth:`cpu_affinity` (set), + :meth:`rlimit` (set), + :meth:`children`, + :meth:`ppid`, + :meth:`parent`, + :meth:`parents`, + :meth:`suspend` + :meth:`resume`, + :meth:`send_signal`, + :meth:`terminate` and + :meth:`kill`. + + .. method:: oneshot() + + Utility context manager which considerably speeds up the retrieval of + multiple process information at the same time. + Internally different process info (e.g. :meth:`name`, :meth:`ppid`, + :meth:`uids`, :meth:`create_time`, ...) may be fetched by using the same + routine, but only one value is returned and the others are discarded. + When using this context manager the internal routine is executed once (in + the example below on :meth:`name()`) the value of interest is returned and + the others are cached. + The subsequent calls sharing the same internal routine will return the + cached value. + The cache is cleared when exiting the context manager block. + The advice is to use this every time you retrieve more than one information + about the process. If you're lucky, you'll get a hell of a speedup. + Example: + + >>> import psutil + >>> p = psutil.Process() + >>> with p.oneshot(): + ... p.name() # execute internal routine once collecting multiple info + ... p.cpu_times() # return cached value + ... p.cpu_percent() # return cached value + ... p.create_time() # return cached value + ... p.ppid() # return cached value + ... p.status() # return cached value + ... + >>> + + Here's a list of methods which can take advantage of the speedup depending + on what platform you're on. + In the table below horizontal empty rows indicate what process methods can + be efficiently grouped together internally. + The last column (speedup) shows an approximation of the speedup you can get + if you call all the methods together (best case scenario). + + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | Linux | Windows | macOS | BSD | SunOS | AIX | + +==============================+===============================+==============================+==============================+==========================+==========================+ + | :meth:`cpu_num` | :meth:`~Process.cpu_percent` | :meth:`~Process.cpu_percent` | :meth:`cpu_num` | :meth:`name` | :meth:`name` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`~Process.cpu_percent` | :meth:`cpu_times` | :meth:`cpu_times` | :meth:`~Process.cpu_percent` | :meth:`cmdline` | :meth:`cmdline` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`cpu_times` | :meth:`io_counters()` | :meth:`memory_info` | :meth:`cpu_times` | :meth:`create_time` | :meth:`create_time` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`create_time` | :meth:`memory_info` | :meth:`memory_percent` | :meth:`create_time` | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`name` | :meth:`memory_info_ex` | :meth:`num_ctx_switches` | :meth:`gids` | :meth:`memory_info` | :meth:`memory_info` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`ppid` | :meth:`num_ctx_switches` | :meth:`num_threads` | :meth:`io_counters` | :meth:`memory_percent` | :meth:`memory_percent` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`status` | :meth:`num_handles` | | :meth:`name` | :meth:`num_threads` | :meth:`num_threads` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`terminal` | :meth:`num_threads` | :meth:`create_time` | :meth:`memory_info` | :meth:`ppid` | :meth:`ppid` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | | | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`gids` | :meth:`exe` | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`memory_info_ex` | :meth:`name` | :meth:`ppid` | :meth:`ppid` | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`num_ctx_switches` | | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`num_threads` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`uids` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`username` | | :meth:`uids` | :meth:`uids` | :meth:`username` | :meth:`username` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | | | :meth:`username` | :meth:`username` | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`memory_footprint` | | | | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`memory_maps` | | | | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | *speedup: +2.6x* | *speedup: +1.8x / +6.5x* | *speedup: +1.9x* | *speedup: +2.0x* | *speedup: +1.3x* | *speedup: +1.3x* | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + + .. versionadded:: 5.0.0 + + .. attribute:: pid + + The process PID. This is the only (read-only) attribute of the class. + + .. method:: ppid() + + The process parent PID. On Windows the return value is cached after first + call. Not on POSIX because ppid may change if process becomes a zombie + See also :meth:`parent` and :meth:`parents` methods. + + .. method:: name() + + The process name. On Windows the return value is cached after first + call. Not on POSIX because the process name may change. + See also how to `find a process by name <#find-process-by-name>`__. + + .. method:: exe() + + The process executable as an absolute path. On some systems, if exe cannot + be determined for some internal reason (e.g. system process or path no + longer exists), this may be an empty string. The return value is cached + after first call. + + >>> import psutil + >>> psutil.Process().exe() + '/usr/bin/python3' + + .. method:: cmdline() + + The command line this process has been called with as a list of strings. + The return value is not cached because the cmdline of a process may change. + + >>> import psutil + >>> psutil.Process().cmdline() + ['python', 'manage.py', 'runserver'] + + .. method:: environ() + + The environment variables of the process as a dict. Note: this might not + reflect changes made after the process started. + + >>> import psutil + >>> psutil.Process().environ() + {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} + + .. note:: + on macOS Big Sur this function returns something meaningful only for the + current process or in + `other specific circumstances `__). + + .. versionadded:: 4.0.0 + .. versionchanged:: 5.3.0 added SunOS support + .. versionchanged:: 5.6.3 added AIX support + .. versionchanged:: 5.7.3 added BSD support + + .. method:: create_time() + + The process creation time as a floating point number expressed in seconds + since the epoch (seconds since January 1, 1970, at midnight UTC). The + return value, which is cached after first call, is based on the system + clock, which means it may be affected by changes such as manual adjustments + or time synchronization (e.g. NTP). + + >>> import psutil, datetime + >>> p = psutil.Process() + >>> p.create_time() + 1307289803.47 + >>> datetime.datetime.fromtimestamp(p.create_time()).strftime("%Y-%m-%d %H:%M:%S") + '2011-03-05 18:03:52' + + .. method:: as_dict(attrs=None, ad_value=None) + + Utility method retrieving multiple process information as a dictionary. + If *attrs* is specified it must be a list of strings reflecting available + :class:`Process` class's attribute names. Here's a list of possible string + values: + ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_footprint'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_info_ex'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. + If *attrs* argument is not passed all public read only attributes are + assumed. + *ad_value* is the value which gets assigned to a dict key in case + :class:`AccessDenied` or :class:`ZombieProcess` exception is raised when + retrieving that particular process information. + Internally, :meth:`as_dict` uses :meth:`oneshot` context manager so + there's no need you use it also. + + >>> import psutil + >>> p = psutil.Process() + >>> p.as_dict(attrs=['pid', 'name', 'username']) + {'username': 'giampaolo', 'pid': 12366, 'name': 'python'} + >>> + >>> # get a list of valid attrs names + >>> list(psutil.Process().as_dict().keys()) + ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] + + .. versionchanged:: + 3.0.0 *ad_value* is used also when incurring into + :class:`ZombieProcess` exception, not only :class:`AccessDenied` + + .. versionchanged:: 4.5.0 :meth:`as_dict` is considerably faster thanks + to :meth:`oneshot` context manager. + + .. method:: parent() + + Utility method which returns the parent process as a :class:`Process` + object, preemptively checking whether PID has been reused. If no parent + PID is known return ``None``. + See also :meth:`ppid` and :meth:`parents` methods. + + .. method:: parents() + + Utility method which return the parents of this process as a list of + :class:`Process` instances. If no parents are known return an empty list. + See also :meth:`ppid` and :meth:`parent` methods. + + .. versionadded:: 5.6.0 + + .. method:: status() + + The current process status as a :class:`psutil.ProcessStatus` enum member. + The returned value is one of the + `psutil.STATUS_* <#process-status-constants>`_ constants. + + .. versionchanged:: 8.0.0 return value is now a :class:`psutil.ProcessStatus` + enum member instead of a plain ``str``. + + .. method:: cwd() + + The process current working directory as an absolute path. If cwd cannot be + determined for some internal reason (e.g. system process or directory no + longer exists) it may return an empty string. + + .. versionchanged:: 5.6.4 added support for NetBSD + + .. method:: username() + + The name of the user that owns the process. On UNIX this is calculated by + using real process uid. + + .. method:: uids() + + The real, effective and saved user ids of this process as a named tuple. + This is the same as `os.getresuid`_ but can be used for any process PID. + + Availability: UNIX + + .. method:: gids() + + The real, effective and saved group ids of this process as a named tuple. + This is the same as `os.getresgid`_ but can be used for any process PID. + + Availability: UNIX + + .. method:: terminal() + + The terminal associated with this process, if any, else ``None``. This is + similar to "tty" command but can be used for any process PID. + + Availability: UNIX + + .. method:: nice(value=None) + + Get or set process niceness (priority). + On UNIX this is a number which usually goes from ``-20`` to ``20``. + The higher the nice value, the lower the priority of the process. + + >>> import psutil + >>> p = psutil.Process() + >>> p.nice(10) # set + >>> p.nice() # get + 10 + >>> + + Starting from Python 3.3 this functionality is also available as + `os.getpriority`_ and `os.setpriority`_ (see `BPO-10784`_). + On Windows this is implemented via `GetPriorityClass`_ and + `SetPriorityClass`_ Windows APIs and *value* is one of the + :data:`psutil.*_PRIORITY_CLASS ` + constants reflecting the MSDN documentation. + The return value on Windows is a :class:`psutil.ProcessPriority` enum member. + Example which increases process priority on Windows: + + >>> p.nice(psutil.HIGH_PRIORITY_CLASS) + + .. versionchanged:: 8.0.0 on Windows, return value is now a + :class:`psutil.ProcessPriority` enum member. + + .. method:: ionice(ioclass=None, value=None) + + Get or set process I/O niceness (priority). + If no argument is provided it acts as a get, returning a ``(ioclass, value)`` + tuple on Linux and a *ioclass* integer on Windows. + If *ioclass* is provided it acts as a set. In this case an additional + *value* can be specified on Linux only in order to increase or decrease the + I/O priority even further. + Here's the possible platform-dependent *ioclass* values. + + Linux (see `ioprio_get`_ manual): + + * ``IOPRIO_CLASS_RT``: (high) the process gets first access to the disk + every time. Use it with care as it can starve the entire + system. Additional priority *level* can be specified and ranges from + ``0`` (highest) to ``7`` (lowest). + * ``IOPRIO_CLASS_BE``: (normal) the default for any process that hasn't set + a specific I/O priority. Additional priority *level* ranges from + ``0`` (highest) to ``7`` (lowest). + * ``IOPRIO_CLASS_IDLE``: (low) get I/O time when no-one else needs the disk. + No additional *value* is accepted. + * ``IOPRIO_CLASS_NONE``: returned when no priority was previously set. + + Windows: + + * ``IOPRIO_HIGH``: highest priority. + * ``IOPRIO_NORMAL``: default priority. + * ``IOPRIO_LOW``: low priority. + * ``IOPRIO_VERYLOW``: lowest priority. + + Here's an example on how to set the highest I/O priority depending on what + platform you're on:: + + >>> import psutil + >>> p = psutil.Process() + >>> if psutil.LINUX: + ... p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + ... else: + ... p.ionice(psutil.IOPRIO_HIGH) + ... + >>> p.ionice() # get + pionice(ioclass=, value=7) + + Availability: Linux, Windows Vista+ + + .. versionchanged:: 5.6.2 Windows accepts new ``IOPRIO_*`` constants. + + .. versionchanged:: 8.0.0 *ioclass* is now a + :class:`psutil.ProcessIOPriority` enum member. + + .. method:: rlimit(resource, limits=None) + + Get or set process resource limits (see `man prlimit`_). *resource* is one + of the `psutil.RLIMIT_* <#process-resources-constants>`_ constants. + *limits* is a ``(soft, hard)`` tuple. + This is the same as `resource.getrlimit`_ and `resource.setrlimit`_ + but can be used for any process PID, not only `os.getpid`_. + For get, return value is a ``(soft, hard)`` tuple. Each value may be either + and integer or :data:`psutil.RLIMIT_* `. + Example: + + >>> import psutil + >>> p = psutil.Process() + >>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128)) # process can open max 128 file descriptors + >>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024)) # can create files no bigger than 1024 bytes + >>> p.rlimit(psutil.RLIMIT_FSIZE) # get + (1024, 1024) + >>> + + Also see `procinfo.py`_ script. + + Availability: Linux, FreeBSD + + .. versionchanged:: 5.7.3 added FreeBSD support + + .. method:: io_counters() + + Return process I/O statistics as a named tuple. + For Linux you can refer to + `/proc filesystem documentation `__. + + - **read_count**: the number of read operations performed (cumulative). + This is supposed to count the number of read-related syscalls such as + ``read()`` and ``pread()`` on UNIX. + - **write_count**: the number of write operations performed (cumulative). + This is supposed to count the number of write-related syscalls such as + ``write()`` and ``pwrite()`` on UNIX. + - **read_bytes**: the number of bytes read (cumulative). + Always ``-1`` on BSD. + - **write_bytes**: the number of bytes written (cumulative). + Always ``-1`` on BSD. + + Linux specific: + + - **read_chars** *(Linux)*: the amount of bytes which this process passed + to ``read()`` and ``pread()`` syscalls (cumulative). + Differently from *read_bytes* it doesn't care whether or not actual + physical disk I/O occurred. + - **write_chars** *(Linux)*: the amount of bytes which this process passed + to ``write()`` and ``pwrite()`` syscalls (cumulative). + Differently from *write_bytes* it doesn't care whether or not actual + physical disk I/O occurred. + + Windows specific: + + - **other_count** *(Windows)*: the number of I/O operations performed + other than read and write operations. + - **other_bytes** *(Windows)*: the number of bytes transferred during + operations other than read and write operations. + + >>> import psutil + >>> p = psutil.Process() + >>> p.io_counters() + pio(read_count=454556, write_count=3456, read_bytes=110592, write_bytes=0, read_chars=769931, write_chars=203) + + Availability: Linux, BSD, Windows, AIX + + .. versionchanged:: 5.2.0 added *read_chars* and *write_chars* on Linux; + added *other_count* and *other_bytes* on Windows. + + .. method:: num_ctx_switches() + + The number voluntary and involuntary context switches performed by + this process (cumulative). + + .. note:: + (Windows, macOS) *involuntary* value is always set to 0, while + *voluntary* value reflect the total number of context switches (voluntary + + involuntary). This is a limitation of the OS. + + .. versionchanged:: 5.4.1 added AIX support + + .. method:: num_fds() + + The number of file descriptors currently opened by this process + (non cumulative). + + Availability: UNIX + + .. method:: num_handles() + + The number of handles currently used by this process (non cumulative). + + Availability: Windows + + .. method:: num_threads() + + The number of threads currently used by this process (non cumulative). + + .. method:: threads() + + Return threads opened by process as a list of named tuples. On OpenBSD this + method requires root privileges. + + - **id**: the native thread ID assigned by the kernel. If :attr:`pid` refers + to the current process, this matches the + `native_id `__ + attribute of the `threading.Thread`_ class, and can be used to reference + individual Python threads running within your own Python app. + - **user_time**: time spent in user mode. + - **system_time**: time spent in kernel mode. + + .. method:: cpu_times() + + Return a named tuple representing the accumulated process times, in seconds + (see `explanation `__). + This is similar to `os.times`_ but can be used for any process PID. + + - **user**: time spent in user mode. + - **system**: time spent in kernel mode. + - **children_user**: user time of all child processes (always ``0`` on + Windows and macOS). + - **children_system**: system time of all child processes (always ``0`` on + Windows and macOS). + - **iowait**: (Linux) time spent waiting for blocking I/O to complete. + This value is excluded from `user` and `system` times count (because the + CPU is not doing any work). + + >>> import psutil + >>> p = psutil.Process() + >>> p.cpu_times() + pcputimes(user=0.03, system=0.67, children_user=0.0, children_system=0.0, iowait=0.08) + >>> sum(p.cpu_times()[:2]) # cumulative, excluding children and iowait + 0.70 + + .. versionchanged:: + 4.1.0 return two extra fields: *children_user* and *children_system*. + + .. versionchanged:: + 5.6.4 added *iowait* on Linux. + + .. method:: cpu_percent(interval=None) + + Return a float representing the process CPU utilization as a percentage + which can also be ``> 100.0`` in case of a process running multiple threads + on different CPUs. + When *interval* is > ``0.0`` compares process times to system CPU times + elapsed before and after the interval (blocking). When interval is ``0.0`` + or ``None`` compares process times to system CPU times elapsed since last + call, returning immediately. That means the first time this is called it + will return a meaningless ``0.0`` value which you are supposed to ignore. + In this case is recommended for accuracy that this function be called a + second time with at least ``0.1`` seconds between calls. + Example: + + >>> import psutil + >>> p = psutil.Process() + >>> # blocking + >>> p.cpu_percent(interval=1) + 2.0 + >>> # non-blocking (percentage since last call) + >>> p.cpu_percent(interval=None) + 2.9 + + .. note:: + the returned value can be > 100.0 in case of a process running multiple + threads on different CPU cores. + + .. note:: + the returned value is explicitly *not* split evenly between all available + CPUs (differently from :func:`psutil.cpu_percent()`). + This means that a busy loop process running on a system with 2 logical + CPUs will be reported as having 100% CPU utilization instead of 50%. + This was done in order to be consistent with ``top`` UNIX utility + and also to make it easier to identify processes hogging CPU resources + independently from the number of CPUs. + It must be noted that ``taskmgr.exe`` on Windows does not behave like + this (it would report 50% usage instead). + To emulate Windows ``taskmgr.exe`` behavior you can do: + ``p.cpu_percent() / psutil.cpu_count()``. + + .. warning:: + the first time this method is called with interval = ``0.0`` or + ``None`` it will return a meaningless ``0.0`` value which you are + supposed to ignore. + + .. method:: cpu_affinity(cpus=None) + + Get or set process current + `CPU affinity `__. + CPU affinity consists in telling the OS to run a process on a limited set + of CPUs only (on Linux cmdline, ``taskset`` command is typically used). + If no argument is passed it returns the current CPU affinity as a list + of integers. + If passed it must be a list of integers specifying the new CPUs affinity. + If an empty list is passed all eligible CPUs are assumed (and set). + On some systems such as Linux this may not necessarily mean all available + logical CPUs as in ``list(range(psutil.cpu_count()))``). + + >>> import psutil + >>> psutil.cpu_count() + 4 + >>> p = psutil.Process() + >>> # get + >>> p.cpu_affinity() + [0, 1, 2, 3] + >>> # set; from now on, process will run on CPU #0 and #1 only + >>> p.cpu_affinity([0, 1]) + >>> p.cpu_affinity() + [0, 1] + >>> # reset affinity against all eligible CPUs + >>> p.cpu_affinity([]) + + Availability: Linux, Windows, FreeBSD + + .. versionchanged:: 2.2.0 added support for FreeBSD + .. versionchanged:: 5.1.0 an empty list can be passed to set affinity + against all eligible CPUs. + + .. method:: cpu_num() + + Return what CPU this process is currently running on. + The returned number should be ``<=`` :func:`psutil.cpu_count()`. + On FreeBSD certain kernel process may return ``-1``. + It may be used in conjunction with ``psutil.cpu_percent(percpu=True)`` to + observe the system workload distributed across multiple CPUs as shown by + `cpu_distribution.py`_ example script. + + Availability: Linux, FreeBSD, SunOS + + .. versionadded:: 5.1.0 + + .. method:: memory_info() + + Return a named tuple with variable fields depending on the platform + representing memory information about the process. + The "portable" fields available on all platforms are `rss` and `vms`. + All numbers are expressed in bytes. + + +---------+---------+----------+---------+-----+-----------------+ + | Linux | macOS | BSD | Solaris | AIX | Windows | + +=========+=========+==========+=========+=====+=================+ + | rss | rss | rss | rss | rss | rss | + +---------+---------+----------+---------+-----+-----------------+ + | vms | vms | vms | vms | vms | vms | + +---------+---------+----------+---------+-----+-----------------+ + | shared | | text | | | | + +---------+---------+----------+---------+-----+-----------------+ + | text | | data | | | | + +---------+---------+----------+---------+-----+-----------------+ + | data | | stack | | | | + +---------+---------+----------+---------+-----+-----------------+ + | | | peak_rss | | | peak_rss | + +---------+---------+----------+---------+-----+-----------------+ + | | | | | | peak_vms | + +---------+---------+----------+---------+-----+-----------------+ + + - **rss**: aka "Resident Set Size". The portion of physical memory + currently held by this process (code, data, stack, and mapped files that + are resident). Pages swapped out to disk are **not** counted. On UNIX it + matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. + + - **vms**: aka "Virtual Memory Size". The total address space reserved by + the process, including pages not yet touched, pages in swap, and + memory-mapped files not yet accessed. Typically much larger than + **rss**. On UNIX it matches the ``top`` VIRT column. On Windows this + maps to ``PrivateUsage`` (private committed pages only), which differs + from the UNIX definition; use ``virtual`` from :meth:`memory_info_ex` + for the true virtual address space size. + + - **shared** *(Linux)*: memory backed by a file or device (shared + libraries, mmap'd files, POSIX shared memory) that *could* be shared + with other processes. A page is counted here even if no other process + is currently mapping it. Matches ``top``'s SHR column. + + - **text** *(Linux, BSD)*: aka TRS (Text Resident Set). Resident memory + devoted to executable code. These pages are read-only and typically + shared across all processes running the same binary. Matches ``top``'s + CODE column. + + - **data** *(Linux, BSD)*: aka DRS (Data Resident Set). On Linux this + covers the data **and** stack segments combined (from + ``/proc//statm``). On BSD it covers the data segment only (see + **stack**). Matches ``top``'s DATA column. + + - **stack** *(BSD)*: size of the process stack segment. Reported + separately from **data** (unlike Linux where both are combined). + + - **peak_rss** *(BSD, Windows)*: the highest RSS value (high water mark) + the process has ever reached. On BSD this may be ``0`` for kernel PIDs. + On Windows it maps to ``PeakWorkingSetSize``. + + - **peak_vms** *(Windows)*: peak private committed (page-file-backed) + virtual memory. Maps to ``PeakPagefileUsage``. + + For the full definitions of Windows fields see + `PROCESS_MEMORY_COUNTERS_EX`_. + + Example on Linux: + + >>> import psutil + >>> p = psutil.Process() + >>> p.memory_info() + pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, data=9891840) + + .. versionchanged:: + 4.0.0 multiple fields are returned, not only *rss* and *vms*. + + .. versionchanged:: + 8.0.0 Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). + Deprecated aliases returning 0 and emitting `DeprecationWarning` are + kept. + + .. versionchanged:: + 8.0.0 macOS: *pfaults* and *pageins* removed with **no backward-compat + aliases**. Use :meth:`page_faults` instead. + + .. versionchanged:: + 8.0.0 Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*, + *num_page_faults* → :meth:`page_faults` method. At the same time + *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, *peak_nonpaged_pool* + were moved to :meth:`memory_info_ex`. All these old names still work but + raise `DeprecationWarning`. + + .. versionchanged:: + 8.0.0 BSD: added *peak_rss*. + + .. warning:: + in version 8.0.0 the named tuple changed size and field order. Positional + access (e.g. ``p.memory_info()[3]`` or ``a, b, c = p.memory_info()``) may + break or silently return the wrong field. Always use attribute access + instead (e.g. ``p.memory_info().rss``). + + .. method:: memory_info_ex() + + Return a named tuple extending :meth:`memory_info` with additional + platform-specific memory metrics. On platforms where extra fields are not + implemented this returns the same result as :meth:`memory_info`. All + numbers are expressed in bytes. + + +-------------+----------------+--------------------+ + | Linux | macOS | Windows | + +=============+================+====================+ + | peak_rss | peak_rss | virtual | + +-------------+----------------+--------------------+ + | peak_vms | | peak_virtual | + +-------------+----------------+--------------------+ + | rss_anon | rss_anon | paged_pool | + +-------------+----------------+--------------------+ + | rss_file | rss_file | nonpaged_pool | + +-------------+----------------+--------------------+ + | rss_shmem | wired | peak_paged_pool | + +-------------+----------------+--------------------+ + | swap | compressed | peak_nonpaged_pool | + +-------------+----------------+--------------------+ + | hugetlb | phys_footprint | | + +-------------+----------------+--------------------+ + + - **peak_rss** *(Linux, macOS)*: the highest RSS value (high water mark) + the process has reached since it started. + - **peak_vms** *(Linux)*: the highest VMS value the process has reached + since it started. + - **rss_anon** *(Linux, macOS)*: resident anonymous pages (heap, + stack, private mappings) not backed by any file, such as heap + allocations, stack, and private ``mmap(MAP_ANONYMOUS)`` regions. Set to 0 + on Linux < 4.5. + - **rss_file** *(Linux, macOS)*: resident file-backed memory; pages mapped + from files (shared libraries, mmap'd files). Set to 0 on Linux < 4.5. + - **rss_shmem** *(Linux)*: resident shared memory pages (``tmpfs``, + ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals **rss**. Set to + 0 on Linux < 4.5. + - **wired** *(macOS)*: memory pinned in RAM by the kernel on behalf of this + process; cannot be compressed or paged out. + - **swap** *(Linux)*: process memory currently in swap. Equivalent to + ``memory_footprint().swap`` but cheaper, as it reads from + ``/proc//status`` instead of ``/proc//smaps``. + - **compressed** *(macOS)*: pages held in the in-RAM memory compressor; not + counted in **rss**. A large value signals memory pressure but has not yet + triggered swapping. + - **hugetlb** *(Linux)*: resident memory backed by huge pages. Set to 0 on + Linux < 4.4. + - **phys_footprint** *(macOS)*: total physical memory impact including + compressed pages. What Xcode and ``footprint(1)`` report; prefer this + over **rss** macOS memory monitoring. + - **virtual** *(Windows)*: true virtual address space size, including + reserved-but-uncommitted regions (unlike **vms** in + :meth:`memory_info`). + - **peak_virtual** *(Windows)*: peak virtual address space size. + - **paged_pool** *(Windows)*: kernel memory used for objects created by + this process (open file handles, registry keys, etc.) that the OS may + swap to disk under memory pressure. + - **nonpaged_pool** *(Windows)*: kernel memory used for objects that must + stay in RAM at all times (I/O request packets, device driver buffers, + etc.). A large or growing value may indicate a driver memory leak. + - **peak_paged_pool** *(Windows)*: peak paged-pool usage. + - **peak_nonpaged_pool** *(Windows)*: peak non-paged-pool usage. + + For the full definitions of Windows fields see + `PROCESS_MEMORY_COUNTERS_EX`_. + + .. versionadded:: 8.0.0 + + .. method:: memory_footprint() + + Return a named tuple with USS, PSS and swap memory metrics. These give + a more accurate picture of actual memory consumption than + :meth:`memory_info`, as explained in this + `blog post `__. + It works by walking the full process address space, so it is + considerably slower than :meth:`memory_info` and may require elevated + privileges. + + - **uss** *(Linux, macOS, Windows)*: aka "Unique Set Size". This is the + memory which is unique to a process and which would be freed if the + process were terminated right now. The most representative metric for + actual memory usage. + + - **pss** *(Linux)*: aka "Proportional Set Size", is the amount of memory + shared with other processes, accounted in a way that the amount is + divided evenly between the processes that share it. I.e. if a process has + 10 MBs all to itself, and 10 MBs shared with another process, its PSS + will be 15 MBs. + + - **swap** *(Linux)*: process memory currently in swap, counted per-mapping + (slower, but may be more accurate than ``memory_info_ex().swap``). + + Example on Linux: + + >>> import psutil + >>> p = psutil.Process() + >>> p.memory_footprint() + pfootprint(uss=6545408, pss=6872064, swap=0) + + See also `procsmem.py`_ for an example application. + + .. versionadded:: 8.0.0 + + Availability: Linux, macOS, Windows + + .. method:: memory_full_info() + + This method returns the same information as :meth:`memory_info` plus + :meth:`memory_footprint` in a single named tuple. + + .. versionadded:: 4.0.0 + + .. warning:: + deprecated in version 8.0.0; use :meth:`memory_footprint` instead. + + .. method:: memory_percent(memtype="rss") + + Return process memory usage as a percentage of total physical memory + (``process.memory_info().rss / virtual_memory().total * 100``). + *memtype* can be any field name from :meth:`memory_info`, + :meth:`memory_info_ex`, or :meth:`memory_footprint` and controls which + memory value is used in the calculation (defaults to ``"rss"``). + + .. versionchanged:: 4.0.0 added `memtype` parameter. + + .. method:: memory_maps(grouped=True) + + Return process's mapped memory regions as a list of named tuples whose + fields vary by platform (all values in bytes). If *grouped* is ``True`` + regions with the same *path* are merged and their numeric fields summed. + If *grouped* is ``False`` each region is listed individually and the + tuple also includes *addr* (address range) and *perms* (permission + string e.g. ``"r-xp"``). + See `pmap.py`_ for an example application. + + +---------------+---------+--------------+-----------+ + | Linux | Windows | FreeBSD | Solaris | + +===============+=========+==============+===========+ + | rss | rss | rss | rss | + +---------------+---------+--------------+-----------+ + | size | | private | anonymous | + +---------------+---------+--------------+-----------+ + | pss | | ref_count | locked | + +---------------+---------+--------------+-----------+ + | shared_clean | | shadow_count | | + +---------------+---------+--------------+-----------+ + | shared_dirty | | | | + +---------------+---------+--------------+-----------+ + | private_clean | | | | + +---------------+---------+--------------+-----------+ + | private_dirty | | | | + +---------------+---------+--------------+-----------+ + | referenced | | | | + +---------------+---------+--------------+-----------+ + | anonymous | | | | + +---------------+---------+--------------+-----------+ + | swap | | | | + +---------------+---------+--------------+-----------+ + + Linux fields (from ``/proc//smaps``): + + - **rss**: resident pages in this mapping. + - **size**: total virtual size; may far exceed **rss** for sparse or + reserved-but-unaccessed mappings. + - **pss**: proportional RSS. **rss** divided by the number of processes + sharing this mapping. Useful for fair per-process accounting. + - **shared_clean**: shared pages not modified (e.g. shared library code); + can be dropped from RAM without writing to swap. + - **shared_dirty**: shared pages that have been written to. + - **private_clean**: private unmodified pages; can be dropped without + writing to swap. + - **private_dirty**: private modified pages; must be written to swap + before they can be reclaimed. The key indicator of a mapping's real + memory cost. + - **referenced**: pages recently accessed. + - **anonymous**: pages not backed by a file (heap, stack allocations). + - **swap**: pages from this mapping currently in swap. + + FreeBSD fields: + + - **private**: pages in this mapping private to this process. + - **ref_count**: reference count on the VM object backing this mapping. + - **shadow_count**: depth of the copy-on-write shadow object chain. + + >>> import psutil + >>> p = psutil.Process() + >>> p.memory_maps() + [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), + pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), + ...] + + Availability: Linux, Windows, FreeBSD, SunOS + + .. versionchanged:: + 5.6.0 removed macOS support because inherently broken (see + issue `#1291 `__) + + .. method:: children(recursive=False) + + Return the children of this process as a list of :class:`Process` + instances. + If recursive is `True` return all the parent descendants. + Pseudo code example assuming *A == this process*: + :: + + A ─┐ + │ + ├─ B (child) ─┐ + │ └─ X (grandchild) ─┐ + │ └─ Y (great grandchild) + ├─ C (child) + └─ D (child) + + >>> p.children() + B, C, D + >>> p.children(recursive=True) + B, X, Y, C, D + + Note that in the example above if process X disappears process Y won't be + returned either as the reference to process A is lost. + This concept is well summaried by this + `unit test `__. + See also how to `kill a process tree <#kill-process-tree>`__ and + `terminate my children <#terminate-my-children>`__. + + .. method:: page_faults() + + Return the number of page faults for this process as a ``(minor, major)`` + named tuple. + + - **minor** (a.k.a. *soft* faults): occur when a memory page is not + currently mapped into the process address space, but is already present + in physical RAM (e.g. a shared library loaded by another process). The + kernel resolves these without disk I/O. + - **major** (a.k.a. *hard* faults): occur when the page must be fetched + from disk. These are expensive because they stall the process until I/O + completes. + + Both counters are cumulative since process creation. Example:: + + >>> import psutil + >>> p = psutil.Process() + >>> p.page_faults() + ppagefaults(minor=5905, major=3) + + .. versionadded:: 8.0.0 + + .. method:: open_files() + + Return regular files opened by process as a list of named tuples including + the following fields: + + - **path**: the absolute file name. + - **fd**: the file descriptor number; on Windows this is always ``-1``. + + Linux only: + + - **position** (*Linux*): the file (offset) position. + - **mode** (*Linux*): a string indicating how the file was opened, similarly + to `open`_ builtin ``mode`` argument. + Possible values are ``'r'``, ``'w'``, ``'a'``, ``'r+'`` and ``'a+'``. + There's no distinction between files opened in binary or text mode + (``"b"`` or ``"t"``). + - **flags** (*Linux*): the flags which were passed to the underlying + `os.open`_ C call when the file was opened (e.g. `os.O_RDONLY`_, + `os.O_TRUNC`_, etc). + + >>> import psutil + >>> f = open('file.ext', 'w') + >>> p = psutil.Process() + >>> p.open_files() + [popenfile(path='/home/giampaolo/svn/psutil/file.ext', fd=3, position=0, mode='w', flags=32769)] + + .. warning:: + on Windows this method is not reliable due to some limitations of the + underlying Windows API which may hang when retrieving certain file + handles. + In order to work around that psutil spawns a thread to determine the file + handle name and kills it if it's not responding after 100ms. + That implies that this method on Windows is not guaranteed to enumerate + all regular file handles (see + `issue 597 `_). + Tools like ProcessHacker has the same limitation. + + .. warning:: + on BSD this method can return files with a null path ("") due to a + kernel bug, hence it's not reliable + (see `issue 595 `_). + + .. versionchanged:: + 3.1.0 no longer hangs on Windows. + + .. versionchanged:: + 4.1.0 new *position*, *mode* and *flags* fields on Linux. + + .. method:: net_connections(kind="inet") + + Return socket connections opened by process as a list of named tuples. + To get system-wide connections use :func:`psutil.net_connections()`. + Every named tuple provides 6 attributes: + + - **fd**: the socket file descriptor. If the connection refers to the + current process this may be passed to `socket.fromfd`_ to obtain a usable + socket object. + On Windows, FreeBSD and SunOS this is always set to ``-1``. + - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or + `AF_UNIX`_. + - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or + `SOCK_SEQPACKET`_. . + - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` + in case of AF_UNIX sockets. For UNIX sockets see notes below. + - **raddr**: the remote address as a ``(ip, port)`` named tuple or an + absolute ``path`` in case of UNIX sockets. + When the remote endpoint is not connected you'll get an empty tuple + (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. + - **status**: represents the status of a TCP connection. The return value + is one of the :data:`psutil.CONN_* ` constants. + For UDP and UNIX sockets this is always going to be + :const:`psutil.CONN_NONE`. + + The *kind* parameter is a string which filters for connections that fit the + following criteria: + + +----------------+-----------------------------------------------------+ + | **Kind value** | **Connections using** | + +================+=====================================================+ + | ``"inet"`` | IPv4 and IPv6 | + +----------------+-----------------------------------------------------+ + | ``"inet4"`` | IPv4 | + +----------------+-----------------------------------------------------+ + | ``"inet6"`` | IPv6 | + +----------------+-----------------------------------------------------+ + | ``"tcp"`` | TCP | + +----------------+-----------------------------------------------------+ + | ``"tcp4"`` | TCP over IPv4 | + +----------------+-----------------------------------------------------+ + | ``"tcp6"`` | TCP over IPv6 | + +----------------+-----------------------------------------------------+ + | ``"udp"`` | UDP | + +----------------+-----------------------------------------------------+ + | ``"udp4"`` | UDP over IPv4 | + +----------------+-----------------------------------------------------+ + | ``"udp6"`` | UDP over IPv6 | + +----------------+-----------------------------------------------------+ + | ``"unix"`` | UNIX socket (both UDP and TCP protocols) | + +----------------+-----------------------------------------------------+ + | ``"all"`` | the sum of all the possible families and protocols | + +----------------+-----------------------------------------------------+ + + Example: + + >>> import psutil + >>> p = psutil.Process(1694) + >>> p.name() + 'firefox' + >>> p.net_connections() + [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=), + pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=), + pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=)] + + .. warning:: + On Linux, retrieving connections for certain processes requires root + privileges. If psutil is not run as root, those connections are silently + skipped instead of raising :class:`psutil.AccessDenied`. That means + the returned list may be incomplete. + + .. note:: + (Solaris) UNIX sockets are not supported. + + .. note:: + (Linux, FreeBSD) *raddr* field for UNIX sockets is always set to "". + This is a limitation of the OS. + + .. note:: + (OpenBSD) *laddr* and *raddr* fields for UNIX sockets are always set to + "". This is a limitation of the OS. + + .. note:: + (AIX) :class:`psutil.AccessDenied` is always raised unless running + as root (lsof does the same). + + .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. + + .. versionchanged:: 6.0.0 : method renamed from `connections` to + `net_connections`. + + .. versionchanged:: 8.0.0 *status* field is now a + :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + + .. method:: connections() + + Same as :meth:`net_connections` (deprecated). + + .. warning:: + deprecated in version 6.0.0; use :meth:`net_connections` instead. + + .. method:: is_running() + + Return whether the current process is running in the current process list. + This is reliable also in case the process is gone and its PID reused by + another process, therefore it must be preferred over doing + ``psutil.pid_exists(p.pid)``. + If PID has been reused this method will also remove the process from + :func:`process_iter()` internal cache. + + .. note:: + this will return ``True`` also if the process is a zombie + (``p.status() == psutil.STATUS_ZOMBIE``). + + .. versionchanged:: 6.0.0 : automatically remove process from + :func:`process_iter()` internal cache if PID has been reused by another + process. + + .. method:: send_signal(signal) + + Send a signal to process (see `signal module`_ constants) preemptively + checking whether PID has been reused. + On UNIX this is the same as ``os.kill(pid, sig)``. + On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals + are supported and *SIGTERM* is treated as an alias for :meth:`kill()`. + See also how to `kill a process tree <#kill-process-tree>`__ and + `terminate my children <#terminate-my-children>`__. + + .. versionchanged:: + 3.2.0 support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows + was added. + + .. method:: suspend() + + Suspend process execution with *SIGSTOP* signal preemptively checking + whether PID has been reused. + On UNIX this is the same as ``os.kill(pid, signal.SIGSTOP)``. + On Windows this is done by suspending all process threads execution. + + .. method:: resume() + + Resume process execution with *SIGCONT* signal preemptively checking + whether PID has been reused. + On UNIX this is the same as ``os.kill(pid, signal.SIGCONT)``. + On Windows this is done by resuming all process threads execution. + + .. method:: terminate() + + Terminate the process with *SIGTERM* signal preemptively checking + whether PID has been reused. + On UNIX this is the same as ``os.kill(pid, signal.SIGTERM)``. + On Windows this is an alias for :meth:`kill`. + See also how to `kill a process tree <#kill-process-tree>`__ and + `terminate my children <#terminate-my-children>`__. + + .. method:: kill() + + Kill the current process by using *SIGKILL* signal preemptively + checking whether PID has been reused. + On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. + On Windows this is done by using `TerminateProcess`_. + See also how to `kill a process tree <#kill-process-tree>`__ and + `terminate my children <#terminate-my-children>`__. + + .. method:: wait(timeout=None) + + Wait for a process PID to terminate. The details about the return value + differ on UNIX and Windows. + + *On UNIX*: if the process terminated normally, the return value is a + positive integer >= 0 indicating the exit code. + If the process was terminated by a signal return the negated value of the + signal which caused the termination (e.g. ``-SIGTERM``). + If PID is not a child of `os.getpid`_ (current process) just wait until + the process disappears and return ``None``. + If PID does not exist return ``None`` immediately. + + *On Windows*: always return the exit code, which is a positive integer as + returned by `GetExitCodeProcess`_. + + *timeout* is expressed in seconds. If specified and the process is still + alive raise :class:`TimeoutExpired` exception. + ``timeout=0`` can be used in non-blocking apps: it will either return + immediately or raise :class:`TimeoutExpired`. + + The return value is cached. + To wait for multiple processes use :func:`psutil.wait_procs()`. + + >>> import psutil + >>> p = psutil.Process(9891) + >>> p.terminate() + >>> p.wait() + + + .. note:: + + When ``timeout`` is not ``None`` and the platform supports it, an + efficient event-driven mechanism is used to wait for process termination: + + - Linux >= 5.3 with Python >= 3.9 uses `os.pidfd_open`_ + `select.poll`_ + - macOS and other BSD variants use `select.kqueue`_ + ``KQ_FILTER_PROC`` + + ``KQ_NOTE_EXIT`` + - Windows uses ``WaitForSingleObject`` + + If none of these mechanisms are available, the function falls back to a + busy loop (non-blocking call and short sleeps). + + .. versionchanged:: 5.7.2 + if *timeout* is not ``None``, use efficient event-driven implementation + on Linux >= 5.3 and macOS / BSD. + + .. versionchanged:: 5.7.1 return value is cached (instead of returning + ``None``). + + .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it + as a human readable `enum`_. + + .. versionchanged:: 7.2.2 on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, + use `os.pidfd_open`_ and `select.kqueue`_ respectively, instead of less + efficient busy-loop polling. + +---- + +Popen class +^^^^^^^^^^^ + +.. class:: Popen(*args, **kwargs) + + Same as `subprocess.Popen`_ but in addition it provides all + :class:`psutil.Process` methods in a single class. + For the following methods which are common to both classes, psutil + implementation takes precedence: + :meth:`send_signal() `, + :meth:`terminate() `, + :meth:`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 + >>> + >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) + >>> p.name() + 'python' + >>> p.username() + 'giampaolo' + >>> p.communicate() + ('hello\n', None) + >>> p.wait(timeout=2) + 0 + >>> + + .. versionchanged:: 4.4.0 added context manager support + +---- + +C heap introspection +-------------------- + +The following functions provide direct access to the platform's native C heap +allocator (such as glibc's ``malloc`` on Linux or ``jemalloc`` on BSD). They +are low-level interfaces intended for detecting memory leaks in C extensions, +which are usually not revealed via standard RSS/VMS metrics. These functions do +not reflect Python object memory; they operate solely on allocations made in C +via ``malloc()``, ``free()``, and related calls. + +The general idea behind these functions is straightforward: capture the state +of the C heap before and after repeatedly invoking a function implemented in a +C extension, and compare the results. If ``heap_used`` or ``mmap_used`` grows +steadily across iterations, the C code is likely retaining memory it should be +releasing. This provides an allocator-level way to spot native leaks that +Python's memory tracking misses. + +Check out `psleak`_ project to see a practical example of how these APIs can be +used to detect memory leaks in C extensions. + +.. function:: heap_info() + + Return low-level heap statistics from the system's C allocator. On Linux, + this exposes ``uordblks`` and ``hblkhd`` fields from glibc's `mallinfo2`_. + Returns a named tuple containing: + + - ``heap_used``: total number of bytes currently allocated via ``malloc()`` + (small allocations). + - ``mmap_used``: total number of bytes currently allocated via ``mmap()`` or + via large ``malloc()`` allocations. Always set to 0 on macOS. + - ``heap_count``: (Windows only) number of private heaps created via + ``HeapCreate()``. + + >>> import psutil + >>> psutil.heap_info() + pheap(heap_used=5177792, mmap_used=819200) + + These fields reflect how unreleased C allocations affect the heap: + + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Platform | Allocation type | Affected field | + +===============+====================================================================================+=================+ + | UNIX / glibc | small ``malloc()`` ≤128KB without ``free()`` | ``heap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | UNIX / glibc | large ``malloc()`` >128KB without ``free()`` , or ``mmap()`` without ``munmap()`` | ``mmap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Windows | ``HeapAlloc()`` without ``HeapFree()`` | ``heap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Windows | ``VirtualAlloc()`` without ``VirtualFree()`` | ``mmap_used`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + | Windows | ``HeapCreate()`` without ``HeapDestroy()`` | ``heap_count`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + + .. versionadded:: 7.2.0 + + Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD + +.. function:: heap_trim() + + Request that the underlying allocator free any unused memory it's holding in + the heap (typically small ``malloc()`` allocations). + + In practice, modern allocators rarely comply, so this is not a + general-purpose memory-reduction tool and won't meaningfully shrink RSS in + real programs. Its primary value is in **leak detection tools**. + + Calling ``heap_trim()`` before taking measurements helps reduce allocator + noise, giving you a cleaner baseline so that changes in ``heap_used`` come + from the code you're testing, not from internal allocator caching or + fragmentation. Its effectiveness depends on allocator behavior and + fragmentation patterns. + + .. versionadded:: 7.2.0 + + Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD + +---- + +Windows services +---------------- + +.. function:: win_service_iter() + + Return an iterator yielding a :class:`WindowsService` class instance for all + Windows services installed. + + .. versionadded:: 4.2.0 + + Availability: Windows + +.. function:: win_service_get(name) + + Get a Windows service by name, returning a :class:`WindowsService` instance. + Raise :class:`psutil.NoSuchProcess` if no service with such name exists. + + .. versionadded:: 4.2.0 + + Availability: Windows + +.. class:: WindowsService + + Represents a Windows service with the given *name*. This class is returned + by :func:`win_service_iter` and :func:`win_service_get` functions and it is + not supposed to be instantiated directly. + + .. method:: name() + + The service name. This string is how a service is referenced and can be + passed to :func:`win_service_get` to get a new :class:`WindowsService` + instance. + + .. method:: display_name() + + The service display name. The value is cached when this class is + instantiated. + + .. method:: binpath() + + The fully qualified path to the service binary/exe file as a string, + including command line arguments. + + .. method:: username() + + The name of the user that owns this service. + + .. method:: start_type() + + A string which can either be `"automatic"`, `"manual"` or `"disabled"`. + + .. method:: pid() + + The process PID, if any, else `None`. This can be passed to + :class:`Process` class to control the service's process. + + .. method:: status() + + Service status as a string, which may be either `"running"`, `"paused"`, + `"start_pending"`, `"pause_pending"`, `"continue_pending"`, + `"stop_pending"` or `"stopped"`. + + .. method:: description() + + Service long description. + + .. method:: as_dict() + + Utility method retrieving all the information above as a dictionary. + + .. versionadded:: 4.2.0 + + Availability: Windows + +Example code: + + >>> import psutil + >>> list(psutil.win_service_iter()) + [, + , + , + , + ...] + >>> s = psutil.win_service_get('alg') + >>> s.as_dict() + {'binpath': 'C:\\Windows\\System32\\alg.exe', + 'description': 'Provides support for 3rd party protocol plug-ins for Internet Connection Sharing', + 'display_name': 'Application Layer Gateway Service', + 'name': 'alg', + 'pid': None, + 'start_type': 'manual', + 'status': 'stopped', + 'username': 'NT AUTHORITY\\LocalService'} + +---- + +Constants +--------- + +The following enum classes group related constants and are useful for type +annotations and introspection. The individual constants (e.g. +:data:`psutil.STATUS_RUNNING`) are also accessible directly from the psutil +namespace as aliases for the enum members and should be preferred over +accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over +``psutil.ProcessStatus.STATUS_RUNNING``). + +.. class:: psutil.ProcessStatus + + `enum.StrEnum`_ collection of :data:`STATUS_* ` + constants. Returned by :meth:`psutil.Process.status()`. + + .. versionadded:: 8.0.0 + +.. class:: psutil.ProcessPriority + + `enum.IntEnum`_ collection of + :data:`*_PRIORITY_CLASS ` constants for + :meth:`psutil.Process.nice` on Windows. + + Availability: Windows + + .. versionadded:: 8.0.0 + +.. class:: psutil.ProcessIOPriority + + `enum.IntEnum`_ collection of I/O priority constants for + :meth:`psutil.Process.ionice`. On Linux: ``IOPRIO_CLASS_*`` constants. + On Windows: ``IOPRIO_*`` constants. + + Availability: Linux, Windows + + .. versionadded:: 8.0.0 + +.. class:: psutil.ProcessRlimit + + `enum.IntEnum`_ collection of :data:`RLIMIT_* ` + constants for :meth:`psutil.Process.rlimit`. + + Availability: Linux, FreeBSD + + .. versionadded:: 8.0.0 + +.. class:: psutil.ConnectionStatus + + `enum.StrEnum`_ collection of :data:`CONN_* ` + constants. Returned in the *status* field of + :func:`psutil.net_connections` and :meth:`psutil.Process.net_connections`. + + .. versionadded:: 8.0.0 + +.. class:: psutil.NicDuplex + + `enum.IntEnum`_ collection of :data:`NIC_DUPLEX_* ` + constants. Returned in the *duplex* field of :func:`psutil.net_if_stats`. + + .. versionadded:: 3.0.0 + +.. class:: psutil.BatteryTime + + `enum.IntEnum`_ collection of :data:`POWER_TIME_* ` + constants. May appear in the *secsleft* field of :func:`psutil.sensors_battery`. + + .. versionadded:: 5.1.0 + +Operating system constants +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _const-oses: +.. data:: POSIX +.. data:: LINUX +.. data:: WINDOWS +.. data:: MACOS +.. data:: FREEBSD +.. data:: NETBSD +.. data:: OPENBSD +.. data:: BSD +.. data:: SUNOS +.. data:: AIX + + ``bool`` constants which define what platform you're on. E.g. if on Windows, + :const:`WINDOWS` constant will be ``True``, all others will be ``False``. + + .. versionadded:: 4.0.0 + .. versionchanged:: 5.4.0 added AIX + +.. data:: OSX + + Alias for :const:`MACOS`. + + .. warning:: + deprecated in version 5.4.7; use :const:`MACOS` instead. + +.. _const-procfs_path: +.. data:: PROCFS_PATH + + The path of the /proc filesystem on Linux, Solaris and AIX (defaults to + ``"/proc"``). + You may want to re-set this constant right after importing psutil in case + your /proc filesystem is mounted elsewhere or if you want to retrieve + information about Linux containers such as Docker, Heroku or LXC (see + `here `__ + for more info). + It must be noted that this trick works only for APIs which rely on /proc + filesystem (e.g. `memory`_ APIs and most :class:`Process` class methods). + + Availability: Linux, Solaris, AIX + + .. versionadded:: 3.2.3 + .. versionchanged:: 3.4.2 also available on Solaris. + .. versionchanged:: 5.4.0 also available on AIX. + +Process status constants +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _const-pstatus: +.. data:: STATUS_RUNNING +.. data:: STATUS_SLEEPING +.. data:: STATUS_DISK_SLEEP +.. data:: STATUS_STOPPED +.. data:: STATUS_TRACING_STOP +.. data:: STATUS_ZOMBIE +.. data:: STATUS_DEAD +.. data:: STATUS_WAKE_KILL +.. data:: STATUS_WAKING +.. data:: STATUS_PARKED (Linux) +.. data:: STATUS_IDLE (Linux, macOS, FreeBSD) +.. data:: STATUS_LOCKED (FreeBSD) +.. data:: STATUS_WAITING (FreeBSD) +.. data:: STATUS_SUSPENDED (NetBSD) + + Represent a process status. Returned by :meth:`psutil.Process.status()`. + These constants are members of the :class:`psutil.ProcessStatus` enum. + + .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) + .. versionadded:: 5.4.7 ``STATUS_PARKED`` (Linux) + .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessStatus` + enum members (were plain strings). + +Process priority constants +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _const-prio: +.. data:: REALTIME_PRIORITY_CLASS +.. data:: HIGH_PRIORITY_CLASS +.. data:: ABOVE_NORMAL_PRIORITY_CLASS +.. data:: NORMAL_PRIORITY_CLASS +.. data:: IDLE_PRIORITY_CLASS +.. data:: BELOW_NORMAL_PRIORITY_CLASS + + Represent the priority of a process on Windows (see `SetPriorityClass`_). + They can be used in conjunction with :meth:`psutil.Process.nice()` to get or + set process priority. + These constants are members of the :class:`psutil.ProcessPriority` enum. + + Availability: Windows + + .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessPriority` + enum members (were plain integers). + +.. _const-ioprio: +.. data:: IOPRIO_CLASS_NONE +.. data:: IOPRIO_CLASS_RT +.. data:: IOPRIO_CLASS_BE +.. data:: IOPRIO_CLASS_IDLE + + A set of integers representing the I/O priority of a process on Linux. They + can be used in conjunction with :meth:`psutil.Process.ionice()` to get or set + process I/O priority. + These constants are members of the :class:`psutil.ProcessIOPriority` + enum. + *IOPRIO_CLASS_NONE* and *IOPRIO_CLASS_BE* (best effort) is the default for + any process that hasn't set a specific I/O priority. + *IOPRIO_CLASS_RT* (real time) means the process is given first access to the + disk, regardless of what else is going on in the system. + *IOPRIO_CLASS_IDLE* means the process will get I/O time when no-one else + needs the disk. + For further information refer to manuals of + `ionice `__ command line utility or + `ioprio_get`_ system call. + + Availability: Linux + + .. versionchanged:: 8.0.0 constants are now + :class:`psutil.ProcessIOPriority` enum members (previously + ``IOPriority`` enum). + +.. data:: IOPRIO_VERYLOW +.. data:: IOPRIO_LOW +.. data:: IOPRIO_NORMAL +.. data:: IOPRIO_HIGH + + A set of integers representing the I/O priority of a process on Windows. + They can be used in conjunction with :meth:`psutil.Process.ionice()` to get + or set process I/O priority. + These constants are members of the :class:`psutil.ProcessIOPriority` + enum. + + Availability: Windows + + .. versionadded:: 5.6.2 + .. versionchanged:: 8.0.0 constants are now + :class:`psutil.ProcessIOPriority` enum members (previously + ``IOPriority`` enum). + +Process resources constants +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Linux / FreeBSD: + + .. data:: RLIM_INFINITY + .. data:: RLIMIT_AS + .. data:: RLIMIT_CORE + .. data:: RLIMIT_CPU + .. data:: RLIMIT_DATA + .. data:: RLIMIT_FSIZE + .. data:: RLIMIT_MEMLOCK + .. data:: RLIMIT_NOFILE + .. data:: RLIMIT_NPROC + .. data:: RLIMIT_RSS + .. data:: RLIMIT_STACK + +Linux specific: + + .. data:: RLIMIT_LOCKS + .. data:: RLIMIT_MSGQUEUE + .. data:: RLIMIT_NICE + .. data:: RLIMIT_RTPRIO + .. data:: RLIMIT_RTTIME + .. data:: RLIMIT_SIGPENDING + +FreeBSD specific: + + .. data:: RLIMIT_SWAP + .. data:: RLIMIT_SBSIZE + .. data:: RLIMIT_NPTS + +Constants used for getting and setting process resource limits to be used in +conjunction with :meth:`psutil.Process.rlimit()`. See `resource.getrlimit`_ +for further information. +These constants are members of the :class:`psutil.ProcessRlimit` enum. + +Availability: Linux, FreeBSD + +.. versionchanged:: 5.7.3 added FreeBSD support, added ``RLIMIT_SWAP``, + ``RLIMIT_SBSIZE``, ``RLIMIT_NPTS``. +.. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessRlimit` + enum members (were plain integers). + +Connections constants +^^^^^^^^^^^^^^^^^^^^^ + +.. _const-conn: +.. data:: CONN_ESTABLISHED +.. data:: CONN_SYN_SENT +.. data:: CONN_SYN_RECV +.. data:: CONN_FIN_WAIT1 +.. data:: CONN_FIN_WAIT2 +.. data:: CONN_TIME_WAIT +.. data:: CONN_CLOSE +.. data:: CONN_CLOSE_WAIT +.. data:: CONN_LAST_ACK +.. data:: CONN_LISTEN +.. data:: CONN_CLOSING +.. data:: CONN_NONE +.. data:: CONN_DELETE_TCB (Windows) +.. data:: CONN_IDLE (Solaris) +.. data:: CONN_BOUND (Solaris) + + A set of strings representing the status of a TCP connection. + Returned by :meth:`psutil.Process.net_connections()` and + :func:`psutil.net_connections` (`status` field). + These constants are members of the :class:`psutil.ConnectionStatus` enum. + + .. versionchanged:: 8.0.0 constants are now :class:`psutil.ConnectionStatus` + enum members (were plain strings). + +Hardware constants +^^^^^^^^^^^^^^^^^^ + +.. _const-aflink: +.. data:: AF_LINK + + Constant which identifies a MAC address associated with a network interface. + To be used in conjunction with :func:`psutil.net_if_addrs()`. + + .. versionadded:: 3.0.0 + +.. _const-duplex: +.. data:: NIC_DUPLEX_FULL +.. data:: NIC_DUPLEX_HALF +.. data:: NIC_DUPLEX_UNKNOWN + + Constants which identifies whether a NIC (network interface card) has full or + half mode speed. NIC_DUPLEX_FULL means the NIC is able to send and receive + data (files) simultaneously, NIC_DUPLEX_FULL means the NIC can either send or + receive data at a time. + To be used in conjunction with :func:`psutil.net_if_stats()`. + + .. versionadded:: 3.0.0 + +.. _const-power: +.. data:: POWER_TIME_UNKNOWN +.. data:: POWER_TIME_UNLIMITED + + Whether the remaining time of the battery cannot be determined or is + unlimited. + May be assigned to :func:`psutil.sensors_battery()`'s *secsleft* field. + + .. versionadded:: 5.1.0 + +.. _const-version-info: +.. data:: version_info + + A tuple to check psutil installed version. Example: + + >>> import psutil + >>> if psutil.version_info >= (4, 5): + ... pass diff --git a/docs/auto-build.sh b/docs/auto-build.sh new file mode 100644 index 0000000000..3bd98973d4 --- /dev/null +++ b/docs/auto-build.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# This script uses inotifywait to watch for changes in the current directory +# and its parents, and automatically rebuilds the Sphinx doc whenever a change +# is detected. Requires: sudo apt install inotify-tools. + +DIR="." +make clean +make html +while inotifywait -r -e modify,create,delete,move "$DIR"; do + make html +done diff --git a/docs/DEVGUIDE.rst b/docs/devguide.rst similarity index 84% rename from docs/DEVGUIDE.rst rename to docs/devguide.rst index 11f4727655..7b24181f93 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/devguide.rst @@ -1,8 +1,8 @@ -psutil development guide -======================== +Development guide +================= Build, setup and running tests -.............................. +------------------------------ psutil makes extensive use of C extension modules, meaning a C compiler is required, see @@ -65,6 +65,28 @@ Windows make build make test-parallel +Debug mode +---------- + +If you want to debug unusual situations or want to report a bug, it may be +useful to enable debug mode via ``PSUTIL_DEBUG`` environment variable. In this +mode, psutil may (or may not) print additional information to stderr. Usually +these are error conditions which are not severe, and hence are ignored (instead +of crashing). Unit tests automatically run with debug mode enabled. On UNIX: + +:: + + $ PSUTIL_DEBUG=1 python3 script.py + psutil-debug [psutil/_psutil_linux.c:150]> setmntent() failed (ignored) + +On Windows: + +:: + + set PSUTIL_DEBUG=1 python.exe script.py + psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) + + Coding style ------------ @@ -129,14 +151,14 @@ Make a pull request Continuous integration ---------------------- -Unit tests are automatically run on every ``git push`` on **Linux**, **macOS**, -**Windows**, **FreeBSD**, **NetBSD**, **OpenBSD**. -AIX and Solaris does not have continuous test integration. +Unit tests are automatically run on every ``git push`` on all platforms except +AIX. See config files in `.github/workflows `_ +directory. Documentation ------------- -- doc source code is written in a single file: ``docs/index.rst``. +- doc is under ``docs/``. - doc can be built with ``make install-pydeps-dev; cd docs; make html``. - public doc is hosted at https://psutil.readthedocs.io. diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000000..dedf288c63 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,20 @@ +.. include:: _links.rst + +FAQs +==== + +* Q: Why do I get :class:`AccessDenied` for certain processes? +* A: This may happen when you query processes owned by another user, + especially on macOS (see issue `#883`_) and Windows. + Unfortunately there's not much you can do about this except running the + Python process with higher privileges. + On Unix you may run the Python process as root or use the SUID bit + (``ps`` and ``netstat`` does this). + On Windows you may run the Python process as NT AUTHORITY\\SYSTEM or install + the Python script as a Windows service (ProcessHacker does this). + +---- + +* Q: is MinGW supported on Windows? +* A: no, Visual Studio is the only compiler support on Windows (see + :doc:`devguide`). diff --git a/docs/index.rst b/docs/index.rst index 0ad2f8c3b9..e917ea2c3d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,24 +1,29 @@ .. module:: psutil :synopsis: psutil module .. moduleauthor:: Giampaolo Rodola' +.. include:: _links.rst -psutil documentation +Psutil documentation ==================== -Quick links ------------ +| |downloads| |stars| |forks| |contributors| -- `Home page `__ -- `Install `_ -- `Forum `__ -- `Download `__ -- `Blog `__ -- `Contributing `__ -- `Development guide `_ -- `What's new `__ +.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg + :target: https://clickpy.clickhouse.com/dashboard/psutil + :alt: Downloads + +.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/stargazers + :alt: Github stars + +.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/network/members + :alt: Github forks + +.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/graphs/contributors + :alt: Contributors -About ------ psutil (python system and process utilities) is a cross-platform library for retrieving information on running @@ -29,7 +34,7 @@ process resources** and the **management of running processes**. It implements many functionalities offered by UNIX command line tools such as: *ps, top, lsof, netstat, ifconfig, who, df, kill, free, nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap*. -psutil currently supports the following platforms: +psutil currently supports the following platforms, from **Python 3.7** onwards: - **Linux** - **Windows** @@ -38,14 +43,8 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are cPython 3.7+ and `PyPy `__. -Latest psutil version supporting Python 2.7 is -`psutil 6.1.1 `__. - -The psutil documentation you're reading is distributed as a single HTML page. - Sponsors -======== +-------- .. raw:: html @@ -69,10 +68,10 @@ Sponsors - add your logo +      add your logo

Funding -======= +------- While psutil is free software and will always be, the project would benefit immensely from some funding. @@ -86,3553 +85,22 @@ becoming a sponsor via `GitHub `__, `PayPal `__. Sponsors can have their logo displayed here and in the psutil `documentation `__. -Install -======= - -On Linux, Windows, macOS:: - - pip install psutil - -For other platforms see more detailed -`install `_ -instructions. - -System related functions -======================== - -CPU ---- - -.. function:: cpu_times(percpu=False) - - Return system CPU times as a named tuple. - Every attribute represents the seconds the CPU has spent in the given mode. - The attributes availability varies depending on the platform. - Cross-platform fields: - - - **user**: time spent by normal processes executing in user mode; on Linux - this also includes **guest** time - - **system**: time spent by processes executing in kernel mode - - **idle**: time spent doing nothing - - Platform-specific fields: - - - **nice** *(Linux, macOS, BSD)*: time spent by niced (prioritized) processes - executing in user mode; on Linux this also includes **guest_nice** time - - **iowait** *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete. - This is *not* accounted in **idle** time counter. - - **irq** *(Linux, BSD)*: time spent for servicing hardware interrupts - - **softirq** *(Linux)*: time spent for servicing software interrupts - - **steal** *(Linux)*: time spent by other operating systems running - in a virtualized environment - - **guest** *(Linux)*: time spent running a virtual CPU for guest - operating systems under the control of the Linux kernel - - **guest_nice** *(Linux)*: time spent running a niced guest - (virtual CPU for guest operating systems under the control of the Linux - kernel) - - **interrupt** *(Windows)*: time spent for servicing hardware interrupts ( - similar to "irq" on UNIX) - - **dpc** *(Windows)*: time spent servicing deferred procedure calls (DPCs); - DPCs are interrupts that run at a lower priority than standard interrupts. - - When *percpu* is ``True`` return a list of named tuples for each logical CPU - on the system. - First element of the list refers to first CPU, second element to second CPU - and so on. - The order of the list is consistent across calls. - Example output on Linux: - - >>> import psutil - >>> psutil.cpu_times() - scputimes(user=17411.7, system=3797.02, idle=51266.57, nice=77.99, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) - - .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. - - .. versionchanged:: 8.0.0 - ``cpu_times()`` field order was standardized: ``user``, ``system``, - ``idle`` are now always the first three fields. Previously on Linux, - macOS, and BSD the first three were ``user``, ``nice``, ``system``. - .. warning:: - in version 8.0.0 the named tuple changed field order. Positional access - (e.g. ``cpu_times()[3]``) may silently return the wrong field. Always use - attribute access instead (e.g. ``cpu_times().idle``). - - .. warning:: - 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 `#1210 - `__. - -.. function:: cpu_percent(interval=None, percpu=False) - - Return a float representing the current system-wide CPU utilization as a - percentage. When *interval* is > ``0.0`` compares system CPU times elapsed - before and after the interval (blocking). - When *interval* is ``0.0`` or ``None`` compares system CPU times elapsed - since last call or module import, returning immediately. - That means the first time this is called it will return a meaningless ``0.0`` - value which you are supposed to ignore. - In this case it is recommended for accuracy that this function be called with - at least ``0.1`` seconds between calls. - When *percpu* is ``True`` returns a list of floats representing the - utilization as a percentage for each CPU. - First element of the list refers to first CPU, second element to second CPU - and so on. The order of the list is consistent across calls. - Internally this function maintains a global map (a dict) where each key is - the ID of the calling thread (`threading.get_ident`_). This means it can be - called from different threads, at different intervals, and still return - meaningful and independent results. - - >>> import psutil - >>> # blocking - >>> psutil.cpu_percent(interval=1) - 2.0 - >>> # non-blocking (percentage since last call) - >>> psutil.cpu_percent(interval=None) - 2.9 - >>> # blocking, per-cpu - >>> psutil.cpu_percent(interval=1, percpu=True) - [2.0, 1.0] - >>> - - .. warning:: - the first time this function is called with *interval* = ``0.0`` or ``None`` - it will return a meaningless ``0.0`` value which you are supposed to - ignore. - - .. versionchanged:: 5.9.6 function is now thread safe. - -.. function:: cpu_times_percent(interval=None, percpu=False) - - Same as :func:`cpu_percent()` but provides utilization percentages for each - specific CPU time as is returned by - :func:`psutil.cpu_times(percpu=True)`. - *interval* and - *percpu* arguments have the same meaning as in :func:`cpu_percent()`. - On Linux "guest" and "guest_nice" percentages are not accounted in "user" - and "user_nice" percentages. - - .. warning:: - the first time this function is called with *interval* = ``0.0`` or - ``None`` it will return a meaningless ``0.0`` value which you are supposed - to ignore. - - .. versionchanged:: - 4.1.0 two new *interrupt* and *dpc* fields are returned on Windows. - - .. versionchanged:: 5.9.6 function is now thread safe. - -.. function:: cpu_count(logical=True) - - Return the number of logical CPUs in the system (similar to `os.cpu_count`_) - or ``None`` if undetermined. - Unlike `os.cpu_count`_, this is not influenced by the ``PYTHON_CPU_COUNT`` - environment variable introduced in Python 3.13. - "logical CPUs" means the number of physical cores multiplied by the number - of threads that can run on each core (this is known as Hyper Threading). - This is what cloud providers often refer to as vCPUs. - If *logical* is ``False`` return the number of physical cores only, or - ``None`` if undetermined. - On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return - ``None``. - Example on a system having 2 cores + Hyper Threading: - - >>> import psutil - >>> psutil.cpu_count() - 4 - >>> psutil.cpu_count(logical=False) - 2 - - Note that ``psutil.cpu_count()`` may not necessarily be equivalent to the - actual number of CPUs the current process can use. - That can vary in case process CPU affinity has been changed, Linux cgroups - are being used or (in case of Windows) on systems using processor groups or - having more than 64 CPUs. - The number of usable CPUs can be obtained with: - - >>> len(psutil.Process().cpu_affinity()) - 1 - -.. function:: cpu_stats() - - Return various CPU statistics as a named tuple: - - - **ctx_switches**: - number of context switches (voluntary + involuntary) since boot. - - **interrupts**: - number of interrupts since boot. - - **soft_interrupts**: - number of software interrupts since boot. Always set to ``0`` on Windows - and SunOS. - - **syscalls**: number of system calls since boot. Always set to ``0`` on - Linux. - - Example (Linux): - - .. code-block:: python - - >>> import psutil - >>> psutil.cpu_stats() - scpustats(ctx_switches=20455687, interrupts=6598984, soft_interrupts=2134212, syscalls=0) - - .. versionadded:: 4.1.0 - - -.. function:: cpu_freq(percpu=False) - - Return CPU frequency as a named tuple including *current*, *min* and *max* - frequencies expressed in Mhz. On Linux *current* frequency reports the - real-time value, on all other platforms this usually represents the - nominal "fixed" value (never changing). If *percpu* is ``True`` and the - system supports per-cpu frequency retrieval (Linux and FreeBSD), a list of - frequencies is returned for each CPU, if not, a list with a single element - is returned. If *min* and *max* cannot be determined they are set to - ``0.0``. - - Example (Linux): - - .. code-block:: python - - >>> import psutil - >>> psutil.cpu_freq() - scpufreq(current=931.42925, min=800.0, max=3500.0) - >>> psutil.cpu_freq(percpu=True) - [scpufreq(current=2394.945, min=800.0, max=3500.0), - scpufreq(current=2236.812, min=800.0, max=3500.0), - scpufreq(current=1703.609, min=800.0, max=3500.0), - scpufreq(current=1754.289, min=800.0, max=3500.0)] - - Availability: Linux, macOS, Windows, FreeBSD, OpenBSD. *percpu* only - supported on Linux and FreeBSD. - - .. versionadded:: 5.1.0 - - .. versionchanged:: 5.5.1 added FreeBSD support. - - .. versionchanged:: 5.9.1 added OpenBSD support. - -.. function:: getloadavg() - - Return the average system load over the last 1, 5 and 15 minutes as a tuple. - The "load" represents the processes which are in a runnable state, either - using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). - On UNIX systems this relies on `os.getloadavg`_. On Windows this is emulated - by using a Windows API that spawns a thread which keeps running in - background and updates results every 5 seconds, mimicking the UNIX behavior. - Thus, on Windows, the first time this is called and for the next 5 seconds - it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. - The numbers returned only make sense if related to the number of CPU cores - installed on the system. So, for instance, a value of `3.14` on a system - with 10 logical CPUs means that the system load was 31.4% percent over the - last N minutes. - - .. code-block:: python - - >>> import psutil - >>> psutil.getloadavg() - (3.14, 3.89, 4.67) - >>> psutil.cpu_count() - 10 - >>> # percentage representation - >>> [x / psutil.cpu_count() * 100 for x in psutil.getloadavg()] - [31.4, 38.9, 46.7] - - .. versionadded:: 5.6.2 - -Memory ------- - -.. function:: virtual_memory() - - Return statistics about system memory usage as a named tuple including the - following fields, expressed in bytes. - - - **total**: total physical memory (exclusive swap). - - **available**: memory that can be given instantly to processes without the - system going into swap. On Linux it uses the ``MemAvailable`` field from - ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back to an - estimate. This is the recommended field for monitoring actual memory usage - in a cross-platform fashion. - - **percent**: the percentage usage calculated as - ``(total - available) / total * 100``. - - **used**: memory in use, calculated differently depending on the platform - (see the table below). It is meant for informational purposes. Neither - ``total - free`` nor ``total - available`` necessarily equals ``used``. - - **free**: memory that is not in use at all (zeroed pages). This is - typically much lower than **available** because the OS keeps recently - freed memory as reclaimable cache (see **cached** and **buffers**) - rather than zeroing it immediately. Do not use this to check for - memory pressure; use **available** instead. - - **active** *(Linux, macOS, BSD)*: memory currently mapped by processes - or recently accessed, held in RAM. It is unlikely to be reclaimed unless - the system is under significant memory pressure. - - **inactive** *(Linux, macOS, BSD)*: memory not recently accessed. It - still holds valid data (file cache, old allocations) but is a candidate - for reclamation or swapping. On BSD systems it is counted in - **available**. - - **buffers** *(Linux, BSD)*: kernel buffer cache for raw block-device I/O - (e.g. disk blocks read before the filesystem processes them). Can be - reclaimed by the OS when needed. - - **cached** *(Linux, BSD)*: in-memory page cache for files read from disk. - The OS reclaims this memory automatically when processes need it, so a - large **cached** value is healthy and does not indicate memory pressure. - On Linux this includes ``SReclaimable`` (reclaimable slab memory). - - **shared** *(Linux, BSD)*: memory accessible by multiple processes - simultaneously, such as POSIX shared memory (``shm_open``) and - ``tmpfs``-backed files. On Linux this corresponds to ``Shmem`` in - ``/proc/meminfo`` and is already counted within **active** / **inactive**. - - **slab** *(Linux)*: memory used by the kernel's internal object caches - (e.g. inode and dentry caches). The reclaimable portion - (``SReclaimable``) is already included in **cached**. - - **wired** *(macOS, BSD)*: memory pinned in RAM by the kernel (e.g. kernel - code and critical data structures). It can never be moved to disk. - - .. note:: - - On Linux, **total**, **free**, **used**, **shared**, and **available** - match the output of the ``free`` command. - - On macOS, **free**, **active**, **inactive**, and **wired** match - ``vm_stat`` output. - - On Windows, **total**, **used** ("In use"), and **available** match - the Task Manager (Performance > Memory tab). - - Follows a table showing implementation details. All info on Linux are retrieved from `/proc/meminfo`. - - .. list-table:: - :header-rows: 1 - - * - Field - - Linux - - macOS - - Windows - - FreeBSD - * - total - - ``MemTotal`` - - ``sysctl() hw.memsize`` - - ``GetPerformanceInfo() PhysicalTotal`` - - ``sysctl() hw.physmem`` - * - available - - ``MemAvailable`` - - ``host_statistics64() inactive + free`` - - ``GetPerformanceInfo() PhysicalAvailable`` - - ``inactive + cached + free`` - * - used - - ``total - available`` - - ``host_statistics64() active + wired`` - - ``total - available`` - - ``active + wired + cached`` - * - free - - ``MemFree`` - - ``host_statistics64() free - speculative`` - - same as ``available`` - - ``sysctl() vm.stats.vm.v_free_count`` - * - active - - ``Active`` - - ``host_statistics64() active`` - - - - ``sysctl() vm.stats.vm.v_active_count`` - * - inactive - - ``Inactive`` - - ``host_statistics64() inactive`` - - - - ``sysctl() vm.stats.vm.v_inactive_count`` - * - buffers - - ``Buffers`` - - - - - - ``sysctl() vfs.bufspace`` - * - cached - - ``Cached + SReclaimable`` - - - - - - ``sysctl() vm.stats.vm.v_cache_count`` - * - shared - - ``Shmem`` - - - - - - ``sysctl(CTL_VM/VM_METER) t_vmshr + t_rmshr`` - * - slab - - ``Slab`` - - - - - - - * - wired - - - - ``host_statistics64() wired`` - - - - ``sysctl() vm.stats.vm.v_wire_count`` - - Example on Linux: - - >>> import psutil - >>> mem = psutil.virtual_memory() - >>> mem - svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304, slab=199348224) - >>> - >>> THRESHOLD = 500 * 1024 * 1024 # 500MB - >>> if mem.available <= THRESHOLD: - ... print("warning") - ... - >>> - - .. note:: if you just want to know how much physical memory is left in a - cross platform fashion simply rely on **available** and **percent** - fields. - - .. note:: see `meminfo.py`_ script providing an example on how to convert - bytes in a human readable form. - - .. versionchanged:: 4.2.0 added *shared* metric on Linux. - - .. versionchanged:: 5.4.4 added *slab* metric on Linux. - -.. function:: swap_memory() - - Return system swap memory statistics as a named tuple including the following - fields: - - * **total**: total swap space. On Windows this is derived as - ``CommitLimit - PhysicalTotal``, representing virtual memory backed by - the page file rather than the raw page-file size. - * **used**: swap space currently in use. - * **free**: swap space not in use (``total - used``). - * **percent**: swap usage as a percentage, calculated as - ``used / total * 100``. - * **sin**: number of bytes the system has paged *in* from disk (pages moved - from swap space back into RAM) since boot (cumulative). - * **sout**: number of bytes the system has paged *out* to disk (pages moved - from RAM into swap space) since boot (cumulative). A continuously - increasing **sout** is a sign of memory pressure. - - **sin** and **sout** are cumulative counters since boot; monitor their rate - of change rather than the absolute value to detect active swapping. - On Windows both are always ``0``. - See `meminfo.py`_ script providing an example on how to convert bytes in a - human readable form. - - >>> import psutil - >>> psutil.swap_memory() - sswap(total=2097147904L, used=886620160L, free=1210527744L, percent=42.3, sin=1050411008, sout=1906720768) - - .. versionchanged:: 5.2.3 on Linux this function relies on /proc fs instead - of sysinfo() syscall so that it can be used in conjunction with - :const:`psutil.PROCFS_PATH` in order to retrieve memory info about - Linux containers such as Docker and Heroku. - -Disks ------ - -.. function:: disk_partitions(all=False) - - Return all mounted disk partitions as a list of named tuples including device, - mount point and filesystem type, similarly to "df" command on UNIX. If *all* - parameter is ``False`` it tries to distinguish and return physical devices - only (e.g. hard disks, cd-rom drives, USB keys) and ignore all others - (e.g. pseudo, memory, duplicate, inaccessible filesystems). - Note that this may not be fully reliable on all systems (e.g. on BSD this - parameter is ignored). - See `disk_usage.py`_ script providing an example usage. - Returns a list of named tuples with the following fields: - - * **device**: the device path (e.g. ``"/dev/hda1"``). On Windows this is the - drive letter (e.g. ``"C:\\"``). - * **mountpoint**: the mount point path (e.g. ``"/"``). On Windows this is the - drive letter (e.g. ``"C:\\"``). - * **fstype**: the partition filesystem (e.g. ``"ext3"`` on UNIX or ``"NTFS"`` - on Windows). - * **opts**: a comma-separated string indicating different mount options for - the drive/partition. Platform-dependent. - - >>> import psutil - >>> psutil.disk_partitions() - [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), - sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] - - .. versionchanged:: 5.7.4 added *maxfile* and *maxpath* fields - - .. versionchanged:: 6.0.0 removed *maxfile* and *maxpath* fields - -.. function:: disk_usage(path) - - Return disk usage statistics about the partition which contains the given - *path* as a named tuple including **total**, **used** and **free** space - expressed in bytes, plus the **percentage** usage. - ``OSError`` is raised if *path* does not exist. - Starting from Python 3.3 this is also available as `shutil.disk_usage`_ - (see `BPO-12442`_). - See `disk_usage.py`_ script providing an example usage. - - >>> import psutil - >>> psutil.disk_usage('/') - sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) - - .. note:: - UNIX usually reserves 5% of the total disk space for the root user. - *total* and *used* fields on UNIX refer to the overall total and used - space, whereas *free* represents the space available for the **user** and - *percent* represents the **user** utilization (see - `source code `__). - That is why *percent* value may look 5% bigger than what you would expect - it to be. - Also note that both 4 values match "df" cmdline utility. - - .. versionchanged:: - 4.3.0 *percent* value takes root reserved space into account. - -.. function:: disk_io_counters(perdisk=False, nowrap=True) - - Return system-wide disk I/O statistics as a named tuple including the - following fields: - - - **read_count**: number of reads - - **write_count**: number of writes - - **read_bytes**: number of bytes read - - **write_bytes**: number of bytes written - - Platform-specific fields: - - - **read_time**: (all except *NetBSD* and *OpenBSD*) time spent reading from - disk (in milliseconds) - - **write_time**: (all except *NetBSD* and *OpenBSD*) time spent writing to disk - (in milliseconds) - - **busy_time**: (*Linux*, *FreeBSD*) time spent doing actual I/Os (in - milliseconds) - - **read_merged_count** (*Linux*): number of merged reads (see `iostats doc`_) - - **write_merged_count** (*Linux*): number of merged writes (see `iostats doc`_) - - If *perdisk* is ``True`` return the same information for every physical disk - installed on the system as a dictionary with partition names as the keys and - the named tuple described above as the values. - See `iotop.py`_ for an example application. - On some systems such as Linux, on a very busy or long-lived system, the - numbers returned by the kernel may overflow and wrap (restart from zero). - If *nowrap* is ``True`` psutil will detect and adjust those numbers across - function calls 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 *nowrap* - cache. - On Windows it may be necessary to issue ``diskperf -y`` command from cmd.exe - first in order to enable IO counters. - On diskless machines this function will return ``None`` or ``{}`` if - *perdisk* is ``True``. - - >>> import psutil - >>> psutil.disk_io_counters() - sdiskio(read_count=8141, write_count=2431, read_bytes=290203, write_bytes=537676, read_time=5868, write_time=94922) - >>> - >>> psutil.disk_io_counters(perdisk=True) - {'sda1': sdiskio(read_count=920, write_count=1, read_bytes=2933248, write_bytes=512, read_time=6016, write_time=4), - 'sda2': sdiskio(read_count=18707, write_count=8830, read_bytes=6060, write_bytes=3443, read_time=24585, write_time=1572), - 'sdb1': sdiskio(read_count=161, write_count=0, read_bytes=786432, write_bytes=0, read_time=44, write_time=0)} - - .. note:: - on Windows ``"diskperf -y"`` command may need to be executed first - otherwise this function won't find any disk. - - .. versionchanged:: - 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new - *nowrap* argument. - - .. versionchanged:: - 4.0.0 added *busy_time* (Linux, FreeBSD), *read_merged_count* and - *write_merged_count* (Linux) fields. - - .. versionchanged:: - 4.0.0 NetBSD no longer has *read_time* and *write_time* fields. - -Network -------- - -.. function:: net_io_counters(pernic=False, nowrap=True) - - Return system-wide network I/O statistics as a named tuple including the - following attributes: - - - **bytes_sent**: number of bytes sent - - **bytes_recv**: number of bytes received - - **packets_sent**: number of packets sent - - **packets_recv**: number of packets received - - **errin**: total number of errors while receiving - - **errout**: total number of errors while sending - - **dropin**: total number of incoming packets which were dropped - - **dropout**: total number of outgoing packets which were dropped (always 0 - on macOS and BSD) - - If *pernic* is ``True`` return the same information for every network - interface installed on the system as a dictionary with network interface - names as the keys and the named tuple described above as the values. - On some systems such as Linux, on a very busy or long-lived system, the - numbers returned by the kernel may overflow and wrap (restart from zero). - If *nowrap* is ``True`` psutil will detect and adjust those numbers across - function calls and add "old value" to "new value" so that the returned - numbers will always be increasing or remain the same, but never decrease. - ``net_io_counters.cache_clear()`` can be used to invalidate the *nowrap* - cache. - On machines with no network interfaces this function will return ``None`` or - ``{}`` if *pernic* is ``True``. - - >>> import psutil - >>> psutil.net_io_counters() - snetio(bytes_sent=14508483, bytes_recv=62749361, packets_sent=84311, packets_recv=94888, errin=0, errout=0, dropin=0, dropout=0) - >>> - >>> psutil.net_io_counters(pernic=True) - {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), - 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} - - Also see `nettop.py`_ and `ifconfig.py`_ for an example application. - - .. versionchanged:: - 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new - *nowrap* argument. - -.. function:: net_connections(kind='inet') - - Return system-wide socket connections as a list of named tuples. - Every named tuple provides 7 attributes: - - - **fd**: the socket file descriptor. If the connection refers to the current - process this may be passed to `socket.fromfd`_ - to obtain a usable socket object. - On Windows and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. - - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or - `SOCK_SEQPACKET`_. - - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` - in case of AF_UNIX sockets. For UNIX sockets see notes below. - - **raddr**: the remote address as a ``(ip, port)`` named tuple or an - absolute ``path`` in case of UNIX sockets. - When the remote endpoint is not connected you'll get an empty tuple - (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. - - **status**: represents the status of a TCP connection. The return value - is one of the `psutil.CONN_* <#connections-constants>`_ constants - (a string). - For UDP and UNIX sockets this is always going to be - :const:`psutil.CONN_NONE`. - - **pid**: the PID of the process which opened the socket, if retrievable, - else ``None``. On some platforms (e.g. Linux) the availability of this - field changes depending on process privileges (root is needed). - - The *kind* parameter is a string which filters for connections matching the - following criteria: - - .. table:: - - +----------------+-----------------------------------------------------+ - | **Kind value** | **Connections using** | - +================+=====================================================+ - | ``"inet"`` | IPv4 and IPv6 | - +----------------+-----------------------------------------------------+ - | ``"inet4"`` | IPv4 | - +----------------+-----------------------------------------------------+ - | ``"inet6"`` | IPv6 | - +----------------+-----------------------------------------------------+ - | ``"tcp"`` | TCP | - +----------------+-----------------------------------------------------+ - | ``"tcp4"`` | TCP over IPv4 | - +----------------+-----------------------------------------------------+ - | ``"tcp6"`` | TCP over IPv6 | - +----------------+-----------------------------------------------------+ - | ``"udp"`` | UDP | - +----------------+-----------------------------------------------------+ - | ``"udp4"`` | UDP over IPv4 | - +----------------+-----------------------------------------------------+ - | ``"udp6"`` | UDP over IPv6 | - +----------------+-----------------------------------------------------+ - | ``"unix"`` | UNIX socket (both UDP and TCP protocols) | - +----------------+-----------------------------------------------------+ - | ``"all"`` | the sum of all the possible families and protocols | - +----------------+-----------------------------------------------------+ - - On macOS and AIX this function requires root privileges. - To get per-process connections use :meth:`Process.net_connections`. - Also, see `netstat.py`_ example script. - Example: - - >>> import psutil - >>> psutil.net_connections() - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=, pid=1254), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=, pid=2987), - pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=, pid=None), - pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=, pid=None) - ...] - - .. warning:: - On Linux, retrieving some connections requires root privileges. If psutil is - not run as root, those connections are silently skipped instead of raising - ``PermissionError``. That means the returned list may be incomplete. - - .. note:: - (macOS and AIX) :class:`psutil.AccessDenied` is always raised unless running - as root. This is a limitation of the OS and ``lsof`` does the same. - - .. note:: - (Solaris) UNIX sockets are not supported. - - .. note:: - (Linux, FreeBSD, OpenBSD) *raddr* field for UNIX sockets is always set to - ``""`` (empty string). This is a limitation of the OS. - - .. versionadded:: 2.1.0 - - .. versionchanged:: 5.3.0 : socket "fd" is now set for real instead of being - ``-1``. - - .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. - - .. versionchanged:: 5.9.5 : OpenBSD: retrieve *laddr* path for AF_UNIX - sockets (before it was an empty string). - - .. versionchanged:: 8.0.0 *status* field is now a - :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. - -.. function:: net_if_addrs() - - Return the addresses associated to each NIC (network interface card) - installed on the system as a dictionary whose keys are the NIC names and - value is a list of named tuples for each address assigned to the NIC. - Each named tuple includes 5 fields: - - - **family**: the address family, either `AF_INET`_ or `AF_INET6`_ - or :const:`psutil.AF_LINK`, which refers to a MAC address. - - **address**: the primary NIC address (always set). - - **netmask**: the netmask address (may be ``None``). - - **broadcast**: the broadcast address (may be ``None``). - - **ptp**: stands for "point to point"; it's the destination address on a - point to point interface (typically a VPN). *broadcast* and *ptp* are - mutually exclusive. May be ``None``. - - Example:: - - >>> import psutil - >>> psutil.net_if_addrs() - {'lo': [snicaddr(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), - snicaddr(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), - snicaddr(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], - 'wlan0': [snicaddr(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), - snicaddr(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), - snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} - >>> - - See also `nettop.py`_ and `ifconfig.py`_ for an example application. - - .. note:: - if you're interested in others families (e.g. AF_BLUETOOTH) you can use - the more powerful `netifaces `__ - extension. - - .. note:: - you can have more than one address of the same family associated with each - interface (that's why dict values are lists). - - .. note:: - *broadcast* and *ptp* are not supported on Windows and are always ``None``. - - .. versionadded:: 3.0.0 - - .. versionchanged:: 3.2.0 *ptp* field was added. - - .. versionchanged:: 4.4.0 added support for *netmask* field on Windows which - is no longer ``None``. - - .. versionchanged:: 7.0.0 added support for *broadcast* field on Windows - which is no longer ``None``. - -.. function:: net_if_stats() - - Return information about each NIC (network interface card) installed on the - system as a dictionary whose keys are the NIC names and value is a named tuple - with the following fields: - - - **isup**: a bool indicating whether the NIC is up and running (meaning - ethernet cable or Wi-Fi is connected). - - **duplex**: the duplex communication type; - it can be either :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or - :const:`NIC_DUPLEX_UNKNOWN`. - - **speed**: the NIC speed expressed in mega bits (MB), if it can't be - determined (e.g. 'localhost') it will be set to ``0``. - - **mtu**: NIC's maximum transmission unit expressed in bytes. - - **flags**: a string of comma-separated flags on the interface (may be an empty string). - Possible flags are: ``up``, ``broadcast``, ``debug``, ``loopback``, - ``pointopoint``, ``notrailers``, ``running``, ``noarp``, ``promisc``, - ``allmulti``, ``master``, ``slave``, ``multicast``, ``portsel``, - ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, - and ``d2`` (some flags are only available on certain platforms). - - Availability: UNIX - - Example: - - >>> import psutil - >>> psutil.net_if_stats() - {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast'), - 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running')} - - Also see `nettop.py`_ and `ifconfig.py`_ for an example application. - - .. versionadded:: 3.0.0 - - .. versionchanged:: 5.7.3 `isup` on UNIX also checks whether the NIC is running. - - .. versionchanged:: 5.9.3 *flags* field was added on POSIX. - -Sensors -------- - -.. function:: sensors_temperatures(fahrenheit=False) - - Return hardware temperatures. Each entry is a named tuple representing a - certain hardware temperature sensor (it may be a CPU, an hard disk or - something else, depending on the OS and its configuration). - All temperatures are expressed in celsius unless *fahrenheit* is set to - ``True``. - If sensors are not supported by the OS an empty dict is returned. - Each named tuple includes 4 fields: - - - **label**: a string label for the sensor, if available, else ``""``. - - **current**: current temperature, or ``None`` if not available. - - **high**: temperature at which the system will throttle, or ``None`` - if not available. - - **critical**: temperature at which the system will shut down, or - ``None`` if not available. - - Example:: - - >>> import psutil - >>> psutil.sensors_temperatures() - {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], - 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], - 'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), - shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0), - shwtemp(label='Core 1', current=52.0, high=100.0, critical=100.0), - shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), - shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} - - See also `temperatures.py`_ and `sensors.py`_ for an example application. - - Availability: Linux, FreeBSD - - .. versionadded:: 5.1.0 - - .. versionchanged:: 5.5.0 added FreeBSD support - -.. function:: sensors_fans() - - Return hardware fans speed. Each entry is a named tuple representing a - certain hardware sensor fan. - Fan speed is expressed in RPM (revolutions per minute). - If sensors are not supported by the OS an empty dict is returned. - Example:: - - >>> import psutil - >>> psutil.sensors_fans() - {'asus': [sfan(label='cpu_fan', current=3200)]} - - See also `fans.py`_ and `sensors.py`_ for an example application. - - Availability: Linux - - .. versionadded:: 5.2.0 - -.. function:: sensors_battery() - - Return battery status information as a named tuple including the following - values. If no battery is installed or metrics can't be determined ``None`` - is returned. - - - **percent**: battery power left as a percentage. - - **secsleft**: a rough approximation of how many seconds are left before the - battery runs out of power. - If the AC power cable is connected this is set to - :data:`psutil.POWER_TIME_UNLIMITED `. - If it can't be determined it is set to - :data:`psutil.POWER_TIME_UNKNOWN `. - - **power_plugged**: ``True`` if the AC power cable is connected, ``False`` - if not or ``None`` if it can't be determined. - - Example:: - - >>> import psutil - >>> - >>> def secs2hours(secs): - ... mm, ss = divmod(secs, 60) - ... hh, mm = divmod(mm, 60) - ... return "%d:%02d:%02d" % (hh, mm, ss) - ... - >>> battery = psutil.sensors_battery() - >>> battery - sbattery(percent=93, secsleft=16628, power_plugged=False) - >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) - charge = 93%, time left = 4:37:08 - - See also `battery.py`_ and `sensors.py`_ for an example application. - - Availability: Linux, Windows, FreeBSD - - .. versionadded:: 5.1.0 +Security +-------- - .. versionchanged:: 5.4.2 added macOS support +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. -Other system info +Table of Contents ----------------- -.. function:: boot_time() - - Return the system boot time expressed in seconds since the epoch (seconds - since January 1, 1970, at midnight UTC). The returned value is based on the - system clock, which means it may be affected by changes such as manual - adjustments or time synchronization (e.g. NTP). - - .. code-block:: python - - >>> import psutil, datetime - >>> psutil.boot_time() - 1389563460.0 - >>> datetime.datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S") - '2014-01-12 22:51:00' - - .. note:: - on Windows this function may return a time which is off by 1 second if it's - used across different processes (see `issue #1007`_). - -.. function:: users() - - Return users currently connected on the system as a list of named tuples - including the following fields: - - - **name**: the name of the user. - - **terminal**: the tty or pseudo-tty associated with the user, if any, - else ``None``. - - **host**: the host name associated with the entry, if any, else ``None``. - - **started**: the creation time as a floating point number expressed in - seconds since the epoch. - - **pid**: the PID of the login process (like sshd, tmux, gdm-session-worker, - ...). On Windows and OpenBSD this is always set to ``None``. - - Example:: - - >>> import psutil - >>> psutil.users() - [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), - suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] - - .. versionchanged:: - 5.3.0 added "pid" field - -Processes -========= - -Functions ---------- - -.. function:: pids() - - Return a sorted list of current running PIDs. - To iterate over all processes and avoid race conditions :func:`process_iter()` - should be preferred. - - >>> import psutil - >>> psutil.pids() - [1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, ..., 32498] - - .. versionchanged:: - 5.6.0 PIDs are returned in sorted order - -.. function:: process_iter(attrs=None, ad_value=None) - - Return an iterator yielding a :class:`Process` class instance for all running - processes on the local machine. - This should be preferred over :func:`psutil.pids()` to iterate over - processes, as retrieving info is safe from race conditions. - - Every :class:`Process` instance is only created once, and then cached for the - next time :func:`psutil.process_iter()` is called (if PID is still alive). - Cache can optionally be cleared via ``process_iter.cache_clear()``. - - *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. - If *attrs* is specified :meth:`Process.as_dict()` result will be stored as a - ``info`` attribute attached to the returned :class:`Process` instances. - If *attrs* is an empty list it will retrieve all process info (slow). - - Sorting order in which processes are returned is based on their PID. - - Example:: - - >>> import psutil - >>> for proc in psutil.process_iter(['pid', 'name', 'username']): - ... print(proc.info) - ... - {'name': 'systemd', 'pid': 1, 'username': 'root'} - {'name': 'kthreadd', 'pid': 2, 'username': 'root'} - {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} - ... - - A dict comprehensions to create a ``{pid: info, ...}`` data structure:: - - >>> import psutil - >>> procs = {p.pid: p.info for p in psutil.process_iter(['name', 'username'])} - >>> procs - {1: {'name': 'systemd', 'username': 'root'}, - 2: {'name': 'kthreadd', 'username': 'root'}, - 3: {'name': 'ksoftirqd/0', 'username': 'root'}, - ...} - - Clear internal cache:: - - >>> psutil.process_iter.cache_clear() - - .. versionchanged:: - 5.3.0 added "attrs" and "ad_value" parameters. - - .. versionchanged:: - 6.0.0 no longer checks whether each yielded process PID has been reused. - - .. versionchanged:: - 6.0.0 added ``psutil.process_iter.cache_clear()`` API. - -.. function:: pid_exists(pid) - - Check whether the given PID exists in the current process list. This is - faster than doing ``pid in psutil.pids()`` and should be preferred. - -.. function:: wait_procs(procs, timeout=None, callback=None) - - Convenience function which waits for a list of :class:`Process` instances to - terminate. Return a ``(gone, alive)`` tuple indicating which processes are - gone and which ones are still alive. The *gone* ones will have a new - *returncode* attribute indicating process exit status as returned by - :meth:`Process.wait`. - ``callback`` is a function which gets called when one of the processes being - waited on is terminated and a :class:`Process` instance is passed as callback - argument (the instance will also have a *returncode* attribute set). - This function will return as soon as all processes terminate or when - *timeout* (seconds) occurs. - Differently from :meth:`Process.wait` it will not raise - :class:`TimeoutExpired` if timeout occurs. - A typical use case may be: - - - send SIGTERM to a list of processes - - give them some time to terminate - - send SIGKILL to those ones which are still alive - - Example which terminates and waits all the children of this process:: - - import psutil - - def on_terminate(proc): - print("process {} terminated with exit code {}".format(proc, proc.returncode)) - - procs = psutil.Process().children() - for p in procs: - p.terminate() - gone, alive = psutil.wait_procs(procs, timeout=3, callback=on_terminate) - for p in alive: - p.kill() - -Exceptions ----------- - -.. class:: Error() - - Base exception class. All other exceptions inherit from this one. - -.. class:: NoSuchProcess(pid, name=None, msg=None) - - Raised by :class:`Process` class methods when no process with the given - *pid* is found in the current process list, or when a process no longer - exists. *name* is the name the process had before disappearing - and gets set only if :meth:`Process.name()` was previously called. - -.. class:: ZombieProcess(pid, name=None, ppid=None, msg=None) - - This may be raised by :class:`Process` class methods when querying a zombie - process on UNIX (Windows doesn't have zombie processes). - *name* and *ppid* attributes are available if :meth:`Process.name()` or - :meth:`Process.ppid()` methods were called before the process turned into a - zombie. - - .. note:: - - this is a subclass of :class:`NoSuchProcess` so if you're not interested - in retrieving zombies (e.g. when using :func:`process_iter()`) you can - ignore this exception and just catch :class:`NoSuchProcess`. - - .. versionadded:: 3.0.0 - -.. class:: AccessDenied(pid=None, name=None, msg=None) - - Raised by :class:`Process` class methods when permission to perform an - action is denied due to insufficient privileges. - *name* attribute is available if :meth:`Process.name()` was previously called. - -.. class:: TimeoutExpired(seconds, pid=None, name=None, msg=None) - - Raised by :meth:`Process.wait` method if timeout expires and the process is - still alive. - *name* attribute is available if :meth:`Process.name()` was previously called. - -Process class -------------- - -.. class:: Process(pid=None) - - Represents an OS process with the given *pid*. - If *pid* is omitted current process *pid* (`os.getpid`_) is used. - Raise :class:`NoSuchProcess` if *pid* does not exist. - On Linux *pid* can also refer to a thread ID (the *id* field returned by - :meth:`threads` method). - When accessing methods of this class always be prepared to catch - :class:`NoSuchProcess` and :class:`AccessDenied` exceptions. - `hash`_ builtin can be used against instances of this class in order to - identify a process univocally over time (the hash is determined by mixing - process PID + creation time). As such it can also be used with `set`_. - - .. note:: - - In order to efficiently fetch more than one information about the process - at the same time, make sure to use either :meth:`oneshot` context manager - or :meth:`as_dict` utility method. - - .. note:: - - the way this class is bound to a process is via its **PID**. - That means that if the process terminates and the OS reuses its PID you may - inadvertently end up querying another process. To prevent this problem - you can use :meth:`is_running()` first. - The only methods which preemptively check whether PID has been reused - (via PID + creation time) are: - :meth:`nice` (set), - :meth:`ionice` (set), - :meth:`cpu_affinity` (set), - :meth:`rlimit` (set), - :meth:`children`, - :meth:`ppid`, - :meth:`parent`, - :meth:`parents`, - :meth:`suspend` - :meth:`resume`, - :meth:`send_signal`, - :meth:`terminate` and - :meth:`kill`. - - .. method:: oneshot() - - Utility context manager which considerably speeds up the retrieval of - multiple process information at the same time. - Internally different process info (e.g. :meth:`name`, :meth:`ppid`, - :meth:`uids`, :meth:`create_time`, ...) may be fetched by using the same - routine, but only one value is returned and the others are discarded. - When using this context manager the internal routine is executed once (in - the example below on :meth:`name()`) the value of interest is returned and - the others are cached. - The subsequent calls sharing the same internal routine will return the - cached value. - The cache is cleared when exiting the context manager block. - The advice is to use this every time you retrieve more than one information - about the process. If you're lucky, you'll get a hell of a speedup. - Example: - - >>> import psutil - >>> p = psutil.Process() - >>> with p.oneshot(): - ... p.name() # execute internal routine once collecting multiple info - ... p.cpu_times() # return cached value - ... p.cpu_percent() # return cached value - ... p.create_time() # return cached value - ... p.ppid() # return cached value - ... p.status() # return cached value - ... - >>> - - Here's a list of methods which can take advantage of the speedup depending - on what platform you're on. - In the table below horizontal empty rows indicate what process methods can - be efficiently grouped together internally. - The last column (speedup) shows an approximation of the speedup you can get - if you call all the methods together (best case scenario). - - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | Linux | Windows | macOS | BSD | SunOS | AIX | - +==============================+===============================+==============================+==============================+==========================+==========================+ - | :meth:`cpu_num` | :meth:`~Process.cpu_percent` | :meth:`~Process.cpu_percent` | :meth:`cpu_num` | :meth:`name` | :meth:`name` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`~Process.cpu_percent` | :meth:`cpu_times` | :meth:`cpu_times` | :meth:`~Process.cpu_percent` | :meth:`cmdline` | :meth:`cmdline` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`cpu_times` | :meth:`io_counters()` | :meth:`memory_info` | :meth:`cpu_times` | :meth:`create_time` | :meth:`create_time` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`create_time` | :meth:`memory_info` | :meth:`memory_percent` | :meth:`create_time` | | | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`name` | :meth:`memory_info_ex` | :meth:`num_ctx_switches` | :meth:`gids` | :meth:`memory_info` | :meth:`memory_info` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`ppid` | :meth:`num_ctx_switches` | :meth:`num_threads` | :meth:`io_counters` | :meth:`memory_percent` | :meth:`memory_percent` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`status` | :meth:`num_handles` | | :meth:`name` | :meth:`num_threads` | :meth:`num_threads` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`terminal` | :meth:`num_threads` | :meth:`create_time` | :meth:`memory_info` | :meth:`ppid` | :meth:`ppid` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | | | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`gids` | :meth:`exe` | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`memory_info_ex` | :meth:`name` | :meth:`ppid` | :meth:`ppid` | | | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_ctx_switches` | | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_threads` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`uids` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`username` | | :meth:`uids` | :meth:`uids` | :meth:`username` | :meth:`username` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | | | :meth:`username` | :meth:`username` | | | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`memory_footprint` | | | | | | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`memory_maps` | | | | | | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | *speedup: +2.6x* | *speedup: +1.8x / +6.5x* | *speedup: +1.9x* | *speedup: +2.0x* | *speedup: +1.3x* | *speedup: +1.3x* | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - - .. versionadded:: 5.0.0 - - .. attribute:: pid - - The process PID. This is the only (read-only) attribute of the class. - - .. method:: ppid() - - The process parent PID. On Windows the return value is cached after first - call. Not on POSIX because ppid may change if process becomes a zombie - See also :meth:`parent` and :meth:`parents` methods. - - .. method:: name() - - The process name. On Windows the return value is cached after first - call. Not on POSIX because the process name may change. - See also how to `find a process by name <#find-process-by-name>`__. - - .. method:: exe() - - The process executable as an absolute path. On some systems, if exe cannot - be determined for some internal reason (e.g. system process or path no - longer exists), this may be an empty string. The return value is cached - after first call. - - >>> import psutil - >>> psutil.Process().exe() - '/usr/bin/python3' - - .. method:: cmdline() - - The command line this process has been called with as a list of strings. - The return value is not cached because the cmdline of a process may change. - - >>> import psutil - >>> psutil.Process().cmdline() - ['python', 'manage.py', 'runserver'] - - .. method:: environ() - - The environment variables of the process as a dict. Note: this might not - reflect changes made after the process started. - - >>> import psutil - >>> psutil.Process().environ() - {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} - - .. note:: - on macOS Big Sur this function returns something meaningful only for the - current process or in - `other specific circumstances `__). - - .. versionadded:: 4.0.0 - .. versionchanged:: 5.3.0 added SunOS support - .. versionchanged:: 5.6.3 added AIX support - .. versionchanged:: 5.7.3 added BSD support - - .. method:: create_time() - - The process creation time as a floating point number expressed in seconds - since the epoch (seconds since January 1, 1970, at midnight UTC). The - return value, which is cached after first call, is based on the system - clock, which means it may be affected by changes such as manual adjustments - or time synchronization (e.g. NTP). - - >>> import psutil, datetime - >>> p = psutil.Process() - >>> p.create_time() - 1307289803.47 - >>> datetime.datetime.fromtimestamp(p.create_time()).strftime("%Y-%m-%d %H:%M:%S") - '2011-03-05 18:03:52' - - .. method:: as_dict(attrs=None, ad_value=None) - - Utility method retrieving multiple process information as a dictionary. - If *attrs* is specified it must be a list of strings reflecting available - :class:`Process` class's attribute names. Here's a list of possible string - values: - ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_footprint'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_info_ex'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. - If *attrs* argument is not passed all public read only attributes are - assumed. - *ad_value* is the value which gets assigned to a dict key in case - :class:`AccessDenied` or :class:`ZombieProcess` exception is raised when - retrieving that particular process information. - Internally, :meth:`as_dict` uses :meth:`oneshot` context manager so - there's no need you use it also. - - >>> import psutil - >>> p = psutil.Process() - >>> p.as_dict(attrs=['pid', 'name', 'username']) - {'username': 'giampaolo', 'pid': 12366, 'name': 'python'} - >>> - >>> # get a list of valid attrs names - >>> list(psutil.Process().as_dict().keys()) - ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] - - .. versionchanged:: - 3.0.0 *ad_value* is used also when incurring into - :class:`ZombieProcess` exception, not only :class:`AccessDenied` - - .. versionchanged:: 4.5.0 :meth:`as_dict` is considerably faster thanks - to :meth:`oneshot` context manager. - - .. method:: parent() - - Utility method which returns the parent process as a :class:`Process` - object, preemptively checking whether PID has been reused. If no parent - PID is known return ``None``. - See also :meth:`ppid` and :meth:`parents` methods. - - .. method:: parents() - - Utility method which return the parents of this process as a list of - :class:`Process` instances. If no parents are known return an empty list. - See also :meth:`ppid` and :meth:`parent` methods. - - .. versionadded:: 5.6.0 - - .. method:: status() - - The current process status as a :class:`psutil.ProcessStatus` enum member. - The returned value is one of the - `psutil.STATUS_* <#process-status-constants>`_ constants. - - .. versionchanged:: 8.0.0 return value is now a :class:`psutil.ProcessStatus` - enum member instead of a plain ``str``. - - .. method:: cwd() - - The process current working directory as an absolute path. If cwd cannot be - determined for some internal reason (e.g. system process or directory no - longer exists) it may return an empty string. - - .. versionchanged:: 5.6.4 added support for NetBSD - - .. method:: username() - - The name of the user that owns the process. On UNIX this is calculated by - using real process uid. - - .. method:: uids() - - The real, effective and saved user ids of this process as a named tuple. - This is the same as `os.getresuid`_ but can be used for any process PID. - - Availability: UNIX - - .. method:: gids() - - The real, effective and saved group ids of this process as a named tuple. - This is the same as `os.getresgid`_ but can be used for any process PID. - - Availability: UNIX - - .. method:: terminal() - - The terminal associated with this process, if any, else ``None``. This is - similar to "tty" command but can be used for any process PID. - - Availability: UNIX - - .. method:: nice(value=None) - - Get or set process niceness (priority). - On UNIX this is a number which usually goes from ``-20`` to ``20``. - The higher the nice value, the lower the priority of the process. - - >>> import psutil - >>> p = psutil.Process() - >>> p.nice(10) # set - >>> p.nice() # get - 10 - >>> - - Starting from Python 3.3 this functionality is also available as - `os.getpriority`_ and `os.setpriority`_ (see `BPO-10784`_). - On Windows this is implemented via `GetPriorityClass`_ and - `SetPriorityClass`_ Windows APIs and *value* is one of the - :data:`psutil.*_PRIORITY_CLASS ` - constants reflecting the MSDN documentation. - The return value on Windows is a :class:`psutil.ProcessPriority` enum member. - Example which increases process priority on Windows: - - >>> p.nice(psutil.HIGH_PRIORITY_CLASS) - - .. versionchanged:: 8.0.0 on Windows, return value is now a - :class:`psutil.ProcessPriority` enum member. - - .. method:: ionice(ioclass=None, value=None) - - Get or set process I/O niceness (priority). - If no argument is provided it acts as a get, returning a ``(ioclass, value)`` - tuple on Linux and a *ioclass* integer on Windows. - If *ioclass* is provided it acts as a set. In this case an additional - *value* can be specified on Linux only in order to increase or decrease the - I/O priority even further. - Here's the possible platform-dependent *ioclass* values. - - Linux (see `ioprio_get`_ manual): - - * ``IOPRIO_CLASS_RT``: (high) the process gets first access to the disk - every time. Use it with care as it can starve the entire - system. Additional priority *level* can be specified and ranges from - ``0`` (highest) to ``7`` (lowest). - * ``IOPRIO_CLASS_BE``: (normal) the default for any process that hasn't set - a specific I/O priority. Additional priority *level* ranges from - ``0`` (highest) to ``7`` (lowest). - * ``IOPRIO_CLASS_IDLE``: (low) get I/O time when no-one else needs the disk. - No additional *value* is accepted. - * ``IOPRIO_CLASS_NONE``: returned when no priority was previously set. - - Windows: - - * ``IOPRIO_HIGH``: highest priority. - * ``IOPRIO_NORMAL``: default priority. - * ``IOPRIO_LOW``: low priority. - * ``IOPRIO_VERYLOW``: lowest priority. - - Here's an example on how to set the highest I/O priority depending on what - platform you're on:: - - >>> import psutil - >>> p = psutil.Process() - >>> if psutil.LINUX: - ... p.ionice(psutil.IOPRIO_CLASS_RT, value=7) - ... else: - ... p.ionice(psutil.IOPRIO_HIGH) - ... - >>> p.ionice() # get - pionice(ioclass=, value=7) - - Availability: Linux, Windows Vista+ - - .. versionchanged:: 5.6.2 Windows accepts new ``IOPRIO_*`` constants. - - .. versionchanged:: 8.0.0 *ioclass* is now a - :class:`psutil.ProcessIOPriority` enum member. - - .. method:: rlimit(resource, limits=None) - - Get or set process resource limits (see `man prlimit`_). *resource* is one - of the `psutil.RLIMIT_* <#process-resources-constants>`_ constants. - *limits* is a ``(soft, hard)`` tuple. - This is the same as `resource.getrlimit`_ and `resource.setrlimit`_ - but can be used for any process PID, not only `os.getpid`_. - For get, return value is a ``(soft, hard)`` tuple. Each value may be either - and integer or :data:`psutil.RLIMIT_* `. - Example: - - >>> import psutil - >>> p = psutil.Process() - >>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128)) # process can open max 128 file descriptors - >>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024)) # can create files no bigger than 1024 bytes - >>> p.rlimit(psutil.RLIMIT_FSIZE) # get - (1024, 1024) - >>> - - Also see `procinfo.py`_ script. - - Availability: Linux, FreeBSD - - .. versionchanged:: 5.7.3 added FreeBSD support - - .. method:: io_counters() - - Return process I/O statistics as a named tuple. - For Linux you can refer to - `/proc filesystem documentation `__. - - - **read_count**: the number of read operations performed (cumulative). - This is supposed to count the number of read-related syscalls such as - ``read()`` and ``pread()`` on UNIX. - - **write_count**: the number of write operations performed (cumulative). - This is supposed to count the number of write-related syscalls such as - ``write()`` and ``pwrite()`` on UNIX. - - **read_bytes**: the number of bytes read (cumulative). - Always ``-1`` on BSD. - - **write_bytes**: the number of bytes written (cumulative). - Always ``-1`` on BSD. - - Linux specific: - - - **read_chars** *(Linux)*: the amount of bytes which this process passed - to ``read()`` and ``pread()`` syscalls (cumulative). - Differently from *read_bytes* it doesn't care whether or not actual - physical disk I/O occurred. - - **write_chars** *(Linux)*: the amount of bytes which this process passed - to ``write()`` and ``pwrite()`` syscalls (cumulative). - Differently from *write_bytes* it doesn't care whether or not actual - physical disk I/O occurred. - - Windows specific: - - - **other_count** *(Windows)*: the number of I/O operations performed - other than read and write operations. - - **other_bytes** *(Windows)*: the number of bytes transferred during - operations other than read and write operations. - - >>> import psutil - >>> p = psutil.Process() - >>> p.io_counters() - pio(read_count=454556, write_count=3456, read_bytes=110592, write_bytes=0, read_chars=769931, write_chars=203) - - Availability: Linux, BSD, Windows, AIX - - .. versionchanged:: 5.2.0 added *read_chars* and *write_chars* on Linux; - added *other_count* and *other_bytes* on Windows. - - .. method:: num_ctx_switches() - - The number voluntary and involuntary context switches performed by - this process (cumulative). - - .. note:: - (Windows, macOS) *involuntary* value is always set to 0, while - *voluntary* value reflect the total number of context switches (voluntary - + involuntary). This is a limitation of the OS. - - .. versionchanged:: 5.4.1 added AIX support - - .. method:: num_fds() - - The number of file descriptors currently opened by this process - (non cumulative). - - Availability: UNIX - - .. method:: num_handles() - - The number of handles currently used by this process (non cumulative). - - Availability: Windows - - .. method:: num_threads() - - The number of threads currently used by this process (non cumulative). - - .. method:: threads() - - Return threads opened by process as a list of named tuples. On OpenBSD this - method requires root privileges. - - - **id**: the native thread ID assigned by the kernel. If :attr:`pid` refers - to the current process, this matches the - `native_id `__ - attribute of the `threading.Thread`_ class, and can be used to reference - individual Python threads running within your own Python app. - - **user_time**: time spent in user mode. - - **system_time**: time spent in kernel mode. - - .. method:: cpu_times() - - Return a named tuple representing the accumulated process times, in seconds - (see `explanation `__). - This is similar to `os.times`_ but can be used for any process PID. - - - **user**: time spent in user mode. - - **system**: time spent in kernel mode. - - **children_user**: user time of all child processes (always ``0`` on - Windows and macOS). - - **children_system**: system time of all child processes (always ``0`` on - Windows and macOS). - - **iowait**: (Linux) time spent waiting for blocking I/O to complete. - This value is excluded from `user` and `system` times count (because the - CPU is not doing any work). - - >>> import psutil - >>> p = psutil.Process() - >>> p.cpu_times() - pcputimes(user=0.03, system=0.67, children_user=0.0, children_system=0.0, iowait=0.08) - >>> sum(p.cpu_times()[:2]) # cumulative, excluding children and iowait - 0.70 - - .. versionchanged:: - 4.1.0 return two extra fields: *children_user* and *children_system*. - - .. versionchanged:: - 5.6.4 added *iowait* on Linux. - - .. method:: cpu_percent(interval=None) - - Return a float representing the process CPU utilization as a percentage - which can also be ``> 100.0`` in case of a process running multiple threads - on different CPUs. - When *interval* is > ``0.0`` compares process times to system CPU times - elapsed before and after the interval (blocking). When interval is ``0.0`` - or ``None`` compares process times to system CPU times elapsed since last - call, returning immediately. That means the first time this is called it - will return a meaningless ``0.0`` value which you are supposed to ignore. - In this case is recommended for accuracy that this function be called a - second time with at least ``0.1`` seconds between calls. - Example: - - >>> import psutil - >>> p = psutil.Process() - >>> # blocking - >>> p.cpu_percent(interval=1) - 2.0 - >>> # non-blocking (percentage since last call) - >>> p.cpu_percent(interval=None) - 2.9 - - .. note:: - the returned value can be > 100.0 in case of a process running multiple - threads on different CPU cores. - - .. note:: - the returned value is explicitly *not* split evenly between all available - CPUs (differently from :func:`psutil.cpu_percent()`). - This means that a busy loop process running on a system with 2 logical - CPUs will be reported as having 100% CPU utilization instead of 50%. - This was done in order to be consistent with ``top`` UNIX utility - and also to make it easier to identify processes hogging CPU resources - independently from the number of CPUs. - It must be noted that ``taskmgr.exe`` on Windows does not behave like - this (it would report 50% usage instead). - To emulate Windows ``taskmgr.exe`` behavior you can do: - ``p.cpu_percent() / psutil.cpu_count()``. - - .. warning:: - the first time this method is called with interval = ``0.0`` or - ``None`` it will return a meaningless ``0.0`` value which you are - supposed to ignore. - - .. method:: cpu_affinity(cpus=None) - - Get or set process current - `CPU affinity `__. - CPU affinity consists in telling the OS to run a process on a limited set - of CPUs only (on Linux cmdline, ``taskset`` command is typically used). - If no argument is passed it returns the current CPU affinity as a list - of integers. - If passed it must be a list of integers specifying the new CPUs affinity. - If an empty list is passed all eligible CPUs are assumed (and set). - On some systems such as Linux this may not necessarily mean all available - logical CPUs as in ``list(range(psutil.cpu_count()))``). - - >>> import psutil - >>> psutil.cpu_count() - 4 - >>> p = psutil.Process() - >>> # get - >>> p.cpu_affinity() - [0, 1, 2, 3] - >>> # set; from now on, process will run on CPU #0 and #1 only - >>> p.cpu_affinity([0, 1]) - >>> p.cpu_affinity() - [0, 1] - >>> # reset affinity against all eligible CPUs - >>> p.cpu_affinity([]) - - Availability: Linux, Windows, FreeBSD - - .. versionchanged:: 2.2.0 added support for FreeBSD - .. versionchanged:: 5.1.0 an empty list can be passed to set affinity - against all eligible CPUs. - - .. method:: cpu_num() - - Return what CPU this process is currently running on. - The returned number should be ``<=`` :func:`psutil.cpu_count()`. - On FreeBSD certain kernel process may return ``-1``. - It may be used in conjunction with ``psutil.cpu_percent(percpu=True)`` to - observe the system workload distributed across multiple CPUs as shown by - `cpu_distribution.py`_ example script. - - Availability: Linux, FreeBSD, SunOS - - .. versionadded:: 5.1.0 - - .. method:: memory_info() - - Return a named tuple with variable fields depending on the platform - representing memory information about the process. - The "portable" fields available on all platforms are `rss` and `vms`. - All numbers are expressed in bytes. - - +---------+---------+----------+---------+-----+-----------------+ - | Linux | macOS | BSD | Solaris | AIX | Windows | - +=========+=========+==========+=========+=====+=================+ - | rss | rss | rss | rss | rss | rss | - +---------+---------+----------+---------+-----+-----------------+ - | vms | vms | vms | vms | vms | vms | - +---------+---------+----------+---------+-----+-----------------+ - | shared | | text | | | | - +---------+---------+----------+---------+-----+-----------------+ - | text | | data | | | | - +---------+---------+----------+---------+-----+-----------------+ - | data | | stack | | | | - +---------+---------+----------+---------+-----+-----------------+ - | | | peak_rss | | | peak_rss | - +---------+---------+----------+---------+-----+-----------------+ - | | | | | | peak_vms | - +---------+---------+----------+---------+-----+-----------------+ - - - **rss**: aka "Resident Set Size". The portion of physical memory - currently held by this process (code, data, stack, and mapped files that - are resident). Pages swapped out to disk are **not** counted. On UNIX it - matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. - - - **vms**: aka "Virtual Memory Size". The total address space reserved by - the process, including pages not yet touched, pages in swap, and - memory-mapped files not yet accessed. Typically much larger than - **rss**. On UNIX it matches the ``top`` VIRT column. On Windows this - maps to ``PrivateUsage`` (private committed pages only), which differs - from the UNIX definition; use ``virtual`` from :meth:`memory_info_ex` - for the true virtual address space size. - - - **shared** *(Linux)*: memory backed by a file or device (shared - libraries, mmap'd files, POSIX shared memory) that *could* be shared - with other processes. A page is counted here even if no other process - is currently mapping it. Matches ``top``'s SHR column. - - - **text** *(Linux, BSD)*: aka TRS (Text Resident Set). Resident memory - devoted to executable code. These pages are read-only and typically - shared across all processes running the same binary. Matches ``top``'s - CODE column. - - - **data** *(Linux, BSD)*: aka DRS (Data Resident Set). On Linux this - covers the data **and** stack segments combined (from - ``/proc//statm``). On BSD it covers the data segment only (see - **stack**). Matches ``top``'s DATA column. - - - **stack** *(BSD)*: size of the process stack segment. Reported - separately from **data** (unlike Linux where both are combined). - - - **peak_rss** *(BSD, Windows)*: the highest RSS value (high water mark) - the process has ever reached. On BSD this may be ``0`` for kernel PIDs. - On Windows it maps to ``PeakWorkingSetSize``. - - - **peak_vms** *(Windows)*: peak private committed (page-file-backed) - virtual memory. Maps to ``PeakPagefileUsage``. - - For the full definitions of Windows fields see - `PROCESS_MEMORY_COUNTERS_EX`_. - - Example on Linux: - - >>> import psutil - >>> p = psutil.Process() - >>> p.memory_info() - pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, data=9891840) - - .. versionchanged:: - 4.0.0 multiple fields are returned, not only *rss* and *vms*. - - .. versionchanged:: - 8.0.0 Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). - Deprecated aliases returning 0 and emitting `DeprecationWarning` are - kept. - - .. versionchanged:: - 8.0.0 macOS: *pfaults* and *pageins* removed with **no backward-compat - aliases**. Use :meth:`page_faults` instead. - - .. versionchanged:: - 8.0.0 Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*, - *num_page_faults* → :meth:`page_faults` method. At the same time - *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, *peak_nonpaged_pool* - were moved to :meth:`memory_info_ex`. All these old names still work but - raise `DeprecationWarning`. - - .. versionchanged:: - 8.0.0 BSD: added *peak_rss*. - - .. warning:: - in version 8.0.0 the named tuple changed size and field order. Positional - access (e.g. ``p.memory_info()[3]`` or ``a, b, c = p.memory_info()``) may - break or silently return the wrong field. Always use attribute access - instead (e.g. ``p.memory_info().rss``). - - .. method:: memory_info_ex() - - Return a named tuple extending :meth:`memory_info` with additional - platform-specific memory metrics. On platforms where extra fields are not - implemented this returns the same result as :meth:`memory_info`. All - numbers are expressed in bytes. - - +-------------+----------------+--------------------+ - | Linux | macOS | Windows | - +=============+================+====================+ - | peak_rss | peak_rss | virtual | - +-------------+----------------+--------------------+ - | peak_vms | | peak_virtual | - +-------------+----------------+--------------------+ - | rss_anon | rss_anon | paged_pool | - +-------------+----------------+--------------------+ - | rss_file | rss_file | nonpaged_pool | - +-------------+----------------+--------------------+ - | rss_shmem | wired | peak_paged_pool | - +-------------+----------------+--------------------+ - | swap | compressed | peak_nonpaged_pool | - +-------------+----------------+--------------------+ - | hugetlb | phys_footprint | | - +-------------+----------------+--------------------+ - - - **peak_rss** *(Linux, macOS)*: the highest RSS value (high water mark) - the process has reached since it started. - - **peak_vms** *(Linux)*: the highest VMS value the process has reached - since it started. - - **rss_anon** *(Linux, macOS)*: resident anonymous pages (heap, - stack, private mappings) not backed by any file, such as heap - allocations, stack, and private ``mmap(MAP_ANONYMOUS)`` regions. Set to 0 - on Linux < 4.5. - - **rss_file** *(Linux, macOS)*: resident file-backed memory; pages mapped - from files (shared libraries, mmap'd files). Set to 0 on Linux < 4.5. - - **rss_shmem** *(Linux)*: resident shared memory pages (``tmpfs``, - ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals **rss**. Set to - 0 on Linux < 4.5. - - **wired** *(macOS)*: memory pinned in RAM by the kernel on behalf of this - process; cannot be compressed or paged out. - - **swap** *(Linux)*: process memory currently in swap. Equivalent to - ``memory_footprint().swap`` but cheaper, as it reads from - ``/proc//status`` instead of ``/proc//smaps``. - - **compressed** *(macOS)*: pages held in the in-RAM memory compressor; not - counted in **rss**. A large value signals memory pressure but has not yet - triggered swapping. - - **hugetlb** *(Linux)*: resident memory backed by huge pages. Set to 0 on - Linux < 4.4. - - **phys_footprint** *(macOS)*: total physical memory impact including - compressed pages. What Xcode and ``footprint(1)`` report; prefer this - over **rss** macOS memory monitoring. - - **virtual** *(Windows)*: true virtual address space size, including - reserved-but-uncommitted regions (unlike **vms** in - :meth:`memory_info`). - - **peak_virtual** *(Windows)*: peak virtual address space size. - - **paged_pool** *(Windows)*: kernel memory used for objects created by - this process (open file handles, registry keys, etc.) that the OS may - swap to disk under memory pressure. - - **nonpaged_pool** *(Windows)*: kernel memory used for objects that must - stay in RAM at all times (I/O request packets, device driver buffers, - etc.). A large or growing value may indicate a driver memory leak. - - **peak_paged_pool** *(Windows)*: peak paged-pool usage. - - **peak_nonpaged_pool** *(Windows)*: peak non-paged-pool usage. - - For the full definitions of Windows fields see - `PROCESS_MEMORY_COUNTERS_EX`_. - - .. versionadded:: 8.0.0 - - .. method:: memory_footprint() - - Return a named tuple with USS, PSS and swap memory metrics. These give - a more accurate picture of actual memory consumption than - :meth:`memory_info`, as explained in this - `blog post `__. - It works by walking the full process address space, so it is - considerably slower than :meth:`memory_info` and may require elevated - privileges. - - - **uss** *(Linux, macOS, Windows)*: aka "Unique Set Size". This is the - memory which is unique to a process and which would be freed if the - process were terminated right now. The most representative metric for - actual memory usage. - - - **pss** *(Linux)*: aka "Proportional Set Size", is the amount of memory - shared with other processes, accounted in a way that the amount is - divided evenly between the processes that share it. I.e. if a process has - 10 MBs all to itself, and 10 MBs shared with another process, its PSS - will be 15 MBs. - - - **swap** *(Linux)*: process memory currently in swap, counted per-mapping - (slower, but may be more accurate than ``memory_info_ex().swap``). - - Example on Linux: - - >>> import psutil - >>> p = psutil.Process() - >>> p.memory_footprint() - pfootprint(uss=6545408, pss=6872064, swap=0) - - See also `procsmem.py`_ for an example application. - - .. versionadded:: 8.0.0 - - Availability: Linux, macOS, Windows - - .. method:: memory_full_info() - - This method returns the same information as :meth:`memory_info` plus - :meth:`memory_footprint` in a single named tuple. - - .. versionadded:: 4.0.0 - - .. warning:: - deprecated in version 8.0.0; use :meth:`memory_footprint` instead. - - .. method:: memory_percent(memtype="rss") - - Return process memory usage as a percentage of total physical memory - (``process.memory_info().rss / virtual_memory().total * 100``). - *memtype* can be any field name from :meth:`memory_info`, - :meth:`memory_info_ex`, or :meth:`memory_footprint` and controls which - memory value is used in the calculation (defaults to ``"rss"``). - - .. versionchanged:: 4.0.0 added `memtype` parameter. - - .. method:: memory_maps(grouped=True) - - Return process's mapped memory regions as a list of named tuples whose - fields vary by platform (all values in bytes). If *grouped* is ``True`` - regions with the same *path* are merged and their numeric fields summed. - If *grouped* is ``False`` each region is listed individually and the - tuple also includes *addr* (address range) and *perms* (permission - string e.g. ``"r-xp"``). - See `pmap.py`_ for an example application. - - +---------------+---------+--------------+-----------+ - | Linux | Windows | FreeBSD | Solaris | - +===============+=========+==============+===========+ - | rss | rss | rss | rss | - +---------------+---------+--------------+-----------+ - | size | | private | anonymous | - +---------------+---------+--------------+-----------+ - | pss | | ref_count | locked | - +---------------+---------+--------------+-----------+ - | shared_clean | | shadow_count | | - +---------------+---------+--------------+-----------+ - | shared_dirty | | | | - +---------------+---------+--------------+-----------+ - | private_clean | | | | - +---------------+---------+--------------+-----------+ - | private_dirty | | | | - +---------------+---------+--------------+-----------+ - | referenced | | | | - +---------------+---------+--------------+-----------+ - | anonymous | | | | - +---------------+---------+--------------+-----------+ - | swap | | | | - +---------------+---------+--------------+-----------+ - - Linux fields (from ``/proc//smaps``): - - - **rss**: resident pages in this mapping. - - **size**: total virtual size; may far exceed **rss** for sparse or - reserved-but-unaccessed mappings. - - **pss**: proportional RSS. **rss** divided by the number of processes - sharing this mapping. Useful for fair per-process accounting. - - **shared_clean**: shared pages not modified (e.g. shared library code); - can be dropped from RAM without writing to swap. - - **shared_dirty**: shared pages that have been written to. - - **private_clean**: private unmodified pages; can be dropped without - writing to swap. - - **private_dirty**: private modified pages; must be written to swap - before they can be reclaimed. The key indicator of a mapping's real - memory cost. - - **referenced**: pages recently accessed. - - **anonymous**: pages not backed by a file (heap, stack allocations). - - **swap**: pages from this mapping currently in swap. - - FreeBSD fields: - - - **private**: pages in this mapping private to this process. - - **ref_count**: reference count on the VM object backing this mapping. - - **shadow_count**: depth of the copy-on-write shadow object chain. - - >>> import psutil - >>> p = psutil.Process() - >>> p.memory_maps() - [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), - pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), - ...] - - Availability: Linux, Windows, FreeBSD, SunOS - - .. versionchanged:: - 5.6.0 removed macOS support because inherently broken (see - issue `#1291 `__) - - .. method:: children(recursive=False) - - Return the children of this process as a list of :class:`Process` - instances. - If recursive is `True` return all the parent descendants. - Pseudo code example assuming *A == this process*: - :: - - A ─┐ - │ - ├─ B (child) ─┐ - │ └─ X (grandchild) ─┐ - │ └─ Y (great grandchild) - ├─ C (child) - └─ D (child) - - >>> p.children() - B, C, D - >>> p.children(recursive=True) - B, X, Y, C, D - - Note that in the example above if process X disappears process Y won't be - returned either as the reference to process A is lost. - This concept is well summaried by this - `unit test `__. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. - - .. method:: page_faults() - - Return the number of page faults for this process as a ``(minor, major)`` - named tuple. - - - **minor** (a.k.a. *soft* faults): occur when a memory page is not - currently mapped into the process address space, but is already present - in physical RAM (e.g. a shared library loaded by another process). The - kernel resolves these without disk I/O. - - **major** (a.k.a. *hard* faults): occur when the page must be fetched - from disk. These are expensive because they stall the process until I/O - completes. - - Both counters are cumulative since process creation. Example:: - - >>> import psutil - >>> p = psutil.Process() - >>> p.page_faults() - ppagefaults(minor=5905, major=3) - - .. versionadded:: 8.0.0 - - .. method:: open_files() - - Return regular files opened by process as a list of named tuples including - the following fields: - - - **path**: the absolute file name. - - **fd**: the file descriptor number; on Windows this is always ``-1``. - - Linux only: - - - **position** (*Linux*): the file (offset) position. - - **mode** (*Linux*): a string indicating how the file was opened, similarly - to `open`_ builtin ``mode`` argument. - Possible values are ``'r'``, ``'w'``, ``'a'``, ``'r+'`` and ``'a+'``. - There's no distinction between files opened in binary or text mode - (``"b"`` or ``"t"``). - - **flags** (*Linux*): the flags which were passed to the underlying - `os.open`_ C call when the file was opened (e.g. `os.O_RDONLY`_, - `os.O_TRUNC`_, etc). - - >>> import psutil - >>> f = open('file.ext', 'w') - >>> p = psutil.Process() - >>> p.open_files() - [popenfile(path='/home/giampaolo/svn/psutil/file.ext', fd=3, position=0, mode='w', flags=32769)] - - .. warning:: - on Windows this method is not reliable due to some limitations of the - underlying Windows API which may hang when retrieving certain file - handles. - In order to work around that psutil spawns a thread to determine the file - handle name and kills it if it's not responding after 100ms. - That implies that this method on Windows is not guaranteed to enumerate - all regular file handles (see - `issue 597 `_). - Tools like ProcessHacker has the same limitation. - - .. warning:: - on BSD this method can return files with a null path ("") due to a - kernel bug, hence it's not reliable - (see `issue 595 `_). - - .. versionchanged:: - 3.1.0 no longer hangs on Windows. - - .. versionchanged:: - 4.1.0 new *position*, *mode* and *flags* fields on Linux. - - .. method:: net_connections(kind="inet") - - Return socket connections opened by process as a list of named tuples. - To get system-wide connections use :func:`psutil.net_connections()`. - Every named tuple provides 6 attributes: - - - **fd**: the socket file descriptor. If the connection refers to the - current process this may be passed to `socket.fromfd`_ to obtain a usable - socket object. - On Windows, FreeBSD and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or - `AF_UNIX`_. - - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or - `SOCK_SEQPACKET`_. . - - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` - in case of AF_UNIX sockets. For UNIX sockets see notes below. - - **raddr**: the remote address as a ``(ip, port)`` named tuple or an - absolute ``path`` in case of UNIX sockets. - When the remote endpoint is not connected you'll get an empty tuple - (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. - - **status**: represents the status of a TCP connection. The return value - is one of the :data:`psutil.CONN_* ` constants. - For UDP and UNIX sockets this is always going to be - :const:`psutil.CONN_NONE`. - - The *kind* parameter is a string which filters for connections that fit the - following criteria: - - +----------------+-----------------------------------------------------+ - | **Kind value** | **Connections using** | - +================+=====================================================+ - | ``"inet"`` | IPv4 and IPv6 | - +----------------+-----------------------------------------------------+ - | ``"inet4"`` | IPv4 | - +----------------+-----------------------------------------------------+ - | ``"inet6"`` | IPv6 | - +----------------+-----------------------------------------------------+ - | ``"tcp"`` | TCP | - +----------------+-----------------------------------------------------+ - | ``"tcp4"`` | TCP over IPv4 | - +----------------+-----------------------------------------------------+ - | ``"tcp6"`` | TCP over IPv6 | - +----------------+-----------------------------------------------------+ - | ``"udp"`` | UDP | - +----------------+-----------------------------------------------------+ - | ``"udp4"`` | UDP over IPv4 | - +----------------+-----------------------------------------------------+ - | ``"udp6"`` | UDP over IPv6 | - +----------------+-----------------------------------------------------+ - | ``"unix"`` | UNIX socket (both UDP and TCP protocols) | - +----------------+-----------------------------------------------------+ - | ``"all"`` | the sum of all the possible families and protocols | - +----------------+-----------------------------------------------------+ - - Example: - - >>> import psutil - >>> p = psutil.Process(1694) - >>> p.name() - 'firefox' - >>> p.net_connections() - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=), - pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=), - pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=)] - - .. warning:: - On Linux, retrieving connections for certain processes requires root - privileges. If psutil is not run as root, those connections are silently - skipped instead of raising :class:`psutil.AccessDenied`. That means - the returned list may be incomplete. - - .. note:: - (Solaris) UNIX sockets are not supported. - - .. note:: - (Linux, FreeBSD) *raddr* field for UNIX sockets is always set to "". - This is a limitation of the OS. - - .. note:: - (OpenBSD) *laddr* and *raddr* fields for UNIX sockets are always set to - "". This is a limitation of the OS. - - .. note:: - (AIX) :class:`psutil.AccessDenied` is always raised unless running - as root (lsof does the same). - - .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. - - .. versionchanged:: 6.0.0 : method renamed from `connections` to - `net_connections`. - - .. versionchanged:: 8.0.0 *status* field is now a - :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. - - .. method:: connections() - - Same as :meth:`net_connections` (deprecated). - - .. warning:: - deprecated in version 6.0.0; use :meth:`net_connections` instead. - - .. method:: is_running() - - Return whether the current process is running in the current process list. - This is reliable also in case the process is gone and its PID reused by - another process, therefore it must be preferred over doing - ``psutil.pid_exists(p.pid)``. - If PID has been reused this method will also remove the process from - :func:`process_iter()` internal cache. - - .. note:: - this will return ``True`` also if the process is a zombie - (``p.status() == psutil.STATUS_ZOMBIE``). - - .. versionchanged:: 6.0.0 : automatically remove process from - :func:`process_iter()` internal cache if PID has been reused by another - process. - - .. method:: send_signal(signal) - - Send a signal to process (see `signal module`_ constants) preemptively - checking whether PID has been reused. - On UNIX this is the same as ``os.kill(pid, sig)``. - On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals - are supported and *SIGTERM* is treated as an alias for :meth:`kill()`. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. - - .. versionchanged:: - 3.2.0 support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows - was added. - - .. method:: suspend() - - Suspend process execution with *SIGSTOP* signal preemptively checking - whether PID has been reused. - On UNIX this is the same as ``os.kill(pid, signal.SIGSTOP)``. - On Windows this is done by suspending all process threads execution. - - .. method:: resume() - - Resume process execution with *SIGCONT* signal preemptively checking - whether PID has been reused. - On UNIX this is the same as ``os.kill(pid, signal.SIGCONT)``. - On Windows this is done by resuming all process threads execution. - - .. method:: terminate() - - Terminate the process with *SIGTERM* signal preemptively checking - whether PID has been reused. - On UNIX this is the same as ``os.kill(pid, signal.SIGTERM)``. - On Windows this is an alias for :meth:`kill`. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. - - .. method:: kill() - - Kill the current process by using *SIGKILL* signal preemptively - checking whether PID has been reused. - On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. - On Windows this is done by using `TerminateProcess`_. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. - - .. method:: wait(timeout=None) - - Wait for a process PID to terminate. The details about the return value - differ on UNIX and Windows. - - *On UNIX*: if the process terminated normally, the return value is a - positive integer >= 0 indicating the exit code. - If the process was terminated by a signal return the negated value of the - signal which caused the termination (e.g. ``-SIGTERM``). - If PID is not a child of `os.getpid`_ (current process) just wait until - the process disappears and return ``None``. - If PID does not exist return ``None`` immediately. - - *On Windows*: always return the exit code, which is a positive integer as - returned by `GetExitCodeProcess`_. - - *timeout* is expressed in seconds. If specified and the process is still - alive raise :class:`TimeoutExpired` exception. - ``timeout=0`` can be used in non-blocking apps: it will either return - immediately or raise :class:`TimeoutExpired`. - - The return value is cached. - To wait for multiple processes use :func:`psutil.wait_procs()`. - - >>> import psutil - >>> p = psutil.Process(9891) - >>> p.terminate() - >>> p.wait() - - - .. note:: - - When ``timeout`` is not ``None`` and the platform supports it, an - efficient event-driven mechanism is used to wait for process termination: - - - Linux >= 5.3 with Python >= 3.9 uses `os.pidfd_open`_ + `select.poll`_ - - macOS and other BSD variants use `select.kqueue`_ + ``KQ_FILTER_PROC`` - + ``KQ_NOTE_EXIT`` - - Windows uses ``WaitForSingleObject`` - - If none of these mechanisms are available, the function falls back to a - busy loop (non-blocking call and short sleeps). - - .. versionchanged:: 5.7.2 - if *timeout* is not ``None``, use efficient event-driven implementation - on Linux >= 5.3 and macOS / BSD. - - .. versionchanged:: 5.7.1 return value is cached (instead of returning - ``None``). - - .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it - as a human readable `enum`_. - - .. versionchanged:: 7.2.2 on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, - use `os.pidfd_open`_ and `select.kqueue`_ respectively, instead of less - efficient busy-loop polling. - -.. class:: Popen(*args, **kwargs) - - Same as `subprocess.Popen`_ but in addition it provides all - :class:`psutil.Process` methods in a single class. - For the following methods which are common to both classes, psutil - implementation takes precedence: - :meth:`send_signal() `, - :meth:`terminate() `, - :meth:`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 - >>> - >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) - >>> p.name() - 'python' - >>> p.username() - 'giampaolo' - >>> p.communicate() - ('hello\n', None) - >>> p.wait(timeout=2) - 0 - >>> - - .. versionchanged:: 4.4.0 added context manager support - -C heap introspection -==================== - -The following functions provide direct access to the platform's native C heap -allocator (such as glibc's ``malloc`` on Linux or ``jemalloc`` on BSD). They -are low-level interfaces intended for detecting memory leaks in C extensions, -which are usually not revealed via standard RSS/VMS metrics. These functions do -not reflect Python object memory; they operate solely on allocations made in C -via ``malloc()``, ``free()``, and related calls. - -The general idea behind these functions is straightforward: capture the state -of the C heap before and after repeatedly invoking a function implemented in a -C extension, and compare the results. If ``heap_used`` or ``mmap_used`` grows -steadily across iterations, the C code is likely retaining memory it should be -releasing. This provides an allocator-level way to spot native leaks that -Python's memory tracking misses. - -Check out `psleak`_ project to see a practical example of how these APIs can be -used to detect memory leaks in C extensions. - -.. function:: heap_info() - - Return low-level heap statistics from the system's C allocator. On Linux, - this exposes ``uordblks`` and ``hblkhd`` fields from glibc's `mallinfo2`_. - Returns a named tuple containing: - - - ``heap_used``: total number of bytes currently allocated via ``malloc()`` - (small allocations). - - ``mmap_used``: total number of bytes currently allocated via ``mmap()`` or - via large ``malloc()`` allocations. Always set to 0 on macOS. - - ``heap_count``: (Windows only) number of private heaps created via - ``HeapCreate()``. - - >>> import psutil - >>> psutil.heap_info() - pheap(heap_used=5177792, mmap_used=819200) - - These fields reflect how unreleased C allocations affect the heap: - - +---------------+------------------------------------------------------------------------------------+-----------------+ - | Platform | Allocation type | Affected field | - +===============+====================================================================================+=================+ - | UNIX / glibc | small ``malloc()`` ≤128KB without ``free()`` | ``heap_used`` | - +---------------+------------------------------------------------------------------------------------+-----------------+ - | UNIX / glibc | large ``malloc()`` >128KB without ``free()`` , or ``mmap()`` without ``munmap()`` | ``mmap_used`` | - +---------------+------------------------------------------------------------------------------------+-----------------+ - | Windows | ``HeapAlloc()`` without ``HeapFree()`` | ``heap_used`` | - +---------------+------------------------------------------------------------------------------------+-----------------+ - | Windows | ``VirtualAlloc()`` without ``VirtualFree()`` | ``mmap_used`` | - +---------------+------------------------------------------------------------------------------------+-----------------+ - | Windows | ``HeapCreate()`` without ``HeapDestroy()`` | ``heap_count`` | - +---------------+------------------------------------------------------------------------------------+-----------------+ - - .. versionadded:: 7.2.0 - - Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD - -.. function:: heap_trim() - - Request that the underlying allocator free any unused memory it's holding in - the heap (typically small ``malloc()`` allocations). - - In practice, modern allocators rarely comply, so this is not a - general-purpose memory-reduction tool and won't meaningfully shrink RSS in - real programs. Its primary value is in **leak detection tools**. - - Calling ``heap_trim()`` before taking measurements helps reduce allocator - noise, giving you a cleaner baseline so that changes in ``heap_used`` come - from the code you're testing, not from internal allocator caching or - fragmentation. Its effectiveness depends on allocator behavior and - fragmentation patterns. - - .. versionadded:: 7.2.0 - - Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD - -Windows services -================ - -.. function:: win_service_iter() - - Return an iterator yielding a :class:`WindowsService` class instance for all - Windows services installed. - - .. versionadded:: 4.2.0 - - Availability: Windows - -.. function:: win_service_get(name) - - Get a Windows service by name, returning a :class:`WindowsService` instance. - Raise :class:`psutil.NoSuchProcess` if no service with such name exists. - - .. versionadded:: 4.2.0 - - Availability: Windows - -.. class:: WindowsService - - Represents a Windows service with the given *name*. This class is returned - by :func:`win_service_iter` and :func:`win_service_get` functions and it is - not supposed to be instantiated directly. - - .. method:: name() - - The service name. This string is how a service is referenced and can be - passed to :func:`win_service_get` to get a new :class:`WindowsService` - instance. - - .. method:: display_name() - - The service display name. The value is cached when this class is - instantiated. - - .. method:: binpath() - - The fully qualified path to the service binary/exe file as a string, - including command line arguments. - - .. method:: username() - - The name of the user that owns this service. - - .. method:: start_type() - - A string which can either be `"automatic"`, `"manual"` or `"disabled"`. - - .. method:: pid() - - The process PID, if any, else `None`. This can be passed to - :class:`Process` class to control the service's process. - - .. method:: status() - - Service status as a string, which may be either `"running"`, `"paused"`, - `"start_pending"`, `"pause_pending"`, `"continue_pending"`, - `"stop_pending"` or `"stopped"`. - - .. method:: description() - - Service long description. - - .. method:: as_dict() - - Utility method retrieving all the information above as a dictionary. - - .. versionadded:: 4.2.0 - - Availability: Windows - -Example code: - - >>> import psutil - >>> list(psutil.win_service_iter()) - [, - , - , - , - ...] - >>> s = psutil.win_service_get('alg') - >>> s.as_dict() - {'binpath': 'C:\\Windows\\System32\\alg.exe', - 'description': 'Provides support for 3rd party protocol plug-ins for Internet Connection Sharing', - 'display_name': 'Application Layer Gateway Service', - 'name': 'alg', - 'pid': None, - 'start_type': 'manual', - 'status': 'stopped', - 'username': 'NT AUTHORITY\\LocalService'} - -Constants -========= - -The following enum classes group related constants and are useful for type -annotations and introspection. The individual constants (e.g. -:data:`psutil.STATUS_RUNNING`) are also accessible directly from the psutil -namespace as aliases for the enum members and should be preferred over -accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over -``psutil.ProcessStatus.STATUS_RUNNING``). - -.. class:: psutil.ProcessStatus - - `enum.StrEnum`_ collection of :data:`STATUS_* ` - constants. Returned by :meth:`psutil.Process.status()`. - - .. versionadded:: 8.0.0 - -.. class:: psutil.ProcessPriority - - `enum.IntEnum`_ collection of - :data:`*_PRIORITY_CLASS ` constants for - :meth:`psutil.Process.nice` on Windows. - - Availability: Windows - - .. versionadded:: 8.0.0 - -.. class:: psutil.ProcessIOPriority - - `enum.IntEnum`_ collection of I/O priority constants for - :meth:`psutil.Process.ionice`. On Linux: ``IOPRIO_CLASS_*`` constants. - On Windows: ``IOPRIO_*`` constants. - - Availability: Linux, Windows - - .. versionadded:: 8.0.0 - -.. class:: psutil.ProcessRlimit - - `enum.IntEnum`_ collection of :data:`RLIMIT_* ` - constants for :meth:`psutil.Process.rlimit`. - - Availability: Linux, FreeBSD - - .. versionadded:: 8.0.0 - -.. class:: psutil.ConnectionStatus - - `enum.StrEnum`_ collection of :data:`CONN_* ` - constants. Returned in the *status* field of - :func:`psutil.net_connections` and :meth:`psutil.Process.net_connections`. - - .. versionadded:: 8.0.0 - -.. class:: psutil.NicDuplex - - `enum.IntEnum`_ collection of :data:`NIC_DUPLEX_* ` - constants. Returned in the *duplex* field of :func:`psutil.net_if_stats`. - - .. versionadded:: 3.0.0 - -.. class:: psutil.BatteryTime - - `enum.IntEnum`_ collection of :data:`POWER_TIME_* ` - constants. May appear in the *secsleft* field of :func:`psutil.sensors_battery`. - - .. versionadded:: 5.1.0 - -Operating system constants --------------------------- - -.. _const-oses: -.. data:: POSIX -.. data:: LINUX -.. data:: WINDOWS -.. data:: MACOS -.. data:: FREEBSD -.. data:: NETBSD -.. data:: OPENBSD -.. data:: BSD -.. data:: SUNOS -.. data:: AIX - - ``bool`` constants which define what platform you're on. E.g. if on Windows, - :const:`WINDOWS` constant will be ``True``, all others will be ``False``. - - .. versionadded:: 4.0.0 - .. versionchanged:: 5.4.0 added AIX - -.. data:: OSX - - Alias for :const:`MACOS`. - - .. warning:: - deprecated in version 5.4.7; use :const:`MACOS` instead. - -.. _const-procfs_path: -.. data:: PROCFS_PATH - - The path of the /proc filesystem on Linux, Solaris and AIX (defaults to - ``"/proc"``). - You may want to re-set this constant right after importing psutil in case - your /proc filesystem is mounted elsewhere or if you want to retrieve - information about Linux containers such as Docker, Heroku or LXC (see - `here `__ - for more info). - It must be noted that this trick works only for APIs which rely on /proc - filesystem (e.g. `memory`_ APIs and most :class:`Process` class methods). - - Availability: Linux, Solaris, AIX - - .. versionadded:: 3.2.3 - .. versionchanged:: 3.4.2 also available on Solaris. - .. versionchanged:: 5.4.0 also available on AIX. - -Process status constants ------------------------- - -.. _const-pstatus: -.. data:: STATUS_RUNNING -.. data:: STATUS_SLEEPING -.. data:: STATUS_DISK_SLEEP -.. data:: STATUS_STOPPED -.. data:: STATUS_TRACING_STOP -.. data:: STATUS_ZOMBIE -.. data:: STATUS_DEAD -.. data:: STATUS_WAKE_KILL -.. data:: STATUS_WAKING -.. data:: STATUS_PARKED (Linux) -.. data:: STATUS_IDLE (Linux, macOS, FreeBSD) -.. data:: STATUS_LOCKED (FreeBSD) -.. data:: STATUS_WAITING (FreeBSD) -.. data:: STATUS_SUSPENDED (NetBSD) - - Represent a process status. Returned by :meth:`psutil.Process.status()`. - These constants are members of the :class:`psutil.ProcessStatus` enum. - - .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) - .. versionadded:: 5.4.7 ``STATUS_PARKED`` (Linux) - .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessStatus` - enum members (were plain strings). - -Process priority constants --------------------------- - -.. _const-prio: -.. data:: REALTIME_PRIORITY_CLASS -.. data:: HIGH_PRIORITY_CLASS -.. data:: ABOVE_NORMAL_PRIORITY_CLASS -.. data:: NORMAL_PRIORITY_CLASS -.. data:: IDLE_PRIORITY_CLASS -.. data:: BELOW_NORMAL_PRIORITY_CLASS - - Represent the priority of a process on Windows (see `SetPriorityClass`_). - They can be used in conjunction with :meth:`psutil.Process.nice()` to get or - set process priority. - These constants are members of the :class:`psutil.ProcessPriority` enum. - - Availability: Windows - - .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessPriority` - enum members (were plain integers). - -.. _const-ioprio: -.. data:: IOPRIO_CLASS_NONE -.. data:: IOPRIO_CLASS_RT -.. data:: IOPRIO_CLASS_BE -.. data:: IOPRIO_CLASS_IDLE - - A set of integers representing the I/O priority of a process on Linux. They - can be used in conjunction with :meth:`psutil.Process.ionice()` to get or set - process I/O priority. - These constants are members of the :class:`psutil.ProcessIOPriority` - enum. - *IOPRIO_CLASS_NONE* and *IOPRIO_CLASS_BE* (best effort) is the default for - any process that hasn't set a specific I/O priority. - *IOPRIO_CLASS_RT* (real time) means the process is given first access to the - disk, regardless of what else is going on in the system. - *IOPRIO_CLASS_IDLE* means the process will get I/O time when no-one else - needs the disk. - For further information refer to manuals of - `ionice `__ command line utility or - `ioprio_get`_ system call. - - Availability: Linux - - .. versionchanged:: 8.0.0 constants are now - :class:`psutil.ProcessIOPriority` enum members (previously - ``IOPriority`` enum). - -.. data:: IOPRIO_VERYLOW -.. data:: IOPRIO_LOW -.. data:: IOPRIO_NORMAL -.. data:: IOPRIO_HIGH - - A set of integers representing the I/O priority of a process on Windows. - They can be used in conjunction with :meth:`psutil.Process.ionice()` to get - or set process I/O priority. - These constants are members of the :class:`psutil.ProcessIOPriority` - enum. - - Availability: Windows - - .. versionadded:: 5.6.2 - .. versionchanged:: 8.0.0 constants are now - :class:`psutil.ProcessIOPriority` enum members (previously - ``IOPriority`` enum). - -Process resources constants ---------------------------- - -Linux / FreeBSD: - - .. data:: RLIM_INFINITY - .. data:: RLIMIT_AS - .. data:: RLIMIT_CORE - .. data:: RLIMIT_CPU - .. data:: RLIMIT_DATA - .. data:: RLIMIT_FSIZE - .. data:: RLIMIT_MEMLOCK - .. data:: RLIMIT_NOFILE - .. data:: RLIMIT_NPROC - .. data:: RLIMIT_RSS - .. data:: RLIMIT_STACK - -Linux specific: - - .. data:: RLIMIT_LOCKS - .. data:: RLIMIT_MSGQUEUE - .. data:: RLIMIT_NICE - .. data:: RLIMIT_RTPRIO - .. data:: RLIMIT_RTTIME - .. data:: RLIMIT_SIGPENDING - -FreeBSD specific: - - .. data:: RLIMIT_SWAP - .. data:: RLIMIT_SBSIZE - .. data:: RLIMIT_NPTS - -Constants used for getting and setting process resource limits to be used in -conjunction with :meth:`psutil.Process.rlimit()`. See `resource.getrlimit`_ -for further information. -These constants are members of the :class:`psutil.ProcessRlimit` enum. - -Availability: Linux, FreeBSD - -.. versionchanged:: 5.7.3 added FreeBSD support, added ``RLIMIT_SWAP``, - ``RLIMIT_SBSIZE``, ``RLIMIT_NPTS``. -.. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessRlimit` - enum members (were plain integers). - -Connections constants ---------------------- - -.. _const-conn: -.. data:: CONN_ESTABLISHED -.. data:: CONN_SYN_SENT -.. data:: CONN_SYN_RECV -.. data:: CONN_FIN_WAIT1 -.. data:: CONN_FIN_WAIT2 -.. data:: CONN_TIME_WAIT -.. data:: CONN_CLOSE -.. data:: CONN_CLOSE_WAIT -.. data:: CONN_LAST_ACK -.. data:: CONN_LISTEN -.. data:: CONN_CLOSING -.. data:: CONN_NONE -.. data:: CONN_DELETE_TCB (Windows) -.. data:: CONN_IDLE (Solaris) -.. data:: CONN_BOUND (Solaris) - - A set of strings representing the status of a TCP connection. - Returned by :meth:`psutil.Process.net_connections()` and - :func:`psutil.net_connections` (`status` field). - These constants are members of the :class:`psutil.ConnectionStatus` enum. - - .. versionchanged:: 8.0.0 constants are now :class:`psutil.ConnectionStatus` - enum members (were plain strings). - -Hardware constants ------------------- - -.. _const-aflink: -.. data:: AF_LINK - - Constant which identifies a MAC address associated with a network interface. - To be used in conjunction with :func:`psutil.net_if_addrs()`. - - .. versionadded:: 3.0.0 - -.. _const-duplex: -.. data:: NIC_DUPLEX_FULL -.. data:: NIC_DUPLEX_HALF -.. data:: NIC_DUPLEX_UNKNOWN - - Constants which identifies whether a NIC (network interface card) has full or - half mode speed. NIC_DUPLEX_FULL means the NIC is able to send and receive - data (files) simultaneously, NIC_DUPLEX_FULL means the NIC can either send or - receive data at a time. - To be used in conjunction with :func:`psutil.net_if_stats()`. - - .. versionadded:: 3.0.0 - -.. _const-power: -.. data:: POWER_TIME_UNKNOWN -.. data:: POWER_TIME_UNLIMITED - - Whether the remaining time of the battery cannot be determined or is - unlimited. - May be assigned to :func:`psutil.sensors_battery()`'s *secsleft* field. - - .. versionadded:: 5.1.0 - -.. _const-version-info: -.. data:: version_info - - A tuple to check psutil installed version. Example: - - >>> import psutil - >>> if psutil.version_info >= (4, 5): - ... pass - -Recipes -======= - -Find process by name --------------------- - -Check string against :meth:`Process.name()`: - -:: - - import psutil - - def find_procs_by_name(name): - "Return a list of processes matching 'name'." - ls = [] - for p in psutil.process_iter(['name']): - if p.info['name'] == name: - ls.append(p) - return ls - -A bit more advanced, check string against :meth:`Process.name()`, -:meth:`Process.exe()` and :meth:`Process.cmdline()`: - -:: - - import os - import psutil - - def find_procs_by_name(name): - "Return a list of processes matching 'name'." - ls = [] - for p in psutil.process_iter(["name", "exe", "cmdline"]): - if name == p.info['name'] or \ - p.info['exe'] and os.path.basename(p.info['exe']) == name or \ - p.info['cmdline'] and p.info['cmdline'][0] == name: - ls.append(p) - return ls - -Kill process tree ------------------ - -:: - - import os - import signal - import psutil - - def kill_proc_tree(pid, sig=signal.SIGTERM, include_parent=True, - timeout=None, on_terminate=None): - """Kill a process tree (including grandchildren) with signal - "sig" and return a (gone, still_alive) tuple. - "on_terminate", if specified, is a callback function which is - called as soon as a child terminates. - """ - assert pid != os.getpid(), "won't kill myself" - parent = psutil.Process(pid) - children = parent.children(recursive=True) - if include_parent: - children.append(parent) - for p in children: - try: - p.send_signal(sig) - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(children, timeout=timeout, - callback=on_terminate) - return (gone, alive) - -Filtering and sorting processes -------------------------------- - -A collection of code samples showing how to use :func:`process_iter()` to filter processes and sort them. Setup:: - - >>> import psutil - >>> from pprint import pprint as pp - -Processes owned by user:: - - >>> import getpass - >>> pp([(p.pid, p.info['name']) for p in psutil.process_iter(['name', 'username']) if p.info['username'] == getpass.getuser()]) - (16832, 'bash'), - (19772, 'ssh'), - (20492, 'python')] - -Processes actively running:: - - >>> pp([(p.pid, p.info) for p in psutil.process_iter(['name', 'status']) if p.info['status'] == psutil.STATUS_RUNNING]) - [(1150, {'name': 'Xorg', 'status': }), - (1776, {'name': 'unity-panel-service', 'status': }), - (20492, {'name': 'python', 'status': })] - -Processes using log files:: - - >>> for p in psutil.process_iter(['name', 'open_files']): - ... for file in p.info['open_files'] or []: - ... if file.path.endswith('.log'): - ... print("%-5s %-10s %s" % (p.pid, p.info['name'][:10], file.path)) - ... - 1510 upstart /home/giampaolo/.cache/upstart/unity-settings-daemon.log - 2174 nautilus /home/giampaolo/.local/share/gvfs-metadata/home-ce08efac.log - 2650 chrome /home/giampaolo/.config/google-chrome/Default/data_reduction_proxy_leveldb/000003.log - -Processes consuming more than 500M of memory:: - - >>> pp([(p.pid, p.info['name'], p.info['memory_info'].rss) for p in psutil.process_iter(['name', 'memory_info']) if p.info['memory_info'].rss > 500 * 1024 * 1024]) - [(2650, 'chrome', 532324352), - (3038, 'chrome', 1120088064), - (21915, 'sublime_text', 615407616)] - -Top 3 processes which consumed the most CPU time:: - - >>> pp([(p.pid, p.info['name'], sum(p.info['cpu_times'])) for p in sorted(psutil.process_iter(['name', 'cpu_times']), key=lambda p: sum(p.info['cpu_times'][:2]))][-3:]) - [(2721, 'chrome', 10219.73), - (1150, 'Xorg', 11116.989999999998), - (2650, 'chrome', 18451.97)] - -Bytes conversion ----------------- - -:: - - import psutil - - def bytes2human(n): - # http://code.activestate.com/recipes/578019 - # >>> bytes2human(10000) - # '9.8K' - # >>> bytes2human(100001221) - # '95.4M' - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if abs(n) >= prefix[s]: - value = float(n) / prefix[s] - return '%.1f%s' % (value, s) - return "%sB" % n - - total = psutil.disk_usage('/').total - print(total) - print(bytes2human(total)) - -...prints:: - - 100399730688 - 93.5G - -FAQs -==== - -* Q: Why do I get :class:`AccessDenied` for certain processes? -* A: This may happen when you query processes owned by another user, - especially on macOS (see `issue #883`_) and Windows. - Unfortunately there's not much you can do about this except running the - Python process with higher privileges. - On Unix you may run the Python process as root or use the SUID bit - (``ps`` and ``netstat`` does this). - On Windows you may run the Python process as NT AUTHORITY\\SYSTEM or install - the Python script as a Windows service (ProcessHacker does this). - -* Q: is MinGW supported on Windows? -* A: no, you should Visual Studio (see `development guide `_). - -Running tests -============= - -:: - - $ make test - -Debug mode -========== - -If you want to debug unusual situations or want to report a bug, it may be -useful to enable debug mode via ``PSUTIL_DEBUG`` environment variable. -In this mode, psutil may (or may not) print additional information to stderr. -Usually these are error conditions which are not severe, and hence are ignored -(instead of crashing). -Unit tests automatically run with debug mode enabled. -On UNIX: - -:: - - $ PSUTIL_DEBUG=1 python3 script.py - psutil-debug [psutil/_psutil_linux.c:150]> setmntent() failed (ignored) - -On Windows: - -:: - - set PSUTIL_DEBUG=1 python.exe script.py - psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) - -Python 2.7 -========== - -Latest version spporting Python 2.7 is `psutil 6.1.1 `__. -The 6.1.X serie may receive critical bug-fixes but no new features. It will -be maintained in the dedicated -`python2 `__ branch. -To install it: - -:: - - $ python2 -m pip install psutil==6.1.* - -Security -======== - -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -Development guide -================= - -If you want to develop psutil take a look at the `DEVGUIDE.rst`_. - -Platforms support history -========================= - -* psutil 7.2.0 (2025-12): publish wheels for Linux musl -* psutil 7.1.2 (2025-10): publish wheels for free-threaded Python -* psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python - (**Linux** and **Windows**) -* psutil 8.0.0 (XXXX-XX): drop Python 3.6 -* psutil 7.1.1 (2025-10): drop **SunOS 10** -* psutil 7.1.0 (2025-09): drop **FreeBSD 8** -* psutil 7.0.0 (2025-02): drop Python 2.7 -* psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 -* psutil 5.9.1 (2022-05): drop Python 2.6 -* psutil 5.9.0 (2021-12): add **MidnightBSD** -* psutil 5.8.0 (2020-12): add **PyPy 2** on Windows -* psutil 5.7.1 (2020-07): add **Windows Nano** -* psutil 5.7.0 (2020-02): drop Windows XP & Windows Server 2003 -* psutil 5.7.0 (2020-02): add **PyPy 3** on Windows -* psutil 5.4.0 (2017-11): add **AIX** -* psutil 3.4.1 (2016-01): add **NetBSD** -* psutil 3.3.0 (2015-11): add **OpenBSD** -* psutil 1.0.0 (2013-07): add **Solaris** -* psutil 0.1.1 (2009-03): add **FreeBSD** -* psutil 0.1.0 (2009-01): add **Linux, Windows, macOS** - -Supported Python versions at the time of writing are cPython 2.7, 3.6+ and -PyPy3. - -Timeline -======== - -- 2026-02-08: - `7.2.3 `__ - - `what's new `__ - - `diff `__ -- 2026-01-28: - `7.2.2 `__ - - `what's new `__ - - `diff `__ -- 2025-12-29: - `7.2.1 `__ - - `what's new `__ - - `diff `__ -- 2025-12-23: - `7.2.0 `__ - - `what's new `__ - - `diff `__ -- 2025-11-02: - `7.1.3 `__ - - `what's new `__ - - `diff `__ -- 2025-10-25: - `7.1.2 `__ - - `what's new `__ - - `diff `__ -- 2025-10-19: - `7.1.1 `__ - - `what's new `__ - - `diff `__ -- 2025-09-17: - `7.1.0 `__ - - `what's new `__ - - `diff `__ -- 2025-02-13: - `7.0.0 `__ - - `what's new `__ - - `diff `__ -- 2024-12-19: - `6.1.1 `__ - - `what's new `__ - - `diff `__ -- 2024-10-17: - `6.1.0 `__ - - `what's new `__ - - `diff `__ -- 2024-06-18: - `6.0.0 `__ - - `what's new `__ - - `diff `__ -- 2024-01-19: - `5.9.8 `__ - - `what's new `__ - - `diff `__ -- 2023-12-17: - `5.9.7 `__ - - `what's new `__ - - `diff `__ -- 2023-10-15: - `5.9.6 `__ - - `what's new `__ - - `diff `__ -- 2023-04-17: - `5.9.5 `__ - - `what's new `__ - - `diff `__ -- 2022-11-07: - `5.9.4 `__ - - `what's new `__ - - `diff `__ -- 2022-10-18: - `5.9.3 `__ - - `what's new `__ - - `diff `__ -- 2022-09-04: - `5.9.2 `__ - - `what's new `__ - - `diff `__ -- 2022-05-20: - `5.9.1 `__ - - `what's new `__ - - `diff `__ -- 2021-12-29: - `5.9.0 `__ - - `what's new `__ - - `diff `__ -- 2020-12-19: - `5.8.0 `__ - - `what's new `__ - - `diff `__ -- 2020-10-24: - `5.7.3 `__ - - `what's new `__ - - `diff `__ -- 2020-07-15: - `5.7.2 `__ - - `what's new `__ - - `diff `__ -- 2020-07-15: - `5.7.1 `__ - - `what's new `__ - - `diff `__ -- 2020-02-18: - `5.7.0 `__ - - `what's new `__ - - `diff `__ -- 2019-11-26: - `5.6.7 `__ - - `what's new `__ - - `diff `__ -- 2019-11-25: - `5.6.6 `__ - - `what's new `__ - - `diff `__ -- 2019-11-06: - `5.6.5 `__ - - `what's new `__ - - `diff `__ -- 2019-11-04: - `5.6.4 `__ - - `what's new `__ - - `diff `__ -- 2019-06-11: - `5.6.3 `__ - - `what's new `__ - - `diff `__ -- 2019-04-26: - `5.6.2 `__ - - `what's new `__ - - `diff `__ -- 2019-03-11: - `5.6.1 `__ - - `what's new `__ - - `diff `__ -- 2019-03-05: - `5.6.0 `__ - - `what's new `__ - - `diff `__ -- 2019-02-15: - `5.5.1 `__ - - `what's new `__ - - `diff `__ -- 2019-01-23: - `5.5.0 `__ - - `what's new `__ - - `diff `__ -- 2018-10-30: - `5.4.8 `__ - - `what's new `__ - - `diff `__ -- 2018-08-14: - `5.4.7 `__ - - `what's new `__ - - `diff `__ -- 2018-06-07: - `5.4.6 `__ - - `what's new `__ - - `diff `__ -- 2018-04-13: - `5.4.5 `__ - - `what's new `__ - - `diff `__ -- 2018-04-13: - `5.4.4 `__ - - `what's new `__ - - `diff `__ -- 2018-01-01: - `5.4.3 `__ - - `what's new `__ - - `diff `__ -- 2017-12-07: - `5.4.2 `__ - - `what's new `__ - - `diff `__ -- 2017-11-08: - `5.4.1 `__ - - `what's new `__ - - `diff `__ -- 2017-10-12: - `5.4.0 `__ - - `what's new `__ - - `diff `__ -- 2017-09-10: - `5.3.1 `__ - - `what's new `__ - - `diff `__ -- 2017-09-01: - `5.3.0 `__ - - `what's new `__ - - `diff `__ -- 2017-04-10: - `5.2.2 `__ - - `what's new `__ - - `diff `__ -- 2017-03-24: - `5.2.1 `__ - - `what's new `__ - - `diff `__ -- 2017-03-05: - `5.2.0 `__ - - `what's new `__ - - `diff `__ -- 2017-02-07: - `5.1.3 `__ - - `what's new `__ - - `diff `__ -- 2017-02-03: - `5.1.2 `__ - - `what's new `__ - - `diff `__ -- 2017-02-03: - `5.1.1 `__ - - `what's new `__ - - `diff `__ -- 2017-02-01: - `5.1.0 `__ - - `what's new `__ - - `diff `__ -- 2016-12-21: - `5.0.1 `__ - - `what's new `__ - - `diff `__ -- 2016-11-06: - `5.0.0 `__ - - `what's new `__ - - `diff `__ -- 2016-10-05: - `4.4.2 `__ - - `what's new `__ - - `diff `__ -- 2016-10-25: - `4.4.1 `__ - - `what's new `__ - - `diff `__ -- 2016-10-23: - `4.4.0 `__ - - `what's new `__ - - `diff `__ -- 2016-09-01: - `4.3.1 `__ - - `what's new `__ - - `diff `__ -- 2016-06-18: - `4.3.0 `__ - - `what's new `__ - - `diff `__ -- 2016-05-14: - `4.2.0 `__ - - `what's new `__ - - `diff `__ -- 2016-03-12: - `4.1.0 `__ - - `what's new `__ - - `diff `__ -- 2016-02-17: - `4.0.0 `__ - - `what's new `__ - - `diff `__ -- 2016-01-20: - `3.4.2 `__ - - `what's new `__ - - `diff `__ -- 2016-01-15: - `3.4.1 `__ - - `what's new `__ - - `diff `__ -- 2015-11-25: - `3.3.0 `__ - - `what's new `__ - - `diff `__ -- 2015-10-04: - `3.2.2 `__ - - `what's new `__ - - `diff `__ -- 2015-09-03: - `3.2.1 `__ - - `what's new `__ - - `diff `__ -- 2015-09-02: - `3.2.0 `__ - - `what's new `__ - - `diff `__ -- 2015-07-15: - `3.1.1 `__ - - `what's new `__ - - `diff `__ -- 2015-07-15: - `3.1.0 `__ - - `what's new `__ - - `diff `__ -- 2015-06-18: - `3.0.1 `__ - - `what's new `__ - - `diff `__ -- 2015-06-13: - `3.0.0 `__ - - `what's new `__ - - `diff `__ -- 2015-02-02: - `2.2.1 `__ - - `what's new `__ - - `diff `__ -- 2015-01-06: - `2.2.0 `__ - - `what's new `__ - - `diff `__ -- 2014-09-26: - `2.1.3 `__ - - `what's new `__ - - `diff `__ -- 2014-09-21: - `2.1.2 `__ - - `what's new `__ - - `diff `__ -- 2014-04-30: - `2.1.1 `__ - - `what's new `__ - - `diff `__ -- 2014-04-08: - `2.1.0 `__ - - `what's new `__ - - `diff `__ -- 2014-03-10: - `2.0.0 `__ - - `what's new `__ - - `diff `__ -- 2013-11-25: - `1.2.1 `__ - - `what's new `__ - - `diff `__ -- 2013-11-20: - `1.2.0 `__ - - `what's new `__ - - `diff `__ -- 2013-10-22: - `1.1.2 `__ - - `what's new `__ - - `diff `__ -- 2013-10-08: - `1.1.1 `__ - - `what's new `__ - - `diff `__ -- 2013-09-28: - `1.1.0 `__ - - `what's new `__ - - `diff `__ -- 2013-07-12: - `1.0.1 `__ - - `what's new `__ - - `diff `__ -- 2013-07-10: - `1.0.0 `__ - - `what's new `__ - - `diff `__ -- 2013-05-03: - `0.7.1 `__ - - `what's new `__ - - `diff `__ -- 2013-04-12: - `0.7.0 `__ - - `what's new `__ - - `diff `__ -- 2012-08-16: - `0.6.1 `__ - - `what's new `__ - - `diff `__ -- 2012-08-13: - `0.6.0 `__ - - `what's new `__ - - `diff `__ -- 2012-06-29: - `0.5.1 `__ - - `what's new `__ - - `diff `__ -- 2012-06-27: - `0.5.0 `__ - - `what's new `__ - - `diff `__ -- 2011-12-14: - `0.4.1 `__ - - `what's new `__ - - `diff `__ -- 2011-10-29: - `0.4.0 `__ - - `what's new `__ - - `diff `__ -- 2011-07-08: - `0.3.0 `__ - - `what's new `__ - - `diff `__ -- 2011-03-20: - `0.2.1 `__ - - `what's new `__ - - `diff `__ -- 2010-11-13: - `0.2.0 `__ - - `what's new `__ - - `diff `__ -- 2010-03-02: - `0.1.3 `__ - - `what's new `__ - - `diff `__ -- 2009-05-06: - `0.1.2 `__ - - `what's new `__ - - `diff `__ -- 2009-03-06: - `0.1.1 `__ - - `what's new `__ - - `diff `__ -- 2009-01-27: - `0.1.0 `__ - - `what's new `__ - - `diff `__ - +.. toctree:: + :maxdepth: 2 -.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 -.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET -.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX -.. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py -.. _`BPO-10784`: https://bugs.python.org/issue10784 -.. _`BPO-12442`: https://bugs.python.org/issue12442 -.. _`BPO-6973`: https://bugs.python.org/issue6973 -.. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 -.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`DEVGUIDE.rst`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst -.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum -.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum -.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum -.. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py -.. _`GetDriveType`: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea -.. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess -.. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ -.. _`GetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass -.. _`Giampaolo Rodola`: https://gmpy.dev/about -.. _`hash`: https://docs.python.org/3/library/functions.html#hash -.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py -.. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get -.. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt -.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`issue #1007`: https://github.com/giampaolo/psutil/issues/1007 -.. _`issue #1040`: https://github.com/giampaolo/psutil/issues/1040 -.. _`issue #883`: https://github.com/giampaolo/psutil/issues/883 -.. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html -.. _`man prlimit`: https://linux.die.net/man/2/prlimit -.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`open`: https://docs.python.org/3/library/functions.html#open -.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count -.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg -.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid -.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority -.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid -.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid -.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY -.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC -.. _`os.open`: https://docs.python.org/3/library/os.html#os.open -.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open -.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority -.. _`os.times`: https://docs.python.org//library/os.html#os.times -.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex -.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`psleak`: https://github.com/giampaolo/psleak -.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit -.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit -.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue -.. _`select.poll`: https://docs.python.org//library/select.html#select.poll -.. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py -.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. -.. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass -.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. -.. _`signal module`: https://docs.python.org//library/signal.html -.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM -.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET -.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM -.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd -.. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait -.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen -.. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py -.. _`TerminateProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess -.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident -.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread -.. _`Tidelift security contact`: https://tidelift.com/security + Install + API Reference + Recipes + Platform support + Development guide + FAQs + Timeline diff --git a/INSTALL.rst b/docs/install.rst similarity index 96% rename from INSTALL.rst rename to docs/install.rst index 1619b7d341..f627f1d357 100644 --- a/INSTALL.rst +++ b/docs/install.rst @@ -13,10 +13,10 @@ If wheels are not available for your platform or architecture, or you wish to build & install psutil from sources, keep reading. Compile psutil from sources -=========================== +--------------------------- UNIX ----- +^^^^ On all UNIX systems you can use the `install-sysdeps.sh `__ @@ -35,7 +35,7 @@ After system deps are installed, you can compile & install psutil with:: pip install --no-binary :all: psutil Linux ------ +^^^^^ Debian / Ubuntu:: @@ -58,7 +58,7 @@ Alpine:: pip install --no-binary :all: psutil Windows -------- +^^^^^^^ - To build or install psutil from source on Windows, you need to have `Visual Studio 2017 `__. @@ -79,7 +79,7 @@ Windows make install macOS ------ +^^^^^ Install Xcode first: @@ -89,7 +89,7 @@ Install Xcode first: pip install --no-binary :all: psutil FreeBSD -------- +^^^^^^^ :: @@ -97,7 +97,7 @@ FreeBSD python3 -m pip install psutil OpenBSD -------- +^^^^^^^ :: @@ -106,7 +106,7 @@ OpenBSD pip install psutil NetBSD ------- +^^^^^^ Assuming Python 3.11 (the most recent at the time of writing): @@ -118,7 +118,7 @@ Assuming Python 3.11 (the most recent at the time of writing): python3.11 -m pip install psutil Sun Solaris ------------ +^^^^^^^^^^^ If ``cc`` compiler is not installed create a symbolic link to ``gcc``:: @@ -130,10 +130,10 @@ Install:: pip install psutil Troubleshooting -=============== +--------------- Install pip ------------ +^^^^^^^^^^^ If you don't have pip you can install it with wget:: @@ -149,7 +149,7 @@ cmd.exe and install it with:: py get-pip.py "pip not found" ---------------- +^^^^^^^^^^^^^^^ Sometimes pip is installed but it's not available in your ``PATH`` ("pip command not found" or similar). Try this:: @@ -157,7 +157,7 @@ Sometimes pip is installed but it's not available in your ``PATH`` python3 -m pip install psutil Permission errors (UNIX) ------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^ If you want to install psutil system-wide and you bump into permission errors either run as root user or prepend ``sudo``:: diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index f8473cff8d..0000000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyftpdlib.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyftpdlib.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/platform.rst b/docs/platform.rst new file mode 100644 index 0000000000..f038cea0cb --- /dev/null +++ b/docs/platform.rst @@ -0,0 +1,45 @@ +Platform support +================ + +Python +------ + +Supported Python versions: Python 3.7+ and PyPy. + +Python 2.7 +---------- + +Latest psutil version supporting Python 2.7 is `psutil 6.1.1 `__. +The 6.1.X serie may receive critical bug-fixes but no new features. It will +be maintained in the dedicated +`python2 `__ branch. +To install it: + +:: + + $ python2 -m pip install psutil==6.1.* + +Support history +--------------- + +* psutil 8.0.0 (2026-XX): drop Python 3.6 +* psutil 7.2.0 (2025-12): publish Linux musl wheels +* psutil 7.1.2 (2025-10): publish free-threaded Python wheels +* psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (Linux and + Windows) +* psutil 7.1.1 (2025-10): drop **SunOS 10** +* psutil 7.1.0 (2025-09): drop **FreeBSD 8** +* psutil 7.0.0 (2025-02): drop Python 2.7 +* psutil 5.9.6 (2023-10): drop Python 3.4 and 3.5 +* psutil 5.9.1 (2022-05): drop Python 2.6 +* psutil 5.9.0 (2021-12): add **MidnightBSD** +* psutil 5.8.0 (2020-12): add **PyPy 2** on Windows +* psutil 5.7.1 (2020-07): add **Windows Nano** +* psutil 5.7.0 (2020-02): drop Windows XP & Windows Server 2003 +* psutil 5.7.0 (2020-02): add **PyPy 3** on Windows +* psutil 5.4.0 (2017-11): add **AIX** +* psutil 3.4.1 (2016-01): add **NetBSD** +* psutil 3.3.0 (2015-11): add **OpenBSD** +* psutil 1.0.0 (2013-07): add **Solaris** +* psutil 0.1.1 (2009-03): add **FreeBSD** +* psutil 0.1.0 (2009-01): add **Linux, Windows, macOS** diff --git a/docs/recipes.rst b/docs/recipes.rst new file mode 100644 index 0000000000..dc3f90bd97 --- /dev/null +++ b/docs/recipes.rst @@ -0,0 +1,158 @@ +.. include:: _links.rst + +Recipes +======= + +Find process by name +-------------------- + +Check string against :meth:`Process.name()`: + +.. code-block:: python + + import psutil + + def find_procs_by_name(name): + "Return a list of processes matching 'name'." + ls = [] + for p in psutil.process_iter(['name']): + if p.info['name'] == name: + ls.append(p) + return ls + +A bit more advanced, check string against :meth:`Process.name()`, +:meth:`Process.exe()` and :meth:`Process.cmdline()`: + +.. code-block:: python + + import os + import psutil + + def find_procs_by_name(name): + "Return a list of processes matching 'name'." + ls = [] + for p in psutil.process_iter(["name", "exe", "cmdline"]): + if name == p.info['name'] or \ + p.info['exe'] and os.path.basename(p.info['exe']) == name or \ + p.info['cmdline'] and p.info['cmdline'][0] == name: + ls.append(p) + return ls + +Kill process tree +----------------- + +.. code-block:: python + + import os + import signal + import psutil + + def kill_proc_tree(pid, sig=signal.SIGTERM, include_parent=True, + timeout=None, on_terminate=None): + """Kill a process tree (including grandchildren) with signal + "sig" and return a (gone, still_alive) tuple. + "on_terminate", if specified, is a callback function which is + called as soon as a child terminates. + """ + assert pid != os.getpid(), "won't kill myself" + parent = psutil.Process(pid) + children = parent.children(recursive=True) + if include_parent: + children.append(parent) + for p in children: + try: + p.send_signal(sig) + except psutil.NoSuchProcess: + pass + gone, alive = psutil.wait_procs(children, timeout=timeout, + callback=on_terminate) + return (gone, alive) + +Filtering and sorting processes +------------------------------- + +A collection of code samples showing how to use :func:`process_iter()` to filter processes and sort them. + +Processes owned by user: + +.. code-block:: python + + >>> import getpass + >>> import psutil + >>> from pprint import pprint as pp + >>> pp([(p.pid, p.info['name']) for p in psutil.process_iter(['name', 'username']) if p.info['username'] == getpass.getuser()]) + (16832, 'bash'), + (19772, 'ssh'), + (20492, 'python')] + +Processes actively running: + +.. code-block:: python + + >>> pp([(p.pid, p.info) for p in psutil.process_iter(['name', 'status']) if p.info['status'] == psutil.STATUS_RUNNING]) + [(1150, {'name': 'Xorg', 'status': }), + (1776, {'name': 'unity-panel-service', 'status': }), + (20492, {'name': 'python', 'status': })] + +Processes using log files: + +.. code-block:: python + + >>> for p in psutil.process_iter(['name', 'open_files']): + ... for file in p.info['open_files'] or []: + ... if file.path.endswith('.log'): + ... print("%-5s %-10s %s" % (p.pid, p.info['name'][:10], file.path)) + ... + 1510 upstart /home/giampaolo/.cache/upstart/unity-settings-daemon.log + 2174 nautilus /home/giampaolo/.local/share/gvfs-metadata/home-ce08efac.log + 2650 chrome /home/giampaolo/.config/google-chrome/Default/data_reduction_proxy_leveldb/000003.log + +Processes consuming more than 500M of memory: + +.. code-block:: python + + >>> pp([(p.pid, p.info['name'], p.info['memory_info'].rss) for p in psutil.process_iter(['name', 'memory_info']) if p.info['memory_info'].rss > 500 * 1024 * 1024]) + [(2650, 'chrome', 532324352), + (3038, 'chrome', 1120088064), + (21915, 'sublime_text', 615407616)] + +Top 3 processes which consumed the most CPU time: + +.. code-block:: python + + >>> pp([(p.pid, p.info['name'], sum(p.info['cpu_times'])) for p in sorted(psutil.process_iter(['name', 'cpu_times']), key=lambda p: sum(p.info['cpu_times'][:2]))][-3:]) + [(2721, 'chrome', 10219.73), + (1150, 'Xorg', 11116.989999999998), + (2650, 'chrome', 18451.97)] + +Bytes conversion +---------------- + +.. code-block:: python + + import psutil + + def bytes2human(n): + # http://code.activestate.com/recipes/578019 + # >>> bytes2human(10000) + # '9.8K' + # >>> bytes2human(100001221) + # '95.4M' + symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + prefix = {} + for i, s in enumerate(symbols): + prefix[s] = 1 << (i + 1) * 10 + for s in reversed(symbols): + if abs(n) >= prefix[s]: + value = float(n) / prefix[s] + return '%.1f%s' % (value, s) + return "%sB" % n + + total = psutil.disk_usage('/').total + print(total) + print(bytes2human(total)) + +...prints:: + + 100399730688 + 93.5G diff --git a/docs/timeline.rst b/docs/timeline.rst new file mode 100644 index 0000000000..8cf25b9d6b --- /dev/null +++ b/docs/timeline.rst @@ -0,0 +1,415 @@ +Timeline +======== + +- 2026-02-08: + `7.2.3 `__ - + `what's new `__ - + `diff `__ +- 2026-01-28: + `7.2.2 `__ - + `what's new `__ - + `diff `__ +- 2025-12-29: + `7.2.1 `__ - + `what's new `__ - + `diff `__ +- 2025-12-23: + `7.2.0 `__ - + `what's new `__ - + `diff `__ +- 2025-11-02: + `7.1.3 `__ - + `what's new `__ - + `diff `__ +- 2025-10-25: + `7.1.2 `__ - + `what's new `__ - + `diff `__ +- 2025-10-19: + `7.1.1 `__ - + `what's new `__ - + `diff `__ +- 2025-09-17: + `7.1.0 `__ - + `what's new `__ - + `diff `__ +- 2025-02-13: + `7.0.0 `__ - + `what's new `__ - + `diff `__ +- 2024-12-19: + `6.1.1 `__ - + `what's new `__ - + `diff `__ +- 2024-10-17: + `6.1.0 `__ - + `what's new `__ - + `diff `__ +- 2024-06-18: + `6.0.0 `__ - + `what's new `__ - + `diff `__ +- 2024-01-19: + `5.9.8 `__ - + `what's new `__ - + `diff `__ +- 2023-12-17: + `5.9.7 `__ - + `what's new `__ - + `diff `__ +- 2023-10-15: + `5.9.6 `__ - + `what's new `__ - + `diff `__ +- 2023-04-17: + `5.9.5 `__ - + `what's new `__ - + `diff `__ +- 2022-11-07: + `5.9.4 `__ - + `what's new `__ - + `diff `__ +- 2022-10-18: + `5.9.3 `__ - + `what's new `__ - + `diff `__ +- 2022-09-04: + `5.9.2 `__ - + `what's new `__ - + `diff `__ +- 2022-05-20: + `5.9.1 `__ - + `what's new `__ - + `diff `__ +- 2021-12-29: + `5.9.0 `__ - + `what's new `__ - + `diff `__ +- 2020-12-19: + `5.8.0 `__ - + `what's new `__ - + `diff `__ +- 2020-10-24: + `5.7.3 `__ - + `what's new `__ - + `diff `__ +- 2020-07-15: + `5.7.2 `__ - + `what's new `__ - + `diff `__ +- 2020-07-15: + `5.7.1 `__ - + `what's new `__ - + `diff `__ +- 2020-02-18: + `5.7.0 `__ - + `what's new `__ - + `diff `__ +- 2019-11-26: + `5.6.7 `__ - + `what's new `__ - + `diff `__ +- 2019-11-25: + `5.6.6 `__ - + `what's new `__ - + `diff `__ +- 2019-11-06: + `5.6.5 `__ - + `what's new `__ - + `diff `__ +- 2019-11-04: + `5.6.4 `__ - + `what's new `__ - + `diff `__ +- 2019-06-11: + `5.6.3 `__ - + `what's new `__ - + `diff `__ +- 2019-04-26: + `5.6.2 `__ - + `what's new `__ - + `diff `__ +- 2019-03-11: + `5.6.1 `__ - + `what's new `__ - + `diff `__ +- 2019-03-05: + `5.6.0 `__ - + `what's new `__ - + `diff `__ +- 2019-02-15: + `5.5.1 `__ - + `what's new `__ - + `diff `__ +- 2019-01-23: + `5.5.0 `__ - + `what's new `__ - + `diff `__ +- 2018-10-30: + `5.4.8 `__ - + `what's new `__ - + `diff `__ +- 2018-08-14: + `5.4.7 `__ - + `what's new `__ - + `diff `__ +- 2018-06-07: + `5.4.6 `__ - + `what's new `__ - + `diff `__ +- 2018-04-13: + `5.4.5 `__ - + `what's new `__ - + `diff `__ +- 2018-04-13: + `5.4.4 `__ - + `what's new `__ - + `diff `__ +- 2018-01-01: + `5.4.3 `__ - + `what's new `__ - + `diff `__ +- 2017-12-07: + `5.4.2 `__ - + `what's new `__ - + `diff `__ +- 2017-11-08: + `5.4.1 `__ - + `what's new `__ - + `diff `__ +- 2017-10-12: + `5.4.0 `__ - + `what's new `__ - + `diff `__ +- 2017-09-10: + `5.3.1 `__ - + `what's new `__ - + `diff `__ +- 2017-09-01: + `5.3.0 `__ - + `what's new `__ - + `diff `__ +- 2017-04-10: + `5.2.2 `__ - + `what's new `__ - + `diff `__ +- 2017-03-24: + `5.2.1 `__ - + `what's new `__ - + `diff `__ +- 2017-03-05: + `5.2.0 `__ - + `what's new `__ - + `diff `__ +- 2017-02-07: + `5.1.3 `__ - + `what's new `__ - + `diff `__ +- 2017-02-03: + `5.1.2 `__ - + `what's new `__ - + `diff `__ +- 2017-02-03: + `5.1.1 `__ - + `what's new `__ - + `diff `__ +- 2017-02-01: + `5.1.0 `__ - + `what's new `__ - + `diff `__ +- 2016-12-21: + `5.0.1 `__ - + `what's new `__ - + `diff `__ +- 2016-11-06: + `5.0.0 `__ - + `what's new `__ - + `diff `__ +- 2016-10-05: + `4.4.2 `__ - + `what's new `__ - + `diff `__ +- 2016-10-25: + `4.4.1 `__ - + `what's new `__ - + `diff `__ +- 2016-10-23: + `4.4.0 `__ - + `what's new `__ - + `diff `__ +- 2016-09-01: + `4.3.1 `__ - + `what's new `__ - + `diff `__ +- 2016-06-18: + `4.3.0 `__ - + `what's new `__ - + `diff `__ +- 2016-05-14: + `4.2.0 `__ - + `what's new `__ - + `diff `__ +- 2016-03-12: + `4.1.0 `__ - + `what's new `__ - + `diff `__ +- 2016-02-17: + `4.0.0 `__ - + `what's new `__ - + `diff `__ +- 2016-01-20: + `3.4.2 `__ - + `what's new `__ - + `diff `__ +- 2016-01-15: + `3.4.1 `__ - + `what's new `__ - + `diff `__ +- 2015-11-25: + `3.3.0 `__ - + `what's new `__ - + `diff `__ +- 2015-10-04: + `3.2.2 `__ - + `what's new `__ - + `diff `__ +- 2015-09-03: + `3.2.1 `__ - + `what's new `__ - + `diff `__ +- 2015-09-02: + `3.2.0 `__ - + `what's new `__ - + `diff `__ +- 2015-07-15: + `3.1.1 `__ - + `what's new `__ - + `diff `__ +- 2015-07-15: + `3.1.0 `__ - + `what's new `__ - + `diff `__ +- 2015-06-18: + `3.0.1 `__ - + `what's new `__ - + `diff `__ +- 2015-06-13: + `3.0.0 `__ - + `what's new `__ - + `diff `__ +- 2015-02-02: + `2.2.1 `__ - + `what's new `__ - + `diff `__ +- 2015-01-06: + `2.2.0 `__ - + `what's new `__ - + `diff `__ +- 2014-09-26: + `2.1.3 `__ - + `what's new `__ - + `diff `__ +- 2014-09-21: + `2.1.2 `__ - + `what's new `__ - + `diff `__ +- 2014-04-30: + `2.1.1 `__ - + `what's new `__ - + `diff `__ +- 2014-04-08: + `2.1.0 `__ - + `what's new `__ - + `diff `__ +- 2014-03-10: + `2.0.0 `__ - + `what's new `__ - + `diff `__ +- 2013-11-25: + `1.2.1 `__ - + `what's new `__ - + `diff `__ +- 2013-11-20: + `1.2.0 `__ - + `what's new `__ - + `diff `__ +- 2013-10-22: + `1.1.2 `__ - + `what's new `__ - + `diff `__ +- 2013-10-08: + `1.1.1 `__ - + `what's new `__ - + `diff `__ +- 2013-09-28: + `1.1.0 `__ - + `what's new `__ - + `diff `__ +- 2013-07-12: + `1.0.1 `__ - + `what's new `__ - + `diff `__ +- 2013-07-10: + `1.0.0 `__ - + `what's new `__ - + `diff `__ +- 2013-05-03: + `0.7.1 `__ - + `what's new `__ - + `diff `__ +- 2013-04-12: + `0.7.0 `__ - + `what's new `__ - + `diff `__ +- 2012-08-16: + `0.6.1 `__ - + `what's new `__ - + `diff `__ +- 2012-08-13: + `0.6.0 `__ - + `what's new `__ - + `diff `__ +- 2012-06-29: + `0.5.1 `__ - + `what's new `__ - + `diff `__ +- 2012-06-27: + `0.5.0 `__ - + `what's new `__ - + `diff `__ +- 2011-12-14: + `0.4.1 `__ - + `what's new `__ - + `diff `__ +- 2011-10-29: + `0.4.0 `__ - + `what's new `__ - + `diff `__ +- 2011-07-08: + `0.3.0 `__ - + `what's new `__ - + `diff `__ +- 2011-03-20: + `0.2.1 `__ - + `what's new `__ - + `diff `__ +- 2010-11-13: + `0.2.0 `__ - + `what's new `__ - + `diff `__ +- 2010-03-02: + `0.1.3 `__ - + `what's new `__ - + `diff `__ +- 2009-05-06: + `0.1.2 `__ - + `what's new `__ - + `diff `__ +- 2009-03-06: + `0.1.1 `__ - + `what's new `__ - + `diff `__ +- 2009-01-27: + `0.1.0 `__ - + `what's new `__ - + `diff `__ From efd3be860e72ec4022cc1e733ad16059addb13ae Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 11 Mar 2026 00:56:38 +0100 Subject: [PATCH 1575/1714] Add scripts/internal/_mirror.py to avoid installing psutil for certain scripts --- MANIFEST.in | 1 + scripts/internal/_mirror.py | 30 +++++++++++++++++++++++++++++ scripts/internal/download_wheels.py | 2 +- scripts/internal/print_dist.py | 4 ++-- tests/test_scripts.py | 2 +- 5 files changed, 35 insertions(+), 4 deletions(-) create mode 100755 scripts/internal/_mirror.py diff --git a/MANIFEST.in b/MANIFEST.in index 464b51e011..d3a4da62e2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -154,6 +154,7 @@ include scripts/fans.py include scripts/free.py include scripts/ifconfig.py include scripts/internal/README +include scripts/internal/_mirror.py include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/convert_readme.py diff --git a/scripts/internal/_mirror.py b/scripts/internal/_mirror.py new file mode 100755 index 0000000000..f7183783f2 --- /dev/null +++ b/scripts/internal/_mirror.py @@ -0,0 +1,30 @@ +#!/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. + +"""Module needed so that certain scripts in this directory can freely +import psutil/_common.py without installing psutil first, and/or to +avoid circular import errors deriving from "psutil._common import …". +""" + +import importlib +import os +import pathlib + +ROOT = pathlib.Path(__file__).resolve().parent.parent.parent + + +def _import_module_by_path(path): + name = os.path.splitext(os.path.basename(path))[0] + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +_common = _import_module_by_path(ROOT / "psutil/_common.py") + +bytes2human = _common.bytes2human +print_color = _common.print_color diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py index 589a329631..7d240a8f5f 100755 --- a/scripts/internal/download_wheels.py +++ b/scripts/internal/download_wheels.py @@ -23,7 +23,7 @@ import requests -from psutil._common import bytes2human +from scripts.internal._mirror import bytes2human USER = "giampaolo" PROJECT = "psutil" diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 3ff399e22c..02b5176cd3 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -10,8 +10,8 @@ import collections import os -from psutil._common import bytes2human -from psutil._common import print_color +from scripts.internal._mirror import bytes2human +from scripts.internal._mirror import print_color class Wheel: diff --git a/tests/test_scripts.py b/tests/test_scripts.py index dda82ab29e..5d0bad7a36 100755 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -75,7 +75,7 @@ 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 name.endswith('.py') and not name.startswith("_"): if 'test_' + os.path.splitext(name)[0] not in meths: # self.assert_stdout(name) return pytest.fail( From 75762618a11f07e77e81dd6844c59d08d26d932b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 11 Mar 2026 01:00:02 +0100 Subject: [PATCH 1576/1714] Get rid of @memoize deco and use @lru_cache instead (saves 200 LOC) --- psutil/_common.py | 51 +--------- psutil/_psbsd.py | 3 +- psutil/_psposix.py | 8 +- psutil/_pswindows.py | 3 +- scripts/internal/find_broken_links.py | 2 +- scripts/internal/print_downloads.py | 7 +- tests/__init__.py | 5 +- tests/test_misc.py | 136 -------------------------- 8 files changed, 15 insertions(+), 200 deletions(-) diff --git a/psutil/_common.py b/psutil/_common.py index 2c36ff0d6e..63b2dfd2d0 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -41,7 +41,7 @@ # other constants 'ENCODING', 'ENCODING_ERRS', 'AF_INET6', # utility functions - 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize', + 'conn_tmap', 'deprecated_method', 'isfile_strict', 'parse_environ_block', 'path_exists_strict', 'usage_percent', 'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers", 'open_text', 'open_binary', 'cat', 'bcat', @@ -223,51 +223,6 @@ def usage_percent(used, total, round_=None): return ret -def memoize(fun): - """A simple memoize decorator for functions supporting (hashable) - positional arguments. - It also provides a cache_clear() function for clearing the cache: - - >>> @memoize - ... def foo() - ... return 1 - ... - >>> foo() - 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: - try: - ret = cache[key] = fun(*args, **kwargs) - except Exception as err: - raise err from None - return ret - - def cache_clear(): - """Clear cache.""" - cache.clear() - - cache = {} - wrapper.cache_clear = cache_clear - return wrapper - - def memoize_when_activated(fun): """A memoize decorator which is disabled by default. It can be activated and deactivated on request. @@ -275,7 +230,7 @@ def memoize_when_activated(fun): accepting no arguments. >>> class Foo: - ... @memoize + ... @memoize_when_activated ... def foo() ... print(1) ... @@ -720,7 +675,7 @@ def decode(s): # ===================================================================== -@memoize +@functools.lru_cache def term_supports_colors(file=sys.stdout): # pragma: no cover if not hasattr(file, "isatty") or not file.isatty(): return False diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 852f1dd9eb..b198e56432 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -24,7 +24,6 @@ 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 ._enums import BatteryTime from ._enums import ConnectionStatus @@ -417,7 +416,7 @@ def users(): # ===================================================================== -@memoize +@functools.lru_cache def _pid_0_exists(): try: Process(0).name() diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 11250424f9..b33f471841 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -6,6 +6,7 @@ import enum import errno +import functools import glob import os import select @@ -16,7 +17,6 @@ from ._common import MACOS from ._common import TimeoutExpired from ._common import debug -from ._common import memoize from ._common import usage_percent if MACOS: @@ -234,7 +234,7 @@ def wait_pid_kqueue(pid, timeout=None): kq.close() -@memoize +@functools.lru_cache def can_use_pidfd_open(): # Availability: Linux >= 5.3, Python >= 3.9 if not hasattr(os, "pidfd_open"): @@ -253,7 +253,7 @@ def can_use_pidfd_open(): return True -@memoize +@functools.lru_cache def can_use_kqueue(): # Availability: macOS, BSD names = ( @@ -340,7 +340,7 @@ def disk_usage(path): ) -@memoize +@functools.lru_cache def get_terminal_map(): """Get a map of device-id -> path as a dict. Used by Process.terminal(). diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 81cac78bb1..8103d7bc23 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -24,7 +24,6 @@ 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 usage_percent @@ -113,7 +112,7 @@ def convert_dos_path(s): return os.path.join(driveletter, remainder) -@memoize +@functools.lru_cache def getpagesize(): return cext.getpagesize() diff --git a/scripts/internal/find_broken_links.py b/scripts/internal/find_broken_links.py index 246c9c050f..f4333b5488 100755 --- a/scripts/internal/find_broken_links.py +++ b/scripts/internal/find_broken_links.py @@ -181,7 +181,7 @@ def get_urls(fname): return parse_generic(fname) -@memoize +@functools.lru_cache def validate_url(url): """Validate the URL by attempting an HTTP connection. Makes an HTTP-HEAD request for each URL. diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py index be081144d1..933aae017f 100755 --- a/scripts/internal/print_downloads.py +++ b/scripts/internal/print_downloads.py @@ -11,6 +11,7 @@ * https://hugovk.github.io/top-pypi-packages/. """ +import functools import json import os import shlex @@ -19,8 +20,6 @@ import pypinfo # noqa: F401 -from psutil._common import memoize - AUTH_FILE = os.path.expanduser("~/.pypinfo.json") PKGNAME = 'psutil' DAYS = 30 @@ -36,7 +35,7 @@ # --- get -@memoize +@functools.lru_cache def sh(cmd): assert os.path.exists(AUTH_FILE) env = os.environ.copy() @@ -55,7 +54,7 @@ def sh(cmd): return stdout.strip() -@memoize +@functools.lru_cache def query(cmd): global bytes_billed ret = json.loads(sh(cmd)) diff --git a/tests/__init__.py b/tests/__init__.py index dfb0701f0b..54cf486af1 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -55,7 +55,6 @@ from psutil import WINDOWS from psutil import _enums from psutil._common import debug -from psutil._common import memoize from psutil._common import supports_ipv6 if POSIX: @@ -124,7 +123,7 @@ RISCV64 = platform.machine() == "riscv64" -@memoize +@functools.lru_cache def macos_version(): version_str = platform.mac_ver()[0] version = tuple(map(int, version_str.split(".")[:2])) @@ -1082,7 +1081,7 @@ def check_proc_memory(self, nt): def is_win_secure_system_proc(pid): # see: https://github.com/giampaolo/psutil/issues/2338 - @memoize + @functools.lru_cache def get_procs(): ret = {} out = sh("tasklist.exe /NH /FO csv") diff --git a/tests/test_misc.py b/tests/test_misc.py index fd608ebae8..6fd0612ae4 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -22,7 +22,6 @@ 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 @@ -364,141 +363,6 @@ def test_sanity_version_check(self): # =================================================================== -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() - assert self.calls == [((), {})] - if expected_retval is not None: - assert ret == expected_retval - # with args - for _ in range(2): - ret = obj(1) - assert self.calls == [((), {}), ((1,), {})] - if expected_retval is not None: - assert ret == expected_retval - # with args + kwargs - for _ in range(2): - ret = obj(1, bar=2) - assert self.calls == [((), {}), ((1,), {}), ((1,), {'bar': 2})] - if expected_retval is not None: - assert ret == expected_retval - # clear cache - assert len(self.calls) == 3 - obj.cache_clear() - ret = obj() - if expected_retval is not None: - assert ret == expected_retval - assert len(self.calls) == 4 - # docstring - assert 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) - assert 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 - - assert Bar() is Bar() - assert id(Bar()) == id(Bar()) - assert id(Bar(1)) == id(Bar(1)) - assert id(Bar(1, foo=3)) == id(Bar(1, foo=3)) - assert 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 = ((), {}) - assert ret == expected - assert len(calls) == 1 - # with args - for _ in range(2): - ret = foo(1) - expected = ((1,), {}) - assert ret == expected - assert len(calls) == 2 - # with args + kwargs - for _ in range(2): - ret = foo(1, bar=2) - expected = ((1,), {'bar': 2}) - assert ret == expected - assert len(calls) == 3 - # clear cache - foo.cache_clear() - ret = foo() - expected = ((), {}) - assert ret == expected - assert len(calls) == 4 - # docstring - assert foo.__doc__ == "Foo docstring." - - class TestCommonModule(PsutilTestCase): def test_memoize_when_activated(self): class Foo: From 0b40d3175e85abbfc77bece53e3d05754f368336 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 11 Mar 2026 01:27:28 +0100 Subject: [PATCH 1577/1714] Move HISTORY / CHANGELOG into official doc (#2760) For the last 18 years we have kept track of changes inside the `HISTORY.rst` file. We now integrate it into the official doc, moving it from the root dir into `docs/changelog.rst`. Integration with doc is a lot better because we can reuse links already defined by the doc itself.. E.g. we can just do `:func:disk_partitions()` and the link will point to the right function def. Also, a changelog shown in the doc is quite more visible than a .rst file in the root dir. In previous https://github.com/giampaolo/psutil/pull/2758 I also moved `DEVNOTES.rst` and `INSTALL.rst` into the doc dir, so at this point it is natural to also move `HISTORY.rst` . Since both INSTALL.rst and HISTORY.rst have existed since forever and are referenced in many places across the internet, the original files remain in the root directory with a short note pointing to their new locations: - https://github.com/giampaolo/psutil/blob/master/HISTORY.rst - https://github.com/giampaolo/psutil/blob/master/INSTALL.rst --- .github/workflows/issues.py | 4 +- CONTRIBUTING.md | 4 +- HISTORY.rst | 6113 +------------------------ INSTALL.rst | 4 + MANIFEST.in | 5 +- Makefile | 23 +- README.rst | 3 +- docs/_ext/add_home_link.py | 24 + docs/_ext/changelog_anchors.py | 37 + docs/_links.rst | 7 - docs/api.rst | 2 +- docs/changelog.rst | 3078 +++++++++++++ docs/conf.py | 342 +- docs/devguide.rst | 87 +- docs/faq.rst | 2 +- docs/index.rst | 5 + docs/timeline.rst | 206 +- scripts/internal/convert_readme.py | 3 +- scripts/internal/find_broken_links.py | 10 +- scripts/internal/print_announce.py | 36 +- scripts/internal/print_timeline.py | 52 - 21 files changed, 3374 insertions(+), 6673 deletions(-) create mode 100644 INSTALL.rst create mode 100644 docs/_ext/add_home_link.py create mode 100644 docs/_ext/changelog_anchors.py create mode 100644 docs/changelog.rst delete mode 100755 scripts/internal/print_timeline.py diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 3bc00d3fe7..35a1d77cb6 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -123,7 +123,7 @@ It looks like you're missing `Python.h` headers. This usually means you have \ to install them first, then retry psutil installation. Please read \ -[INSTALL](https://github.com/giampaolo/psutil/blob/master/INSTALL.rst) \ +[install](https://psutil.readthedocs.io/install) \ instructions for your platform. \ This is an auto-generated response based on the text you submitted. \ If this was a mistake or you think there's a bug with psutil installation \ @@ -321,7 +321,7 @@ def on_new_pr(issue): pass # pr = get_repo().get_pull(issue.number) # files = [x.filename for x in list(pr.get_files())] - # if "HISTORY.rst" not in files: + # if "changelog.rst" not in files: # issue.create_comment(REPLY_UPDATE_CHANGELOG) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8da409f06..85fa70ccab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ better to **discuss it first**, either on the issue tracker, the forum or via private email. - In order to get acquainted with the code base and tooling, take a look at the - **[Development Guide](https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst)**. + **[Development Guide](https://psutil.readthedocs.io/devguide)**. - If you can, remember to update - [HISTORY.rst](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) + [changelog.rst](https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst) and [CREDITS](https://github.com/giampaolo/psutil/blob/master/CREDITS) file. diff --git a/HISTORY.rst b/HISTORY.rst index 8660f09b4c..77a827a6f6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6111 +1,4 @@ -*Bug tracker at https://github.com/giampaolo/psutil/issues* +History has moved to: -8.0.0 (IN DEVELOPMENT) -====================== - -**Enhancements** - -- 1946_: add inline type hints to all public APIs in `psutil/__init__.py`. - Type checkers (mypy, pyright, etc.) can now statically verify code that - uses psutil. No runtime behavior is changed; the annotations are purely - informational. -- 2729_: New `Process.page_faults()`_ method, returning a ``(minor, major)`` - namedtuple. -- 2745_: Drastically improve `virtual_memory()`_ docstring, which is now more - detailed, and includes a table with all the available metrics on each - platform. -- 2731_, 2736_, 2723_, 2733_: Reorganization of process memory APIs. - - - Add new `Process.memory_info_ex()`_ method, which extends - `Process.memory_info()`_ with platform-specific metrics: - - - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, - *swap*, *hugetlb* - - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, - *phys_footprint* - - Windows: *virtual*, *peak_virtual* - - - Add new `Process.memory_footprint()`_ method, which returns *uss*, *pss* - and *swap* metrics (what `Process.memory_full_info()`_ used to return, - which is now **deprecated**). - - - `Process.memory_info()`_ named tuple changed: - - - BSD: added *peak_rss*. - - - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated - aliases returning 0 and emitting `DeprecationWarning` are kept. - - - macOS: *pfaults* and *pageins* removed with **no - backward-compataliases**. Use `Process.page_faults()`_ instead. - - - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. - At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, - *peak_nonpaged_pool* were moved to `Process.memory_info_ex()`_. All these - old names still work but raise `DeprecationWarning`. - - - `Process.memory_full_info()`_ is **deprecated**. Use the new - `Process.memory_footprint()`_ instead. - -- 2747_: the field order of the named tuple returned by `cpu_times()`_ has been - normalized on all platforms, and the first 3 fields are now always ``user, - system, idle``. See compatibility notes below. -- 2751_: convert all named tuples in `psutil/_ntuples.py`_ from - ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type - annotations**. This makes the classes self-documenting, effectively turning - this module into a readable API reference. -- 2753_: Introduce enum classes (`ProcessStatus`_, `ConnectionStatus`_, - `ProcessIOPriority`_, `ProcessPriority`_, `ProcessRlimit`_) grouping related - constants. The individual top-level constants (e.g. - ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases for - the corresponding enum members. -- 2754_: standardize `sensors_battery()`_'s `percent` so that it returns a - `float` instead of `int` on all systems, not only Linux. -- 2757_: split the documentation from a single-page HTML document into multiple - sub-sections. Sections now include separate pages for API reference, - installation, release timeline, FAQs, and more. - -**Bug fixes** - -- 2726_, [macOS]: `Process.num_ctx_switches()`_ return an unusual high number - due to a C type precision issue. -- 2411_ [macOS]: `Process.cpu_times()`_ and `Process.cpu_percent()`_ - calculation on macOS x86_64 (arm64 is fine) was highly inaccurate (41.67x - lower). -- 2732_, [Linux]: net_if_duplex_speed: handle EBUSY from ioctl(SIOCETHTOOL). -- 2744_, [NetBSD]: fix possible double `free()` in `swap_memory()`_. -- 2746_, [FreeBSD]: `Process.memory_maps()`_, `rss` and `private` fields, are - erroneously reported in memory pages instead of bytes. Other platforms - (Linux, macOS, Windows) return bytes. - -**Compatibility notes** - -Changes that break backwards compatibility. - -- Dropped support for Python 3.6. - -Named tuples: - -- `cpu_times()`_: - - - On Linux, macOS and BSD the field order of the returned named tuple - changed: ``user, system, idle`` are now always the first 3 fields on all - platforms, with platform-specific fields (e.g. ``nice``) following. - Positional access (e.g. ``cpu_times()[3]``) may silently return the wrong - field. Always use attribute access instead (e.g. ``cpu_times().idle``). - -- `Process.memory_info()`_: - - - The returned named tuple changed size and field order. - Positional access (e.g. ``p.memory_info()[3]`` or ``a, b, c = - p.memory_info()``) may break or silently return the wrong field. Always use - attribute access instead (e.g. ``p.memory_info().rss``). - -Enums: - -- `Process.status()`_ now returns a :class:`psutil.ProcessStatus` enum member - instead of a plain ``str``. Since :class:`psutil.ProcessStatus` is a - ``StrEnum``, it compares equal to its string value (e.g. - ``p.status() == "running"`` still works), but ``repr()`` and ``type()`` - differ. Use :data:`psutil.STATUS_RUNNING` and friends as before; - ``psutil.STATUS_RUNNING`` is now an alias for the enum member. - -- `net_connections()`_ and `Process.net_connections()`_: the ``status`` field - now returns a :class:`psutil.ConnectionStatus` enum member instead of a - plain ``str``. Same ``StrEnum`` compatibility rules as above apply. - -- ``RLIMIT_*`` / ``RLIM_*`` constants (Linux, FreeBSD): these are now members - of the :class:`psutil.ProcessRlimit` ``IntEnum`` instead of plain integers. - Since ``IntEnum`` compares equal to integers, existing code using them as - arguments to :meth:`Process.rlimit` is unaffected. - -7.2.3 -===== - -2026-02-08 - -**Bug fixes** - -- 2715_, [Linux]: ``wait_pid_pidfd_open()`` (from `Process.wait()`_) crashes - with ``EINVAL`` due to kernel race condition. - -7.2.2 -===== - -2026-01-28 - -**Enhancements** - -- 2705_: [Linux]: `Process.wait()`_ now uses ``pidfd_open()`` + ``poll()`` for - waiting, resulting in no busy loop and faster response times. Requires - Linux >= 5.3 and Python >= 3.9. Falls back to traditional polling if - unavailable. -- 2705_: [macOS], [BSD]: `Process.wait()`_ now uses ``kqueue()`` for waiting, - resulting in no busy loop and faster response times. - -**Bug fixes** - -- 2701_, [macOS]: fix compilation error on macOS < 10.7. (patch by Sergey - Fedorov) -- 2707_, [macOS]: fix potential memory leaks in error paths of - `Process.memory_full_info()` and `Process.threads()`. -- 2708_, [macOS]: Process.cmdline()`_ and `Process.environ()`_ may fail with - ``OSError: [Errno 0] Undefined error`` (from ``sysctl(KERN_PROCARGS2)``). - They now raise `AccessDenied`_ instead. - -7.2.1 -===== - -2025-12-29 - -**Bug fixes** - -- 2699_, [FreeBSD], [NetBSD]: `heap_info()`_ does not detect small allocations - (<= 1K). In order to fix that, we now flush internal jemalloc cache before - fetching the metrics. - -7.2.0 -===== - -2025-12-23 - -**Enhancements** - -- 1275_: new `heap_info()`_ and `heap_trim()`_ functions, providing direct - access to the platform's native C heap allocator (glibc, mimalloc, - libmalloc). Useful to create tools to detect memory leaks. -- 2403_, [Linux]: publish wheels for Linux musl. -- 2680_: unit tests are no longer installed / part of the distribution. They - now live under `tests/` instead of `psutil/tests`. - -**Bug fixes** - -* 2684_, [FreeBSD], [critical]: compilation fails on FreeBSD 14 due to missing - include. -* 2691_, [Windows]: fix memory leak in `net_if_stats()`_ due to missing - ``Py_CLEAR``. - -**Compatibility notes** - -- 2680_: `import psutil.tests` no longer works (but it was never documented to - begin with). - -7.1.3 -===== - -2025-11-02 - -**Enhancements** - -- 2667_: enforce `clang-format` on all C and header files. It is now the - mandatory formatting style for all C sources. -- 2672_, [macOS], [BSD]: increase the chances to recognize zombie processes and - raise the appropriate exception (`ZombieProcess`_). -- 2676_, 2678_: replace unsafe `sprintf` / `snprintf` / `sprintf_s` calls with - `str_format()`. Replace `strlcat` / `strlcpy` with safe `str_copy` / - `str_append`. This unifies string handling across platforms and reduces - unsafe usage of standard string functions, improving robustness. - -**Bug fixes** - -- 2674_, [Windows]: `disk_usage()`_ could truncate values on 32-bit platforms, - potentially reporting incorrect total/free/used space for drives larger than - 4GB. -- 2675_, [macOS]: `Process.status()`_ incorrectly returns "running" for 99% - of the processes. -- 2677_, [Windows]: fix MAC address string construction in `net_if_addrs()`_. - Previously, the MAC address buffer was incorrectly updated using a fixed - increment and `sprintf_s`, which could overflow or misformat the - string if the MAC length or formatting changed. Also, the final '\n' was - inserted unnecessarily. -- 2679_, [OpenBSD], [NetBSD], [critical]: can't build due to C syntax error. - -7.1.2 -===== - -2025-10-25 - -**Enhancements** - -- 2657_: stop publishing prebuilt Linux and Windows wheels for 32-bit Python. - 32-bit CPython is still supported, but psutil must now be built from source. - 2565_: produce wheels for free-thread cPython 3.13 and 3.14 (patch by - Lysandros Nikolaou) - -**Bug fixes** - -- 2650_, [macOS]: `Process.cmdline()`_ and `Process.environ()`_ may incorrectly - raise `NoSuchProcess`_ instead of `ZombieProcess`_. -- 2658_, [macOS]: double ``free()`` in `Process.environ()`_ when it fails - internally. This posed a risk of segfault. -- 2662_, [macOS]: massive C code cleanup to guard against possible segfaults - which were (not so) sporadically spotted on CI. - -**Compatibility notes** - -- 2657_: stop publishing prebuilt Linux and Windows wheels for 32-bit Python. - -7.1.1 -===== - -2025-10-19 - -**Enhancements** - -- 2645_, [SunOS]: dropped support for SunOS 10. -- 2646_, [SunOS]: add CI test runner for SunOS. - -**Bug fixes** - -- 2641_, [SunOS]: cannot compile psutil from sources due to missing C include. -- 2357_, [SunOS]: `Process.cmdline()`_ does not handle spaces properly. (patch - by Ben Raz) - -**Compatibility notes** - -* 2645_: SunOS 10 is no longer supported. - -7.1.0 -===== - -2025-09-17 - -**Enhancements** - -- 2581_, [Windows]: publish ARM64 wheels. (patch by Matthieu Darbois) -- 2571_, [FreeBSD]: Dropped support for FreeBSD 8 and earlier. FreeBSD 8 was - maintained from 2009 to 2013. -- 2575_: introduced `dprint` CLI tool to format .yml and .md files. - -**Bug fixes** - -- 2473_, [macOS]: Fix build issue on macOS 11 and lower. -- 2494_, [Windows]: All APIs dealing with paths, such as - `Process.memory_maps()`_, `Process.exe()`_ and `Process.open_files()`_ does - not properly handle UNC paths. Paths such as ``\\??\\C:\\Windows\\Temp`` and - ``'\\Device\\HarddiskVolume1\\Windows\\Temp'`` are now converted to - ``C:\\Windows\\Temp``. (patch by Ben Peddell) -- 2506_, [Windows]: Windows service APIs had issues with unicode services using - special characters in their name. -- 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due - to a race condition. -- 2526_, [Linux]: `Process.create_time()`_, which is used to univocally - identify a process over time, is subject to system clock updates, and as such - can lead to `Process.is_running()`_ returning a wrong result. A monotonic - creation time is now used instead. (patch by Jonathan Kohler) -- 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will - now raise `AccessDenied`_ instead. -- 2540_, [macOS]: `boot_time()`_ is off by 45 seconds (C precision issue). -- 2541_, 2570_, 2578_ [Linux], [macOS], [NetBSD]: `Process.create_time()`_ does - not reflect system clock updates. -- 2542_: if system clock is updated `Process.children()`_ and - `Process.parent()`_ may not be able to return the right information. -- 2545_: [Illumos]: Fix handling of MIB2_UDP_ENTRY in `net_connections()`_. -- 2552_, [Windows]: `boot_time()`_ didn't take into account the time spent - during suspend / hibernation. -- 2560_, [Linux]: `Process.memory_maps()`_ may crash with `IndexError` on - RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien - Stephan) -- 2586_, [macOS], [CRITICAL]: fixed different places in C code which can - trigger a segfault. -- 2604_, [Linux]: `virtual_memory()`_ "used" memory does not match recent - versions of ``free`` CLI utility. (patch by Isaac K. Ko) -- 2605_, [Linux]: `sensors_battery()`_ reports a negative amount for - seconds left. -- 2607_, [Windows]: ``WindowsService.description()`` method may fail with - ``ERROR_NOT_FOUND``. Now it returns an empty string instead. -- 2610:, [macOS], [CRITICAL]: fix `cpu_freq()`_ segfault on ARM architectures. - -**Compatibility notes** - -- 2571_: dropped support for FreeBSD 8 and earlier. - -7.0.0 -===== - -2025-02-13 - -**Enhancements** - -- 669_, [Windows]: `net_if_addrs()`_ also returns the ``broadcast`` address - instead of ``None``. -- 2480_: Python 2.7 is no longer supported. Latest version supporting Python - 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. -- 2490_: removed long deprecated ``Process.memory_info_ex()`` method. It was - deprecated in psutil 4.0.0, released 8 years ago. Substitute is - ``Process.memory_full_info()``. - -**Bug fixes** - -- 2496_, [Linux]: Avoid segfault (a cPython bug) on ``Process.memory_maps()`` - for processes that use hundreds of GBs of memory. -- 2502_, [macOS]: `virtual_memory()`_ now relies on ``host_statistics64`` - instead of ``host_statistics``. This is the same approach used by ``vm_stat`` - CLI tool, and should grant more accurate results. - -**Compatibility notes** - -- 2480_: Python 2.7 is no longer supported. -- 2490_: removed long deprecated ``Process.memory_info_ex()`` method. - -6.1.1 -===== - -2024-12-19 - -**Enhancements** - -- 2471_: use Vulture CLI tool to detect dead code. - -**Bug fixes** - -- 2418_, [Linux]: fix race condition in case /proc/PID/stat does not exist, but - /proc/PID does, resulting in FileNotFoundError. -- 2470_, [Linux]: `users()`_ may return "localhost" instead of the actual IP - address of the user logged in. - -6.1.0 -===== - -2024-10-17 - -**Enhancements** - -- 2366_, [Windows]: drastically speedup `process_iter()`_. We now determine - process unique identity by using process "fast" create time method. This - will considerably speedup those apps which use `process_iter()`_ only once, - e.g. to look for a process with a certain name. -- 2446_: use pytest instead of unittest. -- 2448_: add ``make install-sysdeps`` target to install the necessary system - dependencies (python-dev, gcc, etc.) on all supported UNIX flavors. -- 2449_: add ``make install-pydeps-test`` and ``make install-pydeps-dev`` - targets. They can be used to install dependencies meant for running tests and - for local development. They can also be installed via ``pip install .[test]`` - and ``pip install .[dev]``. -- 2456_: allow to run tests via ``python3 -m psutil.tests`` even if ``pytest`` - module is not installed. This is useful for production environments that - don't have pytest installed, but still want to be able to test psutil - installation. - -**Bug fixes** - -- 2427_: psutil (segfault) on import in the free-threaded (no GIL) version of - Python 3.13. (patch by Sam Gross) -- 2455_, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and - field 40 (blkio_ticks) is missing. -- 2457_, [AIX]: significantly improve the speed of `Process.open_files()`_ for - some edge cases. -- 2460_, [OpenBSD]: `Process.num_fds()`_ and `Process.open_files()`_ may fail - with `NoSuchProcess`_ for PID 0. Instead, we now return "null" values (0 and - [] respectively). - -6.0.0 -====== - -2024-06-18 - -**Enhancements** - -- 2109_: ``maxfile`` and ``maxpath`` fields were removed from the namedtuple - returned by `disk_partitions()`_. Reason: on network filesystems (NFS) this - can potentially take a very long time to complete. -- 2366_, [Windows]: log debug message when using slower process APIs. -- 2375_, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) -- 2396_: `process_iter()`_ no longer pre-emptively checks whether PIDs have - been reused. This makes `process_iter()`_ around 20x times faster. -- 2396_: a new ``psutil.process_iter.cache_clear()`` API can be used the clear - `process_iter()`_ internal cache. -- 2401_, Support building with free-threaded CPython 3.13. (patch by Sam Gross) -- 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. - The old name is still available, but it's deprecated (triggers a - ``DeprecationWarning``) and will be removed in the future. -- 2425_: [Linux]: provide aarch64 wheels. (patch by Matthieu Darbois / Ben Raz) - -**Bug fixes** - -- 2250_, [NetBSD]: `Process.cmdline()`_ sometimes fail with EBUSY. It usually - happens for long cmdlines with lots of arguments. In this case retry getting - the cmdline for up to 50 times, and return an empty list as last resort. -- 2254_, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch - by Shade Gladden) -- 2272_: Add pickle support to psutil Exceptions. -- 2359_, [Windows], [CRITICAL]: `pid_exists()`_ disagrees with `Process`_ on - whether a pid exists when ERROR_ACCESS_DENIED. -- 2360_, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) -- 2362_, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) -- 2365_, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) -- 2395_, [OpenBSD]: `pid_exists()`_ erroneously return True if the argument is - a thread ID (TID) instead of a PID (process ID). -- 2412_, [macOS]: can't compile on macOS 10.4 PowerPC due to missing `MNT_` - constants. - -**Porting notes** - -Version 6.0.0 introduces some changes which affect backward compatibility: - -- 2109_: the namedtuple returned by `disk_partitions()`_' no longer has - ``maxfile`` and ``maxpath`` fields. -- 2396_: `process_iter()`_ no longer pre-emptively checks whether PIDs have - been reused. If you want to check for PID reusage you are supposed to use - `Process.is_running()`_ against the yielded `Process`_ instances. That will - also automatically remove reused PIDs from `process_iter()`_ internal cache. -- 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. - The old name is still available, but it's deprecated (triggers a - ``DeprecationWarning``) and will be removed in the future. - -5.9.8 -===== - -2024-01-19 - -**Enhancements** - -- 2343_, [FreeBSD]: filter `net_connections()`_ returned list in C instead of - Python, and avoid to retrieve unnecessary connection types unless explicitly - asked. E.g., on an IDLE system with few IPv6 connections this will run around - 4 times faster. Before all connection types (TCP, UDP, UNIX) were retrieved - internally, even if only a portion was returned. -- 2342_, [NetBSD]: same as above (#2343) but for NetBSD. -- 2349_: adopted black formatting style. - -**Bug fixes** - -- 930_, [NetBSD], [critical]: `net_connections()`_ implementation was broken. - It could either leak memory or core dump. -- 2340_, [NetBSD]: if process is terminated, `Process.cwd()`_ will return an - empty string instead of raising `NoSuchProcess`_. -- 2345_, [Linux]: fix compilation on older compiler missing DUPLEX_UNKNOWN. -- 2222_, [macOS]: `cpu_freq()` now returns fixed values for `min` and `max` - frequencies in all Apple Silicon chips. - -5.9.7 -===== - -2023-12-17 - -**Enhancements** - -- 2324_: enforce Ruff rule `raw-string-in-exception`, which helps providing - clearer tracebacks when exceptions are raised by psutil. - -**Bug fixes** - -- 2325_, [PyPy]: psutil did not compile on PyPy due to missing - `PyErr_SetExcFromWindowsErrWithFilenameObject` cPython API. - -5.9.6 -===== - -2023-10-15 - -**Enhancements** - -- 1703_: `cpu_percent()`_ and `cpu_times_percent()`_ are now thread safe, - meaning they can be called from different threads and still return - meaningful and independent results. Before, if (say) 10 threads called - ``cpu_percent(interval=None)`` at the same time, only 1 thread out of 10 - would get the right result. -- 2266_: if `Process`_ class is passed a very high PID, raise `NoSuchProcess`_ - instead of OverflowError. (patch by Xuehai Pan) -- 2246_: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) -- 2290_: PID reuse is now pre-emptively checked for `Process.ppid()`_ and - `Process.parents()`_. -- 2312_: use ``ruff`` Python linter instead of ``flake8 + isort``. It's an - order of magnitude faster + it adds a ton of new code quality checks. - -**Bug fixes** - -- 2195_, [Linux]: no longer print exception at import time in case /proc/stat - can't be read due to permission error. Redirect it to ``PSUTIL_DEBUG`` - instead. -- 2241_, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas - Klausner) -- 2245_, [Windows]: fix var unbound error on possibly in `swap_memory()`_ - (patch by student_2333) -- 2268_: ``bytes2human()`` utility function was unable to properly represent - negative values. -- 2252_, [Windows]: `disk_usage()`_ fails on Python 3.12+. (patch by - Matthieu Darbois) -- 2284_, [Linux]: `Process.memory_full_info()`_ may incorrectly raise - `ZombieProcess`_ if it's determined via ``/proc/pid/smaps_rollup``. Instead - we now fallback on reading ``/proc/pid/smaps``. -- 2287_, [OpenBSD], [NetBSD]: `Process.is_running()`_ erroneously return - ``False`` for zombie processes, because creation time cannot be determined. -- 2288_, [Linux]: correctly raise `ZombieProcess`_ on `Process.exe()`_, - `Process.cmdline()`_ and `Process.memory_maps()`_ instead of returning a - "null" value. -- 2290_: differently from what stated in the doc, PID reuse is not - pre-emptively checked for `Process.nice()`_ (set), `Process.ionice()`_, - (set), `Process.cpu_affinity()`_ (set), `Process.rlimit()`_ - (set), `Process.parent()`_. -- 2308_, [OpenBSD]: `Process.threads()`_ always fail with AccessDenied (also as - root). - -5.9.5 -===== - -2023-04-17 - -**Enhancements** - -- 2196_: in case of exception, display a cleaner error traceback by hiding the - `KeyError` bit deriving from a missed cache hit. -- 2217_: print the full traceback when a `DeprecationWarning` or `UserWarning` - is raised. -- 2230_, [OpenBSD]: `net_connections()`_ implementation was rewritten - from scratch: - - We're now able to retrieve the path of AF_UNIX sockets (before it was an - empty string) - - The function is faster since it no longer iterates over all processes. - - No longer produces duplicate connection entries. -- 2238_: there are cases where `Process.cwd()`_ cannot be determined - (e.g. directory no longer exists), in which case we returned either ``None`` - or an empty string. This was consolidated and we now return ``""`` on all - platforms. -- 2239_, [UNIX]: if process is a zombie, and we can only determine part of the - its truncated `Process.name()`_ (15 chars), don't fail with `ZombieProcess`_ - when we try to guess the full name from the `Process.cmdline()`_. Just - return the truncated name. -- 2240_, [NetBSD], [OpenBSD]: add CI testing on every commit for NetBSD and - OpenBSD platforms (python 3 only). - -**Bug fixes** - -- 1043_, [OpenBSD] `net_connections()`_ returns duplicate entries. -- 1915_, [Linux]: on certain kernels, ``"MemAvailable"`` field from - ``/proc/meminfo`` returns ``0`` (possibly a kernel bug), in which case we - calculate an approximation for ``available`` memory which matches "free" - CLI utility. -- 2164_, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). -- 2186_, [FreeBSD]: compilation fails with Clang 15. (patch by Po-Chuan Hsieh) -- 2191_, [Linux]: `disk_partitions()`_: do not unnecessarily read - /proc/filesystems and raise `AccessDenied`_ unless user specified `all=False` - argument. -- 2216_, [Windows]: fix tests when running in a virtual environment (patch by - Matthieu Darbois) -- 2225_, [POSIX]: `users()`_ loses precision for ``started`` attribute (off by - 1 minute). -- 2229_, [OpenBSD]: unable to properly recognize zombie processes. - `NoSuchProcess`_ may be raised instead of `ZombieProcess`_. -- 2231_, [NetBSD]: *available* `virtual_memory()`_ is higher than *total*. -- 2234_, [NetBSD]: `virtual_memory()`_ metrics are wrong: *available* and - *used* are too high. We now match values shown by *htop* CLI utility. -- 2236_, [NetBSD]: `Process.num_threads()`_ and `Process.threads()`_ return - threads that are already terminated. -- 2237_, [OpenBSD], [NetBSD]: `Process.cwd()`_ may raise ``FileNotFoundError`` - if cwd no longer exists. Return an empty string instead. - -5.9.4 -===== - -2022-11-07 - -**Enhancements** - -- 2102_: use Limited API when building wheels with CPython 3.6+ on Linux, - macOS and Windows. This allows to use pre-built wheels in all future versions - of cPython 3. (patch by Matthieu Darbois) - -**Bug fixes** - -- 2077_, [Windows]: Use system-level values for `virtual_memory()`_. (patch by - Daniel Widdis) -- 2156_, [Linux]: compilation may fail on very old gcc compilers due to missing - ``SPEED_UNKNOWN`` definition. (patch by Amir Rossert) -- 2010_, [macOS]: on MacOS, arm64 ``IFM_1000_TX`` and ``IFM_1000_T`` are the - same value, causing a build failure. (patch by Lawrence D'Anna) -- 2160_, [Windows]: Get Windows percent swap usage from performance counters. - (patch by Daniel Widdis) - -5.9.3 -===== - -2022-10-18 - -**Enhancements** - -- 2040_, [macOS]: provide wheels for arm64 architecture. (patch by Matthieu - Darbois) - -**Bug fixes** - -- 2116_, [macOS], [critical]: `net_connections()`_ fails with RuntimeError. -- 2135_, [macOS]: `Process.environ()`_ may contain garbage data. Fix - out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) -- 2138_, [Linux], **[critical]**: can't compile psutil on Android due to - undefined ``ethtool_cmd_speed`` symbol. -- 2142_, [POSIX]: `net_if_stats()`_ 's ``flags`` on Python 2 returned unicode - instead of str. (patch by Matthieu Darbois) -- 2147_, [macOS] Fix disk usage report on macOS 12+. (patch by Matthieu Darbois) -- 2150_, [Linux] `Process.threads()`_ may raise ``NoSuchProcess``. Fix race - condition. (patch by Daniel Li) -- 2153_, [macOS] Fix race condition in test_posix.TestProcess.test_cmdline. - (patch by Matthieu Darbois) - -5.9.2 -===== - -2022-09-04 - -**Bug fixes** - -- 2093_, [FreeBSD], **[critical]**: `pids()`_ may fail with ENOMEM. Dynamically - increase the ``malloc()`` buffer size until it's big enough. -- 2095_, [Linux]: `net_if_stats()`_ returns incorrect interface speed for - 100GbE network cards. -- 2113_, [FreeBSD], **[critical]**: `virtual_memory()`_ may raise ENOMEM due to - missing ``#include `` directive. (patch by Peter Jeremy) -- 2128_, [NetBSD]: `swap_memory()`_ was miscalculated. (patch by Thomas Klausner) - -5.9.1 -===== - -2022-05-20 - -**Enhancements** - -- 1053_: drop Python 2.6 support. (patches by Matthieu Darbois and Hugo van - Kemenade) -- 2037_: Add additional flags to net_if_stats. -- 2050_, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading - ``/proc`` pseudo files line by line. This should help having more consistent - results. -- 2057_, [OpenBSD]: add support for `cpu_freq()`_. -- 2107_, [Linux]: `Process.memory_full_info()`_ (reporting process USS/PSS/Swap - memory) now reads ``/proc/pid/smaps_rollup`` instead of ``/proc/pids/smaps``, - which makes it 5 times faster. - -**Bug fixes** - -- 2048_: ``AttributeError`` is raised if ``psutil.Error`` class is raised - manually and passed through ``str``. -- 2049_, [Linux]: `cpu_freq()`_ erroneously returns ``curr`` value in GHz while - ``min`` and ``max`` are in MHz. -- 2050_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` if running in a - LCX container. - -5.9.0 -===== - -2021-12-29 - -**Enhancements** - -- 1851_, [Linux]: `cpu_freq()`_ is slow on systems with many CPUs. Read current - frequency values for all CPUs from ``/proc/cpuinfo`` instead of opening many - files in ``/sys`` fs. (patch by marxin) -- 1992_: `NoSuchProcess`_ message now specifies if the PID has been reused. -- 1992_: error classes (`NoSuchProcess`_, `AccessDenied`_, etc.) now have a better - formatted and separated ``__repr__`` and ``__str__`` implementations. -- 1996_, [BSD]: add support for MidnightBSD. (patch by Saeed Rasooli) -- 1999_, [Linux]: `disk_partitions()`_: convert ``/dev/root`` device (an alias - used on some Linux distros) to real root device path. -- 2005_: ``PSUTIL_DEBUG`` mode now prints file name and line number of the debug - messages coming from C extension modules. -- 2042_: rewrite HISTORY.rst to use hyperlinks pointing to psutil API doc. - -**Bug fixes** - -- 1456_, [macOS], **[critical]**: `cpu_freq()`_ ``min`` and ``max`` are set to - 0 if can't be determined (instead of crashing). -- 1512_, [macOS]: sometimes `Process.connections()`_ will crash with - ``EOPNOTSUPP`` for one connection; this is now ignored. -- 1598_, [Windows]: `disk_partitions()`_ only returns mountpoints on drives - where it first finds one. -- 1874_, [SunOS]: swap output error due to incorrect range. -- 1892_, [macOS]: `cpu_freq()`_ broken on Apple M1. -- 1901_, [macOS]: different functions, especially `Process.open_files()`_ and - `Process.connections()`_, could randomly raise `AccessDenied`_ because the - internal buffer of ``proc_pidinfo(PROC_PIDLISTFDS)`` syscall was not big enough. - We now dynamically increase the buffer size until it's big enough instead of - giving up and raising `AccessDenied`_, which was a fallback to avoid crashing. -- 1904_, [Windows]: ``OpenProcess`` fails with ``ERROR_SUCCESS`` due to - ``GetLastError()`` called after ``sprintf()``. (patch by alxchk) -- 1913_, [Linux]: `wait_procs()`_ should catch ``subprocess.TimeoutExpired`` - exception. -- 1919_, [Linux]: `sensors_battery()`_ can raise ``TypeError`` on PureOS. -- 1921_, [Windows]: `swap_memory()`_ shows committed memory instead of swap. -- 1940_, [Linux]: psutil does not handle ``ENAMETOOLONG`` when accessing process - file descriptors in procfs. (patch by Nikita Radchenko) -- 1948_, **[critical]**: ``memoize_when_activated`` decorator is not thread-safe. - (patch by Xuehai Pan) -- 1953_, [Windows], **[critical]**: `disk_partitions()`_ crashes due to - insufficient buffer len. (patch by MaWe2019) -- 1965_, [Windows], **[critical]**: fix "Fatal Python error: deallocating None" - when calling `users()`_ multiple times. -- 1980_, [Windows]: 32bit / WoW64 processes fails to read `Process.name()`_ longer - than 128 characters resulting in `AccessDenied`_. This is now fixed. (patch - by PetrPospisil) -- 1991_, **[critical]**: `process_iter()`_ is not thread safe and can raise - ``TypeError`` if invoked from multiple threads. -- 1956_, [macOS]: `Process.cpu_times()`_ reports incorrect timings on M1 machines. - (patch by Olivier Dormond) -- 2023_, [Linux]: `cpu_freq()`_ return order is wrong on systems with more than - 9 CPUs. - -5.8.0 -===== - -2020-12-19 - -**Enhancements** - -- 1863_: `disk_partitions()`_ exposes 2 extra fields: ``maxfile`` and ``maxpath``, - which are the maximum file name and path name length. -- 1872_, [Windows]: added support for PyPy 2.7. -- 1879_: provide pre-compiled wheels for Linux and macOS (yey!). -- 1880_: get rid of Travis and Cirrus CI services (they are no longer free). - CI testing is now done by GitHub Actions on Linux, macOS and FreeBSD (yes). - AppVeyor is still being used for Windows CI. - -**Bug fixes** - -- 1708_, [Linux]: get rid of `sensors_temperatures()`_ duplicates. (patch by Tim - Schlueter). -- 1839_, [Windows], **[critical]**: always raise `AccessDenied`_ instead of - ``WindowsError`` when failing to query 64 processes from 32 bit ones by using - ``NtWoW64`` APIs. -- 1866_, [Windows], **[critical]**: `Process.exe()`_, `Process.cmdline()`_, - `Process.environ()`_ may raise "[WinError 998] Invalid access to memory - location" on Python 3.9 / VS 2019. -- 1874_, [SunOS]: wrong swap output given when encrypted column is present. -- 1875_, [Windows], **[critical]**: `Process.username()`_ may raise - ``ERROR_NONE_MAPPED`` if the SID has no corresponding account name. In this - case `AccessDenied`_ is now raised. -- 1886_, [macOS]: ``EIO`` error may be raised on `Process.cmdline()`_ and - `Process.environ()`_. Now it gets translated into `AccessDenied`_. -- 1887_, [Windows], **[critical]**: ``OpenProcess`` may fail with - "[WinError 0] The operation completed successfully"." - Turn it into `AccessDenied`_ or `NoSuchProcess`_ depending on whether the - PID is alive. -- 1891_, [macOS]: get rid of deprecated ``getpagesize()``. - -5.7.3 -===== - -2020-10-23 - -**Enhancements** - -- 809_, [FreeBSD]: add support for `Process.rlimit()`_. -- 893_, [BSD]: add support for `Process.environ()`_ (patch by Armin Gruner) -- 1830_, [POSIX]: `net_if_stats()`_ ``isup`` also checks whether the NIC is - running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) -- 1837_, [Linux]: improved battery detection and charge ``secsleft`` calculation - (patch by aristocratos) - -**Bug fixes** - -- 1620_, [Linux]: `cpu_count()`_ with ``logical=False`` result is incorrect on - systems with more than one CPU socket. (patch by Vincent A. Arcila) -- 1738_, [macOS]: `Process.exe()`_ may raise ``FileNotFoundError`` if process is still - alive but the exe file which launched it got deleted. -- 1791_, [macOS]: fix missing include for ``getpagesize()``. -- 1823_, [Windows], **[critical]**: `Process.open_files()`_ may cause a segfault - due to a NULL pointer. -- 1838_, [Linux]: `sensors_battery()`_: if `percent` can be determined but not - the remaining values, still return a result instead of ``None``. - (patch by aristocratos) - -5.7.2 -===== - -2020-07-15 - -**Bug fixes** - -- wheels for 2.7 were inadvertently deleted. - -5.7.1 -===== - -2020-07-15 - -**Enhancements** - -- 1729_: parallel tests on POSIX (``make test-parallel``). They're twice as fast! -- 1741_, [POSIX]: ``make build`` now runs in parallel on Python >= 3.6 and - it's about 15% faster. -- 1747_: `Process.wait()`_ return value is cached so that the exit code can be - retrieved on then next call. -- 1747_, [POSIX]: `Process.wait()`_ on POSIX now returns an enum, showing the - negative signal which was used to terminate the process. It returns something - like ````. -- 1747_: `Process`_ class provides more info about the process on ``str()`` - and ``repr()`` (status and exit code). -- 1757_: memory leak tests are now stable. -- 1768_, [Windows]: added support for Windows Nano Server. (contributed by - Julien Lebot) - -**Bug fixes** - -- 1726_, [Linux]: `cpu_freq()`_ parsing should use spaces instead of tabs on ia64. - (patch by Michał Górny) -- 1760_, [Linux]: `Process.rlimit()`_ does not handle long long type properly. -- 1766_, [macOS]: `NoSuchProcess`_ may be raised instead of `ZombieProcess`_. -- 1781_, **[critical]**: `getloadavg()`_ can crash the Python interpreter. - (patch by Ammar Askar) - -5.7.0 -===== - -2020-02-18 - -**Enhancements** - -- 1637_, [SunOS]: add partial support for old SunOS 5.10 Update 0 to 3. -- 1648_, [Linux]: `sensors_temperatures()`_ looks into an additional - ``/sys/device/`` directory for additional data. (patch by Javad Karabi) -- 1652_, [Windows]: dropped support for Windows XP and Windows Server 2003. - Minimum supported Windows version now is Windows Vista. -- 1671_, [FreeBSD]: add CI testing/service for FreeBSD (Cirrus CI). -- 1677_, [Windows]: `Process.exe()`_ will succeed for all process PIDs (instead of - raising `AccessDenied`_). -- 1679_, [Windows]: `net_connections()`_ and `Process.connections()`_ are 10% faster. -- 1682_, [PyPy]: added CI / test integration for PyPy via Travis. -- 1686_, [Windows]: added support for PyPy on Windows. -- 1693_, [Windows]: `boot_time()`_, `Process.create_time()`_ and `users()`_'s - login time now have 1 micro second precision (before the precision was of 1 - second). - -**Bug fixes** - -- 1538_, [NetBSD]: `Process.cwd()`_ may return ``ENOENT`` instead of `NoSuchProcess`_. -- 1627_, [Linux]: `Process.memory_maps()`_ can raise ``KeyError``. -- 1642_, [SunOS]: querying basic info for PID 0 results in ``FileNotFoundError``. -- 1646_, [FreeBSD], **[critical]**: many `Process`_ methods may cause a segfault - due to a backward incompatible change in a C type on FreeBSD 12.0. -- 1656_, [Windows]: `Process.memory_full_info()`_ raises `AccessDenied`_ even for the - current user and os.getpid(). -- 1660_, [Windows]: `Process.open_files()`_ complete rewrite + check of errors. -- 1662_, [Windows], **[critical]**: `Process.exe()`_ may raise "[WinError 0] - The operation completed successfully". -- 1665_, [Linux]: `disk_io_counters()`_ does not take into account extra fields - added to recent kernels. (patch by Mike Hommey) -- 1672_: use the right C type when dealing with PIDs (int or long). Thus far - (long) was almost always assumed, which is wrong on most platforms. -- 1673_, [OpenBSD]: `Process.connections()`_, `Process.num_fds()`_ and - `Process.threads()`_ returned improper exception if process is gone. -- 1674_, [SunOS]: `disk_partitions()`_ may raise ``OSError``. -- 1684_, [Linux]: `disk_io_counters()`_ may raise ``ValueError`` on systems not - having ``/proc/diskstats``. -- 1695_, [Linux]: could not compile on kernels <= 2.6.13 due to - ``PSUTIL_HAS_IOPRIO`` not being defined. (patch by Anselm Kruis) - -5.6.7 -===== - -2019-11-26 - -**Bug fixes** - -- 1630_, [Windows], **[critical]**: can't compile source distribution due to C - syntax error. - -5.6.6 -===== - -2019-11-25 - -**Bug fixes** - -- 1179_, [Linux]: `Process.cmdline()`_ now takes into account misbehaving processes - renaming the command line and using inappropriate chars to separate args. -- 1616_, **[critical]**: use of ``Py_DECREF`` instead of ``Py_CLEAR`` will - result in double ``free()`` and segfault - (`CVE-2019-18874 `__). - (patch by Riccardo Schirone) -- 1619_, [OpenBSD], **[critical]**: compilation fails due to C syntax error. - (patch by Nathan Houghton) - -5.6.5 -===== - -2019-11-06 - -**Bug fixes** - -- 1615_: remove ``pyproject.toml`` as it was causing installation issues. - -5.6.4 -===== - -2019-11-04 - -**Enhancements** - -- 1527_, [Linux]: added `Process.cpu_times()`_ ``iowait`` counter, which is the - time spent waiting for blocking I/O to complete. -- 1565_: add PEP 517/8 build backend and requirements specification for better - pip integration. (patch by Bernát Gábor) - -**Bug fixes** - -- 875_, [Windows], **[critical]**: `Process.cmdline()`_, `Process.environ()`_ or - `Process.cwd()`_ may occasionally fail with ``ERROR_PARTIAL_COPY`` which now - gets translated to `AccessDenied`_. -- 1126_, [Linux], **[critical]**: `Process.cpu_affinity()`_ segfaults on CentOS - 5 / manylinux. `Process.cpu_affinity()`_ support for CentOS 5 was removed. -- 1528_, [AIX], **[critical]**: compilation error on AIX 7.2 due to 32 vs 64 - bit differences. (patch by Arnon Yaari) -- 1535_: ``type`` and ``family`` fields returned by `net_connections()`_ are not - always turned into enums. -- 1536_, [NetBSD]: `Process.cmdline()`_ erroneously raise `ZombieProcess`_ error if - cmdline has non encodable chars. -- 1546_: usage percent may be rounded to 0 on Python 2. -- 1552_, [Windows]: `getloadavg()`_ math for calculating 5 and 15 mins values is - incorrect. -- 1568_, [Linux]: use CC compiler env var if defined. -- 1570_, [Windows]: ``NtWow64*`` syscalls fail to raise the proper error code -- 1585_, [OSX]: avoid calling ``close()`` (in C) on possible negative integers. - (patch by Athos Ribeiro) -- 1606_, [SunOS], **[critical]**: compilation fails on SunOS 5.10. - (patch by vser1) - -5.6.3 -===== - -2019-06-11 - -**Enhancements** - -- 1494_, [AIX]: added support for `Process.environ()`_. (patch by Arnon Yaari) - -**Bug fixes** - -- 1276_, [AIX]: can't get whole `Process.cmdline()`_. (patch by Arnon Yaari) -- 1501_, [Windows]: `Process.cmdline()`_ and `Process.exe()`_ raise unhandled - "WinError 1168 element not found" exceptions for "Registry" and - "Memory Compression" pseudo processes on Windows 10. -- 1526_, [NetBSD], **[critical]**: `Process.cmdline()`_ could raise - ``MemoryError``. (patch by Kamil Rytarowski) - -5.6.2 -===== - -2019-04-26 - -**Enhancements** - -- 604_, [Windows]: add new `getloadavg()`_, returning system load average - calculation, including on Windows (emulated). (patch by Ammar Askar) -- 1404_, [Linux]: `cpu_count()`_ with ``logical=False`` uses a second method - (read from ``/sys/devices/system/cpu/cpu[0-9]/topology/core_id``) in order to - determine the number of CPU cores in case ``/proc/cpuinfo`` does not provide this - info. -- 1458_: provide coloured test output. Also show failures on - ``KeyboardInterrupt``. -- 1464_: various docfixes (always point to Python 3 doc, fix links, etc.). -- 1476_, [Windows]: it is now possible to set process high I/O priority - (`Process.ionice()`_). Also, I/O priority values are now exposed as 4 new - constants: ``IOPRIO_VERYLOW``, ``IOPRIO_LOW``, ``IOPRIO_NORMAL``, - ``IOPRIO_HIGH``. -- 1478_: add make command to re-run tests failed on last run. - -**Bug fixes** - -- 1223_, [Windows]: `boot_time()`_ may return incorrect value on Windows XP. -- 1456_, [Linux]: `cpu_freq()`_ returns ``None`` instead of 0.0 when ``min`` - and ``max`` fields can't be determined. (patch by Alex Manuskin) -- 1462_, [Linux]: (tests) make tests invariant to ``LANG`` setting (patch by - Benjamin Drung) -- 1463_: `cpu_distribution.py`_ script was broken. -- 1470_, [Linux]: `disk_partitions()`_: fix corner case when ``/etc/mtab`` - doesn't exist. (patch by Cedric Lamoriniere) -- 1471_, [SunOS]: `Process.name()`_ and `Process.cmdline()`_ can return - ``SystemError``. (patch by Daniel Beer) -- 1472_, [Linux]: `cpu_freq()`_ does not return all CPUs on Raspberry-pi 3. -- 1474_: fix formatting of ``psutil.tests()`` which mimics ``ps aux`` output. -- 1475_, [Windows], **[critical]**: ``OSError.winerror`` attribute wasn't - properly checked resulting in ``WindowsError(ERROR_ACCESS_DENIED)`` being - raised instead of `AccessDenied`_. -- 1477_, [Windows]: wrong or absent error handling for private ``NTSTATUS`` - Windows APIs. Different process methods were affected by this. -- 1480_, [Windows], **[critical]**: `cpu_count()`_ with ``logical=False`` could - cause a crash due to fixed read violation. (patch by Samer Masterson) -- 1486_, [AIX], [SunOS]: ``AttributeError`` when interacting with `Process`_ - methods involved into `Process.oneshot()`_ context. -- 1491_, [SunOS]: `net_if_addrs()`_: use ``free()`` against ``ifap`` struct - on error. (patch by Agnewee) -- 1493_, [Linux]: `cpu_freq()`_: handle the case where - ``/sys/devices/system/cpu/cpufreq/`` exists but it's empty. - -5.6.1 -===== - -2019-03-11 - -**Bug fixes** - -- 1329_, [AIX]: psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) -- 1448_, [Windows], **[critical]**: crash on import due to ``rtlIpv6AddressToStringA`` - not available on Wine. -- 1451_, [Windows], **[critical]**: `Process.memory_full_info()`_ segfaults. - ``NtQueryVirtualMemory`` is now used instead of ``QueryWorkingSet`` to - calculate USS memory. - -5.6.0 -===== - -2019-03-05 - -**Enhancements** - -- 1379_, [Windows]: `Process.suspend()`_ and `Process.resume()`_ now use - ``NtSuspendProcess`` and ``NtResumeProcess`` instead of stopping/resuming all - threads of a process. This is faster and more reliable (aka this is what - ProcessHacker does). -- 1420_, [Windows]: in case of exception `disk_usage()`_ now also shows the path - name. -- 1422_, [Windows]: Windows APIs requiring to be dynamically loaded from DLL - libraries are now loaded only once on startup (instead of on per function - call) significantly speeding up different functions and methods. -- 1426_, [Windows]: ``PAGESIZE`` and number of processors is now calculated on - startup. -- 1428_: in case of error, the traceback message now shows the underlying C - function called which failed. -- 1433_: new `Process.parents()`_ method. (idea by Ghislain Le Meur) -- 1437_: `pids()`_ are returned in sorted order. -- 1442_: Python 3 is now the default interpreter used by Makefile. - -**Bug fixes** - -- 1353_: `process_iter()`_ is now thread safe (it rarely raised ``TypeError``). -- 1394_, [Windows], **[critical]**: `Process.name()`_ and `Process.exe()`_ may - erroneously return "Registry" or fail with "[Error 0] The operation completed - successfully". - ``QueryFullProcessImageNameW`` is now used instead of - ``GetProcessImageFileNameW`` in order to prevent that. -- 1411_, [BSD]: lack of ``Py_DECREF`` could cause segmentation fault on process - instantiation. -- 1419_, [Windows]: `Process.environ()`_ raises ``NotImplementedError`` when - querying a 64-bit process in 32-bit-WoW mode. Now it raises `AccessDenied`_. -- 1427_, [OSX]: `Process.cmdline()`_ and `Process.environ()`_ may erroneously - raise ``OSError`` on failed ``malloc()``. -- 1429_, [Windows]: ``SE DEBUG`` was not properly set for current process. It is - now, and it should result in less `AccessDenied`_ exceptions for low PID - processes. -- 1432_, [Windows]: `Process.memory_info_ex()`_'s USS memory is miscalculated - because we're not using the actual system ``PAGESIZE``. -- 1439_, [NetBSD]: `Process.connections()`_ may return incomplete results if using - `Process.oneshot()`_. -- 1447_: original exception wasn't turned into `NoSuchProcess`_ / `AccessDenied`_ - exceptions when using `Process.oneshot()`_ context manager. - -**Incompatible API changes** - -- 1291_, [OSX], **[critical]**: `Process.memory_maps()`_ was removed because - inherently broken (segfault) for years. - -5.5.1 -===== - -2019-02-15 - -**Enhancements** - -- 1348_, [Windows]: on Windows >= 8.1 if `Process.cmdline()`_ fails due to - ``ERROR_ACCESS_DENIED`` attempt using ``NtQueryInformationProcess`` + - ``ProcessCommandLineInformation``. (patch by EccoTheFlintstone) - -**Bug fixes** - -- 1394_, [Windows]: `Process.exe()`_ returns "[Error 0] The operation completed - successfully" when Python process runs in "Virtual Secure Mode". -- 1402_: psutil exceptions' ``repr()`` show the internal private module path. -- 1408_, [AIX], **[critical]**: psutil won't compile on AIX 7.1 due to missing - header. (patch by Arnon Yaari) - -5.5.0 -===== - -2019-01-23 - -**Enhancements** - -- 1350_, [FreeBSD]: added support for `sensors_temperatures()`_. (patch by Alex - Manuskin) -- 1352_, [FreeBSD]: added support for `cpu_freq()`_. (patch by Alex Manuskin) - -**Bug fixes** - -- 1111_: `Process.oneshot()`_ is now thread safe. -- 1354_, [Linux]: `disk_io_counters()`_ fails on Linux kernel 4.18+. -- 1357_, [Linux]: `Process.memory_maps()`_ and `Process.io_counters()`_ methods - are no longer exposed if not supported by the kernel. -- 1368_, [Windows]: fix `Process.ionice()`_ mismatch. (patch by - EccoTheFlintstone) -- 1370_, [Windows]: improper usage of ``CloseHandle()`` may lead to override the - original error code when raising an exception. -- 1373_, **[critical]**: incorrect handling of cache in `Process.oneshot()`_ - context causes `Process`_ instances to return incorrect results. -- 1376_, [Windows]: ``OpenProcess`` now uses ``PROCESS_QUERY_LIMITED_INFORMATION`` - access rights wherever possible, resulting in less `AccessDenied`_ exceptions - being thrown for system processes. -- 1376_, [Windows]: check if variable is ``NULL`` before ``free()`` ing it. - (patch by EccoTheFlintstone) - -5.4.8 -===== - -2018-10-30 - -**Enhancements** - -- 1197_, [Linux]: `cpu_freq()`_ is now implemented by parsing ``/proc/cpuinfo`` - in case ``/sys/devices/system/cpu/*`` filesystem is not available. -- 1310_, [Linux]: `sensors_temperatures()`_ now parses ``/sys/class/thermal`` - in case ``/sys/class/hwmon`` fs is not available (e.g. Raspberry Pi). (patch - by Alex Manuskin) -- 1320_, [POSIX]: better compilation support when using g++ instead of GCC. - (patch by Jaime Fullaondo) - -**Bug fixes** - -- 715_: do not print exception on import time in case `cpu_times()`_ fails. -- 1004_, [Linux]: `Process.io_counters()`_ may raise ``ValueError``. -- 1277_, [OSX]: available and used memory (`virtual_memory()`_) metrics are - not accurate. -- 1294_, [Windows]: `Process.connections()`_ may sometimes fail with - intermittent ``0xC0000001``. (patch by Sylvain Duchesne) -- 1307_, [Linux]: `disk_partitions()`_ does not honour `PROCFS_PATH`_. -- 1320_, [AIX]: system CPU times (`cpu_times()`_) were being reported with - ticks unit as opposed to seconds. (patch by Jaime Fullaondo) -- 1332_, [OSX]: psutil debug messages are erroneously printed all the time. - (patch by Ilya Yanok) -- 1346_, [SunOS]: `net_connections()`_ returns an empty list. (patch by Oleksii - Shevchuk) - -5.4.7 -===== - -2018-08-14 - -**Enhancements** - -- 1286_, [macOS]: ``psutil.OSX`` constant is now deprecated in favor of new - ``psutil.MACOS``. -- 1309_, [Linux]: added ``psutil.STATUS_PARKED`` constant for `Process.status()`_. -- 1321_, [Linux]: add `disk_io_counters()`_ dual implementation relying on - ``/sys/block`` filesystem in case ``/proc/diskstats`` is not available. - (patch by Lawrence Ye) - -**Bug fixes** - -- 1209_, [macOS]: `Process.memory_maps()`_ may fail with ``EINVAL`` due to poor - ``task_for_pid()`` syscall. `AccessDenied`_ is now raised instead. -- 1278_, [macOS]: `Process.threads()`_ incorrectly return microseconds instead of - seconds. (patch by Nikhil Marathe) -- 1279_, [Linux], [macOS], [BSD]: `net_if_stats()`_ may return ``ENODEV``. -- 1294_, [Windows]: `Process.connections()`_ may sometime fail with - ``MemoryError``. (patch by sylvainduchesne) -- 1305_, [Linux]: `disk_io_counters()`_ may report inflated r/w bytes values. -- 1309_, [Linux]: `Process.status()`_ is unable to recognize ``"idle"`` and - ``"parked"`` statuses (returns ``"?"``). -- 1313_, [Linux]: `disk_io_counters()`_ can report inflated values due to - counting base disk device and its partition(s) twice. -- 1323_, [Linux]: `sensors_temperatures()`_ may fail with ``ValueError``. - -5.4.6 -===== - -2018-06-07 - -**Bug fixes** - -- 1258_, [Windows], **[critical]**: `Process.username()`_ may cause a segfault - (Python interpreter crash). (patch by Jean-Luc Migot) -- 1273_: `net_if_addrs()`_ namedtuple's name has been renamed from ``snic`` to - ``snicaddr``. -- 1274_, [Linux]: there was a small chance `Process.children()`_ may swallow - `AccessDenied`_ exceptions. - -5.4.5 -===== - -2018-04-14 - -**Bug fixes** - -- 1268_: setup.py's ``extra_require`` parameter requires latest setuptools version, - breaking quite a lot of installations. - -5.4.4 -===== - -2018-04-13 - -**Enhancements** - -- 1239_, [Linux]: expose kernel ``slab`` memory field for `virtual_memory()`_. - (patch by Maxime Mouial) - -**Bug fixes** - -- 694_, [SunOS]: `Process.cmdline()`_ could be truncated at the 15th character when - reading it from ``/proc``. An extra effort is made by reading it from process - address space first. (patch by Georg Sauthoff) -- 771_, [Windows]: `cpu_count()`_ (both logical and cores) return a wrong - (smaller) number on systems using process groups (> 64 cores). -- 771_, [Windows]: `cpu_times()`_ with ``percpu=True`` return fewer CPUs on - systems using process groups (> 64 cores). -- 771_, [Windows]: `cpu_stats()`_ and `cpu_freq()`_ may return incorrect results on - systems using process groups (> 64 cores). -- 1193_, [SunOS]: return uid/gid from ``/proc/pid/psinfo`` if there aren't - enough permissions for ``/proc/pid/cred``. (patch by Georg Sauthoff) -- 1194_, [SunOS]: return nice value from ``psinfo`` as ``getpriority()`` doesn't - support real-time processes. (patch by Georg Sauthoff) -- 1194_, [SunOS]: fix double ``free()`` in `Process.cpu_num()`_. (patch by Georg - Sauthoff) -- 1194_, [SunOS]: fix undefined behavior related to strict-aliasing rules - and warnings. (patch by Georg Sauthoff) -- 1210_, [Linux]: `cpu_percent()`_ steal time may remain stuck at 100% due to Linux - erroneously reporting a decreased steal time between calls. (patch by Arnon - Yaari) -- 1216_: fix compatibility with Python 2.6 on Windows (patch by Dan Vinakovsky) -- 1222_, [Linux]: `Process.memory_full_info()`_ was erroneously summing "Swap:" and - "SwapPss:". Same for "Pss:" and "SwapPss". Not anymore. -- 1224_, [Windows]: `Process.wait()`_ may erroneously raise `TimeoutExpired`_. -- 1238_, [Linux]: `sensors_battery()`_ may return ``None`` in case battery is not - listed as "BAT0" under ``/sys/class/power_supply``. -- 1240_, [Windows]: `cpu_times()`_ float loses accuracy in a long running system. - (patch by stswandering) -- 1245_, [Linux]: `sensors_temperatures()`_ may fail with ``IOError`` "no such file". -- 1255_, [FreeBSD]: `swap_memory()`_ stats were erroneously represented in KB. - (patch by Denis Krienbühl) - -**Backward compatibility** - -- 771_, [Windows]: `cpu_count()`_ with ``logical=False`` on Windows XP and Vista - is no longer supported and returns ``None``. - -5.4.3 -===== - -*2018-01-01* - -**Enhancements** - -- 775_: `disk_partitions()`_ on Windows return mount points. - -**Bug fixes** - -- 1193_: `pids()`_ may return ``False`` on macOS. - -5.4.2 -===== - -*2017-12-07* - -**Enhancements** - -- 1173_: introduced ``PSUTIL_DEBUG`` environment variable which can be set in order - to print useful debug messages on stderr (useful in case of nasty errors). -- 1177_, [macOS]: added support for `sensors_battery()`_. (patch by Arnon Yaari) -- 1183_: `Process.children()`_ is 2x faster on POSIX and 2.4x faster on Linux. -- 1188_: deprecated method `Process.memory_info_ex()`_ now warns by using - ``FutureWarning`` instead of ``DeprecationWarning``. - -**Bug fixes** - -- 1152_, [Windows]: `disk_io_counters()`_ may return an empty dict. -- 1169_, [Linux]: `users()`_ ``hostname`` returns username instead. (patch by - janderbrain) -- 1172_, [Windows]: ``make test`` does not work. -- 1179_, [Linux]: `Process.cmdline()`_ is now able to split cmdline args for - misbehaving processes which overwrite ``/proc/pid/cmdline`` and use spaces - instead of null bytes as args separator. -- 1181_, [macOS]: `Process.memory_maps()`_ may raise ``ENOENT``. -- 1187_, [macOS]: `pids()`_ does not return PID 0 on recent macOS versions. - -5.4.1 -===== - -*2017-11-08* - -**Enhancements** - -- 1164_, [AIX]: add support for `Process.num_ctx_switches()`_. (patch by Arnon - Yaari) -- 1053_: drop Python 3.3 support (psutil still works but it's no longer - tested). - -**Bug fixes** - -- 1150_, [Windows]: when a process is terminated now the exit code is set to - ``SIGTERM`` instead of ``0``. (patch by Akos Kiss) -- 1151_: ``python -m psutil.tests`` fail. -- 1154_, [AIX], **[critical]**: psutil won't compile on AIX 6.1.0. - (patch by Arnon Yaari) -- 1167_, [Windows]: `net_io_counters()`_ packets count now include also non-unicast - packets. (patch by Matthew Long) - -5.4.0 -===== - -*2017-10-12* - -**Enhancements** - -- 1123_, [AIX]: added support for AIX platform. (patch by Arnon Yaari) - -**Bug fixes** - -- 1009_, [Linux]: `sensors_temperatures()`_ may crash with ``IOError``. -- 1012_, [Windows]: `disk_io_counters()`_ ``read_time`` and ``write_time`` - were expressed in tens of micro seconds instead of milliseconds. -- 1127_, [macOS], **[critical]**: invalid reference counting in - `Process.open_files()`_ may lead to segfault. (patch by Jakub Bacic) -- 1129_, [Linux]: `sensors_fans()`_ may crash with ``IOError``. (patch by - Sebastian Saip) -- 1131_, [SunOS]: fix compilation warnings. (patch by Arnon Yaari) -- 1133_, [Windows]: can't compile on newer versions of Visual Studio 2017 15.4. - (patch by Max Bélanger) -- 1138_, [Linux]: can't compile on CentOS 5.0 and RedHat 5.0. (patch by Prodesire) - -5.3.1 -===== - -*2017-09-10* - -**Enhancements** - -- 1124_: documentation moved to http://psutil.readthedocs.io - -**Bug fixes** - -- 1105_, [FreeBSD]: psutil does not compile on FreeBSD 12. -- 1125_, [BSD]: `net_connections()`_ raises ``TypeError``. - -**Compatibility notes** - -- 1120_: ``.exe`` files for Windows are no longer uploaded on PyPI as per - PEP-527. Only wheels are provided. - -5.3.0 -===== - -*2017-09-01* - -**Enhancements** - -- 802_: `disk_io_counters()`_ and `net_io_counters()`_ numbers no longer wrap - (restart from 0). Introduced a new ``nowrap`` argument. -- 928_: `net_connections()`_ and `Process.connections()`_ ``laddr`` and - ``raddr`` are now named tuples. -- 1015_: `swap_memory()`_ now relies on ``/proc/meminfo`` instead of ``sysinfo()`` - syscall so that it can be used in conjunction with `PROCFS_PATH`_ in order to - retrieve memory info about Linux containers such as Docker and Heroku. -- 1022_: `users()`_ provides a new ``pid`` field. -- 1025_: `process_iter()`_ accepts two new parameters in order to invoke - `Process.as_dict()`_: ``attrs`` and ``ad_value``. With these you can iterate - over all processes in one shot without needing to catch `NoSuchProcess`_ and - do list/dict comprehensions. -- 1040_: implemented full unicode support. -- 1051_: `disk_usage()`_ on Python 3 is now able to accept bytes. -- 1058_: test suite now enables all warnings by default. -- 1060_: source distribution is dynamically generated so that it only includes - relevant files. -- 1079_, [FreeBSD]: `net_connections()`_ ``fd`` number is now being set for real - (instead of ``-1``). (patch by Gleb Smirnoff) -- 1091_, [SunOS]: implemented `Process.environ()`_. (patch by Oleksii Shevchuk) - -**Bug fixes** - -- 989_, [Windows]: `boot_time()`_ may return a negative value. -- 1007_, [Windows]: `boot_time()`_ can have a 1 sec fluctuation between calls. - The value of the first call is now cached so that `boot_time()`_ always - returns the same value if fluctuation is <= 1 second. -- 1013_, [FreeBSD]: `net_connections()`_ may return incorrect PID. (patch - by Gleb Smirnoff) -- 1014_, [Linux]: `Process`_ class can mask legitimate ``ENOENT`` exceptions as - `NoSuchProcess`_. -- 1016_: `disk_io_counters()`_ raises ``RuntimeError`` on a system with no disks. -- 1017_: `net_io_counters()`_ raises ``RuntimeError`` on a system with no network - cards installed. -- 1021_, [Linux]: `Process.open_files()`_ may erroneously raise `NoSuchProcess`_ - instead of skipping a file which gets deleted while open files are retrieved. -- 1029_, [macOS], [FreeBSD]: `Process.connections()`_ with ``family=unix`` on Python - 3 doesn't properly handle unicode paths and may raise ``UnicodeDecodeError``. -- 1033_, [macOS], [FreeBSD]: memory leak for `net_connections()`_ and - `Process.connections()`_ when retrieving UNIX sockets (``kind='unix'``). -- 1040_: fixed many unicode related issues such as ``UnicodeDecodeError`` on - Python 3 + POSIX and invalid encoded data on Windows. -- 1042_, [FreeBSD], **[critical]**: psutil won't compile on FreeBSD 12. -- 1044_, [macOS]: different `Process`_ methods incorrectly raise `AccessDenied`_ - for zombie processes. -- 1046_, [Windows]: `disk_partitions()`_ on Windows overrides user's ``SetErrorMode``. -- 1047_, [Windows]: `Process.username()`_: memory leak in case exception is thrown. -- 1048_, [Windows]: `users()`_ ``host`` field report an invalid IP address. -- 1050_, [Windows]: `Process.memory_maps()`_ leaks memory. -- 1055_: `cpu_count()`_ is no longer cached. This is useful on systems such as - Linux where CPUs can be disabled at runtime. This also reflects on - `Process.cpu_percent()`_ which no longer uses the cache. -- 1058_: fixed Python warnings. -- 1062_: `disk_io_counters()`_ and `net_io_counters()`_ raise ``TypeError`` if - no disks or NICs are installed on the system. -- 1063_, [NetBSD]: `net_connections()`_ may list incorrect sockets. -- 1064_, [NetBSD], **[critical]**: `swap_memory()`_ may segfault in case of error. -- 1065_, [OpenBSD], **[critical]**: `Process.cmdline()`_ may raise ``SystemError``. -- 1067_, [NetBSD]: `Process.cmdline()`_ leaks memory if process has terminated. -- 1069_, [FreeBSD]: `Process.cpu_num()`_ may return 255 for certain kernel - processes. -- 1071_, [Linux]: `cpu_freq()`_ may raise ``IOError`` on old RedHat distros. -- 1074_, [FreeBSD]: `sensors_battery()`_ raises ``OSError`` in case of no battery. -- 1075_, [Windows]: `net_if_addrs()`_: ``inet_ntop()`` return value is not checked. -- 1077_, [SunOS]: `net_if_addrs()`_ shows garbage addresses on SunOS 5.10. - (patch by Oleksii Shevchuk) -- 1077_, [SunOS]: `net_connections()`_ does not work on SunOS 5.10. (patch by - Oleksii Shevchuk) -- 1079_, [FreeBSD]: `net_connections()`_ didn't list locally connected sockets. - (patch by Gleb Smirnoff) -- 1085_: `cpu_count()`_ return value is now checked and forced to ``None`` if <= 1. -- 1087_: `Process.cpu_percent()`_ guard against `cpu_count()`_ returning ``None`` - and assumes 1 instead. -- 1093_, [SunOS]: `Process.memory_maps()`_ shows wrong 64 bit addresses. -- 1094_, [Windows]: `pid_exists()`_ may lie. Also, all process APIs relying - on ``OpenProcess`` Windows API now check whether the PID is actually running. -- 1098_, [Windows]: `Process.wait()`_ may erroneously return sooner, when the PID - is still alive. -- 1099_, [Windows]: `Process.terminate()`_ may raise `AccessDenied`_ even if the - process already died. -- 1101_, [Linux]: `sensors_temperatures()`_ may raise ``ENODEV``. - -**Porting notes** - -- 1039_: returned types consolidation. 1) Windows / `Process.cpu_times()`_: - fields #3 and #4 were int instead of float. 2) Linux / FreeBSD / OpenBSD: - `Process.connections()`_ ``raddr`` is now set to ``""`` instead of ``None`` - when retrieving UNIX sockets. -- 1040_: all strings are encoded by using OS fs encoding. -- 1040_: the following Windows APIs on Python 2 now return a string instead of - unicode: ``Process.memory_maps().path``, ``WindowsService.bin_path()``, - ``WindowsService.description()``, ``WindowsService.display_name()``, - ``WindowsService.username()``. - -5.2.2 -===== - -*2017-04-10* - -**Bug fixes** - -- 1000_: fixed some setup.py warnings. -- 1002_, [SunOS]: remove C macro which will not be available on new Solaris - versions. (patch by Danek Duvall) -- 1004_, [Linux]: `Process.io_counters()`_ may raise ``ValueError``. -- 1006_, [Linux]: `cpu_freq()`_ may return ``None`` on some Linux versions does not - support the function. Let's not make the function available instead. -- 1009_, [Linux]: `sensors_temperatures()`_ may raise ``OSError``. -- 1010_, [Linux]: `virtual_memory()`_ may raise ``ValueError`` on Ubuntu 14.04. - -5.2.1 -===== - -*2017-03-24* - -**Bug fixes** - -- 981_, [Linux]: `cpu_freq()`_ may return an empty list. -- 993_, [Windows]: `Process.memory_maps()`_ on Python 3 may raise - ``UnicodeDecodeError``. -- 996_, [Linux]: `sensors_temperatures()`_ may not show all temperatures. -- 997_, [FreeBSD]: `virtual_memory()`_ may fail due to missing ``sysctl`` - parameter on FreeBSD 12. - -5.2.0 -===== - -*2017-03-05* - -**Enhancements** - -- 971_, [Linux]: Add `sensors_fans()`_ function. (patch by Nicolas Hennion) -- 976_, [Windows]: `Process.io_counters()`_ has 2 new fields: ``other_count`` and - ``other_bytes``. -- 976_, [Linux]: `Process.io_counters()`_ has 2 new fields: ``read_chars`` and - ``write_chars``. - -**Bug fixes** - -- 872_, [Linux]: can now compile on Linux by using MUSL C library. -- 985_, [Windows]: Fix a crash in `Process.open_files()`_ when the worker thread - for ``NtQueryObject`` times out. -- 986_, [Linux]: `Process.cwd()`_ may raise `NoSuchProcess`_ instead of `ZombieProcess`_. - -5.1.3 -===== - -**Bug fixes** - -- 971_, [Linux]: `sensors_temperatures()`_ didn't work on CentOS 7. -- 973_, **[critical]**: `cpu_percent()`_ may raise ``ZeroDivisionError``. - -5.1.2 -===== - -*2017-02-03* - -**Bug fixes** - -- 966_, [Linux]: `sensors_battery()`_ ``power_plugged`` may erroneously return - ``None`` on Python 3. -- 968_, [Linux]: `disk_io_counters()`_ raises ``TypeError`` on Python 3. -- 970_, [Linux]: `sensors_battery()`_ ``name`` and ``label`` fields on Python 3 - are bytes instead of str. - -5.1.1 -===== - -*2017-02-03* - -**Enhancements** - -- 966_, [Linux]: `sensors_battery()`_ ``percent`` is a float and is more precise. - -**Bug fixes** - -- 964_, [Windows]: `Process.username()`_ and `users()`_ may return badly - decoded character on Python 3. -- 965_, [Linux]: `disk_io_counters()`_ may miscalculate sector size and report - the wrong number of bytes read and written. -- 966_, [Linux]: `sensors_battery()`_ may fail with ``FileNotFoundError``. -- 966_, [Linux]: `sensors_battery()`_ ``power_plugged`` may lie. - -5.1.0 -===== - -*2017-02-01* - -**Enhancements** - -- 357_: added `Process.cpu_num()`_ (what CPU a process is on). -- 371_: added `sensors_temperatures()`_ (Linux only). -- 941_: added `cpu_freq()`_ (CPU frequency). -- 955_: added `sensors_battery()`_ (Linux, Windows, only). -- 956_: `Process.cpu_affinity()`_ can now be passed ``[]`` argument as an - alias to set affinity against all eligible CPUs. - -**Bug fixes** - -- 687_, [Linux]: `pid_exists()`_ no longer returns ``True`` if passed a process - thread ID. -- 948_: cannot install psutil with ``PYTHONOPTIMIZE=2``. -- 950_, [Windows]: `Process.cpu_percent()`_ was calculated incorrectly and showed - higher number than real usage. -- 951_, [Windows]: the uploaded wheels for Python 3.6 64 bit didn't work. -- 959_: psutil exception objects could not be pickled. -- 960_: `psutil.Popen`_ ``wait()`` did not return the correct negative exit - status if process is killed by a signal. -- 961_, [Windows]: ``WindowsService.description()`` method may fail with - ``ERROR_MUI_FILE_NOT_FOUND``. - -5.0.1 -===== - -*2016-12-21* - -**Enhancements** - -- 939_: tar.gz distribution went from 1.8M to 258K. -- 811_, [Windows]: provide a more meaningful error message if trying to use - psutil on unsupported Windows XP. - -**Bug fixes** - -- 609_, [SunOS], **[critical]**: psutil does not compile on Solaris 10. -- 936_, [Windows]: fix compilation error on VS 2013 (patch by Max Bélanger). -- 940_, [Linux]: `cpu_percent()`_ and `cpu_times_percent()`_ was calculated - incorrectly as ``iowait``, ``guest`` and ``guest_nice`` times were not - properly taken into account. -- 944_, [OpenBSD]: `pids()`_ was omitting PID 0. - -5.0.0 -===== - -*2016-11-06* - -**Enhncements** - -- 799_: new `Process.oneshot()`_ context manager making `Process`_ methods around - +2x faster in general and from +2x to +6x faster on Windows. -- 943_: better error message in case of version conflict on import. - -**Bug fixes** - -- 932_, [NetBSD]: `net_connections()`_ and `Process.connections()`_ may fail - without raising an exception. -- 933_, [Windows]: memory leak in `cpu_stats()`_ and - ``WindowsService.description()`` method. - -4.4.2 -===== - -*2016-10-26* - -**Bug fixes** - -- 931_, **[critical]**: psutil no longer compiles on Solaris. - -4.4.1 -===== - -*2016-10-25* - -**Bug fixes** - -- 927_, **[critical]**: `psutil.Popen`_ ``__del__`` may cause maximum recursion - depth error. - -4.4.0 -===== - -*2016-10-23* - -**Enhancements** - -- 874_, [Windows]: make `net_if_addrs()`_ also return the ``netmask``. -- 887_, [Linux]: `virtual_memory()`_ ``available`` and ``used`` values are more - precise and match ``free`` cmdline utility. ``available`` also takes into - account LCX containers preventing ``available`` to overflow ``total``. -- 891_: `procinfo.py`_ script has been updated and provides a lot more info. - -**Bug fixes** - -- 514_, [macOS], **[critical]**: `Process.memory_maps()`_ can segfault. -- 783_, [macOS]: `Process.status()`_ may erroneously return ``"running"`` for - zombie processes. -- 798_, [Windows]: `Process.open_files()`_ returns and empty list on Windows 10. -- 825_, [Linux]: `Process.cpu_affinity()`_: fix possible double close and use of - unopened socket. -- 880_, [Windows]: fix race condition inside `net_connections()`_. -- 885_: ``ValueError`` is raised if a negative integer is passed to `cpu_percent()`_ - functions. -- 892_, [Linux], **[critical]**: `Process.cpu_affinity()`_ with ``[-1]`` as arg - raises ``SystemError`` with no error set; now ``ValueError`` is raised. -- 906_, [BSD]: `disk_partitions()`_ with ``all=False`` returned an empty list. - Now the argument is ignored and all partitions are always returned. -- 907_, [FreeBSD]: `Process.exe()`_ may fail with ``OSError(ENOENT)``. -- 908_, [macOS], [BSD]: different process methods could errounesuly mask the real - error for high-privileged PIDs and raise `NoSuchProcess`_ and `AccessDenied`_ - instead of ``OSError`` and ``RuntimeError``. -- 909_, [macOS]: `Process.open_files()`_ and `Process.connections()`_ methods - may raise ``OSError`` with no exception set if process is gone. -- 916_, [macOS]: fix many compilation warnings. - -4.3.1 -===== - -*2016-09-01* - -**Enhancements** - -- 881_: ``make install`` now works also when using a virtual env. - -**Bug fixes** - -- 854_: `Process.as_dict()`_ raises ``ValueError`` if passed an erroneous attrs name. -- 857_, [SunOS]: `Process.cpu_times()`_, `Process.cpu_percent()`_, - `Process.threads()`_ and `Process.memory_maps()`_ may raise ``RuntimeError`` if - attempting to query a 64bit process with a 32bit Python. "Null" values are - returned as a fallback. -- 858_: `Process.as_dict()`_ should not call `Process.memory_info_ex()`_ - because it's deprecated. -- 863_, [Windows]: `Process.memory_maps()`_ truncates addresses above 32 bits. -- 866_, [Windows]: `win_service_iter()`_ and services in general are not able to - handle unicode service names / descriptions. -- 869_, [Windows]: `Process.wait()`_ may raise `TimeoutExpired`_ with wrong timeout - unit (ms instead of sec). -- 870_, [Windows]: handle leak inside ``psutil_get_process_data``. - -4.3.0 -===== - -*2016-06-18* - -**Enhancements** - -- 819_, [Linux]: different speedup improvements: - `Process.ppid()`_ +20% faster. - `Process.status()`_ +28% faster. - `Process.name()`_ +25% faster. - `Process.num_threads()`_ +20% faster on Python 3. - -**Bug fixes** - -- 810_, [Windows]: Windows wheels are incompatible with pip 7.1.2. -- 812_, [NetBSD], **[critical]**: fix compilation on NetBSD-5.x. -- 823_, [NetBSD]: `virtual_memory()`_ raises ``TypeError`` on Python 3. -- 829_, [POSIX]: `disk_usage()`_ ``percent`` field takes root reserved space - into account. -- 816_, [Windows]: fixed `net_io_counters()`_ values wrapping after 4.3GB in - Windows Vista (NT 6.0) and above using 64bit values from newer win APIs. - -4.2.0 -===== - -*2016-05-14* - -**Enhancements** - -- 795_, [Windows]: new APIs to deal with Windows services: `win_service_iter()`_ - and `win_service_get()`_. -- 800_, [Linux]: `virtual_memory()`_ returns a new ``shared`` memory field. -- 819_, [Linux]: speedup ``/proc`` parsing: - `Process.ppid()`_ +20% faster. - `Process.status()`_ +28% faster. - `Process.name()`_ +25% faster. - `Process.num_threads()`_ +20% faster on Python 3. - -**Bug fixes** - -- 797_, [Linux]: `net_if_stats()`_ may raise ``OSError`` for certain NIC cards. -- 813_: `Process.as_dict()`_ should ignore extraneous attribute names which gets - attached to the `Process`_ instance. - -4.1.0 -===== - -*2016-03-12* - -**Enhancements** - -- 777_, [Linux]: `Process.open_files()`_ on Linux return 3 new fields: - ``position``, ``mode`` and ``flags``. -- 779_: `Process.cpu_times()`_ returns two new fields, ``children_user`` and - ``children_system`` (always set to 0 on macOS and Windows). -- 789_, [Windows]: `cpu_times()`_ return two new fields: ``interrupt`` and - ``dpc``. Same for `cpu_times_percent()`_. -- 792_: new `cpu_stats()`_ function returning number of CPU ``ctx_switches``, - ``interrupts``, ``soft_interrupts`` and ``syscalls``. - -**Bug fixes** - -- 774_, [FreeBSD]: `net_io_counters()`_ dropout is no longer set to 0 if the kernel - provides it. -- 776_, [Linux]: `Process.cpu_affinity()`_ may erroneously raise `NoSuchProcess`_. - (patch by wxwright) -- 780_, [macOS]: psutil does not compile with some GCC versions. -- 786_: `net_if_addrs()`_ may report incomplete MAC addresses. -- 788_, [NetBSD]: `virtual_memory()`_ ``buffers`` and ``shared`` values were - set to 0. -- 790_, [macOS], **[critical]**: psutil won't compile on macOS 10.4. - -4.0.0 -===== - -*2016-02-17* - -**Enhancements** - -- 523_, [Linux], [FreeBSD]: `disk_io_counters()`_ return a new ``busy_time`` field. -- 660_, [Windows]: make.bat is smarter in finding alternative VS install - locations. (patch by mpderbec) -- 732_: `Process.environ()`_. (patch by Frank Benkstein) -- 753_, [Linux], [macOS], [Windows]: process USS and PSS (Linux) "real" memory - stats. (patch by Eric Rahm) -- 755_: `Process.memory_percent()`_ ``memtype`` parameter. -- 758_: tests now live in psutil namespace. -- 760_: expose OS constants (``psutil.LINUX``, ``psutil.OSX``, etc.) -- 756_, [Linux]: `disk_io_counters()`_ return 2 new fields: ``read_merged_count`` - and ``write_merged_count``. -- 762_: new `procsmem.py`_ script. - -**Bug fixes** - -- 685_, [Linux]: `virtual_memory()`_ provides wrong results on systems with a lot - of physical memory. -- 704_, [SunOS]: psutil does not compile on Solaris sparc. -- 734_: on Python 3 invalid UTF-8 data is not correctly handled for - `Process.name()`_, `Process.cwd()`_, `Process.exe()`_, `Process.cmdline()`_ - and `Process.open_files()`_ methods resulting in ``UnicodeDecodeError`` - exceptions. ``'surrogateescape'`` error handler is now used as a workaround for - replacing the corrupted data. -- 737_, [Windows]: when the bitness of psutil and the target process was - different, `Process.cmdline()`_ and `Process.cwd()`_ could return a wrong - result or incorrectly report an `AccessDenied`_ error. -- 741_, [OpenBSD]: psutil does not compile on mips64. -- 751_, [Linux]: fixed call to ``Py_DECREF`` on possible ``NULL`` object. -- 754_, [Linux]: `Process.cmdline()`_ can be wrong in case of zombie process. -- 759_, [Linux]: `Process.memory_maps()`_ may return paths ending with ``" (deleted)"``. -- 761_, [Windows]: `boot_time()`_ wraps to 0 after 49 days. -- 764_, [NetBSD]: fix compilation on NetBSD-6.x. -- 766_, [Linux]: `net_connections()`_ can't handle malformed ``/proc/net/unix`` - file. -- 767_, [Linux]: `disk_io_counters()`_ may raise ``ValueError`` on 2.6 kernels and it's - broken on 2.4 kernels. -- 770_, [NetBSD]: `disk_io_counters()`_ metrics didn't update. - -3.4.2 -===== - -*2016-01-20* - -**Enhancements** - -- 728_, [SunOS]: exposed `PROCFS_PATH`_ constant to change the default - location of ``/proc`` filesystem. - -**Bug fixes** - -- 724_, [FreeBSD]: `virtual_memory()`_ ``total`` is incorrect. -- 730_, [FreeBSD], **[critical]**: `virtual_memory()`_ crashes with - "OSError: [Errno 12] Cannot allocate memory". - -3.4.1 -===== - -*2016-01-15* - -**Enhancements** - -- 557_, [NetBSD]: added NetBSD support. (contributed by Ryo Onodera and - Thomas Klausner) -- 708_, [Linux]: `net_connections()`_ and `Process.connections()`_ on Python 2 - can be up to 3x faster in case of many connections. - Also `Process.memory_maps()`_ is slightly faster. -- 718_: `process_iter()`_ is now thread safe. - -**Bug fixes** - -- 714_, [OpenBSD]: `virtual_memory()`_ ``cached`` value was always set to 0. -- 715_, **[critical]**: don't crash at import time if `cpu_times()`_ fail for - some reason. -- 717_, [Linux]: `Process.open_files()`_ fails if deleted files still visible. -- 722_, [Linux]: `swap_memory()`_ no longer crashes if ``sin`` / ``sout`` can't - be determined due to missing ``/proc/vmstat``. -- 724_, [FreeBSD]: `virtual_memory()`_ ``total`` is slightly incorrect. - -3.3.0 -===== - -*2015-11-25* - -**Enhancements** - -- 558_, [Linux]: exposed `PROCFS_PATH`_ constant to change the default - location of ``/proc`` filesystem. -- 615_, [OpenBSD]: added OpenBSD support. (contributed by Landry Breuil) - -**Bug fixes** - -- 692_, [POSIX]: `Process.name()`_ is no longer cached as it may change. - -3.2.2 -===== - -*2015-10-04* - -**Bug fixes** - -- 517_, [SunOS]: `net_io_counters()`_ failed to detect network interfaces - correctly on Solaris 10 -- 541_, [FreeBSD]: `disk_io_counters()`_ r/w times were expressed in seconds instead - of milliseconds. (patch by dasumin) -- 610_, [SunOS]: fix build and tests on Solaris 10 -- 623_, [Linux]: process or system connections raises ``ValueError`` if IPv6 is not - supported by the system. -- 678_, [Linux], **[critical]**: can't install psutil due to bug in setup.py. -- 688_, [Windows]: compilation fails with MSVC 2015, Python 3.5. (patch by - Mike Sarahan) - -3.2.1 -===== - -*2015-09-03* - -**Bug fixes** - -- 677_, [Linux], **[critical]**: can't install psutil due to bug in setup.py. - -3.2.0 -===== - -*2015-09-02* - -**Enhancements** - -- 644_, [Windows]: added support for ``CTRL_C_EVENT`` and ``CTRL_BREAK_EVENT`` - signals to use with `Process.send_signal()`_. -- 648_: CI test integration for macOS. (patch by Jeff Tang) -- 663_, [POSIX]: `net_if_addrs()`_ now returns point-to-point (VPNs) addresses. -- 655_, [Windows]: different issues regarding unicode handling were fixed. On - Python 2 all APIs returning a string will now return an encoded version of it - by using sys.getfilesystemencoding() codec. The APIs involved are: - `net_if_addrs()`_, `net_if_stats()`_, `net_io_counters()`_, - `Process.cmdline()`_, `Process.name()`_, `Process.username()`_, `users()`_. - -**Bug fixes** - -- 513_, [Linux]: fixed integer overflow for ``RLIM_INFINITY``. -- 641_, [Windows]: fixed many compilation warnings. (patch by Jeff Tang) -- 652_, [Windows]: `net_if_addrs()`_ ``UnicodeDecodeError`` in case of non-ASCII NIC - names. -- 655_, [Windows]: `net_if_stats()`_ ``UnicodeDecodeError`` in case of non-ASCII NIC - names. -- 659_, [Linux]: compilation error on Suse 10. (patch by maozguttman) -- 664_, [Linux]: compilation error on Alpine Linux. (patch by Bart van Kleef) -- 670_, [Windows]: segfgault of `net_if_addrs()`_ in case of non-ASCII NIC names. - (patch by sk6249) -- 672_, [Windows]: compilation fails if using Windows SDK v8.0. (patch by - Steven Winfield) -- 675_, [Linux]: `net_connections()`_: ``UnicodeDecodeError`` may occur when - listing UNIX sockets. - -3.1.1 -===== - -*2015-07-15* - -**Bug fixes** - -- 603_, [Linux]: `Process.ionice()`_ set value range is incorrect. - (patch by spacewander) -- 645_, [Linux]: `cpu_times_percent()`_ may produce negative results. -- 656_: ``from psutil import *`` does not work. - -3.1.0 -===== - -*2015-07-15* - -**Enhancements** - -- 534_, [Linux]: `disk_partitions()`_ added support for ZFS filesystems. -- 646_, [Windows]: continuous tests integration for Windows with - https://ci.appveyor.com/project/giampaolo/psutil. -- 647_: new dev guide: - https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst -- 651_: continuous code quality test integration with scrutinizer-ci.com - -**Bug fixes** - -- 340_, [Windows], **[critical]**: `Process.open_files()`_ no longer hangs. - Instead it uses a thread which times out and skips the file handle in case it's - taking too long to be retrieved. (patch by Jeff Tang) -- 627_, [Windows]: `Process.name()`_ no longer raises `AccessDenied`_ for pids - owned by another user. -- 636_, [Windows]: `Process.memory_info()`_ raise `AccessDenied`_. -- 637_, [POSIX]: raise exception if trying to send signal to PID 0 as it will - affect ``os.getpid()`` 's process group and not PID 0. -- 639_, [Linux]: `Process.cmdline()`_ can be truncated. -- 640_, [Linux]: ``*connections`` functions may swallow errors and return an - incomplete list of connections. -- 642_: ``repr()`` of exceptions is incorrect. -- 653_, [Windows]: add ``inet_ntop()`` function for Windows XP to support IPv6. -- 641_, [Windows]: replace deprecated string functions with safe equivalents. - -3.0.1 -===== - -*2015-06-18* - -**Bug fixes** - -- 632_, [Linux]: better error message if cannot parse process UNIX connections. -- 634_, [Linux]: `Process.cmdline()`_ does not include empty string arguments. -- 635_, [POSIX], **[critical]**: crash on module import if ``enum`` package is - installed on Python < 3.4. - -3.0.0 -===== - -*2015-06-13* - -**Enhancements** - -- 250_: new `net_if_stats()`_ returning NIC statistics (``isup``, ``duplex``, - ``speed``, ``mtu``). -- 376_: new `net_if_addrs()`_ returning all NIC addresses a-la ``ifconfig``. -- 469_: on Python >= 3.4 ``IOPRIO_CLASS_*`` and ``*_PRIORITY_CLASS`` constants - returned by `Process.ionice()`_ and `Process.nice()`_ are enums instead of - plain integers. -- 581_: add ``.gitignore``. (patch by Gabi Davar) -- 582_: connection constants returned by `net_connections()`_ and - `Process.connections()`_ were turned from int to enums on Python > 3.4. -- 587_: move native extension into the package. -- 589_: `Process.cpu_affinity()`_ accepts any kind of iterable (set, tuple, ...), - not only lists. -- 594_: all deprecated APIs were removed. -- 599_, [Windows]: `Process.name()`_ can now be determined for all processes even - when running as a limited user. -- 602_: pre-commit GIT hook. -- 629_: enhanced support for ``pytest`` and ``nose`` test runners. -- 616_, [Windows]: add ``inet_ntop()`` function for Windows XP. - -**Bug fixes** - -- 428_, [POSIX], **[critical]**: correct handling of zombie processes on POSIX. - Introduced new `ZombieProcess`_ exception class. -- 512_, [BSD], **[critical]**: fix segfault in `net_connections()`_. -- 555_, [Linux]: `users()`_ correctly handles ``":0"`` as an alias for - ``"localhost"``. -- 579_, [Windows]: fixed `Process.open_files()`_ for PID > 64K. -- 579_, [Windows]: fixed many compiler warnings. -- 585_, [FreeBSD]: `net_connections()`_ may raise ``KeyError``. -- 586_, [FreeBSD], **[critical]**: `Process.cpu_affinity()`_ segfaults on set - in case an invalid CPU number is provided. -- 593_, [FreeBSD], **[critical]**: `Process.memory_maps()`_ segfaults. -- 606_: `Process.parent()`_ may swallow `NoSuchProcess`_ exceptions. -- 611_, [SunOS]: `net_io_counters()`_ has send and received swapped -- 614_, [Linux]:: `cpu_count()`_ with ``logical=False`` return the number of - sockets instead of cores. -- 618_, [SunOS]: swap tests fail on Solaris when run as normal user. -- 628_, [Linux]: `Process.name()`_ truncates string in case it contains spaces - or parentheses. - -2.2.1 -===== - -*2015-02-02* - -**Bug fixes** - -- 572_, [Linux]: fix "ValueError: ambiguous inode with multiple PIDs references" - for `Process.connections()`_. (patch by Bruno Binet) - -2.2.0 -===== - -*2015-01-06* - -**Enhancements** - -- 521_: drop support for Python 2.4 and 2.5. -- 553_: new `pstree.py`_ script. -- 564_: C extension version mismatch in case the user messed up with psutil - installation or with sys.path is now detected at import time. -- 568_: new `pidof.py`_ script. -- 569_, [FreeBSD]: add support for `Process.cpu_affinity()`_ on FreeBSD. - -**Bug fixes** - -- 496_, [SunOS], **[critical]**: can't import psutil. -- 547_, [POSIX]: `Process.username()`_ may raise ``KeyError`` if UID can't be resolved. -- 551_, [Windows]: get rid of the unicode hack for `net_io_counters()`_ NIC names. -- 556_, [Linux]: lots of file handles were left open. -- 561_, [Linux]: `net_connections()`_ might skip some legitimate UNIX sockets. - (patch by spacewander) -- 565_, [Windows]: use proper encoding for `Process.username()`_ and `users()`_. - (patch by Sylvain Mouquet) -- 567_, [Linux]: in the alternative implementation of `Process.cpu_affinity()`_ - ``PyList_Append`` and ``Py_BuildValue`` return values are not checked. -- 569_, [FreeBSD]: fix memory leak in `cpu_count()`_ with ``logical=False``. -- 571_, [Linux]: `Process.open_files()`_ might swallow `AccessDenied`_ - exceptions and return an incomplete list of open files. - -2.1.3 -===== - -*2014-09-26* - -- 536_, [Linux], **[critical]**: fix "undefined symbol: CPU_ALLOC" compilation - error. - -2.1.2 -===== - -*2014-09-21* - -**Enhancements** - -- 407_: project moved from Google Code to Github; code moved from Mercurial - to Git. -- 492_: use ``tox`` to run tests on multiple Python versions. (patch by msabramo) -- 505_, [Windows]: distribution as wheel packages. -- 511_: add `ps.py`_ script. - -**Bug fixes** - -- 340_, [Windows]: `Process.open_files()`_ no longer hangs. (patch by - Jeff Tang) -- 501_, [Windows]: `disk_io_counters()`_ may return negative values. -- 503_, [Linux]: in rare conditions `Process.exe()`_, `Process.open_files()`_ and - `Process.connections()`_ can raise ``OSError(ESRCH)`` instead of `NoSuchProcess`_. -- 504_, [Linux]: can't build RPM packages via setup.py -- 506_, [Linux], **[critical]**: Python 2.4 support was broken. -- 522_, [Linux]: `Process.cpu_affinity()`_ might return ``EINVAL``. (patch by David - Daeschler) -- 529_, [Windows]: `Process.exe()`_ may raise unhandled ``WindowsError`` exception - for PIDs 0 and 4. (patch by Jeff Tang) -- 530_, [Linux]: `disk_io_counters()`_ may crash on old Linux distros - (< 2.6.5) (patch by Yaolong Huang) -- 533_, [Linux]: `Process.memory_maps()`_ may raise ``TypeError`` on old Linux - distros. - -2.1.1 -===== - -*2014-04-30* - -**Bug fixes** - -- 446_, [Windows]: fix encoding error when using `net_io_counters()`_ on Python 3. - (patch by Szigeti Gabor Niif) -- 460_, [Windows]: `net_io_counters()`_ wraps after 4G. -- 491_, [Linux]: `net_connections()`_ exceptions. (patch by Alexander Grothe) - -2.1.0 -===== - -*2014-04-08* - -**Enhancements** - -- 387_: system-wide open connections a-la ``netstat`` (add `net_connections()`_). - -**Bug fixes** - -- 421_, [SunOS], **[critical]**: psutil does not compile on SunOS 5.10. - (patch by Naveed Roudsari) -- 489_, [Linux]: `disk_partitions()`_ return an empty list. - -2.0.0 -===== - -*2014-03-10* - -**Enhancements** - -- 424_, [Windows]: installer for Python 3.X 64 bit. -- 427_: number of logical CPUs and physical cores (`cpu_count()`_). -- 447_: `wait_procs()`_ ``timeout`` parameter is now optional. -- 452_: make `Process`_ instances hashable and usable with ``set()`` s. -- 453_: tests on Python < 2.7 require ``unittest2`` module. -- 459_: add a Makefile for running tests and other repetitive tasks (also - on Windows). -- 463_: make timeout parameter of ``cpu_percent*`` functions default to ``0.0`` - 'cause it's a common trap to introduce slowdowns. -- 468_: move documentation to readthedocs.com. -- 477_: `Process.cpu_percent()`_ is about 30% faster. (suggested by crusaderky) -- 478_, [Linux]: almost all APIs are about 30% faster on Python 3.X. -- 479_: long deprecated ``psutil.error`` module is gone; exception classes now - live in psutil namespace only. - -**Bug fixes** - -- 193_: `psutil.Popen`_ constructor can throw an exception if the spawned process - terminates quickly. -- 340_, [Windows]: `Process.open_files()`_ no longer hangs. (patch by - jtang@vahna.net) -- 443_, [Linux]: fix a potential overflow issue for `Process.cpu_affinity()`_ - (set) on systems with more than 64 CPUs. -- 448_, [Windows]: `Process.children()`_ and `Process.ppid()`_ memory leak (patch - by Ulrich Klank). -- 457_, [POSIX]: `pid_exists()`_ always returns ``True`` for PID 0. -- 461_: namedtuples are not pickle-able. -- 466_, [Linux]: `Process.exe()`_ improper null bytes handling. (patch by - Gautam Singh) -- 470_: `wait_procs()`_ might not wait. (patch by crusaderky) -- 471_, [Windows]: `Process.exe()`_ improper unicode handling. (patch by - alex@mroja.net) -- 473_: `psutil.Popen`_ ``wait()`` method does not set returncode attribute. -- 474_, [Windows]: `Process.cpu_percent()`_ is no longer capped at 100%. -- 476_, [Linux]: encoding error for `Process.name()`_ and `Process.cmdline()`_. - -**API changes** - -For the sake of consistency a lot of psutil APIs have been renamed. -In most cases accessing the old names will work but it will cause a -``DeprecationWarning``. - -- ``psutil.*`` module level constants have being replaced by functions: - - +-----------------------+----------------------------------+ - | Old name | Replacement | - +=======================+==================================+ - | psutil.NUM_CPUS | psutil.cpu_count() | - +-----------------------+----------------------------------+ - | psutil.BOOT_TIME | psutil.boot_time() | - +-----------------------+----------------------------------+ - | psutil.TOTAL_PHYMEM | virtual_memory.total | - +-----------------------+----------------------------------+ - -- Renamed ``psutil.*`` functions: - - +------------------------+-------------------------------+ - | Old name | Replacement | - +========================+===============================+ - | psutil.get_pid_list() | psutil.pids() | - +------------------------+-------------------------------+ - | psutil.get_users() | psutil.users() | - +------------------------+-------------------------------+ - | psutil.get_boot_time() | psutil.boot_time() | - +------------------------+-------------------------------+ - -- All `Process`_ ``get_*`` methods lost the ``get_`` prefix. - E.g. ``get_ext_memory_info()`` was renamed to ``memory_info_ex()``. - Assuming ``p = psutil.Process()``: - - +--------------------------+----------------------+ - | Old name | Replacement | - +==========================+======================+ - | p.get_children() | p.children() | - +--------------------------+----------------------+ - | p.get_connections() | p.connections() | - +--------------------------+----------------------+ - | p.get_cpu_affinity() | p.cpu_affinity() | - +--------------------------+----------------------+ - | p.get_cpu_percent() | p.cpu_percent() | - +--------------------------+----------------------+ - | p.get_cpu_times() | p.cpu_times() | - +--------------------------+----------------------+ - | p.get_ext_memory_info() | p.memory_info_ex() | - +--------------------------+----------------------+ - | p.get_io_counters() | p.io_counters() | - +--------------------------+----------------------+ - | p.get_ionice() | p.ionice() | - +--------------------------+----------------------+ - | p.get_memory_info() | p.memory_info() | - +--------------------------+----------------------+ - | p.get_memory_maps() | p.memory_maps() | - +--------------------------+----------------------+ - | p.get_memory_percent() | p.memory_percent() | - +--------------------------+----------------------+ - | p.get_nice() | p.nice() | - +--------------------------+----------------------+ - | p.get_num_ctx_switches() | p.num_ctx_switches() | - +--------------------------+----------------------+ - | p.get_num_fds() | p.num_fds() | - +--------------------------+----------------------+ - | p.get_num_threads() | p.num_threads() | - +--------------------------+----------------------+ - | p.get_open_files() | p.open_files() | - +--------------------------+----------------------+ - | p.get_rlimit() | p.rlimit() | - +--------------------------+----------------------+ - | p.get_threads() | p.threads() | - +--------------------------+----------------------+ - | p.getcwd() | p.cwd() | - +--------------------------+----------------------+ - -- All `Process`_ ``set_*`` methods lost the ``set_`` prefix. - Assuming ``p = psutil.Process()``: - - +----------------------+---------------------------------+ - | Old name | Replacement | - +======================+=================================+ - | p.set_nice() | p.nice(value) | - +----------------------+---------------------------------+ - | p.set_ionice() | p.ionice(ioclass, value=None) | - +----------------------+---------------------------------+ - | p.set_cpu_affinity() | p.cpu_affinity(cpus) | - +----------------------+---------------------------------+ - | p.set_rlimit() | p.rlimit(resource, limits=None) | - +----------------------+---------------------------------+ - -- Except for ``pid``, all `Process`_ class properties have been turned into - methods. This is the only case which there are no aliases. - Assuming ``p = psutil.Process()``: - - +---------------+-----------------+ - | Old name | Replacement | - +===============+=================+ - | p.name | p.name() | - +---------------+-----------------+ - | p.parent | p.parent() | - +---------------+-----------------+ - | p.ppid | p.ppid() | - +---------------+-----------------+ - | p.exe | p.exe() | - +---------------+-----------------+ - | p.cmdline | p.cmdline() | - +---------------+-----------------+ - | p.status | p.status() | - +---------------+-----------------+ - | p.uids | p.uids() | - +---------------+-----------------+ - | p.gids | p.gids() | - +---------------+-----------------+ - | p.username | p.username() | - +---------------+-----------------+ - | p.create_time | p.create_time() | - +---------------+-----------------+ - -- timeout parameter of ``cpu_percent*`` functions defaults to 0.0 instead of 0.1. -- long deprecated ``psutil.error`` module is gone; exception classes now live in - "psutil" namespace only. -- `Process`_ instances' ``retcode`` attribute returned by `wait_procs()`_ has - been renamed to ``returncode`` for consistency with ``subprocess.Popen``. - -1.2.1 -===== - -*2013-11-25* - -**Bug fixes** - -- 348_, [Windows], **[critical]**: fixed "ImportError: DLL load failed" occurring - on module import on Windows XP. -- 425_, [SunOS], **[critical]**: crash on import due to failure at determining - ``BOOT_TIME``. -- 443_, [Linux]: `Process.cpu_affinity()`_ can't set affinity on systems with - more than 64 cores. - -1.2.0 -===== - -*2013-11-20* - -**Enhancements** - -- 439_: assume ``os.getpid()`` if no argument is passed to `Process`_ class - constructor. -- 440_: new `wait_procs()`_ utility function which waits for multiple - processes to terminate. - -**Bug fixes** - -- 348_, [Windows]: fix "ImportError: DLL load failed" occurring on module - import on Windows XP / Vista. - -1.1.3 -===== - -*2013-11-07* - -**Bug fixes** - -- 442_, [Linux], **[critical]**: psutil won't compile on certain version of - Linux because of missing ``prlimit(2)`` syscall. - -1.1.2 -===== - -*2013-10-22* - -**Bug fixes** - -- 442_, [Linux], **[critical]**: psutil won't compile on Debian 6.0 because of - missing ``prlimit(2)`` syscall. - -1.1.1 -===== - -*2013-10-08* - -**Bug fixes** - -- 442_, [Linux], **[critical]**: psutil won't compile on kernels < 2.6.36 due - to missing ``prlimit(2)`` syscall. - -1.1.0 -===== - -*2013-09-28* - -**Enhancements** - -- 410_: host tar.gz and Windows binary files are on PyPI. -- 412_, [Linux]: get/set process resource limits (`Process.rlimit()`_). -- 415_, [Windows]: `Process.children()`_ is an order of magnitude faster. -- 426_, [Windows]: `Process.name()`_ is an order of magnitude faster. -- 431_, [POSIX]: `Process.name()`_ is slightly faster because it unnecessarily - retrieved also `Process.cmdline()`_. - -**Bug fixes** - -- 391_, [Windows]: `cpu_times_percent()`_ returns negative percentages. -- 408_: ``STATUS_*`` and ``CONN_*`` constants don't properly serialize on JSON. -- 411_, [Windows]: `disk_usage.py`_ may pop-up a GUI error. -- 413_, [Windows]: `Process.memory_info()`_ leaks memory. -- 414_, [Windows]: `Process.exe()`_ on Windows XP may raise ``ERROR_INVALID_PARAMETER``. -- 416_: `disk_usage()`_ doesn't work well with unicode path names. -- 430_, [Linux]: `Process.io_counters()`_ report wrong number of r/w syscalls. -- 435_, [Linux]: `net_io_counters()`_ might report erreneous NIC names. -- 436_, [Linux]: `net_io_counters()`_ reports a wrong ``dropin`` value. - -**API changes** - -- 408_: turn ``STATUS_*`` and ``CONN_*`` constants into plain Python strings. - -1.0.1 -===== - -*2013-07-12* - -**Bug fixes** - -- 405_: `net_io_counters()`_ ``pernic=True`` no longer works as intended in 1.0.0. - -1.0.0 -===== - -*2013-07-10* - -**Enhancements** - -- 18_, [SunOS]: add Solaris support (yay!) (thanks Justin Venus) -- 367_: `Process.connections()`_ ``status`` strings are now constants. -- 380_: test suite exits with non-zero on failure. (patch by floppymaster) -- 391_: introduce unittest2 facilities and provide workarounds if unittest2 - is not installed (Python < 2.7). - -**Bug fixes** - -- 374_, [Windows]: negative memory usage reported if process uses a lot of - memory. -- 379_, [Linux]: `Process.memory_maps()`_ may raise ``ValueError``. -- 394_, [macOS]: mapped memory regions of `Process.memory_maps()`_ report - incorrect file name. -- 404_, [Linux]: ``sched_*affinity()`` are implicitly declared. (patch by Arfrever) - -**API changes** - -- `Process.connections()`_ ``status`` field is no longer a string but a - constant object (``psutil.CONN_*``). -- `Process.connections()`_ ``local_address`` and ``remote_address`` fields - renamed to ``laddr`` and ``raddr``. -- psutil.network_io_counters() renamed to `net_io_counters()`_. - -0.7.1 -===== - -*2013-05-03* - -**Bug fixes** - -- 325_, [BSD], **[critical]**: `virtual_memory()`_ can raise ``SystemError``. - (patch by Jan Beich) -- 370_, [BSD]: `Process.connections()`_ requires root. (patch by John Baldwin) -- 372_, [BSD]: different process methods raise `NoSuchProcess`_ instead of - `AccessDenied`_. - -0.7.0 -===== - -*2013-04-12* - -**Enhancements** - -- 233_: code migrated to Mercurial (yay!) -- 246_: psutil.error module is deprecated and scheduled for removal. -- 328_, [Windows]: `Process.ionice()`_ support. -- 359_: add `boot_time()`_ as a substitute of ``psutil.BOOT_TIME`` since the - latter cannot reflect system clock updates. -- 361_, [Linux]: `cpu_times()`_ now includes new ``steal``, ``guest`` and - ``guest_nice`` fields available on recent Linux kernels. Also, `cpu_percent()`_ - is more accurate. -- 362_: add `cpu_times_percent()`_ (per-CPU-time utilization as a percentage). - -**Bug fixes** - -- 234_, [Windows]: `disk_io_counters()`_ fails to list certain disks. -- 264_, [Windows]: use of `disk_partitions()`_ may cause a message box to - appear. -- 313_, [Linux], **[critical]**: `virtual_memory()`_ and `swap_memory()`_ can - crash on certain exotic Linux flavors having an incomplete ``/proc`` interface. - If that's the case we now set the unretrievable stats to ``0`` and raise - ``RuntimeWarning`` instead. -- 315_, [macOS]: fix some compilation warnings. -- 317_, [Windows]: cannot set process CPU affinity above 31 cores. -- 319_, [Linux]: `Process.memory_maps()`_ raises ``KeyError`` 'Anonymous' on Debian - squeeze. -- 321_, [POSIX]: `Process.ppid()`_ property is no longer cached as the kernel may set - the PPID to 1 in case of a zombie process. -- 323_, [macOS]: `disk_io_counters()`_ ``read_time`` and ``write_time`` - parameters were reporting microseconds not milliseconds. (patch by Gregory Szorc) -- 331_: `Process.cmdline()`_ is no longer cached after first access as it may - change. -- 333_, [macOS]: leak of Mach ports (patch by rsesek@google.com) -- 337_, [Linux], **[critical]**: `Process`_ methods not working because of a - poor ``/proc`` implementation will raise ``NotImplementedError`` rather than - ``RuntimeError`` and `Process.as_dict()`_ will not blow up. - (patch by Curtin1060) -- 338_, [Linux]: `disk_io_counters()`_ fails to find some disks. -- 339_, [FreeBSD]: ``get_pid_list()`` can allocate all the memory on system. -- 341_, [Linux], **[critical]**: psutil might crash on import due to error in - retrieving system terminals map. -- 344_, [FreeBSD]: `swap_memory()`_ might return incorrect results due to - ``kvm_open(3)`` not being called. (patch by Jean Sebastien) -- 338_, [Linux]: `disk_io_counters()`_ fails to find some disks. -- 351_, [Windows]: if psutil is compiled with MinGW32 (provided installers for - py2.4 and py2.5 are) `disk_io_counters()`_ will fail. (Patch by m.malycha) -- 353_, [macOS]: `users()`_ returns an empty list on macOS 10.8. -- 356_: `Process.parent()`_ now checks whether parent PID has been reused in which - case returns ``None``. -- 365_: `Process.nice()`_ (set) should check PID has not been reused by another - process. -- 366_, [FreeBSD], **[critical]**: `Process.memory_maps()`_, `Process.num_fds()`_, - `Process.open_files()`_ and `Process.cwd()`_ methods raise ``RuntimeError`` - instead of `AccessDenied`_. - -**API changes** - -- `Process.cmdline()`_ property is no longer cached after first access. -- `Process.ppid()`_ property is no longer cached after first access. -- [Linux] `Process`_ methods not working because of a poor ``/proc`` - implementation will raise ``NotImplementedError`` instead of ``RuntimeError``. -- ``psutil.error`` module is deprecated and scheduled for removal. - -0.6.1 -===== - -*2012-08-16* - -**Enhancements** - -- 316_: `Process.cmdline()`_ property now makes a better job at guessing the - process executable from the cmdline. - -**Bug fixes** - -- 316_: `Process.exe()`_ was resolved in case it was a symlink. -- 318_, **[critical]**: Python 2.4 compatibility was broken. - -**API changes** - -- `Process.exe()`_ can now return an empty string instead of raising `AccessDenied`_. -- `Process.exe()`_ is no longer resolved in case it's a symlink. - -0.6.0 -===== - -*2012-08-13* - -**Enhancements** - -- 216_, [POSIX]: `Process.connections()`_ UNIX sockets support. -- 220_, [FreeBSD]: ``get_connections()`` has been rewritten in C and no longer - requires ``lsof``. -- 222_, [macOS]: add support for `Process.cwd()`_. -- 261_: per-process extended memory info (`Process.memory_info_ex()`_). -- 295_, [macOS]: `Process.exe()`_ path is now determined by asking the OS - instead of being guessed from `Process.cmdline()`_. -- 297_, [macOS]: the `Process`_ methods below were always raising `AccessDenied`_ - for any process except the current one. Now this is no longer true. Also - they are 2.5x faster. `Process.name()`_, `Process.memory_info()`_, - `Process.memory_percent()`_, `Process.cpu_times()`_, `Process.cpu_percent()`_, - `Process.num_threads()`_. -- 300_: add `pmap.py`_ script. -- 301_: `process_iter()`_ now yields processes sorted by their PIDs. -- 302_: per-process number of voluntary and involuntary context switches - (`Process.num_ctx_switches()`_). -- 303_, [Windows]: the `Process`_ methods below were always raising `AccessDenied`_ - for any process not owned by current user. Now this is no longer true: - `Process.create_time()`_, `Process.cpu_times()`_, `Process.cpu_percent()`_, - `Process.memory_info()`_, `Process.memory_percent()`_, `Process.num_handles()`_, - `Process.io_counters()`_. -- 305_: add `netstat.py`_ script. -- 311_: system memory functions has been refactorized and rewritten and now - provide a more detailed and consistent representation of the system - memory. Added new `virtual_memory()`_ and `swap_memory()`_ functions. - All old memory-related functions are deprecated. Also two new example scripts - were added: `free.py`_ and `meminfo.py`_. -- 312_: ``net_io_counters()`` namedtuple includes 4 new fields: - ``errin``, ``errout``, ``dropin`` and ``dropout``, reflecting the number of - packets dropped and with errors. - -**Bug fixes** - -- 298_, [macOS], [BSD]: memory leak in `Process.num_fds()`_. -- 299_: potential memory leak every time ``PyList_New(0)`` is used. -- 303_, [Windows], **[critical]**: potential heap corruption in - `Process.num_threads()`_ and `Process.status()`_ methods. -- 305_, [FreeBSD], **[critical]**: can't compile on FreeBSD 9 due to removal of - ``utmp.h``. -- 306_, **[critical]**: at C level, errors are not checked when invoking ``Py*`` - functions which create or manipulate Python objects leading to potential - memory related errors and/or segmentation faults. -- 307_, [FreeBSD]: values returned by `net_io_counters()`_ are wrong. -- 308_, [BSD], [Windows]: ``psutil.virtmem_usage()`` wasn't actually returning - information about swap memory usage as it was supposed to do. It does now. -- 309_: `Process.open_files()`_ might not return files which can not be accessed - due to limited permissions. `AccessDenied`_ is now raised instead. - -**API changes** - -- ``psutil.phymem_usage()`` is deprecated (use `virtual_memory()`_) -- ``psutil.virtmem_usage()`` is deprecated (use `swap_memory()`_) -- [Linux]: ``psutil.phymem_buffers()`` is deprecated (use `virtual_memory()`_) -- [Linux]: ``psutil.cached_phymem()`` is deprecated (use `virtual_memory()`_) -- [Windows], [BSD]: ``psutil.virtmem_usage()`` now returns information about - swap memory instead of virtual memory. - -0.5.1 -===== - -*2012-06-29* - -**Enhancements** - -- 293_, [Windows]: `Process.exe()`_ path is now determined by asking the OS - instead of being guessed from `Process.cmdline()`_. - -**Bug fixes** - -- 292_, [Linux]: race condition in process `Process.open_files()`_, - `Process.connections()`_, `Process.threads()`_. -- 294_, [Windows]: `Process.cpu_affinity()`_ is only able to set CPU #0. - -0.5.0 -===== - -*2012-06-27* - -**Enhancements** - -- 195_, [Windows]: number of handles opened by process (`Process.num_handles()`_). -- 209_: `disk_partitions()`_ now provides also mount options. -- 229_: list users currently connected on the system (`users()`_). -- 238_, [Linux], [Windows]: process CPU affinity (get and set, - `Process.cpu_affinity()`_). -- 242_: add ``recursive=True`` to `Process.children()`_: return all process - descendants. -- 245_, [POSIX]: `Process.wait()`_ incrementally consumes less CPU cycles. -- 257_, [Windows]: removed Windows 2000 support. -- 258_, [Linux]: `Process.memory_info()`_ is now 0.5x faster. -- 260_: process's mapped memory regions. (Windows patch by wj32.64, macOS patch - by Jeremy Whitlock) -- 262_, [Windows]: `disk_partitions()`_ was slow due to inspecting the - floppy disk drive also when parameter is ``all=False``. -- 273_: ``psutil.get_process_list()`` is deprecated. -- 274_: psutil no longer requires ``2to3`` at installation time in order to work - with Python 3. -- 278_: new `Process.as_dict()`_ method. -- 281_: `Process.ppid()`_, `Process.name()`_, `Process.exe()`_, - `Process.cmdline()`_ and `Process.create_time()`_ properties of `Process`_ class - are now cached after being accessed. -- 282_: ``psutil.STATUS_*`` constants can now be compared by using their string - representation. -- 283_: speedup `Process.is_running()`_ by caching its return value in case the - process is terminated. -- 284_, [POSIX]: per-process number of opened file descriptors - (`Process.num_fds()`_). -- 287_: `process_iter()`_ now caches `Process`_ instances between calls. -- 290_: `Process.nice()`_ property is deprecated in favor of new ``get_nice()`` - and ``set_nice()`` methods. - -**Bug fixes** - -- 193_: `psutil.Popen`_ constructor can throw an exception if the spawned process - terminates quickly. -- 240_, [macOS]: incorrect use of ``free()`` for `Process.connections()`_. -- 244_, [POSIX]: `Process.wait()`_ can hog CPU resources if called against a - process which is not our children. -- 248_, [Linux]: `net_io_counters()`_ might return erroneous NIC names. -- 252_, [Windows]: `Process.cwd()`_ erroneously raise `NoSuchProcess`_ for - processes owned by another user. It now raises `AccessDenied`_ instead. -- 266_, [Windows]: ``psutil.get_pid_list()`` only shows 1024 processes. - (patch by Amoser) -- 267_, [macOS]: `Process.connections()`_ returns wrong remote address. - (Patch by Amoser) -- 272_, [Linux]: `Process.open_files()`_ potential race condition can lead to - unexpected `NoSuchProcess`_ exception. Also, we can get incorrect reports - of not absolutized path names. -- 275_, [Linux]: ``Process.io_counters()`` erroneously raise `NoSuchProcess`_ on - old Linux versions. Where not available it now raises ``NotImplementedError``. -- 286_: `Process.is_running()`_ doesn't actually check whether PID has been - reused. -- 314_: `Process.children()`_ can sometimes return non-children. - -**API changes** - -- ``Process.nice`` property is deprecated in favor of new ``get_nice()`` and - ``set_nice()`` methods. -- ``psutil.get_process_list()`` is deprecated. -- `Process.ppid()`_, `Process.name()`_, `Process.exe()`_, `Process.cmdline()`_ - and `Process.create_time()`_ properties of `Process`_ class are now cached after - being accessed, meaning `NoSuchProcess`_ will no longer be raised in case the - process is gone in the meantime. -- ``psutil.STATUS_*`` constants can now be compared by using their string - representation. - -0.4.1 -===== - -*2011-12-14* - -**Bug fixes** - -- 228_: some example scripts were not working with Python 3. -- 230_, [Windows], [macOS]: fix memory leak in `Process.connections()`_. -- 232_, [Linux]: ``psutil.phymem_usage()`` can report erroneous values which are - different than ``free`` command. -- 236_, [Windows]: fix memory/handle leak in `Process.memory_info()`_, - `Process.suspend()`_ and `Process.resume()`_ methods. - -0.4.0 -===== - -*2011-10-29* - -**Enhancements** - -- 150_: network I/O counters (`net_io_counters()`_). (macOS and Windows patch - by Jeremy Whitlock) -- 154_, [FreeBSD]: add support for `Process.cwd()`_. -- 157_, [Windows]: provide installer for Python 3.2 64-bit. -- 198_: `Process.wait()`_ with ``timeout=0`` can now be used to make the - function return immediately. -- 206_: disk I/O counters (`disk_io_counters()`_). (macOS and Windows patch by - Jeremy Whitlock) -- 213_: add `iotop.py`_ script. -- 217_: `Process.connections()`_ now has a ``kind`` argument to filter - for connections with different criteria. -- 221_, [FreeBSD]: `Process.open_files()`_ has been rewritten in C and no longer - relies on ``lsof``. -- 223_: add `top.py`_ script. -- 227_: add `nettop.py`_ script. - -**Bug fixes** - -- 135_, [macOS]: psutil cannot create `Process`_ object. -- 144_, [Linux]: no longer support 0 special PID. -- 188_, [Linux]: psutil import error on Linux ARM architectures. -- 194_, [POSIX]: `Process.cpu_percent()`_ now reports a percentage over - 100 on multicore processors. -- 197_, [Linux]: `Process.connections()`_ is broken on platforms not - supporting IPv6. -- 200_, [Linux], **[critical]**: ``psutil.NUM_CPUS`` not working on armel and - sparc architectures and causing crash on module import. -- 201_, [Linux]: `Process.connections()`_ is broken on big-endian - architectures. -- 211_: `Process`_ instance can unexpectedly raise `NoSuchProcess`_ if tested - for equality with another object. -- 218_, [Linux], **[critical]**: crash at import time on Debian 64-bit because - of a missing line in ``/proc/meminfo``. -- 226_, [FreeBSD], **[critical]**: crash at import time on FreeBSD 7 and minor. - -0.3.0 -===== - -*2011-07-08* - -**Enhancements** - -- 125_: system per-cpu percentage utilization and times (`Process.cpu_times()`_, - `Process.cpu_percent()`_). -- 163_: per-process associated terminal / TTY (`Process.terminal()`_). -- 171_: added ``get_phymem()`` and ``get_virtmem()`` functions returning system - memory information (``total``, ``used``, ``free``) and memory percent usage. - ``total_*``, ``avail_*`` and ``used_*`` memory functions are deprecated. -- 172_: disk usage statistics (`disk_usage()`_). -- 174_: mounted disk partitions (`disk_partitions()`_). -- 179_: setuptools is now used in setup.py - -**Bug fixes** - -- 159_, [Windows]: ``SetSeDebug()`` does not close handles or unset - impersonation on return. -- 164_, [Windows]: wait function raises a ``TimeoutException`` when a process - returns ``-1``. -- 165_: `Process.status()`_ raises an unhandled exception. -- 166_: `Process.memory_info()`_ leaks handles hogging system resources. -- 168_: `cpu_percent()`_ returns erroneous results when used in - non-blocking mode. (patch by Philip Roberts) -- 178_, [macOS]: `Process.threads()`_ leaks memory. -- 180_, [Windows]: `Process.num_threads()`_ and `Process.threads()`_ methods - can raise `NoSuchProcess`_ exception while process still exists. - -0.2.1 -===== - -*2011-03-20* - -**Enhancements** - -- 64_: per-process I/O counters (`Process.io_counters()`_). -- 116_: per-process `Process.wait()`_ (wait for process to terminate and return - its exit code). -- 134_: per-process threads (`Process.threads()`_). -- 136_: `Process.exe()`_ path on FreeBSD is now determined by asking the - kernel instead of guessing it from cmdline[0]. -- 137_: per-process real, effective and saved user and group ids - (`Process.gids()`_). -- 140_: system boot time (`boot_time()`_). -- 142_: per-process get and set niceness (priority) (`Process.nice()`_). -- 143_: per-process status (`Process.status()`_). -- 147_ [Linux]: per-process I/O niceness / priority (`Process.ionice()`_). -- 148_: `psutil.Popen`_ class which tidies up ``subprocess.Popen`` and `Process`_ - class in a single interface. -- 152_, [macOS]: `Process.open_files()`_ implementation has been rewritten - in C and no longer relies on ``lsof`` resulting in a 3x speedup. -- 153_, [macOS]: `Process.connections()`_ implementation has been rewritten - in C and no longer relies on ``lsof`` resulting in a 3x speedup. - -**Bug fixes** - -- 83_, [macOS]: `Process.cmdline()`_ is empty on macOS 64-bit. -- 130_, [Linux]: a race condition can cause ``IOError`` exception be raised on - if process disappears between ``open()`` and the subsequent ``read()`` call. -- 145_, [Windows], **[critical]**: ``WindowsError`` was raised instead of - `AccessDenied`_ when using `Process.resume()`_ or `Process.suspend()`_. -- 146_, [Linux]: `Process.exe()`_ property can raise ``TypeError`` if path - contains NULL bytes. -- 151_, [Linux]: `Process.exe()`_ and `Process.cwd()`_ for PID 0 return - inconsistent data. - -**API changes** - -- `Process`_ ``uid`` and ``gid`` properties are deprecated in favor of ``uids`` - and ``gids`` properties. - -0.2.0 -===== - -*2010-11-13* - -**Enhancements** - -- 79_: per-process open files (`Process.open_files()`_). -- 88_: total system physical cached memory. -- 88_: total system physical memory buffers used by the kernel. -- 91_: add `Process.send_signal()`_ and `Process.terminate()`_ methods. -- 95_: `NoSuchProcess`_ and `AccessDenied`_ exception classes now provide - ``pid``, ``name`` and ``msg`` attributes. -- 97_: per-process children (`Process.children()`_). -- 98_: `Process.cpu_times()`_ and `Process.memory_info()`_ now return - a namedtuple instead of a tuple. -- 103_: per-process opened TCP and UDP connections (`Process.connections()`_). -- 107_, [Windows]: add support for Windows 64 bit. (patch by cjgohlke) -- 111_: per-process executable name (`Process.exe()`_). -- 113_: exception messages now include `Process.name()`_ and `Process.pid`_. -- 114_, [Windows]: `Process.username()`_ has been rewritten in pure C and no - longer uses WMI resulting in a big speedup. Also, pywin32 is no longer - required as a third-party dependency. (patch by wj32) -- 117_, [Windows]: added support for Windows 2000. -- 123_: `cpu_percent()`_ and `Process.cpu_percent()`_ accept a - new ``interval`` parameter. -- 129_: per-process threads (`Process.threads()`_). - -**Bug fixes** - -- 80_: fixed warnings when installing psutil with easy_install. -- 81_, [Windows]: psutil fails to compile with Visual Studio. -- 94_: `Process.suspend()`_ raises ``OSError`` instead of `AccessDenied`_. -- 86_, [FreeBSD]: psutil didn't compile against FreeBSD 6.x. -- 102_, [Windows]: orphaned process handles obtained by using ``OpenProcess`` - in C were left behind every time `Process`_ class was instantiated. -- 111_, [POSIX]: ``path`` and ``name`` `Process`_ properties report truncated - or erroneous values on POSIX. -- 120_, [macOS]: `cpu_percent()`_ always returning 100%. -- 112_: ``uid`` and ``gid`` properties don't change if process changes effective - user/group id at some point. -- 126_: `Process.ppid()`_, `Process.uids()`_, `Process.gids()`_, `Process.name()`_, - `Process.exe()`_, `Process.cmdline()`_ and `Process.create_time()`_ - properties are no longer cached and correctly raise `NoSuchProcess`_ exception - if the process disappears. - -**API changes** - -- ``psutil.Process.path`` property is deprecated and works as an alias for - ``psutil.Process.exe`` property. -- `Process.kill()`_: signal argument was removed - to send a signal to the - process use `Process.send_signal()`_ method instead. -- `Process.memory_info()`_ returns a nametuple instead of a tuple. -- `cpu_times()`_ returns a nametuple instead of a tuple. -- New `Process`_ methods: `Process.open_files()`_, `Process.connections()`_, - `Process.send_signal()`_ and `Process.terminate()`_. -- `Process.ppid()`_, `Process.uids()`_, `Process.gids()`_, `Process.name()`_, - `Process.exe()`_, `Process.cmdline()`_ and `Process.create_time()`_ - properties are no longer cached and raise `NoSuchProcess`_ exception if process - disappears. -- `cpu_percent()`_ no longer returns immediately (see issue 123). -- `Process.cpu_percent()`_ and `cpu_percent()`_ no longer returns immediately - by default (see issue 123_). - -0.1.3 -===== - -*2010-03-02* - -**Enhancements** - -- 14_: `Process.username()`_. -- 51_, [Linux], [Windows]: per-process current working directory (`Process.cwd()`_). -- 59_: `Process.is_running()`_ is now 10 times faster. -- 61_, [FreeBSD]: added supoprt for FreeBSD 64 bit. -- 71_: per-process suspend and resume (`Process.suspend()`_ and `Process.resume()`_). -- 75_: Python 3 support. - -**Bug fixes** - -- 36_: `Process.cpu_times()`_ and `Process.memory_info()`_ functions succeeded. - also for dead processes while a `NoSuchProcess`_ exception is supposed to be raised. -- 48_, [FreeBSD]: incorrect size for MIB array defined in ``getcmdargs``. -- 49_, [FreeBSD]: possible memory leak due to missing ``free()`` on error - condition in ``getcmdpath()``. -- 50_, [BSD]: fixed ``getcmdargs()`` memory fragmentation. -- 55_, [Windows]: ``test_pid_4`` was failing on Windows Vista. -- 57_: some unit tests were failing on systems where no swap memory is - available. -- 58_: `Process.is_running()`_ is now called before `Process.kill()`_ to make - sure we are going to kill the correct process. -- 73_, [macOS]: virtual memory size reported on includes shared library size. -- 77_: `NoSuchProcess`_ wasn't raised on `Process.create_time()`_ if `Process.kill()`_ - was used first. - -0.1.2 -===== - -*2009-05-06* - -**Enhancements** - -- 32_: Per-process CPU user/kernel times (`Process.cpu_times()`_). -- 33_: Per-process create time (`Process.create_time()`_). -- 34_: Per-process CPU utilization percentage (`Process.cpu_percent()`_). -- 38_: Per-process memory usage (bytes) (`Process.memory_info()`_). -- 41_: Per-process memory percent (`Process.memory_percent()`_). -- 39_: System uptime (`boot_time()`_). -- 43_: Total system virtual memory. -- 46_: Total system physical memory. -- 44_: Total system used/free virtual and physical memory. - -**Bug fixes** - -- 36_, [Windows]: `NoSuchProcess`_ not raised when accessing timing methods. -- 40_, [FreeBSD], [macOS]: fix ``test_get_cpu_times`` failures. -- 42_, [Windows]: `Process.memory_percent()`_ raises `AccessDenied`_. - -0.1.1 -===== - -*2009-03-06* - -**Enhancements** - -- 4_, [FreeBSD]: support for all functions of psutil. -- 9_, [macOS], [Windows]: add ``Process.uid`` and ``Process.gid``, returning - process UID and GID. -- 11_: per-process parent object: `Process.parent()`_ property returns a - `Process`_ object representing the parent process, and `Process.ppid()`_ - returns the parent PID. -- 12_, 15_: - `NoSuchProcess`_ exception now raised when creating an object - for a nonexistent process, or when retrieving information about a process - that has gone away. -- 21_, [Windows]: `AccessDenied`_ exception created for raising access denied - errors from ``OSError`` or ``WindowsError`` on individual platforms. -- 26_: `process_iter()`_ function to iterate over processes as - `Process`_ objects with a generator. -- `Process`_ objects can now also be compared with == operator for equality - (PID, name, command line are compared). - -**Bug fixes** - -- 16_, [Windows]: Special case for "System Idle Process" (PID 0) which - otherwise would return an "invalid parameter" exception. -- 17_: get_process_list() ignores `NoSuchProcess`_ and `AccessDenied`_ - exceptions during building of the list. -- 22_, [Windows]: `Process.kill()`_ for PID 0 was failing with an unset exception. -- 23_, [Linux], [macOS]: create special case for `pid_exists()`_ with PID 0. -- 24_, [Windows], **[critical]**: `Process.kill()`_ for PID 0 now raises - `AccessDenied`_ exception instead of ``WindowsError``. -- 30_: psutil.get_pid_list() was returning two 0 PIDs. - - -.. _`PROCFS_PATH`: https://psutil.readthedocs.io/en/latest/#psutil.PROCFS_PATH - -.. _`boot_time()`: https://psutil.readthedocs.io/en/latest/#psutil.boot_time -.. _`cpu_count()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_count -.. _`cpu_freq()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_freq -.. _`cpu_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent -.. _`cpu_stats()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_stats -.. _`cpu_times()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_times -.. _`cpu_times_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.cpu_times_percent -.. _`disk_io_counters()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_io_counters -.. _`disk_partitions()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_partitions -.. _`disk_usage()`: https://psutil.readthedocs.io/en/latest/#psutil.disk_usage -.. _`getloadavg()`: https://psutil.readthedocs.io/en/latest/#psutil.getloadavg -.. _`heap_info()`: https://psutil.readthedocs.io/en/latest/#psutil.heap_info -.. _`heap_trim()`: https://psutil.readthedocs.io/en/latest/#psutil.heap_trim -.. _`net_connections()`: https://psutil.readthedocs.io/en/latest/#psutil.net_connections -.. _`net_if_addrs()`: https://psutil.readthedocs.io/en/latest/#psutil.net_if_addrs -.. _`net_if_stats()`: https://psutil.readthedocs.io/en/latest/#psutil.net_if_stats -.. _`net_io_counters()`: https://psutil.readthedocs.io/en/latest/#psutil.net_io_counters -.. _`pid_exists()`: https://psutil.readthedocs.io/en/latest/#psutil.pid_exists -.. _`pids()`: https://psutil.readthedocs.io/en/latest/#psutil.pids -.. _`process_iter()`: https://psutil.readthedocs.io/en/latest/#psutil.process_iter -.. _`sensors_battery()`: https://psutil.readthedocs.io/en/latest/#psutil.sensors_battery -.. _`sensors_fans()`: https://psutil.readthedocs.io/en/latest/#psutil.sensors_fans -.. _`sensors_temperatures()`: https://psutil.readthedocs.io/en/latest/#psutil.sensors_temperatures -.. _`swap_memory()`: https://psutil.readthedocs.io/en/latest/#psutil.swap_memory -.. _`users()`: https://psutil.readthedocs.io/en/latest/#psutil.users -.. _`virtual_memory()`: https://psutil.readthedocs.io/en/latest/#psutil.virtual_memory -.. _`wait_procs()`: https://psutil.readthedocs.io/en/latest/#psutil.wait_procs -.. _`win_service_get()`: https://psutil.readthedocs.io/en/latest/#psutil.win_service_get -.. _`win_service_iter()`: https://psutil.readthedocs.io/en/latest/#psutil.win_service_iter - -.. _`Process`: https://psutil.readthedocs.io/en/latest/#psutil.Process -.. _`psutil.Popen`: https://psutil.readthedocs.io/en/latest/#psutil.Popen - -.. _`AccessDenied`: https://psutil.readthedocs.io/en/latest/#psutil.AccessDenied -.. _`NoSuchProcess`: https://psutil.readthedocs.io/en/latest/#psutil.NoSuchProcess -.. _`TimeoutExpired`: https://psutil.readthedocs.io/en/latest/#psutil.TimeoutExpired -.. _`ZombieProcess`: https://psutil.readthedocs.io/en/latest/#psutil.ZombieProcess - -.. _`Process.as_dict()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.as_dict -.. _`Process.children()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.children -.. _`Process.cmdline()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.connections -.. _`Process.connections()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.connections -.. _`Process.cpu_affinity()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_affinity -.. _`Process.cpu_num()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_num -.. _`Process.cpu_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_percent -.. _`Process.cpu_times()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_times -.. _`Process.create_time()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.create_time -.. _`Process.cwd()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.cwd -.. _`Process.environ()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.environ -.. _`Process.exe()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.exe -.. _`Process.gids()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.gids -.. _`Process.io_counters()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.io_counters -.. _`Process.ionice()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.ionice -.. _`Process.is_running()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.is_running -.. _`Process.kill()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.kill -.. _`Process.memory_footprint()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_footprint -.. _`Process.memory_full_info()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_full_info -.. _`Process.memory_info()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info -.. _`Process.memory_info_ex()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info_ex -.. _`Process.memory_maps()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_maps -.. _`Process.memory_percent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_percent -.. _`Process.name()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.name -.. _`Process.net_connections()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.net_connections -.. _`Process.nice()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.nice -.. _`Process.num_ctx_switches()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_ctx_switches -.. _`Process.num_fds()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_fds -.. _`Process.num_handles()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_handles -.. _`Process.num_threads()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.num_threads -.. _`Process.oneshot()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.oneshot -.. _`Process.open_files()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.open_files -.. _`Process.page_faults()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.page_faults -.. _`Process.parent()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.parent -.. _`Process.parents()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.parents -.. _`Process.pid`: https://psutil.readthedocs.io/en/latest/#psutil.Process.pid -.. _`Process.ppid()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.ppid -.. _`Process.resume()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.resume -.. _`Process.rlimit()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.rlimit -.. _`Process.send_signal()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.send_signal -.. _`Process.status()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.status -.. _`Process.suspend()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.suspend -.. _`Process.terminal()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.terminal -.. _`Process.terminate()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.terminate -.. _`Process.threads()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.threads -.. _`Process.uids()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.uids -.. _`Process.username()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.username -.. _`Process.wait()`: https://psutil.readthedocs.io/en/latest/#psutil.Process.wait - -.. _`ProcessStatus`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessStatus -.. _`ProcessPriority`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessPriority -.. _`ProcessIOPriority`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessIOPriority -.. _`ProcessRlimit`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ProcessRlimit -.. _`ConnectionStatus`: https://psutil.readthedocs.io/en/latest/#psutil.psutil.ConnectionStatus - -.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py -.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py -.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py -.. _`pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py -.. _`top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py - -.. _`psutil/_ntuples.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_ntuples.py - -.. _1: https://github.com/giampaolo/psutil/issues/1 -.. _2: https://github.com/giampaolo/psutil/issues/2 -.. _3: https://github.com/giampaolo/psutil/issues/3 -.. _4: https://github.com/giampaolo/psutil/issues/4 -.. _5: https://github.com/giampaolo/psutil/issues/5 -.. _6: https://github.com/giampaolo/psutil/issues/6 -.. _7: https://github.com/giampaolo/psutil/issues/7 -.. _8: https://github.com/giampaolo/psutil/issues/8 -.. _9: https://github.com/giampaolo/psutil/issues/9 -.. _10: https://github.com/giampaolo/psutil/issues/10 -.. _11: https://github.com/giampaolo/psutil/issues/11 -.. _12: https://github.com/giampaolo/psutil/issues/12 -.. _13: https://github.com/giampaolo/psutil/issues/13 -.. _14: https://github.com/giampaolo/psutil/issues/14 -.. _15: https://github.com/giampaolo/psutil/issues/15 -.. _16: https://github.com/giampaolo/psutil/issues/16 -.. _17: https://github.com/giampaolo/psutil/issues/17 -.. _18: https://github.com/giampaolo/psutil/issues/18 -.. _19: https://github.com/giampaolo/psutil/issues/19 -.. _20: https://github.com/giampaolo/psutil/issues/20 -.. _21: https://github.com/giampaolo/psutil/issues/21 -.. _22: https://github.com/giampaolo/psutil/issues/22 -.. _23: https://github.com/giampaolo/psutil/issues/23 -.. _24: https://github.com/giampaolo/psutil/issues/24 -.. _25: https://github.com/giampaolo/psutil/issues/25 -.. _26: https://github.com/giampaolo/psutil/issues/26 -.. _27: https://github.com/giampaolo/psutil/issues/27 -.. _28: https://github.com/giampaolo/psutil/issues/28 -.. _29: https://github.com/giampaolo/psutil/issues/29 -.. _30: https://github.com/giampaolo/psutil/issues/30 -.. _31: https://github.com/giampaolo/psutil/issues/31 -.. _32: https://github.com/giampaolo/psutil/issues/32 -.. _33: https://github.com/giampaolo/psutil/issues/33 -.. _34: https://github.com/giampaolo/psutil/issues/34 -.. _35: https://github.com/giampaolo/psutil/issues/35 -.. _36: https://github.com/giampaolo/psutil/issues/36 -.. _37: https://github.com/giampaolo/psutil/issues/37 -.. _38: https://github.com/giampaolo/psutil/issues/38 -.. _39: https://github.com/giampaolo/psutil/issues/39 -.. _40: https://github.com/giampaolo/psutil/issues/40 -.. _41: https://github.com/giampaolo/psutil/issues/41 -.. _42: https://github.com/giampaolo/psutil/issues/42 -.. _43: https://github.com/giampaolo/psutil/issues/43 -.. _44: https://github.com/giampaolo/psutil/issues/44 -.. _45: https://github.com/giampaolo/psutil/issues/45 -.. _46: https://github.com/giampaolo/psutil/issues/46 -.. _47: https://github.com/giampaolo/psutil/issues/47 -.. _48: https://github.com/giampaolo/psutil/issues/48 -.. _49: https://github.com/giampaolo/psutil/issues/49 -.. _50: https://github.com/giampaolo/psutil/issues/50 -.. _51: https://github.com/giampaolo/psutil/issues/51 -.. _52: https://github.com/giampaolo/psutil/issues/52 -.. _53: https://github.com/giampaolo/psutil/issues/53 -.. _54: https://github.com/giampaolo/psutil/issues/54 -.. _55: https://github.com/giampaolo/psutil/issues/55 -.. _56: https://github.com/giampaolo/psutil/issues/56 -.. _57: https://github.com/giampaolo/psutil/issues/57 -.. _58: https://github.com/giampaolo/psutil/issues/58 -.. _59: https://github.com/giampaolo/psutil/issues/59 -.. _60: https://github.com/giampaolo/psutil/issues/60 -.. _61: https://github.com/giampaolo/psutil/issues/61 -.. _62: https://github.com/giampaolo/psutil/issues/62 -.. _63: https://github.com/giampaolo/psutil/issues/63 -.. _64: https://github.com/giampaolo/psutil/issues/64 -.. _65: https://github.com/giampaolo/psutil/issues/65 -.. _66: https://github.com/giampaolo/psutil/issues/66 -.. _67: https://github.com/giampaolo/psutil/issues/67 -.. _68: https://github.com/giampaolo/psutil/issues/68 -.. _69: https://github.com/giampaolo/psutil/issues/69 -.. _70: https://github.com/giampaolo/psutil/issues/70 -.. _71: https://github.com/giampaolo/psutil/issues/71 -.. _72: https://github.com/giampaolo/psutil/issues/72 -.. _73: https://github.com/giampaolo/psutil/issues/73 -.. _74: https://github.com/giampaolo/psutil/issues/74 -.. _75: https://github.com/giampaolo/psutil/issues/75 -.. _76: https://github.com/giampaolo/psutil/issues/76 -.. _77: https://github.com/giampaolo/psutil/issues/77 -.. _78: https://github.com/giampaolo/psutil/issues/78 -.. _79: https://github.com/giampaolo/psutil/issues/79 -.. _80: https://github.com/giampaolo/psutil/issues/80 -.. _81: https://github.com/giampaolo/psutil/issues/81 -.. _82: https://github.com/giampaolo/psutil/issues/82 -.. _83: https://github.com/giampaolo/psutil/issues/83 -.. _84: https://github.com/giampaolo/psutil/issues/84 -.. _85: https://github.com/giampaolo/psutil/issues/85 -.. _86: https://github.com/giampaolo/psutil/issues/86 -.. _87: https://github.com/giampaolo/psutil/issues/87 -.. _88: https://github.com/giampaolo/psutil/issues/88 -.. _89: https://github.com/giampaolo/psutil/issues/89 -.. _90: https://github.com/giampaolo/psutil/issues/90 -.. _91: https://github.com/giampaolo/psutil/issues/91 -.. _92: https://github.com/giampaolo/psutil/issues/92 -.. _93: https://github.com/giampaolo/psutil/issues/93 -.. _94: https://github.com/giampaolo/psutil/issues/94 -.. _95: https://github.com/giampaolo/psutil/issues/95 -.. _96: https://github.com/giampaolo/psutil/issues/96 -.. _97: https://github.com/giampaolo/psutil/issues/97 -.. _98: https://github.com/giampaolo/psutil/issues/98 -.. _99: https://github.com/giampaolo/psutil/issues/99 -.. _100: https://github.com/giampaolo/psutil/issues/100 -.. _101: https://github.com/giampaolo/psutil/issues/101 -.. _102: https://github.com/giampaolo/psutil/issues/102 -.. _103: https://github.com/giampaolo/psutil/issues/103 -.. _104: https://github.com/giampaolo/psutil/issues/104 -.. _105: https://github.com/giampaolo/psutil/issues/105 -.. _106: https://github.com/giampaolo/psutil/issues/106 -.. _107: https://github.com/giampaolo/psutil/issues/107 -.. _108: https://github.com/giampaolo/psutil/issues/108 -.. _109: https://github.com/giampaolo/psutil/issues/109 -.. _110: https://github.com/giampaolo/psutil/issues/110 -.. _111: https://github.com/giampaolo/psutil/issues/111 -.. _112: https://github.com/giampaolo/psutil/issues/112 -.. _113: https://github.com/giampaolo/psutil/issues/113 -.. _114: https://github.com/giampaolo/psutil/issues/114 -.. _115: https://github.com/giampaolo/psutil/issues/115 -.. _116: https://github.com/giampaolo/psutil/issues/116 -.. _117: https://github.com/giampaolo/psutil/issues/117 -.. _118: https://github.com/giampaolo/psutil/issues/118 -.. _119: https://github.com/giampaolo/psutil/issues/119 -.. _120: https://github.com/giampaolo/psutil/issues/120 -.. _121: https://github.com/giampaolo/psutil/issues/121 -.. _122: https://github.com/giampaolo/psutil/issues/122 -.. _123: https://github.com/giampaolo/psutil/issues/123 -.. _124: https://github.com/giampaolo/psutil/issues/124 -.. _125: https://github.com/giampaolo/psutil/issues/125 -.. _126: https://github.com/giampaolo/psutil/issues/126 -.. _127: https://github.com/giampaolo/psutil/issues/127 -.. _128: https://github.com/giampaolo/psutil/issues/128 -.. _129: https://github.com/giampaolo/psutil/issues/129 -.. _130: https://github.com/giampaolo/psutil/issues/130 -.. _131: https://github.com/giampaolo/psutil/issues/131 -.. _132: https://github.com/giampaolo/psutil/issues/132 -.. _133: https://github.com/giampaolo/psutil/issues/133 -.. _134: https://github.com/giampaolo/psutil/issues/134 -.. _135: https://github.com/giampaolo/psutil/issues/135 -.. _136: https://github.com/giampaolo/psutil/issues/136 -.. _137: https://github.com/giampaolo/psutil/issues/137 -.. _138: https://github.com/giampaolo/psutil/issues/138 -.. _139: https://github.com/giampaolo/psutil/issues/139 -.. _140: https://github.com/giampaolo/psutil/issues/140 -.. _141: https://github.com/giampaolo/psutil/issues/141 -.. _142: https://github.com/giampaolo/psutil/issues/142 -.. _143: https://github.com/giampaolo/psutil/issues/143 -.. _144: https://github.com/giampaolo/psutil/issues/144 -.. _145: https://github.com/giampaolo/psutil/issues/145 -.. _146: https://github.com/giampaolo/psutil/issues/146 -.. _147: https://github.com/giampaolo/psutil/issues/147 -.. _148: https://github.com/giampaolo/psutil/issues/148 -.. _149: https://github.com/giampaolo/psutil/issues/149 -.. _150: https://github.com/giampaolo/psutil/issues/150 -.. _151: https://github.com/giampaolo/psutil/issues/151 -.. _152: https://github.com/giampaolo/psutil/issues/152 -.. _153: https://github.com/giampaolo/psutil/issues/153 -.. _154: https://github.com/giampaolo/psutil/issues/154 -.. _155: https://github.com/giampaolo/psutil/issues/155 -.. _156: https://github.com/giampaolo/psutil/issues/156 -.. _157: https://github.com/giampaolo/psutil/issues/157 -.. _158: https://github.com/giampaolo/psutil/issues/158 -.. _159: https://github.com/giampaolo/psutil/issues/159 -.. _160: https://github.com/giampaolo/psutil/issues/160 -.. _161: https://github.com/giampaolo/psutil/issues/161 -.. _162: https://github.com/giampaolo/psutil/issues/162 -.. _163: https://github.com/giampaolo/psutil/issues/163 -.. _164: https://github.com/giampaolo/psutil/issues/164 -.. _165: https://github.com/giampaolo/psutil/issues/165 -.. _166: https://github.com/giampaolo/psutil/issues/166 -.. _167: https://github.com/giampaolo/psutil/issues/167 -.. _168: https://github.com/giampaolo/psutil/issues/168 -.. _169: https://github.com/giampaolo/psutil/issues/169 -.. _170: https://github.com/giampaolo/psutil/issues/170 -.. _171: https://github.com/giampaolo/psutil/issues/171 -.. _172: https://github.com/giampaolo/psutil/issues/172 -.. _173: https://github.com/giampaolo/psutil/issues/173 -.. _174: https://github.com/giampaolo/psutil/issues/174 -.. _175: https://github.com/giampaolo/psutil/issues/175 -.. _176: https://github.com/giampaolo/psutil/issues/176 -.. _177: https://github.com/giampaolo/psutil/issues/177 -.. _178: https://github.com/giampaolo/psutil/issues/178 -.. _179: https://github.com/giampaolo/psutil/issues/179 -.. _180: https://github.com/giampaolo/psutil/issues/180 -.. _181: https://github.com/giampaolo/psutil/issues/181 -.. _182: https://github.com/giampaolo/psutil/issues/182 -.. _183: https://github.com/giampaolo/psutil/issues/183 -.. _184: https://github.com/giampaolo/psutil/issues/184 -.. _185: https://github.com/giampaolo/psutil/issues/185 -.. _186: https://github.com/giampaolo/psutil/issues/186 -.. _187: https://github.com/giampaolo/psutil/issues/187 -.. _188: https://github.com/giampaolo/psutil/issues/188 -.. _189: https://github.com/giampaolo/psutil/issues/189 -.. _190: https://github.com/giampaolo/psutil/issues/190 -.. _191: https://github.com/giampaolo/psutil/issues/191 -.. _192: https://github.com/giampaolo/psutil/issues/192 -.. _193: https://github.com/giampaolo/psutil/issues/193 -.. _194: https://github.com/giampaolo/psutil/issues/194 -.. _195: https://github.com/giampaolo/psutil/issues/195 -.. _196: https://github.com/giampaolo/psutil/issues/196 -.. _197: https://github.com/giampaolo/psutil/issues/197 -.. _198: https://github.com/giampaolo/psutil/issues/198 -.. _199: https://github.com/giampaolo/psutil/issues/199 -.. _200: https://github.com/giampaolo/psutil/issues/200 -.. _201: https://github.com/giampaolo/psutil/issues/201 -.. _202: https://github.com/giampaolo/psutil/issues/202 -.. _203: https://github.com/giampaolo/psutil/issues/203 -.. _204: https://github.com/giampaolo/psutil/issues/204 -.. _205: https://github.com/giampaolo/psutil/issues/205 -.. _206: https://github.com/giampaolo/psutil/issues/206 -.. _207: https://github.com/giampaolo/psutil/issues/207 -.. _208: https://github.com/giampaolo/psutil/issues/208 -.. _209: https://github.com/giampaolo/psutil/issues/209 -.. _210: https://github.com/giampaolo/psutil/issues/210 -.. _211: https://github.com/giampaolo/psutil/issues/211 -.. _212: https://github.com/giampaolo/psutil/issues/212 -.. _213: https://github.com/giampaolo/psutil/issues/213 -.. _214: https://github.com/giampaolo/psutil/issues/214 -.. _215: https://github.com/giampaolo/psutil/issues/215 -.. _216: https://github.com/giampaolo/psutil/issues/216 -.. _217: https://github.com/giampaolo/psutil/issues/217 -.. _218: https://github.com/giampaolo/psutil/issues/218 -.. _219: https://github.com/giampaolo/psutil/issues/219 -.. _220: https://github.com/giampaolo/psutil/issues/220 -.. _221: https://github.com/giampaolo/psutil/issues/221 -.. _222: https://github.com/giampaolo/psutil/issues/222 -.. _223: https://github.com/giampaolo/psutil/issues/223 -.. _224: https://github.com/giampaolo/psutil/issues/224 -.. _225: https://github.com/giampaolo/psutil/issues/225 -.. _226: https://github.com/giampaolo/psutil/issues/226 -.. _227: https://github.com/giampaolo/psutil/issues/227 -.. _228: https://github.com/giampaolo/psutil/issues/228 -.. _229: https://github.com/giampaolo/psutil/issues/229 -.. _230: https://github.com/giampaolo/psutil/issues/230 -.. _231: https://github.com/giampaolo/psutil/issues/231 -.. _232: https://github.com/giampaolo/psutil/issues/232 -.. _233: https://github.com/giampaolo/psutil/issues/233 -.. _234: https://github.com/giampaolo/psutil/issues/234 -.. _235: https://github.com/giampaolo/psutil/issues/235 -.. _236: https://github.com/giampaolo/psutil/issues/236 -.. _237: https://github.com/giampaolo/psutil/issues/237 -.. _238: https://github.com/giampaolo/psutil/issues/238 -.. _239: https://github.com/giampaolo/psutil/issues/239 -.. _240: https://github.com/giampaolo/psutil/issues/240 -.. _241: https://github.com/giampaolo/psutil/issues/241 -.. _242: https://github.com/giampaolo/psutil/issues/242 -.. _243: https://github.com/giampaolo/psutil/issues/243 -.. _244: https://github.com/giampaolo/psutil/issues/244 -.. _245: https://github.com/giampaolo/psutil/issues/245 -.. _246: https://github.com/giampaolo/psutil/issues/246 -.. _247: https://github.com/giampaolo/psutil/issues/247 -.. _248: https://github.com/giampaolo/psutil/issues/248 -.. _249: https://github.com/giampaolo/psutil/issues/249 -.. _250: https://github.com/giampaolo/psutil/issues/250 -.. _251: https://github.com/giampaolo/psutil/issues/251 -.. _252: https://github.com/giampaolo/psutil/issues/252 -.. _253: https://github.com/giampaolo/psutil/issues/253 -.. _254: https://github.com/giampaolo/psutil/issues/254 -.. _255: https://github.com/giampaolo/psutil/issues/255 -.. _256: https://github.com/giampaolo/psutil/issues/256 -.. _257: https://github.com/giampaolo/psutil/issues/257 -.. _258: https://github.com/giampaolo/psutil/issues/258 -.. _259: https://github.com/giampaolo/psutil/issues/259 -.. _260: https://github.com/giampaolo/psutil/issues/260 -.. _261: https://github.com/giampaolo/psutil/issues/261 -.. _262: https://github.com/giampaolo/psutil/issues/262 -.. _263: https://github.com/giampaolo/psutil/issues/263 -.. _264: https://github.com/giampaolo/psutil/issues/264 -.. _265: https://github.com/giampaolo/psutil/issues/265 -.. _266: https://github.com/giampaolo/psutil/issues/266 -.. _267: https://github.com/giampaolo/psutil/issues/267 -.. _268: https://github.com/giampaolo/psutil/issues/268 -.. _269: https://github.com/giampaolo/psutil/issues/269 -.. _270: https://github.com/giampaolo/psutil/issues/270 -.. _271: https://github.com/giampaolo/psutil/issues/271 -.. _272: https://github.com/giampaolo/psutil/issues/272 -.. _273: https://github.com/giampaolo/psutil/issues/273 -.. _274: https://github.com/giampaolo/psutil/issues/274 -.. _275: https://github.com/giampaolo/psutil/issues/275 -.. _276: https://github.com/giampaolo/psutil/issues/276 -.. _277: https://github.com/giampaolo/psutil/issues/277 -.. _278: https://github.com/giampaolo/psutil/issues/278 -.. _279: https://github.com/giampaolo/psutil/issues/279 -.. _280: https://github.com/giampaolo/psutil/issues/280 -.. _281: https://github.com/giampaolo/psutil/issues/281 -.. _282: https://github.com/giampaolo/psutil/issues/282 -.. _283: https://github.com/giampaolo/psutil/issues/283 -.. _284: https://github.com/giampaolo/psutil/issues/284 -.. _285: https://github.com/giampaolo/psutil/issues/285 -.. _286: https://github.com/giampaolo/psutil/issues/286 -.. _287: https://github.com/giampaolo/psutil/issues/287 -.. _288: https://github.com/giampaolo/psutil/issues/288 -.. _289: https://github.com/giampaolo/psutil/issues/289 -.. _290: https://github.com/giampaolo/psutil/issues/290 -.. _291: https://github.com/giampaolo/psutil/issues/291 -.. _292: https://github.com/giampaolo/psutil/issues/292 -.. _293: https://github.com/giampaolo/psutil/issues/293 -.. _294: https://github.com/giampaolo/psutil/issues/294 -.. _295: https://github.com/giampaolo/psutil/issues/295 -.. _296: https://github.com/giampaolo/psutil/issues/296 -.. _297: https://github.com/giampaolo/psutil/issues/297 -.. _298: https://github.com/giampaolo/psutil/issues/298 -.. _299: https://github.com/giampaolo/psutil/issues/299 -.. _300: https://github.com/giampaolo/psutil/issues/300 -.. _301: https://github.com/giampaolo/psutil/issues/301 -.. _302: https://github.com/giampaolo/psutil/issues/302 -.. _303: https://github.com/giampaolo/psutil/issues/303 -.. _304: https://github.com/giampaolo/psutil/issues/304 -.. _305: https://github.com/giampaolo/psutil/issues/305 -.. _306: https://github.com/giampaolo/psutil/issues/306 -.. _307: https://github.com/giampaolo/psutil/issues/307 -.. _308: https://github.com/giampaolo/psutil/issues/308 -.. _309: https://github.com/giampaolo/psutil/issues/309 -.. _310: https://github.com/giampaolo/psutil/issues/310 -.. _311: https://github.com/giampaolo/psutil/issues/311 -.. _312: https://github.com/giampaolo/psutil/issues/312 -.. _313: https://github.com/giampaolo/psutil/issues/313 -.. _314: https://github.com/giampaolo/psutil/issues/314 -.. _315: https://github.com/giampaolo/psutil/issues/315 -.. _316: https://github.com/giampaolo/psutil/issues/316 -.. _317: https://github.com/giampaolo/psutil/issues/317 -.. _318: https://github.com/giampaolo/psutil/issues/318 -.. _319: https://github.com/giampaolo/psutil/issues/319 -.. _320: https://github.com/giampaolo/psutil/issues/320 -.. _321: https://github.com/giampaolo/psutil/issues/321 -.. _322: https://github.com/giampaolo/psutil/issues/322 -.. _323: https://github.com/giampaolo/psutil/issues/323 -.. _324: https://github.com/giampaolo/psutil/issues/324 -.. _325: https://github.com/giampaolo/psutil/issues/325 -.. _326: https://github.com/giampaolo/psutil/issues/326 -.. _327: https://github.com/giampaolo/psutil/issues/327 -.. _328: https://github.com/giampaolo/psutil/issues/328 -.. _329: https://github.com/giampaolo/psutil/issues/329 -.. _330: https://github.com/giampaolo/psutil/issues/330 -.. _331: https://github.com/giampaolo/psutil/issues/331 -.. _332: https://github.com/giampaolo/psutil/issues/332 -.. _333: https://github.com/giampaolo/psutil/issues/333 -.. _334: https://github.com/giampaolo/psutil/issues/334 -.. _335: https://github.com/giampaolo/psutil/issues/335 -.. _336: https://github.com/giampaolo/psutil/issues/336 -.. _337: https://github.com/giampaolo/psutil/issues/337 -.. _338: https://github.com/giampaolo/psutil/issues/338 -.. _339: https://github.com/giampaolo/psutil/issues/339 -.. _340: https://github.com/giampaolo/psutil/issues/340 -.. _341: https://github.com/giampaolo/psutil/issues/341 -.. _342: https://github.com/giampaolo/psutil/issues/342 -.. _343: https://github.com/giampaolo/psutil/issues/343 -.. _344: https://github.com/giampaolo/psutil/issues/344 -.. _345: https://github.com/giampaolo/psutil/issues/345 -.. _346: https://github.com/giampaolo/psutil/issues/346 -.. _347: https://github.com/giampaolo/psutil/issues/347 -.. _348: https://github.com/giampaolo/psutil/issues/348 -.. _349: https://github.com/giampaolo/psutil/issues/349 -.. _350: https://github.com/giampaolo/psutil/issues/350 -.. _351: https://github.com/giampaolo/psutil/issues/351 -.. _352: https://github.com/giampaolo/psutil/issues/352 -.. _353: https://github.com/giampaolo/psutil/issues/353 -.. _354: https://github.com/giampaolo/psutil/issues/354 -.. _355: https://github.com/giampaolo/psutil/issues/355 -.. _356: https://github.com/giampaolo/psutil/issues/356 -.. _357: https://github.com/giampaolo/psutil/issues/357 -.. _358: https://github.com/giampaolo/psutil/issues/358 -.. _359: https://github.com/giampaolo/psutil/issues/359 -.. _360: https://github.com/giampaolo/psutil/issues/360 -.. _361: https://github.com/giampaolo/psutil/issues/361 -.. _362: https://github.com/giampaolo/psutil/issues/362 -.. _363: https://github.com/giampaolo/psutil/issues/363 -.. _364: https://github.com/giampaolo/psutil/issues/364 -.. _365: https://github.com/giampaolo/psutil/issues/365 -.. _366: https://github.com/giampaolo/psutil/issues/366 -.. _367: https://github.com/giampaolo/psutil/issues/367 -.. _368: https://github.com/giampaolo/psutil/issues/368 -.. _369: https://github.com/giampaolo/psutil/issues/369 -.. _370: https://github.com/giampaolo/psutil/issues/370 -.. _371: https://github.com/giampaolo/psutil/issues/371 -.. _372: https://github.com/giampaolo/psutil/issues/372 -.. _373: https://github.com/giampaolo/psutil/issues/373 -.. _374: https://github.com/giampaolo/psutil/issues/374 -.. _375: https://github.com/giampaolo/psutil/issues/375 -.. _376: https://github.com/giampaolo/psutil/issues/376 -.. _377: https://github.com/giampaolo/psutil/issues/377 -.. _378: https://github.com/giampaolo/psutil/issues/378 -.. _379: https://github.com/giampaolo/psutil/issues/379 -.. _380: https://github.com/giampaolo/psutil/issues/380 -.. _381: https://github.com/giampaolo/psutil/issues/381 -.. _382: https://github.com/giampaolo/psutil/issues/382 -.. _383: https://github.com/giampaolo/psutil/issues/383 -.. _384: https://github.com/giampaolo/psutil/issues/384 -.. _385: https://github.com/giampaolo/psutil/issues/385 -.. _386: https://github.com/giampaolo/psutil/issues/386 -.. _387: https://github.com/giampaolo/psutil/issues/387 -.. _388: https://github.com/giampaolo/psutil/issues/388 -.. _389: https://github.com/giampaolo/psutil/issues/389 -.. _390: https://github.com/giampaolo/psutil/issues/390 -.. _391: https://github.com/giampaolo/psutil/issues/391 -.. _392: https://github.com/giampaolo/psutil/issues/392 -.. _393: https://github.com/giampaolo/psutil/issues/393 -.. _394: https://github.com/giampaolo/psutil/issues/394 -.. _395: https://github.com/giampaolo/psutil/issues/395 -.. _396: https://github.com/giampaolo/psutil/issues/396 -.. _397: https://github.com/giampaolo/psutil/issues/397 -.. _398: https://github.com/giampaolo/psutil/issues/398 -.. _399: https://github.com/giampaolo/psutil/issues/399 -.. _400: https://github.com/giampaolo/psutil/issues/400 -.. _401: https://github.com/giampaolo/psutil/issues/401 -.. _402: https://github.com/giampaolo/psutil/issues/402 -.. _403: https://github.com/giampaolo/psutil/issues/403 -.. _404: https://github.com/giampaolo/psutil/issues/404 -.. _405: https://github.com/giampaolo/psutil/issues/405 -.. _406: https://github.com/giampaolo/psutil/issues/406 -.. _407: https://github.com/giampaolo/psutil/issues/407 -.. _408: https://github.com/giampaolo/psutil/issues/408 -.. _409: https://github.com/giampaolo/psutil/issues/409 -.. _410: https://github.com/giampaolo/psutil/issues/410 -.. _411: https://github.com/giampaolo/psutil/issues/411 -.. _412: https://github.com/giampaolo/psutil/issues/412 -.. _413: https://github.com/giampaolo/psutil/issues/413 -.. _414: https://github.com/giampaolo/psutil/issues/414 -.. _415: https://github.com/giampaolo/psutil/issues/415 -.. _416: https://github.com/giampaolo/psutil/issues/416 -.. _417: https://github.com/giampaolo/psutil/issues/417 -.. _418: https://github.com/giampaolo/psutil/issues/418 -.. _419: https://github.com/giampaolo/psutil/issues/419 -.. _420: https://github.com/giampaolo/psutil/issues/420 -.. _421: https://github.com/giampaolo/psutil/issues/421 -.. _422: https://github.com/giampaolo/psutil/issues/422 -.. _423: https://github.com/giampaolo/psutil/issues/423 -.. _424: https://github.com/giampaolo/psutil/issues/424 -.. _425: https://github.com/giampaolo/psutil/issues/425 -.. _426: https://github.com/giampaolo/psutil/issues/426 -.. _427: https://github.com/giampaolo/psutil/issues/427 -.. _428: https://github.com/giampaolo/psutil/issues/428 -.. _429: https://github.com/giampaolo/psutil/issues/429 -.. _430: https://github.com/giampaolo/psutil/issues/430 -.. _431: https://github.com/giampaolo/psutil/issues/431 -.. _432: https://github.com/giampaolo/psutil/issues/432 -.. _433: https://github.com/giampaolo/psutil/issues/433 -.. _434: https://github.com/giampaolo/psutil/issues/434 -.. _435: https://github.com/giampaolo/psutil/issues/435 -.. _436: https://github.com/giampaolo/psutil/issues/436 -.. _437: https://github.com/giampaolo/psutil/issues/437 -.. _438: https://github.com/giampaolo/psutil/issues/438 -.. _439: https://github.com/giampaolo/psutil/issues/439 -.. _440: https://github.com/giampaolo/psutil/issues/440 -.. _441: https://github.com/giampaolo/psutil/issues/441 -.. _442: https://github.com/giampaolo/psutil/issues/442 -.. _443: https://github.com/giampaolo/psutil/issues/443 -.. _444: https://github.com/giampaolo/psutil/issues/444 -.. _445: https://github.com/giampaolo/psutil/issues/445 -.. _446: https://github.com/giampaolo/psutil/issues/446 -.. _447: https://github.com/giampaolo/psutil/issues/447 -.. _448: https://github.com/giampaolo/psutil/issues/448 -.. _449: https://github.com/giampaolo/psutil/issues/449 -.. _450: https://github.com/giampaolo/psutil/issues/450 -.. _451: https://github.com/giampaolo/psutil/issues/451 -.. _452: https://github.com/giampaolo/psutil/issues/452 -.. _453: https://github.com/giampaolo/psutil/issues/453 -.. _454: https://github.com/giampaolo/psutil/issues/454 -.. _455: https://github.com/giampaolo/psutil/issues/455 -.. _456: https://github.com/giampaolo/psutil/issues/456 -.. _457: https://github.com/giampaolo/psutil/issues/457 -.. _458: https://github.com/giampaolo/psutil/issues/458 -.. _459: https://github.com/giampaolo/psutil/issues/459 -.. _460: https://github.com/giampaolo/psutil/issues/460 -.. _461: https://github.com/giampaolo/psutil/issues/461 -.. _462: https://github.com/giampaolo/psutil/issues/462 -.. _463: https://github.com/giampaolo/psutil/issues/463 -.. _464: https://github.com/giampaolo/psutil/issues/464 -.. _465: https://github.com/giampaolo/psutil/issues/465 -.. _466: https://github.com/giampaolo/psutil/issues/466 -.. _467: https://github.com/giampaolo/psutil/issues/467 -.. _468: https://github.com/giampaolo/psutil/issues/468 -.. _469: https://github.com/giampaolo/psutil/issues/469 -.. _470: https://github.com/giampaolo/psutil/issues/470 -.. _471: https://github.com/giampaolo/psutil/issues/471 -.. _472: https://github.com/giampaolo/psutil/issues/472 -.. _473: https://github.com/giampaolo/psutil/issues/473 -.. _474: https://github.com/giampaolo/psutil/issues/474 -.. _475: https://github.com/giampaolo/psutil/issues/475 -.. _476: https://github.com/giampaolo/psutil/issues/476 -.. _477: https://github.com/giampaolo/psutil/issues/477 -.. _478: https://github.com/giampaolo/psutil/issues/478 -.. _479: https://github.com/giampaolo/psutil/issues/479 -.. _480: https://github.com/giampaolo/psutil/issues/480 -.. _481: https://github.com/giampaolo/psutil/issues/481 -.. _482: https://github.com/giampaolo/psutil/issues/482 -.. _483: https://github.com/giampaolo/psutil/issues/483 -.. _484: https://github.com/giampaolo/psutil/issues/484 -.. _485: https://github.com/giampaolo/psutil/issues/485 -.. _486: https://github.com/giampaolo/psutil/issues/486 -.. _487: https://github.com/giampaolo/psutil/issues/487 -.. _488: https://github.com/giampaolo/psutil/issues/488 -.. _489: https://github.com/giampaolo/psutil/issues/489 -.. _490: https://github.com/giampaolo/psutil/issues/490 -.. _491: https://github.com/giampaolo/psutil/issues/491 -.. _492: https://github.com/giampaolo/psutil/issues/492 -.. _493: https://github.com/giampaolo/psutil/issues/493 -.. _494: https://github.com/giampaolo/psutil/issues/494 -.. _495: https://github.com/giampaolo/psutil/issues/495 -.. _496: https://github.com/giampaolo/psutil/issues/496 -.. _497: https://github.com/giampaolo/psutil/issues/497 -.. _498: https://github.com/giampaolo/psutil/issues/498 -.. _499: https://github.com/giampaolo/psutil/issues/499 -.. _500: https://github.com/giampaolo/psutil/issues/500 -.. _501: https://github.com/giampaolo/psutil/issues/501 -.. _502: https://github.com/giampaolo/psutil/issues/502 -.. _503: https://github.com/giampaolo/psutil/issues/503 -.. _504: https://github.com/giampaolo/psutil/issues/504 -.. _505: https://github.com/giampaolo/psutil/issues/505 -.. _506: https://github.com/giampaolo/psutil/issues/506 -.. _507: https://github.com/giampaolo/psutil/issues/507 -.. _508: https://github.com/giampaolo/psutil/issues/508 -.. _509: https://github.com/giampaolo/psutil/issues/509 -.. _510: https://github.com/giampaolo/psutil/issues/510 -.. _511: https://github.com/giampaolo/psutil/issues/511 -.. _512: https://github.com/giampaolo/psutil/issues/512 -.. _513: https://github.com/giampaolo/psutil/issues/513 -.. _514: https://github.com/giampaolo/psutil/issues/514 -.. _515: https://github.com/giampaolo/psutil/issues/515 -.. _516: https://github.com/giampaolo/psutil/issues/516 -.. _517: https://github.com/giampaolo/psutil/issues/517 -.. _518: https://github.com/giampaolo/psutil/issues/518 -.. _519: https://github.com/giampaolo/psutil/issues/519 -.. _520: https://github.com/giampaolo/psutil/issues/520 -.. _521: https://github.com/giampaolo/psutil/issues/521 -.. _522: https://github.com/giampaolo/psutil/issues/522 -.. _523: https://github.com/giampaolo/psutil/issues/523 -.. _524: https://github.com/giampaolo/psutil/issues/524 -.. _525: https://github.com/giampaolo/psutil/issues/525 -.. _526: https://github.com/giampaolo/psutil/issues/526 -.. _527: https://github.com/giampaolo/psutil/issues/527 -.. _528: https://github.com/giampaolo/psutil/issues/528 -.. _529: https://github.com/giampaolo/psutil/issues/529 -.. _530: https://github.com/giampaolo/psutil/issues/530 -.. _531: https://github.com/giampaolo/psutil/issues/531 -.. _532: https://github.com/giampaolo/psutil/issues/532 -.. _533: https://github.com/giampaolo/psutil/issues/533 -.. _534: https://github.com/giampaolo/psutil/issues/534 -.. _535: https://github.com/giampaolo/psutil/issues/535 -.. _536: https://github.com/giampaolo/psutil/issues/536 -.. _537: https://github.com/giampaolo/psutil/issues/537 -.. _538: https://github.com/giampaolo/psutil/issues/538 -.. _539: https://github.com/giampaolo/psutil/issues/539 -.. _540: https://github.com/giampaolo/psutil/issues/540 -.. _541: https://github.com/giampaolo/psutil/issues/541 -.. _542: https://github.com/giampaolo/psutil/issues/542 -.. _543: https://github.com/giampaolo/psutil/issues/543 -.. _544: https://github.com/giampaolo/psutil/issues/544 -.. _545: https://github.com/giampaolo/psutil/issues/545 -.. _546: https://github.com/giampaolo/psutil/issues/546 -.. _547: https://github.com/giampaolo/psutil/issues/547 -.. _548: https://github.com/giampaolo/psutil/issues/548 -.. _549: https://github.com/giampaolo/psutil/issues/549 -.. _550: https://github.com/giampaolo/psutil/issues/550 -.. _551: https://github.com/giampaolo/psutil/issues/551 -.. _552: https://github.com/giampaolo/psutil/issues/552 -.. _553: https://github.com/giampaolo/psutil/issues/553 -.. _554: https://github.com/giampaolo/psutil/issues/554 -.. _555: https://github.com/giampaolo/psutil/issues/555 -.. _556: https://github.com/giampaolo/psutil/issues/556 -.. _557: https://github.com/giampaolo/psutil/issues/557 -.. _558: https://github.com/giampaolo/psutil/issues/558 -.. _559: https://github.com/giampaolo/psutil/issues/559 -.. _560: https://github.com/giampaolo/psutil/issues/560 -.. _561: https://github.com/giampaolo/psutil/issues/561 -.. _562: https://github.com/giampaolo/psutil/issues/562 -.. _563: https://github.com/giampaolo/psutil/issues/563 -.. _564: https://github.com/giampaolo/psutil/issues/564 -.. _565: https://github.com/giampaolo/psutil/issues/565 -.. _566: https://github.com/giampaolo/psutil/issues/566 -.. _567: https://github.com/giampaolo/psutil/issues/567 -.. _568: https://github.com/giampaolo/psutil/issues/568 -.. _569: https://github.com/giampaolo/psutil/issues/569 -.. _570: https://github.com/giampaolo/psutil/issues/570 -.. _571: https://github.com/giampaolo/psutil/issues/571 -.. _572: https://github.com/giampaolo/psutil/issues/572 -.. _573: https://github.com/giampaolo/psutil/issues/573 -.. _574: https://github.com/giampaolo/psutil/issues/574 -.. _575: https://github.com/giampaolo/psutil/issues/575 -.. _576: https://github.com/giampaolo/psutil/issues/576 -.. _577: https://github.com/giampaolo/psutil/issues/577 -.. _578: https://github.com/giampaolo/psutil/issues/578 -.. _579: https://github.com/giampaolo/psutil/issues/579 -.. _580: https://github.com/giampaolo/psutil/issues/580 -.. _581: https://github.com/giampaolo/psutil/issues/581 -.. _582: https://github.com/giampaolo/psutil/issues/582 -.. _583: https://github.com/giampaolo/psutil/issues/583 -.. _584: https://github.com/giampaolo/psutil/issues/584 -.. _585: https://github.com/giampaolo/psutil/issues/585 -.. _586: https://github.com/giampaolo/psutil/issues/586 -.. _587: https://github.com/giampaolo/psutil/issues/587 -.. _588: https://github.com/giampaolo/psutil/issues/588 -.. _589: https://github.com/giampaolo/psutil/issues/589 -.. _590: https://github.com/giampaolo/psutil/issues/590 -.. _591: https://github.com/giampaolo/psutil/issues/591 -.. _592: https://github.com/giampaolo/psutil/issues/592 -.. _593: https://github.com/giampaolo/psutil/issues/593 -.. _594: https://github.com/giampaolo/psutil/issues/594 -.. _595: https://github.com/giampaolo/psutil/issues/595 -.. _596: https://github.com/giampaolo/psutil/issues/596 -.. _597: https://github.com/giampaolo/psutil/issues/597 -.. _598: https://github.com/giampaolo/psutil/issues/598 -.. _599: https://github.com/giampaolo/psutil/issues/599 -.. _600: https://github.com/giampaolo/psutil/issues/600 -.. _601: https://github.com/giampaolo/psutil/issues/601 -.. _602: https://github.com/giampaolo/psutil/issues/602 -.. _603: https://github.com/giampaolo/psutil/issues/603 -.. _604: https://github.com/giampaolo/psutil/issues/604 -.. _605: https://github.com/giampaolo/psutil/issues/605 -.. _606: https://github.com/giampaolo/psutil/issues/606 -.. _607: https://github.com/giampaolo/psutil/issues/607 -.. _608: https://github.com/giampaolo/psutil/issues/608 -.. _609: https://github.com/giampaolo/psutil/issues/609 -.. _610: https://github.com/giampaolo/psutil/issues/610 -.. _611: https://github.com/giampaolo/psutil/issues/611 -.. _612: https://github.com/giampaolo/psutil/issues/612 -.. _613: https://github.com/giampaolo/psutil/issues/613 -.. _614: https://github.com/giampaolo/psutil/issues/614 -.. _615: https://github.com/giampaolo/psutil/issues/615 -.. _616: https://github.com/giampaolo/psutil/issues/616 -.. _617: https://github.com/giampaolo/psutil/issues/617 -.. _618: https://github.com/giampaolo/psutil/issues/618 -.. _619: https://github.com/giampaolo/psutil/issues/619 -.. _620: https://github.com/giampaolo/psutil/issues/620 -.. _621: https://github.com/giampaolo/psutil/issues/621 -.. _622: https://github.com/giampaolo/psutil/issues/622 -.. _623: https://github.com/giampaolo/psutil/issues/623 -.. _624: https://github.com/giampaolo/psutil/issues/624 -.. _625: https://github.com/giampaolo/psutil/issues/625 -.. _626: https://github.com/giampaolo/psutil/issues/626 -.. _627: https://github.com/giampaolo/psutil/issues/627 -.. _628: https://github.com/giampaolo/psutil/issues/628 -.. _629: https://github.com/giampaolo/psutil/issues/629 -.. _630: https://github.com/giampaolo/psutil/issues/630 -.. _631: https://github.com/giampaolo/psutil/issues/631 -.. _632: https://github.com/giampaolo/psutil/issues/632 -.. _633: https://github.com/giampaolo/psutil/issues/633 -.. _634: https://github.com/giampaolo/psutil/issues/634 -.. _635: https://github.com/giampaolo/psutil/issues/635 -.. _636: https://github.com/giampaolo/psutil/issues/636 -.. _637: https://github.com/giampaolo/psutil/issues/637 -.. _638: https://github.com/giampaolo/psutil/issues/638 -.. _639: https://github.com/giampaolo/psutil/issues/639 -.. _640: https://github.com/giampaolo/psutil/issues/640 -.. _641: https://github.com/giampaolo/psutil/issues/641 -.. _642: https://github.com/giampaolo/psutil/issues/642 -.. _643: https://github.com/giampaolo/psutil/issues/643 -.. _644: https://github.com/giampaolo/psutil/issues/644 -.. _645: https://github.com/giampaolo/psutil/issues/645 -.. _646: https://github.com/giampaolo/psutil/issues/646 -.. _647: https://github.com/giampaolo/psutil/issues/647 -.. _648: https://github.com/giampaolo/psutil/issues/648 -.. _649: https://github.com/giampaolo/psutil/issues/649 -.. _650: https://github.com/giampaolo/psutil/issues/650 -.. _651: https://github.com/giampaolo/psutil/issues/651 -.. _652: https://github.com/giampaolo/psutil/issues/652 -.. _653: https://github.com/giampaolo/psutil/issues/653 -.. _654: https://github.com/giampaolo/psutil/issues/654 -.. _655: https://github.com/giampaolo/psutil/issues/655 -.. _656: https://github.com/giampaolo/psutil/issues/656 -.. _657: https://github.com/giampaolo/psutil/issues/657 -.. _658: https://github.com/giampaolo/psutil/issues/658 -.. _659: https://github.com/giampaolo/psutil/issues/659 -.. _660: https://github.com/giampaolo/psutil/issues/660 -.. _661: https://github.com/giampaolo/psutil/issues/661 -.. _662: https://github.com/giampaolo/psutil/issues/662 -.. _663: https://github.com/giampaolo/psutil/issues/663 -.. _664: https://github.com/giampaolo/psutil/issues/664 -.. _665: https://github.com/giampaolo/psutil/issues/665 -.. _666: https://github.com/giampaolo/psutil/issues/666 -.. _667: https://github.com/giampaolo/psutil/issues/667 -.. _668: https://github.com/giampaolo/psutil/issues/668 -.. _669: https://github.com/giampaolo/psutil/issues/669 -.. _670: https://github.com/giampaolo/psutil/issues/670 -.. _671: https://github.com/giampaolo/psutil/issues/671 -.. _672: https://github.com/giampaolo/psutil/issues/672 -.. _673: https://github.com/giampaolo/psutil/issues/673 -.. _674: https://github.com/giampaolo/psutil/issues/674 -.. _675: https://github.com/giampaolo/psutil/issues/675 -.. _676: https://github.com/giampaolo/psutil/issues/676 -.. _677: https://github.com/giampaolo/psutil/issues/677 -.. _678: https://github.com/giampaolo/psutil/issues/678 -.. _679: https://github.com/giampaolo/psutil/issues/679 -.. _680: https://github.com/giampaolo/psutil/issues/680 -.. _681: https://github.com/giampaolo/psutil/issues/681 -.. _682: https://github.com/giampaolo/psutil/issues/682 -.. _683: https://github.com/giampaolo/psutil/issues/683 -.. _684: https://github.com/giampaolo/psutil/issues/684 -.. _685: https://github.com/giampaolo/psutil/issues/685 -.. _686: https://github.com/giampaolo/psutil/issues/686 -.. _687: https://github.com/giampaolo/psutil/issues/687 -.. _688: https://github.com/giampaolo/psutil/issues/688 -.. _689: https://github.com/giampaolo/psutil/issues/689 -.. _690: https://github.com/giampaolo/psutil/issues/690 -.. _691: https://github.com/giampaolo/psutil/issues/691 -.. _692: https://github.com/giampaolo/psutil/issues/692 -.. _693: https://github.com/giampaolo/psutil/issues/693 -.. _694: https://github.com/giampaolo/psutil/issues/694 -.. _695: https://github.com/giampaolo/psutil/issues/695 -.. _696: https://github.com/giampaolo/psutil/issues/696 -.. _697: https://github.com/giampaolo/psutil/issues/697 -.. _698: https://github.com/giampaolo/psutil/issues/698 -.. _699: https://github.com/giampaolo/psutil/issues/699 -.. _700: https://github.com/giampaolo/psutil/issues/700 -.. _701: https://github.com/giampaolo/psutil/issues/701 -.. _702: https://github.com/giampaolo/psutil/issues/702 -.. _703: https://github.com/giampaolo/psutil/issues/703 -.. _704: https://github.com/giampaolo/psutil/issues/704 -.. _705: https://github.com/giampaolo/psutil/issues/705 -.. _706: https://github.com/giampaolo/psutil/issues/706 -.. _707: https://github.com/giampaolo/psutil/issues/707 -.. _708: https://github.com/giampaolo/psutil/issues/708 -.. _709: https://github.com/giampaolo/psutil/issues/709 -.. _710: https://github.com/giampaolo/psutil/issues/710 -.. _711: https://github.com/giampaolo/psutil/issues/711 -.. _712: https://github.com/giampaolo/psutil/issues/712 -.. _713: https://github.com/giampaolo/psutil/issues/713 -.. _714: https://github.com/giampaolo/psutil/issues/714 -.. _715: https://github.com/giampaolo/psutil/issues/715 -.. _716: https://github.com/giampaolo/psutil/issues/716 -.. _717: https://github.com/giampaolo/psutil/issues/717 -.. _718: https://github.com/giampaolo/psutil/issues/718 -.. _719: https://github.com/giampaolo/psutil/issues/719 -.. _720: https://github.com/giampaolo/psutil/issues/720 -.. _721: https://github.com/giampaolo/psutil/issues/721 -.. _722: https://github.com/giampaolo/psutil/issues/722 -.. _723: https://github.com/giampaolo/psutil/issues/723 -.. _724: https://github.com/giampaolo/psutil/issues/724 -.. _725: https://github.com/giampaolo/psutil/issues/725 -.. _726: https://github.com/giampaolo/psutil/issues/726 -.. _727: https://github.com/giampaolo/psutil/issues/727 -.. _728: https://github.com/giampaolo/psutil/issues/728 -.. _729: https://github.com/giampaolo/psutil/issues/729 -.. _730: https://github.com/giampaolo/psutil/issues/730 -.. _731: https://github.com/giampaolo/psutil/issues/731 -.. _732: https://github.com/giampaolo/psutil/issues/732 -.. _733: https://github.com/giampaolo/psutil/issues/733 -.. _734: https://github.com/giampaolo/psutil/issues/734 -.. _735: https://github.com/giampaolo/psutil/issues/735 -.. _736: https://github.com/giampaolo/psutil/issues/736 -.. _737: https://github.com/giampaolo/psutil/issues/737 -.. _738: https://github.com/giampaolo/psutil/issues/738 -.. _739: https://github.com/giampaolo/psutil/issues/739 -.. _740: https://github.com/giampaolo/psutil/issues/740 -.. _741: https://github.com/giampaolo/psutil/issues/741 -.. _742: https://github.com/giampaolo/psutil/issues/742 -.. _743: https://github.com/giampaolo/psutil/issues/743 -.. _744: https://github.com/giampaolo/psutil/issues/744 -.. _745: https://github.com/giampaolo/psutil/issues/745 -.. _746: https://github.com/giampaolo/psutil/issues/746 -.. _747: https://github.com/giampaolo/psutil/issues/747 -.. _748: https://github.com/giampaolo/psutil/issues/748 -.. _749: https://github.com/giampaolo/psutil/issues/749 -.. _750: https://github.com/giampaolo/psutil/issues/750 -.. _751: https://github.com/giampaolo/psutil/issues/751 -.. _752: https://github.com/giampaolo/psutil/issues/752 -.. _753: https://github.com/giampaolo/psutil/issues/753 -.. _754: https://github.com/giampaolo/psutil/issues/754 -.. _755: https://github.com/giampaolo/psutil/issues/755 -.. _756: https://github.com/giampaolo/psutil/issues/756 -.. _757: https://github.com/giampaolo/psutil/issues/757 -.. _758: https://github.com/giampaolo/psutil/issues/758 -.. _759: https://github.com/giampaolo/psutil/issues/759 -.. _760: https://github.com/giampaolo/psutil/issues/760 -.. _761: https://github.com/giampaolo/psutil/issues/761 -.. _762: https://github.com/giampaolo/psutil/issues/762 -.. _763: https://github.com/giampaolo/psutil/issues/763 -.. _764: https://github.com/giampaolo/psutil/issues/764 -.. _765: https://github.com/giampaolo/psutil/issues/765 -.. _766: https://github.com/giampaolo/psutil/issues/766 -.. _767: https://github.com/giampaolo/psutil/issues/767 -.. _768: https://github.com/giampaolo/psutil/issues/768 -.. _769: https://github.com/giampaolo/psutil/issues/769 -.. _770: https://github.com/giampaolo/psutil/issues/770 -.. _771: https://github.com/giampaolo/psutil/issues/771 -.. _772: https://github.com/giampaolo/psutil/issues/772 -.. _773: https://github.com/giampaolo/psutil/issues/773 -.. _774: https://github.com/giampaolo/psutil/issues/774 -.. _775: https://github.com/giampaolo/psutil/issues/775 -.. _776: https://github.com/giampaolo/psutil/issues/776 -.. _777: https://github.com/giampaolo/psutil/issues/777 -.. _778: https://github.com/giampaolo/psutil/issues/778 -.. _779: https://github.com/giampaolo/psutil/issues/779 -.. _780: https://github.com/giampaolo/psutil/issues/780 -.. _781: https://github.com/giampaolo/psutil/issues/781 -.. _782: https://github.com/giampaolo/psutil/issues/782 -.. _783: https://github.com/giampaolo/psutil/issues/783 -.. _784: https://github.com/giampaolo/psutil/issues/784 -.. _785: https://github.com/giampaolo/psutil/issues/785 -.. _786: https://github.com/giampaolo/psutil/issues/786 -.. _787: https://github.com/giampaolo/psutil/issues/787 -.. _788: https://github.com/giampaolo/psutil/issues/788 -.. _789: https://github.com/giampaolo/psutil/issues/789 -.. _790: https://github.com/giampaolo/psutil/issues/790 -.. _791: https://github.com/giampaolo/psutil/issues/791 -.. _792: https://github.com/giampaolo/psutil/issues/792 -.. _793: https://github.com/giampaolo/psutil/issues/793 -.. _794: https://github.com/giampaolo/psutil/issues/794 -.. _795: https://github.com/giampaolo/psutil/issues/795 -.. _796: https://github.com/giampaolo/psutil/issues/796 -.. _797: https://github.com/giampaolo/psutil/issues/797 -.. _798: https://github.com/giampaolo/psutil/issues/798 -.. _799: https://github.com/giampaolo/psutil/issues/799 -.. _800: https://github.com/giampaolo/psutil/issues/800 -.. _801: https://github.com/giampaolo/psutil/issues/801 -.. _802: https://github.com/giampaolo/psutil/issues/802 -.. _803: https://github.com/giampaolo/psutil/issues/803 -.. _804: https://github.com/giampaolo/psutil/issues/804 -.. _805: https://github.com/giampaolo/psutil/issues/805 -.. _806: https://github.com/giampaolo/psutil/issues/806 -.. _807: https://github.com/giampaolo/psutil/issues/807 -.. _808: https://github.com/giampaolo/psutil/issues/808 -.. _809: https://github.com/giampaolo/psutil/issues/809 -.. _810: https://github.com/giampaolo/psutil/issues/810 -.. _811: https://github.com/giampaolo/psutil/issues/811 -.. _812: https://github.com/giampaolo/psutil/issues/812 -.. _813: https://github.com/giampaolo/psutil/issues/813 -.. _814: https://github.com/giampaolo/psutil/issues/814 -.. _815: https://github.com/giampaolo/psutil/issues/815 -.. _816: https://github.com/giampaolo/psutil/issues/816 -.. _817: https://github.com/giampaolo/psutil/issues/817 -.. _818: https://github.com/giampaolo/psutil/issues/818 -.. _819: https://github.com/giampaolo/psutil/issues/819 -.. _820: https://github.com/giampaolo/psutil/issues/820 -.. _821: https://github.com/giampaolo/psutil/issues/821 -.. _822: https://github.com/giampaolo/psutil/issues/822 -.. _823: https://github.com/giampaolo/psutil/issues/823 -.. _824: https://github.com/giampaolo/psutil/issues/824 -.. _825: https://github.com/giampaolo/psutil/issues/825 -.. _826: https://github.com/giampaolo/psutil/issues/826 -.. _827: https://github.com/giampaolo/psutil/issues/827 -.. _828: https://github.com/giampaolo/psutil/issues/828 -.. _829: https://github.com/giampaolo/psutil/issues/829 -.. _830: https://github.com/giampaolo/psutil/issues/830 -.. _831: https://github.com/giampaolo/psutil/issues/831 -.. _832: https://github.com/giampaolo/psutil/issues/832 -.. _833: https://github.com/giampaolo/psutil/issues/833 -.. _834: https://github.com/giampaolo/psutil/issues/834 -.. _835: https://github.com/giampaolo/psutil/issues/835 -.. _836: https://github.com/giampaolo/psutil/issues/836 -.. _837: https://github.com/giampaolo/psutil/issues/837 -.. _838: https://github.com/giampaolo/psutil/issues/838 -.. _839: https://github.com/giampaolo/psutil/issues/839 -.. _840: https://github.com/giampaolo/psutil/issues/840 -.. _841: https://github.com/giampaolo/psutil/issues/841 -.. _842: https://github.com/giampaolo/psutil/issues/842 -.. _843: https://github.com/giampaolo/psutil/issues/843 -.. _844: https://github.com/giampaolo/psutil/issues/844 -.. _845: https://github.com/giampaolo/psutil/issues/845 -.. _846: https://github.com/giampaolo/psutil/issues/846 -.. _847: https://github.com/giampaolo/psutil/issues/847 -.. _848: https://github.com/giampaolo/psutil/issues/848 -.. _849: https://github.com/giampaolo/psutil/issues/849 -.. _850: https://github.com/giampaolo/psutil/issues/850 -.. _851: https://github.com/giampaolo/psutil/issues/851 -.. _852: https://github.com/giampaolo/psutil/issues/852 -.. _853: https://github.com/giampaolo/psutil/issues/853 -.. _854: https://github.com/giampaolo/psutil/issues/854 -.. _855: https://github.com/giampaolo/psutil/issues/855 -.. _856: https://github.com/giampaolo/psutil/issues/856 -.. _857: https://github.com/giampaolo/psutil/issues/857 -.. _858: https://github.com/giampaolo/psutil/issues/858 -.. _859: https://github.com/giampaolo/psutil/issues/859 -.. _860: https://github.com/giampaolo/psutil/issues/860 -.. _861: https://github.com/giampaolo/psutil/issues/861 -.. _862: https://github.com/giampaolo/psutil/issues/862 -.. _863: https://github.com/giampaolo/psutil/issues/863 -.. _864: https://github.com/giampaolo/psutil/issues/864 -.. _865: https://github.com/giampaolo/psutil/issues/865 -.. _866: https://github.com/giampaolo/psutil/issues/866 -.. _867: https://github.com/giampaolo/psutil/issues/867 -.. _868: https://github.com/giampaolo/psutil/issues/868 -.. _869: https://github.com/giampaolo/psutil/issues/869 -.. _870: https://github.com/giampaolo/psutil/issues/870 -.. _871: https://github.com/giampaolo/psutil/issues/871 -.. _872: https://github.com/giampaolo/psutil/issues/872 -.. _873: https://github.com/giampaolo/psutil/issues/873 -.. _874: https://github.com/giampaolo/psutil/issues/874 -.. _875: https://github.com/giampaolo/psutil/issues/875 -.. _876: https://github.com/giampaolo/psutil/issues/876 -.. _877: https://github.com/giampaolo/psutil/issues/877 -.. _878: https://github.com/giampaolo/psutil/issues/878 -.. _879: https://github.com/giampaolo/psutil/issues/879 -.. _880: https://github.com/giampaolo/psutil/issues/880 -.. _881: https://github.com/giampaolo/psutil/issues/881 -.. _882: https://github.com/giampaolo/psutil/issues/882 -.. _883: https://github.com/giampaolo/psutil/issues/883 -.. _884: https://github.com/giampaolo/psutil/issues/884 -.. _885: https://github.com/giampaolo/psutil/issues/885 -.. _886: https://github.com/giampaolo/psutil/issues/886 -.. _887: https://github.com/giampaolo/psutil/issues/887 -.. _888: https://github.com/giampaolo/psutil/issues/888 -.. _889: https://github.com/giampaolo/psutil/issues/889 -.. _890: https://github.com/giampaolo/psutil/issues/890 -.. _891: https://github.com/giampaolo/psutil/issues/891 -.. _892: https://github.com/giampaolo/psutil/issues/892 -.. _893: https://github.com/giampaolo/psutil/issues/893 -.. _894: https://github.com/giampaolo/psutil/issues/894 -.. _895: https://github.com/giampaolo/psutil/issues/895 -.. _896: https://github.com/giampaolo/psutil/issues/896 -.. _897: https://github.com/giampaolo/psutil/issues/897 -.. _898: https://github.com/giampaolo/psutil/issues/898 -.. _899: https://github.com/giampaolo/psutil/issues/899 -.. _900: https://github.com/giampaolo/psutil/issues/900 -.. _901: https://github.com/giampaolo/psutil/issues/901 -.. _902: https://github.com/giampaolo/psutil/issues/902 -.. _903: https://github.com/giampaolo/psutil/issues/903 -.. _904: https://github.com/giampaolo/psutil/issues/904 -.. _905: https://github.com/giampaolo/psutil/issues/905 -.. _906: https://github.com/giampaolo/psutil/issues/906 -.. _907: https://github.com/giampaolo/psutil/issues/907 -.. _908: https://github.com/giampaolo/psutil/issues/908 -.. _909: https://github.com/giampaolo/psutil/issues/909 -.. _910: https://github.com/giampaolo/psutil/issues/910 -.. _911: https://github.com/giampaolo/psutil/issues/911 -.. _912: https://github.com/giampaolo/psutil/issues/912 -.. _913: https://github.com/giampaolo/psutil/issues/913 -.. _914: https://github.com/giampaolo/psutil/issues/914 -.. _915: https://github.com/giampaolo/psutil/issues/915 -.. _916: https://github.com/giampaolo/psutil/issues/916 -.. _917: https://github.com/giampaolo/psutil/issues/917 -.. _918: https://github.com/giampaolo/psutil/issues/918 -.. _919: https://github.com/giampaolo/psutil/issues/919 -.. _920: https://github.com/giampaolo/psutil/issues/920 -.. _921: https://github.com/giampaolo/psutil/issues/921 -.. _922: https://github.com/giampaolo/psutil/issues/922 -.. _923: https://github.com/giampaolo/psutil/issues/923 -.. _924: https://github.com/giampaolo/psutil/issues/924 -.. _925: https://github.com/giampaolo/psutil/issues/925 -.. _926: https://github.com/giampaolo/psutil/issues/926 -.. _927: https://github.com/giampaolo/psutil/issues/927 -.. _928: https://github.com/giampaolo/psutil/issues/928 -.. _929: https://github.com/giampaolo/psutil/issues/929 -.. _930: https://github.com/giampaolo/psutil/issues/930 -.. _931: https://github.com/giampaolo/psutil/issues/931 -.. _932: https://github.com/giampaolo/psutil/issues/932 -.. _933: https://github.com/giampaolo/psutil/issues/933 -.. _934: https://github.com/giampaolo/psutil/issues/934 -.. _935: https://github.com/giampaolo/psutil/issues/935 -.. _936: https://github.com/giampaolo/psutil/issues/936 -.. _937: https://github.com/giampaolo/psutil/issues/937 -.. _938: https://github.com/giampaolo/psutil/issues/938 -.. _939: https://github.com/giampaolo/psutil/issues/939 -.. _940: https://github.com/giampaolo/psutil/issues/940 -.. _941: https://github.com/giampaolo/psutil/issues/941 -.. _942: https://github.com/giampaolo/psutil/issues/942 -.. _943: https://github.com/giampaolo/psutil/issues/943 -.. _944: https://github.com/giampaolo/psutil/issues/944 -.. _945: https://github.com/giampaolo/psutil/issues/945 -.. _946: https://github.com/giampaolo/psutil/issues/946 -.. _947: https://github.com/giampaolo/psutil/issues/947 -.. _948: https://github.com/giampaolo/psutil/issues/948 -.. _949: https://github.com/giampaolo/psutil/issues/949 -.. _950: https://github.com/giampaolo/psutil/issues/950 -.. _951: https://github.com/giampaolo/psutil/issues/951 -.. _952: https://github.com/giampaolo/psutil/issues/952 -.. _953: https://github.com/giampaolo/psutil/issues/953 -.. _954: https://github.com/giampaolo/psutil/issues/954 -.. _955: https://github.com/giampaolo/psutil/issues/955 -.. _956: https://github.com/giampaolo/psutil/issues/956 -.. _957: https://github.com/giampaolo/psutil/issues/957 -.. _958: https://github.com/giampaolo/psutil/issues/958 -.. _959: https://github.com/giampaolo/psutil/issues/959 -.. _960: https://github.com/giampaolo/psutil/issues/960 -.. _961: https://github.com/giampaolo/psutil/issues/961 -.. _962: https://github.com/giampaolo/psutil/issues/962 -.. _963: https://github.com/giampaolo/psutil/issues/963 -.. _964: https://github.com/giampaolo/psutil/issues/964 -.. _965: https://github.com/giampaolo/psutil/issues/965 -.. _966: https://github.com/giampaolo/psutil/issues/966 -.. _967: https://github.com/giampaolo/psutil/issues/967 -.. _968: https://github.com/giampaolo/psutil/issues/968 -.. _969: https://github.com/giampaolo/psutil/issues/969 -.. _970: https://github.com/giampaolo/psutil/issues/970 -.. _971: https://github.com/giampaolo/psutil/issues/971 -.. _972: https://github.com/giampaolo/psutil/issues/972 -.. _973: https://github.com/giampaolo/psutil/issues/973 -.. _974: https://github.com/giampaolo/psutil/issues/974 -.. _975: https://github.com/giampaolo/psutil/issues/975 -.. _976: https://github.com/giampaolo/psutil/issues/976 -.. _977: https://github.com/giampaolo/psutil/issues/977 -.. _978: https://github.com/giampaolo/psutil/issues/978 -.. _979: https://github.com/giampaolo/psutil/issues/979 -.. _980: https://github.com/giampaolo/psutil/issues/980 -.. _981: https://github.com/giampaolo/psutil/issues/981 -.. _982: https://github.com/giampaolo/psutil/issues/982 -.. _983: https://github.com/giampaolo/psutil/issues/983 -.. _984: https://github.com/giampaolo/psutil/issues/984 -.. _985: https://github.com/giampaolo/psutil/issues/985 -.. _986: https://github.com/giampaolo/psutil/issues/986 -.. _987: https://github.com/giampaolo/psutil/issues/987 -.. _988: https://github.com/giampaolo/psutil/issues/988 -.. _989: https://github.com/giampaolo/psutil/issues/989 -.. _990: https://github.com/giampaolo/psutil/issues/990 -.. _991: https://github.com/giampaolo/psutil/issues/991 -.. _992: https://github.com/giampaolo/psutil/issues/992 -.. _993: https://github.com/giampaolo/psutil/issues/993 -.. _994: https://github.com/giampaolo/psutil/issues/994 -.. _995: https://github.com/giampaolo/psutil/issues/995 -.. _996: https://github.com/giampaolo/psutil/issues/996 -.. _997: https://github.com/giampaolo/psutil/issues/997 -.. _998: https://github.com/giampaolo/psutil/issues/998 -.. _999: https://github.com/giampaolo/psutil/issues/999 -.. _1000: https://github.com/giampaolo/psutil/issues/1000 -.. _1001: https://github.com/giampaolo/psutil/issues/1001 -.. _1002: https://github.com/giampaolo/psutil/issues/1002 -.. _1003: https://github.com/giampaolo/psutil/issues/1003 -.. _1004: https://github.com/giampaolo/psutil/issues/1004 -.. _1005: https://github.com/giampaolo/psutil/issues/1005 -.. _1006: https://github.com/giampaolo/psutil/issues/1006 -.. _1007: https://github.com/giampaolo/psutil/issues/1007 -.. _1008: https://github.com/giampaolo/psutil/issues/1008 -.. _1009: https://github.com/giampaolo/psutil/issues/1009 -.. _1010: https://github.com/giampaolo/psutil/issues/1010 -.. _1011: https://github.com/giampaolo/psutil/issues/1011 -.. _1012: https://github.com/giampaolo/psutil/issues/1012 -.. _1013: https://github.com/giampaolo/psutil/issues/1013 -.. _1014: https://github.com/giampaolo/psutil/issues/1014 -.. _1015: https://github.com/giampaolo/psutil/issues/1015 -.. _1016: https://github.com/giampaolo/psutil/issues/1016 -.. _1017: https://github.com/giampaolo/psutil/issues/1017 -.. _1018: https://github.com/giampaolo/psutil/issues/1018 -.. _1019: https://github.com/giampaolo/psutil/issues/1019 -.. _1020: https://github.com/giampaolo/psutil/issues/1020 -.. _1021: https://github.com/giampaolo/psutil/issues/1021 -.. _1022: https://github.com/giampaolo/psutil/issues/1022 -.. _1023: https://github.com/giampaolo/psutil/issues/1023 -.. _1024: https://github.com/giampaolo/psutil/issues/1024 -.. _1025: https://github.com/giampaolo/psutil/issues/1025 -.. _1026: https://github.com/giampaolo/psutil/issues/1026 -.. _1027: https://github.com/giampaolo/psutil/issues/1027 -.. _1028: https://github.com/giampaolo/psutil/issues/1028 -.. _1029: https://github.com/giampaolo/psutil/issues/1029 -.. _1030: https://github.com/giampaolo/psutil/issues/1030 -.. _1031: https://github.com/giampaolo/psutil/issues/1031 -.. _1032: https://github.com/giampaolo/psutil/issues/1032 -.. _1033: https://github.com/giampaolo/psutil/issues/1033 -.. _1034: https://github.com/giampaolo/psutil/issues/1034 -.. _1035: https://github.com/giampaolo/psutil/issues/1035 -.. _1036: https://github.com/giampaolo/psutil/issues/1036 -.. _1037: https://github.com/giampaolo/psutil/issues/1037 -.. _1038: https://github.com/giampaolo/psutil/issues/1038 -.. _1039: https://github.com/giampaolo/psutil/issues/1039 -.. _1040: https://github.com/giampaolo/psutil/issues/1040 -.. _1041: https://github.com/giampaolo/psutil/issues/1041 -.. _1042: https://github.com/giampaolo/psutil/issues/1042 -.. _1043: https://github.com/giampaolo/psutil/issues/1043 -.. _1044: https://github.com/giampaolo/psutil/issues/1044 -.. _1045: https://github.com/giampaolo/psutil/issues/1045 -.. _1046: https://github.com/giampaolo/psutil/issues/1046 -.. _1047: https://github.com/giampaolo/psutil/issues/1047 -.. _1048: https://github.com/giampaolo/psutil/issues/1048 -.. _1049: https://github.com/giampaolo/psutil/issues/1049 -.. _1050: https://github.com/giampaolo/psutil/issues/1050 -.. _1051: https://github.com/giampaolo/psutil/issues/1051 -.. _1052: https://github.com/giampaolo/psutil/issues/1052 -.. _1053: https://github.com/giampaolo/psutil/issues/1053 -.. _1054: https://github.com/giampaolo/psutil/issues/1054 -.. _1055: https://github.com/giampaolo/psutil/issues/1055 -.. _1056: https://github.com/giampaolo/psutil/issues/1056 -.. _1057: https://github.com/giampaolo/psutil/issues/1057 -.. _1058: https://github.com/giampaolo/psutil/issues/1058 -.. _1059: https://github.com/giampaolo/psutil/issues/1059 -.. _1060: https://github.com/giampaolo/psutil/issues/1060 -.. _1061: https://github.com/giampaolo/psutil/issues/1061 -.. _1062: https://github.com/giampaolo/psutil/issues/1062 -.. _1063: https://github.com/giampaolo/psutil/issues/1063 -.. _1064: https://github.com/giampaolo/psutil/issues/1064 -.. _1065: https://github.com/giampaolo/psutil/issues/1065 -.. _1066: https://github.com/giampaolo/psutil/issues/1066 -.. _1067: https://github.com/giampaolo/psutil/issues/1067 -.. _1068: https://github.com/giampaolo/psutil/issues/1068 -.. _1069: https://github.com/giampaolo/psutil/issues/1069 -.. _1070: https://github.com/giampaolo/psutil/issues/1070 -.. _1071: https://github.com/giampaolo/psutil/issues/1071 -.. _1072: https://github.com/giampaolo/psutil/issues/1072 -.. _1073: https://github.com/giampaolo/psutil/issues/1073 -.. _1074: https://github.com/giampaolo/psutil/issues/1074 -.. _1075: https://github.com/giampaolo/psutil/issues/1075 -.. _1076: https://github.com/giampaolo/psutil/issues/1076 -.. _1077: https://github.com/giampaolo/psutil/issues/1077 -.. _1078: https://github.com/giampaolo/psutil/issues/1078 -.. _1079: https://github.com/giampaolo/psutil/issues/1079 -.. _1080: https://github.com/giampaolo/psutil/issues/1080 -.. _1081: https://github.com/giampaolo/psutil/issues/1081 -.. _1082: https://github.com/giampaolo/psutil/issues/1082 -.. _1083: https://github.com/giampaolo/psutil/issues/1083 -.. _1084: https://github.com/giampaolo/psutil/issues/1084 -.. _1085: https://github.com/giampaolo/psutil/issues/1085 -.. _1086: https://github.com/giampaolo/psutil/issues/1086 -.. _1087: https://github.com/giampaolo/psutil/issues/1087 -.. _1088: https://github.com/giampaolo/psutil/issues/1088 -.. _1089: https://github.com/giampaolo/psutil/issues/1089 -.. _1090: https://github.com/giampaolo/psutil/issues/1090 -.. _1091: https://github.com/giampaolo/psutil/issues/1091 -.. _1092: https://github.com/giampaolo/psutil/issues/1092 -.. _1093: https://github.com/giampaolo/psutil/issues/1093 -.. _1094: https://github.com/giampaolo/psutil/issues/1094 -.. _1095: https://github.com/giampaolo/psutil/issues/1095 -.. _1096: https://github.com/giampaolo/psutil/issues/1096 -.. _1097: https://github.com/giampaolo/psutil/issues/1097 -.. _1098: https://github.com/giampaolo/psutil/issues/1098 -.. _1099: https://github.com/giampaolo/psutil/issues/1099 -.. _1100: https://github.com/giampaolo/psutil/issues/1100 -.. _1101: https://github.com/giampaolo/psutil/issues/1101 -.. _1102: https://github.com/giampaolo/psutil/issues/1102 -.. _1103: https://github.com/giampaolo/psutil/issues/1103 -.. _1104: https://github.com/giampaolo/psutil/issues/1104 -.. _1105: https://github.com/giampaolo/psutil/issues/1105 -.. _1106: https://github.com/giampaolo/psutil/issues/1106 -.. _1107: https://github.com/giampaolo/psutil/issues/1107 -.. _1108: https://github.com/giampaolo/psutil/issues/1108 -.. _1109: https://github.com/giampaolo/psutil/issues/1109 -.. _1110: https://github.com/giampaolo/psutil/issues/1110 -.. _1111: https://github.com/giampaolo/psutil/issues/1111 -.. _1112: https://github.com/giampaolo/psutil/issues/1112 -.. _1113: https://github.com/giampaolo/psutil/issues/1113 -.. _1114: https://github.com/giampaolo/psutil/issues/1114 -.. _1115: https://github.com/giampaolo/psutil/issues/1115 -.. _1116: https://github.com/giampaolo/psutil/issues/1116 -.. _1117: https://github.com/giampaolo/psutil/issues/1117 -.. _1118: https://github.com/giampaolo/psutil/issues/1118 -.. _1119: https://github.com/giampaolo/psutil/issues/1119 -.. _1120: https://github.com/giampaolo/psutil/issues/1120 -.. _1121: https://github.com/giampaolo/psutil/issues/1121 -.. _1122: https://github.com/giampaolo/psutil/issues/1122 -.. _1123: https://github.com/giampaolo/psutil/issues/1123 -.. _1124: https://github.com/giampaolo/psutil/issues/1124 -.. _1125: https://github.com/giampaolo/psutil/issues/1125 -.. _1126: https://github.com/giampaolo/psutil/issues/1126 -.. _1127: https://github.com/giampaolo/psutil/issues/1127 -.. _1128: https://github.com/giampaolo/psutil/issues/1128 -.. _1129: https://github.com/giampaolo/psutil/issues/1129 -.. _1130: https://github.com/giampaolo/psutil/issues/1130 -.. _1131: https://github.com/giampaolo/psutil/issues/1131 -.. _1132: https://github.com/giampaolo/psutil/issues/1132 -.. _1133: https://github.com/giampaolo/psutil/issues/1133 -.. _1134: https://github.com/giampaolo/psutil/issues/1134 -.. _1135: https://github.com/giampaolo/psutil/issues/1135 -.. _1136: https://github.com/giampaolo/psutil/issues/1136 -.. _1137: https://github.com/giampaolo/psutil/issues/1137 -.. _1138: https://github.com/giampaolo/psutil/issues/1138 -.. _1139: https://github.com/giampaolo/psutil/issues/1139 -.. _1140: https://github.com/giampaolo/psutil/issues/1140 -.. _1141: https://github.com/giampaolo/psutil/issues/1141 -.. _1142: https://github.com/giampaolo/psutil/issues/1142 -.. _1143: https://github.com/giampaolo/psutil/issues/1143 -.. _1144: https://github.com/giampaolo/psutil/issues/1144 -.. _1145: https://github.com/giampaolo/psutil/issues/1145 -.. _1146: https://github.com/giampaolo/psutil/issues/1146 -.. _1147: https://github.com/giampaolo/psutil/issues/1147 -.. _1148: https://github.com/giampaolo/psutil/issues/1148 -.. _1149: https://github.com/giampaolo/psutil/issues/1149 -.. _1150: https://github.com/giampaolo/psutil/issues/1150 -.. _1151: https://github.com/giampaolo/psutil/issues/1151 -.. _1152: https://github.com/giampaolo/psutil/issues/1152 -.. _1153: https://github.com/giampaolo/psutil/issues/1153 -.. _1154: https://github.com/giampaolo/psutil/issues/1154 -.. _1155: https://github.com/giampaolo/psutil/issues/1155 -.. _1156: https://github.com/giampaolo/psutil/issues/1156 -.. _1157: https://github.com/giampaolo/psutil/issues/1157 -.. _1158: https://github.com/giampaolo/psutil/issues/1158 -.. _1159: https://github.com/giampaolo/psutil/issues/1159 -.. _1160: https://github.com/giampaolo/psutil/issues/1160 -.. _1161: https://github.com/giampaolo/psutil/issues/1161 -.. _1162: https://github.com/giampaolo/psutil/issues/1162 -.. _1163: https://github.com/giampaolo/psutil/issues/1163 -.. _1164: https://github.com/giampaolo/psutil/issues/1164 -.. _1165: https://github.com/giampaolo/psutil/issues/1165 -.. _1166: https://github.com/giampaolo/psutil/issues/1166 -.. _1167: https://github.com/giampaolo/psutil/issues/1167 -.. _1168: https://github.com/giampaolo/psutil/issues/1168 -.. _1169: https://github.com/giampaolo/psutil/issues/1169 -.. _1170: https://github.com/giampaolo/psutil/issues/1170 -.. _1171: https://github.com/giampaolo/psutil/issues/1171 -.. _1172: https://github.com/giampaolo/psutil/issues/1172 -.. _1173: https://github.com/giampaolo/psutil/issues/1173 -.. _1174: https://github.com/giampaolo/psutil/issues/1174 -.. _1175: https://github.com/giampaolo/psutil/issues/1175 -.. _1176: https://github.com/giampaolo/psutil/issues/1176 -.. _1177: https://github.com/giampaolo/psutil/issues/1177 -.. _1178: https://github.com/giampaolo/psutil/issues/1178 -.. _1179: https://github.com/giampaolo/psutil/issues/1179 -.. _1180: https://github.com/giampaolo/psutil/issues/1180 -.. _1181: https://github.com/giampaolo/psutil/issues/1181 -.. _1182: https://github.com/giampaolo/psutil/issues/1182 -.. _1183: https://github.com/giampaolo/psutil/issues/1183 -.. _1184: https://github.com/giampaolo/psutil/issues/1184 -.. _1185: https://github.com/giampaolo/psutil/issues/1185 -.. _1186: https://github.com/giampaolo/psutil/issues/1186 -.. _1187: https://github.com/giampaolo/psutil/issues/1187 -.. _1188: https://github.com/giampaolo/psutil/issues/1188 -.. _1189: https://github.com/giampaolo/psutil/issues/1189 -.. _1190: https://github.com/giampaolo/psutil/issues/1190 -.. _1191: https://github.com/giampaolo/psutil/issues/1191 -.. _1192: https://github.com/giampaolo/psutil/issues/1192 -.. _1193: https://github.com/giampaolo/psutil/issues/1193 -.. _1194: https://github.com/giampaolo/psutil/issues/1194 -.. _1195: https://github.com/giampaolo/psutil/issues/1195 -.. _1196: https://github.com/giampaolo/psutil/issues/1196 -.. _1197: https://github.com/giampaolo/psutil/issues/1197 -.. _1198: https://github.com/giampaolo/psutil/issues/1198 -.. _1199: https://github.com/giampaolo/psutil/issues/1199 -.. _1200: https://github.com/giampaolo/psutil/issues/1200 -.. _1201: https://github.com/giampaolo/psutil/issues/1201 -.. _1202: https://github.com/giampaolo/psutil/issues/1202 -.. _1203: https://github.com/giampaolo/psutil/issues/1203 -.. _1204: https://github.com/giampaolo/psutil/issues/1204 -.. _1205: https://github.com/giampaolo/psutil/issues/1205 -.. _1206: https://github.com/giampaolo/psutil/issues/1206 -.. _1207: https://github.com/giampaolo/psutil/issues/1207 -.. _1208: https://github.com/giampaolo/psutil/issues/1208 -.. _1209: https://github.com/giampaolo/psutil/issues/1209 -.. _1210: https://github.com/giampaolo/psutil/issues/1210 -.. _1211: https://github.com/giampaolo/psutil/issues/1211 -.. _1212: https://github.com/giampaolo/psutil/issues/1212 -.. _1213: https://github.com/giampaolo/psutil/issues/1213 -.. _1214: https://github.com/giampaolo/psutil/issues/1214 -.. _1215: https://github.com/giampaolo/psutil/issues/1215 -.. _1216: https://github.com/giampaolo/psutil/issues/1216 -.. _1217: https://github.com/giampaolo/psutil/issues/1217 -.. _1218: https://github.com/giampaolo/psutil/issues/1218 -.. _1219: https://github.com/giampaolo/psutil/issues/1219 -.. _1220: https://github.com/giampaolo/psutil/issues/1220 -.. _1221: https://github.com/giampaolo/psutil/issues/1221 -.. _1222: https://github.com/giampaolo/psutil/issues/1222 -.. _1223: https://github.com/giampaolo/psutil/issues/1223 -.. _1224: https://github.com/giampaolo/psutil/issues/1224 -.. _1225: https://github.com/giampaolo/psutil/issues/1225 -.. _1226: https://github.com/giampaolo/psutil/issues/1226 -.. _1227: https://github.com/giampaolo/psutil/issues/1227 -.. _1228: https://github.com/giampaolo/psutil/issues/1228 -.. _1229: https://github.com/giampaolo/psutil/issues/1229 -.. _1230: https://github.com/giampaolo/psutil/issues/1230 -.. _1231: https://github.com/giampaolo/psutil/issues/1231 -.. _1232: https://github.com/giampaolo/psutil/issues/1232 -.. _1233: https://github.com/giampaolo/psutil/issues/1233 -.. _1234: https://github.com/giampaolo/psutil/issues/1234 -.. _1235: https://github.com/giampaolo/psutil/issues/1235 -.. _1236: https://github.com/giampaolo/psutil/issues/1236 -.. _1237: https://github.com/giampaolo/psutil/issues/1237 -.. _1238: https://github.com/giampaolo/psutil/issues/1238 -.. _1239: https://github.com/giampaolo/psutil/issues/1239 -.. _1240: https://github.com/giampaolo/psutil/issues/1240 -.. _1241: https://github.com/giampaolo/psutil/issues/1241 -.. _1242: https://github.com/giampaolo/psutil/issues/1242 -.. _1243: https://github.com/giampaolo/psutil/issues/1243 -.. _1244: https://github.com/giampaolo/psutil/issues/1244 -.. _1245: https://github.com/giampaolo/psutil/issues/1245 -.. _1246: https://github.com/giampaolo/psutil/issues/1246 -.. _1247: https://github.com/giampaolo/psutil/issues/1247 -.. _1248: https://github.com/giampaolo/psutil/issues/1248 -.. _1249: https://github.com/giampaolo/psutil/issues/1249 -.. _1250: https://github.com/giampaolo/psutil/issues/1250 -.. _1251: https://github.com/giampaolo/psutil/issues/1251 -.. _1252: https://github.com/giampaolo/psutil/issues/1252 -.. _1253: https://github.com/giampaolo/psutil/issues/1253 -.. _1254: https://github.com/giampaolo/psutil/issues/1254 -.. _1255: https://github.com/giampaolo/psutil/issues/1255 -.. _1256: https://github.com/giampaolo/psutil/issues/1256 -.. _1257: https://github.com/giampaolo/psutil/issues/1257 -.. _1258: https://github.com/giampaolo/psutil/issues/1258 -.. _1259: https://github.com/giampaolo/psutil/issues/1259 -.. _1260: https://github.com/giampaolo/psutil/issues/1260 -.. _1261: https://github.com/giampaolo/psutil/issues/1261 -.. _1262: https://github.com/giampaolo/psutil/issues/1262 -.. _1263: https://github.com/giampaolo/psutil/issues/1263 -.. _1264: https://github.com/giampaolo/psutil/issues/1264 -.. _1265: https://github.com/giampaolo/psutil/issues/1265 -.. _1266: https://github.com/giampaolo/psutil/issues/1266 -.. _1267: https://github.com/giampaolo/psutil/issues/1267 -.. _1268: https://github.com/giampaolo/psutil/issues/1268 -.. _1269: https://github.com/giampaolo/psutil/issues/1269 -.. _1270: https://github.com/giampaolo/psutil/issues/1270 -.. _1271: https://github.com/giampaolo/psutil/issues/1271 -.. _1272: https://github.com/giampaolo/psutil/issues/1272 -.. _1273: https://github.com/giampaolo/psutil/issues/1273 -.. _1274: https://github.com/giampaolo/psutil/issues/1274 -.. _1275: https://github.com/giampaolo/psutil/issues/1275 -.. _1276: https://github.com/giampaolo/psutil/issues/1276 -.. _1277: https://github.com/giampaolo/psutil/issues/1277 -.. _1278: https://github.com/giampaolo/psutil/issues/1278 -.. _1279: https://github.com/giampaolo/psutil/issues/1279 -.. _1280: https://github.com/giampaolo/psutil/issues/1280 -.. _1281: https://github.com/giampaolo/psutil/issues/1281 -.. _1282: https://github.com/giampaolo/psutil/issues/1282 -.. _1283: https://github.com/giampaolo/psutil/issues/1283 -.. _1284: https://github.com/giampaolo/psutil/issues/1284 -.. _1285: https://github.com/giampaolo/psutil/issues/1285 -.. _1286: https://github.com/giampaolo/psutil/issues/1286 -.. _1287: https://github.com/giampaolo/psutil/issues/1287 -.. _1288: https://github.com/giampaolo/psutil/issues/1288 -.. _1289: https://github.com/giampaolo/psutil/issues/1289 -.. _1290: https://github.com/giampaolo/psutil/issues/1290 -.. _1291: https://github.com/giampaolo/psutil/issues/1291 -.. _1292: https://github.com/giampaolo/psutil/issues/1292 -.. _1293: https://github.com/giampaolo/psutil/issues/1293 -.. _1294: https://github.com/giampaolo/psutil/issues/1294 -.. _1295: https://github.com/giampaolo/psutil/issues/1295 -.. _1296: https://github.com/giampaolo/psutil/issues/1296 -.. _1297: https://github.com/giampaolo/psutil/issues/1297 -.. _1298: https://github.com/giampaolo/psutil/issues/1298 -.. _1299: https://github.com/giampaolo/psutil/issues/1299 -.. _1300: https://github.com/giampaolo/psutil/issues/1300 -.. _1301: https://github.com/giampaolo/psutil/issues/1301 -.. _1302: https://github.com/giampaolo/psutil/issues/1302 -.. _1303: https://github.com/giampaolo/psutil/issues/1303 -.. _1304: https://github.com/giampaolo/psutil/issues/1304 -.. _1305: https://github.com/giampaolo/psutil/issues/1305 -.. _1306: https://github.com/giampaolo/psutil/issues/1306 -.. _1307: https://github.com/giampaolo/psutil/issues/1307 -.. _1308: https://github.com/giampaolo/psutil/issues/1308 -.. _1309: https://github.com/giampaolo/psutil/issues/1309 -.. _1310: https://github.com/giampaolo/psutil/issues/1310 -.. _1311: https://github.com/giampaolo/psutil/issues/1311 -.. _1312: https://github.com/giampaolo/psutil/issues/1312 -.. _1313: https://github.com/giampaolo/psutil/issues/1313 -.. _1314: https://github.com/giampaolo/psutil/issues/1314 -.. _1315: https://github.com/giampaolo/psutil/issues/1315 -.. _1316: https://github.com/giampaolo/psutil/issues/1316 -.. _1317: https://github.com/giampaolo/psutil/issues/1317 -.. _1318: https://github.com/giampaolo/psutil/issues/1318 -.. _1319: https://github.com/giampaolo/psutil/issues/1319 -.. _1320: https://github.com/giampaolo/psutil/issues/1320 -.. _1321: https://github.com/giampaolo/psutil/issues/1321 -.. _1322: https://github.com/giampaolo/psutil/issues/1322 -.. _1323: https://github.com/giampaolo/psutil/issues/1323 -.. _1324: https://github.com/giampaolo/psutil/issues/1324 -.. _1325: https://github.com/giampaolo/psutil/issues/1325 -.. _1326: https://github.com/giampaolo/psutil/issues/1326 -.. _1327: https://github.com/giampaolo/psutil/issues/1327 -.. _1328: https://github.com/giampaolo/psutil/issues/1328 -.. _1329: https://github.com/giampaolo/psutil/issues/1329 -.. _1330: https://github.com/giampaolo/psutil/issues/1330 -.. _1331: https://github.com/giampaolo/psutil/issues/1331 -.. _1332: https://github.com/giampaolo/psutil/issues/1332 -.. _1333: https://github.com/giampaolo/psutil/issues/1333 -.. _1334: https://github.com/giampaolo/psutil/issues/1334 -.. _1335: https://github.com/giampaolo/psutil/issues/1335 -.. _1336: https://github.com/giampaolo/psutil/issues/1336 -.. _1337: https://github.com/giampaolo/psutil/issues/1337 -.. _1338: https://github.com/giampaolo/psutil/issues/1338 -.. _1339: https://github.com/giampaolo/psutil/issues/1339 -.. _1340: https://github.com/giampaolo/psutil/issues/1340 -.. _1341: https://github.com/giampaolo/psutil/issues/1341 -.. _1342: https://github.com/giampaolo/psutil/issues/1342 -.. _1343: https://github.com/giampaolo/psutil/issues/1343 -.. _1344: https://github.com/giampaolo/psutil/issues/1344 -.. _1345: https://github.com/giampaolo/psutil/issues/1345 -.. _1346: https://github.com/giampaolo/psutil/issues/1346 -.. _1347: https://github.com/giampaolo/psutil/issues/1347 -.. _1348: https://github.com/giampaolo/psutil/issues/1348 -.. _1349: https://github.com/giampaolo/psutil/issues/1349 -.. _1350: https://github.com/giampaolo/psutil/issues/1350 -.. _1351: https://github.com/giampaolo/psutil/issues/1351 -.. _1352: https://github.com/giampaolo/psutil/issues/1352 -.. _1353: https://github.com/giampaolo/psutil/issues/1353 -.. _1354: https://github.com/giampaolo/psutil/issues/1354 -.. _1355: https://github.com/giampaolo/psutil/issues/1355 -.. _1356: https://github.com/giampaolo/psutil/issues/1356 -.. _1357: https://github.com/giampaolo/psutil/issues/1357 -.. _1358: https://github.com/giampaolo/psutil/issues/1358 -.. _1359: https://github.com/giampaolo/psutil/issues/1359 -.. _1360: https://github.com/giampaolo/psutil/issues/1360 -.. _1361: https://github.com/giampaolo/psutil/issues/1361 -.. _1362: https://github.com/giampaolo/psutil/issues/1362 -.. _1363: https://github.com/giampaolo/psutil/issues/1363 -.. _1364: https://github.com/giampaolo/psutil/issues/1364 -.. _1365: https://github.com/giampaolo/psutil/issues/1365 -.. _1366: https://github.com/giampaolo/psutil/issues/1366 -.. _1367: https://github.com/giampaolo/psutil/issues/1367 -.. _1368: https://github.com/giampaolo/psutil/issues/1368 -.. _1369: https://github.com/giampaolo/psutil/issues/1369 -.. _1370: https://github.com/giampaolo/psutil/issues/1370 -.. _1371: https://github.com/giampaolo/psutil/issues/1371 -.. _1372: https://github.com/giampaolo/psutil/issues/1372 -.. _1373: https://github.com/giampaolo/psutil/issues/1373 -.. _1374: https://github.com/giampaolo/psutil/issues/1374 -.. _1375: https://github.com/giampaolo/psutil/issues/1375 -.. _1376: https://github.com/giampaolo/psutil/issues/1376 -.. _1377: https://github.com/giampaolo/psutil/issues/1377 -.. _1378: https://github.com/giampaolo/psutil/issues/1378 -.. _1379: https://github.com/giampaolo/psutil/issues/1379 -.. _1380: https://github.com/giampaolo/psutil/issues/1380 -.. _1381: https://github.com/giampaolo/psutil/issues/1381 -.. _1382: https://github.com/giampaolo/psutil/issues/1382 -.. _1383: https://github.com/giampaolo/psutil/issues/1383 -.. _1384: https://github.com/giampaolo/psutil/issues/1384 -.. _1385: https://github.com/giampaolo/psutil/issues/1385 -.. _1386: https://github.com/giampaolo/psutil/issues/1386 -.. _1387: https://github.com/giampaolo/psutil/issues/1387 -.. _1388: https://github.com/giampaolo/psutil/issues/1388 -.. _1389: https://github.com/giampaolo/psutil/issues/1389 -.. _1390: https://github.com/giampaolo/psutil/issues/1390 -.. _1391: https://github.com/giampaolo/psutil/issues/1391 -.. _1392: https://github.com/giampaolo/psutil/issues/1392 -.. _1393: https://github.com/giampaolo/psutil/issues/1393 -.. _1394: https://github.com/giampaolo/psutil/issues/1394 -.. _1395: https://github.com/giampaolo/psutil/issues/1395 -.. _1396: https://github.com/giampaolo/psutil/issues/1396 -.. _1397: https://github.com/giampaolo/psutil/issues/1397 -.. _1398: https://github.com/giampaolo/psutil/issues/1398 -.. _1399: https://github.com/giampaolo/psutil/issues/1399 -.. _1400: https://github.com/giampaolo/psutil/issues/1400 -.. _1401: https://github.com/giampaolo/psutil/issues/1401 -.. _1402: https://github.com/giampaolo/psutil/issues/1402 -.. _1403: https://github.com/giampaolo/psutil/issues/1403 -.. _1404: https://github.com/giampaolo/psutil/issues/1404 -.. _1405: https://github.com/giampaolo/psutil/issues/1405 -.. _1406: https://github.com/giampaolo/psutil/issues/1406 -.. _1407: https://github.com/giampaolo/psutil/issues/1407 -.. _1408: https://github.com/giampaolo/psutil/issues/1408 -.. _1409: https://github.com/giampaolo/psutil/issues/1409 -.. _1410: https://github.com/giampaolo/psutil/issues/1410 -.. _1411: https://github.com/giampaolo/psutil/issues/1411 -.. _1412: https://github.com/giampaolo/psutil/issues/1412 -.. _1413: https://github.com/giampaolo/psutil/issues/1413 -.. _1414: https://github.com/giampaolo/psutil/issues/1414 -.. _1415: https://github.com/giampaolo/psutil/issues/1415 -.. _1416: https://github.com/giampaolo/psutil/issues/1416 -.. _1417: https://github.com/giampaolo/psutil/issues/1417 -.. _1418: https://github.com/giampaolo/psutil/issues/1418 -.. _1419: https://github.com/giampaolo/psutil/issues/1419 -.. _1420: https://github.com/giampaolo/psutil/issues/1420 -.. _1421: https://github.com/giampaolo/psutil/issues/1421 -.. _1422: https://github.com/giampaolo/psutil/issues/1422 -.. _1423: https://github.com/giampaolo/psutil/issues/1423 -.. _1424: https://github.com/giampaolo/psutil/issues/1424 -.. _1425: https://github.com/giampaolo/psutil/issues/1425 -.. _1426: https://github.com/giampaolo/psutil/issues/1426 -.. _1427: https://github.com/giampaolo/psutil/issues/1427 -.. _1428: https://github.com/giampaolo/psutil/issues/1428 -.. _1429: https://github.com/giampaolo/psutil/issues/1429 -.. _1430: https://github.com/giampaolo/psutil/issues/1430 -.. _1431: https://github.com/giampaolo/psutil/issues/1431 -.. _1432: https://github.com/giampaolo/psutil/issues/1432 -.. _1433: https://github.com/giampaolo/psutil/issues/1433 -.. _1434: https://github.com/giampaolo/psutil/issues/1434 -.. _1435: https://github.com/giampaolo/psutil/issues/1435 -.. _1436: https://github.com/giampaolo/psutil/issues/1436 -.. _1437: https://github.com/giampaolo/psutil/issues/1437 -.. _1438: https://github.com/giampaolo/psutil/issues/1438 -.. _1439: https://github.com/giampaolo/psutil/issues/1439 -.. _1440: https://github.com/giampaolo/psutil/issues/1440 -.. _1441: https://github.com/giampaolo/psutil/issues/1441 -.. _1442: https://github.com/giampaolo/psutil/issues/1442 -.. _1443: https://github.com/giampaolo/psutil/issues/1443 -.. _1444: https://github.com/giampaolo/psutil/issues/1444 -.. _1445: https://github.com/giampaolo/psutil/issues/1445 -.. _1446: https://github.com/giampaolo/psutil/issues/1446 -.. _1447: https://github.com/giampaolo/psutil/issues/1447 -.. _1448: https://github.com/giampaolo/psutil/issues/1448 -.. _1449: https://github.com/giampaolo/psutil/issues/1449 -.. _1450: https://github.com/giampaolo/psutil/issues/1450 -.. _1451: https://github.com/giampaolo/psutil/issues/1451 -.. _1452: https://github.com/giampaolo/psutil/issues/1452 -.. _1453: https://github.com/giampaolo/psutil/issues/1453 -.. _1454: https://github.com/giampaolo/psutil/issues/1454 -.. _1455: https://github.com/giampaolo/psutil/issues/1455 -.. _1456: https://github.com/giampaolo/psutil/issues/1456 -.. _1457: https://github.com/giampaolo/psutil/issues/1457 -.. _1458: https://github.com/giampaolo/psutil/issues/1458 -.. _1459: https://github.com/giampaolo/psutil/issues/1459 -.. _1460: https://github.com/giampaolo/psutil/issues/1460 -.. _1461: https://github.com/giampaolo/psutil/issues/1461 -.. _1462: https://github.com/giampaolo/psutil/issues/1462 -.. _1463: https://github.com/giampaolo/psutil/issues/1463 -.. _1464: https://github.com/giampaolo/psutil/issues/1464 -.. _1465: https://github.com/giampaolo/psutil/issues/1465 -.. _1466: https://github.com/giampaolo/psutil/issues/1466 -.. _1467: https://github.com/giampaolo/psutil/issues/1467 -.. _1468: https://github.com/giampaolo/psutil/issues/1468 -.. _1469: https://github.com/giampaolo/psutil/issues/1469 -.. _1470: https://github.com/giampaolo/psutil/issues/1470 -.. _1471: https://github.com/giampaolo/psutil/issues/1471 -.. _1472: https://github.com/giampaolo/psutil/issues/1472 -.. _1473: https://github.com/giampaolo/psutil/issues/1473 -.. _1474: https://github.com/giampaolo/psutil/issues/1474 -.. _1475: https://github.com/giampaolo/psutil/issues/1475 -.. _1476: https://github.com/giampaolo/psutil/issues/1476 -.. _1477: https://github.com/giampaolo/psutil/issues/1477 -.. _1478: https://github.com/giampaolo/psutil/issues/1478 -.. _1479: https://github.com/giampaolo/psutil/issues/1479 -.. _1480: https://github.com/giampaolo/psutil/issues/1480 -.. _1481: https://github.com/giampaolo/psutil/issues/1481 -.. _1482: https://github.com/giampaolo/psutil/issues/1482 -.. _1483: https://github.com/giampaolo/psutil/issues/1483 -.. _1484: https://github.com/giampaolo/psutil/issues/1484 -.. _1485: https://github.com/giampaolo/psutil/issues/1485 -.. _1486: https://github.com/giampaolo/psutil/issues/1486 -.. _1487: https://github.com/giampaolo/psutil/issues/1487 -.. _1488: https://github.com/giampaolo/psutil/issues/1488 -.. _1489: https://github.com/giampaolo/psutil/issues/1489 -.. _1490: https://github.com/giampaolo/psutil/issues/1490 -.. _1491: https://github.com/giampaolo/psutil/issues/1491 -.. _1492: https://github.com/giampaolo/psutil/issues/1492 -.. _1493: https://github.com/giampaolo/psutil/issues/1493 -.. _1494: https://github.com/giampaolo/psutil/issues/1494 -.. _1495: https://github.com/giampaolo/psutil/issues/1495 -.. _1496: https://github.com/giampaolo/psutil/issues/1496 -.. _1497: https://github.com/giampaolo/psutil/issues/1497 -.. _1498: https://github.com/giampaolo/psutil/issues/1498 -.. _1499: https://github.com/giampaolo/psutil/issues/1499 -.. _1500: https://github.com/giampaolo/psutil/issues/1500 -.. _1501: https://github.com/giampaolo/psutil/issues/1501 -.. _1502: https://github.com/giampaolo/psutil/issues/1502 -.. _1503: https://github.com/giampaolo/psutil/issues/1503 -.. _1504: https://github.com/giampaolo/psutil/issues/1504 -.. _1505: https://github.com/giampaolo/psutil/issues/1505 -.. _1506: https://github.com/giampaolo/psutil/issues/1506 -.. _1507: https://github.com/giampaolo/psutil/issues/1507 -.. _1508: https://github.com/giampaolo/psutil/issues/1508 -.. _1509: https://github.com/giampaolo/psutil/issues/1509 -.. _1510: https://github.com/giampaolo/psutil/issues/1510 -.. _1511: https://github.com/giampaolo/psutil/issues/1511 -.. _1512: https://github.com/giampaolo/psutil/issues/1512 -.. _1513: https://github.com/giampaolo/psutil/issues/1513 -.. _1514: https://github.com/giampaolo/psutil/issues/1514 -.. _1515: https://github.com/giampaolo/psutil/issues/1515 -.. _1516: https://github.com/giampaolo/psutil/issues/1516 -.. _1517: https://github.com/giampaolo/psutil/issues/1517 -.. _1518: https://github.com/giampaolo/psutil/issues/1518 -.. _1519: https://github.com/giampaolo/psutil/issues/1519 -.. _1520: https://github.com/giampaolo/psutil/issues/1520 -.. _1521: https://github.com/giampaolo/psutil/issues/1521 -.. _1522: https://github.com/giampaolo/psutil/issues/1522 -.. _1523: https://github.com/giampaolo/psutil/issues/1523 -.. _1524: https://github.com/giampaolo/psutil/issues/1524 -.. _1525: https://github.com/giampaolo/psutil/issues/1525 -.. _1526: https://github.com/giampaolo/psutil/issues/1526 -.. _1527: https://github.com/giampaolo/psutil/issues/1527 -.. _1528: https://github.com/giampaolo/psutil/issues/1528 -.. _1529: https://github.com/giampaolo/psutil/issues/1529 -.. _1530: https://github.com/giampaolo/psutil/issues/1530 -.. _1531: https://github.com/giampaolo/psutil/issues/1531 -.. _1532: https://github.com/giampaolo/psutil/issues/1532 -.. _1533: https://github.com/giampaolo/psutil/issues/1533 -.. _1534: https://github.com/giampaolo/psutil/issues/1534 -.. _1535: https://github.com/giampaolo/psutil/issues/1535 -.. _1536: https://github.com/giampaolo/psutil/issues/1536 -.. _1537: https://github.com/giampaolo/psutil/issues/1537 -.. _1538: https://github.com/giampaolo/psutil/issues/1538 -.. _1539: https://github.com/giampaolo/psutil/issues/1539 -.. _1540: https://github.com/giampaolo/psutil/issues/1540 -.. _1541: https://github.com/giampaolo/psutil/issues/1541 -.. _1542: https://github.com/giampaolo/psutil/issues/1542 -.. _1543: https://github.com/giampaolo/psutil/issues/1543 -.. _1544: https://github.com/giampaolo/psutil/issues/1544 -.. _1545: https://github.com/giampaolo/psutil/issues/1545 -.. _1546: https://github.com/giampaolo/psutil/issues/1546 -.. _1547: https://github.com/giampaolo/psutil/issues/1547 -.. _1548: https://github.com/giampaolo/psutil/issues/1548 -.. _1549: https://github.com/giampaolo/psutil/issues/1549 -.. _1550: https://github.com/giampaolo/psutil/issues/1550 -.. _1551: https://github.com/giampaolo/psutil/issues/1551 -.. _1552: https://github.com/giampaolo/psutil/issues/1552 -.. _1553: https://github.com/giampaolo/psutil/issues/1553 -.. _1554: https://github.com/giampaolo/psutil/issues/1554 -.. _1555: https://github.com/giampaolo/psutil/issues/1555 -.. _1556: https://github.com/giampaolo/psutil/issues/1556 -.. _1557: https://github.com/giampaolo/psutil/issues/1557 -.. _1558: https://github.com/giampaolo/psutil/issues/1558 -.. _1559: https://github.com/giampaolo/psutil/issues/1559 -.. _1560: https://github.com/giampaolo/psutil/issues/1560 -.. _1561: https://github.com/giampaolo/psutil/issues/1561 -.. _1562: https://github.com/giampaolo/psutil/issues/1562 -.. _1563: https://github.com/giampaolo/psutil/issues/1563 -.. _1564: https://github.com/giampaolo/psutil/issues/1564 -.. _1565: https://github.com/giampaolo/psutil/issues/1565 -.. _1566: https://github.com/giampaolo/psutil/issues/1566 -.. _1567: https://github.com/giampaolo/psutil/issues/1567 -.. _1568: https://github.com/giampaolo/psutil/issues/1568 -.. _1569: https://github.com/giampaolo/psutil/issues/1569 -.. _1570: https://github.com/giampaolo/psutil/issues/1570 -.. _1571: https://github.com/giampaolo/psutil/issues/1571 -.. _1572: https://github.com/giampaolo/psutil/issues/1572 -.. _1573: https://github.com/giampaolo/psutil/issues/1573 -.. _1574: https://github.com/giampaolo/psutil/issues/1574 -.. _1575: https://github.com/giampaolo/psutil/issues/1575 -.. _1576: https://github.com/giampaolo/psutil/issues/1576 -.. _1577: https://github.com/giampaolo/psutil/issues/1577 -.. _1578: https://github.com/giampaolo/psutil/issues/1578 -.. _1579: https://github.com/giampaolo/psutil/issues/1579 -.. _1580: https://github.com/giampaolo/psutil/issues/1580 -.. _1581: https://github.com/giampaolo/psutil/issues/1581 -.. _1582: https://github.com/giampaolo/psutil/issues/1582 -.. _1583: https://github.com/giampaolo/psutil/issues/1583 -.. _1584: https://github.com/giampaolo/psutil/issues/1584 -.. _1585: https://github.com/giampaolo/psutil/issues/1585 -.. _1586: https://github.com/giampaolo/psutil/issues/1586 -.. _1587: https://github.com/giampaolo/psutil/issues/1587 -.. _1588: https://github.com/giampaolo/psutil/issues/1588 -.. _1589: https://github.com/giampaolo/psutil/issues/1589 -.. _1590: https://github.com/giampaolo/psutil/issues/1590 -.. _1591: https://github.com/giampaolo/psutil/issues/1591 -.. _1592: https://github.com/giampaolo/psutil/issues/1592 -.. _1593: https://github.com/giampaolo/psutil/issues/1593 -.. _1594: https://github.com/giampaolo/psutil/issues/1594 -.. _1595: https://github.com/giampaolo/psutil/issues/1595 -.. _1596: https://github.com/giampaolo/psutil/issues/1596 -.. _1597: https://github.com/giampaolo/psutil/issues/1597 -.. _1598: https://github.com/giampaolo/psutil/issues/1598 -.. _1599: https://github.com/giampaolo/psutil/issues/1599 -.. _1600: https://github.com/giampaolo/psutil/issues/1600 -.. _1601: https://github.com/giampaolo/psutil/issues/1601 -.. _1602: https://github.com/giampaolo/psutil/issues/1602 -.. _1603: https://github.com/giampaolo/psutil/issues/1603 -.. _1604: https://github.com/giampaolo/psutil/issues/1604 -.. _1605: https://github.com/giampaolo/psutil/issues/1605 -.. _1606: https://github.com/giampaolo/psutil/issues/1606 -.. _1607: https://github.com/giampaolo/psutil/issues/1607 -.. _1608: https://github.com/giampaolo/psutil/issues/1608 -.. _1609: https://github.com/giampaolo/psutil/issues/1609 -.. _1610: https://github.com/giampaolo/psutil/issues/1610 -.. _1611: https://github.com/giampaolo/psutil/issues/1611 -.. _1612: https://github.com/giampaolo/psutil/issues/1612 -.. _1613: https://github.com/giampaolo/psutil/issues/1613 -.. _1614: https://github.com/giampaolo/psutil/issues/1614 -.. _1615: https://github.com/giampaolo/psutil/issues/1615 -.. _1616: https://github.com/giampaolo/psutil/issues/1616 -.. _1617: https://github.com/giampaolo/psutil/issues/1617 -.. _1618: https://github.com/giampaolo/psutil/issues/1618 -.. _1619: https://github.com/giampaolo/psutil/issues/1619 -.. _1620: https://github.com/giampaolo/psutil/issues/1620 -.. _1621: https://github.com/giampaolo/psutil/issues/1621 -.. _1622: https://github.com/giampaolo/psutil/issues/1622 -.. _1623: https://github.com/giampaolo/psutil/issues/1623 -.. _1624: https://github.com/giampaolo/psutil/issues/1624 -.. _1625: https://github.com/giampaolo/psutil/issues/1625 -.. _1626: https://github.com/giampaolo/psutil/issues/1626 -.. _1627: https://github.com/giampaolo/psutil/issues/1627 -.. _1628: https://github.com/giampaolo/psutil/issues/1628 -.. _1629: https://github.com/giampaolo/psutil/issues/1629 -.. _1630: https://github.com/giampaolo/psutil/issues/1630 -.. _1631: https://github.com/giampaolo/psutil/issues/1631 -.. _1632: https://github.com/giampaolo/psutil/issues/1632 -.. _1633: https://github.com/giampaolo/psutil/issues/1633 -.. _1634: https://github.com/giampaolo/psutil/issues/1634 -.. _1635: https://github.com/giampaolo/psutil/issues/1635 -.. _1636: https://github.com/giampaolo/psutil/issues/1636 -.. _1637: https://github.com/giampaolo/psutil/issues/1637 -.. _1638: https://github.com/giampaolo/psutil/issues/1638 -.. _1639: https://github.com/giampaolo/psutil/issues/1639 -.. _1640: https://github.com/giampaolo/psutil/issues/1640 -.. _1641: https://github.com/giampaolo/psutil/issues/1641 -.. _1642: https://github.com/giampaolo/psutil/issues/1642 -.. _1643: https://github.com/giampaolo/psutil/issues/1643 -.. _1644: https://github.com/giampaolo/psutil/issues/1644 -.. _1645: https://github.com/giampaolo/psutil/issues/1645 -.. _1646: https://github.com/giampaolo/psutil/issues/1646 -.. _1647: https://github.com/giampaolo/psutil/issues/1647 -.. _1648: https://github.com/giampaolo/psutil/issues/1648 -.. _1649: https://github.com/giampaolo/psutil/issues/1649 -.. _1650: https://github.com/giampaolo/psutil/issues/1650 -.. _1651: https://github.com/giampaolo/psutil/issues/1651 -.. _1652: https://github.com/giampaolo/psutil/issues/1652 -.. _1653: https://github.com/giampaolo/psutil/issues/1653 -.. _1654: https://github.com/giampaolo/psutil/issues/1654 -.. _1655: https://github.com/giampaolo/psutil/issues/1655 -.. _1656: https://github.com/giampaolo/psutil/issues/1656 -.. _1657: https://github.com/giampaolo/psutil/issues/1657 -.. _1658: https://github.com/giampaolo/psutil/issues/1658 -.. _1659: https://github.com/giampaolo/psutil/issues/1659 -.. _1660: https://github.com/giampaolo/psutil/issues/1660 -.. _1661: https://github.com/giampaolo/psutil/issues/1661 -.. _1662: https://github.com/giampaolo/psutil/issues/1662 -.. _1663: https://github.com/giampaolo/psutil/issues/1663 -.. _1664: https://github.com/giampaolo/psutil/issues/1664 -.. _1665: https://github.com/giampaolo/psutil/issues/1665 -.. _1666: https://github.com/giampaolo/psutil/issues/1666 -.. _1667: https://github.com/giampaolo/psutil/issues/1667 -.. _1668: https://github.com/giampaolo/psutil/issues/1668 -.. _1669: https://github.com/giampaolo/psutil/issues/1669 -.. _1670: https://github.com/giampaolo/psutil/issues/1670 -.. _1671: https://github.com/giampaolo/psutil/issues/1671 -.. _1672: https://github.com/giampaolo/psutil/issues/1672 -.. _1673: https://github.com/giampaolo/psutil/issues/1673 -.. _1674: https://github.com/giampaolo/psutil/issues/1674 -.. _1675: https://github.com/giampaolo/psutil/issues/1675 -.. _1676: https://github.com/giampaolo/psutil/issues/1676 -.. _1677: https://github.com/giampaolo/psutil/issues/1677 -.. _1678: https://github.com/giampaolo/psutil/issues/1678 -.. _1679: https://github.com/giampaolo/psutil/issues/1679 -.. _1680: https://github.com/giampaolo/psutil/issues/1680 -.. _1681: https://github.com/giampaolo/psutil/issues/1681 -.. _1682: https://github.com/giampaolo/psutil/issues/1682 -.. _1683: https://github.com/giampaolo/psutil/issues/1683 -.. _1684: https://github.com/giampaolo/psutil/issues/1684 -.. _1685: https://github.com/giampaolo/psutil/issues/1685 -.. _1686: https://github.com/giampaolo/psutil/issues/1686 -.. _1687: https://github.com/giampaolo/psutil/issues/1687 -.. _1688: https://github.com/giampaolo/psutil/issues/1688 -.. _1689: https://github.com/giampaolo/psutil/issues/1689 -.. _1690: https://github.com/giampaolo/psutil/issues/1690 -.. _1691: https://github.com/giampaolo/psutil/issues/1691 -.. _1692: https://github.com/giampaolo/psutil/issues/1692 -.. _1693: https://github.com/giampaolo/psutil/issues/1693 -.. _1694: https://github.com/giampaolo/psutil/issues/1694 -.. _1695: https://github.com/giampaolo/psutil/issues/1695 -.. _1696: https://github.com/giampaolo/psutil/issues/1696 -.. _1697: https://github.com/giampaolo/psutil/issues/1697 -.. _1698: https://github.com/giampaolo/psutil/issues/1698 -.. _1699: https://github.com/giampaolo/psutil/issues/1699 -.. _1700: https://github.com/giampaolo/psutil/issues/1700 -.. _1701: https://github.com/giampaolo/psutil/issues/1701 -.. _1702: https://github.com/giampaolo/psutil/issues/1702 -.. _1703: https://github.com/giampaolo/psutil/issues/1703 -.. _1704: https://github.com/giampaolo/psutil/issues/1704 -.. _1705: https://github.com/giampaolo/psutil/issues/1705 -.. _1706: https://github.com/giampaolo/psutil/issues/1706 -.. _1707: https://github.com/giampaolo/psutil/issues/1707 -.. _1708: https://github.com/giampaolo/psutil/issues/1708 -.. _1709: https://github.com/giampaolo/psutil/issues/1709 -.. _1710: https://github.com/giampaolo/psutil/issues/1710 -.. _1711: https://github.com/giampaolo/psutil/issues/1711 -.. _1712: https://github.com/giampaolo/psutil/issues/1712 -.. _1713: https://github.com/giampaolo/psutil/issues/1713 -.. _1714: https://github.com/giampaolo/psutil/issues/1714 -.. _1715: https://github.com/giampaolo/psutil/issues/1715 -.. _1716: https://github.com/giampaolo/psutil/issues/1716 -.. _1717: https://github.com/giampaolo/psutil/issues/1717 -.. _1718: https://github.com/giampaolo/psutil/issues/1718 -.. _1719: https://github.com/giampaolo/psutil/issues/1719 -.. _1720: https://github.com/giampaolo/psutil/issues/1720 -.. _1721: https://github.com/giampaolo/psutil/issues/1721 -.. _1722: https://github.com/giampaolo/psutil/issues/1722 -.. _1723: https://github.com/giampaolo/psutil/issues/1723 -.. _1724: https://github.com/giampaolo/psutil/issues/1724 -.. _1725: https://github.com/giampaolo/psutil/issues/1725 -.. _1726: https://github.com/giampaolo/psutil/issues/1726 -.. _1727: https://github.com/giampaolo/psutil/issues/1727 -.. _1728: https://github.com/giampaolo/psutil/issues/1728 -.. _1729: https://github.com/giampaolo/psutil/issues/1729 -.. _1730: https://github.com/giampaolo/psutil/issues/1730 -.. _1731: https://github.com/giampaolo/psutil/issues/1731 -.. _1732: https://github.com/giampaolo/psutil/issues/1732 -.. _1733: https://github.com/giampaolo/psutil/issues/1733 -.. _1734: https://github.com/giampaolo/psutil/issues/1734 -.. _1735: https://github.com/giampaolo/psutil/issues/1735 -.. _1736: https://github.com/giampaolo/psutil/issues/1736 -.. _1737: https://github.com/giampaolo/psutil/issues/1737 -.. _1738: https://github.com/giampaolo/psutil/issues/1738 -.. _1739: https://github.com/giampaolo/psutil/issues/1739 -.. _1740: https://github.com/giampaolo/psutil/issues/1740 -.. _1741: https://github.com/giampaolo/psutil/issues/1741 -.. _1742: https://github.com/giampaolo/psutil/issues/1742 -.. _1743: https://github.com/giampaolo/psutil/issues/1743 -.. _1744: https://github.com/giampaolo/psutil/issues/1744 -.. _1745: https://github.com/giampaolo/psutil/issues/1745 -.. _1746: https://github.com/giampaolo/psutil/issues/1746 -.. _1747: https://github.com/giampaolo/psutil/issues/1747 -.. _1748: https://github.com/giampaolo/psutil/issues/1748 -.. _1749: https://github.com/giampaolo/psutil/issues/1749 -.. _1750: https://github.com/giampaolo/psutil/issues/1750 -.. _1751: https://github.com/giampaolo/psutil/issues/1751 -.. _1752: https://github.com/giampaolo/psutil/issues/1752 -.. _1753: https://github.com/giampaolo/psutil/issues/1753 -.. _1754: https://github.com/giampaolo/psutil/issues/1754 -.. _1755: https://github.com/giampaolo/psutil/issues/1755 -.. _1756: https://github.com/giampaolo/psutil/issues/1756 -.. _1757: https://github.com/giampaolo/psutil/issues/1757 -.. _1758: https://github.com/giampaolo/psutil/issues/1758 -.. _1759: https://github.com/giampaolo/psutil/issues/1759 -.. _1760: https://github.com/giampaolo/psutil/issues/1760 -.. _1761: https://github.com/giampaolo/psutil/issues/1761 -.. _1762: https://github.com/giampaolo/psutil/issues/1762 -.. _1763: https://github.com/giampaolo/psutil/issues/1763 -.. _1764: https://github.com/giampaolo/psutil/issues/1764 -.. _1765: https://github.com/giampaolo/psutil/issues/1765 -.. _1766: https://github.com/giampaolo/psutil/issues/1766 -.. _1767: https://github.com/giampaolo/psutil/issues/1767 -.. _1768: https://github.com/giampaolo/psutil/issues/1768 -.. _1769: https://github.com/giampaolo/psutil/issues/1769 -.. _1770: https://github.com/giampaolo/psutil/issues/1770 -.. _1771: https://github.com/giampaolo/psutil/issues/1771 -.. _1772: https://github.com/giampaolo/psutil/issues/1772 -.. _1773: https://github.com/giampaolo/psutil/issues/1773 -.. _1774: https://github.com/giampaolo/psutil/issues/1774 -.. _1775: https://github.com/giampaolo/psutil/issues/1775 -.. _1776: https://github.com/giampaolo/psutil/issues/1776 -.. _1777: https://github.com/giampaolo/psutil/issues/1777 -.. _1778: https://github.com/giampaolo/psutil/issues/1778 -.. _1779: https://github.com/giampaolo/psutil/issues/1779 -.. _1780: https://github.com/giampaolo/psutil/issues/1780 -.. _1781: https://github.com/giampaolo/psutil/issues/1781 -.. _1782: https://github.com/giampaolo/psutil/issues/1782 -.. _1783: https://github.com/giampaolo/psutil/issues/1783 -.. _1784: https://github.com/giampaolo/psutil/issues/1784 -.. _1785: https://github.com/giampaolo/psutil/issues/1785 -.. _1786: https://github.com/giampaolo/psutil/issues/1786 -.. _1787: https://github.com/giampaolo/psutil/issues/1787 -.. _1788: https://github.com/giampaolo/psutil/issues/1788 -.. _1789: https://github.com/giampaolo/psutil/issues/1789 -.. _1790: https://github.com/giampaolo/psutil/issues/1790 -.. _1791: https://github.com/giampaolo/psutil/issues/1791 -.. _1792: https://github.com/giampaolo/psutil/issues/1792 -.. _1793: https://github.com/giampaolo/psutil/issues/1793 -.. _1794: https://github.com/giampaolo/psutil/issues/1794 -.. _1795: https://github.com/giampaolo/psutil/issues/1795 -.. _1796: https://github.com/giampaolo/psutil/issues/1796 -.. _1797: https://github.com/giampaolo/psutil/issues/1797 -.. _1798: https://github.com/giampaolo/psutil/issues/1798 -.. _1799: https://github.com/giampaolo/psutil/issues/1799 -.. _1800: https://github.com/giampaolo/psutil/issues/1800 -.. _1801: https://github.com/giampaolo/psutil/issues/1801 -.. _1802: https://github.com/giampaolo/psutil/issues/1802 -.. _1803: https://github.com/giampaolo/psutil/issues/1803 -.. _1804: https://github.com/giampaolo/psutil/issues/1804 -.. _1805: https://github.com/giampaolo/psutil/issues/1805 -.. _1806: https://github.com/giampaolo/psutil/issues/1806 -.. _1807: https://github.com/giampaolo/psutil/issues/1807 -.. _1808: https://github.com/giampaolo/psutil/issues/1808 -.. _1809: https://github.com/giampaolo/psutil/issues/1809 -.. _1810: https://github.com/giampaolo/psutil/issues/1810 -.. _1811: https://github.com/giampaolo/psutil/issues/1811 -.. _1812: https://github.com/giampaolo/psutil/issues/1812 -.. _1813: https://github.com/giampaolo/psutil/issues/1813 -.. _1814: https://github.com/giampaolo/psutil/issues/1814 -.. _1815: https://github.com/giampaolo/psutil/issues/1815 -.. _1816: https://github.com/giampaolo/psutil/issues/1816 -.. _1817: https://github.com/giampaolo/psutil/issues/1817 -.. _1818: https://github.com/giampaolo/psutil/issues/1818 -.. _1819: https://github.com/giampaolo/psutil/issues/1819 -.. _1820: https://github.com/giampaolo/psutil/issues/1820 -.. _1821: https://github.com/giampaolo/psutil/issues/1821 -.. _1822: https://github.com/giampaolo/psutil/issues/1822 -.. _1823: https://github.com/giampaolo/psutil/issues/1823 -.. _1824: https://github.com/giampaolo/psutil/issues/1824 -.. _1825: https://github.com/giampaolo/psutil/issues/1825 -.. _1826: https://github.com/giampaolo/psutil/issues/1826 -.. _1827: https://github.com/giampaolo/psutil/issues/1827 -.. _1828: https://github.com/giampaolo/psutil/issues/1828 -.. _1829: https://github.com/giampaolo/psutil/issues/1829 -.. _1830: https://github.com/giampaolo/psutil/issues/1830 -.. _1831: https://github.com/giampaolo/psutil/issues/1831 -.. _1832: https://github.com/giampaolo/psutil/issues/1832 -.. _1833: https://github.com/giampaolo/psutil/issues/1833 -.. _1834: https://github.com/giampaolo/psutil/issues/1834 -.. _1835: https://github.com/giampaolo/psutil/issues/1835 -.. _1836: https://github.com/giampaolo/psutil/issues/1836 -.. _1837: https://github.com/giampaolo/psutil/issues/1837 -.. _1838: https://github.com/giampaolo/psutil/issues/1838 -.. _1839: https://github.com/giampaolo/psutil/issues/1839 -.. _1840: https://github.com/giampaolo/psutil/issues/1840 -.. _1841: https://github.com/giampaolo/psutil/issues/1841 -.. _1842: https://github.com/giampaolo/psutil/issues/1842 -.. _1843: https://github.com/giampaolo/psutil/issues/1843 -.. _1844: https://github.com/giampaolo/psutil/issues/1844 -.. _1845: https://github.com/giampaolo/psutil/issues/1845 -.. _1846: https://github.com/giampaolo/psutil/issues/1846 -.. _1847: https://github.com/giampaolo/psutil/issues/1847 -.. _1848: https://github.com/giampaolo/psutil/issues/1848 -.. _1849: https://github.com/giampaolo/psutil/issues/1849 -.. _1850: https://github.com/giampaolo/psutil/issues/1850 -.. _1851: https://github.com/giampaolo/psutil/issues/1851 -.. _1852: https://github.com/giampaolo/psutil/issues/1852 -.. _1853: https://github.com/giampaolo/psutil/issues/1853 -.. _1854: https://github.com/giampaolo/psutil/issues/1854 -.. _1855: https://github.com/giampaolo/psutil/issues/1855 -.. _1856: https://github.com/giampaolo/psutil/issues/1856 -.. _1857: https://github.com/giampaolo/psutil/issues/1857 -.. _1858: https://github.com/giampaolo/psutil/issues/1858 -.. _1859: https://github.com/giampaolo/psutil/issues/1859 -.. _1860: https://github.com/giampaolo/psutil/issues/1860 -.. _1861: https://github.com/giampaolo/psutil/issues/1861 -.. _1862: https://github.com/giampaolo/psutil/issues/1862 -.. _1863: https://github.com/giampaolo/psutil/issues/1863 -.. _1864: https://github.com/giampaolo/psutil/issues/1864 -.. _1865: https://github.com/giampaolo/psutil/issues/1865 -.. _1866: https://github.com/giampaolo/psutil/issues/1866 -.. _1867: https://github.com/giampaolo/psutil/issues/1867 -.. _1868: https://github.com/giampaolo/psutil/issues/1868 -.. _1869: https://github.com/giampaolo/psutil/issues/1869 -.. _1870: https://github.com/giampaolo/psutil/issues/1870 -.. _1871: https://github.com/giampaolo/psutil/issues/1871 -.. _1872: https://github.com/giampaolo/psutil/issues/1872 -.. _1873: https://github.com/giampaolo/psutil/issues/1873 -.. _1874: https://github.com/giampaolo/psutil/issues/1874 -.. _1875: https://github.com/giampaolo/psutil/issues/1875 -.. _1876: https://github.com/giampaolo/psutil/issues/1876 -.. _1877: https://github.com/giampaolo/psutil/issues/1877 -.. _1878: https://github.com/giampaolo/psutil/issues/1878 -.. _1879: https://github.com/giampaolo/psutil/issues/1879 -.. _1880: https://github.com/giampaolo/psutil/issues/1880 -.. _1881: https://github.com/giampaolo/psutil/issues/1881 -.. _1882: https://github.com/giampaolo/psutil/issues/1882 -.. _1883: https://github.com/giampaolo/psutil/issues/1883 -.. _1884: https://github.com/giampaolo/psutil/issues/1884 -.. _1885: https://github.com/giampaolo/psutil/issues/1885 -.. _1886: https://github.com/giampaolo/psutil/issues/1886 -.. _1887: https://github.com/giampaolo/psutil/issues/1887 -.. _1888: https://github.com/giampaolo/psutil/issues/1888 -.. _1889: https://github.com/giampaolo/psutil/issues/1889 -.. _1890: https://github.com/giampaolo/psutil/issues/1890 -.. _1891: https://github.com/giampaolo/psutil/issues/1891 -.. _1892: https://github.com/giampaolo/psutil/issues/1892 -.. _1893: https://github.com/giampaolo/psutil/issues/1893 -.. _1894: https://github.com/giampaolo/psutil/issues/1894 -.. _1895: https://github.com/giampaolo/psutil/issues/1895 -.. _1896: https://github.com/giampaolo/psutil/issues/1896 -.. _1897: https://github.com/giampaolo/psutil/issues/1897 -.. _1898: https://github.com/giampaolo/psutil/issues/1898 -.. _1899: https://github.com/giampaolo/psutil/issues/1899 -.. _1900: https://github.com/giampaolo/psutil/issues/1900 -.. _1901: https://github.com/giampaolo/psutil/issues/1901 -.. _1902: https://github.com/giampaolo/psutil/issues/1902 -.. _1903: https://github.com/giampaolo/psutil/issues/1903 -.. _1904: https://github.com/giampaolo/psutil/issues/1904 -.. _1905: https://github.com/giampaolo/psutil/issues/1905 -.. _1906: https://github.com/giampaolo/psutil/issues/1906 -.. _1907: https://github.com/giampaolo/psutil/issues/1907 -.. _1908: https://github.com/giampaolo/psutil/issues/1908 -.. _1909: https://github.com/giampaolo/psutil/issues/1909 -.. _1910: https://github.com/giampaolo/psutil/issues/1910 -.. _1911: https://github.com/giampaolo/psutil/issues/1911 -.. _1912: https://github.com/giampaolo/psutil/issues/1912 -.. _1913: https://github.com/giampaolo/psutil/issues/1913 -.. _1914: https://github.com/giampaolo/psutil/issues/1914 -.. _1915: https://github.com/giampaolo/psutil/issues/1915 -.. _1916: https://github.com/giampaolo/psutil/issues/1916 -.. _1917: https://github.com/giampaolo/psutil/issues/1917 -.. _1918: https://github.com/giampaolo/psutil/issues/1918 -.. _1919: https://github.com/giampaolo/psutil/issues/1919 -.. _1920: https://github.com/giampaolo/psutil/issues/1920 -.. _1921: https://github.com/giampaolo/psutil/issues/1921 -.. _1922: https://github.com/giampaolo/psutil/issues/1922 -.. _1923: https://github.com/giampaolo/psutil/issues/1923 -.. _1924: https://github.com/giampaolo/psutil/issues/1924 -.. _1925: https://github.com/giampaolo/psutil/issues/1925 -.. _1926: https://github.com/giampaolo/psutil/issues/1926 -.. _1927: https://github.com/giampaolo/psutil/issues/1927 -.. _1928: https://github.com/giampaolo/psutil/issues/1928 -.. _1929: https://github.com/giampaolo/psutil/issues/1929 -.. _1930: https://github.com/giampaolo/psutil/issues/1930 -.. _1931: https://github.com/giampaolo/psutil/issues/1931 -.. _1932: https://github.com/giampaolo/psutil/issues/1932 -.. _1933: https://github.com/giampaolo/psutil/issues/1933 -.. _1934: https://github.com/giampaolo/psutil/issues/1934 -.. _1935: https://github.com/giampaolo/psutil/issues/1935 -.. _1936: https://github.com/giampaolo/psutil/issues/1936 -.. _1937: https://github.com/giampaolo/psutil/issues/1937 -.. _1938: https://github.com/giampaolo/psutil/issues/1938 -.. _1939: https://github.com/giampaolo/psutil/issues/1939 -.. _1940: https://github.com/giampaolo/psutil/issues/1940 -.. _1941: https://github.com/giampaolo/psutil/issues/1941 -.. _1942: https://github.com/giampaolo/psutil/issues/1942 -.. _1943: https://github.com/giampaolo/psutil/issues/1943 -.. _1944: https://github.com/giampaolo/psutil/issues/1944 -.. _1945: https://github.com/giampaolo/psutil/issues/1945 -.. _1946: https://github.com/giampaolo/psutil/issues/1946 -.. _1947: https://github.com/giampaolo/psutil/issues/1947 -.. _1948: https://github.com/giampaolo/psutil/issues/1948 -.. _1949: https://github.com/giampaolo/psutil/issues/1949 -.. _1950: https://github.com/giampaolo/psutil/issues/1950 -.. _1951: https://github.com/giampaolo/psutil/issues/1951 -.. _1952: https://github.com/giampaolo/psutil/issues/1952 -.. _1953: https://github.com/giampaolo/psutil/issues/1953 -.. _1954: https://github.com/giampaolo/psutil/issues/1954 -.. _1955: https://github.com/giampaolo/psutil/issues/1955 -.. _1956: https://github.com/giampaolo/psutil/issues/1956 -.. _1957: https://github.com/giampaolo/psutil/issues/1957 -.. _1958: https://github.com/giampaolo/psutil/issues/1958 -.. _1959: https://github.com/giampaolo/psutil/issues/1959 -.. _1960: https://github.com/giampaolo/psutil/issues/1960 -.. _1961: https://github.com/giampaolo/psutil/issues/1961 -.. _1962: https://github.com/giampaolo/psutil/issues/1962 -.. _1963: https://github.com/giampaolo/psutil/issues/1963 -.. _1964: https://github.com/giampaolo/psutil/issues/1964 -.. _1965: https://github.com/giampaolo/psutil/issues/1965 -.. _1966: https://github.com/giampaolo/psutil/issues/1966 -.. _1967: https://github.com/giampaolo/psutil/issues/1967 -.. _1968: https://github.com/giampaolo/psutil/issues/1968 -.. _1969: https://github.com/giampaolo/psutil/issues/1969 -.. _1970: https://github.com/giampaolo/psutil/issues/1970 -.. _1971: https://github.com/giampaolo/psutil/issues/1971 -.. _1972: https://github.com/giampaolo/psutil/issues/1972 -.. _1973: https://github.com/giampaolo/psutil/issues/1973 -.. _1974: https://github.com/giampaolo/psutil/issues/1974 -.. _1975: https://github.com/giampaolo/psutil/issues/1975 -.. _1976: https://github.com/giampaolo/psutil/issues/1976 -.. _1977: https://github.com/giampaolo/psutil/issues/1977 -.. _1978: https://github.com/giampaolo/psutil/issues/1978 -.. _1979: https://github.com/giampaolo/psutil/issues/1979 -.. _1980: https://github.com/giampaolo/psutil/issues/1980 -.. _1981: https://github.com/giampaolo/psutil/issues/1981 -.. _1982: https://github.com/giampaolo/psutil/issues/1982 -.. _1983: https://github.com/giampaolo/psutil/issues/1983 -.. _1984: https://github.com/giampaolo/psutil/issues/1984 -.. _1985: https://github.com/giampaolo/psutil/issues/1985 -.. _1986: https://github.com/giampaolo/psutil/issues/1986 -.. _1987: https://github.com/giampaolo/psutil/issues/1987 -.. _1988: https://github.com/giampaolo/psutil/issues/1988 -.. _1989: https://github.com/giampaolo/psutil/issues/1989 -.. _1990: https://github.com/giampaolo/psutil/issues/1990 -.. _1991: https://github.com/giampaolo/psutil/issues/1991 -.. _1992: https://github.com/giampaolo/psutil/issues/1992 -.. _1993: https://github.com/giampaolo/psutil/issues/1993 -.. _1994: https://github.com/giampaolo/psutil/issues/1994 -.. _1995: https://github.com/giampaolo/psutil/issues/1995 -.. _1996: https://github.com/giampaolo/psutil/issues/1996 -.. _1997: https://github.com/giampaolo/psutil/issues/1997 -.. _1998: https://github.com/giampaolo/psutil/issues/1998 -.. _1999: https://github.com/giampaolo/psutil/issues/1999 -.. _2000: https://github.com/giampaolo/psutil/issues/2000 -.. _2001: https://github.com/giampaolo/psutil/issues/2001 -.. _2002: https://github.com/giampaolo/psutil/issues/2002 -.. _2003: https://github.com/giampaolo/psutil/issues/2003 -.. _2004: https://github.com/giampaolo/psutil/issues/2004 -.. _2005: https://github.com/giampaolo/psutil/issues/2005 -.. _2006: https://github.com/giampaolo/psutil/issues/2006 -.. _2007: https://github.com/giampaolo/psutil/issues/2007 -.. _2008: https://github.com/giampaolo/psutil/issues/2008 -.. _2009: https://github.com/giampaolo/psutil/issues/2009 -.. _2010: https://github.com/giampaolo/psutil/issues/2010 -.. _2011: https://github.com/giampaolo/psutil/issues/2011 -.. _2012: https://github.com/giampaolo/psutil/issues/2012 -.. _2013: https://github.com/giampaolo/psutil/issues/2013 -.. _2014: https://github.com/giampaolo/psutil/issues/2014 -.. _2015: https://github.com/giampaolo/psutil/issues/2015 -.. _2016: https://github.com/giampaolo/psutil/issues/2016 -.. _2017: https://github.com/giampaolo/psutil/issues/2017 -.. _2018: https://github.com/giampaolo/psutil/issues/2018 -.. _2019: https://github.com/giampaolo/psutil/issues/2019 -.. _2020: https://github.com/giampaolo/psutil/issues/2020 -.. _2021: https://github.com/giampaolo/psutil/issues/2021 -.. _2022: https://github.com/giampaolo/psutil/issues/2022 -.. _2023: https://github.com/giampaolo/psutil/issues/2023 -.. _2024: https://github.com/giampaolo/psutil/issues/2024 -.. _2025: https://github.com/giampaolo/psutil/issues/2025 -.. _2026: https://github.com/giampaolo/psutil/issues/2026 -.. _2027: https://github.com/giampaolo/psutil/issues/2027 -.. _2028: https://github.com/giampaolo/psutil/issues/2028 -.. _2029: https://github.com/giampaolo/psutil/issues/2029 -.. _2030: https://github.com/giampaolo/psutil/issues/2030 -.. _2031: https://github.com/giampaolo/psutil/issues/2031 -.. _2032: https://github.com/giampaolo/psutil/issues/2032 -.. _2033: https://github.com/giampaolo/psutil/issues/2033 -.. _2034: https://github.com/giampaolo/psutil/issues/2034 -.. _2035: https://github.com/giampaolo/psutil/issues/2035 -.. _2036: https://github.com/giampaolo/psutil/issues/2036 -.. _2037: https://github.com/giampaolo/psutil/issues/2037 -.. _2038: https://github.com/giampaolo/psutil/issues/2038 -.. _2039: https://github.com/giampaolo/psutil/issues/2039 -.. _2040: https://github.com/giampaolo/psutil/issues/2040 -.. _2041: https://github.com/giampaolo/psutil/issues/2041 -.. _2042: https://github.com/giampaolo/psutil/issues/2042 -.. _2043: https://github.com/giampaolo/psutil/issues/2043 -.. _2044: https://github.com/giampaolo/psutil/issues/2044 -.. _2045: https://github.com/giampaolo/psutil/issues/2045 -.. _2046: https://github.com/giampaolo/psutil/issues/2046 -.. _2047: https://github.com/giampaolo/psutil/issues/2047 -.. _2048: https://github.com/giampaolo/psutil/issues/2048 -.. _2049: https://github.com/giampaolo/psutil/issues/2049 -.. _2050: https://github.com/giampaolo/psutil/issues/2050 -.. _2051: https://github.com/giampaolo/psutil/issues/2051 -.. _2052: https://github.com/giampaolo/psutil/issues/2052 -.. _2053: https://github.com/giampaolo/psutil/issues/2053 -.. _2054: https://github.com/giampaolo/psutil/issues/2054 -.. _2055: https://github.com/giampaolo/psutil/issues/2055 -.. _2056: https://github.com/giampaolo/psutil/issues/2056 -.. _2057: https://github.com/giampaolo/psutil/issues/2057 -.. _2058: https://github.com/giampaolo/psutil/issues/2058 -.. _2059: https://github.com/giampaolo/psutil/issues/2059 -.. _2060: https://github.com/giampaolo/psutil/issues/2060 -.. _2061: https://github.com/giampaolo/psutil/issues/2061 -.. _2062: https://github.com/giampaolo/psutil/issues/2062 -.. _2063: https://github.com/giampaolo/psutil/issues/2063 -.. _2064: https://github.com/giampaolo/psutil/issues/2064 -.. _2065: https://github.com/giampaolo/psutil/issues/2065 -.. _2066: https://github.com/giampaolo/psutil/issues/2066 -.. _2067: https://github.com/giampaolo/psutil/issues/2067 -.. _2068: https://github.com/giampaolo/psutil/issues/2068 -.. _2069: https://github.com/giampaolo/psutil/issues/2069 -.. _2070: https://github.com/giampaolo/psutil/issues/2070 -.. _2071: https://github.com/giampaolo/psutil/issues/2071 -.. _2072: https://github.com/giampaolo/psutil/issues/2072 -.. _2073: https://github.com/giampaolo/psutil/issues/2073 -.. _2074: https://github.com/giampaolo/psutil/issues/2074 -.. _2075: https://github.com/giampaolo/psutil/issues/2075 -.. _2076: https://github.com/giampaolo/psutil/issues/2076 -.. _2077: https://github.com/giampaolo/psutil/issues/2077 -.. _2078: https://github.com/giampaolo/psutil/issues/2078 -.. _2079: https://github.com/giampaolo/psutil/issues/2079 -.. _2080: https://github.com/giampaolo/psutil/issues/2080 -.. _2081: https://github.com/giampaolo/psutil/issues/2081 -.. _2082: https://github.com/giampaolo/psutil/issues/2082 -.. _2083: https://github.com/giampaolo/psutil/issues/2083 -.. _2084: https://github.com/giampaolo/psutil/issues/2084 -.. _2085: https://github.com/giampaolo/psutil/issues/2085 -.. _2086: https://github.com/giampaolo/psutil/issues/2086 -.. _2087: https://github.com/giampaolo/psutil/issues/2087 -.. _2088: https://github.com/giampaolo/psutil/issues/2088 -.. _2089: https://github.com/giampaolo/psutil/issues/2089 -.. _2090: https://github.com/giampaolo/psutil/issues/2090 -.. _2091: https://github.com/giampaolo/psutil/issues/2091 -.. _2092: https://github.com/giampaolo/psutil/issues/2092 -.. _2093: https://github.com/giampaolo/psutil/issues/2093 -.. _2094: https://github.com/giampaolo/psutil/issues/2094 -.. _2095: https://github.com/giampaolo/psutil/issues/2095 -.. _2096: https://github.com/giampaolo/psutil/issues/2096 -.. _2097: https://github.com/giampaolo/psutil/issues/2097 -.. _2098: https://github.com/giampaolo/psutil/issues/2098 -.. _2099: https://github.com/giampaolo/psutil/issues/2099 -.. _2100: https://github.com/giampaolo/psutil/issues/2100 -.. _2101: https://github.com/giampaolo/psutil/issues/2101 -.. _2102: https://github.com/giampaolo/psutil/issues/2102 -.. _2103: https://github.com/giampaolo/psutil/issues/2103 -.. _2104: https://github.com/giampaolo/psutil/issues/2104 -.. _2105: https://github.com/giampaolo/psutil/issues/2105 -.. _2106: https://github.com/giampaolo/psutil/issues/2106 -.. _2107: https://github.com/giampaolo/psutil/issues/2107 -.. _2108: https://github.com/giampaolo/psutil/issues/2108 -.. _2109: https://github.com/giampaolo/psutil/issues/2109 -.. _2110: https://github.com/giampaolo/psutil/issues/2110 -.. _2111: https://github.com/giampaolo/psutil/issues/2111 -.. _2112: https://github.com/giampaolo/psutil/issues/2112 -.. _2113: https://github.com/giampaolo/psutil/issues/2113 -.. _2114: https://github.com/giampaolo/psutil/issues/2114 -.. _2115: https://github.com/giampaolo/psutil/issues/2115 -.. _2116: https://github.com/giampaolo/psutil/issues/2116 -.. _2117: https://github.com/giampaolo/psutil/issues/2117 -.. _2118: https://github.com/giampaolo/psutil/issues/2118 -.. _2119: https://github.com/giampaolo/psutil/issues/2119 -.. _2120: https://github.com/giampaolo/psutil/issues/2120 -.. _2121: https://github.com/giampaolo/psutil/issues/2121 -.. _2122: https://github.com/giampaolo/psutil/issues/2122 -.. _2123: https://github.com/giampaolo/psutil/issues/2123 -.. _2124: https://github.com/giampaolo/psutil/issues/2124 -.. _2125: https://github.com/giampaolo/psutil/issues/2125 -.. _2126: https://github.com/giampaolo/psutil/issues/2126 -.. _2127: https://github.com/giampaolo/psutil/issues/2127 -.. _2128: https://github.com/giampaolo/psutil/issues/2128 -.. _2129: https://github.com/giampaolo/psutil/issues/2129 -.. _2130: https://github.com/giampaolo/psutil/issues/2130 -.. _2131: https://github.com/giampaolo/psutil/issues/2131 -.. _2132: https://github.com/giampaolo/psutil/issues/2132 -.. _2133: https://github.com/giampaolo/psutil/issues/2133 -.. _2134: https://github.com/giampaolo/psutil/issues/2134 -.. _2135: https://github.com/giampaolo/psutil/issues/2135 -.. _2136: https://github.com/giampaolo/psutil/issues/2136 -.. _2137: https://github.com/giampaolo/psutil/issues/2137 -.. _2138: https://github.com/giampaolo/psutil/issues/2138 -.. _2139: https://github.com/giampaolo/psutil/issues/2139 -.. _2140: https://github.com/giampaolo/psutil/issues/2140 -.. _2141: https://github.com/giampaolo/psutil/issues/2141 -.. _2142: https://github.com/giampaolo/psutil/issues/2142 -.. _2143: https://github.com/giampaolo/psutil/issues/2143 -.. _2144: https://github.com/giampaolo/psutil/issues/2144 -.. _2145: https://github.com/giampaolo/psutil/issues/2145 -.. _2146: https://github.com/giampaolo/psutil/issues/2146 -.. _2147: https://github.com/giampaolo/psutil/issues/2147 -.. _2148: https://github.com/giampaolo/psutil/issues/2148 -.. _2149: https://github.com/giampaolo/psutil/issues/2149 -.. _2150: https://github.com/giampaolo/psutil/issues/2150 -.. _2151: https://github.com/giampaolo/psutil/issues/2151 -.. _2152: https://github.com/giampaolo/psutil/issues/2152 -.. _2153: https://github.com/giampaolo/psutil/issues/2153 -.. _2154: https://github.com/giampaolo/psutil/issues/2154 -.. _2155: https://github.com/giampaolo/psutil/issues/2155 -.. _2156: https://github.com/giampaolo/psutil/issues/2156 -.. _2157: https://github.com/giampaolo/psutil/issues/2157 -.. _2158: https://github.com/giampaolo/psutil/issues/2158 -.. _2159: https://github.com/giampaolo/psutil/issues/2159 -.. _2160: https://github.com/giampaolo/psutil/issues/2160 -.. _2161: https://github.com/giampaolo/psutil/issues/2161 -.. _2162: https://github.com/giampaolo/psutil/issues/2162 -.. _2163: https://github.com/giampaolo/psutil/issues/2163 -.. _2164: https://github.com/giampaolo/psutil/issues/2164 -.. _2165: https://github.com/giampaolo/psutil/issues/2165 -.. _2166: https://github.com/giampaolo/psutil/issues/2166 -.. _2167: https://github.com/giampaolo/psutil/issues/2167 -.. _2168: https://github.com/giampaolo/psutil/issues/2168 -.. _2169: https://github.com/giampaolo/psutil/issues/2169 -.. _2170: https://github.com/giampaolo/psutil/issues/2170 -.. _2171: https://github.com/giampaolo/psutil/issues/2171 -.. _2172: https://github.com/giampaolo/psutil/issues/2172 -.. _2173: https://github.com/giampaolo/psutil/issues/2173 -.. _2174: https://github.com/giampaolo/psutil/issues/2174 -.. _2175: https://github.com/giampaolo/psutil/issues/2175 -.. _2176: https://github.com/giampaolo/psutil/issues/2176 -.. _2177: https://github.com/giampaolo/psutil/issues/2177 -.. _2178: https://github.com/giampaolo/psutil/issues/2178 -.. _2179: https://github.com/giampaolo/psutil/issues/2179 -.. _2180: https://github.com/giampaolo/psutil/issues/2180 -.. _2181: https://github.com/giampaolo/psutil/issues/2181 -.. _2182: https://github.com/giampaolo/psutil/issues/2182 -.. _2183: https://github.com/giampaolo/psutil/issues/2183 -.. _2184: https://github.com/giampaolo/psutil/issues/2184 -.. _2185: https://github.com/giampaolo/psutil/issues/2185 -.. _2186: https://github.com/giampaolo/psutil/issues/2186 -.. _2187: https://github.com/giampaolo/psutil/issues/2187 -.. _2188: https://github.com/giampaolo/psutil/issues/2188 -.. _2189: https://github.com/giampaolo/psutil/issues/2189 -.. _2190: https://github.com/giampaolo/psutil/issues/2190 -.. _2191: https://github.com/giampaolo/psutil/issues/2191 -.. _2192: https://github.com/giampaolo/psutil/issues/2192 -.. _2193: https://github.com/giampaolo/psutil/issues/2193 -.. _2194: https://github.com/giampaolo/psutil/issues/2194 -.. _2195: https://github.com/giampaolo/psutil/issues/2195 -.. _2196: https://github.com/giampaolo/psutil/issues/2196 -.. _2197: https://github.com/giampaolo/psutil/issues/2197 -.. _2198: https://github.com/giampaolo/psutil/issues/2198 -.. _2199: https://github.com/giampaolo/psutil/issues/2199 -.. _2200: https://github.com/giampaolo/psutil/issues/2200 -.. _2201: https://github.com/giampaolo/psutil/issues/2201 -.. _2202: https://github.com/giampaolo/psutil/issues/2202 -.. _2203: https://github.com/giampaolo/psutil/issues/2203 -.. _2204: https://github.com/giampaolo/psutil/issues/2204 -.. _2205: https://github.com/giampaolo/psutil/issues/2205 -.. _2206: https://github.com/giampaolo/psutil/issues/2206 -.. _2207: https://github.com/giampaolo/psutil/issues/2207 -.. _2208: https://github.com/giampaolo/psutil/issues/2208 -.. _2209: https://github.com/giampaolo/psutil/issues/2209 -.. _2210: https://github.com/giampaolo/psutil/issues/2210 -.. _2211: https://github.com/giampaolo/psutil/issues/2211 -.. _2212: https://github.com/giampaolo/psutil/issues/2212 -.. _2213: https://github.com/giampaolo/psutil/issues/2213 -.. _2214: https://github.com/giampaolo/psutil/issues/2214 -.. _2215: https://github.com/giampaolo/psutil/issues/2215 -.. _2216: https://github.com/giampaolo/psutil/issues/2216 -.. _2217: https://github.com/giampaolo/psutil/issues/2217 -.. _2218: https://github.com/giampaolo/psutil/issues/2218 -.. _2219: https://github.com/giampaolo/psutil/issues/2219 -.. _2220: https://github.com/giampaolo/psutil/issues/2220 -.. _2221: https://github.com/giampaolo/psutil/issues/2221 -.. _2222: https://github.com/giampaolo/psutil/issues/2222 -.. _2223: https://github.com/giampaolo/psutil/issues/2223 -.. _2224: https://github.com/giampaolo/psutil/issues/2224 -.. _2225: https://github.com/giampaolo/psutil/issues/2225 -.. _2226: https://github.com/giampaolo/psutil/issues/2226 -.. _2227: https://github.com/giampaolo/psutil/issues/2227 -.. _2228: https://github.com/giampaolo/psutil/issues/2228 -.. _2229: https://github.com/giampaolo/psutil/issues/2229 -.. _2230: https://github.com/giampaolo/psutil/issues/2230 -.. _2231: https://github.com/giampaolo/psutil/issues/2231 -.. _2232: https://github.com/giampaolo/psutil/issues/2232 -.. _2233: https://github.com/giampaolo/psutil/issues/2233 -.. _2234: https://github.com/giampaolo/psutil/issues/2234 -.. _2235: https://github.com/giampaolo/psutil/issues/2235 -.. _2236: https://github.com/giampaolo/psutil/issues/2236 -.. _2237: https://github.com/giampaolo/psutil/issues/2237 -.. _2238: https://github.com/giampaolo/psutil/issues/2238 -.. _2239: https://github.com/giampaolo/psutil/issues/2239 -.. _2240: https://github.com/giampaolo/psutil/issues/2240 -.. _2241: https://github.com/giampaolo/psutil/issues/2241 -.. _2242: https://github.com/giampaolo/psutil/issues/2242 -.. _2243: https://github.com/giampaolo/psutil/issues/2243 -.. _2244: https://github.com/giampaolo/psutil/issues/2244 -.. _2245: https://github.com/giampaolo/psutil/issues/2245 -.. _2246: https://github.com/giampaolo/psutil/issues/2246 -.. _2247: https://github.com/giampaolo/psutil/issues/2247 -.. _2248: https://github.com/giampaolo/psutil/issues/2248 -.. _2249: https://github.com/giampaolo/psutil/issues/2249 -.. _2250: https://github.com/giampaolo/psutil/issues/2250 -.. _2251: https://github.com/giampaolo/psutil/issues/2251 -.. _2252: https://github.com/giampaolo/psutil/issues/2252 -.. _2253: https://github.com/giampaolo/psutil/issues/2253 -.. _2254: https://github.com/giampaolo/psutil/issues/2254 -.. _2255: https://github.com/giampaolo/psutil/issues/2255 -.. _2256: https://github.com/giampaolo/psutil/issues/2256 -.. _2257: https://github.com/giampaolo/psutil/issues/2257 -.. _2258: https://github.com/giampaolo/psutil/issues/2258 -.. _2259: https://github.com/giampaolo/psutil/issues/2259 -.. _2260: https://github.com/giampaolo/psutil/issues/2260 -.. _2261: https://github.com/giampaolo/psutil/issues/2261 -.. _2262: https://github.com/giampaolo/psutil/issues/2262 -.. _2263: https://github.com/giampaolo/psutil/issues/2263 -.. _2264: https://github.com/giampaolo/psutil/issues/2264 -.. _2265: https://github.com/giampaolo/psutil/issues/2265 -.. _2266: https://github.com/giampaolo/psutil/issues/2266 -.. _2267: https://github.com/giampaolo/psutil/issues/2267 -.. _2268: https://github.com/giampaolo/psutil/issues/2268 -.. _2269: https://github.com/giampaolo/psutil/issues/2269 -.. _2270: https://github.com/giampaolo/psutil/issues/2270 -.. _2271: https://github.com/giampaolo/psutil/issues/2271 -.. _2272: https://github.com/giampaolo/psutil/issues/2272 -.. _2273: https://github.com/giampaolo/psutil/issues/2273 -.. _2274: https://github.com/giampaolo/psutil/issues/2274 -.. _2275: https://github.com/giampaolo/psutil/issues/2275 -.. _2276: https://github.com/giampaolo/psutil/issues/2276 -.. _2277: https://github.com/giampaolo/psutil/issues/2277 -.. _2278: https://github.com/giampaolo/psutil/issues/2278 -.. _2279: https://github.com/giampaolo/psutil/issues/2279 -.. _2280: https://github.com/giampaolo/psutil/issues/2280 -.. _2281: https://github.com/giampaolo/psutil/issues/2281 -.. _2282: https://github.com/giampaolo/psutil/issues/2282 -.. _2283: https://github.com/giampaolo/psutil/issues/2283 -.. _2284: https://github.com/giampaolo/psutil/issues/2284 -.. _2285: https://github.com/giampaolo/psutil/issues/2285 -.. _2286: https://github.com/giampaolo/psutil/issues/2286 -.. _2287: https://github.com/giampaolo/psutil/issues/2287 -.. _2288: https://github.com/giampaolo/psutil/issues/2288 -.. _2289: https://github.com/giampaolo/psutil/issues/2289 -.. _2290: https://github.com/giampaolo/psutil/issues/2290 -.. _2291: https://github.com/giampaolo/psutil/issues/2291 -.. _2292: https://github.com/giampaolo/psutil/issues/2292 -.. _2293: https://github.com/giampaolo/psutil/issues/2293 -.. _2294: https://github.com/giampaolo/psutil/issues/2294 -.. _2295: https://github.com/giampaolo/psutil/issues/2295 -.. _2296: https://github.com/giampaolo/psutil/issues/2296 -.. _2297: https://github.com/giampaolo/psutil/issues/2297 -.. _2298: https://github.com/giampaolo/psutil/issues/2298 -.. _2299: https://github.com/giampaolo/psutil/issues/2299 -.. _2300: https://github.com/giampaolo/psutil/issues/2300 -.. _2301: https://github.com/giampaolo/psutil/issues/2301 -.. _2302: https://github.com/giampaolo/psutil/issues/2302 -.. _2303: https://github.com/giampaolo/psutil/issues/2303 -.. _2304: https://github.com/giampaolo/psutil/issues/2304 -.. _2305: https://github.com/giampaolo/psutil/issues/2305 -.. _2306: https://github.com/giampaolo/psutil/issues/2306 -.. _2307: https://github.com/giampaolo/psutil/issues/2307 -.. _2308: https://github.com/giampaolo/psutil/issues/2308 -.. _2309: https://github.com/giampaolo/psutil/issues/2309 -.. _2310: https://github.com/giampaolo/psutil/issues/2310 -.. _2311: https://github.com/giampaolo/psutil/issues/2311 -.. _2312: https://github.com/giampaolo/psutil/issues/2312 -.. _2313: https://github.com/giampaolo/psutil/issues/2313 -.. _2314: https://github.com/giampaolo/psutil/issues/2314 -.. _2315: https://github.com/giampaolo/psutil/issues/2315 -.. _2316: https://github.com/giampaolo/psutil/issues/2316 -.. _2317: https://github.com/giampaolo/psutil/issues/2317 -.. _2318: https://github.com/giampaolo/psutil/issues/2318 -.. _2319: https://github.com/giampaolo/psutil/issues/2319 -.. _2320: https://github.com/giampaolo/psutil/issues/2320 -.. _2321: https://github.com/giampaolo/psutil/issues/2321 -.. _2322: https://github.com/giampaolo/psutil/issues/2322 -.. _2323: https://github.com/giampaolo/psutil/issues/2323 -.. _2324: https://github.com/giampaolo/psutil/issues/2324 -.. _2325: https://github.com/giampaolo/psutil/issues/2325 -.. _2326: https://github.com/giampaolo/psutil/issues/2326 -.. _2327: https://github.com/giampaolo/psutil/issues/2327 -.. _2328: https://github.com/giampaolo/psutil/issues/2328 -.. _2329: https://github.com/giampaolo/psutil/issues/2329 -.. _2330: https://github.com/giampaolo/psutil/issues/2330 -.. _2331: https://github.com/giampaolo/psutil/issues/2331 -.. _2332: https://github.com/giampaolo/psutil/issues/2332 -.. _2333: https://github.com/giampaolo/psutil/issues/2333 -.. _2334: https://github.com/giampaolo/psutil/issues/2334 -.. _2335: https://github.com/giampaolo/psutil/issues/2335 -.. _2336: https://github.com/giampaolo/psutil/issues/2336 -.. _2337: https://github.com/giampaolo/psutil/issues/2337 -.. _2338: https://github.com/giampaolo/psutil/issues/2338 -.. _2339: https://github.com/giampaolo/psutil/issues/2339 -.. _2340: https://github.com/giampaolo/psutil/issues/2340 -.. _2341: https://github.com/giampaolo/psutil/issues/2341 -.. _2342: https://github.com/giampaolo/psutil/issues/2342 -.. _2343: https://github.com/giampaolo/psutil/issues/2343 -.. _2344: https://github.com/giampaolo/psutil/issues/2344 -.. _2345: https://github.com/giampaolo/psutil/issues/2345 -.. _2346: https://github.com/giampaolo/psutil/issues/2346 -.. _2347: https://github.com/giampaolo/psutil/issues/2347 -.. _2348: https://github.com/giampaolo/psutil/issues/2348 -.. _2349: https://github.com/giampaolo/psutil/issues/2349 -.. _2350: https://github.com/giampaolo/psutil/issues/2350 -.. _2351: https://github.com/giampaolo/psutil/issues/2351 -.. _2352: https://github.com/giampaolo/psutil/issues/2352 -.. _2353: https://github.com/giampaolo/psutil/issues/2353 -.. _2354: https://github.com/giampaolo/psutil/issues/2354 -.. _2355: https://github.com/giampaolo/psutil/issues/2355 -.. _2356: https://github.com/giampaolo/psutil/issues/2356 -.. _2357: https://github.com/giampaolo/psutil/issues/2357 -.. _2358: https://github.com/giampaolo/psutil/issues/2358 -.. _2359: https://github.com/giampaolo/psutil/issues/2359 -.. _2360: https://github.com/giampaolo/psutil/issues/2360 -.. _2361: https://github.com/giampaolo/psutil/issues/2361 -.. _2362: https://github.com/giampaolo/psutil/issues/2362 -.. _2363: https://github.com/giampaolo/psutil/issues/2363 -.. _2364: https://github.com/giampaolo/psutil/issues/2364 -.. _2365: https://github.com/giampaolo/psutil/issues/2365 -.. _2366: https://github.com/giampaolo/psutil/issues/2366 -.. _2367: https://github.com/giampaolo/psutil/issues/2367 -.. _2368: https://github.com/giampaolo/psutil/issues/2368 -.. _2369: https://github.com/giampaolo/psutil/issues/2369 -.. _2370: https://github.com/giampaolo/psutil/issues/2370 -.. _2371: https://github.com/giampaolo/psutil/issues/2371 -.. _2372: https://github.com/giampaolo/psutil/issues/2372 -.. _2373: https://github.com/giampaolo/psutil/issues/2373 -.. _2374: https://github.com/giampaolo/psutil/issues/2374 -.. _2375: https://github.com/giampaolo/psutil/issues/2375 -.. _2376: https://github.com/giampaolo/psutil/issues/2376 -.. _2377: https://github.com/giampaolo/psutil/issues/2377 -.. _2378: https://github.com/giampaolo/psutil/issues/2378 -.. _2379: https://github.com/giampaolo/psutil/issues/2379 -.. _2380: https://github.com/giampaolo/psutil/issues/2380 -.. _2381: https://github.com/giampaolo/psutil/issues/2381 -.. _2382: https://github.com/giampaolo/psutil/issues/2382 -.. _2383: https://github.com/giampaolo/psutil/issues/2383 -.. _2384: https://github.com/giampaolo/psutil/issues/2384 -.. _2385: https://github.com/giampaolo/psutil/issues/2385 -.. _2386: https://github.com/giampaolo/psutil/issues/2386 -.. _2387: https://github.com/giampaolo/psutil/issues/2387 -.. _2388: https://github.com/giampaolo/psutil/issues/2388 -.. _2389: https://github.com/giampaolo/psutil/issues/2389 -.. _2390: https://github.com/giampaolo/psutil/issues/2390 -.. _2391: https://github.com/giampaolo/psutil/issues/2391 -.. _2392: https://github.com/giampaolo/psutil/issues/2392 -.. _2393: https://github.com/giampaolo/psutil/issues/2393 -.. _2394: https://github.com/giampaolo/psutil/issues/2394 -.. _2395: https://github.com/giampaolo/psutil/issues/2395 -.. _2396: https://github.com/giampaolo/psutil/issues/2396 -.. _2397: https://github.com/giampaolo/psutil/issues/2397 -.. _2398: https://github.com/giampaolo/psutil/issues/2398 -.. _2399: https://github.com/giampaolo/psutil/issues/2399 -.. _2400: https://github.com/giampaolo/psutil/issues/2400 -.. _2401: https://github.com/giampaolo/psutil/issues/2401 -.. _2402: https://github.com/giampaolo/psutil/issues/2402 -.. _2403: https://github.com/giampaolo/psutil/issues/2403 -.. _2404: https://github.com/giampaolo/psutil/issues/2404 -.. _2405: https://github.com/giampaolo/psutil/issues/2405 -.. _2406: https://github.com/giampaolo/psutil/issues/2406 -.. _2407: https://github.com/giampaolo/psutil/issues/2407 -.. _2408: https://github.com/giampaolo/psutil/issues/2408 -.. _2409: https://github.com/giampaolo/psutil/issues/2409 -.. _2410: https://github.com/giampaolo/psutil/issues/2410 -.. _2411: https://github.com/giampaolo/psutil/issues/2411 -.. _2412: https://github.com/giampaolo/psutil/issues/2412 -.. _2413: https://github.com/giampaolo/psutil/issues/2413 -.. _2414: https://github.com/giampaolo/psutil/issues/2414 -.. _2415: https://github.com/giampaolo/psutil/issues/2415 -.. _2416: https://github.com/giampaolo/psutil/issues/2416 -.. _2417: https://github.com/giampaolo/psutil/issues/2417 -.. _2418: https://github.com/giampaolo/psutil/issues/2418 -.. _2419: https://github.com/giampaolo/psutil/issues/2419 -.. _2420: https://github.com/giampaolo/psutil/issues/2420 -.. _2421: https://github.com/giampaolo/psutil/issues/2421 -.. _2422: https://github.com/giampaolo/psutil/issues/2422 -.. _2423: https://github.com/giampaolo/psutil/issues/2423 -.. _2424: https://github.com/giampaolo/psutil/issues/2424 -.. _2425: https://github.com/giampaolo/psutil/issues/2425 -.. _2426: https://github.com/giampaolo/psutil/issues/2426 -.. _2427: https://github.com/giampaolo/psutil/issues/2427 -.. _2428: https://github.com/giampaolo/psutil/issues/2428 -.. _2429: https://github.com/giampaolo/psutil/issues/2429 -.. _2430: https://github.com/giampaolo/psutil/issues/2430 -.. _2431: https://github.com/giampaolo/psutil/issues/2431 -.. _2432: https://github.com/giampaolo/psutil/issues/2432 -.. _2433: https://github.com/giampaolo/psutil/issues/2433 -.. _2434: https://github.com/giampaolo/psutil/issues/2434 -.. _2435: https://github.com/giampaolo/psutil/issues/2435 -.. _2436: https://github.com/giampaolo/psutil/issues/2436 -.. _2437: https://github.com/giampaolo/psutil/issues/2437 -.. _2438: https://github.com/giampaolo/psutil/issues/2438 -.. _2439: https://github.com/giampaolo/psutil/issues/2439 -.. _2440: https://github.com/giampaolo/psutil/issues/2440 -.. _2441: https://github.com/giampaolo/psutil/issues/2441 -.. _2442: https://github.com/giampaolo/psutil/issues/2442 -.. _2443: https://github.com/giampaolo/psutil/issues/2443 -.. _2444: https://github.com/giampaolo/psutil/issues/2444 -.. _2445: https://github.com/giampaolo/psutil/issues/2445 -.. _2446: https://github.com/giampaolo/psutil/issues/2446 -.. _2447: https://github.com/giampaolo/psutil/issues/2447 -.. _2448: https://github.com/giampaolo/psutil/issues/2448 -.. _2449: https://github.com/giampaolo/psutil/issues/2449 -.. _2450: https://github.com/giampaolo/psutil/issues/2450 -.. _2451: https://github.com/giampaolo/psutil/issues/2451 -.. _2452: https://github.com/giampaolo/psutil/issues/2452 -.. _2453: https://github.com/giampaolo/psutil/issues/2453 -.. _2454: https://github.com/giampaolo/psutil/issues/2454 -.. _2455: https://github.com/giampaolo/psutil/issues/2455 -.. _2456: https://github.com/giampaolo/psutil/issues/2456 -.. _2457: https://github.com/giampaolo/psutil/issues/2457 -.. _2458: https://github.com/giampaolo/psutil/issues/2458 -.. _2459: https://github.com/giampaolo/psutil/issues/2459 -.. _2460: https://github.com/giampaolo/psutil/issues/2460 -.. _2461: https://github.com/giampaolo/psutil/issues/2461 -.. _2462: https://github.com/giampaolo/psutil/issues/2462 -.. _2463: https://github.com/giampaolo/psutil/issues/2463 -.. _2464: https://github.com/giampaolo/psutil/issues/2464 -.. _2465: https://github.com/giampaolo/psutil/issues/2465 -.. _2466: https://github.com/giampaolo/psutil/issues/2466 -.. _2467: https://github.com/giampaolo/psutil/issues/2467 -.. _2468: https://github.com/giampaolo/psutil/issues/2468 -.. _2469: https://github.com/giampaolo/psutil/issues/2469 -.. _2470: https://github.com/giampaolo/psutil/issues/2470 -.. _2471: https://github.com/giampaolo/psutil/issues/2471 -.. _2472: https://github.com/giampaolo/psutil/issues/2472 -.. _2473: https://github.com/giampaolo/psutil/issues/2473 -.. _2474: https://github.com/giampaolo/psutil/issues/2474 -.. _2475: https://github.com/giampaolo/psutil/issues/2475 -.. _2476: https://github.com/giampaolo/psutil/issues/2476 -.. _2477: https://github.com/giampaolo/psutil/issues/2477 -.. _2478: https://github.com/giampaolo/psutil/issues/2478 -.. _2479: https://github.com/giampaolo/psutil/issues/2479 -.. _2480: https://github.com/giampaolo/psutil/issues/2480 -.. _2481: https://github.com/giampaolo/psutil/issues/2481 -.. _2482: https://github.com/giampaolo/psutil/issues/2482 -.. _2483: https://github.com/giampaolo/psutil/issues/2483 -.. _2484: https://github.com/giampaolo/psutil/issues/2484 -.. _2485: https://github.com/giampaolo/psutil/issues/2485 -.. _2486: https://github.com/giampaolo/psutil/issues/2486 -.. _2487: https://github.com/giampaolo/psutil/issues/2487 -.. _2488: https://github.com/giampaolo/psutil/issues/2488 -.. _2489: https://github.com/giampaolo/psutil/issues/2489 -.. _2490: https://github.com/giampaolo/psutil/issues/2490 -.. _2491: https://github.com/giampaolo/psutil/issues/2491 -.. _2492: https://github.com/giampaolo/psutil/issues/2492 -.. _2493: https://github.com/giampaolo/psutil/issues/2493 -.. _2494: https://github.com/giampaolo/psutil/issues/2494 -.. _2495: https://github.com/giampaolo/psutil/issues/2495 -.. _2496: https://github.com/giampaolo/psutil/issues/2496 -.. _2497: https://github.com/giampaolo/psutil/issues/2497 -.. _2498: https://github.com/giampaolo/psutil/issues/2498 -.. _2499: https://github.com/giampaolo/psutil/issues/2499 -.. _2500: https://github.com/giampaolo/psutil/issues/2500 -.. _2501: https://github.com/giampaolo/psutil/issues/2501 -.. _2502: https://github.com/giampaolo/psutil/issues/2502 -.. _2503: https://github.com/giampaolo/psutil/issues/2503 -.. _2504: https://github.com/giampaolo/psutil/issues/2504 -.. _2505: https://github.com/giampaolo/psutil/issues/2505 -.. _2506: https://github.com/giampaolo/psutil/issues/2506 -.. _2507: https://github.com/giampaolo/psutil/issues/2507 -.. _2508: https://github.com/giampaolo/psutil/issues/2508 -.. _2509: https://github.com/giampaolo/psutil/issues/2509 -.. _2510: https://github.com/giampaolo/psutil/issues/2510 -.. _2511: https://github.com/giampaolo/psutil/issues/2511 -.. _2512: https://github.com/giampaolo/psutil/issues/2512 -.. _2513: https://github.com/giampaolo/psutil/issues/2513 -.. _2514: https://github.com/giampaolo/psutil/issues/2514 -.. _2515: https://github.com/giampaolo/psutil/issues/2515 -.. _2516: https://github.com/giampaolo/psutil/issues/2516 -.. _2517: https://github.com/giampaolo/psutil/issues/2517 -.. _2518: https://github.com/giampaolo/psutil/issues/2518 -.. _2519: https://github.com/giampaolo/psutil/issues/2519 -.. _2520: https://github.com/giampaolo/psutil/issues/2520 -.. _2521: https://github.com/giampaolo/psutil/issues/2521 -.. _2522: https://github.com/giampaolo/psutil/issues/2522 -.. _2523: https://github.com/giampaolo/psutil/issues/2523 -.. _2524: https://github.com/giampaolo/psutil/issues/2524 -.. _2525: https://github.com/giampaolo/psutil/issues/2525 -.. _2526: https://github.com/giampaolo/psutil/issues/2526 -.. _2527: https://github.com/giampaolo/psutil/issues/2527 -.. _2528: https://github.com/giampaolo/psutil/issues/2528 -.. _2529: https://github.com/giampaolo/psutil/issues/2529 -.. _2530: https://github.com/giampaolo/psutil/issues/2530 -.. _2531: https://github.com/giampaolo/psutil/issues/2531 -.. _2532: https://github.com/giampaolo/psutil/issues/2532 -.. _2533: https://github.com/giampaolo/psutil/issues/2533 -.. _2534: https://github.com/giampaolo/psutil/issues/2534 -.. _2535: https://github.com/giampaolo/psutil/issues/2535 -.. _2536: https://github.com/giampaolo/psutil/issues/2536 -.. _2537: https://github.com/giampaolo/psutil/issues/2537 -.. _2538: https://github.com/giampaolo/psutil/issues/2538 -.. _2539: https://github.com/giampaolo/psutil/issues/2539 -.. _2540: https://github.com/giampaolo/psutil/issues/2540 -.. _2541: https://github.com/giampaolo/psutil/issues/2541 -.. _2542: https://github.com/giampaolo/psutil/issues/2542 -.. _2543: https://github.com/giampaolo/psutil/issues/2543 -.. _2544: https://github.com/giampaolo/psutil/issues/2544 -.. _2545: https://github.com/giampaolo/psutil/issues/2545 -.. _2546: https://github.com/giampaolo/psutil/issues/2546 -.. _2547: https://github.com/giampaolo/psutil/issues/2547 -.. _2548: https://github.com/giampaolo/psutil/issues/2548 -.. _2549: https://github.com/giampaolo/psutil/issues/2549 -.. _2550: https://github.com/giampaolo/psutil/issues/2550 -.. _2551: https://github.com/giampaolo/psutil/issues/2551 -.. _2552: https://github.com/giampaolo/psutil/issues/2552 -.. _2553: https://github.com/giampaolo/psutil/issues/2553 -.. _2554: https://github.com/giampaolo/psutil/issues/2554 -.. _2555: https://github.com/giampaolo/psutil/issues/2555 -.. _2556: https://github.com/giampaolo/psutil/issues/2556 -.. _2557: https://github.com/giampaolo/psutil/issues/2557 -.. _2558: https://github.com/giampaolo/psutil/issues/2558 -.. _2559: https://github.com/giampaolo/psutil/issues/2559 -.. _2560: https://github.com/giampaolo/psutil/issues/2560 -.. _2561: https://github.com/giampaolo/psutil/issues/2561 -.. _2562: https://github.com/giampaolo/psutil/issues/2562 -.. _2563: https://github.com/giampaolo/psutil/issues/2563 -.. _2564: https://github.com/giampaolo/psutil/issues/2564 -.. _2565: https://github.com/giampaolo/psutil/issues/2565 -.. _2566: https://github.com/giampaolo/psutil/issues/2566 -.. _2567: https://github.com/giampaolo/psutil/issues/2567 -.. _2568: https://github.com/giampaolo/psutil/issues/2568 -.. _2569: https://github.com/giampaolo/psutil/issues/2569 -.. _2570: https://github.com/giampaolo/psutil/issues/2570 -.. _2571: https://github.com/giampaolo/psutil/issues/2571 -.. _2572: https://github.com/giampaolo/psutil/issues/2572 -.. _2573: https://github.com/giampaolo/psutil/issues/2573 -.. _2574: https://github.com/giampaolo/psutil/issues/2574 -.. _2575: https://github.com/giampaolo/psutil/issues/2575 -.. _2576: https://github.com/giampaolo/psutil/issues/2576 -.. _2577: https://github.com/giampaolo/psutil/issues/2577 -.. _2578: https://github.com/giampaolo/psutil/issues/2578 -.. _2579: https://github.com/giampaolo/psutil/issues/2579 -.. _2580: https://github.com/giampaolo/psutil/issues/2580 -.. _2581: https://github.com/giampaolo/psutil/issues/2581 -.. _2582: https://github.com/giampaolo/psutil/issues/2582 -.. _2583: https://github.com/giampaolo/psutil/issues/2583 -.. _2584: https://github.com/giampaolo/psutil/issues/2584 -.. _2585: https://github.com/giampaolo/psutil/issues/2585 -.. _2586: https://github.com/giampaolo/psutil/issues/2586 -.. _2587: https://github.com/giampaolo/psutil/issues/2587 -.. _2588: https://github.com/giampaolo/psutil/issues/2588 -.. _2589: https://github.com/giampaolo/psutil/issues/2589 -.. _2590: https://github.com/giampaolo/psutil/issues/2590 -.. _2591: https://github.com/giampaolo/psutil/issues/2591 -.. _2592: https://github.com/giampaolo/psutil/issues/2592 -.. _2593: https://github.com/giampaolo/psutil/issues/2593 -.. _2594: https://github.com/giampaolo/psutil/issues/2594 -.. _2595: https://github.com/giampaolo/psutil/issues/2595 -.. _2596: https://github.com/giampaolo/psutil/issues/2596 -.. _2597: https://github.com/giampaolo/psutil/issues/2597 -.. _2598: https://github.com/giampaolo/psutil/issues/2598 -.. _2599: https://github.com/giampaolo/psutil/issues/2599 -.. _2600: https://github.com/giampaolo/psutil/issues/2600 -.. _2601: https://github.com/giampaolo/psutil/issues/2601 -.. _2602: https://github.com/giampaolo/psutil/issues/2602 -.. _2603: https://github.com/giampaolo/psutil/issues/2603 -.. _2604: https://github.com/giampaolo/psutil/issues/2604 -.. _2605: https://github.com/giampaolo/psutil/issues/2605 -.. _2606: https://github.com/giampaolo/psutil/issues/2606 -.. _2607: https://github.com/giampaolo/psutil/issues/2607 -.. _2608: https://github.com/giampaolo/psutil/issues/2608 -.. _2609: https://github.com/giampaolo/psutil/issues/2609 -.. _2610: https://github.com/giampaolo/psutil/issues/2610 -.. _2611: https://github.com/giampaolo/psutil/issues/2611 -.. _2612: https://github.com/giampaolo/psutil/issues/2612 -.. _2613: https://github.com/giampaolo/psutil/issues/2613 -.. _2614: https://github.com/giampaolo/psutil/issues/2614 -.. _2615: https://github.com/giampaolo/psutil/issues/2615 -.. _2616: https://github.com/giampaolo/psutil/issues/2616 -.. _2617: https://github.com/giampaolo/psutil/issues/2617 -.. _2618: https://github.com/giampaolo/psutil/issues/2618 -.. _2619: https://github.com/giampaolo/psutil/issues/2619 -.. _2620: https://github.com/giampaolo/psutil/issues/2620 -.. _2621: https://github.com/giampaolo/psutil/issues/2621 -.. _2622: https://github.com/giampaolo/psutil/issues/2622 -.. _2623: https://github.com/giampaolo/psutil/issues/2623 -.. _2624: https://github.com/giampaolo/psutil/issues/2624 -.. _2625: https://github.com/giampaolo/psutil/issues/2625 -.. _2626: https://github.com/giampaolo/psutil/issues/2626 -.. _2627: https://github.com/giampaolo/psutil/issues/2627 -.. _2628: https://github.com/giampaolo/psutil/issues/2628 -.. _2629: https://github.com/giampaolo/psutil/issues/2629 -.. _2630: https://github.com/giampaolo/psutil/issues/2630 -.. _2631: https://github.com/giampaolo/psutil/issues/2631 -.. _2632: https://github.com/giampaolo/psutil/issues/2632 -.. _2633: https://github.com/giampaolo/psutil/issues/2633 -.. _2634: https://github.com/giampaolo/psutil/issues/2634 -.. _2635: https://github.com/giampaolo/psutil/issues/2635 -.. _2636: https://github.com/giampaolo/psutil/issues/2636 -.. _2637: https://github.com/giampaolo/psutil/issues/2637 -.. _2638: https://github.com/giampaolo/psutil/issues/2638 -.. _2639: https://github.com/giampaolo/psutil/issues/2639 -.. _2640: https://github.com/giampaolo/psutil/issues/2640 -.. _2641: https://github.com/giampaolo/psutil/issues/2641 -.. _2642: https://github.com/giampaolo/psutil/issues/2642 -.. _2643: https://github.com/giampaolo/psutil/issues/2643 -.. _2644: https://github.com/giampaolo/psutil/issues/2644 -.. _2645: https://github.com/giampaolo/psutil/issues/2645 -.. _2646: https://github.com/giampaolo/psutil/issues/2646 -.. _2647: https://github.com/giampaolo/psutil/issues/2647 -.. _2648: https://github.com/giampaolo/psutil/issues/2648 -.. _2649: https://github.com/giampaolo/psutil/issues/2649 -.. _2650: https://github.com/giampaolo/psutil/issues/2650 -.. _2651: https://github.com/giampaolo/psutil/issues/2651 -.. _2652: https://github.com/giampaolo/psutil/issues/2652 -.. _2653: https://github.com/giampaolo/psutil/issues/2653 -.. _2654: https://github.com/giampaolo/psutil/issues/2654 -.. _2655: https://github.com/giampaolo/psutil/issues/2655 -.. _2656: https://github.com/giampaolo/psutil/issues/2656 -.. _2657: https://github.com/giampaolo/psutil/issues/2657 -.. _2658: https://github.com/giampaolo/psutil/issues/2658 -.. _2659: https://github.com/giampaolo/psutil/issues/2659 -.. _2660: https://github.com/giampaolo/psutil/issues/2660 -.. _2661: https://github.com/giampaolo/psutil/issues/2661 -.. _2662: https://github.com/giampaolo/psutil/issues/2662 -.. _2663: https://github.com/giampaolo/psutil/issues/2663 -.. _2664: https://github.com/giampaolo/psutil/issues/2664 -.. _2665: https://github.com/giampaolo/psutil/issues/2665 -.. _2666: https://github.com/giampaolo/psutil/issues/2666 -.. _2667: https://github.com/giampaolo/psutil/issues/2667 -.. _2668: https://github.com/giampaolo/psutil/issues/2668 -.. _2669: https://github.com/giampaolo/psutil/issues/2669 -.. _2670: https://github.com/giampaolo/psutil/issues/2670 -.. _2671: https://github.com/giampaolo/psutil/issues/2671 -.. _2672: https://github.com/giampaolo/psutil/issues/2672 -.. _2673: https://github.com/giampaolo/psutil/issues/2673 -.. _2674: https://github.com/giampaolo/psutil/issues/2674 -.. _2675: https://github.com/giampaolo/psutil/issues/2675 -.. _2676: https://github.com/giampaolo/psutil/issues/2676 -.. _2677: https://github.com/giampaolo/psutil/issues/2677 -.. _2678: https://github.com/giampaolo/psutil/issues/2678 -.. _2679: https://github.com/giampaolo/psutil/issues/2679 -.. _2680: https://github.com/giampaolo/psutil/issues/2680 -.. _2681: https://github.com/giampaolo/psutil/issues/2681 -.. _2682: https://github.com/giampaolo/psutil/issues/2682 -.. _2683: https://github.com/giampaolo/psutil/issues/2683 -.. _2684: https://github.com/giampaolo/psutil/issues/2684 -.. _2685: https://github.com/giampaolo/psutil/issues/2685 -.. _2686: https://github.com/giampaolo/psutil/issues/2686 -.. _2687: https://github.com/giampaolo/psutil/issues/2687 -.. _2688: https://github.com/giampaolo/psutil/issues/2688 -.. _2689: https://github.com/giampaolo/psutil/issues/2689 -.. _2690: https://github.com/giampaolo/psutil/issues/2690 -.. _2691: https://github.com/giampaolo/psutil/issues/2691 -.. _2692: https://github.com/giampaolo/psutil/issues/2692 -.. _2693: https://github.com/giampaolo/psutil/issues/2693 -.. _2694: https://github.com/giampaolo/psutil/issues/2694 -.. _2695: https://github.com/giampaolo/psutil/issues/2695 -.. _2696: https://github.com/giampaolo/psutil/issues/2696 -.. _2697: https://github.com/giampaolo/psutil/issues/2697 -.. _2698: https://github.com/giampaolo/psutil/issues/2698 -.. _2699: https://github.com/giampaolo/psutil/issues/2699 -.. _2700: https://github.com/giampaolo/psutil/issues/2700 -.. _2701: https://github.com/giampaolo/psutil/issues/2701 -.. _2702: https://github.com/giampaolo/psutil/issues/2702 -.. _2703: https://github.com/giampaolo/psutil/issues/2703 -.. _2704: https://github.com/giampaolo/psutil/issues/2704 -.. _2705: https://github.com/giampaolo/psutil/issues/2705 -.. _2706: https://github.com/giampaolo/psutil/issues/2706 -.. _2707: https://github.com/giampaolo/psutil/issues/2707 -.. _2708: https://github.com/giampaolo/psutil/issues/2708 -.. _2709: https://github.com/giampaolo/psutil/issues/2709 -.. _2710: https://github.com/giampaolo/psutil/issues/2710 -.. _2711: https://github.com/giampaolo/psutil/issues/2711 -.. _2712: https://github.com/giampaolo/psutil/issues/2712 -.. _2713: https://github.com/giampaolo/psutil/issues/2713 -.. _2714: https://github.com/giampaolo/psutil/issues/2714 -.. _2715: https://github.com/giampaolo/psutil/issues/2715 -.. _2716: https://github.com/giampaolo/psutil/issues/2716 -.. _2717: https://github.com/giampaolo/psutil/issues/2717 -.. _2718: https://github.com/giampaolo/psutil/issues/2718 -.. _2719: https://github.com/giampaolo/psutil/issues/2719 -.. _2720: https://github.com/giampaolo/psutil/issues/2720 -.. _2721: https://github.com/giampaolo/psutil/issues/2721 -.. _2722: https://github.com/giampaolo/psutil/issues/2722 -.. _2723: https://github.com/giampaolo/psutil/issues/2723 -.. _2724: https://github.com/giampaolo/psutil/issues/2724 -.. _2725: https://github.com/giampaolo/psutil/issues/2725 -.. _2726: https://github.com/giampaolo/psutil/issues/2726 -.. _2727: https://github.com/giampaolo/psutil/issues/2727 -.. _2728: https://github.com/giampaolo/psutil/issues/2728 -.. _2729: https://github.com/giampaolo/psutil/issues/2729 -.. _2730: https://github.com/giampaolo/psutil/issues/2730 -.. _2731: https://github.com/giampaolo/psutil/issues/2731 -.. _2732: https://github.com/giampaolo/psutil/issues/2732 -.. _2733: https://github.com/giampaolo/psutil/issues/2733 -.. _2734: https://github.com/giampaolo/psutil/issues/2734 -.. _2735: https://github.com/giampaolo/psutil/issues/2735 -.. _2736: https://github.com/giampaolo/psutil/issues/2736 -.. _2737: https://github.com/giampaolo/psutil/issues/2737 -.. _2738: https://github.com/giampaolo/psutil/issues/2738 -.. _2739: https://github.com/giampaolo/psutil/issues/2739 -.. _2740: https://github.com/giampaolo/psutil/issues/2740 -.. _2741: https://github.com/giampaolo/psutil/issues/2741 -.. _2742: https://github.com/giampaolo/psutil/issues/2742 -.. _2743: https://github.com/giampaolo/psutil/issues/2743 -.. _2744: https://github.com/giampaolo/psutil/issues/2744 -.. _2745: https://github.com/giampaolo/psutil/issues/2745 -.. _2746: https://github.com/giampaolo/psutil/issues/2746 -.. _2747: https://github.com/giampaolo/psutil/issues/2747 -.. _2748: https://github.com/giampaolo/psutil/issues/2748 -.. _2749: https://github.com/giampaolo/psutil/issues/2749 -.. _2750: https://github.com/giampaolo/psutil/issues/2750 -.. _2751: https://github.com/giampaolo/psutil/issues/2751 -.. _2752: https://github.com/giampaolo/psutil/issues/2752 -.. _2753: https://github.com/giampaolo/psutil/issues/2753 -.. _2754: https://github.com/giampaolo/psutil/issues/2754 -.. _2755: https://github.com/giampaolo/psutil/issues/2755 -.. _2756: https://github.com/giampaolo/psutil/issues/2756 -.. _2757: https://github.com/giampaolo/psutil/issues/2757 -.. _2758: https://github.com/giampaolo/psutil/issues/2758 -.. _2759: https://github.com/giampaolo/psutil/issues/2759 -.. _2760: https://github.com/giampaolo/psutil/issues/2760 -.. _2761: https://github.com/giampaolo/psutil/issues/2761 -.. _2762: https://github.com/giampaolo/psutil/issues/2762 -.. _2763: https://github.com/giampaolo/psutil/issues/2763 -.. _2764: https://github.com/giampaolo/psutil/issues/2764 -.. _2765: https://github.com/giampaolo/psutil/issues/2765 -.. _2766: https://github.com/giampaolo/psutil/issues/2766 -.. _2767: https://github.com/giampaolo/psutil/issues/2767 -.. _2768: https://github.com/giampaolo/psutil/issues/2768 -.. _2769: https://github.com/giampaolo/psutil/issues/2769 -.. _2770: https://github.com/giampaolo/psutil/issues/2770 -.. _2771: https://github.com/giampaolo/psutil/issues/2771 -.. _2772: https://github.com/giampaolo/psutil/issues/2772 -.. _2773: https://github.com/giampaolo/psutil/issues/2773 -.. _2774: https://github.com/giampaolo/psutil/issues/2774 -.. _2775: https://github.com/giampaolo/psutil/issues/2775 -.. _2776: https://github.com/giampaolo/psutil/issues/2776 -.. _2777: https://github.com/giampaolo/psutil/issues/2777 -.. _2778: https://github.com/giampaolo/psutil/issues/2778 -.. _2779: https://github.com/giampaolo/psutil/issues/2779 -.. _2780: https://github.com/giampaolo/psutil/issues/2780 -.. _2781: https://github.com/giampaolo/psutil/issues/2781 -.. _2782: https://github.com/giampaolo/psutil/issues/2782 -.. _2783: https://github.com/giampaolo/psutil/issues/2783 -.. _2784: https://github.com/giampaolo/psutil/issues/2784 -.. _2785: https://github.com/giampaolo/psutil/issues/2785 -.. _2786: https://github.com/giampaolo/psutil/issues/2786 -.. _2787: https://github.com/giampaolo/psutil/issues/2787 -.. _2788: https://github.com/giampaolo/psutil/issues/2788 -.. _2789: https://github.com/giampaolo/psutil/issues/2789 -.. _2790: https://github.com/giampaolo/psutil/issues/2790 -.. _2791: https://github.com/giampaolo/psutil/issues/2791 -.. _2792: https://github.com/giampaolo/psutil/issues/2792 -.. _2793: https://github.com/giampaolo/psutil/issues/2793 -.. _2794: https://github.com/giampaolo/psutil/issues/2794 -.. _2795: https://github.com/giampaolo/psutil/issues/2795 -.. _2796: https://github.com/giampaolo/psutil/issues/2796 -.. _2797: https://github.com/giampaolo/psutil/issues/2797 -.. _2798: https://github.com/giampaolo/psutil/issues/2798 -.. _2799: https://github.com/giampaolo/psutil/issues/2799 -.. _2800: https://github.com/giampaolo/psutil/issues/2800 -.. _2801: https://github.com/giampaolo/psutil/issues/2801 -.. _2802: https://github.com/giampaolo/psutil/issues/2802 -.. _2803: https://github.com/giampaolo/psutil/issues/2803 -.. _2804: https://github.com/giampaolo/psutil/issues/2804 -.. _2805: https://github.com/giampaolo/psutil/issues/2805 -.. _2806: https://github.com/giampaolo/psutil/issues/2806 -.. _2807: https://github.com/giampaolo/psutil/issues/2807 -.. _2808: https://github.com/giampaolo/psutil/issues/2808 -.. _2809: https://github.com/giampaolo/psutil/issues/2809 -.. _2810: https://github.com/giampaolo/psutil/issues/2810 -.. _2811: https://github.com/giampaolo/psutil/issues/2811 -.. _2812: https://github.com/giampaolo/psutil/issues/2812 -.. _2813: https://github.com/giampaolo/psutil/issues/2813 -.. _2814: https://github.com/giampaolo/psutil/issues/2814 -.. _2815: https://github.com/giampaolo/psutil/issues/2815 -.. _2816: https://github.com/giampaolo/psutil/issues/2816 -.. _2817: https://github.com/giampaolo/psutil/issues/2817 -.. _2818: https://github.com/giampaolo/psutil/issues/2818 -.. _2819: https://github.com/giampaolo/psutil/issues/2819 -.. _2820: https://github.com/giampaolo/psutil/issues/2820 -.. _2821: https://github.com/giampaolo/psutil/issues/2821 -.. _2822: https://github.com/giampaolo/psutil/issues/2822 -.. _2823: https://github.com/giampaolo/psutil/issues/2823 -.. _2824: https://github.com/giampaolo/psutil/issues/2824 -.. _2825: https://github.com/giampaolo/psutil/issues/2825 -.. _2826: https://github.com/giampaolo/psutil/issues/2826 -.. _2827: https://github.com/giampaolo/psutil/issues/2827 -.. _2828: https://github.com/giampaolo/psutil/issues/2828 -.. _2829: https://github.com/giampaolo/psutil/issues/2829 -.. _2830: https://github.com/giampaolo/psutil/issues/2830 -.. _2831: https://github.com/giampaolo/psutil/issues/2831 -.. _2832: https://github.com/giampaolo/psutil/issues/2832 -.. _2833: https://github.com/giampaolo/psutil/issues/2833 -.. _2834: https://github.com/giampaolo/psutil/issues/2834 -.. _2835: https://github.com/giampaolo/psutil/issues/2835 -.. _2836: https://github.com/giampaolo/psutil/issues/2836 -.. _2837: https://github.com/giampaolo/psutil/issues/2837 -.. _2838: https://github.com/giampaolo/psutil/issues/2838 -.. _2839: https://github.com/giampaolo/psutil/issues/2839 -.. _2840: https://github.com/giampaolo/psutil/issues/2840 -.. _2841: https://github.com/giampaolo/psutil/issues/2841 -.. _2842: https://github.com/giampaolo/psutil/issues/2842 -.. _2843: https://github.com/giampaolo/psutil/issues/2843 -.. _2844: https://github.com/giampaolo/psutil/issues/2844 -.. _2845: https://github.com/giampaolo/psutil/issues/2845 -.. _2846: https://github.com/giampaolo/psutil/issues/2846 -.. _2847: https://github.com/giampaolo/psutil/issues/2847 -.. _2848: https://github.com/giampaolo/psutil/issues/2848 -.. _2849: https://github.com/giampaolo/psutil/issues/2849 -.. _2850: https://github.com/giampaolo/psutil/issues/2850 -.. _2851: https://github.com/giampaolo/psutil/issues/2851 -.. _2852: https://github.com/giampaolo/psutil/issues/2852 -.. _2853: https://github.com/giampaolo/psutil/issues/2853 -.. _2854: https://github.com/giampaolo/psutil/issues/2854 -.. _2855: https://github.com/giampaolo/psutil/issues/2855 -.. _2856: https://github.com/giampaolo/psutil/issues/2856 -.. _2857: https://github.com/giampaolo/psutil/issues/2857 -.. _2858: https://github.com/giampaolo/psutil/issues/2858 -.. _2859: https://github.com/giampaolo/psutil/issues/2859 -.. _2860: https://github.com/giampaolo/psutil/issues/2860 -.. _2861: https://github.com/giampaolo/psutil/issues/2861 -.. _2862: https://github.com/giampaolo/psutil/issues/2862 -.. _2863: https://github.com/giampaolo/psutil/issues/2863 -.. _2864: https://github.com/giampaolo/psutil/issues/2864 -.. _2865: https://github.com/giampaolo/psutil/issues/2865 -.. _2866: https://github.com/giampaolo/psutil/issues/2866 -.. _2867: https://github.com/giampaolo/psutil/issues/2867 -.. _2868: https://github.com/giampaolo/psutil/issues/2868 -.. _2869: https://github.com/giampaolo/psutil/issues/2869 -.. _2870: https://github.com/giampaolo/psutil/issues/2870 -.. _2871: https://github.com/giampaolo/psutil/issues/2871 -.. _2872: https://github.com/giampaolo/psutil/issues/2872 -.. _2873: https://github.com/giampaolo/psutil/issues/2873 -.. _2874: https://github.com/giampaolo/psutil/issues/2874 -.. _2875: https://github.com/giampaolo/psutil/issues/2875 -.. _2876: https://github.com/giampaolo/psutil/issues/2876 -.. _2877: https://github.com/giampaolo/psutil/issues/2877 -.. _2878: https://github.com/giampaolo/psutil/issues/2878 -.. _2879: https://github.com/giampaolo/psutil/issues/2879 -.. _2880: https://github.com/giampaolo/psutil/issues/2880 -.. _2881: https://github.com/giampaolo/psutil/issues/2881 -.. _2882: https://github.com/giampaolo/psutil/issues/2882 -.. _2883: https://github.com/giampaolo/psutil/issues/2883 -.. _2884: https://github.com/giampaolo/psutil/issues/2884 -.. _2885: https://github.com/giampaolo/psutil/issues/2885 -.. _2886: https://github.com/giampaolo/psutil/issues/2886 -.. _2887: https://github.com/giampaolo/psutil/issues/2887 -.. _2888: https://github.com/giampaolo/psutil/issues/2888 -.. _2889: https://github.com/giampaolo/psutil/issues/2889 -.. _2890: https://github.com/giampaolo/psutil/issues/2890 -.. _2891: https://github.com/giampaolo/psutil/issues/2891 -.. _2892: https://github.com/giampaolo/psutil/issues/2892 -.. _2893: https://github.com/giampaolo/psutil/issues/2893 -.. _2894: https://github.com/giampaolo/psutil/issues/2894 -.. _2895: https://github.com/giampaolo/psutil/issues/2895 -.. _2896: https://github.com/giampaolo/psutil/issues/2896 -.. _2897: https://github.com/giampaolo/psutil/issues/2897 -.. _2898: https://github.com/giampaolo/psutil/issues/2898 -.. _2899: https://github.com/giampaolo/psutil/issues/2899 -.. _2900: https://github.com/giampaolo/psutil/issues/2900 -.. _2901: https://github.com/giampaolo/psutil/issues/2901 -.. _2902: https://github.com/giampaolo/psutil/issues/2902 -.. _2903: https://github.com/giampaolo/psutil/issues/2903 -.. _2904: https://github.com/giampaolo/psutil/issues/2904 -.. _2905: https://github.com/giampaolo/psutil/issues/2905 -.. _2906: https://github.com/giampaolo/psutil/issues/2906 -.. _2907: https://github.com/giampaolo/psutil/issues/2907 -.. _2908: https://github.com/giampaolo/psutil/issues/2908 -.. _2909: https://github.com/giampaolo/psutil/issues/2909 -.. _2910: https://github.com/giampaolo/psutil/issues/2910 -.. _2911: https://github.com/giampaolo/psutil/issues/2911 -.. _2912: https://github.com/giampaolo/psutil/issues/2912 -.. _2913: https://github.com/giampaolo/psutil/issues/2913 -.. _2914: https://github.com/giampaolo/psutil/issues/2914 -.. _2915: https://github.com/giampaolo/psutil/issues/2915 -.. _2916: https://github.com/giampaolo/psutil/issues/2916 -.. _2917: https://github.com/giampaolo/psutil/issues/2917 -.. _2918: https://github.com/giampaolo/psutil/issues/2918 -.. _2919: https://github.com/giampaolo/psutil/issues/2919 -.. _2920: https://github.com/giampaolo/psutil/issues/2920 -.. _2921: https://github.com/giampaolo/psutil/issues/2921 -.. _2922: https://github.com/giampaolo/psutil/issues/2922 -.. _2923: https://github.com/giampaolo/psutil/issues/2923 -.. _2924: https://github.com/giampaolo/psutil/issues/2924 -.. _2925: https://github.com/giampaolo/psutil/issues/2925 -.. _2926: https://github.com/giampaolo/psutil/issues/2926 -.. _2927: https://github.com/giampaolo/psutil/issues/2927 -.. _2928: https://github.com/giampaolo/psutil/issues/2928 -.. _2929: https://github.com/giampaolo/psutil/issues/2929 -.. _2930: https://github.com/giampaolo/psutil/issues/2930 -.. _2931: https://github.com/giampaolo/psutil/issues/2931 -.. _2932: https://github.com/giampaolo/psutil/issues/2932 -.. _2933: https://github.com/giampaolo/psutil/issues/2933 -.. _2934: https://github.com/giampaolo/psutil/issues/2934 -.. _2935: https://github.com/giampaolo/psutil/issues/2935 -.. _2936: https://github.com/giampaolo/psutil/issues/2936 -.. _2937: https://github.com/giampaolo/psutil/issues/2937 -.. _2938: https://github.com/giampaolo/psutil/issues/2938 -.. _2939: https://github.com/giampaolo/psutil/issues/2939 -.. _2940: https://github.com/giampaolo/psutil/issues/2940 -.. _2941: https://github.com/giampaolo/psutil/issues/2941 -.. _2942: https://github.com/giampaolo/psutil/issues/2942 -.. _2943: https://github.com/giampaolo/psutil/issues/2943 -.. _2944: https://github.com/giampaolo/psutil/issues/2944 -.. _2945: https://github.com/giampaolo/psutil/issues/2945 -.. _2946: https://github.com/giampaolo/psutil/issues/2946 -.. _2947: https://github.com/giampaolo/psutil/issues/2947 -.. _2948: https://github.com/giampaolo/psutil/issues/2948 -.. _2949: https://github.com/giampaolo/psutil/issues/2949 -.. _2950: https://github.com/giampaolo/psutil/issues/2950 -.. _2951: https://github.com/giampaolo/psutil/issues/2951 -.. _2952: https://github.com/giampaolo/psutil/issues/2952 -.. _2953: https://github.com/giampaolo/psutil/issues/2953 -.. _2954: https://github.com/giampaolo/psutil/issues/2954 -.. _2955: https://github.com/giampaolo/psutil/issues/2955 -.. _2956: https://github.com/giampaolo/psutil/issues/2956 -.. _2957: https://github.com/giampaolo/psutil/issues/2957 -.. _2958: https://github.com/giampaolo/psutil/issues/2958 -.. _2959: https://github.com/giampaolo/psutil/issues/2959 -.. _2960: https://github.com/giampaolo/psutil/issues/2960 -.. _2961: https://github.com/giampaolo/psutil/issues/2961 -.. _2962: https://github.com/giampaolo/psutil/issues/2962 -.. _2963: https://github.com/giampaolo/psutil/issues/2963 -.. _2964: https://github.com/giampaolo/psutil/issues/2964 -.. _2965: https://github.com/giampaolo/psutil/issues/2965 -.. _2966: https://github.com/giampaolo/psutil/issues/2966 -.. _2967: https://github.com/giampaolo/psutil/issues/2967 -.. _2968: https://github.com/giampaolo/psutil/issues/2968 -.. _2969: https://github.com/giampaolo/psutil/issues/2969 -.. _2970: https://github.com/giampaolo/psutil/issues/2970 -.. _2971: https://github.com/giampaolo/psutil/issues/2971 -.. _2972: https://github.com/giampaolo/psutil/issues/2972 -.. _2973: https://github.com/giampaolo/psutil/issues/2973 -.. _2974: https://github.com/giampaolo/psutil/issues/2974 -.. _2975: https://github.com/giampaolo/psutil/issues/2975 -.. _2976: https://github.com/giampaolo/psutil/issues/2976 -.. _2977: https://github.com/giampaolo/psutil/issues/2977 -.. _2978: https://github.com/giampaolo/psutil/issues/2978 -.. _2979: https://github.com/giampaolo/psutil/issues/2979 -.. _2980: https://github.com/giampaolo/psutil/issues/2980 -.. _2981: https://github.com/giampaolo/psutil/issues/2981 -.. _2982: https://github.com/giampaolo/psutil/issues/2982 -.. _2983: https://github.com/giampaolo/psutil/issues/2983 -.. _2984: https://github.com/giampaolo/psutil/issues/2984 -.. _2985: https://github.com/giampaolo/psutil/issues/2985 -.. _2986: https://github.com/giampaolo/psutil/issues/2986 -.. _2987: https://github.com/giampaolo/psutil/issues/2987 -.. _2988: https://github.com/giampaolo/psutil/issues/2988 -.. _2989: https://github.com/giampaolo/psutil/issues/2989 -.. _2990: https://github.com/giampaolo/psutil/issues/2990 -.. _2991: https://github.com/giampaolo/psutil/issues/2991 -.. _2992: https://github.com/giampaolo/psutil/issues/2992 -.. _2993: https://github.com/giampaolo/psutil/issues/2993 -.. _2994: https://github.com/giampaolo/psutil/issues/2994 -.. _2995: https://github.com/giampaolo/psutil/issues/2995 -.. _2996: https://github.com/giampaolo/psutil/issues/2996 -.. _2997: https://github.com/giampaolo/psutil/issues/2997 -.. _2998: https://github.com/giampaolo/psutil/issues/2998 -.. _2999: https://github.com/giampaolo/psutil/issues/2999 -.. _3000: https://github.com/giampaolo/psutil/issues/3000 +- https://psutil.readthedocs.io/changelog +- https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst (source) diff --git a/INSTALL.rst b/INSTALL.rst new file mode 100644 index 0000000000..33ac744d46 --- /dev/null +++ b/INSTALL.rst @@ -0,0 +1,4 @@ +Installation instructions have moved to: + +- https://psutil.readthedocs.io/install +- https://github.com/giampaolo/psutil/blob/master/docs/install.rst (source) diff --git a/MANIFEST.in b/MANIFEST.in index d3a4da62e2..8b26f85e4b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include .gitignore include CONTRIBUTING.md include CREDITS include HISTORY.rst +include INSTALL.rst include LICENSE include MANIFEST.in include Makefile @@ -13,6 +14,8 @@ include docs/.readthedocs.yaml include docs/DEVNOTES include docs/Makefile include docs/README +include docs/_ext/add_home_link.py +include docs/_ext/changelog_anchors.py include docs/_links.rst include docs/_static/copybutton.js include docs/_static/css/custom.css @@ -21,6 +24,7 @@ include docs/_static/sidebar.js include docs/_templates/layout.html include docs/api.rst include docs/auto-build.sh +include docs/changelog.rst include docs/conf.py include docs/devguide.rst include docs/faq.rst @@ -171,7 +175,6 @@ include scripts/internal/print_dist.py include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_sysinfo.py -include scripts/internal/print_timeline.py include scripts/internal/purge_installation.py include scripts/internal/test_python2_setup_py.py include scripts/iotop.py diff --git a/Makefile b/Makefile index de25dbf846..d76bb60d33 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ -# Shortcuts for various tasks (UNIX only). -# To use a specific Python version run: "make install PYTHON=python3.3" -# You can set the variables below from the command line. +# Shortcuts for various development tasks. +# +# - To use this on Windows install Git For Windows first, then launch a Git +# Bash Shell. +# - To use a specific Python version run: `make install PYTHON=python3.3`. +# - To append an argument to a command use ARGS, e.g: `make test ARGS="-k +# some_test`. # Configurable PYTHON = python3 @@ -323,13 +327,9 @@ pre-release: ## Check if we're ready to produce a new release. res = requests.get('https://pypi.org/pypi/psutil/json', timeout=5); \ versions = sorted(res.json()['releases'], key=parse, reverse=True); \ sys.exit('version %r already exists on PYPI' % __version__) if __version__ in versions else 0" - @$(PYTHON) -c \ - "from psutil import __version__ as ver; \ - doc = open('docs/index.rst').read(); \ - history = open('HISTORY.rst').read(); \ - assert ver in doc, '%r not found in docs/index.rst' % ver; \ - assert ver in history, '%r not found in HISTORY.rst' % ver; \ - assert 'XXXX' not in history, 'XXXX found in HISTORY.rst';" + @ver=$$($(PYTHON) -c "from psutil import __version__; print(__version__)"); \ + grep -q "$$ver" docs/changelog.rst || { echo "ERR: version $$ver not found in docs/changelog.rst"; exit 1; }; \ + grep -q "$$ver" docs/timeline.rst || { echo "ERR: version $$ver not found in docs/timeline.rst"; exit 1; } $(MAKE) print-hashes $(MAKE) print-dist @@ -349,9 +349,6 @@ git-tag-release: ## Git-tag a new release. print-announce: ## Print announce of new release. @$(PYTHON) scripts/internal/print_announce.py -print-timeline: ## Print releases' timeline. - @$(PYTHON) scripts/internal/print_timeline.py - print-access-denied: ## Print AD exceptions $(PYTHON) scripts/internal/print_access_denied.py diff --git a/README.rst b/README.rst index c6de10976b..cb765a2997 100644 --- a/README.rst +++ b/README.rst @@ -59,13 +59,12 @@

Home    - Install    Documentation    Download    Forum    Blog    Funding    - What's new    + What's new   
Summary diff --git a/docs/_ext/add_home_link.py b/docs/_ext/add_home_link.py new file mode 100644 index 0000000000..250c09e89b --- /dev/null +++ b/docs/_ext/add_home_link.py @@ -0,0 +1,24 @@ +# 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. + +"""Sphinx extension that prepends a 'Home' link to the TOC sidebar. +This script gets called on `make html`. +""" + +from sphinx.application import Sphinx + + +def add_home_link(app: Sphinx, pagename, templatename, context, doctree): + if "toctree" in context: + toctree_func = context["toctree"] + toc_html = toctree_func( + maxdepth=2, collapse=False, includehidden=False + ) + # prepend Home link manually + home_link = '
  • Home
  • ' + context["toctree"] = lambda **_kw: home_link + toc_html + + +def setup(app: Sphinx): + app.connect("html-page-context", add_home_link) diff --git a/docs/_ext/changelog_anchors.py b/docs/_ext/changelog_anchors.py new file mode 100644 index 0000000000..4644a79392 --- /dev/null +++ b/docs/_ext/changelog_anchors.py @@ -0,0 +1,37 @@ +# 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. + +"""Sphinx extension for adding anchors to each section in +docs/changelog.rst. + +This script gets called on `make html`, and adds numeric anchors for +version titles (e.g., 7.2.3 -> #723). +""" + +import re + +from docutils import nodes + +VERSION_RE = re.compile(r"^(\d+\.\d+\.\d+)") + + +def add_version_anchors(app, doctree, docname): + if docname != "changelog": + return + + for node in doctree.traverse(nodes.section): + title = node.next_node(nodes.title) + if not title: + continue + + text = title.astext() + m = VERSION_RE.match(text) + if m: + anchor = m.group(1).replace(".", "") + if anchor not in node["ids"]: + node["ids"].insert(0, anchor) + + +def setup(app): + app.connect("doctree-resolved", add_version_anchors) diff --git a/docs/_links.rst b/docs/_links.rst index 7fe64f8214..ca51ee1876 100644 --- a/docs/_links.rst +++ b/docs/_links.rst @@ -2,7 +2,6 @@ .. _`BPO-12442`: https://bugs.python.org/issue12442 .. _`BPO-6973`: https://bugs.python.org/issue6973 .. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 -.. _`DEVGUIDE.rst`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ .. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get @@ -75,9 +74,3 @@ .. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex .. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass .. _`TerminateProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess - -.. === Issues - -.. _`#1007`: https://github.com/giampaolo/psutil/issues/1007 -.. _`#1040`: https://github.com/giampaolo/psutil/issues/1040 -.. _`#883`: https://github.com/giampaolo/psutil/issues/883 diff --git a/docs/api.rst b/docs/api.rst index e691503d71..d52af0fc5e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -904,7 +904,7 @@ Other system info .. note:: on Windows this function may return a time which is off by 1 second if it's - used across different processes (see issue `#1007`_). + used across different processes (see issue :gh:`1007`). .. function:: users() diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000000..141dfea6ad --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,3078 @@ +.. currentmodule:: psutil +.. include:: _links.rst + +Changelog +========= + +8.0.0 (IN DEVELOPMENT) +^^^^^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1946`: add inline type hints to all public APIs in `psutil/__init__.py`. + Type checkers (mypy, pyright, etc.) can now statically verify code that uses + psutil. No runtime behavior is changed; the annotations are purely + informational. +- :gh:`2729`: New :meth:`Process.page_faults()` method, returning a ``(minor, + major)`` namedtuple. +- :gh:`2745`: Drastically improve :func:`virtual_memory()` docstring, which is + now more detailed, and includes a table with all the available metrics on + each platform. +- :gh:`2731`, :gh:`2736`, :gh:`2723`, :gh:`2733`: Reorganization of process + memory APIs. + + - Add new :meth:`Process.memory_info_ex()` method, which extends + :meth:`Process.memory_info()` with platform-specific metrics: + + - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, + *swap*, *hugetlb* + - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, + *phys_footprint* + - Windows: *virtual*, *peak_virtual* + + - Add new :meth:`Process.memory_footprint()` method, which returns *uss*, + *pss* and *swap* metrics (what :meth:`Process.memory_full_info()` used to + return, which is now **deprecated**). + + - :meth:`Process.memory_info()` named tuple changed: + + - BSD: added *peak_rss*. + + - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated + aliases returning 0 and emitting `DeprecationWarning` are kept. + + - macOS: *pfaults* and *pageins* removed with **no + backward-compataliases**. Use :meth:`Process.page_faults()` instead. + + - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. + At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, + *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex()`. All + these old names still work but raise `DeprecationWarning`. + + - :meth:`Process.memory_full_info()` is **deprecated**. Use the new + :meth:`Process.memory_footprint()` instead. + +- :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times()` + has been normalized on all platforms, and the first 3 fields are now always + ``user, system, idle``. See compatibility notes below. +- :gh:`2751`: convert all named tuples in `psutil/_ntuples.py`_ from + ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type + annotations**. This makes the classes self-documenting, effectively turning + this module into a readable API reference. +- :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, + :class:`ConnectionStatus`, + :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) + grouping related constants. The individual top-level constants (e.g. + ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases + for the corresponding enum members. +- :gh:`2754`: standardize :func:`sensors_battery()`'s `percent` so that it + returns a `float` instead of `int` on all systems, not only Linux. +- :gh:`2757`: split the documentation from a single-page HTML document into + multiple sub-sections. Sections now include separate pages for API reference, + installation, release timeline, FAQs, and more. +- :gh:`2760`: moved 18 years old HISTORY.rst into docs/changelog.rst for + better integration. Original file remains in the root with a note pointing to + the new location. + +**Bug fixes** + +- :gh:`2726`, [macOS]: :meth:`Process.num_ctx_switches()` return an unusual + high number due to a C type precision issue. +- :gh:`2411` [macOS]: :meth:`Process.cpu_times()` and + :meth:`Process.cpu_percent()` calculation on macOS x86_64 (arm64 is fine) was + highly inaccurate (41.67x lower). +- :gh:`2732`, [Linux]: net_if_duplex_speed: handle EBUSY from + ioctl(SIOCETHTOOL). +- :gh:`2744`, [NetBSD]: fix possible double `free()` in :func:`swap_memory()`. +- :gh:`2746`, [FreeBSD]: :meth:`Process.memory_maps()`, `rss` and `private` + fields, are erroneously reported in memory pages instead of bytes. Other + platforms (Linux, macOS, Windows) return bytes. + +**Compatibility notes** + +Changes that break backwards compatibility. + +- Dropped support for Python 3.6. + +Named tuples: + +- :func:`cpu_times()`: + + - On Linux, macOS and BSD the field order of the returned named tuple + changed: ``user, system, idle`` are now always the first 3 fields on all + platforms, with platform-specific fields (e.g. ``nice``) following. + Positional access (e.g. ``cpu_times()[3]``) may silently return the wrong + field. Always use attribute access instead (e.g. ``cpu_times().idle``). + +- :meth:`Process.memory_info()`: + + - The returned named tuple changed size and field order. Positional access + (e.g. ``p.memory_info()[3]`` or ``a, b, c = p.memory_info()``) may break or + silently return the wrong field. Always use attribute access instead (e.g. + ``p.memory_info().rss``). + +Enums: + +- :meth:`Process.status()` now returns a :class:`psutil.ProcessStatus` enum + member instead of a plain ``str``. Since :class:`psutil.ProcessStatus` is a + ``StrEnum``, it compares equal to its string value (e.g. ``p.status() == + "running"`` still works), but ``repr()`` and ``type()`` differ. Use + :data:`psutil.STATUS_RUNNING` and friends as before; + ``psutil.STATUS_RUNNING`` is now an alias for the enum member. + +- :func:`net_connections()` and :meth:`Process.net_connections()`: the + ``status`` field now returns a :class:`psutil.ConnectionStatus` enum member + instead of a plain ``str``. Same ``StrEnum`` compatibility rules as above + apply. + +- ``RLIMIT_*`` / ``RLIM_*`` constants (Linux, FreeBSD): these are now members + of the :class:`psutil.ProcessRlimit` ``IntEnum`` instead of plain integers. + Since ``IntEnum`` compares equal to integers, existing code using them as + arguments to :meth:`Process.rlimit` is unaffected. + +7.2.3 — 2026-02-08 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`2715`, [Linux]: ``wait_pid_pidfd_open()`` (from :meth:`Process.wait()`) + crashes with ``EINVAL`` due to kernel race condition. + +7.2.2 — 2026-01-28 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2705`: [Linux]: :meth:`Process.wait()` now uses ``pidfd_open()`` + + ``poll()`` for waiting, resulting in no busy loop and faster response times. + Requires Linux >= 5.3 and Python >= 3.9. Falls back to traditional polling if + unavailable. +- :gh:`2705`: [macOS], [BSD]: :meth:`Process.wait()` now uses ``kqueue()`` for + waiting, resulting in no busy loop and faster response times. + +**Bug fixes** + +- :gh:`2701`, [macOS]: fix compilation error on macOS < 10.7. (patch by Sergey + Fedorov) +- :gh:`2707`, [macOS]: fix potential memory leaks in error paths of + `Process.memory_full_info()` and `Process.threads()`. +- :gh:`2708`, [macOS]: :meth:`Process.cmdline()` and :meth:`Process.environ()` + may fail with ``OSError: [Errno 0] Undefined error`` (from + ``sysctl(KERN_PROCARGS2)``). They now raise :exc:`AccessDenied` instead. + +7.2.1 — 2025-12-29 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`2699`, [FreeBSD], [NetBSD]: :func:`heap_info()` does not detect small + allocations (<= 1K). In order to fix that, we now flush internal jemalloc + cache before fetching the metrics. + +7.2.0 — 2025-12-23 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1275`: new :func:`heap_info()` and :func:`heap_trim()` functions, + providing direct access to the platform's native C heap allocator (glibc, + mimalloc, libmalloc). Useful to create tools to detect memory leaks. +- :gh:`2403`, [Linux]: publish wheels for Linux musl. +- :gh:`2680`: unit tests are no longer installed / part of the distribution. + They now live under `tests/` instead of `psutil/tests`. + +**Bug fixes** + +* :gh:`2684`, [FreeBSD], [critical]: compilation fails on FreeBSD 14 due to + missing include. +* :gh:`2691`, [Windows]: fix memory leak in :func:`net_if_stats()` due to + missing ``Py_CLEAR``. + +**Compatibility notes** + +- :gh:`2680`: `import psutil.tests` no longer works (but it was never + documented to begin with). + +7.1.3 — 2025-11-02 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2667`: enforce `clang-format` on all C and header files. It is now the + mandatory formatting style for all C sources. +- :gh:`2672`, [macOS], [BSD]: increase the chances to recognize zombie + processes and raise the appropriate exception (:exc:`ZombieProcess`). +- :gh:`2676`, :gh:`2678`: replace unsafe `sprintf` / `snprintf` / `sprintf_s` + calls with `str_format()`. Replace `strlcat` / `strlcpy` with safe `str_copy` + / `str_append`. This unifies string handling across platforms and reduces + unsafe usage of standard string functions, improving robustness. + +**Bug fixes** + +- :gh:`2674`, [Windows]: :func:`disk_usage()` could truncate values on 32-bit + platforms, potentially reporting incorrect total/free/used space for drives + larger than 4GB. +- :gh:`2675`, [macOS]: :meth:`Process.status()` incorrectly returns "running" + for 99% of the processes. +- :gh:`2677`, [Windows]: fix MAC address string construction in + :func:`net_if_addrs()`. Previously, the MAC address buffer was incorrectly + updated using a fixed increment and `sprintf_s`, which could overflow or + misformat the string if the MAC length or formatting changed. Also, the + final '\n' was inserted unnecessarily. +- :gh:`2679`, [OpenBSD], [NetBSD], [critical]: can't build due to C syntax + error. + +7.1.2 — 2025-10-25 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2657`: stop publishing prebuilt Linux and Windows wheels for 32-bit + Python. 32-bit CPython is still supported, but psutil must now be built from + source. + :gh:`2565`: produce wheels for free-thread cPython 3.13 and 3.14 (patch by + Lysandros Nikolaou) + +**Bug fixes** + +- :gh:`2650`, [macOS]: :meth:`Process.cmdline()` and :meth:`Process.environ()` + may incorrectly raise :exc:`NoSuchProcess` instead of :exc:`ZombieProcess`. +- :gh:`2658`, [macOS]: double ``free()`` in :meth:`Process.environ()` when it + fails internally. This posed a risk of segfault. +- :gh:`2662`, [macOS]: massive C code cleanup to guard against possible + segfaults which were (not so) sporadically spotted on CI. + +**Compatibility notes** + +- :gh:`2657`: stop publishing prebuilt Linux and Windows wheels for 32-bit + Python. + +7.1.1 — 2025-10-19 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2645`, [SunOS]: dropped support for SunOS 10. +- :gh:`2646`, [SunOS]: add CI test runner for SunOS. + +**Bug fixes** + +- :gh:`2641`, [SunOS]: cannot compile psutil from sources due to missing C + include. +- :gh:`2357`, [SunOS]: :meth:`Process.cmdline()` does not handle spaces + properly. (patch by Ben Raz) + +**Compatibility notes** + +* :gh:`2645`: SunOS 10 is no longer supported. + +7.1.0 — 2025-09-17 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2581`, [Windows]: publish ARM64 wheels. (patch by Matthieu Darbois) +- :gh:`2571`, [FreeBSD]: Dropped support for FreeBSD 8 and earlier. FreeBSD 8 + was maintained from 2009 to 2013. +- :gh:`2575`: introduced `dprint` CLI tool to format .yml and .md files. + +**Bug fixes** + +- :gh:`2473`, [macOS]: Fix build issue on macOS 11 and lower. +- :gh:`2494`, [Windows]: All APIs dealing with paths, such as + :meth:`Process.memory_maps()`, :meth:`Process.exe()` and + :meth:`Process.open_files()` does not properly handle UNC paths. Paths such + as ``\\??\\C:\\Windows\\Temp`` and + ``'\\Device\\HarddiskVolume1\\Windows\\Temp'`` are now converted to + ``C:\\Windows\\Temp``. (patch by Ben Peddell) +- :gh:`2506`, [Windows]: Windows service APIs had issues with unicode services + using special characters in their name. +- :gh:`2514`, [Linux]: :meth:`Process.cwd()` sometimes fail with + `FileNotFoundError` due to a race condition. +- :gh:`2526`, [Linux]: :meth:`Process.create_time()`, which is used to + univocally identify a process over time, is subject to system clock updates, + and as such can lead to :meth:`Process.is_running()` returning a wrong + result. A monotonic creation time is now used instead. (patch by Jonathan + Kohler) +- :gh:`2528`, [Linux]: :meth:`Process.children()` may raise + ``PermissionError``. It will now raise :exc:`AccessDenied` instead. +- :gh:`2540`, [macOS]: :func:`boot_time()` is off by 45 seconds (C precision + issue). +- :gh:`2541`, :gh:`2570`, :gh:`2578` [Linux], [macOS], [NetBSD]: + :meth:`Process.create_time()` does not reflect system clock updates. +- :gh:`2542`: if system clock is updated :meth:`Process.children()` and + :meth:`Process.parent()` may not be able to return the right information. +- :gh:`2545`: [Illumos]: Fix handling of MIB2_UDP_ENTRY in + :func:`net_connections()`. +- :gh:`2552`, [Windows]: :func:`boot_time()` didn't take into account the time + spent during suspend / hibernation. +- :gh:`2560`, [Linux]: :meth:`Process.memory_maps()` may crash with + `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch + by Julien Stephan) +- :gh:`2586`, [macOS], [CRITICAL]: fixed different places in C code which can + trigger a segfault. +- :gh:`2604`, [Linux]: :func:`virtual_memory()` "used" memory does not match + recent versions of ``free`` CLI utility. (patch by Isaac K. Ko) +- :gh:`2605`, [Linux]: :func:`sensors_battery()` reports a negative amount for + seconds left. +- :gh:`2607`, [Windows]: ``WindowsService.description()`` method may fail with + ``ERROR_NOT_FOUND``. Now it returns an empty string instead. +- 2610:, [macOS], [CRITICAL]: fix :func:`cpu_freq()` segfault on ARM + architectures. + +**Compatibility notes** + +- :gh:`2571`: dropped support for FreeBSD 8 and earlier. + +7.0.0 — 2025-02-13 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`669`, [Windows]: :func:`net_if_addrs()` also returns the ``broadcast`` + address instead of ``None``. +- :gh:`2480`: Python 2.7 is no longer supported. Latest version supporting + Python 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. +- :gh:`2490`: removed long deprecated ``Process.memory_info_ex()`` method. It + was deprecated in psutil 4.0.0, released 8 years ago. Substitute is + ``Process.memory_full_info()``. + +**Bug fixes** + +- :gh:`2496`, [Linux]: Avoid segfault (a cPython bug) on + ``Process.memory_maps()`` for processes that use hundreds of GBs of memory. +- :gh:`2502`, [macOS]: :func:`virtual_memory()` now relies on + ``host_statistics64`` instead of ``host_statistics``. This is the same + approach used by ``vm_stat`` CLI tool, and should grant more accurate + results. + +**Compatibility notes** + +- :gh:`2480`: Python 2.7 is no longer supported. +- :gh:`2490`: removed long deprecated ``Process.memory_info_ex()`` method. + +6.1.1 — 2024-12-19 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2471`: use Vulture CLI tool to detect dead code. + +**Bug fixes** + +- :gh:`2418`, [Linux]: fix race condition in case /proc/PID/stat does not + exist, but /proc/PID does, resulting in FileNotFoundError. +- :gh:`2470`, [Linux]: :func:`users()` may return "localhost" instead of the + actual IP address of the user logged in. + +6.1.0 — 2024-10-17 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2366`, [Windows]: drastically speedup :func:`process_iter()`. We now + determine process unique identity by using process "fast" create time method. + This will considerably speedup those apps which use :func:`process_iter()` + only once, e.g. to look for a process with a certain name. +- :gh:`2446`: use pytest instead of unittest. +- :gh:`2448`: add ``make install-sysdeps`` target to install the necessary + system dependencies (python-dev, gcc, etc.) on all supported UNIX flavors. +- :gh:`2449`: add ``make install-pydeps-test`` and ``make install-pydeps-dev`` + targets. They can be used to install dependencies meant for running tests and + for local development. They can also be installed via ``pip install .[test]`` + and ``pip install .[dev]``. +- :gh:`2456`: allow to run tests via ``python3 -m psutil.tests`` even if + ``pytest`` module is not installed. This is useful for production + environments that don't have pytest installed, but still want to be able to + test psutil installation. + +**Bug fixes** + +- :gh:`2427`: psutil (segfault) on import in the free-threaded (no GIL) version + of Python 3.13. (patch by Sam Gross) +- :gh:`2455`, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and + field 40 (blkio_ticks) is missing. +- :gh:`2457`, [AIX]: significantly improve the speed of + :meth:`Process.open_files()` for some edge cases. +- :gh:`2460`, [OpenBSD]: :meth:`Process.num_fds()` and + :meth:`Process.open_files()` may fail with :exc:`NoSuchProcess` for PID 0. + Instead, we now return "null" values (0 and [] respectively). + +6.0.0 — 2024-06-18 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2109`: ``maxfile`` and ``maxpath`` fields were removed from the + namedtuple returned by :func:`disk_partitions()`. Reason: on network + filesystems (NFS) this can potentially take a very long time to complete. +- :gh:`2366`, [Windows]: log debug message when using slower process APIs. +- :gh:`2375`, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) +- :gh:`2396`: :func:`process_iter()` no longer pre-emptively checks whether + PIDs have been reused. This makes :func:`process_iter()` around 20x times + faster. +- :gh:`2396`: a new ``psutil.process_iter.cache_clear()`` API can be used the + clear + :func:`process_iter()` internal cache. +- :gh:`2401`, Support building with free-threaded CPython 3.13. (patch by Sam + Gross) +- :gh:`2407`: :meth:`Process.connections()` was renamed to + :meth:`Process.net_connections()`. The old name is still available, but it's + deprecated (triggers a ``DeprecationWarning``) and will be removed in the + future. +- :gh:`2425`: [Linux]: provide aarch64 wheels. (patch by Matthieu Darbois / + Ben Raz) + +**Bug fixes** + +- :gh:`2250`, [NetBSD]: :meth:`Process.cmdline()` sometimes fail with EBUSY. It + usually happens for long cmdlines with lots of arguments. In this case retry + getting the cmdline for up to 50 times, and return an empty list as last + resort. +- :gh:`2254`, [Linux]: offline cpus raise NotImplementedError in cpu_freq() + (patch by Shade Gladden) +- :gh:`2272`: Add pickle support to psutil Exceptions. +- :gh:`2359`, [Windows], [CRITICAL]: :func:`pid_exists()` disagrees with + :class:`Process` on whether a pid exists when ERROR_ACCESS_DENIED. +- :gh:`2360`, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) +- :gh:`2362`, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) +- :gh:`2365`, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) +- :gh:`2395`, [OpenBSD]: :func:`pid_exists()` erroneously return True if the + argument is a thread ID (TID) instead of a PID (process ID). +- :gh:`2412`, [macOS]: can't compile on macOS 10.4 PowerPC due to missing + `MNT_` constants. + +**Porting notes** + +Version 6.0.0 introduces some changes which affect backward compatibility: + +- :gh:`2109`: the namedtuple returned by :func:`disk_partitions()`' no longer + has ``maxfile`` and ``maxpath`` fields. +- :gh:`2396`: :func:`process_iter()` no longer pre-emptively checks whether + PIDs have been reused. If you want to check for PID reusage you are supposed + to use + :meth:`Process.is_running()` against the yielded :class:`Process` instances. + That will also automatically remove reused PIDs from :func:`process_iter()` + internal cache. +- :gh:`2407`: :meth:`Process.connections()` was renamed to + :meth:`Process.net_connections()`. The old name is still available, but it's + deprecated (triggers a ``DeprecationWarning``) and will be removed in the + future. + +5.9.8 — 2024-01-19 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2343`, [FreeBSD]: filter :func:`net_connections()` returned list in C + instead of Python, and avoid to retrieve unnecessary connection types unless + explicitly asked. E.g., on an IDLE system with few IPv6 connections this will + run around 4 times faster. Before all connection types (TCP, UDP, UNIX) were + retrieved internally, even if only a portion was returned. +- :gh:`2342`, [NetBSD]: same as above (#2343) but for NetBSD. +- :gh:`2349`: adopted black formatting style. + +**Bug fixes** + +- :gh:`930`, [NetBSD], [critical]: :func:`net_connections()` implementation was + broken. It could either leak memory or core dump. +- :gh:`2340`, [NetBSD]: if process is terminated, :meth:`Process.cwd()` will + return an empty string instead of raising :exc:`NoSuchProcess`. +- :gh:`2345`, [Linux]: fix compilation on older compiler missing + DUPLEX_UNKNOWN. +- :gh:`2222`, [macOS]: `cpu_freq()` now returns fixed values for `min` and + `max` frequencies in all Apple Silicon chips. + +5.9.7 — 2023-12-17 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2324`: enforce Ruff rule `raw-string-in-exception`, which helps + providing clearer tracebacks when exceptions are raised by psutil. + +**Bug fixes** + +- :gh:`2325`, [PyPy]: psutil did not compile on PyPy due to missing + `PyErr_SetExcFromWindowsErrWithFilenameObject` cPython API. + +5.9.6 — 2023-10-15 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1703`: :func:`cpu_percent()` and :func:`cpu_times_percent()` are now + thread safe, meaning they can be called from different threads and still + return meaningful and independent results. Before, if (say) 10 threads called + ``cpu_percent(interval=None)`` at the same time, only 1 thread out of 10 + would get the right result. +- :gh:`2266`: if :class:`Process` class is passed a very high PID, raise + :exc:`NoSuchProcess` instead of OverflowError. (patch by Xuehai Pan) +- :gh:`2246`: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) +- :gh:`2290`: PID reuse is now pre-emptively checked for :meth:`Process.ppid()` + and + :meth:`Process.parents()`. +- :gh:`2312`: use ``ruff`` Python linter instead of ``flake8 + isort``. It's an + order of magnitude faster + it adds a ton of new code quality checks. + +**Bug fixes** + +- :gh:`2195`, [Linux]: no longer print exception at import time in case + /proc/stat can't be read due to permission error. Redirect it to + ``PSUTIL_DEBUG`` instead. +- :gh:`2241`, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by + Thomas Klausner) +- :gh:`2245`, [Windows]: fix var unbound error on possibly in + :func:`swap_memory()` (patch by student_2333) +- :gh:`2268`: ``bytes2human()`` utility function was unable to properly + represent negative values. +- :gh:`2252`, [Windows]: :func:`disk_usage()` fails on Python 3.12+. (patch by + Matthieu Darbois) +- :gh:`2284`, [Linux]: :meth:`Process.memory_full_info()` may incorrectly raise + :exc:`ZombieProcess` if it's determined via ``/proc/pid/smaps_rollup``. + Instead we now fallback on reading ``/proc/pid/smaps``. +- :gh:`2287`, [OpenBSD], [NetBSD]: :meth:`Process.is_running()` erroneously + return ``False`` for zombie processes, because creation time cannot be + determined. +- :gh:`2288`, [Linux]: correctly raise :exc:`ZombieProcess` on + :meth:`Process.exe()`, + :meth:`Process.cmdline()` and :meth:`Process.memory_maps()` instead of + returning a "null" value. +- :gh:`2290`: differently from what stated in the doc, PID reuse is not + pre-emptively checked for :meth:`Process.nice()` (set), + :meth:`Process.ionice()`, (set), :meth:`Process.cpu_affinity()` (set), + :meth:`Process.rlimit()` (set), :meth:`Process.parent()`. +- :gh:`2308`, [OpenBSD]: :meth:`Process.threads()` always fail with + AccessDenied (also as root). + +5.9.5 — 2023-04-17 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2196`: in case of exception, display a cleaner error traceback by hiding + the `KeyError` bit deriving from a missed cache hit. +- :gh:`2217`: print the full traceback when a `DeprecationWarning` or + `UserWarning` is raised. +- :gh:`2230`, [OpenBSD]: :func:`net_connections()` implementation was rewritten + from scratch: + + - We're now able to retrieve the path of AF_UNIX sockets (before it was an + empty string) + - The function is faster since it no longer iterates over all processes. + - No longer produces duplicate connection entries. +- :gh:`2238`: there are cases where :meth:`Process.cwd()` cannot be determined + (e.g. directory no longer exists), in which case we returned either ``None`` + or an empty string. This was consolidated and we now return ``""`` on all + platforms. +- :gh:`2239`, [UNIX]: if process is a zombie, and we can only determine part of + the its truncated :meth:`Process.name()` (15 chars), don't fail with + :exc:`ZombieProcess` when we try to guess the full name from the + :meth:`Process.cmdline()`. Just return the truncated name. +- :gh:`2240`, [NetBSD], [OpenBSD]: add CI testing on every commit for NetBSD + and OpenBSD platforms (python 3 only). + +**Bug fixes** + +- :gh:`1043`, [OpenBSD] :func:`net_connections()` returns duplicate entries. +- :gh:`1915`, [Linux]: on certain kernels, ``"MemAvailable"`` field from + ``/proc/meminfo`` returns ``0`` (possibly a kernel bug), in which case we + calculate an approximation for ``available`` memory which matches "free" CLI + utility. +- :gh:`2164`, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). +- :gh:`2186`, [FreeBSD]: compilation fails with Clang 15. (patch by Po-Chuan + Hsieh) +- :gh:`2191`, [Linux]: :func:`disk_partitions()`: do not unnecessarily read + /proc/filesystems and raise :exc:`AccessDenied` unless user specified + `all=False` argument. +- :gh:`2216`, [Windows]: fix tests when running in a virtual environment (patch + by Matthieu Darbois) +- :gh:`2225`, [POSIX]: :func:`users()` loses precision for ``started`` + attribute (off by 1 minute). +- :gh:`2229`, [OpenBSD]: unable to properly recognize zombie processes. + :exc:`NoSuchProcess` may be raised instead of :exc:`ZombieProcess`. +- :gh:`2231`, [NetBSD]: *available* :func:`virtual_memory()` is higher than + *total*. +- :gh:`2234`, [NetBSD]: :func:`virtual_memory()` metrics are wrong: *available* + and *used* are too high. We now match values shown by *htop* CLI utility. +- :gh:`2236`, [NetBSD]: :meth:`Process.num_threads()` and + :meth:`Process.threads()` return threads that are already terminated. +- :gh:`2237`, [OpenBSD], [NetBSD]: :meth:`Process.cwd()` may raise + ``FileNotFoundError`` if cwd no longer exists. Return an empty string + instead. + +5.9.4 — 2022-11-07 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2102`: use Limited API when building wheels with CPython 3.6+ on Linux, + macOS and Windows. This allows to use pre-built wheels in all future versions + of cPython 3. (patch by Matthieu Darbois) + +**Bug fixes** + +- :gh:`2077`, [Windows]: Use system-level values for :func:`virtual_memory()`. + (patch by Daniel Widdis) +- :gh:`2156`, [Linux]: compilation may fail on very old gcc compilers due to + missing ``SPEED_UNKNOWN`` definition. (patch by Amir Rossert) +- :gh:`2010`, [macOS]: on MacOS, arm64 ``IFM_1000_TX`` and ``IFM_1000_T`` are + the same value, causing a build failure. (patch by Lawrence D'Anna) +- :gh:`2160`, [Windows]: Get Windows percent swap usage from performance + counters. (patch by Daniel Widdis) + +5.9.3 — 2022-10-18 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2040`, [macOS]: provide wheels for arm64 architecture. (patch by + Matthieu Darbois) + +**Bug fixes** + +- :gh:`2116`, [macOS], [critical]: :func:`net_connections()` fails with + RuntimeError. +- :gh:`2135`, [macOS]: :meth:`Process.environ()` may contain garbage data. Fix + out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard + Urban-Forster) +- :gh:`2138`, [Linux], **[critical]**: can't compile psutil on Android due to + undefined ``ethtool_cmd_speed`` symbol. +- :gh:`2142`, [POSIX]: :func:`net_if_stats()` 's ``flags`` on Python 2 returned + unicode instead of str. (patch by Matthieu Darbois) +- :gh:`2147`, [macOS] Fix disk usage report on macOS 12+. (patch by Matthieu + Darbois) +- :gh:`2150`, [Linux] :meth:`Process.threads()` may raise ``NoSuchProcess``. + Fix race condition. (patch by Daniel Li) +- :gh:`2153`, [macOS] Fix race condition in + test_posix.TestProcess.test_cmdline. (patch by Matthieu Darbois) + +5.9.2 — 2022-09-04 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`2093`, [FreeBSD], **[critical]**: :func:`pids()` may fail with ENOMEM. + Dynamically increase the ``malloc()`` buffer size until it's big enough. +- :gh:`2095`, [Linux]: :func:`net_if_stats()` returns incorrect interface speed + for 100GbE network cards. +- :gh:`2113`, [FreeBSD], **[critical]**: :func:`virtual_memory()` may raise + ENOMEM due to missing ``#include `` directive. (patch by Peter + Jeremy) +- :gh:`2128`, [NetBSD]: :func:`swap_memory()` was miscalculated. (patch by + Thomas Klausner) + +5.9.1 — 2022-05-20 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1053`: drop Python 2.6 support. (patches by Matthieu Darbois and Hugo + van Kemenade) +- :gh:`2037`: Add additional flags to net_if_stats. +- :gh:`2050`, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when + reading ``/proc`` pseudo files line by line. This should help having more + consistent results. +- :gh:`2057`, [OpenBSD]: add support for :func:`cpu_freq()`. +- :gh:`2107`, [Linux]: :meth:`Process.memory_full_info()` (reporting process + USS/PSS/Swap memory) now reads ``/proc/pid/smaps_rollup`` instead of + ``/proc/pids/smaps``, which makes it 5 times faster. + +**Bug fixes** + +- :gh:`2048`: ``AttributeError`` is raised if ``psutil.Error`` class is raised + manually and passed through ``str``. +- :gh:`2049`, [Linux]: :func:`cpu_freq()` erroneously returns ``curr`` value in + GHz while ``min`` and ``max`` are in MHz. +- :gh:`2050`, [Linux]: :func:`virtual_memory()` may raise ``ValueError`` if + running in a LCX container. + +5.9.0 — 2021-12-29 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1851`, [Linux]: :func:`cpu_freq()` is slow on systems with many CPUs. + Read current frequency values for all CPUs from ``/proc/cpuinfo`` instead of + opening many files in ``/sys`` fs. (patch by marxin) +- :gh:`1992`: :exc:`NoSuchProcess` message now specifies if the PID has been + reused. +- :gh:`1992`: error classes (:exc:`NoSuchProcess`, :exc:`AccessDenied`, etc.) + now have a better formatted and separated ``__repr__`` and ``__str__`` + implementations. +- :gh:`1996`, [BSD]: add support for MidnightBSD. (patch by Saeed Rasooli) +- :gh:`1999`, [Linux]: :func:`disk_partitions()`: convert ``/dev/root`` device + (an alias used on some Linux distros) to real root device path. +- :gh:`2005`: ``PSUTIL_DEBUG`` mode now prints file name and line number of the + debug messages coming from C extension modules. +- :gh:`2042`: rewrite HISTORY.rst to use hyperlinks pointing to psutil API doc. + +**Bug fixes** + +- :gh:`1456`, [macOS], **[critical]**: :func:`cpu_freq()` ``min`` and ``max`` + are set to 0 if can't be determined (instead of crashing). +- :gh:`1512`, [macOS]: sometimes :meth:`Process.connections()` will crash with + ``EOPNOTSUPP`` for one connection; this is now ignored. +- :gh:`1598`, [Windows]: :func:`disk_partitions()` only returns mountpoints on + drives where it first finds one. +- :gh:`1874`, [SunOS]: swap output error due to incorrect range. +- :gh:`1892`, [macOS]: :func:`cpu_freq()` broken on Apple M1. +- :gh:`1901`, [macOS]: different functions, especially + :meth:`Process.open_files()` and + :meth:`Process.connections()`, could randomly raise :exc:`AccessDenied` + because the internal buffer of ``proc_pidinfo(PROC_PIDLISTFDS)`` syscall + was not big enough. We now dynamically increase the buffer size until + it's big enough instead of giving up and raising :exc:`AccessDenied`, + which was a fallback to avoid crashing. +- :gh:`1904`, [Windows]: ``OpenProcess`` fails with ``ERROR_SUCCESS`` due to + ``GetLastError()`` called after ``sprintf()``. (patch by alxchk) +- :gh:`1913`, [Linux]: :func:`wait_procs()` should catch + ``subprocess.TimeoutExpired`` exception. +- :gh:`1919`, [Linux]: :func:`sensors_battery()` can raise ``TypeError`` on + PureOS. +- :gh:`1921`, [Windows]: :func:`swap_memory()` shows committed memory instead + of swap. +- :gh:`1940`, [Linux]: psutil does not handle ``ENAMETOOLONG`` when accessing + process file descriptors in procfs. (patch by Nikita Radchenko) +- :gh:`1948`, **[critical]**: ``memoize_when_activated`` decorator is not + thread-safe. (patch by Xuehai Pan) +- :gh:`1953`, [Windows], **[critical]**: :func:`disk_partitions()` crashes due + to insufficient buffer len. (patch by MaWe2019) +- :gh:`1965`, [Windows], **[critical]**: fix "Fatal Python error: deallocating + None" when calling :func:`users()` multiple times. +- :gh:`1980`, [Windows]: 32bit / WoW64 processes fails to read + :meth:`Process.name()` longer than 128 characters resulting in + :exc:`AccessDenied`. This is now fixed. (patch by PetrPospisil) +- :gh:`1991`, **[critical]**: :func:`process_iter()` is not thread safe and can + raise ``TypeError`` if invoked from multiple threads. +- :gh:`1956`, [macOS]: :meth:`Process.cpu_times()` reports incorrect timings on + M1 machines. (patch by Olivier Dormond) +- :gh:`2023`, [Linux]: :func:`cpu_freq()` return order is wrong on systems with + more than 9 CPUs. + +5.8.0 — 2020-12-19 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1863`: :func:`disk_partitions()` exposes 2 extra fields: ``maxfile`` and + ``maxpath``, which are the maximum file name and path name length. +- :gh:`1872`, [Windows]: added support for PyPy 2.7. +- :gh:`1879`: provide pre-compiled wheels for Linux and macOS (yey!). +- :gh:`1880`: get rid of Travis and Cirrus CI services (they are no longer + free). CI testing is now done by GitHub Actions on Linux, macOS and FreeBSD + (yes). AppVeyor is still being used for Windows CI. + +**Bug fixes** + +- :gh:`1708`, [Linux]: get rid of :func:`sensors_temperatures()` duplicates. + (patch by Tim Schlueter). +- :gh:`1839`, [Windows], **[critical]**: always raise :exc:`AccessDenied` + instead of ``WindowsError`` when failing to query 64 processes from 32 bit + ones by using ``NtWoW64`` APIs. +- :gh:`1866`, [Windows], **[critical]**: :meth:`Process.exe()`, + :meth:`Process.cmdline()`, + :meth:`Process.environ()` may raise "[WinError 998] Invalid access to memory + location" on Python 3.9 / VS 2019. +- :gh:`1874`, [SunOS]: wrong swap output given when encrypted column is + present. +- :gh:`1875`, [Windows], **[critical]**: :meth:`Process.username()` may raise + ``ERROR_NONE_MAPPED`` if the SID has no corresponding account name. In this + case :exc:`AccessDenied` is now raised. +- :gh:`1886`, [macOS]: ``EIO`` error may be raised on :meth:`Process.cmdline()` + and :meth:`Process.environ()`. Now it gets translated into + :exc:`AccessDenied`. +- :gh:`1887`, [Windows], **[critical]**: ``OpenProcess`` may fail with + "[WinError 0] The operation completed successfully"." Turn it into + :exc:`AccessDenied` or :exc:`NoSuchProcess` depending on whether the PID is + alive. +- :gh:`1891`, [macOS]: get rid of deprecated ``getpagesize()``. + +5.7.3 — 2020-10-23 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`809`, [FreeBSD]: add support for :meth:`Process.rlimit()`. +- :gh:`893`, [BSD]: add support for :meth:`Process.environ()` (patch by Armin + Gruner) +- :gh:`1830`, [POSIX]: :func:`net_if_stats()` ``isup`` also checks whether the + NIC is running (meaning Wi-Fi or ethernet cable is connected). (patch by + Chris Burger) +- :gh:`1837`, [Linux]: improved battery detection and charge ``secsleft`` + calculation (patch by aristocratos) + +**Bug fixes** + +- :gh:`1620`, [Linux]: :func:`cpu_count()` with ``logical=False`` result is + incorrect on systems with more than one CPU socket. (patch by Vincent A. + Arcila) +- :gh:`1738`, [macOS]: :meth:`Process.exe()` may raise ``FileNotFoundError`` if + process is still alive but the exe file which launched it got deleted. +- :gh:`1791`, [macOS]: fix missing include for ``getpagesize()``. +- :gh:`1823`, [Windows], **[critical]**: :meth:`Process.open_files()` may cause + a segfault due to a NULL pointer. +- :gh:`1838`, [Linux]: :func:`sensors_battery()`: if `percent` can be + determined but not the remaining values, still return a result instead of + ``None``. (patch by aristocratos) + +5.7.2 — 2020-07-15 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- wheels for 2.7 were inadvertently deleted. + +5.7.1 — 2020-07-15 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1729`: parallel tests on POSIX (``make test-parallel``). They're twice + as fast! +- :gh:`1741`, [POSIX]: ``make build`` now runs in parallel on Python >= 3.6 and + it's about 15% faster. +- :gh:`1747`: :meth:`Process.wait()` return value is cached so that the exit + code can be retrieved on then next call. +- :gh:`1747`, [POSIX]: :meth:`Process.wait()` on POSIX now returns an enum, + showing the negative signal which was used to terminate the process. It + returns something like ````. +- :gh:`1747`: :class:`Process` class provides more info about the process on + ``str()`` and ``repr()`` (status and exit code). +- :gh:`1757`: memory leak tests are now stable. +- :gh:`1768`, [Windows]: added support for Windows Nano Server. (contributed by + Julien Lebot) + +**Bug fixes** + +- :gh:`1726`, [Linux]: :func:`cpu_freq()` parsing should use spaces instead of + tabs on ia64. (patch by Michał Górny) +- :gh:`1760`, [Linux]: :meth:`Process.rlimit()` does not handle long long type + properly. +- :gh:`1766`, [macOS]: :exc:`NoSuchProcess` may be raised instead of + :exc:`ZombieProcess`. +- :gh:`1781`, **[critical]**: :func:`getloadavg()` can crash the Python + interpreter. (patch by Ammar Askar) + +5.7.0 — 2020-02-18 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1637`, [SunOS]: add partial support for old SunOS 5.10 Update 0 to 3. +- :gh:`1648`, [Linux]: :func:`sensors_temperatures()` looks into an additional + ``/sys/device/`` directory for additional data. (patch by Javad Karabi) +- :gh:`1652`, [Windows]: dropped support for Windows XP and Windows Server + 2003. Minimum supported Windows version now is Windows Vista. +- :gh:`1671`, [FreeBSD]: add CI testing/service for FreeBSD (Cirrus CI). +- :gh:`1677`, [Windows]: :meth:`Process.exe()` will succeed for all process + PIDs (instead of raising :exc:`AccessDenied`). +- :gh:`1679`, [Windows]: :func:`net_connections()` and + :meth:`Process.connections()` are 10% faster. +- :gh:`1682`, [PyPy]: added CI / test integration for PyPy via Travis. +- :gh:`1686`, [Windows]: added support for PyPy on Windows. +- :gh:`1693`, [Windows]: :func:`boot_time()`, :meth:`Process.create_time()` and + :func:`users()`'s login time now have 1 micro second precision (before the + precision was of 1 second). + +**Bug fixes** + +- :gh:`1538`, [NetBSD]: :meth:`Process.cwd()` may return ``ENOENT`` instead of + :exc:`NoSuchProcess`. +- :gh:`1627`, [Linux]: :meth:`Process.memory_maps()` can raise ``KeyError``. +- :gh:`1642`, [SunOS]: querying basic info for PID 0 results in + ``FileNotFoundError``. +- :gh:`1646`, [FreeBSD], **[critical]**: many :class:`Process` methods may + cause a segfault due to a backward incompatible change in a C type on FreeBSD + 12.0. +- :gh:`1656`, [Windows]: :meth:`Process.memory_full_info()` raises + :exc:`AccessDenied` even for the current user and os.getpid(). +- :gh:`1660`, [Windows]: :meth:`Process.open_files()` complete rewrite + check + of errors. +- :gh:`1662`, [Windows], **[critical]**: :meth:`Process.exe()` may raise + "[WinError 0] The operation completed successfully". +- :gh:`1665`, [Linux]: :func:`disk_io_counters()` does not take into account + extra fields added to recent kernels. (patch by Mike Hommey) +- :gh:`1672`: use the right C type when dealing with PIDs (int or long). Thus + far (long) was almost always assumed, which is wrong on most platforms. +- :gh:`1673`, [OpenBSD]: :meth:`Process.connections()`, + :meth:`Process.num_fds()` and + :meth:`Process.threads()` returned improper exception if process is gone. +- :gh:`1674`, [SunOS]: :func:`disk_partitions()` may raise ``OSError``. +- :gh:`1684`, [Linux]: :func:`disk_io_counters()` may raise ``ValueError`` on + systems not having ``/proc/diskstats``. +- :gh:`1695`, [Linux]: could not compile on kernels <= 2.6.13 due to + ``PSUTIL_HAS_IOPRIO`` not being defined. (patch by Anselm Kruis) + +5.6.7 — 2019-11-26 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`1630`, [Windows], **[critical]**: can't compile source distribution due + to C syntax error. + +5.6.6 — 2019-11-25 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`1179`, [Linux]: :meth:`Process.cmdline()` now takes into account + misbehaving processes renaming the command line and using inappropriate chars + to separate args. +- :gh:`1616`, **[critical]**: use of ``Py_DECREF`` instead of ``Py_CLEAR`` will + result in double ``free()`` and segfault (`CVE-2019-18874 + `__). (patch + by Riccardo Schirone) +- :gh:`1619`, [OpenBSD], **[critical]**: compilation fails due to C syntax + error. (patch by Nathan Houghton) + +5.6.5 — 2019-11-06 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`1615`: remove ``pyproject.toml`` as it was causing installation issues. + +5.6.4 — 2019-11-04 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1527`, [Linux]: added :meth:`Process.cpu_times()` ``iowait`` counter, + which is the time spent waiting for blocking I/O to complete. +- :gh:`1565`: add PEP 517/8 build backend and requirements specification for + better pip integration. (patch by Bernát Gábor) + +**Bug fixes** + +- :gh:`875`, [Windows], **[critical]**: :meth:`Process.cmdline()`, + :meth:`Process.environ()` or + :meth:`Process.cwd()` may occasionally fail with ``ERROR_PARTIAL_COPY`` which + now gets translated to :exc:`AccessDenied`. +- :gh:`1126`, [Linux], **[critical]**: :meth:`Process.cpu_affinity()` segfaults + on CentOS 5 / manylinux. :meth:`Process.cpu_affinity()` support for CentOS 5 + was removed. +- :gh:`1528`, [AIX], **[critical]**: compilation error on AIX 7.2 due to 32 vs + 64 bit differences. (patch by Arnon Yaari) +- :gh:`1535`: ``type`` and ``family`` fields returned by + :func:`net_connections()` are not always turned into enums. +- :gh:`1536`, [NetBSD]: :meth:`Process.cmdline()` erroneously raise + :exc:`ZombieProcess` error if cmdline has non encodable chars. +- :gh:`1546`: usage percent may be rounded to 0 on Python 2. +- :gh:`1552`, [Windows]: :func:`getloadavg()` math for calculating 5 and 15 + mins values is incorrect. +- :gh:`1568`, [Linux]: use CC compiler env var if defined. +- :gh:`1570`, [Windows]: ``NtWow64*`` syscalls fail to raise the proper error + code +- :gh:`1585`, [OSX]: avoid calling ``close()`` (in C) on possible negative + integers. (patch by Athos Ribeiro) +- :gh:`1606`, [SunOS], **[critical]**: compilation fails on SunOS 5.10. (patch + by vser1) + +5.6.3 — 2019-06-11 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1494`, [AIX]: added support for :meth:`Process.environ()`. (patch by + Arnon Yaari) + +**Bug fixes** + +- :gh:`1276`, [AIX]: can't get whole :meth:`Process.cmdline()`. (patch by + Arnon Yaari) +- :gh:`1501`, [Windows]: :meth:`Process.cmdline()` and :meth:`Process.exe()` + raise unhandled "WinError 1168 element not found" exceptions for "Registry" + and "Memory Compression" pseudo processes on Windows 10. +- :gh:`1526`, [NetBSD], **[critical]**: :meth:`Process.cmdline()` could raise + ``MemoryError``. (patch by Kamil Rytarowski) + +5.6.2 — 2019-04-26 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`604`, [Windows]: add new :func:`getloadavg()`, returning system load + average calculation, including on Windows (emulated). (patch by Ammar Askar) +- :gh:`1404`, [Linux]: :func:`cpu_count()` with ``logical=False`` uses a second + method (read from ``/sys/devices/system/cpu/cpu[0-9]/topology/core_id``) in + order to determine the number of CPU cores in case ``/proc/cpuinfo`` does not + provide this info. +- :gh:`1458`: provide coloured test output. Also show failures on + ``KeyboardInterrupt``. +- :gh:`1464`: various docfixes (always point to Python 3 doc, fix links, etc.). +- :gh:`1476`, [Windows]: it is now possible to set process high I/O priority + (:meth:`Process.ionice()`). Also, I/O priority values are now exposed as 4 + new constants: ``IOPRIO_VERYLOW``, ``IOPRIO_LOW``, ``IOPRIO_NORMAL``, + ``IOPRIO_HIGH``. +- :gh:`1478`: add make command to re-run tests failed on last run. + +**Bug fixes** + +- :gh:`1223`, [Windows]: :func:`boot_time()` may return incorrect value on + Windows XP. +- :gh:`1456`, [Linux]: :func:`cpu_freq()` returns ``None`` instead of 0.0 when + ``min`` and ``max`` fields can't be determined. (patch by Alex Manuskin) +- :gh:`1462`, [Linux]: (tests) make tests invariant to ``LANG`` setting (patch + by Benjamin Drung) +- :gh:`1463`: `cpu_distribution.py`_ script was broken. +- :gh:`1470`, [Linux]: :func:`disk_partitions()`: fix corner case when + ``/etc/mtab`` doesn't exist. (patch by Cedric Lamoriniere) +- :gh:`1471`, [SunOS]: :meth:`Process.name()` and :meth:`Process.cmdline()` can + return ``SystemError``. (patch by Daniel Beer) +- :gh:`1472`, [Linux]: :func:`cpu_freq()` does not return all CPUs on + Raspberry-pi 3. +- :gh:`1474`: fix formatting of ``psutil.tests()`` which mimics ``ps aux`` + output. +- :gh:`1475`, [Windows], **[critical]**: ``OSError.winerror`` attribute wasn't + properly checked resulting in ``WindowsError(ERROR_ACCESS_DENIED)`` being + raised instead of :exc:`AccessDenied`. +- :gh:`1477`, [Windows]: wrong or absent error handling for private + ``NTSTATUS`` Windows APIs. Different process methods were affected by this. +- :gh:`1480`, [Windows], **[critical]**: :func:`cpu_count()` with + ``logical=False`` could cause a crash due to fixed read violation. (patch by + Samer Masterson) +- :gh:`1486`, [AIX], [SunOS]: ``AttributeError`` when interacting with + :class:`Process` methods involved into :meth:`Process.oneshot()` context. +- :gh:`1491`, [SunOS]: :func:`net_if_addrs()`: use ``free()`` against ``ifap`` + struct on error. (patch by Agnewee) +- :gh:`1493`, [Linux]: :func:`cpu_freq()`: handle the case where + ``/sys/devices/system/cpu/cpufreq/`` exists but it's empty. + +5.6.1 — 2019-03-11 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`1329`, [AIX]: psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) +- :gh:`1448`, [Windows], **[critical]**: crash on import due to + ``rtlIpv6AddressToStringA`` not available on Wine. +- :gh:`1451`, [Windows], **[critical]**: :meth:`Process.memory_full_info()` + segfaults. ``NtQueryVirtualMemory`` is now used instead of + ``QueryWorkingSet`` to calculate USS memory. + +5.6.0 — 2019-03-05 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1379`, [Windows]: :meth:`Process.suspend()` and :meth:`Process.resume()` + now use ``NtSuspendProcess`` and ``NtResumeProcess`` instead of + stopping/resuming all threads of a process. This is faster and more reliable + (aka this is what ProcessHacker does). +- :gh:`1420`, [Windows]: in case of exception :func:`disk_usage()` now also + shows the path name. +- :gh:`1422`, [Windows]: Windows APIs requiring to be dynamically loaded from + DLL libraries are now loaded only once on startup (instead of on per function + call) significantly speeding up different functions and methods. +- :gh:`1426`, [Windows]: ``PAGESIZE`` and number of processors is now + calculated on startup. +- :gh:`1428`: in case of error, the traceback message now shows the underlying + C function called which failed. +- :gh:`1433`: new :meth:`Process.parents()` method. (idea by Ghislain Le Meur) +- :gh:`1437`: :func:`pids()` are returned in sorted order. +- :gh:`1442`: Python 3 is now the default interpreter used by Makefile. + +**Bug fixes** + +- :gh:`1353`: :func:`process_iter()` is now thread safe (it rarely raised + ``TypeError``). +- :gh:`1394`, [Windows], **[critical]**: :meth:`Process.name()` and + :meth:`Process.exe()` may erroneously return "Registry" or fail with "[Error + 0] The operation completed successfully". ``QueryFullProcessImageNameW`` + is now used instead of ``GetProcessImageFileNameW`` in order to prevent + that. +- :gh:`1411`, [BSD]: lack of ``Py_DECREF`` could cause segmentation fault on + process instantiation. +- :gh:`1419`, [Windows]: :meth:`Process.environ()` raises + ``NotImplementedError`` when querying a 64-bit process in 32-bit-WoW mode. + Now it raises + :exc:`AccessDenied`. +- :gh:`1427`, [OSX]: :meth:`Process.cmdline()` and :meth:`Process.environ()` + may erroneously raise ``OSError`` on failed ``malloc()``. +- :gh:`1429`, [Windows]: ``SE DEBUG`` was not properly set for current process. + It is now, and it should result in less :exc:`AccessDenied` exceptions for + low PID processes. +- :gh:`1432`, [Windows]: :meth:`Process.memory_info_ex()`'s USS memory is + miscalculated because we're not using the actual system ``PAGESIZE``. +- :gh:`1439`, [NetBSD]: :meth:`Process.connections()` may return incomplete + results if using + :meth:`Process.oneshot()`. +- :gh:`1447`: original exception wasn't turned into :exc:`NoSuchProcess` / + :exc:`AccessDenied` exceptions when using :meth:`Process.oneshot()` context + manager. + +**Incompatible API changes** + +- :gh:`1291`, [OSX], **[critical]**: :meth:`Process.memory_maps()` was removed + because inherently broken (segfault) for years. + +5.5.1 — 2019-02-15 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1348`, [Windows]: on Windows >= 8.1 if :meth:`Process.cmdline()` fails + due to ``ERROR_ACCESS_DENIED`` attempt using ``NtQueryInformationProcess`` + + ``ProcessCommandLineInformation``. (patch by EccoTheFlintstone) + +**Bug fixes** + +- :gh:`1394`, [Windows]: :meth:`Process.exe()` returns "[Error 0] The operation + completed successfully" when Python process runs in "Virtual Secure Mode". +- :gh:`1402`: psutil exceptions' ``repr()`` show the internal private module + path. +- :gh:`1408`, [AIX], **[critical]**: psutil won't compile on AIX 7.1 due to + missing header. (patch by Arnon Yaari) + +5.5.0 — 2019-01-23 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1350`, [FreeBSD]: added support for :func:`sensors_temperatures()`. + (patch by Alex Manuskin) +- :gh:`1352`, [FreeBSD]: added support for :func:`cpu_freq()`. (patch by Alex + Manuskin) + +**Bug fixes** + +- :gh:`1111`: :meth:`Process.oneshot()` is now thread safe. +- :gh:`1354`, [Linux]: :func:`disk_io_counters()` fails on Linux kernel 4.18+. +- :gh:`1357`, [Linux]: :meth:`Process.memory_maps()` and + :meth:`Process.io_counters()` methods are no longer exposed if not supported + by the kernel. +- :gh:`1368`, [Windows]: fix :meth:`Process.ionice()` mismatch. (patch by + EccoTheFlintstone) +- :gh:`1370`, [Windows]: improper usage of ``CloseHandle()`` may lead to + override the original error code when raising an exception. +- :gh:`1373`, **[critical]**: incorrect handling of cache in + :meth:`Process.oneshot()` context causes :class:`Process` instances to return + incorrect results. +- :gh:`1376`, [Windows]: ``OpenProcess`` now uses + ``PROCESS_QUERY_LIMITED_INFORMATION`` access rights wherever possible, + resulting in less :exc:`AccessDenied` exceptions being thrown for system + processes. +- :gh:`1376`, [Windows]: check if variable is ``NULL`` before ``free()`` ing + it. (patch by EccoTheFlintstone) + +5.4.8 — 2018-10-30 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1197`, [Linux]: :func:`cpu_freq()` is now implemented by parsing + ``/proc/cpuinfo`` in case ``/sys/devices/system/cpu/*`` filesystem is not + available. +- :gh:`1310`, [Linux]: :func:`sensors_temperatures()` now parses + ``/sys/class/thermal`` in case ``/sys/class/hwmon`` fs is not available (e.g. + Raspberry Pi). (patch by Alex Manuskin) +- :gh:`1320`, [POSIX]: better compilation support when using g++ instead of + GCC. (patch by Jaime Fullaondo) + +**Bug fixes** + +- :gh:`715`: do not print exception on import time in case :func:`cpu_times()` + fails. +- :gh:`1004`, [Linux]: :meth:`Process.io_counters()` may raise ``ValueError``. +- :gh:`1277`, [OSX]: available and used memory (:func:`virtual_memory()`) + metrics are not accurate. +- :gh:`1294`, [Windows]: :meth:`Process.connections()` may sometimes fail with + intermittent ``0xC0000001``. (patch by Sylvain Duchesne) +- :gh:`1307`, [Linux]: :func:`disk_partitions()` does not honour + :data:`PROCFS_PATH`. +- :gh:`1320`, [AIX]: system CPU times (:func:`cpu_times()`) were being reported + with ticks unit as opposed to seconds. (patch by Jaime Fullaondo) +- :gh:`1332`, [OSX]: psutil debug messages are erroneously printed all the + time. (patch by Ilya Yanok) +- :gh:`1346`, [SunOS]: :func:`net_connections()` returns an empty list. (patch + by Oleksii Shevchuk) + +5.4.7 — 2018-08-14 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1286`, [macOS]: ``psutil.OSX`` constant is now deprecated in favor of + new ``psutil.MACOS``. +- :gh:`1309`, [Linux]: added ``psutil.STATUS_PARKED`` constant for + :meth:`Process.status()`. +- :gh:`1321`, [Linux]: add :func:`disk_io_counters()` dual implementation + relying on ``/sys/block`` filesystem in case ``/proc/diskstats`` is not + available. (patch by Lawrence Ye) + +**Bug fixes** + +- :gh:`1209`, [macOS]: :meth:`Process.memory_maps()` may fail with ``EINVAL`` + due to poor ``task_for_pid()`` syscall. :exc:`AccessDenied` is now raised + instead. +- :gh:`1278`, [macOS]: :meth:`Process.threads()` incorrectly return + microseconds instead of seconds. (patch by Nikhil Marathe) +- :gh:`1279`, [Linux], [macOS], [BSD]: :func:`net_if_stats()` may return + ``ENODEV``. +- :gh:`1294`, [Windows]: :meth:`Process.connections()` may sometime fail with + ``MemoryError``. (patch by sylvainduchesne) +- :gh:`1305`, [Linux]: :func:`disk_io_counters()` may report inflated r/w bytes + values. +- :gh:`1309`, [Linux]: :meth:`Process.status()` is unable to recognize + ``"idle"`` and ``"parked"`` statuses (returns ``"?"``). +- :gh:`1313`, [Linux]: :func:`disk_io_counters()` can report inflated values + due to counting base disk device and its partition(s) twice. +- :gh:`1323`, [Linux]: :func:`sensors_temperatures()` may fail with + ``ValueError``. + +5.4.6 — 2018-06-07 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`1258`, [Windows], **[critical]**: :meth:`Process.username()` may cause a + segfault (Python interpreter crash). (patch by Jean-Luc Migot) +- :gh:`1273`: :func:`net_if_addrs()` namedtuple's name has been renamed from + ``snic`` to ``snicaddr``. +- :gh:`1274`, [Linux]: there was a small chance :meth:`Process.children()` may + swallow + :exc:`AccessDenied` exceptions. + +5.4.5 — 2018-04-14 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`1268`: setup.py's ``extra_require`` parameter requires latest setuptools + version, breaking quite a lot of installations. + +5.4.4 — 2018-04-13 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1239`, [Linux]: expose kernel ``slab`` memory field for + :func:`virtual_memory()`. (patch by Maxime Mouial) + +**Bug fixes** + +- :gh:`694`, [SunOS]: :meth:`Process.cmdline()` could be truncated at the 15th + character when reading it from ``/proc``. An extra effort is made by reading + it from process address space first. (patch by Georg Sauthoff) +- :gh:`771`, [Windows]: :func:`cpu_count()` (both logical and cores) return a + wrong (smaller) number on systems using process groups (> 64 cores). +- :gh:`771`, [Windows]: :func:`cpu_times()` with ``percpu=True`` return fewer + CPUs on systems using process groups (> 64 cores). +- :gh:`771`, [Windows]: :func:`cpu_stats()` and :func:`cpu_freq()` may return + incorrect results on systems using process groups (> 64 cores). +- :gh:`1193`, [SunOS]: return uid/gid from ``/proc/pid/psinfo`` if there aren't + enough permissions for ``/proc/pid/cred``. (patch by Georg Sauthoff) +- :gh:`1194`, [SunOS]: return nice value from ``psinfo`` as ``getpriority()`` + doesn't support real-time processes. (patch by Georg Sauthoff) +- :gh:`1194`, [SunOS]: fix double ``free()`` in :meth:`Process.cpu_num()`. + (patch by Georg Sauthoff) +- :gh:`1194`, [SunOS]: fix undefined behavior related to strict-aliasing rules + and warnings. (patch by Georg Sauthoff) +- :gh:`1210`, [Linux]: :func:`cpu_percent()` steal time may remain stuck at + 100% due to Linux erroneously reporting a decreased steal time between calls. + (patch by Arnon Yaari) +- :gh:`1216`: fix compatibility with Python 2.6 on Windows (patch by Dan + Vinakovsky) +- :gh:`1222`, [Linux]: :meth:`Process.memory_full_info()` was erroneously + summing "Swap:" and "SwapPss:". Same for "Pss:" and "SwapPss". Not anymore. +- :gh:`1224`, [Windows]: :meth:`Process.wait()` may erroneously raise + :exc:`TimeoutExpired`. +- :gh:`1238`, [Linux]: :func:`sensors_battery()` may return ``None`` in case + battery is not listed as "BAT0" under ``/sys/class/power_supply``. +- :gh:`1240`, [Windows]: :func:`cpu_times()` float loses accuracy in a long + running system. (patch by stswandering) +- :gh:`1245`, [Linux]: :func:`sensors_temperatures()` may fail with ``IOError`` + "no such file". +- :gh:`1255`, [FreeBSD]: :func:`swap_memory()` stats were erroneously + represented in KB. (patch by Denis Krienbühl) + +**Backward compatibility** + +- :gh:`771`, [Windows]: :func:`cpu_count()` with ``logical=False`` on Windows + XP and Vista is no longer supported and returns ``None``. + +5.4.3 — 2018-01-01 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`775`: :func:`disk_partitions()` on Windows return mount points. + +**Bug fixes** + +- :gh:`1193`: :func:`pids()` may return ``False`` on macOS. + +5.4.2 — 2017-12-07 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1173`: introduced ``PSUTIL_DEBUG`` environment variable which can be set + in order to print useful debug messages on stderr (useful in case of nasty + errors). +- :gh:`1177`, [macOS]: added support for :func:`sensors_battery()`. (patch by + Arnon Yaari) +- :gh:`1183`: :meth:`Process.children()` is 2x faster on POSIX and 2.4x faster + on Linux. +- :gh:`1188`: deprecated method :meth:`Process.memory_info_ex()` now warns by + using ``FutureWarning`` instead of ``DeprecationWarning``. + +**Bug fixes** + +- :gh:`1152`, [Windows]: :func:`disk_io_counters()` may return an empty dict. +- :gh:`1169`, [Linux]: :func:`users()` ``hostname`` returns username instead. + (patch by janderbrain) +- :gh:`1172`, [Windows]: ``make test`` does not work. +- :gh:`1179`, [Linux]: :meth:`Process.cmdline()` is now able to split cmdline + args for misbehaving processes which overwrite ``/proc/pid/cmdline`` and use + spaces instead of null bytes as args separator. +- :gh:`1181`, [macOS]: :meth:`Process.memory_maps()` may raise ``ENOENT``. +- :gh:`1187`, [macOS]: :func:`pids()` does not return PID 0 on recent macOS + versions. + +5.4.1 — 2017-11-08 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1164`, [AIX]: add support for :meth:`Process.num_ctx_switches()`. + (patch by Arnon Yaari) +- :gh:`1053`: drop Python 3.3 support (psutil still works but it's no longer + tested). + +**Bug fixes** + +- :gh:`1150`, [Windows]: when a process is terminated now the exit code is set + to ``SIGTERM`` instead of ``0``. (patch by Akos Kiss) +- :gh:`1151`: ``python -m psutil.tests`` fail. +- :gh:`1154`, [AIX], **[critical]**: psutil won't compile on AIX 6.1.0. (patch + by Arnon Yaari) +- :gh:`1167`, [Windows]: :func:`net_io_counters()` packets count now include + also non-unicast packets. (patch by Matthew Long) + +5.4.0 — 2017-10-12 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1123`, [AIX]: added support for AIX platform. (patch by Arnon Yaari) + +**Bug fixes** + +- :gh:`1009`, [Linux]: :func:`sensors_temperatures()` may crash with + ``IOError``. +- :gh:`1012`, [Windows]: :func:`disk_io_counters()` ``read_time`` and + ``write_time`` were expressed in tens of micro seconds instead of + milliseconds. +- :gh:`1127`, [macOS], **[critical]**: invalid reference counting in + :meth:`Process.open_files()` may lead to segfault. (patch by Jakub Bacic) +- :gh:`1129`, [Linux]: :func:`sensors_fans()` may crash with ``IOError``. + (patch by Sebastian Saip) +- :gh:`1131`, [SunOS]: fix compilation warnings. (patch by Arnon Yaari) +- :gh:`1133`, [Windows]: can't compile on newer versions of Visual Studio 2017 + 15.4. (patch by Max Bélanger) +- :gh:`1138`, [Linux]: can't compile on CentOS 5.0 and RedHat 5.0. (patch by + Prodesire) + +5.3.1 — 2017-09-10 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`1124`: documentation moved to http://psutil.readthedocs.io + +**Bug fixes** + +- :gh:`1105`, [FreeBSD]: psutil does not compile on FreeBSD 12. +- :gh:`1125`, [BSD]: :func:`net_connections()` raises ``TypeError``. + +**Compatibility notes** + +- :gh:`1120`: ``.exe`` files for Windows are no longer uploaded on PyPI as per + PEP-527. Only wheels are provided. + +5.3.0 — 2017-09-01 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`802`: :func:`disk_io_counters()` and :func:`net_io_counters()` numbers + no longer wrap (restart from 0). Introduced a new ``nowrap`` argument. +- :gh:`928`: :func:`net_connections()` and :meth:`Process.connections()` + ``laddr`` and ``raddr`` are now named tuples. +- :gh:`1015`: :func:`swap_memory()` now relies on ``/proc/meminfo`` instead of + ``sysinfo()`` syscall so that it can be used in conjunction with + :data:`PROCFS_PATH` in order to retrieve memory info about Linux containers + such as Docker and Heroku. +- :gh:`1022`: :func:`users()` provides a new ``pid`` field. +- :gh:`1025`: :func:`process_iter()` accepts two new parameters in order to + invoke + :meth:`Process.as_dict()`: ``attrs`` and ``ad_value``. With these you can + iterate over all processes in one shot without needing to catch + :exc:`NoSuchProcess` and do list/dict comprehensions. +- :gh:`1040`: implemented full unicode support. +- :gh:`1051`: :func:`disk_usage()` on Python 3 is now able to accept bytes. +- :gh:`1058`: test suite now enables all warnings by default. +- :gh:`1060`: source distribution is dynamically generated so that it only + includes relevant files. +- :gh:`1079`, [FreeBSD]: :func:`net_connections()` ``fd`` number is now being + set for real (instead of ``-1``). (patch by Gleb Smirnoff) +- :gh:`1091`, [SunOS]: implemented :meth:`Process.environ()`. (patch by + Oleksii Shevchuk) + +**Bug fixes** + +- :gh:`989`, [Windows]: :func:`boot_time()` may return a negative value. +- :gh:`1007`, [Windows]: :func:`boot_time()` can have a 1 sec fluctuation + between calls. The value of the first call is now cached so that + :func:`boot_time()` always returns the same value if fluctuation is <= 1 + second. +- :gh:`1013`, [FreeBSD]: :func:`net_connections()` may return incorrect PID. + (patch by Gleb Smirnoff) +- :gh:`1014`, [Linux]: :class:`Process` class can mask legitimate ``ENOENT`` + exceptions as + :exc:`NoSuchProcess`. +- :gh:`1016`: :func:`disk_io_counters()` raises ``RuntimeError`` on a system + with no disks. +- :gh:`1017`: :func:`net_io_counters()` raises ``RuntimeError`` on a system + with no network cards installed. +- :gh:`1021`, [Linux]: :meth:`Process.open_files()` may erroneously raise + :exc:`NoSuchProcess` instead of skipping a file which gets deleted while open + files are retrieved. +- :gh:`1029`, [macOS], [FreeBSD]: :meth:`Process.connections()` with + ``family=unix`` on Python 3 doesn't properly handle unicode paths and may + raise ``UnicodeDecodeError``. +- :gh:`1033`, [macOS], [FreeBSD]: memory leak for :func:`net_connections()` and + :meth:`Process.connections()` when retrieving UNIX sockets (``kind='unix'``). +- :gh:`1040`: fixed many unicode related issues such as ``UnicodeDecodeError`` + on Python 3 + POSIX and invalid encoded data on Windows. +- :gh:`1042`, [FreeBSD], **[critical]**: psutil won't compile on FreeBSD 12. +- :gh:`1044`, [macOS]: different :class:`Process` methods incorrectly raise + :exc:`AccessDenied` for zombie processes. +- :gh:`1046`, [Windows]: :func:`disk_partitions()` on Windows overrides user's + ``SetErrorMode``. +- :gh:`1047`, [Windows]: :meth:`Process.username()`: memory leak in case + exception is thrown. +- :gh:`1048`, [Windows]: :func:`users()` ``host`` field report an invalid IP + address. +- :gh:`1050`, [Windows]: :meth:`Process.memory_maps()` leaks memory. +- :gh:`1055`: :func:`cpu_count()` is no longer cached. This is useful on + systems such as Linux where CPUs can be disabled at runtime. This also + reflects on + :meth:`Process.cpu_percent()` which no longer uses the cache. +- :gh:`1058`: fixed Python warnings. +- :gh:`1062`: :func:`disk_io_counters()` and :func:`net_io_counters()` raise + ``TypeError`` if no disks or NICs are installed on the system. +- :gh:`1063`, [NetBSD]: :func:`net_connections()` may list incorrect sockets. +- :gh:`1064`, [NetBSD], **[critical]**: :func:`swap_memory()` may segfault in + case of error. +- :gh:`1065`, [OpenBSD], **[critical]**: :meth:`Process.cmdline()` may raise + ``SystemError``. +- :gh:`1067`, [NetBSD]: :meth:`Process.cmdline()` leaks memory if process has + terminated. +- :gh:`1069`, [FreeBSD]: :meth:`Process.cpu_num()` may return 255 for certain + kernel processes. +- :gh:`1071`, [Linux]: :func:`cpu_freq()` may raise ``IOError`` on old RedHat + distros. +- :gh:`1074`, [FreeBSD]: :func:`sensors_battery()` raises ``OSError`` in case + of no battery. +- :gh:`1075`, [Windows]: :func:`net_if_addrs()`: ``inet_ntop()`` return value + is not checked. +- :gh:`1077`, [SunOS]: :func:`net_if_addrs()` shows garbage addresses on SunOS + 5.10. (patch by Oleksii Shevchuk) +- :gh:`1077`, [SunOS]: :func:`net_connections()` does not work on SunOS 5.10. + (patch by Oleksii Shevchuk) +- :gh:`1079`, [FreeBSD]: :func:`net_connections()` didn't list locally + connected sockets. (patch by Gleb Smirnoff) +- :gh:`1085`: :func:`cpu_count()` return value is now checked and forced to + ``None`` if <= 1. +- :gh:`1087`: :meth:`Process.cpu_percent()` guard against :func:`cpu_count()` + returning ``None`` and assumes 1 instead. +- :gh:`1093`, [SunOS]: :meth:`Process.memory_maps()` shows wrong 64 bit + addresses. +- :gh:`1094`, [Windows]: :func:`pid_exists()` may lie. Also, all process APIs + relying on ``OpenProcess`` Windows API now check whether the PID is actually + running. +- :gh:`1098`, [Windows]: :meth:`Process.wait()` may erroneously return sooner, + when the PID is still alive. +- :gh:`1099`, [Windows]: :meth:`Process.terminate()` may raise + :exc:`AccessDenied` even if the process already died. +- :gh:`1101`, [Linux]: :func:`sensors_temperatures()` may raise ``ENODEV``. + +**Porting notes** + +- :gh:`1039`: returned types consolidation. 1) Windows / + :meth:`Process.cpu_times()`: fields #3 and #4 were int instead of float. 2) + Linux / FreeBSD / OpenBSD: + :meth:`Process.connections()` ``raddr`` is now set to ``""`` instead of + ``None`` when retrieving UNIX sockets. +- :gh:`1040`: all strings are encoded by using OS fs encoding. +- :gh:`1040`: the following Windows APIs on Python 2 now return a string + instead of unicode: ``Process.memory_maps().path``, + ``WindowsService.bin_path()``, ``WindowsService.description()``, + ``WindowsService.display_name()``, ``WindowsService.username()``. + +5.2.2 — 2017-04-10 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`1000`: fixed some setup.py warnings. +- :gh:`1002`, [SunOS]: remove C macro which will not be available on new + Solaris versions. (patch by Danek Duvall) +- :gh:`1004`, [Linux]: :meth:`Process.io_counters()` may raise ``ValueError``. +- :gh:`1006`, [Linux]: :func:`cpu_freq()` may return ``None`` on some Linux + versions does not support the function. Let's not make the function available + instead. +- :gh:`1009`, [Linux]: :func:`sensors_temperatures()` may raise ``OSError``. +- :gh:`1010`, [Linux]: :func:`virtual_memory()` may raise ``ValueError`` on + Ubuntu 14.04. + +5.2.1 — 2017-03-24 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`981`, [Linux]: :func:`cpu_freq()` may return an empty list. +- :gh:`993`, [Windows]: :meth:`Process.memory_maps()` on Python 3 may raise + ``UnicodeDecodeError``. +- :gh:`996`, [Linux]: :func:`sensors_temperatures()` may not show all + temperatures. +- :gh:`997`, [FreeBSD]: :func:`virtual_memory()` may fail due to missing + ``sysctl`` parameter on FreeBSD 12. + +5.2.0 — 2017-03-05 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`971`, [Linux]: Add :func:`sensors_fans()` function. (patch by Nicolas + Hennion) +- :gh:`976`, [Windows]: :meth:`Process.io_counters()` has 2 new fields: + ``other_count`` and ``other_bytes``. +- :gh:`976`, [Linux]: :meth:`Process.io_counters()` has 2 new fields: + ``read_chars`` and ``write_chars``. + +**Bug fixes** + +- :gh:`872`, [Linux]: can now compile on Linux by using MUSL C library. +- :gh:`985`, [Windows]: Fix a crash in :meth:`Process.open_files()` when the + worker thread for ``NtQueryObject`` times out. +- :gh:`986`, [Linux]: :meth:`Process.cwd()` may raise :exc:`NoSuchProcess` + instead of :exc:`ZombieProcess`. + +5.1.3 +^^^^^ + +**Bug fixes** + +- :gh:`971`, [Linux]: :func:`sensors_temperatures()` didn't work on CentOS 7. +- :gh:`973`, **[critical]**: :func:`cpu_percent()` may raise + ``ZeroDivisionError``. + +5.1.2 — 2017-02-03 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`966`, [Linux]: :func:`sensors_battery()` ``power_plugged`` may + erroneously return ``None`` on Python 3. +- :gh:`968`, [Linux]: :func:`disk_io_counters()` raises ``TypeError`` on Python + 3. +- :gh:`970`, [Linux]: :func:`sensors_battery()` ``name`` and ``label`` fields + on Python 3 are bytes instead of str. + +5.1.1 — 2017-02-03 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`966`, [Linux]: :func:`sensors_battery()` ``percent`` is a float and is + more precise. + +**Bug fixes** + +- :gh:`964`, [Windows]: :meth:`Process.username()` and :func:`users()` may + return badly decoded character on Python 3. +- :gh:`965`, [Linux]: :func:`disk_io_counters()` may miscalculate sector size + and report the wrong number of bytes read and written. +- :gh:`966`, [Linux]: :func:`sensors_battery()` may fail with + ``FileNotFoundError``. +- :gh:`966`, [Linux]: :func:`sensors_battery()` ``power_plugged`` may lie. + +5.1.0 — 2017-02-01 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`357`: added :meth:`Process.cpu_num()` (what CPU a process is on). +- :gh:`371`: added :func:`sensors_temperatures()` (Linux only). +- :gh:`941`: added :func:`cpu_freq()` (CPU frequency). +- :gh:`955`: added :func:`sensors_battery()` (Linux, Windows, only). +- :gh:`956`: :meth:`Process.cpu_affinity()` can now be passed ``[]`` argument + as an alias to set affinity against all eligible CPUs. + +**Bug fixes** + +- :gh:`687`, [Linux]: :func:`pid_exists()` no longer returns ``True`` if passed + a process thread ID. +- :gh:`948`: cannot install psutil with ``PYTHONOPTIMIZE=2``. +- :gh:`950`, [Windows]: :meth:`Process.cpu_percent()` was calculated + incorrectly and showed higher number than real usage. +- :gh:`951`, [Windows]: the uploaded wheels for Python 3.6 64 bit didn't work. +- :gh:`959`: psutil exception objects could not be pickled. +- :gh:`960`: :class:`Popen` ``wait()`` did not return the correct negative exit + status if process is killed by a signal. +- :gh:`961`, [Windows]: ``WindowsService.description()`` method may fail with + ``ERROR_MUI_FILE_NOT_FOUND``. + +5.0.1 — 2016-12-21 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`939`: tar.gz distribution went from 1.8M to 258K. +- :gh:`811`, [Windows]: provide a more meaningful error message if trying to + use psutil on unsupported Windows XP. + +**Bug fixes** + +- :gh:`609`, [SunOS], **[critical]**: psutil does not compile on Solaris 10. +- :gh:`936`, [Windows]: fix compilation error on VS 2013 (patch by Max + Bélanger). +- :gh:`940`, [Linux]: :func:`cpu_percent()` and :func:`cpu_times_percent()` was + calculated incorrectly as ``iowait``, ``guest`` and ``guest_nice`` times were + not properly taken into account. +- :gh:`944`, [OpenBSD]: :func:`pids()` was omitting PID 0. + +5.0.0 — 2016-11-06 +^^^^^^^^^^^^^^^^^^ + +**Enhncements** + +- :gh:`799`: new :meth:`Process.oneshot()` context manager making + :class:`Process` methods around +2x faster in general and from +2x to +6x + faster on Windows. +- :gh:`943`: better error message in case of version conflict on import. + +**Bug fixes** + +- :gh:`932`, [NetBSD]: :func:`net_connections()` and + :meth:`Process.connections()` may fail without raising an exception. +- :gh:`933`, [Windows]: memory leak in :func:`cpu_stats()` and + ``WindowsService.description()`` method. + +4.4.2 — 2016-10-26 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`931`, **[critical]**: psutil no longer compiles on Solaris. + +4.4.1 — 2016-10-25 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`927`, **[critical]**: :class:`Popen` ``__del__`` may cause maximum + recursion depth error. + +4.4.0 — 2016-10-23 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`874`, [Windows]: make :func:`net_if_addrs()` also return the + ``netmask``. +- :gh:`887`, [Linux]: :func:`virtual_memory()` ``available`` and ``used`` + values are more precise and match ``free`` cmdline utility. ``available`` + also takes into account LCX containers preventing ``available`` to overflow + ``total``. +- :gh:`891`: `procinfo.py`_ script has been updated and provides a lot more + info. + +**Bug fixes** + +- :gh:`514`, [macOS], **[critical]**: :meth:`Process.memory_maps()` can + segfault. +- :gh:`783`, [macOS]: :meth:`Process.status()` may erroneously return + ``"running"`` for zombie processes. +- :gh:`798`, [Windows]: :meth:`Process.open_files()` returns and empty list on + Windows 10. +- :gh:`825`, [Linux]: :meth:`Process.cpu_affinity()`: fix possible double close + and use of unopened socket. +- :gh:`880`, [Windows]: fix race condition inside :func:`net_connections()`. +- :gh:`885`: ``ValueError`` is raised if a negative integer is passed to + :func:`cpu_percent()` functions. +- :gh:`892`, [Linux], **[critical]**: :meth:`Process.cpu_affinity()` with + ``[-1]`` as arg raises ``SystemError`` with no error set; now ``ValueError`` + is raised. +- :gh:`906`, [BSD]: :func:`disk_partitions()` with ``all=False`` returned an + empty list. Now the argument is ignored and all partitions are always + returned. +- :gh:`907`, [FreeBSD]: :meth:`Process.exe()` may fail with + ``OSError(ENOENT)``. +- :gh:`908`, [macOS], [BSD]: different process methods could errounesuly mask + the real error for high-privileged PIDs and raise :exc:`NoSuchProcess` and + :exc:`AccessDenied` instead of ``OSError`` and ``RuntimeError``. +- :gh:`909`, [macOS]: :meth:`Process.open_files()` and + :meth:`Process.connections()` methods may raise ``OSError`` with no exception + set if process is gone. +- :gh:`916`, [macOS]: fix many compilation warnings. + +4.3.1 — 2016-09-01 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`881`: ``make install`` now works also when using a virtual env. + +**Bug fixes** + +- :gh:`854`: :meth:`Process.as_dict()` raises ``ValueError`` if passed an + erroneous attrs name. +- :gh:`857`, [SunOS]: :meth:`Process.cpu_times()`, + :meth:`Process.cpu_percent()`, + :meth:`Process.threads()` and :meth:`Process.memory_maps()` may raise + ``RuntimeError`` if attempting to query a 64bit process with a 32bit Python. + "Null" values are returned as a fallback. +- :gh:`858`: :meth:`Process.as_dict()` should not call + :meth:`Process.memory_info_ex()` because it's deprecated. +- :gh:`863`, [Windows]: :meth:`Process.memory_maps()` truncates addresses above + 32 bits. +- :gh:`866`, [Windows]: :func:`win_service_iter()` and services in general are + not able to handle unicode service names / descriptions. +- :gh:`869`, [Windows]: :meth:`Process.wait()` may raise :exc:`TimeoutExpired` + with wrong timeout unit (ms instead of sec). +- :gh:`870`, [Windows]: handle leak inside ``psutil_get_process_data``. + +4.3.0 — 2016-06-18 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`819`, [Linux]: different speedup improvements: + :meth:`Process.ppid()` +20% faster. + :meth:`Process.status()` +28% faster. + :meth:`Process.name()` +25% faster. + :meth:`Process.num_threads()` +20% faster on Python 3. + +**Bug fixes** + +- :gh:`810`, [Windows]: Windows wheels are incompatible with pip 7.1.2. +- :gh:`812`, [NetBSD], **[critical]**: fix compilation on NetBSD-5.x. +- :gh:`823`, [NetBSD]: :func:`virtual_memory()` raises ``TypeError`` on Python + 3. +- :gh:`829`, [POSIX]: :func:`disk_usage()` ``percent`` field takes root + reserved space into account. +- :gh:`816`, [Windows]: fixed :func:`net_io_counters()` values wrapping after + 4.3GB in Windows Vista (NT 6.0) and above using 64bit values from newer win + APIs. + +4.2.0 — 2016-05-14 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`795`, [Windows]: new APIs to deal with Windows services: + :func:`win_service_iter()` and :func:`win_service_get()`. +- :gh:`800`, [Linux]: :func:`virtual_memory()` returns a new ``shared`` memory + field. +- :gh:`819`, [Linux]: speedup ``/proc`` parsing: + :meth:`Process.ppid()` +20% faster. + :meth:`Process.status()` +28% faster. + :meth:`Process.name()` +25% faster. + :meth:`Process.num_threads()` +20% faster on Python 3. + +**Bug fixes** + +- :gh:`797`, [Linux]: :func:`net_if_stats()` may raise ``OSError`` for certain + NIC cards. +- :gh:`813`: :meth:`Process.as_dict()` should ignore extraneous attribute names + which gets attached to the :class:`Process` instance. + +4.1.0 — 2016-03-12 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`777`, [Linux]: :meth:`Process.open_files()` on Linux return 3 new + fields: ``position``, ``mode`` and ``flags``. +- :gh:`779`: :meth:`Process.cpu_times()` returns two new fields, + ``children_user`` and ``children_system`` (always set to 0 on macOS and + Windows). +- :gh:`789`, [Windows]: :func:`cpu_times()` return two new fields: + ``interrupt`` and ``dpc``. Same for :func:`cpu_times_percent()`. +- :gh:`792`: new :func:`cpu_stats()` function returning number of CPU + ``ctx_switches``, ``interrupts``, ``soft_interrupts`` and ``syscalls``. + +**Bug fixes** + +- :gh:`774`, [FreeBSD]: :func:`net_io_counters()` dropout is no longer set to 0 + if the kernel provides it. +- :gh:`776`, [Linux]: :meth:`Process.cpu_affinity()` may erroneously raise + :exc:`NoSuchProcess`. (patch by wxwright) +- :gh:`780`, [macOS]: psutil does not compile with some GCC versions. +- :gh:`786`: :func:`net_if_addrs()` may report incomplete MAC addresses. +- :gh:`788`, [NetBSD]: :func:`virtual_memory()` ``buffers`` and ``shared`` + values were set to 0. +- :gh:`790`, [macOS], **[critical]**: psutil won't compile on macOS 10.4. + +4.0.0 — 2016-02-17 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`523`, [Linux], [FreeBSD]: :func:`disk_io_counters()` return a new + ``busy_time`` field. +- :gh:`660`, [Windows]: make.bat is smarter in finding alternative VS install + locations. (patch by mpderbec) +- :gh:`732`: :meth:`Process.environ()`. (patch by Frank Benkstein) +- :gh:`753`, [Linux], [macOS], [Windows]: process USS and PSS (Linux) "real" + memory stats. (patch by Eric Rahm) +- :gh:`755`: :meth:`Process.memory_percent()` ``memtype`` parameter. +- :gh:`758`: tests now live in psutil namespace. +- :gh:`760`: expose OS constants (``psutil.LINUX``, ``psutil.OSX``, etc.) +- :gh:`756`, [Linux]: :func:`disk_io_counters()` return 2 new fields: + ``read_merged_count`` and ``write_merged_count``. +- :gh:`762`: new `procsmem.py`_ script. + +**Bug fixes** + +- :gh:`685`, [Linux]: :func:`virtual_memory()` provides wrong results on + systems with a lot of physical memory. +- :gh:`704`, [SunOS]: psutil does not compile on Solaris sparc. +- :gh:`734`: on Python 3 invalid UTF-8 data is not correctly handled for + :meth:`Process.name()`, :meth:`Process.cwd()`, :meth:`Process.exe()`, + :meth:`Process.cmdline()` and :meth:`Process.open_files()` methods resulting + in ``UnicodeDecodeError`` exceptions. ``'surrogateescape'`` error handler + is now used as a workaround for replacing the corrupted data. +- :gh:`737`, [Windows]: when the bitness of psutil and the target process was + different, :meth:`Process.cmdline()` and :meth:`Process.cwd()` could return a + wrong result or incorrectly report an :exc:`AccessDenied` error. +- :gh:`741`, [OpenBSD]: psutil does not compile on mips64. +- :gh:`751`, [Linux]: fixed call to ``Py_DECREF`` on possible ``NULL`` object. +- :gh:`754`, [Linux]: :meth:`Process.cmdline()` can be wrong in case of zombie + process. +- :gh:`759`, [Linux]: :meth:`Process.memory_maps()` may return paths ending + with ``" (deleted)"``. +- :gh:`761`, [Windows]: :func:`boot_time()` wraps to 0 after 49 days. +- :gh:`764`, [NetBSD]: fix compilation on NetBSD-6.x. +- :gh:`766`, [Linux]: :func:`net_connections()` can't handle malformed + ``/proc/net/unix`` file. +- :gh:`767`, [Linux]: :func:`disk_io_counters()` may raise ``ValueError`` on + 2.6 kernels and it's broken on 2.4 kernels. +- :gh:`770`, [NetBSD]: :func:`disk_io_counters()` metrics didn't update. + +3.4.2 — 2016-01-20 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`728`, [SunOS]: exposed :data:`PROCFS_PATH` constant to change the + default location of ``/proc`` filesystem. + +**Bug fixes** + +- :gh:`724`, [FreeBSD]: :func:`virtual_memory()` ``total`` is incorrect. +- :gh:`730`, [FreeBSD], **[critical]**: :func:`virtual_memory()` crashes with + "OSError: [Errno 12] Cannot allocate memory". + +3.4.1 — 2016-01-15 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`557`, [NetBSD]: added NetBSD support. (contributed by Ryo Onodera and + Thomas Klausner) +- :gh:`708`, [Linux]: :func:`net_connections()` and + :meth:`Process.connections()` on Python 2 can be up to 3x faster in case of + many connections. Also + :meth:`Process.memory_maps()` is slightly faster. +- :gh:`718`: :func:`process_iter()` is now thread safe. + +**Bug fixes** + +- :gh:`714`, [OpenBSD]: :func:`virtual_memory()` ``cached`` value was always + set to 0. +- :gh:`715`, **[critical]**: don't crash at import time if :func:`cpu_times()` + fail for some reason. +- :gh:`717`, [Linux]: :meth:`Process.open_files()` fails if deleted files still + visible. +- :gh:`722`, [Linux]: :func:`swap_memory()` no longer crashes if ``sin`` / + ``sout`` can't be determined due to missing ``/proc/vmstat``. +- :gh:`724`, [FreeBSD]: :func:`virtual_memory()` ``total`` is slightly + incorrect. + +3.3.0 — 2015-11-25 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`558`, [Linux]: exposed :data:`PROCFS_PATH` constant to change the + default location of ``/proc`` filesystem. +- :gh:`615`, [OpenBSD]: added OpenBSD support. (contributed by Landry Breuil) + +**Bug fixes** + +- :gh:`692`, [POSIX]: :meth:`Process.name()` is no longer cached as it may + change. + +3.2.2 — 2015-10-04 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`517`, [SunOS]: :func:`net_io_counters()` failed to detect network + interfaces correctly on Solaris 10 +- :gh:`541`, [FreeBSD]: :func:`disk_io_counters()` r/w times were expressed in + seconds instead of milliseconds. (patch by dasumin) +- :gh:`610`, [SunOS]: fix build and tests on Solaris 10 +- :gh:`623`, [Linux]: process or system connections raises ``ValueError`` if + IPv6 is not supported by the system. +- :gh:`678`, [Linux], **[critical]**: can't install psutil due to bug in + setup.py. +- :gh:`688`, [Windows]: compilation fails with MSVC 2015, Python 3.5. (patch by + Mike Sarahan) + +3.2.1 — 2015-09-03 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`677`, [Linux], **[critical]**: can't install psutil due to bug in + setup.py. + +3.2.0 — 2015-09-02 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`644`, [Windows]: added support for ``CTRL_C_EVENT`` and + ``CTRL_BREAK_EVENT`` signals to use with :meth:`Process.send_signal()`. +- :gh:`648`: CI test integration for macOS. (patch by Jeff Tang) +- :gh:`663`, [POSIX]: :func:`net_if_addrs()` now returns point-to-point (VPNs) + addresses. +- :gh:`655`, [Windows]: different issues regarding unicode handling were fixed. + On Python 2 all APIs returning a string will now return an encoded version of + it by using sys.getfilesystemencoding() codec. The APIs involved are: + :func:`net_if_addrs()`, :func:`net_if_stats()`, :func:`net_io_counters()`, + :meth:`Process.cmdline()`, :meth:`Process.name()`, + :meth:`Process.username()`, :func:`users()`. + +**Bug fixes** + +- :gh:`513`, [Linux]: fixed integer overflow for ``RLIM_INFINITY``. +- :gh:`641`, [Windows]: fixed many compilation warnings. (patch by Jeff Tang) +- :gh:`652`, [Windows]: :func:`net_if_addrs()` ``UnicodeDecodeError`` in case + of non-ASCII NIC names. +- :gh:`655`, [Windows]: :func:`net_if_stats()` ``UnicodeDecodeError`` in case + of non-ASCII NIC names. +- :gh:`659`, [Linux]: compilation error on Suse 10. (patch by maozguttman) +- :gh:`664`, [Linux]: compilation error on Alpine Linux. (patch by Bart van + Kleef) +- :gh:`670`, [Windows]: segfgault of :func:`net_if_addrs()` in case of + non-ASCII NIC names. (patch by sk6249) +- :gh:`672`, [Windows]: compilation fails if using Windows SDK v8.0. (patch by + Steven Winfield) +- :gh:`675`, [Linux]: :func:`net_connections()`: ``UnicodeDecodeError`` may + occur when listing UNIX sockets. + +3.1.1 — 2015-07-15 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`603`, [Linux]: :meth:`Process.ionice()` set value range is incorrect. + (patch by spacewander) +- :gh:`645`, [Linux]: :func:`cpu_times_percent()` may produce negative results. +- :gh:`656`: ``from psutil import *`` does not work. + +3.1.0 — 2015-07-15 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`534`, [Linux]: :func:`disk_partitions()` added support for ZFS + filesystems. +- :gh:`646`, [Windows]: continuous tests integration for Windows with + https://ci.appveyor.com/project/giampaolo/psutil. +- :gh:`647`: new dev guide: + https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst +- :gh:`651`: continuous code quality test integration with scrutinizer-ci.com + +**Bug fixes** + +- :gh:`340`, [Windows], **[critical]**: :meth:`Process.open_files()` no longer + hangs. Instead it uses a thread which times out and skips the file handle in + case it's taking too long to be retrieved. (patch by Jeff Tang) +- :gh:`627`, [Windows]: :meth:`Process.name()` no longer raises + :exc:`AccessDenied` for pids owned by another user. +- :gh:`636`, [Windows]: :meth:`Process.memory_info()` raise + :exc:`AccessDenied`. +- :gh:`637`, [POSIX]: raise exception if trying to send signal to PID 0 as it + will affect ``os.getpid()`` 's process group and not PID 0. +- :gh:`639`, [Linux]: :meth:`Process.cmdline()` can be truncated. +- :gh:`640`, [Linux]: ``*connections`` functions may swallow errors and return + an incomplete list of connections. +- :gh:`642`: ``repr()`` of exceptions is incorrect. +- :gh:`653`, [Windows]: add ``inet_ntop()`` function for Windows XP to support + IPv6. +- :gh:`641`, [Windows]: replace deprecated string functions with safe + equivalents. + +3.0.1 — 2015-06-18 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`632`, [Linux]: better error message if cannot parse process UNIX + connections. +- :gh:`634`, [Linux]: :meth:`Process.cmdline()` does not include empty string + arguments. +- :gh:`635`, [POSIX], **[critical]**: crash on module import if ``enum`` + package is installed on Python < 3.4. + +3.0.0 — 2015-06-13 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`250`: new :func:`net_if_stats()` returning NIC statistics (``isup``, + ``duplex``, ``speed``, ``mtu``). +- :gh:`376`: new :func:`net_if_addrs()` returning all NIC addresses a-la + ``ifconfig``. +- :gh:`469`: on Python >= 3.4 ``IOPRIO_CLASS_*`` and ``*_PRIORITY_CLASS`` + constants returned by :meth:`Process.ionice()` and :meth:`Process.nice()` are + enums instead of plain integers. +- :gh:`581`: add ``.gitignore``. (patch by Gabi Davar) +- :gh:`582`: connection constants returned by :func:`net_connections()` and + :meth:`Process.connections()` were turned from int to enums on Python > 3.4. +- :gh:`587`: move native extension into the package. +- :gh:`589`: :meth:`Process.cpu_affinity()` accepts any kind of iterable (set, + tuple, ...), not only lists. +- :gh:`594`: all deprecated APIs were removed. +- :gh:`599`, [Windows]: :meth:`Process.name()` can now be determined for all + processes even when running as a limited user. +- :gh:`602`: pre-commit GIT hook. +- :gh:`629`: enhanced support for ``pytest`` and ``nose`` test runners. +- :gh:`616`, [Windows]: add ``inet_ntop()`` function for Windows XP. + +**Bug fixes** + +- :gh:`428`, [POSIX], **[critical]**: correct handling of zombie processes on + POSIX. Introduced new :exc:`ZombieProcess` exception class. +- :gh:`512`, [BSD], **[critical]**: fix segfault in :func:`net_connections()`. +- :gh:`555`, [Linux]: :func:`users()` correctly handles ``":0"`` as an alias + for ``"localhost"``. +- :gh:`579`, [Windows]: fixed :meth:`Process.open_files()` for PID > 64K. +- :gh:`579`, [Windows]: fixed many compiler warnings. +- :gh:`585`, [FreeBSD]: :func:`net_connections()` may raise ``KeyError``. +- :gh:`586`, [FreeBSD], **[critical]**: :meth:`Process.cpu_affinity()` + segfaults on set in case an invalid CPU number is provided. +- :gh:`593`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps()` + segfaults. +- :gh:`606`: :meth:`Process.parent()` may swallow :exc:`NoSuchProcess` + exceptions. +- :gh:`611`, [SunOS]: :func:`net_io_counters()` has send and received swapped +- :gh:`614`, [Linux]:: :func:`cpu_count()` with ``logical=False`` return the + number of sockets instead of cores. +- :gh:`618`, [SunOS]: swap tests fail on Solaris when run as normal user. +- :gh:`628`, [Linux]: :meth:`Process.name()` truncates string in case it + contains spaces or parentheses. + +2.2.1 — 2015-02-02 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`572`, [Linux]: fix "ValueError: ambiguous inode with multiple PIDs + references" for :meth:`Process.connections()`. (patch by Bruno Binet) + +2.2.0 — 2015-01-06 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`521`: drop support for Python 2.4 and 2.5. +- :gh:`553`: new `pstree.py`_ script. +- :gh:`564`: C extension version mismatch in case the user messed up with + psutil installation or with sys.path is now detected at import time. +- :gh:`568`: new `pidof.py`_ script. +- :gh:`569`, [FreeBSD]: add support for :meth:`Process.cpu_affinity()` on + FreeBSD. + +**Bug fixes** + +- :gh:`496`, [SunOS], **[critical]**: can't import psutil. +- :gh:`547`, [POSIX]: :meth:`Process.username()` may raise ``KeyError`` if UID + can't be resolved. +- :gh:`551`, [Windows]: get rid of the unicode hack for + :func:`net_io_counters()` NIC names. +- :gh:`556`, [Linux]: lots of file handles were left open. +- :gh:`561`, [Linux]: :func:`net_connections()` might skip some legitimate UNIX + sockets. (patch by spacewander) +- :gh:`565`, [Windows]: use proper encoding for :meth:`Process.username()` and + :func:`users()`. (patch by Sylvain Mouquet) +- :gh:`567`, [Linux]: in the alternative implementation of + :meth:`Process.cpu_affinity()` ``PyList_Append`` and ``Py_BuildValue`` return + values are not checked. +- :gh:`569`, [FreeBSD]: fix memory leak in :func:`cpu_count()` with + ``logical=False``. +- :gh:`571`, [Linux]: :meth:`Process.open_files()` might swallow + :exc:`AccessDenied` exceptions and return an incomplete list of open files. + +2.1.3 — 2014-09-26 +^^^^^^^^^^^^^^^^^^ + +- :gh:`536`, [Linux], **[critical]**: fix "undefined symbol: CPU_ALLOC" + compilation error. + +2.1.2 — 2014-09-21 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`407`: project moved from Google Code to Github; code moved from + Mercurial to Git. +- :gh:`492`: use ``tox`` to run tests on multiple Python versions. (patch by + msabramo) +- :gh:`505`, [Windows]: distribution as wheel packages. +- :gh:`511`: add `ps.py`_ script. + +**Bug fixes** + +- :gh:`340`, [Windows]: :meth:`Process.open_files()` no longer hangs. (patch + by Jeff Tang) +- :gh:`501`, [Windows]: :func:`disk_io_counters()` may return negative values. +- :gh:`503`, [Linux]: in rare conditions :meth:`Process.exe()`, + :meth:`Process.open_files()` and + :meth:`Process.connections()` can raise ``OSError(ESRCH)`` instead of + :exc:`NoSuchProcess`. +- :gh:`504`, [Linux]: can't build RPM packages via setup.py +- :gh:`506`, [Linux], **[critical]**: Python 2.4 support was broken. +- :gh:`522`, [Linux]: :meth:`Process.cpu_affinity()` might return ``EINVAL``. + (patch by David Daeschler) +- :gh:`529`, [Windows]: :meth:`Process.exe()` may raise unhandled + ``WindowsError`` exception for PIDs 0 and 4. (patch by Jeff Tang) +- :gh:`530`, [Linux]: :func:`disk_io_counters()` may crash on old Linux distros + (< 2.6.5) (patch by Yaolong Huang) +- :gh:`533`, [Linux]: :meth:`Process.memory_maps()` may raise ``TypeError`` on + old Linux distros. + +2.1.1 — 2014-04-30 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`446`, [Windows]: fix encoding error when using :func:`net_io_counters()` + on Python 3. (patch by Szigeti Gabor Niif) +- :gh:`460`, [Windows]: :func:`net_io_counters()` wraps after 4G. +- :gh:`491`, [Linux]: :func:`net_connections()` exceptions. (patch by Alexander + Grothe) + +2.1.0 — 2014-04-08 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`387`: system-wide open connections a-la ``netstat`` (add + :func:`net_connections()`). + +**Bug fixes** + +- :gh:`421`, [SunOS], **[critical]**: psutil does not compile on SunOS 5.10. + (patch by Naveed Roudsari) +- :gh:`489`, [Linux]: :func:`disk_partitions()` return an empty list. + +2.0.0 — 2014-03-10 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`424`, [Windows]: installer for Python 3.X 64 bit. +- :gh:`427`: number of logical CPUs and physical cores (:func:`cpu_count()`). +- :gh:`447`: :func:`wait_procs()` ``timeout`` parameter is now optional. +- :gh:`452`: make :class:`Process` instances hashable and usable with ``set()`` + s. +- :gh:`453`: tests on Python < 2.7 require ``unittest2`` module. +- :gh:`459`: add a Makefile for running tests and other repetitive tasks (also + on Windows). +- :gh:`463`: make timeout parameter of ``cpu_percent*`` functions default to + ``0.0`` 'cause it's a common trap to introduce slowdowns. +- :gh:`468`: move documentation to readthedocs.com. +- :gh:`477`: :meth:`Process.cpu_percent()` is about 30% faster. (suggested by + crusaderky) +- :gh:`478`, [Linux]: almost all APIs are about 30% faster on Python 3.X. +- :gh:`479`: long deprecated ``psutil.error`` module is gone; exception classes + now live in psutil namespace only. + +**Bug fixes** + +- :gh:`193`: :class:`Popen` constructor can throw an exception if the spawned + process terminates quickly. +- :gh:`340`, [Windows]: :meth:`Process.open_files()` no longer hangs. (patch + by jtang@vahna.net) +- :gh:`443`, [Linux]: fix a potential overflow issue for + :meth:`Process.cpu_affinity()` (set) on systems with more than 64 CPUs. +- :gh:`448`, [Windows]: :meth:`Process.children()` and :meth:`Process.ppid()` + memory leak (patch by Ulrich Klank). +- :gh:`457`, [POSIX]: :func:`pid_exists()` always returns ``True`` for PID 0. +- :gh:`461`: namedtuples are not pickle-able. +- :gh:`466`, [Linux]: :meth:`Process.exe()` improper null bytes handling. + (patch by Gautam Singh) +- :gh:`470`: :func:`wait_procs()` might not wait. (patch by crusaderky) +- :gh:`471`, [Windows]: :meth:`Process.exe()` improper unicode handling. (patch + by alex@mroja.net) +- :gh:`473`: :class:`Popen` ``wait()`` method does not set returncode + attribute. +- :gh:`474`, [Windows]: :meth:`Process.cpu_percent()` is no longer capped at + 100%. +- :gh:`476`, [Linux]: encoding error for :meth:`Process.name()` and + :meth:`Process.cmdline()`. + +**API changes** + +For the sake of consistency a lot of psutil APIs have been renamed. In most +cases accessing the old names will work but it will cause a +``DeprecationWarning``. + +- ``psutil.*`` module level constants have being replaced by functions: + + +-----------------------+----------------------------------+ + | Old name | Replacement | + +=======================+==================================+ + | psutil.NUM_CPUS | psutil.cpu_count() | + +-----------------------+----------------------------------+ + | psutil.BOOT_TIME | psutil.boot_time() | + +-----------------------+----------------------------------+ + | psutil.TOTAL_PHYMEM | virtual_memory.total | + +-----------------------+----------------------------------+ + +- Renamed ``psutil.*`` functions: + + +------------------------+-------------------------------+ + | Old name | Replacement | + +========================+===============================+ + | psutil.get_pid_list() | psutil.pids() | + +------------------------+-------------------------------+ + | psutil.get_users() | psutil.users() | + +------------------------+-------------------------------+ + | psutil.get_boot_time() | psutil.boot_time() | + +------------------------+-------------------------------+ + +- All :class:`Process` ``get_*`` methods lost the ``get_`` prefix. E.g. + ``get_ext_memory_info()`` was renamed to ``memory_info_ex()``. Assuming ``p = + psutil.Process()``: + + +--------------------------+----------------------+ + | Old name | Replacement | + +==========================+======================+ + | p.get_children() | p.children() | + +--------------------------+----------------------+ + | p.get_connections() | p.connections() | + +--------------------------+----------------------+ + | p.get_cpu_affinity() | p.cpu_affinity() | + +--------------------------+----------------------+ + | p.get_cpu_percent() | p.cpu_percent() | + +--------------------------+----------------------+ + | p.get_cpu_times() | p.cpu_times() | + +--------------------------+----------------------+ + | p.get_ext_memory_info() | p.memory_info_ex() | + +--------------------------+----------------------+ + | p.get_io_counters() | p.io_counters() | + +--------------------------+----------------------+ + | p.get_ionice() | p.ionice() | + +--------------------------+----------------------+ + | p.get_memory_info() | p.memory_info() | + +--------------------------+----------------------+ + | p.get_memory_maps() | p.memory_maps() | + +--------------------------+----------------------+ + | p.get_memory_percent() | p.memory_percent() | + +--------------------------+----------------------+ + | p.get_nice() | p.nice() | + +--------------------------+----------------------+ + | p.get_num_ctx_switches() | p.num_ctx_switches() | + +--------------------------+----------------------+ + | p.get_num_fds() | p.num_fds() | + +--------------------------+----------------------+ + | p.get_num_threads() | p.num_threads() | + +--------------------------+----------------------+ + | p.get_open_files() | p.open_files() | + +--------------------------+----------------------+ + | p.get_rlimit() | p.rlimit() | + +--------------------------+----------------------+ + | p.get_threads() | p.threads() | + +--------------------------+----------------------+ + | p.getcwd() | p.cwd() | + +--------------------------+----------------------+ + +- All :class:`Process` ``set_*`` methods lost the ``set_`` prefix. Assuming ``p + = psutil.Process()``: + + +----------------------+---------------------------------+ + | Old name | Replacement | + +======================+=================================+ + | p.set_nice() | p.nice(value) | + +----------------------+---------------------------------+ + | p.set_ionice() | p.ionice(ioclass, value=None) | + +----------------------+---------------------------------+ + | p.set_cpu_affinity() | p.cpu_affinity(cpus) | + +----------------------+---------------------------------+ + | p.set_rlimit() | p.rlimit(resource, limits=None) | + +----------------------+---------------------------------+ + +- Except for ``pid``, all :class:`Process` class properties have been turned + into methods. This is the only case which there are no aliases. Assuming ``p + = psutil.Process()``: + + +---------------+-----------------+ + | Old name | Replacement | + +===============+=================+ + | p.name | p.name() | + +---------------+-----------------+ + | p.parent | p.parent() | + +---------------+-----------------+ + | p.ppid | p.ppid() | + +---------------+-----------------+ + | p.exe | p.exe() | + +---------------+-----------------+ + | p.cmdline | p.cmdline() | + +---------------+-----------------+ + | p.status | p.status() | + +---------------+-----------------+ + | p.uids | p.uids() | + +---------------+-----------------+ + | p.gids | p.gids() | + +---------------+-----------------+ + | p.username | p.username() | + +---------------+-----------------+ + | p.create_time | p.create_time() | + +---------------+-----------------+ + +- timeout parameter of ``cpu_percent*`` functions defaults to 0.0 instead of + 0.1. +- long deprecated ``psutil.error`` module is gone; exception classes now live + in "psutil" namespace only. +- :class:`Process` instances' ``retcode`` attribute returned by + :func:`wait_procs()` has been renamed to ``returncode`` for consistency with + ``subprocess.Popen``. + +1.2.1 — 2013-11-25 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`348`, [Windows], **[critical]**: fixed "ImportError: DLL load failed" + occurring on module import on Windows XP. +- :gh:`425`, [SunOS], **[critical]**: crash on import due to failure at + determining ``BOOT_TIME``. +- :gh:`443`, [Linux]: :meth:`Process.cpu_affinity()` can't set affinity on + systems with more than 64 cores. + +1.2.0 — 2013-11-20 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`439`: assume ``os.getpid()`` if no argument is passed to + :class:`Process` class constructor. +- :gh:`440`: new :func:`wait_procs()` utility function which waits for multiple + processes to terminate. + +**Bug fixes** + +- :gh:`348`, [Windows]: fix "ImportError: DLL load failed" occurring on module + import on Windows XP / Vista. + +1.1.3 — 2013-11-07 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`442`, [Linux], **[critical]**: psutil won't compile on certain version + of Linux because of missing ``prlimit(2)`` syscall. + +1.1.2 — 2013-10-22 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`442`, [Linux], **[critical]**: psutil won't compile on Debian 6.0 + because of missing ``prlimit(2)`` syscall. + +1.1.1 — 2013-10-08 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`442`, [Linux], **[critical]**: psutil won't compile on kernels < 2.6.36 + due to missing ``prlimit(2)`` syscall. + +1.1.0 — 2013-09-28 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`410`: host tar.gz and Windows binary files are on PyPI. +- :gh:`412`, [Linux]: get/set process resource limits + (:meth:`Process.rlimit()`). +- :gh:`415`, [Windows]: :meth:`Process.children()` is an order of magnitude + faster. +- :gh:`426`, [Windows]: :meth:`Process.name()` is an order of magnitude faster. +- :gh:`431`, [POSIX]: :meth:`Process.name()` is slightly faster because it + unnecessarily retrieved also :meth:`Process.cmdline()`. + +**Bug fixes** + +- :gh:`391`, [Windows]: :func:`cpu_times_percent()` returns negative + percentages. +- :gh:`408`: ``STATUS_*`` and ``CONN_*`` constants don't properly serialize on + JSON. +- :gh:`411`, [Windows]: `disk_usage.py`_ may pop-up a GUI error. +- :gh:`413`, [Windows]: :meth:`Process.memory_info()` leaks memory. +- :gh:`414`, [Windows]: :meth:`Process.exe()` on Windows XP may raise + ``ERROR_INVALID_PARAMETER``. +- :gh:`416`: :func:`disk_usage()` doesn't work well with unicode path names. +- :gh:`430`, [Linux]: :meth:`Process.io_counters()` report wrong number of r/w + syscalls. +- :gh:`435`, [Linux]: :func:`net_io_counters()` might report erreneous NIC + names. +- :gh:`436`, [Linux]: :func:`net_io_counters()` reports a wrong ``dropin`` + value. + +**API changes** + +- :gh:`408`: turn ``STATUS_*`` and ``CONN_*`` constants into plain Python + strings. + +1.0.1 — 2013-07-12 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`405`: :func:`net_io_counters()` ``pernic=True`` no longer works as + intended in 1.0.0. + +1.0.0 — 2013-07-10 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`18`, [SunOS]: add Solaris support (yay!) (thanks Justin Venus) +- :gh:`367`: :meth:`Process.connections()` ``status`` strings are now + constants. +- :gh:`380`: test suite exits with non-zero on failure. (patch by + floppymaster) +- :gh:`391`: introduce unittest2 facilities and provide workarounds if + unittest2 is not installed (Python < 2.7). + +**Bug fixes** + +- :gh:`374`, [Windows]: negative memory usage reported if process uses a lot of + memory. +- :gh:`379`, [Linux]: :meth:`Process.memory_maps()` may raise ``ValueError``. +- :gh:`394`, [macOS]: mapped memory regions of :meth:`Process.memory_maps()` + report incorrect file name. +- :gh:`404`, [Linux]: ``sched_*affinity()`` are implicitly declared. (patch by + Arfrever) + +**API changes** + +- :meth:`Process.connections()` ``status`` field is no longer a string but a + constant object (``psutil.CONN_*``). +- :meth:`Process.connections()` ``local_address`` and ``remote_address`` fields + renamed to ``laddr`` and ``raddr``. +- psutil.network_io_counters() renamed to :func:`net_io_counters()`. + +0.7.1 — 2013-05-03 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`325`, [BSD], **[critical]**: :func:`virtual_memory()` can raise + ``SystemError``. (patch by Jan Beich) +- :gh:`370`, [BSD]: :meth:`Process.connections()` requires root. (patch by + John Baldwin) +- :gh:`372`, [BSD]: different process methods raise :exc:`NoSuchProcess` + instead of + :exc:`AccessDenied`. + +0.7.0 — 2013-04-12 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`233`: code migrated to Mercurial (yay!) +- :gh:`246`: psutil.error module is deprecated and scheduled for removal. +- :gh:`328`, [Windows]: :meth:`Process.ionice()` support. +- :gh:`359`: add :func:`boot_time()` as a substitute of ``psutil.BOOT_TIME`` + since the latter cannot reflect system clock updates. +- :gh:`361`, [Linux]: :func:`cpu_times()` now includes new ``steal``, ``guest`` + and ``guest_nice`` fields available on recent Linux kernels. Also, + :func:`cpu_percent()` is more accurate. +- :gh:`362`: add :func:`cpu_times_percent()` (per-CPU-time utilization as a + percentage). + +**Bug fixes** + +- :gh:`234`, [Windows]: :func:`disk_io_counters()` fails to list certain disks. +- :gh:`264`, [Windows]: use of :func:`disk_partitions()` may cause a message + box to appear. +- :gh:`313`, [Linux], **[critical]**: :func:`virtual_memory()` and + :func:`swap_memory()` can crash on certain exotic Linux flavors having an + incomplete ``/proc`` interface. If that's the case we now set the + unretrievable stats to ``0`` and raise ``RuntimeWarning`` instead. +- :gh:`315`, [macOS]: fix some compilation warnings. +- :gh:`317`, [Windows]: cannot set process CPU affinity above 31 cores. +- :gh:`319`, [Linux]: :meth:`Process.memory_maps()` raises ``KeyError`` + 'Anonymous' on Debian squeeze. +- :gh:`321`, [POSIX]: :meth:`Process.ppid()` property is no longer cached as + the kernel may set the PPID to 1 in case of a zombie process. +- :gh:`323`, [macOS]: :func:`disk_io_counters()` ``read_time`` and + ``write_time`` parameters were reporting microseconds not milliseconds. + (patch by Gregory Szorc) +- :gh:`331`: :meth:`Process.cmdline()` is no longer cached after first access + as it may change. +- :gh:`333`, [macOS]: leak of Mach ports (patch by rsesek@google.com) +- :gh:`337`, [Linux], **[critical]**: :class:`Process` methods not working + because of a poor ``/proc`` implementation will raise ``NotImplementedError`` + rather than ``RuntimeError`` and :meth:`Process.as_dict()` will not blow up. + (patch by Curtin1060) +- :gh:`338`, [Linux]: :func:`disk_io_counters()` fails to find some disks. +- :gh:`339`, [FreeBSD]: ``get_pid_list()`` can allocate all the memory on + system. +- :gh:`341`, [Linux], **[critical]**: psutil might crash on import due to error + in retrieving system terminals map. +- :gh:`344`, [FreeBSD]: :func:`swap_memory()` might return incorrect results + due to ``kvm_open(3)`` not being called. (patch by Jean Sebastien) +- :gh:`338`, [Linux]: :func:`disk_io_counters()` fails to find some disks. +- :gh:`351`, [Windows]: if psutil is compiled with MinGW32 (provided installers + for py2.4 and py2.5 are) :func:`disk_io_counters()` will fail. (Patch by + m.malycha) +- :gh:`353`, [macOS]: :func:`users()` returns an empty list on macOS 10.8. +- :gh:`356`: :meth:`Process.parent()` now checks whether parent PID has been + reused in which case returns ``None``. +- :gh:`365`: :meth:`Process.nice()` (set) should check PID has not been reused + by another process. +- :gh:`366`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps()`, + :meth:`Process.num_fds()`, + :meth:`Process.open_files()` and :meth:`Process.cwd()` methods raise + ``RuntimeError`` instead of :exc:`AccessDenied`. + +**API changes** + +- :meth:`Process.cmdline()` property is no longer cached after first access. +- :meth:`Process.ppid()` property is no longer cached after first access. +- [Linux] :class:`Process` methods not working because of a poor ``/proc`` + implementation will raise ``NotImplementedError`` instead of + ``RuntimeError``. +- ``psutil.error`` module is deprecated and scheduled for removal. + +0.6.1 — 2012-08-16 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`316`: :meth:`Process.cmdline()` property now makes a better job at + guessing the process executable from the cmdline. + +**Bug fixes** + +- :gh:`316`: :meth:`Process.exe()` was resolved in case it was a symlink. +- :gh:`318`, **[critical]**: Python 2.4 compatibility was broken. + +**API changes** + +- :meth:`Process.exe()` can now return an empty string instead of raising + :exc:`AccessDenied`. +- :meth:`Process.exe()` is no longer resolved in case it's a symlink. + +0.6.0 — 2012-08-13 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`216`, [POSIX]: :meth:`Process.connections()` UNIX sockets support. +- :gh:`220`, [FreeBSD]: ``get_connections()`` has been rewritten in C and no + longer requires ``lsof``. +- :gh:`222`, [macOS]: add support for :meth:`Process.cwd()`. +- :gh:`261`: per-process extended memory info + (:meth:`Process.memory_info_ex()`). +- :gh:`295`, [macOS]: :meth:`Process.exe()` path is now determined by asking + the OS instead of being guessed from :meth:`Process.cmdline()`. +- :gh:`297`, [macOS]: the :class:`Process` methods below were always raising + :exc:`AccessDenied` for any process except the current one. Now this is no + longer true. Also they are 2.5x faster. :meth:`Process.name()`, + :meth:`Process.memory_info()`, + :meth:`Process.memory_percent()`, :meth:`Process.cpu_times()`, + :meth:`Process.cpu_percent()`, + :meth:`Process.num_threads()`. +- :gh:`300`: add `pmap.py`_ script. +- :gh:`301`: :func:`process_iter()` now yields processes sorted by their PIDs. +- :gh:`302`: per-process number of voluntary and involuntary context switches + (:meth:`Process.num_ctx_switches()`). +- :gh:`303`, [Windows]: the :class:`Process` methods below were always raising + :exc:`AccessDenied` for any process not owned by current user. Now this is no + longer true: + :meth:`Process.create_time()`, :meth:`Process.cpu_times()`, + :meth:`Process.cpu_percent()`, + :meth:`Process.memory_info()`, :meth:`Process.memory_percent()`, + :meth:`Process.num_handles()`, + :meth:`Process.io_counters()`. +- :gh:`305`: add `netstat.py`_ script. +- :gh:`311`: system memory functions has been refactorized and rewritten and + now provide a more detailed and consistent representation of the system + memory. Added new :func:`virtual_memory()` and :func:`swap_memory()` + functions. All old memory-related functions are deprecated. Also two new + example scripts were added: `free.py`_ and `meminfo.py`_. +- :gh:`312`: ``net_io_counters()`` namedtuple includes 4 new fields: ``errin``, + ``errout``, ``dropin`` and ``dropout``, reflecting the number of packets + dropped and with errors. + +**Bug fixes** + +- :gh:`298`, [macOS], [BSD]: memory leak in :meth:`Process.num_fds()`. +- :gh:`299`: potential memory leak every time ``PyList_New(0)`` is used. +- :gh:`303`, [Windows], **[critical]**: potential heap corruption in + :meth:`Process.num_threads()` and :meth:`Process.status()` methods. +- :gh:`305`, [FreeBSD], **[critical]**: can't compile on FreeBSD 9 due to + removal of ``utmp.h``. +- :gh:`306`, **[critical]**: at C level, errors are not checked when invoking + ``Py*`` functions which create or manipulate Python objects leading to + potential memory related errors and/or segmentation faults. +- :gh:`307`, [FreeBSD]: values returned by :func:`net_io_counters()` are wrong. +- :gh:`308`, [BSD], [Windows]: ``psutil.virtmem_usage()`` wasn't actually + returning information about swap memory usage as it was supposed to do. It + does now. +- :gh:`309`: :meth:`Process.open_files()` might not return files which can not + be accessed due to limited permissions. :exc:`AccessDenied` is now raised + instead. + +**API changes** + +- ``psutil.phymem_usage()`` is deprecated (use :func:`virtual_memory()`) +- ``psutil.virtmem_usage()`` is deprecated (use :func:`swap_memory()`) +- [Linux]: ``psutil.phymem_buffers()`` is deprecated (use + :func:`virtual_memory()`) +- [Linux]: ``psutil.cached_phymem()`` is deprecated (use + :func:`virtual_memory()`) +- [Windows], [BSD]: ``psutil.virtmem_usage()`` now returns information about + swap memory instead of virtual memory. + +0.5.1 — 2012-06-29 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`293`, [Windows]: :meth:`Process.exe()` path is now determined by asking + the OS instead of being guessed from :meth:`Process.cmdline()`. + +**Bug fixes** + +- :gh:`292`, [Linux]: race condition in process :meth:`Process.open_files()`, + :meth:`Process.connections()`, :meth:`Process.threads()`. +- :gh:`294`, [Windows]: :meth:`Process.cpu_affinity()` is only able to set CPU + #0. + +0.5.0 — 2012-06-27 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`195`, [Windows]: number of handles opened by process + (:meth:`Process.num_handles()`). +- :gh:`209`: :func:`disk_partitions()` now provides also mount options. +- :gh:`229`: list users currently connected on the system (:func:`users()`). +- :gh:`238`, [Linux], [Windows]: process CPU affinity (get and set, + :meth:`Process.cpu_affinity()`). +- :gh:`242`: add ``recursive=True`` to :meth:`Process.children()`: return all + process descendants. +- :gh:`245`, [POSIX]: :meth:`Process.wait()` incrementally consumes less CPU + cycles. +- :gh:`257`, [Windows]: removed Windows 2000 support. +- :gh:`258`, [Linux]: :meth:`Process.memory_info()` is now 0.5x faster. +- :gh:`260`: process's mapped memory regions. (Windows patch by wj32.64, macOS + patch by Jeremy Whitlock) +- :gh:`262`, [Windows]: :func:`disk_partitions()` was slow due to inspecting + the floppy disk drive also when parameter is ``all=False``. +- :gh:`273`: ``psutil.get_process_list()`` is deprecated. +- :gh:`274`: psutil no longer requires ``2to3`` at installation time in order + to work with Python 3. +- :gh:`278`: new :meth:`Process.as_dict()` method. +- :gh:`281`: :meth:`Process.ppid()`, :meth:`Process.name()`, + :meth:`Process.exe()`, + :meth:`Process.cmdline()` and :meth:`Process.create_time()` properties of + :class:`Process` class are now cached after being accessed. +- :gh:`282`: ``psutil.STATUS_*`` constants can now be compared by using their + string representation. +- :gh:`283`: speedup :meth:`Process.is_running()` by caching its return value + in case the process is terminated. +- :gh:`284`, [POSIX]: per-process number of opened file descriptors + (:meth:`Process.num_fds()`). +- :gh:`287`: :func:`process_iter()` now caches :class:`Process` instances + between calls. +- :gh:`290`: :meth:`Process.nice()` property is deprecated in favor of new + ``get_nice()`` and ``set_nice()`` methods. + +**Bug fixes** + +- :gh:`193`: :class:`Popen` constructor can throw an exception if the spawned + process terminates quickly. +- :gh:`240`, [macOS]: incorrect use of ``free()`` for + :meth:`Process.connections()`. +- :gh:`244`, [POSIX]: :meth:`Process.wait()` can hog CPU resources if called + against a process which is not our children. +- :gh:`248`, [Linux]: :func:`net_io_counters()` might return erroneous NIC + names. +- :gh:`252`, [Windows]: :meth:`Process.cwd()` erroneously raise + :exc:`NoSuchProcess` for processes owned by another user. It now raises + :exc:`AccessDenied` instead. +- :gh:`266`, [Windows]: ``psutil.get_pid_list()`` only shows 1024 processes. + (patch by Amoser) +- :gh:`267`, [macOS]: :meth:`Process.connections()` returns wrong remote + address. (Patch by Amoser) +- :gh:`272`, [Linux]: :meth:`Process.open_files()` potential race condition can + lead to unexpected :exc:`NoSuchProcess` exception. Also, we can get incorrect + reports of not absolutized path names. +- :gh:`275`, [Linux]: ``Process.io_counters()`` erroneously raise + :exc:`NoSuchProcess` on old Linux versions. Where not available it now raises + ``NotImplementedError``. +- :gh:`286`: :meth:`Process.is_running()` doesn't actually check whether PID + has been reused. +- :gh:`314`: :meth:`Process.children()` can sometimes return non-children. + +**API changes** + +- ``Process.nice`` property is deprecated in favor of new ``get_nice()`` and + ``set_nice()`` methods. +- ``psutil.get_process_list()`` is deprecated. +- :meth:`Process.ppid()`, :meth:`Process.name()`, :meth:`Process.exe()`, + :meth:`Process.cmdline()` and :meth:`Process.create_time()` properties of + :class:`Process` class are now cached after being accessed, meaning + :exc:`NoSuchProcess` will no longer be raised in case the process is gone in + the meantime. +- ``psutil.STATUS_*`` constants can now be compared by using their string + representation. + +0.4.1 — 2011-12-14 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`228`: some example scripts were not working with Python 3. +- :gh:`230`, [Windows], [macOS]: fix memory leak in + :meth:`Process.connections()`. +- :gh:`232`, [Linux]: ``psutil.phymem_usage()`` can report erroneous values + which are different than ``free`` command. +- :gh:`236`, [Windows]: fix memory/handle leak in + :meth:`Process.memory_info()`, + :meth:`Process.suspend()` and :meth:`Process.resume()` methods. + +0.4.0 — 2011-10-29 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`150`: network I/O counters (:func:`net_io_counters()`). (macOS and + Windows patch by Jeremy Whitlock) +- :gh:`154`, [FreeBSD]: add support for :meth:`Process.cwd()`. +- :gh:`157`, [Windows]: provide installer for Python 3.2 64-bit. +- :gh:`198`: :meth:`Process.wait()` with ``timeout=0`` can now be used to make + the function return immediately. +- :gh:`206`: disk I/O counters (:func:`disk_io_counters()`). (macOS and Windows + patch by Jeremy Whitlock) +- :gh:`213`: add `iotop.py`_ script. +- :gh:`217`: :meth:`Process.connections()` now has a ``kind`` argument to + filter for connections with different criteria. +- :gh:`221`, [FreeBSD]: :meth:`Process.open_files()` has been rewritten in C + and no longer relies on ``lsof``. +- :gh:`223`: add `top.py`_ script. +- :gh:`227`: add `nettop.py`_ script. + +**Bug fixes** + +- :gh:`135`, [macOS]: psutil cannot create :class:`Process` object. +- :gh:`144`, [Linux]: no longer support 0 special PID. +- :gh:`188`, [Linux]: psutil import error on Linux ARM architectures. +- :gh:`194`, [POSIX]: :meth:`Process.cpu_percent()` now reports a percentage + over 100 on multicore processors. +- :gh:`197`, [Linux]: :meth:`Process.connections()` is broken on platforms not + supporting IPv6. +- :gh:`200`, [Linux], **[critical]**: ``psutil.NUM_CPUS`` not working on armel + and sparc architectures and causing crash on module import. +- :gh:`201`, [Linux]: :meth:`Process.connections()` is broken on big-endian + architectures. +- :gh:`211`: :class:`Process` instance can unexpectedly raise + :exc:`NoSuchProcess` if tested for equality with another object. +- :gh:`218`, [Linux], **[critical]**: crash at import time on Debian 64-bit + because of a missing line in ``/proc/meminfo``. +- :gh:`226`, [FreeBSD], **[critical]**: crash at import time on FreeBSD 7 and + minor. + +0.3.0 — 2011-07-08 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`125`: system per-cpu percentage utilization and times + (:meth:`Process.cpu_times()`, + :meth:`Process.cpu_percent()`). +- :gh:`163`: per-process associated terminal / TTY + (:meth:`Process.terminal()`). +- :gh:`171`: added ``get_phymem()`` and ``get_virtmem()`` functions returning + system memory information (``total``, ``used``, ``free``) and memory percent + usage. ``total_*``, ``avail_*`` and ``used_*`` memory functions are + deprecated. +- :gh:`172`: disk usage statistics (:func:`disk_usage()`). +- :gh:`174`: mounted disk partitions (:func:`disk_partitions()`). +- :gh:`179`: setuptools is now used in setup.py + +**Bug fixes** + +- :gh:`159`, [Windows]: ``SetSeDebug()`` does not close handles or unset + impersonation on return. +- :gh:`164`, [Windows]: wait function raises a ``TimeoutException`` when a + process returns ``-1``. +- :gh:`165`: :meth:`Process.status()` raises an unhandled exception. +- :gh:`166`: :meth:`Process.memory_info()` leaks handles hogging system + resources. +- :gh:`168`: :func:`cpu_percent()` returns erroneous results when used in + non-blocking mode. (patch by Philip Roberts) +- :gh:`178`, [macOS]: :meth:`Process.threads()` leaks memory. +- :gh:`180`, [Windows]: :meth:`Process.num_threads()` and + :meth:`Process.threads()` methods can raise :exc:`NoSuchProcess` exception + while process still exists. + +0.2.1 — 2011-03-20 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`64`: per-process I/O counters (:meth:`Process.io_counters()`). +- :gh:`116`: per-process :meth:`Process.wait()` (wait for process to terminate + and return its exit code). +- :gh:`134`: per-process threads (:meth:`Process.threads()`). +- :gh:`136`: :meth:`Process.exe()` path on FreeBSD is now determined by asking + the kernel instead of guessing it from cmdline[0]. +- :gh:`137`: per-process real, effective and saved user and group ids + (:meth:`Process.gids()`). +- :gh:`140`: system boot time (:func:`boot_time()`). +- :gh:`142`: per-process get and set niceness (priority) + (:meth:`Process.nice()`). +- :gh:`143`: per-process status (:meth:`Process.status()`). +- :gh:`147` [Linux]: per-process I/O niceness / priority + (:meth:`Process.ionice()`). +- :gh:`148`: :class:`Popen` class which tidies up ``subprocess.Popen`` and + :class:`Process` class in a single interface. +- :gh:`152`, [macOS]: :meth:`Process.open_files()` implementation has been + rewritten in C and no longer relies on ``lsof`` resulting in a 3x speedup. +- :gh:`153`, [macOS]: :meth:`Process.connections()` implementation has been + rewritten in C and no longer relies on ``lsof`` resulting in a 3x speedup. + +**Bug fixes** + +- :gh:`83`, [macOS]: :meth:`Process.cmdline()` is empty on macOS 64-bit. +- :gh:`130`, [Linux]: a race condition can cause ``IOError`` exception be + raised on if process disappears between ``open()`` and the subsequent + ``read()`` call. +- :gh:`145`, [Windows], **[critical]**: ``WindowsError`` was raised instead of + :exc:`AccessDenied` when using :meth:`Process.resume()` or + :meth:`Process.suspend()`. +- :gh:`146`, [Linux]: :meth:`Process.exe()` property can raise ``TypeError`` if + path contains NULL bytes. +- :gh:`151`, [Linux]: :meth:`Process.exe()` and :meth:`Process.cwd()` for PID 0 + return inconsistent data. + +**API changes** + +- :class:`Process` ``uid`` and ``gid`` properties are deprecated in favor of + ``uids`` and ``gids`` properties. + +0.2.0 — 2010-11-13 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`79`: per-process open files (:meth:`Process.open_files()`). +- :gh:`88`: total system physical cached memory. +- :gh:`88`: total system physical memory buffers used by the kernel. +- :gh:`91`: add :meth:`Process.send_signal()` and :meth:`Process.terminate()` + methods. +- :gh:`95`: :exc:`NoSuchProcess` and :exc:`AccessDenied` exception classes now + provide ``pid``, ``name`` and ``msg`` attributes. +- :gh:`97`: per-process children (:meth:`Process.children()`). +- :gh:`98`: :meth:`Process.cpu_times()` and :meth:`Process.memory_info()` now + return a namedtuple instead of a tuple. +- :gh:`103`: per-process opened TCP and UDP connections + (:meth:`Process.connections()`). +- :gh:`107`, [Windows]: add support for Windows 64 bit. (patch by cjgohlke) +- :gh:`111`: per-process executable name (:meth:`Process.exe()`). +- :gh:`113`: exception messages now include :meth:`Process.name()` and + :attr:`Process.pid`. +- :gh:`114`, [Windows]: :meth:`Process.username()` has been rewritten in pure C + and no longer uses WMI resulting in a big speedup. Also, pywin32 is no longer + required as a third-party dependency. (patch by wj32) +- :gh:`117`, [Windows]: added support for Windows 2000. +- :gh:`123`: :func:`cpu_percent()` and :meth:`Process.cpu_percent()` accept a + new ``interval`` parameter. +- :gh:`129`: per-process threads (:meth:`Process.threads()`). + +**Bug fixes** + +- :gh:`80`: fixed warnings when installing psutil with easy_install. +- :gh:`81`, [Windows]: psutil fails to compile with Visual Studio. +- :gh:`94`: :meth:`Process.suspend()` raises ``OSError`` instead of + :exc:`AccessDenied`. +- :gh:`86`, [FreeBSD]: psutil didn't compile against FreeBSD 6.x. +- :gh:`102`, [Windows]: orphaned process handles obtained by using + ``OpenProcess`` in C were left behind every time :class:`Process` class was + instantiated. +- :gh:`111`, [POSIX]: ``path`` and ``name`` :class:`Process` properties report + truncated or erroneous values on POSIX. +- :gh:`120`, [macOS]: :func:`cpu_percent()` always returning 100%. +- :gh:`112`: ``uid`` and ``gid`` properties don't change if process changes + effective user/group id at some point. +- :gh:`126`: :meth:`Process.ppid()`, :meth:`Process.uids()`, + :meth:`Process.gids()`, + :meth:`Process.name()`, + :meth:`Process.exe()`, :meth:`Process.cmdline()` and + :meth:`Process.create_time()` properties are no longer cached and correctly + raise :exc:`NoSuchProcess` exception if the process disappears. + +**API changes** + +- ``psutil.Process.path`` property is deprecated and works as an alias for + ``psutil.Process.exe`` property. +- :meth:`Process.kill()`: signal argument was removed - to send a signal to the + process use :meth:`Process.send_signal()` method instead. +- :meth:`Process.memory_info()` returns a nametuple instead of a tuple. +- :func:`cpu_times()` returns a nametuple instead of a tuple. +- New :class:`Process` methods: :meth:`Process.open_files()`, + :meth:`Process.connections()`, + :meth:`Process.send_signal()` and :meth:`Process.terminate()`. +- :meth:`Process.ppid()`, :meth:`Process.uids()`, :meth:`Process.gids()`, + :meth:`Process.name()`, + :meth:`Process.exe()`, :meth:`Process.cmdline()` and + :meth:`Process.create_time()` properties are no longer cached and raise + :exc:`NoSuchProcess` exception if process disappears. +- :func:`cpu_percent()` no longer returns immediately (see issue 123). +- :meth:`Process.cpu_percent()` and :func:`cpu_percent()` no longer returns + immediately by default (see issue :gh:`123`). + +0.1.3 — 2010-03-02 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`14`: :meth:`Process.username()`. +- :gh:`51`, [Linux], [Windows]: per-process current working directory + (:meth:`Process.cwd()`). +- :gh:`59`: :meth:`Process.is_running()` is now 10 times faster. +- :gh:`61`, [FreeBSD]: added supoprt for FreeBSD 64 bit. +- :gh:`71`: per-process suspend and resume (:meth:`Process.suspend()` and + :meth:`Process.resume()`). +- :gh:`75`: Python 3 support. + +**Bug fixes** + +- :gh:`36`: :meth:`Process.cpu_times()` and :meth:`Process.memory_info()` + functions succeeded. also for dead processes while a :exc:`NoSuchProcess` + exception is supposed to be raised. +- :gh:`48`, [FreeBSD]: incorrect size for MIB array defined in ``getcmdargs``. +- :gh:`49`, [FreeBSD]: possible memory leak due to missing ``free()`` on error + condition in ``getcmdpath()``. +- :gh:`50`, [BSD]: fixed ``getcmdargs()`` memory fragmentation. +- :gh:`55`, [Windows]: ``test_pid_4`` was failing on Windows Vista. +- :gh:`57`: some unit tests were failing on systems where no swap memory is + available. +- :gh:`58`: :meth:`Process.is_running()` is now called before + :meth:`Process.kill()` to make sure we are going to kill the correct process. +- :gh:`73`, [macOS]: virtual memory size reported on includes shared library + size. +- :gh:`77`: :exc:`NoSuchProcess` wasn't raised on :meth:`Process.create_time()` + if + :meth:`Process.kill()` was used first. + +0.1.2 — 2009-05-06 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`32`: Per-process CPU user/kernel times (:meth:`Process.cpu_times()`). +- :gh:`33`: Per-process create time (:meth:`Process.create_time()`). +- :gh:`34`: Per-process CPU utilization percentage + (:meth:`Process.cpu_percent()`). +- :gh:`38`: Per-process memory usage (bytes) (:meth:`Process.memory_info()`). +- :gh:`41`: Per-process memory percent (:meth:`Process.memory_percent()`). +- :gh:`39`: System uptime (:func:`boot_time()`). +- :gh:`43`: Total system virtual memory. +- :gh:`46`: Total system physical memory. +- :gh:`44`: Total system used/free virtual and physical memory. + +**Bug fixes** + +- :gh:`36`, [Windows]: :exc:`NoSuchProcess` not raised when accessing timing + methods. +- :gh:`40`, [FreeBSD], [macOS]: fix ``test_get_cpu_times`` failures. +- :gh:`42`, [Windows]: :meth:`Process.memory_percent()` raises + :exc:`AccessDenied`. + +0.1.1 — 2009-03-06 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`4`, [FreeBSD]: support for all functions of psutil. +- :gh:`9`, [macOS], [Windows]: add ``Process.uid`` and ``Process.gid``, + returning process UID and GID. +- :gh:`11`: per-process parent object: :meth:`Process.parent()` property + returns a + :class:`Process` object representing the parent process, and + :meth:`Process.ppid()` returns the parent PID. +- :gh:`12`, :gh:`15`: + :exc:`NoSuchProcess` exception now raised when creating an object for a + nonexistent process, or when retrieving information about a process that + has gone away. +- :gh:`21`, [Windows]: :exc:`AccessDenied` exception created for raising access + denied errors from ``OSError`` or ``WindowsError`` on individual platforms. +- :gh:`26`: :func:`process_iter()` function to iterate over processes as + :class:`Process` objects with a generator. +- :class:`Process` objects can now also be compared with == operator for + equality (PID, name, command line are compared). + +**Bug fixes** + +- :gh:`16`, [Windows]: Special case for "System Idle Process" (PID 0) which + otherwise would return an "invalid parameter" exception. +- :gh:`17`: get_process_list() ignores :exc:`NoSuchProcess` and + :exc:`AccessDenied` exceptions during building of the list. +- :gh:`22`, [Windows]: :meth:`Process.kill()` for PID 0 was failing with an + unset exception. +- :gh:`23`, [Linux], [macOS]: create special case for :func:`pid_exists()` with + PID 0. +- :gh:`24`, [Windows], **[critical]**: :meth:`Process.kill()` for PID 0 now + raises + :exc:`AccessDenied` exception instead of ``WindowsError``. +- :gh:`30`: psutil.get_pid_list() was returning two 0 PIDs. + + +.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py +.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py +.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py +.. _`pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py +.. _`top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py + +.. _`psutil/_ntuples.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_ntuples.py diff --git a/docs/conf.py b/docs/conf.py index fd0fe294aa..3d12521509 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,31 +2,17 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -# psutil documentation build configuration file, created by -# sphinx-quickstart on Wed Oct 19 21:54:30 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +"""Sphinx config file.""" -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ +# See doc at: +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output import ast import datetime import os +import sys PROJECT_NAME = "psutil" AUTHOR = "Giampaolo Rodola" @@ -50,326 +36,50 @@ def get_version(): VERSION = get_version() -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. +sys.path.insert(0, os.path.join(HERE, '_ext')) + extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.imgmath', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.extlinks", + "sphinx.ext.imgmath", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + # our own custom extensions in _ext/ dir + "add_home_link", + "changelog_anchors", ] -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -# -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. +# project info project = PROJECT_NAME copyright = f"2009-{THIS_YEAR}, {AUTHOR}" author = AUTHOR - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. version = VERSION -# The full version, including alpha/beta/rc tags. release = VERSION -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "eng" - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# -# today = '' -# -# Else, today_fmt is used as the format for a strftime call. -# -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'DEVGUIDE.rst'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# -# add_module_names = True +extlinks = { + 'gh': ('https://github.com/giampaolo/psutil/issues/%s', '#%s'), +} -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# -# show_authors = False +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' -# The name of the Pygments (syntax highlighting) style to use. +language = "eng" +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -# -# html_title = u'psutil v1.0' - -# A shorter title for the navigation bar. Default is the same as html_title. -# -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# -# html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or -# 32x32 pixels large. - - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# -# html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -# -# html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# -# html_additional_pages = {} - -# If false, no module index is generated. -# -# html_domain_indices = True - -# If false, no index is generated. -# -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -# -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -# -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# -# html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. htmlhelp_basename = f"{PROJECT_NAME}-doc" -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'psutil.tex', 'psutil Documentation', AUTHOR, 'manual') ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# -# latex_use_parts = False - -# If true, show page references after internal links. -# -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# -# latex_appendices = [] - -# It false, will not define \strong, \code, itleref, \crossref ... but only -# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added -# packages. -# -# latex_keep_old_macro_names = True - -# If false, no module index is generated. -# -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, 'psutil', 'psutil Documentation', [author], 1)] - -# If true, show URL addresses after external links. -# -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [( - master_doc, - 'psutil', - 'psutil Documentation', - author, - 'psutil', - 'One line description of project.', - 'Miscellaneous', -)] - -# Documents to append as an appendix to all manuals. -# -# texinfo_appendices = [] - -# If false, no module index is generated. -# -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# -# texinfo_no_detailmenu = False - - -html_static_path = ['_static'] - html_css_files = [ 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', diff --git a/docs/devguide.rst b/docs/devguide.rst index 7b24181f93..d89213bc5a 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -4,42 +4,41 @@ Development guide Build, setup and running tests ------------------------------ -psutil makes extensive use of C extension modules, meaning a C compiler is -required, see -`install instructions `__. -Once you have a compiler installed run: +- psutil makes extensive use of C extension modules, meaning a C compiler is + required, see :doc:`install instructions `. Once you have a compiler + installed run: -.. code-block:: bash + .. code-block:: bash - git clone git@github.com:giampaolo/psutil.git - make install-sysdeps # install gcc and python headers - make install-pydeps-test # install python deps necessary to run unit tests - make build - make install - make test + git clone git@github.com:giampaolo/psutil.git + make install-sysdeps # install gcc and python headers + make install-pydeps-test # install python deps necessary to run unit tests + make build + make install + make test - ``make`` (and the accompanying `Makefile`_) is the designated tool to build, - install, run tests and do pretty much anything that involves development. - useful commands are: - -.. code-block:: bash - - make clean # remove build files - make install-pydeps-dev # install dev deps (ruff, black, ...) - make test # run tests - make test-parallel # run tests in parallel (faster) - make test-memleaks # run memory leak tests - make test-coverage # run test coverage - make lint-all # run linters - make fix-all # fix linters errors - make uninstall - make help + install, run tests and do pretty much anything that involves development, + including on Windows. Some useful commands: + + .. code-block:: bash + + make clean # remove build files + make install-pydeps-dev # install dev deps (ruff, black, ...) + make test # run tests + make test-parallel # run tests in parallel (faster) + make test-memleaks # run memory leak tests + make test-coverage # run test coverage + make lint-all # run linters + make fix-all # fix linters errors + make uninstall + make help - To run a specific unit test: -.. code-block:: bash + .. code-block:: - make test ARGS=tests/test_system.py + make test ARGS=tests/test_system.py - Do not use ``sudo``. ``make install`` installs psutil as a limited user in "edit" / development mode, meaning you can edit psutil code on the fly while @@ -47,9 +46,9 @@ Once you have a compiler installed run: - If you want to target a specific Python version: -.. code-block:: bash + .. code-block:: - make test PYTHON=python3.8 + make test PYTHON=python3.8 Windows ------- @@ -60,10 +59,10 @@ Windows provides a Unix-like environment where ``make`` works. - Once inside Git Bash, you can run the usual ``make`` commands: -.. code-block:: bash + .. code-block:: bash - make build - make test-parallel + make build + make test-parallel Debug mode ---------- @@ -110,12 +109,16 @@ Code organization .. code-block:: bash - psutil/__init__.py # main psutil namespace ("import psutil") - psutil/_ps{platform}.py # platform-specific python wrapper - psutil/_psutil_{platform}.c # platform-specific C extension - psutil/arch/{platform}/*.c # platform-specific C extension - tests/test_process|system.py # main test suite - tests/test_{platform}.py # platform-specific test suite + psutil/__init__.py # Main API namespace ("import psutil") + psutil/_common.py # Generic utilities + psutil/_ntuples.py # Named tuples returned by psutil APIs + psutil/_enums.py # Enum containers backing psutil constants + psutil/_ps{platform}.py # Platform-specific python wrappers + psutil/_psutil_{platform}.c # Platform-specific C extensions (entry point) + psutil/arch/all/*.c # C code common to all platforms + psutil/arch/{platform}/*.c # Platform-specific C extension + tests/test_process|system.py # Main system/process API tests + tests/test_{platform}.py # Platform-specific tests Adding a new API ---------------- @@ -133,8 +136,8 @@ Typically, this is what you do: ``tests/test_{platform}.py`` (e.g. `tests/test_linux.py`_). This usually means testing the return value of the new API against a system CLI tool. -- Update the doc in ``docs/index.py``. -- Update `HISTORY.rst`_ and `CREDITS`_ files. +- Update the doc in ``docs/api.rst``. +- Update `changelog.rst`_ and `CREDITS`_ files. - Make a pull request. Make a pull request @@ -165,7 +168,7 @@ Documentation .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS .. _`Git for Windows`: ` -.. _`HISTORY.rst`: https://github.com/giampaolo/psutil/blob/master/HISTORY.rst +.. _`changelog.rst`: https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile .. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ .. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ diff --git a/docs/faq.rst b/docs/faq.rst index dedf288c63..88b447daaf 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -5,7 +5,7 @@ FAQs * Q: Why do I get :class:`AccessDenied` for certain processes? * A: This may happen when you query processes owned by another user, - especially on macOS (see issue `#883`_) and Windows. + especially on macOS (see issue :gh:`883`) and Windows. Unfortunately there's not much you can do about this except running the Python process with higher privileges. On Unix you may run the Python process as root or use the SUID bit diff --git a/docs/index.rst b/docs/index.rst index e917ea2c3d..63fa823ae9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -104,3 +104,8 @@ Table of Contents Development guide FAQs Timeline + +.. toctree:: + :titlesonly: + + Changelog diff --git a/docs/timeline.rst b/docs/timeline.rst index 8cf25b9d6b..33ecb959af 100644 --- a/docs/timeline.rst +++ b/docs/timeline.rst @@ -3,413 +3,413 @@ Timeline - 2026-02-08: `7.2.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2026-01-28: `7.2.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-12-29: `7.2.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-12-23: `7.2.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-11-02: `7.1.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-10-25: `7.1.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-10-19: `7.1.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-09-17: `7.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2025-02-13: `7.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2024-12-19: `6.1.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2024-10-17: `6.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2024-06-18: `6.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2024-01-19: `5.9.8 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2023-12-17: `5.9.7 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2023-10-15: `5.9.6 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2023-04-17: `5.9.5 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2022-11-07: `5.9.4 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2022-10-18: `5.9.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2022-09-04: `5.9.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2022-05-20: `5.9.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2021-12-29: `5.9.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2020-12-19: `5.8.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2020-10-24: `5.7.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2020-07-15: `5.7.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2020-07-15: `5.7.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2020-02-18: `5.7.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-11-26: `5.6.7 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-11-25: `5.6.6 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-11-06: `5.6.5 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-11-04: `5.6.4 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-06-11: `5.6.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-04-26: `5.6.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-03-11: `5.6.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-03-05: `5.6.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-02-15: `5.5.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2019-01-23: `5.5.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2018-10-30: `5.4.8 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2018-08-14: `5.4.7 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2018-06-07: `5.4.6 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2018-04-13: `5.4.5 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2018-04-13: `5.4.4 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2018-01-01: `5.4.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-12-07: `5.4.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-11-08: `5.4.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-10-12: `5.4.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-09-10: `5.3.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-09-01: `5.3.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-04-10: `5.2.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-03-24: `5.2.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-03-05: `5.2.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-02-07: `5.1.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-02-03: `5.1.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-02-03: `5.1.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2017-02-01: `5.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-12-21: `5.0.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-11-06: `5.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-10-05: `4.4.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-10-25: `4.4.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-10-23: `4.4.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-09-01: `4.3.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-06-18: `4.3.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-05-14: `4.2.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-03-12: `4.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-02-17: `4.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-01-20: `3.4.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2016-01-15: `3.4.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-11-25: `3.3.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-10-04: `3.2.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-09-03: `3.2.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-09-02: `3.2.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-07-15: `3.1.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-07-15: `3.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-06-18: `3.0.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-06-13: `3.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-02-02: `2.2.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2015-01-06: `2.2.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2014-09-26: `2.1.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2014-09-21: `2.1.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2014-04-30: `2.1.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2014-04-08: `2.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2014-03-10: `2.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-11-25: `1.2.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-11-20: `1.2.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-10-22: `1.1.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-10-08: `1.1.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-09-28: `1.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-07-12: `1.0.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-07-10: `1.0.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-05-03: `0.7.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2013-04-12: `0.7.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2012-08-16: `0.6.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2012-08-13: `0.6.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2012-06-29: `0.5.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2012-06-27: `0.5.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2011-12-14: `0.4.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2011-10-29: `0.4.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2011-07-08: `0.3.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2011-03-20: `0.2.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2010-11-13: `0.2.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2010-03-02: `0.1.3 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2009-05-06: `0.1.2 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2009-03-06: `0.1.1 `__ - - `what's new `__ - + `what's new `__ - `diff `__ - 2009-01-27: `0.1.0 `__ - - `what's new `__ - + `what's new `__ - `diff `__ diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index f909607487..ae1fed8daa 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -16,13 +16,12 @@ =========== - `Home page `_ -- `Install `_ - `Documentation `_ - `Download `_ - `Forum `_ - `StackOverflow `_ - `Blog `_ -- `What's new `_ +- `What's new `_ """ diff --git a/scripts/internal/find_broken_links.py b/scripts/internal/find_broken_links.py index f4333b5488..142d7d9655 100755 --- a/scripts/internal/find_broken_links.py +++ b/scripts/internal/find_broken_links.py @@ -97,15 +97,7 @@ def parse_rst(fname): """Look for links in a .rst file.""" with open(fname) as f: text = f.read() - urls = find_urls(text) - # HISTORY file has a lot of dead links. - if fname == 'HISTORY.rst' and urls: - urls = [ - x - for x in urls - if not x.startswith('https://github.com/giampaolo/psutil/issues') - ] - return urls + return find_urls(text) def parse_py(fname): diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index cd984de7c3..5fa98b669e 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -4,7 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Prints release announce based on HISTORY.rst file content. +"""Prints release announce based on docs/changelog.rst file content. See: https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode. """ @@ -18,7 +18,7 @@ HERE = os.path.abspath(os.path.dirname(__file__)) ROOT = os.path.realpath(os.path.join(HERE, '..', '..')) -HISTORY = os.path.join(ROOT, 'HISTORY.rst') +CHANGELOG = os.path.join(ROOT, 'docs', 'changelog.rst') PRINT_HASHES_SCRIPT = os.path.join( ROOT, 'scripts', 'internal', 'print_hashes.py' ) @@ -28,9 +28,7 @@ PRJ_URL_HOME = 'https://github.com/giampaolo/psutil' PRJ_URL_DOC = 'http://psutil.readthedocs.io' PRJ_URL_DOWNLOAD = 'https://pypi.org/project/psutil/#files' -PRJ_URL_WHATSNEW = ( - 'https://github.com/giampaolo/psutil/blob/master/HISTORY.rst' -) +PRJ_URL_WHATSNEW = 'https://psutil.readthedocs.io/en/latest/changelog.html' template = """\ Hello all, @@ -74,11 +72,27 @@ """ +def rst_to_text(s): + """Strip RST/Sphinx markup, returning plain text.""" + # :gh:`123` -> #123 + s = re.sub(r':gh:`(\d+)`', r'#\1', s) + # :meth:, :func:, :class:, :attr:, :exc:, :mod:, :data:, etc. + # :role:`text` -> text (also handles :role:`~text` and :role:`mod.text`) + s = re.sub(r':[a-z]+:`~?([^`]+)`', r'\1', s) + # ``code`` -> `code` + s = re.sub(r'``([^`]+)``', r'`\1`', s) + # **bold** -> bold + s = re.sub(r'\*\*([^*]+)\*\*', r'\1', s) + # *italic* -> italic + s = re.sub(r'\*([^*]+)\*', r'\1', s) + return s + + def get_changes(): """Get the most recent changes for this release by parsing - HISTORY.rst file. + docs/changelog.rst file. """ - with open(HISTORY) as f: + with open(CHANGELOG) as f: lines = f.readlines() block = [] @@ -86,7 +100,7 @@ def get_changes(): # eliminate the part preceding the first block while lines: line = lines.pop(0) - if line.startswith('===='): + if line.startswith('^^^^'): break else: raise ValueError("something wrong") @@ -98,7 +112,7 @@ def get_changes(): if re.match(r"^- \d+_", line): line = re.sub(r"^- (\d+)_", r"- #\1", line) - if line.startswith('===='): + if line.startswith('^^^^'): break block.append(line) else: @@ -109,7 +123,9 @@ def get_changes(): while not block[-1]: block.pop(-1) - return "\n".join(block) + text = "\n".join(block) + text = rst_to_text(text) + return text def main(): diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py deleted file mode 100755 index 54bfefe1d4..0000000000 --- a/scripts/internal/print_timeline.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/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. - -"""Prints releases' timeline in RST format.""" - -import shlex -import subprocess - -entry = """\ -- {date}: - `{ver} `__ - - `what's new `__ - - `diff `__""" # noqa: E501 - - -def sh(cmd): - return subprocess.check_output( - shlex.split(cmd), universal_newlines=True - ).strip() - - -def get_tag_date(tag): - out = sh(f"git log -1 --format=%ai {tag}") - return out.split(' ')[0] - - -def main(): - releases = [] - out = sh("git tag") - for line in out.split('\n'): - tag = line.split(' ')[0] - ver = tag.replace('release-', '') - nodotver = ver.replace('.', '') - date = get_tag_date(tag) - releases.append((tag, ver, nodotver, date)) - releases.sort(reverse=True) - - for i, rel in enumerate(releases): - tag, ver, nodotver, date = rel - try: - prevtag = releases[i + 1][0] - except IndexError: - # get first commit - prevtag = sh("git rev-list --max-parents=0 HEAD") - print(entry.format(**locals())) - - -if __name__ == '__main__': - main() From 785a56d9b1f2f61abdd1d3b891af22ad0d9b2d82 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 11 Mar 2026 22:38:11 +0100 Subject: [PATCH 1578/1714] Doc: extend recipes, add COPY button, check py code ext (#2761) - Added tons of new examples in `docs/recipes.rst`. - Added `sphinx_copybutton` integration (show a COPY button to copy code snippets) - Added **`check_python_syntax.py`** Sphinx extension to validate Python code blocks in the documentation at build time. - Refactored API docs references to remove redundant `psutil.` prefixes in `:meth:` and `:func:` roles for consistency. - Updated historical release notes. --- MANIFEST.in | 1 + docs/_ext/check_python_syntax.py | 44 +++ docs/api.rst | 67 ++-- docs/changelog.rst | 18 +- docs/conf.py | 4 + docs/platform.rst | 17 +- docs/recipes.rst | 615 ++++++++++++++++++++++++++++--- docs/requirements.txt | 1 + 8 files changed, 659 insertions(+), 108 deletions(-) create mode 100644 docs/_ext/check_python_syntax.py diff --git a/MANIFEST.in b/MANIFEST.in index 8b26f85e4b..88c59b1853 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,6 +16,7 @@ include docs/Makefile include docs/README include docs/_ext/add_home_link.py include docs/_ext/changelog_anchors.py +include docs/_ext/check_python_syntax.py include docs/_links.rst include docs/_static/copybutton.js include docs/_static/css/custom.css diff --git a/docs/_ext/check_python_syntax.py b/docs/_ext/check_python_syntax.py new file mode 100644 index 0000000000..5c1bec7ef3 --- /dev/null +++ b/docs/_ext/check_python_syntax.py @@ -0,0 +1,44 @@ +# 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. + +"""Sphinx extension that checks the Python syntax of code blocks in the +documentation. This script gets called on `make html`. +""" + +import ast + +import docutils.nodes +import sphinx.errors + + +def check_python_blocks(app, doctree, docname): + path = app.env.doc2path(docname) + + for node in doctree.traverse(docutils.nodes.literal_block): + lang = node.get("language") + if lang not in {"python", "py"}: + continue + + code = node.astext() + + # skip empty blocks + if not code.strip(): + continue + + # skip REPL examples containing >>> + if ">>>" in code: + continue + + try: + ast.parse(code) + except SyntaxError as err: + lineno = node.line or "?" + msg = ( + f"invalid Python syntax in {path}:{lineno}:\n\n{code}\n\n{err}" + ) + raise sphinx.errors.SphinxError(msg) from None + + +def setup(app): + app.connect("doctree-resolved", check_python_blocks) diff --git a/docs/api.rst b/docs/api.rst index d52af0fc5e..83058810ed 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2518,7 +2518,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ProcessStatus `enum.StrEnum`_ collection of :data:`STATUS_* ` - constants. Returned by :meth:`psutil.Process.status()`. + constants. Returned by :meth:`Process.status()`. .. versionadded:: 8.0.0 @@ -2526,7 +2526,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over `enum.IntEnum`_ collection of :data:`*_PRIORITY_CLASS ` constants for - :meth:`psutil.Process.nice` on Windows. + :meth:`Process.nice` on Windows. Availability: Windows @@ -2535,7 +2535,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ProcessIOPriority `enum.IntEnum`_ collection of I/O priority constants for - :meth:`psutil.Process.ionice`. On Linux: ``IOPRIO_CLASS_*`` constants. + :meth:`Process.ionice`. On Linux: ``IOPRIO_CLASS_*`` constants. On Windows: ``IOPRIO_*`` constants. Availability: Linux, Windows @@ -2545,7 +2545,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ProcessRlimit `enum.IntEnum`_ collection of :data:`RLIMIT_* ` - constants for :meth:`psutil.Process.rlimit`. + constants for :meth:`Process.rlimit`. Availability: Linux, FreeBSD @@ -2555,7 +2555,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over `enum.StrEnum`_ collection of :data:`CONN_* ` constants. Returned in the *status* field of - :func:`psutil.net_connections` and :meth:`psutil.Process.net_connections`. + :func:`psutil.net_connections` and :meth:`Process.net_connections`. .. versionadded:: 8.0.0 @@ -2601,25 +2601,6 @@ Operating system constants .. warning:: deprecated in version 5.4.7; use :const:`MACOS` instead. -.. _const-procfs_path: -.. data:: PROCFS_PATH - - The path of the /proc filesystem on Linux, Solaris and AIX (defaults to - ``"/proc"``). - You may want to re-set this constant right after importing psutil in case - your /proc filesystem is mounted elsewhere or if you want to retrieve - information about Linux containers such as Docker, Heroku or LXC (see - `here `__ - for more info). - It must be noted that this trick works only for APIs which rely on /proc - filesystem (e.g. `memory`_ APIs and most :class:`Process` class methods). - - Availability: Linux, Solaris, AIX - - .. versionadded:: 3.2.3 - .. versionchanged:: 3.4.2 also available on Solaris. - .. versionchanged:: 5.4.0 also available on AIX. - Process status constants ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2639,7 +2620,7 @@ Process status constants .. data:: STATUS_WAITING (FreeBSD) .. data:: STATUS_SUSPENDED (NetBSD) - Represent a process status. Returned by :meth:`psutil.Process.status()`. + Represent a process status. Returned by :meth:`Process.status()`. These constants are members of the :class:`psutil.ProcessStatus` enum. .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) @@ -2659,7 +2640,7 @@ Process priority constants .. data:: BELOW_NORMAL_PRIORITY_CLASS Represent the priority of a process on Windows (see `SetPriorityClass`_). - They can be used in conjunction with :meth:`psutil.Process.nice()` to get or + They can be used in conjunction with :meth:`Process.nice()` to get or set process priority. These constants are members of the :class:`psutil.ProcessPriority` enum. @@ -2675,7 +2656,7 @@ Process priority constants .. data:: IOPRIO_CLASS_IDLE A set of integers representing the I/O priority of a process on Linux. They - can be used in conjunction with :meth:`psutil.Process.ionice()` to get or set + can be used in conjunction with :meth:`Process.ionice()` to get or set process I/O priority. These constants are members of the :class:`psutil.ProcessIOPriority` enum. @@ -2701,7 +2682,7 @@ Process priority constants .. data:: IOPRIO_HIGH A set of integers representing the I/O priority of a process on Windows. - They can be used in conjunction with :meth:`psutil.Process.ionice()` to get + They can be used in conjunction with :meth:`Process.ionice()` to get or set process I/O priority. These constants are members of the :class:`psutil.ProcessIOPriority` enum. @@ -2713,8 +2694,8 @@ Process priority constants :class:`psutil.ProcessIOPriority` enum members (previously ``IOPriority`` enum). -Process resources constants -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Process resource constants +^^^^^^^^^^^^^^^^^^^^^^^^^^ Linux / FreeBSD: @@ -2746,7 +2727,7 @@ FreeBSD specific: .. data:: RLIMIT_NPTS Constants used for getting and setting process resource limits to be used in -conjunction with :meth:`psutil.Process.rlimit()`. See `resource.getrlimit`_ +conjunction with :meth:`Process.rlimit()`. See `resource.getrlimit`_ for further information. These constants are members of the :class:`psutil.ProcessRlimit` enum. @@ -2778,7 +2759,7 @@ Connections constants .. data:: CONN_BOUND (Solaris) A set of strings representing the status of a TCP connection. - Returned by :meth:`psutil.Process.net_connections()` and + Returned by :meth:`Process.net_connections()` and :func:`psutil.net_connections` (`status` field). These constants are members of the :class:`psutil.ConnectionStatus` enum. @@ -2819,6 +2800,28 @@ Hardware constants .. versionadded:: 5.1.0 +Other constants +^^^^^^^^^^^^^^^ + +.. _const-procfs_path: +.. data:: PROCFS_PATH + + The path of the /proc filesystem on Linux, Solaris and AIX (defaults to + ``"/proc"``). + You may want to re-set this constant right after importing psutil in case + your /proc filesystem is mounted elsewhere or if you want to retrieve + information about Linux containers such as Docker, Heroku or LXC (see + `here `__ + for more info). + It must be noted that this trick works only for APIs which rely on /proc + filesystem (e.g. `memory`_ APIs and most :class:`Process` class methods). + + Availability: Linux, Solaris, AIX + + .. versionadded:: 3.2.3 + .. versionchanged:: 3.4.2 also available on Solaris. + .. versionchanged:: 5.4.0 also available on AIX. + .. _const-version-info: .. data:: version_info diff --git a/docs/changelog.rst b/docs/changelog.rst index 141dfea6ad..27877f98b5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,18 @@ Changelog **Enhancements** +- :gh:`2761`, :gh:`2757`, :gh:`2760`: Tons of doc improvements: + - Split the documentation from a single-page HTML document into multiple + sub-sections. Sections now include separate pages for API reference, + installation, release timeline, FAQs, and more. + - Moved 18 years old HISTORY.rst into docs/changelog.rst for better + integration. Original file remains in the root with a note pointing to the + new location. + - Added tons of new examples in docs/recipes.rst. + - Show a clickable COPY button to copy code snippets. + - Show ``psutil.`` prefix for all APIs. + - Use sphinx extension to validate Python code snippets syntax at build-time. + - :gh:`1946`: add inline type hints to all public APIs in `psutil/__init__.py`. Type checkers (mypy, pyright, etc.) can now statically verify code that uses psutil. No runtime behavior is changed; the annotations are purely @@ -68,12 +80,6 @@ Changelog for the corresponding enum members. - :gh:`2754`: standardize :func:`sensors_battery()`'s `percent` so that it returns a `float` instead of `int` on all systems, not only Linux. -- :gh:`2757`: split the documentation from a single-page HTML document into - multiple sub-sections. Sections now include separate pages for API reference, - installation, release timeline, FAQs, and more. -- :gh:`2760`: moved 18 years old HISTORY.rst into docs/changelog.rst for - better integration. Original file remains in the root with a note pointing to - the new location. **Bug fixes** diff --git a/docs/conf.py b/docs/conf.py index 3d12521509..ae65f7b84a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,9 +46,11 @@ def get_version(): "sphinx.ext.imgmath", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", + "sphinx_copybutton", # our own custom extensions in _ext/ dir "add_home_link", "changelog_anchors", + "check_python_syntax", ] # project info @@ -76,6 +78,8 @@ def get_version(): html_static_path = ['_static'] htmlhelp_basename = f"{PROJECT_NAME}-doc" +copybutton_exclude = '.linenos, .gp' + latex_documents = [ (master_doc, 'psutil.tex', 'psutil Documentation', AUTHOR, 'manual') ] diff --git a/docs/platform.rst b/docs/platform.rst index f038cea0cb..a4e4b51f7e 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -2,14 +2,12 @@ Platform support ================ Python ------- +^^^^^^ -Supported Python versions: Python 3.7+ and PyPy. +**Current Python:** 3.7+ and PyPy. -Python 2.7 ----------- - -Latest psutil version supporting Python 2.7 is `psutil 6.1.1 `__. +**Python 2.7**: latest psutil version supporting Python 2.7 is +`psutil 6.1.1 `__. The 6.1.X serie may receive critical bug-fixes but no new features. It will be maintained in the dedicated `python2 `__ branch. @@ -23,10 +21,9 @@ Support history --------------- * psutil 8.0.0 (2026-XX): drop Python 3.6 -* psutil 7.2.0 (2025-12): publish Linux musl wheels -* psutil 7.1.2 (2025-10): publish free-threaded Python wheels -* psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (Linux and - Windows) +* psutil 7.2.0 (2025-12): publish wheels for **Linux musl** +* psutil 7.1.2 (2025-10): publish wheels for **free-threaded Python** +* psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (Linux and Windows) * psutil 7.1.1 (2025-10): drop **SunOS 10** * psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 diff --git a/docs/recipes.rst b/docs/recipes.rst index dc3f90bd97..9c10e982e3 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -3,23 +3,37 @@ Recipes ======= -Find process by name --------------------- +A collection of standalone, copy-paste solutions to specific problems. Each +recipe focuses on a single problem and provides a minimal solution which can be +adapted to real-world code. The examples are intentionally short and avoid +unnecessary abstractions so that the underlying psutil APIs are easy to +understand. Most of them are not meant to be used in production. -Check string against :meth:`Process.name()`: +.. contents:: + :local: + :depth: 3 + +Processes +--------- + +Finding processes +^^^^^^^^^^^^^^^^^ + +Find process by name: .. code-block:: python import psutil def find_procs_by_name(name): - "Return a list of processes matching 'name'." ls = [] - for p in psutil.process_iter(['name']): - if p.info['name'] == name: + for p in psutil.process_iter(["name"]): + if p.info["name"] == name: ls.append(p) return ls +---- + A bit more advanced, check string against :meth:`Process.name()`, :meth:`Process.exe()` and :meth:`Process.cmdline()`: @@ -28,50 +42,77 @@ A bit more advanced, check string against :meth:`Process.name()`, import os import psutil - def find_procs_by_name(name): - "Return a list of processes matching 'name'." + def find_procs_by_name_ex(name): ls = [] for p in psutil.process_iter(["name", "exe", "cmdline"]): - if name == p.info['name'] or \ - p.info['exe'] and os.path.basename(p.info['exe']) == name or \ - p.info['cmdline'] and p.info['cmdline'][0] == name: + if ( + name == p.info["name"] + or (p.info["exe"] and os.path.basename(p.info["exe"]) == name) + or (p.info["cmdline"] and p.info["cmdline"][0] == name) + ): ls.append(p) return ls -Kill process tree ------------------ +---- + +Find the process listening on a given TCP port: .. code-block:: python - import os - import signal import psutil - def kill_proc_tree(pid, sig=signal.SIGTERM, include_parent=True, - timeout=None, on_terminate=None): - """Kill a process tree (including grandchildren) with signal - "sig" and return a (gone, still_alive) tuple. - "on_terminate", if specified, is a callback function which is - called as soon as a child terminates. - """ - assert pid != os.getpid(), "won't kill myself" - parent = psutil.Process(pid) - children = parent.children(recursive=True) - if include_parent: - children.append(parent) - for p in children: + def find_proc_by_port(port): + for proc in psutil.process_iter(): try: - p.send_signal(sig) - except psutil.NoSuchProcess: + cons = proc.net_connections(kind="tcp") + except psutil.Error: pass - gone, alive = psutil.wait_procs(children, timeout=timeout, - callback=on_terminate) - return (gone, alive) + else: + for conn in cons: + if conn.laddr.port == port and conn.status == psutil.CONN_LISTEN: + return proc + return None -Filtering and sorting processes -------------------------------- +---- + +Find all processes that have an active connection to a given remote IP: + +.. code-block:: python + + import psutil + + def find_procs_by_remote_host(host): + ls = [] + for proc in psutil.process_iter(): + try: + cons = proc.net_connections(kind="inet") + except psutil.Error: + pass + else: + for conn in cons: + if conn.raddr and conn.raddr.ip == host: + ls.append(proc) + return ls + +---- + +Find all processes that have a given file open (useful on Windows): + +.. code-block:: python + + import psutil -A collection of code samples showing how to use :func:`process_iter()` to filter processes and sort them. + def find_procs_using_file(path): + ls = [] + for p in psutil.process_iter(["open_files"]): + for f in p.info["open_files"]: + if f.path == path: + ls.append(p) + break + return ls + +Filtering and sorting processes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Processes owned by user: @@ -80,79 +121,533 @@ Processes owned by user: >>> import getpass >>> import psutil >>> from pprint import pprint as pp - >>> pp([(p.pid, p.info['name']) for p in psutil.process_iter(['name', 'username']) if p.info['username'] == getpass.getuser()]) + >>> pp([(p.pid, p.info["name"]) for p in psutil.process_iter(["name", "username"]) if p.info["username"] == getpass.getuser()]) (16832, 'bash'), (19772, 'ssh'), (20492, 'python')] +---- + Processes actively running: .. code-block:: python - >>> pp([(p.pid, p.info) for p in psutil.process_iter(['name', 'status']) if p.info['status'] == psutil.STATUS_RUNNING]) + >>> pp([(p.pid, p.info) for p in psutil.process_iter(["name", "status"]) if p.info["status"] == psutil.STATUS_RUNNING]) [(1150, {'name': 'Xorg', 'status': }), (1776, {'name': 'unity-panel-service', 'status': }), (20492, {'name': 'python', 'status': })] +---- + Processes using log files: .. code-block:: python - >>> for p in psutil.process_iter(['name', 'open_files']): - ... for file in p.info['open_files'] or []: - ... if file.path.endswith('.log'): - ... print("%-5s %-10s %s" % (p.pid, p.info['name'][:10], file.path)) + >>> for p in psutil.process_iter(["name", "open_files"]): + ... for file in p.info["open_files"] or []: + ... if file.path.endswith(".log"): + ... print("{:<5} {:<10} {}".format(p.pid, p.info["name"][:10], file.path)) ... 1510 upstart /home/giampaolo/.cache/upstart/unity-settings-daemon.log 2174 nautilus /home/giampaolo/.local/share/gvfs-metadata/home-ce08efac.log 2650 chrome /home/giampaolo/.config/google-chrome/Default/data_reduction_proxy_leveldb/000003.log +---- + Processes consuming more than 500M of memory: .. code-block:: python - >>> pp([(p.pid, p.info['name'], p.info['memory_info'].rss) for p in psutil.process_iter(['name', 'memory_info']) if p.info['memory_info'].rss > 500 * 1024 * 1024]) + >>> pp([(p.pid, p.info["name"], p.info["memory_info"].rss) for p in psutil.process_iter(["name", "memory_info"]) if p.info["memory_info"].rss > 500 * 1024 * 1024]) [(2650, 'chrome', 532324352), (3038, 'chrome', 1120088064), (21915, 'sublime_text', 615407616)] +---- + Top 3 processes which consumed the most CPU time: .. code-block:: python - >>> pp([(p.pid, p.info['name'], sum(p.info['cpu_times'])) for p in sorted(psutil.process_iter(['name', 'cpu_times']), key=lambda p: sum(p.info['cpu_times'][:2]))][-3:]) + >>> pp([(p.pid, p.info["name"], sum(p.info["cpu_times"])) for p in sorted(psutil.process_iter(["name", "cpu_times"]), key=lambda p: sum(p.info["cpu_times"][:2]))][-3:]) [(2721, 'chrome', 10219.73), (1150, 'Xorg', 11116.989999999998), (2650, 'chrome', 18451.97)] -Bytes conversion ----------------- +---- + +Top N processes by cumulative disk read + write bytes (similar to ``iotop``): + +.. code-block:: python + + import psutil + + + def top_io_procs(n=5): + procs = [] + for p in psutil.process_iter(["io_counters"]): + try: + io = p.io_counters() + except psutil.Error: + pass + else: + procs.append((io.read_bytes + io.write_bytes, p)) + procs.sort(key=lambda x: x[0], reverse=True) + return procs[:n] + +---- + +Top N processes by open file descriptors (useful for diagnosing fd leaks): + +.. code-block:: python + + import psutil + + def top_open_files(n=5): + procs = [] + for p in psutil.process_iter(): + try: + procs.append((p.num_fds(), p)) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + procs.sort(key=lambda x: x[0], reverse=True) + return procs[:n] + +Monitoring processes +^^^^^^^^^^^^^^^^^^^^ + +Periodically monitor CPU and memory usage of a process using +:meth:`Process.oneshot` for efficiency: + +.. code-block:: python + + import time + import psutil + + def monitor(pid, interval=1): + p = psutil.Process(pid) + while p.is_running(): + with p.oneshot(): + cpu = p.cpu_percent() + mem = p.memory_info().rss + print("cpu={:<6} mem={}".format(str(cpu) + "%", mem / 1024 / 1024)) + time.sleep(interval) + +.. code-block:: none + + cpu=4.2% mem=23.4M + cpu=3.1% mem=23.5M + +Controlling processes +^^^^^^^^^^^^^^^^^^^^^ + +Kill a process tree (including grandchildren): + +.. code-block:: python + + import os + import signal + + import psutil + + + def kill_proc_tree( + pid, + sig=signal.SIGTERM, + include_parent=True, + timeout=None, + on_terminate=None, + ): + """Kill a process tree (including grandchildren) with signal + "sig" and return a (gone, still_alive) tuple. + "on_terminate", if specified, is a callback function which is + called as soon as a child terminates. + """ + assert pid != os.getpid(), "I won't kill myself!" + parent = psutil.Process(pid) + children = parent.children(recursive=True) + if include_parent: + children.append(parent) + for p in children: + try: + p.send_signal(sig) + except psutil.NoSuchProcess: + pass + gone, alive = psutil.wait_procs( + children, timeout=timeout, callback=on_terminate + ) + return (gone, alive) + +---- + +Kill / reap zombie (defunct) processes: + + +.. code-block:: python + + import psutil + + def kill_zombies(): + for p in psutil.process_iter(["status"]): + if p.info["status"] == psutil.STATUS_ZOMBIE: + parent = p.parent() + if parent: + parent.terminate() + parent.wait() + p.wait() + +---- + +Terminate all processes matching a given name: .. code-block:: python import psutil + def terminate_procs_by_name(name): + for p in psutil.process_iter(["name"]): + if p.info["name"] == name: + p.terminate() + +---- + +Terminate a process gracefully, falling back to ``SIGKILL`` if it does not +exit within the timeout: + +.. code-block:: python + + import psutil + + def graceful_kill(pid, timeout=3): + p = psutil.Process(pid) + p.terminate() + try: + p.wait(timeout=timeout) + except psutil.TimeoutExpired: + p.kill() + +---- + +Restart a process: + +.. code-block:: python + + import subprocess + import psutil + + def restart_process(pid): + p = psutil.Process(pid) + cmd = p.cmdline() + p.terminate() + p.wait() + return subprocess.Popen(cmd) + +---- + +Temporarily pause and resume a process using a context manager: + +.. code-block:: python + + import contextlib + import psutil + + @contextlib.contextmanager + def suspended(pid): + p = psutil.Process(pid) + p.suspend() + try: + yield p + finally: + p.resume() + + # usage + with suspended(pid): + pass # process is paused here + +---- + +CPU throttle: limit a process's CPU usage to a target percentage by +alternating :meth:`Process.suspend` and :meth:`Process.resume`: + +.. code-block:: python + + import time + import psutil + + def throttle(pid, max_cpu_percent=50, interval=0.1): + """Slow down a process so it uses at most max_cpu_percent% CPU.""" + p = psutil.Process(pid) + while p.is_running(): + cpu = p.cpu_percent(interval=interval) + if cpu > max_cpu_percent: + p.suspend() + time.sleep(interval * cpu / max_cpu_percent) + p.resume() + +---- + +Restart a process automatically if it dies: + +.. code-block:: python + + import subprocess + import time + + import psutil + + + def watchdog(cmd, max_restarts=None, interval=1): + """Run cmd as a persistent process. Restart on failure, optionally + with a max restarts. Logs start, exit, and restart events. + """ + restarts = 0 + while True: + proc = subprocess.Popen(cmd) + p = psutil.Process(proc.pid) + print(f"started PID {p.pid}") + proc.wait() + + if proc.returncode == 0: + # success + print(f"PID {p.pid} exited cleanly") + break + + # failure + restarts += 1 + print(f"PID {p.pid} died, restarting ({restarts})") + + if max_restarts is not None and restarts > max_restarts: + print("max restarts reached, giving up") + break + + time.sleep(interval) + + + if __name__ == "__main__": + watchdog(["python3", "script.py"]) + + +System +------ + +All APIs returning amounts (memory, disk, network I/O) express them in bytes. +This ``bytes2human()`` utility function used in the examples below converts +them to a human-readable string: + +.. code-block:: python + def bytes2human(n): - # http://code.activestate.com/recipes/578019 - # >>> bytes2human(10000) - # '9.8K' - # >>> bytes2human(100001221) - # '95.4M' - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + """ + >>> bytes2human(10000) + '9.8K' + >>> bytes2human(100001221) + '95.4M' + """ + symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") prefix = {} for i, s in enumerate(symbols): prefix[s] = 1 << (i + 1) * 10 for s in reversed(symbols): if abs(n) >= prefix[s]: value = float(n) / prefix[s] - return '%.1f%s' % (value, s) - return "%sB" % n + return "{:.1f}{}".format(value, s) + return "{}B".format(n) + +Memory +^^^^^^ + +Show both RAM and swap usage in human-readable form: + +.. code-block:: python + + import psutil - total = psutil.disk_usage('/').total - print(total) - print(bytes2human(total)) + def print_memory(): + ram = psutil.virtual_memory() + swap = psutil.swap_memory() + print( + "RAM: total={}, used={}, free={}, percent={}%".format( + bytes2human(ram.total), + bytes2human(ram.used), + bytes2human(ram.available), + ram.percent, + ) + ) + print( + "Swap: total={}, used={}, free={}, percent={}%".format( + bytes2human(swap.total), + bytes2human(swap.used), + bytes2human(swap.free), + swap.percent, + ) + ) + + +.. code-block:: none + + RAM: total=8.0G, used=4.5G, free=3.0G, percent=56.2% + Swap: total=2.0G, used=0.1G, free=1.9G, percent=4.1% + +CPU +^^^ + +Print real-time CPU usage percentage: -...prints:: +.. code-block:: python + + import psutil + + while True: + print("CPU: {}%".format(psutil.cpu_percent(interval=1))) + +.. code-block:: none + + CPU: 2.1% + CPU: 1.4% + CPU: 0.9% + +---- + +For each CPU core: + +.. code-block:: python + + import psutil + + while True: + for i, pct in enumerate(psutil.cpu_percent(percpu=True, interval=1)): + print("CPU-{}: {}%".format(i, pct)) + print() + +.. code-block:: none + + CPU-0: 1.0% + CPU-1: 2.1% + CPU-2: 3.0% + +Disks +^^^^^ + +Show disk usage for all mounted partitions: + +.. code-block:: python + + import psutil + + def print_disk_usage(): + for part in psutil.disk_partitions(): + usage = psutil.disk_usage(part.mountpoint) + print("{:<20} total={:<8} used={:<8} free={:<8} percent={}%".format( + part.mountpoint, + bytes2human(usage.total), bytes2human(usage.used), + bytes2human(usage.free), usage.percent)) + +.. code-block:: none + + / total=47.8G used=17.4G free=27.9G percent=38.4% + /boot/efi total=256.0M used=73.6M free=182.4M percent=28.8% + /home total=878.7G used=497.5G free=336.5G percent=59.7% + + +---- + +Show real-time disk I/O: + +.. code-block:: python + + import psutil, time + + def disk_io(): + while True: + before = psutil.disk_io_counters() + time.sleep(1) + after = psutil.disk_io_counters() + r = after.read_bytes - before.read_bytes + w = after.write_bytes - before.write_bytes + print("Read: {}/s, Write: {}/s".format(bytes2human(r), bytes2human(w))) + +.. code-block:: none + + Read: 1.2M/s, Write: 256.0K/s + Read: 0.0B/s, Write: 128.0K/s + +Network +^^^^^^^ + +List IP addresses for each network interface: + +.. code-block:: python + + import psutil, socket + + def print_net_addrs(): + for iface, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == socket.AF_INET: + print( + "{:<15} address={:<15} netmask={}".format( + iface, addr.address, addr.netmask + ) + ) + +.. code-block:: none + + lo address=127.0.0.1 netmask=255.0.0.0 + eth0 address=10.0.0.4 netmask=255.255.255.0 + +---- + +Show real-time network I/O per interface: + +.. code-block:: python + + import psutil, time + + def net_io(): + while True: + before = psutil.net_io_counters(pernic=True) + time.sleep(1) + after = psutil.net_io_counters(pernic=True) + for iface in after: + s = after[iface].bytes_sent - before[iface].bytes_sent + r = after[iface].bytes_recv - before[iface].bytes_recv + print( + "{:<10} sent={:<10} recv={}".format( + iface, bytes2human(s) + "/s", bytes2human(r) + "/s" + ) + ) + print() + +.. code-block:: none + + lo sent=0.0B/s recv=0.0B/s + eth0 sent=12.3K/s recv=45.6K/s + +---- + +List all active TCP connections with their status: + +.. code-block:: python + + import psutil - 100399730688 - 93.5G + def netstat(): + templ = "{:<20} {:<20} {:<13} {:<6}" + print(templ.format("Local", "Remote", "Status", "PID")) + for conn in psutil.net_connections(kind="tcp"): + laddr = "{}:{}".format(conn.laddr.ip, conn.laddr.port) + raddr = ( + "{}:{}".format(conn.raddr.ip, conn.raddr.port) + if conn.raddr + else "-" + ) + print(templ.format(laddr, raddr, conn.status, conn.pid or "")) + +.. code-block:: none + + Local Remote Status PID + :::1716 - LISTEN 223441 + 127.0.0.1:631 - LISTEN + 10.0.0.4:45278 20.222.111.74:443 ESTABLISHED 437213 + 10.0.0.4:40130 172.14.148.135:443 ESTABLISHED + 0.0.0.0:22 - LISTEN 723345 diff --git a/docs/requirements.txt b/docs/requirements.txt index bd8714de8e..f52abbc989 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ sphinx sphinx_rtd_theme +sphinx_copybutton sphinxcontrib-googleanalytics From 5e606fe1143aa27a66ffba4c1221d18b9bcaa738 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 12 Mar 2026 00:10:48 +0100 Subject: [PATCH 1579/1714] Doc: add summary table of officially supported OSes and architectures --- docs/_static/css/custom.css | 11 ++++++++ docs/changelog.rst | 54 ++++++++++++++++++++++------------- docs/platform.rst | 56 +++++++++++++++++++++++++++++++------ 3 files changed, 93 insertions(+), 28 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 3652a848aa..d97d5c5c66 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -15,6 +15,17 @@ footer div[role="navigation"][aria-label="Footer"] { display: block; } +/* HTML tables */ +.wy-table-responsive table th, +.wy-table-responsive table td { + padding: 10px !important; +} + +/* HTML tables */ +.wy-table-responsive table thead { + background-color: #eeeeee; +} + .rst-content dl:not(.docutils) { margin: 0px 0px 0px 0px !important; } diff --git a/docs/changelog.rst b/docs/changelog.rst index 27877f98b5..f0a2cf1efa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,27 +9,49 @@ Changelog **Enhancements** -- :gh:`2761`, :gh:`2757`, :gh:`2760`: Tons of doc improvements: +Doc: + +- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`. Tons of doc improvements. In + order of importance: + - Split the documentation from a single-page HTML document into multiple - sub-sections. Sections now include separate pages for API reference, + sections. Sections now include separate pages for API reference, installation, release timeline, FAQs, and more. - - Moved 18 years old HISTORY.rst into docs/changelog.rst for better - integration. Original file remains in the root with a note pointing to the - new location. - - Added tons of new examples in docs/recipes.rst. + - Moved 18 years old ``HISTORY.rst`` and ``INSTALL.rst`` files into + ``docs/changelog.rst`` and ``docs/install.rst`` for better integration. + Original files remain in the project root with a note pointing to the new + locations. + - Added a summary table of officially supported operating systems and + architectures. + - Added tons of new examples in ``docs/recipes.rst``. + - Drastically improved :func:`virtual_memory()` doc, which is now more + detailed, and includes a table with all the available metrics on each + platform. - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. - Use sphinx extension to validate Python code snippets syntax at build-time. -- :gh:`1946`: add inline type hints to all public APIs in `psutil/__init__.py`. +Type hints / enums: + +- :gh:`1946`: Add inline type hints to all public APIs in `psutil/__init__.py`. Type checkers (mypy, pyright, etc.) can now statically verify code that uses psutil. No runtime behavior is changed; the annotations are purely informational. +- :gh:`2751`: Convert all named tuples in `psutil/_ntuples.py`_ from + ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type + annotations**. This makes the classes self-documenting, effectively turning + this module into a readable API reference. +- :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, + :class:`ConnectionStatus`, + :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) + grouping related constants. The individual top-level constants (e.g. + ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases + for the corresponding enum members. + +New APIs: + - :gh:`2729`: New :meth:`Process.page_faults()` method, returning a ``(minor, major)`` namedtuple. -- :gh:`2745`: Drastically improve :func:`virtual_memory()` docstring, which is - now more detailed, and includes a table with all the available metrics on - each platform. - :gh:`2731`, :gh:`2736`, :gh:`2723`, :gh:`2733`: Reorganization of process memory APIs. @@ -65,19 +87,11 @@ Changelog - :meth:`Process.memory_full_info()` is **deprecated**. Use the new :meth:`Process.memory_footprint()` instead. +Others: + - :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times()` has been normalized on all platforms, and the first 3 fields are now always ``user, system, idle``. See compatibility notes below. -- :gh:`2751`: convert all named tuples in `psutil/_ntuples.py`_ from - ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type - annotations**. This makes the classes self-documenting, effectively turning - this module into a readable API reference. -- :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, - :class:`ConnectionStatus`, - :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) - grouping related constants. The individual top-level constants (e.g. - ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases - for the corresponding enum members. - :gh:`2754`: standardize :func:`sensors_battery()`'s `percent` so that it returns a `float` instead of `int` on all systems, not only Linux. diff --git a/docs/platform.rst b/docs/platform.rst index a4e4b51f7e..1d8aabefa7 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -4,21 +4,61 @@ Platform support Python ^^^^^^ -**Current Python:** 3.7+ and PyPy. +**Current Python:** 3.7 and PyPy3. -**Python 2.7**: latest psutil version supporting Python 2.7 is -`psutil 6.1.1 `__. -The 6.1.X serie may receive critical bug-fixes but no new features. It will -be maintained in the dedicated +**Python 2.7**: latest psutil version supporting it is +`psutil 6.1.1 `__ (Dec 2024). +The 6.1.X series may still receive critical bug-fixes but no new features. +It will be maintained in the dedicated `python2 `__ branch. -To install it: +To install psutil on Python 2.7 run: :: - $ python2 -m pip install psutil==6.1.* + python2 -m pip install psutil==6.1.* + +Operating systems +^^^^^^^^^^^^^^^^^ + +================ ================ ==== ================================================= ========= +Platform Minimum version Year How enforced CI tested +================ ================ ==== ================================================= ========= +Linux 2.6.13 (soft) 2005 graceful fallbacks; no hard check yes +Windows Vista 2007 hard check at import + build time yes +macOS 10.7 (Lion) 2011 ``MAC_OS_X_VERSION_MIN_REQUIRED`` in C yes +FreeBSD 12.0 2018 graceful fallbacks via ``#if __FreeBSD_version`` yes +NetBSD 5.0 2009 graceful fallbacks via ``#if __NetBSD_Version__`` yes +OpenBSD unknown yes +SunOS / Solaris unknown memleak tests only +AIX unknown no +================ ================ ==== ================================================= ========= + +Note: psutil may work on older versions of the above platforms but it is not +guaranteed. + +Architectures +^^^^^^^^^^^^^ + +Supported CPU architectures and platforms tested in CI or with prebuilt wheels: + +================ =========================== =========================== +Architecture CI-tested platforms Wheel builds +================ =========================== =========================== +x86_64 Linux, macOS, Windows Linux, macOS, Windows +aarch64 / ARM64 Linux, macOS, Windows Linux, macOS, Windows +================ =========================== =========================== + +Notes: + +- Linux wheels are built for both glibc (manylinux) and musl. +- macOS wheels are universal2 (include both x86_64 and arm64 slices). +- Windows wheels are labeled AMD64 or ARM64 according to architecture. +- Other architectures (i686, ppc64le, s390x, riscv64, ...) are supported + but not CI-tested. They can be compiled from the source tarball + (``pip install psutil --no-binary psutil``). Support history ---------------- +^^^^^^^^^^^^^^^ * psutil 8.0.0 (2026-XX): drop Python 3.6 * psutil 7.2.0 (2025-12): publish wheels for **Linux musl** From d1b3d6292f665c2b121ae272d597708b054d7a86 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 12 Mar 2026 17:02:58 +0100 Subject: [PATCH 1580/1714] Ruff: add unsafe rule to turn `1 in (1, 2)` into `1 in {1, 2}` --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a2876e15f2..e4fde43500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,9 @@ line-length = 79 [tool.ruff.lint] preview = true +extend-safe-fixes = [ + "PLR6201", # turn `1 in (1, 2)` into `1 in {1, 2}` +] select = [ # To get a list of all values: `python3 -m ruff linter`. "ALL", From aeae3ace81f9ba054cf8218bb11d6a3759b77f0a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Mar 2026 10:55:00 +0100 Subject: [PATCH 1581/1714] Sphinx Makefile: remove commands in don't use --- docs/Makefile | 214 ++------------------------------------------------ 1 file changed, 7 insertions(+), 207 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index be7e058b02..96896b05a8 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,234 +1,34 @@ # Makefile for Sphinx documentation -# -# You can set these variables from the command line. PYTHON = python3 -SPHINXOPTS = SPHINXBUILD = $(PYTHON) -m sphinx PAPER = BUILDDIR = _build +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) . -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -DEPS = \ - sphinx \ - sphinx_rtd_theme - -.PHONY: help -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" - -.PHONY: clean clean: rm -rf $(BUILDDIR) -.PHONY: html -html: +html: ## Generate doc in HTML format $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: +singlehtml: ## Generate doc as a single HTML page $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/psutil.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/psutil.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/psutil" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/psutil" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: +text: ## Generate doc in .txt format $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: +check-links: ## Check links $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." - -.PHONY: install-pydeps -install-pydeps: ## Install GIT hooks, pip, test deps (also upgrades them). - $(PYTHON) -m pip install --user --upgrade --trusted-host files.pythonhosted.org $(DEPS) +help: ## Display callable targets. + @awk -F':.*?## ' '/^[a-zA-Z0-9_.-]+:.*?## / {printf "\033[36m%-24s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort From c714bdc2dbc4aebb828f0420b0bdf30114201aa0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Mar 2026 14:01:34 +0100 Subject: [PATCH 1582/1714] RsT: use _ instead of __ in links --- README.rst | 16 +- docs/api.rst | 44 ++-- docs/index.rst | 10 +- docs/install.rst | 12 +- docs/platform.rst | 4 +- docs/timeline.rst | 618 +++++++++++++++++++++++----------------------- 6 files changed, 352 insertions(+), 352 deletions(-) diff --git a/README.rst b/README.rst index cb765a2997..78900814b7 100644 --- a/README.rst +++ b/README.rst @@ -86,9 +86,9 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are cPython 3.7+ and `PyPy `__. +Supported Python versions are cPython 3.7+ and `PyPy `_. Latest psutil version supporting Python 2.7 is -`psutil 6.1.1 `__. +`psutil 6.1.1 `_. Sponsors ======== @@ -122,15 +122,15 @@ Funding While psutil is free software and will always be, the project would benefit immensely from some funding. -psutil is among the `top 100 `__ +psutil is among the `top 100 `_ most-downloaded Python packages, and keeping up with bug reports, user support, and ongoing maintenance has become increasingly difficult to sustain as a one-person effort. If you're a company that's making significant use of psutil you can consider -becoming a sponsor via `GitHub `__, -`Open Collective `__ or -`PayPal `__. -Sponsors can have their logo displayed here and in the psutil `documentation `__. +becoming a sponsor via `GitHub `_, +`Open Collective `_ or +`PayPal `_. +Sponsors can have their logo displayed here and in the psutil `documentation `_. Example usages ============== @@ -446,7 +446,7 @@ Heap info pheap(heap_used=5177792, mmap_used=819200) >>> psutil.heap_trim() -See also `psleak `__ +See also `psleak `_ Windows services ---------------- diff --git a/docs/api.rst b/docs/api.rst index 83058810ed..7ed2c6872b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -72,7 +72,7 @@ CPU 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 `#1210 - `__. + `_. .. function:: cpu_percent(interval=None, percpu=False) @@ -481,7 +481,7 @@ Disks *total* and *used* fields on UNIX refer to the overall total and used space, whereas *free* represents the space available for the **user** and *percent* represents the **user** utilization (see - `source code `__). + `source code `_). That is why *percent* value may look 5% bigger than what you would expect it to be. Also note that both 4 values match "df" cmdline utility. @@ -727,7 +727,7 @@ Network .. note:: if you're interested in others families (e.g. AF_BLUETOOTH) you can use - the more powerful `netifaces `__ + the more powerful `netifaces `_ extension. .. note:: @@ -1222,7 +1222,7 @@ Process class The process name. On Windows the return value is cached after first call. Not on POSIX because the process name may change. - See also how to `find a process by name <#find-process-by-name>`__. + See also how to `find a process by name <#find-process-by-name>`_. .. method:: exe() @@ -1256,7 +1256,7 @@ Process class .. note:: on macOS Big Sur this function returns something meaningful only for the current process or in - `other specific circumstances `__). + `other specific circumstances `_). .. versionadded:: 4.0.0 .. versionchanged:: 5.3.0 added SunOS support @@ -1473,7 +1473,7 @@ Process class Return process I/O statistics as a named tuple. For Linux you can refer to - `/proc filesystem documentation `__. + `/proc filesystem documentation `_. - **read_count**: the number of read operations performed (cumulative). This is supposed to count the number of read-related syscalls such as @@ -1550,7 +1550,7 @@ Process class - **id**: the native thread ID assigned by the kernel. If :attr:`pid` refers to the current process, this matches the - `native_id `__ + `native_id `_ attribute of the `threading.Thread`_ class, and can be used to reference individual Python threads running within your own Python app. - **user_time**: time spent in user mode. @@ -1559,7 +1559,7 @@ Process class .. method:: cpu_times() Return a named tuple representing the accumulated process times, in seconds - (see `explanation `__). + (see `explanation `_). This is similar to `os.times`_ but can be used for any process PID. - **user**: time spent in user mode. @@ -1633,7 +1633,7 @@ Process class .. method:: cpu_affinity(cpus=None) Get or set process current - `CPU affinity `__. + `CPU affinity `_. CPU affinity consists in telling the OS to run a process on a limited set of CPUs only (on Linux cmdline, ``taskset`` command is typically used). If no argument is passed it returns the current CPU affinity as a list @@ -1852,7 +1852,7 @@ Process class Return a named tuple with USS, PSS and swap memory metrics. These give a more accurate picture of actual memory consumption than :meth:`memory_info`, as explained in this - `blog post `__. + `blog post `_. It works by walking the full process address space, so it is considerably slower than :meth:`memory_info` and may require elevated privileges. @@ -1974,7 +1974,7 @@ Process class .. versionchanged:: 5.6.0 removed macOS support because inherently broken (see - issue `#1291 `__) + issue `#1291 `_) .. method:: children(recursive=False) @@ -2000,9 +2000,9 @@ Process class Note that in the example above if process X disappears process Y won't be returned either as the reference to process A is lost. This concept is well summaried by this - `unit test `__. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. + `unit test `_. + See also how to `kill a process tree <#kill-process-tree>`_ and + `terminate my children <#terminate-my-children>`_. .. method:: page_faults() @@ -2200,8 +2200,8 @@ Process class On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals are supported and *SIGTERM* is treated as an alias for :meth:`kill()`. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. + See also how to `kill a process tree <#kill-process-tree>`_ and + `terminate my children <#terminate-my-children>`_. .. versionchanged:: 3.2.0 support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows @@ -2227,8 +2227,8 @@ Process class whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGTERM)``. On Windows this is an alias for :meth:`kill`. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. + See also how to `kill a process tree <#kill-process-tree>`_ and + `terminate my children <#terminate-my-children>`_. .. method:: kill() @@ -2236,8 +2236,8 @@ Process class checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. On Windows this is done by using `TerminateProcess`_. - See also how to `kill a process tree <#kill-process-tree>`__ and - `terminate my children <#terminate-my-children>`__. + See also how to `kill a process tree <#kill-process-tree>`_ and + `terminate my children <#terminate-my-children>`_. .. method:: wait(timeout=None) @@ -2667,7 +2667,7 @@ Process priority constants *IOPRIO_CLASS_IDLE* means the process will get I/O time when no-one else needs the disk. For further information refer to manuals of - `ionice `__ command line utility or + `ionice `_ command line utility or `ioprio_get`_ system call. Availability: Linux @@ -2811,7 +2811,7 @@ Other constants You may want to re-set this constant right after importing psutil in case your /proc filesystem is mounted elsewhere or if you want to retrieve information about Linux containers such as Docker, Heroku or LXC (see - `here `__ + `here `_ for more info). It must be noted that this trick works only for APIs which rely on /proc filesystem (e.g. `memory`_ APIs and most :class:`Process` class methods). diff --git a/docs/index.rst b/docs/index.rst index 63fa823ae9..f6684324bf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -75,15 +75,15 @@ Funding While psutil is free software and will always be, the project would benefit immensely from some funding. -psutil is among the `top 100 `__ +psutil is among the `top 100 `_ most-downloaded Python packages, and keeping up with bug reports, user support, and ongoing maintenance has become increasingly difficult to sustain as a one-person effort. If you're a company that's making significant use of psutil you can consider -becoming a sponsor via `GitHub `__, -`Open Collective `__ or -`PayPal `__. -Sponsors can have their logo displayed here and in the psutil `documentation `__. +becoming a sponsor via `GitHub `_, +`Open Collective `_ or +`PayPal `_. +Sponsors can have their logo displayed here and in the psutil `documentation `_. Security -------- diff --git a/docs/install.rst b/docs/install.rst index f627f1d357..d7b1a037b4 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -19,7 +19,7 @@ UNIX ^^^^ On all UNIX systems you can use the `install-sysdeps.sh -`__ +`_ script. This will install the system dependencies necessary to compile psutil from sources. You can invoke this script from the Makefile as:: @@ -30,7 +30,7 @@ After system deps are installed, you can compile & install psutil with:: make build make install -...or this, which will fetch the latest source distribution from `PyPI `__:: +...or this, which will fetch the latest source distribution from `PyPI `_:: pip install --no-binary :all: psutil @@ -61,16 +61,16 @@ Windows ^^^^^^^ - To build or install psutil from source on Windows, you need to have - `Visual Studio 2017 `__. + `Visual Studio 2017 `_. or later installed. For detailed instructions, see the - `CPython Developer Guide `__. + `CPython Developer Guide `_. - MinGW is not supported for building psutil on Windows. - To build directly from the source tarball (.tar.gz) on PYPI, run:: pip install --no-binary :all: psutil - If you want to clone psutil's GIT repository and build / develop locally, - first install: `Git for Windows `__ + first install: `Git for Windows `_ and launch a Git Bash shell. This provides a Unix-like environment where ``make`` works. - Once inside Git Bash, you can run the usual ``make`` commands:: @@ -143,7 +143,7 @@ If you don't have pip you can install it with wget:: python3 < <(curl -s https://bootstrap.pypa.io/get-pip.py) -On Windows, `download pip `__, open +On Windows, `download pip `_, open cmd.exe and install it with:: py get-pip.py diff --git a/docs/platform.rst b/docs/platform.rst index 1d8aabefa7..f18abba6ce 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -7,10 +7,10 @@ Python **Current Python:** 3.7 and PyPy3. **Python 2.7**: latest psutil version supporting it is -`psutil 6.1.1 `__ (Dec 2024). +`psutil 6.1.1 `_ (Dec 2024). The 6.1.X series may still receive critical bug-fixes but no new features. It will be maintained in the dedicated -`python2 `__ branch. +`python2 `_ branch. To install psutil on Python 2.7 run: :: diff --git a/docs/timeline.rst b/docs/timeline.rst index 33ecb959af..54159a719d 100644 --- a/docs/timeline.rst +++ b/docs/timeline.rst @@ -2,414 +2,414 @@ Timeline ======== - 2026-02-08: - `7.2.3 `__ - - `what's new `__ - - `diff `__ + `7.2.3 `_ - + `what's new `_ - + `diff `_ - 2026-01-28: - `7.2.2 `__ - - `what's new `__ - - `diff `__ + `7.2.2 `_ - + `what's new `_ - + `diff `_ - 2025-12-29: - `7.2.1 `__ - - `what's new `__ - - `diff `__ + `7.2.1 `_ - + `what's new `_ - + `diff `_ - 2025-12-23: - `7.2.0 `__ - - `what's new `__ - - `diff `__ + `7.2.0 `_ - + `what's new `_ - + `diff `_ - 2025-11-02: - `7.1.3 `__ - - `what's new `__ - - `diff `__ + `7.1.3 `_ - + `what's new `_ - + `diff `_ - 2025-10-25: - `7.1.2 `__ - - `what's new `__ - - `diff `__ + `7.1.2 `_ - + `what's new `_ - + `diff `_ - 2025-10-19: - `7.1.1 `__ - - `what's new `__ - - `diff `__ + `7.1.1 `_ - + `what's new `_ - + `diff `_ - 2025-09-17: - `7.1.0 `__ - - `what's new `__ - - `diff `__ + `7.1.0 `_ - + `what's new `_ - + `diff `_ - 2025-02-13: - `7.0.0 `__ - - `what's new `__ - - `diff `__ + `7.0.0 `_ - + `what's new `_ - + `diff `_ - 2024-12-19: - `6.1.1 `__ - - `what's new `__ - - `diff `__ + `6.1.1 `_ - + `what's new `_ - + `diff `_ - 2024-10-17: - `6.1.0 `__ - - `what's new `__ - - `diff `__ + `6.1.0 `_ - + `what's new `_ - + `diff `_ - 2024-06-18: - `6.0.0 `__ - - `what's new `__ - - `diff `__ + `6.0.0 `_ - + `what's new `_ - + `diff `_ - 2024-01-19: - `5.9.8 `__ - - `what's new `__ - - `diff `__ + `5.9.8 `_ - + `what's new `_ - + `diff `_ - 2023-12-17: - `5.9.7 `__ - - `what's new `__ - - `diff `__ + `5.9.7 `_ - + `what's new `_ - + `diff `_ - 2023-10-15: - `5.9.6 `__ - - `what's new `__ - - `diff `__ + `5.9.6 `_ - + `what's new `_ - + `diff `_ - 2023-04-17: - `5.9.5 `__ - - `what's new `__ - - `diff `__ + `5.9.5 `_ - + `what's new `_ - + `diff `_ - 2022-11-07: - `5.9.4 `__ - - `what's new `__ - - `diff `__ + `5.9.4 `_ - + `what's new `_ - + `diff `_ - 2022-10-18: - `5.9.3 `__ - - `what's new `__ - - `diff `__ + `5.9.3 `_ - + `what's new `_ - + `diff `_ - 2022-09-04: - `5.9.2 `__ - - `what's new `__ - - `diff `__ + `5.9.2 `_ - + `what's new `_ - + `diff `_ - 2022-05-20: - `5.9.1 `__ - - `what's new `__ - - `diff `__ + `5.9.1 `_ - + `what's new `_ - + `diff `_ - 2021-12-29: - `5.9.0 `__ - - `what's new `__ - - `diff `__ + `5.9.0 `_ - + `what's new `_ - + `diff `_ - 2020-12-19: - `5.8.0 `__ - - `what's new `__ - - `diff `__ + `5.8.0 `_ - + `what's new `_ - + `diff `_ - 2020-10-24: - `5.7.3 `__ - - `what's new `__ - - `diff `__ + `5.7.3 `_ - + `what's new `_ - + `diff `_ - 2020-07-15: - `5.7.2 `__ - - `what's new `__ - - `diff `__ + `5.7.2 `_ - + `what's new `_ - + `diff `_ - 2020-07-15: - `5.7.1 `__ - - `what's new `__ - - `diff `__ + `5.7.1 `_ - + `what's new `_ - + `diff `_ - 2020-02-18: - `5.7.0 `__ - - `what's new `__ - - `diff `__ + `5.7.0 `_ - + `what's new `_ - + `diff `_ - 2019-11-26: - `5.6.7 `__ - - `what's new `__ - - `diff `__ + `5.6.7 `_ - + `what's new `_ - + `diff `_ - 2019-11-25: - `5.6.6 `__ - - `what's new `__ - - `diff `__ + `5.6.6 `_ - + `what's new `_ - + `diff `_ - 2019-11-06: - `5.6.5 `__ - - `what's new `__ - - `diff `__ + `5.6.5 `_ - + `what's new `_ - + `diff `_ - 2019-11-04: - `5.6.4 `__ - - `what's new `__ - - `diff `__ + `5.6.4 `_ - + `what's new `_ - + `diff `_ - 2019-06-11: - `5.6.3 `__ - - `what's new `__ - - `diff `__ + `5.6.3 `_ - + `what's new `_ - + `diff `_ - 2019-04-26: - `5.6.2 `__ - - `what's new `__ - - `diff `__ + `5.6.2 `_ - + `what's new `_ - + `diff `_ - 2019-03-11: - `5.6.1 `__ - - `what's new `__ - - `diff `__ + `5.6.1 `_ - + `what's new `_ - + `diff `_ - 2019-03-05: - `5.6.0 `__ - - `what's new `__ - - `diff `__ + `5.6.0 `_ - + `what's new `_ - + `diff `_ - 2019-02-15: - `5.5.1 `__ - - `what's new `__ - - `diff `__ + `5.5.1 `_ - + `what's new `_ - + `diff `_ - 2019-01-23: - `5.5.0 `__ - - `what's new `__ - - `diff `__ + `5.5.0 `_ - + `what's new `_ - + `diff `_ - 2018-10-30: - `5.4.8 `__ - - `what's new `__ - - `diff `__ + `5.4.8 `_ - + `what's new `_ - + `diff `_ - 2018-08-14: - `5.4.7 `__ - - `what's new `__ - - `diff `__ + `5.4.7 `_ - + `what's new `_ - + `diff `_ - 2018-06-07: - `5.4.6 `__ - - `what's new `__ - - `diff `__ + `5.4.6 `_ - + `what's new `_ - + `diff `_ - 2018-04-13: - `5.4.5 `__ - - `what's new `__ - - `diff `__ + `5.4.5 `_ - + `what's new `_ - + `diff `_ - 2018-04-13: - `5.4.4 `__ - - `what's new `__ - - `diff `__ + `5.4.4 `_ - + `what's new `_ - + `diff `_ - 2018-01-01: - `5.4.3 `__ - - `what's new `__ - - `diff `__ + `5.4.3 `_ - + `what's new `_ - + `diff `_ - 2017-12-07: - `5.4.2 `__ - - `what's new `__ - - `diff `__ + `5.4.2 `_ - + `what's new `_ - + `diff `_ - 2017-11-08: - `5.4.1 `__ - - `what's new `__ - - `diff `__ + `5.4.1 `_ - + `what's new `_ - + `diff `_ - 2017-10-12: - `5.4.0 `__ - - `what's new `__ - - `diff `__ + `5.4.0 `_ - + `what's new `_ - + `diff `_ - 2017-09-10: - `5.3.1 `__ - - `what's new `__ - - `diff `__ + `5.3.1 `_ - + `what's new `_ - + `diff `_ - 2017-09-01: - `5.3.0 `__ - - `what's new `__ - - `diff `__ + `5.3.0 `_ - + `what's new `_ - + `diff `_ - 2017-04-10: - `5.2.2 `__ - - `what's new `__ - - `diff `__ + `5.2.2 `_ - + `what's new `_ - + `diff `_ - 2017-03-24: - `5.2.1 `__ - - `what's new `__ - - `diff `__ + `5.2.1 `_ - + `what's new `_ - + `diff `_ - 2017-03-05: - `5.2.0 `__ - - `what's new `__ - - `diff `__ + `5.2.0 `_ - + `what's new `_ - + `diff `_ - 2017-02-07: - `5.1.3 `__ - - `what's new `__ - - `diff `__ + `5.1.3 `_ - + `what's new `_ - + `diff `_ - 2017-02-03: - `5.1.2 `__ - - `what's new `__ - - `diff `__ + `5.1.2 `_ - + `what's new `_ - + `diff `_ - 2017-02-03: - `5.1.1 `__ - - `what's new `__ - - `diff `__ + `5.1.1 `_ - + `what's new `_ - + `diff `_ - 2017-02-01: - `5.1.0 `__ - - `what's new `__ - - `diff `__ + `5.1.0 `_ - + `what's new `_ - + `diff `_ - 2016-12-21: - `5.0.1 `__ - - `what's new `__ - - `diff `__ + `5.0.1 `_ - + `what's new `_ - + `diff `_ - 2016-11-06: - `5.0.0 `__ - - `what's new `__ - - `diff `__ + `5.0.0 `_ - + `what's new `_ - + `diff `_ - 2016-10-05: - `4.4.2 `__ - - `what's new `__ - - `diff `__ + `4.4.2 `_ - + `what's new `_ - + `diff `_ - 2016-10-25: - `4.4.1 `__ - - `what's new `__ - - `diff `__ + `4.4.1 `_ - + `what's new `_ - + `diff `_ - 2016-10-23: - `4.4.0 `__ - - `what's new `__ - - `diff `__ + `4.4.0 `_ - + `what's new `_ - + `diff `_ - 2016-09-01: - `4.3.1 `__ - - `what's new `__ - - `diff `__ + `4.3.1 `_ - + `what's new `_ - + `diff `_ - 2016-06-18: - `4.3.0 `__ - - `what's new `__ - - `diff `__ + `4.3.0 `_ - + `what's new `_ - + `diff `_ - 2016-05-14: - `4.2.0 `__ - - `what's new `__ - - `diff `__ + `4.2.0 `_ - + `what's new `_ - + `diff `_ - 2016-03-12: - `4.1.0 `__ - - `what's new `__ - - `diff `__ + `4.1.0 `_ - + `what's new `_ - + `diff `_ - 2016-02-17: - `4.0.0 `__ - - `what's new `__ - - `diff `__ + `4.0.0 `_ - + `what's new `_ - + `diff `_ - 2016-01-20: - `3.4.2 `__ - - `what's new `__ - - `diff `__ + `3.4.2 `_ - + `what's new `_ - + `diff `_ - 2016-01-15: - `3.4.1 `__ - - `what's new `__ - - `diff `__ + `3.4.1 `_ - + `what's new `_ - + `diff `_ - 2015-11-25: - `3.3.0 `__ - - `what's new `__ - - `diff `__ + `3.3.0 `_ - + `what's new `_ - + `diff `_ - 2015-10-04: - `3.2.2 `__ - - `what's new `__ - - `diff `__ + `3.2.2 `_ - + `what's new `_ - + `diff `_ - 2015-09-03: - `3.2.1 `__ - - `what's new `__ - - `diff `__ + `3.2.1 `_ - + `what's new `_ - + `diff `_ - 2015-09-02: - `3.2.0 `__ - - `what's new `__ - - `diff `__ + `3.2.0 `_ - + `what's new `_ - + `diff `_ - 2015-07-15: - `3.1.1 `__ - - `what's new `__ - - `diff `__ + `3.1.1 `_ - + `what's new `_ - + `diff `_ - 2015-07-15: - `3.1.0 `__ - - `what's new `__ - - `diff `__ + `3.1.0 `_ - + `what's new `_ - + `diff `_ - 2015-06-18: - `3.0.1 `__ - - `what's new `__ - - `diff `__ + `3.0.1 `_ - + `what's new `_ - + `diff `_ - 2015-06-13: - `3.0.0 `__ - - `what's new `__ - - `diff `__ + `3.0.0 `_ - + `what's new `_ - + `diff `_ - 2015-02-02: - `2.2.1 `__ - - `what's new `__ - - `diff `__ + `2.2.1 `_ - + `what's new `_ - + `diff `_ - 2015-01-06: - `2.2.0 `__ - - `what's new `__ - - `diff `__ + `2.2.0 `_ - + `what's new `_ - + `diff `_ - 2014-09-26: - `2.1.3 `__ - - `what's new `__ - - `diff `__ + `2.1.3 `_ - + `what's new `_ - + `diff `_ - 2014-09-21: - `2.1.2 `__ - - `what's new `__ - - `diff `__ + `2.1.2 `_ - + `what's new `_ - + `diff `_ - 2014-04-30: - `2.1.1 `__ - - `what's new `__ - - `diff `__ + `2.1.1 `_ - + `what's new `_ - + `diff `_ - 2014-04-08: - `2.1.0 `__ - - `what's new `__ - - `diff `__ + `2.1.0 `_ - + `what's new `_ - + `diff `_ - 2014-03-10: - `2.0.0 `__ - - `what's new `__ - - `diff `__ + `2.0.0 `_ - + `what's new `_ - + `diff `_ - 2013-11-25: - `1.2.1 `__ - - `what's new `__ - - `diff `__ + `1.2.1 `_ - + `what's new `_ - + `diff `_ - 2013-11-20: - `1.2.0 `__ - - `what's new `__ - - `diff `__ + `1.2.0 `_ - + `what's new `_ - + `diff `_ - 2013-10-22: - `1.1.2 `__ - - `what's new `__ - - `diff `__ + `1.1.2 `_ - + `what's new `_ - + `diff `_ - 2013-10-08: - `1.1.1 `__ - - `what's new `__ - - `diff `__ + `1.1.1 `_ - + `what's new `_ - + `diff `_ - 2013-09-28: - `1.1.0 `__ - - `what's new `__ - - `diff `__ + `1.1.0 `_ - + `what's new `_ - + `diff `_ - 2013-07-12: - `1.0.1 `__ - - `what's new `__ - - `diff `__ + `1.0.1 `_ - + `what's new `_ - + `diff `_ - 2013-07-10: - `1.0.0 `__ - - `what's new `__ - - `diff `__ + `1.0.0 `_ - + `what's new `_ - + `diff `_ - 2013-05-03: - `0.7.1 `__ - - `what's new `__ - - `diff `__ + `0.7.1 `_ - + `what's new `_ - + `diff `_ - 2013-04-12: - `0.7.0 `__ - - `what's new `__ - - `diff `__ + `0.7.0 `_ - + `what's new `_ - + `diff `_ - 2012-08-16: - `0.6.1 `__ - - `what's new `__ - - `diff `__ + `0.6.1 `_ - + `what's new `_ - + `diff `_ - 2012-08-13: - `0.6.0 `__ - - `what's new `__ - - `diff `__ + `0.6.0 `_ - + `what's new `_ - + `diff `_ - 2012-06-29: - `0.5.1 `__ - - `what's new `__ - - `diff `__ + `0.5.1 `_ - + `what's new `_ - + `diff `_ - 2012-06-27: - `0.5.0 `__ - - `what's new `__ - - `diff `__ + `0.5.0 `_ - + `what's new `_ - + `diff `_ - 2011-12-14: - `0.4.1 `__ - - `what's new `__ - - `diff `__ + `0.4.1 `_ - + `what's new `_ - + `diff `_ - 2011-10-29: - `0.4.0 `__ - - `what's new `__ - - `diff `__ + `0.4.0 `_ - + `what's new `_ - + `diff `_ - 2011-07-08: - `0.3.0 `__ - - `what's new `__ - - `diff `__ + `0.3.0 `_ - + `what's new `_ - + `diff `_ - 2011-03-20: - `0.2.1 `__ - - `what's new `__ - - `diff `__ + `0.2.1 `_ - + `what's new `_ - + `diff `_ - 2010-11-13: - `0.2.0 `__ - - `what's new `__ - - `diff `__ + `0.2.0 `_ - + `what's new `_ - + `diff `_ - 2010-03-02: - `0.1.3 `__ - - `what's new `__ - - `diff `__ + `0.1.3 `_ - + `what's new `_ - + `diff `_ - 2009-05-06: - `0.1.2 `__ - - `what's new `__ - - `diff `__ + `0.1.2 `_ - + `what's new `_ - + `diff `_ - 2009-03-06: - `0.1.1 `__ - - `what's new `__ - - `diff `__ + `0.1.1 `_ - + `what's new `_ - + `diff `_ - 2009-01-27: - `0.1.0 `__ - - `what's new `__ - - `diff `__ + `0.1.0 `_ - + `what's new `_ - + `diff `_ From b6dabc9097920cddb70b5a051b6f2c0625591592 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Mar 2026 15:09:35 +0100 Subject: [PATCH 1583/1714] Doc: show a list of notable software that depends on psutil (#2763) --- MANIFEST.in | 2 + README.rst | 179 ++++--- docs/_static/css/custom.css | 7 + docs/adoption.rst | 242 +++++++++ docs/api.rst | 2 +- docs/changelog.rst | 5 +- docs/index.rst | 33 +- docs/install.rst | 18 +- scripts/internal/convert_readme.py | 41 +- scripts/internal/find_adopters.py | 799 +++++++++++++++++++++++++++++ 10 files changed, 1201 insertions(+), 127 deletions(-) create mode 100644 docs/adoption.rst create mode 100755 scripts/internal/find_adopters.py diff --git a/MANIFEST.in b/MANIFEST.in index 88c59b1853..e70cdbac84 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -23,6 +23,7 @@ include docs/_static/css/custom.css include docs/_static/favicon.ico include docs/_static/sidebar.js include docs/_templates/layout.html +include docs/adoption.rst include docs/api.rst include docs/auto-build.sh include docs/changelog.rst @@ -164,6 +165,7 @@ include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/convert_readme.py include scripts/internal/download_wheels.py +include scripts/internal/find_adopters.py include scripts/internal/find_broken_links.py include scripts/internal/generate_manifest.py include scripts/internal/git_pre_commit.py diff --git a/README.rst b/README.rst index 78900814b7..5f5e16fe35 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild :alt: Linux, macOS, Windows -.. |github-actions-bsd| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/bsd.yml.svg?label=FreeBSD,%20NetBSD,%20OpenBSD +.. |github-actions-bsd| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/bsd.yml.svg?label=BSD :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests :alt: FreeBSD, NetBSD, OpenBSD @@ -60,8 +60,8 @@
    Home    Documentation    + Who uses psutil    Download    - Forum    Blog    Funding    What's new    @@ -121,21 +121,106 @@ Funding ======= While psutil is free software and will always be, the project would benefit -immensely from some funding. -psutil is among the `top 100 `_ -most-downloaded Python packages, and keeping up with bug reports, user support, -and ongoing maintenance has become increasingly difficult to sustain as a -one-person effort. -If you're a company that's making significant use of psutil you can consider -becoming a sponsor via `GitHub `_, -`Open Collective `_ or -`PayPal `_. -Sponsors can have their logo displayed here and in the psutil `documentation `_. +immensely from some funding. psutil is among the `top 100`_ most-downloaded +Python packages, and keeping up with bug reports, user support, and ongoing +maintenance has become increasingly difficult to sustain as a one-person +effort. If you're a company that's making significant use of psutil you can +consider becoming a sponsor via `GitHub +`_, `Open Collective +`_ or `PayPal +`_. +Sponsors can have their logo displayed here and in the psutil `documentation +`_. + +Projects using psutil +===================== + +psutil is one of the `top 100`_ most-downloaded packages on PyPI, with 280+ +million downloads per month, `760,000+ GitHub repositories +`_ using it, and +14,000+ packages depending on it. The projects below are a small sample of +notable software that depends on it. Some notable projects using psutil: + +- `TensorFlow `_, + `PyTorch `_, +- `Home Assistant `_, + `Ansible `_, + `Apache Airflow `_, + `Sentry `_ +- `Celery `_, + `Dask `_ +- `Glances `_, + `bpytop `_, + `Ajenti `_, + `GRR `_ +- `psleak`_ + +`Full list `_ + + + +Portings +======== + +- Go: `gopsutil `_ +- C: `cpslib `_ +- Rust: `rust-psutil `_ +- Nim: `psutil-nim `_ + +.. + +Supporters +========== + +People who donated money over the years: + +.. raw:: html + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + add your avatar + +..
    + +---- Example usages ============== -This represents pretty much the whole psutil API. +Below are interactive examples demonstrating all parts of the psutil API, +including CPU, memory, disks, network, sensors, and process management. CPU --- @@ -446,7 +531,7 @@ Heap info pheap(heap_used=5177792, mmap_used=819200) >>> psutil.heap_trim() -See also `psleak `_ +See also `psleak`_. Windows services ---------------- @@ -470,68 +555,6 @@ Windows services 'status': 'stopped', 'username': 'NT AUTHORITY\\LocalService'} -Projects using psutil -===================== - -Here's some I find particularly interesting: - -- https://github.com/giampaolo/psleak -- https://github.com/google/grr -- https://github.com/facebook/osquery/ -- https://github.com/nicolargo/glances -- https://github.com/aristocratos/bpytop -- https://github.com/Jahaja/psdash -- https://github.com/ajenti/ajenti -- https://github.com/home-assistant/home-assistant/ - -Portings -======== - -- Go: https://github.com/shirou/gopsutil -- C: https://github.com/hamon-in/cpslib -- Rust: https://github.com/rust-psutil/rust-psutil -- Nim: https://github.com/johnscillieri/psutil-nim - - -Supporters -========== - -People who donated money over the years: - -.. raw:: html - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - add your avatar +.. _`psleak`: https://github.com/giampaolo/psleak +.. _`top 100`: https://clickpy.clickhouse.com/dashboard/psutil diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index d97d5c5c66..326df029b1 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -193,3 +193,10 @@ div.body div.admonition, div.body div.impl-detail { .note code { background: #d6d6d6 !important; } + +/* Project logos in adoption page tables */ +.document td img[alt$="-logo"] { + height: 20px !important; + width: 20px !important; + vertical-align: middle; +} diff --git a/docs/adoption.rst b/docs/adoption.rst new file mode 100644 index 0000000000..072bf2ca05 --- /dev/null +++ b/docs/adoption.rst @@ -0,0 +1,242 @@ +.. currentmodule:: psutil + +Who uses psutil +=============== + +psutil is one of the `top 100 `_ +most-downloaded packages on PyPI, with 280+ million downloads per month, +`760,000+ GitHub repositories `_ using +it, and 14,000+ packages depending on it. The projects below are a small +sample of notable software that depends on it. + +Infrastructure / automation +--------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 28 32 14 26 + + * - Project + - Description + - Stars + - psutil usage + * - |homeassistant-logo| `Home Assistant `_ + - Open source home automation platform + - |homeassistant-stars| + - system monitor integration + * - |ansible-logo| `Ansible `_ + - IT automation platform + - |ansible-stars| + - system fact gathering + * - |airflow-logo| `Apache Airflow `_ + - Workflow orchestration platform + - |airflow-stars| + - process supervisor, unit testing + * - |celery-logo| `Celery `_ + - Distributed task queue + - |celery-stars| + - worker process monitoring, memleak detection + * - |salt-logo| `Salt `_ + - Infrastructure automation at scale + - |salt-stars| + - deep system data collection (grains) + * - |dask-logo| `Dask `_ + - Parallel computing with task scheduling + - |dask-stars| + - `metrics dashboard `_, profiling + * - |ajenti-logo| `Ajenti `_ + - Web-based server administration panel + - |ajenti-stars| + - monitoring plugins, deep integration + +AI / Machine learning +--------------------- + +.. list-table:: + :header-rows: 1 + :widths: 28 32 14 26 + + * - Project + - Description + - Stars + - psutil usage + * - |tensorflow-logo| `TensorFlow `_ + - Open source machine learning framework by Google + - |tensorflow-stars| + - unit tests + * - |pytorch-logo| `PyTorch `_ + - Tensors and dynamic neural networks with GPU acceleration + - |pytorch-stars| + - benchmark scripts + * - |ray-logo| `Ray `_ + - AI compute engine with distributed runtime + - |ray-stars| + - metrics dashboard + * - |mlflow-logo| `MLflow `_ + - AI/ML engineering platform + - |mlflow-stars| + - deep system monitoring integration + +Developer tools +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 28 32 14 26 + + * - Project + - Description + - Stars + - psutil usage + * - |sentry-logo| `Sentry `_ + - Error tracking and performance monitoring + - |sentry-stars| + - send telemetry metrics + * - |locust-logo| `Locust `_ + - Scalable load testing in Python + - |locust-stars| + - monitoring of the Locust process + * - |spyder-logo| `Spyder `_ + - Scientific Python IDE + - |spyder-stars| + - deep integration, UI stats, process management + * - |psleak-logo| `psleak `_ + - Test framework to detect memory in Python C extensions + - |psleak-stars| + - heap process memory (:func:`heap_info()`) + +System monitoring +----------------- + +.. list-table:: + :header-rows: 1 + :widths: 28 32 14 26 + + * - Project + - Description + - Stars + - psutil usage + * - |glances-logo| `Glances `_ + - System monitoring tool (top/htop alternative) + - |glances-stars| + - core dependency for all metrics + * - |osquery-logo| `OSQuery `_ + - SQL powered OS monitoring and analytics + - |osquery-stars| + - unit tests + * - |bpytop-logo| `bpytop `_ + - Terminal resource monitor + - |bpytop-stars| + - core dependency for all metrics + * - |auto-cpufreq-logo| `auto-cpufreq `_ + - Automatic CPU speed and power optimizer for Linux + - |auto-cpufreq-stars| + - core dependency for CPU monitoring + * - |grr-logo| `GRR `_ + - Remote live forensics by Google + - |grr-stars| + - endpoint system data collection, deep integration + * - |stui-logo| `s-tui `_ + - Terminal CPU stress and monitoring utility + - |stui-stars| + - core dependency for metrics + * - |asitop-logo| `asitop `_ + - Apple Silicon performance monitoring CLI + - |asitop-stars| + - core dependency for system metrics + * - |psdash-logo| `psdash `_ + - Web dashboard using psutil and Flask + - |psdash-stars| + - core dependency for all metrics + * - |dd-agent-logo| `dd-agent `_ + - Original monitoring agent by Datadog + - |dd-agent-stars| + - system metrics collection + * - |ddtrace-logo| `dd-trace-py `_ + - Python tracing and profiling library + - |ddtrace-stars| + - system metrics collection + +How this list was compiled +-------------------------- + +- `GitHub dependency graph `_ + was used to identify packages and repositories that depend on psutil. +- GitHub code search with query "psutil in:readme language:Python", sorted + by stars, was used to find additional projects that mention psutil in their + README. +- Each candidate was then manually verified by checking the project's + pyproject.toml, setup.py, setup.cfg or requirements.txt to confirm that + psutil is an actual dependency (direct, build-time, or optional), not just + a passing mention. +- Projects were excluded if they only mention psutil in documentation or + examples without declaring it as a dependency. +- Star counts are pulled dynamically from `shields.io `_ badges. +- The final list was manually curated to include notable projects and + meaningful usages of psutil across different areas of the Python ecosystem. + +.. ============================================================================ +.. ============================================================================ +.. ============================================================================ + +.. Star badges +.. ============================================================================ + +.. |airflow-stars| image:: https://img.shields.io/github/stars/apache/airflow.svg?style=social&label=%20 +.. |ajenti-stars| image:: https://img.shields.io/github/stars/ajenti/ajenti.svg?style=social&label=%20 +.. |ansible-stars| image:: https://img.shields.io/github/stars/ansible/ansible.svg?style=social&label=%20 +.. |asitop-stars| image:: https://img.shields.io/github/stars/tlkh/asitop.svg?style=social&label=%20 +.. |auto-cpufreq-stars| image:: https://img.shields.io/github/stars/AdnanHodzic/auto-cpufreq.svg?style=social&label=%20 +.. |bpytop-stars| image:: https://img.shields.io/github/stars/aristocratos/bpytop.svg?style=social&label=%20 +.. |celery-stars| image:: https://img.shields.io/github/stars/celery/celery.svg?style=social&label=%20 +.. |dask-stars| image:: https://img.shields.io/github/stars/dask/dask.svg?style=social&label=%20 +.. |ddtrace-stars| image:: https://img.shields.io/github/stars/DataDog/dd-trace-py.svg?style=social&label=%20 +.. |dd-agent-stars| image:: https://img.shields.io/github/stars/DataDog/dd-agent.svg?style=social&label=%20 +.. |glances-stars| image:: https://img.shields.io/github/stars/nicolargo/glances.svg?style=social&label=%20 +.. |grr-stars| image:: https://img.shields.io/github/stars/google/grr.svg?style=social&label=%20 +.. |homeassistant-stars| image:: https://img.shields.io/github/stars/home-assistant/core.svg?style=social&label=%20 +.. |locust-stars| image:: https://img.shields.io/github/stars/locustio/locust.svg?style=social&label=%20 +.. |mlflow-stars| image:: https://img.shields.io/github/stars/mlflow/mlflow.svg?style=social&label=%20 +.. |osquery-stars| image:: https://img.shields.io/github/stars/osquery/osquery.svg?style=social&label=%20 +.. |psdash-stars| image:: https://img.shields.io/github/stars/Jahaja/psdash.svg?style=social&label=%20 +.. |psleak-stars| image:: https://img.shields.io/github/stars/giampaolo/psleak.svg?style=social&label=%20 +.. |pytorch-stars| image:: https://img.shields.io/github/stars/pytorch/pytorch.svg?style=social&label=%20 +.. |ray-stars| image:: https://img.shields.io/github/stars/ray-project/ray.svg?style=social&label=%20 +.. |salt-stars| image:: https://img.shields.io/github/stars/saltstack/salt.svg?style=social&label=%20 +.. |sentry-stars| image:: https://img.shields.io/github/stars/getsentry/sentry.svg?style=social&label=%20 +.. |spyder-stars| image:: https://img.shields.io/github/stars/spyder-ide/spyder.svg?style=social&label=%20 +.. |stui-stars| image:: https://img.shields.io/github/stars/amanusk/s-tui.svg?style=social&label=%20 +.. |tensorflow-stars| image:: https://img.shields.io/github/stars/tensorflow/tensorflow.svg?style=social&label=%20 + +.. Logo images +.. ============================================================================ + +.. |airflow-logo| image:: https://github.com/apache.png?s=28 :height: 28 +.. |ajenti-logo| image:: https://github.com/ajenti.png?s=28 :height: 28 +.. |ansible-logo| image:: https://github.com/ansible.png?s=28 :height: 28 +.. |asitop-logo| image:: https://github.com/tlkh.png?s=28 :height: 28 +.. |auto-cpufreq-logo| image:: https://github.com/AdnanHodzic.png?s=28 :height: 28 +.. |bpytop-logo| image:: https://github.com/aristocratos.png?s=28 :height: 28 +.. |celery-logo| image:: https://github.com/celery.png?s=28 :height: 28 +.. |dask-logo| image:: https://github.com/dask.png?s=28 :height: 28 +.. |ddtrace-logo| image:: https://github.com/DataDog.png?s=28 :height: 28 +.. |dd-agent-logo| image:: https://github.com/DataDog.png?s=28 :height: 28 +.. |glances-logo| image:: https://github.com/nicolargo.png?s=28 :height: 28 +.. |grr-logo| image:: https://github.com/google.png?s=28 :height: 28 +.. |homeassistant-logo| image:: https://github.com/home-assistant.png?s=28 :height: 28 +.. |locust-logo| image:: https://github.com/locustio.png?s=28 :height: 28 +.. |mlflow-logo| image:: https://github.com/mlflow.png?s=28 :height: 28 +.. |osquery-logo| image:: https://github.com/osquery.png?s=28 :height: 28 +.. |psdash-logo| image:: https://github.com/Jahaja.png?s=28 :height: 28 +.. |psleak-logo| image:: https://github.com/giampaolo.png?s=28 :height: 28 +.. |pytorch-logo| image:: https://github.com/pytorch.png?s=28 :height: 28 +.. |ray-logo| image:: https://github.com/ray-project.png?s=28 :height: 28 +.. |salt-logo| image:: https://github.com/saltstack.png?s=28 :height: 28 +.. |sentry-logo| image:: https://github.com/getsentry.png?s=28 :height: 28 +.. |spyder-logo| image:: https://github.com/spyder-ide.png?s=28 :height: 28 +.. |stui-logo| image:: https://github.com/amanusk.png?s=28 :height: 28 +.. |tensorflow-logo| image:: https://github.com/tensorflow.png?s=28 :height: 28 + +.. --- Notes +.. Stars shield: +.. https://shields.io/badges/git-hub-repo-stars diff --git a/docs/api.rst b/docs/api.rst index 7ed2c6872b..16d34ee117 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,7 +6,7 @@ API reference .. contents:: :local: - :depth: 3 + :depth: 5 System related functions ------------------------ diff --git a/docs/changelog.rst b/docs/changelog.rst index f0a2cf1efa..b9a31f74dd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,8 +11,8 @@ Changelog Doc: -- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`. Tons of doc improvements. In - order of importance: +- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`. Tons of doc + improvements. In order of importance: - Split the documentation from a single-page HTML document into multiple sections. Sections now include separate pages for API reference, @@ -23,6 +23,7 @@ Doc: locations. - Added a summary table of officially supported operating systems and architectures. + - Show a section of notable software that depends on psutil. - Added tons of new examples in ``docs/recipes.rst``. - Drastically improved :func:`virtual_memory()` doc, which is now more detailed, and includes a table with all the available metrics on each diff --git a/docs/index.rst b/docs/index.rst index f6684324bf..eb2a54f169 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,24 +6,21 @@ Psutil documentation ==================== -| |downloads| |stars| |forks| |contributors| +.. image:: https://img.shields.io/badge/GitHub-repo-blue + :target: https://github.com/giampaolo/psutil + :alt: GitHub repo -.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg - :target: https://clickpy.clickhouse.com/dashboard/psutil - :alt: Downloads +.. image:: https://img.shields.io/badge/GitHub-issues-blue + :target: https://github.com/giampaolo/psutil/issues + :alt: GitHub issues -.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/stargazers - :alt: Github stars - -.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/network/members - :alt: Github forks - -.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/graphs/contributors - :alt: Contributors +.. image:: https://readthedocs.org/projects/psutil/badge/?version=latest + :target: https://app.readthedocs.org/projects/psutil/builds/ + :alt: ReadTheDocs +.. image:: https://img.shields.io/pypi/v/psutil.svg?label=Latest%20version&color=red + :target: https://pypi.org/project/psutil + :alt: Latest version psutil (python system and process utilities) is a cross-platform library for retrieving information on running @@ -33,7 +30,7 @@ It is useful mainly for **system monitoring**, **profiling**, **limiting process resources** and the **management of running processes**. It implements many functionalities offered by UNIX command line tools such as: *ps, top, lsof, netstat, ifconfig, who, df, kill, free, nice, -ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap*. +ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap* and others. psutil currently supports the following platforms, from **Python 3.7** onwards: - **Linux** @@ -43,6 +40,9 @@ psutil currently supports the following platforms, from **Python 3.7** onwards: - **Sun Solaris** - **AIX** +psutil is used by `many notable projects `__ including +TensorFlow, PyTorch, Home Assistant, Ansible, and Celery. + Sponsors -------- @@ -101,6 +101,7 @@ Table of Contents API Reference Recipes Platform support + Who uses psutil Development guide FAQs Timeline diff --git a/docs/install.rst b/docs/install.rst index d7b1a037b4..90ebe6063e 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -4,16 +4,16 @@ Install psutil Linux, Windows, macOS (wheels) ------------------------------ -Pre-compiled wheels are distributed for these platforms, so you won't have to -install a C compiler. All you have to do is:: +Pre-compiled wheels are distributed for these platforms, so you usually won't +need a C compiler. All you have to do is:: pip install psutil If wheels are not available for your platform or architecture, or you wish to build & install psutil from sources, keep reading. -Compile psutil from sources ---------------------------- +Compile psutil from source +-------------------------- UNIX ^^^^ @@ -25,7 +25,7 @@ from sources. You can invoke this script from the Makefile as:: make install-sysdeps -After system deps are installed, you can compile & install psutil with:: +After system deps are installed, you can compile and install psutil with:: make build make install @@ -39,7 +39,7 @@ Linux Debian / Ubuntu:: - sudo apt-get install gcc python3-dev + sudo apt install gcc python3-dev pip install --no-binary :all: psutil RedHat / CentOS:: @@ -49,7 +49,7 @@ RedHat / CentOS:: Arch:: - sudo pacman -S cmake gcc python + sudo pacman -S gcc python pip install --no-binary :all: psutil Alpine:: @@ -61,7 +61,7 @@ Windows ^^^^^^^ - To build or install psutil from source on Windows, you need to have - `Visual Studio 2017 `_. + `Visual Studio 2017 `_ or later installed. For detailed instructions, see the `CPython Developer Guide `_. - MinGW is not supported for building psutil on Windows. @@ -69,7 +69,7 @@ Windows pip install --no-binary :all: psutil -- If you want to clone psutil's GIT repository and build / develop locally, +- If you want to clone psutil's Git repository and build / develop locally, first install: `Git for Windows `_ and launch a Git Bash shell. This provides a Unix-like environment where ``make`` works. diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index ae1fed8daa..6f7844481d 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -4,8 +4,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Remove raw HTML from README.rst to make it compatible with PyPI on -dist upload. +"""Strip raw HTML and other unsupported parts from README.rst +so it renders correctly on PyPI when uploading a new release. """ import argparse @@ -16,10 +16,9 @@ =========== - `Home page `_ -- `Documentation `_ +- `Documentation `_ +- `Who uses psutil `_ - `Download `_ -- `Forum `_ -- `StackOverflow `_ - `Blog `_ - `What's new `_ """ @@ -29,29 +28,29 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('file', type=str) args = parser.parse_args() + + lines = [] + with open(args.file) as f: - text = f.read() + excluding = False + for line in f: + # Exclude sections which are not meant to be rendered on PYPI + if line.startswith(".. "): + excluding = True + continue + if line.startswith(".. "): + excluding = False + continue + if not excluding: + lines.append(line) + + text = "".join(lines) # Rewrite summary text = re.sub( r".. raw:: html\n+\s+
    ", quick_links, text ) - # Remove "Sponsors" section - pattern = re.compile( - r"^Sponsors\n=+\n.*?^Example usages\n=+", - re.DOTALL | re.MULTILINE, - ) - text = pattern.sub("Example usages\n==============", text) - - # Remove "Supporters" section - text = re.sub( - r"^Supporters\n=+\n[\s\S]*\Z", - "", - text, - flags=re.MULTILINE, - ) - print(text) diff --git a/scripts/internal/find_adopters.py b/scripts/internal/find_adopters.py new file mode 100755 index 0000000000..c4b60d0900 --- /dev/null +++ b/scripts/internal/find_adopters.py @@ -0,0 +1,799 @@ +#!/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. + +r"""Search GitHub for notable projects that use a given project +as a dependency. + +How it works: + +1. Enumerate all popular Python repos on GitHub via GraphQL + (paginated, >=MIN_STARS). +2. Batch-fetch data needed for the requested filters + (README content, dependency files). +3. Apply filters. Only repos passing ALL specified filters + are confirmed. Available filters: + - --inreadme : PROJECT mentioned in the README + - --indeps : PROJECT mentioned in dep files + (pyproject.toml, setup.py, setup.cfg, requirements*.txt) + +At least one filter must be specified. + +Output is RsT formatted, ready to paste into docs/adoption.rst. + +Usage: + python3 scripts/internal/find_adopters.py \ + --project=psutil \ + --token=~/.github.api.key \ + --skip-file-urls=docs/adoption.rst \ + --min-stars=10000 --indeps +""" + +import argparse +import os +import pickle +import re +import sys +import time + +import requests + +from psutil._common import hilite +from psutil._common import print_color + +GITHUB_GRAPHQL = "https://api.github.com/graphql" +_CACHE_FILE = ".find_adopters.cache" + +# Set by parse_cli(). +PROJECT = "" +MIN_STARS = 0 +MAX_STARS = 0 +TOKEN = "" +SKIP = set() +INREADME = False +INDEPS = False +NO_CACHE = False + +# Fixed files to check for dependency declarations. +_FIXED_DEP_FILES = { + "pyproject.toml": "pyprojectToml", + "setup.py": "setupPy", + "setup.cfg": "setupCfg", + "requirements.txt": "requirementsTxt", +} + +# Max repos to batch in a single GraphQL query. +_BATCH_SIZE = 5 + + +green = lambda msg: hilite(msg, color="green") # noqa: E731 +yellow = lambda msg: hilite(msg, color="yellow") # noqa: E731 + + +def stderr(msg="", color=None): + if color: + print_color(msg, color=color, file=sys.stderr) + else: + print(msg, file=sys.stderr) + + +def graphql(session, query, variables=None): + """Execute a GraphQL query. Returns the 'data' dict.""" + payload = {"query": query} + if variables: + payload["variables"] = variables + try: + resp = session.post(GITHUB_GRAPHQL, json=payload) + except requests.exceptions.RequestException as err: + stderr(f" GraphQL request error: {err}") + return None + if resp.status_code != 200: + stderr(f" GraphQL HTTP error: {resp.status_code} {resp.text}") + return None + body = resp.json() + if "errors" in body: + for err in body["errors"]: + stderr(f" GraphQL error: {err.get('message', err)}") + return None + return body.get("data") + + +def get_session(token): + s = requests.Session() + s.headers["Authorization"] = f"Bearer {token}" + s.headers["Content-Type"] = "application/json" + return s + + +# --- Enumerate repos --- + + +def enumerate_repos(session): + """Enumerate all Python repos with >=MIN_STARS on GitHub.""" + stars_q = f"stars:>={MIN_STARS}" + if MAX_STARS: + stars_q = f"stars:{MIN_STARS}..{MAX_STARS}" + search_q = f"language:Python {stars_q}" + query = """ + query($q: String!, $first: Int!, $after: String) { + search(query: $q, type: REPOSITORY, + first: $first, after: $after) { + repositoryCount + edges { + node { + ... on Repository { + nameWithOwner + owner { login } + name + url + description + stargazerCount + isArchived + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + results = [] + cursor = None + page = 0 + while True: + page += 1 + variables = { + "q": search_q, + "first": 100, + "after": cursor, + } + data = graphql(session, query, variables) + if data is None: + break + search_data = data["search"] + edges = search_data["edges"] + if not edges: + break + for edge in edges: + node = edge["node"] + if not node: + continue + results.append({ + "full_name": node["nameWithOwner"], + "owner": node["owner"]["login"], + "repo": node["name"], + "stars": node["stargazerCount"], + "description": node.get("description") or "", + "html_url": node["url"], + "archived": node["isArchived"], + }) + stderr( + f" page {page}: got {len(edges)} repos " + f"(total so far: {len(results)})" + ) + page_info = search_data["pageInfo"] + if not page_info["hasNextPage"]: + break + cursor = page_info["endCursor"] + time.sleep(1) + return results + + +# --- Fetch README --- + + +def fetch_readmes(session, repos): + """Batch-fetch README content for a list of repos. + + Returns a dict mapping full_name to README text + (or empty string if not found). + """ + result = {} + total = len(repos) + stderr(f" fetching READMEs ({total} repos)...") + for start in range(0, total, _BATCH_SIZE): + batch = repos[start : start + _BATCH_SIZE] + stderr(f" batch {start + 1}-{start + len(batch)}/{total}...") + repo_parts = [] + for i, c in enumerate(batch): + # Try both README.md and README.rst. + repo_parts.append( + f" repo{i}: repository(" + f'owner: "{c["owner"]}", ' + f'name: "{c["repo"]}") {{\n' + f" readmeMd: object(" + f'expression: "HEAD:README.md") {{\n' + f" ... on Blob {{ text }}\n" + f" }}\n" + f" readmeRst: object(" + f'expression: "HEAD:README.rst") {{\n' + f" ... on Blob {{ text }}\n" + f" }}\n" + f" }}" + ) + query = "query {\n" + "\n".join(repo_parts) + "\n}" + data = graphql(session, query) + if data is None: + for c in batch: + result[c["full_name"]] = "" + continue + for i, c in enumerate(batch): + repo_data = data.get(f"repo{i}") + text = "" + if repo_data: + for key in ("readmeMd", "readmeRst"): + obj = repo_data.get(key) + if obj and "text" in obj: + text = obj["text"] + break + result[c["full_name"]] = text + return result + + +# --- Fetch dep files --- + + +def _build_fixed_dep_fragment(): + """Build GraphQL fields for fetching fixed dep files + + requirements/ dir listing. + """ + fields = [] + for dep_file, alias in _FIXED_DEP_FILES.items(): + fields.append( + f" {alias}: object(" + f'expression: "HEAD:{dep_file}") {{\n' + f" ... on Blob {{ text }}\n" + f" }}" + ) + # Also fetch the requirements/ directory listing. + fields.append( + " requirementsDir: object(" + "expression: \"HEAD:requirements\") {\n" # noqa: Q003 + " ... on Tree { entries { name } }\n" + " }" + ) + return "\n".join(fields) + + +def _make_req_alias(filename): + """Turn a requirements/*.txt filename into a GraphQL alias.""" + base = filename.replace(".txt", "") + base = re.sub(r"[^a-zA-Z0-9]", "_", base) + return f"req_{base}" + + +def fetch_dep_files(session, repos): + """Batch-fetch dependency files for a list of repos. + + Phase 1: fetch fixed dep files + requirements/ dir listing. + Phase 2: for repos with a requirements/ dir, fetch all *.txt + files found there. + + Returns a dict mapping full_name to a dict of + {dep_file: content}. + """ + fixed_fragment = _build_fixed_dep_fragment() + result = {} + # Track which repos have requirements/ entries. + req_dir_entries = {} # full_name -> [filename, ...] + + # --- Phase 1: fixed files + dir listing --- + total = len(repos) + stderr( + " phase 1: fixed files + requirements/ dir listing " + f"({total} repos)..." + ) + for start in range(0, total, _BATCH_SIZE): + batch = repos[start : start + _BATCH_SIZE] + stderr(f" batch {start + 1}-{start + len(batch)}/{total}...") + repo_parts = [] + for i, c in enumerate(batch): + repo_parts.append( + f" repo{i}: repository(" + f'owner: "{c["owner"]}", ' + f'name: "{c["repo"]}") {{\n' + f"{fixed_fragment}\n" + f" }}" + ) + query = "query {\n" + "\n".join(repo_parts) + "\n}" + data = graphql(session, query) + if data is None: + for c in batch: + result[c["full_name"]] = {} + continue + for i, c in enumerate(batch): + repo_data = data.get(f"repo{i}") + files = {} + if repo_data: + for dep_file, alias in _FIXED_DEP_FILES.items(): + obj = repo_data.get(alias) + if obj and "text" in obj: + files[dep_file] = obj["text"] + # Check for requirements/ directory. + req_obj = repo_data.get("requirementsDir") + if req_obj and "entries" in req_obj: + txt_files = [ + e["name"] + for e in req_obj["entries"] + if e["name"].endswith(".txt") + ] + if txt_files: + req_dir_entries[c["full_name"]] = txt_files + result[c["full_name"]] = files + + # --- Phase 2: fetch discovered requirements/*.txt files --- + if req_dir_entries: + # Collect repos that need follow-up. + need_fetch = [c for c in repos if c["full_name"] in req_dir_entries] + stderr( + " phase 2: fetching requirements/*.txt from " + f"{len(need_fetch)} repos..." + ) + for start in range(0, len(need_fetch), _BATCH_SIZE): + batch = need_fetch[start : start + _BATCH_SIZE] + repo_parts = [] + for i, c in enumerate(batch): + txt_files = req_dir_entries[c["full_name"]] + file_fields = [] + for fname in txt_files: + alias = _make_req_alias(fname) + path = f"requirements/{fname}" + file_fields.append( + f" {alias}: object(" + f'expression: "HEAD:{path}") {{\n' + f" ... on Blob {{ text }}\n" + f" }}" + ) + repo_parts.append( + f" repo{i}: repository(" + f'owner: "{c["owner"]}", ' + f'name: "{c["repo"]}") {{\n' + + "\n".join(file_fields) + + "\n }" + ) + query = "query {\n" + "\n".join(repo_parts) + "\n}" + data = graphql(session, query) + if data is None: + continue + for i, c in enumerate(batch): + repo_data = data.get(f"repo{i}") + if not repo_data: + continue + txt_files = req_dir_entries[c["full_name"]] + for fname in txt_files: + alias = _make_req_alias(fname) + obj = repo_data.get(alias) + if obj and "text" in obj: + path = f"requirements/{fname}" + result[c["full_name"]][path] = obj["text"] + + return result + + +# --- Classify --- + + +def classify_dependency(file_contents): + """Classify the dependency type from fetched file contents. + + Returns a tuple (status, detail) where status is one of: + - "direct" : PROJECT in install/runtime dependencies + - "build" : PROJECT in build/setup dependencies only + - "test" : PROJECT in test/dev dependencies only + - "optional" : PROJECT in optional/extras dependencies + - "no" : not found in any dependency file + """ + pat = re.escape(PROJECT) + found_in = [] + for dep_file, content in file_contents.items(): + if content is None: + continue + if not re.search(r"\b" + pat + r"\b", content): + continue + found_in.append(dep_file) + + if not found_in: + return "no", "" + + # Classify the dependency type based on which files it was + # found in. + for f in found_in: + content = file_contents[f] + if f == "pyproject.toml": + if re.search( + r"\[project\].*?dependencies\s*=\s*\[.*?" + pat, + content, + re.DOTALL, + ): + return "direct", f + if re.search( + r"\[tool\.poetry\.dependencies\].*?" + pat, + content, + re.DOTALL, + ): + return "direct", f + if re.search( + r"\[build-system\].*?requires\s*=\s*\[.*?" + pat, + content, + re.DOTALL, + ): + return "build", f + if r"optional-dependencies" in content: + return "optional", f + if re.search(r"test|dev", content): + return "test", f + return "direct", f + elif f == "setup.py": + if re.search(r"install_requires.*?" + pat, content, re.DOTALL): + return "direct", f + if re.search(r"setup_requires.*?" + pat, content, re.DOTALL): + return "build", f + if re.search(r"tests_require.*?" + pat, content, re.DOTALL): + return "test", f + if re.search(r"extras_require.*?" + pat, content, re.DOTALL): + return "optional", f + return "direct", f + elif f == "setup.cfg": + if re.search(r"install_requires.*?" + pat, content, re.DOTALL): + return "direct", f + if re.search(r"extras_require.*?" + pat, content, re.DOTALL): + return "optional", f + return "direct", f + elif "requirements" in f: + return "direct", f + + return "direct", ", ".join(found_in) + + +# --- Misc --- + + +def make_subst_name(full_name): + """Turn 'owner/repo' into a substitution-safe base name.""" + name = full_name.split("/")[1] + # Replace underscores and dots with hyphens. + name = re.sub(r"[_.]", "-", name) + return name.lower() + + +def tier_label(stars): + if stars >= 40000: + return 1 + elif stars >= 10000: + return 2 + else: + return 3 + + +def generate_rst(projects): + """Generate RST output for adoption.rst.""" + tiers = {1: [], 2: [], 3: []} + for p in projects: + t = tier_label(p["stars"]) + tiers[t].append(p) + + lines = [] + star_badges = [] + logo_images = [] + + tier_headers = { + 1: "Tier 1 (>40k GitHub stars)", + 2: "Tier 2 (10k-40k GitHub stars)", + 3: "Tier 3 (1k-10k GitHub stars)", + } + + for tier_num in (1, 2, 3): + tier_projects = sorted(tiers[tier_num], key=lambda x: -x["stars"]) + if not tier_projects: + continue + + header = tier_headers[tier_num] + lines.extend([ + header, + "-" * len(header), + "", + ".. list-table::", + " :header-rows: 1", + " :widths: 18 42 12 28", + "", + " * - Project", + " - Description", + " - Stars", + " - Usage", + ]) + + for p in tier_projects: + name = make_subst_name(p["full_name"]) + owner = p["owner"] + repo = p["repo"] + full = p["full_name"] + desc = p["description"] + # Truncate description to fit RST table. + if len(desc) > 60: + desc = desc[:57] + "..." + dep_type = p.get("dep_type", "") + usage = "" + if dep_type == "build": + usage = "build-time dependency" + elif dep_type == "test": + usage = "test dependency" + elif dep_type == "optional": + usage = "optional dependency" + + proj_link = f"|{name}-logo| `{repo} `__" + lines.extend([ + f" * - {proj_link}", + f" - {desc}", + f" - |{name}-stars|", + f" - {usage}", + ]) + + star_badges.append( + f".. |{name}-stars| image:: " + "https://img.shields.io/github/stars/" + f"{full}.svg?style=plastic" + ) + + logo_images.append( + f".. |{name}-logo| image:: " + f"https://github.com/{owner}.png?s=28 :height: 28" + ) + + lines.append("") + + # Combine everything. + output = [] + output.extend(lines) + output.extend([ + "", + ".. Star badges", + "", + ]) + output.extend(star_badges) + output.extend([ + "", + ".. Logo images", + "", + ]) + output.extend(logo_images) + return "\n".join(output) + + +# --- Cache --- + + +def load_cache(): + """Load cached data from disk. + + Returns a dict with keys: min_stars, repos, readmes, dep_files. + Returns None if cache is missing, stale, or --no-cache is set. + The cache is invalidated if the current MIN_STARS is lower + than the min_stars used to build the cache (we'd be missing + repos). + """ + if NO_CACHE: + return None + if not os.path.exists(_CACHE_FILE): + return None + try: + with open(_CACHE_FILE, "rb") as f: + data = pickle.load(f) + except (OSError, pickle.UnpicklingError, EOFError) as err: + stderr(f" cache load error: {err}") + return None + cached_min_stars = data.get("min_stars", 0) + if cached_min_stars > MIN_STARS: + stderr( + f" cache built with min_stars={cached_min_stars}, " + f"but current min_stars={MIN_STARS}; ignoring cache" + ) + return None + stderr( + f" loaded cache ({len(data.get('repos', []))} repos, " + f"min_stars={cached_min_stars})" + ) + return data + + +def save_cache(repos, readmes, dep_files): + """Save fetched data to disk.""" + data = { + "min_stars": MIN_STARS, + "repos": repos, + "readmes": readmes, + "dep_files": dep_files, + } + with open(_CACHE_FILE, "wb") as f: + pickle.dump(data, f) + stderr(f" saved cache to {_CACHE_FILE}") + + +# --- CLI --- + + +def parse_cli(): + """Parse CLI arguments and set global constants.""" + global PROJECT, MIN_STARS, MAX_STARS, TOKEN, SKIP, INREADME, INDEPS, NO_CACHE # noqa: E501 + + parser = argparse.ArgumentParser( + description=( + "Find notable GitHub projects using a given " + "project as a dependency." + ) + ) + parser.add_argument( + "--project", + required=True, + help="Project name to search for (e.g. 'psutil').", + ) + parser.add_argument( + "--min-stars", + type=int, + default=300, + help="Minimum GitHub stars to consider (default: 300).", + ) + parser.add_argument( + "--max-stars", + type=int, + default=0, + help="Maximum GitHub stars (default: no limit).", + ) + parser.add_argument( + "--token", + required=True, + help="Path to a file containing the GitHub token.", + ) + parser.add_argument( + "--skip", + nargs="*", + default=[], + help="Repos URLs to skip.", + ) + parser.add_argument( + "--skip-file-urls", + default=None, + help="Path to file with GitHub repo URLs to skip (found via regex).", + ) + parser.add_argument( + "--inreadme", + action="store_true", + default=False, + help="Filter: PROJECT must be mentioned in README.", + ) + parser.add_argument( + "--indeps", + action="store_true", + default=False, + help=( + "Filter: PROJECT must be mentioned in dep files " + "(pyproject.toml, setup.py, setup.cfg, " + "requirements*.txt)." + ), + ) + parser.add_argument( + "--no-cache", + action="store_true", + default=False, + help="Force fresh fetch, ignoring cached data.", + ) + args = parser.parse_args() + + if not args.inreadme and not args.indeps: + parser.error("at least one of --inreadme, --indeps is required") + + PROJECT = args.project + MIN_STARS = args.min_stars + MAX_STARS = args.max_stars + with open(os.path.expanduser(args.token)) as f: + TOKEN = f.read().strip() + INREADME = args.inreadme + INDEPS = args.indeps + + SKIP = set(args.skip) + SKIP.add("https://github.com/vinta/awesome-python") + NO_CACHE = args.no_cache + if args.skip_file_urls: + path = args.skip_file_urls + with open(path) as f: + text = f.read() + urls = [ + m.group(1) + for m in re.finditer( + r"(https://github\.com/[\w.-]+/[\w.-]+)", text + ) + ] + SKIP.update(urls) + + +# --- Main --- + + +def main(): + parse_cli() + session = get_session(TOKEN) + + cached = load_cache() + if cached is not None: + repos = cached["repos"] + readmes = cached.get("readmes", {}) + dep_files = cached.get("dep_files", {}) + need_save = False + else: + repos = None + readmes = {} + dep_files = {} + need_save = True + + # Step 1: enumerate all popular Python repos. + if repos is None: + stderr(f"Enumerating Python repos (>={MIN_STARS} stars)...") + repos = enumerate_repos(session) + stderr(f"Found {len(repos)} repos.") + + # Filter out skipped and archived repos. + filtered = [] + for repo in repos: + if repo["html_url"] in SKIP: + print(f"skipping {yellow(repo['html_url'])}") + continue + if repo["archived"]: + print(f"skipping {yellow(repo['html_url'])}") + continue + filtered.append(repo) + active_repos = filtered + stderr(f"After skip/archive filtering: {len(active_repos)} repos.") + + # Step 2: fetch data needed for the requested filters. + # Only fetch what's missing from cache. + if INREADME and not readmes: + stderr("Fetching READMEs...") + readmes = fetch_readmes(session, active_repos) + need_save = True + if INDEPS and not dep_files: + stderr("Fetching dependency files...") + dep_files = fetch_dep_files(session, active_repos) + need_save = True + + if need_save: + save_cache(repos, readmes, dep_files) + + # Step 3: apply filters. + confirmed = [] + for repo in active_repos: + name = repo["full_name"] + pat = re.escape(PROJECT) + + if INREADME: + readme = readmes.get(name, "") + if not re.search(r"\b" + pat + r"\b", readme): + continue + + if INDEPS: + files = dep_files.get(name, {}) + status, detail = classify_dependency(files) + if status == "no": + continue + repo["dep_type"] = status + repo["dep_detail"] = detail + + confirmed.append(repo) + + stderr() + stderr(f"Confirmed {len(confirmed)} projects:") + for c in confirmed: + stderr(f" {c['stars']:,} stars: {green(c['html_url'])}") + + # Generate RST. + if confirmed: + ans = input("\nGenerate RsT content? [y/N] ").strip().lower() + if ans in {"y", "yes"}: + rst = generate_rst(confirmed) + print(rst) + + +if __name__ == "__main__": + main() From 9a5a6663ef75f865c9d5140132057b193313bb8d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 13 Mar 2026 15:13:52 +0100 Subject: [PATCH 1584/1714] Fix adoption.rst images --- docs/adoption.rst | 60 +++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/adoption.rst b/docs/adoption.rst index 072bf2ca05..d95fd32b6d 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -3,9 +3,9 @@ Who uses psutil =============== -psutil is one of the `top 100 `_ +psutil is one of the `top 100 `__ most-downloaded packages on PyPI, with 280+ million downloads per month, -`760,000+ GitHub repositories `_ using +`760,000+ GitHub repositories `__ using it, and 14,000+ packages depending on it. The projects below are a small sample of notable software that depends on it. @@ -20,31 +20,31 @@ Infrastructure / automation - Description - Stars - psutil usage - * - |homeassistant-logo| `Home Assistant `_ + * - |homeassistant-logo| `Home Assistant `__ - Open source home automation platform - |homeassistant-stars| - system monitor integration - * - |ansible-logo| `Ansible `_ + * - |ansible-logo| `Ansible `__ - IT automation platform - |ansible-stars| - system fact gathering - * - |airflow-logo| `Apache Airflow `_ + * - |airflow-logo| `Apache Airflow `__ - Workflow orchestration platform - |airflow-stars| - process supervisor, unit testing - * - |celery-logo| `Celery `_ + * - |celery-logo| `Celery `__ - Distributed task queue - |celery-stars| - worker process monitoring, memleak detection - * - |salt-logo| `Salt `_ + * - |salt-logo| `Salt `__ - Infrastructure automation at scale - |salt-stars| - deep system data collection (grains) - * - |dask-logo| `Dask `_ + * - |dask-logo| `Dask `__ - Parallel computing with task scheduling - |dask-stars| - - `metrics dashboard `_, profiling - * - |ajenti-logo| `Ajenti `_ + - `metrics dashboard `__, profiling + * - |ajenti-logo| `Ajenti `__ - Web-based server administration panel - |ajenti-stars| - monitoring plugins, deep integration @@ -60,19 +60,19 @@ AI / Machine learning - Description - Stars - psutil usage - * - |tensorflow-logo| `TensorFlow `_ + * - |tensorflow-logo| `TensorFlow `__ - Open source machine learning framework by Google - |tensorflow-stars| - unit tests - * - |pytorch-logo| `PyTorch `_ + * - |pytorch-logo| `PyTorch `__ - Tensors and dynamic neural networks with GPU acceleration - |pytorch-stars| - benchmark scripts - * - |ray-logo| `Ray `_ + * - |ray-logo| `Ray `__ - AI compute engine with distributed runtime - |ray-stars| - metrics dashboard - * - |mlflow-logo| `MLflow `_ + * - |mlflow-logo| `MLflow `__ - AI/ML engineering platform - |mlflow-stars| - deep system monitoring integration @@ -88,19 +88,19 @@ Developer tools - Description - Stars - psutil usage - * - |sentry-logo| `Sentry `_ + * - |sentry-logo| `Sentry `__ - Error tracking and performance monitoring - |sentry-stars| - send telemetry metrics - * - |locust-logo| `Locust `_ + * - |locust-logo| `Locust `__ - Scalable load testing in Python - |locust-stars| - monitoring of the Locust process - * - |spyder-logo| `Spyder `_ + * - |spyder-logo| `Spyder `__ - Scientific Python IDE - |spyder-stars| - deep integration, UI stats, process management - * - |psleak-logo| `psleak `_ + * - |psleak-logo| `psleak `__ - Test framework to detect memory in Python C extensions - |psleak-stars| - heap process memory (:func:`heap_info()`) @@ -116,43 +116,43 @@ System monitoring - Description - Stars - psutil usage - * - |glances-logo| `Glances `_ + * - |glances-logo| `Glances `__ - System monitoring tool (top/htop alternative) - |glances-stars| - core dependency for all metrics - * - |osquery-logo| `OSQuery `_ + * - |osquery-logo| `OSQuery `__ - SQL powered OS monitoring and analytics - |osquery-stars| - unit tests - * - |bpytop-logo| `bpytop `_ + * - |bpytop-logo| `bpytop `__ - Terminal resource monitor - |bpytop-stars| - core dependency for all metrics - * - |auto-cpufreq-logo| `auto-cpufreq `_ + * - |auto-cpufreq-logo| `auto-cpufreq `__ - Automatic CPU speed and power optimizer for Linux - |auto-cpufreq-stars| - core dependency for CPU monitoring - * - |grr-logo| `GRR `_ + * - |grr-logo| `GRR `__ - Remote live forensics by Google - |grr-stars| - endpoint system data collection, deep integration - * - |stui-logo| `s-tui `_ + * - |stui-logo| `s-tui `__ - Terminal CPU stress and monitoring utility - |stui-stars| - core dependency for metrics - * - |asitop-logo| `asitop `_ + * - |asitop-logo| `asitop `__ - Apple Silicon performance monitoring CLI - |asitop-stars| - core dependency for system metrics - * - |psdash-logo| `psdash `_ + * - |psdash-logo| `psdash `__ - Web dashboard using psutil and Flask - |psdash-stars| - core dependency for all metrics - * - |dd-agent-logo| `dd-agent `_ + * - |dd-agent-logo| `dd-agent `__ - Original monitoring agent by Datadog - |dd-agent-stars| - system metrics collection - * - |ddtrace-logo| `dd-trace-py `_ + * - |ddtrace-logo| `dd-trace-py `__ - Python tracing and profiling library - |ddtrace-stars| - system metrics collection @@ -160,7 +160,7 @@ System monitoring How this list was compiled -------------------------- -- `GitHub dependency graph `_ +- `GitHub dependency graph `__ was used to identify packages and repositories that depend on psutil. - GitHub code search with query "psutil in:readme language:Python", sorted by stars, was used to find additional projects that mention psutil in their @@ -171,7 +171,7 @@ How this list was compiled a passing mention. - Projects were excluded if they only mention psutil in documentation or examples without declaring it as a dependency. -- Star counts are pulled dynamically from `shields.io `_ badges. +- Star counts are pulled dynamically from `shields.io `__ badges. - The final list was manually curated to include notable projects and meaningful usages of psutil across different areas of the Python ecosystem. From ab4acc117582bbc0c10df1b09f0735a1d84fa02e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 14 Mar 2026 21:46:41 +0100 Subject: [PATCH 1585/1714] Doc: create `credits` section with all people who contributed (#2764) CC of people being mentioned in this PR: @abramov-v @adpag @agrethe @akosthekiss @akruis @AlekseyLobanov @alexanha @alexdlaird @alxchk @amanusk @ammaraskar @anders-chrigstrom @AndreLouisCaron @anthonyryan1 @arcivanov @aristocratos @ArminGruner @arnimarj @arossert @ArtyomVancyan @athos-ribeiro @barracuda156 @baruchsiach @bbinet @bdrung @bebound @ben9923 @BoboTiG @bolaum @bsebi @c0m4r @calebjbassi @canonical @ceda-ei @clalancette @clamoriniere @codecov @codingjoe @colesbury @coskundeniz @crusaderky @cuishuang @cvijdea @cybersecgeek @Czaki @daniellockyer @dasumin @davidbrochart @davidkna @dbeer1 @dbwiddis @ddaeschler @ddaeschler' @desbma @dhduvall @dothebart @eallrich @EccoTheFlintstone @elisw93 @emmanuelferdman @erwan-le-pape @ewedlund @fabian @fafanoulele @fbenkstein @felixonmars @florentx @gaborbernat @garrisoncarter @giampaolo @gigi206 @glandium @glebius @gpshead @great-work-told-is @gsauthof @guille @guilt @hexaclock @himanshub16 @href @hroncok @hugovk @hush-hush @iam-py-test @ilius @inarikami @indeedeng @indygreg @jakub-bacic @janderbrain @jandrovins @jdufresne @JeremyGrosser @jhumble @jloden @jmigot-tehtris @johnburnett @jomann09 @josiahcarlson @juju @julien-lebot @justeph @kalekundert @karabijavad @karthikrev @khanzf @kianmeng @klightspeed @koenkooi @kohlerjl @korli @krytarowski @kubernetes @kuzmich321 @kyet @landryb @LEAFERx @lewurm @lgc2333 @li-dan @lysnikolaou @maozguttman @marxin @matray @MaWe2019 @maxbelanger @maxesisn @mayeut @maynk27 @Mehver @mgood @mgorny @mindw @mirbyte @modelrockettier @mpderbec @mrjefftang @msabramo @msarahan @mtelka @mx-psi @myheroyuki @n1000 @nicolargo @nikhilm @nox @nradchenko @odormond @ofek @perrinjerome @peterjeremy @PetrPospisil @phobozad @PierreF @pitrou @prlw1 @Prodesire @PySimpleGUI @qcha0 @ReenigneArcher @ret2libc @roboflow @robusta-dev @ryandesign @ryoon @samertm @sansecio @sasozivanovic @scarabeusiv @scoutapm-sponsorships @shadeyg56 @shanman190 @shawaj @shenxianpeng @sk6249 @smoofra @snom3ad @spacewander @ssaip @stevenwinfield @stswandering @sunpoet @sylvainduchesne @sylvainmouquet @syohex @tijko @timgates42 @tirkarthi @tklauser @Torsten-B @Trash-Nothing @truthbk @u93 @vser1 @wdh @wiggin15 @wj32 @wxwright @xBeAsTx @xiaolingbao @XuehaiPan @yanok @ygingras @YJesus @zooba --- CREDITS | 879 --------------------------------------------- MANIFEST.in | 3 +- Makefile | 2 +- README.rst | 5 +- docs/_links.rst | 1 - docs/changelog.rst | 10 +- docs/credits.rst | 591 ++++++++++++++++++++++++++++++ docs/faq.rst | 20 -- docs/index.rst | 16 +- 9 files changed, 611 insertions(+), 916 deletions(-) delete mode 100644 CREDITS create mode 100644 docs/credits.rst delete mode 100644 docs/faq.rst diff --git a/CREDITS b/CREDITS deleted file mode 100644 index fdd1e755bc..0000000000 --- a/CREDITS +++ /dev/null @@ -1,879 +0,0 @@ -Intro -------------------------------------------------------------------------------- - -I would like to recognize some of the people who have been instrumental in the -development of psutil. I'm sure I'm forgetting somebody (feel free to email me) -but here is a short list. It's modeled after the Linux CREDITS file where the -fields are: name (N), e-mail (E), website (W), country (C), description (D), -(I) issues. Issue tracker is at: -https://github.com/giampaolo/psutil/issues. -A big thanks to all of you. - -- Giampaolo - -Author -------------------------------------------------------------------------------- - -N: Giampaolo Rodola -C: Italy -E: g.rodola@gmail.com -W: https://gmpy.dev - -Experts -------------------------------------------------------------------------------- - -Github usernames of people to CC on github when in need of help. - -- NetBSD: - - 0-wiz-0, Thomas Klausner - - ryoqun, Ryo Onodera -- OpenBSD: - - landryb, Landry Breuil -- FreeBSD: - - glebius, Gleb Smirnoff (#1013) - - sunpoet, Po-Chuan Hsieh (pkg maintainer, #1105) - - kostikbel, Konstantin Belousov (#1105) -- macOS: - - whitlockjc, Jeremy Whitlock -- Windows: - - mrjefftang, Jeff Tang - - wj32, Wen Jia Liu - - fbenkstein, Frank Benkstein -- SunOS: - - wiggin15, Arnon Yaari - - alxchk, Oleksii Shevchuk -- AIX: - - wiggin15, Arnon Yaari (maintainer) -- wheels / packaging / CI matrix: - - mayeut, Matthieu Darbois - -Top contributors -------------------------------------------------------------------------------- - -N: Jay Loden -C: NJ, USA -E: jloden@gmail.com -D: original co-author, initial design/bootstrap and occasional bug fixes -W: http://www.jayloden.com - -N: Arnon Yaari (wiggin15) -W: https://github.com/wiggin15 -D: AIX implementation, expert on multiple fronts -I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214, 1408, - 1329, 1276, 1494, 1528. - -N: Alex Manuskin -W: https://github.com/amanusk -D: FreeBSD cpu_freq(), OSX temperatures, support for Linux temperatures. -I: 1284, 1345, 1350, 1352, 1472, 1481, 1487. - -N: Jeff Tang -W: https://github.com/mrjefftang -I: 340, 529, 616, 653, 654, 648, 641 - -N: Jeremy Whitlock -D: great help with macOS C development. -I: 125, 150, 174, 206 - -N: Landry Breuil -W: https://github.com/landryb -D: OpenBSD implementation. -I: 615 - -N: Justin Venus -D: Solaris support -I: 18 - -N: Thomas Klausner -W: https://github.com/0-wiz-0 -D: NetBSD implementation (co-author). -I: 557, 2128, 2241 - -N: Ryo Onodera -W: https://github.com/ryoon -D: NetBSD implementation (co-author). -I: 557 - -Donations -------------------------------------------------------------------------------- - -N: aristocratos -W: https://github.com/aristocratos - -N: Daniel Widdis -C: Washington, USA -W: https://github.com/dbwiddis - -N: Rodion Stratov -C: Canada - -N: Remi Chateauneu -C: London, UK - -N: Olivier Grisel -C: Paris, France - -N: Praveen Bhamidipati -C: Bellevue, USA - -N: Willem de Groot -C: Netherlands - -N: Sigmund Vik - -N: Kahntent -C: NYC, USA - -N: Gyula Áfra -C: Budapest, Hungary - -N: Mahmut Dumlupinar - -N: Thomas Guettler -C: Germany - -N: Karthik Kumar -C: India - -N: Oche Ejembi -C: UK - -N: Russell Robinson -C: New Zealand - -N: Wompasoft -C: Texas, USA - -N: Amit Kulkarni -C: Santa Clara, USA - -N: Alexander Kaftan -C: Augsburg Germany - -N: Andrew Bays -C: Maynard, USA - -N: Carver Koella -C: Pittsburgh, USA - -N: Kristjan Võrk -C: Tallin, Estonia - -N: HTB Industries -C: Willow Springs, USA - -N: Brett Harris -C: Melbourne, Australia - -N: Peter Friedland -C: CT, USA - -N: Matthew Callow -C: Australia - -N: Marco Schrank -C: Germany - -N: Mindview LLC -C: USA - -N: Григорьев Андрей -C: Russia - -N: Heijdemann Morgan -C: Singapore - -N: Florian Bruhin -C: Winterthur, Switzerland - -N: Heijdemann Morgan -C: Singapore - -N: Morgan Heijdemann -C: Singapore - -Contributors -------------------------------------------------------------------------------- - -N: wj32 -D: process username() and get_connections() on Windows -I: 114, 115 - -N: Yan Raber -C: Bologna, Italy -D: help on Windows development (initial version of Process.username()) - -N: Dave Daeschler -C: USA -W: http://daviddaeschler.com -D: some contributions to initial design/bootstrap plus occasional bug fixing -I: 522, 536 - -N: cjgohlke -D: Windows 64 bit support -I: 107 - -N: Frank Benkstein -D: process environ() -W: https://github.com/fbenkstein -I: 732, 733 - -N: Mozilla Foundation -D: sample code for process USS memory. - -N: EccoTheFlintstone -W: https://github.com/EccoTheFlintstone -I: 1368, 1348 - ----- - -N: Jeffery Kline -I: 130 - -N: Grabriel Monnerat -I: 146 - -N: Philip Roberts -I: 168 - -N: jcscoobyrs -I: 125 - -N: Sandro Tosi -I: 200, 201 - -N: Andrew Colin -I: 248 - -N: Amoser -I: 266, 267, 340 - -N: Matthew Grant -I: 271 - -N: oweidner -I: 275 - -N: Tarek Ziade -I: 281 - -N: Luca Cipriani -C: Turin, Italy -I: 278 - -N: Maciej Lach, -I: 294 - -N: James Pye -I: 305, 306 - -N: Stanchev Emil -I: 314 - -N: Kim Gräsman -D: ...also kindly donated some money. -I: 316 - -N: Riccardo Murri -C: Italy -I: 318 - -N: Florent Xicluna -I: 319 - -N: Michal Spondr -I: 313 - -N: Jean Sebastien -I: 344 - -N: Rob Smith -W: http://www.kormoc.com/ -I: 341 - -N: Youngsik Kim -W: https://plus.google.com/101320747613749824490/ -I: 317 - -N: Gregory Szorc -W: https://plus.google.com/116873264322260110710/posts -I: 323 - -N: André Oriani -I: 361 - -N: clackwell -I: 356 - -N: m.malycha -I: 351 - -N: John Baldwin -I: 370 - -N: Jan Beich -I: 325 - -N: floppymaster -I: 380 - -N: Arfrever.FTA -I: 369, 404 - -N: danudey -I: 386 - -N: Adrien Fallou -I: 224 - -N: Gisle Vanem -I: 411 - -N: thepyr0 -I: 414 - -N: John Pankov -I: 435 - -N: Matt Good -W: http://matt-good.net/ -I: 438 - -N: Ulrich Klank -I: 448 - -N: Josiah Carlson -I: 451, 452 - -N: Raymond Hettinger -D: namedtuple and lru_cache backward compatible implementations. - -N: Jason Kirtland -D: backward compatible implementation of collections.defaultdict. - -M: Ken Seeho -D: @cached_property decorator - -N: crusaderky -I: 470, 477 - -I: 471 - -N: Gautam Singh -I: 466 - -I: 476, 479 - -N: Francois Charron -I: 474 - -N: Naveed Roudsari -I: 421 - -N: Alexander Grothe -I: 497 - -N: Szigeti Gabor Niif -I: 446 - -N: msabramo -I: 492 - -N: Yaolong Huang -W: http://airekans.github.io/ -I: 530 - -N: Anders Chrigström -W: https://github.com/anders-chrigstrom -I: 496 - -N: spacewander -W: https://github.com/spacewander -I: 561, 603 - -N: Sylvain Mouquet -I: 565 - -N: karthikrev -I: 568 - -N: Bruno Binet -I: 572 - -N: Gabi Davar -C: Israel -W: https://github.com/mindw -I: 578, 581, 587 - -N: spacewanderlzx -C: Guangzhou,China -I: 555 - -N: Fabian Groffen -I: 611, 618 - -N: desbma -W: https://github.com/desbma -C: France -I: 628 - -N: John Burnett -W: http://www.johnburnett.com/ -C: Irvine, CA, US -I: 614 - -N: Árni Már Jónsson -I: 634 - -N: Bart van Kleef -W: https://github.com/bkleef -I: 664 - -N: Steven Winfield -W: https://github.com/stevenwinfield -I: 672 - -N: sk6249 -W: https://github.com/sk6249 -I: 670 - -N: maozguttman -W: https://github.com/maozguttman -I: 659 - -N: dasumin -W: https://github.com/dasumin -I: 541 - -N: Mike Sarahan -W: https://github.com/msarahan -I: 688 - -N: Syohei YOSHIDA -W: https://github.com/syohex -I: 730 - -N: Visa Hankala -I: 741 - -N: Sebastian-Gabriel Brestin -C: Romania -I: 704 - -N: Timmy Konick -W: https://github.com/tijko -I: 751 - -N: mpderbec -W: https://github.com/mpderbec -I: 660 - -N: wxwright -W: https://github.com/wxwright -I: 776 - -N: Farhan Khan -I: 823 - -N: Jake Omann -W: https://github.com/jomann09 -I: 816, 775, 1874 - -N: Jeremy Humble -W: https://github.com/jhumble -I: 863 - -N: Ilya Georgievsky -W: https://github.com/xBeAsTx -I: 870 - -N: Yago Jesus -W: https://github.com/YJesus -I: 798 - -N: Andre Caron -C: Montreal, QC, Canada -W: https://github.com/AndreLouisCaron -I: 880 - -N: ewedlund -W: https://github.com/ewedlund -I: 874 - -N: Arcadiy Ivanov -W: https://github.com/arcivanov -I: 919 - -N: Max Bélanger -W: https://github.com/maxbelanger -I: 936, 1133 - -N: Pierre Fersing -C: France -I: 950 - -N: Thiago Borges Abdnur -W: https://github.com/bolaum -I: 959 - -N: Nicolas Hennion -W: https://github.com/nicolargo -I: 974 - -N: Baruch Siach -W: https://github.com/baruchsiach -I: 872 - -N: Danek Duvall -W: https://github.com/dhduvall -I: 1002 - -N: Alexander Hasselhuhn -C: Germany -W: https://github.com/alexanha - -N: Himanshu Shekhar -W: https://github.com/himanshub16 -I: 1036 - -N: Yannick Gingras -W: https://github.com/ygingras -I: 1057 - -N: Gleb Smirnoff -W: https://github.com/glebius -D: good help with FreeBSD -I: 1042, 1079, 1070 - -N: Oleksii Shevchuk -W: https://github.com/alxchk -I: 1077, 1093, 1091, 1220, 1346, 1904 - -N: Prodesire -W: https://github.com/Prodesire -I: 1138 - -N: Sebastian Saip -W: https://github.com/ssaip -I: 1141 - -N: Jakub Bacic -W: https://github.com/jakub-bacic -I: 1127 - -N: Akos Kiss -W: https://github.com/akosthekiss -I: 1150 - -N: Adrian Page -W: https://github.com/adpag -I: 1159, 1160, 1161 - -N: Matthew Long -W: https://github.com/matray -I: 1167 - -N: janderbrain -W: https://github.com/janderbrain -I: 1169 - -N: Dan Vinakovsky -W: https://github.com/hexaclock -I: 1216 - -N: stswandering -W: https://github.com/stswandering -I: 1243 - -N: Georg Sauthoff -W: https://github.com/gsauthof -I: 1193, 1194 - -N: Maxime Mouial -W: https://github.com/hush-hush -I: 1239 - -N: Denis Krienbühl -W: https://github.com/href -I: 1260 - -N: Jean-Luc Migot -W: https://github.com/jmigot-tehtris -I: 1258, 1289 - -N: Nikhil Marathe -W: https://github.com/nikhilm -I: 1278 - -N: Sylvain Duchesne -W: https://github.com/sylvainduchesne -I: 1294 - -N: Lawrence Ye -W: https://github.com/LEAFERx -I: 1321 - -N: Ilya Yanok -W: https://github.com/yanok -I: 1332 - -N: Jaime Fullaondo -W: https://github.com/truthbk -D: AIX support -I: 1320 - -N: Koen Kooi -W: https://github.com/koenkooi -I: 1360 - -N: Ghislain Le Meur -W: https://github.com/gigi206 -D: idea for Process.parents() -I: 1379 - -N: Benjamin Drung -D: make tests invariant to LANG setting -W: https://github.com/bdrung -I: 1462 - -N: Xiaoling Bao -I: 1223 - -N: Cedric Lamoriniere -W: https://github.com/clamoriniere -I: 1470 - -N: Daniel Beer -W: https://github.com/dbeer1 -I: 1471 - -N: Samer Masterson -W: https://github.com/samertm -I: 1480 - -N: Ammar Askar -W: http://ammaraskar.com/ -I: 604, 1484, 1781 - -N: agnewee -W: https://github.com/Agnewee -C: China -I: 1491 - -N: Kamil Rytarowski -W: https://github.com/krytarowski -C: Poland -I: 1526, 1530 - -N: Athos Ribeiro -W: https://github.com/athos-ribeiro -I: 1585 - -N: Erwan Le Pape -W: https://github.com/erwan-le-pape -I: 1570 - -N: Étienne Servais -W: https://github.com/vser1 -I: 1607, 1637 - -N: Bernát Gábor -W: https://github.com/gaborbernat -I: 1565 - -N: Nathan Houghton -W: https://github.com/n1000 -I: 1619 - -N: Riccardo Schirone -W: https://github.com/ret2libc -C: Milano, Italy -I: 1616 - -N: Po-Chuan Hsieh -W: https://github.com/sunpoet -C: Taiwan -I: 1646, 2186 - -N: Javad Karabi -W: https://github.com/karabijavad -I: 1648 - -N: Mike Hommey -W: https://github.com/glandium -I: 1665 - -N: Anselm Kruis -W: https://github.com/akruis -I: 1695 - -N: Michał Górny -W: https://github.com/mgorny -I: 1726 - -N: Julien Lebot -W: https://github.com/julien-lebot -I: 1768 - -N: Armin Gruner -W: https://github.com/ArminGruner -I: 1800 - -N: Chris Burger -W: https://github.com/phobozad -I: 1830 - -N: aristocratos -W: https://github.com/aristocratos -I: 1837, 1838 - -N: Vincent A. Arcila -W: https://github.com/jandrovins -I: 1620, 1727 - -N: Tim Schlueter -W: https://github.com/modelrockettier -I: 1822 - -N: marxin -W: https://github.com/marxin -I: 1851 - -N: guille -W: https://github.com/guille -I: 1913 - -N: David Knaack -W: https://github.com/davidkna -I: 1921 - -N: Nikita Radchenko -W: https://github.com/nradchenko -I: 1940 - -N: MaWe2019 -W: https://github.com/MaWe2019 -I: 1953 - -N: Dmitry Gorbunov -C: Russia -E: gorbunov.dmitry.1999@gmail.com -W: https://gorbunov-dmitry.github.io -D: fix typos in documentation - -N: Pablo Baeyens -W: https://github.com/mx-psi -I: 1598 - -N: Xuehai Pan -W: https://github.com/XuehaiPan -I: 1948, 2264 - -N: Saeed Rasooli -W: https://github.com/ilius -I: 1996 - -N: PetrPospisil -W: https://github.com/PetrPospisil -I: 1980 - -N: Olivier Dormond -W: https://github.com/odormond -I: 1956 - -N: Matthieu Darbois -W: https://github.com/mayeut -I: 2039, 2142, 2147, 2153, 2040, 2102, 2216, 2246, 2252, 2581 - -N: Hugo van Kemenade -W: https://github.com/hugovk -I: 2099 - -N: Torsten Blum -I: 2114 - -N: Chris Lalancette -W: https://github.com/clalancette -I: 2037 - -N: Bernhard Urban-Forster -C: Austria -W: https://github.com/lewurm -I: 2135 - -N: Daniel Li -I: 2150 - -N: Daniel Widdis -W: https://github.com/dbwiddis -I: 2077, 2160 - -N: Amir Rossert -W: https://github.com/arossert -I: 2156, 2345 - -N: Lawrence D'Anna -W: https://github.com/smoofra -I: 2010 - -N: Oliver Tomé -W: https://github.com/snom3ad -I: 2222 - -N: Ryan Carsten Schmidt -W: https://github.com/ryandesign -I: 2361, 2365 - -N: Shade Gladden -W: https://github.com/shadeyg56 -I: 2376 - -N: Anthony Ryan -W: https://github.com/anthonyryan1 -I: 2272 - -N: Sam Gross -W: https://github.com/colesbury -I: 2401, 2427 - -N: Aleksey Lobanov -C: Russia -E: alex_github@likemath.ru -W: https://github.com/AlekseyLobanov - -N: Will Hawes -W: https://github.com/wdh -I: 2496 - -N: Fabien Bousquet -W: https://github.com/fafanoulele -I: 2473 - -N: Jonathan Kohler -W: https://github.com/kohlerjl -I: 2526, 2527 - -N: Marcel Telka -W: https://github.com/mtelka -I: 2545 - -N: Julien Stephan -W: https://github.com/justeph -I: 2560 - -N: Ben Peddell -W: https://github.com/klightspeed -I: 2494 - -N Isaac K. Ko -W: https://github.com/kyet -I: 2604 - -N: Ben Raz -W: https://github.com/ben9923 -I: 2357 - -N: Sergey Fedorov -W: https://github.com/barracuda156 -C: Taiwan -I: 2694 diff --git a/MANIFEST.in b/MANIFEST.in index e70cdbac84..1f01c64050 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,6 @@ include .clang-format include .dprint.jsonc include .gitignore include CONTRIBUTING.md -include CREDITS include HISTORY.rst include INSTALL.rst include LICENSE @@ -28,8 +27,8 @@ include docs/api.rst include docs/auto-build.sh include docs/changelog.rst include docs/conf.py +include docs/credits.rst include docs/devguide.rst -include docs/faq.rst include docs/index.rst include docs/install.rst include docs/platform.rst diff --git a/Makefile b/Makefile index d76bb60d33..393fe6d527 100644 --- a/Makefile +++ b/Makefile @@ -180,7 +180,7 @@ dprint: @$(DPRINT) check lint-rst: ## Run linter for .rst files. - @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml + @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml --log-level ERROR lint-toml: ## Run linter for pyproject.toml. @git ls-files '*.toml' | xargs toml-sort --check diff --git a/README.rst b/README.rst index 5f5e16fe35..45408edf73 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,6 @@ Download    Blog    Funding    - What's new   
    Summary @@ -90,6 +89,8 @@ Supported Python versions are cPython 3.7+ and `PyPy `_. Latest psutil version supporting Python 2.7 is `psutil 6.1.1 `_. +.. + Sponsors ======== @@ -117,6 +118,8 @@ Sponsors add your logo +.. + Funding ======= diff --git a/docs/_links.rst b/docs/_links.rst index ca51ee1876..cf62f6fedf 100644 --- a/docs/_links.rst +++ b/docs/_links.rst @@ -3,7 +3,6 @@ .. _`BPO-6973`: https://bugs.python.org/issue6973 .. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ -.. _`Giampaolo Rodola`: https://gmpy.dev/about .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get .. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt .. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html diff --git a/docs/changelog.rst b/docs/changelog.rst index b9a31f74dd..10328bddeb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,19 +11,21 @@ Changelog Doc: -- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`. Tons of doc - improvements. In order of importance: +- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`. + Tons of doc improvements. In order of importance: - Split the documentation from a single-page HTML document into multiple sections. Sections now include separate pages for API reference, - installation, release timeline, FAQs, and more. + installation, release timeline and more. - Moved 18 years old ``HISTORY.rst`` and ``INSTALL.rst`` files into ``docs/changelog.rst`` and ``docs/install.rst`` for better integration. Original files remain in the project root with a note pointing to the new locations. + - Show a section of notable software that depends on psutil (``/adoption``). + - Show a section of people who contributed or donated to psutil over the + years (``/credits``). - Added a summary table of officially supported operating systems and architectures. - - Show a section of notable software that depends on psutil. - Added tons of new examples in ``docs/recipes.rst``. - Drastically improved :func:`virtual_memory()` doc, which is now more detailed, and includes a table with all the available metrics on each diff --git a/docs/credits.rst b/docs/credits.rst new file mode 100644 index 0000000000..8b350f7d6a --- /dev/null +++ b/docs/credits.rst @@ -0,0 +1,591 @@ +.. currentmodule:: psutil +.. include:: _links.rst + +Credits +======= + +I would like to recognize some of the people who have been instrumental in the +development of psutil. I'm sure I'm forgetting someone (feel free to email me) +but here is a short list. + +A big thanks to all of you. + +— Giampaolo + +Top contributors +---------------- + +* `Giampaolo Rodola`_: creator, primary author and long-time maintainer +* `Jay Loden`_: original co-author, initial design and bootstrap, original + macOS / Windows / FreeBSD implementations +* `Arnon Yaari`_: AIX implementation +* `Landry Breuil`_: original OpenBSD implementation +* `Ryo Onodera`_ and `Thomas Klausner`_: original NetBSD implementation + +Donations +--------- + +The following individuals and organizations have supported +psutil development through donations. + +Companies: + +* `Apivoid`_ *(sponsor)* +* `Canonical Juju`_ +* `Canonical Launchpad`_ +* `Canonical`_ +* `Codecov`_ +* `Indeed Engineering`_ +* `Kubernetes`_ +* `Robusta`_ +* `sansec.io`_ *(sponsor)* +* `Sentry`_ +* `Sourcegraph`_ +* `Tidelift`_ *(sponsor)* + +People: + +* `Alex Laird`_ +* Alexander Kaftan +* `Alexey Vazhnov`_ +* Amit Kulkarni +* Andrew Bays +* `Artyom Vancyan`_ +* Brett Harris +* `c0m4r`_ +* Carver Koella +* `Chenyoo Hao`_ +* `Coşkun Deniz`_ +* `cybersecgeek`_ +* `Daniel Widdis`_ +* `Eugenio E Breijo`_ +* `Evan Allrich`_ +* Florian Bruhin +* `great-work-told-is`_ +* Gyula Áfra +* HTB Industries +* `inarikami`_ +* `JeremyGrosser`_ +* `Johannes Maron`_ +* `Jakob P. Liljenberg`_ +* `Karthik Kumar`_ +* Kahntent +* Kristjan Võrk +* Mahmut Dumlupinar +* Marco Schrank +* Matthew Callow +* Mindview LLC +* `Maximilian Wu`_ +* Mehver +* mirko +* Morgan Heijdemann +* Oche Ejembi +* `Ofek Lev`_ +* Olivier Grisel +* Pavan Maddamsetti +* `PySimpleGUI`_ +* Peter Friedland +* Praveen Bhamidipati +* Remi Chateauneu +* `roboflow.com`_ +* Rodion Stratov +* Russell Robinson +* `Sašo Živanović`_ +* `scoutapm-sponsorships`_ +* Sigmund Vik +* `trashnothing.com`_ +* Thomas Guettler +* Willem de Groot +* Wompasoft +* `Valeriy Abramov`_ +* Григорьев Андрей + +Code contributors by year +------------------------- + +.. image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg?label=Total%20contributors&style=flat + :target: https://github.com/giampaolo/psutil/graphs/contributors + :alt: contributors + +2026 +~~~~ + +* `Felix Yan`_ - :gh:`2732` +* `Sergey Fedorov`_ - :gh:`2701` + +2025 +~~~~ + +* `Ben Peddell`_ - :gh:`2495`, :gh:`2568` +* `Ben Raz`_ - :gh:`2643` +* `Eli Wenig`_ - :gh:`2638` +* `Fabien Bousquet`_ - :gh:`2529` +* `Irene Sheen`_ - :gh:`2606` +* `Isaac K. Ko`_ - :gh:`2612` +* `Jonathan Kohler`_ - :gh:`2527` +* `Lysandros Nikolaou`_ - :gh:`2565`, :gh:`2588`, :gh:`2589`, :gh:`2590`, :gh:`2591`, :gh:`2609`, :gh:`2615`, :gh:`2627`, :gh:`2659` (wheels for free-threaded Python) +* `Marcel Telka`_ - :gh:`2469`, :gh:`2545`, :gh:`2546`, :gh:`2592`, :gh:`2594` +* `Matthieu Darbois`_ - :gh:`2503`, :gh:`2581` (Windows ARM64 wheels) +* `Sergey Fedorov`_ - :gh:`2694` +* `Will Hawes`_ - :gh:`2496` +* `Xianpeng Shen`_ - :gh:`2640` + +2024 +~~~~ + +* `Aleksey Lobanov`_ - :gh:`2457` +* `Cristian Vîjdea`_ - :gh:`2442` +* `Matthieu Darbois`_ - :gh:`2370`, :gh:`2375`, :gh:`2417`, :gh:`2425`, :gh:`2429`, :gh:`2450`, :gh:`2479`, :gh:`2486` (macOS and Linux ARM64 wheels) +* `Mayank Jha`_ - :gh:`2379` +* `Oliver Tomé`_ - :gh:`2222` +* `Ryan Carsten Schmidt`_ - :gh:`2361`, :gh:`2364`, :gh:`2365` +* `Sam Gross`_ - :gh:`2401`, :gh:`2402`, :gh:`2427`, :gh:`2428` (free-threading Python) +* `Shade Gladden`_ - :gh:`2376` + +2023 +~~~~ + +* `Amir Rossert`_ - :gh:`2346` +* `Matthieu Darbois`_ - :gh:`2211`, :gh:`2216`, :gh:`2246`, :gh:`2247`, :gh:`2252`, :gh:`2269`, :gh:`2270`, :gh:`2315` +* `Po-Chuan Hsieh`_ - :gh:`2186`, :gh:`1646` +* `Thomas Klausner`_ - :gh:`2241` +* `Xuehai Pan`_ - :gh:`2266` + +2022 +~~~~ + +* `Amir Rossert`_ - :gh:`2156`, :gh:`2345` +* `Bernhard Urban-Forster`_ - :gh:`2135` +* `Chris Lalancette`_ - :gh:`2037` (:func:`net_if_stats()` flags arg on POSIX) +* `Daniel Li`_ - :gh:`2150` +* `Daniel Widdis`_ - :gh:`2077`, :gh:`2160` +* `Garrison Carter`_ - :gh:`2096` +* `Hiroyuki Tanaka`_ - :gh:`2086` +* `Hugo van Kemenade`_ - :gh:`2099` (Drop Python 2.6 support) +* `Lawrence D'Anna`_ - :gh:`2010` +* `Matthieu Darbois`_ - :gh:`1954`, :gh:`2021`, :gh:`2039`, :gh:`2040`, :gh:`2102`, :gh:`2111`, :gh:`2142`, :gh:`2145`, :gh:`2146`, :gh:`2147`, :gh:`2153`, :gh:`2155`, :gh:`2168` +* `Steve Dower`_ - :gh:`2080` +* `Thomas Klausner`_ - :gh:`2088`, :gh:`2128` +* Torsten Blum - :gh:`2114` + +2021 +~~~~ + +* `David Knaack`_ - :gh:`1921` +* `Guillermo`_ - :gh:`1913` +* `Martin Liška`_ - :gh:`1851` +* `MaWe2019`_ - :gh:`1953` +* `Nikita Radchenko`_ - :gh:`1940` +* `Oleksii Shevchuk`_ - :gh:`1904` +* `Olivier Dormond`_ - :gh:`1956` +* `Pablo Baeyens`_ - :gh:`1598` +* `PetrPospisil`_ - :gh:`1980` +* `Saeed Rasooli`_ - :gh:`1996` +* `Wilfried Goesgens`_ - :gh:`1990` +* `Xuehai Pan`_ - :gh:`1949` + +2020 +~~~~ + +* `Anselm Kruis`_ - :gh:`1695` +* `Armin Gruner`_ - :gh:`1800` (:meth:`Process.environ()` on BSD) +* `Chris Burger`_ - :gh:`1830` +* `vser1`_ - :gh:`1637` +* `Grzegorz Bokota`_ - :gh:`1758`, :gh:`1762` +* `Jake Omann`_ - :gh:`1876` +* `Jakob P. Liljenberg`_ - :gh:`1837`, :gh:`1838` +* `Javad Karabi`_ - :gh:`1648` +* `Julien Lebot`_ - :gh:`1768` (Windows Nano server support) +* `Michał Górny`_ - :gh:`1726` +* `Mike Hommey`_ - :gh:`1665` +* `Po-Chuan Hsieh`_ - :gh:`1646` +* `Riccardo Schirone`_ - :gh:`1616` +* `Tim Schlueter`_ - :gh:`1708` +* `Vincent A. Arcila`_ - :gh:`1620`, :gh:`1727` + +2019 +~~~~ + +* `qcha0`_ - :gh:`1491` +* `Alex Manuskin`_ - :gh:`1487` +* `Ammar Askar`_ - :gh:`1485` (:func:`getloadavg()` on Windows) +* `Arnon Yaari`_ - :gh:`607`, :gh:`1349`, :gh:`1409`, :gh:`1500`, :gh:`1505`, :gh:`1507`, :gh:`1533` +* `Athos Ribeiro`_ - :gh:`1585` +* `Benjamin Drung`_ - :gh:`1462` +* `Bernát Gábor`_ - :gh:`1565` +* `Cedric Lamoriniere`_ - :gh:`1470` +* `Daniel Beer`_ - :gh:`1471` +* `David Brochart`_ - :gh:`1493`, :gh:`1496` +* `EccoTheFlintstone`_ - :gh:`1368`, :gh:`1348` +* `Erwan Le Pape`_ - :gh:`1570` +* `Ghislain Le Meur`_ - :gh:`1379` +* `Kamil Rytarowski`_ - :gh:`1526`, :gh:`1530` (:meth:`Process.cwd()` for NetBSD) +* `Nathan Houghton`_ - :gh:`1619` +* `Samer Masterson`_ - :gh:`1480` +* `Xiaoling Bao`_ - :gh:`1223` +* Mozilla Foundation - Sample code for process USS memory + +2018 +~~~~ + +* `Alex Manuskin`_ - :gh:`1284`, :gh:`1345`, :gh:`1350`, :gh:`1369` (:func:`sensors_temperatures()` for macOS, FreeBSD, Linux) +* `Arnon Yaari`_ - :gh:`1214` +* `Dan Vinakovsky`_ - :gh:`1216` +* `Denis Krienbühl`_ - :gh:`1260` +* `Ilya Yanok`_ - :gh:`1332` +* `janderbrain`_ - :gh:`1169` +* `Jaime Fullaondo`_ - :gh:`1320` +* `Jean-Luc Migot`_ - :gh:`1258`, :gh:`1289` +* `Koen Kooi`_ - :gh:`1360` +* `Lawrence Ye`_ - :gh:`1321` +* `Maxime Mouial`_ - :gh:`1239` +* `Nikhil Marathe`_ - :gh:`1278` +* `stswandering`_ - :gh:`1243` +* `Sylvain Duchesne`_ - :gh:`1294` + +2017 +~~~~ + +* `Adrian Page`_ - :gh:`1160` +* `Akos Kiss`_ - :gh:`1150` +* `Alexander Hasselhuhn`_ - :gh:`1022` +* `Antoine Pitrou`_ - :gh:`1186` +* `Arnon Yaari`_ - :gh:`1130`, :gh:`1137`, :gh:`1145`, :gh:`1156`, :gh:`1164`, :gh:`1174`, :gh:`1177`, :gh:`1123` (AIX implementation) +* `Baruch Siach`_ - :gh:`872` +* `Danek Duvall`_ - :gh:`1002` +* `Gleb Smirnoff`_ - :gh:`1070`, :gh:`1076`, :gh:`1079` +* `Himanshu Shekhar`_ - :gh:`1036` +* `Jakub Bacic`_ - :gh:`1127` +* `Matthew Long`_ - :gh:`1167` +* `Nicolas Hennion`_ - :gh:`974` +* `Oleksii Shevchuk`_ - :gh:`1091`, :gh:`1093`, :gh:`1220`, :gh:`1346` +* `Pierre Fersing`_ - :gh:`950` +* `Sebastian Saip`_ - :gh:`1141` +* `Thiago Borges Abdnur`_ - :gh:`959` +* `Yannick Gingras`_ - :gh:`1057` + +2016 +~~~~ + +* `Andre Caron`_ - :gh:`880` +* `Arcadiy Ivanov`_ - :gh:`919` +* `ewedlund`_ - :gh:`874` +* `Farhan Khan`_ - :gh:`823` +* `Frank Benkstein`_ - :gh:`732`, :gh:`733`, :gh:`736`, :gh:`738`, :gh:`739`, :gh:`740` +* `Ilya Georgievsky`_ - :gh:`870` +* `Jake Omann`_ - :gh:`816`, :gh:`775`, :gh:`1874` +* `Jeremy Humble`_ - :gh:`863` +* `Landry Breuil`_ - :gh:`741` +* `Mark Derbecker`_ - :gh:`660` +* `Max Bélanger`_ - :gh:`936`, :gh:`1133` +* `Patrick Welche`_ - :gh:`812` +* `Syohei YOSHIDA`_ - :gh:`730` +* `Timmy Konick`_ - :gh:`751` +* `Yago Jesus`_ - :gh:`798` + +2015 +~~~~ + +* `Arnon Yaari`_ - :gh:`680`, :gh:`679`, :gh:`610` +* `Bruno Binet`_ - :gh:`572` +* `dasumin`_ - :gh:`541` +* `Fabian Groffen`_ - :gh:`611`, :gh:`618` +* `Gabi Davar`_ - :gh:`578`, :gh:`581`, :gh:`587` +* `Jeff Tang`_ - :gh:`616`, :gh:`648`, :gh:`653`, :gh:`654` +* `John Burnett`_ - :gh:`614` +* `karthik`_ - :gh:`568` +* `Landry Breuil`_ - :gh:`713`, :gh:`709` (OpenBSD implementation) +* `Mike Sarahan`_ - :gh:`690` +* `Sebastian-Gabriel Brestin`_ - :gh:`704` +* `sk6249`_ - :gh:`670` +* `spacewander`_ - :gh:`561`, :gh:`603`, :gh:`555` +* `Steven Winfield`_ - :gh:`672` +* `Sylvain Mouquet`_ - :gh:`565` +* `Árni Már Jónsson`_ - :gh:`634` +* `Ryo Onodera`_: `e124acba `_ (NetBSD implementation) + +2014 +~~~~ + +* `Alexander Grothe`_ - :gh:`497` +* `Anders Chrigström`_ - :gh:`548` +* Francois Charron - :gh:`474` +* `Guido Imperiale`_ - :gh:`470`, :gh:`477` +* `Jeff Tang`_ - :gh:`340`, :gh:`519`, :gh:`529`, :gh:`654` +* `Marc Abramowitz`_ - :gh:`492` +* Naveed Roudsari - :gh:`421` +* `Yaolong Huang`_ - :gh:`530` + +2013 +~~~~ + +* Arfrever.FTA - :gh:`404` +* danudey - :gh:`386` +* Jason Kirtland - backward compatible implementation of collections.defaultdict +* John Baldwin - :gh:`370` +* John Pankov - :gh:`435` +* `Josiah Carlson`_ - :gh:`451`, :gh:`452` +* m.malycha - :gh:`351` +* `Matt Good`_ - :gh:`438` +* `Thomas Klausner`_ - :gh:`557` (NetBSD implementation) +* Ulrich Klank - :gh:`448` + +2012 +~~~~ + +* Amoser - :gh:`266`, :gh:`267`, :gh:`340` +* `Florent Xicluna`_ - :gh:`319` +* `Gregory Szorc`_ - :gh:`323` +* Jan Beich - :gh:`344` +* Youngsik Kim - :gh:`317` + +2011 +~~~~ + +* Jeremy Whitlock - :gh:`125`, :gh:`150`, :gh:`206`, :gh:`217`, :gh:`260` (:func:`net_io_counters()` and :func:`disk_io_counters()` on macOS) + +2010 +~~~~ + +* cjgohlke - :gh:`107` +* `Wen Jia Liu (wj32)`_ - :gh:`114`, :gh:`115` + +2009 +~~~~ + +* Yan Raber: `c861c08b `_ (Windows :func:`cpu_times()`), `15159111 `_ (Windows :meth:`Process.username()`) +* `Jay Loden`_ - `79128baa `_ (first commit of FreeBSD implementation) + +2008 +~~~~ + +* `Jay Loden`_ - `efe9236a `_ (first commit of macOS implementation) +* Dave Daeschler - `71875761 `_ (first commit of Windows implementation) +* `Giampaolo Rodola`_ - `6296c2ab `_ (first commit of Linux implementation) +* `Giampaolo Rodola`_ - `8472a17f `_ (inception / initial directory structure) + +.. People Donors +.. ============================================================================ + +.. _`Alex Laird`: https://github.com/alexdlaird +.. _`Alexey Vazhnov`: https://opencollective.com/alexey-vazhnov +.. _`Jakob P. Liljenberg`: https://github.com/aristocratos +.. _`Artyom Vancyan`: https://github.com/ArtyomVancyan +.. _`c0m4r`: https://github.com/c0m4r +.. _`Chenyoo Hao`: https://opencollective.com/chenyoo-hao +.. _`Coşkun Deniz`: https://github.com/coskundeniz +.. _`cybersecgeek`: https://github.com/cybersecgeek +.. _`Eugenio E Breijo`: https://github.com/u93 +.. _`Evan Allrich`: https://github.com/eallrich +.. _`great-work-told-is`: https://github.com/great-work-told-is +.. _`inarikami`: https://github.com/inarikami +.. _`JeremyGrosser`: https://github.com/JeremyGrosser +.. _`Johannes Maron`: https://github.com/codingjoe +.. _`Karthik Kumar`: https://github.com/guilt +.. _`Maximilian Wu`: https://github.com/maxesisn +.. _`Mehver`: https://github.com/Mehver +.. _`mirko`: https://github.com/mirbyte +.. _`PySimpleGUI`: https://github.com/PySimpleGUI +.. _`roboflow.com`: https://github.com/roboflow +.. _`sansec.io`: https://github.com/sansecio +.. _`Sašo Živanović`: https://github.com/sasozivanovic +.. _`scoutapm-sponsorships`: https://github.com/scoutapm-sponsorships +.. _`trashnothing.com`: https://github.com/Trash-Nothing +.. _`Valeriy Abramov`: https://github.com/abramov-v + +.. Company donors +.. ============================================================================ + +.. _`Apivoid`: https://www.apivoid.com +.. _`Canonical Juju`: https://github.com/juju +.. _`Canonical Launchpad`: https://launchpad.net/ +.. _`Canonical`: https://github.com/canonical +.. _`Codecov`: https://github.com/codecov +.. _`Kubernetes`: https://github.com/kubernetes/kubernetes +.. _`Indeed Engineering`: https://github.com/indeedeng +.. _`Robusta`: https://github.com/robusta-dev +.. _`Sentry`: https://sentry.io/ +.. _`Sourcegraph`: https://sourcegraph.com/ +.. _`Tidelift`: https://tidelift.com + +.. Code contributors +.. ============================================================================ + +.. _`Aaron Shaw`: https://github.com/shawaj +.. _`Adrian Page`: https://github.com/adpag +.. _`qcha0`: https://github.com/qcha0 +.. _`Akos Kiss`: https://github.com/akosthekiss +.. _`Aleksey Lobanov`: https://github.com/AlekseyLobanov +.. _`Alex Manuskin`: https://github.com/amanusk +.. _`Alexander Grothe`: https://github.com/agrethe +.. _`Alexander Hasselhuhn`: https://github.com/alexanha +.. _`Amir Rossert`: https://github.com/arossert +.. _`Ammar Askar`: https://github.com/ammaraskar +.. _`Anders Chrigström`: https://github.com/anders-chrigstrom +.. _`Andre Caron`: https://github.com/AndreLouisCaron +.. _`Anselm Kruis`: https://github.com/akruis +.. _`Anthony Ramine`: https://github.com/nox +.. _`Anthony Ryan`: https://github.com/anthonyryan1 +.. _`Antoine Pitrou`: https://github.com/pitrou +.. _`Arcadiy Ivanov`: https://github.com/arcivanov +.. _`Armin Gruner`: https://github.com/ArminGruner +.. _`Arnon Yaari`: https://github.com/wiggin15 +.. _`Athos Ribeiro`: https://github.com/athos-ribeiro +.. _`Baruch Siach`: https://github.com/baruchsiach +.. _`Ben Peddell`: https://github.com/klightspeed +.. _`Ben Raz`: https://github.com/ben9923 +.. _`Benjamin Drung`: https://github.com/bdrung +.. _`Bernhard Urban-Forster`: https://github.com/lewurm +.. _`Bernát Gábor`: https://github.com/gaborbernat +.. _`Bruno Binet`: https://github.com/bbinet +.. _`Caleb Bassi`: https://github.com/calebjbassi +.. _`Cedric Lamoriniere`: https://github.com/clamoriniere +.. _`Chris Burger`: https://github.com/phobozad +.. _`Chris Lalancette`: https://github.com/clalancette +.. _`Cristian Vîjdea`: https://github.com/cvijdea +.. _`cui fliter`: https://github.com/cuishuang +.. _`Dan Vinakovsky`: https://github.com/hexaclock +.. _`Danek Duvall`: https://github.com/dhduvall +.. _`Daniel Beer`: https://github.com/dbeer1 +.. _`Daniel Li`: https://github.com/li-dan +.. _`Daniel Lockyer`: https://github.com/daniellockyer +.. _`Daniel Widdis`: https://github.com/dbwiddis +.. _`dasumin`: https://github.com/dasumin +.. _`David Brochart`: https://github.com/davidbrochart +.. _`David Knaack`: https://github.com/davidkna +.. _`Denis Krienbühl`: https://github.com/href +.. _`desbma`: https://github.com/desbma +.. _`EccoTheFlintstone`: https://github.com/EccoTheFlintstone +.. _`Eli Wenig`: https://github.com/elisw93 +.. _`Emmanuel Ferdman`: https://github.com/emmanuelferdman +.. _`Erwan Le Pape`: https://github.com/erwan-le-pape +.. _`ewedlund`: https://github.com/ewedlund +.. _`Fabian Groffen`: https://github.com/fabian +.. _`Fabien Bousquet`: https://github.com/fafanoulele +.. _`Farhan Khan`: https://github.com/khanzf +.. _`Felix Yan`: https://github.com/felixonmars +.. _`Florent Xicluna`: https://github.com/florentx +.. _`Frank Benkstein`: https://github.com/fbenkstein +.. _`Gabi Davar`: https://github.com/mindw +.. _`Garrison Carter`: https://github.com/garrisoncarter +.. _`Georg Sauthoff`: https://github.com/gsauthof +.. _`Ghislain Le Meur`: https://github.com/gigi206 +.. _`Giampaolo Rodola`: https://github.com/giampaolo +.. _`Gleb Smirnoff`: https://github.com/glebius +.. _`Gregory P. Smith`: https://github.com/gpshead +.. _`Gregory Szorc`: https://github.com/indygreg +.. _`Grzegorz Bokota`: https://github.com/Czaki +.. _`Guido Imperiale`: https://github.com/crusaderky +.. _`Guillermo`: https://github.com/guille +.. _`Hang`: https://github.com/bebound +.. _`Himanshu Shekhar`: https://github.com/himanshub16 +.. _`Hiroyuki Tanaka`: https://github.com/myheroyuki +.. _`Hugo van Kemenade`: https://github.com/hugovk +.. _`iam-py-test`: https://github.com/iam-py-test +.. _`Ilya Georgievsky`: https://github.com/xBeAsTx +.. _`Ilya Yanok`: https://github.com/yanok +.. _`Irene Sheen`: https://github.com/ceda-ei +.. _`Isaac K. Ko`: https://github.com/kyet +.. _`Jaime Fullaondo`: https://github.com/truthbk +.. _`Jake Omann`: https://github.com/jomann09 +.. _`Jakob P. Liljenberg`: https://github.com/aristocratos +.. _`Jakub Bacic`: https://github.com/jakub-bacic +.. _`janderbrain`: https://github.com/janderbrain +.. _`Javad Karabi`: https://github.com/karabijavad +.. _`Jay Loden`: https://github.com/jloden +.. _`Jean-Luc Migot`: https://github.com/jmigot-tehtris +.. _`Jeff Tang`: https://github.com/mrjefftang +.. _`Jeremy Humble`: https://github.com/jhumble +.. _`John Burnett`: https://github.com/johnburnett +.. _`Jon Dufresne`: https://github.com/jdufresne +.. _`Jonathan Kohler`: https://github.com/kohlerjl +.. _`Josiah Carlson`: https://github.com/josiahcarlson +.. _`Julien Lebot`: https://github.com/julien-lebot +.. _`Julien Stephan`: https://github.com/justeph +.. _`Jérome Perrin`: https://github.com/perrinjerome +.. _`Jérôme Duval`: https://github.com/korli +.. _`Kale Kundert`: https://github.com/kalekundert +.. _`Kamil Rytarowski`: https://github.com/krytarowski +.. _`karthik`: https://github.com/karthikrev +.. _`Karthikeyan Singaravelan`: https://github.com/tirkarthi +.. _`Kian-Meng Ang`: https://github.com/kianmeng +.. _`Koen Kooi`: https://github.com/koenkooi +.. _`kuzmich321`: https://github.com/kuzmich321 +.. _`Landry Breuil`: https://github.com/landryb +.. _`Lawrence D'Anna`: https://github.com/smoofra +.. _`Lawrence Ye`: https://github.com/LEAFERx +.. _`lgc2333`: https://github.com/lgc2333 +.. _`Lysandros Nikolaou`: https://github.com/lysnikolaou +.. _`maozguttman`: https://github.com/maozguttman +.. _`Marc Abramowitz`: https://github.com/msabramo +.. _`Marcel Telka`: https://github.com/mtelka +.. _`Mark Derbecker`: https://github.com/mpderbec +.. _`Martin Liška`: https://github.com/marxin +.. _`Matt Good`: https://github.com/mgood +.. _`Matthew Long`: https://github.com/matray +.. _`Matthieu Darbois`: https://github.com/mayeut +.. _`MaWe2019`: https://github.com/MaWe2019 +.. _`Max Bélanger`: https://github.com/maxbelanger +.. _`Maxime Mouial`: https://github.com/hush-hush +.. _`Mayank Jha`: https://github.com/maynk27 +.. _`Michał Górny`: https://github.com/mgorny +.. _`Mickaël Schoentgen`: https://github.com/BoboTiG +.. _`Mike Hommey`: https://github.com/glandium +.. _`Mike Sarahan`: https://github.com/msarahan +.. _`Miro Hrončok`: https://github.com/hroncok +.. _`Nathan Houghton`: https://github.com/n1000 +.. _`Nicolas Hennion`: https://github.com/nicolargo +.. _`Nikhil Marathe`: https://github.com/nikhilm +.. _`Nikita Radchenko`: https://github.com/nradchenko +.. _`Ofek Lev`: https://github.com/ofek +.. _`Oleksii Shevchuk`: https://github.com/alxchk +.. _`Oliver Tomé`: https://github.com/snom3ad +.. _`Olivier Dormond`: https://github.com/odormond +.. _`Pablo Baeyens`: https://github.com/mx-psi +.. _`Patrick Welche`: https://github.com/prlw1 +.. _`Peter Jeremy`: https://github.com/peterjeremy +.. _`PetrPospisil`: https://github.com/PetrPospisil +.. _`Pierre Fersing`: https://github.com/PierreF +.. _`Po-Chuan Hsieh`: https://github.com/sunpoet +.. _`Prodesire`: https://github.com/Prodesire +.. _`ReenigneArcher`: https://github.com/ReenigneArcher +.. _`Riccardo Schirone`: https://github.com/ret2libc +.. _`Ryan Carsten Schmidt`: https://github.com/ryandesign +.. _`Ryo Onodera`: https://github.com/ryoon +.. _`Saeed Rasooli`: https://github.com/ilius +.. _`Sam Gross`: https://github.com/colesbury +.. _`Samer Masterson`: https://github.com/samertm +.. _`Sebastian Saip`: https://github.com/ssaip +.. _`Sebastian-Gabriel Brestin`: https://github.com/bsebi +.. _`Sergey Fedorov`: https://github.com/barracuda156 +.. _`Shade Gladden`: https://github.com/shadeyg56 +.. _`Shannon Pamperl`: https://github.com/shanman190 +.. _`sk6249`: https://github.com/sk6249 +.. _`spacewander`: https://github.com/spacewander +.. _`Steve Dower`: https://github.com/zooba +.. _`Steven Winfield`: https://github.com/stevenwinfield +.. _`stswandering`: https://github.com/stswandering +.. _`Sylvain Duchesne`: https://github.com/sylvainduchesne +.. _`Sylvain Mouquet`: https://github.com/sylvainmouquet +.. _`Syohei YOSHIDA`: https://github.com/syohex +.. _`Thiago Borges Abdnur`: https://github.com/bolaum +.. _`Thomas Klausner`: https://github.com/tklauser +.. _`Tim Gates`: https://github.com/timgates42 +.. _`Tim Schlueter`: https://github.com/modelrockettier +.. _`Timmy Konick`: https://github.com/tijko +.. _`Tomáš Chvátal`: https://github.com/scarabeusiv +.. _`Torsten Blum`: https://github.com/Torsten-B +.. _`Vincent A. Arcila`: https://github.com/jandrovins +.. _`Wen Jia Liu (wj32)`: https://github.com/wj32 +.. _`Wilfried Goesgens`: https://github.com/dothebart +.. _`Will Hawes`: https://github.com/wdh +.. _`wxwright`: https://github.com/wxwright +.. _`Xianpeng Shen`: https://github.com/shenxianpeng +.. _`Xiaoling Bao`: https://github.com/xiaolingbao +.. _`Xuehai Pan`: https://github.com/XuehaiPan +.. _`Yago Jesus`: https://github.com/YJesus +.. _`Yannick Gingras`: https://github.com/ygingras +.. _`Yaolong Huang`: http://airekans.github.io/ +.. _`Árni Már Jónsson`: https://github.com/arnimarj +.. _`vser1`: https://github.com/vser1 +.. _`David Daeschler`: https://github.com/ddaeschler diff --git a/docs/faq.rst b/docs/faq.rst deleted file mode 100644 index 88b447daaf..0000000000 --- a/docs/faq.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. include:: _links.rst - -FAQs -==== - -* Q: Why do I get :class:`AccessDenied` for certain processes? -* A: This may happen when you query processes owned by another user, - especially on macOS (see issue :gh:`883`) and Windows. - Unfortunately there's not much you can do about this except running the - Python process with higher privileges. - On Unix you may run the Python process as root or use the SUID bit - (``ps`` and ``netstat`` does this). - On Windows you may run the Python process as NT AUTHORITY\\SYSTEM or install - the Python script as a Windows service (ProcessHacker does this). - ----- - -* Q: is MinGW supported on Windows? -* A: no, Visual Studio is the only compiler support on Windows (see - :doc:`devguide`). diff --git a/docs/index.rst b/docs/index.rst index eb2a54f169..6bf8898492 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,17 +10,17 @@ Psutil documentation :target: https://github.com/giampaolo/psutil :alt: GitHub repo -.. image:: https://img.shields.io/badge/GitHub-issues-blue - :target: https://github.com/giampaolo/psutil/issues - :alt: GitHub issues - .. image:: https://readthedocs.org/projects/psutil/badge/?version=latest :target: https://app.readthedocs.org/projects/psutil/builds/ :alt: ReadTheDocs -.. image:: https://img.shields.io/pypi/v/psutil.svg?label=Latest%20version&color=red - :target: https://pypi.org/project/psutil - :alt: Latest version +.. image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi&color=red + :target: https://pypi.org/project/psutil + :alt: Latest version + +.. image:: https://img.shields.io/pypi/dm/psutil.svg + :target: https://clickpy.clickhouse.com/dashboard/psutil + :alt: Downloads psutil (python system and process utilities) is a cross-platform library for retrieving information on running @@ -102,8 +102,8 @@ Table of Contents Recipes Platform support Who uses psutil + Credits Development guide - FAQs Timeline .. toctree:: From 245b7403a7dfb27e32cd4f02cf8826fefcba37d1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 03:41:05 +0100 Subject: [PATCH 1586/1714] Add PR script which updates changelog (#2765) Activated when commenting "/changelog" on a PR. This bot will ask Claude to add an entry into docs/changelog.rst based on the changes introduced in the PR, and also add an entry to docs/credits.rst. Requires: a subscription to Claude API + the ANTHROPIC_API_KEY env var to be set via GitHub web interface (Settings -> Secrets & Variables). --- .github/workflows/changelog_bot.py | 507 ++++++++++++++++++++++++++++ .github/workflows/changelog_bot.yml | 73 ++++ .github/workflows/issues.yml | 3 + docs/changelog.rst | 2 + 4 files changed, 585 insertions(+) create mode 100644 .github/workflows/changelog_bot.py create mode 100644 .github/workflows/changelog_bot.yml diff --git a/.github/workflows/changelog_bot.py b/.github/workflows/changelog_bot.py new file mode 100644 index 0000000000..ff89070e17 --- /dev/null +++ b/.github/workflows/changelog_bot.py @@ -0,0 +1,507 @@ +# 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. + +"""Activated when commenting "/changelog" on a PR. + +This bot will ask Claude to add an entry into docs/changelog.rst based +on the changes introduced in the PR, and also add an entry to +docs/credits.rst. + +Requires: + +- A subscription to Claude API +- The "ANTHROPIC_API_KEY" environment variable to be set via GitHub + web interface (Settings -> Secrets & Variables) +""" + +import argparse +import datetime +import json +import os +import re +import sys +import urllib.request + +import anthropic + +# CLI args +PR_NUMBER = None +REPO = None +TOKEN = None + +CHANGELOG_FILE = "docs/changelog.rst" +CREDITS_FILE = "docs/credits.rst" +MAX_DIFF_CHARS = 20_000 +MAX_TOKENS = 1024 + +PROMPT = """\ +You are helping maintain the official changelog for psutil, a Python +system monitoring library. + +Your task is to generate ONE changelog entry describing the user-visible +change introduced by a pull request. Do not generate more than one entry. + +PR #{number}: {title} +Author: @{author} (full name: {author_name}) + +Description: +{body} + +Diff: +{diff} + +ISSUE NUMBER SELECTION + +The changelog should reference the GitHub ISSUE number when one exists, +not the pull request number. + +Determine the correct issue number using these rules: + +1. If the PR title or description references an issue such as: + - "Fixes #1234" + - "Closes #1234" + - "Refs #1234" + - "#1234" + then use that number. + +2. If multiple issues are referenced, choose the primary one most + closely related to the change. + +3. If no issue reference exists, fall back to the PR number. + +The chosen number will be used in the :gh:`...` role. + +STYLE + +- Write concise entries (1-2 sentences max). +- Focus on the user-visible behavior change. +- Avoid implementation details unless relevant. +- Prefer imperative verbs: "fix", "add", "improve", "avoid", "detect". +- Do not repeat the PR title verbatim. +- Do not mention "PR" in the text. +- Wrap lines around ~79 characters. + +CLASSIFICATION + +Classify the change into one of: + +Bug fixes +Enhancements + +Bug fixes include: +- crashes +- incorrect behavior +- race conditions +- compilation failures +- incorrect return values +- memory leaks +- platform regressions + +Enhancements include: +- new features +- performance improvements +- better detection +- improved error handling +- new platform support +- packaging improvements (e.g. wheels) + +PLATFORM TAGS + +If the change is platform specific, add tags immediately after the issue +reference. + +Examples: + +:gh:`1234`, [Linux]: +:gh:`1234`, [Windows]: +:gh:`1234`, [macOS], [BSD]: + +Only add platform tags if the change clearly affects specific OSes. + +RST FORMATTING + +Use Sphinx roles for psutil APIs: + +Functions: +:func:`function_name` + +Methods: +:meth:`Class.method` + +Classes: +:class:`ClassName` + +Exceptions: +:exc:`ExceptionName` + +C functions or identifiers must use double backticks: + +``function_name()`` + +FORMAT + +Each entry must follow this structure: + +- :gh:`ISSUE_NUMBER`: . + +Or with platforms: + +- :gh:`ISSUE_NUMBER`, [Linux]: . + +DESCRIPTION GUIDELINES + +- Describe the user-visible change. +- Mention affected psutil APIs when applicable. +- Avoid mentioning internal helper functions. +- If fixing incorrect behavior, describe the previous issue. +- If fixing a crash or compilation error, state it clearly. + +EXAMPLES + +- :gh:`2708`, [macOS]: :meth:`Process.cmdline()` and + :meth:`Process.environ()` may fail with ``OSError: [Errno 0]``. + They now raise :exc:`AccessDenied` instead. + +- :gh:`2674`, [Windows]: :func:`disk_usage()` could truncate values on + 32-bit systems for drives larger than 4GB. + +- :gh:`2705`, [Linux]: :meth:`Process.wait()` now uses + ``pidfd_open()`` + ``poll()`` for waiting, avoiding busy loops + and improving response times. + +CREDITS + +psutil maintains a list of contributors in docs/credits.rst under +"Code contributors by year". + +Generate a credits entry unless the author is @giampaolo, in which +case set credits_entry to null. + +Use the contributor's full name from their GitHub profile ({author_name}) +unless it is not set, in which case fall back to their username (@{author}). + +The format used in docs/credits.rst is: + +* `Name or username`_ - :gh:`ISSUE_NUMBER` + +Examples: + +* `Sergey Fedorov`_ - :gh:`2701` +* `someuser`_ - :gh:`2710` + +Use the same issue number used in the changelog entry. +""" + +SUBMIT_TOOL = { + "name": "submit", + "description": "Submit the changelog and credits entries.", + "input_schema": { + "type": "object", + "properties": { + "section": { + "type": "string", + "enum": ["Bug fixes", "Enhancements"], + }, + "changelog_entry": { + "type": "string", + "description": "The RST changelog entry.", + }, + "credits_entry": { + "type": ["string", "null"], + "description": ( + "The RST credits line, or null if author is @giampaolo." + ), + }, + }, + "required": ["section", "changelog_entry", "credits_entry"], + }, +} + + +def gh_request(path, accept="application/vnd.github+json"): + url = f"https://api.github.com{path}" + req = urllib.request.Request( + url, + headers={ + "Authorization": f"Bearer {TOKEN}", + "Accept": accept, + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + with urllib.request.urlopen(req) as resp: + return resp.read() + + +def fetch_pr_metadata(): + pr = json.loads(gh_request(f"/repos/{REPO}/pulls/{PR_NUMBER}")) + author = pr["user"]["login"] + return { + "number": pr["number"], + "title": pr["title"], + "body": pr.get("body") or "", + "author": author, + "author_name": pr["user"].get("name") or author, + } + + +def fetch_pr_diff(): + return gh_request( + f"/repos/{REPO}/pulls/{PR_NUMBER}", + accept="application/vnd.github.v3.diff", + ).decode("utf-8", errors="replace") + + +def ask_claude(pr, diff): + prompt = PROMPT.format( + number=pr["number"], + title=pr["title"], + author=pr["author"], + author_name=pr["author_name"], + body=pr["body"], + diff=diff[:MAX_DIFF_CHARS], + ) + client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) + message = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=MAX_TOKENS, + tools=[SUBMIT_TOOL], + tool_choice={"type": "tool", "name": "submit"}, + messages=[{"role": "user", "content": prompt}], + ) + tool_use = next(b for b in message.content if b.type == "tool_use") + return tool_use.input + + +def insert_changelog_entry(section, entry): + with open(CHANGELOG_FILE) as f: + lines = f.readlines() + + version_re = re.compile(r"^(\d+\.\d+\.\d+|X\.X\.X).*$") + version_idx = next( + (i for i, ln in enumerate(lines) if version_re.match(ln.rstrip())), + None, + ) + if version_idx is None: + sys.exit(f"Could not find version block in {CHANGELOG_FILE}") + next_version_idx = next( + ( + i + for i in range(version_idx + 1, len(lines)) + if version_re.match(lines[i].rstrip()) + ), + len(lines), + ) + block = lines[version_idx:next_version_idx] + + # Skip if this issue is already referenced in the version block + gh_ref = re.search(r":gh:`\d+`", entry) + if gh_ref and any(gh_ref.group(0) in ln for ln in block): + print( + f"Changelog entry for {gh_ref.group(0)} already exists, skipping" + ) + return + + header = f"**{section}**" + header_idx = next( + (i for i, ln in enumerate(block) if ln.rstrip() == header), None + ) + if header_idx is None: + insert_at = next( + ( + i + for i, ln in enumerate(block) + if ln.startswith("**") and ln.rstrip() != header + ), + len(block), + ) + new_block = ( + block[:insert_at] + + [f"{header}\n", "\n", f"{entry}\n", "\n"] + + block[insert_at:] + ) + else: + insert_at = header_idx + 1 + if insert_at < len(block) and not block[insert_at].strip(): + insert_at += 1 + new_block = block[:insert_at] + [f"{entry}\n"] + block[insert_at:] + + lines[version_idx:next_version_idx] = new_block + with open(CHANGELOG_FILE, "w") as f: + f.writelines(lines) + + +def update_credits(credits_entry, author, author_name): + """Insert credits entry and link definition into CREDITS_FILE.""" + with open(CREDITS_FILE) as f: + lines = f.readlines() + + year = str(datetime.date.today().year) + year_re = re.compile(r"^\d{4}$") + + def sort_key(e): + m = re.match(r"\*\s+`([^`]+)`_", e.strip()) + return m.group(1).lower() if m else e.strip().lower() + + # Insert year entry + section_idx = next( + ( + i + for i, ln in enumerate(lines) + if ln.rstrip() == "Code contributors by year" + ), + None, + ) + if section_idx is None: + sys.exit( + f"Could not find 'Code contributors by year' in {CREDITS_FILE}" + ) + + year_idx = next( + ( + i + for i in range(section_idx, len(lines)) + if lines[i].rstrip() == year + ), + None, + ) + if year_idx is None: + first_year = next( + ( + i + for i in range(section_idx, len(lines)) + if year_re.match(lines[i].rstrip()) + ), + len(lines), + ) + lines[first_year:first_year] = [ + f"{year}\n", + "~" * len(year) + "\n", + "\n", + f"{credits_entry}\n", + "\n", + ] + else: + year_end = next( + ( + i + for i in range(year_idx + 2, len(lines)) + if year_re.match(lines[i].rstrip()) + ), + len(lines), + ) + new_key = sort_key(credits_entry) + insert_idx = year_end + skip = False + for i in range(year_idx + 2, year_end): + if lines[i].startswith("* "): + k = sort_key(lines[i]) + if k == new_key: + print( + f"Credits entry for {new_key!r} already exists," + " skipping" + ) + skip = True + break + if k > new_key: + insert_idx = i + break + if not skip: + while ( + insert_idx > year_idx + 2 and not lines[insert_idx - 1].strip() + ): + insert_idx -= 1 + lines.insert(insert_idx, f"{credits_entry}\n") + + # Insert link definition if missing + target = f".. _`{author_name}`:" + if not any(ln.startswith(target) for ln in lines): + link_section = next( + ( + i + for i, ln in enumerate(lines) + if ln.rstrip() == ".. Code contributors" + ), + None, + ) + if link_section is None: + sys.exit( + "Could not find code contributors link section in" + f" {CREDITS_FILE}" + ) + definition = f".. _`{author_name}`: https://github.com/{author}\n" + insert_idx = len(lines) + for i in range(link_section, len(lines)): + m = re.match(r"\.\. _`([^`]+)`:", lines[i]) + if m and m.group(1).lower() > author_name.lower(): + insert_idx = i + break + lines.insert(insert_idx, definition) + + with open(CREDITS_FILE, "w") as f: + f.writelines(lines) + + +def post_comment(body): + url = f"https://api.github.com/repos/{REPO}/issues/{PR_NUMBER}/comments" + data = json.dumps({"body": body}).encode() + req = urllib.request.Request( + url, + data=data, + headers={ + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github+json", + "Content-Type": "application/json", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + with urllib.request.urlopen(req): + pass + + +def parse_cli(): + global PR_NUMBER, REPO, TOKEN + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--pr-number", type=int, required=True) + p.add_argument( + "--repo", type=str, required=True, help="e.g. giampaolo/psutil" + ) + p.add_argument("--token", type=str, required=True, help="GitHub token") + args = p.parse_args() + PR_NUMBER = args.pr_number + REPO = args.repo + TOKEN = args.token + + +def main(): + parse_cli() + print(f"Fetching PR #{PR_NUMBER} from {REPO}...") + pr = fetch_pr_metadata() + diff = fetch_pr_diff() + print("Asking Claude for changelog entry...") + result = ask_claude(pr, diff) + section = result["section"] + changelog_entry = result["changelog_entry"] + credits_entry = result["credits_entry"] + print(f"Section: {section}") + print(f"Entry: {changelog_entry}") + insert_changelog_entry(section, changelog_entry) + print(f"Inserted entry into {CHANGELOG_FILE}") + comment = ( + f"`{CHANGELOG_FILE}` entry added under **{section}**:\n\n" + f"```rst\n{changelog_entry}\n```" + ) + if credits_entry: + print(f"Credits: {credits_entry}") + update_credits(credits_entry, pr["author"], pr["author_name"]) + print(f"Updated {CREDITS_FILE}") + comment += ( + f"\n\n`{CREDITS_FILE}` entry added:\n\n" + f"```rst\n{credits_entry}\n```" + ) + post_comment(comment) + print("Posted confirmation comment on PR") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/changelog_bot.yml b/.github/workflows/changelog_bot.yml new file mode 100644 index 0000000000..9a3a163f53 --- /dev/null +++ b/.github/workflows/changelog_bot.yml @@ -0,0 +1,73 @@ +name: changelog-bot + +on: + issue_comment: + types: [created] + +permissions: + contents: write + pull-requests: write + +jobs: + changelog: + if: > + github.event.comment.body == '/changelog' && + github.event.issue.pull_request != null + runs-on: ubuntu-latest + + steps: + - name: Check commenter permissions + id: check-perms + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PERMISSION=$(gh api \ + repos/${{ github.repository }}/collaborators/${{ github.event.comment.user.login }}/permission \ + --jq '.permission') + echo "permission=$PERMISSION" + if [[ "$PERMISSION" != "write" && "$PERMISSION" != "admin" ]]; then + echo "User ${{ github.event.comment.user.login }} does not have write/admin permission" + exit 1 + fi + + - name: Get PR head branch + id: get-branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.issue.number }} + HEAD_BRANCH=$(gh pr view $PR_NUMBER \ + --repo ${{ github.repository }} \ + --json headRefName \ + --jq '.headRefName') + echo "branch=$HEAD_BRANCH" >> "$GITHUB_OUTPUT" + + - name: Checkout PR branch + uses: actions/checkout@v5 + with: + ref: ${{ steps.get-branch.outputs.branch }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: pip install anthropic + + - name: Run changelog bot + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + python .github/workflows/changelog_bot.py \ + --pr-number ${{ github.event.issue.number }} \ + --repo ${{ github.repository }} \ + --token ${{ secrets.GITHUB_TOKEN }} + + - name: Commit and push if changelog changed + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/changelog.rst docs/credits.rst + if git diff --cached --quiet; then + echo "No changes, skipping commit" + else + git commit -m "Update changelog for PR #${{ github.event.issue.number }}" + git push + fi diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 2cfec564d3..ee081e1aec 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -15,6 +15,9 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Install Python uses: actions/setup-python@v6 with: diff --git a/docs/changelog.rst b/docs/changelog.rst index 10328bddeb..17b512ae22 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -97,6 +97,8 @@ Others: ``user, system, idle``. See compatibility notes below. - :gh:`2754`: standardize :func:`sensors_battery()`'s `percent` so that it returns a `float` instead of `int` on all systems, not only Linux. +- :gh:2765: add a PR bot that uses Claude to summarize PR changes and update + changelog.rst and credits.rst when commenting with /changelog. **Bug fixes** From c8a733f522026ddf903fb42faa26274025b363b9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 16:49:10 +0100 Subject: [PATCH 1587/1714] Doc: unify .. versionchanged:: / .. versionadded::, to match Python style --- README.rst | 19 +- docs/_static/css/custom.css | 41 ++++ docs/adoption.rst | 2 +- docs/api.rst | 400 ++++++++++++++++++++---------------- docs/changelog.rst | 2 +- docs/devguide.rst | 8 +- docs/index.rst | 6 +- 7 files changed, 285 insertions(+), 193 deletions(-) diff --git a/README.rst b/README.rst index 45408edf73..398f73591c 100644 --- a/README.rst +++ b/README.rst @@ -70,10 +70,10 @@ Summary ======= psutil (process and system utilities) is a cross-platform library for -retrieving information on **running processes** and **system utilization** +retrieving information about **running processes** and **system utilization** (CPU, memory, disks, network, sensors) in Python. -It is useful mainly for **system monitoring**, **profiling and limiting process -resources** and **management of running processes**. +It is useful mainly for **system monitoring**, **profiling**, **limiting +process resources**, and **managing running processes**. It implements many functionalities offered by classic UNIX command line tools such as *ps, top, iotop, lsof, netstat, ifconfig, free* and others. psutil currently supports the following platforms: @@ -85,10 +85,6 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** -Supported Python versions are cPython 3.7+ and `PyPy `_. -Latest psutil version supporting Python 2.7 is -`psutil 6.1.1 `_. - .. Sponsors @@ -123,7 +119,7 @@ Sponsors Funding ======= -While psutil is free software and will always be, the project would benefit +While psutil is free software and will always remain so, the project would benefit immensely from some funding. psutil is among the `top 100`_ most-downloaded Python packages, and keeping up with bug reports, user support, and ongoing maintenance has become increasingly difficult to sustain as a one-person @@ -141,8 +137,7 @@ Projects using psutil psutil is one of the `top 100`_ most-downloaded packages on PyPI, with 280+ million downloads per month, `760,000+ GitHub repositories `_ using it, and -14,000+ packages depending on it. The projects below are a small sample of -notable software that depends on it. Some notable projects using psutil: +14,000+ packages depending on it. Some notable projects using psutil: - `TensorFlow `_, `PyTorch `_, @@ -162,8 +157,8 @@ notable software that depends on it. Some notable projects using psutil: -Portings -======== +Ports +===== - Go: `gopsutil `_ - C: `cpslib `_ diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 326df029b1..b013743885 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -200,3 +200,44 @@ div.body div.admonition, div.body div.impl-detail { width: 20px !important; vertical-align: middle; } + +/* ================================================================== */ +/* versionadded / versionchanged / deprecated - like python doc */ +/* ================================================================== */ + +div.versionadded, +div.versionchanged, +div.deprecated { + border-left: 3px solid; + padding: 0 1rem; +} + +div.versionadded { + border-left-color: rgb(79, 196, 100); +} + +div.versionchanged { + border-left-color: rgb(244, 227, 76); +} + +div.deprecated { + border-left-color: rgb(244, 76, 78); +} + +div.versionadded .versionmodified { + color: rgb(41, 100, 51); +} + +div.versionchanged .versionmodified { + color: rgb(133, 72, 38); +} + +div.deprecated .versionmodified { + color: rgb(159, 49, 51); +} + +div.tip { + background-color: #dfd !important; + border: 1px solid green !important; + border-radius: 3px !important; +} diff --git a/docs/adoption.rst b/docs/adoption.rst index d95fd32b6d..942b14e291 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -3,7 +3,7 @@ Who uses psutil =============== -psutil is one of the `top 100 `__ +psutil is among the `top 100 `__ most-downloaded packages on PyPI, with 280+ million downloads per month, `760,000+ GitHub repositories `__ using it, and 14,000+ packages depending on it. The projects below are a small diff --git a/docs/api.rst b/docs/api.rst index 16d34ee117..3b830d4859 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,15 +41,14 @@ CPU - **guest_nice** *(Linux)*: time spent running a niced guest (virtual CPU for guest operating systems under the control of the Linux kernel) - - **interrupt** *(Windows)*: time spent for servicing hardware interrupts ( - similar to "irq" on UNIX) + - **interrupt** *(Windows)*: time spent for servicing hardware interrupts + (similar to "irq" on UNIX) - **dpc** *(Windows)*: time spent servicing deferred procedure calls (DPCs); DPCs are interrupts that run at a lower priority than standard interrupts. When *percpu* is ``True`` return a list of named tuples for each logical CPU on the system. - First element of the list refers to first CPU, second element to second CPU - and so on. + The list is ordered by CPU index. The order of the list is consistent across calls. Example output on Linux: @@ -57,7 +56,8 @@ CPU >>> psutil.cpu_times() scputimes(user=17411.7, system=3797.02, idle=51266.57, nice=77.99, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) - .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. + .. versionchanged:: 4.1.0 + added *interrupt* and *dpc* fields on Windows. .. versionchanged:: 8.0.0 ``cpu_times()`` field order was standardized: ``user``, ``system``, @@ -87,8 +87,8 @@ CPU at least ``0.1`` seconds between calls. When *percpu* is ``True`` returns a list of floats representing the utilization as a percentage for each CPU. - First element of the list refers to first CPU, second element to second CPU - and so on. The order of the list is consistent across calls. + The list is ordered by CPU index. The order of the list is consistent across + calls. Internally this function maintains a global map (a dict) where each key is the ID of the calling thread (`threading.get_ident`_). This means it can be called from different threads, at different intervals, and still return @@ -111,7 +111,8 @@ CPU it will return a meaningless ``0.0`` value which you are supposed to ignore. - .. versionchanged:: 5.9.6 function is now thread safe. + .. versionchanged:: 5.9.6 + the function is now thread safe. .. function:: cpu_times_percent(interval=None, percpu=False) @@ -128,10 +129,11 @@ CPU ``None`` it will return a meaningless ``0.0`` value which you are supposed to ignore. - .. versionchanged:: - 4.1.0 two new *interrupt* and *dpc* fields are returned on Windows. + .. versionchanged:: 4.1.0 + two new *interrupt* and *dpc* fields are returned on Windows. - .. versionchanged:: 5.9.6 function is now thread safe. + .. versionchanged:: 5.9.6 + function is now thread safe. .. function:: cpu_count(logical=True) @@ -218,9 +220,11 @@ CPU .. versionadded:: 5.1.0 - .. versionchanged:: 5.5.1 added FreeBSD support. + .. versionchanged:: 5.5.1 + added FreeBSD support. - .. versionchanged:: 5.9.1 added OpenBSD support. + .. versionchanged:: 5.9.1 + added OpenBSD support. .. function:: getloadavg() @@ -232,7 +236,7 @@ CPU background and updates results every 5 seconds, mimicking the UNIX behavior. Thus, on Windows, the first time this is called and for the next 5 seconds it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. - The numbers returned only make sense if related to the number of CPU cores + The numbers returned only make sense when compared to the number of CPU cores installed on the system. So, for instance, a value of `3.14` on a system with 10 logical CPUs means that the system load was 31.4% percent over the last N minutes. @@ -306,7 +310,7 @@ Memory - On Windows, **total**, **used** ("In use"), and **available** match the Task Manager (Performance > Memory tab). - Follows a table showing implementation details. All info on Linux are retrieved from `/proc/meminfo`. + Below is a table showing implementation details. All info on Linux is retrieved from `/proc/meminfo`. .. list-table:: :header-rows: 1 @@ -386,15 +390,17 @@ Memory >>> .. note:: if you just want to know how much physical memory is left in a - cross platform fashion simply rely on **available** and **percent** + cross-platform manner, simply rely on **available** and **percent** fields. .. note:: see `meminfo.py`_ script providing an example on how to convert bytes in a human readable form. - .. versionchanged:: 4.2.0 added *shared* metric on Linux. + .. versionchanged:: 4.2.0 + added *shared* metric on Linux. - .. versionchanged:: 5.4.4 added *slab* metric on Linux. + .. versionchanged:: 5.4.4 + added *slab* metric on Linux. .. function:: swap_memory() @@ -424,8 +430,9 @@ Memory >>> psutil.swap_memory() sswap(total=2097147904L, used=886620160L, free=1210527744L, percent=42.3, sin=1050411008, sout=1906720768) - .. versionchanged:: 5.2.3 on Linux this function relies on /proc fs instead - of sysinfo() syscall so that it can be used in conjunction with + .. versionchanged:: 5.2.3 + on Linux this function relies on /proc fs instead of sysinfo() syscall so + that it can be used in conjunction with :const:`psutil.PROCFS_PATH` in order to retrieve memory info about Linux containers such as Docker and Heroku. @@ -458,9 +465,11 @@ Disks [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] - .. versionchanged:: 5.7.4 added *maxfile* and *maxpath* fields + .. versionchanged:: 5.7.4 + added *maxfile* and *maxpath* fields. - .. versionchanged:: 6.0.0 removed *maxfile* and *maxpath* fields + .. versionchanged:: 6.0.0 + removed *maxfile* and *maxpath* fields. .. function:: disk_usage(path) @@ -486,8 +495,8 @@ Disks it to be. Also note that both 4 values match "df" cmdline utility. - .. versionchanged:: - 4.3.0 *percent* value takes root reserved space into account. + .. versionchanged:: 4.3.0 + *percent* value takes root reserved space into account. .. function:: disk_io_counters(perdisk=False, nowrap=True) @@ -539,16 +548,16 @@ Disks on Windows ``"diskperf -y"`` command may need to be executed first otherwise this function won't find any disk. - .. versionchanged:: - 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new + .. versionchanged:: 5.3.0 + numbers no longer wrap (restart from zero) across calls thanks to new *nowrap* argument. - .. versionchanged:: - 4.0.0 added *busy_time* (Linux, FreeBSD), *read_merged_count* and + .. versionchanged:: 4.0.0 + added *busy_time* (Linux, FreeBSD), *read_merged_count* and *write_merged_count* (Linux) fields. - .. versionchanged:: - 4.0.0 NetBSD no longer has *read_time* and *write_time* fields. + .. versionchanged:: 4.0.0 + NetBSD no longer has *read_time* and *write_time* fields. Network ^^^^^^^ @@ -591,9 +600,9 @@ Network Also see `nettop.py`_ and `ifconfig.py`_ for an example application. - .. versionchanged:: - 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new - *nowrap* argument. + .. versionchanged:: 5.3.0 + numbers no longer wrap (restart from zero) across calls thanks to new + *nowrap* argument. .. function:: net_connections(kind='inet') @@ -684,16 +693,19 @@ Network .. versionadded:: 2.1.0 - .. versionchanged:: 5.3.0 : socket "fd" is now set for real instead of being - ``-1``. + .. versionchanged:: 5.3.0 + socket "fd" is now set for real instead of being ``-1``. - .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. + .. versionchanged:: 5.3.0 + *laddr* and *raddr* are named tuples. - .. versionchanged:: 5.9.5 : OpenBSD: retrieve *laddr* path for AF_UNIX - sockets (before it was an empty string). + .. versionchanged:: 5.9.5 + OpenBSD: retrieve *laddr* path for AF_UNIX sockets (before it was an empty + string). - .. versionchanged:: 8.0.0 *status* field is now a - :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + .. versionchanged:: 8.0.0 + *status* field is now a :class:`psutil.ConnectionStatus` enum member + instead of a plain ``str``. .. function:: net_if_addrs() @@ -739,13 +751,14 @@ Network .. versionadded:: 3.0.0 - .. versionchanged:: 3.2.0 *ptp* field was added. + .. versionchanged:: 3.2.0 + *ptp* field was added. - .. versionchanged:: 4.4.0 added support for *netmask* field on Windows which - is no longer ``None``. + .. versionchanged:: 4.4.0 + added support for *netmask* field on Windows which is no longer ``None``. - .. versionchanged:: 7.0.0 added support for *broadcast* field on Windows - which is no longer ``None``. + .. versionchanged:: 7.0.0 + added support for *broadcast* field on Windows which is no longer ``None``. .. function:: net_if_stats() @@ -758,7 +771,7 @@ Network - **duplex**: the duplex communication type; it can be either :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or :const:`NIC_DUPLEX_UNKNOWN`. - - **speed**: the NIC speed expressed in mega bits (MB), if it can't be + - **speed**: the NIC speed expressed in megabits (Mbps), if it can't be determined (e.g. 'localhost') it will be set to ``0``. - **mtu**: NIC's maximum transmission unit expressed in bytes. - **flags**: a string of comma-separated flags on the interface (may be an empty string). @@ -781,9 +794,11 @@ Network .. versionadded:: 3.0.0 - .. versionchanged:: 5.7.3 `isup` on UNIX also checks whether the NIC is running. + .. versionchanged:: 5.7.3 + `isup` on UNIX also checks whether the NIC is running. - .. versionchanged:: 5.9.3 *flags* field was added on POSIX. + .. versionchanged:: 5.9.3 + *flags* field was added on POSIX. Sensors ^^^^^^^ @@ -823,7 +838,8 @@ Sensors .. versionadded:: 5.1.0 - .. versionchanged:: 5.5.0 added FreeBSD support + .. versionchanged:: 5.5.0 + added FreeBSD support. .. function:: sensors_fans() @@ -876,11 +892,12 @@ Sensors See also `battery.py`_ and `sensors.py`_ for an example application. - Availability: Linux, Windows, FreeBSD + Availability: Linux, Windows, macOS, FreeBSD .. versionadded:: 5.1.0 - .. versionchanged:: 5.4.2 added macOS support + .. versionchanged:: 5.4.2 + added macOS support. ---- @@ -927,8 +944,8 @@ Other system info [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] - .. versionchanged:: - 5.3.0 added "pid" field + .. versionchanged:: 5.3.0 + added "pid" field. ---- @@ -948,8 +965,8 @@ Functions >>> psutil.pids() [1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, ..., 32498] - .. versionchanged:: - 5.6.0 PIDs are returned in sorted order + .. versionchanged:: 5.6.0 + PIDs are returned in sorted order. .. function:: process_iter(attrs=None, ad_value=None) @@ -994,14 +1011,14 @@ Functions >>> psutil.process_iter.cache_clear() - .. versionchanged:: - 5.3.0 added "attrs" and "ad_value" parameters. + .. versionchanged:: 5.3.0 + added "attrs" and "ad_value" parameters. - .. versionchanged:: - 6.0.0 no longer checks whether each yielded process PID has been reused. + .. versionchanged:: 6.0.0 + no longer checks whether each yielded process PID has been reused. - .. versionchanged:: - 6.0.0 added ``psutil.process_iter.cache_clear()`` API. + .. versionchanged:: 6.0.0 + added ``psutil.process_iter.cache_clear()`` API. .. function:: pid_exists(pid) @@ -1094,7 +1111,7 @@ Process class Raise :class:`NoSuchProcess` if *pid* does not exist. On Linux *pid* can also refer to a thread ID (the *id* field returned by :meth:`threads` method). - When accessing methods of this class always be prepared to catch + When calling methods of this class, always be prepared to catch :class:`NoSuchProcess` and :class:`AccessDenied` exceptions. `hash`_ builtin can be used against instances of this class in order to identify a process univocally over time (the hash is determined by mixing @@ -1214,8 +1231,9 @@ Process class .. method:: ppid() - The process parent PID. On Windows the return value is cached after first - call. Not on POSIX because ppid may change if process becomes a zombie + The process parent PID. On Windows the return value is cached after the + first call. On POSIX it is not cached because the ppid may change if the + process becomes a zombie. See also :meth:`parent` and :meth:`parents` methods. .. method:: name() @@ -1237,7 +1255,7 @@ Process class .. method:: cmdline() - The command line this process has been called with as a list of strings. + The command line used to start this process, as a list of strings. The return value is not cached because the cmdline of a process may change. >>> import psutil @@ -1259,9 +1277,15 @@ Process class `other specific circumstances `_). .. versionadded:: 4.0.0 - .. versionchanged:: 5.3.0 added SunOS support - .. versionchanged:: 5.6.3 added AIX support - .. versionchanged:: 5.7.3 added BSD support + + .. versionchanged:: 5.3.0 + added SunOS support. + + .. versionchanged:: 5.6.3 + added AIX support. + + .. versionchanged:: 5.7.3 + added BSD support. .. method:: create_time() @@ -1302,12 +1326,13 @@ Process class >>> list(psutil.Process().as_dict().keys()) ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] - .. versionchanged:: - 3.0.0 *ad_value* is used also when incurring into - :class:`ZombieProcess` exception, not only :class:`AccessDenied` + .. versionchanged:: 3.0.0 + *ad_value* is used also when incurring into :class:`ZombieProcess` + exception, not only :class:`AccessDenied`. - .. versionchanged:: 4.5.0 :meth:`as_dict` is considerably faster thanks - to :meth:`oneshot` context manager. + .. versionchanged:: 4.5.0 + :meth:`as_dict` is considerably faster thanks to :meth:`oneshot` + context manager. .. method:: parent() @@ -1330,8 +1355,9 @@ Process class The returned value is one of the `psutil.STATUS_* <#process-status-constants>`_ constants. - .. versionchanged:: 8.0.0 return value is now a :class:`psutil.ProcessStatus` - enum member instead of a plain ``str``. + .. versionchanged:: 8.0.0 + return value is now a :class:`psutil.ProcessStatus` enum member instead + of a plain ``str``. .. method:: cwd() @@ -1339,7 +1365,8 @@ Process class determined for some internal reason (e.g. system process or directory no longer exists) it may return an empty string. - .. versionchanged:: 5.6.4 added support for NetBSD + .. versionchanged:: 5.6.4 + added support for NetBSD. .. method:: username() @@ -1391,8 +1418,9 @@ Process class >>> p.nice(psutil.HIGH_PRIORITY_CLASS) - .. versionchanged:: 8.0.0 on Windows, return value is now a - :class:`psutil.ProcessPriority` enum member. + .. versionchanged:: 8.0.0 + on Windows, return value is now a :class:`psutil.ProcessPriority` enum + member. .. method:: ionice(ioclass=None, value=None) @@ -1439,10 +1467,11 @@ Process class Availability: Linux, Windows Vista+ - .. versionchanged:: 5.6.2 Windows accepts new ``IOPRIO_*`` constants. + .. versionchanged:: 5.6.2 + Windows accepts new ``IOPRIO_*`` constants. - .. versionchanged:: 8.0.0 *ioclass* is now a - :class:`psutil.ProcessIOPriority` enum member. + .. versionchanged:: 8.0.0 + *ioclass* is now a :class:`psutil.ProcessIOPriority` enum member. .. method:: rlimit(resource, limits=None) @@ -1467,7 +1496,8 @@ Process class Availability: Linux, FreeBSD - .. versionchanged:: 5.7.3 added FreeBSD support + .. versionchanged:: 5.7.3 + added FreeBSD support. .. method:: io_counters() @@ -1511,12 +1541,13 @@ Process class Availability: Linux, BSD, Windows, AIX - .. versionchanged:: 5.2.0 added *read_chars* and *write_chars* on Linux; - added *other_count* and *other_bytes* on Windows. + .. versionchanged:: 5.2.0 + added *read_chars* + *write_chars* on Linux and *other_count* + + *other_bytes* on Windows. .. method:: num_ctx_switches() - The number voluntary and involuntary context switches performed by + The number of voluntary and involuntary context switches performed by this process (cumulative). .. note:: @@ -1524,7 +1555,8 @@ Process class *voluntary* value reflect the total number of context switches (voluntary + involuntary). This is a limitation of the OS. - .. versionchanged:: 5.4.1 added AIX support + .. versionchanged:: 5.4.1 + added AIX support. .. method:: num_fds() @@ -1579,11 +1611,11 @@ Process class >>> sum(p.cpu_times()[:2]) # cumulative, excluding children and iowait 0.70 - .. versionchanged:: - 4.1.0 return two extra fields: *children_user* and *children_system*. + .. versionchanged:: 4.1.0 + return two extra fields: *children_user* and *children_system*. - .. versionchanged:: - 5.6.4 added *iowait* on Linux. + .. versionchanged:: 5.6.4 + added *iowait* on Linux. .. method:: cpu_percent(interval=None) @@ -1595,8 +1627,8 @@ Process class or ``None`` compares process times to system CPU times elapsed since last call, returning immediately. That means the first time this is called it will return a meaningless ``0.0`` value which you are supposed to ignore. - In this case is recommended for accuracy that this function be called a - second time with at least ``0.1`` seconds between calls. + For accuracy, it is recommended to call this function a second time with + at least ``0.1`` seconds between calls. Example: >>> import psutil @@ -1659,9 +1691,11 @@ Process class Availability: Linux, Windows, FreeBSD - .. versionchanged:: 2.2.0 added support for FreeBSD - .. versionchanged:: 5.1.0 an empty list can be passed to set affinity - against all eligible CPUs. + .. versionchanged:: 2.2.0 + added support for FreeBSD. + + .. versionchanged:: 5.1.0 + an empty list can be passed to set affinity against all eligible CPUs. .. method:: cpu_num() @@ -1749,28 +1783,27 @@ Process class >>> p.memory_info() pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, data=9891840) - .. versionchanged:: - 4.0.0 multiple fields are returned, not only *rss* and *vms*. + .. versionchanged:: 4.0.0 + multiple fields are returned, not only *rss* and *vms*. - .. versionchanged:: - 8.0.0 Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). - Deprecated aliases returning 0 and emitting `DeprecationWarning` are - kept. + .. versionchanged:: 8.0.0 + Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated + aliases returning 0 and emitting `DeprecationWarning` are kept. - .. versionchanged:: - 8.0.0 macOS: *pfaults* and *pageins* removed with **no backward-compat - aliases**. Use :meth:`page_faults` instead. + .. versionchanged:: 8.0.0 + macOS: *pfaults* and *pageins* removed with **no backward-compat + aliases**. Use :meth:`page_faults` instead. - .. versionchanged:: - 8.0.0 Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*, - *num_page_faults* → :meth:`page_faults` method. At the same time - *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, *peak_nonpaged_pool* - were moved to :meth:`memory_info_ex`. All these old names still work but - raise `DeprecationWarning`. + .. versionchanged:: 8.0.0 + Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → + *peak_vms*, *num_page_faults* → :meth:`page_faults` method. At the same + time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, + *peak_nonpaged_pool* were moved to :meth:`memory_info_ex`. All these old + names still work but raise `DeprecationWarning`. - .. versionchanged:: - 8.0.0 BSD: added *peak_rss*. + .. versionchanged:: 8.0.0 + BSD: added *peak_rss*. .. warning:: in version 8.0.0 the named tuple changed size and field order. Positional @@ -1886,13 +1919,13 @@ Process class .. method:: memory_full_info() - This method returns the same information as :meth:`memory_info` plus - :meth:`memory_footprint` in a single named tuple. + This deprecated method returns the same information as :meth:`memory_info` + plus :meth:`memory_footprint` in a single named tuple. .. versionadded:: 4.0.0 - .. warning:: - deprecated in version 8.0.0; use :meth:`memory_footprint` instead. + .. deprecated:: 8.0.0 + use :meth:`memory_footprint` instead. .. method:: memory_percent(memtype="rss") @@ -1902,7 +1935,8 @@ Process class :meth:`memory_info_ex`, or :meth:`memory_footprint` and controls which memory value is used in the calculation (defaults to ``"rss"``). - .. versionchanged:: 4.0.0 added `memtype` parameter. + .. versionchanged:: 4.0.0 + added `memtype` parameter. .. method:: memory_maps(grouped=True) @@ -1972,9 +2006,9 @@ Process class Availability: Linux, Windows, FreeBSD, SunOS - .. versionchanged:: - 5.6.0 removed macOS support because inherently broken (see - issue `#1291 `_) + .. versionchanged:: 5.6.0 + removed macOS support because inherently broken (see issue `#1291 + `_) .. method:: children(recursive=False) @@ -1999,7 +2033,7 @@ Process class Note that in the example above if process X disappears process Y won't be returned either as the reference to process A is lost. - This concept is well summaried by this + This concept is well illustrated by this `unit test `_. See also how to `kill a process tree <#kill-process-tree>`_ and `terminate my children <#terminate-my-children>`_. @@ -2068,11 +2102,11 @@ Process class kernel bug, hence it's not reliable (see `issue 595 `_). - .. versionchanged:: - 3.1.0 no longer hangs on Windows. + .. versionchanged:: 3.1.0 + no longer hangs on Windows. - .. versionchanged:: - 4.1.0 new *position*, *mode* and *flags* fields on Linux. + .. versionchanged:: 4.1.0 + new *position*, *mode* and *flags* fields on Linux. .. method:: net_connections(kind="inet") @@ -2161,20 +2195,22 @@ Process class (AIX) :class:`psutil.AccessDenied` is always raised unless running as root (lsof does the same). - .. versionchanged:: 5.3.0 : *laddr* and *raddr* are named tuples. + .. versionchanged:: 5.3.0 + *laddr* and *raddr* are named tuples. - .. versionchanged:: 6.0.0 : method renamed from `connections` to - `net_connections`. + .. versionchanged:: 6.0.0 + method renamed from `connections` to `net_connections`. - .. versionchanged:: 8.0.0 *status* field is now a - :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + .. versionchanged:: 8.0.0 + *status* field is now a :class:`psutil.ConnectionStatus` enum member + instead of a plain ``str``. .. method:: connections() Same as :meth:`net_connections` (deprecated). - .. warning:: - deprecated in version 6.0.0; use :meth:`net_connections` instead. + .. deprecated:: 6.0.0 + use :meth:`net_connections` instead. .. method:: is_running() @@ -2189,9 +2225,9 @@ Process class this will return ``True`` also if the process is a zombie (``p.status() == psutil.STATUS_ZOMBIE``). - .. versionchanged:: 6.0.0 : automatically remove process from - :func:`process_iter()` internal cache if PID has been reused by another - process. + .. versionchanged:: 6.0.0 + automatically remove process from :func:`process_iter()` internal cache + if PID has been reused by another process. .. method:: send_signal(signal) @@ -2203,9 +2239,9 @@ Process class See also how to `kill a process tree <#kill-process-tree>`_ and `terminate my children <#terminate-my-children>`_. - .. versionchanged:: - 3.2.0 support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows - was added. + .. versionchanged:: 3.2.0 + support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows was + added. .. method:: suspend() @@ -2283,18 +2319,20 @@ Process class busy loop (non-blocking call and short sleeps). .. versionchanged:: 5.7.2 - if *timeout* is not ``None``, use efficient event-driven implementation - on Linux >= 5.3 and macOS / BSD. + if *timeout* is not ``None``, use efficient event-driven implementation + on Linux >= 5.3 and macOS / BSD. - .. versionchanged:: 5.7.1 return value is cached (instead of returning - ``None``). + .. versionchanged:: 5.7.1 + return value is cached (instead of returning ``None``). - .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it - as a human readable `enum`_. + .. versionchanged:: 5.7.1 + on POSIX, in case of negative signal, return it as a human readable + `enum`_. - .. versionchanged:: 7.2.2 on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, - use `os.pidfd_open`_ and `select.kqueue`_ respectively, instead of less - efficient busy-loop polling. + .. versionchanged:: 7.2.2 + on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, use `os.pidfd_open`_ and + `select.kqueue`_ respectively, instead of less efficient busy-loop + polling. ---- @@ -2327,7 +2365,8 @@ Popen class 0 >>> - .. versionchanged:: 4.4.0 added context manager support + .. versionchanged:: 4.4.0 + added context manager support. ---- @@ -2348,8 +2387,10 @@ steadily across iterations, the C code is likely retaining memory it should be releasing. This provides an allocator-level way to spot native leaks that Python's memory tracking misses. -Check out `psleak`_ project to see a practical example of how these APIs can be -used to detect memory leaks in C extensions. +.. tip:: + + Check out `psleak`_ project to see a practical example of how these APIs can be + used to detect memory leaks in C extensions. .. function:: heap_info() @@ -2592,14 +2633,16 @@ Operating system constants :const:`WINDOWS` constant will be ``True``, all others will be ``False``. .. versionadded:: 4.0.0 - .. versionchanged:: 5.4.0 added AIX + + .. versionchanged:: 5.4.0 + added AIX. .. data:: OSX Alias for :const:`MACOS`. - .. warning:: - deprecated in version 5.4.7; use :const:`MACOS` instead. + .. deprecated:: 5.4.7 + use :const:`MACOS` instead. Process status constants ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2624,9 +2667,12 @@ Process status constants These constants are members of the :class:`psutil.ProcessStatus` enum. .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) + .. versionadded:: 5.4.7 ``STATUS_PARKED`` (Linux) - .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessStatus` - enum members (were plain strings). + + .. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessStatus` enum members (were plain + strings). Process priority constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2646,8 +2692,9 @@ Process priority constants Availability: Windows - .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessPriority` - enum members (were plain integers). + .. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessPriority` enum members (were plain + integers). .. _const-ioprio: .. data:: IOPRIO_CLASS_NONE @@ -2672,9 +2719,9 @@ Process priority constants Availability: Linux - .. versionchanged:: 8.0.0 constants are now - :class:`psutil.ProcessIOPriority` enum members (previously - ``IOPriority`` enum). + .. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessIOPriority` enum members + (previously ``IOPriority`` enum). .. data:: IOPRIO_VERYLOW .. data:: IOPRIO_LOW @@ -2690,9 +2737,10 @@ Process priority constants Availability: Windows .. versionadded:: 5.6.2 - .. versionchanged:: 8.0.0 constants are now - :class:`psutil.ProcessIOPriority` enum members (previously - ``IOPriority`` enum). + + .. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessIOPriority` enum members + (previously ``IOPriority`` enum). Process resource constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2733,10 +2781,13 @@ These constants are members of the :class:`psutil.ProcessRlimit` enum. Availability: Linux, FreeBSD -.. versionchanged:: 5.7.3 added FreeBSD support, added ``RLIMIT_SWAP``, - ``RLIMIT_SBSIZE``, ``RLIMIT_NPTS``. -.. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessRlimit` - enum members (were plain integers). +.. versionchanged:: 5.7.3 + added FreeBSD support, added ``RLIMIT_SWAP``, ``RLIMIT_SBSIZE``, + ``RLIMIT_NPTS``. + +.. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessRlimit` enum members (were plain + integers). Connections constants ^^^^^^^^^^^^^^^^^^^^^ @@ -2763,8 +2814,9 @@ Connections constants :func:`psutil.net_connections` (`status` field). These constants are members of the :class:`psutil.ConnectionStatus` enum. - .. versionchanged:: 8.0.0 constants are now :class:`psutil.ConnectionStatus` - enum members (were plain strings). + .. versionchanged:: 8.0.0 + constants are now :class:`psutil.ConnectionStatus` enum members (were + plain strings). Hardware constants ^^^^^^^^^^^^^^^^^^ @@ -2819,8 +2871,12 @@ Other constants Availability: Linux, Solaris, AIX .. versionadded:: 3.2.3 - .. versionchanged:: 3.4.2 also available on Solaris. - .. versionchanged:: 5.4.0 also available on AIX. + + .. versionchanged:: 3.4.2 + also available on Solaris. + + .. versionchanged:: 5.4.0 + also available on AIX. .. _const-version-info: .. data:: version_info diff --git a/docs/changelog.rst b/docs/changelog.rst index 17b512ae22..df7f8bbd6c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -97,7 +97,7 @@ Others: ``user, system, idle``. See compatibility notes below. - :gh:`2754`: standardize :func:`sensors_battery()`'s `percent` so that it returns a `float` instead of `int` on all systems, not only Linux. -- :gh:2765: add a PR bot that uses Claude to summarize PR changes and update +- :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update changelog.rst and credits.rst when commenting with /changelog. **Bug fixes** diff --git a/docs/devguide.rst b/docs/devguide.rst index d89213bc5a..5b39d41ec7 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -69,9 +69,9 @@ Debug mode If you want to debug unusual situations or want to report a bug, it may be useful to enable debug mode via ``PSUTIL_DEBUG`` environment variable. In this -mode, psutil may (or may not) print additional information to stderr. Usually -these are error conditions which are not severe, and hence are ignored (instead -of crashing). Unit tests automatically run with debug mode enabled. On UNIX: +mode, psutil may print additional information to stderr. Usually these are +non-severe error conditions that are ignored instead of causing a crash. +Unit tests automatically run with debug mode enabled. On UNIX: :: @@ -155,7 +155,7 @@ Continuous integration ---------------------- Unit tests are automatically run on every ``git push`` on all platforms except -AIX. See config files in `.github/workflows `_ +AIX. See config files in the `.github/workflows `_ directory. Documentation diff --git a/docs/index.rst b/docs/index.rst index 6bf8898492..2ad4f58f93 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,12 +22,12 @@ Psutil documentation :target: https://clickpy.clickhouse.com/dashboard/psutil :alt: Downloads -psutil (python system and process utilities) is a cross-platform library for -retrieving information on running +psutil (Python system and process utilities) is a cross-platform library for +retrieving information about running **processes** and **system utilization** (CPU, memory, disks, network, sensors) in **Python**. It is useful mainly for **system monitoring**, **profiling**, **limiting -process resources** and the **management of running processes**. +process resources**, and **managing running processes**. It implements many functionalities offered by UNIX command line tools such as: *ps, top, lsof, netstat, ifconfig, who, df, kill, free, nice, ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap* and others. From 34388d768db6f26926d34bbd2a17b8a03e30a9fb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 17:52:58 +0100 Subject: [PATCH 1588/1714] Doc: add support for .. availability:: directive Also move docs/auto-build.sh into docs/Makefile --- MANIFEST.in | 2 +- docs/Makefile | 20 ++++---- docs/_ext/availability.py | 105 ++++++++++++++++++++++++++++++++++++++ docs/api.rst | 65 ++++++++++++----------- docs/auto-build.sh | 12 ----- docs/conf.py | 1 + docs/index.rst | 1 + pyproject.toml | 1 + tests/test_posix.py | 2 +- 9 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 docs/_ext/availability.py delete mode 100644 docs/auto-build.sh diff --git a/MANIFEST.in b/MANIFEST.in index 1f01c64050..f68f9d5433 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,6 +14,7 @@ include docs/DEVNOTES include docs/Makefile include docs/README include docs/_ext/add_home_link.py +include docs/_ext/availability.py include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py include docs/_links.rst @@ -24,7 +25,6 @@ include docs/_static/sidebar.js include docs/_templates/layout.html include docs/adoption.rst include docs/api.rst -include docs/auto-build.sh include docs/changelog.rst include docs/conf.py include docs/credits.rst diff --git a/docs/Makefile b/docs/Makefile index 96896b05a8..5355a77259 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,32 +1,32 @@ # Makefile for Sphinx documentation -PYTHON = python3 -SPHINXBUILD = $(PYTHON) -m sphinx -PAPER = -BUILDDIR = _build -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) . +PYTHON = python3 +SPHINXBUILD = $(PYTHON) -m sphinx +BUILDDIR = _build +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees . -clean: +clean: ## Remove all build files rm -rf $(BUILDDIR) html: ## Generate doc in HTML format $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." singlehtml: ## Generate doc as a single HTML page $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." text: ## Generate doc in .txt format $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." +auto-build: ## Rebuild HTML on file changes (requires inotify-tools) + $(MAKE) clean + $(MAKE) html + while inotifywait -r -e modify,create,delete,move .; do $(MAKE) html; done + check-links: ## Check links $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." diff --git a/docs/_ext/availability.py b/docs/_ext/availability.py new file mode 100644 index 0000000000..b923079f6e --- /dev/null +++ b/docs/_ext/availability.py @@ -0,0 +1,105 @@ +# noqa: CPY001 + +# Slightly adapted from CPython's: +# https://github.com/python/cpython/blob/main/Doc/tools/extensions/availability.py +# Copyright (c) PSF +# Licensed under the Python Software Foundation License Version 2. + +"""Support for `.. availability:: …` directive, to document platform +availability. +""" + +from docutils import nodes +from sphinx.locale import _ as sphinx_gettext +from sphinx.util import logging +from sphinx.util.docutils import SphinxDirective + +logger = logging.getLogger(__name__) + +_PLATFORMS = frozenset({ + "AIX", + "BSD", + "FreeBSD", + "Linux", + "Linux with glibc", + "macOS", + "NetBSD", + "OpenBSD", + "POSIX", + "SunOS", + "UNIX", + "Windows", +}) + +_LIBC = frozenset({ + "glibc", + "musl", +}) + +KNOWN_PLATFORMS = _PLATFORMS | _LIBC + + +class Availability(SphinxDirective): + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + + def run(self): + title = sphinx_gettext("Availability") + sep = nodes.Text(": ") + parsed, msgs = self.state.inline_text(self.arguments[0], self.lineno) + pnode = nodes.paragraph( + title, "", nodes.emphasis(title, title), sep, *parsed, *msgs + ) + self.set_source_info(pnode) + cnode = nodes.container("", pnode, classes=["availability"]) + self.set_source_info(cnode) + if self.content: + self.state.nested_parse(self.content, self.content_offset, cnode) + self.parse_platforms() + + return [cnode] + + def parse_platforms(self): + """Parse platform information from arguments + + Arguments is a comma-separated string of platforms. A platform may + be prefixed with "not " to indicate that a feature is not available. + Example:: + + .. availability:: Windows, Linux >= 4.2, not glibc + """ + platforms = {} + for arg in self.arguments[0].rstrip(".").split(","): + arg = arg.strip() + platform, _, version = arg.partition(" >= ") + if platform.startswith("not "): + version = False + platform = platform.removeprefix("not ") + elif not version: + version = True + platforms[platform] = version + + unknown = set(platforms).difference(KNOWN_PLATFORMS) + if unknown: + logger.warning( + "Unknown platform%s or syntax '%s' in '.. availability:: %s', " + "see %s:KNOWN_PLATFORMS for a set of known platforms.", + "s" if len(platforms) != 1 else "", + " ".join(sorted(unknown)), + self.arguments[0], + __file__, + location=self.get_location(), + ) + + return platforms + + +def setup(app): + app.add_directive("availability", Availability) + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/api.rst b/docs/api.rst index 3b830d4859..82099d3344 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -215,8 +215,7 @@ CPU scpufreq(current=1703.609, min=800.0, max=3500.0), scpufreq(current=1754.289, min=800.0, max=3500.0)] - Availability: Linux, macOS, Windows, FreeBSD, OpenBSD. *percpu* only - supported on Linux and FreeBSD. + .. availability:: Linux, macOS, Windows, FreeBSD, OpenBSD. .. versionadded:: 5.1.0 @@ -781,7 +780,7 @@ Network ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, and ``d2`` (some flags are only available on certain platforms). - Availability: UNIX + .. availability:: UNIX Example: @@ -834,7 +833,7 @@ Sensors See also `temperatures.py`_ and `sensors.py`_ for an example application. - Availability: Linux, FreeBSD + .. availability:: Linux, FreeBSD .. versionadded:: 5.1.0 @@ -855,7 +854,7 @@ Sensors See also `fans.py`_ and `sensors.py`_ for an example application. - Availability: Linux + .. availability:: Linux .. versionadded:: 5.2.0 @@ -892,7 +891,7 @@ Sensors See also `battery.py`_ and `sensors.py`_ for an example application. - Availability: Linux, Windows, macOS, FreeBSD + .. availability:: Linux, Windows, macOS, FreeBSD .. versionadded:: 5.1.0 @@ -1378,21 +1377,21 @@ Process class The real, effective and saved user ids of this process as a named tuple. This is the same as `os.getresuid`_ but can be used for any process PID. - Availability: UNIX + .. availability:: UNIX .. method:: gids() The real, effective and saved group ids of this process as a named tuple. This is the same as `os.getresgid`_ but can be used for any process PID. - Availability: UNIX + .. availability:: UNIX .. method:: terminal() The terminal associated with this process, if any, else ``None``. This is similar to "tty" command but can be used for any process PID. - Availability: UNIX + .. availability:: UNIX .. method:: nice(value=None) @@ -1465,7 +1464,7 @@ Process class >>> p.ionice() # get pionice(ioclass=, value=7) - Availability: Linux, Windows Vista+ + .. availability:: Linux, Windows .. versionchanged:: 5.6.2 Windows accepts new ``IOPRIO_*`` constants. @@ -1494,7 +1493,7 @@ Process class Also see `procinfo.py`_ script. - Availability: Linux, FreeBSD + .. availability:: Linux, FreeBSD .. versionchanged:: 5.7.3 added FreeBSD support. @@ -1539,7 +1538,7 @@ Process class >>> p.io_counters() pio(read_count=454556, write_count=3456, read_bytes=110592, write_bytes=0, read_chars=769931, write_chars=203) - Availability: Linux, BSD, Windows, AIX + .. availability:: Linux, BSD, Windows, AIX .. versionchanged:: 5.2.0 added *read_chars* + *write_chars* on Linux and *other_count* + @@ -1563,13 +1562,13 @@ Process class The number of file descriptors currently opened by this process (non cumulative). - Availability: UNIX + .. availability:: UNIX .. method:: num_handles() The number of handles currently used by this process (non cumulative). - Availability: Windows + .. availability:: Windows .. method:: num_threads() @@ -1689,7 +1688,7 @@ Process class >>> # reset affinity against all eligible CPUs >>> p.cpu_affinity([]) - Availability: Linux, Windows, FreeBSD + .. availability:: Linux, Windows, FreeBSD .. versionchanged:: 2.2.0 added support for FreeBSD. @@ -1706,7 +1705,7 @@ Process class observe the system workload distributed across multiple CPUs as shown by `cpu_distribution.py`_ example script. - Availability: Linux, FreeBSD, SunOS + .. availability:: Linux, FreeBSD, SunOS .. versionadded:: 5.1.0 @@ -1915,7 +1914,7 @@ Process class .. versionadded:: 8.0.0 - Availability: Linux, macOS, Windows + .. availability:: Linux, macOS, Windows .. method:: memory_full_info() @@ -2004,7 +2003,7 @@ Process class pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), ...] - Availability: Linux, Windows, FreeBSD, SunOS + .. availability:: Linux, Windows, FreeBSD, SunOS .. versionchanged:: 5.6.0 removed macOS support because inherently broken (see issue `#1291 @@ -2425,9 +2424,9 @@ Python's memory tracking misses. | Windows | ``HeapCreate()`` without ``HeapDestroy()`` | ``heap_count`` | +---------------+------------------------------------------------------------------------------------+-----------------+ - .. versionadded:: 7.2.0 + .. availability:: Linux with glibc, Windows, macOS, FreeBSD, NetBSD - Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD + .. versionadded:: 7.2.0 .. function:: heap_trim() @@ -2444,9 +2443,9 @@ Python's memory tracking misses. fragmentation. Its effectiveness depends on allocator behavior and fragmentation patterns. - .. versionadded:: 7.2.0 + .. availability:: Linux with glibc, Windows, macOS, FreeBSD, NetBSD - Availability: Linux + glibc (e.g. not MUSL), Windows, macOS, FreeBSD, NetBSD + .. versionadded:: 7.2.0 ---- @@ -2460,7 +2459,7 @@ Windows services .. versionadded:: 4.2.0 - Availability: Windows + .. availability:: Windows .. function:: win_service_get(name) @@ -2469,7 +2468,7 @@ Windows services .. versionadded:: 4.2.0 - Availability: Windows + .. availability:: Windows .. class:: WindowsService @@ -2522,7 +2521,7 @@ Windows services .. versionadded:: 4.2.0 - Availability: Windows + .. availability:: Windows Example code: @@ -2569,7 +2568,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over :data:`*_PRIORITY_CLASS ` constants for :meth:`Process.nice` on Windows. - Availability: Windows + .. availability:: Windows .. versionadded:: 8.0.0 @@ -2579,7 +2578,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over :meth:`Process.ionice`. On Linux: ``IOPRIO_CLASS_*`` constants. On Windows: ``IOPRIO_*`` constants. - Availability: Linux, Windows + .. availability:: Linux, Windows .. versionadded:: 8.0.0 @@ -2588,7 +2587,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over `enum.IntEnum`_ collection of :data:`RLIMIT_* ` constants for :meth:`Process.rlimit`. - Availability: Linux, FreeBSD + .. availability:: Linux, FreeBSD .. versionadded:: 8.0.0 @@ -2690,7 +2689,7 @@ Process priority constants set process priority. These constants are members of the :class:`psutil.ProcessPriority` enum. - Availability: Windows + .. availability:: Windows .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessPriority` enum members (were plain @@ -2717,7 +2716,7 @@ Process priority constants `ionice `_ command line utility or `ioprio_get`_ system call. - Availability: Linux + .. availability:: Linux .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessIOPriority` enum members @@ -2734,7 +2733,7 @@ Process priority constants These constants are members of the :class:`psutil.ProcessIOPriority` enum. - Availability: Windows + .. availability:: Windows .. versionadded:: 5.6.2 @@ -2779,7 +2778,7 @@ conjunction with :meth:`Process.rlimit()`. See `resource.getrlimit`_ for further information. These constants are members of the :class:`psutil.ProcessRlimit` enum. -Availability: Linux, FreeBSD +.. availability:: Linux, FreeBSD .. versionchanged:: 5.7.3 added FreeBSD support, added ``RLIMIT_SWAP``, ``RLIMIT_SBSIZE``, @@ -2868,7 +2867,7 @@ Other constants It must be noted that this trick works only for APIs which rely on /proc filesystem (e.g. `memory`_ APIs and most :class:`Process` class methods). - Availability: Linux, Solaris, AIX + .. availability:: Linux, SunOS, AIX .. versionadded:: 3.2.3 diff --git a/docs/auto-build.sh b/docs/auto-build.sh deleted file mode 100644 index 3bd98973d4..0000000000 --- a/docs/auto-build.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# This script uses inotifywait to watch for changes in the current directory -# and its parents, and automatically rebuilds the Sphinx doc whenever a change -# is detected. Requires: sudo apt install inotify-tools. - -DIR="." -make clean -make html -while inotifywait -r -e modify,create,delete,move "$DIR"; do - make html -done diff --git a/docs/conf.py b/docs/conf.py index ae65f7b84a..0ad96e65fa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,6 +48,7 @@ def get_version(): "sphinx.ext.intersphinx", "sphinx_copybutton", # our own custom extensions in _ext/ dir + "availability", "add_home_link", "changelog_anchors", "check_python_syntax", diff --git a/docs/index.rst b/docs/index.rst index 2ad4f58f93..ba530616e5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,6 +2,7 @@ :synopsis: psutil module .. moduleauthor:: Giampaolo Rodola' .. include:: _links.rst +.. _availability: Psutil documentation ==================== diff --git a/pyproject.toml b/pyproject.toml index e4fde43500..b8a8ac73e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,7 @@ ignore = [ "tests/test_sudo.py" = ["PT009"] "scripts/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] "scripts/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] +"doc/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "setup.py" = [ "B904", # Use ` raise from` to specify exception cause (PYTHON2.7 COMPAT) "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) diff --git a/tests/test_posix.py b/tests/test_posix.py index acc3eb87e5..61ce2cb30e 100755 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -323,7 +323,7 @@ def test_nice(self): def test_num_ctx_switches(self): ru = resource.getrusage(resource.RUSAGE_SELF) cws = psutil.Process().num_ctx_switches() - tol = 10 + tol = 50 if "PYTEST_XDIST_WORKER_COUNT" in os.environ: tol *= int(os.environ["PYTEST_XDIST_WORKER_COUNT"]) if MACOS: From de1a7e760b0c50cba2b727c61fed8a70a5df9b93 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 18:10:59 +0100 Subject: [PATCH 1589/1714] Use ast to retrieve psutil version --- docs/conf.py | 25 ++++++++++++------------- setup.py | 21 ++++++++++----------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0ad96e65fa..a6f5780b76 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# 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. @@ -21,17 +21,16 @@ def get_version(): - INIT = os.path.abspath(os.path.join(HERE, '../psutil/__init__.py')) - with open(INIT) as f: - for line in f: - if line.startswith('__version__'): - ret = ast.literal_eval(line.strip().split(' = ')[1]) - assert ret.count('.') == 2, ret - for num in ret.split('.'): - assert num.isdigit(), ret - return ret - msg = "couldn't find version string" - raise ValueError(msg) + path = os.path.join(HERE, "..", "psutil", "__init__.py") + with open(path) as f: + mod = ast.parse(f.read()) + for node in mod.body: + if isinstance(node, ast.Assign): + for target in node.targets: + if getattr(target, "id", None) == "__version__": + return ast.literal_eval(node.value) + msg = "could not find __version__" + raise RuntimeError(msg) VERSION = get_version() @@ -69,7 +68,7 @@ def get_version(): source_suffix = '.rst' master_doc = 'index' -language = "eng" +language = "en" exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] pygments_style = 'sphinx' diff --git a/setup.py b/setup.py index feac788ed2..31ae2162e3 100755 --- a/setup.py +++ b/setup.py @@ -138,17 +138,16 @@ def get_version(): - INIT = os.path.join(HERE, 'psutil/__init__.py') - with open(INIT) as f: - for line in f: - if line.startswith('__version__'): - ret = ast.literal_eval(line.strip().split(' = ')[1]) - assert ret.count('.') == 2, ret - for num in ret.split('.'): - assert num.isdigit(), ret - return ret - msg = "couldn't find version string" - raise ValueError(msg) + path = os.path.join(HERE, "psutil", "__init__.py") + with open(path) as f: + mod = ast.parse(f.read()) + for node in mod.body: + if isinstance(node, ast.Assign): + for target in node.targets: + if getattr(target, "id", None) == "__version__": + return ast.literal_eval(node.value) + msg = "could not find __version__" + raise RuntimeError(msg) VERSION = get_version() From b63cf74cc7f59c86ce6601428c889ebd7ca95e5a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 18:40:00 +0100 Subject: [PATCH 1590/1714] Add _bootstrap.py module to project root --- MANIFEST.in | 2 +- _bootstrap.py | 40 ++++++++++++++++++++++++++ docs/conf.py | 16 ++--------- scripts/internal/_mirror.py | 30 -------------------- scripts/internal/download_wheels.py | 8 +++++- scripts/internal/git_pre_commit.py | 37 ++++++------------------ scripts/internal/print_dist.py | 11 ++++++-- scripts/internal/print_sysinfo.py | 15 ++++------ setup.py | 44 +++++++++++------------------ tests/__init__.py | 8 ++---- 10 files changed, 92 insertions(+), 119 deletions(-) create mode 100644 _bootstrap.py delete mode 100755 scripts/internal/_mirror.py diff --git a/MANIFEST.in b/MANIFEST.in index f68f9d5433..6ffddee430 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,6 +9,7 @@ include MANIFEST.in include Makefile include README.rst include SECURITY.md +include _bootstrap.py include docs/.readthedocs.yaml include docs/DEVNOTES include docs/Makefile @@ -159,7 +160,6 @@ include scripts/fans.py include scripts/free.py include scripts/ifconfig.py include scripts/internal/README -include scripts/internal/_mirror.py include scripts/internal/bench_oneshot.py include scripts/internal/bench_oneshot_2.py include scripts/internal/convert_readme.py diff --git a/_bootstrap.py b/_bootstrap.py new file mode 100644 index 0000000000..7c5e1f1a3a --- /dev/null +++ b/_bootstrap.py @@ -0,0 +1,40 @@ +# 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. + +"""Bootstrap utilities for loading psutil modules without psutil +being installed. +""" + +import ast +import importlib.util +import os + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def load_module(path): + """Load a Python module by file path without importing it + as part of a package. + """ + name = os.path.splitext(os.path.basename(path))[0] + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def get_version(): + """Extract __version__ from psutil/__init__.py using AST + (no imports needed). + """ + path = os.path.join(HERE, "psutil", "__init__.py") + with open(path) as f: + mod = ast.parse(f.read()) + for node in mod.body: + if isinstance(node, ast.Assign): + for target in node.targets: + if getattr(target, "id", None) == "__version__": + return ast.literal_eval(node.value) + msg = "could not find __version__" + raise RuntimeError(msg) diff --git a/docs/conf.py b/docs/conf.py index a6f5780b76..5c83061ed4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,6 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -import ast import datetime import os import sys @@ -19,19 +18,8 @@ THIS_YEAR = str(datetime.datetime.now().year) HERE = os.path.abspath(os.path.dirname(__file__)) - -def get_version(): - path = os.path.join(HERE, "..", "psutil", "__init__.py") - with open(path) as f: - mod = ast.parse(f.read()) - for node in mod.body: - if isinstance(node, ast.Assign): - for target in node.targets: - if getattr(target, "id", None) == "__version__": - return ast.literal_eval(node.value) - msg = "could not find __version__" - raise RuntimeError(msg) - +sys.path.insert(0, os.path.join(HERE, "..")) +from _bootstrap import get_version # noqa: E402 VERSION = get_version() diff --git a/scripts/internal/_mirror.py b/scripts/internal/_mirror.py deleted file mode 100755 index f7183783f2..0000000000 --- a/scripts/internal/_mirror.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/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. - -"""Module needed so that certain scripts in this directory can freely -import psutil/_common.py without installing psutil first, and/or to -avoid circular import errors deriving from "psutil._common import …". -""" - -import importlib -import os -import pathlib - -ROOT = pathlib.Path(__file__).resolve().parent.parent.parent - - -def _import_module_by_path(path): - name = os.path.splitext(os.path.basename(path))[0] - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -_common = _import_module_by_path(ROOT / "psutil/_common.py") - -bytes2human = _common.bytes2human -print_color = _common.print_color diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py index 7d240a8f5f..d8cf52380b 100755 --- a/scripts/internal/download_wheels.py +++ b/scripts/internal/download_wheels.py @@ -17,13 +17,19 @@ import argparse import json import os +import pathlib import shutil import sys import zipfile import requests -from scripts.internal._mirror import bytes2human +ROOT = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT)) +from _bootstrap import load_module # noqa: E402 + +_common = load_module(ROOT / "psutil" / "_common.py") +bytes2human = _common.bytes2human USER = "giampaolo" PROJECT = "psutil" diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 08574ae46a..141a3fb37f 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -11,44 +11,25 @@ """ import os +import pathlib import shlex import shutil import subprocess import sys -PYTHON = sys.executable -LINUX = sys.platform.startswith("linux") - - -def term_supports_colors(): - try: - import curses +ROOT = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT)) +from _bootstrap import load_module # noqa: E402 - assert sys.stderr.isatty() - curses.setupterm() - return curses.tigetnum("colors") > 0 - except Exception: # noqa: BLE001 - return False +_common = load_module(ROOT / "psutil" / "_common.py") +hilite = _common.hilite - -def hilite(s, ok=True, bold=False): - """Return an highlighted version of 'string'.""" - if not term_supports_colors(): - return s - attr = [] - if ok is None: # no color - pass - elif ok: # green - attr.append("32") - else: # red - attr.append("31") - if bold: - attr.append("1") - return f"\x1b[{';'.join(attr)}m{s}\x1b[0m" +PYTHON = sys.executable +LINUX = sys.platform.startswith("linux") def exit_with(msg): - print(hilite("Commit aborted. " + msg, ok=False), file=sys.stderr) + print(hilite("Commit aborted. " + msg, color="red"), file=sys.stderr) sys.exit(1) diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index 02b5176cd3..c0ddc6c44c 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -9,9 +9,16 @@ import argparse import collections import os +import pathlib +import sys -from scripts.internal._mirror import bytes2human -from scripts.internal._mirror import print_color +ROOT = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT)) +from _bootstrap import load_module # noqa: E402 + +_common = load_module(ROOT / "psutil" / "_common.py") +bytes2human = _common.bytes2human +print_color = _common.print_color class Wheel: diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py index c2889fac9e..16b1253fc9 100755 --- a/scripts/internal/print_sysinfo.py +++ b/scripts/internal/print_sysinfo.py @@ -9,9 +9,9 @@ import collections import datetime import getpass -import importlib.util import locale import os +import pathlib import platform import shlex import shutil @@ -32,6 +32,9 @@ HERE = os.path.realpath(os.path.abspath(os.path.dirname(__file__))) +ROOT = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT)) +from _bootstrap import load_module # noqa: E402 def sh(cmd): @@ -40,19 +43,11 @@ def sh(cmd): return subprocess.check_output(cmd, universal_newlines=True).strip() -def import_module_by_path(path): - name = os.path.splitext(os.path.basename(path))[0] - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - tests_init = os.path.realpath( os.path.join(HERE, "..", "..", "tests", "__init__.py") ) -tests_init_mod = import_module_by_path(tests_init) +tests_init_mod = load_module(tests_init) def main(): diff --git a/setup.py b/setup.py index 31ae2162e3..d34ee99403 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,6 @@ from __future__ import print_function -import ast import contextlib import glob import io @@ -52,20 +51,22 @@ HERE = os.path.abspath(os.path.dirname(__file__)) -# ...so we can import _common.py -sys.path.insert(0, os.path.join(HERE, "psutil")) - -from _common import AIX # noqa: E402 -from _common import BSD # noqa: E402 -from _common import FREEBSD # noqa: E402 -from _common import LINUX # noqa: E402 -from _common import MACOS # noqa: E402 -from _common import NETBSD # noqa: E402 -from _common import OPENBSD # noqa: E402 -from _common import POSIX # noqa: E402 -from _common import SUNOS # noqa: E402 -from _common import WINDOWS # noqa: E402 -from _common import hilite # noqa: E402 +from _bootstrap import get_version # noqa: E402 +from _bootstrap import load_module # noqa: E402 + +_common = load_module(os.path.join(HERE, "psutil", "_common.py")) + +AIX = _common.AIX +BSD = _common.BSD +FREEBSD = _common.FREEBSD +LINUX = _common.LINUX +MACOS = _common.MACOS +NETBSD = _common.NETBSD +OPENBSD = _common.OPENBSD +POSIX = _common.POSIX +SUNOS = _common.SUNOS +WINDOWS = _common.WINDOWS +hilite = _common.hilite PYPY = '__pypy__' in sys.builtin_module_names PY36_PLUS = sys.version_info[:2] >= (3, 6) @@ -137,19 +138,6 @@ sources.extend(glob.glob("psutil/arch/posix/*.c")) -def get_version(): - path = os.path.join(HERE, "psutil", "__init__.py") - with open(path) as f: - mod = ast.parse(f.read()) - for node in mod.body: - if isinstance(node, ast.Assign): - for target in node.targets: - if getattr(target, "id", None) == "__version__": - return ast.literal_eval(node.value) - msg = "could not find __version__" - raise RuntimeError(msg) - - VERSION = get_version() macros.append(('PSUTIL_VERSION', int(VERSION.replace('.', '')))) diff --git a/tests/__init__.py b/tests/__init__.py index 54cf486af1..dc24fc4332 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1806,11 +1806,9 @@ def reload_module(module): def import_module_by_path(path): - name = os.path.splitext(os.path.basename(path))[0] - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod + from _bootstrap import load_module + + return load_module(path) # =================================================================== From 7a813c46668cabb278a9e677340b6010a9620a28 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 20:26:04 +0100 Subject: [PATCH 1591/1714] Use pathlib --- .github/workflows/issues.py | 7 +++---- _bootstrap.py | 5 +++-- docs/conf.py | 9 +++++---- scripts/internal/download_wheels.py | 6 +++--- scripts/internal/find_broken_links.py | 1 - scripts/internal/git_pre_commit.py | 6 +++--- scripts/internal/print_announce.py | 13 +++++-------- scripts/internal/print_dist.py | 6 +++--- scripts/internal/print_sysinfo.py | 9 +++------ setup.py | 10 ++++++---- tests/__init__.py | 9 ++++----- tests/test_linux.py | 1 - tests/test_scripts.py | 7 ++++--- 13 files changed, 42 insertions(+), 47 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index 35a1d77cb6..b24a77922e 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -12,15 +12,14 @@ import functools import json import os +import pathlib import re from pprint import pprint as pp from github import Github -ROOT_DIR = os.path.realpath( - os.path.join(os.path.dirname(__file__), '..', '..') -) -SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +SCRIPTS_DIR = ROOT_DIR / 'scripts' # --- constants diff --git a/_bootstrap.py b/_bootstrap.py index 7c5e1f1a3a..ed887a51fb 100644 --- a/_bootstrap.py +++ b/_bootstrap.py @@ -9,8 +9,9 @@ import ast import importlib.util import os +import pathlib -HERE = os.path.abspath(os.path.dirname(__file__)) +ROOT_DIR = pathlib.Path(__file__).resolve().parent def load_module(path): @@ -28,7 +29,7 @@ def get_version(): """Extract __version__ from psutil/__init__.py using AST (no imports needed). """ - path = os.path.join(HERE, "psutil", "__init__.py") + path = ROOT_DIR / "psutil" / "__init__.py" with open(path) as f: mod = ast.parse(f.read()) for node in mod.body: diff --git a/docs/conf.py b/docs/conf.py index 5c83061ed4..37ffb244cd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,21 +10,22 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output import datetime -import os +import pathlib import sys PROJECT_NAME = "psutil" AUTHOR = "Giampaolo Rodola" THIS_YEAR = str(datetime.datetime.now().year) -HERE = os.path.abspath(os.path.dirname(__file__)) +HERE = pathlib.Path(__file__).resolve().parent +ROOT_DIR = HERE.parent -sys.path.insert(0, os.path.join(HERE, "..")) +sys.path.insert(0, str(ROOT_DIR)) from _bootstrap import get_version # noqa: E402 VERSION = get_version() -sys.path.insert(0, os.path.join(HERE, '_ext')) +sys.path.insert(0, str(HERE / '_ext')) extensions = [ "sphinx.ext.autodoc", diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py index d8cf52380b..54ba4cf099 100755 --- a/scripts/internal/download_wheels.py +++ b/scripts/internal/download_wheels.py @@ -24,11 +24,11 @@ import requests -ROOT = pathlib.Path(__file__).resolve().parent.parent.parent -sys.path.insert(0, str(ROOT)) +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT_DIR)) from _bootstrap import load_module # noqa: E402 -_common = load_module(ROOT / "psutil" / "_common.py") +_common = load_module(ROOT_DIR / "psutil" / "_common.py") bytes2human = _common.bytes2human USER = "giampaolo" diff --git a/scripts/internal/find_broken_links.py b/scripts/internal/find_broken_links.py index 142d7d9655..00d23a7d03 100755 --- a/scripts/internal/find_broken_links.py +++ b/scripts/internal/find_broken_links.py @@ -48,7 +48,6 @@ import requests -HERE = os.path.abspath(os.path.dirname(__file__)) REGEX = re.compile( r'(?:http|ftp|https)?://' r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 141a3fb37f..eabf47b11c 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -17,11 +17,11 @@ import subprocess import sys -ROOT = pathlib.Path(__file__).resolve().parent.parent.parent -sys.path.insert(0, str(ROOT)) +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT_DIR)) from _bootstrap import load_module # noqa: E402 -_common = load_module(ROOT / "psutil" / "_common.py") +_common = load_module(ROOT_DIR / "psutil" / "_common.py") hilite = _common.hilite PYTHON = sys.executable diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 5fa98b669e..2f931ed090 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -9,19 +9,16 @@ """ -import os +import pathlib import re import subprocess import sys from psutil import __version__ -HERE = os.path.abspath(os.path.dirname(__file__)) -ROOT = os.path.realpath(os.path.join(HERE, '..', '..')) -CHANGELOG = os.path.join(ROOT, 'docs', 'changelog.rst') -PRINT_HASHES_SCRIPT = os.path.join( - ROOT, 'scripts', 'internal', 'print_hashes.py' -) +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +CHANGELOG = ROOT_DIR / 'docs' / 'changelog.rst' +PRINT_HASHES_PY = ROOT_DIR / 'scripts' / 'internal' / 'print_hashes.py' PRJ_NAME = 'psutil' PRJ_VERSION = __version__ @@ -131,7 +128,7 @@ def get_changes(): def main(): changes = get_changes() hashes = ( - subprocess.check_output([sys.executable, PRINT_HASHES_SCRIPT, 'dist/']) + subprocess.check_output([sys.executable, PRINT_HASHES_PY, 'dist/']) .strip() .decode() ) diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py index c0ddc6c44c..01d5c88ab8 100755 --- a/scripts/internal/print_dist.py +++ b/scripts/internal/print_dist.py @@ -12,11 +12,11 @@ import pathlib import sys -ROOT = pathlib.Path(__file__).resolve().parent.parent.parent -sys.path.insert(0, str(ROOT)) +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT_DIR)) from _bootstrap import load_module # noqa: E402 -_common = load_module(ROOT / "psutil" / "_common.py") +_common = load_module(ROOT_DIR / "psutil" / "_common.py") bytes2human = _common.bytes2human print_color = _common.print_color diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py index 16b1253fc9..7d9b765fca 100755 --- a/scripts/internal/print_sysinfo.py +++ b/scripts/internal/print_sysinfo.py @@ -31,9 +31,8 @@ wheel = None -HERE = os.path.realpath(os.path.abspath(os.path.dirname(__file__))) -ROOT = pathlib.Path(__file__).resolve().parent.parent.parent -sys.path.insert(0, str(ROOT)) +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT_DIR)) from _bootstrap import load_module # noqa: E402 @@ -43,9 +42,7 @@ def sh(cmd): return subprocess.check_output(cmd, universal_newlines=True).strip() -tests_init = os.path.realpath( - os.path.join(HERE, "..", "..", "tests", "__init__.py") -) +tests_init = ROOT_DIR / "tests" / "__init__.py" tests_init_mod = load_module(tests_init) diff --git a/setup.py b/setup.py index d34ee99403..5660f8b499 100755 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ import glob import io import os +import pathlib import shutil import struct import subprocess @@ -49,12 +50,13 @@ from distutils.core import setup -HERE = os.path.abspath(os.path.dirname(__file__)) +ROOT_DIR = pathlib.Path(__file__).resolve().parent +sys.path.insert(0, str(ROOT_DIR)) from _bootstrap import get_version # noqa: E402 from _bootstrap import load_module # noqa: E402 -_common = load_module(os.path.join(HERE, "psutil", "_common.py")) +_common = load_module(ROOT_DIR / "psutil" / "_common.py") AIX = _common.AIX BSD = _common.BSD @@ -159,8 +161,8 @@ def get_long_description(): - script = os.path.join(HERE, "scripts", "internal", "convert_readme.py") - readme = os.path.join(HERE, 'README.rst') + script = ROOT_DIR / "scripts" / "internal" / "convert_readme.py" + readme = ROOT_DIR / 'README.rst' p = subprocess.Popen( [sys.executable, script, readme], stdout=subprocess.PIPE, diff --git a/tests/__init__.py b/tests/__init__.py index dc24fc4332..b0e63f204f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -13,6 +13,7 @@ import importlib import ipaddress import os +import pathlib import platform import random import re @@ -65,7 +66,7 @@ __all__ = [ # constants 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', - 'PYPY', 'PYTHON_EXE', 'PYTHON_EXE_ENV', 'ROOT_DIR', 'SCRIPTS_DIR', + 'PYPY', 'PYTHON_EXE', 'PYTHON_EXE_ENV', 'ROOT_DIR', 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', 'IS_64BIT', "HAS_PROC_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_PROC_ENVIRON", @@ -179,11 +180,9 @@ def macos_version(): # --- paths -ROOT_DIR = os.environ.get("PSUTIL_ROOT_DIR") or os.path.realpath( - os.path.join(os.path.dirname(__file__), "..") +ROOT_DIR = os.environ.get("PSUTIL_ROOT") or str( + pathlib.Path(__file__).resolve().parent.parent ) -SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') -HERE = os.path.realpath(os.path.dirname(__file__)) # --- support diff --git a/tests/test_linux.py b/tests/test_linux.py index 1b18c68038..5ab64e47d6 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -50,7 +50,6 @@ from psutil._pslinux import open_binary -HERE = os.path.abspath(os.path.dirname(__file__)) SIOCGIFADDR = 0x8915 SIOCGIFHWADDR = 0x8927 SIOCGIFNETMASK = 0x891B diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 5d0bad7a36..da4a737c0e 100755 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -8,6 +8,7 @@ import ast import os +import pathlib import shutil import stat import subprocess @@ -28,14 +29,14 @@ from . import PYTHON_EXE from . import PYTHON_EXE_ENV from . import ROOT_DIR -from . import SCRIPTS_DIR from . import PsutilTestCase from . import import_module_by_path from . import psutil from . import sh -INTERNAL_SCRIPTS_DIR = os.path.join(SCRIPTS_DIR, "internal") -SETUP_PY = os.path.join(ROOT_DIR, 'setup.py') +SCRIPTS_DIR = pathlib.Path(ROOT_DIR) / "scripts" +INTERNAL_SCRIPTS_DIR = SCRIPTS_DIR / "internal" +SETUP_PY = pathlib.Path(ROOT_DIR) / 'setup.py' # =================================================================== From 5e34d7f6911e1818a8b549cc09a7378ead90d595 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 20:55:31 +0100 Subject: [PATCH 1592/1714] Remove Python 2.7 remaining compatibility (#2766) --- .github/workflows/build.yml | 12 ----- .github/workflows/changelog_bot.py | 3 +- MANIFEST.in | 1 - _bootstrap.py | 2 +- docs/changelog.rst | 2 + pyproject.toml | 11 ----- scripts/internal/test_python2_setup_py.py | 38 --------------- setup.py | 59 ++++------------------- tests/test_scripts.py | 38 --------------- 9 files changed, 14 insertions(+), 152 deletions(-) delete mode 100755 scripts/internal/test_python2_setup_py.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc3e1d0b5d..1e952fcd65 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,18 +54,6 @@ jobs: name: wheels-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse - # Test python 2.7 fallback installation message produced by setup.py - py2-fallback: - name: py2.7 setup.py check - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - uses: actions/checkout@v5 - - uses: LizardByte/actions/actions/setup_python@master - with: - python-version: 2.7 - - run: python scripts/internal/test_python2_setup_py.py - # Run linters. linters: runs-on: ubuntu-latest diff --git a/.github/workflows/changelog_bot.py b/.github/workflows/changelog_bot.py index ff89070e17..1a9860944f 100644 --- a/.github/workflows/changelog_bot.py +++ b/.github/workflows/changelog_bot.py @@ -261,7 +261,8 @@ def ask_claude(pr, diff): body=pr["body"], diff=diff[:MAX_DIFF_CHARS], ) - client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) + api_key = os.environ.get("ANTHROPIC_API_KEY", "").strip() + client = anthropic.Anthropic(api_key=api_key) message = client.messages.create( model="claude-sonnet-4-6", max_tokens=MAX_TOKENS, diff --git a/MANIFEST.in b/MANIFEST.in index 6ffddee430..a5e88302e7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -178,7 +178,6 @@ include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_sysinfo.py include scripts/internal/purge_installation.py -include scripts/internal/test_python2_setup_py.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py diff --git a/_bootstrap.py b/_bootstrap.py index ed887a51fb..7a1da56fd4 100644 --- a/_bootstrap.py +++ b/_bootstrap.py @@ -30,7 +30,7 @@ def get_version(): (no imports needed). """ path = ROOT_DIR / "psutil" / "__init__.py" - with open(path) as f: + with open(path, encoding="utf-8") as f: mod = ast.parse(f.read()) for node in mod.body: if isinstance(node, ast.Assign): diff --git a/docs/changelog.rst b/docs/changelog.rst index df7f8bbd6c..0f79a9f5e2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -99,6 +99,8 @@ Others: returns a `float` instead of `int` on all systems, not only Linux. - :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update changelog.rst and credits.rst when commenting with /changelog. +- :gh:`2766`: remove remaining Python 2.7 compatibility shims from + ``setup.py``, simplifying the build infrastructure. **Bug fixes** diff --git a/pyproject.toml b/pyproject.toml index b8a8ac73e0..9a63118834 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,19 +115,8 @@ ignore = [ "scripts/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "doc/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] "setup.py" = [ - "B904", # Use ` raise from` to specify exception cause (PYTHON2.7 COMPAT) - "C4", # flake8-comprehensions (PYTHON2.7 COMPAT) - "FLY", # flynt (PYTHON2.7 COMPAT) - "FURB145", # [*] Prefer `copy` method over slicing (PYTHON2.7 COMPAT) "T201", "T203", - "UP009", # [*] UTF-8 encoding declaration is unnecessary (PYTHON2.7 COMPAT) - "UP010", # [*] Unnecessary `__future__` import `print_function` (PYTHON2.7 COMPAT) - "UP024", # [*] Replace aliased errors with `OSError` (PYTHON2.7 COMPAT) - "UP025", # [*] Remove unicode literals from strings (PYTHON2.7 COMPAT) - "UP028", # [*] Replace `yield` over `for` loop with `yield from` (PYTHON2.7 COMPAT) - "UP032", # [*] Use f-string instead of `format` call (PYTHON2.7 COMPAT) - "UP036", # Version block is outdated for minimum Python version ] [tool.ruff.lint.isort] diff --git a/scripts/internal/test_python2_setup_py.py b/scripts/internal/test_python2_setup_py.py deleted file mode 100755 index 016dcfaa03..0000000000 --- a/scripts/internal/test_python2_setup_py.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python2 - -# 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. - - -"""Invoke setup.py with Python 2.7, make sure it fails but not due to -SyntaxError, and that it prints a meaningful error message. -""" - -import os -import subprocess -import sys - -ROOT_DIR = os.path.realpath( - os.path.join(os.path.dirname(__file__), "..", "..") -) - - -def main(): - if sys.version_info[:2] != (2, 7): - raise RuntimeError("this script is supposed to be run with python 2.7") - setup_py = os.path.join(ROOT_DIR, "setup.py") - p = subprocess.Popen( - [sys.executable, setup_py], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = p.communicate() - assert p.wait() == 1 - assert not stdout, stdout - assert "psutil no longer supports Python 2.7" in stderr, stderr - assert "Latest version supporting Python 2.7 is" in stderr, stderr - - -if __name__ == "__main__": - main() diff --git a/setup.py b/setup.py index 5660f8b499..37a8755e5a 100755 --- a/setup.py +++ b/setup.py @@ -4,12 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Cross-platform lib for process and system monitoring in Python. - -NOTE: the syntax of this script MUST be kept compatible with Python 2.7. -""" - -from __future__ import print_function +"""Cross-platform lib for process and system monitoring in Python.""" import contextlib import glob @@ -22,20 +17,8 @@ import sys import sysconfig import tempfile -import textwrap import warnings -if sys.version_info[0] == 2: - sys.exit(textwrap.dedent("""\ - As of version 7.0.0 psutil no longer supports Python 2.7, see: - https://github.com/giampaolo/psutil/issues/2480 - Latest version supporting Python 2.7 is psutil 6.1.X. - Install it with: - - python2 -m pip install psutil==6.1.*\ - """)) - - with warnings.catch_warnings(): warnings.simplefilter("ignore") try: @@ -304,10 +287,7 @@ def get_winver(): ], # extra_compile_args=["/W 4"], # extra_link_args=["/DEBUG"], - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) elif MACOS: @@ -326,10 +306,7 @@ def get_winver(): '-framework', 'IOKit', ], - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) elif FREEBSD: @@ -345,10 +322,7 @@ def get_winver(): ), define_macros=macros, libraries=["devstat"], - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) elif OPENBSD: @@ -364,10 +338,7 @@ def get_winver(): ), define_macros=macros, libraries=["kvm"], - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) elif NETBSD: @@ -383,10 +354,7 @@ def get_winver(): ), define_macros=macros, libraries=["kvm", "jemalloc"], - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) elif LINUX: @@ -403,10 +371,7 @@ def get_winver(): + glob.glob("psutil/arch/linux/*.c") ), define_macros=macros, - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) elif SUNOS: @@ -421,10 +386,7 @@ def get_winver(): ), define_macros=macros, libraries=["kstat", "nsl", "socket"], - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) elif AIX: @@ -439,10 +401,7 @@ def get_winver(): ), libraries=["perfstat"], define_macros=macros, - # fmt: off - # python 2.7 compatibility requires no comma - **py_limited_api - # fmt: on + **py_limited_api, ) else: diff --git a/tests/test_scripts.py b/tests/test_scripts.py index da4a737c0e..f555cc68b8 100755 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -9,9 +9,7 @@ import ast import os import pathlib -import shutil import stat -import subprocess import pytest @@ -36,7 +34,6 @@ SCRIPTS_DIR = pathlib.Path(ROOT_DIR) / "scripts" INTERNAL_SCRIPTS_DIR = SCRIPTS_DIR / "internal" -SETUP_PY = pathlib.Path(ROOT_DIR) / 'setup.py' # =================================================================== @@ -206,38 +203,3 @@ def test_import_all(self): import_module_by_path(path) except SystemExit: pass - - -# =================================================================== -# --- Tests for setup.py script -# =================================================================== - - -@pytest.mark.skipif( - CI_TESTING and not os.path.exists(SETUP_PY), reason="can't find setup.py" -) -class TestSetupScript(PsutilTestCase): - def test_invocation(self): - module = import_module_by_path(SETUP_PY) - with pytest.raises(SystemExit): - module.setup() - assert module.get_version() == psutil.__version__ - - @pytest.mark.skipif( - not shutil.which("python2.7"), reason="python2.7 not installed" - ) - def test_python2(self): - # There's a duplicate of this test in scripts/internal - # directory, which is only executed by CI. We replicate it here - # to run it when developing locally. - p = subprocess.Popen( - [shutil.which("python2.7"), SETUP_PY], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - ) - stdout, stderr = p.communicate() - assert p.wait() == 1 - assert not stdout - assert "psutil no longer supports Python 2.7" in stderr - assert "Latest version supporting Python 2.7 is" in stderr From 1adffa022575de93faf6283af50934794aeefeaf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 21:16:27 +0100 Subject: [PATCH 1593/1714] No longer use OrderedDict --- psutil/__init__.py | 4 ++-- psutil/_common.py | 11 ++++------- psutil/_pssunos.py | 4 +--- scripts/cpu_distribution.py | 6 +++--- scripts/fans.py | 4 +--- scripts/internal/find_broken_links.py | 2 +- scripts/internal/print_access_denied.py | 4 ++-- scripts/internal/print_api_speed.py | 2 +- scripts/internal/print_sysinfo.py | 8 ++++---- scripts/netstat.py | 4 ++-- 10 files changed, 21 insertions(+), 28 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index 0f72cfe1fc..e0cd95f78f 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -402,7 +402,7 @@ def _get_ident(self): return (self.pid, self.create_time()) def __str__(self): - info = collections.OrderedDict() + info = {} info["pid"] = self.pid if self._name: info['name'] = self._name @@ -1624,7 +1624,7 @@ def remove(pid): pmap = _pmap.copy() a = set(pids()) - b = set(pmap.keys()) + b = set(pmap) new_pids = a - b gone_pids = b - a for pid in gone_pids: diff --git a/psutil/_common.py b/psutil/_common.py index 63b2dfd2d0..3393bff3cb 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -112,7 +112,7 @@ class Error(Exception): __module__ = 'psutil' def _infodict(self, attrs): - info = collections.OrderedDict() + info = {} for name in attrs: value = getattr(self, name, None) if value or (name == "pid" and value == 0): @@ -500,7 +500,7 @@ def _remove_dead_reminders(self, input_dict, name): disk disappears) this removes the entry from self.reminders. """ old_dict = self.cache[name] - gone_keys = set(old_dict.keys()) - set(input_dict.keys()) + gone_keys = set(old_dict) - set(input_dict) for gone_key in gone_keys: for remkey in self.reminder_keys[name][gone_key]: del self.reminders[name][remkey] @@ -706,7 +706,7 @@ def hilite(s, color=None, bold=False): # pragma: no cover try: color = colors[color] except KeyError: - msg = f"invalid color {color!r}; choose amongst {list(colors.keys())}" + msg = f"invalid color {color!r}; choose amongst {list(colors)}" raise ValueError(msg) from None attr.append(color) if bold: @@ -736,10 +736,7 @@ def print_color( try: color = colors[color] except KeyError: - msg = ( - f"invalid color {color!r}; choose between" - f" {list(colors.keys())!r}" - ) + msg = f"invalid color {color!r}; choose between {list(colors)!r}" raise ValueError(msg) from None if bold and color <= 7: color += 8 diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 6c316006ae..7ffd22efd7 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -649,9 +649,7 @@ def net_connections(self, kind='inet'): @wrap_exceptions def memory_maps(self): def toaddr(start, end): - return "{}-{}".format( - hex(start)[2:].strip('L'), hex(end)[2:].strip('L') - ) + return f"{hex(start)[2:].strip('L')}-{hex(end)[2:].strip('L')}" procfs_path = self._procfs_path retlist = [] diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index 4b39bddc2d..59109f5278 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -70,13 +70,13 @@ def main(): clean_screen() cpus_percent = psutil.cpu_percent(percpu=True) for i in range(num_cpus): - print("CPU {:<6}".format(i), end="") + print(f"CPU {i:<6}", end="") if cpus_hidden: print(" (+ hidden)", end="") print() for _ in range(num_cpus): - print("{:<10}".format(cpus_percent.pop(0)), end="") + print(f"{cpus_percent.pop(0):<10}", end="") print() # processes @@ -91,7 +91,7 @@ def main(): pname = procs[num].pop() except IndexError: pname = "" - print("{:<10}".format(pname[:10]), end="") + print(f"{pname[:10]:<10}", end="") print() curr_line += 1 if curr_line >= shutil.get_terminal_size()[1]: diff --git a/scripts/fans.py b/scripts/fans.py index 283542c6ca..16f0266709 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -26,9 +26,7 @@ def main(): for name, entries in fans.items(): print(name) for entry in entries: - print( - " {:<20} {} RPM".format(entry.label or name, entry.current) - ) + print(f" {entry.label or name:<20} {entry.current} RPM") print() diff --git a/scripts/internal/find_broken_links.py b/scripts/internal/find_broken_links.py index 00d23a7d03..4300482f41 100755 --- a/scripts/internal/find_broken_links.py +++ b/scripts/internal/find_broken_links.py @@ -240,7 +240,7 @@ def main(): else: for fail in fails: fname, url = fail - print("{:<30}: {} ".format(fname, url)) + print(f"{fname:<30}: {url} ") print('-' * 20) print(f"total: {len(fails)} fails!") sys.exit(1) diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py index 48b1d3f011..1633d6198c 100755 --- a/scripts/internal/print_access_denied.py +++ b/scripts/internal/print_access_denied.py @@ -82,8 +82,8 @@ def main(): tot_perc = round((tot_ads / tot_calls) * 100, 1) print("-" * 50) print( - "Totals: access-denied={} ({}%%), calls={}, processes={}, elapsed={}s" - .format(tot_ads, tot_perc, tot_calls, tot_procs, round(elapsed, 2)) + f"Totals: access-denied={tot_ads} ({tot_perc}%%), calls={tot_calls}," + f" processes={tot_procs}, elapsed={round(elapsed, 2)}s" ) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 5842600c14..0be1bac6dc 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -102,7 +102,7 @@ def print_timings(): def timecall(title, fun, *args, **kw): - print("{:<50}".format(title), end="") + print(f"{title:<50}", end="") sys.stdout.flush() t = timer() for n in range(TIMES): diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py index 7d9b765fca..f230dd052e 100755 --- a/scripts/internal/print_sysinfo.py +++ b/scripts/internal/print_sysinfo.py @@ -6,7 +6,6 @@ """Print system information. Run before CI test run.""" -import collections import datetime import getpass import locale @@ -48,7 +47,7 @@ def sh(cmd): def main(): - info = collections.OrderedDict() + info = {} # python info['python'] = ', '.join([ @@ -107,8 +106,9 @@ def main(): # metrics info['cpus'] = psutil.cpu_count() - info['loadavg'] = "{:.1f}%, {:.1f}%, {:.1f}%".format( - *tuple(x / psutil.cpu_count() * 100 for x in psutil.getloadavg()) + loadavg = tuple(x / psutil.cpu_count() * 100 for x in psutil.getloadavg()) + info['loadavg'] = ( + f"{loadavg[0]:.1f}%, {loadavg[1]:.1f}%, {loadavg[2]:.1f}%" ) mem = psutil.virtual_memory() info['memory'] = "{}%%, used={}, total={}".format( diff --git a/scripts/netstat.py b/scripts/netstat.py index 5e8615f261..7b3efcba29 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -50,10 +50,10 @@ def main(): for p in psutil.process_iter(['pid', 'name']): proc_names[p.info['pid']] = p.info['name'] for c in psutil.net_connections(kind='inet'): - laddr = "{}:{}".format(*c.laddr) + laddr = f"{c.laddr[0]}:{c.laddr[1]}" raddr = "" if c.raddr: - raddr = "{}:{}".format(*c.raddr) + raddr = f"{c.raddr[0]}:{c.raddr[1]}" name = proc_names.get(c.pid, '?') or '' line = templ.format( proto_map[(c.family, c.type)], From 1a4a97ff745caa921366833356c26a254581d5fc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 21:24:38 +0100 Subject: [PATCH 1594/1714] Use PyUnicode_FromString instead of Py_BuildValue --- psutil/arch/freebsd/cpu.c | 2 +- psutil/arch/netbsd/proc.c | 2 +- psutil/arch/openbsd/proc.c | 2 +- psutil/arch/osx/proc.c | 4 ++-- psutil/arch/posix/net.c | 16 ++++++---------- psutil/arch/windows/disk.c | 4 ++-- psutil/arch/windows/net.c | 2 +- psutil/arch/windows/proc.c | 2 +- psutil/arch/windows/proc_info.c | 2 +- psutil/arch/windows/services.c | 6 +++--- psutil/arch/windows/sys.c | 2 +- 11 files changed, 20 insertions(+), 24 deletions(-) diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 623c699fb7..816c0bdc7b 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -95,7 +95,7 @@ psutil_cpu_topology(PyObject *self, PyObject *args) { Py_RETURN_NONE; } - py_str = Py_BuildValue("s", topology); + py_str = PyUnicode_FromString(topology); free(topology); return py_str; } diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 1c191ba626..40f6355e23 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -69,7 +69,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { return NULL; if (pid == 0) { // else returns ENOENT - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); } mib[0] = CTL_KERN; diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 1f44c1f921..65bfe50308 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -185,7 +185,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { if (sysctl(name, 3, path, &pathlen, NULL, 0) != 0) { if (errno == ENOENT) { psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); } else { psutil_oserror(); diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index 4375032e6b..c6b59a6b4c 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -238,7 +238,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { // still alive but the executable which launched it got // deleted, see: // https://github.com/giampaolo/psutil/issues/1738 - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); } else { psutil_raise_for_pid(pid, "proc_pidpath()"); @@ -1101,7 +1101,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { psutil_debug("set environ to empty"); if (procargs != NULL) free(procargs); - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); error: Py_XDECREF(py_ret); diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index 8d9513a004..aa57d1de84 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -61,8 +61,7 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { char *ptr; if (addr == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } else if (family == AF_INET || family == AF_INET6) { if (family == AF_INET) @@ -78,11 +77,10 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { // ifconfig does not show anything BTW. // psutil_runtime_error(gai_strerror(err)); // return NULL; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } else { - return Py_BuildValue("s", buf); + return PyUnicode_FromString(buf); } } #ifdef PSUTIL_LINUX @@ -102,8 +100,7 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { #endif else { // unknown family - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } // AF_PACKET or AF_LINK @@ -114,11 +111,10 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { ptr += 3; } *--ptr = '\0'; - return Py_BuildValue("s", buf); + return PyUnicode_FromString(buf); } else { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } } diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index ace067a798..d4654b4add 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -391,10 +391,10 @@ psutil_QueryDosDevice(PyObject *self, PyObject *args) { if (QueryDosDevice(szDeviceName, szTarget, 511) != 0) { if (_tcscmp(lpDevicePath, szTarget) == 0) { _stprintf_s(szBuff, _countof(szBuff), TEXT("%c:"), d); - return Py_BuildValue("s", szBuff); + return PyUnicode_FromString(szBuff); } } d++; } - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); } diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 0e0166a75a..2b2716736a 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -208,7 +208,7 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { remaining -= n; } - py_mac_address = Py_BuildValue("s", buff_macaddr); + py_mac_address = PyUnicode_FromString(buff_macaddr); if (py_mac_address == NULL) goto error; diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index ed5d36f87b..f48f317bff 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -307,7 +307,7 @@ psutil_proc_exe(PyObject *self, PyObject *args) { if (processIdInfo.ImageName.Buffer == NULL) { // Happens for PID 4. - py_exe = Py_BuildValue("s", ""); + py_exe = PyUnicode_FromString(""); } else { py_exe = PyUnicode_FromWideChar( diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index ebf569cc93..e50ad8803b 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -679,7 +679,7 @@ psutil_proc_environ(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) return NULL; if ((pid == 0) || (pid == 4)) - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); pid_return = psutil_pid_is_running(pid); if (pid_return == 0) diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 07c6571ed7..6306775675 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -341,7 +341,7 @@ psutil_winservice_query_status(PyObject *self, PyObject *args) { // empty string. CloseServiceHandle(hService); PyMem_Free(service_name); - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { psutil_oserror_wsyscall("QueryServiceStatusEx"); @@ -418,7 +418,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { psutil_debug("set empty string for NOT_FOUND service description"); CloseServiceHandle(hService); PyMem_Free(service_name); - return Py_BuildValue("s", ""); + return PyUnicode_FromString(""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { @@ -445,7 +445,7 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { } if (scd->lpDescription == NULL) { - py_retstr = Py_BuildValue("s", ""); + py_retstr = PyUnicode_FromString(""); } else { py_retstr = PyUnicode_FromWideChar( diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index fbd6706302..161e94bd5e 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -134,7 +134,7 @@ psutil_users(PyObject *self, PyObject *args) { address->Address[4], address->Address[5] ); - py_address = Py_BuildValue("s", address_str); + py_address = PyUnicode_FromString(address_str); if (!py_address) goto error; } From 66a35ba260a3e3d964e3af5776cea7295472e4a7 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 15 Mar 2026 22:57:42 +0100 Subject: [PATCH 1595/1714] Doc: remove unnecessary parentheses like ':meth:`Process.name()`' --- MANIFEST.in | 1 - docs/Makefile | 2 +- docs/_links.rst | 75 --- docs/api.rst | 143 +++-- docs/changelog.rst | 1407 ++++++++++++++++++++++---------------------- docs/credits.rst | 16 +- docs/devguide.rst | 2 +- docs/index.rst | 6 +- docs/recipes.rst | 6 +- pyproject.toml | 6 +- 10 files changed, 829 insertions(+), 835 deletions(-) delete mode 100644 docs/_links.rst diff --git a/MANIFEST.in b/MANIFEST.in index a5e88302e7..eb4eb1033d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,7 +18,6 @@ include docs/_ext/add_home_link.py include docs/_ext/availability.py include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py -include docs/_links.rst include docs/_static/copybutton.js include docs/_static/css/custom.css include docs/_static/favicon.ico diff --git a/docs/Makefile b/docs/Makefile index 5355a77259..34c17de526 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ PYTHON = python3 SPHINXBUILD = $(PYTHON) -m sphinx BUILDDIR = _build -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees . +ALLSPHINXOPTS = --fail-on-warning -d $(BUILDDIR)/doctrees . clean: ## Remove all build files rm -rf $(BUILDDIR) diff --git a/docs/_links.rst b/docs/_links.rst deleted file mode 100644 index cf62f6fedf..0000000000 --- a/docs/_links.rst +++ /dev/null @@ -1,75 +0,0 @@ -.. _`BPO-10784`: https://bugs.python.org/issue10784 -.. _`BPO-12442`: https://bugs.python.org/issue12442 -.. _`BPO-6973`: https://bugs.python.org/issue6973 -.. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 -.. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ -.. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get -.. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt -.. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html -.. _`man prlimit`: https://linux.die.net/man/2/prlimit -.. _`psleak`: https://github.com/giampaolo/psleak -.. _`Tidelift security contact`: https://tidelift.com/security - -.. === docs.python.org - -.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 -.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET -.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX -.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum -.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum -.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum -.. _`hash`: https://docs.python.org/3/library/functions.html#hash -.. _`open`: https://docs.python.org/3/library/functions.html#open -.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count -.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg -.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid -.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority -.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid -.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid -.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY -.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC -.. _`os.open`: https://docs.python.org/3/library/os.html#os.open -.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open -.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority -.. _`os.times`: https://docs.python.org//library/os.html#os.times -.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit -.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit -.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue -.. _`select.poll`: https://docs.python.org//library/select.html#select.poll -.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. -.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. -.. _`signal module`: https://docs.python.org//library/signal.html -.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM -.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET -.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM -.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd -.. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait -.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen -.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident -.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread - -.. === scripts - -.. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py -.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py -.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py -.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py -.. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py - -.. === Windows API - -.. _`GetDriveType`: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea -.. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess -.. _`GetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass -.. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex -.. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass -.. _`TerminateProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess diff --git a/docs/api.rst b/docs/api.rst index 82099d3344..6309374f05 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,5 +1,5 @@ -.. include:: _links.rst .. currentmodule:: psutil +.. _availability: API reference ============= @@ -116,11 +116,11 @@ CPU .. function:: cpu_times_percent(interval=None, percpu=False) - Same as :func:`cpu_percent()` but provides utilization percentages for each + Same as :func:`cpu_percent` but provides utilization percentages for each specific CPU time as is returned by :func:`psutil.cpu_times(percpu=True)`. *interval* and - *percpu* arguments have the same meaning as in :func:`cpu_percent()`. + *percpu* arguments have the same meaning as in :func:`cpu_percent`. On Linux "guest" and "guest_nice" percentages are not accounted in "user" and "user_nice" percentages. @@ -957,7 +957,7 @@ Functions .. function:: pids() Return a sorted list of current running PIDs. - To iterate over all processes and avoid race conditions :func:`process_iter()` + To iterate over all processes and avoid race conditions :func:`process_iter` should be preferred. >>> import psutil @@ -971,15 +971,15 @@ Functions Return an iterator yielding a :class:`Process` class instance for all running processes on the local machine. - This should be preferred over :func:`psutil.pids()` to iterate over + This should be preferred over :func:`psutil.pids` to iterate over processes, as retrieving info is safe from race conditions. Every :class:`Process` instance is only created once, and then cached for the - next time :func:`psutil.process_iter()` is called (if PID is still alive). + next time :func:`psutil.process_iter` is called (if PID is still alive). Cache can optionally be cleared via ``process_iter.cache_clear()``. - *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict()`. - If *attrs* is specified :meth:`Process.as_dict()` result will be stored as a + *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict`. + If *attrs* is specified :meth:`Process.as_dict` result will be stored as a ``info`` attribute attached to the returned :class:`Process` instances. If *attrs* is an empty list it will retrieve all process info (slow). @@ -1070,20 +1070,20 @@ Exceptions Raised by :class:`Process` class methods when no process with the given *pid* is found in the current process list, or when a process no longer exists. *name* is the name the process had before disappearing - and gets set only if :meth:`Process.name()` was previously called. + and gets set only if :meth:`Process.name` was previously called. .. class:: ZombieProcess(pid, name=None, ppid=None, msg=None) This may be raised by :class:`Process` class methods when querying a zombie process on UNIX (Windows doesn't have zombie processes). - *name* and *ppid* attributes are available if :meth:`Process.name()` or - :meth:`Process.ppid()` methods were called before the process turned into a + *name* and *ppid* attributes are available if :meth:`Process.name` or + :meth:`Process.ppid` methods were called before the process turned into a zombie. .. note:: this is a subclass of :class:`NoSuchProcess` so if you're not interested - in retrieving zombies (e.g. when using :func:`process_iter()`) you can + in retrieving zombies (e.g. when using :func:`process_iter`) you can ignore this exception and just catch :class:`NoSuchProcess`. .. versionadded:: 3.0.0 @@ -1092,13 +1092,13 @@ Exceptions Raised by :class:`Process` class methods when permission to perform an action is denied due to insufficient privileges. - *name* attribute is available if :meth:`Process.name()` was previously called. + *name* attribute is available if :meth:`Process.name` was previously called. .. class:: TimeoutExpired(seconds, pid=None, name=None, msg=None) Raised by :meth:`Process.wait` method if timeout expires and the process is still alive. - *name* attribute is available if :meth:`Process.name()` was previously called. + *name* attribute is available if :meth:`Process.name` was previously called. Process class ^^^^^^^^^^^^^ @@ -1127,7 +1127,7 @@ Process class the way this class is bound to a process is via its **PID**. That means that if the process terminates and the OS reuses its PID you may inadvertently end up querying another process. To prevent this problem - you can use :meth:`is_running()` first. + you can use :meth:`is_running` first. The only methods which preemptively check whether PID has been reused (via PID + creation time) are: :meth:`nice` (set), @@ -1152,7 +1152,7 @@ Process class :meth:`uids`, :meth:`create_time`, ...) may be fetched by using the same routine, but only one value is returned and the others are discarded. When using this context manager the internal routine is executed once (in - the example below on :meth:`name()`) the value of interest is returned and + the example below on :meth:`name`) the value of interest is returned and the others are cached. The subsequent calls sharing the same internal routine will return the cached value. @@ -1187,7 +1187,7 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`~Process.cpu_percent` | :meth:`cpu_times` | :meth:`cpu_times` | :meth:`~Process.cpu_percent` | :meth:`cmdline` | :meth:`cmdline` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`cpu_times` | :meth:`io_counters()` | :meth:`memory_info` | :meth:`cpu_times` | :meth:`create_time` | :meth:`create_time` | + | :meth:`cpu_times` | :meth:`io_counters` | :meth:`memory_info` | :meth:`cpu_times` | :meth:`create_time` | :meth:`create_time` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`create_time` | :meth:`memory_info` | :meth:`memory_percent` | :meth:`create_time` | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ @@ -1645,7 +1645,7 @@ Process class .. note:: the returned value is explicitly *not* split evenly between all available - CPUs (differently from :func:`psutil.cpu_percent()`). + CPUs (differently from :func:`psutil.cpu_percent`). This means that a busy loop process running on a system with 2 logical CPUs will be reported as having 100% CPU utilization instead of 50%. This was done in order to be consistent with ``top`` UNIX utility @@ -1699,7 +1699,7 @@ Process class .. method:: cpu_num() Return what CPU this process is currently running on. - The returned number should be ``<=`` :func:`psutil.cpu_count()`. + The returned number should be ``<=`` :func:`psutil.cpu_count`. On FreeBSD certain kernel process may return ``-1``. It may be used in conjunction with ``psutil.cpu_percent(percpu=True)`` to observe the system workload distributed across multiple CPUs as shown by @@ -2110,7 +2110,7 @@ Process class .. method:: net_connections(kind="inet") Return socket connections opened by process as a list of named tuples. - To get system-wide connections use :func:`psutil.net_connections()`. + To get system-wide connections use :func:`psutil.net_connections`. Every named tuple provides 6 attributes: - **fd**: the socket file descriptor. If the connection refers to the @@ -2218,14 +2218,14 @@ Process class another process, therefore it must be preferred over doing ``psutil.pid_exists(p.pid)``. If PID has been reused this method will also remove the process from - :func:`process_iter()` internal cache. + :func:`process_iter` internal cache. .. note:: this will return ``True`` also if the process is a zombie (``p.status() == psutil.STATUS_ZOMBIE``). .. versionchanged:: 6.0.0 - automatically remove process from :func:`process_iter()` internal cache + automatically remove process from :func:`process_iter` internal cache if PID has been reused by another process. .. method:: send_signal(signal) @@ -2234,7 +2234,7 @@ Process class checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals - are supported and *SIGTERM* is treated as an alias for :meth:`kill()`. + are supported and *SIGTERM* is treated as an alias for :meth:`kill`. See also how to `kill a process tree <#kill-process-tree>`_ and `terminate my children <#terminate-my-children>`_. @@ -2296,7 +2296,7 @@ Process class immediately or raise :class:`TimeoutExpired`. The return value is cached. - To wait for multiple processes use :func:`psutil.wait_procs()`. + To wait for multiple processes use :func:`psutil.wait_procs`. >>> import psutil >>> p = psutil.Process(9891) @@ -2558,7 +2558,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ProcessStatus `enum.StrEnum`_ collection of :data:`STATUS_* ` - constants. Returned by :meth:`Process.status()`. + constants. Returned by :meth:`Process.status`. .. versionadded:: 8.0.0 @@ -2662,7 +2662,7 @@ Process status constants .. data:: STATUS_WAITING (FreeBSD) .. data:: STATUS_SUSPENDED (NetBSD) - Represent a process status. Returned by :meth:`Process.status()`. + Represent a process status. Returned by :meth:`Process.status`. These constants are members of the :class:`psutil.ProcessStatus` enum. .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) @@ -2685,7 +2685,7 @@ Process priority constants .. data:: BELOW_NORMAL_PRIORITY_CLASS Represent the priority of a process on Windows (see `SetPriorityClass`_). - They can be used in conjunction with :meth:`Process.nice()` to get or + They can be used in conjunction with :meth:`Process.nice` to get or set process priority. These constants are members of the :class:`psutil.ProcessPriority` enum. @@ -2702,7 +2702,7 @@ Process priority constants .. data:: IOPRIO_CLASS_IDLE A set of integers representing the I/O priority of a process on Linux. They - can be used in conjunction with :meth:`Process.ionice()` to get or set + can be used in conjunction with :meth:`Process.ionice` to get or set process I/O priority. These constants are members of the :class:`psutil.ProcessIOPriority` enum. @@ -2728,7 +2728,7 @@ Process priority constants .. data:: IOPRIO_HIGH A set of integers representing the I/O priority of a process on Windows. - They can be used in conjunction with :meth:`Process.ionice()` to get + They can be used in conjunction with :meth:`Process.ionice` to get or set process I/O priority. These constants are members of the :class:`psutil.ProcessIOPriority` enum. @@ -2774,7 +2774,7 @@ FreeBSD specific: .. data:: RLIMIT_NPTS Constants used for getting and setting process resource limits to be used in -conjunction with :meth:`Process.rlimit()`. See `resource.getrlimit`_ +conjunction with :meth:`Process.rlimit`. See `resource.getrlimit`_ for further information. These constants are members of the :class:`psutil.ProcessRlimit` enum. @@ -2809,7 +2809,7 @@ Connections constants .. data:: CONN_BOUND (Solaris) A set of strings representing the status of a TCP connection. - Returned by :meth:`Process.net_connections()` and + Returned by :meth:`Process.net_connections` and :func:`psutil.net_connections` (`status` field). These constants are members of the :class:`psutil.ConnectionStatus` enum. @@ -2824,7 +2824,7 @@ Hardware constants .. data:: AF_LINK Constant which identifies a MAC address associated with a network interface. - To be used in conjunction with :func:`psutil.net_if_addrs()`. + To be used in conjunction with :func:`psutil.net_if_addrs`. .. versionadded:: 3.0.0 @@ -2837,7 +2837,7 @@ Hardware constants half mode speed. NIC_DUPLEX_FULL means the NIC is able to send and receive data (files) simultaneously, NIC_DUPLEX_FULL means the NIC can either send or receive data at a time. - To be used in conjunction with :func:`psutil.net_if_stats()`. + To be used in conjunction with :func:`psutil.net_if_stats`. .. versionadded:: 3.0.0 @@ -2847,7 +2847,7 @@ Hardware constants Whether the remaining time of the battery cannot be determined or is unlimited. - May be assigned to :func:`psutil.sensors_battery()`'s *secsleft* field. + May be assigned to :func:`psutil.sensors_battery`'s *secsleft* field. .. versionadded:: 5.1.0 @@ -2885,3 +2885,78 @@ Other constants >>> import psutil >>> if psutil.version_info >= (4, 5): ... pass + +.. _`BPO-10784`: https://bugs.python.org/issue10784 +.. _`BPO-12442`: https://bugs.python.org/issue12442 +.. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 +.. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ +.. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get +.. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt +.. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html +.. _`man prlimit`: https://linux.die.net/man/2/prlimit +.. _`psleak`: https://github.com/giampaolo/psleak + +.. === docs.python.org + +.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 +.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET +.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX +.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum +.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum +.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum +.. _`hash`: https://docs.python.org/3/library/functions.html#hash +.. _`open`: https://docs.python.org/3/library/functions.html#open +.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count +.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg +.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid +.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority +.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid +.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid +.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY +.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC +.. _`os.open`: https://docs.python.org/3/library/os.html#os.open +.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open +.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority +.. _`os.times`: https://docs.python.org//library/os.html#os.times +.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit +.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit +.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue +.. _`select.poll`: https://docs.python.org//library/select.html#select.poll +.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. +.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. +.. _`signal module`: https://docs.python.org//library/signal.html +.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM +.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET +.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM +.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd +.. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait +.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen +.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident +.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread + +.. === scripts + +.. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py + +.. === Windows API + +.. _`GetDriveType`: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea +.. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess +.. _`GetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +.. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex +.. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass +.. _`TerminateProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f79a9f5e2..0308ca307c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,4 @@ .. currentmodule:: psutil -.. include:: _links.rst Changelog ========= @@ -27,7 +26,7 @@ Doc: - Added a summary table of officially supported operating systems and architectures. - Added tons of new examples in ``docs/recipes.rst``. - - Drastically improved :func:`virtual_memory()` doc, which is now more + - Drastically improved :func:`virtual_memory` doc, which is now more detailed, and includes a table with all the available metrics on each platform. - Show a clickable COPY button to copy code snippets. @@ -53,13 +52,13 @@ Type hints / enums: New APIs: -- :gh:`2729`: New :meth:`Process.page_faults()` method, returning a ``(minor, +- :gh:`2729`: New :meth:`Process.page_faults` method, returning a ``(minor, major)`` namedtuple. - :gh:`2731`, :gh:`2736`, :gh:`2723`, :gh:`2733`: Reorganization of process memory APIs. - - Add new :meth:`Process.memory_info_ex()` method, which extends - :meth:`Process.memory_info()` with platform-specific metrics: + - Add new :meth:`Process.memory_info_ex` method, which extends + :meth:`Process.memory_info` with platform-specific metrics: - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, *swap*, *hugetlb* @@ -67,11 +66,11 @@ New APIs: *phys_footprint* - Windows: *virtual*, *peak_virtual* - - Add new :meth:`Process.memory_footprint()` method, which returns *uss*, - *pss* and *swap* metrics (what :meth:`Process.memory_full_info()` used to + - Add new :meth:`Process.memory_footprint` method, which returns *uss*, + *pss* and *swap* metrics (what :meth:`Process.memory_full_info` used to return, which is now **deprecated**). - - :meth:`Process.memory_info()` named tuple changed: + - :meth:`Process.memory_info` named tuple changed: - BSD: added *peak_rss*. @@ -79,23 +78,23 @@ New APIs: aliases returning 0 and emitting `DeprecationWarning` are kept. - macOS: *pfaults* and *pageins* removed with **no - backward-compataliases**. Use :meth:`Process.page_faults()` instead. + backward-compataliases**. Use :meth:`Process.page_faults` instead. - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, - *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex()`. All + *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex`. All these old names still work but raise `DeprecationWarning`. - - :meth:`Process.memory_full_info()` is **deprecated**. Use the new - :meth:`Process.memory_footprint()` instead. + - :meth:`Process.memory_full_info` is **deprecated**. Use the new + :meth:`Process.memory_footprint` instead. Others: -- :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times()` +- :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times` has been normalized on all platforms, and the first 3 fields are now always ``user, system, idle``. See compatibility notes below. -- :gh:`2754`: standardize :func:`sensors_battery()`'s `percent` so that it +- :gh:`2754`: standardize :func:`sensors_battery`'s `percent` so that it returns a `float` instead of `int` on all systems, not only Linux. - :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update changelog.rst and credits.rst when commenting with /changelog. @@ -104,15 +103,15 @@ Others: **Bug fixes** -- :gh:`2726`, [macOS]: :meth:`Process.num_ctx_switches()` return an unusual +- :gh:`2726`, [macOS]: :meth:`Process.num_ctx_switches` return an unusual high number due to a C type precision issue. -- :gh:`2411` [macOS]: :meth:`Process.cpu_times()` and - :meth:`Process.cpu_percent()` calculation on macOS x86_64 (arm64 is fine) was +- :gh:`2411` [macOS]: :meth:`Process.cpu_times` and + :meth:`Process.cpu_percent` calculation on macOS x86_64 (arm64 is fine) was highly inaccurate (41.67x lower). - :gh:`2732`, [Linux]: net_if_duplex_speed: handle EBUSY from ioctl(SIOCETHTOOL). -- :gh:`2744`, [NetBSD]: fix possible double `free()` in :func:`swap_memory()`. -- :gh:`2746`, [FreeBSD]: :meth:`Process.memory_maps()`, `rss` and `private` +- :gh:`2744`, [NetBSD]: fix possible double `free()` in :func:`swap_memory`. +- :gh:`2746`, [FreeBSD]: :meth:`Process.memory_maps`, `rss` and `private` fields, are erroneously reported in memory pages instead of bytes. Other platforms (Linux, macOS, Windows) return bytes. @@ -124,7 +123,7 @@ Changes that break backwards compatibility. Named tuples: -- :func:`cpu_times()`: +- :func:`cpu_times`: - On Linux, macOS and BSD the field order of the returned named tuple changed: ``user, system, idle`` are now always the first 3 fields on all @@ -132,7 +131,7 @@ Named tuples: Positional access (e.g. ``cpu_times()[3]``) may silently return the wrong field. Always use attribute access instead (e.g. ``cpu_times().idle``). -- :meth:`Process.memory_info()`: +- :meth:`Process.memory_info`: - The returned named tuple changed size and field order. Positional access (e.g. ``p.memory_info()[3]`` or ``a, b, c = p.memory_info()``) may break or @@ -141,14 +140,14 @@ Named tuples: Enums: -- :meth:`Process.status()` now returns a :class:`psutil.ProcessStatus` enum +- :meth:`Process.status` now returns a :class:`psutil.ProcessStatus` enum member instead of a plain ``str``. Since :class:`psutil.ProcessStatus` is a ``StrEnum``, it compares equal to its string value (e.g. ``p.status() == "running"`` still works), but ``repr()`` and ``type()`` differ. Use :data:`psutil.STATUS_RUNNING` and friends as before; ``psutil.STATUS_RUNNING`` is now an alias for the enum member. -- :func:`net_connections()` and :meth:`Process.net_connections()`: the +- :func:`net_connections` and :meth:`Process.net_connections`: the ``status`` field now returns a :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. Same ``StrEnum`` compatibility rules as above apply. @@ -163,7 +162,7 @@ Enums: **Bug fixes** -- :gh:`2715`, [Linux]: ``wait_pid_pidfd_open()`` (from :meth:`Process.wait()`) +- :gh:`2715`, [Linux]: ``wait_pid_pidfd_open()`` (from :meth:`Process.wait`) crashes with ``EINVAL`` due to kernel race condition. 7.2.2 — 2026-01-28 @@ -171,11 +170,11 @@ Enums: **Enhancements** -- :gh:`2705`: [Linux]: :meth:`Process.wait()` now uses ``pidfd_open()`` + +- :gh:`2705`: [Linux]: :meth:`Process.wait` now uses ``pidfd_open()`` + ``poll()`` for waiting, resulting in no busy loop and faster response times. Requires Linux >= 5.3 and Python >= 3.9. Falls back to traditional polling if unavailable. -- :gh:`2705`: [macOS], [BSD]: :meth:`Process.wait()` now uses ``kqueue()`` for +- :gh:`2705`: [macOS], [BSD]: :meth:`Process.wait` now uses ``kqueue()`` for waiting, resulting in no busy loop and faster response times. **Bug fixes** @@ -184,7 +183,7 @@ Enums: Fedorov) - :gh:`2707`, [macOS]: fix potential memory leaks in error paths of `Process.memory_full_info()` and `Process.threads()`. -- :gh:`2708`, [macOS]: :meth:`Process.cmdline()` and :meth:`Process.environ()` +- :gh:`2708`, [macOS]: :meth:`Process.cmdline` and :meth:`Process.environ` may fail with ``OSError: [Errno 0] Undefined error`` (from ``sysctl(KERN_PROCARGS2)``). They now raise :exc:`AccessDenied` instead. @@ -193,7 +192,7 @@ Enums: **Bug fixes** -- :gh:`2699`, [FreeBSD], [NetBSD]: :func:`heap_info()` does not detect small +- :gh:`2699`, [FreeBSD], [NetBSD]: :func:`heap_info` does not detect small allocations (<= 1K). In order to fix that, we now flush internal jemalloc cache before fetching the metrics. @@ -202,7 +201,7 @@ Enums: **Enhancements** -- :gh:`1275`: new :func:`heap_info()` and :func:`heap_trim()` functions, +- :gh:`1275`: new :func:`heap_info` and :func:`heap_trim` functions, providing direct access to the platform's native C heap allocator (glibc, mimalloc, libmalloc). Useful to create tools to detect memory leaks. - :gh:`2403`, [Linux]: publish wheels for Linux musl. @@ -213,7 +212,7 @@ Enums: * :gh:`2684`, [FreeBSD], [critical]: compilation fails on FreeBSD 14 due to missing include. -* :gh:`2691`, [Windows]: fix memory leak in :func:`net_if_stats()` due to +* :gh:`2691`, [Windows]: fix memory leak in :func:`net_if_stats` due to missing ``Py_CLEAR``. **Compatibility notes** @@ -237,13 +236,13 @@ Enums: **Bug fixes** -- :gh:`2674`, [Windows]: :func:`disk_usage()` could truncate values on 32-bit +- :gh:`2674`, [Windows]: :func:`disk_usage` could truncate values on 32-bit platforms, potentially reporting incorrect total/free/used space for drives larger than 4GB. -- :gh:`2675`, [macOS]: :meth:`Process.status()` incorrectly returns "running" +- :gh:`2675`, [macOS]: :meth:`Process.status` incorrectly returns "running" for 99% of the processes. - :gh:`2677`, [Windows]: fix MAC address string construction in - :func:`net_if_addrs()`. Previously, the MAC address buffer was incorrectly + :func:`net_if_addrs`. Previously, the MAC address buffer was incorrectly updated using a fixed increment and `sprintf_s`, which could overflow or misformat the string if the MAC length or formatting changed. Also, the final '\n' was inserted unnecessarily. @@ -263,9 +262,9 @@ Enums: **Bug fixes** -- :gh:`2650`, [macOS]: :meth:`Process.cmdline()` and :meth:`Process.environ()` +- :gh:`2650`, [macOS]: :meth:`Process.cmdline` and :meth:`Process.environ` may incorrectly raise :exc:`NoSuchProcess` instead of :exc:`ZombieProcess`. -- :gh:`2658`, [macOS]: double ``free()`` in :meth:`Process.environ()` when it +- :gh:`2658`, [macOS]: double ``free()`` in :meth:`Process.environ` when it fails internally. This posed a risk of segfault. - :gh:`2662`, [macOS]: massive C code cleanup to guard against possible segfaults which were (not so) sporadically spotted on CI. @@ -287,7 +286,7 @@ Enums: - :gh:`2641`, [SunOS]: cannot compile psutil from sources due to missing C include. -- :gh:`2357`, [SunOS]: :meth:`Process.cmdline()` does not handle spaces +- :gh:`2357`, [SunOS]: :meth:`Process.cmdline` does not handle spaces properly. (patch by Ben Raz) **Compatibility notes** @@ -308,44 +307,44 @@ Enums: - :gh:`2473`, [macOS]: Fix build issue on macOS 11 and lower. - :gh:`2494`, [Windows]: All APIs dealing with paths, such as - :meth:`Process.memory_maps()`, :meth:`Process.exe()` and - :meth:`Process.open_files()` does not properly handle UNC paths. Paths such + :meth:`Process.memory_maps`, :meth:`Process.exe` and + :meth:`Process.open_files` does not properly handle UNC paths. Paths such as ``\\??\\C:\\Windows\\Temp`` and ``'\\Device\\HarddiskVolume1\\Windows\\Temp'`` are now converted to ``C:\\Windows\\Temp``. (patch by Ben Peddell) - :gh:`2506`, [Windows]: Windows service APIs had issues with unicode services using special characters in their name. -- :gh:`2514`, [Linux]: :meth:`Process.cwd()` sometimes fail with +- :gh:`2514`, [Linux]: :meth:`Process.cwd` sometimes fail with `FileNotFoundError` due to a race condition. -- :gh:`2526`, [Linux]: :meth:`Process.create_time()`, which is used to +- :gh:`2526`, [Linux]: :meth:`Process.create_time`, which is used to univocally identify a process over time, is subject to system clock updates, - and as such can lead to :meth:`Process.is_running()` returning a wrong + and as such can lead to :meth:`Process.is_running` returning a wrong result. A monotonic creation time is now used instead. (patch by Jonathan Kohler) -- :gh:`2528`, [Linux]: :meth:`Process.children()` may raise +- :gh:`2528`, [Linux]: :meth:`Process.children` may raise ``PermissionError``. It will now raise :exc:`AccessDenied` instead. -- :gh:`2540`, [macOS]: :func:`boot_time()` is off by 45 seconds (C precision +- :gh:`2540`, [macOS]: :func:`boot_time` is off by 45 seconds (C precision issue). - :gh:`2541`, :gh:`2570`, :gh:`2578` [Linux], [macOS], [NetBSD]: - :meth:`Process.create_time()` does not reflect system clock updates. -- :gh:`2542`: if system clock is updated :meth:`Process.children()` and - :meth:`Process.parent()` may not be able to return the right information. + :meth:`Process.create_time` does not reflect system clock updates. +- :gh:`2542`: if system clock is updated :meth:`Process.children` and + :meth:`Process.parent` may not be able to return the right information. - :gh:`2545`: [Illumos]: Fix handling of MIB2_UDP_ENTRY in - :func:`net_connections()`. -- :gh:`2552`, [Windows]: :func:`boot_time()` didn't take into account the time + :func:`net_connections`. +- :gh:`2552`, [Windows]: :func:`boot_time` didn't take into account the time spent during suspend / hibernation. -- :gh:`2560`, [Linux]: :meth:`Process.memory_maps()` may crash with +- :gh:`2560`, [Linux]: :meth:`Process.memory_maps` may crash with `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch by Julien Stephan) - :gh:`2586`, [macOS], [CRITICAL]: fixed different places in C code which can trigger a segfault. -- :gh:`2604`, [Linux]: :func:`virtual_memory()` "used" memory does not match +- :gh:`2604`, [Linux]: :func:`virtual_memory` "used" memory does not match recent versions of ``free`` CLI utility. (patch by Isaac K. Ko) -- :gh:`2605`, [Linux]: :func:`sensors_battery()` reports a negative amount for +- :gh:`2605`, [Linux]: :func:`sensors_battery` reports a negative amount for seconds left. - :gh:`2607`, [Windows]: ``WindowsService.description()`` method may fail with ``ERROR_NOT_FOUND``. Now it returns an empty string instead. -- 2610:, [macOS], [CRITICAL]: fix :func:`cpu_freq()` segfault on ARM +- 2610:, [macOS], [CRITICAL]: fix :func:`cpu_freq` segfault on ARM architectures. **Compatibility notes** @@ -357,7 +356,7 @@ Enums: **Enhancements** -- :gh:`669`, [Windows]: :func:`net_if_addrs()` also returns the ``broadcast`` +- :gh:`669`, [Windows]: :func:`net_if_addrs` also returns the ``broadcast`` address instead of ``None``. - :gh:`2480`: Python 2.7 is no longer supported. Latest version supporting Python 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. @@ -369,7 +368,7 @@ Enums: - :gh:`2496`, [Linux]: Avoid segfault (a cPython bug) on ``Process.memory_maps()`` for processes that use hundreds of GBs of memory. -- :gh:`2502`, [macOS]: :func:`virtual_memory()` now relies on +- :gh:`2502`, [macOS]: :func:`virtual_memory` now relies on ``host_statistics64`` instead of ``host_statistics``. This is the same approach used by ``vm_stat`` CLI tool, and should grant more accurate results. @@ -390,7 +389,7 @@ Enums: - :gh:`2418`, [Linux]: fix race condition in case /proc/PID/stat does not exist, but /proc/PID does, resulting in FileNotFoundError. -- :gh:`2470`, [Linux]: :func:`users()` may return "localhost" instead of the +- :gh:`2470`, [Linux]: :func:`users` may return "localhost" instead of the actual IP address of the user logged in. 6.1.0 — 2024-10-17 @@ -398,9 +397,9 @@ Enums: **Enhancements** -- :gh:`2366`, [Windows]: drastically speedup :func:`process_iter()`. We now +- :gh:`2366`, [Windows]: drastically speedup :func:`process_iter`. We now determine process unique identity by using process "fast" create time method. - This will considerably speedup those apps which use :func:`process_iter()` + This will considerably speedup those apps which use :func:`process_iter` only once, e.g. to look for a process with a certain name. - :gh:`2446`: use pytest instead of unittest. - :gh:`2448`: add ``make install-sysdeps`` target to install the necessary @@ -421,9 +420,9 @@ Enums: - :gh:`2455`, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and field 40 (blkio_ticks) is missing. - :gh:`2457`, [AIX]: significantly improve the speed of - :meth:`Process.open_files()` for some edge cases. -- :gh:`2460`, [OpenBSD]: :meth:`Process.num_fds()` and - :meth:`Process.open_files()` may fail with :exc:`NoSuchProcess` for PID 0. + :meth:`Process.open_files` for some edge cases. +- :gh:`2460`, [OpenBSD]: :meth:`Process.num_fds` and + :meth:`Process.open_files` may fail with :exc:`NoSuchProcess` for PID 0. Instead, we now return "null" values (0 and [] respectively). 6.0.0 — 2024-06-18 @@ -432,20 +431,20 @@ Enums: **Enhancements** - :gh:`2109`: ``maxfile`` and ``maxpath`` fields were removed from the - namedtuple returned by :func:`disk_partitions()`. Reason: on network + namedtuple returned by :func:`disk_partitions`. Reason: on network filesystems (NFS) this can potentially take a very long time to complete. - :gh:`2366`, [Windows]: log debug message when using slower process APIs. - :gh:`2375`, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) -- :gh:`2396`: :func:`process_iter()` no longer pre-emptively checks whether - PIDs have been reused. This makes :func:`process_iter()` around 20x times +- :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether + PIDs have been reused. This makes :func:`process_iter` around 20x times faster. - :gh:`2396`: a new ``psutil.process_iter.cache_clear()`` API can be used the clear - :func:`process_iter()` internal cache. + :func:`process_iter` internal cache. - :gh:`2401`, Support building with free-threaded CPython 3.13. (patch by Sam Gross) -- :gh:`2407`: :meth:`Process.connections()` was renamed to - :meth:`Process.net_connections()`. The old name is still available, but it's +- :gh:`2407`: :meth:`Process.connections` was renamed to + :meth:`Process.net_connections`. The old name is still available, but it's deprecated (triggers a ``DeprecationWarning``) and will be removed in the future. - :gh:`2425`: [Linux]: provide aarch64 wheels. (patch by Matthieu Darbois / @@ -453,19 +452,19 @@ Enums: **Bug fixes** -- :gh:`2250`, [NetBSD]: :meth:`Process.cmdline()` sometimes fail with EBUSY. It +- :gh:`2250`, [NetBSD]: :meth:`Process.cmdline` sometimes fail with EBUSY. It usually happens for long cmdlines with lots of arguments. In this case retry getting the cmdline for up to 50 times, and return an empty list as last resort. - :gh:`2254`, [Linux]: offline cpus raise NotImplementedError in cpu_freq() (patch by Shade Gladden) - :gh:`2272`: Add pickle support to psutil Exceptions. -- :gh:`2359`, [Windows], [CRITICAL]: :func:`pid_exists()` disagrees with +- :gh:`2359`, [Windows], [CRITICAL]: :func:`pid_exists` disagrees with :class:`Process` on whether a pid exists when ERROR_ACCESS_DENIED. - :gh:`2360`, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) - :gh:`2362`, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) - :gh:`2365`, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) -- :gh:`2395`, [OpenBSD]: :func:`pid_exists()` erroneously return True if the +- :gh:`2395`, [OpenBSD]: :func:`pid_exists` erroneously return True if the argument is a thread ID (TID) instead of a PID (process ID). - :gh:`2412`, [macOS]: can't compile on macOS 10.4 PowerPC due to missing `MNT_` constants. @@ -474,16 +473,16 @@ Enums: Version 6.0.0 introduces some changes which affect backward compatibility: -- :gh:`2109`: the namedtuple returned by :func:`disk_partitions()`' no longer +- :gh:`2109`: the namedtuple returned by :func:`disk_partitions`' no longer has ``maxfile`` and ``maxpath`` fields. -- :gh:`2396`: :func:`process_iter()` no longer pre-emptively checks whether +- :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether PIDs have been reused. If you want to check for PID reusage you are supposed to use - :meth:`Process.is_running()` against the yielded :class:`Process` instances. - That will also automatically remove reused PIDs from :func:`process_iter()` + :meth:`Process.is_running` against the yielded :class:`Process` instances. + That will also automatically remove reused PIDs from :func:`process_iter` internal cache. -- :gh:`2407`: :meth:`Process.connections()` was renamed to - :meth:`Process.net_connections()`. The old name is still available, but it's +- :gh:`2407`: :meth:`Process.connections` was renamed to + :meth:`Process.net_connections`. The old name is still available, but it's deprecated (triggers a ``DeprecationWarning``) and will be removed in the future. @@ -492,7 +491,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`2343`, [FreeBSD]: filter :func:`net_connections()` returned list in C +- :gh:`2343`, [FreeBSD]: filter :func:`net_connections` returned list in C instead of Python, and avoid to retrieve unnecessary connection types unless explicitly asked. E.g., on an IDLE system with few IPv6 connections this will run around 4 times faster. Before all connection types (TCP, UDP, UNIX) were @@ -502,9 +501,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`930`, [NetBSD], [critical]: :func:`net_connections()` implementation was +- :gh:`930`, [NetBSD], [critical]: :func:`net_connections` implementation was broken. It could either leak memory or core dump. -- :gh:`2340`, [NetBSD]: if process is terminated, :meth:`Process.cwd()` will +- :gh:`2340`, [NetBSD]: if process is terminated, :meth:`Process.cwd` will return an empty string instead of raising :exc:`NoSuchProcess`. - :gh:`2345`, [Linux]: fix compilation on older compiler missing DUPLEX_UNKNOWN. @@ -529,7 +528,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1703`: :func:`cpu_percent()` and :func:`cpu_times_percent()` are now +- :gh:`1703`: :func:`cpu_percent` and :func:`cpu_times_percent` are now thread safe, meaning they can be called from different threads and still return meaningful and independent results. Before, if (say) 10 threads called ``cpu_percent(interval=None)`` at the same time, only 1 thread out of 10 @@ -537,9 +536,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2266`: if :class:`Process` class is passed a very high PID, raise :exc:`NoSuchProcess` instead of OverflowError. (patch by Xuehai Pan) - :gh:`2246`: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) -- :gh:`2290`: PID reuse is now pre-emptively checked for :meth:`Process.ppid()` +- :gh:`2290`: PID reuse is now pre-emptively checked for :meth:`Process.ppid` and - :meth:`Process.parents()`. + :meth:`Process.parents`. - :gh:`2312`: use ``ruff`` Python linter instead of ``flake8 + isort``. It's an order of magnitude faster + it adds a ton of new code quality checks. @@ -551,26 +550,26 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2241`, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas Klausner) - :gh:`2245`, [Windows]: fix var unbound error on possibly in - :func:`swap_memory()` (patch by student_2333) + :func:`swap_memory` (patch by student_2333) - :gh:`2268`: ``bytes2human()`` utility function was unable to properly represent negative values. -- :gh:`2252`, [Windows]: :func:`disk_usage()` fails on Python 3.12+. (patch by +- :gh:`2252`, [Windows]: :func:`disk_usage` fails on Python 3.12+. (patch by Matthieu Darbois) -- :gh:`2284`, [Linux]: :meth:`Process.memory_full_info()` may incorrectly raise +- :gh:`2284`, [Linux]: :meth:`Process.memory_full_info` may incorrectly raise :exc:`ZombieProcess` if it's determined via ``/proc/pid/smaps_rollup``. Instead we now fallback on reading ``/proc/pid/smaps``. -- :gh:`2287`, [OpenBSD], [NetBSD]: :meth:`Process.is_running()` erroneously +- :gh:`2287`, [OpenBSD], [NetBSD]: :meth:`Process.is_running` erroneously return ``False`` for zombie processes, because creation time cannot be determined. - :gh:`2288`, [Linux]: correctly raise :exc:`ZombieProcess` on - :meth:`Process.exe()`, - :meth:`Process.cmdline()` and :meth:`Process.memory_maps()` instead of + :meth:`Process.exe`, + :meth:`Process.cmdline` and :meth:`Process.memory_maps` instead of returning a "null" value. - :gh:`2290`: differently from what stated in the doc, PID reuse is not - pre-emptively checked for :meth:`Process.nice()` (set), - :meth:`Process.ionice()`, (set), :meth:`Process.cpu_affinity()` (set), - :meth:`Process.rlimit()` (set), :meth:`Process.parent()`. -- :gh:`2308`, [OpenBSD]: :meth:`Process.threads()` always fail with + pre-emptively checked for :meth:`Process.nice` (set), + :meth:`Process.ionice`, (set), :meth:`Process.cpu_affinity` (set), + :meth:`Process.rlimit` (set), :meth:`Process.parent`. +- :gh:`2308`, [OpenBSD]: :meth:`Process.threads` always fail with AccessDenied (also as root). 5.9.5 — 2023-04-17 @@ -582,27 +581,27 @@ Version 6.0.0 introduces some changes which affect backward compatibility: the `KeyError` bit deriving from a missed cache hit. - :gh:`2217`: print the full traceback when a `DeprecationWarning` or `UserWarning` is raised. -- :gh:`2230`, [OpenBSD]: :func:`net_connections()` implementation was rewritten +- :gh:`2230`, [OpenBSD]: :func:`net_connections` implementation was rewritten from scratch: - We're now able to retrieve the path of AF_UNIX sockets (before it was an empty string) - The function is faster since it no longer iterates over all processes. - No longer produces duplicate connection entries. -- :gh:`2238`: there are cases where :meth:`Process.cwd()` cannot be determined +- :gh:`2238`: there are cases where :meth:`Process.cwd` cannot be determined (e.g. directory no longer exists), in which case we returned either ``None`` or an empty string. This was consolidated and we now return ``""`` on all platforms. - :gh:`2239`, [UNIX]: if process is a zombie, and we can only determine part of - the its truncated :meth:`Process.name()` (15 chars), don't fail with + the its truncated :meth:`Process.name` (15 chars), don't fail with :exc:`ZombieProcess` when we try to guess the full name from the - :meth:`Process.cmdline()`. Just return the truncated name. + :meth:`Process.cmdline`. Just return the truncated name. - :gh:`2240`, [NetBSD], [OpenBSD]: add CI testing on every commit for NetBSD and OpenBSD platforms (python 3 only). **Bug fixes** -- :gh:`1043`, [OpenBSD] :func:`net_connections()` returns duplicate entries. +- :gh:`1043`, [OpenBSD] :func:`net_connections` returns duplicate entries. - :gh:`1915`, [Linux]: on certain kernels, ``"MemAvailable"`` field from ``/proc/meminfo`` returns ``0`` (possibly a kernel bug), in which case we calculate an approximation for ``available`` memory which matches "free" CLI @@ -610,22 +609,22 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2164`, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). - :gh:`2186`, [FreeBSD]: compilation fails with Clang 15. (patch by Po-Chuan Hsieh) -- :gh:`2191`, [Linux]: :func:`disk_partitions()`: do not unnecessarily read +- :gh:`2191`, [Linux]: :func:`disk_partitions`: do not unnecessarily read /proc/filesystems and raise :exc:`AccessDenied` unless user specified `all=False` argument. - :gh:`2216`, [Windows]: fix tests when running in a virtual environment (patch by Matthieu Darbois) -- :gh:`2225`, [POSIX]: :func:`users()` loses precision for ``started`` +- :gh:`2225`, [POSIX]: :func:`users` loses precision for ``started`` attribute (off by 1 minute). - :gh:`2229`, [OpenBSD]: unable to properly recognize zombie processes. :exc:`NoSuchProcess` may be raised instead of :exc:`ZombieProcess`. -- :gh:`2231`, [NetBSD]: *available* :func:`virtual_memory()` is higher than +- :gh:`2231`, [NetBSD]: *available* :func:`virtual_memory` is higher than *total*. -- :gh:`2234`, [NetBSD]: :func:`virtual_memory()` metrics are wrong: *available* +- :gh:`2234`, [NetBSD]: :func:`virtual_memory` metrics are wrong: *available* and *used* are too high. We now match values shown by *htop* CLI utility. -- :gh:`2236`, [NetBSD]: :meth:`Process.num_threads()` and - :meth:`Process.threads()` return threads that are already terminated. -- :gh:`2237`, [OpenBSD], [NetBSD]: :meth:`Process.cwd()` may raise +- :gh:`2236`, [NetBSD]: :meth:`Process.num_threads` and + :meth:`Process.threads` return threads that are already terminated. +- :gh:`2237`, [OpenBSD], [NetBSD]: :meth:`Process.cwd` may raise ``FileNotFoundError`` if cwd no longer exists. Return an empty string instead. @@ -640,7 +639,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`2077`, [Windows]: Use system-level values for :func:`virtual_memory()`. +- :gh:`2077`, [Windows]: Use system-level values for :func:`virtual_memory`. (patch by Daniel Widdis) - :gh:`2156`, [Linux]: compilation may fail on very old gcc compilers due to missing ``SPEED_UNKNOWN`` definition. (patch by Amir Rossert) @@ -659,18 +658,18 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`2116`, [macOS], [critical]: :func:`net_connections()` fails with +- :gh:`2116`, [macOS], [critical]: :func:`net_connections` fails with RuntimeError. -- :gh:`2135`, [macOS]: :meth:`Process.environ()` may contain garbage data. Fix +- :gh:`2135`, [macOS]: :meth:`Process.environ` may contain garbage data. Fix out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) - :gh:`2138`, [Linux], **[critical]**: can't compile psutil on Android due to undefined ``ethtool_cmd_speed`` symbol. -- :gh:`2142`, [POSIX]: :func:`net_if_stats()` 's ``flags`` on Python 2 returned +- :gh:`2142`, [POSIX]: :func:`net_if_stats` 's ``flags`` on Python 2 returned unicode instead of str. (patch by Matthieu Darbois) - :gh:`2147`, [macOS] Fix disk usage report on macOS 12+. (patch by Matthieu Darbois) -- :gh:`2150`, [Linux] :meth:`Process.threads()` may raise ``NoSuchProcess``. +- :gh:`2150`, [Linux] :meth:`Process.threads` may raise ``NoSuchProcess``. Fix race condition. (patch by Daniel Li) - :gh:`2153`, [macOS] Fix race condition in test_posix.TestProcess.test_cmdline. (patch by Matthieu Darbois) @@ -680,14 +679,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`2093`, [FreeBSD], **[critical]**: :func:`pids()` may fail with ENOMEM. +- :gh:`2093`, [FreeBSD], **[critical]**: :func:`pids` may fail with ENOMEM. Dynamically increase the ``malloc()`` buffer size until it's big enough. -- :gh:`2095`, [Linux]: :func:`net_if_stats()` returns incorrect interface speed +- :gh:`2095`, [Linux]: :func:`net_if_stats` returns incorrect interface speed for 100GbE network cards. -- :gh:`2113`, [FreeBSD], **[critical]**: :func:`virtual_memory()` may raise +- :gh:`2113`, [FreeBSD], **[critical]**: :func:`virtual_memory` may raise ENOMEM due to missing ``#include `` directive. (patch by Peter Jeremy) -- :gh:`2128`, [NetBSD]: :func:`swap_memory()` was miscalculated. (patch by +- :gh:`2128`, [NetBSD]: :func:`swap_memory` was miscalculated. (patch by Thomas Klausner) 5.9.1 — 2022-05-20 @@ -701,8 +700,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2050`, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading ``/proc`` pseudo files line by line. This should help having more consistent results. -- :gh:`2057`, [OpenBSD]: add support for :func:`cpu_freq()`. -- :gh:`2107`, [Linux]: :meth:`Process.memory_full_info()` (reporting process +- :gh:`2057`, [OpenBSD]: add support for :func:`cpu_freq`. +- :gh:`2107`, [Linux]: :meth:`Process.memory_full_info` (reporting process USS/PSS/Swap memory) now reads ``/proc/pid/smaps_rollup`` instead of ``/proc/pids/smaps``, which makes it 5 times faster. @@ -710,9 +709,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2048`: ``AttributeError`` is raised if ``psutil.Error`` class is raised manually and passed through ``str``. -- :gh:`2049`, [Linux]: :func:`cpu_freq()` erroneously returns ``curr`` value in +- :gh:`2049`, [Linux]: :func:`cpu_freq` erroneously returns ``curr`` value in GHz while ``min`` and ``max`` are in MHz. -- :gh:`2050`, [Linux]: :func:`virtual_memory()` may raise ``ValueError`` if +- :gh:`2050`, [Linux]: :func:`virtual_memory` may raise ``ValueError`` if running in a LCX container. 5.9.0 — 2021-12-29 @@ -720,7 +719,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1851`, [Linux]: :func:`cpu_freq()` is slow on systems with many CPUs. +- :gh:`1851`, [Linux]: :func:`cpu_freq` is slow on systems with many CPUs. Read current frequency values for all CPUs from ``/proc/cpuinfo`` instead of opening many files in ``/sys`` fs. (patch by marxin) - :gh:`1992`: :exc:`NoSuchProcess` message now specifies if the PID has been @@ -729,7 +728,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: now have a better formatted and separated ``__repr__`` and ``__str__`` implementations. - :gh:`1996`, [BSD]: add support for MidnightBSD. (patch by Saeed Rasooli) -- :gh:`1999`, [Linux]: :func:`disk_partitions()`: convert ``/dev/root`` device +- :gh:`1999`, [Linux]: :func:`disk_partitions`: convert ``/dev/root`` device (an alias used on some Linux distros) to real root device path. - :gh:`2005`: ``PSUTIL_DEBUG`` mode now prints file name and line number of the debug messages coming from C extension modules. @@ -737,45 +736,45 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1456`, [macOS], **[critical]**: :func:`cpu_freq()` ``min`` and ``max`` +- :gh:`1456`, [macOS], **[critical]**: :func:`cpu_freq` ``min`` and ``max`` are set to 0 if can't be determined (instead of crashing). -- :gh:`1512`, [macOS]: sometimes :meth:`Process.connections()` will crash with +- :gh:`1512`, [macOS]: sometimes :meth:`Process.connections` will crash with ``EOPNOTSUPP`` for one connection; this is now ignored. -- :gh:`1598`, [Windows]: :func:`disk_partitions()` only returns mountpoints on +- :gh:`1598`, [Windows]: :func:`disk_partitions` only returns mountpoints on drives where it first finds one. - :gh:`1874`, [SunOS]: swap output error due to incorrect range. -- :gh:`1892`, [macOS]: :func:`cpu_freq()` broken on Apple M1. +- :gh:`1892`, [macOS]: :func:`cpu_freq` broken on Apple M1. - :gh:`1901`, [macOS]: different functions, especially - :meth:`Process.open_files()` and - :meth:`Process.connections()`, could randomly raise :exc:`AccessDenied` + :meth:`Process.open_files` and + :meth:`Process.connections`, could randomly raise :exc:`AccessDenied` because the internal buffer of ``proc_pidinfo(PROC_PIDLISTFDS)`` syscall was not big enough. We now dynamically increase the buffer size until it's big enough instead of giving up and raising :exc:`AccessDenied`, which was a fallback to avoid crashing. - :gh:`1904`, [Windows]: ``OpenProcess`` fails with ``ERROR_SUCCESS`` due to ``GetLastError()`` called after ``sprintf()``. (patch by alxchk) -- :gh:`1913`, [Linux]: :func:`wait_procs()` should catch +- :gh:`1913`, [Linux]: :func:`wait_procs` should catch ``subprocess.TimeoutExpired`` exception. -- :gh:`1919`, [Linux]: :func:`sensors_battery()` can raise ``TypeError`` on +- :gh:`1919`, [Linux]: :func:`sensors_battery` can raise ``TypeError`` on PureOS. -- :gh:`1921`, [Windows]: :func:`swap_memory()` shows committed memory instead +- :gh:`1921`, [Windows]: :func:`swap_memory` shows committed memory instead of swap. - :gh:`1940`, [Linux]: psutil does not handle ``ENAMETOOLONG`` when accessing process file descriptors in procfs. (patch by Nikita Radchenko) - :gh:`1948`, **[critical]**: ``memoize_when_activated`` decorator is not thread-safe. (patch by Xuehai Pan) -- :gh:`1953`, [Windows], **[critical]**: :func:`disk_partitions()` crashes due +- :gh:`1953`, [Windows], **[critical]**: :func:`disk_partitions` crashes due to insufficient buffer len. (patch by MaWe2019) - :gh:`1965`, [Windows], **[critical]**: fix "Fatal Python error: deallocating - None" when calling :func:`users()` multiple times. + None" when calling :func:`users` multiple times. - :gh:`1980`, [Windows]: 32bit / WoW64 processes fails to read - :meth:`Process.name()` longer than 128 characters resulting in + :meth:`Process.name` longer than 128 characters resulting in :exc:`AccessDenied`. This is now fixed. (patch by PetrPospisil) -- :gh:`1991`, **[critical]**: :func:`process_iter()` is not thread safe and can +- :gh:`1991`, **[critical]**: :func:`process_iter` is not thread safe and can raise ``TypeError`` if invoked from multiple threads. -- :gh:`1956`, [macOS]: :meth:`Process.cpu_times()` reports incorrect timings on +- :gh:`1956`, [macOS]: :meth:`Process.cpu_times` reports incorrect timings on M1 machines. (patch by Olivier Dormond) -- :gh:`2023`, [Linux]: :func:`cpu_freq()` return order is wrong on systems with +- :gh:`2023`, [Linux]: :func:`cpu_freq` return order is wrong on systems with more than 9 CPUs. 5.8.0 — 2020-12-19 @@ -783,7 +782,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1863`: :func:`disk_partitions()` exposes 2 extra fields: ``maxfile`` and +- :gh:`1863`: :func:`disk_partitions` exposes 2 extra fields: ``maxfile`` and ``maxpath``, which are the maximum file name and path name length. - :gh:`1872`, [Windows]: added support for PyPy 2.7. - :gh:`1879`: provide pre-compiled wheels for Linux and macOS (yey!). @@ -793,22 +792,22 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1708`, [Linux]: get rid of :func:`sensors_temperatures()` duplicates. +- :gh:`1708`, [Linux]: get rid of :func:`sensors_temperatures` duplicates. (patch by Tim Schlueter). - :gh:`1839`, [Windows], **[critical]**: always raise :exc:`AccessDenied` instead of ``WindowsError`` when failing to query 64 processes from 32 bit ones by using ``NtWoW64`` APIs. -- :gh:`1866`, [Windows], **[critical]**: :meth:`Process.exe()`, - :meth:`Process.cmdline()`, - :meth:`Process.environ()` may raise "[WinError 998] Invalid access to memory +- :gh:`1866`, [Windows], **[critical]**: :meth:`Process.exe`, + :meth:`Process.cmdline`, + :meth:`Process.environ` may raise "[WinError 998] Invalid access to memory location" on Python 3.9 / VS 2019. - :gh:`1874`, [SunOS]: wrong swap output given when encrypted column is present. -- :gh:`1875`, [Windows], **[critical]**: :meth:`Process.username()` may raise +- :gh:`1875`, [Windows], **[critical]**: :meth:`Process.username` may raise ``ERROR_NONE_MAPPED`` if the SID has no corresponding account name. In this case :exc:`AccessDenied` is now raised. -- :gh:`1886`, [macOS]: ``EIO`` error may be raised on :meth:`Process.cmdline()` - and :meth:`Process.environ()`. Now it gets translated into +- :gh:`1886`, [macOS]: ``EIO`` error may be raised on :meth:`Process.cmdline` + and :meth:`Process.environ`. Now it gets translated into :exc:`AccessDenied`. - :gh:`1887`, [Windows], **[critical]**: ``OpenProcess`` may fail with "[WinError 0] The operation completed successfully"." Turn it into @@ -821,10 +820,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`809`, [FreeBSD]: add support for :meth:`Process.rlimit()`. -- :gh:`893`, [BSD]: add support for :meth:`Process.environ()` (patch by Armin +- :gh:`809`, [FreeBSD]: add support for :meth:`Process.rlimit`. +- :gh:`893`, [BSD]: add support for :meth:`Process.environ` (patch by Armin Gruner) -- :gh:`1830`, [POSIX]: :func:`net_if_stats()` ``isup`` also checks whether the +- :gh:`1830`, [POSIX]: :func:`net_if_stats` ``isup`` also checks whether the NIC is running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) - :gh:`1837`, [Linux]: improved battery detection and charge ``secsleft`` @@ -832,15 +831,15 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1620`, [Linux]: :func:`cpu_count()` with ``logical=False`` result is +- :gh:`1620`, [Linux]: :func:`cpu_count` with ``logical=False`` result is incorrect on systems with more than one CPU socket. (patch by Vincent A. Arcila) -- :gh:`1738`, [macOS]: :meth:`Process.exe()` may raise ``FileNotFoundError`` if +- :gh:`1738`, [macOS]: :meth:`Process.exe` may raise ``FileNotFoundError`` if process is still alive but the exe file which launched it got deleted. - :gh:`1791`, [macOS]: fix missing include for ``getpagesize()``. -- :gh:`1823`, [Windows], **[critical]**: :meth:`Process.open_files()` may cause +- :gh:`1823`, [Windows], **[critical]**: :meth:`Process.open_files` may cause a segfault due to a NULL pointer. -- :gh:`1838`, [Linux]: :func:`sensors_battery()`: if `percent` can be +- :gh:`1838`, [Linux]: :func:`sensors_battery`: if `percent` can be determined but not the remaining values, still return a result instead of ``None``. (patch by aristocratos) @@ -860,9 +859,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: as fast! - :gh:`1741`, [POSIX]: ``make build`` now runs in parallel on Python >= 3.6 and it's about 15% faster. -- :gh:`1747`: :meth:`Process.wait()` return value is cached so that the exit +- :gh:`1747`: :meth:`Process.wait` return value is cached so that the exit code can be retrieved on then next call. -- :gh:`1747`, [POSIX]: :meth:`Process.wait()` on POSIX now returns an enum, +- :gh:`1747`, [POSIX]: :meth:`Process.wait` on POSIX now returns an enum, showing the negative signal which was used to terminate the process. It returns something like ````. - :gh:`1747`: :class:`Process` class provides more info about the process on @@ -873,13 +872,13 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1726`, [Linux]: :func:`cpu_freq()` parsing should use spaces instead of +- :gh:`1726`, [Linux]: :func:`cpu_freq` parsing should use spaces instead of tabs on ia64. (patch by Michał Górny) -- :gh:`1760`, [Linux]: :meth:`Process.rlimit()` does not handle long long type +- :gh:`1760`, [Linux]: :meth:`Process.rlimit` does not handle long long type properly. - :gh:`1766`, [macOS]: :exc:`NoSuchProcess` may be raised instead of :exc:`ZombieProcess`. -- :gh:`1781`, **[critical]**: :func:`getloadavg()` can crash the Python +- :gh:`1781`, **[critical]**: :func:`getloadavg` can crash the Python interpreter. (patch by Ammar Askar) 5.7.0 — 2020-02-18 @@ -888,46 +887,46 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`1637`, [SunOS]: add partial support for old SunOS 5.10 Update 0 to 3. -- :gh:`1648`, [Linux]: :func:`sensors_temperatures()` looks into an additional +- :gh:`1648`, [Linux]: :func:`sensors_temperatures` looks into an additional ``/sys/device/`` directory for additional data. (patch by Javad Karabi) - :gh:`1652`, [Windows]: dropped support for Windows XP and Windows Server 2003. Minimum supported Windows version now is Windows Vista. - :gh:`1671`, [FreeBSD]: add CI testing/service for FreeBSD (Cirrus CI). -- :gh:`1677`, [Windows]: :meth:`Process.exe()` will succeed for all process +- :gh:`1677`, [Windows]: :meth:`Process.exe` will succeed for all process PIDs (instead of raising :exc:`AccessDenied`). -- :gh:`1679`, [Windows]: :func:`net_connections()` and - :meth:`Process.connections()` are 10% faster. +- :gh:`1679`, [Windows]: :func:`net_connections` and + :meth:`Process.connections` are 10% faster. - :gh:`1682`, [PyPy]: added CI / test integration for PyPy via Travis. - :gh:`1686`, [Windows]: added support for PyPy on Windows. -- :gh:`1693`, [Windows]: :func:`boot_time()`, :meth:`Process.create_time()` and - :func:`users()`'s login time now have 1 micro second precision (before the +- :gh:`1693`, [Windows]: :func:`boot_time`, :meth:`Process.create_time` and + :func:`users`'s login time now have 1 micro second precision (before the precision was of 1 second). **Bug fixes** -- :gh:`1538`, [NetBSD]: :meth:`Process.cwd()` may return ``ENOENT`` instead of +- :gh:`1538`, [NetBSD]: :meth:`Process.cwd` may return ``ENOENT`` instead of :exc:`NoSuchProcess`. -- :gh:`1627`, [Linux]: :meth:`Process.memory_maps()` can raise ``KeyError``. +- :gh:`1627`, [Linux]: :meth:`Process.memory_maps` can raise ``KeyError``. - :gh:`1642`, [SunOS]: querying basic info for PID 0 results in ``FileNotFoundError``. - :gh:`1646`, [FreeBSD], **[critical]**: many :class:`Process` methods may cause a segfault due to a backward incompatible change in a C type on FreeBSD 12.0. -- :gh:`1656`, [Windows]: :meth:`Process.memory_full_info()` raises +- :gh:`1656`, [Windows]: :meth:`Process.memory_full_info` raises :exc:`AccessDenied` even for the current user and os.getpid(). -- :gh:`1660`, [Windows]: :meth:`Process.open_files()` complete rewrite + check +- :gh:`1660`, [Windows]: :meth:`Process.open_files` complete rewrite + check of errors. -- :gh:`1662`, [Windows], **[critical]**: :meth:`Process.exe()` may raise +- :gh:`1662`, [Windows], **[critical]**: :meth:`Process.exe` may raise "[WinError 0] The operation completed successfully". -- :gh:`1665`, [Linux]: :func:`disk_io_counters()` does not take into account +- :gh:`1665`, [Linux]: :func:`disk_io_counters` does not take into account extra fields added to recent kernels. (patch by Mike Hommey) - :gh:`1672`: use the right C type when dealing with PIDs (int or long). Thus far (long) was almost always assumed, which is wrong on most platforms. -- :gh:`1673`, [OpenBSD]: :meth:`Process.connections()`, - :meth:`Process.num_fds()` and - :meth:`Process.threads()` returned improper exception if process is gone. -- :gh:`1674`, [SunOS]: :func:`disk_partitions()` may raise ``OSError``. -- :gh:`1684`, [Linux]: :func:`disk_io_counters()` may raise ``ValueError`` on +- :gh:`1673`, [OpenBSD]: :meth:`Process.connections`, + :meth:`Process.num_fds` and + :meth:`Process.threads` returned improper exception if process is gone. +- :gh:`1674`, [SunOS]: :func:`disk_partitions` may raise ``OSError``. +- :gh:`1684`, [Linux]: :func:`disk_io_counters` may raise ``ValueError`` on systems not having ``/proc/diskstats``. - :gh:`1695`, [Linux]: could not compile on kernels <= 2.6.13 due to ``PSUTIL_HAS_IOPRIO`` not being defined. (patch by Anselm Kruis) @@ -945,7 +944,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1179`, [Linux]: :meth:`Process.cmdline()` now takes into account +- :gh:`1179`, [Linux]: :meth:`Process.cmdline` now takes into account misbehaving processes renaming the command line and using inappropriate chars to separate args. - :gh:`1616`, **[critical]**: use of ``Py_DECREF`` instead of ``Py_CLEAR`` will @@ -967,28 +966,28 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1527`, [Linux]: added :meth:`Process.cpu_times()` ``iowait`` counter, +- :gh:`1527`, [Linux]: added :meth:`Process.cpu_times` ``iowait`` counter, which is the time spent waiting for blocking I/O to complete. - :gh:`1565`: add PEP 517/8 build backend and requirements specification for better pip integration. (patch by Bernát Gábor) **Bug fixes** -- :gh:`875`, [Windows], **[critical]**: :meth:`Process.cmdline()`, - :meth:`Process.environ()` or - :meth:`Process.cwd()` may occasionally fail with ``ERROR_PARTIAL_COPY`` which +- :gh:`875`, [Windows], **[critical]**: :meth:`Process.cmdline`, + :meth:`Process.environ` or + :meth:`Process.cwd` may occasionally fail with ``ERROR_PARTIAL_COPY`` which now gets translated to :exc:`AccessDenied`. -- :gh:`1126`, [Linux], **[critical]**: :meth:`Process.cpu_affinity()` segfaults - on CentOS 5 / manylinux. :meth:`Process.cpu_affinity()` support for CentOS 5 +- :gh:`1126`, [Linux], **[critical]**: :meth:`Process.cpu_affinity` segfaults + on CentOS 5 / manylinux. :meth:`Process.cpu_affinity` support for CentOS 5 was removed. - :gh:`1528`, [AIX], **[critical]**: compilation error on AIX 7.2 due to 32 vs 64 bit differences. (patch by Arnon Yaari) - :gh:`1535`: ``type`` and ``family`` fields returned by - :func:`net_connections()` are not always turned into enums. -- :gh:`1536`, [NetBSD]: :meth:`Process.cmdline()` erroneously raise + :func:`net_connections` are not always turned into enums. +- :gh:`1536`, [NetBSD]: :meth:`Process.cmdline` erroneously raise :exc:`ZombieProcess` error if cmdline has non encodable chars. - :gh:`1546`: usage percent may be rounded to 0 on Python 2. -- :gh:`1552`, [Windows]: :func:`getloadavg()` math for calculating 5 and 15 +- :gh:`1552`, [Windows]: :func:`getloadavg` math for calculating 5 and 15 mins values is incorrect. - :gh:`1568`, [Linux]: use CC compiler env var if defined. - :gh:`1570`, [Windows]: ``NtWow64*`` syscalls fail to raise the proper error @@ -1003,17 +1002,17 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1494`, [AIX]: added support for :meth:`Process.environ()`. (patch by +- :gh:`1494`, [AIX]: added support for :meth:`Process.environ`. (patch by Arnon Yaari) **Bug fixes** -- :gh:`1276`, [AIX]: can't get whole :meth:`Process.cmdline()`. (patch by +- :gh:`1276`, [AIX]: can't get whole :meth:`Process.cmdline`. (patch by Arnon Yaari) -- :gh:`1501`, [Windows]: :meth:`Process.cmdline()` and :meth:`Process.exe()` +- :gh:`1501`, [Windows]: :meth:`Process.cmdline` and :meth:`Process.exe` raise unhandled "WinError 1168 element not found" exceptions for "Registry" and "Memory Compression" pseudo processes on Windows 10. -- :gh:`1526`, [NetBSD], **[critical]**: :meth:`Process.cmdline()` could raise +- :gh:`1526`, [NetBSD], **[critical]**: :meth:`Process.cmdline` could raise ``MemoryError``. (patch by Kamil Rytarowski) 5.6.2 — 2019-04-26 @@ -1021,9 +1020,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`604`, [Windows]: add new :func:`getloadavg()`, returning system load +- :gh:`604`, [Windows]: add new :func:`getloadavg`, returning system load average calculation, including on Windows (emulated). (patch by Ammar Askar) -- :gh:`1404`, [Linux]: :func:`cpu_count()` with ``logical=False`` uses a second +- :gh:`1404`, [Linux]: :func:`cpu_count` with ``logical=False`` uses a second method (read from ``/sys/devices/system/cpu/cpu[0-9]/topology/core_id``) in order to determine the number of CPU cores in case ``/proc/cpuinfo`` does not provide this info. @@ -1031,25 +1030,25 @@ Version 6.0.0 introduces some changes which affect backward compatibility: ``KeyboardInterrupt``. - :gh:`1464`: various docfixes (always point to Python 3 doc, fix links, etc.). - :gh:`1476`, [Windows]: it is now possible to set process high I/O priority - (:meth:`Process.ionice()`). Also, I/O priority values are now exposed as 4 + (:meth:`Process.ionice`). Also, I/O priority values are now exposed as 4 new constants: ``IOPRIO_VERYLOW``, ``IOPRIO_LOW``, ``IOPRIO_NORMAL``, ``IOPRIO_HIGH``. - :gh:`1478`: add make command to re-run tests failed on last run. **Bug fixes** -- :gh:`1223`, [Windows]: :func:`boot_time()` may return incorrect value on +- :gh:`1223`, [Windows]: :func:`boot_time` may return incorrect value on Windows XP. -- :gh:`1456`, [Linux]: :func:`cpu_freq()` returns ``None`` instead of 0.0 when +- :gh:`1456`, [Linux]: :func:`cpu_freq` returns ``None`` instead of 0.0 when ``min`` and ``max`` fields can't be determined. (patch by Alex Manuskin) - :gh:`1462`, [Linux]: (tests) make tests invariant to ``LANG`` setting (patch by Benjamin Drung) - :gh:`1463`: `cpu_distribution.py`_ script was broken. -- :gh:`1470`, [Linux]: :func:`disk_partitions()`: fix corner case when +- :gh:`1470`, [Linux]: :func:`disk_partitions`: fix corner case when ``/etc/mtab`` doesn't exist. (patch by Cedric Lamoriniere) -- :gh:`1471`, [SunOS]: :meth:`Process.name()` and :meth:`Process.cmdline()` can +- :gh:`1471`, [SunOS]: :meth:`Process.name` and :meth:`Process.cmdline` can return ``SystemError``. (patch by Daniel Beer) -- :gh:`1472`, [Linux]: :func:`cpu_freq()` does not return all CPUs on +- :gh:`1472`, [Linux]: :func:`cpu_freq` does not return all CPUs on Raspberry-pi 3. - :gh:`1474`: fix formatting of ``psutil.tests()`` which mimics ``ps aux`` output. @@ -1058,14 +1057,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: raised instead of :exc:`AccessDenied`. - :gh:`1477`, [Windows]: wrong or absent error handling for private ``NTSTATUS`` Windows APIs. Different process methods were affected by this. -- :gh:`1480`, [Windows], **[critical]**: :func:`cpu_count()` with +- :gh:`1480`, [Windows], **[critical]**: :func:`cpu_count` with ``logical=False`` could cause a crash due to fixed read violation. (patch by Samer Masterson) - :gh:`1486`, [AIX], [SunOS]: ``AttributeError`` when interacting with - :class:`Process` methods involved into :meth:`Process.oneshot()` context. -- :gh:`1491`, [SunOS]: :func:`net_if_addrs()`: use ``free()`` against ``ifap`` + :class:`Process` methods involved into :meth:`Process.oneshot` context. +- :gh:`1491`, [SunOS]: :func:`net_if_addrs`: use ``free()`` against ``ifap`` struct on error. (patch by Agnewee) -- :gh:`1493`, [Linux]: :func:`cpu_freq()`: handle the case where +- :gh:`1493`, [Linux]: :func:`cpu_freq`: handle the case where ``/sys/devices/system/cpu/cpufreq/`` exists but it's empty. 5.6.1 — 2019-03-11 @@ -1076,7 +1075,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1329`, [AIX]: psutil doesn't compile on AIX 6.1. (patch by Arnon Yaari) - :gh:`1448`, [Windows], **[critical]**: crash on import due to ``rtlIpv6AddressToStringA`` not available on Wine. -- :gh:`1451`, [Windows], **[critical]**: :meth:`Process.memory_full_info()` +- :gh:`1451`, [Windows], **[critical]**: :meth:`Process.memory_full_info` segfaults. ``NtQueryVirtualMemory`` is now used instead of ``QueryWorkingSet`` to calculate USS memory. @@ -1085,11 +1084,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1379`, [Windows]: :meth:`Process.suspend()` and :meth:`Process.resume()` +- :gh:`1379`, [Windows]: :meth:`Process.suspend` and :meth:`Process.resume` now use ``NtSuspendProcess`` and ``NtResumeProcess`` instead of stopping/resuming all threads of a process. This is faster and more reliable (aka this is what ProcessHacker does). -- :gh:`1420`, [Windows]: in case of exception :func:`disk_usage()` now also +- :gh:`1420`, [Windows]: in case of exception :func:`disk_usage` now also shows the path name. - :gh:`1422`, [Windows]: Windows APIs requiring to be dynamically loaded from DLL libraries are now loaded only once on startup (instead of on per function @@ -1098,42 +1097,42 @@ Version 6.0.0 introduces some changes which affect backward compatibility: calculated on startup. - :gh:`1428`: in case of error, the traceback message now shows the underlying C function called which failed. -- :gh:`1433`: new :meth:`Process.parents()` method. (idea by Ghislain Le Meur) -- :gh:`1437`: :func:`pids()` are returned in sorted order. +- :gh:`1433`: new :meth:`Process.parents` method. (idea by Ghislain Le Meur) +- :gh:`1437`: :func:`pids` are returned in sorted order. - :gh:`1442`: Python 3 is now the default interpreter used by Makefile. **Bug fixes** -- :gh:`1353`: :func:`process_iter()` is now thread safe (it rarely raised +- :gh:`1353`: :func:`process_iter` is now thread safe (it rarely raised ``TypeError``). -- :gh:`1394`, [Windows], **[critical]**: :meth:`Process.name()` and - :meth:`Process.exe()` may erroneously return "Registry" or fail with "[Error +- :gh:`1394`, [Windows], **[critical]**: :meth:`Process.name` and + :meth:`Process.exe` may erroneously return "Registry" or fail with "[Error 0] The operation completed successfully". ``QueryFullProcessImageNameW`` is now used instead of ``GetProcessImageFileNameW`` in order to prevent that. - :gh:`1411`, [BSD]: lack of ``Py_DECREF`` could cause segmentation fault on process instantiation. -- :gh:`1419`, [Windows]: :meth:`Process.environ()` raises +- :gh:`1419`, [Windows]: :meth:`Process.environ` raises ``NotImplementedError`` when querying a 64-bit process in 32-bit-WoW mode. Now it raises :exc:`AccessDenied`. -- :gh:`1427`, [OSX]: :meth:`Process.cmdline()` and :meth:`Process.environ()` +- :gh:`1427`, [OSX]: :meth:`Process.cmdline` and :meth:`Process.environ` may erroneously raise ``OSError`` on failed ``malloc()``. - :gh:`1429`, [Windows]: ``SE DEBUG`` was not properly set for current process. It is now, and it should result in less :exc:`AccessDenied` exceptions for low PID processes. -- :gh:`1432`, [Windows]: :meth:`Process.memory_info_ex()`'s USS memory is +- :gh:`1432`, [Windows]: :meth:`Process.memory_info_ex`'s USS memory is miscalculated because we're not using the actual system ``PAGESIZE``. -- :gh:`1439`, [NetBSD]: :meth:`Process.connections()` may return incomplete +- :gh:`1439`, [NetBSD]: :meth:`Process.connections` may return incomplete results if using - :meth:`Process.oneshot()`. + :meth:`Process.oneshot`. - :gh:`1447`: original exception wasn't turned into :exc:`NoSuchProcess` / - :exc:`AccessDenied` exceptions when using :meth:`Process.oneshot()` context + :exc:`AccessDenied` exceptions when using :meth:`Process.oneshot` context manager. **Incompatible API changes** -- :gh:`1291`, [OSX], **[critical]**: :meth:`Process.memory_maps()` was removed +- :gh:`1291`, [OSX], **[critical]**: :meth:`Process.memory_maps` was removed because inherently broken (segfault) for years. 5.5.1 — 2019-02-15 @@ -1141,13 +1140,13 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1348`, [Windows]: on Windows >= 8.1 if :meth:`Process.cmdline()` fails +- :gh:`1348`, [Windows]: on Windows >= 8.1 if :meth:`Process.cmdline` fails due to ``ERROR_ACCESS_DENIED`` attempt using ``NtQueryInformationProcess`` + ``ProcessCommandLineInformation``. (patch by EccoTheFlintstone) **Bug fixes** -- :gh:`1394`, [Windows]: :meth:`Process.exe()` returns "[Error 0] The operation +- :gh:`1394`, [Windows]: :meth:`Process.exe` returns "[Error 0] The operation completed successfully" when Python process runs in "Virtual Secure Mode". - :gh:`1402`: psutil exceptions' ``repr()`` show the internal private module path. @@ -1159,24 +1158,24 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1350`, [FreeBSD]: added support for :func:`sensors_temperatures()`. +- :gh:`1350`, [FreeBSD]: added support for :func:`sensors_temperatures`. (patch by Alex Manuskin) -- :gh:`1352`, [FreeBSD]: added support for :func:`cpu_freq()`. (patch by Alex +- :gh:`1352`, [FreeBSD]: added support for :func:`cpu_freq`. (patch by Alex Manuskin) **Bug fixes** -- :gh:`1111`: :meth:`Process.oneshot()` is now thread safe. -- :gh:`1354`, [Linux]: :func:`disk_io_counters()` fails on Linux kernel 4.18+. -- :gh:`1357`, [Linux]: :meth:`Process.memory_maps()` and - :meth:`Process.io_counters()` methods are no longer exposed if not supported +- :gh:`1111`: :meth:`Process.oneshot` is now thread safe. +- :gh:`1354`, [Linux]: :func:`disk_io_counters` fails on Linux kernel 4.18+. +- :gh:`1357`, [Linux]: :meth:`Process.memory_maps` and + :meth:`Process.io_counters` methods are no longer exposed if not supported by the kernel. -- :gh:`1368`, [Windows]: fix :meth:`Process.ionice()` mismatch. (patch by +- :gh:`1368`, [Windows]: fix :meth:`Process.ionice` mismatch. (patch by EccoTheFlintstone) - :gh:`1370`, [Windows]: improper usage of ``CloseHandle()`` may lead to override the original error code when raising an exception. - :gh:`1373`, **[critical]**: incorrect handling of cache in - :meth:`Process.oneshot()` context causes :class:`Process` instances to return + :meth:`Process.oneshot` context causes :class:`Process` instances to return incorrect results. - :gh:`1376`, [Windows]: ``OpenProcess`` now uses ``PROCESS_QUERY_LIMITED_INFORMATION`` access rights wherever possible, @@ -1190,10 +1189,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1197`, [Linux]: :func:`cpu_freq()` is now implemented by parsing +- :gh:`1197`, [Linux]: :func:`cpu_freq` is now implemented by parsing ``/proc/cpuinfo`` in case ``/sys/devices/system/cpu/*`` filesystem is not available. -- :gh:`1310`, [Linux]: :func:`sensors_temperatures()` now parses +- :gh:`1310`, [Linux]: :func:`sensors_temperatures` now parses ``/sys/class/thermal`` in case ``/sys/class/hwmon`` fs is not available (e.g. Raspberry Pi). (patch by Alex Manuskin) - :gh:`1320`, [POSIX]: better compilation support when using g++ instead of @@ -1201,20 +1200,20 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`715`: do not print exception on import time in case :func:`cpu_times()` +- :gh:`715`: do not print exception on import time in case :func:`cpu_times` fails. -- :gh:`1004`, [Linux]: :meth:`Process.io_counters()` may raise ``ValueError``. -- :gh:`1277`, [OSX]: available and used memory (:func:`virtual_memory()`) +- :gh:`1004`, [Linux]: :meth:`Process.io_counters` may raise ``ValueError``. +- :gh:`1277`, [OSX]: available and used memory (:func:`virtual_memory`) metrics are not accurate. -- :gh:`1294`, [Windows]: :meth:`Process.connections()` may sometimes fail with +- :gh:`1294`, [Windows]: :meth:`Process.connections` may sometimes fail with intermittent ``0xC0000001``. (patch by Sylvain Duchesne) -- :gh:`1307`, [Linux]: :func:`disk_partitions()` does not honour +- :gh:`1307`, [Linux]: :func:`disk_partitions` does not honour :data:`PROCFS_PATH`. -- :gh:`1320`, [AIX]: system CPU times (:func:`cpu_times()`) were being reported +- :gh:`1320`, [AIX]: system CPU times (:func:`cpu_times`) were being reported with ticks unit as opposed to seconds. (patch by Jaime Fullaondo) - :gh:`1332`, [OSX]: psutil debug messages are erroneously printed all the time. (patch by Ilya Yanok) -- :gh:`1346`, [SunOS]: :func:`net_connections()` returns an empty list. (patch +- :gh:`1346`, [SunOS]: :func:`net_connections` returns an empty list. (patch by Oleksii Shevchuk) 5.4.7 — 2018-08-14 @@ -1225,29 +1224,29 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1286`, [macOS]: ``psutil.OSX`` constant is now deprecated in favor of new ``psutil.MACOS``. - :gh:`1309`, [Linux]: added ``psutil.STATUS_PARKED`` constant for - :meth:`Process.status()`. -- :gh:`1321`, [Linux]: add :func:`disk_io_counters()` dual implementation + :meth:`Process.status`. +- :gh:`1321`, [Linux]: add :func:`disk_io_counters` dual implementation relying on ``/sys/block`` filesystem in case ``/proc/diskstats`` is not available. (patch by Lawrence Ye) **Bug fixes** -- :gh:`1209`, [macOS]: :meth:`Process.memory_maps()` may fail with ``EINVAL`` +- :gh:`1209`, [macOS]: :meth:`Process.memory_maps` may fail with ``EINVAL`` due to poor ``task_for_pid()`` syscall. :exc:`AccessDenied` is now raised instead. -- :gh:`1278`, [macOS]: :meth:`Process.threads()` incorrectly return +- :gh:`1278`, [macOS]: :meth:`Process.threads` incorrectly return microseconds instead of seconds. (patch by Nikhil Marathe) -- :gh:`1279`, [Linux], [macOS], [BSD]: :func:`net_if_stats()` may return +- :gh:`1279`, [Linux], [macOS], [BSD]: :func:`net_if_stats` may return ``ENODEV``. -- :gh:`1294`, [Windows]: :meth:`Process.connections()` may sometime fail with +- :gh:`1294`, [Windows]: :meth:`Process.connections` may sometime fail with ``MemoryError``. (patch by sylvainduchesne) -- :gh:`1305`, [Linux]: :func:`disk_io_counters()` may report inflated r/w bytes +- :gh:`1305`, [Linux]: :func:`disk_io_counters` may report inflated r/w bytes values. -- :gh:`1309`, [Linux]: :meth:`Process.status()` is unable to recognize +- :gh:`1309`, [Linux]: :meth:`Process.status` is unable to recognize ``"idle"`` and ``"parked"`` statuses (returns ``"?"``). -- :gh:`1313`, [Linux]: :func:`disk_io_counters()` can report inflated values +- :gh:`1313`, [Linux]: :func:`disk_io_counters` can report inflated values due to counting base disk device and its partition(s) twice. -- :gh:`1323`, [Linux]: :func:`sensors_temperatures()` may fail with +- :gh:`1323`, [Linux]: :func:`sensors_temperatures` may fail with ``ValueError``. 5.4.6 — 2018-06-07 @@ -1255,11 +1254,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1258`, [Windows], **[critical]**: :meth:`Process.username()` may cause a +- :gh:`1258`, [Windows], **[critical]**: :meth:`Process.username` may cause a segfault (Python interpreter crash). (patch by Jean-Luc Migot) -- :gh:`1273`: :func:`net_if_addrs()` namedtuple's name has been renamed from +- :gh:`1273`: :func:`net_if_addrs` namedtuple's name has been renamed from ``snic`` to ``snicaddr``. -- :gh:`1274`, [Linux]: there was a small chance :meth:`Process.children()` may +- :gh:`1274`, [Linux]: there was a small chance :meth:`Process.children` may swallow :exc:`AccessDenied` exceptions. @@ -1277,48 +1276,48 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`1239`, [Linux]: expose kernel ``slab`` memory field for - :func:`virtual_memory()`. (patch by Maxime Mouial) + :func:`virtual_memory`. (patch by Maxime Mouial) **Bug fixes** -- :gh:`694`, [SunOS]: :meth:`Process.cmdline()` could be truncated at the 15th +- :gh:`694`, [SunOS]: :meth:`Process.cmdline` could be truncated at the 15th character when reading it from ``/proc``. An extra effort is made by reading it from process address space first. (patch by Georg Sauthoff) -- :gh:`771`, [Windows]: :func:`cpu_count()` (both logical and cores) return a +- :gh:`771`, [Windows]: :func:`cpu_count` (both logical and cores) return a wrong (smaller) number on systems using process groups (> 64 cores). -- :gh:`771`, [Windows]: :func:`cpu_times()` with ``percpu=True`` return fewer +- :gh:`771`, [Windows]: :func:`cpu_times` with ``percpu=True`` return fewer CPUs on systems using process groups (> 64 cores). -- :gh:`771`, [Windows]: :func:`cpu_stats()` and :func:`cpu_freq()` may return +- :gh:`771`, [Windows]: :func:`cpu_stats` and :func:`cpu_freq` may return incorrect results on systems using process groups (> 64 cores). - :gh:`1193`, [SunOS]: return uid/gid from ``/proc/pid/psinfo`` if there aren't enough permissions for ``/proc/pid/cred``. (patch by Georg Sauthoff) - :gh:`1194`, [SunOS]: return nice value from ``psinfo`` as ``getpriority()`` doesn't support real-time processes. (patch by Georg Sauthoff) -- :gh:`1194`, [SunOS]: fix double ``free()`` in :meth:`Process.cpu_num()`. +- :gh:`1194`, [SunOS]: fix double ``free()`` in :meth:`Process.cpu_num`. (patch by Georg Sauthoff) - :gh:`1194`, [SunOS]: fix undefined behavior related to strict-aliasing rules and warnings. (patch by Georg Sauthoff) -- :gh:`1210`, [Linux]: :func:`cpu_percent()` steal time may remain stuck at +- :gh:`1210`, [Linux]: :func:`cpu_percent` steal time may remain stuck at 100% due to Linux erroneously reporting a decreased steal time between calls. (patch by Arnon Yaari) - :gh:`1216`: fix compatibility with Python 2.6 on Windows (patch by Dan Vinakovsky) -- :gh:`1222`, [Linux]: :meth:`Process.memory_full_info()` was erroneously +- :gh:`1222`, [Linux]: :meth:`Process.memory_full_info` was erroneously summing "Swap:" and "SwapPss:". Same for "Pss:" and "SwapPss". Not anymore. -- :gh:`1224`, [Windows]: :meth:`Process.wait()` may erroneously raise +- :gh:`1224`, [Windows]: :meth:`Process.wait` may erroneously raise :exc:`TimeoutExpired`. -- :gh:`1238`, [Linux]: :func:`sensors_battery()` may return ``None`` in case +- :gh:`1238`, [Linux]: :func:`sensors_battery` may return ``None`` in case battery is not listed as "BAT0" under ``/sys/class/power_supply``. -- :gh:`1240`, [Windows]: :func:`cpu_times()` float loses accuracy in a long +- :gh:`1240`, [Windows]: :func:`cpu_times` float loses accuracy in a long running system. (patch by stswandering) -- :gh:`1245`, [Linux]: :func:`sensors_temperatures()` may fail with ``IOError`` +- :gh:`1245`, [Linux]: :func:`sensors_temperatures` may fail with ``IOError`` "no such file". -- :gh:`1255`, [FreeBSD]: :func:`swap_memory()` stats were erroneously +- :gh:`1255`, [FreeBSD]: :func:`swap_memory` stats were erroneously represented in KB. (patch by Denis Krienbühl) **Backward compatibility** -- :gh:`771`, [Windows]: :func:`cpu_count()` with ``logical=False`` on Windows +- :gh:`771`, [Windows]: :func:`cpu_count` with ``logical=False`` on Windows XP and Vista is no longer supported and returns ``None``. 5.4.3 — 2018-01-01 @@ -1326,11 +1325,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`775`: :func:`disk_partitions()` on Windows return mount points. +- :gh:`775`: :func:`disk_partitions` on Windows return mount points. **Bug fixes** -- :gh:`1193`: :func:`pids()` may return ``False`` on macOS. +- :gh:`1193`: :func:`pids` may return ``False`` on macOS. 5.4.2 — 2017-12-07 ^^^^^^^^^^^^^^^^^^ @@ -1340,24 +1339,24 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1173`: introduced ``PSUTIL_DEBUG`` environment variable which can be set in order to print useful debug messages on stderr (useful in case of nasty errors). -- :gh:`1177`, [macOS]: added support for :func:`sensors_battery()`. (patch by +- :gh:`1177`, [macOS]: added support for :func:`sensors_battery`. (patch by Arnon Yaari) -- :gh:`1183`: :meth:`Process.children()` is 2x faster on POSIX and 2.4x faster +- :gh:`1183`: :meth:`Process.children` is 2x faster on POSIX and 2.4x faster on Linux. -- :gh:`1188`: deprecated method :meth:`Process.memory_info_ex()` now warns by +- :gh:`1188`: deprecated method :meth:`Process.memory_info_ex` now warns by using ``FutureWarning`` instead of ``DeprecationWarning``. **Bug fixes** -- :gh:`1152`, [Windows]: :func:`disk_io_counters()` may return an empty dict. -- :gh:`1169`, [Linux]: :func:`users()` ``hostname`` returns username instead. +- :gh:`1152`, [Windows]: :func:`disk_io_counters` may return an empty dict. +- :gh:`1169`, [Linux]: :func:`users` ``hostname`` returns username instead. (patch by janderbrain) - :gh:`1172`, [Windows]: ``make test`` does not work. -- :gh:`1179`, [Linux]: :meth:`Process.cmdline()` is now able to split cmdline +- :gh:`1179`, [Linux]: :meth:`Process.cmdline` is now able to split cmdline args for misbehaving processes which overwrite ``/proc/pid/cmdline`` and use spaces instead of null bytes as args separator. -- :gh:`1181`, [macOS]: :meth:`Process.memory_maps()` may raise ``ENOENT``. -- :gh:`1187`, [macOS]: :func:`pids()` does not return PID 0 on recent macOS +- :gh:`1181`, [macOS]: :meth:`Process.memory_maps` may raise ``ENOENT``. +- :gh:`1187`, [macOS]: :func:`pids` does not return PID 0 on recent macOS versions. 5.4.1 — 2017-11-08 @@ -1365,7 +1364,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1164`, [AIX]: add support for :meth:`Process.num_ctx_switches()`. +- :gh:`1164`, [AIX]: add support for :meth:`Process.num_ctx_switches`. (patch by Arnon Yaari) - :gh:`1053`: drop Python 3.3 support (psutil still works but it's no longer tested). @@ -1377,7 +1376,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1151`: ``python -m psutil.tests`` fail. - :gh:`1154`, [AIX], **[critical]**: psutil won't compile on AIX 6.1.0. (patch by Arnon Yaari) -- :gh:`1167`, [Windows]: :func:`net_io_counters()` packets count now include +- :gh:`1167`, [Windows]: :func:`net_io_counters` packets count now include also non-unicast packets. (patch by Matthew Long) 5.4.0 — 2017-10-12 @@ -1389,14 +1388,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1009`, [Linux]: :func:`sensors_temperatures()` may crash with +- :gh:`1009`, [Linux]: :func:`sensors_temperatures` may crash with ``IOError``. -- :gh:`1012`, [Windows]: :func:`disk_io_counters()` ``read_time`` and +- :gh:`1012`, [Windows]: :func:`disk_io_counters` ``read_time`` and ``write_time`` were expressed in tens of micro seconds instead of milliseconds. - :gh:`1127`, [macOS], **[critical]**: invalid reference counting in - :meth:`Process.open_files()` may lead to segfault. (patch by Jakub Bacic) -- :gh:`1129`, [Linux]: :func:`sensors_fans()` may crash with ``IOError``. + :meth:`Process.open_files` may lead to segfault. (patch by Jakub Bacic) +- :gh:`1129`, [Linux]: :func:`sensors_fans` may crash with ``IOError``. (patch by Sebastian Saip) - :gh:`1131`, [SunOS]: fix compilation warnings. (patch by Arnon Yaari) - :gh:`1133`, [Windows]: can't compile on newer versions of Visual Studio 2017 @@ -1414,7 +1413,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`1105`, [FreeBSD]: psutil does not compile on FreeBSD 12. -- :gh:`1125`, [BSD]: :func:`net_connections()` raises ``TypeError``. +- :gh:`1125`, [BSD]: :func:`net_connections` raises ``TypeError``. **Compatibility notes** @@ -1426,115 +1425,115 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`802`: :func:`disk_io_counters()` and :func:`net_io_counters()` numbers +- :gh:`802`: :func:`disk_io_counters` and :func:`net_io_counters` numbers no longer wrap (restart from 0). Introduced a new ``nowrap`` argument. -- :gh:`928`: :func:`net_connections()` and :meth:`Process.connections()` +- :gh:`928`: :func:`net_connections` and :meth:`Process.connections` ``laddr`` and ``raddr`` are now named tuples. -- :gh:`1015`: :func:`swap_memory()` now relies on ``/proc/meminfo`` instead of +- :gh:`1015`: :func:`swap_memory` now relies on ``/proc/meminfo`` instead of ``sysinfo()`` syscall so that it can be used in conjunction with :data:`PROCFS_PATH` in order to retrieve memory info about Linux containers such as Docker and Heroku. -- :gh:`1022`: :func:`users()` provides a new ``pid`` field. -- :gh:`1025`: :func:`process_iter()` accepts two new parameters in order to +- :gh:`1022`: :func:`users` provides a new ``pid`` field. +- :gh:`1025`: :func:`process_iter` accepts two new parameters in order to invoke - :meth:`Process.as_dict()`: ``attrs`` and ``ad_value``. With these you can + :meth:`Process.as_dict`: ``attrs`` and ``ad_value``. With these you can iterate over all processes in one shot without needing to catch :exc:`NoSuchProcess` and do list/dict comprehensions. - :gh:`1040`: implemented full unicode support. -- :gh:`1051`: :func:`disk_usage()` on Python 3 is now able to accept bytes. +- :gh:`1051`: :func:`disk_usage` on Python 3 is now able to accept bytes. - :gh:`1058`: test suite now enables all warnings by default. - :gh:`1060`: source distribution is dynamically generated so that it only includes relevant files. -- :gh:`1079`, [FreeBSD]: :func:`net_connections()` ``fd`` number is now being +- :gh:`1079`, [FreeBSD]: :func:`net_connections` ``fd`` number is now being set for real (instead of ``-1``). (patch by Gleb Smirnoff) -- :gh:`1091`, [SunOS]: implemented :meth:`Process.environ()`. (patch by +- :gh:`1091`, [SunOS]: implemented :meth:`Process.environ`. (patch by Oleksii Shevchuk) **Bug fixes** -- :gh:`989`, [Windows]: :func:`boot_time()` may return a negative value. -- :gh:`1007`, [Windows]: :func:`boot_time()` can have a 1 sec fluctuation +- :gh:`989`, [Windows]: :func:`boot_time` may return a negative value. +- :gh:`1007`, [Windows]: :func:`boot_time` can have a 1 sec fluctuation between calls. The value of the first call is now cached so that - :func:`boot_time()` always returns the same value if fluctuation is <= 1 + :func:`boot_time` always returns the same value if fluctuation is <= 1 second. -- :gh:`1013`, [FreeBSD]: :func:`net_connections()` may return incorrect PID. +- :gh:`1013`, [FreeBSD]: :func:`net_connections` may return incorrect PID. (patch by Gleb Smirnoff) - :gh:`1014`, [Linux]: :class:`Process` class can mask legitimate ``ENOENT`` exceptions as :exc:`NoSuchProcess`. -- :gh:`1016`: :func:`disk_io_counters()` raises ``RuntimeError`` on a system +- :gh:`1016`: :func:`disk_io_counters` raises ``RuntimeError`` on a system with no disks. -- :gh:`1017`: :func:`net_io_counters()` raises ``RuntimeError`` on a system +- :gh:`1017`: :func:`net_io_counters` raises ``RuntimeError`` on a system with no network cards installed. -- :gh:`1021`, [Linux]: :meth:`Process.open_files()` may erroneously raise +- :gh:`1021`, [Linux]: :meth:`Process.open_files` may erroneously raise :exc:`NoSuchProcess` instead of skipping a file which gets deleted while open files are retrieved. -- :gh:`1029`, [macOS], [FreeBSD]: :meth:`Process.connections()` with +- :gh:`1029`, [macOS], [FreeBSD]: :meth:`Process.connections` with ``family=unix`` on Python 3 doesn't properly handle unicode paths and may raise ``UnicodeDecodeError``. -- :gh:`1033`, [macOS], [FreeBSD]: memory leak for :func:`net_connections()` and - :meth:`Process.connections()` when retrieving UNIX sockets (``kind='unix'``). +- :gh:`1033`, [macOS], [FreeBSD]: memory leak for :func:`net_connections` and + :meth:`Process.connections` when retrieving UNIX sockets (``kind='unix'``). - :gh:`1040`: fixed many unicode related issues such as ``UnicodeDecodeError`` on Python 3 + POSIX and invalid encoded data on Windows. - :gh:`1042`, [FreeBSD], **[critical]**: psutil won't compile on FreeBSD 12. - :gh:`1044`, [macOS]: different :class:`Process` methods incorrectly raise :exc:`AccessDenied` for zombie processes. -- :gh:`1046`, [Windows]: :func:`disk_partitions()` on Windows overrides user's +- :gh:`1046`, [Windows]: :func:`disk_partitions` on Windows overrides user's ``SetErrorMode``. -- :gh:`1047`, [Windows]: :meth:`Process.username()`: memory leak in case +- :gh:`1047`, [Windows]: :meth:`Process.username`: memory leak in case exception is thrown. -- :gh:`1048`, [Windows]: :func:`users()` ``host`` field report an invalid IP +- :gh:`1048`, [Windows]: :func:`users` ``host`` field report an invalid IP address. -- :gh:`1050`, [Windows]: :meth:`Process.memory_maps()` leaks memory. -- :gh:`1055`: :func:`cpu_count()` is no longer cached. This is useful on +- :gh:`1050`, [Windows]: :meth:`Process.memory_maps` leaks memory. +- :gh:`1055`: :func:`cpu_count` is no longer cached. This is useful on systems such as Linux where CPUs can be disabled at runtime. This also reflects on - :meth:`Process.cpu_percent()` which no longer uses the cache. + :meth:`Process.cpu_percent` which no longer uses the cache. - :gh:`1058`: fixed Python warnings. -- :gh:`1062`: :func:`disk_io_counters()` and :func:`net_io_counters()` raise +- :gh:`1062`: :func:`disk_io_counters` and :func:`net_io_counters` raise ``TypeError`` if no disks or NICs are installed on the system. -- :gh:`1063`, [NetBSD]: :func:`net_connections()` may list incorrect sockets. -- :gh:`1064`, [NetBSD], **[critical]**: :func:`swap_memory()` may segfault in +- :gh:`1063`, [NetBSD]: :func:`net_connections` may list incorrect sockets. +- :gh:`1064`, [NetBSD], **[critical]**: :func:`swap_memory` may segfault in case of error. -- :gh:`1065`, [OpenBSD], **[critical]**: :meth:`Process.cmdline()` may raise +- :gh:`1065`, [OpenBSD], **[critical]**: :meth:`Process.cmdline` may raise ``SystemError``. -- :gh:`1067`, [NetBSD]: :meth:`Process.cmdline()` leaks memory if process has +- :gh:`1067`, [NetBSD]: :meth:`Process.cmdline` leaks memory if process has terminated. -- :gh:`1069`, [FreeBSD]: :meth:`Process.cpu_num()` may return 255 for certain +- :gh:`1069`, [FreeBSD]: :meth:`Process.cpu_num` may return 255 for certain kernel processes. -- :gh:`1071`, [Linux]: :func:`cpu_freq()` may raise ``IOError`` on old RedHat +- :gh:`1071`, [Linux]: :func:`cpu_freq` may raise ``IOError`` on old RedHat distros. -- :gh:`1074`, [FreeBSD]: :func:`sensors_battery()` raises ``OSError`` in case +- :gh:`1074`, [FreeBSD]: :func:`sensors_battery` raises ``OSError`` in case of no battery. -- :gh:`1075`, [Windows]: :func:`net_if_addrs()`: ``inet_ntop()`` return value +- :gh:`1075`, [Windows]: :func:`net_if_addrs`: ``inet_ntop()`` return value is not checked. -- :gh:`1077`, [SunOS]: :func:`net_if_addrs()` shows garbage addresses on SunOS +- :gh:`1077`, [SunOS]: :func:`net_if_addrs` shows garbage addresses on SunOS 5.10. (patch by Oleksii Shevchuk) -- :gh:`1077`, [SunOS]: :func:`net_connections()` does not work on SunOS 5.10. +- :gh:`1077`, [SunOS]: :func:`net_connections` does not work on SunOS 5.10. (patch by Oleksii Shevchuk) -- :gh:`1079`, [FreeBSD]: :func:`net_connections()` didn't list locally +- :gh:`1079`, [FreeBSD]: :func:`net_connections` didn't list locally connected sockets. (patch by Gleb Smirnoff) -- :gh:`1085`: :func:`cpu_count()` return value is now checked and forced to +- :gh:`1085`: :func:`cpu_count` return value is now checked and forced to ``None`` if <= 1. -- :gh:`1087`: :meth:`Process.cpu_percent()` guard against :func:`cpu_count()` +- :gh:`1087`: :meth:`Process.cpu_percent` guard against :func:`cpu_count` returning ``None`` and assumes 1 instead. -- :gh:`1093`, [SunOS]: :meth:`Process.memory_maps()` shows wrong 64 bit +- :gh:`1093`, [SunOS]: :meth:`Process.memory_maps` shows wrong 64 bit addresses. -- :gh:`1094`, [Windows]: :func:`pid_exists()` may lie. Also, all process APIs +- :gh:`1094`, [Windows]: :func:`pid_exists` may lie. Also, all process APIs relying on ``OpenProcess`` Windows API now check whether the PID is actually running. -- :gh:`1098`, [Windows]: :meth:`Process.wait()` may erroneously return sooner, +- :gh:`1098`, [Windows]: :meth:`Process.wait` may erroneously return sooner, when the PID is still alive. -- :gh:`1099`, [Windows]: :meth:`Process.terminate()` may raise +- :gh:`1099`, [Windows]: :meth:`Process.terminate` may raise :exc:`AccessDenied` even if the process already died. -- :gh:`1101`, [Linux]: :func:`sensors_temperatures()` may raise ``ENODEV``. +- :gh:`1101`, [Linux]: :func:`sensors_temperatures` may raise ``ENODEV``. **Porting notes** - :gh:`1039`: returned types consolidation. 1) Windows / - :meth:`Process.cpu_times()`: fields #3 and #4 were int instead of float. 2) + :meth:`Process.cpu_times`: fields #3 and #4 were int instead of float. 2) Linux / FreeBSD / OpenBSD: - :meth:`Process.connections()` ``raddr`` is now set to ``""`` instead of + :meth:`Process.connections` ``raddr`` is now set to ``""`` instead of ``None`` when retrieving UNIX sockets. - :gh:`1040`: all strings are encoded by using OS fs encoding. - :gh:`1040`: the following Windows APIs on Python 2 now return a string @@ -1550,12 +1549,12 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1000`: fixed some setup.py warnings. - :gh:`1002`, [SunOS]: remove C macro which will not be available on new Solaris versions. (patch by Danek Duvall) -- :gh:`1004`, [Linux]: :meth:`Process.io_counters()` may raise ``ValueError``. -- :gh:`1006`, [Linux]: :func:`cpu_freq()` may return ``None`` on some Linux +- :gh:`1004`, [Linux]: :meth:`Process.io_counters` may raise ``ValueError``. +- :gh:`1006`, [Linux]: :func:`cpu_freq` may return ``None`` on some Linux versions does not support the function. Let's not make the function available instead. -- :gh:`1009`, [Linux]: :func:`sensors_temperatures()` may raise ``OSError``. -- :gh:`1010`, [Linux]: :func:`virtual_memory()` may raise ``ValueError`` on +- :gh:`1009`, [Linux]: :func:`sensors_temperatures` may raise ``OSError``. +- :gh:`1010`, [Linux]: :func:`virtual_memory` may raise ``ValueError`` on Ubuntu 14.04. 5.2.1 — 2017-03-24 @@ -1563,12 +1562,12 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`981`, [Linux]: :func:`cpu_freq()` may return an empty list. -- :gh:`993`, [Windows]: :meth:`Process.memory_maps()` on Python 3 may raise +- :gh:`981`, [Linux]: :func:`cpu_freq` may return an empty list. +- :gh:`993`, [Windows]: :meth:`Process.memory_maps` on Python 3 may raise ``UnicodeDecodeError``. -- :gh:`996`, [Linux]: :func:`sensors_temperatures()` may not show all +- :gh:`996`, [Linux]: :func:`sensors_temperatures` may not show all temperatures. -- :gh:`997`, [FreeBSD]: :func:`virtual_memory()` may fail due to missing +- :gh:`997`, [FreeBSD]: :func:`virtual_memory` may fail due to missing ``sysctl`` parameter on FreeBSD 12. 5.2.0 — 2017-03-05 @@ -1576,19 +1575,19 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`971`, [Linux]: Add :func:`sensors_fans()` function. (patch by Nicolas +- :gh:`971`, [Linux]: Add :func:`sensors_fans` function. (patch by Nicolas Hennion) -- :gh:`976`, [Windows]: :meth:`Process.io_counters()` has 2 new fields: +- :gh:`976`, [Windows]: :meth:`Process.io_counters` has 2 new fields: ``other_count`` and ``other_bytes``. -- :gh:`976`, [Linux]: :meth:`Process.io_counters()` has 2 new fields: +- :gh:`976`, [Linux]: :meth:`Process.io_counters` has 2 new fields: ``read_chars`` and ``write_chars``. **Bug fixes** - :gh:`872`, [Linux]: can now compile on Linux by using MUSL C library. -- :gh:`985`, [Windows]: Fix a crash in :meth:`Process.open_files()` when the +- :gh:`985`, [Windows]: Fix a crash in :meth:`Process.open_files` when the worker thread for ``NtQueryObject`` times out. -- :gh:`986`, [Linux]: :meth:`Process.cwd()` may raise :exc:`NoSuchProcess` +- :gh:`986`, [Linux]: :meth:`Process.cwd` may raise :exc:`NoSuchProcess` instead of :exc:`ZombieProcess`. 5.1.3 @@ -1596,8 +1595,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`971`, [Linux]: :func:`sensors_temperatures()` didn't work on CentOS 7. -- :gh:`973`, **[critical]**: :func:`cpu_percent()` may raise +- :gh:`971`, [Linux]: :func:`sensors_temperatures` didn't work on CentOS 7. +- :gh:`973`, **[critical]**: :func:`cpu_percent` may raise ``ZeroDivisionError``. 5.1.2 — 2017-02-03 @@ -1605,11 +1604,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`966`, [Linux]: :func:`sensors_battery()` ``power_plugged`` may +- :gh:`966`, [Linux]: :func:`sensors_battery` ``power_plugged`` may erroneously return ``None`` on Python 3. -- :gh:`968`, [Linux]: :func:`disk_io_counters()` raises ``TypeError`` on Python +- :gh:`968`, [Linux]: :func:`disk_io_counters` raises ``TypeError`` on Python 3. -- :gh:`970`, [Linux]: :func:`sensors_battery()` ``name`` and ``label`` fields +- :gh:`970`, [Linux]: :func:`sensors_battery` ``name`` and ``label`` fields on Python 3 are bytes instead of str. 5.1.1 — 2017-02-03 @@ -1617,37 +1616,37 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`966`, [Linux]: :func:`sensors_battery()` ``percent`` is a float and is +- :gh:`966`, [Linux]: :func:`sensors_battery` ``percent`` is a float and is more precise. **Bug fixes** -- :gh:`964`, [Windows]: :meth:`Process.username()` and :func:`users()` may +- :gh:`964`, [Windows]: :meth:`Process.username` and :func:`users` may return badly decoded character on Python 3. -- :gh:`965`, [Linux]: :func:`disk_io_counters()` may miscalculate sector size +- :gh:`965`, [Linux]: :func:`disk_io_counters` may miscalculate sector size and report the wrong number of bytes read and written. -- :gh:`966`, [Linux]: :func:`sensors_battery()` may fail with +- :gh:`966`, [Linux]: :func:`sensors_battery` may fail with ``FileNotFoundError``. -- :gh:`966`, [Linux]: :func:`sensors_battery()` ``power_plugged`` may lie. +- :gh:`966`, [Linux]: :func:`sensors_battery` ``power_plugged`` may lie. 5.1.0 — 2017-02-01 ^^^^^^^^^^^^^^^^^^ **Enhancements** -- :gh:`357`: added :meth:`Process.cpu_num()` (what CPU a process is on). -- :gh:`371`: added :func:`sensors_temperatures()` (Linux only). -- :gh:`941`: added :func:`cpu_freq()` (CPU frequency). -- :gh:`955`: added :func:`sensors_battery()` (Linux, Windows, only). -- :gh:`956`: :meth:`Process.cpu_affinity()` can now be passed ``[]`` argument +- :gh:`357`: added :meth:`Process.cpu_num` (what CPU a process is on). +- :gh:`371`: added :func:`sensors_temperatures` (Linux only). +- :gh:`941`: added :func:`cpu_freq` (CPU frequency). +- :gh:`955`: added :func:`sensors_battery` (Linux, Windows, only). +- :gh:`956`: :meth:`Process.cpu_affinity` can now be passed ``[]`` argument as an alias to set affinity against all eligible CPUs. **Bug fixes** -- :gh:`687`, [Linux]: :func:`pid_exists()` no longer returns ``True`` if passed +- :gh:`687`, [Linux]: :func:`pid_exists` no longer returns ``True`` if passed a process thread ID. - :gh:`948`: cannot install psutil with ``PYTHONOPTIMIZE=2``. -- :gh:`950`, [Windows]: :meth:`Process.cpu_percent()` was calculated +- :gh:`950`, [Windows]: :meth:`Process.cpu_percent` was calculated incorrectly and showed higher number than real usage. - :gh:`951`, [Windows]: the uploaded wheels for Python 3.6 64 bit didn't work. - :gh:`959`: psutil exception objects could not be pickled. @@ -1670,26 +1669,26 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`609`, [SunOS], **[critical]**: psutil does not compile on Solaris 10. - :gh:`936`, [Windows]: fix compilation error on VS 2013 (patch by Max Bélanger). -- :gh:`940`, [Linux]: :func:`cpu_percent()` and :func:`cpu_times_percent()` was +- :gh:`940`, [Linux]: :func:`cpu_percent` and :func:`cpu_times_percent` was calculated incorrectly as ``iowait``, ``guest`` and ``guest_nice`` times were not properly taken into account. -- :gh:`944`, [OpenBSD]: :func:`pids()` was omitting PID 0. +- :gh:`944`, [OpenBSD]: :func:`pids` was omitting PID 0. 5.0.0 — 2016-11-06 ^^^^^^^^^^^^^^^^^^ **Enhncements** -- :gh:`799`: new :meth:`Process.oneshot()` context manager making +- :gh:`799`: new :meth:`Process.oneshot` context manager making :class:`Process` methods around +2x faster in general and from +2x to +6x faster on Windows. - :gh:`943`: better error message in case of version conflict on import. **Bug fixes** -- :gh:`932`, [NetBSD]: :func:`net_connections()` and - :meth:`Process.connections()` may fail without raising an exception. -- :gh:`933`, [Windows]: memory leak in :func:`cpu_stats()` and +- :gh:`932`, [NetBSD]: :func:`net_connections` and + :meth:`Process.connections` may fail without raising an exception. +- :gh:`933`, [Windows]: memory leak in :func:`cpu_stats` and ``WindowsService.description()`` method. 4.4.2 — 2016-10-26 @@ -1712,9 +1711,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`874`, [Windows]: make :func:`net_if_addrs()` also return the +- :gh:`874`, [Windows]: make :func:`net_if_addrs` also return the ``netmask``. -- :gh:`887`, [Linux]: :func:`virtual_memory()` ``available`` and ``used`` +- :gh:`887`, [Linux]: :func:`virtual_memory` ``available`` and ``used`` values are more precise and match ``free`` cmdline utility. ``available`` also takes into account LCX containers preventing ``available`` to overflow ``total``. @@ -1723,30 +1722,30 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`514`, [macOS], **[critical]**: :meth:`Process.memory_maps()` can +- :gh:`514`, [macOS], **[critical]**: :meth:`Process.memory_maps` can segfault. -- :gh:`783`, [macOS]: :meth:`Process.status()` may erroneously return +- :gh:`783`, [macOS]: :meth:`Process.status` may erroneously return ``"running"`` for zombie processes. -- :gh:`798`, [Windows]: :meth:`Process.open_files()` returns and empty list on +- :gh:`798`, [Windows]: :meth:`Process.open_files` returns and empty list on Windows 10. -- :gh:`825`, [Linux]: :meth:`Process.cpu_affinity()`: fix possible double close +- :gh:`825`, [Linux]: :meth:`Process.cpu_affinity`: fix possible double close and use of unopened socket. -- :gh:`880`, [Windows]: fix race condition inside :func:`net_connections()`. +- :gh:`880`, [Windows]: fix race condition inside :func:`net_connections`. - :gh:`885`: ``ValueError`` is raised if a negative integer is passed to - :func:`cpu_percent()` functions. -- :gh:`892`, [Linux], **[critical]**: :meth:`Process.cpu_affinity()` with + :func:`cpu_percent` functions. +- :gh:`892`, [Linux], **[critical]**: :meth:`Process.cpu_affinity` with ``[-1]`` as arg raises ``SystemError`` with no error set; now ``ValueError`` is raised. -- :gh:`906`, [BSD]: :func:`disk_partitions()` with ``all=False`` returned an +- :gh:`906`, [BSD]: :func:`disk_partitions` with ``all=False`` returned an empty list. Now the argument is ignored and all partitions are always returned. -- :gh:`907`, [FreeBSD]: :meth:`Process.exe()` may fail with +- :gh:`907`, [FreeBSD]: :meth:`Process.exe` may fail with ``OSError(ENOENT)``. - :gh:`908`, [macOS], [BSD]: different process methods could errounesuly mask the real error for high-privileged PIDs and raise :exc:`NoSuchProcess` and :exc:`AccessDenied` instead of ``OSError`` and ``RuntimeError``. -- :gh:`909`, [macOS]: :meth:`Process.open_files()` and - :meth:`Process.connections()` methods may raise ``OSError`` with no exception +- :gh:`909`, [macOS]: :meth:`Process.open_files` and + :meth:`Process.connections` methods may raise ``OSError`` with no exception set if process is gone. - :gh:`916`, [macOS]: fix many compilation warnings. @@ -1759,20 +1758,20 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`854`: :meth:`Process.as_dict()` raises ``ValueError`` if passed an +- :gh:`854`: :meth:`Process.as_dict` raises ``ValueError`` if passed an erroneous attrs name. -- :gh:`857`, [SunOS]: :meth:`Process.cpu_times()`, - :meth:`Process.cpu_percent()`, - :meth:`Process.threads()` and :meth:`Process.memory_maps()` may raise +- :gh:`857`, [SunOS]: :meth:`Process.cpu_times`, + :meth:`Process.cpu_percent`, + :meth:`Process.threads` and :meth:`Process.memory_maps` may raise ``RuntimeError`` if attempting to query a 64bit process with a 32bit Python. "Null" values are returned as a fallback. -- :gh:`858`: :meth:`Process.as_dict()` should not call - :meth:`Process.memory_info_ex()` because it's deprecated. -- :gh:`863`, [Windows]: :meth:`Process.memory_maps()` truncates addresses above +- :gh:`858`: :meth:`Process.as_dict` should not call + :meth:`Process.memory_info_ex` because it's deprecated. +- :gh:`863`, [Windows]: :meth:`Process.memory_maps` truncates addresses above 32 bits. -- :gh:`866`, [Windows]: :func:`win_service_iter()` and services in general are +- :gh:`866`, [Windows]: :func:`win_service_iter` and services in general are not able to handle unicode service names / descriptions. -- :gh:`869`, [Windows]: :meth:`Process.wait()` may raise :exc:`TimeoutExpired` +- :gh:`869`, [Windows]: :meth:`Process.wait` may raise :exc:`TimeoutExpired` with wrong timeout unit (ms instead of sec). - :gh:`870`, [Windows]: handle leak inside ``psutil_get_process_data``. @@ -1782,20 +1781,20 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`819`, [Linux]: different speedup improvements: - :meth:`Process.ppid()` +20% faster. - :meth:`Process.status()` +28% faster. - :meth:`Process.name()` +25% faster. - :meth:`Process.num_threads()` +20% faster on Python 3. + :meth:`Process.ppid` +20% faster. + :meth:`Process.status` +28% faster. + :meth:`Process.name` +25% faster. + :meth:`Process.num_threads` +20% faster on Python 3. **Bug fixes** - :gh:`810`, [Windows]: Windows wheels are incompatible with pip 7.1.2. - :gh:`812`, [NetBSD], **[critical]**: fix compilation on NetBSD-5.x. -- :gh:`823`, [NetBSD]: :func:`virtual_memory()` raises ``TypeError`` on Python +- :gh:`823`, [NetBSD]: :func:`virtual_memory` raises ``TypeError`` on Python 3. -- :gh:`829`, [POSIX]: :func:`disk_usage()` ``percent`` field takes root +- :gh:`829`, [POSIX]: :func:`disk_usage` ``percent`` field takes root reserved space into account. -- :gh:`816`, [Windows]: fixed :func:`net_io_counters()` values wrapping after +- :gh:`816`, [Windows]: fixed :func:`net_io_counters` values wrapping after 4.3GB in Windows Vista (NT 6.0) and above using 64bit values from newer win APIs. @@ -1805,20 +1804,20 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`795`, [Windows]: new APIs to deal with Windows services: - :func:`win_service_iter()` and :func:`win_service_get()`. -- :gh:`800`, [Linux]: :func:`virtual_memory()` returns a new ``shared`` memory + :func:`win_service_iter` and :func:`win_service_get`. +- :gh:`800`, [Linux]: :func:`virtual_memory` returns a new ``shared`` memory field. - :gh:`819`, [Linux]: speedup ``/proc`` parsing: - :meth:`Process.ppid()` +20% faster. - :meth:`Process.status()` +28% faster. - :meth:`Process.name()` +25% faster. - :meth:`Process.num_threads()` +20% faster on Python 3. + :meth:`Process.ppid` +20% faster. + :meth:`Process.status` +28% faster. + :meth:`Process.name` +25% faster. + :meth:`Process.num_threads` +20% faster on Python 3. **Bug fixes** -- :gh:`797`, [Linux]: :func:`net_if_stats()` may raise ``OSError`` for certain +- :gh:`797`, [Linux]: :func:`net_if_stats` may raise ``OSError`` for certain NIC cards. -- :gh:`813`: :meth:`Process.as_dict()` should ignore extraneous attribute names +- :gh:`813`: :meth:`Process.as_dict` should ignore extraneous attribute names which gets attached to the :class:`Process` instance. 4.1.0 — 2016-03-12 @@ -1826,25 +1825,25 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`777`, [Linux]: :meth:`Process.open_files()` on Linux return 3 new +- :gh:`777`, [Linux]: :meth:`Process.open_files` on Linux return 3 new fields: ``position``, ``mode`` and ``flags``. -- :gh:`779`: :meth:`Process.cpu_times()` returns two new fields, +- :gh:`779`: :meth:`Process.cpu_times` returns two new fields, ``children_user`` and ``children_system`` (always set to 0 on macOS and Windows). -- :gh:`789`, [Windows]: :func:`cpu_times()` return two new fields: - ``interrupt`` and ``dpc``. Same for :func:`cpu_times_percent()`. -- :gh:`792`: new :func:`cpu_stats()` function returning number of CPU +- :gh:`789`, [Windows]: :func:`cpu_times` return two new fields: + ``interrupt`` and ``dpc``. Same for :func:`cpu_times_percent`. +- :gh:`792`: new :func:`cpu_stats` function returning number of CPU ``ctx_switches``, ``interrupts``, ``soft_interrupts`` and ``syscalls``. **Bug fixes** -- :gh:`774`, [FreeBSD]: :func:`net_io_counters()` dropout is no longer set to 0 +- :gh:`774`, [FreeBSD]: :func:`net_io_counters` dropout is no longer set to 0 if the kernel provides it. -- :gh:`776`, [Linux]: :meth:`Process.cpu_affinity()` may erroneously raise +- :gh:`776`, [Linux]: :meth:`Process.cpu_affinity` may erroneously raise :exc:`NoSuchProcess`. (patch by wxwright) - :gh:`780`, [macOS]: psutil does not compile with some GCC versions. -- :gh:`786`: :func:`net_if_addrs()` may report incomplete MAC addresses. -- :gh:`788`, [NetBSD]: :func:`virtual_memory()` ``buffers`` and ``shared`` +- :gh:`786`: :func:`net_if_addrs` may report incomplete MAC addresses. +- :gh:`788`, [NetBSD]: :func:`virtual_memory` ``buffers`` and ``shared`` values were set to 0. - :gh:`790`, [macOS], **[critical]**: psutil won't compile on macOS 10.4. @@ -1853,46 +1852,46 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`523`, [Linux], [FreeBSD]: :func:`disk_io_counters()` return a new +- :gh:`523`, [Linux], [FreeBSD]: :func:`disk_io_counters` return a new ``busy_time`` field. - :gh:`660`, [Windows]: make.bat is smarter in finding alternative VS install locations. (patch by mpderbec) -- :gh:`732`: :meth:`Process.environ()`. (patch by Frank Benkstein) +- :gh:`732`: :meth:`Process.environ`. (patch by Frank Benkstein) - :gh:`753`, [Linux], [macOS], [Windows]: process USS and PSS (Linux) "real" memory stats. (patch by Eric Rahm) -- :gh:`755`: :meth:`Process.memory_percent()` ``memtype`` parameter. +- :gh:`755`: :meth:`Process.memory_percent` ``memtype`` parameter. - :gh:`758`: tests now live in psutil namespace. - :gh:`760`: expose OS constants (``psutil.LINUX``, ``psutil.OSX``, etc.) -- :gh:`756`, [Linux]: :func:`disk_io_counters()` return 2 new fields: +- :gh:`756`, [Linux]: :func:`disk_io_counters` return 2 new fields: ``read_merged_count`` and ``write_merged_count``. - :gh:`762`: new `procsmem.py`_ script. **Bug fixes** -- :gh:`685`, [Linux]: :func:`virtual_memory()` provides wrong results on +- :gh:`685`, [Linux]: :func:`virtual_memory` provides wrong results on systems with a lot of physical memory. - :gh:`704`, [SunOS]: psutil does not compile on Solaris sparc. - :gh:`734`: on Python 3 invalid UTF-8 data is not correctly handled for - :meth:`Process.name()`, :meth:`Process.cwd()`, :meth:`Process.exe()`, - :meth:`Process.cmdline()` and :meth:`Process.open_files()` methods resulting + :meth:`Process.name`, :meth:`Process.cwd`, :meth:`Process.exe`, + :meth:`Process.cmdline` and :meth:`Process.open_files` methods resulting in ``UnicodeDecodeError`` exceptions. ``'surrogateescape'`` error handler is now used as a workaround for replacing the corrupted data. - :gh:`737`, [Windows]: when the bitness of psutil and the target process was - different, :meth:`Process.cmdline()` and :meth:`Process.cwd()` could return a + different, :meth:`Process.cmdline` and :meth:`Process.cwd` could return a wrong result or incorrectly report an :exc:`AccessDenied` error. - :gh:`741`, [OpenBSD]: psutil does not compile on mips64. - :gh:`751`, [Linux]: fixed call to ``Py_DECREF`` on possible ``NULL`` object. -- :gh:`754`, [Linux]: :meth:`Process.cmdline()` can be wrong in case of zombie +- :gh:`754`, [Linux]: :meth:`Process.cmdline` can be wrong in case of zombie process. -- :gh:`759`, [Linux]: :meth:`Process.memory_maps()` may return paths ending +- :gh:`759`, [Linux]: :meth:`Process.memory_maps` may return paths ending with ``" (deleted)"``. -- :gh:`761`, [Windows]: :func:`boot_time()` wraps to 0 after 49 days. +- :gh:`761`, [Windows]: :func:`boot_time` wraps to 0 after 49 days. - :gh:`764`, [NetBSD]: fix compilation on NetBSD-6.x. -- :gh:`766`, [Linux]: :func:`net_connections()` can't handle malformed +- :gh:`766`, [Linux]: :func:`net_connections` can't handle malformed ``/proc/net/unix`` file. -- :gh:`767`, [Linux]: :func:`disk_io_counters()` may raise ``ValueError`` on +- :gh:`767`, [Linux]: :func:`disk_io_counters` may raise ``ValueError`` on 2.6 kernels and it's broken on 2.4 kernels. -- :gh:`770`, [NetBSD]: :func:`disk_io_counters()` metrics didn't update. +- :gh:`770`, [NetBSD]: :func:`disk_io_counters` metrics didn't update. 3.4.2 — 2016-01-20 ^^^^^^^^^^^^^^^^^^ @@ -1904,8 +1903,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`724`, [FreeBSD]: :func:`virtual_memory()` ``total`` is incorrect. -- :gh:`730`, [FreeBSD], **[critical]**: :func:`virtual_memory()` crashes with +- :gh:`724`, [FreeBSD]: :func:`virtual_memory` ``total`` is incorrect. +- :gh:`730`, [FreeBSD], **[critical]**: :func:`virtual_memory` crashes with "OSError: [Errno 12] Cannot allocate memory". 3.4.1 — 2016-01-15 @@ -1915,23 +1914,23 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`557`, [NetBSD]: added NetBSD support. (contributed by Ryo Onodera and Thomas Klausner) -- :gh:`708`, [Linux]: :func:`net_connections()` and - :meth:`Process.connections()` on Python 2 can be up to 3x faster in case of +- :gh:`708`, [Linux]: :func:`net_connections` and + :meth:`Process.connections` on Python 2 can be up to 3x faster in case of many connections. Also - :meth:`Process.memory_maps()` is slightly faster. -- :gh:`718`: :func:`process_iter()` is now thread safe. + :meth:`Process.memory_maps` is slightly faster. +- :gh:`718`: :func:`process_iter` is now thread safe. **Bug fixes** -- :gh:`714`, [OpenBSD]: :func:`virtual_memory()` ``cached`` value was always +- :gh:`714`, [OpenBSD]: :func:`virtual_memory` ``cached`` value was always set to 0. -- :gh:`715`, **[critical]**: don't crash at import time if :func:`cpu_times()` +- :gh:`715`, **[critical]**: don't crash at import time if :func:`cpu_times` fail for some reason. -- :gh:`717`, [Linux]: :meth:`Process.open_files()` fails if deleted files still +- :gh:`717`, [Linux]: :meth:`Process.open_files` fails if deleted files still visible. -- :gh:`722`, [Linux]: :func:`swap_memory()` no longer crashes if ``sin`` / +- :gh:`722`, [Linux]: :func:`swap_memory` no longer crashes if ``sin`` / ``sout`` can't be determined due to missing ``/proc/vmstat``. -- :gh:`724`, [FreeBSD]: :func:`virtual_memory()` ``total`` is slightly +- :gh:`724`, [FreeBSD]: :func:`virtual_memory` ``total`` is slightly incorrect. 3.3.0 — 2015-11-25 @@ -1945,7 +1944,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`692`, [POSIX]: :meth:`Process.name()` is no longer cached as it may +- :gh:`692`, [POSIX]: :meth:`Process.name` is no longer cached as it may change. 3.2.2 — 2015-10-04 @@ -1953,9 +1952,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`517`, [SunOS]: :func:`net_io_counters()` failed to detect network +- :gh:`517`, [SunOS]: :func:`net_io_counters` failed to detect network interfaces correctly on Solaris 10 -- :gh:`541`, [FreeBSD]: :func:`disk_io_counters()` r/w times were expressed in +- :gh:`541`, [FreeBSD]: :func:`disk_io_counters` r/w times were expressed in seconds instead of milliseconds. (patch by dasumin) - :gh:`610`, [SunOS]: fix build and tests on Solaris 10 - :gh:`623`, [Linux]: process or system connections raises ``ValueError`` if @@ -1979,33 +1978,33 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`644`, [Windows]: added support for ``CTRL_C_EVENT`` and - ``CTRL_BREAK_EVENT`` signals to use with :meth:`Process.send_signal()`. + ``CTRL_BREAK_EVENT`` signals to use with :meth:`Process.send_signal`. - :gh:`648`: CI test integration for macOS. (patch by Jeff Tang) -- :gh:`663`, [POSIX]: :func:`net_if_addrs()` now returns point-to-point (VPNs) +- :gh:`663`, [POSIX]: :func:`net_if_addrs` now returns point-to-point (VPNs) addresses. - :gh:`655`, [Windows]: different issues regarding unicode handling were fixed. On Python 2 all APIs returning a string will now return an encoded version of it by using sys.getfilesystemencoding() codec. The APIs involved are: - :func:`net_if_addrs()`, :func:`net_if_stats()`, :func:`net_io_counters()`, - :meth:`Process.cmdline()`, :meth:`Process.name()`, - :meth:`Process.username()`, :func:`users()`. + :func:`net_if_addrs`, :func:`net_if_stats`, :func:`net_io_counters`, + :meth:`Process.cmdline`, :meth:`Process.name`, + :meth:`Process.username`, :func:`users`. **Bug fixes** - :gh:`513`, [Linux]: fixed integer overflow for ``RLIM_INFINITY``. - :gh:`641`, [Windows]: fixed many compilation warnings. (patch by Jeff Tang) -- :gh:`652`, [Windows]: :func:`net_if_addrs()` ``UnicodeDecodeError`` in case +- :gh:`652`, [Windows]: :func:`net_if_addrs` ``UnicodeDecodeError`` in case of non-ASCII NIC names. -- :gh:`655`, [Windows]: :func:`net_if_stats()` ``UnicodeDecodeError`` in case +- :gh:`655`, [Windows]: :func:`net_if_stats` ``UnicodeDecodeError`` in case of non-ASCII NIC names. - :gh:`659`, [Linux]: compilation error on Suse 10. (patch by maozguttman) - :gh:`664`, [Linux]: compilation error on Alpine Linux. (patch by Bart van Kleef) -- :gh:`670`, [Windows]: segfgault of :func:`net_if_addrs()` in case of +- :gh:`670`, [Windows]: segfgault of :func:`net_if_addrs` in case of non-ASCII NIC names. (patch by sk6249) - :gh:`672`, [Windows]: compilation fails if using Windows SDK v8.0. (patch by Steven Winfield) -- :gh:`675`, [Linux]: :func:`net_connections()`: ``UnicodeDecodeError`` may +- :gh:`675`, [Linux]: :func:`net_connections`: ``UnicodeDecodeError`` may occur when listing UNIX sockets. 3.1.1 — 2015-07-15 @@ -2013,9 +2012,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`603`, [Linux]: :meth:`Process.ionice()` set value range is incorrect. +- :gh:`603`, [Linux]: :meth:`Process.ionice` set value range is incorrect. (patch by spacewander) -- :gh:`645`, [Linux]: :func:`cpu_times_percent()` may produce negative results. +- :gh:`645`, [Linux]: :func:`cpu_times_percent` may produce negative results. - :gh:`656`: ``from psutil import *`` does not work. 3.1.0 — 2015-07-15 @@ -2023,7 +2022,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`534`, [Linux]: :func:`disk_partitions()` added support for ZFS +- :gh:`534`, [Linux]: :func:`disk_partitions` added support for ZFS filesystems. - :gh:`646`, [Windows]: continuous tests integration for Windows with https://ci.appveyor.com/project/giampaolo/psutil. @@ -2033,16 +2032,16 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`340`, [Windows], **[critical]**: :meth:`Process.open_files()` no longer +- :gh:`340`, [Windows], **[critical]**: :meth:`Process.open_files` no longer hangs. Instead it uses a thread which times out and skips the file handle in case it's taking too long to be retrieved. (patch by Jeff Tang) -- :gh:`627`, [Windows]: :meth:`Process.name()` no longer raises +- :gh:`627`, [Windows]: :meth:`Process.name` no longer raises :exc:`AccessDenied` for pids owned by another user. -- :gh:`636`, [Windows]: :meth:`Process.memory_info()` raise +- :gh:`636`, [Windows]: :meth:`Process.memory_info` raise :exc:`AccessDenied`. - :gh:`637`, [POSIX]: raise exception if trying to send signal to PID 0 as it will affect ``os.getpid()`` 's process group and not PID 0. -- :gh:`639`, [Linux]: :meth:`Process.cmdline()` can be truncated. +- :gh:`639`, [Linux]: :meth:`Process.cmdline` can be truncated. - :gh:`640`, [Linux]: ``*connections`` functions may swallow errors and return an incomplete list of connections. - :gh:`642`: ``repr()`` of exceptions is incorrect. @@ -2058,7 +2057,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`632`, [Linux]: better error message if cannot parse process UNIX connections. -- :gh:`634`, [Linux]: :meth:`Process.cmdline()` does not include empty string +- :gh:`634`, [Linux]: :meth:`Process.cmdline` does not include empty string arguments. - :gh:`635`, [POSIX], **[critical]**: crash on module import if ``enum`` package is installed on Python < 3.4. @@ -2068,21 +2067,21 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`250`: new :func:`net_if_stats()` returning NIC statistics (``isup``, +- :gh:`250`: new :func:`net_if_stats` returning NIC statistics (``isup``, ``duplex``, ``speed``, ``mtu``). -- :gh:`376`: new :func:`net_if_addrs()` returning all NIC addresses a-la +- :gh:`376`: new :func:`net_if_addrs` returning all NIC addresses a-la ``ifconfig``. - :gh:`469`: on Python >= 3.4 ``IOPRIO_CLASS_*`` and ``*_PRIORITY_CLASS`` - constants returned by :meth:`Process.ionice()` and :meth:`Process.nice()` are + constants returned by :meth:`Process.ionice` and :meth:`Process.nice` are enums instead of plain integers. - :gh:`581`: add ``.gitignore``. (patch by Gabi Davar) -- :gh:`582`: connection constants returned by :func:`net_connections()` and - :meth:`Process.connections()` were turned from int to enums on Python > 3.4. +- :gh:`582`: connection constants returned by :func:`net_connections` and + :meth:`Process.connections` were turned from int to enums on Python > 3.4. - :gh:`587`: move native extension into the package. -- :gh:`589`: :meth:`Process.cpu_affinity()` accepts any kind of iterable (set, +- :gh:`589`: :meth:`Process.cpu_affinity` accepts any kind of iterable (set, tuple, ...), not only lists. - :gh:`594`: all deprecated APIs were removed. -- :gh:`599`, [Windows]: :meth:`Process.name()` can now be determined for all +- :gh:`599`, [Windows]: :meth:`Process.name` can now be determined for all processes even when running as a limited user. - :gh:`602`: pre-commit GIT hook. - :gh:`629`: enhanced support for ``pytest`` and ``nose`` test runners. @@ -2092,23 +2091,23 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`428`, [POSIX], **[critical]**: correct handling of zombie processes on POSIX. Introduced new :exc:`ZombieProcess` exception class. -- :gh:`512`, [BSD], **[critical]**: fix segfault in :func:`net_connections()`. -- :gh:`555`, [Linux]: :func:`users()` correctly handles ``":0"`` as an alias +- :gh:`512`, [BSD], **[critical]**: fix segfault in :func:`net_connections`. +- :gh:`555`, [Linux]: :func:`users` correctly handles ``":0"`` as an alias for ``"localhost"``. -- :gh:`579`, [Windows]: fixed :meth:`Process.open_files()` for PID > 64K. +- :gh:`579`, [Windows]: fixed :meth:`Process.open_files` for PID > 64K. - :gh:`579`, [Windows]: fixed many compiler warnings. -- :gh:`585`, [FreeBSD]: :func:`net_connections()` may raise ``KeyError``. -- :gh:`586`, [FreeBSD], **[critical]**: :meth:`Process.cpu_affinity()` +- :gh:`585`, [FreeBSD]: :func:`net_connections` may raise ``KeyError``. +- :gh:`586`, [FreeBSD], **[critical]**: :meth:`Process.cpu_affinity` segfaults on set in case an invalid CPU number is provided. -- :gh:`593`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps()` +- :gh:`593`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps` segfaults. -- :gh:`606`: :meth:`Process.parent()` may swallow :exc:`NoSuchProcess` +- :gh:`606`: :meth:`Process.parent` may swallow :exc:`NoSuchProcess` exceptions. -- :gh:`611`, [SunOS]: :func:`net_io_counters()` has send and received swapped -- :gh:`614`, [Linux]:: :func:`cpu_count()` with ``logical=False`` return the +- :gh:`611`, [SunOS]: :func:`net_io_counters` has send and received swapped +- :gh:`614`, [Linux]:: :func:`cpu_count` with ``logical=False`` return the number of sockets instead of cores. - :gh:`618`, [SunOS]: swap tests fail on Solaris when run as normal user. -- :gh:`628`, [Linux]: :meth:`Process.name()` truncates string in case it +- :gh:`628`, [Linux]: :meth:`Process.name` truncates string in case it contains spaces or parentheses. 2.2.1 — 2015-02-02 @@ -2117,7 +2116,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`572`, [Linux]: fix "ValueError: ambiguous inode with multiple PIDs - references" for :meth:`Process.connections()`. (patch by Bruno Binet) + references" for :meth:`Process.connections`. (patch by Bruno Binet) 2.2.0 — 2015-01-06 ^^^^^^^^^^^^^^^^^^ @@ -2129,27 +2128,27 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`564`: C extension version mismatch in case the user messed up with psutil installation or with sys.path is now detected at import time. - :gh:`568`: new `pidof.py`_ script. -- :gh:`569`, [FreeBSD]: add support for :meth:`Process.cpu_affinity()` on +- :gh:`569`, [FreeBSD]: add support for :meth:`Process.cpu_affinity` on FreeBSD. **Bug fixes** - :gh:`496`, [SunOS], **[critical]**: can't import psutil. -- :gh:`547`, [POSIX]: :meth:`Process.username()` may raise ``KeyError`` if UID +- :gh:`547`, [POSIX]: :meth:`Process.username` may raise ``KeyError`` if UID can't be resolved. - :gh:`551`, [Windows]: get rid of the unicode hack for - :func:`net_io_counters()` NIC names. + :func:`net_io_counters` NIC names. - :gh:`556`, [Linux]: lots of file handles were left open. -- :gh:`561`, [Linux]: :func:`net_connections()` might skip some legitimate UNIX +- :gh:`561`, [Linux]: :func:`net_connections` might skip some legitimate UNIX sockets. (patch by spacewander) -- :gh:`565`, [Windows]: use proper encoding for :meth:`Process.username()` and - :func:`users()`. (patch by Sylvain Mouquet) +- :gh:`565`, [Windows]: use proper encoding for :meth:`Process.username` and + :func:`users`. (patch by Sylvain Mouquet) - :gh:`567`, [Linux]: in the alternative implementation of - :meth:`Process.cpu_affinity()` ``PyList_Append`` and ``Py_BuildValue`` return + :meth:`Process.cpu_affinity` ``PyList_Append`` and ``Py_BuildValue`` return values are not checked. -- :gh:`569`, [FreeBSD]: fix memory leak in :func:`cpu_count()` with +- :gh:`569`, [FreeBSD]: fix memory leak in :func:`cpu_count` with ``logical=False``. -- :gh:`571`, [Linux]: :meth:`Process.open_files()` might swallow +- :gh:`571`, [Linux]: :meth:`Process.open_files` might swallow :exc:`AccessDenied` exceptions and return an incomplete list of open files. 2.1.3 — 2014-09-26 @@ -2172,22 +2171,22 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`340`, [Windows]: :meth:`Process.open_files()` no longer hangs. (patch +- :gh:`340`, [Windows]: :meth:`Process.open_files` no longer hangs. (patch by Jeff Tang) -- :gh:`501`, [Windows]: :func:`disk_io_counters()` may return negative values. -- :gh:`503`, [Linux]: in rare conditions :meth:`Process.exe()`, - :meth:`Process.open_files()` and - :meth:`Process.connections()` can raise ``OSError(ESRCH)`` instead of +- :gh:`501`, [Windows]: :func:`disk_io_counters` may return negative values. +- :gh:`503`, [Linux]: in rare conditions :meth:`Process.exe`, + :meth:`Process.open_files` and + :meth:`Process.connections` can raise ``OSError(ESRCH)`` instead of :exc:`NoSuchProcess`. - :gh:`504`, [Linux]: can't build RPM packages via setup.py - :gh:`506`, [Linux], **[critical]**: Python 2.4 support was broken. -- :gh:`522`, [Linux]: :meth:`Process.cpu_affinity()` might return ``EINVAL``. +- :gh:`522`, [Linux]: :meth:`Process.cpu_affinity` might return ``EINVAL``. (patch by David Daeschler) -- :gh:`529`, [Windows]: :meth:`Process.exe()` may raise unhandled +- :gh:`529`, [Windows]: :meth:`Process.exe` may raise unhandled ``WindowsError`` exception for PIDs 0 and 4. (patch by Jeff Tang) -- :gh:`530`, [Linux]: :func:`disk_io_counters()` may crash on old Linux distros +- :gh:`530`, [Linux]: :func:`disk_io_counters` may crash on old Linux distros (< 2.6.5) (patch by Yaolong Huang) -- :gh:`533`, [Linux]: :meth:`Process.memory_maps()` may raise ``TypeError`` on +- :gh:`533`, [Linux]: :meth:`Process.memory_maps` may raise ``TypeError`` on old Linux distros. 2.1.1 — 2014-04-30 @@ -2195,10 +2194,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`446`, [Windows]: fix encoding error when using :func:`net_io_counters()` +- :gh:`446`, [Windows]: fix encoding error when using :func:`net_io_counters` on Python 3. (patch by Szigeti Gabor Niif) -- :gh:`460`, [Windows]: :func:`net_io_counters()` wraps after 4G. -- :gh:`491`, [Linux]: :func:`net_connections()` exceptions. (patch by Alexander +- :gh:`460`, [Windows]: :func:`net_io_counters` wraps after 4G. +- :gh:`491`, [Linux]: :func:`net_connections` exceptions. (patch by Alexander Grothe) 2.1.0 — 2014-04-08 @@ -2207,13 +2206,13 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`387`: system-wide open connections a-la ``netstat`` (add - :func:`net_connections()`). + :func:`net_connections`). **Bug fixes** - :gh:`421`, [SunOS], **[critical]**: psutil does not compile on SunOS 5.10. (patch by Naveed Roudsari) -- :gh:`489`, [Linux]: :func:`disk_partitions()` return an empty list. +- :gh:`489`, [Linux]: :func:`disk_partitions` return an empty list. 2.0.0 — 2014-03-10 ^^^^^^^^^^^^^^^^^^ @@ -2221,8 +2220,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`424`, [Windows]: installer for Python 3.X 64 bit. -- :gh:`427`: number of logical CPUs and physical cores (:func:`cpu_count()`). -- :gh:`447`: :func:`wait_procs()` ``timeout`` parameter is now optional. +- :gh:`427`: number of logical CPUs and physical cores (:func:`cpu_count`). +- :gh:`447`: :func:`wait_procs` ``timeout`` parameter is now optional. - :gh:`452`: make :class:`Process` instances hashable and usable with ``set()`` s. - :gh:`453`: tests on Python < 2.7 require ``unittest2`` module. @@ -2231,7 +2230,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`463`: make timeout parameter of ``cpu_percent*`` functions default to ``0.0`` 'cause it's a common trap to introduce slowdowns. - :gh:`468`: move documentation to readthedocs.com. -- :gh:`477`: :meth:`Process.cpu_percent()` is about 30% faster. (suggested by +- :gh:`477`: :meth:`Process.cpu_percent` is about 30% faster. (suggested by crusaderky) - :gh:`478`, [Linux]: almost all APIs are about 30% faster on Python 3.X. - :gh:`479`: long deprecated ``psutil.error`` module is gone; exception classes @@ -2241,25 +2240,25 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`193`: :class:`Popen` constructor can throw an exception if the spawned process terminates quickly. -- :gh:`340`, [Windows]: :meth:`Process.open_files()` no longer hangs. (patch +- :gh:`340`, [Windows]: :meth:`Process.open_files` no longer hangs. (patch by jtang@vahna.net) - :gh:`443`, [Linux]: fix a potential overflow issue for - :meth:`Process.cpu_affinity()` (set) on systems with more than 64 CPUs. -- :gh:`448`, [Windows]: :meth:`Process.children()` and :meth:`Process.ppid()` + :meth:`Process.cpu_affinity` (set) on systems with more than 64 CPUs. +- :gh:`448`, [Windows]: :meth:`Process.children` and :meth:`Process.ppid` memory leak (patch by Ulrich Klank). -- :gh:`457`, [POSIX]: :func:`pid_exists()` always returns ``True`` for PID 0. +- :gh:`457`, [POSIX]: :func:`pid_exists` always returns ``True`` for PID 0. - :gh:`461`: namedtuples are not pickle-able. -- :gh:`466`, [Linux]: :meth:`Process.exe()` improper null bytes handling. +- :gh:`466`, [Linux]: :meth:`Process.exe` improper null bytes handling. (patch by Gautam Singh) -- :gh:`470`: :func:`wait_procs()` might not wait. (patch by crusaderky) -- :gh:`471`, [Windows]: :meth:`Process.exe()` improper unicode handling. (patch +- :gh:`470`: :func:`wait_procs` might not wait. (patch by crusaderky) +- :gh:`471`, [Windows]: :meth:`Process.exe` improper unicode handling. (patch by alex@mroja.net) - :gh:`473`: :class:`Popen` ``wait()`` method does not set returncode attribute. -- :gh:`474`, [Windows]: :meth:`Process.cpu_percent()` is no longer capped at +- :gh:`474`, [Windows]: :meth:`Process.cpu_percent` is no longer capped at 100%. -- :gh:`476`, [Linux]: encoding error for :meth:`Process.name()` and - :meth:`Process.cmdline()`. +- :gh:`476`, [Linux]: encoding error for :meth:`Process.name` and + :meth:`Process.cmdline`. **API changes** @@ -2385,7 +2384,7 @@ cases accessing the old names will work but it will cause a - long deprecated ``psutil.error`` module is gone; exception classes now live in "psutil" namespace only. - :class:`Process` instances' ``retcode`` attribute returned by - :func:`wait_procs()` has been renamed to ``returncode`` for consistency with + :func:`wait_procs` has been renamed to ``returncode`` for consistency with ``subprocess.Popen``. 1.2.1 — 2013-11-25 @@ -2397,7 +2396,7 @@ cases accessing the old names will work but it will cause a occurring on module import on Windows XP. - :gh:`425`, [SunOS], **[critical]**: crash on import due to failure at determining ``BOOT_TIME``. -- :gh:`443`, [Linux]: :meth:`Process.cpu_affinity()` can't set affinity on +- :gh:`443`, [Linux]: :meth:`Process.cpu_affinity` can't set affinity on systems with more than 64 cores. 1.2.0 — 2013-11-20 @@ -2407,7 +2406,7 @@ cases accessing the old names will work but it will cause a - :gh:`439`: assume ``os.getpid()`` if no argument is passed to :class:`Process` class constructor. -- :gh:`440`: new :func:`wait_procs()` utility function which waits for multiple +- :gh:`440`: new :func:`wait_procs` utility function which waits for multiple processes to terminate. **Bug fixes** @@ -2446,29 +2445,29 @@ cases accessing the old names will work but it will cause a - :gh:`410`: host tar.gz and Windows binary files are on PyPI. - :gh:`412`, [Linux]: get/set process resource limits - (:meth:`Process.rlimit()`). -- :gh:`415`, [Windows]: :meth:`Process.children()` is an order of magnitude + (:meth:`Process.rlimit`). +- :gh:`415`, [Windows]: :meth:`Process.children` is an order of magnitude faster. -- :gh:`426`, [Windows]: :meth:`Process.name()` is an order of magnitude faster. -- :gh:`431`, [POSIX]: :meth:`Process.name()` is slightly faster because it - unnecessarily retrieved also :meth:`Process.cmdline()`. +- :gh:`426`, [Windows]: :meth:`Process.name` is an order of magnitude faster. +- :gh:`431`, [POSIX]: :meth:`Process.name` is slightly faster because it + unnecessarily retrieved also :meth:`Process.cmdline`. **Bug fixes** -- :gh:`391`, [Windows]: :func:`cpu_times_percent()` returns negative +- :gh:`391`, [Windows]: :func:`cpu_times_percent` returns negative percentages. - :gh:`408`: ``STATUS_*`` and ``CONN_*`` constants don't properly serialize on JSON. - :gh:`411`, [Windows]: `disk_usage.py`_ may pop-up a GUI error. -- :gh:`413`, [Windows]: :meth:`Process.memory_info()` leaks memory. -- :gh:`414`, [Windows]: :meth:`Process.exe()` on Windows XP may raise +- :gh:`413`, [Windows]: :meth:`Process.memory_info` leaks memory. +- :gh:`414`, [Windows]: :meth:`Process.exe` on Windows XP may raise ``ERROR_INVALID_PARAMETER``. -- :gh:`416`: :func:`disk_usage()` doesn't work well with unicode path names. -- :gh:`430`, [Linux]: :meth:`Process.io_counters()` report wrong number of r/w +- :gh:`416`: :func:`disk_usage` doesn't work well with unicode path names. +- :gh:`430`, [Linux]: :meth:`Process.io_counters` report wrong number of r/w syscalls. -- :gh:`435`, [Linux]: :func:`net_io_counters()` might report erreneous NIC +- :gh:`435`, [Linux]: :func:`net_io_counters` might report erreneous NIC names. -- :gh:`436`, [Linux]: :func:`net_io_counters()` reports a wrong ``dropin`` +- :gh:`436`, [Linux]: :func:`net_io_counters` reports a wrong ``dropin`` value. **API changes** @@ -2481,7 +2480,7 @@ cases accessing the old names will work but it will cause a **Bug fixes** -- :gh:`405`: :func:`net_io_counters()` ``pernic=True`` no longer works as +- :gh:`405`: :func:`net_io_counters` ``pernic=True`` no longer works as intended in 1.0.0. 1.0.0 — 2013-07-10 @@ -2490,7 +2489,7 @@ cases accessing the old names will work but it will cause a **Enhancements** - :gh:`18`, [SunOS]: add Solaris support (yay!) (thanks Justin Venus) -- :gh:`367`: :meth:`Process.connections()` ``status`` strings are now +- :gh:`367`: :meth:`Process.connections` ``status`` strings are now constants. - :gh:`380`: test suite exits with non-zero on failure. (patch by floppymaster) @@ -2501,28 +2500,28 @@ cases accessing the old names will work but it will cause a - :gh:`374`, [Windows]: negative memory usage reported if process uses a lot of memory. -- :gh:`379`, [Linux]: :meth:`Process.memory_maps()` may raise ``ValueError``. -- :gh:`394`, [macOS]: mapped memory regions of :meth:`Process.memory_maps()` +- :gh:`379`, [Linux]: :meth:`Process.memory_maps` may raise ``ValueError``. +- :gh:`394`, [macOS]: mapped memory regions of :meth:`Process.memory_maps` report incorrect file name. - :gh:`404`, [Linux]: ``sched_*affinity()`` are implicitly declared. (patch by Arfrever) **API changes** -- :meth:`Process.connections()` ``status`` field is no longer a string but a +- :meth:`Process.connections` ``status`` field is no longer a string but a constant object (``psutil.CONN_*``). -- :meth:`Process.connections()` ``local_address`` and ``remote_address`` fields +- :meth:`Process.connections` ``local_address`` and ``remote_address`` fields renamed to ``laddr`` and ``raddr``. -- psutil.network_io_counters() renamed to :func:`net_io_counters()`. +- psutil.network_io_counters() renamed to :func:`net_io_counters`. 0.7.1 — 2013-05-03 ^^^^^^^^^^^^^^^^^^ **Bug fixes** -- :gh:`325`, [BSD], **[critical]**: :func:`virtual_memory()` can raise +- :gh:`325`, [BSD], **[critical]**: :func:`virtual_memory` can raise ``SystemError``. (patch by Jan Beich) -- :gh:`370`, [BSD]: :meth:`Process.connections()` requires root. (patch by +- :gh:`370`, [BSD]: :meth:`Process.connections` requires root. (patch by John Baldwin) - :gh:`372`, [BSD]: different process methods raise :exc:`NoSuchProcess` instead of @@ -2535,65 +2534,65 @@ cases accessing the old names will work but it will cause a - :gh:`233`: code migrated to Mercurial (yay!) - :gh:`246`: psutil.error module is deprecated and scheduled for removal. -- :gh:`328`, [Windows]: :meth:`Process.ionice()` support. -- :gh:`359`: add :func:`boot_time()` as a substitute of ``psutil.BOOT_TIME`` +- :gh:`328`, [Windows]: :meth:`Process.ionice` support. +- :gh:`359`: add :func:`boot_time` as a substitute of ``psutil.BOOT_TIME`` since the latter cannot reflect system clock updates. -- :gh:`361`, [Linux]: :func:`cpu_times()` now includes new ``steal``, ``guest`` +- :gh:`361`, [Linux]: :func:`cpu_times` now includes new ``steal``, ``guest`` and ``guest_nice`` fields available on recent Linux kernels. Also, - :func:`cpu_percent()` is more accurate. -- :gh:`362`: add :func:`cpu_times_percent()` (per-CPU-time utilization as a + :func:`cpu_percent` is more accurate. +- :gh:`362`: add :func:`cpu_times_percent` (per-CPU-time utilization as a percentage). **Bug fixes** -- :gh:`234`, [Windows]: :func:`disk_io_counters()` fails to list certain disks. -- :gh:`264`, [Windows]: use of :func:`disk_partitions()` may cause a message +- :gh:`234`, [Windows]: :func:`disk_io_counters` fails to list certain disks. +- :gh:`264`, [Windows]: use of :func:`disk_partitions` may cause a message box to appear. -- :gh:`313`, [Linux], **[critical]**: :func:`virtual_memory()` and - :func:`swap_memory()` can crash on certain exotic Linux flavors having an +- :gh:`313`, [Linux], **[critical]**: :func:`virtual_memory` and + :func:`swap_memory` can crash on certain exotic Linux flavors having an incomplete ``/proc`` interface. If that's the case we now set the unretrievable stats to ``0`` and raise ``RuntimeWarning`` instead. - :gh:`315`, [macOS]: fix some compilation warnings. - :gh:`317`, [Windows]: cannot set process CPU affinity above 31 cores. -- :gh:`319`, [Linux]: :meth:`Process.memory_maps()` raises ``KeyError`` +- :gh:`319`, [Linux]: :meth:`Process.memory_maps` raises ``KeyError`` 'Anonymous' on Debian squeeze. -- :gh:`321`, [POSIX]: :meth:`Process.ppid()` property is no longer cached as +- :gh:`321`, [POSIX]: :meth:`Process.ppid` property is no longer cached as the kernel may set the PPID to 1 in case of a zombie process. -- :gh:`323`, [macOS]: :func:`disk_io_counters()` ``read_time`` and +- :gh:`323`, [macOS]: :func:`disk_io_counters` ``read_time`` and ``write_time`` parameters were reporting microseconds not milliseconds. (patch by Gregory Szorc) -- :gh:`331`: :meth:`Process.cmdline()` is no longer cached after first access +- :gh:`331`: :meth:`Process.cmdline` is no longer cached after first access as it may change. - :gh:`333`, [macOS]: leak of Mach ports (patch by rsesek@google.com) - :gh:`337`, [Linux], **[critical]**: :class:`Process` methods not working because of a poor ``/proc`` implementation will raise ``NotImplementedError`` - rather than ``RuntimeError`` and :meth:`Process.as_dict()` will not blow up. + rather than ``RuntimeError`` and :meth:`Process.as_dict` will not blow up. (patch by Curtin1060) -- :gh:`338`, [Linux]: :func:`disk_io_counters()` fails to find some disks. +- :gh:`338`, [Linux]: :func:`disk_io_counters` fails to find some disks. - :gh:`339`, [FreeBSD]: ``get_pid_list()`` can allocate all the memory on system. - :gh:`341`, [Linux], **[critical]**: psutil might crash on import due to error in retrieving system terminals map. -- :gh:`344`, [FreeBSD]: :func:`swap_memory()` might return incorrect results +- :gh:`344`, [FreeBSD]: :func:`swap_memory` might return incorrect results due to ``kvm_open(3)`` not being called. (patch by Jean Sebastien) -- :gh:`338`, [Linux]: :func:`disk_io_counters()` fails to find some disks. +- :gh:`338`, [Linux]: :func:`disk_io_counters` fails to find some disks. - :gh:`351`, [Windows]: if psutil is compiled with MinGW32 (provided installers - for py2.4 and py2.5 are) :func:`disk_io_counters()` will fail. (Patch by + for py2.4 and py2.5 are) :func:`disk_io_counters` will fail. (Patch by m.malycha) -- :gh:`353`, [macOS]: :func:`users()` returns an empty list on macOS 10.8. -- :gh:`356`: :meth:`Process.parent()` now checks whether parent PID has been +- :gh:`353`, [macOS]: :func:`users` returns an empty list on macOS 10.8. +- :gh:`356`: :meth:`Process.parent` now checks whether parent PID has been reused in which case returns ``None``. -- :gh:`365`: :meth:`Process.nice()` (set) should check PID has not been reused +- :gh:`365`: :meth:`Process.nice` (set) should check PID has not been reused by another process. -- :gh:`366`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps()`, - :meth:`Process.num_fds()`, - :meth:`Process.open_files()` and :meth:`Process.cwd()` methods raise +- :gh:`366`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps`, + :meth:`Process.num_fds`, + :meth:`Process.open_files` and :meth:`Process.cwd` methods raise ``RuntimeError`` instead of :exc:`AccessDenied`. **API changes** -- :meth:`Process.cmdline()` property is no longer cached after first access. -- :meth:`Process.ppid()` property is no longer cached after first access. +- :meth:`Process.cmdline` property is no longer cached after first access. +- :meth:`Process.ppid` property is no longer cached after first access. - [Linux] :class:`Process` methods not working because of a poor ``/proc`` implementation will raise ``NotImplementedError`` instead of ``RuntimeError``. @@ -2604,56 +2603,56 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`316`: :meth:`Process.cmdline()` property now makes a better job at +- :gh:`316`: :meth:`Process.cmdline` property now makes a better job at guessing the process executable from the cmdline. **Bug fixes** -- :gh:`316`: :meth:`Process.exe()` was resolved in case it was a symlink. +- :gh:`316`: :meth:`Process.exe` was resolved in case it was a symlink. - :gh:`318`, **[critical]**: Python 2.4 compatibility was broken. **API changes** -- :meth:`Process.exe()` can now return an empty string instead of raising +- :meth:`Process.exe` can now return an empty string instead of raising :exc:`AccessDenied`. -- :meth:`Process.exe()` is no longer resolved in case it's a symlink. +- :meth:`Process.exe` is no longer resolved in case it's a symlink. 0.6.0 — 2012-08-13 ^^^^^^^^^^^^^^^^^^ **Enhancements** -- :gh:`216`, [POSIX]: :meth:`Process.connections()` UNIX sockets support. +- :gh:`216`, [POSIX]: :meth:`Process.connections` UNIX sockets support. - :gh:`220`, [FreeBSD]: ``get_connections()`` has been rewritten in C and no longer requires ``lsof``. -- :gh:`222`, [macOS]: add support for :meth:`Process.cwd()`. +- :gh:`222`, [macOS]: add support for :meth:`Process.cwd`. - :gh:`261`: per-process extended memory info - (:meth:`Process.memory_info_ex()`). -- :gh:`295`, [macOS]: :meth:`Process.exe()` path is now determined by asking - the OS instead of being guessed from :meth:`Process.cmdline()`. + (:meth:`Process.memory_info_ex`). +- :gh:`295`, [macOS]: :meth:`Process.exe` path is now determined by asking + the OS instead of being guessed from :meth:`Process.cmdline`. - :gh:`297`, [macOS]: the :class:`Process` methods below were always raising :exc:`AccessDenied` for any process except the current one. Now this is no - longer true. Also they are 2.5x faster. :meth:`Process.name()`, - :meth:`Process.memory_info()`, - :meth:`Process.memory_percent()`, :meth:`Process.cpu_times()`, - :meth:`Process.cpu_percent()`, - :meth:`Process.num_threads()`. + longer true. Also they are 2.5x faster. :meth:`Process.name`, + :meth:`Process.memory_info`, + :meth:`Process.memory_percent`, :meth:`Process.cpu_times`, + :meth:`Process.cpu_percent`, + :meth:`Process.num_threads`. - :gh:`300`: add `pmap.py`_ script. -- :gh:`301`: :func:`process_iter()` now yields processes sorted by their PIDs. +- :gh:`301`: :func:`process_iter` now yields processes sorted by their PIDs. - :gh:`302`: per-process number of voluntary and involuntary context switches - (:meth:`Process.num_ctx_switches()`). + (:meth:`Process.num_ctx_switches`). - :gh:`303`, [Windows]: the :class:`Process` methods below were always raising :exc:`AccessDenied` for any process not owned by current user. Now this is no longer true: - :meth:`Process.create_time()`, :meth:`Process.cpu_times()`, - :meth:`Process.cpu_percent()`, - :meth:`Process.memory_info()`, :meth:`Process.memory_percent()`, - :meth:`Process.num_handles()`, - :meth:`Process.io_counters()`. + :meth:`Process.create_time`, :meth:`Process.cpu_times`, + :meth:`Process.cpu_percent`, + :meth:`Process.memory_info`, :meth:`Process.memory_percent`, + :meth:`Process.num_handles`, + :meth:`Process.io_counters`. - :gh:`305`: add `netstat.py`_ script. - :gh:`311`: system memory functions has been refactorized and rewritten and now provide a more detailed and consistent representation of the system - memory. Added new :func:`virtual_memory()` and :func:`swap_memory()` + memory. Added new :func:`virtual_memory` and :func:`swap_memory` functions. All old memory-related functions are deprecated. Also two new example scripts were added: `free.py`_ and `meminfo.py`_. - :gh:`312`: ``net_io_counters()`` namedtuple includes 4 new fields: ``errin``, @@ -2662,31 +2661,31 @@ cases accessing the old names will work but it will cause a **Bug fixes** -- :gh:`298`, [macOS], [BSD]: memory leak in :meth:`Process.num_fds()`. +- :gh:`298`, [macOS], [BSD]: memory leak in :meth:`Process.num_fds`. - :gh:`299`: potential memory leak every time ``PyList_New(0)`` is used. - :gh:`303`, [Windows], **[critical]**: potential heap corruption in - :meth:`Process.num_threads()` and :meth:`Process.status()` methods. + :meth:`Process.num_threads` and :meth:`Process.status` methods. - :gh:`305`, [FreeBSD], **[critical]**: can't compile on FreeBSD 9 due to removal of ``utmp.h``. - :gh:`306`, **[critical]**: at C level, errors are not checked when invoking ``Py*`` functions which create or manipulate Python objects leading to potential memory related errors and/or segmentation faults. -- :gh:`307`, [FreeBSD]: values returned by :func:`net_io_counters()` are wrong. +- :gh:`307`, [FreeBSD]: values returned by :func:`net_io_counters` are wrong. - :gh:`308`, [BSD], [Windows]: ``psutil.virtmem_usage()`` wasn't actually returning information about swap memory usage as it was supposed to do. It does now. -- :gh:`309`: :meth:`Process.open_files()` might not return files which can not +- :gh:`309`: :meth:`Process.open_files` might not return files which can not be accessed due to limited permissions. :exc:`AccessDenied` is now raised instead. **API changes** -- ``psutil.phymem_usage()`` is deprecated (use :func:`virtual_memory()`) -- ``psutil.virtmem_usage()`` is deprecated (use :func:`swap_memory()`) +- ``psutil.phymem_usage()`` is deprecated (use :func:`virtual_memory`) +- ``psutil.virtmem_usage()`` is deprecated (use :func:`swap_memory`) - [Linux]: ``psutil.phymem_buffers()`` is deprecated (use - :func:`virtual_memory()`) + :func:`virtual_memory`) - [Linux]: ``psutil.cached_phymem()`` is deprecated (use - :func:`virtual_memory()`) + :func:`virtual_memory`) - [Windows], [BSD]: ``psutil.virtmem_usage()`` now returns information about swap memory instead of virtual memory. @@ -2695,14 +2694,14 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`293`, [Windows]: :meth:`Process.exe()` path is now determined by asking - the OS instead of being guessed from :meth:`Process.cmdline()`. +- :gh:`293`, [Windows]: :meth:`Process.exe` path is now determined by asking + the OS instead of being guessed from :meth:`Process.cmdline`. **Bug fixes** -- :gh:`292`, [Linux]: race condition in process :meth:`Process.open_files()`, - :meth:`Process.connections()`, :meth:`Process.threads()`. -- :gh:`294`, [Windows]: :meth:`Process.cpu_affinity()` is only able to set CPU +- :gh:`292`, [Linux]: race condition in process :meth:`Process.open_files`, + :meth:`Process.connections`, :meth:`Process.threads`. +- :gh:`294`, [Windows]: :meth:`Process.cpu_affinity` is only able to set CPU #0. 0.5.0 — 2012-06-27 @@ -2711,38 +2710,38 @@ cases accessing the old names will work but it will cause a **Enhancements** - :gh:`195`, [Windows]: number of handles opened by process - (:meth:`Process.num_handles()`). -- :gh:`209`: :func:`disk_partitions()` now provides also mount options. -- :gh:`229`: list users currently connected on the system (:func:`users()`). + (:meth:`Process.num_handles`). +- :gh:`209`: :func:`disk_partitions` now provides also mount options. +- :gh:`229`: list users currently connected on the system (:func:`users`). - :gh:`238`, [Linux], [Windows]: process CPU affinity (get and set, - :meth:`Process.cpu_affinity()`). -- :gh:`242`: add ``recursive=True`` to :meth:`Process.children()`: return all + :meth:`Process.cpu_affinity`). +- :gh:`242`: add ``recursive=True`` to :meth:`Process.children`: return all process descendants. -- :gh:`245`, [POSIX]: :meth:`Process.wait()` incrementally consumes less CPU +- :gh:`245`, [POSIX]: :meth:`Process.wait` incrementally consumes less CPU cycles. - :gh:`257`, [Windows]: removed Windows 2000 support. -- :gh:`258`, [Linux]: :meth:`Process.memory_info()` is now 0.5x faster. +- :gh:`258`, [Linux]: :meth:`Process.memory_info` is now 0.5x faster. - :gh:`260`: process's mapped memory regions. (Windows patch by wj32.64, macOS patch by Jeremy Whitlock) -- :gh:`262`, [Windows]: :func:`disk_partitions()` was slow due to inspecting +- :gh:`262`, [Windows]: :func:`disk_partitions` was slow due to inspecting the floppy disk drive also when parameter is ``all=False``. - :gh:`273`: ``psutil.get_process_list()`` is deprecated. - :gh:`274`: psutil no longer requires ``2to3`` at installation time in order to work with Python 3. -- :gh:`278`: new :meth:`Process.as_dict()` method. -- :gh:`281`: :meth:`Process.ppid()`, :meth:`Process.name()`, - :meth:`Process.exe()`, - :meth:`Process.cmdline()` and :meth:`Process.create_time()` properties of +- :gh:`278`: new :meth:`Process.as_dict` method. +- :gh:`281`: :meth:`Process.ppid`, :meth:`Process.name`, + :meth:`Process.exe`, + :meth:`Process.cmdline` and :meth:`Process.create_time` properties of :class:`Process` class are now cached after being accessed. - :gh:`282`: ``psutil.STATUS_*`` constants can now be compared by using their string representation. -- :gh:`283`: speedup :meth:`Process.is_running()` by caching its return value +- :gh:`283`: speedup :meth:`Process.is_running` by caching its return value in case the process is terminated. - :gh:`284`, [POSIX]: per-process number of opened file descriptors - (:meth:`Process.num_fds()`). -- :gh:`287`: :func:`process_iter()` now caches :class:`Process` instances + (:meth:`Process.num_fds`). +- :gh:`287`: :func:`process_iter` now caches :class:`Process` instances between calls. -- :gh:`290`: :meth:`Process.nice()` property is deprecated in favor of new +- :gh:`290`: :meth:`Process.nice` property is deprecated in favor of new ``get_nice()`` and ``set_nice()`` methods. **Bug fixes** @@ -2750,35 +2749,35 @@ cases accessing the old names will work but it will cause a - :gh:`193`: :class:`Popen` constructor can throw an exception if the spawned process terminates quickly. - :gh:`240`, [macOS]: incorrect use of ``free()`` for - :meth:`Process.connections()`. -- :gh:`244`, [POSIX]: :meth:`Process.wait()` can hog CPU resources if called + :meth:`Process.connections`. +- :gh:`244`, [POSIX]: :meth:`Process.wait` can hog CPU resources if called against a process which is not our children. -- :gh:`248`, [Linux]: :func:`net_io_counters()` might return erroneous NIC +- :gh:`248`, [Linux]: :func:`net_io_counters` might return erroneous NIC names. -- :gh:`252`, [Windows]: :meth:`Process.cwd()` erroneously raise +- :gh:`252`, [Windows]: :meth:`Process.cwd` erroneously raise :exc:`NoSuchProcess` for processes owned by another user. It now raises :exc:`AccessDenied` instead. - :gh:`266`, [Windows]: ``psutil.get_pid_list()`` only shows 1024 processes. (patch by Amoser) -- :gh:`267`, [macOS]: :meth:`Process.connections()` returns wrong remote +- :gh:`267`, [macOS]: :meth:`Process.connections` returns wrong remote address. (Patch by Amoser) -- :gh:`272`, [Linux]: :meth:`Process.open_files()` potential race condition can +- :gh:`272`, [Linux]: :meth:`Process.open_files` potential race condition can lead to unexpected :exc:`NoSuchProcess` exception. Also, we can get incorrect reports of not absolutized path names. - :gh:`275`, [Linux]: ``Process.io_counters()`` erroneously raise :exc:`NoSuchProcess` on old Linux versions. Where not available it now raises ``NotImplementedError``. -- :gh:`286`: :meth:`Process.is_running()` doesn't actually check whether PID +- :gh:`286`: :meth:`Process.is_running` doesn't actually check whether PID has been reused. -- :gh:`314`: :meth:`Process.children()` can sometimes return non-children. +- :gh:`314`: :meth:`Process.children` can sometimes return non-children. **API changes** - ``Process.nice`` property is deprecated in favor of new ``get_nice()`` and ``set_nice()`` methods. - ``psutil.get_process_list()`` is deprecated. -- :meth:`Process.ppid()`, :meth:`Process.name()`, :meth:`Process.exe()`, - :meth:`Process.cmdline()` and :meth:`Process.create_time()` properties of +- :meth:`Process.ppid`, :meth:`Process.name`, :meth:`Process.exe`, + :meth:`Process.cmdline` and :meth:`Process.create_time` properties of :class:`Process` class are now cached after being accessed, meaning :exc:`NoSuchProcess` will no longer be raised in case the process is gone in the meantime. @@ -2792,30 +2791,30 @@ cases accessing the old names will work but it will cause a - :gh:`228`: some example scripts were not working with Python 3. - :gh:`230`, [Windows], [macOS]: fix memory leak in - :meth:`Process.connections()`. + :meth:`Process.connections`. - :gh:`232`, [Linux]: ``psutil.phymem_usage()`` can report erroneous values which are different than ``free`` command. - :gh:`236`, [Windows]: fix memory/handle leak in - :meth:`Process.memory_info()`, - :meth:`Process.suspend()` and :meth:`Process.resume()` methods. + :meth:`Process.memory_info`, + :meth:`Process.suspend` and :meth:`Process.resume` methods. 0.4.0 — 2011-10-29 ^^^^^^^^^^^^^^^^^^ **Enhancements** -- :gh:`150`: network I/O counters (:func:`net_io_counters()`). (macOS and +- :gh:`150`: network I/O counters (:func:`net_io_counters`). (macOS and Windows patch by Jeremy Whitlock) -- :gh:`154`, [FreeBSD]: add support for :meth:`Process.cwd()`. +- :gh:`154`, [FreeBSD]: add support for :meth:`Process.cwd`. - :gh:`157`, [Windows]: provide installer for Python 3.2 64-bit. -- :gh:`198`: :meth:`Process.wait()` with ``timeout=0`` can now be used to make +- :gh:`198`: :meth:`Process.wait` with ``timeout=0`` can now be used to make the function return immediately. -- :gh:`206`: disk I/O counters (:func:`disk_io_counters()`). (macOS and Windows +- :gh:`206`: disk I/O counters (:func:`disk_io_counters`). (macOS and Windows patch by Jeremy Whitlock) - :gh:`213`: add `iotop.py`_ script. -- :gh:`217`: :meth:`Process.connections()` now has a ``kind`` argument to +- :gh:`217`: :meth:`Process.connections` now has a ``kind`` argument to filter for connections with different criteria. -- :gh:`221`, [FreeBSD]: :meth:`Process.open_files()` has been rewritten in C +- :gh:`221`, [FreeBSD]: :meth:`Process.open_files` has been rewritten in C and no longer relies on ``lsof``. - :gh:`223`: add `top.py`_ script. - :gh:`227`: add `nettop.py`_ script. @@ -2825,13 +2824,13 @@ cases accessing the old names will work but it will cause a - :gh:`135`, [macOS]: psutil cannot create :class:`Process` object. - :gh:`144`, [Linux]: no longer support 0 special PID. - :gh:`188`, [Linux]: psutil import error on Linux ARM architectures. -- :gh:`194`, [POSIX]: :meth:`Process.cpu_percent()` now reports a percentage +- :gh:`194`, [POSIX]: :meth:`Process.cpu_percent` now reports a percentage over 100 on multicore processors. -- :gh:`197`, [Linux]: :meth:`Process.connections()` is broken on platforms not +- :gh:`197`, [Linux]: :meth:`Process.connections` is broken on platforms not supporting IPv6. - :gh:`200`, [Linux], **[critical]**: ``psutil.NUM_CPUS`` not working on armel and sparc architectures and causing crash on module import. -- :gh:`201`, [Linux]: :meth:`Process.connections()` is broken on big-endian +- :gh:`201`, [Linux]: :meth:`Process.connections` is broken on big-endian architectures. - :gh:`211`: :class:`Process` instance can unexpectedly raise :exc:`NoSuchProcess` if tested for equality with another object. @@ -2846,16 +2845,16 @@ cases accessing the old names will work but it will cause a **Enhancements** - :gh:`125`: system per-cpu percentage utilization and times - (:meth:`Process.cpu_times()`, - :meth:`Process.cpu_percent()`). + (:meth:`Process.cpu_times`, + :meth:`Process.cpu_percent`). - :gh:`163`: per-process associated terminal / TTY - (:meth:`Process.terminal()`). + (:meth:`Process.terminal`). - :gh:`171`: added ``get_phymem()`` and ``get_virtmem()`` functions returning system memory information (``total``, ``used``, ``free``) and memory percent usage. ``total_*``, ``avail_*`` and ``used_*`` memory functions are deprecated. -- :gh:`172`: disk usage statistics (:func:`disk_usage()`). -- :gh:`174`: mounted disk partitions (:func:`disk_partitions()`). +- :gh:`172`: disk usage statistics (:func:`disk_usage`). +- :gh:`174`: mounted disk partitions (:func:`disk_partitions`). - :gh:`179`: setuptools is now used in setup.py **Bug fixes** @@ -2864,14 +2863,14 @@ cases accessing the old names will work but it will cause a impersonation on return. - :gh:`164`, [Windows]: wait function raises a ``TimeoutException`` when a process returns ``-1``. -- :gh:`165`: :meth:`Process.status()` raises an unhandled exception. -- :gh:`166`: :meth:`Process.memory_info()` leaks handles hogging system +- :gh:`165`: :meth:`Process.status` raises an unhandled exception. +- :gh:`166`: :meth:`Process.memory_info` leaks handles hogging system resources. -- :gh:`168`: :func:`cpu_percent()` returns erroneous results when used in +- :gh:`168`: :func:`cpu_percent` returns erroneous results when used in non-blocking mode. (patch by Philip Roberts) -- :gh:`178`, [macOS]: :meth:`Process.threads()` leaks memory. -- :gh:`180`, [Windows]: :meth:`Process.num_threads()` and - :meth:`Process.threads()` methods can raise :exc:`NoSuchProcess` exception +- :gh:`178`, [macOS]: :meth:`Process.threads` leaks memory. +- :gh:`180`, [Windows]: :meth:`Process.num_threads` and + :meth:`Process.threads` methods can raise :exc:`NoSuchProcess` exception while process still exists. 0.2.1 — 2011-03-20 @@ -2879,39 +2878,39 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`64`: per-process I/O counters (:meth:`Process.io_counters()`). -- :gh:`116`: per-process :meth:`Process.wait()` (wait for process to terminate +- :gh:`64`: per-process I/O counters (:meth:`Process.io_counters`). +- :gh:`116`: per-process :meth:`Process.wait` (wait for process to terminate and return its exit code). -- :gh:`134`: per-process threads (:meth:`Process.threads()`). -- :gh:`136`: :meth:`Process.exe()` path on FreeBSD is now determined by asking +- :gh:`134`: per-process threads (:meth:`Process.threads`). +- :gh:`136`: :meth:`Process.exe` path on FreeBSD is now determined by asking the kernel instead of guessing it from cmdline[0]. - :gh:`137`: per-process real, effective and saved user and group ids - (:meth:`Process.gids()`). -- :gh:`140`: system boot time (:func:`boot_time()`). + (:meth:`Process.gids`). +- :gh:`140`: system boot time (:func:`boot_time`). - :gh:`142`: per-process get and set niceness (priority) - (:meth:`Process.nice()`). -- :gh:`143`: per-process status (:meth:`Process.status()`). + (:meth:`Process.nice`). +- :gh:`143`: per-process status (:meth:`Process.status`). - :gh:`147` [Linux]: per-process I/O niceness / priority - (:meth:`Process.ionice()`). + (:meth:`Process.ionice`). - :gh:`148`: :class:`Popen` class which tidies up ``subprocess.Popen`` and :class:`Process` class in a single interface. -- :gh:`152`, [macOS]: :meth:`Process.open_files()` implementation has been +- :gh:`152`, [macOS]: :meth:`Process.open_files` implementation has been rewritten in C and no longer relies on ``lsof`` resulting in a 3x speedup. -- :gh:`153`, [macOS]: :meth:`Process.connections()` implementation has been +- :gh:`153`, [macOS]: :meth:`Process.connections` implementation has been rewritten in C and no longer relies on ``lsof`` resulting in a 3x speedup. **Bug fixes** -- :gh:`83`, [macOS]: :meth:`Process.cmdline()` is empty on macOS 64-bit. +- :gh:`83`, [macOS]: :meth:`Process.cmdline` is empty on macOS 64-bit. - :gh:`130`, [Linux]: a race condition can cause ``IOError`` exception be raised on if process disappears between ``open()`` and the subsequent ``read()`` call. - :gh:`145`, [Windows], **[critical]**: ``WindowsError`` was raised instead of - :exc:`AccessDenied` when using :meth:`Process.resume()` or - :meth:`Process.suspend()`. -- :gh:`146`, [Linux]: :meth:`Process.exe()` property can raise ``TypeError`` if + :exc:`AccessDenied` when using :meth:`Process.resume` or + :meth:`Process.suspend`. +- :gh:`146`, [Linux]: :meth:`Process.exe` property can raise ``TypeError`` if path contains NULL bytes. -- :gh:`151`, [Linux]: :meth:`Process.exe()` and :meth:`Process.cwd()` for PID 0 +- :gh:`151`, [Linux]: :meth:`Process.exe` and :meth:`Process.cwd` for PID 0 return inconsistent data. **API changes** @@ -2924,35 +2923,35 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`79`: per-process open files (:meth:`Process.open_files()`). +- :gh:`79`: per-process open files (:meth:`Process.open_files`). - :gh:`88`: total system physical cached memory. - :gh:`88`: total system physical memory buffers used by the kernel. -- :gh:`91`: add :meth:`Process.send_signal()` and :meth:`Process.terminate()` +- :gh:`91`: add :meth:`Process.send_signal` and :meth:`Process.terminate` methods. - :gh:`95`: :exc:`NoSuchProcess` and :exc:`AccessDenied` exception classes now provide ``pid``, ``name`` and ``msg`` attributes. -- :gh:`97`: per-process children (:meth:`Process.children()`). -- :gh:`98`: :meth:`Process.cpu_times()` and :meth:`Process.memory_info()` now +- :gh:`97`: per-process children (:meth:`Process.children`). +- :gh:`98`: :meth:`Process.cpu_times` and :meth:`Process.memory_info` now return a namedtuple instead of a tuple. - :gh:`103`: per-process opened TCP and UDP connections - (:meth:`Process.connections()`). + (:meth:`Process.connections`). - :gh:`107`, [Windows]: add support for Windows 64 bit. (patch by cjgohlke) -- :gh:`111`: per-process executable name (:meth:`Process.exe()`). -- :gh:`113`: exception messages now include :meth:`Process.name()` and +- :gh:`111`: per-process executable name (:meth:`Process.exe`). +- :gh:`113`: exception messages now include :meth:`Process.name` and :attr:`Process.pid`. -- :gh:`114`, [Windows]: :meth:`Process.username()` has been rewritten in pure C +- :gh:`114`, [Windows]: :meth:`Process.username` has been rewritten in pure C and no longer uses WMI resulting in a big speedup. Also, pywin32 is no longer required as a third-party dependency. (patch by wj32) - :gh:`117`, [Windows]: added support for Windows 2000. -- :gh:`123`: :func:`cpu_percent()` and :meth:`Process.cpu_percent()` accept a +- :gh:`123`: :func:`cpu_percent` and :meth:`Process.cpu_percent` accept a new ``interval`` parameter. -- :gh:`129`: per-process threads (:meth:`Process.threads()`). +- :gh:`129`: per-process threads (:meth:`Process.threads`). **Bug fixes** - :gh:`80`: fixed warnings when installing psutil with easy_install. - :gh:`81`, [Windows]: psutil fails to compile with Visual Studio. -- :gh:`94`: :meth:`Process.suspend()` raises ``OSError`` instead of +- :gh:`94`: :meth:`Process.suspend` raises ``OSError`` instead of :exc:`AccessDenied`. - :gh:`86`, [FreeBSD]: psutil didn't compile against FreeBSD 6.x. - :gh:`102`, [Windows]: orphaned process handles obtained by using @@ -2960,34 +2959,34 @@ cases accessing the old names will work but it will cause a instantiated. - :gh:`111`, [POSIX]: ``path`` and ``name`` :class:`Process` properties report truncated or erroneous values on POSIX. -- :gh:`120`, [macOS]: :func:`cpu_percent()` always returning 100%. +- :gh:`120`, [macOS]: :func:`cpu_percent` always returning 100%. - :gh:`112`: ``uid`` and ``gid`` properties don't change if process changes effective user/group id at some point. -- :gh:`126`: :meth:`Process.ppid()`, :meth:`Process.uids()`, - :meth:`Process.gids()`, - :meth:`Process.name()`, - :meth:`Process.exe()`, :meth:`Process.cmdline()` and - :meth:`Process.create_time()` properties are no longer cached and correctly +- :gh:`126`: :meth:`Process.ppid`, :meth:`Process.uids`, + :meth:`Process.gids`, + :meth:`Process.name`, + :meth:`Process.exe`, :meth:`Process.cmdline` and + :meth:`Process.create_time` properties are no longer cached and correctly raise :exc:`NoSuchProcess` exception if the process disappears. **API changes** - ``psutil.Process.path`` property is deprecated and works as an alias for ``psutil.Process.exe`` property. -- :meth:`Process.kill()`: signal argument was removed - to send a signal to the - process use :meth:`Process.send_signal()` method instead. -- :meth:`Process.memory_info()` returns a nametuple instead of a tuple. -- :func:`cpu_times()` returns a nametuple instead of a tuple. -- New :class:`Process` methods: :meth:`Process.open_files()`, - :meth:`Process.connections()`, - :meth:`Process.send_signal()` and :meth:`Process.terminate()`. -- :meth:`Process.ppid()`, :meth:`Process.uids()`, :meth:`Process.gids()`, - :meth:`Process.name()`, - :meth:`Process.exe()`, :meth:`Process.cmdline()` and - :meth:`Process.create_time()` properties are no longer cached and raise +- :meth:`Process.kill`: signal argument was removed - to send a signal to the + process use :meth:`Process.send_signal` method instead. +- :meth:`Process.memory_info` returns a nametuple instead of a tuple. +- :func:`cpu_times` returns a nametuple instead of a tuple. +- New :class:`Process` methods: :meth:`Process.open_files`, + :meth:`Process.connections`, + :meth:`Process.send_signal` and :meth:`Process.terminate`. +- :meth:`Process.ppid`, :meth:`Process.uids`, :meth:`Process.gids`, + :meth:`Process.name`, + :meth:`Process.exe`, :meth:`Process.cmdline` and + :meth:`Process.create_time` properties are no longer cached and raise :exc:`NoSuchProcess` exception if process disappears. -- :func:`cpu_percent()` no longer returns immediately (see issue 123). -- :meth:`Process.cpu_percent()` and :func:`cpu_percent()` no longer returns +- :func:`cpu_percent` no longer returns immediately (see issue 123). +- :meth:`Process.cpu_percent` and :func:`cpu_percent` no longer returns immediately by default (see issue :gh:`123`). 0.1.3 — 2010-03-02 @@ -2995,18 +2994,18 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`14`: :meth:`Process.username()`. +- :gh:`14`: :meth:`Process.username`. - :gh:`51`, [Linux], [Windows]: per-process current working directory - (:meth:`Process.cwd()`). -- :gh:`59`: :meth:`Process.is_running()` is now 10 times faster. + (:meth:`Process.cwd`). +- :gh:`59`: :meth:`Process.is_running` is now 10 times faster. - :gh:`61`, [FreeBSD]: added supoprt for FreeBSD 64 bit. -- :gh:`71`: per-process suspend and resume (:meth:`Process.suspend()` and - :meth:`Process.resume()`). +- :gh:`71`: per-process suspend and resume (:meth:`Process.suspend` and + :meth:`Process.resume`). - :gh:`75`: Python 3 support. **Bug fixes** -- :gh:`36`: :meth:`Process.cpu_times()` and :meth:`Process.memory_info()` +- :gh:`36`: :meth:`Process.cpu_times` and :meth:`Process.memory_info` functions succeeded. also for dead processes while a :exc:`NoSuchProcess` exception is supposed to be raised. - :gh:`48`, [FreeBSD]: incorrect size for MIB array defined in ``getcmdargs``. @@ -3016,26 +3015,26 @@ cases accessing the old names will work but it will cause a - :gh:`55`, [Windows]: ``test_pid_4`` was failing on Windows Vista. - :gh:`57`: some unit tests were failing on systems where no swap memory is available. -- :gh:`58`: :meth:`Process.is_running()` is now called before - :meth:`Process.kill()` to make sure we are going to kill the correct process. +- :gh:`58`: :meth:`Process.is_running` is now called before + :meth:`Process.kill` to make sure we are going to kill the correct process. - :gh:`73`, [macOS]: virtual memory size reported on includes shared library size. -- :gh:`77`: :exc:`NoSuchProcess` wasn't raised on :meth:`Process.create_time()` +- :gh:`77`: :exc:`NoSuchProcess` wasn't raised on :meth:`Process.create_time` if - :meth:`Process.kill()` was used first. + :meth:`Process.kill` was used first. 0.1.2 — 2009-05-06 ^^^^^^^^^^^^^^^^^^ **Enhancements** -- :gh:`32`: Per-process CPU user/kernel times (:meth:`Process.cpu_times()`). -- :gh:`33`: Per-process create time (:meth:`Process.create_time()`). +- :gh:`32`: Per-process CPU user/kernel times (:meth:`Process.cpu_times`). +- :gh:`33`: Per-process create time (:meth:`Process.create_time`). - :gh:`34`: Per-process CPU utilization percentage - (:meth:`Process.cpu_percent()`). -- :gh:`38`: Per-process memory usage (bytes) (:meth:`Process.memory_info()`). -- :gh:`41`: Per-process memory percent (:meth:`Process.memory_percent()`). -- :gh:`39`: System uptime (:func:`boot_time()`). + (:meth:`Process.cpu_percent`). +- :gh:`38`: Per-process memory usage (bytes) (:meth:`Process.memory_info`). +- :gh:`41`: Per-process memory percent (:meth:`Process.memory_percent`). +- :gh:`39`: System uptime (:func:`boot_time`). - :gh:`43`: Total system virtual memory. - :gh:`46`: Total system physical memory. - :gh:`44`: Total system used/free virtual and physical memory. @@ -3045,7 +3044,7 @@ cases accessing the old names will work but it will cause a - :gh:`36`, [Windows]: :exc:`NoSuchProcess` not raised when accessing timing methods. - :gh:`40`, [FreeBSD], [macOS]: fix ``test_get_cpu_times`` failures. -- :gh:`42`, [Windows]: :meth:`Process.memory_percent()` raises +- :gh:`42`, [Windows]: :meth:`Process.memory_percent` raises :exc:`AccessDenied`. 0.1.1 — 2009-03-06 @@ -3056,17 +3055,17 @@ cases accessing the old names will work but it will cause a - :gh:`4`, [FreeBSD]: support for all functions of psutil. - :gh:`9`, [macOS], [Windows]: add ``Process.uid`` and ``Process.gid``, returning process UID and GID. -- :gh:`11`: per-process parent object: :meth:`Process.parent()` property +- :gh:`11`: per-process parent object: :meth:`Process.parent` property returns a :class:`Process` object representing the parent process, and - :meth:`Process.ppid()` returns the parent PID. + :meth:`Process.ppid` returns the parent PID. - :gh:`12`, :gh:`15`: :exc:`NoSuchProcess` exception now raised when creating an object for a nonexistent process, or when retrieving information about a process that has gone away. - :gh:`21`, [Windows]: :exc:`AccessDenied` exception created for raising access denied errors from ``OSError`` or ``WindowsError`` on individual platforms. -- :gh:`26`: :func:`process_iter()` function to iterate over processes as +- :gh:`26`: :func:`process_iter` function to iterate over processes as :class:`Process` objects with a generator. - :class:`Process` objects can now also be compared with == operator for equality (PID, name, command line are compared). @@ -3077,11 +3076,11 @@ cases accessing the old names will work but it will cause a otherwise would return an "invalid parameter" exception. - :gh:`17`: get_process_list() ignores :exc:`NoSuchProcess` and :exc:`AccessDenied` exceptions during building of the list. -- :gh:`22`, [Windows]: :meth:`Process.kill()` for PID 0 was failing with an +- :gh:`22`, [Windows]: :meth:`Process.kill` for PID 0 was failing with an unset exception. -- :gh:`23`, [Linux], [macOS]: create special case for :func:`pid_exists()` with +- :gh:`23`, [Linux], [macOS]: create special case for :func:`pid_exists` with PID 0. -- :gh:`24`, [Windows], **[critical]**: :meth:`Process.kill()` for PID 0 now +- :gh:`24`, [Windows], **[critical]**: :meth:`Process.kill` for PID 0 now raises :exc:`AccessDenied` exception instead of ``WindowsError``. - :gh:`30`: psutil.get_pid_list() was returning two 0 PIDs. diff --git a/docs/credits.rst b/docs/credits.rst index 8b350f7d6a..7ccc0ae5ef 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -1,5 +1,4 @@ .. currentmodule:: psutil -.. include:: _links.rst Credits ======= @@ -156,7 +155,7 @@ Code contributors by year * `Amir Rossert`_ - :gh:`2156`, :gh:`2345` * `Bernhard Urban-Forster`_ - :gh:`2135` -* `Chris Lalancette`_ - :gh:`2037` (:func:`net_if_stats()` flags arg on POSIX) +* `Chris Lalancette`_ - :gh:`2037` (:func:`net_if_stats` flags arg on POSIX) * `Daniel Li`_ - :gh:`2150` * `Daniel Widdis`_ - :gh:`2077`, :gh:`2160` * `Garrison Carter`_ - :gh:`2096` @@ -188,7 +187,7 @@ Code contributors by year ~~~~ * `Anselm Kruis`_ - :gh:`1695` -* `Armin Gruner`_ - :gh:`1800` (:meth:`Process.environ()` on BSD) +* `Armin Gruner`_ - :gh:`1800` (:meth:`Process.environ` on BSD) * `Chris Burger`_ - :gh:`1830` * `vser1`_ - :gh:`1637` * `Grzegorz Bokota`_ - :gh:`1758`, :gh:`1762` @@ -208,7 +207,7 @@ Code contributors by year * `qcha0`_ - :gh:`1491` * `Alex Manuskin`_ - :gh:`1487` -* `Ammar Askar`_ - :gh:`1485` (:func:`getloadavg()` on Windows) +* `Ammar Askar`_ - :gh:`1485` (:func:`getloadavg` on Windows) * `Arnon Yaari`_ - :gh:`607`, :gh:`1349`, :gh:`1409`, :gh:`1500`, :gh:`1505`, :gh:`1507`, :gh:`1533` * `Athos Ribeiro`_ - :gh:`1585` * `Benjamin Drung`_ - :gh:`1462` @@ -219,7 +218,7 @@ Code contributors by year * `EccoTheFlintstone`_ - :gh:`1368`, :gh:`1348` * `Erwan Le Pape`_ - :gh:`1570` * `Ghislain Le Meur`_ - :gh:`1379` -* `Kamil Rytarowski`_ - :gh:`1526`, :gh:`1530` (:meth:`Process.cwd()` for NetBSD) +* `Kamil Rytarowski`_ - :gh:`1526`, :gh:`1530` (:meth:`Process.cwd` for NetBSD) * `Nathan Houghton`_ - :gh:`1619` * `Samer Masterson`_ - :gh:`1480` * `Xiaoling Bao`_ - :gh:`1223` @@ -228,7 +227,7 @@ Code contributors by year 2018 ~~~~ -* `Alex Manuskin`_ - :gh:`1284`, :gh:`1345`, :gh:`1350`, :gh:`1369` (:func:`sensors_temperatures()` for macOS, FreeBSD, Linux) +* `Alex Manuskin`_ - :gh:`1284`, :gh:`1345`, :gh:`1350`, :gh:`1369` (:func:`sensors_temperatures` for macOS, FreeBSD, Linux) * `Arnon Yaari`_ - :gh:`1214` * `Dan Vinakovsky`_ - :gh:`1216` * `Denis Krienbühl`_ - :gh:`1260` @@ -342,7 +341,7 @@ Code contributors by year 2011 ~~~~ -* Jeremy Whitlock - :gh:`125`, :gh:`150`, :gh:`206`, :gh:`217`, :gh:`260` (:func:`net_io_counters()` and :func:`disk_io_counters()` on macOS) +* Jeremy Whitlock - :gh:`125`, :gh:`150`, :gh:`206`, :gh:`217`, :gh:`260` (:func:`net_io_counters` and :func:`disk_io_counters` on macOS) 2010 ~~~~ @@ -353,7 +352,7 @@ Code contributors by year 2009 ~~~~ -* Yan Raber: `c861c08b `_ (Windows :func:`cpu_times()`), `15159111 `_ (Windows :meth:`Process.username()`) +* Yan Raber: `c861c08b `_ (Windows :func:`cpu_times`), `15159111 `_ (Windows :meth:`Process.username`) * `Jay Loden`_ - `79128baa `_ (first commit of FreeBSD implementation) 2008 @@ -369,7 +368,6 @@ Code contributors by year .. _`Alex Laird`: https://github.com/alexdlaird .. _`Alexey Vazhnov`: https://opencollective.com/alexey-vazhnov -.. _`Jakob P. Liljenberg`: https://github.com/aristocratos .. _`Artyom Vancyan`: https://github.com/ArtyomVancyan .. _`c0m4r`: https://github.com/c0m4r .. _`Chenyoo Hao`: https://opencollective.com/chenyoo-hao diff --git a/docs/devguide.rst b/docs/devguide.rst index 5b39d41ec7..5d92c4b816 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -167,7 +167,7 @@ Documentation .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS -.. _`Git for Windows`: ` +.. _`Git for Windows`: https://git-scm.com/install/windows .. _`changelog.rst`: https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile .. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ diff --git a/docs/index.rst b/docs/index.rst index ba530616e5..6e2dde310f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,6 @@ .. module:: psutil :synopsis: psutil module -.. moduleauthor:: Giampaolo Rodola' -.. include:: _links.rst -.. _availability: +.. moduleauthor:: Giampaolo Rodola Psutil documentation ==================== @@ -111,3 +109,5 @@ Table of Contents :titlesonly: Changelog + +.. _`Tidelift security contact`: https://tidelift.com/security diff --git a/docs/recipes.rst b/docs/recipes.rst index 9c10e982e3..6373cf05a9 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -1,5 +1,3 @@ -.. include:: _links.rst - Recipes ======= @@ -34,8 +32,8 @@ Find process by name: ---- -A bit more advanced, check string against :meth:`Process.name()`, -:meth:`Process.exe()` and :meth:`Process.cmdline()`: +A bit more advanced, check string against :meth:`Process.name`, +:meth:`Process.exe` and :meth:`Process.cmdline`: .. code-block:: python diff --git a/pyproject.toml b/pyproject.toml index 9a63118834..553bcc07ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,12 +198,12 @@ ignore_decorators = [ ] [tool.rstcheck] +ignore_directives = ["availability"] +ignore_roles = ["gh"] ignore_messages = [ - "Duplicate explicit target name", + "Document or section may not begin with a transition", "Duplicate implicit target name", - "Duplicate name \"development guide\" for external target", "Hyperlink target \".*?\" is not referenced", - "Target name overrides implicit target name \"development guide\"", ] [tool.tomlsort] From bbb377d0a99d090b6efd7fdee7b8d6e7091820a6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 16 Mar 2026 01:00:09 +0100 Subject: [PATCH 1596/1714] Replace rstcheck with sphinx-lint for RST linting (#2767) ...+ add a custom script to detect dead reference links in ``.rst`` files --- MANIFEST.in | 1 + Makefile | 18 ++-- docs/api.rst | 7 +- docs/changelog.rst | 6 +- docs/credits.rst | 35 -------- docs/devguide.rst | 10 +-- docs/index.rst | 4 +- pyproject.toml | 9 -- scripts/internal/git_pre_commit.py | 13 ++- scripts/internal/rst_check_dead_refs.py | 105 ++++++++++++++++++++++++ setup.py | 22 +++-- 11 files changed, 148 insertions(+), 82 deletions(-) create mode 100755 scripts/internal/rst_check_dead_refs.py diff --git a/MANIFEST.in b/MANIFEST.in index eb4eb1033d..20c912b006 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -177,6 +177,7 @@ include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_sysinfo.py include scripts/internal/purge_installation.py +include scripts/internal/rst_check_dead_refs.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py diff --git a/Makefile b/Makefile index 393fe6d527..b114a2d56e 100644 --- a/Makefile +++ b/Makefile @@ -82,14 +82,15 @@ install-pydeps-test: ## Install python deps necessary to run unit tests. PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) setuptools PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) .[test] -install-pydeps-dev: ## Install python deps meant for local development. - $(MAKE) install-git-hooks +install-pydeps-lint: ## Install python deps necessary to run linters. $(MAKE) install-pip - $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) .[dev] + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) setuptools + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) .[lint] -install-git-hooks: ## Install GIT pre-commit hook. - ln -sf ../../scripts/internal/git_pre_commit.py .git/hooks/pre-commit - chmod +x .git/hooks/pre-commit +install-pydeps-dev: ## Install python deps meant for local development. + $(MAKE) install-pip + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) setuptools + PIP_BREAK_SYSTEM_PACKAGES=1 $(PYTHON) -m pip install $(PIP_INSTALL_ARGS) .[dev] # =================================================================== # Tests @@ -180,7 +181,8 @@ dprint: @$(DPRINT) check lint-rst: ## Run linter for .rst files. - @git ls-files '*.rst' | xargs rstcheck --config=pyproject.toml --log-level ERROR + @git ls-files '*.rst' | xargs python3 scripts/internal/rst_check_dead_refs.py + @git ls-files '*.rst' | xargs sphinx-lint lint-toml: ## Run linter for pyproject.toml. @git ls-files '*.toml' | xargs toml-sort --check @@ -231,7 +233,7 @@ fix-all: ## Run all code fixers. # =================================================================== ci-lint: ## Run all linters on GitHub CI. - $(PYTHON) -m pip install -U black ruff rstcheck toml-sort sphinx + $(MAKE) install-pydeps-lint curl -fsSL https://dprint.dev/install.sh | sh $(DPRINT) --version clang-format --version diff --git a/docs/api.rst b/docs/api.rst index 6309374f05..dac9142195 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2865,7 +2865,8 @@ Other constants `here `_ for more info). It must be noted that this trick works only for APIs which rely on /proc - filesystem (e.g. `memory`_ APIs and most :class:`Process` class methods). + filesystem (e.g. memory-related APIs and many (but not all) + :class:`Process` class methods). .. availability:: Linux, SunOS, AIX @@ -2889,8 +2890,6 @@ Other constants .. _`BPO-10784`: https://bugs.python.org/issue10784 .. _`BPO-12442`: https://bugs.python.org/issue12442 .. _`BPO-6973`: https://bugs.python.org/issue6973 -.. _`CPU affinity`: https://www.linuxjournal.com/article/6799?page=0,0 -.. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/ .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get .. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt .. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html @@ -2930,7 +2929,6 @@ Other constants .. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET .. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM .. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd -.. _`subprocess.Popen.wait`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait .. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen .. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident .. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread @@ -2954,7 +2952,6 @@ Other constants .. === Windows API -.. _`GetDriveType`: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea .. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess .. _`GetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass .. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex diff --git a/docs/changelog.rst b/docs/changelog.rst index 0308ca307c..e3bd9fa3be 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,8 +10,8 @@ Changelog Doc: -- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`. - Tons of doc improvements. In order of importance: +- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, + :gh:`2767`. Tons of doc improvements. In order of importance: - Split the documentation from a single-page HTML document into multiple sections. Sections now include separate pages for API reference, @@ -32,6 +32,8 @@ Doc: - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. - Use sphinx extension to validate Python code snippets syntax at build-time. + - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting, and add a custom + script to detect dead reference links in ``.rst`` files. Type hints / enums: diff --git a/docs/credits.rst b/docs/credits.rst index 7ccc0ae5ef..dc0c7b2a17 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -381,8 +381,6 @@ Code contributors by year .. _`Johannes Maron`: https://github.com/codingjoe .. _`Karthik Kumar`: https://github.com/guilt .. _`Maximilian Wu`: https://github.com/maxesisn -.. _`Mehver`: https://github.com/Mehver -.. _`mirko`: https://github.com/mirbyte .. _`PySimpleGUI`: https://github.com/PySimpleGUI .. _`roboflow.com`: https://github.com/roboflow .. _`sansec.io`: https://github.com/sansecio @@ -409,7 +407,6 @@ Code contributors by year .. Code contributors .. ============================================================================ -.. _`Aaron Shaw`: https://github.com/shawaj .. _`Adrian Page`: https://github.com/adpag .. _`qcha0`: https://github.com/qcha0 .. _`Akos Kiss`: https://github.com/akosthekiss @@ -422,8 +419,6 @@ Code contributors by year .. _`Anders Chrigström`: https://github.com/anders-chrigstrom .. _`Andre Caron`: https://github.com/AndreLouisCaron .. _`Anselm Kruis`: https://github.com/akruis -.. _`Anthony Ramine`: https://github.com/nox -.. _`Anthony Ryan`: https://github.com/anthonyryan1 .. _`Antoine Pitrou`: https://github.com/pitrou .. _`Arcadiy Ivanov`: https://github.com/arcivanov .. _`Armin Gruner`: https://github.com/ArminGruner @@ -436,26 +431,21 @@ Code contributors by year .. _`Bernhard Urban-Forster`: https://github.com/lewurm .. _`Bernát Gábor`: https://github.com/gaborbernat .. _`Bruno Binet`: https://github.com/bbinet -.. _`Caleb Bassi`: https://github.com/calebjbassi .. _`Cedric Lamoriniere`: https://github.com/clamoriniere .. _`Chris Burger`: https://github.com/phobozad .. _`Chris Lalancette`: https://github.com/clalancette .. _`Cristian Vîjdea`: https://github.com/cvijdea -.. _`cui fliter`: https://github.com/cuishuang .. _`Dan Vinakovsky`: https://github.com/hexaclock .. _`Danek Duvall`: https://github.com/dhduvall .. _`Daniel Beer`: https://github.com/dbeer1 .. _`Daniel Li`: https://github.com/li-dan -.. _`Daniel Lockyer`: https://github.com/daniellockyer .. _`Daniel Widdis`: https://github.com/dbwiddis .. _`dasumin`: https://github.com/dasumin .. _`David Brochart`: https://github.com/davidbrochart .. _`David Knaack`: https://github.com/davidkna .. _`Denis Krienbühl`: https://github.com/href -.. _`desbma`: https://github.com/desbma .. _`EccoTheFlintstone`: https://github.com/EccoTheFlintstone .. _`Eli Wenig`: https://github.com/elisw93 -.. _`Emmanuel Ferdman`: https://github.com/emmanuelferdman .. _`Erwan Le Pape`: https://github.com/erwan-le-pape .. _`ewedlund`: https://github.com/ewedlund .. _`Fabian Groffen`: https://github.com/fabian @@ -466,20 +456,16 @@ Code contributors by year .. _`Frank Benkstein`: https://github.com/fbenkstein .. _`Gabi Davar`: https://github.com/mindw .. _`Garrison Carter`: https://github.com/garrisoncarter -.. _`Georg Sauthoff`: https://github.com/gsauthof .. _`Ghislain Le Meur`: https://github.com/gigi206 .. _`Giampaolo Rodola`: https://github.com/giampaolo .. _`Gleb Smirnoff`: https://github.com/glebius -.. _`Gregory P. Smith`: https://github.com/gpshead .. _`Gregory Szorc`: https://github.com/indygreg .. _`Grzegorz Bokota`: https://github.com/Czaki .. _`Guido Imperiale`: https://github.com/crusaderky .. _`Guillermo`: https://github.com/guille -.. _`Hang`: https://github.com/bebound .. _`Himanshu Shekhar`: https://github.com/himanshub16 .. _`Hiroyuki Tanaka`: https://github.com/myheroyuki .. _`Hugo van Kemenade`: https://github.com/hugovk -.. _`iam-py-test`: https://github.com/iam-py-test .. _`Ilya Georgievsky`: https://github.com/xBeAsTx .. _`Ilya Yanok`: https://github.com/yanok .. _`Irene Sheen`: https://github.com/ceda-ei @@ -495,26 +481,16 @@ Code contributors by year .. _`Jeff Tang`: https://github.com/mrjefftang .. _`Jeremy Humble`: https://github.com/jhumble .. _`John Burnett`: https://github.com/johnburnett -.. _`Jon Dufresne`: https://github.com/jdufresne .. _`Jonathan Kohler`: https://github.com/kohlerjl .. _`Josiah Carlson`: https://github.com/josiahcarlson .. _`Julien Lebot`: https://github.com/julien-lebot -.. _`Julien Stephan`: https://github.com/justeph -.. _`Jérome Perrin`: https://github.com/perrinjerome -.. _`Jérôme Duval`: https://github.com/korli -.. _`Kale Kundert`: https://github.com/kalekundert .. _`Kamil Rytarowski`: https://github.com/krytarowski .. _`karthik`: https://github.com/karthikrev -.. _`Karthikeyan Singaravelan`: https://github.com/tirkarthi -.. _`Kian-Meng Ang`: https://github.com/kianmeng .. _`Koen Kooi`: https://github.com/koenkooi -.. _`kuzmich321`: https://github.com/kuzmich321 .. _`Landry Breuil`: https://github.com/landryb .. _`Lawrence D'Anna`: https://github.com/smoofra .. _`Lawrence Ye`: https://github.com/LEAFERx -.. _`lgc2333`: https://github.com/lgc2333 .. _`Lysandros Nikolaou`: https://github.com/lysnikolaou -.. _`maozguttman`: https://github.com/maozguttman .. _`Marc Abramowitz`: https://github.com/msabramo .. _`Marcel Telka`: https://github.com/mtelka .. _`Mark Derbecker`: https://github.com/mpderbec @@ -527,10 +503,8 @@ Code contributors by year .. _`Maxime Mouial`: https://github.com/hush-hush .. _`Mayank Jha`: https://github.com/maynk27 .. _`Michał Górny`: https://github.com/mgorny -.. _`Mickaël Schoentgen`: https://github.com/BoboTiG .. _`Mike Hommey`: https://github.com/glandium .. _`Mike Sarahan`: https://github.com/msarahan -.. _`Miro Hrončok`: https://github.com/hroncok .. _`Nathan Houghton`: https://github.com/n1000 .. _`Nicolas Hennion`: https://github.com/nicolargo .. _`Nikhil Marathe`: https://github.com/nikhilm @@ -541,12 +515,9 @@ Code contributors by year .. _`Olivier Dormond`: https://github.com/odormond .. _`Pablo Baeyens`: https://github.com/mx-psi .. _`Patrick Welche`: https://github.com/prlw1 -.. _`Peter Jeremy`: https://github.com/peterjeremy .. _`PetrPospisil`: https://github.com/PetrPospisil .. _`Pierre Fersing`: https://github.com/PierreF .. _`Po-Chuan Hsieh`: https://github.com/sunpoet -.. _`Prodesire`: https://github.com/Prodesire -.. _`ReenigneArcher`: https://github.com/ReenigneArcher .. _`Riccardo Schirone`: https://github.com/ret2libc .. _`Ryan Carsten Schmidt`: https://github.com/ryandesign .. _`Ryo Onodera`: https://github.com/ryoon @@ -557,7 +528,6 @@ Code contributors by year .. _`Sebastian-Gabriel Brestin`: https://github.com/bsebi .. _`Sergey Fedorov`: https://github.com/barracuda156 .. _`Shade Gladden`: https://github.com/shadeyg56 -.. _`Shannon Pamperl`: https://github.com/shanman190 .. _`sk6249`: https://github.com/sk6249 .. _`spacewander`: https://github.com/spacewander .. _`Steve Dower`: https://github.com/zooba @@ -568,16 +538,12 @@ Code contributors by year .. _`Syohei YOSHIDA`: https://github.com/syohex .. _`Thiago Borges Abdnur`: https://github.com/bolaum .. _`Thomas Klausner`: https://github.com/tklauser -.. _`Tim Gates`: https://github.com/timgates42 .. _`Tim Schlueter`: https://github.com/modelrockettier .. _`Timmy Konick`: https://github.com/tijko -.. _`Tomáš Chvátal`: https://github.com/scarabeusiv -.. _`Torsten Blum`: https://github.com/Torsten-B .. _`Vincent A. Arcila`: https://github.com/jandrovins .. _`Wen Jia Liu (wj32)`: https://github.com/wj32 .. _`Wilfried Goesgens`: https://github.com/dothebart .. _`Will Hawes`: https://github.com/wdh -.. _`wxwright`: https://github.com/wxwright .. _`Xianpeng Shen`: https://github.com/shenxianpeng .. _`Xiaoling Bao`: https://github.com/xiaolingbao .. _`Xuehai Pan`: https://github.com/XuehaiPan @@ -586,4 +552,3 @@ Code contributors by year .. _`Yaolong Huang`: http://airekans.github.io/ .. _`Árni Már Jónsson`: https://github.com/arnimarj .. _`vser1`: https://github.com/vser1 -.. _`David Daeschler`: https://github.com/ddaeschler diff --git a/docs/devguide.rst b/docs/devguide.rst index 5d92c4b816..0a8c1237a4 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -24,7 +24,7 @@ Build, setup and running tests .. code-block:: bash make clean # remove build files - make install-pydeps-dev # install dev deps (ruff, black, ...) + make install-pydeps-dev # install all development deps (ruff, black, coverage, ...) make test # run tests make test-parallel # run tests in parallel (faster) make test-memleaks # run memory leak tests @@ -92,9 +92,6 @@ Coding style All style and formatting checks are automatically enforced both **locally on each `git commit`** and **remotely via a GitHub Actions pipeline**. -- A **Git commit hook**, installed with `make install-git-hooks`, runs all - formatters and linters before each commit. The commit is rejected if any - check fails. - **Python** code follows the `PEP-8`_ style guide. We use `black` and `ruff` for formatting and linting. - **C** code generally follows the `PEP-7`_ style guide, with formatting @@ -129,7 +126,7 @@ Typically, this is what you do: - Write the platform specific implementation in ``psutil/_ps{platform}.py`` (e.g. `psutil/_pslinux.py`_). - If the change requires C code, write the C implementation in - ``psutil/arch/{platform}/file.c`` (e.g. `psutil/arch/linux/cpu.c`). + ``psutil/arch/{platform}/file.c`` (e.g. `psutil/arch/linux/disk.c`_). - Write a generic test in `tests/test_system.py`_ or `tests/test_process.py`_. - If possible, write a platform-specific test in @@ -174,8 +171,7 @@ Documentation .. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ .. _`psutil/__init__.py`: https://github.com/giampaolo/psutil/blob/master/psutil/__init__.py .. _`psutil/_pslinux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_pslinux.py -.. _`psutil/_psutil_linux.c`: https://github.com/giampaolo/psutil/blob/master/psutil/_psutil_linux.c +.. _`psutil/arch/linux/disk.c`: https://github.com/giampaolo/psutil/blob/master/psutil/arch/linux/disk.c .. _`tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_linux.py .. _`tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_process.py .. _`tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_system.py -.. _`winmake.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/winmake.py diff --git a/docs/index.rst b/docs/index.rst index 6e2dde310f..a1d2861540 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -87,8 +87,8 @@ Sponsors can have their logo displayed here and in the psutil `documentation `_ and NOT `text`__ +RE_REF = re.compile(r'`([^`<\n]+)`_(?!_)') + + +def line_number(text, pos): + return text.count('\n', 0, pos) + 1 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("files", nargs="+", metavar="FILE") + args = parser.parse_args() + + # Pass 1: collect all defined targets across all files. + all_targets = set() # lower-case names + # URL-only targets: lower-case name -> (original name, file) + url_targets = {} + + for path in args.files: + with open(path) as f: + text = f.read() + for m in RE_ANY_TARGET.finditer(text): # noqa: FURB142 + all_targets.add(m.group(1).strip().lower()) + for m in RE_URL_TARGET.finditer(text): + name = m.group(1).strip() + url_targets[name.lower()] = ( + name, + path, + line_number(text, m.start()), + ) + + # Pass 2: collect all backtick references (with file + line). + all_refs = [] # list of (lower-case name, original, file, lineno) + referenced = set() # lower-case names of all referenced targets + + for path in args.files: + with open(path) as f: + text = f.read() + for m in RE_REF.finditer(text): + name = m.group(1).strip() + all_refs.append(( + name.lower(), + name, + path, + line_number(text, m.start()), + )) + referenced.add(name.lower()) + + errors = [] + + # Check 1: URL targets that are never referenced. + for key, (name, path, lineno) in url_targets.items(): + if key not in referenced: + errors.append( + (path, lineno, f"unreferenced hyperlink target: {name!r}") + ) + + # Check 2: backtick references with no matching target. + for key, name, path, lineno in all_refs: + if key not in all_targets: + errors.append((path, lineno, f"unknown target name: {name!r}")) + + errors.sort() + for path, lineno, msg in errors: + print(f"{path}:{lineno}: {msg}") + + if errors: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 37a8755e5a..05185b3260 100755 --- a/setup.py +++ b/setup.py @@ -73,24 +73,31 @@ 'wmi ; os_name == "nt" and implementation_name != "pypy"', ] +# Linter deps, installable via `pip install .[lint]` or +# `make install-pydeps-lint`. +LINT_DEPS = [ + "black", + "ruff", + "sphinx-lint", + "toml-sort", +] + # Development deps, installable via `pip install .[dev]` or # `make install-pydeps-dev`. -DEV_DEPS = TEST_DEPS + [ +DEV_DEPS = [ + *TEST_DEPS, + *LINT_DEPS, "abi3audit", - "black", "check-manifest", "coverage", "packaging", - "pylint", + "pylint", # not enforced "pyperf", "pypinfo", "pytest-cov", "requests", - "rstcheck", - "ruff", "sphinx", "sphinx_rtd_theme", - "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", @@ -478,8 +485,9 @@ def main(): ) if setuptools is not None: extras_require = { - "dev": DEV_DEPS, "test": TEST_DEPS, + "lint": LINT_DEPS, + "dev": DEV_DEPS, } kwargs.update( python_requires=">=3.7", From 9c3df5c128ffdd3d9d790bf4c1ea0342ddd097a9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 16 Mar 2026 02:18:59 +0100 Subject: [PATCH 1597/1714] BUild doc as part of CI + fix some broken links --- .github/workflows/build.yml | 26 ++++++++++++-------------- docs/api.rst | 4 ++-- docs/credits.rst | 8 ++++---- docs/devguide.rst | 6 +++--- docs/platform.rst | 2 -- docs/timeline.rst | 1 - 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e952fcd65..7547a4a4ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,8 +54,8 @@ jobs: name: wheels-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse - # Run linters. - linters: + # Run linters and build doc. + linters-and-doc: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -65,28 +65,26 @@ jobs: - name: "Run linters" run: | make ci-lint + - name: "Build doc" + run: | + pip install -r docs/requirements.txt + cd docs && make html - # Produce wheels as artifacts. - upload-wheels: + # Merge wheels and check sanity of the package distribution. + merge-and-check-dist: needs: [tests] runs-on: ubuntu-latest steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: 3.x - uses: actions/upload-artifact/merge@v4 with: name: wheels pattern: wheels-* separate-directories: false delete-merged: true - - # Check sanity of the package distribution (.tar.gz and *.whl files) - check-dist: - needs: [upload-wheels] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: 3.x - uses: actions/download-artifact@v4 with: name: wheels diff --git a/docs/api.rst b/docs/api.rst index dac9142195..7e32d3ca1c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2922,8 +2922,8 @@ Other constants .. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit .. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue .. _`select.poll`: https://docs.python.org//library/select.html#select.poll -.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set. -.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage. +.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set +.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage .. _`signal module`: https://docs.python.org//library/signal.html .. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM .. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET diff --git a/docs/credits.rst b/docs/credits.rst index dc0c7b2a17..60a90e9b40 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -287,7 +287,7 @@ Code contributors by year * `Arnon Yaari`_ - :gh:`680`, :gh:`679`, :gh:`610` * `Bruno Binet`_ - :gh:`572` -* `dasumin`_ - :gh:`541` +* `Denis`_ - :gh:`541` * `Fabian Groffen`_ - :gh:`611`, :gh:`618` * `Gabi Davar`_ - :gh:`578`, :gh:`581`, :gh:`587` * `Jeff Tang`_ - :gh:`616`, :gh:`648`, :gh:`653`, :gh:`654` @@ -434,13 +434,13 @@ Code contributors by year .. _`Cedric Lamoriniere`: https://github.com/clamoriniere .. _`Chris Burger`: https://github.com/phobozad .. _`Chris Lalancette`: https://github.com/clalancette -.. _`Cristian Vîjdea`: https://github.com/cvijdea +.. _`Cristian Vîjdea`: https://github.com/cvijdea-bd .. _`Dan Vinakovsky`: https://github.com/hexaclock .. _`Danek Duvall`: https://github.com/dhduvall .. _`Daniel Beer`: https://github.com/dbeer1 .. _`Daniel Li`: https://github.com/li-dan .. _`Daniel Widdis`: https://github.com/dbwiddis -.. _`dasumin`: https://github.com/dasumin +.. _`Denis`: https://github.com/denis-sumin .. _`David Brochart`: https://github.com/davidbrochart .. _`David Knaack`: https://github.com/davidkna .. _`Denis Krienbühl`: https://github.com/href @@ -469,7 +469,7 @@ Code contributors by year .. _`Ilya Georgievsky`: https://github.com/xBeAsTx .. _`Ilya Yanok`: https://github.com/yanok .. _`Irene Sheen`: https://github.com/ceda-ei -.. _`Isaac K. Ko`: https://github.com/kyet +.. _`Isaac K. Ko`: https://github.com/1saac-k .. _`Jaime Fullaondo`: https://github.com/truthbk .. _`Jake Omann`: https://github.com/jomann09 .. _`Jakob P. Liljenberg`: https://github.com/aristocratos diff --git a/docs/devguide.rst b/docs/devguide.rst index 0a8c1237a4..74186171e8 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -134,7 +134,7 @@ Typically, this is what you do: This usually means testing the return value of the new API against a system CLI tool. - Update the doc in ``docs/api.rst``. -- Update `changelog.rst`_ and `CREDITS`_ files. +- Update `changelog.rst`_ and `credits.rst`_ files. - Make a pull request. Make a pull request @@ -162,10 +162,10 @@ Documentation - doc can be built with ``make install-pydeps-dev; cd docs; make html``. - public doc is hosted at https://psutil.readthedocs.io. +.. _`changelog.rst`: https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md -.. _`CREDITS`: https://github.com/giampaolo/psutil/blob/master/CREDITS +.. _`credits.rst`: https://github.com/giampaolo/psutil/blob/master/docs/credits.rst .. _`Git for Windows`: https://git-scm.com/install/windows -.. _`changelog.rst`: https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile .. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ .. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ diff --git a/docs/platform.rst b/docs/platform.rst index f18abba6ce..c280f79847 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -9,8 +9,6 @@ Python **Python 2.7**: latest psutil version supporting it is `psutil 6.1.1 `_ (Dec 2024). The 6.1.X series may still receive critical bug-fixes but no new features. -It will be maintained in the dedicated -`python2 `_ branch. To install psutil on Python 2.7 run: :: diff --git a/docs/timeline.rst b/docs/timeline.rst index 54159a719d..7d0c0410b8 100644 --- a/docs/timeline.rst +++ b/docs/timeline.rst @@ -411,5 +411,4 @@ Timeline `diff `_ - 2009-01-27: `0.1.0 `_ - - `what's new `_ - `diff `_ From dc9fde8691218f6cb69de6f7eef161c222a18ef3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 16 Mar 2026 02:59:17 +0100 Subject: [PATCH 1598/1714] Doc: use .. code-block:: pycon directive --- docs/api.rst | 706 ++++++++++++++++++++++++++++----------------------- 1 file changed, 394 insertions(+), 312 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 7e32d3ca1c..1ba449990b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -52,9 +52,11 @@ CPU The order of the list is consistent across calls. Example output on Linux: - >>> import psutil - >>> psutil.cpu_times() - scputimes(user=17411.7, system=3797.02, idle=51266.57, nice=77.99, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) + .. code-block:: pycon + + >>> import psutil + >>> psutil.cpu_times() + scputimes(user=17411.7, system=3797.02, idle=51266.57, nice=77.99, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. @@ -94,17 +96,19 @@ CPU called from different threads, at different intervals, and still return meaningful and independent results. - >>> import psutil - >>> # blocking - >>> psutil.cpu_percent(interval=1) - 2.0 - >>> # non-blocking (percentage since last call) - >>> psutil.cpu_percent(interval=None) - 2.9 - >>> # blocking, per-cpu - >>> psutil.cpu_percent(interval=1, percpu=True) - [2.0, 1.0] - >>> + .. code-block:: pycon + + >>> import psutil + >>> # blocking + >>> psutil.cpu_percent(interval=1) + 2.0 + >>> # non-blocking (percentage since last call) + >>> psutil.cpu_percent(interval=None) + 2.9 + >>> # blocking, per-cpu + >>> psutil.cpu_percent(interval=1, percpu=True) + [2.0, 1.0] + >>> .. warning:: the first time this function is called with *interval* = ``0.0`` or ``None`` @@ -150,11 +154,13 @@ CPU ``None``. Example on a system having 2 cores + Hyper Threading: - >>> import psutil - >>> psutil.cpu_count() - 4 - >>> psutil.cpu_count(logical=False) - 2 + .. code-block:: pycon + + >>> import psutil + >>> psutil.cpu_count() + 4 + >>> psutil.cpu_count(logical=False) + 2 Note that ``psutil.cpu_count()`` may not necessarily be equivalent to the actual number of CPUs the current process can use. @@ -163,8 +169,10 @@ CPU having more than 64 CPUs. The number of usable CPUs can be obtained with: - >>> len(psutil.Process().cpu_affinity()) - 1 + .. code-block:: pycon + + >>> len(psutil.Process().cpu_affinity()) + 1 .. function:: cpu_stats() @@ -377,16 +385,18 @@ Memory Example on Linux: - >>> import psutil - >>> mem = psutil.virtual_memory() - >>> mem - svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304, slab=199348224) - >>> - >>> THRESHOLD = 500 * 1024 * 1024 # 500MB - >>> if mem.available <= THRESHOLD: - ... print("warning") - ... - >>> + .. code-block:: pycon + + >>> import psutil + >>> mem = psutil.virtual_memory() + >>> mem + svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304, slab=199348224) + >>> + >>> THRESHOLD = 500 * 1024 * 1024 # 500MB + >>> if mem.available <= THRESHOLD: + ... print("warning") + ... + >>> .. note:: if you just want to know how much physical memory is left in a cross-platform manner, simply rely on **available** and **percent** @@ -425,9 +435,11 @@ Memory See `meminfo.py`_ script providing an example on how to convert bytes in a human readable form. - >>> import psutil - >>> psutil.swap_memory() - sswap(total=2097147904L, used=886620160L, free=1210527744L, percent=42.3, sin=1050411008, sout=1906720768) + .. code-block:: pycon + + >>> import psutil + >>> psutil.swap_memory() + sswap(total=2097147904L, used=886620160L, free=1210527744L, percent=42.3, sin=1050411008, sout=1906720768) .. versionchanged:: 5.2.3 on Linux this function relies on /proc fs instead of sysinfo() syscall so @@ -459,10 +471,12 @@ Disks * **opts**: a comma-separated string indicating different mount options for the drive/partition. Platform-dependent. - >>> import psutil - >>> psutil.disk_partitions() - [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), - sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] + .. code-block:: pycon + + >>> import psutil + >>> psutil.disk_partitions() + [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), + sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] .. versionchanged:: 5.7.4 added *maxfile* and *maxpath* fields. @@ -480,9 +494,11 @@ Disks (see `BPO-12442`_). See `disk_usage.py`_ script providing an example usage. - >>> import psutil - >>> psutil.disk_usage('/') - sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) + .. code-block:: pycon + + >>> import psutil + >>> psutil.disk_usage('/') + sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) .. note:: UNIX usually reserves 5% of the total disk space for the root user. @@ -534,14 +550,16 @@ Disks On diskless machines this function will return ``None`` or ``{}`` if *perdisk* is ``True``. - >>> import psutil - >>> psutil.disk_io_counters() - sdiskio(read_count=8141, write_count=2431, read_bytes=290203, write_bytes=537676, read_time=5868, write_time=94922) - >>> - >>> psutil.disk_io_counters(perdisk=True) - {'sda1': sdiskio(read_count=920, write_count=1, read_bytes=2933248, write_bytes=512, read_time=6016, write_time=4), - 'sda2': sdiskio(read_count=18707, write_count=8830, read_bytes=6060, write_bytes=3443, read_time=24585, write_time=1572), - 'sdb1': sdiskio(read_count=161, write_count=0, read_bytes=786432, write_bytes=0, read_time=44, write_time=0)} + .. code-block:: pycon + + >>> import psutil + >>> psutil.disk_io_counters() + sdiskio(read_count=8141, write_count=2431, read_bytes=290203, write_bytes=537676, read_time=5868, write_time=94922) + >>> + >>> psutil.disk_io_counters(perdisk=True) + {'sda1': sdiskio(read_count=920, write_count=1, read_bytes=2933248, write_bytes=512, read_time=6016, write_time=4), + 'sda2': sdiskio(read_count=18707, write_count=8830, read_bytes=6060, write_bytes=3443, read_time=24585, write_time=1572), + 'sdb1': sdiskio(read_count=161, write_count=0, read_bytes=786432, write_bytes=0, read_time=44, write_time=0)} .. note:: on Windows ``"diskperf -y"`` command may need to be executed first @@ -589,13 +607,15 @@ Network On machines with no network interfaces this function will return ``None`` or ``{}`` if *pernic* is ``True``. - >>> import psutil - >>> psutil.net_io_counters() - snetio(bytes_sent=14508483, bytes_recv=62749361, packets_sent=84311, packets_recv=94888, errin=0, errout=0, dropin=0, dropout=0) - >>> - >>> psutil.net_io_counters(pernic=True) - {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), - 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} + .. code-block:: pycon + + >>> import psutil + >>> psutil.net_io_counters() + snetio(bytes_sent=14508483, bytes_recv=62749361, packets_sent=84311, packets_recv=94888, errin=0, errout=0, dropin=0, dropout=0) + >>> + >>> psutil.net_io_counters(pernic=True) + {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), + 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} Also see `nettop.py`_ and `ifconfig.py`_ for an example application. @@ -666,13 +686,15 @@ Network Also, see `netstat.py`_ example script. Example: - >>> import psutil - >>> psutil.net_connections() - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=, pid=1254), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=, pid=2987), - pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=, pid=None), - pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=, pid=None) - ...] + .. code-block:: pycon + + >>> import psutil + >>> psutil.net_connections() + [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=, pid=1254), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=, pid=2987), + pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=, pid=None), + pconn(fd=-1, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=, pid=None) + ...] .. warning:: On Linux, retrieving some connections requires root privileges. If psutil is @@ -722,17 +744,17 @@ Network point to point interface (typically a VPN). *broadcast* and *ptp* are mutually exclusive. May be ``None``. - Example:: + .. code-block:: pycon - >>> import psutil - >>> psutil.net_if_addrs() - {'lo': [snicaddr(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), - snicaddr(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), - snicaddr(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], - 'wlan0': [snicaddr(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), - snicaddr(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), - snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} - >>> + >>> import psutil + >>> psutil.net_if_addrs() + {'lo': [snicaddr(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), + snicaddr(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), + snicaddr(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], + 'wlan0': [snicaddr(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), + snicaddr(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), + snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} + >>> See also `nettop.py`_ and `ifconfig.py`_ for an example application. @@ -780,16 +802,16 @@ Network ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, and ``d2`` (some flags are only available on certain platforms). - .. availability:: UNIX + Also see `nettop.py`_ and `ifconfig.py`_ for an example application. - Example: + .. code-block:: pycon - >>> import psutil - >>> psutil.net_if_stats() - {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast'), - 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running')} + >>> import psutil + >>> psutil.net_if_stats() + {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast'), + 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running')} - Also see `nettop.py`_ and `ifconfig.py`_ for an example application. + .. availability:: UNIX .. versionadded:: 3.0.0 @@ -819,19 +841,19 @@ Sensors - **critical**: temperature at which the system will shut down, or ``None`` if not available. - Example:: + See also `temperatures.py`_ and `sensors.py`_ for an example application. - >>> import psutil - >>> psutil.sensors_temperatures() - {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], - 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], - 'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), - shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0), - shwtemp(label='Core 1', current=52.0, high=100.0, critical=100.0), - shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), - shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} + .. code-block:: pycon - See also `temperatures.py`_ and `sensors.py`_ for an example application. + >>> import psutil + >>> psutil.sensors_temperatures() + {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], + 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], + 'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), + shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0), + shwtemp(label='Core 1', current=52.0, high=100.0, critical=100.0), + shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), + shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} .. availability:: Linux, FreeBSD @@ -846,11 +868,12 @@ Sensors certain hardware sensor fan. Fan speed is expressed in RPM (revolutions per minute). If sensors are not supported by the OS an empty dict is returned. - Example:: - >>> import psutil - >>> psutil.sensors_fans() - {'asus': [sfan(label='cpu_fan', current=3200)]} + .. code-block:: pycon + + >>> import psutil + >>> psutil.sensors_fans() + {'asus': [sfan(label='cpu_fan', current=3200)]} See also `fans.py`_ and `sensors.py`_ for an example application. @@ -874,20 +897,20 @@ Sensors - **power_plugged**: ``True`` if the AC power cable is connected, ``False`` if not or ``None`` if it can't be determined. - Example:: - - >>> import psutil - >>> - >>> def secs2hours(secs): - ... mm, ss = divmod(secs, 60) - ... hh, mm = divmod(mm, 60) - ... return "%d:%02d:%02d" % (hh, mm, ss) - ... - >>> battery = psutil.sensors_battery() - >>> battery - sbattery(percent=93, secsleft=16628, power_plugged=False) - >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) - charge = 93%, time left = 4:37:08 + .. code-block:: pycon + + >>> import psutil + >>> + >>> def secs2hours(secs): + ... mm, ss = divmod(secs, 60) + ... hh, mm = divmod(mm, 60) + ... return "%d:%02d:%02d" % (hh, mm, ss) + ... + >>> battery = psutil.sensors_battery() + >>> battery + sbattery(percent=93, secsleft=16628, power_plugged=False) + >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) + charge = 93%, time left = 4:37:08 See also `battery.py`_ and `sensors.py`_ for an example application. @@ -936,12 +959,12 @@ Other system info - **pid**: the PID of the login process (like sshd, tmux, gdm-session-worker, ...). On Windows and OpenBSD this is always set to ``None``. - Example:: + .. code-block:: pycon - >>> import psutil - >>> psutil.users() - [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), - suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] + >>> import psutil + >>> psutil.users() + [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), + suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] .. versionchanged:: 5.3.0 added "pid" field. @@ -960,9 +983,12 @@ Functions To iterate over all processes and avoid race conditions :func:`process_iter` should be preferred. - >>> import psutil - >>> psutil.pids() - [1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, ..., 32498] + .. code-block:: pycon + + >>> import psutil + >>> psutil.pids() + [1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, ..., 32498] + .. versionchanged:: 5.6.0 PIDs are returned in sorted order. @@ -985,30 +1011,34 @@ Functions Sorting order in which processes are returned is based on their PID. - Example:: + .. code-block:: pycon + + >>> import psutil + >>> for proc in psutil.process_iter(['pid', 'name', 'username']): + ... print(proc.info) + ... + {'name': 'systemd', 'pid': 1, 'username': 'root'} + {'name': 'kthreadd', 'pid': 2, 'username': 'root'} + {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} + ... - >>> import psutil - >>> for proc in psutil.process_iter(['pid', 'name', 'username']): - ... print(proc.info) - ... - {'name': 'systemd', 'pid': 1, 'username': 'root'} - {'name': 'kthreadd', 'pid': 2, 'username': 'root'} - {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} - ... + A dict comprehensions to create a ``{pid: info, ...}`` data structure: - A dict comprehensions to create a ``{pid: info, ...}`` data structure:: + .. code-block:: pycon - >>> import psutil - >>> procs = {p.pid: p.info for p in psutil.process_iter(['name', 'username'])} - >>> procs - {1: {'name': 'systemd', 'username': 'root'}, - 2: {'name': 'kthreadd', 'username': 'root'}, - 3: {'name': 'ksoftirqd/0', 'username': 'root'}, - ...} + >>> import psutil + >>> procs = {p.pid: p.info for p in psutil.process_iter(['name', 'username'])} + >>> procs + {1: {'name': 'systemd', 'username': 'root'}, + 2: {'name': 'kthreadd', 'username': 'root'}, + 3: {'name': 'ksoftirqd/0', 'username': 'root'}, + ...} - Clear internal cache:: + Clear internal cache: - >>> psutil.process_iter.cache_clear() + .. code-block:: pycon + + >>> psutil.process_iter.cache_clear() .. versionchanged:: 5.3.0 added "attrs" and "ad_value" parameters. @@ -1159,19 +1189,20 @@ Process class The cache is cleared when exiting the context manager block. The advice is to use this every time you retrieve more than one information about the process. If you're lucky, you'll get a hell of a speedup. - Example: - - >>> import psutil - >>> p = psutil.Process() - >>> with p.oneshot(): - ... p.name() # execute internal routine once collecting multiple info - ... p.cpu_times() # return cached value - ... p.cpu_percent() # return cached value - ... p.create_time() # return cached value - ... p.ppid() # return cached value - ... p.status() # return cached value - ... - >>> + + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> with p.oneshot(): + ... p.name() # execute internal routine once collecting multiple info + ... p.cpu_times() # return cached value + ... p.cpu_percent() # return cached value + ... p.create_time() # return cached value + ... p.ppid() # return cached value + ... p.status() # return cached value + ... + >>> Here's a list of methods which can take advantage of the speedup depending on what platform you're on. @@ -1248,27 +1279,34 @@ Process class longer exists), this may be an empty string. The return value is cached after first call. - >>> import psutil - >>> psutil.Process().exe() - '/usr/bin/python3' + .. code-block:: pycon + + >>> import psutil + >>> psutil.Process().exe() + '/usr/bin/python3' .. method:: cmdline() The command line used to start this process, as a list of strings. The return value is not cached because the cmdline of a process may change. - >>> import psutil - >>> psutil.Process().cmdline() - ['python', 'manage.py', 'runserver'] + .. code-block:: pycon + + >>> import psutil + >>> psutil.Process().cmdline() + ['python', 'manage.py', 'runserver'] .. method:: environ() The environment variables of the process as a dict. Note: this might not reflect changes made after the process started. - >>> import psutil - >>> psutil.Process().environ() - {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} + .. code-block:: pycon + + >>> import psutil + >>> psutil.Process().environ() + {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} + .. note:: on macOS Big Sur this function returns something meaningful only for the @@ -1294,12 +1332,14 @@ Process class clock, which means it may be affected by changes such as manual adjustments or time synchronization (e.g. NTP). - >>> import psutil, datetime - >>> p = psutil.Process() - >>> p.create_time() - 1307289803.47 - >>> datetime.datetime.fromtimestamp(p.create_time()).strftime("%Y-%m-%d %H:%M:%S") - '2011-03-05 18:03:52' + .. code-block:: pycon + + >>> import psutil, datetime + >>> p = psutil.Process() + >>> p.create_time() + 1307289803.47 + >>> datetime.datetime.fromtimestamp(p.create_time()).strftime("%Y-%m-%d %H:%M:%S") + '2011-03-05 18:03:52' .. method:: as_dict(attrs=None, ad_value=None) @@ -1316,14 +1356,16 @@ Process class Internally, :meth:`as_dict` uses :meth:`oneshot` context manager so there's no need you use it also. - >>> import psutil - >>> p = psutil.Process() - >>> p.as_dict(attrs=['pid', 'name', 'username']) - {'username': 'giampaolo', 'pid': 12366, 'name': 'python'} - >>> - >>> # get a list of valid attrs names - >>> list(psutil.Process().as_dict().keys()) - ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.as_dict(attrs=['pid', 'name', 'username']) + {'username': 'giampaolo', 'pid': 12366, 'name': 'python'} + >>> + >>> # get a list of valid attrs names + >>> list(psutil.Process().as_dict().keys()) + ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] .. versionchanged:: 3.0.0 *ad_value* is used also when incurring into :class:`ZombieProcess` @@ -1399,12 +1441,14 @@ Process class On UNIX this is a number which usually goes from ``-20`` to ``20``. The higher the nice value, the lower the priority of the process. - >>> import psutil - >>> p = psutil.Process() - >>> p.nice(10) # set - >>> p.nice() # get - 10 - >>> + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.nice(10) # set + >>> p.nice() # get + 10 + >>> Starting from Python 3.3 this functionality is also available as `os.getpriority`_ and `os.setpriority`_ (see `BPO-10784`_). @@ -1415,7 +1459,9 @@ Process class The return value on Windows is a :class:`psutil.ProcessPriority` enum member. Example which increases process priority on Windows: - >>> p.nice(psutil.HIGH_PRIORITY_CLASS) + .. code-block:: pycon + + >>> p.nice(psutil.HIGH_PRIORITY_CLASS) .. versionchanged:: 8.0.0 on Windows, return value is now a :class:`psutil.ProcessPriority` enum @@ -1452,17 +1498,19 @@ Process class * ``IOPRIO_VERYLOW``: lowest priority. Here's an example on how to set the highest I/O priority depending on what - platform you're on:: + platform you're on: - >>> import psutil - >>> p = psutil.Process() - >>> if psutil.LINUX: - ... p.ionice(psutil.IOPRIO_CLASS_RT, value=7) - ... else: - ... p.ionice(psutil.IOPRIO_HIGH) - ... - >>> p.ionice() # get - pionice(ioclass=, value=7) + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> if psutil.LINUX: + ... p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + ... else: + ... p.ionice(psutil.IOPRIO_HIGH) + ... + >>> p.ionice() # get + pionice(ioclass=, value=7) .. availability:: Linux, Windows @@ -1481,17 +1529,17 @@ Process class but can be used for any process PID, not only `os.getpid`_. For get, return value is a ``(soft, hard)`` tuple. Each value may be either and integer or :data:`psutil.RLIMIT_* `. - Example: + Also see `procinfo.py`_ script. - >>> import psutil - >>> p = psutil.Process() - >>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128)) # process can open max 128 file descriptors - >>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024)) # can create files no bigger than 1024 bytes - >>> p.rlimit(psutil.RLIMIT_FSIZE) # get - (1024, 1024) - >>> + .. code-block:: pycon - Also see `procinfo.py`_ script. + >>> import psutil + >>> p = psutil.Process() + >>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128)) # process can open max 128 file descriptors + >>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024)) # can create files no bigger than 1024 bytes + >>> p.rlimit(psutil.RLIMIT_FSIZE) # get + (1024, 1024) + >>> .. availability:: Linux, FreeBSD @@ -1533,10 +1581,13 @@ Process class - **other_bytes** *(Windows)*: the number of bytes transferred during operations other than read and write operations. - >>> import psutil - >>> p = psutil.Process() - >>> p.io_counters() - pio(read_count=454556, write_count=3456, read_bytes=110592, write_bytes=0, read_chars=769931, write_chars=203) + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.io_counters() + pio(read_count=454556, write_count=3456, read_bytes=110592, write_bytes=0, read_chars=769931, write_chars=203) + .. availability:: Linux, BSD, Windows, AIX @@ -1603,12 +1654,15 @@ Process class This value is excluded from `user` and `system` times count (because the CPU is not doing any work). - >>> import psutil - >>> p = psutil.Process() - >>> p.cpu_times() - pcputimes(user=0.03, system=0.67, children_user=0.0, children_system=0.0, iowait=0.08) - >>> sum(p.cpu_times()[:2]) # cumulative, excluding children and iowait - 0.70 + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.cpu_times() + pcputimes(user=0.03, system=0.67, children_user=0.0, children_system=0.0, iowait=0.08) + >>> sum(p.cpu_times()[:2]) # cumulative, excluding children and iowait + 0.70 + .. versionchanged:: 4.1.0 return two extra fields: *children_user* and *children_system*. @@ -1628,16 +1682,17 @@ Process class will return a meaningless ``0.0`` value which you are supposed to ignore. For accuracy, it is recommended to call this function a second time with at least ``0.1`` seconds between calls. - Example: - >>> import psutil - >>> p = psutil.Process() - >>> # blocking - >>> p.cpu_percent(interval=1) - 2.0 - >>> # non-blocking (percentage since last call) - >>> p.cpu_percent(interval=None) - 2.9 + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> # blocking + >>> p.cpu_percent(interval=1) + 2.0 + >>> # non-blocking (percentage since last call) + >>> p.cpu_percent(interval=None) + 2.9 .. note:: the returned value can be > 100.0 in case of a process running multiple @@ -1674,19 +1729,21 @@ Process class On some systems such as Linux this may not necessarily mean all available logical CPUs as in ``list(range(psutil.cpu_count()))``). - >>> import psutil - >>> psutil.cpu_count() - 4 - >>> p = psutil.Process() - >>> # get - >>> p.cpu_affinity() - [0, 1, 2, 3] - >>> # set; from now on, process will run on CPU #0 and #1 only - >>> p.cpu_affinity([0, 1]) - >>> p.cpu_affinity() - [0, 1] - >>> # reset affinity against all eligible CPUs - >>> p.cpu_affinity([]) + .. code-block:: pycon + + >>> import psutil + >>> psutil.cpu_count() + 4 + >>> p = psutil.Process() + >>> # get + >>> p.cpu_affinity() + [0, 1, 2, 3] + >>> # set; from now on, process will run on CPU #0 and #1 only + >>> p.cpu_affinity([0, 1]) + >>> p.cpu_affinity() + [0, 1] + >>> # reset affinity against all eligible CPUs + >>> p.cpu_affinity([]) .. availability:: Linux, Windows, FreeBSD @@ -1777,10 +1834,12 @@ Process class Example on Linux: - >>> import psutil - >>> p = psutil.Process() - >>> p.memory_info() - pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, data=9891840) + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.memory_info() + pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, data=9891840) .. versionchanged:: 4.0.0 multiple fields are returned, not only *rss* and *vms*. @@ -1905,10 +1964,12 @@ Process class Example on Linux: - >>> import psutil - >>> p = psutil.Process() - >>> p.memory_footprint() - pfootprint(uss=6545408, pss=6872064, swap=0) + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.memory_footprint() + pfootprint(uss=6545408, pss=6872064, swap=0) See also `procsmem.py`_ for an example application. @@ -1996,12 +2057,14 @@ Process class - **ref_count**: reference count on the VM object backing this mapping. - **shadow_count**: depth of the copy-on-write shadow object chain. - >>> import psutil - >>> p = psutil.Process() - >>> p.memory_maps() - [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), - pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), - ...] + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.memory_maps() + [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), + pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), + ...] .. availability:: Linux, Windows, FreeBSD, SunOS @@ -2015,6 +2078,7 @@ Process class instances. If recursive is `True` return all the parent descendants. Pseudo code example assuming *A == this process*: + :: A ─┐ @@ -2025,10 +2089,12 @@ Process class ├─ C (child) └─ D (child) - >>> p.children() - B, C, D - >>> p.children(recursive=True) - B, X, Y, C, D + .. code-block:: pycon + + >>> p.children() + B, C, D + >>> p.children(recursive=True) + B, X, Y, C, D Note that in the example above if process X disappears process Y won't be returned either as the reference to process A is lost. @@ -2050,12 +2116,14 @@ Process class from disk. These are expensive because they stall the process until I/O completes. - Both counters are cumulative since process creation. Example:: + Both counters are cumulative since process creation. - >>> import psutil - >>> p = psutil.Process() - >>> p.page_faults() - ppagefaults(minor=5905, major=3) + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.page_faults() + ppagefaults(minor=5905, major=3) .. versionadded:: 8.0.0 @@ -2079,11 +2147,14 @@ Process class `os.open`_ C call when the file was opened (e.g. `os.O_RDONLY`_, `os.O_TRUNC`_, etc). - >>> import psutil - >>> f = open('file.ext', 'w') - >>> p = psutil.Process() - >>> p.open_files() - [popenfile(path='/home/giampaolo/svn/psutil/file.ext', fd=3, position=0, mode='w', flags=32769)] + .. code-block:: pycon + + >>> import psutil + >>> f = open('file.ext', 'w') + >>> p = psutil.Process() + >>> p.open_files() + [popenfile(path='/home/giampaolo/svn/psutil/file.ext', fd=3, position=0, mode='w', flags=32769)] + .. warning:: on Windows this method is not reliable due to some limitations of the @@ -2161,17 +2232,17 @@ Process class | ``"all"`` | the sum of all the possible families and protocols | +----------------+-----------------------------------------------------+ - Example: + .. code-block:: pycon - >>> import psutil - >>> p = psutil.Process(1694) - >>> p.name() - 'firefox' - >>> p.net_connections() - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=), - pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=), - pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=)] + >>> import psutil + >>> p = psutil.Process(1694) + >>> p.name() + 'firefox' + >>> p.net_connections() + [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=), + pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=), + pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=)] .. warning:: On Linux, retrieving connections for certain processes requires root @@ -2298,11 +2369,13 @@ Process class The return value is cached. To wait for multiple processes use :func:`psutil.wait_procs`. - >>> import psutil - >>> p = psutil.Process(9891) - >>> p.terminate() - >>> p.wait() - + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process(9891) + >>> p.terminate() + >>> p.wait() + .. note:: @@ -2350,19 +2423,22 @@ Popen class 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 - >>> - >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) - >>> p.name() - 'python' - >>> p.username() - 'giampaolo' - >>> p.communicate() - ('hello\n', None) - >>> p.wait(timeout=2) - 0 - >>> + .. code-block:: pycon + + >>> import psutil + >>> from subprocess import PIPE + >>> + >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) + >>> p.name() + 'python' + >>> p.username() + 'giampaolo' + >>> p.communicate() + ('hello\n', None) + >>> p.wait(timeout=2) + 0 + >>> + .. versionchanged:: 4.4.0 added context manager support. @@ -2404,9 +2480,11 @@ Python's memory tracking misses. - ``heap_count``: (Windows only) number of private heaps created via ``HeapCreate()``. - >>> import psutil - >>> psutil.heap_info() - pheap(heap_used=5177792, mmap_used=819200) + .. code-block:: pycon + + >>> import psutil + >>> psutil.heap_info() + pheap(heap_used=5177792, mmap_used=819200) These fields reflect how unreleased C allocations affect the heap: @@ -2519,29 +2597,31 @@ Windows services Utility method retrieving all the information above as a dictionary. - .. versionadded:: 4.2.0 + Example code: + + .. code-block:: pycon + + >>> import psutil + >>> list(psutil.win_service_iter()) + [, + , + , + , + ...] + >>> s = psutil.win_service_get('alg') + >>> s.as_dict() + {'binpath': 'C:\\Windows\\System32\\alg.exe', + 'description': 'Provides support for 3rd party protocol plug-ins for Internet Connection Sharing', + 'display_name': 'Application Layer Gateway Service', + 'name': 'alg', + 'pid': None, + 'start_type': 'manual', + 'status': 'stopped', + 'username': 'NT AUTHORITY\\LocalService'} .. availability:: Windows -Example code: - - >>> import psutil - >>> list(psutil.win_service_iter()) - [, - , - , - , - ...] - >>> s = psutil.win_service_get('alg') - >>> s.as_dict() - {'binpath': 'C:\\Windows\\System32\\alg.exe', - 'description': 'Provides support for 3rd party protocol plug-ins for Internet Connection Sharing', - 'display_name': 'Application Layer Gateway Service', - 'name': 'alg', - 'pid': None, - 'start_type': 'manual', - 'status': 'stopped', - 'username': 'NT AUTHORITY\\LocalService'} + .. versionadded:: 4.2.0 ---- @@ -2883,9 +2963,11 @@ Other constants A tuple to check psutil installed version. Example: - >>> import psutil - >>> if psutil.version_info >= (4, 5): - ... pass + .. code-block:: pycon + + >>> import psutil + >>> if psutil.version_info >= (4, 5): + ... pass .. _`BPO-10784`: https://bugs.python.org/issue10784 .. _`BPO-12442`: https://bugs.python.org/issue12442 From e681a4eeb44a058390efdb74fedc7a7ed3d3288e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 16 Mar 2026 08:39:28 +0100 Subject: [PATCH 1599/1714] Doc: remove useless conf --- docs/conf.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 37ffb244cd..22660413ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,51 +28,30 @@ sys.path.insert(0, str(HERE / '_ext')) extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.coverage", "sphinx.ext.extlinks", - "sphinx.ext.imgmath", "sphinx.ext.viewcode", - "sphinx.ext.intersphinx", "sphinx_copybutton", - # our own custom extensions in _ext/ dir + # custom extensions in _ext/ dir "availability", "add_home_link", "changelog_anchors", "check_python_syntax", ] -# project info project = PROJECT_NAME copyright = f"2009-{THIS_YEAR}, {AUTHOR}" author = AUTHOR version = VERSION release = VERSION - extlinks = { 'gh': ('https://github.com/giampaolo/psutil/issues/%s', '#%s'), } - templates_path = ['_templates'] -source_suffix = '.rst' -master_doc = 'index' - -language = "en" +html_static_path = ['_static'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -pygments_style = 'sphinx' - -todo_include_todos = False - html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] htmlhelp_basename = f"{PROJECT_NAME}-doc" - copybutton_exclude = '.linenos, .gp' - -latex_documents = [ - (master_doc, 'psutil.tex', 'psutil Documentation', AUTHOR, 'manual') -] - html_css_files = [ 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', From e160395942e765372613b0d30b161e2363de2ec5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 17 Mar 2026 11:40:09 +0100 Subject: [PATCH 1600/1714] Doc: shell equivalents section (#2768) This is something I've wanted to do for a long time: showing the corresponding CLI command for each psutil API. It turns out psutil test suite already fills most of the gaps, as it already use these CLI tools to test its own implementation (especially on Linux). --- MANIFEST.in | 1 + README.rst | 12 +- docs/changelog.rst | 198 ++++++------- docs/conf.py | 25 +- docs/index.rst | 6 +- docs/shell_equivalents.rst | 563 +++++++++++++++++++++++++++++++++++++ tests/test_linux.py | 62 ++++ tests/test_windows.py | 102 ++++++- 8 files changed, 833 insertions(+), 136 deletions(-) create mode 100644 docs/shell_equivalents.rst diff --git a/MANIFEST.in b/MANIFEST.in index 20c912b006..0e4f0e95c6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -34,6 +34,7 @@ include docs/install.rst include docs/platform.rst include docs/recipes.rst include docs/requirements.txt +include docs/shell_equivalents.rst include docs/timeline.rst include psutil/__init__.py include psutil/_common.py diff --git a/README.rst b/README.rst index 398f73591c..5748ccd91b 100644 --- a/README.rst +++ b/README.rst @@ -71,11 +71,12 @@ Summary psutil (process and system utilities) is a cross-platform library for retrieving information about **running processes** and **system utilization** -(CPU, memory, disks, network, sensors) in Python. -It is useful mainly for **system monitoring**, **profiling**, **limiting -process resources**, and **managing running processes**. -It implements many functionalities offered by classic UNIX command line tools -such as *ps, top, iotop, lsof, netstat, ifconfig, free* and others. +(CPU, memory, disks, network, sensors) in Python. It is useful mainly for +**system monitoring**, **profiling**, **limiting process resources**, and +**managing running processes**. It implements many functionalities offered by +classic UNIX command line tools such as +*ps, top, free, iotop, netstat, ifconfig, lsof* +and others (see `shell equivalents`_). psutil currently supports the following platforms: - **Linux** @@ -555,4 +556,5 @@ Windows services .. _`psleak`: https://github.com/giampaolo/psleak +.. _`shell equivalents`: https://psutil.readthedocs.io/en/latest/shell_equivalents.html .. _`top 100`: https://clickpy.clickhouse.com/dashboard/psutil diff --git a/docs/changelog.rst b/docs/changelog.rst index e3bd9fa3be..6449eb21fd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,100 +8,108 @@ Changelog **Enhancements** -Doc: - -- :gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, - :gh:`2767`. Tons of doc improvements. In order of importance: - - - Split the documentation from a single-page HTML document into multiple - sections. Sections now include separate pages for API reference, - installation, release timeline and more. - - Moved 18 years old ``HISTORY.rst`` and ``INSTALL.rst`` files into - ``docs/changelog.rst`` and ``docs/install.rst`` for better integration. - Original files remain in the project root with a note pointing to the new - locations. - - Show a section of notable software that depends on psutil (``/adoption``). - - Show a section of people who contributed or donated to psutil over the - years (``/credits``). - - Added a summary table of officially supported operating systems and - architectures. - - Added tons of new examples in ``docs/recipes.rst``. - - Drastically improved :func:`virtual_memory` doc, which is now more - detailed, and includes a table with all the available metrics on each - platform. - - Show a clickable COPY button to copy code snippets. - - Show ``psutil.`` prefix for all APIs. - - Use sphinx extension to validate Python code snippets syntax at build-time. - - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting, and add a custom - script to detect dead reference links in ``.rst`` files. - -Type hints / enums: - -- :gh:`1946`: Add inline type hints to all public APIs in `psutil/__init__.py`. - Type checkers (mypy, pyright, etc.) can now statically verify code that uses - psutil. No runtime behavior is changed; the annotations are purely - informational. -- :gh:`2751`: Convert all named tuples in `psutil/_ntuples.py`_ from - ``collections.namedtuple`` to ``typing.NamedTuple`` classes with **type - annotations**. This makes the classes self-documenting, effectively turning - this module into a readable API reference. -- :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, - :class:`ConnectionStatus`, - :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) - grouping related constants. The individual top-level constants (e.g. - ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases - for the corresponding enum members. - -New APIs: - -- :gh:`2729`: New :meth:`Process.page_faults` method, returning a ``(minor, - major)`` namedtuple. -- :gh:`2731`, :gh:`2736`, :gh:`2723`, :gh:`2733`: Reorganization of process - memory APIs. - - - Add new :meth:`Process.memory_info_ex` method, which extends - :meth:`Process.memory_info` with platform-specific metrics: - - - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, - *swap*, *hugetlb* - - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, - *phys_footprint* - - Windows: *virtual*, *peak_virtual* - - - Add new :meth:`Process.memory_footprint` method, which returns *uss*, - *pss* and *swap* metrics (what :meth:`Process.memory_full_info` used to - return, which is now **deprecated**). - - - :meth:`Process.memory_info` named tuple changed: - - - BSD: added *peak_rss*. - - - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated - aliases returning 0 and emitting `DeprecationWarning` are kept. - - - macOS: *pfaults* and *pageins* removed with **no - backward-compataliases**. Use :meth:`Process.page_faults` instead. - - - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. - At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, - *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex`. All - these old names still work but raise `DeprecationWarning`. - - - :meth:`Process.memory_full_info` is **deprecated**. Use the new - :meth:`Process.memory_footprint` instead. - -Others: - -- :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times` - has been normalized on all platforms, and the first 3 fields are now always - ``user, system, idle``. See compatibility notes below. -- :gh:`2754`: standardize :func:`sensors_battery`'s `percent` so that it - returns a `float` instead of `int` on all systems, not only Linux. -- :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update - changelog.rst and credits.rst when commenting with /changelog. -- :gh:`2766`: remove remaining Python 2.7 compatibility shims from - ``setup.py``, simplifying the build infrastructure. +- Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, + :gh:`2764`, :gh:`2767`, :gh:`2768`): + + - Split docs from a single HTML file into multiple sections (API reference, + install, etc.). + + - Added new sections: + + - `/recipes `__: + show code samples + - `/adoption `__: + notable software using psutil + - `/shell_equivalents `__: + maps each psutil API to native CLI commands + - `/install `__ + (was old ``INSTALL.rst`` in root dir) + - `/credits `__: + list contributors and donors (was old ``CREDITS`` in root dir) + - `/platform `__: + summary of OSes and architectures support + + - Usability: + + - Show a clickable COPY button to copy code snippets. + - Show ``psutil.`` prefix for all APIs. + - Use sphinx extension to validate Python code snippets syntax at build-time. + + - Testing: + + - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting. + - Add custom script to detect dead reference links in ``.rst`` files. + - Build doc as part of CI process. + + - Greatly improved :func:`virtual_memory` doc. + +- Type hints / enums: + + - :gh:`1946`: Add inline type hints to all public APIs in `psutil/__init__.py`. + Type checkers (mypy, pyright, etc.) can now statically verify code that uses + psutil. No runtime behavior is changed; the annotations are purely + informational. + - :gh:`2751`: Convert all named tuples from ``collections.namedtuple`` to + ``typing.NamedTuple`` classes with **type annotations**. This makes the + classes self-documenting, effectively turning this module into a readable + API reference. + - :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, + :class:`ConnectionStatus`, + :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) + grouping related constants. The individual top-level constants (e.g. + ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases + for the corresponding enum members. + +- New APIs: + + - :gh:`2729`: New :meth:`Process.page_faults` method, returning a ``(minor, + major)`` namedtuple. + - Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`, + :gh:`2733`). + + - Add new :meth:`Process.memory_info_ex` method, which extends + :meth:`Process.memory_info` with platform-specific metrics: + + - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, + *swap*, *hugetlb* + - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, + *phys_footprint* + - Windows: *virtual*, *peak_virtual* + + - Add new :meth:`Process.memory_footprint` method, which returns *uss*, + *pss* and *swap* metrics (what :meth:`Process.memory_full_info` used to + return, which is now **deprecated**). + + - :meth:`Process.memory_info` named tuple changed: + + - BSD: added *peak_rss*. + + - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated + aliases returning 0 and emitting `DeprecationWarning` are kept. + + - macOS: *pfaults* and *pageins* removed with **no + backward-compataliases**. Use :meth:`Process.page_faults` instead. + + - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. + At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, + *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex`. All + these old names still work but raise `DeprecationWarning`. + + - :meth:`Process.memory_full_info` is **deprecated**. Use the new + :meth:`Process.memory_footprint` instead. + +- Others: + + - :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times` + has been normalized on all platforms, and the first 3 fields are now always + ``user, system, idle``. See compatibility notes below. + - :gh:`2754`: standardize :func:`sensors_battery`'s `percent` so that it + returns a `float` instead of `int` on all systems, not only Linux. + - :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update + changelog.rst and credits.rst when commenting with /changelog. + - :gh:`2766`: remove remaining Python 2.7 compatibility shims from + ``setup.py``, simplifying the build infrastructure. **Bug fixes** @@ -3102,5 +3110,3 @@ cases accessing the old names will work but it will cause a .. _`ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py .. _`pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py .. _`top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py - -.. _`psutil/_ntuples.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_ntuples.py diff --git a/docs/conf.py b/docs/conf.py index 37ffb244cd..22660413ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,51 +28,30 @@ sys.path.insert(0, str(HERE / '_ext')) extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.coverage", "sphinx.ext.extlinks", - "sphinx.ext.imgmath", "sphinx.ext.viewcode", - "sphinx.ext.intersphinx", "sphinx_copybutton", - # our own custom extensions in _ext/ dir + # custom extensions in _ext/ dir "availability", "add_home_link", "changelog_anchors", "check_python_syntax", ] -# project info project = PROJECT_NAME copyright = f"2009-{THIS_YEAR}, {AUTHOR}" author = AUTHOR version = VERSION release = VERSION - extlinks = { 'gh': ('https://github.com/giampaolo/psutil/issues/%s', '#%s'), } - templates_path = ['_templates'] -source_suffix = '.rst' -master_doc = 'index' - -language = "en" +html_static_path = ['_static'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -pygments_style = 'sphinx' - -todo_include_todos = False - html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] htmlhelp_basename = f"{PROJECT_NAME}-doc" - copybutton_exclude = '.linenos, .gp' - -latex_documents = [ - (master_doc, 'psutil.tex', 'psutil Documentation', AUTHOR, 'manual') -] - html_css_files = [ 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', diff --git a/docs/index.rst b/docs/index.rst index a1d2861540..6c781dcb77 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,8 +28,8 @@ in **Python**. It is useful mainly for **system monitoring**, **profiling**, **limiting process resources**, and **managing running processes**. It implements many functionalities offered by UNIX command line tools -such as: *ps, top, lsof, netstat, ifconfig, who, df, kill, free, nice, -ionice, iostat, iotop, uptime, pidof, tty, taskset, pmap* and others. +such as *ps, top, free, iotop, netstat, ifconfig, lsof* and others +(see :doc:`shell equivalents `). psutil currently supports the following platforms, from **Python 3.7** onwards: - **Linux** @@ -99,6 +99,7 @@ Table of Contents Install API Reference Recipes + Shell equivalents Platform support Who uses psutil Credits @@ -111,3 +112,4 @@ Table of Contents Changelog .. _`Tidelift security contact`: https://tidelift.com/security +.. diff --git a/docs/shell_equivalents.rst b/docs/shell_equivalents.rst new file mode 100644 index 0000000000..63e3c6aa56 --- /dev/null +++ b/docs/shell_equivalents.rst @@ -0,0 +1,563 @@ + +.. currentmodule:: psutil + +Shell equivalents +================= + +This page maps psutil's Python API to the equivalent native terminal commands +on each platform. This is useful for understanding what psutil replaces and for +cross-checking results. + +System-wide functions +--------------------- + +CPU +~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`cpu_percent` + - ``top`` + - same + - same + - + * - :func:`cpu_count(logical=True) ` + - ``nproc`` + - ``sysctl hw.logicalcpu`` + - ``sysctl hw.ncpu`` + - + * - :func:`cpu_count(logical=False) ` + - ``lscpu | grep '^Core(s)'`` + - ``sysctl hw.physicalcpu`` + - + - + * - :func:`cpu_times(percpu=False) ` + - ``cat /proc/stat | grep '^cpu\s'`` + - + - ``systat -vmstat`` + - + * - :func:`cpu_times(percpu=True) ` + - ``cat /proc/stat | grep '^cpu'`` + - + - ``systat -vmstat`` + - + * - :func:`cpu_times_percent(percpu=False) ` + - ``mpstat`` + - + - + - + * - :func:`cpu_times_percent(percpu=True) ` + - ``mpstat -P ALL`` + - + - + - + * - :func:`cpu_freq` + - ``cpufreq-info``, ``lscpu | grep "MHz"`` + - ``sysctl hw.cpufrequency`` + - ``sysctl dev.cpu.0.freq`` + - ``systeminfo`` + * - :func:`cpu_stats` + - + - + - ``sysctl vm.stats.sys`` + - + * - :func:`getloadavg` + - ``uptime`` + - same + - same + - + +Memory +~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`virtual_memory` + - ``free``, ``vmstat``, ``cat /proc/meminfo`` + - ``vm_stat`` + - ``sysctl vm.stats`` + - ``systeminfo`` + * - :func:`swap_memory` + - ``free``, ``vmstat``, ``swapon`` + - ``sysctl vm.swapusage`` + - ``swapinfo`` + - + * - :func:`heap_info` + - + - + - + - + * - :func:`heap_trim` + - + - + - + - + +Disks +~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`disk_usage` + - ``df`` + - same + - same + - ``fsutil volume diskfree C:\`` + * - :func:`disk_partitions` + - ``findmnt``, ``mount`` + - ``mount`` + - ``mount`` + - + * - :func:`disk_io_counters` + - ``iostat -dx`` + - ``iostat`` + - ``iostat -x`` + - + +Network +~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`net_connections` + - ``netstat -anp``, ``ss``, ``lsof -nP -i -U`` + - ``netstat -anp`` + - ``netstat -an`` + - ``netstat -an`` + * - :func:`net_if_addrs` + - ``ifconfig``, ``ip addr`` + - ``ifconfig`` + - ``ifconfig`` + - ``ipconfig``, ``systeminfo`` + * - :func:`net_io_counters` + - ``netstat -i``, ``ifconfig``, ``ip -s link`` + - ``netstat -i`` + - ``netstat -i`` + - ``netstat -e`` + * - :func:`net_if_stats` + - ``ifconfig``, ``ip -br link``, ``ip link`` + - ``ifconfig`` + - ``ifconfig`` + - ``netsh interface show interface`` + +Sensors +~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`sensors_temperatures` + - ``sensors`` + - + - ``sysctl dev.cpu.*.temperature`` + - + * - :func:`sensors_fans` + - ``sensors`` + - + - ``sysctl dev.cpu.*.fan`` + - + * - :func:`sensors_battery` + - ``acpi -b`` + - ``pmset -g batt`` + - ``apm -b`` + - + +Other +~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`boot_time` + - ``uptime``, ``who -b`` + - ``sysctl kern.boottime`` + - ``sysctl kern.boottime`` + - ``systeminfo`` + * - :func:`users` + - ``who -a``, ``w`` + - same + - same + - + * - :func:`pids` + - ``ps -eo pid`` + - same + - same + - ``tasklist`` + * - :func:`pid_exists` + - ``kill -0 PID`` + - same + - same + - + +Process methods +--------------- + +Identity +~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.name` + - ``ps -o comm -p PID`` + - same + - ``procstat -b PID`` + - + * - :meth:`Process.exe` + - ``readlink /proc/PID/exe`` + - ``lsof -p PID`` + - ``procstat -b PID`` + - + * - :meth:`Process.cmdline` + - ``ps -o args -p PID`` + - same + - ``procstat -c PID`` + - + * - :meth:`Process.status` + - ``ps -o stat -p PID`` + - same + - same + - + * - :meth:`Process.create_time` + - ``ps -o lstart -p PID`` + - same + - same + - + * - :meth:`Process.is_running` + - ``kill -0 PID`` + - same + - same + - + * - :meth:`Process.environ` + - ``xargs -0 -a /proc/PID/environ`` + - + - ``procstat -e PID`` + - + * - :meth:`Process.cwd` + - ``pwdx PID`` + - ``lsof -p PID -a -d cwd`` + - + - + +Process tree +~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.ppid` + - ``ps -o ppid= -p PID`` + - same + - same + - + * - :meth:`Process.parent` + - ``ps -p $(ps -o ppid= -p PID)`` + - same + - same + - + * - :meth:`Process.parents` + - ``pstree -s PID`` + - same + - same + - + * - :meth:`Process.children(recursive=False) ` + - ``pgrep -P PID`` + - same + - same + - + * - :meth:`Process.children(recursive=True) ` + - ``pstree -p PID`` + - same + - same + - + +Credentials +~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.uids` + - ``ps -o uid,ruid,suid -p PID`` + - same + - ``procstat -s PID`` + - + * - :meth:`Process.gids` + - ``ps -o gid,rgid,sgid -p PID`` + - same + - ``procstat -s PID`` + - + * - :meth:`Process.username` + - ``ps -o user -p PID`` + - same + - same + - + * - :meth:`Process.terminal` + - ``ps -o tty -p PID`` + - same + - same + - + +CPU / scheduling +~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.cpu_percent` + - ``ps -o %cpu -p PID`` + - same + - same + - + * - :meth:`Process.cpu_times` + - ``ps -o cputime -p PID`` + - same + - ``procstat -r PID`` + - + * - :meth:`Process.cpu_num` + - ``ps -o psr -p PID`` + - + - + - + * - :meth:`Process.num_ctx_switches` + - ``pidstat -w -p PID`` + - + - ``procstat -r PID`` + - + * - :meth:`Process.cpu_affinity() ` + - ``taskset -p PID`` + - + - ``cpuset -g -p PID`` + - + * - :meth:`Process.cpu_affinity(CPUS) ` + - ``taskset -p MASK PID`` + - + - ``cpuset -s -p PID -l CPUS`` + - + * - :meth:`Process.ionice() ` + - ``ionice -p PID`` + - + - + - + * - :meth:`Process.ionice(CLASS) ` + - ``ionice -c CLASS -p PID`` + - + - + - + * - :meth:`Process.nice() ` + - ``ps -o nice -p PID`` + - same + - same + - + * - :meth:`Process.nice(VALUE) ` + - ``renice -n VALUE -p PID`` + - same + - same + - + * - :meth:`Process.rlimit(RES) ` + - ``prlimit --pid PID`` + - + - ``procstat rlimit PID`` + - + * - :meth:`Process.rlimit(RES, LIMITS) ` + - ``prlimit --pid PID --RES=SOFT:HARD`` + - + - + - + +Memory +~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.memory_info` + - ``ps -o rss,vsz -p PID`` + - same + - same + - + * - :meth:`Process.memory_info_ex` + - ``cat /proc/PID/status`` + - + - + - + * - :meth:`Process.memory_percent` + - ``ps -o %mem -p PID`` + - same + - same + - + * - :meth:`Process.memory_maps` + - ``pmap PID`` + - ``vmmap PID`` + - ``procstat -v PID`` + - + * - :meth:`Process.memory_footprint` + - ``smem``, ``smemstat`` + - + - + - + * - :meth:`Process.page_faults` + - ``ps -o maj_flt,min_flt -p PID`` + - ``ps -o faults -p PID`` + - ``procstat -r PID`` + - + +Threads +~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.num_threads` + - ``ps -o nlwp -p PID`` + - same + - same + - + * - :meth:`Process.threads` + - ``ps -T -p PID`` + - + - + - + +Files and connections +~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.net_connections` + - ``ss -p``, ``lsof -p PID -i`` + - ``lsof -p PID -i`` + - + - ``netstat -ano | findstr PID`` + * - :meth:`Process.open_files` + - ``lsof -p PID`` + - same + - ``procstat -f PID`` + - ``handle.exe -p PID`` + * - :meth:`Process.io_counters` + - ``cat /proc/PID/io`` + - + - + - + * - :meth:`Process.num_fds` + - ``ls /proc/PID/fd | wc -l`` + - + - + - + * - :meth:`Process.num_handles` + - + - + - + - + +Signals +~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - psutil method + - Linux + - macOS + - BSD + - Windows + * - :meth:`Process.send_signal` + - ``kill -SIG PID`` + - same + - same + - + * - :meth:`Process.suspend` + - ``kill -STOP PID`` + - same + - same + - + * - :meth:`Process.resume` + - ``kill -CONT PID`` + - same + - same + - + * - :meth:`Process.terminate` + - ``kill -TERM PID`` + - same + - same + - ``taskkill /PID PID`` + * - :meth:`Process.kill` + - ``kill -KILL PID`` + - same + - same + - ``taskkill /F /PID PID`` + * - :meth:`Process.wait` + - ``tail --pid=PID -f /dev/null`` + - ``lsof -p PID +r 1`` + - ``pwait PID`` + - diff --git a/tests/test_linux.py b/tests/test_linux.py index 5ab64e47d6..bf519602d7 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -930,6 +930,68 @@ def test_ips(self): address = addr.address.split('%')[0] assert address in get_ipv6_addresses(name) + @pytest.mark.skipif( + not shutil.which("ip"), reason="'ip' command not available" + ) + @retry_on_failure() + def test_against_ip_addr_v4(self): + # Parse IPv4 addresses per interface from `ip addr` output and + # compare against psutil. Use the label at the end of each inet + # line as the interface name, since it reflects aliases like + # "vboxnet0:avahi" that psutil also uses as keys. + out = sh("ip addr") + ip_addrs = {} # {ifname: [addr, ...]} + for line in out.splitlines(): + # " inet 1.2.3.4/24 brd ... scope global eth0" + m = re.match(r'^\s+inet\s+(\S+).*\s+(\S+)$', line) + if m: + addr = m.group(1).split('/')[0] + ifname = m.group(2) + ip_addrs.setdefault(ifname, []).append(addr) + psutil_addrs = psutil.net_if_addrs() + for ifname, addrs in ip_addrs.items(): + if ifname not in psutil_addrs: + continue + psutil_ipv4 = { + a.address + for a in psutil_addrs[ifname] + if a.family == socket.AF_INET + } + for addr in addrs: + assert addr in psutil_ipv4 + + @pytest.mark.skipif( + not shutil.which("ip"), reason="'ip' command not available" + ) + @retry_on_failure() + def test_against_ip_addr_v6(self): + # Parse IPv6 addresses per interface from `ip addr` output and + # compare against psutil. Unlike inet, inet6 lines have no label, + # so the interface name comes from the header line. + out = sh("ip addr") + ip_addrs = {} # {ifname: [addr, ...]} + current_if = None + for line in out.splitlines(): + m = re.match(r'^\d+:\s+(\S+):', line) + if m: + current_if = m.group(1).rstrip(':') + m = re.match(r'^\s+inet6\s+(\S+)', line) + if m and current_if: + addr = m.group(1).split('/')[0] + ip_addrs.setdefault(current_if, []).append(addr) + psutil_addrs = psutil.net_if_addrs() + for ifname, addrs in ip_addrs.items(): + if ifname not in psutil_addrs: + continue + # psutil may append %ifname zone ID to link-local addresses. + psutil_ipv6 = { + a.address.split('%')[0] + for a in psutil_addrs[ifname] + if a.family == socket.AF_INET6 + } + for addr in addrs: + assert addr in psutil_ipv6 + # XXX - not reliable when having virtual NICs installed by Docker. # @pytest.mark.skipif(not shutil.which("ip"), # reason="'ip' utility not available") diff --git a/tests/test_windows.py b/tests/test_windows.py index 2f3d01fb81..1767a33f32 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -12,8 +12,8 @@ import os import platform import re -import shutil import signal +import socket import subprocess import sys import time @@ -68,18 +68,34 @@ def OpenProcess(self, pid=None): return handle -def powershell(cmd): - """Currently not used, but available just in case. Usage: +# Note used but could be useful in the future +def is_bash_env(): + env = os.environ + if "MSYSTEM" in env or "MINGW_PREFIX" in env or "MINGW_CHOST" in env: + return True + return "bash" in env.get("SHELL", "") + +def powershell(cmd): + """Run a powershell command and return its output. + Example usage: >>> powershell( - "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") - """ - if not shutil.which("powershell.exe"): - return pytest.skip("powershell.exe not available") - cmdline = ( - "powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive " - f"-NoProfile -WindowStyle Hidden -Command \"{cmd}\"" # noqa: Q003 + "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize" ) + """ + exe = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" + cmdline = [ + exe, + "-ExecutionPolicy", + "Bypass", + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-WindowStyle", + "Hidden", + "-Command", + cmd, + ] return sh(cmdline) @@ -155,6 +171,32 @@ def test_nic_names(self): f"{nic!r} nic wasn't found in 'ipconfig /all' output" ) + def test_net_if_addrs(self): + ps_addrs = set() + for addrs in psutil.net_if_addrs().values(): + for addr in addrs: + if addr.family == socket.AF_INET: + ps_addrs.add(addr.address) + out = powershell( + "(Get-NetIPAddress -AddressFamily IPv4).IPAddress -join ','" + ) + win_addrs = set(out.strip().split(',')) + assert win_addrs == ps_addrs + + def test_net_connections(self): + # Compare listening TCP ports; they're stable unlike active + # connections. + ps_ports = { + c.laddr.port + for c in psutil.net_connections(kind='tcp') + if c.status == psutil.CONN_LISTEN + } + out = powershell( + "(Get-NetTCPConnection -State Listen).LocalPort -join ','" + ) + win_ports = {int(p) for p in out.strip().split(',') if p.strip()} + assert ps_ports == win_ports + def test_total_phymem(self): w = wmi.WMI().Win32_ComputerSystem()[0] assert int(w.TotalPhysicalMemory) == psutil.virtual_memory().total @@ -355,6 +397,29 @@ def test_power_plugged(self): # https://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx assert battery_psutil.power_plugged == (battery_wmi.BatteryStatus == 2) + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_secsleft(self): + class SYSTEM_POWER_STATUS(ctypes.Structure): + _fields_ = [ + ('ACLineStatus', ctypes.c_byte), + ('BatteryFlag', ctypes.c_byte), + ('BatteryLifePercent', ctypes.c_byte), + ('SystemStatusFlag', ctypes.c_byte), + ('BatteryLifeTime', ctypes.c_ulong), + ('BatteryFullLifeTime', ctypes.c_ulong), + ] + + status = SYSTEM_POWER_STATUS() + ctypes.windll.kernel32.GetSystemPowerStatus(ctypes.byref(status)) + bat = psutil.sensors_battery() + if status.ACLineStatus == 1 or status.BatteryFlag & 8: + # plugged/charging + assert bat.secsleft == psutil.POWER_TIME_UNLIMITED + elif status.BatteryLifeTime == 0xFFFFFFFF: + assert bat.secsleft == psutil.POWER_TIME_UNKNOWN + else: + assert bat.secsleft == status.BatteryLifeTime + def test_emulate_no_battery(self): with mock.patch( "psutil._pswindows.cext.sensors_battery", @@ -566,6 +631,11 @@ def test_wait(self): win = win32process.GetExitCodeProcess(self.OpenProcess(self.pid)) assert ps == win + def test_num_threads(self): + ps = psutil.Process(self.pid).num_threads() + win = int(powershell(f"(Get-Process -Id {self.pid}).Threads.Count")) + assert ps == win + def test_cpu_affinity(self): def from_bitmask(x): return [i for i in range(64) if (1 << i) & x] @@ -691,6 +761,18 @@ def test_memory_vms(self): if vms not in {wmi_usage, wmi_usage * 1024}: return pytest.fail(f"wmi={wmi_usage}, psutil={vms}") + def test_cpu_times(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + ps = p.cpu_times() + assert abs(ps.user - int(w.UserModeTime) / 1e7) < 0.1 + assert abs(ps.system - int(w.KernelModeTime) / 1e7) < 0.1 + + def test_ppid(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + assert p.ppid() == int(w.ParentProcessId) + def test_create_time(self): w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] p = psutil.Process(self.pid) From 7d5b95bcc114d3e04c6447ab70c0350a683d43df Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 17 Mar 2026 21:11:18 +0100 Subject: [PATCH 1601/1714] Refact git hook script: - pass FILES to Makefile instead of re-definings linters logic - windows: drop support for colored output for cmd.exe --- Makefile | 17 +++-- psutil/_common.py | 50 ++++---------- scripts/internal/git_pre_commit.py | 105 ++++++++--------------------- 3 files changed, 52 insertions(+), 120 deletions(-) diff --git a/Makefile b/Makefile index b114a2d56e..1b227dc9f3 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ # Configurable PYTHON = python3 ARGS = +FILES = PIP_INSTALL_ARGS = --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade --upgrade-strategy eager PYTHON_ENV_VARS = PYTHONWARNINGS=always PYTHONUNBUFFERED=1 PSUTIL_DEBUG=1 PSUTIL_TESTING=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 @@ -168,24 +169,28 @@ coverage: ## Run test coverage. # Linters # =================================================================== +# Return a shell pipeline that outputs one file per line. Uses +# $(FILES) if set, else "git ls-files" with given pattern(s). +_ls = $(if $(FILES), printf '%s\n' $(FILES), git ls-files $(1)) + ruff: ## Run ruff linter. - @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --output-format=concise + @$(call _ls,'*.py') | xargs $(PYTHON) -m ruff check --output-format=concise black: ## Run black formatter. - @git ls-files '*.py' | xargs $(PYTHON) -m black --check --safe + @$(call _ls,'*.py') | xargs $(PYTHON) -m black --check --safe lint-c: ## Run C linter. - @git ls-files '*.c' '*.h' | xargs -P0 -I{} clang-format --dry-run --Werror {} + @$(call _ls,'*.c' '*.h') | xargs -P0 -I{} clang-format --dry-run --Werror {} dprint: @$(DPRINT) check lint-rst: ## Run linter for .rst files. - @git ls-files '*.rst' | xargs python3 scripts/internal/rst_check_dead_refs.py - @git ls-files '*.rst' | xargs sphinx-lint + @$(call _ls,'*.rst') | xargs python3 scripts/internal/rst_check_dead_refs.py + @$(call _ls,'*.rst') | xargs sphinx-lint lint-toml: ## Run linter for pyproject.toml. - @git ls-files '*.toml' | xargs toml-sort --check + @$(call _ls,'*.toml') | xargs toml-sort --check lint-all: ## Run all linters $(MAKE) black diff --git a/psutil/_common.py b/psutil/_common.py index 3393bff3cb..8fbfa78d61 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -676,19 +676,23 @@ def decode(s): @functools.lru_cache -def term_supports_colors(file=sys.stdout): # pragma: no cover - if not hasattr(file, "isatty") or not file.isatty(): +def term_supports_colors(force_color=False): + if WINDOWS: + return False + if force_color: + return True + if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty(): return False try: - file.fileno() + sys.stdout.fileno() except Exception: # noqa: BLE001 return False return True -def hilite(s, color=None, bold=False): # pragma: no cover +def hilite(s, color=None, bold=False, force_color=False): """Return an highlighted version of 'string'.""" - if not term_supports_colors(): + if not term_supports_colors(force_color=force_color): return s attr = [] colors = dict( @@ -698,7 +702,7 @@ def hilite(s, color=None, bold=False): # pragma: no cover green='32', grey='37', lightblue='36', - red='91', + red='31', violet='35', yellow='93', ) @@ -718,37 +722,9 @@ 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) - elif POSIX: - print(hilite(s, color, bold), file=file) - 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: - msg = f"invalid color {color!r}; choose between {list(colors)!r}" - raise ValueError(msg) from None - 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) - finally: - SetConsoleTextAttribute(handle, DEFAULT_COLOR) + if term_supports_colors(): + s = hilite(s, color=color, bold=bold) + print(s, file=file, flush=True) def debug(msg): diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py index 7be4c2e7f9..b98eab768d 100755 --- a/scripts/internal/git_pre_commit.py +++ b/scripts/internal/git_pre_commit.py @@ -12,7 +12,6 @@ import os import pathlib import shlex -import shutil import subprocess import sys @@ -24,11 +23,17 @@ hilite = _common.hilite PYTHON = sys.executable -LINUX = sys.platform.startswith("linux") + + +def log(msg="", color=None, bold=None): + msg = "Git pre-commit > " + msg + if msg: + msg = hilite(msg, color=color, bold=bold, force_color=True) + print(msg, flush=True) def exit_with(msg): - print(hilite("Commit aborted. " + msg, color="red"), file=sys.stderr) + log(msg + " Commit aborted.", color="red") sys.exit(1) @@ -45,7 +50,7 @@ def sh(cmd): if p.returncode != 0: raise RuntimeError(stderr) if stderr: - print(stderr, file=sys.stderr) + log(stderr) return stdout.rstrip() @@ -60,77 +65,12 @@ def git_commit_files(): c = [f for f in out if f.endswith((".c", ".h"))] rst = [f for f in out if f.endswith(".rst")] toml = [f for f in out if f.endswith(".toml")] - # XXX: we should escape spaces and possibly other amenities here new_rm_mv = sh( ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] ).split() return py, c, rst, toml, new_rm_mv -def run_cmd(base_cmd, files, tool, fixer=""): - if not files: - return - cmd = base_cmd + files - if subprocess.call(cmd) != 0: - msg = f"'{tool}' failed." - if fixer: - msg += f" Try running '{fixer}'." - exit_with(msg) - - -def black(files): - run_cmd( - [PYTHON, "-m", "black", "--check", "--safe"], - files, - "black", - fixer="fix-black", - ) - - -def ruff(files): - run_cmd( - [ - PYTHON, - "-m", - "ruff", - "check", - "--no-cache", - "--output-format=concise", - ], - files, - "ruff", - fixer="fix-ruff", - ) - - -def clang_format(files): - if not LINUX and not shutil.which("clang-format"): - return print("clang-format not installed; skip lint check") - run_cmd( - ["clang-format", "--dry-run", "--Werror"], - files, - "clang-format", - fixer="fix-c", - ) - - -def lint_toml(files): - run_cmd(["toml-sort", "--check"], files, "toml-sort", fixer="fix-toml") - - -def lint_rst(files): - run_cmd(["sphinx-lint"], files, "sphinx-lint") - - -def dprint(): - run_cmd( - ["dprint", "check", "--list-different"], - [], - "dprint", - fixer="fix-dprint", - ) - - def lint_manifest(): out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) with open("MANIFEST.in", encoding="utf8") as f: @@ -141,16 +81,27 @@ def lint_manifest(): ) -def main(): - py, c, rst, toml, new_rm_mv = git_commit_files() +def run_make(target, files): + ls = ", ".join([os.path.basename(x) for x in files]) + plural = "s" if len(files) > 1 else "" + msg = f"Running 'make {target}' against {len(files)} file{plural}: {ls}" + log(msg, color="lightblue") + files = "FILES=" + " ".join(shlex.quote(f) for f in files) + if subprocess.call(["make", target, files]) != 0: + exit_with(f"'make {target}' failed.") - black(py) - ruff(py) - clang_format(c) - lint_rst(rst) - lint_toml(toml) - dprint() +def main(): + py, c, rst, toml, new_rm_mv = git_commit_files() + if py: + run_make("black", py) + run_make("ruff", py) + if c: + run_make("lint-c", c) + if rst: + run_make("lint-rst", rst) + if toml: + run_make("lint-toml", toml) if new_rm_mv: lint_manifest() From 5555b91b0cb7d89dc2878599b582d3cd12c5a31f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 17 Mar 2026 22:55:03 +0100 Subject: [PATCH 1602/1714] RsT: define exceptions as :exc: instead of :class: --- docs/api.rst | 38 +++++++++++++++++++------------------- docs/changelog.rst | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 1ba449990b..545b315aa5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -702,7 +702,7 @@ Network ``PermissionError``. That means the returned list may be incomplete. .. note:: - (macOS and AIX) :class:`psutil.AccessDenied` is always raised unless running + (macOS and AIX) :exc:`psutil.AccessDenied` is always raised unless running as root. This is a limitation of the OS and ``lsof`` does the same. .. note:: @@ -1067,7 +1067,7 @@ Functions This function will return as soon as all processes terminate or when *timeout* (seconds) occurs. Differently from :meth:`Process.wait` it will not raise - :class:`TimeoutExpired` if timeout occurs. + :exc:`TimeoutExpired` if timeout occurs. A typical use case may be: - send SIGTERM to a list of processes @@ -1091,18 +1091,18 @@ Functions Exceptions ^^^^^^^^^^ -.. class:: Error() +.. exception:: Error() Base exception class. All other exceptions inherit from this one. -.. class:: NoSuchProcess(pid, name=None, msg=None) +.. exception:: NoSuchProcess(pid, name=None, msg=None) Raised by :class:`Process` class methods when no process with the given *pid* is found in the current process list, or when a process no longer exists. *name* is the name the process had before disappearing and gets set only if :meth:`Process.name` was previously called. -.. class:: ZombieProcess(pid, name=None, ppid=None, msg=None) +.. exception:: ZombieProcess(pid, name=None, ppid=None, msg=None) This may be raised by :class:`Process` class methods when querying a zombie process on UNIX (Windows doesn't have zombie processes). @@ -1112,19 +1112,19 @@ Exceptions .. note:: - this is a subclass of :class:`NoSuchProcess` so if you're not interested + this is a subclass of :exc:`NoSuchProcess` so if you're not interested in retrieving zombies (e.g. when using :func:`process_iter`) you can - ignore this exception and just catch :class:`NoSuchProcess`. + ignore this exception and just catch :exc:`NoSuchProcess`. .. versionadded:: 3.0.0 -.. class:: AccessDenied(pid=None, name=None, msg=None) +.. exception:: AccessDenied(pid=None, name=None, msg=None) Raised by :class:`Process` class methods when permission to perform an action is denied due to insufficient privileges. *name* attribute is available if :meth:`Process.name` was previously called. -.. class:: TimeoutExpired(seconds, pid=None, name=None, msg=None) +.. exception:: TimeoutExpired(seconds, pid=None, name=None, msg=None) Raised by :meth:`Process.wait` method if timeout expires and the process is still alive. @@ -1137,11 +1137,11 @@ Process class Represents an OS process with the given *pid*. If *pid* is omitted current process *pid* (`os.getpid`_) is used. - Raise :class:`NoSuchProcess` if *pid* does not exist. + Raise :exc:`NoSuchProcess` if *pid* does not exist. On Linux *pid* can also refer to a thread ID (the *id* field returned by :meth:`threads` method). When calling methods of this class, always be prepared to catch - :class:`NoSuchProcess` and :class:`AccessDenied` exceptions. + :exc:`NoSuchProcess` and :exc:`AccessDenied` exceptions. `hash`_ builtin can be used against instances of this class in order to identify a process univocally over time (the hash is determined by mixing process PID + creation time). As such it can also be used with `set`_. @@ -1351,7 +1351,7 @@ Process class If *attrs* argument is not passed all public read only attributes are assumed. *ad_value* is the value which gets assigned to a dict key in case - :class:`AccessDenied` or :class:`ZombieProcess` exception is raised when + :exc:`AccessDenied` or :exc:`ZombieProcess` exception is raised when retrieving that particular process information. Internally, :meth:`as_dict` uses :meth:`oneshot` context manager so there's no need you use it also. @@ -1368,8 +1368,8 @@ Process class ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] .. versionchanged:: 3.0.0 - *ad_value* is used also when incurring into :class:`ZombieProcess` - exception, not only :class:`AccessDenied`. + *ad_value* is used also when incurring into :exc:`ZombieProcess` + exception, not only :exc:`AccessDenied`. .. versionchanged:: 4.5.0 :meth:`as_dict` is considerably faster thanks to :meth:`oneshot` @@ -2247,7 +2247,7 @@ Process class .. warning:: On Linux, retrieving connections for certain processes requires root privileges. If psutil is not run as root, those connections are silently - skipped instead of raising :class:`psutil.AccessDenied`. That means + skipped instead of raising :exc:`psutil.AccessDenied`. That means the returned list may be incomplete. .. note:: @@ -2262,7 +2262,7 @@ Process class "". This is a limitation of the OS. .. note:: - (AIX) :class:`psutil.AccessDenied` is always raised unless running + (AIX) :exc:`psutil.AccessDenied` is always raised unless running as root (lsof does the same). .. versionchanged:: 5.3.0 @@ -2362,9 +2362,9 @@ Process class returned by `GetExitCodeProcess`_. *timeout* is expressed in seconds. If specified and the process is still - alive raise :class:`TimeoutExpired` exception. + alive raise :exc:`TimeoutExpired` exception. ``timeout=0`` can be used in non-blocking apps: it will either return - immediately or raise :class:`TimeoutExpired`. + immediately or raise :exc:`TimeoutExpired`. The return value is cached. To wait for multiple processes use :func:`psutil.wait_procs`. @@ -2542,7 +2542,7 @@ Windows services .. function:: win_service_get(name) Get a Windows service by name, returning a :class:`WindowsService` instance. - Raise :class:`psutil.NoSuchProcess` if no service with such name exists. + Raise :exc:`psutil.NoSuchProcess` if no service with such name exists. .. versionadded:: 4.2.0 diff --git a/docs/changelog.rst b/docs/changelog.rst index 6449eb21fd..59d4669b5e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -580,7 +580,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: :meth:`Process.ionice`, (set), :meth:`Process.cpu_affinity` (set), :meth:`Process.rlimit` (set), :meth:`Process.parent`. - :gh:`2308`, [OpenBSD]: :meth:`Process.threads` always fail with - AccessDenied (also as root). + :exc:`AccessDenied` (also as root). 5.9.5 — 2023-04-17 ^^^^^^^^^^^^^^^^^^ From 14277c0bae438b1595512e248c1f33631d0886d1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Mar 2026 11:06:51 +0100 Subject: [PATCH 1603/1714] Doc: re-add improved FAQs (#2769) I removed it some days ago during doc refactoring. Add it back improved + add more FAQs. --- MANIFEST.in | 1 + docs/Makefile | 2 +- docs/api.rst | 95 +++++++------- docs/changelog.rst | 4 +- docs/faq.rst | 316 +++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/recipes.rst | 13 +- 7 files changed, 372 insertions(+), 60 deletions(-) create mode 100644 docs/faq.rst diff --git a/MANIFEST.in b/MANIFEST.in index 0e4f0e95c6..be7a4ea279 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -29,6 +29,7 @@ include docs/changelog.rst include docs/conf.py include docs/credits.rst include docs/devguide.rst +include docs/faq.rst include docs/index.rst include docs/install.rst include docs/platform.rst diff --git a/docs/Makefile b/docs/Makefile index 34c17de526..c696ebdeef 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ PYTHON = python3 SPHINXBUILD = $(PYTHON) -m sphinx BUILDDIR = _build -ALLSPHINXOPTS = --fail-on-warning -d $(BUILDDIR)/doctrees . +ALLSPHINXOPTS = -d -fail-on-warning $(BUILDDIR)/doctrees . clean: ## Remove all build files rm -rf $(BUILDDIR) diff --git a/docs/api.rst b/docs/api.rst index 545b315aa5..bc8ae5f2e6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -58,6 +58,17 @@ CPU >>> psutil.cpu_times() scputimes(user=17411.7, system=3797.02, idle=51266.57, nice=77.99, iowait=732.58, irq=0.01, softirq=142.43, steal=0.0, guest=0.0, guest_nice=0.0) + .. note:: + 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 `#1210 + `_. + + .. warning:: + in version 8.0.0 the named tuple changed field order. Positional access + (e.g. ``cpu_times()[3]``) may silently return the wrong field. Always use + attribute access instead (e.g. ``cpu_times().idle``). + .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. @@ -65,16 +76,6 @@ CPU ``cpu_times()`` field order was standardized: ``user``, ``system``, ``idle`` are now always the first three fields. Previously on Linux, macOS, and BSD the first three were ``user``, ``nice``, ``system``. - .. warning:: - in version 8.0.0 the named tuple changed field order. Positional access - (e.g. ``cpu_times()[3]``) may silently return the wrong field. Always use - attribute access instead (e.g. ``cpu_times().idle``). - - .. warning:: - 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 `#1210 - `_. .. function:: cpu_percent(interval=None, percpu=False) @@ -110,10 +111,10 @@ CPU [2.0, 1.0] >>> - .. warning:: + .. note:: the first time this function is called with *interval* = ``0.0`` or ``None`` it will return a meaningless ``0.0`` value which you are supposed to - ignore. + ignore. See also :ref:`faq_cpu_percent` FAQ. .. versionchanged:: 5.9.6 the function is now thread safe. @@ -128,10 +129,10 @@ CPU On Linux "guest" and "guest_nice" percentages are not accounted in "user" and "user_nice" percentages. - .. warning:: + .. note:: the first time this function is called with *interval* = ``0.0`` or ``None`` it will return a meaningless ``0.0`` value which you are supposed - to ignore. + to ignore. See also :ref:`faq_cpu_percent` FAQ. .. versionchanged:: 4.1.0 two new *interrupt* and *dpc* fields are returned on Windows. @@ -309,14 +310,6 @@ Memory - **wired** *(macOS, BSD)*: memory pinned in RAM by the kernel (e.g. kernel code and critical data structures). It can never be moved to disk. - .. note:: - - On Linux, **total**, **free**, **used**, **shared**, and **available** - match the output of the ``free`` command. - - On macOS, **free**, **active**, **inactive**, and **wired** match - ``vm_stat`` output. - - On Windows, **total**, **used** ("In use"), and **available** match - the Task Manager (Performance > Memory tab). - Below is a table showing implementation details. All info on Linux is retrieved from `/proc/meminfo`. .. list-table:: @@ -402,6 +395,14 @@ Memory cross-platform manner, simply rely on **available** and **percent** fields. + .. note:: + - On Linux, **total**, **free**, **used**, **shared**, and **available** + match the output of the ``free`` command. + - On macOS, **free**, **active**, **inactive**, and **wired** match + ``vm_stat`` output. + - On Windows, **total**, **used** ("In use"), and **available** match + the Task Manager (Performance > Memory tab). + .. note:: see `meminfo.py`_ script providing an example on how to convert bytes in a human readable form. @@ -1102,6 +1103,8 @@ Exceptions exists. *name* is the name the process had before disappearing and gets set only if :meth:`Process.name` was previously called. + See also :ref:`faq_no_such_process` FAQ. + .. exception:: ZombieProcess(pid, name=None, ppid=None, msg=None) This may be raised by :class:`Process` class methods when querying a zombie @@ -1110,6 +1113,8 @@ Exceptions :meth:`Process.ppid` methods were called before the process turned into a zombie. + See also :ref:`faq_zombie_process` FAQ. + .. note:: this is a subclass of :exc:`NoSuchProcess` so if you're not interested @@ -1124,6 +1129,8 @@ Exceptions action is denied due to insufficient privileges. *name* attribute is available if :meth:`Process.name` was previously called. + See also :ref:`faq_access_denied` FAQ. + .. exception:: TimeoutExpired(seconds, pid=None, name=None, msg=None) Raised by :meth:`Process.wait` method if timeout expires and the process is @@ -1156,23 +1163,12 @@ Process class the way this class is bound to a process is via its **PID**. That means that if the process terminates and the OS reuses its PID you may - inadvertently end up querying another process. To prevent this problem - you can use :meth:`is_running` first. - The only methods which preemptively check whether PID has been reused - (via PID + creation time) are: - :meth:`nice` (set), - :meth:`ionice` (set), - :meth:`cpu_affinity` (set), - :meth:`rlimit` (set), - :meth:`children`, - :meth:`ppid`, - :meth:`parent`, - :meth:`parents`, - :meth:`suspend` - :meth:`resume`, - :meth:`send_signal`, - :meth:`terminate` and - :meth:`kill`. + inadvertently end up interacting with another process. To prevent this + problem you can use :meth:`is_running` first. + Some methods (e.g. setters and signal-related methods) perform an + additional check based on PID + creation time and will raise + :exc:`NoSuchProcess` if the PID has been reused. See :ref:`faq_pid_reuse` + FAQ for details. .. method:: oneshot() @@ -1694,6 +1690,11 @@ Process class >>> p.cpu_percent(interval=None) 2.9 + .. note:: + the first time this method is called with interval = ``0.0`` or + ``None`` it will return a meaningless ``0.0`` value which you are + supposed to ignore. See also :ref:`faq_cpu_percent` FAQ. + .. note:: the returned value can be > 100.0 in case of a process running multiple threads on different CPU cores. @@ -1703,7 +1704,7 @@ Process class CPUs (differently from :func:`psutil.cpu_percent`). This means that a busy loop process running on a system with 2 logical CPUs will be reported as having 100% CPU utilization instead of 50%. - This was done in order to be consistent with ``top`` UNIX utility + This was done in order to be consistent with ``top`` UNIX utility, and also to make it easier to identify processes hogging CPU resources independently from the number of CPUs. It must be noted that ``taskmgr.exe`` on Windows does not behave like @@ -1711,11 +1712,6 @@ Process class To emulate Windows ``taskmgr.exe`` behavior you can do: ``p.cpu_percent() / psutil.cpu_count()``. - .. warning:: - the first time this method is called with interval = ``0.0`` or - ``None`` it will return a meaningless ``0.0`` value which you are - supposed to ignore. - .. method:: cpu_affinity(cpus=None) Get or set process current @@ -1795,6 +1791,7 @@ Process class currently held by this process (code, data, stack, and mapped files that are resident). Pages swapped out to disk are **not** counted. On UNIX it matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. + See also :ref:`faq_memory_rss_vs_vms` FAQ. - **vms**: aka "Virtual Memory Size". The total address space reserved by the process, including pages not yet touched, pages in swap, and @@ -2285,10 +2282,10 @@ Process class .. method:: is_running() Return whether the current process is running in the current process list. - This is reliable also in case the process is gone and its PID reused by - another process, therefore it must be preferred over doing - ``psutil.pid_exists(p.pid)``. - If PID has been reused this method will also remove the process from + Differently from ``psutil.pid_exists(p.pid)``, this is reliable also in + case the process is gone and its PID reused by another process. + + If PID has been reused, this method will also remove the process from :func:`process_iter` internal cache. .. note:: diff --git a/docs/changelog.rst b/docs/changelog.rst index 59d4669b5e..98cd32dabf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,7 +9,7 @@ Changelog **Enhancements** - Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - :gh:`2764`, :gh:`2767`, :gh:`2768`): + :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`): - Split docs from a single HTML file into multiple sections (API reference, install, etc.). @@ -28,6 +28,8 @@ Changelog list contributors and donors (was old ``CREDITS`` in root dir) - `/platform `__: summary of OSes and architectures support + - `/faq `__: + extended FAQ section. - Usability: diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000000..5abb7cc98e --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,316 @@ +.. currentmodule:: psutil + +FAQ +=== + +This section answers common questions and pitfalls when using psutil. + +.. contents:: + :local: + :depth: 3 + +Exceptions +---------- + +.. _faq_access_denied: + +Why do I get AccessDenied? +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:exc:`AccessDenied` is raised when the OS refuses to return information about +a process because the calling user does not have sufficient privileges. +This is expected behavior and is not a bug. It typically happens when: + +- querying processes owned by other users (e.g. *root*) +- calling certain methods like :meth:`Process.memory_maps`, + :meth:`Process.open_files` or :meth:`Process.net_connections` for privileged + processes + +You have two options to deal with it. + +- Option 1: call the method directly and catch the exception: + + .. code-block:: python + + import psutil + + p = psutil.Process(pid) + try: + print(p.memory_maps()) + except (psutil.AccessDenied, psutil.NoSuchProcess): + pass + +- Option 2: use :func:`process_iter` with a list of attribute names to pre-fetch. If + fetching an attribute raises :exc:`AccessDenied` internally, its value in + ``p.info`` is set to ``None`` (or to the ``ad_value`` argument, if specified): + + .. code-block:: python + + import psutil + + for p in psutil.process_iter(["name", "username"], ad_value="N/A"): + print(p.info["username"]) # may print "N/A" + +.. _faq_no_such_process: + +Why do I get NoSuchProcess? +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:exc:`NoSuchProcess` is raised when a process no longer exists. +The most common cause is a TOCTOU (time-of-check / time-of-use) race +condition: a process can die between the moment its PID is obtained and +the moment it is queried. The following 2 naive patterns are racy: + +.. code-block:: python + + import psutil + + for pid in psutil.pids(): + p = psutil.Process(pid) # may raise NoSuchProcess + print(p.name()) # may raise NoSuchProcess + +.. code-block:: python + + import psutil + + if psutil.pid_exists(pid): + p = psutil.Process(pid) # may raise NoSuchProcess + print(p.name()) # may raise NoSuchProcess + +The correct approach is to use :func:`process_iter`, which handles +:exc:`NoSuchProcess` internally and skips processes that disappear +during iteration: + +.. code-block:: python + + import psutil + + for p in psutil.process_iter(["name"]): + print(p.info["name"]) + +If you have a specific PID (e.g. a known child process), wrap the +call in a try/except: + +.. code-block:: python + + import psutil + + try: + p = psutil.Process(pid) + print(p.name(), p.status()) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + +An even simpler pattern is to catch :exc:`Error`, which implies both +:exc:`AccessDenied` and :exc:`NoSuchProcess`: + +.. code-block:: python + + import psutil + + try: + p = psutil.Process(pid) + print(p.name(), p.status()) + except psutil.Error: + pass + +.. _faq_zombie_process: + +What is ZombieProcess? +^^^^^^^^^^^^^^^^^^^^^^^ + +:exc:`ZombieProcess` is a subclass of :exc:`NoSuchProcess` that is raised +on UNIX when a process has terminated but has not yet been reaped by its +parent. The process has finished executing but its entry remains in the +process table until the parent calls ``wait()`` (or the parent itself +exits). + +**What you can and cannot do with a zombie:** + +- A zombie can be instantiated via :class:`Process` (pid) without error. +- :meth:`Process.status` always returns :data:`STATUS_ZOMBIE`. +- :meth:`Process.is_running` and :func:`pid_exists` return ``True``. +- The zombie appears in :func:`process_iter` and :func:`pids`. +- Sending signals (:meth:`Process.terminate`, :meth:`Process.kill`, + etc.) has no effect. +- Most other methods (:meth:`Process.cmdline`, :meth:`Process.exe`, + :meth:`Process.memory_maps`, etc.) may raise :exc:`ZombieProcess`, + return a meaningful value, or return a null/empty value depending on + the platform. +- :meth:`Process.as_dict` will not crash. + +**How to detect zombies:** + +.. code-block:: python + + import psutil + + for p in psutil.process_iter(["status"]): + if p.info["status"] == psutil.STATUS_ZOMBIE: + print(f"zombie: pid={p.pid}") + +**How to get rid of a zombie:** the only way is to have its parent +process call ``wait()`` (or ``waitpid()``). If the parent never does +this, killing the parent will cause the zombie to be re-parented to +``init`` / ``systemd``, which will reap it automatically. + +---- + +Processes +--------- + +.. _faq_pid_reuse: + +PID reuse +^^^^^^^^^ + +Operating systems recycle PIDs. A :class:`Process` object obtained now may +later refer to a different process if the original one terminated and a new one +was assigned the same PID. + +**How psutil handles this:** + +- *Most read-only methods* (e.g. :meth:`Process.name`, + :meth:`Process.cpu_percent`) do **not** check for PID reuse and instead + query whatever process currently holds that PID. + +- *Signal methods* (e.g. :meth:`Process.send_signal`, + :meth:`Process.suspend`, :meth:`Process.resume`, + :meth:`Process.terminate`, :meth:`Process.kill`) **do** check for PID + reuse (via PID + creation time) before acting, raising + :exc:`NoSuchProcess` if the PID was recycled. This prevents accidentally + killing the wrong process (`BPO-6973 + `_). + +- *Set methods* :meth:`Process.nice` (set), :meth:`Process.ionice` (set), + :meth:`Process.cpu_affinity` (set), and + :meth:`Process.rlimit` (set) also perform this check before applying + changes. + +:meth:`Process.is_running` is the recommended way to verify whether a +:class:`Process` instance still refers to the same process. It compares +PID and creation time, and returns ``False`` if the PID was reused. +Prefer it over :func:`pid_exists`. + +.. _faq_pid_exists_vs_isrunning: + +What is the difference between pid_exists() and Process.is_running()? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:func:`pid_exists` checks whether a PID is present in the process list. +:meth:`Process.is_running` does the same, but also detects :ref:`PID +reuse ` by comparing the process creation time. Use +:func:`pid_exists` when you have a bare PID and don't need to guard +against reuse (it's faster). Use :meth:`Process.is_running` when you +hold a :class:`Process` object and want to confirm it still refers to +the same process. + +---- + +CPU +--- + +.. _faq_cpu_percent: + +Why does cpu_percent() return 0.0 on first call? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:func:`cpu_percent` (and :meth:`Process.cpu_percent`) measures CPU usage +*between two calls*. The very first call has no prior sample to compare +against, so it always returns ``0.0``. The fix is to call it once to +initialize the baseline, discard the result, then call it again after a +short sleep: + +.. code-block:: python + + import time + import psutil + + psutil.cpu_percent() # discard first call + time.sleep(0.5) + print(psutil.cpu_percent()) # meaningful value + +Alternatively, pass ``interval`` to make it block internally: + +.. code-block:: python + + print(psutil.cpu_percent(interval=0.5)) + +The same applies to :meth:`Process.cpu_percent`: + +.. code-block:: python + + p = psutil.Process() + p.cpu_percent() # discard + time.sleep(0.5) + print(p.cpu_percent()) # meaningful value + +.. _faq_cpu_percent_gt_100: + +Can Process.cpu_percent() return a value higher than 100%? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Yes. On a multi-core system a process can run threads on several CPUs at +the same time. The maximum value is ``psutil.cpu_count() * 100``. For +example, on a 4-core machine a fully-loaded process can reach 400%. +The system-wide :func:`cpu_percent` (without a :class:`Process`) always +stays in the 0–100% range because it averages across all cores. + +---- + +Memory +------ + +.. _faq_virtual_memory_available: + +What is the difference between virtual_memory().available and virtual_memory().free? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:func:`virtual_memory` returns both ``free`` and ``available``, but they +measure different things: + +- ``free``: memory that is not being used at all. +- ``available``: how much memory can be given to processes without swapping. + This includes reclaimable caches and buffers that the OS can reclaim under + pressure. + +In practice, ``available`` is almost always the metric you want when monitoring +memory. ``free`` can be misleadingly low on systems where the OS aggressively +uses RAM for caches (which is normal and healthy). On Windows, ``free`` and +``available`` are the same value. + +.. _faq_memory_rss_vs_vms: + +What is the difference between RSS and VMS? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- ``rss`` (Resident Set Size): the amount of physical memory (RAM) + currently mapped into the process. +- ``vms`` (Virtual Memory Size): the total virtual address space of the + process, including memory that has been swapped out, shared libraries, + and memory-mapped files. + +``rss`` is the go-to metric for answering "how much RAM is this process +using?". Note that it includes shared memory, so it may overestimate +actual usage when compared across processes. ``vms`` is generally larger +and can be misleadingly high, as it includes memory that is not resident +in physical RAM. + +Both values are portable across platforms and are returned by +:meth:`Process.memory_info`. + +.. _faq_memory_footprint: + +When should I use memory_footprint() vs memory_info()? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`Process.memory_info` returns ``rss``, which includes shared +libraries counted in every process that uses them. For example, if +``libc`` uses 2 MB and 100 processes map it, each process includes those +2 MB in its ``rss``. + +:meth:`Process.memory_footprint` returns USS (Unique Set Size), i.e. +memory private to the process. It represents the amount of memory that +would be freed if the process were terminated. +It is more accurate than RSS, but substantially slower and requires higher +privileges. On Linux it also returns PSS (Proportional Set Size) and swap. diff --git a/docs/index.rst b/docs/index.rst index 6c781dcb77..a921a06328 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,6 +98,7 @@ Table of Contents Install API Reference + FAQ Recipes Shell equivalents Platform support diff --git a/docs/recipes.rst b/docs/recipes.rst index 6373cf05a9..21e67eede0 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -278,21 +278,16 @@ Kill a process tree (including grandchildren): ---- -Kill / reap zombie (defunct) processes: +Find zombie (defunct) processes: .. code-block:: python import psutil - def kill_zombies(): - for p in psutil.process_iter(["status"]): - if p.info["status"] == psutil.STATUS_ZOMBIE: - parent = p.parent() - if parent: - parent.terminate() - parent.wait() - p.wait() + for p in psutil.process_iter(["status"]): + if p.info["status"] == psutil.STATUS_ZOMBIE: + print(f"zombie: pid={p.pid}") ---- From deba6b8c9d69b1b9cacbebd874dd284b8680cf13 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Mar 2026 11:10:03 +0100 Subject: [PATCH 1604/1714] fix sphinx arg Signed-off-by: Giampaolo Rodola --- docs/Makefile | 2 +- docs/faq.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index c696ebdeef..34c17de526 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ PYTHON = python3 SPHINXBUILD = $(PYTHON) -m sphinx BUILDDIR = _build -ALLSPHINXOPTS = -d -fail-on-warning $(BUILDDIR)/doctrees . +ALLSPHINXOPTS = --fail-on-warning -d $(BUILDDIR)/doctrees . clean: ## Remove all build files rm -rf $(BUILDDIR) diff --git a/docs/faq.rst b/docs/faq.rst index 5abb7cc98e..bbabd0f43e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -199,7 +199,7 @@ What is the difference between pid_exists() and Process.is_running()? :func:`pid_exists` checks whether a PID is present in the process list. :meth:`Process.is_running` does the same, but also detects :ref:`PID -reuse ` by comparing the process creation time. Use +reuse ` by comparing the process creation time. Use :func:`pid_exists` when you have a bare PID and don't need to guard against reuse (it's faster). Use :meth:`Process.is_running` when you hold a :class:`Process` object and want to confirm it still refers to From 989cea2e3a8a9fc859b154cf0b6931c89e182975 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 18 Mar 2026 23:23:25 +0100 Subject: [PATCH 1605/1714] Doc: add migration section (#2771) --- MANIFEST.in | 2 + docs/_links.rst | 36 +++ docs/api.rst | 73 ++---- docs/changelog.rst | 227 ++++++++--------- docs/faq.rst | 6 +- docs/index.rst | 1 + docs/migration.rst | 308 ++++++++++++++++++++++++ scripts/internal/rst_check_dead_refs.py | 46 +++- 8 files changed, 509 insertions(+), 190 deletions(-) create mode 100644 docs/_links.rst create mode 100644 docs/migration.rst diff --git a/MANIFEST.in b/MANIFEST.in index be7a4ea279..14d9243a19 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,6 +18,7 @@ include docs/_ext/add_home_link.py include docs/_ext/availability.py include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py +include docs/_links.rst include docs/_static/copybutton.js include docs/_static/css/custom.css include docs/_static/favicon.ico @@ -32,6 +33,7 @@ include docs/devguide.rst include docs/faq.rst include docs/index.rst include docs/install.rst +include docs/migration.rst include docs/platform.rst include docs/recipes.rst include docs/requirements.txt diff --git a/docs/_links.rst b/docs/_links.rst new file mode 100644 index 0000000000..03aa1a659b --- /dev/null +++ b/docs/_links.rst @@ -0,0 +1,36 @@ +.. === docs.python.org + +.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 +.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET +.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX +.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum +.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum +.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum +.. _`hash`: https://docs.python.org/3/library/functions.html#hash +.. _`open`: https://docs.python.org/3/library/functions.html#open +.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count +.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg +.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid +.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority +.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid +.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid +.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY +.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC +.. _`os.open`: https://docs.python.org/3/library/os.html#os.open +.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open +.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority +.. _`os.times`: https://docs.python.org//library/os.html#os.times +.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit +.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit +.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue +.. _`select.poll`: https://docs.python.org//library/select.html#select.poll +.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set +.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage +.. _`signal module`: https://docs.python.org//library/signal.html +.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM +.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET +.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM +.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd +.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen +.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident +.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread diff --git a/docs/api.rst b/docs/api.rst index bc8ae5f2e6..dfe91a0c07 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,9 +1,14 @@ .. currentmodule:: psutil +.. include:: _links.rst .. _availability: API reference ============= +.. note:: + psutil 8.0 introduces breaking API changes. See the + :ref:`migration guide ` if upgrading from 7.x. + .. contents:: :local: :depth: 5 @@ -64,11 +69,6 @@ CPU this might not be the case (at least on Windows and Linux), see `#1210 `_. - .. warning:: - in version 8.0.0 the named tuple changed field order. Positional access - (e.g. ``cpu_times()[3]``) may silently return the wrong field. Always use - attribute access instead (e.g. ``cpu_times().idle``). - .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. @@ -76,6 +76,7 @@ CPU ``cpu_times()`` field order was standardized: ``user``, ``system``, ``idle`` are now always the first three fields. Previously on Linux, macOS, and BSD the first three were ``user``, ``nice``, ``system``. + See :ref:`migration guide `. .. function:: cpu_percent(interval=None, percpu=False) @@ -728,6 +729,7 @@ Network .. versionchanged:: 8.0.0 *status* field is now a :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + See :ref:`migration guide `. .. function:: net_if_addrs() @@ -1395,6 +1397,7 @@ Process class .. versionchanged:: 8.0.0 return value is now a :class:`psutil.ProcessStatus` enum member instead of a plain ``str``. + See :ref:`migration guide `. .. method:: cwd() @@ -1462,6 +1465,7 @@ Process class .. versionchanged:: 8.0.0 on Windows, return value is now a :class:`psutil.ProcessPriority` enum member. + See :ref:`migration guide `. .. method:: ionice(ioclass=None, value=None) @@ -1515,6 +1519,7 @@ Process class .. versionchanged:: 8.0.0 *ioclass* is now a :class:`psutil.ProcessIOPriority` enum member. + See :ref:`migration guide `. .. method:: rlimit(resource, limits=None) @@ -1789,7 +1794,7 @@ Process class - **rss**: aka "Resident Set Size". The portion of physical memory currently held by this process (code, data, stack, and mapped files that - are resident). Pages swapped out to disk are **not** counted. On UNIX it + are resident). Pages swapped out to disk are not counted. On UNIX it matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. See also :ref:`faq_memory_rss_vs_vms` FAQ. @@ -1844,10 +1849,12 @@ Process class .. versionchanged:: 8.0.0 Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated aliases returning 0 and emitting `DeprecationWarning` are kept. + See :ref:`migration guide `. .. versionchanged:: 8.0.0 - macOS: *pfaults* and *pageins* removed with **no backward-compat + macOS: *pfaults* and *pageins* removed with **no backward-compatible aliases**. Use :meth:`page_faults` instead. + See :ref:`migration guide `. .. versionchanged:: 8.0.0 Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → @@ -1856,16 +1863,11 @@ Process class time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, *peak_nonpaged_pool* were moved to :meth:`memory_info_ex`. All these old names still work but raise `DeprecationWarning`. + See :ref:`migration guide `. .. versionchanged:: 8.0.0 BSD: added *peak_rss*. - .. warning:: - in version 8.0.0 the named tuple changed size and field order. Positional - access (e.g. ``p.memory_info()[3]`` or ``a, b, c = p.memory_info()``) may - break or silently return the wrong field. Always use attribute access - instead (e.g. ``p.memory_info().rss``). - .. method:: memory_info_ex() Return a named tuple extending :meth:`memory_info` with additional @@ -1983,6 +1985,7 @@ Process class .. deprecated:: 8.0.0 use :meth:`memory_footprint` instead. + See :ref:`migration guide `. .. method:: memory_percent(memtype="rss") @@ -2271,6 +2274,7 @@ Process class .. versionchanged:: 8.0.0 *status* field is now a :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. + See :ref:`migration guide `. .. method:: connections() @@ -2749,6 +2753,7 @@ Process status constants .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessStatus` enum members (were plain strings). + See :ref:`migration guide `. Process priority constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2771,6 +2776,7 @@ Process priority constants .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessPriority` enum members (were plain integers). + See :ref:`migration guide `. .. _const-ioprio: .. data:: IOPRIO_CLASS_NONE @@ -2798,6 +2804,7 @@ Process priority constants .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessIOPriority` enum members (previously ``IOPriority`` enum). + See :ref:`migration guide `. .. data:: IOPRIO_VERYLOW .. data:: IOPRIO_LOW @@ -2817,6 +2824,7 @@ Process priority constants .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessIOPriority` enum members (previously ``IOPriority`` enum). + See :ref:`migration guide `. Process resource constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2864,6 +2872,7 @@ These constants are members of the :class:`psutil.ProcessRlimit` enum. .. versionchanged:: 8.0.0 constants are now :class:`psutil.ProcessRlimit` enum members (were plain integers). + See :ref:`migration guide `. Connections constants ^^^^^^^^^^^^^^^^^^^^^ @@ -2893,6 +2902,7 @@ Connections constants .. versionchanged:: 8.0.0 constants are now :class:`psutil.ConnectionStatus` enum members (were plain strings). + See :ref:`migration guide `. Hardware constants ^^^^^^^^^^^^^^^^^^ @@ -2975,43 +2985,6 @@ Other constants .. _`man prlimit`: https://linux.die.net/man/2/prlimit .. _`psleak`: https://github.com/giampaolo/psleak -.. === docs.python.org - -.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 -.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET -.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX -.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum -.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum -.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum -.. _`hash`: https://docs.python.org/3/library/functions.html#hash -.. _`open`: https://docs.python.org/3/library/functions.html#open -.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count -.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg -.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid -.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority -.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid -.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid -.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY -.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC -.. _`os.open`: https://docs.python.org/3/library/os.html#os.open -.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open -.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority -.. _`os.times`: https://docs.python.org//library/os.html#os.times -.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit -.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit -.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue -.. _`select.poll`: https://docs.python.org//library/select.html#select.poll -.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set -.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage -.. _`signal module`: https://docs.python.org//library/signal.html -.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM -.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET -.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM -.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd -.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen -.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident -.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread - .. === scripts .. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 98cd32dabf..a87dce93f5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,110 +8,116 @@ Changelog **Enhancements** -- Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`): - - - Split docs from a single HTML file into multiple sections (API reference, - install, etc.). - - - Added new sections: - - - `/recipes `__: - show code samples - - `/adoption `__: - notable software using psutil - - `/shell_equivalents `__: - maps each psutil API to native CLI commands - - `/install `__ - (was old ``INSTALL.rst`` in root dir) - - `/credits `__: - list contributors and donors (was old ``CREDITS`` in root dir) - - `/platform `__: - summary of OSes and architectures support - - `/faq `__: - extended FAQ section. - - - Usability: - - - Show a clickable COPY button to copy code snippets. - - Show ``psutil.`` prefix for all APIs. - - Use sphinx extension to validate Python code snippets syntax at build-time. - - - Testing: - - - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting. - - Add custom script to detect dead reference links in ``.rst`` files. - - Build doc as part of CI process. - - - Greatly improved :func:`virtual_memory` doc. - -- Type hints / enums: - - - :gh:`1946`: Add inline type hints to all public APIs in `psutil/__init__.py`. - Type checkers (mypy, pyright, etc.) can now statically verify code that uses - psutil. No runtime behavior is changed; the annotations are purely - informational. - - :gh:`2751`: Convert all named tuples from ``collections.namedtuple`` to - ``typing.NamedTuple`` classes with **type annotations**. This makes the - classes self-documenting, effectively turning this module into a readable - API reference. - - :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, - :class:`ConnectionStatus`, - :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) - grouping related constants. The individual top-level constants (e.g. - ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases - for the corresponding enum members. +Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, +:gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`) + +- Split docs from a single HTML file into multiple sections (API reference, + install, etc.). + +- Added new sections: + + - `/recipes `__: + show code samples + - `/adoption `__: + notable software using psutil + - `/shell_equivalents `__: + maps each psutil API to native CLI commands + - `/install `__ + (was old ``INSTALL.rst`` in root dir) + - `/credits `__: + list contributors and donors (was old ``CREDITS`` in root dir) + - `/platform `__: + summary of OSes and architectures support + - `/faq `__: + extended FAQ section. + - `/migration `__: a + section explaining how to migrate to newer psutil versions that break + backward compatibility. + +- Usability: + + - Show a clickable COPY button to copy code snippets. + - Show ``psutil.`` prefix for all APIs. + - Use sphinx extension to validate Python code snippets syntax at build-time. + +- Testing: + + - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting. + - Add custom script to detect dead reference links in ``.rst`` files. + - Build doc as part of CI process. + +- Greatly improved :func:`virtual_memory` doc. + +Type hints / enums: + +- :gh:`1946`: Add inline type hints to all public APIs in `psutil/__init__.py`. + Type checkers (mypy, pyright, etc.) can now statically verify code that uses + psutil. No runtime behavior is changed; the annotations are purely + informational. +- :gh:`2751`: Convert all named tuples from ``collections.namedtuple`` to + ``typing.NamedTuple`` classes with **type annotations**. This makes the + classes self-documenting, effectively turning this module into a readable + API reference. +- :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, + :class:`ConnectionStatus`, + :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) + grouping related constants. The individual top-level constants (e.g. + ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases + for the corresponding enum members. - New APIs: - - :gh:`2729`: New :meth:`Process.page_faults` method, returning a ``(minor, - major)`` namedtuple. - - Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`, - :gh:`2733`). +- :gh:`2729`: New :meth:`Process.page_faults` method, returning a ``(minor, + major)`` namedtuple. +- Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`, + :gh:`2733`). - - Add new :meth:`Process.memory_info_ex` method, which extends - :meth:`Process.memory_info` with platform-specific metrics: + - Add new :meth:`Process.memory_info_ex` method, which extends + :meth:`Process.memory_info` with platform-specific metrics: - - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, - *swap*, *hugetlb* - - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, - *phys_footprint* - - Windows: *virtual*, *peak_virtual* + - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, + *swap*, *hugetlb* + - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, + *phys_footprint* + - Windows: *virtual*, *peak_virtual* - - Add new :meth:`Process.memory_footprint` method, which returns *uss*, - *pss* and *swap* metrics (what :meth:`Process.memory_full_info` used to - return, which is now **deprecated**). + - Add new :meth:`Process.memory_footprint` method, which returns *uss*, + *pss* and *swap* metrics (what :meth:`Process.memory_full_info` used to + return, which is now **deprecated**, see + :ref:`migration guide `). - - :meth:`Process.memory_info` named tuple changed: + - :meth:`Process.memory_info` named tuple changed: - - BSD: added *peak_rss*. + - BSD: added *peak_rss*. - - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated - aliases returning 0 and emitting `DeprecationWarning` are kept. + - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated + aliases returning 0 and emitting `DeprecationWarning` are kept. - - macOS: *pfaults* and *pageins* removed with **no - backward-compataliases**. Use :meth:`Process.page_faults` instead. + - macOS: *pfaults* and *pageins* removed with **no + backward-compataliases**. Use :meth:`Process.page_faults` instead. - - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. - At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, - *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex`. All - these old names still work but raise `DeprecationWarning`. + - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → + *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. + At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, + *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex`. All + these old names still work but raise `DeprecationWarning`. + See :ref:`migration guide `. - - :meth:`Process.memory_full_info` is **deprecated**. Use the new - :meth:`Process.memory_footprint` instead. + - :meth:`Process.memory_full_info` is **deprecated**. Use the new + :meth:`Process.memory_footprint` instead. + See :ref:`migration guide `. -- Others: +Others - - :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times` - has been normalized on all platforms, and the first 3 fields are now always - ``user, system, idle``. See compatibility notes below. - - :gh:`2754`: standardize :func:`sensors_battery`'s `percent` so that it - returns a `float` instead of `int` on all systems, not only Linux. - - :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update - changelog.rst and credits.rst when commenting with /changelog. - - :gh:`2766`: remove remaining Python 2.7 compatibility shims from - ``setup.py``, simplifying the build infrastructure. +- :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times` + has been normalized on all platforms, and the first 3 fields are now always + ``user, system, idle``. See compatibility notes below. +- :gh:`2754`: standardize :func:`sensors_battery`'s `percent` so that it + returns a `float` instead of `int` on all systems, not only Linux. +- :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update + ``changelog.rst`` and ``credits.rst`` when commenting with /changelog. +- :gh:`2766`: remove remaining Python 2.7 compatibility shims from + ``setup.py``, simplifying the build infrastructure. **Bug fixes** @@ -129,45 +135,8 @@ Changelog **Compatibility notes** -Changes that break backwards compatibility. - -- Dropped support for Python 3.6. - -Named tuples: - -- :func:`cpu_times`: - - - On Linux, macOS and BSD the field order of the returned named tuple - changed: ``user, system, idle`` are now always the first 3 fields on all - platforms, with platform-specific fields (e.g. ``nice``) following. - Positional access (e.g. ``cpu_times()[3]``) may silently return the wrong - field. Always use attribute access instead (e.g. ``cpu_times().idle``). - -- :meth:`Process.memory_info`: - - - The returned named tuple changed size and field order. Positional access - (e.g. ``p.memory_info()[3]`` or ``a, b, c = p.memory_info()``) may break or - silently return the wrong field. Always use attribute access instead (e.g. - ``p.memory_info().rss``). - -Enums: - -- :meth:`Process.status` now returns a :class:`psutil.ProcessStatus` enum - member instead of a plain ``str``. Since :class:`psutil.ProcessStatus` is a - ``StrEnum``, it compares equal to its string value (e.g. ``p.status() == - "running"`` still works), but ``repr()`` and ``type()`` differ. Use - :data:`psutil.STATUS_RUNNING` and friends as before; - ``psutil.STATUS_RUNNING`` is now an alias for the enum member. - -- :func:`net_connections` and :meth:`Process.net_connections`: the - ``status`` field now returns a :class:`psutil.ConnectionStatus` enum member - instead of a plain ``str``. Same ``StrEnum`` compatibility rules as above - apply. - -- ``RLIMIT_*`` / ``RLIM_*`` constants (Linux, FreeBSD): these are now members - of the :class:`psutil.ProcessRlimit` ``IntEnum`` instead of plain integers. - Since ``IntEnum`` compares equal to integers, existing code using them as - arguments to :meth:`Process.rlimit` is unaffected. + psutil 8.0 introduces breaking API changes and drops support for Python 3.6. + See the :ref:`migration guide ` if upgrading from 7.x. 7.2.3 — 2026-02-08 ^^^^^^^^^^^^^^^^^^ diff --git a/docs/faq.rst b/docs/faq.rst index bbabd0f43e..4a9cf96cd9 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -263,8 +263,8 @@ Memory .. _faq_virtual_memory_available: -What is the difference between virtual_memory().available and virtual_memory().free? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +What is the difference between virtual_memory() available and free? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :func:`virtual_memory` returns both ``free`` and ``available``, but they measure different things: @@ -311,6 +311,6 @@ libraries counted in every process that uses them. For example, if :meth:`Process.memory_footprint` returns USS (Unique Set Size), i.e. memory private to the process. It represents the amount of memory that -would be freed if the process were terminated. +would be freed if the process were terminated right now. It is more accurate than RSS, but substantially slower and requires higher privileges. On Linux it also returns PSS (Proportional Set Size) and swap. diff --git a/docs/index.rst b/docs/index.rst index a921a06328..53f07ed1da 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -105,6 +105,7 @@ Table of Contents Who uses psutil Credits Development guide + Migration Timeline .. toctree:: diff --git a/docs/migration.rst b/docs/migration.rst new file mode 100644 index 0000000000..8cd339cf6e --- /dev/null +++ b/docs/migration.rst @@ -0,0 +1,308 @@ +.. currentmodule:: psutil +.. include:: _links.rst + +Migration guide +=============== + +.. contents:: + :local: + :depth: 2 + +This page summarises the breaking changes introduced in each major +release and shows the code changes required to upgrade. + +.. note:: + Minor and patch releases (e.g. 6.1.x, 7.1.x) never contain + breaking changes. Only major releases are listed here. + +.. _migration-8.0: + +Migrating to 8.0 +----------------- + +Named tuple field order changed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- :func:`cpu_times`: ``user, system, idle`` fields changed order on Linux, + macOS and BSD. They are now always the first 3 fields on all platforms, with + platform-specific fields (e.g. ``nice``) following. Positional access (e.g. + ``cpu_times()[3]``) will silently return the wrong field. Always use + attribute access instead (e.g. ``cpu_times().idle``). + + .. code-block:: python + + # before + user, nice, system, idle = psutil.cpu_times() + + # after + t = psutil.cpu_times() + user, system, idle = t.user, t.system, t.idle + +- :meth:`Process.memory_info`: + + - The returned named tuple changed size and field + order. Positional access (e.g. ``p.memory_info()[3]`` or ``a, b, c = + p.memory_info()``) may break or silently return the wrong field. Always use + attribute access instead (e.g. ``p.memory_info().rss``). Also, ``lib`` and ``dirty`` on Linux were removed and turned into aliases emitting `DeprecationWarning`. + + .. code-block:: python + + # Linux before + rss, vms, shared, text, lib, data, dirty = p.memory_info() + + # Linux after + t = p.memory_info() + rss, vms, shared, text, data = t.rss, t.vms, t.shared, t.text, t.data + + - macOS: ``pfaults`` and ``pageins`` fields were removed with **no + backward-compatible aliases**. Use :meth:`page_faults` instead. + + .. code-block:: python + + # before + rss, vms, pfaults, pageins = p.memory_info() + + # after + rss, vms = p.memory_info() + minor, major = p.page_faults() + + - Windows: eliminated old aliases: ``wset`` → ``rss``, ``peak_wset`` → + ``peak_rss``, ``pagefile`` / ``private`` → ``vms``, ``peak_pagefile`` → + ``peak_vms``, ``num_page_faults`` → :meth:`page_faults` method. At the same + time ``paged_pool``, ``nonpaged_pool``, ``peak_paged_pool``, + ``peak_nonpaged_pool`` were moved to :meth:`memory_info_ex`. All these old + names still work but raise `DeprecationWarning`. + + - BSD: a new ``peak_rss`` field was added. + +Status and connection fields are now enums +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- :meth:`Process.status` now returns a :class:`psutil.ProcessStatus` member + instead of a plain ``str``. +- :meth:`Process.net_connections` and :func:`net_connections` ``status`` field + now returns a :class:`psutil.ConnectionStatus` member instead of a plain + ``str``. + +Because both are `enum.StrEnum`_ subclasses they compare equal to their +string values, so existing comparisons continue to work unchanged: + +.. code-block:: python + + # these still work + p.status() == "running" + p.status() == psutil.STATUS_RUNNING + + # repr() and type() differ, so code inspecting these may need updating + + +The individual constants (e.g. :data:`psutil.STATUS_RUNNING`) are kept as +aliases for the enum members, and should be preferred over accessing them via +the enum class: + +.. code-block:: python + + # prefer this + p.status() == psutil.STATUS_RUNNING + + # not this + p.status() == psutil.ProcessStatus.STATUS_RUNNING + +memory_full_info() is deprecated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`Process.memory_full_info` is deprecated. Use the new +:meth:`Process.memory_footprint` instead: + +.. code-block:: python + + # before + mem = p.memory_full_info() + uss = mem.uss + + # after + mem = p.memory_footprint() + uss = mem.uss + +Python 3.6 dropped +^^^^^^^^^^^^^^^^^^^^ + +Python 3.6 is no longer supported. Minimum version is Python 3.7. + +.. _migration-7.0: + +Migrating to 7.0 +----------------- + +Process.memory_info_ex() removed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The long-deprecated :meth:`Process.memory_info_ex` was removed (it was +deprecated since 4.0.0 in 2016). Use :meth:`Process.memory_full_info` +instead: + +.. code-block:: python + + # before + p.memory_info_ex() + + # after + p.memory_full_info() + +Python 2.7 dropped +^^^^^^^^^^^^^^^^^^^^ + +Python 2.7 is no longer supported. The last release to support Python +2.7 is psutil 6.1.x: + +.. code-block:: bash + + pip2 install "psutil==6.1.*" + +.. _migration-6.0: + +Migrating to 6.0 +----------------- + +Process.connections() renamed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`Process.connections` was renamed to +:meth:`Process.net_connections` for consistency with the system-level +:func:`net_connections`. The old name triggers a ``DeprecationWarning`` +and will be removed in a future release: + +.. code-block:: python + + # before + p.connections() + p.connections(kind="tcp") + + # after + p.net_connections() + p.net_connections(kind="tcp") + +disk_partitions() lost two fields +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``maxfile`` and ``maxpath`` fields were removed from the named tuple +returned by :func:`disk_partitions`. Code unpacking the tuple +positionally will break: + +.. code-block:: python + + # before (broken) + device, mountpoint, fstype, opts, maxfile, maxpath = part + + # after + device, mountpoint, fstype, opts = ( + part.device, part.mountpoint, part.fstype, part.opts + ) + +process_iter() no longer checks for PID reuse +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:func:`process_iter` no longer pre-emptively checks whether yielded +PIDs have been reused (this made it ~20× faster). If you need to verify +that a process object is still alive and refers to the same process, use +:meth:`Process.is_running` explicitly: + +.. code-block:: python + + for p in psutil.process_iter(["name"]): + if p.is_running(): + print(p.pid, p.info["name"]) + +.. _migration-5.0: + +Migrating to 5.0 +----------------- + +5.0.0 was the largest renaming in psutil history. All ``get_*`` and +``set_*`` :class:`Process` methods lost their prefix, and several +module-level names were changed. + +Old :class:`Process` method names still worked but raised +``DeprecationWarning``. They were fully removed in 6.0. + +Process methods +^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 40 40 + + * - Old (< 5.0) + - New (>= 5.0) + * - ``p.get_children()`` + - ``p.children()`` + * - ``p.get_connections()`` + - ``p.connections()`` → ``p.net_connections()`` in 6.0 + * - ``p.get_cpu_affinity()`` + - ``p.cpu_affinity()`` + * - ``p.get_cpu_percent()`` + - ``p.cpu_percent()`` + * - ``p.get_cpu_times()`` + - ``p.cpu_times()`` + * - ``p.get_ext_memory_info()`` + - ``p.memory_info_ex()`` → ``p.memory_full_info()`` in 4.0 + * - ``p.get_io_counters()`` + - ``p.io_counters()`` + * - ``p.get_ionice()`` + - ``p.ionice()`` + * - ``p.get_memory_info()`` + - ``p.memory_info()`` + * - ``p.get_memory_maps()`` + - ``p.memory_maps()`` + * - ``p.get_memory_percent()`` + - ``p.memory_percent()`` + * - ``p.get_nice()`` + - ``p.nice()`` + * - ``p.get_num_ctx_switches()`` + - ``p.num_ctx_switches()`` + * - ``p.get_num_fds()`` + - ``p.num_fds()`` + * - ``p.get_num_threads()`` + - ``p.num_threads()`` + * - ``p.get_open_files()`` + - ``p.open_files()`` + * - ``p.get_rlimit()`` + - ``p.rlimit()`` + * - ``p.get_threads()`` + - ``p.threads()`` + * - ``p.getcwd()`` + - ``p.cwd()`` + * - ``p.set_nice(v)`` + - ``p.nice(v)`` + * - ``p.set_ionice(cls)`` + - ``p.ionice(cls)`` + * - ``p.set_cpu_affinity(cpus)`` + - ``p.cpu_affinity(cpus)`` + +Module-level renames +^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 40 40 + + * - Old (< 5.0) + - New (>= 5.0) + * - ``psutil.NUM_CPUS`` + - ``psutil.cpu_count()`` + * - ``psutil.BOOT_TIME`` + - ``psutil.boot_time()`` + * - ``psutil.TOTAL_PHYMEM`` + - ``psutil.virtual_memory().total`` + * - ``psutil.get_pid_list()`` + - ``psutil.pids()`` + * - ``psutil.get_users()`` + - ``psutil.users()`` + * - ``psutil.get_boot_time()`` + - ``psutil.boot_time()`` + * - ``psutil.network_io_counters()`` + - ``psutil.net_io_counters()`` + * - ``psutil.phymem_usage()`` + - ``psutil.virtual_memory()`` + * - ``psutil.virtmem_usage()`` + - ``psutil.swap_memory()`` diff --git a/scripts/internal/rst_check_dead_refs.py b/scripts/internal/rst_check_dead_refs.py index 463c918df6..af518f2761 100755 --- a/scripts/internal/rst_check_dead_refs.py +++ b/scripts/internal/rst_check_dead_refs.py @@ -18,6 +18,7 @@ """ import argparse +import os import re import sys @@ -33,33 +34,62 @@ ) # `Foo Bar`_ but NOT `text `_ and NOT `text`__ RE_REF = re.compile(r'`([^`<\n]+)`_(?!_)') +# .. include:: somefile.rst +RE_INCLUDE = re.compile(r'^\.\. include::\s*(\S+)', re.MULTILINE) def line_number(text, pos): return text.count('\n', 0, pos) + 1 +def read_with_includes(path, _seen=None): + """Return file text plus the text of any ``.. include::`` files.""" + if _seen is None: + _seen = set() + path = os.path.normpath(path) + if path in _seen: + return "" + _seen.add(path) + try: + with open(path) as f: + text = f.read() + except OSError: + return "" + base = os.path.dirname(path) + for m in RE_INCLUDE.finditer(text): + inc = os.path.join(base, m.group(1)) + text += read_with_includes(inc, _seen) + return text + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("files", nargs="+", metavar="FILE") args = parser.parse_args() # Pass 1: collect all defined targets across all files. - all_targets = set() # lower-case names - # URL-only targets: lower-case name -> (original name, file) + # Targets from ".. include::" files are also collected so that + # cross-file references resolve correctly. + all_targets = set() + # URL-only targets defined directly in a file (not via include), used + # to detect targets that are defined but never referenced. url_targets = {} for path in args.files: - with open(path) as f: - text = f.read() - for m in RE_ANY_TARGET.finditer(text): # noqa: FURB142 + # Use expanded text (with includes) so cross-file references resolve. + expanded = read_with_includes(path) + for m in RE_ANY_TARGET.finditer(expanded): # noqa: FURB142 all_targets.add(m.group(1).strip().lower()) - for m in RE_URL_TARGET.finditer(text): + # Only record URL targets from the file itself, not from includes, + # so we don't falsely report included targets as unreferenced. + with open(path) as f: + own_text = f.read() + for m in RE_URL_TARGET.finditer(own_text): name = m.group(1).strip() url_targets[name.lower()] = ( name, path, - line_number(text, m.start()), + line_number(own_text, m.start()), ) # Pass 2: collect all backtick references (with file + line). @@ -69,7 +99,7 @@ def main(): for path in args.files: with open(path) as f: text = f.read() - for m in RE_REF.finditer(text): + for m in RE_REF.finditer(text): # references from the file itself only name = m.group(1).strip() all_refs.append(( name.lower(), From 76ac3393fb0b17ee6864851a7efc5d4a6a229dee Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Mar 2026 02:12:14 +0100 Subject: [PATCH 1606/1714] Windows, `cpu_times()`: rename `interrupt` field to `irq` (#2773) On Windows, rename the interrupt field of cpu_times() to irq, to match the name used on Linux and BSD. The old name still works but raises DeprecationWarning. Fixes #2772. --- docs/api.rst | 17 +++++++++++------ docs/changelog.rst | 4 ++++ docs/migration.rst | 23 +++++++++++++++++++++++ psutil/_ntuples.py | 8 +++++++- psutil/_pswindows.py | 6 +++--- tests/test_windows.py | 6 ++++++ 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index dfe91a0c07..375ec03d8f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -34,10 +34,11 @@ CPU Platform-specific fields: - **nice** *(Linux, macOS, BSD)*: time spent by niced (prioritized) processes - executing in user mode; on Linux this also includes **guest_nice** time + executing in user mode; on Linux this also includes **guest_nice** time. - **iowait** *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete. This is *not* accounted in **idle** time counter. - - **irq** *(Linux, BSD)*: time spent for servicing hardware interrupts + - **irq** *(Linux, Windows, BSD)*: time spent for servicing hardware + interrupts - **softirq** *(Linux)*: time spent for servicing software interrupts - **steal** *(Linux)*: time spent by other operating systems running in a virtualized environment @@ -46,8 +47,6 @@ CPU - **guest_nice** *(Linux)*: time spent running a niced guest (virtual CPU for guest operating systems under the control of the Linux kernel) - - **interrupt** *(Windows)*: time spent for servicing hardware interrupts - (similar to "irq" on UNIX) - **dpc** *(Windows)*: time spent servicing deferred procedure calls (DPCs); DPCs are interrupts that run at a lower priority than standard interrupts. @@ -70,7 +69,12 @@ CPU `_. .. versionchanged:: 4.1.0 - added *interrupt* and *dpc* fields on Windows. + added *irq* and *dpc* fields on Windows (*irq* was called *interrupt* + before 8.0.0). + + .. versionchanged:: 8.0.0 + *interrupt* field on Windows was renamed to *irq*; *interrupt* still + works but raises :exc:`DeprecationWarning`. .. versionchanged:: 8.0.0 ``cpu_times()`` field order was standardized: ``user``, ``system``, @@ -136,7 +140,8 @@ CPU to ignore. See also :ref:`faq_cpu_percent` FAQ. .. versionchanged:: 4.1.0 - two new *interrupt* and *dpc* fields are returned on Windows. + two new *irq* and *dpc* fields are returned on Windows (*irq* was + called *interrupt* before 8.0.0). .. versionchanged:: 5.9.6 function is now thread safe. diff --git a/docs/changelog.rst b/docs/changelog.rst index a87dce93f5..b89ee875a5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -118,6 +118,10 @@ Others ``changelog.rst`` and ``credits.rst`` when commenting with /changelog. - :gh:`2766`: remove remaining Python 2.7 compatibility shims from ``setup.py``, simplifying the build infrastructure. +- :gh:`2772`, [Windows]: :func:`cpu_times` ``interrupt`` field renamed to + ``irq`` to match the field name used on Linux and BSD. ``interrupt`` still + works but raises :exc:`DeprecationWarning`. + See :ref:`migration guide `. **Bug fixes** diff --git a/docs/migration.rst b/docs/migration.rst index 8cd339cf6e..246f3bca85 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -75,6 +75,23 @@ Named tuple field order changed - BSD: a new ``peak_rss`` field was added. +cpu_times() interrupt renamed to irq on Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``interrupt`` field of :func:`cpu_times` on Windows was renamed to ``irq`` +to match the name used on Linux and BSD. The old name still works but raises +:exc:`DeprecationWarning`: + +.. code-block:: python + + # before + t = psutil.cpu_times() + print(t.interrupt) + + # after + t = psutil.cpu_times() + print(t.irq) + Status and connection fields are now enums ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -129,6 +146,8 @@ Python 3.6 dropped Python 3.6 is no longer supported. Minimum version is Python 3.7. +---- + .. _migration-7.0: Migrating to 7.0 @@ -159,6 +178,8 @@ Python 2.7 is no longer supported. The last release to support Python pip2 install "psutil==6.1.*" +---- + .. _migration-6.0: Migrating to 6.0 @@ -213,6 +234,8 @@ that a process object is still alive and refers to the same process, use if p.is_running(): print(p.pid, p.info["name"]) +---- + .. _migration-5.0: Migrating to 5.0 diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index bb89865657..b2c2b33065 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -150,9 +150,15 @@ class scputimes(NamedTuple): if SUNOS or AIX: iowait: float if WINDOWS: - interrupt: float + irq: float dpc: float + @property + def interrupt(self): + msg = "'interrupt' field is deprecated, use 'irq' instead" + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return self.irq + # psutil.cpu_stats() class scpustats(NamedTuple): diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 8103d7bc23..a8652da17d 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -207,15 +207,15 @@ def cpu_times(): *[sum(n) for n in zip(*cext.per_cpu_times())] ) return ntp.scputimes( - user, system, idle, percpu_summed.interrupt, percpu_summed.dpc + user, system, idle, percpu_summed.irq, percpu_summed.dpc ) def per_cpu_times(): """Return system per-CPU times as a list of named tuples.""" ret = [] - for user, system, idle, interrupt, dpc in cext.per_cpu_times(): - item = ntp.scputimes(user, system, idle, interrupt, dpc) + for user, system, idle, irq, dpc in cext.per_cpu_times(): + item = ntp.scputimes(user, system, idle, irq, dpc) ret.append(item) return ret diff --git a/tests/test_windows.py b/tests/test_windows.py index 1767a33f32..e9da443fae 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -152,6 +152,12 @@ def test_cpu_count_cores_vs_wmi(self): def test_cpu_count_vs_cpu_times(self): assert psutil.cpu_count() == len(psutil.cpu_times(percpu=True)) + def test_cpu_times_irq_field(self): + t = psutil.cpu_times() + assert t.irq >= 0 + with pytest.warns(DeprecationWarning, match="interrupt"): + assert t.interrupt == t.irq + def test_cpu_freq(self): w = wmi.WMI() proc = w.Win32_Processor()[0] From 51ac94023480730e67ffada955f2d6a7b223c032 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Mar 2026 08:49:02 +0100 Subject: [PATCH 1607/1714] Sonnet's attempt to fix its own changelog bot which fails when PR is opened by a user Signed-off-by: Giampaolo Rodola --- .github/workflows/changelog_bot.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/changelog_bot.yml b/.github/workflows/changelog_bot.yml index 9a3a163f53..3af467eea3 100644 --- a/.github/workflows/changelog_bot.yml +++ b/.github/workflows/changelog_bot.yml @@ -30,22 +30,25 @@ jobs: exit 1 fi - - name: Get PR head branch - id: get-branch + - name: Get PR info + id: pr-info env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_NUMBER=${{ github.event.issue.number }} - HEAD_BRANCH=$(gh pr view $PR_NUMBER \ + PR_JSON=$(gh pr view $PR_NUMBER \ --repo ${{ github.repository }} \ - --json headRefName \ - --jq '.headRefName') + --json headRefName,headRepository,headRepositoryOwner) + HEAD_BRANCH=$(echo "$PR_JSON" | jq -r '.headRefName') + HEAD_OWNER=$(echo "$PR_JSON" | jq -r '.headRepositoryOwner.login') + HEAD_REPO=$(echo "$PR_JSON" | jq -r '.headRepository.name') echo "branch=$HEAD_BRANCH" >> "$GITHUB_OUTPUT" + echo "head_repo=$HEAD_OWNER/$HEAD_REPO" >> "$GITHUB_OUTPUT" - name: Checkout PR branch uses: actions/checkout@v5 with: - ref: ${{ steps.get-branch.outputs.branch }} + ref: refs/pull/${{ github.event.issue.number }}/head token: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies @@ -61,6 +64,8 @@ jobs: --token ${{ secrets.GITHUB_TOKEN }} - name: Commit and push if changelog changed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -69,5 +74,8 @@ jobs: echo "No changes, skipping commit" else git commit -m "Update changelog for PR #${{ github.event.issue.number }}" - git push + HEAD_REPO="${{ steps.pr-info.outputs.head_repo }}" + HEAD_BRANCH="${{ steps.pr-info.outputs.branch }}" + PUSH_URL="https://x-access-token:${GH_TOKEN}@github.com/${HEAD_REPO}.git" + git push "$PUSH_URL" "HEAD:refs/heads/${HEAD_BRANCH}" fi From cbc0bed620de90131aad5c1944635f9b523e589e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Mar 2026 09:14:06 +0100 Subject: [PATCH 1608/1714] changelog_bot.py: fetch author full name + ensure new line after header --- .github/workflows/changelog_bot.py | 39 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/.github/workflows/changelog_bot.py b/.github/workflows/changelog_bot.py index 1a9860944f..a5cd064846 100644 --- a/.github/workflows/changelog_bot.py +++ b/.github/workflows/changelog_bot.py @@ -236,12 +236,15 @@ def gh_request(path, accept="application/vnd.github+json"): def fetch_pr_metadata(): pr = json.loads(gh_request(f"/repos/{REPO}/pulls/{PR_NUMBER}")) author = pr["user"]["login"] + # Fetch the user profile to get the full name. + user = json.loads(gh_request(f"/users/{author}")) + author_name = user.get("name") or author return { "number": pr["number"], "title": pr["title"], "body": pr.get("body") or "", "author": author, - "author_name": pr["user"].get("name") or author, + "author_name": author_name, } @@ -307,6 +310,14 @@ def insert_changelog_entry(section, entry): header_idx = next( (i for i, ln in enumerate(block) if ln.rstrip() == header), None ) + + def _entry_gh_number(line): + """Extract the ticket number from a :gh:`N` reference.""" + m = re.search(r":gh:`(\d+)`", line) + return int(m.group(1)) if m else None + + new_entry_num = _entry_gh_number(entry) + if header_idx is None: insert_at = next( ( @@ -322,9 +333,29 @@ def insert_changelog_entry(section, entry): + block[insert_at:] ) else: - insert_at = header_idx + 1 - if insert_at < len(block) and not block[insert_at].strip(): - insert_at += 1 + # Find the end of this section (next ** header or end of block). + section_end = next( + ( + i + for i in range(header_idx + 1, len(block)) + if block[i].startswith("**") + ), + len(block), + ) + # Skip the blank line after the header. + first_entry = header_idx + 1 + if first_entry < len(block) and not block[first_entry].strip(): + first_entry += 1 + # Find the right position sorted by ticket number. + insert_at = section_end + if new_entry_num is not None: + for i in range(first_entry, section_end): + num = _entry_gh_number(block[i]) + if num is not None and num > new_entry_num: + insert_at = i + break + else: + insert_at = first_entry new_block = block[:insert_at] + [f"{entry}\n"] + block[insert_at:] lines[version_idx:next_version_idx] = new_block From 982af2ce2d1678811dde5cde2f69aad0b21caa52 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Mar 2026 09:18:31 +0100 Subject: [PATCH 1609/1714] changelog_bot.py: try to insert the entry at the right line --- .github/workflows/changelog_bot.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/changelog_bot.py b/.github/workflows/changelog_bot.py index a5cd064846..c18b6cb95d 100644 --- a/.github/workflows/changelog_bot.py +++ b/.github/workflows/changelog_bot.py @@ -439,9 +439,11 @@ def sort_key(e): insert_idx = i break if not skip: - while ( - insert_idx > year_idx + 2 and not lines[insert_idx - 1].strip() - ): + # Don't back up past the blank line after the year + # underline (year_idx + 2 = "~~~~\n", + 3 = first + # content line). + min_idx = year_idx + 3 + while insert_idx > min_idx and not lines[insert_idx - 1].strip(): insert_idx -= 1 lines.insert(insert_idx, f"{credits_entry}\n") From 3f68b26369a5e006231f2b11a52eca3ad502e8d2 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Thu, 19 Mar 2026 04:22:00 -0400 Subject: [PATCH 1610/1714] linux: fix cpu_count_cores() on s390x (#2770) The s390x kernel uses spaces before the colon in /proc/cpuinfo while x86 uses a tab. Splitting on b'\t:' fails on s390x, raising ValueError. Split on b':' instead. Co-authored-by: github-actions[bot] Co-authored-by: Giampaolo Rodola --- docs/changelog.rst | 3 +++ docs/credits.rst | 2 ++ psutil/_pslinux.py | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b89ee875a5..b0039176b5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -125,6 +125,9 @@ Others **Bug fixes** +- :gh:`2770`, [Linux]: fix :func:`cpu_count_cores()` raising ``ValueError`` + on s390x architecture, where ``/proc/cpuinfo`` uses spaces before the + colon separator instead of a tab. - :gh:`2726`, [macOS]: :meth:`Process.num_ctx_switches` return an unusual high number due to a C type precision issue. - :gh:`2411` [macOS]: :meth:`Process.cpu_times` and diff --git a/docs/credits.rst b/docs/credits.rst index 60a90e9b40..8d0c990770 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -109,6 +109,7 @@ Code contributors by year 2026 ~~~~ +* `Amaan Qureshi`_ - :gh:`2770` * `Felix Yan`_ - :gh:`2732` * `Sergey Fedorov`_ - :gh:`2701` @@ -408,6 +409,7 @@ Code contributors by year .. ============================================================================ .. _`Adrian Page`: https://github.com/adpag +.. _`Amaan Qureshi`: https://github.com/amaanq .. _`qcha0`: https://github.com/qcha0 .. _`Akos Kiss`: https://github.com/akosthekiss .. _`Aleksey Lobanov`: https://github.com/AlekseyLobanov diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 82e2b5d891..7937bea628 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -550,8 +550,8 @@ def cpu_count_cores(): current_info = {} elif line.startswith((b'physical id', b'cpu cores')): # ongoing section - key, value = line.split(b'\t:', 1) - current_info[key] = int(value) + key, value = line.split(b':', 1) + current_info[key.strip()] = int(value) result = sum(mapping.values()) return result or None # mimic os.cpu_count() From cd35b89e38fc49329b0f0a5ea4631be47a32b315 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 19 Mar 2026 20:11:43 +0100 Subject: [PATCH 1611/1714] Doc: add glossary section (#2774) --- MANIFEST.in | 1 + docs/_links.rst | 36 ---- docs/_static/css/custom.css | 16 ++ docs/api.rst | 412 +++++++++++++++++++----------------- docs/changelog.rst | 16 +- docs/conf.py | 4 + docs/faq.rst | 7 +- docs/glossary.rst | 348 ++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/migration.rst | 2 +- docs/recipes.rst | 2 + 11 files changed, 604 insertions(+), 241 deletions(-) create mode 100644 docs/glossary.rst diff --git a/MANIFEST.in b/MANIFEST.in index 14d9243a19..ed2d040670 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -31,6 +31,7 @@ include docs/conf.py include docs/credits.rst include docs/devguide.rst include docs/faq.rst +include docs/glossary.rst include docs/index.rst include docs/install.rst include docs/migration.rst diff --git a/docs/_links.rst b/docs/_links.rst index 03aa1a659b..e69de29bb2 100644 --- a/docs/_links.rst +++ b/docs/_links.rst @@ -1,36 +0,0 @@ -.. === docs.python.org - -.. _`AF_INET6`: https://docs.python.org/3/library/socket.html#socket.AF_INET6 -.. _`AF_INET`: https://docs.python.org/3/library/socket.html#socket.AF_INET -.. _`AF_UNIX`: https://docs.python.org/3/library/socket.html#socket.AF_UNIX -.. _`enum.IntEnum`: https://docs.python.org/3/library/enum.html#enum.IntEnum -.. _`enum.StrEnum`: https://docs.python.org/3/library/enum.html#enum.StrEnum -.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum -.. _`hash`: https://docs.python.org/3/library/functions.html#hash -.. _`open`: https://docs.python.org/3/library/functions.html#open -.. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count -.. _`os.getloadavg`: https://docs.python.org//library/os.html#os.getloadavg -.. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid -.. _`os.getpriority`: https://docs.python.org/3/library/os.html#os.getpriority -.. _`os.getresgid`: https://docs.python.org//library/os.html#os.getresgid -.. _`os.getresuid`: https://docs.python.org//library/os.html#os.getresuid -.. _`os.O_RDONLY`: https://docs.python.org/3/library/os.html#os.O_RDONLY -.. _`os.O_TRUNC`: https://docs.python.org/3/library/os.html#os.O_TRUNC -.. _`os.open`: https://docs.python.org/3/library/os.html#os.open -.. _`os.pidfd_open`: https://docs.python.org//library/os.html#os.pidfd_open -.. _`os.setpriority`: https://docs.python.org/3/library/os.html#os.setpriority -.. _`os.times`: https://docs.python.org//library/os.html#os.times -.. _`resource.getrlimit`: https://docs.python.org/3/library/resource.html#resource.getrlimit -.. _`resource.setrlimit`: https://docs.python.org/3/library/resource.html#resource.setrlimit -.. _`select.kqueue`: https://docs.python.org//library/select.html#select.kqueue -.. _`select.poll`: https://docs.python.org//library/select.html#select.poll -.. _`set`: https://docs.python.org/3/library/stdtypes.html#types-set -.. _`shutil.disk_usage`: https://docs.python.org/3/library/shutil.html#shutil.disk_usage -.. _`signal module`: https://docs.python.org//library/signal.html -.. _`SOCK_DGRAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_DGRAM -.. _`SOCK_SEQPACKET`: https://docs.python.org/3/library/socket.html#socket.SOCK_SEQPACKET -.. _`SOCK_STREAM`: https://docs.python.org/3/library/socket.html#socket.SOCK_STREAM -.. _`socket.fromfd`: https://docs.python.org/3/library/socket.html#socket.fromfd -.. _`subprocess.Popen`: https://docs.python.org/3/library/subprocess.html#subprocess.Popen -.. _`threading.get_ident`: https://docs.python.org/3/library/threading.html#threading.get_ident -.. _`threading.Thread`: https://docs.python.org/3/library/threading.html#threading.Thread diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index b013743885..26098cba90 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -148,6 +148,22 @@ pre { padding: 5px !important; } +/* Show external links with a slightly different style (dotted underline). + Excludes Python API cross-references (intersphinx -> docs.python.org). */ + +a.reference.external:not([href*="docs.python.org"]) { + text-decoration: underline dotted; +} + +a.reference.external:not([href*="docs.python.org"]):hover { + text-decoration: underline solid; +} + +a.external[href^="#"] { + text-decoration: none; /* keep same-page anchors normal */ + color: inherit; +} + /* ================================================================== */ /* Warnings and info boxes like python doc */ /* ================================================================== */ diff --git a/docs/api.rst b/docs/api.rst index 375ec03d8f..bb16e42d55 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -21,32 +21,44 @@ CPU .. function:: cpu_times(percpu=False) - Return system CPU times as a named tuple. - Every attribute represents the seconds the CPU has spent in the given mode. + Return system CPU times as a :term:`named tuple`. All fields are + :term:`cumulative counters ` (seconds) representing time + the CPU has spent in each mode since boot. The attributes availability varies depending on the platform. Cross-platform fields: - **user**: time spent by normal processes executing in user mode; on Linux this also includes **guest** time + - **system**: time spent by processes executing in kernel mode + - **idle**: time spent doing nothing Platform-specific fields: - - **nice** *(Linux, macOS, BSD)*: time spent by niced (prioritized) processes - executing in user mode; on Linux this also includes **guest_nice** time. - - **iowait** *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete. - This is *not* accounted in **idle** time counter. - - **irq** *(Linux, Windows, BSD)*: time spent for servicing hardware - interrupts - - **softirq** *(Linux)*: time spent for servicing software interrupts - - **steal** *(Linux)*: time spent by other operating systems running - in a virtualized environment - - **guest** *(Linux)*: time spent running a virtual CPU for guest - operating systems under the control of the Linux kernel - - **guest_nice** *(Linux)*: time spent running a niced guest - (virtual CPU for guest operating systems under the control of the Linux - kernel) + - **nice** *(Linux, macOS, BSD)*: time spent by :term:`niced ` + (lower-priority) processes executing in user mode; on Linux this also + includes **guest_nice** time. + + - **iowait** *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete + (:term:`iowait`). This is *not* accounted in **idle** time counter. + + - **irq** *(Linux, Windows, BSD)*: time spent for servicing + :term:`hardware interrupts ` + + - **softirq** *(Linux)*: time spent for servicing + :term:`soft interrupts ` + + - **steal** *(Linux)*: CPU time the virtual machine wanted to run but was + used by other virtual machines or the host. A sustained non-zero steal rate + indicates CPU contention. + + - **guest** *(Linux)*: time the host CPU spent running a guest operating + system (virtual machine). Already included in **user** time. + + - **guest_nice** *(Linux)*: like **guest**, but for virtual CPUs running at a + lower :term:`nice` priority. Already included in **nice** time. + - **dpc** *(Windows)*: time spent servicing deferred procedure calls (DPCs); DPCs are interrupts that run at a lower priority than standard interrupts. @@ -98,7 +110,7 @@ CPU The list is ordered by CPU index. The order of the list is consistent across calls. Internally this function maintains a global map (a dict) where each key is - the ID of the calling thread (`threading.get_ident`_). This means it can be + the ID of the calling thread (:func:`threading.get_ident`). This means it can be called from different threads, at different intervals, and still return meaningful and independent results. @@ -148,9 +160,9 @@ CPU .. function:: cpu_count(logical=True) - Return the number of logical CPUs in the system (similar to `os.cpu_count`_) - or ``None`` if undetermined. - Unlike `os.cpu_count`_, this is not influenced by the ``PYTHON_CPU_COUNT`` + Return the number of :term:`logical CPUs ` in the system + (similar to :func:`os.cpu_count`) or ``None`` if undetermined. + Unlike :func:`os.cpu_count`, this is not influenced by the ``PYTHON_CPU_COUNT`` environment variable introduced in Python 3.13. "logical CPUs" means the number of physical cores multiplied by the number of threads that can run on each core (this is known as Hyper Threading). @@ -183,17 +195,17 @@ CPU .. function:: cpu_stats() - Return various CPU statistics as a named tuple: + Return various CPU statistics as a named tuple. All fields are + :term:`cumulative counters ` since boot. - - **ctx_switches**: - number of context switches (voluntary + involuntary) since boot. + - **ctx_switches**: number of :term:`context switches ` + (voluntary + involuntary). - **interrupts**: - number of interrupts since boot. + number of :term:`hardware interrupts `. - **soft_interrupts**: - number of software interrupts since boot. Always set to ``0`` on Windows - and SunOS. - - **syscalls**: number of system calls since boot. Always set to ``0`` on - Linux. + number of :term:`soft interrupts `. Always set to ``0`` on + Windows and SunOS. + - **syscalls**: number of system calls. Always set to ``0`` on Linux. Example (Linux): @@ -245,7 +257,7 @@ CPU Return the average system load over the last 1, 5 and 15 minutes as a tuple. The "load" represents the processes which are in a runnable state, either using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). - On UNIX systems this relies on `os.getloadavg`_. On Windows this is emulated + On UNIX systems this relies on :func:`os.getloadavg`. On Windows this is emulated by using a Windows API that spawns a thread which keeps running in background and updates results every 5 seconds, mimicking the UNIX behavior. Thus, on Windows, the first time this is called and for the next 5 seconds @@ -281,13 +293,13 @@ Memory system going into swap. On Linux it uses the ``MemAvailable`` field from ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back to an estimate. This is the recommended field for monitoring actual memory usage - in a cross-platform fashion. + in a cross-platform fashion. See :term:`available memory`. - **percent**: the percentage usage calculated as ``(total - available) / total * 100``. - **used**: memory in use, calculated differently depending on the platform (see the table below). It is meant for informational purposes. Neither ``total - free`` nor ``total - available`` necessarily equals ``used``. - - **free**: memory that is not in use at all (zeroed pages). This is + - **free**: memory not currently allocated to anything. This is typically much lower than **available** because the OS keeps recently freed memory as reclaimable cache (see **cached** and **buffers**) rather than zeroing it immediately. Do not use this to check for @@ -296,13 +308,13 @@ Memory or recently accessed, held in RAM. It is unlikely to be reclaimed unless the system is under significant memory pressure. - **inactive** *(Linux, macOS, BSD)*: memory not recently accessed. It - still holds valid data (file cache, old allocations) but is a candidate - for reclamation or swapping. On BSD systems it is counted in + still holds valid data (:term:`page cache`, old allocations) but is a + candidate for reclamation or swapping. On BSD systems it is counted in **available**. - **buffers** *(Linux, BSD)*: kernel buffer cache for raw block-device I/O (e.g. disk blocks read before the filesystem processes them). Can be reclaimed by the OS when needed. - - **cached** *(Linux, BSD)*: in-memory page cache for files read from disk. + - **cached** *(Linux, BSD)*: in-memory :term:`page cache` for files read from disk. The OS reclaims this memory automatically when processes need it, so a large **cached** value is healthy and does not indicate memory pressure. On Linux this includes ``SReclaimable`` (reclaimable slab memory). @@ -316,7 +328,8 @@ Memory - **wired** *(macOS, BSD)*: memory pinned in RAM by the kernel (e.g. kernel code and critical data structures). It can never be moved to disk. - Below is a table showing implementation details. All info on Linux is retrieved from `/proc/meminfo`. + Below is a table showing implementation details. All info on Linux is + retrieved from `/proc/meminfo`. .. list-table:: :header-rows: 1 @@ -409,7 +422,7 @@ Memory - On Windows, **total**, **used** ("In use"), and **available** match the Task Manager (Performance > Memory tab). - .. note:: see `meminfo.py`_ script providing an example on how to convert + .. note:: see `scripts/meminfo.py`_ script providing an example on how to convert bytes in a human readable form. .. versionchanged:: 4.2.0 @@ -431,15 +444,16 @@ Memory * **percent**: swap usage as a percentage, calculated as ``used / total * 100``. * **sin**: number of bytes the system has paged *in* from disk (pages moved - from swap space back into RAM) since boot (cumulative). + from swap space back into RAM) since boot. See :term:`swap-in`. * **sout**: number of bytes the system has paged *out* to disk (pages moved - from RAM into swap space) since boot (cumulative). A continuously - increasing **sout** is a sign of memory pressure. + from RAM into swap space) since boot. A continuously increasing **sout** + is a sign of memory pressure. See :term:`swap-out`. - **sin** and **sout** are cumulative counters since boot; monitor their rate - of change rather than the absolute value to detect active swapping. + **sin** and **sout** are :term:`cumulative counters ` since boot; monitor + their rate of change rather than the absolute value to detect active + swapping. See :term:`swap-in` and :term:`swap-out`. On Windows both are always ``0``. - See `meminfo.py`_ script providing an example on how to convert bytes in a + See `scripts/meminfo.py`_ script providing an example on how to convert bytes in a human readable form. .. code-block:: pycon @@ -466,7 +480,7 @@ Disks (e.g. pseudo, memory, duplicate, inaccessible filesystems). Note that this may not be fully reliable on all systems (e.g. on BSD this parameter is ignored). - See `disk_usage.py`_ script providing an example usage. + See `scripts/disk_usage.py`_ script providing an example usage. Returns a list of named tuples with the following fields: * **device**: the device path (e.g. ``"/dev/hda1"``). On Windows this is the @@ -497,9 +511,9 @@ Disks *path* as a named tuple including **total**, **used** and **free** space expressed in bytes, plus the **percentage** usage. ``OSError`` is raised if *path* does not exist. - Starting from Python 3.3 this is also available as `shutil.disk_usage`_ + Starting from Python 3.3 this is also available as :func:`shutil.disk_usage` (see `BPO-12442`_). - See `disk_usage.py`_ script providing an example usage. + See `scripts/disk_usage.py`_ script providing an example usage. .. code-block:: pycon @@ -537,14 +551,14 @@ Disks - **write_time**: (all except *NetBSD* and *OpenBSD*) time spent writing to disk (in milliseconds) - **busy_time**: (*Linux*, *FreeBSD*) time spent doing actual I/Os (in - milliseconds) + milliseconds). See :term:`busy_time`. - **read_merged_count** (*Linux*): number of merged reads (see `iostats doc`_) - **write_merged_count** (*Linux*): number of merged writes (see `iostats doc`_) If *perdisk* is ``True`` return the same information for every physical disk installed on the system as a dictionary with partition names as the keys and the named tuple described above as the values. - See `iotop.py`_ for an example application. + See `scripts/iotop.py`_ for an example application. On some systems such as Linux, on a very busy or long-lived system, the numbers returned by the kernel may overflow and wrap (restart from zero). If *nowrap* is ``True`` psutil will detect and adjust those numbers across @@ -599,7 +613,7 @@ Network - **errout**: total number of errors while sending - **dropin**: total number of incoming packets which were dropped - **dropout**: total number of outgoing packets which were dropped (always 0 - on macOS and BSD) + on macOS and BSD). See :term:`dropin / dropout`. If *pernic* is ``True`` return the same information for every network interface installed on the system as a dictionary with network interface @@ -624,7 +638,7 @@ Network {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} - Also see `nettop.py`_ and `ifconfig.py`_ for an example application. + Also see `scripts/nettop.py`_ and `scripts/ifconfig.py`_ for an example application. .. versionchanged:: 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new @@ -636,12 +650,12 @@ Network Every named tuple provides 7 attributes: - **fd**: the socket file descriptor. If the connection refers to the current - process this may be passed to `socket.fromfd`_ + process this may be passed to :func:`socket.fromfd` to obtain a usable socket object. On Windows and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or `AF_UNIX`_. - - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or - `SOCK_SEQPACKET`_. + - **family**: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6` or :data:`socket.AF_UNIX`. + - **type**: the address type, either :data:`socket.SOCK_STREAM`, :data:`socket.SOCK_DGRAM` or + :data:`socket.SOCK_SEQPACKET`. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -690,7 +704,7 @@ Network On macOS and AIX this function requires root privileges. To get per-process connections use :meth:`Process.net_connections`. - Also, see `netstat.py`_ example script. + Also, see `scripts/netstat.py`_ example script. Example: .. code-block:: pycon @@ -706,7 +720,7 @@ Network .. warning:: On Linux, retrieving some connections requires root privileges. If psutil is not run as root, those connections are silently skipped instead of raising - ``PermissionError``. That means the returned list may be incomplete. + :exc:`PermissionError`. That means the returned list may be incomplete. .. note:: (macOS and AIX) :exc:`psutil.AccessDenied` is always raised unless running @@ -738,12 +752,12 @@ Network .. function:: net_if_addrs() - Return the addresses associated to each NIC (network interface card) + Return the addresses associated to each :term:`NIC` (network interface card) installed on the system as a dictionary whose keys are the NIC names and value is a list of named tuples for each address assigned to the NIC. Each named tuple includes 5 fields: - - **family**: the address family, either `AF_INET`_ or `AF_INET6`_ + - **family**: the address family, either :data:`socket.AF_INET` or :data:`socket.AF_INET6` or :const:`psutil.AF_LINK`, which refers to a MAC address. - **address**: the primary NIC address (always set). - **netmask**: the netmask address (may be ``None``). @@ -764,7 +778,7 @@ Network snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> - See also `nettop.py`_ and `ifconfig.py`_ for an example application. + See also `scripts/nettop.py`_ and `scripts/ifconfig.py`_ for an example application. .. note:: if you're interested in others families (e.g. AF_BLUETOOTH) you can use @@ -791,7 +805,7 @@ Network .. function:: net_if_stats() - Return information about each NIC (network interface card) installed on the + Return information about each :term:`NIC` (network interface card) installed on the system as a dictionary whose keys are the NIC names and value is a named tuple with the following fields: @@ -810,7 +824,7 @@ Network ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, and ``d2`` (some flags are only available on certain platforms). - Also see `nettop.py`_ and `ifconfig.py`_ for an example application. + Also see `scripts/nettop.py`_ and `scripts/ifconfig.py`_ for an example application. .. code-block:: pycon @@ -849,7 +863,7 @@ Sensors - **critical**: temperature at which the system will shut down, or ``None`` if not available. - See also `temperatures.py`_ and `sensors.py`_ for an example application. + See also `scripts/temperatures.py`_ and `scripts/sensors.py`_ for an example application. .. code-block:: pycon @@ -883,7 +897,7 @@ Sensors >>> psutil.sensors_fans() {'asus': [sfan(label='cpu_fan', current=3200)]} - See also `fans.py`_ and `sensors.py`_ for an example application. + See also `scripts/fans.py`_ and `scripts/sensors.py`_ for an example application. .. availability:: Linux @@ -920,7 +934,7 @@ Sensors >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) charge = 93%, time left = 4:37:08 - See also `battery.py`_ and `sensors.py`_ for an example application. + See also `scripts/battery.py`_ and `scripts/sensors.py`_ for an example application. .. availability:: Linux, Windows, macOS, FreeBSD @@ -1114,8 +1128,8 @@ Exceptions .. exception:: ZombieProcess(pid, name=None, ppid=None, msg=None) - This may be raised by :class:`Process` class methods when querying a zombie - process on UNIX (Windows doesn't have zombie processes). + This may be raised by :class:`Process` class methods when querying a + :term:`zombie process` on UNIX (Windows doesn't have zombie processes). *name* and *ppid* attributes are available if :meth:`Process.name` or :meth:`Process.ppid` methods were called before the process turned into a zombie. @@ -1150,15 +1164,15 @@ Process class .. class:: Process(pid=None) Represents an OS process with the given *pid*. - If *pid* is omitted current process *pid* (`os.getpid`_) is used. + If *pid* is omitted current process *pid* (:func:`os.getpid`) is used. Raise :exc:`NoSuchProcess` if *pid* does not exist. On Linux *pid* can also refer to a thread ID (the *id* field returned by :meth:`threads` method). When calling methods of this class, always be prepared to catch :exc:`NoSuchProcess` and :exc:`AccessDenied` exceptions. - `hash`_ builtin can be used against instances of this class in order to + :func:`hash` builtin can be used against instances of this class in order to identify a process univocally over time (the hash is determined by mixing - process PID + creation time). As such it can also be used with `set`_. + process PID + creation time). As such it can also be used with :class:`set`. .. note:: @@ -1266,7 +1280,7 @@ Process class The process parent PID. On Windows the return value is cached after the first call. On POSIX it is not cached because the ppid may change if the - process becomes a zombie. + process becomes a :term:`zombie process`. See also :meth:`parent` and :meth:`parents` methods. .. method:: name() @@ -1398,6 +1412,8 @@ Process class The current process status as a :class:`psutil.ProcessStatus` enum member. The returned value is one of the `psutil.STATUS_* <#process-status-constants>`_ constants. + A common use case is detecting :term:`zombie processes ` + (``p.status() == psutil.STATUS_ZOMBIE``). .. versionchanged:: 8.0.0 return value is now a :class:`psutil.ProcessStatus` enum member instead @@ -1421,14 +1437,14 @@ Process class .. method:: uids() The real, effective and saved user ids of this process as a named tuple. - This is the same as `os.getresuid`_ but can be used for any process PID. + This is the same as :func:`os.getresuid` but can be used for any process PID. .. availability:: UNIX .. method:: gids() The real, effective and saved group ids of this process as a named tuple. - This is the same as `os.getresgid`_ but can be used for any process PID. + This is the same as :func:`os.getresgid` but can be used for any process PID. .. availability:: UNIX @@ -1455,7 +1471,7 @@ Process class >>> Starting from Python 3.3 this functionality is also available as - `os.getpriority`_ and `os.setpriority`_ (see `BPO-10784`_). + :func:`os.getpriority` and :func:`os.setpriority` (see `BPO-10784`_). On Windows this is implemented via `GetPriorityClass`_ and `SetPriorityClass`_ Windows APIs and *value* is one of the :data:`psutil.*_PRIORITY_CLASS ` @@ -1484,23 +1500,23 @@ Process class Linux (see `ioprio_get`_ manual): - * ``IOPRIO_CLASS_RT``: (high) the process gets first access to the disk + * :const:`IOPRIO_CLASS_RT`: (high) the process gets first access to the disk every time. Use it with care as it can starve the entire system. Additional priority *level* can be specified and ranges from ``0`` (highest) to ``7`` (lowest). - * ``IOPRIO_CLASS_BE``: (normal) the default for any process that hasn't set + * :const:`IOPRIO_CLASS_BE`: (normal) the default for any process that hasn't set a specific I/O priority. Additional priority *level* ranges from ``0`` (highest) to ``7`` (lowest). - * ``IOPRIO_CLASS_IDLE``: (low) get I/O time when no-one else needs the disk. + * :const:`IOPRIO_CLASS_IDLE`: (low) get I/O time when no-one else needs the disk. No additional *value* is accepted. - * ``IOPRIO_CLASS_NONE``: returned when no priority was previously set. + * :const:`IOPRIO_CLASS_NONE`: returned when no priority was previously set. Windows: - * ``IOPRIO_HIGH``: highest priority. - * ``IOPRIO_NORMAL``: default priority. - * ``IOPRIO_LOW``: low priority. - * ``IOPRIO_VERYLOW``: lowest priority. + * :const:`IOPRIO_HIGH`: highest priority. + * :const:`IOPRIO_NORMAL`: default priority. + * :const:`IOPRIO_LOW`: low priority. + * :const:`IOPRIO_VERYLOW`: lowest priority. Here's an example on how to set the highest I/O priority depending on what platform you're on: @@ -1520,7 +1536,7 @@ Process class .. availability:: Linux, Windows .. versionchanged:: 5.6.2 - Windows accepts new ``IOPRIO_*`` constants. + Windows accepts new :data:`IOPRIO_* ` constants. .. versionchanged:: 8.0.0 *ioclass* is now a :class:`psutil.ProcessIOPriority` enum member. @@ -1528,14 +1544,14 @@ Process class .. method:: rlimit(resource, limits=None) - Get or set process resource limits (see `man prlimit`_). *resource* is one + Get or set process :term:`resource limits ` (see `man prlimit`_). *resource* is one of the `psutil.RLIMIT_* <#process-resources-constants>`_ constants. *limits* is a ``(soft, hard)`` tuple. - This is the same as `resource.getrlimit`_ and `resource.setrlimit`_ - but can be used for any process PID, not only `os.getpid`_. + This is the same as :func:`resource.getrlimit` and :func:`resource.setrlimit` + but can be used for any process PID, not only :func:`os.getpid`. For get, return value is a ``(soft, hard)`` tuple. Each value may be either and integer or :data:`psutil.RLIMIT_* `. - Also see `procinfo.py`_ script. + Also see `scripts/procinfo.py`_ script. .. code-block:: pycon @@ -1558,27 +1574,25 @@ Process class For Linux you can refer to `/proc filesystem documentation `_. - - **read_count**: the number of read operations performed (cumulative). + All fields are :term:`cumulative counters ` since process creation. + + - **read_count**: the number of read operations performed. This is supposed to count the number of read-related syscalls such as ``read()`` and ``pread()`` on UNIX. - - **write_count**: the number of write operations performed (cumulative). + - **write_count**: the number of write operations performed. This is supposed to count the number of write-related syscalls such as ``write()`` and ``pwrite()`` on UNIX. - - **read_bytes**: the number of bytes read (cumulative). - Always ``-1`` on BSD. - - **write_bytes**: the number of bytes written (cumulative). - Always ``-1`` on BSD. + - **read_bytes**: the number of bytes read. Always ``-1`` on BSD. + - **write_bytes**: the number of bytes written. Always ``-1`` on BSD. Linux specific: - **read_chars** *(Linux)*: the amount of bytes which this process passed - to ``read()`` and ``pread()`` syscalls (cumulative). - Differently from *read_bytes* it doesn't care whether or not actual - physical disk I/O occurred. + to ``read()`` and ``pread()`` syscalls. Differently from *read_bytes* + it doesn't care whether or not actual physical disk I/O occurred. - **write_chars** *(Linux)*: the amount of bytes which this process passed - to ``write()`` and ``pwrite()`` syscalls (cumulative). - Differently from *write_bytes* it doesn't care whether or not actual - physical disk I/O occurred. + to ``write()`` and ``pwrite()`` syscalls. Differently from *write_bytes* + it doesn't care whether or not actual physical disk I/O occurred. Windows specific: @@ -1603,8 +1617,8 @@ Process class .. method:: num_ctx_switches() - The number of voluntary and involuntary context switches performed by - this process (cumulative). + The number of :term:`context switches ` performed by this process + (:term:`cumulative counter`). .. note:: (Windows, macOS) *involuntary* value is always set to 0, while @@ -1616,14 +1630,14 @@ Process class .. method:: num_fds() - The number of file descriptors currently opened by this process + The number of :term:`file descriptors ` currently opened by this process (non cumulative). .. availability:: UNIX .. method:: num_handles() - The number of handles currently used by this process (non cumulative). + The number of :term:`handles ` currently used by this process (non cumulative). .. availability:: Windows @@ -1639,16 +1653,17 @@ Process class - **id**: the native thread ID assigned by the kernel. If :attr:`pid` refers to the current process, this matches the `native_id `_ - attribute of the `threading.Thread`_ class, and can be used to reference + attribute of the :class:`threading.Thread` class, and can be used to reference individual Python threads running within your own Python app. - **user_time**: time spent in user mode. - **system_time**: time spent in kernel mode. .. method:: cpu_times() - Return a named tuple representing the accumulated process times, in seconds + Return a :term:`named tuple` of :term:`cumulative counters ` (seconds) + representing the accumulated process CPU times (see `explanation `_). - This is similar to `os.times`_ but can be used for any process PID. + This is similar to :func:`os.times` but can be used for any process PID. - **user**: time spent in user mode. - **system**: time spent in kernel mode. @@ -1656,7 +1671,7 @@ Process class Windows and macOS). - **children_system**: system time of all child processes (always ``0`` on Windows and macOS). - - **iowait**: (Linux) time spent waiting for blocking I/O to complete. + - **iowait**: (Linux) time spent waiting for blocking I/O to complete (:term:`iowait`). This value is excluded from `user` and `system` times count (because the CPU is not doing any work). @@ -1766,7 +1781,7 @@ Process class On FreeBSD certain kernel process may return ``-1``. It may be used in conjunction with ``psutil.cpu_percent(percpu=True)`` to observe the system workload distributed across multiple CPUs as shown by - `cpu_distribution.py`_ example script. + `scripts/cpu_distribution.py`_ example script. .. availability:: Linux, FreeBSD, SunOS @@ -1797,13 +1812,13 @@ Process class | | | | | | peak_vms | +---------+---------+----------+---------+-----+-----------------+ - - **rss**: aka "Resident Set Size". The portion of physical memory + - **rss**: aka :term:`RSS`. The portion of physical memory currently held by this process (code, data, stack, and mapped files that are resident). Pages swapped out to disk are not counted. On UNIX it matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. See also :ref:`faq_memory_rss_vs_vms` FAQ. - - **vms**: aka "Virtual Memory Size". The total address space reserved by + - **vms**: aka :term:`VMS`. The total address space reserved by the process, including pages not yet touched, pages in swap, and memory-mapped files not yet accessed. Typically much larger than **rss**. On UNIX it matches the ``top`` VIRT column. On Windows this @@ -1829,8 +1844,9 @@ Process class - **stack** *(BSD)*: size of the process stack segment. Reported separately from **data** (unlike Linux where both are combined). - - **peak_rss** *(BSD, Windows)*: the highest RSS value (high water mark) - the process has ever reached. On BSD this may be ``0`` for kernel PIDs. + - **peak_rss** *(BSD, Windows)*: the highest :term:`RSS` value (high water mark) + the process has ever reached. See :term:`peak_rss`. On BSD this may be + ``0`` for kernel PIDs. On Windows it maps to ``PeakWorkingSetSize``. - **peak_vms** *(Windows)*: peak private committed (page-file-backed) @@ -1898,13 +1914,12 @@ Process class | hugetlb | phys_footprint | | +-------------+----------------+--------------------+ - - **peak_rss** *(Linux, macOS)*: the highest RSS value (high water mark) - the process has reached since it started. + - **peak_rss** *(Linux, macOS)*: the highest :term:`RSS` value (high water + mark) the process has reached since it started. See :term:`peak_rss`. - **peak_vms** *(Linux)*: the highest VMS value the process has reached since it started. - - **rss_anon** *(Linux, macOS)*: resident anonymous pages (heap, - stack, private mappings) not backed by any file, such as heap - allocations, stack, and private ``mmap(MAP_ANONYMOUS)`` regions. Set to 0 + - **rss_anon** *(Linux, macOS)*: resident :term:`anonymous memory` pages + (heap, stack, private mappings) not backed by any file. Set to 0 on Linux < 4.5. - **rss_file** *(Linux, macOS)*: resident file-backed memory; pages mapped from files (shared libraries, mmap'd files). Set to 0 on Linux < 4.5. @@ -1952,12 +1967,12 @@ Process class considerably slower than :meth:`memory_info` and may require elevated privileges. - - **uss** *(Linux, macOS, Windows)*: aka "Unique Set Size". This is the + - **uss** *(Linux, macOS, Windows)*: aka :term:`USS`. This is the memory which is unique to a process and which would be freed if the process were terminated right now. The most representative metric for actual memory usage. - - **pss** *(Linux)*: aka "Proportional Set Size", is the amount of memory + - **pss** *(Linux)*: aka :term:`PSS`, is the amount of memory shared with other processes, accounted in a way that the amount is divided evenly between the processes that share it. I.e. if a process has 10 MBs all to itself, and 10 MBs shared with another process, its PSS @@ -1975,7 +1990,7 @@ Process class >>> p.memory_footprint() pfootprint(uss=6545408, pss=6872064, swap=0) - See also `procsmem.py`_ for an example application. + See also `scripts/procsmem.py`_ for an example application. .. versionadded:: 8.0.0 @@ -2005,13 +2020,13 @@ Process class .. method:: memory_maps(grouped=True) - Return process's mapped memory regions as a list of named tuples whose + Return process's memory-mapped file regions as a list of named tuples whose fields vary by platform (all values in bytes). If *grouped* is ``True`` regions with the same *path* are merged and their numeric fields summed. If *grouped* is ``False`` each region is listed individually and the tuple also includes *addr* (address range) and *perms* (permission string e.g. ``"r-xp"``). - See `pmap.py`_ for an example application. + See `scripts/pmap.py`_ for an example application. +---------------+---------+--------------+-----------+ | Linux | Windows | FreeBSD | Solaris | @@ -2053,7 +2068,7 @@ Process class before they can be reclaimed. The key indicator of a mapping's real memory cost. - **referenced**: pages recently accessed. - - **anonymous**: pages not backed by a file (heap, stack allocations). + - **anonymous**: :term:`anonymous memory` pages not backed by a file (heap, stack allocations). - **swap**: pages from this mapping currently in swap. FreeBSD fields: @@ -2105,13 +2120,12 @@ Process class returned either as the reference to process A is lost. This concept is well illustrated by this `unit test `_. - See also how to `kill a process tree <#kill-process-tree>`_ and - `terminate my children <#terminate-my-children>`_. + See also how to :ref:`kill a process tree `. .. method:: page_faults() - Return the number of page faults for this process as a ``(minor, major)`` - named tuple. + Return the number of :term:`page faults ` for this process as a + ``(minor, major)`` :term:`named tuple`. - **minor** (a.k.a. *soft* faults): occur when a memory page is not currently mapped into the process address space, but is already present @@ -2121,7 +2135,7 @@ Process class from disk. These are expensive because they stall the process until I/O completes. - Both counters are cumulative since process creation. + Both counters are :term:`cumulative counters ` since process creation. .. code-block:: pycon @@ -2144,13 +2158,13 @@ Process class - **position** (*Linux*): the file (offset) position. - **mode** (*Linux*): a string indicating how the file was opened, similarly - to `open`_ builtin ``mode`` argument. + to :func:`open` builtin ``mode`` argument. Possible values are ``'r'``, ``'w'``, ``'a'``, ``'r+'`` and ``'a+'``. There's no distinction between files opened in binary or text mode (``"b"`` or ``"t"``). - **flags** (*Linux*): the flags which were passed to the underlying - `os.open`_ C call when the file was opened (e.g. `os.O_RDONLY`_, - `os.O_TRUNC`_, etc). + :func:`os.open` C call when the file was opened (e.g. :data:`os.O_RDONLY`, + :data:`os.O_TRUNC`, etc). .. code-block:: pycon @@ -2190,13 +2204,13 @@ Process class Every named tuple provides 6 attributes: - **fd**: the socket file descriptor. If the connection refers to the - current process this may be passed to `socket.fromfd`_ to obtain a usable + current process this may be passed to :func:`socket.fromfd` to obtain a usable socket object. On Windows, FreeBSD and SunOS this is always set to ``-1``. - - **family**: the address family, either `AF_INET`_, `AF_INET6`_ or - `AF_UNIX`_. - - **type**: the address type, either `SOCK_STREAM`_, `SOCK_DGRAM`_ or - `SOCK_SEQPACKET`_. . + - **family**: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6` or + :data:`socket.AF_UNIX`. + - **type**: the address type, either :data:`socket.SOCK_STREAM`, :data:`socket.SOCK_DGRAM` or + :data:`socket.SOCK_SEQPACKET`. . - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -2292,13 +2306,13 @@ Process class Return whether the current process is running in the current process list. Differently from ``psutil.pid_exists(p.pid)``, this is reliable also in - case the process is gone and its PID reused by another process. + case the process is gone and its PID reused by another process (:term:`PID reuse`). If PID has been reused, this method will also remove the process from :func:`process_iter` internal cache. .. note:: - this will return ``True`` also if the process is a zombie + this will return ``True`` also if the process is a :term:`zombie process` (``p.status() == psutil.STATUS_ZOMBIE``). .. versionchanged:: 6.0.0 @@ -2307,13 +2321,12 @@ Process class .. method:: send_signal(signal) - Send a signal to process (see `signal module`_ constants) preemptively - checking whether PID has been reused. + Send a :term:`signal` to process (see :mod:`signal` module constants) + preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals are supported and *SIGTERM* is treated as an alias for :meth:`kill`. - See also how to `kill a process tree <#kill-process-tree>`_ and - `terminate my children <#terminate-my-children>`_. + See also how to :ref:`kill a process tree `. .. versionchanged:: 3.2.0 support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows was @@ -2321,35 +2334,33 @@ Process class .. method:: suspend() - Suspend process execution with *SIGSTOP* signal preemptively checking - whether PID has been reused. + Suspend process execution with *SIGSTOP* :term:`signal` preemptively + checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGSTOP)``. On Windows this is done by suspending all process threads execution. .. method:: resume() - Resume process execution with *SIGCONT* signal preemptively checking - whether PID has been reused. + Resume process execution with *SIGCONT* :term:`signal` preemptively + checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGCONT)``. On Windows this is done by resuming all process threads execution. .. method:: terminate() - Terminate the process with *SIGTERM* signal preemptively checking + Terminate the process with *SIGTERM* :term:`signal` preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGTERM)``. On Windows this is an alias for :meth:`kill`. - See also how to `kill a process tree <#kill-process-tree>`_ and - `terminate my children <#terminate-my-children>`_. + See also how to :ref:`kill a process tree `. .. method:: kill() - Kill the current process by using *SIGKILL* signal preemptively + Kill the current process by using *SIGKILL* :term:`signal` preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. On Windows this is done by using `TerminateProcess`_. - See also how to `kill a process tree <#kill-process-tree>`_ and - `terminate my children <#terminate-my-children>`_. + See also how to :ref:`kill a process tree `. .. method:: wait(timeout=None) @@ -2360,7 +2371,7 @@ Process class positive integer >= 0 indicating the exit code. If the process was terminated by a signal return the negated value of the signal which caused the termination (e.g. ``-SIGTERM``). - If PID is not a child of `os.getpid`_ (current process) just wait until + If PID is not a child of :func:`os.getpid` (current process) just wait until the process disappears and return ``None``. If PID does not exist return ``None`` immediately. @@ -2388,10 +2399,10 @@ Process class When ``timeout`` is not ``None`` and the platform supports it, an efficient event-driven mechanism is used to wait for process termination: - - Linux >= 5.3 with Python >= 3.9 uses `os.pidfd_open`_ + `select.poll`_ - - macOS and other BSD variants use `select.kqueue`_ + ``KQ_FILTER_PROC`` + - Linux >= 5.3 with Python >= 3.9 uses :func:`os.pidfd_open` + :func:`select.poll` + - macOS and other BSD variants use :func:`select.kqueue` + ``KQ_FILTER_PROC`` + ``KQ_NOTE_EXIT`` - - Windows uses ``WaitForSingleObject`` + - Windows uses `WaitForSingleObject`_ If none of these mechanisms are available, the function falls back to a busy loop (non-blocking call and short sleeps). @@ -2405,11 +2416,11 @@ Process class .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it as a human readable - `enum`_. + :mod:`enum`. .. versionchanged:: 7.2.2 - on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, use `os.pidfd_open`_ and - `select.kqueue`_ respectively, instead of less efficient busy-loop + on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, use :func:`os.pidfd_open` and + :func:`select.kqueue` respectively, instead of less efficient busy-loop polling. ---- @@ -2419,7 +2430,7 @@ Popen class .. class:: Popen(*args, **kwargs) - Same as `subprocess.Popen`_ but in addition it provides all + Same as :class:`subprocess.Popen` but in addition it provides all :class:`psutil.Process` methods in a single class. For the following methods which are common to both classes, psutil implementation takes precedence: @@ -2643,14 +2654,14 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ProcessStatus - `enum.StrEnum`_ collection of :data:`STATUS_* ` + :class:`enum.StrEnum` collection of :data:`STATUS_* ` constants. Returned by :meth:`Process.status`. .. versionadded:: 8.0.0 .. class:: psutil.ProcessPriority - `enum.IntEnum`_ collection of + :class:`enum.IntEnum` collection of :data:`*_PRIORITY_CLASS ` constants for :meth:`Process.nice` on Windows. @@ -2660,9 +2671,11 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ProcessIOPriority - `enum.IntEnum`_ collection of I/O priority constants for - :meth:`Process.ionice`. On Linux: ``IOPRIO_CLASS_*`` constants. - On Windows: ``IOPRIO_*`` constants. + :class:`enum.IntEnum` collection of I/O priority constants for + :meth:`Process.ionice`. + + :data:`IOPRIO_CLASS_* ` on Linux, + :data:`IOPRIO_* ` on Windows. .. availability:: Linux, Windows @@ -2670,7 +2683,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ProcessRlimit - `enum.IntEnum`_ collection of :data:`RLIMIT_* ` + :class:`enum.IntEnum` collection of :data:`RLIMIT_* ` constants for :meth:`Process.rlimit`. .. availability:: Linux, FreeBSD @@ -2679,7 +2692,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ConnectionStatus - `enum.StrEnum`_ collection of :data:`CONN_* ` + :class:`enum.StrEnum` collection of :data:`CONN_* ` constants. Returned in the *status* field of :func:`psutil.net_connections` and :meth:`Process.net_connections`. @@ -2687,14 +2700,14 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.NicDuplex - `enum.IntEnum`_ collection of :data:`NIC_DUPLEX_* ` + :class:`enum.IntEnum` collection of :data:`NIC_DUPLEX_* ` constants. Returned in the *duplex* field of :func:`psutil.net_if_stats`. .. versionadded:: 3.0.0 .. class:: psutil.BatteryTime - `enum.IntEnum`_ collection of :data:`POWER_TIME_* ` + :class:`enum.IntEnum` collection of :data:`POWER_TIME_* ` constants. May appear in the *secsleft* field of :func:`psutil.sensors_battery`. .. versionadded:: 5.1.0 @@ -2703,6 +2716,7 @@ Operating system constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. _const-oses: + .. data:: POSIX .. data:: LINUX .. data:: WINDOWS @@ -2733,6 +2747,7 @@ Process status constants ^^^^^^^^^^^^^^^^^^^^^^^^ .. _const-pstatus: + .. data:: STATUS_RUNNING .. data:: STATUS_SLEEPING .. data:: STATUS_DISK_SLEEP @@ -2764,6 +2779,7 @@ Process priority constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. _const-prio: + .. data:: REALTIME_PRIORITY_CLASS .. data:: HIGH_PRIORITY_CLASS .. data:: ABOVE_NORMAL_PRIORITY_CLASS @@ -2783,7 +2799,8 @@ Process priority constants integers). See :ref:`migration guide `. -.. _const-ioprio: +.. _const-ioprio-linux: + .. data:: IOPRIO_CLASS_NONE .. data:: IOPRIO_CLASS_RT .. data:: IOPRIO_CLASS_BE @@ -2794,11 +2811,11 @@ Process priority constants process I/O priority. These constants are members of the :class:`psutil.ProcessIOPriority` enum. - *IOPRIO_CLASS_NONE* and *IOPRIO_CLASS_BE* (best effort) is the default for - any process that hasn't set a specific I/O priority. - *IOPRIO_CLASS_RT* (real time) means the process is given first access to the - disk, regardless of what else is going on in the system. - *IOPRIO_CLASS_IDLE* means the process will get I/O time when no-one else + :const:`IOPRIO_CLASS_NONE` and :const:`IOPRIO_CLASS_BE` (best effort) is the + default for any process that hasn't set a specific I/O priority. + :const:`IOPRIO_CLASS_RT` (real time) means the process is given first access + to the disk, regardless of what else is going on in the system. + :const:`IOPRIO_CLASS_IDLE` means the process will get I/O time when no-one else needs the disk. For further information refer to manuals of `ionice `_ command line utility or @@ -2811,6 +2828,8 @@ Process priority constants (previously ``IOPriority`` enum). See :ref:`migration guide `. +.. _const-ioprio-windows: + .. data:: IOPRIO_VERYLOW .. data:: IOPRIO_LOW .. data:: IOPRIO_NORMAL @@ -2834,6 +2853,8 @@ Process priority constants Process resource constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. _const-rlimit: + Linux / FreeBSD: .. data:: RLIM_INFINITY @@ -2864,7 +2885,7 @@ FreeBSD specific: .. data:: RLIMIT_NPTS Constants used for getting and setting process resource limits to be used in -conjunction with :meth:`Process.rlimit`. See `resource.getrlimit`_ +conjunction with :meth:`Process.rlimit`. See :func:`resource.getrlimit` for further information. These constants are members of the :class:`psutil.ProcessRlimit` enum. @@ -2883,6 +2904,7 @@ Connections constants ^^^^^^^^^^^^^^^^^^^^^ .. _const-conn: + .. data:: CONN_ESTABLISHED .. data:: CONN_SYN_SENT .. data:: CONN_SYN_RECV @@ -2913,6 +2935,7 @@ Hardware constants ^^^^^^^^^^^^^^^^^^ .. _const-aflink: + .. data:: AF_LINK Constant which identifies a MAC address associated with a network interface. @@ -2921,19 +2944,21 @@ Hardware constants .. versionadded:: 3.0.0 .. _const-duplex: + .. data:: NIC_DUPLEX_FULL .. data:: NIC_DUPLEX_HALF .. data:: NIC_DUPLEX_UNKNOWN - Constants which identifies whether a NIC (network interface card) has full or - half mode speed. NIC_DUPLEX_FULL means the NIC is able to send and receive - data (files) simultaneously, NIC_DUPLEX_FULL means the NIC can either send or - receive data at a time. + Constants which identifies whether a :term:`NIC` (network interface card) has + full or half mode speed. NIC_DUPLEX_FULL means the NIC is able to send and + receive data (files) simultaneously, NIC_DUPLEX_FULL means the NIC can either + send or receive data at a time. To be used in conjunction with :func:`psutil.net_if_stats`. .. versionadded:: 3.0.0 .. _const-power: + .. data:: POWER_TIME_UNKNOWN .. data:: POWER_TIME_UNLIMITED @@ -2947,6 +2972,7 @@ Other constants ^^^^^^^^^^^^^^^ .. _const-procfs_path: + .. data:: PROCFS_PATH The path of the /proc filesystem on Linux, Solaris and AIX (defaults to @@ -2971,6 +2997,7 @@ Other constants also available on AIX. .. _const-version-info: + .. data:: version_info A tuple to check psutil installed version. Example: @@ -2992,20 +3019,20 @@ Other constants .. === scripts -.. _`battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py -.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py -.. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py -.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py -.. _`temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py +.. _`scripts/battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`scripts/cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`scripts/disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`scripts/fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`scripts/ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`scripts/iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`scripts/meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`scripts/netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`scripts/nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`scripts/pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`scripts/procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`scripts/procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`scripts/sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`scripts/temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py .. === Windows API @@ -3014,3 +3041,4 @@ Other constants .. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex .. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass .. _`TerminateProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess +.. _`WaitForSingleObject`: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject diff --git a/docs/changelog.rst b/docs/changelog.rst index b0039176b5..4d2aa95a6f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,10 +6,15 @@ Changelog 8.0.0 (IN DEVELOPMENT) ^^^^^^^^^^^^^^^^^^^^^^ +**Compatibility notes** + + psutil 8.0 introduces breaking API changes and drops support for Python 3.6. + See the :ref:`migration guide ` if upgrading from 7.x. + **Enhancements** Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, -:gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`) +:gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`) - Split docs from a single HTML file into multiple sections (API reference, install, etc.). @@ -20,6 +25,8 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, show code samples - `/adoption `__: notable software using psutil + - `/glossary `__: + a section explaining the core concepts - `/shell_equivalents `__: maps each psutil API to native CLI commands - `/install `__ @@ -125,7 +132,7 @@ Others **Bug fixes** -- :gh:`2770`, [Linux]: fix :func:`cpu_count_cores()` raising ``ValueError`` +- :gh:`2770`, [Linux]: fix :func:`cpu_count_cores` raising ``ValueError`` on s390x architecture, where ``/proc/cpuinfo`` uses spaces before the colon separator instead of a tab. - :gh:`2726`, [macOS]: :meth:`Process.num_ctx_switches` return an unusual @@ -140,11 +147,6 @@ Others fields, are erroneously reported in memory pages instead of bytes. Other platforms (Linux, macOS, Windows) return bytes. -**Compatibility notes** - - psutil 8.0 introduces breaking API changes and drops support for Python 3.6. - See the :ref:`migration guide ` if upgrading from 7.x. - 7.2.3 — 2026-02-08 ^^^^^^^^^^^^^^^^^^ diff --git a/docs/conf.py b/docs/conf.py index 22660413ed..7ac1a4bcc6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,7 @@ extensions = [ "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_copybutton", # custom extensions in _ext/ dir @@ -43,6 +44,9 @@ author = AUTHOR version = VERSION release = VERSION +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} extlinks = { 'gh': ('https://github.com/giampaolo/psutil/issues/%s', '#%s'), } diff --git a/docs/faq.rst b/docs/faq.rst index 4a9cf96cd9..74db2d64e2 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -119,11 +119,8 @@ An even simpler pattern is to catch :exc:`Error`, which implies both What is ZombieProcess? ^^^^^^^^^^^^^^^^^^^^^^^ -:exc:`ZombieProcess` is a subclass of :exc:`NoSuchProcess` that is raised -on UNIX when a process has terminated but has not yet been reaped by its -parent. The process has finished executing but its entry remains in the -process table until the parent calls ``wait()`` (or the parent itself -exits). +:exc:`ZombieProcess` is a subclass of :exc:`NoSuchProcess` raised on UNIX +for a :term:`zombie process`. **What you can and cannot do with a zombie:** diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000000..cbfc822643 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,348 @@ +.. currentmodule:: psutil + +Glossary +======== + +.. glossary:: + :sorted: + + anonymous memory + + RAM used by the program that is not associated with any file (unlike the + :term:`page cache`), such as the heap, the stack, and other memory + allocated directly by the program (e.g. via ``malloc()``). + Anonymous pages have no on-disk counterpart and must be written to swap + if evicted. Visible in the ``path`` column of :meth:`Process.memory_maps` + as ``"[heap]"``, ``"[stack]"``, or an empty string. + + available memory + + The amount of RAM that can be given to processes without the system + going into swap. This is the right field to watch for memory + pressure, not ``free``. ``free`` is often deceptively low because + the OS keeps recently freed pages as reclaimable cache; those pages + are counted in ``available`` but not in ``free``. A monitoring + alert should fire on ``available`` (or ``percent``) falling below a + threshold, not on ``free``. See :func:`virtual_memory`. + + busy_time + + A :term:`cumulative counter` (milliseconds) tracking the time a + disk device spent actually performing I/O, as reported in the + ``busy_time`` field of :func:`disk_io_counters` (Linux and FreeBSD + only). To use it, sample twice and divide the delta by elapsed + time to get a utilisation percentage (analogous to CPU percent but + for disks). When it approaches 100% the disk queue is growing and + I/O latency will spike. Unlike ``read_bytes``/``write_bytes``, + ``busy_time`` reveals saturation even when throughput looks modest + (e.g. many small random I/Os). + + CPU affinity + + A property of a process (or thread) that restricts which logical CPUs + it is allowed to run on. For example, pinning a process to CPU 0 and + CPU 1 prevents the OS scheduler from moving it to other cores. See + :meth:`Process.cpu_affinity`. + + CPU percent + + The fraction of CPU time consumed by a process or the whole system + over a measurement interval, expressed as a percentage. A value of + 100 % means one full logical CPU core was busy for the entire + interval. Values above 100 % are possible on multi-core systems when + multiple threads run in parallel. See :func:`cpu_percent` and + :meth:`Process.cpu_percent`. + + CPU times + + :term:`Cumulative counters ` (in seconds) recording + how much time a CPU or process spent in different modes: + **user** (normal code), **system** (kernel code on behalf of the process), + **idle**, **iowait**, etc. + These always increase monotonically. See :func:`cpu_times` and + :meth:`Process.cpu_times`. + + context switch + + Occurs whenever the CPU stops executing one process or thread and starts + executing another. Frequent context switching can indicate high system + load or excessive thread contention. See :func:`cpu_stats` and + :meth:`Process.num_ctx_switches`. + + The ``voluntary`` and ``involuntary`` fields of + :meth:`Process.num_ctx_switches` tell you *why* the process was switched + out. A **voluntary** switch means the process gave up the CPU itself + (waiting for I/O, a lock, or a timer); a high rate is normal for + I/O-bound processes. An **involuntary** switch means the OS forcibly took + the CPU away (time slice expired, higher-priority process woke up); a + high rate means the process wants to run but keeps getting interrupted — + a sign it is competing for CPU. If involuntary switches dominate, adding + CPU capacity or reducing other load will directly speed up the process; + if voluntary switches dominate, the bottleneck is I/O or locking, not + CPU. + + cumulative counter + + A field whose value only increases over time (since boot or process + creation) and never resets. Examples include :func:`cpu_times`, + :func:`disk_io_counters`, :func:`net_io_counters`, + :meth:`Process.io_counters`, and :meth:`Process.num_ctx_switches`. + The raw value is rarely useful on its own; divide the delta between + two samples by the elapsed time to get a meaningful rate (e.g. + bytes per second, context switches per second). + + dropin / dropout + + Fields in :func:`net_io_counters` counting packets dropped at the + NIC level before they could be processed (``dropin``) or sent + (``dropout``). Unlike transmission errors, drops indicate the + interface or kernel buffer was overwhelmed. A non-zero and growing + count is a sign of network saturation or misconfiguration. + + file descriptor + + An integer handle used by UNIX processes to reference open files, + sockets, pipes, and other I/O resources. On Windows the equivalent + are *handles*. Leaking file descriptors (opening without closing) + eventually causes ``EMFILE`` / ``Too many open files`` errors. See + :meth:`Process.num_fds` and :meth:`Process.open_files`. + + handle + + On Windows, an opaque reference to a kernel object such as a file, + thread, process, event, mutex, or registry key. Handles are the + Windows equivalent of UNIX :term:`file descriptors `. Each open + handle consumes a small amount of kernel memory. Leaking / unclosed + handles eventually causes ``ERROR_NO_MORE_FILES`` or similar errors. See + :meth:`Process.num_handles`. + + hardware interrupt + + A signal sent by a hardware device (disk controller, NIC, keyboard) + to the CPU to request attention. Each interrupt briefly preempts + whatever the CPU was doing. Reported as the ``interrupts`` field of + :func:`cpu_stats` and ``irq`` field of :func:`cpu_times`. + A very high rate may indicate a misbehaving device driver or a heavily + loaded NIC. + + involuntary context switch + + See :term:`context switch`. + + iowait + + A CPU time field (Linux, SunOS, AIX) measuring time spent by the CPU + waiting for I/O operations to complete. High iowait indicates a + disk or network bottleneck. It is reported as part of + :func:`cpu_times` but is *not* included in the idle counter. + + ionice + + An I/O scheduling priority that controls how much disk bandwidth a + process receives. On Linux three scheduling classes are supported: + ``IOPRIO_CLASS_RT`` (real-time), ``IOPRIO_CLASS_BE`` (best-effort, + the default), and ``IOPRIO_CLASS_IDLE``. See + :meth:`Process.ionice`. + + logical CPU + + A CPU as seen by the operating system scheduler. On systems with + *hyper-threading* each physical core exposes two logical CPUs, so a + 4-core hyper-threaded chip has 8 logical CPUs. This is the count + returned by :func:`cpu_count` (the default) and the number of + entries returned by ``cpu_percent(percpu=True)``. See also + :term:`physical CPU`. + + load average + + Three floating-point values representing the average number of + processes in a *runnable* or *uninterruptible* state over the last + 1, 5, and 15 minutes. A load average equal to the number of logical + CPUs means the system is fully saturated. See :func:`getloadavg`. + + named tuple + + A :func:`collections.namedtuple` subclass, a tuple whose fields + can be accessed by name as well as by index. Most psutil functions + return named tuples (e.g. ``sswap``, ``pmem``, ``scputimes``). + + nice + + A process priority value that influences how much CPU time the OS + scheduler gives to a process. Lower nice values mean higher priority. + The range is −20 (highest priority) to 19 (lowest) on UNIX; on + Windows the concept maps to priority classes. See + :meth:`Process.nice`. + + page cache + + RAM used by the kernel to cache file data read from or written to disk. + When a process reads a file, the data stays in the page cache. Subsequent + reads are served from RAM without any disk I/O. The OS reclaims page + cache memory automatically under pressure, so a large cache is healthy. + Shown as the ``cached`` field of :func:`virtual_memory` on Linux/BSD. + See also :term:`available memory`. + + peak_rss + + The highest :term:`RSS` value a process has ever reached since it + started (memory high-water mark). Available via + :meth:`Process.memory_info` (BSD, Windows) and + :meth:`Process.memory_info_ex` (Linux, macOS). Useful for capacity + planning and leak detection: if ``peak_rss`` keeps growing across + successive runs or over time, the process is likely leaking memory. + + page fault + + An event that occurs when a process accesses a virtual memory page + that is not currently mapped in physical RAM. A **minor** fault is + resolved without disk I/O (e.g. the page is already in RAM but not + yet mapped, or it is copy-on-write). A **major** fault requires + reading the page from disk (e.g. from a memory-mapped file or the + swap area) and is significantly more expensive. Many major faults + may indicate memory pressure or excessive swapping. See + :meth:`Process.page_faults`. + + physical CPU + + An actual hardware CPU core on the motherboard, as opposed to a + :term:`logical CPU`. A single physical core may appear as multiple + logical CPUs when hyper-threading is enabled. The physical count is + returned by ``cpu_count(logical=False)``. + + PID + + *Process identifier*, a non-negative integer assigned by the OS to + every running process. PIDs are unique at any instant but are + recycled over time (:term:`PID reuse`). PID 0 is typically the + idle/swapper process; PID 1 is ``init`` / ``systemd`` on UNIX. + + PID reuse + + When a process exits its PID is eventually recycled and assigned to + a new process. A :class:`Process` object held across this boundary + would silently refer to the wrong process. psutil detects reuse by + comparing the recorded creation time against the live value, and + raises :exc:`NoSuchProcess` if they differ. + + PSS + + *Proportional Set Size*, the amount of RAM used by a process, + where shared pages are divided proportionally among all processes + that map them. PSS gives a fairer per-process memory estimate than + :term:`RSS` when shared libraries are involved. Available on Linux + via :meth:`Process.memory_footprint`. + + RSS + + *Resident Set Size*, the amount of physical RAM currently occupied + by a process, including shared library pages. It is the most + commonly reported memory metric (shown as ``RES`` in ``top``), but + it can be misleading because shared pages are counted in full for + every process that maps them. See :meth:`Process.memory_info`. + + status (process) + + The scheduling state of a process at a given instant. Common + values are: + + - ``running``: actively executing on a CPU. + - ``sleeping``: waiting for an event (interruptible sleep). + - ``disk-sleep``: waiting for I/O (uninterruptible sleep). + - ``stopped``: suspended via ``SIGSTOP`` or a debugger. + - ``zombie``: exited but not yet reaped by its parent. + - ``idle``: doing nothing. + + See :meth:`Process.status` and the ``STATUS_*`` constants. + + soft interrupt + + Deferred work scheduled by a :term:`hardware interrupt` handler to + run later in a less time-critical context (e.g. network packet + processing, block I/O completion). Using soft interrupts lets the + hardware interrupt return quickly while the heavier processing + happens shortly after. Reported as the ``soft_interrupts`` field of + :func:`cpu_stats`. A high rate usually points to heavy network or + disk I/O throughput rather than a hardware problem. + + swap-in + + A page moved from swap space on disk back into RAM. Reported as the + ``sin`` :term:`cumulative counter` of :func:`swap_memory`. On its + own a non-zero ``sin`` rate is not alarming — it may just mean the + system is reloading pages that were quietly evicted during idle + time. It becomes a concern when it coincides with a high + :term:`swap-out` rate, meaning the system is continuously trading + pages in and out. See also :term:`swap-out`. + + swap-out + + A page evicted from RAM to swap space on disk to free memory. + Reported as the ``sout`` :term:`cumulative counter` of + :func:`swap_memory`. A sustained non-zero rate is the clearest + sign of memory pressure: the system is running out of RAM and + actively offloading pages to disk. Compute the rate of change + over an interval rather than reading the absolute value. + See also :term:`swap-in`. + + swap memory + + Disk space used as an overflow extension of physical RAM. When the + OS runs low on RAM it *swaps out* memory pages to disk and restores + them on demand. Heavy swapping significantly degrades performance. + See :func:`swap_memory`. + + NIC + + *Network Interface Card*, a hardware or virtual network interface. + psutil uses this term when referring to per-interface network + statistics. See :func:`net_if_addrs` and :func:`net_if_stats`. + + resource limit + + A per-process cap on a system resource enforced by the kernel (POSIX + :data:`RLIMIT_* ` constants). + Each limit has a *soft* value (the current enforcement threshold, which + the process may raise up to the hard limit) and a *hard* value + (the ceiling, settable only by root). + Common limits include :data:`RLIM_INFINITY` (open file descriptors), + :data:`RLIMIT_AS` (virtual address space), and :data:`RLIMIT_CPU` + (CPU time in seconds). See :meth:`Process.rlimit`. + + signal + + An asynchronous notification delivered by the OS to a process to + indicate an event. The process can catch it (run a handler), ignore it, + or let the default action happen (often termination). Common signals on + UNIX: ``SIGTERM`` (request graceful shutdown), ``SIGKILL`` (force + immediate termination, cannot be caught), ``SIGSTOP`` / ``SIGCONT`` + (pause and resume). psutil exposes signals via :meth:`Process.send_signal`, + :meth:`Process.terminate`, :meth:`Process.suspend`, + :meth:`Process.resume` and :meth:`Process.kill`. See also :mod:`signal` module. + + USS + + *Unique Set Size*, the amount of RAM that belongs exclusively to a + process and would be freed if it exited. It excludes shared pages + entirely, making it the most accurate single-process memory metric. + Available on Linux, macOS, and Windows via + :meth:`Process.memory_full_info`. + + voluntary context switch + + See :term:`context switch`. + + VMS + + *Virtual Memory Size*, the total virtual address space reserved by a + process, including mapped files, shared libraries, stack, heap, and + swap. VMS is almost always much larger than :term:`RSS` because most + virtual pages are never actually loaded into RAM. See + :meth:`Process.memory_info`. + + zombie process + + A process that has exited but whose entry remains in the process + table until its parent calls ``wait()``. Zombies hold a PID but consume + no CPU or memory. + See :ref:`faq_zombie_process`. diff --git a/docs/index.rst b/docs/index.rst index 53f07ed1da..e448db5601 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -103,6 +103,7 @@ Table of Contents Shell equivalents Platform support Who uses psutil + Glossary Credits Development guide Migration diff --git a/docs/migration.rst b/docs/migration.rst index 246f3bca85..c40662ade4 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -101,7 +101,7 @@ Status and connection fields are now enums now returns a :class:`psutil.ConnectionStatus` member instead of a plain ``str``. -Because both are `enum.StrEnum`_ subclasses they compare equal to their +Because both are :class:`enum.StrEnum` subclasses they compare equal to their string values, so existing comparisons continue to work unchanged: .. code-block:: python diff --git a/docs/recipes.rst b/docs/recipes.rst index 21e67eede0..7a6b86d2c7 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -239,6 +239,8 @@ Periodically monitor CPU and memory usage of a process using Controlling processes ^^^^^^^^^^^^^^^^^^^^^ +.. _recipe_kill_proc_tree: + Kill a process tree (including grandchildren): .. code-block:: python From 9c4caed07a5bf335afd6605977c889e42c9a4c1b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Mar 2026 02:17:30 +0100 Subject: [PATCH 1612/1714] Docs: add alternatives section (#2775) --- MANIFEST.in | 1 + docs/_static/css/custom.css | 7 ++ docs/adoption.rst | 8 +- docs/alternatives.rst | 175 ++++++++++++++++++++++++++++++++++++ docs/changelog.rst | 7 +- docs/index.rst | 7 +- docs/migration.rst | 8 +- 7 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 docs/alternatives.rst diff --git a/MANIFEST.in b/MANIFEST.in index ed2d040670..1c2381a859 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -25,6 +25,7 @@ include docs/_static/favicon.ico include docs/_static/sidebar.js include docs/_templates/layout.html include docs/adoption.rst +include docs/alternatives.rst include docs/api.rst include docs/changelog.rst include docs/conf.py diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 26098cba90..2b08020163 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -19,6 +19,13 @@ footer div[role="navigation"][aria-label="Footer"] { .wy-table-responsive table th, .wy-table-responsive table td { padding: 10px !important; + white-space: normal !important; + word-wrap: break-word; +} + +.wy-table-responsive table { + table-layout: fixed; + width: 100%; } /* HTML tables */ diff --git a/docs/adoption.rst b/docs/adoption.rst index 942b14e291..6b5f100da3 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -9,6 +9,9 @@ most-downloaded packages on PyPI, with 280+ million downloads per month, it, and 14,000+ packages depending on it. The projects below are a small sample of notable software that depends on it. +See also :doc:`alternatives` for related Python libraries and equivalents in +other languages. + Infrastructure / automation --------------------------- @@ -120,10 +123,6 @@ System monitoring - System monitoring tool (top/htop alternative) - |glances-stars| - core dependency for all metrics - * - |osquery-logo| `OSQuery `__ - - SQL powered OS monitoring and analytics - - |osquery-stars| - - unit tests * - |bpytop-logo| `bpytop `__ - Terminal resource monitor - |bpytop-stars| @@ -197,7 +196,6 @@ How this list was compiled .. |homeassistant-stars| image:: https://img.shields.io/github/stars/home-assistant/core.svg?style=social&label=%20 .. |locust-stars| image:: https://img.shields.io/github/stars/locustio/locust.svg?style=social&label=%20 .. |mlflow-stars| image:: https://img.shields.io/github/stars/mlflow/mlflow.svg?style=social&label=%20 -.. |osquery-stars| image:: https://img.shields.io/github/stars/osquery/osquery.svg?style=social&label=%20 .. |psdash-stars| image:: https://img.shields.io/github/stars/Jahaja/psdash.svg?style=social&label=%20 .. |psleak-stars| image:: https://img.shields.io/github/stars/giampaolo/psleak.svg?style=social&label=%20 .. |pytorch-stars| image:: https://img.shields.io/github/stars/pytorch/pytorch.svg?style=social&label=%20 diff --git a/docs/alternatives.rst b/docs/alternatives.rst new file mode 100644 index 0000000000..b7740bb8f5 --- /dev/null +++ b/docs/alternatives.rst @@ -0,0 +1,175 @@ +.. currentmodule:: psutil + +Alternatives +============ + +This page describes Python tools and modules that overlap with psutil, +to help you pick the right tool for the job. +See also :doc:`adoption` for notable projects that use psutil. + +Python standard library +----------------------- + +os module +^^^^^^^^^ + +The :mod:`os` module provides a handful of process-related functions: +:func:`os.getpid`, :func:`os.getppid`, :func:`os.getuid`, +:func:`os.cpu_count`, :func:`os.getloadavg` (UNIX only). These are +cheap wrappers around POSIX syscalls and are perfectly fine when you +only need information about the *current* process and don't need +cross-platform code. + +psutil goes further in several directions. Its primary goal is to provide a +**single portable interface** for concepts that are natively UNIX-only. Things +like process CPU and memory usage, open file descriptors, network connections, +signals, nice levels, and I/O counters exist as first-class OS primitives on +Linux and macOS, but have no direct equivalent on Windows. psutil implements +all of them on Windows too (using Win32 APIs, ``NtQuerySystemInformation`` and +WMI) so that code written against psutil runs unmodified on every supported +platform. Beyond portability, it also exposes the same information for *any* +process (not just the current one), and returns structured named tuples instead +of raw integers. + +resource module +^^^^^^^^^^^^^^^ + +:mod:`resource` (UNIX only) lets you read and set resource limits +(``RLIMIT_*``) and get basic usage counters (user/system time, page +faults, I/O ops) for the *current* process or its children via +:func:`resource.getrusage`. It is the right tool when you specifically +want to enforce or inspect ``ulimit``-style limits. + +psutil's :meth:`Process.rlimit` exposes the same interface but extends it +to all processes, not just the caller. + +subprocess module +^^^^^^^^^^^^^^^^^ + +Calling ``ps``, ``top``, ``netstat``, ``vmstat`` via +:mod:`subprocess` and parsing the text output is fragile: output +formats differ across OS versions and locales, parsing is error-prone, +and spawning a subprocess per sample is slow. psutil reads the same +kernel data sources directly without spawning any external processes. + +platform module +^^^^^^^^^^^^^^^ + +:mod:`platform` provides information about the OS and Python runtime, +such as OS name, kernel version, architecture, and machine type. +It is useful for identifying the environment, but does not expose +runtime metrics or process information like psutil. Overlaps with +psutil's OS constants (:data:`LINUX`, :data:`WINDOWS`, :data:`MACOS`, +etc.). + +/proc filesystem +^^^^^^^^^^^^^^^^ + +On Linux, ``/proc`` exposes process and system information as virtual files. +Reading ``/proc/pid/status`` or ``/proc/meminfo`` directly is fast and has no +dependencies, which is why some minimal containers or scripts do this. The +downsides are that it is Linux-only, the format may vary across kernel +versions, and you have to parse raw text yourself. psutil parses ``/proc`` +internally, exposes the same information through a consistent +cross-platform API and handles edge cases (numeric overflow, compatibility +with old kernels, graceful fallbacks, etc.) transparently. + +Third-party libraries +--------------------- + +Libraries that cover areas psutil does not, or that go deeper on a +specific platform or subsystem. + +.. list-table:: + :header-rows: 1 + :widths: 5 25 + :class: longtable + + * - Library + - Focus + + * - `distro `_ + - Linux distro info (name, version, codename). psutil does not + expose OS details. + + * - `GPUtil `_ / + `pynvml `_ + - NVIDIA GPU utilization and VRAM usage. + + * - `ifaddr `_ + - Network interface address enumeration. + Overlaps with :func:`net_if_addrs`. + + * - `libvirt-python `_ + - Manage KVM/QEMU/Xen VMs: enumerate guests, query + CPU/memory allocation. Complements psutil's host-level view. + + * - `prometheus_client `_ + - Export metrics to Prometheus. Use *alongside* psutil. + + * - `py-cpuinfo `_ + - CPU brand string, microarchitecture, feature flags. + + * - `pyroute2 `_ + - Linux netlink (interfaces, routes, connections). + Overlaps with :func:`net_if_addrs`, :func:`net_if_stats`, + :func:`net_connections`. + + * - `pywifi `_ + - WiFi scanning, signal strength, SSID. Exposes wireless + details that :func:`net_if_addrs` does not. + + * - `pySMART `_ + - S.M.A.R.T. disk health data. Complements + :func:`disk_io_counters`. + + * - `pywin32 `_ + - Win32 API bindings (Windows only). + + * - `setproctitle `_ + - Set process title shown by ``ps``/``top``. Writes what + :meth:`Process.name` reads. + + * - `wmi `_ + - WMI interface (Windows only). + +Other languages +--------------- + +Equivalent libraries in other languages providing cross-platform system and +process information. + +.. list-table:: + :header-rows: 1 + :widths: 5 5 20 + :class: longtable + + * - Library + - Language + - Focus + + * - `gopsutil `_ + - Go + - CPU, memory, disk, network, processes. Directly inspired + by psutil and follows a similar API. + + * - `Hardware.Info `_ + - C# / .NET + - CPU, RAM, GPU, disk, network, battery. + + * - `hwinfo `_ + - C++ + - CPU, RAM, GPU, disks, mainboard. More hardware-focused. + + * - `OSHI `_ + - Java + - OS and hardware information: CPU, memory, disk, network, + processes, sensors, USB devices. + + * - `sysinfo `_ + - Rust + - CPU, memory, disk, network, processes, components. + + * - `systeminformation `_ + - Node.js + - CPU, memory, disk, network, processes, battery, Docker. diff --git a/docs/changelog.rst b/docs/changelog.rst index 4d2aa95a6f..4dec13da8b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,7 +14,8 @@ Changelog **Enhancements** Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, -:gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`) +:gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, +:gh:`2775`) - Split docs from a single HTML file into multiple sections (API reference, install, etc.). @@ -37,6 +38,8 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, summary of OSes and architectures support - `/faq `__: extended FAQ section. + - `/alternatives `__: + list of alternative Python libraries and tools that overlap with psutil. - `/migration `__: a section explaining how to migrate to newer psutil versions that break backward compatibility. @@ -74,7 +77,7 @@ Type hints / enums: - New APIs: -- :gh:`2729`: New :meth:`Process.page_faults` method, returning a ``(minor, +- :gh:`1541`: New :meth:`Process.page_faults` method, returning a ``(minor, major)`` namedtuple. - Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`, :gh:`2733`). diff --git a/docs/index.rst b/docs/index.rst index e448db5601..c200c662cb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -101,12 +101,13 @@ Table of Contents FAQ Recipes Shell equivalents + Glossary Platform support Who uses psutil - Glossary - Credits - Development guide + Alternatives Migration + Development guide + Credits Timeline .. toctree:: diff --git a/docs/migration.rst b/docs/migration.rst index c40662ade4..d9eb252e53 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -4,10 +4,6 @@ Migration guide =============== -.. contents:: - :local: - :depth: 2 - This page summarises the breaking changes introduced in each major release and shows the code changes required to upgrade. @@ -15,6 +11,10 @@ release and shows the code changes required to upgrade. Minor and patch releases (e.g. 6.1.x, 7.1.x) never contain breaking changes. Only major releases are listed here. +.. contents:: + :local: + :depth: 2 + .. _migration-8.0: Migrating to 8.0 From a59dae7c3998b0fd42b15ac8bf82e1fdbf8cdaa0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Mar 2026 02:40:31 +0100 Subject: [PATCH 1613/1714] Rename "namedtuple" to "named tuple" in doc, comments, docstrings --- docs/changelog.rst | 22 +++++++------- psutil/__init__.py | 62 ++++++++++++++++++++-------------------- psutil/_psaix.py | 2 +- psutil/_psbsd.py | 10 +++---- psutil/_pslinux.py | 6 ++-- psutil/_psosx.py | 8 +++--- psutil/_pssunos.py | 2 +- psutil/_pswindows.py | 4 +-- tests/__init__.py | 4 +-- tests/test_type_hints.py | 2 +- 10 files changed, 61 insertions(+), 61 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4dec13da8b..bc371c6bb6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,7 +22,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Added new sections: - - `/recipes `__: + - `/recipes `__: show code samples - `/adoption `__: notable software using psutil @@ -34,9 +34,9 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, (was old ``INSTALL.rst`` in root dir) - `/credits `__: list contributors and donors (was old ``CREDITS`` in root dir) - - `/platform `__: + - `/platform `__: summary of OSes and architectures support - - `/faq `__: + - `/faq `__: extended FAQ section. - `/alternatives `__: list of alternative Python libraries and tools that overlap with psutil. @@ -78,7 +78,7 @@ Type hints / enums: - New APIs: - :gh:`1541`: New :meth:`Process.page_faults` method, returning a ``(minor, - major)`` namedtuple. + major)`` named tuple. - Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`, :gh:`2733`). @@ -104,7 +104,7 @@ Type hints / enums: aliases returning 0 and emitting `DeprecationWarning` are kept. - macOS: *pfaults* and *pageins* removed with **no - backward-compataliases**. Use :meth:`Process.page_faults` instead. + backward-compatible aliases**. Use :meth:`Process.page_faults` instead. - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. @@ -424,7 +424,7 @@ Others **Enhancements** - :gh:`2109`: ``maxfile`` and ``maxpath`` fields were removed from the - namedtuple returned by :func:`disk_partitions`. Reason: on network + named tuple returned by :func:`disk_partitions`. Reason: on network filesystems (NFS) this can potentially take a very long time to complete. - :gh:`2366`, [Windows]: log debug message when using slower process APIs. - :gh:`2375`, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) @@ -466,7 +466,7 @@ Others Version 6.0.0 introduces some changes which affect backward compatibility: -- :gh:`2109`: the namedtuple returned by :func:`disk_partitions`' no longer +- :gh:`2109`: the named tuple returned by :func:`disk_partitions`' no longer has ``maxfile`` and ``maxpath`` fields. - :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether PIDs have been reused. If you want to check for PID reusage you are supposed @@ -1249,7 +1249,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1258`, [Windows], **[critical]**: :meth:`Process.username` may cause a segfault (Python interpreter crash). (patch by Jean-Luc Migot) -- :gh:`1273`: :func:`net_if_addrs` namedtuple's name has been renamed from +- :gh:`1273`: :func:`net_if_addrs` named tuple's name has been renamed from ``snic`` to ``snicaddr``. - :gh:`1274`, [Linux]: there was a small chance :meth:`Process.children` may swallow @@ -2240,7 +2240,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`448`, [Windows]: :meth:`Process.children` and :meth:`Process.ppid` memory leak (patch by Ulrich Klank). - :gh:`457`, [POSIX]: :func:`pid_exists` always returns ``True`` for PID 0. -- :gh:`461`: namedtuples are not pickle-able. +- :gh:`461`: named tuples are not pickle-able. - :gh:`466`, [Linux]: :meth:`Process.exe` improper null bytes handling. (patch by Gautam Singh) - :gh:`470`: :func:`wait_procs` might not wait. (patch by crusaderky) @@ -2648,7 +2648,7 @@ cases accessing the old names will work but it will cause a memory. Added new :func:`virtual_memory` and :func:`swap_memory` functions. All old memory-related functions are deprecated. Also two new example scripts were added: `free.py`_ and `meminfo.py`_. -- :gh:`312`: ``net_io_counters()`` namedtuple includes 4 new fields: ``errin``, +- :gh:`312`: ``net_io_counters()`` named tuple includes 4 new fields: ``errin``, ``errout``, ``dropin`` and ``dropout``, reflecting the number of packets dropped and with errors. @@ -2925,7 +2925,7 @@ cases accessing the old names will work but it will cause a provide ``pid``, ``name`` and ``msg`` attributes. - :gh:`97`: per-process children (:meth:`Process.children`). - :gh:`98`: :meth:`Process.cpu_times` and :meth:`Process.memory_info` now - return a namedtuple instead of a tuple. + return a named tuple instead of a tuple. - :gh:`103`: per-process opened TCP and UDP connections (:meth:`Process.connections`). - :gh:`107`, [Windows]: add support for Windows 64 bit. (patch by cjgohlke) diff --git a/psutil/__init__.py b/psutil/__init__.py index e0cd95f78f..ffae933add 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -809,13 +809,13 @@ def nice(self, value: int | None = None) -> int | None: @memoize_when_activated def uids(self) -> puids: """Return process UIDs as a (real, effective, saved) - namedtuple. + named tuple. """ return self._proc.uids() def gids(self) -> pgids: """Return process GIDs as a (real, effective, saved) - namedtuple. + named tuple. """ return self._proc.gids() @@ -837,7 +837,7 @@ def num_fds(self) -> int: def io_counters(self) -> pio: """Return process I/O statistics as a (read_count, write_count, read_bytes, write_bytes) - namedtuple. + named tuple. Those are the number of read/write calls performed and the amount of bytes read and written by the process. """ @@ -958,7 +958,7 @@ def num_threads(self) -> int: def threads(self) -> list[pthread]: """Return threads opened by process as a list of - (id, user_time, system_time) namedtuples representing + (id, user_time, system_time) named tuples representing thread id and thread CPU times (user/system). On OpenBSD this method requires root access. """ @@ -1134,7 +1134,7 @@ def timer(): @memoize_when_activated def cpu_times(self) -> pcputimes: """Return a (user, system, children_user, children_system) - namedtuple representing the accumulated process time, in + named tuple representing the accumulated process time, in seconds. This is similar to os.times() but per-process. On macOS and Windows children_user and children_system are @@ -1144,7 +1144,7 @@ def cpu_times(self) -> pcputimes: @memoize_when_activated def memory_info(self) -> pmem: - """Return a namedtuple with variable fields depending on the + """Return a named tuple with variable fields depending on the platform, representing memory information about the process. The "portable" fields available on all platforms are `rss` and `vms`. @@ -1155,7 +1155,7 @@ def memory_info(self) -> pmem: @memoize_when_activated def memory_info_ex(self) -> pmem_ex: - """Return a namedtuple extending memory_info() with extra + """Return a named tuple extending memory_info() with extra metrics. All numbers are expressed in bytes. @@ -1253,14 +1253,14 @@ def memory_percent(self, memtype: str = "rss") -> float: def memory_maps( self, grouped: bool = True ) -> list[pmmap_grouped] | list[pmmap_ext]: - """Return process' mapped memory regions as a list of namedtuples + """Return process' mapped memory regions as a list of named tuples whose fields are variable depending on the platform. If *grouped* is True the mapped regions with the same 'path' are grouped together and the different memory fields are summed. If *grouped* is False every mapped region is shown as a single - entity and the namedtuple will also include the mapped region's + entity and the named tuple will also include the mapped region's address space ('addr') and permission set ('perms'). """ it = self._proc.memory_maps() @@ -1279,7 +1279,7 @@ def memory_maps( def page_faults(self) -> ppagefaults: """Return the number of page faults for this process as a - (minor, major) namedtuple. + (minor, major) named tuple. - *minor* (a.k.a. *soft* faults): occur when a memory page is not currently mapped into the process address space, but is @@ -1297,14 +1297,14 @@ def page_faults(self) -> ppagefaults: def open_files(self) -> list[popenfile]: """Return files opened by process as a list of - (path, fd) namedtuples including the absolute file name + (path, fd) named tuples including the absolute file name and file descriptor number. """ return self._proc.open_files() def net_connections(self, kind: str = "inet") -> list[pconn]: """Return socket connections opened by process as a list of - (fd, family, type, laddr, raddr, status) namedtuples. + (fd, family, type, laddr, raddr, status) named tuples. The *kind* parameter filters for connections that match the following criteria: @@ -1777,9 +1777,9 @@ def cpu_count(logical: bool = True) -> int | None: def cpu_times(percpu: bool = False) -> scputimes | list[scputimes]: - """Return system-wide CPU times as a namedtuple. + """Return system-wide CPU times as a named tuple. Every CPU time represents the seconds the CPU has spent in the - given mode. The namedtuple's fields availability varies depending on the + given mode. The named tuple's fields availability varies depending on the platform: - user @@ -1793,7 +1793,7 @@ def cpu_times(percpu: bool = False) -> scputimes | list[scputimes]: - guest (Linux) - guest_nice (Linux) - When *percpu* is True return a list of namedtuples for each CPU. + When *percpu* is True return a list of named tuples for each CPU. First element of the list refers to first CPU, second element to second CPU and so on. The order of the list is consistent across calls. @@ -2025,7 +2025,7 @@ def cpu_stats() -> scpustats: if hasattr(_psplatform, "cpu_freq"): def cpu_freq(percpu: bool = False) -> scpufreq | list[scpufreq] | None: - """Return CPU frequency as a namedtuple including current, + """Return CPU frequency as a named tuple including current, min and max frequency expressed in Mhz. If *percpu* is True and the system supports per-cpu frequency @@ -2085,7 +2085,7 @@ def getloadavg() -> tuple[float, float, float]: def virtual_memory() -> svmem: - """Return statistics about system memory usage as a namedtuple + """Return statistics about system memory usage as a named tuple including the following fields, expressed in bytes: - total: @@ -2144,7 +2144,7 @@ def virtual_memory() -> svmem: def swap_memory() -> sswap: - """Return system swap memory statistics as a namedtuple including + """Return system swap memory statistics as a named tuple including the following fields: - total: total swap memory in bytes @@ -2166,7 +2166,7 @@ def swap_memory() -> sswap: def disk_usage(path: str) -> sdiskusage: """Return disk usage statistics about the given *path* as a - namedtuple including total, used and free space expressed in bytes + named tuple including total, used and free space expressed in bytes plus the percentage usage. """ return _psplatform.disk_usage(path) @@ -2174,7 +2174,7 @@ def disk_usage(path: str) -> sdiskusage: def disk_partitions(all: bool = False) -> list[sdiskpart]: """Return mounted partitions as a list of - (device, mountpoint, fstype, opts) namedtuple. + (device, mountpoint, fstype, opts) named tuple. 'opts' field is a raw string separated by commas indicating mount options which may vary depending on the platform. @@ -2187,7 +2187,7 @@ def disk_partitions(all: bool = False) -> list[sdiskpart]: def disk_io_counters( perdisk: bool = False, nowrap: bool = True ) -> sdiskio | dict[str, sdiskio]: - """Return system disk I/O statistics as a namedtuple including + """Return system disk I/O statistics as a named tuple including the following fields: - read_count: number of reads @@ -2205,7 +2205,7 @@ def disk_io_counters( If *perdisk* is True return the same information for every physical disk installed on the system as a dictionary - with partition names as the keys and the namedtuple + with partition names as the keys and the named tuple described above as the values. If *nowrap* is True it detects and adjust the numbers which overflow @@ -2246,7 +2246,7 @@ def disk_io_counters( def net_io_counters( pernic: bool = False, nowrap: bool = True ) -> snetio | dict[str, snetio] | None: - """Return network I/O statistics as a namedtuple including + """Return network I/O statistics as a named tuple including the following fields: - bytes_sent: number of bytes sent @@ -2261,7 +2261,7 @@ def net_io_counters( If *pernic* is True return the same information for every network interface installed on the system as a dictionary - with network interface names as the keys and the namedtuple + with network interface names as the keys and the named tuple described above as the values. If *nowrap* is True it detects and adjust the numbers which overflow @@ -2292,7 +2292,7 @@ def net_io_counters( def net_connections(kind: str = 'inet') -> list[sconn]: """Return system-wide socket connections as a list of - (fd, family, type, laddr, raddr, status, pid) namedtuples. + (fd, family, type, laddr, raddr, status, pid) named tuples. In case of limited privileges 'fd' and 'pid' may be set to -1 and None respectively. The *kind* parameter filters for connections that fit the @@ -2323,8 +2323,8 @@ def net_connections(kind: str = 'inet') -> list[sconn]: def net_if_addrs() -> dict[str, list[snicaddr]]: """Return the addresses associated to each NIC (network interface card) installed on the system as a dictionary whose keys are the - NIC names and value is a list of namedtuples for each address - assigned to the NIC. Each namedtuple includes 5 fields: + NIC names and value is a list of named tuples for each address + assigned to the NIC. Each named tuple includes 5 fields: - family: can be either socket.AF_INET, socket.AF_INET6 or psutil.AF_LINK, which refers to a MAC address. @@ -2384,7 +2384,7 @@ def net_if_addrs() -> dict[str, list[snicaddr]]: def net_if_stats() -> dict[str, snicstats]: """Return information about each NIC (network interface card) installed on the system as a dictionary whose keys are the - NIC names and value is a namedtuple with the following fields: + NIC names and value is a named tuple with the following fields: - isup: whether the interface is up (bool) - duplex: can be either NIC_DUPLEX_FULL, NIC_DUPLEX_HALF or @@ -2407,7 +2407,7 @@ def net_if_stats() -> dict[str, snicstats]: def sensors_temperatures( fahrenheit: bool = False, ) -> dict[str, list[shwtemp]]: - """Return hardware temperatures. Each entry is a namedtuple + """Return hardware temperatures. Each entry is a named tuple representing a certain hardware sensor (it may be a CPU, an hard disk or something else, depending on the OS and its configuration). @@ -2445,7 +2445,7 @@ def convert(n): if hasattr(_psplatform, "sensors_fans"): def sensors_fans() -> dict[str, list[sfan]]: - """Return fans speed. Each entry is a namedtuple + """Return fans speed. Each entry is a named tuple representing a certain hardware sensor. All speed are expressed in RPM (rounds per minute). """ @@ -2489,7 +2489,7 @@ def boot_time() -> float: def users() -> list[suser]: """Return users currently connected on the system as a list of - namedtuples including the following fields. + named tuples including the following fields. - user: the name of the user - terminal: the tty or pseudo-tty associated with the user, if any. diff --git a/psutil/_psaix.py b/psutil/_psaix.py index 5fcc367ae1..429d40686d 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -261,7 +261,7 @@ def boot_time(): def users(): - """Return currently connected users as a list of namedtuples.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.users() localhost = (':0.0', ':0') diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index b198e56432..89dfea222f 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -131,13 +131,13 @@ def swap_memory(): def cpu_times(): - """Return system per-CPU times as a namedtuple.""" + """Return system per-CPU times as a named tuple.""" user, nice, system, idle, irq = cext.cpu_times() return ntp.scputimes(user, system, idle, nice, irq) def per_cpu_times(): - """Return system CPU times as a namedtuple.""" + """Return system CPU times as a named tuple.""" ret = [] for cpu_t in cext.per_cpu_times(): user, nice, system, idle, irq = cpu_t @@ -257,7 +257,7 @@ def cpu_freq(): def disk_partitions(all=False): - """Return mounted disk partitions as a list of namedtuples. + """Return mounted disk partitions as a list of named tuples. 'all' argument is ignored, see: https://github.com/giampaolo/psutil/issues/906. """ @@ -399,7 +399,7 @@ def adjust_proc_create_time(ctime): def users(): - """Return currently connected users as a list of namedtuples.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.users() for item in rawlist: @@ -751,7 +751,7 @@ def cwd(self): @wrap_exceptions def open_files(self): - """Return files opened by process as a list of namedtuples.""" + """Return files opened by process as a list of named tuples.""" rawlist = cext.proc_open_files(self.pid) return [ntp.popenfile(path, fd) for path, fd in rawlist] diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 7937bea628..2cbc60b673 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -467,7 +467,7 @@ def lsget(lst, idx, field_name): def per_cpu_times(): - """Return a list of namedtuple representing the CPU times + """Return a list of named tuples representing the CPU times for every CPU available on the system. """ procfs_path = get_procfs_path() @@ -1160,7 +1160,7 @@ def find(self): def disk_partitions(all=False): - """Return mounted disk partitions as a list of namedtuples.""" + """Return mounted disk partitions as a list of named tuples.""" fstypes = set() procfs_path = get_procfs_path() if not all: @@ -1445,7 +1445,7 @@ def multi_bcat(*paths): def users(): - """Return currently connected users as a list of namedtuples.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.users() for item in rawlist: diff --git a/psutil/_psosx.py b/psutil/_psosx.py index 207f1c81a2..bdb914063f 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -67,7 +67,7 @@ def virtual_memory(): - """System virtual memory as a namedtuple.""" + """System virtual memory as a named tuple.""" d = cext.virtual_mem() d["percent"] = usage_percent( (d["total"] - d["available"]), d["total"], round_=1 @@ -93,7 +93,7 @@ def swap_memory(): def cpu_times(): - """Return system CPU times as a namedtuple.""" + """Return system CPU times as a named tuple.""" user, nice, system, idle = cext.cpu_times() return ntp.scputimes(user, system, idle, nice) @@ -147,7 +147,7 @@ def cpu_freq(): def disk_partitions(all=False): - """Return mounted disk partitions as a list of namedtuples.""" + """Return mounted disk partitions as a list of named tuples.""" retlist = [] partitions = cext.disk_partitions() for partition in partitions: @@ -266,7 +266,7 @@ def adjust_proc_create_time(ctime): def users(): - """Return currently connected users as a list of namedtuples.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.users() for item in rawlist: diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index 7ffd22efd7..8707981727 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -279,7 +279,7 @@ def boot_time(): def users(): - """Return currently connected users as a list of namedtuples.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.users() localhost = (':0.0', ':0') diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index a8652da17d..e28490ba26 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -123,7 +123,7 @@ def getpagesize(): def virtual_memory(): - """System virtual memory as a namedtuple.""" + """System virtual memory as a named tuple.""" info = cext.GetPerformanceInfo() page = info["PageSize"] total = info["PhysicalTotal"] * page @@ -373,7 +373,7 @@ def boot_time(): def users(): - """Return currently connected users as a list of namedtuples.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.users() for item in rawlist: diff --git a/tests/__init__.py b/tests/__init__.py index b0e63f204f..b62db32689 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1545,7 +1545,7 @@ def check_net_address(addr, family): def check_connection_ntuple(conn): - """Check validity of a connection namedtuple.""" + """Check validity of a connection named tuple.""" def check_ntuple(conn): has_pid = len(conn) == 7 @@ -1821,7 +1821,7 @@ def warn(msg): def is_namedtuple(x): - """Check if object is an instance of namedtuple.""" + """Check if object is an instance of named tuple.""" t = type(x) if tuple not in t.__mro__: return False diff --git a/tests/test_type_hints.py b/tests/test_type_hints.py index b199809b8b..788dd2dcc0 100755 --- a/tests/test_type_hints.py +++ b/tests/test_type_hints.py @@ -35,7 +35,7 @@ class TypeHintTestCase(PsutilTestCase): class TestTypeHintsNtuples(TypeHintTestCase): - """Check that namedtuple field values match the type annotations + """Check that named tuple field values match the type annotations defined in psutil/_ntuples.py. """ From 659163b4de0a395ce429bbd99c755fe7e98bbc5f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Mar 2026 10:50:54 +0100 Subject: [PATCH 1614/1714] Doc: various improvements + fix virtual_memory() definitions and table --- docs/adoption.rst | 4 +-- docs/api.rst | 77 ++++++++++++++++++++++++----------------------- docs/devguide.rst | 2 +- docs/faq.rst | 18 +++++++++++ docs/glossary.rst | 36 ++-------------------- docs/index.rst | 4 +-- docs/install.rst | 2 +- docs/platform.rst | 2 +- docs/recipes.rst | 2 +- docs/timeline.rst | 8 ++--- 10 files changed, 71 insertions(+), 84 deletions(-) diff --git a/docs/adoption.rst b/docs/adoption.rst index 6b5f100da3..da370bbd23 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -52,7 +52,7 @@ Infrastructure / automation - |ajenti-stars| - monitoring plugins, deep integration -AI / Machine learning +AI / machine learning --------------------- .. list-table:: @@ -104,7 +104,7 @@ Developer tools - |spyder-stars| - deep integration, UI stats, process management * - |psleak-logo| `psleak `__ - - Test framework to detect memory in Python C extensions + - Test framework to detect memory leaks in Python C extensions - |psleak-stars| - heap process memory (:func:`heap_info()`) diff --git a/docs/api.rst b/docs/api.rst index bb16e42d55..e9481fa8c3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -21,7 +21,7 @@ CPU .. function:: cpu_times(percpu=False) - Return system CPU times as a :term:`named tuple`. All fields are + Return system CPU times as a named tuple. All fields are :term:`cumulative counters ` (seconds) representing time the CPU has spent in each mode since boot. The attributes availability varies depending on the platform. @@ -311,17 +311,15 @@ Memory still holds valid data (:term:`page cache`, old allocations) but is a candidate for reclamation or swapping. On BSD systems it is counted in **available**. - - **buffers** *(Linux, BSD)*: kernel buffer cache for raw block-device I/O - (e.g. disk blocks read before the filesystem processes them). Can be - reclaimed by the OS when needed. - - **cached** *(Linux, BSD)*: in-memory :term:`page cache` for files read from disk. - The OS reclaims this memory automatically when processes need it, so a - large **cached** value is healthy and does not indicate memory pressure. - On Linux this includes ``SReclaimable`` (reclaimable slab memory). + - **buffers** *(Linux, BSD)*: memory used by the kernel to cache disk + metadata (e.g. filesystem structures). Reclaimable by the OS when needed. + - **cached** *(Linux, BSD)*: RAM used by the kernel to cache file + contents (data read from or written to disk). Reclaimable by the OS when + needed. See :term:`page cache`. - **shared** *(Linux, BSD)*: memory accessible by multiple processes - simultaneously, such as POSIX shared memory (``shm_open``) and - ``tmpfs``-backed files. On Linux this corresponds to ``Shmem`` in - ``/proc/meminfo`` and is already counted within **active** / **inactive**. + simultaneously, such as in-memory ``tmpfs`` and POSIX shared memory objects + (``shm_open``). On Linux this corresponds to ``Shmem`` in ``/proc/meminfo`` + and is already counted within **active** / **inactive**. - **slab** *(Linux)*: memory used by the kernel's internal object caches (e.g. inode and dentry caches). The reclaimable portion (``SReclaimable``) is already included in **cached**. @@ -329,10 +327,12 @@ Memory code and critical data structures). It can never be moved to disk. Below is a table showing implementation details. All info on Linux is - retrieved from `/proc/meminfo`. + retrieved from `/proc/meminfo`. On macOS via ``host_statistics64()``. On + Windows via `GetPerformanceInfo`_. .. list-table:: :header-rows: 1 + :widths: 9 15 14 14 26 * - Field - Linux @@ -342,31 +342,31 @@ Memory * - total - ``MemTotal`` - ``sysctl() hw.memsize`` - - ``GetPerformanceInfo() PhysicalTotal`` + - ``PhysicalTotal`` - ``sysctl() hw.physmem`` * - available - ``MemAvailable`` - - ``host_statistics64() inactive + free`` - - ``GetPerformanceInfo() PhysicalAvailable`` + - ``inactive + free`` + - ``PhysicalAvailable`` - ``inactive + cached + free`` * - used - ``total - available`` - - ``host_statistics64() active + wired`` + - ``active + wired`` - ``total - available`` - ``active + wired + cached`` * - free - ``MemFree`` - - ``host_statistics64() free - speculative`` + - ``free - speculative`` - same as ``available`` - ``sysctl() vm.stats.vm.v_free_count`` * - active - ``Active`` - - ``host_statistics64() active`` + - ``active`` - - ``sysctl() vm.stats.vm.v_active_count`` * - inactive - ``Inactive`` - - ``host_statistics64() inactive`` + - ``inactive`` - - ``sysctl() vm.stats.vm.v_inactive_count`` * - buffers @@ -391,7 +391,7 @@ Memory - * - wired - - - ``host_statistics64() wired`` + - ``wired`` - - ``sysctl() vm.stats.vm.v_wire_count`` @@ -415,7 +415,7 @@ Memory fields. .. note:: - - On Linux, **total**, **free**, **used**, **shared**, and **available** + - On Linux, **total**, **free**, **used**, **shared**, and **available** match the output of the ``free`` command. - On macOS, **free**, **active**, **inactive**, and **wired** match ``vm_stat`` output. @@ -1328,7 +1328,7 @@ Process class .. note:: on macOS Big Sur this function returns something meaningful only for the current process or in - `other specific circumstances `_). + `other specific circumstances `_. .. versionadded:: 4.0.0 @@ -1401,7 +1401,7 @@ Process class .. method:: parents() - Utility method which return the parents of this process as a list of + Utility method which returns the parents of this process as a list of :class:`Process` instances. If no parents are known return an empty list. See also :meth:`ppid` and :meth:`parent` methods. @@ -1550,7 +1550,7 @@ Process class This is the same as :func:`resource.getrlimit` and :func:`resource.setrlimit` but can be used for any process PID, not only :func:`os.getpid`. For get, return value is a ``(soft, hard)`` tuple. Each value may be either - and integer or :data:`psutil.RLIMIT_* `. + an integer or :data:`psutil.RLIMIT_* `. Also see `scripts/procinfo.py`_ script. .. code-block:: pycon @@ -1612,7 +1612,7 @@ Process class .. availability:: Linux, BSD, Windows, AIX .. versionchanged:: 5.2.0 - added *read_chars* + *write_chars* on Linux and *other_count* + + added *read_chars* + *write_chars* on Linux and *other_count* + *other_bytes* on Windows. .. method:: num_ctx_switches() @@ -1660,7 +1660,7 @@ Process class .. method:: cpu_times() - Return a :term:`named tuple` of :term:`cumulative counters ` (seconds) + Return a named tuple of :term:`cumulative counters ` (seconds) representing the accumulated process CPU times (see `explanation `_). This is similar to :func:`os.times` but can be used for any process PID. @@ -2096,7 +2096,7 @@ Process class Return the children of this process as a list of :class:`Process` instances. - If recursive is `True` return all the parent descendants. + If recursive is ``True`` return all the parent descendants. Pseudo code example assuming *A == this process*: :: @@ -2125,7 +2125,7 @@ Process class .. method:: page_faults() Return the number of :term:`page faults ` for this process as a - ``(minor, major)`` :term:`named tuple`. + ``(minor, major)`` named tuple. - **minor** (a.k.a. *soft* faults): occur when a memory page is not currently mapped into the process address space, but is already present @@ -2184,7 +2184,7 @@ Process class That implies that this method on Windows is not guaranteed to enumerate all regular file handles (see `issue 597 `_). - Tools like ProcessHacker has the same limitation. + Tools like ProcessHacker have the same limitation. .. warning:: on BSD this method can return files with a null path ("") due to a @@ -2210,7 +2210,7 @@ Process class - **family**: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6` or :data:`socket.AF_UNIX`. - **type**: the address type, either :data:`socket.SOCK_STREAM`, :data:`socket.SOCK_DGRAM` or - :data:`socket.SOCK_SEQPACKET`. . + :data:`socket.SOCK_SEQPACKET`. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` in case of AF_UNIX sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an @@ -2306,7 +2306,7 @@ Process class Return whether the current process is running in the current process list. Differently from ``psutil.pid_exists(p.pid)``, this is reliable also in - case the process is gone and its PID reused by another process (:term:`PID reuse`). + case the process is gone and its PID reused by another process (:ref:`PID reuse `). If PID has been reused, this method will also remove the process from :func:`process_iter` internal cache. @@ -2321,7 +2321,7 @@ Process class .. method:: send_signal(signal) - Send a :term:`signal` to process (see :mod:`signal` module constants) + Send a signal to process (see :mod:`signal` module constants) preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals @@ -2334,21 +2334,21 @@ Process class .. method:: suspend() - Suspend process execution with *SIGSTOP* :term:`signal` preemptively + Suspend process execution with *SIGSTOP* signal preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGSTOP)``. On Windows this is done by suspending all process threads execution. .. method:: resume() - Resume process execution with *SIGCONT* :term:`signal` preemptively + Resume process execution with *SIGCONT* signal preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGCONT)``. On Windows this is done by resuming all process threads execution. .. method:: terminate() - Terminate the process with *SIGTERM* :term:`signal` preemptively checking + Terminate the process with *SIGTERM* signal preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGTERM)``. On Windows this is an alias for :meth:`kill`. @@ -2356,7 +2356,7 @@ Process class .. method:: kill() - Kill the current process by using *SIGKILL* :term:`signal` preemptively + Kill the current process by using *SIGKILL* signal preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. On Windows this is done by using `TerminateProcess`_. @@ -2438,7 +2438,7 @@ Popen class :meth:`terminate() `, :meth:`kill() `. This is done in order to avoid killing another process in case its PID has - been reused, fixing `BPO-6973`_. + been reused, fixing `BPO-6973`_. .. code-block:: pycon @@ -2951,7 +2951,7 @@ Hardware constants Constants which identifies whether a :term:`NIC` (network interface card) has full or half mode speed. NIC_DUPLEX_FULL means the NIC is able to send and - receive data (files) simultaneously, NIC_DUPLEX_FULL means the NIC can either + receive data (files) simultaneously, NIC_DUPLEX_HALF means the NIC can either send or receive data at a time. To be used in conjunction with :func:`psutil.net_if_stats`. @@ -3037,6 +3037,7 @@ Other constants .. === Windows API .. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess +.. _`GetPerformanceInfo`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getperformanceinfo .. _`GetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass .. _`PROCESS_MEMORY_COUNTERS_EX`: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex .. _`SetPriorityClass`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass diff --git a/docs/devguide.rst b/docs/devguide.rst index 74186171e8..4f2a858fea 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -82,7 +82,7 @@ On Windows: :: - set PSUTIL_DEBUG=1 python.exe script.py + set PSUTIL_DEBUG=1 && python.exe script.py psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) diff --git a/docs/faq.rst b/docs/faq.rst index 74db2d64e2..26725ee672 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -311,3 +311,21 @@ memory private to the process. It represents the amount of memory that would be freed if the process were terminated right now. It is more accurate than RSS, but substantially slower and requires higher privileges. On Linux it also returns PSS (Proportional Set Size) and swap. + +.. _faq_used_plus_free: + +Why does virtual_memory().used + free != total? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Because some memory (like cache and buffers) is reclaimable and accounted +separately: + +.. code-block:: pycon + + >>> import psutil + >>> m = psutil.virtual_memory() + >>> m.used + m.free == m.total + False + +The ``available`` field already includes this reclaimable memory and is the +best indicator of memory pressure. See :ref:`faq_virtual_memory_available`. diff --git a/docs/glossary.rst b/docs/glossary.rst index cbfc822643..39a147e13f 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -123,7 +123,7 @@ Glossary whatever the CPU was doing. Reported as the ``interrupts`` field of :func:`cpu_stats` and ``irq`` field of :func:`cpu_times`. A very high rate may indicate a misbehaving device driver or a heavily - loaded NIC. + loaded NIC. Also see :term:`soft interrupt`. involuntary context switch @@ -160,12 +160,6 @@ Glossary 1, 5, and 15 minutes. A load average equal to the number of logical CPUs means the system is fully saturated. See :func:`getloadavg`. - named tuple - - A :func:`collections.namedtuple` subclass, a tuple whose fields - can be accessed by name as well as by index. Most psutil functions - return named tuples (e.g. ``sswap``, ``pmem``, ``scputimes``). - nice A process priority value that influences how much CPU time the OS @@ -210,21 +204,6 @@ Glossary logical CPUs when hyper-threading is enabled. The physical count is returned by ``cpu_count(logical=False)``. - PID - - *Process identifier*, a non-negative integer assigned by the OS to - every running process. PIDs are unique at any instant but are - recycled over time (:term:`PID reuse`). PID 0 is typically the - idle/swapper process; PID 1 is ``init`` / ``systemd`` on UNIX. - - PID reuse - - When a process exits its PID is eventually recycled and assigned to - a new process. A :class:`Process` object held across this boundary - would silently refer to the wrong process. psutil detects reuse by - comparing the recorded creation time against the live value, and - raises :exc:`NoSuchProcess` if they differ. - PSS *Proportional Set Size*, the amount of RAM used by a process, @@ -309,24 +288,13 @@ Glossary :data:`RLIMIT_AS` (virtual address space), and :data:`RLIMIT_CPU` (CPU time in seconds). See :meth:`Process.rlimit`. - signal - - An asynchronous notification delivered by the OS to a process to - indicate an event. The process can catch it (run a handler), ignore it, - or let the default action happen (often termination). Common signals on - UNIX: ``SIGTERM`` (request graceful shutdown), ``SIGKILL`` (force - immediate termination, cannot be caught), ``SIGSTOP`` / ``SIGCONT`` - (pause and resume). psutil exposes signals via :meth:`Process.send_signal`, - :meth:`Process.terminate`, :meth:`Process.suspend`, - :meth:`Process.resume` and :meth:`Process.kill`. See also :mod:`signal` module. - USS *Unique Set Size*, the amount of RAM that belongs exclusively to a process and would be freed if it exited. It excludes shared pages entirely, making it the most accurate single-process memory metric. Available on Linux, macOS, and Windows via - :meth:`Process.memory_full_info`. + :meth:`Process.memory_footprint`. voluntary context switch diff --git a/docs/index.rst b/docs/index.rst index c200c662cb..f324fc7233 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,7 +2,7 @@ :synopsis: psutil module .. moduleauthor:: Giampaolo Rodola -Psutil documentation +psutil documentation ==================== .. image:: https://img.shields.io/badge/GitHub-repo-blue @@ -90,7 +90,7 @@ Security To report a security vulnerability, please use the `Tidelift security contact`_. Tidelift will coordinate the fix and disclosure. -Table of Contents +Table of contents ----------------- .. toctree:: diff --git a/docs/install.rst b/docs/install.rst index 90ebe6063e..3ddbbd4052 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -108,7 +108,7 @@ OpenBSD NetBSD ^^^^^^ -Assuming Python 3.11 (the most recent at the time of writing): +Assuming Python 3.11: :: diff --git a/docs/platform.rst b/docs/platform.rst index c280f79847..6c2abb3725 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -61,7 +61,7 @@ Support history * psutil 8.0.0 (2026-XX): drop Python 3.6 * psutil 7.2.0 (2025-12): publish wheels for **Linux musl** * psutil 7.1.2 (2025-10): publish wheels for **free-threaded Python** -* psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (Linux and Windows) +* psutil 7.1.2 (2025-10): no longer publish wheels for 32-bit Python (Linux and Windows) * psutil 7.1.1 (2025-10): drop **SunOS 10** * psutil 7.1.0 (2025-09): drop **FreeBSD 8** * psutil 7.0.0 (2025-02): drop Python 2.7 diff --git a/docs/recipes.rst b/docs/recipes.rst index 7a6b86d2c7..5dd8021582 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -103,7 +103,7 @@ Find all processes that have a given file open (useful on Windows): def find_procs_using_file(path): ls = [] for p in psutil.process_iter(["open_files"]): - for f in p.info["open_files"]: + for f in p.info["open_files"] or []: if f.path == path: ls.append(p) break diff --git a/docs/timeline.rst b/docs/timeline.rst index 7d0c0410b8..680398ae04 100644 --- a/docs/timeline.rst +++ b/docs/timeline.rst @@ -32,7 +32,7 @@ Timeline - 2025-09-17: `7.1.0 `_ - `what's new `_ - - `diff `_ + `diff `_ - 2025-02-13: `7.0.0 `_ - `what's new `_ - @@ -225,15 +225,15 @@ Timeline `5.0.0 `_ - `what's new `_ - `diff `_ -- 2016-10-05: +- 2016-10-25: `4.4.2 `_ - `what's new `_ - `diff `_ -- 2016-10-25: +- 2016-10-23: `4.4.1 `_ - `what's new `_ - `diff `_ -- 2016-10-23: +- 2016-10-05: `4.4.0 `_ - `what's new `_ - `diff `_ From 5ece6f5e7aa2f1e88cd315ed8963516738565b50 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Mar 2026 12:02:10 +0100 Subject: [PATCH 1615/1714] [Windows] expose cached and wired memory metrics #2776 (#2777) --- docs/api.rst | 13 ++++++++----- docs/changelog.rst | 2 ++ docs/migration.rst | 12 ++++++++++++ psutil/_ntuples.py | 3 +++ psutil/_pswindows.py | 16 +++++++++------- psutil/arch/windows/init.h | 1 - tests/test_system.py | 7 ++++++- tests/test_windows.py | 33 ++++++++++++++++++++++----------- 8 files changed, 62 insertions(+), 25 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e9481fa8c3..e9b1aef15a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -313,7 +313,7 @@ Memory **available**. - **buffers** *(Linux, BSD)*: memory used by the kernel to cache disk metadata (e.g. filesystem structures). Reclaimable by the OS when needed. - - **cached** *(Linux, BSD)*: RAM used by the kernel to cache file + - **cached** *(Linux, BSD, Windows)*: RAM used by the kernel to cache file contents (data read from or written to disk). Reclaimable by the OS when needed. See :term:`page cache`. - **shared** *(Linux, BSD)*: memory accessible by multiple processes @@ -323,8 +323,8 @@ Memory - **slab** *(Linux)*: memory used by the kernel's internal object caches (e.g. inode and dentry caches). The reclaimable portion (``SReclaimable``) is already included in **cached**. - - **wired** *(macOS, BSD)*: memory pinned in RAM by the kernel (e.g. kernel - code and critical data structures). It can never be moved to disk. + - **wired** *(macOS, BSD, Windows)*: memory pinned in RAM by the kernel (e.g. + kernel code and critical data structures). It can never be moved to disk. Below is a table showing implementation details. All info on Linux is retrieved from `/proc/meminfo`. On macOS via ``host_statistics64()``. On @@ -377,7 +377,7 @@ Memory * - cached - ``Cached + SReclaimable`` - - - + - ``SystemCache`` - ``sysctl() vm.stats.vm.v_cache_count`` * - shared - ``Shmem`` @@ -392,7 +392,7 @@ Memory * - wired - - ``wired`` - - + - ``KernelNonpaged`` - ``sysctl() vm.stats.vm.v_wire_count`` Example on Linux: @@ -431,6 +431,9 @@ Memory .. versionchanged:: 5.4.4 added *slab* metric on Linux. + .. versionchanged:: 8.0.0 + added *cached* and *wired* metric on Windows. + .. function:: swap_memory() Return system swap memory statistics as a named tuple including the following diff --git a/docs/changelog.rst b/docs/changelog.rst index bc371c6bb6..d3d5a6065b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -132,6 +132,8 @@ Others ``irq`` to match the field name used on Linux and BSD. ``interrupt`` still works but raises :exc:`DeprecationWarning`. See :ref:`migration guide `. +- :gh:`2776`: Windows: :func:`virtual_memory` now includes ``cached`` and + ``wired`` fields. **Bug fixes** diff --git a/docs/migration.rst b/docs/migration.rst index d9eb252e53..5ea7200c1d 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -75,6 +75,18 @@ Named tuple field order changed - BSD: a new ``peak_rss`` field was added. +- :func:`virtual_memory`: on Windows, new ``cached`` and ``wired`` fields were + added. Code using positional unpacking will break: + + .. code-block:: python + + # before + total, avail, percent, used, free = psutil.virtual_memory() + + # after + m = psutil.virtual_memory() + total, avail, percent, used, free = m.total, m.available, m.percent, m.used, m.free + cpu_times() interrupt renamed to irq on Windows ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index b2c2b33065..3e284f91df 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -227,6 +227,9 @@ class svmem(NamedTuple): cached: int shared: int wired: int + elif WINDOWS: + cached: int + wired: int elif MACOS: active: int inactive: int diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index e28490ba26..5cc244e92a 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -125,25 +125,27 @@ def getpagesize(): def virtual_memory(): """System virtual memory as a named tuple.""" info = cext.GetPerformanceInfo() - page = info["PageSize"] - total = info["PhysicalTotal"] * page - avail = info["PhysicalAvailable"] * page + pagesize = info["PageSize"] + total = info["PhysicalTotal"] * pagesize + avail = info["PhysicalAvailable"] * pagesize + cached = info["SystemCache"] * pagesize + wired = info["KernelNonpaged"] * pagesize free = avail used = total - avail percent = usage_percent((total - avail), total, round_=1) - return ntp.svmem(total, avail, percent, used, free) + return ntp.svmem(total, avail, percent, used, free, cached, wired) def swap_memory(): """Swap system memory as a (total, used, free, sin, sout) tuple.""" info = cext.GetPerformanceInfo() - page = info["PageSize"] - total_phys = info["PhysicalTotal"] * page + pagesize = info["PageSize"] + total_phys = info["PhysicalTotal"] * pagesize # CommitLimit == Maximum pages that can be committed into RAM + # page file (swap). In the context of swap, it's the total "system # memory" (physical + virtual), thus substract the physical part # from it to get the "total swap". - total_system = info["CommitLimit"] * page # physical + swap + total_system = info["CommitLimit"] * pagesize # physical + swap total = total_system - total_phys # commit total is incremented immediately (decrementing free_system) diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h index 8844dde292..b64299c8e1 100644 --- a/psutil/arch/windows/init.h +++ b/psutil/arch/windows/init.h @@ -131,7 +131,6 @@ PyObject *psutil_swap_percent(PyObject *self, PyObject *args); PyObject *psutil_uptime(PyObject *self, PyObject *args); PyObject *psutil_users(PyObject *self, PyObject *args); PyObject *psutil_GetPerformanceInfo(PyObject *self, PyObject *args); -PyObject *psutil_virtual_mem(PyObject *self, PyObject *args); PyObject *psutil_winservice_enumerate(PyObject *self, PyObject *args); PyObject *psutil_winservice_query_config(PyObject *self, PyObject *args); PyObject *psutil_winservice_query_descr(PyObject *self, PyObject *args); diff --git a/tests/test_system.py b/tests/test_system.py index 492165b896..470de16dce 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -372,7 +372,12 @@ def test_virtual_memory_fields_order(self): "shared", "wired", ) - elif WINDOWS or SUNOS or AIX: + elif WINDOWS: + assert mem._fields[5:] == ( + "cached", + "wired", + ) + elif SUNOS or AIX: assert mem._fields[5:] == () def test_swap_memory(self): diff --git a/tests/test_windows.py b/tests/test_windows.py index e9da443fae..474e8fa921 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -165,6 +165,28 @@ def test_cpu_freq(self): assert proc.MaxClockSpeed == psutil.cpu_freq().max +class TestVirtualMemory(WindowsTestCase): + + def test_total(self): + w = wmi.WMI().Win32_ComputerSystem()[0] + assert int(w.TotalPhysicalMemory) == psutil.virtual_memory().total + + def test_free(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + abs(int(w.AvailableBytes) - psutil.virtual_memory().free) + < TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_wired(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + abs(int(w.PoolNonpagedBytes) - psutil.virtual_memory().wired) + < TOLERANCE_SYS_MEM + ) + + class TestSystemAPIs(WindowsTestCase): def test_nic_names(self): out = sh('ipconfig /all') @@ -203,17 +225,6 @@ def test_net_connections(self): win_ports = {int(p) for p in out.strip().split(',') if p.strip()} assert ps_ports == win_ports - def test_total_phymem(self): - w = wmi.WMI().Win32_ComputerSystem()[0] - assert int(w.TotalPhysicalMemory) == psutil.virtual_memory().total - - def test_free_phymem(self): - w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] - assert ( - abs(int(w.AvailableBytes) - psutil.virtual_memory().free) - < TOLERANCE_SYS_MEM - ) - def test_total_swapmem(self): w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] assert ( From deb0df5b6c6e2e7a9393b55bb877e0271564e842 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 20 Mar 2026 12:41:26 +0100 Subject: [PATCH 1616/1714] Organize custom.css by sections --- docs/_static/css/custom.css | 226 ++++++++++++++++++++---------------- docs/api.rst | 15 +-- 2 files changed, 133 insertions(+), 108 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 2b08020163..67b865b008 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -1,3 +1,7 @@ +/* ================================================================== */ +/* Layout */ +/* ================================================================== */ + @media (min-width: 1200px) { .wy-nav-content { max-width: 1000px; @@ -5,159 +9,167 @@ } } -/* hide top navigation */ +/* ================================================================== */ +/* Navigation */ +/* ================================================================== */ + +/* hide top navigation, keep footer navigation */ .rst-content > div[role="navigation"] { display: none; } -/* leave footer navigation visible */ footer div[role="navigation"][aria-label="Footer"] { display: block; } -/* HTML tables */ -.wy-table-responsive table th, -.wy-table-responsive table td { - padding: 10px !important; - white-space: normal !important; - word-wrap: break-word; +/* ================================================================== */ +/* Sidebar */ +/* ================================================================== */ + +.wy-side-nav-search { + background-color: grey !important } -.wy-table-responsive table { - table-layout: fixed; - width: 100%; +/* ================================================================== */ +/* Headings */ +/* ================================================================== */ + +h1, h2, h3 { + background: #eee; + padding: 5px; + border-bottom: 1px solid #ccc; +} + +h1 { + font-size: 35px; } -/* HTML tables */ +/* ================================================================== */ +/* Tables */ +/* ================================================================== */ + .wy-table-responsive table thead { background-color: #eeeeee; } -.rst-content dl:not(.docutils) { - margin: 0px 0px 0px 0px !important; +.document th { + padding: 4px 8px !important; + font-weight: 600; } -.data dd { +.document th p { margin-bottom: 0px !important; } -.data .descname { - border-right:10px !important; +.document td { + padding: 4px 8px !important; } -.rst-content ul p { +.document td p { margin-bottom: 0px !important; } -.rst-content ul { - margin-top: 0px !important; -} - -.rst-content li { - list-style: outside; - margin-left: 15px; +/* "longtable" class (used by alternatives.rst): fixed layout + with extra padding for multi-line cells */ +.wy-table-responsive table.longtable { + table-layout: fixed; + width: 100%; } -.document td { - padding-bottom: 0px !important; +.wy-table-responsive table.longtable th, +.wy-table-responsive table.longtable td { + padding: 10px !important; + white-space: normal !important; + word-wrap: break-word; } -.document th { - padding-top: 0px !important; - padding-bottom: 0px !important; +/* adoption page logos */ +.document td img[alt$="-logo"] { + height: 20px !important; + width: 20px !important; + vertical-align: middle; } -.document th p { - margin-bottom: 0px !important; -} +/* ================================================================== */ +/* Lists */ +/* ================================================================== */ -.document th p { +.rst-content ul { + margin-top: 0px !important; } -.function .descclassname { - font-weight: normal !important; +.rst-content ul p { + margin-bottom: 0px !important; } -.class .descclassname { - font-weight: normal !important; +.rst-content li { + list-style: outside; + margin-left: 15px; } -.admonition.warning { - padding-top: 2px !important; - padding-bottom: 2px !important; -} +/* ================================================================== */ +/* API signatures */ +/* ================================================================== */ -.admonition.note { - padding-top: 2px !important; - padding-bottom: 2px !important; +.rst-content dl:not(.docutils) { + margin: 0px 0px 0px 0px !important; } .rst-content dl:not(.docutils) dt { color: #555; } -.sig-paren { - padding-left: 2px; - padding-right: 2px; -} - -h1, h2, h3 { - background: #eee; - padding: 5px; - border-bottom: 1px solid #ccc; +.data dd { + margin-bottom: 0px !important; } -h1 { - font-size: 35px; +.data .descname { + border-right:10px !important; } -.admonition.warning { - padding-top: 5px !important; - padding-bottom: 5px !important; +.function .descclassname, +.class .descclassname { + font-weight: normal !important; } -.admonition.warning p { - margin-bottom: 5px !important; +.sig-paren { + padding-left: 2px; + padding-right: 2px; } -.admonition.note { - padding-top: 5px !important; - padding-bottom: 5px !important; -} +/* ================================================================== */ +/* Code blocks */ +/* ================================================================== */ -.admonition.note p { - margin-bottom: 5px !important; - backround-color: rgb(238, 255, 204) !important; +pre { + padding: 5px !important; } -.codeblock div[class^='highlight'], pre.literal-block div[class^='highlight'], .rst-content .literal-block div[class^='highlight'], div[class^='highlight'] div[class^='highlight'] { - background-color: #eeffcc !important; +.highlight { + background: #eeffcc; } .highlight .hll { background-color: #ffffcc } -.highlight { - background: #eeffcc; -} - .highlight .c { color: #408090; font-style: italic } -.wy-side-nav-search { - background-color: grey !important -} - -pre { - padding: 5px !important; +.codeblock div[class^='highlight'], +pre.literal-block div[class^='highlight'], +.rst-content .literal-block div[class^='highlight'], +div[class^='highlight'] div[class^='highlight'] { + background-color: #eeffcc !important; } -/* Show external links with a slightly different style (dotted underline). - Excludes Python API cross-references (intersphinx -> docs.python.org). */ +/* ================================================================== */ +/* Links */ +/* ================================================================== */ +/* external links: dotted underline (except intersphinx) */ a.reference.external:not([href*="docs.python.org"]) { text-decoration: underline dotted; } @@ -167,12 +179,12 @@ a.reference.external:not([href*="docs.python.org"]):hover { } a.external[href^="#"] { - text-decoration: none; /* keep same-page anchors normal */ + text-decoration: none; color: inherit; } /* ================================================================== */ -/* Warnings and info boxes like python doc */ +/* Admonitions (note, warning, tip) - styled like python doc */ /* ================================================================== */ div.admonition { @@ -192,6 +204,12 @@ div.note { border-radius: 3px !important; } +div.tip { + background-color: #dfd !important; + border: 1px solid green !important; + border-radius: 3px !important; +} + div.admonition p.admonition-title + p { display: inline !important; } @@ -206,26 +224,38 @@ p.admonition-title:after { content: ":" !important; } -div.body div.admonition, div.body div.impl-detail { +/* hide admonition icons */ +.fa-exclamation-circle:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before, +.rst-content .admonition-title:before { + display: none !important; } -.fa-exclamation-circle:before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, .wy-inline-validate.wy-inline-validate-info .wy-input-context:before, .rst-content .admonition-title:before { - display: none !important; +.admonition.warning { + padding-top: 5px !important; + padding-bottom: 5px !important; } -.note code { - background: #d6d6d6 !important; +.admonition.warning p { + margin-bottom: 5px !important; } -/* Project logos in adoption page tables */ -.document td img[alt$="-logo"] { - height: 20px !important; - width: 20px !important; - vertical-align: middle; +.admonition.note { + padding-top: 5px !important; + padding-bottom: 5px !important; +} + +.admonition.note p { + margin-bottom: 5px !important; +} + +.note code { + background: #d6d6d6 !important; } /* ================================================================== */ -/* versionadded / versionchanged / deprecated - like python doc */ +/* Version directives (versionadded / versionchanged / deprecated) */ /* ================================================================== */ div.versionadded, @@ -258,9 +288,3 @@ div.versionchanged .versionmodified { div.deprecated .versionmodified { color: rgb(159, 49, 51); } - -div.tip { - background-color: #dfd !important; - border: 1px solid green !important; - border-radius: 3px !important; -} diff --git a/docs/api.rst b/docs/api.rst index e9b1aef15a..adf47a94e1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -327,7 +327,7 @@ Memory kernel code and critical data structures). It can never be moved to disk. Below is a table showing implementation details. All info on Linux is - retrieved from `/proc/meminfo`. On macOS via ``host_statistics64()``. On + retrieved from `/proc/meminfo`_. On macOS via ``host_statistics64()``. On Windows via `GetPerformanceInfo`_. .. list-table:: @@ -422,8 +422,7 @@ Memory - On Windows, **total**, **used** ("In use"), and **available** match the Task Manager (Performance > Memory tab). - .. note:: see `scripts/meminfo.py`_ script providing an example on how to convert - bytes in a human readable form. + .. note:: see also `scripts/meminfo.py`_. .. versionchanged:: 4.2.0 added *shared* metric on Linux. @@ -680,7 +679,7 @@ Network .. table:: +----------------+-----------------------------------------------------+ - | **Kind value** | **Connections using** | + | Kind value | Connections using | +================+=====================================================+ | ``"inet"`` | IPv4 and IPv6 | +----------------+-----------------------------------------------------+ @@ -1547,8 +1546,9 @@ Process class .. method:: rlimit(resource, limits=None) - Get or set process :term:`resource limits ` (see `man prlimit`_). *resource* is one - of the `psutil.RLIMIT_* <#process-resources-constants>`_ constants. + Get or set process :term:`resource limits ` (see `man prlimit`_). + *resource* is one of the :data:`psutil.RLIMIT_* ` + constants. *limits* is a ``(soft, hard)`` tuple. This is the same as :func:`resource.getrlimit` and :func:`resource.setrlimit` but can be used for any process PID, not only :func:`os.getpid`. @@ -2229,7 +2229,7 @@ Process class following criteria: +----------------+-----------------------------------------------------+ - | **Kind value** | **Connections using** | + | Kind value | Connections using | +================+=====================================================+ | ``"inet"`` | IPv4 and IPv6 | +----------------+-----------------------------------------------------+ @@ -3019,6 +3019,7 @@ Other constants .. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html .. _`man prlimit`: https://linux.die.net/man/2/prlimit .. _`psleak`: https://github.com/giampaolo/psleak +.. _`/proc/meminfo`: https://man7.org/linux/man-pages/man5/proc_meminfo.5.html .. === scripts From c662db7e590bf11b6c052f7e9b80d09a5db12079 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Mar 2026 02:38:08 +0100 Subject: [PATCH 1617/1714] UNIX, net_if_addrs(): don't skip NICs with no address (#2779) E.g. they can be seen on macOS: ``` $ ifconfig lo0: flags=8049 mtu 16384 options=1203 inet 127.0.0.1 netmask 0xff000000 inet6 ::1 prefixlen 128 inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 nd6 options=201 gif0: flags=8010 mtu 1280 stf0: flags=0<> mtu 1280 en0: flags=8863 mtu 1500 options=2b ether 08:00:27:2b:c2:ed inet6 fe80::1879:e2e:f12d:4171%en0 prefixlen 64 secured scopeid 0x4 inet 10.0.2.15 netmask 0xffffff00 broadcast 10.0.2.255 nd6 options=201 media: autoselect (1000baseT ) status: active ``` In here, `gif0` and `sft0` are generic IPv4/IPv6 tunnel interfaces with no address associated. Because of this `net_if_addrs()` skips them. It should not, because it creates an inconsistency with `net_io_counters(pernic=True)` and `net_if_stats()` functions which **do** return these interface names. The test demonstrates the problem (because it fails): ```python @pytest.mark.skipif(not POSIX, reason="POSIX only") def test_nic_names(self): stdlib_names = {name for _, name in socket.if_nameindex()} assert stdlib_names == set(psutil.net_io_counters(pernic=True).keys()) assert stdlib_names == set(psutil.net_if_addrs().keys()) assert stdlib_names == set(psutil.net_if_stats().keys()) ``` Proposal: do not skip these NICs. Instead, set family to `AF_UNSPEC` and the remaining address fields to `None` --- docs/changelog.rst | 6 ++++++ psutil/_ntuples.py | 2 +- psutil/arch/posix/net.c | 34 ++++++++++++++++++++++++++++------ tests/test_contracts.py | 5 ++++- tests/test_system.py | 18 ++++++++++++++++-- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d3d5a6065b..6b269dabb9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -151,6 +151,12 @@ Others - :gh:`2746`, [FreeBSD]: :meth:`Process.memory_maps`, `rss` and `private` fields, are erroneously reported in memory pages instead of bytes. Other platforms (Linux, macOS, Windows) return bytes. +- :gh:`2778`, [UNIX]: :func:`net_if_addrs` skips interfaces with no addresses, + which are typically virtual IPv4/IPv6 tunnel interfaces. Now they are + included in the returned dict with family == ``AF_UNSPEC`` and an empty list + of addresses. Main reason: it creates an inconsistency with + :func:`net_io_counters` and :func:`net_if_stats` which do return these + interface names. 7.2.3 — 2026-02-08 ^^^^^^^^^^^^^^^^^^ diff --git a/psutil/_ntuples.py b/psutil/_ntuples.py index 3e284f91df..c95d85b11b 100644 --- a/psutil/_ntuples.py +++ b/psutil/_ntuples.py @@ -116,7 +116,7 @@ class sconn(NamedTuple): # psutil.net_if_addrs() class snicaddr(NamedTuple): family: socket.AddressFamily - address: str + address: str | None netmask: str | None broadcast: str | None ptp: str | None diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index aa57d1de84..b117f5578a 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -142,16 +142,19 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { } for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { - if (!ifa->ifa_addr) - continue; + if (!ifa->ifa_addr) // virtual NIC + goto append_none; + family = ifa->ifa_addr->sa_family; + py_address = psutil_convert_ipaddr(ifa->ifa_addr, family); - // If the primary address can't be determined just skip it. - // I've never seen this happen on Linux but I did on FreeBSD. - if (py_address == Py_None) - continue; + if (py_address == Py_None) { // virtual NIC + Py_CLEAR(py_address); + goto append_none; + } if (py_address == NULL) goto error; + py_netmask = psutil_convert_ipaddr(ifa->ifa_netmask, family); if (py_netmask == NULL) goto error; @@ -192,6 +195,25 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { Py_CLEAR(py_netmask); Py_CLEAR(py_broadcast); Py_CLEAR(py_ptp); + continue; + + append_none: + // When the primary address can't be determined, still include + // the NIC with None values. These are usually virtual + // IPv4/IPv6 tunnel interfaces. + if (!pylist_append_fmt( + py_retlist, + "(siOOOO)", + ifa->ifa_name, + AF_UNSPEC, + Py_None, + Py_None, + Py_None, + Py_None + )) + { + goto error; + } } freeifaddrs(ifaddr); diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 276e8d098e..c6893f4ce7 100755 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -10,6 +10,7 @@ """ import platform +import socket import psutil from psutil import AIX @@ -421,7 +422,9 @@ def test_net_if_addrs(self): assert isinstance(ifname, str) for addr in addrs: assert isinstance(addr.family, enum.IntEnum) - assert isinstance(addr.address, str) + assert isinstance(addr.address, (str, type(None))) + if addr.address is None: # virtual NIC + assert addr.family == socket.AF_UNSPEC assert isinstance(addr.netmask, (str, type(None))) assert isinstance(addr.broadcast, (str, type(None))) diff --git a/tests/test_system.py b/tests/test_system.py index 470de16dce..8422b896cf 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -849,13 +849,20 @@ def test_net_if_addrs(self): # psutil.net_io_counters(pernic=True).keys() # ) - families = {socket.AF_INET, socket.AF_INET6, psutil.AF_LINK} + families = { + socket.AF_INET, + socket.AF_INET6, + socket.AF_UNSPEC, + psutil.AF_LINK, + } for nic, addrs in nics.items(): assert isinstance(nic, str) assert len(set(addrs)) == len(addrs) for addr in addrs: assert isinstance(addr.family, int) - assert isinstance(addr.address, str) + assert isinstance(addr.address, (str, type(None))) + if addr.address is None: # virtual NIC + assert addr.family == socket.AF_UNSPEC assert isinstance(addr.netmask, (str, type(None))) assert isinstance(addr.broadcast, (str, type(None))) assert addr.family in families @@ -961,6 +968,13 @@ def test_net_if_stats_enodev(self): assert ret == {} assert m.called + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_nic_names(self): + stdlib_names = {name for _, name in socket.if_nameindex()} + assert stdlib_names == set(psutil.net_io_counters(pernic=True).keys()) + assert stdlib_names == set(psutil.net_if_addrs().keys()) + assert stdlib_names == set(psutil.net_if_stats().keys()) + class TestSensorsAPIs(PsutilTestCase): @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") From e261f6891577c4cb6797caec8b23c2877af34175 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Mar 2026 23:10:49 +0100 Subject: [PATCH 1618/1714] Docs: add stdlib equivalents section (#2781) Add a page maps psutil's Python API to the closest equivalent in the Python standard library. This is useful for understanding what psutil replaces and how the two APIs differ. --- .dprint.jsonc | 2 +- MANIFEST.in | 3 +- docs/DEVNOTES | 3 + docs/_links.rst | 4 + docs/alternatives.rst | 3 + docs/api.rst | 35 ++- docs/faq.rst | 18 ++ docs/index.rst | 5 +- ..._equivalents.rst => shell-equivalents.rst} | 0 docs/stdlib-equivalents.rst | 289 ++++++++++++++++++ scripts/internal/rst_check_dead_refs.py | 22 +- 11 files changed, 361 insertions(+), 23 deletions(-) rename docs/{shell_equivalents.rst => shell-equivalents.rst} (100%) create mode 100644 docs/stdlib-equivalents.rst diff --git a/.dprint.jsonc b/.dprint.jsonc index f4afe95463..be59266815 100644 --- a/.dprint.jsonc +++ b/.dprint.jsonc @@ -25,7 +25,7 @@ ], "plugins": [ "https://plugins.dprint.dev/markdown-0.21.1.wasm", - "https://plugins.dprint.dev/json-0.21.1.wasm", + "https://plugins.dprint.dev/json-0.21.3.wasm", "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm", ], } diff --git a/MANIFEST.in b/MANIFEST.in index 1c2381a859..2d4d2a1b8e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -39,7 +39,8 @@ include docs/migration.rst include docs/platform.rst include docs/recipes.rst include docs/requirements.txt -include docs/shell_equivalents.rst +include docs/shell-equivalents.rst +include docs/stdlib-equivalents.rst include docs/timeline.rst include psutil/__init__.py include psutil/_common.py diff --git a/docs/DEVNOTES b/docs/DEVNOTES index d98971b5e2..1453cb2038 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -8,6 +8,9 @@ https://github.com/giampaolo/psutil/issues FEATURES ======== +- Use resource.getrusage() to get current process CPU times (it has more + precision) + - (UNIX) process root (different from cwd) - (Linux) locked files via /proc/locks: diff --git a/docs/_links.rst b/docs/_links.rst index e69de29bb2..96c16834e6 100644 --- a/docs/_links.rst +++ b/docs/_links.rst @@ -0,0 +1,4 @@ +.. _`BPO-10784`: https://bugs.python.org/issue10784 +.. _`BPO-12442`: https://bugs.python.org/issue12442 +.. _`GH-144047`: https://github.com/python/cpython/pull/144047 +.. _`BPO-6973`: https://bugs.python.org/issue6973 diff --git a/docs/alternatives.rst b/docs/alternatives.rst index b7740bb8f5..5456c525cd 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -10,6 +10,9 @@ See also :doc:`adoption` for notable projects that use psutil. Python standard library ----------------------- +See also :doc:`stdlib-equivalents` for a detailed function-by-function +comparison. + os module ^^^^^^^^^ diff --git a/docs/api.rst b/docs/api.rst index adf47a94e1..a483ab67fc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -485,11 +485,11 @@ Disks See `scripts/disk_usage.py`_ script providing an example usage. Returns a list of named tuples with the following fields: - * **device**: the device path (e.g. ``"/dev/hda1"``). On Windows this is the - drive letter (e.g. ``"C:\\"``). - * **mountpoint**: the mount point path (e.g. ``"/"``). On Windows this is the - drive letter (e.g. ``"C:\\"``). - * **fstype**: the partition filesystem (e.g. ``"ext3"`` on UNIX or ``"NTFS"`` + * **device**: the device path (e.g. "/dev/hda1"). On Windows this is the + drive letter (e.g. "C:\\"). + * **mountpoint**: the mount point path (e.g. "/"). On Windows this is the + drive letter (e.g. "C:\\"). + * **fstype**: the partition filesystem (e.g. "ext3" on UNIX or "NTFS" on Windows). * **opts**: a comma-separated string indicating different mount options for the drive/partition. Platform-dependent. @@ -512,17 +512,20 @@ Disks Return disk usage statistics about the partition which contains the given *path* as a named tuple including **total**, **used** and **free** space expressed in bytes, plus the **percentage** usage. - ``OSError`` is raised if *path* does not exist. - Starting from Python 3.3 this is also available as :func:`shutil.disk_usage` - (see `BPO-12442`_). See `scripts/disk_usage.py`_ script providing an example usage. + This function was later incorporated in Python 3.3 as + :func:`shutil.disk_usage` (`BPO-12442`_). + .. code-block:: pycon >>> import psutil >>> psutil.disk_usage('/') sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) + .. note:: + Note On UNIX, path must point to a path within a **mounted** filesystem partition. + .. note:: UNIX usually reserves 5% of the total disk space for the root user. *total* and *used* fields on UNIX refer to the overall total and used @@ -759,9 +762,11 @@ Network value is a list of named tuples for each address assigned to the NIC. Each named tuple includes 5 fields: - - **family**: the address family, either :data:`socket.AF_INET` or :data:`socket.AF_INET6` - or :const:`psutil.AF_LINK`, which refers to a MAC address. - - **address**: the primary NIC address (always set). + - **family**: the address family, either :data:`socket.AF_INET`, + :data:`socket.AF_INET6`, :const:`psutil.AF_LINK` in case of MAC address, + :data:`socket.AF_UNSPEC` in case of virtual or unconfigured interfaces. + - **address**: the primary NIC address (may be ``None`` in case of virtual + or unconfigured interfaces). - **netmask**: the netmask address (may be ``None``). - **broadcast**: the broadcast address (may be ``None``). - **ptp**: stands for "point to point"; it's the destination address on a @@ -1472,8 +1477,9 @@ Process class 10 >>> - Starting from Python 3.3 this functionality is also available as + This method was later incorporated in Python 3.3 as :func:`os.getpriority` and :func:`os.setpriority` (see `BPO-10784`_). + On Windows this is implemented via `GetPriorityClass`_ and `SetPriorityClass`_ Windows APIs and *value* is one of the :data:`psutil.*_PRIORITY_CLASS ` @@ -2424,7 +2430,7 @@ Process class .. versionchanged:: 7.2.2 on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, use :func:`os.pidfd_open` and :func:`select.kqueue` respectively, instead of less efficient busy-loop - polling. + polling. Later added to CPython 3.15 in `GH-144047`_. ---- @@ -3011,9 +3017,6 @@ Other constants >>> if psutil.version_info >= (4, 5): ... pass -.. _`BPO-10784`: https://bugs.python.org/issue10784 -.. _`BPO-12442`: https://bugs.python.org/issue12442 -.. _`BPO-6973`: https://bugs.python.org/issue6973 .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get .. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt .. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html diff --git a/docs/faq.rst b/docs/faq.rst index 26725ee672..eab20d7dd2 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -253,6 +253,24 @@ example, on a 4-core machine a fully-loaded process can reach 400%. The system-wide :func:`cpu_percent` (without a :class:`Process`) always stays in the 0–100% range because it averages across all cores. +.. _faq_cpu_count: + +What is the difference between psutil, os, and multiprocessing cpu_count? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- :func:`os.cpu_count` returns the number of **logical** CPUs (including + hyperthreads). It is the same as ``psutil.cpu_count(logical=True)``, but + psutil does not honour + `PYTHON_CPU_COUNT `_ + environment variable introduced in Python 3.13. +- :func:`os.process_cpu_count` (Python 3.13+) returns the number of CPUs the + calling process is **allowed to use** (respects CPU affinity and cgroups). + The psutil equivalent is ``len(psutil.Process().cpu_affinity())``. +- :func:`multiprocessing.cpu_count` returns the same value as + :func:`os.process_cpu_count` (Python 3.13+). +- :func:`psutil.cpu_count` with ``logical=False`` returns the number of + **physical** cores, which has no stdlib equivalent. + ---- Memory diff --git a/docs/index.rst b/docs/index.rst index f324fc7233..9e585d68ae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,7 +29,7 @@ It is useful mainly for **system monitoring**, **profiling**, **limiting process resources**, and **managing running processes**. It implements many functionalities offered by UNIX command line tools such as *ps, top, free, iotop, netstat, ifconfig, lsof* and others -(see :doc:`shell equivalents `). +(see :doc:`shell equivalents `). psutil currently supports the following platforms, from **Python 3.7** onwards: - **Linux** @@ -100,7 +100,8 @@ Table of contents API Reference FAQ Recipes - Shell equivalents + Shell equivalents + Stdlib equivalents Glossary Platform support Who uses psutil diff --git a/docs/shell_equivalents.rst b/docs/shell-equivalents.rst similarity index 100% rename from docs/shell_equivalents.rst rename to docs/shell-equivalents.rst diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst new file mode 100644 index 0000000000..e45a4e6c02 --- /dev/null +++ b/docs/stdlib-equivalents.rst @@ -0,0 +1,289 @@ +.. currentmodule:: psutil +.. include:: _links.rst + +Stdlib equivalents +================== + +This page maps psutil's Python API to the closest equivalent in the Python +standard library. This is useful for understanding what psutil replaces and +how the two APIs differ. The most common difference is that stdlib functions +only operate on the **current process**, while psutil works on **any process** +(PID). + +See also the :doc:`alternatives` page for a higher-level discussion of +how psutil compares to the standard library and third-party tools. + +System-wide functions +--------------------- + +CPU +~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 20 37 100 + + * - psutil + - stdlib + - notes + * - :func:`cpu_count` + - :func:`os.cpu_count`, + :func:`multiprocessing.cpu_count` + - Same as ``cpu_count(logical=True)``; no support for + physical cores (``logical=False``). See :ref:`FAQ `. + * - :func:`cpu_count` + - :func:`os.process_cpu_count` + - CPUs the process is allowed to use (Python 3.13+). + Equivalent to ``len(psutil.Process().cpu_affinity())``. + See :ref:`FAQ `. + * - :func:`getloadavg` + - :func:`os.getloadavg` + - Same on POSIX; psutil also supports Windows. + +Disk +~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 25 24 100 + + * - psutil + - stdlib + - notes + * - :func:`disk_usage` + - :func:`shutil.disk_usage` + - Same as :func:`shutil.disk_usage`; psutil also adds + ``percent``. Added to CPython 3.3 (BPO-12442_). + * - :func:`disk_partitions` + - :func:`os.listdrives`, + :func:`os.listmounts`, + :func:`os.listvolumes` + - Windows only (Python 3.12+). Low-level APIs: + drive letters, volume GUIDs, mount points. psutil + combines them in one cross-platform call. + +Network +~~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 25 30 100 + + * - psutil + - stdlib + - notes + * - :func:`net_if_addrs` + - :func:`socket.if_nameindex` + - Stdlib returns NIC names only; psutil also returns + addresses, netmasks, broadcast, and PTP. + +Process +~~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 25 30 100 + + * - psutil + - stdlib + - notes + * - :func:`pid_exists` + - ``os.kill(pid, 0)`` + - Common POSIX idiom; psutil also supports Windows. + +Process methods +--------------- + +Identity +~~~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 23 20 100 + + * - psutil + - stdlib + - notes + * - :attr:`Process.pid` + - :func:`os.getpid` + - + * - :meth:`Process.ppid` + - :func:`os.getppid` + - + * - :meth:`Process.cwd` + - :func:`os.getcwd` + - + * - :meth:`Process.environ` + - :data:`os.environ` + - Will differ from launch environment if modified at runtime. + * - :meth:`Process.exe` + - :data:`sys.executable` + - Python interpreter path only. + * - :meth:`Process.cmdline` + - :data:`sys.argv` + - Python process only. + +Credentials +~~~~~~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 20 37 50 + + * - psutil + - stdlib + - notes + * - :meth:`Process.uids` + - :func:`os.getuid`, + :func:`os.geteuid`, + :func:`os.getresuid` + - + * - :meth:`Process.gids` + - :func:`os.getgid`, + :func:`os.getegid`, + :func:`os.getresgid` + - + * - :meth:`Process.username` + - :func:`os.getlogin`, :func:`getpass.getuser` + - Rough equivalent; not per-process. + +CPU / scheduling +~~~~~~~~~~~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 40 35 100 + + * - psutil + - stdlib + - notes + * - :meth:`Process.cpu_times` + - :func:`os.times` + - :func:`os.times` also has ``elapsed``; psutil adds + ``iowait`` (Linux). + * - :meth:`Process.cpu_times` + - :func:`resource.getrusage` + - ``ru_utime`` / ``ru_stime`` match; have higher precision. + * - :meth:`Process.num_ctx_switches` + - :func:`resource.getrusage` + - Current process only; psutil works for any PID. + * - :meth:`Process.nice() ` + - :func:`os.getpriority`, + :func:`os.setpriority` + - POSIX only; psutil also supports Windows. + Added to CPython 3.3 (BPO-10784_). + * - :meth:`Process.nice() ` + - :func:`os.nice` + - POSIX only; psutil also supports Windows. + * - *no equivalent* + - :func:`os.sched_getscheduler`, + :func:`os.sched_setscheduler` + - Sets scheduling *policy* (``SCHED_*``). Unlike nice, + which sets priority within ``SCHED_OTHER``. Real-time + policies preempt normal processes. + * - :meth:`Process.cpu_affinity() ` + - :func:`os.sched_getaffinity`, + :func:`os.sched_setaffinity` + - Nearly equivalent; both accept a PID. Stdlib is + Linux/BSD; psutil also supports Windows. + * - :meth:`Process.rlimit() ` + - :func:`resource.getrlimit`, + :func:`resource.setrlimit` + - Same interface; psutil works for any PID (Linux only). + +Memory +~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 28 27 100 + + * - psutil + - stdlib + - notes + * - :meth:`Process.memory_info` + - :func:`resource.getrusage` + - Only peak RSS (``ru_maxrss``); psutil returns current + RSS, VMS, and more. + * - :meth:`Process.page_faults` + - :func:`resource.getrusage` + - Current process only. + +I/O +~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 28 27 100 + + * - psutil + - stdlib + - notes + * - :meth:`Process.io_counters` + - :func:`resource.getrusage` + - Block I/O only (``ru_inblock`` / ``ru_oublock``), + current process. psutil returns bytes and counts for any PID. + +Threads +~~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 30 35 100 + + * - psutil + - stdlib + - notes + * - :meth:`Process.num_threads` + - :func:`threading.active_count` + - Stdlib counts Python threads only; psutil counts all OS threads. + * - :meth:`Process.threads` + - :func:`threading.enumerate` + - Stdlib returns :class:`threading.Thread` objects; psutil + returns OS thread IDs with CPU times. + +Signals +~~~~~~~ + +.. list-table:: + :class: longtable + :header-rows: 1 + :widths: 33 45 100 + + * - psutil + - stdlib + - notes + * - :meth:`Process.send_signal` + - :func:`os.kill` + - Same on POSIX; limited on Windows. psutil adds + :exc:`NoSuchProcess` / :exc:`AccessDenied` and avoids + killing reused PIDs. + * - :meth:`Process.suspend` + - :func:`os.kill` + :data:`signal.SIGSTOP` + - Same as above. + * - :meth:`Process.resume` + - :func:`os.kill` + :data:`signal.SIGCONT` + - Same as above. + * - :meth:`Process.terminate` + - :func:`os.kill` + :data:`signal.SIGTERM` + - Same as above. On Windows uses ``TerminateProcess()``. + * - :meth:`Process.kill` + - :func:`os.kill` + :data:`signal.SIGKILL` + - Same as above. On Windows uses ``TerminateProcess()``. + * - :meth:`Process.wait` + - :func:`os.waitpid` + - Child processes only; psutil works for any PID. + * - :meth:`Process.wait` + - :meth:`subprocess.Popen.wait` + - Equivalent; psutil uses efficient OS-level waiting on + Linux/BSD. Added to CPython 3.15 (GH-144047_). diff --git a/scripts/internal/rst_check_dead_refs.py b/scripts/internal/rst_check_dead_refs.py index af518f2761..9e7bdb4384 100755 --- a/scripts/internal/rst_check_dead_refs.py +++ b/scripts/internal/rst_check_dead_refs.py @@ -33,7 +33,11 @@ re.MULTILINE, ) # `Foo Bar`_ but NOT `text `_ and NOT `text`__ -RE_REF = re.compile(r'`([^`<\n]+)`_(?!_)') +RE_BACKTICK_REF = re.compile(r'`([^`<\n]+)`_(?!_)') +# bare reference: BPO-12442_ (word chars and hyphens, no backticks) +RE_BARE_REF = re.compile( + r'(? Date: Sat, 21 Mar 2026 23:12:18 +0100 Subject: [PATCH 1619/1714] Update changelog.rst --- docs/changelog.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6b269dabb9..b3d25c79a5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,7 +15,7 @@ Changelog Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, -:gh:`2775`) +:gh:`2775`, :gh:`2781`) - Split docs from a single HTML file into multiple sections (API reference, install, etc.). @@ -28,8 +28,11 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, notable software using psutil - `/glossary `__: a section explaining the core concepts - - `/shell_equivalents `__: + - `/shell-equivalents `__: maps each psutil API to native CLI commands + - `/stdlib-equivalents `__: + maps psutil's Python API to the closest equivalent in the Python standard + library. - `/install `__ (was old ``INSTALL.rst`` in root dir) - `/credits `__: From 3d343a308dfe4a31ef02607ecfac33e4ecb72f85 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Mar 2026 23:22:04 +0100 Subject: [PATCH 1620/1714] Fix #2776, Windows: make disk_usage() accept a directory path --- docs/changelog.rst | 4 +++- psutil/_pswindows.py | 7 ++++++- tests/test_system.py | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b3d25c79a5..aa6040541a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -135,8 +135,10 @@ Others ``irq`` to match the field name used on Linux and BSD. ``interrupt`` still works but raises :exc:`DeprecationWarning`. See :ref:`migration guide `. -- :gh:`2776`: Windows: :func:`virtual_memory` now includes ``cached`` and +- :gh:`2776`, [Windows]: :func:`virtual_memory` now includes ``cached`` and ``wired`` fields. +- :gh:`2780`, [Windows]: :func:`disk_usage` now can accept a file path (not + only a directory path). **Bug fixes** diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 5cc244e92a..d3cf3b27b8 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -183,7 +183,12 @@ def disk_usage(path): # XXX: do we want to use "strict"? Probably yes, in order # to fail immediately. After all we are accepting input here... path = path.decode(ENCODING, errors="strict") - total, used, free = cext.disk_usage(path) + try: + total, used, free = cext.disk_usage(path) + except NotADirectoryError: + if not os.path.isabs(path): + path = os.path.join(os.getcwd(), path) + total, used, free = cext.disk_usage(os.path.dirname(path)) percent = usage_percent(used, total, round_=1) return ntp.sdiskusage(total, used, free, percent) diff --git a/tests/test_system.py b/tests/test_system.py index 8422b896cf..254d18d91d 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -678,12 +678,15 @@ def test_disk_usage(self): # see https://github.com/giampaolo/psutil/issues/2147 assert abs(usage.used - shutil_usage.used) < tolerance - # if path does not exist OSError ENOENT is expected across + # if path does not exist FileNotFoundError is expected across # all platforms fname = self.get_testfn() with pytest.raises(FileNotFoundError): psutil.disk_usage(fname) + # we should also be able to use a file path + psutil.disk_usage(__file__) + @pytest.mark.skipif(not ASCII_FS, reason="not an ASCII fs") def test_disk_usage_unicode(self): # See: https://github.com/giampaolo/psutil/issues/416 From f1fded83b13d7ddefa94bb70358e1129a1a02b45 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Mar 2026 23:33:19 +0100 Subject: [PATCH 1621/1714] Linux: test disk_io_counters() against iostat CLI --- tests/test_linux.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_linux.py b/tests/test_linux.py index bf519602d7..e2425ae529 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -1339,6 +1339,35 @@ def exists(path): with pytest.raises(NotImplementedError): psutil.disk_io_counters() + @pytest.mark.skipif( + not shutil.which("iostat"), reason="'iostat' command not available" + ) + @retry_on_failure() + def test_against_iostat(self): + # Cross-check read_bytes/write_bytes against 'iostat -d -k' + # cumulative totals (kB_read, kB_wrtn columns). + out = sh(["iostat", "-d", "-k"]) + iostat_disks = {} + for line in out.splitlines(): + fields = line.split() + if len(fields) < 7 or fields[0] in {"Linux", "Device"}: + continue + name = fields[0] + try: + kb_read = int(fields[5]) + kb_wrtn = int(fields[6]) + except ValueError: + continue + iostat_disks[name] = (kb_read * 1024, kb_wrtn * 1024) + + psutil_disks = psutil.disk_io_counters(perdisk=True, nowrap=False) + for name, (bytes_read, bytes_wrtn) in iostat_disks.items(): + if name not in psutil_disks: + continue + stats = psutil_disks[name] + assert abs(stats.read_bytes - bytes_read) < 1024 * 1024 + assert abs(stats.write_bytes - bytes_wrtn) < 1024 * 1024 + class TestRootFsDeviceFinder(LinuxTestCase): def setUp(self): From 89a1765d5271ece4151830ab2a09f7fb46bd480d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Mar 2026 23:41:16 +0100 Subject: [PATCH 1622/1714] Linux: write test for cpu_times() against /proc/stat --- docs/shell-equivalents.rst | 2 +- tests/test_linux.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/shell-equivalents.rst b/docs/shell-equivalents.rst index 63e3c6aa56..7017826d25 100644 --- a/docs/shell-equivalents.rst +++ b/docs/shell-equivalents.rst @@ -213,7 +213,7 @@ Other - same - * - :func:`pids` - - ``ps -eo pid`` + - ``ps -A -eo pid`` - same - same - ``tasklist`` diff --git a/tests/test_linux.py b/tests/test_linux.py index e2425ae529..15f9eebbfd 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -877,6 +877,26 @@ def open_mock(name, *args, **kwargs): assert freq.current == 200 +class TestSystemCPUTimes(LinuxTestCase): + + @retry_on_failure() + def test_against_proc_stat(self): + with open("/proc/stat") as f: + line = f.readline() + ticks = [float(x) for x in line.split()[1:]] + fields = [t / CLOCK_TICKS for t in ticks] + ct = psutil.cpu_times() + TOLERANCE = 1 # 1 second + assert abs(ct.user - fields[0]) < TOLERANCE + assert abs(ct.nice - fields[1]) < TOLERANCE + assert abs(ct.system - fields[2]) < TOLERANCE + assert abs(ct.idle - fields[3]) < TOLERANCE + assert abs(ct.iowait - fields[4]) < TOLERANCE + assert abs(ct.irq - fields[5]) < TOLERANCE + assert abs(ct.softirq - fields[6]) < TOLERANCE + assert abs(ct.steal - fields[7]) < TOLERANCE + + class TestSystemCPUStats(LinuxTestCase): # XXX: fails too often. From eaa56012d2f9b35081f5cab9accd9d4b529a16af Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Mar 2026 23:46:31 +0100 Subject: [PATCH 1623/1714] Linux: test net_connections() against ss CLI --- tests/test_linux.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_linux.py b/tests/test_linux.py index 15f9eebbfd..510d5d1160 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -1156,6 +1156,30 @@ def test_emulate_unix(self): psutil.net_connections(kind='unix') assert m.called + @pytest.mark.skipif( + not shutil.which("ss"), reason="'ss' command not available" + ) + def test_against_ss(self): + # Listening ports are stable, so an exact set comparison is + # reliable. + out = sh(["ss", "-tuanp"]) + ss_ports = set() + for line in out.splitlines(): + fields = line.split() + if ( + len(fields) >= 5 + and fields[0] == "tcp" + and fields[1] == "LISTEN" + ): + port = int(fields[4].rsplit(":", 1)[-1]) + ss_ports.add(port) + psutil_ports = { + c.laddr.port + for c in psutil.net_connections(kind="tcp") + if c.status == psutil.CONN_LISTEN + } + assert ss_ports == psutil_ports + # ===================================================================== # --- system disks From ab295a1b96c03ca1d49ed41327712cf2eaf96bb8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 21 Mar 2026 23:53:09 +0100 Subject: [PATCH 1624/1714] Linux: test swap_memory() sin / sout --- tests/test_linux.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_linux.py b/tests/test_linux.py index 510d5d1160..1e2e22e588 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -555,6 +555,19 @@ def test_free(self): psutil_value = psutil.swap_memory().free assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + @retry_on_failure() + def test_sin_sout(self): + # Cross-check sin/sout against /proc/vmstat pswpin/pswpout fields. + # psutil converts pages to bytes using a 4096-byte page size. + PAGE_SIZE = 4 * 1024 + with open("/proc/vmstat") as f: + vmstat = dict(line.split() for line in f if line.split()) + sin = int(vmstat["pswpin"]) * PAGE_SIZE + sout = int(vmstat["pswpout"]) * PAGE_SIZE + swap = psutil.swap_memory() + assert swap.sin == sin + assert swap.sout == sout + def test_missing_sin_sout(self): with mock.patch('psutil._common.open', create=True) as m: with warnings.catch_warnings(record=True) as ws: From 46b3b7effac3e895e67075ccc6ec02641065a7a0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 00:04:51 +0100 Subject: [PATCH 1625/1714] Linux: test virtual_memory() cached and buffers --- tests/test_linux.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_linux.py b/tests/test_linux.py index 1e2e22e588..345383ec6b 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -324,6 +324,32 @@ def test_inactive(self): assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM +class TestSystemVirtualMemoryAgainstMeminfo(LinuxTestCase): + @staticmethod + def read_meminfo(): + mems = {} + with open("/proc/meminfo") as f: + for line in f: + fields = line.split() + if len(fields) >= 2: + mems[fields[0]] = int(fields[1]) * 1024 + return mems + + @retry_on_failure() + def test_buffers(self): + proc_value = self.read_meminfo()["Buffers:"] + psutil_value = psutil.virtual_memory().buffers + assert abs(psutil_value - proc_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_cached(self): + # psutil cached = Cached + SReclaimable + mems = self.read_meminfo() + proc_value = mems["Cached:"] + mems.get("SReclaimable:", 0) + psutil_value = psutil.virtual_memory().cached + assert abs(psutil_value - proc_value) < TOLERANCE_SYS_MEM + + class TestSystemVirtualMemoryMocks(LinuxTestCase): def test_warnings_on_misses(self): # Emulate a case where /proc/meminfo provides few info. From c464357b4cc27a611c4863bd414ec175d9328fb5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 00:10:10 +0100 Subject: [PATCH 1626/1714] Windows: test virtual_memory() available and used --- tests/test_windows.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_windows.py b/tests/test_windows.py index 474e8fa921..ff2847a4e0 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -186,6 +186,21 @@ def test_wired(self): < TOLERANCE_SYS_MEM ) + @retry_on_failure() + def test_available(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + abs(int(w.AvailableBytes) - psutil.virtual_memory().available) + < TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_used(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + total = psutil.virtual_memory().total + wmi_used = total - int(w.AvailableBytes) + assert abs(psutil.virtual_memory().used - wmi_used) < TOLERANCE_SYS_MEM + class TestSystemAPIs(WindowsTestCase): def test_nic_names(self): From 63e966f1e9244cca72c7d0c417fa014476f593e9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 00:17:46 +0100 Subject: [PATCH 1627/1714] Windows: add tests for cpu_stats() --- tests/test_windows.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/tests/test_windows.py b/tests/test_windows.py index ff2847a4e0..01e5b1812c 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -121,43 +121,45 @@ def wmic(path, what, converter=int): # =================================================================== -class TestCpuAPIs(WindowsTestCase): +class TestCpuCount(WindowsTestCase): @pytest.mark.skipif( 'NUMBER_OF_PROCESSORS' not in os.environ, reason="NUMBER_OF_PROCESSORS env var is not available", ) - def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): + def test_against_NUMBER_OF_PROCESSORS(self): # Will likely fail on many-cores systems: # https://stackoverflow.com/questions/31209256 num_cpus = int(os.environ['NUMBER_OF_PROCESSORS']) assert num_cpus == psutil.cpu_count() - def test_cpu_count_vs_GetSystemInfo(self): + def test_against_GetSystemInfo(self): # Will likely fail on many-cores systems: # https://stackoverflow.com/questions/31209256 assert psutil.cpu_count() == win32api.GetSystemInfo()[5] - def test_cpu_count_logical_vs_wmi(self): + def test_against_wmi(self): w = wmi.WMI() procs = sum( proc.NumberOfLogicalProcessors for proc in w.Win32_Processor() ) assert psutil.cpu_count() == procs - def test_cpu_count_cores_vs_wmi(self): + def test_cores_against_wmi(self): w = wmi.WMI() cores = sum(proc.NumberOfCores for proc in w.Win32_Processor()) assert psutil.cpu_count(logical=False) == cores - def test_cpu_count_vs_cpu_times(self): + def test_against_cpu_times(self): assert psutil.cpu_count() == len(psutil.cpu_times(percpu=True)) - def test_cpu_times_irq_field(self): + def test_irq_field(self): t = psutil.cpu_times() assert t.irq >= 0 with pytest.warns(DeprecationWarning, match="interrupt"): assert t.interrupt == t.irq + +class TestCpuFreq(WindowsTestCase): def test_cpu_freq(self): w = wmi.WMI() proc = w.Win32_Processor()[0] @@ -165,6 +167,29 @@ def test_cpu_freq(self): assert proc.MaxClockSpeed == psutil.cpu_freq().max +class TestCpuStats(WindowsTestCase): + + @retry_on_failure() + def test_ctx_switches(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_System()[0] + wmi_value = int(w.ContextSwitchesPersec) + psutil_value = psutil.cpu_stats().ctx_switches + assert abs(psutil_value - wmi_value) < 1000 + + @retry_on_failure() + def test_interrupts(self): + # Interrupts are summed across all CPUs; use _Total from + # Win32_PerfRawData_PerfOS_Processor. + w = wmi.WMI().Win32_PerfRawData_PerfOS_Processor(Name="_Total")[0] + wmi_value = int(w.InterruptsPersec) + psutil_value = psutil.cpu_stats().interrupts + assert abs(psutil_value - wmi_value) < 1000 + + def test_soft_interrupts(self): + # Always 0 on Windows. + assert psutil.cpu_stats().soft_interrupts == 0 + + class TestVirtualMemory(WindowsTestCase): def test_total(self): From 159414836badb6c3baf2db830346bbce73da0812 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 00:23:26 +0100 Subject: [PATCH 1628/1714] Move windows tests around --- tests/test_windows.py | 104 +++++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/tests/test_windows.py b/tests/test_windows.py index 01e5b1812c..7ac620bdb2 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -227,8 +227,9 @@ def test_used(self): assert abs(psutil.virtual_memory().used - wmi_used) < TOLERANCE_SYS_MEM -class TestSystemAPIs(WindowsTestCase): - def test_nic_names(self): +class NetAPIs(WindowsTestCase): + + def test_net_io_counters_nic_names(self): out = sh('ipconfig /all') nics = psutil.net_io_counters(pernic=True).keys() for nic in nics: @@ -265,7 +266,21 @@ def test_net_connections(self): win_ports = {int(p) for p in out.strip().split(',') if p.strip()} assert ps_ports == win_ports - def test_total_swapmem(self): + 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) + assert ( + ps_names & wmi_names + ), f"no common entries in {ps_names}, {wmi_names}" + + +class TestSwapMemory(WindowsTestCase): + + def test_total(self): w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] assert ( int(w.CommitLimit) - psutil.virtual_memory().total @@ -275,7 +290,7 @@ def test_total_swapmem(self): assert psutil.swap_memory().free == 0 assert psutil.swap_memory().used == 0 - def test_percent_swapmem(self): + def test_percent(self): if psutil.swap_memory().total > 0: w = wmi.WMI().Win32_PerfRawData_PerfOS_PagingFile(Name="_Total")[0] # calculate swap usage to percent @@ -286,29 +301,11 @@ def test_percent_swapmem(self): assert abs(psutil.swap_memory().percent - percentSwap) < 5 assert psutil.swap_memory().percent <= 100 - # @pytest.mark.skipif(wmi is None, reason="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 - @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 = {x.ProcessId for x in w} - psutil_pids = set(psutil.pids()) - assert wmi_pids == psutil_pids +class TestDiskApis(WindowsTestCase): @retry_on_failure() - def test_disks(self): + def test_disk_partitions(self): ps_parts = psutil.disk_partitions(all=True) wmi_parts = wmi.WMI().Win32_LogicalDisk() for ps_part in ps_parts: @@ -338,18 +335,7 @@ def test_disks(self): else: return pytest.fail(f"can't find partition {ps_part!r}") - @retry_on_failure() - def test_disk_usage(self): - for disk in psutil.disk_partitions(): - if 'cdrom' in disk.opts: - continue - win = win32api.GetDiskFreeSpaceEx(disk.mountpoint) - ps = psutil.disk_usage(disk.mountpoint) - assert abs(win[0] - ps.free) < TOLERANCE_DISK_USAGE - assert abs(win[1] - ps.total) < TOLERANCE_DISK_USAGE - assert ps.used == ps.total - ps.free - - def test_disk_partitions(self): + def test_disk_partitions_mountpoint(self): win = [ x + '\\' for x in win32api.GetLogicalDriveStrings().split("\\\x00") @@ -362,6 +348,41 @@ def test_disk_partitions(self): ] assert win == ps + @retry_on_failure() + def test_disk_usage(self): + for disk in psutil.disk_partitions(): + if 'cdrom' in disk.opts: + continue + win = win32api.GetDiskFreeSpaceEx(disk.mountpoint) + ps = psutil.disk_usage(disk.mountpoint) + assert abs(win[0] - ps.free) < TOLERANCE_DISK_USAGE + assert abs(win[1] - ps.total) < TOLERANCE_DISK_USAGE + assert ps.used == ps.total - ps.free + + +class TestOtherSystemAPIs(WindowsTestCase): + + # @pytest.mark.skipif(wmi is None, reason="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 + @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 = {x.ProcessId for x in w} + psutil_pids = set(psutil.pids()) + assert wmi_pids == psutil_pids + def test_convert_dos_path_drive(self): winpath = 'C:\\Windows\\Temp' driveletter = 'C:' @@ -394,17 +415,6 @@ def test_convert_dos_path_unc(self): assert psutil._pswindows.convert_dos_path(ntpath1) == winpath assert psutil._pswindows.convert_dos_path(ntpath2) == winpath - 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) - assert ( - ps_names & wmi_names - ), f"no common entries in {ps_names}, {wmi_names}" - def test_boot_time(self): wmi_os = wmi.WMI().Win32_OperatingSystem() wmi_btime_str = wmi_os[0].LastBootUpTime.split('.')[0] From e8dff65959064fe3a5e13d78d60eab21714805b3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 00:49:39 +0100 Subject: [PATCH 1629/1714] Windows: add test for disk_io_counters() --- tests/test_windows.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_windows.py b/tests/test_windows.py index 7ac620bdb2..a9c09da849 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -122,9 +122,10 @@ def wmic(path, what, converter=int): class TestCpuCount(WindowsTestCase): + @pytest.mark.skipif( 'NUMBER_OF_PROCESSORS' not in os.environ, - reason="NUMBER_OF_PROCESSORS env var is not available", + reason="env var not available", ) def test_against_NUMBER_OF_PROCESSORS(self): # Will likely fail on many-cores systems: @@ -359,6 +360,16 @@ def test_disk_usage(self): assert abs(win[1] - ps.total) < TOLERANCE_DISK_USAGE assert ps.used == ps.total - ps.free + @retry_on_failure() + def test_disk_io_counters(self): + stats = psutil.disk_io_counters() + w = wmi.WMI().Win32_PerfRawData_PerfDisk_PhysicalDisk(Name="_Total")[0] + tolerance = 10 * 1024 * 1024 + assert abs(stats.read_bytes - int(w.DiskReadBytesPersec)) < tolerance + assert abs(stats.write_bytes - int(w.DiskWriteBytesPersec)) < tolerance + assert abs(stats.read_count - int(w.DiskReadsPersec)) < 1000 + assert abs(stats.write_count - int(w.DiskWritesPersec)) < 1000 + class TestOtherSystemAPIs(WindowsTestCase): From 659170a992be60c1ce54d195f39e55184bb7d685 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 00:58:52 +0100 Subject: [PATCH 1630/1714] Windows: add test for net_io_counters() --- tests/test_windows.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_windows.py b/tests/test_windows.py index a9c09da849..db9b9edad8 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -241,6 +241,17 @@ def test_net_io_counters_nic_names(self): f"{nic!r} nic wasn't found in 'ipconfig /all' output" ) + @retry_on_failure() + def test_net_io_counters(self): + ps = psutil.net_io_counters(pernic=False) + wmi_recv = wmi_sent = 0 + for nic in wmi.WMI().Win32_PerfRawData_Tcpip_NetworkInterface(): + wmi_recv += int(nic.BytesReceivedPerSec) + wmi_sent += int(nic.BytesSentPerSec) + tolerance = 1 * 1024 * 1024 # 1 MB + assert abs(ps.bytes_recv - wmi_recv) < tolerance + assert abs(ps.bytes_sent - wmi_sent) < tolerance + def test_net_if_addrs(self): ps_addrs = set() for addrs in psutil.net_if_addrs().values(): From 959212908526412ed4276076f99a0a3ffbbccbc2 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 01:09:53 +0100 Subject: [PATCH 1631/1714] Windows: add test for users() --- tests/test_windows.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_windows.py b/tests/test_windows.py index db9b9edad8..4d9f22eb42 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -242,6 +242,7 @@ def test_net_io_counters_nic_names(self): ) @retry_on_failure() + @pytest.mark.skipif(GITHUB_ACTIONS, reason="unreliable on GITHUB") def test_net_io_counters(self): ps = psutil.net_io_counters(pernic=False) wmi_recv = wmi_sent = 0 @@ -454,6 +455,10 @@ def test_uptime(self): secs = ms / 1000.0 assert abs(cext.uptime() - secs) < 0.5 + def test_users(self): + current = win32api.GetUserName() + assert current in {u.name for u in psutil.users()} + # =================================================================== # sensors_battery() From a5363c1135538370bb39b72bb3bbf343922ba741 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 01:36:09 +0100 Subject: [PATCH 1632/1714] Reorg test classes/methods --- tests/test_linux.py | 36 ++++++----- tests/test_osx.py | 135 +++++++++++++++++++++++------------------- tests/test_windows.py | 50 ++++++++-------- 3 files changed, 117 insertions(+), 104 deletions(-) diff --git a/tests/test_linux.py b/tests/test_linux.py index 345383ec6b..fc8c92573e 100755 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -229,7 +229,7 @@ def open_mock(name, *args, **kwargs): # ===================================================================== -class TestSystemVirtualMemoryAgainstFree(LinuxTestCase): +class TestVirtualMemoryAgainstFree(LinuxTestCase): def test_total(self): cli_value = free_physmem().total psutil_value = psutil.virtual_memory().total @@ -279,7 +279,7 @@ def test_available(self): assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM -class TestSystemVirtualMemoryAgainstVmstat(LinuxTestCase): +class TestVirtualMemoryAgainstVmstat(LinuxTestCase): def test_total(self): vmstat_value = vmstat('total memory') * 1024 psutil_value = psutil.virtual_memory().total @@ -324,7 +324,7 @@ def test_inactive(self): assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM -class TestSystemVirtualMemoryAgainstMeminfo(LinuxTestCase): +class TestVirtualMemoryAgainstMeminfo(LinuxTestCase): @staticmethod def read_meminfo(): mems = {} @@ -350,7 +350,7 @@ def test_cached(self): assert abs(psutil_value - proc_value) < TOLERANCE_SYS_MEM -class TestSystemVirtualMemoryMocks(LinuxTestCase): +class TestVirtualMemoryMocks(LinuxTestCase): 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 @@ -556,7 +556,7 @@ def test_virtual_memory_mocked(self): # ===================================================================== -class TestSystemSwapMemory(LinuxTestCase): +class TestSwapMemory(LinuxTestCase): @staticmethod def meminfo_has_swap_info(): """Return True if /proc/meminfo provides swap metrics.""" @@ -657,7 +657,7 @@ def test_emulate_meminfo_has_no_metrics(self): # ===================================================================== -class TestSystemCPUCountLogical(LinuxTestCase): +class TestCpuCountLogical(LinuxTestCase): @pytest.mark.skipif( not os.path.exists("/sys/devices/system/cpu/online"), reason="/sys/devices/system/cpu/online does not exist", @@ -730,7 +730,7 @@ def test_emulate_fallbacks(self): assert m.called -class TestSystemCPUCountCores(LinuxTestCase): +class TestCpuCountCores(LinuxTestCase): @pytest.mark.skipif( not shutil.which("lscpu"), reason="lscpu utility not available" ) @@ -762,7 +762,7 @@ def test_emulate_none(self): assert m2.called -class TestSystemCPUFrequency(LinuxTestCase): +class TestCpuFreq(LinuxTestCase): @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") @pytest.mark.skipif( AARCH64, reason="aarch64 does not always expose frequency" @@ -916,7 +916,7 @@ def open_mock(name, *args, **kwargs): assert freq.current == 200 -class TestSystemCPUTimes(LinuxTestCase): +class TestCpuTimes(LinuxTestCase): @retry_on_failure() def test_against_proc_stat(self): @@ -936,7 +936,7 @@ def test_against_proc_stat(self): assert abs(ct.steal - fields[7]) < TOLERANCE -class TestSystemCPUStats(LinuxTestCase): +class TestCpuStats(LinuxTestCase): # XXX: fails too often. # def test_ctx_switches(self): @@ -966,7 +966,7 @@ def test_getloadavg(self): # ===================================================================== -class TestSystemNetIfAddrs(LinuxTestCase): +class TestNetIfAddrs(LinuxTestCase): def test_ips(self): for name, addrs in psutil.net_if_addrs().items(): for addr in addrs: @@ -1067,7 +1067,7 @@ def test_against_ip_addr_v6(self): # assert len(nics) == found -class TestSystemNetIfStats(LinuxTestCase): +class TestNetIfStats(LinuxTestCase): @pytest.mark.skipif( not shutil.which("ifconfig"), reason="ifconfig utility not available" ) @@ -1121,7 +1121,7 @@ def test_flags(self): return pytest.fail("no matches were found") -class TestSystemNetIOCounters(LinuxTestCase): +class TestNetIoCounters(LinuxTestCase): @pytest.mark.skipif( not shutil.which("ifconfig"), reason="ifconfig utility not available" ) @@ -1172,7 +1172,7 @@ def ifconfig(nic): assert abs(stats.dropout - ifconfig_ret['dropout']) < 10 -class TestSystemNetConnections(LinuxTestCase): +class TestNetConnections(LinuxTestCase): @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): @@ -1225,7 +1225,7 @@ def test_against_ss(self): # ===================================================================== -class TestSystemDiskPartitions(LinuxTestCase): +class TestDiskPartitions(LinuxTestCase): @pytest.mark.skipif( not hasattr(os, 'statvfs'), reason="os.statvfs() not available" ) @@ -1288,7 +1288,7 @@ def test_emulate_realpath_fail(self): psutil.PROCFS_PATH = "/proc" -class TestSystemDiskIoCounters(LinuxTestCase): +class TestDiskIoCounters(LinuxTestCase): def test_emulate_kernel_2_4(self): # Tests /proc/diskstats parsing format for 2.4 kernels, see: # https://github.com/giampaolo/psutil/issues/767 @@ -1847,9 +1847,7 @@ def test_emulate_no_power(self): ): assert psutil.sensors_battery().power_plugged is None - -class TestSensorsBatteryEmulated(LinuxTestCase): - def test_it(self): + def test_fully_emulated(self): def open_mock(name, *args, **kwargs): if name.endswith("/energy_now"): return io.StringIO("60000000") diff --git a/tests/test_osx.py b/tests/test_osx.py index 16e5589799..adbee49ca2 100755 --- a/tests/test_osx.py +++ b/tests/test_osx.py @@ -57,7 +57,13 @@ class MacosTestCase(PsutilTestCase): pass +# ===================================================================== +# --- Process APIs (most are tested in test_posix.py) +# ===================================================================== + + class TestProcess(MacosTestCase): + @classmethod def setUpClass(cls): cls.pid = spawn_subproc().pid @@ -66,7 +72,7 @@ def setUpClass(cls): def tearDownClass(cls): terminate(cls.pid) - def test_process_create_time(self): + def test_create_time(self): output = sh(f"ps -o lstart -p {self.pid}") start_ps = output.replace('STARTED', '').strip() hhmmss = start_ps.split(' ')[-2] @@ -78,58 +84,14 @@ def test_process_create_time(self): assert year == time.strftime("%Y", time.localtime(start_psutil)) -class TestSystemAPIs(MacosTestCase): - - # --- disk - - @retry_on_failure() - def test_disks(self): - # test psutil.disk_usage() and psutil.disk_partitions() - # against "df -a" - def df(path): - out = sh(f'df -k "{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) - assert part.device == dev - assert usage.total == total - assert abs(usage.free - free) < TOLERANCE_DISK_USAGE - assert abs(usage.used - used) < TOLERANCE_DISK_USAGE - - # --- cpu - - def test_cpu_count_logical(self): - num = sysctl("sysctl hw.logicalcpu") - assert num == psutil.cpu_count(logical=True) - - def test_cpu_count_cores(self): - num = sysctl("sysctl hw.physicalcpu") - assert num == psutil.cpu_count(logical=False) +# ===================================================================== +# --- Test system APIs +# ===================================================================== - @pytest.mark.skipif( - MACOS and AARCH64 and not HAS_CPU_FREQ, - reason="not available on MACOS + AARCH64", - ) - def test_cpu_freq(self): - freq = psutil.cpu_freq() - assert freq.current * 1000 * 1000 == sysctl("sysctl hw.cpufrequency") - assert freq.min * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_min") - assert freq.max * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_max") - # --- virtual mem +class TestVirtualMemory(MacosTestCase): - def test_vmem_total(self): + def test_total(self): sysctl_hwphymem = sysctl('sysctl hw.memsize') assert sysctl_hwphymem == psutil.virtual_memory().total @@ -138,7 +100,7 @@ def test_vmem_total(self): reason="skipped on MACOS + ARM64 + CI_TESTING", ) @retry_on_failure() - def test_vmem_free(self): + def test_free(self): vmstat_val = vm_stat("free") psutil_val = psutil.virtual_memory().free assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM @@ -148,7 +110,7 @@ def test_vmem_free(self): reason="skipped on MACOS + ARM64 + CI_TESTING", ) @retry_on_failure() - def test_vmem_active(self): + def test_active(self): vmstat_val = vm_stat("active") psutil_val = psutil.virtual_memory().active assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM @@ -156,32 +118,83 @@ def test_vmem_active(self): # XXX: fails too often @pytest.mark.skipif(CI_TESTING, reason="skipped on CI_TESTING") @retry_on_failure() - def test_vmem_inactive(self): + def test_inactive(self): vmstat_val = vm_stat("inactive") psutil_val = psutil.virtual_memory().inactive assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM @retry_on_failure() - def test_vmem_wired(self): + def test_wired(self): vmstat_val = vm_stat("wired") psutil_val = psutil.virtual_memory().wired assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM - # --- swap mem + +class TestSwapMemory(MacosTestCase): @retry_on_failure() - def test_swapmem_sin(self): + def test_sin(self): vmstat_val = vm_stat("Pageins") psutil_val = psutil.swap_memory().sin assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM @retry_on_failure() - def test_swapmem_sout(self): + def test_sout(self): vmstat_val = vm_stat("Pageout") psutil_val = psutil.swap_memory().sout assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM - # --- network + +class TestCpuAPIs(MacosTestCase): + + def test_cpu_count_logical(self): + num = sysctl("sysctl hw.logicalcpu") + assert num == psutil.cpu_count(logical=True) + + def test_cpu_count_cores(self): + num = sysctl("sysctl hw.physicalcpu") + assert num == psutil.cpu_count(logical=False) + + @pytest.mark.skipif( + MACOS and AARCH64 and not HAS_CPU_FREQ, + reason="not available on MACOS + AARCH64", + ) + def test_cpu_freq(self): + freq = psutil.cpu_freq() + assert freq.current * 1000 * 1000 == sysctl("sysctl hw.cpufrequency") + assert freq.min * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_min") + assert freq.max * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_max") + + +class TestDiskAPIs(MacosTestCase): + + @retry_on_failure() + def test_disk_partitions(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh(f'df -k "{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) + assert part.device == dev + assert usage.total == total + assert abs(usage.free - free) < TOLERANCE_DISK_USAGE + assert abs(usage.used - used) < TOLERANCE_DISK_USAGE + + +class TestNetAPIs(MacosTestCase): def test_net_if_stats(self): for name, stats in psutil.net_if_stats().items(): @@ -193,7 +206,8 @@ def test_net_if_stats(self): assert stats.isup == ('RUNNING' in out), out assert stats.mtu == int(re.findall(r'mtu (\d+)', out)[0]) - # --- sensors_battery + +class TestSensorsAPIs(MacosTestCase): @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") def test_sensors_battery(self): @@ -205,7 +219,8 @@ def test_sensors_battery(self): assert psutil_result.power_plugged == power_plugged assert psutil_result.percent == int(percent) - # --- others + +class TestOtherSystemAPIs(MacosTestCase): def test_boot_time(self): out = sh('sysctl kern.boottime') diff --git a/tests/test_windows.py b/tests/test_windows.py index 4d9f22eb42..171042afb9 100755 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -228,7 +228,31 @@ def test_used(self): assert abs(psutil.virtual_memory().used - wmi_used) < TOLERANCE_SYS_MEM -class NetAPIs(WindowsTestCase): +class TestSwapMemory(WindowsTestCase): + + def test_total(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + int(w.CommitLimit) - psutil.virtual_memory().total + == psutil.swap_memory().total + ) + if psutil.swap_memory().total == 0: + assert psutil.swap_memory().free == 0 + assert psutil.swap_memory().used == 0 + + def test_percent(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% + assert psutil.swap_memory().percent >= 0 + assert abs(psutil.swap_memory().percent - percentSwap) < 5 + assert psutil.swap_memory().percent <= 100 + + +class TestNetAPIs(WindowsTestCase): def test_net_io_counters_nic_names(self): out = sh('ipconfig /all') @@ -291,30 +315,6 @@ def test_net_if_stats(self): ), f"no common entries in {ps_names}, {wmi_names}" -class TestSwapMemory(WindowsTestCase): - - def test_total(self): - w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] - assert ( - int(w.CommitLimit) - psutil.virtual_memory().total - == psutil.swap_memory().total - ) - if psutil.swap_memory().total == 0: - assert psutil.swap_memory().free == 0 - assert psutil.swap_memory().used == 0 - - def test_percent(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% - assert psutil.swap_memory().percent >= 0 - assert abs(psutil.swap_memory().percent - percentSwap) < 5 - assert psutil.swap_memory().percent <= 100 - - class TestDiskApis(WindowsTestCase): @retry_on_failure() From 9df45f5aa149c2424c90ce570efef79422f35e05 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 01:38:45 +0100 Subject: [PATCH 1633/1714] Osx: add tests for swap memory total/used/free --- tests/test_osx.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_osx.py b/tests/test_osx.py index adbee49ca2..e3d1dfdff8 100755 --- a/tests/test_osx.py +++ b/tests/test_osx.py @@ -132,6 +132,35 @@ def test_wired(self): class TestSwapMemory(MacosTestCase): + @staticmethod + def parse_swapusage(out): + # Parse 'sysctl vm.swapusage' output into bytes. + # E.g. 'total = 2.00G' -> 2147483648. + units = {"K": 1024, "M": 1024**2, "G": 1024**3} + ret = {} + for key in ("total", "used", "free"): + m = re.search(rf"{key}\s*=\s*([0-9.]+)([KMG])", out) + ret[key] = int(float(m.group(1)) * units[m.group(2)]) + return ret + + def test_total(self): + out = sh("sysctl vm.swapusage") + sysctl_val = self.parse_swapusage(out)["total"] + # 0.01M display precision = ~10KB rounding + assert abs(psutil.swap_memory().total - sysctl_val) < 100 * 1024 + + @retry_on_failure() + def test_used(self): + out = sh("sysctl vm.swapusage") + sysctl_val = self.parse_swapusage(out)["used"] + assert abs(psutil.swap_memory().used - sysctl_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_free(self): + out = sh("sysctl vm.swapusage") + sysctl_val = self.parse_swapusage(out)["free"] + assert abs(psutil.swap_memory().free - sysctl_val) < TOLERANCE_SYS_MEM + @retry_on_failure() def test_sin(self): vmstat_val = vm_stat("Pageins") From 915d438f065b501f9d35a111275682122ae418f4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 02:13:23 +0100 Subject: [PATCH 1634/1714] Osx: test net_io_counters() --- tests/test_osx.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_osx.py b/tests/test_osx.py index e3d1dfdff8..7bf09e33b7 100755 --- a/tests/test_osx.py +++ b/tests/test_osx.py @@ -235,6 +235,28 @@ def test_net_if_stats(self): assert stats.isup == ('RUNNING' in out), out assert stats.mtu == int(re.findall(r'mtu (\d+)', out)[0]) + @retry_on_failure() + def test_net_io_counters(self): + out = sh("netstat -ib") + netstat = {} + for line in out.splitlines(): + fields = line.split() + if len(fields) < 10 or " Date: Sun, 22 Mar 2026 02:29:43 +0100 Subject: [PATCH 1635/1714] FreeBSD: test net_io_counters() --- tests/test_bsd.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_bsd.py b/tests/test_bsd.py index 60b3e1dd78..419384f6a1 100755 --- a/tests/test_bsd.py +++ b/tests/test_bsd.py @@ -401,6 +401,25 @@ def test_swapmem_total(self): total, _used, _free = self.parse_swapinfo() assert abs(psutil.swap_memory().total - total) < TOLERANCE_SYS_MEM + # --- net + + @retry_on_failure() + def test_net_io_counters(self): + out = sh("netstat -ib") + netstat = {} + for line in out.splitlines(): + fields = line.split() + if len(fields) == 12 and " Date: Sun, 22 Mar 2026 02:33:24 +0100 Subject: [PATCH 1636/1714] BSD: reorganize test classes --- tests/test_bsd.py | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/test_bsd.py b/tests/test_bsd.py index 419384f6a1..fc4155fa97 100755 --- a/tests/test_bsd.py +++ b/tests/test_bsd.py @@ -71,26 +71,8 @@ def muse(field): @pytest.mark.skipif(not BSD, reason="BSD only") -class BSDTestCase(PsutilTestCase): - """Generic tests common to all BSD variants.""" - - @classmethod - def setUpClass(cls): - cls.pid = spawn_subproc().pid - - @classmethod - def tearDownClass(cls): - terminate(cls.pid) - - @pytest.mark.skipif(NETBSD, reason="-o lstart doesn't work on NETBSD") - def test_process_create_time(self): - output = sh(f"ps -o lstart -p {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) - ) - assert start_ps == start_psutil +class TestSystemAPIs(PsutilTestCase): + """System tests common to all BSD variants.""" def test_disks(self): # test psutil.disk_usage() and psutil.disk_partitions() @@ -151,13 +133,35 @@ def test_net_if_stats(self): assert stats.mtu == int(re.findall(r'mtu (\d+)', out)[0]) +@pytest.mark.skipif(not BSD, reason="BSD only") +class TestProcessAPIs(PsutilTestCase): + + @classmethod + def setUpClass(cls): + cls.pid = spawn_subproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @pytest.mark.skipif(NETBSD, reason="-o lstart doesn't work on NETBSD") + def test_create_time(self): + output = sh(f"ps -o lstart -p {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) + ) + assert start_ps == start_psutil + + # ===================================================================== # --- FreeBSD # ===================================================================== @pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") -class FreeBSDTestCase(PsutilTestCase): +class FreeBSDProcessTestCase(PsutilTestCase): @classmethod def setUpClass(cls): cls.pid = spawn_subproc().pid From dab42fa6837d67af5d3ae0f7e4b872d2af2d06d5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 02:36:24 +0100 Subject: [PATCH 1637/1714] FreeBSD: remove all tests relying on muse CLI (no longer available) --- tests/test_bsd.py | 63 +---------------------------------------------- 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/tests/test_bsd.py b/tests/test_bsd.py index fc4155fa97..3556859e32 100755 --- a/tests/test_bsd.py +++ b/tests/test_bsd.py @@ -10,7 +10,6 @@ """Tests specific to all BSD platforms.""" import datetime -import os import re import shutil import time @@ -30,13 +29,7 @@ from . import spawn_subproc from . import terminate -if BSD: - PAGESIZE = psutil._psplatform.cext.getpagesize() - # muse requires root privileges - MUSE_AVAILABLE = os.getuid() == 0 and shutil.which("muse") -else: - PAGESIZE = None - MUSE_AVAILABLE = False +PAGESIZE = psutil._psplatform.cext.getpagesize() if BSD else None def sysctl(cmdline): @@ -54,17 +47,6 @@ def sysctl(cmdline): 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* # ===================================================================== @@ -316,49 +298,6 @@ def test_vmem_buffers(self): syst = sysctl("vfs.bufspace") assert abs(psutil.virtual_memory().buffers - syst) < TOLERANCE_SYS_MEM - # --- virtual_memory(); tests against muse - - @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") - def test_muse_vmem_total(self): - num = muse('Total') - assert psutil.virtual_memory().total == num - - @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") - @retry_on_failure() - def test_muse_vmem_active(self): - num = muse('Active') - assert abs(psutil.virtual_memory().active - num) < TOLERANCE_SYS_MEM - - @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") - @retry_on_failure() - def test_muse_vmem_inactive(self): - num = muse('Inactive') - assert abs(psutil.virtual_memory().inactive - num) < TOLERANCE_SYS_MEM - - @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") - @retry_on_failure() - def test_muse_vmem_wired(self): - num = muse('Wired') - assert abs(psutil.virtual_memory().wired - num) < TOLERANCE_SYS_MEM - - @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") - @retry_on_failure() - def test_muse_vmem_cached(self): - num = muse('Cache') - assert abs(psutil.virtual_memory().cached - num) < TOLERANCE_SYS_MEM - - @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") - @retry_on_failure() - def test_muse_vmem_free(self): - num = muse('Free') - assert abs(psutil.virtual_memory().free - num) < TOLERANCE_SYS_MEM - - @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") - @retry_on_failure() - def test_muse_vmem_buffers(self): - num = muse('Buffer') - assert abs(psutil.virtual_memory().buffers - num) < TOLERANCE_SYS_MEM - def test_cpu_stats_ctx_switches(self): assert ( abs( From f525199297fbe58a92bb1114b8ee9a9b0079f2ab Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 03:14:28 +0100 Subject: [PATCH 1638/1714] Fix #2782, null CPU cores on FreeBSD This likely happens on systems without hyper threading. It turns out there's no need to parse the XML topology. We can just use this: $ sysctl -d kern.smp.cores kern.smp.cores: Number of physical cores online --- docs/changelog.rst | 2 ++ psutil/_psbsd.py | 25 +------------------------ psutil/_psutil_bsd.c | 2 +- psutil/arch/freebsd/cpu.c | 21 +++++---------------- psutil/arch/freebsd/init.h | 2 +- tests/test_bsd.py | 4 ++++ 6 files changed, 14 insertions(+), 42 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index aa6040541a..f33a424ee9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -162,6 +162,8 @@ Others of addresses. Main reason: it creates an inconsistency with :func:`net_io_counters` and :func:`net_if_stats` which do return these interface names. +- :gh:`2782`, [FreeBSD]: :func:`cpu_count` ``logical=False`` return None on + systems without hyper threading. 7.2.3 — 2026-02-08 ^^^^^^^^^^^^^^^^^^ diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 89dfea222f..f6f36b7090 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -10,7 +10,6 @@ import os from collections import defaultdict from collections import namedtuple -from xml.etree import ElementTree # noqa: ICN001 from . import _ntuples as ntp from . import _psposix @@ -161,29 +160,7 @@ def cpu_count_cores(): 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_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 = 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 == 1 it's obvious we' have only 1 core. - if cpu_count_logical() == 1: - return 1 - return ret + return cext.cpu_count_cores() def cpu_stats(): diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 7053a2d93f..a370ba8db7 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -86,7 +86,7 @@ static PyMethodDef mod_methods[] = { {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) - {"cpu_topology", psutil_cpu_topology, METH_VARARGS}, + {"cpu_count_cores", psutil_cpu_count_cores, METH_VARARGS}, {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, {"sensors_cpu_temperature", psutil_sensors_cpu_temperature, METH_VARARGS}, #endif diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 816c0bdc7b..8064348313 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -81,23 +81,12 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { PyObject * -psutil_cpu_topology(PyObject *self, PyObject *args) { - char *topology = NULL; - size_t size = 0; - PyObject *py_str; - - if (psutil_sysctlbyname_malloc( - "kern.sched.topology_spec", &topology, &size - ) - != 0) - { - psutil_debug("ignore sysctlbyname('kern.sched.topology_spec') error"); - Py_RETURN_NONE; - } +psutil_cpu_count_cores(PyObject *self, PyObject *args) { + int num; - py_str = PyUnicode_FromString(topology); - free(topology); - return py_str; + if (psutil_sysctlbyname("kern.smp.cores", &num, sizeof(num)) != 0) + Py_RETURN_NONE; + return Py_BuildValue("i", num); } diff --git a/psutil/arch/freebsd/init.h b/psutil/arch/freebsd/init.h index e4a388052a..7ecb8790c3 100644 --- a/psutil/arch/freebsd/init.h +++ b/psutil/arch/freebsd/init.h @@ -10,9 +10,9 @@ int _psutil_pids(pid_t **pids_array, int *pids_count); +PyObject *psutil_cpu_count_cores(PyObject *self, PyObject *args); PyObject *psutil_cpu_freq(PyObject *self, PyObject *args); PyObject *psutil_cpu_stats(PyObject *self, PyObject *args); -PyObject *psutil_cpu_topology(PyObject *self, PyObject *args); PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); PyObject *psutil_net_connections(PyObject *self, PyObject *args); PyObject *psutil_per_cpu_times(PyObject *self, PyObject *args); diff --git a/tests/test_bsd.py b/tests/test_bsd.py index 3556859e32..108181ce2b 100755 --- a/tests/test_bsd.py +++ b/tests/test_bsd.py @@ -246,6 +246,10 @@ def parse_swapinfo(): total, used, free = (int(p) * 1024 for p in parts[1:4]) return total, used, free + def test_cpu_count_cores(self): + cores = sysctl("kern.smp.cores") + assert psutil.cpu_count(logical=False) == cores + def test_cpu_frequency_against_sysctl(self): # Currently only cpu 0 is frequency is supported in FreeBSD # All other cores use the same frequency. From 6801f964a2045559aae0edb0f2453c3b673c386c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 03:18:32 +0100 Subject: [PATCH 1639/1714] FreeBSD: add test for cpu_times() --- tests/test_bsd.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_bsd.py b/tests/test_bsd.py index 108181ce2b..81e35d71be 100755 --- a/tests/test_bsd.py +++ b/tests/test_bsd.py @@ -10,6 +10,7 @@ """Tests specific to all BSD platforms.""" import datetime +import os import re import shutil import time @@ -250,6 +251,18 @@ def test_cpu_count_cores(self): cores = sysctl("kern.smp.cores") assert psutil.cpu_count(logical=False) == cores + @retry_on_failure() + def test_cpu_times(self): + clk_tck = os.sysconf("SC_CLK_TCK") + ticks = [int(x) for x in sysctl("kern.cp_time").split()] + ct = psutil.cpu_times() + tolerance = 0.5 + assert abs(ct.user - ticks[0] / clk_tck) < tolerance + assert abs(ct.nice - ticks[1] / clk_tck) < tolerance + assert abs(ct.system - ticks[2] / clk_tck) < tolerance + assert abs(ct.irq - ticks[3] / clk_tck) < tolerance + assert abs(ct.idle - ticks[4] / clk_tck) < tolerance + def test_cpu_frequency_against_sysctl(self): # Currently only cpu 0 is frequency is supported in FreeBSD # All other cores use the same frequency. From 87bde09cfdddabb0a2b8dbaea4376dcbc717a43c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 22 Mar 2026 15:30:26 +0100 Subject: [PATCH 1640/1714] Process.__repr__: cast status to str --- psutil/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/psutil/__init__.py b/psutil/__init__.py index ffae933add..4eee588942 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -404,15 +404,13 @@ def _get_ident(self): def __str__(self): info = {} info["pid"] = self.pid - if self._name: - info['name'] = self._name with self.oneshot(): if self._pid_reused: info["status"] = "terminated + PID reused" else: try: - info["name"] = self.name() - info["status"] = self.status() + info["name"] = self._name or self.name() + info["status"] = str(self.status()) except ZombieProcess: info["status"] = "zombie" except NoSuchProcess: From 4f1364a4682e1fb082eca21dbf430d0aed702bd3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 23 Mar 2026 10:47:21 +0100 Subject: [PATCH 1641/1714] `process_iter()`: pre-fetch Process methods + deprecate `.info` (#2786) ## The problem `process_iter(attrs=["name", "status"])` currently stores results in a `p.info` dict, creating two ways to access process attributes: `p.info["name"]` and `p.name()`. I never liked this. As the Python Zen says: > There should be one, and preferably only one, obvious way to do it ## The plan The goal is to make methods like `p.name()` return cached values directly when prefetched via `process_iter(attrs=...)`, eliminating the need for `p.info`. Before: ```python for p in psutil.process_iter(attrs=["name", "status"]): print(p.info["name"]) print(p.info["status"]) ``` After: ```python for p in psutil.process_iter(attrs=["name", "status"]): print(p.name()) # cached, no syscall, no exceptions print(p.status()) # cached, no syscall, no exceptions ``` This is easier to read and type, and removes the awkward dual model (methods vs info dict). `p.info` usage will still work (there's tons of code out there relying on it), but it will raise a `DeprecationWarning`. This is expected to be the most significant change in psutil 8.0.0. --- docs/api.rst | 52 +++++++++++++++------ docs/changelog.rst | 5 +++ docs/faq.rst | 13 +++--- docs/migration.rst | 54 +++++++++++++++++++++- docs/recipes.rst | 32 ++++++------- psutil/__init__.py | 90 ++++++++++++++++++++++++++++++++++--- scripts/cpu_distribution.py | 2 +- scripts/netstat.py | 2 +- scripts/pidof.py | 4 +- scripts/ps.py | 34 +++++++------- tests/__init__.py | 1 + tests/test_system.py | 77 ++++++++++++++++++++++++++++--- 12 files changed, 298 insertions(+), 68 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index a483ab67fc..e57ffa6862 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1034,33 +1034,43 @@ Functions Cache can optionally be cleared via ``process_iter.cache_clear()``. *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict`. - If *attrs* is specified :meth:`Process.as_dict` result will be stored as a - ``info`` attribute attached to the returned :class:`Process` instances. - If *attrs* is an empty list it will retrieve all process info (slow). + If *attrs* is specified, :meth:`Process.as_dict` is called internally and + the results are cached so that subsequent method calls (e.g. + :meth:`Process.name`, :meth:`Process.status`) return the cached values + instead of issuing new system calls. + If a method raises :exc:`AccessDenied` during pre-fetch, it will return + *ad_value* (default ``None``) instead of raising. If *attrs* is an empty list + it will retrieve all process info (slow). - Sorting order in which processes are returned is based on their PID. + Processes are returned sorted by PID. + + .. note:: + + Since :class:`Process` instances are reused across calls, a subsequent + :func:`process_iter` call will overwrite or clear any previously + pre-fetched values. Do not rely on cached values from a prior iteration. .. code-block:: pycon >>> import psutil >>> for proc in psutil.process_iter(['pid', 'name', 'username']): - ... print(proc.info) + ... print(proc.pid, proc.name(), proc.username()) ... - {'name': 'systemd', 'pid': 1, 'username': 'root'} - {'name': 'kthreadd', 'pid': 2, 'username': 'root'} - {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} + 1 systemd root + 2 kthreadd root + 3 ksoftirqd/0 root ... - A dict comprehensions to create a ``{pid: info, ...}`` data structure: + A dict comprehension to create a ``{pid: name, ...}`` data structure: .. code-block:: pycon >>> import psutil - >>> procs = {p.pid: p.info for p in psutil.process_iter(['name', 'username'])} + >>> procs = {p.pid: p.name() for p in psutil.process_iter(['name'])} >>> procs - {1: {'name': 'systemd', 'username': 'root'}, - 2: {'name': 'kthreadd', 'username': 'root'}, - 3: {'name': 'ksoftirqd/0', 'username': 'root'}, + {1: 'systemd', + 2: 'kthreadd', + 3: 'ksoftirqd/0', ...} Clear internal cache: @@ -1078,6 +1088,13 @@ Functions .. versionchanged:: 6.0.0 added ``psutil.process_iter.cache_clear()`` API. + .. versionchanged:: 8.0.0 + when *attrs* is specified, the pre-fetched values are cached + directly on the :class:`Process` instance so that subsequent + method calls (e.g. ``p.name()``, ``p.status()``) return the + cached values instead of making new system calls. The :attr:`Process.info` + dict is deprecated in favor of this new approach. + .. function:: pid_exists(pid) Check whether the given PID exists in the current process list. This is @@ -1283,6 +1300,15 @@ Process class The process PID. This is the only (read-only) attribute of the class. + .. attribute:: info + + A dict containing pre-fetched process info, set by + :func:`process_iter` when called with ``attrs``. Use method + calls instead (e.g. ``p.name()`` instead of ``p.info['name']``). + Accessing this attribute raises :exc:`DeprecationWarning`. + + .. deprecated:: 8.0.0 + .. method:: ppid() The process parent PID. On Windows the return value is cached after the diff --git a/docs/changelog.rst b/docs/changelog.rst index f33a424ee9..3aa15a918d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -139,6 +139,11 @@ Others ``wired`` fields. - :gh:`2780`, [Windows]: :func:`disk_usage` now can accept a file path (not only a directory path). +- :gh:`2784`: :func:`process_iter`: when *attrs* is specified, the pre-fetched + values are now cached directly on the :class:`Process` instance. Subsequent + method calls (e.g. ``p.name()``, ``p.status()``) return the cached values + instead of making new system calls. The ``p.info`` dict is deprecated. See + :ref:`migration guide `. **Bug fixes** diff --git a/docs/faq.rst b/docs/faq.rst index eab20d7dd2..00aa2df91f 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -40,16 +40,17 @@ You have two options to deal with it. except (psutil.AccessDenied, psutil.NoSuchProcess): pass -- Option 2: use :func:`process_iter` with a list of attribute names to pre-fetch. If - fetching an attribute raises :exc:`AccessDenied` internally, its value in - ``p.info`` is set to ``None`` (or to the ``ad_value`` argument, if specified): +- Option 2: use :func:`process_iter` with a list of attribute names to pre-fetch. + If fetching an attribute raises :exc:`AccessDenied` internally, the + corresponding method returns ``None`` (or the ``ad_value`` argument, if + specified): .. code-block:: python import psutil for p in psutil.process_iter(["name", "username"], ad_value="N/A"): - print(p.info["username"]) # may print "N/A" + print(p.username()) # may print "N/A" .. _faq_no_such_process: @@ -86,7 +87,7 @@ during iteration: import psutil for p in psutil.process_iter(["name"]): - print(p.info["name"]) + print(p.name()) If you have a specific PID (e.g. a known child process), wrap the call in a try/except: @@ -143,7 +144,7 @@ for a :term:`zombie process`. import psutil for p in psutil.process_iter(["status"]): - if p.info["status"] == psutil.STATUS_ZOMBIE: + if p.status() == psutil.STATUS_ZOMBIE: print(f"zombie: pid={p.pid}") **How to get rid of a zombie:** the only way is to have its parent diff --git a/docs/migration.rst b/docs/migration.rst index 5ea7200c1d..b81cf0cbb8 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -20,6 +20,58 @@ release and shows the code changes required to upgrade. Migrating to 8.0 ----------------- +process_iter(): p.info is deprecated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:func:`process_iter` now caches pre-fetched values internally, so they +can be accessed via normal method calls instead of the :attr:`Process.info` +dict. ``p.info`` still works but raises :exc:`DeprecationWarning`: + +.. code-block:: python + + # before + for p in psutil.process_iter(attrs=["name", "status"]): + print(p.info["name"], p.info["status"]) + + # after + for p in psutil.process_iter(attrs=["name", "status"]): + print(p.name(), p.status()) + +When ``attrs`` are specified, method calls return cached values +(no extra syscall), and :exc:`AccessDenied` / :exc:`ZombieProcess` +are handled transparently (returning the ``ad_value``, which defaults +to ``None``): + +.. code-block:: python + + # before + for p in psutil.process_iter(attrs=["exe"], ad_value="access-denied"): + print(p.info["exe"]) + + # after + for p in psutil.process_iter(attrs=["exe"], ad_value="access-denied"): + print(p.exe()) + +.. note:: + + This is a silent behavior change. Before, calling ``p.exe()`` + directly could raise :exc:`AccessDenied`. Now, if ``"exe"`` was + pre-fetched via ``attrs``, the same call returns ``ad_value`` + (default ``None``) instead. Code that relied on catching + exceptions will silently stop seeing them: + + .. code-block:: python + + # this no longer raises AccessDenied if "exe" was prefetched + for p in psutil.process_iter(attrs=["exe"]): + try: + print(p.exe()) + except psutil.AccessDenied: + pass # never reached + + If you need the exception, do not include the method in ``attrs``, + or call it on a fresh :class:`Process` instance. + Named tuple field order changed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -244,7 +296,7 @@ that a process object is still alive and refers to the same process, use for p in psutil.process_iter(["name"]): if p.is_running(): - print(p.pid, p.info["name"]) + print(p.pid, p.name()) ---- diff --git a/docs/recipes.rst b/docs/recipes.rst index 5dd8021582..39c5cd395e 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -26,7 +26,7 @@ Find process by name: def find_procs_by_name(name): ls = [] for p in psutil.process_iter(["name"]): - if p.info["name"] == name: + if p.name() == name: ls.append(p) return ls @@ -44,9 +44,9 @@ A bit more advanced, check string against :meth:`Process.name`, ls = [] for p in psutil.process_iter(["name", "exe", "cmdline"]): if ( - name == p.info["name"] - or (p.info["exe"] and os.path.basename(p.info["exe"]) == name) - or (p.info["cmdline"] and p.info["cmdline"][0] == name) + name == p.name() + or (p.exe() and os.path.basename(p.exe()) == name) + or (p.cmdline() and p.cmdline()[0] == name) ): ls.append(p) return ls @@ -103,7 +103,7 @@ Find all processes that have a given file open (useful on Windows): def find_procs_using_file(path): ls = [] for p in psutil.process_iter(["open_files"]): - for f in p.info["open_files"] or []: + for f in p.open_files() or []: if f.path == path: ls.append(p) break @@ -119,7 +119,7 @@ Processes owned by user: >>> import getpass >>> import psutil >>> from pprint import pprint as pp - >>> pp([(p.pid, p.info["name"]) for p in psutil.process_iter(["name", "username"]) if p.info["username"] == getpass.getuser()]) + >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "username"]) if p.username() == getpass.getuser()]) (16832, 'bash'), (19772, 'ssh'), (20492, 'python')] @@ -130,10 +130,10 @@ Processes actively running: .. code-block:: python - >>> pp([(p.pid, p.info) for p in psutil.process_iter(["name", "status"]) if p.info["status"] == psutil.STATUS_RUNNING]) - [(1150, {'name': 'Xorg', 'status': }), - (1776, {'name': 'unity-panel-service', 'status': }), - (20492, {'name': 'python', 'status': })] + >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "status"]) if p.status() == psutil.STATUS_RUNNING]) + [(1150, 'Xorg'), + (1776, 'unity-panel-service'), + (20492, 'python')] ---- @@ -142,9 +142,9 @@ Processes using log files: .. code-block:: python >>> for p in psutil.process_iter(["name", "open_files"]): - ... for file in p.info["open_files"] or []: + ... for file in p.open_files() or []: ... if file.path.endswith(".log"): - ... print("{:<5} {:<10} {}".format(p.pid, p.info["name"][:10], file.path)) + ... print("{:<5} {:<10} {}".format(p.pid, p.name()[:10], file.path)) ... 1510 upstart /home/giampaolo/.cache/upstart/unity-settings-daemon.log 2174 nautilus /home/giampaolo/.local/share/gvfs-metadata/home-ce08efac.log @@ -156,7 +156,7 @@ Processes consuming more than 500M of memory: .. code-block:: python - >>> pp([(p.pid, p.info["name"], p.info["memory_info"].rss) for p in psutil.process_iter(["name", "memory_info"]) if p.info["memory_info"].rss > 500 * 1024 * 1024]) + >>> pp([(p.pid, p.name(), p.memory_info().rss) for p in psutil.process_iter(["name", "memory_info"]) if p.memory_info().rss > 500 * 1024 * 1024]) [(2650, 'chrome', 532324352), (3038, 'chrome', 1120088064), (21915, 'sublime_text', 615407616)] @@ -167,7 +167,7 @@ Top 3 processes which consumed the most CPU time: .. code-block:: python - >>> pp([(p.pid, p.info["name"], sum(p.info["cpu_times"])) for p in sorted(psutil.process_iter(["name", "cpu_times"]), key=lambda p: sum(p.info["cpu_times"][:2]))][-3:]) + >>> pp([(p.pid, p.name(), sum(p.cpu_times())) for p in sorted(psutil.process_iter(["name", "cpu_times"]), key=lambda p: sum(p.cpu_times()[:2]))][-3:]) [(2721, 'chrome', 10219.73), (1150, 'Xorg', 11116.989999999998), (2650, 'chrome', 18451.97)] @@ -288,7 +288,7 @@ Find zombie (defunct) processes: import psutil for p in psutil.process_iter(["status"]): - if p.info["status"] == psutil.STATUS_ZOMBIE: + if p.status() == psutil.STATUS_ZOMBIE: print(f"zombie: pid={p.pid}") ---- @@ -301,7 +301,7 @@ Terminate all processes matching a given name: def terminate_procs_by_name(name): for p in psutil.process_iter(["name"]): - if p.info["name"] == name: + if p.name() == name: p.terminate() ---- diff --git a/psutil/__init__.py b/psutil/__init__.py index 4eee588942..54fa67f1ff 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -288,6 +288,28 @@ def _check_conn_kind(kind): # ===================================================================== +def _use_prefetch(method): + """Decorator returning cached values from process_iter(attrs=...). + + When process_iter() is called with an *attrs* argument, it + pre-fetches the requested attributes via as_dict() and stores + them in Process._prefetch. This decorator makes the decorated + method return the cached value (if present) instead of issuing + a new system call. + """ + + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + if not args and not kwargs: + try: + return self._prefetch[method.__name__] + except KeyError: + pass + return method(self, *args, **kwargs) + + return wrapper + + class Process: """Represents an OS process with the given PID. If PID is omitted current process PID (os.getpid()) is used. @@ -348,6 +370,7 @@ def _init(self, pid, _ignore_nsp=False): self._last_sys_cpu_times = None self._last_proc_cpu_times = None self._exitcode = _SENTINEL + self._prefetch = {} self._ident = (self.pid, None) try: self._ident = self._get_ident() @@ -477,6 +500,22 @@ def pid(self) -> int: """The process PID.""" return self._pid + # DEPRECATED + @property + def info(self) -> dict: + """Return pre-fetched process_iter() info dict. + + Deprecated: use method calls instead (e.g. p.name()). + """ + msg = ( + "Process.info is deprecated; use method calls instead" + " (e.g. p.name() instead of p.info['name'])" + ) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + # Return a copy to prevent the user from mutating the dict and + # corrupting the prefetch cache. + return self._prefetch.copy() + # --- utility methods @contextlib.contextmanager @@ -657,11 +696,13 @@ def is_running(self) -> bool: # --- actual API + @_use_prefetch @memoize_when_activated def ppid(self) -> int: """The process parent PID. On Windows the return value is cached after first call. """ + # On POSIX we don't want to cache the ppid as it may unexpectedly # change to 1 (init) in case this process turns into a zombie: # https://github.com/giampaolo/psutil/issues/321 @@ -676,8 +717,10 @@ def ppid(self) -> int: self._ppid = self._ppid or self._proc.ppid() return self._ppid + @_use_prefetch def name(self) -> str: """The process name. The return value is cached after first call.""" + # Process name is only cached on Windows as on POSIX it may # change, see: # https://github.com/giampaolo/psutil/issues/692 @@ -707,6 +750,7 @@ def name(self) -> str: self._proc._name = name return name + @_use_prefetch def exe(self) -> str: """The process executable as an absolute path. May also be an empty string. @@ -749,10 +793,12 @@ def guess_it(fallback): self._exe = exe return self._exe + @_use_prefetch def cmdline(self) -> list[str]: """The command line this process has been called with.""" return self._proc.cmdline() + @_use_prefetch def status(self) -> ProcessStatus | str: """The process current status as a STATUS_* constant.""" try: @@ -760,10 +806,12 @@ def status(self) -> ProcessStatus | str: except ZombieProcess: return ProcessStatus.STATUS_ZOMBIE + @_use_prefetch def username(self) -> str: """The name of the user that owns the process. On UNIX this is calculated by using *real* process uid. """ + if POSIX: if pwd is None: # might happen if python was installed from sources @@ -778,6 +826,7 @@ def username(self) -> str: else: return self._proc.username() + @_use_prefetch def create_time(self) -> float: """The process creation time as a floating point number expressed in seconds since the epoch (seconds since January 1, @@ -790,10 +839,12 @@ def create_time(self) -> float: self._create_time = self._proc.create_time() return self._create_time + @_use_prefetch def cwd(self) -> str: """Process current working directory as an absolute path.""" return self._proc.cwd() + @_use_prefetch def nice(self, value: int | None = None) -> int | None: """Get or set process niceness (priority).""" if value is None: @@ -804,6 +855,7 @@ def nice(self, value: int | None = None) -> int | None: if POSIX: + @_use_prefetch @memoize_when_activated def uids(self) -> puids: """Return process UIDs as a (real, effective, saved) @@ -811,18 +863,21 @@ def uids(self) -> puids: """ return self._proc.uids() + @_use_prefetch def gids(self) -> pgids: """Return process GIDs as a (real, effective, saved) named tuple. """ return self._proc.gids() + @_use_prefetch def terminal(self) -> str | None: """The terminal associated with this process, if any, else None. """ return self._proc.terminal() + @_use_prefetch def num_fds(self) -> int: """Return the number of file descriptors opened by this process (POSIX only). @@ -832,6 +887,7 @@ def num_fds(self) -> int: # Linux, BSD, AIX and Windows only if hasattr(_psplatform.Process, "io_counters"): + @_use_prefetch def io_counters(self) -> pio: """Return process I/O statistics as a (read_count, write_count, read_bytes, write_bytes) @@ -844,6 +900,7 @@ def io_counters(self) -> pio: # Linux and Windows if hasattr(_psplatform.Process, "ionice_get"): + @_use_prefetch def ionice( self, ioclass: int | None = None, value: int | None = None ) -> pionice | ProcessIOPriority | None: @@ -891,6 +948,7 @@ def rlimit( # Windows, Linux and FreeBSD only if hasattr(_psplatform.Process, "cpu_affinity_get"): + @_use_prefetch def cpu_affinity( self, cpus: list[int] | None = None ) -> list[int] | None: @@ -915,6 +973,7 @@ def cpu_affinity( # Linux, FreeBSD, SunOS if hasattr(_psplatform.Process, "cpu_num"): + @_use_prefetch def cpu_num(self) -> int: """Return what CPU this process is currently running on. The returned number should be <= psutil.cpu_count() @@ -928,6 +987,7 @@ def cpu_num(self) -> int: # All platforms has it, but maybe not in the future. if hasattr(_psplatform.Process, "environ"): + @_use_prefetch def environ(self) -> dict[str, str]: """The environment variables of the process as a dict. Note: this might not reflect changes made after the process started. @@ -936,24 +996,28 @@ def environ(self) -> dict[str, str]: if WINDOWS: + @_use_prefetch def num_handles(self) -> int: """Return the number of handles opened by this process (Windows only). """ return self._proc.num_handles() + @_use_prefetch def num_ctx_switches(self) -> pctxsw: """Return the number of voluntary and involuntary context switches performed by this process. """ return self._proc.num_ctx_switches() + @_use_prefetch def num_threads(self) -> int: """Return the number of threads used by this process.""" return self._proc.num_threads() if hasattr(_psplatform.Process, "threads"): + @_use_prefetch def threads(self) -> list[pthread]: """Return threads opened by process as a list of (id, user_time, system_time) named tuples representing @@ -1036,6 +1100,7 @@ def children(self, recursive: bool = False) -> list[Process]: pass return ret + @_use_prefetch def cpu_percent(self, interval: float | None = None) -> float: """Return a float representing the current process CPU utilization as a percentage. @@ -1071,6 +1136,7 @@ def cpu_percent(self, interval: float | None = None) -> float: 2.9 >>> """ + blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: msg = f"interval is not positive (got {interval!r})" @@ -1129,6 +1195,7 @@ def timer(): single_cpu_percent = overall_cpus_percent * num_cpus return round(single_cpu_percent, 1) + @_use_prefetch @memoize_when_activated def cpu_times(self) -> pcputimes: """Return a (user, system, children_user, children_system) @@ -1140,6 +1207,7 @@ def cpu_times(self) -> pcputimes: """ return self._proc.cpu_times() + @_use_prefetch @memoize_when_activated def memory_info(self) -> pmem: """Return a named tuple with variable fields depending on the @@ -1151,6 +1219,7 @@ def memory_info(self) -> pmem: """ return self._proc.memory_info() + @_use_prefetch @memoize_when_activated def memory_info_ex(self) -> pmem_ex: """Return a named tuple extending memory_info() with extra @@ -1167,6 +1236,7 @@ def memory_info_ex(self) -> pmem_ex: # Linux, macOS, Windows if hasattr(_psplatform.Process, "memory_footprint"): + @_use_prefetch def memory_footprint(self) -> pfootprint: """Return a named tuple with USS, PSS and swap memory metrics. These provide a better representation of @@ -1209,6 +1279,7 @@ def memory_percent(self, memtype: str = "rss") -> float: >>> psutil.Process().memory_info()._fields ('rss', 'vms', 'shared', 'text', 'lib', 'data', 'dirty', 'uss', 'pss') """ + valid_types = list(_ntp.pmem._fields) if hasattr(_ntp, "pmem_ex"): valid_types += [ @@ -1248,6 +1319,7 @@ def memory_percent(self, memtype: str = "rss") -> float: if hasattr(_psplatform.Process, "memory_maps"): + @_use_prefetch def memory_maps( self, grouped: bool = True ) -> list[pmmap_grouped] | list[pmmap_ext]: @@ -1261,6 +1333,7 @@ def memory_maps( entity and the named tuple will also include the mapped region's address space ('addr') and permission set ('perms'). """ + it = self._proc.memory_maps() if grouped: d = {} @@ -1275,6 +1348,7 @@ def memory_maps( else: return [_ntp.pmmap_ext(*x) for x in it] + @_use_prefetch def page_faults(self) -> ppagefaults: """Return the number of page faults for this process as a (minor, major) named tuple. @@ -1293,6 +1367,7 @@ def page_faults(self) -> ppagefaults: """ return self._proc.page_faults() + @_use_prefetch def open_files(self) -> list[popenfile]: """Return files opened by process as a list of (path, fd) named tuples including the absolute file name @@ -1300,6 +1375,7 @@ def open_files(self) -> list[popenfile]: """ return self._proc.open_files() + @_use_prefetch def net_connections(self, kind: str = "inet") -> list[pconn]: """Return socket connections opened by process as a list of (fd, family, type, laddr, raddr, status) named tuples. @@ -1462,7 +1538,7 @@ def wait(self, timeout: float | None = None) -> int | None: 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', - 'connections', 'memory_full_info', 'oneshot'} + 'connections', 'memory_full_info', 'oneshot', 'info'} } # fmt: on @@ -1604,9 +1680,10 @@ def process_iter( their PIDs. *attrs* and *ad_value* have the same meaning as in - Process.as_dict(). If *attrs* is specified as_dict() is called - and the resulting dict is stored as a 'info' attribute attached - to returned Process instance. + Process.as_dict(). If *attrs* is specified, as_dict() is called and + the results are cached so that subsequent method calls (e.g. + p.name()) return cached values. + If *attrs* is an empty list it will retrieve all process info (slow). """ @@ -1637,8 +1714,11 @@ def remove(pid): try: if proc is None: # new process proc = add(pid) + proc._prefetch = {} # clear cache if attrs is not None: - proc.info = proc.as_dict(attrs=attrs, ad_value=ad_value) + proc._prefetch = proc.as_dict( + attrs=attrs, ad_value=ad_value + ) yield proc except NoSuchProcess: remove(pid) diff --git a/scripts/cpu_distribution.py b/scripts/cpu_distribution.py index 59109f5278..b462033f9e 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -82,7 +82,7 @@ def main(): # processes procs = collections.defaultdict(list) for p in psutil.process_iter(['name', 'cpu_num']): - procs[p.info['cpu_num']].append(p.info['name'][:5]) + procs[p.cpu_num()].append(p.name()[:5]) curr_line = 3 while True: diff --git a/scripts/netstat.py b/scripts/netstat.py index 7b3efcba29..9f26ee0790 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -48,7 +48,7 @@ def main(): print(header) proc_names = {} for p in psutil.process_iter(['pid', 'name']): - proc_names[p.info['pid']] = p.info['name'] + proc_names[p.pid] = p.name() for c in psutil.net_connections(kind='inet'): laddr = f"{c.laddr[0]}:{c.laddr[1]}" raddr = "" diff --git a/scripts/pidof.py b/scripts/pidof.py index 274541dc6d..2353126621 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -21,8 +21,8 @@ def pidof(pgname): return [ str(proc.pid) for proc in psutil.process_iter(['name', 'cmdline']) - if proc.info["name"] == pgname - or (proc.info["cmdline"] and proc.info["cmdline"][0] == pgname) + if proc.name() == pgname + or (proc.cmdline() and proc.cmdline()[0] == pgname) ] diff --git a/scripts/ps.py b/scripts/ps.py index 585eca2f46..8ba464d998 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -49,22 +49,22 @@ def main(): "STATUS", "START", "TIME", "CMDLINE")) # fmt: on for p in psutil.process_iter(attrs, ad_value=None): - if p.info['create_time']: - ctime = datetime.datetime.fromtimestamp(p.info['create_time']) + if p.create_time(): + ctime = datetime.datetime.fromtimestamp(p.create_time()) if ctime.date() == today_day: ctime = ctime.strftime("%H:%M") else: ctime = ctime.strftime("%b%d") else: ctime = '' - if p.info['cpu_times']: + if p.cpu_times(): cputime = time.strftime( - "%M:%S", time.localtime(sum(p.info['cpu_times'])) + "%M:%S", time.localtime(sum(p.cpu_times())) ) else: cputime = '' - user = p.info['username'] + user = p.username() if not user and psutil.POSIX: try: user = p.uids()[0] @@ -76,30 +76,30 @@ def main(): user = '' user = user[:9] vms = ( - bytes2human(p.info['memory_info'].vms) - if p.info['memory_info'] is not None + bytes2human(p.memory_info().vms) + if p.memory_info() is not None else '' ) rss = ( - bytes2human(p.info['memory_info'].rss) - if p.info['memory_info'] is not None + bytes2human(p.memory_info().rss) + if p.memory_info() is not None else '' ) memp = ( - round(p.info['memory_percent'], 1) - if p.info['memory_percent'] is not None + round(p.memory_percent(), 1) + if p.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']) + nice = int(p.nice()) if p.nice() else '' + if p.cmdline(): # noqa: SIM108 + cmdline = ' '.join(p.cmdline()) else: - cmdline = p.info['name'] - status = p.info['status'][:5] if p.info['status'] else '' + cmdline = p.name() + status = p.status()[:5] if p.status() else '' line = templ.format( user, - p.info['pid'], + p.pid, memp, vms, rss, diff --git a/tests/__init__.py b/tests/__init__.py index b62db32689..af3b4947fd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1121,6 +1121,7 @@ class process_namespace: ('as_dict', (), {}), ('children', (), {'recursive': True}), ('connections', (), {}), # deprecated + ('info', (), {}), ('is_running', (), {}), ('memory_full_info', (), {}), # deprecated ('oneshot', (), {}), diff --git a/tests/test_system.py b/tests/test_system.py index 254d18d91d..a8c678ca79 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -16,6 +16,7 @@ import socket import sys import time +import warnings from unittest import mock import psutil @@ -95,10 +96,10 @@ def test_emulate_access_denied(self): def test_attrs(self): for p in psutil.process_iter(attrs=['pid']): - assert list(p.info.keys()) == ['pid'] + assert list(p._prefetch.keys()) == ['pid'] # yield again for p in psutil.process_iter(attrs=['pid']): - assert list(p.info.keys()) == ['pid'] + assert list(p._prefetch.keys()) == ['pid'] with pytest.raises(ValueError): list(psutil.process_iter(attrs=['foo'])) with mock.patch( @@ -106,8 +107,8 @@ def test_attrs(self): side_effect=psutil.AccessDenied(0, ""), ) as m: for p in psutil.process_iter(attrs=["pid", "cpu_times"]): - assert p.info['cpu_times'] is None - assert p.info['pid'] >= 0 + assert p._prefetch['cpu_times'] is None + assert p._prefetch['pid'] >= 0 assert m.called with mock.patch( "psutil._psplatform.Process.cpu_times", @@ -117,10 +118,74 @@ def test_attrs(self): for p in psutil.process_iter( attrs=["pid", "cpu_times"], ad_value=flag ): - assert p.info['cpu_times'] is flag - assert p.info['pid'] >= 0 + assert p._prefetch['cpu_times'] is flag + assert p._prefetch['pid'] >= 0 assert m.called + def test_prefetch(self): + for p in psutil.process_iter(attrs=["name", "status"]): + assert p.name() == p._prefetch["name"] + assert p.status() == p._prefetch["status"] + + def test_info_deprecation(self): + for p in psutil.process_iter(attrs=["name"]): + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + p.info # noqa: B018 + assert len(ws) == 1 + assert issubclass(ws[0].category, DeprecationWarning) + assert "deprecated" in str(ws[0].message) + break + + def test_prefetch_ad_value(self): + with mock.patch( + "psutil._psplatform.Process.cpu_times", + side_effect=psutil.AccessDenied(0, ""), + ): + for p in psutil.process_iter(attrs=["cpu_times"]): + assert p.cpu_times() is None + + def test_prefetch_cleared(self): + # Prefetch cache is cleared when attrs is not specified. + for p in psutil.process_iter(attrs=["name"]): + assert p._prefetch + for p in psutil.process_iter(): + assert not p._prefetch + + def test_prefetch_with_args_bypasses_cache(self): + # Methods called with args should bypass the prefetch cache + # (e.g. nice(10) is a setter, not a getter). + p = psutil.Process() + p._prefetch["nice"] = -999 + # Without args: returns cached value. + assert p.nice() == -999 + # With args: bypasses cache, calls real method. + real_nice = psutil.Process().nice() + assert p.nice(real_nice) is None + assert p.nice() == -999 + + def test_prefetch_double_call(self): + # Multiple calls should return the same cached value. + for p in psutil.process_iter(attrs=["name"]): + name1 = p.name() + name2 = p.name() + assert name1 == name2 + assert name1 == p._prefetch["name"] + break + + def test_prefetch_empty_attrs(self): + # attrs=[] should prefetch all methods. + p = next(psutil.process_iter(attrs=[])) + assert p._prefetch.keys() == psutil._as_dict_attrnames + + def test_prefetch_with_non_prefetched(self): + # A method not in attrs should still do a live syscall. + for p in psutil.process_iter(attrs=["name"]): + assert "status" not in p._prefetch + # status() should still work via syscall + assert p.status() + break + def test_cache_clear(self): list(psutil.process_iter()) # populate cache assert psutil._pmap From bf119c83b52dc07dd4f0b832c8944595028cbcb3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 23 Mar 2026 19:53:22 +0100 Subject: [PATCH 1642/1714] Docs: add a /performance section (#2787) --- MANIFEST.in | 1 + docs/api.rst | 47 +++--- docs/changelog.rst | 47 +++--- docs/index.rst | 21 ++- docs/performance.rst | 228 ++++++++++++++++++++++++++++ psutil/__init__.py | 5 - scripts/internal/bench_oneshot.py | 85 +++++++---- scripts/internal/bench_oneshot_2.py | 8 +- scripts/internal/print_api_speed.py | 55 +++---- scripts/internal/print_sysinfo.py | 6 +- tests/test_scripts.py | 49 +++--- 11 files changed, 411 insertions(+), 141 deletions(-) create mode 100644 docs/performance.rst diff --git a/MANIFEST.in b/MANIFEST.in index 2d4d2a1b8e..2a6010b596 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -36,6 +36,7 @@ include docs/glossary.rst include docs/index.rst include docs/install.rst include docs/migration.rst +include docs/performance.rst include docs/platform.rst include docs/recipes.rst include docs/requirements.txt diff --git a/docs/api.rst b/docs/api.rst index e57ffa6862..cc0f6c8d17 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1010,7 +1010,7 @@ Functions Return a sorted list of current running PIDs. To iterate over all processes and avoid race conditions :func:`process_iter` - should be preferred. + should be preferred, see :ref:`performance section `. .. code-block:: pycon @@ -1044,6 +1044,8 @@ Functions Processes are returned sorted by PID. + .. seealso:: :ref:`perf-process-iter` + .. note:: Since :class:`Process` instances are reused across calls, a subsequent @@ -1218,19 +1220,23 @@ Process class .. method:: oneshot() Utility context manager which considerably speeds up the retrieval of - multiple process information at the same time. + multiple process attributes at the same time. Internally different process info (e.g. :meth:`name`, :meth:`ppid`, :meth:`uids`, :meth:`create_time`, ...) may be fetched by using the same routine, but only one value is returned and the others are discarded. When using this context manager the internal routine is executed once (in - the example below on :meth:`name`) the value of interest is returned and + the example below on :meth:`name`); the value of interest is returned and the others are cached. The subsequent calls sharing the same internal routine will return the cached value. The cache is cleared when exiting the context manager block. - The advice is to use this every time you retrieve more than one information + The advice is to use this every time you retrieve more than one attribute about the process. If you're lucky, you'll get a hell of a speedup. + .. seealso:: + - :ref:`perf-oneshot` + - :ref:`perf-oneshot-bench` + .. code-block:: pycon >>> import psutil @@ -1247,10 +1253,13 @@ Process class Here's a list of methods which can take advantage of the speedup depending on what platform you're on. - In the table below horizontal empty rows indicate what process methods can - be efficiently grouped together internally. + Empty rows indicate methods that are grouped together internally (i.e. + they share the same system call). The last column (speedup) shows an approximation of the speedup you can get - if you call all the methods together (best case scenario). + if you call all the methods together (best case scenario). It was + calculated via + `bench_oneshot.py `_ + script. +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | Linux | Windows | macOS | BSD | SunOS | AIX | @@ -1265,33 +1274,35 @@ Process class +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`name` | :meth:`memory_info_ex` | :meth:`num_ctx_switches` | :meth:`gids` | :meth:`memory_info` | :meth:`memory_info` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`ppid` | :meth:`num_ctx_switches` | :meth:`num_threads` | :meth:`io_counters` | :meth:`memory_percent` | :meth:`memory_percent` | + | :meth:`page_faults` | :meth:`num_ctx_switches` | :meth:`num_threads` | :meth:`io_counters` | :meth:`memory_percent` | :meth:`memory_percent` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`ppid` | :meth:`num_handles` | | :meth:`name` | :meth:`num_threads` | :meth:`num_threads` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`status` | :meth:`num_handles` | | :meth:`name` | :meth:`num_threads` | :meth:`num_threads` | + | :meth:`status` | :meth:`num_threads` | :meth:`create_time` | :meth:`memory_info` | :meth:`ppid` | :meth:`ppid` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`terminal` | :meth:`num_threads` | :meth:`create_time` | :meth:`memory_info` | :meth:`ppid` | :meth:`ppid` | + | :meth:`terminal` | | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | | | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | + | | :meth:`exe` | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`gids` | :meth:`exe` | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | + | :meth:`gids` | :meth:`name` | :meth:`ppid` | :meth:`ppid` | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`memory_info_ex` | :meth:`name` | :meth:`ppid` | :meth:`ppid` | | | + | :meth:`memory_info_ex` | | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_ctx_switches` | | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | + | :meth:`num_ctx_switches` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`num_threads` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`uids` | | :meth:`terminal` | :meth:`terminal` | :meth:`uids` | :meth:`uids` | + | :meth:`uids` | | :meth:`uids` | :meth:`uids` | :meth:`username` | :meth:`username` | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`username` | | :meth:`uids` | :meth:`uids` | :meth:`username` | :meth:`username` | + | :meth:`username` | | :meth:`username` | :meth:`username` | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | | | :meth:`username` | :meth:`username` | | | + | | | | | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`memory_footprint` | | | | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ | :meth:`memory_maps` | | | | | | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | *speedup: +2.6x* | *speedup: +1.8x / +6.5x* | *speedup: +1.9x* | *speedup: +2.0x* | *speedup: +1.3x* | *speedup: +1.3x* | + | *speedup: +1.8x* | *speedup: +1.8x / +6.5x* | *speedup: +1.9x* | *speedup: +2.0x* | *speedup: +1.3x* | *speedup: +1.3x* | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ .. versionadded:: 5.0.0 diff --git a/docs/changelog.rst b/docs/changelog.rst index 3aa15a918d..5f9ec9ce3a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,58 +6,45 @@ Changelog 8.0.0 (IN DEVELOPMENT) ^^^^^^^^^^^^^^^^^^^^^^ -**Compatibility notes** - - psutil 8.0 introduces breaking API changes and drops support for Python 3.6. - See the :ref:`migration guide ` if upgrading from 7.x. +.. note:: + psutil 8.0 introduces breaking API changes. See the + :ref:`migration guide ` if upgrading from 7.x. **Enhancements** Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, -:gh:`2775`, :gh:`2781`) +:gh:`2775`, :gh:`2781`, :gh:`2787`) - Split docs from a single HTML file into multiple sections (API reference, install, etc.). - Added new sections: - - `/recipes `__: - show code samples - - `/adoption `__: - notable software using psutil - - `/glossary `__: - a section explaining the core concepts - - `/shell-equivalents `__: - maps each psutil API to native CLI commands - - `/stdlib-equivalents `__: - maps psutil's Python API to the closest equivalent in the Python standard - library. - - `/install `__ - (was old ``INSTALL.rst`` in root dir) - - `/credits `__: - list contributors and donors (was old ``CREDITS`` in root dir) - - `/platform `__: - summary of OSes and architectures support - - `/faq `__: - extended FAQ section. - - `/alternatives `__: - list of alternative Python libraries and tools that overlap with psutil. - - `/migration `__: a - section explaining how to migrate to newer psutil versions that break - backward compatibility. + - :doc:`/adoption `: notable software using psutil + - :doc:`/alternatives `: list of alternative Python libraries and tools that overlap with psutil. + - :doc:`/credits `: list contributors and donors (was old ``CREDITS`` in root dir) + - :doc:`/faq `: extended FAQ section + - :doc:`/glossary `: core concepts explained + - :doc:`/install `: (was old ``INSTALL.rst`` in root dir) + - :doc:`/migration `: explain how to migrate to newer psutil versions that break backward compatibility + - :doc:`/performance `: how to use psutil efficiently + - :doc:`/platform `: summary of OSes and architectures support + - :doc:`/recipes `: code samples + - :doc:`/shell-equivalents `: maps each psutil API to native CLI commands + - :doc:`/stdlib-equivalents `: maps psutil's Python API to the closest equivalent in the Python standard library - Usability: - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. - - Use sphinx extension to validate Python code snippets syntax at build-time. - Testing: - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting. - Add custom script to detect dead reference links in ``.rst`` files. - Build doc as part of CI process. + - Use sphinx extension to validate Python code snippets syntax at build-time. - Greatly improved :func:`virtual_memory` doc. diff --git a/docs/index.rst b/docs/index.rst index 9e585d68ae..5e6a6f10eb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -95,19 +95,36 @@ Table of contents .. toctree:: :maxdepth: 2 + :caption: User guide Install API Reference + Performance FAQ Recipes Shell equivalents Stdlib equivalents + +.. toctree:: + :maxdepth: 2 + :caption: Reference + Glossary Platform support - Who uses psutil - Alternatives + +.. toctree:: + :maxdepth: 2 + :caption: Development + Migration Development guide + +.. toctree:: + :maxdepth: 2 + :caption: About + + Who uses psutil + Alternatives Credits Timeline diff --git a/docs/performance.rst b/docs/performance.rst new file mode 100644 index 0000000000..7d9e36decc --- /dev/null +++ b/docs/performance.rst @@ -0,0 +1,228 @@ +.. currentmodule:: psutil + +Performance +=========== + +This page describes how to use psutil efficiently. + +.. _perf-oneshot: + +Use oneshot() when reading multiple process attributes +------------------------------------------------------ + +If you're dealing with a single :class:`Process` instance and need to retrieve +multiple process attributes, use :meth:`Process.oneshot`. Each method call +issues a separate system call, but the OS often returns multiple attributes at +once, which :meth:`Process.oneshot` caches for subsequent calls. + +Slow: + +.. code-block:: python + + import psutil + + p = psutil.Process() + p.name() # syscall + p.cpu_times() # syscall + p.memory_info() # syscall + p.status() # syscall + +Fast: + +.. code-block:: python + + import psutil + + p = psutil.Process() + with p.oneshot(): + p.name() # one syscall, result cached + p.cpu_times() # from cache + p.memory_info() # from cache + p.status() # from cache + +The speed improvement depends on the platform and on how many attributes +you read. On Linux the gain is typically around 1.5x–2x; on Windows it can +be much higher. As a rule of thumb: if you read more than two attributes +from the same process, use :meth:`Process.oneshot`. + +.. _perf-process-iter: + +Use process_iter() with an attrs list +-------------------------------------- + +If you iterate over multiple PIDs, use :func:`process_iter`. +It accepts an ``attrs`` argument that pre-fetches only the requested attributes +in a single pass, minimizing system calls by fetching multiple attributes at once. +This is faster than calling individual methods in a loop. + +Slow: + +.. code-block:: python + + import psutil + + for p in psutil.process_iter(): + try: + print(p.pid, p.name(), p.status()) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + +Fast: + +.. code-block:: python + + import psutil + + for p in psutil.process_iter(["name", "status"]): + print(p.pid, p.name(), p.status()) + +:func:`process_iter(attrs=...) ` is effectively equivalent +to using :meth:`Process.oneshot` on each process. + +Using :func:`process_iter` also saves you from race conditions (e.g. +if a process disappears while iterating), since attributes are retrieved in a +single pass and exceptions like :exc:`NoSuchProcess` and :exc:`AccessDenied` +are handled internally. + +.. _perf-pids: + +Avoid pids() + loop +--------------------- + +A common but inefficient pattern is to call :func:`pids` and then +construct a :class:`Process` for each PID manually: + +.. code-block:: python + + import psutil + + for pid in psutil.pids(): + try: + p = psutil.Process(pid) + print(p.name()) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + +Prefer :func:`process_iter` instead. It supports the ``attrs`` pre-fetch, +avoids race conditions, and caches :class:`Process` instances internally: + +.. code-block:: python + + import psutil + + for p in psutil.process_iter(["name"]): + print(p.pid, p.name()) + +.. _perf-oneshot-bench: + +Measuring oneshot() speedup +--------------------------- + +`bench_oneshot.py `_ +script measures :meth:`Process.oneshot` speedup. E.g. on Linux: + +.. code-block:: + + $ python3 scripts/internal/bench_oneshot.py --times 10000 + 17 methods pre-fetched by oneshot() on platform 'linux' (10,000 times, psutil 8.0.0): + + cpu_num + cpu_percent + cpu_times + gids + memory_info + memory_info_ex + memory_percent + name + num_ctx_switches + num_threads + page_faults + parent + ppid + status + terminal + uids + username + + regular: 2.766 secs + oneshot: 1.537 secs + speedup: +1.80x + +This also shows which APIs share the same internal kernel routines. + +.. _perf-api-speed: + +Measuring APIs speed +-------------------- + +`print_api_speed.py `_ +script shows the relative cost of each API call. +This helps you understand which operations are more expensive. +E.g. on Linux: + +.. code-block:: + + $ python3 scripts/internal/print_api_speed.py + SYSTEM APIS NUM CALLS SECONDS + ------------------------------------------------- + getloadavg 300 0.00013 + heap_trim 300 0.00027 + heap_info 300 0.00028 + cpu_count 300 0.00066 + disk_usage 300 0.00071 + pid_exists 300 0.00249 + users 300 0.00394 + cpu_times 300 0.00647 + virtual_memory 300 0.00648 + boot_time 300 0.00727 + cpu_percent 300 0.00745 + net_io_counters 300 0.00754 + cpu_times_percent 300 0.00870 + net_if_addrs 300 0.01156 + cpu_stats 300 0.01195 + swap_memory 300 0.01292 + net_if_stats 300 0.01360 + disk_partitions 300 0.01696 + disk_io_counters 300 0.02583 + sensors_battery 300 0.03103 + pids 300 0.04896 + cpu_count (cores) 300 0.07208 + process_iter (all) 300 0.07900 + cpu_freq 300 0.15635 + sensors_fans 300 0.75810 + net_connections 224 2.00111 + sensors_temperatures 81 2.00266 + + PROCESS APIS NUM CALLS SECONDS + ------------------------------------------------- + create_time 300 0.00013 + exe 300 0.00016 + nice 300 0.00024 + ionice 300 0.00039 + cwd 300 0.00052 + cpu_affinity 300 0.00057 + num_fds 300 0.00100 + memory_info 300 0.00208 + io_counters 300 0.00229 + cmdline 300 0.00232 + cpu_num 300 0.00254 + terminal 300 0.00255 + status 300 0.00258 + page_faults 300 0.00259 + name 300 0.00261 + memory_percent 300 0.00265 + cpu_times 300 0.00278 + threads 300 0.00300 + gids 300 0.00304 + num_threads 300 0.00305 + num_ctx_switches 300 0.00308 + uids 300 0.00321 + cpu_percent 300 0.00372 + net_connections 300 0.00376 + open_files 300 0.00453 + username 300 0.00505 + ppid 300 0.00554 + memory_info_ex 300 0.00651 + environ 300 0.01013 + memory_footprint 300 0.02241 + memory_maps 300 0.30282 diff --git a/psutil/__init__.py b/psutil/__init__.py index 54fa67f1ff..f0386b259f 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -702,7 +702,6 @@ def ppid(self) -> int: """The process parent PID. On Windows the return value is cached after first call. """ - # On POSIX we don't want to cache the ppid as it may unexpectedly # change to 1 (init) in case this process turns into a zombie: # https://github.com/giampaolo/psutil/issues/321 @@ -720,7 +719,6 @@ def ppid(self) -> int: @_use_prefetch def name(self) -> str: """The process name. The return value is cached after first call.""" - # Process name is only cached on Windows as on POSIX it may # change, see: # https://github.com/giampaolo/psutil/issues/692 @@ -811,7 +809,6 @@ def username(self) -> str: """The name of the user that owns the process. On UNIX this is calculated by using *real* process uid. """ - if POSIX: if pwd is None: # might happen if python was installed from sources @@ -1136,7 +1133,6 @@ def cpu_percent(self, interval: float | None = None) -> float: 2.9 >>> """ - blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: msg = f"interval is not positive (got {interval!r})" @@ -1279,7 +1275,6 @@ def memory_percent(self, memtype: str = "rss") -> float: >>> psutil.Process().memory_info()._fields ('rss', 'vms', 'shared', 'text', 'lib', 'data', 'dirty', 'uss', 'pss') """ - valid_types = list(_ntp.pmem._fields) if hasattr(_ntp, "pmem_ex"): valid_types += [ diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 8eee21a010..b89fe1ce7f 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -9,17 +9,20 @@ See: https://github.com/giampaolo/psutil/issues/799. """ +import argparse +import os import sys import textwrap import timeit import psutil -ITERATIONS = 1000 +TIMES = 1000 +PID = os.getpid() # The list of Process methods which gets collected in one shot and # as such get advantage of the speedup. -names = [ +NAMES = [ 'cpu_times', 'cpu_percent', 'memory_info', @@ -29,25 +32,27 @@ ] if psutil.POSIX: - names.extend(('uids', 'username')) + NAMES.extend(('uids', 'username')) if psutil.LINUX: - names += [ + NAMES += [ # 'memory_footprint', # 'memory_maps', 'cpu_num', 'cpu_times', 'gids', + 'memory_info_ex', 'name', 'num_ctx_switches', 'num_threads', + 'page_faults', 'ppid', 'status', 'terminal', 'uids', ] elif psutil.BSD: - names = [ + NAMES = [ 'cpu_times', 'gids', 'io_counters', @@ -61,9 +66,9 @@ 'uids', ] if psutil.FREEBSD: - names.append('cpu_num') + NAMES.append('cpu_num') elif psutil.SUNOS: - names += [ + NAMES += [ 'cmdline', 'gids', 'memory_footprint', @@ -76,7 +81,7 @@ 'uids', ] elif psutil.MACOS: - names += [ + NAMES += [ 'cpu_times', 'create_time', 'gids', @@ -89,7 +94,7 @@ 'uids', ] elif psutil.WINDOWS: - names += [ + NAMES += [ 'num_ctx_switches', 'num_threads', # dual implementation, called in case of AccessDenied @@ -101,10 +106,8 @@ 'memory_info', ] -names = sorted(set(names)) - -setup = textwrap.dedent(""" - from __main__ import names +setup_code = textwrap.dedent(""" + from __main__ import NAMES import psutil def call_normal(funs): @@ -116,37 +119,55 @@ def call_oneshot(funs): for fun in funs: fun() - p = psutil.Process() - funs = [getattr(p, n) for n in names] + p = psutil.Process({}) + funs = [getattr(p, n) for n in NAMES] """) +def parse_cli(): + global TIMES, PID, NAMES + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument("-i", "--times", type=int, default=TIMES) + parser.add_argument("-p", "--pid", type=int, default=PID) + parser.add_argument("-n", "--names", default=None, metavar="METHOD,METHOD") + args = parser.parse_args() + TIMES = args.times + PID = args.pid + if args.names: + NAMES = args.names.split(",") + NAMES = sorted(set(NAMES)) + + def main(): + parse_cli() print( - f"{len(names)} methods involved on platform" - f" {sys.platform!r} ({ITERATIONS} iterations, psutil" - f" {psutil.__version__}):" + f"{len(NAMES)} methods pre-fetched by oneshot() on platform" + f" {sys.platform!r} ({TIMES:,} times, psutil" + f" {psutil.__version__}):\n" ) - for name in sorted(names): - print(" " + name) + for name in sorted(NAMES): + print(" " + name) + attr = getattr(psutil.Process, name, None) + if attr is None or not callable(attr): + raise ValueError(f"invalid name {name!r}") - # "normal" run - elapsed1 = timeit.timeit( - "call_normal(funs)", setup=setup, number=ITERATIONS - ) - print(f"normal: {elapsed1:.3f} secs") + # regular run + setup = setup_code.format(PID) + elapsed1 = timeit.timeit("call_normal(funs)", setup=setup, number=TIMES) + print(f"\nregular: {elapsed1:.3f} secs") - # "one shot" run - elapsed2 = timeit.timeit( - "call_oneshot(funs)", setup=setup, number=ITERATIONS - ) - print(f"onshot: {elapsed2:.3f} secs") + # oneshot() run + elapsed2 = timeit.timeit("call_oneshot(funs)", setup=setup, number=TIMES) + print(f"oneshot: {elapsed2:.3f} secs") # done if elapsed2 < elapsed1: - print(f"speedup: +{elapsed1 / elapsed2:.2f}x") + print(f"speedup: +{elapsed1 / elapsed2:.2f}x") elif elapsed2 > elapsed1: - print(f"slowdown: -{elapsed2 / elapsed1:.2f}x") + print(f"slowdown: -{elapsed2 / elapsed1:.2f}x") else: print("same speed") diff --git a/scripts/internal/bench_oneshot_2.py b/scripts/internal/bench_oneshot_2.py index 4520683b69..2844cc0c5d 100755 --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -29,20 +29,20 @@ def call_oneshot(funs): def main(): - from bench_oneshot import names + from bench_oneshot import NAMES runner = pyperf.Runner() args = runner.parse_args() if not args.worker: print( - f"{len(names)} methods involved on platform" + f"{len(NAMES)} methods involved on platform" f" {sys.platform!r} (psutil {psutil.__version__}):" ) - for name in sorted(names): + for name in sorted(NAMES): print(" " + name) - funs = [getattr(p, n) for n in names] + funs = [getattr(p, n) for n in NAMES] runner.bench_func("normal", call_normal, funs) runner.bench_func("oneshot", call_oneshot, funs) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index 0be1bac6dc..beaa61e102 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -79,6 +79,7 @@ from psutil._common import print_color TIMES = 300 +PID = os.getpid() timings = [] templ = "{:<25} {:>10} {:>10}" @@ -106,11 +107,15 @@ def timecall(title, fun, *args, **kw): sys.stdout.flush() t = timer() for n in range(TIMES): - fun(*args, **kw) - elapsed = timer() - t - if elapsed > 2: - break - print("\r", end="") + try: + fun(*args, **kw) + except psutil.AccessDenied: + return + else: + elapsed = timer() - t + if elapsed > 2: + break + print("\033[2K\r", end="") sys.stdout.flush() timings.append((title, n + 1, elapsed)) @@ -129,17 +134,22 @@ def set_highest_priority(): p.ionice(psutil.IOPRIO_HIGH) -def main(): - global TIMES - +def parse_cli(): + global TIMES, PID parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawTextHelpFormatter ) parser.add_argument('-t', '--times', type=int, default=TIMES) + parser.add_argument('-p', '--pid', type=int, default=PID) args = parser.parse_args() TIMES = args.times + PID = args.pid assert TIMES > 1, TIMES + +def main(): + parse_cli() + try: set_highest_priority() except psutil.AccessDenied: @@ -169,7 +179,7 @@ def main(): fun = getattr(psutil, name) args = () if name == 'pid_exists': - args = (os.getpid(),) + args = (PID,) elif name == 'disk_usage': args = (os.getcwd(),) timecall(name, fun, *args) @@ -180,28 +190,13 @@ def main(): # --- process print() print_header("PROCESS APIS") - ignore = [ - 'send_signal', - 'suspend', - 'resume', - 'terminate', - 'kill', - 'wait', - 'as_dict', - 'parent', - 'parents', - 'oneshot', - 'pid', - 'rlimit', - 'children', - ] - if psutil.MACOS: - ignore.append('memory_maps') # XXX - p = psutil.Process() - for name in sorted(dir(p)): - if not name.startswith('_') and name not in ignore: - fun = getattr(p, name) + p = psutil.Process(PID) + names = p.as_dict().keys() + for name in sorted(names): + fun = getattr(p, name) + if callable(fun): timecall(name, fun) + print_timings() if not prio_set: diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py index f230dd052e..cce3dc894b 100755 --- a/scripts/internal/print_sysinfo.py +++ b/scripts/internal/print_sysinfo.py @@ -139,10 +139,10 @@ def main(): # info['proc'] = pprint.pformat(pinfo) # print - print("=" * 70, file=sys.stderr) + print("=" * 70) for k, v in info.items(): - print("{:<17} {}".format(k + ":", v), file=sys.stderr) - print("=" * 70, file=sys.stderr) + print("{:<17} {}".format(k + ":", v)) + print("=" * 70) sys.stdout.flush() diff --git a/tests/test_scripts.py b/tests/test_scripts.py index f555cc68b8..0990ec1049 100755 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -36,21 +36,16 @@ INTERNAL_SCRIPTS_DIR = SCRIPTS_DIR / "internal" -# =================================================================== -# --- Tests scripts in scripts/ directory -# =================================================================== - - -@pytest.mark.skipif( - CI_TESTING and not os.path.exists(SCRIPTS_DIR), - reason="can't find scripts/ directory", -) -class TestExampleScripts(PsutilTestCase): - @staticmethod - def assert_stdout(exe, *args): +class ScriptsTestCase(PsutilTestCase): + scripts_dir = SCRIPTS_DIR + + def assert_stdout(self, exe, *args): + """Execute the script, make sure it doesn't crash and prints + something. + """ + exe = os.path.join(self.scripts_dir, exe) env = PYTHON_EXE_ENV.copy() env.pop("PSUTIL_DEBUG", None) # avoid spamming to stderr - exe = os.path.join(SCRIPTS_DIR, exe) cmd = [PYTHON_EXE, exe, *args] try: out = sh(cmd, env=env).strip() @@ -62,13 +57,25 @@ def assert_stdout(exe, *args): assert out, out return out - @staticmethod - def assert_syntax(exe): - exe = os.path.join(SCRIPTS_DIR, exe) + def assert_syntax(self, exe): + """Check script's syntax without executing it.""" + exe = os.path.join(self.scripts_dir, exe) with open(exe, encoding="utf8") as f: src = f.read() ast.parse(src) + +# =================================================================== +# --- Tests scripts in scripts/ directory +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(SCRIPTS_DIR), + reason="can't find scripts/ directory", +) +class TestExampleScripts(ScriptsTestCase): + def test_coverage(self): # make sure all example scripts have a test method defined meths = dir(self) @@ -181,7 +188,9 @@ def test_sensors(self): CI_TESTING and not os.path.exists(INTERNAL_SCRIPTS_DIR), reason="can't find scripts/internal/ directory", ) -class TestInternalScripts(PsutilTestCase): +class TestInternalScripts(ScriptsTestCase): + scripts_dir = INTERNAL_SCRIPTS_DIR + @staticmethod def ls(): for name in os.listdir(INTERNAL_SCRIPTS_DIR): @@ -203,3 +212,9 @@ def test_import_all(self): import_module_by_path(path) except SystemExit: pass + + def test_print_api_speed(self): + self.assert_stdout("print_api_speed.py", "-t", "2") + + def test_print_sysinfo(self): + self.assert_stdout("print_sysinfo.py") From 91a4fc566e9dd6a117895352b3be0eecaf0bcd64 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 24 Mar 2026 18:03:00 +0100 Subject: [PATCH 1643/1714] Rename Git tags from release-X.Y.Z to vX.Y.Z --- Makefile | 2 +- docs/changelog.rst | 3 +++ docs/devguide.rst | 8 ++++++++ docs/migration.rst | 8 ++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1b227dc9f3..844a710af4 100644 --- a/Makefile +++ b/Makefile @@ -346,7 +346,7 @@ release: ## Upload a new release. $(MAKE) git-tag-release git-tag-release: ## Git-tag a new release. - git tag -a release-`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` + git tag -a v`python3 -c "import setup; print(setup.get_version())"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` git push --follow-tags # =================================================================== diff --git a/docs/changelog.rst b/docs/changelog.rst index 5f9ec9ce3a..a187a6c5eb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -131,6 +131,9 @@ Others method calls (e.g. ``p.name()``, ``p.status()``) return the cached values instead of making new system calls. The ``p.info`` dict is deprecated. See :ref:`migration guide `. +- :gh:`2788`: git tags renamed from ``release-X.Y.Z`` to ``vX.Y.Z`` (e.g. + ``release-7.2.2`` → ``v7.2.2``). Old tags are kept for backward + compatibility. **Bug fixes** diff --git a/docs/devguide.rst b/docs/devguide.rst index 4f2a858fea..d72c30f930 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -155,6 +155,14 @@ Unit tests are automatically run on every ``git push`` on all platforms except AIX. See config files in the `.github/workflows `_ directory. +Releases +-------- + +- Releases are uploaded to `PyPI `_ via + ``make release``. +- Git tags use the ``vX.Y.Z`` format (e.g. ``v7.2.2``). +- The version string is defined in ``psutil/__init__.py`` (``__version__``). + Documentation ------------- diff --git a/docs/migration.rst b/docs/migration.rst index b81cf0cbb8..d7a42c9867 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -205,6 +205,14 @@ memory_full_info() is deprecated mem = p.memory_footprint() uss = mem.uss +Git tags renamed +^^^^^^^^^^^^^^^^^ + +Git tags were renamed from ``release-X.Y.Z`` to ``vX.Y.Z`` +(e.g. ``release-7.2.2`` → ``v7.2.2``). Old tags are kept for +backward compatibility. If you reference psutil tags in scripts or +URLs, update them to the new format. See :gh:`2788`. + Python 3.6 dropped ^^^^^^^^^^^^^^^^^^^^ From dff63e8e4f20fe4e9a009e55d8532e7f56adec91 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 Mar 2026 00:11:36 +0100 Subject: [PATCH 1644/1714] Update devguide with notes about documentation --- HISTORY.rst | 3 +-- INSTALL.rst | 3 +-- README.rst | 4 ++-- docs/_static/css/custom.css | 11 ++++++++--- docs/api.rst | 14 +++++++------- docs/changelog.rst | 13 ++++++++++--- docs/devguide.rst | 34 +++++++++++++++++++++++++++------- docs/index.rst | 12 ++++-------- 8 files changed, 60 insertions(+), 34 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 77a827a6f6..eef7a6b80a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,4 +1,3 @@ History has moved to: -- https://psutil.readthedocs.io/changelog -- https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst (source) +- https://psutil.readthedocs.io/en/latest/changelog.html diff --git a/INSTALL.rst b/INSTALL.rst index 33ac744d46..9b1fee68b7 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,4 +1,3 @@ Installation instructions have moved to: -- https://psutil.readthedocs.io/install -- https://github.com/giampaolo/psutil/blob/master/docs/install.rst (source) +- https://psutil.readthedocs.io/en/latest/install.html diff --git a/README.rst b/README.rst index 5748ccd91b..efdcec326e 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ | |downloads| |stars| |forks| |contributors| |packages| -| |version| |license| |stackoverflow| |twitter| |tidelift| +| |version| |license| |tidelift| | |github-actions-wheels| |github-actions-bsd| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg @@ -30,7 +30,7 @@ :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests :alt: FreeBSD, NetBSD, OpenBSD -.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi +.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi&color=yellowgreen :target: https://pypi.org/project/psutil :alt: Latest version diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 67b865b008..f0b43b6345 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -141,6 +141,11 @@ h1 { /* Code blocks */ /* ================================================================== */ +/* reduce bottom margin of code blocks inside list items */ +.rst-content li > div[class^='highlight'] { + margin-bottom: 6px !important; +} + pre { padding: 5px !important; } @@ -169,12 +174,12 @@ div[class^='highlight'] div[class^='highlight'] { /* Links */ /* ================================================================== */ -/* external links: dotted underline (except intersphinx) */ -a.reference.external:not([href*="docs.python.org"]) { +/* external links: dotted underline (except intersphinx and image links) */ +a.reference.external:not([href*="docs.python.org"]):not(:has(img)) { text-decoration: underline dotted; } -a.reference.external:not([href*="docs.python.org"]):hover { +a.reference.external:not([href*="docs.python.org"]):not(:has(img)):hover { text-decoration: underline solid; } diff --git a/docs/api.rst b/docs/api.rst index cc0f6c8d17..614f863289 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1109,7 +1109,7 @@ Functions gone and which ones are still alive. The *gone* ones will have a new *returncode* attribute indicating process exit status as returned by :meth:`Process.wait`. - ``callback`` is a function which gets called when one of the processes being + *callback* is a function which gets called when one of the processes being waited on is terminated and a :class:`Process` instance is passed as callback argument (the instance will also have a *returncode* attribute set). This function will return as soon as all processes terminate or when @@ -1255,7 +1255,7 @@ Process class on what platform you're on. Empty rows indicate methods that are grouped together internally (i.e. they share the same system call). - The last column (speedup) shows an approximation of the speedup you can get + The last row (*speedup*) shows an approximation of the speedup you can get if you call all the methods together (best case scenario). It was calculated via `bench_oneshot.py `_ @@ -1743,7 +1743,7 @@ Process class which can also be ``> 100.0`` in case of a process running multiple threads on different CPUs. When *interval* is > ``0.0`` compares process times to system CPU times - elapsed before and after the interval (blocking). When interval is ``0.0`` + elapsed before and after the interval (blocking). When *interval* is ``0.0`` or ``None`` compares process times to system CPU times elapsed since last call, returning immediately. That means the first time this is called it will return a meaningless ``0.0`` value which you are supposed to ignore. @@ -2062,7 +2062,7 @@ Process class memory value is used in the calculation (defaults to ``"rss"``). .. versionchanged:: 4.0.0 - added `memtype` parameter. + added *memtype* parameter. .. method:: memory_maps(grouped=True) @@ -2142,7 +2142,7 @@ Process class Return the children of this process as a list of :class:`Process` instances. - If recursive is ``True`` return all the parent descendants. + If *recursive* is ``True`` return all the parent descendants. Pseudo code example assuming *A == this process*: :: @@ -2204,7 +2204,7 @@ Process class - **position** (*Linux*): the file (offset) position. - **mode** (*Linux*): a string indicating how the file was opened, similarly - to :func:`open` builtin ``mode`` argument. + to :func:`open` builtin *mode* argument. Possible values are ``'r'``, ``'w'``, ``'a'``, ``'r+'`` and ``'a+'``. There's no distinction between files opened in binary or text mode (``"b"`` or ``"t"``). @@ -2442,7 +2442,7 @@ Process class .. note:: - When ``timeout`` is not ``None`` and the platform supports it, an + When *timeout* is not ``None`` and the platform supports it, an efficient event-driven mechanism is used to wait for process termination: - Linux >= 5.3 with Python >= 3.9 uses :func:`os.pidfd_open` + :func:`select.poll` diff --git a/docs/changelog.rst b/docs/changelog.rst index a187a6c5eb..1ac94ee991 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,7 +14,7 @@ Changelog Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, -:gh:`2775`, :gh:`2781`, :gh:`2787`) +:gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`) - Split docs from a single HTML file into multiple sections (API reference, install, etc.). @@ -38,15 +38,22 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. + - Greatly improved :func:`virtual_memory` doc and many other APIs. - Testing: - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting. - Add custom script to detect dead reference links in ``.rst`` files. - - Build doc as part of CI process. - Use sphinx extension to validate Python code snippets syntax at build-time. -- Greatly improved :func:`virtual_memory` doc. +- Build / RTD: + + - Configured RTD to automatically public doc from Git tags, instead of from + main on every push. + - Build doc as part of CI process (CI fails on error). + - Removed /en language from RTD URLs. Turn that into a redirect. + - Before: https://psutil.readthedocs.io/en/stable/ + - Now: https://psutil.readthedocs.io/stable/ Type hints / enums: diff --git a/docs/devguide.rst b/docs/devguide.rst index d72c30f930..d981dec6e6 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -155,6 +155,32 @@ Unit tests are automatically run on every ``git push`` on all platforms except AIX. See config files in the `.github/workflows `_ directory. +Documentation +------------- + +- The documentation source is located in the `docs/`_ directory. +- To build it and generate the HTML (in the ``_build/html`` directory): + + .. code-block:: bash + + cd docs/ + python3 -m pip install -r requirements.txt + make html + +- The public documentation is hosted at https://psutil.readthedocs.io. +- There are 2 versions, which you can select from the dropdown menu at the top + of the page: + + - `/stable `_: generated from the most + recent Git tag (latest released psutil version). + - `/latest `_: generated from the + master branch. It is automatically updated on git push. + +Redirects: + +- https://psutil.readthedocs.io redirects to + `/stable `_ by default. + Releases -------- @@ -163,16 +189,10 @@ Releases - Git tags use the ``vX.Y.Z`` format (e.g. ``v7.2.2``). - The version string is defined in ``psutil/__init__.py`` (``__version__``). -Documentation -------------- - -- doc is under ``docs/``. -- doc can be built with ``make install-pydeps-dev; cd docs; make html``. -- public doc is hosted at https://psutil.readthedocs.io. - .. _`changelog.rst`: https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`credits.rst`: https://github.com/giampaolo/psutil/blob/master/docs/credits.rst +.. _`docs/`: https://github.com/giampaolo/psutil/blob/master/psutil/docs/ .. _`Git for Windows`: https://git-scm.com/install/windows .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile .. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ diff --git a/docs/index.rst b/docs/index.rst index 5e6a6f10eb..174e0672b8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,18 +9,14 @@ psutil documentation :target: https://github.com/giampaolo/psutil :alt: GitHub repo -.. image:: https://readthedocs.org/projects/psutil/badge/?version=latest - :target: https://app.readthedocs.org/projects/psutil/builds/ - :alt: ReadTheDocs +.. image:: https://img.shields.io/pypi/dm/psutil.svg?color=green + :target: https://clickpy.clickhouse.com/dashboard/psutil + :alt: Downloads -.. image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi&color=red +.. image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi&color=yellowgreen :target: https://pypi.org/project/psutil :alt: Latest version -.. image:: https://img.shields.io/pypi/dm/psutil.svg - :target: https://clickpy.clickhouse.com/dashboard/psutil - :alt: Downloads - psutil (Python system and process utilities) is a cross-platform library for retrieving information about running **processes** and **system utilization** (CPU, memory, disks, network, sensors) From 25fd8aad6018f120f9f8b64de85483e30f8e3da1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 25 Mar 2026 19:49:14 +0100 Subject: [PATCH 1645/1714] Doc: standardize **fields** and *args* + add .. seealso:: entries --- README.rst | 20 ++-- docs/api.rst | 248 +++++++++++++++++++++++++++-------------------- docs/faq.rst | 4 +- docs/index.rst | 1 - docs/recipes.rst | 10 +- 5 files changed, 160 insertions(+), 123 deletions(-) diff --git a/README.rst b/README.rst index efdcec326e..c462e28783 100644 --- a/README.rst +++ b/README.rst @@ -224,7 +224,7 @@ including CPU, memory, disks, network, sensors, and process management. CPU --- -.. code-block:: python +.. code-block:: pycon >>> import psutil >>> @@ -269,7 +269,7 @@ CPU Memory ------ -.. code-block:: python +.. code-block:: pycon >>> psutil.virtual_memory() svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304) @@ -280,7 +280,7 @@ Memory Disks ----- -.. code-block:: python +.. code-block:: pycon >>> psutil.disk_partitions() [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), @@ -296,7 +296,7 @@ Disks Network ------- -.. code-block:: python +.. code-block:: pycon >>> psutil.net_io_counters(pernic=True) {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0), @@ -323,7 +323,7 @@ Network Sensors ------- -.. code-block:: python +.. code-block:: pycon >>> import psutil >>> psutil.sensors_temperatures() @@ -342,7 +342,7 @@ Sensors Other system info ----------------- -.. code-block:: python +.. code-block:: pycon >>> import psutil >>> psutil.users() @@ -356,7 +356,7 @@ Other system info Process management ------------------ -.. code-block:: python +.. code-block:: pycon >>> import psutil >>> psutil.pids() @@ -499,7 +499,7 @@ Process management Further process APIs -------------------- -.. code-block:: python +.. code-block:: pycon >>> import psutil >>> for proc in psutil.process_iter(['pid', 'name']): @@ -523,7 +523,7 @@ Further process APIs Heap info --------- -.. code-block:: python +.. code-block:: pycon >>> import psutil >>> psutil.heap_info() @@ -535,7 +535,7 @@ See also `psleak`_. Windows services ---------------- -.. code-block:: python +.. code-block:: pycon >>> list(psutil.win_service_iter()) [, diff --git a/docs/api.rst b/docs/api.rst index 614f863289..8e3f1de4d0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -81,18 +81,18 @@ CPU `_. .. versionchanged:: 4.1.0 - added *irq* and *dpc* fields on Windows (*irq* was called *interrupt* + added **irq** and **dpc** fields on Windows (**irq** was called **interrupt** before 8.0.0). .. versionchanged:: 8.0.0 - *interrupt* field on Windows was renamed to *irq*; *interrupt* still + **interrupt** field on Windows was renamed to **irq**; **interrupt** still works but raises :exc:`DeprecationWarning`. .. versionchanged:: 8.0.0 - ``cpu_times()`` field order was standardized: ``user``, ``system``, - ``idle`` are now always the first three fields. Previously on Linux, - macOS, and BSD the first three were ``user``, ``nice``, ``system``. - See :ref:`migration guide `. + field order was standardized: **user**, **system**, **idle** are now + always the first three fields. Previously on Linux, macOS, and BSD the + first three were **user**, **nice**, **system**. See :ref:`migration guide + `. .. function:: cpu_percent(interval=None, percpu=False) @@ -131,7 +131,9 @@ CPU .. note:: the first time this function is called with *interval* = ``0.0`` or ``None`` it will return a meaningless ``0.0`` value which you are supposed to - ignore. See also :ref:`faq_cpu_percent` FAQ. + ignore. + + .. seealso:: :ref:`faq_cpu_percent` .. versionchanged:: 5.9.6 the function is now thread safe. @@ -149,11 +151,13 @@ CPU .. note:: the first time this function is called with *interval* = ``0.0`` or ``None`` it will return a meaningless ``0.0`` value which you are supposed - to ignore. See also :ref:`faq_cpu_percent` FAQ. + to ignore. + + .. seealso:: :ref:`faq_cpu_percent` .. versionchanged:: 4.1.0 - two new *irq* and *dpc* fields are returned on Windows (*irq* was - called *interrupt* before 8.0.0). + two new **irq** and **dpc** fields are returned on Windows (**irq** was + called **interrupt** before 8.0.0). .. versionchanged:: 5.9.6 function is now thread safe. @@ -167,8 +171,10 @@ CPU "logical CPUs" means the number of physical cores multiplied by the number of threads that can run on each core (this is known as Hyper Threading). This is what cloud providers often refer to as vCPUs. + If *logical* is ``False`` return the number of physical cores only, or ``None`` if undetermined. + On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return ``None``. Example on a system having 2 cores + Hyper Threading: @@ -193,6 +199,8 @@ CPU >>> len(psutil.Process().cpu_affinity()) 1 + .. seealso:: :ref:`faq_cpu_count` + .. function:: cpu_stats() Return various CPU statistics as a named tuple. All fields are @@ -422,7 +430,10 @@ Memory - On Windows, **total**, **used** ("In use"), and **available** match the Task Manager (Performance > Memory tab). - .. note:: see also `scripts/meminfo.py`_. + .. seealso:: + - `scripts/meminfo.py`_ + - :ref:`faq_virtual_memory_available` + - :ref:`faq_used_plus_free` .. versionchanged:: 4.2.0 added *shared* metric on Linux. @@ -455,8 +466,6 @@ Memory their rate of change rather than the absolute value to detect active swapping. See :term:`swap-in` and :term:`swap-out`. On Windows both are always ``0``. - See `scripts/meminfo.py`_ script providing an example on how to convert bytes in a - human readable form. .. code-block:: pycon @@ -464,6 +473,8 @@ Memory >>> psutil.swap_memory() sswap(total=2097147904L, used=886620160L, free=1210527744L, percent=42.3, sin=1050411008, sout=1906720768) + .. seealso:: `scripts/meminfo.py`_. + .. versionchanged:: 5.2.3 on Linux this function relies on /proc fs instead of sysinfo() syscall so that it can be used in conjunction with @@ -482,7 +493,7 @@ Disks (e.g. pseudo, memory, duplicate, inaccessible filesystems). Note that this may not be fully reliable on all systems (e.g. on BSD this parameter is ignored). - See `scripts/disk_usage.py`_ script providing an example usage. + Returns a list of named tuples with the following fields: * **device**: the device path (e.g. "/dev/hda1"). On Windows this is the @@ -501,18 +512,19 @@ Disks [sdiskpart(device='/dev/sda3', mountpoint='/', fstype='ext4', opts='rw,errors=remount-ro'), sdiskpart(device='/dev/sda7', mountpoint='/home', fstype='ext4', opts='rw')] + .. seealso:: `scripts/disk_usage.py`_. + .. versionchanged:: 5.7.4 - added *maxfile* and *maxpath* fields. + added **maxfile** and **maxpath** fields. .. versionchanged:: 6.0.0 - removed *maxfile* and *maxpath* fields. + removed **maxfile** and **maxpath** fields. .. function:: disk_usage(path) Return disk usage statistics about the partition which contains the given *path* as a named tuple including **total**, **used** and **free** space expressed in bytes, plus the **percentage** usage. - See `scripts/disk_usage.py`_ script providing an example usage. This function was later incorporated in Python 3.3 as :func:`shutil.disk_usage` (`BPO-12442`_). @@ -524,17 +536,18 @@ Disks sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) .. note:: - Note On UNIX, path must point to a path within a **mounted** filesystem partition. + On UNIX, *path* must point to a path within a **mounted** filesystem partition. .. note:: UNIX usually reserves 5% of the total disk space for the root user. - *total* and *used* fields on UNIX refer to the overall total and used - space, whereas *free* represents the space available for the **user** and - *percent* represents the **user** utilization (see - `source code `_). - That is why *percent* value may look 5% bigger than what you would expect - it to be. - Also note that both 4 values match "df" cmdline utility. + **total** and **used** fields on UNIX refer to the overall total and used + space, whereas **free** represents the space available to + unprivileged users and **percent** represents unprivileged user + utilization (see `source code `_). + That is why the **percent** value may look 5% bigger than expected. + Also note that all four values match the "df" command-line utility. + + .. seealso:: `scripts/disk_usage.py`_. .. versionchanged:: 4.3.0 *percent* value takes root reserved space into account. @@ -563,7 +576,7 @@ Disks If *perdisk* is ``True`` return the same information for every physical disk installed on the system as a dictionary with partition names as the keys and the named tuple described above as the values. - See `scripts/iotop.py`_ for an example application. + On some systems such as Linux, on a very busy or long-lived system, the numbers returned by the kernel may overflow and wrap (restart from zero). If *nowrap* is ``True`` psutil will detect and adjust those numbers across @@ -591,16 +604,18 @@ Disks on Windows ``"diskperf -y"`` command may need to be executed first otherwise this function won't find any disk. + .. seealso:: `scripts/iotop.py`_. + .. versionchanged:: 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new *nowrap* argument. .. versionchanged:: 4.0.0 - added *busy_time* (Linux, FreeBSD), *read_merged_count* and - *write_merged_count* (Linux) fields. + added **busy_time** (Linux, FreeBSD), **read_merged_count** and + **write_merged_count** (Linux) fields. .. versionchanged:: 4.0.0 - NetBSD no longer has *read_time* and *write_time* fields. + NetBSD no longer has **read_time** and **write_time** fields. Network ^^^^^^^ @@ -643,7 +658,7 @@ Network {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} - Also see `scripts/nettop.py`_ and `scripts/ifconfig.py`_ for an example application. + .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. .. versionchanged:: 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new @@ -662,7 +677,7 @@ Network - **type**: the address type, either :data:`socket.SOCK_STREAM`, :data:`socket.SOCK_DGRAM` or :data:`socket.SOCK_SEQPACKET`. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` - in case of AF_UNIX sockets. For UNIX sockets see notes below. + in case of :data:`socket.AF_UNIX` sockets. For UNIX sockets see notes below. - **raddr**: the remote address as a ``(ip, port)`` named tuple or an absolute ``path`` in case of UNIX sockets. When the remote endpoint is not connected you'll get an empty tuple @@ -709,8 +724,6 @@ Network On macOS and AIX this function requires root privileges. To get per-process connections use :meth:`Process.net_connections`. - Also, see `scripts/netstat.py`_ example script. - Example: .. code-block:: pycon @@ -738,20 +751,22 @@ Network (Linux, FreeBSD, OpenBSD) *raddr* field for UNIX sockets is always set to ``""`` (empty string). This is a limitation of the OS. + .. seealso:: `scripts/netstat.py`_. + .. versionadded:: 2.1.0 .. versionchanged:: 5.3.0 socket "fd" is now set for real instead of being ``-1``. .. versionchanged:: 5.3.0 - *laddr* and *raddr* are named tuples. + **laddr** and **raddr** are named tuples. .. versionchanged:: 5.9.5 - OpenBSD: retrieve *laddr* path for AF_UNIX sockets (before it was an empty - string). + OpenBSD: retrieve **laddr** path for :data:`socket.AF_UNIX` sockets + (before it was an empty string). .. versionchanged:: 8.0.0 - *status* field is now a :class:`psutil.ConnectionStatus` enum member + **status** field is now a :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. See :ref:`migration guide `. @@ -785,8 +800,6 @@ Network snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> - See also `scripts/nettop.py`_ and `scripts/ifconfig.py`_ for an example application. - .. note:: if you're interested in others families (e.g. AF_BLUETOOTH) you can use the more powerful `netifaces `_ @@ -799,16 +812,18 @@ Network .. note:: *broadcast* and *ptp* are not supported on Windows and are always ``None``. + .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. + .. versionadded:: 3.0.0 .. versionchanged:: 3.2.0 - *ptp* field was added. + **ptp** field was added. .. versionchanged:: 4.4.0 - added support for *netmask* field on Windows which is no longer ``None``. + added support for **netmask** field on Windows which is no longer ``None``. .. versionchanged:: 7.0.0 - added support for *broadcast* field on Windows which is no longer ``None``. + added support for **broadcast** field on Windows which is no longer ``None``. .. function:: net_if_stats() @@ -831,8 +846,6 @@ Network ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, and ``d2`` (some flags are only available on certain platforms). - Also see `scripts/nettop.py`_ and `scripts/ifconfig.py`_ for an example application. - .. code-block:: pycon >>> import psutil @@ -840,15 +853,17 @@ Network {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast'), 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running')} + .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. + .. availability:: UNIX .. versionadded:: 3.0.0 .. versionchanged:: 5.7.3 - `isup` on UNIX also checks whether the NIC is running. + **isup** on UNIX also checks whether the NIC is running. .. versionchanged:: 5.9.3 - *flags* field was added on POSIX. + **flags** field was added on POSIX. Sensors ^^^^^^^ @@ -870,8 +885,6 @@ Sensors - **critical**: temperature at which the system will shut down, or ``None`` if not available. - See also `scripts/temperatures.py`_ and `scripts/sensors.py`_ for an example application. - .. code-block:: pycon >>> import psutil @@ -884,6 +897,8 @@ Sensors shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0), shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]} + .. seealso:: `scripts/temperatures.py`_ and `scripts/sensors.py`_. + .. availability:: Linux, FreeBSD .. versionadded:: 5.1.0 @@ -904,7 +919,7 @@ Sensors >>> psutil.sensors_fans() {'asus': [sfan(label='cpu_fan', current=3200)]} - See also `scripts/fans.py`_ and `scripts/sensors.py`_ for an example application. + .. seealso:: `scripts/fans.py`_ and `scripts/sensors.py`_. .. availability:: Linux @@ -941,7 +956,7 @@ Sensors >>> print("charge = %s%%, time left = %s" % (battery.percent, secs2hours(battery.secsleft))) charge = 93%, time left = 4:37:08 - See also `scripts/battery.py`_ and `scripts/sensors.py`_ for an example application. + .. seealso:: `scripts/battery.py`_ and `scripts/sensors.py`_. .. availability:: Linux, Windows, macOS, FreeBSD @@ -1044,14 +1059,6 @@ Functions Processes are returned sorted by PID. - .. seealso:: :ref:`perf-process-iter` - - .. note:: - - Since :class:`Process` instances are reused across calls, a subsequent - :func:`process_iter` call will overwrite or clear any previously - pre-fetched values. Do not rely on cached values from a prior iteration. - .. code-block:: pycon >>> import psutil @@ -1081,6 +1088,14 @@ Functions >>> psutil.process_iter.cache_clear() + .. note:: + + Since :class:`Process` instances are reused across calls, a subsequent + :func:`process_iter` call will overwrite or clear any previously + pre-fetched values. Do not rely on cached values from a prior iteration. + + .. seealso:: :ref:`perf-process-iter` + .. versionchanged:: 5.3.0 added "attrs" and "ad_value" parameters. @@ -1102,6 +1117,8 @@ Functions Check whether the given PID exists in the current process list. This is faster than doing ``pid in psutil.pids()`` and should be preferred. + .. seealso:: :ref:`faq_pid_exists_vs_isrunning` + .. function:: wait_procs(procs, timeout=None, callback=None) Convenience function which waits for a list of :class:`Process` instances to @@ -1150,7 +1167,7 @@ Exceptions exists. *name* is the name the process had before disappearing and gets set only if :meth:`Process.name` was previously called. - See also :ref:`faq_no_such_process` FAQ. + .. seealso:: :ref:`faq_no_such_process` .. exception:: ZombieProcess(pid, name=None, ppid=None, msg=None) @@ -1160,14 +1177,14 @@ Exceptions :meth:`Process.ppid` methods were called before the process turned into a zombie. - See also :ref:`faq_zombie_process` FAQ. - .. note:: this is a subclass of :exc:`NoSuchProcess` so if you're not interested in retrieving zombies (e.g. when using :func:`process_iter`) you can ignore this exception and just catch :exc:`NoSuchProcess`. + .. seealso:: :ref:`faq_zombie_process` + .. versionadded:: 3.0.0 .. exception:: AccessDenied(pid=None, name=None, msg=None) @@ -1176,7 +1193,7 @@ Exceptions action is denied due to insufficient privileges. *name* attribute is available if :meth:`Process.name` was previously called. - See also :ref:`faq_access_denied` FAQ. + .. seealso:: :ref:`faq_access_denied` .. exception:: TimeoutExpired(seconds, pid=None, name=None, msg=None) @@ -1233,10 +1250,6 @@ Process class The advice is to use this every time you retrieve more than one attribute about the process. If you're lucky, you'll get a hell of a speedup. - .. seealso:: - - :ref:`perf-oneshot` - - :ref:`perf-oneshot-bench` - .. code-block:: pycon >>> import psutil @@ -1305,6 +1318,10 @@ Process class | *speedup: +1.8x* | *speedup: +1.8x / +6.5x* | *speedup: +1.9x* | *speedup: +2.0x* | *speedup: +1.3x* | *speedup: +1.3x* | +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + .. seealso:: + - :ref:`perf-oneshot` + - :ref:`perf-oneshot-bench` + .. versionadded:: 5.0.0 .. attribute:: pid @@ -1331,7 +1348,8 @@ Process class The process name. On Windows the return value is cached after first call. Not on POSIX because the process name may change. - See also how to `find a process by name <#find-process-by-name>`_. + + .. seealso:: how to `find a process by name <#find-process-by-name>`_. .. method:: exe() @@ -1597,7 +1615,6 @@ Process class but can be used for any process PID, not only :func:`os.getpid`. For get, return value is a ``(soft, hard)`` tuple. Each value may be either an integer or :data:`psutil.RLIMIT_* `. - Also see `scripts/procinfo.py`_ script. .. code-block:: pycon @@ -1609,6 +1626,8 @@ Process class (1024, 1024) >>> + .. seealso:: `scripts/procinfo.py`_. + .. availability:: Linux, FreeBSD .. versionchanged:: 5.7.3 @@ -1654,12 +1673,11 @@ Process class >>> p.io_counters() pio(read_count=454556, write_count=3456, read_bytes=110592, write_bytes=0, read_chars=769931, write_chars=203) - .. availability:: Linux, BSD, Windows, AIX .. versionchanged:: 5.2.0 - added *read_chars* + *write_chars* on Linux and *other_count* + - *other_bytes* on Windows. + added **read_chars** + **write_chars** on Linux and **other_count** + + **other_bytes** on Windows. .. method:: num_ctx_switches() @@ -1667,8 +1685,8 @@ Process class (:term:`cumulative counter`). .. note:: - (Windows, macOS) *involuntary* value is always set to 0, while - *voluntary* value reflect the total number of context switches (voluntary + (Windows, macOS) **involuntary** value is always set to 0, while + **voluntary** value reflect the total number of context switches (voluntary + involuntary). This is a limitation of the OS. .. versionchanged:: 5.4.1 @@ -1732,10 +1750,10 @@ Process class .. versionchanged:: 4.1.0 - return two extra fields: *children_user* and *children_system*. + return two extra fields: **children_user** and **children_system**. .. versionchanged:: 5.6.4 - added *iowait* on Linux. + added **iowait** on Linux. .. method:: cpu_percent(interval=None) @@ -1764,7 +1782,7 @@ Process class .. note:: the first time this method is called with interval = ``0.0`` or ``None`` it will return a meaningless ``0.0`` value which you are - supposed to ignore. See also :ref:`faq_cpu_percent` FAQ. + supposed to ignore. .. note:: the returned value can be > 100.0 in case of a process running multiple @@ -1783,6 +1801,10 @@ Process class To emulate Windows ``taskmgr.exe`` behavior you can do: ``p.cpu_percent() / psutil.cpu_count()``. + .. seealso:: + - :ref:`faq_cpu_percent` + - :ref:`faq_cpu_percent_gt_100` + .. method:: cpu_affinity(cpus=None) Get or set process current @@ -1826,8 +1848,9 @@ Process class The returned number should be ``<=`` :func:`psutil.cpu_count`. On FreeBSD certain kernel process may return ``-1``. It may be used in conjunction with ``psutil.cpu_percent(percpu=True)`` to - observe the system workload distributed across multiple CPUs as shown by - `scripts/cpu_distribution.py`_ example script. + observe the system workload distributed across multiple CPUs. + + .. seealso:: `scripts/cpu_distribution.py`_. .. availability:: Linux, FreeBSD, SunOS @@ -1837,7 +1860,7 @@ Process class Return a named tuple with variable fields depending on the platform representing memory information about the process. - The "portable" fields available on all platforms are `rss` and `vms`. + The "portable" fields available on all platforms are **rss** and **vms**. All numbers are expressed in bytes. +---------+---------+----------+---------+-----+-----------------+ @@ -1862,7 +1885,6 @@ Process class currently held by this process (code, data, stack, and mapped files that are resident). Pages swapped out to disk are not counted. On UNIX it matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. - See also :ref:`faq_memory_rss_vs_vms` FAQ. - **vms**: aka :term:`VMS`. The total address space reserved by the process, including pages not yet touched, pages in swap, and @@ -1910,30 +1932,35 @@ Process class >>> p.memory_info() pmem(rss=15491072, vms=84025344, shared=5206016, text=2555904, data=9891840) + + .. seealso:: + - :ref:`faq_memory_rss_vs_vms` + - :ref:`faq_memory_footprint` + .. versionchanged:: 4.0.0 - multiple fields are returned, not only *rss* and *vms*. + multiple fields are returned, not only **rss** and **vms**. .. versionchanged:: 8.0.0 - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated + Linux: **lib** and **dirty** removed (always 0 since Linux 2.6). Deprecated aliases returning 0 and emitting `DeprecationWarning` are kept. See :ref:`migration guide `. .. versionchanged:: 8.0.0 - macOS: *pfaults* and *pageins* removed with **no backward-compatible + macOS: **pfaults** and **pageins** removed with **no backward-compatible aliases**. Use :meth:`page_faults` instead. See :ref:`migration guide `. .. versionchanged:: 8.0.0 - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → - *peak_vms*, *num_page_faults* → :meth:`page_faults` method. At the same - time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, - *peak_nonpaged_pool* were moved to :meth:`memory_info_ex`. All these old + Windows: eliminated old aliases: **wset** → **rss**, **peak_wset** → + **peak_rss**, **pagefile** / **private** → **vms**, **peak_pagefile** → + **peak_vms**, **num_page_faults** → :meth:`page_faults` method. At the same + time **paged_pool**, **nonpaged_pool**, **peak_paged_pool**, + **peak_nonpaged_pool** were moved to :meth:`memory_info_ex`. All these old names still work but raise `DeprecationWarning`. See :ref:`migration guide `. .. versionchanged:: 8.0.0 - BSD: added *peak_rss*. + BSD: added **peak_rss**. .. method:: memory_info_ex() @@ -2036,12 +2063,14 @@ Process class >>> p.memory_footprint() pfootprint(uss=6545408, pss=6872064, swap=0) - See also `scripts/procsmem.py`_ for an example application. - - .. versionadded:: 8.0.0 + .. seealso:: + - `scripts/procsmem.py`_. + - :ref:`faq_memory_footprint` .. availability:: Linux, macOS, Windows + .. versionadded:: 8.0.0 + .. method:: memory_full_info() This deprecated method returns the same information as :meth:`memory_info` @@ -2072,7 +2101,6 @@ Process class If *grouped* is ``False`` each region is listed individually and the tuple also includes *addr* (address range) and *perms* (permission string e.g. ``"r-xp"``). - See `scripts/pmap.py`_ for an example application. +---------------+---------+--------------+-----------+ | Linux | Windows | FreeBSD | Solaris | @@ -2132,6 +2160,8 @@ Process class pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), ...] + .. seealso:: `scripts/pmap.py`_. + .. availability:: Linux, Windows, FreeBSD, SunOS .. versionchanged:: 5.6.0 @@ -2166,7 +2196,8 @@ Process class returned either as the reference to process A is lost. This concept is well illustrated by this `unit test `_. - See also how to :ref:`kill a process tree `. + + .. seealso:: how to :ref:`kill a process tree `. .. method:: page_faults() @@ -2241,7 +2272,7 @@ Process class no longer hangs on Windows. .. versionchanged:: 4.1.0 - new *position*, *mode* and *flags* fields on Linux. + new **position**, **mode** and **flags** fields on Linux. .. method:: net_connections(kind="inet") @@ -2319,11 +2350,11 @@ Process class (Solaris) UNIX sockets are not supported. .. note:: - (Linux, FreeBSD) *raddr* field for UNIX sockets is always set to "". + (Linux, FreeBSD) **raddr** field for UNIX sockets is always set to "". This is a limitation of the OS. .. note:: - (OpenBSD) *laddr* and *raddr* fields for UNIX sockets are always set to + (OpenBSD) **laddr** and **raddr** fields for UNIX sockets are always set to "". This is a limitation of the OS. .. note:: @@ -2331,13 +2362,13 @@ Process class as root (lsof does the same). .. versionchanged:: 5.3.0 - *laddr* and *raddr* are named tuples. + **laddr** and **raddr** are named tuples. .. versionchanged:: 6.0.0 method renamed from `connections` to `net_connections`. .. versionchanged:: 8.0.0 - *status* field is now a :class:`psutil.ConnectionStatus` enum member + **status** field is now a :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. See :ref:`migration guide `. @@ -2352,7 +2383,7 @@ Process class Return whether the current process is running in the current process list. Differently from ``psutil.pid_exists(p.pid)``, this is reliable also in - case the process is gone and its PID reused by another process (:ref:`PID reuse `). + case the process is gone and its PID reused by another process. If PID has been reused, this method will also remove the process from :func:`process_iter` internal cache. @@ -2361,6 +2392,10 @@ Process class this will return ``True`` also if the process is a :term:`zombie process` (``p.status() == psutil.STATUS_ZOMBIE``). + .. seealso:: + - :ref:`faq_pid_reuse` + - :ref:`faq_pid_exists_vs_isrunning` + .. versionchanged:: 6.0.0 automatically remove process from :func:`process_iter` internal cache if PID has been reused by another process. @@ -2372,7 +2407,8 @@ Process class On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals are supported and *SIGTERM* is treated as an alias for :meth:`kill`. - See also how to :ref:`kill a process tree `. + + .. seealso:: how to :ref:`kill a process tree ` .. versionchanged:: 3.2.0 support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows was @@ -2398,7 +2434,8 @@ Process class whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGTERM)``. On Windows this is an alias for :meth:`kill`. - See also how to :ref:`kill a process tree `. + + .. seealso:: how to :ref:`kill a process tree `. .. method:: kill() @@ -2406,7 +2443,8 @@ Process class checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, signal.SIGKILL)``. On Windows this is done by using `TerminateProcess`_. - See also how to :ref:`kill a process tree `. + + .. seealso:: how to :ref:`kill a process tree `. .. method:: wait(timeout=None) @@ -2739,7 +2777,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ConnectionStatus :class:`enum.StrEnum` collection of :data:`CONN_* ` - constants. Returned in the *status* field of + constants. Returned in the **status** field of :func:`psutil.net_connections` and :meth:`Process.net_connections`. .. versionadded:: 8.0.0 @@ -2969,7 +3007,7 @@ Connections constants A set of strings representing the status of a TCP connection. Returned by :meth:`Process.net_connections` and - :func:`psutil.net_connections` (`status` field). + :func:`psutil.net_connections` (**status** field). These constants are members of the :class:`psutil.ConnectionStatus` enum. .. versionchanged:: 8.0.0 diff --git a/docs/faq.rst b/docs/faq.rst index 00aa2df91f..7f0bc1498a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -333,8 +333,8 @@ privileges. On Linux it also returns PSS (Proportional Set Size) and swap. .. _faq_used_plus_free: -Why does virtual_memory().used + free != total? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Why does virtual_memory() used + free != total? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Because some memory (like cache and buffers) is reclaimable and accounted separately: diff --git a/docs/index.rst b/docs/index.rst index 174e0672b8..dd7f2b8619 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -130,4 +130,3 @@ Table of contents Changelog .. _`Tidelift security contact`: https://tidelift.com/security -.. diff --git a/docs/recipes.rst b/docs/recipes.rst index 39c5cd395e..e5f64d5f84 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -114,7 +114,7 @@ Filtering and sorting processes Processes owned by user: -.. code-block:: python +.. code-block:: pycon >>> import getpass >>> import psutil @@ -128,7 +128,7 @@ Processes owned by user: Processes actively running: -.. code-block:: python +.. code-block:: pycon >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "status"]) if p.status() == psutil.STATUS_RUNNING]) [(1150, 'Xorg'), @@ -139,7 +139,7 @@ Processes actively running: Processes using log files: -.. code-block:: python +.. code-block:: pycon >>> for p in psutil.process_iter(["name", "open_files"]): ... for file in p.open_files() or []: @@ -154,7 +154,7 @@ Processes using log files: Processes consuming more than 500M of memory: -.. code-block:: python +.. code-block:: pycon >>> pp([(p.pid, p.name(), p.memory_info().rss) for p in psutil.process_iter(["name", "memory_info"]) if p.memory_info().rss > 500 * 1024 * 1024]) [(2650, 'chrome', 532324352), @@ -165,7 +165,7 @@ Processes consuming more than 500M of memory: Top 3 processes which consumed the most CPU time: -.. code-block:: python +.. code-block:: pycon >>> pp([(p.pid, p.name(), sum(p.cpu_times())) for p in sorted(psutil.process_iter(["name", "cpu_times"]), key=lambda p: sum(p.cpu_times()[:2]))][-3:]) [(2721, 'chrome', 10219.73), From 2311002ec0cde6f871a8568d3373b536e34923c9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Mar 2026 12:07:19 +0100 Subject: [PATCH 1646/1714] Doc: various improvements and cleanups --- .github/workflows/issues.py | 2 +- CONTRIBUTING.md | 3 +- HISTORY.rst | 2 +- INSTALL.rst | 2 +- README.rst | 67 ++++++------ docs/.readthedocs.yaml | 2 +- docs/adoption.rst | 6 +- docs/api.rst | 165 ++++++++++++++--------------- docs/changelog.rst | 2 +- docs/devguide.rst | 70 ++++++------ docs/faq.rst | 10 +- docs/migration.rst | 6 +- docs/performance.rst | 2 +- docs/recipes.rst | 44 ++++---- pyproject.toml | 1 - scripts/internal/convert_readme.py | 4 +- scripts/internal/print_announce.py | 2 +- 17 files changed, 191 insertions(+), 199 deletions(-) diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py index b24a77922e..b0af035392 100755 --- a/.github/workflows/issues.py +++ b/.github/workflows/issues.py @@ -122,7 +122,7 @@ It looks like you're missing `Python.h` headers. This usually means you have \ to install them first, then retry psutil installation. Please read \ -[install](https://psutil.readthedocs.io/install) \ +[install](https://psutil.readthedocs.io/latest/install) \ instructions for your platform. \ This is an auto-generated response based on the text you submitted. \ If this was a mistake or you think there's a bug with psutil installation \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85fa70ccab..e13b4d5eed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,8 @@ keeping the issues properly organized and searchable (by OS, issue type, etc.). - When reporting a malfunction, consider enabling - [debug mode](https://psutil.readthedocs.io/en/latest/#debug-mode) first. + [debug mode](https://psutil.readthedocs.io/latest/devguide.html#debug-mode) + first. - To report a **security vulnerability**, use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and the disclosure of the reported problem. diff --git a/HISTORY.rst b/HISTORY.rst index eef7a6b80a..54808c578f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,3 @@ History has moved to: -- https://psutil.readthedocs.io/en/latest/changelog.html +- https://psutil.readthedocs.io/latest/changelog.html diff --git a/INSTALL.rst b/INSTALL.rst index 9b1fee68b7..62020237c1 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,3 +1,3 @@ Installation instructions have moved to: -- https://psutil.readthedocs.io/en/latest/install.html +- https://psutil.readthedocs.io/latest/install.html diff --git a/README.rst b/README.rst index c462e28783..cbc92b9577 100644 --- a/README.rst +++ b/README.rst @@ -1,27 +1,22 @@ -| |downloads| |stars| |forks| |contributors| |packages| -| |version| |license| |tidelift| +| |downloads| |contributors| |packages| |version| |license| | |github-actions-wheels| |github-actions-bsd| .. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg :target: https://clickpy.clickhouse.com/dashboard/psutil :alt: Downloads -.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/stargazers - :alt: Github stars +.. .. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg +.. :target: https://github.com/giampaolo/psutil/stargazers +.. :alt: Github stars -.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/network/members - :alt: Github forks +.. .. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg +.. :target: https://github.com/giampaolo/psutil/network/members +.. :alt: Github forks .. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg :target: https://github.com/giampaolo/psutil/graphs/contributors :alt: Contributors -.. |stackoverflow| image:: https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg - :target: https://stackoverflow.com/questions/tagged/psutil - :alt: Stackoverflow - .. |github-actions-wheels| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml.svg?label=Linux%2C%20macOS%2C%20Windows :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild :alt: Linux, macOS, Windows @@ -46,9 +41,9 @@ :target: https://twitter.com/grodola :alt: Twitter Follow -.. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat - :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme - :alt: Tidelift +.. .. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat +.. :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme +.. :alt: Tidelift ----- @@ -60,7 +55,7 @@
    Home    Documentation    - Who uses psutil    + Who uses psutil    Download    Blog    Funding    @@ -125,12 +120,12 @@ immensely from some funding. psutil is among the `top 100`_ most-downloaded Python packages, and keeping up with bug reports, user support, and ongoing maintenance has become increasingly difficult to sustain as a one-person effort. If you're a company that's making significant use of psutil you can -consider becoming a sponsor via `GitHub -`_, `Open Collective -`_ or `PayPal -`_. -Sponsors can have their logo displayed here and in the psutil `documentation -`_. +consider becoming a sponsor via +`GitHub `_, +`Open Collective `_ +or `PayPal `_. +Sponsors can have their logo displayed here and in the psutil +`documentation `_. Projects using psutil ===================== @@ -154,9 +149,7 @@ million downloads per month, `760,000+ GitHub repositories `GRR `_ - `psleak`_ -`Full list `_ - - +`Full list `_ Ports ===== @@ -213,7 +206,7 @@ People who donated money over the years: ..
    ----- +------------------------------------------------------------------------------- Example usages ============== @@ -224,7 +217,7 @@ including CPU, memory, disks, network, sensors, and process management. CPU --- -.. code-block:: pycon +.. code-block:: python >>> import psutil >>> @@ -269,7 +262,7 @@ CPU Memory ------ -.. code-block:: pycon +.. code-block:: python >>> psutil.virtual_memory() svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304) @@ -280,7 +273,7 @@ Memory Disks ----- -.. code-block:: pycon +.. code-block:: python >>> psutil.disk_partitions() [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), @@ -296,7 +289,7 @@ Disks Network ------- -.. code-block:: pycon +.. code-block:: python >>> psutil.net_io_counters(pernic=True) {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0), @@ -323,7 +316,7 @@ Network Sensors ------- -.. code-block:: pycon +.. code-block:: python >>> import psutil >>> psutil.sensors_temperatures() @@ -342,7 +335,7 @@ Sensors Other system info ----------------- -.. code-block:: pycon +.. code-block:: python >>> import psutil >>> psutil.users() @@ -356,7 +349,7 @@ Other system info Process management ------------------ -.. code-block:: pycon +.. code-block:: python >>> import psutil >>> psutil.pids() @@ -499,7 +492,7 @@ Process management Further process APIs -------------------- -.. code-block:: pycon +.. code-block:: python >>> import psutil >>> for proc in psutil.process_iter(['pid', 'name']): @@ -523,7 +516,7 @@ Further process APIs Heap info --------- -.. code-block:: pycon +.. code-block:: python >>> import psutil >>> psutil.heap_info() @@ -535,7 +528,7 @@ See also `psleak`_. Windows services ---------------- -.. code-block:: pycon +.. code-block:: python >>> list(psutil.win_service_iter()) [, @@ -556,5 +549,5 @@ Windows services .. _`psleak`: https://github.com/giampaolo/psleak -.. _`shell equivalents`: https://psutil.readthedocs.io/en/latest/shell_equivalents.html +.. _`shell equivalents`: https://psutil.readthedocs.io/stable/shell-equivalents.html .. _`top 100`: https://clickpy.clickhouse.com/dashboard/psutil diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index 03ab738f20..763475d3fd 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -1,6 +1,6 @@ # .readthedocs.yaml # Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +# See https://docs.readthedocs.io/en/stable/config-file for details version: 2 diff --git a/docs/adoption.rst b/docs/adoption.rst index da370bbd23..000f6b9bd7 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -4,10 +4,10 @@ Who uses psutil =============== psutil is among the `top 100 `__ -most-downloaded packages on PyPI, with 280+ million downloads per month, +most-downloaded packages on PyPI, with 300+ million downloads per month, `760,000+ GitHub repositories `__ using it, and 14,000+ packages depending on it. The projects below are a small -sample of notable software that depends on it. +sample of notable software that depends on psutil. See also :doc:`alternatives` for related Python libraries and equivalents in other languages. @@ -134,7 +134,7 @@ System monitoring * - |grr-logo| `GRR `__ - Remote live forensics by Google - |grr-stars| - - endpoint system data collection, deep integration + - core dependency for system data collection * - |stui-logo| `s-tui `__ - Terminal CPU stress and monitoring utility - |stui-stars| diff --git a/docs/api.rst b/docs/api.rst index 8e3f1de4d0..ab71b32557 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,6 +1,5 @@ .. currentmodule:: psutil .. include:: _links.rst -.. _availability: API reference ============= @@ -77,8 +76,8 @@ CPU .. note:: 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 `#1210 - `_. + this might not be the case (at least on Windows and Linux), see + `#1210 `_. .. versionchanged:: 4.1.0 added **irq** and **dpc** fields on Windows (**irq** was called **interrupt** @@ -225,68 +224,67 @@ CPU .. versionadded:: 4.1.0 - .. function:: cpu_freq(percpu=False) - Return CPU frequency as a named tuple including *current*, *min* and *max* - frequencies expressed in Mhz. On Linux *current* frequency reports the - real-time value, on all other platforms this usually represents the - nominal "fixed" value (never changing). If *percpu* is ``True`` and the - system supports per-cpu frequency retrieval (Linux and FreeBSD), a list of - frequencies is returned for each CPU, if not, a list with a single element - is returned. If *min* and *max* cannot be determined they are set to - ``0.0``. + Return CPU frequency as a named tuple including *current*, *min* and *max* + frequencies expressed in Mhz. On Linux *current* frequency reports the + real-time value, on all other platforms this usually represents the + nominal "fixed" value (never changing). If *percpu* is ``True`` and the + system supports per-cpu frequency retrieval (Linux and FreeBSD), a list of + frequencies is returned for each CPU, if not, a list with a single element + is returned. If *min* and *max* cannot be determined they are set to + ``0.0``. - Example (Linux): + Example (Linux): - .. code-block:: python + .. code-block:: python - >>> import psutil - >>> psutil.cpu_freq() - scpufreq(current=931.42925, min=800.0, max=3500.0) - >>> psutil.cpu_freq(percpu=True) - [scpufreq(current=2394.945, min=800.0, max=3500.0), - scpufreq(current=2236.812, min=800.0, max=3500.0), - scpufreq(current=1703.609, min=800.0, max=3500.0), - scpufreq(current=1754.289, min=800.0, max=3500.0)] + >>> import psutil + >>> psutil.cpu_freq() + scpufreq(current=931.42925, min=800.0, max=3500.0) + >>> psutil.cpu_freq(percpu=True) + [scpufreq(current=2394.945, min=800.0, max=3500.0), + scpufreq(current=2236.812, min=800.0, max=3500.0), + scpufreq(current=1703.609, min=800.0, max=3500.0), + scpufreq(current=1754.289, min=800.0, max=3500.0)] - .. availability:: Linux, macOS, Windows, FreeBSD, OpenBSD. + .. availability:: Linux, macOS, Windows, FreeBSD, OpenBSD. - .. versionadded:: 5.1.0 + .. versionadded:: 5.1.0 - .. versionchanged:: 5.5.1 - added FreeBSD support. + .. versionchanged:: 5.5.1 + added FreeBSD support. - .. versionchanged:: 5.9.1 - added OpenBSD support. + .. versionchanged:: 5.9.1 + added OpenBSD support. .. function:: getloadavg() - Return the average system load over the last 1, 5 and 15 minutes as a tuple. - The "load" represents the processes which are in a runnable state, either - using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). - On UNIX systems this relies on :func:`os.getloadavg`. On Windows this is emulated - by using a Windows API that spawns a thread which keeps running in - background and updates results every 5 seconds, mimicking the UNIX behavior. - Thus, on Windows, the first time this is called and for the next 5 seconds - it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. - The numbers returned only make sense when compared to the number of CPU cores - installed on the system. So, for instance, a value of `3.14` on a system - with 10 logical CPUs means that the system load was 31.4% percent over the - last N minutes. - - .. code-block:: python + Return the average system load over the last 1, 5 and 15 minutes as a tuple. + The "load" represents the processes which are in a runnable state, either + using the CPU or waiting to use the CPU (e.g. waiting for disk I/O). + On UNIX systems this relies on :func:`os.getloadavg`. On Windows this is emulated + by using a Windows API that spawns a thread which keeps running in + background and updates results every 5 seconds, mimicking the UNIX behavior. + Thus, on Windows, the first time this is called and for the next 5 seconds + it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. + The numbers returned only make sense when compared to the number of CPU cores + installed on the system. So, for instance, a value of `3.14` on a system + with 10 logical CPUs means that the system load was 31.4% percent over the + last N minutes. - >>> import psutil - >>> psutil.getloadavg() - (3.14, 3.89, 4.67) - >>> psutil.cpu_count() - 10 - >>> # percentage representation - >>> [x / psutil.cpu_count() * 100 for x in psutil.getloadavg()] - [31.4, 38.9, 46.7] + .. code-block:: python - .. versionadded:: 5.6.2 + >>> import psutil + >>> psutil.getloadavg() + (3.14, 3.89, 4.67) + >>> psutil.cpu_count() + 10 + >>> # percentage representation + >>> [x / psutil.cpu_count() * 100 for x in psutil.getloadavg()] + [31.4, 38.9, 46.7] + + .. versionadded:: 5.6.2 Memory ^^^^^^ @@ -433,7 +431,7 @@ Memory .. seealso:: - `scripts/meminfo.py`_ - :ref:`faq_virtual_memory_available` - - :ref:`faq_used_plus_free` + - :ref:`faq_used_plus_free` .. versionchanged:: 4.2.0 added *shared* metric on Linux. @@ -683,7 +681,7 @@ Network When the remote endpoint is not connected you'll get an empty tuple (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. - **status**: represents the status of a TCP connection. The return value - is one of the `psutil.CONN_* <#connections-constants>`_ constants + is one of the :data:`psutil.CONN_* ` constants (a string). For UDP and UNIX sockets this is always going to be :const:`psutil.CONN_NONE`. @@ -965,7 +963,7 @@ Sensors .. versionchanged:: 5.4.2 added macOS support. ----- +------------------------------------------------------------------------------- Other system info ^^^^^^^^^^^^^^^^^ @@ -1013,7 +1011,7 @@ Other system info .. versionchanged:: 5.3.0 added "pid" field. ----- +------------------------------------------------------------------------------- Processes --------- @@ -1139,7 +1137,9 @@ Functions - give them some time to terminate - send SIGKILL to those ones which are still alive - Example which terminates and waits all the children of this process:: + Example which terminates and waits all the children of this process: + + .. code-block:: python import psutil @@ -1326,16 +1326,16 @@ Process class .. attribute:: pid - The process PID. This is the only (read-only) attribute of the class. + The process PID. This is the only (read-only) attribute of the class. .. attribute:: info - A dict containing pre-fetched process info, set by - :func:`process_iter` when called with ``attrs``. Use method - calls instead (e.g. ``p.name()`` instead of ``p.info['name']``). - Accessing this attribute raises :exc:`DeprecationWarning`. + A dict containing pre-fetched process info, set by + :func:`process_iter` when called with ``attrs``. Use method + calls instead (e.g. ``p.name()`` instead of ``p.info['name']``). + Accessing this attribute raises :exc:`DeprecationWarning`. - .. deprecated:: 8.0.0 + .. deprecated:: 8.0.0 .. method:: ppid() @@ -1349,7 +1349,7 @@ Process class The process name. On Windows the return value is cached after first call. Not on POSIX because the process name may change. - .. seealso:: how to `find a process by name <#find-process-by-name>`_. + .. seealso:: how to :ref:`find a process by name `. .. method:: exe() @@ -1386,7 +1386,6 @@ Process class >>> psutil.Process().environ() {'LC_NUMERIC': 'it_IT.UTF-8', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'IM_CONFIG_PHASE': '1', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/giampaolo', 'XDG_CURRENT_DESKTOP': 'Unity', 'UPSTART_EVENTS': 'started starting', 'GNOME_KEYRING_PID': '', 'XDG_VTNR': '7', 'QT_IM_MODULE': 'ibus', 'LOGNAME': 'giampaolo', 'USER': 'giampaolo', 'PATH': '/home/giampaolo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/giampaolo/svn/sysconf/bin', 'LC_PAPER': 'it_IT.UTF-8', 'GNOME_KEYRING_CONTROL': '', 'GTK_IM_MODULE': 'ibus', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'LESS_TERMCAP_se': '\x1b[0m', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/giampaolo/.Xauthority', 'LANGUAGE': 'en_US', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'LC_MONETARY': 'it_IT.UTF-8', 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON': '1', 'LESS_TERMCAP_me': '\x1b[0m', 'LESS_TERMCAP_md': '\x1b[01;38;5;74m', 'LESS_TERMCAP_mb': '\x1b[01;31m', 'HISTSIZE': '100000', 'UPSTART_INSTANCE': '', 'CLUTTER_IM_MODULE': 'xim', 'WINDOWID': '58786407', 'EDITOR': 'vim', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=ibus', 'GPG_AGENT_INFO': '/home/giampaolo/.gnupg/S.gpg-agent:0:1', 'HOME': '/home/giampaolo', 'HISTFILESIZE': '100000', 'QT4_IM_MODULE': 'xim', 'GTK2_MODULES': 'overlay-scrollbar', 'XDG_SESSION_DESKTOP': 'ubuntu', 'SHLVL': '1', 'XDG_RUNTIME_DIR': '/run/user/1000', 'INSTANCE': 'Unity', 'LC_ADDRESS': 'it_IT.UTF-8', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'VTE_VERSION': '4205', 'GDMSESSION': 'ubuntu', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'VISUAL': 'vim', 'DESKTOP_SESSION': 'ubuntu', 'QT_ACCESSIBILITY': '1', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'LESSCLOSE': '/usr/bin/lesspipe %s %s', 'LESSOPEN': '| /usr/bin/lesspipe %s', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-9GAJpvnt8r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'it_IT.UTF-8', 'LESS_TERMCAP_ue': '\x1b[0m', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1294', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GTK_MODULES': 'gail:atk-bridge:unity-gtk-module', 'XDG_SESSION_TYPE': 'x11', 'PYTHONSTARTUP': '/home/giampaolo/.pythonstart', 'LC_NAME': 'it_IT.UTF-8', 'OLDPWD': '/home/giampaolo/svn/curio_giampaolo/tests', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'it_IT.UTF-8', 'HISTCONTROL': 'ignoredups:erasedups', 'LC_MEASUREMENT': 'it_IT.UTF-8', 'PWD': '/home/giampaolo/svn/curio_giampaolo', 'JOB': 'gnome-session', 'LESS_TERMCAP_us': '\x1b[04;38;5;146m', 'UPSTART_JOB': 'unity-settings-daemon', 'LC_TIME': 'it_IT.UTF-8', 'LESS_TERMCAP_so': '\x1b[38;5;246m', 'PAGER': 'less', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop', 'XDG_SEAT': 'seat0'} - .. note:: on macOS Big Sur this function returns something meaningful only for the current process or in @@ -1473,7 +1472,7 @@ Process class The current process status as a :class:`psutil.ProcessStatus` enum member. The returned value is one of the - `psutil.STATUS_* <#process-status-constants>`_ constants. + :data:`psutil.STATUS_* ` constants. A common use case is detecting :term:`zombie processes ` (``p.status() == psutil.STATUS_ZOMBIE``). @@ -1748,7 +1747,6 @@ Process class >>> sum(p.cpu_times()[:2]) # cumulative, excluding children and iowait 0.70 - .. versionchanged:: 4.1.0 return two extra fields: **children_user** and **children_system**. @@ -2035,7 +2033,7 @@ Process class Return a named tuple with USS, PSS and swap memory metrics. These give a more accurate picture of actual memory consumption than :meth:`memory_info`, as explained in this - `blog post `_. + `blog post `_ It works by walking the full process address space, so it is considerably slower than :meth:`memory_info` and may require elevated privileges. @@ -2165,8 +2163,7 @@ Process class .. availability:: Linux, Windows, FreeBSD, SunOS .. versionchanged:: 5.6.0 - removed macOS support because inherently broken (see issue `#1291 - `_) + removed macOS support because inherently broken (see issue :gh:`1291`) .. method:: children(recursive=False) @@ -2185,12 +2182,12 @@ Process class ├─ C (child) └─ D (child) - .. code-block:: pycon + .. code-block:: pycon - >>> p.children() - B, C, D - >>> p.children(recursive=True) - B, X, Y, C, D + >>> p.children() + B, C, D + >>> p.children(recursive=True) + B, X, Y, C, D Note that in the example above if process X disappears process Y won't be returned either as the reference to process A is lost. @@ -2251,7 +2248,6 @@ Process class >>> p.open_files() [popenfile(path='/home/giampaolo/svn/psutil/file.ext', fd=3, position=0, mode='w', flags=32769)] - .. warning:: on Windows this method is not reliable due to some limitations of the underlying Windows API which may hang when retrieving certain file @@ -2507,7 +2503,7 @@ Process class :func:`select.kqueue` respectively, instead of less efficient busy-loop polling. Later added to CPython 3.15 in `GH-144047`_. ----- +------------------------------------------------------------------------------- Popen class ^^^^^^^^^^^ @@ -2540,11 +2536,10 @@ Popen class 0 >>> - .. versionchanged:: 4.4.0 added context manager support. ----- +------------------------------------------------------------------------------- C heap introspection -------------------- @@ -2581,11 +2576,11 @@ Python's memory tracking misses. - ``heap_count``: (Windows only) number of private heaps created via ``HeapCreate()``. - .. code-block:: pycon + .. code-block:: pycon - >>> import psutil - >>> psutil.heap_info() - pheap(heap_used=5177792, mmap_used=819200) + >>> import psutil + >>> psutil.heap_info() + pheap(heap_used=5177792, mmap_used=819200) These fields reflect how unreleased C allocations affect the heap: @@ -2626,7 +2621,7 @@ Python's memory tracking misses. .. versionadded:: 7.2.0 ----- +------------------------------------------------------------------------------- Windows services ---------------- @@ -2681,7 +2676,7 @@ Windows services .. method:: pid() - The process PID, if any, else `None`. This can be passed to + The process PID, if any, else ``None``. This can be passed to :class:`Process` class to control the service's process. .. method:: status() @@ -2724,7 +2719,7 @@ Windows services .. versionadded:: 4.2.0 ----- +------------------------------------------------------------------------------- Constants --------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 1ac94ee991..eb55decbce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,7 +14,7 @@ Changelog Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, -:gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`) +:gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`, :gh:`2790`) - Split docs from a single HTML file into multiple sections (API reference, install, etc.). diff --git a/docs/devguide.rst b/docs/devguide.rst index d981dec6e6..913c4bde98 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -10,12 +10,12 @@ Build, setup and running tests .. code-block:: bash - git clone git@github.com:giampaolo/psutil.git - make install-sysdeps # install gcc and python headers - make install-pydeps-test # install python deps necessary to run unit tests - make build - make install - make test + git clone git@github.com:giampaolo/psutil.git + make install-sysdeps # install gcc and python headers + make install-pydeps-test # install python deps necessary to run unit tests + make build + make install + make test - ``make`` (and the accompanying `Makefile`_) is the designated tool to build, install, run tests and do pretty much anything that involves development, @@ -23,22 +23,22 @@ Build, setup and running tests .. code-block:: bash - make clean # remove build files - make install-pydeps-dev # install all development deps (ruff, black, coverage, ...) - make test # run tests - make test-parallel # run tests in parallel (faster) - make test-memleaks # run memory leak tests - make test-coverage # run test coverage - make lint-all # run linters - make fix-all # fix linters errors - make uninstall - make help + make clean # remove build files + make install-pydeps-dev # install all development deps (ruff, black, coverage, ...) + make test # run tests + make test-parallel # run tests in parallel (faster) + make test-memleaks # run memory leak tests + make test-coverage # run test coverage + make lint-all # run linters + make fix-all # fix linters errors + make uninstall + make help - To run a specific unit test: .. code-block:: - make test ARGS=tests/test_system.py + make test ARGS=tests/test_system.py - Do not use ``sudo``. ``make install`` installs psutil as a limited user in "edit" / development mode, meaning you can edit psutil code on the fly while @@ -48,7 +48,7 @@ Build, setup and running tests .. code-block:: - make test PYTHON=python3.8 + make test PYTHON=python3.8 Windows ------- @@ -61,8 +61,10 @@ Windows .. code-block:: bash - make build - make test-parallel + make build + make test-parallel + +.. _devguide_debug_mode: Debug mode ---------- @@ -106,16 +108,16 @@ Code organization .. code-block:: bash - psutil/__init__.py # Main API namespace ("import psutil") - psutil/_common.py # Generic utilities - psutil/_ntuples.py # Named tuples returned by psutil APIs - psutil/_enums.py # Enum containers backing psutil constants - psutil/_ps{platform}.py # Platform-specific python wrappers - psutil/_psutil_{platform}.c # Platform-specific C extensions (entry point) - psutil/arch/all/*.c # C code common to all platforms - psutil/arch/{platform}/*.c # Platform-specific C extension - tests/test_process|system.py # Main system/process API tests - tests/test_{platform}.py # Platform-specific tests + psutil/__init__.py # Main API namespace ("import psutil") + psutil/_common.py # Generic utilities + psutil/_ntuples.py # Named tuples returned by psutil APIs + psutil/_enums.py # Enum containers backing psutil constants + psutil/_ps{platform}.py # Platform-specific python wrappers + psutil/_psutil_{platform}.c # Platform-specific C extensions (entry point) + psutil/arch/all/*.c # C code common to all platforms + psutil/arch/{platform}/*.c # Platform-specific C extension + tests/test_process|system.py # Main system/process API tests + tests/test_{platform}.py # Platform-specific tests Adding a new API ---------------- @@ -163,13 +165,13 @@ Documentation .. code-block:: bash - cd docs/ - python3 -m pip install -r requirements.txt - make html + cd docs/ + python3 -m pip install -r requirements.txt + make html - The public documentation is hosted at https://psutil.readthedocs.io. - There are 2 versions, which you can select from the dropdown menu at the top - of the page: + left of the page: - `/stable `_: generated from the most recent Git tag (latest released psutil version). diff --git a/docs/faq.rst b/docs/faq.rst index 7f0bc1498a..592293c0e0 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -152,7 +152,7 @@ process call ``wait()`` (or ``waitpid()``). If the parent never does this, killing the parent will cause the zombie to be re-parented to ``init`` / ``systemd``, which will reap it automatically. ----- +------------------------------------------------------------------------------- Processes --------- @@ -203,7 +203,7 @@ against reuse (it's faster). Use :meth:`Process.is_running` when you hold a :class:`Process` object and want to confirm it still refers to the same process. ----- +------------------------------------------------------------------------------- CPU --- @@ -256,8 +256,8 @@ stays in the 0–100% range because it averages across all cores. .. _faq_cpu_count: -What is the difference between psutil, os, and multiprocessing cpu_count? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +What is the difference between psutil, os, and multiprocessing cpu_count()? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - :func:`os.cpu_count` returns the number of **logical** CPUs (including hyperthreads). It is the same as ``psutil.cpu_count(logical=True)``, but @@ -272,7 +272,7 @@ What is the difference between psutil, os, and multiprocessing cpu_count? - :func:`psutil.cpu_count` with ``logical=False`` returns the number of **physical** cores, which has no stdlib equivalent. ----- +------------------------------------------------------------------------------- Memory ------ diff --git a/docs/migration.rst b/docs/migration.rst index d7a42c9867..233b416d65 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -218,7 +218,7 @@ Python 3.6 dropped Python 3.6 is no longer supported. Minimum version is Python 3.7. ----- +------------------------------------------------------------------------------- .. _migration-7.0: @@ -250,7 +250,7 @@ Python 2.7 is no longer supported. The last release to support Python pip2 install "psutil==6.1.*" ----- +------------------------------------------------------------------------------- .. _migration-6.0: @@ -306,7 +306,7 @@ that a process object is still alive and refers to the same process, use if p.is_running(): print(p.pid, p.name()) ----- +------------------------------------------------------------------------------- .. _migration-5.0: diff --git a/docs/performance.rst b/docs/performance.rst index 7d9e36decc..51a788469a 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -87,7 +87,7 @@ are handled internally. .. _perf-pids: Avoid pids() + loop ---------------------- +------------------- A common but inefficient pattern is to call :func:`pids` and then construct a :class:`Process` for each PID manually: diff --git a/docs/recipes.rst b/docs/recipes.rst index e5f64d5f84..852da3b510 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -17,6 +17,8 @@ Processes Finding processes ^^^^^^^^^^^^^^^^^ +.. _recipe_find_process_by_name: + Find process by name: .. code-block:: python @@ -30,7 +32,7 @@ Find process by name: ls.append(p) return ls ----- +------------------------------------------------------------------------------- A bit more advanced, check string against :meth:`Process.name`, :meth:`Process.exe` and :meth:`Process.cmdline`: @@ -51,7 +53,7 @@ A bit more advanced, check string against :meth:`Process.name`, ls.append(p) return ls ----- +------------------------------------------------------------------------------- Find the process listening on a given TCP port: @@ -71,7 +73,7 @@ Find the process listening on a given TCP port: return proc return None ----- +------------------------------------------------------------------------------- Find all processes that have an active connection to a given remote IP: @@ -92,7 +94,7 @@ Find all processes that have an active connection to a given remote IP: ls.append(proc) return ls ----- +------------------------------------------------------------------------------- Find all processes that have a given file open (useful on Windows): @@ -124,7 +126,7 @@ Processes owned by user: (19772, 'ssh'), (20492, 'python')] ----- +------------------------------------------------------------------------------- Processes actively running: @@ -135,7 +137,7 @@ Processes actively running: (1776, 'unity-panel-service'), (20492, 'python')] ----- +------------------------------------------------------------------------------- Processes using log files: @@ -150,7 +152,7 @@ Processes using log files: 2174 nautilus /home/giampaolo/.local/share/gvfs-metadata/home-ce08efac.log 2650 chrome /home/giampaolo/.config/google-chrome/Default/data_reduction_proxy_leveldb/000003.log ----- +------------------------------------------------------------------------------- Processes consuming more than 500M of memory: @@ -161,7 +163,7 @@ Processes consuming more than 500M of memory: (3038, 'chrome', 1120088064), (21915, 'sublime_text', 615407616)] ----- +------------------------------------------------------------------------------- Top 3 processes which consumed the most CPU time: @@ -172,7 +174,7 @@ Top 3 processes which consumed the most CPU time: (1150, 'Xorg', 11116.989999999998), (2650, 'chrome', 18451.97)] ----- +------------------------------------------------------------------------------- Top N processes by cumulative disk read + write bytes (similar to ``iotop``): @@ -193,7 +195,7 @@ Top N processes by cumulative disk read + write bytes (similar to ``iotop``): procs.sort(key=lambda x: x[0], reverse=True) return procs[:n] ----- +------------------------------------------------------------------------------- Top N processes by open file descriptors (useful for diagnosing fd leaks): @@ -278,7 +280,7 @@ Kill a process tree (including grandchildren): ) return (gone, alive) ----- +------------------------------------------------------------------------------- Find zombie (defunct) processes: @@ -291,7 +293,7 @@ Find zombie (defunct) processes: if p.status() == psutil.STATUS_ZOMBIE: print(f"zombie: pid={p.pid}") ----- +------------------------------------------------------------------------------- Terminate all processes matching a given name: @@ -304,7 +306,7 @@ Terminate all processes matching a given name: if p.name() == name: p.terminate() ----- +------------------------------------------------------------------------------- Terminate a process gracefully, falling back to ``SIGKILL`` if it does not exit within the timeout: @@ -321,7 +323,7 @@ exit within the timeout: except psutil.TimeoutExpired: p.kill() ----- +------------------------------------------------------------------------------- Restart a process: @@ -337,7 +339,7 @@ Restart a process: p.wait() return subprocess.Popen(cmd) ----- +------------------------------------------------------------------------------- Temporarily pause and resume a process using a context manager: @@ -359,7 +361,7 @@ Temporarily pause and resume a process using a context manager: with suspended(pid): pass # process is paused here ----- +------------------------------------------------------------------------------- CPU throttle: limit a process's CPU usage to a target percentage by alternating :meth:`Process.suspend` and :meth:`Process.resume`: @@ -379,7 +381,7 @@ alternating :meth:`Process.suspend` and :meth:`Process.resume`: time.sleep(interval * cpu / max_cpu_percent) p.resume() ----- +------------------------------------------------------------------------------- Restart a process automatically if it dies: @@ -501,7 +503,7 @@ Print real-time CPU usage percentage: CPU: 1.4% CPU: 0.9% ----- +------------------------------------------------------------------------------- For each CPU core: @@ -544,7 +546,7 @@ Show disk usage for all mounted partitions: /home total=878.7G used=497.5G free=336.5G percent=59.7% ----- +------------------------------------------------------------------------------- Show real-time disk I/O: @@ -590,7 +592,7 @@ List IP addresses for each network interface: lo address=127.0.0.1 netmask=255.0.0.0 eth0 address=10.0.0.4 netmask=255.255.255.0 ----- +------------------------------------------------------------------------------- Show real-time network I/O per interface: @@ -618,7 +620,7 @@ Show real-time network I/O per interface: lo sent=0.0B/s recv=0.0B/s eth0 sent=12.3K/s recv=45.6K/s ----- +------------------------------------------------------------------------------- List all active TCP connections with their status: diff --git a/pyproject.toml b/pyproject.toml index 6069696a4b..b0818a2a0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,6 @@ target-version = ["py37"] line-length = 79 skip-string-normalization = true -# https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html preview = true enable-unstable-feature = ["hug_parens_with_braces_and_square_brackets", "string_processing", "wrap_long_dict_values_in_parens"] diff --git a/scripts/internal/convert_readme.py b/scripts/internal/convert_readme.py index 6f7844481d..cb5193052d 100755 --- a/scripts/internal/convert_readme.py +++ b/scripts/internal/convert_readme.py @@ -17,10 +17,10 @@ - `Home page `_ - `Documentation `_ -- `Who uses psutil `_ +- `Who uses psutil `_ - `Download `_ - `Blog `_ -- `What's new `_ +- `What's new `_ """ diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 2f931ed090..0239d465e7 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -25,7 +25,7 @@ PRJ_URL_HOME = 'https://github.com/giampaolo/psutil' PRJ_URL_DOC = 'http://psutil.readthedocs.io' PRJ_URL_DOWNLOAD = 'https://pypi.org/project/psutil/#files' -PRJ_URL_WHATSNEW = 'https://psutil.readthedocs.io/en/latest/changelog.html' +PRJ_URL_WHATSNEW = 'https://psutil.readthedocs.io/latest/changelog.html' template = """\ Hello all, From bd0b8ac9f724edc68bce14a540256c4894f6fe94 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Mar 2026 16:03:34 +0100 Subject: [PATCH 1647/1714] Add FAQ about named tuples --- docs/DEVNOTES | 13 ---- docs/_static/css/custom.css | 12 ++-- docs/alternatives.rst | 8 +-- docs/api.rst | 4 ++ docs/credits.rst | 8 +-- docs/devguide.rst | 23 ++++--- docs/faq.rst | 31 ++++++++- docs/migration.rst | 132 +++++++++++------------------------- 8 files changed, 102 insertions(+), 129 deletions(-) diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 1453cb2038..1e063cf680 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -38,16 +38,6 @@ FEATURES - Number of system threads. - Windows: http://msdn.microsoft.com/en-us/library/windows/desktop/ms684824(v=vs.85).aspx -- Doc / wiki which compares similarities between UNIX cli tools and psutil. - Example: - ``` - df -a -> psutil.disk_partitions - lsof -> psutil.Process.open_files() and psutil.Process.net_connections() - killall-> (actual script) - tty -> psutil.Process.terminal() - who -> psutil.users() - ``` - - psutil.proc_tree() something which obtains a {pid:ppid, ...} dict for all running processes in one shot. This can be factored out from Process.children() and exposed as a first class function. @@ -70,8 +60,6 @@ FEATURES http://docs.python.org/dev/library/os.html#interface-to-the-scheduler It might be worth to take a look and figure out whether we can include some of those in psutil. - Also, we can probably reimplement wait_pid() on POSIX which is currently - implemented as a busy-loop. - os.times() provides 'elapsed' times (cpu_times() might). @@ -123,7 +111,6 @@ INCONSISTENCIES RESOURCES ========= -- conky: https://github.com/brndnmtthws/conky/ - sigar: https://github.com/hyperic/sigar (Java) - zabbix: https://zabbix.org/wiki/Get_Zabbix - libstatgrab: http://www.i-scream.org/libstatgrab/ diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index f0b43b6345..05f94ea325 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -203,7 +203,8 @@ div.warning { border-radius: 3px !important; } -div.note { +div.note, +div.important { background-color: #eee !important; border: 1px solid #ccc !important; border-radius: 3px !important; @@ -246,16 +247,19 @@ p.admonition-title:after { margin-bottom: 5px !important; } -.admonition.note { +.admonition.note, +.admonition.important { padding-top: 5px !important; padding-bottom: 5px !important; } -.admonition.note p { +.admonition.note p, +.admonition.important p { margin-bottom: 5px !important; } -.note code { +.note code, +.important code { background: #d6d6d6 !important; } diff --git a/docs/alternatives.rst b/docs/alternatives.rst index 5456c525cd..d1090fd9ac 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -24,7 +24,7 @@ only need information about the *current* process and don't need cross-platform code. psutil goes further in several directions. Its primary goal is to provide a -**single portable interface** for concepts that are natively UNIX-only. Things +**single portable interface** for concepts that are traditionally UNIX-only. Things like process CPU and memory usage, open file descriptors, network connections, signals, nice levels, and I/O counters exist as first-class OS primitives on Linux and macOS, but have no direct equivalent on Windows. psutil implements @@ -32,7 +32,7 @@ all of them on Windows too (using Win32 APIs, ``NtQuerySystemInformation`` and WMI) so that code written against psutil runs unmodified on every supported platform. Beyond portability, it also exposes the same information for *any* process (not just the current one), and returns structured named tuples instead -of raw integers. +of raw values. resource module ^^^^^^^^^^^^^^^ @@ -49,8 +49,8 @@ to all processes, not just the caller. subprocess module ^^^^^^^^^^^^^^^^^ -Calling ``ps``, ``top``, ``netstat``, ``vmstat`` via -:mod:`subprocess` and parsing the text output is fragile: output +Calling tools like ``ps``, ``top``, ``netstat``, ``vmstat`` via +:mod:`subprocess` and parsing their output is fragile: output formats differ across OS versions and locales, parsing is error-prone, and spawning a subprocess per sample is slow. psutil reads the same kernel data sources directly without spawning any external processes. diff --git a/docs/api.rst b/docs/api.rst index ab71b32557..b92ecb5da0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -8,6 +8,10 @@ API reference psutil 8.0 introduces breaking API changes. See the :ref:`migration guide ` if upgrading from 7.x. +.. important:: + Do not rely on positional unpacking of named tuples. + Always use attribute access (e.g. ``t.rss``). + .. contents:: :local: :depth: 5 diff --git a/docs/credits.rst b/docs/credits.rst index 8d0c990770..38eab06c18 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -15,8 +15,8 @@ Top contributors ---------------- * `Giampaolo Rodola`_: creator, primary author and long-time maintainer -* `Jay Loden`_: original co-author, initial design and bootstrap, original - macOS / Windows / FreeBSD implementations +* `Jay Loden`_: original co-author, initial design and project bootstrap, + original macOS / Windows / FreeBSD implementations * `Arnon Yaari`_: AIX implementation * `Landry Breuil`_: original OpenBSD implementation * `Ryo Onodera`_ and `Thomas Klausner`_: original NetBSD implementation @@ -24,8 +24,8 @@ Top contributors Donations --------- -The following individuals and organizations have supported -psutil development through donations. +The following individuals and organizations have supported psutil +development through donations. Companies: diff --git a/docs/devguide.rst b/docs/devguide.rst index 913c4bde98..fcf0690a28 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -1,8 +1,8 @@ Development guide ================= -Build, setup and running tests ------------------------------- +Build, setup and test +--------------------- - psutil makes extensive use of C extension modules, meaning a C compiler is required, see :doc:`install instructions `. Once you have a compiler @@ -17,9 +17,9 @@ Build, setup and running tests make install make test -- ``make`` (and the accompanying `Makefile`_) is the designated tool to build, - install, run tests and do pretty much anything that involves development, - including on Windows. Some useful commands: +- ``make`` (and the accompanying `Makefile`_) is the designated tool for + building, installing, running tests, and general development tasks, + including on Windows (see later). .. code-block:: bash @@ -53,8 +53,8 @@ Build, setup and running tests Windows ------- -- The recommended way to develop on Windows is using ``make``, just like on - UNIX systems. +- The recommended way to develop on Windows is to use ``make``, just like + on UNIX systems. - First, install `Git for Windows`_ and launch a **Git Bash shell**. This provides a Unix-like environment where ``make`` works. - Once inside Git Bash, you can run the usual ``make`` commands: @@ -69,8 +69,8 @@ Windows Debug mode ---------- -If you want to debug unusual situations or want to report a bug, it may be -useful to enable debug mode via ``PSUTIL_DEBUG`` environment variable. In this +If you need to debug unusual situations or report a bug, you can enable +debug mode via the ``PSUTIL_DEBUG`` environment variable. In this mode, psutil may print additional information to stderr. Usually these are non-severe error conditions that are ignored instead of causing a crash. Unit tests automatically run with debug mode enabled. On UNIX: @@ -183,6 +183,11 @@ Redirects: - https://psutil.readthedocs.io redirects to `/stable `_ by default. +.. note:: + + The ``/latest`` version reflects the development branch and may contain + unreleased changes. For stable documentation, use ``/stable``. + Releases -------- diff --git a/docs/faq.rst b/docs/faq.rst index 592293c0e0..3e2ba56b34 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -9,6 +9,33 @@ This section answers common questions and pitfalls when using psutil. :local: :depth: 3 +General +------- + +.. _faq_named_tuple_unpacking: + +Why should I avoid positional unpacking of named tuples? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Most psutil functions return named tuples. It is tempting to unpack them +positionally, but **field order may change across major releases** (as +happened in 8.0 with :func:`cpu_times` and :meth:`Process.memory_info`). +Always use attribute access instead: + +.. code-block:: python + + # bad: breaks if field order changes + rss, vms = p.memory_info() + + # good: safe across versions + m = p.memory_info() + print(m.rss, m.vms) + +See the :ref:`migration guide ` for the full list of +field-order changes in 8.0. + +------------------------------------------------------------------------------- + Exceptions ---------- @@ -60,7 +87,7 @@ Why do I get NoSuchProcess? :exc:`NoSuchProcess` is raised when a process no longer exists. The most common cause is a TOCTOU (time-of-check / time-of-use) race condition: a process can die between the moment its PID is obtained and -the moment it is queried. The following 2 naive patterns are racy: +the moment it is queried. The following two naive patterns are racy: .. code-block:: python @@ -125,7 +152,7 @@ for a :term:`zombie process`. **What you can and cannot do with a zombie:** -- A zombie can be instantiated via :class:`Process` (pid) without error. +- A zombie process can be instantiated via :class:`Process` (pid) without error. - :meth:`Process.status` always returns :data:`STATUS_ZOMBIE`. - :meth:`Process.is_running` and :func:`pid_exists` return ``True``. - The zombie appears in :func:`process_iter` and :func:`pids`. diff --git a/docs/migration.rst b/docs/migration.rst index 233b416d65..0a0d3f1055 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -20,6 +20,19 @@ release and shows the code changes required to upgrade. Migrating to 8.0 ----------------- +Key breaking changes in 8.0: + +- ``Process.info`` is deprecated: use direct methods. +- Named tuple field order changed: stop positional unpacking. +- Some return types are now enums instead of strings. +- ``memory_full_info()`` deprecated: use ``memory_footprint()`` +- Python 3.6 dropped. + +.. important:: + + Do not rely on positional unpacking of named tuples. + Always use attribute access (e.g. ``t.rss``). + process_iter(): p.info is deprecated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -39,38 +52,12 @@ dict. ``p.info`` still works but raises :exc:`DeprecationWarning`: When ``attrs`` are specified, method calls return cached values (no extra syscall), and :exc:`AccessDenied` / :exc:`ZombieProcess` -are handled transparently (returning the ``ad_value``, which defaults -to ``None``): - -.. code-block:: python - - # before - for p in psutil.process_iter(attrs=["exe"], ad_value="access-denied"): - print(p.info["exe"]) - - # after - for p in psutil.process_iter(attrs=["exe"], ad_value="access-denied"): - print(p.exe()) +are handled transparently (returning ``ad_value``, default ``None``). .. note:: - - This is a silent behavior change. Before, calling ``p.exe()`` - directly could raise :exc:`AccessDenied`. Now, if ``"exe"`` was - pre-fetched via ``attrs``, the same call returns ``ad_value`` - (default ``None``) instead. Code that relied on catching - exceptions will silently stop seeing them: - - .. code-block:: python - - # this no longer raises AccessDenied if "exe" was prefetched - for p in psutil.process_iter(attrs=["exe"]): - try: - print(p.exe()) - except psutil.AccessDenied: - pass # never reached - - If you need the exception, do not include the method in ``attrs``, - or call it on a fresh :class:`Process` instance. + If ``"name"`` was pre-fetched via ``attrs``, calling ``p.name()`` no + longer raises :exc:`AccessDenied`. It returns ``ad_value`` instead. + If you need the exception, do not include the method in ``attrs``. Named tuple field order changed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -90,41 +77,19 @@ Named tuple field order changed t = psutil.cpu_times() user, system, idle = t.user, t.system, t.idle -- :meth:`Process.memory_info`: - - - The returned named tuple changed size and field - order. Positional access (e.g. ``p.memory_info()[3]`` or ``a, b, c = - p.memory_info()``) may break or silently return the wrong field. Always use - attribute access instead (e.g. ``p.memory_info().rss``). Also, ``lib`` and ``dirty`` on Linux were removed and turned into aliases emitting `DeprecationWarning`. - - .. code-block:: python - - # Linux before - rss, vms, shared, text, lib, data, dirty = p.memory_info() - - # Linux after - t = p.memory_info() - rss, vms, shared, text, data = t.rss, t.vms, t.shared, t.text, t.data - - - macOS: ``pfaults`` and ``pageins`` fields were removed with **no - backward-compatible aliases**. Use :meth:`page_faults` instead. - - .. code-block:: python - - # before - rss, vms, pfaults, pageins = p.memory_info() - - # after - rss, vms = p.memory_info() - minor, major = p.page_faults() - - - Windows: eliminated old aliases: ``wset`` → ``rss``, ``peak_wset`` → - ``peak_rss``, ``pagefile`` / ``private`` → ``vms``, ``peak_pagefile`` → - ``peak_vms``, ``num_page_faults`` → :meth:`page_faults` method. At the same - time ``paged_pool``, ``nonpaged_pool``, ``peak_paged_pool``, - ``peak_nonpaged_pool`` were moved to :meth:`memory_info_ex`. All these old - names still work but raise `DeprecationWarning`. - +- :meth:`Process.memory_info`: the returned named tuple changed size + and field order. Always use attribute access (e.g. + ``p.memory_info().rss``) instead of positional unpacking. + + - Linux: ``lib`` and ``dirty`` fields removed (aliases emitting + :exc:`DeprecationWarning` are kept). + - macOS: ``pfaults`` and ``pageins`` removed with **no aliases**. + Use :meth:`Process.page_faults` instead. + - Windows: old aliases (``wset``, ``peak_wset``, ``pagefile``, + ``private``, ``peak_pagefile``, ``num_page_faults``) were + renamed. Old names still work but raise :exc:`DeprecationWarning`. + ``paged_pool``, ``nonpaged_pool``, ``peak_paged_pool``, + ``peak_nonpaged_pool`` were moved to :meth:`memory_info_ex`. - BSD: a new ``peak_rss`` field was added. - :func:`virtual_memory`: on Windows, new ``cached`` and ``wired`` fields were @@ -165,29 +130,10 @@ Status and connection fields are now enums now returns a :class:`psutil.ConnectionStatus` member instead of a plain ``str``. -Because both are :class:`enum.StrEnum` subclasses they compare equal to their -string values, so existing comparisons continue to work unchanged: - -.. code-block:: python - - # these still work - p.status() == "running" - p.status() == psutil.STATUS_RUNNING - - # repr() and type() differ, so code inspecting these may need updating - - -The individual constants (e.g. :data:`psutil.STATUS_RUNNING`) are kept as -aliases for the enum members, and should be preferred over accessing them via -the enum class: - -.. code-block:: python - - # prefer this - p.status() == psutil.STATUS_RUNNING - - # not this - p.status() == psutil.ProcessStatus.STATUS_RUNNING +Because both are :class:`enum.StrEnum` subclasses they compare equal to +their string values, so existing comparisons like +``p.status() == psutil.STATUS_RUNNING`` continue to work unchanged. +Code inspecting ``repr()`` or ``type()`` may need updating. memory_full_info() is deprecated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -205,6 +151,11 @@ memory_full_info() is deprecated mem = p.memory_footprint() uss = mem.uss +Python 3.6 dropped +^^^^^^^^^^^^^^^^^^^^ + +Python 3.6 is no longer supported. Minimum version is Python 3.7. + Git tags renamed ^^^^^^^^^^^^^^^^^ @@ -213,11 +164,6 @@ Git tags were renamed from ``release-X.Y.Z`` to ``vX.Y.Z`` backward compatibility. If you reference psutil tags in scripts or URLs, update them to the new format. See :gh:`2788`. -Python 3.6 dropped -^^^^^^^^^^^^^^^^^^^^ - -Python 3.6 is no longer supported. Minimum version is Python 3.7. - ------------------------------------------------------------------------------- .. _migration-7.0: From 29349f42dc8a4a20170e1b06b893f4ae7220bacf Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Mar 2026 19:25:28 +0100 Subject: [PATCH 1648/1714] Fix long HTML tables (also on mobile) --- README.rst | 10 ++++----- docs/DEVNOTES | 44 +++++++++++++++++++------------------ docs/_static/css/custom.css | 16 ++++++++++++++ docs/api.rst | 8 +++---- docs/faq.rst | 15 +++++++------ docs/shell-equivalents.rst | 14 ++++++++++++ docs/stdlib-equivalents.rst | 22 +++++++++---------- 7 files changed, 81 insertions(+), 48 deletions(-) diff --git a/README.rst b/README.rst index cbc92b9577..42b88e400c 100644 --- a/README.rst +++ b/README.rst @@ -495,12 +495,12 @@ Further process APIs .. code-block:: python >>> import psutil - >>> for proc in psutil.process_iter(['pid', 'name']): - ... print(proc.info) + >>> for p in psutil.process_iter(['pid', 'name']): + ... print(p.pid, p.name()) ... - {'pid': 1, 'name': 'systemd'} - {'pid': 2, 'name': 'kthreadd'} - {'pid': 3, 'name': 'ksoftirqd/0'} + 1 systemd + 2 kthreadd + 3 ksoftirqd/0 ... >>> >>> psutil.pid_exists(3) diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 1e063cf680..6f4225671b 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -2,8 +2,29 @@ TODO ==== A collection of ideas and notes about stuff to implement in future versions. -"#NNN" occurrences refer to bug tracker issues at: -https://github.com/giampaolo/psutil/issues + +INCONSISTENCIES +=============== + +(the "too late" section) + +- PROCFS_PATH should have been set_procfs_path(). + +- `virtual_memory()` should have been `memory_virtual()`. + +- `swap_memory()` should have been `memory_swap()`. + +- Named tuples are problematic. Positional unpacking of named tuples could be + deprecated. Return frozen dataclasses (with a common base class) instead of + `typing.NamedTuple`. The base class would keep `_fields`, `_asdict()` and + `len()` for compat, but `__iter__` and `__getitem__` would emit + DeprecationWarning. Main concern: `isinstance(x, tuple)` would break. + +REJECTED IDEAS +============== + +- #550: threads per core +- #1667: process_iter(new_only=True) FEATURES ======== @@ -90,31 +111,12 @@ FEATURES - round Process.memory_percent() result? -BUGFIXES -======== - -- #600: windows / open_files(): support network file handles. - -REJECTED IDEAS -============== - -- #550: threads per core -- #1667: process_iter(new_only=True) - -INCONSISTENCIES -=============== - -- PROCFS_PATH should have been set_procfs_path(). -- `virtual_memory()` should have been `memory_virtual()`. -- `swap_memory()` should have been `memory_swap()`. - RESOURCES ========= - sigar: https://github.com/hyperic/sigar (Java) - zabbix: https://zabbix.org/wiki/Get_Zabbix - libstatgrab: http://www.i-scream.org/libstatgrab/ -- top: http://www.unixtop.org/ - oshi: https://github.com/oshi/oshi - netdata: https://github.com/netdata/netdata diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 05f94ea325..23ded42bad 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -69,6 +69,20 @@ h1 { margin-bottom: 0px !important; } +/* "wide-table" class: force horizontal scroll on narrow + screens instead of squishing columns */ +@media (max-width: 768px) { + table.wide-table { + min-width: 700px; + table-layout: auto !important; + } + + table.wide-table td, + table.wide-table th { + white-space: nowrap; + } +} + /* "longtable" class (used by alternatives.rst): fixed layout with extra padding for multi-line cells */ .wy-table-responsive table.longtable { @@ -81,8 +95,10 @@ h1 { padding: 10px !important; white-space: normal !important; word-wrap: break-word; + overflow-wrap: break-word; } + /* adoption page logos */ .document td img[alt$="-logo"] { height: 20px !important; diff --git a/docs/api.rst b/docs/api.rst index b92ecb5da0..0fb492a7d0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -464,10 +464,10 @@ Memory from RAM into swap space) since boot. A continuously increasing **sout** is a sign of memory pressure. See :term:`swap-out`. - **sin** and **sout** are :term:`cumulative counters ` since boot; monitor - their rate of change rather than the absolute value to detect active - swapping. See :term:`swap-in` and :term:`swap-out`. - On Windows both are always ``0``. + **sin** and **sout** are :term:`cumulative counters ` + since boot; monitor their rate of change rather than the absolute value to + detect active swapping. See :term:`swap-in` and :term:`swap-out`. On Windows + both are always ``0``. .. code-block:: pycon diff --git a/docs/faq.rst b/docs/faq.rst index 3e2ba56b34..3d0533dc01 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -24,10 +24,10 @@ Always use attribute access instead: .. code-block:: python - # bad: breaks if field order changes + # bad rss, vms = p.memory_info() - # good: safe across versions + # good m = p.memory_info() print(m.rss, m.vms) @@ -67,17 +67,18 @@ You have two options to deal with it. except (psutil.AccessDenied, psutil.NoSuchProcess): pass -- Option 2: use :func:`process_iter` with a list of attribute names to pre-fetch. - If fetching an attribute raises :exc:`AccessDenied` internally, the - corresponding method returns ``None`` (or the ``ad_value`` argument, if - specified): +- Option 2: use :func:`process_iter` with a list of attribute names to + pre-fetch. Both :exc:`AccessDenied` and :exc:`NoSuchProcess` are handled + internally: the corresponding method returns ``None`` (or ``ad_value``) + instead of raising. This also avoids the race condition where a process + disappears between iteration and method call: .. code-block:: python import psutil for p in psutil.process_iter(["name", "username"], ad_value="N/A"): - print(p.username()) # may print "N/A" + print(p.name(), p.username()) # no try/except needed .. _faq_no_such_process: diff --git a/docs/shell-equivalents.rst b/docs/shell-equivalents.rst index 7017826d25..02c722acb7 100644 --- a/docs/shell-equivalents.rst +++ b/docs/shell-equivalents.rst @@ -16,6 +16,7 @@ CPU .. list-table:: :header-rows: 1 + :class: wide-table * - psutil function - Linux @@ -78,6 +79,7 @@ Memory .. list-table:: :header-rows: 1 + :class: wide-table * - psutil function - Linux @@ -110,6 +112,7 @@ Disks .. list-table:: :header-rows: 1 + :class: wide-table * - psutil function - Linux @@ -137,6 +140,7 @@ Network .. list-table:: :header-rows: 1 + :class: wide-table * - psutil function - Linux @@ -169,6 +173,7 @@ Sensors .. list-table:: :header-rows: 1 + :class: wide-table * - psutil function - Linux @@ -196,6 +201,7 @@ Other .. list-table:: :header-rows: 1 + :class: wide-table * - psutil function - Linux @@ -231,6 +237,7 @@ Identity .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux @@ -283,6 +290,7 @@ Process tree .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux @@ -320,6 +328,7 @@ Credentials .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux @@ -352,6 +361,7 @@ CPU / scheduling .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux @@ -424,6 +434,7 @@ Memory .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux @@ -466,6 +477,7 @@ Threads .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux @@ -488,6 +500,7 @@ Files and connections .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux @@ -525,6 +538,7 @@ Signals .. list-table:: :header-rows: 1 + :class: wide-table * - psutil method - Linux diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst index e45a4e6c02..ce6902d3b9 100644 --- a/docs/stdlib-equivalents.rst +++ b/docs/stdlib-equivalents.rst @@ -20,7 +20,7 @@ CPU ~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 20 37 100 @@ -45,7 +45,7 @@ Disk ~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 25 24 100 @@ -68,7 +68,7 @@ Network ~~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 25 30 100 @@ -84,7 +84,7 @@ Process ~~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 25 30 100 @@ -102,7 +102,7 @@ Identity ~~~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 23 20 100 @@ -132,7 +132,7 @@ Credentials ~~~~~~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 20 37 50 @@ -157,7 +157,7 @@ CPU / scheduling ~~~~~~~~~~~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 40 35 100 @@ -202,7 +202,7 @@ Memory ~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 28 27 100 @@ -221,7 +221,7 @@ I/O ~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 28 27 100 @@ -237,7 +237,7 @@ Threads ~~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 30 35 100 @@ -256,7 +256,7 @@ Signals ~~~~~~~ .. list-table:: - :class: longtable + :class: longtable wide-table :header-rows: 1 :widths: 33 45 100 From 228ac061e6d397e9022a504086669fd01fbebc94 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Mar 2026 22:02:03 +0100 Subject: [PATCH 1649/1714] Add docs/api-overview.rst section --- MANIFEST.in | 1 + README.rst | 286 ++----------------- docs/_static/css/custom.css | 16 +- docs/api-overview.rst | 549 ++++++++++++++++++++++++++++++++++++ docs/changelog.rst | 1 + docs/conf.py | 3 + docs/index.rst | 1 + docs/recipes.rst | 15 +- 8 files changed, 599 insertions(+), 273 deletions(-) create mode 100644 docs/api-overview.rst diff --git a/MANIFEST.in b/MANIFEST.in index 2a6010b596..01a9534669 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -26,6 +26,7 @@ include docs/_static/sidebar.js include docs/_templates/layout.html include docs/adoption.rst include docs/alternatives.rst +include docs/api-overview.rst include docs/api.rst include docs/changelog.rst include docs/conf.py diff --git a/README.rst b/README.rst index 42b88e400c..38bb49f318 100644 --- a/README.rst +++ b/README.rst @@ -81,6 +81,16 @@ psutil currently supports the following platforms: - **Sun Solaris** - **AIX** +Install +======= + +:: + + pip install psutil + +For platform-specific details see +`installation `_. + .. Sponsors @@ -211,53 +221,18 @@ People who donated money over the years: Example usages ============== -Below are interactive examples demonstrating all parts of the psutil API, -including CPU, memory, disks, network, sensors, and process management. - CPU --- .. code-block:: python >>> import psutil - >>> - >>> psutil.cpu_times() - scputimes(user=3961.46, nice=169.729, system=2150.659, idle=16900.540, iowait=629.59, irq=0.0, softirq=19.42, steal=0.0, guest=0, guest_nice=0.0) - >>> - >>> for x in range(3): - ... psutil.cpu_percent(interval=1) - ... - 4.0 - 5.9 - 3.8 - >>> - >>> for x in range(3): - ... psutil.cpu_percent(interval=1, percpu=True) - ... + >>> psutil.cpu_percent(interval=1, percpu=True) [4.0, 6.9, 3.7, 9.2] - [7.0, 8.5, 2.4, 2.1] - [1.2, 9.0, 9.9, 7.2] - >>> - >>> for x in range(3): - ... psutil.cpu_times_percent(interval=1, percpu=False) - ... - scputimes(user=1.5, nice=0.0, system=0.5, idle=96.5, iowait=1.5, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) - scputimes(user=1.0, nice=0.0, system=0.0, idle=99.0, iowait=0.0, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) - scputimes(user=2.0, nice=0.0, system=0.0, idle=98.0, iowait=0.0, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) - >>> - >>> psutil.cpu_count() - 4 >>> psutil.cpu_count(logical=False) 2 - >>> - >>> psutil.cpu_stats() - scpustats(ctx_switches=20455687, interrupts=6598984, soft_interrupts=2134212, syscalls=0) - >>> >>> psutil.cpu_freq() - scpufreq(current=931.42925, min=800.0, max=3500.0) - >>> - >>> psutil.getloadavg() # also on Windows (emulated) - (3.14, 3.89, 4.67) + scpufreq(current=931.42, min=800.0, max=3500.0) Memory ------ @@ -265,10 +240,9 @@ Memory .. code-block:: python >>> psutil.virtual_memory() - svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304) + svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, ...) >>> psutil.swap_memory() sswap(total=2097147904, used=296128512, free=1801019392, percent=14.1, sin=304193536, sout=677842944) - >>> Disks ----- @@ -278,13 +252,8 @@ Disks >>> psutil.disk_partitions() [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext', opts='rw')] - >>> >>> psutil.disk_usage('/') sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) - >>> - >>> psutil.disk_io_counters(perdisk=False) - sdiskio(read_count=719566, write_count=1082197, read_bytes=18626220032, write_bytes=24081764352, read_time=5023392, write_time=63199568, read_merged_count=619166, write_merged_count=812396, busy_time=4523412) - >>> Network ------- @@ -292,209 +261,45 @@ Network .. code-block:: python >>> psutil.net_io_counters(pernic=True) - {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0), - 'lo': netio(bytes_sent=2838627, bytes_recv=2838627, packets_sent=30567, packets_recv=30567, errin=0, errout=0, dropin=0, dropout=0)} - >>> + {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, ...), + 'lo': netio(bytes_sent=2838627, bytes_recv=2838627, ...)} >>> psutil.net_connections(kind='tcp') - [sconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254), - sconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING', pid=2987), + [sconn(fd=115, family=2, type=1, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254), ...] - >>> - >>> psutil.net_if_addrs() - {'lo': [snicaddr(family=, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None), - snicaddr(family=, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None), - snicaddr(family=, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)], - 'wlan0': [snicaddr(family=, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None), - snicaddr(family=, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), - snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} - >>> - >>> psutil.net_if_stats() - {'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536, flags='up,loopback,running'), - 'wlan0': snicstats(isup=True, duplex=, speed=100, mtu=1500, flags='up,broadcast,running,multicast')} - >>> Sensors ------- .. code-block:: python - >>> import psutil >>> psutil.sensors_temperatures() - {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], - 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], - 'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), + {'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0)]} - >>> - >>> psutil.sensors_fans() - {'asus': [sfan(label='cpu_fan', current=3200)]} - >>> >>> psutil.sensors_battery() sbattery(percent=93, secsleft=16628, power_plugged=False) - >>> - -Other system info ------------------ -.. code-block:: python - - >>> import psutil - >>> psutil.users() - [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), - suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] - >>> - >>> psutil.boot_time() - 1365519115.0 - >>> - -Process management ------------------- +Processes +--------- .. code-block:: python >>> import psutil - >>> psutil.pids() - [1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, 222, 223, 224, 268, 1215, - 1216, 1220, 1221, 1243, 1244, 1301, 1601, 2237, 2355, 2637, 2774, 3932, - 4176, 4177, 4185, 4187, 4189, 4225, 4243, 4245, 4263, 4282, 4306, 4311, - 4312, 4313, 4314, 4337, 4339, 4357, 4358, 4363, 4383, 4395, 4408, 4433, - 4443, 4445, 4446, 5167, 5234, 5235, 5252, 5318, 5424, 5644, 6987, 7054, - 7055, 7071] - >>> >>> p = psutil.Process(7055) - >>> p - psutil.Process(pid=7055, name='python3', status=, started='09:04:44') - >>> p.pid - 7055 >>> p.name() 'python3' >>> p.exe() '/usr/bin/python3' - >>> p.cwd() - '/home/giampaolo' - >>> p.cmdline() - ['/usr/bin/python3', 'main.py'] - >>> - >>> p.ppid() - 7054 - >>> p.parent() - psutil.Process(pid=4699, name='bash', status=, started='09:06:44') - >>> p.parents() - [psutil.Process(pid=4699, name='bash', started='09:06:44'), - psutil.Process(pid=4689, name='gnome-terminal-server', status=, started='0:06:44'), - psutil.Process(pid=1, name='systemd', status=, started='05:56:55')] - >>> p.children(recursive=True) - [psutil.Process(pid=29835, name='python3', status=, started='11:45:38'), - psutil.Process(pid=29836, name='python3', status=, started='11:43:39')] - >>> - >>> p.status() - - >>> p.create_time() - 1267551141.5019531 - >>> p.terminal() - '/dev/pts/0' - >>> - >>> p.username() - 'giampaolo' - >>> p.uids() - puids(real=1000, effective=1000, saved=1000) - >>> p.gids() - pgids(real=1000, effective=1000, saved=1000) - >>> - >>> p.cpu_times() - pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1, iowait=0.0) >>> p.cpu_percent(interval=1.0) 12.1 - >>> p.cpu_affinity() - [0, 1, 2, 3] - >>> p.cpu_affinity([0, 1]) # set - >>> p.cpu_num() - 1 - >>> >>> p.memory_info() pmem(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374) - >>> p.memory_info_ex() - pmem_ex(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374, peak_rss=4172190, peak_vms=6399001, rss_anon=2266726, rss_file=897433, rss_shmem=0, swap=0, hugetlb=0) - >>> p.memory_footprint() # "real" USS memory usage - pfootprint(uss=2355200, pss=2483712, swap=0) - >>> p.memory_percent() - 0.7823 - >>> p.memory_maps() - [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), - pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), - pmmap_grouped(path='[heap]', rss=32768, size=139264, pss=32768, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=32768, referenced=32768, anonymous=32768, swap=0), - pmmap_grouped(path='[stack]', rss=2465792, size=2494464, pss=2465792, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=2465792, referenced=2277376, anonymous=2465792, swap=0), - ...] - >>> - >>> p.page_faults() - ppagefaults(minor=5905, major=3) - >>> - >>> p.io_counters() - pio(read_count=478001, write_count=59371, read_bytes=700416, write_bytes=69632, read_chars=456232, write_chars=517543) - >>> - >>> p.open_files() - [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768), - popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] - >>> >>> p.net_connections(kind='tcp') - [pconn(fd=115, family=, type=, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status=), - pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=)] - >>> - >>> p.threads() - [pthread(id=5234, user_time=22.5, system_time=9.2891), - pthread(id=5237, user_time=0.0707, system_time=1.1)] - >>> - >>> p.num_threads() - 4 - >>> p.num_fds() - 8 - >>> p.num_ctx_switches() - pctxsw(voluntary=78, involuntary=19) - >>> - >>> p.nice() - 0 - >>> p.nice(10) # set - >>> - >>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # IO priority (Win and Linux only) - >>> p.ionice() - pionice(ioclass=, value=0) - >>> - >>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits (Linux only) - >>> p.rlimit(psutil.RLIMIT_NOFILE) - (5, 5) - >>> - >>> p.environ() - {'LC_PAPER': 'it_IT.UTF-8', 'SHELL': '/bin/bash', 'GREP_OPTIONS': '--color=auto', - 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', - ...} - >>> - >>> p.as_dict() - {'status': , 'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), 'pid': 5457, ...} - >>> p.is_running() - True - >>> p.suspend() - >>> p.resume() - >>> - >>> p.terminate() - >>> p.kill() - >>> p.wait(timeout=3) - - >>> - >>> psutil.test() - USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND - root 1 0.0 0.0 24584 2240 Jun17 00:00 init - root 2 0.0 0.0 0 0 Jun17 00:00 kthreadd - ... - giampaolo 31475 0.0 0.0 20760 3024 /dev/pts/0 Jun19 00:00 python2.4 - giampaolo 31721 0.0 2.2 773060 181896 00:04 10:30 chrome - root 31763 0.0 0.0 0 0 00:05 00:00 kworker/0:1 - >>> - -Further process APIs --------------------- + [pconn(fd=115, family=2, type=1, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED')] + >>> p.open_files() + [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768)] .. code-block:: python - >>> import psutil >>> for p in psutil.process_iter(['pid', 'name']): ... print(p.pid, p.name()) ... @@ -502,51 +307,10 @@ Further process APIs 2 kthreadd 3 ksoftirqd/0 ... - >>> - >>> psutil.pid_exists(3) - True - >>> - >>> def on_terminate(proc): - ... print("process {} terminated".format(proc)) - ... - >>> # waits for multiple processes to terminate - >>> gone, alive = psutil.wait_procs(procs_list, timeout=3, callback=on_terminate) - >>> - -Heap info ---------- - -.. code-block:: python - - >>> import psutil - >>> psutil.heap_info() - pheap(heap_used=5177792, mmap_used=819200) - >>> psutil.heap_trim() - -See also `psleak`_. - -Windows services ----------------- - -.. code-block:: python - - >>> list(psutil.win_service_iter()) - [, - , - , - , - ...] - >>> s = psutil.win_service_get('alg') - >>> s.as_dict() - {'binpath': 'C:\\Windows\\System32\\alg.exe', - 'description': 'Provides support for 3rd party protocol plug-ins for Internet Connection Sharing', - 'display_name': 'Application Layer Gateway Service', - 'name': 'alg', - 'pid': None, - 'start_type': 'manual', - 'status': 'stopped', - 'username': 'NT AUTHORITY\\LocalService'} +For the full API with more examples, see the +`API overview `_ and +`API reference `_. .. _`psleak`: https://github.com/giampaolo/psleak .. _`shell equivalents`: https://psutil.readthedocs.io/stable/shell-equivalents.html diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 23ded42bad..2148b3457a 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -34,7 +34,7 @@ footer div[role="navigation"][aria-label="Footer"] { /* Headings */ /* ================================================================== */ -h1, h2, h3 { +h1, h2 { background: #eee; padding: 5px; border-bottom: 1px solid #ccc; @@ -44,6 +44,19 @@ h1 { font-size: 35px; } +h3 { + background: none; + padding: 5px 0 5px 0; + border-bottom: 1px solid #ccc; + font-size: 120%; +} + +h4 { + background: none; + padding: 5px 0 5px 0; + font-size: 90%; +} + /* ================================================================== */ /* Tables */ /* ================================================================== */ @@ -186,6 +199,7 @@ div[class^='highlight'] div[class^='highlight'] { background-color: #eeffcc !important; } + /* ================================================================== */ /* Links */ /* ================================================================== */ diff --git a/docs/api-overview.rst b/docs/api-overview.rst new file mode 100644 index 0000000000..7ae1530039 --- /dev/null +++ b/docs/api-overview.rst @@ -0,0 +1,549 @@ +API overview +============ + +Overview of the entire psutil API (on Linux). This serves as a quick reference +to all available functions. For detailed documentation of each function see the +full :doc:`API reference `. + +System related functions +------------------------ + +CPU +^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> psutil.cpu_times() + scputimes(user=3961.46, + nice=169.729, + system=2150.659, + idle=16900.540, + iowait=629.59, + irq=0.0, + softirq=19.42, + steal=0.0, + guest=0, + guest_nice=0.0) + >>> + >>> for x in range(3): + ... psutil.cpu_percent(interval=1) + ... + 4.0 + 5.9 + 3.8 + >>> + >>> for x in range(3): + ... psutil.cpu_percent(interval=1, percpu=True) + ... + [4.0, 6.9, 3.7, 9.2] + [7.0, 8.5, 2.4, 2.1] + [1.2, 9.0, 9.9, 7.2] + >>> + >>> for x in range(3): + ... psutil.cpu_times_percent(interval=1, percpu=False) + ... + scputimes(user=1.5, nice=0.0, system=0.5, idle=96.5, iowait=1.5, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) + scputimes(user=1.0, nice=0.0, system=0.0, idle=99.0, iowait=0.0, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) + scputimes(user=2.0, nice=0.0, system=0.0, idle=98.0, iowait=0.0, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) + >>> + >>> psutil.cpu_count() + 4 + >>> psutil.cpu_count(logical=False) + 2 + >>> + >>> psutil.cpu_stats() + scpustats(ctx_switches=20455687, interrupts=6598984, soft_interrupts=2134212, syscalls=0) + >>> + >>> psutil.cpu_freq() + scpufreq(current=931.42925, min=800.0, max=3500.0) + >>> + >>> psutil.getloadavg() # also on Windows (emulated) + (3.14, 3.89, 4.67) + >>> + +Memory +^^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> psutil.virtual_memory() + svmem(total=10367352832, + available=6472179712, + percent=37.6, + used=8186245120, + free=2181107712, + active=4748992512, + inactive=2758115328, + buffers=790724608, + cached=3500347392, + shared=787554304) + >>> + >>> psutil.swap_memory() + sswap(total=2097147904, + used=296128512, + free=1801019392, + percent=14.1, + sin=304193536, + sout=677842944) + >>> + +Disks +^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> psutil.disk_partitions() + [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), + sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext', opts='rw')] + >>> + >>> psutil.disk_usage('/') + sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) + >>> + >>> psutil.disk_io_counters(perdisk=False) + sdiskio(read_count=719566, + write_count=1082197, + read_bytes=18626220032, + write_bytes=24081764352, + read_time=5023392, + write_time=63199568, + read_merged_count=619166, + write_merged_count=812396, + busy_time=4523412) + >>> + +Network +^^^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> psutil.net_io_counters(pernic=True) + {'eth0': netio(bytes_sent=485291293, + bytes_recv=6004858642, + packets_sent=3251564, + packets_recv=4787798, + errin=0, + errout=0, + dropin=0, + dropout=0), + 'lo': netio(bytes_sent=2838627, + bytes_recv=2838627, + packets_sent=30567, + packets_recv=30567, + errin=0, + errout=0, + dropin=0, + dropout=0)} + >>> + >>> psutil.net_connections(kind='tcp') + [sconn(fd=115, + family=, + type=, + laddr=addr(ip='10.0.0.1', port=48776), + raddr=addr(ip='93.186.135.91', port=80), + status='ESTABLISHED', + pid=1254), + sconn(fd=117, + family=, + type=, + laddr=addr(ip='10.0.0.1', port=43761), + raddr=addr(ip='72.14.234.100', port=80), + status='CLOSING', + pid=2987), + ...] + >>> + >>> psutil.net_if_addrs() + {'lo': [snicaddr(family=, + address='127.0.0.1', + netmask='255.0.0.0', + broadcast='127.0.0.1', + ptp=None), + snicaddr(family=, + address='::1', + netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + broadcast=None, + ptp=None), + snicaddr(family=, + address='00:00:00:00:00:00', + netmask=None, + broadcast='00:00:00:00:00:00', + ptp=None)], + 'wlan0': [snicaddr(family=, + address='192.168.1.3', + netmask='255.255.255.0', + broadcast='192.168.1.255', + ptp=None), + snicaddr(family=, + address='fe80::c685:8ff:fe45:641%wlan0', + netmask='ffff:ffff:ffff:ffff::', + broadcast=None, + ptp=None), + snicaddr(family=, + address='c4:85:08:45:06:41', + netmask=None, + broadcast='ff:ff:ff:ff:ff:ff', + ptp=None)]} + >>> + >>> psutil.net_if_stats() + {'lo': snicstats(isup=True, + duplex=, + speed=0, + mtu=65536, + flags='up,loopback,running'), + 'wlan0': snicstats(isup=True, + duplex=, + speed=100, + mtu=1500, + flags='up,broadcast,running,multicast')} + >>> + +Sensors +^^^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> psutil.sensors_temperatures() + {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], + 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], + 'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0), + shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0)]} + >>> + >>> psutil.sensors_fans() + {'asus': [sfan(label='cpu_fan', current=3200)]} + >>> + >>> psutil.sensors_battery() + sbattery(percent=93, secsleft=16628, power_plugged=False) + >>> + +Other system info +^^^^^^^^^^^^^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> psutil.users() + [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), + suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] + >>> + >>> psutil.boot_time() + 1365519115.0 + >>> + +Processes +--------- + +Identity +^^^^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> p = psutil.Process(7055) + >>> p + psutil.Process(pid=7055, name='python3', status=, started='09:04:44') + >>> p.pid + 7055 + >>> p.name() + 'python3' + >>> p.exe() + '/usr/bin/python3' + >>> p.cwd() + '/home/giampaolo' + >>> p.cmdline() + ['/usr/bin/python3', 'main.py'] + >>> p.status() + + >>> p.create_time() + 1267551141.5019531 + >>> p.terminal() + '/dev/pts/0' + >>> p.environ() + {'GREP_OPTIONS': '--color=auto', + 'LC_PAPER': 'it_IT.UTF-8', + 'SHELL': '/bin/bash', + 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', + ...} + >>> p.is_running() + True + >>> p.as_dict() + {'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), + 'pid': 5457, + 'status': , + ...} + >>> + +Process tree +^^^^^^^^^^^^ + +.. code-block:: pycon + + >>> p.ppid() + 7054 + >>> p.parent() + psutil.Process(pid=4699, name='bash', status=, started='09:06:44') + >>> + >>> p.parents() + [psutil.Process(pid=4699, name='bash', started='09:06:44'), + psutil.Process(pid=4689, name='gnome-terminal-server', status=, started='0:06:44'), + psutil.Process(pid=1, name='systemd', status=, started='05:56:55')] + >>> + >>> p.children(recursive=True) + [psutil.Process(pid=29835, name='python3', status=, started='11:45:38'), + psutil.Process(pid=29836, name='python3', status=, started='11:43:39')] + >>> + +Credentials +^^^^^^^^^^^^ + +.. code-block:: pycon + + >>> p.username() + 'giampaolo' + >>> p.uids() + puids(real=1000, effective=1000, saved=1000) + >>> p.gids() + pgids(real=1000, effective=1000, saved=1000) + >>> + +CPU / scheduling +^^^^^^^^^^^^^^^^ + +.. code-block:: pycon + + >>> p.cpu_times() + pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1, iowait=0.0) + >>> p.cpu_percent(interval=1.0) + 12.1 + >>> p.cpu_affinity() + [0, 1, 2, 3] + >>> p.cpu_affinity([0, 1]) # set + >>> p.cpu_num() + 1 + >>> p.num_ctx_switches() + pctxsw(voluntary=78, involuntary=19) + >>> p.nice() + 0 + >>> p.nice(10) # set + >>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # IO priority (Win and Linux only) + >>> p.ionice() + pionice(ioclass=, value=0) + >>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits (Linux only) + >>> p.rlimit(psutil.RLIMIT_NOFILE) + (5, 5) + >>> + +Memory +^^^^^^ + +.. code-block:: pycon + + >>> p.memory_info() + pmem(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374) + >>> p.memory_info_ex() + pmem_ex(rss=3164160, + vms=4410163, + shared=897433, + text=302694, + data=2422374, + peak_rss=4172190, + peak_vms=6399001, + rss_anon=2266726, + rss_file=897433, + rss_shmem=0, + swap=0, + hugetlb=0) + >>> p.memory_footprint() # "real" USS memory usage + pfootprint(uss=2355200, pss=2483712, swap=0) + >>> p.memory_percent() + 0.7823 + >>> p.memory_maps() + [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', + rss=32768, + size=2125824, + pss=32768, + shared_clean=0, + shared_dirty=0, + private_clean=20480, + private_dirty=12288, + referenced=32768, + anonymous=12288, + swap=0), + pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', + rss=3821568, + size=3842048, + pss=3821568, + shared_clean=0, + shared_dirty=0, + private_clean=0, + private_dirty=3821568, + referenced=3575808, + anonymous=3821568, + swap=0), + pmmap_grouped(path='[heap]', + rss=32768, + size=139264, + pss=32768, + shared_clean=0, + shared_dirty=0, + private_clean=0, + private_dirty=32768, + referenced=32768, + anonymous=32768, + swap=0), + pmmap_grouped(path='[stack]', + rss=2465792, + size=2494464, + pss=2465792, + shared_clean=0, + shared_dirty=0, + private_clean=0, + private_dirty=2465792, + referenced=2277376, + anonymous=2465792, + swap=0), + ...] + >>> p.page_faults() + ppagefaults(minor=5905, major=3) + >>> + +Threads +^^^^^^^ + +.. code-block:: pycon + + >>> p.threads() + [pthread(id=5234, user_time=22.5, system_time=9.2891), + pthread(id=5237, user_time=0.0707, system_time=1.1)] + >>> p.num_threads() + 4 + >>> + +Files and connections +^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: pycon + + >>> p.io_counters() + pio(read_count=478001, + write_count=59371, + read_bytes=700416, + write_bytes=69632, + read_chars=456232, + write_chars=517543) + >>> p.open_files() + [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768), + popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] + >>> p.net_connections(kind='tcp') + [pconn(fd=115, + family=, + type=, + laddr=addr(ip='10.0.0.1', port=48776), + raddr=addr(ip='93.186.135.91', port=80), + status=), + pconn(fd=117, + family=, + type=, + laddr=addr(ip='10.0.0.1', port=43761), + raddr=addr(ip='72.14.234.100', port=80), + status=)] + >>> p.num_fds() + 8 + >>> + +Signals +^^^^^^^ + +.. code-block:: pycon + + >>> p.suspend() + >>> p.resume() + >>> p.terminate() + >>> p.kill() + >>> p.wait(timeout=3) + + >>> + +Other process functions +^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> psutil.pids() + [1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, ...] + >>> psutil.pid_exists(3) + True + >>> + >>> for p in psutil.process_iter(['pid', 'name']): + ... print(p.pid, p.name()) + ... + 1 systemd + 2 kthreadd + 3 ksoftirqd/0 + ... + >>> + >>> def on_terminate(proc): + ... print("process {} terminated".format(proc)) + ... + >>> # waits for multiple processes to terminate + >>> gone, alive = psutil.wait_procs(procs_list, timeout=3, callback=on_terminate) + >>> + >>> psutil.test() + USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND + root 1 0.0 0.0 24584 2240 Jun17 00:00 init + root 2 0.0 0.0 0 0 Jun17 00:00 kthreadd + ... + giampaolo 31475 0.0 0.0 20760 3024 /dev/pts/0 Jun19 00:00 python2.4 + giampaolo 31721 0.0 2.2 773060 181896 00:04 10:30 chrome + root 31763 0.0 0.0 0 0 00:05 00:00 kworker/0:1 + >>> + +C heap introspection +-------------------- + +.. code-block:: pycon + + >>> import psutil + >>> + >>> psutil.heap_info() + pheap(heap_used=5177792, mmap_used=819200) + >>> psutil.heap_trim() + >>> + +See also `psleak `_. + +Windows services +---------------- + +.. code-block:: pycon + + >>> import psutil + >>> + >>> list(psutil.win_service_iter()) + [, + , + , + , + ...] + >>> s = psutil.win_service_get('alg') + >>> s.as_dict() + {'binpath': 'C:\\Windows\\System32\\alg.exe', + 'description': 'Provides support for 3rd party protocol plug-ins for Internet Connection Sharing', + 'display_name': 'Application Layer Gateway Service', + 'name': 'alg', + 'pid': None, + 'start_type': 'manual', + 'status': 'stopped', + 'username': 'NT AUTHORITY\\LocalService'} + >>> diff --git a/docs/changelog.rst b/docs/changelog.rst index eb55decbce..a7a984136e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Added new sections: - :doc:`/adoption `: notable software using psutil + - :doc:`/api-overview `: show entire API via REPL usage examples - :doc:`/alternatives `: list of alternative Python libraries and tools that overlap with psutil. - :doc:`/credits `: list contributors and donors (was old ``CREDITS`` in root dir) - :doc:`/faq `: extended FAQ section diff --git a/docs/conf.py b/docs/conf.py index 7ac1a4bcc6..30068e6373 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,3 +61,6 @@ 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', 'css/custom.css', ] + +# https://pygments.org/styles/ +# pygments_style = "monokai" diff --git a/docs/index.rst b/docs/index.rst index dd7f2b8619..b407b01c07 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -94,6 +94,7 @@ Table of contents :caption: User guide Install + API overview API Reference Performance FAQ diff --git a/docs/recipes.rst b/docs/recipes.rst index 852da3b510..6282e4a7b9 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -186,12 +186,8 @@ Top N processes by cumulative disk read + write bytes (similar to ``iotop``): def top_io_procs(n=5): procs = [] for p in psutil.process_iter(["io_counters"]): - try: - io = p.io_counters() - except psutil.Error: - pass - else: - procs.append((io.read_bytes + io.write_bytes, p)) + io = p.io_counters() + procs.append((io.read_bytes + io.write_bytes, p)) procs.sort(key=lambda x: x[0], reverse=True) return procs[:n] @@ -205,11 +201,8 @@ Top N processes by open file descriptors (useful for diagnosing fd leaks): def top_open_files(n=5): procs = [] - for p in psutil.process_iter(): - try: - procs.append((p.num_fds(), p)) - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass + for p in psutil.process_iter(["num_fds"]): + procs.append((p.num_fds(), p)) procs.sort(key=lambda x: x[0], reverse=True) return procs[:n] From d796d2f0528bd264de47a6003af0322246abe5bc Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Mar 2026 22:58:38 +0100 Subject: [PATCH 1650/1714] Use .. code-block:: none instead of :: + define reflinks at the bottom --- README.rst | 2 +- docs/_ext/availability.py | 3 +- docs/api.rst | 2 +- docs/devguide.rst | 8 +-- docs/faq.rst | 11 ++-- docs/install.rst | 105 ++++++++++++++++++++++++++------------ docs/performance.rst | 11 ++-- docs/platform.rst | 2 +- 8 files changed, 92 insertions(+), 52 deletions(-) diff --git a/README.rst b/README.rst index 38bb49f318..424b7a078a 100644 --- a/README.rst +++ b/README.rst @@ -84,7 +84,7 @@ psutil currently supports the following platforms: Install ======= -:: +.. code-block:: none pip install psutil diff --git a/docs/_ext/availability.py b/docs/_ext/availability.py index b923079f6e..3a34315d1a 100644 --- a/docs/_ext/availability.py +++ b/docs/_ext/availability.py @@ -66,8 +66,7 @@ def parse_platforms(self): Arguments is a comma-separated string of platforms. A platform may be prefixed with "not " to indicate that a feature is not available. - Example:: - + Example: .. availability:: Windows, Linux >= 4.2, not glibc """ platforms = {} diff --git a/docs/api.rst b/docs/api.rst index 0fb492a7d0..3e2b42b096 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2176,7 +2176,7 @@ Process class If *recursive* is ``True`` return all the parent descendants. Pseudo code example assuming *A == this process*: - :: + .. code-block:: none A ─┐ │ diff --git a/docs/devguide.rst b/docs/devguide.rst index fcf0690a28..0dbc014452 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -36,7 +36,7 @@ Build, setup and test - To run a specific unit test: - .. code-block:: + .. code-block:: none make test ARGS=tests/test_system.py @@ -46,7 +46,7 @@ Build, setup and test - If you want to target a specific Python version: - .. code-block:: + .. code-block:: none make test PYTHON=python3.8 @@ -75,14 +75,14 @@ mode, psutil may print additional information to stderr. Usually these are non-severe error conditions that are ignored instead of causing a crash. Unit tests automatically run with debug mode enabled. On UNIX: -:: +.. code-block:: none $ PSUTIL_DEBUG=1 python3 script.py psutil-debug [psutil/_psutil_linux.c:150]> setmntent() failed (ignored) On Windows: -:: +.. code-block:: none set PSUTIL_DEBUG=1 && python.exe script.py psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) diff --git a/docs/faq.rst b/docs/faq.rst index 3d0533dc01..5edc609fec 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -205,8 +205,7 @@ was assigned the same PID. :meth:`Process.terminate`, :meth:`Process.kill`) **do** check for PID reuse (via PID + creation time) before acting, raising :exc:`NoSuchProcess` if the PID was recycled. This prevents accidentally - killing the wrong process (`BPO-6973 - `_). + killing the wrong process (`BPO-6973`_). - *Set methods* :meth:`Process.nice` (set), :meth:`Process.ionice` (set), :meth:`Process.cpu_affinity` (set), and @@ -289,9 +288,8 @@ What is the difference between psutil, os, and multiprocessing cpu_count()? - :func:`os.cpu_count` returns the number of **logical** CPUs (including hyperthreads). It is the same as ``psutil.cpu_count(logical=True)``, but - psutil does not honour - `PYTHON_CPU_COUNT `_ - environment variable introduced in Python 3.13. + psutil does not honour `PYTHON_CPU_COUNT`_ environment variable introduced in + Python 3.13. - :func:`os.process_cpu_count` (Python 3.13+) returns the number of CPUs the calling process is **allowed to use** (respects CPU affinity and cgroups). The psutil equivalent is ``len(psutil.Process().cpu_affinity())``. @@ -376,3 +374,6 @@ separately: The ``available`` field already includes this reclaimable memory and is the best indicator of memory pressure. See :ref:`faq_virtual_memory_available`. + +.. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`PYTHON_CPU_COUNT`: https://docs.python.org/3/using/cmdline.html#envvar-PYTHON_CPU_COUNT diff --git a/docs/install.rst b/docs/install.rst index 3ddbbd4052..0e02ab659f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -5,7 +5,9 @@ Linux, Windows, macOS (wheels) ------------------------------ Pre-compiled wheels are distributed for these platforms, so you usually won't -need a C compiler. All you have to do is:: +need a C compiler. All you have to do is: + +.. code-block:: none pip install psutil @@ -18,41 +20,54 @@ Compile psutil from source UNIX ^^^^ -On all UNIX systems you can use the `install-sysdeps.sh -`_ -script. This will install the system dependencies necessary to compile psutil -from sources. You can invoke this script from the Makefile as:: +On all UNIX systems you can use the `install-sysdeps.sh`_ script. This will +install the system dependencies necessary to compile psutil from sources. You +can invoke this script from the Makefile as: + +.. code-block:: none make install-sysdeps -After system deps are installed, you can compile and install psutil with:: +After system deps are installed, you can compile and install psutil with: + +.. code-block:: none make build make install -...or this, which will fetch the latest source distribution from `PyPI `_:: +...or this, which will fetch the latest source distribution from `PyPI`_: + +.. code-block:: none pip install --no-binary :all: psutil Linux ^^^^^ -Debian / Ubuntu:: +Debian / Ubuntu: + +.. code-block:: none sudo apt install gcc python3-dev pip install --no-binary :all: psutil -RedHat / CentOS:: +RedHat / CentOS: + +.. code-block:: none sudo yum install gcc python3-devel pip install --no-binary :all: psutil -Arch:: +Arch: + +.. code-block:: none sudo pacman -S gcc python pip install --no-binary :all: psutil -Alpine:: +Alpine: + +.. code-block:: none sudo apk add gcc python3-dev musl-dev linux-headers pip install --no-binary :all: psutil @@ -61,29 +76,31 @@ Windows ^^^^^^^ - To build or install psutil from source on Windows, you need to have - `Visual Studio 2017 `_ - or later installed. For detailed instructions, see the - `CPython Developer Guide `_. + `Visual Studio 2017`_ or later installed. For detailed instructions, see the + `CPython Developer Guide`_. - MinGW is not supported for building psutil on Windows. -- To build directly from the source tarball (.tar.gz) on PYPI, run:: +- To build directly from the source tarball (.tar.gz) on PYPI, run: - pip install --no-binary :all: psutil + .. code-block:: none + + pip install --no-binary :all: psutil - If you want to clone psutil's Git repository and build / develop locally, - first install: `Git for Windows `_ - and launch a Git Bash shell. This provides a Unix-like environment where - ``make`` works. -- Once inside Git Bash, you can run the usual ``make`` commands:: + first install `Git for Windows`_ and launch a Git Bash shell. This provides + a Unix-like environment where ``make`` works. +- Once inside Git Bash, you can run the usual ``make`` commands: - make build - make install + .. code-block:: none + + make build + make install macOS ^^^^^ Install Xcode first: -:: +.. code-block:: none xcode-select --install pip install --no-binary :all: psutil @@ -91,7 +108,7 @@ Install Xcode first: FreeBSD ^^^^^^^ -:: +.. code-block:: none pkg install python3 gcc python3 -m pip install psutil @@ -99,7 +116,7 @@ FreeBSD OpenBSD ^^^^^^^ -:: +.. code-block:: none export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ pkg_add -v python3 gcc @@ -110,7 +127,7 @@ NetBSD Assuming Python 3.11: -:: +.. code-block:: none export PKG_PATH="https://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/`uname -m`/`uname -r`/All" pkg_add -v pkgin @@ -120,11 +137,15 @@ Assuming Python 3.11: Sun Solaris ^^^^^^^^^^^ -If ``cc`` compiler is not installed create a symbolic link to ``gcc``:: +If ``cc`` compiler is not installed create a symbolic link to ``gcc``: + +.. code-block:: none sudo ln -s /usr/bin/gcc /usr/local/bin/cc -Install:: +Install: + +.. code-block:: none pkg install gcc pip install psutil @@ -135,16 +156,21 @@ Troubleshooting Install pip ^^^^^^^^^^^ -If you don't have pip you can install it with wget:: +If you don't have pip you can install it with wget: + +.. code-block:: none wget https://bootstrap.pypa.io/get-pip.py -O - | python3 -...or with curl:: +...or with curl: + +.. code-block:: none python3 < <(curl -s https://bootstrap.pypa.io/get-pip.py) -On Windows, `download pip `_, open -cmd.exe and install it with:: +On Windows, `download pip`_, open cmd.exe and install it with: + +.. code-block:: none py get-pip.py @@ -152,7 +178,9 @@ cmd.exe and install it with:: ^^^^^^^^^^^^^^^ Sometimes pip is installed but it's not available in your ``PATH`` -("pip command not found" or similar). Try this:: +("pip command not found" or similar). Try this: + +.. code-block:: none python3 -m pip install psutil @@ -160,6 +188,15 @@ Permission errors (UNIX) ^^^^^^^^^^^^^^^^^^^^^^^^ If you want to install psutil system-wide and you bump into permission errors -either run as root user or prepend ``sudo``:: +either run as root user or prepend ``sudo``: + +.. code-block:: none sudo pip install psutil + +.. _`install-sysdeps.sh`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/install-sysdeps.sh +.. _`PyPI`: https://pypi.org/project/psutil/ +.. _`Visual Studio 2017`: https://visualstudio.microsoft.com/vs/older-downloads/ +.. _`CPython Developer Guide`: https://devguide.python.org/getting-started/setup-building/#windows +.. _`Git for Windows`: https://git-scm.com/install/windows +.. _`download pip`: https://pip.pypa.io/en/latest/installing/ diff --git a/docs/performance.rst b/docs/performance.rst index 51a788469a..31396de382 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -118,10 +118,10 @@ avoids race conditions, and caches :class:`Process` instances internally: Measuring oneshot() speedup --------------------------- -`bench_oneshot.py `_ +`bench_oneshot.py`_ script measures :meth:`Process.oneshot` speedup. E.g. on Linux: -.. code-block:: +.. code-block:: none $ python3 scripts/internal/bench_oneshot.py --times 10000 17 methods pre-fetched by oneshot() on platform 'linux' (10,000 times, psutil 8.0.0): @@ -155,12 +155,12 @@ This also shows which APIs share the same internal kernel routines. Measuring APIs speed -------------------- -`print_api_speed.py `_ +`print_api_speed.py`_ script shows the relative cost of each API call. This helps you understand which operations are more expensive. E.g. on Linux: -.. code-block:: +.. code-block:: none $ python3 scripts/internal/print_api_speed.py SYSTEM APIS NUM CALLS SECONDS @@ -226,3 +226,6 @@ E.g. on Linux: environ 300 0.01013 memory_footprint 300 0.02241 memory_maps 300 0.30282 + +.. _`bench_oneshot.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/bench_oneshot.py +.. _`print_api_speed.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/print_api_speed.py diff --git a/docs/platform.rst b/docs/platform.rst index 6c2abb3725..493f712162 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -11,7 +11,7 @@ Python The 6.1.X series may still receive critical bug-fixes but no new features. To install psutil on Python 2.7 run: -:: +.. code-block:: none python2 -m pip install psutil==6.1.* From 24cf3217ceaf7d8e6b8685089860a684c09e4f98 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 26 Mar 2026 23:56:50 +0100 Subject: [PATCH 1651/1714] Sidebar style --- docs/_static/css/custom.css | 12 ++++++++++++ docs/install.rst | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 2148b3457a..2be5847914 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -30,6 +30,18 @@ footer div[role="navigation"][aria-label="Footer"] { background-color: grey !important } +.wy-menu-vertical header, .wy-menu-vertical p.caption { + color: #55a5d9; +} + +.wy-menu-vertical li.toctree-l1:not(.current) > a { + color: #d9d9d9 !important; +} + +.wy-menu-vertical li.toctree-l2 > a { + color: #333 !important; +} + /* ================================================================== */ /* Headings */ /* ================================================================== */ diff --git a/docs/install.rst b/docs/install.rst index 0e02ab659f..47bbf6bdce 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -194,9 +194,9 @@ either run as root user or prepend ``sudo``: sudo pip install psutil +.. _`CPython Developer Guide`: https://devguide.python.org/getting-started/setup-building/#windows +.. _`download pip`: https://pip.pypa.io/en/latest/installing/ +.. _`Git for Windows`: https://git-scm.com/install/windows .. _`install-sysdeps.sh`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/install-sysdeps.sh .. _`PyPI`: https://pypi.org/project/psutil/ .. _`Visual Studio 2017`: https://visualstudio.microsoft.com/vs/older-downloads/ -.. _`CPython Developer Guide`: https://devguide.python.org/getting-started/setup-building/#windows -.. _`Git for Windows`: https://git-scm.com/install/windows -.. _`download pip`: https://pip.pypa.io/en/latest/installing/ From 19e7ee7fdb1119d05158275475dd6fa2caa5bf74 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 27 Mar 2026 12:42:52 +0100 Subject: [PATCH 1652/1714] Doc: renewed theme (CSS) --- docs/_static/css/custom.css | 262 ++++++++++++++++++++++++++---------- docs/api-overview.rst | 83 +++++++----- docs/changelog.rst | 1 + docs/conf.py | 2 +- docs/recipes.rst | 28 +--- 5 files changed, 253 insertions(+), 123 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 2be5847914..9878cb4560 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -1,3 +1,81 @@ +/* ================================================================== */ +/* Variables */ +/* ================================================================== */ + +:root { + /* base palette */ + --gray-100: #f5f5f5; + --gray-200: #f0f4f7; + --gray-300: #d6d6d6; + --gray-400: #ccc; + --gray-500: #555; + --gray-600: #3b3b3b; + --gray-700: #222; + --gray-800: #000; + + --blue-200: #f3f9fe; + --blue-300: #b0cfe8; + --blue-400: #2980b9; + --blue-500: #336699; + --blue-600: #224466; + + --green-200: #f0faf0; + --green-400: #8aba8a; + --green-500: #4fc464; + --green-700: #296433; + + --yellow-300: #f4e34c; + --yellow-700: #854826; + + --red-200: #fff0f0; + --red-400: #e8a0a0; + --red-500: #f44c4e; + --red-700: #9f3133; + + --dark-100: #2d2e27; + + /* semantic */ + --border-radius: 3px; + --h1-h2-border: var(--gray-400); + + --adm-padding-y: 5px; + --adm-title: black; + + --code-font: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + --code-line-height: 1.5; + --code-bg: var(--dark-100); + + --adm-note-bg: var(--gray-100); + --adm-note-border: var(--gray-400); + --adm-inline-code-bg: var(--gray-300); + --adm-seealso-accent-border: var(--blue-500); + --adm-seealso-bg: var(--blue-200); + --adm-seealso-border: var(--blue-300); + --adm-seealso-hover: var(--blue-400); + --adm-seealso-title: var(--blue-600); + --adm-tip-bg: var(--green-200); + --adm-tip-border: var(--green-400); + --adm-warning-bg: var(--red-200); + --adm-warning-border: var(--red-400); + + --sidebar-search-bg: var(--gray-600); + + --versionadded-border: var(--green-500); + --versionadded-color: var(--green-700); + --versionchanged-border: var(--yellow-300); + --versionchanged-color: var(--yellow-700); + --deprecated-border: var(--red-500); + --deprecated-color: var(--red-700); + + --func-sig-bg: var(--gray-200); + --func-sig-border: var(--blue-500); + --func-sig-text: var(--gray-700); + --func-sig-name: var(--gray-800); + --func-sig-param: var(--gray-500); + + --table-header-bg: var(--adm-note-bg); +} + /* ================================================================== */ /* Layout */ /* ================================================================== */ @@ -27,7 +105,17 @@ footer div[role="navigation"][aria-label="Footer"] { /* ================================================================== */ .wy-side-nav-search { - background-color: grey !important + background-color: var(--sidebar-search-bg) !important; +} + +.wy-side-nav-search input[type="text"] { + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.wy-side-nav-search input[type="text"]:focus { + border-color: #7ab3d4; + box-shadow: 0 0 0 2px rgba(122, 179, 212, 0.3); + outline: none; } .wy-menu-vertical header, .wy-menu-vertical p.caption { @@ -47,26 +135,25 @@ footer div[role="navigation"][aria-label="Footer"] { /* ================================================================== */ h1, h2 { - background: #eee; padding: 5px; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid var(--h1-h2-border); } h1 { - font-size: 35px; + font-size: 2.2rem; } h3 { background: none; padding: 5px 0 5px 0; - border-bottom: 1px solid #ccc; - font-size: 120%; + border-bottom: 1px solid var(--h1-h2-border); + font-size: 1.2rem; } h4 { background: none; padding: 5px 0 5px 0; - font-size: 90%; + font-size: 0.9rem; } /* ================================================================== */ @@ -74,7 +161,7 @@ h4 { /* ================================================================== */ .wy-table-responsive table thead { - background-color: #eeeeee; + background-color: var(--table-header-bg); } .document th { @@ -157,7 +244,7 @@ h4 { } .rst-content dl:not(.docutils) dt { - color: #555; + color: var(--func-sig-text); } .data dd { @@ -187,31 +274,28 @@ h4 { margin-bottom: 6px !important; } -pre { - padding: 5px !important; +.highlight, +.highlight pre { + font-family: var(--code-font) !important; } -.highlight { - background: #eeffcc; +div[class^="highlight-"] { + border-radius: var(--border-radius); + background: transparent; } -.highlight .hll { - background-color: #ffffcc -} - -.highlight .c { - color: #408090; - font-style: italic +.highlight { + background-color: var(--code-bg); + border-radius: var(--border-radius); + overflow: hidden; } -.codeblock div[class^='highlight'], -pre.literal-block div[class^='highlight'], -.rst-content .literal-block div[class^='highlight'], -div[class^='highlight'] div[class^='highlight'] { - background-color: #eeffcc !important; +.highlight pre { + line-height: var(--code-line-height) !important; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } - /* ================================================================== */ /* Links */ /* ================================================================== */ @@ -234,38 +318,63 @@ a.external[href^="#"] { /* Admonitions (note, warning, tip) - styled like python doc */ /* ================================================================== */ +/* shared structure for all admonition types */ div.admonition { margin-top: 10px !important; margin-bottom: 10px !important; + padding: var(--adm-padding-y) 10px !important; + background-color: var(--adm-note-bg) !important; + border: 1px solid var(--adm-note-border) !important; + border-left: 3px solid var(--adm-note-border) !important; + border-radius: var(--border-radius) !important; } -div.warning { - background-color: #ffe4e4 !important; - border: 1px solid #f66 !important; - border-radius: 3px !important; +div.admonition p { + margin-bottom: var(--adm-padding-y) !important; } -div.note, -div.important { - background-color: #eee !important; - border: 1px solid #ccc !important; - border-radius: 3px !important; +/* per-type color overrides */ +div.warning { + background-color: var(--adm-warning-bg) !important; + border-color: var(--adm-warning-border) !important; + border-left-color: var(--adm-warning-border) !important; } div.tip { - background-color: #dfd !important; - border: 1px solid green !important; - border-radius: 3px !important; + background-color: var(--adm-tip-bg) !important; + border-color: var(--adm-tip-border) !important; + border-left-color: var(--adm-tip-border) !important; +} + +div.seealso { + background-color: var(--adm-seealso-bg) !important; + border-color: var(--adm-seealso-border) !important; + border-left-color: var(--adm-seealso-accent-border) !important; } -div.admonition p.admonition-title + p { +div.seealso a.reference { + color: var(--adm-seealso-accent-border) !important; + text-decoration: underline; + text-underline-offset: 2px; +} + +div.seealso a.reference:hover { + color: var(--adm-seealso-hover) !important; +} + +/* title: inline with content, no extra spacing */ +p.admonition-title, +p.admonition-title + p { display: inline !important; + margin: 0 !important; + padding: 0 !important; } p.admonition-title { - display: inline !important; background: none !important; - color: black !important; + color: var(--adm-title) !important; + font-weight: 700 !important; + margin-right: 0.4em !important; } p.admonition-title:after { @@ -280,29 +389,9 @@ p.admonition-title:after { display: none !important; } -.admonition.warning { - padding-top: 5px !important; - padding-bottom: 5px !important; -} - -.admonition.warning p { - margin-bottom: 5px !important; -} - -.admonition.note, -.admonition.important { - padding-top: 5px !important; - padding-bottom: 5px !important; -} - -.admonition.note p, -.admonition.important p { - margin-bottom: 5px !important; -} - .note code, .important code { - background: #d6d6d6 !important; + background: var(--adm-inline-code-bg) !important; } /* ================================================================== */ @@ -317,25 +406,62 @@ div.deprecated { } div.versionadded { - border-left-color: rgb(79, 196, 100); + border-left-color: var(--versionadded-border); } div.versionchanged { - border-left-color: rgb(244, 227, 76); + border-left-color: var(--versionchanged-border); } div.deprecated { - border-left-color: rgb(244, 76, 78); + border-left-color: var(--deprecated-border); } div.versionadded .versionmodified { - color: rgb(41, 100, 51); + color: var(--versionadded-color); } div.versionchanged .versionmodified { - color: rgb(133, 72, 38); + color: var(--versionchanged-color); } div.deprecated .versionmodified { - color: rgb(159, 49, 51); + color: var(--deprecated-color); +} + +/* ================================================================== */ +/* API function / class signatures */ +/* ================================================================== */ + +/* The container/background bar */ +.rst-content dl:not(.docutils) dt { + background: var(--func-sig-bg) !important; + border-top: 2px solid var(--func-sig-border) !important; + border-left: none !important; + color: var(--func-sig-text) !important; + padding: 6px 10px !important; + font-size: 1.1em; +} + +/* The function name itself */ +.rst-content dl:not(.docutils) dt .descname { + color: var(--func-sig-name) !important; + font-weight: bold !important; +} + +/* The parameters inside the parentheses */ +.rst-content dl:not(.docutils) dt .sig-paren, +.rst-content dl:not(.docutils) dt .n, +.rst-content dl:not(.docutils) dt .o, +.rst-content dl:not(.docutils) dt .property, +.rst-content dl:not(.docutils) dt .descclassname { + color: var(--func-sig-param) !important; + font-weight: normal; +} + +/* Optional: Keep the [source] link subtle */ +.viewcode-link { + color: var(--func-sig-border) !important; + font-size: 0.8em; + font-style: italic; } diff --git a/docs/api-overview.rst b/docs/api-overview.rst index 7ae1530039..f3fd27368a 100644 --- a/docs/api-overview.rst +++ b/docs/api-overview.rst @@ -210,6 +210,7 @@ Sensors .. code-block:: pycon >>> import psutil + >>> >>> psutil.sensors_temperatures() {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)], 'asus': [shwtemp(label='', current=47.0, high=None, critical=None)], @@ -241,6 +242,24 @@ Other system info Processes --------- +Oneshot +^^^^^^^ + +.. code-block:: pycon + + >>> import psutil + >>> + >>> p = psutil.Process(7055) + >>> with p.oneshot(): + ... p.name() + ... p.cpu_times() + ... p.memory_info() + ... + 'python3' + pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1, iowait=0.0) + pmem(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374) + >>> + Identity ^^^^^^^^ @@ -253,28 +272,38 @@ Identity psutil.Process(pid=7055, name='python3', status=, started='09:04:44') >>> p.pid 7055 + >>> >>> p.name() 'python3' + >>> >>> p.exe() '/usr/bin/python3' + >>> >>> p.cwd() '/home/giampaolo' + >>> >>> p.cmdline() ['/usr/bin/python3', 'main.py'] + >>> >>> p.status() + >>> >>> p.create_time() 1267551141.5019531 + >>> >>> p.terminal() '/dev/pts/0' + >>> >>> p.environ() {'GREP_OPTIONS': '--color=auto', 'LC_PAPER': 'it_IT.UTF-8', 'SHELL': '/bin/bash', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', ...} + >>> >>> p.is_running() True + >>> >>> p.as_dict() {'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), 'pid': 5457, @@ -322,22 +351,29 @@ CPU / scheduling >>> p.cpu_times() pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1, iowait=0.0) + >>> >>> p.cpu_percent(interval=1.0) 12.1 + >>> >>> p.cpu_affinity() [0, 1, 2, 3] >>> p.cpu_affinity([0, 1]) # set + >>> >>> p.cpu_num() 1 + >>> >>> p.num_ctx_switches() pctxsw(voluntary=78, involuntary=19) + >>> >>> p.nice() 0 >>> p.nice(10) # set - >>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # IO priority (Win and Linux only) + >>> + >>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # set IO priority >>> p.ionice() pionice(ioclass=, value=0) - >>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits (Linux only) + >>> + >>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits >>> p.rlimit(psutil.RLIMIT_NOFILE) (5, 5) >>> @@ -349,6 +385,7 @@ Memory >>> p.memory_info() pmem(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374) + >>> >>> p.memory_info_ex() pmem_ex(rss=3164160, vms=4410163, @@ -362,22 +399,14 @@ Memory rss_shmem=0, swap=0, hugetlb=0) + >>> >>> p.memory_footprint() # "real" USS memory usage pfootprint(uss=2355200, pss=2483712, swap=0) + >>> >>> p.memory_percent() 0.7823 + >>> >>> p.memory_maps() - [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', - rss=32768, - size=2125824, - pss=32768, - shared_clean=0, - shared_dirty=0, - private_clean=20480, - private_dirty=12288, - referenced=32768, - anonymous=12288, - swap=0), pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, @@ -400,18 +429,8 @@ Memory referenced=32768, anonymous=32768, swap=0), - pmmap_grouped(path='[stack]', - rss=2465792, - size=2494464, - pss=2465792, - shared_clean=0, - shared_dirty=0, - private_clean=0, - private_dirty=2465792, - referenced=2277376, - anonymous=2465792, - swap=0), ...] + >>> >>> p.page_faults() ppagefaults(minor=5905, major=3) >>> @@ -440,9 +459,11 @@ Files and connections write_bytes=69632, read_chars=456232, write_chars=517543) + >>> >>> p.open_files() [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768), popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)] + >>> >>> p.net_connections(kind='tcp') [pconn(fd=115, family=, @@ -456,6 +477,7 @@ Files and connections laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status=)] + >>> >>> p.num_fds() 8 >>> @@ -465,6 +487,7 @@ Signals .. code-block:: pycon + >>> p.send_signal(signal.SIGTERM) >>> p.suspend() >>> p.resume() >>> p.terminate() @@ -482,6 +505,7 @@ Other process functions >>> >>> psutil.pids() [1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, ...] + >>> >>> psutil.pid_exists(3) True >>> @@ -499,15 +523,6 @@ Other process functions >>> # waits for multiple processes to terminate >>> gone, alive = psutil.wait_procs(procs_list, timeout=3, callback=on_terminate) >>> - >>> psutil.test() - USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND - root 1 0.0 0.0 24584 2240 Jun17 00:00 init - root 2 0.0 0.0 0 0 Jun17 00:00 kthreadd - ... - giampaolo 31475 0.0 0.0 20760 3024 /dev/pts/0 Jun19 00:00 python2.4 - giampaolo 31721 0.0 2.2 773060 181896 00:04 10:30 chrome - root 31763 0.0 0.0 0 0 00:05 00:00 kworker/0:1 - >>> C heap introspection -------------------- @@ -518,6 +533,7 @@ C heap introspection >>> >>> psutil.heap_info() pheap(heap_used=5177792, mmap_used=819200) + >>> >>> psutil.heap_trim() >>> @@ -536,6 +552,7 @@ Windows services , , ...] + >>> >>> s = psutil.win_service_get('alg') >>> s.as_dict() {'binpath': 'C:\\Windows\\System32\\alg.exe', diff --git a/docs/changelog.rst b/docs/changelog.rst index a7a984136e..1b547008e3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -37,6 +37,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Usability: + - Renewed, more modern theme. - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. - Greatly improved :func:`virtual_memory` doc and many other APIs. diff --git a/docs/conf.py b/docs/conf.py index 30068e6373..58d146ec9e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,4 +63,4 @@ ] # https://pygments.org/styles/ -# pygments_style = "monokai" +pygments_style = "monokai" diff --git a/docs/recipes.rst b/docs/recipes.rst index 6282e4a7b9..98e9255cf1 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -39,8 +39,7 @@ A bit more advanced, check string against :meth:`Process.name`, .. code-block:: python - import os - import psutil + import os, psutil def find_procs_by_name_ex(name): ls = [] @@ -182,7 +181,6 @@ Top N processes by cumulative disk read + write bytes (similar to ``iotop``): import psutil - def top_io_procs(n=5): procs = [] for p in psutil.process_iter(["io_counters"]): @@ -214,8 +212,7 @@ Periodically monitor CPU and memory usage of a process using .. code-block:: python - import time - import psutil + import time, psutil def monitor(pid, interval=1): p = psutil.Process(pid) @@ -240,11 +237,7 @@ Kill a process tree (including grandchildren): .. code-block:: python - import os - import signal - - import psutil - + import os, signal, psutil def kill_proc_tree( pid, @@ -322,8 +315,7 @@ Restart a process: .. code-block:: python - import subprocess - import psutil + import subprocess, psutil def restart_process(pid): p = psutil.Process(pid) @@ -338,8 +330,7 @@ Temporarily pause and resume a process using a context manager: .. code-block:: python - import contextlib - import psutil + import contextlib, psutil @contextlib.contextmanager def suspended(pid): @@ -361,8 +352,7 @@ alternating :meth:`Process.suspend` and :meth:`Process.resume`: .. code-block:: python - import time - import psutil + import time, psutil def throttle(pid, max_cpu_percent=50, interval=0.1): """Slow down a process so it uses at most max_cpu_percent% CPU.""" @@ -380,11 +370,7 @@ Restart a process automatically if it dies: .. code-block:: python - import subprocess - import time - - import psutil - + import subprocess, time, psutil def watchdog(cmd, max_restarts=None, interval=1): """Run cmd as a persistent process. Restart on failure, optionally From 4e091ec11c09b18ad1923dabeb2e8a39f286b474 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 27 Mar 2026 15:54:58 +0100 Subject: [PATCH 1653/1714] Doc: re-arrange CONSTANTs section + remove superfluous parts --- docs/DEVNOTES | 4 + docs/_static/css/custom.css | 6 +- docs/api.rst | 250 ++++++++++++++++-------------------- 3 files changed, 114 insertions(+), 146 deletions(-) diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 6f4225671b..100b78167c 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -29,6 +29,10 @@ REJECTED IDEAS FEATURES ======== + +- `net_if_addrs()` could return AF_BLUETOOTH interfaces. E.g. + https://pypi.org/project/netifaces does this. + - Use resource.getrusage() to get current process CPU times (it has more precision) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 9878cb4560..61fff3bc54 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -47,7 +47,7 @@ --adm-note-bg: var(--gray-100); --adm-note-border: var(--gray-400); - --adm-inline-code-bg: var(--gray-300); + --adm-seealso-accent-border: var(--blue-500); --adm-seealso-bg: var(--blue-200); --adm-seealso-border: var(--blue-300); @@ -389,10 +389,6 @@ p.admonition-title:after { display: none !important; } -.note code, -.important code { - background: var(--adm-inline-code-bg) !important; -} /* ================================================================== */ /* Version directives (versionadded / versionchanged / deprecated) */ diff --git a/docs/api.rst b/docs/api.rst index 3e2b42b096..f0d3b838be 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -666,7 +666,7 @@ Network numbers no longer wrap (restart from zero) across calls thanks to new *nowrap* argument. -.. function:: net_connections(kind='inet') +.. function:: net_connections(kind="inet") Return system-wide socket connections as a list of named tuples. Every named tuple provides 7 attributes: @@ -777,6 +777,8 @@ Network Return the addresses associated to each :term:`NIC` (network interface card) installed on the system as a dictionary whose keys are the NIC names and value is a list of named tuples for each address assigned to the NIC. + You can have more than one address of the same family associated with each + interface (that's why dict values are lists). Each named tuple includes 5 fields: - **family**: the address family, either :data:`socket.AF_INET`, @@ -785,10 +787,11 @@ Network - **address**: the primary NIC address (may be ``None`` in case of virtual or unconfigured interfaces). - **netmask**: the netmask address (may be ``None``). - - **broadcast**: the broadcast address (may be ``None``). + - **broadcast**: the broadcast address. May be ``None``. Always ``None`` on + Windows. - **ptp**: stands for "point to point"; it's the destination address on a point to point interface (typically a VPN). *broadcast* and *ptp* are - mutually exclusive. May be ``None``. + mutually exclusive. May be ``None``. Always ``None`` on Windows. .. code-block:: pycon @@ -802,18 +805,6 @@ Network snicaddr(family=, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]} >>> - .. note:: - if you're interested in others families (e.g. AF_BLUETOOTH) you can use - the more powerful `netifaces `_ - extension. - - .. note:: - you can have more than one address of the same family associated with each - interface (that's why dict values are lists). - - .. note:: - *broadcast* and *ptp* are not supported on Windows and are always ``None``. - .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. .. versionadded:: 3.0.0 @@ -1072,18 +1063,6 @@ Functions 3 ksoftirqd/0 root ... - A dict comprehension to create a ``{pid: name, ...}`` data structure: - - .. code-block:: pycon - - >>> import psutil - >>> procs = {p.pid: p.name() for p in psutil.process_iter(['name'])} - >>> procs - {1: 'systemd', - 2: 'kthreadd', - 3: 'ksoftirqd/0', - ...} - Clear internal cache: .. code-block:: pycon @@ -1175,17 +1154,15 @@ Exceptions .. exception:: ZombieProcess(pid, name=None, ppid=None, msg=None) - This may be raised by :class:`Process` class methods when querying a - :term:`zombie process` on UNIX (Windows doesn't have zombie processes). - *name* and *ppid* attributes are available if :meth:`Process.name` or + A subclass of :exc:`NoSuchProcess` which may be raised by :class:`Process` + class methods when dealing with a :term:`zombie process` on UNIX (Windows + doesn't have zombie processes). + *name* and *ppid* attributes are set if :meth:`Process.name` or :meth:`Process.ppid` methods were called before the process turned into a zombie. - .. note:: - - this is a subclass of :exc:`NoSuchProcess` so if you're not interested - in retrieving zombies (e.g. when using :func:`process_iter`) you can - ignore this exception and just catch :exc:`NoSuchProcess`. + If you're not interested in detecting zombie processes you can ignore this + exception and just catch :exc:`NoSuchProcess`. .. seealso:: :ref:`faq_zombie_process` @@ -1230,8 +1207,8 @@ Process class .. note:: the way this class is bound to a process is via its **PID**. - That means that if the process terminates and the OS reuses its PID you may - inadvertently end up interacting with another process. To prevent this + That means that if the process terminates, and the OS reuses its PID, you + may inadvertently end up interacting with another process. To prevent this problem you can use :meth:`is_running` first. Some methods (e.g. setters and signal-related methods) perform an additional check based on PID + creation time and will raise @@ -1423,21 +1400,26 @@ Process class >>> datetime.datetime.fromtimestamp(p.create_time()).strftime("%Y-%m-%d %H:%M:%S") '2011-03-05 18:03:52' - .. method:: as_dict(attrs=None, ad_value=None) - - Utility method retrieving multiple process information as a dictionary. - If *attrs* is specified it must be a list of strings reflecting available - :class:`Process` class's attribute names. Here's a list of possible string - values: - ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_footprint'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_info_ex'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'```. - If *attrs* argument is not passed all public read only attributes are - assumed. - *ad_value* is the value which gets assigned to a dict key in case - :exc:`AccessDenied` or :exc:`ZombieProcess` exception is raised when - retrieving that particular process information. - Internally, :meth:`as_dict` uses :meth:`oneshot` context manager so - there's no need you use it also. + .. method:: as_dict(attrs=None, ad_value=None) + + Utility method retrieving multiple process information as a dictionary. + If *attrs* is specified it must be a list of strings reflecting available + :class:`Process` class's attribute names. Here's a list of possible string + values: + ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_footprint'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_info_ex'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'``. + + If *attrs* argument is not passed all public read only attributes are + assumed. + + *ad_value* is the value which gets assigned to a dict key in case + :exc:`AccessDenied` or :exc:`ZombieProcess` exception is raised when + retrieving that particular process information (default ``None``). + The ``'net_connections'`` attribute is retrieved by calling + :meth:`Process.net_connections` with ``kind="inet"``. + + Internally, :meth:`as_dict` uses :meth:`oneshot` context manager so + there's no need you use it also. .. code-block:: pycon >>> import psutil @@ -2264,8 +2246,8 @@ Process class Tools like ProcessHacker have the same limitation. .. warning:: - on BSD this method can return files with a null path ("") due to a - kernel bug, hence it's not reliable + on BSD this method can return paths as an empty string due to a kernel + bug, hence it's not reliable (see `issue 595 `_). .. versionchanged:: 3.1.0 @@ -2347,11 +2329,11 @@ Process class the returned list may be incomplete. .. note:: - (Solaris) UNIX sockets are not supported. + (Linux, FreeBSD) **raddr** field for UNIX sockets is always set to an + empty string. This is a limitation of the OS. .. note:: - (Linux, FreeBSD) **raddr** field for UNIX sockets is always set to "". - This is a limitation of the OS. + (Solaris) UNIX sockets are not supported. .. note:: (OpenBSD) **laddr** and **raddr** fields for UNIX sockets are always set to @@ -2795,10 +2777,13 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. versionadded:: 5.1.0 +.. _const-oses: + Operating system constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. _const-oses: +``bool`` constants which define what platform you're on. +``True`` if on the platform, ``False`` otherwise. .. data:: POSIX .. data:: LINUX @@ -2810,15 +2795,6 @@ Operating system constants .. data:: BSD .. data:: SUNOS .. data:: AIX - - ``bool`` constants which define what platform you're on. E.g. if on Windows, - :const:`WINDOWS` constant will be ``True``, all others will be ``False``. - - .. versionadded:: 4.0.0 - - .. versionchanged:: 5.4.0 - added AIX. - .. data:: OSX Alias for :const:`MACOS`. @@ -2826,10 +2802,17 @@ Operating system constants .. deprecated:: 5.4.7 use :const:`MACOS` instead. +.. _const-pstatus: + Process status constants ^^^^^^^^^^^^^^^^^^^^^^^^ -.. _const-pstatus: +Represent the current status of a process. Returned by :meth:`Process.status`. + +.. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessStatus` enum members (were plain + strings). + See :ref:`migration guide `. .. data:: STATUS_RUNNING .. data:: STATUS_SLEEPING @@ -2841,25 +2824,29 @@ Process status constants .. data:: STATUS_WAKE_KILL .. data:: STATUS_WAKING .. data:: STATUS_PARKED (Linux) + + .. versionadded:: 5.4.7 + .. data:: STATUS_IDLE (Linux, macOS, FreeBSD) .. data:: STATUS_LOCKED (FreeBSD) .. data:: STATUS_WAITING (FreeBSD) .. data:: STATUS_SUSPENDED (NetBSD) - Represent a process status. Returned by :meth:`Process.status`. - These constants are members of the :class:`psutil.ProcessStatus` enum. + .. versionadded:: 3.4.1 - .. versionadded:: 3.4.1 ``STATUS_SUSPENDED`` (NetBSD) +Process priority constants +^^^^^^^^^^^^^^^^^^^^^^^^^^ - .. versionadded:: 5.4.7 ``STATUS_PARKED`` (Linux) +Represent the priority of a process on Windows (see `SetPriorityClass`_). +They can be used in conjunction with :meth:`Process.nice` to get or +set process priority. - .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessStatus` enum members (were plain - strings). - See :ref:`migration guide `. +.. availability:: Windows -Process priority constants -^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessPriority` enum members (were plain + integers). + See :ref:`migration guide `. .. _const-prio: @@ -2870,17 +2857,18 @@ Process priority constants .. data:: IDLE_PRIORITY_CLASS .. data:: BELOW_NORMAL_PRIORITY_CLASS - Represent the priority of a process on Windows (see `SetPriorityClass`_). - They can be used in conjunction with :meth:`Process.nice` to get or - set process priority. - These constants are members of the :class:`psutil.ProcessPriority` enum. +------------------------------------------------------------------------------- - .. availability:: Windows +Process I/O priority constants +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessPriority` enum members (were plain - integers). - See :ref:`migration guide `. +Represent the priority I/O priority of a process (Linux and Windows only). +They can be used in conjunction with :meth:`Process.ionice`. + +.. versionchanged:: 8.0.0 + constants are now :class:`psutil.ProcessIOPriority` enum members (previously + ``IOPriority`` enum). + See :ref:`migration guide `. .. _const-ioprio-linux: @@ -2889,11 +2877,6 @@ Process priority constants .. data:: IOPRIO_CLASS_BE .. data:: IOPRIO_CLASS_IDLE - A set of integers representing the I/O priority of a process on Linux. They - can be used in conjunction with :meth:`Process.ionice` to get or set - process I/O priority. - These constants are members of the :class:`psutil.ProcessIOPriority` - enum. :const:`IOPRIO_CLASS_NONE` and :const:`IOPRIO_CLASS_BE` (best effort) is the default for any process that hasn't set a specific I/O priority. :const:`IOPRIO_CLASS_RT` (real time) means the process is given first access @@ -2906,11 +2889,6 @@ Process priority constants .. availability:: Linux - .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessIOPriority` enum members - (previously ``IOPriority`` enum). - See :ref:`migration guide `. - .. _const-ioprio-windows: .. data:: IOPRIO_VERYLOW @@ -2918,25 +2896,25 @@ Process priority constants .. data:: IOPRIO_NORMAL .. data:: IOPRIO_HIGH - A set of integers representing the I/O priority of a process on Windows. - They can be used in conjunction with :meth:`Process.ionice` to get - or set process I/O priority. - These constants are members of the :class:`psutil.ProcessIOPriority` - enum. - .. availability:: Windows .. versionadded:: 5.6.2 - .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessIOPriority` enum members - (previously ``IOPriority`` enum). - See :ref:`migration guide `. +.. _const-rlimit: Process resource constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. _const-rlimit: +Constants for getting or setting process resource limits, to be used in in +conjunction with :meth:`Process.rlimit`. See :func:`resource.getrlimit` for +further information. + +.. availability:: Linux, FreeBSD + +.. versionchanged:: 8.0.0 + these constants are now :class:`psutil.ProcessRlimit` enum members (were + plain integers). + See :ref:`migration guide `. Linux / FreeBSD: @@ -2964,29 +2942,31 @@ Linux specific: FreeBSD specific: .. data:: RLIMIT_SWAP + + .. versionadded:: 5.7.3 + + .. data:: RLIMIT_SBSIZE - .. data:: RLIMIT_NPTS -Constants used for getting and setting process resource limits to be used in -conjunction with :meth:`Process.rlimit`. See :func:`resource.getrlimit` -for further information. -These constants are members of the :class:`psutil.ProcessRlimit` enum. + .. versionadded:: 5.7.3 -.. availability:: Linux, FreeBSD + .. data:: RLIMIT_NPTS -.. versionchanged:: 5.7.3 - added FreeBSD support, added ``RLIMIT_SWAP``, ``RLIMIT_SBSIZE``, - ``RLIMIT_NPTS``. + .. versionadded:: 5.7.3 -.. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessRlimit` enum members (were plain - integers). - See :ref:`migration guide `. +.. _const-conn: Connections constants ^^^^^^^^^^^^^^^^^^^^^ -.. _const-conn: +:class:`enum.StrEnum` constants representing the status of a TCP connection. +Returned by :meth:`Process.net_connections` and :func:`psutil.net_connections` +(**status** field). + +.. versionchanged:: 8.0.0 + constants are now :class:`psutil.ConnectionStatus` enum members (were + plain strings). + See :ref:`migration guide `. .. data:: CONN_ESTABLISHED .. data:: CONN_SYN_SENT @@ -3004,16 +2984,6 @@ Connections constants .. data:: CONN_IDLE (Solaris) .. data:: CONN_BOUND (Solaris) - A set of strings representing the status of a TCP connection. - Returned by :meth:`Process.net_connections` and - :func:`psutil.net_connections` (**status** field). - These constants are members of the :class:`psutil.ConnectionStatus` enum. - - .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ConnectionStatus` enum members (were - plain strings). - See :ref:`migration guide `. - Hardware constants ^^^^^^^^^^^^^^^^^^ @@ -3021,8 +2991,8 @@ Hardware constants .. data:: AF_LINK - Constant which identifies a MAC address associated with a network interface. - To be used in conjunction with :func:`psutil.net_if_addrs`. + Identifies a MAC address associated with a network interface. Returned by + :func:`psutil.net_if_addrs` (**family** field). .. versionadded:: 3.0.0 @@ -3032,11 +3002,10 @@ Hardware constants .. data:: NIC_DUPLEX_HALF .. data:: NIC_DUPLEX_UNKNOWN - Constants which identifies whether a :term:`NIC` (network interface card) has - full or half mode speed. NIC_DUPLEX_FULL means the NIC is able to send and - receive data (files) simultaneously, NIC_DUPLEX_HALF means the NIC can either - send or receive data at a time. - To be used in conjunction with :func:`psutil.net_if_stats`. + Identifies whether a :term:`NIC` operates in full, half, or unknown duplex + mode. + FULL allows simultaneous send/receive, HALF allows only one at a time. + Returned by :func:`psutil.net_if_stats` (**duplex** field). .. versionadded:: 3.0.0 @@ -3045,9 +3014,8 @@ Hardware constants .. data:: POWER_TIME_UNKNOWN .. data:: POWER_TIME_UNLIMITED - Whether the remaining time of the battery cannot be determined or is - unlimited. - May be assigned to :func:`psutil.sensors_battery`'s *secsleft* field. + Whether the remaining time of a battery cannot be determined or is unlimited. + May be assigned to :func:`psutil.sensors_battery`'s **secsleft** field. .. versionadded:: 5.1.0 From 445df3abcb57b85d2bb80d6b7b494dbad61c6026 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Mar 2026 00:24:20 +0100 Subject: [PATCH 1654/1714] Doc: explain process states + add example on how to create a zombie --- docs/api.rst | 80 ++++++++++++++++++++++++++++++++++++++++++++++------ docs/faq.rst | 12 ++++++++ 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index f0d3b838be..61d19c4757 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2815,24 +2815,86 @@ Represent the current status of a process. Returned by :meth:`Process.status`. See :ref:`migration guide `. .. data:: STATUS_RUNNING + + The process is running or ready to run (e.g. ``while True: pass``). + .. data:: STATUS_SLEEPING + + The process is dormant (e.g. during ``time.sleep()``) but can be woken up, + e.g. via a signal. + .. data:: STATUS_DISK_SLEEP + + The process is waiting for disk I/O to complete. The kernel usually ignores + signals in this state to prevent data corruption. E.g. ``os.read(fd, 1024)`` + on a slow / blocked device can produce this state. + .. data:: STATUS_STOPPED + + The process was suspended via ``SIGSTOP`` signal and won't run until it + receives ``SIGCONT``. + .. data:: STATUS_TRACING_STOP + + The process is temporarily halted because it is being inspected by a + debugger (e.g. via ``strace -p ``). + .. data:: STATUS_ZOMBIE + + The process has finished execution and released its resources, but it + remains in the process table until the parent reaps it via ``wait()``. + .. data:: STATUS_DEAD + + The final process state where the kernel is actively removing its entry from + the system. Occurs after the process is reaped. + .. data:: STATUS_WAKE_KILL + + (Linux only) A variant of :data:`STATUS_DISK_SLEEP` where the process can be + awakened by ``SIGKILL``. Used for tasks which might otherwise remain blocked + indefinitely, e.g. unresponsive network filesystems such as NFS, as in + ``open("/mnt/nfs_hung/file").read()``. + .. data:: STATUS_WAKING -.. data:: STATUS_PARKED (Linux) - .. versionadded:: 5.4.7 + (Linux only) A transient state right before the process becomes runnable + (:data:`STATUS_RUNNING`). + +.. data:: STATUS_PARKED + + (Linux only) A dormant state for kernel threads tied to a specific CPU. + These threads are "parked" when a CPU core is taken offline and will + remain inactive until the core is re-enabled. + + .. versionadded:: 5.4.7 + +.. data:: STATUS_IDLE + + (Linux, macOS, FreeBSD) A sleep for kernel threads waiting for work. + + .. versionadded:: 3.4.1 + +.. data:: STATUS_LOCKED + + (FreeBSD only) The process is blocked specifically waiting for a + kernel-level synchronization primitive (e.g. a mutex). + + .. versionadded:: 3.4.1 + +.. data:: STATUS_WAITING + + (FreeBSD only) The process is waiting in a kernel sleep queue for a + specific system event to occur. + + .. versionadded:: 3.4.1 + +.. data:: STATUS_SUSPENDED -.. data:: STATUS_IDLE (Linux, macOS, FreeBSD) -.. data:: STATUS_LOCKED (FreeBSD) -.. data:: STATUS_WAITING (FreeBSD) -.. data:: STATUS_SUSPENDED (NetBSD) + (NetBSD only) The process has been explicitly paused, similar to + the stopped state but managed by the NetBSD scheduler. - .. versionadded:: 3.4.1 + .. versionadded:: 3.4.1 Process priority constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2906,8 +2968,8 @@ Process resource constants ^^^^^^^^^^^^^^^^^^^^^^^^^^ Constants for getting or setting process resource limits, to be used in in -conjunction with :meth:`Process.rlimit`. See :func:`resource.getrlimit` for -further information. +conjunction with :meth:`Process.rlimit`. The meaning of each constant is +explained in :func:`resource.getrlimit` documentation. .. availability:: Linux, FreeBSD diff --git a/docs/faq.rst b/docs/faq.rst index 5edc609fec..e39b4e98c1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -165,6 +165,18 @@ for a :term:`zombie process`. the platform. - :meth:`Process.as_dict` will not crash. +**How to create a zombie:** + +.. code-block:: python + + import os, time + + pid = os.fork() # the zombie + if pid == 0: + os._exit(0) # child exits immediately + else: + time.sleep(1000) # parent does NOT call wait() + **How to detect zombies:** .. code-block:: python From 0f0cdaa7d05f6a1dbbdb05b0e116186502c4e82a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 28 Mar 2026 12:27:29 +0100 Subject: [PATCH 1655/1714] Change CSS style of inline literals and API symbols --- docs/_static/css/custom.css | 43 +++++++++++++++++ docs/api.rst | 61 +++++++++++------------ docs/faq.rst | 96 ++++++++++++++++++------------------- docs/glossary.rst | 2 + docs/migration.rst | 7 +-- 5 files changed, 126 insertions(+), 83 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 61fff3bc54..8726fcebd9 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -87,6 +87,12 @@ } } +@media (max-width: 768px) { + .wy-nav-content { + padding: 0 12px 1.618em 12px !important; + } +} + /* ================================================================== */ /* Navigation */ /* ================================================================== */ @@ -134,6 +140,11 @@ footer div[role="navigation"][aria-label="Footer"] { /* Headings */ /* ================================================================== */ +h1, h2, h3, h4 { + margin-top: 20px !important; + margin-bottom: 10px !important; +} + h1, h2 { padding: 5px; border-bottom: 1px solid var(--h1-h2-border); @@ -156,6 +167,10 @@ h4 { font-size: 0.9rem; } +.rst-content p { + margin-bottom: 16px !important; +} + /* ================================================================== */ /* Tables */ /* ================================================================== */ @@ -265,6 +280,33 @@ h4 { padding-right: 2px; } +/* ================================================================== */ +/* Inline code / API refs */ +/* ================================================================== */ + +/* inline literals: ``like this`` */ +.rst-content code, .rst-content tt, code { + color: inherit !important; + background: rgba(175, 184, 193, 0.2) !important; + border: none !important; + border-radius: 4px !important; + padding: .1em .35em !important; + font-family: var(--code-font) !important; + font-weight: normal !important; +} + +/* cross-references to API symbols like :func:`psutil.users` */ +.rst-content a.reference code, +.rst-content a.reference tt { + color: inherit !important; + /*background: rgba(175, 184, 193, 0.2) !important;*/ + background: none !important; + /*font-weight: bold !important;*/ + font-size:90% !important; + text-decoration: underline; + text-underline-offset: 4px; +} + /* ================================================================== */ /* Code blocks */ /* ================================================================== */ @@ -276,6 +318,7 @@ h4 { .highlight, .highlight pre { + font-size: 92% !important; font-family: var(--code-font) !important; } diff --git a/docs/api.rst b/docs/api.rst index 61d19c4757..742df94656 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,7 +9,7 @@ API reference :ref:`migration guide ` if upgrading from 7.x. .. important:: - Do not rely on positional unpacking of named tuples. + do not rely on positional unpacking of named tuples. Always use attribute access (e.g. ``t.rss``). .. contents:: @@ -420,7 +420,8 @@ Memory ... >>> - .. note:: if you just want to know how much physical memory is left in a + .. note:: + if you just want to know how much physical memory is left in a cross-platform manner, simply rely on **available** and **percent** fields. @@ -538,7 +539,7 @@ Disks sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) .. note:: - On UNIX, *path* must point to a path within a **mounted** filesystem partition. + on UNIX, *path* must point to a path within a **mounted** filesystem partition. .. note:: UNIX usually reserves 5% of the total disk space for the root user. @@ -701,27 +702,27 @@ Network +----------------+-----------------------------------------------------+ | Kind value | Connections using | +================+=====================================================+ - | ``"inet"`` | IPv4 and IPv6 | + | ``'inet'`` | IPv4 and IPv6 | +----------------+-----------------------------------------------------+ - | ``"inet4"`` | IPv4 | + | ``'inet4'`` | IPv4 | +----------------+-----------------------------------------------------+ - | ``"inet6"`` | IPv6 | + | ``'inet6'`` | IPv6 | +----------------+-----------------------------------------------------+ - | ``"tcp"`` | TCP | + | ``'tcp'`` | TCP | +----------------+-----------------------------------------------------+ - | ``"tcp4"`` | TCP over IPv4 | + | ``'tcp4'`` | TCP over IPv4 | +----------------+-----------------------------------------------------+ - | ``"tcp6"`` | TCP over IPv6 | + | ``'tcp6'`` | TCP over IPv6 | +----------------+-----------------------------------------------------+ - | ``"udp"`` | UDP | + | ``'udp'`` | UDP | +----------------+-----------------------------------------------------+ - | ``"udp4"`` | UDP over IPv4 | + | ``'udp4'`` | UDP over IPv4 | +----------------+-----------------------------------------------------+ - | ``"udp6"`` | UDP over IPv6 | + | ``'udp6'`` | UDP over IPv6 | +----------------+-----------------------------------------------------+ - | ``"unix"`` | UNIX socket (both UDP and TCP protocols) | + | ``'unix'`` | UNIX socket (both UDP and TCP protocols) | +----------------+-----------------------------------------------------+ - | ``"all"`` | the sum of all the possible families and protocols | + | ``'all'`` | the sum of all the possible families and protocols | +----------------+-----------------------------------------------------+ On macOS and AIX this function requires root privileges. @@ -738,7 +739,7 @@ Network ...] .. warning:: - On Linux, retrieving some connections requires root privileges. If psutil is + on Linux, retrieving some connections requires root privileges. If psutil is not run as root, those connections are silently skipped instead of raising :exc:`PermissionError`. That means the returned list may be incomplete. @@ -1071,7 +1072,7 @@ Functions .. note:: - Since :class:`Process` instances are reused across calls, a subsequent + since :class:`Process` instances are reused across calls, a subsequent :func:`process_iter` call will overwrite or clear any previously pre-fetched values. Do not rely on cached values from a prior iteration. @@ -1200,7 +1201,7 @@ Process class .. note:: - In order to efficiently fetch more than one information about the process + in order to efficiently fetch more than one information about the process at the same time, make sure to use either :meth:`oneshot` context manager or :meth:`as_dict` utility method. @@ -2287,27 +2288,27 @@ Process class +----------------+-----------------------------------------------------+ | Kind value | Connections using | +================+=====================================================+ - | ``"inet"`` | IPv4 and IPv6 | + | ``'inet'`` | IPv4 and IPv6 | +----------------+-----------------------------------------------------+ - | ``"inet4"`` | IPv4 | + | ``'inet4'`` | IPv4 | +----------------+-----------------------------------------------------+ - | ``"inet6"`` | IPv6 | + | ``'inet6'`` | IPv6 | +----------------+-----------------------------------------------------+ - | ``"tcp"`` | TCP | + | ``'tcp'`` | TCP | +----------------+-----------------------------------------------------+ - | ``"tcp4"`` | TCP over IPv4 | + | ``'tcp4'`` | TCP over IPv4 | +----------------+-----------------------------------------------------+ - | ``"tcp6"`` | TCP over IPv6 | + | ``'tcp6'`` | TCP over IPv6 | +----------------+-----------------------------------------------------+ - | ``"udp"`` | UDP | + | ``'udp'`` | UDP | +----------------+-----------------------------------------------------+ - | ``"udp4"`` | UDP over IPv4 | + | ``'udp4'`` | UDP over IPv4 | +----------------+-----------------------------------------------------+ - | ``"udp6"`` | UDP over IPv6 | + | ``'udp6'`` | UDP over IPv6 | +----------------+-----------------------------------------------------+ - | ``"unix"`` | UNIX socket (both UDP and TCP protocols) | + | ``'unix'`` | UNIX socket (both UDP and TCP protocols) | +----------------+-----------------------------------------------------+ - | ``"all"`` | the sum of all the possible families and protocols | + | ``'all'`` | the sum of all the possible families and protocols | +----------------+-----------------------------------------------------+ .. code-block:: pycon @@ -2323,7 +2324,7 @@ Process class pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=)] .. warning:: - On Linux, retrieving connections for certain processes requires root + on Linux, retrieving connections for certain processes requires root privileges. If psutil is not run as root, those connections are silently skipped instead of raising :exc:`psutil.AccessDenied`. That means the returned list may be incomplete. @@ -2462,7 +2463,7 @@ Process class .. note:: - When *timeout* is not ``None`` and the platform supports it, an + when *timeout* is not ``None`` and the platform supports it, an efficient event-driven mechanism is used to wait for process termination: - Linux >= 5.3 with Python >= 3.9 uses :func:`os.pidfd_open` + :func:`select.poll` diff --git a/docs/faq.rst b/docs/faq.rst index e39b4e98c1..c036a52646 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -34,8 +34,6 @@ Always use attribute access instead: See the :ref:`migration guide ` for the full list of field-order changes in 8.0. -------------------------------------------------------------------------------- - Exceptions ---------- @@ -143,13 +141,50 @@ An even simpler pattern is to catch :exc:`Error`, which implies both except psutil.Error: pass +Processes +--------- + +.. _faq_pid_reuse: + +PID reuse +^^^^^^^^^ + +Operating systems recycle PIDs. A :class:`Process` object obtained now may +later refer to a different process if the original one terminated and a new one +was assigned the same PID. + +**How psutil handles this:** + +- *Most read-only methods* (e.g. :meth:`Process.name`, + :meth:`Process.cpu_percent`) do **not** check for PID reuse and instead + query whatever process currently holds that PID. + +- *Signal methods* (e.g. :meth:`Process.send_signal`, + :meth:`Process.suspend`, :meth:`Process.resume`, + :meth:`Process.terminate`, :meth:`Process.kill`) **do** check for PID + reuse (via PID + creation time) before acting, raising + :exc:`NoSuchProcess` if the PID was recycled. This prevents accidentally + killing the wrong process (`BPO-6973`_). + +- *Set methods* :meth:`Process.nice` (set), :meth:`Process.ionice` (set), + :meth:`Process.cpu_affinity` (set), and + :meth:`Process.rlimit` (set) also perform this check before applying + changes. + +:meth:`Process.is_running` is the recommended way to verify whether a +:class:`Process` instance still refers to the same process. It compares +PID and creation time, and returns ``False`` if the PID was reused. +Prefer it over :func:`pid_exists`. + .. _faq_zombie_process: -What is ZombieProcess? -^^^^^^^^^^^^^^^^^^^^^^^ +What is a zombie process? +^^^^^^^^^^^^^^^^^^^^^^^^^ -:exc:`ZombieProcess` is a subclass of :exc:`NoSuchProcess` raised on UNIX -for a :term:`zombie process`. +A :term:`zombie process` is a process that has finished execution but whose +entry remains in the process table until the parent calls ``wait()``. When +psutil encounters a zombie process it raises :exc:`ZombieProcess`, a subclass +of :exc:`NoSuchProcess`. **What you can and cannot do with a zombie:** @@ -159,7 +194,7 @@ for a :term:`zombie process`. - The zombie appears in :func:`process_iter` and :func:`pids`. - Sending signals (:meth:`Process.terminate`, :meth:`Process.kill`, etc.) has no effect. -- Most other methods (:meth:`Process.cmdline`, :meth:`Process.exe`, +- Most methods (:meth:`Process.cmdline`, :meth:`Process.exe`, :meth:`Process.memory_maps`, etc.) may raise :exc:`ZombieProcess`, return a meaningful value, or return a null/empty value depending on the platform. @@ -187,47 +222,12 @@ for a :term:`zombie process`. if p.status() == psutil.STATUS_ZOMBIE: print(f"zombie: pid={p.pid}") -**How to get rid of a zombie:** the only way is to have its parent -process call ``wait()`` (or ``waitpid()``). If the parent never does -this, killing the parent will cause the zombie to be re-parented to -``init`` / ``systemd``, which will reap it automatically. +**How to get rid of a zombie:** -------------------------------------------------------------------------------- +The only way is to have its parent process call ``wait()`` (or ``waitpid()``). +If the parent never does this, killing the parent will cause the zombie to be +re-parented to ``init`` / ``systemd``, which will reap it automatically. -Processes ---------- - -.. _faq_pid_reuse: - -PID reuse -^^^^^^^^^ - -Operating systems recycle PIDs. A :class:`Process` object obtained now may -later refer to a different process if the original one terminated and a new one -was assigned the same PID. - -**How psutil handles this:** - -- *Most read-only methods* (e.g. :meth:`Process.name`, - :meth:`Process.cpu_percent`) do **not** check for PID reuse and instead - query whatever process currently holds that PID. - -- *Signal methods* (e.g. :meth:`Process.send_signal`, - :meth:`Process.suspend`, :meth:`Process.resume`, - :meth:`Process.terminate`, :meth:`Process.kill`) **do** check for PID - reuse (via PID + creation time) before acting, raising - :exc:`NoSuchProcess` if the PID was recycled. This prevents accidentally - killing the wrong process (`BPO-6973`_). - -- *Set methods* :meth:`Process.nice` (set), :meth:`Process.ionice` (set), - :meth:`Process.cpu_affinity` (set), and - :meth:`Process.rlimit` (set) also perform this check before applying - changes. - -:meth:`Process.is_running` is the recommended way to verify whether a -:class:`Process` instance still refers to the same process. It compares -PID and creation time, and returns ``False`` if the PID was reused. -Prefer it over :func:`pid_exists`. .. _faq_pid_exists_vs_isrunning: @@ -242,8 +242,6 @@ against reuse (it's faster). Use :meth:`Process.is_running` when you hold a :class:`Process` object and want to confirm it still refers to the same process. -------------------------------------------------------------------------------- - CPU --- @@ -310,8 +308,6 @@ What is the difference between psutil, os, and multiprocessing cpu_count()? - :func:`psutil.cpu_count` with ``logical=False`` returns the number of **physical** cores, which has no stdlib equivalent. -------------------------------------------------------------------------------- - Memory ------ diff --git a/docs/glossary.rst b/docs/glossary.rst index 39a147e13f..f5d9570567 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -314,3 +314,5 @@ Glossary table until its parent calls ``wait()``. Zombies hold a PID but consume no CPU or memory. See :ref:`faq_zombie_process`. + + .. seealso:: :ref:`faq_zombie_process` diff --git a/docs/migration.rst b/docs/migration.rst index 0a0d3f1055..9519b2e842 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -22,10 +22,11 @@ Migrating to 8.0 Key breaking changes in 8.0: -- ``Process.info`` is deprecated: use direct methods. +- :attr:`Process.info` is deprecated: use direct methods. - Named tuple field order changed: stop positional unpacking. - Some return types are now enums instead of strings. -- ``memory_full_info()`` deprecated: use ``memory_footprint()`` +- :meth:`Process.memory_full_info` deprecated: use + :meth:`Process.memory_footprint`. - Python 3.6 dropped. .. important:: @@ -62,7 +63,7 @@ are handled transparently (returning ``ad_value``, default ``None``). Named tuple field order changed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- :func:`cpu_times`: ``user, system, idle`` fields changed order on Linux, +- :func:`cpu_times`: ``user``, ``system``, ``idle`` fields changed order on Linux, macOS and BSD. They are now always the first 3 fields on all platforms, with platform-specific fields (e.g. ``nice``) following. Positional access (e.g. ``cpu_times()[3]``) will silently return the wrong field. Always use From 388a3a48f8bfd1f8a75c7e92d5ca2e183385fb8d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 29 Mar 2026 12:03:52 +0200 Subject: [PATCH 1656/1714] Rename non-public enum Negsignal -> NegSignal Signed-off-by: Giampaolo Rodola --- docs/api.rst | 2 +- psutil/_psposix.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 742df94656..d786a216ed 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2459,7 +2459,7 @@ Process class >>> p = psutil.Process(9891) >>> p.terminate() >>> p.wait() - + .. note:: diff --git a/psutil/_psposix.py b/psutil/_psposix.py index b33f471841..384ba1b97a 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -48,15 +48,15 @@ def pid_exists(pid): return True -Negsignal = enum.IntEnum( - 'Negsignal', {x.name: -x.value for x in signal.Signals} +NegSignal = enum.IntEnum( + 'NegSignal', {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) + return NegSignal(num) except ValueError: return num From 2ba5be62fec40bb4ce3bd446bbeba704a9e858c5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 29 Mar 2026 13:06:28 +0200 Subject: [PATCH 1657/1714] Add JS script which highlights numbers and str in pycon REPL --- MANIFEST.in | 1 + docs/DEVNOTES | 1 + docs/_static/css/custom.css | 20 +++++++++--- docs/_static/highlight-numbers.js | 21 ++++++++++++ docs/api-overview.rst | 54 +++++++++++++++---------------- docs/api.rst | 22 +++++++------ docs/conf.py | 3 ++ docs/migration.rst | 24 ++++++++++++-- 8 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 docs/_static/highlight-numbers.js diff --git a/MANIFEST.in b/MANIFEST.in index 01a9534669..c2257a6419 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,6 +22,7 @@ include docs/_links.rst include docs/_static/copybutton.js include docs/_static/css/custom.css include docs/_static/favicon.ico +include docs/_static/highlight-numbers.js include docs/_static/sidebar.js include docs/_templates/layout.html include docs/adoption.rst diff --git a/docs/DEVNOTES b/docs/DEVNOTES index 100b78167c..6ed5892451 100644 --- a/docs/DEVNOTES +++ b/docs/DEVNOTES @@ -29,6 +29,7 @@ REJECTED IDEAS FEATURES ======== +- #2794: enrich Process.wait() return value with exit code enums. - `net_if_addrs()` could return AF_BLUETOOTH interfaces. E.g. https://pypi.org/project/netifaces does this. diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 8726fcebd9..63db2af0ef 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -291,6 +291,7 @@ h4 { border: none !important; border-radius: 4px !important; padding: .1em .35em !important; + font-size: 85% !important; font-family: var(--code-font) !important; font-weight: normal !important; } @@ -311,6 +312,11 @@ h4 { /* Code blocks */ /* ================================================================== */ +/* Python number, string, and namedtuple name highlights in pycon output */ +.highlight-pycon .pycon-number { color: #ae81ff; } +.highlight-pycon .pycon-string { color: #e6db74; } +.highlight-pycon .pycon-name { color: #89b8d4; } + /* reduce bottom margin of code blocks inside list items */ .rst-content li > div[class^='highlight'] { margin-bottom: 6px !important; @@ -343,13 +349,19 @@ div[class^="highlight-"] { /* Links */ /* ================================================================== */ -/* external links: dotted underline (except intersphinx and image links) */ +/* external links: small icon instead of dotted underline */ a.reference.external:not([href*="docs.python.org"]):not(:has(img)) { - text-decoration: underline dotted; + text-decoration: none; } -a.reference.external:not([href*="docs.python.org"]):not(:has(img)):hover { - text-decoration: underline solid; +a.reference.external:not([href*="docs.python.org"]):not(:has(img))::after { + content: '\f08e'; + font-family: FontAwesome; + font-size: 0.4em; + margin-left: 0.25em; + padding-left: 1px; + vertical-align: super; + opacity: 0.6; } a.external[href^="#"] { diff --git a/docs/_static/highlight-numbers.js b/docs/_static/highlight-numbers.js new file mode 100644 index 0000000000..a95e5d80fd --- /dev/null +++ b/docs/_static/highlight-numbers.js @@ -0,0 +1,21 @@ +// Syntax-highlight numbers and strings inside pycon output spans (class="go"). +// Pygments tokenizes >>> lines as Python, but leaves output as plain +// Generic.Output. + +document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".highlight-pycon .go").forEach(function (span) { + span.innerHTML = span.innerHTML.replace( + /('[^']*'|"[^"]*"|\b[a-z]\w*(?=\()|\b\d+\.?\d*\b)/g, + function (match) { + var cls; + if (match[0] === "'" || match[0] === '"') + cls = "pycon-string"; + else if (/^[a-z]/.test(match)) + cls = "pycon-name"; + else + cls = "pycon-number"; + return '' + match + ''; + } + ); + }); +}); diff --git a/docs/api-overview.rst b/docs/api-overview.rst index f3fd27368a..a7c067b12e 100644 --- a/docs/api-overview.rst +++ b/docs/api-overview.rst @@ -400,36 +400,36 @@ Memory swap=0, hugetlb=0) >>> - >>> p.memory_footprint() # "real" USS memory usage - pfootprint(uss=2355200, pss=2483712, swap=0) - >>> >>> p.memory_percent() 0.7823 >>> + >>> p.memory_footprint() # "real" USS memory usage + pfootprint(uss=2355200, pss=2483712, swap=0) + >>> >>> p.memory_maps() - pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', - rss=3821568, - size=3842048, - pss=3821568, - shared_clean=0, - shared_dirty=0, - private_clean=0, - private_dirty=3821568, - referenced=3575808, - anonymous=3821568, - swap=0), - pmmap_grouped(path='[heap]', - rss=32768, - size=139264, - pss=32768, - shared_clean=0, - shared_dirty=0, - private_clean=0, - private_dirty=32768, - referenced=32768, - anonymous=32768, - swap=0), - ...] + pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', + rss=3821568, + size=3842048, + pss=3821568, + shared_clean=0, + shared_dirty=0, + private_clean=0, + private_dirty=3821568, + referenced=3575808, + anonymous=3821568, + swap=0), + pmmap_grouped(path='[heap]', + rss=32768, + size=139264, + pss=32768, + shared_clean=0, + shared_dirty=0, + private_clean=0, + private_dirty=32768, + referenced=32768, + anonymous=32768, + swap=0), + ...] >>> >>> p.page_faults() ppagefaults(minor=5905, major=3) @@ -493,7 +493,7 @@ Signals >>> p.terminate() >>> p.kill() >>> p.wait(timeout=3) - + >>> Other process functions diff --git a/docs/api.rst b/docs/api.rst index d786a216ed..341d4384e1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -220,7 +220,7 @@ CPU Example (Linux): - .. code-block:: python + .. code-block:: pycon >>> import psutil >>> psutil.cpu_stats() @@ -241,7 +241,7 @@ CPU Example (Linux): - .. code-block:: python + .. code-block:: pycon >>> import psutil >>> psutil.cpu_freq() @@ -277,7 +277,7 @@ CPU with 10 logical CPUs means that the system load was 31.4% percent over the last N minutes. - .. code-block:: python + .. code-block:: pycon >>> import psutil >>> psutil.getloadavg() @@ -474,7 +474,7 @@ Memory >>> import psutil >>> psutil.swap_memory() - sswap(total=2097147904L, used=886620160L, free=1210527744L, percent=42.3, sin=1050411008, sout=1906720768) + sswap(total=2097147904, used=886620160, free=1210527744, percent=42.3, sin=1050411008, sout=1906720768) .. seealso:: `scripts/meminfo.py`_. @@ -659,7 +659,7 @@ Network >>> >>> psutil.net_io_counters(pernic=True) {'lo': snetio(bytes_sent=547971, bytes_recv=547971, packets_sent=5075, packets_recv=5075, errin=0, errout=0, dropin=0, dropout=0), - 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} + 'wlan0': snetio(bytes_sent=13921765, bytes_recv=62162574, packets_sent=79097, packets_recv=89648, errin=0, errout=0, dropin=0, dropout=0)} .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. @@ -971,7 +971,7 @@ Other system info system clock, which means it may be affected by changes such as manual adjustments or time synchronization (e.g. NTP). - .. code-block:: python + .. code-block:: pycon >>> import psutil, datetime >>> psutil.boot_time() @@ -1313,9 +1313,13 @@ Process class .. attribute:: info A dict containing pre-fetched process info, set by - :func:`process_iter` when called with ``attrs``. Use method - calls instead (e.g. ``p.name()`` instead of ``p.info['name']``). - Accessing this attribute raises :exc:`DeprecationWarning`. + :func:`process_iter` when called with ``attrs`` argument. + Accessing this attribute is deprecated and raises :exc:`DeprecationWarning`. + Use method calls instead (e.g. ``p.name()`` instead of ``p.info['name']``) + or :func:`process_iter` + :meth:`Process.as_dict` if you need a dict + structure. + + .. seealso:: :ref:`migration guide `. .. deprecated:: 8.0.0 diff --git a/docs/conf.py b/docs/conf.py index 58d146ec9e..f363320da1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,6 +61,9 @@ 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', 'css/custom.css', ] +html_js_files = [ + 'highlight-numbers.js', +] # https://pygments.org/styles/ pygments_style = "monokai" diff --git a/docs/migration.rst b/docs/migration.rst index 9519b2e842..48283f3c7d 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -22,6 +22,7 @@ Migrating to 8.0 Key breaking changes in 8.0: +- :func:`process_iter` pre-fetches values - :attr:`Process.info` is deprecated: use direct methods. - Named tuple field order changed: stop positional unpacking. - Some return types are now enums instead of strings. @@ -39,10 +40,12 @@ process_iter(): p.info is deprecated :func:`process_iter` now caches pre-fetched values internally, so they can be accessed via normal method calls instead of the :attr:`Process.info` -dict. ``p.info`` still works but raises :exc:`DeprecationWarning`: +dict. ``p.info`` still works, but raises :exc:`DeprecationWarning`. .. code-block:: python + import psutil + # before for p in psutil.process_iter(attrs=["name", "status"]): print(p.info["name"], p.info["status"]) @@ -53,7 +56,24 @@ dict. ``p.info`` still works but raises :exc:`DeprecationWarning`: When ``attrs`` are specified, method calls return cached values (no extra syscall), and :exc:`AccessDenied` / :exc:`ZombieProcess` -are handled transparently (returning ``ad_value``, default ``None``). +are handled transparently (returning ``ad_value``). + +If you relied on :attr:`Process.info` because you needed a dict structure, use +:meth:`Process.as_dict` instead. + +.. code-block:: python + + import psutil + + # before + for p in psutil.process_iter(attrs=["name", "status"]): + print(p.info) + + # after + attrs = ["name", "status"] + for p in psutil.process_iter(attrs=attrs): + print(p.as_dict(attrs)) # non syscall, return pre-fetched values + .. note:: If ``"name"`` was pre-fetched via ``attrs``, calling ``p.name()`` no From 1d9a64370d8c56d626d4519bf59f9b99c3a20695 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Mar 2026 02:13:49 +0200 Subject: [PATCH 1658/1714] Doc: add "last updated" + external icons to footer --- MANIFEST.in | 1 + docs/_static/css/custom.css | 56 +++++++++++++++++++++++++++++++++++-- docs/_templates/footer.html | 42 ++++++++++++++++++++++++++++ docs/api.rst | 6 ++-- docs/changelog.rst | 15 ++++++---- docs/conf.py | 19 ++++++++----- docs/recipes.rst | 4 +-- psutil/__init__.py | 2 +- 8 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 docs/_templates/footer.html diff --git a/MANIFEST.in b/MANIFEST.in index c2257a6419..669ddbae81 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -24,6 +24,7 @@ include docs/_static/css/custom.css include docs/_static/favicon.ico include docs/_static/highlight-numbers.js include docs/_static/sidebar.js +include docs/_templates/footer.html include docs/_templates/layout.html include docs/adoption.rst include docs/alternatives.rst diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 63db2af0ef..a6312f6e79 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -73,7 +73,7 @@ --func-sig-name: var(--gray-800); --func-sig-param: var(--gray-500); - --table-header-bg: var(--adm-note-bg); + --table-header-bg: #f0f4f7; } /* ================================================================== */ @@ -87,6 +87,11 @@ } } +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + @media (max-width: 768px) { .wy-nav-content { padding: 0 12px 1.618em 12px !important; @@ -136,6 +141,41 @@ footer div[role="navigation"][aria-label="Footer"] { color: #333 !important; } +.wy-menu-vertical li.current > a { + border-left: 3px solid #55a5d9 !important; +} + +/* ================================================================== */ +/* Footer */ +/* ================================================================== */ + +.footer-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.footer-row p { + margin: 0 !important; +} + +.footer-icons { + display: flex; + gap: 14px; + flex-shrink: 0; +} + +.footer-icon svg { + width: 20px; + height: 20px; + fill: #999; + transition: fill 0.2s; +} + +.footer-icon:hover svg { + fill: #444; +} + /* ================================================================== */ /* Headings */ /* ================================================================== */ @@ -179,6 +219,14 @@ h4 { background-color: var(--table-header-bg); } +.wy-table-responsive table tbody tr.row-odd td { + background-color: #f8f9fa !important; +} + +.wy-table-responsive table tbody tr.row-even td { + background-color: #ffffff !important; +} + .document th { padding: 4px 8px !important; font-weight: 600; @@ -258,6 +306,10 @@ h4 { margin: 0px 0px 0px 0px !important; } +.rst-content dl.py + dl.py > dt { + margin-top: 28px !important; +} + .rst-content dl:not(.docutils) dt { color: var(--func-sig-text); } @@ -341,8 +393,6 @@ div[class^="highlight-"] { .highlight pre { line-height: var(--code-line-height) !important; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } /* ================================================================== */ diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html new file mode 100644 index 0000000000..714f961912 --- /dev/null +++ b/docs/_templates/footer.html @@ -0,0 +1,42 @@ +
    + {%- if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %} + + {%- endif %} + +
    + + + +
    diff --git a/docs/api.rst b/docs/api.rst index 341d4384e1..747e79192a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1359,7 +1359,7 @@ Process class >>> import psutil >>> psutil.Process().cmdline() - ['python', 'manage.py', 'runserver'] + ['python3', 'manage.py', 'runserver'] .. method:: environ() @@ -2516,9 +2516,9 @@ Popen class >>> import psutil >>> from subprocess import PIPE >>> - >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) + >>> p = psutil.Popen(["/usr/bin/python3", "-c", "print('hello')"], stdout=PIPE) >>> p.name() - 'python' + 'python3' >>> p.username() 'giampaolo' >>> p.communicate() diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b547008e3..385d7a5e79 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,10 +16,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, :gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`, :gh:`2790`) -- Split docs from a single HTML file into multiple sections (API reference, - install, etc.). - -- Added new sections: +- Split docs from a single HTML file into multiple new sections: - :doc:`/adoption `: notable software using psutil - :doc:`/api-overview `: show entire API via REPL usage examples @@ -35,9 +32,15 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - :doc:`/shell-equivalents `: maps each psutil API to native CLI commands - :doc:`/stdlib-equivalents `: maps psutil's Python API to the closest equivalent in the Python standard library +- Theming: + + - Renewed, more modern, custom theme. + - Show "last updated" and external icons in the footer. + - Show icon for external URLs. + - Use Monokai theme for code snippets. + - Usability: - - Renewed, more modern theme. - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. - Greatly improved :func:`virtual_memory` doc and many other APIs. @@ -52,7 +55,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Configured RTD to automatically public doc from Git tags, instead of from main on every push. - - Build doc as part of CI process (CI fails on error). + - Build doc as part of CI process (fails on error). - Removed /en language from RTD URLs. Turn that into a redirect. - Before: https://psutil.readthedocs.io/en/stable/ - Now: https://psutil.readthedocs.io/stable/ diff --git a/docs/conf.py b/docs/conf.py index f363320da1..03ccdcca92 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,7 +40,6 @@ ] project = PROJECT_NAME -copyright = f"2009-{THIS_YEAR}, {AUTHOR}" author = AUTHOR version = VERSION release = VERSION @@ -50,12 +49,13 @@ extlinks = { 'gh': ('https://github.com/giampaolo/psutil/issues/%s', '#%s'), } -templates_path = ['_templates'] -html_static_path = ['_static'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -html_theme = 'sphinx_rtd_theme' htmlhelp_basename = f"{PROJECT_NAME}-doc" copybutton_exclude = '.linenos, .gp' + +# --- paths + +templates_path = ['_templates'] +html_static_path = ['_static'] html_css_files = [ 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', @@ -64,6 +64,11 @@ html_js_files = [ 'highlight-numbers.js', ] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# https://pygments.org/styles/ -pygments_style = "monokai" +# --- theming / visual + +html_theme = 'sphinx_rtd_theme' +pygments_style = "monokai" # https://pygments.org/styles/ +copyright = f"2009-{THIS_YEAR}, {AUTHOR}" # shown in the footer +html_last_updated_fmt = "%b %d, %Y" # shown in the footer diff --git a/docs/recipes.rst b/docs/recipes.rst index 98e9255cf1..0820c6c4c7 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -123,7 +123,7 @@ Processes owned by user: >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "username"]) if p.username() == getpass.getuser()]) (16832, 'bash'), (19772, 'ssh'), - (20492, 'python')] + (20492, 'python3')] ------------------------------------------------------------------------------- @@ -134,7 +134,7 @@ Processes actively running: >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "status"]) if p.status() == psutil.STATUS_RUNNING]) [(1150, 'Xorg'), (1776, 'unity-panel-service'), - (20492, 'python')] + (20492, 'python3')] ------------------------------------------------------------------------------- diff --git a/psutil/__init__.py b/psutil/__init__.py index f0386b259f..afd8435358 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1564,7 +1564,7 @@ class Popen(Process): >>> from subprocess import PIPE >>> p = psutil.Popen(["python", "-c", "print 'hi'"], stdout=PIPE) >>> p.name() - 'python' + 'python3' >>> p.uids() user(real=1000, effective=1000, saved=1000) >>> p.username() From 0dfc24c8996f5c39b4114e6c8485d4ef09761179 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Mar 2026 12:39:19 +0200 Subject: [PATCH 1659/1714] Doc: modernize home page with quick boxes --- MANIFEST.in | 1 + docs/_static/css/custom.css | 196 +++++++++++++++++++++++++++++++++++- docs/_templates/footer.html | 42 ++++++++ docs/api.rst | 6 +- docs/changelog.rst | 15 +-- docs/conf.py | 19 ++-- docs/index.rst | 168 ++++++++++++++++++++++--------- docs/recipes.rst | 4 +- psutil/__init__.py | 2 +- 9 files changed, 383 insertions(+), 70 deletions(-) create mode 100644 docs/_templates/footer.html diff --git a/MANIFEST.in b/MANIFEST.in index c2257a6419..669ddbae81 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -24,6 +24,7 @@ include docs/_static/css/custom.css include docs/_static/favicon.ico include docs/_static/highlight-numbers.js include docs/_static/sidebar.js +include docs/_templates/footer.html include docs/_templates/layout.html include docs/adoption.rst include docs/alternatives.rst diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 63db2af0ef..afd3e68fdd 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -73,7 +73,147 @@ --func-sig-name: var(--gray-800); --func-sig-param: var(--gray-500); - --table-header-bg: var(--adm-note-bg); + --table-header-bg: #f0f4f7; +} + +/* ================================================================== */ +/* Home page */ +/* ================================================================== */ + +.hero { + text-align: center; + padding: 1.5em 0 2em; +} + +.hero-title { + font-size: 3rem; + font-weight: 700; + color: var(--gray-800); + line-height: 1.1; + margin-bottom: 0.3em; + font-variant-ligatures: none; +} + +.hero-subtitle { + font-size: 1.15rem; + color: var(--gray-500); + margin-bottom: 0.9em; +} + +.hero-badges { + display: flex; + justify-content: center; + gap: 8px; + flex-wrap: wrap; +} + +.hero-badges img { + vertical-align: middle; +} + +.sponsor-table { + margin-left: auto !important; + margin-right: auto !important; +} + +/* +.sponsor-logo { + filter: grayscale(100%) opacity(100%); + transition: filter 0.2s; +} + +.sponsor-logo:hover { + filter: grayscale(0%) opacity(100%); +} +*/ + +.home-platforms { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 8px; + margin: 1em 0 2em; +} + +.home-platform-label { + font-size: 0.92rem; + color: var(--gray-500); + font-weight: 600; + align-self: center; +} + +.home-platform-pill { + display: inline-block; + padding: 3px 12px; + border: 1px solid var(--gray-300); + border-radius: 999px; + font-size: 0.82rem; + color: var(--gray-500); + background: var(--gray-100); +} + +.home-feature-cards { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 14px; + margin: 0 0 2.5em; +} + +@media (max-width: 900px) { + .home-feature-cards { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 480px) { + .home-feature-cards { + grid-template-columns: repeat(2, 1fr); + } +} + +.home-feature-card { + border: 1px solid var(--blue-300); + border-radius: 8px; + padding: 20px 14px; + text-align: center; + background: #fff; + text-decoration: none !important; + transition: box-shadow 0.15s, border-color 0.15s; + display: block; +} + +.home-feature-card:hover { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + border-color: var(--blue-400); +} + +.home-icon-svg { + width: 2.2rem; + height: 2.2rem; + stroke: var(--blue-400); + display: block; + margin: 0 auto 10px; +} + +.home-icon-fa { + font-size: 2.2rem; + color: var(--blue-400); + display: block; + margin-bottom: 10px; +} + +.home-feature-title { + font-weight: 700; + font-size: 0.95rem; + margin-bottom: 10px; + color: var(--gray-700); +} + +.home-feature-apis { + font-size: 0.78rem; + color: var(--gray-500); + font-family: var(--code-font); + line-height: 1.9; } /* ================================================================== */ @@ -87,6 +227,11 @@ } } +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + @media (max-width: 768px) { .wy-nav-content { padding: 0 12px 1.618em 12px !important; @@ -136,6 +281,41 @@ footer div[role="navigation"][aria-label="Footer"] { color: #333 !important; } +.wy-menu-vertical li.current > a { + border-left: 3px solid #55a5d9 !important; +} + +/* ================================================================== */ +/* Footer */ +/* ================================================================== */ + +.footer-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.footer-row p { + margin: 0 !important; +} + +.footer-icons { + display: flex; + gap: 14px; + flex-shrink: 0; +} + +.footer-icon svg { + width: 20px; + height: 20px; + fill: #999; + transition: fill 0.2s; +} + +.footer-icon:hover svg { + fill: #444; +} + /* ================================================================== */ /* Headings */ /* ================================================================== */ @@ -179,6 +359,14 @@ h4 { background-color: var(--table-header-bg); } +.wy-table-responsive table tbody tr.row-odd td { + background-color: #f8f9fa !important; +} + +.wy-table-responsive table tbody tr.row-even td { + background-color: #ffffff !important; +} + .document th { padding: 4px 8px !important; font-weight: 600; @@ -258,6 +446,10 @@ h4 { margin: 0px 0px 0px 0px !important; } +.rst-content dl.py + dl.py > dt { + margin-top: 28px !important; +} + .rst-content dl:not(.docutils) dt { color: var(--func-sig-text); } @@ -341,8 +533,6 @@ div[class^="highlight-"] { .highlight pre { line-height: var(--code-line-height) !important; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } /* ================================================================== */ diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html new file mode 100644 index 0000000000..714f961912 --- /dev/null +++ b/docs/_templates/footer.html @@ -0,0 +1,42 @@ +
    + {%- if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %} + + {%- endif %} + +
    + + + +
    diff --git a/docs/api.rst b/docs/api.rst index 341d4384e1..747e79192a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1359,7 +1359,7 @@ Process class >>> import psutil >>> psutil.Process().cmdline() - ['python', 'manage.py', 'runserver'] + ['python3', 'manage.py', 'runserver'] .. method:: environ() @@ -2516,9 +2516,9 @@ Popen class >>> import psutil >>> from subprocess import PIPE >>> - >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) + >>> p = psutil.Popen(["/usr/bin/python3", "-c", "print('hello')"], stdout=PIPE) >>> p.name() - 'python' + 'python3' >>> p.username() 'giampaolo' >>> p.communicate() diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b547008e3..385d7a5e79 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,10 +16,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, :gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`, :gh:`2790`) -- Split docs from a single HTML file into multiple sections (API reference, - install, etc.). - -- Added new sections: +- Split docs from a single HTML file into multiple new sections: - :doc:`/adoption `: notable software using psutil - :doc:`/api-overview `: show entire API via REPL usage examples @@ -35,9 +32,15 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - :doc:`/shell-equivalents `: maps each psutil API to native CLI commands - :doc:`/stdlib-equivalents `: maps psutil's Python API to the closest equivalent in the Python standard library +- Theming: + + - Renewed, more modern, custom theme. + - Show "last updated" and external icons in the footer. + - Show icon for external URLs. + - Use Monokai theme for code snippets. + - Usability: - - Renewed, more modern theme. - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. - Greatly improved :func:`virtual_memory` doc and many other APIs. @@ -52,7 +55,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Configured RTD to automatically public doc from Git tags, instead of from main on every push. - - Build doc as part of CI process (CI fails on error). + - Build doc as part of CI process (fails on error). - Removed /en language from RTD URLs. Turn that into a redirect. - Before: https://psutil.readthedocs.io/en/stable/ - Now: https://psutil.readthedocs.io/stable/ diff --git a/docs/conf.py b/docs/conf.py index f363320da1..03ccdcca92 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,7 +40,6 @@ ] project = PROJECT_NAME -copyright = f"2009-{THIS_YEAR}, {AUTHOR}" author = AUTHOR version = VERSION release = VERSION @@ -50,12 +49,13 @@ extlinks = { 'gh': ('https://github.com/giampaolo/psutil/issues/%s', '#%s'), } -templates_path = ['_templates'] -html_static_path = ['_static'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -html_theme = 'sphinx_rtd_theme' htmlhelp_basename = f"{PROJECT_NAME}-doc" copybutton_exclude = '.linenos, .gp' + +# --- paths + +templates_path = ['_templates'] +html_static_path = ['_static'] html_css_files = [ 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', @@ -64,6 +64,11 @@ html_js_files = [ 'highlight-numbers.js', ] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# https://pygments.org/styles/ -pygments_style = "monokai" +# --- theming / visual + +html_theme = 'sphinx_rtd_theme' +pygments_style = "monokai" # https://pygments.org/styles/ +copyright = f"2009-{THIS_YEAR}, {AUTHOR}" # shown in the footer +html_last_updated_fmt = "%b %d, %Y" # shown in the footer diff --git a/docs/index.rst b/docs/index.rst index b407b01c07..8be9f8e375 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,75 +2,157 @@ :synopsis: psutil module .. moduleauthor:: Giampaolo Rodola -psutil documentation -==================== +.. raw:: html -.. image:: https://img.shields.io/badge/GitHub-repo-blue - :target: https://github.com/giampaolo/psutil - :alt: GitHub repo + -.. image:: https://img.shields.io/pypi/dm/psutil.svg?color=green - :target: https://clickpy.clickhouse.com/dashboard/psutil - :alt: Downloads +
    +
    psutil
    +
    Process and System Utilities for Python
    +
    + GitHub repo + Downloads + Latest version +
    +
    -.. image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi&color=yellowgreen - :target: https://pypi.org/project/psutil - :alt: Latest version +------------------------------------------------------------------------------- -psutil (Python system and process utilities) is a cross-platform library for -retrieving information about running +psutil is a cross-platform library for retrieving information about running **processes** and **system utilization** (CPU, memory, disks, network, sensors) -in **Python**. -It is useful mainly for **system monitoring**, **profiling**, **limiting -process resources**, and **managing running processes**. -It implements many functionalities offered by UNIX command line tools -such as *ps, top, free, iotop, netstat, ifconfig, lsof* and others -(see :doc:`shell equivalents `). -psutil currently supports the following platforms, from **Python 3.7** onwards: +in Python. It is useful mainly for **system monitoring**, **profiling**, +**limiting process resources**, and **managing running processes**. -- **Linux** -- **Windows** -- **macOS** -- **FreeBSD, OpenBSD**, **NetBSD** -- **Sun Solaris** -- **AIX** +.. raw:: html + +
    + Runs on: + Linux + Windows + macOS + FreeBSD + OpenBSD + NetBSD + Solaris + AIX +
    + + + + +It implements many functionalities offered by UNIX command line tool such as +*ps, top, free, iotop, netstat, ifconfig, lsof* and others +(see :doc:`shell equivalents `). +It is used by :doc:`many notable projects ` including TensorFlow, +PyTorch, Home Assistant, Ansible, and Celery. -psutil is used by `many notable projects `__ including -TensorFlow, PyTorch, Home Assistant, Ansible, and Celery. +------------------------------------------------------------------------------- Sponsors -------- .. raw:: html - +
    -      add your logo

    - -Funding -------- +
    While psutil is free software and will always be, the project would benefit immensely from some funding. -psutil is among the `top 100 `_ +psutil is among the :doc:`top 100 ` most-downloaded Python packages, and keeping up with bug reports, user support, and ongoing maintenance has become increasingly difficult to sustain as a one-person effort. @@ -78,16 +160,8 @@ If you're a company that's making significant use of psutil you can consider becoming a sponsor via `GitHub `_, `Open Collective `_ or `PayPal `_. -Sponsors can have their logo displayed here and in the psutil `documentation `_. - -Security --------- -To report a security vulnerability, please use the `Tidelift security contact`_. -Tidelift will coordinate the fix and disclosure. - -Table of contents ------------------ +------------------------------------------------------------------------------- .. toctree:: :maxdepth: 2 @@ -129,5 +203,3 @@ Table of contents :titlesonly: Changelog - -.. _`Tidelift security contact`: https://tidelift.com/security diff --git a/docs/recipes.rst b/docs/recipes.rst index 98e9255cf1..0820c6c4c7 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -123,7 +123,7 @@ Processes owned by user: >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "username"]) if p.username() == getpass.getuser()]) (16832, 'bash'), (19772, 'ssh'), - (20492, 'python')] + (20492, 'python3')] ------------------------------------------------------------------------------- @@ -134,7 +134,7 @@ Processes actively running: >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "status"]) if p.status() == psutil.STATUS_RUNNING]) [(1150, 'Xorg'), (1776, 'unity-panel-service'), - (20492, 'python')] + (20492, 'python3')] ------------------------------------------------------------------------------- diff --git a/psutil/__init__.py b/psutil/__init__.py index f0386b259f..afd8435358 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -1564,7 +1564,7 @@ class Popen(Process): >>> from subprocess import PIPE >>> p = psutil.Popen(["python", "-c", "print 'hi'"], stdout=PIPE) >>> p.name() - 'python' + 'python3' >>> p.uids() user(real=1000, effective=1000, saved=1000) >>> p.username() From d790c785dc7e1b69c13ccf8749b8b10724d05c8d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Mar 2026 20:55:41 +0200 Subject: [PATCH 1660/1714] Doc: resize logos + remove dead static files --- MANIFEST.in | 5 +- README.rst | 2 +- docs/_static/copybutton.js | 57 ------- docs/_static/css/custom.css | 104 +++++++++-- docs/_static/favicon.ico | Bin 15086 -> 0 bytes docs/_static/images/favicon.svg | 5 + docs/_static/images/logo-apivoid.svg | 12 ++ docs/_static/images/logo-psutil.svg | 5 + docs/_static/images/logo-sansec.svg | 1 + .../logo-tidelift.svg} | 2 +- docs/_static/{ => js}/highlight-numbers.js | 0 docs/_static/logo.png | Bin 4922 -> 0 bytes docs/_static/pmap-small.png | Bin 169063 -> 0 bytes docs/_static/pmap.png | Bin 269776 -> 0 bytes docs/_static/procinfo-small.png | Bin 90122 -> 0 bytes docs/_static/procinfo.png | Bin 159281 -> 0 bytes docs/_static/procsmem-small.png | Bin 207059 -> 0 bytes docs/_static/procsmem.png | Bin 326209 -> 0 bytes docs/_static/psutil-logo.png | Bin 7228 -> 0 bytes docs/_static/sidebar.js | 161 ------------------ docs/_static/top-small.png | Bin 137860 -> 0 bytes docs/_static/top.png | Bin 215631 -> 0 bytes docs/conf.py | 5 +- docs/index.rst | 15 +- 24 files changed, 125 insertions(+), 249 deletions(-) delete mode 100644 docs/_static/copybutton.js delete mode 100644 docs/_static/favicon.ico create mode 100644 docs/_static/images/favicon.svg create mode 100644 docs/_static/images/logo-apivoid.svg create mode 100644 docs/_static/images/logo-psutil.svg create mode 100644 docs/_static/images/logo-sansec.svg rename docs/_static/{tidelift-logo.svg => images/logo-tidelift.svg} (95%) rename docs/_static/{ => js}/highlight-numbers.js (100%) delete mode 100644 docs/_static/logo.png delete mode 100644 docs/_static/pmap-small.png delete mode 100644 docs/_static/pmap.png delete mode 100644 docs/_static/procinfo-small.png delete mode 100644 docs/_static/procinfo.png delete mode 100644 docs/_static/procsmem-small.png delete mode 100644 docs/_static/procsmem.png delete mode 100644 docs/_static/psutil-logo.png delete mode 100644 docs/_static/sidebar.js delete mode 100644 docs/_static/top-small.png delete mode 100644 docs/_static/top.png diff --git a/MANIFEST.in b/MANIFEST.in index 669ddbae81..896e7341b8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,11 +19,8 @@ include docs/_ext/availability.py include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py include docs/_links.rst -include docs/_static/copybutton.js include docs/_static/css/custom.css -include docs/_static/favicon.ico -include docs/_static/highlight-numbers.js -include docs/_static/sidebar.js +include docs/_static/js/highlight-numbers.js include docs/_templates/footer.html include docs/_templates/layout.html include docs/adoption.rst diff --git a/README.rst b/README.rst index 424b7a078a..19a442b5eb 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ .. raw:: html
    - +

    Home    diff --git a/docs/_static/copybutton.js b/docs/_static/copybutton.js deleted file mode 100644 index 5d82c672be..0000000000 --- a/docs/_static/copybutton.js +++ /dev/null @@ -1,57 +0,0 @@ -$(document).ready(function() { - /* Add a [>>>] button on the top-right corner of code samples to hide - * the >>> and ... prompts and the output and thus make the code - * copyable. */ - var div = $('.highlight-python .highlight,' + - '.highlight-python3 .highlight') - var pre = div.find('pre'); - - // get the styles from the current theme - pre.parent().parent().css('position', 'relative'); - var hide_text = 'Hide the prompts and output'; - var show_text = 'Show the prompts and output'; - var border_width = pre.css('border-top-width'); - var border_style = pre.css('border-top-style'); - var border_color = pre.css('border-top-color'); - var button_styles = { - 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', - 'border-color': border_color, 'border-style': border_style, - 'border-width': border_width, 'color': border_color, 'text-size': '75%', - 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em', - 'border-radius': '0 3px 0 0' - } - - // create and add the button to all the code blocks that contain >>> - div.each(function(index) { - var jthis = $(this); - if (jthis.find('.gp').length > 0) { - var button = $('>>>'); - button.css(button_styles) - button.attr('title', hide_text); - jthis.prepend(button); - } - // tracebacks (.gt) contain bare text elements that need to be - // wrapped in a span to work with .nextUntil() (see later) - jthis.find('pre:has(.gt)').contents().filter(function() { - return ((this.nodeType == 3) && (this.data.trim().length > 0)); - }).wrap(''); - }); - - // define the behavior of the button when it's clicked - $('.copybutton').toggle( - function() { - var button = $(this); - button.parent().find('.go, .gp, .gt').hide(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); - button.css('text-decoration', 'line-through'); - button.attr('title', show_text); - }, - function() { - var button = $(this); - button.parent().find('.go, .gp, .gt').show(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); - button.css('text-decoration', 'none'); - button.attr('title', hide_text); - }); -}); - diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index c48429f5c8..aef053c1ea 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -73,7 +73,22 @@ --func-sig-name: var(--gray-800); --func-sig-param: var(--gray-500); - --table-header-bg: var(--adm-note-bg); + --table-header-bg: var(--gray-200); + --table-row-odd: #f8f9fa; + --table-row-even: #ffffff; + + --sidebar-bg: #343131; + --sidebar-caption: #55a5d9; + --sidebar-item: #d9d9d9; + --sidebar-item-nested: #333; + --sidebar-active-border: #55a5d9; + --sidebar-search-focus-border: #7ab3d4; + --sidebar-search-focus-shadow: rgba(122, 179, 212, 0.3); + + --footer-icon: #999; + --footer-icon-hover: #444; + + --card-bg: #fff; } /* ================================================================== */ @@ -86,12 +101,27 @@ } .hero-title { - font-size: 3rem; + font-size: 3.5rem; font-weight: 700; color: var(--gray-800); line-height: 1.1; margin-bottom: 0.3em; font-variant-ligatures: none; + letter-spacing: 0.02em; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.hero-logo { + height: 55px; + width: auto; + padding-right: 5px; +} + +.hero-title span { + position: relative; + bottom: -2px; } .hero-subtitle { @@ -167,7 +197,21 @@ @media (max-width: 480px) { .home-feature-cards { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(3, 1fr); + gap: 8px; + } + .home-feature-card { + padding: 12px 8px; + } + .home-icon-svg { + width: 1.6rem; + height: 1.6rem; + } + .home-icon-fa { + font-size: 1.6rem; + } + .home-feature-title { + font-size: 0.8rem; } } @@ -176,7 +220,7 @@ border-radius: 8px; padding: 20px 14px; text-align: center; - background: #fff; + background: var(--card-bg); text-decoration: none !important; transition: box-shadow 0.15s, border-color 0.15s; display: block; @@ -214,7 +258,6 @@ color: var(--gray-500); font-family: var(--code-font); line-height: 1.9; ->>>>>>> doc-reorg-2 } /* ================================================================== */ @@ -256,8 +299,35 @@ footer div[role="navigation"][aria-label="Footer"] { /* Sidebar */ /* ================================================================== */ +.wy-nav-top { + background-color: var(--sidebar-bg) !important; +} + .wy-side-nav-search { - background-color: var(--sidebar-search-bg) !important; + background-color: var(--sidebar-bg) !important; + position: sticky; + top: 0; + z-index: 10; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.wy-side-nav-search a.icon-home { + display: flex; + align-items: center; + justify-content: center; + gap: 2px; +} + +.wy-side-nav-search a.icon-home::before { + display: none; +} + +.wy-side-nav-search a.icon-home img.logo { + display: inline-block; + height: 26px; + width: auto; + margin: 0; + order: -1; } .wy-side-nav-search input[type="text"] { @@ -265,25 +335,25 @@ footer div[role="navigation"][aria-label="Footer"] { } .wy-side-nav-search input[type="text"]:focus { - border-color: #7ab3d4; - box-shadow: 0 0 0 2px rgba(122, 179, 212, 0.3); + border-color: var(--sidebar-search-focus-border); + box-shadow: 0 0 0 2px var(--sidebar-search-focus-shadow); outline: none; } .wy-menu-vertical header, .wy-menu-vertical p.caption { - color: #55a5d9; + color: var(--sidebar-caption); } .wy-menu-vertical li.toctree-l1:not(.current) > a { - color: #d9d9d9 !important; + color: var(--sidebar-item) !important; } .wy-menu-vertical li.toctree-l2 > a { - color: #333 !important; + color: var(--sidebar-item-nested) !important; } .wy-menu-vertical li.current > a { - border-left: 3px solid #55a5d9 !important; + border-left: 3px solid var(--sidebar-active-border) !important; } /* ================================================================== */ @@ -309,12 +379,12 @@ footer div[role="navigation"][aria-label="Footer"] { .footer-icon svg { width: 20px; height: 20px; - fill: #999; + fill: var(--footer-icon); transition: fill 0.2s; } .footer-icon:hover svg { - fill: #444; + fill: var(--footer-icon-hover); } /* ================================================================== */ @@ -361,11 +431,11 @@ h4 { } .wy-table-responsive table tbody tr.row-odd td { - background-color: #f8f9fa !important; + background-color: var(--table-row-odd) !important; } .wy-table-responsive table tbody tr.row-even td { - background-color: #ffffff !important; + background-color: var(--table-row-even) !important; } .document th { @@ -517,7 +587,7 @@ h4 { .highlight, .highlight pre { - font-size: 92% !important; + font-size: 13px !important; font-family: var(--code-font) !important; } diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico deleted file mode 100644 index c9efc5844a2627a8474949724a2aefe4ab2baee4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 + + + + diff --git a/docs/_static/images/logo-apivoid.svg b/docs/_static/images/logo-apivoid.svg new file mode 100644 index 0000000000..5187310d60 --- /dev/null +++ b/docs/_static/images/logo-apivoid.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/docs/_static/images/logo-psutil.svg b/docs/_static/images/logo-psutil.svg new file mode 100644 index 0000000000..245e606bca --- /dev/null +++ b/docs/_static/images/logo-psutil.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/_static/images/logo-sansec.svg b/docs/_static/images/logo-sansec.svg new file mode 100644 index 0000000000..5d74c6be87 --- /dev/null +++ b/docs/_static/images/logo-sansec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_static/tidelift-logo.svg b/docs/_static/images/logo-tidelift.svg similarity index 95% rename from docs/_static/tidelift-logo.svg rename to docs/_static/images/logo-tidelift.svg index af12d68417..5394cbd6fc 100644 --- a/docs/_static/tidelift-logo.svg +++ b/docs/_static/images/logo-tidelift.svg @@ -1,7 +1,7 @@ + viewBox="2 2 187 29" xml:space="preserve"> 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 diff --git a/docs/_static/pmap-small.png b/docs/_static/pmap-small.png deleted file mode 100644 index 70ed13731d3e8cfc2afc976c04b911d7dcf9b761..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169063 zcmW(+19Tl-7fqWqR%7$UXl(n1jcwajP}ZfOr8n{W^kxaAkskI5B{L;7Nsmz;?)NQ{)35fHsnm6odHuZ^`W} zNdULNxB%tEVYcAmA@I?P&~Dkm7efHWgjGCN&feAI2-VXM;XZV97|87`sv3o0p}uNC zLJ7x@%N8@v=eK#A*^W0&xY-gvbG6rWTEG6`WnJgzRi2+$ol8>HEGvaWC22r}60vsO zwPIL%KkMs+g^*>U3zueTXT6+?8Wei!9Ev4I`CNPF{k;7wEFSb?W?2gxJ1-OqN28E_ zQx8T3fM^b|)#CKL?-L!kvtUWHZA7uZ!o``w9XoHbBSJy|L2-a%=X3aA)E{eV`Tt$k z#Rme!%bWkSRijHUh71upQ=~{Iz`7CnlQ|3$7D+A?VM;L}kjDm}BuzPU<7uMF2yOX? zH!D>Iy-WqbPS1@*R>7^r+NUgZj98-=DErnKB zeH|e=m=Vr|ITQa&Agot7tYvf?o5aJMm9+ix4F3uArE(KNlyyOc*$Yf z>nzp$h>SKoI>mJfUpeko8P-biZ=#)9d%fCw1bQdsP$G*nigDR+_uwx^eZ9%^^{1;Y z3}8G|UYsK|56-T?Dv<`dN-NoFJvdKO5hKlmPl7C748kn~2?&B$=!+2H$%6iCR{tG7 zkSk#dWZaSX!d2PM%wiPEkivsqq+?4g>L(We2G<@ixJ?7<|Z zoqW}{ix=+dCfqT``ThRe-R>(}qwDMm{?@{~xC?(!@#)b~_o*X;BKXhQWZ41YiILgD za)7uqv9i8beC;Jk0M@5N>1zCzFisse8&4KN+92BSpj^P7nvFC6i;M(U!Y|O)#pV?*Ebj)dr5zhW(*XIW>mf0Nl8>z*c z;`R-*a&`)RT5#&}2dOMU;mY*_WHfZ`Ff_@uWlzDKncshjGGM>}L`(nW|6BX&NHP7N zt_$E|Z(K({Lpaod3lSqb^NUj}L7o9DDqzPEL%iqMg$&4&UR+yWA5?5TJXJ0dOp+7j za5#x^eFZJ}eEb8~qJ|td7FBK%ZSorsfG*1~kG>!`oJRfO5#zum!C@o8sR;}1HbZP- z^K2}Y4@rT2xV?)>#&Ay^5m=2|L^JqbQ0kEe3-S1#l*T>yebI|A5T4A33&%91V5s10 z*JV$Gm}Wx?-U8BNeZ^1}XN0yUz59`Pi5n`EOo3QHeV;&47$^o6_h1qnV!|naGzMlC?^3^lvOewCl zaF#uZ6b8OP9jCcHw2@@1ZvqDZ5e?0*m+nf6mShpHY+{y1wg27BAC~kdD=*N`du6Y4 zXm_*sF$Zd6GpH~W)!QCPe_Ot5=K&lw*X^RY#p-A|pM)TKV>xgAEijTOTRJuTVd!xic`7%T-$nAj+^GzfTuP9$Y+I`$8B%K`+$%^xFP1{}g&O!PyW8CscT-$M}wuS3_^P$A6TM!GHJeoVM10FRvNl7okU~e1H3AAYI&_ zu3%UReC`n$?(Lcu-tLI_2lw(wrH0@_cD#4B^ZSYMq@%SSkU=U}Z*c%9_}8f%vd){| ziS_ZE{#Ue^L?I1V4p&0F@tGlzVP2(YxO;CIRAslz8tJ+c_x~L3_E1p-+NXM&yC-Uu zUmY{p(LgDS{!cuOm>eSUd|~oP39Ok>C>&(!4Oc<_4@m!f0;NQZcg{$i+GXhJ?a*cR zjtMRIukXW5?hlE_G4zQNAeH8@Dn;Q43M}c7Jn#NQXyptDBnBi#=;%IaiVRMFt4?r~ zWeoVP$i!~`=idBhRqo+m?zH|r)@ew}hCfk_>LU9)@+r@H0z{dup?ZQB%d_jW?w3;X2C2ux_|WL7TXgK#kd$csTB-z zc<{9YZKp^Cl;M{bN1iMYTnW#`IMt5%eWKnT_~zy+OzNMN{xDv{MCvSWj6Gj=Or~*8 z%`TkvomJ;{_4cogLsMoDX7mLnkHklbmt7F|=7MFM9BG zM$KD_;}~6zD15%5mmnSt5|A+jp;9A5#wQ7}GpSlpGeQm|46VdTvLFC3gP|ovNSn>b zZl^<&F_*&ThAElmt*G^oXlmlx>_w-fe`g}95y}(|cASv}4VGs2j~w_8VLW#{IN zj0IIRTbx`B8LMXljir{>Vnn?^J@j*K5$X?!>5z z01V((W$EAF^sJr9CT9-Vk&nczuJ=6ZjGHj?5$l$sVz`qk)yn37ZU@9Gm@C>6(N<)dIBTMf@Cnq64|P8wmaO(I>}ocN!10-? z+4=imYAX|?zw+kfOfV|$pdfN%u53>xt=d%DeuwWbJw1^?sYd^81`Ko4(1K-hd_dIu zZfK}&?Bvh-Xg^_q1061vXu}a!sdaQ>ZTdVz?+Gb8$>JsXLYf3Pd8}t@9~cf-{?`Jy zvHC`i?mGBVkxZ?%WJF3Y8apd`9BxenPo*75h~2c zc;MkMfQ)*d{qBJS{WffxEn19Ags~)#Z?}NV@DKAl&V*XuwK2Jv8>EWs9TU@R^qif@ zm?@AFw0DEo>H><|Gx9J(tCH_AIwTnNZ0Az;lA>*+uF_S&E|2#c@Q$Gfh@qo- zF-n?vXXVImn$aZ!t|D{lyca_SGq)ao2#^sXqr-M9j4BEHhV)JyaK@X`!3uNi9M1W- z$gq{5ZLaTNe|knNw^F!$9_X%URV=4xkY*s>L=u03tYQNJz{<6bN%l?{lQbRrzuuqE zz8wep6Z;W`QESPfwFoS%98oP1iFKrKaGsLlj(8TUX2InJuX1~2w4sdgo+d*&RpfOn z;ZvEZ)@)+(A_{51lE@zYuLC6kaZFBVJkv%do`o&2QaQJ<7=bqQe@g^AW%Qcj!l7H; z4>`*CD*0OmP1yqqrTE^iSiEQlM(7Gg0DZnKvhWg&Jye3;DF$TvQo<(aK2JIR!j1S$kg!-a`lc@*h?cth z?~ke&K#{3^gu|VhVhy-sog-U=kIoDnj1473hQ9qAzi~UUJ~C5S3dDwCT1~^^s)B&M zNoXZs6f|gMV&C4~Fgk;KCSUu|Of{)3SpK2;oVUcJEc*9OTr;~3LuaZO9a++mi2jqH z44rt;QXxI-V=_~vX~!qZDGq3SmoA577V`HI^6eO@7PQs%)Y|^sYb+{AX|_G_DM)<9 z{LQjh3|Rg zMh<0K5z|c4A}T1jyqqMew?B~{8+#B3_DdZZIh-Ps9|+-<6_Jp%kYBxvUnQW3Kj?Sr zux-T@HFCs@o#5FbT9^tnn-Ac6Kdo>CbBW#?;!_i3OoB_ zYktyd^N#SBh`~`K?knF*h|3_DCeu5!W>A;Es4SC?u-&+tyH<`S*FJ9s)ss zAG&MA{J`F_B9Wg*h=r2>*t;Ys{Xe0p+a< z8C1X@Cx@nB0&j~0J((_{_qolzmnlJyBj&2gf?R7~4k&$1$+l37Z$g{7Q}A{tm!KTn@gflAEIm5fbxDer zS!}W-#yz~dCjB_6sks_;*Ec*UKuLu})a}z-?tfm?c^mk=ao_VlG;I1mGlZk5XiBzv zT4@)2ow%&2t99a>utsxI~E6Hu(5m z&qtOd6OaOAa<@-+ksz;apWOW1pDM#o&uw0ZsSB@*s5?wVfr!XQTh=i<*&y3bN+RHL zhNU6wvvDsL*;$zm9~(Vi2D8?|5pCtVMRU9L(8g58Qk&&^V5VNHuJY&e z%#dE|>wt|Ug1y6iA1@n9%&d~W1P9*MMTz^LrOecS|Mg3LzyErvh|-aO2h{!nxxn)^ z!AMK^dP{Wpi2Q}`3(e3ZVLwgD^2&-C1E|*s6doo5P$7o&MY5TkL{~s4bO~H;#k?s) z<+Ph%^)vW=ER%v-Z55NrD{JK)Uet*CaANjxw&@qy{o?<|yWplgK+qyaNo43NPDX@b zQwTDdL)OW1rT;-+*O?7ydy6Y=O>Ux1+IJN^pV&7cmaDHlry^XIu+ZGGl)^KPLeg%% zj(k)+^}f2}tmWzcUA#g~=N=z5AXPG3IZ+cY+pnpoCHaG<_djAtpLQl@`&CKJH zigRWlp`#!Q5Z-&Ummes)7+vy#1X9s}w`Iw*_C)NN%WuO__pN~U(h~(?VyN~^-&nK> zyB?yxhS5D=Q4uGM@?Y{ZV7_w}8kLK&r!l!ii#!zOmc|ADMKxf_Ky8aDKApOgkx)c1 z?PQbNJS~`Zbs_z!ifM?^A(OpSEVI_nn$P z^9O}1hbGp>_=IUEFFZ=5g4ZfNU+>HKPgt>SXQb~#RA`&_CHX2(qx=dj)6cUd?cA}UZ` z!RgnqRZWNGxo_Zr3|o)Lo+2Nim2XA}??r#E=PT3scy(dG56!hbBdPH}WxGe(KdU^S z1_Q0I*mKd?u=tMq%+CXEE4nkADw{5*b27xBlk6{pgDa&L7kyMi%h6_zZD&W%e9#82 z&k=`4$p+F}E-TYF&p*1W6<}ooUuh*o^87t7L z;>zN<^FdxdIaq7fmqegP|7}vcWV>{Z)u2NJ3D;h6#V4Da4!-H!*nB0{##2+QZ}9TV>^u6DDmGeavyC>ooH zGtL*Vdd{Jz$_RT%*CDnlY)y`L^S&k&2y=ocyZF4q-p<_C?)HChSRC)}xX$HV$8~8B z-%wEIe(suG%%MC#C6x7^6l^wh!&X+a49aKb+5^7yXkPF?IG;^8w}=O$2xRUg5qCty zOF7)8Tq^QAMm1Sct=22AwbYA?db3+Ejt^sD+ka#bE1Jcpwcat)0bz#ZW42TfKQ8=V zVk@Cp-=Li?|D)fahuPPC|1EMks<5*#2}o;)gy?^i#^llQ)eAd{YgSWeH&vdKT3rqK6VuJL30Ar8Q43dZyTT zkIdPzIzQa%(IfJ`VpNu)iP7bfPaAvdRrxK{TS;5_!@jn$C8DS}>q3jS`z_NtbZ`{@H*dby+Ari*j zx(pgm&;J5K)b7~E==p;1LQtc)qgPl~ncKmMK-EN3f3@&-OZdcEXYZD$)h1a-UUaHN zBUP6qV}kj9nLn@x>f@g9#5?TRrVMbfNlMqgEXW-N{D5pRpn&7gI9O=EtZzvx%wLf- zmE#846gPFj>&sE7)yrzSBFSgG?4VKtQ>(Cue4iMJ{aHe?f#~yc1l<0?cY{0#2o)}b zd{UxS8CMTH75HC3Jq#DRIlHFPP!(!)@%y-bHz|VTMVb~TR(*!E;bQp~JpbHK>ijDc z*7uPxN@9b|V+#h!`*$sS^AI7!kd^h%0SS{JdBBJ{>)F}atgi*AX`%eHn9^1TtT%i) zRvG#p5C1eG!!fF3#v)P#&T5Yqpm_rTNLjWT!*wj(G49cu`tUk zF^ZcTQRO9?x;jIux{^gxBNr=p0R~ZVR_$hCb+_X)<+AQi! zBTsWRKN)J2v7@J@U74Y~FXX%#>Gz4PhK#s1)l83bHbPhAkG{{R%OzNI|4mi{3TYo( zslEjcMGtsdwFgw_bTbUPBC#!Al|_>F0Dxy z5rp^M!>uRROhzy^fW=VmIg>i`%%IlC`TH;lsh~?W^)ETRCn|o;J?r#)hh+OP;h?n%^;n`kIBhapA=MgqOFp{119$zvGkD(UB!-PE-Om<;siq)sl(P z48}f^TtkCe0-|Iby*v+0J99jF{HXHgGSD|V%!4|ju@W@d#S!Ha7{xfVHYc8@Oo1i0 zRc4vn*cs!(7Lm7Nad|mNc?#IEj8i>X%^*n;scSnvy8LAm;j)mM*B&Wdlcd2FT3;91 zR4q{#j)P~W_1fmB1$C`~r?1Q^H<1J&Q@pETVuJk=a|6%gwHn{RmewKWtiY+d|Iq8W zz%e~DFDsDt_ky9XQbJ z(>ofYw}RjC4;g7EdH3w1c-l#`oEN6dX>S>+Kl~}k#M^y24oo3X<8RXe(8{)X08k>$D^;e-(;+d2`sL9E|y<3Zi0lJr2R_UXgcciVoM` zrGZGqSd^catiqZIhx&rjj|RN)LQ$-n@6es#%ykbV=^cj4%_wuSng=XvL~gcAJvtDR z5fy&k(M8E2&Z-;&%*%FqwW*GAPbj z=loxFzt+bdbKlb@goRpKGg8s$==iix54lhe2XN|{Te$qvijC4*?^#wynOonK0K{#r zJ#G}bx1f~#uKV-neV$?CCw=B?f&JX9ij0CJgJwdCafzhm@v(TMIM>yn=gA4aMRnco z=}AdVchcmBSu)XN$I>`_fRQ7!+YL2jkZs{tznzh@4Q-&W`VV#i{3JO!>RyARe&&}K zEh%U>Acl;!uO<|HLXEBvT+Qw|vAItJVVx#OGX&i}djPiTPX}J!;WPjyHpXaH8Op67 zm480d_Z@X?E|S3W)zzv)o+X|M*Kqf5n(;q%UbrV?7h=A@(_P4*JlLR`%%bl0Y#$c+ zzr@d->aa=RFez&F(YBR6FPzR8)HQ6m>QFX<4@fUdg){rj|X`|`VCP1nb>=s{vH9Le>8={1H zVR1R`(CneTvYj{W5)Yqa<-l6(L&p%*C7*kO%p-Ge%McTu*ygUZpc0hbTMeN3H|);| z|Do`p^WhjL36usBoBd3`UDVOBQpVuFJ7x5V4jH;IdFV^<)BXn-_xmioJ+K!}R3a?Q zUQPt!%#LSoz_aq);`&G*gXnb!JccY1P%_u`0&tB81`{b-=V?;Ap>curdDO838B=$0 z#u+!Thb?R>r`OoKj0il2z(i-t_J_l`!JZ99dYc?cxs^P#5;~rD%6%Jri^O*UV`! zGLYu?M@Z8bQ<||SddwY%&k-kQ^(LeO$5fLP9ganiU6MYnZEPVL2*) z_(&_pfjVN@&bus3rH`rf{7eL;svdum1TJgX?pNlQBiyEDkrvtt$95Z7hKb%F$s>^- zo7(C5&B4)si0N_DEHokkOsV6Dg#5D6_)0WLs*%q7<|)*dIA$(?Fym!vueQE>$_-k@ z%anJ4jRt@S2CI%;>&xD36z5~?6u85xyE_ht#~=ldCyVIhJknYGTb<`E=+O4iWFB61 z{}1L!>+fDe&>G#|k*Dg!TXq-y6~!%Yw=z%Y)13bX`xF?RKR3vbAR%#j*adtqSy>KP zc(ii6(4jyX=2I-56K{H? zbbbCBiUkYPb|R5at={)Uo{<^lEckPr7vEtAJbli2m`8#@F{lxRent9dBs8?;mG!kv z;Q?YC-_5;k6J+(Km*yu&8)a-LFvrWsyAs0wn4syr?wh)aj{NY=6k&1PdV_mIv9Ig+ zK4|ke%|0FdIpce=u1S)#U~zmb5qXX#V**!cpD=X@H}LJ1s=ei7qXbY7y|E&pg*V!I z`$3_`QIM$mEOlnbeCv_4GKnlJk^USO8*Y9R45m!>T{+YjMoxAfb;won`|YtUbE@9m zYkp&fuxS5vK9E*WA^uTu_EbI*>NfyeZo$^5ZgKF9Qn4FIk^JLD)=(BPQ#G1EU}18| z{}oR2-84p-M0=gU|DO7B(>G<^0-5@v*@H3IkOW{kl`8{+Pjd4ie2;SRemW{qpQdPa z@H07XHI{(w&t6vPNvz1VitkFkRRYWQ6EhwbDGfNG*SS7 zt~5eSphj4+*xUjy6m>Wvf}a1>tpsZYHMf)!^&58%V5~GEDtQt9!bc(V9`RRTuq+Nw z+m|Gyd#);_9tB9;JnJ841y(GidecLjmOoKL+JdKWNik(W?`_}6L)wxR?De|+us<;# z+XF|%8hIQv0G0a!H}QmSkRc_03gQ%dJ7UJTLTB?WK4_`B8!W-eC3Urd(aRKd&s0{) zfntuA5~|bt z^{c_yjr@vEP_(LGf_+EN_1vxrx97VzcG+#(^2UV2!|SHeX8@5%g*96=@qRC- z{C47OKN3vM`%LrEWtF+LXF|L>qtnS7z{>@@H9rX~oAOM!%BvkLTH>xx?ac`1Yio1i zzdr5yzZ0G*C9gXq2VqaK3d~F#^wPeUOgbLyd{2}O?kEW5^n0QtWDww3_)6e;Di@Q9 zL($m|+@hF;?8X$^9=YG?ei9I1m-qA*oZoi~jX9(0YLy_X>g1ZDA88pIqq+5*uGhTL z>Y2aLq!Nvb!^BkxL1`Y7;K({K)sIHhWpX*-v&CdleI2y8>wPf8(FNy%6P#BO5Ok;C z35Za-%2}dxl6CYXS-4xSf;dM)0}0+2aCb7a`;BShxq_!lt@4@S3)Sa5ajoMZN&D|Nk7_5a&*GLIr8s!8Mz&_mIbi|yTsp{K5$gH88lsl>MMeQF{`ZtLqJxIb zf4n}4WUSkl`0bo}2rqQ~e;L*qshXNqubc2@7MAB_FP+&E(GR9^{TsOXM;~jyzIzZY zY2xg;v&}RjmONVS)L`+ueenXMKCwhkd&_BGUyI+j?HIqLv;UE0X;Sg*PPUKP`&V!I z?*}Agsw@ZIYC2N|JXQs+czo9tjRpi|2D(xE+fL0#0Fr;^&4HVIhZ_h!7HQeY{A`>C z8b*srv(jRQUBKg!vZ|mnRd2@+fEdu+^)2joZ4<8eC{@c{v%i9r>vp4As@I?2R_x?J zc(;t662W&=b#Vr_NaOVy|t z+oOHR`iBJluq#UNVs#nHguzZZ&~RidkwKY#N$cpyzYP8ep}Y!K@i!hvNka& z#3xT7Z|EG9P%-xK7t}^6ZuR$14$brBy2o%A>=f?R85HtojeNwK9#|x68>fglWOTphd`8q|JMw7z=(p^xOy2^~frt>AWGz>zIUfTGOUOk`EI@(x z)$YT=2~ET=s=7g$0=zv71>bH4Q%=YrM5G!}-0Itzz`{*}{BcyKfM}~!T_YxG2Y#`o z2dFDsOQ{wL%wCA`1=u*xZ75By-6{3>{neQ)iyVTRq|fDATVh~|H-MEBV$AAQR8+85 zrbrWWs)<(01oM<*uWu63}AHyg!t|E0INrN`5zjdk8_Cf37~0G%GCpMS*EbKEs_$4FbV3w2~EM8v9Y@| zK2lP7n-$9pNxTJ~yEuh>MX>)EO2oHzRSF^*gF5cg$||tc?E6(1GcVthk9?r^s)=jb zXq#1oSnyA-fclQF&7wWst*=RpoS#EXh>($e0TIaMxa1iUbv%jcpAW1H3vYXkKk%!*rRwidPg`adp?S4-4-^QowKg~NOm z%Lkgw%c|;Prdi;~DGisfYS5d}t5$V56-Rb|e10I|iuP!0hR;?1X|I(dLRWQFmC{Q7 zB^Xsw4j-9c-`*H$RbODrqG6+^blrb>w+7~zHpylMyxXT61BDaHX+!Wr6l7DWakznX zQTZ5AnJxuC$50Vn z;U(g~itEF}{Ul_BLVs2eY8p%U#Q1HF5u_@XdF)b7Kh267sl`~L1rnZp~255NHPg8!5+vJWe^}BN=!i!Oq zrHt9_0iXsv{2LN)c34fiBto4zJ`ADMYQz;zX;sL-A8v0_A4Z+F0t> z#2bPZa#rD*WpT;G&|UF(p6I$1S?uu;vC~%Lt%gb(Pfk*BeOA|`v{-Ql%4W{aKm4Ki zI0sEt+E5f_Wjo_P&7sl8bW2Htef4%_m59~j8H(8(0 z?G5GZZstP7AF{kcSW6^4KYx6NMo}_5eIR1%pKc4j#eT;3*>?vF70)i8nF^R3{uSc| z#Sh%Mr*Pf9Xv#*2M5`ZM`a9iPD+din1P&Z;|D}TofBEFkGT+Rt&;t7pj!PPdc_pIp zZ;qh^-y;XGIZ2?(*T>=!s2t01&4?HbIq=8%bV{6CVx!AmgLgIK{_fBoj%S!VjBwle zUcu4fZ>PX7fsBg%jZ&}9Q5+XJojp3DZ_=&T{98t@0=_IT{vZz??{Y3VvaB)MK0vZt z&+O4s?E5Q(ektH3yZYoedMk&RsMUY^Q40cfJbl$}U>`In8^-RRt{hV1DG0?{B zdKB1SxIa#zP5lYB*|9ju8M9h`a`0|*$w97rsPvrd&^EQaO=L&Pe+fg-wK*I;rszzU%G<`Zc{7p1ihQEvqbGsWGXlO>b<-n)qfJ(8`<|pm6Z_ zo!mfgEKsIf(=F-gP*POf$wn!iPD|m)Kb%{ed3y#eYd^L)JtH>;p(8f5E^o-~RM_`3 zoK`YcZ`+iX6qSXp?_=)pfQR-1KtW~I<(75zhgEcDwm3*CBuLB%cRb?nafNAaL1tAJ zhA|AxpSCeBH#^YsR5d#AK)-TL)PSz0sHQVLOx+%BLK8>OJNxqe(SNkxgcvI<=pB2K zneSg{=eVSIM#99;?A;nd5nmuVm9{4myIy$6GsCp=_=d04`x8_nQP4n=ErA}4h|)3P zC7=b<;OvMrlM*Z#+0AOpKpAV!8Vc6muFC51NqDv*G}i>~)^v$jD`T!32c=SE>Bvhe z45ejh8E$U(d7O{RSoeV$XHoH(;IAgEG%O>0J@ZJQWyBE;kStT6ASzjgY@FQwlBOt8E2F zW}gc-c7auFXj&j>gwn{S${u}fWl(ue$k>{HpYgXOI+(NGehfqSMly~yZZ;>AGR1-z z^m1WW$4mE(N(idXN+g*i1&EH=f5pzByzNZdK%XH~VZl?1MW@?2K`%d%udI=6$C5E~ z1Kk-$S>^_pKf~rEFcSS(vvrEM|JQ;yG}+YKGf79!zz?^|FdWT)i+aJ~gRr6onWkxE z9>snu_Lz+$(eX5Bj4xZSD8DG_s+Zi($14Q`nO65|xjjWw2YGsWdAxVxbgh#;dZ%g- zgy6LQZf*08g{@J=`LbY)&#NW4IWKK*kZaCOJjX5Zkhm&)oy?=b1N7a_$Y?5zp zaQ@Xql~bU7a{jRb&ZKZWzN+oJ!j1tMQUPcjjPk{dj(W4+=2b(AaP23 zy(3xe9YS;_#LY2ab@dCI964^J!{H6mLJG%{&L$}uV*KxtS`Rv|n^DqP?NgoiebwIU z@f1!cC3=E3hrnpwMUB-6L)fIYK_0cswVjzeyb+6tn{=;+B}PC0#J_{c;TnN0W`;f; zNmCo0BD56+D|{+m3~L^bXfk8%OFE1qr*z{Jwm;`*qp()@XF4r=XP|oI1j9W*Srdd# z6yzmf!UkhjK)anOqW+;S|Cb^)HU$j}OD;aNl4H1YUZr}T&3pojn$paV-swo&VR#G~ zdGLf!f1N4a%Y+z=I>3>gcnyl%KWqm6`Y6;%QF?05JN5p+8$4X~5GL+cn-8E)B=xpK z3|^vB9Arfg(O>&ft6qM3?1)L5b163p7tq*|K65Qm-RVsz*D>x0ZxR{#J|vO)vd2;z zrO1>^TWR*=Odn&)zBUSbVAiT@!OkoFOsCrgTa>o+7oun`#ysbgSI|$J+hvYy{>T4n zPbL)0<+|J}kzv&8MFeU{Gsd~rk)wS66H>=QXoHH6Q|Z+R+~~9fQ0gM)_C?Tm+8F;V z5o%P?YUzop$$`%et~P}I!=06?8s1S{kPg;noA|kN`p(7t$ea@}2^e8uAqhhcx&+eG zefmYe*Kubhcaid0C2I`+-LJ#m3mjh3mB-M`wS=auKx$uCviP(XQ^Fvy!=@A$`LQ6) zrb%BK5nP!bhgxce5YlolUWRbo>J=SQrx)_K2|JDgjv>Lo)Q%AHm;y2<1m1mn*zQV{c?E$6IBNFUT*0P8g8WcIHmEDjB!V zE2>Y>;gHd_rr=OVn%~D7Hx;l^L(pBXDy)NUL?Rls(*9F{XNttWck6Hxtt|D7_>rWl z!|;B`!QFZ@CX;&V&E)M$T-Bv8psBrHWW5QoT7T;zFR!%vQ^Lv}U3(XzrqeNwSMZY}_Rw9=1Cqug z;y*wA2ScLI*L9`A{u#q@ER+JtLD*_~Lz;^IVptP*q|MjG&BC_4aJ(PmQ#L-9;4dmz*vEy}Y%LPpTxjXt^I>NLRdG zr79=t8#?5~ovEJJO*!6c!47f79Xb&;jF7Bl%Yy@sI8{uB?~FLyxM&fGW9}D!nh1s2 z%L?D{*SguGN$G||2k*d?!(d%$xwL^}%<(a%$$W6Oyu;_HoRVii{xI0BIW(&UF;EDC z!;H#gGr>SKvtKizO6=dIqq=*-dNdO`TiqOdIU|b%XS?q9F{6WOO?LW+4^`YhRc>lg z9|hWFs+WcaR-2cvhJMP?hF7r@t)&SMk1ldXh>)Za5b*dX7EKA5a9-y6=MttYO3Iic zP>zr!$ttVuimG;TrIumgKW||XzrDZGGuS9^a!$H`%s*O`HO9@DwmrtqE~o`rvBbmG zI6fEf{&Q*WNQ|FD2WU!=%yA1ut&%x-(OxC-!qrST+^+D+o!hdhaJAg+>I;dptE$?m z5w988k;>X8mHc$bYZxvP3$G#Yyx3s)*Cwx3Ub*&sgmr^LiC%u3j&2^FVA) z-Z~h{*`8X(CA2kvnafyf6;#-a7v1AfZ*@Gz^YTPrpU}JTd&8X@6CR8qHhAqJx6|vC z(|lx7J>Y8=e2Hpo>3=dIcFtMfeyFnKK_(D`w{?DUueIi6Yj01ifi26NYvTnqj@uzjh~;hG zxHuO1l}+Dc(e{6QvzUAcB1;rfv;BcC~)$Glq235w%1ureH zZLNgm)YjtyzK1*AUw#B+G#@ZQ{kw~DxOnr!vX{p`q5MF9`6!h7l|LHn_TIk>%#Vjk zGs&EOfS!xw&3yjgl=?AQU)v=^mQ9I^lrX**$^B4cnhHjCK@xaP?y&7+(bCx-^dUw~ zJ1&|)38OtE2Ty*SWsDIOton(7^7HGEZbC*L_4=L zSD;->A*!&-mn4Y+LhXLdoeEDCOBK~=oFUs_g=hdi8r#~-%n;<8(Z$gK`HC!>Nb@!l zgAkhH7TiA>hDFFzys!640CXDTw9B$s0l~{w5{oY2g1MA2$dIiQD~rRbOZls@&&{bN zq<%eV1-qlbkxSnkdG)}Y_j~W)9Z$Y}qTpnTwDJ6MmNsM-9#7H1b5~G|#0k?BBCKq+L|B2X-8YpW$)`1Xp4edbb&pj7bZ-sV7d+h-+q&LPg|X z^_NN|o0_y*>;(tkgxE@XoBdQ+i32>0=bGTUtW&27)8r(4j)Y9X!P~;-VFT-kA zN>SfeAGk}z5a-o*g4U4)RkPj+_&6enP~v+g_m+46y;MFm_ccUcnA`YsI({NYl!W$~ z$L=WILiOQ>#t;_Ow8v}>B5x$iBj>EeCdR}MXQXm@XC7!_^JWOAhf1WT5|CiR$4$~9 z%ElOD19!NSY7-=nFYN&hjiR2`oMG}8lJf~-2~2J$T;~k?XLc7#tE|I&GroJ|!cGy9 zL5DHQ=j_{?;nS_35CsKU{cnCsX>a!I1_)m^R-i*49^<12EbumlmiAz?2vyHscB685 zfs)<~wFbclz6DVV5Akqe)b=)mc7eqodUs-}E)t(RzZif6UwiUfrm&%ax>N}x1-|T+TS_Rt2)i=*U2uWosGDT~ z1XT_5?TTS|%>H+&1?E&t!BLtlS4gr-X6hF9H%{&xBYQoJ08bnyW``3%ITX8|$K#yb zr0=OJ(FfPCmqnNulhk6GGUB=2wF7^^N!9+9CBn%&j-1NleI5>|L(S)|L6`>lxi8vq!(xzEEeps zP$iTc>=reBXJ_uzijwKm_XvOHg$hmfNZE@7}zE%YUt=k=R5w)embmDQ4HJwv$38#jx<;Q6C3aPZ~ZIRccL0tJ#O7UO3h%vrE& zXiV0&!pC_|y}dp{#6-_Fr@pyQC3?oHOcB*o&@lt$6!G6XmQYnC(xQS>=&x|(8Cwrj zi)D7F=4Zpq^4U0py2_HDQoI`xECd_yF`0Z~&XVNMTV!AQCYA!g0uxvK9YEGD% zlE`7CL;Uowe$L>z^EB1CzIX+r1i5sQSR_Vd*N?rt0=wOc(+aV0l*iX@FgL%60ocka zXlSe<8&Cby46B~>MHs|`K{A;fMlL}l5@YesEpA+2W%T$2QYuU;okNmk%Ih1bEOU^G zr%NTeN|gATP^_WsJ9))Ma%+iO*B-FxkKuMG-1_J<7G_s4xx8#Xy3hSP(}WT_7Lnxo z$2VAB+QM1pVdmO(rXS9c#)1~w;O3`yS>Fufc3Zjg$yMg2myqpl0`t?{yE(;fIEUSo z}GybhDG#+B~MR>O zv6333M2WA67nb$X^`g>3DtLlO@-s>fLlhJrLZIt9k|d+33c9AD>jtW+;De#-2C^bQ zWAY^JsWmiBFO=jeDkz?AGhSdMR8WIF*M^~E=mJ?)ieiL$3gP6f{mVHhCGU%blig$7kTW1dy`jK$-lx38LeR~}+WFK1XK3_TPoDtRM1kpJ@|ZXvgs_2vT`b`eK?&~GO0A~nnv~?9mhhBa3q1Nyd1k# zC7a1VcTrkgWsD!6q|$34xKk+5>v7w#cq$nlZDea@2joJ4Q0!$KJiL$K<|bJUdd4Rx zQ`5wAGL=>3Xqim0k5yO{K{lJm?)Fkv?j)Ped?l~wQcZo3G&aHB8ev}PlQ0rrrB8DMo4nuX7Cc%m?TMrH&)pS zr17|Hcs`_oskp!(V_uTS%Y*W%&tE{b6+oJBgy8*rgNYBD-nfmr zvG2lb96NK6a%&#VQO}z{c#XdPX8h{`PJaK}95^zDnhs$&D;Ym=l(QGcS$e$8`G5W| z7-(^^_-Gb;LpNt%KZ+I$5D4e!9PXpSYb6%TaOCYD@XE=4Hs=?J84k|9{VHx#j)dXl zmA8+P2`7k!B3Q~Q89H=`V+XoK)NTXW?K7tX=|@18;_9Ao_ZS?~##QoP6s7rsM`In<>ux(OXnG6%yeX$z&SES=~PUNGDvNui)128>(BrV_2tCFG0JMI zk%XYCx{6F9j<9&R@VyK8A5WoFwQ+E=fxA~9QQp$Uxr>KboqCAosOF77I*%St5(`Ih zR@c+FuNzflNu)HIdwXbUt|AeRV)xb1Gt`Bd9PwCc&(B3x%^dsA4>*6apUwFtbbBQy zPYpA7?;)wIpzqjGY8@%2r#EqwSI|A!gP9zute|b6kA}K3;^7E+%|I02fNHz@IePv$ zV-r0r-J2qQo#OtQQHTGamYC`ik+b&<5V?O5RD{g+1HPyUPYwin0=M(JF=hp zGArSfM$dQ$SN`4K{Fk5YG&o3}Hv%$z~-=wwPi7DN{W^4!5Y@_GU5$YN%vDEfp78`&O{=qt| zRx|eUTAG_Gh(;5%jSk=pThWs-tX0*F9NABe$4n$6=pO6j`ptR1j+M~DFsSJ0XQ;o1 za3YJ<>7jq}5ZOco$!x||-N40@$N69X@Ba>eIQ>FfQy~l-z9wG#)+;=?av4RIu$5O( zU*}_MVF`<;nm51uL$ccoy!-wG&cA&U|9XtJ7B4^k>&r#U(l6C(OpbDz8(e()$qM1H zLMW0bY%&ZC1w0ilgt3XFnDEv%@#+to_?!Rt|Bkh#10}h`ra!^q?_6N0r-s`<{d;0g zH!hbAk&`KFXyeq`F+%H`G}M=~z8NlfV(SJ?efyX^(8=b~I@L85^z@D)<>CmF1w}Pc z*U^SZM%Y;0W^-ke#%={92|b_1>T_c@NtD-jarVu_L?cjc|Agw%0q%czmqSN}`S8Qr zochjdn36mE?Z5xeI4Y{Ct#-4%u*TRcuhLSXQc+RC<@YW#`Nm;lA%iNH4Oee3GnZ~L zacq*m|6l$+a#I_YY>Ap5V!pP?_c?v6WV}?L6TWJ}YpuVw!Ttz#T76bEG z4?q9;P2Tv^w^+JziKW?94!!+N8ch*C3T}PwOh{PlcDCmin4eokc)X^z2B>yk`J*3D?=z$1L;UoYQ@r~2X{J8-fW^n)@KsV<<7I1cg{{XA_&BR`@wKr% zZ}GwtILu%FjJ&RJc&MCgB*^^CD*Gn7Fsn-O^cx@q_51eo+M8qC_}RPoYg)PZ?K8w< zX}a3W`7eL@QQ=LZ#Mg$b8#;l-ITn|8(DNF(WRyp@9+Jsw=$gjr!v}2nqiA`JWN?S+ z`*Y;;I;!1EUwKx|-eTDP|t8VF-h4 zJj~RsDY7{YUCXob-~l_k@j@SWx0rq~kER)4Tb)0FL`_o*WnMRpZEdvnbWvAVNqKc0 zZJn*CBFEaw7R`N~*v((8`&LY5(t#~Ld;bc%;S@Hvmxi`R>{gYCe+OGt9d;#8CNIcD zx4Cy~nzDwPmy#$7!=SRY12ySqdpC(JOUNcOHiwODBFxglCT52dvsopdjBxkU>u5F? zHoKLMfezNEXGo_r1Vc%hJL*Y964-540;}uf6cZKIKBCKyxqWYq?&0=A0fr)Wr=9h? zH(1kNT!(=uQT%F;; z-DN6#E+mr$r`v(qY$6`qMR8Z)bx6b$S?u0&8d@8$TCG%9S2B0w7J*0xv(-XubCJ8( zXYu(wNU9m9+gT`=hC=&b7n?IPWVAwB#ALN%wV4R6ud*FXVRt!D6q(5S5_fJ*~XVYvHuZy*f0NcyU1QU7e zP8*6WAxR1ryB({;O=DvX!Q}<+-+oMBb(!1O9@E@6Ky{fNi_1%6YXer3jI+9lvll10 z^~?7NC-d07KAKx=K^Rnb4RQ29Gnan$5qU`=vAf0QW}JqGa%8iWmd+NeW`*7P$2_>d zM(5xF4UP4v$so6`P2s7lMlHqZuN}piF~8F`I)=}Z=kh;%K-yNtzR^x*u3Y8*^d^Hx zCXu4seDaG=aWr?)-Cn`%_bwC8T4*S@@Ziognoy{$ts%O#Odu3PaaXZ#tcR6*cUj#@ zQQusHo=Gu%`yM&78zmEFeRG$Lu(0p&IH|2=?%iLZb9_JbJ}WnV{ysZdGZV+h&?4Jh zy>ge%$w}&~>^!)9nV8wf@V+)4eex+&bAE=89>550@X0T);%@7uv$>30?|sTnAo;bV zK_p2cxVFsH{W&m6Ji2^^8<%boN#%*GuMkh^Ji2rlrMd$-?q@fYe1RH7pjaJLcr7f= zEn^^)iG*01StOa!=^Py)ICX;vE>xj0a`q%uPBRZaxytHGqcmx10IF8}g6war!R z?#8KWt|b=>bNA{kto2=VG<&&s=^C5c5fFv^Y&xBzWn_#>hr#uCKjE{Fud^FXkq&M# zH?_jn;u70Cg(q?+ z2ab<$_ruFXQ+W&nTKA1nVac<+w27s>fqlbmY^|(Q-@lKxhH~zIbcKv;A-cJRYOxa7 z4O8FQhTCCbV{RU6bpy3EKJt+OGmn?i4EB=j?#gQ9Oo;W(5D0;*wvBILg}8G-`P*?VU~7RG7MU19L+s z!-LICeex+Q>tA1ru|xrB*w!vovjrK0TrOWQ6SA1dWwICo%vK9R%ahl1WYttKSIT5T zR#0TA;LAawB$(KnOi{2{&Ezu~bO9Et6&ZtUHV2Z7RGi^PlCW5<82KD|-N0nFphyOp zOb$tsvDmB_T8>;!D^y*PrGm+kT+n;JsE>+j!lX)t39jG4AJda@5Q3swK!7A83>_o| zSrU7dWPbH*NGP(rH>S^`s49k*2T8_N*TVUW1N`ED`vtLNmM^~=MIc$+y!wNS-1)m7 zvmQtl6)P$hi%LG5MK_A$s%z*5SS%*8sT5@$Lmb;*%TNCD7if}-B7>$GsAd&i(+lUb zc+Q@j3th4>d~`o$4hu#;%hc6t?8fq_a$!=qFo{~2v@(q6$3_T&$znxDFaCY1g{S>o zp-Ox~@KAWgx7w`exg3U&Fm$b`ykT#=g(0XP80P4iNi5kQ|K&gYEy;XA86hJI$7!>o z=Q21OyE%ENg@5>qpOO^?RfcLdqsoxY7S4mkY9gP_At@$Q1+wWZvZ^A4fh@@wLSVL7 zz|hEM^QdMsCRHJu&C@YA^t=X&g2iG% z&u7Wy3KgKtrsDNqS^@R7Q>ekXC*3J}KJYo|73|eWq_|Bg`bIpNIhR!vZm*ksI!P*- z*_#0`t{-`NZY2q)$AfI-i5IJ!NRouf=D_PQ6W9qq{jhj4WiK$El@|%|8~5>dg)p8U z$0z;#ZPXtu+)!0A>FhtD4Y_bb&FO{GVlGTRY;0|W7&>{1$wU3bcD7!) z_~h$ANHRSK4|D9)ezM^pRqdU)Z3>-Z!{oxdB)_1n*KfW90g{4hk_)B2UZyc2$YyeM z9y(5MlLNz5&$s{hG)wm$63<9XUc5-9DaP7PmRJ7d4Ya^I!EhQw*U*HCQ*XbH9^4?7 z%oTL)ij23und7gWp|!!w*2*UCx)vsmkCBfC*j(Kr5(*OuMak>X(A~%AfnH)eeo|S3 zku#@gsg_w^3+~;JRhygBKlnC14GNDQuHdX~u|7LTMgw!P#^n=DKTy>67o84^ zPu9-{b}gSHujz=QW?MB|P!;)C+AuVYye@O%+pnV}f^;4`!;$eC?%$fluv6u=HxZ>NQj35z>5e|lk1jD4#`Dd<+456gL z`8STSF*6G`AFuz5@8VMT_>nLI^5aIywD~vou!Q*<9H~ z7kj6jrlAo|zjgwTMI#W^Idt(Hg9AbFBm2QN_q`Jr;L0ayHAv`C~X-ZkiiCIO{s; z?x?1|vlT;w$rIz)y_HnEHKrace=TLbo<#@)yRVVAeslqw(@j&I16yqyM^22A4{h`4 z!Q!u79xf|N;c2J%W}+&}oSFqKzF*)AE-5WDZzIv9% zmPQ(?9e6tjXsfp3t8XH_6XcaY`vFyUoyDm+qKWL@;-RvZUVil7eHS$oW$w{D6`g~e z`o;;YMu>-ZR|-DhvP3SOL^Hc+tTFNM?ker0ql}C;@bKy#%++mt_g{aPp^+ZqJAQ^v zOrmFW4jmptH#ymVXo!~HZt|gRqRBj}s$%E{%_9?BeEU`QkM$4^CwTSG-ezQcfOKG& zp2HJJxjd7{4&q;&NAWdr@L&hGKKztmN}{W~f(O@UsO|3O;IUrjZ`~m!dpP&Ki?p^h zBc-F%PaNgK8z-o*w6h(|bMa5VLwi#>J8N6?pM8~+C-Que+DTe!9fUF}um90EX{dIvy}UtI zqp+||2vn0oIwP1kd64BhcS#iGqLL(`DiS%#!GVMQJi2iY!{lWDnIi;drtpVSdv?B6 z-9voiM;EDZo7nNkIrgn@aQMVHYBs{gW+`X=bt8Mq$_Xsav$VQRSwkJBOoHhY8fsxb3v|4%1Mk@c8y)ZoT&r_a1G~(b>TLk1kU;JVH~Qla)KyxOR7yk#om6 zbMX{5*`T_$0TVhVy9aNX1<7JZNyVAIHw|t#K97@?2M^hfB&lkwBkSMd@uLMS2XvHeC<8F`u*3b@!65B z4i3Nd2K&c5u{d1Vq!6E8nP&LJ7$*74=LCc>a8)-@ZqBg064P`;;pR0=5%xL&DS|_pck1!K3NzB!q8Z_ zd!KN|K#{>-R*n%{VQx7^e{U<*O*Po=GE`Mim%+u z$boTYKlzxXZlk)k5+%3I)O3)6{&uPwYq8sH)Hc?lNYL2ZLws$KNTN{q!7vPjFj$*@ z#AYak$*cep{+auHeCZ+m$Hr-B^m705RmvMW@VU&$s!~`DDnM2XUgn0TKh0c75?VUJ z?48HldhcWIPc5jjN@S3j?Q?=`B+ z?ZiS+eD$>~-1v+@Jc}e3S6>Q)SR_epy^njBt`LnUNhZ=5nug70E(8UopjeXR;_BpF z4qsCbEpCmEKDf)^iE%7nW7WJ81?0_@MaT>)Go4-{fr{5Gwl1PPjh~%NJzKZb57Rk^q zKmLz@OXrC*wAFjR*vQGyn7(|8Ygg{0BNJI!;G@5PpWSd8Bb(yZFW=+-gCz_Bp1KzH zjdl~?3GBV%ei?)T&HX(@R~Lw-G=!cfCpj5CJciRI5eX$|92lX!*@wVF-~LgQRE(5n zqO+@(xrcKob`MQ;POe?LN84a8be6Z2Dt`m4(?9x8HrAyLaaiNTic#D%$($?QI~RNs=}kj2#}uZjs2Q(3F;_!L6lPnOUKE_yB$FRrr^dNJ}1$9N&*B6;=Zp#TBcXrlIBY7>0qCO_R)9 z89h9T%dV1$XJ{TArKQf!+RA$2y7=%Wq1_NG^BeR`9-*zFjNQc3y9+Zsyg$eKsvkX<;P$7tSy@;m zoikXvdzabAi)?L2$wqd#eQk=Irr}@RAQ*}wfM~!^U^hfAn_**l6Jg}J`{_-V=T?Zu zGi=V!FgLeOG!(?Y<0q5Jv9r2PPKUW$w^^KDCmPKXT3O`w)q5<=E}r9o);*Zl}XC+H6m-u9DBaoWx@;mH+~&q7H%O$jEIyhgCnT2d-RIGRIX1TBHmo zpr8}+``Ov?lS=2n(4RRMK|))8QYnpOaGQsB9-|3}1$K$XGT5vp=I`8QeqoDzE?+DO z2N)XR-4OYFj&LAECX*u>jS>pQ(eoKXyAiZpmPj~(!{cJ>@k1UwT*fdAGRXu1e+U#4 z786Y0xXJ9JIo7tqXz>uYF5e~nTQo^4@tg5NBPUHWP)uskBuPV(l|7Rr-4Lj%3PmsE zClfkP4UP(vJGy2RUZZ5G@I*!N^~x~zOll+wCAkC=>Ru(1q9N0BJ_G_iUua)7754qb zeJn`L)UU|M={H3@Q`)TX4qWt}_;p53zfr>ilhZ>>YdLfG7Z8&2QpYKqZM5|?v;Js? zRPn+yo-MytoFIEr9_>lVGFMF>B^fQqPlCPrt%P@3w&A{p|W9-Bv zWljqspXI^jtE}%t3nko&=c!P*mtQeydSc8~$mLU<+Or-Wy zLr~GuN^f5a!Q}-WPp_aG&(D=TQ4_hU>Zq%*Fh9M7ke(WKJ^6drHH{~w@SaFf3+K9M z{3M>1auZJspo)L{l47Fw-~=rVJ^)@hWdIRsYC*IbuCS;4V0BT zNk(Iow{&s*{6S25hF~C$uf7hKLnW2W>`jnK2yEpw9KU#yDzAl|&0Xxi3aY9+q!KAK z&7gT`KRxv});EJtFb%`*DWkc&m8zNwL_ULLb<@;ZOFo%EN2Q^?6}w50OlI&_*HKqj zPCB0Wr};eplaVN^slsXkv%`rh8MrEZ$a)^_i*(jS71Z*WSk1SMlh;z@ZvFY!EF*LjeIIWAdsMI zd=zso_MGBIwb*FrXvJldNhZ?BCM&hg4UkQ+_;7~#N3$$EUL>B%VRpEvZK^@bq{-)X zoW2U&4uy0owHdT=NoJ`VDRzXW=6Ox`Kugf%a zv{B}^kc=mb&WBR*FPOb+_xWS_-O6JBP*L{nJNwRCKe#~uXdkIvKcla{#(~2lRJ&z_ zw~342Iz#V38=>tmr@#MA_U#|WVZ@1RX8I;3IDKxE#mB3>{;&R=mU5ZLcc=07j&tt9 z0TMf#1Y;V5hxXG@>n0pdaq35Z!m){F7VkeIW2xfx?_a>IC0Jhz(m6iH@e5~hBsW=$ z!sy{K>~fwgW;%ujIQy*&WOg}-AMNg19x33fAoL-mpF1eRQ8V1)7?nlNEfTqGo-RFdW96oCOiF;`|0UzAsr5o zNadb$IeYT9f)#^yRq4i{Aw4qR2WSVV?ID!aGX z#8zI(_=!WbHu{Lg(hMG%q@%r-Xdp~YTQia_XliLB6$yjAob%s0&Dy;?WbIW<>~G-i zXOF1t=;PG6F_s_PCvU6d^*?$QD-aKc@HBTYad-ralp_+$(>pOvXJ0gdEyxu zIJlpd<_aSIAi2D@r&q6T?_zjzjLD<>SiF0G@9jwliI$;3h9>v3Z?K7l*-g6lk8%9s zaY73-1S07@>7>P7&hVlA)YrNQ`oq-s4KTE?lT6#Qq|K=e}51O3*ye%=*Iz>;x0se(xhT{Ry&(BvV&!vKh>C z^y~p7-Jr9-9lPB|b#p5hfB0QG>s+LgS(ffTV0wC;k&{R8m;`lgZS?M+Ys`bsE#s@ouZg}~yjpt0J@+~YNp z(Fn143IS-DG_h2sXyhaihE7gbIr;W0lsQzY+B-4An{4ewsqP==U;W?yoGOnQA(`;{ z%CK2g+;y!S7;goWlg`e%J+ZT47&P<^)7M^()mu$VTO$Y09U~LkL08PE77Jbb$LZ{B zK;*N8c0=T~g0WC67({kEFq#>a_^XK83_<@7ht z;B}cXP#M2?p1z*?=X_E?Lg+f#bdIIF515%*AsPrGIqg`jD*0@h&~6ybFu?5MwLdOg z7jORPB5ebGoPPZ{PMeA$4BT~%j2;@p>$0#h{g9j29ubem(ex*Z5(7yR%-y)b!w1Wx z;z?4GAZsf-*qk;BnG~^?f~@Hu=EN%pu&Us$>)`Aw2XI!_bN0={sK1MA>kk|`n@JPi zTw`-1L^>8Hla90aXn{;JLo%IaXK|iDFi9#JClw2^G`B`JnZe|$ps%Nrsk^geQYm(~ z0<0}87L)~F}SF8oV#eER-%l6i%}14FD&JtP`WlTF1~e6&b5nIV-)vpqjYD3l@#=aE#_j5Tf$^EqpAv$ER#>gSY6u0?)AKsL{S(9)g7IP$R_?!3Rx=TvAyM9^h}(M zH9vN5Id;1RJ(FZ*ZV^qjV76H48fatn(F|ElC!NmX^f)jKL7B(N`t)N`s8m#Wh;A$~ zJ+nnq=ko!|WK!6gdBmDOMQd*h4yy@wMGY+-%~(t-s@YUH&fC+goJ5Y18ME5~F~NQHJ;UEad# zaiNlD=GGn3IRz=c!>8}vpm}JVu0}6+-~Wh+?qc%bFzPdMOI^zn*a{S`i`@vJtu3Ny zL0P#MRgo}|aC*HsT~5lp&cb!^;Z3#{7x?VbLwY6-(p2p(m|j`UNCyuTw^>>oIaEiS!N+c$t)N^t3?9}}0|3=XyO;G;`Ce6&vY#2ERV zWiG$_8A@dn9nBuD{o)d_yoEB0U}kC_9T}I`Lu6}>P&h_TaWXX0#mr~dSzHZMSyzpg zjPU6812R}Kl0mk2g2dB0{S%|amS_0%!y9;Ip<|7?;ZP0w?|%iY66>(p9n zSz2pdt>q}p03bjR=Nx|6wLSZN{=%-U(biyxJmdjELJS5YTkEPWtt+*bkr`TR7yBV1 zv$A@+2bd<%J;c^y);3m?5gxC*zry`}?&rE`Z?JLW-Oq_6a^Lu?nji=$*#wjK9^sE9 z2re%XPvn`pcAdPX1}PgM^c5f{Q8r;S!^+Yc8UndwoVA4&as>@E1^@C2p>P7TyOL9{ zp2sOD-1*=N8-e8S4ssF<>_2~!$kcrnR->S+7@f8Bbk?x8utXxG(>*$b65S*xxfmGg zV(IoR7B-64G>*jAg&mR&pcous9@I8tjw>lzT_icD3I4B_MAOV zPg@oDuHGb9$P?I1pk^Y>ucjG3FhbhD%*=9(1ILHC`_UDWSq-JAP}A8*rA1|VaUI?0 zW?-m|&BYZw&D}KB*tq@v71F9m%(scA3xp#HntD2slspTQvp5=CsjjdST3Kdk(Z^1$ zlEv;sO^5LZVxVgn95wWI*RnRhL^1KZ zGr;=d8o9hm@DrwkqYy zCA?5(2k6?i4N%E)Nc)l-RkqMRalLok}IWta#C$ z>W^PM7g3USD5uI+d8$DF(Svj}R{^km?+%YAS9Vx!3Fu|}JWVS(&xnSd_hM^YP*!QE zXkSpZlq}kI>*O8epZ4Cu~=BNhT}RwH0LJQKHf0b~aqo^e1W%p+t78tgA*)vIPARG+o#p0J^5* ztg6K>g zuDc)^G0Os?$%@?~5b%X4sya5e8=;sXl~Jf~Zy>(mBb`wY4F&{sOjbL3K0`MDB)O@V zWnH>nl4GLj2$F~>N;o_&WT~|9r=l^kd8O>{Seopf@?$KAD4VFRuOb(V5R0V|3`X2; z8;NKPRr|uV`U;@Y7ekWe63s4~+uqCl;`P>b9Z@nM2s$Q<4b&p`iVE~>f>1b7vMcjA zNylR7vX#1e7wZe_Y&mA`yzgJUpL!YOWOq5Sm`fFd*<^xDPNA}{2BDZG98TeI+ek)Z z2(p#xIxmS}fK;jg!qeC4xAhl66w%c$=Xi)^x?_p(SC$oi3A5?x)$<#o=vdfPqFLN zaoU<(_?9=2ELL`(zCd@S%G7)Sm)DK16tGk^aPGCs3=g*xUfZDK&~eUQJ_@M_MRPUJ zeeVU@8(plghB@%ua~wS}O4{!u8qa<^9ZS~{j5f}G|5dzl8c$m<6;2Zahxd~9`@S0F zBnVjScAQQd=q1gtT2wKb&D)dOlg~M@S&bBOh2N!RPlZ}k=|6dno;sO~p@P@`@EnT| z9+D~;IQr5HRESYlHnLp&qn8om0m9*QN&hQZx$wQ0$orSc6m>LJ$7GVJXzk|QE9dF% zsKGbCLPcjEhfnP#7YX3s43kM`$fPr+<#wX0CErPl!-2zLrC2B+8jLtSE<{w)$uyZ%>Aylz zrRUgLUjOb{v}~Ms!NkQEjGav}{&?R=J(kKE>_&ls{kxG2Mp{}cuvN9v z+UTOPxd};>*mYnC!Q`N!60W{?9l=>gtzBVaHq7N8yheU~mV39SIr`o2GT7l|b77G{ zOyThJ7dd%)4}ql>c0czLwWcgLKfXodfs>p+J4|G4m2k?y3qN|5!QnpAD|1-7Mmc@? zI9+XCvZ90G{yIdn8$Fw2@99H`l9A>X501Kan(LfYwKgFN26pWqK(IKe^=LePu<(t8 zoS?KHJaF^~yAKbL3$Ed49pU_?{rIQGnV4Ap@{X=+m_1E=_s_mZW325?|(r5a35AZ$;wLb>w%n%R*qgc%Hr)C%q|4z80}{D(HKS9 z&B1dAP{J$BE=1V>`~~`YI%uqSGCwhgt)Y|MgVo%*In9w5pQCT6n<}S-wWWuaDie;1 z8lr(1&;6V4;xZIinVx&HaOyf$-NXFg&t5@K$61)1r*>dB`;U(x=EF=pT)}L&VUPs! znG{9YPD8bU@rNt43=XnuPZML;?_sL%=6nCk>kN$alM9C!J#!c(qj3Dx0dks=W9N_1 zHPA)Uw?ZfmZ)-+XZT^u;p%ly3uqy-0OUOP{7OA8h~L({?IoVj?2)zD&`4Avg|pwuMBch zwznCTT$)5qX3xHE#%|oBfQj8F4&onwL?D_$KtR(q>W267`VXJOE-M7XdCq;<9w;oV5nsD08EIxS1Y9NNI(o5FwW9myq9!Ad$N-yb+RmL7nBiSt9O2%5(HLO*Q96mORu8Pz(H()dx@m6`r zXVT@0voHA`V6-`rvr*pu+h4J?9HD*pQU3UU|1-MVT@*zV2cN&h?p>W2%~tfxIv;&_ zpY8)g$X{bU(lrfNeG5)8#p-&rv?Wp=e5s(1@v%h=79${#kNJ7`Z-0TQr30_aN^5@) z;rUr&ktnNyJeS`%LtuH8LZ*mpwqmvjIBVbHj8R6g;Ku*;SjRYQEXFX-)(3yiA zKCu@;)2VH0z~XSQ@A5_V?(IQD-+ui@SDAnKh;XKeBnp@v4$>RbJQ@qq)zN^f%8SWl z!dvMfxHwPc$PxM)O>At&sqGo${43}2dRz>Q^z-Q5_XsD!Q|X})S>*1WWxBeWaaDRS zn@xBsJcvN+KrhjyITBfQ2NPOnZEBLWUdQK?$EJ+h^k5xci#UPle-?L ztP+hRkPJqQl0YIHp{Rk`XxuT+2^4cFW*$uO;FGJ2Pc5_fc#L4GfI%v0XoPY>ifGw6 zE-Z*@j(9AEESHQqDm!~Q|LO&*+!k^=28)T|lx~UQqy!-3xbRHPO_zdLqjfc2AGl{9T zg@y_{Q#bFkkyPj%?jbU}!1(waHQhb9M3u1{_emM-^z<~bHol1LtimN^SXm7ricl;R zP*e>imt-}Nrf0O5L?DR8TZPAIBo>KMP*qf=K(VNTUSw%$gQmej48=4Hlhc@*y4ka@ zi)eJxA$m_Wnk~bSrdCC7aEnsydl?n1ZUX zwy=Vd%ae*mx&HHCap&#~nhuri{Tx5Bi_pUA*MjQkIAv5#`CNuTSmn_9AQn#2I(mSvW-F89Go^8HXPjg* zM>H6xb#R2n8YhA23Eug;xA2G4bR9X){@u;!k{P?zK;6J@Uir>xBw+V*m*{M?BU>F* zH8#-I*MgGEqv<-<%6eY^lh>)R%VS=t zTd!_v!bFa>l}*g93hL_I_?K2l7X=#Hno*MxHa8?hLj-~;YFnEys9DyRH;|oP>S|nUF0GJNL>k(gD8@r<`eV3j>+w1a ztSzlkRKBg9wbDMW#pOg66a<4AC7VLBxe*H~;)&dszClW|8JE*SED}XS#8v5`m@8m3 z2*jdsprmqeHh1#}e|DBn|N0#k=2poT)ZYuoxv!RW?`FFnO!7N=4R}Hz+D9M!N&62@=utQ?J*8BvaSggqjMo zxf#PzSwp4ANWdR0SxglKWT8M_H`3TrMJ4di1X(ozL4zO;v*{>5LDEa(V=M0HamvX;iT7R2QA zQdjH3zrKm9wjPV5vbMN}+3BR1$zZnH$Yyg?H8vorS$r!S*s5x&a9aucHjo@1JT5B$ z@r@0V1p%1?i9`k+0eeLi)fJZQd(qg|Kq|OFGAC}2i*PJYeQN_+Hi>U_vz#z3EyRwh zDr#%J=*2v~rByW9hQ}e}-w5NbZ=lj+N6Dq|`{PtK)}v<=tgmdM8_ZPKdI9ycn{5pN^S^STNFlz18RYmz4ASV@7)ytgRlH6+xrmpLH$yq`Y z%hI16GQb^@suJ+21PGGKk}*LtU=Tr3p2&qnq0|lp@u@Z%WCH@frywU?D}#l8H;~hp z9rIub004jhNklYJ+xuB=l~o@`O<B{IWSf)0{iC>SMs%gX^qu=`hEgEx zu$=mWRol+k_9gs+mE2S3Q~n&=ERgc{2|JY-;I40=t-X%O>M|?KfiKh&K1oJey%l(! zGQJi6_TsS(WfGrm$5U2jy8hH}|Ax((UIICFj1JRS=K;VsHNous#uuPdJMYC4S?qQj zcc`A8x{f>gkzOu67o?JsYU}!LwPUADaO<9x&a*VGDTAEY!8!TovZi|$zk3L~9DRCI zcPAFI%uOv}t*WD?xstW%SrR$u80bcc2UuF%q@uZv+6puC6LUD~8mX`If|_Ic!4%%E zAr2nsV&>K@#>UsEZEr!$Cs^Bv$5D$QKm4 z_8+Iw7~+%9C$@_y3??gW{ar};IP=ph*sB|8YVxu=HAAu>(K*mbKC;R3a)9dAcB1k*EiIyD%dg*-)X+;NyyCx5;$w?4Sh^n1z(~*H z9}j8&{^x)E;_IjA8tg@hN7;M%G6N%hG*%lCy=|Pic!=)7E)tO>XWn>$?!jI>a)ym? zmX?7Ljvw!5ZpO!}|LdPoZOroc_83hEPH^_@D52#gqDh_oXHU@CUPU+%=gc4d8xHhW zF>(7Ly0?{AzIOpN5oFC5XY}+*PF_5P6j{b!v~lFzQJk`ht8JKLhr6im=*CdUaNvdK z=pXK-&TBxf>EiVH1N01b5RYUy_vQ<94fNs^63oqQeDi}q*O48Sy!xl#rDw2*8ka~# z*C2yK9kg}UG5>g)T=C06PLkQl!SkmX9BKnWq^izEG@PMpxSwJoLN;G=ERpSQj=p%F zp`mutq2Sl@n$$Fbfm25*tWGdKxsI)(mc!>yQ0FxhOKTi^@jQKfO+_W_Lh9H9si^S?q8TJ_w@C6 zHc>x|e>|j9$6x(%r725X5pcS#RJvSj_ySZkd9j#1sHrfT)rQ4l#cMaR5lT{7C35wx z>(urQ;D2zPN0Xbh_4P4#^&=*hl3aS@9Er^!wRJ95S3-1*3~}`IQG(<5DA*cMd{caS zWt@XAT*6w6W2>yhV5wxNr-IE;5koP<;Moh*SKH|xY$31_#p||U^44H8fY<9`!xyBY z(SyxWK`{{ohXae*iq~pjBa+7J6?lASro@^)+y0C|PpMFk8@^Jj}NxBC6Xlt)#d3K4;{RgPD=;S0PXD$!n z+bl3LTF1n_S@hqv+#F~g?4z)`$nt6wS9KkIdqy~Xc!Y2$!O-3&GOC9rkC~nWdkL?s zFnD|~vc*Ajoe7h-13A9R!djH(-U0G~MSQ`;{}`+LSv-qp@s%N_5^*xIFtKQw_-2qy zK15!%rq#ppZ$j zJhy_y>!I)9e%k7-#G+}821rDc!~#KL@ib~N&)WPVy2*;uX+~ERvgvGTZ(4-V(mbEO zeU-5rH+ei4#p95fpIjoB&l2)0GtBvBxj&Z3JV21y{|TV-QC zg3W5;S#`v-coyGGNamROV`o!4rd)`t?_VR5mFVoM<^D%kn3!ClvA>_>@(kBMxkJ%X zL1VR<>+gL|T9Gj-Sr+D2P!W-h5|K?G@kEkDPN1u=nenSPm|O5;_jt&~f{Z_yBvXW3 zJVZDWBN9o|I?zXOdW;*N-9@fyq`u0`_|2P)&G@KptRv~)B$U+X=x$>C(@%K#aE6R- zp`*QuyB~bU;A;Q*dFh*cozTv9>S=lL)W(J|I5iFev@tR z6pN!=c~ufOJjs-6Tbbf=M!d|W+4*y9ftiFINwl4AA6uUj${9%kp~OSbN-&%fPeT7E zw(;AWICM?>O?88=gZSIj8(-i`l#_f=NHBG1g%mqf< ztbF|Q_gMEQD6wC}vdz{GmdQ3H=*fPwu50K>v<;7N^z$ZYM zf2z-Y6R{Hl^|WGQE9qNyZuw_!lcs6UR2R?UsgP=9?~i4Z30V?R%hEZM*@#~DMl_mD zWr>i6WRNi#Wi(aAU@~Dgn@Z9=Rl(?R)6v(75rtGLi)1t+Z_&}VWI})>TWIa=#N#lM zjHf^{U^2<*YDwl*(b!78*GM#;-Uc&)j@461XI~qFk|Fquu;zJR4RmgG&6{G^TF zbS6vJ!C)x)-2sRO1LBrqW9#3)738!N_9JXBI8PR`(%+SAipA+wR89EW6PF+egqG%6 zSO{Q{L~Io`w6s)^iX~7r@YdF0l{KKd!b zrPFA7c@fcfz`nk);KP%C_tbsb`Mt6#YIyu$81T?Cic=skIk^Ut5a ztR~5us(9&#uh83B!}>~qBd@&7i8FgCL;^Tkx;cL7G-odCVR>qiqi_BJNB6Z7SXrTH zuH@wNCo$_;f}s@EjrCYf0@<9x=!F+~<+Y=vLw#uV3 z)Nb$c4y^5s}ClXFlC@P312bX^EBB`}S6v2R^=-6x)YI+8_ z@X9&*dz)FGTcmzyFGo-AA{E#qo_w-ZBS zC8EK|t_$ZGYB%xd-Xc|<16;VYmxad@6m$bsjrBO}MzWbaHkTcAu-U8_j3$gS7|b?| zqE@PyrTn%f$))evWb!!$Z(SW8r-f`Lht*+6&~v{^f;=cffv7jity3~=~M=z-9>Ff4X6bQ1r>L7HI-FP^63nc#ZGNw z4H7E(va+YtFcD8}4HXqma_J1N<}NNhcaWv=39?0SR#sA7?WUN`J?Rs=j;o=Sb8kGy z-UEGX%+8U@7PocNwS9wJcnY<6TUS3gnbRN-@#i-AZKq3Y-26XaD#|G};QJS7tC;?2H^gPG4swzxq#q zO>=J-N<6~yypQ2?7dU*NiLq;Os#<$E_sU7URSo#APIK(F*XZx6L5Z*P)~_GX($>Jz zrw>Re7EbT(CY#POdSV~dQ8RXIR==&0tw&pu&&Q^g~zir~l{AFg3PeC&fl6&(YUjqS9%^H-49-vjvA) zK(y9S)6&7k?>|R&V+p6-yp1r`HI3T-VV-;SB)-WBEEY5UBfF@rbCSwA5Cjodr5C-B zBOXmKGd_p6x5VIZxE*-v+{gw2PeTuH{?R!KsUmmZevgK|!%Tesn0@;PxP5ny{U>)r zG0SKF@GFXf5vScuJRD=-)LC{9HQ_WH`0(v39DD67O1j9{2Or=W7-0V9Lv|n9%e(*W zuTg7Sa2v9$t@;T`GxRmL0|GXu9Z#Jbi$x}(>$vNiIrYXXtUkPj*3eH^gNsaHgLmJ5 z$cumc3W4$aeDZFRJ+FO-c2|LG?|wo&mflVjo7`2r_P=}|Ay;Ji)>URg20V2hOhyrt zxt2Hn{r8B^j`8W+pHa|7Ty6*1M4WVJgO7jlAus&!Roo6Un~^L~DrgBW&GGTuDlfi% z95Z=lA5Aj2e}Jk=2a~h@(%YS`<7(~ZJAe8jzK3_Y`f-7SFTX&Q(@4g@z|a5Y1B%+S z+4}b-M5D=we|m!H*>z-_6)hFx!S#FOkuVtzEZn=t>P8f^%}Uz8&cnOo=&}JMGra?i z+`oEk z)t|kEy|tS?hjvj|nd0O3ZlRl8?AkL(Cgdj)Pm>4*n0++E?N6=~PARC#C=YHtpePzJ z86@WJ-eoNi$858b@~!gd{v`Y}s3iWWq3h5wvWKQRH{P0hItGW>wYwLK)k$+l3$j$w z)0%DOuWXYJ$YvW_%+Gs2d7Je>3QtojhtD6Q(rsaVZl1c)160U45;>h>Y?Y5cxI@!$ z_tykD>3Eyku&OcEHsVNu>c?b^ew2W$K|rnKG;KGc7|jk z!Rls;BWL#!Sn;8&pcE7&lZ3anhTO^+w;%f0du$MaCsmT(>MGXnTxBr~`%ms?_r4)4 zW*f~Ntyt|g29F(OaIm#xH71sL*n*(5G&W8+Rm5NrFrx1Zn`^N+?3m3a zY<3$U&_38pXnvMdUMpQ2QA8BMH#@^xAc<@;AP541na6zi{%z_9x^dYBZoT^J|6+e>9#6OAqd@BQQ*oE_~rtn#;FjrT0RJjD62hZq|= zX{)jF_{uGo!Z~&y8fD}01o!WbQ9rZ`7dh@K2uiUPSz z8b#Gnb8+TY;_N=Qm*Cgc*a z$50P}xfwDA4Xv0boDey3;RJ4nOlTuS)9?WXdaB9fC62#%5oLXuMBYS4tB0A1MU1XW z+Uv~xDMT$#T~9xoH$Py)4_#fgY>YqTt-pDja3s#c{4%>PU8JwO9wnb6mCTUO zq(~(btSrYlaN#s&semTi=;)|qc47fUiA*et=x*c4@nIyrNFb2o;JMRyZ2|^Y6Q|GZ zW%g>keZ%B_8;oO9bbwA9)W4H9ahh^M|8hnQkHQsCm7mx;~J zlFSsJZG-$Cg)n06(w!9*$P|c#;>Z>o9YrO^>E+ia-$427bKY_{U9aFPgx$tmEiu0R%4B7rc-W+YT{**t!MF%>7C z$YS$&aoUZg-_|86sw+@3Nn+77X1k4IHce5{Fk0<++*YE&5JgqQTV08gNf1wDu)5sX zEe0ZiFzPq?RG^oVW>$xlLMDTvXvh{TlAt1pGFmZ*V6-Bt1+rgZ7$8bA7K=W;jQ+PPbE-AQwcET4`6BR=hE3u{{BDy0|mi|%_@^l=dd}fF>gBmO(ADG%-U_wqh1l!l5Xd1_rYUqXDv+FI*cz#8Xv?o=*{rrAwx@ z4kL+J3P}=BQ~?Qvf+kT}?IoW~kW+N@LJrMf!hljbW?A7<*Eh(COUJ3P6!~xe;jhT3 zI>jQ$qDEd3sHiFRN8Q!La~B8r>3{rtGD=CG-rP7!c8%p_w8WSawz#{rr`EF8n? z_FzCE9EhOm0#3J+Y%+l=81PnAASgM)!6>51ir4EP9tx3r25b7g3SrdRp{t7e6v%1I z5qb-tB#7d+r=nK&hC?r9`~^xNC(tp-a*3C#l+ceQ{(`uJGa;0{@}3-9Ra7W@+m+lw zOvs{6HdiQl-WkO0-`(2l-TJp^kiHZ)Rchn619I9{5^VVxie+_3S?ehLBc;w?p7rM` zrGjKMBNS%qyYB@nRKEfmV=R+Gbt%?Qy*vV2NU zt1#53fLkTGrO|4qbGR3~r37+Xo|qySO_#k3r6)dtl&r>{G_uqu)nc)1IaT%)EcZLT zY&okVlsqJN_)={HsGgD=g1w@Wy7~%Y8$Lpz#J0Rt*LTPip>&>Rk!U!9Ae2C3s-iq4 zJJqz(I5C?IWYf7a*y;<46731=Wm|P28ffh8rl!&f=mZxQSXm2}Y`FBNkG~CC;y2}i zx~}a2p&82O@#J?^^~u=U@?req=YXf$xDD-krkDS{3#s1P_~ZVA`)F(O65b5rZS7+B z!6AytD0$56I(Cq1w~__t4T@K{ODdsA%*YJjGz6k+q1%^FMl> zo{lOume$A>we7)I+u6&m{rwc;5ekx(-Nz47>5vEq6SNGCGC0&qV(ZD^8}4%@$(KM~ zS1+S``^d(kc-%YSvdzG@;Dc1%LLs*p}*pPrb0*U&Xmu_TSdd+BR& zVXE!qrPq(JGBHgmr!#uyEVb4wzEFX)-@i=3ze+ToM=KV{K2G`)y#c@5v{2HCudr>=p%;ZAa~7}>l^^WY%0UWsrx zwLNJVZEi07!5g$y>CDY+V6Ld=?8SrlmzKzX3B-G=pFJVTJ$-Ei0rKf2p^YG!Z2n6Z zx~*fVg#ww3!oCY01I<0~cQ2$HL>;r@7)gEZCm- za8j0W>LdaI)|XcCEw4SLlqn?~YdL%Q2*Ks0Qok3zWb)K?%+3mSA3sP_os+OXM2P`b zS`?f$4eUL!zcdD;8G82ZrLV84G%oV$Q@{5#?T;O*$7d7v?^77s_8jKi%m$H` zLnm=r4RkfTnB0&!c%TCT70LK1`!AoxAd1ww1wOgHgx1^7?vV;+Cf9lKM{gi)jPZFS z&Au03W?z2=pZ@fh$W}K8FC51yX88QW+njpybp{%AX0Lok)1Je;@Ztd;ee@v=(Dbd0Q`C$K)Co4ObqNDB69Zw7G^g&e5N0Zsie6c-q*+K z)dh4tOigDmopoLwe0GBypFhS}QAclY9gDXm4xT$fA{@hNC?eMN(BI;~Wa%dvO|$Ej z*Kn&r-d>zTEIZNYx`wT$g*X2EJEWHRGP6^C^{G9R%iU{nqR?ITY=S8!QoSTnY(qDc&7N(bG0Nhbm}xmj_<;Nj<>CwbFW>b z+G9mD**N&zC3cN=V0C+GYLHo2OLFw`Aq-#VJar9gMJ+W>o%#6j|@ra(2N2qk0sPE|^<69&U2vacE@TdQeKSD`FNT&)^)YW4(gRQ2C zCYMSeDY5ThFT##Wz^0x)toa~COEtR=40H1Gagv*x7_1IVRvY^+m2ABPU8RuEqo^vn zuCg*YO-3*vOW>$&pwf{irMcNV+D-RJFK&;AfstO)p&+BLe4jIi+K7ac)btE;?$vX6 z+)fUhIYn}54o7thE$z+JRzW0VX5XGZI){7lc-{05_hXWD>brZ$`&S4?vnYiEilU;U zvpGLUIICbb8vux|Eik8Z4DM?r&rN5m0TuIDxM^l%aKf_NvG0?Mk5`&chgd9CK8S#ijd9Z$fiFuf*_DjMoAY6CJ1&SpZ!_lkK$kwqq1k zBH<*}tsT@ZJKYhqz+2HjxB<&arl4N%okPj!`>)v+-WMG1#ZB$>*P zN#`+HO;43=<}zv0=?wXTg2f`UHa(AOu;Qt3BkBq(las8j2G9{G7F0Y9tu!`x$tL4u zH8UN(ZJ3QBnM91hMi|)uS=C7AKsRPt#Oiiai22Eh7Cde%$-oBFkEY4wifF2a)#b!u zHX;D|R2=_W09h8u6ht}(da#-$G{H!Be=AGl)2Ik2MFneRJ*}-Z6tgK3S!nO=!e)^W zB^js7PBE85Qx!t%K5}^#JssiBl?Qa}KhAJ>4O7={64yMOJ%0qFsF$_QrE5~mrr2EB z#9)xfr4pnSBkg@17>Wt*etL(-kwfemY9SwwGW&3Xa5ROdwU<}Ef0mVrNpgyYt*U{u zFQ39<&}rU%m<#6ynSDHsWVPe4iY(9jsj06*GFdtP@&b-*AY!t5}RvGj?I!v zXGw%Z#1bh&n_(&%8(4q*kh?b?QZPDknia>G7)q(Onq(wOI-SGhuHx{y{lu3Rxc2@H z;$MSjtxIMOUpmg}y_>B1QwW+uI;~MzX~)01K_nE%?yaH_3*ZmssH}AI=;JFy6ekDv zw{rdcYp4brRx!h;?|hEIYGQF}0ee*y!I>%U-+V;8Xu@qSa`ThBD1wa1Y{Y0Xk_ZIQ zEFOe(kei?0VRmAM)ulCj^UDPMQPQy(>2wZ35XdD{7;RppLW0eW5b3l|RfP@T(kjtV zl)y%aXu!|>e2|)kDwZb3iDgt)AKYegA%djl2>QdsBXJZ(A(bnZNRSOqZoK~~oBk*P zUyyhx$n3O_n#LN|rYG1;N*p-Q$&C*_CtK7hW;0-LU^NujSPzhh=c%gkvbnfSL9k&V z%Z>N16OE+^`vOEGaiXy_)s1z;*Or(Vo5xmNi%lvpe(y1nL>{xngwbe3&^6NWB!P85 z@mP$2KSV06QB~m}7>H8USWnu&#`vRY67dwVaFl#O!RmHlvltO21F2AutRhiWVP|7` z9h1j{Nh&!e#UpX*ySp(KqTKxK9+^B;*H;l;U#B3NX>6?{va-n7-G{6N($qJ2dGOIy zHiO?>1@N=@$3qykbm*#Dw%syFplPU@hA5U^nZB4&)^xTr_FJ$SK@iG+(@O;mTQ)@E zPE`SjVo7mOQk!gjuat?FBoS3n%CIH_k|?04Dni*(NZV?oB$nJhG#ybYsX@M~jo&uN zsdNoWu%E5#@QbPd>;R)at*X&= zq_Sn(R^rLq(nVLJg*%@=Lf@fY*y37fWo3aVN$6!|j>+le@Wm5UIL+vV9Cttbm?d9q zTS>H~!q~FR+SyK3DXADlL#aP1MRnWdMb~$1sg{`*PXWAsBYCD%*E_)8{r$vN=D6|M zeF|lS%och~E5SH$H+Rt6>g3MV2WUb`(WGjn>miClS@lxlzHD)5wjpL;P=l2EzWAh# zqRjBgV|2Dw13HVhZ*u?fQdtdBDn{K>Nr_@9S*oM|hG~MX>pOnCH10}m6t~q#PmWQ# zhfl^`>0X!4`RValx{l&Ag5vL42qTtG-Qut#=?dvo7SU+JZZnfkBvCZ5I~{0+9GPq$ z*=)sXmPp1^nCx~eW+R$XBpFX)_Egi;-$HnKiRI-0X1g5;l}tMOlsBGianLi;i<%5E zJFy5xGZwRfR6K>*T}kg?2l4e~mX<@b4)x$6X`?(MH8^Q z9BBD0nQQ^0#fsS^k&36j)%`YI*D;#S2)c@nh^7`xm1;@><+s?8Yg>crw^?85C2K7~ z5D=xWpFq_#4SPimZVTuZHx1Qh#_o=j$!XNKwIO6eYzDIo92~?qK29WFKomjK1!`Mc zD1y~yOz1reLmj+)D%==!!Xfl@4@3lesRm0T)KQ7M7J41z{JuWX;A z!D6R>&j2akBJ&G=B(sCIwi;IF7ReX2ZOPda@53+c;!j`4vajIQg0AoIFnrqcvP6}v zX=y=AC#Y=iK+O1=npj1bOtf{i;9s0aF}mp=tmV}8#l>hqpiJ;wfP0|Mg7S!zR)KGQNreQpciwbckF`$hz1$E(?&6qCYw{R zIvp4Um1H9Q8{1}EmTO{JKC71>GM+tD|9*wk*LCQ}FaPLOcJJ*avbxUD`OBQXbQG7A zqiC(;JdKj!eRM%L#S zu`~_x!#{b6LNq{5baVdIv-Ayh5!={g*X8Fqb>T2}J&opS=H(x}NN-Oa8*5=sfA0+r zpV)&I+r+mS`&OQ=bzR5gs^&ZY_dmfcM5*lAMRUEE(PMiFF3gk8ez|l{kYwuH+h}U8 zp_opRD=1G0d_2(ri?WfX?oMiJJ!BFIimLv#iWf8u_FTG5y)}z3s&nbPr&+rHfUIug z)T_^7$!@ab%W(cjui}x5#KQ?P*#ZWOo6FyOj_~3FMu!7U6YzNK)D7(7-18^sAMC_8 zw@CZJV;ng;KxBQLSTtRlBo!6WXyf4J3k(l7vaz^ALA3Gm|N3v}tS&M!wpO}^fFMY8 zA3n-phnYur7pd zvON(S%vM@^yQuWo$Yu*Pc6U%yF8-iR7*w`FTrO$cWdO3Gt5A%=5$rc2vTUuypsV1LFQYfn1dTn<@8>e47 z&w*p3Y|cy*FZ0`VO{cDRkkc=oWpr0N%QLGC96!#f7fzyt*4YRoc0jcZ)VH@$U+*QI zNR-;x-GY)${Z@(Tv-sx2&^f#tuLW|Fhh4|_QPbW)CY_;c{|MHA0fU;yQs2m)V+Uxh zcag}d3?As=KmUhcVXLS`3&n|~b-Z2|Yme@;5zKPw&%cL9NRzCsU}hmk_ntkB4mR=g z|L^~atFnUF>MHXyYwSAr0t4L@EEHo{ZA*B(9(=R6SzQiLcW@Lf6=UY{9FEpe#Dt%e zrH(yE_fXqWOFEsU=in%=h#67I;%aJU&$0cq)YwVnbOsOfa{uAdxA1hWBgz)`pFV`B z7ciJjxawQDaQq~H_y76}0+H;~=C@nl(lx9VO}z5`mzleNoABByCWnRUYA50K4RQ)p zx74F1qvVYhy!eOD6WdJE)A^8J{ru+F0y)VxH*L*sZd_j?7=}P zYltQbO?|^Oban9efAKe%TRJf2!}tPej=lLh9nEegKl_M;vl6>i29txzrgkp8c#OpQ z8da4}R@WmysnDabZAZ|gF@D9L6i(sH#eeZlLR(GEX^%b+fxEL zNuq$g%7xJ&;BD^Vg&&+C9oL!s_#^7}4DtB0ha5UI%$3g{bL@rFNVzC)|J6?^N?s}} zYy?*QjGVtjZ?glh+r=mEe#ViPPmqbL)L;7yXMaDl*Y2_J$N_%!fBhviv|&+WtgQvH znU=|PmOxIDC}Q>6v07yknvU7&VffT3TG}U2YPxA~%8*O(&O3K`=}%r`<@R;_tD7A9 z?wd55l6>}7oPa;JbsI3ct9bs8UL+sO)7^B3dy6@2RZh%C5pz{7ul~F56L@r+(CQj_ z-A+w?B?`jTnL`kO}&Dd~XU>2g&TDf1rU!Hy)9QMp>R&V(ivE!YK`ln&6YS zuF^6xK>ywm8mg@zSa6$RX?ls-2ai}-3FCIiT>tfZnCe@ou&FFgFEM%hKE6N%-@+f1cQmT!G4;W zs*nr@vXM2efBulFj%EyB3*@9x(b9o27h)rjL=r_rgN)H+BpVMiJF|*xv0{)Uipen7 z-}@NV;lXaR&@tGBZ+eZ(XBjdT0KDnt9b5qB(` z4!4ulyVqFo=NUaRN>_I)E>9)x-R)Sd7Fq`SX>P7Y5K6E929ptkB(OF!LpWW;WR{Ul zCc;aTJh-<+ZH)(m*@DGt!E7-S+gQg~-AuJZAR5cysI8-Kv=^JrN>gJUq__qeNh70beME*Z+U=9WHuz4dQiK5sh|^KYyCFJD;)Siz8dCB?POW zWA@Z?^1^=Zz5hOuv_d`^V`aflZDSRpC{f*5kHu^t6Wrv{ooT9?T4`u%qQb0kZhevd+&xpy0}wv(1hBX>W(&eUpxy~lR5c>4jjuivL;cn|hMoZBB?XCr53 z_wF{vua6;F-4p^FYz9hJPRVG5e6a*_8k-8R@6WY~Ljl=++Y zxqagS4ZHSXN{6}q$#nvXon6CCj9s7jRwisg5XpvD`S1Uq|HLQnea__LIl{hGe(|6G zhPO>r)=zpCX$|aqF#jxKlPej`}t+*YvV)Ung22!^UQjqZf{25%NSr zNivBTk#LB)nGgrh9ix~@P}AMZ(#OB#&U}W>&RYCaW4!m%_X$ViOpZ^p>-ozJ_BB$- zq=|&$YJ&rbz-fo=lsj3 zvC0BXL&G#TxT$OJpthlgy{C^*=@3|32_l;9oOt;X4V5+wCNmb35u??PkO?ximf*sh zm)N}jxKtDHY#ZeFCWH}7r*5`c%gMkTqHM%!HIYfCP<61{?dZiE*<2CXWX5cgNGCHO zO2`J0LZJjvH^@dbwOCSm7-VdA3%OJZMFp$FjwEQLlNkibfS{`sm6FoK>a?TgvgGm# zveAgH7E#oa(!(GL6bnT}*@(qzBAZN=vfWR$QNZf3q2+Vr@;Ydrnv2qq^l z{O~32|Kg`C`_qVkWHfDqq%;kz4jXDAhpJ0htR^z?IIi{~P9JXO=l}IrD1yA*2aIMT z#e!P87lj;|Odi>6!E6-C7X|wEkK(eH*dWt)?i0&{QPfDMvt_l10ZA;WJ)UkUSZsC# ztw1)DFSXGumq1`*Y2RHyL)FAmLM@j?)j(5<=%N8hcw#=FYdZAcY>U>k`>feJG8H8EkDVBt}MGK8L--nq!MZPnr{z+C}DG0$fina z4$&ZE5Op${lB&QU8_~5QiV7~Ti)lz)f4%YNuRG5G5=&GsRqvLP0?kC1hFJen9wQd=&(1XPE6y zJYEOMNSM?PzRJ#H0LW$wMp+=AErL)|_GpEC$>Bv5k&QA+zJOpb;&hpbhvR%%n)m6x zzvZzhQq|mq$7KU_VjDgJ!T6K52qH$KL@{4lw7)ohb{_kQq*Z$ARJNp}TfUKBlV+c(V7(|b{q5we1vqnFRnSZ%@Yi_m@WF#8Vn zlL!Q;?it|Vi31FccHm!LrR(T9E~etdXAjt#aBlr(*?#v1>SWVd+V>x!uhoI*ZRNEeoMidY zIH{b#zKfTrF{M}w&%u4d{Al5tEHFpp%M332g=mZ3ana^N5m!B$o8gmdXY$zHpq4`8jfhQq4o@ z82XOer(^ZjaOCo7I@>As-b*6WL*>(Dy7 zi&HP3X8PR(O4TquO?K92XPCKN`0_EJ>lmHYT>j2;$b|?KlxtG%V&s{&1c7`q!uX>(_IKI< ziI(0jS~_YlNZ_b%<)!amK*%Qf>=z%>G}6b?;{|&AT3K9=(bDKbkxVSzy~)Z(a(hB< z7~I9N(|a)&l6?IBO%A?v9}!``;)Yt zZz+MC+S=&oZQ#)d5@}JQZTA7bWAt$6y^k=o4zhcw1>g8Xu3Vqz)N2++bEjCGTp}8Xu=~U%vKuRW^7%AE z*__x~S5PaUMXt^$YCM(>{eG<#)c9!zuhgHL!czDCbLJF~a$(zI(273Mrs_ouk_ z)`!e3Zm>Kx&-nG*T>b1GlHG;NZKq>sH^T#+NVya<4o|zI?x0N5|x>q}DF*@z3AG z7b-At^fZ6`zyCXihZ+$~HjcdVBK!7qV|KW3ThrXVzr?=t2av_D2XeAkH&AIQvbY!k zO0U$Ku9FIFGBve~*pV)debatZnW=KjN_8;HF$&1IxM55R{6_{-f4!!gO z2M+WhqM@m!idtP$nSb<{L{Y~eg1xc|x#(vtpffVmK~q;VHiwL35UaOup&Gr^H`QSiW6UpS7#-=Rp`#gx!$xaIE3yDhy}hK@ z7l|Yb+iY%K*I1vPVk4fzXp#Vd;QRzP@65C7%mG?EYnZuvpSrFAsw?ccDqN`fJgTbW z@Om)H0)-vs6FNGw)j^e4V|>iV?mfLMKYm0suVOHi41V2}ZnQ!HrKsSjYvSVfU!lFJ zl4K}^YOoSpoG6h6MWOsQ0Wp6Nqr=Ja!+QiH3F6TNB091xm3Gd`+cJ{LjJwi>lFK5y z>exHn!u1cX(|h6|F6%Qj$nR5#{`FNvs|}k`W^H1ISYE;9c9L4(WNBpsv&)H5RaqRL zAtxGeI&4Ii{TSV~GMq`Y9_Bm72t7w3|vX0K~X0p-v*8)~)8Z`IykX&0J zo>o9pP$WA;dxx+XAsS85&^t(TqX!+C*1m3}e43ml)7{&^+}I3)*+G4cm9cxXH1~E< zRpG!tH%l@vViXIciy{__5k*n9w{&zJ645A%!HiLsQ1V$GUcE^ytst9ZDCQWu`Z*I5 zOXvvXvw138d+6+}rI<;P)ol#z9mHZ1$*0rABXI;>A(=HWa$px0qll-zk(_Uim};ZG z&P`}(p6egoAfC*SN@uC>?xeE90h&%Bn;{mCA)t~-!{CA4*sT(pWTLCDiG}f!478Zb zVXJASf1nkum?fDN7}z_4(`vwI^U&VaOe!2jDJmqR(NZB!KE>4iY1;Q3VW_KymGKGE zcsO6C@dvNcQsbbI zj4^e8hOPt0>29!*Rs_0-dr^}~3X1l1QzFmeUmzrNB>&@#A`xGkx>Y z++uNN8H^TI#~w2AXog@kkC2aY`}2|xJGQZj-(M5EE{VjNcbvX<~|{)hKE#bYeoy2eH*1DZlO5XWqk2yF(4Y=%(< znOro)+DaH%(z*B1RkFrv_Kwzb?Zca>k_jyp=GuohDC!yu6SD|rE30E;Jib3kAd*AO z$GLNT9FxO^!)eEAx02ZO5z9a}u*&^gW2`T&u(r6u%FGhcXqtQ`Lq4w{hyvMIoLo^v z%_ay1ql5zqOh%E7r4{_XAc1v1!OczPr`Ir9WTqcJCYs2yaQ6QKndS z@JWP2WO6DZ3c-yKfxkFOJsXP+e+e~;ozZM~E-OzDYb))#HP3?(4b7Vt?F&{PFYFK73+*cFn9 zBnWJ)O+*QF6;;)r0?L$ZiKJ}^(-s#)RdzsowzJt1x~^^mpMKNUOF`54R&2e#XpgkT zL_sfAmFb!WqPWe-_{{_8Wf;~IP}bJ&#296!3qq!1*rF(aH zIJQ(AodbJZ?HWegG`4NqcGB3k8rx=LTaBGGcG9@9&57MkY&AIhJO5y=xn}QqpZ9+5 zwQT<&UoB`E&tYQaEws{8<;xeA)z(%swaLPjYU1lFFOk~opE~qsx7ja+IXo@STvs}f z9DpR={$UOLY?b%Y)Fe}<{XF4-OyBKU{4P2!+R_FnLP*gSzj^pLgDgUF$ZGQ84U`bF z)kt@5&FXEwnQPPZR>E2S@#m$dPr8F>lyTLcp^Ac_uE+$&qhyrY+rQhG&^ZWQn&kV5 zCiOPv?#3+r&v=%WZ-|8Jk%m*~Q8E>P>O|3ZTM7%;%e)g zMugm7il#|VR%x-HJIN-b=v>)=1us1#SS)M^U;dQtA%AQ3 z=6nN^9EuW6!?4hyp&-r)Er{1JpVxyDq|pft#LCVPlz2|*nFi!imXQ!*tDF)gcuwJ| z=Dgx{g(fb%$>HSV5_R?rnrfkELLCK5Czy%EwI&TdxVYAq$9uS-khZOQIaKO%o6E0x zP3CH^@uxri`)OQhTpa&MRDQbQhlj;EKa4I^lI`(Kivz zTz_5r+Dp8BX78w=iqpqvp-oSy%}5vDzlaYPfivJpycLR^LU0z z$X{13)BNheeLdjCGB2_wzU&$4rRZYAD#lcB^du4*=!o^x{h_ya)Ay$Ci$66L zKfP;Bw7YpLJthqiW(MW^7zeaGsEB7>_q&3AgUq&SlxC8!4S@NH%HA|zF(@MQ{dE=> ze8Vcj9a`Z>p8zJa0xKNo!XrH911d85g> zU^0Keme42ONrSK8o`5_L+L90^rb=Z>yZR9#s;SPQR2|mbi7h*4Lqb$k+Zv%kDfe#Y zt9TN1*5{8`Pi#4#xpz1@s^fE&l>7JajsYhb{gm;aTJCc{!%~-G=>c5IIAT*tio8>< znVTiDvLO;I`m!c7e#E0IP=|Vn^j*s}F8!GDdAL0P-Pa;cu30_+=7+A%S#;`_Q1*kW zZkD&UL~(lEd5>$GAT&$VTC==4<@X4sKy1yvzF@}c44!>+fPkwP*B*^li8e!(s1*{7SkGm6XcCXmPvybz5mnc zoA>1>cApfR@P|FJ^$j~&N%FC6--P_X$Mg4%YI_NK>+Km++@5)Y1O3gT&Tw?t*xc>u zZZ3!q346T&Z z{3;pV_?)YSH-5ag9Q5(Wpz{}jz9f>B5x>Ii zF6^HIk?&)xKH+n0>ph&S8$a^&sELu$Vk_i>RAVqXv9!wqHrO`Ls(=wAf6yWQr|qeP zjQuVGrOk$4TF}3b*LKcTKGq+#yoTJ2S`-%%+zAD^1J)i33u>+Bm?bv&NM9(!rwPO0|2ca-{5mZ%QcHCjG?^2Kdl zQ5>^Gd%JEGe=nSX^dQsP-s#T`Nw=tBJc5`uY@L&%2ogey!la3agM-K4XHK5&_oU&Y ztB2Hgt$tl$;{9f00BM;9ZD3zeQVD}`*aQVXEO@Gp@$^L&xS7tf{tj;R ziFqc6?#c{)#~-;+N|B`k>1>;P^N-Pyk_1k7*VhiVo$I)~Etwho3>~=%Tcrp3+a?~q z8TZq;*c6)|hw#Pi(Cw0g?cM$RPryQG$nFa@f&3)nRDBVczc))Z!=H_inBZ9>t`a7- zj2ru=Mkx*hwv6`hrgSjSk{X-tINeFXv-Fh8d8TZ2mhJJGrUnZAP+hg0SSfY38mP0~ zE>60i;SV{o`5&n#m?w=#@O0dQm;iqob#?s2I=w=Z1Uq{FVcl=WN=(f$8gbwVJ*cYR zfduuEw_anT(e6c7{#sUxC?=YwxJJkw1Ll>F&G|v3q(F_wm1D9$^CgCwz%n+gu3u?| z+G^|wVQ3%E7QcF|%x%p<>lH!%5Grdz59K-?k675K#I2#Vg-R8m?v5=?Yn z)HxAaU6t{W&kgK<)m6#mGV9x@Vn8Zz3t%^Y{Hb_elRWrl(ZjDzziVTgR z)Q(XyFUpTC+0f~%2iep?PMwoCu`Xv`5?uw)BA+-CHRLuXMG+lOMZBFieBe?jKLUWr zW!#ZH_j2;d>ND2AwX8R+D@gdCLKf>1GQ&WJN(}T(JRR_zEn|&|CpX&hjMK53@{h$Q zUaIW~DA@Jj5D|qA$1K~5qe>d2r~lgYM5TN%?3JMU?6{L9#yZA%?|I;nCD!J8a*s2{ zB4X~!{GD-db5Qndo8BHfI_6jlAHN%jq#lKz;)=_%_x2pC=rs3q*4ccHD0`)@0&318Kbm?|Y)~5E%VF54Q8>&jXD#_{cAHN=>y3M7hBz1ORlQk(L zP%f!@+J3(YEUwX4jX|!$4zvHTSIV`_D+|{|^u!s2c`#j6GkCinHJ{c*Ol)C$AJ-hQ zd_^SMN<`P$YkqVzu>+SkgNAE?bx_=Ke-2r6!-~$X#4SJmPr+j+f_!dRp`nU`uTS{n z-6My~v-3SnGGNkvnHQdA(!Vm)48)OB3$N_>5Fqbp%OB&_?gfC55 zTV#Nm!!0`2wO8x?0@XvlfUM~SnVbB#xz-fU+OCY{XUPSh=>E4Z)nbC5E4C3{_f7A+ z#2gHGfdBJw21c89jQORr>gg`;bctIVV=Yc~XGN45|M2~f7u$;G0_gB?n}%l%QP>A- zwzQcO<+6C8%K4yY_SZd#>2;41V75M2YI0|0T>mp8g{RGydA!*A5yPgX9kOP(@%OaG zbP-3qScdk8q&$>vY}6Sar-ZFr`Y%)J9?lUp4Z!F0-x7w8W4M8^vMOiO$19xgMwO5E z0kWhaRYS4_QM47NhGd10a~gf~z(O%fXmdpJo+o#P zRwbCYxOIE@4Ml_|RsoO*&=SCf7jt@gm~z3_C@3}K$<5DkNc8BN+c-k)44~)apdoVe zM6wV|uW(6lO-6_e7yX$jiqw{YXHLikMb@3rf+4pPgr$Hx<$`>3U)S_}icuTSD#kh~ z84o z(mN&uy3dUUa&e?4Nfz#!bgkD2zbQh2cr{~#R#qlOwDD^Cg(YKR2q_8(1664AygK5= zvpR|q$cqa^T(YCWG=yA z*U42og%3LUn&ygt6iPuT;P&Y&KR|Fe(Jn<$u4}D31>QWijo+I)G7n&Ky#G1vloh7G zlvyXS$*3@9q(n%S#Hlf6qyfQakqYCrhbOH$q7)uyx+zGe*c~z0)P;stPc#Jycfz*Z zRCP-!`vex(9N0T9rk8Y=d+-?cFXCL0Kr5;tPcg@n4HNIxc|WgpxwbEUUquBNP$is# zZ7uTpA6*>1hfd03#-CKV_bQ!}l>z^2vvR;OU3@y}e$^Fum5*uTA;!Dpvzb7U{zq|~ zqW3=diJ<4Qz2$l%sy1xJ$>1s|f*a1%MvUn&*7yTAB+8z>u*!rpDoP5&QkN7zy2#d~ zG5xL&)Gg&<##L#Oq@X005-(GnK#EuNgT~lDWz&Bh`*b`Y1yQQIH60fT-I{)0v|*?M zwSbn~b9<*2b#mD+Tq8t-KGGzq5i|vG=IhT5SaPOx9H>GLw5csUP>;}|R8TD-4TtyL zi;os5(eY5_#x}iA zRC??`KUj?VuwU%zs!sOhCuK8q1^Mrn$s;0h?-?6a_{@qa8S}>_B_yR9X}ua8S{IC?E2KLpF-h5Jwu<9Ato8 z{HObwzz9K*Gn(wUsq?b)-Kz^S5eZpK@A&U=4q$H{z~UoqX$@!LD5u`mCr=^75< z`9Fg+mV=Rd(Lr z42)@HT{aZ@RmE}0+JsD_4O%*5%s~vwds$X*K`VykpbYrqR=3wU$0dEa-<67g|Fr^*y7D3yN+eFA{DTkCtpK0$0}tf#-sI;dq?d zUs$?*!j7(?Tz#N`guPLBZt}&s1Qa)HDzPXLF`IiZHI7WYlfS8ISWPj8{g5j#*{JSE zD-jCNK*oV*_hRoa0uIQlFmr4je|v^(rq>z1i!9Me=ymC9ED+;^YAw`liJGTFqTbKO zUyNmOJ0D3b%zr{jCh`3kU*Prqq^i#A3C48`jDEH`IYE^1NZU zCa6jHw>20vnCvD^JGDiQDdT$bZ>Ok!=68jNzC>Fm@6CWuHhp~ezjmj_KU1>Z$DI9K zB9ro;DRVM0{@FGxd$?Yiu<%J9&Rr`y&i+KaBrrX=(1h?|$O{edPQh;ueD9zNu?ZfK6oT5E_#xE#p6kJmA_4VF3;08eSyBiMbfu^FH|Ir=C681SKeLpFy z-!(AIL4k4ArMf?w(0DV+jNZwQ(!SjCRaVVZJTHrrcD{yOUi}jEg zPOA^bv$og^g<)j;1D6jTC56*5G+Ot?UyIV2^FZ=b{PL&ogQ$~OWdFP%Fs!jHn!kN) zI&36b+vvM^g6WWSU2!UXW}LyI^5PUfP0*bXEK8%e^xB?#{ei>j(X@yU*2mKq;?K2% zU2AUkVQUiT!a4mWoFU~Y%940~tkc277^L%aD+&R2RM2n+(|O(Rk(s2a#}l*LB{s99 zZeHbh_+V38$JG2@Xbnng;eTQ2qR=2xwJ98%9j_>}QRxP2K4JfVZVBC`+?hU8||<(@UzrsALC!$e{Dhn;j;0~UQMmo6g6(&Kipy4;e)2;SMRuc6$qyQ zr03t%7>VUrjN`B zpBSR6KT-n@qYA-%9r8@3g|&IW5-(grsv}#=!ok%MHEd>ev?oMep*4p(7RCddj))dI z$eTdWV90~VkvRuXL1c#5MB-*NMO9VfubyssH3svX0Cyy=#l^w#7=FJbrCRrMslzFJDiFDzvz*dQP zf^7)Tr)$1o{FzQ_LD!73bZ4Kg$~)gY_-9-a;~{H~VCMgT`X4lXBcLt$@C|YKAeQ7Z zK47!0H)^C>#5aF;f*zznoHH3ZtTs@FUo0OtOT*{x`b3wcZYYn?SlI;$&yAcX{JXIk zMwQ*C+oM(I3>mri$pG*0&b(fCRr8owkTOLxNuuG`Cg#Ac!pso()TX?t7ruE|!6AUF zzYOXGCHGtDR)Q0Ul;I9)=1#Xn`0Di7wxNRm6Iyh4@ZkbDak!O1y0|7-*ry8Ot9faZzd}v1o%{liA{`;k7W3ebZgytUrc-m7ptMBoL ziBw5hbNJ9cJO@XtcaDAo0{)^GNosN}C(mjVUvtP=Rr!R0xeeWS+V4PVg%)2~#;6Op3CpUfXw`bAA;P&wtSK96iZsiDUsw%k@Ly;w=9 zq_CwFITG;XOy}`oIR2&Np99~G1LZd6)!jLL=6dC%%jn|*ZrBQpeZI%YxZ7l)yzd#l z&Lipx57xGC@4bieaS{nm=bZ_#%GggQzT6l3wIJW28)j2vY0jrX@J)~O9{h9Gp)}p2 zoc`(uwyzf*9Qxb%Ri=^_T&&=wjpSzLCgaId8}C`?@0>F;|9Mjhp>bMq;W?#8U#?+( zZkLK!w$P?bR&LcpJSPXX4xe5KyEss!;BB8ML+dLL8>Sg#3pn+SZgl}WYNFIx^S1ASUPH>CVS+>qQnx1tsfu)k0O-^r47 zQdDT=W{ci9d9(_v>BH?4+}XpcFkvOJ9`gBP2I-4su{(WFvPcTP;>gX>$}7@zJwANT zrpZ8G3wT9!>4n(EjI%FD+lDR1Iu{@5iC*MP5Hlr>Wp zfbNN6mVt#uH2O7EX(m)y8MeBLMz~D4wj*mYsXUrIr~!f$4<2gPycY>YjW{#(HS}Y! z6dGLE#K$(KLPD~rnVm_U`s$`lU@JH_A@|8C;7htN$D}#}Cada2GsYE+ipOVT3#w;{ z9~$M~r}f3u-KaKVV~3jO)<7Y;v`w5sDA*{Lu3;szlqKOYt~~urA~DP=%bOvi@Cb{s zI^CL_ty-zEkh;1^>N}vAY1m2iW@~Jedk>=(%VSv_$?UETT8VaDeI&J&HHnUm-wSu- z9eqbSkC*K3M4OsiODn&0vW$N3Iv17hkJYh<*~iInzCG^92MO|Zl@z;hyI)0q47&M0 zG<#)b>5$&*`yGKvH@uIG-;%O;f<~VFam7%tmX=fvErAXH;9~3Yvg)j-WBlx4dByb9 z=?)+G2WX=JACjQw`j6ZilcM5HG2XPongvC{?6Th4WK;5~n|Lcvc+7vEWnVSiP zff!S!;eL#z_AAKyeF5)cH?}}OQ)Z6`*ObFWs{6s6P0SE0^*Cp*z+-%XgZ}M|$C78Y zL`Z7MVp-_%6O2o@!5ukt->i!TKGc73MCT@ujxV*gn=+2*>TM33R2LV`Ouy{&_eMzB zH%;u{kz?0KinB*A?@{I{sBf*u)tKvK=~JsORXR8f)0^w$?yNOWryyEp`>r?QQ=tbd zuB;4bv~#9xZ&TLdTc0})liEURYpYH>Rxubp;bg*wp+B9Ru+h`K219by)_UhjapPo0 zSrv6ur<=OxEH9s18Jq+`lOgJE+!@kN6OYNxCm|X`$HIv>Pg`8r z7G=vT{W;0$*>=zXONoKEx$MEot(7E8>9V{uzqu{r6CSIPE0Y_HEc%NVgR=v64A@s@Jf|Syyv;Xue3fXyU{RtFcY>q zu<_t1^urpSU$FWIwO6@Btzl^gR+tv1FL%2J1dg^81c($ z2NBGhNk~)&fd3$6C5t_rqna~Ebh7nId%JSptdvM4xRdC+p2k0(gxZL= zrK-RO0wTX;R!wlSRH7PjjLmoceo9wLn%wy14Cv4Tm?T2rW&%F2d?WugRH*VTy||L#Bj1(16(ou9y0z37kOevO1I3Yz)3dH?0y&dKsd?6BL+SSQ8t9TWWVl?oOW|Kj8}#Y zG4at=zZUhSphUYjpZhyx?R5H3i0Vm~-W*n5c?L8hH_dXo5};VeZAGkf4|G~~V5#2U z?Ko)RN>7cl!sGXdn@Ql`?}z0&XM(MgQn$kZTjTbh9K15n==Qqh#LbVszqM}m3Cl`K zvjC7lv+MUSF~_dVAAfz$ptHkcD*ctfiz z3SM3el`7vu`W0sXk}AoPp~k7N8qOXcVkVns%jvFZQCBN|c)7Nr$4^cn=nmA>Te zM4?-w*9ei(_V%Q>(>mShf{*LEmURYXn6p7hW3r?B)d2EWj9g#e{8rL{>z@qaosf4SxZiLXG#QzcA5mF;`U9|IYvE=U=u)IAB+FUM2b)q$QsxhU z-GIIa(6>)!biP|g0Ijbq8z z|DMzZK^$tV3W)00B9(Ndtjtg+81PC_H-uB?Bq>^Uq7aod_$d9l@&gK0+XnC~j zHmmBG3C1HT>FLVbYqS(ZG9(mU8|_Us(^%DTIb1@kX;PAu7Z_?^gQo;kpoa)$>XG9n zN!^T>!MOmxtg|OXJu{Kr_2rXNWESuuQ(4^{YW$;Y!Xs;4CC-jdk;#&0j%iL|2s8Q@ z>>xf{@#}V;>-8P>+8RB;Oa4o+KjYAJqSuHd`|!#>I+Wo6ZLHshbapuz?i8W-J0lVo zb4=Cmwc%I|KFUtzywZ}gvn>3>0ljjeb zewQH?cMb>zB&qutkIU>HUhwlLd(R|2a<+TIv36)nrfBmfL$(~LvsO_KwLqylE|!{ktjZb~vP$UN*GOXOMOLSI+$?_b<0EeXR>LtXi=ZcL_Z(|M;T zU?ES~#KEUl6(ZG?G0CqAKp8Ec{_Nc4qBjWVIFYm%vkCI`n?gcGDy?m{l%^kyo_neZ zXDsR8C@F?sR~BEg!DCCm&U31Wq$1XG7v`l zTWTamHK5I`oBvidQ4@u|TH?}(Hfqbo>gPqMuS><Z26_Ii2$mZ zpG~w5O?~5p=jm9L*ug3t=F(6CU0DKOT(a*AW+?|~{ zy1xQL(+g+zC=56KFX1IaVY`ly?9jAVT@w9YiS{;C%y?%`*{@avhbdEuv#4>DE~}jCM;SVXIus3U zVzgXD2Uuobk7IQrUsN=7{H`TP6>L_2tsoJwPg1*adffYG9wPO`e!B<9 zP$4~zXt?;8f=5MAJ(?oeb#y6e5XngbV9@h$46C%quW9wzRQCGzuW+!v3%ln#Z!eL4J-|VnTKBw}eEeY0!OpJAK#o59 z9xn%;iIXX^qC$yDtS$+eZr5?zRQQ%D z{X9)P3``sO$rw#5;L0vb5BBQQ8Flh#T9HR)DOPE6px2d(HkQrsyD>`?a_?omBIzL& zWHTwan8`e$#;es)fG8f1Sfp<1IH~ygm)Bo7Ogu)SN+v*>V!oA2ElXO1uX-{*LosC> ztH4`F?q;TEPR^@}avGyy&0$O}+9*?rk`7aYGPIilv^eJxJbJvYrVEX+9M5G=5uBp6 zZZh!40~IM;m8#`oJNkl$)I?}#t*WYC9}~hPYH(>D);Ef8pOM?p^9qJ zZ9%M-%$nUVW!RIm3~EimH+K>vJJa~~$GBL*;wzIWS$Gh`O&Mb-X@)ly)y)yk>8rKD zSqh^3Qa{VURPRYwxD|})dsf(z-r~n%Y~LgMzlT;-9o>@iF}j&PXTq;!N;A6$Hz>wM zC1r8(>7QVXyn`_+q{qVBUD_XVkbca*^*>(Wt9k4D{&da(i}1_Y+T|^+2IoSebxp=ko_{iwPJv1bi2+&Cu~Bjc z%xWy}U7NJ$j9a-WJAg{*@dK=87LCY~ho-AVghy|-{*}f+C-k`E@t|q71Lp3*%w%=4 zfjtfIXPX9>YZ#W~Ej_Y&c4-kO{g0*tYLa4$6l$|duj!^!^str?@D zLh7o-riD=rc9wr#6bP8Wrz|m2`lA>_tT`!S`Ky{1#RZPmw;ZRa;mUsQU->rb#~->u zDQF>t7_m#M#7c6`4?JJ?DUdZt-S+|&V~xo-0(G5<(_eRa&Gxg3MSQkC*Z*kNvW=-y z$ITjat}}c@nbb5m?Hy4ZGviIg%HDQWXYy_Syx&JJfYH<+A;g-}Wv5`;|o zpmiu!k(gTF{d*7NtHi1{@Jn#pTfTj=$FG*xS5{bqjzx0nfDuy%sEm#UD4{=9X=w#> z8b831o4p^bweiI8;#4q7!Qxg0B8?`f1eFPMbFHt_58%o+B}DKPiipxS7Ltpjz4a zq&^=xxTj)d&dbk{P`+-Dt{Df#zhtvFu^}wqFaJ7|Dy96@ZA71^T9a>!OngC;IqtH1 zCRRm(?a$0Hl1(PnO#b#3Zw~K#B_VvUa)II+FH$}IDkDo~W z>lGh24Y3L9n0p-Z!n*2LaUB@3-}~+%YNUDpubDJ5<3Yk1Ti_kF5+kB}XY)Vzh;)8W znQ(bqby62$|9y847a`r*!P7f17wqV+xOeQdZAzgwWXqN$TMicVjA@+rBy5`TbMl{< zzs!%cPZn_M4Xv-wEYnB9B_ulT>3d~JYx4EiN{{CfA+w1|$VML7avClut3I`rgSY9a3tv1%Dp<+WaA`8$;-h`7M zXxEW$7!{VaCcqq27iRvVuw|b9a~Uucec2=RwKh|q<2LN^esO(kARXnh=z;^RqmnCq{h

    u$CD7=;wy5o6k@DVjtWbkL4ZL+oMkh_(vInB;79Q zy=EmX?(J&Ft^1WfO9seT-JG)j6&P5!S|Uke*qk~X_tUIKQQs`N-;;;``Nh*ab$o^y zD_>^T1hW1P(>^kejVTcd$Aq`nAMb~WL~U>R1)=@2fmp%h@t2HK-SyQ4XYulDt-F%p zYD4-mhv!ccRZi7ajYQRqMuFPD>#O?l;P3M%qH^Khr!3nhHhdDdyWifgK5j79v(GT0 zC`xc<*?8wd`pu;jYi{5D+HJOWLa%K^m9itc+TdVhSGbF*T7K%Vf0l`C`;n_NV8Zs9 zDrs>e=vn@7T?#OzPWR+N{`nI%T3qweoa+aM++aAxyrnNTAF|GH*48YsS8(woA6pv_ zgd`5mov5w`b>8CHxnzB<80GbLhty50w5b9bHXN?+jKF^i1-yWBz~?zlY#{eb(@LWH zoH{LK8W8jEvEACCu7A(t8)ufYAE3Fyc7BXoFtRY2B&ldB$>CthA35-x-`W^{Yu_eA zU3B&_JPe8T4|I#p$j~D+!l6!u%N&jV#QErfrkhQX&XDyv?V-?PrJ_O?g_;eQ3RRA@ z!fU5jcF`F{I59ZKDd>B@YV=pe-5o?T&W#4#qr5MS0)7Y32mp%1;^SS(y)3G^Jd55f zrdsrM16%%G2KL zo;aYVd!&rLT zM$w|54R&iM8SsoO&yCtVt`A2C-z9XzBPWiz^}4bFM$quZ&0KSR=E#f^p=nC|FS#9O zo(&8RLBfKPwtAAtjD!B=jZ#HN-_BFrz=kKL;9Q9tHj$uPl6Qc-8VlJrSW2<+y1(;A zW$^}MZFd(~d#tIBwE$+!Mg~cQxie1=9Qw;j;YGXNF$*^{^D#;F!cL zFeNJE=?~Xc^Z8HMkkMI%;|gO36d3KJ#f#K8RGaQ z=($iSc+{FsTge-IPU~sR=NK8h>zP({ELCHPXQ;Vmpd;1d*DwbE9#0xm1#B?05|E~k z2{!i8N3kDyEL&&c7MVa)S0L6;%J5>2nxN8RLYc=La(!9$z%n;OV}~LVDiGt)NxFv^ zH}^~|GhAMt9G9o5bn7p8?|Y}vRU|D~Ws{@Vl`QS-jS&|V-!>RO!iji#?t88;jVX(4 zsPBXi&9K-Y>9_V$RlT4*MA@gU|IJusU;E6)Px!NgQK~uIpE>V(s zm!KmU*VWe06q=HSNy?BG%__7p54!~OJsjy@!5z#zwbN~^=Ng5Gv0xj&%#9&}WDgd&1 zkA!Xv%=T&C>5J=2F`?_F)OmoEG`B7tL;IdF!uKmoKhiYLt#mF6iMBR6`jh;gb#2{B zAtRR9$A8PDhCwV# z8&zWFGZkEE1BH;k+Qq}K1+za4&YD{Db2_r-X7SVUSE-K9iO*bm2E+jN@_c%3TcRDt z0-a?18uHsHPnz^X*A_a|+Th+r8yLgFe-GcQwzHSEW0A8KatfWK4`)BsHkHM@5Po1a z^uMTW8ufwC@9sFCnbG_QUcso5uClxv6z~=u0#cPPTeQ~>$5uQ)tsP3rfxGHZ8$vFm zrR5AVTfPyIAQ|0N*2t0dAHAOOkr<~-o-O3&#Wi*InY<_hc@yfZ6It?+aMQ|-4fti9 zb?DLL%_x7axLWsFF@N-mC>n9h&FLwddt@*{> zs9@&~qUN@+_Ao@u9<9TbQCt@|^JWezK&!`xsm6VymZYr};UC2jZ*a0}jzp55QEl*X zf$cdl$JhN##yaV~j4Lg2mWz|qVW)Gk&gy0CcV_&poqe|_#{A)SG}pm7vHTzx%+C#3 zJEh<;n~6MX-}T8Ko0xpQEd(usB(M7vBh1FX?UR18@VK16bGJ=Ek3dhX=^)%rkOYya zTW4=XGyco>3vMi;7k(Qiz^!4HQYINGrt0uH61M97;HQX34|02S1CEN+%3Pz!T>HkL zA+YCMkhO><)nJ6tbYhTeaKVSjdjTU=?V2Go^eo9&me-Je9HZZM91EZ`3ue>3xqeSb zs!6}T*E9+iqu-RVOTJWrZ#e?WJvaU*b3hK`n*Xsk$veefi$uI10q?Ixq&R+kTu>i) z{>QvJ_UoU715Rw9Bs5~8l9jhsSlbs{3x*>fN7o+ud*yucyvSP$;U(NXu&#C1F4F%K zA&p04&-hoU$d3V4qv>Fb5=`-GLEN3gIX4~h2`thLO;-UAS(E}P{#r%E%bOAE>q~bx(c>c09BXxt}Jr zm|ot2ACq16sJVd8EuL7vp{{#@KihUy67mYms$Pu_oUQ_h8T*yr3ab;q`j4`7*iy5G zyM948W+MrA!0CA{=pFG(fTGTpg}b{iY#F4Ik#O8Ar<~nqcSWkq@uHt)Zyw4PSXL|| z(tg?q@%Pvo>Qs>lajrM7XyOc~h=#lX)sTc(|Fcy9OOFE<<_*A~@~?!RrEa&nW(>3< z8!0sh&v<9#(0$IsL}Ypy8`ynD+zM3+Zg zxlCm0f8QM{V6M(M!HrIhFCV8V@xOV{gq{sGcBA<8>H8;H2{Dbl@&&VJz2|(=n8Oqo zZY^$D#~0vg6DZ|LWT}&8$%vR*!a@`?e)&L^KJjojG`~7T+l+t!D|}8>B_~2??A;$V zA5#2e0~K1Q|ZjRA8(F)e(DLWrNXFaSVLyyqxH{WL)D57js`EIUMVQ2p1Y($V?BigEtau%2q62x(vT29sz+EwAg zN`yl8uV^v)Do)mX9C#MrXm4vSc3C_N6$QZeG#o4Bg+=FjK5zpi><@(8o+*~flue$7 zEMY{VOFNp<`R;R1*!$OQ>lPY<`NJP4bB~3hT^462@Wk4RI{unxB6Gi&+v`gH?yJ+_ zb}8js{2ZPc?eon<3WA_LXS&prIIraQ?5QW-7-gbEk;sOLd^ds{sX%x`SkWjGCKDdE znFc&ZMx77RCe@=C@lR3rH;Ixi*I0}-R@bwXrE;N<71XgA{m%k}#DN_M}1%gy)ROhJK_Gg%^nwER7 z1CJkLma#t^uo0orG?&@*m~i70N!6PApKz;nf17X@+u?`&hrpA+#DHYvCP-1KDghoH zd$wYF_#N|hED5;S?KJlwySSSQZxKQ~fl z%K=J6M2a~1zWeBq!E-m{lMG~W(0G25z=~2jWicBj*2L*aBOWI^A7GazUebs&5ZmB8 zqE*vU4^4&$2JF&KTQu2YTz+%%F2Hc0e6-kp;uuOnGX>Nt~54B}5`>xiB`A~={9@2?* z62D|W)IlWyBH|{Q#wQEhw0Zjx>!gQWt3(e-1uce~3tXNo)|_|&H4ux|_2kZ5`sxj1 zAPt!Zcl`U^FiFqAl?+XPx`*$K!R{u)Xam^h=2*_?d7i=DL#w$1eRs{S-u3K`)|N4< zwJp5_N6F=4KQN;rllT3N#KqZINI!P+?}MCDpEE>B3N@;n`qt+6E%KCcY)}vw%g7gu z3fD){uRF7MOT+gufY!FkjmPhoVP-WV<4{BTlKuSN=b!#`!x?Oy(7BBk_@T_)$j>&q z8;-;42Bx*73O20=5a&G-R{Br`c4SKams|;XghjX@Q-P#cDgKnY(=~cq-sc6$5#+~) z#s;aK%EjDHcL+0KG0oXeTv$pG2}GowO|Pvf;$mr6%S$Sn=U>MyX)(QEGQMve^wZk> zAXGB{+|AXR_Ky&#$5d?Rta9S=2L(%MC!36n0kUa}=b3#ItPcy{{h2XLLT#bKln`h*ptSUH^%-$3HwjpO*J1y^36PU~WGY^!nvz zNz8GiU*}UG@zX#q73R~eF8{D z6XFyZLpRSQetG;=hL;yjaB92;n)+02?cZw5Bh4eue)u_J$tZDU7zsd*egn4Y|MkqX zSDtyRq=;H36%5T$L-Z*!bgqFbrFH_?fvCTb{^nVvDoLEi%l{*06%q4%BxnIxbpH3W zA|=$=)fZG_*3YEajySq|elKYiA93OEB%O}>=6XV(xaSAEvAH+|VT%WSQ>4|=$N3`& z?v5~L6B)a{ji3+by@pTmL#Ocr@@0K)QIca32&}AXt3pFhIy1YxM|wtgVxF1c0X=Q7 zQUI)-XiSKrlA8z|j_c}BizMIn=W)m35=|e)18MjdK!~uoB_Wzq_V5Q`#u2fmmXfcd z@k1(q=EyY}kWtI{KB|25^Ko=fVC)O*`9wXB5<%@3?4Vom-=-{AaUROn39{=u+>r_@ z8tdTx@1Q8Jr9r)F@@;X12Tupe#M7JsPS1GDWcUNkI_Oc*C?U^;KEZz$KfY-qW3VcSnR+eQF>wP84S|GVX? zaqkljmsan5n|VP9$7iUyD*oN-a=IfI_;&f8IhfZ9?q_M~` zb80e#UB|yW2UX&!7LiQkVh8i&2DHR-Eh0|>&iy{!2i)&p!r~pi&d(c+o)`4aYJaB2 zAQQ(JFyY?6TAAzR1s>DH1G>)-7+6pa96ZJtqW`Z2&mT?8CcCi2cexjZY= zR3V+Q(W7{*0hhuj(^?y>O!mZcBCzrv%M|!*ueRt0pbWTPcA>(r^ z_Cctxg^flz{k+DD=aauR3JjPXc=l^uOq+;g{|Lt4*v0oeLe*(_sn&Gc*0-ufhI^l# z`}N$V=&W>k#Ux0ofsKRmto-7coC9P*x&BiGE0f4Uz}84`>Sx?~D+y6?#dJjdVP;m; z*Sl9rl;nwn3Ucbjj{PL=8wdq3qDuH9?DB0ln0D*^;RD{%;~)2;ZR)mhEaI7 zT(T@p=kr+yN6)*6zY*OZVxgLAEU)8XNO3IP9|1XW=ltN5q~A-7=oiW*3iEtxKESOv9zvol z<6{r6onJ(ym&K2pYY_|>q%bPwhM0aOC*~Yx`XrXEmou*w6j~s9%H{j|pHW3t-3ZNu zU-;FyUa~-*gnQio%+yLMOyLz!fSQ_EzY(Xz;c;KKIFf!+wpo#u$i&x|aMcWqBaL)T zLH>Tj!tG3>T#>fahxXdiQkpEc^u_>AK*iix0ZV~0Ca4H#{IzgPGsZf7oI`hqMSyX{ zfX0#e`7IMOoYsB8%IttDA8&WeXuDM{FEOyA0}x~~81i%s07Dk zjo*O0E^F0wzJx=ph8=iK+w>3`>P)IGOJ}GtP%5sL_&9tW#FQgK)6>CZJrAGWUC45V z!LIi4Im=d6HFX0PZD#G6jk+v_$gOuCSeXShhQBarFy+z{L7zTeg^342fbd!z2XRK_ zYWJn1@~^|7)8f->9{Mi}IxCu)sbT5dLoII-a5|Y{A|p{{72~lQlEm0iUHO{!!L-ZO)YX{u541*8OaGej!cHXR4hmNxrREJtIT`uSj$`a- z9!|LdzJ%HA!MkZ)u`&#$#fuKJj;y+x(bl&1=%^u3a%iWtdH%YCbFE-ROfm8n{lcu$ zv7%~dqBp_0@kgHNN0aH&B|wh9h1W!n-I?dyct3!nTpYdX)3y>V0Jr;LVvF;>aY(6A zq$sMYicgPZ8O1h!9DI6pi#fnyC!k55!>BSy0(8H;I)K&N#?5>RkT0W}HdG()R0U)@ zIGqwrR5_iMw)G~>yels`1^q1e=s2I+Cb$8^v^ z3c52VU#}u--oa?Y)(W)1NP8&%=?ej=qYRq`f;21u1c}#D2gaWV>WEm^^h`{0XkuPi z(S+6AzAHbT@z9sF1U)}1JHH=(`CiO%WQDi-@Ji#`WvAw*qevI^! zN-Z%i^4sQ@;AO`#n^a5-Y%LyKgzT`AlIggU2n))Hqxq%L*=0WW=M*V!&88swj3QA5*?7vD`O!Iy3%yJR?l& zj2kf%Yu3td;{e1DS39rXbSfJf_iCLjKZ=}~S;;+6)7;57R5W@c3dLfJqhdurNT}cb zA%sLS#}L%90u>F~3OxS1yo@GVXa|SUg7Ysy(3B|p>+U&|alusAx=0XwYB!Vt_bKZ#q&z%R_7}&PNipt7yFc)h=o+h`vIHt?f#&{(F zG)#<>tvtAuhLIa};7y*iz_fr?PCyQ2PL3K=W8ZN}pNN>|ZEIw2EFODV_@f@_3k0 zMK(~%4a7V-v}rS@?LvUYZZGMPuev2Yj@G~ke?&3bf-_?CY}6r@!D9XZiDs?Pgq_+> z`sQg*XB$Or)WP?6c|ydhoixM}69t^R7}s;P29zHcwi`(!OWv38tEma{fH_2i>!owx z=$o9M| z7-UNc$3ZRztkPx6Vp`GecNz(PrBTJ8(~g_`1lj&9iAv7PoQo6>@xMTJq>khlcA~eE zqva&1^Zd&T4VoR3BV#h<|ohlR!7pg8!j(>)krxLcj zKJ~x5aU#)2$1|VeXu7;?;Bd)-oUv!~&W;*B-ew`yP*7038OVAogmk0x{BomMnSaq@ zla-lC#!Pri7^N*%zZKK3h&aK}s0e5O{Oe-YLL~b$S+liO-EbGm#4j-Z-#N&s0C)!C zjD|lC-85urAWRg9MAFh@Hcf6PbPbI+Co-`K4Qi_Hxkl)4YIK-UYQG0gJgDgf_O7Q& zXj3h6jrhQNpbW)CMf^qqhl2$M_a^QO3&i;*{*bxo5oSzu83uYHyd2V0lFpFu;r&sN zBr}QRob-k$AJEQsKgxL!d~@dL!#XIfrujmx_Mev4L(g$*-qc&Y%69#MN-79rz=pG^ z21#CxVbv~b9y>p}TMl*AMgoibrgO;LGSW|ZE7@SpT3ZYEYXypl3RHzHV|F$5#5@`> zxDQqmF&c9wJGguFxLA@*1$m-3{ONXW=%*V$L1z04`2d9?R!l1X{eV$YO0+N_12E_Q zvwcdRD^%%4L)@wuiJ*8R()|BtPS14koPv|3ZB8w9U&CfZ(Ne>dDmvh*{GZ@$&a9%d z8#QP3TRom$P8K!RZ0e)7(7G1D)%zFPD~Ln|2ap)(3+-qKOMgKB#}sg!lF`5XUWOjD zx@14b&{H`aie}* zDfS6n-)+okDqSjLh?LS7L(|wMb1~wn23oYY;L${>3F^0e1GJ#RM&2E$` z_msV*xoftUbUkbyZ%HEg%;*1&-}OF-Y=s-&wfS2S-%p2Ynm2r$?955F@@8Oepr|gb zO|Q%WH`?Dxw1IJR*@Qjl2Wgp)&G&%eb5Mxs#CkV@fUu&D3iMzcOEx;m@qCQT9M@Ng zIpI8NN$+#4ITt8#tcOy$wDY%~=NGn}W`sXhd@pDTzc!Z@InxW@20nFlpG7@&%*FA- zp`jF)QFQW0s(AF8CzuwfF7H%8WS(c@n=l)Av(hUh~ zDh|^z5*wZP$z4Sfn=7JHMl)>1KKryC6#RP6n50Xl#E?9BM$+g1iK9u0J83)T1OQ}% zRY=RbrsQpyfR>~3M{wY8&D69`+}E=l2WOaZJguHK)VuRD5OG}MbX6OzLwy!+WgxLw0&a(D&q|rs8c^k^L&38n^JPZ8pcf)_nfMQb=%vD zR7=hNHiIN!;6CC;GZIYb%q)MHf<;4l&h%}Jw7GS?C~E;N;m;Yi2|rAHN++99!^-m? zyz_~m?-at$pwJeKQjM%@Q+hq4`OWaxZi76?Bxsk&JMH^RX27)Ds2GAA7b^FcarQ zmjRqI?PYmI!MA#g=&lKF{co0dlmj1d z1*?A-+3)t{)xt$e}0h+Ez{VNa-N zc^9J&I88nQcX95`*?CNuFgfd;uMtLM2AzY}gl+{r=oJ;@6qNWVs(6=;p z`StDAGTuluhJ$>!#Y?or&Ly_*-nHC$YiBpqveXbZfqo(Am0u)JYs#p5LVIZ3bevw%;0dRxE42QI6Cm8~-L5mm=8MUdVT(RZ(~K&x$;aowe}B~7cRA}_Z`X*Q2HymWtIb#^!92>X$SA>)R7C1r|WNqtK*L`A&S4R zoZPzn?kB>FtifL{gXHJbCX-TLB3Y)dAbx!GmHi$MfUUza@=%Kh#JjY*60f?s8m+|E z%-{RX;Had&9O`2TYS%sgxnBFbMQYgy4+)Rx-nw`#D>2`ef5E>tN4(MFA2&Ggn zp4#j}O+$UyCBQeq*B`kkWFpEMok?NWuowPUh!0~Kkq!~;LZm@< z)BAE?7YrDG$ji;rM^j`2fq~9*f}@CM-Gds6|L=9$BT~9sKll{PKlP9?HKBjn#&<^7pOp@;Pt7=+ zc<9f0?%UyxFgtkB-?iyffN z78aG0$4`=O)Y0DOrbV=7MnP|1m|qWHI4^GL$Sd7e*+$;pf3zVTR1K-V2+a38r!q>2 zY0?-&R8lf=T~yuHjF?q3;bI2qeJS$0gZ%6RIEW+%Udo`_-vi43m7GqbnY8$%&r?hcP*Mxugs4|vZ zV`%NJ=W!ib`jIYwrV!gNwgK|P?7JkiZXFsk8k-~Zo4dOXSHSV7^D%UdZPobA!$Rx7 z2!f8g0RBlB|{2 zeHTqO9K3BMH#XuZChYj}&A~jO>l2`YOjv@WYe`Ya3~1|lPSKI8XG@x~UojwKtY)gL zWGu;?G;#H*vaJ+XUjW@mgVaF1xF*Bo%x^eT_<99~#T4JZ%lcElvaI?d9$;arOT)@x z$T`YDMkRax>0GkA>_Yv+y>G}6uw|^l-k(xrDSrHF^0F&qb8D5UtAMcDnm}Z($b-1^oj0D ziofv18?_#^89C-?;_guP&=A2rWaXu$lf$ENu*%bHk1Vg2ZGK%j;Ts2~97GF&@sej= z-w<89K1D3V7vg31`z4HwD7nKRJyC5vYZDPqjF({_xFhEUoa@F?ty<(>dE&^;%i^QD zqyIDEjb53YfJ7`oOv*NnDf&7`-*V7pEpKq5rG>T9_(||S(-kOV-A5$Re%m zNK0TVy5Dg(b92N*19UPIrUpQHipNtxnS^U|c{b{lloYGYSPlHi`AWN_Kc3(G#ieIQ ze|Vi!*1^Yf{4yGOty>4Vqg&6A4$@z=Mxe=4%3wnCpn7<3Xg7BX_pRwOCoA`jY^*k zWurW24Mom*?$d>(-6Zo$&6mfSgaYbDb`?x$+(n~yyydSz#UB*4dwWcHS|`b5LD27u z{3QTg2F*Qj+KQp3W`r3yvQ4ybU2K1rJ(7K|=z_>O`Gw7jb&qMY$BjAe-A62E=I9OX zu?&m7CHi&HMq0wwnxzwyZ&FntSPW6}@`aR%rzv5h!HkzKz(67TTcTYAxyC(p@&G@R z?&e`95Lopp?k3elDb7%O;&JH`huSAhB_Lu*s1+(f6>a5e#)VwE4-|kzD413%tKBgT zj4W(L;>Rf6&W7!$X6sFF)0#K}3tl=DtYGj&M*gW>Br@4-``<_cGHqLlCMedNfoXJS z45Se~|Et7*pYcR8sFI!quacE{vp{(h>4nNqBoN)^(tE%b9R-u6h?dfw+qCe4pgikx z-kSsB3j5!>v)$!j@2j?xp*dF~oDvP$uvSd!vVto=5 zmt^28Ca_H!WDCry_ zc$y;V+!62S&3nR-8Ca92Gd8Fs~A&~ zW9Hw3GZx|)qB9&KJhjvwp}*GQ_l)DLOx=M~rm<3MZ|}2DQMDn9RwEW1B^G2$#A?>Gq^=>yj}?w z>Ed;dDN&M9r8BGR8B0>09uJ^EFDj=Ifw}jKBE4_w!@H1-?^-T6K7-K z^c)ncjxIloEN&eR@hyk9RzLC^7Vi$=xR_YP|ypAHn%l!eJ6WV$PX{}*m=eMBDC5vZ!i zAwc=1YpOIW%X`ebdu}X?GgSKp>>)Nqyd%8+lja%UKMkvncM5;-!ZggH}4_J%(my^ zUP(($oZfH}f01I17363gD;Aj0O4?EEj8*$@+x3-H(B8#Y1fbzUG|^I~`$}5k0iR3b zs-lg2ur7c2h>ns<5La-RPkMTLQ&+AmeJNH}FEJ|u6kJYBUy@OD(|=4%;(&fETOWiO zr9p3}OUj#SkS?J%?9ftt!0DZ{R@r#yhW6>S4Nt}h{@)Ea&5c|!)_J9 z3bpmE_{24<0HNWXCd#GDZ8{ua?ooIG)plNY5JLRDBnt~s4PGr>q3hKk+=3Ij`1|mA zX{MZpD%sn_91bpsuwqh~HmOuqlU=`dJmfhttLSW88y?y`w3HVwGiz0@#^G|Cu(AgMHWwW@!mkNi}z+WoDcbOknHq= z{HD3wng^TpI8JCoeC-k4LDA8S+s_X4Tfkn;Uet zg@uKId*4WnhW^pr{TECwQx~nTJ9mgP#zdC3c2T!Mci3?Ho6IV+=G9L7z&8Dtx|+ls zNnOCEcOI=ege0Q>HXvV!?_1DU!s$J$4@ilD{|N!I*9C=6>ut)%KbfES6&Orik0l?w z9uf=a>>X$>r|xXmpJO$_hj*6&8T;1gdx_09`LssQgSS9Lde=C?1wgc0{#^mS_6DA< z4!$Jeo^-{JXpB63G<;z3nND=y&uJQu?j)H)-QJeE8MAC+gQ!vS&bfJYG-PSV4^!C) zD^7i*>X$Z_l?>}L6q%L97xV3gmN0H^n5#9S zChziA=Q_JcEBG;^9w}!po*d*!980=}ca@Kjr5h-0^kw<{ywv-ToQH(DWxL(Mb7?!j zhp*3?Vl5A}zzOdhT0~QuU-qZ^WA)@552XRIdxTR>HX6aJRedh7H#$ z#g?p8d!IbaYDCh=f%kz_!Jog5l1n$GTu6Xzo10rRF@B80j}h={0kLQcsupYdTH77^ zhT@ujQfgS&YDK>wK0LW1dR^d_r?9z;q&IQcNJ~LR2V0+2vY!6e#=2bRGMF>T^zOz! zS9C)hgh7!cTVh)l@U>)kxG2V&-}jyV=bM9rGov zFX9yIeqT|+Pr3;Q>kFN#aWZH^B*s4DTQy0o`nRLrbfb5+1tC<3Q_`&x3iHZKo6|&D zM5iYOGJhhZEErW+8|f=4`JJ&IX^G1di5Q2ycI9+af?H$5gd#<(% z#)@bvs~Rkv1i+I6{|!Scy-t5TY@2!TBe1eGaraN0f)PS%uq5r`Wann8_MvHds#j(0 zxMZ302W?UrC`>$j1n%}|PHC-e?dhqL6c=~}zfqfkXUiM4v|27#=-Z6AnsSvN-qA$+ zd#_jsvVdMYu{q$OJ5mRLr!&=X`9eO)VlVw4K`ZfA{WsPn_4X?hMBdkavs@ngM1gug z|K6(`o>Ts42A3mv)cvE6VkcZ3F1swC>q#o?9hqbc^2B{(_^b?<6HzWmr_)f25f%Bp zgrTtY2Tm*%YXP+3wiI_}rH$ROs{&WhzlIMKHw~t6;4bfTbO~UJ&u@3V=zpO_%pi`~ z{BDGEz0~G%-^pP{OsuKpqq`mpRhDKIFiRnmaNh{{;q}5Y?&~ovctXv%Ddpj+zYqb_ zzxE|)z#^lut_SQcvqRdZq5utd-O0|L{g~{ihyQ&-71721uFM0!yG3GxSl<}r(m-nF z`{QEJGgEY*E8`@o1B+wh zN2Ka@NGE}50QL7x;7ZiMxJqbnbOO?4I6%WaXjZbnV|q$F$&lKiW{I0P>g#h>_XXI1 zUpD%`A=$^AkvwgARq~BK{fXPz=OY97CmDgSjMs~>u1W0XGV#B5usL)i^s*DUe9gqb za_0A=*0OEW1e^@v=$^NeAFpVrBE7M3p4a#NZ(hI@pF|lpl$nw!_BZ#EMyw1%xS9X+ zWCzFFj$dXh=byjVoeM>%qK`RVKG$OnW{D=Y{?WnIFJC(y{sO^p($(F=dGZgkku=1i zvo9zHJ+g5MV}A*D>w-RvS5S`grXJ8frW1j`S2MPi zUPR$nY833B*c(PnUk(9UqXClBvBv!|z0JX=jO4f;aooJii7WTmDu;<6Oz~yiF0Y)d zLw3tWqnjv%{fLuGSGp8RM#eZg!X z`VUkGJV>W?V~tflrJ{gci&#Ycjy0f?xU~}!(r55$t)R!@?AGPysz}h!Ohj2Sd$tXeI4Lg7vdrF1f`tenYGO`=4ZD% z@;?<-Rd^i@Axpp$(dJsLtJBo+QwTN2;mDjOK&8sLMV6XJ!izx+g~)naW&mAecnv@Wx@T{Tl?r)P=@C<5X@)zmqVN6eL)(Txc- z+R%t;Q$CSK0So$eD=P>?*_-vM#gIiZVqJYBuCjTXULq*z&l*&LalwF=PTXpt%R>p% z0S}ie?bP`!nd;nzxMNmY6w|UCJziILb`lx1fS!SxdDqec6_a=ZNeG(XlH-zb6(jA z@)GR}9@LPux2M)RbRrA9V?Z2?9kUMr&=PPG&BxPXj_YC}v$ZaVeB|nlS{&RPGaGGZ z!kIT*E`KS^I0q`64F|V=OIzCuJZ$w#@LQKR0THkD6#6bHX-CwkY0Ho#emE5#NovDd z5$^$g3K>HUTz%2W-+6vGabzfA^!CJ*v1sJeJglS6Vj!XotJiy&XyztSj;I6My>W<9SwU7xB_e2fF^4$^YspCbPet20dKI&Gt+4qbRd$_NIsETIS!IAsG5kMLl>qo&(zM!X zmZSxxze2+Dls!>@s?2SEMC|7EJMI=bBMOepcT7yH$;0wK4v@xY1mt0%t#z}Qaw9m~g6O{`^q(t(w^@sLfR z%swP|E7Y6-S#4=4#G6>?#EHe)Pbhk|5DV#$lGu#P_Fy1M{AO2aFFI@J2<40cuuG24 z-(e|mDOk9{a<|0IwT%6-tKZq3?ndBrcv&Dwg<%qpn=_~W1EJX0_(HD%Y0_DX zPoU0+OCA+{(ldCQQu)}qS3-h3Lc96`T6rJ6AFp{_lL1B`9ylh>ghJ8d3zt(xTwSM+ zA})`nPDW7~m7)r3u}ztza+m|$7bWBpnFmvKY-VR#0hfDveV4eCEXVmT$Q#7QWBcu@ zOIvczUR>`{U#;+mfAcICS;*i|#Cg6Tp#D61M+#JaoAF&xSedP*^EIKfCb zL`_tHq+^phrxoVk7YQ*FXF7r$#zZ!MYJq%aO_7G4!b8^oU&|2y+!I(N8SsvEtOdAZ zSRghXaD>-0QcOgKgx~NWf=y*ST9(O-wQB2kD{Xh5G9tWRRJN(or>@r{;!Rv{hR}_7 zX4rxT_1QI`ZZBKyMRTMV6kJ%97z$6RtXhH{7{e+&zNMJAy`Ii)mC}!oCx2E`;~+w% zmu~xQ?0J#mz-^ie8{MV&vHSMJ5qxJBTt!@fEnh<*m1n?fyq1DLLi%qGKUrTeli3Q! zkfwl%VggQ)HVa_Ht1JDX!HuX69?^yhs>%k|p>xuRawZ7@MfQR`0nj93egrjGht?nO z$E7j8ka%V9+E{-ydq*Mb`1Y$Bu@@enx>OSG%&GBWWmcWf7&KFO)L{;2b0!k$6giMI z?!)YMbB+r0=4^4TajRPbMXOkFlmV^E!Y>L`9;5$wX~{UdaR%%Rpk;Grk_9GB0d;{6 z2LWp32vrxo8E;M!DA=DjhJuiNoNCp*ymW|z;2y^ec>U#mL9Ci>y1tFri9?R*8Mi~E z?)wM5?a)W(W1f|%WTjF;f0c#gMHP5cfT&Op0t5V_NkXl0}mlNcGI*ZQ7L6MjXsVxCHPfG6;T2Au{&qjrfuI< zPj1Z2g7b-uZq)tPT#^zkO7gejKL|E}y!^LByaO+K$ajcCY35|G!_a;c2I9}Ya^-}F zH$(q#ksakk9ScZ*Kv3)7hakPBr(4H;zk|X=ZVt$7)s`PjG>yrH+U<;aSnmucc>CYo$38KaLlqzt52D|-YDa^xn`eB9sSi5>Rks_WI__Us;U20 zN%RjI{_!G#)f5j`N`^2(ncUQ(HsaR=r{3l$BtyAG>J7&NUuK4^2;(|M79Z1+uw?FA z`B^Ip`OZofG!8Z;5*$9AaSyOP%&MEF4;WKn_iW7h*1!Y3?j zn!xLbY(LErre7wcObxD-9api_0Ikoj*JEr&Fw2NCEjHC5#gJUzb54DW68A3k8D4^eQ)zo?wjJ!0988hbGdcj-=hlpL zsFwlC>deYB?^oUKi_wb8wPrPTN1ga+Euum)MspklC^XY8`p@s15MvBj&yAdiyOr*u zgWH3fFcD`}h{@-47;Y`LlQ*x4U#V2E(g%-TMxT{sY{h)ysb83Uy#3Ht)n-O?X0Z*k zB)t}IUcJzp?p*Ty)udBFC}8L=ZNV-!2pnLbP-0SpRkFf{BC67jm1S215m2VEe4{Yq z&LQ*hCWmM7A^`(}&F_CG3yTMojijr$Zc@uhQ=Z`2 z0@Dv&e8m0&3&H;7QCcs;F(ZBzXBE<5;H?7><_~m2;lQWE37p4M!5G2PbF26`U-~+x z`~>t)B%Op@Qla73)L;nPR7(R7*uml@6VzO18Glad!JO@?zm-r5h@{m9Da)P?8!mQO z8+J_!utQ67flj`(UY8Po;=$ut!@{=B(QrY7S!0)g=M%G*viC#7_VE=me&zkqk3K>Z zH=6IOhLEb^e>h0&-meJe$Ka@$E$F7Cl4CT?oK8s;KKy<+GIPq&x|)>Sz<2)Qa`g)= z-FV$Ur_3@J&Qh2Tf5o^$ijj2XZty=2Q%EtU`G~vF>R!P7uGj2bm*J|D@F$L*&D`&a zQSR?GbasYgf2#LJ$c?~?@L9R~Bj@Qp-qu-LbDyjlj3{#%L+VZ4wXP<}`$OcW-7b3{ zemuB1sj4=6Vp(A3@nZdiv()$0TUD;KJ8> z``quF_v^O^t$W;Beqg+?SL({oG5#{TvF`Ii6zPJ{IS0z`?=IZ&3i$xsc;O%Y zcbRl$QB4Z?Huf%D9*0Pj{v;wCfS@NSsuNUU#2>Dh1d#l{>#obm^hPX3cs(7Y*rFK- zD@c}Qp$FdM4Ij^PtWi*1aXD?~!>X|oIu`F5XI4DFO2a*Culj#{8gdHP(9q4#-d~BC zof8z9P7Wg8-3bJMlbKmt|5bgL#GCS*>3Qe03};^6dM79<&MO&v0XBG77i?l6!(RnT zE5M0y(};YxOgeoT>XGP_9dBMyEK|aZzv`+}w;O%SJVVu`$rV zAk~kwHeRJUZyfNY%ZTn+TiGGT%#L&vEiQYYD(I2Y&5}~!D(zO$kJ+>>tS(R7?_sUJ z>g+?@-4&JDL`#PJ#6D2Z-cdFql&{A=CJTNoYs5LVy522j5NvJ-uU`D59qSAAiC{Kaj1oC3zp3hK?9lIT@Ccqtl_nGRO&hIHiUH-l{3CH^M=6|5^ts?as1 z!Acad54I*ki^mmK{W118lX)A;>_?L;sj z|44s%c{10X-lD~VOCUK31Fk#Ij50xQUKU)k5%3{7I+ii#?xMZ8u)ZxXNU&?G8QldJ zCt?U%!i%)V`Gpf*oEHEe(U=8mmW1Qc8_Lb+JqJsjC;2A33ep?->?ebIyYCi>hwrh- zdjSYCJ3CT18}fE;IIDWTm0HW$gN2=#I`c@v``l!y@z9F(3|8vkTp|1g{T~eLNe&FA znX|4Rp$XoNZm)tF5cREjNo#AC5rgnrEA#C^@mks>e_lz(^v}yNAWp28_;@o8gvV22 zNUbs^tA16(EtWy%;X;QfW%f9RN~z81Wm)x5SiLPyvp{ka%pYkpxx6B3co$tuZitkO z4kz)PuLIgM)CR4a{?F9+f zmnHa#D$GCF(TZfLk^&cXkK29Eu=4Tt0!ri*#p(MqX;SloQ5X$U*kJh zwf2(e2hS-^93ZFy%_npik;${AEBboY9!^nhBd8WJd1ivjAOdq!{}7Yf$VFysgD;k* zg9(KeKLQSUCy{IDBbm2)C;uK$try{pcz;`8rlY5S?tpTgj}~ZOUeYtxwX$@={m3pz zI}yOk2mwAT^TtgXtO%+87;m4^nSb(zPsRYhwXp~)V^JLMn;Os))%Hkh3kG*96hi zzss0dS+g>=Lj(n{c0b7Om|0ZDpIsW$HArxzP!U0j@j1Uss)Z1XOQ~7-S*Dq=E3o#A z)Tvxztsr5oAvL#M&=DyBE-2js5^FO%*i}2Gwy}SHm2k*L#)#y#mitrF_2zBs}%-_ksYrz zKMq~Sv^sNjWU{Pi9#slMqRMF0*aAQiwh)q4TO0=^1G#ziIr_}M04|$PtWQrK>P+xj z^?yDIAd%bG1YA6H0K6>0PV~P~WZU$gj{K<)XUe|$C52Tvvs4PX2{cd@%#j)osb0Fm zO*d&9I7+Y4o>{AkXs}vJPG@!Hw3|=gdx_2wLH| zm~J3xI={6%G%+m42^{Vt7r40tmQhRHojn(syR_9m@zO6snFO3~3VS-!w)(7wn2;4k4W#DC(wZj!U`S)iXt8+US{>1*|MJ?p@+N1d zS-q2~#wQD&@JtiyZmFnZN`4-7zP5aB`dHr=49|7>M@|fdMb9-a_}FqgKP2XV92kpE1zUjXCid)>e_U=y zjkKUiye|tQyq2`9qYH`J2O!lDoU=|$8h+>Hg)l(j0z{n?{y|le?hb%Y^erfWIH!T6 zVLiP3VAWvtr`q~ZUlQ*U-DpVTGBaOqLx_dOxfYVOFX3nVlWa>|z8(Ro*liuK@n zyj7!ZP|os|_T%_yiN!6zAIve}RAy}`WE6api7x`~_i-52)pkLxP14Gkm9-6Exlx2L zZ2k#GmvxI*;=t7HLk2MElrla*qB+=C3mJs5{MVLAlXu>R+AK|KtOaXF8&#}O`NRzF z4wYaOXZ`pmXm`#bxV8=Fx)u=mUU&H-B%!70A6KWjqG?s0zWX(LYabhfW@5JL+kNta6=^v5-Hu`L5JP4AlmwUB@he?; zFR{n(btb+l`R&LW>Nv14n5z_*kk8T(+1VlvI|HPGKCcviHNQKPM=ECf2l4hZ(rVkX zh%6A5diGCurHbO-m>Dp2LGQyBt+CykGe|hj+_4I&;0gTB4DW7tMdlZVATk^NYXe!yEddHsk*xghEa-RQyv8 zS^F7{bnkDNDPFmb&#v*Cg8rnX6}jU61;O6yZNxZC2f|p)U3{FXhAAD{w95^=*wODn zymVTtaU>v!EYtb8hiT=!F|@WatMa}~j+qq@S=5zK@S4zoo!ceD&ot*{ld6HQlC*79 z^0Fa43OtUL8pfo)&--GF`vWiL-I%~f=w+j`1BSWi)Q$X=*Zr;HB6s+V$XFA!()SK1 z3s}1Cmy(QM^IxI9P4%Oo;-u?*{QmEfh{fq@v!j4Ug+xL&e)pRS4-jP#x@XnNkcK5wwcJdhU(*#CGPMjn?A zv*4Gf{ZiQOECwG@{lm*Gx_)1#>8L|b7fEAoW8zn^QLB51qIjvF`}g*CJFFsvLl)9I zl-A@J8998K_+FRDvhD}DLs)&d=M$R-YSOqw2%fEocWfUV_QNF$hyO0j$hq`V$NX=1 zPmachP=8^0HFho+@6@AkPO~C@2^Mrt~xU4Efie93q z^6%R`$EpDe2}#kAJuQ~J>hhuj;1yRFl~(5C^0r9L<|Hnz32|d1l~%Hh1Sh+b3xJ(t z;;#LWHF-MBF>f*_t6kPD9dJGFF3+Uo=w!?Kw^(}a6n4D?>4al~N-23g?y2xKEV!fc zC5O2+`4QlYH6L0R_v#rCrM%S?TDK6__}3O$;Z9QPMv0ySC;y z1`(8uNV)g~FE0KULO=iY3Z_Idj$RrzrXu>;RcO~FHoH&x`B@N9k47r0;sc2 z(R~aoBT(QU_);bheqp-kXma}QV$J4}(qf_{%3957#+~NB_1wQ?2;A)5<_fPu7e~d{ zCx}5h710b6ue6t4?#eO*d>T{6?|G}- z$+Q-22t{UU8VZgN?AgAW1Ys{VSI_r0W+f`(*{utCKcNREly}L;8w(~#GA3(C#rOJW z-3!unI>-eXjpN&hd57$7>6<0dx4ZI%)T=En%obt45D0li-(NTxz3eXwxEz7l0*_Y~ zR$E14g6=8tkOYeDnE>Juq-_3xOYVqlCUNora_H(XJ&lD@K3+a=u}67Sp#w~s2}neJ zyBITn2ubqhQ8i+kSX%X!GLMe{$aBMqY@$CR!txq%X^Q%TR=*Kz;Q?Tr8Bm#a@BrSJ z13%9ZnJ+kN@sJl$r+@QlCaKUe*g=6)Fe4_F92~>i+WxE72W7~y^NGrcrqE@~HVJSk z(I?a9D5gaH9{{{TL%s%Bm|3NmD?F9m)!)0}M3MPhpYiGSC5~M>i6I~0*Z<`=_&WD7 z&{Ib)yvd!;j|I5;&NV7J2Iy?A zL=s`;!9%8|mMQ86I{I2!ot&Yd0SK7v4(xU_k*y8Fi2^Q<3xh0@3~aJ6zmCP@#NsZe zuA-#;5k(PQl(4&}hunLOiHuQN5XiKDWPGHa2gl)27iXEB-oWcJv$42BQ8MFlSy{Y4PQ%z~nmt8Ux3YNb zMi%egl?r&W%%?rACZ*Z3=Xw0{_&?gnB1WI;3#Tnl~3Qj zhT;Fu-hV#Vk)?T>;N#+IEdX3-tpNgrpw^m|A754 zYqmDKZM!X|MNR3d)EOCCGN?63Xf3YR+Qojj7a$oKkyXjcitLK!YZev;0@v?x_v7ar zf6woIes%5iw7I$Z)<+~tX8cxxhxcbubrF}>OJZ}CSUiPlbg^r!kGZSYSX@g|+ZaH} zrkJ{UkD_Qn&qdh`#xdAj)HzK|-_Ps`wHg9E;^eWeD$HyaGIQIpcvmG6is9H1UPtPkn8V!Os1fsmQ|Yi zhw&JTtSoIJ+G-gZY+++@mD;WWnrf_Ed-Fq#jeWE@i(LKeA-VvnU}0!%h{Vz(B4rB` z`v#f2e3|PvX6YE8q_?Mm@ah^yrx&YPWbM%-X6GvarM}J@rmo*axBGBe1?KKdQ#U-0 zO9^uO&SK>R4Niu4_p$Nl9(i*OqoW<%fA0hCJXmL7|2{fey=<+lWAoKw5>*x-JYaDp zgr}*C_68gGuHV7rt;ZsknZG|rG!(Afi=u+TVxzX9hEgiV)a{3eCNo>hOXLehl39h3 z!~1Efvl0%*(Um;WNC71iWo11|T}MBSUWw^Dk1*FY5t+KlojbFrMmx2&4%VjU2u0G* z)fLa<*FqSvbn4sX%xZP#SC&L73X#fc53OR^RbhY#6_v)ei^UVm*X^BSs>+gz^-%@# zw6p8XZ+@-ujDos`>9{u=O%KIsMvc zZvX6OY{m+e$)b9nwr!mR^qp(Gz3J%d9%kPzKX3l+yC~I-!_PyQwk^E`p`uVIl~l$r zTtM1GAKLP(@TWB;}pb@F#g0 zl@fFLu_Yd$>)ZA-mFrmPpW0LRLr1`1kaz6Vp2zd}dtFN66Dl&pC*8X%zpK*R#K#XY{~&OOC*WAW;SU87 zSr#glaL;;$V{Sh)U-o_5{dU{C^o#!P^Dw1<807Rkz78R^)%O4JiTJ3~&97(ITLz8alJ z^Fe&AJzRY8AhUPwq8h9?92QhXXYj}|PMkjgnJ6WbpYv~Aq`lF>##)q#^XEBuWQ0s` zgGB0goCXnP`VSsq&v1bG=@r_>_jCTG!&oJi&9xxz)?O~XahQ#nc?z1D6K}kP*O0^A z)`Qn!WN`mD`OqfW{Fi~81WYz7His2WDL=KsGMG#Vx`t$^fO}+<1+&>exm14U(^1L_ z{YOvI+i0R-tl_mkIL*@SJ7h`{2QR&dPfW0~Rp8tYUWWJ<(P*{;KoZTI{?<#Bf-7W; z6=|KpXu#Xr#i>`$(A8eU#^NfT_8#^hAE%Uv5RYe{I8BL;(cMh!?I*FhNv;I@Uw)B} zfWY!%cxSSe%{Gp_{xXBD2JYWk!qwc(`HKfwemG506|gxRn2ZwTl2Y+g)WKkoE2~~~ z1i7+;etM-UU~@Sz86*@{!(w+}Hp`StWei3`3uLMW8s#CN_$Pp!a58*><~PWD|mOIy8#U;f!o7&&qPhm;|- zy2;XF2*Kj!=tM1=rIy#eeVVrRE>_-IW$&ez7#ZoI!C1i6*unW1PogG*+`KZw!DHj7 zr4k3u9%LnMrZM2c(7u;k*~sW*FNHE3JU+tme}4CO2XfMNB$I{umKJ*I^St>}sBde> ztYleQSVeZ$u|MWkYpsSn@f@@F%Tv9-TN2u2${{VmMA zd7Z_T65&&WNTPtn>881}o?z5kwHvGF#5Z53`{5Kn{mWlrYG|RxuJdtH<>)IHku-&T zaEZL93$I;5_Vv)%w#tb=dILSQ#QZh!F(Opc@OKaM`VU@W>%kpFL7-#zI71VCBp<~< z2cy{xnnEFy;QsAt_VpS-kO;Ii5@_~fkigy2%{Tw(JX$WtCqH_V=J8=>u03RQw3qwS z8w~DhLj~@<^A2m_JSL+|F<+o_-ysej9KuG4w|@RHhhIO3R4j7kr*GmP8esX>6hmXX zc>nKzgyLu42-Nou@yegNc<|x7g!MWO9_(Xl z<{=;c>QhP>kOZAlNnvC90S2p+(Xl~pUZ1WQ>>{9RDswk)VDX$|V6cOqy?vjJP?o(D zBYgJmHPTf`lBNqxT)f0chl`u|lAgg&k{F@&!znXYnpz<{i(QbDTnwe&9 zVFO34A0eCK-nF|ZCNnO(g{9kfSPLa_*ZRo^H@JWM5t7}4qUf}DH*o#kPY7;o@$lwD zKKMA40_R-SSg4bgu(9%u4Q{>UjhunPcL+(9Xp;*#r>uKTEJ0D_e z?8gvZ;O(D(if;8X*cITtzx*+)TPb>WkE5;M=db?b-w{e{2-zg}uH8ekSaI1bEZn-o zMl_AP)=zeGjr(_JkR8_FEyzg_P_hYbeteBYriduO!^q68z#Xe!^NXg}<|pv){Ntz2C;l z^eo*c&eLQskt~Rag)QED_cr|pMld|1t(U951(%d%c{Nr=j8@FIGT}`gJXpkFF@Y{n zOayuJFMooyqYJOwLhC>;vBg>9u^4L+g^S-lLuh%GLav0_=D=bRaMm>-gyy(26X(FO zT~C3W>YAH~+`GbN&dmOk`#5-f50q4D8XK@U9PEAZ0+W+{mDRCONzm!K%G~WcMDq%g z2xg~~d}xOIQ&D=loAA_mF`3Qy>wJWl=BXV!!eFz7&CLXXzG2S3b{4P4&G7DF?)~a5 zVp$PijTa@k%*|VC^!B#msr6wtoAA~85UH?J5=(QWa%wf5RpCCbKAd7LoWW29IRzix z=Iytx(|uqM`wt9r=jT7-7eD=koFXuAVP`4mMLkqt&?h>Y8Oo}TB+B%RjuXH0DG%0i^zZJd684_Q z*EPhIxoJ#wP1JjB%v`(8R#u^Vpp*FgA`c%fP}9|oQ&gF{dYi0lr>m=pjfcw^oV9qQ zBFih0$|hT>gsNyLg$(QAJY7RQWFj%_we|R1Mv}2OWmQ8}$|z+8UDGf+ywuusmX@}N zZ>?f(>Sp(N7x`qA&3J*mrw?H>!TQn?uFhdjT|ACc&JxV3bPse8otfw1^df=oZtPl# zsjIgsn4NTX)U*C@<#!Hp5)2l4cJ6Wahf;(`WI|Qva}Bk zu=3gaq>QyR1{`=>dpLM_gxJzL>a)qLX&b5xx|xECrj*Dj7WN(Ahtnnz4<~6InWU%7 zkD{BHJaGge86i_P(%n_d?1Om>4j=7JcHaHjr*!Wg#%VK>jz-BAG)kE$8PP#|TP=}b z98If~LhG7Nczqqw+eE-`CA_xAJAeISX680XCo`1uS+4x-7hJnGjV=gelWF{&yBOKk zidxK*DO=fhd>=Lw6mkXfnGEG(o=9A0->E~GC5^hS9+Ho)vYIu~*-^{ZqkFvhw{H?n zq}kevFnHt;?X5Lvib648AfL%nF69YDN=%+Sg2ODJ$rieM>X~~mhc1c~G8xRZolNfS zL(obj(mIpJ4&t#(n7mEwJ2Xmoc8>X_AiGYSL^JPdtSQ4!O2bpvy*e*dkLdf!sc?))YCVWAtV9vi5NM>%;f%2 z;v35ZM)ok+;bU_t$nOO+`8>X|5JqjidZ4`(vz}*VaRalbhQ>xOn+uC%ivrDEZ7As| z8|x9=^^N#FW>)8xC<`WhZY$AH6cqut-%mM{Ae%2?b$e-Qt|h#(Ou7I~ovoOJGOKgT zV0L1pNFtd*E zxNH0<=>(G9iQOOIprK%LLR z>ijZg!9+`EGnvpPs>w-Rjgzg#MRa#NFJIclU;TgnoPub<=dlrwW$@NGNhR|%v^S%q zVyvx(aMd^Ba~jxK*}`2Pz-*L2RSB(aVymskDwkPZSff-{F*%&rOpu6Wp31!nl98s) zR+Myvjg1(tx&~@IMz%I%m`nnNqKJ_q1>HnTMw_;E- ztgZyHxcxLVxY=CUz|+u#RZ>}*TcRWwY3Xc3R&y*btWqjhVPb+vprZ}9-Ao}BXJu&% zSD+rTkR+BW(bV0J%_Nf!2T7Gg8X9XzY;6$SievTqaamwA!eDklOI1jbu5Uw_wtrUz994FE zc3?2u;2c2|5Jjmfzhegiq$rP}Ks%5jQ3SoxM%f@E@YRBxv~89|RgS0WkAXo@RXK=4 z1@xopk7awJuuT{GjUcBAoal=c08nv2F<2cmG&)(CTSph3%_dAVm}qRPC%CXgvFhHv z4MKW+y>&!Us>go#z$AL zvk^{JB%0Oxw0(c2&s()W0hAJ}ZLC<(eO{+twFdhFZpM=!Cr@J&on4JYSC&{_+6Cj8yy`1vWXO)rWU&UI|ozp`6Q5EUEY!oA5X#(&@qu;K*qAaN-*m zX>@C>EQK+;YB_Xloap*GWmVsqICrF))j{=XS?3evIstNtI4g5Y#Nru*9iPb09|M#^ zo;kCxE%Cnb!Zzb_TecF#Z? zS|Lj=Uw-^Ifn&;giNmZZ09M+?0exHa$=cBa}g#_ z?_uT6UCKr;Ctf;*8e3**EzQwyy-aso9a=t1B%HwL4siO#11#R3p>3!agV91$lb42( zNhZg;=pGs%vb4gUmtSSLw}#c31=6|F4qDUfspHbWdV@xv%;LiZ44wwQ`M>-t8tobG z-de5>K%gllto5z*v{|@!Yl+VBJ?z`t#={%;z~<-hi>K)6Y9Jnr(K^@-icV*D8wQ)3 zrbZ8*hGvXvmUOnlG!YPR1{yhZ{scXp0g{Ov<7ZAV(BDEL6rriF8&TEh?Cm5S4I|iU zIDP3b3)ioa7F~>uH*)8*DFVGi96LG6!tFa0&3-O^|3%yu3zyt*iX;c7z3SNrf$zv(W@1eQTOlwg9r8#uoT%!s0{3xWZ%Jl9$vjmsj^T< z*P*_5fc?iNuu3J8IhZ(ojPCXtA{$$j)aQljU*`~;I=WDjQR3MWonyn)+6`>3ZsBfk zp|_`v)Yc{i!9d^mFgCr&=2ndQo<=sOXGrJET>a%ctZ&4LZHBn{{->Rx)tNmmFrABTw;B8jzp%!t+(G}X(dW*E6SY@FSEEDLJ*)- zC=qCDV)W=y#`^s%Jeo&0d#SO>Y_0|IcDB*o)k

    lcH>*Z)_N|l4mOvr@p7@pB3cv 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

    =c(QYhx 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

    #4 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 z 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

    y!Kz84+tv550-BD8&fc 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 }8C1KDn&%E> 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

    Bb_`Rgd5lWJ~CN6%PDmQ3l%0!%B1AopOVj z(vj8+65k%({wY_A0p0sb!Qw?TUBJ(LzD}>X)S=#RaWe(}S354VyAkJ<_8Pw@tH{kf zlL~~2+0bhI;WU}~%kVYrne{~H>5CeoI?Bi!3IpaQ7^PWNKp$(JIX zD3nUBk{ai8pUc_k+q}64U8o}COD6BJ<`qxt^YL{yjmHs7;g}lFbUGBV(w79IN)3iu~@4<0~7Nqh}~;mbWc=6vRuBGVgX+!$lik zSROI@vUu^6!JDG;?V~obQ)hZq)bB-@ixaz^ZDX6&-&qa7;SQH4Ms1f*Cd8ze%&FB4 z28OmCN3_Ls#2nr?vfqr7(L`o!7hC~uP?Vn*7~IclVJz@{6=oSX%OpQ)Ec?X#n@wUN zbT4hF@&25eCG?!$*LtV6{%>E>(`~c{FLbaKnrH)r1Yvip9klS+1y61lyN?u+X`pWE z5kVPQWPYM+tMS5u(7%O~FxPz6hzEmM8%eH%&$ny0-u8|hpjKjGrV?Y4BtFOJr?3=E^B<8kStVMttS=zt->0qb2ck&tUwZxOK&3@2far`NloI#8I zDFlF?tJM+1<_yYsyd{p67ARmzlFu;Nh7jJFiVJ)}5@60MIm~TTd%vL8fC{#$$F&YT?fz=tqt(;uFnVkLh2EV=#qkcmRqQU{=}-A+)#?4vCuA*(lCmfd7FoTPMo2XBzUP=V?^0%J3wGZ8LaoAfxb z32y@z2AqWIk(MO-vdv}sPvG+=2&@9`quJfKq%(*5R&Kh0CE5kCxEpdEKi@j1NDUD* zgn8Lc@*4t8I<<;>6gwVV?xZuniSpigZlk7qZDp*;%zC3_BN6SzfZVd+;c&_&8gI1d z(nVy@JZjC7-^*31e9k`^;r41%F`7B3PG?S6XzBRUS_CNmA(8@x+PTNsXBH?R7G@Ag z6H8MPhce##ZW*)V-)ZE_$>{|&KN~{@$e5seBpo~&n-qyw^)jj`fbqBkF+P*g%eUdF z-R~S4%3wJ`h2CzHL#EyC?Lje)wDHs(Doz**7XDI}=D-1~*u%MZZwHs$Q??4Se-+_5 z(yyu2YHlZ8BvtBv` zg?7~=%s}Dq))C|o0p-9IG+)68cbuXoz2bWjId9(J;JQK|ZeRdleYv#x8q-$re1QrJW4-5{Jm7VIFx*kyLsym~$sr~_KbDwQfv5MHX{1hi{4yDlHdgSk zxyo86ATG5T4{4(Hf&Ka*vl4i^@MJnKakw>jH}|EDW&vU?-0yJk+D(zgjXINCg@?Oj>2qgfwCr|`%}nBIVkQ#apx5X6MKch_I1oW;l2J*2rNI7cOCO1jEJ2CB1ER-huV_^@eWlx%OH z{a}(ck3jMhby_cD$mtHjd9CY<8q`ouyGri_Dl(+hWwU1o*k1S<3suf9l;>`XhVB2jFT5PeI+eobN_jNH`vu<_A)h`H zh%>tU{uG-ivUhp33-G6&VKLlO7aEw+(fKC%3WZvlvtN~a&L#wuMAgO6~J6nS~0TQk+;SWWtDBe+ZT0t zAK|2ZclO1iss;z?sNUXYV`aeIM{v?NIH>*;oVhbrrW-4pRK@$QT%A2zCTqDu7=~)9 z@}38p^6U{U(>wwu<2SI(-wnz5)?y}@_B#e%%HbVuvy_{7M+p!_l5^<)1r}+a%@cv# zTNyo~gK%xe^xc;)O+(X{zS0?%@85Hbs-{~3QK@Mc1UI`dFri~}d-n4q=}72VAguCK zM-)gs@2${ zPg@k&kAi3=@gBK^_6BoMD>JyWSrt$);HLw9JTaD(Xy%gT$*hZb+@K%kl@Nma$<9~7 z4(8dYetBV{MO-&7PS68~%ko2m=yU(CkY@T|V-TLtpeHDmtBbOm1W8s1%U{q{gi1+1 zDO1;BAbY^QU~7R&oRhyVJDBy58@N+OduRe}S)bb= zPTXP>ora>4%xVgnC@No9pR%9qgxzgv>+(6L!cOn3vTH(L6lV=s4F=lMDEP=YgC-%< zsv-r}w<33YaKVG35hlX$b}cvQ#tVzA%23Ob7K_2y8?*dS?yRMO_ZYt;)MTAP+yjm7 zm$V-XzTGv5*27aIfTF|^HmKyPZR-Bwcpn>s>k#QX^9wwn0JG^Ad{!J&`g>}sa;#&3 z!`QjT;(*SB-kzXG*VPRxj4FLNVY(LeQ+<=jDb#a{T(a$*ilWO|nj`J66NqNLJ}Jf$ z4{qkfdL6HES;;W?S0&hqd&+xZ_Lb&>!Ocxgre0Hb)o13kQ_VOl)%j|~0aos_iAq~F z<)2XvOGhk(Dco;k%puk>!Bi6>#3p!LHC^c#K|-I1!x@E@Z;$`!Vr4){wao7ZV7#u~ zV?Do6FF%k@ax*8vg)lJVb0ovBw?;ofj5p_O9P<3=ShpHW>A20>oK$_J#^VbCi)p-C zzXS7l*?fgLUimD#jMKj)dF3woxgLAMXU|zK=kWWro2x>H;oh&2b6def>>-lSr|I2& zpmL%Ns3f({>sg7oq%*A|8+fKY22|ki{cer%4%v52(%NRqDnE;WI(}kvT(Du)znpsR-PupFNh{cz-(i%LjCJ%o5lOZ zfpOeyuo&fI!2|BI&tM+_7;sge&~e4xWQO_7Dn!WsJrS7$@B8!dMMpbi{<|=lHdg;H_)@K(I>;pm18+5K9C5Sy(#qb< zl_@*-L+p+^{@Egfn|j(zy$BP$Jh3aytb+r?_FhU8gXb>klfqV_7xMY-%bW54hz~NB zls=`$-3<0nv#f6e;rHKTcr7?MjBI33=(Ma)qTFYtjBRM&A!c%1 z$H`Y$vgsB9H;~xJT0;++=vUh}$7Z0BIK$K${qIJ^&fa?irrP|9+vYW# zlHDZ~*E52y)}sUk`4K&ITg$HsLX3s2@Q2w|Gu(!}=|dUb?w^~(g4^BELmE+aD>qk^ zP?(|kOus%|LqfcJ;P}d?RzS=XT>T)LE1x!OLy#5N>t0#y8PI89Uw0(E%p;D?O73;~ z9(=LC{Bap5tTPdYB_8Ed5FPK{2g7JJzU~(ct7wEJSf3BpMrR{EPdr~%jI*u^#oz>o zDsi@zG+(V4sen|1GVo4DLsJfz9LLYN&`|Z#QKU=Tw$;H1avz7$`6)SP&J~0yR2~_D zRh~T%`Rh?BGNV0?J4d|eybEeCwqrENEG*v)=fT+svF?Ls_-GE&s1U(qi*_u5Q*9Vf%3P3V_6SQSMXoxBSiY#*HHECki^q6n zk_b<(eS?dE)zZTbR zIre>bwVztKJxdewTJG6r70F7FCYcMST#Js_3xea3LfEhnfRT1tP2v3{`pD;A_D;CO zYIm^FSu4ku7ss}IZ%|9==V`v-jmk{6ZTQ;ok6zoZX33jQ56cJ%l}c=Vfd-Wux_IN1 z=|}BqvBw3#3df!<&-L)KPxZ-|q}FjB?ka*J1^z^gnU~RFblnWKdM`qD`TKRdln3}- zhdaw~a{ZxoFajGr!QP+zkU z@OslS8J}~=k;RYib%s`Hv4>(;Dytd{e6u;nF`gJMT1E#$O9H2uQ1=R`T={G$c(d&G z)kp+!=$o9rCG$+9k3|9&F{>E0`Q7Dtt0Ub2JhP*4^6eb_mJNXy^=A*?)g(AcSk%1Q#054EhgJX zw?|4}YFI*<&TvNi*ZExCu1c_YGC$9!p#!Pzyg$*-HjZy3xZw9CT;s;6;Cy$-TVT)$ zXt7rL*|V$@FzWVvNSemCYxMT&WWxLQ!q#AP%zi{VcZLJA?NRxW=C$d)9d`@eEKD$Z zr?V-R`?eLyLB%42*{xjn9?S{Q?Y1(PHsP*zql7Cp6%&<56o{A1Hke^NC79Y=O-Sl9 z1-0otH;T@N0a>oNnVmJER64F9CTzUDDi+)~nHL0BjK;Q$_sOKV7yr>980!>JO{dv? z)6x9$?a@dQ%=<)22``d8a8wF`xNtvwi zZHAvs=UCmjo3W!T881(uIKWh{E^bGhD(Uk2j4?k{^>!}zrSOzV^LjzC0yeMKa7&^i zy}eK>8Q9DH1k$)6rHkgg6ZVZcK}vntg{A$GSFq*A4%7aE?(lfMahTmc z$B*Y(yls3JsjXc*$_;W0Ax7_$brrH8opbb==DWXMfR50h+KkP!L7iQj81QAPPLr#R z!sLc(G`38obo6Hwn~ks#$)>yX^}NojFBViyeDLQpNn7Mc#IVca_dYvR6s&i^pQVh{wG!M`3xp@dhDJ`@6}w!)PktQ$JTtzB zG7ddiams4noV?vXBPl1_{tMl4-s3A`%6mH&RgSc}O0fZXhkQZwTLI%g@9=PF<_ES- zhs#I3wiY7Tm~8ucIPuk;1*aU_;0rg^0Ks*bk>6~OCK)eso1FoR&n9T2f@Zirrh#)(W<_JL+R3bNb#Hxnm9}0Zm|GrbGvc4`@ z4%sM*(|#cXil`;Z@E9~|nq^Y+lv^GC{f5n>aP`7BE?ko-3ftc6ANF!kCL-`Zc+rwy z9eSdF^aFqPY@B6KRo|fsXZ5v%JmE0n7P3DM1B4t27D*q}fF^gI@Fj%t=x64Wc^;9H zViKO?g2krKu_>8-lWBjQ73^p8)Sr}5v0&z^vgK^DU=yAFbC5)n|C4HSG-m!^skVP8 z-pJ?_(yqYa1W4hsccLzvDDW@wjjU-GAKVxHNkCLm^MU-6>rW^oW$bYg>D}?cmbkko zkX46q5;WI}jbZ$ZER}Etzdy{~zne>og<}sASwSZ@1AOb(sw826w1%sGtk+k5H`G@} zuxk#IBu_ZJYL_FtUnOD;gRhvH?iZgl(PXwL<%nRPxM(6NicwofGxO;!ge37!eQm@WS4a%?d42*_pF#D|^w66tX&|BCbJ-+#p@e%t(TViw6)!A@1cMbp?%@CR$9G!}?O@ z?)cg7iKma;!5O(M%YUH=dkxufK?=a1D{5Th`m6|!IXng^nYNar($q`%&By#hJlm1{ z#0Iviv6K452Q$2%I4Q+XXjTW(2;RM;zo(@$v??m}Kv_ICy(0b%zKXo-;+ia zinoX6!%9!d7|ZCcH19eORVGp zQ+Jf;cm<&!Xxyr)_hcZ#vNGWYtr9Y(l}<7te4k`<`@VHzKc|8C!PLO$wedHA|A$L9 z$h{J;x&djnFp5LPy>dGQ4%H%K=Ttjb<{BnW@qdCV)1^IZ^+;+^O&RdDQ9%=+YGeEe zk$+cLhMNMEXj~!hY4sI2@9<&7aZgQ{jMGQ7|MW|F-VUK`=mG|Y&#yHBK|f;jI`;43 zYgDQk*15C0h1)bdtp69<`DdFr7Q5QLB{PSTbmPucUs>pe4H@5XIS-sf_wdqrk`dOr z2Gsam57R0UlJ`v(P(MMTK`g~v7i#@T?{H{OYd|GZ$8R49PWeW(Wwy?U=B~)p=4?w^ zmnUt)6>wO5G6W@X$;s<&gsReUfi!9D5m>q4dce0S@XK&)qvD*2fw1jAgO(uQ!q6g2 z4BK;0G-k(l%Ze;=k1_>7a{5Y?&{n+72B$qVz1FXWoK8aa1%ZC~xjb{HP+}drjBM$U zyIVNbfh7$Kp@u?YUK~@S@7FCO+MVn6(Jc&MvN`4ZF0ZVoB?56lUc$}Y%F*m`c1U=q z3($BjOpRCSgoO+5Xyb7hJogr^NOIMG=`*5Y&7ez$r=*WB)t9P4e^nd7x$+V6zsm1N zKSuOlO1?ikX*`VYUr!aGjQtD<_1|ELCxEPC_xNv>WWlg{$y)Pu6;#C88Mk`s&oF&_ zai9Lp+P_ML$0NF=ig+m?uJjB~{M=w1`^CcK*Je`-=pi3-`MLv(J#qrbyAc_>QS%{%}NH>k#x z(K44(5s`6A!w$+6@gzr}B;a|}I%__mQFz%*{ABwtql?kz9M@J~`*^S}`bffI^M0B8 zozwnNa?*BiXf|XvR9vSu@u}-Bcg67E=qBv@QbO{n;mh2)P4D(6p7AWP*$dX%={F^| zejzB;&icHn3(Ne_48Ie?R>+lG$GF5|3Q!8RxdBSM@th(vobJ&1mDN9@y zb)FN$?Nxny$Des$@Sb4g2u5|!@iz{0VwhP&hxmCN3UI}m`eV?<6ZZj|-O!5+w|B>C zZSzq)v@vL~F12imag6y5V_Ws)d(p-uQ{FGztJk-2d5&$F!p&%=mzo!E%y^h}>4yfQ zOnl=)&zp@5CA0F@HNQ{GR=kq+9bsIj2AIv*??PhrM}?m6)`FWu`iR@q|^{Ieq6TAZRL>(npyLr#9*G6Lwz z`#2@gNIXQ%i=6Zai<72K7`ZI`({bBSg_AzA?}QdSos((CYKN5)DL$I>nmQ6rJX4gZ0FM3K|5=p{1mTrjF&SM z3vejEsTKySL+!)OKkhLcjKC${N{h09B)&m-X1d7H{W2Up7N%}#44xl`U?kX$ zh8lVGHv43xGm?&Gc(}%k7EXJ8t?To0)ob35Xei&dhkSNAm3VCP{2?80q_?iTDI5l*4fyBzKK<1m|V7;Z>Kj448E z1f_Hei|g!~-0E}jOw1fS$TM5Hq&~0&6oFT5?|Gn+sW4P1&(ZWtMv)K9un{NcmpVt= ze4X~t70tPfNydihl)?zd1zhhH=DNKm%RUr78yQotgIIAQX}#wBsT1<#Ju*J!;RKQc zMK>Np;(}DzkmH|5iK=>aCktXX%k`$E70##$=qZ0h_FB$K%7p;`SaE`)k;r626Ad`( zD`v?9G!3sJ6n>IxyN2J3#P1&s3+tsrhGR+;cG2m6HH*58;v&1eVn@A*t0zq6a~>4} z{HayMvWHm9>Q~aglieSIygemK@4pKs|9_TJG2s8Bc>ZU(%Q@pf#=KY(g0aTBBiSZL zWof`x;bZfSN!K#sy|Ev7-Nv*F6qVupy_d)s>XE8T26xU!r?Yy|)+T#z+_zt3UQ8=Z)7l8vp?h0Hc z_g-vT@sJthP@yDyGk_yRc~-%8CGsRv000oZBnsgZ{easBYz~LrUu_DrT^QT*8smzn zefz5w1LU{@<>R3&H?I*@t}Ay>n_iDiFl@Q|etV<(zU=u=V*W(#02@r;d4maTzd&R{ z=aIla-@MIPQIFcGhR~=oe&KUH#vtz;Bpz+dhBm+=-;TCep;2c4QD1+%y4)G%+V$5*9mo}1?3KnI<|B%$2X9f~*Qf=EO@(P^s- zU+wkaQ69}&!W8HV3N^PKX}pSGjU76~!Bp1|`9`zIN zJ>aM&tRGSo;nX<8TY7ENge}cKkPV9m|z22^}a&wznY2#YWp#pS=8dJTBLSa4eQ;B`i!v-Z!E9K5*Dz|XeA z?wh@au3|Y6OeE8AJ++yPb?Qy!=HL!c5m^m=>N*a0Gs575O%iEAodi3V-Hs7hN#=EC zzMkc-!}r3@q>>s>zjQ$(;R?wbmS2N=L!Vsa4px_ciN zOGqM|tG;{r2!aTPg(b0I%u#>^NfN$6!kg%1#ZGAmXc^!#@61cxU7ns2hC0e=LdgUL zd@{vNbHvT!SB?%?d>*C*7LEE$>-u31VsT#80jHoqp47>OdLUNHpOvE1Qa5Kl&#%!d z6{l}bavO$%&A7GiMuidvpfTKofovC9HpB%hUFCP{Igs0_e+#qfv?i9DK1b!M4$&S` zpFDR+sODS&Qm$#U#Po#k^y=`fj-$;M?R~$nO7Sgy1>)qGee7&D%MY=@xK-^xS*uIV z>GN;|{^tqJ7l?cDQt3eZ$@1#4=jYwF8!J|_D*Z#{oQdRd+{TOn$?G^tC~&67<3@lQ zaxJEVE!lCLw~@xIbhuX;2rN$Yvg!XMV-?Iu8yT1HPCAmmwy^bFdg67bSX=y#>#yaC z+<+LPm|LshEsON8%#EAQ;1tvf7cfy-=dKX9IipHjJU4nGqc(^$@GmRpbdQatVG&|J z>=3UVE;wy+Jg|ikjF7g)(ZY9V(CU=zND>*}%H#5+=P+4_Qp8xqjJ1olJPmd~^kNb; zk7tZ?t4{xU9qIkl`+`fME9)Q6=)Z-OO!siT{r<}}lV*|(Kv>e)o43Xj0TJD9 zi_f3#Uue?@nRf;0eG4L908Al`d*it-z$4|{c4qQSb`c)DalV%ekG7Fc$T)#u@G_@<@li(9A1WI;~K{5M=-dHEsTry-5x zhRcn0y;^oZ9Avof;j7H_QrA;~Kku?i&0!-YSYi*vC?;qVLVA8fGxkupQyOcJ~Z~wkz1fr$EO*|+BGQoaY==taPJ_B zIp1_n4nl|sEOt!xZ^TUVJqy`;AndnFUdnV0v)j}Wk@&*1&W#~D--6A7xU45k^z5Z$ z&7lGIAlu(jSNPcy$5<{kpkhR#mxaQHD=zdD&emvU1!>;0&*Nv$|nzDUU$ zi7E>_aHtSf{p6@+Y%=RpO1m3^@}lC4)ViG64ynEw7c)LA!QH`@XU1QOG4(LqOOOVk zx$^c*JX|rh9c_izAAO7*rt+vSf&gAgh%?t7t|GTKdYzj``JNIT&6jB7wI0#2&87XU zi(&FUyu8%BRW*KHMNr(J<)XwaBrwp%V-)qd zhsX72^>2RhFM-E&@jeei#g-s5~br zFP2G9yvL#u``Od@_G=E6S&zKCq!BFrmvA=gL3e%S3+{FAadP!8iG&y5Q!IH< z-sNThauU^WT zNT)t;h1#w5?7wfjIkNERVAe7A{tcbCFeosAY5w$Xf@wSEyb-m3;PPiX;8;-AG>i+5N$kgVSwg+=OdzE6BOUaj{&a}pJ)5uqPgSGu&D>v!Z*b|+hXxXVPD$Q*&7lI+?bJ?m6 zQi)>sYMyM1^_pIJ)ca2P{7kEb+iW8Q)!Fvzr>AW6ua)qv&W`%F?*emgM)OG(n$O*A zV-p|Vwhy0bGgT7HvCOY{w-d#P^WKQc&e1}PgqSoHby^!jI!zQmBx`QH3h1uoeLddQ zwu}$hiB|5F$$(CJg1=YB+8#Lh57hnR{FAXE8imN}cUX`2X7Ze+On6pMJ^XLO;VBJo zT2im|V)U1>5gvre2v4}Pc4Z#i=Bv}gm)Oi634pn=7weBci7nKB<9DEUiUaewnd%PXO_j(L$njRCu76@0y0+=1iL zm!w0$Wf$hCEk~+-f8x2VxbXPQ|LTGBX2zEpV=-M!P;0yE!zi#8r7oD)+M z{8x5@?PMZWE4pIpw(pTf4#N3uT9~{;y^(Cj)$Je0gt-UuI5!0 zU6nT13#rhTpPeV{)07%I#+%>69Dy>{T_t^IV^UF>GgEpys)2Fe}gi(6%;CIJyz z+0TCJ$4Ou9wW?#pK};~&bHtllYlDTG@;TQdGOEn}ZSrb@E9Lc-4xcE#woE-w?9C!m z&Fjs9Z!1oy);o(%;PQn|IqP~-_c#&&elq#FC|aFiy#dWp=YW}aB4oMd{+JkVkfuGmWkYxY}pRw*fPV$cTlEM9BwrqSu;Q*ioJtkdadc?MXbU55@cm` z-#XjFd0$Yfl_aOcO*R^JXk)T+-FU028wTr&!MA(6>73hUZQ$Fe(-#IU4A+)_y$?TM zi3VU4P=Hj@o~sJJR+c8gyI?>4WsCIF+ZTVE_=DpDVEO+SQbg{I#})4#nC?}*d`M(ZC7U3@7;PkTIM^kU%pO7P>6w>hh{Kc?7jZ^ zQpkZ{$;BVM^X7eb#?2W@X|-`tSyI|Md+10`LW0oXkGq_RhyaR$unaDTh-xCo%f_!M z?~B+EhM}jJ7K6AdVyh{Sg7FCvW%)-I0xRNYgwK*lf&?UoB<+fe_o5JDs$9|hz~>G! z6T|k$>x~H&a~k&xGz-?NvJH45nvDbg+eE~rc_1Zp2(pmBY6j}}2hKP-HgB6BL*#Fq zEw{$mSQSo&wE*(YmLomPrX=w2466H~WMuA`4vBflFWX1?2o7JYggoDY*B=c@X0>0F zyq5|YU8FhZY&1Q*|CE6UD97)QFC?&&-8^F=+izGg)s%WZyS|;6JsvaI9-X{4@>9X&_5{Cg`EU;C6J00f>8vM7 zT3|obs=hw(XYk$tNAWU-6dNdJ`HY=HT1v!W9{g@|>ZQ#v)n)BxERvAipx;*?;aJ|M z;{@VdGMC-mA_kGeLoxmUgvA_a?)fhj)=-=2;L{5YL0}HwMg+E$*S8==gu~T-Cx=BG zN!KD{K9W_Sb61N4tz_J&z|sG2#DB;hcGA&!y1!H1A7RnIM%oAUvz1xF*{iboBJFI# zUz8GjO}PTPl!wIEEz`=ty0sn8XeAL3=U({b&#A+1rXV8<_0)kQ+OyG z{%I-UoOu?11-{FYkq)_C%!zkPoXqAI4+e6~!}s6UTR}}L8M3YkLg()h^You~o`z+Z zeL@K{25{l6rt(>}z4-#h9%4s^mvM;ata^D%(l`YYMQN zdU>PcxbyA>Y5mu*{0pzJ+TrG?B=7&sYeqgQI2+TPRpP+$-+riHqYO2t?4=?!cc zB;+QwgT+8z_EA6b+kd9O5v+~Jek4GX64s={OwAdpDw90pL;}pgrtW(udMwf3jtfDB z5VZ@X5D^o2=V^)a`DO>bD@!*KU+#0UWsV3kP{2VXS(!Z4&cV)}peZpb*i_^9Vf%E= zkGmD2BJcbT4ps+yINXgM{shuiiJ}Km*>ziS5y5g)v<6RG>kSh_pnXR{pC6{r7VQX>kKX)OikfZX;UfPYpE{!u|TlOe0)6k9?-hyzt zQ7Ar#VQQoV!JZ#XbqSufY^)W>o%^+wb8==iR3l(}E-wmel90BsG`hf(?uH5HiM{># z&4nfjFoiV(URIfFJ)r4a92m{>JOB~-YeEpyg#k1x>Wdj~4HSQALG-;}P+dPBqkL5| zp-DPEu5G1VEfs22>S5EEIG|aNx!8qNMOHKEM{O^Gbrt#bTI2JD_Mz>c_{#}Lxx2+O z^xV+z-UiFFyxguW_McaVEQLAShDw1Px_2-o_sMJr9J#`~i>u>5aCpY?(LZr4if-wm zYk1GuQBh8Or!=kt00>_xgKeS;T!RjU-5a!JPUy z?Q15GNk08OLM_7xrvH_ypogFSGVylO6e}Ojh^YkQqjj5n=Tk;2@!)TBw(2nWqqV)* z`t38-@nkGNl!*r!j17kuB+KVgTHkpi-BSYN)l7e&ki39w!4NQsXxntsf`Pl6~hJ7p%;~3JY0k&T37q=@u2&ax|dnl8V^{7@9K;znvJ!S2cmSorp5+p4ywfa*FZTAic7AyNkiGQnO zyli5-ixm+avr1h)hN1cHX(?(PJFySux)b_gB<4Z%IQySuwI?rx1+-)QVy;^V8uD$nDj~7y@^VdHQ_nlkeq?CfaGx~b(yewSm`?Th9iEA^qVWheH zK*s~%`cf6&w~pi#=k&d`2%EPkD{H@W_~WouZBvK{)gR*-ON-Rbf61am6W*9m z&Mw<*-(6cP^ZO)J_lWv)blyPrPoujE;Gu8C4z#8>_&i9c9Y70v_g24#b%qF>-xnIh z5^l51JvmdPc0M!k6p7J3@g(6MmR=ZAPxC)9~&^NVaPaYs~<-9=W`G| zG~42d*cCgEu53t!a_wI3-;yI&$@Vw_f;U=b3$vKqT6uzV<|RHgva_5_3MmVSH^<9T zSlwf@mpmk%9TMwoZ+T@ctgr^_!~8ZID(DQ%bh|}b&c4hUo`kuPNGanQqOq9 zEWeQA3(wz2I{rkE(;Av^71=ZboS73{1{WV{$H~PlOE02ZPdUEmdoXUoN-+Ri-=|zC z69v!Unj26vg@YV$OQ@8Oi`VVHoHc69xJ3>;bK2Px2jz5Yf4f|$imdB<>L%ff8#U6~3N6$7Sa3F3Jj&euu8WSyw|CVu zo6+pzOB|8on0dPhik0JWi<&2j_JP1=5y3TNS5I^O#*wLSo6PEWxM$)cerkVfs>zpQ z(7ATHr5y*FI7?$=xrpYS=~>T9aX!wkx>Uf~!IfR;E($Q$x3J5}h56!EmF>olYa81O zNvST@s{F-^c>91q%(&N?;EFP2pj0Ut?IEi<#q@So*5qDSVBbYm(^Z9dvTKk8)_BKn z+w()6G!rJv+z+4HWPDsrf;H=buY_nx2?ff_h>3vHMY)4HZ)JE44ZuvN z#smMo6?WN3TfMvFSe(~%lQh7Lj9$OzyT9_vIj$Su_;+AXX~*`^&e*7R`3;w+-=)im z%o_MaZQT&7?I(502FxCetWOmC|5kDAzquI+%3%4De(}HMlJFuCC|j z9ixaD0iqjC1P7nOv{@HPp9ld{0c4;KW4AY!^!V!rHs+fkq;_x;_st0#8wJ@N zXOq!xfAV}O@xPaR?EyQIuJ&ZC-I;M#{|D{%OnpU?YK1jnjn#d!A(u>VjUc^%*|ps? zL8CZzSXcy$4A`I=F^%2o_JOY^vYv8G@If77N#%pt^?~p3kKo708-RgMSBe`2$v>=T zex!DPAA`RoHi50;_e^>uC^Br=ZHOGj1ohO$Hbtu}I(H_qk(^`XdR^t(Qus4c2dC=} zko9^;9c;@i3&F`2sv>n8h1-dsqMaWskmfKplKgNbDlzEkSXK8u<%lD?3u>D}b=;(4 zejE223Q_{T0~ye+k}fZ34lswX$eX@vAT}22!z!h>mCwp3z--52A|8{VTM@Zc7#c^F znJpPhohX0Z$nJrrR-7dD{ant=YlCYflE$9&xbhQvoB?{AH%6e~-ZAlzdNExE$9fBx zAzTVGY<#H~$FNX;P21Q^_)wl~!9Ev| zv)cpJD9xHo=NmN(cbxP1b#nr#-QWGX&?{_xar=RN365JZK~jC)sdLbxOor|J;pPQC zwX3*6d!7t}Ge2zGfnVaYfN5`h&sje;t1R(j@O>>nYH-db9eh_RBi*cANpyyv5$ zENHoTKHEe{w9CDdp0Oxuoqss4Y{kUzNo#N13Hw;9?{sQpbRr!5aZ&N3=2%I?;2CQ2 z0Ila{*9i#-l|d+OT!XM!g)2bEW7nyId6C=@sr0VRs+vGK8VtMC(^W!yX;y z_DDidi|xi-9u>dJFY`$}?Qh_`ExnfXlmHB$^zM$B2qE0oUqcXRun^4uMn;wndkxD$ zIn&ZRQmV{?YBb`ky^}q!Tb^o!Z4O8R{>WPUF;#C68l9VnMAC_^m)j9oJ>L8w>i}i6GJAT7DGu3q&!G1SzhkK$alNO|-Vu5;?#!Fgz7T#tFw+%s zFq2@fZEPItEO-gKK_#R$Q-8HIr$a%}%+Ddf^nE2lrunv4>^on>0ncEBj79=n_|`fJ z+=2FZw=sj!@lWS(C+)O>2~rjx_U~~{M=3}XulD;My7*%otR^yiH#hE!loLbDr+%1f z$Y08}+#H0tVM2zC(;nD$$P+Ms$IP~@L0-Jj3_LydVb#O~3G#H(GxUUW_D`Wh(kw4S z8N7_*oC|c*M!2+lHrT%u}%YqgWl-_rX{EMsUr&E`qoEimC_?#!puN9IsJ!@};5HJyQ451uI_Y z#Rq9GLdJ8hbMaTnlgLV<)8D6+17(d?)>`x1JAMN6F^785jP{d|rCn7f+`m~!fyg|1 z#1!HA+pQpka7bUYAOepoHq3e_ye*@=?5z*HdhG789g}AEWjwKZ>*pT;rXe}cJsg)JRMcPOS`>jpgPmVXOOLrH4BA6^-<;!V?6?oRlFpX;%8)(T`D zT$B(_SVL-S`Dhc8711#2&avefsSSt!e7@SrpI)}cQN^0GRKv`qI%y6m*oQzE)6lj-$Ng|Ycd0#o!hZ`Xl83bw8o{PIEU-AZR-O~lT5{6 zk{?e7xJ8c9Uz}}B&dvvxrUw9OyLrMqT z0_|Su3&&a)naZi1S}Neg*4pgDW*WT5xu4rj%iYnB+Z`^=Ik|Te`E1s3C(UC)B9mfg z-D?uX_9$C9pEoUV9H_CF`>Hdawi1W6pu7YrgY(g!$YR8REucC9ei6tfO~U0|7s?R? zy>=^>_7J%aOM7^`s6>CCDyLS`q3zGZP>4!799I0uiuXttAXaA{u&LIE zk}>m+r{vxuz|)0!_MWp5-MU8QM*N)T7V$0L4h4@|hi05s)vNP#>2EKjl?w^N_ZOrbP1n%A z6_9dumfZ}Ba__RxAijN!SAs7W(@rFSvY_)bIScpEnMk}584!AQ z;oGidUmLE;b z|Lb`i7*VZ(MMOTFKn@-f>7Uu=&=5Hn}(}T{Xi^ zh7j2fmQA_?Bl8W*N=JBIes`^!d>vkoXZQmm{f@vP83%2BUIUi0U_8o7?V zXb2vD_^F(?_Fg}0Qyn>KJ&$dR7t$b<)!u%A-<&>DR7FxG~!10P`^*YaU0id z;uUztB{1vd#JBt--;4!4x76J%}yM}7fc@OVbx-tlG06E^RwMf3RFo;TQm=V@Q@bR8pKe8canA9 zuwvsG=h8LnLvKQ?om}ak@2X`DX{y0p4}^~~1a3B3+Ph16Mv*jf_tilM8%+?4b>i9= zu#zQc*3}pPxic~=sjR^g^z*HoCUZQ89q0XctABEO+Uq+75$WoiPe0t7{H%aPb#fi! zl=8%Gz_$K;1DIJ)0-uoYGb6HXCca20f%3~;KlyL*dDb=)W`(ECX_j{q`@gV{l{U?} z;ik5gWPL!MYwjJsp%#$DK9*t+Es54Bi|Oz3QLXHlAqPXb1@g@aZX`ctFz%sNNaZY_)aM>o?l z9@iUws7E@SC))O4i)g1xCUjY@vxvWQO(A?ln1OvYmPKHDD96ogm_-wb&F73p4h{ZdIVSZ{kz-1g%rVkv>Z={P3C>G)t4nPmPqOPY{`AuTPTX-sP^ zO4!r{>zPAPyTOi4JkH%R4I~fi$Rx&+?a#i(Ne>n zVg;)?logK*)ZpCkjnIy0_4W7tQenN5^qHUbj2SPrM{ED)pEsl>hsHd`)_7pWIcVqz zYvoHCzQf6{flu>1XaUQ?v(}j%G4gmdDiz{zLD!zHUh_~_{$!l{anyE$+{H52!e3Rb zUT8dMl*&((DfAqIS0CnSy;p$(A-}BMIO}aP_YMKP8>SA|-pt5?p=0JZb#L&NGv8Xm zF!nDV8Z4iBqnW&lBQ}IXPzd!J7ON66O+g!*bh!czn?d{dmpl#l0CnT#Er{aE!~^F! zH*=R_gdcF=XQ<{9hOo4p#OxA9(pAvM+;)*FQOEWLy1^gBqoDR?gm)LG1<;(H39p^m z=7odz=~qp`1w~Gbl8SN?3)Cflqw+VIA<@v-za$qq7`L{nt{zA9I~p(=<}vcv5Q1rI^BCSLfX zMV^5-q9MJRw2Iaw`Apvo*J8n4oky`mq7XWt-?o!Ct?WM*JVO?`zHiscs(ZkHcu zOT<({d}NkI)rNmF@F5>xc}NE&s>)d;Fmm#8#cx+#Kt5Ufy7I?$<$elGa6oHZEy56( z=-k=1b(9(_cl0zXcqwL(oEG5iD=Qz8wzd`LhvWf)sIzN`Gia_9VrU3Nx)S*=_f(d> z;%odaBgB=Jce%s_)-%g7QG;dXE5=t8G$m9ot#cT9$HL@4GH2E_K&1$ZFyOM5RJC9R zNrZ8eZ!8p8uO6V&?>LHu6$^3p&Hr4dXm)iG&SWjr1Xc-P9!_VKQ%1UFCePOR0Z>zg z#qO=+n(J`v5D&(sCR8nY{`o{co4?5A4DWwKJli)j50>%`V?<#FdkwVBey5Y|vPI>8 zscKkqp>3L_7`5uLv%~E616{8?stJ0ST;#NDkI7WViF@-4=mX*V(_rd0P6sB;=|f;HtQ@pI6X( zY3QTk0b;K7zpgCLyvLV%kJEIup!=jdGl)pO!??A(pT7V9f(eGIveteggfp!>Jvywn zpl3A?w%AN{lL6nfB%f^6dOPrB_hkg8thEVp5JlLO|JNEgwygF`E%G$ts=NR8`OoLS z@ca%mvTe7`o!7^V%Vfb#r)#2l&{6N}T$vQ%*CuQ?i;Tz2XU|<}NJnN@T&A^*=`s7< z+R86=Ql`rpH?1i3nWk8}24|*-dhP?I`%L}(sVMzw?WpN8H?e4zkltkCq*glV&bL*U zr5DrEoRvQK+WXt>iVX0xH=LD1eM+57!{G3Sl<`USa(4Qj5qcY zczr70FqXV3bxKkRA&kZw?z&Ca(dTPvbmibLS)xEoJD$BdP&>X`MFwvD%)zdRa<#4N z9o;iSLm9A777ty$ZUO{)kU}i%hv9tM@BO7amRN(Ho?7 zxeO3~N2#Bayf*|=uAC1ye2HQHFnE|O?QY{pxQZp7-N3sT&a<~_VEbdb>8P|$H6EZH}H6Wh9=NUhiobHJ^d9D6Z$^|pllF|$i7Mc0xGTUEhv1{)(2GOa(dphz>eENR=$R(gP)N}M4 z`nIgR`fs(bFU386i7Q|9$|v=tY8G-#>X0D!Wc5cEsyMZp9!7z5>Hp&exC($9Z!U1u zI`(`ZT{RWAN}>VMU0&nrQdz+GE%)^{J9ECLcnRljJvNGntm zu=v|4{l%OZ4OAjt*csE@7R5;wr8!b&Ut&r_4QW%0V*Js2*jn0@g*OkBdbYA%{ZCm& zjUKCEdA)q};FdYRF2sIf z(#KVP#HrqlPgRd`7OJ$R9&2=|=~O}ldFGp@3*IUHmtJTC)IYsKfU|R+fNIb&R?it` zc4(p9kp(!(t*^qT_kf*?!v=P3ay&xDN(qLHAX0sdkaF5{wDO+JPl zx~GH^c5vc*Sym| zo6E$QTqnF$hwlU0Rv~arhH$6y>AP#etTC#5LXBhW%0_nJtffyo;U#XCyBP)C{;CvF zH@x2g zt}q~3h3h^1(&~cm^a(@E8_NriAG2``6dCsS&_)0WCY)-9d3tyaIGJ`>^5b}ofBLM< z<|A63=cp_i?uagEX&O24TGw7g3?$~lx-;qqE@$Nl6`eKR@!<~H&x=Rgfw*LV3aEmT zS05OhTy2b74yHU&TPhR>YAaH-w$w`~`=}j#OYQrb-w11GM)UpcTY2@w=ma&+!_ALX zF203pd})pp%aerQ^wlk0mYzCxKdIrE4s|iadgj4cUi13qkBB*kXQ_+?{|uu}(5CFN zZ_n=JV#hm`4e`>U7nTKiZ&h?UQnRzBINnYYI@aBe<0a?cr^KcXopbFdg#sppf=iK% z_n#*)L|~I|hzGyjAKl3VJ=~}SYAPL89~TSO%I>hkUc8f5 zA6N(pJCj;q6S4}Gnk4bWn*%?;gSR0E3|zDkZbQbq5-Kq_?7V(}b3xtJW$xj8L6D?MjsiT>Vt>z{Ua$UJU%#n<9Ge>buXY#gJ`sOjA3*zksqepy2 z+M2fehdY*;N_UnY$CGh`O0H3b{u|HZK4yi^15oiH@M`ED$sScoa{Nc9yhO5PT^umM zXD*fKjHmdr9q>X&3?-7tnp1OKi-0u29iNlaiLakl@Ggk=^DTIvI8V%o~X z&>V%MXUk~MMRNb`Bg~4gKFocDJ7Rcv&llbx-Rj-6z-GQj3befnXTtlBa^6r3iSTq& zmpDIxOCGTrP4SQ#au~BV@wPD3zW*$4(hr z+(EZ(U7*oE$GMp7H!Pz<*Ip_c;+k{KOER{Ic&f61i=G!*M9~!zQ>c~BV?Z{WmkaowGaIM^g zP+aR?{zaJx39?GxwY2^9SsmK@L7?qdLnZmrs|@|`iIUT)k+<+|(CUa0aTnsK%g^8Q z+3rre{bO%#8Qf0q9pW=s%)ymLQh<0;m-HMql8%>W}CHMeH0SQE!8*bmMe zQD$MJXRw~Gx8I8U@dBJu=x}{It<)$!NoEyZa^ACa=<*?jz48#!KC{eGmXrTB7j!eB zae3=1Cb!>rsnTlvNRk>Urek3!vke$R!_s{Zs{o7wMd>sR6s3iubBMbhnacgFGY|cB zh4Uf}#r5ItgBGdlW*eKo0ETKs*$%x}t3AOuHs{O^^|GFU&3pbma%riw^;RJWXK{K{ zz86>#dqgV~Sv_4dy_Du|vdRLXNNwAnAz6f{s$&DZOSVKgxp8+Ym@dyQ!2d&nh_Dm= zp|XUz1)k3}ZUX%19lPbUzmnVzS}p7Bq4r(GkpsuNjXfVkfPtZBNd14k{mgg&e^EP- z`8laEo?`R+XIjABHhlfukuCfMnT~OaW>7c}Hg03+b5glwq|%z%s&?u@P;!Vz&pqS@m~YvcR*VqBh3EQsEke~hp7qb1s^_bF}|#>DBnM-k)#FBlhT*Ce0T z+8Y6{7izTZSlUAPO2>3hJL_nAhFns?tpaWWiQJ!qE*frZMBO}r*FV!t!62NLZ#gO; zkJlBb9pd}V3^&jdhOws|%GP_Ix5w^0=-L~iv`@}NN2sI>7Zo!l+olyf0PvE664$(>}J-P8}_L+;`9=}&YX)jxJxd+ zA;*=eBjYz>+ozLY*j7a(#USF8*q!e>&vJi4W4&}yVVld88~RovuIn~y^i<$0X5*1qxFpqemd84-NvQ- z{=L&+X9+@_enjibv6v31w|7^6GrmPYetZ3+s?YFd{N?6<>|IR9aPV3c3n@5pR)SvK zb&z>iIN~!1OvNT}Ayyw~MQ=*sl+%w>cOYM|sV( zLykqnh1gOLCY_@o8F+qu0?L;YJ4(+}WeZk;>BQt@v|^)lj>WpoLq5fWw+*idAi@X= zNXjqxdit0+^f@N2%L{G@x~fw7kO4R|73J3#k?8U8r?^|&uM5Kq+#=krUx^hae-5v_ zDej-0HQR$uBR?6aTNaoH#ui*{T;h2=wmYI>HD#4cS3IG;+yfDUtHihtdF3Cj{}RsU z>Hm{(4zCRB1p^Dhs}=>^erUlEI>z}4CUIO*87qk z^L|wlm{w)K${mzv>-xM+%(FU1>)Hg0w-g(2IA0L+_+hK8Xne1h> zsW*s<&Yeff=tEZLWUPlz^!`6uKYhWFQ0(-8w{{%%M`xQ?RGfIQaiHZ*hMIvkdY=EJ z?tdPllr1F^NQwx?RBv(+nD>0|u#ATp5%z;t?pG3Y_3R1|#00$sB@jaFS|3S5X;YM~ z){O4~t@>83_F9w)BwAZ`n~*woNAT8cVmU;mbz%P(q~0ta;7(79ZT#*8p`va*G%;&$ zJAnSSjoEyVP1t%#?>b$G z=Ze72+FnPLZ*8S6&G;$xc_cA0V0cctGz+d`e&}W=nAN+oZIr={@1*q{d3iNu5YT2j zSPTM~tSS)2^{ambK6E#yqs1S-T5^jaNUmf=#T)1nL78dD3?0zzT_W4dv%T{GR(gpO zYL*!+0qQzc0mB8gR}5>Za$?L!)4L&=}=zq7z?@nUr$L1f5UtWc(7WNf!vI>Z+ z#{Qy9PtRWGst^9dMyrycoMzwj(^(Ime#i!0V~mTCW~L$XtnuxV-cG*4z=>ZuoQ+Ta zA2xkI3GT-)`c9tf+Yy#MmW9H3Om_Bq%4>rC0_CA5M!`%jGV3;PpB#KW?p%Y&fZO8+ za!h9#Z`x&ca!(SNz1X#?{F`30oKMWPP#wL>@y;H8KbjR0`oqVO`U&Ybsm*YiX1~(} zw@g+1Nadf_5>dfSvRVBWjw~OZMWx9(H*Wql44k~(lw2{*zz@3)>n_amI!I5qUj22; zRd(woVp|h^DdAHFY%_Ny9j2bg4~f+T;*%GM(d>-Z!L0LoI9n!9Ug%D4z4l@ejj0s; zo-b(DKnp_*N*w1p646NKb2sM@@A)+qIJxwWmSbh##Dx9_vc1oPTc-!en~7>}_~_m@ z^zZOD;w3`;Y9BO;eZ7h0h&=toi4wb~n5g9icdlNu=fvvI1Sl??bVNCY;TK^&ca#u_ z3IAVMa8T)4Za_XheD^;Y1nCe*(UK)Uiulouvg6rZz-Dl^J45quPo>Ti?<(yhX1X?j zBK?>?+G4%bGu{b+=s@+{kf{G|$FWwqh#+r;Ur+|<&IXf!___(snr`jV%jWdpaPkYQ z*k$m7GQ{*iEp|}YCvlO4CUN>w$>Z}w1`_(z=ZgzWd0s}mvHus94>tK)MR>N7?w+$5 zOnb{$CmqzhsEM{2{<#kGXh`GUxeh``}P02_?6ZHTej^2Of z=Ljvet1n5~>m2&yQl-i5q-ek55pLK+WQonhLi*`adG#Toupoa0BSrx$vng>*KIAa> ztz*S(xzKc${Oz_q+~e-Ap%%;A`In1%=d(cs^*OfikIY<%a^r1@|Jxs;8Z^-Ky=5Ev zz~e&Ulvb)E#!{1*KV^Wh2~h7XTzWQ0YiTrlGrQF|#&qx_X3V65&Sn^_Zl+_OWe=9H zIN7Z$(<|oImzf)foZmrJ(Om)997?(ZEa-_-2e7N*>2gNxS^x&1)WZC-##35Vaj7WG#oe&p(J zRaJ7p1p+4@j;JS8d-I^GP-gJUbY9=Z$ks?6-a- zuvwxecknQDSGi75=GwYUYzI0tSAQXGnVwSw4770NVzr3#^%#-b5hHz}pI!(n|Ko1? zv}g3_08*_ylzEeNqy(DKJFvrc+!YD3R+lR#`?RZW_9t) zcHk6%vUz<@wVtvJnA}qxIwHsb&fn5g1P5x)69sm01EKH!Pw^z= z>Y0p-r{Qmx`N3p5kbX-4rHz#T+d(6B)+rEhC)e8BkCuFUlQv%M3t+C1EIFQZcV&X? zd`R58#O`hT7Jhu?e7hxA9Wwo8NhRsT{leiCqL$!(4u0(#;0n&I(^Oo{D5q2iLyvr#kk=(TC5s;1&kduBktDgrpS$4Usf*j!@t>p&Smul?$im8X+Z~vdIIgrB zOds+?{lICzVdli^Pk~B&S4Z_D;SN3?vPtnfawTU6h77@Pcv|uT<%=4=#~o03#mkmT zbtKun{;4IC;V6!25QWt0!`*j}aeCX?w@j0@7rs9#g@~rMpmZ8ca<*xa18h;||uU?l= zbKb=$y(39hjB8yI)!GlNS*weIO(?-Tg`LVdjzDFvrA;yzGQkn_F7!1Ba?z9pfISQKGsL3rY z!3h_HQ-Fu)*#`boc$gD@`9vj@$ie{P$k9M%uFeJ3oPUw^C3%I-gSHuKiV;>SM0nU8 zo?|5wV^LRV*>X*y9}IpSXn7Loq-55gnA#_IvfJZD>Q4`odtiM5o%s%JR=A&Mq79ZA zpqAO;vQX#~)#iyk@`47QrL?7CGKv4Yn@EXv`F#HnI{nKOCl*gf+WM|bE@#v)Jhno< z&nwPz=qI*@zkNg%S(lvq(SCrag>ns{8GwAQNZdeyxQrE(wrlNNK(avJ4uXK$J8cNL z5!X<{mZ17IjXOx8^(ZFt)kxrcutl%3ZOScQEc`z2RQc$Zm&qMgqcN-c1LaCmmxOO) zFI_rRP!IOp?Xkbu?77j-!HwCsdF>_A2$k5J`+4FHxTF5*M8N8GYSQb&i%i;pqr?$2 zo=Rfn0~dZ0RJqH*35A zrasw>B*=uF8tXRI%$DBina1Z=>tJdZ>#M;2*d>QN=dlfz>9w+V2EKH8M_b{ya-t}e zh~nQ><#W@R;BYZs4#-t%LDavQOLiwSn|Jtki%^;VryQyOVIwk`fgCPAp@OPcztr57 z>~if}=xRSQG#?RdH$ijasS7LkJdU>)9=uoW*-hi2XB)ShI)wR8^!l9&PjI&Br`&R* z_vo$cb&&O|iJ@Z^aatz!FOU09*k%2%^ufDl)wqmdy8a(dM}tOUK7*}yFRvZUT1aW% z*QQ8&^sD`qb~8I^9*y-SoTUD!i4>j=GCN`Dk_8qSn^w>F3q4Ga>)WS*%%#}x8@3iJ zt?BPy^BVQS*IN9@!=1`?rY@xWF@|*D4!l$9}X)%#kh$eT;p8tR;|@{ul&Nd z+gYZPY5J}n&2*z+jAgY@bcrVDM_H;vDA#FId-piH3onHy&*)5xM#6lc%M=1E=T_JD%V9VVF2Q<9X}&;#QKrDMsC`p~ zET`+){PEoOHybDni~OgN8ZV;ggw0nn4i)uCK}5SguK9BWg8ca;YYgl8efs<35EQ|V zXYo_X*%?N;>DxLr{$uiy(f!lIRAZm$rUSV$u~S5RbEWj?sF2)Z`<-(5iW_TRgFe|j zocZ5&rR-%aPhn!ZV_9;5i@?G~#?q7Ut3C!Z^z`*UM#1SR#paAn3agIErFr`5FR2FA zfNSJ5cA0g*`=>r4lhnR~yyJq{oxJJacBlHI8Pk9^udi9Y@`n!EYN%)+S%zeBm_yXP z=@E$?4rGbae)z`UE?`b7`x?*TzFrG{6ym=m(}UCtt*z|Ub9*>n>yBJ(z3k_%8K8?V zdy)s^!xa2GmVdsWolnZ#Pqj&)9+V`^k7&Kx^=LI==|C%js5MsMmr;u;{sKjrGu2)Ht~KxK!0=Bb3df@JhNhH_lm~TkL<6;(>fd2~ybaA1{`k{tFhDrU+-aZ}#mLBssHyzEbry62MD3b0 z^By_%HdqhN15+$;^~K`0HJ9Y=f|-z8^-0(@x04MR}$p5UDX z8j&>deI-)Uz@}nzKFbY(=cTpGWM9%m@nq2t?JKJAaCnWTooppw*rB=a-PM~?x$yNa zA)rE@5KE&kQxJBL4z0HPcI9QVUGR%uN#JRDdZN!k#L_s2=kF5VP393FL=A2mG9LMb zA93WO?=-uPRoqpClAwfzqGqigoBD##4@ok@UrEob+~xDinzRZRAA9uj!I8TBDazEk zq0+#QT1RHh`yH`;4dg|*9sB+A@BCwe?M7mQcSQ-nMNUkwFm{Q-3QVp~!O>^b?)#_o zC62ZysM$Frgpge8T864}bPPWCDy zJD=U;{5<4R6G=EP2}#isMU4y>+$ zUmsaCs(n!PS{=84E+k72k$nyRaQ98mtM}=WW~jET_2%S$yry(+`Hp;YqCtteo@$x@ zDadYS6kK#GVqRKmB$=!`9M*asx|z%%a%B5hFBuOAG3)i|n6+{Q79}?NF)Ush9=|jr ziVy5k@?^Pr-a-VAKbhz;t?%nX)9rrZBQ3D$JXU3tN+f;|T_No?TKCYo*F9 zD66oRDJ|G9H@OAj+_n5yc~US+-K)4 zBL4lFSsU2A`a(YJtaOCs-BD4RT)2CFb5TOOzgoKM0eufcY&$opSZGg6>uea#)a|Bm zpRyJQ8I(;j?J^=S(6zyq-9%M?D#3_}+xP}-Ll=Wx8mf1)BAWQKbYPSiW)0(W2cLQe zpFEH-wi=Zyz(=1dZKTW+#iXMn<3-Bd18&+AV`E4ejADm!Eb)O@FMD!VkPmL%X`+dQ z)CBcQ3y(Ve^C;X0@CMP9l`VDtLWEdEWQ&qd+PHI{N#b zkeBFdn(n)c!fJ*5%lrhNuNQ0s;juS#b08VEZBfm$Wm{J!W>HR^%i{aWAzI>a&tr+B z(lc05@0x{!u^-7?qhI?Zia8TZrINoSV+-z)0~1JhnGt3c?#(SLevMzo2#G)zT(v(=Om5$J#2zAvvJOUWM)OtwE;UpV*rOk|`oflYr z`!UiJ=bG8#GSk=r5VBqoPw(ycWMib9>ec-DA1{E>^@ktw!3Fa?w6b9&h9X+t^vzZp zdTBVd-ES%mP+viOg?P0dfFp@biF4<1t=@I7$BjJm3G969zG69e&AS+K%QPb)xrH-N zrTicY7aQJ+pJWm>J~r#hm2)o*y3d)_mC{*)<50*;3s1Da8{|*AwzJ|G+-sn7*;fTH zD#icE{>i7_F^_tZ6q$@IY8$W8A7V1Dgt>?w?r+zLPvpulb-H9$RK zvzCh&zpuKuxfI?%MJN>GY6)H2=dZi^mK%9K9&6PXGY1pD&*VZu(&i%^|FpE>%_UTE zk@~U5gCOlvcl%&6SpCfPl+9Mx{0L`O;oCy9>!=l^$91$h>^`F2Y59obb5E)mIATC# zdo4fy#$Lqo7_GH179#FwOUw)lyz{x;2dCZWHAL5r1AkQt_L!OLTA+yo>*ZU#oTTfW z^h_9>jo0h<{HZ$Lp?E&-0^~v|8b-}F<89vsF~VD;n22Hw(9y!rm2+HsfD%E5%IXYuS$>D=8{nH=xg%S zo{I&Q2&D$}V(?uH;YPR#ru)UPwkP_nzp+_sNqf>@D70YBkZi9p&-Vj`X|r^2$#(x7 zk^g+CMtw@qxx*O=u^?#0VEM+l2sOPu*u!-A{>Q^~_nxNzyyG$9s-3b?GRK{O^rBl3 zvwb`1%}WQ^o}30fZ8=0MND`J>vK#guPp0w2OLzW*4#3kzhF3KM-&KOUV;Tp+_5B3t zQgW}2bNq2o!ybY+DEf-WXYcKi)a=L#>yb%2+sRJUjQjJnH=h;M z^HqPxf&)n?L$mo45S=g6sq1p?MC3kHOxs2O1_)1(<+FEI2EWbc)Mk{QeqASzJ~&8< znY8(%b<1Q4pHYBw+-kF4RofbWJbF8`+0!6UMM8E0fvgwnECMZW2i2wC%Ulq~ zI1cS}r}^N2=l&u+C1iL-K*H3tOChM!koUf&nPZ;l+VDJrhM)CLVID#WfNBLj)zSl3 zvfylzpG(i8hj-CE*V$;KZ~?f@One7>r6j$LQJPh&IyeNTD}NoX6uqUV7$!>8xc3bn zTY7~PcGa;q%3@aA=JZ|K=R#yUk8@AwPfEcw!{!S?bt{P@h0_M2$6R7NSXn<0;xe$0 zYj*jo2{ndR4YmSQ9th4!(_NoZ)Su9rS4ypq5%cHvKb|D8#${L~Qx)?OiHPn9JuaMK zm9e)x*jAI2V=863RvmOQkM!R*L%jpTSYUsZ01Phj2FfjHx$tB}O;Hxn=}eSC@TdJ8 z{&|qn&tMIj8&bNUkl`=F*sd9!;e<~kGXmj%!J)i-6sez za@OhAJP%Llgc0aK*AHzH=5n@#K0eZ-qcju);4mhENVzLE7t^r!>_F!^T8;a$=BuF;)( zX0gT&5Rx`zs#e6Lv3VrC4(A5<5b=&bLrS@wH8+{|-SulZ!W;2US-HOmWNau-e<|5p zVYSWCZl=62-;)eyhm-cbEqjHfT|&(cKZmBtnGEZWi-0f;oogv2RBNxD?7aPu$k+^z zOT#Nxr#muUmOPqpTz2l6toHt9&^<0F58NU!?ca#mw!NG;d9xRu);v*(?9=GmzeL6( zQK=j9S?axwbME;Ko@4*jc^~M2JZ}3v&4j_|>-`e11QL`q?b7^?^u*M4mQ*R?U-!uG%wwJQhW;z;#_rEZ=RRE18^P_Ev@Hbu{-j!qZ z+jHb3;X!$1pW8cDgz~pFACt~cg5S7Wlb0N2SVzo41P|^VvN#W3eOC?=pJ9f^@;6Uk zlKR&1cdVuJ@4Q`=0+V_vz9u1RlW9{W*qb*cUL`8-7(&iH6CTTP)iQrlY_>R^S2S%_ z^E+ko*@Ldm-X5yK9GMMowqUO`fJIsaXsn`R(%E_}=;bP57hKZdysI@C z(UoRs#kDj4zBXaYJxnvN9Vfaq5eS}T({VMdl&s_CGgDD?N zLOMFmQTW;D24_o>8=-Gfgv^TmS%vVc1I78XTQ8L32SR~f-hQhg@R=#!ksE55GXIP3 zFHfjtc28AVw6kW<-Mrrd5RgYAYn_BMz?9)f8bb}G=B4910|rmN+dY#Aa#A+@$J!M2Dg-uDml-kF@V{)_LB=hlM*(t5W_hw1vZ?d~aUffxhPI(FY86#_X?nI`*(xWbWSpIzbL@34^ zpUBo@RD2|MaovuL1)*hAzzl(^s-?=hkIswu7%qh&GaTyd#`+?FHsg$1D z^(zX`8IKBe&6Gk#P8uIPevD<*q7>%rLKMByJMetnA;pXhsP&Gt=Su++`4|57whwM^ z5)I)$&Z%@W+J^U4m>O|N;>37&n=e#rcjEHAZi zrp4f>Te(9sI*Q&eZQ6}4wv-`dT%)#U*ZstAj5&=u8H3VcCJwzLsMx3UHS>IV^ zu(92uDSB{z^Q9)Vue|}aD3Q;I#sPO_YL6vIL10)s+$*VIoh#`%>_zsz-$V|sh{E%C zVA(@hw0X8? zB)b&mS+{$O%ydPY;>n(h&K~{T|75)4Ks0LVR6)eer71Rds@W8Lg0Y;B;@LVKHCQZT z1+uZUbz#!z&GI;{eYq<&;(fBe0`ML9=AJ~mhk~HB^g>^ALVvEqx%iM^dw5M>HnV>z zO%b~WQRQ@&+Z^7z&@NRi9Ly!A;Kyi|_;xT7l{oAPS`%wa;vnOJ-QBD?yY-mFfH~9W zYgqi!o%l+*^F!WMMP@5lwd(^X~AsGRid0J_82hIK= zonvcMoAu-GDT{7tcUv{D1g3HuP%6y&qO6}bs~O|Ls`u5+(K7p+7YSR=2lf1ajP%Z~ zgYSMfZ7$7%HR`UQ>wLNq;c>`ET}Hbm@oc$f9{J*`gsBo_=}L6j+px9WVkEe?Lr!>?`hV7^Y z?A$S+VJAfS?#>wm0sb`*0(bYyF5=J)881kl4--wC&!GQ>6oT7Y?s z$KqwFQl96D&av^VseN;i&9$F@qvqIIJ5=%WW(sz)>vhxUnYkwY9?$a>ulHe#h55{V zNe@To$b*As+BtHvk56}(rSDlPO zE0v4#Xia97H*bk$*86@I<#;Wqa~#mA0<&KGIWN}^YAiLQfD_hO@fe3>H}@2bcVECL zUUQ%IGu#I?`|&(KsSq*Gz!M8*2+V^{DJ<1VBZvdG1B%^4 zOSC=oZZE2>^ROq^)Ptm+sv2{d{33&c_Sxqh0ckt3RDJ$CZc27LAUvMntulbEgUV9i zdw%n6U7O!rGXf9k%mJ~+^y#yvNSa#b7-GU&9?dE{RcgFVTL*Ck=4g=@E|6*{ta%`{`hSyZP{xn?R1+FG(fXfu;Hy7AjL-AF@zoX$&v;|u+4=IpQw9b z@^Jq*fn@@n+dMGoV<0Ko8A-9Tu@zSv{E|pi)7*^_D=F&{PA)N?VXZS2%o(~77Dz$^ zb9t^rxy*FwYDk}l4VY+MsLsfddp&?4_bL~9{PUHW{kh?=JHsk$@js=C zTIueZD-|Y#ZMem%5ub~FhgNQ;v2q0=iOMUY<&iu>&ghGPl&%S5I%k&)VwNx5qMrDj zMC}|+x!6g(rC>-;$UY%671yLB4r|i0UJ+DSFb3k zqCb-zq|4BqkU?ps?6_EzU|a+9RdB&)vuVLbf~V`{z0*s({dtnT4+^vcd8o+Ekk)eq5?lRIo^=+Bq}# zyjad>tu#RIo05^AmwOXzsjkBHlh9tAXiy>SM*ipJU`8TBD}r3J?b+=w?6QMn-+Ei} zuO$Ij*nUMc4XGYxPpY|6M-bP~o|4LfyF(gwMyFjlOfta7l~#1f97mluhx2aKy}ynl zV1Sko9)CjCH2uR2GS2osYRgd9#oXjPQjz)L?AyOv5vduS<_B_8#Po>BoxsVwM3&`X zG5J-Lv!koVc~u-)#BxCb!sbU2l*u>5f`k0=;qw<3r0UMNC6ZS{IuvTp31WJ#&_=Zx zsiJ;We)q*+Y%ORYBlI(43nhl@{U_(0A}wcATb>|0t3iGD;P;G?YerUu6ND2yXpal~ zgIdjSbhBmHGkFF#e4+J<**V>prOgKCsZA-#9g1-|-K?57(|)v-_S1SQ8^Vb`UfHyV z{X_w#rt(kE@gTh}e(a0z{kgCLB~GbB1R&r0Q(Ru*#_105txBry9rE?w)H3nF7cV-;LqhQo`F250O5TIXM=2C>QSWMo8E!d2>a|uvgYy zx>;)}VEb-Pn68p3bP7d5G*YqPWVc(zN!;}`4BSdRaIfV+V|TbcI%`dP9We04iH5%U8J54jMJ_g&!5)>=ZdE||5M^tvqqF1v3JRW3qSn~ zm`K{=KJ4~RiqdmZB>H5I%u_Dek(ZS^0UUSgV$`^O@$}Hpn>w!6mz9bcTxz2zZzJ|`F@PUqowRclvp2xK`fNXJ(#tQ(IA4evcAE+_ni_N=|#A;Dp` zEV8|Vqg!0~3)a$6u;6AMZ+Q{z>!Q#>C<{>e0)-`^z5~{9Qq|g<5ZKd9=JG-b8iSwb z%;mlIN4OwyHKo0(+8NP{Ph7ee+y$Zb#yzeEc0c85vhTK?PP+iU z9)RF#c+(v=_}v_^&NCUSh>HtnDQDX~`2$?in{j#ri~atdd;~hj#q{iq=`?wO5-Vpei|W6zy_pZ(i(0j6Z#=t-CGZr;eSFf3Axh%AL$Ps%{-VACGeOF>I zS*8CyccIfd-4nuo^F%hs`Dhec3^xnRMkpLXUy^rKKRC~Y<{kX=aPB2vEUEX}Ryk$; z(o{Q6{w%G$;A*Klgu_YcVALFh)+qSzcUxytnBXYN%Bq#%0KNi~_qD(HZ zJrMGq;`5)eRe#z;St9vf!Tt)8Na+v(^va#-pU~|S(_qXnSh|N&FlPn3h1+~t9X*g} zO~SZ@h~(p)FLzr!WbhbLeOn`EMr8P+03m%cbTwX zbd!#5(P-6-#2t;kSaP~pB1wJO6ujM04RLdQ2>%Gkn!YM<5v-@`)yw=Ah&}^`({n9k zXn2q7&YJyWGR}A?l4tj-%K7sc7khwi)#$xBp}qB86npEVP?|E)l)r`$66j`)RBM=_U;kIU}%MGW9q>tGuDL(JQ~G#YR7Ya|u$qB_eP zT$i)Jw7~D)K3}<9ayx3KHO(Cj@3psnRaX5Rb>%;G=bNUtdH_lMo!jvV$>`6n$v3hjMD?8hUr}H7 zb`I4f{_1_>e_92= zH`GIRxZuXEIrP?Wg;4SE_7arEUVx~hD&k>kuRq07-r~2U+(9u&HJ*vJVXk<|YPNQ- z<00QKH6!D{-Xey%htN~bAVt78cZT(7w(Ip=mD6p9EqsS`(7u-REj;j`2p9jmyOY`w z1lkS*6OX}xril9@eyx;) zJI{5!%rk=&?uN?)CHSP`IzXWQkNp1)TjY{wE1w{IIO>5%X?v!@zA7WYX8&o zk6=-G!rDC7sfIWkYqkz0^*N8hc{4l%<6y#wn_Xt^xC_=4hOYBw3`H zw&)m6bah8=7Kfxb{pMPC;2wQ-o>OS;N`d-^fjXo`l9+o2m|O^_i)#?BZ+zPUWy*US=#E3_+xJz}<(%x)irmcW!#&;HgD8Vz40kbh9f) z)Q-q4Qeycag=RS#Al3Al`)3u)gtw#>`0^86tfTmj6TOqdc{jf|CA{ubc8_ksWC;FU z+ZY(g-izLGh$x#!!kW=vwlWqjfd{OlNNH>@Tn-K*?ij9v|64g&eG>#H$dj5;Ti^Ep zags#|dY$)VrTsIRRCsqTE@~k55q`3UAaXLV717Wi&3gK`F5!yU$Q71yUjf3+v3(2r z%w}GW9AN1rT&Q^O<*+Xh`BS7B`xk{d^Ky1ym(QWt}QuUwsoYQsLf6Ngo^)es3B7YVEaI{t`a5U5ec-UM+*y3|LIgx zm^zQ$8PtmS)XWqWOeecPQ;5_q8P@Mm`u^{V=z=a=1b-NDEu<`p0%Rd4ymA?4;4*Z$ zyR=N1r9O2J1@V-m!c|A1SoG`6p-C$Iz=h46w$5@>{whs2vzo}^6kZ!0ul{2>evDr>vkI7U0CIC2#=MQ|%6~H)A1sdid(rjF zGYlPbT1#nsdWli~5pO>`7H|lSU~>(lp}%o&YHS8Q!>n$!l-9ipg^tB(wml(ELXU*n z@($%Iu|<(z=x1@TqMwvCk+UoCvR^v12_33)GKF_dWkfIwUwP1#qB_gkarRqq3%Ff| zrWMaSzR5@%#+(r?JSso+TCjUECrKiN={m=cy-w7-qR6m}bffJvQ>g$C=a7>#7XIl6f}QIO5<2=# ztle(JeWmaw)q4_@y#-{L4jYqv(eV8LkuTEJ3i&F6^drJbYg;h7zc))6O%)>RO1ObR z6?B&BHQ5~-Giw}fdL1#aB&9i&WQ=Sh57`HE+`HUUV-6oZ?pIyi!erAUpK3WKI#cWH zH|Nn_0#bx8xrKT%lT7X&*qT9$x9Buh`8(O2#==;L)PDnmqAZ1EN<-E@N}SyFqzrCL z+6(_{=DgUs>4GyHEOUVuJkbocN=GbO1yNg(A(77Cyr{w1~vm<Vf>z&}1_`vKh9fK1zXu>q|hIx@4a1`43*dGj}5()KB;HzQqLu9Is8&b?>{C zf{R$t4~`&lB|@+q5Bj#`Y%~HaxZYqZg;TwOgV@K0$2b`Yqh;x{ zb5aq5l7r7Ve8lGRXxOXuIu0nF-X;Ty{x3QCauoY2}1Z&e#~5j!6LYrfeV`P*EJgiAh-k+wv%G(%&w#D?Xi#$k1Dq z&oMn&ucti#aZLY>vI(=p$x145=@yBHM_~b1L`Q9Ah>$R{OLrgk&{?@XF6N$4G!cA7 zAGXp`QtWe2ct}|=EY(jc{|t@3LOFdO$&n~ zZVkOg5yHzxaMK?mm?n(V(8VDdq!#&lvvwh@i%mPgI)2sQ#q8$wvQLNz1-vF*)XFg| zivNu{3*Cr-U4>ja@6=DnXFo!!YfMZ~Qf}GL3tvBa2$*aCz5PvxW>zA3TrY&{Ih>M$ zpJKyWGt}rsj?75+KfTGp;N%Xry2=nRaY7+?c+K)D%`%*Nt6Sd6w0d1<;km3|LjNw8 zW$H$T4(7O$>*d#*@*)(@$cZmb3W^m!lA!TZJ85pF<}1Oh7^GTT@o*!n$HFn!f7S1p zC0?|5dhit_{1=2^>UE=TMjcaA2{~9r`0#4K1>epHIYnD3{Jp}=7POM&VxVdwUr$pD zEi)wD^CaD!eg3R1gb3vR`8&#fGrfXFQ9XSqw*c!@-J{GhI8nU*%n$9`m>)R4Ae!Dk zwd*R|4HR*W`0Y!SYA<^UgyM5VLh~FE9KFv%?ow{#-eAT_{que{HKHH?GfQQYn)HLt zKj7oH&-$CB_i%mtCAInkQcj`iLH-@%AlXp+hKM*1F&kgy6@7dyjYZyeDJhP}6-m}h z3Qn}vEu8>Bkp4o{q9Hg=n$C!~At9xu?cKW~rdS9W z!k`;C@K|rJfy`;)n3p=_7mUlJrLaxypx^h=+Jdyb3}1ZM=HauyHNrpMG|$xD8aH=I za`&!AeZ+;~W6qTACyUrNF~TNt3?Ym+VIz zQEKpQkaLadVhvG!?i5p}k4BrrQ9Jq{I;vOYKqRGnN1QLe2gn8TqM8SHo~{oRP%dZ8 z#ax8=!7Wh@#Wg%C$+A`QpsJM+?JSFZhakU88)V7xo#)=b%@as6Vsk;ke>GSi=_Xuo zc)4wvU96RH_lb4#zIqFasHnxkeAJ~~n{A05VQ;M#!ub z0`Ml%wfcWIY9%V+K*Yep@%>aJYZftoOfCxQZ0w0v84f=D&gMM?G>t( zwS)i4UsToJJpf&N&#^7Yee%Lq`GV`zs;;~@ribQm{7do1?(ga02|?qt|9JM&qQ5%f zw$i8Sia})zN@gKv#u6En!h)QvILi@Hz7r#{GrlKjYR;G{(+% zD8Oj3yPqS!w-Nbb{&T|dRPGi6z$U~%k=RlO_4B`l!JAtNzLl6zRJ2BA!kBd2N^rCxz_Q7Kr zEuDB+Imnpp_P++lw2`f(#tS3poWfN2<-1F}_z|bj>$P@Wmouwy66FghsIOtH0nbgw zg-U=ofk#2U;uWBq!^*bwd%phK<*__uSu|Etw9unoa(!V>!UmlZXbaxWsTJBgyibh>{yJM-$vf;9SP6WwA8`89fV_YvQwBLajGy>mQ&8h_~)LclLbZh?Be-me#`q%N`F;@c-f;`zv+VKL_swNc3QDdH3cy6LrHSS=$SB_jdTqcLoQ}F@ zeWNA)_1>1)8js%&-f!R#pS(Za-!~zT zrh}#`)aLR;Y{5|fh@Ta_8p9q^ugN6z=@(P2$KLUAJXMhgy9>6|L<+F|iHi;rA5O2q zSkujT^^wVvQ9W1`RMT1yhjalUq0Vk-d+kMX@|KXo5Q)=yw-4d0sRqy0I#<8&9{(`b z<|Er#xesJJKu_}Xb+ftBp8a}I2ir*4P3Ed;#1<|?K;&DOPG<>fZTsM4f%yUGr&bfj zHC)&DfgOr_U^D*{TCx}MT4T}p?eWh`f&x%fw!!QZVYT8fasS?LGb@O~uf&Jn34cgF z0=T38m0Vzlog<>?|D6TRrMETQpzsHDDJ$5Wb!YP>j1jR{joAP|8{S`S1~<0sw5J5||!K~!5Q$h*|sY#;_}joV(lf-@#cB;50XqTIzgN=tXN$tx%Q69*(IPG3^-J`=7hU7kJ$lDrsA947^{?TH9IdM0`=S|J}jV$?0Zb=a`d5ongj;+c`# z9-+0rMbJ`9B5&*JAOKW@ntB?9842X%{pRYMFins0>p?Xj?`YiZ&a>6Y=ZbLnJ?5o2 z-4+DXRjkvn0_Is`pt$wnIWJR!7L8%>3fW3d^hmfy#Ey*#KfK2^qrfNSM0;pu^8y4URNmEK>gFcjzZ+B(F#4~&m9n@f*Wl{dCXK@suzJwV#t3IP zr^T4=P`xNkUHFwLNO75SVq?h#?kd5)ucC&B=59$y1o5=Yh~0s7B37Y~+8}JDFGe=6 zYCV5(M6)F(=L@r%2&5WiH>@ws)r5Kv8~fF=pgSuqLp0o3@cW3-&;p%3@_nsc39IBZ zc>TxIt)C06P-gP5`1Hb@87%h2ja+MP!1;*sjTC&y^O*+x`;0v~1^+Z}liKsagy5^; z-?Mx-SK~N96yzl$M)m3^(o{-soFK@x-Zrz;TGOkvx=AJLlG+F%q;IJgzRB^H);}4W zY9zv8BXZGKhTG&U>bi)@gBFV@q=r=+UbdY{u~iQRTbhEk$=^7=n~h=KMBDvXDG>*1 zCA=l1zqCX%&SF7id4oH2KeqzHFzBklV7x$|J$bAZuJ19IbuWh7lLE`JuKV6>AvI*F zcg?UYbxOo=Z$AR6(G#V0Z^S26)-2^?=V6#(r6IAElO!(zW|g&>0nh3s-q-+vY|^ei zsq-ETx5c9{=ovecOq)I|IrhzX7MT3n!az72DSmUck;w4Sh2>hNstylC%hfyI_OmdG zbyobby257L;d5TQ|IRtJyH(F~?SjQC;0s1zGB{7TC7W*%D%Ea`^nRS>8%}J@!bi?v z?^!l>rCt+O;!B@-TOI$Q{4R#wZI~Z>@R-Anho>uA#G!)6(=F@VtWx(fzNI-mP8+Sd z*uIk$-1owX&f!~#iFC#80_v0f!jaZzGKbD`am9qzbN@G=171pp>WES(`zOZaq&C6U zd?XI=5gc{6#<^w3^+l-8T(Jos{(68wYES+flUjtsdG)G`fXmAJlwe~nnIT}`>JMB# zaHLNc{zfwmY*qgjW_q|b*w2Li)>5DdPVwp;KuWv>xd`vt4@Iobsl#+bC7J1vwJD+*|g>r~pp3=WcS0l_fNLXaQMQ2zeU}5z;eurPO6P zc8`NkZ#k&4C4`Qh3qnuWWPWs?MyVnNOQ5hj1RKG z@j6eih9)iEMXEZ50>0foKlE`1v1!_VXF{I)7lP?$rZG`yWe?pn&FmwhVgJ6JH$k`< zfH1t=ICiFhS40*FX#Y~~!IXlZW=1 z=+0xYWz=Vsdxcap9BYJ}@*PFhKH&;TDD|!p@LU?-jRoDDbsU`xv3rtKSLoio6~CtF zo~|AloDjdSdEV_&U4`!+F|RIm8wtaHwa(FL_7+=iNg8jdT6K1#%CI93;PzDc&sKm1 z;1J@SF43CJb&Yz-lOYwsnnojz`WsEUR!wLNdp0}vic=J15OVv`S_^}T`bk+d9rjHZ zQFK+>A7bliz$c>N%7YJBtbHzsKa(n7OHWhfWy?~ithg}kEstD;0Rb4Q%m z{Cc||=?^}>=i-UfX4uW*Sxl}>vF%=n`3wcTH;+#h`n=?X9|jSo-Qi=LTD0Ovd;w71 zET+-SfB4Zq&caL`6UMQt~hjebrg$xSuB&8NKO@~!1 zu@8o{C-oR^mOL}u#pZ$z8SC6v^j^1tCfZ4fr77X!D0RDj;bbyIJgevK3*G>mcN^X2C#+NAnZ4B$xE1+5@vOljwR>ztf++upFL{1qdCq)hG-0+6^c&YbeSI$VW- z%D54%878>d$3|9Ew>OMqtWBS%(!%vtK4YJ}PS+|!=IKU8 zkfdCs6ZD5=M1S!P#OkqZz+ejB;QQ&3ZnKvItJV6?3!Wnxv*v=6E&h@xXF0<{4Aip)_N+6m(`PFLTV1Y#yTvW1{t*Jb3}xH&m|yTL50TmCoo6}L zsAGRRu?-N$`|1w3dR6FcfVeI!#aiB zawgQUga7YZ-M-u7)JtnwdAYCo%>c~mp17=EalR*w!V$zwc2ES;&F)@Np7o>S2~B}A zf*HsvJ7=r6*?6NjA!Jqd@%%)V_q_PiGnEa z!;_mHH+{#Ko7M(xHo-bXNo=fXhx2y%O7JtZW3&;%S^|e@BD5bky%1#iKEhwfN=*9A zz!f#`ikWw9g9G748phRSXwknQ5}o&^qQd}e&l`w|2Ju=pI+^Dn2IPs`e!r;1*BrfWbWghI{A_UVIVyRi%Nh~%Sd=`jyA^4i}HVmfPxygJCIp)Pk4yU6y}1%D2W(*sa<0sN6R8t3;0>SYbNsiAK6)f#;`^?)fPj^d#YitQDuN=X1 z8IOstwh}u_`U~SlxKXGgg+S)tK$X=N987D9=t}v6X^D2tQSpXZ(btZiYNn1;l&ka1 z8ZpC_MEcaXAzNZn*2-|VR8*3(IG>CQAYb1@mQqhz)xUotAvOlJsT$6(Ow=2@K%SZ7 zl@o<0U0=ifbTUOBy_cI~`UtZ50R4+A0?{PQdy$2Vx?s(!2%IVM%{$Tz9fuI;W#Fo+R9(PMOw7H7HiC;V+S06n zymS@_=9HjShAGlkzu3+URNC^I;l*_vGDYKDVcbKj>qJjcXNPxjsfdt1 zZ~=JB1-HQABmF^r7z}?o2$%M9N}1bUyaN7%UcDjpSlD6TPeVZlpeekFLlmns)7pzt zrMsxo)d;fN!~>hZL<&HBRV-doL2cl2ZHK<@4>tyV2Ib4Wis~)bUd$u{hm1FC=O{MrXx}DM6kx|RWUr=W5`ZC)1qv`dVyt`kud>2U3%%fuD%Ov7x1}@C(^EoD zj6UfsILY+5t4k6~9`SsgG+sqocPa1TDr##!PDynp6m3-(zs(do&&Sx`z@q9~2&%#_ zk_7beB*mLa7c33?PzVrI22$+0=8w$e`_CM|VE zgUw*}AL}r|hkc(9N+Tf`n6|%L^$mvDRz&H#C0v6bYqnEjPZVX~=HjS=-bS=zmML^t zqXLO#I@mTZAsc2YHthQc+tUkKB#_=Lt@X1JH%GJz*@Yu>V*ISUFtEAy77;vYRk|%UDnRBb87?3Zp4&@~1L4GxJw2=t zs(N)~RN&@OYTBC+7A&LdSAOim)ZMS!86tzvbTD>QW8!PJqEqwx!}e6uG{#EYqdXV+jnhG$gs%Qe%)Ymuzjl&d%* z;cmTr_ab&n2<~x>{B#)5iXg+daBi{Q$^Jq45V#>RKdY5~ zpO@V2(l5Fz^c3o2b4GXCM5WbmLyUfbO7qTw1nnfVPw_(n4qnHtA2Cfgp3YY6$mF1C z$aGgf(eP(+dMbK7dtl$_o?IuDRRKN^#g+_Bn(z!Uy;?lYr!UFL5n^D~0c}SZzpZ_DRW$=0b zFTD+bLi)!dey|b>{YQioD)kJa1ggpx#vT^i?s$}wJS2#tXtI^%b}CF<_+)qxuma7q z5XVdG(h>3`)eu~NY94%}r}$TUWUW$Q08%1#mm~!7e4KqJez=CgT~_GUb77!2x)W6f zhT*m$)Hd&2Woqjs;>1gFXl#*O8y@>Y_{T{?1?4tLN|}%?7QCHey-TAt*Y~ahxS;4z zMPd8ZOMS{hdwtRc#Zs)-wW|2(WVQUM!Xpza(^VCGRR7FsSAu%YByI7r7R zID|P21O?0EqZq^bDIZ~<{EV`kc-Y~H_?hRZPs^n-09c&FV)?!lXJ%voOYqLRoL*ND zMmSwg!3^jF-M#p+OpY)NZMavK$55ir|MJV+(Y;3}5k%S%_kA*cYP5Pou%%~8|8YG< zQgmsr^~G=ot?3b#g0qb9rJ2FS&|Kql7kfYi8VAqw|G$Enu@1pMA1FTrC<*%O{-EXk>lC zW7J~E9ZdfJ-?>BJA_)eFs(iL8c)*5@#&yNMDA>w-VzF;uGHwq=DfO+hd5^!dB*4p%CCPSW z@7Z4y(qGe1Xs_;M3Xsj?z*tK*Hk{MP-JimkRAH)cXTf#&Nf}Cm_ll(XTMVr9;KM3b zQA9G za}rfO&8>VVk=z3Wu+y2|5x!M9^ z1y!pb2fr}mae%5+!U3ktTU(8GRlsf=5xmjVseBKs_zH7`Qg7jY@QHp;cy+WP*j^K` zdF05m_1Ht{X0X4&s*);cJ1X2irig>f>ADFm+X+Vywl~lYCUA;BB1zv+B1(Fw@~UNM zFOWxZZkSh*0Cgw;aTi+0!joZeY_OZn_5MXA{*N09oDu_)D3LgqqcIr9h0V^X8oLzw z?dDK2wh#9edH0%0e4t{;tqgM+B?$MGXtMnrv;oM!g&-Uze|D0^l=z*U7i+8 z8#5!x9mHqYEy3TnnqgA2zxveo%IX8T%hS)r;@LGDJ)W(_FD*TwJsakDu@K))D3`++ z@C?DqD`ZRQZ|q}JvWl!zP7eo%VBVHzghdYV@|Eec``oXcf44fZvu6O@YTJ*0AbfEpCrvTVJa6ro@`8 zH4VK+{d3)bkDc?KSrZ5Ch(xU7oJ;U6px}y2T0Wa65CObO$YbQV?uo&<5i!MRn6XJP z8%+`5&!P+M6dfMzVGorK4v(D_H?hht!)_pcYyhA4TxLb%9Cm=>DE`%z;b`s zFL!A@m06n-duJG_X5=5c3*tKFib+Qw%tK#N)2xd#*OzzkLSc%K@fr8OV?-g@HAIX#~FIo;G=(mS^#*= zDt~TCVxi4xEmrsg`K*`lg$ObVt*sj+e(B^1BF6<4We*Fu81k7!+Hq;tp09BkAKShF=rDdu{D$#XqvgF4$;t^x%XZmwIJyCVs8K3FY7!??aAK=6q2Y@_mfd5y z8|^)GUO8J8H*tycE_9pGGY((A_#TNR$uKD~o#8hJqkg>k-Z_>P|D&1whLar@jPGBR zQ*1y9S$F=93zI(j9ro+ptxYPJ_R8X}8cm0BuQ8uYb5nG1CAC*Fwwg0&b^|mLEc2keN96wmT{e*aj$Ai<5B=8f?2mIuMJz%G zp@5;&RfCfV`njeAUu=_VLi`Udnyxqm@A#hpYD4O)oEcdttzbAGxu~ z9K*((@3+&3l3tRtL^oQ|di;UZRW?EP(kE!W&N?HK`Ux!=E(4GvNm8M6$tNc6F!eYH z&H3EwcWm2N&8c}S_@|>L?=)T~$b8Z_NW@-JD6cNMCugpo6fxSzSl@mRDZ&6+DuCt*efHGly?4 zhKYz3d@)<9J^@@YlvgW4LYrABS;^=zIEERz^M{Lp*Bg}DHha2?9N-!mj4S6bvZ)Ks zozJu?mZ%i$)G*0T@yd%nuX@`xS+jY2aI9L4T#z4iZuFkZQc7R33|XkylUjWfjA%?G z>t(B2Ii4=9&E>P`B-7%xx%ZZy;vZHDioj$XQ8p8v`2&rPd9ujkf(1L%oRLeqh{#*R zz{=R(pv*MXJ}=8-R*GS&p&>S1Dx<`0PR)_%2)if2p5aAh2X@WuuVoRM0dY~P@9N65 z)oy*_8nP5p8Cp>Wrw@_4kX8bPV3gYDK2<66;n1NSF2T+x!2*yU5gS@O?L&MT-%>#HaO10}hfu2L} zZA3&R*zCx9R8^-wUHzMf5`_EeTKMQY`M#2q_b(&fCvt$l4x)7pc_ZE3E7A2v)ayj5 z=}7JK+}iSw!fc;pdkJkGEz-IJ^-dvUIKGw@Uec7&M<`H@ymGU}zwEpVgE4RW}OM^c_>N=g(5(pwg?tvQQ?0_h`=9G=8Y78M}&>gFaLjA7E4kvMO5vcy3eV| zBCX|EGn-C487M08cdAo80{2TXqO$j+6Nrr{?iCaYa3UOkY4ok0aA7?a{N}i^&`~oU zl`Rc>E2mJD-~K1GsGf7e4$OxJryqku%HFJiFC=|YXY!tyA&ziV2%R#yZ`!O=a{f^q z6n*TPT>U3l?~9~Rj`IZiufPtD7*~{DP;0M7TS*wC=4EB+k**T4{QnlCUQwH_ugPXd za<#Dk(kIJ6qWuw&{%C8izOfp>mrq_F(;oQa0+p?ds$7Y^3ugr(Cp~tvF7?Qvp)Jj@ ziZz1QK&{{C8Hp10&mumhy3{*c=CMsxNTaGHu-94Uq_l54!T;RcnGmK7$Pwp|Pv+jx zZ98r6wNT#3=siY1qR+r#@8-*V$xN!y{SdT0Y2mTT77^rgAOqB|V#Mtl>^D)i(}X>3 zh;Teh_MaoqlF4MZnS9i!{8H*#VDVzSGmOngA$&a{PgaD>5`HY+pMXf`eu3kj^sMpX zwkc*BhTXUPtYc!i-pCI$;nvLFRl?)0)rMSFJDdLz%^a3#_9jp@0+F*V&@mImfz$Gz(=!DVJGdCT_E5Gy89!l5;uRN~_3@ut_~V~VwSS-<+*GVt^SuS#pqzDc z?et>`Nzq5Nax$*23b0#R5~a}E_daFXJ{^=O)#1U-Q-Unu5=85I7+lmcUz7}1e^}a0 z`~hHDTHwR+Fwhz|R2$>M!J9jUVZQlG?*%wG9gj>O&FzwKa^o==@i;SCPuvo{%_)LT zDmMXS&p_EWzG4cwU68x5f7BOhK=7S38d&6X?H0;eWeC;V*HhNPnkZ67e|&j=ltV6Vpg2r0ymSO)%f;3O`^RR9*E~{OjHeT2U8{{%dtQeuA7s+$fxws z<+QrLV0EO8u5z$DWHFUgm<1KBk9R%cmC?R4Hn_HXA7dXxj&||6RMSWDU`jq`N72so zJP@e|Bjhm8aW(~38<8IyH11ESW$rtXlkE$A*ZZWmom!60C~lobqu(#g{z8zUa!knq zq>*`ZE-vjH=%WHxou#FAX%}6RrWi?557srl28HZ(1omU3Q#mH8LXQ&fT6AgU7+{OF zEGWJ#vNP*E}Amh^Iwv9Dz_v$ys-Teq=3fo0Y7ZBX*aRa5@tM3 z;p@K0!*v>;yDh<2GR2OPz#bN9y&J0LzR`V`i2AhE%OKi0KP0N_NCcj{N@&seVHnNy92VA>`C;lm zQXKLU5lfHIXuYsFVc1NWY9v@4?2ORsFCAPTI{@{I>Ki^v)Iw6r#p7@CSe2c&_{-1& zGfJ7%J~rYa#g7u3CrM+6v%e8jsr+P6#r65bz!j9}cgmX!gCFMbjJ-6u31=E7jz=5p z5M3yzbx;$)BcwhMCw1RHVtp~%>JS(=c^h?iV*La_*ow|MJ%5?8WM8$SE19}YY;Jd< zRf#*66TqPw$*teX+T5i;XsrWV3nCHsTaqjOef;>sF~I=UYGu)V3Ni@U^bw!tv9{?v zGZdqD%LfWBz@-tUT>pk0q56rS{HEUwfO_b~#l>H4)T~q)%#*LVZ@ZuS3JG${IEztT zF2Ok;_kn20NwLHpLy48{zd99WEvJWlKEH0l;3dA|+%7$RP!$`BoJb^M%QBl&b(*Dx zw{06*{01J?HCwIqaM8a6KZiZe3dQNMg$G+_;>))(3M#8|WPfL2*pq8ft-BW|$;`!; zVyj`dK!Qr#0G=$tapNNLZ~f)!dm$r)M|jEhe+~t!>Bl}wIvB7z`P7n&_uGxfv4s0~ zVA+{(-PYTL9s^G%RzIggP{+gp+LM9aJ|_5$3wUl^xIlHMaKS)pb%NQL&!iZ(VIAzd zqnG~C6429;I=benF|GzYbzxEQlZ#>{zDDL|F;`HraW92j?;*5{{Mj3wY&qoq5mkS5onHR4SD<+BcreL!lD)d~Y8>G$=B0r&VN zUM~2nqUoZ6sL=NLT(3rx_^lJMqY!1!)_(%|~Q6HRUNI1z~L?|0-*Zt*OP5t{pw9b07 zK>mtP4y0iYB!#sdfiA*U&oxO!qgUEyuIMtnCtkZ0<_+A}q!?R-723m+u-7ojH%7vQ zcsDzb{E{dGsD`2}7xx}Bmd}?o^j3p5u1QB3v%UdFahFsCyC+=QZL0qVp?VYO_@Uq8 zDdCEvhjyQB?sfzT`Bt%yF!ts{Ht-rpacJp%4Boaz#C=j!9imC&&D+g~hL%eXbr%oT z$+WvrBP)xu>4^NT%^W@x7P8aPFZE-b2xfnE4PE=|XJlh~?1w$E>#4fATHe8f+GQ5` zn5oZ4EOXXUpL*(O7c}(nzv4X^xyx!$>pg2TWK}ogC-{lHG~(@?l-b-YGh^Ax2N^=` z5q~>Gqx@0E{RUN^As}WhPormSDtrag5c1THiJZj-%|9KvKH~Ww;kM@mlx%w;o^&C6 zzTc+=HTa*BL$#rvKgLMWHFm#0aXqCXUF^K*7inkwpBT}>-1P$cqaR$(+&9RzJ?{fj z#2kwC#_!g}e=1^GV^tD{n3+@-u$KH!tf)uvf3l*l{9@)w{a26#iPrPzD;{>o>l}mu z6Wq|xA{?6ry;V`6CbWLg0N0>d5ZI9HjhU$K&o>WJhfL3HMD@Y5s`DilSIU?ZdoXqD zS@&Kz27_p|JV3zj{e1Q7j}moNzZ+Acc+rQB$E}NMg-p$D-9@+W;deis9qHhOo^c}s z!&Au9*|R)s_hIoa>owshcI{8=3jV+!Vi1~EPz>$M(eGdR)0)?~&`n9>joz%@{-}ee zwCN##yMK3Ku775c`$x!WoG{4(rik{{WzNtp0Qg`hWv@q`&g*DEL1kV!Sv22Y@()EQ z=K@~_Q3Hh~+5N?1_EYT-Yr`rJqSW^Z?f%*76*s^lQ9PfVXxed+-T26)69Os;{p7!| zsCM<+!bX5mOt}$9^YJw9xy3k>D^;OOTiLgSvC!Aj9=7Y}-YI0RYr27ymV+WY;k+F3t9wK0dyydq>uIKV4~LhybeXq!n8Rc2Ui1G7}w zzifvzTlZmTg$S?#TycK+AH1Uy)v%EAOQ~#ns zsd<<`#&oa|Q93R~%!2*5oaxF%Xvc$}O@C`qT_zf8TPp+aLzQfF%lL=HfPsUXR3GAE zG*ad>AsjbT(O5s`2o6J+B)>CSu$X8&ZpCm?J*kuN{RkT9;vR7=`3O5#*Uik-$B|1$ zwsD2ueGLlX8$A6#R+&{prc_j2jPhqW2c*S}8w~>ILhr;l!q7L5OjYCu+%2Z>--zU! zBE}CUI_Z*xk%cjpRCQ2uL-c3ZILuC}aGz22$m%wzs@(g73_8=<18!?3YgRuA^cQ$9 zTOiZIluWzLe~WmdGpFZk(ba&A$arC2mQ?#XM!hk>{-Q`nY3Y`+=!U7Pl8*#eWxEb^ zQHT~qYaKcbHz01XQJvA2-X_l%@W)`JrY?$k@dKB_h(&*fu@B1+AuVL@#Fd{1xnD6! z36ngkZq}?dA;Q;QwnS39+W3*wHk| z7bacChPshvZMCjPe!h%0rHf+Xz!CP7x8@CViLaO~oG`CR8KEMV?Z{fp-OHjt{WmID^m-h`7~Ab_j?Z!Y%2 zOPOdzvh|zRRQ?(2YHhdcqq1k425X@7foXiw-MfGn4MjXlc6JpQ=)m%Eo02!P>rAF2 zbW&K-Iw*guc1P^-r~s1kIv;Q|M^!Y)v6$yA7nqu(sIVQTx%1)?6#~YMG&_d95-^UP zyYJ6S!sh!LT$X?3D!{@F978WI zZbfl24HQ@2Aii`!Y!5SaqBa4R53n>LsvJ$tr<1i&xC|BYEzbul+{aW%T&Pp&kC0;* zm)1^P$-)f_s~EZ`geklWnNP)gDF1zrHZN_=LGqT}=(iTY3n4KU6+DXEiVBTcq5P-i zZ|+yvtVmbi`M$+$Iz;^%8AZB`Nv52Xz2QTFT>I73Qq}T*<&TF{t;px>P(TPjiu_SD zOF?0$4}DZVdy?Tk;IzCV72)B*Gu=Y2MIhWq3Nmp8%Np4;s>=%J{Df)yxcJk${h49@ zLvXD8(nH~&EPt;aj0^)mqUWG3eS9+GY3bM+6N}O@Z8h8XR49;Eoge#h54m}kkx9Fd($yqTxGO@a1#`EGRD^Q^mhptUQr z>;*NsUbi@~0!;a^uueOjv9oA?e+t}Z>eDw$;0X!MWS6W4(bqTKbGn?G`kP;g&ix+! zjVZ%;@{hpQK2g)uAO{1_qvGve^*^*x0b3xurr_Yl?BHn&*@|u{NqZ+1e~}v;p8NVW z)BI1%ihj%B6Y8Y;^G`9Am~qcX<4v^}9QxF*;g?59Jl5=>;>1CUT4wfY3uLOlAAUOP zk6fDa58<45%?|)a|0@f^;7jWKPgL~313~=9A3V`uPEE7B&FqtzIOgMiCrhgWw(3*c z%>1rZ6rtU8^n)()Un#NX@MZg2h-CS~xe|$cSvF#Q;b9~rJ+`IR85FP_lw#D;8F75GcyFkVE-RvM_+n6 zw!gFEv%B1T9}DtVJvp*UXs7c%%|qSXMH__VD#c%OZP-?}>i&3Qob8wB58EP-3;Ib^ z(&*3A&5Jl|NH-!G%T?uiH3a6O2t~?D3zL)`Q%TQzGw}8|=36)&S8Qm;M*rx3i;ap- zffLtUFt!Crva7C#zf>C2NY1;00ArG)?JU4kvLJ3ZOEMrUWF&|~_vy#9)bD&cIrZP; z)Xs(kwa^#;y&8uUku#1!hYCfU;7&tBn=Awc4XX*a|6B7fL;VkMt8XT)gtZQ}lXH-| zE-IFKWG>C*$NG)?OK+w~g`&2FYk~G7A$aI+DtMd$WTf<+7+@iT5=J$CgWQi%;IOgg z_el|WPSFk?3n6dd;$IGgZ-{>0#p0KHiHjO&0d>_TZ`ozFKeRe$_vj%aMs%8TV*_Gx z8^>3RNZny*?jIXEo%ozepbYd1!5J`$(5f$~G4@KV#e$rSM%6>~RkW+13T& zrwG1VTf9Pds5#)j4Xn93aOJQt+aEfcKebU+J?t+@{`(_M<>{sJh(Vat4XvxSRzA9||AU73#yxf|PKs}o$@XQ$73hW&cr&+Ci zb_uZ*i?*|sucBt&(0M+Z(-8Tp&^|BPUOO~+kgcw`sPLm@Ge?yLt{B=dtEB{hmJ2$c z5||YB&WM?qbWL@q(?A`?(nBW&=`=gwlB1GO8VTxQz@dKrpj6aXRF=`;hmHubD zV$T3B3iLD4*WG*Fm{Z+Z$=_gq>;zlWHYNc2pX~V@UjHHfpA?ny?|Phn4}$+8;0cKl zDz>2(c;WKG=3teyS9nti(A*f82?!@eVBLNd{=E1p`XuLtSHs3^1 z1t{+8Ux>uENf>K|LRMfkBk2}-dCnYig+g)(>YOFc?tOOH_osy8()DEu*+$$v^BH<3 z3s*FnQL|)nduh`itUif_$cbDKGI;b~E?I^bM&E`9S6-SmMb-PMATo12Jm+_h9hdLB z#w3YG8xX*uOKcs~Xpusv{Ve67u~-(2CSkghJL9Nq1@*0Y2&QeE%Eg`eDo#xp~M_RowetLkUO zDf0H2nu~1qSgc-@JVyE3WQ=k8G{TnZ7LXrNq?lP%m5pBg{kM0%Dn8@a;-F{9*66EU zcDPkZJtw|4(j~bXxRj)^#`m_|lxKIw5ITAH7g_CEZVTvOaviv8dsb!ASB&Z2*{-Bgf@AQoMR^P(l9WygQ1&zZvVCW9)9RIU+L&=p3y#-z-vgWO%=ii0*TqWIcS62R^0@H4f#;7Y-^`oc= zw-1#N+0IpBx6#~AtDd%OIX7L48ypBIuVSTkzemwGuNSzX~-+Ke-Hlq;VQtz=_yb%*Zp_{0~WpcYvA4d$7Ru(}-J zKhY~4SZB#82T(|y$pqYTT_M=CsP;5eV>*Tg?@I~-;Ml1~bv&DUS-I!Xakw7LmR&+y z=wH@MvU@ZJp}X^k@K!Iz4s)4w&n)r@shYx-b(B7?067jzT5Nt)B`6P|5R4%&55n5 zp_P{9!)5p@E^H+3?hi|dqRR>j6k3Ukm)+bgMUM4jN3Cn>7L z2fbJC)U9XJjUn-Rk2LqW!0DJWYcp4kh}%+&t^&=ZPYZ zK5jC_oqnYwTiw`ji45+jqZScsfJ{7tC2w@)#+ANaRo8U1>~CWm(IFg(2k`^n|1 zP!^B?t9%5I^h{kd0B#Z}3>nb4Nye;lGCtA|*o3F3AtJrUdK|?k&%)fGZwZZa zW=yhw2F>;HsMXO;5lR?$+Ui(`FL%|cDD_9%Kmy+SwJqH7scWc9%M;S2ccGJ;1ZK|) z+K>ix#LcPDGDb~wzRw1Ch`IsaLf7;WOI|xSKr)xi;_?g)JIQ^NHawiG$A;sbOgvDuy4L5)gG;Sx zel`H!lc;u3_U#~wQ_ZU6O2ZL&%L@9&BZF@By{mFb{PnKf;iWu`Ow5ZcnCWXkm?Lyq zuhc^gtcL|o(-ZofK}G_IDUAOe0O>@wd=cz}rxf44Y2rQFrBW>3b>KoC58{$SKsSrV z+dbP0TF+^UiJ-k^PnXk!z7R3wbFe=tIQB8jnQ}UU@D`=;T!TeR{VWVEqXiJhV?gD} z%euI`1tL_LtIp379L@%3g|Pp^;^ju4^8eU;32n-!PU%^0v$1&3!|kWvrWOt;|GOB$)b**>ca#R430B z&MsH)lAl0e3FT!L$^NVh)O-MLfOq9!vv}&5ubtF#Z$+?N-q1h-A&zE{xga5A(W(j3 zd=Y;5XSnNQ$fF@hgU-*Uu-kw+EH3?G;{7(1=l&*}ZWmMT2Dm49cWahYAAE_x3gbkF zmeEuL8^^lspHDKSpVl91qul0d19WZ$ApDNzWnq|onskP*;nu5Rs(F<1nQuGEA3l#T zU)=}zJ;SwZ0*%B(DD212OAHdv&V>_=lxXd)y)do?ugZfZGcT3dPA6&;Z+1!@JvH3t zC6=#2R-Ny5&)Sa%&)!$4JUGEdNVWu*T(@YB;Nc1{iWyprZh3^ z<;s&hI-8&n&*0(AUBpki+xhOaUFcSF7cwCN5b$y1cr~ODhObpK$UO42^sDy3*?moDan-lD;3nj<97fj4m z%G|~MpRf|5PifT|g*vR#gwv^77I^qbv+sTX%g(*laLI@<4# z7dAVu(THvF!k~=7wum}yLz5M?=I}M88Im@wi4)5v`HUM|Na^Zx=qXC& zSzN31+p0!ii#O_#OYqP+T&*0;Foq ziMYErnWwSBcTU6a-Ir8=_Z;bY&AXkV)7~$xhHxcv>wm_G0sz<>m}ljFf3`{+WtB@x4$C&wa8Av=Xnhx_oXJSSYSup!nxfx4 z>So1ranMIbDMit};tDqE?!24cwVf$i@PL7ij;oj#6eL2r%Kh3zh_VbeoTCtR(su#> zkvG4`*}jr6pbFlFf~L2q%!i#^27_yWqcT9N$sM}(l_KiN*t`w7@4shqpU$9tHmazy zN@yUWRUQ`=)s|Z~cc9rXZz{5<8xCF#ni^-Kvp;4l?27G=+Y+6+PDLJ?pNKLrVU6|_ zx5Hb!M7PdF@E?q(=S@GC1-n9UL+}bV!!Ta>j9wlS=>XIwSdrmkq1#AuBK$UDIU;Z? z(+0GZBCnU>63-9cr`10qZ|D_3x zOW7h>V48O%n1+~cIe!>hck6=pxQ~|hI^O1R9z!`&Fppbie0+bFcn?~6p6Bf5mhwYl z%3WBLogb}~8$~#}h-!I0>lIak_9nnGSTvFcNTX)F8WG{pM!$n*8AQ&Fs5l-JAql6( z{b-+Ss^{Vdy29O7y22f1ZGC6zb`8m$^vsbmLsV(M4|Nefko$>?#!NX3-YI-%wK5t0 z9t+pvSmcQABvQJ@jflgr>%RFA(A1GBXuq}*P7YZ(8+8c%r7aWN6dV!4Twi6QM!9iW6?=}EP3Ko@t|=7_V4txM};cZZI+Vv z$PS>^YN+;CMn=Bo0TV|__HI^LHEY@mfkjSx1!Cg;jw$S8sIIgUbW%`bL90#vdS(|~ z_~%`^(zj1z@=4YMfzRwDKp|eH3K*s`c6AXh7>|+3nqsZ^u1^ItOsCY0o6C(JEO*SQ zh)Gm{;xZ+J4k6JEUlU>PVu{R z56==nU_FEu4ykS0$rT@g%*A!vb!fVIvCQE*$DG_)St|s{_4i8n%$UorfcUJ_Kdb(i~qwn2$4OqtkrF6I}Z;vPcVG7wY}3_rwdL?78Z_5 z4%HQ4($6b2V-%1Tpubggz=eh>!7zCG-Y=5%YDP`BVbS0T$cf z*o&^y@m7L_Aye{Ksa);dcE177X9@3IhKSbL6fQY?%?~Yp74sB#o2!I?fV5;I)+f8< zIrQ_9RNJqIQ-t(9VGPGe&=HH$;N7Sq*#$&n&ew`EMR?+l~+clc}1E!=)%S z?X=3 z{$&9|7o9~Td+vv$HqY@f>Z#AHr(aZ&#jD&%Lm`iutu|WU@8Dv$wXx?9g$`4`(8?;Y zznYB-F6x2N*elKJhJB%TfwV~=1Yzl`P1RW5Po%k7b=C-IEX=99zuE(&nmM8!G^**$ z!eScU2~Y523=1qv-#!xj-@juA3s57~XSiI^r`GH#2X4QUnf z_^wqctPg+Qj<;ACU_lmbRzj0h-WI8IyZJR5eKt3k-MHh2_^=utu#)9KYxzLC;Lj_z zsNWD8!sCY06R^2HuNJU->7KBdSno&*YH5x#C$LAI!?Wh39Dtuqdv@nW<5#0-Vu62G z72BdA9OSJbKP!{N&dLxgCmItkmhWpH7f?hsvUUs@0P~!6e_m!Mxc#k&^ z&vL%+d!W9Z^-FdjNsL{oy>?&ur>3U~<(}4j@Qd$@*xff-8$;FB)_02-PUO{qcj_+Q zUayDWz}EW3bvy6OD3#N^7h~ENlp5(7%}`5QIHH8#Eob#oZdC^GkkaZ7^tYEjGE^-I zf5g1Nj9G1e1PkYqIr>ihvP3gl?iY$R=1cb$SLb-DB3*p*%2ksdEs#r=2oX%B$l2cs zs9-T8_?+E@s?5KHmePAT2HQVpmV<-Wwsur`BcAN>_-Q-{bOZ+MT{uBdtKlS1L?rn1 zp3VF^7l*Zusc}L$t;KeG049V}M$4j}Jh~!yK;$V#*A#C)?sDrLOcYh_Q0$XURzRX% z|JWPzGA^YukV`M^VMBBvc9|s+=z?&c*9FfFYbCMJYV+k}2Gk{fu&6i1zDH=C{`{@B zH1{m@`AtpNMe8AQk&pV#3=XtVDP=xpA4p?1)b2IvLN~@3jNh4avH7{|<(4J}fZKlL z`$VtN&%6_2a?H9@6Iter-OF*kk?BMO;Y3*c8iJ_mF?4DGf7n%%kfg(WHdO0i<=QcN zM6S!ZO;qb70S3M>!RT?xkcN0`zr7G{YS69fhS|Ep!Krec|NLHBXYMs|g&^_Djbo0{ zjdWBpd9>up=9WUBdyz~4`IQiz{%O8<6CdnGphz`MG^GZ!C`gp+Fld(>2XQn#y3+jX zsrw`CD*U}CiN{`-)T7i6d2eUOpUd%}746i=Q+YMRG<(<&KszmKD3@_%O{=SaA0=bBWmI3#ui`c!pN96_1rxSR6i zK)Kx?ostAvW>@Gu&_1+OVjkKxTbDSnD63xkRlPkSe1WpSFTXldRDw@Tg|Y*-EyC*! zBOmo%(SfpUV7-7?gKgyqC@+QeDHPZg3 z14k5}-Y%dDI8hOqvpqX!nIvZ*ag!!ey9gLDD2Keb`n60sV^&WAKY!m$2alz8THj(p zmhdxwG7cjkeziEhYJ_Q88vIzozbSm0`6F&P~+gO2!o)injMv$8f;^llLgWZE1xf16enJ3ou7K@3f(_ zo6^3Y^xEc>T5df#u%oilx6*Y$|D2lI*SO$U&%957J#kXq-bBA<-r?n2i85z@K27j5 z3C)r{gHC?G>roE$#iiZ;={58dhH#5}ID(0o&OC0w^6ETBKotN$_MZ7*aEqq_g={qS zPZqU~G`Exc8~z@Dadgo{9p)_gc1#xLs8E1~1g6(lE<6{??;EMctD+_pVgGRD)oDo!1hJCXI-n ziZ#2c@IWdV6(_BnzRh#p7!hVn9q-khlHP}BIA+<+RY4Q4#AAnmB_yvnanF>_*nFMS z{n>dK77EFYxSpm_gHmZgS}o~rTEu%ezREyD7XJAVFTMz?cTysF-o&t|bFJBm5;*4> zGk;KcyDfD{P0amy>zNn3{~7e!mHNCGSkv=K=Q$!Sg?Msv1vxh%ZdI&#xUY;S;Put$Ysz`YR{j%CV^Chv)5=T{lj{=o zN64{QH}6-Kjl^5U>PZ3L>h1js>hQ#EDdhD_SXx=PG2xGf@k0?(h&plEk%V|~!Zqf4 ziw}mUtMQBl*ZaI(@m5W2?uaj;a8mGWBjNfd4c~?yYa;P`F_zFL+)LCR`!&?}p)u(i zF=}pI&h~RQo)!Px?vB6%aD;vHoMTmXi!%N5NMKe5Svr1O&W0i%+sVcpF`AqNE)l;M zBMTX{ygaYO#o86`3M}@a{(<3jte4A*-Nh{-yu|8Ex?TMo{g7mMDNWccsXUHBjtm@R z#d6lMqKv2!nanpIW9TLFdQ4BKD=3({pHhB5A{)m1m|=Lkqw_ar90!ZwCXiHe&k7}K z5w9=wo>8;xoccs$VWi&4MZ*l2USpPiL;QbzK+F^zxS zXtzmkcnlL&oKmx|ePffZlE3PAaiM{TAG75#9?L%0Q=jJ(8Qgbv~cc0biZomsge zIUqQuq>Gth^pdSS~#FC$noes13&CzRM=_J>U6qx@bhgbEu$ZPSI=?R z2}Z64w=U)i(pmZv+DNM-{}-lYbOe2?rPic;@~^f6JwChbptKK z_@Y}5jR!#L+EB75mM&BcUFt`8Fde7wQZd`gF{mM`n?rMsK!TLXoTqpBm+w^0H?!}b zo8vx$H2i_RCk_i|kp&r0OqnZ?lO`}8R}$*x*0F%(;VQFSqjAPOo-gR^;@Dz7-2#pei_eVCOCE)P+D&cfZ@_zYUNPbn3519a^r9<1 zuW_T@0k;Ax%IQV%wQXS#?PdFbpuymXcyM=qo0c)Q@L|3y zfYcLOjGgsnK^YTrVS#wz71Ogm5^YVc*;Sb**J%qU#4YE9b^F*{m(S*vp#7vynjVR;%X-6O$ZnVOPO-x!U+zroD`U7IbRxDx z&bK+Mosm04j$g%RFwY>e?zHrunAh1XyvnWAL4kAoSFXj}t*uo_YOm;UF50LRuPPyN z)uwF9N$4hNSrS|X6iVCP#+zL)@3!Jcd;EYXkBCSBhZ7*$w6mu|E1>Un6~P`5yp?Wm zG2W#3g#pC|Hfu_1mb!Y*zpo*FLcLqFK-*_diDf9A~6_)p^1yu9ua!CA%9s)YzrFS_TYG9 z%@z7>^8NQp@Vu?#TQJxj*&iK;5Tt@jncIeS5Nzg@e((&<-!ZD)Q_1(w$>kMjW3RbY z7L+x1agDLef8FzUChdzEe;DJFT=6?N5k5f?bgKp5y+WueU<^CGg`W7e`~1FFLT82A zLRYtFAyyPeYv&+Ku35)ek&?pR*>H8wzT-Nj|GL`X^($+{DN)=PQ9txkw(f?CVW;&? zKF;w5^lSmnt*o6-!3ZJctecWQaX>6FKLO!u=Z~E#v;;91h>S#(c4&hymnDG55gMoR zd5h1m{WCgj;tSt~raziKaRLj0np&$0Sw9C__6&AS;f#1N)<8FzWQ+dk1chZ6_wJ^H z@v}95N`TaY835kfxg^0*^0R5la(VKTG6k`?VzO4&(jp*TE%z!fEa0`gGq3P8ID5ej zGtzQKq&tOFdo%a7}FaxmVz!0;Ft`#2-+ika#Y^)KJY(Y0$-qJo>{UVTDnC32zLm zs(Wy7v)M_uC?FQI;A|-M)QRmvgZTP25dH%*L_y824`y?)l5U%u?FWY0j|RFRMZuAv z45LR_U8}Fb91!8hIDuP+7$+*G1Izf0L>=xZaD# zDM~8Iyib8DU~RAO3M+1#Oq*7VsJ$gq_F?;6=G6m}29YTiQlMltBPDZTUtgX{^eZ)f z%@q3_yXjDzj^cft0*?9W!O5s3ACnsw&oF!YEmy-Ndo6;4lOmnz&A6PGs`?W#!i0aL zYW#X3a0S0e)R60>RYG7U12+rps9A1#4v2)L&wdLoJYpJ6k6)y(=gl}iM_rCwK!8C) z+%e=Q$lOD|^b_$05zs4jiQqiwS_Hd~QTfGzypTsD)Xp4{osq@TqL@;#wIwFz+`oMP z`>H?h<8pX;TAYgVGbiB}$lPG2 zg|Nm(sGlYb5yfYbx#aH~a4DU-gc^heuC0i_!jt!|^yyua!yj~fDPUr-+U=xPq`A05 zr@*S7dSwe7&_^?Af^HPUuZzE>5NjfN1y`V^zx+~{%6}4p5RLBg-DOg+4sU`5IDfe# zVjqN!$m$}w8l^ZC^h5NG(k!GzB^Z9O21TSFWiY9h6S9`WFRb#WEm5oKF()QThGc9u zUu^GN{DM1e@_Q)*JdaWqmz<+EWLG%7N~N>jR*|}+fcXZ%Lo=i22(*6rWgV`Y--0i= zQMY>Z{|SJcm|xn#RA~oi$xb8h`H&-LKh_Edy_lpgg& zTgcF``f#b%x}BvV&bueN+j^X2*!Xd+`-D8D=#!v+spd(v?#N|L8UOc3vW4tgfWwT| zjRD#du`eu}=M5?Ji&zJ7X2zc#fouHgd1mxIFwPJ9U0rj8mvyZXOaVllg0(&QUX^9V z;&@9(Pmc-~7FT+=vXIY8w)T9MH^dHt7o#cI(GbCd1tMNT04{(v|H?1z;`DQnT8xH+Q&(VMC1kP+{FNmI?k@^!S zR=-B)3OkkZj|YGy&S~(rm*JH#|LpefwvxU zxLoKXqzE_lHmL?onk=wHI)uXO=qQ(QLb6CAxG#g!x4E_ZB1aRS5LTd(kAt*@D^AyE z^0jeQ`|`jgK3}#|ggy(v)e8q=HOAGy5_`Gv@Yv#t@7*am83{r2UwaM;3Gp))#?WYY z-`-bnTiB$Y(-1pMcQSre%6%xuVCGBI* zW+KEX-GFu3R*Rafn^XDU7w!224}0uZ-H2!{0ie2_oa77rS^l#v%43tky1Kn&im{u-< zfV7bOO!Pqz0_EF0dck>R+}{Q2J#3uX->ST>I6HDMMepjRhlZerh(lWJ_VU&Kmmp?) z>hm=yTEl!g>c}4wjEQ38Z`RSCG4C|y=E~=M9=FQ0GyO$N6YGjY(jql2NgCNWmxZn( zsEk(d-X20R1{RcqMHc~WP z_y{Z{HH}trGk^3~!;QvGaFb-Q#_0sCYVCGI687|a zHYWDLuEIj6Ou*(+1*^-W6N{v^d6CSZ4f8qY+C~)YttFqkI$M;i=kRio*fJ|WR;ZCW#QUUG1$4(SjDL!G0fwj@qJLJmNzcY zvd9RC0rn55T-wR-4M*tV$}lSmk;OhT$3Fhx9Vt?a_t@sHgk~G)5#{-4G4mixQ~XDB z!?o~NbA#WgJwInK#u@&G|Dq0zWURmMw z=sPVceVnUGY;$)}C@$?Mb83k5l3&)AOgZlbe9^*KK!f|*7q|K9d0A2w zv^)#EVI$7uT)#38z;G2@umevt*IT>9sOo>89kKLxePLFcydmq%@AST=;;^SnXEd*H z=sLE>m|{zpZ8Go#e{3LWHMmNID}47~OD}l~ixL(5w&D%TM|IlkI$*2Gy}?CM^5OXn z7PJX1^gHIU&+T%Wh84z2&4`b%6G*7<^O>bT{(AU96Xspt;H}dPEH*hdg&&hPGlr2! z-pd~4z0O==r!%q!&ZJDyoLfd{-e1=&M@?r0fu$7p7Z-$$ z-zELVU~nB?arv7~{o0B+(e2o|sup9mzB1wRiyqXuh`?M=poXeAJxz8?hSOZr8|S~; zAjJ)n&p0C5C-7TRS**l6N@)R)eq)yya&ixQBsfGi2@`j zrfs}gRFU5+%1Xa2_Txmy0^2=!+Ms}1E6$9hRkXX>HzgqfX=!lQnn%{-bo7S8*4iWu55(ftV>AOue+?Rg> z{D*&C$*3c{~e0TvRX_fa#+%U8x=@yeQ{Lpq}IYSMKn{tLvc$H&R3e7N8XaADvR}vjI=} zm+6$zi@cyKLzUo|Li(&un%qm)M~hQhV@$WfUKVz8`1^pwEfK~K>>lK+@!6V1EAdM1 zk0E+VZ!-ZLDr`4V)sG$F{Js0H;`Up#Xy|5?b8M;Y6ge)7Ik_pcRg(Jx?n&*TRg2$# zeOqpSd+K^MA|Ovo)-ATVX^*j?eLvH4aSP-R$M*P`?~TB#>Q6G$^$F$qYuwd}Mv|&) z2l*j0w=t$8Ims(drYotGRbLBb$-CfstYeTY_is#R6@ z`@9C^ZV0;DXiDTVuX;M@+-&~b*ZhD1MOqx+SLsUQ31dS>Ux~p=2 zP!tl(d&V=~XbOdSYAtiy&~E57T;ZB^;l^N~) z^o|a~lOvAUM}siWHNXr%j9qr?VAo$aTeI&Y%l78Q9OF>JC5GSe@aCe)=FJ2@L$L}Q zXw54rN@Ma@T*Yu&_~k&Y>-hJ+1?LcRQ2X}Ql2i&pg4#QPJvRZkc2!pyRN-qjn87Vt zb+%nalcI);WwbbyWdA)ZTx10&JDb5F-xB6)25BC@(j-`g@~--HC9psUBgJVm0yIAG ze(GA6cCvSA4&NuznqO&;Q@a&nAONPGjw2kSNzM`8zjErCTtKne5pgmjSz$7r4U=A% zst(gT!u}gR*ks$xvoj&7Bqzh7Ms1-)vo9eqxHRJQ`Y!z6rX2j=F3Z}sIn8vdC@s){G?`(H}pyi-uN3U~pCUMzGdAfc+ zaoAr8f24lu8tENIgM6%}VNY^*uJ)8dUYaY_?>j^^rgu-vBEXvTHWp``#AsUyIET$x zOk@u2X&K|uPR&Ls$iDwb-4MIE5j1pu6d6@(VXb!p6ClpoJrVW&u)rWzX6GUaJf~cl z^ny9@QS(@cM$6ldE@$^bHF{$QB!n5aY1vDy^!IP?iheJarK0wUpQpx)Qf%@UPDos# zLC4S5G|B-GDIprrSO12l?(Hoc;q%n@-yy5m64=M2AvT9_nM<)K)Y!dz_YljOk-Z1$ zo*v|p_ipU=IzKo9cM-b*{ zL$dfU1BjI7hu2D`q}MtJR>q^frFENISd}E(Y36nA?`*(3l)FoHUV5Y0)DKFkOSZY) zi5ubir&hl(|2!H#c|RZ_ox+IT5+Bt~wPNW+2g4&jlr!Jb&-JhD#q?%ltEXS@*7|B0 z!<#Tjk<={jt+Xv>iKcF(dC9ck| zlm_px7w)%4JbSNfED{?Yw^C-Qwuz)|x3^cF-4bB5LQ6wrPQC7o`d$b4W2;kb|SRcJ`wH>uATs%72jRoJcU=Ooy2H&RV zeVv8hwQ7J=|4NqzQmEiDPb_ z*#}X$M1U^=BRi;m*vs`Lv?(P0*B5S16B!@7;#^>ftWuZk>{5=4w4{Lbvz>P%kzG(qHnT+SW04Cf0fZ2 z*{{9Vh>~v^mY;g`zM%#6WkUh4red#01Xu;yHq~;+4R&4iHW&O1H`!cSU}Ivf2;F`X zKjbeLH0a(RPDX*Twd1lYg|>46Zs@i7upzeA?M$G~&~p0Bb0bd=Lw`2z<9asPaa+;* z$n*V0Je~Sk$r$W*KOA}N`Ie@D5d0?g4Y}$ykKA{1Z_|owPVXC&0y*jU!$iMuBF?H0 zJR*B;15OXByP*!!@m?h@;twPUpd6&>{nCO-D6V4UUvhN~zWc~nFA}ZW**X~G{V4nQ zhr-qN%%YsJA$32=n~rg212%@Ux_;Q;03PL>`qVCi-Zu|&B|`32$HL9}>}>Zl$z}^v zimr(@gO;Dhb}Brc8fa9+dCdIfa-_vrjw0mu^Rla`%9e!ni#dFaIMK&cgc3=i}v5APui!m?5%+pc#w?MNRrs*jp_{s)1I;gGBB6RwyCTa z(LKakXP(ogQ{aH-gSajQ4(fK}za?_|9mxzD1rz z-p@unIg~5%S}U^b*Fl$0W6-+Mt~+EnwmAhGVSQb$WPvypq+>evQ5&~fsl|q0WgO@4 z-|lY&Knz*I3<*Jmd=0*F8-aLCGkmOqWwuGcq4ib$~o#z z(&r}!f;S}>>pgh`OO2^-GfwLtsEJ-_hhwUJ`?xc2GWQBUDBh)EdG07L>%4q?b#LXB zS)G`Dbly|&z(Q(pwz6I2bosEQpI}tHmLy%5YMF$lFi?4uJ(cR+g0Ml9u-ISQrH9Sy zv`p1FxwTNUMbSiv8MzYPkNsWFo-~rS{r6CJDB9hYe&!MrY--N?^>|0^okp#NBQije zdRX^W=4?10epwl=8pUM&{I5k$XQN{g7-|)x73(P_(reM>aCp`g{q`*tHu8e%T`Sbm z-sc$o}7rq%)(n6!$-|04nh= zmUU~hx#aYR~D2Rhlg4`2-UltEu1|5YO}?NG>Peg=qA^cLhTXxkn7d3I~Gc)-YpS6 zDMhX3g@03qRwUA-OF$cMIwE=3$g=uQHXUJBO2UjNzK)dNO0{gMkrsVuc_Dsp)iY!O ziHLcB+>j>~fA&-&c?y0Bym%C%td3hBrC|bP(L<<_=5QW=hAbTfcFKk=QhQi}r*B#G zYRBwWj-TpGP4KFm#EzA;a8(b2U|3bHWs{*DDgC@`)aFu8C}8gnch|~}`8Wq~HToP< zClWsG^+J#yCTegR*)j;y_`QEuKXvs}C9SEcxiI)FJz`ThrvzqF`Dl=u6nvLP;NU%W zXM`P>MRz2cLQ}*_g_GXoYpAl%J6u~I-T73mgM8CYcF0qnW2lBC;Is7;AyoWDQ=e!k zmA}95f7gxE7Zg(WjDeXvBL;6MbkqbZ$9jV6?yoOZR(PofL23THxx}T(Sj_7@hoAL? z2^mA5TaoGU*nuXV-Kh=6Q)esg_bq$k)ytGmOCZWHqIQ`zdarfY9P!z5QjhETM-L)} zuq?=zp}kxmV?-UVS?W+^4}pUI7!KWABQ>h{LK&B=88n!PW3llf0!FACgC#XC0GgYX z7_L*PZ}TG_a{7edHi_#BTlK2N*mGT&<>KaL7SD&m(EbJ^E6&)lEFnCU@Oxh($fK;2 z(noy=V!p=&Kk^cz+fSpM@C&DNB7T^2;hz`hU2oIqtAb!MT~haW3dc@ta~K136&t&O z@d!`K;PaGs!c^`gukkiK^1w3`2fC8X(s-hC54(e+ry;ACul`<2!y{4jjHmZ(MVrL3 z8=Rc_LjGX-@$4LHx0T1|!h&Y?h{8J-Y%d5Z;J#IkQONiss^G28Bs=}1u_M_2)s*{J zyZT?UeDnV$%lE%p)qj1VF@(jR#VxEr$L;53!WEsX_s(x|_Xl3m+fMeXpU54k2I16C ze;K(l`HBq6HpA2%&wbS+VAGjftr-Ac;nWHZ@Rq>Emj^u}Mtl6H6wrQNFYrUM3mBTH&5&cHx_wRB@iR68H zMUCMJCKK3`Fvwi|VYH_$VklGaZQwxhKW*7H8!7Ek`i`ptfy7(M{Uug0FIRXm5b!6} zK7>>g-AHABm6I`Ew@kWWob4j*+Hgq4X{W=&Y#i#QTUN^=G^$FZ(Sj@&J8Mp6!7?Z& z>DL~G!IDQO8$#g?3-UtS0U~QNXy;*LTIMd4@|nDndrBxEf3%1&*F}e-0-HjvjwW!_ z;DFhM+0{3%@cwSN^s}tmhe9+}oBxUyGN*eCl-+e6yaCNWS;((E!qcBVx~&I+VET z70rb5r>I6C8+zabg%ymadZeL*@{5__#CQ8k`@c8(W@k7a_lG8}AI`$eQ@X-{>BdjZ zsrEA86O#L@G7fInh8+r+`3H1H*&elOpYEj(6K2qA)^;?0-#!6u(3H?hxTl_7q#b-FP zQ~f-&&3t{C^+*-Bi)FtaH3x>!+*lf?F!v~v)3fn};mL7C_0UDw(3nWn%|qKf^oHT@ zMXoBREi*3n6$sOs`uxv7>oBzqe;J_JRZ;tM_+3GSem)qU$wy4G{K4s4k(J!98Q83) zUEu7=&JANlzGoiJTWxJ59|_1^jT?=&L8rWesqC19$kc7$A{9od7>KylHueeppaus`OT zJg+s+`qUH7ilBm;yE&gUl?GYs$oYYYCvxiZxY#7W0?zAroM_Ic+y#jC#5E$&t9JIB z%0)oxw5J$x?`V%fxXJa^0lwM|p1Xk`_O%#WSBV0Lv;3Ed$IP?)cc03!WFD~EkA8h$ za6H?qN^oOzarK57aZh+@^CsoH?e5_B^W-Js7iFV(a_f$mx{%kp#lv?DY6C3%0;xeo`BSl{dkH^=AlFM6Z3 zJsL_$6v$qB(lL-oE!G6I_A(?BtIrk*`Yqc1e8K)XwzukwY*R*>PfZ|5dd~2qz#g4~ zU@Nq<5i#;=X{pbk4nRJ^<~jhcSjNmBb#G_bi^{*z<0?zBP$MSnO~r`r-$_UwSgy&&5BtSJx=}3qf* zx2ZN69$)|h_c;Hm$p<-#-TqWMM?`q{T{@f`&{TWyD%k*|8ew?h!OkqQzYWu8t?BTD>mkK&gg)btMrtd*#Lhp ziA=WGulq^tLN@({ra82QDKj)Xt~mX2;m2|B%?gsUTa6eM&@lb88R{-?$(@r_v`+fh z$tT8Kc&;xBm|u1FiI2U2}NT$R+;r8MVzVY2aH4z;_1 z#b)BJyQ6RFAmO6AN$XZ2rm6uPTZBa-W$pafXnk#n*bcO*`ZP7CqO+EHk6ru=usl2+ z{um{b9zi;#nE(_)*&XG_KQ3Yaj$IOI=HH0%|NSJVlmDBOTxYx*EI9gs`HWHKe^XH9 zKStFfT9F~<$Sop1)DjNHmp)AmWJB+5?~RG-%1F{Ee{@p#ysIpMJ-;RW-TO7Al-l)% zac1M73uz#2CA6WxoJ=F2aD~iz?FQGA@IF6L-pkmL%!N`pL*;%-5RJY$ihS^JN z>;M4*=#{_bO+S@3_H3?%3nR+P}$iWiL&%3vviPergCegEBMm-#J^Yi{7V&GOw|4ZbvWvW(ezHD zimS~g*r1wyT`Vz5wPWnt&*RH$8T1Kx8923AGwJ_h48TVhin`h{R|u56=}qmNs}wPy z>#jDYmj_THj8Hp@Acs#6%m{eH2`pbRGRR6P1+l#Q&}S=fmhCwkA|3)R z=#_=Now^ZfO)@}W$|5kwl7o6YH@YGSTdj)6;YZH*oO(y+*6by#(Jn^E0DHG?dI+1I z)SFhFI*1-&f9pQ3(Bn`?DEisI5o-|DRE}h3s~?JQG-HC7C9bYucPqErsCyVO!)to4 zbK$kr^57NXWYs=8)o^zrg^Gw=>{Zg}iT~Tqe+LB)Ix6rG1OFZqd^{&k7H&QKQB^|8 zJrUL%O}mbpZ$a}CZP=}?{7}1X60fTr?=v(;Mvjl}&nE!KJontc)jN|sIRX;NXa5~2 zz5<(+4%NdTu6LiT5#NIutVw=Tj5rL}Q9N7CxprW~KRul27icEjaL@Rs9R=o*QuBjD z9?_>z*`au*OfDu)FN}AM;DW=tE75^HK;#iTRw8;Ny;~%M!`4$5vfEr>i=1J)eL**P zz%}2PP~q_t^$0^)mb!T_&y0$^10*#KF4o0nXlwPS9zZ3w?|QCkG>3o#f~Z#=piRhv zmkbu{+c`V$r^6`nz0$hF*tx4^PFLYSUH&z?GD9#}XFiL5={ef66M{c@)Pfd>S|a`L zN zAZ(RAWTC377W;*3Mqzi-dqb_lkZG-}^PtBR?+vY0$^jGI%hgjicpyhu@L#y3EJIyP zq`9IT&EF@z{>hK@6esS*lsEjHa^&P^fQGAeP?Z ztrkVYbcxtC4Fd3mJ7flGe|YLN$4hawCU9R%Y};I1wyg((E554Qsc&QRZ2Vj7-tpsE zdMu^~H?belV;mW1)12PFu?uW3(|zfCe+@@p%hlZ(#e2Y6M{L~bWyvi;PWQFK_5`}K zk%J|Vh#od_qfB5c31geg3WUFE!~U@Byt9Z>W$j93wP;*PMs;0^zK4_?nMH$y7bM`~ z-d}3z-9P>(pYA?-H}#sX*GPWb3mkiW{3e;v<6fay{--H)X>&-WXqh$UX%cw;So(eVll((O3jaC`;Ieu$q8R%%xVj{z8@2_G#FP= zSeUZmFbZwK2WL--ft~VFTy}qa?#t(fOmTl?gTK)L{(7sC@?S-Rb$5tTxp!8^nQr3{ zL$S?aq|K`EZ`pp?A0a?07TMJy{1pKP#+yxb`(2Bl7!0vf`pP_pgmU8^;{|dOE5W|M zLjf$x(sI#NYEwVt&&aygCc(_CyJY+MzjbY_`*Sf<@K+GjvA8(m1x8)99>T;1Ie%)k zEz6Gmu*VE``{&=84iNd|eg_=F?uR{{gk26NQLh>3PFVc9#3$1YxaOmXIo2N=BU?Dm z&FB~rcpE_-tkWo&+99riq&8*&bAe~6N)Z4!3Pt)!EplpJ>sb$dr-mB zkC%x6UVdVQlrQRSCFHr_ct;MD7SX3g;gJk$&BFsDRve>TeI#fa!efCwWm&i>Igk}` z9pN;Z=^NR>l?`15emvmhnAATblKOL@Y(BbM#)K5*N4(Ga0(Q_rt#WpnN5d8bF+YUV z(V=jP!FL%sPT!4$IjSJLzi(TEu1CyNPN`{*W`&28#!P@c(_9(%Gn*9$XU&k;3)*_p z#sxv6nT{LZ(-ZReP$Nv^qu>N7PNULpC^;$;S@gHj2bEP(iHq?{wKK~xVh^1Ev{HG; z$qh04w}~_z_$3N|Fttt`cKvVCHwxSwWeVP&R~{``{`V-Ty|5r-*X+^}?+LMT1dN=o zJ_3D6rWXG;l*-9Yb6qoII6FuHJy(~3N~rzANE?bdg?qwxGUadHDiE08eGR!K78gY8 zEDD+F{b3rgDEzer2M<@Z_hV(sN)u1I%D(G^16$~pxc<>W>gziI(xev0peu*c7;dU_df#jP?x)Ud2MhgaNLxA z1zRDlbZL>`&%JL>d1#6s+}H2ija35)K70{+?<^%TMWZoN);GD#Oj{!Y3_aI7t+*^JBnR1pM1WxniDrm{Gn>TS!QaW+C%_oTS+SS2C)Q}sm%*Lp zp2VDCf0l^+A=->$H3rY-DYfEfW51A}04@)(dD6fqs<#e$KOW1L@iyZ2pP_<+`(p^Y zUns4Km5T(7HtZD>L?o#`>g!*yb|vgk>>~*5SOD$OPYQo@CuM**2w>@W30$5QvgH(Q z^X8Qnip8;)Bp+lJO2cb&PVwp$y(a>b{j3T(J0k|@7Vu>%J#_x8C!Xc<7S*{}1x2Mr z-t7MT#$rY8I7Quf?Nv1Ndk7`g#{KZrCHa8`+JMw00}|RHAz!#@Lfljcc?3?b0-xS+ zXcB@BFIV?l|z6bkZy2`**q3w+i z^V!h?rnNAq1xe1Y2r9yxT7=o3xHkRyUDrG2FXSs7TCOyZ_~vU2iOPao z-^T7ZlNeLw2iS{ki*&9^junPFxbpwpe5}r#Qr!KLeXy$a=ka&-O4E%lUrxxa6wk$3L+{nt=E($qa$nGLQ9}`52Oo}llAF{bL)c5sFNxt9^Qv$v@ z7mzK9UTMG}+ptDe$Zx4@gj;1LrJ@IDic%6vFve8Z{~QTjSJV*fdobrPA@Qb1$Kepmdx+ct@hDF^}qL4KIv5WuDUi18ft`v0suo~jn@$kd8w`YS4 zuUJv>VLBka%DVPd((feAODiT1ps2f-ll7HVDangD_Rn7Ie&$Q#BQ+>I7KkUtnH$@h zr5+%{jl@=1I6nqcwu=l~OA!$=wAMIbP=Zjls?48AuSr?Nt8E-|iai0NH9_o&GOU8U zx5I@zhjGWI`07DCLCJp+3o84(Am=_ZTLPftzXG{-)w!TcVK$#kq7mGGnz0J?+P><( z^~XhKs*20$18BC;+~1zj^Xo$uR%_U z9sU9w5|v8KpjrJe(zuJfvQSDY0Y_6CoRe|B-ueg5e*%7U-9A4Sa>A~^00_2NcpZK% zxdsL!9|g?Sc^6nd@3*kWoA&4#x5?c%cgV!2?#-lEFkILo%E1s!1fu&BXbtwt(Qh4n zB)SF{(xmXVJiUEuo&S+1A#EemZ2b$6{{+xH6G#@IkYjJVU;OED{7rSZGe>3sKyfGQ zur0ofdsWGbv?E!NlAT@m^gZ;&!e{Ut@e#6gR z(v&~mPi=w3NQbcGy%*t^x;e9hNABQ)O12vkY(%(Q!eErs9p6+`JF!W1_RQPWLzYMd*IUQi z-yQ@e$2^F73rm?CuX;tR@~%4hi-PtLoaaUSukAD>I*_&I#VCOFs`UECW*^GF-lY2` zAv%_5ohP93jUhsf3K8O(k4({i!Ah>?ChT17gc7}D_z1*_l%i4}>1G1Ew|N+h0XOFWhlM#y zeT`s|)b|U3Os0xfaQ|rpUe_`sHa&MG#R?5LJUUrj0sNgwwM}HGd0E?%Dt{N$dF3oK zY8ksZB$LbW7Rq7`;)RyOWGVA&U0HJX&#Oe;k;C$GKKlbCR6-TIY4jOJVijeY_xQyO zq0A!I=|{$#bft+<*RT$KM!)O`cBGzqv@r- z5i3#j5$Hc5;iu!nOuI&74X0eRw&=B)=Jv$iTF)a6(*^W;$ zOZML8_F0>0Z+{SobwZ#c>uGOZFr-*JyxE!Rsd&2ZehtWV8S6!x5`nqCz4f45|M?w& zJ?vmV!f@-?rIh;Iue3Prl!&pq@EFD_*tyr7EG$wj*r(qKpoFQf zkYz{`*vb6rD2Nbu*xxZ`%#g52y!-uJ(@AxbEd1+}DAWf&`<;M2N&J_2pA#CD)-fRKLqjf*mB`fJ&!8n&R57)j0B`{uTt#}{4soU%oKI-Tn-b_;HW3ad66C|qY7 zP1XQSL>=Mj0s8&tD(XN?LD8%I2n8v2N}q4{24kO}Vo(w}Z$;X};kHhcLe-e;(Nc)z zDIvc2rRaQPz~6hyvykzEHcS=u3u#xZg`%VX#n~}*jIlB^YS(Z92>12Zd8lv_eY*9) zsXTiJW&H%Wnt63_DWxt;%f4r0!h;{H1f7Lmc|St)?9%MC9`uvPZ}R9QhR{`^N;&dV z&3{qWTR4sa9~*#<=aN%~y(CH&?dP-B%}uGLc^DBNai>~M8|UX3D)Y()UkO+EU<*kW z?A$Kqh$%^UmY` zpICqgWh(wKZo$^#~6Y04Iy5eFz`v?^C;_0GB7Km6=?{P}t1wgo_wNuJXn5X0EP zw0mKXKVg(`f31yMAkLA9z>th61bekvhI3DnPSZ&}{$pqY$sOtS#hXN^N%!;70M-k3 zbDR-22yN=Z5g&n1)w{j=>TNV4@NmD{_l(G3(VpRs4fTloBgR$TrK z&Mny8@!Jl3sdPbNIr|ap;ZuyOc_ln%MY(+(|8?wyF&fJ#yYp?K632{^TqSu%p-lCC zO;Nh%BSaaJBe!`uqOtVY-q}MEP1A1GC0R+w?o;-SMd}R42XE-e!@xch`w00fo)4_j zbbZ{8F%?gu_Z>9x(;dCRHSBBr5}S#3A<9uFgmMN7nxrLZAsy)R-Jag4JL$ZAML83t zI85KAtj{j4KOj}oZ!xmaotnmZNE$QbXKbpDaN1!oi?_qo&0?PTJ-vaGBOBSqMI48ZYghQ2Zhqq&dlI)N*#~7hUg11Uxnwy{b59KBxV@S#dAgJ&Uv!csr~fs&!55>z-wtUY$;V$szfZXSLpM^2prZqdd-VSc=fFN;I3-+CIrU_iy< zo1<~zOut44)*`vy$&p!lx>#*~e2O^vd^zB1H;q|1WyGLWg8gpEEAZ==V2#02MisRyLHPwnzPN>;DS90jykui=6HTq;mR-aDkm)5m4{Lp1x#zdbsd9vA)By`^HVsmoPpcDXZ z6ffkqySL;^t(4tQN#V*V;_;n}1>5LZw0Hu&M5kw7o zdX>`u!b+~_^T~@JkIyye0FNmo1bzydtz!-0#93#H-vIK=eAJKtZX-cjhnf8WvcuJ< zv11z3HQPdKM8qV2&dYz)#&?SsPl`u%h)$&|!hA)9k((@9 z)GuN`?D?tA6|t<9#fT@~2=EeWAR_wm`^t9GcGjS1N)vHKI=EcSH09)>tH(0Qjgs)ZY=rM_;?qeb(B$3V!H9J;d7C#uHx>=)@s=y49$<=L%+bpb;;guB)f6sNp-_eUVV@l6$WHWF7i3cc>}nZ!bD7V&2% z$ zECCAV)LQ&@`^q`%4(Z zOVafFkJjuiR&?%~LnDaXkiR_ljU(P?F{dojeOlQ5S|c|6&VxKH5AsEl57J93h=IqJ zFS$1X2hVM=nj>$469A+cQVM6TTddDCe!v$-S(VE7BPxRuW_HL6xeFAW2gp%=Qjx+Q z!ya;=3Efn+Jx)|0p2vWKBuLM`+~$au%CxiUL_qrZvP8j@yLDYp;!WZWUWux(ls;vN z(e**=ykz}1ra|Kv1aQ#n(@#j+wlc<}n_J(?9E5qr$ggw@etIpfHcLj(=nUrnSoVG$ zN|jJw7PIrX!X6~2JtZk{V7EYM<*nHu$)c9QdZiA`Dd2UzuZyZJJrkAfCLM;t%(e4) zg+y-25OO7rGuY50dFW?!k%Zzo{+K5Z%ZDTDOW@heY-Gg+kBS-PV_$*_gX|JUnYDq{ z7xm_jUp_>0ROX!tu%7|ZAMkM^@`ICOfhRF7_g8zG;hAdO;b;!5QSyMwysEd7`$F~= zMx|0rS!HYSfj5Isb-YE2L=0-QgG7HV7i9t5l+q;1@N-Ug%F?-j`WY*M9pRYUrS)tL z>GLSqTX815vv@?+#t@{v2vpYtRE-9dEIKyRI_LN?<32|_z@IZ3ZcOoa8>4w8qXU_XEUpNalF`ios16be;Wff3HO$V zqsPL;Aj8vkXLIIf)=!k|@8|8A`8`>p+Af;r=Z**F`<~A{RQvqJGD!u!z3o>Y zJ*7n&-t;l(E55CB^B{NQSKdA5uC{cdu|rWi8>f||dYEyR?~M~N_rx4%#F^s70uNHK z%uzb;ZcdR&>$&#l+Iti%Ag5h4L7-Lzcd<;~QCv!o4z|;JXO%W3{0k!L9L;ALVD(4o zU?8#&Wn9`6o1W@aBpvOEOS=~qcMw6hEjy@MIGX>jjdh3>{7`En2bs`)-guS)J0&N* zj=s4ZN>RxeZ~k+U`X{ z+fYG?k5l~1ty{D~ersX+3j#K)@Tq-~9iBohz!izKFXEGK{YcuQP5FX}m2PzW&|ab? zf!2KhcrnDM9~;avvX7vmr&ce>De{JH2b$Ex*v$sOJ%Gjq#2nwtZoIxp`c!3sY5(D- ziAu=>@jC)*Sh?8PKH<}X#A1)3YLRiszOtoha(oGq0Rqumg( zyIV_1B@<2uLQSKXlGhW}hDe3WDg_$$_{%tuAOHxn50Bf2NFC2_3jt9R}7O zcKiiFa;2+RB%#S2U7Im>uiA24|Cm;8GheK)KY~&h>tK+MICE}H$Cmi71d`?wSPODp z(Bh+>z%>cyQv1! zz`o&sAc-n8rB5G}lvWmHTRWcAnegHxFXlgF)m}kqc-@OHQ;J?8>_IpA0k?i0P0h(6 z1O=6HM1#k^GgWPCQ)pk8v#NRGPYP!}!DUxJ2ea(|7@Hs0Ndf*EI=cK&6*r?A-zV2H zoq2y$R&+w;eFL`jR2yKMicvQ|W%W-~AZz2QOgpY@E06-bO{p%XU&Bqd$xqJhX-TZs z%)JO%((9_42Eqwxl$?Oo{T1YEv@R%T9qr`0rWiQp?5!SFKgOBjF2FxA!S?KbAb^y( zWXKM1+ldhQokvUP1jZ}CTv#fSM-ZK!wjt3>ly^q~ap_N_$h+~)1V%3*$-D0dL0V?4 z#wgb;4L&5s{(={?0sXi-1+$`Ay?5w>f^h8nKiKx?y^W#P zZq7IDatPIR7CpHW)O#dTnsS<3 zqyCl*=6V6HvFyeb`k~q)wVwjl`*<&!oZ1&f?rD^P@kA-M_r%9Nm%hoKU*2 zcfb)oEWug3^6Z#+Zuz%uTG=mA);_$9Ee& z4|6Ou>gNyd01C*FlaDoI+ogBkqtIuonO)GNok`$e-a^E`Q*a~{}&FKTW(=ac!#2%<(^)>NiAQ9MRlL;adw%H-8 zGiPJf`z*HLfvO$fMwWWk1*mTcj<0ZKH#h(5}&84WBx8~I34$(q16rYHPL z@qP*(aCOsz=O9JXC$>YsaGqRq`x(7=zJu}QGg(Xsw60O3ST``XsW5(zbfQWj<$y5Yhy~p z(fcj1FeCxWOi+^j-4MEowFlNIDTZc!stw4&h9Yw zT~}vPEbRn$`zJ&AcrRpI##n7?i#3ltgy8zaFCW6yIBj29ZAzIrt360lKHI%wifENV zWgr%IM)0j9RkIO5{|FK7Y*Km$1|@;4BZ}?(GszHj5k3M6@dSHBmjUj&9_(O$_UNU|8L_1W2Tb=u%MreHkjcbrXn*M? z|CjQiczC{Y+Y4;i`V(TZmA69Zt|uJ-WWwQ9L1yTpf?yeF%aRGlOWE`h?cooeShh@a zmvcbb2NeNTxRj6?TjEf`B6w?7k6+5wSs|WSCUoCpWJr${e-dY8#*SngG>LkB<=|!u z)cY6)73|eoS;BpSXSRTr47IB{xhWmJl>-#zGXs!w=Cx!&H<$KTlSF(2iG8v(qk%-{ znpn2Lv51j0J!;6y@?`S1lId2x!~k!tKY~J7*uYf*&1EiCp#u`{xvkZo-k4hONjZ*H zo9x|F+k77ZN5goDc9k7$DOR7)<}Ip^%PMRM9p$0*op^9%GYj68LUQnJhWp_jKPStD z+aE`Y=9FULkA&bTn#(51g~1mpEf?pgHo5pnO~9Xy;wmg)F&r&0+r8l?fMwXblExZ& zOdT6n>N<_kh_0I^yd=(9%&V-NQ?Zn3ddSwe1aBYiFyOA>pDSig2A?)h5dk*;#K!06 zwX+pf;b=cUj%@R$*yyZol?QIc7&MOX+e&yT%59c{N3k8kDsx*WpzOrzGgp|yZ)FF-c9 z2k~frr~IbJHxy&w@Qk4HytIIV0>g96=@r-vVy^pJy5u+jS6X~0H#X25jY%yTvMHJ$ z2wb%a4%*#a+D=;(`8?y2uM|g}5ns5BJF&~iswT&%*}5<)WrJ%%59cyj8r9u2&mpY^ z5*3{`Xs(uGRd3(7KRWBBnYQOcUp~6uuoSpC{0)?=8P(&_FM4~;JZrLoH(A8FIyE}G zPYC$xj42msIvYDT2m{_mZ!_uuZ7)sYkev6KW!nxQnK*pB3GY>1b0KNtUX-fsm2(RT zZK6-seGYsf1BQ6GPTu%)o45o04W#sUNgVC>+Z~ruvFlF2OFxcyXut?GxSOv}%M`72 zYQCbA^pby;D}QsXY zui+FV6g)^vB6!*zQk3%z_yd`d%bd20@gi$P((I8gyDu)4`%)Q0al1Ou=gR@MpOMoA zZ0z#gT0{UnU(9!rPV)L4B3@&H^Y?W5jxCHcx}ap9oo4-Wb$hWoe9^IG+2b6aZwyol z2pvPxd-LvG#_%vge-9$%Ero3wgd`6<|!h_Urhi0{>dia#=YpNt;Z9W2)5*HA#_oKCpoHHDg^Agu|i~SwJJHZUgATgXuHRwWpT#0fmdR5)Km@3Mb)s2uoqWaMyizN zaz{vo&ZBZWRO7Tb2e=tSl{JxwnEH5QmkguVF)CT))ejCpyzxRo}|}eim3x26bbl-x+*sHfNtudk^>N?fNedj1qR%!`!y``L8(s`n*-?g`N+~s%ssy(t8k>V)?HWj*crQZj``}+mY_ku+H21`jGCEC1+T; za(-F&iSOvrnJM8n&i|MHDF#VH$RIoh(=M36KwlksIM4yM2~d)%hl$+R^6!e?tFkx| zYD8eJ_EeGV5#j%l01+%_rStOCgFZvmE+` zhQ=Lqo6MEf+?w^bO#Z`eDEdj@k5;EEr%}AY!F}^A*|A5hIlA}vYF#`&_v%VwLK(4( z;YkT?B`%TM=`31X88@L_e#akSW>x@vo+TqGttAP_>RQlQrS^oq$dTk@ZtkCib8Tc!81n_&?@oNPo(SS=#rjTKteO|YHyd|nwx+bFmu=59(tiZqd^FUDBW0a{1c7D@L60ck127P*mjF(flvg z-a4wPb^jg)DUp)ykd*F{Qc|QuK)SnAx)czQl5P+wk?v;G4V&(6q&K;V&2NKx?z#7# zbME&Y<6VF4!B~3_)>_Z=ne#K}eAXivD!#~v7-8-kNSdYmqnQqlp(InmC8t163ic}{ zmYof5Ycp&FI}I0!AB`g(2ENQOQuklWZoNFJw!CpP_Do3%kMv~>krRf3H+1^cl8r<) z0nBK7(O^pttj?(1r_VuBpu3k0#(wUkWi-*;lP1gm@|e>{D!AhyEG7hL3a4;sp@2R1 zVAVQl;7`2j8;o_K>9;)lucsqHD-$nBhZCQ8PQZ|mA9cPc)6vRg`L4%VPfy!B${cO#fmuHd0FX% zJ5|d9O!1wE>CvDB-ahp1+v<)&s&M)DNVR`=Ro9FVvW{mu?312QN4-UsP zV7SR9uLTdrbg;}6$|WBy7KcUGf7KvHIo^Cv)c1j}TF6;8Z#Sh1xh=`t$=tl_7)mtw zcRyEL#s8AQgB;`@{ZFEw+-SyI**1 z5e8Sf7KOjgukOdu<^LoQBD6Tklfs7ApKANH*pYA3!t#DF(d$CyjUjXKP%!TLYDo7# zBrp_HPvgmHMvBM0YMyWQ7a3pOPb{c~#5^SW@aXejqAzPJ$NdZ_ z2*!b8^wDQWvlZYNpnh3jmd3;z29kMZ_<4a$|H-{DJ|V90j-f}MYGN+D~h2|Dw#~H!$+HU>QsP8$x^VMg8H)H69802fSmfpBuV3U8_epSiYG&her*g z6x;h4UyOF7p2I=bSH^cce`GxHQ(_0*33!h=9C!{%>-^m!y_BUOH)-LnzA@T1Q&&74 z3bD;!OOK}<3J%G_X~5S@pYO!PC%ppdg5?et@ltMvXAD(a@5G4>9_q|trxRC|<}LV{ z2V&q=9uYeEcQ7p-7YR>kFEnqP(^BtDHD+)lxF4&)k-^u-nS4oHy3ekb4WX z1ikEm)c{vo!^ss^lP8!f{{bR)(wgWE3;ELG;pKp=`7pfC4gU+%(eBE^UL`Sr{vo+Y z|uj55!}|GQBpcZz;;u=KM3wGZuen*W1cA&taIcl25tfxk1p*fyi^ zT%t(Gg(N}JAp$-0^8ihV4Y8E53<9}ojcX{}>=|aX&4aJO4Lm`L3M0ruQ_u~{$|ixb zFMgP>D8bHJIHd~4(3MYuB+?O5`k7}$|55otbvY~hbk*^7?rBu|&iZWQZgGb_Ws6t;!6}ya&E@@}Eg%DUKjr^DHnEme}DE z50X9sjXbHcAs5xQ8nTdy7p@L7oG3S4gLi(0t~a=y%9L=dQO-iy- zMqj_Ucq+}H?lSogsYO+zEc0JV3kl)de}Ve}o%cZ}M-&N3JEjWuv+6v5A^}ewPond& z*HvWk`Jx9j_jPM!nbP*SFmXDR@O8e-H?}>C!?W+Tp4Yo>Z#_@QywY|Ca?^`fWX-$E zo`(ODI~-5;sy}1n>Kd7GcV<|gI$>*`k zekaLR7A)NvkCEn5Z9w}4T=pPVJF^Ze^umiULS}%`bSI|g1Z_gZq^b7&OL_`A@`~6L zo3!@uuq_c=Hf~C~k()p@kWJ~qPwPy6KSd*Lj)j-3y_6|PY2^q(@0Iapg&e`eNv4d9 zKw5En$Dn>-RxF_64_^st^0AL5o?e%kv57=zcAv|Ss+ZFt{1E&!TS>g0{)7P?5sjpu zm@eM&DL=X9B4Q03zHLx^I)QWp2r(5v)Lg30Zd9+_rU^)ezUI=ff;#+f)B}^2Q$NB{ zeux5XQkoyQq&U$F5knA49l0M7GXjxzkSa2`A>roALgHl1JoQ$jOy9X5V{aF zM71hlFe#ueG@V<@ zB@l6bi2c@*Vv}hZ`>R?xxWU+d>}<-i!HnO4;o$8E%x8}?@~Vgv)629uV%qe3wotIo zcjwH_f_rvJ$p>aW-Uc)1JqN5k`SB&ves{qu$$&R{c)ibrDkWf)gTIpf&M$O)4^%E@hft3@oey7 z*=Nbjp4jou|ACpT;Qh@679>2%|FQ>HKm>3u#4q|`ZuL}{zjoa(D{D zEUs#{@Z74^lsc)}ckNDu{3|=64qIiy&n{~@oz?*iUpa41D%f8J_PSE(wc2&U`2Gi^ zr-o-cWK3rn!#70_j==cPro#Y=#0bdv$mLf0LeU!i38jb>u4DC!$&4ogy{4l-2U^pR zTezmu6}rN9l!4bWlgE;69Cu&CT0~61UlTU34$Dj@si}Pe2L%R8)MnV{yq(1xlTB~i zYN_vWI?R06Z1%B88FSI@p#Temv5S@(TbzWw4L|uk6@l;1!?gPJ-WOd#(anLRhxTf#01|^B?`S6K};2!c*$hA-`17OYM`1y!>}d(XhBv!?^-V z6FHeMikQfJ)sOU-e7h2`h{9j|7tk}EdiLXFrO^uGmJ~`(_K3N0crfdps5ke_z3JZ^ zbWxqd$nGA$_P2AkLT!6afP{79s;}xY!#h$&#g*G15rWnAzs(PXUv;1aYh;QVg8lct z#{bc-VAsA0qdCTWoAF2A{eOmL#^$dv`vpWQd1K#dn!hi=sX44%jw8{Wi`s{z{)79# zvS}jMQ=Jy^@AgAc_H-HaX(tENet-JZ_eYWN$`4BLrSi|WmKiHkUhT( z$w==nLi;oV+N#7UKI-m4vZl$^qzjuC_kJ=}FL0e-N%7~_n(;_J`cRD2e8vR6u0q&q6C;ce@ExBH)&^qC7ciusGKn3sBp<}3pIX$iY>>-Nej2MK*8 zn~vcN>@Sq-@;5l1F%8vak56QzU0y6_S4j=Y{I*v*={oGtuI!Dh-tpBMgP*-;ksjwwQ|3lyZ{d1-WXPOr{Ee)PGFT9v%w579fHf6@tx z)WG%JW(@Q3p}ot}d&wKWJP@uY1m9l#N4x+9nCcG2z6vUF+ZQm)XhU5oc|I=kjvsOg zW#7QWOv3c%2e{ck&uNIpGkfCi+MTCuIaHFyX%Vr}a3e+@U~Uz!*k9U4lzhIIpDiixn)aW;l7bI&zqa9k{z2vQ1GVeB zb`H9_1R!VevF#=;x;i31=>VP4Rbi6LYz*&|$5}IWoyaHPu6KJ@Z_qsCmt$fqA7`i^47;}!L z+FJMxZnem9xJrY?o3}|)D|)vN=i?!wT~|U+p6n<6(R4$Duo#+L-a=rT77y3kQz2Nf z%euiyor5gbDQ)xZNvSfEbHoomjnXoJ&S;)dh@Q$^%hxL9wm;hGB%r(k z=M(j={jlQIP;v-TIG!%;-p>_RjgH2Vj|9L1zYLDyJ5WZ zkWBr`0;QlcFs=Tbxb04OV9K^F^pfr`>fL8tJw=Xp2x+(G{7UYorOs=5w5bjoJuSK% zf?0S~10Lmv;;?#R<1QwTai)@Fh&S0XeQ<)!1NhuM48sSTPl~@dEAMbe@&g*1gY_FP z0JTRrFiS62g8>}7Vpz21DY#>Z z?^T)Z<3rvCC$h%G?%_*Y->TCy&&qP!a1jdDC_@DT;uBpAT&ncUopFci_ zg#TYu?MHh0kyw_mJ3GU!G}p2Y)K& za5!e+Df%H}EtU5{!dyLp^V5e1-}Zl4sl7FvcS}ni40v48CmRig(7eLdzYBq)uqAi1 z!h5&``p}3kr`jX}yU zKE1?^WqB&``uSu3q2xDN(kh=RB_4cz__VP>8TWPF$z%W5aPU|Z75>yE$maD2Em6t7o%jihfrsa8|_Uk^^&Hg)j|gp3?V zkeg}ut~V81P1MMcXbGl9qFI3C3#Z%1ccp+o$Rf7%)&op~>1feY8nno7$s(l2@m9B>OAn zCPA+C2#}PxxQt~>|e8Om3U8CAwsMs@P+KZq=Di|4z z^6{vsi?(yzs*knHR_7(n`=Nu`(SmI9_Y0?6kWuLVYDaLZv=!Ns+p>9!i1cUhAsUxu zTbG)ssf%eJ27QMyrIg+>oD+AAoyvfX+pG|IT=WcmbZK{kyx!P;6g)SW5^;h-IXB%D z1DMOE3RWqND!1?nlt z$yX?W%7AN>Uh?&bprD3s)NHLsYUyVb;^J$=b}RzE#+JIP@nv!e9&aWOVgoa;F)<_Y zQ{$e81PN}2lDW}qOraUZm3f|6qcDk78i46>Sq+HqekhBM!B;{&&2cc>lkj}@%1FAQ z|Gm1`5orlh`NUaJ*xe9`N;5;Q2)aMAQ0gAW{r$5N>+i5?;20+32PYF$#vDir>VLBg}JYn&(j|E1#o-nRFTOs zzBAC@8YK(s&NOJv6xV)seaI=}hq2=0%z-<%?>bWp@1-y&d=z~>)zb_b#K2q?FQWCj zaNa~eOYnQhM!r-nA_?-gS4G7TAam<{>LV^Lp2nq(pph2p|8$Tn{6Qdm(VXjyAEp~T zDoewh@uNkw!M!dbVbL@0x?cTJM5N|FtJIii?zfEuo|d_DC+fijD`eDFr)aP^Hi;n^BKM z)V}3dctM!tabF5A|HPh``wfBYb*3vE4Jqf++dL8MOQOSkwGf&Y_IZJcTHRYXebWF8 zd%>Y=Rzoa)ULOJ~+nZbG@Y)86N?V!8$@%7Gwfw!+NK^m2iL+)tjJ4&|&tfV4YD2JN z?Q(!{PuKqvjk(`)Cfq!?Z}fB)(Wo2_bw7r%@i74CSYrE))!Nd#cT2^2>^Qk&Q)P~Q zY9^dHlrWFW9MiBK&0zyE6GcWuTMI2SUx z3&}p}aknI5J#l-T+v>W>t44Za1Do=;%)?~QsiMdBK7}jIDm!9fQfZjI;Z{G=# z&fuEvZtU??V#i$o#n{1P*`rd4{3DxiAARseDN9z1QH+pNI_Mf@%Cg$X4CZ==?3KA? zcSKfKa~Nx!d^$XmhyA_^nXoylvG7!TMJ$Jis$BPGE^8zZ6;Ip0yRk62u%^1G6X=*l zGhRxWR`avu)Yk=plmuKb3|ME5&Klm0aX#NY5=p!PQ+Qvjf(oO#tJ-0v)-BsukpN%V zn#V;5&xS=svB_)%L7_i~B{#J4!4%*z5`^lN7)V$64*SB|_1-=M%N8jo15j?0@zsL0 z6o8aVQH%nElC#hesMG{n+h7u?(poZUvS;7zaK-0ZAKa$1j)NHT7LrrCQ3m>(IT`@HmRHNpa zC8)S_IytZMcw|L%9xdW>n%|V&6joUU!@fk-?li?s8p&8!y*`koL9|U17a||Mdst{c zAC?qC33WMjq*1Befr++XOVE26CTj>wSnpWV&XG?3saECG#9-UYa=j|GQiA}GD>>$q zC5{Oahc7D^!xc|sXK)oHAf%QCg2BF;Z+UH)pM?QryKxT>k7SL_<*+6{RI%rd#_zsz zO>{qX5cZT6#WBS|Eb*9@m28+y7qa0t8=XR`_u(dUOS&tClnN1#^k6C6jb(a`4_fv; ziZUEl$K>T#21$il@&w3of#QqRn`bZf<_zR0Szt*x`4;0ndFA&uQ2E^&Evhti zYf#fVzUj=p9BYE}|2OOh2kzM*KLH${CLbIP41PViUPaP~cTmJ+its<3p-0#Co#PH! zO^oVBoSegPLuMguc-iWwW7d#*iqS8jX;6Sy@Rn_cko`LTs9PxmTK+NJ+?@VXL=Fzr%2+v|SU=NLVPiruv?7e&>^eG25fLN_Ti1Wze4n2;{^J z<}>Q`u1^)HE7uWeZP*wnVbXZD$h}(grj#*%aH`~qIyG~tlsf3fJ_(Jicgd#ox`zAx z*ZIIV9fRq`n`y6fY;5f^U8~U>oKGpbM2if&wSvB1wjJpHalr>p6x zMBY7Ed3fu=ZHUKA$#zN3v$C*x&j=(S@9!AF93OB{5w>8#E||JcrRv?4ukdo)HjgXi zsdiM*L$$V=q_zZ?>kSg|2N{dixzp?6PTM!a^F9_KRC>XJ1B1v{(X+MHeN)&>7TYol z+KvRz076ohvNTW67APVEF@Zft*4z7nDLP)JY^Kx+5ru}YZeB2<%k#GwIMFIqVT~P( z$Jr$A!E$c86q*{$4@0XyB*TDXqwr<9jdpix5B!8>%6w_uQDu|<;4A?JL2+?vZ73Sw<1*#;hevup&y>bjhWSRbSoJ## z5OV*mmvvtrsNH`_d;|fvnUeCHtp+a(;$M61P7?&<_;?lq2~IlzFWKp4VNSj&1hee& z$#|?c9(4YYj9B4HE)5mQyu)IHy^oO%s$&Rkg%7a_8LJGs>%y$+$;5s6KUXG89k=A~ zyZG=WH$0uRCAvjHp=|a{I3<=ES~A`gm?>qKOnZ2LY2`8qB|+V(sAS3)&Ma+7G^IaA zO+3{qNtLdThknG9n;rM=VRbHCsSt&xb$kLKz8-F~^U2TyxpepwYJFF_L0gG?yh+{&=k9 zK6=xpXk~BP9}hsw@I-7AYwyPXIh%6wVC1_Bd7v;Xn?6%tU^Wcj4NIi6(0Ed6_8mkaFn<*<0&W(fqIB zE=~!4-~DdXa2&Y!X)RdutG!$VI=6A%$}YFKwY%^+N>uc8L9L|yays~k2kv)2AD3axICD_-_+)W)(={P zKL|DD$aIACn=ixZ~{3dGT!a=_mDRo(Iu!f zVhzlAc70Mwo^EK4$VfkEUb!KnDbMXkdIk_ZPRkBbpG$^hzAZEEVD%jLB%x~{2AR(@ z_3TYyQ7L@9rw)lZK9eKt^#wHaiN`q6JG`b9KA1mvE7*$71#(hZ6eewnKM$Zc2Sn-m z08J}1)km6e$yK4yf`Q2C*g{rBoyBF;|GdK%pS?dlavvLmrw8u-hnbCg`b$^ar!Gx z$@tJ3gXPPP9kn;!CbbShaQc!Cv3>TI`~@^mmM4z&C=C~w2nKSI(G^}nq^m|^deUAl zjbs{K86XIV<{Zh7IP;|QC|;yq^Gy>_DL}KlP)?QTd=N(^ZM(wiXa&@Hz?sti=9ojO z%62srh+K!tAc}|dP^OH|5FO*8Nb3SxJ^$lKc=#nBDeRn-%Iz@@loxOd`iIpm)DO-~ zL+np?EL_^hGDf@9`M}23m!6IvT$^#UKIf0QeOo4SoXGa554Exw4k~4&DuKCjPVFrG z;r^_Gad2v2mR+R9c?}`PH9WSd&CP2-&zt`Qac5FzeO;0Uc^8Z+0^d0wUaq0^DG`Ya zyDCsfUoz>$LV6pz48F#yok~Htfuv<{nk6=x2~n%hNw2+Gi-ce#kyJHWv$`(^Tj498 z>+}cH1rk2D>Bs^0PeXQg1Iqz^uu)b)!P1r`pskG|0VcoW5h4{qff^RGp6qnA zNtlV$t)i7We-OFo!g}*cHq(_p8NeFw+_M*-vdgnLI^L0K#x<=Ssp25!_8rI}F2GVu zDRr+TRtYlMW9pE7ajorwy9|?1zG~H;(>k8x%#_#oM#oc9%VC2ia8(_WQkIlSxcKGE z6Kq_KMyaFh)hTR!LxXmb_TqJT2jE^$L~dQ~HJjVpeXEyFy*~5%qnQTyw8gvrI|`&` z0;kFhK6EB$&h8=4#IR~yTVgNsoGoNR1$FK@ifQ5_J!unZ${vGd2t6`m>3hd5#Wj~t zjaOs`Rc5lc??|_socZ=Nuls`u7!jk9sqnC{Mw2LCvV1F?s4ys?R&QR@b6>(T`bBOQ z!ri*C_uU7vJc9G%q^85Oq!59xP7=pIWGUK?+JXqN5(iR(4~`0N2|haMc3g>t&$wcB ziJpY#IrZMcrSX?7>xVw!QNX~vew*y430xTd7>PQ5$zdqL$fP8kF|f2JzMwcyEtx{| zNTJNsxtWMudWyf9olaOsZ-H38VZ4gKjpLr{0D1PH^zo}|RhHHn*{*l=r9ZU&iTtWV3EaIXcTixtD zo{K>#&>Gi5486(n+%R@Z-KzbiY;Qp~bViQQD8-5590slLW*llV5@E5zm+;~=JaRvJ zEHDwRz8d_DxV82rd6M@iPH{BK1vp%UtA0+7ps|MA+1uJ&WNUu~fnvK=__}TP>C88H zGI~>|T~7uj8U?JbiPW`^VeBBIp99W12{x{RQ1Gch7OSn+%wW+?hB#eqWPK;2tHFkY z5r{;*wI{ZJekK4GJ7;+DNTABUR`H9Dm#6RN7?UVbZ^Mo6I-z8osqdYYrkw@@J{<5+ zi9B{LFV2$L)o_BBJTtZfz~fl~KHM-$95op7-H|83QG5XG@!>HfoMz`kP`h9JBR4yz zjRy}h3nPk1rQ@-H2L_wd!d&F%2w@&Tnx(IBZbVL-(ebNOB4_ZW^!A9Rwho(pL5-R$ zg1asB9>S0C$~WEB*fN%peKAebMREGmL#}bZD*~&Lvq^6a=4O@nh#bp#Rdq-&2-gjK zjGIRuHt5oay7Y1)FF#2^4(TBBNB}SH=k{!shVlugbiChJCcGd-U}er7P3r5xe_lrO z2lfuU?#=dX+Rjxfvh@L`qoAG)9@G11*MpU))HRql&X=Vwvbvu`!uv$CSpi=+yJEXH zV+CD^J zA`&23+@Lw+=x~56(Ujo;2g5(EZ#N0A3sZ#iyLntpo?K=LPN%?HGO32|8??}AgO8r~ z3i^BD^}6gSm@Zb+r?nvmQC%O5hnfrezT1o@vlfU#C$wXk^DV-z1KEsg)jUAP$0$Fl zSM+SJ4sPt#^juB9eDLh*Lle`?V(e!Dl%E7FvP)d%%Q}FwM(HFvr@@mu z=(7xO3GnTQ8Mx#qdLwiI`S?EjJyUbzfl}Gq!NKSUDnz4|`mlm~zEG(S0;cl1l%;&l zJBoXE7PMWz(|PU=D21D)2>$)?&uq>Bjw*v%+e`2AVk04V3P(rc$t zcu|%l*sa;EBuxck;&4+eTI7P@-rtzqIrX5~Q8gtbxDW=}Jzb_$^qo34-0) zf{c?GNSI`|6-ZQ+F6C6Jn_wp5=s@kvkL7GbP;P?=YMOL-)-;oz8hM!zUvW1Psc(ji zblgoqIhPmd>h1<2kPV6l{asTP9*?XUj2mJJB-1exZP6(yX42PvZhAi*FfK)DXd==% zOzibeCi9lhlR$}NkWdDIwkf(9 zZooI~O#5=Tij31`EBH+o!r|BpXy6JPJg*^{nR{g`d~31tq<>8k!pr!X)j-&fxH3Z~ zWVxWaL86+5>f1_e3Q=6i_=$HVPZ&dmsdu*@QQ2yCT(o-0H)5LN>)y4h)UCNCrAwb9 zPGY#CD0n@)-2SbA3$HXkFEKB3&0{6W;w!n&!Pm3e3?jyC`(FexU^fS9P{-26u`XjyCrq+Hm2>zh0LN3VaOS5|GOFpmOnL>9c-xwN)x)0VKYS_AZoukG9~+ z-PEiXcfHH%TK)8_?(?p-;$GL-1+Nuwq5m?VR`okh>xdN&&SkCaT9j_{E= z;+Opf_r580#!}}~kxe^B-d0Z1r=DVU`3+EOqbz$?xtI?(5*jyPOS9EQX6xDaPI{rO zE{kco4?zqt!KT5>{u&I!Wm4FWG+69}3<-skE)ZhX2M-q*kyQ|=%oU*u!cZ-Je?LYr#OUK6Tu#dU>>PDmXW;&_-~6LM{kbarY)qqApjFQFGaNrLn%*U> z&-K6yc&+)i@9+G+C5^{`8X{#+J3$^5BBKIposcwDVlQE%CjKDbn64^yTsyasK#Kcc z0g~W0gZkOm&3jeLHKT8boGvG<v}>QZgP$QCD8g5s1C4)N7Hfcv=i_R z#nogtnwPB7#-MB_>7E(L1TU$aBYFA~AGv1o9z#1|HC-eHvF|THTGFd7ml0sNziN&H zu9VvRrGo36E|yK>5Xswbca0cogwFi1pG3~j_R7XbAtSR|Y}3oEi) z9T3Mg7ZoE!to-zdD zbrz)y8#|rw!NjiJA|-xO_wZ ze5Xelr|UO0?#_sz=|H-$zIjO$e^w*l_LsGtH$%As^7{ zybOw=8IQcWIGqf~ak{KdeRa7k-a@83n!O^i!FABLv6JV|_fHM45AokO%ZpySXho7EQKHwoujwaXEXmq_a7?mG{6l6oDV@VTmh|-68C=J)?b!q4(+~7lTr=q_G93l_iKP zp2mv1QMXVo^U+=+Ify)bK8}4=w5J%bSw1t&WWM+*BA+LF9?37URu4nzQWZ66vs z#--OUY6Mk#7$LDrtN|SqSYrT2DhJZP!1q z_dY@QC+tySK|+V=WKt#`O0arsn^ZX6p>vE`buAR46XvjdXcPg}zSc@d?<|isrFeOA z%q&K{vyi#)#J$})cXVL0dBvV~RCbd` zaNj>+@8$->z~5ndL8%tMeN+O;Ff~?JEH)iKu9HG`BEbN7b$Chd$x2ftu!?hr!^BUB z(9!kz1?HK})WG{7Y6_TJpT)u~-nWeA;~CD04lB|)NUWNN{mF22tO8~Hl9)v3Mf?qR zY|s0((Fp9AN8e+@Ioj*JGd1Kx?ni!%L4fuP_$Uti`D>`oTcEV390i{Zdf_>d| z;@fi`vWnNmPRI?rw)o91W&Q`o*1iEyy@0y>MyK@{ATJ!v*s*y~2jJr}?V5hLp*$n~ zP@va%u6x(*;9Uy22%To9Rq)@)0}UpVYmpq~)uWnYgi*~MC`Drm+?_#E9W1gs)5w$Q z&xV|Th=iK=B_ ze(c;Y;C&uiNOOh-jco`?v&a&URYS@YtaXeY%z}by-}U#|zsA6j9Aj zqUU;rr1N8Dii$y!|AzA%@C_SKi&ZxDs|`Qui%~G+bQOq{u1@b$yD?<&F!{Ldk>{d{ zfjy0*k?I^E(j`(^{u&fow&zl}m~&nFxY3Xy6?E%s8gAuEU>Gzo>W2Ur-&mR`Z-@KX z?urFlU0{eBgMRL!8^nln2|8|J$~g=rT$i4Mg_q{)>m<^1}n`yeWxwbR=>@V z>ggN(POGr`_Lh!O{u)=rXhxrw)^I1$SP95)yp%e2WgFl(5@de6owJw=CFQ=H%GPBEM9rJ z8!gBHKCrqA=f%??T|%Zw+lC)uBdk}#8+Bm0tES}je`TMvG4IgyMj2Dn=C^CDxW2;%u^TkC4@ay~XYq8*JzE0}_s6VO@k zcM*y_?U9#jHh-@gJpPCNpB|stVSW64x+2S&Dk4)@^HprCYi>pQW3tx761AwWqWt!g zU;+%!BG??q6hY_rYRqq4_oq$-D(QVf`L1xj>Or*DieHDhH_1KSyTD&vNNXKVCI|hK z*K_*723jb9jL<@nxXv(J=k+}OaM_7iF!GW`S{%MWUu!`BoEe3d9uD^^Y?X=?t~9p_ zq%;aEJDbi?XcTNGt+@=^dRwzJtQIY^>maM5*524SKNc0A293wKSMbru4cKgpr;YsxX1O4H`UfuH%MlIraE$l7lvqsq&Dg#G{PFAS9_r zr#HdB6^HW6A%l@u=n`|_T9UsriDU~6p&j97Tt;PJ~AOBkB{C;JH00fyk$wuzf z;O#y6r3AvwBmJ9qbNA;cmOlkIpjPm`q!8)!0_v^YA6Y1O*^j&9r%MDXPCR_77>_P; z-9NJAcafedi%^{m>fyX^)j(N!FnE|?-n#_GJH*?oW6oFN*80wqXBBx@Xh?=Oh}2O=o9*i= z*xTm|bSGvaewnq&9A?rkpZG*%Lw~VZ!a;>?xpNGu(vC)UT?mgqh;YA99q)x~?)`mgQW$Xji+VA1GC3=Q394%*Gd4KA7vYJ(|E!-008w znEiSH=4?TEi~loO>SBhS3HH(ES*b<4Ugxc6XhMu-CNXEsSMWtkg^gND zfB5rO?$#>cm#Zsp`1ul{4aHL$;3GJj*h|igVpzbZN;NE;_*b%&n;RI0YlXLqgY5Cu z-_ES*L3PA@B$k!TonYLgb0&%Ym8ui+$^bJ)E0JB<;G0Hp#lEXEu+i!2fNp0NRbwu0 ziglK)YFSir8)uXA)xGOl@eS&a?Uu!584W7Q7KwZ~jK~EUw^1}%&!8I~&^z5!!CEe+ zNU2d$Ya{D*#5bzhojoc&V^u7geO-4g7<}B3RJP=V75t=+a4zRT2%dsTx9+{V_!j8`uDC-l5JJ0JwFiOsGiE5{9z2&7%t zymceI3zgNG0}-~`J>fwu>1G-a=K@&O=n&nXjJent{`l&WV}ozat(ej4+C=}^F?t)1 zZrdjm8UQ2>XhCkGarJhW&kN3@m}G6h{XHWx@1g{;0hziksGrRwW5&1Bs66fKbdvT- z-}1};U~MLHo&MVtVupTA4br~b_6JH1MY36JVn@)g$~_~ zskBCT6TaZ=4dZaD#ZE%#%UlVS{h81o$31*|EX~8pp9uN@M}mH|k^1Z=@45^HMP3&! zD(J%cX}TO!hr(zMdr2J10p3MOMlti+u26@jR<}*nO)V+s>~!rZN!t5Sv)OuT8Lx9J z4@QP4v5>eDXH}+ij{i@&n3&!-!iIx&PPXxpa?C{R&c^U~dW%J)H7eQ)fzBO-^=<-x zL3#y<%bsjRLI*bbWb2sgKzRO+DrcK5am(a9{esO?xXu1fBCTZPD;tIB+5e2k9DwUz zGIp{k$qNRrEE%Juxj8Pa8B6hWJr!+h;SR({M+_>Ri7Y=QaNp3DKF6qjoVW(@Uw8;s z{h9sz5xcghsR z7#fF6+7DPgN`PRZ4o(d|HcO@N(7h^tnOrm1+85AR9UBtn~274f&yKT~KlJCx)h*`MPv73-(kkx3(zJl}N{5IbA}7tA|+UcIU+uQ+(@9nvek zhk>%}DrH4>b>{Xv3t+ItA)G%HgU$?C@iAp!cRi;50Z!Pr0LBrp-BWD(u^9EPgET5G zlVXYV47ceqL&~d6rDW0=)8PA&w>^ydE3VVrxadU!0u7HOI-TisWy$N7bMoPJd7cvR zRPTAo@vV9L@q%7p(SEA_E>VOaV*jkN(Ave>vhu_7a()Asu70UONADh5OJ2hQ)MBKG zbvgSwJ<$hd8yRZe8-u|Eyw?ET^ zxr_R^%o;l&hULl`l6$AyT~0`+a2Zk#Vg=Ama|-B%MNsdOEG1x*v$7qa(Jfd|8=gkw z?Ise&9b_+PQ>Aw24E?BJyyk~&&>b&yizwYk0fp`|SHDfOrgNEaB^@1BL^|5At)bwkuB3Ap!ymy9A zCNc?E&*6}|hRzD@Nt%3k-wR6oif?f?M^)9lK1=ltC#@S=Tcbq(*r9)*e|a%rU4t9k{2`nenk<;ELAk*|WkT*AK!CJZcbvHcw7i6NcTn z)8#LHRk%$s-OC%ZP?dLi!y_6{+tX%x737fEtZ&6+S=EZl3TKf{kjdq%Td?yEqQu3U zi8{wgpf7=2ffohXDwOw;ZjW4f)!mNaNi}1+C8c7YmAkjmL@_Du?w$%awcKp)&PGS3 zKZCXRzimun55dDENQIY_B9T+io3kY$_n=Xr=}Nmv4!iM6ETJ;)uWA?k-n2%D^>@ry zJ;B0>)^Ony*Um)1T%xRAW9KxfC86#P@djyFwa~}EmZhf%^Lp37S@-cbjk}2SmhN-6 zYZB0~&AG-5>8(GJT=k6LD&swA27UX>ywnhu**#Ill*Z@!8!5XRrTt}&yCGO;CViXK z!SFVH#(}~#6INDaDFOghrt>}2Ds3LClqZqd>sD17Iss*-FUOxQWkl?CN!>reucKbR z5^D$szic-)JkYg*>4mIY5@>vP!btZN&8vQI_4{yifv_{Y6pheIla*yxo|6kL)6>@e zH$L{1GoWIR=zBX&T*nE@;nxtN{;UpPVKD!@dMw!4L^hwW4tR4{p&GU-@EB4#-Bq(r zV+gOYYLfR9UqJIr4})fUx1JHQ&4u>7{uE)tV(2B_d;D7)t;-}OHV`MH%LE4Y^FpC# z4+sKy8+V-J+ST%QugRSriIktTL)MJ*HVmz1gaUYCITj#nPRzh>B@~d$Mq!(^7MFwN zwG`I|v zLPjN%`1S_=gVgTo_qPoYs-G(nMv!ouABa1c4P#C0e`>s?|8F%fFbvae?hkwKy6xG} z|6MNxCZ<3ae=zS&aixV=CHurQWiDh}O#~DOru&cf*}fTL4^<%sMkky-Mu-u)tIX1L z6(!Omzd|?m=3N+1G!t%=Scw`U(U982I)el=v0B3&2zfa?Ak~%-;rS*kWX)1vv5#-K zzU>EKA~`guq@l+19&fVmJ@QKV85qHbS}+&|?TCXJQjxr7<0j}`oZ~sSb%obW6}Sjm zs2G@3pseWx4?GmS=EHPhG;$T)TgU;;_MAJORppx>qr6AXy^>&eVey;NkJ#+Dbv;h< z;Gec(}B;?5d!cWr+K&v zw8CasrAm#@Ui9A@Yi<7;QmkBi{}c>q*kh+uc=gvx&BfL^hw%3%D>J(NP?456S}uZ- zaH>&syLlnHY3R}wtNDN!0>6>WkWHnA^ zJI^XwWW>0iW311Z-{WDr4H620-c*zjcQP4+w`TFwye&FFN(?&fJojTq;Z7hF+)R-H zbL>~2_e8lS!j)gH2>d_V-Z8xHu3O(unkH@1ps{V+Mq}HyZMHER+qP}nwrx9kS9;&~ z-p~H;XFuQGe(2GoX;!m-YtAvwd5vq1p$%hLdJ2pFSM7B2zXPc~EX6-D_2U~qe*JNq zrhWZ8qWr#EU@HDZ1rzM>cd)$7xM7GYTvPUpxHEDMSXtmMv z>dM{w2FH^%vcyRKd6>uYP3-VUH~FH9(;#@E4LhX5iqT(OzPWG_i@b1ztE7<=Q4ifF z_q}(06rT)V`(PQI7g4*XvEn(f|9Al;56C92^a%i9XpO)^V7=StWkC^H?MZ3UK>5ez za8J^AzrY!x7q=hqpK5C}wa|o=f1VLH18nRVYAgAa>kyEQoWmXPWd3*p< zLv?EUY?BC3EV|qk{V_yQ`++^xxTE@VInSQDAM@`g;%GrOcKE|!=~ZlS1AzdoDXg&g zuWgekk7Jb7#2|6v%6z0nkZc>@Y^-#*sdmLR+<}6gv?2pCm{^O4(H)hqp%+5dy-c+!` z%t^cf&5mCH!%e2$jE56w4RfY)euP?J^$xvhpm+?0%{RI8rr$nRQZJ{l_HSdq#giZU z|4&0lEoK*rrUhNH)P3e6`=d>gTTntir?89RU)wE7^-S*R-YERrtb!u{Fx>@Fe$WfQ*#VX1GlA z|CWkkW%bvh{wWpVl{)lX#K3{r;xE_R^zDDv07JwL0a3rHI&A>j;&ZG% z9Y0N292+a$zko~;TwWtoIcknyK418?&JWN-uL{NTBk1gM#=k(mQ#CHEU(SP8eIDD2 z2K=)rorpH?A&^Cbp&()zj281wdMm)y!;^tMlpn?{4SN?|oj|mzy<{+dj6yd8pcOfT zr0>T&^o_FaZ&WYm9#bipM)(Po+VQP)r9$@`uV~cKv)x^_!^{^w;m49vPM0aOx}d*c zAr&ctkZ90ow6|??r_u=Bsm(wrS~T(q5>M~pl75R$v~T=%Z2tLiY-YXtm-5p6_v^r? zDCES%*3`AEvEQGmG_7;r0Cp!1qmNyF1?WFTVM2!ziqgDi1ZMd;Wqa|OZXz19#05rlxACAL-^Hek)<0Lne@wiYvA+7*@5 zW4-WcwgrEssl z5;#(#b{rZ$?%n9=pnYlCf(-KBbl1d_4XGu9jRy)xO~H27K{RuR1ZRovWDHx>6XT$@ z+1S}lGK3QP=dCZZ4; zqdSRh4^RmK>40f3D#!u^SS|7czq09(zoS_A{rdx5D-@A{^_4QDW37kD>IhFpZ?d=& zQ-y9fp`|7n`Tvn5Pj@>B1g%fbg~cVBC)#3TSNABKg{<@+pF$$e8e~}%&v^PJQ5Vp4 z$H~)gHeV?^M^YNNX9=fFMD<33{=*%W^4IpLx`pnMcSmjs!3cw%%S{Ss`~0DfaS~?} z*3>z5qA*Y>ygzYTNghaS=|3J~Y={$Hd8Igr?X$aA`~e1+ph|V9JEP%Vw|uJdN~4MI ziaXfC%h>tdVb>=B7s?Skg!b?wBjmYkcw~1IooOPt?pT$_3|Cxvv$YUA?FlxUx1XQ? zb>=z)SMWEz{Q)4wC1s#xdv#_)gACnv8}}UpoVyuCPg6E;lV`ccg;{REJB=$gKfVL@ z=n9y&xDYsMCV+*l@V8+MSgjXO+wlzY6+ad>x{tg$r}YuB6!AS*8H#rhJisc`1^Ljz zbFlqosX%YalZ8$H$3ckeCA9ocD&~;0_j3C;`I&Iyui^rrjsW&A#f2y?RQe0kc%PeN zS(JXERr|gvhG$cpccb&R?IN;wpm47^<89lG;}S74HY4noo|>-WbFTGXxgZt4510sAR#ZTl`` ztenM(`*wqWb(aW4#s#mS37*QMa9C!{ZLx6F5EO~5yJq>mKRShLK=UZ2b`Mh!B5kD2 z&Bo+7a&P2Q7(BNf4a;dF&6h@Zw>~`XX)Jn31+Q{+99B>CJ^kSN(UoTp&WD)$D{)~88P_F&e+M#SXH(1Vwv;RajVb+F!$=r=$^4!)K{0$`*^0kMTG zF8do2!4@x^LF#7KEbpOqPvbp#bfaF2IVr8tbtSp?-X{HJ7YE_@kA977_H@!p9i*7( zSBqJ@CHL$BM&>wNNj&blQdb_yd?Y|6NA&C*zvZ5}`hsmC+Yi!_vcdKHAL?m=Vsa9Sc=tOI;rDup=mi<9!M4< zzjg<5(C1-i+2$<%Twg%4r?pTueOhmQ5AaKWS*3q^@o`k%4lPy!ctSVg=a?Q<<7IYH zs-fZzMvfkM93@HK^&v`%5LTr?x9oDhL09FE$KGHcevEN}07C_)+7K6I%oTbU6rfJ* zt?6bCa@FrxS&Q@i7C{3<@KBW@W5&dgGBDJ;yRbGJTUuY^?Gy8iYYfc)fYsOL25m=A z-1Jr%dN*Duq)|e|D7L<{??@kXbWgtS2&u1g??cj0rTfqYjbYvvX`Kq?(Dt6%>w#4@ zI2zkH-`O~A-AAeUi>v{0I)b~)n~-szVw)V$f18&ws91LBuG*vaCnNarOy(@?tg5}!Jt&fUYtU%_RJ^{f#5~Jr zluCup(xPrwq}=h$@#6_!)2mvl>>*l1^iYmrUM=hA2CN_ZV6bH%ein#mN`(XE+`D2V z9JwS+zcj}Pm!;l!l}|w3VE?e4o&KerkK-><_VX91z=GGvfR7*RT=TbUM@|74brv`D zW1;{Qx(c@IaJMX(1nGttN&$`D;{jH8>?0uT7W#QTjeyMRbQDEV= zH|A%jGyxFj0Hut_An2Mh!!gnbftXx7D=FK=2-J&8!Zv*M#MUb+*NspwnI(H{%atPJ z$NmM+uLSFNI}5-t49**fC*s@D8paxXqJS<}jhdG;bq0!5mYX?SZ+CiZSBj6j zNKJ0cD)>QX=bZ%&@S><2y=~u)xM;b-g&9i43I1*wg`8~vm%Wl zX&A3d16g$DJpSs(b?N!UzyX*~a*U#|3pBgrl;v?=CjMYXf)6vMZGFD}Rg}hi%^MiR zWlY#X)t*GU2tlyEe?_|kh$Oa=qS_fe^dK$3#z=E?w`T6S8)YlJPj)<<5 zam|p^2HPc-oD;8dbX#B)%1Ml;dgE9cx~@Wp58IxW;XZQ3*HaiM21FRox&r#FhZ)(^ zs{zTLYi@{!1|PkmZDwp?)8}!T(B_)VRLvB>pKSl^8t{A&oiz|(ZM6K=BrHk65}0S{ z#l~bBEVipGS<)K1_c)2iKIN734ro^7oIUupf=1M~g1P=?^?=3!M=AaR&Ppx`&PHhQ zZN8OwKEMRq_EZ+K=&T!MWxj(o3?v-z6&sSS0yg_hw+EM4@(^N6{mN}>^H=iUDCVRp zlX2b($(JGY-d%A`a;py;ZNx^@{&4x#dEy~X75x1A;-ad&munQMp8j#)Rvs1HmrY zDhk*mPrt0|d8}HRH{cTM2(10dpnz0P3g}IBnIyLqmupIB76a|@Xs=^+Q~X@Doku|3 z#q#H+5D20G(^`AX#uRTj_h&&q-JLtV9pm#YM>iu7KK5+Au6@a30pfTS-dEt_-70o~(LZ7+-}uIXs^ zMp%?}`=Bo%g%&g5ZsOjl?D$vfEQn7c-hQw#%4|v*2}5yJi0Php=gDO_)Xf8_5X)%T z^!mM4=FJYM=N0KD+Nqa`tX2-_{ay6=tmWW6LfgY{fPM&#O^6Uza3BRIh0LDdUjARU zvM={1CKK%o%p)21XbJ1ELk;8rNjP9h`~C&zQuOy9GI^Qbp_=IWs@jEgpP4O0yY&hp z;<-5bD22FVfXphZEbFp>=b0!Q=k0*5!v3OU8L+k~$S3HHl)98Mu*yy6IfPjQU3>6w z`jyCP_Pewh(_2tqUxCZ819-cnQY#+iE${)+%^P(4aduKWJR7hDuE< zb$HXevqxadpQy^$f)E9SY(2zRd(fy2k?v3;DMOEQ3%i*%jJoIopxX@lqh5b+6M4yq zdGka5;_KdUK!!|^AqtJF1w8?46aXCLwH@>IKU(>94grJP9k z2%Nc`8RjnEo40=t;L4_g7XsWL?kS*F131`%W7v@wIPD<5c{U6F1eRuHhZq_euSSob0Z5zXMK4Gv1>3>t=(@GY3K@LL#;j8a%IZ}nL5>I<9)FO$X z?8O4?#0(mOUT0{wasB46dS;$9R?`1|zmsGjmPA$ZLj@i5EeLE|mtrk*|3H<%9JRxD zkvNdvJHH*2kt*C^qW>F9{pMRVm&S>g_!^yekJU4M`S$rp9O<`ml0SgcGmOV=1)B61 zTEw%|L7$1W6Rn~?3?e96aE<%f7`zSuwl8{714XXp(?lC z@girw8YL#eutV<))SQ0279}eZ4>6djLYIq|0X!?k7-)vnh1e}HA=eYmkoY|^VyaRd z@%VZgk2B<%1w~WIavwwyu3Gf_cuMzuCiF!o zyIm+#c=P;rhx|C8T@RT;uEs-F1c(p38m+-5WsSFpGGo?0!>F3ZVHE$^rp*F8R9XtF z%NO=`LP!*~Xp1sUg7~+DNRV%KB~HkDr>=om4c2!|a}v-j@cA@jb0W>u3aG2e~a*&-botlKe2z*_?o}V%loR>VdRSJkk@PSez zxPnT+!}X%tkrEM-P%t=fX2DCjVGT*F^O+1zEM9k=(I!KWJBdx9D%@MOSv90S*`O;+ zRYAXqsq9{V5-+dhRdLrbP_>fj#*r$bH>H-N*&7L`pFhw)@aJK?2h~XyQS73^gP^sv z8H*$bud3K_5}LV*))=nTRAHds;v;YCADk%=e?W(?vy1-c1|Is~2le4H3eeJ}9>h!c zXG6a2{&ZIxkYM+3VHv*rm|r=QE6L=^Vh%)R_v|-rq>e zj#UD9Z-VZF#;B5Kk0(KbLq9M6fgzW{yRcA?BC74V2N zjL-A~(9%|DC>i4_hPiw-?mCC@qo#bm9jwm=ft=N+r+gnI-P1v-T#(5sAoY(y={g58 z%(bcd5~hSe3P$Lce48DqA+xagW0`{THBv8g6h-;atc5X9$@foA-`WLWp3lDnqvaCb zCj_{t&Xu*J#A+f^9i;{#(xbWB@mhXV1tr5d#K(%F;D=!)=L855=5cFup(FWeJ7S!` zvW?u!%mD_>f|`JL$BBZq+VhST^H{*>G9;Z#u+omf zYg(r@qetT{TRQS)KO{%n9q*r9+(at&Y)NpmusXo<)Z87~3PM*odHAOG4nY8>CaPFNs6A`=5}MMM z|7~@+{5YV}xR1@V!q%w&@N2;ZwBz`$5w$XH^z zLN;5pu-UbdMeAq;AP3r{&@;gxZLgpBQPN11<6rpurN;@pe>8VbMPcaa!o4omT;Q#? zrT32<(=Tip0eTv1$!xwsw=JM2GWl^c#bhjuK-Kgf!e%sehnd|A#|(ZcNBU>Wf?Moj7h+(8|1V10+bjuqmb3iQ42i!mv1Ktbe5^Q@{%qX6 z@OEgl@WK!~j`YH(1lQ=q2r-c_p*3cv(Zt7q!JRI3ib$}=F?e(auVPEfDTqCsNSkWY z%IHI_Aw+NXoAtN&OfKih>o&~GRwxF@T)(xcfKuN|>PS4l;Wn2hH~}o;#$c|SRDLZh zMKkTkzGjT>3Ihc}F`@lMYdNgK{EcD$O;@IQ(7Z8r%_{~8fol0T7QVksULXWy#8-BY z-=L*l+Il>}m$b&Y?}a6*DI ztk}1vhi0%Ggjo}5^aAdv!2lV@)Ze%rH<^4Tu(~2NdN+RkgJWNDH3LI|LXh(-GLQCG z3o$T-2VM!pgXcR8WSxS2OSXBu37o1t|H3+FM&VBsnkh@+S7FU`EqX2|GdxH9mKd6Y z4L01H_e3z;Xkv38S`h%<%0DOm*Z5rk4CYt;{h#@gdv3TG9{{5smlTd^})2 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

    d4#rWozkVOII1r1JJ*D0>5!;p%ynb4_ftHxrp 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%0AY==$luwz` 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 Uh`V6ucc4v7NLsL*U)SgV0rL$BJ^%m! diff --git a/docs/_static/psutil-logo.png b/docs/_static/psutil-logo.png deleted file mode 100644 index e1c67233f5dcbed72cf8e421058824f238106d5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7228 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 diff --git a/docs/_static/sidebar.js b/docs/_static/sidebar.js deleted file mode 100644 index 3376963911..0000000000 --- a/docs/_static/sidebar.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * sidebar.js - * ~~~~~~~~~~ - * - * This script makes the Sphinx sidebar collapsible. - * - * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds in - * .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton used to - * collapse and expand the sidebar. - * - * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden and the - * width of the sidebar and the margin-left of the document are decreased. - * When the sidebar is expanded the opposite happens. This script saves a - * per-browser/per-session cookie used to remember the position of the sidebar - * among the pages. Once the browser is closed the cookie is deleted and the - * position reset to the default (expanded). - * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -$(function() { - // global elements used by the functions. - // the 'sidebarbutton' element is defined as global after its - // creation, in the add_sidebar_button function - var bodywrapper = $('.bodywrapper'); - var sidebar = $('.sphinxsidebar'); - var sidebarwrapper = $('.sphinxsidebarwrapper'); - - // original margin-left of the bodywrapper and width of the sidebar - // with the sidebar expanded - var bw_margin_expanded = bodywrapper.css('margin-left'); - var ssb_width_expanded = sidebar.width(); - - // margin-left of the bodywrapper and width of the sidebar - // with the sidebar collapsed - var bw_margin_collapsed = '.8em'; - var ssb_width_collapsed = '.8em'; - - // colors used by the current theme - var dark_color = '#AAAAAA'; - var light_color = '#CCCCCC'; - - function sidebar_is_collapsed() { - return sidebarwrapper.is(':not(:visible)'); - } - - function toggle_sidebar() { - if (sidebar_is_collapsed()) - expand_sidebar(); - else - collapse_sidebar(); - } - - function collapse_sidebar() { - sidebarwrapper.hide(); - sidebar.css('width', ssb_width_collapsed); - bodywrapper.css('margin-left', bw_margin_collapsed); - sidebarbutton.css({ - 'margin-left': '0', - //'height': bodywrapper.height(), - 'height': sidebar.height(), - 'border-radius': '5px' - }); - sidebarbutton.find('span').text('»'); - sidebarbutton.attr('title', _('Expand sidebar')); - document.cookie = 'sidebar=collapsed'; - } - - function expand_sidebar() { - bodywrapper.css('margin-left', bw_margin_expanded); - sidebar.css('width', ssb_width_expanded); - sidebarwrapper.show(); - sidebarbutton.css({ - 'margin-left': ssb_width_expanded-12, - //'height': bodywrapper.height(), - 'height': sidebar.height(), - 'border-radius': '0 5px 5px 0' - }); - sidebarbutton.find('span').text('«'); - sidebarbutton.attr('title', _('Collapse sidebar')); - //sidebarwrapper.css({'padding-top': - // Math.max(window.pageYOffset - sidebarwrapper.offset().top, 10)}); - document.cookie = 'sidebar=expanded'; - } - - function add_sidebar_button() { - sidebarwrapper.css({ - 'float': 'left', - 'margin-right': '0', - 'width': ssb_width_expanded - 28 - }); - // create the button - sidebar.append( - '
    «
    ' - ); - var sidebarbutton = $('#sidebarbutton'); - // find the height of the viewport to center the '<<' in the page - var viewport_height; - if (window.innerHeight) - viewport_height = window.innerHeight; - else - viewport_height = $(window).height(); - var sidebar_offset = sidebar.offset().top; - - var sidebar_height = sidebar.height(); - //var sidebar_height = Math.max(bodywrapper.height(), sidebar.height()); - sidebarbutton.find('span').css({ - 'display': 'block', - 'margin-top': sidebar_height/2 - 10 - //'margin-top': (viewport_height - sidebar.position().top - 20) / 2 - //'position': 'fixed', - //'top': Math.min(viewport_height/2, sidebar_height/2 + sidebar_offset) - 10 - }); - - sidebarbutton.click(toggle_sidebar); - sidebarbutton.attr('title', _('Collapse sidebar')); - sidebarbutton.css({ - 'border-radius': '0 5px 5px 0', - 'color': '#444444', - 'background-color': '#CCCCCC', - 'font-size': '1.2em', - 'cursor': 'pointer', - 'height': sidebar_height, - 'padding-top': '1px', - 'padding-left': '1px', - 'margin-left': ssb_width_expanded - 12 - }); - - sidebarbutton.hover( - function () { - $(this).css('background-color', dark_color); - }, - function () { - $(this).css('background-color', light_color); - } - ); - } - - function set_position_from_cookie() { - if (!document.cookie) - return; - var items = document.cookie.split(';'); - for(var k=0; kHXlobR4sf~g8FogL2PG~Hl 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�z^Ygg`Z!k%UfO(+fNeSw`3O zLK|iiLf6m?v5BjeFqsS2S%~v0#>?piE{dwk)stHvsxJPoi z9E!Tu$FH`wUvSt+s3wJcHdpXMo6JZUdw2*W^9RLo`-EtkhI1ghBt!YJIw z2#mF;FIi>Jnb&Yc#`w+kX>1NFS~g2{|6cZX+4%Wie~ck1sAdx~0!`DYZ*9b4u>cSb zgb4Zq+g85cQ#}iEvUdt01VvxcddA#ZywLANPbUjJ^=nCzLA;_kvCVX6%{s>utXQokbWHFuZ@ z6v}e?Tdy;?s~bHTCKyU=9^`~X_kkmvdhIYqGD2n7F3wy!N?nbc)%le#g%iE#I_>4P zoO$OGUG0^u&MlGC*W46>^5#y?y?cSWG84=5o{d*cg~05t;N;tv>F=q-JHJFWZ>&$8 z?z$Gvy>pS~S_jMXo9B3MR0~ZM-gb3er?R!1bMKy~+AZVpB{_KcB766C5n5Uzxpt7` z=~{GF5Tii*`{M$Z>K>=wHfvs&o0fx_W8FJp?YVsF{O`O8PJ%Q~w|UWBX^!B7f= zCr)zg^j@S)oHO6PLVa@sR-2UvH|}!%!_Qt>7|6E%Vd)u=Q$CeoX?~T9@1ErG=eH=W z>|)>X-T3Z?xc=GHrjHCi^FhMERoBV8-+zs`XO7Q*eUtt(XKAi+GWO|5_(dg0jt!FX zE%2N7?{Vby3z&S3WdHo{nE0CbAcg@0M~>3nS&M9G#Wy?6?S}=x(N~pj2)c(4GSJtE zp|qk2GhMB<_!Am~gY8UAtZW+OWG<=U>TWcAnzD8E2rWlx_~Q>)%2aaU z!Y&pbjqvf`UqyG-(AwzY;m;xr?CZsDgqWXSK{I5IUU>_3`5rf?^PD)lpKK;iQ;iAr z>F<`?OV-;eO3tT~Bok@0T%NM}W=@>KYyR`>8}&yB;#^AS$Z_el0Sm8#J&YC ziG<7LX7%x7=9YsvN*yRB6KFZi4(k>eD*}_%fs&3hdgmd!&5FsSU@|G_hK9pw+5EL|o=sVXQcPOF*eyAKE?b2wa<4TnG=;c~n2PK-0V=)+m=L_$JQ zl|nkY-Ok*Dhj=1MoF%p`4C8aPD02{9ndkG5?~uog)1oqU_W{9lo~pWPq6>3OOe|n4 za~1ocB1!UkE#_AOI7^)`obU`o(0%w6J@qP&M;EE<+QlFKkN=DV!=1DZ?jkdFkB3Vs z28a8ot*zwYuYSe#8{-(Nlhf~9CbKeyC~fE1kpc1=R5w*qS=T^t^bYU;;&W0torazs zvfc&4@hp$8f5uA2$+^o%v8o0WH*YXDv5M7Ef)NcedjB!9%fYKSIJ~WIp5&6+1{&(i zNGEfsBE!dj^9zUp6E0Cd+O=rmE@+8B4GS*TK9Nk51VUeZPApOUVP_Al(0x{z8 z1bv5wX=|zY+IGPup|iBOKuJR*E=91q7~BHLNwB;ykE6DZGMmcEjF)UWO(Yb>WU;X6 z)5DTPCKkY-(r9XLB09fHJf0>IiIB+|n9b@IuG3(7VF6ctJtby|Dax8?#^1`ffb^-Ry@!J2)dwYwD~8D@5JflOAzUS5vFY!LLv zUhWN72+WQW%H3wX9zO;W6^+f**SMG;n;?yq&c1e%-endS{a8!N>F91IurQ0q8~vKz zt_8BmN?TtSMk2`E%<7gNQj%h(rMC+u7hz#ONNayLR#j(lVuo;Z^C1pG2;9|mw6s*Q z_;?c8RY`MeH8HP;h1r$O2P6QJYN55S3n>+1c6u4JtBm?uHw#nqNLCjeeH|pdODrt~ zUU-5|QcN`WbYa%w%uFsI+uSrZRZ+YA zOcy?@qqm*d>H;gP5$ZZSDYY6*kIkUT7Mk1Z@y^bX(N$V{yHImcW~P=l4r&krlif*q znT2=?N}LK=UBSp^vD(b|y&)RAyReEB(-RAnRMn7;28gF}lvGzE<&&6P<>aG5RChHE z^`$INOp%prSjZ3v#;{ej@ZOKk@RNV_X9N;DwQX(GRJmB0n#LbYP~F;soC@RdMJaD+ zroP5W$m8Rm|INRnb4ND-|N4LVKltbW)Bn1?!T5WsFT{Eo#pH^$O^mg-+E+4JJ{5Tc zPdFj6T!8Nw#i%OzDTq%Yz2ob(Dy_v)y$nRB&_=-yXXtAHpRHeu5QXuP<)W3X2p)Qw z&88=Pl-FGo>%V@w7F%h9f8X3aa8Il!#z;^8&SJH4_}npE zC2jzw9*y(({^+)K@Ap(+u05UK!HZ3tQEio`uFflqaGdeIdD=;!TFfbcT9aR7T03~!qSaf7zbY(hiZ)9m^ zc>ppnF*YqRFfB1KR4_3*Gcr0eI4dwRIxsNI{G&hs000?uMObuGZ)S9NVRB^vXKrt8 fWi4}Ka%E+1b7*gL?*qR+00000NkvXXu0mjfPz+K| diff --git a/docs/_static/top.png b/docs/_static/top.png deleted file mode 100644 index fa72de87095a8e72d6a6380142733075b060275c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 215631 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
    xK(2h2J7=+95znSkpwng>k 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

    &?yZ#7(d(>+ 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

    N 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�iP6KuT9Ix(TID{M_Y}03QH)b18DGSy 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 diff --git a/docs/conf.py b/docs/conf.py index 03ccdcca92..3781f1343a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,12 +62,15 @@ 'css/custom.css', ] html_js_files = [ - 'highlight-numbers.js', + 'js/highlight-numbers.js', ] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # --- theming / visual +html_title = "psutil" +html_logo = "_static/images/logo.svg" +html_favicon = "_static/images/favicon.svg" html_theme = 'sphinx_rtd_theme' pygments_style = "monokai" # https://pygments.org/styles/ copyright = f"2009-{THIS_YEAR}, {AUTHOR}" # shown in the footer diff --git a/docs/index.rst b/docs/index.rst index 8be9f8e375..4afed98cb6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,14 @@ .. module:: psutil :synopsis: psutil module .. moduleauthor:: Giampaolo Rodola +.. title:: Home .. raw:: html

    -
    psutil
    +
    psutil
    Process and System Utilities for Python
    GitHub repo @@ -130,19 +131,19 @@ Sponsors - - - From f21aa556a2aa193b7c12f6116934fd28035e44b8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 30 Mar 2026 21:07:25 +0200 Subject: [PATCH 1661/1714] Fix image urls --- README.rst | 8 ++++---- docs/conf.py | 2 +- docs/index.rst | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 19a442b5eb..def596f97c 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ .. raw:: html
    - +

    Home    @@ -102,17 +102,17 @@ Sponsors
    diff --git a/docs/conf.py b/docs/conf.py index 3781f1343a..aec9811fc9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -69,7 +69,7 @@ # --- theming / visual html_title = "psutil" -html_logo = "_static/images/logo.svg" +html_logo = "_static/images/logo-psutil.svg" html_favicon = "_static/images/favicon.svg" html_theme = 'sphinx_rtd_theme' pygments_style = "monokai" # https://pygments.org/styles/ diff --git a/docs/index.rst b/docs/index.rst index 4afed98cb6..29fdc8e680 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@
    -
    psutil
    +
    psutil
    Process and System Utilities for Python
    GitHub repo From 04794aa03cb9d30287e8e7fd9c80acc8c4f2aa46 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 31 Mar 2026 12:34:13 +0200 Subject: [PATCH 1662/1714] FreeBSD: zero-initialize buf str for cpu_freq() #2796, #2791, #2795 #2791, #2795, FreeBSD: zero-initialize buf str for cpu_freq() psutil_sysctlbyname() helpers previously required the returned data size to exactly match the allocated buffer size. Relaxed the check to allow the kernel to return fewer bytes than the buffer (normal for variable-length sysctl data), while still raising on overflow. - :gh:`2795`, [FreeBSD]: fix :func:`cpu_freq` failing with RuntimeError: sysctlbyname('dev.cpu.0.freq_levels') size mismatch on some systems. --- docs/_static/favicon.svg | 5 +++++ docs/_static/logo.svg | 5 +++++ docs/changelog.rst | 8 ++++++++ psutil/arch/freebsd/cpu.c | 2 +- psutil/arch/posix/sysctl.c | 4 ++-- 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 docs/_static/favicon.svg create mode 100644 docs/_static/logo.svg diff --git a/docs/_static/favicon.svg b/docs/_static/favicon.svg new file mode 100644 index 0000000000..65a8a36c53 --- /dev/null +++ b/docs/_static/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/_static/logo.svg b/docs/_static/logo.svg new file mode 100644 index 0000000000..245e606bca --- /dev/null +++ b/docs/_static/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/changelog.rst b/docs/changelog.rst index 385d7a5e79..0e3d5df6cd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -171,6 +171,14 @@ Others interface names. - :gh:`2782`, [FreeBSD]: :func:`cpu_count` ``logical=False`` return None on systems without hyper threading. +- :gh:`2791`, [FreeBSD]: The internal ``psutil_sysctl()`` and + ``psutil_sysctlbyname()`` helpers previously required the returned data size + to exactly match the allocated buffer size. Relaxed the check to allow the + kernel to return fewer bytes than the buffer (normal for variable-length + sysctl data), while still raising on overflow. +- :gh:`2795`, [FreeBSD]: fix :func:`cpu_freq` failing with + ``RuntimeError: sysctlbyname('dev.cpu.0.freq_levels') size mismatch`` on some + systems. 7.2.3 — 2026-02-08 ^^^^^^^^^^^^^^^^^^ diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 8064348313..987d23b369 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -131,7 +131,7 @@ psutil_cpu_freq(PyObject *self, PyObject *args) { int current; int core; char sensor[26]; - char available_freq_levels[1000]; + char available_freq_levels[1000] = {0}; size_t size; if (!PyArg_ParseTuple(args, "i", &core)) diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c index dbec5159db..a868c92fe7 100644 --- a/psutil/arch/posix/sysctl.c +++ b/psutil/arch/posix/sysctl.c @@ -28,7 +28,7 @@ psutil_sysctl(int *mib, u_int miblen, void *buf, size_t buflen) { return -1; } - if (len != buflen) { + if (len > buflen) { psutil_runtime_error("sysctl() size mismatch"); return -1; } @@ -139,7 +139,7 @@ psutil_sysctlbyname(const char *name, void *buf, size_t buflen) { return -1; } - if (len != buflen) { + if (len > buflen) { str_format( errbuf, sizeof(errbuf), From 736a051023a4dc1400fd9c53010cf2cdb1b8ee31 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 31 Mar 2026 15:50:25 +0200 Subject: [PATCH 1663/1714] Update README --- README.rst | 287 +++++++++------------ docs/_static/css/custom.css | 1 - docs/_static/images/icon-cpu.svg | 16 ++ docs/_static/images/icon-disks.svg | 10 + docs/_static/images/icon-memory.svg | 19 ++ docs/_static/images/icon-network.svg | 6 + docs/_static/images/icon-processes.svg | 9 + docs/_static/images/logo-psutil-readme.svg | 20 ++ docs/index.rst | 66 +---- 9 files changed, 206 insertions(+), 228 deletions(-) create mode 100644 docs/_static/images/icon-cpu.svg create mode 100644 docs/_static/images/icon-disks.svg create mode 100644 docs/_static/images/icon-memory.svg create mode 100644 docs/_static/images/icon-network.svg create mode 100644 docs/_static/images/icon-processes.svg create mode 100644 docs/_static/images/logo-psutil-readme.svg diff --git a/README.rst b/README.rst index def596f97c..9dec00b74a 100644 --- a/README.rst +++ b/README.rst @@ -1,78 +1,52 @@ -| |downloads| |contributors| |packages| |version| |license| -| |github-actions-wheels| |github-actions-bsd| - -.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg - :target: https://clickpy.clickhouse.com/dashboard/psutil - :alt: Downloads - -.. .. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg -.. :target: https://github.com/giampaolo/psutil/stargazers -.. :alt: Github stars - -.. .. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg -.. :target: https://github.com/giampaolo/psutil/network/members -.. :alt: Github forks - -.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg - :target: https://github.com/giampaolo/psutil/graphs/contributors - :alt: Contributors +.. -.. |github-actions-wheels| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml.svg?label=Linux%2C%20macOS%2C%20Windows - :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild - :alt: Linux, macOS, Windows +.. raw:: html -.. |github-actions-bsd| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/bsd.yml.svg?label=BSD - :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests - :alt: FreeBSD, NetBSD, OpenBSD +
    + psutil +

    Process and System Utilities for Python

    + Documentation    + Blog    + Who uses psutil    +
    -.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi&color=yellowgreen - :target: https://pypi.org/project/psutil - :alt: Latest version +
    -.. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg - :target: https://repology.org/metapackage/python:psutil/versions - :alt: Binary packages +
    + + Downloads + -.. |license| image:: https://img.shields.io/pypi/l/psutil.svg - :target: https://github.com/giampaolo/psutil/blob/master/LICENSE - :alt: License + + Binary packages + -.. |twitter| image:: https://img.shields.io/twitter/follow/grodola?style=flat - :target: https://twitter.com/grodola - :alt: Twitter Follow + + Latest version + -.. .. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat -.. :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme -.. :alt: Tidelift + + Linux, macOS, Windows + ------ + + FreeBSD, NetBSD, OpenBSD + +
    -.. raw:: html +..
    -
    - -
    -
    - Home    - Documentation    - Who uses psutil    - Download    - Blog    - Funding    -
    +About +===== -Summary -======= +psutil is a cross-platform library for retrieving information about running +**processes** and **system utilization** (CPU, memory, disks, network, sensors) +in Python. It is useful mainly for **system monitoring**, **profiling**, +**limiting process resources**, and **managing running processes**. -psutil (process and system utilities) is a cross-platform library for -retrieving information about **running processes** and **system utilization** -(CPU, memory, disks, network, sensors) in Python. It is useful mainly for -**system monitoring**, **profiling**, **limiting process resources**, and -**managing running processes**. It implements many functionalities offered by -classic UNIX command line tools such as -*ps, top, free, iotop, netstat, ifconfig, lsof* -and others (see `shell equivalents`_). -psutil currently supports the following platforms: +It implements many functionalities offered by UNIX command line tool such as +*ps, top, free, iotop, netstat, ifconfig, lsof* and others (see +`shell equivalents`_). psutil supports the following platforms: - **Linux** - **Windows** @@ -84,13 +58,19 @@ psutil currently supports the following platforms: Install ======= -.. code-block:: none +.. code-block:: pip install psutil For platform-specific details see `installation `_. + +Documentation +============= + +psutil documentation is available at https://psutil.readthedocs.io/. + .. Sponsors @@ -98,50 +78,35 @@ Sponsors .. raw:: html -
    +
    - - - - add your logo + .. -Funding -======= - -While psutil is free software and will always remain so, the project would benefit -immensely from some funding. psutil is among the `top 100`_ most-downloaded -Python packages, and keeping up with bug reports, user support, and ongoing -maintenance has become increasingly difficult to sustain as a one-person -effort. If you're a company that's making significant use of psutil you can -consider becoming a sponsor via -`GitHub `_, -`Open Collective `_ -or `PayPal `_. -Sponsors can have their logo displayed here and in the psutil -`documentation `_. - Projects using psutil ===================== -psutil is one of the `top 100`_ most-downloaded packages on PyPI, with 280+ -million downloads per month, `760,000+ GitHub repositories +psutil is one of the `top 100`_ most-downloaded packages on PyPI, with **300+ +million** downloads per month, `760,000+ GitHub repositories `_ using it, and 14,000+ packages depending on it. Some notable projects using psutil: @@ -157,72 +122,17 @@ million downloads per month, `760,000+ GitHub repositories `bpytop `_, `Ajenti `_, `GRR `_ -- `psleak`_ `Full list `_ -Ports -===== - -- Go: `gopsutil `_ -- C: `cpslib `_ -- Rust: `rust-psutil `_ -- Nim: `psutil-nim `_ - -.. - -Supporters -========== - -People who donated money over the years: - -.. raw:: html - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - add your avatar - -..
    - -------------------------------------------------------------------------------- - Example usages ============== -CPU ---- +For the full API with more examples, see the +`API overview `_ and +`API reference `_. + +**CPU** .. code-block:: python @@ -234,8 +144,7 @@ CPU >>> psutil.cpu_freq() scpufreq(current=931.42, min=800.0, max=3500.0) -Memory ------- +**Memory** .. code-block:: python @@ -244,8 +153,7 @@ Memory >>> psutil.swap_memory() sswap(total=2097147904, used=296128512, free=1801019392, percent=14.1, sin=304193536, sout=677842944) -Disks ------ +**Disks** .. code-block:: python @@ -255,8 +163,7 @@ Disks >>> psutil.disk_usage('/') sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) -Network -------- +**Network** .. code-block:: python @@ -267,8 +174,7 @@ Network [sconn(fd=115, family=2, type=1, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254), ...] -Sensors -------- +**Sensors** .. code-block:: python @@ -278,12 +184,10 @@ Sensors >>> psutil.sensors_battery() sbattery(percent=93, secsleft=16628, power_plugged=False) -Processes ---------- +**Processes** .. code-block:: python - >>> import psutil >>> p = psutil.Process(7055) >>> p.name() 'python3' @@ -297,9 +201,7 @@ Processes [pconn(fd=115, family=2, type=1, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED')] >>> p.open_files() [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768)] - -.. code-block:: python - + >>> >>> for p in psutil.process_iter(['pid', 'name']): ... print(p.pid, p.name()) ... @@ -308,10 +210,57 @@ Processes 3 ksoftirqd/0 ... -For the full API with more examples, see the -`API overview `_ and -`API reference `_. - -.. _`psleak`: https://github.com/giampaolo/psleak -.. _`shell equivalents`: https://psutil.readthedocs.io/stable/shell-equivalents.html +.. _`shell equivalents`: https://psutil.readthedocs.io/latest/shell-equivalents.html .. _`top 100`: https://clickpy.clickhouse.com/dashboard/psutil + +.. + +Supporters +========== + +People who donated money over the years: + +.. raw:: html + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + add your avatar + +..
    + +License +======= + +BSD-3 diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index aef053c1ea..2b45f8faff 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -234,7 +234,6 @@ .home-icon-svg { width: 2.2rem; height: 2.2rem; - stroke: var(--blue-400); display: block; margin: 0 auto 10px; } diff --git a/docs/_static/images/icon-cpu.svg b/docs/_static/images/icon-cpu.svg new file mode 100644 index 0000000000..59a2c5e107 --- /dev/null +++ b/docs/_static/images/icon-cpu.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/_static/images/icon-disks.svg b/docs/_static/images/icon-disks.svg new file mode 100644 index 0000000000..54dc55d08d --- /dev/null +++ b/docs/_static/images/icon-disks.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/_static/images/icon-memory.svg b/docs/_static/images/icon-memory.svg new file mode 100644 index 0000000000..9bfd6cefb6 --- /dev/null +++ b/docs/_static/images/icon-memory.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/images/icon-network.svg b/docs/_static/images/icon-network.svg new file mode 100644 index 0000000000..4abfa54c31 --- /dev/null +++ b/docs/_static/images/icon-network.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/_static/images/icon-processes.svg b/docs/_static/images/icon-processes.svg new file mode 100644 index 0000000000..90b94bf577 --- /dev/null +++ b/docs/_static/images/icon-processes.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/docs/_static/images/logo-psutil-readme.svg b/docs/_static/images/logo-psutil-readme.svg new file mode 100644 index 0000000000..45ac91842f --- /dev/null +++ b/docs/_static/images/logo-psutil-readme.svg @@ -0,0 +1,20 @@ + + + + + + + + psutil + diff --git a/docs/index.rst b/docs/index.rst index 29fdc8e680..b54739a6d0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,7 +12,7 @@
    Process and System Utilities for Python
    GitHub repo - Downloads + Downloads Latest version
    @@ -40,62 +40,19 @@ in Python. It is useful mainly for **system monitoring**, **profiling**, -It implements many functionalities offered by UNIX command line tool such as -*ps, top, free, iotop, netstat, ifconfig, lsof* and others +psutil implements many functionalities offered by UNIX command line tool such +as *ps, top, free, iotop, netstat, ifconfig, lsof* and others (see :doc:`shell equivalents `). It is used by :doc:`many notable projects ` including TensorFlow, PyTorch, Home Assistant, Ansible, and Celery. From f80c9a2c2f488b7bb80db1b933511648203f4bc1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 31 Mar 2026 21:20:19 +0200 Subject: [PATCH 1664/1714] Doc: add JS script to open external URLs in a new tab --- MANIFEST.in | 14 ++++++++++++++ docs/_static/js/external-urls.js | 11 +++++++++++ docs/conf.py | 3 ++- scripts/internal/generate_manifest.py | 2 +- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 docs/_static/js/external-urls.js diff --git a/MANIFEST.in b/MANIFEST.in index 896e7341b8..a401989f4c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,7 +20,21 @@ include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py include docs/_links.rst include docs/_static/css/custom.css +include docs/_static/favicon.svg +include docs/_static/images/favicon.svg +include docs/_static/images/icon-cpu.svg +include docs/_static/images/icon-disks.svg +include docs/_static/images/icon-memory.svg +include docs/_static/images/icon-network.svg +include docs/_static/images/icon-processes.svg +include docs/_static/images/logo-apivoid.svg +include docs/_static/images/logo-psutil-readme.svg +include docs/_static/images/logo-psutil.svg +include docs/_static/images/logo-sansec.svg +include docs/_static/images/logo-tidelift.svg +include docs/_static/js/external-urls.js include docs/_static/js/highlight-numbers.js +include docs/_static/logo.svg include docs/_templates/footer.html include docs/_templates/layout.html include docs/adoption.rst diff --git a/docs/_static/js/external-urls.js b/docs/_static/js/external-urls.js new file mode 100644 index 0000000000..29a78dab5c --- /dev/null +++ b/docs/_static/js/external-urls.js @@ -0,0 +1,11 @@ +// Open all external URLs in a new tab. + +document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll("a[href^='http']").forEach(a => { + if (a.hostname !== location.hostname) { + a.target = "_blank"; + a.rel = "noopener noreferrer"; + } + }); +}); +// diff --git a/docs/conf.py b/docs/conf.py index aec9811fc9..b15b40997d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,7 +62,8 @@ 'css/custom.css', ] html_js_files = [ - 'js/highlight-numbers.js', + "js/highlight-numbers.js", + "js/external-urls.js", ] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 090324a93e..63c0d38cf9 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -10,7 +10,7 @@ import shlex import subprocess -SKIP_EXTS = ('.png', '.jpg', '.jpeg', '.svg') +SKIP_EXTS = ('.png', '.jpg', '.jpeg') SKIP_FILES = () SKIP_PREFIXES = ('.ci/', '.github/') From 92f826326210aca70a2908cae79b36f85fc977eb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Apr 2026 10:33:01 +0200 Subject: [PATCH 1665/1714] Add doc notes about memory_info_ex() name reuse --- README.rst | 4 ++-- docs/alternatives.rst | 4 ++-- docs/changelog.rst | 6 ++++-- docs/migration.rst | 21 ++++++++++++++++++++- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 9dec00b74a..80084ab71f 100644 --- a/README.rst +++ b/README.rst @@ -105,8 +105,8 @@ Sponsors Projects using psutil ===================== -psutil is one of the `top 100`_ most-downloaded packages on PyPI, with **300+ -million** downloads per month, `760,000+ GitHub repositories +psutil is one of the `top 100`_ most-downloaded packages on PyPI, with 300+ +million downloads per month, `760,000+ GitHub repositories `_ using it, and 14,000+ packages depending on it. Some notable projects using psutil: diff --git a/docs/alternatives.rst b/docs/alternatives.rst index d1090fd9ac..0fd9f71497 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -74,8 +74,8 @@ dependencies, which is why some minimal containers or scripts do this. The downsides are that it is Linux-only, the format may vary across kernel versions, and you have to parse raw text yourself. psutil parses ``/proc`` internally, exposes the same information through a consistent -cross-platform API and handles edge cases (numeric overflow, compatibility -with old kernels, graceful fallbacks, etc.) transparently. +cross-platform API and handles edge cases (invalid format, compatibility with +old kernels, graceful fallbacks, etc.). Third-party libraries --------------------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 0e3d5df6cd..16360178c5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -84,8 +84,10 @@ Type hints / enums: - Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`, :gh:`2733`). - - Add new :meth:`Process.memory_info_ex` method, which extends - :meth:`Process.memory_info` with platform-specific metrics: + - Add new :meth:`Process.memory_info_ex` method (not to be confused + with the old method with the same name, deprecated in 4.0 and + removed in 7.0), which extends :meth:`Process.memory_info` with + platform-specific metrics: - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, *swap*, *hugetlb* diff --git a/docs/migration.rst b/docs/migration.rst index 48283f3c7d..2a0a1e26b5 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -28,6 +28,8 @@ Key breaking changes in 8.0: - Some return types are now enums instead of strings. - :meth:`Process.memory_full_info` deprecated: use :meth:`Process.memory_footprint`. +- New :meth:`Process.memory_info_ex` (unrelated to the old method deprecated in + 4.0 and removed in 7.0). - Python 3.6 dropped. .. important:: @@ -172,6 +174,16 @@ memory_full_info() is deprecated mem = p.memory_footprint() uss = mem.uss +New memory_info_ex() method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +8.0 introduces a new :meth:`Process.memory_info_ex` method that extends +:meth:`Process.memory_info` with platform-specific metrics (e.g. +``peak_rss``, ``swap``, ``rss_anon`` on Linux). This is **unrelated** to +the old :meth:`Process.memory_info_ex` that was deprecated in 4.0 and +removed in 7.0 (which corresponded to what later became +:meth:`Process.memory_full_info`). + Python 3.6 dropped ^^^^^^^^^^^^^^^^^^^^ @@ -197,7 +209,14 @@ Process.memory_info_ex() removed The long-deprecated :meth:`Process.memory_info_ex` was removed (it was deprecated since 4.0.0 in 2016). Use :meth:`Process.memory_full_info` -instead: +instead. + +.. note:: + + In 8.0, a new :meth:`Process.memory_info_ex` method was introduced + with different semantics: it extends :meth:`Process.memory_info` + with platform-specific metrics. It is unrelated to the old method + documented here. .. code-block:: python From 17c239c754e4c353e7a5b9628a35637f3834fe4f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Apr 2026 11:22:38 +0200 Subject: [PATCH 1666/1714] Doc: create sponsors.html page (#2797) --- MANIFEST.in | 4 ++ README.rst | 4 +- docs/_sponsors.html | 21 +++++++++++ docs/_static/css/custom.css | 24 ++++++++---- docs/_templates/footer.html | 13 +------ docs/_templates/icon-footer-github.svg | 3 ++ docs/_templates/icon-footer-pypi.svg | 3 ++ docs/changelog.rst | 3 +- docs/funding.rst | 52 ++++++++++++++++++++++++++ docs/index.rst | 35 +---------------- 10 files changed, 108 insertions(+), 54 deletions(-) create mode 100644 docs/_sponsors.html create mode 100644 docs/_templates/icon-footer-github.svg create mode 100644 docs/_templates/icon-footer-pypi.svg create mode 100644 docs/funding.rst diff --git a/MANIFEST.in b/MANIFEST.in index a401989f4c..f0f6dc4f82 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,6 +19,7 @@ include docs/_ext/availability.py include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py include docs/_links.rst +include docs/_sponsors.html include docs/_static/css/custom.css include docs/_static/favicon.svg include docs/_static/images/favicon.svg @@ -36,6 +37,8 @@ include docs/_static/js/external-urls.js include docs/_static/js/highlight-numbers.js include docs/_static/logo.svg include docs/_templates/footer.html +include docs/_templates/icon-footer-github.svg +include docs/_templates/icon-footer-pypi.svg include docs/_templates/layout.html include docs/adoption.rst include docs/alternatives.rst @@ -46,6 +49,7 @@ include docs/conf.py include docs/credits.rst include docs/devguide.rst include docs/faq.rst +include docs/funding.rst include docs/glossary.rst include docs/index.rst include docs/install.rst diff --git a/README.rst b/README.rst index 80084ab71f..4832cb6fa4 100644 --- a/README.rst +++ b/README.rst @@ -98,7 +98,7 @@ Sponsors - + .. @@ -123,7 +123,7 @@ million downloads per month, `760,000+ GitHub repositories `Ajenti `_, `GRR `_ -`Full list `_ +`Full list `_ Example usages ============== diff --git a/docs/_sponsors.html b/docs/_sponsors.html new file mode 100644 index 0000000000..48443b0014 --- /dev/null +++ b/docs/_sponsors.html @@ -0,0 +1,21 @@ + + + + + + + + +
    diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 2b45f8faff..2f5bce9388 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -360,22 +360,32 @@ footer div[role="navigation"][aria-label="Footer"] { /* ================================================================== */ .footer-row { - display: flex; - align-items: center; - justify-content: space-between; + display: table; + width: 100%; } -.footer-row p { +.footer-row p, +.footer-icons { + display: table-cell; + vertical-align: middle; margin: 0 !important; + padding: 0; } .footer-icons { - display: flex; - gap: 14px; - flex-shrink: 0; + text-align: right; + white-space: nowrap; + width: 1px; +} + +.footer-icon { + display: inline-flex; + align-items: center; + margin-left: 14px; } .footer-icon svg { + display: block; width: 20px; height: 20px; fill: var(--footer-icon); diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html index 714f961912..819099cb3f 100644 --- a/docs/_templates/footer.html +++ b/docs/_templates/footer.html @@ -21,19 +21,10 @@

    {%- endblock %} diff --git a/docs/_templates/icon-footer-github.svg b/docs/_templates/icon-footer-github.svg new file mode 100644 index 0000000000..c87cd5c4e0 --- /dev/null +++ b/docs/_templates/icon-footer-github.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/_templates/icon-footer-pypi.svg b/docs/_templates/icon-footer-pypi.svg new file mode 100644 index 0000000000..1fc5c177bb --- /dev/null +++ b/docs/_templates/icon-footer-pypi.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/changelog.rst b/docs/changelog.rst index 16360178c5..46a5eaf3c1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,7 +14,7 @@ Changelog Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, -:gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`, :gh:`2790`) +:gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`, :gh:`2790`, :gh:`2797`) - Split docs from a single HTML file into multiple new sections: @@ -23,6 +23,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - :doc:`/alternatives `: list of alternative Python libraries and tools that overlap with psutil. - :doc:`/credits `: list contributors and donors (was old ``CREDITS`` in root dir) - :doc:`/faq `: extended FAQ section + - :doc:`/funding `: list funding methods and current sponsors - :doc:`/glossary `: core concepts explained - :doc:`/install `: (was old ``INSTALL.rst`` in root dir) - :doc:`/migration `: explain how to migrate to newer psutil versions that break backward compatibility diff --git a/docs/funding.rst b/docs/funding.rst new file mode 100644 index 0000000000..44d352f832 --- /dev/null +++ b/docs/funding.rst @@ -0,0 +1,52 @@ +.. currentmodule:: psutil + +Funding +======= + +psutil is free and open source software, maintained by a single developer in +his spare time. It is among the +`top 100 most-downloaded Python packages `_, +used by millions of developers and hundreds of thousands of projects worldwide, +including TensorFlow, PyTorch, Home Assistant, Ansible, and Celery. + +Keeping up with bug reports, platform compatibility, user support, and ongoing +maintenance has become increasingly difficult to sustain as a one-person effort. +Financial support helps dedicate more time to the project and ensures its +long-term health. + +How to fund +----------- + +There are several ways to support psutil development financially: + +`GitHub Sponsors `_ + The preferred platform for recurring or one-time sponsorships. GitHub + matches contributions for eligible sponsors. + +`Open Collective `_ + Transparent, open funding for individuals and companies. Expenses and + income are publicly visible. + +`PayPal `_ + One-time donations via PayPal. + +For companies +------------- + +If your company relies on psutil in production, consider becoming a sponsor. +Benefits include: + +- Your logo displayed on the `psutil homepage `_ + and the `GitHub repository `_. +- Priority response to bug reports and feature requests. +- The assurance that the library you depend on is actively maintained. + +To discuss sponsorship options, contact the author at g.rodola@gmail.com. + +Current sponsors +---------------- + +.. raw:: html + :file: _sponsors.html + +Past donors are listed in the :doc:`credits` page. diff --git a/docs/index.rst b/docs/index.rst index b54739a6d0..2a2a64df99 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -78,39 +78,7 @@ Sponsors -------- .. raw:: html - - - - - - - - - -
    - -While psutil is free software and will always be, the project would benefit -immensely from some funding. -psutil is among the :doc:`top 100 ` -most-downloaded Python packages, and keeping up with bug reports, user support, -and ongoing maintenance has become increasingly difficult to sustain as a -one-person effort. -If you're a company that's making significant use of psutil you can consider -becoming a sponsor via `GitHub `_, -`Open Collective `_ or -`PayPal `_. + :file: _sponsors.html ------------------------------------------------------------------------------- @@ -147,6 +115,7 @@ becoming a sponsor via `GitHub `_, Who uses psutil Alternatives + Funding Credits Timeline From 861ef8b57bcf4d7f6711ca474bf94443bc2fa616 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Apr 2026 18:30:50 +0200 Subject: [PATCH 1667/1714] Expose `Process.attrs`, a frozenset of valid `as_dict()` names (#2800) ## Problem Currently `_as_dict_attrnames` is a private set used internally by `Process.as_dict()` and `psutil.process_iter()` to validate the `attrs=` parameter. Users have no official way to know which names are valid without reading the source. The suggested way to do that according to official doc is: ```python # get a list of valid attrs names list(psutil.Process().as_dict().keys()) ``` But that implies fetching process info, which is slow. ## Solution Expose a new `Process.attrs` class attribute returning a frozenset. These are all the valid attribute names accepted by `Process.as_dict()` and `psutil.process_iter()`, excluding deprecated names (e.g. `memory_full_info`). Usage examples: All attrs: ```python psutil.process_iter(attrs=psutil.Process.attrs) ``` All except connections: ```python psutil.process_iter(attrs=psutil.Process.attrs - {"connections"}) ``` ## The attrs=[] case of process_iter() Up till now `process_iter(attrs=[])` (empty list) means "give me all the attributes" (it's also documented). This is now deprecated (`DeprecationWarning`). --- docs/api.rst | 55 ++++++++++++++++------ docs/changelog.rst | 14 ++++-- docs/migration.rst | 43 +++++++++++++++++ docs/performance.rst | 12 ++++- psutil/__init__.py | 72 ++++++++++++++++++++--------- scripts/internal/print_api_speed.py | 3 +- tests/__init__.py | 1 + tests/test_process.py | 21 +++++++++ tests/test_system.py | 7 +-- 9 files changed, 183 insertions(+), 45 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 747e79192a..4f077441d6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1043,13 +1043,15 @@ Functions Cache can optionally be cleared via ``process_iter.cache_clear()``. *attrs* and *ad_value* have the same meaning as in :meth:`Process.as_dict`. + If *attrs* is specified, :meth:`Process.as_dict` is called internally and the results are cached so that subsequent method calls (e.g. :meth:`Process.name`, :meth:`Process.status`) return the cached values - instead of issuing new system calls. + instead of issuing new system calls. See :attr:`Process.attrs` for a + list of valid *attrs* names. + If a method raises :exc:`AccessDenied` during pre-fetch, it will return - *ad_value* (default ``None``) instead of raising. If *attrs* is an empty list - it will retrieve all process info (slow). + *ad_value* (default ``None``) instead of raising. Processes are returned sorted by PID. @@ -1064,6 +1066,13 @@ Functions 3 ksoftirqd/0 root ... + All process *attrs* except slow ones: + + .. code-block:: pycon + + >>> for p in psutil.process_iter(psutil.Process.attrs - {'memory_footprint', 'memory_maps'}): + ... print(p) + Clear internal cache: .. code-block:: pycon @@ -1094,6 +1103,10 @@ Functions cached values instead of making new system calls. The :attr:`Process.info` dict is deprecated in favor of this new approach. + .. versionchanged:: 8.0.0 + passing an empty list (``attrs=[]``) to mean "all attributes" is + deprecated; use :attr:`Process.attrs` instead. + .. function:: pid_exists(pid) Check whether the given PID exists in the current process list. This is @@ -1310,6 +1323,25 @@ Process class The process PID. This is the only (read-only) attribute of the class. + .. attribute:: attrs + + A ``frozenset`` of strings representing the valid attribute names accepted + by :meth:`as_dict` and :func:`process_iter`. It defaults to all read-only + :class:`Process` method names, minus the utility methods such as + :meth:`as_dict`, :meth:`children`, etc. + + .. code-block:: pycon + + >>> import psutil + >>> psutil.Process.attrs + frozenset({'cmdline', 'cpu_num', 'cpu_percent', ...}) + >>> # all attrs + >>> psutil.process_iter(attrs=psutil.Process.attrs) + >>> # all attrs except 'net_connections' + >>> psutil.process_iter(attrs=psutil.Process.attrs - {"net_connections"}) + + .. versionadded:: 8.0.0 + .. attribute:: info A dict containing pre-fetched process info, set by @@ -1407,14 +1439,11 @@ Process class .. method:: as_dict(attrs=None, ad_value=None) - Utility method retrieving multiple process information as a dictionary. - If *attrs* is specified it must be a list of strings reflecting available - :class:`Process` class's attribute names. Here's a list of possible string - values: - ``'cmdline'``, ``'net_connections'``, ``'cpu_affinity'``, ``'cpu_num'``, ``'cpu_percent'``, ``'cpu_times'``, ``'create_time'``, ``'cwd'``, ``'environ'``, ``'exe'``, ``'gids'``, ``'io_counters'``, ``'ionice'``, ``'memory_footprint'``, ``'memory_full_info'``, ``'memory_info'``, ``'memory_info_ex'``, ``'memory_maps'``, ``'memory_percent'``, ``'name'``, ``'nice'``, ``'num_ctx_switches'``, ``'num_fds'``, ``'num_handles'``, ``'num_threads'``, ``'open_files'``, ``'pid'``, ``'ppid'``, ``'status'``, ``'terminal'``, ``'threads'``, ``'uids'``, ``'username'``. + Utility method returning multiple process information as a dictionary. - If *attrs* argument is not passed all public read only attributes are - assumed. + If *attrs* is specified, it must be a collection of strings reflecting + available :class:`Process` class's attribute names. If not passed all + :attr:`Process.attrs` names are assumed. *ad_value* is the value which gets assigned to a dict key in case :exc:`AccessDenied` or :exc:`ZombieProcess` exception is raised when @@ -1431,10 +1460,10 @@ Process class >>> p = psutil.Process() >>> p.as_dict(attrs=['pid', 'name', 'username']) {'username': 'giampaolo', 'pid': 12366, 'name': 'python'} + >>> # all attrs except slow ones + >>> p.as_dict(attrs=p.attrs - {'memory_footprint', 'memory_maps'}) + {'username': 'giampaolo', 'pid': 12366, 'name': 'python', ...} >>> - >>> # get a list of valid attrs names - >>> list(psutil.Process().as_dict().keys()) - ['cmdline', 'connections', 'cpu_affinity', 'cpu_num', 'cpu_percent', 'cpu_times', 'create_time', 'cwd', 'environ', 'exe', 'gids', 'io_counters', 'ionice', 'memory_footprint', 'memory_full_info', 'memory_info', 'memory_info_ex', 'memory_maps', 'memory_percent', 'name', 'net_connections', 'nice', 'num_ctx_switches', 'num_fds', 'num_threads', 'open_files', 'pid', 'ppid', 'status', 'terminal', 'threads', 'uids', 'username'] .. versionchanged:: 3.0.0 *ad_value* is used also when incurring into :exc:`ZombieProcess` diff --git a/docs/changelog.rst b/docs/changelog.rst index 46a5eaf3c1..6fe229b134 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,8 +19,8 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Split docs from a single HTML file into multiple new sections: - :doc:`/adoption `: notable software using psutil - - :doc:`/api-overview `: show entire API via REPL usage examples - :doc:`/alternatives `: list of alternative Python libraries and tools that overlap with psutil. + - :doc:`/api-overview `: show entire API via REPL usage examples - :doc:`/credits `: list contributors and donors (was old ``CREDITS`` in root dir) - :doc:`/faq `: extended FAQ section - :doc:`/funding `: list funding methods and current sponsors @@ -78,8 +78,16 @@ Type hints / enums: ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases for the corresponding enum members. -- New APIs: +New APIs: +- :gh:`2798`: new :attr:`Process.attrs` class attribute, a ``frozenset`` of + valid attribute names accepted by :meth:`Process.as_dict` and + :func:`process_iter`. Replaces the old pattern of calling + ``list(psutil.Process().as_dict().keys())``. + Passing an empty list to :func:`process_iter` (``attrs=[]``) to mean + "retrieve all attributes" is **deprecated**. Use + ``attrs=Process.attrs`` instead. + See :ref:`migration guide `. - :gh:`1541`: New :meth:`Process.page_faults` method, returning a ``(minor, major)`` named tuple. - Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`, @@ -122,7 +130,7 @@ Type hints / enums: :meth:`Process.memory_footprint` instead. See :ref:`migration guide `. -Others +Others: - :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times` has been normalized on all platforms, and the first 3 fields are now always diff --git a/docs/migration.rst b/docs/migration.rst index 2a0a1e26b5..f05f363130 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -30,6 +30,8 @@ Key breaking changes in 8.0: :meth:`Process.memory_footprint`. - New :meth:`Process.memory_info_ex` (unrelated to the old method deprecated in 4.0 and removed in 7.0). +- New :attr:`Process.attrs`: frozenset of valid attribute names. +- ``process_iter(attrs=[])`` is deprecated. - Python 3.6 dropped. .. important:: @@ -184,6 +186,47 @@ the old :meth:`Process.memory_info_ex` that was deprecated in 4.0 and removed in 7.0 (which corresponded to what later became :meth:`Process.memory_full_info`). +New Process.attrs class attribute +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:attr:`Process.attrs` is a new ``frozenset`` exposing the valid attribute +names accepted by :meth:`Process.as_dict` and :func:`process_iter`. It +replaces the previous pattern of creating a throwaway process just to +discover available names: + +.. code-block:: python + + # before + attrs = list(psutil.Process().as_dict().keys()) + + # after + attrs = psutil.Process.attrs + +It also makes it easy to pass all or a subset of attributes: + +.. code-block:: python + + # all attrs + psutil.process_iter(attrs=psutil.Process.attrs) + + # all except connections + psutil.process_iter(attrs=psutil.Process.attrs - {"net_connections"}) + +process_iter(attrs=[]) is deprecated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Passing an empty list to :func:`process_iter` to mean "retrieve all +attributes" is deprecated and raises :exc:`DeprecationWarning`. Use +:attr:`Process.attrs` instead: + +.. code-block:: python + + # before + psutil.process_iter(attrs=[]) + + # after + psutil.process_iter(attrs=psutil.Process.attrs) + Python 3.6 dropped ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/performance.rst b/docs/performance.rst index 31396de382..2f67ab6b5b 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -50,7 +50,7 @@ from the same process, use :meth:`Process.oneshot`. Use process_iter() with an attrs list -------------------------------------- -If you iterate over multiple PIDs, use :func:`process_iter`. +If you iterate over multiple PIDs, always use :func:`process_iter`. It accepts an ``attrs`` argument that pre-fetches only the requested attributes in a single pass, minimizing system calls by fetching multiple attributes at once. This is faster than calling individual methods in a loop. @@ -84,6 +84,16 @@ if a process disappears while iterating), since attributes are retrieved in a single pass and exceptions like :exc:`NoSuchProcess` and :exc:`AccessDenied` are handled internally. +A typical use case may be to fetch all process attrs except the slow ones (see +:ref:`perf-api-speed` table below): + +.. code-block:: python + + import psutil + + for p in psutil.process_iter(psutil.Process.attrs - {"memory_footprint", "memory_maps"}): + ... + .. _perf-pids: Avoid pids() + loop diff --git a/psutil/__init__.py b/psutil/__init__.py index afd8435358..38e9c090ae 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -66,6 +66,7 @@ from ._enums import ProcessStatus if _TYPE_CHECKING: + from collections.abc import Collection from typing import Any from typing import Callable from typing import Generator @@ -338,6 +339,8 @@ class Process: is_running() before querying the process. """ + attrs: frozenset[str] = frozenset() # dynamically set later + def __init__(self, pid: int | None = None) -> None: self._init(pid) @@ -586,25 +589,31 @@ def oneshot(self) -> Generator[None, None, None]: self._proc.oneshot_exit() def as_dict( - self, attrs: list[str] | None = None, ad_value: Any = None + self, attrs: Collection[str] | None = None, ad_value: Any = None ) -> dict[str, Any]: """Utility method returning process information as a hashable dictionary. - If *attrs* is specified it must be a list of strings - reflecting available Process class' attribute names - (e.g. ['cpu_times', 'name']) else all public (read - only) attributes are assumed. + + If *attrs* is specified it must be a collection of strings + reflecting available Process class' attribute names (e.g. + ['cpu_times', 'name']) else all public (read-only) attributes + are assumed. See `Process.attrs` for a full list. + *ad_value* is the value which gets assigned in case AccessDenied or ZombieProcess exception is raised when retrieving that particular process information. """ - valid_names = _as_dict_attrnames + valid_names = self.attrs + # Deprecated attrs: not returned by default but still accepted if + # explicitly requested. + deprecated_names = {"memory_full_info"} + if attrs is not None: if not isinstance(attrs, (list, tuple, set, frozenset)): msg = f"invalid attrs type {type(attrs)}" raise TypeError(msg) attrs = set(attrs) - invalid_names = attrs - valid_names - _as_dict_attrnames_deprecated + invalid_names = attrs - valid_names - deprecated_names if invalid_names: msg = "invalid attr name{} {}".format( "s" if len(invalid_names) > 1 else "", @@ -1527,20 +1536,17 @@ def wait(self, timeout: float | None = None) -> int | None: return self._exitcode -# The valid attr names which can be processed by Process.as_dict(). +# The valid attr names which can be processed by Process.as_dict(attrs=...) +# and process_iter(attrs=...). # fmt: off -_as_dict_attrnames = { +Process.attrs = frozenset( 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', - 'connections', 'memory_full_info', 'oneshot', 'info'} -} + 'connections', 'memory_full_info', 'oneshot', 'info', 'attrs'} +) # fmt: on -# Deprecated attrs: not returned by default but still accepted if -# explicitly requested via as_dict(attrs=[...]). -_as_dict_attrnames_deprecated = {'memory_full_info'} - # ===================================================================== # --- Popen class @@ -1662,12 +1668,12 @@ def pid_exists(pid: int) -> bool: def process_iter( - attrs: list[str] | None = None, ad_value: Any = None + attrs: Collection[str] | None = None, ad_value: Any = None ) -> Iterator[Process]: - """Return a generator yielding a Process instance for all + """Return a generator yielding a `Process` instance for all running processes. - Every new Process instance is only created once and then cached + Every new `Process` instance is only created once and then cached into an internal table which is updated every time this is used. Cache can optionally be cleared via `process_iter.cache_clear()`. @@ -1675,12 +1681,15 @@ def process_iter( their PIDs. *attrs* and *ad_value* have the same meaning as in - Process.as_dict(). If *attrs* is specified, as_dict() is called and - the results are cached so that subsequent method calls (e.g. - p.name()) return cached values. + Process.as_dict(). - If *attrs* is an empty list it will retrieve all process info - (slow). + If *attrs* is specified, `Process.as_dict()` is called and the + results are cached, so that subsequent method calls (e.g. + `p.name()`) return cached values. Use `attrs=Process.attrs` to + retrieve all process info (slow). + + If a method raises `AccessDenied` during pre-fetch, it will return + *ad_value* (default None) instead of raising. """ global _pmap @@ -1692,6 +1701,23 @@ def add(pid): def remove(pid): pmap.pop(pid, None) + if attrs is not None: + if attrs == []: # deprecated in 8.0.0 + msg = ( + "process_iter(attrs=[]) is deprecated; use " + "process_iter(attrs=Process.attrs) to retrieve all attributes" + ) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + elif not attrs: + # as_dict() will resolve an empty list|tuple|set to "all + # attribute names", but it's ambiguous and should be + # signaled. + msg = ( + f"process_iter(attrs={attrs}) is ambiguous; use " + "process_iter(attrs=Process.attrs) to retrieve all attributes" + ) + warnings.warn(msg, UserWarning, stacklevel=2) + pmap = _pmap.copy() a = set(pids()) b = set(pmap) diff --git a/scripts/internal/print_api_speed.py b/scripts/internal/print_api_speed.py index beaa61e102..4e5ec8a6ab 100755 --- a/scripts/internal/print_api_speed.py +++ b/scripts/internal/print_api_speed.py @@ -191,8 +191,7 @@ def main(): print() print_header("PROCESS APIS") p = psutil.Process(PID) - names = p.as_dict().keys() - for name in sorted(names): + for name in sorted(p.attrs): fun = getattr(p, name) if callable(fun): timecall(name, fun) diff --git a/tests/__init__.py b/tests/__init__.py index af3b4947fd..1768fe1b71 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1119,6 +1119,7 @@ class process_namespace: ignored = [ ('as_dict', (), {}), + ('attrs', (), {}), ('children', (), {'recursive': True}), ('connections', (), {}), # deprecated ('info', (), {}), diff --git a/tests/test_process.py b/tests/test_process.py index aa457017c6..1120c583dc 100755 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1232,6 +1232,27 @@ def test_as_dict(self): with pytest.raises(ValueError): p.as_dict(['foo', 'bar']) + def test_attrs(self): + # The `Process.attrs` attribute to use with `as_dict()`. + p = psutil.Process() + assert p.as_dict().keys() == p.attrs + + for g in process_namespace.getters: + name = g[0] + if name != 'rlimit': + assert name in p.attrs + + for g in process_namespace.ignored + process_namespace.killers: + name = g[0] + if name != 'pid': + assert name not in p.attrs + + # test excluded attrs + assert "net_connections" in list(p.as_dict(p.attrs)) + assert "net_connections" not in list( + p.as_dict(p.attrs - {"net_connections"}) + ) + def test_oneshot(self): p = psutil.Process() with mock.patch("psutil._psplatform.Process.cpu_times") as m: diff --git a/tests/test_system.py b/tests/test_system.py index a8c678ca79..85325ccc43 100755 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -173,10 +173,11 @@ def test_prefetch_double_call(self): assert name1 == p._prefetch["name"] break - def test_prefetch_empty_attrs(self): + def test_deprecated_prefetch_empty_attrs(self): # attrs=[] should prefetch all methods. - p = next(psutil.process_iter(attrs=[])) - assert p._prefetch.keys() == psutil._as_dict_attrnames + with pytest.warns(DeprecationWarning): + p = next(psutil.process_iter(attrs=[])) + assert p._prefetch.keys() == psutil.Process.attrs def test_prefetch_with_non_prefetched(self): # A method not in attrs should still do a live syscall. From c1e7c41adff3758a64ba43aac995fcaf7a77569d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Apr 2026 18:51:16 +0200 Subject: [PATCH 1668/1714] Fix #2799: fix Process.as_dict() names alphabetically --- docs/changelog.rst | 2 ++ psutil/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6fe229b134..496ac3a699 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -157,6 +157,8 @@ Others: - :gh:`2788`: git tags renamed from ``release-X.Y.Z`` to ``vX.Y.Z`` (e.g. ``release-7.2.2`` → ``v7.2.2``). Old tags are kept for backward compatibility. +- :gh:`2799`: :meth:`Process.as_dict` now returns a dict with keys sorted + alphabetically when *attrs* is not specified. **Bug fixes** diff --git a/psutil/__init__.py b/psutil/__init__.py index 38e9c090ae..1649a70728 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -622,9 +622,9 @@ def as_dict( raise ValueError(msg) retdict = {} - ls = attrs or valid_names + names = attrs or sorted(valid_names) with self.oneshot(): - for name in ls: + for name in names: try: if name == 'pid': ret = self.pid From 7108bfd858cc7fa4b291455bdd41a7db2d4b4b39 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Apr 2026 23:12:01 +0200 Subject: [PATCH 1669/1714] Doc: shorten and improve clarity of docstrings and doc (#2801) --- CONTRIBUTING.md | 4 +- docs/api.rst | 380 ++++++++++++----------------------------- docs/changelog.rst | 4 +- docs/devguide.rst | 157 ++++++++--------- docs/faq.rst | 17 ++ docs/migration.rst | 49 +----- docs/performance.rst | 39 +---- docs/recipes.rst | 152 ----------------- psutil/__init__.py | 392 ++++++++++++++++++++++--------------------- 9 files changed, 416 insertions(+), 778 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e13b4d5eed..318a53f311 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,13 +22,13 @@ ## Pull Requests +- In order to get acquainted with the code base and tooling, take a look at the + **[Development Guide](https://psutil.readthedocs.io/latest/devguide.html)**. - The PR system is for fixing bugs or make enhancements related to the **program code**. - If you wish to implement a new feature or add support for a new platform it's better to **discuss it first**, either on the issue tracker, the forum or via private email. -- In order to get acquainted with the code base and tooling, take a look at the - **[Development Guide](https://psutil.readthedocs.io/devguide)**. - If you can, remember to update [changelog.rst](https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst) and [CREDITS](https://github.com/giampaolo/psutil/blob/master/CREDITS) file. diff --git a/docs/api.rst b/docs/api.rst index 4f077441d6..2408df75a9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -131,11 +131,6 @@ CPU [2.0, 1.0] >>> - .. note:: - the first time this function is called with *interval* = ``0.0`` or ``None`` - it will return a meaningless ``0.0`` value which you are supposed to - ignore. - .. seealso:: :ref:`faq_cpu_percent` .. versionchanged:: 5.9.6 @@ -151,11 +146,6 @@ CPU On Linux "guest" and "guest_nice" percentages are not accounted in "user" and "user_nice" percentages. - .. note:: - the first time this function is called with *interval* = ``0.0`` or - ``None`` it will return a meaningless ``0.0`` value which you are supposed - to ignore. - .. seealso:: :ref:`faq_cpu_percent` .. versionchanged:: 4.1.0 @@ -192,9 +182,9 @@ CPU Note that ``psutil.cpu_count()`` may not necessarily be equivalent to the actual number of CPUs the current process can use. - That can vary in case process CPU affinity has been changed, Linux cgroups - are being used or (in case of Windows) on systems using processor groups or - having more than 64 CPUs. + That can vary if process CPU affinity has been changed, Linux cgroups are + being used or (on Windows) on systems using processor groups or having more + than 64 CPUs. The number of usable CPUs can be obtained with: .. code-block:: pycon @@ -480,9 +470,8 @@ Memory .. versionchanged:: 5.2.3 on Linux this function relies on /proc fs instead of sysinfo() syscall so - that it can be used in conjunction with - :const:`psutil.PROCFS_PATH` in order to retrieve memory info about - Linux containers such as Docker and Heroku. + that it can be used in conjunction with :const:`psutil.PROCFS_PATH` to + retrieve memory info about Linux containers such as Docker and Heroku. Disks ^^^^^ @@ -577,18 +566,13 @@ Disks - **write_merged_count** (*Linux*): number of merged writes (see `iostats doc`_) If *perdisk* is ``True`` return the same information for every physical disk - installed on the system as a dictionary with partition names as the keys and - the named tuple described above as the values. - - On some systems such as Linux, on a very busy or long-lived system, the - numbers returned by the kernel may overflow and wrap (restart from zero). - If *nowrap* is ``True`` psutil will detect and adjust those numbers across - function calls 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 *nowrap* - cache. - On Windows it may be necessary to issue ``diskperf -y`` command from cmd.exe - first in order to enable IO counters. + as a dictionary with partition names as the keys. + + If *nowrap* is ``True`` (default), counters that overflow and wrap to zero + are automatically adjusted so they never decrease (this can happen on very + busy or long-lived systems). ``disk_io_counters.cache_clear()`` can be used + to invalidate the *nowrap* cache. + On diskless machines this function will return ``None`` or ``{}`` if *perdisk* is ``True``. @@ -639,15 +623,13 @@ Network on macOS and BSD). See :term:`dropin / dropout`. If *pernic* is ``True`` return the same information for every network - interface installed on the system as a dictionary with network interface - names as the keys and the named tuple described above as the values. - On some systems such as Linux, on a very busy or long-lived system, the - numbers returned by the kernel may overflow and wrap (restart from zero). - If *nowrap* is ``True`` psutil will detect and adjust those numbers across - function calls and add "old value" to "new value" so that the returned - numbers will always be increasing or remain the same, but never decrease. - ``net_io_counters.cache_clear()`` can be used to invalidate the *nowrap* - cache. + interface as a dictionary with interface names as the keys. + + If *nowrap* is ``True`` (default), counters that overflow and wrap to zero + are automatically adjusted so they never decrease (this can happen on very + busy or long-lived systems). ``net_io_counters.cache_clear()`` can be + used to invalidate the *nowrap* cache. + On machines with no network interfaces this function will return ``None`` or ``{}`` if *pernic* is ``True``. @@ -672,27 +654,19 @@ Network Return system-wide socket connections as a list of named tuples. Every named tuple provides 7 attributes: - - **fd**: the socket file descriptor. If the connection refers to the current - process this may be passed to :func:`socket.fromfd` - to obtain a usable socket object. - On Windows and SunOS this is always set to ``-1``. - - **family**: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6` or :data:`socket.AF_UNIX`. - - **type**: the address type, either :data:`socket.SOCK_STREAM`, :data:`socket.SOCK_DGRAM` or - :data:`socket.SOCK_SEQPACKET`. + - **fd**: the socket file descriptor; ``-1`` on Windows and SunOS. + - **family**: the address family, either :data:`socket.AF_INET`, + :data:`socket.AF_INET6` or :data:`socket.AF_UNIX`. + - **type**: the address type, either :data:`socket.SOCK_STREAM`, + :data:`socket.SOCK_DGRAM` or :data:`socket.SOCK_SEQPACKET`. - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` - in case of :data:`socket.AF_UNIX` sockets. For UNIX sockets see notes below. - - **raddr**: the remote address as a ``(ip, port)`` named tuple or an - absolute ``path`` in case of UNIX sockets. - When the remote endpoint is not connected you'll get an empty tuple - (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. - - **status**: represents the status of a TCP connection. The return value - is one of the :data:`psutil.CONN_* ` constants - (a string). - For UDP and UNIX sockets this is always going to be - :const:`psutil.CONN_NONE`. - - **pid**: the PID of the process which opened the socket, if retrievable, - else ``None``. On some platforms (e.g. Linux) the availability of this - field changes depending on process privileges (root is needed). + for :data:`socket.AF_UNIX` sockets (see notes below). + - **raddr**: the remote address, either an empty tuple (``AF_INET*``) or + ``""`` (``AF_UNIX``) when not connected. For UNIX sockets see notes below. + - **status**: a :data:`psutil.CONN_* ` constant; + always :const:`psutil.CONN_NONE` for UDP and UNIX sockets. + - **pid**: PID of the process which opened the socket. Set to ``None`` if it + can't be retrieved due to insufficient permissions (e.g. Linux). The *kind* parameter is a string which filters for connections matching the following criteria: @@ -725,9 +699,6 @@ Network | ``'all'`` | the sum of all the possible families and protocols | +----------------+-----------------------------------------------------+ - On macOS and AIX this function requires root privileges. - To get per-process connections use :meth:`Process.net_connections`. - .. code-block:: pycon >>> import psutil @@ -744,17 +715,16 @@ Network :exc:`PermissionError`. That means the returned list may be incomplete. .. note:: - (macOS and AIX) :exc:`psutil.AccessDenied` is always raised unless running - as root. This is a limitation of the OS and ``lsof`` does the same. + - Linux, FreeBSD, OpenBSD: *raddr* field for UNIX sockets is always set to + ``""`` (empty string); this is a limitation of the OS + - macOS and AIX: :exc:`psutil.AccessDenied` is always raised unless running + as root; this is a limitation of the OS + - Solaris: UNIX sockets are not supported - .. note:: - (Solaris) UNIX sockets are not supported. - - .. note:: - (Linux, FreeBSD, OpenBSD) *raddr* field for UNIX sockets is always set to - ``""`` (empty string). This is a limitation of the OS. + .. seealso:: - .. seealso:: `scripts/netstat.py`_. + - :meth:`Process.net_connections` to get per-process connections + - `scripts/netstat.py`_ .. versionadded:: 2.1.0 @@ -775,24 +745,19 @@ Network .. function:: net_if_addrs() - Return the addresses associated to each :term:`NIC` (network interface card) - installed on the system as a dictionary whose keys are the NIC names and - value is a list of named tuples for each address assigned to the NIC. - You can have more than one address of the same family associated with each - interface (that's why dict values are lists). - Each named tuple includes 5 fields: + Return a dictionary mapping each :term:`NIC` to a list of named tuples + representing its addresses. Multiple addresses of the same family can exist + per interface. Each named tuple includes 5 fields (addresses may be + ``None``): - **family**: the address family, either :data:`socket.AF_INET`, - :data:`socket.AF_INET6`, :const:`psutil.AF_LINK` in case of MAC address, - :data:`socket.AF_UNSPEC` in case of virtual or unconfigured interfaces. - - **address**: the primary NIC address (may be ``None`` in case of virtual - or unconfigured interfaces). - - **netmask**: the netmask address (may be ``None``). - - **broadcast**: the broadcast address. May be ``None``. Always ``None`` on - Windows. - - **ptp**: stands for "point to point"; it's the destination address on a - point to point interface (typically a VPN). *broadcast* and *ptp* are - mutually exclusive. May be ``None``. Always ``None`` on Windows. + :data:`socket.AF_INET6`, :const:`psutil.AF_LINK` (a MAC address) or + :data:`socket.AF_UNSPEC` (a virtual or unconfigured NIC). + - **address**: the primary NIC address + - **netmask**: the netmask address + - **broadcast**: the broadcast address; always ``None`` on Windows + - **ptp**: a "point to point" address (typically a VPN); always ``None`` on + Windows .. code-block:: pycon @@ -821,24 +786,16 @@ Network .. function:: net_if_stats() - Return information about each :term:`NIC` (network interface card) installed on the - system as a dictionary whose keys are the NIC names and value is a named tuple - with the following fields: + Return a dictionary mapping each :term:`NIC` to a named tuple with the + following fields: - - **isup**: a bool indicating whether the NIC is up and running (meaning - ethernet cable or Wi-Fi is connected). - - **duplex**: the duplex communication type; - it can be either :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or + - **isup**: whether the NIC is up and running (bool). + - **duplex**: :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or :const:`NIC_DUPLEX_UNKNOWN`. - - **speed**: the NIC speed expressed in megabits (Mbps), if it can't be - determined (e.g. 'localhost') it will be set to ``0``. - - **mtu**: NIC's maximum transmission unit expressed in bytes. - - **flags**: a string of comma-separated flags on the interface (may be an empty string). - Possible flags are: ``up``, ``broadcast``, ``debug``, ``loopback``, - ``pointopoint``, ``notrailers``, ``running``, ``noarp``, ``promisc``, - ``allmulti``, ``master``, ``slave``, ``multicast``, ``portsel``, - ``dynamic``, ``oactive``, ``simplex``, ``link0``, ``link1``, ``link2``, - and ``d2`` (some flags are only available on certain platforms). + - **speed**: NIC speed in megabits (Mbps); ``0`` if undetermined. + - **mtu**: maximum transmission unit in bytes. + - **flags**: a comma-separated string of interface flags (e.g. + ``"up,broadcast,running,multicast"``); may be an emty string. .. code-block:: pycon @@ -1019,7 +976,7 @@ Functions Return a sorted list of current running PIDs. To iterate over all processes and avoid race conditions :func:`process_iter` - should be preferred, see :ref:`performance section `. + should be preferred, see :ref:`perf-process-iter`. .. code-block:: pycon @@ -1208,15 +1165,15 @@ Process class :meth:`threads` method). When calling methods of this class, always be prepared to catch :exc:`NoSuchProcess` and :exc:`AccessDenied` exceptions. - :func:`hash` builtin can be used against instances of this class in order to - identify a process univocally over time (the hash is determined by mixing - process PID + creation time). As such it can also be used with :class:`set`. + :func:`hash` builtin can be used against instances of this class to identify + a process univocally over time (the hash is determined by mixing process PID + + creation time). As such it can also be used with :class:`set`. .. note:: - in order to efficiently fetch more than one information about the process - at the same time, make sure to use either :meth:`oneshot` context manager - or :meth:`as_dict` utility method. + to efficiently fetch multiple attributes about the process at the same + time, use either :meth:`oneshot` context manager or :meth:`as_dict` + utility method. .. note:: @@ -1231,19 +1188,13 @@ Process class .. method:: oneshot() - Utility context manager which considerably speeds up the retrieval of - multiple process attributes at the same time. - Internally different process info (e.g. :meth:`name`, :meth:`ppid`, - :meth:`uids`, :meth:`create_time`, ...) may be fetched by using the same - routine, but only one value is returned and the others are discarded. - When using this context manager the internal routine is executed once (in - the example below on :meth:`name`); the value of interest is returned and - the others are cached. - The subsequent calls sharing the same internal routine will return the - cached value. - The cache is cleared when exiting the context manager block. - The advice is to use this every time you retrieve more than one attribute - about the process. If you're lucky, you'll get a hell of a speedup. + Context manager which speeds up the retrieval of multiple process + attributes at the same time. Internally, many attributes (e.g. + :meth:`name`, :meth:`ppid`, :meth:`uids`, :meth:`create_time`, ...) + share the same system call. This context manager executes each system call + once and caches the results. Subsequent calls return cached values. + The cache is cleared when exiting the context manager block. Use this + every time you retrieve more than one attribute about the process. .. code-block:: pycon @@ -1576,7 +1527,7 @@ Process class If no argument is provided it acts as a get, returning a ``(ioclass, value)`` tuple on Linux and a *ioclass* integer on Windows. If *ioclass* is provided it acts as a set. In this case an additional - *value* can be specified on Linux only in order to increase or decrease the + *value* can be specified on Linux only to increase or decrease the I/O priority even further. Here's the possible platform-dependent *ioclass* values. @@ -1776,7 +1727,7 @@ Process class .. method:: cpu_percent(interval=None) Return a float representing the process CPU utilization as a percentage - which can also be ``> 100.0`` in case of a process running multiple threads + which can also be ``> 100.0`` if the process runs multiple threads on different CPUs. When *interval* is > ``0.0`` compares process times to system CPU times elapsed before and after the interval (blocking). When *interval* is ``0.0`` @@ -1798,26 +1749,9 @@ Process class 2.9 .. note:: - the first time this method is called with interval = ``0.0`` or - ``None`` it will return a meaningless ``0.0`` value which you are - supposed to ignore. - - .. note:: - the returned value can be > 100.0 in case of a process running multiple - threads on different CPU cores. - - .. note:: - the returned value is explicitly *not* split evenly between all available - CPUs (differently from :func:`psutil.cpu_percent`). - This means that a busy loop process running on a system with 2 logical - CPUs will be reported as having 100% CPU utilization instead of 50%. - This was done in order to be consistent with ``top`` UNIX utility, - and also to make it easier to identify processes hogging CPU resources - independently from the number of CPUs. - It must be noted that ``taskmgr.exe`` on Windows does not behave like - this (it would report 50% usage instead). - To emulate Windows ``taskmgr.exe`` behavior you can do: - ``p.cpu_percent() / psutil.cpu_count()``. + the returned value is *not* split evenly between all available CPUs + (differently from :func:`psutil.cpu_percent`). To emulate Windows + ``taskmgr.exe`` behavior: ``p.cpu_percent() / psutil.cpu_count()``. .. seealso:: - :ref:`faq_cpu_percent` @@ -1825,16 +1759,12 @@ Process class .. method:: cpu_affinity(cpus=None) - Get or set process current - `CPU affinity `_. - CPU affinity consists in telling the OS to run a process on a limited set - of CPUs only (on Linux cmdline, ``taskset`` command is typically used). - If no argument is passed it returns the current CPU affinity as a list - of integers. - If passed it must be a list of integers specifying the new CPUs affinity. - If an empty list is passed all eligible CPUs are assumed (and set). - On some systems such as Linux this may not necessarily mean all available - logical CPUs as in ``list(range(psutil.cpu_count()))``). + Get or set process + `CPU affinity `_ + (the set of CPUs the process is allowed to run on). + If no argument is passed, return the current affinity as a list of + integers. If passed, *cpus* must be a list of CPU integers. An empty + list sets affinity to all eligible CPUs. .. code-block:: pycon @@ -2052,25 +1982,21 @@ Process class Return a named tuple with USS, PSS and swap memory metrics. These give a more accurate picture of actual memory consumption than - :meth:`memory_info`, as explained in this - `blog post `_ - It works by walking the full process address space, so it is - considerably slower than :meth:`memory_info` and may require elevated - privileges. - - - **uss** *(Linux, macOS, Windows)*: aka :term:`USS`. This is the - memory which is unique to a process and which would be freed if the - process were terminated right now. The most representative metric for - actual memory usage. - - - **pss** *(Linux)*: aka :term:`PSS`, is the amount of memory - shared with other processes, accounted in a way that the amount is - divided evenly between the processes that share it. I.e. if a process has - 10 MBs all to itself, and 10 MBs shared with another process, its PSS - will be 15 MBs. - - - **swap** *(Linux)*: process memory currently in swap, counted per-mapping - (slower, but may be more accurate than ``memory_info_ex().swap``). + :meth:`memory_info` (see this + `blog post `_). + It walks the full process address space, so it is slower than + :meth:`memory_info` and may require elevated privileges. + + - **uss** *(Linux, macOS, Windows)*: aka :term:`USS`; memory which is + unique to the process, and which would be freed if the process were + terminated right now. + + - **pss** *(Linux)*: aka :term:`PSS`; shared memory divided evenly among + the processes sharing it. I.e. if a process has 10 MBs all to itself, and + 10 MBs shared with another process, its PSS will be 15 MBs. + + - **swap** *(Linux)*: process memory currently in swap, counted + per-mapping. Example on Linux: @@ -2269,20 +2195,10 @@ Process class [popenfile(path='/home/giampaolo/svn/psutil/file.ext', fd=3, position=0, mode='w', flags=32769)] .. warning:: - on Windows this method is not reliable due to some limitations of the - underlying Windows API which may hang when retrieving certain file - handles. - In order to work around that psutil spawns a thread to determine the file - handle name and kills it if it's not responding after 100ms. - That implies that this method on Windows is not guaranteed to enumerate - all regular file handles (see - `issue 597 `_). - Tools like ProcessHacker have the same limitation. - - .. warning:: - on BSD this method can return paths as an empty string due to a kernel - bug, hence it's not reliable - (see `issue 595 `_). + - Windows: this is not guaranteed to enumerate all file handles (see + :ref:`faq_open_files_windows`) + - BSD: can return empty-string paths due to a kernel bug (see + `issue 595 `_) .. versionchanged:: 3.1.0 no longer hangs on Windows. @@ -2292,57 +2208,9 @@ Process class .. method:: net_connections(kind="inet") - Return socket connections opened by process as a list of named tuples. - To get system-wide connections use :func:`psutil.net_connections`. - Every named tuple provides 6 attributes: - - - **fd**: the socket file descriptor. If the connection refers to the - current process this may be passed to :func:`socket.fromfd` to obtain a usable - socket object. - On Windows, FreeBSD and SunOS this is always set to ``-1``. - - **family**: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6` or - :data:`socket.AF_UNIX`. - - **type**: the address type, either :data:`socket.SOCK_STREAM`, :data:`socket.SOCK_DGRAM` or - :data:`socket.SOCK_SEQPACKET`. - - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` - in case of AF_UNIX sockets. For UNIX sockets see notes below. - - **raddr**: the remote address as a ``(ip, port)`` named tuple or an - absolute ``path`` in case of UNIX sockets. - When the remote endpoint is not connected you'll get an empty tuple - (AF_INET*) or ``""`` (AF_UNIX). For UNIX sockets see notes below. - - **status**: represents the status of a TCP connection. The return value - is one of the :data:`psutil.CONN_* ` constants. - For UDP and UNIX sockets this is always going to be - :const:`psutil.CONN_NONE`. - - The *kind* parameter is a string which filters for connections that fit the - following criteria: - - +----------------+-----------------------------------------------------+ - | Kind value | Connections using | - +================+=====================================================+ - | ``'inet'`` | IPv4 and IPv6 | - +----------------+-----------------------------------------------------+ - | ``'inet4'`` | IPv4 | - +----------------+-----------------------------------------------------+ - | ``'inet6'`` | IPv6 | - +----------------+-----------------------------------------------------+ - | ``'tcp'`` | TCP | - +----------------+-----------------------------------------------------+ - | ``'tcp4'`` | TCP over IPv4 | - +----------------+-----------------------------------------------------+ - | ``'tcp6'`` | TCP over IPv6 | - +----------------+-----------------------------------------------------+ - | ``'udp'`` | UDP | - +----------------+-----------------------------------------------------+ - | ``'udp4'`` | UDP over IPv4 | - +----------------+-----------------------------------------------------+ - | ``'udp6'`` | UDP over IPv6 | - +----------------+-----------------------------------------------------+ - | ``'unix'`` | UNIX socket (both UDP and TCP protocols) | - +----------------+-----------------------------------------------------+ - | ``'all'`` | the sum of all the possible families and protocols | - +----------------+-----------------------------------------------------+ + Same as :func:`psutil.net_connections` but for this process only (the + returned named tuples have no **pid** field). The *kind* parameter and + the same limitations apply (root may be needed on some platforms). .. code-block:: pycon @@ -2356,38 +2224,6 @@ Process class pconn(fd=119, family=, type=, laddr=addr(ip='10.0.0.1', port=60759), raddr=addr(ip='72.14.234.104', port=80), status=), pconn(fd=123, family=, type=, laddr=addr(ip='10.0.0.1', port=51314), raddr=addr(ip='72.14.234.83', port=443), status=)] - .. warning:: - on Linux, retrieving connections for certain processes requires root - privileges. If psutil is not run as root, those connections are silently - skipped instead of raising :exc:`psutil.AccessDenied`. That means - the returned list may be incomplete. - - .. note:: - (Linux, FreeBSD) **raddr** field for UNIX sockets is always set to an - empty string. This is a limitation of the OS. - - .. note:: - (Solaris) UNIX sockets are not supported. - - .. note:: - (OpenBSD) **laddr** and **raddr** fields for UNIX sockets are always set to - "". This is a limitation of the OS. - - .. note:: - (AIX) :exc:`psutil.AccessDenied` is always raised unless running - as root (lsof does the same). - - .. versionchanged:: 5.3.0 - **laddr** and **raddr** are named tuples. - - .. versionchanged:: 6.0.0 - method renamed from `connections` to `net_connections`. - - .. versionchanged:: 8.0.0 - **status** field is now a :class:`psutil.ConnectionStatus` enum member - instead of a plain ``str``. - See :ref:`migration guide `. - .. method:: connections() Same as :meth:`net_connections` (deprecated). @@ -2515,7 +2351,7 @@ Process class return value is cached (instead of returning ``None``). .. versionchanged:: 5.7.1 - on POSIX, in case of negative signal, return it as a human readable + on POSIX, if the signal is negative, return it as a human readable :mod:`enum`. .. versionchanged:: 7.2.2 @@ -2537,8 +2373,8 @@ Popen class :meth:`send_signal() `, :meth:`terminate() `, :meth:`kill() `. - This is done in order to avoid killing another process in case its PID has - been reused, fixing `BPO-6973`_. + This is done to avoid killing another process if its PID has been reused, + fixing `BPO-6973`_. .. code-block:: pycon diff --git a/docs/changelog.rst b/docs/changelog.rst index 496ac3a699..3aac4fffa2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,7 +14,8 @@ Changelog Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, :gh:`2764`, :gh:`2767`, :gh:`2768`, :gh:`2769`, :gh:`2771`, :gh:`2774`, -:gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`, :gh:`2790`, :gh:`2797`) +:gh:`2775`, :gh:`2781`, :gh:`2787`, :gh:`2739`, :gh:`2790`, :gh:`2797`, +:gh:`2801`) - Split docs from a single HTML file into multiple new sections: @@ -42,6 +43,7 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Usability: + - Improve overall doc clarity and shorten long sentences. - Show a clickable COPY button to copy code snippets. - Show ``psutil.`` prefix for all APIs. - Greatly improved :func:`virtual_memory` doc and many other APIs. diff --git a/docs/devguide.rst b/docs/devguide.rst index 0dbc014452..6f3de13ca9 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -1,69 +1,69 @@ Development guide ================= +.. seealso:: `Contributing to psutil project `_ + Build, setup and test --------------------- - psutil makes extensive use of C extension modules, meaning a C compiler is - required, see :doc:`install instructions `. Once you have a compiler - installed run: + required, see :doc:`install instructions `. Once installed run: .. code-block:: bash git clone git@github.com:giampaolo/psutil.git make install-sysdeps # install gcc and python headers - make install-pydeps-test # install python deps necessary to run unit tests + make install-pydeps-test # install test dependencies make build make install make test -- ``make`` (and the accompanying `Makefile`_) is the designated tool for - building, installing, running tests, and general development tasks, - including on Windows (see later). +- ``make`` (via the `Makefile`_) is used for building, testing and general + development tasks, including on Windows (see below): .. code-block:: bash - make clean # remove build files - make install-pydeps-dev # install all development deps (ruff, black, coverage, ...) - make test # run tests - make test-parallel # run tests in parallel (faster) - make test-memleaks # run memory leak tests - make test-coverage # run test coverage - make lint-all # run linters - make fix-all # fix linters errors + make clean + make install-pydeps-dev # install dev deps (ruff, black, coverage, ...) + make test + make test-parallel + make test-memleaks + make test-coverage + make lint-all + make fix-all make uninstall make help -- To run a specific unit test: +- To run a specific test: .. code-block:: none make test ARGS=tests/test_system.py -- Do not use ``sudo``. ``make install`` installs psutil as a limited user in - "edit" / development mode, meaning you can edit psutil code on the fly while - you develop. +- Do not use ``sudo``. ``make install`` installs psutil in editable mode, + so you can modify the code while developing. -- If you want to target a specific Python version: +- To target a specific Python version: .. code-block:: none make test PYTHON=python3.8 + Windows ------- -- The recommended way to develop on Windows is to use ``make``, just like - on UNIX systems. -- First, install `Git for Windows`_ and launch a **Git Bash shell**. This - provides a Unix-like environment where ``make`` works. -- Once inside Git Bash, you can run the usual ``make`` commands: +- The recommended way to develop on Windows is to use ``make``. +- Install `Git for Windows`_ and launch a **Git Bash shell**, which provides + a Unix-like environment where ``make`` works. +- Then run: .. code-block:: bash make build make test-parallel + .. _devguide_debug_mode: Debug mode @@ -85,23 +85,24 @@ On Windows: .. code-block:: none set PSUTIL_DEBUG=1 && python.exe script.py - psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress) -> 998 (Unknown error) (ignored) + psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(...) -> 998 (Unknown error) (ignored) Coding style ------------ -All style and formatting checks are automatically enforced both **locally on -each `git commit`** and **remotely via a GitHub Actions pipeline**. +All style and formatting checks are enforced locally on each +`git commit` and via a GitHub Actions pipeline. + +- Python: follows `PEP-8`_, formatted and linted with ``black`` and ``ruff``. +- C: generally follows `PEP-7`_, formatted with ``clang-format``. +- Other files (``.rst``, ``.toml``, ``.md``, ``.yml``): validated by linters. + +The pipeline re-runs all checks for consistency (``make lint-all``). + +Run ``make fix-all`` before committing; it usually fixes Python issues +(via ``black`` and ``ruff``) and C issues (via ``clang-format``). -- **Python** code follows the `PEP-8`_ style guide. We use `black` and `ruff` - for formatting and linting. -- **C** code generally follows the `PEP-7`_ style guide, with formatting - enforced by `clang-format`. -- **Other files** (`.rst`, `.toml`, `.md`, `.yml`) are also validated by - dedicated command-line linters. -- The **GitHub Actions pipeline** re-runs all these checks to ensure - consistency (via ``make lint-all``). Code organization ----------------- @@ -111,57 +112,49 @@ Code organization psutil/__init__.py # Main API namespace ("import psutil") psutil/_common.py # Generic utilities psutil/_ntuples.py # Named tuples returned by psutil APIs - psutil/_enums.py # Enum containers backing psutil constants - psutil/_ps{platform}.py # Platform-specific python wrappers - psutil/_psutil_{platform}.c # Platform-specific C extensions (entry point) - psutil/arch/all/*.c # C code common to all platforms - psutil/arch/{platform}/*.c # Platform-specific C extension + psutil/_enums.py # Enum containers + psutil/_ps{platform}.py # OS-specific python wrapper + psutil/_psutil_{platform}.c # OS-specific C extension (entry point) + psutil/arch/all/*.c # C code common to all OSes + psutil/arch/{platform}/*.c # OS-specific C extension tests/test_process|system.py # Main system/process API tests - tests/test_{platform}.py # Platform-specific tests + tests/test_{platform}.py # OS-specific tests Adding a new API ---------------- -Typically, this is what you do: - -- Define the new API in `psutil/__init__.py`_. -- Write the platform specific implementation in ``psutil/_ps{platform}.py`` - (e.g. `psutil/_pslinux.py`_). -- If the change requires C code, write the C implementation in - ``psutil/arch/{platform}/file.c`` (e.g. `psutil/arch/linux/disk.c`_). -- Write a generic test in `tests/test_system.py`_ or - `tests/test_process.py`_. -- If possible, write a platform-specific test in - ``tests/test_{platform}.py`` (e.g. `tests/test_linux.py`_). - This usually means testing the return value of the new API against - a system CLI tool. -- Update the doc in ``docs/api.rst``. -- Update `changelog.rst`_ and `credits.rst`_ files. -- Make a pull request. +- Define the API in `psutil/__init__.py`_. +- Implement it in ``psutil/_ps{platform}.py`` (e.g. `psutil/_pslinux.py`_). +- If needed, add C code in ``psutil/arch/{platform}/file.c``. +- Add a generic test in `tests/test_system.py`_ or `tests/test_process.py`_. +- Add a platform-specific test in ``tests/test_{platform}.py``. +- Update ``docs/api.rst``. +- Update `changelog.rst`_ and `credits.rst`_. +- Open a pull request. Make a pull request ------------------- -- Fork psutil (go to https://github.com/giampaolo/psutil and click on "fork") -- Git clone the fork locally: ``git clone git@github.com:YOUR-USERNAME/psutil.git`` +- Fork psutil on GitHub. +- Clone your fork: ``git clone git@github.com:YOUR-USERNAME/psutil.git`` - Create a branch: ``git checkout -b new-feature`` -- Commit your changes: ``git commit -am 'add some feature'`` -- Push the branch: ``git push origin new-feature`` -- Create a new PR via the GitHub web interface and sign-off your work (see - `CONTRIBUTING.md`_ guidelines) +- Commit changes: ``git commit -am 'add some feature'`` +- Push: ``git push origin new-feature`` +- Open a PR and sign off your work (see `CONTRIBUTING.md`_). + Continuous integration ---------------------- -Unit tests are automatically run on every ``git push`` on all platforms except -AIX. See config files in the `.github/workflows `_ -directory. +Unit tests run automatically on every ``git push`` on all platforms except +AIX. See `.github/workflows `_. + Documentation ------------- -- The documentation source is located in the `docs/`_ directory. -- To build it and generate the HTML (in the ``_build/html`` directory): +- Source is in the `docs/`_ directory. +- To build HTML: .. code-block:: bash @@ -169,33 +162,26 @@ Documentation python3 -m pip install -r requirements.txt make html -- The public documentation is hosted at https://psutil.readthedocs.io. -- There are 2 versions, which you can select from the dropdown menu at the top - left of the page: - - - `/stable `_: generated from the most - recent Git tag (latest released psutil version). - - `/latest `_: generated from the - master branch. It is automatically updated on git push. - -Redirects: +- Doc is hosted at https://psutil.readthedocs.io (redirects to `/stable`_). +- There's 2 versions of the doc (can be selected via dropdown on the top left): -- https://psutil.readthedocs.io redirects to - `/stable `_ by default. + - `/stable`_: latest release published on `PyPI`_ + - `/latest`_: ``master`` development branch .. note:: - The ``/latest`` version reflects the development branch and may contain - unreleased changes. For stable documentation, use ``/stable``. + ``/latest`` may contain unreleased changes. Use ``/stable`` for + production docs. Releases -------- -- Releases are uploaded to `PyPI `_ via - ``make release``. +- Uploaded to `PyPI`_ via ``make release``. - Git tags use the ``vX.Y.Z`` format (e.g. ``v7.2.2``). - The version string is defined in ``psutil/__init__.py`` (``__version__``). +.. _`/latest`: https://psutil.readthedocs.io/latest +.. _`/stable`: https://psutil.readthedocs.io/stable .. _`changelog.rst`: https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`credits.rst`: https://github.com/giampaolo/psutil/blob/master/docs/credits.rst @@ -206,7 +192,6 @@ Releases .. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/ .. _`psutil/__init__.py`: https://github.com/giampaolo/psutil/blob/master/psutil/__init__.py .. _`psutil/_pslinux.py`: https://github.com/giampaolo/psutil/blob/master/psutil/_pslinux.py -.. _`psutil/arch/linux/disk.c`: https://github.com/giampaolo/psutil/blob/master/psutil/arch/linux/disk.c -.. _`tests/test_linux.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_linux.py +.. _`PyPI`: https://pypi.org/project/psutil/ .. _`tests/test_process.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_process.py .. _`tests/test_system.py`: https://github.com/giampaolo/psutil/blob/master/tests/test_system.py diff --git a/docs/faq.rst b/docs/faq.rst index c036a52646..8e34646534 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -228,6 +228,17 @@ The only way is to have its parent process call ``wait()`` (or ``waitpid()``). If the parent never does this, killing the parent will cause the zombie to be re-parented to ``init`` / ``systemd``, which will reap it automatically. +.. _faq_open_files_windows: + +Why does open_files() not return all files on Windows? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`Process.open_files` on Windows is not guaranteed to enumerate all +regular file handles. The underlying Windows API may hang when retrieving +certain handle names, so psutil spawns a thread to query each handle and +kills it if it doesn't respond within 100 ms. This means some entries can be +missed. This is a known OS-level limitation shared by tools like Process +Hacker (see `issue 597 `_). .. _faq_pid_exists_vs_isrunning: @@ -291,6 +302,12 @@ example, on a 4-core machine a fully-loaded process can reach 400%. The system-wide :func:`cpu_percent` (without a :class:`Process`) always stays in the 0–100% range because it averages across all cores. +The returned value is explicitly *not* split evenly between all available +CPUs. This is consistent with the ``top`` UNIX utility: a busy loop on a +system with 2 logical CPUs is reported as 100%, not 50%. Note that Windows +``taskmgr.exe`` behaves differently (it would report 50%). To emulate that: +``p.cpu_percent() / psutil.cpu_count()``. + .. _faq_cpu_count: What is the difference between psutil, os, and multiprocessing cpu_count()? diff --git a/docs/migration.rst b/docs/migration.rst index f05f363130..642685e590 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -30,8 +30,8 @@ Key breaking changes in 8.0: :meth:`Process.memory_footprint`. - New :meth:`Process.memory_info_ex` (unrelated to the old method deprecated in 4.0 and removed in 7.0). -- New :attr:`Process.attrs`: frozenset of valid attribute names. -- ``process_iter(attrs=[])`` is deprecated. +- New :attr:`Process.attrs`: frozenset of valid attribute names; + ``process_iter(attrs=[])`` is deprecated. - Python 3.6 dropped. .. important:: @@ -134,17 +134,7 @@ cpu_times() interrupt renamed to irq on Windows The ``interrupt`` field of :func:`cpu_times` on Windows was renamed to ``irq`` to match the name used on Linux and BSD. The old name still works but raises -:exc:`DeprecationWarning`: - -.. code-block:: python - - # before - t = psutil.cpu_times() - print(t.interrupt) - - # after - t = psutil.cpu_times() - print(t.irq) +:exc:`DeprecationWarning`. Status and connection fields are now enums ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -163,18 +153,8 @@ Code inspecting ``repr()`` or ``type()`` may need updating. memory_full_info() is deprecated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:meth:`Process.memory_full_info` is deprecated. Use the new -:meth:`Process.memory_footprint` instead: - -.. code-block:: python - - # before - mem = p.memory_full_info() - uss = mem.uss - - # after - mem = p.memory_footprint() - uss = mem.uss +:meth:`Process.memory_full_info` is deprecated. Use +:meth:`Process.memory_footprint` instead (same fields). New memory_info_ex() method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -202,7 +182,9 @@ discover available names: # after attrs = psutil.Process.attrs -It also makes it easy to pass all or a subset of attributes: +It also makes it easy to pass all or a subset of attributes. +``process_iter(attrs=[])`` (empty list meaning "all") is now deprecated; +use ``Process.attrs`` instead: .. code-block:: python @@ -212,21 +194,6 @@ It also makes it easy to pass all or a subset of attributes: # all except connections psutil.process_iter(attrs=psutil.Process.attrs - {"net_connections"}) -process_iter(attrs=[]) is deprecated -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Passing an empty list to :func:`process_iter` to mean "retrieve all -attributes" is deprecated and raises :exc:`DeprecationWarning`. Use -:attr:`Process.attrs` instead: - -.. code-block:: python - - # before - psutil.process_iter(attrs=[]) - - # after - psutil.process_iter(attrs=psutil.Process.attrs) - Python 3.6 dropped ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/performance.rst b/docs/performance.rst index 2f67ab6b5b..2f8b28447c 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -40,10 +40,10 @@ Fast: p.memory_info() # from cache p.status() # from cache -The speed improvement depends on the platform and on how many attributes -you read. On Linux the gain is typically around 1.5x–2x; on Windows it can -be much higher. As a rule of thumb: if you read more than two attributes -from the same process, use :meth:`Process.oneshot`. +The speed improvement depends on the platform and on how many attributes you +read. On Linux the gain is typically around 1.5x–2x; on Windows it can be much +higher. As a rule of thumb: if you read more than one attribute from the same +process, use :meth:`Process.oneshot`. .. _perf-process-iter: @@ -84,7 +84,7 @@ if a process disappears while iterating), since attributes are retrieved in a single pass and exceptions like :exc:`NoSuchProcess` and :exc:`AccessDenied` are handled internally. -A typical use case may be to fetch all process attrs except the slow ones (see +A typical use case is to fetch all process attrs except the slow ones (see :ref:`perf-api-speed` table below): .. code-block:: python @@ -94,35 +94,6 @@ A typical use case may be to fetch all process attrs except the slow ones (see for p in psutil.process_iter(psutil.Process.attrs - {"memory_footprint", "memory_maps"}): ... -.. _perf-pids: - -Avoid pids() + loop -------------------- - -A common but inefficient pattern is to call :func:`pids` and then -construct a :class:`Process` for each PID manually: - -.. code-block:: python - - import psutil - - for pid in psutil.pids(): - try: - p = psutil.Process(pid) - print(p.name()) - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - -Prefer :func:`process_iter` instead. It supports the ``attrs`` pre-fetch, -avoids race conditions, and caches :class:`Process` instances internally: - -.. code-block:: python - - import psutil - - for p in psutil.process_iter(["name"]): - print(p.pid, p.name()) - .. _perf-oneshot-bench: Measuring oneshot() speedup diff --git a/docs/recipes.rst b/docs/recipes.rst index 0820c6c4c7..809a6ca157 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -127,17 +127,6 @@ Processes owned by user: ------------------------------------------------------------------------------- -Processes actively running: - -.. code-block:: pycon - - >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "status"]) if p.status() == psutil.STATUS_RUNNING]) - [(1150, 'Xorg'), - (1776, 'unity-panel-service'), - (20492, 'python3')] - -------------------------------------------------------------------------------- - Processes using log files: .. code-block:: pycon @@ -268,32 +257,6 @@ Kill a process tree (including grandchildren): ------------------------------------------------------------------------------- -Find zombie (defunct) processes: - - -.. code-block:: python - - import psutil - - for p in psutil.process_iter(["status"]): - if p.status() == psutil.STATUS_ZOMBIE: - print(f"zombie: pid={p.pid}") - -------------------------------------------------------------------------------- - -Terminate all processes matching a given name: - -.. code-block:: python - - import psutil - - def terminate_procs_by_name(name): - for p in psutil.process_iter(["name"]): - if p.name() == name: - p.terminate() - -------------------------------------------------------------------------------- - Terminate a process gracefully, falling back to ``SIGKILL`` if it does not exit within the timeout: @@ -311,21 +274,6 @@ exit within the timeout: ------------------------------------------------------------------------------- -Restart a process: - -.. code-block:: python - - import subprocess, psutil - - def restart_process(pid): - p = psutil.Process(pid) - cmd = p.cmdline() - p.terminate() - p.wait() - return subprocess.Popen(cmd) - -------------------------------------------------------------------------------- - Temporarily pause and resume a process using a context manager: .. code-block:: python @@ -429,41 +377,6 @@ them to a human-readable string: return "{:.1f}{}".format(value, s) return "{}B".format(n) -Memory -^^^^^^ - -Show both RAM and swap usage in human-readable form: - -.. code-block:: python - - import psutil - - def print_memory(): - ram = psutil.virtual_memory() - swap = psutil.swap_memory() - print( - "RAM: total={}, used={}, free={}, percent={}%".format( - bytes2human(ram.total), - bytes2human(ram.used), - bytes2human(ram.available), - ram.percent, - ) - ) - print( - "Swap: total={}, used={}, free={}, percent={}%".format( - bytes2human(swap.total), - bytes2human(swap.used), - bytes2human(swap.free), - swap.percent, - ) - ) - - -.. code-block:: none - - RAM: total=8.0G, used=4.5G, free=3.0G, percent=56.2% - Swap: total=2.0G, used=0.1G, free=1.9G, percent=4.1% - CPU ^^^ @@ -482,51 +395,9 @@ Print real-time CPU usage percentage: CPU: 1.4% CPU: 0.9% -------------------------------------------------------------------------------- - -For each CPU core: - -.. code-block:: python - - import psutil - - while True: - for i, pct in enumerate(psutil.cpu_percent(percpu=True, interval=1)): - print("CPU-{}: {}%".format(i, pct)) - print() - -.. code-block:: none - - CPU-0: 1.0% - CPU-1: 2.1% - CPU-2: 3.0% - Disks ^^^^^ -Show disk usage for all mounted partitions: - -.. code-block:: python - - import psutil - - def print_disk_usage(): - for part in psutil.disk_partitions(): - usage = psutil.disk_usage(part.mountpoint) - print("{:<20} total={:<8} used={:<8} free={:<8} percent={}%".format( - part.mountpoint, - bytes2human(usage.total), bytes2human(usage.used), - bytes2human(usage.free), usage.percent)) - -.. code-block:: none - - / total=47.8G used=17.4G free=27.9G percent=38.4% - /boot/efi total=256.0M used=73.6M free=182.4M percent=28.8% - /home total=878.7G used=497.5G free=336.5G percent=59.7% - - -------------------------------------------------------------------------------- - Show real-time disk I/O: .. code-block:: python @@ -550,29 +421,6 @@ Show real-time disk I/O: Network ^^^^^^^ -List IP addresses for each network interface: - -.. code-block:: python - - import psutil, socket - - def print_net_addrs(): - for iface, addrs in psutil.net_if_addrs().items(): - for addr in addrs: - if addr.family == socket.AF_INET: - print( - "{:<15} address={:<15} netmask={}".format( - iface, addr.address, addr.netmask - ) - ) - -.. code-block:: none - - lo address=127.0.0.1 netmask=255.0.0.0 - eth0 address=10.0.0.4 netmask=255.255.255.0 - -------------------------------------------------------------------------------- - Show real-time network I/O per interface: .. code-block:: python diff --git a/psutil/__init__.py b/psutil/__init__.py index 1649a70728..a99f427738 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -256,8 +256,8 @@ def _export_enum(cls): 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(). + """Return a `{pid: ppid, ...}` dict for all running processes in + one shot. Used to speed up `Process.children()`. """ ret = {} for pid in pids(): @@ -290,10 +290,10 @@ def _check_conn_kind(kind): def _use_prefetch(method): - """Decorator returning cached values from process_iter(attrs=...). + """Decorator returning cached values from `process_iter(attrs=...)`. - When process_iter() is called with an *attrs* argument, it - pre-fetches the requested attributes via as_dict() and stores + When `process_iter()` is called with an *attrs* argument, it + pre-fetches the requested attributes via `as_dict()` and stores them in Process._prefetch. This decorator makes the decorated method return the cached value (if present) instead of issuing a new system call. @@ -314,7 +314,7 @@ def wrapper(self, *args, **kwargs): class Process: """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. + Raise `NoSuchProcess` if PID does not exist. Note that most of the methods of this class do not make sure that the PID of the process being queried has been reused. That means @@ -323,20 +323,20 @@ class Process: The only exceptions for which process identity is pre-emptively checked and guaranteed are: - - parent() - - children() - - nice() (set) - - ionice() (set) - - rlimit() (set) - - cpu_affinity (set) - - suspend() - - resume() - - send_signal() - - terminate() - - kill() + - `parent()` + - `children()` + - `nice()` (set) + - `ionice()` (set) + - `rlimit()` (set) + - `cpu_affinity` (set) + - `suspend()` + - `resume()` + - `send_signal()` + - `terminate()` + - `kill()` To prevent this problem for all other methods you can use - is_running() before querying the process. + `is_running()` before querying the process. """ attrs: frozenset[str] = frozenset() # dynamically set later @@ -487,7 +487,7 @@ def __hash__(self): return self._hash def _raise_if_pid_reused(self): - """Raises NoSuchProcess in case process PID has been reused.""" + """Raise `NoSuchProcess` in case process PID has been reused.""" if self._pid_reused or (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 @@ -506,7 +506,7 @@ def pid(self) -> int: # DEPRECATED @property def info(self) -> dict: - """Return pre-fetched process_iter() info dict. + """Return pre-fetched `process_iter()` info dict. Deprecated: use method calls instead (e.g. p.name()). """ @@ -523,20 +523,14 @@ def info(self) -> dict: @contextlib.contextmanager def oneshot(self) -> Generator[None, None, None]: - """Utility context manager which considerably speeds up the - retrieval of multiple process information at the same time. - - Internally different process info (e.g. name, ppid, uids, - gids, ...) may be fetched by using the same routine, but - only one information is returned and the others are discarded. - When using this context manager the internal routine is - executed once (in the example below on name()) and the - other info are cached. - - The cache is cleared when exiting the context manager block. - The advice is to use this every time you retrieve more than - one information about the process. If you're lucky, you'll - get a hell of a speedup. + """Context manager which speeds up the retrieval of multiple + process attributes at the same time. Internally, many + attributes (e.g. `name()`, `ppid()`, `uids()`, `create_time()`, + ...) share the same system call. This context manager executes + each system call once, and caches the results, so subsequent + calls return cached values. The cache is cleared when exiting + the context manager block. Use this every time you retrieve + more than one attribute about the process. >>> import psutil >>> p = psutil.Process() @@ -600,7 +594,7 @@ def as_dict( are assumed. See `Process.attrs` for a full list. *ad_value* is the value which gets assigned in case - AccessDenied or ZombieProcess exception is raised when + `AccessDenied` or `ZombieProcess` exception is raised when retrieving that particular process information. """ valid_names = self.attrs @@ -807,7 +801,7 @@ def cmdline(self) -> list[str]: @_use_prefetch def status(self) -> ProcessStatus | str: - """The process current status as a STATUS_* constant.""" + """The process current status as a `STATUS_` constant.""" try: return self._proc.status() except ZombieProcess: @@ -816,7 +810,8 @@ def status(self) -> ProcessStatus | str: @_use_prefetch def username(self) -> str: """The name of the user that owns the process. - On UNIX this is calculated by using *real* process uid. + + On UNIX this is calculated by using the real process uid. """ if POSIX: if pwd is None: @@ -895,11 +890,8 @@ def num_fds(self) -> int: @_use_prefetch def io_counters(self) -> pio: - """Return process I/O statistics as a - (read_count, write_count, read_bytes, write_bytes) - named tuple. - Those are the number of read/write calls performed and the - amount of bytes read and written by the process. + """Return process I/O statistics (primarily read and + written bytes). """ return self._proc.io_counters() @@ -912,14 +904,15 @@ def ionice( ) -> pionice | ProcessIOPriority | None: """Get or set process I/O niceness (priority). - On Linux *ioclass* is one of the IOPRIO_CLASS_* constants. + On Linux *ioclass* is one of the `IOPRIO_CLASS_*` constants. *value* is a number which goes from 0 to 7. The higher the value, the lower the I/O priority of the process. - On Windows only *ioclass* is used and it can be set to 2 - (normal), 1 (low) or 0 (very low). + On Windows only *ioclass* is used and it can be set to + one of the `IOPRIO_*` constants (IOPRIO_VERYLOW, + IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH). - Available on Linux and Windows > Vista only. + Availability: Linux, Windows """ if ioclass is None: if value is not None: @@ -941,11 +934,12 @@ def rlimit( """Get or set process resource limits as a (soft, hard) tuple. - *resource* is one of the RLIMIT_* constants. - *limits* is supposed to be a (soft, hard) tuple. + - *resource*: one of the `RLIMIT_*` constants. + - *limits*: a (soft, hard) tuple (set). See "man prlimit" for further info. - Available on Linux and FreeBSD only. + + Availability: Linux and FreeBSD """ if limits is not None: self._raise_if_pid_reused() @@ -959,11 +953,12 @@ def cpu_affinity( self, cpus: list[int] | None = None ) -> list[int] | None: """Get or set process CPU affinity. + If specified, *cpus* must be a list of CPUs for which you - want to set the affinity (e.g. [0, 1]). - If an empty list is passed, all egible CPUs are assumed - (and set). - (Windows, Linux and BSD only). + want to set the affinity (e.g. [0, 1]). If an empty list is + passed, all eligible CPUs are assumed (and set). + + Availability: Linux, Windows, FreeBSD """ if cpus is None: return sorted(set(self._proc.cpu_affinity_get())) @@ -982,6 +977,7 @@ def cpu_affinity( @_use_prefetch def cpu_num(self) -> int: """Return what CPU this process is currently running on. + The returned number should be <= psutil.cpu_count() and <= len(psutil.cpu_percent(percpu=True)). It may be used in conjunction with @@ -1114,7 +1110,7 @@ def cpu_percent(self, interval: float | None = None) -> float: When *interval* is 0.0 or None (default) compares process times to system CPU times elapsed since last call, returning immediately (non-blocking). That means that the first time - this is called it will return a meaningful 0.0 value. + this is called it will return a meaningless 0.0 value. When *interval* is > 0.0 compares process times to system CPU times elapsed before and after the interval (blocking). @@ -1204,11 +1200,13 @@ def timer(): @memoize_when_activated def cpu_times(self) -> pcputimes: """Return a (user, system, children_user, children_system) - named tuple representing the accumulated process time, in - seconds. - This is similar to os.times() but per-process. - On macOS and Windows children_user and children_system are - always set to 0. + named tuple representing the accumulated process time, + expressed in seconds. + + Linux includes an additional `iowait` field. + + On macOS and Windows `children_user` and `children_system` + fields are always set to 0. """ return self._proc.cpu_times() @@ -1227,7 +1225,7 @@ def memory_info(self) -> pmem: @_use_prefetch @memoize_when_activated def memory_info_ex(self) -> pmem_ex: - """Return a named tuple extending memory_info() with extra + """Return a named tuple extending `memory_info()` with extra metrics. All numbers are expressed in bytes. @@ -1252,17 +1250,17 @@ def memory_footprint(self) -> pfootprint: It does so by passing through the whole process address. As such it usually requires higher user privileges than - memory_info() or memory_info_ex() and is considerably + `memory_info()` or `memory_info_ex()` and is considerably slower. """ return self._proc.memory_footprint() # DEPRECATED def memory_full_info(self) -> pfullmem: - """Return the same information as memory_info() plus - memory_footprint() in a single named tuple. + """Return the same information as `memory_info()` plus + `memory_footprint()` in a single named tuple. - DEPRECATED in 8.0.0. Use memory_footprint() instead. + DEPRECATED in 8.0.0. Use `memory_footprint()` instead. """ msg = ( "memory_full_info() is deprecated; use memory_footprint() instead" @@ -1277,6 +1275,7 @@ def memory_full_info(self) -> pfullmem: def memory_percent(self, memtype: str = "rss") -> float: """Compare process memory to total physical system memory and calculate process memory utilization as a percentage. + *memtype* argument is a string that dictates what type of process memory you want to compare against (defaults to "rss"). The list of available strings can be obtained like this: @@ -1327,8 +1326,8 @@ def memory_percent(self, memtype: str = "rss") -> float: def memory_maps( self, grouped: bool = True ) -> list[pmmap_grouped] | list[pmmap_ext]: - """Return process' mapped memory regions as a list of named tuples - whose fields are variable depending on the platform. + """Return process mapped memory regions as a list of named + tuples whose fields are variable depending on the platform. If *grouped* is True the mapped regions with the same 'path' are grouped together and the different memory fields are summed. @@ -1383,23 +1382,24 @@ def open_files(self) -> list[popenfile]: def net_connections(self, kind: str = "inet") -> list[pconn]: """Return socket connections opened by process as a list of (fd, family, type, laddr, raddr, status) named tuples. + The *kind* parameter filters for connections that match the following criteria: +------------+----------------------------------------------------+ | Kind Value | Connections using | +------------+----------------------------------------------------+ - | inet | IPv4 and IPv6 | - | inet4 | IPv4 | - | inet6 | IPv6 | - | tcp | TCP | - | tcp4 | TCP over IPv4 | - | tcp6 | TCP over IPv6 | - | udp | UDP | - | udp4 | UDP over IPv4 | - | udp6 | UDP over IPv6 | - | unix | UNIX socket (both UDP and TCP protocols) | - | all | the sum of all the possible families and protocols | + | 'inet' | IPv4 and IPv6 | + | 'inet4' | IPv4 | + | 'inet6' | IPv6 | + | 'tcp' | TCP | + | 'tcp4' | TCP over IPv4 | + | 'tcp6' | TCP over IPv6 | + | 'udp' | UDP | + | 'udp4' | UDP over IPv4 | + | 'udp6' | UDP over IPv6 | + | 'unix' | UNIX socket (both UDP and TCP protocols) | + | 'all' | the sum of all the possible families and protocols | +------------+----------------------------------------------------+ """ _check_conn_kind(kind) @@ -1439,10 +1439,11 @@ def _send_signal(self, sig): raise AccessDenied(pid, name) from err def send_signal(self, sig: int) -> None: - """Send a signal *sig* to process pre-emptively checking - whether PID has been reused (see signal module constants) . - On Windows only SIGTERM is valid and is treated as an alias - for kill(). + """Send a signal *sig* to process, pre-emptively checking + whether PID has been reused (see signal module constants). + + On Windows only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT + are valid. SIGTERM is treated as an alias for kill(). """ if POSIX: self._send_signal(sig) @@ -1456,6 +1457,7 @@ def send_signal(self, sig: int) -> None: def suspend(self) -> None: """Suspend process execution with SIGSTOP pre-emptively checking whether PID has been reused. + On Windows this has the effect of suspending all process threads. """ if POSIX: @@ -1467,6 +1469,7 @@ def suspend(self) -> None: def resume(self) -> None: """Resume process execution with SIGCONT pre-emptively checking whether PID has been reused. + On Windows this has the effect of resuming all process threads. """ if POSIX: @@ -1478,6 +1481,7 @@ def resume(self) -> None: def terminate(self) -> None: """Terminate the process with SIGTERM pre-emptively checking whether PID has been reused. + On Windows this is an alias for kill(). """ if POSIX: @@ -1497,21 +1501,22 @@ def kill(self) -> None: self._proc.kill() def wait(self, timeout: float | None = None) -> int | None: - """Wait for process to terminate, and if process is a children + """Wait for process to terminate, and if process is a child 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. + instead of raising `NoSuchProcess`. If *timeout* (in seconds) is specified and process is still - alive, raise TimeoutExpired. + alive, raise `TimeoutExpired`. If *timeout=0* either return immediately or raise - TimeoutExpired (non-blocking). + `TimeoutExpired` (non-blocking). - To wait for multiple Process objects use psutil.wait_procs(). + To wait for multiple Process objects use `psutil.wait_procs()`. """ if self.pid == 0: msg = "can't wait for PID 0" @@ -1554,14 +1559,15 @@ def wait(self, timeout: float | None = None) -> int | None: class Popen(Process): - """Same as subprocess.Popen, but in addition it provides all + """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() + * `send_signal()` + * `terminate()` + * `kill()` This is done in order to avoid killing another process in case its PID has been reused, fixing BPO-6973. @@ -1646,9 +1652,10 @@ def pids() -> list[int]: def pid_exists(pid: int) -> bool: - """Return True if given PID exists in the current process list. - This is faster than doing "pid in psutil.pids()" and - should be preferred. + """Return True if *pid* exists in the current process list. + + This is faster than doing `pid in psutil.pids()` and should be + preferred. """ if pid < 0: return False @@ -1681,7 +1688,7 @@ def process_iter( their PIDs. *attrs* and *ad_value* have the same meaning as in - Process.as_dict(). + `Process.as_dict()`. If *attrs* is specified, `Process.as_dict()` is called and the results are cached, so that subsequent method calls (e.g. @@ -1762,15 +1769,16 @@ def wait_procs( Return a (gone, alive) tuple indicating which processes are gone and which ones are still alive. - The gone ones will have a new *returncode* attribute indicating + The gone ones will have a new `returncode` attribute indicating process exit status (may be None). *callback* is a function which gets called every time a process - terminates (a Process instance is passed as callback argument). + terminates (a `Process` instance is passed as callback argument). Function will return as soon as all processes terminate or when *timeout* occurs. - Differently from Process.wait() it will not raise TimeoutExpired if + + Differently from `Process.wait()` it will not raise `TimeoutExpired` if *timeout* occurs. Typical use case is: @@ -1854,7 +1862,7 @@ def check_gone(proc, timeout): def cpu_count(logical: bool = True) -> int | None: """Return the number of logical CPUs in the system (same as - os.cpu_count()). + `os.cpu_count()`). If *logical* is False return the number of physical cores only (e.g. hyper thread CPUs are excluded). @@ -1877,9 +1885,10 @@ def cpu_count(logical: bool = True) -> int | None: def cpu_times(percpu: bool = False) -> scputimes | list[scputimes]: """Return system-wide CPU times as a named tuple. + Every CPU time represents the seconds the CPU has spent in the - given mode. The named tuple's fields availability varies depending on the - platform: + given mode. The named tuple's fields availability varies depending + on the platform: - user - system @@ -1892,10 +1901,10 @@ def cpu_times(percpu: bool = False) -> scputimes | list[scputimes]: - guest (Linux) - guest_nice (Linux) - When *percpu* is True return a list of named tuples for each CPU. - First element of the list refers to first CPU, second element - to second CPU and so on. - The order of the list is consistent across calls. + When *percpu* is True return a list of named tuples for each + logical CPU. First element of the list refers to first CPU, second + element to second CPU and so on. The order of the list is + consistent across calls. """ if not percpu: return _psplatform.cpu_times() @@ -1919,7 +1928,7 @@ def cpu_times(percpu: bool = False) -> scputimes | list[scputimes]: def _cpu_tot_time(times): - """Given a cpu_time() ntuple calculates the total CPU time + """Given a `cpu_time()` named tuple calculates the total CPU time (including idle time). """ tot = sum(times) @@ -1936,8 +1945,8 @@ def _cpu_tot_time(times): def _cpu_busy_time(times): - """Given a cpu_time() ntuple calculates the busy CPU time. - We do so by subtracting all idle CPU times. + """Given a `cpu_time()` named tuple calculates the busy CPU time by + subtracting all idle CPU times. """ busy = _cpu_tot_time(times) busy -= times.idle @@ -2058,8 +2067,9 @@ def calculate(t1, t2): def cpu_times_percent( interval: float | None = None, percpu: bool = False ) -> scputimes | list[scputimes]: - """Same as cpu_percent() but provides utilization percentages - for each specific CPU time as is returned by cpu_times(). + """Same as `cpu_percent()`, but provides utilization percentages + for each specific CPU time as is returned by `cpu_times()`. + For instance, on Linux we'll get: >>> cpu_times_percent() @@ -2068,7 +2078,7 @@ def cpu_times_percent( >>> *interval* and *percpu* arguments have the same meaning as in - cpu_percent(). + `cpu_percent()`. """ tid = threading.current_thread().ident blocking = interval is not None and interval > 0.0 @@ -2128,8 +2138,9 @@ def cpu_freq(percpu: bool = False) -> scpufreq | list[scpufreq] | None: min and max frequency expressed in Mhz. If *percpu* is True and the system supports per-cpu frequency - retrieval (Linux only) a list of frequencies is returned for - each CPU. If not a list with one element is returned. + retrieval (Linux and FreeBSD), a list of frequencies is + returned for each CPU. If not, a list with one element is + returned. """ ret = _psplatform.cpu_freq() if percpu: @@ -2168,9 +2179,11 @@ def cpu_freq(percpu: bool = False) -> scpufreq | list[scpufreq] | None: def getloadavg() -> tuple[float, float, float]: """Return the average system load over the last 1, 5 and 15 minutes - as a tuple. On Windows this is emulated by using a Windows API that - spawns a thread which keeps running in background and updates - results every 5 seconds, mimicking the UNIX behavior. + as a tuple. + + On Windows this is emulated by using a Windows API that spawns a + thread which keeps running in background and updates results every + 5 seconds, mimicking the UNIX behavior. """ if hasattr(os, "getloadavg"): return os.getloadavg() @@ -2188,7 +2201,7 @@ def virtual_memory() -> svmem: including the following fields, expressed in bytes: - total: - total physical memory available. + total physical memory available - available: the memory that can be given instantly to processes without the @@ -2198,14 +2211,11 @@ def virtual_memory() -> svmem: memory usage in a cross platform fashion. - percent: - the percentage usage calculated as (total - available) / total * 100 + the percentage usage calculated as `(total - available) / total * 100` - used: memory used, calculated differently depending on the platform and - designed for informational purposes only: - macOS: active + wired - BSD: active + wired + cached - Linux: total - free + designed for informational purposes only - free: memory not being used at all (zeroed) that is readily available; @@ -2226,7 +2236,7 @@ def virtual_memory() -> svmem: - cached (BSD, macOS): cache for various things. - - wired (macOS, BSD): + - wired (macOS, BSD, Windows): memory that is marked to always stay in RAM. It is never moved to disk. - shared (BSD): @@ -2274,6 +2284,7 @@ def disk_usage(path: str) -> sdiskusage: def disk_partitions(all: bool = False) -> list[sdiskpart]: """Return mounted partitions as a list of (device, mountpoint, fstype, opts) named tuple. + 'opts' field is a raw string separated by commas indicating mount options which may vary depending on the platform. @@ -2293,8 +2304,10 @@ def disk_io_counters( - write_count: number of writes - read_bytes: number of bytes read - write_bytes: number of bytes written - - read_time: time spent reading from disk (in ms) - - write_time: time spent writing to disk (in ms) + - read_time: (not NetBSD, OpenBSD) time spent reading from + disk (in ms) + - write_time: (not NetBSD, OpenBSD) time spent writing to + disk (in ms) Platform specific: @@ -2303,19 +2316,13 @@ def disk_io_counters( - write_merged_count (Linux): number of merged writes If *perdisk* is True return the same information for every - physical disk installed on the system as a dictionary - with partition names as the keys and the named tuple - described above as the values. - - If *nowrap* is True it detects and adjust the numbers which overflow - 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 - cache. - - On recent Windows versions 'diskperf -y' command may need to be - executed first otherwise this function won't find any disk. + physical disk as a dictionary with partition names as the keys. + + If *nowrap* is True (default), counters that overflow and wrap to + zero are automatically adjusted so they never decrease (this can + happen on very busy or long-lived systems). + `disk_io_counters.cache_clear()` can be used to invalidate the + *nowrap* cache. """ kwargs = dict(perdisk=perdisk) if LINUX else {} rawdict = _psplatform.disk_io_counters(**kwargs) @@ -2359,16 +2366,14 @@ def net_io_counters( (always 0 on macOS and BSD) If *pernic* is True return the same information for every - network interface installed on the system as a dictionary - with network interface names as the keys and the named tuple - described above as the values. - - If *nowrap* is True it detects and adjust the numbers which overflow - 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. - "net_io_counters.cache_clear()" can be used to invalidate the - cache. + network interface as a dictionary with interface names as the + keys. + + If *nowrap* is True (default), counters that overflow and wrap to + zero are automatically adjusted so they never decrease (this can + happen on very busy or long-lived systems). + `net_io_counters.cache_clear()` can be used to invalidate the + *nowrap* cache. """ rawdict = _psplatform.net_io_counters() if not rawdict: @@ -2392,25 +2397,27 @@ def net_io_counters( def net_connections(kind: str = 'inet') -> list[sconn]: """Return system-wide socket connections as a list of (fd, family, type, laddr, raddr, status, pid) named tuples. + In case of limited privileges 'fd' and 'pid' may be set to -1 and None respectively. + The *kind* parameter filters for connections that fit the following criteria: +------------+----------------------------------------------------+ | Kind Value | Connections using | +------------+----------------------------------------------------+ - | inet | IPv4 and IPv6 | - | inet4 | IPv4 | - | inet6 | IPv6 | - | tcp | TCP | - | tcp4 | TCP over IPv4 | - | tcp6 | TCP over IPv6 | - | udp | UDP | - | udp4 | UDP over IPv4 | - | udp6 | UDP over IPv6 | - | unix | UNIX socket (both UDP and TCP protocols) | - | all | the sum of all the possible families and protocols | + | 'inet' | IPv4 and IPv6 | + | 'inet4' | IPv4 | + | 'inet6' | IPv6 | + | 'tcp' | TCP | + | 'tcp4' | TCP over IPv4 | + | 'tcp6' | TCP over IPv6 | + | 'udp' | UDP | + | 'udp4' | UDP over IPv4 | + | 'udp6' | UDP over IPv6 | + | 'unix' | UNIX socket (both UDP and TCP protocols) | + | 'all' | the sum of all the possible families and protocols | +------------+----------------------------------------------------+ On macOS this function requires root privileges. @@ -2420,22 +2427,20 @@ def net_connections(kind: str = 'inet') -> list[sconn]: def net_if_addrs() -> dict[str, list[snicaddr]]: - """Return the addresses associated to each NIC (network interface - card) installed on the system as a dictionary whose keys are the - NIC names and value is a list of named tuples for each address - assigned to the NIC. Each named tuple includes 5 fields: - - - family: can be either socket.AF_INET, socket.AF_INET6 or - psutil.AF_LINK, which refers to a MAC address. - - address: is the primary address and it is always set. - - netmask: and 'broadcast' and 'ptp' may be None. - - ptp: stands for "point to point" and references the - destination address on a point to point interface - (typically a VPN). - - broadcast: and *ptp* are mutually exclusive. - - Note: you can have more than one address of the same family - associated with each interface. + """Return a dictionary mapping each NIC (Network Interface Card) to + a list of named tuples representing its addresses. Multiple + addresses of the same family can exist per interface. + + The named tuple includes 5 fields (addresses may be None): + + - family: the address family, either `AF_INET`, `AF_INET6`, + `psutil.AF_LINK` (a MAC address) or `AF_UNSPEC` (a virtual or + unconfigured NIC). + - address: the primary NIC address + - netmask: the netmask address + - broadcast: the broadcast address; always None on Windows + - ptp: a "point to point" address (typically a VPN); always None on + Windows """ rawlist = _psplatform.net_if_addrs() rawlist.sort(key=lambda x: x[1]) # sort by family @@ -2486,11 +2491,12 @@ def net_if_stats() -> dict[str, snicstats]: NIC names and value is a named tuple with the following fields: - isup: whether the interface is up (bool) - - duplex: can be either NIC_DUPLEX_FULL, NIC_DUPLEX_HALF or - NIC_DUPLEX_UNKNOWN + - duplex: can be either `NIC_DUPLEX_FULL`, `NIC_DUPLEX_HALF` or + `NIC_DUPLEX_UNKNOWN` - speed: the NIC speed expressed in mega bits (MB); if it can't be determined (e.g. 'localhost') it will be set to 0. - mtu: the maximum transmission unit expressed in bytes. + - flags: a string of comma-separated flags on the interface. """ return _psplatform.net_if_stats() @@ -2506,10 +2512,12 @@ def net_if_stats() -> dict[str, snicstats]: def sensors_temperatures( fahrenheit: bool = False, ) -> dict[str, list[shwtemp]]: - """Return hardware temperatures. Each entry is a named tuple - representing a certain hardware sensor (it may be a CPU, an - hard disk or something else, depending on the OS and its - configuration). + """Return hardware temperatures. + + Each entry is a named tuple representing a certain hardware + sensor (it may be a CPU, an hard disk or something else, + depending on the OS and its configuration). + All temperatures are expressed in celsius unless *fahrenheit* is set to True. """ @@ -2563,7 +2571,7 @@ def sensors_battery() -> sbattery | None: - percent: battery power left as a percentage. - secsleft: a rough approximation of how many seconds are left before the battery runs out of power. May be - POWER_TIME_UNLIMITED or POWER_TIME_UNLIMITED. + `POWER_TIME_UNLIMITED` or `POWER_TIME_UNKNOWN`. - power_plugged: True if the AC power cable is connected. """ return _psplatform.sensors_battery() @@ -2578,23 +2586,26 @@ def sensors_battery() -> sbattery | None: def boot_time() -> float: """Return the system boot time expressed in seconds since the epoch - (seconds since January 1, 1970, at midnight UTC). The returned - value is based on the system clock, which means it may be affected - by changes such as manual adjustments or time synchronization (e.g. - NTP). + (seconds since January 1, 1970, at midnight UTC). + + The returned value is based on the system clock, which means it may + be affected by changes such as manual adjustments or time + synchronization (e.g. NTP). """ return _psplatform.boot_time() def users() -> list[suser]: """Return users currently connected on the system as a list of - named tuples including the following fields. + named tuples including the following fields: - user: the name of the user - terminal: the tty or pseudo-tty associated with the user, if any. - host: the host name associated with the entry, if any. - started: the creation time as a floating point number expressed in seconds since the epoch. + - pid: the PID of the login process (None on Windows and + OpenBSD). """ return _psplatform.users() @@ -2607,14 +2618,15 @@ def users() -> list[suser]: if WINDOWS: def win_service_iter() -> Iterator[WindowsService]: - """Return a generator yielding a WindowsService instance for all - Windows services installed. + """Return a generator yielding a `WindowsService` instance for + all Windows services installed. """ return _psplatform.win_service_iter() def win_service_get(name) -> WindowsService: """Get a Windows service by *name*. - Raise NoSuchProcess if no service with such name exists. + + Raise `NoSuchProcess` if no service with such name exists. """ return _psplatform.win_service_get(name) @@ -2669,7 +2681,7 @@ def heap_trim() -> None: def _set_debug(value): - """Enable or disable PSUTIL_DEBUG option, which prints debugging + """Enable or disable `PSUTIL_DEBUG` option, which prints debugging messages to stderr. """ import psutil._common From 90e4de4e774f15102641debc65b18a400eccb4cb Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 1 Apr 2026 23:19:29 +0200 Subject: [PATCH 1670/1714] Doc: move scripts/ link refs into _links.rst --- docs/_links.rst | 21 ++++++++++++++++++++- docs/api.rst | 17 ----------------- docs/changelog.rst | 43 ++++++++++++++----------------------------- 3 files changed, 34 insertions(+), 47 deletions(-) diff --git a/docs/_links.rst b/docs/_links.rst index 96c16834e6..42646f16fa 100644 --- a/docs/_links.rst +++ b/docs/_links.rst @@ -1,4 +1,23 @@ .. _`BPO-10784`: https://bugs.python.org/issue10784 .. _`BPO-12442`: https://bugs.python.org/issue12442 -.. _`GH-144047`: https://github.com/python/cpython/pull/144047 .. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`GH-144047`: https://github.com/python/cpython/pull/144047 +.. _`scripts/battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`scripts/cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`scripts/disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`scripts/fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`scripts/free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py +.. _`scripts/ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`scripts/iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`scripts/meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`scripts/netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`scripts/nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`scripts/pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py +.. _`scripts/pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`scripts/procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`scripts/procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`scripts/ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py +.. _`scripts/pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py +.. _`scripts/sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`scripts/temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py +.. _`scripts/top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py diff --git a/docs/api.rst b/docs/api.rst index 2408df75a9..2dcb913cca 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2998,23 +2998,6 @@ Other constants .. _`psleak`: https://github.com/giampaolo/psleak .. _`/proc/meminfo`: https://man7.org/linux/man-pages/man5/proc_meminfo.5.html -.. === scripts - -.. _`scripts/battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py -.. _`scripts/cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`scripts/disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`scripts/fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py -.. _`scripts/ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py -.. _`scripts/iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`scripts/meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`scripts/netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`scripts/nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`scripts/pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`scripts/procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`scripts/procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`scripts/sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py -.. _`scripts/temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py - .. === Windows API .. _`GetExitCodeProcess`: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess diff --git a/docs/changelog.rst b/docs/changelog.rst index 3aac4fffa2..3b38b2f862 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,4 +1,5 @@ .. currentmodule:: psutil +.. include:: _links.rst Changelog ========= @@ -1081,7 +1082,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: ``min`` and ``max`` fields can't be determined. (patch by Alex Manuskin) - :gh:`1462`, [Linux]: (tests) make tests invariant to ``LANG`` setting (patch by Benjamin Drung) -- :gh:`1463`: `cpu_distribution.py`_ script was broken. +- :gh:`1463`: `scripts/cpu_distribution.py`_ was broken. - :gh:`1470`, [Linux]: :func:`disk_partitions`: fix corner case when ``/etc/mtab`` doesn't exist. (patch by Cedric Lamoriniere) - :gh:`1471`, [SunOS]: :meth:`Process.name` and :meth:`Process.cmdline` can @@ -1755,7 +1756,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: values are more precise and match ``free`` cmdline utility. ``available`` also takes into account LCX containers preventing ``available`` to overflow ``total``. -- :gh:`891`: `procinfo.py`_ script has been updated and provides a lot more +- :gh:`891`: `scripts/procinfo.py`_ has been updated and provides a lot more info. **Bug fixes** @@ -1902,7 +1903,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`760`: expose OS constants (``psutil.LINUX``, ``psutil.OSX``, etc.) - :gh:`756`, [Linux]: :func:`disk_io_counters` return 2 new fields: ``read_merged_count`` and ``write_merged_count``. -- :gh:`762`: new `procsmem.py`_ script. +- :gh:`762`: add `scripts/procsmem.py`_. **Bug fixes** @@ -2162,10 +2163,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`521`: drop support for Python 2.4 and 2.5. -- :gh:`553`: new `pstree.py`_ script. +- :gh:`553`: add `scripts/pstree.py`_. - :gh:`564`: C extension version mismatch in case the user messed up with psutil installation or with sys.path is now detected at import time. -- :gh:`568`: new `pidof.py`_ script. +- :gh:`568`: add `scripts/pidof.py`_. - :gh:`569`, [FreeBSD]: add support for :meth:`Process.cpu_affinity` on FreeBSD. @@ -2205,7 +2206,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`492`: use ``tox`` to run tests on multiple Python versions. (patch by msabramo) - :gh:`505`, [Windows]: distribution as wheel packages. -- :gh:`511`: add `ps.py`_ script. +- :gh:`511`: add `scripts/ps.py`_. **Bug fixes** @@ -2496,7 +2497,7 @@ cases accessing the old names will work but it will cause a percentages. - :gh:`408`: ``STATUS_*`` and ``CONN_*`` constants don't properly serialize on JSON. -- :gh:`411`, [Windows]: `disk_usage.py`_ may pop-up a GUI error. +- :gh:`411`, [Windows]: `scripts/disk_usage.py`_ may pop-up a GUI error. - :gh:`413`, [Windows]: :meth:`Process.memory_info` leaks memory. - :gh:`414`, [Windows]: :meth:`Process.exe` on Windows XP may raise ``ERROR_INVALID_PARAMETER``. @@ -2675,7 +2676,7 @@ cases accessing the old names will work but it will cause a :meth:`Process.memory_percent`, :meth:`Process.cpu_times`, :meth:`Process.cpu_percent`, :meth:`Process.num_threads`. -- :gh:`300`: add `pmap.py`_ script. +- :gh:`300`: add `scripts/pmap.py`_. - :gh:`301`: :func:`process_iter` now yields processes sorted by their PIDs. - :gh:`302`: per-process number of voluntary and involuntary context switches (:meth:`Process.num_ctx_switches`). @@ -2687,12 +2688,12 @@ cases accessing the old names will work but it will cause a :meth:`Process.memory_info`, :meth:`Process.memory_percent`, :meth:`Process.num_handles`, :meth:`Process.io_counters`. -- :gh:`305`: add `netstat.py`_ script. +- :gh:`305`: add `scripts/netstat.py`_. - :gh:`311`: system memory functions has been refactorized and rewritten and now provide a more detailed and consistent representation of the system memory. Added new :func:`virtual_memory` and :func:`swap_memory` functions. All old memory-related functions are deprecated. Also two new - example scripts were added: `free.py`_ and `meminfo.py`_. + example scripts were added: `scripts/free.py`_ and `scripts/meminfo.py`_. - :gh:`312`: ``net_io_counters()`` named tuple includes 4 new fields: ``errin``, ``errout``, ``dropin`` and ``dropout``, reflecting the number of packets dropped and with errors. @@ -2849,13 +2850,13 @@ cases accessing the old names will work but it will cause a the function return immediately. - :gh:`206`: disk I/O counters (:func:`disk_io_counters`). (macOS and Windows patch by Jeremy Whitlock) -- :gh:`213`: add `iotop.py`_ script. +- :gh:`213`: add `scripts/iotop.py`_. - :gh:`217`: :meth:`Process.connections` now has a ``kind`` argument to filter for connections with different criteria. - :gh:`221`, [FreeBSD]: :meth:`Process.open_files` has been rewritten in C and no longer relies on ``lsof``. -- :gh:`223`: add `top.py`_ script. -- :gh:`227`: add `nettop.py`_ script. +- :gh:`223`: add `scripts/top.py`_. +- :gh:`227`: add `scripts/nettop.py`_. **Bug fixes** @@ -3122,19 +3123,3 @@ cases accessing the old names will work but it will cause a raises :exc:`AccessDenied` exception instead of ``WindowsError``. - :gh:`30`: psutil.get_pid_list() was returning two 0 PIDs. - - -.. _`cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py -.. _`iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py -.. _`pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py -.. _`pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py -.. _`top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py From c12ffb4ca451ec8721030a212525f18b62243c4f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Apr 2026 02:17:10 +0200 Subject: [PATCH 1671/1714] Adjust docstrings + add C 'heap' entry to glossary.rst --- docs/alternatives.rst | 4 +- docs/api.rst | 21 ++-- docs/credits.rst | 8 +- docs/devguide.rst | 6 +- docs/glossary.rst | 19 ++- docs/install.rst | 10 -- docs/performance.rst | 14 +-- docs/shell-equivalents.rst | 4 + docs/stdlib-equivalents.rst | 5 +- psutil/__init__.py | 237 +++++++++++++++++------------------- 10 files changed, 159 insertions(+), 169 deletions(-) diff --git a/docs/alternatives.rst b/docs/alternatives.rst index 0fd9f71497..d9dd90ee5c 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -10,8 +10,8 @@ See also :doc:`adoption` for notable projects that use psutil. Python standard library ----------------------- -See also :doc:`stdlib-equivalents` for a detailed function-by-function -comparison. +.. seealso:: + :doc:`stdlib-equivalents` for a detailed function-by-function comparison. os module ^^^^^^^^^ diff --git a/docs/api.rst b/docs/api.rst index 2dcb913cca..585614f44d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1643,7 +1643,7 @@ Process class >>> p.io_counters() pio(read_count=454556, write_count=3456, read_bytes=110592, write_bytes=0, read_chars=769931, write_chars=203) - .. availability:: Linux, BSD, Windows, AIX + .. availability:: Linux, Windows, BSD, AIX .. versionchanged:: 5.2.0 added **read_chars** + **write_chars** on Linux and **other_count** + @@ -1651,13 +1651,14 @@ Process class .. method:: num_ctx_switches() - The number of :term:`context switches ` performed by this process + The number of :term:`context switches ` performed by this + process as a ``(voluntary, involuntary)`` named tuple (:term:`cumulative counter`). .. note:: (Windows, macOS) **involuntary** value is always set to 0, while - **voluntary** value reflect the total number of context switches (voluntary - + involuntary). This is a limitation of the OS. + **voluntary** value reflects the total number of context switches + (voluntary + involuntary). This is a limitation of the OS. .. versionchanged:: 5.4.1 added AIX support. @@ -2400,15 +2401,17 @@ Popen class C heap introspection -------------------- -The following functions provide direct access to the platform's native C heap -allocator (such as glibc's ``malloc`` on Linux or ``jemalloc`` on BSD). They +The following functions provide direct access to the platform's native +:term:`heap` allocator (such as glibc's ``malloc`` on Linux or ``jemalloc`` +on BSD). They are low-level interfaces intended for detecting memory leaks in C extensions, -which are usually not revealed via standard RSS/VMS metrics. These functions do +which are usually not revealed via standard RSS / VMS metrics. These functions do not reflect Python object memory; they operate solely on allocations made in C via ``malloc()``, ``free()``, and related calls. The general idea behind these functions is straightforward: capture the state -of the C heap before and after repeatedly invoking a function implemented in a +of the :term:`heap` before and after repeatedly invoking a function +implemented in a C extension, and compare the results. If ``heap_used`` or ``mmap_used`` grows steadily across iterations, the C code is likely retaining memory it should be releasing. This provides an allocator-level way to spot native leaks that @@ -2461,7 +2464,7 @@ Python's memory tracking misses. .. function:: heap_trim() Request that the underlying allocator free any unused memory it's holding in - the heap (typically small ``malloc()`` allocations). + the :term:`heap` (typically small ``malloc()`` allocations). In practice, modern allocators rarely comply, so this is not a general-purpose memory-reduction tool and won't meaningfully shrink RSS in diff --git a/docs/credits.rst b/docs/credits.rst index 38eab06c18..c15b124cbd 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -9,17 +9,17 @@ but here is a short list. A big thanks to all of you. -— Giampaolo +— Giampaolo Rodola Top contributors ---------------- * `Giampaolo Rodola`_: creator, primary author and long-time maintainer * `Jay Loden`_: original co-author, initial design and project bootstrap, - original macOS / Windows / FreeBSD implementations + initial macOS / Windows / FreeBSD implementations * `Arnon Yaari`_: AIX implementation -* `Landry Breuil`_: original OpenBSD implementation -* `Ryo Onodera`_ and `Thomas Klausner`_: original NetBSD implementation +* `Landry Breuil`_: initial OpenBSD implementation +* `Ryo Onodera`_ and `Thomas Klausner`_: initial NetBSD implementation Donations --------- diff --git a/docs/devguide.rst b/docs/devguide.rst index 6f3de13ca9..be60495724 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -54,8 +54,8 @@ Windows ------- - The recommended way to develop on Windows is to use ``make``. -- Install `Git for Windows`_ and launch a **Git Bash shell**, which provides - a Unix-like environment where ``make`` works. +- Install `Git for Windows`_ and launch a *Git Bash shell*, which provides a + Unix-like environment where ``make`` works. - Then run: .. code-block:: bash @@ -185,7 +185,7 @@ Releases .. _`changelog.rst`: https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst .. _`CONTRIBUTING.md`: https://github.com/giampaolo/psutil/blob/master/CONTRIBUTING.md .. _`credits.rst`: https://github.com/giampaolo/psutil/blob/master/docs/credits.rst -.. _`docs/`: https://github.com/giampaolo/psutil/blob/master/psutil/docs/ +.. _`docs/`: https://github.com/giampaolo/psutil/tree/master/docs .. _`Git for Windows`: https://git-scm.com/install/windows .. _`Makefile`: https://github.com/giampaolo/psutil/blob/master/Makefile .. _`PEP-7`: https://www.python.org/dev/peps/pep-0007/ diff --git a/docs/glossary.rst b/docs/glossary.rst index f5d9570567..7b34847122 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -94,7 +94,7 @@ Glossary dropin / dropout Fields in :func:`net_io_counters` counting packets dropped at the - NIC level before they could be processed (``dropin``) or sent + :term:`NIC` level before they could be processed (``dropin``) or sent (``dropout``). Unlike transmission errors, drops indicate the interface or kernel buffer was overwhelmed. A non-zero and growing count is a sign of network saturation or misconfiguration. @@ -118,12 +118,25 @@ Glossary hardware interrupt - A signal sent by a hardware device (disk controller, NIC, keyboard) + A signal sent by a hardware device (disk controller, :term:`NIC`, keyboard) to the CPU to request attention. Each interrupt briefly preempts whatever the CPU was doing. Reported as the ``interrupts`` field of :func:`cpu_stats` and ``irq`` field of :func:`cpu_times`. A very high rate may indicate a misbehaving device driver or a heavily - loaded NIC. Also see :term:`soft interrupt`. + loaded :term:`NIC`. Also see :term:`soft interrupt`. + + heap + + The memory region managed by the platform's native C allocator + (e.g. glibc's ``malloc`` on Linux, ``jemalloc`` on FreeBSD, + ``HeapAlloc`` on Windows). When a C extension calls ``malloc()`` + and never calls ``free()``, the leaked bytes show up here but + are not always visible to Python's memory tracking tools (:mod:`tracemalloc`, + :func:`sys.getsizeof`) or :term:`RSS` / :term:`VMS` . + :func:`heap_info` exposes the current state of the heap, and + :func:`heap_trim` asks the allocator to release unused portions + of it. Together they provide a way to detect memory leaks in C + extensions that standard process-level metrics would otherwise miss. involuntary context switch diff --git a/docs/install.rst b/docs/install.rst index 47bbf6bdce..f1c6d16ad5 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -174,16 +174,6 @@ On Windows, `download pip`_, open cmd.exe and install it with: py get-pip.py -"pip not found" -^^^^^^^^^^^^^^^ - -Sometimes pip is installed but it's not available in your ``PATH`` -("pip command not found" or similar). Try this: - -.. code-block:: none - - python3 -m pip install psutil - Permission errors (UNIX) ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/performance.rst b/docs/performance.rst index 2f8b28447c..ee9c86cf67 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -99,8 +99,9 @@ A typical use case is to fetch all process attrs except the slow ones (see Measuring oneshot() speedup --------------------------- -`bench_oneshot.py`_ -script measures :meth:`Process.oneshot` speedup. E.g. on Linux: +`scripts/internal/bench_oneshot.py`_ measures :meth:`Process.oneshot` speedup. +It also shows which APIs share the same internal kernel routines. E.g. on +Linux: .. code-block:: none @@ -129,15 +130,12 @@ script measures :meth:`Process.oneshot` speedup. E.g. on Linux: oneshot: 1.537 secs speedup: +1.80x -This also shows which APIs share the same internal kernel routines. - .. _perf-api-speed: Measuring APIs speed -------------------- -`print_api_speed.py`_ -script shows the relative cost of each API call. +`scripts/internal/print_api_speed.py`_ shows the relative cost of each API call. This helps you understand which operations are more expensive. E.g. on Linux: @@ -208,5 +206,5 @@ E.g. on Linux: memory_footprint 300 0.02241 memory_maps 300 0.30282 -.. _`bench_oneshot.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/bench_oneshot.py -.. _`print_api_speed.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/print_api_speed.py +.. _`scripts/internal/bench_oneshot.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/bench_oneshot.py +.. _`scripts/internal/print_api_speed.py`: https://github.com/giampaolo/psutil/blob/master/scripts/internal/print_api_speed.py diff --git a/docs/shell-equivalents.rst b/docs/shell-equivalents.rst index 02c722acb7..44516cd2d6 100644 --- a/docs/shell-equivalents.rst +++ b/docs/shell-equivalents.rst @@ -8,6 +8,10 @@ This page maps psutil's Python API to the equivalent native terminal commands on each platform. This is useful for understanding what psutil replaces and for cross-checking results. +.. seealso:: + - :doc:`stdlib-equivalents` + - :doc:`alternatives` + System-wide functions --------------------- diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst index ce6902d3b9..192043761f 100644 --- a/docs/stdlib-equivalents.rst +++ b/docs/stdlib-equivalents.rst @@ -10,8 +10,9 @@ how the two APIs differ. The most common difference is that stdlib functions only operate on the **current process**, while psutil works on **any process** (PID). -See also the :doc:`alternatives` page for a higher-level discussion of -how psutil compares to the standard library and third-party tools. +.. seealso:: + - :doc:`shell-equivalents` + - :doc:`alternatives` System-wide functions --------------------- diff --git a/psutil/__init__.py b/psutil/__init__.py index a99f427738..1d2bd2603d 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -294,7 +294,7 @@ def _use_prefetch(method): When `process_iter()` is called with an *attrs* argument, it pre-fetches the requested attributes via `as_dict()` and stores - them in Process._prefetch. This decorator makes the decorated + them in `Process._prefetch`. This decorator makes the decorated method return the cached value (if present) instead of issuing a new system call. """ @@ -312,30 +312,19 @@ def wrapper(self, *args, **kwargs): class Process: - """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. - - Note that most of the methods of this class do not make sure that - the PID of the process being queried has been reused. That means - that you may end up retrieving information for another process. - - The only exceptions for which process identity is pre-emptively - checked and guaranteed are: - - - `parent()` - - `children()` - - `nice()` (set) - - `ionice()` (set) - - `rlimit()` (set) - - `cpu_affinity` (set) - - `suspend()` - - `resume()` - - `send_signal()` - - `terminate()` - - `kill()` - - To prevent this problem for all other methods you can use + """Represents an OS process identified by a PID. + + If *pid* arg is omitted, the current process PID (`os.getpid()`) is + used. Raises `NoSuchProcess` if the PID does not exist. + + The way this class is bound to a process is via its PID. Most + methods do not guarantee that the PID has not been reused, so you + may end up retrieving information for a different process. + + Real process identity is checked (via PID + creation time) only for + methods that set attributes or send signals. + + To avoid issues with PID reuse for other read-only methods, call `is_running()` before querying the process. """ @@ -393,15 +382,17 @@ def _init(self, pid, _ignore_nsp=False): self._gone = True def _get_ident(self): - """Return a (pid, uid) tuple which is supposed to identify a - Process instance univocally over time. The PID alone is not - enough, as it can be assigned to a new process after this one - terminates, so we add process creation time to the mix. We need - this in order to prevent killing the wrong process later on. - This is also known as PID reuse or PID recycling problem. + """Return a `(pid, uid)` tuple which is supposed to identify a + Process instance univocally over time. + + The PID alone is not enough, as it can be assigned to a new + process after this one terminates, so we add creation time to + the mix. We need this in order to prevent killing the wrong + process later on. This is also known as PID reuse or PID + recycling problem. The reliability of this strategy mostly depends on - create_time() precision, which is 0.01 secs on Linux. The + `create_time()` precision, which is 0.01 secs on Linux. The assumption is that, after a process terminates, the kernel won't reuse the same PID after such a short period of time (0.01 secs). Technically this is inherently racy, but @@ -508,7 +499,7 @@ def pid(self) -> int: def info(self) -> dict: """Return pre-fetched `process_iter()` info dict. - Deprecated: use method calls instead (e.g. p.name()). + Deprecated: use method calls instead (e.g. `p.name()`). """ msg = ( "Process.info is deprecated; use method calls instead" @@ -524,13 +515,14 @@ def info(self) -> dict: @contextlib.contextmanager def oneshot(self) -> Generator[None, None, None]: """Context manager which speeds up the retrieval of multiple - process attributes at the same time. Internally, many - attributes (e.g. `name()`, `ppid()`, `uids()`, `create_time()`, - ...) share the same system call. This context manager executes - each system call once, and caches the results, so subsequent - calls return cached values. The cache is cleared when exiting - the context manager block. Use this every time you retrieve - more than one attribute about the process. + process attributes at the same time. + + Internally, many attributes (e.g. `name()`, `ppid()`, `uids()`, + `create_time()`, ...) share the same system call. This context + manager executes each system call once, and caches the results, + so subsequent calls return cached values. The cache is cleared + when exiting the context manager block. Use this every time you + retrieve more than one attribute about the process. >>> import psutil >>> p = psutil.Process() @@ -638,8 +630,9 @@ def as_dict( return retdict def parent(self) -> Process | None: - """Return the parent process as a Process object pre-emptively + """Return the parent process as a `Process` object, pre-emptively 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] @@ -660,8 +653,10 @@ def parent(self) -> Process | None: pass def parents(self) -> list[Process]: - """Return the parents of this process as a list of Process - instances. If no parents are known return an empty list. + """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() @@ -754,8 +749,9 @@ def name(self) -> str: @_use_prefetch def exe(self) -> str: """The process executable as an absolute path. - May also be an empty string. - The return value is cached after first call. + + May also be an empty string. The return value is cached after + first call. """ def guess_it(fallback): @@ -831,10 +827,11 @@ def username(self) -> str: def create_time(self) -> float: """The process creation time as a floating point number expressed in seconds since the epoch (seconds since January 1, - 1970, at midnight UTC). The return value, which is cached after - first call, is based on the system clock, which means it may be - affected by changes such as manual adjustments or time - synchronization (e.g. NTP). + 1970, at midnight UTC). + + The return value, which is cached after first call, is based on + the system clock, which means it may be affected by changes + such as manual adjustments or time synchronization (e.g. NTP). """ if self._create_time is None: self._create_time = self._proc.create_time() @@ -859,14 +856,14 @@ def nice(self, value: int | None = None) -> int | None: @_use_prefetch @memoize_when_activated def uids(self) -> puids: - """Return process UIDs as a (real, effective, saved) + """Return process UIDs as a `(real, effective, saved)` named tuple. """ return self._proc.uids() @_use_prefetch def gids(self) -> pgids: - """Return process GIDs as a (real, effective, saved) + """Return process GIDs as a `(real, effective, saved)` named tuple. """ return self._proc.gids() @@ -885,17 +882,17 @@ def num_fds(self) -> int: """ return self._proc.num_fds() - # Linux, BSD, AIX and Windows only if hasattr(_psplatform.Process, "io_counters"): @_use_prefetch def io_counters(self) -> pio: """Return process I/O statistics (primarily read and written bytes). + + Availability: Linux, Windows, BSD, AIX """ return self._proc.io_counters() - # Linux and Windows if hasattr(_psplatform.Process, "ionice_get"): @_use_prefetch @@ -909,8 +906,7 @@ def ionice( value, the lower the I/O priority of the process. On Windows only *ioclass* is used and it can be set to - one of the `IOPRIO_*` constants (IOPRIO_VERYLOW, - IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH). + one of the `IOPRIO_*` constants. Availability: Linux, Windows """ @@ -923,7 +919,6 @@ def ionice( self._raise_if_pid_reused() return self._proc.ionice_set(ioclass, value) - # Linux / FreeBSD only if hasattr(_psplatform.Process, "rlimit"): def rlimit( @@ -931,21 +926,20 @@ def rlimit( resource: int, limits: tuple[int, int] | None = None, ) -> tuple[int, int] | None: - """Get or set process resource limits as a (soft, hard) + """Get or set process resource limits as a `(soft, hard)` tuple. - - *resource*: one of the `RLIMIT_*` constants. - - *limits*: a (soft, hard) tuple (set). + - resource: one of the `RLIMIT_*` constants. + - limits: a `(soft, hard)` tuple (set). See "man prlimit" for further info. - Availability: Linux and FreeBSD + Availability: Linux, FreeBSD """ 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"): @_use_prefetch @@ -955,7 +949,7 @@ def cpu_affinity( """Get or set process CPU affinity. If specified, *cpus* must be a list of CPUs for which you - want to set the affinity (e.g. [0, 1]). If an empty list is + want to set the affinity (e.g. `[0, 1]`). If an empty list is passed, all eligible CPUs are assumed (and set). Availability: Linux, Windows, FreeBSD @@ -978,11 +972,7 @@ def cpu_affinity( def cpu_num(self) -> int: """Return what CPU this process is currently running on. - The returned number should be <= psutil.cpu_count() - and <= len(psutil.cpu_percent(percpu=True)). - It may be used in conjunction with - psutil.cpu_percent(percpu=True) to observe the system - workload distributed across CPUs. + The returned number should be <= `psutil.cpu_count()`. """ return self._proc.cpu_num() @@ -991,8 +981,10 @@ def cpu_num(self) -> int: @_use_prefetch def environ(self) -> dict[str, str]: - """The environment variables of the process as a dict. Note: this - might not reflect changes made after the process started. + """The environment variables of the process as a dict. + + Note: this might not reflect changes made after the process + started. """ return self._proc.environ() @@ -1001,7 +993,8 @@ def environ(self) -> dict[str, str]: @_use_prefetch def num_handles(self) -> int: """Return the number of handles opened by this process - (Windows only). + + Availability: Windows """ return self._proc.num_handles() @@ -1022,8 +1015,8 @@ def num_threads(self) -> int: @_use_prefetch def threads(self) -> list[pthread]: """Return threads opened by process as a list of - (id, user_time, system_time) named tuples representing - thread id and thread CPU times (user/system). + `(id, user_time, system_time)` named tuples. + On OpenBSD this method requires root access. """ return self._proc.threads() @@ -1031,6 +1024,7 @@ def threads(self) -> list[pthread]: def children(self, recursive: bool = False) -> list[Process]: """Return the children of this process as a list of Process instances, pre-emptively checking whether PID has been reused. + If *recursive* is True return all the parent descendants. Example (A == this process): @@ -1199,7 +1193,7 @@ def timer(): @_use_prefetch @memoize_when_activated def cpu_times(self) -> pcputimes: - """Return a (user, system, children_user, children_system) + """Return a `(user, system, children_user, children_system)` named tuple representing the accumulated process time, expressed in seconds. @@ -1216,7 +1210,7 @@ def memory_info(self) -> pmem: """Return a named tuple with variable fields depending on the platform, representing memory information about the process. - The "portable" fields available on all platforms are `rss` and `vms`. + The portable fields available on all platforms are `rss` and `vms`. All numbers are expressed in bytes. """ @@ -1241,8 +1235,10 @@ def memory_info_ex(self) -> pmem_ex: @_use_prefetch def memory_footprint(self) -> pfootprint: - """Return a named tuple with USS, PSS and swap memory - metrics. These provide a better representation of + """Return a named tuple with USS memory, and on Linux also + PSS and swap. + + These values provide a more accurate representation of actual process memory usage. USS is the memory unique to a process and which would @@ -1354,15 +1350,15 @@ def memory_maps( @_use_prefetch def page_faults(self) -> ppagefaults: """Return the number of page faults for this process as a - (minor, major) named tuple. + `(minor, major)` named tuple. - - *minor* (a.k.a. *soft* faults): occur when a memory page is + - `minor` (a.k.a. *soft* faults): occur when a memory page is not currently mapped into the process address space, but is already present in physical RAM (e.g. a shared library page loaded by another process). The kernel resolves these without disk I/O. - - *major* (a.k.a. *hard* faults): occur when the page must be + - `major` (a.k.a. *hard* faults): occur when the page must be fetched from disk. These are expensive because they stall the process until I/O completes. @@ -1372,16 +1368,19 @@ def page_faults(self) -> ppagefaults: @_use_prefetch def open_files(self) -> list[popenfile]: - """Return files opened by process as a list of - (path, fd) named tuples including the absolute file name - and file descriptor number. + """Return files opened by process as a list of `(path, fd)` + named tuples including the absolute file name and file + descriptor number. + + On Linux the named tuple also includes `position`, `mode` and + `flags` fields. """ return self._proc.open_files() @_use_prefetch def net_connections(self, kind: str = "inet") -> list[pconn]: """Return socket connections opened by process as a list of - (fd, family, type, laddr, raddr, status) named tuples. + `(fd, family, type, laddr, raddr, status)` named tuples. The *kind* parameter filters for connections that match the following criteria: @@ -1443,7 +1442,7 @@ def send_signal(self, sig: int) -> None: whether PID has been reused (see signal module constants). On Windows only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT - are valid. SIGTERM is treated as an alias for kill(). + are valid. SIGTERM is treated as an alias for `kill()`. """ if POSIX: self._send_signal(sig) @@ -1482,7 +1481,7 @@ def terminate(self) -> None: """Terminate the process with SIGTERM pre-emptively checking whether PID has been reused. - On Windows this is an alias for kill(). + On Windows this is an alias for `kill()`. """ if POSIX: self._send_signal(signal.SIGTERM) @@ -1560,7 +1559,7 @@ def wait(self, timeout: float | None = None) -> int | None: class Popen(Process): """Same as `subprocess.Popen`, but in addition it provides all - psutil.Process methods in a single class. + `Process` methods in a single class. For the following methods which are common to both classes, psutil implementation takes precedence: @@ -1766,7 +1765,7 @@ def wait_procs( """Convenience function which waits for a list of processes to terminate. - Return a (gone, alive) tuple indicating which processes + Return a `(gone, alive)` tuple indicating which processes are gone and which ones are still alive. The gone ones will have a new `returncode` attribute indicating @@ -1887,19 +1886,19 @@ def cpu_times(percpu: bool = False) -> scputimes | list[scputimes]: """Return system-wide CPU times as a named tuple. Every CPU time represents the seconds the CPU has spent in the - given mode. The named tuple's fields availability varies depending - on the platform: - - - user - - system - - idle - - nice (UNIX) - - iowait (Linux) - - irq (Linux, FreeBSD) - - softirq (Linux) - - steal (Linux) - - guest (Linux) - - guest_nice (Linux) + given mode: + + - `user` + - `system` + - `idle` + - `nice` (UNIX) + - `iowait` (Linux) + - `irq` (Linux, FreeBSD) + - `softirq` (Linux) + - `steal` (Linux) + - `guest` (Linux) + - `guest_nice` (Linux) + - `dpc` (Windows) When *percpu* is True return a list of named tuples for each logical CPU. First element of the list refers to first CPU, second @@ -2197,8 +2196,10 @@ def getloadavg() -> tuple[float, float, float]: def virtual_memory() -> svmem: - """Return statistics about system memory usage as a named tuple - including the following fields, expressed in bytes: + """Return statistics about system memory usage as a named tuple. + + The fields vary by platform (see official doc), but the following + are present on all platforms: - total: total physical memory available @@ -2222,28 +2223,9 @@ def virtual_memory() -> svmem: note that this doesn't reflect the actual memory available (use 'available' instead) - Platform-specific fields: - - - active (UNIX): - memory currently in use or very recently used, and so it is in RAM. - - - inactive (UNIX): - memory that is marked as not used. - - - buffers (BSD, Linux): - cache for things like file system metadata. - - - cached (BSD, macOS): - cache for various things. - - - wired (macOS, BSD, Windows): - memory that is marked to always stay in RAM. It is never moved to disk. - - - shared (BSD): - memory that may be simultaneously accessed by multiple processes. + The sum of `used` and `available` does not necessarily equal `total`. - The sum of 'used' and 'available' does not necessarily equal total. - On Windows 'available' and 'free' are the same. + On Windows `available` and `free` are the same. """ global _TOTAL_PHYMEM ret = _psplatform.virtual_memory() @@ -2263,7 +2245,7 @@ def swap_memory() -> sswap: - sin: no. of bytes the system has swapped in from disk (cumulative) - sout: no. of bytes the system has swapped out from disk (cumulative) - 'sin' and 'sout' on Windows are meaningless and always set to 0. + `sin` and `sout` on Windows are meaningless and always set to 0. """ return _psplatform.swap_memory() @@ -2285,7 +2267,7 @@ def disk_partitions(all: bool = False) -> list[sdiskpart]: """Return mounted partitions as a list of (device, mountpoint, fstype, opts) named tuple. - 'opts' field is a raw string separated by commas indicating mount + `opts` field is a raw string separated by commas indicating mount options which may vary depending on the platform. If *all* parameter is False return physical devices only and ignore @@ -2398,7 +2380,7 @@ def net_connections(kind: str = 'inet') -> list[sconn]: """Return system-wide socket connections as a list of (fd, family, type, laddr, raddr, status, pid) named tuples. - In case of limited privileges 'fd' and 'pid' may be set to -1 + In case of limited privileges `fd` and `pid` may be set to -1 and None respectively. The *kind* parameter filters for connections that fit the @@ -2604,8 +2586,7 @@ def users() -> list[suser]: - host: the host name associated with the entry, if any. - started: the creation time as a floating point number expressed in seconds since the epoch. - - pid: the PID of the login process (None on Windows and - OpenBSD). + - pid: the PID of the login process (None on Windows and OpenBSD). """ return _psplatform.users() From 35d3b4d59e8b8d395a58f6b6bbca0dbc9804a385 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Apr 2026 12:13:11 +0200 Subject: [PATCH 1672/1714] C: convert /* comments to // comments --- psutil/_psutil_aix.c | 152 +++++++++-------------------- psutil/_psutil_bsd.c | 15 --- psutil/_psutil_osx.c | 2 - psutil/_psutil_sunos.c | 23 ++--- psutil/_psutil_windows.c | 20 ++-- psutil/arch/aix/common.c | 5 +- psutil/arch/aix/ifaddrs.c | 13 ++- psutil/arch/aix/net_connections.c | 23 +++-- psutil/arch/bsd/heap.c | 4 +- psutil/arch/bsd/net.c | 3 +- psutil/arch/bsd/proc.c | 17 ++-- psutil/arch/freebsd/cpu.c | 27 +++-- psutil/arch/freebsd/proc.c | 42 ++------ psutil/arch/freebsd/proc_socks.c | 4 +- psutil/arch/freebsd/sensors.c | 13 +-- psutil/arch/freebsd/sys_socks.c | 8 +- psutil/arch/netbsd/cpu.c | 15 ++- psutil/arch/netbsd/disk.c | 12 +-- psutil/arch/netbsd/mem.c | 14 ++- psutil/arch/netbsd/proc.c | 2 - psutil/arch/openbsd/mem.c | 3 +- psutil/arch/openbsd/proc.c | 3 +- psutil/arch/osx/cpu.c | 24 +++-- psutil/arch/osx/disk.c | 9 +- psutil/arch/osx/heap.c | 4 +- psutil/arch/osx/mem.c | 11 +-- psutil/arch/osx/proc.c | 79 +++++---------- psutil/arch/osx/proc_utils.c | 40 ++++---- psutil/arch/osx/sensors.c | 10 +- psutil/arch/posix/init.c | 19 ++-- psutil/arch/posix/net.c | 38 +++----- psutil/arch/sunos/environ.c | 131 ++++++++----------------- psutil/arch/sunos/net.c | 15 +-- psutil/arch/sunos/proc.c | 39 ++------ psutil/arch/sunos/sys.c | 2 +- psutil/arch/windows/cpu.c | 31 ++---- psutil/arch/windows/disk.c | 24 +---- psutil/arch/windows/net.c | 10 +- psutil/arch/windows/proc.c | 83 +++------------- psutil/arch/windows/proc_handles.c | 29 +++--- psutil/arch/windows/proc_info.c | 117 ++++++++-------------- psutil/arch/windows/security.c | 18 ++-- psutil/arch/windows/services.c | 31 ++---- psutil/arch/windows/socks.c | 1 + psutil/arch/windows/sys.c | 18 ++-- psutil/arch/windows/wmi.c | 13 +-- 46 files changed, 397 insertions(+), 819 deletions(-) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index ccfc15c889..de398e484b 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -6,27 +6,25 @@ * found in the LICENSE file. */ -/* - * AIX support is experimental at this time. - * The following functions and methods are unsupported on the AIX platform: - * - psutil.Process.memory_maps - * - * Known limitations: - * - psutil.Process.io_counters read count is always 0 - * - psutil.Process.io_counters may not be available on older AIX versions - * - psutil.Process.threads may not be available on older AIX versions - * - psutil.net_io_counters may not be available on older AIX versions - * - reading basic process info may fail or return incorrect values when - * process is starting (see IBM APAR IV58499 - fixed in newer AIX versions) - * - sockets and pipes may not be counted in num_fds (fixed in newer AIX - * versions) - * - * Useful resources: - * - proc filesystem: http://www-01.ibm.com/support/knowledgecenter/ - * ssw_aix_72/com.ibm.aix.files/proc.htm - * - libperfstat: http://www-01.ibm.com/support/knowledgecenter/ - * ssw_aix_72/com.ibm.aix.files/libperfstat.h.htm - */ +// AIX support is experimental at this time. +// The following functions and methods are unsupported on the AIX platform: +// - psutil.Process.memory_maps +// +// Known limitations: +// - psutil.Process.io_counters read count is always 0 +// - psutil.Process.io_counters may not be available on older AIX versions +// - psutil.Process.threads may not be available on older AIX versions +// - psutil.net_io_counters may not be available on older AIX versions +// - reading basic process info may fail or return incorrect values when +// process is starting (see IBM APAR IV58499 - fixed in newer AIX versions) +// - sockets and pipes may not be counted in num_fds (fixed in newer AIX +// versions) +// +// Useful resources: +// - proc filesystem: +// http://www-01.ibm.com/support/knowledgecenter/ssw_aix_72/com.ibm.aix.files/proc.htm +// - libperfstat: +// http://www-01.ibm.com/support/knowledgecenter/ssw_aix_72/com.ibm.aix.files/libperfstat.h.htm #include #include @@ -58,9 +56,7 @@ #define TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) -/* - * Read a file content and fills a C structure with it. - */ +// Read a file content and fills a C structure with it. int psutil_file_to_struct(char *path, void *fstruct, size_t size) { int fd; @@ -86,10 +82,8 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { } -/* - * Return process ppid, rss, vms, ctime, nice, nthreads, status and tty - * as a Python tuple. - */ +// Return process ppid, rss, vms, ctime, nice, nthreads, status and tty +// as a Python tuple. static PyObject * psutil_proc_oneshot(PyObject *self, PyObject *args) { int pid; @@ -135,9 +129,6 @@ psutil_proc_oneshot(PyObject *self, PyObject *args) { } -/* - * Return process name as a Python string. - */ static PyObject * psutil_proc_name(PyObject *self, PyObject *args) { int pid; @@ -155,9 +146,7 @@ psutil_proc_name(PyObject *self, PyObject *args) { } -/* - * Return process command line arguments as a Python list - */ +// Return process command line arguments as a Python list static PyObject * psutil_proc_args(PyObject *self, PyObject *args) { int pid; @@ -187,9 +176,9 @@ psutil_proc_args(PyObject *self, PyObject *args) { } curarg = argbuf; - /* getargs will always append an extra NULL to end the arg list, - * even if the buffer is not big enough (even though it is supposed - * to be) so the following 'while' is safe */ + // getargs will always append an extra NULL to end the arg list, + // even if the buffer is not big enough (even though it is supposed + // to be) so the following 'while' is safe while (*curarg != '\0') { if (!pylist_append_obj(py_retlist, PyUnicode_DecodeFSDefault(curarg))) goto error; @@ -208,9 +197,6 @@ psutil_proc_args(PyObject *self, PyObject *args) { } -/* - * Return process environment variables as a Python dict - */ static PyObject * psutil_proc_environ(PyObject *self, PyObject *args) { int pid; @@ -243,9 +229,9 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } curvar = envbuf; - /* getevars will always append an extra NULL to end the arg list, - * even if the buffer is not big enough (even though it is supposed - * to be) so the following 'while' is safe */ + // getevars will always append an extra NULL to end the arg list, + // even if the buffer is not big enough (even though it is supposed + // to be) so the following 'while' is safe while (*curvar != '\0') { separator = strchr(curvar, '='); if (separator != NULL) { @@ -280,11 +266,6 @@ psutil_proc_environ(PyObject *self, PyObject *args) { #ifdef CURR_VERSION_THREAD - -/* - * Retrieves all threads used by process returning a list of tuples - * including thread id, user time and system time. - */ static PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { long pid; @@ -298,14 +279,14 @@ psutil_proc_threads(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "l", &pid)) goto error; - /* Get the count of threads */ + // Get the count of threads thread_count = perfstat_thread(NULL, NULL, sizeof(perfstat_thread_t), 0); if (thread_count <= 0) { psutil_oserror(); goto error; } - /* Allocate enough memory */ + // Allocate enough memory threadt = (perfstat_thread_t *)calloc( thread_count, sizeof(perfstat_thread_t) ); @@ -347,12 +328,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { free(threadt); return NULL; } - -#endif +#endif // CURR_VERSION_THREAD #ifdef CURR_VERSION_PROCESS - static PyObject * psutil_proc_io_counters(PyObject *self, PyObject *args) { long pid; @@ -377,13 +356,9 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { procinfo.outBytes ); } - -#endif +#endif // CURR_VERSION_PROCESS -/* - * Return process user and system CPU times as a Python tuple. - */ static PyObject * psutil_proc_cpu_times(PyObject *self, PyObject *args) { int pid; @@ -407,9 +382,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { } -/* - * Return process uids/gids as a Python tuple. - */ +// Return process uids/gids as a Python tuple. static PyObject * psutil_proc_cred(PyObject *self, PyObject *args) { int pid; @@ -434,9 +407,6 @@ psutil_proc_cred(PyObject *self, PyObject *args) { } -/* - * Return process voluntary and involuntary context switches as a Python tuple. - */ static PyObject * psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { PyObject *py_tuple = NULL; @@ -453,30 +423,26 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { if (!processes) return NULL; - /* Loop through processes */ + // Loop through processes for (p = processes; np > 0; np--, p++) { pid = p->pi_pid; if (requested_pid != pid) continue; py_tuple = Py_BuildValue( "LL", - (long long)p->pi_ru.ru_nvcsw, /* voluntary context switches */ - (long long)p->pi_ru.ru_nivcsw - ); /* involuntary */ + (long long)p->pi_ru.ru_nvcsw, // voluntary + (long long)p->pi_ru.ru_nivcsw // involuntary + ); free(processes); return py_tuple; } - /* finished iteration without finding requested pid */ + // finished iteration without finding requested pid free(processes); return psutil_oserror_nsp("psutil_read_process_table (no PID found)"); } -/* - * Return disk mounted partitions as a list of tuples including device, - * mount point and filesystem type. - */ static PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { FILE *file = NULL; @@ -530,10 +496,6 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { #if defined(CURR_VERSION_NETINTERFACE) && CURR_VERSION_NETINTERFACE >= 3 - -/* - * Return a list of tuples for network I/O statistics. - */ static PyObject * psutil_net_io_counters(PyObject *self, PyObject *args) { perfstat_netinterface_t *statp = NULL; @@ -546,7 +508,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { if (py_retdict == NULL) return NULL; - /* check how many perfstat_netinterface_t structures are available */ + // check how many perfstat_netinterface_t structures are available tot = perfstat_netinterface( NULL, NULL, sizeof(perfstat_netinterface_t), 0 ); @@ -603,7 +565,6 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { Py_DECREF(py_retdict); return NULL; } - #endif @@ -682,9 +643,6 @@ psutil_boot_time(PyObject *self, PyObject *args) { } -/* - * Return a Python list of tuple representing per-cpu times - */ static PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { int ncpu, rc, i; @@ -696,21 +654,21 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { if (py_retlist == NULL) return NULL; - /* get the number of ticks per second */ + // get the number of ticks per second ticks = sysconf(_SC_CLK_TCK); if (ticks < 0) { psutil_oserror(); goto error; } - /* get the number of cpus in ncpu */ + // get the number of cpus in ncpu ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); if (ncpu <= 0) { psutil_oserror(); goto error; } - /* allocate enough memory to hold the ncpu structures */ + // allocate enough memory to hold the ncpu structures cpu = (perfstat_cpu_t *)malloc(ncpu * sizeof(perfstat_cpu_t)); if (cpu == NULL) { PyErr_NoMemory(); @@ -749,9 +707,6 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } -/* - * Return disk IO statistics. - */ static PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { PyObject *py_retdict = PyDict_New(); @@ -763,14 +718,14 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { if (py_retdict == NULL) return NULL; - /* Get the count of disks */ + // Get the count of disks disk_count = perfstat_disk(NULL, NULL, sizeof(perfstat_disk_t), 0); if (disk_count <= 0) { psutil_oserror(); goto error; } - /* Allocate enough memory */ + // Allocate enough memory diskt = (perfstat_disk_t *)calloc(disk_count, sizeof(perfstat_disk_t)); if (diskt == NULL) { PyErr_NoMemory(); @@ -812,9 +767,6 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { } -/* - * Return virtual memory usage statistics. - */ static PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { int rc; @@ -840,9 +792,6 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { } -/* - * Return stats about swap memory. - */ static PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { int rc; @@ -867,9 +816,6 @@ psutil_swap_mem(PyObject *self, PyObject *args) { } -/* - * Return CPU statistics. - */ static PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { int ncpu, rc, i; @@ -882,14 +828,14 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { u_longlong_t softintrs = 0; u_longlong_t syscall = 0; - /* get the number of cpus in ncpu */ + // get the number of cpus in ncpu ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); if (ncpu <= 0) { psutil_oserror(); goto error; } - /* allocate enough memory to hold the ncpu structures */ + // allocate enough memory to hold the ncpu structures cpu = (perfstat_cpu_t *)malloc(ncpu * sizeof(perfstat_cpu_t)); if (cpu == NULL) { PyErr_NoMemory(); @@ -922,9 +868,7 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } -/* - * define the psutil C module methods and initialize the module. - */ +// define the psutil C module methods and initialize the module. static PyMethodDef PsutilMethods[] = { // --- process-related functions {"proc_args", psutil_proc_args, METH_VARARGS}, diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index a370ba8db7..f26d524b36 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -6,18 +6,6 @@ * found in the LICENSE file. */ -/* - * Platform-specific module methods for FreeBSD and OpenBSD. - - * OpenBSD references: - * - OpenBSD source code: https://github.com/openbsd/src - * - * OpenBSD / NetBSD: missing APIs compared to FreeBSD implementation: - * - psutil.net_connections() - * - psutil.Process.get/set_cpu_affinity() (not supported natively) - * - psutil.Process.memory_maps() - */ - #include #include #include // BSD version @@ -35,9 +23,6 @@ #endif -/* - * define the psutil C module methods and initialize the module. - */ static PyMethodDef mod_methods[] = { // --- per-process functions diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 8d6ade90e5..76997e334f 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -4,8 +4,6 @@ * found in the LICENSE file. */ -// macOS platform-specific module methods. - #include #include // needed for old macOS versions #include diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 71291dc451..644ec52da1 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -4,16 +4,14 @@ * found in the LICENSE file. */ -/* - * Functions specific to Sun OS Solaris platforms. - * - * Thanks to Justin Venus who originally wrote a consistent part of - * this in Cython which I later on translated in C. - * - * Fix compilation issue on SunOS 5.10, see: - * https://github.com/giampaolo/psutil/issues/421 - * https://github.com/giampaolo/psutil/issues/1077 - */ +// Functions specific to Sun OS Solaris platforms. +// +// Thanks to Justin Venus who originally wrote a consistent part of +// this in Cython which I later on translated in C. +// +// Fix compilation issue on SunOS 5.10, see: +// https://github.com/giampaolo/psutil/issues/421 +// https://github.com/giampaolo/psutil/issues/1077 #define _STRUCTURED_PROC 1 #define NEW_MIB_COMPLIANT 1 @@ -118,9 +116,8 @@ PyInit__psutil_sunos(void) { if (PyModule_AddIntConstant(mod, "SWAIT", SWAIT)) return NULL; #else - /* sys/proc.h started defining SWAIT somewhere - * after Update 3 and prior to Update 5 included. - */ + // sys/proc.h started defining SWAIT somewhere + // after Update 3 and prior to Update 5 included. if (PyModule_AddIntConstant(mod, "SWAIT", 0)) return NULL; #endif diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index d57c287e10..069b56d752 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -4,17 +4,15 @@ * found in the LICENSE file. */ -/* - * Windows platform-specific module methods for _psutil_windows. - * - * List of undocumented Windows NT APIs which are used in here and in - * other modules: - * - NtQuerySystemInformation - * - NtQueryInformationProcess - * - NtQueryObject - * - NtSuspendProcess - * - NtResumeProcess - */ +// Windows module methods. +// +// List of undocumented Windows NT APIs which are used in here and in +// other modules: +// - NtQuerySystemInformation +// - NtQueryInformationProcess +// - NtQueryObject +// - NtSuspendProcess +// - NtResumeProcess #include #include diff --git a/psutil/arch/aix/common.c b/psutil/arch/aix/common.c index 5f8ed67317..5e28288eca 100644 --- a/psutil/arch/aix/common.c +++ b/psutil/arch/aix/common.c @@ -13,7 +13,7 @@ #include "common.h" -/* psutil_kread() - read from kernel memory */ +// Read from kernel memory. int psutil_kread( int Kd, // kernel memory file descriptor @@ -39,6 +39,7 @@ psutil_kread( return 0; } + struct procentry64 * psutil_read_process_table(int *num) { size_t msz; @@ -75,7 +76,7 @@ psutil_read_process_table(int *num) { p = (struct procentry64 *)((char *)processes + (np * PROCSIZE)); } - /* add the number of processes read in the last iteration */ + // add the number of processes read in the last iteration if (i > 0) np += i; diff --git a/psutil/arch/aix/ifaddrs.c b/psutil/arch/aix/ifaddrs.c index e3f0eba3b5..1a1a565f7f 100644 --- a/psutil/arch/aix/ifaddrs.c +++ b/psutil/arch/aix/ifaddrs.c @@ -5,9 +5,8 @@ * found in the LICENSE file. */ -/*! Based on code from - https://lists.samba.org/archive/samba-technical/2009-February/063079.html -!*/ +// Based on code from: +// https://lists.samba.org/archive/samba-technical/2009-February/063079.html #include #include @@ -56,8 +55,8 @@ getifaddrs(struct ifaddrs **ifap) { char *ccp, *ecp; struct ifconf ifc; struct ifreq *ifr; - struct ifaddrs *cifa = NULL; /* current */ - struct ifaddrs *pifa = NULL; /* previous */ + struct ifaddrs *cifa = NULL; // current + struct ifaddrs *pifa = NULL; // previous const size_t IFREQSZ = sizeof(struct ifreq); int fam; @@ -67,7 +66,7 @@ getifaddrs(struct ifaddrs **ifap) { if (sd == -1) goto error; - /* find how much memory to allocate for the SIOCGIFCONF call */ + // find how much memory to allocate for the SIOCGIFCONF call if (ioctl(sd, SIOCGSIZIFCONF, (caddr_t)&ifsize) < 0) goto error; @@ -116,7 +115,7 @@ getifaddrs(struct ifaddrs **ifap) { goto error; } - if (0 == ioctl(sd, SIOCGIFFLAGS, ifr)) /* optional */ + if (0 == ioctl(sd, SIOCGIFFLAGS, ifr)) // optional cifa->ifa_flags = ifr->ifr_flags; if (fam == AF_INET) { diff --git a/psutil/arch/aix/net_connections.c b/psutil/arch/aix/net_connections.c index 2ab139b600..6cc78f24c4 100644 --- a/psutil/arch/aix/net_connections.c +++ b/psutil/arch/aix/net_connections.c @@ -5,13 +5,12 @@ * found in the LICENSE file. */ -/* Baded on code from lsof: - * http://www.ibm.com/developerworks/aix/library/au-lsof.html - * - dialects/aix/dproc.c:gather_proc_info - * - lib/prfp.c:process_file - * - dialects/aix/dsock.c:process_socket - * - dialects/aix/dproc.c:get_kernel_access - */ +// Based on code from lsof: +// http://www.ibm.com/developerworks/aix/library/au-lsof.html +// - dialects/aix/dproc.c:gather_proc_info +// - lib/prfp.c:process_file +// - dialects/aix/dsock.c:process_socket +// - dialects/aix/dproc.c:get_kernel_access #include #include @@ -83,7 +82,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { char unix_laddr_str[PATH_MAX] = {0}; char unix_raddr_str[PATH_MAX] = {0}; - /* Read file structure */ + // Read file structure if (psutil_kread(Kd, fp, (char *)&f, sizeof(f))) { return NULL; } @@ -117,7 +116,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { fam = d.dom_family; if (fam == AF_INET || fam == AF_INET6) { - /* Read protocol control block */ + // Read protocol control block if (!s.so_pcb) { psutil_runtime_error("invalid socket PCB"); return NULL; @@ -127,7 +126,7 @@ process_file(int Kd, pid32_t pid, int fd, KA_T fp) { } if (p.pr_protocol == IPPROTO_TCP) { - /* If this is a TCP socket, read its control block */ + // If this is a TCP socket, read its control block if (inp.inp_ppcb && !psutil_kread( Kd, (KA_T)inp.inp_ppcb, (char *)&t, sizeof(t) @@ -259,7 +258,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { if (!processes) goto error; - /* Loop through processes */ + // Loop through processes for (p = processes; np > 0; np--, p++) { pid = p->pi_pid; if (requested_pid != -1 && requested_pid != pid) @@ -280,7 +279,7 @@ psutil_net_connections(PyObject *self, PyObject *args) { != 1) continue; - /* loop over file descriptors */ + // loop over file descriptors for (i = 0; i < p->pi_maxofile; i++) { fp = (KA_T)fds->pi_ufd[i].fp; if (fp) { diff --git a/psutil/arch/bsd/heap.c b/psutil/arch/bsd/heap.c index fe7fc27d44..334a613cb4 100644 --- a/psutil/arch/bsd/heap.c +++ b/psutil/arch/bsd/heap.c @@ -18,8 +18,8 @@ // Return low-level heap statistics from the C allocator. Return // jemalloc heap stats via `mallctl()`. Mimics Linux `mallinfo2()`: -// - heap_used ~ stats.allocated (like `uordblks`) -// - mmap_used ~ stats.mapped (like `hblkhd`) +// - heap_used ~ stats.allocated (like `uordblks`) +// - mmap_used ~ stats.mapped (like `hblkhd`) PyObject * psutil_heap_info(PyObject *self, PyObject *args) { uint64_t epoch = 0; diff --git a/psutil/arch/bsd/net.c b/psutil/arch/bsd/net.c index 0c2c46734d..efa2fc2520 100644 --- a/psutil/arch/bsd/net.c +++ b/psutil/arch/bsd/net.c @@ -52,8 +52,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { ifc_name[sdl->sdl_nlen] = '\0'; // XXX: ignore usbus interfaces: - // http://lists.freebsd.org/pipermail/freebsd-current/ - // 2011-October/028752.html + // http://lists.freebsd.org/pipermail/freebsd-current/2011-October/028752.html // 'ifconfig -a' doesn't show them, nor do we. if (strncmp(ifc_name, "usbus", 5) == 0) continue; diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c index b54779f24a..985daae4b7 100644 --- a/psutil/arch/bsd/proc.c +++ b/psutil/arch/bsd/proc.c @@ -19,10 +19,8 @@ #include "../../arch/all/init.h" -/* - * Collect different info about a process in one shot and return - * them as a Python dict. - */ +// Collect different info about a process in one shot and return them +// as a Python dict. PyObject * psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { pid_t pid; @@ -122,7 +120,6 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { // were. Not supported. oncpu = -1; #endif - // clang-format off #ifdef PSUTIL_FREEBSD @@ -350,12 +347,10 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } -/* - * Return files opened by process as a list of (path, fd) tuples. - * TODO: this is broken as it may report empty paths. 'procstat' - * utility has the same problem see: - * https://github.com/giampaolo/psutil/issues/595 - */ +// Return files opened by process as a list of (path, fd) tuples. +// TODO: this is broken as it may report empty paths. 'procstat' +// utility has the same problem see: +// https://github.com/giampaolo/psutil/issues/595 PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { pid_t pid; diff --git a/psutil/arch/freebsd/cpu.c b/psutil/arch/freebsd/cpu.c index 987d23b369..78e54aa846 100644 --- a/psutil/arch/freebsd/cpu.c +++ b/psutil/arch/freebsd/cpu.c @@ -4,17 +4,14 @@ * found in the LICENSE file. */ -/* -System-wide CPU related functions. -Original code was refactored and moved from psutil/arch/freebsd/specific.c -in 2020 (and was moved in there previously already) from cset. -a4c0a0eb0d2a872ab7a45e47fcf37ef1fde5b012 -For reference, here's the git history with original(ish) implementations: -- CPU stats: fb0154ef164d0e5942ac85102ab660b8d2938fbb -- CPU freq: 459556dd1e2979cdee22177339ced0761caf4c83 -- CPU cores: e0d6d7865df84dc9a1d123ae452fd311f79b1dde -*/ - +// System-wide CPU related functions. +// Original code was refactored and moved from psutil/arch/freebsd/specific.c +// in 2020 (and was moved in there previously already) from cset +// a4c0a0eb0d2a872ab7a45e47fcf37ef1fde5b012. +// For reference, here's the git history with original(ish) implementations: +// - CPU stats: fb0154ef164d0e5942ac85102ab660b8d2938fbb +// - CPU freq: 459556dd1e2979cdee22177339ced0761caf4c83 +// - CPU cores: e0d6d7865df84dc9a1d123ae452fd311f79b1dde #include #include @@ -121,11 +118,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } -/* - * Return frequency information of a given CPU. - * As of Dec 2018 only CPU 0 appears to be supported and all other - * cores match the frequency of CPU 0. - */ +// Return frequency information of a given CPU. As of Dec 2018 only CPU +// 0 appears to be supported and all other cores match the frequency of +// CPU 0. PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { int current; diff --git a/psutil/arch/freebsd/proc.c b/psutil/arch/freebsd/proc.c index 731891fdbb..236f4a329b 100644 --- a/psutil/arch/freebsd/proc.c +++ b/psutil/arch/freebsd/proc.c @@ -13,11 +13,6 @@ #include "../../arch/all/init.h" -// ============================================================================ -// Utility functions -// ============================================================================ - - // remove spaces from string static void psutil_remove_spaces(char *str) { @@ -30,14 +25,6 @@ psutil_remove_spaces(char *str) { } -// ============================================================================ -// APIS -// ============================================================================ - -/* - * Borrowed from psi Python System Information project - * Based on code from ps. - */ PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; @@ -83,11 +70,9 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { return NULL; } -/* - * Return process pathname executable. - * Thanks to Robert N. M. Watson: - * http://fxr.googlebit.com/source/usr.bin/procstat/procstat_bin.c?v=8-CURRENT - */ + +// Return process pathname executable. Thanks to Robert N. M. Watson: +// http://fxr.googlebit.com/source/usr.bin/procstat/procstat_bin.c?v=8-CURRENT PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { pid_t pid; @@ -150,8 +135,7 @@ psutil_proc_threads(PyObject *self, PyObject *args) { // Retrieves all threads used by process returning a list of tuples // including thread id, user time and system time. // Thanks to Robert N. M. Watson: - // http://code.metager.de/source/xref/freebsd/usr.bin/procstat/ - // procstat_threads.c + // http://code.metager.de/source/xref/freebsd/usr.bin/procstat/procstat_threads.c pid_t pid; int mib[4]; struct kinfo_proc *kip = NULL; @@ -411,11 +395,10 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { } +// Get process CPU affinity. Reference: +// http://sources.freebsd.org/RELENG_9/src/usr.bin/cpuset/cpuset.c PyObject * psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { - // Get process CPU affinity. - // Reference: - // http://sources.freebsd.org/RELENG_9/src/usr.bin/cpuset/cpuset.c pid_t pid; int ret; int i; @@ -449,11 +432,10 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { } +// Set process CPU affinity. Reference: +// http://sources.freebsd.org/RELENG_9/src/usr.bin/cpuset/cpuset.c PyObject * psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { - // Set process CPU affinity. - // Reference: - // http://sources.freebsd.org/RELENG_9/src/usr.bin/cpuset/cpuset.c pid_t pid; int i; int seq_len; @@ -499,9 +481,7 @@ psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { } -/* - * An emulation of Linux prlimit(). Returns a (soft, hard) tuple. - */ +// An emulation of Linux prlimit(). Returns a (soft, hard) tuple. PyObject * psutil_proc_getrlimit(PyObject *self, PyObject *args) { pid_t pid; @@ -531,9 +511,7 @@ psutil_proc_getrlimit(PyObject *self, PyObject *args) { } -/* - * An emulation of Linux prlimit() (set). - */ +// An emulation of Linux prlimit() (set). PyObject * psutil_proc_setrlimit(PyObject *self, PyObject *args) { pid_t pid; diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index 4b5dd5ff29..cb554eedbd 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -3,10 +3,10 @@ * All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. - * - * Retrieves per-process open socket connections. */ +// Retrieves per-process open socket connections. + #include #include #include diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c index 698b50bf41..8259f88e3b 100644 --- a/psutil/arch/freebsd/sensors.c +++ b/psutil/arch/freebsd/sensors.c @@ -4,14 +4,11 @@ * found in the LICENSE file. */ -/* -Original code was refactored and moved from psutil/arch/freebsd/specific.c -For reference, here's the git history with original(ish) implementations: -- sensors_battery(): 022cf0a05d34f4274269d4f8002ee95b9f3e32d2 -- sensors_cpu_temperature(): bb5d032be76980a9e110f03f1203bd35fa85a793 - (patch by Alex Manuskin) -*/ - +// Original code was refactored and moved from psutil/arch/freebsd/specific.c +// For reference, here's the git history with original(ish) implementations: +// - sensors_battery(): 022cf0a05d34f4274269d4f8002ee95b9f3e32d2 +// - sensors_cpu_temperature(): bb5d032be76980a9e110f03f1203bd35fa85a793 +// (patch by Alex Manuskin) #include #include diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index 99c8d61860..69e0875308 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -3,12 +3,12 @@ * All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. - * - * Retrieves system-wide open socket connections. This is based off of - * sockstat utility source code: - * https://github.com/freebsd/freebsd/blob/master/usr.bin/sockstat/sockstat.c */ +// Retrieves system-wide open socket connections. This is based off of +// sockstat utility source code: +// https://github.com/freebsd/freebsd/blob/master/usr.bin/sockstat/sockstat.c + #include #include #include diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c index 94eb85517f..200fda037c 100644 --- a/psutil/arch/netbsd/cpu.c +++ b/psutil/arch/netbsd/cpu.c @@ -12,15 +12,12 @@ #include "../../arch/all/init.h" - -/* -CPU related functions. Original code was refactored and moved from -psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously -already) from cset 84219ad. For reference, here's the git history with -original(ish) implementations: -- per CPU times: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) -- CPU stats: a991494e4502e1235ebc62b5ba450287d0dedec0 (Jan 2016) -*/ +// CPU related functions. Original code was refactored and moved from +// psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously +// already) from cset 84219ad. For reference, here's the git history with +// original(ish) implementations: +// - per CPU times: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) +// - CPU stats: a991494e4502e1235ebc62b5ba450287d0dedec0 (Jan 2016) PyObject * diff --git a/psutil/arch/netbsd/disk.c b/psutil/arch/netbsd/disk.c index 421be47f7a..7f817248cb 100644 --- a/psutil/arch/netbsd/disk.c +++ b/psutil/arch/netbsd/disk.c @@ -5,13 +5,11 @@ * found in the LICENSE file. */ -/* -Disk related functions. Original code was refactored and moved from -psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously -already) from cset 84219ad. For reference, here's the git history with -original(ish) implementations: -- disk IO counters: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) -*/ +// Disk related functions. Original code was refactored and moved from +// psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously +// already) from cset 84219ad. For reference, here's the git history with +// original(ish) implementations: +// - disk IO counters: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) #include #include diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c index dfeb7bac70..375fad6c5b 100644 --- a/psutil/arch/netbsd/mem.c +++ b/psutil/arch/netbsd/mem.c @@ -5,14 +5,12 @@ * found in the LICENSE file. */ -/* -Memory related functions. Original code was refactored and moved from -psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously -already) from cset 84219ad. For reference, here's the git history with -original(ish) implementations: -- virtual memory: 0749a69c01b374ca3e2180aaafc3c95e3b2d91b9 (Oct 2016) -- swap memory: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) -*/ +// Memory related functions. Original code was refactored and moved from +// psutil/arch/netbsd/specific.c in 2023 (and was moved in there previously +// already) from cset 84219ad. For reference, here's the git history with +// original(ish) implementations: +// - virtual memory: 0749a69c01b374ca3e2180aaafc3c95e3b2d91b9 (Oct 2016) +// - swap memory: 312442ad2a5b5d0c608476c5ab3e267735c3bc59 (Jan 2016) #include #include diff --git a/psutil/arch/netbsd/proc.c b/psutil/arch/netbsd/proc.c index 40f6355e23..5913c2cf5d 100644 --- a/psutil/arch/netbsd/proc.c +++ b/psutil/arch/netbsd/proc.c @@ -3,8 +3,6 @@ * All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. - * - * Platform-specific module methods for NetBSD. */ #include diff --git a/psutil/arch/openbsd/mem.c b/psutil/arch/openbsd/mem.c index 7ef2acaef7..5cf5e3f120 100644 --- a/psutil/arch/openbsd/mem.c +++ b/psutil/arch/openbsd/mem.c @@ -59,8 +59,7 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { // * 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 + // * https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/freebsd/memory.c#L143 avail = inactive + cached + free; used = active + wired + cached; percent = psutil_usage_percent((double)(total - avail), (double)total, 1); diff --git a/psutil/arch/openbsd/proc.c b/psutil/arch/openbsd/proc.c index 65bfe50308..bbdce4f067 100644 --- a/psutil/arch/openbsd/proc.c +++ b/psutil/arch/openbsd/proc.c @@ -169,8 +169,7 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { // Reference: - // https://github.com/openbsd/src/blob/ - // 588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L191 + // https://github.com/openbsd/src/blob/588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L191 pid_t pid; struct kinfo_proc kp; char path[MAXPATHLEN]; diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c index b152f2f19c..7bfa7153a9 100644 --- a/psutil/arch/osx/cpu.c +++ b/psutil/arch/osx/cpu.c @@ -4,19 +4,17 @@ * found in the LICENSE file. */ -/* -System-wide CPU related functions. - -Original code was refactored and moved from psutil/_psutil_osx.c in 2020 -right before a4c0a0eb0d2a872ab7a45e47fcf37ef1fde5b012. -For reference, here's the git history with original implementations: - -- CPU count logical: 3d291d425b856077e65163e43244050fb188def1 -- CPU count physical: 4263e354bb4984334bc44adf5dd2f32013d69fba -- CPU times: 32488bdf54aed0f8cef90d639c1667ffaa3c31c7 -- CPU stat: fa00dfb961ef63426c7818899340866ced8d2418 -- CPU frequency: 6ba1ac4ebfcd8c95fca324b15606ab0ec1412d39 -*/ +// System-wide CPU related functions. +// +// Original code was refactored and moved from psutil/_psutil_osx.c in 2020, +// right before a4c0a0eb0d2a872ab7a45e47fcf37ef1fde5b012. +// +// For reference, here's the git history with original implementations: +// - CPU count logical: 3d291d425b856077e65163e43244050fb188def1 +// - CPU count physical: 4263e354bb4984334bc44adf5dd2f32013d69fba +// - CPU times: 32488bdf54aed0f8cef90d639c1667ffaa3c31c7 +// - CPU stat: fa00dfb961ef63426c7818899340866ced8d2418 +// - CPU frequency: 6ba1ac4ebfcd8c95fca324b15606ab0ec1412d39 #include #include diff --git a/psutil/arch/osx/disk.c b/psutil/arch/osx/disk.c index 0775890007..fe1ff3b2dc 100644 --- a/psutil/arch/osx/disk.c +++ b/psutil/arch/osx/disk.c @@ -21,10 +21,6 @@ #include "../../arch/all/init.h" -/* - * Return a list of tuples including device, mount point and fs type - * for all partitions mounted on the system. - */ PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { int num; @@ -183,7 +179,7 @@ psutil_disk_usage_used(PyObject *self, PyObject *args) { } #ifdef ATTR_VOL_SPACEUSED - /* Call getattrlist(ATTR_VOL_SPACEUSED) to get used space info. */ + // Call getattrlist(ATTR_VOL_SPACEUSED) to get used space info. int ret; struct { uint32_t size; @@ -212,9 +208,6 @@ psutil_disk_usage_used(PyObject *self, PyObject *args) { } -/* - * Return a Python dict of tuples for disk I/O information - */ PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { CFDictionaryRef parent_dict = NULL; diff --git a/psutil/arch/osx/heap.c b/psutil/arch/osx/heap.c index 90c45cad02..0ab2a8c9ec 100644 --- a/psutil/arch/osx/heap.c +++ b/psutil/arch/osx/heap.c @@ -59,8 +59,8 @@ get_zones(malloc_zone_t ***out_zones, unsigned int *out_count) { // Compatible with macOS 10.6+ (Sierra and earlier). // // Mapping: -// - heap_used ~ size_in_use (live allocated bytes) -// - mmap_used ~ 0 (no direct stat) +// - heap_used ~ size_in_use (live allocated bytes) +// - mmap_used ~ 0 (no direct stat) PyObject * psutil_heap_info(PyObject *self, PyObject *args) { malloc_zone_t **zones = NULL; diff --git a/psutil/arch/osx/mem.c b/psutil/arch/osx/mem.c index a75f79aa01..e7edbdaf03 100644 --- a/psutil/arch/osx/mem.c +++ b/psutil/arch/osx/mem.c @@ -7,7 +7,6 @@ // System memory related functions. Original code was refactored and moved // from psutil/_psutil_osx.c in 2023. This is the GIT blame before the move: // https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_osx.c - // See: // https://github.com/apple-open-source/macos/blob/master/system_cmds/vm_stat/vm_stat.c @@ -46,11 +45,8 @@ psutil_sys_vminfo(vm_statistics64_t vmstat) { } -/* - * Return system virtual memory stats. - * See: - * https://opensource.apple.com/source/system_cmds/system_cmds-790/vm_stat.tproj/vm_stat.c.auto.html - */ +// Return system virtual memory stats. See: +// https://opensource.apple.com/source/system_cmds/system_cmds-790/vm_stat.tproj/vm_stat.c.auto.html PyObject * psutil_virtual_mem(PyObject *self, PyObject *args) { uint64_t total; @@ -103,9 +99,6 @@ psutil_virtual_mem(PyObject *self, PyObject *args) { } -/* - * Return stats about swap memory. - */ PyObject * psutil_swap_mem(PyObject *self, PyObject *args) { struct xsw_usage totals; diff --git a/psutil/arch/osx/proc.c b/psutil/arch/osx/proc.c index c6b59a6b4c..9620d6c429 100644 --- a/psutil/arch/osx/proc.c +++ b/psutil/arch/osx/proc.c @@ -60,14 +60,11 @@ convert_status(struct extern_proc *p, struct eproc *e) { } -/* - * Return multiple process info as a Python dict in one shot by - * using sysctl() and filling up a kinfo_proc struct. - * It should be possible to do this for all processes without - * incurring into permission (EPERM) errors. - * This will also succeed for zombie processes returning correct - * information. - */ +// Return multiple process info as a Python dict in one shot by using +// `sysctl()` and filling up a `kinfo_proc` struct. It should be +// possible to do this for all processes without incurring into +// permission (EPERM) errors. This will also succeed for zombie +// processes returning correct information. PyObject * psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { pid_t pid; @@ -118,14 +115,12 @@ psutil_proc_oneshot_kinfo(PyObject *self, PyObject *args) { } -/* - * Return multiple process info as a Python dict in one shot by - * using proc_pidinfo(PROC_PIDTASKINFO) and filling a proc_taskinfo - * struct. - * Contrarily from proc_kinfo above this function will fail with - * EACCES for PIDs owned by another user and with ESRCH for zombie - * processes. - */ +// Return multiple process info as a Python dict in one shot by +// using `proc_pidinfo(PROC_PIDTASKINFO)` and filling a proc_taskinfo +// struct. +// Contrarily from `proc_kinfo` above this function will fail with +// EACCES for PIDs owned by another user and with ESRCH for zombie +// processes. PyObject * psutil_proc_oneshot_pidtaskinfo(PyObject *self, PyObject *args) { pid_t pid; @@ -175,9 +170,6 @@ psutil_proc_oneshot_pidtaskinfo(PyObject *self, PyObject *args) { } -/* - * Return process name from kinfo_proc as a Python string. - */ PyObject * psutil_proc_name(PyObject *self, PyObject *args) { pid_t pid; @@ -191,10 +183,7 @@ psutil_proc_name(PyObject *self, PyObject *args) { return PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); } -/* - * Return process current working directory. - * Raises NSP in case of zombie process. - */ + PyObject * psutil_proc_cwd(PyObject *self, PyObject *args) { pid_t pid; @@ -215,9 +204,6 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { } -/* - * Return path of the process executable. - */ PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { pid_t pid; @@ -249,10 +235,8 @@ psutil_proc_exe(PyObject *self, PyObject *args) { } -/* - * Indicates if the given virtual address on the given architecture is in the - * shared VM region. - */ +// Return true if the given virtual address on the given architecture +// is in the shared VM region. static bool psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { mach_vm_address_t base; @@ -279,9 +263,6 @@ psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { } -/* - * Return extended memory info via task_info(TASK_VM_INFO). - */ PyObject * psutil_proc_memory_info_ex(PyObject *self, PyObject *args) { pid_t pid; @@ -333,11 +314,8 @@ psutil_proc_memory_info_ex(PyObject *self, PyObject *args) { } -/* - * Returns the USS (unique set size) of the process. Reference: - * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ - * nsMemoryReporterManager.cpp - */ +// Return process USS (unique set size) memory. Reference: +// https://dxr.mozilla.org/mozilla-central/source/xpcom/base/nsMemoryReporterManager.cpp PyObject * psutil_proc_memory_uss(PyObject *self, PyObject *args) { pid_t pid; @@ -439,9 +417,6 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { } -/* - * Return process threads - */ PyObject * psutil_proc_threads(PyObject *self, PyObject *args) { pid_t pid; @@ -556,12 +531,10 @@ psutil_proc_threads(PyObject *self, PyObject *args) { } -/* - * Return process open files as a Python tuple. - * See lsof source code: - * https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 - * ...and /usr/include/sys/proc_info.h - */ +// Return process open files as a Python tuple. +// See lsof source code: +// https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 +// ...and /usr/include/sys/proc_info.h PyObject * psutil_proc_open_files(PyObject *self, PyObject *args) { pid_t pid; @@ -644,13 +617,10 @@ psutil_proc_open_files(PyObject *self, PyObject *args) { } -/* - * Return process TCP and UDP connections as a list of tuples. - * Raises NSP in case of zombie process. - * See lsof source code: - * https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 - * ...and /usr/include/sys/proc_info.h - */ +// Return process TCP and UDP connections as a list of tuples. +// See lsof source code: +// https://github.com/apple-opensource/lsof/blob/28/lsof/dialects/darwin/libproc/dproc.c#L342 +// ...and /usr/include/sys/proc_info.h PyObject * psutil_proc_net_connections(PyObject *self, PyObject *args) { pid_t pid; @@ -907,7 +877,6 @@ psutil_proc_num_fds(PyObject *self, PyObject *args) { } -// return process args as a python list PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { pid_t pid; diff --git a/psutil/arch/osx/proc_utils.c b/psutil/arch/osx/proc_utils.c index 16673e4dbb..cd4bf1157c 100644 --- a/psutil/arch/osx/proc_utils.c +++ b/psutil/arch/osx/proc_utils.c @@ -110,11 +110,9 @@ psutil_sysctl_procargs(pid_t pid, char *procargs, size_t *argmax) { } -/* - * A wrapper around proc_pidinfo(). - * https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c - * Returns 0 on failure. - */ +// A wrapper around proc_pidinfo(). +// https://opensource.apple.com/source/xnu/xnu-2050.7.9/bsd/kern/proc_info.c +// Returns 0 on failure. int psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { int ret; @@ -141,20 +139,18 @@ psutil_proc_pidinfo(pid_t pid, int flavor, uint64_t arg, void *pti, int size) { } -/* - * A wrapper around task_for_pid() which sucks big time: - * - it's not documented - * - errno is set only sometimes - * - sometimes errno is ENOENT (?!?) - * - for PIDs != getpid() or PIDs which are not members of the procmod - * it requires root - * As such we can only guess what the heck went wrong and fail either - * with NoSuchProcess or give up with AccessDenied. - * References: - * https://github.com/giampaolo/psutil/issues/1181 - * https://github.com/giampaolo/psutil/issues/1209 - * https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 - */ +// A wrapper around task_for_pid() which sucks big time: +// - it's not documented +// - errno is set only sometimes +// - sometimes errno is ENOENT (?!?) +// - for PIDs != getpid() or PIDs which are not members of the procmod +// it requires root +// +// As such we can only guess what the heck went wrong and fail either +// with NoSuchProcess or give up with AccessDenied. References: +// - https://github.com/giampaolo/psutil/issues/1181 +// - https://github.com/giampaolo/psutil/issues/1209 +// - https://github.com/giampaolo/psutil/issues/1291#issuecomment-396062519 int psutil_task_for_pid(pid_t pid, mach_port_t *task) { kern_return_t err; @@ -190,10 +186,8 @@ psutil_task_for_pid(pid_t pid, mach_port_t *task) { } -/* - * A wrapper around proc_pidinfo(PROC_PIDLISTFDS), which dynamically sets - * the buffer size. - */ +// A wrapper around proc_pidinfo(PROC_PIDLISTFDS), which dynamically sets +// the buffer size. struct proc_fdinfo * psutil_proc_list_fds(pid_t pid, int *num_fds) { int ret; diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index f012b2cab5..3ab97016f2 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -89,10 +89,10 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { time_to_empty_ref, kCFNumberIntType, &time_to_empty )) { - /* This value is recommended for non-Apple power sources, so it's not - * an error if it doesn't exist. We'll return -1 for "unknown" */ - /* A value of -1 indicates "Still Calculating the Time" also for - * apple power source */ + // This value is recommended for non-Apple power sources, so + // it's not an error if it doesn't exist. We'll return -1 for + // "unknown" A value of -1 indicates "Still Calculating the + // Time" also for apple power source. time_to_empty = -1; } @@ -104,7 +104,7 @@ psutil_sensors_battery(PyObject *self, PyObject *args) { CFRelease(power_info); CFRelease(power_sources_list); - /* Caller should NOT release power_sources_information */ + // Caller should NOT release power_sources_information return py_tuple; error: diff --git a/psutil/arch/posix/init.c b/psutil/arch/posix/init.c index 503ae49659..fa2a9ec994 100644 --- a/psutil/arch/posix/init.c +++ b/psutil/arch/posix/init.c @@ -12,17 +12,14 @@ PyObject *ZombieProcessError = NULL; -/* - * From "man getpagesize" on Linux, https://linux.die.net/man/2/getpagesize: - * - * > In SUSv2 the getpagesize() call is labeled LEGACY, and in POSIX.1-2001 - * > it has been dropped. - * > Portable applications should employ sysconf(_SC_PAGESIZE) instead - * > of getpagesize(). - * > Most systems allow the synonym _SC_PAGE_SIZE for _SC_PAGESIZE. - * > Whether getpagesize() is present as a Linux system call depends on the - * > architecture. - */ +// "man getpagesize" says: +// +// In SUSv2 the getpagesize() call is labeled LEGACY, and in +// POSIX.1-2001 it has been dropped. Portable applications should +// employ sysconf(_SC_PAGESIZE) instead of getpagesize(). Most systems +// allow the synonym _SC_PAGE_SIZE for _SC_PAGESIZE. Whether +// getpagesize() is present as a Linux system call depends on the +// architecture. long psutil_getpagesize(void) { #ifdef _SC_PAGESIZE diff --git a/psutil/arch/posix/net.c b/psutil/arch/posix/net.c index b117f5578a..49df43535e 100644 --- a/psutil/arch/posix/net.c +++ b/psutil/arch/posix/net.c @@ -46,10 +46,8 @@ #include "../../arch/all/init.h" -/* - * Translate a sockaddr struct into a Python string. - * Return None if address family is not AF_INET* or AF_PACKET. - */ +// Translate a sockaddr struct into a Python string. +// Return None if address family is not AF_INET* or AF_PACKET. PyObject * psutil_convert_ipaddr(struct sockaddr *addr, int family) { char buf[NI_MAXHOST]; @@ -119,10 +117,8 @@ psutil_convert_ipaddr(struct sockaddr *addr, int family) { } -/* - * Return NICs information a-la ifconfig as a list of tuples. - * TODO: on Solaris we won't get any MAC address. - */ +// Return NICs information a-la ifconfig as a list of tuples. +// TODO: on Solaris we won't get any MAC address. PyObject * psutil_net_if_addrs(PyObject *self, PyObject *args) { struct ifaddrs *ifaddr, *ifa; @@ -231,10 +227,8 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { } -/* - * Return NIC MTU. References: - * http://www.i-scream.org/libstatgrab/ - */ +// Return NIC MTU. References: +// http://www.i-scream.org/libstatgrab/ PyObject * psutil_net_if_mtu(PyObject *self, PyObject *args) { char *nic_name; @@ -268,9 +262,7 @@ append_flag(PyObject *py_retlist, const char *flag_name) { return pylist_append_obj(py_retlist, PyUnicode_FromString(flag_name)); } -/* - * Get all of the NIC flags and return them. - */ +// Get all of the NIC flags and return them. PyObject * psutil_net_if_flags(PyObject *self, PyObject *args) { char *nic_name; @@ -456,11 +448,9 @@ psutil_net_if_flags(PyObject *self, PyObject *args) { } -/* - * Inspect NIC flags, returns a bool indicating whether the NIC is - * running. References: - * http://www.i-scream.org/libstatgrab/ - */ +// Inspect NIC flags, returns a bool indicating whether the NIC is +// running. References: +// http://www.i-scream.org/libstatgrab/ PyObject * psutil_net_if_is_running(PyObject *self, PyObject *args) { char *nic_name; @@ -635,11 +625,9 @@ psutil_get_nic_speed(int ifm_active) { } -/* - * Return stats about a particular network interface. - * References: - * http://www.i-scream.org/libstatgrab/ - */ +// Return stats about a particular network interface. +// References: +// http://www.i-scream.org/libstatgrab/ PyObject * psutil_net_if_duplex_speed(PyObject *self, PyObject *args) { char *nic_name; diff --git a/psutil/arch/sunos/environ.c b/psutil/arch/sunos/environ.c index d3b0c42239..6e87ae1895 100644 --- a/psutil/arch/sunos/environ.c +++ b/psutil/arch/sunos/environ.c @@ -2,8 +2,6 @@ * Copyright (c) 2009, Giampaolo Rodola', Oleksii Shevchuk. * All rights reserved. Use of this source code is governed by a BSD-style * license that can be found in the LICENSE file. - * - * Functions specific for Process.environ(). */ #define _STRUCTURED_PROC 1 @@ -22,12 +20,7 @@ #define STRING_SEARCH_BUF_SIZE 512 -/* - * Open address space of specified process and return file descriptor. - * @param pid a pid of process. - * @param procfs_path a path to mounted procfs filesystem. - * @return file descriptor or -1 in case of error. - */ +// Open address space of specified process and return file descriptor. static int open_address_space(pid_t pid, const char *procfs_path) { int fd; @@ -42,15 +35,8 @@ open_address_space(pid_t pid, const char *procfs_path) { } -/* - * Read chunk of data by offset to specified buffer of the same size. - * @param fd a file descriptor. - * @param offset an required offset in file. - * @param buf a buffer where to store result. - * @param buf_size a size of buffer where data will be stored. - * @return amount of bytes stored to the buffer or -1 in case of - * error. - */ +// Read chunk of data by offset to specified buffer of the same size. +// Return the amount of bytes stored to the buffer or -1 on error. static size_t read_offt(int fd, off_t offset, char *buf, size_t buf_size) { size_t to_read = buf_size; @@ -76,13 +62,9 @@ read_offt(int fd, off_t offset, char *buf, size_t buf_size) { } -/* - * Read null-terminated string from file descriptor starting from - * specified offset. - * @param fd a file descriptor of opened address space. - * @param offset an offset in specified file descriptor. - * @return allocated null-terminated string or NULL in case of error. - */ +// Read null-terminated string from file descriptor starting from +// specified offset. Return allocated null-terminated string or NULL in +// case of error. static char * read_cstring_offt(int fd, off_t offset) { int r; @@ -141,17 +123,11 @@ read_cstring_offt(int fd, off_t offset) { } -/* - * Read block of addresses by offset, dereference them one by one - * and create an array of null terminated C strings from them. - * @param fd a file descriptor of address space of interesting process. - * @param offset an offset of address block in address space. - * @param ptr_size a size of pointer. Only 4 or 8 are valid values. - * @param count amount of pointers in block. - * @return allocated array of strings dereferenced and read by offset. - * Number of elements in array are count. In case of error function - * returns NULL. - */ +// Read block of addresses by offset, dereference them one by one and +// create an array of null terminated C strings from them. Return +// allocated array of strings dereferenced and read by offset. Number +// of elements in array are count. In case of error function returns +// NULL. static char ** read_cstrings_block(int fd, off_t offset, size_t ptr_size, size_t count) { char **result = NULL; @@ -203,14 +179,9 @@ read_cstrings_block(int fd, off_t offset, size_t ptr_size, size_t count) { } -/* - * Check that caller process can extract proper values from psinfo_t - * structure. - * @param info a pointer to process info (psinfo_t) structure of the - * interesting process. - * @return 1 in case if caller process can extract proper values from - * psinfo_t structure, or 0 otherwise. - */ +// Check that caller process can extract proper values from psinfo_t +// structure. Return 1 in case if caller process can extract proper +// values from psinfo_t structure, 0 otherwise. static inline int is_ptr_dereference_possible(psinfo_t info) { #if !defined(_LP64) @@ -221,25 +192,16 @@ is_ptr_dereference_possible(psinfo_t info) { } -/* - * Return pointer size according to psinfo_t structure - * @param info a pointer to process info (psinfo_t) structure of the - * interesting process. - * @return pointer size (4 or 8). - */ +// Return pointer size according to psinfo_t structure. Return the +// pointer size (4 or 8). static inline int ptr_size_by_psinfo(psinfo_t info) { return info.pr_dmodel == PR_MODEL_ILP32 ? 4 : 8; } -/* - * Count amount of pointers in a block which ends with NULL. - * @param fd a descriptor of /proc/PID/as special file. - * @param offt an offset of block of pointers at the file. - * @param ptr_size a pointer size (allowed values: {4, 8}). - * @return amount of non-NULL pointers or -1 in case of error. - */ +// Count amount of pointers in a block which ends with NULL. Return the +// amount of non-NULL pointers or -1 on error. static int search_pointers_vector_size_offt(int fd, off_t offt, size_t ptr_size) { int count = 0; @@ -278,17 +240,9 @@ search_pointers_vector_size_offt(int fd, off_t offt, size_t ptr_size) { } -/* - * Dereference and read array of strings by psinfo_t.pr_argv pointer from - * remote process. - * @param info a pointer to process info (psinfo_t) structure of the - * interesting process - * @param procfs_path a cstring with path to mounted procfs filesystem. - * @param count a pointer to variable where to store amount of elements in - * returned array. In case of error value of variable will not be - changed. - * @return allocated array of cstrings or NULL in case of error. - */ +// Dereference and read array of strings by psinfo_t.pr_argv pointer +// from remote process. Return allocated array of cstrings or NULL on +// error. char ** psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count) { int as; @@ -325,24 +279,24 @@ psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count) { } -/* - * Dereference and read array of strings by psinfo_t.pr_envp pointer - * from remote process. - * @param info a pointer to process info (psinfo_t) structure of the - * interesting process. - * @param procfs_path a cstring with path to mounted procfs filesystem. - * @param count a pointer to variable where to store amount of elements in - * returned array. In case of error value of variable will not be - * changed. To detect special case (described later) variable should be - * initialized by -1 or other negative value. - * @return allocated array of cstrings or NULL in case of error. - * Special case: count set to 0, return NULL. - * Special case means there is no error acquired, but no data - * retrieved. - * Special case exists because the nature of the process. From the - * beginning it's not clean how many pointers in envp array. Also - * situation when environment is empty is common for kernel processes. - */ +// Dereference and read array of strings by psinfo_t.pr_envp pointer +// from remote process. +// +// @param info: a pointer to process info (psinfo_t) structure of the +// interesting process. +// @param procfs_path: a cstring with path to mounted procfs filesystem. +// @param count: a pointer to variable where to store amount of elements in +// returned array. In case of error value of variable will not be +// changed. To detect special case (described later) variable should be +// initialized by -1 or other negative value. +// +// Return allocated array of cstrings or NULL in case of error. +// Special case: count set to 0, return NULL. +// Special case means there is no error acquired, but no data +// retrieved. +// Special case exists because the nature of the process. From the +// beginning it's not clear how many pointers in envp array. Also +// situation when environment is empty is common for kernel processes. char ** psutil_read_raw_env(psinfo_t info, const char *procfs_path, ssize_t *count) { int as; @@ -377,12 +331,7 @@ psutil_read_raw_env(psinfo_t info, const char *procfs_path, ssize_t *count) { } -/* - * Free array of cstrings. - * @param array an array of cstrings returned by psutil_read_raw_env, - * psutil_read_raw_args or any other function. - * @param count a count of strings in the passed array - */ +// Free array of cstrings. void psutil_free_cstrings_array(char **array, size_t count) { size_t i; diff --git a/psutil/arch/sunos/net.c b/psutil/arch/sunos/net.c index 8033b80eea..73fd3f4190 100644 --- a/psutil/arch/sunos/net.c +++ b/psutil/arch/sunos/net.c @@ -247,17 +247,10 @@ psutil_net_if_stats(PyObject *self, PyObject *args) { } -/* - * Return TCP and UDP connections opened by process. - * UNIX sockets are excluded. - * - * Thanks to: - * https://github.com/DavidGriffith/finx/blob/master/ - * nxsensor-3.5.0-1/src/sysdeps/solaris.c - * ...and: - * https://hg.java.net/hg/solaris~on-src/file/tip/usr/src/cmd/ - * cmd-inet/usr.bin/netstat/netstat.c - */ +// Return TCP and UDP connections opened by process. UNIX sockets are excluded. +// Thanks to: +// https://github.com/DavidGriffith/finx/blob/master/nxsensor-3.5.0-1/src/sysdeps/solaris.c +// https://hg.java.net/hg/solaris~on-src/file/tip/usr/src/cmd/cmd-inet/usr.bin/netstat/netstat.c PyObject * psutil_net_connections(PyObject *self, PyObject *args) { long pid; diff --git a/psutil/arch/sunos/proc.c b/psutil/arch/sunos/proc.c index a52bac0b89..3d6b3b697f 100644 --- a/psutil/arch/sunos/proc.c +++ b/psutil/arch/sunos/proc.c @@ -38,10 +38,6 @@ psutil_file_to_struct(char *path, void *fstruct, size_t size) { } -/* - * Return process ppid, rss, vms, ctime, nice, nthreads, status and tty - * as a Python tuple. - */ PyObject * psutil_proc_oneshot(PyObject *self, PyObject *args) { int pid; @@ -73,9 +69,7 @@ psutil_proc_oneshot(PyObject *self, PyObject *args) { } -/* - * Return process name and args as a Python tuple. - */ +// Return process name and args as a Python tuple. PyObject * psutil_proc_name_and_args(PyObject *self, PyObject *args) { int pid; @@ -129,8 +123,8 @@ psutil_proc_name_and_args(PyObject *self, PyObject *args) { psutil_free_cstrings_array(argv, argc); } - /* If we can't read process memory or can't decode the result - * then return args from /proc. */ + // If we can't read process memory or can't decode the result + // then return args from /proc. if (!py_args_list) { PyErr_Clear(); py_args_str = PyUnicode_DecodeFSDefault(info.pr_psargs); @@ -170,9 +164,6 @@ psutil_proc_name_and_args(PyObject *self, PyObject *args) { } -/* - * Return process environ block. - */ PyObject * psutil_proc_environ(PyObject *self, PyObject *args) { int pid; @@ -245,9 +236,6 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } -/* - * Return process user and system CPU times as a Python tuple. - */ PyObject * psutil_proc_cpu_times(PyObject *self, PyObject *args) { int pid; @@ -271,9 +259,7 @@ psutil_proc_cpu_times(PyObject *self, PyObject *args) { } -/* - * Return what CPU the process is running on. - */ +// Return what CPU the process is running on. PyObject * psutil_proc_cpu_num(PyObject *self, PyObject *args) { int fd = -1; @@ -342,9 +328,7 @@ psutil_proc_cpu_num(PyObject *self, PyObject *args) { } -/* - * Return process uids/gids as a Python tuple. - */ +// Return process uids/gids as a Python tuple. PyObject * psutil_proc_cred(PyObject *self, PyObject *args) { int pid; @@ -369,9 +353,6 @@ psutil_proc_cred(PyObject *self, PyObject *args) { } -/* - * Return process voluntary and involuntary context switches as a Python tuple. - */ PyObject * psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { int pid; @@ -388,9 +369,6 @@ psutil_proc_num_ctx_switches(PyObject *self, PyObject *args) { } -/* - * Return process page faults as a (minor, major) tuple. - */ PyObject * psutil_proc_page_faults(PyObject *self, PyObject *args) { int pid; @@ -445,9 +423,7 @@ proc_io_counters(PyObject* self, PyObject* args) { */ -/* - * Return information about a given process thread. - */ +// Return information about a given process thread. PyObject * psutil_proc_query_thread(PyObject *self, PyObject *args) { int pid, tid; @@ -468,9 +444,6 @@ psutil_proc_query_thread(PyObject *self, PyObject *args) { } -/* - * Return process memory mappings. - */ PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { int pid; diff --git a/psutil/arch/sunos/sys.c b/psutil/arch/sunos/sys.c index e0d147debd..d3afb12418 100644 --- a/psutil/arch/sunos/sys.c +++ b/psutil/arch/sunos/sys.c @@ -27,7 +27,7 @@ psutil_boot_time(PyObject *self, PyObject *args) { endutxent(); UTXENT_MUTEX_UNLOCK(); if (fabs(boot_time) < 0.000001) { - /* could not find BOOT_TIME in getutxent loop */ + // could not find BOOT_TIME in getutxent loop psutil_runtime_error("can't determine boot time"); return NULL; } diff --git a/psutil/arch/windows/cpu.c b/psutil/arch/windows/cpu.c index a700644d02..9727882cc6 100644 --- a/psutil/arch/windows/cpu.c +++ b/psutil/arch/windows/cpu.c @@ -11,10 +11,8 @@ #include "../../arch/all/init.h" -/* - * Return the number of logical, active CPUs. Return 0 if undetermined. - * See discussion at: https://bugs.python.org/issue33166#msg314631 - */ +// Return the number of logical, active CPUs. Return 0 if undetermined. +// See discussion at: https://bugs.python.org/issue33166#msg314631 static unsigned int psutil_get_num_cpus(int fail_on_err) { unsigned int ncpus = 0; @@ -40,11 +38,8 @@ psutil_get_num_cpus(int fail_on_err) { } -/* - * Retrieves system CPU timing information as a (user, system, idle) - * tuple. On a multiprocessor system, the values returned are the - * sum of the designated times across all processors. - */ +// Retrieves system CPU timing information as a (user, system, idle) +// tuple. The values returned are the sum of times across all CPUs. PyObject * psutil_cpu_times(PyObject *self, PyObject *args) { double idle, kernel, user, system; @@ -70,9 +65,7 @@ psutil_cpu_times(PyObject *self, PyObject *args) { } -/* - * Same as above but for all system CPUs. - */ +// Same as above but for all CPUs. PyObject * psutil_per_cpu_times(PyObject *self, PyObject *args) { double idle, kernel, systemt, user, interrupt, dpc; @@ -151,9 +144,7 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { } -/* - * Return the number of active, logical CPUs. - */ +// Return the number of active, logical CPUs. PyObject * psutil_cpu_count_logical(PyObject *self, PyObject *args) { unsigned int ncpus; @@ -166,9 +157,7 @@ psutil_cpu_count_logical(PyObject *self, PyObject *args) { } -/* - * Return the number of CPU cores (non hyper-threading). - */ +// Return the number of CPU cores (non hyper-threading). PyObject * psutil_cpu_count_cores(PyObject *self, PyObject *args) { DWORD rc; @@ -249,9 +238,6 @@ psutil_cpu_count_cores(PyObject *self, PyObject *args) { } -/* - * Return CPU statistics. - */ PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { NTSTATUS status; @@ -364,9 +350,6 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } -/* - * Return CPU frequency. - */ PyObject * psutil_cpu_freq(PyObject *self, PyObject *args) { PROCESSOR_POWER_INFORMATION *ppi; diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index d4654b4add..825aa373d3 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -38,7 +38,6 @@ psutil_get_drive_type(int type) { } -// Return path's disk total, used, and free space. PyObject * psutil_disk_usage(PyObject *self, PyObject *args) { PyObject *py_path; @@ -73,10 +72,6 @@ psutil_disk_usage(PyObject *self, PyObject *args) { } -/* - * Return a Python dict of tuples for disk I/O information. This may - * require running "diskperf -y" command first. - */ PyObject * psutil_disk_io_counters(PyObject *self, PyObject *args) { DISK_PERFORMANCE diskPerformance; @@ -138,8 +133,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { } else if (GetLastError() == ERROR_INVALID_FUNCTION) { // This happens on AppVeyor: - // https://ci.appveyor.com/project/giampaolo/psutil/build/ - // 1364/job/ascpdi271b06jle3 + // https://ci.appveyor.com/project/giampaolo/psutil/build/1364/job/ascpdi271b06jle3 // Assume it means we're dealing with some exotic disk // and go on. psutil_debug( @@ -159,9 +153,7 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { goto next; } // XXX: it seems we should also catch ERROR_INVALID_PARAMETER: - // https://sites.ualberta.ca/dept/aict/uts/software/openbsd/ - // ports/4.1/i386/openafs/w-openafs-1.4.14-transarc/ - // openafs-1.4.14/src/usd/usd_nt.c + // https://sites.ualberta.ca/dept/aict/uts/software/openbsd/ports/4.1/i386/openafs/w-openafs-1.4.14-transarc/openafs-1.4.14/src/usd/usd_nt.c // XXX: we can also bump into ERROR_MORE_DATA in which case // (quoting doc) we're supposed to retry with a bigger buffer @@ -203,10 +195,6 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { } -/* - * Return disk partitions as a list of tuples such as - * (drive_letter, drive_letter, type, "") - */ PyObject * psutil_disk_partitions(PyObject *self, PyObject *args) { DWORD num_bytes; @@ -371,11 +359,9 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } -/* - Accept a filename's drive in native format like "\Device\HarddiskVolume1\" - and return the corresponding drive letter (e.g. "C:\\"). - If no match is found return an empty string. -*/ +// Accept a filename's drive in native format like "\Device\HarddiskVolume1\" +// and return the corresponding drive letter (e.g. "C:\\"). +// If no match is found return an empty string. PyObject * psutil_QueryDosDevice(PyObject *self, PyObject *args) { LPCTSTR lpDevicePath; diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c index 2b2716736a..25da443502 100644 --- a/psutil/arch/windows/net.c +++ b/psutil/arch/windows/net.c @@ -126,9 +126,7 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { } -/* - * Return NICs addresses. - */ +// Return NICs addresses. PyObject * psutil_net_if_addrs(PyObject *self, PyObject *args) { unsigned int i = 0; @@ -334,10 +332,8 @@ psutil_net_if_addrs(PyObject *self, PyObject *args) { } -/* - * Provides stats about NIC interfaces installed on the system. - * TODO: get 'duplex' (currently it's hard coded to '2', aka 'full duplex') - */ +// Provides stats about NIC interfaces installed on the system. +// TODO: get 'duplex' (currently it's hard coded to '2', aka 'full duplex') PyObject * psutil_net_if_stats(PyObject *self, PyObject *args) { int i; diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index f48f317bff..2bb26d2292 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -31,9 +31,7 @@ PyObject *TimeoutExpired; PyObject *TimeoutAbandoned; -/* - * Return 1 if PID exists in the current process list, else 0. - */ +// Return 1 if PID exists in the current process list, else 0. PyObject * psutil_pid_exists(PyObject *self, PyObject *args) { DWORD pid; @@ -49,9 +47,6 @@ psutil_pid_exists(PyObject *self, PyObject *args) { } -/* - * Kill a process given its PID. - */ PyObject * psutil_proc_kill(PyObject *self, PyObject *args) { HANDLE hProcess; @@ -83,9 +78,6 @@ psutil_proc_kill(PyObject *self, PyObject *args) { } -/* - * Wait for process to terminate and return its exit code. - */ PyObject * psutil_proc_wait(PyObject *self, PyObject *args) { HANDLE hProcess; @@ -156,9 +148,7 @@ psutil_proc_wait(PyObject *self, PyObject *args) { } -/* - * Return a Python tuple (user_time, kernel_time) - */ +// Return (user_time, kernel_time, create_time). PyObject * psutil_proc_times(PyObject *self, PyObject *args) { DWORD pid; @@ -206,12 +196,10 @@ psutil_proc_times(PyObject *self, PyObject *args) { } -/* - * Return process executable path. Works for all processes regardless of - * privilege. NtQuerySystemInformation has some sort of internal cache, - * since it succeeds even when a process is gone (but not if a PID never - * existed). - */ +// Return process executable path. Works for all processes regardless +// of privilege. NtQuerySystemInformation has some sort of internal +// cache, since it succeeds even when a process is gone (but not if a +// PID never existed). PyObject * psutil_proc_exe(PyObject *self, PyObject *args) { DWORD pid; @@ -319,9 +307,6 @@ psutil_proc_exe(PyObject *self, PyObject *args) { } -/* - * Return process memory information as a Python dict. - */ PyObject * psutil_proc_memory_info(PyObject *self, PyObject *args) { HANDLE hProcess; @@ -429,12 +414,9 @@ psutil_GetProcWsetInformation( } -/* - * Returns the USS of the process. - * Reference: - * https://dxr.mozilla.org/mozilla-central/source/xpcom/base/ - * nsMemoryReporterManager.cpp - */ +// Returns the USS of the process. +// Reference: +// https://dxr.mozilla.org/mozilla-central/source/xpcom/base/nsMemoryReporterManager.cpp PyObject * psutil_proc_memory_uss(PyObject *self, PyObject *args) { DWORD pid; @@ -483,9 +465,7 @@ psutil_proc_memory_uss(PyObject *self, PyObject *args) { } -/* - * Resume or suspends a process - */ +// Resume or suspends a process. PyObject * psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { DWORD pid; @@ -695,9 +675,7 @@ _psutil_user_token_from_pid(DWORD pid) { } -/* - * Return process username as a "DOMAIN//USERNAME" string. - */ +// Return process username as a "DOMAIN//USERNAME" string. PyObject * psutil_proc_username(PyObject *self, PyObject *args) { DWORD pid; @@ -796,9 +774,7 @@ psutil_proc_username(PyObject *self, PyObject *args) { } -/* - * Get process priority as a Python integer. - */ +// Get process priority as a Python integer. PyObject * psutil_proc_priority_get(PyObject *self, PyObject *args) { DWORD pid; @@ -823,9 +799,7 @@ psutil_proc_priority_get(PyObject *self, PyObject *args) { } -/* - * Set process priority. - */ +// Set process priority. PyObject * psutil_proc_priority_set(PyObject *self, PyObject *args) { DWORD pid; @@ -852,9 +826,6 @@ psutil_proc_priority_set(PyObject *self, PyObject *args) { } -/* - * Get process IO priority as a Python integer. - */ PyObject * psutil_proc_io_priority_get(PyObject *self, PyObject *args) { DWORD pid; @@ -880,9 +851,6 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { } -/* - * Set process IO priority. - */ PyObject * psutil_proc_io_priority_set(PyObject *self, PyObject *args) { DWORD pid; @@ -909,10 +877,6 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { } -/* - * Return a Python tuple referencing process I/O counters. - */ -PyObject * psutil_proc_io_counters(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; @@ -943,9 +907,7 @@ psutil_proc_io_counters(PyObject *self, PyObject *args) { } -/* - * Return process CPU affinity as a bitmask - */ +// Return process CPU affinity as a bitmask. PyObject * psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { DWORD pid; @@ -970,9 +932,6 @@ psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { } -/* - * Set process CPU affinity - */ PyObject * psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { DWORD pid; @@ -1024,9 +983,7 @@ psutil_proc_page_faults(PyObject *self, PyObject *args) { } -/* - * Return True if all process threads are in waiting/suspended state. - */ +// Return True if all process threads are in waiting/suspended state. PyObject * psutil_proc_is_suspended(PyObject *self, PyObject *args) { DWORD pid; @@ -1051,9 +1008,6 @@ psutil_proc_is_suspended(PyObject *self, PyObject *args) { } -/* - * Return the number of handles opened by process. - */ PyObject * psutil_proc_num_handles(PyObject *self, PyObject *args) { DWORD pid; @@ -1100,9 +1054,6 @@ get_region_protection_string(ULONG protection) { } -/* - * Return a list of process's memory mappings. - */ PyObject * psutil_proc_memory_maps(PyObject *self, PyObject *args) { MEMORY_BASIC_INFORMATION basicInfo; @@ -1170,9 +1121,7 @@ psutil_proc_memory_maps(PyObject *self, PyObject *args) { } -/* - * Return a {pid:ppid, ...} dict for all running processes. - */ +// Return a {pid:ppid, ...} dict for all running processes. PyObject * psutil_ppid_map(PyObject *self, PyObject *args) { PyObject *py_pid = NULL; diff --git a/psutil/arch/windows/proc_handles.c b/psutil/arch/windows/proc_handles.c index eabe70bf4d..e18c741f1b 100644 --- a/psutil/arch/windows/proc_handles.c +++ b/psutil/arch/windows/proc_handles.c @@ -4,20 +4,20 @@ * found in the LICENSE file. */ -/* - * This module retrieves handles opened by a process. - * We use NtQuerySystemInformation to enumerate them and NtQueryObject - * to obtain the corresponding file name. - * Since NtQueryObject hangs for certain handle types we call it in a - * separate thread which gets killed if it doesn't complete within 100ms. - * This is a limitation of the Windows API and ProcessHacker uses the - * same trick: https://github.com/giampaolo/psutil/pull/597 - * - * CREDITS: original implementation was written by Jeff Tang. - * It was then rewritten by Giampaolo Rodola many years later. - * Utility functions for getting the file handles and names were re-adapted - * from the excellent ProcessHacker. - */ +// This module retrieves handles opened by a process. +// +// We use NtQuerySystemInformation to enumerate them and NtQueryObject +// to obtain the corresponding file name. +// +// Since NtQueryObject hangs for certain handle types we call it in a +// separate thread which gets killed if it doesn't complete within +// 100ms. This is a limitation of the Windows API and ProcessHacker +// uses the same trick: https://github.com/giampaolo/psutil/pull/597 +// +// CREDITS: original implementation was written by Jeff Tang. It was +// then rewritten by Giampaolo Rodola many years later. Utility +// functions for getting the file handles and names were re-adapted +// from the excellent ProcessHacker. #include #include @@ -26,6 +26,7 @@ #define THREAD_TIMEOUT 100 // ms + // Global object shared between the 2 threads. PUNICODE_STRING globalFileName = NULL; diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c index e50ad8803b..c5906f4550 100644 --- a/psutil/arch/windows/proc_info.c +++ b/psutil/arch/windows/proc_info.c @@ -2,11 +2,10 @@ * Copyright (c) 2009, Jay Loden, 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. - * - * Helper functions related to fetching process information. Used by - * _psutil_windows module methods. */ +// Helper functions related to fetching process information. + #include #include @@ -35,10 +34,8 @@ typedef NTSTATUS(NTAPI *__NtQueryInformationProcess)( : NULL) -/* - * Given a pointer into a process's memory, figure out how much - * data can be read from it. - */ +// Given a pointer into a process's memory, figure out how much data +// can be read from it. static int psutil_get_process_region_size(HANDLE hProcess, LPCVOID src, SIZE_T *psize) { MEMORY_BASIC_INFORMATION info; @@ -104,13 +101,11 @@ psutil_giveup_with_ad(NTSTATUS status, char *syscall) { } -/* - * Get data from the process with the given pid. The data is returned - * in the pdata output member as a nul terminated string which must be - * freed on success. - * On success 0 is returned. On error the output parameter is not touched, - * -1 is returned, and an appropriate Python exception is set. - */ +// Get data from the process with the given pid. The data is returned +// in the pdata output member as a nul terminated string which must be +// freed on success. On success 0 is returned. On error the output +// parameter is not touched, -1 is returned, and an appropriate Python +// exception is set. static int psutil_get_process_data( DWORD pid, enum psutil_process_data_kind kind, WCHAR **pdata, SIZE_T *psize @@ -160,8 +155,8 @@ psutil_get_process_data( return -1; #ifdef _WIN64 - /* 64 bit case. Check if the target is a 32 bit process running in WoW64 - * mode. */ + // 64 bit case. Check if the target is a 32 bit process running in + // WoW64 mode. status = NtQueryInformationProcess( hProcess, ProcessWow64Information, &ppeb32, sizeof(LPVOID), NULL ); @@ -174,7 +169,7 @@ psutil_get_process_data( } if (ppeb32 != NULL) { - /* We are 64 bit. Target process is 32 bit running in WoW64 mode. */ + // We are 64 bit. Target process is 32 bit running in WoW64 mode. PEB32 peb32; RTL_USER_PROCESS_PARAMETERS32 procParameters32; @@ -238,7 +233,7 @@ psutil_get_process_data( } if (weAreWow64 && !theyAreWow64) { - /* We are 32 bit running in WoW64 mode. Target process is 64 bit. */ + // We are 32 bit running in WoW64 mode. Target process is 64 bit. PROCESS_BASIC_INFORMATION64 pbi64; PEB64 peb64; RTL_USER_PROCESS_PARAMETERS64 procParameters64; @@ -272,11 +267,9 @@ psutil_get_process_data( hProcess, ProcessBasicInformation, &pbi64, sizeof(pbi64), NULL ); if (!NT_SUCCESS(status)) { - /* - psutil_convert_ntstatus_err( - status, - "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); - */ + // psutil_convert_ntstatus_err( + // status, + // "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); psutil_giveup_with_ad( status, "NtWow64QueryInformationProcess64(ProcessBasicInformation)" @@ -289,10 +282,8 @@ psutil_get_process_data( hProcess, pbi64.PebBaseAddress, &peb64, sizeof(peb64), NULL ); if (!NT_SUCCESS(status)) { - /* - psutil_convert_ntstatus_err( - status, "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)"); - */ + // psutil_convert_ntstatus_err( + // status, "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)"); psutil_giveup_with_ad( status, "NtWow64ReadVirtualMemory64(pbi64.PebBaseAddress)" ); @@ -308,10 +299,8 @@ psutil_get_process_data( NULL ); if (!NT_SUCCESS(status)) { - /* - psutil_convert_ntstatus_err( - status, "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)"); - */ + // psutil_convert_ntstatus_err( + // status, "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)"); psutil_giveup_with_ad( status, "NtWow64ReadVirtualMemory64(peb64.ProcessParameters)" ); @@ -334,7 +323,7 @@ psutil_get_process_data( } else #endif - /* Target process is of the same bitness as us. */ + // Target process is of the same bitness as us. { PROCESS_BASIC_INFORMATION pbi; PEB_ peb; @@ -449,11 +438,9 @@ psutil_get_process_data( } -/* - * Get process cmdline by using NtQueryInformationProcess. This is a - * method alternative to PEB which is less likely to result in - * AccessDenied. Requires Windows 8.1+. - */ +// Get process cmdline by using NtQueryInformationProcess. This is a +// method alternative to PEB which is less likely to result in +// AccessDenied. Requires Windows 8.1+. static int psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { HANDLE hProcess = NULL; @@ -541,10 +528,6 @@ psutil_cmdline_query_proc(DWORD pid, WCHAR **pdata, SIZE_T *psize) { } -/* - * Return a Python list representing the arguments for the process - * with given pid or NULL on error. - */ PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { WCHAR *data = NULL; @@ -580,15 +563,13 @@ psutil_proc_cmdline(PyObject *self, PyObject *args, PyObject *kwdict) { use_peb = (py_usepeb == Py_True) ? 1 : 0; - /* - Reading the PEB to get the cmdline seem to be the best method if - somebody has tampered with the parameters after creating the process. - For instance, create a process as suspended, patch the command line - in its PEB and unfreeze it. It requires more privileges than - NtQueryInformationProcess though (the fallback): - - https://github.com/giampaolo/psutil/pull/1398 - - https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/ - */ + // Reading the PEB to get the cmdline seem to be the best method if + // somebody has tampered with the parameters after creating the + // process. For instance, create a process as suspended, patch the + // command line in its PEB and unfreeze it. It requires more + // privileges than NtQueryInformationProcess though (the fallback): + // - https://github.com/giampaolo/psutil/pull/1398 + // - https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/ if (use_peb == 1) func_ret = psutil_get_process_data(pid, KIND_CMDLINE, &data, &size); else @@ -664,10 +645,6 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { } -/* - * returns a Python string containing the environment variable data for the - * process with given pid or NULL on error. - */ PyObject * psutil_proc_environ(PyObject *self, PyObject *args) { DWORD pid; @@ -700,15 +677,12 @@ psutil_proc_environ(PyObject *self, PyObject *args) { } -/* - * Given a process PID and a PSYSTEM_PROCESS_INFORMATION structure - * fills the structure with various process information in one shot - * by using NtQuerySystemInformation. - * We use this as a fallback when faster functions fail with access - * denied. This is slower because it iterates over all processes - * but it doesn't require any privilege (also work for PID 0). - * On success return 0, else -1 with Python exception already set. - */ +// Given a PID and a PSYSTEM_PROCESS_INFORMATION struct, fills it with +// various process information by using NtQuerySystemInformation. We +// use this as a fallback when faster functions fail with access +// denied. This is slower because it iterates over all processes but it +// doesn't require any privilege (also work for PID 0). Return 0 on +// success, else -1 with Python exception set. int psutil_get_proc_info( DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, PVOID *retBuffer @@ -774,20 +748,9 @@ psutil_get_proc_info( } -/* - * Get various process information by using NtQuerySystemInformation. - * We use this as a fallback when faster functions fail with access - * denied. This is slower because it iterates over all processes. - * Returned dict includes the following process info: - * - * - num_threads() - * - ctx_switches() - * - num_handles() (fallback) - * - cpu_times() (fallback) - * - create_time() (fallback) - * - io_counters() (fallback) - * - memory_info() (fallback) - */ +// Get various process info by using NtQuerySystemInformation. We use +// this as a fallback when faster functions fail with access denied. +// This is slower because it iterates over all processes. PyObject * psutil_proc_oneshot(PyObject *self, PyObject *args) { DWORD pid; diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index b00483747b..019c41ad34 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -2,11 +2,11 @@ * Copyright (c) 2009, Jay Loden, 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. - * - * Security related functions for Windows platform (Set privileges such as - * SE DEBUG). */ +// Security related functions for Windows platform (Set privileges such +// as SE DEBUG). + #include #include @@ -107,13 +107,11 @@ psutil_print_err() { } -/* - * Set this process in SE DEBUG mode so that we have more chances of - * querying processes owned by other users, including many owned by - * Administrator and Local System. - * https://docs.microsoft.com/windows-hardware/drivers/debugger/debug-privilege - * This is executed on module import and we don't crash on error. - */ +// Set this process in SE DEBUG mode so that we have more chances of +// querying processes owned by other users, including many owned by +// Administrator and Local System. +// https://docs.microsoft.com/windows-hardware/drivers/debugger/debug-privilege +// This is executed on module import and we don't crash on error. int psutil_set_se_debug() { HANDLE hToken; diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 6306775675..9944c13fdb 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -122,9 +122,7 @@ get_state_string(DWORD state) { // APIs // ================================================================== -/* - * Enumerate all services. - */ +// Enumerate all services. PyObject * psutil_winservice_enumerate(PyObject *self, PyObject *args) { ENUM_SERVICE_STATUS_PROCESSW *lpService = NULL; @@ -209,13 +207,8 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { } -/* - * Get service config information. Returns: - * - display_name - * - binpath - * - username - * - startup_type - */ +// Get service config information. Returns: +// (display_name, binpath, username, startup_type) PyObject * psutil_winservice_query_config(PyObject *self, PyObject *args) { wchar_t *service_name = NULL; @@ -311,11 +304,7 @@ psutil_winservice_query_config(PyObject *self, PyObject *args) { } -/* - * Get service status information. Returns: - * - status - * - pid - */ +// Get service status information. Returns (status, pid) PyObject * psutil_winservice_query_status(PyObject *self, PyObject *args) { wchar_t *service_name = NULL; @@ -472,10 +461,8 @@ psutil_winservice_query_descr(PyObject *self, PyObject *args) { } -/* - * Start service. - * XXX - note: this is exposed but not used. - */ +// Start service. +// XXX - note: this is exposed but not used. PyObject * psutil_winservice_start(PyObject *self, PyObject *args) { BOOL ok; @@ -507,10 +494,8 @@ psutil_winservice_start(PyObject *self, PyObject *args) { } -/* - * Stop service. - * XXX - note: this is exposed but not used. - */ +// Stop service. +// XXX - note: this is exposed but not used. PyObject * psutil_winservice_stop(PyObject *self, PyObject *args) { wchar_t *service_name = NULL; diff --git a/psutil/arch/windows/socks.c b/psutil/arch/windows/socks.c index 29e67aa406..8ed25a2801 100644 --- a/psutil/arch/windows/socks.c +++ b/psutil/arch/windows/socks.c @@ -17,6 +17,7 @@ #define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) #define STATUS_UNSUCCESSFUL 0xC0000001 + // Note about GetExtended[Tcp|Udp]Table syscalls: due to other processes // being active on the machine, it's possible that the size of the table // increases between the moment we query the size and the moment we query diff --git a/psutil/arch/windows/sys.c b/psutil/arch/windows/sys.c index 161e94bd5e..aa991aad3e 100644 --- a/psutil/arch/windows/sys.c +++ b/psutil/arch/windows/sys.c @@ -4,16 +4,14 @@ * found in the LICENSE file. */ -/* -System related functions. Original code moved in here from -psutil/_psutil_windows.c in 2023. For reference, here's the GIT blame -history before the move: - -- boot_time(): - https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L51-L60 -- users(): - https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L1103-L1244 -*/ +// System related functions. Original code moved in here from +// psutil/_psutil_windows.c in 2023. For reference, here's the GIT +// blame history before the move: +// +// - boot_time(): +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L51-L60 +// - users(): +// https://github.com/giampaolo/psutil/blame/efd7ed3/psutil/_psutil_windows.c#L1103-L1244 #include #include diff --git a/psutil/arch/windows/wmi.c b/psutil/arch/windows/wmi.c index c98db34130..043a367597 100644 --- a/psutil/arch/windows/wmi.c +++ b/psutil/arch/windows/wmi.c @@ -2,10 +2,10 @@ * 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. - * - * Functions related to the Windows Management Instrumentation API. */ +// Functions related to the Windows Management Instrumentation API. + #include #include #include @@ -123,12 +123,9 @@ psutil_init_loadavg_counter(PyObject *self, PyObject *args) { } -/* - * Gets the emulated 1 minute, 5 minute and 15 minute load averages - * (processor queue length) for the system. - * `init_loadavg_counter` must be called before this function to engage the - * mechanism that records load values. - */ +// Emulated 1, 5, 15 minutes getloadavg() (processor queue length). +// `init_loadavg_counter()` must be called first to engage the +// mechanism that records load values. PyObject * psutil_get_loadavg(PyObject *self, PyObject *args) { MUTEX_LOCK(&mutex); From 16b4a8c64fd4df0025b800cf0cb2ab2d1f78525e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Apr 2026 19:04:04 +0200 Subject: [PATCH 1673/1714] Doc: cross-ref glossary terms, improve clarity Refactor documentation for better readability by: - Use RST :term: roles consistently for glossary cross-references - Restructure glossary entries for clarity (e.g., expand swap memory, add thrashing entry, clarify memory types) - Add memory and disk I/O recipes with real-time examples --- docs/_static/css/custom.css | 4 + docs/api.rst | 298 ++++++++++++++++++------------------ docs/faq.rst | 16 +- docs/glossary.rst | 278 +++++++++++++++++---------------- docs/recipes.rst | 58 +++++++ 5 files changed, 366 insertions(+), 288 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 2f5bce9388..5bc40b0d1f 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -677,6 +677,10 @@ div.seealso { border-left-color: var(--adm-seealso-accent-border) !important; } +div.admonition a { + text-decoration: none !important; +} + div.seealso a.reference { color: var(--adm-seealso-accent-border) !important; text-decoration: underline; diff --git a/docs/api.rst b/docs/api.rst index 585614f44d..a0cd2ee46b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -65,8 +65,8 @@ CPU - **dpc** *(Windows)*: time spent servicing deferred procedure calls (DPCs); DPCs are interrupts that run at a lower priority than standard interrupts. - When *percpu* is ``True`` return a list of named tuples for each logical CPU - on the system. + When *percpu* is ``True`` return a list of named tuples for each + :term:`logical CPU` on the system. The list is ordered by CPU index. The order of the list is consistent across calls. Example output on Linux: @@ -84,11 +84,11 @@ CPU `#1210 `_. .. versionchanged:: 4.1.0 - added **irq** and **dpc** fields on Windows (**irq** was called **interrupt** + Windows: added **irq** and **dpc** fields (**irq** was called **interrupt** before 8.0.0). .. versionchanged:: 8.0.0 - **interrupt** field on Windows was renamed to **irq**; **interrupt** still + Windows: **interrupt** field was renamed to **irq**; **interrupt** still works but raises :exc:`DeprecationWarning`. .. versionchanged:: 8.0.0 @@ -149,8 +149,8 @@ CPU .. seealso:: :ref:`faq_cpu_percent` .. versionchanged:: 4.1.0 - two new **irq** and **dpc** fields are returned on Windows (**irq** was - called **interrupt** before 8.0.0). + Windows: added **irq** and **dpc** fields (**irq** was called + **interrupt** before 8.0.0). .. versionchanged:: 5.9.6 function is now thread safe. @@ -161,8 +161,9 @@ CPU (similar to :func:`os.cpu_count`) or ``None`` if undetermined. Unlike :func:`os.cpu_count`, this is not influenced by the ``PYTHON_CPU_COUNT`` environment variable introduced in Python 3.13. - "logical CPUs" means the number of physical cores multiplied by the number - of threads that can run on each core (this is known as Hyper Threading). + :term:`Logical CPUs ` means the number of + :term:`physical CPUs ` multiplied by the number of threads + that can run on each core (this is known as Hyper Threading). This is what cloud providers often refer to as vCPUs. If *logical* is ``False`` return the number of physical cores only, or @@ -264,8 +265,8 @@ CPU it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. The numbers returned only make sense when compared to the number of CPU cores installed on the system. So, for instance, a value of `3.14` on a system - with 10 logical CPUs means that the system load was 31.4% percent over the - last N minutes. + with 10 :term:`logical CPUs ` means that the system load was + 31.4% percent over the last N minutes. .. code-block:: pycon @@ -288,12 +289,12 @@ Memory Return statistics about system memory usage as a named tuple including the following fields, expressed in bytes. - - **total**: total physical memory (exclusive swap). + - **total**: total physical memory (exclusive :term:`swap memory`). - **available**: memory that can be given instantly to processes without the - system going into swap. On Linux it uses the ``MemAvailable`` field from - ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back to an - estimate. This is the recommended field for monitoring actual memory usage - in a cross-platform fashion. See :term:`available memory`. + system going into :term:`swap memory`. On Linux it uses the ``MemAvailable`` + field from ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back + to an estimate. This is the recommended field for monitoring actual memory + usage in a cross-platform fashion. See :term:`available memory`. - **percent**: the percentage usage calculated as ``(total - available) / total * 100``. - **used**: memory in use, calculated differently depending on the platform @@ -316,10 +317,10 @@ Memory - **cached** *(Linux, BSD, Windows)*: RAM used by the kernel to cache file contents (data read from or written to disk). Reclaimable by the OS when needed. See :term:`page cache`. - - **shared** *(Linux, BSD)*: memory accessible by multiple processes - simultaneously, such as in-memory ``tmpfs`` and POSIX shared memory objects - (``shm_open``). On Linux this corresponds to ``Shmem`` in ``/proc/meminfo`` - and is already counted within **active** / **inactive**. + - **shared** *(Linux, BSD)*: :term:`shared memory` accessible by multiple + processes simultaneously, such as in-memory ``tmpfs`` and POSIX shared + memory objects (``shm_open``). On Linux this corresponds to ``Shmem`` in + ``/proc/meminfo`` and is already counted within **active** / **inactive**. - **slab** *(Linux)*: memory used by the kernel's internal object caches (e.g. inode and dentry caches). The reclaimable portion (``SReclaimable``) is already included in **cached**. @@ -429,18 +430,18 @@ Memory - :ref:`faq_used_plus_free` .. versionchanged:: 4.2.0 - added *shared* metric on Linux. + Linux: added **shared** field. .. versionchanged:: 5.4.4 - added *slab* metric on Linux. + Linux: added **slab** field. .. versionchanged:: 8.0.0 - added *cached* and *wired* metric on Windows. + Windows: added **cached** and **wired** fields. .. function:: swap_memory() - Return system swap memory statistics as a named tuple including the following - fields: + Return system :term:`swap memory` statistics as a named tuple + including the following fields: * **total**: total swap space. On Windows this is derived as ``CommitLimit - PhysicalTotal``, representing virtual memory backed by @@ -449,16 +450,15 @@ Memory * **free**: swap space not in use (``total - used``). * **percent**: swap usage as a percentage, calculated as ``used / total * 100``. - * **sin**: number of bytes the system has paged *in* from disk (pages moved - from swap space back into RAM) since boot. See :term:`swap-in`. - * **sout**: number of bytes the system has paged *out* to disk (pages moved - from RAM into swap space) since boot. A continuously increasing **sout** - is a sign of memory pressure. See :term:`swap-out`. + * **sin**: number of bytes the system has moved from disk + (:term:`swap `) back into RAM. See :term:`swap-in`. + * **sout**: number of bytes the system has moved from RAM to disk + (:term:`swap `). A continuously increasing + **sout** rate is a sign of memory pressure. See :term:`swap-out`. **sin** and **sout** are :term:`cumulative counters ` - since boot; monitor their rate of change rather than the absolute value to - detect active swapping. See :term:`swap-in` and :term:`swap-out`. On Windows - both are always ``0``. + since boot. Monitor their rate of change rather than the absolute value to + detect active swapping. On Windows both are always ``0``. .. code-block:: pycon @@ -466,12 +466,13 @@ Memory >>> psutil.swap_memory() sswap(total=2097147904, used=886620160, free=1210527744, percent=42.3, sin=1050411008, sout=1906720768) - .. seealso:: `scripts/meminfo.py`_. + .. seealso:: + - `scripts/meminfo.py`_ + - :ref:`Swap activity recipe ` .. versionchanged:: 5.2.3 - on Linux this function relies on /proc fs instead of sysinfo() syscall so - that it can be used in conjunction with :const:`psutil.PROCFS_PATH` to - retrieve memory info about Linux containers such as Docker and Heroku. + Linux: use /proc instead of ``sysinfo()`` syscall to support + :const:`psutil.PROCFS_PATH` usage (e.g. useful for Docker containers ...). Disks ^^^^^ @@ -542,7 +543,7 @@ Disks .. seealso:: `scripts/disk_usage.py`_. .. versionchanged:: 4.3.0 - *percent* value takes root reserved space into account. + **percent** value takes root reserved space into account. .. function:: disk_io_counters(perdisk=False, nowrap=True) @@ -591,18 +592,21 @@ Disks on Windows ``"diskperf -y"`` command may need to be executed first otherwise this function won't find any disk. - .. seealso:: `scripts/iotop.py`_. + .. seealso:: + - `scripts/iotop.py`_ + - :ref:`Real-time disk I/O recipe ` + - :ref:`Real-time disk I/O percent recipe ` .. versionchanged:: 5.3.0 numbers no longer wrap (restart from zero) across calls thanks to new - *nowrap* argument. + *nowrap* argument. .. versionchanged:: 4.0.0 added **busy_time** (Linux, FreeBSD), **read_merged_count** and - **write_merged_count** (Linux) fields. + **write_merged_count** (Linux) fields. .. versionchanged:: 4.0.0 - NetBSD no longer has **read_time** and **write_time** fields. + NetBSD: removed **read_time** and **write_time** fields. Network ^^^^^^^ @@ -618,9 +622,11 @@ Network - **packets_recv**: number of packets received - **errin**: total number of errors while receiving - **errout**: total number of errors while sending - - **dropin**: total number of incoming packets which were dropped - - **dropout**: total number of outgoing packets which were dropped (always 0 - on macOS and BSD). See :term:`dropin / dropout`. + - **dropin**: total number of incoming packets dropped at the + :term:`NIC` level. Unlike **errin**, drops indicate the interface or + kernel buffer was overwhelmed. + - **dropout**: total number of outgoing packets dropped (always 0 on macOS + and BSD). A non-zero and growing count is a sign of network saturation. If *pernic* is ``True`` return the same information for every network interface as a dictionary with interface names as the keys. @@ -654,7 +660,7 @@ Network Return system-wide socket connections as a list of named tuples. Every named tuple provides 7 attributes: - - **fd**: the socket file descriptor; ``-1`` on Windows and SunOS. + - **fd**: the socket :term:`file descriptor`; ``-1`` on Windows and SunOS. - **family**: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6` or :data:`socket.AF_UNIX`. - **type**: the address type, either :data:`socket.SOCK_STREAM`, @@ -729,7 +735,7 @@ Network .. versionadded:: 2.1.0 .. versionchanged:: 5.3.0 - socket "fd" is now set for real instead of being ``-1``. + socket **fd** is now set for real instead of being ``-1``. .. versionchanged:: 5.3.0 **laddr** and **raddr** are named tuples. @@ -776,13 +782,13 @@ Network .. versionadded:: 3.0.0 .. versionchanged:: 3.2.0 - **ptp** field was added. + added **ptp** field. .. versionchanged:: 4.4.0 - added support for **netmask** field on Windows which is no longer ``None``. + Windows: added support for **netmask** field, which is no longer ``None``. .. versionchanged:: 7.0.0 - added support for **broadcast** field on Windows which is no longer ``None``. + Windows: added support for **broadcast** field, which is no longer ``None``. .. function:: net_if_stats() @@ -806,15 +812,13 @@ Network .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. - .. availability:: UNIX - .. versionadded:: 3.0.0 .. versionchanged:: 5.7.3 - **isup** on UNIX also checks whether the NIC is running. + UNIX: **isup** also reflects whether the :term:`NIC` is running. .. versionchanged:: 5.9.3 - **flags** field was added on POSIX. + added **flags** field. Sensors ^^^^^^^ @@ -962,7 +966,7 @@ Other system info suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] .. versionchanged:: 5.3.0 - added "pid" field. + added **pid** field. ------------------------------------------------------------------------------- @@ -1045,7 +1049,7 @@ Functions .. seealso:: :ref:`perf-process-iter` .. versionchanged:: 5.3.0 - added "attrs" and "ad_value" parameters. + added *attrs* and *ad_value* arguments. .. versionchanged:: 6.0.0 no longer checks whether each yielded process PID has been reused. @@ -1054,11 +1058,11 @@ Functions added ``psutil.process_iter.cache_clear()`` API. .. versionchanged:: 8.0.0 - when *attrs* is specified, the pre-fetched values are cached - directly on the :class:`Process` instance so that subsequent - method calls (e.g. ``p.name()``, ``p.status()``) return the - cached values instead of making new system calls. The :attr:`Process.info` - dict is deprecated in favor of this new approach. + when *attrs* is specified, the pre-fetched values are cached directly on + the :class:`Process` instance, so that subsequent method calls (e.g. + ``p.name()``, ``p.status()``) return the cached values instead of making + new system calls. The :attr:`Process.info` dict is deprecated in favor of + this new approach. .. versionchanged:: 8.0.0 passing an empty list (``attrs=[]``) to mean "all attributes" is @@ -1489,7 +1493,7 @@ Process class .. method:: nice(value=None) - Get or set process niceness (priority). + Get or set process :term:`niceness ` (priority). On UNIX this is a number which usually goes from ``-20`` to ``20``. The higher the nice value, the lower the priority of the process. @@ -1517,13 +1521,13 @@ Process class >>> p.nice(psutil.HIGH_PRIORITY_CLASS) .. versionchanged:: 8.0.0 - on Windows, return value is now a :class:`psutil.ProcessPriority` enum - member. + on Windows, the return value is now a :class:`psutil.ProcessPriority` + enum member. See :ref:`migration guide `. .. method:: ionice(ioclass=None, value=None) - Get or set process I/O niceness (priority). + Get or set process :term:`I/O niceness ` (priority). If no argument is provided it acts as a get, returning a ``(ioclass, value)`` tuple on Linux and a *ioclass* integer on Windows. If *ioclass* is provided it acts as a set. In this case an additional @@ -1569,7 +1573,7 @@ Process class .. availability:: Linux, Windows .. versionchanged:: 5.6.2 - Windows accepts new :data:`IOPRIO_* ` constants. + Windows: accept new :data:`IOPRIO_* ` constants. .. versionchanged:: 8.0.0 *ioclass* is now a :class:`psutil.ProcessIOPriority` enum member. @@ -1646,8 +1650,10 @@ Process class .. availability:: Linux, Windows, BSD, AIX .. versionchanged:: 5.2.0 - added **read_chars** + **write_chars** on Linux and **other_count** + - **other_bytes** on Windows. + Linux: added **read_chars** and **write_chars** fields. + + .. versionchanged:: 5.2.0 + Windows: added **other_count** and **other_bytes** fields. .. method:: num_ctx_switches() @@ -1720,10 +1726,10 @@ Process class 0.70 .. versionchanged:: 4.1.0 - return two extra fields: **children_user** and **children_system**. + added **children_user** and **children_system** fields. .. versionchanged:: 5.6.4 - added **iowait** on Linux. + Linux: added **iowait** field. .. method:: cpu_percent(interval=None) @@ -1830,26 +1836,21 @@ Process class | | | | | | peak_vms | +---------+---------+----------+---------+-----+-----------------+ - - **rss**: aka :term:`RSS`. The portion of physical memory - currently held by this process (code, data, stack, and mapped files that - are resident). Pages swapped out to disk are not counted. On UNIX it - matches the ``top`` RES column. On Windows it maps to ``WorkingSetSize``. + - **rss**: aka :term:`RSS`. On UNIX matches the ``top`` RES column. On + Windows maps to ``WorkingSetSize``. - - **vms**: aka :term:`VMS`. The total address space reserved by - the process, including pages not yet touched, pages in swap, and - memory-mapped files not yet accessed. Typically much larger than - **rss**. On UNIX it matches the ``top`` VIRT column. On Windows this - maps to ``PrivateUsage`` (private committed pages only), which differs - from the UNIX definition; use ``virtual`` from :meth:`memory_info_ex` - for the true virtual address space size. + - **vms**: aka :term:`VMS`. On UNIX matches the ``top`` VIRT column. On + Windows maps to ``PrivateUsage`` (private committed pages only), which + differs from the UNIX definition; use **virtual** from + :meth:`memory_info_ex` for the true virtual address space size. - - **shared** *(Linux)*: memory backed by a file or device (shared - libraries, mmap'd files, POSIX shared memory) that *could* be shared - with other processes. A page is counted here even if no other process - is currently mapping it. Matches ``top``'s SHR column. + - **shared** *(Linux)*: :term:`shared memory` that *could* be shared with + other processes (shared libraries, mmap'd files, POSIX shared memory). + Counted even if no other process is currently mapping it. Matches + ``top``'s SHR column. - **text** *(Linux, BSD)*: aka TRS (Text Resident Set). Resident memory - devoted to executable code. These pages are read-only and typically + devoted to executable code. This memory is read-only and typically shared across all processes running the same binary. Matches ``top``'s CODE column. @@ -1861,13 +1862,11 @@ Process class - **stack** *(BSD)*: size of the process stack segment. Reported separately from **data** (unlike Linux where both are combined). - - **peak_rss** *(BSD, Windows)*: the highest :term:`RSS` value (high water mark) - the process has ever reached. See :term:`peak_rss`. On BSD this may be - ``0`` for kernel PIDs. - On Windows it maps to ``PeakWorkingSetSize``. + - **peak_rss** *(BSD, Windows)*: see :term:`peak_rss`. On BSD may be ``0`` + for kernel PIDs. On Windows maps to ``PeakWorkingSetSize``. - - **peak_vms** *(Windows)*: peak private committed (page-file-backed) - virtual memory. Maps to ``PeakPagefileUsage``. + - **peak_vms** *(Windows)*: see :term:`peak_vms`. Maps to + ``PeakPagefileUsage``. For the full definitions of Windows fields see `PROCESS_MEMORY_COUNTERS_EX`_. @@ -1895,8 +1894,8 @@ Process class See :ref:`migration guide `. .. versionchanged:: 8.0.0 - macOS: **pfaults** and **pageins** removed with **no backward-compatible - aliases**. Use :meth:`page_faults` instead. + macOS: removed **pfaults** and **pageins** fields with **no + backward-compatible aliases**. Use :meth:`page_faults` instead. See :ref:`migration guide `. .. versionchanged:: 8.0.0 @@ -1909,7 +1908,7 @@ Process class See :ref:`migration guide `. .. versionchanged:: 8.0.0 - BSD: added **peak_rss**. + BSD: added **peak_rss** field. .. method:: memory_info_ex() @@ -1936,30 +1935,28 @@ Process class | hugetlb | phys_footprint | | +-------------+----------------+--------------------+ - - **peak_rss** *(Linux, macOS)*: the highest :term:`RSS` value (high water - mark) the process has reached since it started. See :term:`peak_rss`. - - **peak_vms** *(Linux)*: the highest VMS value the process has reached - since it started. - - **rss_anon** *(Linux, macOS)*: resident :term:`anonymous memory` pages + - **peak_rss** *(Linux, macOS)*: see :term:`peak_rss`. + - **peak_vms** *(Linux)*: see :term:`peak_vms`. + - **rss_anon** *(Linux, macOS)*: resident :term:`anonymous memory` (heap, stack, private mappings) not backed by any file. Set to 0 on Linux < 4.5. - - **rss_file** *(Linux, macOS)*: resident file-backed memory; pages mapped + - **rss_file** *(Linux, macOS)*: resident file-backed memory mapped from files (shared libraries, mmap'd files). Set to 0 on Linux < 4.5. - - **rss_shmem** *(Linux)*: resident shared memory pages (``tmpfs``, + - **rss_shmem** *(Linux)*: resident :term:`shared memory` (``tmpfs``, ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals **rss**. Set to 0 on Linux < 4.5. - **wired** *(macOS)*: memory pinned in RAM by the kernel on behalf of this process; cannot be compressed or paged out. - - **swap** *(Linux)*: process memory currently in swap. Equivalent to - ``memory_footprint().swap`` but cheaper, as it reads from + - **swap** *(Linux)*: process memory currently in :term:`swap `. + Equivalent to ``memory_footprint().swap`` but cheaper, as it reads from ``/proc//status`` instead of ``/proc//smaps``. - - **compressed** *(macOS)*: pages held in the in-RAM memory compressor; not - counted in **rss**. A large value signals memory pressure but has not yet - triggered swapping. + - **compressed** *(macOS)*: memory held in the in-RAM memory compressor; + not counted in **rss**. A large value signals memory pressure but has + not yet triggered swapping. - **hugetlb** *(Linux)*: resident memory backed by huge pages. Set to 0 on Linux < 4.4. - **phys_footprint** *(macOS)*: total physical memory impact including - compressed pages. What Xcode and ``footprint(1)`` report; prefer this + compressed memory. What Xcode and ``footprint(1)`` report; prefer this over **rss** macOS memory monitoring. - **virtual** *(Windows)*: true virtual address space size, including reserved-but-uncommitted regions (unlike **vms** in @@ -1981,23 +1978,23 @@ Process class .. method:: memory_footprint() - Return a named tuple with USS, PSS and swap memory metrics. These give - a more accurate picture of actual memory consumption than - :meth:`memory_info` (see this + Return a named tuple with :term:`USS`, :term:`PSS` and :term:`swap memory` + metrics. These give a more accurate picture of actual memory consumption + than :meth:`memory_info` (see this `blog post `_). It walks the full process address space, so it is slower than :meth:`memory_info` and may require elevated privileges. - - **uss** *(Linux, macOS, Windows)*: aka :term:`USS`; memory which is - unique to the process, and which would be freed if the process were - terminated right now. + - **uss** *(Linux, macOS, Windows)*: aka :term:`USS`; the + :term:`private memory` of the process, which would be freed if the + process were terminated right now. - **pss** *(Linux)*: aka :term:`PSS`; shared memory divided evenly among the processes sharing it. I.e. if a process has 10 MBs all to itself, and 10 MBs shared with another process, its PSS will be 15 MBs. - - **swap** *(Linux)*: process memory currently in swap, counted - per-mapping. + - **swap** *(Linux)*: process memory currently in :term:`swap `, + counted per-mapping. Example on Linux: @@ -2073,28 +2070,28 @@ Process class Linux fields (from ``/proc//smaps``): - - **rss**: resident pages in this mapping. - - **size**: total virtual size; may far exceed **rss** for sparse or - reserved-but-unaccessed mappings. - - **pss**: proportional RSS. **rss** divided by the number of processes - sharing this mapping. Useful for fair per-process accounting. - - **shared_clean**: shared pages not modified (e.g. shared library code); - can be dropped from RAM without writing to swap. - - **shared_dirty**: shared pages that have been written to. - - **private_clean**: private unmodified pages; can be dropped without - writing to swap. - - **private_dirty**: private modified pages; must be written to swap - before they can be reclaimed. The key indicator of a mapping's real - memory cost. - - **referenced**: pages recently accessed. - - **anonymous**: :term:`anonymous memory` pages not backed by a file (heap, stack allocations). - - **swap**: pages from this mapping currently in swap. + - **rss**: :term:`RSS` for this mapping. + - **size**: total virtual size; may far exceed **rss** if parts have + never been accessed. + - **pss**: :term:`PSS` for this mapping, that is **rss** split + proportionally among all processes sharing it. + - **shared_clean**: :term:`shared memory` not written to since loaded + (clean); can be discarded and reloaded from disk for free. + - **shared_dirty**: :term:`shared memory` that has been written to (dirty). + - **private_clean**: :term:`private memory` not written to (clean). + - **private_dirty**: :term:`private memory` that has been written to + (dirty); must be saved to swap before it can be freed. The key + indicator of real memory cost. + - **referenced**: bytes recently accessed. + - **anonymous**: :term:`anonymous memory` in this mapping (heap, stack). + - **swap**: bytes from this mapping currently in + :term:`swap `. FreeBSD fields: - - **private**: pages in this mapping private to this process. - - **ref_count**: reference count on the VM object backing this mapping. - - **shadow_count**: depth of the copy-on-write shadow object chain. + - **private**: :term:`private memory` in this mapping. + - **ref_count**: reference count on the underlying memory object. + - **shadow_count**: depth of the copy-on-write chain. .. code-block:: pycon @@ -2173,7 +2170,8 @@ Process class the following fields: - **path**: the absolute file name. - - **fd**: the file descriptor number; on Windows this is always ``-1``. + - **fd**: the :term:`file descriptor` number; on Windows this is always + ``-1``. Linux only: @@ -2205,7 +2203,7 @@ Process class no longer hangs on Windows. .. versionchanged:: 4.1.0 - new **position**, **mode** and **flags** fields on Linux. + Linux: added **position**, **mode** and **flags** fields. .. method:: net_connections(kind="inet") @@ -2253,19 +2251,18 @@ Process class automatically remove process from :func:`process_iter` internal cache if PID has been reused by another process. - .. method:: send_signal(signal) + .. method:: send_signal(sig) - Send a signal to process (see :mod:`signal` module constants) + Send signal *sig* to process (see :mod:`signal` module constants), preemptively checking whether PID has been reused. On UNIX this is the same as ``os.kill(pid, sig)``. On Windows only *SIGTERM*, *CTRL_C_EVENT* and *CTRL_BREAK_EVENT* signals - are supported and *SIGTERM* is treated as an alias for :meth:`kill`. + are supported, and *SIGTERM* is treated as an alias for :meth:`kill`. .. seealso:: how to :ref:`kill a process tree ` .. versionchanged:: 3.2.0 - support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals on Windows was - added. + Windows: add support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals. .. method:: suspend() @@ -2344,6 +2341,9 @@ Process class If none of these mechanisms are available, the function falls back to a busy loop (non-blocking call and short sleeps). + Functionality also ported to the :mod:`subprocess` module in Python 3.15, + see `GH-144047`_. + .. versionchanged:: 5.7.2 if *timeout* is not ``None``, use efficient event-driven implementation on Linux >= 5.3 and macOS / BSD. @@ -2352,13 +2352,13 @@ Process class return value is cached (instead of returning ``None``). .. versionchanged:: 5.7.1 - on POSIX, if the signal is negative, return it as a human readable + POSIX: if the signal is negative, return it as a human readable :mod:`enum`. .. versionchanged:: 7.2.2 on Linux >= 5.3 + Python >= 3.9 and macOS/BSD, use :func:`os.pidfd_open` and :func:`select.kqueue` respectively, instead of less efficient busy-loop - polling. Later added to CPython 3.15 in `GH-144047`_. + polling. ------------------------------------------------------------------------------- @@ -2403,11 +2403,11 @@ C heap introspection The following functions provide direct access to the platform's native :term:`heap` allocator (such as glibc's ``malloc`` on Linux or ``jemalloc`` -on BSD). They -are low-level interfaces intended for detecting memory leaks in C extensions, -which are usually not revealed via standard RSS / VMS metrics. These functions do -not reflect Python object memory; they operate solely on allocations made in C -via ``malloc()``, ``free()``, and related calls. +on BSD). They are low-level interfaces intended for detecting memory leaks in C +extensions, which are usually not revealed via standard :term:`RSS` / :term:`VMS` +metrics. +These functions do not reflect Python object memory; they operate solely on +allocations made in C via ``malloc()``, ``free()``, and related calls. The general idea behind these functions is straightforward: capture the state of the :term:`heap` before and after repeatedly invoking a function @@ -2467,7 +2467,7 @@ Python's memory tracking misses. the :term:`heap` (typically small ``malloc()`` allocations). In practice, modern allocators rarely comply, so this is not a - general-purpose memory-reduction tool and won't meaningfully shrink RSS in + general-purpose memory-reduction tool and won't meaningfully shrink :term:`RSS` in real programs. Its primary value is in **leak detection tools**. Calling ``heap_trim()`` before taking measurements helps reduce allocator diff --git a/docs/faq.rst b/docs/faq.rst index 8e34646534..0e061637a1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -304,26 +304,26 @@ stays in the 0–100% range because it averages across all cores. The returned value is explicitly *not* split evenly between all available CPUs. This is consistent with the ``top`` UNIX utility: a busy loop on a -system with 2 logical CPUs is reported as 100%, not 50%. Note that Windows -``taskmgr.exe`` behaves differently (it would report 50%). To emulate that: -``p.cpu_percent() / psutil.cpu_count()``. +system with 2 :term:`logical CPUs ` is reported as 100%, not 50%. +Note that Windows ``taskmgr.exe`` behaves differently (it would report 50%). +To emulate that: ``p.cpu_percent() / psutil.cpu_count()``. .. _faq_cpu_count: What is the difference between psutil, os, and multiprocessing cpu_count()? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- :func:`os.cpu_count` returns the number of **logical** CPUs (including - hyperthreads). It is the same as ``psutil.cpu_count(logical=True)``, but - psutil does not honour `PYTHON_CPU_COUNT`_ environment variable introduced in - Python 3.13. +- :func:`os.cpu_count` returns the number of :term:`logical CPUs ` + (including hyperthreads). It is the same as ``psutil.cpu_count(logical=True)``, + but psutil does not honour `PYTHON_CPU_COUNT`_ environment variable introduced + in Python 3.13. - :func:`os.process_cpu_count` (Python 3.13+) returns the number of CPUs the calling process is **allowed to use** (respects CPU affinity and cgroups). The psutil equivalent is ``len(psutil.Process().cpu_affinity())``. - :func:`multiprocessing.cpu_count` returns the same value as :func:`os.process_cpu_count` (Python 3.13+). - :func:`psutil.cpu_count` with ``logical=False`` returns the number of - **physical** cores, which has no stdlib equivalent. + :term:`physical cores `, which has no stdlib equivalent. Memory ------ diff --git a/docs/glossary.rst b/docs/glossary.rst index 7b34847122..78ab390ecc 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -9,77 +9,61 @@ Glossary anonymous memory RAM used by the program that is not associated with any file (unlike the - :term:`page cache`), such as the heap, the stack, and other memory - allocated directly by the program (e.g. via ``malloc()``). - Anonymous pages have no on-disk counterpart and must be written to swap - if evicted. Visible in the ``path`` column of :meth:`Process.memory_maps` - as ``"[heap]"``, ``"[stack]"``, or an empty string. + :term:`page cache`), such as the :term:`heap`, the stack, and other + memory allocated directly by the program (e.g. via ``malloc()``). + Anonymous pages have no on-disk counterpart and must be written to + :term:`swap memory` if evicted. Exposed by psutil via the ``rss_anon`` field of + :meth:`Process.memory_info_ex` (total resident anonymous pages) and the + ``anonymous`` field of :meth:`Process.memory_maps` (per mapping). + Anonymous regions are also visible in the ``path`` column of + :meth:`Process.memory_maps` as ``"[heap]"``, ``"[stack]"``, or an empty + string. available memory - The amount of RAM that can be given to processes without the system - going into swap. This is the right field to watch for memory - pressure, not ``free``. ``free`` is often deceptively low because - the OS keeps recently freed pages as reclaimable cache; those pages - are counted in ``available`` but not in ``free``. A monitoring - alert should fire on ``available`` (or ``percent``) falling below a - threshold, not on ``free``. See :func:`virtual_memory`. + The amount of RAM that can be given to processes without the system going + into :term:`swap `. This is the right field to watch for + memory pressure, not ``free``. ``free`` is often deceptively low because + the OS keeps recently freed pages as reclaimable cache; those pages are + counted in ``available`` but not in ``free``. A monitoring alert should + fire on ``available`` (or ``percent``) falling below a threshold, not on + ``free``. See :func:`virtual_memory`. busy_time - A :term:`cumulative counter` (milliseconds) tracking the time a - disk device spent actually performing I/O, as reported in the - ``busy_time`` field of :func:`disk_io_counters` (Linux and FreeBSD - only). To use it, sample twice and divide the delta by elapsed - time to get a utilisation percentage (analogous to CPU percent but - for disks). When it approaches 100% the disk queue is growing and - I/O latency will spike. Unlike ``read_bytes``/``write_bytes``, - ``busy_time`` reveals saturation even when throughput looks modest - (e.g. many small random I/Os). + A :term:`cumulative counter` (milliseconds) tracking the time a disk + device spent actually performing I/O, as reported in the ``busy_time`` + field of :func:`disk_io_counters` (Linux and FreeBSD only). To use it, + sample twice and divide the delta by elapsed time to get a utilization + percentage (analogous to CPU percent but for disks). A value close to + 100% means the disk is saturated. - CPU affinity - - A property of a process (or thread) that restricts which logical CPUs - it is allowed to run on. For example, pinning a process to CPU 0 and - CPU 1 prevents the OS scheduler from moving it to other cores. See - :meth:`Process.cpu_affinity`. - - CPU percent + .. seealso:: :ref:`Real-time disk I/O percent recipe ` - The fraction of CPU time consumed by a process or the whole system - over a measurement interval, expressed as a percentage. A value of - 100 % means one full logical CPU core was busy for the entire - interval. Values above 100 % are possible on multi-core systems when - multiple threads run in parallel. See :func:`cpu_percent` and - :meth:`Process.cpu_percent`. - - CPU times + CPU affinity - :term:`Cumulative counters ` (in seconds) recording - how much time a CPU or process spent in different modes: - **user** (normal code), **system** (kernel code on behalf of the process), - **idle**, **iowait**, etc. - These always increase monotonically. See :func:`cpu_times` and - :meth:`Process.cpu_times`. + A property of a process (or thread) that restricts which + :term:`logical CPUs ` it is allowed to run on. For example, + pinning a process to CPU 0 and CPU 1 prevents the OS scheduler from + moving it to other cores. This could be useful, e.g., for benchmarking. + See :meth:`Process.cpu_affinity`. context switch Occurs whenever the CPU stops executing one process or thread and starts executing another. Frequent context switching can indicate high system - load or excessive thread contention. See :func:`cpu_stats` and - :meth:`Process.num_ctx_switches`. - - The ``voluntary`` and ``involuntary`` fields of - :meth:`Process.num_ctx_switches` tell you *why* the process was switched - out. A **voluntary** switch means the process gave up the CPU itself - (waiting for I/O, a lock, or a timer); a high rate is normal for - I/O-bound processes. An **involuntary** switch means the OS forcibly took - the CPU away (time slice expired, higher-priority process woke up); a - high rate means the process wants to run but keeps getting interrupted — - a sign it is competing for CPU. If involuntary switches dominate, adding - CPU capacity or reducing other load will directly speed up the process; - if voluntary switches dominate, the bottleneck is I/O or locking, not - CPU. + load or excessive thread contention. See :meth:`Process.num_ctx_switches` + and :func:`cpu_stats`. + + A **voluntary** context switch occurs when the process yields the CPU + itself (for example, waiting for I/O or a lock). A high rate of voluntary + switches is normal for I/O-bound processes. + + An **involuntary** context switch occurs when the OS forcibly takes the + CPU from the process. A high rate indicates the process wants to run but + keeps getting interrupted. If involuntary switches dominate, reducing + other load will directly speed up the process. If voluntary switches + dominate, the bottleneck is usually I/O or locking, not the CPU. cumulative counter @@ -91,28 +75,20 @@ Glossary two samples by the elapsed time to get a meaningful rate (e.g. bytes per second, context switches per second). - dropin / dropout - - Fields in :func:`net_io_counters` counting packets dropped at the - :term:`NIC` level before they could be processed (``dropin``) or sent - (``dropout``). Unlike transmission errors, drops indicate the - interface or kernel buffer was overwhelmed. A non-zero and growing - count is a sign of network saturation or misconfiguration. - file descriptor An integer handle used by UNIX processes to reference open files, sockets, pipes, and other I/O resources. On Windows the equivalent - are *handles*. Leaking file descriptors (opening without closing) - eventually causes ``EMFILE`` / ``Too many open files`` errors. See - :meth:`Process.num_fds` and :meth:`Process.open_files`. + are :term:`handles `. Leaking file descriptors (opening without + closing) eventually causes ``EMFILE`` / ``Too many open files`` errors. + See :meth:`Process.num_fds` and :meth:`Process.open_files`. handle On Windows, an opaque reference to a kernel object such as a file, - thread, process, event, mutex, or registry key. Handles are the - Windows equivalent of UNIX :term:`file descriptors `. Each open - handle consumes a small amount of kernel memory. Leaking / unclosed + thread, process, event or mutex. Handles are the Windows equivalent of + UNIX :term:`file descriptors `. Each open handle + consumes a small amount of kernel memory. Leaking / unclosed handles eventually causes ``ERROR_NO_MORE_FILES`` or similar errors. See :meth:`Process.num_handles`. @@ -148,6 +124,10 @@ Glossary waiting for I/O operations to complete. High iowait indicates a disk or network bottleneck. It is reported as part of :func:`cpu_times` but is *not* included in the idle counter. + To get it as a percentage: ``psutil.cpu_times_percent(interval=1).iowait``. + Note that this is a CPU metric, not a disk metric: it measures how much + CPU time is wasted waiting, not how busy the disk is. See also + :term:`busy_time` for actual disk utilization. ionice @@ -170,8 +150,15 @@ Glossary Three floating-point values representing the average number of processes in a *runnable* or *uninterruptible* state over the last - 1, 5, and 15 minutes. A load average equal to the number of logical - CPUs means the system is fully saturated. See :func:`getloadavg`. + 1, 5, and 15 minutes. A load average equal to the number of + :term:`logical CPUs ` means the system is fully saturated. + See :func:`getloadavg`. + + NIC + + *Network Interface Card*, a hardware or virtual network interface. + psutil uses this term when referring to per-interface network + statistics. See :func:`net_if_addrs` and :func:`net_if_stats`. nice @@ -183,12 +170,13 @@ Glossary page cache - RAM used by the kernel to cache file data read from or written to disk. - When a process reads a file, the data stays in the page cache. Subsequent - reads are served from RAM without any disk I/O. The OS reclaims page - cache memory automatically under pressure, so a large cache is healthy. - Shown as the ``cached`` field of :func:`virtual_memory` on Linux/BSD. - See also :term:`available memory`. + RAM used to cache data of regular files on disk. + When a process reads a file, the data stays in the page cache, and when + it writes, the data is first stored in the cache before being written to + disk. Subsequent reads or writes can be served from RAM without disk I/O, + making access fast. The OS reclaims page cache automatically under memory + pressure, so a large cache is healthy. Shown as the ``cached`` field of + :func:`virtual_memory` on Linux/BSD. peak_rss @@ -198,6 +186,15 @@ Glossary :meth:`Process.memory_info_ex` (Linux, macOS). Useful for capacity planning and leak detection: if ``peak_rss`` keeps growing across successive runs or over time, the process is likely leaking memory. + See also :term:`peak_vms`. + + peak_vms + + The highest :term:`VMS` value a process has ever reached since it + started. Available via :meth:`Process.memory_info_ex` (Linux) and + :meth:`Process.memory_info` (Windows). On Windows this maps to + ``PeakPagefileUsage`` (peak private committed memory), which is not + the same as UNIX VMS. See also :term:`peak_rss`. page fault @@ -206,8 +203,8 @@ Glossary resolved without disk I/O (e.g. the page is already in RAM but not yet mapped, or it is copy-on-write). A **major** fault requires reading the page from disk (e.g. from a memory-mapped file or the - swap area) and is significantly more expensive. Many major faults - may indicate memory pressure or excessive swapping. See + :term:`swap memory` area) and is significantly more expensive. Many + major faults may indicate memory pressure or excessive swapping. See :meth:`Process.page_faults`. physical CPU @@ -217,36 +214,34 @@ Glossary logical CPUs when hyper-threading is enabled. The physical count is returned by ``cpu_count(logical=False)``. + private memory + + Memory pages not shared with any other process, such as the + :term:`heap`, the stack, and other allocations made directly by the + program, e.g. via ``malloc()``. + :term:`USS`, returned by :meth:`Process.memory_footprint`, measures + exactly the private memory of a process, that is the bytes that would be + freed if the process exited. At a per-mapping level, the + ``private_clean`` and ``private_dirty`` fields of + :meth:`Process.memory_maps` (Linux) and the ``private`` field (FreeBSD) + break it down further. + PSS *Proportional Set Size*, the amount of RAM used by a process, - where shared pages are divided proportionally among all processes - that map them. PSS gives a fairer per-process memory estimate than - :term:`RSS` when shared libraries are involved. Available on Linux + where :term:`shared memory` pages are divided proportionally among all + processes that map them. PSS gives a fairer per-process memory estimate + than :term:`RSS` when shared libraries are involved. Available on Linux via :meth:`Process.memory_footprint`. RSS *Resident Set Size*, the amount of physical RAM currently occupied - by a process, including shared library pages. It is the most + by a process, including :term:`shared memory` pages. It is the most commonly reported memory metric (shown as ``RES`` in ``top``), but it can be misleading because shared pages are counted in full for every process that maps them. See :meth:`Process.memory_info`. - status (process) - - The scheduling state of a process at a given instant. Common - values are: - - - ``running``: actively executing on a CPU. - - ``sleeping``: waiting for an event (interruptible sleep). - - ``disk-sleep``: waiting for I/O (uninterruptible sleep). - - ``stopped``: suspended via ``SIGSTOP`` or a debugger. - - ``zombie``: exited but not yet reaped by its parent. - - ``idle``: doing nothing. - - See :meth:`Process.status` and the ``STATUS_*`` constants. - soft interrupt Deferred work scheduled by a :term:`hardware interrupt` handler to @@ -257,38 +252,50 @@ Glossary :func:`cpu_stats`. A high rate usually points to heavy network or disk I/O throughput rather than a hardware problem. + shared memory + + Memory pages mapped by more than one process at the same time. The most + common example is shared libraries (e.g. ``libc.so``): the OS loads them + once and lets every process that needs them map the same physical pages, + saving RAM. Shared pages are counted in full in :term:`RSS` for every + process that maps them; :term:`PSS` corrects for this by splitting each + shared page proportionally among the processes that use it. + See also :term:`private memory`. + + Exposed by psutil as the ``shared`` field of :func:`virtual_memory` and + :meth:`Process.memory_info` (Linux), the ``rss_shmem`` field of + :meth:`Process.memory_info_ex` (Linux), and the ``shared_clean`` / + ``shared_dirty`` fields of :meth:`Process.memory_maps` (Linux). + swap-in - A page moved from swap space on disk back into RAM. Reported as the - ``sin`` :term:`cumulative counter` of :func:`swap_memory`. On its - own a non-zero ``sin`` rate is not alarming — it may just mean the - system is reloading pages that were quietly evicted during idle - time. It becomes a concern when it coincides with a high - :term:`swap-out` rate, meaning the system is continuously trading - pages in and out. See also :term:`swap-out`. + Memory moved from disk (:term:`swap `) back into RAM. + Reported as the ``sin`` :term:`cumulative counter` of + :func:`swap_memory`. A non-zero ``sin`` rate usually means the system + is bringing memory back into RAM for processes to use. See also + :term:`swap-out`. swap-out - A page evicted from RAM to swap space on disk to free memory. + Memory moved from RAM to disk (:term:`swap `). Reported as the ``sout`` :term:`cumulative counter` of - :func:`swap_memory`. A sustained non-zero rate is the clearest - sign of memory pressure: the system is running out of RAM and - actively offloading pages to disk. Compute the rate of change - over an interval rather than reading the absolute value. - See also :term:`swap-in`. + :func:`swap_memory`. A non-zero ``sout`` rate indicates memory + pressure: the system is running low on RAM and must move data to disk, + which can slow performance. See also :term:`swap-in`. - swap memory + .. seealso:: + - :ref:`swap activity recipe ` + - :term:`thrashing` - Disk space used as an overflow extension of physical RAM. When the - OS runs low on RAM it *swaps out* memory pages to disk and restores - them on demand. Heavy swapping significantly degrades performance. - See :func:`swap_memory`. - - NIC + swap memory - *Network Interface Card*, a hardware or virtual network interface. - psutil uses this term when referring to per-interface network - statistics. See :func:`net_if_addrs` and :func:`net_if_stats`. + Disk space used as an extension of physical RAM. When the OS runs out of + RAM, it moves memory to disk to free space (:term:`swap-out`), and moves + it back into RAM (:term:`swap-in`) when a process needs it. If RAM is + full, the OS may first swap out other pages to make room. Swap prevents + out-of-memory crashes, but is much slower than RAM, so heavy swapping can + significantly degrade performance. See :func:`swap_memory` and + :ref:`swap activity recipe `. resource limit @@ -301,13 +308,22 @@ Glossary :data:`RLIMIT_AS` (virtual address space), and :data:`RLIMIT_CPU` (CPU time in seconds). See :meth:`Process.rlimit`. + thrashing + + A condition where the system spends more time moving memory between RAM + and disk (:term:`swap `) than doing actual work, because memory + demand exceeds available RAM. The symptom is high and sustained rates on + both ``sin`` and ``sout`` from :func:`swap_memory`. As a result, the + system becomes very slow or unresponsive. CPU utilization may look low + while everything is waiting on disk I/O. + USS - *Unique Set Size*, the amount of RAM that belongs exclusively to a - process and would be freed if it exited. It excludes shared pages - entirely, making it the most accurate single-process memory metric. - Available on Linux, macOS, and Windows via - :meth:`Process.memory_footprint`. + *Unique Set Size*, the :term:`private memory` of a process — the RAM + that belongs exclusively to it and would be freed if it exited. It + excludes :term:`shared memory` pages entirely, making it the most + accurate single-process memory metric. Available on Linux, macOS, and + Windows via :meth:`Process.memory_footprint`. voluntary context switch @@ -316,9 +332,10 @@ Glossary VMS *Virtual Memory Size*, the total virtual address space reserved by a - process, including mapped files, shared libraries, stack, heap, and - swap. VMS is almost always much larger than :term:`RSS` because most - virtual pages are never actually loaded into RAM. See + process, including mapped files, :term:`shared memory`, stack, and + :term:`heap`, regardless of whether those pages are currently in RAM or + in :term:`swap memory`. VMS is almost always much larger than :term:`RSS` + because most virtual pages are never actually loaded into RAM. See :meth:`Process.memory_info`. zombie process @@ -326,6 +343,5 @@ Glossary A process that has exited but whose entry remains in the process table until its parent calls ``wait()``. Zombies hold a PID but consume no CPU or memory. - See :ref:`faq_zombie_process`. .. seealso:: :ref:`faq_zombie_process` diff --git a/docs/recipes.rst b/docs/recipes.rst index 809a6ca157..58290b7ae7 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -395,9 +395,42 @@ Print real-time CPU usage percentage: CPU: 1.4% CPU: 0.9% +Memory +^^^^^^ + +.. _recipe_swap_activity: + +Show real-time swap activity *(Linux, BSD)*. ``sout`` (:term:`swap-out`) is the +key metric: a non-zero and growing rate means the OS is moving memory from RAM +to disk because RAM is full. ``sin`` (:term:`swap-in`) alone is not alarming; +it just means the system is moving previously evicted pages back into RAM. +High ``sin`` and ``sout`` together may indicate heavy swapping (:term:`thrashing`). + +.. code-block:: python + + import psutil, time + + def swap_activity(interval=1): + before = psutil.swap_memory() + while True: + time.sleep(interval) + after = psutil.swap_memory() + sin = after.sin - before.sin + sout = after.sout - before.sout + print("swap-in={}/s swap-out={}/s used={}%".format( + bytes2human(sin), bytes2human(sout), after.percent)) + before = after + +.. code-block:: none + + swap-in=0.0B/s swap-out=0.0B/s used=23% + swap-in=0.0B/s swap-out=1.2M/s used=24% + Disks ^^^^^ +.. _recipe_disk_io: + Show real-time disk I/O: .. code-block:: python @@ -418,6 +451,31 @@ Show real-time disk I/O: Read: 1.2M/s, Write: 256.0K/s Read: 0.0B/s, Write: 128.0K/s +------------------------------------------------------------------------------- + +.. _recipe_disk_io_percent: + +Show real-time disk utilization percentage *(Linux, FreeBSD)*: + +.. code-block:: python + + import psutil, time + + def disk_io_percent(interval=1): + before = psutil.disk_io_counters() + while True: + time.sleep(interval) + after = psutil.disk_io_counters() + busy_ms = after.busy_time - before.busy_time + util = min(busy_ms / (interval * 1000) * 100, 100) + print("Disk: {:.1f}%".format(util)) + before = after + +.. code-block:: none + + Disk: 3.2% + Disk: 78.5% + Network ^^^^^^^ From 3839cac88aac32c3411f2879a8e37dd6f62d55ec Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 2 Apr 2026 19:57:00 +0200 Subject: [PATCH 1674/1714] Refact recipes a bit --- docs/api.rst | 2 +- docs/recipes.rst | 52 +++++++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index a0cd2ee46b..9c42a5e6ad 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -291,7 +291,7 @@ Memory - **total**: total physical memory (exclusive :term:`swap memory`). - **available**: memory that can be given instantly to processes without the - system going into :term:`swap memory`. On Linux it uses the ``MemAvailable`` + system going into :term:`swap `. On Linux it uses the ``MemAvailable`` field from ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back to an estimate. This is the recommended field for monitoring actual memory usage in a cross-platform fashion. See :term:`available memory`. diff --git a/docs/recipes.rst b/docs/recipes.rst index 58290b7ae7..7bc3f5958d 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -115,15 +115,18 @@ Filtering and sorting processes Processes owned by user: -.. code-block:: pycon +.. code-block:: python - >>> import getpass - >>> import psutil - >>> from pprint import pprint as pp - >>> pp([(p.pid, p.name()) for p in psutil.process_iter(["name", "username"]) if p.username() == getpass.getuser()]) - (16832, 'bash'), - (19772, 'ssh'), - (20492, 'python3')] + import getpass, psutil + + def procs_by_user(user=None): + if user is None: + user = getpass.getuser() + return [ + (p.pid, p.name()) + for p in psutil.process_iter(["name", "username"]) + if p.username() == user + ] ------------------------------------------------------------------------------- @@ -144,23 +147,31 @@ Processes using log files: Processes consuming more than 500M of memory: -.. code-block:: pycon +.. code-block:: python + + import psutil - >>> pp([(p.pid, p.name(), p.memory_info().rss) for p in psutil.process_iter(["name", "memory_info"]) if p.memory_info().rss > 500 * 1024 * 1024]) - [(2650, 'chrome', 532324352), - (3038, 'chrome', 1120088064), - (21915, 'sublime_text', 615407616)] + def procs_by_memory(min_bytes=500 * 1024 * 1024): + return [ + (p.pid, p.name(), p.memory_info().rss) + for p in psutil.process_iter(["name", "memory_info"]) + if p.memory_info().rss > min_bytes + ] ------------------------------------------------------------------------------- -Top 3 processes which consumed the most CPU time: +Top N processes by cumulative CPU time: -.. code-block:: pycon +.. code-block:: python - >>> pp([(p.pid, p.name(), sum(p.cpu_times())) for p in sorted(psutil.process_iter(["name", "cpu_times"]), key=lambda p: sum(p.cpu_times()[:2]))][-3:]) - [(2721, 'chrome', 10219.73), - (1150, 'Xorg', 11116.989999999998), - (2650, 'chrome', 18451.97)] + import psutil + + def top_cpu_procs(n=3): + procs = sorted( + psutil.process_iter(["name", "cpu_times"]), + key=lambda p: sum(p.cpu_times()[:2]), + ) + return [(p.pid, p.name(), sum(p.cpu_times())) for p in procs[-n:]] ------------------------------------------------------------------------------- @@ -462,14 +473,13 @@ Show real-time disk utilization percentage *(Linux, FreeBSD)*: import psutil, time def disk_io_percent(interval=1): - before = psutil.disk_io_counters() while True: + before = psutil.disk_io_counters() time.sleep(interval) after = psutil.disk_io_counters() busy_ms = after.busy_time - before.busy_time util = min(busy_ms / (interval * 1000) * 100, 100) print("Disk: {:.1f}%".format(util)) - before = after .. code-block:: none From 0e36014bb09636e85fc4e7afaf5b7248e06114f3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 4 Apr 2026 01:51:45 +0200 Subject: [PATCH 1675/1714] Doc: add `:field:` Sphinx role for named tuple fields (#2802) Introduce a new Sphinx extension that provides the `:field:`fieldname` role for **semantically marking named tuple fields** in API documentation, so they are always distinguishable from the rest, both semantically and visually (up until now the were marked as bold, e.g. ``**name**``). This replaces inconsistent use of backticks and bold formatting throughout the docs. The CSS now renders field names differently from the rest. --- MANIFEST.in | 1 + docs/_ext/field_role.py | 21 ++ docs/_static/css/custom.css | 5 + docs/alternatives.rst | 9 + docs/api.rst | 561 ++++++++++++++++++------------------ docs/changelog.rst | 172 +++++------ docs/conf.py | 1 + docs/faq.rst | 28 +- docs/glossary.rst | 70 ++--- docs/migration.rst | 32 +- docs/stdlib-equivalents.rst | 4 +- 11 files changed, 481 insertions(+), 423 deletions(-) create mode 100644 docs/_ext/field_role.py diff --git a/MANIFEST.in b/MANIFEST.in index f0f6dc4f82..918bdff6f2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,6 +18,7 @@ include docs/_ext/add_home_link.py include docs/_ext/availability.py include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py +include docs/_ext/field_role.py include docs/_links.rst include docs/_sponsors.html include docs/_static/css/custom.css diff --git a/docs/_ext/field_role.py b/docs/_ext/field_role.py new file mode 100644 index 0000000000..88427a24c8 --- /dev/null +++ b/docs/_ext/field_role.py @@ -0,0 +1,21 @@ +# 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. + +"""Sphinx extension providing the :field:`name` role for marking named +tuple fields in the API doc. +""" + +from docutils import nodes + + +def field_role( + name, rawtext, text, lineno, inliner, options=None, content=None +): + """Render :field:`name` as inline code (monospace bold).""" + node = nodes.literal(rawtext, text, classes=["ntuple-field"]) + return [node], [] + + +def setup(app): + app.add_role("field", field_role) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 5bc40b0d1f..9e653fdf55 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -580,6 +580,11 @@ h4 { text-underline-offset: 4px; } +/* named tuple field */ +code.ntuple-field { + font-weight: bold !important; +} + /* ================================================================== */ /* Code blocks */ /* ================================================================== */ diff --git a/docs/alternatives.rst b/docs/alternatives.rst index d9dd90ee5c..d077800881 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -156,6 +156,11 @@ process information. - CPU, memory, disk, network, processes. Directly inspired by psutil and follows a similar API. + * - `heim `_ + - Rust + - Async-first library covering CPU, memory, disk, network, + processes and sensors. + * - `Hardware.Info `_ - C# / .NET - CPU, RAM, GPU, disk, network, battery. @@ -169,6 +174,10 @@ process information. - OS and hardware information: CPU, memory, disk, network, processes, sensors, USB devices. + * - `rust-psutil `_ + - Rust + - Directly inspired by psutil and follows a similar API. + * - `sysinfo `_ - Rust - CPU, memory, disk, network, processes, components. diff --git a/docs/api.rst b/docs/api.rst index 9c42a5e6ad..e9967afb57 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -30,39 +30,40 @@ CPU The attributes availability varies depending on the platform. Cross-platform fields: - - **user**: time spent by normal processes executing in user mode; on Linux - this also includes **guest** time + - :field:`user`: time spent by normal processes executing in user mode; on + Linux this also includes :field:`guest` time - - **system**: time spent by processes executing in kernel mode + - :field:`system`: time spent by processes executing in kernel mode - - **idle**: time spent doing nothing + - :field:`idle`: time spent doing nothing Platform-specific fields: - - **nice** *(Linux, macOS, BSD)*: time spent by :term:`niced ` + - :field:`nice` *(Linux, macOS, BSD)*: time spent by :term:`niced ` (lower-priority) processes executing in user mode; on Linux this also - includes **guest_nice** time. + includes :field:`guest_nice` time. - - **iowait** *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete - (:term:`iowait`). This is *not* accounted in **idle** time counter. + - :field:`iowait` *(Linux, SunOS, AIX)*: time spent waiting for I/O to complete + (:term:`iowait`). This is *not* accounted in :field:`idle` time counter. - - **irq** *(Linux, Windows, BSD)*: time spent for servicing + - :field:`irq` *(Linux, Windows, BSD)*: time spent for servicing :term:`hardware interrupts ` - - **softirq** *(Linux)*: time spent for servicing + - :field:`softirq` *(Linux)*: time spent for servicing :term:`soft interrupts ` - - **steal** *(Linux)*: CPU time the virtual machine wanted to run but was + - :field:`steal` *(Linux)*: CPU time the virtual machine wanted to run but was used by other virtual machines or the host. A sustained non-zero steal rate indicates CPU contention. - - **guest** *(Linux)*: time the host CPU spent running a guest operating - system (virtual machine). Already included in **user** time. + - :field:`guest` *(Linux)*: time the host CPU spent running a guest operating + system (virtual machine). Already included in :field:`user` time. - - **guest_nice** *(Linux)*: like **guest**, but for virtual CPUs running at a - lower :term:`nice` priority. Already included in **nice** time. + - :field:`guest_nice` *(Linux)*: like :field:`guest`, but for virtual CPUs + running at a lower :term:`nice` priority. Already included in :field:`nice` + time. - - **dpc** *(Windows)*: time spent servicing deferred procedure calls (DPCs); + - :field:`dpc` *(Windows)*: time spent servicing deferred procedure calls (DPCs); DPCs are interrupts that run at a lower priority than standard interrupts. When *percpu* is ``True`` return a list of named tuples for each @@ -84,18 +85,18 @@ CPU `#1210 `_. .. versionchanged:: 4.1.0 - Windows: added **irq** and **dpc** fields (**irq** was called **interrupt** - before 8.0.0). + Windows: added :field:`irq` and :field:`dpc` fields (:field:`irq` was + called :field:`interrupt` before 8.0.0). .. versionchanged:: 8.0.0 - Windows: **interrupt** field was renamed to **irq**; **interrupt** still - works but raises :exc:`DeprecationWarning`. + Windows: :field:`interrupt` field was renamed to :field:`irq`; + :field:`interrupt` still works but raises :exc:`DeprecationWarning`. .. versionchanged:: 8.0.0 - field order was standardized: **user**, **system**, **idle** are now - always the first three fields. Previously on Linux, macOS, and BSD the - first three were **user**, **nice**, **system**. See :ref:`migration guide - `. + field order was standardized: :field:`user`, :field:`system`, + :field:`idle` are now always the first three fields. Previously on Linux, + macOS, and BSD the first three were :field:`user`, :field:`nice`, + :field:`system`. See :ref:`migration guide `. .. function:: cpu_percent(interval=None, percpu=False) @@ -149,8 +150,8 @@ CPU .. seealso:: :ref:`faq_cpu_percent` .. versionchanged:: 4.1.0 - Windows: added **irq** and **dpc** fields (**irq** was called - **interrupt** before 8.0.0). + Windows: added :field:`irq` and :field:`dpc` fields (:field:`irq` was + called :field:`interrupt` before 8.0.0). .. versionchanged:: 5.9.6 function is now thread safe. @@ -200,14 +201,14 @@ CPU Return various CPU statistics as a named tuple. All fields are :term:`cumulative counters ` since boot. - - **ctx_switches**: number of :term:`context switches ` + - :field:`ctx_switches`: number of :term:`context switches ` (voluntary + involuntary). - - **interrupts**: + - :field:`interrupts`: number of :term:`hardware interrupts `. - - **soft_interrupts**: + - :field:`soft_interrupts`: number of :term:`soft interrupts `. Always set to ``0`` on Windows and SunOS. - - **syscalls**: number of system calls. Always set to ``0`` on Linux. + - :field:`syscalls`: number of system calls. Always set to ``0`` on Linux. Example (Linux): @@ -289,42 +290,43 @@ Memory Return statistics about system memory usage as a named tuple including the following fields, expressed in bytes. - - **total**: total physical memory (exclusive :term:`swap memory`). - - **available**: memory that can be given instantly to processes without the + - :field:`total`: total physical memory (exclusive :term:`swap memory`). + - :field:`available`: memory that can be given instantly to processes without the system going into :term:`swap `. On Linux it uses the ``MemAvailable`` field from ``/proc/meminfo`` *(kernel 3.14+)*; on older kernels it falls back to an estimate. This is the recommended field for monitoring actual memory usage in a cross-platform fashion. See :term:`available memory`. - - **percent**: the percentage usage calculated as + - :field:`percent`: the percentage usage calculated as ``(total - available) / total * 100``. - - **used**: memory in use, calculated differently depending on the platform + - :field:`used`: memory in use, calculated differently depending on the platform (see the table below). It is meant for informational purposes. Neither ``total - free`` nor ``total - available`` necessarily equals ``used``. - - **free**: memory not currently allocated to anything. This is - typically much lower than **available** because the OS keeps recently - freed memory as reclaimable cache (see **cached** and **buffers**) + - :field:`free`: memory not currently allocated to anything. This is + typically much lower than :field:`available` because the OS keeps recently + freed memory as reclaimable cache (see :field:`cached` and :field:`buffers`) rather than zeroing it immediately. Do not use this to check for - memory pressure; use **available** instead. - - **active** *(Linux, macOS, BSD)*: memory currently mapped by processes + memory pressure; use :field:`available` instead. + - :field:`active` *(Linux, macOS, BSD)*: memory currently mapped by processes or recently accessed, held in RAM. It is unlikely to be reclaimed unless the system is under significant memory pressure. - - **inactive** *(Linux, macOS, BSD)*: memory not recently accessed. It + - :field:`inactive` *(Linux, macOS, BSD)*: memory not recently accessed. It still holds valid data (:term:`page cache`, old allocations) but is a candidate for reclamation or swapping. On BSD systems it is counted in - **available**. - - **buffers** *(Linux, BSD)*: memory used by the kernel to cache disk + :field:`available`. + - :field:`buffers` *(Linux, BSD)*: memory used by the kernel to cache disk metadata (e.g. filesystem structures). Reclaimable by the OS when needed. - - **cached** *(Linux, BSD, Windows)*: RAM used by the kernel to cache file + - :field:`cached` *(Linux, BSD, Windows)*: RAM used by the kernel to cache file contents (data read from or written to disk). Reclaimable by the OS when needed. See :term:`page cache`. - - **shared** *(Linux, BSD)*: :term:`shared memory` accessible by multiple + - :field:`shared` *(Linux, BSD)*: :term:`shared memory` accessible by multiple processes simultaneously, such as in-memory ``tmpfs`` and POSIX shared memory objects (``shm_open``). On Linux this corresponds to ``Shmem`` in - ``/proc/meminfo`` and is already counted within **active** / **inactive**. - - **slab** *(Linux)*: memory used by the kernel's internal object caches + ``/proc/meminfo`` and is already counted within :field:`active` / + :field:`inactive`. + - :field:`slab` *(Linux)*: memory used by the kernel's internal object caches (e.g. inode and dentry caches). The reclaimable portion - (``SReclaimable``) is already included in **cached**. - - **wired** *(macOS, BSD, Windows)*: memory pinned in RAM by the kernel (e.g. + (``SReclaimable``) is already included in :field:`cached`. + - :field:`wired` *(macOS, BSD, Windows)*: memory pinned in RAM by the kernel (e.g. kernel code and critical data structures). It can never be moved to disk. Below is a table showing implementation details. All info on Linux is @@ -413,16 +415,16 @@ Memory .. note:: if you just want to know how much physical memory is left in a - cross-platform manner, simply rely on **available** and **percent** - fields. + cross-platform manner, simply rely on :field:`available` and + :field:`percent` fields. .. note:: - - On Linux, **total**, **free**, **used**, **shared**, and **available** - match the output of the ``free`` command. - - On macOS, **free**, **active**, **inactive**, and **wired** match - ``vm_stat`` output. - - On Windows, **total**, **used** ("In use"), and **available** match - the Task Manager (Performance > Memory tab). + - On Linux, :field:`total`, :field:`free`, :field:`used`, :field:`shared`, + and :field:`available` match the output of the ``free`` command. + - On macOS, :field:`free`, :field:`active`, :field:`inactive`, + and :field:`wired` match ``vm_stat`` output. + - On Windows, :field:`total`, :field:`used` ("In use"), and + :field:`available` match the Task Manager (Performance > Memory tab). .. seealso:: - `scripts/meminfo.py`_ @@ -430,33 +432,33 @@ Memory - :ref:`faq_used_plus_free` .. versionchanged:: 4.2.0 - Linux: added **shared** field. + Linux: added :field:`shared` field. .. versionchanged:: 5.4.4 - Linux: added **slab** field. + Linux: added :field:`slab` field. .. versionchanged:: 8.0.0 - Windows: added **cached** and **wired** fields. + Windows: added :field:`cached` and :field:`wired` fields. .. function:: swap_memory() - Return system :term:`swap memory` statistics as a named tuple - including the following fields: + Return system :term:`swap memory` statistics as a named tuple including the + following fields: - * **total**: total swap space. On Windows this is derived as + * :field:`total`: total swap space. On Windows this is derived as ``CommitLimit - PhysicalTotal``, representing virtual memory backed by the page file rather than the raw page-file size. - * **used**: swap space currently in use. - * **free**: swap space not in use (``total - used``). - * **percent**: swap usage as a percentage, calculated as + * :field:`used`: swap space currently in use. + * :field:`free`: swap space not in use (``total - used``). + * :field:`percent`: swap usage as a percentage, calculated as ``used / total * 100``. - * **sin**: number of bytes the system has moved from disk + * :field:`sin`: number of bytes the system has moved from disk (:term:`swap `) back into RAM. See :term:`swap-in`. - * **sout**: number of bytes the system has moved from RAM to disk + * :field:`sout`: number of bytes the system has moved from RAM to disk (:term:`swap `). A continuously increasing - **sout** rate is a sign of memory pressure. See :term:`swap-out`. + :field:`sout` rate is a sign of memory pressure. See :term:`swap-out`. - **sin** and **sout** are :term:`cumulative counters ` + :field:`sin` and :field:`sout` are :term:`cumulative counters ` since boot. Monitor their rate of change rather than the absolute value to detect active swapping. On Windows both are always ``0``. @@ -489,13 +491,13 @@ Disks Returns a list of named tuples with the following fields: - * **device**: the device path (e.g. "/dev/hda1"). On Windows this is the + * :field:`device`: the device path (e.g. "/dev/hda1"). On Windows this is the drive letter (e.g. "C:\\"). - * **mountpoint**: the mount point path (e.g. "/"). On Windows this is the + * :field:`mountpoint`: the mount point path (e.g. "/"). On Windows this is the drive letter (e.g. "C:\\"). - * **fstype**: the partition filesystem (e.g. "ext3" on UNIX or "NTFS" + * :field:`fstype`: the partition filesystem (e.g. "ext3" on UNIX or "NTFS" on Windows). - * **opts**: a comma-separated string indicating different mount options for + * :field:`opts`: a comma-separated string indicating different mount options for the drive/partition. Platform-dependent. .. code-block:: pycon @@ -508,16 +510,16 @@ Disks .. seealso:: `scripts/disk_usage.py`_. .. versionchanged:: 5.7.4 - added **maxfile** and **maxpath** fields. + added :field:`maxfile` and :field:`maxpath` fields. .. versionchanged:: 6.0.0 - removed **maxfile** and **maxpath** fields. + removed :field:`maxfile` and :field:`maxpath` fields. .. function:: disk_usage(path) Return disk usage statistics about the partition which contains the given - *path* as a named tuple including **total**, **used** and **free** space - expressed in bytes, plus the **percentage** usage. + *path* as a named tuple including :field:`total`, :field:`used` and + :field:`free` space expressed in bytes, plus the :field:`percentage` usage. This function was later incorporated in Python 3.3 as :func:`shutil.disk_usage` (`BPO-12442`_). @@ -533,38 +535,40 @@ Disks .. note:: UNIX usually reserves 5% of the total disk space for the root user. - **total** and **used** fields on UNIX refer to the overall total and used - space, whereas **free** represents the space available to - unprivileged users and **percent** represents unprivileged user + :field:`total` and :field:`used` fields on UNIX refer to the overall total and used + space, whereas :field:`free` represents the space available to + unprivileged users and :field:`percent` represents unprivileged user utilization (see `source code `_). - That is why the **percent** value may look 5% bigger than expected. + That is why the :field:`percent` value may look 5% bigger than expected. Also note that all four values match the "df" command-line utility. .. seealso:: `scripts/disk_usage.py`_. .. versionchanged:: 4.3.0 - **percent** value takes root reserved space into account. + :field:`percent` value takes root reserved space into account. .. function:: disk_io_counters(perdisk=False, nowrap=True) Return system-wide disk I/O statistics as a named tuple including the following fields: - - **read_count**: number of reads - - **write_count**: number of writes - - **read_bytes**: number of bytes read - - **write_bytes**: number of bytes written + - :field:`read_count`: number of reads + - :field:`write_count`: number of writes + - :field:`read_bytes`: number of bytes read + - :field:`write_bytes`: number of bytes written Platform-specific fields: - - **read_time**: (all except *NetBSD* and *OpenBSD*) time spent reading from - disk (in milliseconds) - - **write_time**: (all except *NetBSD* and *OpenBSD*) time spent writing to disk - (in milliseconds) - - **busy_time**: (*Linux*, *FreeBSD*) time spent doing actual I/Os (in + - :field:`read_time`: (all except *NetBSD* and *OpenBSD*) time spent reading + from disk (in milliseconds) + - :field:`write_time`: (all except *NetBSD* and *OpenBSD*) time spent writing + to disk (in milliseconds) + - :field:`busy_time`: (*Linux*, *FreeBSD*) time spent doing actual I/Os (in milliseconds). See :term:`busy_time`. - - **read_merged_count** (*Linux*): number of merged reads (see `iostats doc`_) - - **write_merged_count** (*Linux*): number of merged writes (see `iostats doc`_) + - :field:`read_merged_count` (*Linux*): number of merged reads + (see `iostats doc`_) + - :field:`write_merged_count` (*Linux*): number of merged writes + (see `iostats doc`_) If *perdisk* is ``True`` return the same information for every physical disk as a dictionary with partition names as the keys. @@ -602,11 +606,11 @@ Disks *nowrap* argument. .. versionchanged:: 4.0.0 - added **busy_time** (Linux, FreeBSD), **read_merged_count** and - **write_merged_count** (Linux) fields. + added :field:`busy_time` (Linux, FreeBSD), :field:`read_merged_count` and + :field:`write_merged_count` (Linux) fields. .. versionchanged:: 4.0.0 - NetBSD: removed **read_time** and **write_time** fields. + NetBSD: removed :field:`read_time` and :field:`write_time` fields. Network ^^^^^^^ @@ -616,16 +620,16 @@ Network Return system-wide network I/O statistics as a named tuple including the following attributes: - - **bytes_sent**: number of bytes sent - - **bytes_recv**: number of bytes received - - **packets_sent**: number of packets sent - - **packets_recv**: number of packets received - - **errin**: total number of errors while receiving - - **errout**: total number of errors while sending - - **dropin**: total number of incoming packets dropped at the - :term:`NIC` level. Unlike **errin**, drops indicate the interface or + - :field:`bytes_sent`: number of bytes sent + - :field:`bytes_recv`: number of bytes received + - :field:`packets_sent`: number of packets sent + - :field:`packets_recv`: number of packets received + - :field:`errin`: total number of errors while receiving + - :field:`errout`: total number of errors while sending + - :field:`dropin`: total number of incoming packets dropped at the + :term:`NIC` level. Unlike :field:`errin`, drops indicate the interface or kernel buffer was overwhelmed. - - **dropout**: total number of outgoing packets dropped (always 0 on macOS + - :field:`dropout`: total number of outgoing packets dropped (always 0 on macOS and BSD). A non-zero and growing count is a sign of network saturation. If *pernic* is ``True`` return the same information for every network @@ -660,19 +664,19 @@ Network Return system-wide socket connections as a list of named tuples. Every named tuple provides 7 attributes: - - **fd**: the socket :term:`file descriptor`; ``-1`` on Windows and SunOS. - - **family**: the address family, either :data:`socket.AF_INET`, + - :field:`fd`: the socket :term:`file descriptor`; ``-1`` on Windows and SunOS. + - :field:`family`: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6` or :data:`socket.AF_UNIX`. - - **type**: the address type, either :data:`socket.SOCK_STREAM`, + - :field:`type`: the address type, either :data:`socket.SOCK_STREAM`, :data:`socket.SOCK_DGRAM` or :data:`socket.SOCK_SEQPACKET`. - - **laddr**: the local address as a ``(ip, port)`` named tuple or a ``path`` - for :data:`socket.AF_UNIX` sockets (see notes below). - - **raddr**: the remote address, either an empty tuple (``AF_INET*``) or + - :field:`laddr`: the local address as a ``(ip, port)`` named tuple or a + ``path`` for :data:`socket.AF_UNIX` sockets (see notes below). + - :field:`raddr`: the remote address, either an empty tuple (``AF_INET*``) or ``""`` (``AF_UNIX``) when not connected. For UNIX sockets see notes below. - - **status**: a :data:`psutil.CONN_* ` constant; + - :field:`status`: a :data:`psutil.CONN_* ` constant; always :const:`psutil.CONN_NONE` for UDP and UNIX sockets. - - **pid**: PID of the process which opened the socket. Set to ``None`` if it - can't be retrieved due to insufficient permissions (e.g. Linux). + - :field:`pid`: PID of the process which opened the socket. Set to ``None`` + if it can't be retrieved due to insufficient permissions (e.g. Linux). The *kind* parameter is a string which filters for connections matching the following criteria: @@ -735,34 +739,35 @@ Network .. versionadded:: 2.1.0 .. versionchanged:: 5.3.0 - socket **fd** is now set for real instead of being ``-1``. + socket :field:`fd` is now set for real instead of being ``-1``. .. versionchanged:: 5.3.0 - **laddr** and **raddr** are named tuples. + :field:`laddr` and :field:`raddr` are named tuples. .. versionchanged:: 5.9.5 - OpenBSD: retrieve **laddr** path for :data:`socket.AF_UNIX` sockets + OpenBSD: retrieve :field:`laddr` path for :data:`socket.AF_UNIX` sockets (before it was an empty string). .. versionchanged:: 8.0.0 - **status** field is now a :class:`psutil.ConnectionStatus` enum member + :field:`status` field is now a :class:`psutil.ConnectionStatus` enum member instead of a plain ``str``. See :ref:`migration guide `. .. function:: net_if_addrs() + Return a dictionary mapping each :term:`NIC` to a list of named tuples representing its addresses. Multiple addresses of the same family can exist per interface. Each named tuple includes 5 fields (addresses may be ``None``): - - **family**: the address family, either :data:`socket.AF_INET`, + - :field:`family`: the address family, either :data:`socket.AF_INET`, :data:`socket.AF_INET6`, :const:`psutil.AF_LINK` (a MAC address) or :data:`socket.AF_UNSPEC` (a virtual or unconfigured NIC). - - **address**: the primary NIC address - - **netmask**: the netmask address - - **broadcast**: the broadcast address; always ``None`` on Windows - - **ptp**: a "point to point" address (typically a VPN); always ``None`` on + - :field:`address`: the primary NIC address + - :field:`netmask`: the netmask address + - :field:`broadcast`: the broadcast address; always ``None`` on Windows + - :field:`ptp`: a "point to point" address (typically a VPN); always ``None`` on Windows .. code-block:: pycon @@ -782,25 +787,25 @@ Network .. versionadded:: 3.0.0 .. versionchanged:: 3.2.0 - added **ptp** field. + added :field:`ptp` field. .. versionchanged:: 4.4.0 - Windows: added support for **netmask** field, which is no longer ``None``. + Windows: added support for :field:`netmask` field, which is no longer ``None``. .. versionchanged:: 7.0.0 - Windows: added support for **broadcast** field, which is no longer ``None``. + Windows: added support for :field:`broadcast` field, which is no longer ``None``. .. function:: net_if_stats() Return a dictionary mapping each :term:`NIC` to a named tuple with the following fields: - - **isup**: whether the NIC is up and running (bool). - - **duplex**: :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or + - :field:`isup`: whether the NIC is up and running (bool). + - :field:`duplex`: :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or :const:`NIC_DUPLEX_UNKNOWN`. - - **speed**: NIC speed in megabits (Mbps); ``0`` if undetermined. - - **mtu**: maximum transmission unit in bytes. - - **flags**: a comma-separated string of interface flags (e.g. + - :field:`speed`: NIC speed in megabits (Mbps); ``0`` if undetermined. + - :field:`mtu`: maximum transmission unit in bytes. + - :field:`flags`: a comma-separated string of interface flags (e.g. ``"up,broadcast,running,multicast"``); may be an emty string. .. code-block:: pycon @@ -815,10 +820,10 @@ Network .. versionadded:: 3.0.0 .. versionchanged:: 5.7.3 - UNIX: **isup** also reflects whether the :term:`NIC` is running. + UNIX: :field:`isup` also reflects whether the :term:`NIC` is running. .. versionchanged:: 5.9.3 - added **flags** field. + added :field:`flags` field. Sensors ^^^^^^^ @@ -833,11 +838,11 @@ Sensors If sensors are not supported by the OS an empty dict is returned. Each named tuple includes 4 fields: - - **label**: a string label for the sensor, if available, else ``""``. - - **current**: current temperature, or ``None`` if not available. - - **high**: temperature at which the system will throttle, or ``None`` + - :field:`label`: a string label for the sensor, if available, else ``""``. + - :field:`current`: current temperature, or ``None`` if not available. + - :field:`high`: temperature at which the system will throttle, or ``None`` if not available. - - **critical**: temperature at which the system will shut down, or + - :field:`critical`: temperature at which the system will shut down, or ``None`` if not available. .. code-block:: pycon @@ -886,14 +891,14 @@ Sensors values. If no battery is installed or metrics can't be determined ``None`` is returned. - - **percent**: battery power left as a percentage. - - **secsleft**: a rough approximation of how many seconds are left before the + - :field:`percent`: battery power left as a percentage. + - :field:`secsleft`: a rough approximation of how many seconds are left before the battery runs out of power. If the AC power cable is connected this is set to :data:`psutil.POWER_TIME_UNLIMITED `. If it can't be determined it is set to :data:`psutil.POWER_TIME_UNKNOWN `. - - **power_plugged**: ``True`` if the AC power cable is connected, ``False`` + - :field:`power_plugged`: ``True`` if the AC power cable is connected, ``False`` if not or ``None`` if it can't be determined. .. code-block:: pycon @@ -949,13 +954,13 @@ Other system info Return users currently connected on the system as a list of named tuples including the following fields: - - **name**: the name of the user. - - **terminal**: the tty or pseudo-tty associated with the user, if any, + - :field:`name`: the name of the user. + - :field:`terminal`: the tty or pseudo-tty associated with the user, if any, else ``None``. - - **host**: the host name associated with the entry, if any, else ``None``. - - **started**: the creation time as a floating point number expressed in + - :field:`host`: the host name associated with the entry, if any, else ``None``. + - :field:`started`: the creation time as a floating point number expressed in seconds since the epoch. - - **pid**: the PID of the login process (like sshd, tmux, gdm-session-worker, + - :field:`pid`: the PID of the login process (like sshd, tmux, gdm-session-worker, ...). On Windows and OpenBSD this is always set to ``None``. .. code-block:: pycon @@ -966,7 +971,7 @@ Other system info suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] .. versionchanged:: 5.3.0 - added **pid** field. + added :field:`pid` field. ------------------------------------------------------------------------------- @@ -1541,11 +1546,11 @@ Process class every time. Use it with care as it can starve the entire system. Additional priority *level* can be specified and ranges from ``0`` (highest) to ``7`` (lowest). - * :const:`IOPRIO_CLASS_BE`: (normal) the default for any process that hasn't set - a specific I/O priority. Additional priority *level* ranges from + * :const:`IOPRIO_CLASS_BE`: (normal) the default for any process that hasn't + set a specific I/O priority. Additional priority *level* ranges from ``0`` (highest) to ``7`` (lowest). - * :const:`IOPRIO_CLASS_IDLE`: (low) get I/O time when no-one else needs the disk. - No additional *value* is accepted. + * :const:`IOPRIO_CLASS_IDLE`: (low) get I/O time when no-one else needs the + disk. No additional *value* is accepted. * :const:`IOPRIO_CLASS_NONE`: returned when no priority was previously set. Windows: @@ -1613,31 +1618,34 @@ Process class For Linux you can refer to `/proc filesystem documentation `_. - All fields are :term:`cumulative counters ` since process creation. + All fields are :term:`cumulative counters ` since + process creation. - - **read_count**: the number of read operations performed. + - :field:`read_count`: the number of read operations performed. This is supposed to count the number of read-related syscalls such as ``read()`` and ``pread()`` on UNIX. - - **write_count**: the number of write operations performed. + - :field:`write_count`: the number of write operations performed. This is supposed to count the number of write-related syscalls such as ``write()`` and ``pwrite()`` on UNIX. - - **read_bytes**: the number of bytes read. Always ``-1`` on BSD. - - **write_bytes**: the number of bytes written. Always ``-1`` on BSD. + - :field:`read_bytes`: the number of bytes read. Always ``-1`` on BSD. + - :field:`write_bytes`: the number of bytes written. Always ``-1`` on BSD. Linux specific: - - **read_chars** *(Linux)*: the amount of bytes which this process passed - to ``read()`` and ``pread()`` syscalls. Differently from *read_bytes* - it doesn't care whether or not actual physical disk I/O occurred. - - **write_chars** *(Linux)*: the amount of bytes which this process passed - to ``write()`` and ``pwrite()`` syscalls. Differently from *write_bytes* - it doesn't care whether or not actual physical disk I/O occurred. + - :field:`read_chars` *(Linux)*: the amount of bytes which this process + passed to ``read()`` and ``pread()`` syscalls. Differently from + :field:`read_bytes` it doesn't care whether or not actual physical disk + I/O occurred. + - :field:`write_chars` *(Linux)*: the amount of bytes which this process + passed to ``write()`` and ``pwrite()`` syscalls. Differently from + :field:`write_bytes` it doesn't care whether or not actual physical disk + I/O occurred. Windows specific: - - **other_count** *(Windows)*: the number of I/O operations performed + - :field:`other_count` *(Windows)*: the number of I/O operations performed other than read and write operations. - - **other_bytes** *(Windows)*: the number of bytes transferred during + - :field:`other_bytes` *(Windows)*: the number of bytes transferred during operations other than read and write operations. .. code-block:: pycon @@ -1650,10 +1658,10 @@ Process class .. availability:: Linux, Windows, BSD, AIX .. versionchanged:: 5.2.0 - Linux: added **read_chars** and **write_chars** fields. + Linux: added :field:`read_chars` and :field:`write_chars` fields. .. versionchanged:: 5.2.0 - Windows: added **other_count** and **other_bytes** fields. + Windows: added :field:`other_count` and :field:`other_bytes` fields. .. method:: num_ctx_switches() @@ -1662,8 +1670,8 @@ Process class (:term:`cumulative counter`). .. note:: - (Windows, macOS) **involuntary** value is always set to 0, while - **voluntary** value reflects the total number of context switches + (Windows, macOS) :field:`involuntary` value is always set to 0, while + :field:`voluntary` value reflects the total number of context switches (voluntary + involuntary). This is a limitation of the OS. .. versionchanged:: 5.4.1 @@ -1671,14 +1679,15 @@ Process class .. method:: num_fds() - The number of :term:`file descriptors ` currently opened by this process - (non cumulative). + The number of :term:`file descriptors ` currently opened + by this process (non cumulative). .. availability:: UNIX .. method:: num_handles() - The number of :term:`handles ` currently used by this process (non cumulative). + The number of :term:`handles ` currently used by this process (non + cumulative). .. availability:: Windows @@ -1691,13 +1700,12 @@ Process class Return threads opened by process as a list of named tuples. On OpenBSD this method requires root privileges. - - **id**: the native thread ID assigned by the kernel. If :attr:`pid` refers - to the current process, this matches the - `native_id `_ - attribute of the :class:`threading.Thread` class, and can be used to reference + - :field:`id`: the native thread ID assigned by the kernel. If :attr:`pid` + refers to the current process this matches + :attr:`threading.Thread.native_id`, and can be used to reference individual Python threads running within your own Python app. - - **user_time**: time spent in user mode. - - **system_time**: time spent in kernel mode. + - :field:`user_time`: time spent in user mode. + - :field:`system_time`: time spent in kernel mode. .. method:: cpu_times() @@ -1706,15 +1714,15 @@ Process class (see `explanation `_). This is similar to :func:`os.times` but can be used for any process PID. - - **user**: time spent in user mode. - - **system**: time spent in kernel mode. - - **children_user**: user time of all child processes (always ``0`` on + - :field:`user`: time spent in user mode. + - :field:`system`: time spent in kernel mode. + - :field:`children_user`: user time of all child processes (always ``0`` on Windows and macOS). - - **children_system**: system time of all child processes (always ``0`` on - Windows and macOS). - - **iowait**: (Linux) time spent waiting for blocking I/O to complete (:term:`iowait`). - This value is excluded from `user` and `system` times count (because the - CPU is not doing any work). + - :field:`children_system`: system time of all child processes (always + ``0`` on Windows and macOS). + - :field:`iowait`: (Linux) time spent waiting for blocking I/O to complete + (:term:`iowait`). This value is excluded from `user` and `system` times + count (because the CPU is not doing any work). .. code-block:: pycon @@ -1726,10 +1734,10 @@ Process class 0.70 .. versionchanged:: 4.1.0 - added **children_user** and **children_system** fields. + added :field:`children_user` and :field:`children_system` fields. .. versionchanged:: 5.6.4 - Linux: added **iowait** field. + Linux: added :field:`iowait` field. .. method:: cpu_percent(interval=None) @@ -1815,8 +1823,8 @@ Process class Return a named tuple with variable fields depending on the platform representing memory information about the process. - The "portable" fields available on all platforms are **rss** and **vms**. - All numbers are expressed in bytes. + The "portable" fields available on all platforms are :field:`rss` and + :field:`vms`. All numbers are expressed in bytes. +---------+---------+----------+---------+-----+-----------------+ | Linux | macOS | BSD | Solaris | AIX | Windows | @@ -1836,36 +1844,36 @@ Process class | | | | | | peak_vms | +---------+---------+----------+---------+-----+-----------------+ - - **rss**: aka :term:`RSS`. On UNIX matches the ``top`` RES column. On + - :field:`rss`: aka :term:`RSS`. On UNIX matches the ``top`` RES column. On Windows maps to ``WorkingSetSize``. - - **vms**: aka :term:`VMS`. On UNIX matches the ``top`` VIRT column. On + - :field:`vms`: aka :term:`VMS`. On UNIX matches the ``top`` VIRT column. On Windows maps to ``PrivateUsage`` (private committed pages only), which - differs from the UNIX definition; use **virtual** from + differs from the UNIX definition; use :field:`virtual` from :meth:`memory_info_ex` for the true virtual address space size. - - **shared** *(Linux)*: :term:`shared memory` that *could* be shared with - other processes (shared libraries, mmap'd files, POSIX shared memory). + - :field:`shared` *(Linux)*: :term:`shared memory` that *could* be shared + with other processes (shared libraries, mmap'd files, POSIX shared memory). Counted even if no other process is currently mapping it. Matches ``top``'s SHR column. - - **text** *(Linux, BSD)*: aka TRS (Text Resident Set). Resident memory + - :field:`text` *(Linux, BSD)*: aka TRS (Text Resident Set). Resident memory devoted to executable code. This memory is read-only and typically shared across all processes running the same binary. Matches ``top``'s CODE column. - - **data** *(Linux, BSD)*: aka DRS (Data Resident Set). On Linux this + - :field:`data` *(Linux, BSD)*: aka DRS (Data Resident Set). On Linux this covers the data **and** stack segments combined (from ``/proc//statm``). On BSD it covers the data segment only (see - **stack**). Matches ``top``'s DATA column. + :field:`stack`). Matches ``top``'s DATA column. - - **stack** *(BSD)*: size of the process stack segment. Reported - separately from **data** (unlike Linux where both are combined). + - :field:`stack` *(BSD)*: size of the process stack segment. Reported + separately from :field:`data` (unlike Linux where both are combined). - - **peak_rss** *(BSD, Windows)*: see :term:`peak_rss`. On BSD may be ``0`` - for kernel PIDs. On Windows maps to ``PeakWorkingSetSize``. + - :field:`peak_rss` *(BSD, Windows)*: see :term:`peak_rss`. On BSD may be + ``0`` for kernel PIDs. On Windows maps to ``PeakWorkingSetSize``. - - **peak_vms** *(Windows)*: see :term:`peak_vms`. Maps to + - :field:`peak_vms` *(Windows)*: see :term:`peak_vms`. Maps to ``PeakPagefileUsage``. For the full definitions of Windows fields see @@ -1886,29 +1894,33 @@ Process class - :ref:`faq_memory_footprint` .. versionchanged:: 4.0.0 - multiple fields are returned, not only **rss** and **vms**. + multiple fields are returned, not only :field:`rss` and :field:`vms`. .. versionchanged:: 8.0.0 - Linux: **lib** and **dirty** removed (always 0 since Linux 2.6). Deprecated - aliases returning 0 and emitting `DeprecationWarning` are kept. - See :ref:`migration guide `. + Linux: :field:`lib` and :field:`dirty` removed (always 0 since Linux + 2.6). Deprecated aliases returning 0 and emitting :exc:`DeprecationWarning` + are kept. See :ref:`migration guide `. .. versionchanged:: 8.0.0 - macOS: removed **pfaults** and **pageins** fields with **no + macOS: removed :field:`pfaults` and :field:`pageins` fields with **no backward-compatible aliases**. Use :meth:`page_faults` instead. See :ref:`migration guide `. .. versionchanged:: 8.0.0 - Windows: eliminated old aliases: **wset** → **rss**, **peak_wset** → - **peak_rss**, **pagefile** / **private** → **vms**, **peak_pagefile** → - **peak_vms**, **num_page_faults** → :meth:`page_faults` method. At the same - time **paged_pool**, **nonpaged_pool**, **peak_paged_pool**, - **peak_nonpaged_pool** were moved to :meth:`memory_info_ex`. All these old - names still work but raise `DeprecationWarning`. + Windows: eliminated old aliases: + :field:`wset` → :field:`rss`, + :field:`peak_wset` → :field:`peak_rss`, + :field:`pagefile` and :field:`private` → :field:`vms`, + :field:`peak_pagefile` → :field:`peak_vms`, + :field:`num_page_faults` → :meth:`page_faults` method. + At the same time :field:`paged_pool`, :field:`nonpaged_pool`, + :field:`peak_paged_pool`, :field:`peak_nonpaged_pool` were moved to + :meth:`memory_info_ex`. + All these old names still work but raise :exc:`DeprecationWarning`. See :ref:`migration guide `. .. versionchanged:: 8.0.0 - BSD: added **peak_rss** field. + BSD: added :field:`peak_rss` field. .. method:: memory_info_ex() @@ -1935,41 +1947,43 @@ Process class | hugetlb | phys_footprint | | +-------------+----------------+--------------------+ - - **peak_rss** *(Linux, macOS)*: see :term:`peak_rss`. - - **peak_vms** *(Linux)*: see :term:`peak_vms`. - - **rss_anon** *(Linux, macOS)*: resident :term:`anonymous memory` + - :field:`peak_rss` *(Linux, macOS)*: see :term:`peak_rss`. + - :field:`peak_vms` *(Linux)*: see :term:`peak_vms`. + - :field:`rss_anon` *(Linux, macOS)*: resident :term:`anonymous memory` (heap, stack, private mappings) not backed by any file. Set to 0 on Linux < 4.5. - - **rss_file** *(Linux, macOS)*: resident file-backed memory mapped + - :field:`rss_file` *(Linux, macOS)*: resident file-backed memory mapped from files (shared libraries, mmap'd files). Set to 0 on Linux < 4.5. - - **rss_shmem** *(Linux)*: resident :term:`shared memory` (``tmpfs``, - ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals **rss**. Set to + - :field:`rss_shmem` *(Linux)*: resident :term:`shared memory` (``tmpfs``, + ``shm_open``). ``rss_anon + rss_file + rss_shmem`` equals :field:`rss`. Set to 0 on Linux < 4.5. - - **wired** *(macOS)*: memory pinned in RAM by the kernel on behalf of this + - :field:`wired` *(macOS)*: memory pinned in RAM by the kernel on behalf of this process; cannot be compressed or paged out. - - **swap** *(Linux)*: process memory currently in :term:`swap `. - Equivalent to ``memory_footprint().swap`` but cheaper, as it reads from - ``/proc//status`` instead of ``/proc//smaps``. - - **compressed** *(macOS)*: memory held in the in-RAM memory compressor; - not counted in **rss**. A large value signals memory pressure but has + - :field:`swap` *(Linux)*: process memory currently in + :term:`swap `. Equivalent to ``memory_footprint().swap`` but + cheaper, as it reads from ``/proc//status`` instead of + ``/proc//smaps``. + - :field:`compressed` *(macOS)*: memory held in the in-RAM memory compressor; + not counted in :field:`rss`. A large value signals memory pressure but has not yet triggered swapping. - - **hugetlb** *(Linux)*: resident memory backed by huge pages. Set to 0 on - Linux < 4.4. - - **phys_footprint** *(macOS)*: total physical memory impact including + - :field:`hugetlb` *(Linux)*: resident memory backed by huge pages. Set to + 0 on Linux < 4.4. + - :field:`phys_footprint` *(macOS)*: total physical memory impact including compressed memory. What Xcode and ``footprint(1)`` report; prefer this - over **rss** macOS memory monitoring. - - **virtual** *(Windows)*: true virtual address space size, including - reserved-but-uncommitted regions (unlike **vms** in + over :field:`rss` macOS memory monitoring. + - :field:`virtual` *(Windows)*: true virtual address space size, including + reserved-but-uncommitted regions (unlike :field:`vms` in :meth:`memory_info`). - - **peak_virtual** *(Windows)*: peak virtual address space size. - - **paged_pool** *(Windows)*: kernel memory used for objects created by + - :field:`peak_virtual` *(Windows)*: peak virtual address space size. + - :field:`paged_pool` *(Windows)*: kernel memory used for objects created by this process (open file handles, registry keys, etc.) that the OS may swap to disk under memory pressure. - - **nonpaged_pool** *(Windows)*: kernel memory used for objects that must - stay in RAM at all times (I/O request packets, device driver buffers, - etc.). A large or growing value may indicate a driver memory leak. - - **peak_paged_pool** *(Windows)*: peak paged-pool usage. - - **peak_nonpaged_pool** *(Windows)*: peak non-paged-pool usage. + - :field:`nonpaged_pool` *(Windows)*: kernel memory used for objects that + must stay in RAM at all times (I/O request packets, device driver + buffers, etc.). A large or growing value may indicate a driver memory + leak. + - :field:`peak_paged_pool` *(Windows)*: peak paged-pool usage. + - :field:`peak_nonpaged_pool` *(Windows)*: peak non-paged-pool usage. For the full definitions of Windows fields see `PROCESS_MEMORY_COUNTERS_EX`_. @@ -1985,15 +1999,15 @@ Process class It walks the full process address space, so it is slower than :meth:`memory_info` and may require elevated privileges. - - **uss** *(Linux, macOS, Windows)*: aka :term:`USS`; the + - :field:`uss` *(Linux, macOS, Windows)*: aka :term:`USS`; the :term:`private memory` of the process, which would be freed if the process were terminated right now. - - **pss** *(Linux)*: aka :term:`PSS`; shared memory divided evenly among + - :field:`pss` *(Linux)*: aka :term:`PSS`; shared memory divided evenly among the processes sharing it. I.e. if a process has 10 MBs all to itself, and 10 MBs shared with another process, its PSS will be 15 MBs. - - **swap** *(Linux)*: process memory currently in :term:`swap `, + - :field:`swap` *(Linux)*: process memory currently in :term:`swap `, counted per-mapping. Example on Linux: @@ -2070,28 +2084,28 @@ Process class Linux fields (from ``/proc//smaps``): - - **rss**: :term:`RSS` for this mapping. - - **size**: total virtual size; may far exceed **rss** if parts have - never been accessed. - - **pss**: :term:`PSS` for this mapping, that is **rss** split + - :field:`rss`: :term:`RSS` for this mapping. + - :field:`size`: total virtual size; may far exceed :field:`rss` if parts + have never been accessed. + - :field:`pss`: :term:`PSS` for this mapping, that is :field:`rss` split proportionally among all processes sharing it. - - **shared_clean**: :term:`shared memory` not written to since loaded + - :field:`shared_clean`: :term:`shared memory` not written to since loaded (clean); can be discarded and reloaded from disk for free. - - **shared_dirty**: :term:`shared memory` that has been written to (dirty). - - **private_clean**: :term:`private memory` not written to (clean). - - **private_dirty**: :term:`private memory` that has been written to + - :field:`shared_dirty`: :term:`shared memory` that has been written to (dirty). + - :field:`private_clean`: :term:`private memory` not written to (clean). + - :field:`private_dirty`: :term:`private memory` that has been written to (dirty); must be saved to swap before it can be freed. The key indicator of real memory cost. - - **referenced**: bytes recently accessed. - - **anonymous**: :term:`anonymous memory` in this mapping (heap, stack). - - **swap**: bytes from this mapping currently in + - :field:`referenced`: bytes recently accessed. + - :field:`anonymous`: :term:`anonymous memory` in this mapping (heap, stack). + - :field:`swap`: bytes from this mapping currently in :term:`swap `. FreeBSD fields: - - **private**: :term:`private memory` in this mapping. - - **ref_count**: reference count on the underlying memory object. - - **shadow_count**: depth of the copy-on-write chain. + - :field:`private`: :term:`private memory` in this mapping. + - :field:`ref_count`: reference count on the underlying memory object. + - :field:`shadow_count`: depth of the copy-on-write chain. .. code-block:: pycon @@ -2145,15 +2159,16 @@ Process class Return the number of :term:`page faults ` for this process as a ``(minor, major)`` named tuple. - - **minor** (a.k.a. *soft* faults): occur when a memory page is not + - :field:`minor` (a.k.a. *soft* faults): occur when a memory page is not currently mapped into the process address space, but is already present in physical RAM (e.g. a shared library loaded by another process). The kernel resolves these without disk I/O. - - **major** (a.k.a. *hard* faults): occur when the page must be fetched + - :field:`major` (a.k.a. *hard* faults): occur when the page must be fetched from disk. These are expensive because they stall the process until I/O completes. - Both counters are :term:`cumulative counters ` since process creation. + Both counters are :term:`cumulative counters ` since + process creation. .. code-block:: pycon @@ -2169,19 +2184,19 @@ Process class Return regular files opened by process as a list of named tuples including the following fields: - - **path**: the absolute file name. - - **fd**: the :term:`file descriptor` number; on Windows this is always + - :field:`path`: the absolute file name. + - :field:`fd`: the :term:`file descriptor` number; on Windows this is always ``-1``. Linux only: - - **position** (*Linux*): the file (offset) position. - - **mode** (*Linux*): a string indicating how the file was opened, similarly - to :func:`open` builtin *mode* argument. + - :field:`position` (*Linux*): the file (offset) position. + - :field:`mode` (*Linux*): a string indicating how the file was opened, + similarly to :func:`open` builtin *mode* argument. Possible values are ``'r'``, ``'w'``, ``'a'``, ``'r+'`` and ``'a+'``. There's no distinction between files opened in binary or text mode (``"b"`` or ``"t"``). - - **flags** (*Linux*): the flags which were passed to the underlying + - :field:`flags` (*Linux*): the flags which were passed to the underlying :func:`os.open` C call when the file was opened (e.g. :data:`os.O_RDONLY`, :data:`os.O_TRUNC`, etc). @@ -2203,12 +2218,12 @@ Process class no longer hangs on Windows. .. versionchanged:: 4.1.0 - Linux: added **position**, **mode** and **flags** fields. + Linux: added :field:`position`, :field:`mode` and :field:`flags` fields. .. method:: net_connections(kind="inet") Same as :func:`psutil.net_connections` but for this process only (the - returned named tuples have no **pid** field). The *kind* parameter and + returned named tuples have no :field:`pid` field). The *kind* parameter and the same limitations apply (root may be needed on some platforms). .. code-block:: pycon @@ -2467,8 +2482,8 @@ Python's memory tracking misses. the :term:`heap` (typically small ``malloc()`` allocations). In practice, modern allocators rarely comply, so this is not a - general-purpose memory-reduction tool and won't meaningfully shrink :term:`RSS` in - real programs. Its primary value is in **leak detection tools**. + general-purpose memory-reduction tool and won't meaningfully shrink :term:`RSS` + in real programs. Its primary value is in **leak detection tools**. Calling ``heap_trim()`` before taking measurements helps reduce allocator noise, giving you a cleaner baseline so that changes in ``heap_used`` come @@ -2631,7 +2646,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. class:: psutil.ConnectionStatus :class:`enum.StrEnum` collection of :data:`CONN_* ` - constants. Returned in the **status** field of + constants. Returned in the :field:`status` field of :func:`psutil.net_connections` and :meth:`Process.net_connections`. .. versionadded:: 8.0.0 @@ -2896,7 +2911,7 @@ Connections constants :class:`enum.StrEnum` constants representing the status of a TCP connection. Returned by :meth:`Process.net_connections` and :func:`psutil.net_connections` -(**status** field). +(:field:`status` field). .. versionchanged:: 8.0.0 constants are now :class:`psutil.ConnectionStatus` enum members (were @@ -2927,7 +2942,7 @@ Hardware constants .. data:: AF_LINK Identifies a MAC address associated with a network interface. Returned by - :func:`psutil.net_if_addrs` (**family** field). + :func:`psutil.net_if_addrs` (:field:`family` field). .. versionadded:: 3.0.0 @@ -2940,7 +2955,7 @@ Hardware constants Identifies whether a :term:`NIC` operates in full, half, or unknown duplex mode. FULL allows simultaneous send/receive, HALF allows only one at a time. - Returned by :func:`psutil.net_if_stats` (**duplex** field). + Returned by :func:`psutil.net_if_stats` (:field:`duplex` field). .. versionadded:: 3.0.0 @@ -2950,7 +2965,7 @@ Hardware constants .. data:: POWER_TIME_UNLIMITED Whether the remaining time of a battery cannot be determined or is unlimited. - May be assigned to :func:`psutil.sensors_battery`'s **secsleft** field. + May be assigned to :func:`psutil.sensors_battery`'s :field:`secsleft` field. .. versionadded:: 5.1.0 diff --git a/docs/changelog.rst b/docs/changelog.rst index 3b38b2f862..b4050db2a0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -101,32 +101,37 @@ New APIs: removed in 7.0), which extends :meth:`Process.memory_info` with platform-specific metrics: - - Linux: *peak_rss*, *peak_vms*, *rss_anon*, *rss_file*, *rss_shmem*, - *swap*, *hugetlb* - - macOS: *peak_rss*, *rss_anon*, *rss_file*, *wired*, *compressed*, - *phys_footprint* - - Windows: *virtual*, *peak_virtual* - - - Add new :meth:`Process.memory_footprint` method, which returns *uss*, - *pss* and *swap* metrics (what :meth:`Process.memory_full_info` used to - return, which is now **deprecated**, see + - Linux: :field:`peak_rss`, :field:`peak_vms`, :field:`rss_anon`, + :field:`rss_file`, :field:`rss_shmem`, :field:`swap`, :field:`hugetlb` + - macOS: :field:`peak_rss`, :field:`rss_anon`, :field:`rss_file`, + :field:`wired`, :field:`compressed`, :field:`phys_footprint` + - Windows: :field:`virtual`, :field:`peak_virtual` + + - Add new :meth:`Process.memory_footprint` method, which returns :field:`uss`, + :field:`pss` and :field:`swap` metrics (what :meth:`Process.memory_full_info` + used to return, which is now **deprecated**, see :ref:`migration guide `). - :meth:`Process.memory_info` named tuple changed: - - BSD: added *peak_rss*. + - BSD: added :field:`peak_rss`. - - Linux: *lib* and *dirty* removed (always 0 since Linux 2.6). Deprecated - aliases returning 0 and emitting `DeprecationWarning` are kept. + - Linux: :field:`lib` and :field:`dirty` removed (always 0 since Linux 2.6). + Deprecated aliases returning 0 and emitting :exc:`DeprecationWarning` are + kept. - - macOS: *pfaults* and *pageins* removed with **no + - macOS: :field:`pfaults` and :field:`pageins` removed with **no backward-compatible aliases**. Use :meth:`Process.page_faults` instead. - - Windows: eliminated old aliases: *wset* → *rss*, *peak_wset* → - *peak_rss*, *pagefile* / *private* → *vms*, *peak_pagefile* → *peak_vms*. - At the same time *paged_pool*, *nonpaged_pool*, *peak_paged_pool*, - *peak_nonpaged_pool* were moved to :meth:`Process.memory_info_ex`. All - these old names still work but raise `DeprecationWarning`. + - Windows: eliminated old aliases: + :field:`wset` → :field:`rss`, + :field:`peak_wset` → :field:`peak_rss`, + :field:`pagefile` and :field:`private` → :field:`vms`, + :field:`peak_pagefile` → :field:`peak_vms`. + At the same time :field:`paged_pool`, :field:`nonpaged_pool`, + :field:`peak_paged_pool`, :field:`peak_nonpaged_pool` were moved to + :meth:`Process.memory_info_ex`. + All these old names still work but raise :exc:`DeprecationWarning`. See :ref:`migration guide `. - :meth:`Process.memory_full_info` is **deprecated**. Use the new @@ -137,19 +142,19 @@ Others: - :gh:`2747`: the field order of the named tuple returned by :func:`cpu_times` has been normalized on all platforms, and the first 3 fields are now always - ``user, system, idle``. See compatibility notes below. -- :gh:`2754`: standardize :func:`sensors_battery`'s `percent` so that it + :field:`user`, :field:`system`, :field:`idle`. See compatibility notes below. +- :gh:`2754`: standardize :func:`sensors_battery`'s :field:`percent` so that it returns a `float` instead of `int` on all systems, not only Linux. - :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update ``changelog.rst`` and ``credits.rst`` when commenting with /changelog. - :gh:`2766`: remove remaining Python 2.7 compatibility shims from ``setup.py``, simplifying the build infrastructure. -- :gh:`2772`, [Windows]: :func:`cpu_times` ``interrupt`` field renamed to - ``irq`` to match the field name used on Linux and BSD. ``interrupt`` still - works but raises :exc:`DeprecationWarning`. +- :gh:`2772`, [Windows]: :func:`cpu_times` :field:`interrupt` field renamed to + :field:`irq` to match the field name used on Linux and BSD. :field:`interrupt` + still works but raises :exc:`DeprecationWarning`. See :ref:`migration guide `. -- :gh:`2776`, [Windows]: :func:`virtual_memory` now includes ``cached`` and - ``wired`` fields. +- :gh:`2776`, [Windows]: :func:`virtual_memory` now includes :field:`cached` and + :field:`wired` fields. - :gh:`2780`, [Windows]: :func:`disk_usage` now can accept a file path (not only a directory path). - :gh:`2784`: :func:`process_iter`: when *attrs* is specified, the pre-fetched @@ -469,7 +474,7 @@ Others: **Enhancements** -- :gh:`2109`: ``maxfile`` and ``maxpath`` fields were removed from the +- :gh:`2109`: :field:`maxfile` and :field:`maxpath` fields were removed from the named tuple returned by :func:`disk_partitions`. Reason: on network filesystems (NFS) this can potentially take a very long time to complete. - :gh:`2366`, [Windows]: log debug message when using slower process APIs. @@ -484,7 +489,7 @@ Others: Gross) - :gh:`2407`: :meth:`Process.connections` was renamed to :meth:`Process.net_connections`. The old name is still available, but it's - deprecated (triggers a ``DeprecationWarning``) and will be removed in the + deprecated (triggers a :exc:`DeprecationWarning`) and will be removed in the future. - :gh:`2425`: [Linux]: provide aarch64 wheels. (patch by Matthieu Darbois / Ben Raz) @@ -513,7 +518,7 @@ Others: Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2109`: the named tuple returned by :func:`disk_partitions`' no longer - has ``maxfile`` and ``maxpath`` fields. + has :field:`maxfile` and :field:`maxpath` fields. - :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether PIDs have been reused. If you want to check for PID reusage you are supposed to use @@ -522,7 +527,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: internal cache. - :gh:`2407`: :meth:`Process.connections` was renamed to :meth:`Process.net_connections`. The old name is still available, but it's - deprecated (triggers a ``DeprecationWarning``) and will be removed in the + deprecated (triggers a :exc:`DeprecationWarning`) and will be removed in the future. 5.9.8 — 2024-01-19 @@ -618,7 +623,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2196`: in case of exception, display a cleaner error traceback by hiding the `KeyError` bit deriving from a missed cache hit. -- :gh:`2217`: print the full traceback when a `DeprecationWarning` or +- :gh:`2217`: print the full traceback when a :exc:`DeprecationWarning` or `UserWarning` is raised. - :gh:`2230`, [OpenBSD]: :func:`net_connections` implementation was rewritten from scratch: @@ -657,10 +662,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: attribute (off by 1 minute). - :gh:`2229`, [OpenBSD]: unable to properly recognize zombie processes. :exc:`NoSuchProcess` may be raised instead of :exc:`ZombieProcess`. -- :gh:`2231`, [NetBSD]: *available* :func:`virtual_memory` is higher than - *total*. -- :gh:`2234`, [NetBSD]: :func:`virtual_memory` metrics are wrong: *available* - and *used* are too high. We now match values shown by *htop* CLI utility. +- :gh:`2231`, [NetBSD]: :field:`available` :func:`virtual_memory` is higher than + :field:`total`. +- :gh:`2234`, [NetBSD]: :func:`virtual_memory` metrics are wrong: :field:`available` + and :field:`used` are too high. We now match values shown by *htop* CLI utility. - :gh:`2236`, [NetBSD]: :meth:`Process.num_threads` and :meth:`Process.threads` return threads that are already terminated. - :gh:`2237`, [OpenBSD], [NetBSD]: :meth:`Process.cwd` may raise @@ -749,7 +754,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2048`: ``AttributeError`` is raised if ``psutil.Error`` class is raised manually and passed through ``str``. - :gh:`2049`, [Linux]: :func:`cpu_freq` erroneously returns ``curr`` value in - GHz while ``min`` and ``max`` are in MHz. + GHz while :field:`min` and :field:`max` are in MHz. - :gh:`2050`, [Linux]: :func:`virtual_memory` may raise ``ValueError`` if running in a LCX container. @@ -821,8 +826,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1863`: :func:`disk_partitions` exposes 2 extra fields: ``maxfile`` and - ``maxpath``, which are the maximum file name and path name length. +- :gh:`1863`: :func:`disk_partitions` exposes 2 extra fields: :field:`maxfile` and + :field:`maxpath`, which are the maximum file name and path name length. - :gh:`1872`, [Windows]: added support for PyPy 2.7. - :gh:`1879`: provide pre-compiled wheels for Linux and macOS (yey!). - :gh:`1880`: get rid of Travis and Cirrus CI services (they are no longer @@ -862,10 +867,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`809`, [FreeBSD]: add support for :meth:`Process.rlimit`. - :gh:`893`, [BSD]: add support for :meth:`Process.environ` (patch by Armin Gruner) -- :gh:`1830`, [POSIX]: :func:`net_if_stats` ``isup`` also checks whether the +- :gh:`1830`, [POSIX]: :func:`net_if_stats` :field:`isup` also checks whether the NIC is running (meaning Wi-Fi or ethernet cable is connected). (patch by Chris Burger) -- :gh:`1837`, [Linux]: improved battery detection and charge ``secsleft`` +- :gh:`1837`, [Linux]: improved battery detection and charge :field:`secsleft` calculation (patch by aristocratos) **Bug fixes** @@ -878,7 +883,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1791`, [macOS]: fix missing include for ``getpagesize()``. - :gh:`1823`, [Windows], **[critical]**: :meth:`Process.open_files` may cause a segfault due to a NULL pointer. -- :gh:`1838`, [Linux]: :func:`sensors_battery`: if `percent` can be +- :gh:`1838`, [Linux]: :func:`sensors_battery`: if :field:`percent` can be determined but not the remaining values, still return a result instead of ``None``. (patch by aristocratos) @@ -1005,7 +1010,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1527`, [Linux]: added :meth:`Process.cpu_times` ``iowait`` counter, +- :gh:`1527`, [Linux]: added :meth:`Process.cpu_times` :field:`iowait` counter, which is the time spent waiting for blocking I/O to complete. - :gh:`1565`: add PEP 517/8 build backend and requirements specification for better pip integration. (patch by Bernát Gábor) @@ -1079,7 +1084,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1223`, [Windows]: :func:`boot_time` may return incorrect value on Windows XP. - :gh:`1456`, [Linux]: :func:`cpu_freq` returns ``None`` instead of 0.0 when - ``min`` and ``max`` fields can't be determined. (patch by Alex Manuskin) + :field:`min` and :field:`max` fields can't be determined. (patch by Alex Manuskin) - :gh:`1462`, [Linux]: (tests) make tests invariant to ``LANG`` setting (patch by Benjamin Drung) - :gh:`1463`: `scripts/cpu_distribution.py`_ was broken. @@ -1314,7 +1319,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1239`, [Linux]: expose kernel ``slab`` memory field for +- :gh:`1239`, [Linux]: expose kernel :field:`slab` memory field for :func:`virtual_memory`. (patch by Maxime Mouial) **Bug fixes** @@ -1383,7 +1388,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1183`: :meth:`Process.children` is 2x faster on POSIX and 2.4x faster on Linux. - :gh:`1188`: deprecated method :meth:`Process.memory_info_ex` now warns by - using ``FutureWarning`` instead of ``DeprecationWarning``. + using :exc:`FutureWarning` instead of :exc:`DeprecationWarning`. **Bug fixes** @@ -1467,12 +1472,12 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`802`: :func:`disk_io_counters` and :func:`net_io_counters` numbers no longer wrap (restart from 0). Introduced a new ``nowrap`` argument. - :gh:`928`: :func:`net_connections` and :meth:`Process.connections` - ``laddr`` and ``raddr`` are now named tuples. + :field:`laddr` and :field:`raddr` are now named tuples. - :gh:`1015`: :func:`swap_memory` now relies on ``/proc/meminfo`` instead of ``sysinfo()`` syscall so that it can be used in conjunction with :data:`PROCFS_PATH` in order to retrieve memory info about Linux containers such as Docker and Heroku. -- :gh:`1022`: :func:`users` provides a new ``pid`` field. +- :gh:`1022`: :func:`users` provides a new :field:`pid` field. - :gh:`1025`: :func:`process_iter` accepts two new parameters in order to invoke :meth:`Process.as_dict`: ``attrs`` and ``ad_value``. With these you can @@ -1572,7 +1577,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1039`: returned types consolidation. 1) Windows / :meth:`Process.cpu_times`: fields #3 and #4 were int instead of float. 2) Linux / FreeBSD / OpenBSD: - :meth:`Process.connections` ``raddr`` is now set to ``""`` instead of + :meth:`Process.connections` :field:`raddr` is now set to ``""`` instead of ``None`` when retrieving UNIX sockets. - :gh:`1040`: all strings are encoded by using OS fs encoding. - :gh:`1040`: the following Windows APIs on Python 2 now return a string @@ -1617,9 +1622,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`971`, [Linux]: Add :func:`sensors_fans` function. (patch by Nicolas Hennion) - :gh:`976`, [Windows]: :meth:`Process.io_counters` has 2 new fields: - ``other_count`` and ``other_bytes``. + :field:`other_count` and :field:`other_bytes`. - :gh:`976`, [Linux]: :meth:`Process.io_counters` has 2 new fields: - ``read_chars`` and ``write_chars``. + :field:`read_chars` and :field:`write_chars`. **Bug fixes** @@ -1655,7 +1660,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`966`, [Linux]: :func:`sensors_battery` ``percent`` is a float and is +- :gh:`966`, [Linux]: :func:`sensors_battery` :field:`percent` is a float and is more precise. **Bug fixes** @@ -1751,11 +1756,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`874`, [Windows]: make :func:`net_if_addrs` also return the - ``netmask``. -- :gh:`887`, [Linux]: :func:`virtual_memory` ``available`` and ``used`` - values are more precise and match ``free`` cmdline utility. ``available`` - also takes into account LCX containers preventing ``available`` to overflow - ``total``. + :field:`netmask`. +- :gh:`887`, [Linux]: :func:`virtual_memory` :field:`available` and :field:`used` + values are more precise and match ``free`` cmdline utility. :field:`available` + also takes into account LCX containers preventing :field:`available` to overflow + :field:`total`. - :gh:`891`: `scripts/procinfo.py`_ has been updated and provides a lot more info. @@ -1831,7 +1836,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`812`, [NetBSD], **[critical]**: fix compilation on NetBSD-5.x. - :gh:`823`, [NetBSD]: :func:`virtual_memory` raises ``TypeError`` on Python 3. -- :gh:`829`, [POSIX]: :func:`disk_usage` ``percent`` field takes root +- :gh:`829`, [POSIX]: :func:`disk_usage` :field:`percent` field takes root reserved space into account. - :gh:`816`, [Windows]: fixed :func:`net_io_counters` values wrapping after 4.3GB in Windows Vista (NT 6.0) and above using 64bit values from newer win @@ -1844,8 +1849,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`795`, [Windows]: new APIs to deal with Windows services: :func:`win_service_iter` and :func:`win_service_get`. -- :gh:`800`, [Linux]: :func:`virtual_memory` returns a new ``shared`` memory - field. +- :gh:`800`, [Linux]: :func:`virtual_memory` returns a new :field:`shared` + memory field. - :gh:`819`, [Linux]: speedup ``/proc`` parsing: :meth:`Process.ppid` +20% faster. :meth:`Process.status` +28% faster. @@ -1865,14 +1870,15 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`777`, [Linux]: :meth:`Process.open_files` on Linux return 3 new - fields: ``position``, ``mode`` and ``flags``. + fields: :field:`position`, :field:`mode` and :field:`flags`. - :gh:`779`: :meth:`Process.cpu_times` returns two new fields, - ``children_user`` and ``children_system`` (always set to 0 on macOS and - Windows). + :field:`children_user` and :field:`children_system` (always set to 0 on macOS + and Windows). - :gh:`789`, [Windows]: :func:`cpu_times` return two new fields: - ``interrupt`` and ``dpc``. Same for :func:`cpu_times_percent`. + :field:`interrupt` and :field:`dpc`. Same for :func:`cpu_times_percent`. - :gh:`792`: new :func:`cpu_stats` function returning number of CPU - ``ctx_switches``, ``interrupts``, ``soft_interrupts`` and ``syscalls``. + :field:`ctx_switches`, :field:`interrupts`, :field:`soft_interrupts` and + :field:`syscalls`. **Bug fixes** @@ -1892,7 +1898,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`523`, [Linux], [FreeBSD]: :func:`disk_io_counters` return a new - ``busy_time`` field. + :field:`busy_time` field. - :gh:`660`, [Windows]: make.bat is smarter in finding alternative VS install locations. (patch by mpderbec) - :gh:`732`: :meth:`Process.environ`. (patch by Frank Benkstein) @@ -1902,7 +1908,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`758`: tests now live in psutil namespace. - :gh:`760`: expose OS constants (``psutil.LINUX``, ``psutil.OSX``, etc.) - :gh:`756`, [Linux]: :func:`disk_io_counters` return 2 new fields: - ``read_merged_count`` and ``write_merged_count``. + :field:`read_merged_count` and :field:`write_merged_count`. - :gh:`762`: add `scripts/procsmem.py`_. **Bug fixes** @@ -1942,7 +1948,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`724`, [FreeBSD]: :func:`virtual_memory` ``total`` is incorrect. +- :gh:`724`, [FreeBSD]: :func:`virtual_memory` :field:`total` is incorrect. - :gh:`730`, [FreeBSD], **[critical]**: :func:`virtual_memory` crashes with "OSError: [Errno 12] Cannot allocate memory". @@ -1969,7 +1975,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: visible. - :gh:`722`, [Linux]: :func:`swap_memory` no longer crashes if ``sin`` / ``sout`` can't be determined due to missing ``/proc/vmstat``. -- :gh:`724`, [FreeBSD]: :func:`virtual_memory` ``total`` is slightly +- :gh:`724`, [FreeBSD]: :func:`virtual_memory` :field:`total` is slightly incorrect. 3.3.0 — 2015-11-25 @@ -2106,8 +2112,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`250`: new :func:`net_if_stats` returning NIC statistics (``isup``, - ``duplex``, ``speed``, ``mtu``). +- :gh:`250`: new :func:`net_if_stats` returning NIC statistics (:field:`isup`, + :field:`duplex`, :field:`speed`, :field:`mtu`). - :gh:`376`: new :func:`net_if_addrs` returning all NIC addresses a-la ``ifconfig``. - :gh:`469`: on Python >= 3.4 ``IOPRIO_CLASS_*`` and ``*_PRIORITY_CLASS`` @@ -2303,7 +2309,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: For the sake of consistency a lot of psutil APIs have been renamed. In most cases accessing the old names will work but it will cause a -``DeprecationWarning``. +:exc:`DeprecationWarning`. - ``psutil.*`` module level constants have being replaced by functions: @@ -2528,7 +2534,7 @@ cases accessing the old names will work but it will cause a **Enhancements** - :gh:`18`, [SunOS]: add Solaris support (yay!) (thanks Justin Venus) -- :gh:`367`: :meth:`Process.connections` ``status`` strings are now +- :gh:`367`: :meth:`Process.connections` :field:`status` strings are now constants. - :gh:`380`: test suite exits with non-zero on failure. (patch by floppymaster) @@ -2547,10 +2553,10 @@ cases accessing the old names will work but it will cause a **API changes** -- :meth:`Process.connections` ``status`` field is no longer a string but a +- :meth:`Process.connections` :field:`status` field is no longer a string but a constant object (``psutil.CONN_*``). -- :meth:`Process.connections` ``local_address`` and ``remote_address`` fields - renamed to ``laddr`` and ``raddr``. +- :meth:`Process.connections` :field:`local_address` and :field:`remote_address` + fields renamed to :field:`laddr` and :field:`raddr`. - psutil.network_io_counters() renamed to :func:`net_io_counters`. 0.7.1 — 2013-05-03 @@ -2576,9 +2582,9 @@ cases accessing the old names will work but it will cause a - :gh:`328`, [Windows]: :meth:`Process.ionice` support. - :gh:`359`: add :func:`boot_time` as a substitute of ``psutil.BOOT_TIME`` since the latter cannot reflect system clock updates. -- :gh:`361`, [Linux]: :func:`cpu_times` now includes new ``steal``, ``guest`` - and ``guest_nice`` fields available on recent Linux kernels. Also, - :func:`cpu_percent` is more accurate. +- :gh:`361`, [Linux]: :func:`cpu_times` now includes new :field:`steal`, + :field:`guest` and :field:`guest_nice` fields available on recent Linux + kernels. Also, :func:`cpu_percent` is more accurate. - :gh:`362`: add :func:`cpu_times_percent` (per-CPU-time utilization as a percentage). @@ -2694,9 +2700,9 @@ cases accessing the old names will work but it will cause a memory. Added new :func:`virtual_memory` and :func:`swap_memory` functions. All old memory-related functions are deprecated. Also two new example scripts were added: `scripts/free.py`_ and `scripts/meminfo.py`_. -- :gh:`312`: ``net_io_counters()`` named tuple includes 4 new fields: ``errin``, - ``errout``, ``dropin`` and ``dropout``, reflecting the number of packets - dropped and with errors. +- :gh:`312`: ``net_io_counters()`` named tuple includes 4 new fields: :field:`errin`, + :field:`errout`, :field:`dropin` and :field:`dropout`, reflecting the number + of packets dropped and with errors. **Bug fixes** @@ -2889,9 +2895,9 @@ cases accessing the old names will work but it will cause a - :gh:`163`: per-process associated terminal / TTY (:meth:`Process.terminal`). - :gh:`171`: added ``get_phymem()`` and ``get_virtmem()`` functions returning - system memory information (``total``, ``used``, ``free``) and memory percent - usage. ``total_*``, ``avail_*`` and ``used_*`` memory functions are - deprecated. + system memory information (:field:`total`, :field:`used`, :field:`free`) and + memory percent usage. ``total_*``, ``avail_*`` and ``used_*`` memory functions + are deprecated. - :gh:`172`: disk usage statistics (:func:`disk_usage`). - :gh:`174`: mounted disk partitions (:func:`disk_partitions`). - :gh:`179`: setuptools is now used in setup.py diff --git a/docs/conf.py b/docs/conf.py index b15b40997d..1096ae8f96 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,7 @@ "add_home_link", "changelog_anchors", "check_python_syntax", + "field_role", ] project = PROJECT_NAME diff --git a/docs/faq.rst b/docs/faq.rst index 0e061637a1..20dc61b152 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -333,33 +333,33 @@ Memory What is the difference between virtual_memory() available and free? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:func:`virtual_memory` returns both ``free`` and ``available``, but they +:func:`virtual_memory` returns both :field:`free` and :field:`available`, but they measure different things: -- ``free``: memory that is not being used at all. -- ``available``: how much memory can be given to processes without swapping. +- :field:`free`: memory that is not being used at all. +- :field:`available`: how much memory can be given to processes without swapping. This includes reclaimable caches and buffers that the OS can reclaim under pressure. -In practice, ``available`` is almost always the metric you want when monitoring -memory. ``free`` can be misleadingly low on systems where the OS aggressively -uses RAM for caches (which is normal and healthy). On Windows, ``free`` and -``available`` are the same value. +In practice, :field:`available` is almost always the metric you want when +monitoring memory. :field:`free` can be misleadingly low on systems where the +OS aggressively uses RAM for caches (which is normal and healthy). On Windows, +:field:`free` and :field:`available` are the same value. .. _faq_memory_rss_vs_vms: What is the difference between RSS and VMS? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- ``rss`` (Resident Set Size): the amount of physical memory (RAM) +- :field:`rss` (Resident Set Size): the amount of physical memory (RAM) currently mapped into the process. -- ``vms`` (Virtual Memory Size): the total virtual address space of the +- :field:`vms` (Virtual Memory Size): the total virtual address space of the process, including memory that has been swapped out, shared libraries, and memory-mapped files. -``rss`` is the go-to metric for answering "how much RAM is this process +:field:`rss` is the go-to metric for answering "how much RAM is this process using?". Note that it includes shared memory, so it may overestimate -actual usage when compared across processes. ``vms`` is generally larger +actual usage when compared across processes. :field:`vms` is generally larger and can be misleadingly high, as it includes memory that is not resident in physical RAM. @@ -371,10 +371,10 @@ Both values are portable across platforms and are returned by When should I use memory_footprint() vs memory_info()? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:meth:`Process.memory_info` returns ``rss``, which includes shared +:meth:`Process.memory_info` returns :field:`rss`, which includes shared libraries counted in every process that uses them. For example, if ``libc`` uses 2 MB and 100 processes map it, each process includes those -2 MB in its ``rss``. +2 MB in its :field:`rss`. :meth:`Process.memory_footprint` returns USS (Unique Set Size), i.e. memory private to the process. It represents the amount of memory that @@ -397,7 +397,7 @@ separately: >>> m.used + m.free == m.total False -The ``available`` field already includes this reclaimable memory and is the +The :field:`available` field already includes this reclaimable memory and is the best indicator of memory pressure. See :ref:`faq_virtual_memory_available`. .. _`BPO-6973`: https://bugs.python.org/issue6973 diff --git a/docs/glossary.rst b/docs/glossary.rst index 78ab390ecc..ae28b84036 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -12,27 +12,27 @@ Glossary :term:`page cache`), such as the :term:`heap`, the stack, and other memory allocated directly by the program (e.g. via ``malloc()``). Anonymous pages have no on-disk counterpart and must be written to - :term:`swap memory` if evicted. Exposed by psutil via the ``rss_anon`` field of - :meth:`Process.memory_info_ex` (total resident anonymous pages) and the - ``anonymous`` field of :meth:`Process.memory_maps` (per mapping). - Anonymous regions are also visible in the ``path`` column of - :meth:`Process.memory_maps` as ``"[heap]"``, ``"[stack]"``, or an empty - string. + :term:`swap memory` if evicted. Exposed by psutil via the :field:`rss_anon` + field of :meth:`Process.memory_info_ex` (total resident anonymous pages) + and the :field:`anonymous` field of :meth:`Process.memory_maps` (per + mapping). Anonymous regions are also visible in the :field:`path` column + of :meth:`Process.memory_maps` as ``"[heap]"``, ``"[stack]"``, or an + empty string. available memory The amount of RAM that can be given to processes without the system going into :term:`swap `. This is the right field to watch for - memory pressure, not ``free``. ``free`` is often deceptively low because - the OS keeps recently freed pages as reclaimable cache; those pages are - counted in ``available`` but not in ``free``. A monitoring alert should - fire on ``available`` (or ``percent``) falling below a threshold, not on - ``free``. See :func:`virtual_memory`. + memory pressure, not :field:`free`. :field:`free` is often deceptively low + because the OS keeps recently freed pages as reclaimable cache; those pages + are counted in :field:`available` but not in :field:`free`. + A monitoring alert should fire on :field:`available` (or :field:`percent`) + falling below a threshold, not on :field:`free`. See :func:`virtual_memory`. busy_time A :term:`cumulative counter` (milliseconds) tracking the time a disk - device spent actually performing I/O, as reported in the ``busy_time`` + device spent actually performing I/O, as reported in the :field:`busy_time` field of :func:`disk_io_counters` (Linux and FreeBSD only). To use it, sample twice and divide the delta by elapsed time to get a utilization percentage (analogous to CPU percent but for disks). A value close to @@ -96,8 +96,8 @@ Glossary A signal sent by a hardware device (disk controller, :term:`NIC`, keyboard) to the CPU to request attention. Each interrupt briefly preempts - whatever the CPU was doing. Reported as the ``interrupts`` field of - :func:`cpu_stats` and ``irq`` field of :func:`cpu_times`. + whatever the CPU was doing. Reported as the :field:`interrupts` field of + :func:`cpu_stats` and :field:`irq` field of :func:`cpu_times`. A very high rate may indicate a misbehaving device driver or a heavily loaded :term:`NIC`. Also see :term:`soft interrupt`. @@ -107,8 +107,8 @@ Glossary (e.g. glibc's ``malloc`` on Linux, ``jemalloc`` on FreeBSD, ``HeapAlloc`` on Windows). When a C extension calls ``malloc()`` and never calls ``free()``, the leaked bytes show up here but - are not always visible to Python's memory tracking tools (:mod:`tracemalloc`, - :func:`sys.getsizeof`) or :term:`RSS` / :term:`VMS` . + are not always visible to Python's memory tracking tools + (:mod:`tracemalloc`, :func:`sys.getsizeof`) or :term:`RSS` / :term:`VMS`. :func:`heap_info` exposes the current state of the heap, and :func:`heap_trim` asks the allocator to release unused portions of it. Together they provide a way to detect memory leaks in C @@ -175,8 +175,8 @@ Glossary it writes, the data is first stored in the cache before being written to disk. Subsequent reads or writes can be served from RAM without disk I/O, making access fast. The OS reclaims page cache automatically under memory - pressure, so a large cache is healthy. Shown as the ``cached`` field of - :func:`virtual_memory` on Linux/BSD. + pressure, so a large cache is healthy. Shown as the :field:`cached` field + of :func:`virtual_memory` on Linux/BSD. peak_rss @@ -184,7 +184,7 @@ Glossary started (memory high-water mark). Available via :meth:`Process.memory_info` (BSD, Windows) and :meth:`Process.memory_info_ex` (Linux, macOS). Useful for capacity - planning and leak detection: if ``peak_rss`` keeps growing across + planning and leak detection: if :field:`peak_rss` keeps growing across successive runs or over time, the process is likely leaking memory. See also :term:`peak_vms`. @@ -199,9 +199,9 @@ Glossary page fault An event that occurs when a process accesses a virtual memory page - that is not currently mapped in physical RAM. A **minor** fault is + that is not currently mapped in physical RAM. A :field:`minor` fault is resolved without disk I/O (e.g. the page is already in RAM but not - yet mapped, or it is copy-on-write). A **major** fault requires + yet mapped, or it is copy-on-write). A :field:`major` fault requires reading the page from disk (e.g. from a memory-mapped file or the :term:`swap memory` area) and is significantly more expensive. Many major faults may indicate memory pressure or excessive swapping. See @@ -222,8 +222,8 @@ Glossary :term:`USS`, returned by :meth:`Process.memory_footprint`, measures exactly the private memory of a process, that is the bytes that would be freed if the process exited. At a per-mapping level, the - ``private_clean`` and ``private_dirty`` fields of - :meth:`Process.memory_maps` (Linux) and the ``private`` field (FreeBSD) + :field:`private_clean` and :field:`private_dirty` fields of + :meth:`Process.memory_maps` (Linux) and the :field:`private` field (FreeBSD) break it down further. PSS @@ -248,7 +248,7 @@ Glossary run later in a less time-critical context (e.g. network packet processing, block I/O completion). Using soft interrupts lets the hardware interrupt return quickly while the heavier processing - happens shortly after. Reported as the ``soft_interrupts`` field of + happens shortly after. Reported as the :field:`soft_interrupts` field of :func:`cpu_stats`. A high rate usually points to heavy network or disk I/O throughput rather than a hardware problem. @@ -262,24 +262,24 @@ Glossary shared page proportionally among the processes that use it. See also :term:`private memory`. - Exposed by psutil as the ``shared`` field of :func:`virtual_memory` and - :meth:`Process.memory_info` (Linux), the ``rss_shmem`` field of - :meth:`Process.memory_info_ex` (Linux), and the ``shared_clean`` / - ``shared_dirty`` fields of :meth:`Process.memory_maps` (Linux). + Exposed by psutil as the :field:`shared` field of :func:`virtual_memory` and + :meth:`Process.memory_info` (Linux), the :field:`rss_shmem` field of + :meth:`Process.memory_info_ex` (Linux), and the :field:`shared_clean` / + :field:`shared_dirty` fields of :meth:`Process.memory_maps` (Linux). swap-in Memory moved from disk (:term:`swap `) back into RAM. - Reported as the ``sin`` :term:`cumulative counter` of - :func:`swap_memory`. A non-zero ``sin`` rate usually means the system + Reported as the :field:`sin` :term:`cumulative counter` of + :func:`swap_memory`. A non-zero :field:`sin` rate usually means the system is bringing memory back into RAM for processes to use. See also :term:`swap-out`. swap-out Memory moved from RAM to disk (:term:`swap `). - Reported as the ``sout`` :term:`cumulative counter` of - :func:`swap_memory`. A non-zero ``sout`` rate indicates memory + Reported as the :field:`sout` :term:`cumulative counter` of + :func:`swap_memory`. A non-zero :field:`sout` rate indicates memory pressure: the system is running low on RAM and must move data to disk, which can slow performance. See also :term:`swap-in`. @@ -313,9 +313,9 @@ Glossary A condition where the system spends more time moving memory between RAM and disk (:term:`swap `) than doing actual work, because memory demand exceeds available RAM. The symptom is high and sustained rates on - both ``sin`` and ``sout`` from :func:`swap_memory`. As a result, the - system becomes very slow or unresponsive. CPU utilization may look low - while everything is waiting on disk I/O. + both :field:`sin` and :field:`sout` from :func:`swap_memory`. + As a result, the system becomes very slow or unresponsive. CPU utilization + may look low while everything is waiting on disk I/O. USS diff --git a/docs/migration.rst b/docs/migration.rst index 642685e590..b1ae1bb22c 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -87,9 +87,9 @@ If you relied on :attr:`Process.info` because you needed a dict structure, use Named tuple field order changed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- :func:`cpu_times`: ``user``, ``system``, ``idle`` fields changed order on Linux, +- :func:`cpu_times`: :field:`user`, :field:`system`, :field:`idle` fields changed order on Linux, macOS and BSD. They are now always the first 3 fields on all platforms, with - platform-specific fields (e.g. ``nice``) following. Positional access (e.g. + platform-specific fields (e.g. :field:`nice`) following. Positional access (e.g. ``cpu_times()[3]``) will silently return the wrong field. Always use attribute access instead (e.g. ``cpu_times().idle``). @@ -106,18 +106,18 @@ Named tuple field order changed and field order. Always use attribute access (e.g. ``p.memory_info().rss``) instead of positional unpacking. - - Linux: ``lib`` and ``dirty`` fields removed (aliases emitting + - Linux: :field:`lib` and :field:`dirty` fields removed (aliases emitting :exc:`DeprecationWarning` are kept). - - macOS: ``pfaults`` and ``pageins`` removed with **no aliases**. + - macOS: :field:`pfaults` and :field:`pageins` removed with **no aliases**. Use :meth:`Process.page_faults` instead. - - Windows: old aliases (``wset``, ``peak_wset``, ``pagefile``, - ``private``, ``peak_pagefile``, ``num_page_faults``) were + - Windows: old aliases (:field:`wset`, :field:`peak_wset`, :field:`pagefile`, + :field:`private`, :field:`peak_pagefile`, :field:`num_page_faults`) were renamed. Old names still work but raise :exc:`DeprecationWarning`. - ``paged_pool``, ``nonpaged_pool``, ``peak_paged_pool``, - ``peak_nonpaged_pool`` were moved to :meth:`memory_info_ex`. - - BSD: a new ``peak_rss`` field was added. + :field:`paged_pool`, :field:`nonpaged_pool`, :field:`peak_paged_pool`, + :field:`peak_nonpaged_pool` were moved to :meth:`memory_info_ex`. + - BSD: a new :field:`peak_rss` field was added. -- :func:`virtual_memory`: on Windows, new ``cached`` and ``wired`` fields were +- :func:`virtual_memory`: on Windows, new :field:`cached` and :field:`wired` fields were added. Code using positional unpacking will break: .. code-block:: python @@ -132,7 +132,7 @@ Named tuple field order changed cpu_times() interrupt renamed to irq on Windows ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The ``interrupt`` field of :func:`cpu_times` on Windows was renamed to ``irq`` +The :field:`interrupt` field of :func:`cpu_times` on Windows was renamed to :field:`irq` to match the name used on Linux and BSD. The old name still works but raises :exc:`DeprecationWarning`. @@ -141,7 +141,7 @@ Status and connection fields are now enums - :meth:`Process.status` now returns a :class:`psutil.ProcessStatus` member instead of a plain ``str``. -- :meth:`Process.net_connections` and :func:`net_connections` ``status`` field +- :meth:`Process.net_connections` and :func:`net_connections` :field:`status` field now returns a :class:`psutil.ConnectionStatus` member instead of a plain ``str``. @@ -161,7 +161,7 @@ New memory_info_ex() method 8.0 introduces a new :meth:`Process.memory_info_ex` method that extends :meth:`Process.memory_info` with platform-specific metrics (e.g. -``peak_rss``, ``swap``, ``rss_anon`` on Linux). This is **unrelated** to +:field:`peak_rss`, :field:`swap`, :field:`rss_anon` on Linux). This is **unrelated** to the old :meth:`Process.memory_info_ex` that was deprecated in 4.0 and removed in 7.0 (which corresponded to what later became :meth:`Process.memory_full_info`). @@ -258,7 +258,7 @@ Process.connections() renamed :meth:`Process.connections` was renamed to :meth:`Process.net_connections` for consistency with the system-level -:func:`net_connections`. The old name triggers a ``DeprecationWarning`` +:func:`net_connections`. The old name triggers a :exc:`DeprecationWarning` and will be removed in a future release: .. code-block:: python @@ -274,7 +274,7 @@ and will be removed in a future release: disk_partitions() lost two fields ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The ``maxfile`` and ``maxpath`` fields were removed from the named tuple +The :field:`maxfile` and :field:`maxpath` fields were removed from the named tuple returned by :func:`disk_partitions`. Code unpacking the tuple positionally will break: @@ -314,7 +314,7 @@ Migrating to 5.0 module-level names were changed. Old :class:`Process` method names still worked but raised -``DeprecationWarning``. They were fully removed in 6.0. +:exc:`DeprecationWarning`. They were fully removed in 6.0. Process methods ^^^^^^^^^^^^^^^^ diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst index 192043761f..345c66f54c 100644 --- a/docs/stdlib-equivalents.rst +++ b/docs/stdlib-equivalents.rst @@ -56,7 +56,7 @@ Disk * - :func:`disk_usage` - :func:`shutil.disk_usage` - Same as :func:`shutil.disk_usage`; psutil also adds - ``percent``. Added to CPython 3.3 (BPO-12442_). + :field:`percent`. Added to CPython 3.3 (BPO-12442_). * - :func:`disk_partitions` - :func:`os.listdrives`, :func:`os.listmounts`, @@ -168,7 +168,7 @@ CPU / scheduling * - :meth:`Process.cpu_times` - :func:`os.times` - :func:`os.times` also has ``elapsed``; psutil adds - ``iowait`` (Linux). + :field:`iowait` (Linux). * - :meth:`Process.cpu_times` - :func:`resource.getrusage` - ``ru_utime`` / ``ru_stime`` match; have higher precision. From 64586a6de12c74520ed6049378bcbc8a6b2654b8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 4 Apr 2026 01:52:15 +0200 Subject: [PATCH 1676/1714] Fix C syntax err in C on Windows committed by accident --- psutil/arch/windows/proc.c | 1 + 1 file changed, 1 insertion(+) diff --git a/psutil/arch/windows/proc.c b/psutil/arch/windows/proc.c index 2bb26d2292..1eacaa5696 100644 --- a/psutil/arch/windows/proc.c +++ b/psutil/arch/windows/proc.c @@ -877,6 +877,7 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { } +PyObject * psutil_proc_io_counters(PyObject *self, PyObject *args) { DWORD pid; HANDLE hProcess; From 027774dae15103a9a531e2678c302310b2ad449c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 4 Apr 2026 19:17:02 +0200 Subject: [PATCH 1677/1714] Sphix: add --nitpicky (fail on bad ref) --jobs=auto (parallel run) --- docs/Makefile | 2 +- docs/_ext/add_home_link.py | 1 + docs/_ext/availability.py | 6 +----- docs/_ext/changelog_anchors.py | 1 + docs/_ext/check_python_syntax.py | 1 + docs/_ext/field_role.py | 1 + 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 34c17de526..7de016cb11 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ PYTHON = python3 SPHINXBUILD = $(PYTHON) -m sphinx BUILDDIR = _build -ALLSPHINXOPTS = --fail-on-warning -d $(BUILDDIR)/doctrees . +ALLSPHINXOPTS = --fail-on-warning --nitpicky --jobs=auto -d $(BUILDDIR)/doctrees . clean: ## Remove all build files rm -rf $(BUILDDIR) diff --git a/docs/_ext/add_home_link.py b/docs/_ext/add_home_link.py index 250c09e89b..759b4505f0 100644 --- a/docs/_ext/add_home_link.py +++ b/docs/_ext/add_home_link.py @@ -22,3 +22,4 @@ def add_home_link(app: Sphinx, pagename, templatename, context, doctree): def setup(app: Sphinx): app.connect("html-page-context", add_home_link) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/availability.py b/docs/_ext/availability.py index 3a34315d1a..7e1d5a0cd4 100644 --- a/docs/_ext/availability.py +++ b/docs/_ext/availability.py @@ -97,8 +97,4 @@ def parse_platforms(self): def setup(app): app.add_directive("availability", Availability) - return { - "version": "1.0", - "parallel_read_safe": True, - "parallel_write_safe": True, - } + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/changelog_anchors.py b/docs/_ext/changelog_anchors.py index 4644a79392..83d5b9e7ac 100644 --- a/docs/_ext/changelog_anchors.py +++ b/docs/_ext/changelog_anchors.py @@ -35,3 +35,4 @@ def add_version_anchors(app, doctree, docname): def setup(app): app.connect("doctree-resolved", add_version_anchors) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/check_python_syntax.py b/docs/_ext/check_python_syntax.py index 5c1bec7ef3..5e5e05cfcb 100644 --- a/docs/_ext/check_python_syntax.py +++ b/docs/_ext/check_python_syntax.py @@ -42,3 +42,4 @@ def check_python_blocks(app, doctree, docname): def setup(app): app.connect("doctree-resolved", check_python_blocks) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/field_role.py b/docs/_ext/field_role.py index 88427a24c8..c1b475a63b 100644 --- a/docs/_ext/field_role.py +++ b/docs/_ext/field_role.py @@ -19,3 +19,4 @@ def field_role( def setup(app): app.add_role("field", field_role) + return {"parallel_read_safe": True, "parallel_write_safe": True} From 5ae217d18c03eca5fa57d0ccdcd14f357089c3b4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 4 Apr 2026 21:01:10 +0200 Subject: [PATCH 1678/1714] Doc: fix many broken refs, especially in changelog.rst --- docs/Makefile | 2 +- docs/_ext/add_home_link.py | 1 + docs/_ext/availability.py | 6 +- docs/_ext/changelog_anchors.py | 1 + docs/_ext/check_python_syntax.py | 1 + docs/_ext/field_role.py | 1 + docs/api.rst | 151 +++++---- docs/changelog.rst | 556 +++++++++++++++---------------- docs/devguide.rst | 3 +- docs/migration.rst | 12 +- docs/recipes.rst | 2 + 11 files changed, 366 insertions(+), 370 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 34c17de526..7de016cb11 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ PYTHON = python3 SPHINXBUILD = $(PYTHON) -m sphinx BUILDDIR = _build -ALLSPHINXOPTS = --fail-on-warning -d $(BUILDDIR)/doctrees . +ALLSPHINXOPTS = --fail-on-warning --nitpicky --jobs=auto -d $(BUILDDIR)/doctrees . clean: ## Remove all build files rm -rf $(BUILDDIR) diff --git a/docs/_ext/add_home_link.py b/docs/_ext/add_home_link.py index 250c09e89b..759b4505f0 100644 --- a/docs/_ext/add_home_link.py +++ b/docs/_ext/add_home_link.py @@ -22,3 +22,4 @@ def add_home_link(app: Sphinx, pagename, templatename, context, doctree): def setup(app: Sphinx): app.connect("html-page-context", add_home_link) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/availability.py b/docs/_ext/availability.py index 3a34315d1a..7e1d5a0cd4 100644 --- a/docs/_ext/availability.py +++ b/docs/_ext/availability.py @@ -97,8 +97,4 @@ def parse_platforms(self): def setup(app): app.add_directive("availability", Availability) - return { - "version": "1.0", - "parallel_read_safe": True, - "parallel_write_safe": True, - } + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/changelog_anchors.py b/docs/_ext/changelog_anchors.py index 4644a79392..83d5b9e7ac 100644 --- a/docs/_ext/changelog_anchors.py +++ b/docs/_ext/changelog_anchors.py @@ -35,3 +35,4 @@ def add_version_anchors(app, doctree, docname): def setup(app): app.connect("doctree-resolved", add_version_anchors) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/check_python_syntax.py b/docs/_ext/check_python_syntax.py index 5c1bec7ef3..5e5e05cfcb 100644 --- a/docs/_ext/check_python_syntax.py +++ b/docs/_ext/check_python_syntax.py @@ -42,3 +42,4 @@ def check_python_blocks(app, doctree, docname): def setup(app): app.connect("doctree-resolved", check_python_blocks) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/field_role.py b/docs/_ext/field_role.py index 88427a24c8..c1b475a63b 100644 --- a/docs/_ext/field_role.py +++ b/docs/_ext/field_role.py @@ -19,3 +19,4 @@ def field_role( def setup(app): app.add_role("field", field_role) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/api.rst b/docs/api.rst index e9967afb57..3c87062e06 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -184,9 +184,9 @@ CPU Note that ``psutil.cpu_count()`` may not necessarily be equivalent to the actual number of CPUs the current process can use. - That can vary if process CPU affinity has been changed, Linux cgroups are - being used or (on Windows) on systems using processor groups or having more - than 64 CPUs. + That can vary if process :term:`CPU affinity` has been changed, Linux cgroups + are being used or (on Windows) on systems using processor groups or having + more than 64 CPUs. The number of usable CPUs can be obtained with: .. code-block:: pycon @@ -265,7 +265,7 @@ CPU Thus, on Windows, the first time this is called and for the next 5 seconds it will return a meaningless ``(0.0, 0.0, 0.0)`` tuple. The numbers returned only make sense when compared to the number of CPU cores - installed on the system. So, for instance, a value of `3.14` on a system + installed on the system. So, for instance, a value of ``3.14`` on a system with 10 :term:`logical CPUs ` means that the system load was 31.4% percent over the last N minutes. @@ -474,7 +474,7 @@ Memory .. versionchanged:: 5.2.3 Linux: use /proc instead of ``sysinfo()`` syscall to support - :const:`psutil.PROCFS_PATH` usage (e.g. useful for Docker containers ...). + :data:`PROCFS_PATH` usage (e.g. useful for Docker containers ...). Disks ^^^^^ @@ -593,7 +593,7 @@ Disks 'sdb1': sdiskio(read_count=161, write_count=0, read_bytes=786432, write_bytes=0, read_time=44, write_time=0)} .. note:: - on Windows ``"diskperf -y"`` command may need to be executed first + on Windows ``diskperf -y`` command may need to be executed first otherwise this function won't find any disk. .. seealso:: @@ -673,8 +673,8 @@ Network ``path`` for :data:`socket.AF_UNIX` sockets (see notes below). - :field:`raddr`: the remote address, either an empty tuple (``AF_INET*``) or ``""`` (``AF_UNIX``) when not connected. For UNIX sockets see notes below. - - :field:`status`: a :data:`psutil.CONN_* ` constant; - always :const:`psutil.CONN_NONE` for UDP and UNIX sockets. + - :field:`status`: a :data:`CONN_* ` constant; + always :data:`CONN_NONE` for UDP and UNIX sockets. - :field:`pid`: PID of the process which opened the socket. Set to ``None`` if it can't be retrieved due to insufficient permissions (e.g. Linux). @@ -726,10 +726,10 @@ Network .. note:: - Linux, FreeBSD, OpenBSD: *raddr* field for UNIX sockets is always set to - ``""`` (empty string); this is a limitation of the OS - - macOS and AIX: :exc:`psutil.AccessDenied` is always raised unless running - as root; this is a limitation of the OS - - Solaris: UNIX sockets are not supported + ``""`` (empty string); this is a limitation of the OS. + - macOS and AIX: :exc:`AccessDenied` is always raised unless running + as root; this is a limitation of the OS. + - Solaris: UNIX sockets are not supported. .. seealso:: @@ -749,7 +749,7 @@ Network (before it was an empty string). .. versionchanged:: 8.0.0 - :field:`status` field is now a :class:`psutil.ConnectionStatus` enum member + :field:`status` field is now a :class:`ConnectionStatus` enum member instead of a plain ``str``. See :ref:`migration guide `. @@ -761,9 +761,9 @@ Network per interface. Each named tuple includes 5 fields (addresses may be ``None``): - - :field:`family`: the address family, either :data:`socket.AF_INET`, - :data:`socket.AF_INET6`, :const:`psutil.AF_LINK` (a MAC address) or - :data:`socket.AF_UNSPEC` (a virtual or unconfigured NIC). + - :field:`family`: the address family, either :data:`socket.AF_INET` (IPv4), + :data:`socket.AF_INET6` (IPv6), :data:`socket.AF_UNSPEC` (a virtual or + unconfigured NIC), or :data:`AF_LINK` (a MAC address). - :field:`address`: the primary NIC address - :field:`netmask`: the netmask address - :field:`broadcast`: the broadcast address; always ``None`` on Windows @@ -801,8 +801,8 @@ Network following fields: - :field:`isup`: whether the NIC is up and running (bool). - - :field:`duplex`: :const:`NIC_DUPLEX_FULL`, :const:`NIC_DUPLEX_HALF` or - :const:`NIC_DUPLEX_UNKNOWN`. + - :field:`duplex`: :data:`NIC_DUPLEX_FULL`, :data:`NIC_DUPLEX_HALF` or + :data:`NIC_DUPLEX_UNKNOWN`. - :field:`speed`: NIC speed in megabits (Mbps); ``0`` if undetermined. - :field:`mtu`: maximum transmission unit in bytes. - :field:`flags`: a comma-separated string of interface flags (e.g. @@ -894,10 +894,8 @@ Sensors - :field:`percent`: battery power left as a percentage. - :field:`secsleft`: a rough approximation of how many seconds are left before the battery runs out of power. - If the AC power cable is connected this is set to - :data:`psutil.POWER_TIME_UNLIMITED `. - If it can't be determined it is set to - :data:`psutil.POWER_TIME_UNKNOWN `. + If the AC power cable is connected this is set to :data:`POWER_TIME_UNLIMITED`. + If it can't be determined it is set to :data:`POWER_TIME_UNKNOWN`. - :field:`power_plugged`: ``True`` if the AC power cable is connected, ``False`` if not or ``None`` if it can't be determined. @@ -1285,7 +1283,7 @@ Process class .. attribute:: attrs - A ``frozenset`` of strings representing the valid attribute names accepted + A :class:`frozenset` of strings representing the valid attribute names accepted by :meth:`as_dict` and :func:`process_iter`. It defaults to all read-only :class:`Process` method names, minus the utility methods such as :meth:`as_dict`, :meth:`children`, etc. @@ -1450,15 +1448,15 @@ Process class .. method:: status() - The current process status as a :class:`psutil.ProcessStatus` enum member. - The returned value is one of the - :data:`psutil.STATUS_* ` constants. + The current process status as a :class:`ProcessStatus` enum member. + The returned value is one of the :data:`STATUS_* ` + constants. A common use case is detecting :term:`zombie processes ` (``p.status() == psutil.STATUS_ZOMBIE``). .. versionchanged:: 8.0.0 - return value is now a :class:`psutil.ProcessStatus` enum member instead - of a plain ``str``. + return value is now a :class:`ProcessStatus` enum member instead of a + plain ``str``. See :ref:`migration guide `. .. method:: cwd() @@ -1516,9 +1514,9 @@ Process class On Windows this is implemented via `GetPriorityClass`_ and `SetPriorityClass`_ Windows APIs and *value* is one of the - :data:`psutil.*_PRIORITY_CLASS ` + :data:`*_PRIORITY_CLASS ` constants reflecting the MSDN documentation. - The return value on Windows is a :class:`psutil.ProcessPriority` enum member. + The return value on Windows is a :class:`ProcessPriority` enum member. Example which increases process priority on Windows: .. code-block:: pycon @@ -1526,9 +1524,8 @@ Process class >>> p.nice(psutil.HIGH_PRIORITY_CLASS) .. versionchanged:: 8.0.0 - on Windows, the return value is now a :class:`psutil.ProcessPriority` - enum member. - See :ref:`migration guide `. + on Windows, the return value is now a :class:`ProcessPriority` enum + member. See :ref:`migration guide `. .. method:: ionice(ioclass=None, value=None) @@ -1542,23 +1539,23 @@ Process class Linux (see `ioprio_get`_ manual): - * :const:`IOPRIO_CLASS_RT`: (high) the process gets first access to the disk + * :data:`IOPRIO_CLASS_RT`: (high) the process gets first access to the disk every time. Use it with care as it can starve the entire system. Additional priority *level* can be specified and ranges from ``0`` (highest) to ``7`` (lowest). - * :const:`IOPRIO_CLASS_BE`: (normal) the default for any process that hasn't + * :data:`IOPRIO_CLASS_BE`: (normal) the default for any process that hasn't set a specific I/O priority. Additional priority *level* ranges from ``0`` (highest) to ``7`` (lowest). - * :const:`IOPRIO_CLASS_IDLE`: (low) get I/O time when no-one else needs the + * :data:`IOPRIO_CLASS_IDLE`: (low) get I/O time when no-one else needs the disk. No additional *value* is accepted. - * :const:`IOPRIO_CLASS_NONE`: returned when no priority was previously set. + * :data:`IOPRIO_CLASS_NONE`: returned when no priority was previously set. Windows: - * :const:`IOPRIO_HIGH`: highest priority. - * :const:`IOPRIO_NORMAL`: default priority. - * :const:`IOPRIO_LOW`: low priority. - * :const:`IOPRIO_VERYLOW`: lowest priority. + * :data:`IOPRIO_HIGH`: highest priority. + * :data:`IOPRIO_NORMAL`: default priority. + * :data:`IOPRIO_LOW`: low priority. + * :data:`IOPRIO_VERYLOW`: lowest priority. Here's an example on how to set the highest I/O priority depending on what platform you're on: @@ -1581,19 +1578,19 @@ Process class Windows: accept new :data:`IOPRIO_* ` constants. .. versionchanged:: 8.0.0 - *ioclass* is now a :class:`psutil.ProcessIOPriority` enum member. + *ioclass* is now a :class:`ProcessIOPriority` enum member. See :ref:`migration guide `. .. method:: rlimit(resource, limits=None) Get or set process :term:`resource limits ` (see `man prlimit`_). - *resource* is one of the :data:`psutil.RLIMIT_* ` + *resource* is one of the :data:`RLIMIT_* ` constants. *limits* is a ``(soft, hard)`` tuple. This is the same as :func:`resource.getrlimit` and :func:`resource.setrlimit` but can be used for any process PID, not only :func:`os.getpid`. For get, return value is a ``(soft, hard)`` tuple. Each value may be either - an integer or :data:`psutil.RLIMIT_* `. + an integer or :data:`RLIMIT_* `. .. code-block:: pycon @@ -1774,9 +1771,8 @@ Process class .. method:: cpu_affinity(cpus=None) - Get or set process - `CPU affinity `_ - (the set of CPUs the process is allowed to run on). + Get or set process :term:`CPU affinity` (the set of CPUs the process is + allowed to run on). If no argument is passed, return the current affinity as a list of integers. If passed, *cpus* must be a list of CPU integers. An empty list sets affinity to all eligible CPUs. @@ -2512,7 +2508,7 @@ Windows services .. function:: win_service_get(name) Get a Windows service by name, returning a :class:`WindowsService` instance. - Raise :exc:`psutil.NoSuchProcess` if no service with such name exists. + Raise :exc:`NoSuchProcess` if no service with such name exists. .. versionadded:: 4.2.0 @@ -2600,19 +2596,19 @@ Constants The following enum classes group related constants and are useful for type annotations and introspection. The individual constants (e.g. -:data:`psutil.STATUS_RUNNING`) are also accessible directly from the psutil +:data:`STATUS_RUNNING`) are also accessible directly from the psutil namespace as aliases for the enum members and should be preferred over accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over ``psutil.ProcessStatus.STATUS_RUNNING``). -.. class:: psutil.ProcessStatus +.. class:: ProcessStatus :class:`enum.StrEnum` collection of :data:`STATUS_* ` constants. Returned by :meth:`Process.status`. .. versionadded:: 8.0.0 -.. class:: psutil.ProcessPriority +.. class:: ProcessPriority :class:`enum.IntEnum` collection of :data:`*_PRIORITY_CLASS ` constants for @@ -2622,7 +2618,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. versionadded:: 8.0.0 -.. class:: psutil.ProcessIOPriority +.. class:: ProcessIOPriority :class:`enum.IntEnum` collection of I/O priority constants for :meth:`Process.ionice`. @@ -2634,7 +2630,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. versionadded:: 8.0.0 -.. class:: psutil.ProcessRlimit +.. class:: ProcessRlimit :class:`enum.IntEnum` collection of :data:`RLIMIT_* ` constants for :meth:`Process.rlimit`. @@ -2643,7 +2639,7 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. versionadded:: 8.0.0 -.. class:: psutil.ConnectionStatus +.. class:: ConnectionStatus :class:`enum.StrEnum` collection of :data:`CONN_* ` constants. Returned in the :field:`status` field of @@ -2651,14 +2647,14 @@ accessing them via the enum class (e.g. prefer ``psutil.STATUS_RUNNING`` over .. versionadded:: 8.0.0 -.. class:: psutil.NicDuplex +.. class:: NicDuplex :class:`enum.IntEnum` collection of :data:`NIC_DUPLEX_* ` constants. Returned in the *duplex* field of :func:`psutil.net_if_stats`. .. versionadded:: 3.0.0 -.. class:: psutil.BatteryTime +.. class:: BatteryTime :class:`enum.IntEnum` collection of :data:`POWER_TIME_* ` constants. May appear in the *secsleft* field of :func:`psutil.sensors_battery`. @@ -2685,10 +2681,10 @@ Operating system constants .. data:: AIX .. data:: OSX - Alias for :const:`MACOS`. + Alias for :data:`MACOS`. .. deprecated:: 5.4.7 - use :const:`MACOS` instead. + use :data:`MACOS` instead. .. _const-pstatus: @@ -2698,8 +2694,7 @@ Process status constants Represent the current status of a process. Returned by :meth:`Process.status`. .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessStatus` enum members (were plain - strings). + constants are now :class:`ProcessStatus` enum members (were plain strings). See :ref:`migration guide `. .. data:: STATUS_RUNNING @@ -2794,7 +2789,7 @@ set process priority. .. availability:: Windows .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessPriority` enum members (were plain + constants are now :class:`ProcessPriority` enum members (were plain integers). See :ref:`migration guide `. @@ -2816,8 +2811,7 @@ Represent the priority I/O priority of a process (Linux and Windows only). They can be used in conjunction with :meth:`Process.ionice`. .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ProcessIOPriority` enum members (previously - ``IOPriority`` enum). + constants are now :class:`ProcessIOPriority` enum members. See :ref:`migration guide `. .. _const-ioprio-linux: @@ -2827,11 +2821,11 @@ They can be used in conjunction with :meth:`Process.ionice`. .. data:: IOPRIO_CLASS_BE .. data:: IOPRIO_CLASS_IDLE - :const:`IOPRIO_CLASS_NONE` and :const:`IOPRIO_CLASS_BE` (best effort) is the + :data:`IOPRIO_CLASS_NONE` and :data:`IOPRIO_CLASS_BE` (best effort) is the default for any process that hasn't set a specific I/O priority. - :const:`IOPRIO_CLASS_RT` (real time) means the process is given first access + :data:`IOPRIO_CLASS_RT` (real time) means the process is given first access to the disk, regardless of what else is going on in the system. - :const:`IOPRIO_CLASS_IDLE` means the process will get I/O time when no-one else + :data:`IOPRIO_CLASS_IDLE` means the process will get I/O time when no-one else needs the disk. For further information refer to manuals of `ionice `_ command line utility or @@ -2862,9 +2856,8 @@ explained in :func:`resource.getrlimit` documentation. .. availability:: Linux, FreeBSD .. versionchanged:: 8.0.0 - these constants are now :class:`psutil.ProcessRlimit` enum members (were - plain integers). - See :ref:`migration guide `. + these constants are now :class:`ProcessRlimit` enum members (were plain + integers). See :ref:`migration guide `. Linux / FreeBSD: @@ -2914,8 +2907,7 @@ Returned by :meth:`Process.net_connections` and :func:`psutil.net_connections` (:field:`status` field). .. versionchanged:: 8.0.0 - constants are now :class:`psutil.ConnectionStatus` enum members (were - plain strings). + constants are now :class:`ConnectionStatus` enum members (were plain strings). See :ref:`migration guide `. .. data:: CONN_ESTABLISHED @@ -3009,6 +3001,23 @@ Other constants >>> if psutil.version_info >= (4, 5): ... pass +Environment variables +--------------------- + +.. envvar:: PSUTIL_DEBUG + + If set, psutil will print debug messages to stderr. This is useful for + troubleshooting internal errors or understanding the library's behavior at + a lower level. The variable is checked at import time, and affects both the + Python layer and the underlying C extension modules. It can also be toggled + programmatically at runtime via ``psutil._set_debug(True)``. + + .. code-block:: bash + + $ PSUTIL_DEBUG=1 python3 script.py + + .. versionadded:: 5.4.2 + .. _`ioprio_get`: https://linux.die.net/man/2/ioprio_get .. _`iostats doc`: https://www.kernel.org/doc/Documentation/iostats.txt .. _`mallinfo2`: https://man7.org/linux/man-pages/man3/mallinfo.3.html diff --git a/docs/changelog.rst b/docs/changelog.rst index b4050db2a0..26205d18b2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,19 +21,24 @@ Doc improvements (:gh:`2761`, :gh:`2757`, :gh:`2760`, :gh:`2745`, :gh:`2763`, - Split docs from a single HTML file into multiple new sections: - :doc:`/adoption `: notable software using psutil - - :doc:`/alternatives `: list of alternative Python libraries and tools that overlap with psutil. + - :doc:`/alternatives `: list of alternative Python libraries + and tools that overlap with psutil. - :doc:`/api-overview `: show entire API via REPL usage examples - - :doc:`/credits `: list contributors and donors (was old ``CREDITS`` in root dir) + - :doc:`/credits `: list contributors and donors (was old ``CREDITS`` + in root dir) - :doc:`/faq `: extended FAQ section - :doc:`/funding `: list funding methods and current sponsors - :doc:`/glossary `: core concepts explained - :doc:`/install `: (was old ``INSTALL.rst`` in root dir) - - :doc:`/migration `: explain how to migrate to newer psutil versions that break backward compatibility + - :doc:`/migration `: explain how to migrate to newer psutil + versions that break backward compatibility - :doc:`/performance `: how to use psutil efficiently - :doc:`/platform `: summary of OSes and architectures support - :doc:`/recipes `: code samples - - :doc:`/shell-equivalents `: maps each psutil API to native CLI commands - - :doc:`/stdlib-equivalents `: maps psutil's Python API to the closest equivalent in the Python standard library + - :doc:`/shell-equivalents `: maps each psutil API to + native CLI commands + - :doc:`/stdlib-equivalents `: maps psutil's Python API + to the closest equivalent in the Python standard library - Theming: @@ -70,20 +75,19 @@ Type hints / enums: Type checkers (mypy, pyright, etc.) can now statically verify code that uses psutil. No runtime behavior is changed; the annotations are purely informational. -- :gh:`2751`: Convert all named tuples from ``collections.namedtuple`` to - ``typing.NamedTuple`` classes with **type annotations**. This makes the +- :gh:`2751`: Convert all named tuples from :func:`collections.namedtuple` to + :class:`typing.NamedTuple` classes with **type annotations**. This makes the classes self-documenting, effectively turning this module into a readable API reference. - :gh:`2753`: Introduce enum classes (:class:`ProcessStatus`, - :class:`ConnectionStatus`, - :class:`ProcessIOPriority`, :class:`ProcessPriority`, :class:`ProcessRlimit`) - grouping related constants. The individual top-level constants (e.g. - ``psutil.STATUS_RUNNING``) remain the primary API, and are now aliases - for the corresponding enum members. + :class:`ConnectionStatus`, :class:`ProcessIOPriority`, :class:`ProcessPriority`, + :class:`ProcessRlimit`) grouping related constants. + The individual top-level constants (e.g. :data:`STATUS_RUNNING`) remain + the primary API, and are now aliases for the corresponding enum members. New APIs: -- :gh:`2798`: new :attr:`Process.attrs` class attribute, a ``frozenset`` of +- :gh:`2798`: new :attr:`Process.attrs` class attribute, a :class:`frozenset` of valid attribute names accepted by :meth:`Process.as_dict` and :func:`process_iter`. Replaces the old pattern of calling ``list(psutil.Process().as_dict().keys())``. @@ -144,7 +148,7 @@ Others: has been normalized on all platforms, and the first 3 fields are now always :field:`user`, :field:`system`, :field:`idle`. See compatibility notes below. - :gh:`2754`: standardize :func:`sensors_battery`'s :field:`percent` so that it - returns a `float` instead of `int` on all systems, not only Linux. + returns a ``float`` instead of ``int`` on all systems, not only Linux. - :gh:`2765`: add a PR bot that uses Claude to summarize PR changes and update ``changelog.rst`` and ``credits.rst`` when commenting with /changelog. - :gh:`2766`: remove remaining Python 2.7 compatibility shims from @@ -170,24 +174,25 @@ Others: **Bug fixes** -- :gh:`2770`, [Linux]: fix :func:`cpu_count_cores` raising ``ValueError`` - on s390x architecture, where ``/proc/cpuinfo`` uses spaces before the - colon separator instead of a tab. +- :gh:`2770`, [Linux]: fix :func:`cpu_count` (``logical=False``) raising + :exc:`ValueError` on s390x architecture, where ``/proc/cpuinfo`` uses spaces + before the colon separator instead of a tab. - :gh:`2726`, [macOS]: :meth:`Process.num_ctx_switches` return an unusual high number due to a C type precision issue. - :gh:`2411` [macOS]: :meth:`Process.cpu_times` and :meth:`Process.cpu_percent` calculation on macOS x86_64 (arm64 is fine) was highly inaccurate (41.67x lower). -- :gh:`2732`, [Linux]: net_if_duplex_speed: handle EBUSY from - ioctl(SIOCETHTOOL). -- :gh:`2744`, [NetBSD]: fix possible double `free()` in :func:`swap_memory`. -- :gh:`2746`, [FreeBSD]: :meth:`Process.memory_maps`, `rss` and `private` - fields, are erroneously reported in memory pages instead of bytes. Other - platforms (Linux, macOS, Windows) return bytes. +- :gh:`2732`, [Linux]: :func:`net_if_stats`: handle ``EBUSY`` from + ``ioctl(SIOCETHTOOL)``. +- :gh:`2744`, [NetBSD]: fix possible double ``free()`` in :func:`swap_memory`. +- :gh:`2746`, [FreeBSD]: :meth:`Process.memory_maps`, :field:`rss` and + :field:`private` fields are erroneously reported in memory pages instead of + bytes. Other platforms (Linux, macOS, Windows) return bytes. - :gh:`2778`, [UNIX]: :func:`net_if_addrs` skips interfaces with no addresses, which are typically virtual IPv4/IPv6 tunnel interfaces. Now they are - included in the returned dict with family == ``AF_UNSPEC`` and an empty list - of addresses. Main reason: it creates an inconsistency with + included in the returned dict with :field:`family` == + :data:`socket.AF_UNSPEC` and an empty list of addresses. Main reason: it + creates an inconsistency with :func:`net_io_counters` and :func:`net_if_stats` which do return these interface names. - :gh:`2782`, [FreeBSD]: :func:`cpu_count` ``logical=False`` return None on @@ -196,7 +201,7 @@ Others: ``psutil_sysctlbyname()`` helpers previously required the returned data size to exactly match the allocated buffer size. Relaxed the check to allow the kernel to return fewer bytes than the buffer (normal for variable-length - sysctl data), while still raising on overflow. + ``sysctl`` data), while still raising on overflow. - :gh:`2795`, [FreeBSD]: fix :func:`cpu_freq` failing with ``RuntimeError: sysctlbyname('dev.cpu.0.freq_levels') size mismatch`` on some systems. @@ -226,7 +231,7 @@ Others: - :gh:`2701`, [macOS]: fix compilation error on macOS < 10.7. (patch by Sergey Fedorov) - :gh:`2707`, [macOS]: fix potential memory leaks in error paths of - `Process.memory_full_info()` and `Process.threads()`. + :meth:`Process.memory_full_info` and :meth:`Process.threads`. - :gh:`2708`, [macOS]: :meth:`Process.cmdline` and :meth:`Process.environ` may fail with ``OSError: [Errno 0] Undefined error`` (from ``sysctl(KERN_PROCARGS2)``). They now raise :exc:`AccessDenied` instead. @@ -250,7 +255,7 @@ Others: mimalloc, libmalloc). Useful to create tools to detect memory leaks. - :gh:`2403`, [Linux]: publish wheels for Linux musl. - :gh:`2680`: unit tests are no longer installed / part of the distribution. - They now live under `tests/` instead of `psutil/tests`. + They now live under ``tests/`` instead of ``psutil/tests``. **Bug fixes** @@ -261,7 +266,7 @@ Others: **Compatibility notes** -- :gh:`2680`: `import psutil.tests` no longer works (but it was never +- :gh:`2680`: ``import psutil.tests`` no longer works (but it was never documented to begin with). 7.1.3 — 2025-11-02 @@ -273,23 +278,24 @@ Others: mandatory formatting style for all C sources. - :gh:`2672`, [macOS], [BSD]: increase the chances to recognize zombie processes and raise the appropriate exception (:exc:`ZombieProcess`). -- :gh:`2676`, :gh:`2678`: replace unsafe `sprintf` / `snprintf` / `sprintf_s` - calls with `str_format()`. Replace `strlcat` / `strlcpy` with safe `str_copy` - / `str_append`. This unifies string handling across platforms and reduces - unsafe usage of standard string functions, improving robustness. +- :gh:`2676`, :gh:`2678`: replace unsafe ``sprintf`` / ``snprintf`` / + ``sprintf_s`` calls with ``str_format`` utility. Replace ``strlcat`` / + ``strlcpy`` with safe ``str_copy`` / ``str_append``. This unifies string + handling across platforms and reduces unsafe usage of standard string + functions, improving robustness. **Bug fixes** - :gh:`2674`, [Windows]: :func:`disk_usage` could truncate values on 32-bit - platforms, potentially reporting incorrect total/free/used space for drives - larger than 4GB. -- :gh:`2675`, [macOS]: :meth:`Process.status` incorrectly returns "running" - for 99% of the processes. + platforms, potentially reporting incorrect :field:`total`, :field:`free`, + :field:`used` space for drives larger than 4GB. +- :gh:`2675`, [macOS]: :meth:`Process.status` incorrectly returns + :data:`STATUS_RUNNING` for 99% of the processes. - :gh:`2677`, [Windows]: fix MAC address string construction in :func:`net_if_addrs`. Previously, the MAC address buffer was incorrectly - updated using a fixed increment and `sprintf_s`, which could overflow or + updated using a fixed increment and ``sprintf_s``, which could overflow or misformat the string if the MAC length or formatting changed. Also, the - final '\n' was inserted unnecessarily. + final ``'\n'`` was inserted unnecessarily. - :gh:`2679`, [OpenBSD], [NetBSD], [critical]: can't build due to C syntax error. @@ -359,34 +365,34 @@ Others: - :gh:`2506`, [Windows]: Windows service APIs had issues with unicode services using special characters in their name. - :gh:`2514`, [Linux]: :meth:`Process.cwd` sometimes fail with - `FileNotFoundError` due to a race condition. + :exc:`FileNotFoundError` due to a race condition. - :gh:`2526`, [Linux]: :meth:`Process.create_time`, which is used to univocally identify a process over time, is subject to system clock updates, and as such can lead to :meth:`Process.is_running` returning a wrong result. A monotonic creation time is now used instead. (patch by Jonathan Kohler) - :gh:`2528`, [Linux]: :meth:`Process.children` may raise - ``PermissionError``. It will now raise :exc:`AccessDenied` instead. + :exc:`PermissionError`. It will now raise :exc:`AccessDenied` instead. - :gh:`2540`, [macOS]: :func:`boot_time` is off by 45 seconds (C precision issue). - :gh:`2541`, :gh:`2570`, :gh:`2578` [Linux], [macOS], [NetBSD]: :meth:`Process.create_time` does not reflect system clock updates. - :gh:`2542`: if system clock is updated :meth:`Process.children` and :meth:`Process.parent` may not be able to return the right information. -- :gh:`2545`: [Illumos]: Fix handling of MIB2_UDP_ENTRY in +- :gh:`2545`: [Illumos]: Fix handling of ``MIB2_UDP_ENTRY`` in :func:`net_connections`. - :gh:`2552`, [Windows]: :func:`boot_time` didn't take into account the time spent during suspend / hibernation. - :gh:`2560`, [Linux]: :meth:`Process.memory_maps` may crash with - `IndexError` on RISCV64 due to a malformed `/proc/{PID}/smaps` file. (patch - by Julien Stephan) + :exc:`IndexError` on RISCV64 due to a malformed ``/proc/pid/smaps`` file. + (patch by Julien Stephan) - :gh:`2586`, [macOS], [CRITICAL]: fixed different places in C code which can trigger a segfault. - :gh:`2604`, [Linux]: :func:`virtual_memory` "used" memory does not match recent versions of ``free`` CLI utility. (patch by Isaac K. Ko) - :gh:`2605`, [Linux]: :func:`sensors_battery` reports a negative amount for seconds left. -- :gh:`2607`, [Windows]: ``WindowsService.description()`` method may fail with +- :gh:`2607`, [Windows]: :meth:`WindowsService.description` method may fail with ``ERROR_NOT_FOUND``. Now it returns an empty string instead. - 2610:, [macOS], [CRITICAL]: fix :func:`cpu_freq` segfault on ARM architectures. @@ -400,18 +406,18 @@ Others: **Enhancements** -- :gh:`669`, [Windows]: :func:`net_if_addrs` also returns the ``broadcast`` +- :gh:`669`, [Windows]: :func:`net_if_addrs` also returns the :field:`broadcast` address instead of ``None``. - :gh:`2480`: Python 2.7 is no longer supported. Latest version supporting Python 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. -- :gh:`2490`: removed long deprecated ``Process.memory_info_ex()`` method. It +- :gh:`2490`: removed long deprecated :meth:`Process.memory_info_ex` method. It was deprecated in psutil 4.0.0, released 8 years ago. Substitute is - ``Process.memory_full_info()``. + :meth:`Process.memory_full_info`. **Bug fixes** - :gh:`2496`, [Linux]: Avoid segfault (a cPython bug) on - ``Process.memory_maps()`` for processes that use hundreds of GBs of memory. + :meth:`Process.memory_maps` for processes that use hundreds of GBs of memory. - :gh:`2502`, [macOS]: :func:`virtual_memory` now relies on ``host_statistics64`` instead of ``host_statistics``. This is the same approach used by ``vm_stat`` CLI tool, and should grant more accurate @@ -420,7 +426,7 @@ Others: **Compatibility notes** - :gh:`2480`: Python 2.7 is no longer supported. -- :gh:`2490`: removed long deprecated ``Process.memory_info_ex()`` method. +- :gh:`2490`: removed long deprecated :meth:`Process.memory_info_ex` method. 6.1.1 — 2024-12-19 ^^^^^^^^^^^^^^^^^^ @@ -431,8 +437,8 @@ Others: **Bug fixes** -- :gh:`2418`, [Linux]: fix race condition in case /proc/PID/stat does not - exist, but /proc/PID does, resulting in FileNotFoundError. +- :gh:`2418`, [Linux]: fix race condition in case ``/proc/pid/stat`` does not + exist, but ``/proc/pid`` does, resulting in :exc:`FileNotFoundError`. - :gh:`2470`, [Linux]: :func:`users` may return "localhost" instead of the actual IP address of the user logged in. @@ -461,13 +467,13 @@ Others: - :gh:`2427`: psutil (segfault) on import in the free-threaded (no GIL) version of Python 3.13. (patch by Sam Gross) -- :gh:`2455`, [Linux]: ``IndexError`` may occur when reading /proc/pid/stat and - field 40 (blkio_ticks) is missing. +- :gh:`2455`, [Linux]: :exc:`IndexError` may occur when reading ``/proc/pid/stat`` + and field 40 (``blkio_ticks``) is missing. - :gh:`2457`, [AIX]: significantly improve the speed of :meth:`Process.open_files` for some edge cases. - :gh:`2460`, [OpenBSD]: :meth:`Process.num_fds` and :meth:`Process.open_files` may fail with :exc:`NoSuchProcess` for PID 0. - Instead, we now return "null" values (0 and [] respectively). + Instead, we now return "null" values (``0`` and ``[]`` respectively). 6.0.0 — 2024-06-18 ^^^^^^^^^^^^^^^^^^ @@ -482,8 +488,7 @@ Others: - :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether PIDs have been reused. This makes :func:`process_iter` around 20x times faster. -- :gh:`2396`: a new ``psutil.process_iter.cache_clear()`` API can be used the - clear +- :gh:`2396`: a new ``process_iter.cache_clear()`` API can be used to clear :func:`process_iter` internal cache. - :gh:`2401`, Support building with free-threaded CPython 3.13. (patch by Sam Gross) @@ -496,22 +501,22 @@ Others: **Bug fixes** -- :gh:`2250`, [NetBSD]: :meth:`Process.cmdline` sometimes fail with EBUSY. It - usually happens for long cmdlines with lots of arguments. In this case retry +- :gh:`2250`, [NetBSD]: :meth:`Process.cmdline` sometimes fail with ``EBUSY``. + It usually happens for long cmdlines with lots of arguments. In this case retry getting the cmdline for up to 50 times, and return an empty list as last resort. -- :gh:`2254`, [Linux]: offline cpus raise NotImplementedError in cpu_freq() +- :gh:`2254`, [Linux]: offline cpus raise :exc:`NotImplementedError` in :func:`cpu_freq` (patch by Shade Gladden) - :gh:`2272`: Add pickle support to psutil Exceptions. - :gh:`2359`, [Windows], [CRITICAL]: :func:`pid_exists` disagrees with - :class:`Process` on whether a pid exists when ERROR_ACCESS_DENIED. + :class:`Process` on whether a pid exists when ``ERROR_ACCESS_DENIED``. - :gh:`2360`, [macOS]: can't compile on macOS < 10.13. (patch by Ryan Schmidt) - :gh:`2362`, [macOS]: can't compile on macOS 10.11. (patch by Ryan Schmidt) - :gh:`2365`, [macOS]: can't compile on macOS < 10.9. (patch by Ryan Schmidt) - :gh:`2395`, [OpenBSD]: :func:`pid_exists` erroneously return True if the argument is a thread ID (TID) instead of a PID (process ID). - :gh:`2412`, [macOS]: can't compile on macOS 10.4 PowerPC due to missing - `MNT_` constants. + ``MNT_`` constants. **Porting notes** @@ -540,7 +545,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: explicitly asked. E.g., on an IDLE system with few IPv6 connections this will run around 4 times faster. Before all connection types (TCP, UDP, UNIX) were retrieved internally, even if only a portion was returned. -- :gh:`2342`, [NetBSD]: same as above (#2343) but for NetBSD. +- :gh:`2342`, [NetBSD]: same as above (:gh:`2343`) but for NetBSD. - :gh:`2349`: adopted black formatting style. **Bug fixes** @@ -550,22 +555,22 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2340`, [NetBSD]: if process is terminated, :meth:`Process.cwd` will return an empty string instead of raising :exc:`NoSuchProcess`. - :gh:`2345`, [Linux]: fix compilation on older compiler missing - DUPLEX_UNKNOWN. -- :gh:`2222`, [macOS]: `cpu_freq()` now returns fixed values for `min` and - `max` frequencies in all Apple Silicon chips. + :data:`NIC_DUPLEX_UNKNOWN`. +- :gh:`2222`, [macOS]: :func:`cpu_freq` now returns fixed values for + :field:`min` and :field:`max` frequencies in all Apple Silicon chips. 5.9.7 — 2023-12-17 ^^^^^^^^^^^^^^^^^^ **Enhancements** -- :gh:`2324`: enforce Ruff rule `raw-string-in-exception`, which helps +- :gh:`2324`: enforce Ruff rule ``raw-string-in-exception``, which helps providing clearer tracebacks when exceptions are raised by psutil. **Bug fixes** - :gh:`2325`, [PyPy]: psutil did not compile on PyPy due to missing - `PyErr_SetExcFromWindowsErrWithFilenameObject` cPython API. + ``PyErr_SetExcFromWindowsErrWithFilenameObject`` cPython API. 5.9.6 — 2023-10-15 ^^^^^^^^^^^^^^^^^^ @@ -578,7 +583,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: ``cpu_percent(interval=None)`` at the same time, only 1 thread out of 10 would get the right result. - :gh:`2266`: if :class:`Process` class is passed a very high PID, raise - :exc:`NoSuchProcess` instead of OverflowError. (patch by Xuehai Pan) + :exc:`NoSuchProcess` instead of :exc:`OverflowError`. (patch by Xuehai Pan) - :gh:`2246`: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) - :gh:`2290`: PID reuse is now pre-emptively checked for :meth:`Process.ppid` and @@ -589,8 +594,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`2195`, [Linux]: no longer print exception at import time in case - /proc/stat can't be read due to permission error. Redirect it to - ``PSUTIL_DEBUG`` instead. + ``/proc/stat`` can't be read due to permission error. Redirect it to + :envvar:`PSUTIL_DEBUG` instead. - :gh:`2241`, [NetBSD]: can't compile On NetBSD 10.99.3/amd64. (patch by Thomas Klausner) - :gh:`2245`, [Windows]: fix var unbound error on possibly in @@ -622,14 +627,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`2196`: in case of exception, display a cleaner error traceback by hiding - the `KeyError` bit deriving from a missed cache hit. + the :exc:`KeyError` bit deriving from a missed cache hit. - :gh:`2217`: print the full traceback when a :exc:`DeprecationWarning` or - `UserWarning` is raised. + :exc:`UserWarning` is raised. - :gh:`2230`, [OpenBSD]: :func:`net_connections` implementation was rewritten from scratch: - - We're now able to retrieve the path of AF_UNIX sockets (before it was an - empty string) + - We're now able to retrieve the path of :data:`socket.AF_UNIX` sockets + (before it was an empty string) - The function is faster since it no longer iterates over all processes. - No longer produces duplicate connection entries. - :gh:`2238`: there are cases where :meth:`Process.cwd` cannot be determined @@ -648,18 +653,18 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1043`, [OpenBSD] :func:`net_connections` returns duplicate entries. - :gh:`1915`, [Linux]: on certain kernels, ``"MemAvailable"`` field from ``/proc/meminfo`` returns ``0`` (possibly a kernel bug), in which case we - calculate an approximation for ``available`` memory which matches "free" CLI - utility. + calculate an approximation for :field:`available` memory which matches "free" + CLI utility. - :gh:`2164`, [Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5). - :gh:`2186`, [FreeBSD]: compilation fails with Clang 15. (patch by Po-Chuan Hsieh) - :gh:`2191`, [Linux]: :func:`disk_partitions`: do not unnecessarily read - /proc/filesystems and raise :exc:`AccessDenied` unless user specified - `all=False` argument. + ``/proc/filesystems`` and raise :exc:`AccessDenied` unless user specified + ``all=False`` argument. - :gh:`2216`, [Windows]: fix tests when running in a virtual environment (patch by Matthieu Darbois) -- :gh:`2225`, [POSIX]: :func:`users` loses precision for ``started`` - attribute (off by 1 minute). +- :gh:`2225`, [POSIX]: :func:`users` loses precision for :field:`started` + field (off by 1 minute). - :gh:`2229`, [OpenBSD]: unable to properly recognize zombie processes. :exc:`NoSuchProcess` may be raised instead of :exc:`ZombieProcess`. - :gh:`2231`, [NetBSD]: :field:`available` :func:`virtual_memory` is higher than @@ -669,7 +674,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2236`, [NetBSD]: :meth:`Process.num_threads` and :meth:`Process.threads` return threads that are already terminated. - :gh:`2237`, [OpenBSD], [NetBSD]: :meth:`Process.cwd` may raise - ``FileNotFoundError`` if cwd no longer exists. Return an empty string + :exc:`FileNotFoundError` if cwd no longer exists. Return an empty string instead. 5.9.4 — 2022-11-07 @@ -689,8 +694,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: missing ``SPEED_UNKNOWN`` definition. (patch by Amir Rossert) - :gh:`2010`, [macOS]: on MacOS, arm64 ``IFM_1000_TX`` and ``IFM_1000_T`` are the same value, causing a build failure. (patch by Lawrence D'Anna) -- :gh:`2160`, [Windows]: Get Windows percent swap usage from performance - counters. (patch by Daniel Widdis) +- :gh:`2160`, [Windows]: get :func:`swap_memory` :field:`percent` usage from + performance counters. (patch by Daniel Widdis) 5.9.3 — 2022-10-18 ^^^^^^^^^^^^^^^^^^ @@ -703,20 +708,20 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`2116`, [macOS], [critical]: :func:`net_connections` fails with - RuntimeError. + :exc:`RuntimeError`. - :gh:`2135`, [macOS]: :meth:`Process.environ` may contain garbage data. Fix out-of-bounds read around ``sysctl_procargs``. (patch by Bernhard Urban-Forster) - :gh:`2138`, [Linux], **[critical]**: can't compile psutil on Android due to undefined ``ethtool_cmd_speed`` symbol. -- :gh:`2142`, [POSIX]: :func:`net_if_stats` 's ``flags`` on Python 2 returned - unicode instead of str. (patch by Matthieu Darbois) +- :gh:`2142`, [POSIX]: :func:`net_if_stats` 's :field:`flags` on Python 2 + returned unicode instead of str. (patch by Matthieu Darbois) - :gh:`2147`, [macOS] Fix disk usage report on macOS 12+. (patch by Matthieu Darbois) -- :gh:`2150`, [Linux] :meth:`Process.threads` may raise ``NoSuchProcess``. +- :gh:`2150`, [Linux] :meth:`Process.threads` may raise :exc:`NoSuchProcess`. Fix race condition. (patch by Daniel Li) - :gh:`2153`, [macOS] Fix race condition in - test_posix.TestProcess.test_cmdline. (patch by Matthieu Darbois) + ``test_posix.TestProcess.test_cmdline``. (patch by Matthieu Darbois) 5.9.2 — 2022-09-04 ^^^^^^^^^^^^^^^^^^ @@ -740,22 +745,22 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1053`: drop Python 2.6 support. (patches by Matthieu Darbois and Hugo van Kemenade) -- :gh:`2037`: Add additional flags to net_if_stats. +- :gh:`2037`: add :field:`flags` field to :func:`net_if_stats`. - :gh:`2050`, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when reading ``/proc`` pseudo files line by line. This should help having more consistent results. - :gh:`2057`, [OpenBSD]: add support for :func:`cpu_freq`. - :gh:`2107`, [Linux]: :meth:`Process.memory_full_info` (reporting process USS/PSS/Swap memory) now reads ``/proc/pid/smaps_rollup`` instead of - ``/proc/pids/smaps``, which makes it 5 times faster. + ``/proc/pid/smaps``, which makes it 5 times faster. **Bug fixes** -- :gh:`2048`: ``AttributeError`` is raised if ``psutil.Error`` class is raised +- :gh:`2048`: :exc:`AttributeError` is raised if :exc:`psutil.Error` class is raised manually and passed through ``str``. -- :gh:`2049`, [Linux]: :func:`cpu_freq` erroneously returns ``curr`` value in - GHz while :field:`min` and :field:`max` are in MHz. -- :gh:`2050`, [Linux]: :func:`virtual_memory` may raise ``ValueError`` if +- :gh:`2049`, [Linux]: :func:`cpu_freq` erroneously returns :field:`current` + value in GHz while :field:`min` and :field:`max` are in MHz. +- :gh:`2050`, [Linux]: :func:`virtual_memory` may raise :exc:`ValueError` if running in a LCX container. 5.9.0 — 2021-12-29 @@ -774,14 +779,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1996`, [BSD]: add support for MidnightBSD. (patch by Saeed Rasooli) - :gh:`1999`, [Linux]: :func:`disk_partitions`: convert ``/dev/root`` device (an alias used on some Linux distros) to real root device path. -- :gh:`2005`: ``PSUTIL_DEBUG`` mode now prints file name and line number of the - debug messages coming from C extension modules. +- :gh:`2005`: :envvar:`PSUTIL_DEBUG` mode now prints file name and line number + of the debug messages coming from C extension modules. - :gh:`2042`: rewrite HISTORY.rst to use hyperlinks pointing to psutil API doc. **Bug fixes** -- :gh:`1456`, [macOS], **[critical]**: :func:`cpu_freq` ``min`` and ``max`` - are set to 0 if can't be determined (instead of crashing). +- :gh:`1456`, [macOS], **[critical]**: :func:`cpu_freq` :field:`min` and + :field:`max` are set to 0 if can't be determined (instead of crashing). - :gh:`1512`, [macOS]: sometimes :meth:`Process.connections` will crash with ``EOPNOTSUPP`` for one connection; this is now ignored. - :gh:`1598`, [Windows]: :func:`disk_partitions` only returns mountpoints on @@ -798,8 +803,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1904`, [Windows]: ``OpenProcess`` fails with ``ERROR_SUCCESS`` due to ``GetLastError()`` called after ``sprintf()``. (patch by alxchk) - :gh:`1913`, [Linux]: :func:`wait_procs` should catch - ``subprocess.TimeoutExpired`` exception. -- :gh:`1919`, [Linux]: :func:`sensors_battery` can raise ``TypeError`` on + :exc:`subprocess.TimeoutExpired` exception. +- :gh:`1919`, [Linux]: :func:`sensors_battery` can raise :exc:`TypeError` on PureOS. - :gh:`1921`, [Windows]: :func:`swap_memory` shows committed memory instead of swap. @@ -815,7 +820,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: :meth:`Process.name` longer than 128 characters resulting in :exc:`AccessDenied`. This is now fixed. (patch by PetrPospisil) - :gh:`1991`, **[critical]**: :func:`process_iter` is not thread safe and can - raise ``TypeError`` if invoked from multiple threads. + raise :exc:`TypeError` if invoked from multiple threads. - :gh:`1956`, [macOS]: :meth:`Process.cpu_times` reports incorrect timings on M1 machines. (patch by Olivier Dormond) - :gh:`2023`, [Linux]: :func:`cpu_freq` return order is wrong on systems with @@ -839,7 +844,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1708`, [Linux]: get rid of :func:`sensors_temperatures` duplicates. (patch by Tim Schlueter). - :gh:`1839`, [Windows], **[critical]**: always raise :exc:`AccessDenied` - instead of ``WindowsError`` when failing to query 64 processes from 32 bit + instead of :exc:`WindowsError` when failing to query 64 processes from 32 bit ones by using ``NtWoW64`` APIs. - :gh:`1866`, [Windows], **[critical]**: :meth:`Process.exe`, :meth:`Process.cmdline`, @@ -878,7 +883,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1620`, [Linux]: :func:`cpu_count` with ``logical=False`` result is incorrect on systems with more than one CPU socket. (patch by Vincent A. Arcila) -- :gh:`1738`, [macOS]: :meth:`Process.exe` may raise ``FileNotFoundError`` if +- :gh:`1738`, [macOS]: :meth:`Process.exe` may raise :exc:`FileNotFoundError` if process is still alive but the exe file which launched it got deleted. - :gh:`1791`, [macOS]: fix missing include for ``getpagesize()``. - :gh:`1823`, [Windows], **[critical]**: :meth:`Process.open_files` may cause @@ -950,9 +955,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1538`, [NetBSD]: :meth:`Process.cwd` may return ``ENOENT`` instead of :exc:`NoSuchProcess`. -- :gh:`1627`, [Linux]: :meth:`Process.memory_maps` can raise ``KeyError``. +- :gh:`1627`, [Linux]: :meth:`Process.memory_maps` can raise :exc:`KeyError`. - :gh:`1642`, [SunOS]: querying basic info for PID 0 results in - ``FileNotFoundError``. + :exc:`FileNotFoundError`. - :gh:`1646`, [FreeBSD], **[critical]**: many :class:`Process` methods may cause a segfault due to a backward incompatible change in a C type on FreeBSD 12.0. @@ -969,8 +974,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1673`, [OpenBSD]: :meth:`Process.connections`, :meth:`Process.num_fds` and :meth:`Process.threads` returned improper exception if process is gone. -- :gh:`1674`, [SunOS]: :func:`disk_partitions` may raise ``OSError``. -- :gh:`1684`, [Linux]: :func:`disk_io_counters` may raise ``ValueError`` on +- :gh:`1674`, [SunOS]: :func:`disk_partitions` may raise :exc:`OSError`. +- :gh:`1684`, [Linux]: :func:`disk_io_counters` may raise :exc:`ValueError` on systems not having ``/proc/diskstats``. - :gh:`1695`, [Linux]: could not compile on kernels <= 2.6.13 due to ``PSUTIL_HAS_IOPRIO`` not being defined. (patch by Anselm Kruis) @@ -1026,7 +1031,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: was removed. - :gh:`1528`, [AIX], **[critical]**: compilation error on AIX 7.2 due to 32 vs 64 bit differences. (patch by Arnon Yaari) -- :gh:`1535`: ``type`` and ``family`` fields returned by +- :gh:`1535`: :field:`type` and :field:`family` fields returned by :func:`net_connections` are not always turned into enums. - :gh:`1536`, [NetBSD]: :meth:`Process.cmdline` erroneously raise :exc:`ZombieProcess` error if cmdline has non encodable chars. @@ -1057,7 +1062,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: raise unhandled "WinError 1168 element not found" exceptions for "Registry" and "Memory Compression" pseudo processes on Windows 10. - :gh:`1526`, [NetBSD], **[critical]**: :meth:`Process.cmdline` could raise - ``MemoryError``. (patch by Kamil Rytarowski) + :exc:`MemoryError`. (patch by Kamil Rytarowski) 5.6.2 — 2019-04-26 ^^^^^^^^^^^^^^^^^^ @@ -1075,8 +1080,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1464`: various docfixes (always point to Python 3 doc, fix links, etc.). - :gh:`1476`, [Windows]: it is now possible to set process high I/O priority (:meth:`Process.ionice`). Also, I/O priority values are now exposed as 4 - new constants: ``IOPRIO_VERYLOW``, ``IOPRIO_LOW``, ``IOPRIO_NORMAL``, - ``IOPRIO_HIGH``. + new constants: :data:`IOPRIO_VERYLOW`, :data:`IOPRIO_LOW`, :data:`IOPRIO_NORMAL`, + :data:`IOPRIO_HIGH`. - :gh:`1478`: add make command to re-run tests failed on last run. **Bug fixes** @@ -1091,7 +1096,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1470`, [Linux]: :func:`disk_partitions`: fix corner case when ``/etc/mtab`` doesn't exist. (patch by Cedric Lamoriniere) - :gh:`1471`, [SunOS]: :meth:`Process.name` and :meth:`Process.cmdline` can - return ``SystemError``. (patch by Daniel Beer) + return :exc:`SystemError`. (patch by Daniel Beer) - :gh:`1472`, [Linux]: :func:`cpu_freq` does not return all CPUs on Raspberry-pi 3. - :gh:`1474`: fix formatting of ``psutil.tests()`` which mimics ``ps aux`` @@ -1104,7 +1109,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1480`, [Windows], **[critical]**: :func:`cpu_count` with ``logical=False`` could cause a crash due to fixed read violation. (patch by Samer Masterson) -- :gh:`1486`, [AIX], [SunOS]: ``AttributeError`` when interacting with +- :gh:`1486`, [AIX], [SunOS]: :exc:`AttributeError` when interacting with :class:`Process` methods involved into :meth:`Process.oneshot` context. - :gh:`1491`, [SunOS]: :func:`net_if_addrs`: use ``free()`` against ``ifap`` struct on error. (patch by Agnewee) @@ -1148,7 +1153,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`1353`: :func:`process_iter` is now thread safe (it rarely raised - ``TypeError``). + :exc:`TypeError`). - :gh:`1394`, [Windows], **[critical]**: :meth:`Process.name` and :meth:`Process.exe` may erroneously return "Registry" or fail with "[Error 0] The operation completed successfully". ``QueryFullProcessImageNameW`` @@ -1157,11 +1162,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1411`, [BSD]: lack of ``Py_DECREF`` could cause segmentation fault on process instantiation. - :gh:`1419`, [Windows]: :meth:`Process.environ` raises - ``NotImplementedError`` when querying a 64-bit process in 32-bit-WoW mode. + :exc:`NotImplementedError` when querying a 64-bit process in 32-bit-WoW mode. Now it raises :exc:`AccessDenied`. - :gh:`1427`, [OSX]: :meth:`Process.cmdline` and :meth:`Process.environ` - may erroneously raise ``OSError`` on failed ``malloc()``. + may erroneously raise :exc:`OSError` on failed ``malloc()``. - :gh:`1429`, [Windows]: ``SE DEBUG`` was not properly set for current process. It is now, and it should result in less :exc:`AccessDenied` exceptions for low PID processes. @@ -1246,7 +1251,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`715`: do not print exception on import time in case :func:`cpu_times` fails. -- :gh:`1004`, [Linux]: :meth:`Process.io_counters` may raise ``ValueError``. +- :gh:`1004`, [Linux]: :meth:`Process.io_counters` may raise :exc:`ValueError`. - :gh:`1277`, [OSX]: available and used memory (:func:`virtual_memory`) metrics are not accurate. - :gh:`1294`, [Windows]: :meth:`Process.connections` may sometimes fail with @@ -1265,9 +1270,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1286`, [macOS]: ``psutil.OSX`` constant is now deprecated in favor of - new ``psutil.MACOS``. -- :gh:`1309`, [Linux]: added ``psutil.STATUS_PARKED`` constant for +- :gh:`1286`, [macOS]: :data:`OSX` constant is now deprecated in favor of + new :data:`MACOS`. +- :gh:`1309`, [Linux]: added :data:`STATUS_PARKED` constant for :meth:`Process.status`. - :gh:`1321`, [Linux]: add :func:`disk_io_counters` dual implementation relying on ``/sys/block`` filesystem in case ``/proc/diskstats`` is not @@ -1283,15 +1288,15 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1279`, [Linux], [macOS], [BSD]: :func:`net_if_stats` may return ``ENODEV``. - :gh:`1294`, [Windows]: :meth:`Process.connections` may sometime fail with - ``MemoryError``. (patch by sylvainduchesne) + :exc:`MemoryError`. (patch by sylvainduchesne) - :gh:`1305`, [Linux]: :func:`disk_io_counters` may report inflated r/w bytes values. - :gh:`1309`, [Linux]: :meth:`Process.status` is unable to recognize - ``"idle"`` and ``"parked"`` statuses (returns ``"?"``). + :field:`idle` and :field:`parked` statuses (returns ``"?"``). - :gh:`1313`, [Linux]: :func:`disk_io_counters` can report inflated values due to counting base disk device and its partition(s) twice. - :gh:`1323`, [Linux]: :func:`sensors_temperatures` may fail with - ``ValueError``. + :exc:`ValueError`. 5.4.6 — 2018-06-07 ^^^^^^^^^^^^^^^^^^ @@ -1354,7 +1359,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: battery is not listed as "BAT0" under ``/sys/class/power_supply``. - :gh:`1240`, [Windows]: :func:`cpu_times` float loses accuracy in a long running system. (patch by stswandering) -- :gh:`1245`, [Linux]: :func:`sensors_temperatures` may fail with ``IOError`` +- :gh:`1245`, [Linux]: :func:`sensors_temperatures` may fail with :exc:`IOError` "no such file". - :gh:`1255`, [FreeBSD]: :func:`swap_memory` stats were erroneously represented in KB. (patch by Denis Krienbühl) @@ -1380,9 +1385,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1173`: introduced ``PSUTIL_DEBUG`` environment variable which can be set - in order to print useful debug messages on stderr (useful in case of nasty - errors). +- :gh:`1173`: introduced :envvar:`PSUTIL_DEBUG` environment variable which can + be set in order to print useful debug messages on stderr (useful in case of + nasty errors). - :gh:`1177`, [macOS]: added support for :func:`sensors_battery`. (patch by Arnon Yaari) - :gh:`1183`: :meth:`Process.children` is 2x faster on POSIX and 2.4x faster @@ -1433,13 +1438,13 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`1009`, [Linux]: :func:`sensors_temperatures` may crash with - ``IOError``. -- :gh:`1012`, [Windows]: :func:`disk_io_counters` ``read_time`` and - ``write_time`` were expressed in tens of micro seconds instead of + :exc:`IOError`. +- :gh:`1012`, [Windows]: :func:`disk_io_counters` :field:`read_time` and + :field:`write_time` were expressed in tens of micro seconds instead of milliseconds. - :gh:`1127`, [macOS], **[critical]**: invalid reference counting in :meth:`Process.open_files` may lead to segfault. (patch by Jakub Bacic) -- :gh:`1129`, [Linux]: :func:`sensors_fans` may crash with ``IOError``. +- :gh:`1129`, [Linux]: :func:`sensors_fans` may crash with :exc:`IOError`. (patch by Sebastian Saip) - :gh:`1131`, [SunOS]: fix compilation warnings. (patch by Arnon Yaari) - :gh:`1133`, [Windows]: can't compile on newer versions of Visual Studio 2017 @@ -1457,7 +1462,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`1105`, [FreeBSD]: psutil does not compile on FreeBSD 12. -- :gh:`1125`, [BSD]: :func:`net_connections` raises ``TypeError``. +- :gh:`1125`, [BSD]: :func:`net_connections` raises :exc:`TypeError`. **Compatibility notes** @@ -1480,16 +1485,16 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1022`: :func:`users` provides a new :field:`pid` field. - :gh:`1025`: :func:`process_iter` accepts two new parameters in order to invoke - :meth:`Process.as_dict`: ``attrs`` and ``ad_value``. With these you can - iterate over all processes in one shot without needing to catch - :exc:`NoSuchProcess` and do list/dict comprehensions. + :meth:`Process.as_dict`: with *attrs* and *ad_value* parameters. + With these you can iterate over all processes in one shot without needing to + catch :exc:`NoSuchProcess` and do list/dict comprehensions. - :gh:`1040`: implemented full unicode support. - :gh:`1051`: :func:`disk_usage` on Python 3 is now able to accept bytes. - :gh:`1058`: test suite now enables all warnings by default. - :gh:`1060`: source distribution is dynamically generated so that it only includes relevant files. -- :gh:`1079`, [FreeBSD]: :func:`net_connections` ``fd`` number is now being - set for real (instead of ``-1``). (patch by Gleb Smirnoff) +- :gh:`1079`, [FreeBSD]: :func:`net_connections` :field:`fd` number is now + being set for real (instead of ``-1``). (patch by Gleb Smirnoff) - :gh:`1091`, [SunOS]: implemented :meth:`Process.environ`. (patch by Oleksii Shevchuk) @@ -1505,19 +1510,19 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1014`, [Linux]: :class:`Process` class can mask legitimate ``ENOENT`` exceptions as :exc:`NoSuchProcess`. -- :gh:`1016`: :func:`disk_io_counters` raises ``RuntimeError`` on a system +- :gh:`1016`: :func:`disk_io_counters` raises :exc:`RuntimeError` on a system with no disks. -- :gh:`1017`: :func:`net_io_counters` raises ``RuntimeError`` on a system +- :gh:`1017`: :func:`net_io_counters` raises :exc:`RuntimeError` on a system with no network cards installed. - :gh:`1021`, [Linux]: :meth:`Process.open_files` may erroneously raise :exc:`NoSuchProcess` instead of skipping a file which gets deleted while open files are retrieved. - :gh:`1029`, [macOS], [FreeBSD]: :meth:`Process.connections` with ``family=unix`` on Python 3 doesn't properly handle unicode paths and may - raise ``UnicodeDecodeError``. + raise :exc:`UnicodeDecodeError`. - :gh:`1033`, [macOS], [FreeBSD]: memory leak for :func:`net_connections` and :meth:`Process.connections` when retrieving UNIX sockets (``kind='unix'``). -- :gh:`1040`: fixed many unicode related issues such as ``UnicodeDecodeError`` +- :gh:`1040`: fixed many unicode related issues such as :exc:`UnicodeDecodeError` on Python 3 + POSIX and invalid encoded data on Windows. - :gh:`1042`, [FreeBSD], **[critical]**: psutil won't compile on FreeBSD 12. - :gh:`1044`, [macOS]: different :class:`Process` methods incorrectly raise @@ -1526,8 +1531,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: ``SetErrorMode``. - :gh:`1047`, [Windows]: :meth:`Process.username`: memory leak in case exception is thrown. -- :gh:`1048`, [Windows]: :func:`users` ``host`` field report an invalid IP - address. +- :gh:`1048`, [Windows]: :func:`users`'s :field:`host` field report an invalid + IP address. - :gh:`1050`, [Windows]: :meth:`Process.memory_maps` leaks memory. - :gh:`1055`: :func:`cpu_count` is no longer cached. This is useful on systems such as Linux where CPUs can be disabled at runtime. This also @@ -1535,19 +1540,19 @@ Version 6.0.0 introduces some changes which affect backward compatibility: :meth:`Process.cpu_percent` which no longer uses the cache. - :gh:`1058`: fixed Python warnings. - :gh:`1062`: :func:`disk_io_counters` and :func:`net_io_counters` raise - ``TypeError`` if no disks or NICs are installed on the system. + :exc:`TypeError` if no disks or NICs are installed on the system. - :gh:`1063`, [NetBSD]: :func:`net_connections` may list incorrect sockets. - :gh:`1064`, [NetBSD], **[critical]**: :func:`swap_memory` may segfault in case of error. - :gh:`1065`, [OpenBSD], **[critical]**: :meth:`Process.cmdline` may raise - ``SystemError``. + :exc:`SystemError`. - :gh:`1067`, [NetBSD]: :meth:`Process.cmdline` leaks memory if process has terminated. - :gh:`1069`, [FreeBSD]: :meth:`Process.cpu_num` may return 255 for certain kernel processes. -- :gh:`1071`, [Linux]: :func:`cpu_freq` may raise ``IOError`` on old RedHat +- :gh:`1071`, [Linux]: :func:`cpu_freq` may raise :exc:`IOError` on old RedHat distros. -- :gh:`1074`, [FreeBSD]: :func:`sensors_battery` raises ``OSError`` in case +- :gh:`1074`, [FreeBSD]: :func:`sensors_battery` raises :exc:`OSError` in case of no battery. - :gh:`1075`, [Windows]: :func:`net_if_addrs`: ``inet_ntop()`` return value is not checked. @@ -1581,9 +1586,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: ``None`` when retrieving UNIX sockets. - :gh:`1040`: all strings are encoded by using OS fs encoding. - :gh:`1040`: the following Windows APIs on Python 2 now return a string - instead of unicode: ``Process.memory_maps().path``, - ``WindowsService.bin_path()``, ``WindowsService.description()``, - ``WindowsService.display_name()``, ``WindowsService.username()``. + instead of unicode: :meth:`Process.memory_maps`'s :field:`path` field, + :meth:`WindowsService.binpath`, :meth:`WindowsService.description`, + :meth:`WindowsService.display_name`, :meth:`WindowsService.username`. 5.2.2 — 2017-04-10 ^^^^^^^^^^^^^^^^^^ @@ -1593,12 +1598,12 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1000`: fixed some setup.py warnings. - :gh:`1002`, [SunOS]: remove C macro which will not be available on new Solaris versions. (patch by Danek Duvall) -- :gh:`1004`, [Linux]: :meth:`Process.io_counters` may raise ``ValueError``. +- :gh:`1004`, [Linux]: :meth:`Process.io_counters` may raise :exc:`ValueError`. - :gh:`1006`, [Linux]: :func:`cpu_freq` may return ``None`` on some Linux versions does not support the function. Let's not make the function available instead. -- :gh:`1009`, [Linux]: :func:`sensors_temperatures` may raise ``OSError``. -- :gh:`1010`, [Linux]: :func:`virtual_memory` may raise ``ValueError`` on +- :gh:`1009`, [Linux]: :func:`sensors_temperatures` may raise :exc:`OSError`. +- :gh:`1010`, [Linux]: :func:`virtual_memory` may raise :exc:`ValueError` on Ubuntu 14.04. 5.2.1 — 2017-03-24 @@ -1608,7 +1613,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`981`, [Linux]: :func:`cpu_freq` may return an empty list. - :gh:`993`, [Windows]: :meth:`Process.memory_maps` on Python 3 may raise - ``UnicodeDecodeError``. + :exc:`UnicodeDecodeError`. - :gh:`996`, [Linux]: :func:`sensors_temperatures` may not show all temperatures. - :gh:`997`, [FreeBSD]: :func:`virtual_memory` may fail due to missing @@ -1641,19 +1646,19 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`971`, [Linux]: :func:`sensors_temperatures` didn't work on CentOS 7. - :gh:`973`, **[critical]**: :func:`cpu_percent` may raise - ``ZeroDivisionError``. + :exc:`ZeroDivisionError`. 5.1.2 — 2017-02-03 ^^^^^^^^^^^^^^^^^^ **Bug fixes** -- :gh:`966`, [Linux]: :func:`sensors_battery` ``power_plugged`` may +- :gh:`966`, [Linux]: :func:`sensors_battery` :field:`power_plugged` may erroneously return ``None`` on Python 3. -- :gh:`968`, [Linux]: :func:`disk_io_counters` raises ``TypeError`` on Python +- :gh:`968`, [Linux]: :func:`disk_io_counters` raises :exc:`TypeError` on Python 3. -- :gh:`970`, [Linux]: :func:`sensors_battery` ``name`` and ``label`` fields - on Python 3 are bytes instead of str. +- :gh:`970`, [Linux]: :func:`sensors_battery` :field:`name` and :field:`label` + fields on Python 3 are bytes instead of str. 5.1.1 — 2017-02-03 ^^^^^^^^^^^^^^^^^^ @@ -1670,8 +1675,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`965`, [Linux]: :func:`disk_io_counters` may miscalculate sector size and report the wrong number of bytes read and written. - :gh:`966`, [Linux]: :func:`sensors_battery` may fail with - ``FileNotFoundError``. -- :gh:`966`, [Linux]: :func:`sensors_battery` ``power_plugged`` may lie. + :exc:`FileNotFoundError`. +- :gh:`966`, [Linux]: :func:`sensors_battery` :field:`power_plugged` may lie. 5.1.0 — 2017-02-01 ^^^^^^^^^^^^^^^^^^ @@ -1696,7 +1701,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`959`: psutil exception objects could not be pickled. - :gh:`960`: :class:`Popen` ``wait()`` did not return the correct negative exit status if process is killed by a signal. -- :gh:`961`, [Windows]: ``WindowsService.description()`` method may fail with +- :gh:`961`, [Windows]: :meth:`WindowsService.description` method may fail with ``ERROR_MUI_FILE_NOT_FOUND``. 5.0.1 — 2016-12-21 @@ -1714,8 +1719,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`936`, [Windows]: fix compilation error on VS 2013 (patch by Max Bélanger). - :gh:`940`, [Linux]: :func:`cpu_percent` and :func:`cpu_times_percent` was - calculated incorrectly as ``iowait``, ``guest`` and ``guest_nice`` times were - not properly taken into account. + calculated incorrectly as :field:`iowait`, :field:`guest` and + :field:`guest_nice` times were not properly taken into account. - :gh:`944`, [OpenBSD]: :func:`pids` was omitting PID 0. 5.0.0 — 2016-11-06 @@ -1733,7 +1738,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`932`, [NetBSD]: :func:`net_connections` and :meth:`Process.connections` may fail without raising an exception. - :gh:`933`, [Windows]: memory leak in :func:`cpu_stats` and - ``WindowsService.description()`` method. + :meth:`WindowsService.description` method. 4.4.2 — 2016-10-26 ^^^^^^^^^^^^^^^^^^ @@ -1769,27 +1774,27 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`514`, [macOS], **[critical]**: :meth:`Process.memory_maps` can segfault. - :gh:`783`, [macOS]: :meth:`Process.status` may erroneously return - ``"running"`` for zombie processes. + :data:`STATUS_RUNNING` for zombie processes. - :gh:`798`, [Windows]: :meth:`Process.open_files` returns and empty list on Windows 10. - :gh:`825`, [Linux]: :meth:`Process.cpu_affinity`: fix possible double close and use of unopened socket. - :gh:`880`, [Windows]: fix race condition inside :func:`net_connections`. -- :gh:`885`: ``ValueError`` is raised if a negative integer is passed to +- :gh:`885`: :exc:`ValueError` is raised if a negative integer is passed to :func:`cpu_percent` functions. - :gh:`892`, [Linux], **[critical]**: :meth:`Process.cpu_affinity` with - ``[-1]`` as arg raises ``SystemError`` with no error set; now ``ValueError`` + ``[-1]`` as arg raises :exc:`SystemError` with no error set; now :exc:`ValueError` is raised. - :gh:`906`, [BSD]: :func:`disk_partitions` with ``all=False`` returned an empty list. Now the argument is ignored and all partitions are always returned. -- :gh:`907`, [FreeBSD]: :meth:`Process.exe` may fail with - ``OSError(ENOENT)``. +- :gh:`907`, [FreeBSD]: :meth:`Process.exe` may fail with :exc:`OSError` + ``ENOENT``. - :gh:`908`, [macOS], [BSD]: different process methods could errounesuly mask the real error for high-privileged PIDs and raise :exc:`NoSuchProcess` and - :exc:`AccessDenied` instead of ``OSError`` and ``RuntimeError``. + :exc:`AccessDenied` instead of :exc:`OSError` and :exc:`RuntimeError`. - :gh:`909`, [macOS]: :meth:`Process.open_files` and - :meth:`Process.connections` methods may raise ``OSError`` with no exception + :meth:`Process.connections` methods may raise :exc:`OSError` with no exception set if process is gone. - :gh:`916`, [macOS]: fix many compilation warnings. @@ -1802,12 +1807,12 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`854`: :meth:`Process.as_dict` raises ``ValueError`` if passed an +- :gh:`854`: :meth:`Process.as_dict` raises :exc:`ValueError` if passed an erroneous attrs name. - :gh:`857`, [SunOS]: :meth:`Process.cpu_times`, :meth:`Process.cpu_percent`, :meth:`Process.threads` and :meth:`Process.memory_maps` may raise - ``RuntimeError`` if attempting to query a 64bit process with a 32bit Python. + :exc:`RuntimeError` if attempting to query a 64bit process with a 32bit Python. "Null" values are returned as a fallback. - :gh:`858`: :meth:`Process.as_dict` should not call :meth:`Process.memory_info_ex` because it's deprecated. @@ -1834,7 +1839,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`810`, [Windows]: Windows wheels are incompatible with pip 7.1.2. - :gh:`812`, [NetBSD], **[critical]**: fix compilation on NetBSD-5.x. -- :gh:`823`, [NetBSD]: :func:`virtual_memory` raises ``TypeError`` on Python +- :gh:`823`, [NetBSD]: :func:`virtual_memory` raises :exc:`TypeError` on Python 3. - :gh:`829`, [POSIX]: :func:`disk_usage` :field:`percent` field takes root reserved space into account. @@ -1850,7 +1855,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`795`, [Windows]: new APIs to deal with Windows services: :func:`win_service_iter` and :func:`win_service_get`. - :gh:`800`, [Linux]: :func:`virtual_memory` returns a new :field:`shared` - memory field. + field. - :gh:`819`, [Linux]: speedup ``/proc`` parsing: :meth:`Process.ppid` +20% faster. :meth:`Process.status` +28% faster. @@ -1859,7 +1864,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`797`, [Linux]: :func:`net_if_stats` may raise ``OSError`` for certain +- :gh:`797`, [Linux]: :func:`net_if_stats` may raise :exc:`OSError` for certain NIC cards. - :gh:`813`: :meth:`Process.as_dict` should ignore extraneous attribute names which gets attached to the :class:`Process` instance. @@ -1888,8 +1893,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: :exc:`NoSuchProcess`. (patch by wxwright) - :gh:`780`, [macOS]: psutil does not compile with some GCC versions. - :gh:`786`: :func:`net_if_addrs` may report incomplete MAC addresses. -- :gh:`788`, [NetBSD]: :func:`virtual_memory` ``buffers`` and ``shared`` - values were set to 0. +- :gh:`788`, [NetBSD]: :func:`virtual_memory` :field:`buffers` and + :field:`shared` values were set to 0. - :gh:`790`, [macOS], **[critical]**: psutil won't compile on macOS 10.4. 4.0.0 — 2016-02-17 @@ -1906,7 +1911,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: memory stats. (patch by Eric Rahm) - :gh:`755`: :meth:`Process.memory_percent` ``memtype`` parameter. - :gh:`758`: tests now live in psutil namespace. -- :gh:`760`: expose OS constants (``psutil.LINUX``, ``psutil.OSX``, etc.) +- :gh:`760`: expose OS constants (:data:`LINUX`, :data:`OSX`, etc.) - :gh:`756`, [Linux]: :func:`disk_io_counters` return 2 new fields: :field:`read_merged_count` and :field:`write_merged_count`. - :gh:`762`: add `scripts/procsmem.py`_. @@ -1919,7 +1924,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`734`: on Python 3 invalid UTF-8 data is not correctly handled for :meth:`Process.name`, :meth:`Process.cwd`, :meth:`Process.exe`, :meth:`Process.cmdline` and :meth:`Process.open_files` methods resulting - in ``UnicodeDecodeError`` exceptions. ``'surrogateescape'`` error handler + in :exc:`UnicodeDecodeError` exceptions. ``'surrogateescape'`` error handler is now used as a workaround for replacing the corrupted data. - :gh:`737`, [Windows]: when the bitness of psutil and the target process was different, :meth:`Process.cmdline` and :meth:`Process.cwd` could return a @@ -1934,7 +1939,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`764`, [NetBSD]: fix compilation on NetBSD-6.x. - :gh:`766`, [Linux]: :func:`net_connections` can't handle malformed ``/proc/net/unix`` file. -- :gh:`767`, [Linux]: :func:`disk_io_counters` may raise ``ValueError`` on +- :gh:`767`, [Linux]: :func:`disk_io_counters` may raise :exc:`ValueError` on 2.6 kernels and it's broken on 2.4 kernels. - :gh:`770`, [NetBSD]: :func:`disk_io_counters` metrics didn't update. @@ -1967,14 +1972,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`714`, [OpenBSD]: :func:`virtual_memory` ``cached`` value was always +- :gh:`714`, [OpenBSD]: :func:`virtual_memory` :field:`cached` value was always set to 0. - :gh:`715`, **[critical]**: don't crash at import time if :func:`cpu_times` fail for some reason. - :gh:`717`, [Linux]: :meth:`Process.open_files` fails if deleted files still visible. -- :gh:`722`, [Linux]: :func:`swap_memory` no longer crashes if ``sin`` / - ``sout`` can't be determined due to missing ``/proc/vmstat``. +- :gh:`722`, [Linux]: :func:`swap_memory` no longer crashes if :field:`sin` / + :field:`sout` can't be determined due to missing ``/proc/vmstat``. - :gh:`724`, [FreeBSD]: :func:`virtual_memory` :field:`total` is slightly incorrect. @@ -2002,7 +2007,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`541`, [FreeBSD]: :func:`disk_io_counters` r/w times were expressed in seconds instead of milliseconds. (patch by dasumin) - :gh:`610`, [SunOS]: fix build and tests on Solaris 10 -- :gh:`623`, [Linux]: process or system connections raises ``ValueError`` if +- :gh:`623`, [Linux]: process or system connections raises :exc:`ValueError` if IPv6 is not supported by the system. - :gh:`678`, [Linux], **[critical]**: can't install psutil due to bug in setup.py. @@ -2036,11 +2041,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`513`, [Linux]: fixed integer overflow for ``RLIM_INFINITY``. +- :gh:`513`, [Linux]: fixed integer overflow for :data:`RLIM_INFINITY` - :gh:`641`, [Windows]: fixed many compilation warnings. (patch by Jeff Tang) -- :gh:`652`, [Windows]: :func:`net_if_addrs` ``UnicodeDecodeError`` in case +- :gh:`652`, [Windows]: :func:`net_if_addrs` :exc:`UnicodeDecodeError` in case of non-ASCII NIC names. -- :gh:`655`, [Windows]: :func:`net_if_stats` ``UnicodeDecodeError`` in case +- :gh:`655`, [Windows]: :func:`net_if_stats` :exc:`UnicodeDecodeError` in case of non-ASCII NIC names. - :gh:`659`, [Linux]: compilation error on Suse 10. (patch by maozguttman) - :gh:`664`, [Linux]: compilation error on Alpine Linux. (patch by Bart van @@ -2049,7 +2054,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: non-ASCII NIC names. (patch by sk6249) - :gh:`672`, [Windows]: compilation fails if using Windows SDK v8.0. (patch by Steven Winfield) -- :gh:`675`, [Linux]: :func:`net_connections`: ``UnicodeDecodeError`` may +- :gh:`675`, [Linux]: :func:`net_connections`: :exc:`UnicodeDecodeError` may occur when listing UNIX sockets. 3.1.1 — 2015-07-15 @@ -2085,7 +2090,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`636`, [Windows]: :meth:`Process.memory_info` raise :exc:`AccessDenied`. - :gh:`637`, [POSIX]: raise exception if trying to send signal to PID 0 as it - will affect ``os.getpid()`` 's process group and not PID 0. + will affect :func:`os.getpid` 's process group and not PID 0. - :gh:`639`, [Linux]: :meth:`Process.cmdline` can be truncated. - :gh:`640`, [Linux]: ``*connections`` functions may swallow errors and return an incomplete list of connections. @@ -2141,7 +2146,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: for ``"localhost"``. - :gh:`579`, [Windows]: fixed :meth:`Process.open_files` for PID > 64K. - :gh:`579`, [Windows]: fixed many compiler warnings. -- :gh:`585`, [FreeBSD]: :func:`net_connections` may raise ``KeyError``. +- :gh:`585`, [FreeBSD]: :func:`net_connections` may raise :exc:`KeyError`. - :gh:`586`, [FreeBSD], **[critical]**: :meth:`Process.cpu_affinity` segfaults on set in case an invalid CPU number is provided. - :gh:`593`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps` @@ -2179,7 +2184,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`496`, [SunOS], **[critical]**: can't import psutil. -- :gh:`547`, [POSIX]: :meth:`Process.username` may raise ``KeyError`` if UID +- :gh:`547`, [POSIX]: :meth:`Process.username` may raise :exc:`KeyError` if UID can't be resolved. - :gh:`551`, [Windows]: get rid of the unicode hack for :func:`net_io_counters` NIC names. @@ -2228,10 +2233,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`522`, [Linux]: :meth:`Process.cpu_affinity` might return ``EINVAL``. (patch by David Daeschler) - :gh:`529`, [Windows]: :meth:`Process.exe` may raise unhandled - ``WindowsError`` exception for PIDs 0 and 4. (patch by Jeff Tang) + :exc:`WindowsError` exception for PIDs 0 and 4. (patch by Jeff Tang) - :gh:`530`, [Linux]: :func:`disk_io_counters` may crash on old Linux distros (< 2.6.5) (patch by Yaolong Huang) -- :gh:`533`, [Linux]: :meth:`Process.memory_maps` may raise ``TypeError`` on +- :gh:`533`, [Linux]: :meth:`Process.memory_maps` may raise :exc:`TypeError` on old Linux distros. 2.1.1 — 2014-04-30 @@ -2265,7 +2270,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`424`, [Windows]: installer for Python 3.X 64 bit. -- :gh:`427`: number of logical CPUs and physical cores (:func:`cpu_count`). +- :gh:`427`: add :func:`cpu_count`. - :gh:`447`: :func:`wait_procs` ``timeout`` parameter is now optional. - :gh:`452`: make :class:`Process` instances hashable and usable with ``set()`` s. @@ -2430,7 +2435,7 @@ cases accessing the old names will work but it will cause a in "psutil" namespace only. - :class:`Process` instances' ``retcode`` attribute returned by :func:`wait_procs` has been renamed to ``returncode`` for consistency with - ``subprocess.Popen``. + :class:`subprocess.Popen`. 1.2.1 — 2013-11-25 ^^^^^^^^^^^^^^^^^^ @@ -2449,7 +2454,7 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`439`: assume ``os.getpid()`` if no argument is passed to +- :gh:`439`: assume :func:`os.getpid` if no argument is passed to :class:`Process` class constructor. - :gh:`440`: new :func:`wait_procs` utility function which waits for multiple processes to terminate. @@ -2489,8 +2494,7 @@ cases accessing the old names will work but it will cause a **Enhancements** - :gh:`410`: host tar.gz and Windows binary files are on PyPI. -- :gh:`412`, [Linux]: get/set process resource limits - (:meth:`Process.rlimit`). +- :gh:`412`, [Linux]: add :meth:`Process.rlimit`. - :gh:`415`, [Windows]: :meth:`Process.children` is an order of magnitude faster. - :gh:`426`, [Windows]: :meth:`Process.name` is an order of magnitude faster. @@ -2545,7 +2549,7 @@ cases accessing the old names will work but it will cause a - :gh:`374`, [Windows]: negative memory usage reported if process uses a lot of memory. -- :gh:`379`, [Linux]: :meth:`Process.memory_maps` may raise ``ValueError``. +- :gh:`379`, [Linux]: :meth:`Process.memory_maps` may raise :exc:`ValueError`. - :gh:`394`, [macOS]: mapped memory regions of :meth:`Process.memory_maps` report incorrect file name. - :gh:`404`, [Linux]: ``sched_*affinity()`` are implicitly declared. (patch by @@ -2565,7 +2569,7 @@ cases accessing the old names will work but it will cause a **Bug fixes** - :gh:`325`, [BSD], **[critical]**: :func:`virtual_memory` can raise - ``SystemError``. (patch by Jan Beich) + :exc:`SystemError`. (patch by Jan Beich) - :gh:`370`, [BSD]: :meth:`Process.connections` requires root. (patch by John Baldwin) - :gh:`372`, [BSD]: different process methods raise :exc:`NoSuchProcess` @@ -2596,10 +2600,10 @@ cases accessing the old names will work but it will cause a - :gh:`313`, [Linux], **[critical]**: :func:`virtual_memory` and :func:`swap_memory` can crash on certain exotic Linux flavors having an incomplete ``/proc`` interface. If that's the case we now set the - unretrievable stats to ``0`` and raise ``RuntimeWarning`` instead. + unretrievable stats to ``0`` and raise :exc:`RuntimeWarning` instead. - :gh:`315`, [macOS]: fix some compilation warnings. - :gh:`317`, [Windows]: cannot set process CPU affinity above 31 cores. -- :gh:`319`, [Linux]: :meth:`Process.memory_maps` raises ``KeyError`` +- :gh:`319`, [Linux]: :meth:`Process.memory_maps` raises :exc:`KeyError` 'Anonymous' on Debian squeeze. - :gh:`321`, [POSIX]: :meth:`Process.ppid` property is no longer cached as the kernel may set the PPID to 1 in case of a zombie process. @@ -2610,8 +2614,8 @@ cases accessing the old names will work but it will cause a as it may change. - :gh:`333`, [macOS]: leak of Mach ports (patch by rsesek@google.com) - :gh:`337`, [Linux], **[critical]**: :class:`Process` methods not working - because of a poor ``/proc`` implementation will raise ``NotImplementedError`` - rather than ``RuntimeError`` and :meth:`Process.as_dict` will not blow up. + because of a poor ``/proc`` implementation will raise :exc:`NotImplementedError` + rather than :exc:`RuntimeError` and :meth:`Process.as_dict` will not blow up. (patch by Curtin1060) - :gh:`338`, [Linux]: :func:`disk_io_counters` fails to find some disks. - :gh:`339`, [FreeBSD]: ``get_pid_list()`` can allocate all the memory on @@ -2632,15 +2636,15 @@ cases accessing the old names will work but it will cause a - :gh:`366`, [FreeBSD], **[critical]**: :meth:`Process.memory_maps`, :meth:`Process.num_fds`, :meth:`Process.open_files` and :meth:`Process.cwd` methods raise - ``RuntimeError`` instead of :exc:`AccessDenied`. + :exc:`RuntimeError` instead of :exc:`AccessDenied`. **API changes** - :meth:`Process.cmdline` property is no longer cached after first access. - :meth:`Process.ppid` property is no longer cached after first access. - [Linux] :class:`Process` methods not working because of a poor ``/proc`` - implementation will raise ``NotImplementedError`` instead of - ``RuntimeError``. + implementation will raise :exc:`NotImplementedError` instead of + :exc:`RuntimeError`. - ``psutil.error`` module is deprecated and scheduled for removal. 0.6.1 — 2012-08-16 @@ -2667,12 +2671,11 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`216`, [POSIX]: :meth:`Process.connections` UNIX sockets support. -- :gh:`220`, [FreeBSD]: ``get_connections()`` has been rewritten in C and no +- :gh:`216`, [POSIX]: add :meth:`Process.connections` UNIX sockets support. +- :gh:`220`, [FreeBSD]: :func:`net_connections` has been rewritten in C and no longer requires ``lsof``. - :gh:`222`, [macOS]: add support for :meth:`Process.cwd`. -- :gh:`261`: per-process extended memory info - (:meth:`Process.memory_info_ex`). +- :gh:`261`: add :meth:`Process.memory_info_ex`. - :gh:`295`, [macOS]: :meth:`Process.exe` path is now determined by asking the OS instead of being guessed from :meth:`Process.cmdline`. - :gh:`297`, [macOS]: the :class:`Process` methods below were always raising @@ -2684,8 +2687,7 @@ cases accessing the old names will work but it will cause a :meth:`Process.num_threads`. - :gh:`300`: add `scripts/pmap.py`_. - :gh:`301`: :func:`process_iter` now yields processes sorted by their PIDs. -- :gh:`302`: per-process number of voluntary and involuntary context switches - (:meth:`Process.num_ctx_switches`). +- :gh:`302`: add :meth:`Process.num_ctx_switches`. - :gh:`303`, [Windows]: the :class:`Process` methods below were always raising :exc:`AccessDenied` for any process not owned by current user. Now this is no longer true: @@ -2700,7 +2702,7 @@ cases accessing the old names will work but it will cause a memory. Added new :func:`virtual_memory` and :func:`swap_memory` functions. All old memory-related functions are deprecated. Also two new example scripts were added: `scripts/free.py`_ and `scripts/meminfo.py`_. -- :gh:`312`: ``net_io_counters()`` named tuple includes 4 new fields: :field:`errin`, +- :gh:`312`: :func:`net_io_counters` named tuple includes 4 new fields: :field:`errin`, :field:`errout`, :field:`dropin` and :field:`dropout`, reflecting the number of packets dropped and with errors. @@ -2754,26 +2756,24 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`195`, [Windows]: number of handles opened by process - (:meth:`Process.num_handles`). +- :gh:`195`, [Windows]: add :meth:`Process.num_handles`. - :gh:`209`: :func:`disk_partitions` now provides also mount options. -- :gh:`229`: list users currently connected on the system (:func:`users`). -- :gh:`238`, [Linux], [Windows]: process CPU affinity (get and set, - :meth:`Process.cpu_affinity`). +- :gh:`229`: add :func:`users`. +- :gh:`238`, [Linux], [Windows]: add :meth:`Process.cpu_affinity`. - :gh:`242`: add ``recursive=True`` to :meth:`Process.children`: return all process descendants. - :gh:`245`, [POSIX]: :meth:`Process.wait` incrementally consumes less CPU cycles. - :gh:`257`, [Windows]: removed Windows 2000 support. - :gh:`258`, [Linux]: :meth:`Process.memory_info` is now 0.5x faster. -- :gh:`260`: process's mapped memory regions. (Windows patch by wj32.64, macOS +- :gh:`260`: add :meth:`Process.memory_maps`. (Windows patch by wj32.64, macOS patch by Jeremy Whitlock) - :gh:`262`, [Windows]: :func:`disk_partitions` was slow due to inspecting the floppy disk drive also when parameter is ``all=False``. - :gh:`273`: ``psutil.get_process_list()`` is deprecated. - :gh:`274`: psutil no longer requires ``2to3`` at installation time in order to work with Python 3. -- :gh:`278`: new :meth:`Process.as_dict` method. +- :gh:`278`: add :meth:`Process.as_dict`. - :gh:`281`: :meth:`Process.ppid`, :meth:`Process.name`, :meth:`Process.exe`, :meth:`Process.cmdline` and :meth:`Process.create_time` properties of @@ -2782,8 +2782,7 @@ cases accessing the old names will work but it will cause a string representation. - :gh:`283`: speedup :meth:`Process.is_running` by caching its return value in case the process is terminated. -- :gh:`284`, [POSIX]: per-process number of opened file descriptors - (:meth:`Process.num_fds`). +- :gh:`284`, [POSIX]: add :meth:`Process.num_fds`. - :gh:`287`: :func:`process_iter` now caches :class:`Process` instances between calls. - :gh:`290`: :meth:`Process.nice` property is deprecated in favor of new @@ -2809,9 +2808,9 @@ cases accessing the old names will work but it will cause a - :gh:`272`, [Linux]: :meth:`Process.open_files` potential race condition can lead to unexpected :exc:`NoSuchProcess` exception. Also, we can get incorrect reports of not absolutized path names. -- :gh:`275`, [Linux]: ``Process.io_counters()`` erroneously raise +- :gh:`275`, [Linux]: :meth:`Process.io_counters` erroneously raise :exc:`NoSuchProcess` on old Linux versions. Where not available it now raises - ``NotImplementedError``. + :exc:`NotImplementedError`. - :gh:`286`: :meth:`Process.is_running` doesn't actually check whether PID has been reused. - :gh:`314`: :meth:`Process.children` can sometimes return non-children. @@ -2848,14 +2847,14 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`150`: network I/O counters (:func:`net_io_counters`). (macOS and - Windows patch by Jeremy Whitlock) +- :gh:`150`: add :func:`net_io_counters` (macOS and Windows patch by Jeremy + Whitlock) - :gh:`154`, [FreeBSD]: add support for :meth:`Process.cwd`. - :gh:`157`, [Windows]: provide installer for Python 3.2 64-bit. - :gh:`198`: :meth:`Process.wait` with ``timeout=0`` can now be used to make the function return immediately. -- :gh:`206`: disk I/O counters (:func:`disk_io_counters`). (macOS and Windows - patch by Jeremy Whitlock) +- :gh:`206`: add :func:`disk_io_counters`). (macOS and Windows patch by Jeremy + Whitlock) - :gh:`213`: add `scripts/iotop.py`_. - :gh:`217`: :meth:`Process.connections` now has a ``kind`` argument to filter for connections with different criteria. @@ -2889,25 +2888,22 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`125`: system per-cpu percentage utilization and times - (:meth:`Process.cpu_times`, - :meth:`Process.cpu_percent`). -- :gh:`163`: per-process associated terminal / TTY - (:meth:`Process.terminal`). +- :gh:`125`: add :func:`cpu_times` and :func:`cpu_percent` per-cpu support. +- :gh:`163`: add :meth:`Process.terminal`. - :gh:`171`: added ``get_phymem()`` and ``get_virtmem()`` functions returning system memory information (:field:`total`, :field:`used`, :field:`free`) and memory percent usage. ``total_*``, ``avail_*`` and ``used_*`` memory functions are deprecated. -- :gh:`172`: disk usage statistics (:func:`disk_usage`). -- :gh:`174`: mounted disk partitions (:func:`disk_partitions`). +- :gh:`172`: add :func:`disk_usage`. +- :gh:`174`: add :func:`disk_partitions`. - :gh:`179`: setuptools is now used in setup.py **Bug fixes** - :gh:`159`, [Windows]: ``SetSeDebug()`` does not close handles or unset impersonation on return. -- :gh:`164`, [Windows]: wait function raises a ``TimeoutException`` when a - process returns ``-1``. +- :gh:`164`, [Windows]: :meth:`Process.wait` raises a ``TimeoutException`` when + a process returns ``-1``. - :gh:`165`: :meth:`Process.status` raises an unhandled exception. - :gh:`166`: :meth:`Process.memory_info` leaks handles hogging system resources. @@ -2923,22 +2919,18 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`64`: per-process I/O counters (:meth:`Process.io_counters`). -- :gh:`116`: per-process :meth:`Process.wait` (wait for process to terminate - and return its exit code). -- :gh:`134`: per-process threads (:meth:`Process.threads`). +- :gh:`64`: add :meth:`Process.io_counters`. +- :gh:`116`: add :meth:`Process.wait`. +- :gh:`134`: add :meth:`Process.threads`. - :gh:`136`: :meth:`Process.exe` path on FreeBSD is now determined by asking the kernel instead of guessing it from cmdline[0]. -- :gh:`137`: per-process real, effective and saved user and group ids - (:meth:`Process.gids`). -- :gh:`140`: system boot time (:func:`boot_time`). -- :gh:`142`: per-process get and set niceness (priority) - (:meth:`Process.nice`). -- :gh:`143`: per-process status (:meth:`Process.status`). -- :gh:`147` [Linux]: per-process I/O niceness / priority - (:meth:`Process.ionice`). -- :gh:`148`: :class:`Popen` class which tidies up ``subprocess.Popen`` and - :class:`Process` class in a single interface. +- :gh:`137`: add :meth:`Process.uids` and :meth:`Process.gids`. +- :gh:`140`: add :func:`boot_time`. +- :gh:`142`: add :meth:`Process.nice`. +- :gh:`143`: add :meth:`Process.status`. +- :gh:`147` [Linux]: add :meth:`Process.ionice`. +- :gh:`148`: add :class:`Popen` class which tidies up :class:`subprocess.Popen` + and :class:`Process` class in a single interface. - :gh:`152`, [macOS]: :meth:`Process.open_files` implementation has been rewritten in C and no longer relies on ``lsof`` resulting in a 3x speedup. - :gh:`153`, [macOS]: :meth:`Process.connections` implementation has been @@ -2947,13 +2939,13 @@ cases accessing the old names will work but it will cause a **Bug fixes** - :gh:`83`, [macOS]: :meth:`Process.cmdline` is empty on macOS 64-bit. -- :gh:`130`, [Linux]: a race condition can cause ``IOError`` exception be +- :gh:`130`, [Linux]: a race condition can cause :exc:`IOError` exception be raised on if process disappears between ``open()`` and the subsequent ``read()`` call. -- :gh:`145`, [Windows], **[critical]**: ``WindowsError`` was raised instead of +- :gh:`145`, [Windows], **[critical]**: :exc:`WindowsError` was raised instead of :exc:`AccessDenied` when using :meth:`Process.resume` or :meth:`Process.suspend`. -- :gh:`146`, [Linux]: :meth:`Process.exe` property can raise ``TypeError`` if +- :gh:`146`, [Linux]: :meth:`Process.exe` property can raise :exc:`TypeError` if path contains NULL bytes. - :gh:`151`, [Linux]: :meth:`Process.exe` and :meth:`Process.cwd` for PID 0 return inconsistent data. @@ -2968,20 +2960,19 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`79`: per-process open files (:meth:`Process.open_files`). +- :gh:`79`: add :meth:`Process.open_files`. - :gh:`88`: total system physical cached memory. - :gh:`88`: total system physical memory buffers used by the kernel. - :gh:`91`: add :meth:`Process.send_signal` and :meth:`Process.terminate` methods. - :gh:`95`: :exc:`NoSuchProcess` and :exc:`AccessDenied` exception classes now provide ``pid``, ``name`` and ``msg`` attributes. -- :gh:`97`: per-process children (:meth:`Process.children`). +- :gh:`97`: add :meth:`Process.children`. - :gh:`98`: :meth:`Process.cpu_times` and :meth:`Process.memory_info` now return a named tuple instead of a tuple. -- :gh:`103`: per-process opened TCP and UDP connections - (:meth:`Process.connections`). +- :gh:`103`: add :meth:`Process.connections`. - :gh:`107`, [Windows]: add support for Windows 64 bit. (patch by cjgohlke) -- :gh:`111`: per-process executable name (:meth:`Process.exe`). +- :gh:`111`: add :meth:`Process.exe`. - :gh:`113`: exception messages now include :meth:`Process.name` and :attr:`Process.pid`. - :gh:`114`, [Windows]: :meth:`Process.username` has been rewritten in pure C @@ -2990,13 +2981,13 @@ cases accessing the old names will work but it will cause a - :gh:`117`, [Windows]: added support for Windows 2000. - :gh:`123`: :func:`cpu_percent` and :meth:`Process.cpu_percent` accept a new ``interval`` parameter. -- :gh:`129`: per-process threads (:meth:`Process.threads`). +- :gh:`129`: add :meth:`Process.threads`. **Bug fixes** - :gh:`80`: fixed warnings when installing psutil with easy_install. - :gh:`81`, [Windows]: psutil fails to compile with Visual Studio. -- :gh:`94`: :meth:`Process.suspend` raises ``OSError`` instead of +- :gh:`94`: :meth:`Process.suspend` raises :exc:`OSError` instead of :exc:`AccessDenied`. - :gh:`86`, [FreeBSD]: psutil didn't compile against FreeBSD 6.x. - :gh:`102`, [Windows]: orphaned process handles obtained by using @@ -3022,17 +3013,16 @@ cases accessing the old names will work but it will cause a process use :meth:`Process.send_signal` method instead. - :meth:`Process.memory_info` returns a nametuple instead of a tuple. - :func:`cpu_times` returns a nametuple instead of a tuple. -- New :class:`Process` methods: :meth:`Process.open_files`, - :meth:`Process.connections`, +- Add :meth:`Process.open_files`, :meth:`Process.connections`, :meth:`Process.send_signal` and :meth:`Process.terminate`. - :meth:`Process.ppid`, :meth:`Process.uids`, :meth:`Process.gids`, :meth:`Process.name`, :meth:`Process.exe`, :meth:`Process.cmdline` and :meth:`Process.create_time` properties are no longer cached and raise :exc:`NoSuchProcess` exception if process disappears. -- :func:`cpu_percent` no longer returns immediately (see issue 123). +- :func:`cpu_percent` no longer returns immediately (see :gh:`123`). - :meth:`Process.cpu_percent` and :func:`cpu_percent` no longer returns - immediately by default (see issue :gh:`123`). + immediately by default (see :gh:`123`). 0.1.3 — 2010-03-02 ^^^^^^^^^^^^^^^^^^ @@ -3040,12 +3030,10 @@ cases accessing the old names will work but it will cause a **Enhancements** - :gh:`14`: :meth:`Process.username`. -- :gh:`51`, [Linux], [Windows]: per-process current working directory - (:meth:`Process.cwd`). +- :gh:`51`, [Linux], [Windows]: add :meth:`Process.cwd`. - :gh:`59`: :meth:`Process.is_running` is now 10 times faster. - :gh:`61`, [FreeBSD]: added supoprt for FreeBSD 64 bit. -- :gh:`71`: per-process suspend and resume (:meth:`Process.suspend` and - :meth:`Process.resume`). +- :gh:`71`: add :meth:`Process.suspend` and :meth:`Process.resume`. - :gh:`75`: Python 3 support. **Bug fixes** @@ -3073,13 +3061,12 @@ cases accessing the old names will work but it will cause a **Enhancements** -- :gh:`32`: Per-process CPU user/kernel times (:meth:`Process.cpu_times`). -- :gh:`33`: Per-process create time (:meth:`Process.create_time`). -- :gh:`34`: Per-process CPU utilization percentage - (:meth:`Process.cpu_percent`). -- :gh:`38`: Per-process memory usage (bytes) (:meth:`Process.memory_info`). -- :gh:`41`: Per-process memory percent (:meth:`Process.memory_percent`). -- :gh:`39`: System uptime (:func:`boot_time`). +- :gh:`32`: add :meth:`Process.cpu_times`. +- :gh:`33`: add :meth:`Process.create_time`. +- :gh:`34`: add :meth:`Process.cpu_percent`. +- :gh:`38`: add :meth:`Process.memory_info`. +- :gh:`41`: add :meth:`Process.memory_percent`. +- :gh:`39`: add :func:`boot_time`. - :gh:`43`: Total system virtual memory. - :gh:`46`: Total system physical memory. - :gh:`44`: Total system used/free virtual and physical memory. @@ -3101,15 +3088,14 @@ cases accessing the old names will work but it will cause a - :gh:`9`, [macOS], [Windows]: add ``Process.uid`` and ``Process.gid``, returning process UID and GID. - :gh:`11`: per-process parent object: :meth:`Process.parent` property - returns a - :class:`Process` object representing the parent process, and + returns a :class:`Process` object representing the parent process, and :meth:`Process.ppid` returns the parent PID. - :gh:`12`, :gh:`15`: :exc:`NoSuchProcess` exception now raised when creating an object for a nonexistent process, or when retrieving information about a process that has gone away. - :gh:`21`, [Windows]: :exc:`AccessDenied` exception created for raising access - denied errors from ``OSError`` or ``WindowsError`` on individual platforms. + denied errors from :exc:`OSError` or :exc:`WindowsError` on individual platforms. - :gh:`26`: :func:`process_iter` function to iterate over processes as :class:`Process` objects with a generator. - :class:`Process` objects can now also be compared with == operator for @@ -3119,7 +3105,7 @@ cases accessing the old names will work but it will cause a - :gh:`16`, [Windows]: Special case for "System Idle Process" (PID 0) which otherwise would return an "invalid parameter" exception. -- :gh:`17`: get_process_list() ignores :exc:`NoSuchProcess` and +- :gh:`17`: ``get_process_list()`` ignores :exc:`NoSuchProcess` and :exc:`AccessDenied` exceptions during building of the list. - :gh:`22`, [Windows]: :meth:`Process.kill` for PID 0 was failing with an unset exception. @@ -3127,5 +3113,5 @@ cases accessing the old names will work but it will cause a PID 0. - :gh:`24`, [Windows], **[critical]**: :meth:`Process.kill` for PID 0 now raises - :exc:`AccessDenied` exception instead of ``WindowsError``. + :exc:`AccessDenied` exception instead of :exc:`WindowsError`. - :gh:`30`: psutil.get_pid_list() was returning two 0 PIDs. diff --git a/docs/devguide.rst b/docs/devguide.rst index be60495724..73993dab49 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -70,7 +70,7 @@ Debug mode ---------- If you need to debug unusual situations or report a bug, you can enable -debug mode via the ``PSUTIL_DEBUG`` environment variable. In this +debug mode via the :envvar:`PSUTIL_DEBUG` environment variable. In this mode, psutil may print additional information to stderr. Usually these are non-severe error conditions that are ignored instead of causing a crash. Unit tests automatically run with debug mode enabled. On UNIX: @@ -87,7 +87,6 @@ On Windows: set PSUTIL_DEBUG=1 && python.exe script.py psutil-debug [psutil/arch/windows/proc.c:90]> NtWow64ReadVirtualMemory64(...) -> 998 (Unknown error) (ignored) - Coding style ------------ diff --git a/docs/migration.rst b/docs/migration.rst index b1ae1bb22c..61f6611822 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -30,7 +30,7 @@ Key breaking changes in 8.0: :meth:`Process.memory_footprint`. - New :meth:`Process.memory_info_ex` (unrelated to the old method deprecated in 4.0 and removed in 7.0). -- New :attr:`Process.attrs`: frozenset of valid attribute names; +- New :attr:`Process.attrs`: :class:`frozenset` of valid attribute names; ``process_iter(attrs=[])`` is deprecated. - Python 3.6 dropped. @@ -114,7 +114,7 @@ Named tuple field order changed :field:`private`, :field:`peak_pagefile`, :field:`num_page_faults`) were renamed. Old names still work but raise :exc:`DeprecationWarning`. :field:`paged_pool`, :field:`nonpaged_pool`, :field:`peak_paged_pool`, - :field:`peak_nonpaged_pool` were moved to :meth:`memory_info_ex`. + :field:`peak_nonpaged_pool` were moved to :meth:`Process.memory_info_ex`. - BSD: a new :field:`peak_rss` field was added. - :func:`virtual_memory`: on Windows, new :field:`cached` and :field:`wired` fields were @@ -139,10 +139,10 @@ to match the name used on Linux and BSD. The old name still works but raises Status and connection fields are now enums ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- :meth:`Process.status` now returns a :class:`psutil.ProcessStatus` member - instead of a plain ``str``. +- :meth:`Process.status` now returns a :class:`ProcessStatus` member instead of + a plain ``str``. - :meth:`Process.net_connections` and :func:`net_connections` :field:`status` field - now returns a :class:`psutil.ConnectionStatus` member instead of a plain + now returns a :class:`ConnectionStatus` member instead of a plain ``str``. Because both are :class:`enum.StrEnum` subclasses they compare equal to @@ -169,7 +169,7 @@ removed in 7.0 (which corresponded to what later became New Process.attrs class attribute ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:attr:`Process.attrs` is a new ``frozenset`` exposing the valid attribute +:attr:`Process.attrs` is a new :class:`frozenset` exposing the valid attribute names accepted by :meth:`Process.as_dict` and :func:`process_iter`. It replaces the previous pattern of creating a throwaway process just to discover available names: diff --git a/docs/recipes.rst b/docs/recipes.rst index 7bc3f5958d..5459507bea 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Recipes ======= From b8269e1dc01c304f86eda8d242687479905cd05d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 4 Apr 2026 21:21:21 +0200 Subject: [PATCH 1679/1714] Shorten changelog.rst entries --- docs/changelog.rst | 400 +++++++++++++++++---------------------------- 1 file changed, 151 insertions(+), 249 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 26205d18b2..1a00d15a69 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -197,11 +197,9 @@ Others: interface names. - :gh:`2782`, [FreeBSD]: :func:`cpu_count` ``logical=False`` return None on systems without hyper threading. -- :gh:`2791`, [FreeBSD]: The internal ``psutil_sysctl()`` and - ``psutil_sysctlbyname()`` helpers previously required the returned data size - to exactly match the allocated buffer size. Relaxed the check to allow the - kernel to return fewer bytes than the buffer (normal for variable-length - ``sysctl`` data), while still raising on overflow. +- :gh:`2791`, [FreeBSD]: relax ``psutil_sysctl()`` / ``psutil_sysctlbyname()`` + to allow the kernel to return fewer bytes than the buffer (normal for + variable-length ``sysctl`` data). - :gh:`2795`, [FreeBSD]: fix :func:`cpu_freq` failing with ``RuntimeError: sysctlbyname('dev.cpu.0.freq_levels') size mismatch`` on some systems. @@ -220,11 +218,9 @@ Others: **Enhancements** - :gh:`2705`: [Linux]: :meth:`Process.wait` now uses ``pidfd_open()`` + - ``poll()`` for waiting, resulting in no busy loop and faster response times. - Requires Linux >= 5.3 and Python >= 3.9. Falls back to traditional polling if - unavailable. -- :gh:`2705`: [macOS], [BSD]: :meth:`Process.wait` now uses ``kqueue()`` for - waiting, resulting in no busy loop and faster response times. + ``poll()`` (no busy loop). Requires Linux >= 5.3 and Python >= 3.9. +- :gh:`2705`: [macOS], [BSD]: :meth:`Process.wait` now uses ``kqueue()`` + (no busy loop). **Bug fixes** @@ -279,10 +275,8 @@ Others: - :gh:`2672`, [macOS], [BSD]: increase the chances to recognize zombie processes and raise the appropriate exception (:exc:`ZombieProcess`). - :gh:`2676`, :gh:`2678`: replace unsafe ``sprintf`` / ``snprintf`` / - ``sprintf_s`` calls with ``str_format`` utility. Replace ``strlcat`` / - ``strlcpy`` with safe ``str_copy`` / ``str_append``. This unifies string - handling across platforms and reduces unsafe usage of standard string - functions, improving robustness. + ``sprintf_s`` with ``str_format``, and ``strlcat`` / ``strlcpy`` with + ``str_copy`` / ``str_append``. Unifies string handling across platforms. **Bug fixes** @@ -292,10 +286,7 @@ Others: - :gh:`2675`, [macOS]: :meth:`Process.status` incorrectly returns :data:`STATUS_RUNNING` for 99% of the processes. - :gh:`2677`, [Windows]: fix MAC address string construction in - :func:`net_if_addrs`. Previously, the MAC address buffer was incorrectly - updated using a fixed increment and ``sprintf_s``, which could overflow or - misformat the string if the MAC length or formatting changed. Also, the - final ``'\n'`` was inserted unnecessarily. + :func:`net_if_addrs` (buffer overflow / misformat risk). - :gh:`2679`, [OpenBSD], [NetBSD], [critical]: can't build due to C syntax error. @@ -356,21 +347,17 @@ Others: **Bug fixes** - :gh:`2473`, [macOS]: Fix build issue on macOS 11 and lower. -- :gh:`2494`, [Windows]: All APIs dealing with paths, such as - :meth:`Process.memory_maps`, :meth:`Process.exe` and - :meth:`Process.open_files` does not properly handle UNC paths. Paths such - as ``\\??\\C:\\Windows\\Temp`` and - ``'\\Device\\HarddiskVolume1\\Windows\\Temp'`` are now converted to - ``C:\\Windows\\Temp``. (patch by Ben Peddell) +- :gh:`2494`, [Windows]: :meth:`Process.memory_maps`, :meth:`Process.exe` and + :meth:`Process.open_files` now properly handle UNC paths (e.g. + ``\\??\\C:\\Windows\\Temp`` → ``C:\\Windows\\Temp``). + (patch by Ben Peddell) - :gh:`2506`, [Windows]: Windows service APIs had issues with unicode services using special characters in their name. - :gh:`2514`, [Linux]: :meth:`Process.cwd` sometimes fail with :exc:`FileNotFoundError` due to a race condition. -- :gh:`2526`, [Linux]: :meth:`Process.create_time`, which is used to - univocally identify a process over time, is subject to system clock updates, - and as such can lead to :meth:`Process.is_running` returning a wrong - result. A monotonic creation time is now used instead. (patch by Jonathan - Kohler) +- :gh:`2526`, [Linux]: :meth:`Process.create_time` now uses a monotonic clock, + preventing :meth:`Process.is_running` from returning wrong results after + system clock updates. (patch by Jonathan Kohler) - :gh:`2528`, [Linux]: :meth:`Process.children` may raise :exc:`PermissionError`. It will now raise :exc:`AccessDenied` instead. - :gh:`2540`, [macOS]: :func:`boot_time` is off by 45 seconds (C precision @@ -408,20 +395,17 @@ Others: - :gh:`669`, [Windows]: :func:`net_if_addrs` also returns the :field:`broadcast` address instead of ``None``. -- :gh:`2480`: Python 2.7 is no longer supported. Latest version supporting - Python 2.7 is psutil 6.1.X. Install it with: ``pip2 install psutil==6.1.*``. -- :gh:`2490`: removed long deprecated :meth:`Process.memory_info_ex` method. It - was deprecated in psutil 4.0.0, released 8 years ago. Substitute is - :meth:`Process.memory_full_info`. +- :gh:`2480`: drop Python 2.7 support. Latest version supporting it is + psutil 6.1.X (``pip2 install psutil==6.1.*``). +- :gh:`2490`: remove long deprecated :meth:`Process.memory_info_ex` (deprecated + since 4.0.0). Use :meth:`Process.memory_full_info` instead. **Bug fixes** - :gh:`2496`, [Linux]: Avoid segfault (a cPython bug) on :meth:`Process.memory_maps` for processes that use hundreds of GBs of memory. -- :gh:`2502`, [macOS]: :func:`virtual_memory` now relies on - ``host_statistics64`` instead of ``host_statistics``. This is the same - approach used by ``vm_stat`` CLI tool, and should grant more accurate - results. +- :gh:`2502`, [macOS]: :func:`virtual_memory` now uses ``host_statistics64`` + (same as ``vm_stat``), more accurate. **Compatibility notes** @@ -447,10 +431,8 @@ Others: **Enhancements** -- :gh:`2366`, [Windows]: drastically speedup :func:`process_iter`. We now - determine process unique identity by using process "fast" create time method. - This will considerably speedup those apps which use :func:`process_iter` - only once, e.g. to look for a process with a certain name. +- :gh:`2366`, [Windows]: drastically speedup :func:`process_iter` by using + process "fast" create time to determine process identity. - :gh:`2446`: use pytest instead of unittest. - :gh:`2448`: add ``make install-sysdeps`` target to install the necessary system dependencies (python-dev, gcc, etc.) on all supported UNIX flavors. @@ -458,10 +440,8 @@ Others: targets. They can be used to install dependencies meant for running tests and for local development. They can also be installed via ``pip install .[test]`` and ``pip install .[dev]``. -- :gh:`2456`: allow to run tests via ``python3 -m psutil.tests`` even if - ``pytest`` module is not installed. This is useful for production - environments that don't have pytest installed, but still want to be able to - test psutil installation. +- :gh:`2456`: allow running tests via ``python3 -m psutil.tests`` even if + ``pytest`` is not installed. **Bug fixes** @@ -480,31 +460,26 @@ Others: **Enhancements** -- :gh:`2109`: :field:`maxfile` and :field:`maxpath` fields were removed from the - named tuple returned by :func:`disk_partitions`. Reason: on network - filesystems (NFS) this can potentially take a very long time to complete. +- :gh:`2109`: remove :field:`maxfile` and :field:`maxpath` from + :func:`disk_partitions` (can be very slow on NFS). - :gh:`2366`, [Windows]: log debug message when using slower process APIs. - :gh:`2375`, [macOS]: provide arm64 wheels. (patch by Matthieu Darbois) -- :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether - PIDs have been reused. This makes :func:`process_iter` around 20x times - faster. +- :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether PIDs + have been reused, making it around 20x faster. - :gh:`2396`: a new ``process_iter.cache_clear()`` API can be used to clear :func:`process_iter` internal cache. - :gh:`2401`, Support building with free-threaded CPython 3.13. (patch by Sam Gross) -- :gh:`2407`: :meth:`Process.connections` was renamed to - :meth:`Process.net_connections`. The old name is still available, but it's - deprecated (triggers a :exc:`DeprecationWarning`) and will be removed in the - future. +- :gh:`2407`: rename :meth:`Process.connections` to + :meth:`Process.net_connections`. Old name still works but is deprecated. - :gh:`2425`: [Linux]: provide aarch64 wheels. (patch by Matthieu Darbois / Ben Raz) **Bug fixes** -- :gh:`2250`, [NetBSD]: :meth:`Process.cmdline` sometimes fail with ``EBUSY``. - It usually happens for long cmdlines with lots of arguments. In this case retry - getting the cmdline for up to 50 times, and return an empty list as last - resort. +- :gh:`2250`, [NetBSD]: :meth:`Process.cmdline` sometimes fail with ``EBUSY`` + for long cmdlines. Now retries up to 50 times, returning an empty list as + last resort. - :gh:`2254`, [Linux]: offline cpus raise :exc:`NotImplementedError` in :func:`cpu_freq` (patch by Shade Gladden) - :gh:`2272`: Add pickle support to psutil Exceptions. @@ -524,27 +499,19 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`2109`: the named tuple returned by :func:`disk_partitions`' no longer has :field:`maxfile` and :field:`maxpath` fields. -- :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether - PIDs have been reused. If you want to check for PID reusage you are supposed - to use - :meth:`Process.is_running` against the yielded :class:`Process` instances. - That will also automatically remove reused PIDs from :func:`process_iter` - internal cache. -- :gh:`2407`: :meth:`Process.connections` was renamed to - :meth:`Process.net_connections`. The old name is still available, but it's - deprecated (triggers a :exc:`DeprecationWarning`) and will be removed in the - future. +- :gh:`2396`: :func:`process_iter` no longer pre-emptively checks whether PIDs + have been reused. Use :meth:`Process.is_running` on yielded instances instead + (also removes reused PIDs from the internal cache). +- :gh:`2407`: rename :meth:`Process.connections` to + :meth:`Process.net_connections`. Old name still works but is deprecated. 5.9.8 — 2024-01-19 ^^^^^^^^^^^^^^^^^^ **Enhancements** -- :gh:`2343`, [FreeBSD]: filter :func:`net_connections` returned list in C - instead of Python, and avoid to retrieve unnecessary connection types unless - explicitly asked. E.g., on an IDLE system with few IPv6 connections this will - run around 4 times faster. Before all connection types (TCP, UDP, UNIX) were - retrieved internally, even if only a portion was returned. +- :gh:`2343`, [FreeBSD]: filter :func:`net_connections` in C instead of Python, + ~4x faster. Only requested connection types are now retrieved. - :gh:`2342`, [NetBSD]: same as above (:gh:`2343`) but for NetBSD. - :gh:`2349`: adopted black formatting style. @@ -578,18 +545,13 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`1703`: :func:`cpu_percent` and :func:`cpu_times_percent` are now - thread safe, meaning they can be called from different threads and still - return meaningful and independent results. Before, if (say) 10 threads called - ``cpu_percent(interval=None)`` at the same time, only 1 thread out of 10 - would get the right result. + thread safe. - :gh:`2266`: if :class:`Process` class is passed a very high PID, raise :exc:`NoSuchProcess` instead of :exc:`OverflowError`. (patch by Xuehai Pan) - :gh:`2246`: drop python 3.4 & 3.5 support. (patch by Matthieu Darbois) -- :gh:`2290`: PID reuse is now pre-emptively checked for :meth:`Process.ppid` - and - :meth:`Process.parents`. -- :gh:`2312`: use ``ruff`` Python linter instead of ``flake8 + isort``. It's an - order of magnitude faster + it adds a ton of new code quality checks. +- :gh:`2290`: PID reuse is now pre-emptively checked for + :meth:`Process.ppid` and :meth:`Process.parents`. +- :gh:`2312`: use ``ruff`` linter instead of ``flake8 + isort``. **Bug fixes** @@ -630,21 +592,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: the :exc:`KeyError` bit deriving from a missed cache hit. - :gh:`2217`: print the full traceback when a :exc:`DeprecationWarning` or :exc:`UserWarning` is raised. -- :gh:`2230`, [OpenBSD]: :func:`net_connections` implementation was rewritten - from scratch: - - - We're now able to retrieve the path of :data:`socket.AF_UNIX` sockets - (before it was an empty string) - - The function is faster since it no longer iterates over all processes. - - No longer produces duplicate connection entries. -- :gh:`2238`: there are cases where :meth:`Process.cwd` cannot be determined - (e.g. directory no longer exists), in which case we returned either ``None`` - or an empty string. This was consolidated and we now return ``""`` on all - platforms. -- :gh:`2239`, [UNIX]: if process is a zombie, and we can only determine part of - the its truncated :meth:`Process.name` (15 chars), don't fail with - :exc:`ZombieProcess` when we try to guess the full name from the - :meth:`Process.cmdline`. Just return the truncated name. +- :gh:`2230`, [OpenBSD]: :func:`net_connections` rewritten from scratch: now + retrieves :data:`socket.AF_UNIX` socket paths, is faster, and no longer + produces duplicates. +- :gh:`2238`: :meth:`Process.cwd` now consistently returns ``""`` on all + platforms when the directory can't be determined. +- :gh:`2239`, [UNIX]: for zombie processes, return the truncated + :meth:`Process.name` (15 chars) instead of raising :exc:`ZombieProcess` + when the full name can't be determined from :meth:`Process.cmdline`. - :gh:`2240`, [NetBSD], [OpenBSD]: add CI testing on every commit for NetBSD and OpenBSD platforms (python 3 only). @@ -683,8 +638,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** - :gh:`2102`: use Limited API when building wheels with CPython 3.6+ on Linux, - macOS and Windows. This allows to use pre-built wheels in all future versions - of cPython 3. (patch by Matthieu Darbois) + macOS and Windows. (patch by Matthieu Darbois) **Bug fixes** @@ -747,12 +701,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: van Kemenade) - :gh:`2037`: add :field:`flags` field to :func:`net_if_stats`. - :gh:`2050`, [Linux]: increase ``read(2)`` buffer size from 1k to 32k when - reading ``/proc`` pseudo files line by line. This should help having more - consistent results. + reading ``/proc`` pseudo files line by line. - :gh:`2057`, [OpenBSD]: add support for :func:`cpu_freq`. -- :gh:`2107`, [Linux]: :meth:`Process.memory_full_info` (reporting process - USS/PSS/Swap memory) now reads ``/proc/pid/smaps_rollup`` instead of - ``/proc/pid/smaps``, which makes it 5 times faster. +- :gh:`2107`, [Linux]: :meth:`Process.memory_full_info` now reads + ``/proc/pid/smaps_rollup`` instead of ``/proc/pid/smaps`` (5x faster). **Bug fixes** @@ -768,14 +720,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1851`, [Linux]: :func:`cpu_freq` is slow on systems with many CPUs. - Read current frequency values for all CPUs from ``/proc/cpuinfo`` instead of - opening many files in ``/sys`` fs. (patch by marxin) +- :gh:`1851`, [Linux]: :func:`cpu_freq` reads from ``/proc/cpuinfo`` instead of + many files in ``/sys`` fs, faster on systems with many CPUs. (patch by + marxin) - :gh:`1992`: :exc:`NoSuchProcess` message now specifies if the PID has been - reused. -- :gh:`1992`: error classes (:exc:`NoSuchProcess`, :exc:`AccessDenied`, etc.) - now have a better formatted and separated ``__repr__`` and ``__str__`` - implementations. + reused. Error classes now have improved ``__repr__`` and ``__str__``. - :gh:`1996`, [BSD]: add support for MidnightBSD. (patch by Saeed Rasooli) - :gh:`1999`, [Linux]: :func:`disk_partitions`: convert ``/dev/root`` device (an alias used on some Linux distros) to real root device path. @@ -793,13 +742,11 @@ Version 6.0.0 introduces some changes which affect backward compatibility: drives where it first finds one. - :gh:`1874`, [SunOS]: swap output error due to incorrect range. - :gh:`1892`, [macOS]: :func:`cpu_freq` broken on Apple M1. -- :gh:`1901`, [macOS]: different functions, especially - :meth:`Process.open_files` and - :meth:`Process.connections`, could randomly raise :exc:`AccessDenied` - because the internal buffer of ``proc_pidinfo(PROC_PIDLISTFDS)`` syscall - was not big enough. We now dynamically increase the buffer size until - it's big enough instead of giving up and raising :exc:`AccessDenied`, - which was a fallback to avoid crashing. +- :gh:`1901`, [macOS]: :meth:`Process.open_files`, + :meth:`Process.connections` and others could randomly raise + :exc:`AccessDenied` because the internal buffer of + ``proc_pidinfo(PROC_PIDLISTFDS)`` was too small. Now dynamically + increased until sufficient. - :gh:`1904`, [Windows]: ``OpenProcess`` fails with ``ERROR_SUCCESS`` due to ``GetLastError()`` called after ``sprintf()``. (patch by alxchk) - :gh:`1913`, [Linux]: :func:`wait_procs` should catch @@ -817,8 +764,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1965`, [Windows], **[critical]**: fix "Fatal Python error: deallocating None" when calling :func:`users` multiple times. - :gh:`1980`, [Windows]: 32bit / WoW64 processes fails to read - :meth:`Process.name` longer than 128 characters resulting in - :exc:`AccessDenied`. This is now fixed. (patch by PetrPospisil) + :meth:`Process.name` longer than 128 characters. (patch by PetrPospisil) - :gh:`1991`, **[critical]**: :func:`process_iter` is not thread safe and can raise :exc:`TypeError` if invoked from multiple threads. - :gh:`1956`, [macOS]: :meth:`Process.cpu_times` reports incorrect timings on @@ -835,9 +781,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: :field:`maxpath`, which are the maximum file name and path name length. - :gh:`1872`, [Windows]: added support for PyPy 2.7. - :gh:`1879`: provide pre-compiled wheels for Linux and macOS (yey!). -- :gh:`1880`: get rid of Travis and Cirrus CI services (they are no longer - free). CI testing is now done by GitHub Actions on Linux, macOS and FreeBSD - (yes). AppVeyor is still being used for Windows CI. +- :gh:`1880`: switch CI from Travis/Cirrus to GitHub Actions (Linux, macOS, + FreeBSD). AppVeyor still used for Windows. **Bug fixes** @@ -963,17 +908,16 @@ Version 6.0.0 introduces some changes which affect backward compatibility: 12.0. - :gh:`1656`, [Windows]: :meth:`Process.memory_full_info` raises :exc:`AccessDenied` even for the current user and os.getpid(). -- :gh:`1660`, [Windows]: :meth:`Process.open_files` complete rewrite + check - of errors. +- :gh:`1660`, [Windows]: :meth:`Process.open_files` rewritten with proper error + handling. - :gh:`1662`, [Windows], **[critical]**: :meth:`Process.exe` may raise "[WinError 0] The operation completed successfully". - :gh:`1665`, [Linux]: :func:`disk_io_counters` does not take into account extra fields added to recent kernels. (patch by Mike Hommey) -- :gh:`1672`: use the right C type when dealing with PIDs (int or long). Thus - far (long) was almost always assumed, which is wrong on most platforms. +- :gh:`1672`: use the right C type when dealing with PIDs (int or long). - :gh:`1673`, [OpenBSD]: :meth:`Process.connections`, - :meth:`Process.num_fds` and - :meth:`Process.threads` returned improper exception if process is gone. + :meth:`Process.num_fds` and :meth:`Process.threads` raised wrong exception + if process is gone. - :gh:`1674`, [SunOS]: :func:`disk_partitions` may raise :exc:`OSError`. - :gh:`1684`, [Linux]: :func:`disk_io_counters` may raise :exc:`ValueError` on systems not having ``/proc/diskstats``. @@ -993,9 +937,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** -- :gh:`1179`, [Linux]: :meth:`Process.cmdline` now takes into account - misbehaving processes renaming the command line and using inappropriate chars - to separate args. +- :gh:`1179`, [Linux]: :meth:`Process.cmdline` now handles processes that use + inappropriate chars to separate args. - :gh:`1616`, **[critical]**: use of ``Py_DECREF`` instead of ``Py_CLEAR`` will result in double ``free()`` and segfault (`CVE-2019-18874 `__). (patch @@ -1071,17 +1014,15 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`604`, [Windows]: add new :func:`getloadavg`, returning system load average calculation, including on Windows (emulated). (patch by Ammar Askar) -- :gh:`1404`, [Linux]: :func:`cpu_count` with ``logical=False`` uses a second - method (read from ``/sys/devices/system/cpu/cpu[0-9]/topology/core_id``) in - order to determine the number of CPU cores in case ``/proc/cpuinfo`` does not - provide this info. +- :gh:`1404`, [Linux]: :func:`cpu_count` with ``logical=False`` falls back to + reading ``/sys/devices/system/cpu/*/topology/core_id`` if ``/proc/cpuinfo`` + doesn't provide the info. - :gh:`1458`: provide coloured test output. Also show failures on ``KeyboardInterrupt``. - :gh:`1464`: various docfixes (always point to Python 3 doc, fix links, etc.). -- :gh:`1476`, [Windows]: it is now possible to set process high I/O priority - (:meth:`Process.ionice`). Also, I/O priority values are now exposed as 4 - new constants: :data:`IOPRIO_VERYLOW`, :data:`IOPRIO_LOW`, :data:`IOPRIO_NORMAL`, - :data:`IOPRIO_HIGH`. +- :gh:`1476`, [Windows]: :meth:`Process.ionice` can now set high I/O priority. + New constants: :data:`IOPRIO_VERYLOW`, :data:`IOPRIO_LOW`, + :data:`IOPRIO_NORMAL`, :data:`IOPRIO_HIGH`. - :gh:`1478`: add make command to re-run tests failed on last run. **Bug fixes** @@ -1133,15 +1074,13 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1379`, [Windows]: :meth:`Process.suspend` and :meth:`Process.resume` - now use ``NtSuspendProcess`` and ``NtResumeProcess`` instead of - stopping/resuming all threads of a process. This is faster and more reliable - (aka this is what ProcessHacker does). +- :gh:`1379`, [Windows]: :meth:`Process.suspend` and :meth:`Process.resume` now + use ``NtSuspendProcess`` / ``NtResumeProcess`` instead of stopping / resuming + all threads. Faster and more reliable. - :gh:`1420`, [Windows]: in case of exception :func:`disk_usage` now also shows the path name. -- :gh:`1422`, [Windows]: Windows APIs requiring to be dynamically loaded from - DLL libraries are now loaded only once on startup (instead of on per function - call) significantly speeding up different functions and methods. +- :gh:`1422`, [Windows]: DLL-loaded Windows APIs are now loaded once on startup + instead of per function call, significantly faster. - :gh:`1426`, [Windows]: ``PAGESIZE`` and number of processors is now calculated on startup. - :gh:`1428`: in case of error, the traceback message now shows the underlying @@ -1189,9 +1128,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1348`, [Windows]: on Windows >= 8.1 if :meth:`Process.cmdline` fails - due to ``ERROR_ACCESS_DENIED`` attempt using ``NtQueryInformationProcess`` + - ``ProcessCommandLineInformation``. (patch by EccoTheFlintstone) +- :gh:`1348`, [Windows]: on Windows >= 8.1, :meth:`Process.cmdline` falls back + to ``NtQueryInformationProcess`` on ``ERROR_ACCESS_DENIED``. (patch by + EccoTheFlintstone) **Bug fixes** @@ -1227,9 +1166,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: :meth:`Process.oneshot` context causes :class:`Process` instances to return incorrect results. - :gh:`1376`, [Windows]: ``OpenProcess`` now uses - ``PROCESS_QUERY_LIMITED_INFORMATION`` access rights wherever possible, - resulting in less :exc:`AccessDenied` exceptions being thrown for system - processes. + ``PROCESS_QUERY_LIMITED_INFORMATION`` where possible, reducing + :exc:`AccessDenied` for system processes. - :gh:`1376`, [Windows]: check if variable is ``NULL`` before ``free()`` ing it. (patch by EccoTheFlintstone) @@ -1238,11 +1176,10 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1197`, [Linux]: :func:`cpu_freq` is now implemented by parsing - ``/proc/cpuinfo`` in case ``/sys/devices/system/cpu/*`` filesystem is not - available. -- :gh:`1310`, [Linux]: :func:`sensors_temperatures` now parses - ``/sys/class/thermal`` in case ``/sys/class/hwmon`` fs is not available (e.g. +- :gh:`1197`, [Linux]: :func:`cpu_freq` falls back to ``/proc/cpuinfo`` if + ``/sys/devices/system/cpu/*`` is not available. +- :gh:`1310`, [Linux]: :func:`sensors_temperatures` falls back to + ``/sys/class/thermal`` if ``/sys/class/hwmon`` is not available (e.g. Raspberry Pi). (patch by Alex Manuskin) - :gh:`1320`, [POSIX]: better compilation support when using g++ instead of GCC. (patch by Jaime Fullaondo) @@ -1274,9 +1211,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: new :data:`MACOS`. - :gh:`1309`, [Linux]: added :data:`STATUS_PARKED` constant for :meth:`Process.status`. -- :gh:`1321`, [Linux]: add :func:`disk_io_counters` dual implementation - relying on ``/sys/block`` filesystem in case ``/proc/diskstats`` is not - available. (patch by Lawrence Ye) +- :gh:`1321`, [Linux]: :func:`disk_io_counters` falls back to ``/sys/block`` if + ``/proc/diskstats`` is not available. (patch by Lawrence Ye) **Bug fixes** @@ -1385,15 +1321,14 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`1173`: introduced :envvar:`PSUTIL_DEBUG` environment variable which can - be set in order to print useful debug messages on stderr (useful in case of - nasty errors). +- :gh:`1173`: add :envvar:`PSUTIL_DEBUG` environment variable to print debug + messages on stderr. - :gh:`1177`, [macOS]: added support for :func:`sensors_battery`. (patch by Arnon Yaari) - :gh:`1183`: :meth:`Process.children` is 2x faster on POSIX and 2.4x faster on Linux. -- :gh:`1188`: deprecated method :meth:`Process.memory_info_ex` now warns by - using :exc:`FutureWarning` instead of :exc:`DeprecationWarning`. +- :gh:`1188`: :meth:`Process.memory_info_ex` now warns with + :exc:`FutureWarning` instead of :exc:`DeprecationWarning`. **Bug fixes** @@ -1401,9 +1336,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1169`, [Linux]: :func:`users` ``hostname`` returns username instead. (patch by janderbrain) - :gh:`1172`, [Windows]: ``make test`` does not work. -- :gh:`1179`, [Linux]: :meth:`Process.cmdline` is now able to split cmdline - args for misbehaving processes which overwrite ``/proc/pid/cmdline`` and use - spaces instead of null bytes as args separator. +- :gh:`1179`, [Linux]: :meth:`Process.cmdline` can now split args for processes + that overwrite ``/proc/pid/cmdline`` with spaces instead of null bytes. - :gh:`1181`, [macOS]: :meth:`Process.memory_maps` may raise ``ENOENT``. - :gh:`1187`, [macOS]: :func:`pids` does not return PID 0 on recent macOS versions. @@ -1474,25 +1408,19 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhancements** -- :gh:`802`: :func:`disk_io_counters` and :func:`net_io_counters` numbers - no longer wrap (restart from 0). Introduced a new ``nowrap`` argument. +- :gh:`802`: :func:`disk_io_counters` and :func:`net_io_counters` no longer + wrap (restart from 0). New ``nowrap`` argument. - :gh:`928`: :func:`net_connections` and :meth:`Process.connections` :field:`laddr` and :field:`raddr` are now named tuples. -- :gh:`1015`: :func:`swap_memory` now relies on ``/proc/meminfo`` instead of - ``sysinfo()`` syscall so that it can be used in conjunction with - :data:`PROCFS_PATH` in order to retrieve memory info about Linux containers - such as Docker and Heroku. +- :gh:`1015`: :func:`swap_memory` now reads ``/proc/meminfo`` instead of + ``sysinfo()`` syscall, so it works with :data:`PROCFS_PATH` for containers. - :gh:`1022`: :func:`users` provides a new :field:`pid` field. -- :gh:`1025`: :func:`process_iter` accepts two new parameters in order to - invoke - :meth:`Process.as_dict`: with *attrs* and *ad_value* parameters. - With these you can iterate over all processes in one shot without needing to - catch :exc:`NoSuchProcess` and do list/dict comprehensions. +- :gh:`1025`: :func:`process_iter` accepts new *attrs* and *ad_value* + parameters to invoke :meth:`Process.as_dict` inline. - :gh:`1040`: implemented full unicode support. - :gh:`1051`: :func:`disk_usage` on Python 3 is now able to accept bytes. - :gh:`1058`: test suite now enables all warnings by default. -- :gh:`1060`: source distribution is dynamically generated so that it only - includes relevant files. +- :gh:`1060`: source distribution now only includes relevant files. - :gh:`1079`, [FreeBSD]: :func:`net_connections` :field:`fd` number is now being set for real (instead of ``-1``). (patch by Gleb Smirnoff) - :gh:`1091`, [SunOS]: implemented :meth:`Process.environ`. (patch by @@ -1502,9 +1430,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`989`, [Windows]: :func:`boot_time` may return a negative value. - :gh:`1007`, [Windows]: :func:`boot_time` can have a 1 sec fluctuation - between calls. The value of the first call is now cached so that - :func:`boot_time` always returns the same value if fluctuation is <= 1 - second. + between calls. The first call value is now cached. - :gh:`1013`, [FreeBSD]: :func:`net_connections` may return incorrect PID. (patch by Gleb Smirnoff) - :gh:`1014`, [Linux]: :class:`Process` class can mask legitimate ``ENOENT`` @@ -1534,10 +1460,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`1048`, [Windows]: :func:`users`'s :field:`host` field report an invalid IP address. - :gh:`1050`, [Windows]: :meth:`Process.memory_maps` leaks memory. -- :gh:`1055`: :func:`cpu_count` is no longer cached. This is useful on - systems such as Linux where CPUs can be disabled at runtime. This also - reflects on - :meth:`Process.cpu_percent` which no longer uses the cache. +- :gh:`1055`: :func:`cpu_count` is no longer cached (CPUs can be disabled at + runtime on Linux). :meth:`Process.cpu_percent` also affected. - :gh:`1058`: fixed Python warnings. - :gh:`1062`: :func:`disk_io_counters` and :func:`net_io_counters` raise :exc:`TypeError` if no disks or NICs are installed on the system. @@ -1568,9 +1492,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: returning ``None`` and assumes 1 instead. - :gh:`1093`, [SunOS]: :meth:`Process.memory_maps` shows wrong 64 bit addresses. -- :gh:`1094`, [Windows]: :func:`pid_exists` may lie. Also, all process APIs - relying on ``OpenProcess`` Windows API now check whether the PID is actually - running. +- :gh:`1094`, [Windows]: fix :func:`pid_exists` returning wrong result. All + ``OpenProcess`` APIs now verify the PID is actually running. - :gh:`1098`, [Windows]: :meth:`Process.wait` may erroneously return sooner, when the PID is still alive. - :gh:`1099`, [Windows]: :meth:`Process.terminate` may raise @@ -1728,9 +1651,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Enhncements** -- :gh:`799`: new :meth:`Process.oneshot` context manager making - :class:`Process` methods around +2x faster in general and from +2x to +6x - faster on Windows. +- :gh:`799`: new :meth:`Process.oneshot` context manager (+2x faster in + general, +2x to +6x on Windows). - :gh:`943`: better error message in case of version conflict on import. **Bug fixes** @@ -1762,10 +1684,9 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`874`, [Windows]: make :func:`net_if_addrs` also return the :field:`netmask`. -- :gh:`887`, [Linux]: :func:`virtual_memory` :field:`available` and :field:`used` - values are more precise and match ``free`` cmdline utility. :field:`available` - also takes into account LCX containers preventing :field:`available` to overflow - :field:`total`. +- :gh:`887`, [Linux]: :func:`virtual_memory` :field:`available` and + :field:`used` are more precise and match ``free`` utility. Also handles + LXC containers. - :gh:`891`: `scripts/procinfo.py`_ has been updated and provides a lot more info. @@ -2032,12 +1953,8 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`648`: CI test integration for macOS. (patch by Jeff Tang) - :gh:`663`, [POSIX]: :func:`net_if_addrs` now returns point-to-point (VPNs) addresses. -- :gh:`655`, [Windows]: different issues regarding unicode handling were fixed. - On Python 2 all APIs returning a string will now return an encoded version of - it by using sys.getfilesystemencoding() codec. The APIs involved are: - :func:`net_if_addrs`, :func:`net_if_stats`, :func:`net_io_counters`, - :meth:`Process.cmdline`, :meth:`Process.name`, - :meth:`Process.username`, :func:`users`. +- :gh:`655`, [Windows]: fix various unicode handling issues. On Python 2, + string APIs now return encoded strings using ``sys.getfilesystemencoding()``. **Bug fixes** @@ -2083,8 +2000,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: **Bug fixes** - :gh:`340`, [Windows], **[critical]**: :meth:`Process.open_files` no longer - hangs. Instead it uses a thread which times out and skips the file handle in - case it's taking too long to be retrieved. (patch by Jeff Tang) + hangs (uses a thread with timeout). (patch by Jeff Tang) - :gh:`627`, [Windows]: :meth:`Process.name` no longer raises :exc:`AccessDenied` for pids owned by another user. - :gh:`636`, [Windows]: :meth:`Process.memory_info` raise @@ -2175,8 +2091,7 @@ Version 6.0.0 introduces some changes which affect backward compatibility: - :gh:`521`: drop support for Python 2.4 and 2.5. - :gh:`553`: add `scripts/pstree.py`_. -- :gh:`564`: C extension version mismatch in case the user messed up with - psutil installation or with sys.path is now detected at import time. +- :gh:`564`: C extension version mismatch is now detected at import time. - :gh:`568`: add `scripts/pidof.py`_. - :gh:`569`, [FreeBSD]: add support for :meth:`Process.cpu_affinity` on FreeBSD. @@ -2678,33 +2593,24 @@ cases accessing the old names will work but it will cause a - :gh:`261`: add :meth:`Process.memory_info_ex`. - :gh:`295`, [macOS]: :meth:`Process.exe` path is now determined by asking the OS instead of being guessed from :meth:`Process.cmdline`. -- :gh:`297`, [macOS]: the :class:`Process` methods below were always raising - :exc:`AccessDenied` for any process except the current one. Now this is no - longer true. Also they are 2.5x faster. :meth:`Process.name`, - :meth:`Process.memory_info`, +- :gh:`297`, [macOS]: :meth:`Process.name`, :meth:`Process.memory_info`, :meth:`Process.memory_percent`, :meth:`Process.cpu_times`, - :meth:`Process.cpu_percent`, - :meth:`Process.num_threads`. + :meth:`Process.cpu_percent`, :meth:`Process.num_threads` no longer raise + :exc:`AccessDenied` for other users' processes and are 2.5x faster. - :gh:`300`: add `scripts/pmap.py`_. - :gh:`301`: :func:`process_iter` now yields processes sorted by their PIDs. - :gh:`302`: add :meth:`Process.num_ctx_switches`. -- :gh:`303`, [Windows]: the :class:`Process` methods below were always raising - :exc:`AccessDenied` for any process not owned by current user. Now this is no - longer true: - :meth:`Process.create_time`, :meth:`Process.cpu_times`, - :meth:`Process.cpu_percent`, - :meth:`Process.memory_info`, :meth:`Process.memory_percent`, - :meth:`Process.num_handles`, - :meth:`Process.io_counters`. +- :gh:`303`, [Windows]: :meth:`Process.create_time`, :meth:`Process.cpu_times`, + :meth:`Process.cpu_percent`, :meth:`Process.memory_info`, + :meth:`Process.memory_percent`, :meth:`Process.num_handles`, + :meth:`Process.io_counters` no longer raise :exc:`AccessDenied` for other + users' processes. - :gh:`305`: add `scripts/netstat.py`_. -- :gh:`311`: system memory functions has been refactorized and rewritten and - now provide a more detailed and consistent representation of the system - memory. Added new :func:`virtual_memory` and :func:`swap_memory` - functions. All old memory-related functions are deprecated. Also two new - example scripts were added: `scripts/free.py`_ and `scripts/meminfo.py`_. -- :gh:`312`: :func:`net_io_counters` named tuple includes 4 new fields: :field:`errin`, - :field:`errout`, :field:`dropin` and :field:`dropout`, reflecting the number - of packets dropped and with errors. +- :gh:`311`: add :func:`virtual_memory` and :func:`swap_memory`. Old + memory-related functions are deprecated. New example scripts: + `scripts/free.py`_ and `scripts/meminfo.py`_. +- :gh:`312`: :func:`net_io_counters` adds 4 new fields: :field:`errin`, + :field:`errout`, :field:`dropin` and :field:`dropout`. **Bug fixes** @@ -2775,9 +2681,8 @@ cases accessing the old names will work but it will cause a to work with Python 3. - :gh:`278`: add :meth:`Process.as_dict`. - :gh:`281`: :meth:`Process.ppid`, :meth:`Process.name`, - :meth:`Process.exe`, - :meth:`Process.cmdline` and :meth:`Process.create_time` properties of - :class:`Process` class are now cached after being accessed. + :meth:`Process.exe`, :meth:`Process.cmdline` and + :meth:`Process.create_time` are now cached after first access. - :gh:`282`: ``psutil.STATUS_*`` constants can now be compared by using their string representation. - :gh:`283`: speedup :meth:`Process.is_running` by caching its return value @@ -2890,10 +2795,8 @@ cases accessing the old names will work but it will cause a - :gh:`125`: add :func:`cpu_times` and :func:`cpu_percent` per-cpu support. - :gh:`163`: add :meth:`Process.terminal`. -- :gh:`171`: added ``get_phymem()`` and ``get_virtmem()`` functions returning - system memory information (:field:`total`, :field:`used`, :field:`free`) and - memory percent usage. ``total_*``, ``avail_*`` and ``used_*`` memory functions - are deprecated. +- :gh:`171`: add ``get_phymem()`` and ``get_virtmem()``. Old ``total_*``, + ``avail_*`` and ``used_*`` memory functions are deprecated. - :gh:`172`: add :func:`disk_usage`. - :gh:`174`: add :func:`disk_partitions`. - :gh:`179`: setuptools is now used in setup.py @@ -2929,12 +2832,12 @@ cases accessing the old names will work but it will cause a - :gh:`142`: add :meth:`Process.nice`. - :gh:`143`: add :meth:`Process.status`. - :gh:`147` [Linux]: add :meth:`Process.ionice`. -- :gh:`148`: add :class:`Popen` class which tidies up :class:`subprocess.Popen` - and :class:`Process` class in a single interface. -- :gh:`152`, [macOS]: :meth:`Process.open_files` implementation has been - rewritten in C and no longer relies on ``lsof`` resulting in a 3x speedup. -- :gh:`153`, [macOS]: :meth:`Process.connections` implementation has been - rewritten in C and no longer relies on ``lsof`` resulting in a 3x speedup. +- :gh:`148`: add :class:`Popen` class combining :class:`subprocess.Popen` and + :class:`Process` in a single interface. +- :gh:`152`, [macOS]: :meth:`Process.open_files` rewritten in C (no longer + relies on ``lsof``, 3x faster). +- :gh:`153`, [macOS]: :meth:`Process.connections` rewritten in C (no longer + relies on ``lsof``, 3x faster). **Bug fixes** @@ -2975,9 +2878,8 @@ cases accessing the old names will work but it will cause a - :gh:`111`: add :meth:`Process.exe`. - :gh:`113`: exception messages now include :meth:`Process.name` and :attr:`Process.pid`. -- :gh:`114`, [Windows]: :meth:`Process.username` has been rewritten in pure C - and no longer uses WMI resulting in a big speedup. Also, pywin32 is no longer - required as a third-party dependency. (patch by wj32) +- :gh:`114`, [Windows]: :meth:`Process.username` rewritten in C (no longer + uses WMI, much faster, pywin32 no longer required). (patch by wj32) - :gh:`117`, [Windows]: added support for Windows 2000. - :gh:`123`: :func:`cpu_percent` and :meth:`Process.cpu_percent` accept a new ``interval`` parameter. From 55dfba1b86900ef90c1a89b096f3b6783d65f1c0 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 5 Apr 2026 02:33:54 +0200 Subject: [PATCH 1680/1714] Doc dark theme (#2803) --- MANIFEST.in | 4 +- docs/_sponsors.html | 46 ++- docs/_static/css/custom.css | 454 ++++++++++++++++++++------ docs/_static/images/icon-memory.svg | 24 +- docs/_static/images/logo-apivoid.svg | 3 +- docs/_static/images/logo-sansec.svg | 2 +- docs/_static/images/logo-tidelift.svg | 1 + docs/_static/js/highlight-numbers.js | 21 -- docs/_static/js/highlight-repl.js | 20 ++ docs/_static/js/theme-toggle.js | 30 ++ docs/_templates/layout.html | 20 ++ docs/_templates/searchbox.html | 16 + docs/api.rst | 4 +- docs/conf.py | 3 +- docs/index.rst | 14 +- 15 files changed, 505 insertions(+), 157 deletions(-) delete mode 100644 docs/_static/js/highlight-numbers.js create mode 100644 docs/_static/js/highlight-repl.js create mode 100644 docs/_static/js/theme-toggle.js create mode 100644 docs/_templates/searchbox.html diff --git a/MANIFEST.in b/MANIFEST.in index 918bdff6f2..59293f4918 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -35,12 +35,14 @@ include docs/_static/images/logo-psutil.svg include docs/_static/images/logo-sansec.svg include docs/_static/images/logo-tidelift.svg include docs/_static/js/external-urls.js -include docs/_static/js/highlight-numbers.js +include docs/_static/js/highlight-repl.js +include docs/_static/js/theme-toggle.js include docs/_static/logo.svg include docs/_templates/footer.html include docs/_templates/icon-footer-github.svg include docs/_templates/icon-footer-pypi.svg include docs/_templates/layout.html +include docs/_templates/searchbox.html include docs/adoption.rst include docs/alternatives.rst include docs/api-overview.rst diff --git a/docs/_sponsors.html b/docs/_sponsors.html index 48443b0014..25c894de66 100644 --- a/docs/_sponsors.html +++ b/docs/_sponsors.html @@ -2,17 +2,57 @@ - + + + + + + + + + + + + + + + + + + - + + + + + + + + - + + + + + + + + + + + + + diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 9e653fdf55..6d5389e3a3 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -1,94 +1,173 @@ /* ================================================================== */ -/* Variables */ +/* Sizes and effects */ /* ================================================================== */ :root { - /* base palette */ - --gray-100: #f5f5f5; - --gray-200: #f0f4f7; - --gray-300: #d6d6d6; - --gray-400: #ccc; - --gray-500: #555; - --gray-600: #3b3b3b; - --gray-700: #222; - --gray-800: #000; - - --blue-200: #f3f9fe; - --blue-300: #b0cfe8; - --blue-400: #2980b9; - --blue-500: #336699; - --blue-600: #224466; - - --green-200: #f0faf0; - --green-400: #8aba8a; - --green-500: #4fc464; - --green-700: #296433; - - --yellow-300: #f4e34c; - --yellow-700: #854826; - - --red-200: #fff0f0; - --red-400: #e8a0a0; - --red-500: #f44c4e; - --red-700: #9f3133; - - --dark-100: #2d2e27; - - /* semantic */ --border-radius: 3px; - --h1-h2-border: var(--gray-400); - + --theme-transition: 0.2s ease; --adm-padding-y: 5px; - --adm-title: black; - --code-font: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; --code-line-height: 1.5; - --code-bg: var(--dark-100); - - --adm-note-bg: var(--gray-100); - --adm-note-border: var(--gray-400); - - --adm-seealso-accent-border: var(--blue-500); - --adm-seealso-bg: var(--blue-200); - --adm-seealso-border: var(--blue-300); - --adm-seealso-hover: var(--blue-400); - --adm-seealso-title: var(--blue-600); - --adm-tip-bg: var(--green-200); - --adm-tip-border: var(--green-400); - --adm-warning-bg: var(--red-200); - --adm-warning-border: var(--red-400); - - --sidebar-search-bg: var(--gray-600); - - --versionadded-border: var(--green-500); - --versionadded-color: var(--green-700); - --versionchanged-border: var(--yellow-300); - --versionchanged-color: var(--yellow-700); - --deprecated-border: var(--red-500); - --deprecated-color: var(--red-700); - - --func-sig-bg: var(--gray-200); - --func-sig-border: var(--blue-500); - --func-sig-text: var(--gray-700); - --func-sig-name: var(--gray-800); - --func-sig-param: var(--gray-500); - - --table-header-bg: var(--gray-200); - --table-row-odd: #f8f9fa; - --table-row-even: #ffffff; + --h1-h2-border: var(--headings-underline); + --content-font-size: 16.5px; + --content-line-height: 26px; + --sidebar-font-size: 15px; + --sidebar-caption-font-size: 14px; +} +/* ================================================================== */ +/* Colors — light mode (default) */ +/* ================================================================== */ + +:root { + /* page */ + --body-bg: #efefef; + --content-bg: #fcfcfc; + --surface-sunken: #f5f5f5; + --surface-raised: #f0f4f7; + --border: #d6d6d6; + + /* text */ + --content-text: #3b3b3b; + --text-muted: #555; + --headings: #222; + --headings-underline: #ccc; + + /* links */ + --links: #2980b9; + --links-visited: #9b59b6; + --links-api: #336699; + --links-api-underline: var(--links-api); + + /* inline code */ + --inline-code-bg: rgba(175, 184, 193, 0.2); + --inline-code-text: inherit; + + /* code blocks */ + --code-bg: #2d2e27; + + /* sidebar */ --sidebar-bg: #343131; + --sidebar-top-bg: #343131; --sidebar-caption: #55a5d9; --sidebar-item: #d9d9d9; - --sidebar-item-nested: #333; + --sidebar-item-nested: #404040; --sidebar-active-border: #55a5d9; --sidebar-search-focus-border: #7ab3d4; --sidebar-search-focus-shadow: rgba(122, 179, 212, 0.3); + /* ntuple fields */ + --ntuple-color: #333; + + /* admonitions */ + --adm-title: black; + --adm-note-bg: var(--surface-sunken); + --adm-note-border: #ccc; + --adm-seealso-accent-border: #336699; + --adm-seealso-link: #2a6099; + --adm-seealso-bg: #f3f9fe; + --adm-seealso-border: #b0cfe8; + --adm-seealso-hover: #2980b9; + --adm-seealso-title: #224466; + --adm-tip-bg: #f0faf0; + --adm-tip-border: #8aba8a; + --adm-warning-bg: #fff0f0; + --adm-warning-border: #e8a0a0; + + /* version directives */ + --versionadded-border: #4fc464; + --versionadded-color: #296433; + --versionchanged-border: #f4e34c; + --versionchanged-color: #854826; + --deprecated-border: #f44c4e; + --deprecated-color: #9f3133; + + /* tables */ + --table-header-bg: var(--surface-raised); + --table-row-odd: #f8f9fa; + --table-row-even: #ffffff; + + /* API signatures */ + --func-sig-bg: var(--surface-raised); + --func-sig-border: var(--links-api); + --func-sig-text: var(--headings); + --func-sig-name: var(--headings); + --func-sig-param: var(--text-muted); + + /* cards */ + --card-bg: #fff; + --card-border: #b0cfe8; + + /* footer */ --footer-icon: #999; --footer-icon-hover: #444; +} + +/* ================================================================== */ +/* Colors — dark mode */ +/* ================================================================== */ + +[data-theme="dark"] { + /* page */ + --body-bg: #1e1e1e; + --content-bg: #1e1e1e; + --sidebar-bg: #272727; + --sidebar-top-bg: #272727; + --surface-sunken: #2a2a2a; + --surface-raised: #2e2e2e; + --border: #3a3a3a; + + /* text */ + --content-text: #c8c8c8; + --text-muted: #b0b0ba; + --headings: #d1d1d1; + --headings-underline: #3a3a3a; + + /* inline code */ + --inline-code-bg: #2a2f36; + --inline-code-text: #e6e6e6; + + /* code blocks */ + --code-bg: #1f211d; + + /* ntuple fields */ + --ntuple-color: #6db99a; + + /* links */ + --links: #6ab0de; + --links-visited: #c5a3ff; + --links-api: #7ecfff; + --links-api-underline: #7ecfff; + + /* admonitions */ + --adm-title: #ddd; + --adm-note-bg: #2a2a2a; + --adm-note-border: #4a4a4a; + --adm-seealso-accent-border: #5599cc; + --adm-seealso-link: #90c8ee; + --adm-seealso-bg: #243a4e; + --adm-seealso-border: #2d5280; + --adm-seealso-hover: #8bbfe0; + --adm-seealso-title: #7ab0d9; + --adm-tip-bg: #1a2a1a; + --adm-tip-border: #3a6a3a; + --adm-warning-bg: #472424; + --adm-warning-border: #8a3030; + + /* version directives */ + --versionadded-color: #5ec46e; + --versionchanged-color: #c8963a; + --deprecated-color: #d06060; + + /* tables */ + --table-header-bg: #2a2d3a; + --table-row-odd: #222222; + --table-row-even: #272727; + + /* cards */ + --card-bg: #252525; + --card-border: #2a4a6a; - --card-bg: #fff; } /* ================================================================== */ @@ -98,12 +177,13 @@ .hero { text-align: center; padding: 1.5em 0 2em; + /*background: radial-gradient(ellipse, #2d2d2d 0%, var(--content-bg) 70%);*/ } .hero-title { font-size: 3.5rem; font-weight: 700; - color: var(--gray-800); + color: var(--headings); line-height: 1.1; margin-bottom: 0.3em; font-variant-ligatures: none; @@ -126,7 +206,7 @@ .hero-subtitle { font-size: 1.15rem; - color: var(--gray-500); + color: var(--text-muted); margin-bottom: 0.9em; } @@ -167,7 +247,7 @@ .home-platform-label { font-size: 0.92rem; - color: var(--gray-500); + color: var(--text-muted); font-weight: 600; align-self: center; } @@ -175,11 +255,11 @@ .home-platform-pill { display: inline-block; padding: 3px 12px; - border: 1px solid var(--gray-300); + border: 1px solid var(--border); border-radius: 999px; font-size: 0.82rem; - color: var(--gray-500); - background: var(--gray-100); + color: var(--text-muted); + background: var(--surface-sunken); } .home-feature-cards { @@ -216,7 +296,7 @@ } .home-feature-card { - border: 1px solid var(--blue-300); + border: 1px solid var(--card-border); border-radius: 8px; padding: 20px 14px; text-align: center; @@ -228,7 +308,7 @@ .home-feature-card:hover { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - border-color: var(--blue-400); + border-color: var(--links); } .home-icon-svg { @@ -238,9 +318,13 @@ margin: 0 auto 10px; } +[data-theme="dark"] .home-icon-svg { + filter: brightness(1.45) saturate(0.7); +} + .home-icon-fa { font-size: 2.2rem; - color: var(--blue-400); + color: var(--links); display: block; margin-bottom: 10px; } @@ -249,12 +333,12 @@ font-weight: 700; font-size: 0.95rem; margin-bottom: 10px; - color: var(--gray-700); + color: var(--headings); } .home-feature-apis { font-size: 0.78rem; - color: var(--gray-500); + color: var(--text-muted); font-family: var(--code-font); line-height: 1.9; } @@ -275,6 +359,25 @@ body { -moz-osx-font-smoothing: grayscale; } +.wy-nav-content-wrap { + background: var(--body-bg) !important; +} + +.wy-nav-content { + background: var(--content-bg) !important; + /* dark mode only: subtle separator between content and the strip */ + border-right: 1px solid var(--headings-underline); +} + +.rst-content { + color: var(--content-text); +} + +.rst-content p, .rst-content li, .rst-content dd { + font-size: var(--content-font-size) !important; + line-height: var(--content-line-height) !important; +} + @media (max-width: 768px) { .wy-nav-content { padding: 0 12px 1.618em 12px !important; @@ -298,12 +401,13 @@ footer div[role="navigation"][aria-label="Footer"] { /* Sidebar */ /* ================================================================== */ +.wy-nav-side, .wy-nav-top { background-color: var(--sidebar-bg) !important; } .wy-side-nav-search { - background-color: var(--sidebar-bg) !important; + background-color: var(--sidebar-top-bg) !important; position: sticky; top: 0; z-index: 10; @@ -341,6 +445,7 @@ footer div[role="navigation"][aria-label="Footer"] { .wy-menu-vertical header, .wy-menu-vertical p.caption { color: var(--sidebar-caption); + font-size: var(--sidebar-caption-font-size) !important; } .wy-menu-vertical li.toctree-l1:not(.current) > a { @@ -355,6 +460,83 @@ footer div[role="navigation"][aria-label="Footer"] { border-left: 3px solid var(--sidebar-active-border) !important; } +.wy-menu-vertical a { + font-size: var(--sidebar-font-size) !important; +} + +/* ================================================================== */ +/* Sidebar theme switch */ +/* ================================================================== */ + +/* smooth transition effect */ +html, +body, +.wy-body-for-nav, +.wy-nav-content-wrap, +.wy-nav-content { + transition: background-color var(--theme-transition), color var(--theme-transition); +} + +/* suppress all transitions during initial page load */ +.no-transition * { + transition: none !important; +} + +.wy-side-nav-search .wy-form { + display: flex; + align-items: center; + gap: 8px; +} + +.wy-side-nav-search .wy-form input[type="text"] { + flex: 1; + width: auto !important; + margin-bottom: 0; +} + +.theme-switch { + display: flex; + align-items: center; + cursor: pointer; + user-select: none; + margin: 0; + flex-shrink: 0; +} + +.theme-switch input { + display: none; +} + +/* the circle button */ +.theme-switch-btn { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); + color: #fff; + font-size: 1rem; + line-height: 1; + transition: background 0.2s; +} + +.theme-switch:hover .theme-switch-btn { + background: rgba(255, 255, 255, 0.22); +} + +/* light mode: show moon, hide sun */ +.theme-switch-sun { display: none; } +.theme-switch-moon { display: block; } + +/* dark mode: show sun, hide moon */ +.theme-switch input:checked ~ .theme-switch-btn .theme-switch-sun, +[data-theme="dark"] .theme-switch-sun { display: block; } + +.theme-switch input:checked ~ .theme-switch-btn .theme-switch-moon, +[data-theme="dark"] .theme-switch-moon { display: none; } + /* ================================================================== */ /* Footer */ /* ================================================================== */ @@ -403,6 +585,7 @@ footer div[role="navigation"][aria-label="Footer"] { h1, h2, h3, h4 { margin-top: 20px !important; margin-bottom: 10px !important; + color: var(--headings) !important; } h1, h2 { @@ -410,6 +593,13 @@ h1, h2 { border-bottom: 1px solid var(--h1-h2-border); } +h1, h1 a, +h2, h2 a, +h3, h3 a, +h4, h4 a { + color: var(--headings) !important; +} + h1 { font-size: 2.2rem; } @@ -439,6 +629,10 @@ h4 { background-color: var(--table-header-bg); } +.wy-table-responsive table thead th { + color: var(--content-text); +} + .wy-table-responsive table tbody tr.row-odd td { background-color: var(--table-row-odd) !important; } @@ -447,6 +641,12 @@ h4 { background-color: var(--table-row-even) !important; } +.wy-table-responsive table, +.wy-table-responsive table th, +.wy-table-responsive table td { + border-color: var(--border) !important; +} + .document th { padding: 4px 8px !important; font-weight: 600; @@ -518,6 +718,18 @@ h4 { margin-left: 15px; } +/* ================================================================== */ +/* Links */ +/* ================================================================== */ + +.rst-content a { + color: var(--links); +} + +.rst-content a:visited { + color: var(--links-visited); +} + /* ================================================================== */ /* API signatures */ /* ================================================================== */ @@ -558,8 +770,8 @@ h4 { /* inline literals: ``like this`` */ .rst-content code, .rst-content tt, code { - color: inherit !important; - background: rgba(175, 184, 193, 0.2) !important; + color: var(--inline-code-text) !important; + background: var(--inline-code-bg) !important; border: none !important; border-radius: 4px !important; padding: .1em .35em !important; @@ -569,6 +781,10 @@ h4 { } /* cross-references to API symbols like :func:`psutil.users` */ +.rst-content a.reference:has(code.xref) { + color: var(--links-api) !important; +} + .rst-content a.reference code, .rst-content a.reference tt { color: inherit !important; @@ -577,11 +793,14 @@ h4 { /*font-weight: bold !important;*/ font-size:90% !important; text-decoration: underline; + text-decoration-color: var(--links-api-underline); + text-decoration-thickness: 1px; text-underline-offset: 4px; } /* named tuple field */ code.ntuple-field { + color: var(--ntuple-color) !important; font-weight: bold !important; } @@ -589,47 +808,68 @@ code.ntuple-field { /* Code blocks */ /* ================================================================== */ -/* Python number, string, and namedtuple name highlights in pycon output */ -.highlight-pycon .pycon-number { color: #ae81ff; } -.highlight-pycon .pycon-string { color: #e6db74; } -.highlight-pycon .pycon-name { color: #89b8d4; } - /* reduce bottom margin of code blocks inside list items */ .rst-content li > div[class^='highlight'] { margin-bottom: 6px !important; } -.highlight, -.highlight pre { - font-size: 13px !important; - font-family: var(--code-font) !important; -} - div[class^="highlight-"] { border-radius: var(--border-radius); background: transparent; } .highlight { - background-color: var(--code-bg); border-radius: var(--border-radius); overflow: hidden; } +.rst-content div[class^=highlight], +.rst-content pre.literal-block { + border-color: var(--border) !important; +} + +/* ------------------------- font ------------------------- */ + +.highlight, +.highlight pre { + font-size: 13px !important; + font-family: var(--code-font) !important; +} + .highlight pre { line-height: var(--code-line-height) !important; } +/* ----------------- syntax highlight colors --------------- */ + +/* Python number, string, and namedtuple name highlights in pycon output */ +.highlight-pycon .pycon-number { color: #8f6fcc; } +.highlight-pycon .pycon-string { color: #b8a84e; } +.highlight-pycon .pycon-field { color: #6db99a } + +/* tone down Pygments Generic.Prompt (>>>) and Generic.Output */ +.highlight .gp { color: #666666; font-weight: normal; } +.highlight .go { color: #7a9aaa; } + +.highlight { + background-color: var(--code-bg) !important; +} + /* ================================================================== */ /* Links */ /* ================================================================== */ -/* external links: small icon instead of dotted underline */ +a.external[href^="#"] { + text-decoration: none; + color: inherit; +} + a.reference.external:not([href*="docs.python.org"]):not(:has(img)) { text-decoration: none; } -a.reference.external:not([href*="docs.python.org"]):not(:has(img))::after { +/* external links: show a small icon */ +a.reference.external:not(:has(img))::after { content: '\f08e'; font-family: FontAwesome; font-size: 0.4em; @@ -639,9 +879,11 @@ a.reference.external:not([href*="docs.python.org"]):not(:has(img))::after { opacity: 0.6; } -a.external[href^="#"] { - text-decoration: none; - color: inherit; +/* adjust cpython API links (intersphinx) icon position */ +a.reference.external[href*="docs.python.org"]:not(:has(img))::after { + margin-left: -3px; + margin-right: 2px; + padding-left: 0; } /* ================================================================== */ @@ -687,7 +929,7 @@ div.admonition a { } div.seealso a.reference { - color: var(--adm-seealso-accent-border) !important; + color: var(--adm-seealso-link) !important; text-decoration: underline; text-underline-offset: 2px; } @@ -770,7 +1012,7 @@ div.deprecated .versionmodified { border-left: none !important; color: var(--func-sig-text) !important; padding: 6px 10px !important; - font-size: 1.1em; + font-size: var(--content-font-size) !important; } /* The function name itself */ @@ -789,7 +1031,7 @@ div.deprecated .versionmodified { font-weight: normal; } -/* Optional: Keep the [source] link subtle */ +/* Keep the [source] link subtle */ .viewcode-link { color: var(--func-sig-border) !important; font-size: 0.8em; diff --git a/docs/_static/images/icon-memory.svg b/docs/_static/images/icon-memory.svg index 9bfd6cefb6..e499ac5aa1 100644 --- a/docs/_static/images/icon-memory.svg +++ b/docs/_static/images/icon-memory.svg @@ -1,19 +1,19 @@ - + - - - - - - - - - - - + + + + + + + + + + + diff --git a/docs/_static/images/logo-apivoid.svg b/docs/_static/images/logo-apivoid.svg index 5187310d60..49ebb25cd8 100644 --- a/docs/_static/images/logo-apivoid.svg +++ b/docs/_static/images/logo-apivoid.svg @@ -1,5 +1,6 @@ + @@ -8,5 +9,5 @@ - + diff --git a/docs/_static/images/logo-sansec.svg b/docs/_static/images/logo-sansec.svg index 5d74c6be87..9fa30a4e0e 100644 --- a/docs/_static/images/logo-sansec.svg +++ b/docs/_static/images/logo-sansec.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/_static/images/logo-tidelift.svg b/docs/_static/images/logo-tidelift.svg index 5394cbd6fc..448dd7a081 100644 --- a/docs/_static/images/logo-tidelift.svg +++ b/docs/_static/images/logo-tidelift.svg @@ -5,6 +5,7 @@ >> lines as Python, but leaves output as plain -// Generic.Output. - -document.addEventListener("DOMContentLoaded", function () { - document.querySelectorAll(".highlight-pycon .go").forEach(function (span) { - span.innerHTML = span.innerHTML.replace( - /('[^']*'|"[^"]*"|\b[a-z]\w*(?=\()|\b\d+\.?\d*\b)/g, - function (match) { - var cls; - if (match[0] === "'" || match[0] === '"') - cls = "pycon-string"; - else if (/^[a-z]/.test(match)) - cls = "pycon-name"; - else - cls = "pycon-number"; - return '' + match + ''; - } - ); - }); -}); diff --git a/docs/_static/js/highlight-repl.js b/docs/_static/js/highlight-repl.js new file mode 100644 index 0000000000..345ba9d21d --- /dev/null +++ b/docs/_static/js/highlight-repl.js @@ -0,0 +1,20 @@ +// Syntax-highlight REPL inside pycon output spans (class="go"). +// Pygments tokenizes >>> lines as Python, but leaves output as plain +// Generic.Output. + +document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".highlight-pycon .go").forEach(function (span) { + var html = span.innerHTML; + // Highlight quoted strings (must run first, before we inject spans). + html = html.replace(/('[^']*'|"[^"]*")/g, + '$1'); + // Highlight namedtuple field names (word before '='). + // The (?!") lookahead avoids matching class= in the injected span tags. + html = html.replace(/\b([a-z_]\w*)=(?!")/g, + '$1='); + // Highlight numbers. + html = html.replace(/\b\d+\.?\d*\b/g, + '$&'); + span.innerHTML = html; + }); +}); diff --git a/docs/_static/js/theme-toggle.js b/docs/_static/js/theme-toggle.js new file mode 100644 index 0000000000..f685b6c18c --- /dev/null +++ b/docs/_static/js/theme-toggle.js @@ -0,0 +1,30 @@ +(function () { + var KEY = 'psutil-theme'; + var html = document.documentElement; + + function setTheme(dark) { + html.setAttribute('data-theme', dark ? 'dark' : 'light'); + localStorage.setItem(KEY, dark ? 'dark' : 'light'); + + // clear any inline background styles so CSS variables take over + ['.wy-nav-content-wrap', '.wy-nav-content', '.wy-body-for-nav'] + .forEach(function (sel) { + var el = document.querySelector(sel); + if (el) el.style.background = ''; + }); + + var cb = document.getElementById('theme-toggle'); + if (cb) cb.checked = dark; + } + + // restore saved preference + setTheme(localStorage.getItem(KEY) === 'dark'); + + // wire up the checkbox (DOM is ready because script is deferred) + var cb = document.getElementById('theme-toggle'); + if (cb) { + cb.addEventListener('change', function () { + setTheme(cb.checked); + }); + } +})(); diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 1f595cbd46..d6b85b55d4 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -1,6 +1,26 @@ {% extends "!layout.html" %} {% block extrahead %} {{ super() }} + + + + @@ -17,13 +21,22 @@
    -psutil is a cross-platform library for retrieving information about running +Psutil is a cross-platform library for retrieving information about running processes and system utilization (CPU, memory, disks, network, sensors) in Python. It is useful mainly for system monitoring, profiling, limiting process resources, and managing running processes. +Psutil implements many functionalities offered by UNIX command line tool such +as *ps, top, free, iotop, netstat, ifconfig, lsof* and others +(see :doc:`shell equivalents `). +It is used by :doc:`many notable projects ` including TensorFlow, +PyTorch, Home Assistant, Ansible, and Celery. ---- +.. ============================================================================ +.. Platform pills +.. ============================================================================ + .. raw:: html
    @@ -38,6 +51,12 @@ limiting process resources, and managing running processes. AIX
    +.. ============================================================================ +.. Feature cards +.. ============================================================================ + +.. raw:: html + -psutil implements many functionalities offered by UNIX command line tool such -as *ps, top, free, iotop, netstat, ifconfig, lsof* and others -(see :doc:`shell equivalents `). -It is used by :doc:`many notable projects ` including TensorFlow, -PyTorch, Home Assistant, Ansible, and Celery. +.. ============================================================================ +.. Description cards +.. ============================================================================ + +.. raw:: html + +
    +
    +
    + +
    Process management
    +
    +

    Query and control running processes: CPU, memory, files, connections, threads, and more.

    +
    +
    +
    + +
    System monitoring
    +
    +

    Get real-time metrics for CPU, memory, disks, network, and hardware sensors.

    +
    +
    +
    + +
    Cross-platform
    +
    +

    One API for all platforms. Psutil primary goal is to abstract OS differences, so your monitoring code runs unchanged everywhere.

    +
    +
    + +.. ============================================================================ +.. Sponsors +.. ============================================================================ Sponsors -------- @@ -78,6 +125,10 @@ Sponsors .. raw:: html :file: _sponsors.html +.. ============================================================================ +.. TOC: hidden via CSS, needed by Sphinx for sidebar nav +.. ============================================================================ + .. toctree:: :maxdepth: 2 :caption: Documentation diff --git a/docs/performance.rst b/docs/performance.rst index ae06455f70..29b839da03 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -78,12 +78,9 @@ Fast: :func:`process_iter(attrs=...) ` is effectively equivalent to using :meth:`Process.oneshot` on each process. - -Using :func:`process_iter` also saves you from race conditions (e.g. -if a process disappears while iterating), since attributes are retrieved in a -single pass and exceptions like :exc:`NoSuchProcess` and :exc:`AccessDenied` -are handled internally. - +Using :func:`process_iter` also saves you from **race conditions** (e.g. if a +process disappears while iterating), since :exc:`NoSuchProcess` and +:exc:`AccessDenied` exceptions are handled internally. A typical use case is to fetch all process attrs except the slow ones (see :ref:`perf-api-speed` table below): From 67dbfebeb9b2de966625d3eda5a67b878903421c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sat, 11 Apr 2026 22:19:49 +0200 Subject: [PATCH 1699/1714] Some home CSS style --- docs/_static/css/custom.css | 20 +++++++++++++------- docs/api.rst | 9 ++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index d3f8313925..ce78987c68 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -112,7 +112,7 @@ --card-border: #b0cfe8; /* footer */ - --footer-icon: #999; + --footer-icon: #666; --footer-icon-hover: #444; } @@ -250,9 +250,6 @@ height: auto; } - -/* - /* Sponsors --------------------------------------------------------- */ .home-page #sponsors { @@ -264,6 +261,15 @@ text-align: center; } +@media (max-width: 768px) { + .home-page #sponsors { + margin-left: 0; + margin-right: 0; + padding-left: 1rem; + padding-right: 1rem; + } +} + [data-theme="dark"] .home-page #sponsors { background: #1e1e1e; } @@ -443,7 +449,7 @@ flex-shrink: 0; padding-right: 0.35rem; line-height: 1; - vertical-align: middle; + padding-top: 2px; } .home-desc-card:nth-child(1) .home-desc-card-icon { color: #2a6a90; } @@ -580,7 +586,6 @@ color: var(--links); } - /* TOC (hidden) ----------------------------------------------------- */ .home-page .toctree-wrapper { @@ -719,7 +724,8 @@ footer div[role="navigation"][aria-label="Footer"] { .wy-menu-vertical header, .wy-menu-vertical p.caption { color: var(--sidebar-caption); - font-size: var(--sidebar-caption-font-size) !important; + font-size: 13px !important; + letter-spacing: 0.1em; } .wy-menu-vertical li.toctree-l1 > a { diff --git a/docs/api.rst b/docs/api.rst index f992eafceb..0dbc5f7fde 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,9 +1,6 @@ .. currentmodule:: psutil .. include:: _links.rst -API reference -============= - .. note:: psutil 8.0 introduces breaking API changes. See the :ref:`migration guide ` if upgrading from 7.x. @@ -12,6 +9,12 @@ API reference do not rely on positional unpacking of named tuples. Always use attribute access (e.g. ``t.rss``). +API reference +============= + +Complete reference for all psutil classes and functions. For a high-level +overview with short examples see :doc:`api-overview`. + .. contents:: :local: :depth: 5 From 6d8e37ab5ff830b385d3e3961a2949772a287b8f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 12 Apr 2026 03:26:42 +0200 Subject: [PATCH 1700/1714] Doc: add top bar (#2819) The main reason to add a top bar is because of the need to show a GitHub button somewhere. It's too important not to. Right now I showed that in the home and in the footer, but it's not enough. So let's use a top bar, which is what (e.g.) pydata theme or rtdocs does. Move the light/dark theme button in the top bar, and also show the current psutil version (points to PyPI). On the left, we always have a clickable "[LOGO] psutil" thingie, which is the way to "go home". For this reason I removed the "Home" link from the sidebar: it's redundant now, and the sidebar is already pretty long, so let's save space image --- MANIFEST.in | 5 +- docs/_ext/add_home_link.py | 25 --- docs/_static/css/custom.css | 219 +++++++++++++++------ docs/_static/images/footer-icon-github.svg | 3 - docs/_static/images/footer-icon-pypi.svg | 3 - docs/_templates/footer.html | 10 +- docs/_templates/layout.html | 25 +++ docs/_templates/searchbox.html | 16 -- docs/_templates/topbar.html | 17 ++ docs/api.rst | 2 +- docs/changelog.rst | 9 +- docs/conf.py | 3 +- 12 files changed, 213 insertions(+), 124 deletions(-) delete mode 100644 docs/_ext/add_home_link.py delete mode 100644 docs/_static/images/footer-icon-github.svg delete mode 100644 docs/_static/images/footer-icon-pypi.svg delete mode 100644 docs/_templates/searchbox.html create mode 100644 docs/_templates/topbar.html diff --git a/MANIFEST.in b/MANIFEST.in index 97d5601d7a..80f387cb15 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,7 +14,6 @@ include docs/.readthedocs.yaml include docs/DEVNOTES.md include docs/Makefile include docs/README -include docs/_ext/add_home_link.py include docs/_ext/availability.py include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py @@ -23,8 +22,6 @@ include docs/_links.rst include docs/_sponsors.html include docs/_static/css/custom.css include docs/_static/images/favicon.svg -include docs/_static/images/footer-icon-github.svg -include docs/_static/images/footer-icon-pypi.svg include docs/_static/images/icon-cpu.svg include docs/_static/images/icon-memory.svg include docs/_static/images/icon-network.svg @@ -40,7 +37,7 @@ include docs/_static/js/sidebar-close.js include docs/_static/js/theme-toggle.js include docs/_templates/footer.html include docs/_templates/layout.html -include docs/_templates/searchbox.html +include docs/_templates/topbar.html include docs/adoption.rst include docs/alternatives.rst include docs/api-overview.rst diff --git a/docs/_ext/add_home_link.py b/docs/_ext/add_home_link.py deleted file mode 100644 index 759b4505f0..0000000000 --- a/docs/_ext/add_home_link.py +++ /dev/null @@ -1,25 +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. - -"""Sphinx extension that prepends a 'Home' link to the TOC sidebar. -This script gets called on `make html`. -""" - -from sphinx.application import Sphinx - - -def add_home_link(app: Sphinx, pagename, templatename, context, doctree): - if "toctree" in context: - toctree_func = context["toctree"] - toc_html = toctree_func( - maxdepth=2, collapse=False, includehidden=False - ) - # prepend Home link manually - home_link = '
  • Home
  • ' - context["toctree"] = lambda **_kw: home_link + toc_html - - -def setup(app: Sphinx): - app.connect("html-page-context", add_home_link) - return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index ce78987c68..d1cd1f4f18 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -58,6 +58,11 @@ /* code blocks */ --code-bg: #2d2e27; + /* top bar */ + --topbar-bg: #2a2828; + --topbar-fg: #d1d1d1; + --topbar-fg-active: #fff; + /* sidebar */ --sidebar-bg: #343131; --sidebar-top-bg: #343131; @@ -110,10 +115,6 @@ /* cards */ --card-bg: #fff; --card-border: #b0cfe8; - - /* footer */ - --footer-icon: #666; - --footer-icon-hover: #444; } /* ================================================================== */ @@ -124,6 +125,7 @@ /* page */ --body-bg: #1e1e1e; --content-bg: #1e1e1e; + --topbar-bg: #272727; --sidebar-bg: #272727; --sidebar-top-bg: #272727; --surface-sunken: #2a2a2a; @@ -132,7 +134,7 @@ /* text */ --content-text: #c8c8c8; - --text-muted: #b0b0ba; + --text-muted: #d1d1d1; --headings: #e0e0e0; --headings-underline: #3a3a3a; @@ -184,6 +186,117 @@ } +/* ================================================================== */ +/* Top bar */ +/* ================================================================== */ + +.top-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9999; + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%), var(--topbar-bg); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + height: 44px; +} + +.top-bar-inner { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; + padding: 0 2.5rem 0 0; +} + +/* At widths where content hits max-width (300px sidebar + 1000px = 1300px), + grow right padding to keep actions aligned with the content right edge. */ +@media (min-width: 1300px) { + .top-bar-inner { + padding-right: calc(100vw - 1300px + 2.5rem); + } +} + +.top-bar-logo { + display: flex; + align-items: center; + justify-content: flex-start; + width: 300px; + padding-left: 1.25rem; + gap: 7px; + text-decoration: none !important; + font-weight: 700; + font-size: 1.05rem; + color: var(--topbar-fg-active) !important; +} + +.top-bar-logo span { + line-height: 1; + padding-top: 7px; +} + +.top-bar-logo img { + height: 22px; +} + +.top-bar-actions { + display: flex; + align-items: center; + gap: 6px; +} + +.top-bar-link, +.top-bar-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 6px; + border: none; + background: transparent; + cursor: pointer; + color: var(--topbar-fg); + font-size: 1.5rem; + text-decoration: none !important; + transition: background 0.15s, color 0.15s; +} + + +.top-bar-link:hover, +.top-bar-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--topbar-fg-active); +} + +.top-bar-version { + display: inline-flex; + align-items: center; + height: 22px; + font-size: 0.78rem; + font-weight: 600; + color: var(--topbar-fg); + padding: 1px 8px 0; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 999px; + margin-left: 4px; + text-decoration: none !important; +} + +/* prevent browser from overriding link/visited colours in the top bar */ +.top-bar-link:link, +.top-bar-link:visited, +.top-bar-version:link, +.top-bar-version:visited { + color: var(--topbar-fg); +} + +.top-bar-version:hover { + color: var(--topbar-fg-active); + border-color: rgba(255, 255, 255, 0.45); +} + /* ================================================================== */ /* Home page */ /* ================================================================== */ @@ -605,6 +718,7 @@ html { background: var(--body-bg); + scroll-padding-top: 44px; } body { @@ -638,6 +752,10 @@ body { } } +.document { + padding-top: 20px !important; +} + /* ================================================================== */ /* Centered layout */ /* ================================================================== */ @@ -679,12 +797,12 @@ footer div[role="navigation"][aria-label="Footer"] { background-color: var(--sidebar-bg) !important; } + .wy-side-nav-search { background-color: var(--sidebar-top-bg) !important; position: sticky; - top: 0; + top: 44px; z-index: 10; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); } /* hide version switch */ @@ -693,24 +811,9 @@ footer div[role="navigation"][aria-label="Footer"] { } .wy-side-nav-search a.icon-home { - display: flex; - align-items: center; - justify-content: center; - gap: 2px; -} - -.wy-side-nav-search a.icon-home::before { display: none; } -.wy-side-nav-search a.icon-home img.logo { - display: inline-block; - height: 26px; - width: auto; - margin: 0; - order: -1; -} - .wy-side-nav-search input[type="text"] { font-family: var(--font-search); transition: border-color 0.2s ease, box-shadow 0.2s ease; @@ -722,6 +825,10 @@ footer div[role="navigation"][aria-label="Footer"] { outline: none; } +.wy-menu-vertical { + margin-top: 45px; +} + .wy-menu-vertical header, .wy-menu-vertical p.caption { color: var(--sidebar-caption); font-size: 13px !important; @@ -827,12 +934,26 @@ body, /* Sidebar effects */ /* ================================================================== */ -/* Smooth sidebar open/close on mobile */ -@media screen and (max-width: 768px) { +@media (max-width: 768px) { + .top-bar { + height: 40px; + } + .top-bar-inner { + justify-content: space-between; + padding: 0 0.75rem; + } + .top-bar-logo { + display: flex; + width: auto; + padding-left: 0; + } + /* Smooth sidebar open/close */ .wy-nav-side { transition: left 0.2s ease, width 0.2s ease; } .wy-nav-content-wrap { + margin-top: 40px !important; + padding-top: 0 !important; transition: left 0.2s ease; } } @@ -841,43 +962,11 @@ body, /* Footer */ /* ================================================================== */ -.footer-row { - display: table; - width: 100%; -} - -.footer-row p, -.footer-icons { - display: table-cell; - vertical-align: middle; +.footer-row p { margin: 0 !important; padding: 0; } -.footer-icons { - text-align: right; - white-space: nowrap; - width: 1px; -} - -.footer-icon { - display: inline-flex; - align-items: center; - margin-left: 14px; -} - -.footer-icon svg { - display: block; - width: 20px; - height: 20px; - fill: var(--footer-icon); - transition: fill 0.2s; -} - -.footer-icon:hover svg { - fill: var(--footer-icon-hover); -} - /* ================================================================== */ /* Headings */ /* ================================================================== */ @@ -1338,3 +1427,19 @@ div.deprecated .versionmodified { font-size: 0.8em; font-style: italic; } + +/* ================================================================== */ +/* Genindex (Index page) */ +/* ================================================================== */ + +/* On small screens, collapse the two-column index table into a single + column so items are not split across two narrow vertical sections. */ +@media screen and (max-width: 768px) { + table.indextable tr { + display: block; + } + table.indextable td { + display: block; + width: 100% !important; + } +} diff --git a/docs/_static/images/footer-icon-github.svg b/docs/_static/images/footer-icon-github.svg deleted file mode 100644 index c87cd5c4e0..0000000000 --- a/docs/_static/images/footer-icon-github.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_static/images/footer-icon-pypi.svg b/docs/_static/images/footer-icon-pypi.svg deleted file mode 100644 index 1fc5c177bb..0000000000 --- a/docs/_static/images/footer-icon-pypi.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html index ca56970340..f502e838c8 100644 --- a/docs/_templates/footer.html +++ b/docs/_templates/footer.html @@ -12,21 +12,13 @@
    - - .. ============================================================================ .. Description cards .. ============================================================================ diff --git a/docs/install.rst b/docs/install.rst index 16de2487ae..aac2e0209f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -86,8 +86,8 @@ Windows pip install --no-binary :all: psutil - If you want to clone psutil's Git repository and build / develop locally, - first install `Git for Windows`_ and launch a Git Bash shell. This provides - a Unix-like environment where ``make`` works. + first install `Git for Windows`_ and launch a Git Bash shell. This provides a + Unix-like environment where ``make`` works. - Once inside Git Bash, you can run the usual ``make`` commands: .. code-block:: none diff --git a/docs/migration.rst b/docs/migration.rst index 8812276179..abb4e609fa 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -4,12 +4,12 @@ Migration guide =============== -This page summarises the breaking changes introduced in each major -release and shows the code changes required to upgrade. +This page summarises the breaking changes introduced in each major release and +shows the code changes required to upgrade. .. note:: - Minor and patch releases (e.g. 6.1.x, 7.1.x) never contain - breaking changes. Only major releases are listed here. + Minor and patch releases (e.g. 6.1.x, 7.1.x) never contain breaking changes. + Only major releases are listed here. .. contents:: :local: @@ -36,15 +36,15 @@ Key breaking changes in 8.0: .. important:: - Do not rely on positional unpacking of named tuples. - Always use attribute access (e.g. ``t.rss``). + Do not rely on positional unpacking of named tuples. Always use attribute + access (e.g. ``t.rss``). process_iter(): p.info is deprecated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:func:`process_iter` now caches pre-fetched values internally, so they -can be accessed via normal method calls instead of the :attr:`Process.info` -dict. ``p.info`` still works, but raises :exc:`DeprecationWarning`. +:func:`process_iter` now caches pre-fetched values internally, so they can be +accessed via normal method calls instead of the :attr:`Process.info` dict. +``p.info`` still works, but raises :exc:`DeprecationWarning`. .. code-block:: python @@ -58,9 +58,9 @@ dict. ``p.info`` still works, but raises :exc:`DeprecationWarning`. for p in psutil.process_iter(attrs=["name", "status"]): print(p.name(), p.status()) # return cached values, never raise -When ``attrs`` are specified, method calls return cached values -(no extra syscall), and :exc:`AccessDenied` / :exc:`ZombieProcess` -are handled transparently (returning ``ad_value``). +When ``attrs`` are specified, method calls return cached values (no extra +syscall), and :exc:`AccessDenied` / :exc:`ZombieProcess` are handled +transparently (returning ``ad_value``). If you relied on :attr:`Process.info` because you needed a dict structure, use :meth:`Process.as_dict` instead. @@ -78,11 +78,10 @@ If you relied on :attr:`Process.info` because you needed a dict structure, use for p in psutil.process_iter(attrs=attrs): print(p.as_dict(attrs)) # return cached values, never raise - .. note:: - If ``"name"`` was pre-fetched via ``attrs``, calling ``p.name()`` no - longer raises :exc:`AccessDenied`. It returns ``ad_value`` instead. - If you need the exception, do not include the method in ``attrs``. + If ``"name"`` was pre-fetched via ``attrs``, calling ``p.name()`` no longer + raises :exc:`AccessDenied`. It returns ``ad_value`` instead. If you need the + exception, do not include the method in ``attrs``. Named tuple field order changed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -103,9 +102,9 @@ Named tuple field order changed t = psutil.cpu_times() user, system, idle = t.user, t.system, t.idle -- :meth:`Process.memory_info`: the returned named tuple changed size - and field order. Always use attribute access (e.g. - ``p.memory_info().rss``) instead of positional unpacking. +- :meth:`Process.memory_info`: the returned named tuple changed size and field + order. Always use attribute access (e.g. ``p.memory_info().rss``) instead of + positional unpacking. - Linux: :field:`lib` and :field:`dirty` fields removed (aliases emitting :exc:`DeprecationWarning` are kept). @@ -146,10 +145,10 @@ Status and connection fields are now enums field now returns a :class:`ConnectionStatus` member instead of a plain ``str``. -Because both are :class:`enum.StrEnum` subclasses they compare equal to -their string values, so existing comparisons like -``p.status() == psutil.STATUS_RUNNING`` continue to work unchanged. -Code inspecting ``repr()`` or ``type()`` may need updating. +Because both are :class:`enum.StrEnum` subclasses they compare equal to their +string values, so existing comparisons like +``p.status() == psutil.STATUS_RUNNING`` continue to work unchanged. Code +inspecting ``repr()`` or ``type()`` may need updating. memory_full_info() is deprecated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -171,9 +170,9 @@ New Process.attrs class attribute ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :attr:`Process.attrs` is a new :class:`frozenset` exposing the valid attribute -names accepted by :meth:`Process.as_dict` and :func:`process_iter`. It -replaces the previous pattern of creating a throwaway process just to -discover available names: +names accepted by :meth:`Process.as_dict` and :func:`process_iter`. It replaces +the previous pattern of creating a throwaway process just to discover available +names: .. code-block:: python @@ -184,8 +183,8 @@ discover available names: attrs = psutil.Process.attrs It also makes it easy to pass all or a subset of attributes. -``process_iter(attrs=[])`` (empty list meaning "all") is now deprecated; -use ``Process.attrs`` instead: +``process_iter(attrs=[])`` (empty list meaning "all") is now deprecated; use +``Process.attrs`` instead: .. code-block:: python @@ -203,10 +202,10 @@ Python 3.6 is no longer supported. Minimum version is Python 3.7. Git tags renamed ^^^^^^^^^^^^^^^^^ -Git tags were renamed from ``release-X.Y.Z`` to ``vX.Y.Z`` -(e.g. ``release-7.2.2`` → ``v7.2.2``). Old tags are kept for -backward compatibility. If you reference psutil tags in scripts or -URLs, update them to the new format. See :gh:`2788`. +Git tags were renamed from ``release-X.Y.Z`` to ``vX.Y.Z`` (e.g. +``release-7.2.2`` → ``v7.2.2``). Old tags are kept for backward compatibility. +If you reference psutil tags in scripts or URLs, update them to the new format. +See :gh:`2788`. ------------------------------------------------------------------------------- @@ -219,15 +218,13 @@ Process.memory_info_ex() removed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The long-deprecated :meth:`Process.memory_info_ex` was removed (it was -deprecated since 4.0.0 in 2016). Use :meth:`Process.memory_full_info` -instead. +deprecated since 4.0.0 in 2016). Use :meth:`Process.memory_full_info` instead. .. note:: - In 8.0, a new :meth:`Process.memory_info_ex` method was introduced - with different semantics: it extends :meth:`Process.memory_info` - with platform-specific metrics. It is unrelated to the old method - documented here. + In 8.0, a new :meth:`Process.memory_info_ex` method was introduced with + different semantics: it extends :meth:`Process.memory_info` with + platform-specific metrics. It is unrelated to the old method documented here. .. code-block:: python @@ -240,8 +237,8 @@ instead. Python 2.7 dropped ^^^^^^^^^^^^^^^^^^^^ -Python 2.7 is no longer supported. The last release to support Python -2.7 is psutil 6.1.x: +Python 2.7 is no longer supported. The last release to support Python 2.7 is +psutil 6.1.x: .. code-block:: bash @@ -257,10 +254,9 @@ Migrating to 6.0 Process.connections() renamed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:meth:`Process.connections` was renamed to -:meth:`Process.net_connections` for consistency with the system-level -:func:`net_connections`. The old name triggers a :exc:`DeprecationWarning` -and will be removed in a future release: +:meth:`Process.connections` was renamed to :meth:`Process.net_connections` for +consistency with the system-level :func:`net_connections`. The old name +triggers a :exc:`DeprecationWarning` and will be removed in a future release: .. code-block:: python @@ -292,9 +288,9 @@ positionally will break: process_iter() no longer checks for PID reuse ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:func:`process_iter` no longer preemptively checks whether yielded -PIDs have been reused (this made it ~20× faster). If you need to verify -that a process object is still alive and refers to the same process, use +:func:`process_iter` no longer preemptively checks whether yielded PIDs have +been reused (this made it ~20× faster). If you need to verify that a process +object is still alive and refers to the same process, use :meth:`Process.is_running` explicitly: .. code-block:: python @@ -310,9 +306,9 @@ that a process object is still alive and refers to the same process, use Migrating to 5.0 ----------------- -5.0.0 was the largest renaming in psutil history. All ``get_*`` and -``set_*`` :class:`Process` methods lost their prefix, and several -module-level names were changed. +5.0.0 was the largest renaming in psutil history. All ``get_*`` and ``set_*`` +:class:`Process` methods lost their prefix, and several module-level names were +changed. Old :class:`Process` method names still worked but raised :exc:`DeprecationWarning`. They were fully removed in 6.0. diff --git a/docs/performance.rst b/docs/performance.rst index 57daa95e6c..f869fc4dcc 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -77,12 +77,11 @@ Fast: print(p.pid, p.name(), p.status()) # return cached values, never raise :func:`process_iter(attrs=...) ` is effectively equivalent -to using :meth:`Process.oneshot` on each process. -Using :func:`process_iter` also saves you from **race conditions** (e.g. if a -process disappears while iterating), since :exc:`NoSuchProcess` and -:exc:`AccessDenied` exceptions are handled internally. -A typical use case is to fetch all process attrs except the slow ones (see -:ref:`perf-api-speed` table below): +to using :meth:`Process.oneshot` on each process. Using :func:`process_iter` +also saves you from **race conditions** (e.g. if a process disappears while +iterating), since :exc:`NoSuchProcess` and :exc:`AccessDenied` exceptions are +handled internally. A typical use case is to fetch all process attrs except the +slow ones (see :ref:`perf-api-speed` table below): .. code-block:: python diff --git a/docs/platform.rst b/docs/platform.rst index a500c86bfc..fbd4b549b1 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -7,9 +7,9 @@ Python **Current Python:** 3.7 and PyPy3. **Python 2.7**: latest psutil version supporting it is -`psutil 6.1.1 `_ (Dec 2024). -The 6.1.X series may still receive critical bug-fixes but no new features. -To install psutil on Python 2.7 run: +`psutil 6.1.1 `_ (Dec 2024). The 6.1.X +series may still receive critical bug-fixes but no new features. To install +psutil on Python 2.7 run: .. code-block:: none @@ -51,8 +51,8 @@ Notes: - Linux wheels are built for both glibc (manylinux) and musl. - macOS wheels are universal2 (include both x86_64 and arm64 slices). - Windows wheels are labeled AMD64 or ARM64 according to architecture. -- Other architectures (i686, ppc64le, s390x, riscv64, ...) are supported - but not CI-tested. They can be compiled from the source tarball +- Other architectures (i686, ppc64le, s390x, riscv64, ...) are supported but + not CI-tested. They can be compiled from the source tarball (``pip install psutil --no-binary psutil``). Support history diff --git a/docs/recipes.rst b/docs/recipes.rst index 99021f9487..07ce6ba76f 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -271,8 +271,8 @@ Kill a process tree (including grandchildren): ------------------------------------------------------------------------------- -Terminate a process gracefully, falling back to ``SIGKILL`` if it does not -exit within the timeout: +Terminate a process gracefully, falling back to ``SIGKILL`` if it does not exit +within the timeout: .. code-block:: python @@ -309,8 +309,8 @@ Temporarily pause and resume a process using a context manager: ------------------------------------------------------------------------------- -CPU throttle: limit a process's CPU usage to a target percentage by -alternating :meth:`Process.suspend` and :meth:`Process.resume`: +CPU throttle: limit a process's CPU usage to a target percentage by alternating +:meth:`Process.suspend` and :meth:`Process.resume`: .. code-block:: python @@ -364,7 +364,6 @@ Restart a process automatically if it dies: if __name__ == "__main__": watchdog(["python3", "script.py"]) - System ------ diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst index 33c894d74c..7a829fa61a 100644 --- a/docs/stdlib-equivalents.rst +++ b/docs/stdlib-equivalents.rst @@ -5,9 +5,9 @@ Stdlib equivalents ================== This page maps psutil's Python API to the closest equivalent in the Python -standard library. This is useful for understanding what psutil replaces and -how the two APIs differ. The most common difference is that stdlib functions -only operate on the **current process**, while psutil works on **any process** +standard library. This is useful for understanding what psutil replaces and how +the two APIs differ. The most common difference is that stdlib functions only +operate on the **current process**, while psutil works on **any process** (PID). .. seealso:: diff --git a/setup.py b/setup.py index 971bca2a3e..1496c7ac09 100755 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ # `make install-pydeps-lint`. LINT_DEPS = [ "black", + "rstwrap", "ruff", "sphinx-lint", "toml-sort", From 02eb82f0a374b1915a95efd74be59dd1187cce90 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 17 Apr 2026 20:25:44 +0200 Subject: [PATCH 1710/1714] Get rid of _links.rst; use sphinx prolog + remove rst_check_dead_refs.py --- MANIFEST.in | 2 - Makefile | 1 - docs/_links.rst | 23 ---- docs/adoption.rst | 2 - docs/alternatives.rst | 2 - docs/api.rst | 4 - docs/changelog.rst | 3 - docs/conf.py | 33 ++++++ docs/credits.rst | 2 - docs/faq.rst | 2 - docs/funding.rst | 2 - docs/glossary.rst | 2 - docs/migration.rst | 3 - docs/performance.rst | 2 - docs/recipes.rst | 3 - docs/shell-equivalents.rst | 3 - docs/stdlib-equivalents.rst | 3 - scripts/internal/rst_check_dead_refs.py | 151 ------------------------ 18 files changed, 33 insertions(+), 210 deletions(-) delete mode 100644 docs/_links.rst delete mode 100755 scripts/internal/rst_check_dead_refs.py diff --git a/MANIFEST.in b/MANIFEST.in index 7574a7996d..29f2cc57a5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,7 +19,6 @@ include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py include docs/_ext/field_role.py include docs/_ext/genindex_filter.py -include docs/_links.rst include docs/_sponsors.html include docs/_static/css/custom.css include docs/_static/images/favicon.svg @@ -204,7 +203,6 @@ include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_sysinfo.py include scripts/internal/purge_installation.py -include scripts/internal/rst_check_dead_refs.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py diff --git a/Makefile b/Makefile index 5f838cb75f..4be2b16463 100644 --- a/Makefile +++ b/Makefile @@ -186,7 +186,6 @@ dprint: @$(DPRINT) check lint-rst: ## Run linter for .rst files. - @$(call _ls,'*.rst') | xargs python3 scripts/internal/rst_check_dead_refs.py @$(call _ls,'*.rst') | xargs sphinx-lint --enable all --disable line-too-long @$(call _ls,'*.rst') | xargs rstwrap --check diff --git a/docs/_links.rst b/docs/_links.rst deleted file mode 100644 index 42646f16fa..0000000000 --- a/docs/_links.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. _`BPO-10784`: https://bugs.python.org/issue10784 -.. _`BPO-12442`: https://bugs.python.org/issue12442 -.. _`BPO-6973`: https://bugs.python.org/issue6973 -.. _`GH-144047`: https://github.com/python/cpython/pull/144047 -.. _`scripts/battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py -.. _`scripts/cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`scripts/disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`scripts/fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py -.. _`scripts/free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py -.. _`scripts/ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py -.. _`scripts/iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`scripts/meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`scripts/netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`scripts/nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`scripts/pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py -.. _`scripts/pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`scripts/procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`scripts/procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`scripts/ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py -.. _`scripts/pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py -.. _`scripts/sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py -.. _`scripts/temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py -.. _`scripts/top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py diff --git a/docs/adoption.rst b/docs/adoption.rst index acdd826faa..1790af7ff0 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Who uses psutil =============== diff --git a/docs/alternatives.rst b/docs/alternatives.rst index 032d71684a..604ba0df0c 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Alternatives ============ diff --git a/docs/api.rst b/docs/api.rst index 86339ddbd6..79765351ea 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - .. note:: psutil 8.0 introduces breaking API changes. See the :ref:`migration guide ` if upgrading from 7.x. @@ -2790,7 +2787,6 @@ FreeBSD specific: .. versionadded:: 5.7.3 - .. data:: RLIMIT_SBSIZE .. versionadded:: 5.7.3 diff --git a/docs/changelog.rst b/docs/changelog.rst index d5be772d29..351a2963ce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - Changelog ========= diff --git a/docs/conf.py b/docs/conf.py index 73644fc0e3..8ee7a27eb5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -111,3 +111,36 @@ ("js/sidebar-close.js", {"defer": "defer"}), ("js/search-shortcuts.js", {"defer": "defer"}), ] + + +# ===================================================================== +# Links / includes shared by all .rst files +# ===================================================================== + +rst_prolog = """ +.. currentmodule:: psutil + +.. _`BPO-10784`: https://bugs.python.org/issue10784 +.. _`BPO-12442`: https://bugs.python.org/issue12442 +.. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`GH-144047`: https://github.com/python/cpython/pull/144047 +.. _`scripts/battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`scripts/cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`scripts/disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`scripts/fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`scripts/free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py +.. _`scripts/ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`scripts/iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`scripts/meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`scripts/netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`scripts/nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`scripts/pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py +.. _`scripts/pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`scripts/procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`scripts/procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`scripts/ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py +.. _`scripts/pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py +.. _`scripts/sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`scripts/temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py +.. _`scripts/top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py +""" diff --git a/docs/credits.rst b/docs/credits.rst index f41f62c4a5..3e0bab3726 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Credits ======= diff --git a/docs/faq.rst b/docs/faq.rst index 1fe418921e..3946de10d8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - FAQ === diff --git a/docs/funding.rst b/docs/funding.rst index d8b09f2f11..b63abc8cd0 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Funding ======= diff --git a/docs/glossary.rst b/docs/glossary.rst index 1dc657b082..22c29ab504 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Glossary ======== diff --git a/docs/migration.rst b/docs/migration.rst index abb4e609fa..0c6d476853 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - Migration guide =============== diff --git a/docs/performance.rst b/docs/performance.rst index f869fc4dcc..e9cecf4f2a 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Performance =========== diff --git a/docs/recipes.rst b/docs/recipes.rst index 07ce6ba76f..2be06ced49 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Recipes ======= @@ -360,7 +358,6 @@ Restart a process automatically if it dies: time.sleep(interval) - if __name__ == "__main__": watchdog(["python3", "script.py"]) diff --git a/docs/shell-equivalents.rst b/docs/shell-equivalents.rst index 8c5308ba5c..35cef170bb 100644 --- a/docs/shell-equivalents.rst +++ b/docs/shell-equivalents.rst @@ -1,6 +1,3 @@ - -.. currentmodule:: psutil - Shell equivalents ================= diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst index 7a829fa61a..ab7bccd4e8 100644 --- a/docs/stdlib-equivalents.rst +++ b/docs/stdlib-equivalents.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - Stdlib equivalents ================== diff --git a/scripts/internal/rst_check_dead_refs.py b/scripts/internal/rst_check_dead_refs.py deleted file mode 100755 index 9e7bdb4384..0000000000 --- a/scripts/internal/rst_check_dead_refs.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/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. - -"""Check RST files for two classes of reference errors: - -1. Hyperlink targets with a URL that are defined but never referenced. -2. Backtick references (`name`_) that point to an undefined target. - -Targets and references are resolved across all files passed as arguments, -so cross-file references work correctly. - -Usage:: - - python3 scripts/internal/rst_check_dead_refs.py docs/*.rst -""" - -import argparse -import os -import re -import sys - -# .. _`Foo Bar`: https://... or .. _foo: https://... (URL targets only) -RE_URL_TARGET = re.compile( - r'^\.\. _`?([^`\n:]+)`?\s*:\s*(https?://\S+)', - re.MULTILINE, -) -# .. _`Foo Bar`: or .. _foo: (any target, with or without URL) -RE_ANY_TARGET = re.compile( - r'^\.\. _`?([^`\n:]+)`?\s*:', - re.MULTILINE, -) -# `Foo Bar`_ but NOT `text `_ and NOT `text`__ -RE_BACKTICK_REF = re.compile(r'`([^`<\n]+)`_(?!_)') -# bare reference: BPO-12442_ (word chars and hyphens, no backticks) -RE_BARE_REF = re.compile( - r'(? Date: Fri, 17 Apr 2026 20:29:10 +0200 Subject: [PATCH 1711/1714] Revert "Get rid of _links.rst; use sphinx prolog + remove rst_check_dead_refs.py" This reverts commit 02eb82f0a374b1915a95efd74be59dd1187cce90. --- MANIFEST.in | 2 + Makefile | 1 + docs/_links.rst | 23 ++++ docs/adoption.rst | 2 + docs/alternatives.rst | 2 + docs/api.rst | 4 + docs/changelog.rst | 3 + docs/conf.py | 33 ------ docs/credits.rst | 2 + docs/faq.rst | 2 + docs/funding.rst | 2 + docs/glossary.rst | 2 + docs/migration.rst | 3 + docs/performance.rst | 2 + docs/recipes.rst | 3 + docs/shell-equivalents.rst | 3 + docs/stdlib-equivalents.rst | 3 + scripts/internal/rst_check_dead_refs.py | 151 ++++++++++++++++++++++++ 18 files changed, 210 insertions(+), 33 deletions(-) create mode 100644 docs/_links.rst create mode 100755 scripts/internal/rst_check_dead_refs.py diff --git a/MANIFEST.in b/MANIFEST.in index 29f2cc57a5..7574a7996d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,6 +19,7 @@ include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py include docs/_ext/field_role.py include docs/_ext/genindex_filter.py +include docs/_links.rst include docs/_sponsors.html include docs/_static/css/custom.css include docs/_static/images/favicon.svg @@ -203,6 +204,7 @@ include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_sysinfo.py include scripts/internal/purge_installation.py +include scripts/internal/rst_check_dead_refs.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py diff --git a/Makefile b/Makefile index 4be2b16463..5f838cb75f 100644 --- a/Makefile +++ b/Makefile @@ -186,6 +186,7 @@ dprint: @$(DPRINT) check lint-rst: ## Run linter for .rst files. + @$(call _ls,'*.rst') | xargs python3 scripts/internal/rst_check_dead_refs.py @$(call _ls,'*.rst') | xargs sphinx-lint --enable all --disable line-too-long @$(call _ls,'*.rst') | xargs rstwrap --check diff --git a/docs/_links.rst b/docs/_links.rst new file mode 100644 index 0000000000..42646f16fa --- /dev/null +++ b/docs/_links.rst @@ -0,0 +1,23 @@ +.. _`BPO-10784`: https://bugs.python.org/issue10784 +.. _`BPO-12442`: https://bugs.python.org/issue12442 +.. _`BPO-6973`: https://bugs.python.org/issue6973 +.. _`GH-144047`: https://github.com/python/cpython/pull/144047 +.. _`scripts/battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py +.. _`scripts/cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py +.. _`scripts/disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py +.. _`scripts/fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py +.. _`scripts/free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py +.. _`scripts/ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py +.. _`scripts/iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py +.. _`scripts/meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py +.. _`scripts/netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +.. _`scripts/nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py +.. _`scripts/pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py +.. _`scripts/pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py +.. _`scripts/procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py +.. _`scripts/procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py +.. _`scripts/ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py +.. _`scripts/pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py +.. _`scripts/sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py +.. _`scripts/temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py +.. _`scripts/top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py diff --git a/docs/adoption.rst b/docs/adoption.rst index 1790af7ff0..acdd826faa 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Who uses psutil =============== diff --git a/docs/alternatives.rst b/docs/alternatives.rst index 604ba0df0c..032d71684a 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Alternatives ============ diff --git a/docs/api.rst b/docs/api.rst index 79765351ea..86339ddbd6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,3 +1,6 @@ +.. currentmodule:: psutil +.. include:: _links.rst + .. note:: psutil 8.0 introduces breaking API changes. See the :ref:`migration guide ` if upgrading from 7.x. @@ -2787,6 +2790,7 @@ FreeBSD specific: .. versionadded:: 5.7.3 + .. data:: RLIMIT_SBSIZE .. versionadded:: 5.7.3 diff --git a/docs/changelog.rst b/docs/changelog.rst index 351a2963ce..d5be772d29 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,3 +1,6 @@ +.. currentmodule:: psutil +.. include:: _links.rst + Changelog ========= diff --git a/docs/conf.py b/docs/conf.py index 8ee7a27eb5..73644fc0e3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -111,36 +111,3 @@ ("js/sidebar-close.js", {"defer": "defer"}), ("js/search-shortcuts.js", {"defer": "defer"}), ] - - -# ===================================================================== -# Links / includes shared by all .rst files -# ===================================================================== - -rst_prolog = """ -.. currentmodule:: psutil - -.. _`BPO-10784`: https://bugs.python.org/issue10784 -.. _`BPO-12442`: https://bugs.python.org/issue12442 -.. _`BPO-6973`: https://bugs.python.org/issue6973 -.. _`GH-144047`: https://github.com/python/cpython/pull/144047 -.. _`scripts/battery.py`: https://github.com/giampaolo/psutil/blob/master/scripts/battery.py -.. _`scripts/cpu_distribution.py`: https://github.com/giampaolo/psutil/blob/master/scripts/cpu_distribution.py -.. _`scripts/disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py -.. _`scripts/fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py -.. _`scripts/free.py`: https://github.com/giampaolo/psutil/blob/master/scripts/free.py -.. _`scripts/ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py -.. _`scripts/iotop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py -.. _`scripts/meminfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/meminfo.py -.. _`scripts/netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py -.. _`scripts/nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py -.. _`scripts/pidof.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pidof.py -.. _`scripts/pmap.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pmap.py -.. _`scripts/procinfo.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procinfo.py -.. _`scripts/procsmem.py`: https://github.com/giampaolo/psutil/blob/master/scripts/procsmem.py -.. _`scripts/ps.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ps.py -.. _`scripts/pstree.py`: https://github.com/giampaolo/psutil/blob/master/scripts/pstree.py -.. _`scripts/sensors.py`: https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py -.. _`scripts/temperatures.py`: https://github.com/giampaolo/psutil/blob/master/scripts/temperatures.py -.. _`scripts/top.py`: https://github.com/giampaolo/psutil/blob/master/scripts/top.py -""" diff --git a/docs/credits.rst b/docs/credits.rst index 3e0bab3726..f41f62c4a5 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Credits ======= diff --git a/docs/faq.rst b/docs/faq.rst index 3946de10d8..1fe418921e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + FAQ === diff --git a/docs/funding.rst b/docs/funding.rst index b63abc8cd0..d8b09f2f11 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Funding ======= diff --git a/docs/glossary.rst b/docs/glossary.rst index 22c29ab504..1dc657b082 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Glossary ======== diff --git a/docs/migration.rst b/docs/migration.rst index 0c6d476853..abb4e609fa 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -1,3 +1,6 @@ +.. currentmodule:: psutil +.. include:: _links.rst + Migration guide =============== diff --git a/docs/performance.rst b/docs/performance.rst index e9cecf4f2a..f869fc4dcc 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Performance =========== diff --git a/docs/recipes.rst b/docs/recipes.rst index 2be06ced49..07ce6ba76f 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + Recipes ======= @@ -358,6 +360,7 @@ Restart a process automatically if it dies: time.sleep(interval) + if __name__ == "__main__": watchdog(["python3", "script.py"]) diff --git a/docs/shell-equivalents.rst b/docs/shell-equivalents.rst index 35cef170bb..8c5308ba5c 100644 --- a/docs/shell-equivalents.rst +++ b/docs/shell-equivalents.rst @@ -1,3 +1,6 @@ + +.. currentmodule:: psutil + Shell equivalents ================= diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst index ab7bccd4e8..7a829fa61a 100644 --- a/docs/stdlib-equivalents.rst +++ b/docs/stdlib-equivalents.rst @@ -1,3 +1,6 @@ +.. currentmodule:: psutil +.. include:: _links.rst + Stdlib equivalents ================== diff --git a/scripts/internal/rst_check_dead_refs.py b/scripts/internal/rst_check_dead_refs.py new file mode 100755 index 0000000000..9e7bdb4384 --- /dev/null +++ b/scripts/internal/rst_check_dead_refs.py @@ -0,0 +1,151 @@ +#!/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. + +"""Check RST files for two classes of reference errors: + +1. Hyperlink targets with a URL that are defined but never referenced. +2. Backtick references (`name`_) that point to an undefined target. + +Targets and references are resolved across all files passed as arguments, +so cross-file references work correctly. + +Usage:: + + python3 scripts/internal/rst_check_dead_refs.py docs/*.rst +""" + +import argparse +import os +import re +import sys + +# .. _`Foo Bar`: https://... or .. _foo: https://... (URL targets only) +RE_URL_TARGET = re.compile( + r'^\.\. _`?([^`\n:]+)`?\s*:\s*(https?://\S+)', + re.MULTILINE, +) +# .. _`Foo Bar`: or .. _foo: (any target, with or without URL) +RE_ANY_TARGET = re.compile( + r'^\.\. _`?([^`\n:]+)`?\s*:', + re.MULTILINE, +) +# `Foo Bar`_ but NOT `text `_ and NOT `text`__ +RE_BACKTICK_REF = re.compile(r'`([^`<\n]+)`_(?!_)') +# bare reference: BPO-12442_ (word chars and hyphens, no backticks) +RE_BARE_REF = re.compile( + r'(? Date: Fri, 17 Apr 2026 20:49:27 +0200 Subject: [PATCH 1712/1714] Doc: use rst_prolog to avoid including links + module in every .rst file --- MANIFEST.in | 2 +- docs/{_links.rst => _globals.rst} | 2 ++ docs/adoption.rst | 2 -- docs/alternatives.rst | 2 -- docs/api.rst | 3 --- docs/changelog.rst | 3 --- docs/conf.py | 8 +++++++- docs/credits.rst | 2 -- docs/faq.rst | 2 -- docs/funding.rst | 2 -- docs/glossary.rst | 2 -- docs/migration.rst | 3 --- docs/performance.rst | 2 -- docs/recipes.rst | 2 -- docs/shell-equivalents.rst | 3 --- docs/stdlib-equivalents.rst | 3 --- 16 files changed, 10 insertions(+), 33 deletions(-) rename docs/{_links.rst => _globals.rst} (98%) diff --git a/MANIFEST.in b/MANIFEST.in index 7574a7996d..00ab80247d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,7 +19,7 @@ include docs/_ext/changelog_anchors.py include docs/_ext/check_python_syntax.py include docs/_ext/field_role.py include docs/_ext/genindex_filter.py -include docs/_links.rst +include docs/_globals.rst include docs/_sponsors.html include docs/_static/css/custom.css include docs/_static/images/favicon.svg diff --git a/docs/_links.rst b/docs/_globals.rst similarity index 98% rename from docs/_links.rst rename to docs/_globals.rst index 42646f16fa..f7c3a5276f 100644 --- a/docs/_links.rst +++ b/docs/_globals.rst @@ -1,3 +1,5 @@ +.. currentmodule:: psutil + .. _`BPO-10784`: https://bugs.python.org/issue10784 .. _`BPO-12442`: https://bugs.python.org/issue12442 .. _`BPO-6973`: https://bugs.python.org/issue6973 diff --git a/docs/adoption.rst b/docs/adoption.rst index acdd826faa..1790af7ff0 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Who uses psutil =============== diff --git a/docs/alternatives.rst b/docs/alternatives.rst index 032d71684a..604ba0df0c 100644 --- a/docs/alternatives.rst +++ b/docs/alternatives.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Alternatives ============ diff --git a/docs/api.rst b/docs/api.rst index 86339ddbd6..29a9861f2a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - .. note:: psutil 8.0 introduces breaking API changes. See the :ref:`migration guide ` if upgrading from 7.x. diff --git a/docs/changelog.rst b/docs/changelog.rst index d5be772d29..351a2963ce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - Changelog ========= diff --git a/docs/conf.py b/docs/conf.py index 73644fc0e3..8aa9db9b2f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -69,7 +69,7 @@ # ===================================================================== html_static_path = ["_static"] -exclude_patterns = ["_build"] +exclude_patterns = ["_build", "_globals.rst"] # ===================================================================== # HTML @@ -111,3 +111,9 @@ ("js/sidebar-close.js", {"defer": "defer"}), ("js/search-shortcuts.js", {"defer": "defer"}), ] + +# ===================================================================== +# Prolog prepended to every .rst file +# ===================================================================== + +rst_prolog = (_HERE / "_globals.rst").read_text() diff --git a/docs/credits.rst b/docs/credits.rst index f41f62c4a5..3e0bab3726 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Credits ======= diff --git a/docs/faq.rst b/docs/faq.rst index 1fe418921e..3946de10d8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - FAQ === diff --git a/docs/funding.rst b/docs/funding.rst index d8b09f2f11..b63abc8cd0 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Funding ======= diff --git a/docs/glossary.rst b/docs/glossary.rst index 1dc657b082..22c29ab504 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Glossary ======== diff --git a/docs/migration.rst b/docs/migration.rst index abb4e609fa..0c6d476853 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - Migration guide =============== diff --git a/docs/performance.rst b/docs/performance.rst index f869fc4dcc..e9cecf4f2a 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Performance =========== diff --git a/docs/recipes.rst b/docs/recipes.rst index 07ce6ba76f..db2fde4df1 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -1,5 +1,3 @@ -.. currentmodule:: psutil - Recipes ======= diff --git a/docs/shell-equivalents.rst b/docs/shell-equivalents.rst index 8c5308ba5c..35cef170bb 100644 --- a/docs/shell-equivalents.rst +++ b/docs/shell-equivalents.rst @@ -1,6 +1,3 @@ - -.. currentmodule:: psutil - Shell equivalents ================= diff --git a/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst index 7a829fa61a..ab7bccd4e8 100644 --- a/docs/stdlib-equivalents.rst +++ b/docs/stdlib-equivalents.rst @@ -1,6 +1,3 @@ -.. currentmodule:: psutil -.. include:: _links.rst - Stdlib equivalents ================== From 06950267a42107d3859fe3703f20ad313bd87e01 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 17 Apr 2026 21:38:58 +0200 Subject: [PATCH 1713/1714] Simplify and rename rst_unused_targets.py script --- MANIFEST.in | 2 +- Makefile | 2 +- scripts/internal/rst_check_dead_refs.py | 151 ------------------------ scripts/internal/rst_unused_targets.py | 59 +++++++++ 4 files changed, 61 insertions(+), 153 deletions(-) delete mode 100755 scripts/internal/rst_check_dead_refs.py create mode 100755 scripts/internal/rst_unused_targets.py diff --git a/MANIFEST.in b/MANIFEST.in index 00ab80247d..2491e05915 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -204,7 +204,7 @@ include scripts/internal/print_downloads.py include scripts/internal/print_hashes.py include scripts/internal/print_sysinfo.py include scripts/internal/purge_installation.py -include scripts/internal/rst_check_dead_refs.py +include scripts/internal/rst_unused_targets.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py diff --git a/Makefile b/Makefile index 5f838cb75f..38d0f891ae 100644 --- a/Makefile +++ b/Makefile @@ -186,7 +186,7 @@ dprint: @$(DPRINT) check lint-rst: ## Run linter for .rst files. - @$(call _ls,'*.rst') | xargs python3 scripts/internal/rst_check_dead_refs.py + @$(call _ls,'*.rst') | xargs python3 scripts/internal/rst_unused_targets.py @$(call _ls,'*.rst') | xargs sphinx-lint --enable all --disable line-too-long @$(call _ls,'*.rst') | xargs rstwrap --check diff --git a/scripts/internal/rst_check_dead_refs.py b/scripts/internal/rst_check_dead_refs.py deleted file mode 100755 index 9e7bdb4384..0000000000 --- a/scripts/internal/rst_check_dead_refs.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/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. - -"""Check RST files for two classes of reference errors: - -1. Hyperlink targets with a URL that are defined but never referenced. -2. Backtick references (`name`_) that point to an undefined target. - -Targets and references are resolved across all files passed as arguments, -so cross-file references work correctly. - -Usage:: - - python3 scripts/internal/rst_check_dead_refs.py docs/*.rst -""" - -import argparse -import os -import re -import sys - -# .. _`Foo Bar`: https://... or .. _foo: https://... (URL targets only) -RE_URL_TARGET = re.compile( - r'^\.\. _`?([^`\n:]+)`?\s*:\s*(https?://\S+)', - re.MULTILINE, -) -# .. _`Foo Bar`: or .. _foo: (any target, with or without URL) -RE_ANY_TARGET = re.compile( - r'^\.\. _`?([^`\n:]+)`?\s*:', - re.MULTILINE, -) -# `Foo Bar`_ but NOT `text `_ and NOT `text`__ -RE_BACKTICK_REF = re.compile(r'`([^`<\n]+)`_(?!_)') -# bare reference: BPO-12442_ (word chars and hyphens, no backticks) -RE_BARE_REF = re.compile( - r'(?`_ and NOT `text`__ +RE_BACKTICK_REF = re.compile(r"`([^`<\n]+)`_(?!_)") + +# bare ref: BPO-12442_ +RE_BARE_REF = re.compile(r"(? (name, path, lineno) + used = set() + for path in args.files: + with open(path) as f: + text = f.read() + for m in RE_URL_TARGET.finditer(text): + name = m.group(1).strip() + lineno = text.count("\n", 0, m.start()) + 1 + defined[name.lower()] = (name, path, lineno) + for regex in (RE_BACKTICK_REF, RE_BARE_REF): + used.update( + m.group(1).strip().lower() for m in regex.finditer(text) + ) + + errors = sorted( + (path, lineno, f"unreferenced hyperlink target: {name!r}") + for key, (name, path, lineno) in defined.items() + if key not in used + ) + for path, lineno, msg in errors: + print(f"{path}:{lineno}: {msg}") + if errors: + sys.exit(1) + + +if __name__ == "__main__": + main() From a9bc87ab8501c50b96f7e32add69fad20323992f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 19 Apr 2026 17:27:15 +0200 Subject: [PATCH 1714/1714] Theming of pygments, glossary, adoptions, search res, footer --- docs/_sponsors.html | 5 +- docs/_static/css/custom.css | 903 +++++++++++++++++++++++++++--------- docs/_templates/footer.html | 13 +- docs/adoption.rst | 77 +-- docs/conf.py | 2 +- docs/glossary.rst | 4 +- docs/index.rst | 27 +- docs/migration.rst | 4 +- 8 files changed, 732 insertions(+), 303 deletions(-) diff --git a/docs/_sponsors.html b/docs/_sponsors.html index 33f66003c0..f6038a20b2 100644 --- a/docs/_sponsors.html +++ b/docs/_sponsors.html @@ -58,5 +58,6 @@ -
    -
    + diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index a6cacbd20c..a685446bea 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -47,12 +47,12 @@ /* links */ --links: #2980b9; - --links-visited: #9b59b6; + --links-visited: #2980b9; /* same as --links: visited state disabled site-wide */ --links-api: #336699; --links-api-underline: var(--links-api); /* inline code */ - --inline-code-bg: rgba(175, 184, 193, 0.2); + --inline-code-bg: rgba(175, 184, 193, 0.17); --inline-code-text: inherit; /* top bar */ @@ -77,6 +77,7 @@ --adm-title: black; --adm-note-bg: var(--surface-sunken); --adm-note-border: #ccc; + --adm-note-accent-border: #2980b9; --adm-seealso-accent-border: #336699; --adm-seealso-link: #2a6099; --adm-seealso-bg: #f3f9fe; @@ -112,6 +113,10 @@ /* cards */ --card-bg: #fff; --card-border: #b0cfe8; + + /* primary CTA button */ + --cta-fill: color-mix(in srgb, var(--links) 78%, #666 22%); + --cta-fill-hover: var(--links); } /* ================================================================== */ @@ -119,14 +124,13 @@ /* ================================================================== */ [data-theme="dark"] { - /* page */ - --body-bg: #1e1e1e; - --content-bg: #1e1e1e; - --topbar-bg: #272727; - --sidebar-bg: #272727; - --sidebar-top-bg: #272727; - --surface-sunken: #2a2a2a; - --surface-raised: #2e2e2e; + --body-bg: #141414; + --content-bg: #212121; + --topbar-bg: #181818; + --sidebar-bg: #181818; + --sidebar-top-bg: #181818; + --surface-sunken: #272727; + --surface-raised: #2c2c2c; --border: #3a3a3a; /* text */ @@ -136,7 +140,7 @@ --headings-underline: #3a3a3a; /* inline code */ - --inline-code-bg: #2a2f36; + --inline-code-bg: rgba(255, 255, 255, 0.08); --inline-code-text: #e6e6e6; /* ntuple fields */ @@ -144,7 +148,7 @@ /* links */ --links: #6ab0de; - --links-visited: #c5a3ff; + --links-visited: #6ab0de; /* same as --links: visited state disabled site-wide */ --links-api: #7ecfff; --links-api-underline: #7ecfff; @@ -152,9 +156,10 @@ --adm-title: #ddd; --adm-note-bg: #2a2a2a; --adm-note-border: #4a4a4a; + --adm-note-accent-border: #6ab0de; --adm-seealso-accent-border: #5599cc; --adm-seealso-link: #90c8ee; - --adm-seealso-bg: #243a4e; + --adm-seealso-bg: #1d2e40; --adm-seealso-border: #2d5280; --adm-seealso-hover: #8bbfe0; --adm-seealso-title: #7ab0d9; @@ -165,7 +170,7 @@ /* version directives */ --versionadded-color: #5ec46e; - --versionchanged-color: #c8963a; + --versionchanged-color: #b0873a; --deprecated-color: #d06060; /* tables */ @@ -178,6 +183,9 @@ --card-bg: #252525; --card-border: #2a4a6a; + /* primary CTA button */ + --cta-fill: #2d6fa8; + --cta-fill-hover: #3a85c4; } /* ================================================================== */ @@ -292,10 +300,30 @@ .top-bar-link:link, .top-bar-link:visited, .top-bar-version:link, -.top-bar-version:visited { +.top-bar-version:visited, +.top-bar-text:link, +.top-bar-text:visited { color: var(--topbar-fg); } +.top-bar-text { + display: inline-flex; + align-items: center; + height: 32px; + padding: 0 10px; + font-size: 1.05rem; + font-weight: 600; + text-decoration: none; + border-radius: 6px; + transition: background var(--theme-transition), color var(--theme-transition); +} + +.top-bar-text:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--topbar-fg-active); + text-decoration: none; +} + .top-bar-version:hover { color: var(--topbar-fg-active); border-color: rgba(255, 255, 255, 0.45); @@ -356,6 +384,78 @@ vertical-align: middle; } +/* Primary + secondary call-to-action buttons below the description cards. */ +.hero-cta { + margin: 2.5em 0 3em; + display: flex; + justify-content: center; + gap: 12px; + flex-wrap: wrap; +} + +.rst-content a.hero-btn, +.rst-content a.hero-btn:visited { + display: inline-flex; + align-items: center; + gap: 0.5em; + padding: 0.55em 1.4em; + border-radius: var(--border-radius); + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 0.01em; + text-decoration: none !important; + border: 1px solid transparent; + transition: background var(--theme-transition), + color var(--theme-transition), + border-color var(--theme-transition), + box-shadow var(--theme-transition), + transform 0.08s ease; +} + +.rst-content a.hero-btn:active { + transform: translateY(1px); +} + +.rst-content a.hero-btn i.fa { + font-size: 0.8em; + transition: transform 0.15s ease; +} + +.rst-content a.hero-btn:hover i.fa-arrow-right { + transform: translateX(3px); +} + +.rst-content a.hero-btn-primary, +.rst-content a.hero-btn-primary:visited { + background: var(--cta-fill); + color: #fff !important; + border-color: var(--cta-fill); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); +} + +.rst-content a.hero-btn-primary:hover { + background: var(--cta-fill-hover); + border-color: var(--cta-fill-hover); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); +} + +[data-theme="dark"] .rst-content a.hero-btn-primary, +[data-theme="dark"] .rst-content a.hero-btn-primary:visited { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); +} + +/* secondary: outlined, same palette */ +.rst-content a.hero-btn-secondary, +.rst-content a.hero-btn-secondary:visited { + background: transparent; + color: var(--links) !important; + border-color: var(--links); +} + +.rst-content a.hero-btn-secondary:hover { + background: var(--surface-sunken); +} + .sponsor-table { margin-left: auto !important; margin-right: auto !important; @@ -395,11 +495,6 @@ text-align: center; } -.home-page .sponsor-table { - margin-left: auto !important; - margin-right: auto !important; -} - .sponsor-logo { filter: grayscale(100%) opacity(100%); transition: filter 0.2s; @@ -413,20 +508,37 @@ .home-platforms { display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 8px; + flex-direction: column; + align-items: center; + gap: 0.6em; margin: 1em 0 3em; } -.home-platform-label { - font-size: 0.92rem; +.rst-content a.home-platforms-label, +.rst-content a.home-platforms-label:visited { + font-size: 0.82rem; color: var(--text-muted); - font-weight: 600; - align-self: center; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + text-decoration: none; + transition: color var(--theme-transition); } -.home-platform-pill { +.rst-content a.home-platforms-label:hover { + color: var(--content-text); + text-decoration: none; +} + +.home-platforms-pills { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 8px; +} + +.rst-content a.home-platform-pill, +.rst-content a.home-platform-pill:visited { display: inline-block; padding: 3px 12px; border: 1px solid var(--border); @@ -434,6 +546,17 @@ font-size: 0.82rem; color: var(--text-muted); background: var(--surface-sunken); + text-decoration: none !important; + transition: border-color var(--theme-transition), + color var(--theme-transition), + background var(--theme-transition); +} + +.rst-content a.home-platform-pill:hover { + border-color: var(--text-muted); + color: var(--content-text); + background: var(--surface-raised); + text-decoration: none !important; } /* Feature icon cards ------------------------------------------------ */ @@ -570,91 +693,75 @@ } .home-desc-card:nth-child(1) .home-desc-card-icon { color: #2a6a90; } -.home-desc-card:nth-child(2) .home-desc-card-icon { color: #c07800; } +.home-desc-card:nth-child(2) .home-desc-card-icon { color: #8a7550; } .home-desc-card:nth-child(3) .home-desc-card-icon { color: #5d6d7e; } -.home-desc-card:nth-child(1) .home-desc-card-header { - background: linear-gradient(to bottom, #6aafd4, #4a90b8); - border-bottom-color: #3a7aa0; +.home-desc-card:nth-child(1), +.home-desc-card:nth-child(2), +.home-desc-card:nth-child(3) { + position: relative; + overflow: hidden; } -.home-desc-card:nth-child(1) { +.home-desc-card:nth-child(1), +.home-desc-card:nth-child(3) { background: #f0f6fb; - position: relative; - overflow: hidden; } -[data-theme="dark"] .home-desc-card:nth-child(1) { +.home-desc-card:nth-child(2) { + background: #faf5ec; +} + +[data-theme="dark"] .home-desc-card:nth-child(1), +[data-theme="dark"] .home-desc-card:nth-child(3) { background: #1a2430; } -.home-desc-card:nth-child(1)::after { - content: '\f085'; - font-family: 'FontAwesome'; - font-size: 7rem; - position: absolute; - bottom: -0.75rem; - right: 0.5rem; - opacity: 0.07; - color: #2a6a90; - pointer-events: none; - line-height: 1; +[data-theme="dark"] .home-desc-card:nth-child(2) { + background: #1f1c14; } -.home-desc-card:nth-child(2) .home-desc-card-header { - background: linear-gradient(to bottom, #f5ca68, #e8a830); - border-bottom-color: #c98e20; +.home-desc-card:nth-child(1) .home-desc-card-header { + background: linear-gradient(to bottom, #6aafd4, #4a90b8); + border-bottom-color: #3a7aa0; } -.home-desc-card:nth-child(2) { - background: #fdf8ee; - position: relative; - overflow: hidden; +.home-desc-card:nth-child(2) .home-desc-card-header { + background: linear-gradient(to bottom, #d9c49a, #b8a078); + border-bottom-color: #8a7550; } -[data-theme="dark"] .home-desc-card:nth-child(2) { - background: #221a08; +.home-desc-card:nth-child(3) .home-desc-card-header { + background: linear-gradient(to bottom, #a0b0bc, #7a8fa0); + border-bottom-color: #637888; } -.home-desc-card:nth-child(2)::after { - content: '\f080'; +.home-desc-card:nth-child(1)::before, +.home-desc-card:nth-child(2)::before, +.home-desc-card:nth-child(3)::before { font-family: 'FontAwesome'; font-size: 7rem; position: absolute; bottom: -0.75rem; right: 0.5rem; opacity: 0.07; - color: #c07800; pointer-events: none; line-height: 1; } -.home-desc-card:nth-child(3) .home-desc-card-header { - background: linear-gradient(to bottom, #a0b0bc, #7a8fa0); - border-bottom-color: #637888; -} - -.home-desc-card:nth-child(3) { - background: #f0f6fb; - position: relative; - overflow: hidden; +.home-desc-card:nth-child(1)::before { + content: '\f085'; + color: #2a6a90; } -[data-theme="dark"] .home-desc-card:nth-child(3) { - background: #1a2430; +.home-desc-card:nth-child(2)::before { + content: '\f080'; + color: #8a7550; } -.home-desc-card:nth-child(3)::after { +.home-desc-card:nth-child(3)::before { content: '\f0ac'; - font-family: 'FontAwesome'; - font-size: 7rem; - position: absolute; - bottom: -0.75rem; - right: 0.5rem; - opacity: 0.07; color: #5d6d7e; - pointer-events: none; - line-height: 1; } .home-desc-card-header .home-desc-card-title { @@ -723,6 +830,9 @@ html { background: var(--body-bg); scroll-padding-top: 48px; + /* Always reserve scrollbar space so layout doesn't move on refresh + (happens for search results) */ + scrollbar-gutter: stable; } body { @@ -783,15 +893,12 @@ body { /* Navigation */ /* ================================================================== */ -/* hide top navigation, keep footer navigation */ +/* Hide the RTD top navigation (breadcrumb). On blog pages + _templates/breadcrumbs.html renders a custom "Blog" banner instead.*/ .rst-content > div[role="navigation"] { display: none; } -footer div[role="navigation"][aria-label="Footer"] { - display: block; -} - /* ================================================================== */ /* Sidebar */ /* ================================================================== */ @@ -971,6 +1078,9 @@ body, /* Footer */ /* ================================================================== */ +footer div[role="navigation"][aria-label="Footer"] { + display: block; +} .footer-content { display: flex; @@ -978,29 +1088,70 @@ body, gap: 12px; } -.footer-logo { - height: 1.6em !important; - width: auto !important; - flex-shrink: 0; - padding-bottom: 5px; - margin-right: -5px; - filter: grayscale(100%) opacity(0.5); -} - .footer-text { - color: #999 !important; - /*font-size: 0.88em !important;*/ + color: var(--text-muted); + opacity: 0.7; line-height: 1.4; + display: flex; + flex-wrap: wrap; + gap: 0.3em 0.7em; +} + +/* Middot separator between items — attached to the end of each + item (except the last) so wrapping never leaves an orphan `·` + at the start of a line. */ +.footer-text > span:not(:last-child)::after { + content: "·"; + margin-left: 0.7em; + color: var(--text-muted); + opacity: 0.7; } .footer-text a, -.footer-text a:visited, +.footer-text a:visited { + color: var(--text-muted) !important; + text-decoration: underline; + text-decoration-color: var(--border); + text-underline-offset: 3px; +} + .footer-text a:hover { color: var(--links) !important; + text-decoration-color: currentColor; } -footer hr { - border-color: var(--border) !important; +/* RSS icon anchored to the right of the footer row. Icon-only link, + so :visited must not change its color (per the theme convention + for button/icon links). */ +footer a.footer-rss, +footer a.footer-rss:visited { + margin-left: auto; + display: inline-flex; + align-items: center; + color: #555; + text-decoration: none; + flex-shrink: 0; +} + +[data-theme="dark"] footer a.footer-rss, +[data-theme="dark"] footer a.footer-rss:visited { + color: #bbb; +} + +footer a.footer-rss:hover { + color: var(--links); +} + +a.footer-rss svg { + width: 1.4em; + height: 1.4em; + margin-top: -4px; +} + +footer [role="contentinfo"] { + margin-top: 2em; + padding-top: 1.2em; + border-top: 1px solid var(--border); } /* Prev / Next navigation buttons */ @@ -1044,20 +1195,20 @@ h4, h4 a { } h1 { - font-size: 2.6rem; + font-size: 2.2rem; } h2 { - font-size: 1.8rem; + font-size: 1.7rem; } h3 { - font-size: 1.2rem; + font-size: 1.3rem; padding: 5px 0 5px 0; } h4 { - font-size: 0.9rem; + font-size: 1.05rem; padding: 5px 0 5px 0; } @@ -1092,7 +1243,7 @@ h4 { } .document th { - padding: 4px 8px !important; + padding: 6px 10px !important; font-weight: 600; } @@ -1101,7 +1252,7 @@ h4 { } .document td { - padding: 4px 8px !important; + padding: 6px 10px !important; } .document td p { @@ -1137,14 +1288,35 @@ h4 { overflow-wrap: break-word; } +/* list-tables (used by adoption.rst): respect declared :widths: + ratios and wrap content instead of clipping or horizontal-scrolling. */ +.rst-content table.docutils { + table-layout: fixed; + width: 100%; +} -/* adoption page logos */ -.document td img[alt$="-logo"] { - height: 20px !important; - width: 20px !important; +.rst-content table.docutils th, +.rst-content table.docutils td { + white-space: normal !important; + word-wrap: break-word; + overflow-wrap: break-word; +} + +/* shields.io star badges: light dim so they recede a bit. */ +.rst-content td img[alt$="-stars"] { + opacity: 0.9; vertical-align: middle; } +/* Shrink section headers so they don't dominate. */ +.rst-content:has(#who-uses-psutil) h2 { + font-size: 1.3rem; + margin-top: 2em; + margin-bottom: 0.4em; + padding-bottom: 0.2em; + border-bottom: 1px solid var(--border); +} + /* ================================================================== */ /* Lists */ /* ================================================================== */ @@ -1174,38 +1346,36 @@ h4 { color: var(--links-visited); } -/* ================================================================== */ -/* API signatures */ -/* ================================================================== */ - -.rst-content dl:not(.docutils) { - margin: 0px 0px 0px 0px !important; -} - -.rst-content dl.py + dl.py > dt { - margin-top: 28px !important; -} - -.rst-content dl:not(.docutils) dt { - color: var(--func-sig-text); +a.external[href^="#"] { + text-decoration: none; + color: inherit; } -.data dd { - margin-bottom: 0px !important; +a.reference.external:not([href*="docs.python.org"]):not(:has(img)) { + text-decoration: none; } -.data .descname { - border-right:10px !important; +/* external links: show a small icon after the link text */ +a.reference.external:not(:has(img))::after { + content: '\f08e'; + font-family: FontAwesome; + font-size: 0.4em; + margin-left: 0.3em; + vertical-align: super; + opacity: 0.6; } -.function .descclassname, -.class .descclassname { - font-weight: normal !important; +/* adjust cpython API links (intersphinx) icon position */ +a.reference.external[href*="docs.python.org"]:not(:has(img))::after { + margin-left: 0.2em; } -.sig-paren { - padding-left: 2px; - padding-right: 2px; +/* When the link wraps inline code, the has its own right + padding (.35em) which stacks with the icon's margin, making the + icon look detached. Pull it closer. */ +a.reference.external:has(> code):not(:has(img))::after, +a.reference.external:has(> span > code):not(:has(img))::after { + margin-left: 0; } /* ================================================================== */ @@ -1258,37 +1428,6 @@ code.ntuple-field { font-weight: bold !important; } -/* ================================================================== */ -/* Links */ -/* ================================================================== */ - -a.external[href^="#"] { - text-decoration: none; - color: inherit; -} - -a.reference.external:not([href*="docs.python.org"]):not(:has(img)) { - text-decoration: none; -} - -/* external links: show a small icon */ -a.reference.external:not(:has(img))::after { - content: '\f08e'; - font-family: FontAwesome; - font-size: 0.4em; - margin-left: 0.25em; - padding-left: 1px; - vertical-align: super; - opacity: 0.6; -} - -/* adjust cpython API links (intersphinx) icon position */ -a.reference.external[href*="docs.python.org"]:not(:has(img))::after { - margin-left: -3px; - margin-right: 2px; - padding-left: 0; -} - /* ================================================================== */ /* Admonitions (note, warning, tip) - styled like python doc */ /* ================================================================== */ @@ -1300,7 +1439,7 @@ div.admonition { padding: var(--adm-padding-y) 10px !important; background-color: var(--adm-note-bg) !important; border: 1px solid var(--adm-note-border) !important; - border-left: 3px solid var(--adm-note-border) !important; + border-left: 4px solid var(--adm-note-accent-border) !important; border-radius: var(--border-radius) !important; } @@ -1408,6 +1547,18 @@ div.deprecated .versionmodified { /* Search results page */ /* ================================================================== */ +/* ---- Result cards ---- */ + +/* "Search finished, found N pages...": increase size/weight */ +#search-results p.search-summary { + font-size: 1.15em !important; + font-weight: 500 !important; + margin: 0 0 1.2em 0 !important; + padding-bottom: 0.8em !important; + border-bottom: 1px solid var(--border); + color: var(--headings); +} + #search-results ul { padding: 8px 0; display: flex; @@ -1422,7 +1573,7 @@ div.deprecated .versionmodified { display: flex !important; flex-wrap: wrap; align-items: flex-start; - column-gap: 8px; + column-gap: 6px; line-height: normal !important; border: 1px solid rgba(0, 0, 0, 0.08) !important; border-radius: 6px; @@ -1452,12 +1603,60 @@ div.deprecated .versionmodified { box-shadow: 0 0 0 3px rgba(106, 176, 222, 0.2); } -/* icon */ +/* ---- Card content ---- */ + +/* link title — takes remaining space on first row. Result cards look + like buttons/chips, so don't let :visited turn them violet. Force + a single title color so result rows don't look "rainbow" based on + link target (icons are the per-type color cue). */ +#search-results li > a, +#search-results li > a:visited, +#search-results li > a *, +#search-results li > a:visited * { + flex: 1; + font-weight: bold; + margin-bottom: 0; + color: var(--links) !important; +} + +/* description text e.g. "(Python function, in API reference)" */ +#search-results li > span { + font-size: 0.75em; + font-weight: 500; + color: var(--text-muted); + background: var(--surface-sunken); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.1em 0.55em; + align-self: center; + opacity: 0.85; + white-space: nowrap; +} + +/* context paragraph: full width, aligned with the title. */ +#search-results li > p.context { + flex-basis: 100%; + margin: 0 0 0 28px !important; + font-size: 0.90em !important; + color: var(--text-muted); + opacity: 0.8; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ---- Icons: generic (Sphinx SearchResultKind) ---- */ + #search-results li::before { font-family: FontAwesome; - font-size: 13px; + font-size: 18px; flex-shrink: 0; - margin-top: 3px; + margin-top: 1px; + width: 1.2em; + text-align: center; } /* page title match */ @@ -1481,28 +1680,73 @@ div.deprecated .versionmodified { color: #e5a825; } -/* link title — takes remaining space on first row */ -#search-results li > a { - flex: 1; - font-weight: bold; - margin-bottom: 0; +/* ---- Icons: page-specific overrides ---- */ + +/* blog post: RSS feed */ +#search-results li:has(> a[href^="blog/"])::before, +#search-results li:has(> a[href*="/blog/"])::before { + content: "\f09e"; + color: #0d9488; } -/* description text e.g. "(Python function, in API reference)" */ -#search-results li > span { - font-size: 0.82em; +/* FAQ: question-in-circle */ +#search-results li:has(> a[href^="faq.html"])::before, +#search-results li:has(> a[href*="/faq.html"])::before { + content: "\f059"; + color: #2563eb; } -/* context paragraph — full width, indented past the icon */ -#search-results li > p.context { - flex-basis: 100%; - margin: 0 0 0 20px !important; - font-size: 0.90em !important; - color: var(--text-muted); - opacity: 0.8; - line-height: 1.5; +[data-theme="dark"] #search-results li:has(> a[href^="faq.html"])::before, +[data-theme="dark"] #search-results li:has(> a[href*="/faq.html"])::before { + color: #60a5fa; +} + +/* changelog: history / clock */ +#search-results li:has(> a[href^="changelog.html"])::before, +#search-results li:has(> a[href*="/changelog.html"])::before { + content: "\f1da"; + color: #a16207; } +/* install: download arrow */ +#search-results li:has(> a[href^="install.html"])::before, +#search-results li:has(> a[href*="/install.html"])::before { + content: "\f019"; + color: #16a34a; +} + +/* recipes: lightbulb */ +#search-results li:has(> a[href^="recipes.html"])::before, +#search-results li:has(> a[href*="/recipes.html"])::before { + content: "\f0eb"; + color: #eab308; +} + +/* migration: exchange arrows */ +#search-results li:has(> a[href^="migration.html"])::before, +#search-results li:has(> a[href*="/migration.html"])::before { + content: "\f0ec"; + color: #7c3aed; +} + +/* glossary: list with descriptions */ +#search-results li:has(> a[href^="glossary.html"])::before, +#search-results li:has(> a[href*="/glossary.html"])::before { + content: "\f022"; + color: #475569; +} + +/* API reference: gears. Different ahn individual API symbol ⚙️. */ +#search-results li:not(.kind-object):has(> a[href^="api.html"])::before, +#search-results li:not(.kind-object):has(> a[href*="/api.html"])::before, +#search-results li:not(.kind-object):has(> a[href^="api-overview.html"])::before, +#search-results li:not(.kind-object):has(> a[href*="/api-overview.html"])::before { + content: "\f085"; + color: var(--links-api); +} + +/* ---- Search-term highlighting ---- */ + span.highlighted { background-color: #fff3b0 !important; color: inherit !important; @@ -1521,6 +1765,23 @@ span.highlighted { /* API function / class signatures */ /* ================================================================== */ +.rst-content dl:not(.docutils) { + margin: 0px 0px 0px 0px !important; +} + +.rst-content dl.py + dl.py > dt { + margin-top: 28px !important; +} + +.data dd { + margin-bottom: 0px !important; +} + +.sig-paren { + padding-left: 2px; + padding-right: 2px; +} + /* The container/background bar */ .rst-content dl:not(.docutils) dt { background: var(--func-sig-bg) !important; @@ -1554,6 +1815,35 @@ span.highlighted { font-style: italic; } +/* ================================================================== */ +/* Glossary */ +/* ================================================================== */ + +/* Glossary
    inherits the function-signature + styling above (via dl:not(.docutils) dt). Strip it: glossary terms + are definitions, not API signatures. Selector is placed after the + API rules so it wins the cascade at equal specificity. */ +.rst-content dl.glossary > dt { + background: transparent !important; + border: none !important; + padding: 0 !important; + font-size: 1.1em !important; + font-weight: 600 !important; + color: var(--headings) !important; + margin: 1.6em 0 0.3em 0 !important; +} + +.rst-content dl.glossary > dd { + margin: 0 0 1em 1.5em !important; + background: transparent !important; + border: none !important; + padding: 0 !important; +} + +.rst-content dl.glossary > dd p { + margin: 0 0 0.5em 0 !important; +} + /* ================================================================== */ /* Genindex (Index page) */ /* ================================================================== */ @@ -1576,6 +1866,136 @@ span.highlighted { opacity: 0.8; } +.rst-content table.genindextable a:visited, +.rst-content table.genindextable a:visited strong, +.rst-content .genindex-jumpbox a:visited, +.rst-content .genindex-jumpbox a:visited strong { + color: var(--links) !important; +} + +.rst-content table.genindextable { + border-collapse: separate; + border-spacing: 3em 0; + margin-left: -3em; + margin-right: -3em; +} + +.rst-content:has(.genindex-jumpbox) h2 { + font-size: 1.3rem; + margin: 1.2em 0 0.4em 0; + padding-bottom: 0.2em; + border-bottom: 1px solid var(--border); + color: var(--text-muted); + font-weight: 600; +} + +/* ================================================================== */ +/* Funding page */ +/* ================================================================== */ + +.rst-content #how-to-fund dl.simple { + display: flex; + flex-direction: column; + gap: 0.7em; + margin: 1em 0 1.5em 0; +} + +.rst-content #how-to-fund dl.simple dt { + background: var(--surface-sunken) !important; + border: 1px solid var(--border) !important; + border-left: 3px solid var(--links) !important; + border-top: none !important; + border-radius: var(--border-radius); + padding: 0.6em 0.9em !important; + font-size: 1.02em !important; + font-weight: 600 !important; + margin: 0 !important; +} + +.rst-content #how-to-fund dl.simple dd { + margin: 0.2em 0 0 1.1em !important; + color: var(--text-muted); +} + +.rst-content #how-to-fund dl.simple dd p { + margin: 0 !important; +} + +.rst-content a.reference.external[href^="mailto:"]:visited, +.rst-content a.reference.external[href^="mailto:"]:visited * { + color: var(--links) !important; +} + +/* Benefits list: swap plain bullets for checkmarks — each item is + something the sponsor gets. Aesthetic match with the channel + cards above. */ +.rst-content #for-companies ul.simple { + list-style: none !important; + padding-left: 0; + margin: 0.8em 0 1.2em 0; +} + +.rst-content #for-companies ul.simple > li { + list-style: none !important; + list-style-type: none !important; + position: relative; + padding: 0.15em 0 0.15em 1.8em; + margin: 0 !important; +} + +.rst-content #for-companies ul.simple > li::before { + content: "\f00c"; /* FontAwesome checkmark */ + font-family: FontAwesome; + position: absolute; + left: 0; + top: 0.25em; + color: var(--headings); + font-size: 0.9em; +} + +/* Current sponsors section: give the logo row a subtle surface so + it reads as an intentional block rather than free-floating SVGs. */ +.rst-content table.sponsor-table { + background: var(--surface-sunken); + border: 1px solid var(--border); + border-radius: var(--border-radius); + margin: 1em auto 0.5em auto; + padding: 0.8em 1.2em; + display: table; +} + +.rst-content table.sponsor-table, +.rst-content table.sponsor-table tr, +.rst-content table.sponsor-table td { + background: transparent !important; + border: none !important; +} + +.sponsor-cta-wrap { + text-align: center; + margin-top: 1em; +} + +.rst-content a.sponsor-cta, +.rst-content a.sponsor-cta:visited { + font-size: 0.85rem; + color: var(--text-muted); + text-decoration: underline dashed; + text-decoration-color: var(--border); + text-underline-offset: 3px; + opacity: 0.8; + transition: color var(--theme-transition), + opacity var(--theme-transition), + text-decoration-color var(--theme-transition); +} + +.rst-content a.sponsor-cta:hover { + color: var(--links); + opacity: 1; + text-decoration: underline; + text-decoration-color: var(--links); +} + /* ================================================================== */ /* Syntax highlight — imported Monokai rules */ /* ================================================================== */ @@ -1589,7 +2009,7 @@ span.highlighted { [data-theme="dark"] .highlight .k { color: #66D9EF; } /* Keyword */ [data-theme="dark"] .highlight .l { color: #AE81FF; } /* Literal */ [data-theme="dark"] .highlight .n { color: #F8F8F2; } /* Name */ -[data-theme="dark"] .highlight .o { color: #FF4689; } /* Operator */ +[data-theme="dark"] .highlight .o { color: #F8F8F2; } /* Operator */ [data-theme="dark"] .highlight .x { color: #F8F8F2; } /* Other */ [data-theme="dark"] .highlight .p { color: #F8F8F2; } /* Punctuation */ [data-theme="dark"] .highlight .ch { color: #959077; } /* Comment.Hashbang */ @@ -1603,7 +2023,7 @@ span.highlighted { [data-theme="dark"] .highlight .ges { color: #F8F8F2; font-weight: bold; font-style: italic; } /* Generic.EmphStrong */ [data-theme="dark"] .highlight .gr { color: #F8F8F2; } /* Generic.Error */ [data-theme="dark"] .highlight .gh { color: #F8F8F2; } /* Generic.Heading */ -[data-theme="dark"] .highlight .gi { color: #A6E22E; } /* Generic.Inserted */ +[data-theme="dark"] .highlight .gi { color: #9ccfa5; } /* Generic.Inserted */ /* .go, .gp: overridden in the REPL theme sections above */ [data-theme="dark"] .highlight .gs { color: #F8F8F2; font-weight: bold; } /* Generic.Strong */ [data-theme="dark"] .highlight .gu { color: #959077; } /* Generic.Subheading */ @@ -1614,51 +2034,51 @@ span.highlighted { [data-theme="dark"] .highlight .kp { color: #66D9EF; } /* Keyword.Pseudo */ [data-theme="dark"] .highlight .kr { color: #66D9EF; } /* Keyword.Reserved */ [data-theme="dark"] .highlight .kt { color: #66D9EF; } /* Keyword.Type */ -[data-theme="dark"] .highlight .ld { color: #E6DB74; } /* Literal.Date */ -[data-theme="dark"] .highlight .m { color: #AE81FF; } /* Literal.Number */ -[data-theme="dark"] .highlight .s { color: #E6DB74; } /* Literal.String */ -[data-theme="dark"] .highlight .na { color: #A6E22E; } /* Name.Attribute */ +[data-theme="dark"] .highlight .ld { color: #d8c88a; } /* Literal.Date */ +[data-theme="dark"] .highlight .m { color: #b9a5e0; } /* Literal.Number */ +[data-theme="dark"] .highlight .s { color: #d8c88a; } /* Literal.String */ +[data-theme="dark"] .highlight .na { color: #9ccfa5; } /* Name.Attribute */ [data-theme="dark"] .highlight .nb { color: #F8F8F2; } /* Name.Builtin */ -[data-theme="dark"] .highlight .nc { color: #A6E22E; } /* Name.Class */ +[data-theme="dark"] .highlight .nc { color: #9ccfa5; } /* Name.Class */ [data-theme="dark"] .highlight .no { color: #66D9EF; } /* Name.Constant */ -[data-theme="dark"] .highlight .nd { color: #A6E22E; } /* Name.Decorator */ +[data-theme="dark"] .highlight .nd { color: #9ccfa5; } /* Name.Decorator */ [data-theme="dark"] .highlight .ni { color: #F8F8F2; } /* Name.Entity */ -[data-theme="dark"] .highlight .ne { color: #A6E22E; } /* Name.Exception */ -[data-theme="dark"] .highlight .nf { color: #A6E22E; } /* Name.Function */ +[data-theme="dark"] .highlight .ne { color: #9ccfa5; } /* Name.Exception */ +[data-theme="dark"] .highlight .nf { color: #9ccfa5; } /* Name.Function */ [data-theme="dark"] .highlight .nl { color: #F8F8F2; } /* Name.Label */ [data-theme="dark"] .highlight .nn { color: #F8F8F2; } /* Name.Namespace */ -[data-theme="dark"] .highlight .nx { color: #A6E22E; } /* Name.Other */ +[data-theme="dark"] .highlight .nx { color: #9ccfa5; } /* Name.Other */ [data-theme="dark"] .highlight .py { color: #F8F8F2; } /* Name.Property */ [data-theme="dark"] .highlight .nt { color: #FF4689; } /* Name.Tag */ [data-theme="dark"] .highlight .nv { color: #F8F8F2; } /* Name.Variable */ -[data-theme="dark"] .highlight .ow { color: #FF4689; } /* Operator.Word */ +[data-theme="dark"] .highlight .ow { color: #66D9EF; } /* Operator.Word */ [data-theme="dark"] .highlight .pm { color: #F8F8F2; } /* Punctuation.Marker */ [data-theme="dark"] .highlight .w { color: #F8F8F2; } /* Text.Whitespace */ -[data-theme="dark"] .highlight .mb { color: #AE81FF; } /* Literal.Number.Bin */ -[data-theme="dark"] .highlight .mf { color: #AE81FF; } /* Literal.Number.Float */ -[data-theme="dark"] .highlight .mh { color: #AE81FF; } /* Literal.Number.Hex */ -[data-theme="dark"] .highlight .mi { color: #AE81FF; } /* Literal.Number.Integer */ -[data-theme="dark"] .highlight .mo { color: #AE81FF; } /* Literal.Number.Oct */ -[data-theme="dark"] .highlight .sa { color: #E6DB74; } /* Literal.String.Affix */ -[data-theme="dark"] .highlight .sb { color: #E6DB74; } /* Literal.String.Backtick */ -[data-theme="dark"] .highlight .sc { color: #E6DB74; } /* Literal.String.Char */ -[data-theme="dark"] .highlight .dl { color: #E6DB74; } /* Literal.String.Delimiter */ -[data-theme="dark"] .highlight .sd { color: #E6DB74; } /* Literal.String.Doc */ -[data-theme="dark"] .highlight .s2 { color: #E6DB74; } /* Literal.String.Double */ +[data-theme="dark"] .highlight .mb { color: #b9a5e0; } /* Literal.Number.Bin */ +[data-theme="dark"] .highlight .mf { color: #b9a5e0; } /* Literal.Number.Float */ +[data-theme="dark"] .highlight .mh { color: #b9a5e0; } /* Literal.Number.Hex */ +[data-theme="dark"] .highlight .mi { color: #b9a5e0; } /* Literal.Number.Integer */ +[data-theme="dark"] .highlight .mo { color: #b9a5e0; } /* Literal.Number.Oct */ +[data-theme="dark"] .highlight .sa { color: #d8c88a; } /* Literal.String.Affix */ +[data-theme="dark"] .highlight .sb { color: #d8c88a; } /* Literal.String.Backtick */ +[data-theme="dark"] .highlight .sc { color: #d8c88a; } /* Literal.String.Char */ +[data-theme="dark"] .highlight .dl { color: #d8c88a; } /* Literal.String.Delimiter */ +[data-theme="dark"] .highlight .sd { color: #d8c88a; } /* Literal.String.Doc */ +[data-theme="dark"] .highlight .s2 { color: #d8c88a; } /* Literal.String.Double */ [data-theme="dark"] .highlight .se { color: #AE81FF; } /* Literal.String.Escape */ -[data-theme="dark"] .highlight .sh { color: #E6DB74; } /* Literal.String.Heredoc */ -[data-theme="dark"] .highlight .si { color: #E6DB74; } /* Literal.String.Interpol */ -[data-theme="dark"] .highlight .sx { color: #E6DB74; } /* Literal.String.Other */ -[data-theme="dark"] .highlight .sr { color: #E6DB74; } /* Literal.String.Regex */ -[data-theme="dark"] .highlight .s1 { color: #E6DB74; } /* Literal.String.Single */ -[data-theme="dark"] .highlight .ss { color: #E6DB74; } /* Literal.String.Symbol */ +[data-theme="dark"] .highlight .sh { color: #d8c88a; } /* Literal.String.Heredoc */ +[data-theme="dark"] .highlight .si { color: #d8c88a; } /* Literal.String.Interpol */ +[data-theme="dark"] .highlight .sx { color: #d8c88a; } /* Literal.String.Other */ +[data-theme="dark"] .highlight .sr { color: #d8c88a; } /* Literal.String.Regex */ +[data-theme="dark"] .highlight .s1 { color: #d8c88a; } /* Literal.String.Single */ +[data-theme="dark"] .highlight .ss { color: #d8c88a; } /* Literal.String.Symbol */ [data-theme="dark"] .highlight .bp { color: #F8F8F2; } /* Name.Builtin.Pseudo */ -[data-theme="dark"] .highlight .fm { color: #A6E22E; } /* Name.Function.Magic */ +[data-theme="dark"] .highlight .fm { color: #9ccfa5; } /* Name.Function.Magic */ [data-theme="dark"] .highlight .vc { color: #F8F8F2; } /* Name.Variable.Class */ [data-theme="dark"] .highlight .vg { color: #F8F8F2; } /* Name.Variable.Global */ [data-theme="dark"] .highlight .vi { color: #F8F8F2; } /* Name.Variable.Instance */ [data-theme="dark"] .highlight .vm { color: #F8F8F2; } /* Name.Variable.Magic */ -[data-theme="dark"] .highlight .il { color: #AE81FF; } /* Literal.Number.Integer.Long */ +[data-theme="dark"] .highlight .il { color: #b9a5e0; } /* Literal.Number.Integer.Long */ /* ================================================================== */ /* Code blocks */ @@ -1705,8 +2125,16 @@ div[class^="highlight-"] { } /* background */ -[data-theme="light"] .highlight { background: #f8f8f8 !important; } -[data-theme="dark"] .highlight { background: #272727 !important; } +[data-theme="light"] .highlight { + background: #f8f8f8 !important; + border: 1px solid var(--border); + border-radius: var(--border-radius); +} +[data-theme="dark"] .highlight { + background: #262626 !important; + border: 1px solid #3a3a3a; + border-radius: var(--border-radius); +} .highlight .k { font-weight:normal; font-style: normal !important;} /* (, [, ], )*/ .highlight .p { font-weight:normal; } /* (, [, ], )*/ @@ -1716,26 +2144,47 @@ div[class^="highlight-"] { /* ----------------- REPL light theme ---------------- */ [data-theme="light"] .highlight-pycon .pycon-number { color: #204A87; } -[data-theme="light"] .highlight-pycon .pycon-string { color: brown; } +[data-theme="light"] .highlight-pycon .pycon-string { color: #8a5a2d; } +[data-theme="light"] .highlight .s, +[data-theme="light"] .highlight .s1, +[data-theme="light"] .highlight .s2, +[data-theme="light"] .highlight .sa, +[data-theme="light"] .highlight .sb, +[data-theme="light"] .highlight .sc, +[data-theme="light"] .highlight .dl, +[data-theme="light"] .highlight .sd, +[data-theme="light"] .highlight .sh, +[data-theme="light"] .highlight .si, +[data-theme="light"] .highlight .sx, +[data-theme="light"] .highlight .sr, +[data-theme="light"] .highlight .ss, +[data-theme="light"] .highlight .se { color: #6b8e4e; } /* strings: muted olive */ [data-theme="light"] .highlight-pycon .pycon-field { color: #4d4d4d; background: #eff1f3} -[data-theme="light"] .highlight .gp { color: #204A87; } -[data-theme="light"] .highlight .go { color: #666666; } +[data-theme="light"] .highlight .gp { color: #8a8a8a; } /* >>> prompt: muted */ +[data-theme="light"] .highlight .c, +[data-theme="light"] .highlight .ch, +[data-theme="light"] .highlight .cm, +[data-theme="light"] .highlight .cp, +[data-theme="light"] .highlight .cpf, +[data-theme="light"] .highlight .c1, +[data-theme="light"] .highlight .cs { color: #6a737d; font-style: normal; } /* comments: muted gray */ +[data-theme="light"] .highlight .go { color: #8a8a8a; } /* output dimmer than code */ /* ----------------- REPL dark theme ---------------- */ -[data-theme="dark"] .highlight-pycon .pycon-number { color: #ae81ff; } -[data-theme="dark"] .highlight-pycon .pycon-string { color: #e6db74; } -[data-theme="dark"] .highlight-pycon .pycon-field { color: #a6e22e; } +[data-theme="dark"] .highlight-pycon .pycon-number { color: #b9a5e0; } +[data-theme="dark"] .highlight-pycon .pycon-string { color: #d8c88a; } +[data-theme="dark"] .highlight-pycon .pycon-field { color: #9ccfa5; } [data-theme="dark"] .highlight .gp { color: #888888; } /* >>> prompt */ -[data-theme="dark"] .highlight .go { color: #7a9aaa; } /* output */ +[data-theme="dark"] .highlight .go { color: #6c7a88; } /* output dimmer than code */ /* ----------------- syntax highlight ---------------- */ -[data-theme="dark"] .highlight .k { color: #ff3a6e} /* numbers */ -[data-theme="dark"] .highlight .ow { color: #ff3a6e} /* numbers */ -[data-theme="dark"] .highlight .kn { color: #ff3a6e} /* numbers */ +[data-theme="dark"] .highlight .k { color: #f56b8b} /* keyword */ +[data-theme="dark"] .highlight .kn { color: #f56b8b} /* keyword.namespace */ /* --------------------------------------------------- */ [data-theme="light"] .highlight .mi { color: #204A87} /* numbers */ [data-theme="light"] .highlight .nf { color: #A31515} /* functions */ +[data-theme="light"] .highlight .o { color: inherit; font-weight: normal; } /* operators */ diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html index ba5abacdb6..2fb75593c5 100644 --- a/docs/_templates/footer.html +++ b/docs/_templates/footer.html @@ -10,16 +10,17 @@
    {%- endif %} -
    -
    {%- block contentinfo %} {%- endblock %} diff --git a/docs/adoption.rst b/docs/adoption.rst index 1790af7ff0..ce017ec1c0 100644 --- a/docs/adoption.rst +++ b/docs/adoption.rst @@ -22,31 +22,31 @@ Infrastructure / automation - Description - Stars - psutil usage - * - |homeassistant-logo| `Home Assistant `__ + * - `Home Assistant `__ - Open source home automation platform - |homeassistant-stars| - system monitor integration - * - |ansible-logo| `Ansible `__ + * - `Ansible `__ - IT automation platform - |ansible-stars| - system fact gathering - * - |airflow-logo| `Apache Airflow `__ + * - `Apache Airflow `__ - Workflow orchestration platform - |airflow-stars| - process supervisor, unit testing - * - |celery-logo| `Celery `__ + * - `Celery `__ - Distributed task queue - |celery-stars| - worker process monitoring, memleak detection - * - |salt-logo| `Salt `__ + * - `Salt `__ - Infrastructure automation at scale - |salt-stars| - deep system data collection (grains) - * - |dask-logo| `Dask `__ + * - `Dask `__ - Parallel computing with task scheduling - |dask-stars| - `metrics dashboard `__, profiling - * - |ajenti-logo| `Ajenti `__ + * - `Ajenti `__ - Web-based server administration panel - |ajenti-stars| - monitoring plugins, deep integration @@ -62,19 +62,19 @@ AI / machine learning - Description - Stars - psutil usage - * - |tensorflow-logo| `TensorFlow `__ + * - `TensorFlow `__ - Open source machine learning framework by Google - |tensorflow-stars| - unit tests - * - |pytorch-logo| `PyTorch `__ + * - `PyTorch `__ - Tensors and dynamic neural networks with GPU acceleration - |pytorch-stars| - benchmark scripts - * - |ray-logo| `Ray `__ + * - `Ray `__ - AI compute engine with distributed runtime - |ray-stars| - metrics dashboard - * - |mlflow-logo| `MLflow `__ + * - `MLflow `__ - AI/ML engineering platform - |mlflow-stars| - deep system monitoring integration @@ -90,19 +90,19 @@ Developer tools - Description - Stars - psutil usage - * - |sentry-logo| `Sentry `__ + * - `Sentry `__ - Error tracking and performance monitoring - |sentry-stars| - send telemetry metrics - * - |locust-logo| `Locust `__ + * - `Locust `__ - Scalable load testing in Python - |locust-stars| - monitoring of the Locust process - * - |spyder-logo| `Spyder `__ + * - `Spyder `__ - Scientific Python IDE - |spyder-stars| - deep integration, UI stats, process management - * - |psleak-logo| `psleak `__ + * - `psleak `__ - Test framework to detect memory leaks in Python C extensions - |psleak-stars| - heap process memory (:func:`heap_info()`) @@ -118,39 +118,39 @@ System monitoring - Description - Stars - psutil usage - * - |glances-logo| `Glances `__ + * - `Glances `__ - System monitoring tool (top/htop alternative) - |glances-stars| - core dependency for all metrics - * - |bpytop-logo| `bpytop `__ + * - `bpytop `__ - Terminal resource monitor - |bpytop-stars| - core dependency for all metrics - * - |auto-cpufreq-logo| `auto-cpufreq `__ + * - `auto-cpufreq `__ - Automatic CPU speed and power optimizer for Linux - |auto-cpufreq-stars| - core dependency for CPU monitoring - * - |grr-logo| `GRR `__ + * - `GRR `__ - Remote live forensics by Google - |grr-stars| - core dependency for system data collection - * - |stui-logo| `s-tui `__ + * - `s-tui `__ - Terminal CPU stress and monitoring utility - |stui-stars| - core dependency for metrics - * - |asitop-logo| `asitop `__ + * - `asitop `__ - Apple Silicon performance monitoring CLI - |asitop-stars| - core dependency for system metrics - * - |psdash-logo| `psdash `__ + * - `psdash `__ - Web dashboard using psutil and Flask - |psdash-stars| - core dependency for all metrics - * - |dd-agent-logo| `dd-agent `__ + * - `dd-agent `__ - Original monitoring agent by Datadog - |dd-agent-stars| - system metrics collection - * - |ddtrace-logo| `dd-trace-py `__ + * - `dd-trace-py `__ - Python tracing and profiling library - |ddtrace-stars| - system metrics collection @@ -206,35 +206,6 @@ How this list was compiled .. |stui-stars| image:: https://img.shields.io/github/stars/amanusk/s-tui.svg?style=social&label=%20 .. |tensorflow-stars| image:: https://img.shields.io/github/stars/tensorflow/tensorflow.svg?style=social&label=%20 -.. Logo images -.. ============================================================================ - -.. |airflow-logo| image:: https://github.com/apache.png?s=28 :height: 28 -.. |ajenti-logo| image:: https://github.com/ajenti.png?s=28 :height: 28 -.. |ansible-logo| image:: https://github.com/ansible.png?s=28 :height: 28 -.. |asitop-logo| image:: https://github.com/tlkh.png?s=28 :height: 28 -.. |auto-cpufreq-logo| image:: https://github.com/AdnanHodzic.png?s=28 :height: 28 -.. |bpytop-logo| image:: https://github.com/aristocratos.png?s=28 :height: 28 -.. |celery-logo| image:: https://github.com/celery.png?s=28 :height: 28 -.. |dask-logo| image:: https://github.com/dask.png?s=28 :height: 28 -.. |ddtrace-logo| image:: https://github.com/DataDog.png?s=28 :height: 28 -.. |dd-agent-logo| image:: https://github.com/DataDog.png?s=28 :height: 28 -.. |glances-logo| image:: https://github.com/nicolargo.png?s=28 :height: 28 -.. |grr-logo| image:: https://github.com/google.png?s=28 :height: 28 -.. |homeassistant-logo| image:: https://github.com/home-assistant.png?s=28 :height: 28 -.. |locust-logo| image:: https://github.com/locustio.png?s=28 :height: 28 -.. |mlflow-logo| image:: https://github.com/mlflow.png?s=28 :height: 28 -.. |osquery-logo| image:: https://github.com/osquery.png?s=28 :height: 28 -.. |psdash-logo| image:: https://github.com/Jahaja.png?s=28 :height: 28 -.. |psleak-logo| image:: https://github.com/giampaolo.png?s=28 :height: 28 -.. |pytorch-logo| image:: https://github.com/pytorch.png?s=28 :height: 28 -.. |ray-logo| image:: https://github.com/ray-project.png?s=28 :height: 28 -.. |salt-logo| image:: https://github.com/saltstack.png?s=28 :height: 28 -.. |sentry-logo| image:: https://github.com/getsentry.png?s=28 :height: 28 -.. |spyder-logo| image:: https://github.com/spyder-ide.png?s=28 :height: 28 -.. |stui-logo| image:: https://github.com/amanusk.png?s=28 :height: 28 -.. |tensorflow-logo| image:: https://github.com/tensorflow.png?s=28 :height: 28 - .. --- Notes .. Stars shield: .. https://shields.io/badges/git-hub-repo-stars diff --git a/docs/conf.py b/docs/conf.py index 8aa9db9b2f..0a62529d98 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -77,7 +77,7 @@ html_title = PROJECT_NAME html_favicon = "_static/images/favicon.svg" -html_last_updated_fmt = "%b %d, %Y" # shown in the footer +html_last_updated_fmt = "%Y-%m-%d" # ISO date shown in the footer # Sidebar shows method() instead of Class.method() toc_object_entries_show_parents = "hide" diff --git a/docs/glossary.rst b/docs/glossary.rst index 22c29ab504..7e5ade8162 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -139,8 +139,8 @@ Glossary An I/O scheduling priority that controls how much disk bandwidth a process receives. On Linux three scheduling classes are supported: - ``IOPRIO_CLASS_RT`` (real-time), ``IOPRIO_CLASS_BE`` (best-effort, - the default), and ``IOPRIO_CLASS_IDLE``. See + :data:`IOPRIO_CLASS_RT` (real-time), :data:`IOPRIO_CLASS_BE` + (best-effort, the default), and :data:`IOPRIO_CLASS_IDLE`. See :meth:`Process.ionice`. logical CPU diff --git a/docs/index.rst b/docs/index.rst index 09325f68b6..c7b264a53c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,15 +40,17 @@ Assistant, Ansible, and Celery. .. raw:: html
    - Runs on: - Linux - Windows - macOS - FreeBSD - OpenBSD - NetBSD - Solaris - AIX + Runs on +
    .. ============================================================================ @@ -108,12 +110,17 @@ Assistant, Ansible, and Celery.
    -
    Cross-platform
    +
    Platform portability

    One API for all platforms. Psutil primary goal is to abstract OS differences, so your monitoring code runs unchanged everywhere.

    + + .. ============================================================================ .. Sponsors .. ============================================================================ diff --git a/docs/migration.rst b/docs/migration.rst index 0c6d476853..eeb3d81fb9 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -145,7 +145,7 @@ Status and connection fields are now enums Because both are :class:`enum.StrEnum` subclasses they compare equal to their string values, so existing comparisons like ``p.status() == psutil.STATUS_RUNNING`` continue to work unchanged. Code -inspecting ``repr()`` or ``type()`` may need updating. +inspecting :func:`repr` or :class:`type` may need updating. memory_full_info() is deprecated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -181,7 +181,7 @@ names: It also makes it easy to pass all or a subset of attributes. ``process_iter(attrs=[])`` (empty list meaning "all") is now deprecated; use -``Process.attrs`` instead: +:attr:`Process.attrs` instead: .. code-block:: python